diff --git a/dashboard/package.json b/dashboard/package.json index 9d6b7f8667..d8ce1c7af9 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -5,6 +5,7 @@ "author": "CodedThemes", "scripts": { "dev": "vite --host", + "subset-icons": "node scripts/subset-mdi-font.mjs", "build": "vue-tsc --noEmit && vite build", "build-stage": "vue-tsc --noEmit && vite build --base=/vue/free/stage/", "build-prod": "vue-tsc --noEmit && vite build --base=/vue/free/", @@ -33,7 +34,6 @@ "monaco-editor": "^0.52.2", "pinia": "2.1.6", "pinyin-pro": "^3.26.0", - "remixicon": "3.5.0", "shiki": "^3.20.0", "stream-markdown": "^0.0.13", "vee-validate": "4.11.3", @@ -62,8 +62,10 @@ "prettier": "3.0.2", "sass": "1.66.1", "sass-loader": "13.3.2", + "subset-font": "^2.4.0", "typescript": "5.1.6", "vite": "6.4.1", + "vite-plugin-webfont-dl": "^3.12.0", "vue-cli-plugin-vuetify": "2.5.8", "vue-tsc": "1.8.8", "vuetify-loader": "^2.0.0-alpha.9" @@ -74,4 +76,4 @@ "lodash-es": "4.17.23" } } -} +} \ No newline at end of file diff --git a/dashboard/pnpm-lock.yaml b/dashboard/pnpm-lock.yaml index 74f3eb9b31..a3926a9534 100644 --- a/dashboard/pnpm-lock.yaml +++ b/dashboard/pnpm-lock.yaml @@ -72,9 +72,6 @@ importers: pinyin-pro: specifier: ^3.26.0 version: 3.28.0 - remixicon: - specifier: 3.5.0 - version: 3.5.0 shiki: specifier: ^3.20.0 version: 3.22.0 @@ -154,12 +151,18 @@ importers: sass-loader: specifier: 13.3.2 version: 13.3.2(sass@1.66.1)(webpack@5.105.0) + subset-font: + specifier: ^2.4.0 + version: 2.4.0 typescript: specifier: 5.1.6 version: 5.1.6 vite: specifier: 6.4.1 version: 6.4.1(@types/node@20.19.32)(sass@1.66.1)(terser@5.46.0) + vite-plugin-webfont-dl: + specifier: ^3.12.0 + version: 3.12.0(vite@6.4.1(@types/node@20.19.32)(sass@1.66.1)(terser@5.46.0)) vue-cli-plugin-vuetify: specifier: 2.5.8 version: 2.5.8(sass-loader@13.3.2(sass@1.66.1)(webpack@5.105.0))(vue@3.3.4)(vuetify-loader@2.0.0-alpha.9(@vue/compiler-sfc@3.3.4)(vue@3.3.4)(vuetify@3.7.11)(webpack@5.105.0))(webpack@5.105.0) @@ -199,6 +202,12 @@ packages: '@braintree/sanitize-url@7.1.2': resolution: {integrity: sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==} + '@cacheable/memory@2.0.8': + resolution: {integrity: sha512-FvEb29x5wVwu/Kf93IWwsOOEuhHh6dYCJF3vcKLzXc0KXIW181AOzv6ceT4ZpBHDvAfG60eqb+ekmrnLHIy+jw==} + + '@cacheable/utils@2.4.0': + resolution: {integrity: sha512-PeMMsqjVq+bF0WBsxFBxr/WozBJiZKY0rUojuaCoIaKnEl3Ju1wfEwS+SV1DU/cSe8fqHIPiYJFif8T3MVt4cQ==} + '@chevrotain/cst-dts-gen@11.0.3': resolution: {integrity: sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==} @@ -454,6 +463,15 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@keyv/bigmap@1.3.1': + resolution: {integrity: sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==} + engines: {node: '>= 18'} + peerDependencies: + keyv: ^5.6.0 + + '@keyv/serialize@1.1.1': + resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==} + '@mdi/font@7.2.96': resolution: {integrity: sha512-e//lmkmpFUMZKhmCY9zdjRe4zNXfbOIJnn6xveHbaV2kSw5aJ5dLXUxcRt1Gxfi7ZYpFLUWlkG2MGSFAiqAu7w==} @@ -519,79 +537,66 @@ packages: resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} cpu: [arm] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.59.0': resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} cpu: [arm] os: [linux] - libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.59.0': resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} cpu: [arm64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.59.0': resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} cpu: [arm64] os: [linux] - libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.59.0': resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} cpu: [loong64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.59.0': resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} cpu: [loong64] os: [linux] - libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.59.0': resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} cpu: [ppc64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.59.0': resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} cpu: [ppc64] os: [linux] - libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.59.0': resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} cpu: [riscv64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.59.0': resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} cpu: [riscv64] os: [linux] - libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.59.0': resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} cpu: [s390x] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.59.0': resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} cpu: [x64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-musl@4.59.0': resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} cpu: [x64] os: [linux] - libc: [musl] '@rollup/rollup-openbsd-x64@4.59.0': resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} @@ -1258,6 +1263,9 @@ packages: buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + cacheable@2.3.3: + resolution: {integrity: sha512-iffYMX4zxKp54evOH27fm92hs+DeC1DhXmNVN8Tr94M/iZIV42dqTHSR2Ik4TOSPyOAwKr7Yu3rN9ALoLkbWyQ==} + call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -1304,6 +1312,10 @@ packages: resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} engines: {node: '>=6.0'} + clean-css@5.3.3: + resolution: {integrity: sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==} + engines: {node: '>= 10.0'} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -1760,6 +1772,9 @@ packages: resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} engines: {node: ^10.12.0 || >=12.0.0} + flat-cache@6.1.20: + resolution: {integrity: sha512-AhHYqwvN62NVLp4lObVXGVluiABTHapoB57EyegZVmazN+hhGhLTn3uZbOofoTw4DSDvVCadzzyChXhOAvy8uQ==} + flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} @@ -1772,6 +1787,9 @@ packages: debug: optional: true + fontverter@2.0.0: + resolution: {integrity: sha512-DFVX5hvXuhi1Jven1tbpebYTCT9XYnvx6/Z+HFUPb7ZRMCW+pj2clU9VMhoTPgWKPhAs7JJDSk3CW1jNUvKCZQ==} + form-data@4.0.5: resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} engines: {node: '>= 6'} @@ -1831,6 +1849,9 @@ packages: hachure-fill@0.5.2: resolution: {integrity: sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==} + harfbuzzjs@0.4.15: + resolution: {integrity: sha512-p1edvnlc+vpRe2kz7OKzcscf0gyFiDZpco+miDxAiiZ67cu1oNlbuOkmP/A/i1l/w938VrkF2FdZ8scNcnkPrQ==} + has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -1843,6 +1864,10 @@ packages: resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} engines: {node: '>= 0.4'} + hashery@1.5.0: + resolution: {integrity: sha512-nhQ6ExaOIqti2FDWoEMWARUqIKyjr2VcZzXShrI+A3zpeiuPWzx6iPftt44LhP74E5sW36B75N6VHbvRtpvO6Q==} + engines: {node: '>=20'} + hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} @@ -1861,6 +1886,9 @@ packages: resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==} engines: {node: '>=12.0.0'} + hookified@1.15.1: + resolution: {integrity: sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==} + html-void-elements@3.0.0: resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} @@ -1970,6 +1998,9 @@ packages: keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + keyv@5.6.0: + resolution: {integrity: sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==} + khroma@2.1.0: resolution: {integrity: sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==} @@ -2218,6 +2249,9 @@ packages: package-manager-detector@1.6.0: resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} + pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -2385,6 +2419,10 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + qified@0.6.0: + resolution: {integrity: sha512-tsSGN1x3h569ZSU1u6diwhltLyfUWDp3YbFHedapTmpBl0B3P6U3+Qptg7xu+v+1io1EwhdPyyRHYbEw0KN2FA==} + engines: {node: '>=20'} + querystring@0.2.1: resolution: {integrity: sha512-wkvS7mL/JMugcup3/rMitHmd9ecIGd2lhFhK9N3UUQ450h66d1r3Y9nvXzQAW1Lq+wyx61k/1pfKS5KuKiyEbg==} engines: {node: '>=0.4.x'} @@ -2410,9 +2448,6 @@ packages: regex@6.1.0: resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==} - remixicon@3.5.0: - resolution: {integrity: sha512-wNzWGKf4frb3tEmgvP5shry0n1OdTjjEk9RHLuRuAhfA50bvEdpKH1XWNUYrHUSjAQQkkdyIm+lf4mOuysIKTQ==} - require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} @@ -2564,6 +2599,9 @@ packages: stylis@4.3.6: resolution: {integrity: sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==} + subset-font@2.4.0: + resolution: {integrity: sha512-DA/45nIj4NiseVdfHxVdVGL7hvNo3Ol6HjEm3KSYtPyDcsr6jh8Q37vSgz+A722wMfUd6nL8kgsi7uGv9DExXQ==} + supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -2760,6 +2798,11 @@ packages: vue: ^3.0.0 vuetify: '>=3' + vite-plugin-webfont-dl@3.12.0: + resolution: {integrity: sha512-0jxsr8ycuoK/uV5Y3ytttTRhgvfZo8v3O4JZBlVc4C7QWIws/vCLVR4B3ag+TGVkLNQya6hXfY3UnZge3M8vmA==} + peerDependencies: + vite: ^2 || ^3 || ^4 || ^5 || ^6 || ^7 || ^8 + vite@6.4.1: resolution: {integrity: sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -2914,6 +2957,10 @@ packages: resolution: {integrity: sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==} engines: {node: '>=10.13.0'} + wawoff2@2.0.1: + resolution: {integrity: sha512-r0CEmvpH63r4T15ebFqeOjGqU4+EgTx4I510NtK35EMciSdcTxCw3Byy3JnBonz7iyIFZ0AbVo0bbFpEVuhCYA==} + hasBin: true + webpack-sources@3.3.4: resolution: {integrity: sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==} engines: {node: '>=10.13.0'} @@ -2933,6 +2980,9 @@ packages: engines: {node: '>= 8'} hasBin: true + woff2sfnt-sfnt2woff@1.0.0: + resolution: {integrity: sha512-edK4COc1c1EpRfMqCZO1xJOvdUtM5dbVb9iz97rScvnTevqEB3GllnLWCmMVp1MfQBdF1DFg/11I0rSyAdS4qQ==} + word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} @@ -2978,6 +3028,18 @@ snapshots: '@braintree/sanitize-url@7.1.2': {} + '@cacheable/memory@2.0.8': + dependencies: + '@cacheable/utils': 2.4.0 + '@keyv/bigmap': 1.3.1(keyv@5.6.0) + hookified: 1.15.1 + keyv: 5.6.0 + + '@cacheable/utils@2.4.0': + dependencies: + hashery: 1.5.0 + keyv: 5.6.0 + '@chevrotain/cst-dts-gen@11.0.3': dependencies: '@chevrotain/gast': 11.0.3 @@ -3165,6 +3227,14 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@keyv/bigmap@1.3.1(keyv@5.6.0)': + dependencies: + hashery: 1.5.0 + hookified: 1.15.1 + keyv: 5.6.0 + + '@keyv/serialize@1.1.1': {} + '@mdi/font@7.2.96': {} '@mermaid-js/parser@0.6.3': @@ -4067,6 +4137,14 @@ snapshots: buffer-from@1.1.2: {} + cacheable@2.3.3: + dependencies: + '@cacheable/memory': 2.0.8 + '@cacheable/utils': 2.4.0 + hookified: 1.15.1 + keyv: 5.6.0 + qified: 0.6.0 + call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -4119,6 +4197,10 @@ snapshots: chrome-trace-event@1.0.4: {} + clean-css@5.3.3: + dependencies: + source-map: 0.6.1 + color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -4627,10 +4709,21 @@ snapshots: keyv: 4.5.4 rimraf: 3.0.2 + flat-cache@6.1.20: + dependencies: + cacheable: 2.3.3 + flatted: 3.3.3 + hookified: 1.15.1 + flatted@3.3.3: {} follow-redirects@1.15.11: {} + fontverter@2.0.0: + dependencies: + wawoff2: 2.0.1 + woff2sfnt-sfnt2woff: 1.0.0 + form-data@4.0.5: dependencies: asynckit: 0.4.0 @@ -4704,6 +4797,8 @@ snapshots: hachure-fill@0.5.2: {} + harfbuzzjs@0.4.15: {} + has-flag@4.0.0: {} has-symbols@1.1.0: {} @@ -4712,6 +4807,10 @@ snapshots: dependencies: has-symbols: 1.1.0 + hashery@1.5.0: + dependencies: + hookified: 1.15.1 + hasown@2.0.2: dependencies: function-bind: 1.1.2 @@ -4738,6 +4837,8 @@ snapshots: highlight.js@11.11.1: {} + hookified@1.15.1: {} + html-void-elements@3.0.0: {} iconv-lite@0.6.3: @@ -4822,6 +4923,10 @@ snapshots: dependencies: json-buffer: 3.0.1 + keyv@5.6.0: + dependencies: + '@keyv/serialize': 1.1.1 + khroma@2.1.0: {} langium@3.3.1: @@ -5078,6 +5183,8 @@ snapshots: package-manager-detector@1.6.0: {} + pako@1.0.11: {} + parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -5263,6 +5370,10 @@ snapshots: punycode@2.3.1: {} + qified@0.6.0: + dependencies: + hookified: 1.15.1 + querystring@0.2.1: {} queue-microtask@1.2.3: {} @@ -5285,8 +5396,6 @@ snapshots: dependencies: regex-utilities: 2.3.0 - remixicon@3.5.0: {} - require-from-string@2.0.2: {} resolve-from@4.0.0: {} @@ -5457,6 +5566,13 @@ snapshots: stylis@4.3.6: {} + subset-font@2.4.0: + dependencies: + fontverter: 2.0.0 + harfbuzzjs: 0.4.15 + lodash: 4.17.23 + p-limit: 3.1.0 + supports-color@7.2.0: dependencies: has-flag: 4.0.0 @@ -5635,6 +5751,16 @@ snapshots: transitivePeerDependencies: - supports-color + vite-plugin-webfont-dl@3.12.0(vite@6.4.1(@types/node@20.19.32)(sass@1.66.1)(terser@5.46.0)): + dependencies: + axios: 1.13.5 + clean-css: 5.3.3 + flat-cache: 6.1.20 + picocolors: 1.1.1 + vite: 6.4.1(@types/node@20.19.32)(sass@1.66.1)(terser@5.46.0) + transitivePeerDependencies: + - debug + vite@6.4.1(@types/node@20.19.32)(sass@1.66.1)(terser@5.46.0): dependencies: esbuild: 0.25.12 @@ -5765,6 +5891,10 @@ snapshots: glob-to-regexp: 0.4.1 graceful-fs: 4.2.11 + wawoff2@2.0.1: + dependencies: + argparse: 2.0.1 + webpack-sources@3.3.4: {} webpack@5.105.0: @@ -5803,6 +5933,10 @@ snapshots: dependencies: isexe: 2.0.0 + woff2sfnt-sfnt2woff@1.0.0: + dependencies: + pako: 1.0.11 + word-wrap@1.2.5: {} wrappy@1.0.2: {} diff --git a/dashboard/scripts/subset-mdi-font.mjs b/dashboard/scripts/subset-mdi-font.mjs new file mode 100644 index 0000000000..ee8ca831f2 --- /dev/null +++ b/dashboard/scripts/subset-mdi-font.mjs @@ -0,0 +1,291 @@ +/** + * subset-mdi-font.mjs + * + * Build script that: + * 1. Scans src/ for all mdi-* icon names used in .vue/.ts files + * 2. Resolves their Unicode codepoints from @mdi/font CSS + * 3. Subsets the MDI font to include only those glyphs (via subset-font, pure JS) + * 4. Generates a minimal CSS file with only the needed icon classes + * 5. Outputs to src/assets/mdi-subset/ + * + * Fallback: if any step fails, copies the original full @mdi/font CSS and fonts + * so the build never breaks. + */ +import { readFileSync, writeFileSync, copyFileSync, readdirSync, statSync, existsSync, mkdirSync } from "fs"; +import { join, resolve, extname } from "path"; +import { fileURLToPath } from "url"; + +// Derive __dirname portably from import.meta.url (works across all Node ESM versions) +const __dirname = fileURLToPath(new URL(".", import.meta.url)); +const ROOT = resolve(__dirname, ".."); +const SRC = join(ROOT, "src"); +const MDI_CSS_PATH = join(ROOT, "node_modules/@mdi/font/css/materialdesignicons.css"); +const MDI_TTF_PATH = join(ROOT, "node_modules/@mdi/font/fonts/materialdesignicons-webfont.ttf"); +const MDI_WOFF2_PATH = join(ROOT, "node_modules/@mdi/font/fonts/materialdesignicons-webfont.woff2"); +const MDI_WOFF_PATH = join(ROOT, "node_modules/@mdi/font/fonts/materialdesignicons-webfont.woff"); +const OUT_DIR = join(ROOT, "src/assets/mdi-subset"); + +// Utility classes that should not be treated as icon names +const UTILITY_CLASSES = new Set([ + "mdi-set", "mdi-spin", "mdi-rotate-45", "mdi-rotate-90", "mdi-rotate-135", + "mdi-rotate-180", "mdi-rotate-225", "mdi-rotate-270", "mdi-rotate-315", + "mdi-flip-h", "mdi-flip-v", "mdi-light", "mdi-dark", "mdi-inactive", + "mdi-18px", "mdi-24px", "mdi-36px", "mdi-48px", +]); + +// Regex to match individual icon class definitions in MDI CSS +export const ICON_CLASS_PATTERN = /\.(mdi-[a-z][a-z0-9-]*)::before\s*\{\s*content:\s*"\\([0-9A-Fa-f]+)"\s*;?\s*}/g; + +// ── Helper functions ──────────────────────────────────────────────────────── + +/** Recursively collect files with given extensions, skipping node_modules. */ +export function* collectFiles(dir, exts) { + for (const entry of readdirSync(dir, { withFileTypes: true })) { + const full = join(dir, entry.name); + if (entry.isDirectory() && entry.name !== "node_modules") { + yield* collectFiles(full, exts); + } else if (exts.includes(extname(entry.name))) { + yield full; + } + } +} + +/** Scan source files and return a Set of used mdi-* icon names. */ +export function scanUsedIcons(sourceFiles) { + const iconPattern = /mdi-[a-z][a-z0-9-]*/g; + const usedIcons = new Set(); + for (const file of sourceFiles) { + const content = readFileSync(file, "utf-8"); + for (const match of content.matchAll(iconPattern)) { + if (!UTILITY_CLASSES.has(match[0])) { + usedIcons.add(match[0]); + } + } + } + return usedIcons; +} + +/** Parse @mdi/font CSS and return a Map of icon-name → hex codepoint. */ +export function parseIconCodepoints(mdiCSS) { + const iconMap = new Map(); + for (const match of mdiCSS.matchAll(ICON_CLASS_PATTERN)) { + iconMap.set(match[1], match[2]); + } + return iconMap; +} + +/** Resolve used icons against the codepoint map, returning resolved/missing/subsetChars. */ +export function resolveUsedIcons(usedIcons, iconMap) { + const resolvedIcons = []; + const missingIcons = []; + const subsetChars = []; + for (const icon of usedIcons) { + const cp = iconMap.get(icon); + if (cp) { + resolvedIcons.push(icon); + subsetChars.push(String.fromCodePoint(parseInt(cp, 16))); + } else { + missingIcons.push(icon); + } + } + return { resolvedIcons, missingIcons, subsetChars }; +} + +/** + * Extract utility CSS rules (size, rotation, flip, spin, etc.) from the original MDI CSS. + * Uses a subtraction approach: removes the parts we regenerate (icon definitions, + * @font-face, base .mdi rules) and keeps everything else. This is more robust than + * relying on a fixed start marker, as it tolerates CSS reordering in future versions. + */ +export function extractUtilityCss(mdiCSS, iconClassPattern) { + let utilityCss = mdiCSS + .replace(iconClassPattern, "") // Remove icon definitions + .replace(/@font-face\s*\{[\s\S]*?}/g, "") // Remove @font-face + .replace(/\.mdi:before,\s*\.mdi-set\s*\{[\s\S]*?}/g, "") // Remove base rules + .replace(/\/\*# sourceMappingURL=.*\*\//, "") // Remove source map + .trim(); + + // Clean up excess blank lines left after removals + utilityCss = utilityCss.replace(/(\r\n|\n){3,}/g, "\n\n"); + + return utilityCss; +} + +/** Build a fallback CSS that rewrites font URLs to use subset filenames. */ +function buildFallbackCss() { + const mdiCSS = readFileSync(MDI_CSS_PATH, "utf-8"); + return mdiCSS + // Rewrite woff/woff2 URLs to point at subset filenames + .replace(/url\("\.\.\/fonts\/materialdesignicons-webfont\.(woff2?)\?[^"]*"\)/g, + (_, ext) => `url("./materialdesignicons-webfont-subset.${ext}")`) + // Remove legacy eot/ttf sources + .replace(/url\("\.\.\/fonts\/materialdesignicons-webfont\.(eot|ttf)\?[^"]*"\)[^,]*/g, "") + // Clean up dangling commas/separators + .replace(/src:\s*,/g, "src:") + .replace(/,\s*;/g, ";"); +} + +// ── Fallback: copy original full MDI font if subsetting fails ─────────────── +function fallbackToFullFont(reason) { + console.warn(`\n⚠️ Subsetting failed: ${reason}`); + console.warn(`⚠️ Falling back to full @mdi/font (build will not break)\n`); + + // Copy original font files + if (existsSync(MDI_WOFF2_PATH)) { + copyFileSync(MDI_WOFF2_PATH, join(OUT_DIR, "materialdesignicons-webfont-subset.woff2")); + } + if (existsSync(MDI_WOFF_PATH)) { + copyFileSync(MDI_WOFF_PATH, join(OUT_DIR, "materialdesignicons-webfont-subset.woff")); + } + + writeFileSync(join(OUT_DIR, "materialdesignicons-subset.css"), buildFallbackCss()); + + const size = existsSync(MDI_WOFF2_PATH) ? statSync(MDI_WOFF2_PATH).size : 0; + console.warn(`⚠️ Fallback complete: using full font (${(size / 1024).toFixed(1)} KB woff2)`); +} + +// ── Exported entry point ──────────────────────────────────────────────────── + +export async function runMdiSubset() { + mkdirSync(OUT_DIR, { recursive: true }); + + try { + // Pre-checks + if (!existsSync(MDI_CSS_PATH)) { + throw new Error(`@mdi/font CSS not found at ${MDI_CSS_PATH}. Run 'pnpm install' first.`); + } + if (!existsSync(MDI_TTF_PATH)) { + throw new Error(`@mdi/font TTF not found at ${MDI_TTF_PATH}. Run 'pnpm install' first.`); + } + + // Dynamic import subset-font (may not be installed in all environments) + let subsetFont; + try { + subsetFont = (await import("subset-font")).default; + } catch (e) { + throw new Error(`subset-font package not available: ${e.message}. Run 'pnpm install' first.`); + } + + // Step 1: Scan source files for mdi-* icon names + const sourceFiles = collectFiles(SRC, [".vue", ".ts", ".js"]); + const usedIcons = scanUsedIcons(sourceFiles); + if (usedIcons.size === 0) { + throw new Error("No mdi-* icons found in source files. Something is wrong with scanning."); + } + console.log(`✅ Found ${usedIcons.size} unique mdi-* icons in source files`); + + // Step 2: Parse @mdi/font CSS to get codepoints for each icon + const mdiCSS = readFileSync(MDI_CSS_PATH, "utf-8"); + const iconMap = parseIconCodepoints(mdiCSS); + if (iconMap.size === 0) { + throw new Error("Could not parse any icon definitions from @mdi/font CSS. Format may have changed."); + } + console.log(`📦 MDI font CSS contains ${iconMap.size} icon definitions`); + + // Step 3: Resolve codepoints for used icons + const { resolvedIcons, missingIcons, subsetChars } = resolveUsedIcons(usedIcons, iconMap); + if (missingIcons.length > 0) { + console.warn(`⚠️ ${missingIcons.length} icons not found in MDI CSS:`, missingIcons.join(", ")); + } + if (resolvedIcons.length === 0) { + throw new Error("No icon codepoints could be resolved. Icon name format may have changed."); + } + console.log(`🔍 Resolved ${resolvedIcons.length} codepoints for subsetting`); + + // Add space character + subsetChars.push(" "); + const subsetText = subsetChars.join(""); + + // Step 4: Subset font with subset-font (pure JS/WASM) + const fontBuffer = readFileSync(MDI_TTF_PATH); + + console.log(`🔧 Subsetting font to woff2...`); + const woff2Buffer = await subsetFont(fontBuffer, subsetText, { + targetFormat: "woff2", + }); + + console.log(`🔧 Subsetting font to woff...`); + const woffBuffer = await subsetFont(fontBuffer, subsetText, { + targetFormat: "woff", + }); + + if (woff2Buffer.length === 0 || woffBuffer.length === 0) { + throw new Error("subset-font produced empty output. Font file may be corrupted."); + } + + const outWoff2 = join(OUT_DIR, "materialdesignicons-webfont-subset.woff2"); + const outWoff = join(OUT_DIR, "materialdesignicons-webfont-subset.woff"); + writeFileSync(outWoff2, woff2Buffer); + writeFileSync(outWoff, woffBuffer); + + // Step 5: Generate subset CSS + let css = `/* Auto-generated MDI subset – ${resolvedIcons.length} icons */ +/* Do not edit manually. Run: pnpm run subset-icons */ + +@font-face { + font-family: "Material Design Icons"; + src: url("./materialdesignicons-webfont-subset.woff2") format("woff2"), + url("./materialdesignicons-webfont-subset.woff") format("woff"); + font-weight: normal; + font-style: normal; +} + +.mdi:before, +.mdi-set { + display: inline-block; + font: normal normal normal 24px/1 "Material Design Icons"; + font-size: inherit; + text-rendering: auto; + line-height: inherit; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +`; + + for (const icon of resolvedIcons.sort()) { + const cp = iconMap.get(icon); + css += `.${icon}::before {\n content: "\\${cp}";\n}\n\n`; + } + + const utilityCss = extractUtilityCss(mdiCSS, ICON_CLASS_PATTERN); + if (utilityCss) { + css += `/* Utility classes (extracted from @mdi/font) */\n${utilityCss}\n`; + } else { + console.warn("⚠️ Could not find MDI utility classes in original CSS, skipping"); + } + + const outCSS = join(OUT_DIR, "materialdesignicons-subset.css"); + writeFileSync(outCSS, css); + + // Report + const origSize = statSync(MDI_TTF_PATH).size; + const subsetWoff2Size = woff2Buffer.length; + console.log(`\n📊 Results:`); + console.log(` Original TTF font: ${(origSize / 1024).toFixed(1)} KB`); + console.log(` Subset WOFF2: ${(subsetWoff2Size / 1024).toFixed(1)} KB`); + console.log(` Reduction: ${((1 - subsetWoff2Size / origSize) * 100).toFixed(1)}%`); + console.log(` Icons included: ${resolvedIcons.length}`); + console.log(` CSS file: ${outCSS}`); + console.log(`\n✅ MDI font subset generated successfully!`); + + } catch (err) { + // Fallback: any failure → use original full font so build never breaks + try { + fallbackToFullFont(err.message); + } catch (fallbackErr) { + console.error(`❌ Fallback also failed: ${fallbackErr.message}`); + console.error(`❌ Please ensure @mdi/font is installed: pnpm install`); + throw fallbackErr; + } + } +} + +// ── CLI entry point: allows running directly via `node scripts/subset-mdi-font.mjs` ── + +if (import.meta.url.startsWith('file:') && process.argv[1] === fileURLToPath(import.meta.url)) { + runMdiSubset().catch(err => { + console.error(err); + process.exit(1); + }); +} diff --git a/dashboard/src/assets/mdi-subset/materialdesignicons-subset.css b/dashboard/src/assets/mdi-subset/materialdesignicons-subset.css new file mode 100644 index 0000000000..7f734b0498 --- /dev/null +++ b/dashboard/src/assets/mdi-subset/materialdesignicons-subset.css @@ -0,0 +1,1193 @@ +/* Auto-generated MDI subset – 229 icons */ +/* Do not edit manually. Run: pnpm run subset-icons */ + +@font-face { + font-family: "Material Design Icons"; + src: url("./materialdesignicons-webfont-subset.woff2") format("woff2"), + url("./materialdesignicons-webfont-subset.woff") format("woff"); + font-weight: normal; + font-style: normal; +} + +.mdi:before, +.mdi-set { + display: inline-block; + font: normal normal normal 24px/1 "Material Design Icons"; + font-size: inherit; + text-rendering: auto; + line-height: inherit; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.mdi-account::before { + content: "\F0004"; +} + +.mdi-account-circle::before { + content: "\F0009"; +} + +.mdi-account-edit-outline::before { + content: "\F0FFB"; +} + +.mdi-account-heart::before { + content: "\F0899"; +} + +.mdi-account-voice::before { + content: "\F05CB"; +} + +.mdi-alert::before { + content: "\F0026"; +} + +.mdi-alert-circle::before { + content: "\F0028"; +} + +.mdi-alert-circle-outline::before { + content: "\F05D6"; +} + +.mdi-alert-outline::before { + content: "\F002A"; +} + +.mdi-api-off::before { + content: "\F1257"; +} + +.mdi-arrow-down::before { + content: "\F0045"; +} + +.mdi-arrow-down-thin::before { + content: "\F19B3"; +} + +.mdi-arrow-left::before { + content: "\F004D"; +} + +.mdi-arrow-right::before { + content: "\F0054"; +} + +.mdi-arrow-top-right-thick::before { + content: "\F09C6"; +} + +.mdi-arrow-up::before { + content: "\F005D"; +} + +.mdi-arrow-up-bold::before { + content: "\F0737"; +} + +.mdi-arrow-up-circle::before { + content: "\F0CE1"; +} + +.mdi-arrow-up-thin::before { + content: "\F19B2"; +} + +.mdi-backup-restore::before { + content: "\F006F"; +} + +.mdi-book-open-page-variant::before { + content: "\F05DA"; +} + +.mdi-book-open-variant::before { + content: "\F14F7"; +} + +.mdi-brain::before { + content: "\F09D1"; +} + +.mdi-bug::before { + content: "\F00E4"; +} + +.mdi-calendar::before { + content: "\F00ED"; +} + +.mdi-calendar-edit::before { + content: "\F08A7"; +} + +.mdi-calendar-plus::before { + content: "\F00F3"; +} + +.mdi-calendar-range::before { + content: "\F0679"; +} + +.mdi-chat::before { + content: "\F0B79"; +} + +.mdi-chat-processing::before { + content: "\F0B7B"; +} + +.mdi-chat-remove::before { + content: "\F1411"; +} + +.mdi-check::before { + content: "\F012C"; +} + +.mdi-check-all::before { + content: "\F012D"; +} + +.mdi-check-circle::before { + content: "\F05E0"; +} + +.mdi-check-circle-outline::before { + content: "\F05E1"; +} + +.mdi-checkbox-blank-outline::before { + content: "\F0131"; +} + +.mdi-checkbox-marked::before { + content: "\F0132"; +} + +.mdi-checkbox-multiple-marked-outline::before { + content: "\F0139"; +} + +.mdi-chevron-double-left::before { + content: "\F013D"; +} + +.mdi-chevron-double-right::before { + content: "\F013E"; +} + +.mdi-chevron-down::before { + content: "\F0140"; +} + +.mdi-chevron-left::before { + content: "\F0141"; +} + +.mdi-chevron-right::before { + content: "\F0142"; +} + +.mdi-chevron-up::before { + content: "\F0143"; +} + +.mdi-circle::before { + content: "\F0765"; +} + +.mdi-circle-small::before { + content: "\F09DF"; +} + +.mdi-clock-outline::before { + content: "\F0150"; +} + +.mdi-close::before { + content: "\F0156"; +} + +.mdi-close-circle::before { + content: "\F0159"; +} + +.mdi-close-circle-outline::before { + content: "\F015A"; +} + +.mdi-cloud-upload::before { + content: "\F0167"; +} + +.mdi-code-json::before { + content: "\F0626"; +} + +.mdi-code-tags::before { + content: "\F0174"; +} + +.mdi-code-tags-check::before { + content: "\F0694"; +} + +.mdi-cog::before { + content: "\F0493"; +} + +.mdi-cog-outline::before { + content: "\F08BB"; +} + +.mdi-cogs::before { + content: "\F08D6"; +} + +.mdi-comment-question::before { + content: "\F0817"; +} + +.mdi-compare-vertical::before { + content: "\F1493"; +} + +.mdi-connection::before { + content: "\F1616"; +} + +.mdi-console::before { + content: "\F018D"; +} + +.mdi-console-line::before { + content: "\F07B7"; +} + +.mdi-content-copy::before { + content: "\F018F"; +} + +.mdi-content-save::before { + content: "\F0193"; +} + +.mdi-creation::before { + content: "\F0674"; +} + +.mdi-cursor-default-click::before { + content: "\F0CFD"; +} + +.mdi-cursor-move::before { + content: "\F01BE"; +} + +.mdi-database::before { + content: "\F01BC"; +} + +.mdi-database-cog::before { + content: "\F164B"; +} + +.mdi-database-off::before { + content: "\F1640"; +} + +.mdi-delete::before { + content: "\F01B4"; +} + +.mdi-delete-outline::before { + content: "\F09E7"; +} + +.mdi-dots-hexagon::before { + content: "\F15FF"; +} + +.mdi-dots-horizontal::before { + content: "\F01D8"; +} + +.mdi-dots-vertical::before { + content: "\F01D9"; +} + +.mdi-download::before { + content: "\F01DA"; +} + +.mdi-emoticon::before { + content: "\F0C68"; +} + +.mdi-emoticon-confused::before { + content: "\F10DE"; +} + +.mdi-emoticon-confused-outline::before { + content: "\F10DF"; +} + +.mdi-export::before { + content: "\F0207"; +} + +.mdi-eye::before { + content: "\F0208"; +} + +.mdi-eye-off::before { + content: "\F0209"; +} + +.mdi-eye-outline::before { + content: "\F06D0"; +} + +.mdi-file::before { + content: "\F0214"; +} + +.mdi-file-chart::before { + content: "\F0215"; +} + +.mdi-file-code::before { + content: "\F022E"; +} + +.mdi-file-document::before { + content: "\F0219"; +} + +.mdi-file-document-edit-outline::before { + content: "\F0DC9"; +} + +.mdi-file-document-multiple::before { + content: "\F1517"; +} + +.mdi-file-document-outline::before { + content: "\F09EE"; +} + +.mdi-file-excel-box::before { + content: "\F021C"; +} + +.mdi-file-outline::before { + content: "\F0224"; +} + +.mdi-file-pdf-box::before { + content: "\F0226"; +} + +.mdi-file-powerpoint-box::before { + content: "\F0228"; +} + +.mdi-file-question-outline::before { + content: "\F1036"; +} + +.mdi-file-upload::before { + content: "\F0A4D"; +} + +.mdi-file-upload-outline::before { + content: "\F0A4E"; +} + +.mdi-file-word-box::before { + content: "\F022D"; +} + +.mdi-filter-remove::before { + content: "\F0234"; +} + +.mdi-filter-variant::before { + content: "\F0236"; +} + +.mdi-flash::before { + content: "\F0241"; +} + +.mdi-flash-off::before { + content: "\F0243"; +} + +.mdi-folder::before { + content: "\F024B"; +} + +.mdi-folder-move::before { + content: "\F0252"; +} + +.mdi-folder-multiple::before { + content: "\F0253"; +} + +.mdi-folder-open::before { + content: "\F0770"; +} + +.mdi-folder-open-outline::before { + content: "\F0DCF"; +} + +.mdi-folder-outline::before { + content: "\F0256"; +} + +.mdi-folder-plus::before { + content: "\F0257"; +} + +.mdi-folder-zip-outline::before { + content: "\F07B9"; +} + +.mdi-format-list-bulleted::before { + content: "\F0279"; +} + +.mdi-frequently-asked-questions::before { + content: "\F0EB4"; +} + +.mdi-fullscreen::before { + content: "\F0293"; +} + +.mdi-fullscreen-exit::before { + content: "\F0294"; +} + +.mdi-function-variant::before { + content: "\F0871"; +} + +.mdi-github::before { + content: "\F02A4"; +} + +.mdi-grain::before { + content: "\F0D7C"; +} + +.mdi-hand-heart::before { + content: "\F10F1"; +} + +.mdi-hand-wave-outline::before { + content: "\F1822"; +} + +.mdi-heart::before { + content: "\F02D1"; +} + +.mdi-help-circle::before { + content: "\F02D7"; +} + +.mdi-help-circle-outline::before { + content: "\F0625"; +} + +.mdi-home::before { + content: "\F02DC"; +} + +.mdi-identifier::before { + content: "\F0EFE"; +} + +.mdi-import::before { + content: "\F02FA"; +} + +.mdi-information::before { + content: "\F02FC"; +} + +.mdi-information-outline::before { + content: "\F02FD"; +} + +.mdi-key::before { + content: "\F0306"; +} + +.mdi-key-outline::before { + content: "\F0DD6"; +} + +.mdi-key-plus::before { + content: "\F0309"; +} + +.mdi-keyboard-outline::before { + content: "\F097B"; +} + +.mdi-label::before { + content: "\F0315"; +} + +.mdi-lan-connect::before { + content: "\F0318"; +} + +.mdi-language-markdown::before { + content: "\F0354"; +} + +.mdi-layers-outline::before { + content: "\F09FE"; +} + +.mdi-lightbulb-outline::before { + content: "\F0336"; +} + +.mdi-lightning-bolt::before { + content: "\F140B"; +} + +.mdi-link::before { + content: "\F0337"; +} + +.mdi-link-variant::before { + content: "\F0339"; +} + +.mdi-loading::before { + content: "\F0772"; +} + +.mdi-lock::before { + content: "\F033E"; +} + +.mdi-lock-check-outline::before { + content: "\F16A8"; +} + +.mdi-lock-outline::before { + content: "\F0341"; +} + +.mdi-lock-plus-outline::before { + content: "\F16B2"; +} + +.mdi-magnify::before { + content: "\F0349"; +} + +.mdi-memory::before { + content: "\F035B"; +} + +.mdi-menu::before { + content: "\F035C"; +} + +.mdi-message-off-outline::before { + content: "\F164E"; +} + +.mdi-message-text::before { + content: "\F0369"; +} + +.mdi-message-text-outline::before { + content: "\F036A"; +} + +.mdi-microphone::before { + content: "\F036C"; +} + +.mdi-microphone-message::before { + content: "\F050A"; +} + +.mdi-minus::before { + content: "\F0374"; +} + +.mdi-note-text-outline::before { + content: "\F11D7"; +} + +.mdi-numeric-1::before { + content: "\F0B3A"; +} + +.mdi-numeric-1-circle::before { + content: "\F0CA0"; +} + +.mdi-numeric-2::before { + content: "\F0B3B"; +} + +.mdi-numeric-2-circle::before { + content: "\F0CA2"; +} + +.mdi-numeric-3::before { + content: "\F0B3C"; +} + +.mdi-open-in-new::before { + content: "\F03CC"; +} + +.mdi-package-variant::before { + content: "\F03D6"; +} + +.mdi-pause::before { + content: "\F03E4"; +} + +.mdi-pause-circle-outline::before { + content: "\F03E6"; +} + +.mdi-pencil::before { + content: "\F03EB"; +} + +.mdi-pencil-outline::before { + content: "\F0CB6"; +} + +.mdi-pencil-plus::before { + content: "\F0DEB"; +} + +.mdi-pencil-ruler::before { + content: "\F1353"; +} + +.mdi-phone-in-talk::before { + content: "\F03F6"; +} + +.mdi-play::before { + content: "\F040A"; +} + +.mdi-play-circle-outline::before { + content: "\F040D"; +} + +.mdi-plus::before { + content: "\F0415"; +} + +.mdi-pound::before { + content: "\F0423"; +} + +.mdi-progress-check::before { + content: "\F0995"; +} + +.mdi-puzzle::before { + content: "\F0431"; +} + +.mdi-puzzle-outline::before { + content: "\F0A66"; +} + +.mdi-refresh::before { + content: "\F0450"; +} + +.mdi-rename-box::before { + content: "\F0455"; +} + +.mdi-reply::before { + content: "\F045A"; +} + +.mdi-reply-outline::before { + content: "\F0F20"; +} + +.mdi-restart::before { + content: "\F0709"; +} + +.mdi-restore::before { + content: "\F099B"; +} + +.mdi-robot::before { + content: "\F06A9"; +} + +.mdi-robot-off::before { + content: "\F16A7"; +} + +.mdi-send::before { + content: "\F048A"; +} + +.mdi-server::before { + content: "\F048B"; +} + +.mdi-server-network::before { + content: "\F048D"; +} + +.mdi-server-off::before { + content: "\F048F"; +} + +.mdi-shape-outline::before { + content: "\F0832"; +} + +.mdi-shield-check::before { + content: "\F0565"; +} + +.mdi-shield-check-outline::before { + content: "\F0CC8"; +} + +.mdi-shuffle-variant::before { + content: "\F049F"; +} + +.mdi-skip-next-circle-outline::before { + content: "\F0662"; +} + +.mdi-sort::before { + content: "\F04BA"; +} + +.mdi-sort-ascending::before { + content: "\F04BC"; +} + +.mdi-sort-variant::before { + content: "\F04BF"; +} + +.mdi-source-branch::before { + content: "\F062C"; +} + +.mdi-square-edit-outline::before { + content: "\F090C"; +} + +.mdi-star::before { + content: "\F04CE"; +} + +.mdi-star-four-points-small::before { + content: "\F1C55"; +} + +.mdi-stop::before { + content: "\F04DB"; +} + +.mdi-stop-circle::before { + content: "\F0666"; +} + +.mdi-store::before { + content: "\F04DC"; +} + +.mdi-subdirectory-arrow-right::before { + content: "\F060D"; +} + +.mdi-text::before { + content: "\F09A8"; +} + +.mdi-text-box::before { + content: "\F021A"; +} + +.mdi-text-box-outline::before { + content: "\F09ED"; +} + +.mdi-text-box-search::before { + content: "\F0EAE"; +} + +.mdi-text-box-search-outline::before { + content: "\F0EAF"; +} + +.mdi-text-search::before { + content: "\F13B8"; +} + +.mdi-timeline-text-outline::before { + content: "\F0BD4"; +} + +.mdi-tools::before { + content: "\F1064"; +} + +.mdi-translate::before { + content: "\F05CA"; +} + +.mdi-trash-can-outline::before { + content: "\F0A7A"; +} + +.mdi-update::before { + content: "\F06B0"; +} + +.mdi-upload::before { + content: "\F0552"; +} + +.mdi-vector-intersection::before { + content: "\F055D"; +} + +.mdi-vector-link::before { + content: "\F0FE8"; +} + +.mdi-vector-point::before { + content: "\F01C4"; +} + +.mdi-view-dashboard::before { + content: "\F056E"; +} + +.mdi-view-grid::before { + content: "\F0570"; +} + +.mdi-view-list::before { + content: "\F0572"; +} + +.mdi-volume-high::before { + content: "\F057E"; +} + +.mdi-weather-night::before { + content: "\F0594"; +} + +.mdi-web::before { + content: "\F059F"; +} + +.mdi-webhook::before { + content: "\F062F"; +} + +.mdi-white-balance-sunny::before { + content: "\F05A8"; +} + +.mdi-wrench::before { + content: "\F05B7"; +} + +.mdi-wrench-outline::before { + content: "\F0BE0"; +} + +.mdi-zip-box::before { + content: "\F05C4"; +} + +/* Utility classes (extracted from @mdi/font) */ +/* MaterialDesignIcons.com */ + +.mdi-blank::before { + content: "\F68C"; + visibility: hidden; +} + +.mdi-18px.mdi-set, .mdi-18px.mdi:before { + font-size: 18px; +} + +.mdi-24px.mdi-set, .mdi-24px.mdi:before { + font-size: 24px; +} + +.mdi-36px.mdi-set, .mdi-36px.mdi:before { + font-size: 36px; +} + +.mdi-48px.mdi-set, .mdi-48px.mdi:before { + font-size: 48px; +} + +.mdi-dark:before { + color: rgba(0, 0, 0, 0.54); +} + +.mdi-dark.mdi-inactive:before { + color: rgba(0, 0, 0, 0.26); +} + +.mdi-light:before { + color: white; +} + +.mdi-light.mdi-inactive:before { + color: rgba(255, 255, 255, 0.3); +} + +.mdi-rotate-45 { + /* + // Not included in production + &.mdi-flip-h:before { + -webkit-transform: scaleX(-1) rotate(45deg); + transform: scaleX(-1) rotate(45deg); + filter: FlipH; + -ms-filter: "FlipH"; + } + &.mdi-flip-v:before { + -webkit-transform: scaleY(-1) rotate(45deg); + -ms-transform: rotate(45deg); + transform: scaleY(-1) rotate(45deg); + filter: FlipV; + -ms-filter: "FlipV"; + } + */ +} + +.mdi-rotate-45:before { + -webkit-transform: rotate(45deg); + -ms-transform: rotate(45deg); + transform: rotate(45deg); +} + +.mdi-rotate-90 { + /* + // Not included in production + &.mdi-flip-h:before { + -webkit-transform: scaleX(-1) rotate(90deg); + transform: scaleX(-1) rotate(90deg); + filter: FlipH; + -ms-filter: "FlipH"; + } + &.mdi-flip-v:before { + -webkit-transform: scaleY(-1) rotate(90deg); + -ms-transform: rotate(90deg); + transform: scaleY(-1) rotate(90deg); + filter: FlipV; + -ms-filter: "FlipV"; + } + */ +} + +.mdi-rotate-90:before { + -webkit-transform: rotate(90deg); + -ms-transform: rotate(90deg); + transform: rotate(90deg); +} + +.mdi-rotate-135 { + /* + // Not included in production + &.mdi-flip-h:before { + -webkit-transform: scaleX(-1) rotate(135deg); + transform: scaleX(-1) rotate(135deg); + filter: FlipH; + -ms-filter: "FlipH"; + } + &.mdi-flip-v:before { + -webkit-transform: scaleY(-1) rotate(135deg); + -ms-transform: rotate(135deg); + transform: scaleY(-1) rotate(135deg); + filter: FlipV; + -ms-filter: "FlipV"; + } + */ +} + +.mdi-rotate-135:before { + -webkit-transform: rotate(135deg); + -ms-transform: rotate(135deg); + transform: rotate(135deg); +} + +.mdi-rotate-180 { + /* + // Not included in production + &.mdi-flip-h:before { + -webkit-transform: scaleX(-1) rotate(180deg); + transform: scaleX(-1) rotate(180deg); + filter: FlipH; + -ms-filter: "FlipH"; + } + &.mdi-flip-v:before { + -webkit-transform: scaleY(-1) rotate(180deg); + -ms-transform: rotate(180deg); + transform: scaleY(-1) rotate(180deg); + filter: FlipV; + -ms-filter: "FlipV"; + } + */ +} + +.mdi-rotate-180:before { + -webkit-transform: rotate(180deg); + -ms-transform: rotate(180deg); + transform: rotate(180deg); +} + +.mdi-rotate-225 { + /* + // Not included in production + &.mdi-flip-h:before { + -webkit-transform: scaleX(-1) rotate(225deg); + transform: scaleX(-1) rotate(225deg); + filter: FlipH; + -ms-filter: "FlipH"; + } + &.mdi-flip-v:before { + -webkit-transform: scaleY(-1) rotate(225deg); + -ms-transform: rotate(225deg); + transform: scaleY(-1) rotate(225deg); + filter: FlipV; + -ms-filter: "FlipV"; + } + */ +} + +.mdi-rotate-225:before { + -webkit-transform: rotate(225deg); + -ms-transform: rotate(225deg); + transform: rotate(225deg); +} + +.mdi-rotate-270 { + /* + // Not included in production + &.mdi-flip-h:before { + -webkit-transform: scaleX(-1) rotate(270deg); + transform: scaleX(-1) rotate(270deg); + filter: FlipH; + -ms-filter: "FlipH"; + } + &.mdi-flip-v:before { + -webkit-transform: scaleY(-1) rotate(270deg); + -ms-transform: rotate(270deg); + transform: scaleY(-1) rotate(270deg); + filter: FlipV; + -ms-filter: "FlipV"; + } + */ +} + +.mdi-rotate-270:before { + -webkit-transform: rotate(270deg); + -ms-transform: rotate(270deg); + transform: rotate(270deg); +} + +.mdi-rotate-315 { + /* + // Not included in production + &.mdi-flip-h:before { + -webkit-transform: scaleX(-1) rotate(315deg); + transform: scaleX(-1) rotate(315deg); + filter: FlipH; + -ms-filter: "FlipH"; + } + &.mdi-flip-v:before { + -webkit-transform: scaleY(-1) rotate(315deg); + -ms-transform: rotate(315deg); + transform: scaleY(-1) rotate(315deg); + filter: FlipV; + -ms-filter: "FlipV"; + } + */ +} + +.mdi-rotate-315:before { + -webkit-transform: rotate(315deg); + -ms-transform: rotate(315deg); + transform: rotate(315deg); +} + +.mdi-flip-h:before { + -webkit-transform: scaleX(-1); + transform: scaleX(-1); + filter: FlipH; + -ms-filter: "FlipH"; +} + +.mdi-flip-v:before { + -webkit-transform: scaleY(-1); + transform: scaleY(-1); + filter: FlipV; + -ms-filter: "FlipV"; +} + +.mdi-spin:before { + -webkit-animation: mdi-spin 2s infinite linear; + animation: mdi-spin 2s infinite linear; +} + +@-webkit-keyframes mdi-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } +} + +@keyframes mdi-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } +} diff --git a/dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff b/dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff new file mode 100644 index 0000000000..20cd8f5c89 Binary files /dev/null and b/dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff differ diff --git a/dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff2 b/dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff2 new file mode 100644 index 0000000000..5ddf299e2e Binary files /dev/null and b/dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff2 differ diff --git a/dashboard/src/plugins/vuetify.ts b/dashboard/src/plugins/vuetify.ts index 3e9bae9c1e..e38fd388e6 100644 --- a/dashboard/src/plugins/vuetify.ts +++ b/dashboard/src/plugins/vuetify.ts @@ -1,5 +1,5 @@ import { createVuetify } from 'vuetify'; -import '@mdi/font/css/materialdesignicons.css'; +import '@/assets/mdi-subset/materialdesignicons-subset.css'; import * as components from 'vuetify/components'; import * as directives from 'vuetify/directives'; import { PurpleTheme } from '@/theme/LightTheme'; diff --git a/dashboard/tests/subsetMdiFont.test.mjs b/dashboard/tests/subsetMdiFont.test.mjs new file mode 100644 index 0000000000..85b0ad0baa --- /dev/null +++ b/dashboard/tests/subsetMdiFont.test.mjs @@ -0,0 +1,250 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { mkdirSync, writeFileSync, rmSync } from 'fs'; +import { join } from 'path'; +import { tmpdir } from 'os'; + +import { + collectFiles, + scanUsedIcons, + parseIconCodepoints, + resolveUsedIcons, + extractUtilityCss, + ICON_CLASS_PATTERN, +} from '../scripts/subset-mdi-font.mjs'; + +// ── Helper: create a temporary directory tree for file-system tests ───────── + +function makeTmpDir() { + const base = join(tmpdir(), `mdi-test-${Date.now()}-${Math.random().toString(36).slice(2)}`); + mkdirSync(base, { recursive: true }); + return base; +} + +// ── collectFiles ──────────────────────────────────────────────────────────── + +test('collectFiles yields files matching given extensions', () => { + const tmp = makeTmpDir(); + writeFileSync(join(tmp, 'a.vue'), ''); + writeFileSync(join(tmp, 'b.ts'), ''); + writeFileSync(join(tmp, 'c.txt'), ''); + + const files = [...collectFiles(tmp, ['.vue', '.ts'])]; + assert.equal(files.length, 2); + assert.ok(files.some(f => f.endsWith('a.vue'))); + assert.ok(files.some(f => f.endsWith('b.ts'))); + + rmSync(tmp, { recursive: true }); +}); + +test('collectFiles recurses into subdirectories', () => { + const tmp = makeTmpDir(); + const sub = join(tmp, 'sub'); + mkdirSync(sub); + writeFileSync(join(sub, 'deep.vue'), ''); + + const files = [...collectFiles(tmp, ['.vue'])]; + assert.equal(files.length, 1); + assert.ok(files[0].endsWith('deep.vue')); + + rmSync(tmp, { recursive: true }); +}); + +test('collectFiles skips node_modules directories', () => { + const tmp = makeTmpDir(); + const nm = join(tmp, 'node_modules'); + mkdirSync(nm); + writeFileSync(join(nm, 'pkg.vue'), ''); + writeFileSync(join(tmp, 'app.vue'), ''); + + const files = [...collectFiles(tmp, ['.vue'])]; + assert.equal(files.length, 1); + assert.ok(files[0].endsWith('app.vue')); + + rmSync(tmp, { recursive: true }); +}); + +test('collectFiles yields nothing for empty directory', () => { + const tmp = makeTmpDir(); + const files = [...collectFiles(tmp, ['.vue'])]; + assert.equal(files.length, 0); + + rmSync(tmp, { recursive: true }); +}); + +// ── scanUsedIcons ─────────────────────────────────────────────────────────── + +test('scanUsedIcons extracts mdi-* icon names from files', () => { + const tmp = makeTmpDir(); + writeFileSync(join(tmp, 'A.vue'), 'mdi-homemdi-close'); + writeFileSync(join(tmp, 'B.vue'), 'icon="mdi-home"'); + + const icons = scanUsedIcons(collectFiles(tmp, ['.vue'])); + assert.ok(icons instanceof Set); + assert.ok(icons.has('mdi-home')); + assert.ok(icons.has('mdi-close')); + assert.equal(icons.size, 2); // mdi-home deduplicated + + rmSync(tmp, { recursive: true }); +}); + +test('scanUsedIcons excludes utility classes', () => { + const tmp = makeTmpDir(); + writeFileSync(join(tmp, 'A.vue'), 'mdi-spin mdi-rotate-90 mdi-flip-h mdi-home'); + + const icons = scanUsedIcons(collectFiles(tmp, ['.vue'])); + assert.ok(icons.has('mdi-home')); + assert.ok(!icons.has('mdi-spin')); + assert.ok(!icons.has('mdi-rotate-90')); + assert.ok(!icons.has('mdi-flip-h')); + + rmSync(tmp, { recursive: true }); +}); + +test('scanUsedIcons returns empty set when no icons found', () => { + const tmp = makeTmpDir(); + writeFileSync(join(tmp, 'A.vue'), '
Hello
'); + + const icons = scanUsedIcons(collectFiles(tmp, ['.vue'])); + assert.equal(icons.size, 0); + + rmSync(tmp, { recursive: true }); +}); + +// ── parseIconCodepoints ───────────────────────────────────────────────────── + +test('parseIconCodepoints parses icon definitions from CSS', () => { + const css = ` +.mdi-home::before { content: "\\F02DC"; } +.mdi-close::before { content: "\\F0156"; } +`; + const map = parseIconCodepoints(css); + assert.equal(map.size, 2); + assert.equal(map.get('mdi-home'), 'F02DC'); + assert.equal(map.get('mdi-close'), 'F0156'); +}); + +test('parseIconCodepoints handles CSS with semicolons inside braces', () => { + const css = `.mdi-check::before { content: "\\F012C"; }`; + const map = parseIconCodepoints(css); + assert.equal(map.get('mdi-check'), 'F012C'); +}); + +test('parseIconCodepoints returns empty map for non-matching CSS', () => { + const css = `.some-other-class { color: red; }`; + const map = parseIconCodepoints(css); + assert.equal(map.size, 0); +}); + +// ── resolveUsedIcons ──────────────────────────────────────────────────────── + +test('resolveUsedIcons separates resolved and missing icons', () => { + const usedIcons = new Set(['mdi-home', 'mdi-close', 'mdi-nonexistent']); + const iconMap = new Map([ + ['mdi-home', 'F02DC'], + ['mdi-close', 'F0156'], + ]); + + const { resolvedIcons, missingIcons, subsetChars } = resolveUsedIcons(usedIcons, iconMap); + + assert.ok(resolvedIcons.includes('mdi-home')); + assert.ok(resolvedIcons.includes('mdi-close')); + assert.equal(resolvedIcons.length, 2); + + assert.deepEqual(missingIcons, ['mdi-nonexistent']); + + // Verify subsetChars contains correct Unicode characters + assert.equal(subsetChars.length, 2); + assert.equal(subsetChars[0], String.fromCodePoint(0xF02DC)); + assert.equal(subsetChars[1], String.fromCodePoint(0xF0156)); +}); + +test('resolveUsedIcons returns all missing when iconMap is empty', () => { + const usedIcons = new Set(['mdi-home']); + const iconMap = new Map(); + + const { resolvedIcons, missingIcons, subsetChars } = resolveUsedIcons(usedIcons, iconMap); + assert.equal(resolvedIcons.length, 0); + assert.deepEqual(missingIcons, ['mdi-home']); + assert.equal(subsetChars.length, 0); +}); + +// ── extractUtilityCss ─────────────────────────────────────────────────────── + +test('extractUtilityCss removes icon definitions and keeps utility rules', () => { + const css = ` +@font-face { + font-family: "Material Design Icons"; + src: url("../fonts/materialdesignicons-webfont.woff2") format("woff2"); +} + +.mdi:before, +.mdi-set { + display: inline-block; + font: normal normal normal 24px/1 "Material Design Icons"; +} + +.mdi-home::before { content: "\\F02DC"; } +.mdi-close::before { content: "\\F0156"; } + +.mdi-spin:before { + animation: mdi-spin 2s infinite linear; +} + +.mdi-18px.mdi-set, .mdi-18px.mdi:before { + font-size: 18px; +} +/*# sourceMappingURL=materialdesignicons.css.map */ +`; + + const result = extractUtilityCss(css, ICON_CLASS_PATTERN); + + // Should NOT contain icon definitions + assert.ok(!result.includes('mdi-home')); + assert.ok(!result.includes('mdi-close')); + + // Should NOT contain @font-face + assert.ok(!result.includes('@font-face')); + + // Should NOT contain base .mdi rules + assert.ok(!result.includes('display: inline-block')); + + // Should NOT contain source map + assert.ok(!result.includes('sourceMappingURL')); + + // SHOULD contain utility classes + assert.ok(result.includes('mdi-spin')); + assert.ok(result.includes('mdi-18px')); +}); + +test('extractUtilityCss returns empty string when only icon defs exist', () => { + const css = ` +@font-face { font-family: "MDI"; src: url("font.woff2"); } +.mdi:before, .mdi-set { display: inline-block; } +.mdi-home::before { content: "\\F02DC"; } +`; + + const result = extractUtilityCss(css, ICON_CLASS_PATTERN); + assert.equal(result, ''); +}); + +test('extractUtilityCss handles empty CSS input', () => { + const result = extractUtilityCss('', ICON_CLASS_PATTERN); + assert.equal(result, ''); +}); + +// ── ICON_CLASS_PATTERN ────────────────────────────────────────────────────── + +test('ICON_CLASS_PATTERN matches standard MDI icon definitions', () => { + const css = `.mdi-home::before { content: "\\F02DC"; }`; + const matches = [...css.matchAll(ICON_CLASS_PATTERN)]; + assert.equal(matches.length, 1); + assert.equal(matches[0][1], 'mdi-home'); + assert.equal(matches[0][2], 'F02DC'); +}); + +test('ICON_CLASS_PATTERN does not match non-icon classes', () => { + const css = `.some-class::before { content: "hello"; }`; + const matches = [...css.matchAll(ICON_CLASS_PATTERN)]; + assert.equal(matches.length, 0); +}); diff --git a/dashboard/vite.config.ts b/dashboard/vite.config.ts index b53e0310df..45a62d94f1 100644 --- a/dashboard/vite.config.ts +++ b/dashboard/vite.config.ts @@ -2,10 +2,26 @@ import { fileURLToPath, URL } from 'url'; import { defineConfig } from 'vite'; import vue from '@vitejs/plugin-vue'; import vuetify from 'vite-plugin-vuetify'; +import webfontDl from 'vite-plugin-webfont-dl'; +// @ts-ignore — .mjs not in TS project scope; Vite resolves this at runtime +import { runMdiSubset } from './scripts/subset-mdi-font.mjs'; + +// Vite plugin: run MDI icon font subsetting (build only) +function mdiSubset() { + return { + name: 'vite-plugin-mdi-subset', + async buildStart() { + console.log('\n🔧 Running MDI icon font subsetting...'); + await runMdiSubset(); + }, + }; +} // https://vitejs.dev/config/ -export default defineConfig({ +export default defineConfig(({ command }) => ({ plugins: [ + // Only run MDI subsetting during production builds, skip in dev server + ...(command === 'build' ? [mdiSubset()] : []), vue({ template: { compilerOptions: { @@ -15,7 +31,8 @@ export default defineConfig({ }), vuetify({ autoImport: true - }) + }), + webfontDl() ], resolve: { alias: { @@ -47,4 +64,4 @@ export default defineConfig({ } } } -}); +}));