From 09b7a23f51267ce36972c5466e4adfa9bd747b76 Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Tue, 26 May 2026 20:52:31 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9A=A1=EF=B8=8F(frontend)=20switch=20Materia?= =?UTF-8?q?l=20icons=20strategy=20to=20per-icon=20SVG=20imports?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Material icons (and Symbols, the most recent ones from Google Material) were initially adopted to align with the UI kit. Before that, we relied on Remixicon, which loads only the icons used and totals 40ko for the whole library. To benefit from Material icons, the implementation matched the UI kit, which uses two fonts: 150ko and 750ko. Unlike the UI kit, the fonts were preloaded to prevent icon render blinks. On products using UI kit, the fonts are loaded the first time an icon renders, and because they are heavy, the icon swap is visible to the user. Icons are replaced by their text label in the meantime, which also caused screen-reader vocalization or i18n issues that had to be mitigated. Preloading the fonts was a quick win that worked fine on high-speed connections where fonts get loaded fast then cached. However, while optimizing LCP for users on poor connections (a separate concern that already affects videoconference joining), preloading proved harmful: it stole network bandwidth at the most critical moment, when the main JS chunk was loading. Switch to a frugal approach: import only the SVG icons actually needed from Material, treated as components so they are tree-shaken and customizable (color, etc.). Icons do not change often, so the DX remains good. The UI kit also supports custom icons not available in the font, so this approach lets us define them and works like a charm. Removal of Remixicon is planned in a follow-up PR. It save user loading almost 1Mo of assets for few icons. --- src/frontend/index.html | 16 - src/frontend/package-lock.json | 447 +++++++++++++++++- src/frontend/package.json | 3 +- src/frontend/src/assets/icons/article.svg | 1 + .../src/assets/icons/chevron_forward.svg | 1 + .../src/assets/icons/cloud_download.svg | 1 + src/frontend/src/assets/icons/language.svg | 1 + src/frontend/src/assets/icons/login.svg | 1 + src/frontend/src/assets/icons/mail.svg | 1 + .../src/assets/icons/mode_standby.svg | 1 + .../src/assets/icons/person_raised_hand.svg | 1 + .../src/assets/icons/screen_record.svg | 1 + .../src/assets/icons/speech_to_text.svg | 1 + .../recording/components/LoginPrompt.tsx | 2 +- .../components/RecordingStatusIcon.tsx | 4 +- .../recording/components/RequestRecording.tsx | 2 +- .../recording/components/RowWrapper.tsx | 1 - .../rooms/livekit/components/Tools.tsx | 6 +- .../components/SettingsDialogExtended.tsx | 2 +- src/frontend/src/main.tsx | 3 - src/frontend/src/primitives/Icon.tsx | 92 ++-- src/frontend/src/styles/index.css | 33 -- src/frontend/vite.config.ts | 6 + 23 files changed, 501 insertions(+), 126 deletions(-) create mode 100644 src/frontend/src/assets/icons/article.svg create mode 100644 src/frontend/src/assets/icons/chevron_forward.svg create mode 100644 src/frontend/src/assets/icons/cloud_download.svg create mode 100644 src/frontend/src/assets/icons/language.svg create mode 100644 src/frontend/src/assets/icons/login.svg create mode 100644 src/frontend/src/assets/icons/mail.svg create mode 100644 src/frontend/src/assets/icons/mode_standby.svg create mode 100644 src/frontend/src/assets/icons/person_raised_hand.svg create mode 100644 src/frontend/src/assets/icons/screen_record.svg create mode 100644 src/frontend/src/assets/icons/speech_to_text.svg diff --git a/src/frontend/index.html b/src/frontend/index.html index f4720d6d..d6a3eff2 100644 --- a/src/frontend/index.html +++ b/src/frontend/index.html @@ -6,22 +6,6 @@ - - - - %VITE_APP_TITLE% diff --git a/src/frontend/package-lock.json b/src/frontend/package-lock.json index 8f6d7ff5..9fee6bd1 100644 --- a/src/frontend/package-lock.json +++ b/src/frontend/package-lock.json @@ -10,8 +10,6 @@ "dependencies": { "@fontsource-variable/atkinson-hyperlegible-next": "5.2.6", "@fontsource-variable/lexend": "5.2.11", - "@fontsource-variable/material-symbols-outlined": "5.2.34", - "@fontsource/material-icons-outlined": "5.2.6", "@fontsource/opendyslexic": "5.2.5", "@livekit/components-react": "2.9.19", "@livekit/components-styles": "1.2.0", @@ -61,6 +59,7 @@ "rollup-plugin-visualizer": "7.0.1", "typescript": "5.8.3", "vite": "7.3.2", + "vite-plugin-svgr": "5.2.0", "vite-tsconfig-paths": "6.1.1" } }, @@ -1012,24 +1011,6 @@ "url": "https://github.com/sponsors/ayuhito" } }, - "node_modules/@fontsource-variable/material-symbols-outlined": { - "version": "5.2.34", - "resolved": "https://registry.npmjs.org/@fontsource-variable/material-symbols-outlined/-/material-symbols-outlined-5.2.34.tgz", - "integrity": "sha512-FfJVoKdYlxvIHflVZlrevf6e1WmbJGeAdbcLmOmkftoP2Du0qrRD/SmNchIXijsKXF7j3z1m++e92Qi6/U+jRA==", - "license": "OFL-1.1", - "funding": { - "url": "https://github.com/sponsors/ayuhito" - } - }, - "node_modules/@fontsource/material-icons-outlined": { - "version": "5.2.6", - "resolved": "https://registry.npmjs.org/@fontsource/material-icons-outlined/-/material-icons-outlined-5.2.6.tgz", - "integrity": "sha512-99XKAkwnCg0s0/ywax+o3m01HSNM5gGzSBw+WnlWG2+WY3wOjcN+wXMfm4zP37Yme7Yze2DvKxF78tHWOrlwFw==", - "license": "OFL-1.1", - "funding": { - "url": "https://github.com/sponsors/ayuhito" - } - }, "node_modules/@fontsource/opendyslexic": { "version": "5.2.5", "resolved": "https://registry.npmjs.org/@fontsource/opendyslexic/-/opendyslexic-5.2.5.tgz", @@ -4106,6 +4087,42 @@ "dev": true, "license": "MIT" }, + "node_modules/@rollup/pluginutils": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", + "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.59.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", @@ -4456,6 +4473,231 @@ "win32" ] }, + "node_modules/@svgr/babel-plugin-add-jsx-attribute": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz", + "integrity": "sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-attribute": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-8.0.0.tgz", + "integrity": "sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-empty-expression": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-8.0.0.tgz", + "integrity": "sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-replace-jsx-attribute-value": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-8.0.0.tgz", + "integrity": "sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-svg-dynamic-title": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-8.0.0.tgz", + "integrity": "sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-svg-em-dimensions": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-8.0.0.tgz", + "integrity": "sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-transform-react-native-svg": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-8.1.0.tgz", + "integrity": "sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-transform-svg-component": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-8.0.0.tgz", + "integrity": "sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-preset": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-8.1.0.tgz", + "integrity": "sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@svgr/babel-plugin-add-jsx-attribute": "8.0.0", + "@svgr/babel-plugin-remove-jsx-attribute": "8.0.0", + "@svgr/babel-plugin-remove-jsx-empty-expression": "8.0.0", + "@svgr/babel-plugin-replace-jsx-attribute-value": "8.0.0", + "@svgr/babel-plugin-svg-dynamic-title": "8.0.0", + "@svgr/babel-plugin-svg-em-dimensions": "8.0.0", + "@svgr/babel-plugin-transform-react-native-svg": "8.1.0", + "@svgr/babel-plugin-transform-svg-component": "8.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/core": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz", + "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.21.3", + "@svgr/babel-preset": "8.1.0", + "camelcase": "^6.2.0", + "cosmiconfig": "^8.1.3", + "snake-case": "^3.0.4" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/hast-util-to-babel-ast": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-8.0.0.tgz", + "integrity": "sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.21.3", + "entities": "^4.4.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/plugin-jsx": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-8.1.0.tgz", + "integrity": "sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.21.3", + "@svgr/babel-preset": "8.1.0", + "@svgr/hast-util-to-babel-ast": "8.0.0", + "svg-parser": "^2.0.4" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@svgr/core": "*" + } + }, "node_modules/@swc/helpers": { "version": "0.5.13", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.13.tgz", @@ -5880,6 +6122,19 @@ "node": ">=6" } }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001774", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001774.tgz", @@ -6190,6 +6445,33 @@ "url": "https://opencollective.com/express" } }, + "node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/crisp-sdk-web": { "version": "1.0.27", "resolved": "https://registry.npmjs.org/crisp-sdk-web/-/crisp-sdk-web-1.0.27.tgz", @@ -6543,6 +6825,17 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -6621,6 +6914,16 @@ "resolved": "https://registry.npmjs.org/eol/-/eol-0.9.1.tgz", "integrity": "sha512-Ds/TEoZjwggRoz/Q2O7SE3i4Jm66mqTDfmdHdq/7DKVk3bro9Q8h6WdXKdPqFLMoqxrDK5SVRzHVPOS6uuGtrg==" }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, "node_modules/es-abstract": { "version": "1.23.3", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", @@ -8250,6 +8553,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, "node_modules/is-bigint": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", @@ -8643,6 +8953,13 @@ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -9032,6 +9349,13 @@ "url": "https://github.com/sponsors/antonk52" } }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, "node_modules/livekit-client": { "version": "2.17.1", "resolved": "https://registry.npmjs.org/livekit-client/-/livekit-client-2.17.1.tgz", @@ -9138,6 +9462,16 @@ "loose-envify": "cli.js" } }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -9352,6 +9686,17 @@ "node": ">= 0.6" } }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, "node_modules/node-eval": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/node-eval/-/node-eval-2.0.0.tgz", @@ -9610,6 +9955,25 @@ "node": ">=6" } }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/parse5": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", @@ -9702,6 +10066,16 @@ "url": "https://opencollective.com/express" } }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -10928,6 +11302,17 @@ "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, + "node_modules/snake-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", + "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, "node_modules/sort-keys": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-5.1.0.tgz", @@ -11115,6 +11500,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/svg-parser": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", + "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==", + "dev": true, + "license": "MIT" + }, "node_modules/symlink-or-copy": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/symlink-or-copy/-/symlink-or-copy-1.3.1.tgz", @@ -11823,6 +12215,21 @@ } } }, + "node_modules/vite-plugin-svgr": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/vite-plugin-svgr/-/vite-plugin-svgr-5.2.0.tgz", + "integrity": "sha512-qj2eAKF8C6PZWemVTvQA0xgQIcP1hHU6Buh7fl6BhvayWwnuxE+z417miKxeDvRWbDrupQ1oK99hfxElopJ3sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.3.0", + "@svgr/core": "^8.1.0", + "@svgr/plugin-jsx": "^8.1.0" + }, + "peerDependencies": { + "vite": ">=3.0.0" + } + }, "node_modules/vite-tsconfig-paths": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-6.1.1.tgz", diff --git a/src/frontend/package.json b/src/frontend/package.json index 993753f6..d3f8033a 100644 --- a/src/frontend/package.json +++ b/src/frontend/package.json @@ -16,8 +16,6 @@ "dependencies": { "@fontsource-variable/atkinson-hyperlegible-next": "5.2.6", "@fontsource-variable/lexend": "5.2.11", - "@fontsource-variable/material-symbols-outlined": "5.2.34", - "@fontsource/material-icons-outlined": "5.2.6", "@fontsource/opendyslexic": "5.2.5", "@livekit/components-react": "2.9.19", "@livekit/components-styles": "1.2.0", @@ -67,6 +65,7 @@ "rollup-plugin-visualizer": "7.0.1", "typescript": "5.8.3", "vite": "7.3.2", + "vite-plugin-svgr": "5.2.0", "vite-tsconfig-paths": "6.1.1" } } diff --git a/src/frontend/src/assets/icons/article.svg b/src/frontend/src/assets/icons/article.svg new file mode 100644 index 00000000..9cd25255 --- /dev/null +++ b/src/frontend/src/assets/icons/article.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/frontend/src/assets/icons/chevron_forward.svg b/src/frontend/src/assets/icons/chevron_forward.svg new file mode 100644 index 00000000..976b2ae4 --- /dev/null +++ b/src/frontend/src/assets/icons/chevron_forward.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/frontend/src/assets/icons/cloud_download.svg b/src/frontend/src/assets/icons/cloud_download.svg new file mode 100644 index 00000000..d45089bc --- /dev/null +++ b/src/frontend/src/assets/icons/cloud_download.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/frontend/src/assets/icons/language.svg b/src/frontend/src/assets/icons/language.svg new file mode 100644 index 00000000..f851a272 --- /dev/null +++ b/src/frontend/src/assets/icons/language.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/frontend/src/assets/icons/login.svg b/src/frontend/src/assets/icons/login.svg new file mode 100644 index 00000000..ddc929be --- /dev/null +++ b/src/frontend/src/assets/icons/login.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/frontend/src/assets/icons/mail.svg b/src/frontend/src/assets/icons/mail.svg new file mode 100644 index 00000000..82e6eb1e --- /dev/null +++ b/src/frontend/src/assets/icons/mail.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/frontend/src/assets/icons/mode_standby.svg b/src/frontend/src/assets/icons/mode_standby.svg new file mode 100644 index 00000000..f9c74c99 --- /dev/null +++ b/src/frontend/src/assets/icons/mode_standby.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/frontend/src/assets/icons/person_raised_hand.svg b/src/frontend/src/assets/icons/person_raised_hand.svg new file mode 100644 index 00000000..4b89fe3a --- /dev/null +++ b/src/frontend/src/assets/icons/person_raised_hand.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/frontend/src/assets/icons/screen_record.svg b/src/frontend/src/assets/icons/screen_record.svg new file mode 100644 index 00000000..1934b235 --- /dev/null +++ b/src/frontend/src/assets/icons/screen_record.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/frontend/src/assets/icons/speech_to_text.svg b/src/frontend/src/assets/icons/speech_to_text.svg new file mode 100644 index 00000000..ef9a538a --- /dev/null +++ b/src/frontend/src/assets/icons/speech_to_text.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/frontend/src/features/recording/components/LoginPrompt.tsx b/src/frontend/src/features/recording/components/LoginPrompt.tsx index e46740ba..cdc7edf0 100644 --- a/src/frontend/src/features/recording/components/LoginPrompt.tsx +++ b/src/frontend/src/features/recording/components/LoginPrompt.tsx @@ -24,7 +24,7 @@ export const LoginPrompt = ({ heading, body }: LoginPromptProps) => { })} > - + {heading} diff --git a/src/frontend/src/features/recording/components/RecordingStatusIcon.tsx b/src/frontend/src/features/recording/components/RecordingStatusIcon.tsx index 7bf65cb9..bc0f109f 100644 --- a/src/frontend/src/features/recording/components/RecordingStatusIcon.tsx +++ b/src/frontend/src/features/recording/components/RecordingStatusIcon.tsx @@ -15,8 +15,8 @@ export const RecordingStatusIcon = ({ } if (isTranscriptActive) { - return + return } - return + return } diff --git a/src/frontend/src/features/recording/components/RequestRecording.tsx b/src/frontend/src/features/recording/components/RequestRecording.tsx index 8af710bd..7135355d 100644 --- a/src/frontend/src/features/recording/components/RequestRecording.tsx +++ b/src/frontend/src/features/recording/components/RequestRecording.tsx @@ -59,7 +59,7 @@ export const RequestRecording = ({ })} > - + {heading} diff --git a/src/frontend/src/features/recording/components/RowWrapper.tsx b/src/frontend/src/features/recording/components/RowWrapper.tsx index f94140a0..6d7038c1 100644 --- a/src/frontend/src/features/recording/components/RowWrapper.tsx +++ b/src/frontend/src/features/recording/components/RowWrapper.tsx @@ -45,7 +45,6 @@ export const RowWrapper = ({ paddingInline: '0.25rem', })} > - {/* fixme - doesn't handle properly material-symbols */}
- +
) @@ -168,7 +168,7 @@ export const Tools = () => { {isTranscriptEnabled && ( } + icon={} title={t('tools.transcript.title')} description={t('tools.transcript.body')} onPress={() => openTranscript()} @@ -176,7 +176,7 @@ export const Tools = () => { )} {isScreenRecordingEnabled && ( } + icon={} title={t('tools.screenRecording.title')} description={t('tools.screenRecording.body')} onPress={() => openScreenRecording()} diff --git a/src/frontend/src/features/settings/components/SettingsDialogExtended.tsx b/src/frontend/src/features/settings/components/SettingsDialogExtended.tsx index ed4a782b..5f6f6cca 100644 --- a/src/frontend/src/features/settings/components/SettingsDialogExtended.tsx +++ b/src/frontend/src/features/settings/components/SettingsDialogExtended.tsx @@ -116,7 +116,7 @@ export const SettingsDialogExtended = (props: SettingsDialogExtended) => { {isAdminOrOwner && ( - + {isWideScreen && t(`tabs.${SettingsDialogExtendedKey.TRANSCRIPTION}`)} diff --git a/src/frontend/src/main.tsx b/src/frontend/src/main.tsx index ee1483ae..ed319314 100644 --- a/src/frontend/src/main.tsx +++ b/src/frontend/src/main.tsx @@ -2,9 +2,6 @@ import React from 'react' import ReactDOM from 'react-dom/client' import App from './App.tsx' -// Supports weights 100-700 -import '@fontsource-variable/material-symbols-outlined' - ReactDOM.createRoot(document.getElementById('root')!).render( diff --git a/src/frontend/src/primitives/Icon.tsx b/src/frontend/src/primitives/Icon.tsx index 7275fad0..9f64b095 100644 --- a/src/frontend/src/primitives/Icon.tsx +++ b/src/frontend/src/primitives/Icon.tsx @@ -1,48 +1,43 @@ import { cva, RecipeVariantProps } from '@/styled-system/css' -import { ComponentPropsWithoutRef } from 'react' +import { ComponentPropsWithoutRef, FunctionComponent, SVGProps } from 'react' + +// Eagerly import every .svg in the icons folder as a React component. +// Drop a new file in /assets/icons and it's available immediately. +const modules = import.meta.glob<{ + default: FunctionComponent> +}>('../assets/icons/*.svg', { eager: true, query: '?react', import: 'default' }) + +// Build the registry: { 'chevron-down': Component, 'check': Component, ... } +// Key = filename without extension. +const icons = Object.fromEntries( + Object.entries(modules).map(([path, mod]) => { + const name = path + .split('/') + .pop()! + .replace(/\.svg$/, '') + return [name, mod as unknown as FunctionComponent>] + }) +) as Record>> + +export type IconName = keyof typeof icons const iconRecipe = cva({ base: { - fontWeight: 'normal', - fontStyle: 'normal', display: 'inline-block', + flexShrink: 0, lineHeight: 1, - textTransform: 'none', - letterSpacing: 'normal', - wordWrap: 'normal', - whiteSpace: 'nowrap', - direction: 'ltr', + color: 'currentColor', + fill: 'currentColor', }, variants: { - type: { - icons: { - fontFamily: 'Material Icons Outlined', - webkitFontSmoothing: 'antialiased', - mozOsxFontSmoothing: 'grayscale', - textRendering: 'optimizeLegibility', - fontFeatureSettings: '"liga"', - }, - symbols: { - fontFamily: 'Material Symbols Outlined Variable', - }, - }, size: { - sm: { - fontSize: '18px', - }, - md: { - fontSize: '24px', - }, - lg: { - fontSize: '32px', - }, - xl: { - fontSize: '40px', - }, + sm: { width: '18px', height: '18px' }, + md: { width: '24px', height: '24px' }, + lg: { width: '32px', height: '32px' }, + xl: { width: '40px', height: '40px' }, }, }, defaultVariants: { - type: 'icons', size: 'md', }, }) @@ -50,22 +45,33 @@ const iconRecipe = cva({ export type IconRecipeProps = RecipeVariantProps export type IconProps = IconRecipeProps & - ComponentPropsWithoutRef<'span'> & { - name: string + Omit, 'name'> & { + name: IconName } export const Icon = ({ name, ...props }: IconProps) => { const [variantProps, componentProps] = iconRecipe.splitVariantProps(props) - const { className, ...remainingComponentProps } = componentProps + const { className, ...rest } = componentProps + + const SvgIcon = icons[name] + + if (!SvgIcon) { + if (process.env.NODE_ENV !== 'production') { + console.warn( + `[Icon] Unknown icon name: "${name}". Available: ${Object.keys(icons).join(', ')}` + ) + } + return null + } return ( - + focusable="false" + className={[iconRecipe(variantProps), className] + .filter(Boolean) + .join(' ')} + {...rest} + /> ) } diff --git a/src/frontend/src/styles/index.css b/src/frontend/src/styles/index.css index 8944fe9a..69019621 100644 --- a/src/frontend/src/styles/index.css +++ b/src/frontend/src/styles/index.css @@ -76,39 +76,6 @@ html:has(.lk-video-conference) { } } -/* Generated using the Advanced installation guidelines from - * https://fontsource.org/fonts/material-icons-outlined/install - */ -@font-face { - font-family: 'Material Icons Outlined'; - font-style: normal; - font-display: block; - font-weight: 400; - src: url(@fontsource/material-icons-outlined/files/material-icons-outlined-latin-400-normal.woff2) - format('woff2'); - unicode-range: - U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, - U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, - U+2212, U+2215, U+FEFF, U+FFFD; -} - -/* Generated using the Advanced installation guidelines from - * https://fontsource.org/fonts/material-symbols-outlined/install - */ -/* material-symbols-outlined-latin-wght-normal */ -@font-face { - font-family: 'Material Symbols Outlined Variable'; - font-style: normal; - font-display: block; - font-weight: 100 700; - src: url(@fontsource-variable/material-symbols-outlined/files/material-symbols-outlined-latin-wght-normal.woff2) - format('woff2-variations'); - unicode-range: - U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, - U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, - U+2212, U+2215, U+FEFF, U+FFFD; -} - .hoverGroup .hoverGroupChild { opacity: 0; pointer-events: none; diff --git a/src/frontend/vite.config.ts b/src/frontend/vite.config.ts index cfef2a31..a0258422 100644 --- a/src/frontend/vite.config.ts +++ b/src/frontend/vite.config.ts @@ -2,6 +2,7 @@ import { defineConfig, loadEnv } from 'vite' import react from '@vitejs/plugin-react' import tsconfigPaths from 'vite-tsconfig-paths' import { visualizer } from 'rollup-plugin-visualizer' +import svgr from 'vite-plugin-svgr' // https://vitejs.dev/config/ export default defineConfig(({ mode }) => { @@ -10,6 +11,11 @@ export default defineConfig(({ mode }) => { plugins: [ react(), tsconfigPaths(), + svgr({ + svgrOptions: { + replaceAttrValues: { '#000': 'currentColor', '#000000': 'currentColor', '#1f1f1f': 'currentColor' }, + }, + }), env.VITE_ANALYZE === 'true' && visualizer({ open: true, filename: 'rollup-plugin-visualizer/stats.html',