feat(header): icon nav redesign + migrate icons mdi→lucide
Deploy / verify (push) Successful in 1m26s
Deploy / deploy (push) Successful in 2m26s

- 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>
This commit is contained in:
Peter Meier
2026-06-24 10:44:56 +02:00
parent e434ee3620
commit 0ab2c58eb4
46 changed files with 823 additions and 598 deletions
+82 -64
View File
@@ -19,8 +19,6 @@ 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');
const ICONS_JSON = path.join(ROOT, 'node_modules', '@iconify-json', 'mdi', 'icons.json');
const OUT_FILE = path.join(ROOT, 'src', 'lib', 'iconify-mdi-subset.generated.json');
/**
* Icons that appear via dynamic bindings (CMS data, conditional expressions)
@@ -28,59 +26,70 @@ const OUT_FILE = path.join(ROOT, 'src', 'lib', 'iconify-mdi-subset.generated.jso
* pick icons that appear blank — better than shipping the full collection.
*/
const EXTRA_ICONS = [
// PostActions: dynamic share/copy state
'share-variant-outline',
'link-variant',
// Search: file-type fallback
'file-outline',
// Common social icons editors pick (Header.svelte: social.icon)
// 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',
'youtube',
'linkedin',
'mastodon',
'github',
'email',
'email-outline',
'phone',
'whatsapp',
'telegram',
'import-contacts',
'arrow-top-right',
'open-in-new',
'card-account-details-outline',
'qrcode',
'sort-alphabetical-ascending',
'account-group-outline',
// Common file-type icons (FilesBlock: f.iconName)
'file-pdf-box',
'file-word-box',
'file-excel-box',
// 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',
// Tag/link/banner fallbacks (LinkListBlock, Tags, DeadlineBannerBlock)
'link',
'tag-outline',
'flag-outline',
'information-outline',
// Windkarte: dynamic wind-turbine used in panel
'wind-turbine',
// OrganisationsMapBlock: activate-overlay
'cursor-default-click-outline',
// CalendarBlock: social-image modal button
'image-outline',
// StellingnahmeGenerator: KI-Regeln-Accordion + Detail-Anpassen
'tune-variant',
'tune',
'check-decagram',
// CMS-Content (Social-Link o.ä.): tauchte als Runtime-Fetch in Lighthouse auf
'cloud',
'file-down',
'presentation',
// Tag/flag/info/format fallbacks
'tag',
'flag',
'info',
'type',
'image',
// Windkarte / Map / Generator
'fan',
'mouse-pointer-click',
'sliders-horizontal',
'badge-check',
];
const STATIC_PATTERN = /icon="mdi:([a-z0-9-]+)"/g;
function walk(dir) {
const out = [];
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
@@ -95,27 +104,28 @@ function walk(dir) {
return out;
}
function scanStatic() {
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 = STATIC_PATTERN.exec(content)) !== null) {
while ((m = pattern.exec(content)) !== null) {
found.add(m[1]);
}
}
return found;
}
function main() {
if (!fs.existsSync(ICONS_JSON)) {
console.error(
`[generate-icons] missing ${ICONS_JSON} — run \`npm install\` first.`,
);
/** 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(ICONS_JSON, 'utf8'));
const wanted = new Set([...scanStatic(), ...EXTRA_ICONS]);
const collection = JSON.parse(fs.readFileSync(iconsJson, 'utf8'));
const wanted = new Set([...scanStatic(prefix), ...extra]);
const icons = {};
const aliases = {};
@@ -142,22 +152,30 @@ function main() {
icons,
...(Object.keys(aliases).length ? { aliases } : {}),
};
fs.writeFileSync(outFile, JSON.stringify(subset, null, 2) + '\n');
fs.writeFileSync(OUT_FILE, JSON.stringify(subset, null, 2) + '\n');
const sizeBefore = fs.statSync(ICONS_JSON).size;
const sizeAfter = fs.statSync(OUT_FILE).size;
const sizeAfter = fs.statSync(outFile).size;
console.log(
`[generate-icons] ${Object.keys(icons).length} icons + ${Object.keys(aliases).length} aliases → ${path.relative(ROOT, OUT_FILE)}`,
);
console.log(
`[generate-icons] ${(sizeBefore / 1024 / 1024).toFixed(2)} MB → ${(sizeAfter / 1024).toFixed(1)} KB (${((1 - sizeAfter / sizeBefore) * 100).toFixed(1)}% smaller)`,
`[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] ${missing.length} icon(s) not found in MDI: ${missing.join(', ')}`,
);
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();