From 062a863ece1600f8fc2b812932a5deb133ff3f86 Mon Sep 17 00:00:00 2001 From: camera-2018 <40380042+camera-2018@users.noreply.github.com> Date: Sun, 8 Mar 2026 01:20:32 +0800 Subject: [PATCH 1/5] perf(dashboard): subset MDI icon font and self-host Google Fonts --- dashboard/package.json | 7 +- dashboard/scripts/subset-mdi-font.mjs | 184 ++++ .../mdi-subset/materialdesignicons-subset.css | 929 ++++++++++++++++++ .../materialdesignicons-webfont-subset.woff | Bin 0 -> 15936 bytes .../materialdesignicons-webfont-subset.woff2 | Bin 0 -> 12800 bytes dashboard/src/plugins/vuetify.ts | 2 +- dashboard/vite.config.ts | 4 +- 7 files changed, 1121 insertions(+), 5 deletions(-) create mode 100644 dashboard/scripts/subset-mdi-font.mjs create mode 100644 dashboard/src/assets/mdi-subset/materialdesignicons-subset.css create mode 100644 dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff create mode 100644 dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff2 diff --git a/dashboard/package.json b/dashboard/package.json index 9d6b7f8667..225e65d894 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -5,7 +5,8 @@ "author": "CodedThemes", "scripts": { "dev": "vite --host", - "build": "vue-tsc --noEmit && vite build", + "subset-icons": "node scripts/subset-mdi-font.mjs", + "build": "node scripts/subset-mdi-font.mjs && 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/", "preview": "vite preview --port 5050", @@ -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", @@ -64,6 +64,7 @@ "sass-loader": "13.3.2", "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 +75,4 @@ "lodash-es": "4.17.23" } } -} +} \ No newline at end of file diff --git a/dashboard/scripts/subset-mdi-font.mjs b/dashboard/scripts/subset-mdi-font.mjs new file mode 100644 index 0000000000..2f1e5ba024 --- /dev/null +++ b/dashboard/scripts/subset-mdi-font.mjs @@ -0,0 +1,184 @@ +#!/usr/bin/env node +/** + * 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 woff2 font to include only those glyphs (via pyftsubset) + * 4. Generates a minimal CSS file with only the needed icon classes + * 5. Outputs to src/assets/mdi-subset/ + */ +import { execSync } from "child_process"; +import { readFileSync, writeFileSync, readdirSync, statSync } from "fs"; +import { join, resolve, extname } from "path"; + +const ROOT = resolve(import.meta.dirname, ".."); +const SRC = join(ROOT, "src"); +const MDI_CSS = join( + ROOT, + "node_modules/@mdi/font/css/materialdesignicons.css" +); +const MDI_TTF = join( + ROOT, + "node_modules/@mdi/font/fonts/materialdesignicons-webfont.ttf" +); +const OUT_DIR = join(ROOT, "src/assets/mdi-subset"); + +// ── Step 1: Scan source files for mdi-* icon names ────────────────────────── +function collectFiles(dir, exts) { + let files = []; + for (const entry of readdirSync(dir, { withFileTypes: true })) { + const full = join(dir, entry.name); + if (entry.isDirectory() && entry.name !== "node_modules") { + files = files.concat(collectFiles(full, exts)); + } else if (exts.includes(extname(entry.name))) { + files.push(full); + } + } + return files; +} + +const sourceFiles = collectFiles(SRC, [".vue", ".ts", ".js"]); +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)) { + // Exclude pseudo-classes like mdi-set, mdi-spin (utility classes, not icons) + if (!["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"].includes(match[0])) { + usedIcons.add(match[0]); + } + } +} + +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, "utf-8"); +const classPattern = /\.(mdi-[a-z][a-z0-9-]*)::before\s*\{\s*content:\s*"\\([0-9A-Fa-f]+)"/g; +const iconMap = new Map(); // iconName -> unicode codepoint (hex string) +for (const match of mdiCSS.matchAll(classPattern)) { + iconMap.set(match[1], match[2]); +} + +console.log(`📦 MDI font CSS contains ${iconMap.size} icon definitions`); + +// ── Step 3: Resolve codepoints for used icons ─────────────────────────────── +const codepoints = []; +const resolvedIcons = []; +const missingIcons = []; +for (const icon of usedIcons) { + const cp = iconMap.get(icon); + if (cp) { + codepoints.push(`U+${cp}`); + resolvedIcons.push(icon); + } else { + missingIcons.push(icon); + } +} + +if (missingIcons.length > 0) { + console.warn(`⚠️ ${missingIcons.length} icons not found in MDI CSS:`, missingIcons.join(", ")); +} +console.log(`🔍 Resolved ${codepoints.length} codepoints for subsetting`); + +// Always include the base glyph ranges needed for the font to work +// U+F0000-F FFFF is the private use area where MDI places glyphs +const unicodeRange = [ + "U+0020", // space + ...codepoints, +].join(","); + +// ── Step 4: Subset font with pyftsubset ───────────────────────────────────── +const outWoff2 = join(OUT_DIR, "materialdesignicons-webfont-subset.woff2"); +const outWoff = join(OUT_DIR, "materialdesignicons-webfont-subset.woff"); + +const pyftsubsetCmd = [ + "pyftsubset", + `"${MDI_TTF}"`, + `--unicodes="${unicodeRange}"`, + `--output-file="${outWoff2}"`, + "--flavor=woff2", + "--no-hinting", + "--desubroutinize", +].join(" "); +console.log(`🔧 Running pyftsubset for woff2...`); +execSync(pyftsubsetCmd, { stdio: "inherit" }); + +const pyftsubsetWoffCmd = [ + "pyftsubset", + `"${MDI_TTF}"`, + `--unicodes="${unicodeRange}"`, + `--output-file="${outWoff}"`, + "--flavor=woff", + "--no-hinting", + "--desubroutinize", +].join(" "); +console.log(`🔧 Running pyftsubset for woff...`); +execSync(pyftsubsetWoffCmd, { stdio: "inherit" }); + +// ── 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`; +} + +// Add the mdi-spin utility class (used for loading spinners) +css += `/* Utility classes */ +.mdi-spin { + -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); } +} +`; + +const outCSS = join(OUT_DIR, "materialdesignicons-subset.css"); +writeFileSync(outCSS, css); + +// ── Report ────────────────────────────────────────────────────────────────── +const origSize = statSync(MDI_TTF).size; +const subsetWoff2Size = statSync(outWoff2).size; +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!`); 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..56724c3813 --- /dev/null +++ b/dashboard/src/assets/mdi-subset/materialdesignicons-subset.css @@ -0,0 +1,929 @@ +/* Auto-generated MDI subset – 223 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-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-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-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-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-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-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-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-descending::before { + content: "\F04BD"; +} + +.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 */ +.mdi-spin { + -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 0000000000000000000000000000000000000000..44045c3da2568db0f5efd635bcd9e0016cc85afe GIT binary patch literal 15936 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 E11p$8j{pDw literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..1e917b52419cb7e7e496e31740c09b637d3b5bf6 GIT binary patch literal 12800 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 Date: Sun, 8 Mar 2026 01:27:38 +0800 Subject: [PATCH 2/5] perf(dashboard): subset MDI icon font and self-host Google Fonts --- dashboard/package.json | 1 + dashboard/scripts/subset-mdi-font.mjs | 78 ++++++++---------- .../materialdesignicons-webfont-subset.woff | Bin 15936 -> 15928 bytes .../materialdesignicons-webfont-subset.woff2 | Bin 12800 -> 12820 bytes 4 files changed, 36 insertions(+), 43 deletions(-) diff --git a/dashboard/package.json b/dashboard/package.json index 225e65d894..ceffcb3e80 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -62,6 +62,7 @@ "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", diff --git a/dashboard/scripts/subset-mdi-font.mjs b/dashboard/scripts/subset-mdi-font.mjs index 2f1e5ba024..919b6600e1 100644 --- a/dashboard/scripts/subset-mdi-font.mjs +++ b/dashboard/scripts/subset-mdi-font.mjs @@ -5,13 +5,13 @@ * 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 woff2 font to include only those glyphs (via pyftsubset) + * 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/ */ -import { execSync } from "child_process"; -import { readFileSync, writeFileSync, readdirSync, statSync } from "fs"; +import { readFileSync, writeFileSync, readdirSync, statSync, mkdirSync } from "fs"; import { join, resolve, extname } from "path"; +import subsetFont from "subset-font"; const ROOT = resolve(import.meta.dirname, ".."); const SRC = join(ROOT, "src"); @@ -25,6 +25,9 @@ const MDI_TTF = join( ); const OUT_DIR = join(ROOT, "src/assets/mdi-subset"); +// Ensure output directory exists +mkdirSync(OUT_DIR, { recursive: true }); + // ── Step 1: Scan source files for mdi-* icon names ────────────────────────── function collectFiles(dir, exts) { let files = []; @@ -42,14 +45,16 @@ function collectFiles(dir, exts) { const sourceFiles = collectFiles(SRC, [".vue", ".ts", ".js"]); const iconPattern = /mdi-[a-z][a-z0-9-]*/g; const usedIcons = new Set(); +const utilityClasses = 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", +]); for (const file of sourceFiles) { const content = readFileSync(file, "utf-8"); for (const match of content.matchAll(iconPattern)) { - // Exclude pseudo-classes like mdi-set, mdi-spin (utility classes, not icons) - if (!["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"].includes(match[0])) { + if (!utilityClasses.has(match[0])) { usedIcons.add(match[0]); } } @@ -68,14 +73,14 @@ for (const match of mdiCSS.matchAll(classPattern)) { console.log(`📦 MDI font CSS contains ${iconMap.size} icon definitions`); // ── Step 3: Resolve codepoints for used icons ─────────────────────────────── -const codepoints = []; const resolvedIcons = []; const missingIcons = []; +const subsetChars = []; for (const icon of usedIcons) { const cp = iconMap.get(icon); if (cp) { - codepoints.push(`U+${cp}`); resolvedIcons.push(icon); + subsetChars.push(String.fromCodePoint(parseInt(cp, 16))); } else { missingIcons.push(icon); } @@ -84,42 +89,29 @@ for (const icon of usedIcons) { if (missingIcons.length > 0) { console.warn(`⚠️ ${missingIcons.length} icons not found in MDI CSS:`, missingIcons.join(", ")); } -console.log(`🔍 Resolved ${codepoints.length} codepoints for subsetting`); +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); -// Always include the base glyph ranges needed for the font to work -// U+F0000-F FFFF is the private use area where MDI places glyphs -const unicodeRange = [ - "U+0020", // space - ...codepoints, -].join(","); +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", +}); -// ── Step 4: Subset font with pyftsubset ───────────────────────────────────── const outWoff2 = join(OUT_DIR, "materialdesignicons-webfont-subset.woff2"); const outWoff = join(OUT_DIR, "materialdesignicons-webfont-subset.woff"); - -const pyftsubsetCmd = [ - "pyftsubset", - `"${MDI_TTF}"`, - `--unicodes="${unicodeRange}"`, - `--output-file="${outWoff2}"`, - "--flavor=woff2", - "--no-hinting", - "--desubroutinize", -].join(" "); -console.log(`🔧 Running pyftsubset for woff2...`); -execSync(pyftsubsetCmd, { stdio: "inherit" }); - -const pyftsubsetWoffCmd = [ - "pyftsubset", - `"${MDI_TTF}"`, - `--unicodes="${unicodeRange}"`, - `--output-file="${outWoff}"`, - "--flavor=woff", - "--no-hinting", - "--desubroutinize", -].join(" "); -console.log(`🔧 Running pyftsubset for woff...`); -execSync(pyftsubsetWoffCmd, { stdio: "inherit" }); +writeFileSync(outWoff2, woff2Buffer); +writeFileSync(outWoff, woffBuffer); // ── Step 5: Generate subset CSS ───────────────────────────────────────────── let css = `/* Auto-generated MDI subset – ${resolvedIcons.length} icons */ @@ -174,7 +166,7 @@ writeFileSync(outCSS, css); // ── Report ────────────────────────────────────────────────────────────────── const origSize = statSync(MDI_TTF).size; -const subsetWoff2Size = statSync(outWoff2).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`); diff --git a/dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff b/dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff index 44045c3da2568db0f5efd635bcd9e0016cc85afe..9a30e7e301a8442c98591c47a7c90d3d735721b9 100644 GIT binary patch delta 549 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 Date: Sun, 8 Mar 2026 01:35:51 +0800 Subject: [PATCH 3/5] perf(dashboard): subset MDI icon font and self-host Google Fonts --- dashboard/package.json | 2 +- dashboard/scripts/subset-mdi-font.mjs | 288 +++++++++++------- .../mdi-subset/materialdesignicons-subset.css | 245 ++++++++++++++- dashboard/vite.config.ts | 16 + 4 files changed, 429 insertions(+), 122 deletions(-) diff --git a/dashboard/package.json b/dashboard/package.json index ceffcb3e80..d8ce1c7af9 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -6,7 +6,7 @@ "scripts": { "dev": "vite --host", "subset-icons": "node scripts/subset-mdi-font.mjs", - "build": "node scripts/subset-mdi-font.mjs && vue-tsc --noEmit && vite build", + "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/", "preview": "vite preview --port 5050", diff --git a/dashboard/scripts/subset-mdi-font.mjs b/dashboard/scripts/subset-mdi-font.mjs index 919b6600e1..990278861a 100644 --- a/dashboard/scripts/subset-mdi-font.mjs +++ b/dashboard/scripts/subset-mdi-font.mjs @@ -8,113 +8,167 @@ * 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, readdirSync, statSync, mkdirSync } from "fs"; +import { readFileSync, writeFileSync, copyFileSync, readdirSync, statSync, existsSync, mkdirSync } from "fs"; import { join, resolve, extname } from "path"; -import subsetFont from "subset-font"; const ROOT = resolve(import.meta.dirname, ".."); const SRC = join(ROOT, "src"); -const MDI_CSS = join( - ROOT, - "node_modules/@mdi/font/css/materialdesignicons.css" -); -const MDI_TTF = join( - ROOT, - "node_modules/@mdi/font/fonts/materialdesignicons-webfont.ttf" -); +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"); -// Ensure output directory exists mkdirSync(OUT_DIR, { recursive: true }); -// ── Step 1: Scan source files for mdi-* icon names ────────────────────────── -function collectFiles(dir, exts) { - let files = []; - for (const entry of readdirSync(dir, { withFileTypes: true })) { - const full = join(dir, entry.name); - if (entry.isDirectory() && entry.name !== "node_modules") { - files = files.concat(collectFiles(full, exts)); - } else if (exts.includes(extname(entry.name))) { - files.push(full); - } +// ── 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")); } - return files; + if (existsSync(MDI_WOFF_PATH)) { + copyFileSync(MDI_WOFF_PATH, join(OUT_DIR, "materialdesignicons-webfont-subset.woff")); + } + + // Generate a CSS that imports the full font with the subset file names + const mdiCSS = readFileSync(MDI_CSS_PATH, "utf-8"); + const fallbackCSS = mdiCSS + .replace(/url\("\.\.\/fonts\/materialdesignicons-webfont\.(woff2?)\?[^"]*"\)/g, + (_, ext) => `url("./materialdesignicons-webfont-subset.${ext}")`) + .replace(/url\("\.\.\/fonts\/materialdesignicons-webfont\.(eot|ttf)\?[^"]*"\)[^,]*/g, "") + .replace(/src:\s*,/g, "src:") + .replace(/,\s*;/g, ";"); + writeFileSync(join(OUT_DIR, "materialdesignicons-subset.css"), fallbackCSS); + + 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)`); } -const sourceFiles = collectFiles(SRC, [".vue", ".ts", ".js"]); -const iconPattern = /mdi-[a-z][a-z0-9-]*/g; -const usedIcons = new Set(); -const utilityClasses = 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", -]); -for (const file of sourceFiles) { - const content = readFileSync(file, "utf-8"); - for (const match of content.matchAll(iconPattern)) { - if (!utilityClasses.has(match[0])) { - usedIcons.add(match[0]); +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 ──────────────────────── + function collectFiles(dir, exts) { + let files = []; + for (const entry of readdirSync(dir, { withFileTypes: true })) { + const full = join(dir, entry.name); + if (entry.isDirectory() && entry.name !== "node_modules") { + files = files.concat(collectFiles(full, exts)); + } else if (exts.includes(extname(entry.name))) { + files.push(full); + } } + return files; } -} -console.log(`✅ Found ${usedIcons.size} unique mdi-* icons in source files`); + const sourceFiles = collectFiles(SRC, [".vue", ".ts", ".js"]); + const iconPattern = /mdi-[a-z][a-z0-9-]*/g; + const usedIcons = new Set(); + const utilityClasses = 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", + ]); + for (const file of sourceFiles) { + const content = readFileSync(file, "utf-8"); + for (const match of content.matchAll(iconPattern)) { + if (!utilityClasses.has(match[0])) { + usedIcons.add(match[0]); + } + } + } -// ── Step 2: Parse @mdi/font CSS to get codepoints for each icon ───────────── -const mdiCSS = readFileSync(MDI_CSS, "utf-8"); -const classPattern = /\.(mdi-[a-z][a-z0-9-]*)::before\s*\{\s*content:\s*"\\([0-9A-Fa-f]+)"/g; -const iconMap = new Map(); // iconName -> unicode codepoint (hex string) -for (const match of mdiCSS.matchAll(classPattern)) { - iconMap.set(match[1], match[2]); -} + 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 classPattern = /\.(mdi-[a-z][a-z0-9-]*)::before\s*\{\s*content:\s*"\\([0-9A-Fa-f]+)"/g; + const iconMap = new Map(); + for (const match of mdiCSS.matchAll(classPattern)) { + iconMap.set(match[1], match[2]); + } -console.log(`📦 MDI font CSS contains ${iconMap.size} icon definitions`); - -// ── Step 3: Resolve codepoints for used icons ─────────────────────────────── -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); + 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 = []; + 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); + } } -} -if (missingIcons.length > 0) { - console.warn(`⚠️ ${missingIcons.length} icons not found in MDI CSS:`, missingIcons.join(", ")); -} -console.log(`🔍 Resolved ${resolvedIcons.length} codepoints for subsetting`); + 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(""); + // Add space character + subsetChars.push(" "); + const subsetText = subsetChars.join(""); -// ── Step 4: Subset font with subset-font (pure JS/WASM) ──────────────────── -const fontBuffer = readFileSync(MDI_TTF); + // ── 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 woff2...`); + const woff2Buffer = await subsetFont(fontBuffer, subsetText, { + targetFormat: "woff2", + }); -console.log(`🔧 Subsetting font to woff...`); -const woffBuffer = await subsetFont(fontBuffer, subsetText, { - targetFormat: "woff", -}); + console.log(`🔧 Subsetting font to woff...`); + const woffBuffer = await subsetFont(fontBuffer, subsetText, { + targetFormat: "woff", + }); -const outWoff2 = join(OUT_DIR, "materialdesignicons-webfont-subset.woff2"); -const outWoff = join(OUT_DIR, "materialdesignicons-webfont-subset.woff"); -writeFileSync(outWoff2, woff2Buffer); -writeFileSync(outWoff, woffBuffer); + 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 */ + // ── 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 { @@ -138,39 +192,43 @@ let css = `/* Auto-generated MDI subset – ${resolvedIcons.length} icons */ `; -for (const icon of resolvedIcons.sort()) { - const cp = iconMap.get(icon); - css += `.${icon}::before {\n content: "\\${cp}";\n}\n\n`; -} - -// Add the mdi-spin utility class (used for loading spinners) -css += `/* Utility classes */ -.mdi-spin { - -webkit-animation: mdi-spin 2s infinite linear; - animation: mdi-spin 2s infinite linear; -} + for (const icon of resolvedIcons.sort()) { + const cp = iconMap.get(icon); + css += `.${icon}::before {\n content: "\\${cp}";\n}\n\n`; + } -@-webkit-keyframes mdi-spin { - 0% { -webkit-transform: rotate(0deg); transform: rotate(0deg); } - 100% { -webkit-transform: rotate(359deg); transform: rotate(359deg); } -} + // Extract all utility classes from the original MDI CSS + // These are the non-icon classes: size modifiers, color themes, rotation, flip, spin + const utilityStartMarker = ".mdi-18px.mdi-set"; + const utilityStartIndex = mdiCSS.indexOf(utilityStartMarker); + if (utilityStartIndex !== -1) { + const utilityCss = mdiCSS.slice(utilityStartIndex).replace(/\/\*# sourceMappingURL=.*\*\//, "").trim(); + css += `/* Utility classes (extracted from @mdi/font) */\n${utilityCss}\n`; + } else { + console.warn("⚠️ Could not find MDI utility classes in original CSS, skipping"); + } -@keyframes mdi-spin { - 0% { -webkit-transform: rotate(0deg); transform: rotate(0deg); } - 100% { -webkit-transform: rotate(359deg); transform: rotate(359deg); } + 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`); + process.exit(1); + } } -`; - -const outCSS = join(OUT_DIR, "materialdesignicons-subset.css"); -writeFileSync(outCSS, css); - -// ── Report ────────────────────────────────────────────────────────────────── -const origSize = statSync(MDI_TTF).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!`); diff --git a/dashboard/src/assets/mdi-subset/materialdesignicons-subset.css b/dashboard/src/assets/mdi-subset/materialdesignicons-subset.css index 56724c3813..62c05280e2 100644 --- a/dashboard/src/assets/mdi-subset/materialdesignicons-subset.css +++ b/dashboard/src/assets/mdi-subset/materialdesignicons-subset.css @@ -912,18 +912,251 @@ content: "\F05C4"; } -/* Utility classes */ -.mdi-spin { +/* Utility classes (extracted from @mdi/font) */ +.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); } + 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); } + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } } diff --git a/dashboard/vite.config.ts b/dashboard/vite.config.ts index 00b6676faa..e99867acbc 100644 --- a/dashboard/vite.config.ts +++ b/dashboard/vite.config.ts @@ -1,12 +1,28 @@ import { fileURLToPath, URL } from 'url'; import { defineConfig } from 'vite'; +import { execFileSync } from 'child_process'; import vue from '@vitejs/plugin-vue'; import vuetify from 'vite-plugin-vuetify'; import webfontDl from 'vite-plugin-webfont-dl'; +// Vite plugin: run MDI icon font subsetting before each build +function mdiSubset() { + return { + name: 'vite-plugin-mdi-subset', + buildStart() { + console.log('\n🔧 Running MDI icon font subsetting...'); + execFileSync('node', ['scripts/subset-mdi-font.mjs'], { + cwd: fileURLToPath(new URL('.', import.meta.url)), + stdio: 'inherit', + }); + }, + }; +} + // https://vitejs.dev/config/ export default defineConfig({ plugins: [ + mdiSubset(), vue({ template: { compilerOptions: { From 75623ac150a95cf08f027278e901701471d1c181 Mon Sep 17 00:00:00 2001 From: camera-2018 <40380042+camera-2018@users.noreply.github.com> Date: Sun, 15 Mar 2026 21:51:12 +0800 Subject: [PATCH 4/5] perf(dashboard): subset MDI icon font cr fix --- dashboard/scripts/subset-mdi-font.mjs | 339 ++++++++++-------- .../mdi-subset/materialdesignicons-subset.css | 41 ++- .../materialdesignicons-webfont-subset.woff | Bin 15928 -> 16172 bytes .../materialdesignicons-webfont-subset.woff2 | Bin 12820 -> 12996 bytes dashboard/tests/subsetMdiFont.test.mjs | 250 +++++++++++++ dashboard/vite.config.ts | 19 +- 6 files changed, 493 insertions(+), 156 deletions(-) create mode 100644 dashboard/tests/subsetMdiFont.test.mjs diff --git a/dashboard/scripts/subset-mdi-font.mjs b/dashboard/scripts/subset-mdi-font.mjs index 990278861a..ee8ca831f2 100644 --- a/dashboard/scripts/subset-mdi-font.mjs +++ b/dashboard/scripts/subset-mdi-font.mjs @@ -1,4 +1,3 @@ -#!/usr/bin/env node /** * subset-mdi-font.mjs * @@ -14,8 +13,11 @@ */ import { readFileSync, writeFileSync, copyFileSync, readdirSync, statSync, existsSync, mkdirSync } from "fs"; import { join, resolve, extname } from "path"; +import { fileURLToPath } from "url"; -const ROOT = resolve(import.meta.dirname, ".."); +// 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"); @@ -23,103 +25,57 @@ const MDI_WOFF2_PATH = join(ROOT, "node_modules/@mdi/font/fonts/materialdesignic const MDI_WOFF_PATH = join(ROOT, "node_modules/@mdi/font/fonts/materialdesignicons-webfont.woff"); const OUT_DIR = join(ROOT, "src/assets/mdi-subset"); -mkdirSync(OUT_DIR, { recursive: true }); +// 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", +]); -// ── 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")); - } - - // Generate a CSS that imports the full font with the subset file names - const mdiCSS = readFileSync(MDI_CSS_PATH, "utf-8"); - const fallbackCSS = mdiCSS - .replace(/url\("\.\.\/fonts\/materialdesignicons-webfont\.(woff2?)\?[^"]*"\)/g, - (_, ext) => `url("./materialdesignicons-webfont-subset.${ext}")`) - .replace(/url\("\.\.\/fonts\/materialdesignicons-webfont\.(eot|ttf)\?[^"]*"\)[^,]*/g, "") - .replace(/src:\s*,/g, "src:") - .replace(/,\s*;/g, ";"); - writeFileSync(join(OUT_DIR, "materialdesignicons-subset.css"), fallbackCSS); - - 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)`); -} - -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.`); - } +// 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; - // 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.`); - } +// ── Helper functions ──────────────────────────────────────────────────────── - // ── Step 1: Scan source files for mdi-* icon names ──────────────────────── - function collectFiles(dir, exts) { - let files = []; - for (const entry of readdirSync(dir, { withFileTypes: true })) { - const full = join(dir, entry.name); - if (entry.isDirectory() && entry.name !== "node_modules") { - files = files.concat(collectFiles(full, exts)); - } else if (exts.includes(extname(entry.name))) { - files.push(full); - } +/** 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; } - return files; } +} - const sourceFiles = collectFiles(SRC, [".vue", ".ts", ".js"]); +/** 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(); - const utilityClasses = 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", - ]); for (const file of sourceFiles) { const content = readFileSync(file, "utf-8"); for (const match of content.matchAll(iconPattern)) { - if (!utilityClasses.has(match[0])) { + if (!UTILITY_CLASSES.has(match[0])) { usedIcons.add(match[0]); } } } + return usedIcons; +} - 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 classPattern = /\.(mdi-[a-z][a-z0-9-]*)::before\s*\{\s*content:\s*"\\([0-9A-Fa-f]+)"/g; +/** 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(classPattern)) { + for (const match of mdiCSS.matchAll(ICON_CLASS_PATTERN)) { iconMap.set(match[1], match[2]); } + return iconMap; +} - 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 ───────────────────────────── +/** Resolve used icons against the codepoint map, returning resolved/missing/subsetChars. */ +export function resolveUsedIcons(usedIcons, iconMap) { const resolvedIcons = []; const missingIcons = []; const subsetChars = []; @@ -132,43 +88,138 @@ try { 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`); - if (missingIcons.length > 0) { - console.warn(`⚠️ ${missingIcons.length} icons not found in MDI CSS:`, missingIcons.join(", ")); + // Copy original font files + if (existsSync(MDI_WOFF2_PATH)) { + copyFileSync(MDI_WOFF2_PATH, join(OUT_DIR, "materialdesignicons-webfont-subset.woff2")); } - if (resolvedIcons.length === 0) { - throw new Error("No icon codepoints could be resolved. Icon name format may have changed."); + if (existsSync(MDI_WOFF_PATH)) { + copyFileSync(MDI_WOFF_PATH, join(OUT_DIR, "materialdesignicons-webfont-subset.woff")); } - console.log(`🔍 Resolved ${resolvedIcons.length} codepoints for subsetting`); - // Add space character - subsetChars.push(" "); - const subsetText = subsetChars.join(""); + writeFileSync(join(OUT_DIR, "materialdesignicons-subset.css"), buildFallbackCss()); - // ── Step 4: Subset font with subset-font (pure JS/WASM) ────────────────── - const fontBuffer = readFileSync(MDI_TTF_PATH); + 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)`); +} - console.log(`🔧 Subsetting font to woff2...`); - const woff2Buffer = await subsetFont(fontBuffer, subsetText, { - targetFormat: "woff2", - }); +// ── Exported entry point ──────────────────────────────────────────────────── - console.log(`🔧 Subsetting font to woff...`); - const woffBuffer = await subsetFont(fontBuffer, subsetText, { - targetFormat: "woff", - }); +export async function runMdiSubset() { + mkdirSync(OUT_DIR, { recursive: true }); - if (woff2Buffer.length === 0 || woffBuffer.length === 0) { - throw new Error("subset-font produced empty output. Font file may be corrupted."); - } + 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`); - 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 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", + }); - // ── Step 5: Generate subset CSS ─────────────────────────────────────────── - let css = `/* Auto-generated MDI subset – ${resolvedIcons.length} icons */ + 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 { @@ -192,43 +243,49 @@ try { `; - for (const icon of resolvedIcons.sort()) { - const cp = iconMap.get(icon); - css += `.${icon}::before {\n content: "\\${cp}";\n}\n\n`; - } + 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!`); - // Extract all utility classes from the original MDI CSS - // These are the non-icon classes: size modifiers, color themes, rotation, flip, spin - const utilityStartMarker = ".mdi-18px.mdi-set"; - const utilityStartIndex = mdiCSS.indexOf(utilityStartMarker); - if (utilityStartIndex !== -1) { - const utilityCss = mdiCSS.slice(utilityStartIndex).replace(/\/\*# sourceMappingURL=.*\*\//, "").trim(); - css += `/* Utility classes (extracted from @mdi/font) */\n${utilityCss}\n`; - } else { - console.warn("⚠️ Could not find MDI utility classes in original CSS, skipping"); + } 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; + } } +} - 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`); +// ── 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 index 62c05280e2..7f734b0498 100644 --- a/dashboard/src/assets/mdi-subset/materialdesignicons-subset.css +++ b/dashboard/src/assets/mdi-subset/materialdesignicons-subset.css @@ -1,4 +1,4 @@ -/* Auto-generated MDI subset – 223 icons */ +/* Auto-generated MDI subset – 229 icons */ /* Do not edit manually. Run: pnpm run subset-icons */ @font-face { @@ -64,6 +64,10 @@ content: "\F0045"; } +.mdi-arrow-down-thin::before { + content: "\F19B3"; +} + .mdi-arrow-left::before { content: "\F004D"; } @@ -88,6 +92,10 @@ content: "\F0CE1"; } +.mdi-arrow-up-thin::before { + content: "\F19B2"; +} + .mdi-backup-restore::before { content: "\F006F"; } @@ -160,6 +168,10 @@ content: "\F0132"; } +.mdi-checkbox-multiple-marked-outline::before { + content: "\F0139"; +} + .mdi-chevron-double-left::before { content: "\F013D"; } @@ -404,6 +416,10 @@ content: "\F0234"; } +.mdi-filter-variant::before { + content: "\F0236"; +} + .mdi-flash::before { content: "\F0241"; } @@ -524,6 +540,10 @@ content: "\F0309"; } +.mdi-keyboard-outline::before { + content: "\F097B"; +} + .mdi-label::before { content: "\F0315"; } @@ -648,6 +668,10 @@ content: "\F03E4"; } +.mdi-pause-circle-outline::before { + content: "\F03E6"; +} + .mdi-pencil::before { content: "\F03EB"; } @@ -672,6 +696,10 @@ content: "\F040A"; } +.mdi-play-circle-outline::before { + content: "\F040D"; +} + .mdi-plus::before { content: "\F0415"; } @@ -768,10 +796,6 @@ content: "\F04BC"; } -.mdi-sort-descending::before { - content: "\F04BD"; -} - .mdi-sort-variant::before { content: "\F04BF"; } @@ -913,6 +937,13 @@ } /* 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; } diff --git a/dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff b/dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff index 9a30e7e301a8442c98591c47a7c90d3d735721b9..20cd8f5c89e20145b7b71d6d4945f994e33da535 100644 GIT binary patch delta 15857 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 Od0LI)rlCSC>X*dwrU01$e-T0|wQ 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 G00022_Fpgn literal 12820 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 mEP?SR=EtT1U*Ue+rwhdg { + 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 e99867acbc..45a62d94f1 100644 --- a/dashboard/vite.config.ts +++ b/dashboard/vite.config.ts @@ -1,28 +1,27 @@ import { fileURLToPath, URL } from 'url'; import { defineConfig } from 'vite'; -import { execFileSync } from 'child_process'; 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 before each build +// Vite plugin: run MDI icon font subsetting (build only) function mdiSubset() { return { name: 'vite-plugin-mdi-subset', - buildStart() { + async buildStart() { console.log('\n🔧 Running MDI icon font subsetting...'); - execFileSync('node', ['scripts/subset-mdi-font.mjs'], { - cwd: fileURLToPath(new URL('.', import.meta.url)), - stdio: 'inherit', - }); + await runMdiSubset(); }, }; } // https://vitejs.dev/config/ -export default defineConfig({ +export default defineConfig(({ command }) => ({ plugins: [ - mdiSubset(), + // Only run MDI subsetting during production builds, skip in dev server + ...(command === 'build' ? [mdiSubset()] : []), vue({ template: { compilerOptions: { @@ -65,4 +64,4 @@ export default defineConfig({ } } } -}); +})); From c61fda5dd0e03bb948e9775a72d05415f773950a Mon Sep 17 00:00:00 2001 From: camera-2018 <40380042+camera-2018@users.noreply.github.com> Date: Tue, 17 Mar 2026 23:26:23 +0800 Subject: [PATCH 5/5] chore: update lockfile --- dashboard/pnpm-lock.yaml | 176 ++++++++++++++++++++++++++++++++++----- 1 file changed, 155 insertions(+), 21 deletions(-) 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: {}