0ab2c58eb4
- Header desktop nav: social icons grouped in a wald-tinted pill with labels, active-state (aria-current + bold), opacity hover (no per-item bg) - Mobile overlay: social links as icon+label rows with active-state - Search button: lucide:search, icon-only - Migrate all icons mdi→lucide app-wide (234 refs); brand/language logos (google, whatsapp, mastodon, telegram, language-*) stay on mdi - generate-icons: emit mdi + lucide subsets; add @iconify-json/lucide - iconify-offline: register lucide collection - scripts/icon-map|validate|apply: reusable migration tooling Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
182 lines
4.9 KiB
JavaScript
182 lines
4.9 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Tree-shake @iconify-json/mdi: scan src/ for `icon="mdi:..."` references and
|
|
* emit a minimal IconifyJSON containing only those icons. Saves ~3 MB in the
|
|
* client bundle vs. shipping the full 7000+ icon collection.
|
|
*
|
|
* Dynamic icons (from CMS data, conditional names) cannot be detected
|
|
* statically — list them under EXTRA_ICONS below. Anything missing falls
|
|
* through to the iconify API at runtime, which adds a network roundtrip but
|
|
* does not break.
|
|
*
|
|
* Run via `npm run generate:icons`. Hooked into `prebuild`.
|
|
*/
|
|
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
const ROOT = path.resolve(__dirname, '..');
|
|
const SRC_DIR = path.join(ROOT, 'src');
|
|
|
|
/**
|
|
* Icons that appear via dynamic bindings (CMS data, conditional expressions)
|
|
* and would otherwise miss the static scan. Add new ones here when editors
|
|
* pick icons that appear blank — better than shipping the full collection.
|
|
*/
|
|
const EXTRA_ICONS = [
|
|
// Brand/Logo-Icons ohne Lucide-Pendant (Editors picken sie als social.icon)
|
|
'google',
|
|
'whatsapp',
|
|
'mastodon',
|
|
'telegram',
|
|
// Sprach-Logos (CodeBlock: dynamische language->icon-Map, kein literal icon="")
|
|
'language-javascript',
|
|
'language-python',
|
|
'language-rust',
|
|
];
|
|
|
|
/**
|
|
* Lucide-Icons aus CMS-Daten (Header social.icon, FilesBlock file types, …) +
|
|
* dynamische Bindings, die der statische Scan nicht findet.
|
|
*/
|
|
const EXTRA_LUCIDE = [
|
|
// Nav / Header social.icon (CMS)
|
|
'calendar-1',
|
|
'contact-round',
|
|
'search',
|
|
'x',
|
|
'cloud',
|
|
'mail',
|
|
'facebook',
|
|
'youtube',
|
|
'map',
|
|
'video',
|
|
'instagram',
|
|
'twitter',
|
|
'linkedin',
|
|
'github',
|
|
'phone',
|
|
// PostActions / Links
|
|
'share-2',
|
|
'link',
|
|
'external-link',
|
|
'arrow-up-right',
|
|
// Kontakt/Verzeichnis
|
|
'id-card',
|
|
'qr-code',
|
|
'arrow-down-a-z',
|
|
'users',
|
|
// File-type icons (FilesBlock: dynamic f.iconName)
|
|
'file',
|
|
'file-text',
|
|
'file-spreadsheet',
|
|
'file-image',
|
|
'file-video',
|
|
'file-music',
|
|
'file-down',
|
|
'presentation',
|
|
// Tag/flag/info/format fallbacks
|
|
'tag',
|
|
'flag',
|
|
'info',
|
|
'type',
|
|
'image',
|
|
// Windkarte / Map / Generator
|
|
'fan',
|
|
'mouse-pointer-click',
|
|
'sliders-horizontal',
|
|
'badge-check',
|
|
];
|
|
|
|
function walk(dir) {
|
|
const out = [];
|
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue;
|
|
const full = path.join(dir, entry.name);
|
|
if (entry.isDirectory()) {
|
|
out.push(...walk(full));
|
|
} else if (/\.(svelte|ts|tsx|js|mjs)$/.test(entry.name)) {
|
|
out.push(full);
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function scanStatic(prefix) {
|
|
const pattern = new RegExp(`icon="${prefix}:([a-z0-9-]+)"`, 'g');
|
|
const found = new Set();
|
|
for (const file of walk(SRC_DIR)) {
|
|
const content = fs.readFileSync(file, 'utf8');
|
|
let m;
|
|
while ((m = pattern.exec(content)) !== null) {
|
|
found.add(m[1]);
|
|
}
|
|
}
|
|
return found;
|
|
}
|
|
|
|
/** Build a tree-shaken subset for one Iconify collection. */
|
|
function buildSubset({ prefix, pkg, outFile, extra }) {
|
|
const iconsJson = path.join(ROOT, 'node_modules', '@iconify-json', pkg, 'icons.json');
|
|
if (!fs.existsSync(iconsJson)) {
|
|
console.error(`[generate-icons] missing ${iconsJson} — run \`npm install\` first.`);
|
|
process.exit(1);
|
|
}
|
|
const collection = JSON.parse(fs.readFileSync(iconsJson, 'utf8'));
|
|
const wanted = new Set([...scanStatic(prefix), ...extra]);
|
|
|
|
const icons = {};
|
|
const aliases = {};
|
|
const missing = [];
|
|
|
|
for (const name of wanted) {
|
|
if (collection.icons[name]) {
|
|
icons[name] = collection.icons[name];
|
|
} else if (collection.aliases?.[name]) {
|
|
aliases[name] = collection.aliases[name];
|
|
const parent = collection.aliases[name].parent;
|
|
if (parent && collection.icons[parent] && !icons[parent]) {
|
|
icons[parent] = collection.icons[parent];
|
|
}
|
|
} else {
|
|
missing.push(name);
|
|
}
|
|
}
|
|
|
|
const subset = {
|
|
prefix: collection.prefix,
|
|
width: collection.width,
|
|
height: collection.height,
|
|
icons,
|
|
...(Object.keys(aliases).length ? { aliases } : {}),
|
|
};
|
|
fs.writeFileSync(outFile, JSON.stringify(subset, null, 2) + '\n');
|
|
|
|
const sizeAfter = fs.statSync(outFile).size;
|
|
console.log(
|
|
`[generate-icons] ${prefix}: ${Object.keys(icons).length} icons + ${Object.keys(aliases).length} aliases → ${path.relative(ROOT, outFile)} (${(sizeAfter / 1024).toFixed(1)} KB)`,
|
|
);
|
|
if (missing.length) {
|
|
console.warn(`[generate-icons] ${prefix}: ${missing.length} not found: ${missing.join(', ')}`);
|
|
}
|
|
}
|
|
|
|
function main() {
|
|
buildSubset({
|
|
prefix: 'mdi',
|
|
pkg: 'mdi',
|
|
outFile: path.join(ROOT, 'src', 'lib', 'iconify-mdi-subset.generated.json'),
|
|
extra: EXTRA_ICONS,
|
|
});
|
|
buildSubset({
|
|
prefix: 'lucide',
|
|
pkg: 'lucide',
|
|
outFile: path.join(ROOT, 'src', 'lib', 'iconify-lucide-subset.generated.json'),
|
|
extra: EXTRA_LUCIDE,
|
|
});
|
|
}
|
|
|
|
main();
|