From 0ab2c58eb4419a9adbe9a1aeb6da1ace05bebb1c Mon Sep 17 00:00:00 2001 From: Peter Meier Date: Wed, 24 Jun 2026 10:44:56 +0200 Subject: [PATCH] =?UTF-8?q?feat(header):=20icon=20nav=20redesign=20+=20mig?= =?UTF-8?q?rate=20icons=20mdi=E2=86=92lucide?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- package-lock.json | 11 + package.json | 1 + scripts/generate-icons.mjs | 146 +++++---- scripts/icon-apply.mjs | 55 ++++ scripts/icon-map.mjs | 104 +++++++ scripts/icon-validate.mjs | 33 ++ src/lib/cms-api.generated.ts | 8 +- src/lib/components/Accordion.svelte | 2 +- src/lib/components/BlogOverview.svelte | 12 +- src/lib/components/Carousel.svelte | 4 +- src/lib/components/Comments.svelte | 14 +- src/lib/components/ContactCard.svelte | 6 +- src/lib/components/EventBadges.svelte | 6 +- src/lib/components/EventMap.svelte | 6 +- src/lib/components/Header.svelte | 111 ++++--- src/lib/components/ImageModal.svelte | 2 +- src/lib/components/InfoAccordion.svelte | 4 +- src/lib/components/Pagination.svelte | 4 +- src/lib/components/PostActions.svelte | 12 +- src/lib/components/PostCard.svelte | 2 +- src/lib/components/QrButton.svelte | 2 +- src/lib/components/QrModal.svelte | 4 +- src/lib/components/Search.svelte | 6 +- src/lib/components/SocialImageModal.svelte | 10 +- src/lib/components/WindAreaPanel.svelte | 28 +- .../components/blocks/AdressbuchBlock.svelte | 24 +- .../components/blocks/CalendarBlock.svelte | 44 +-- .../blocks/CalendarCompactBlock.svelte | 4 +- .../blocks/DeadlineBannerBlock.svelte | 4 +- src/lib/components/blocks/FilesBlock.svelte | 22 +- src/lib/components/blocks/IframeBlock.svelte | 2 +- .../blocks/ImageGalleryBlock.svelte | 12 +- .../blocks/OrganisationsBlock.svelte | 6 +- .../blocks/OrganisationsMapBlock.svelte | 12 +- .../blocks/PostOverviewBlock.svelte | 4 +- .../blocks/SearchableTextBlock.svelte | 12 +- .../blocks/StellingnahmeGeneratorBlock.svelte | 62 ++-- .../components/blocks/StrommixBlock.svelte | 18 +- .../blocks/YoutubeVideoGalleryBlock.svelte | 12 +- src/lib/iconify-lucide-subset.generated.json | 264 ++++++++++++++++ src/lib/iconify-mdi-subset.generated.json | 290 +----------------- src/lib/iconify-offline.ts | 2 + src/lib/ui/Breadcrumbs.svelte | 4 +- src/routes/+error.svelte | 4 +- src/routes/+layout.server.ts | 2 +- .../kalender/termin/[slug]/+page.svelte | 24 +- 46 files changed, 823 insertions(+), 598 deletions(-) create mode 100644 scripts/icon-apply.mjs create mode 100644 scripts/icon-map.mjs create mode 100644 scripts/icon-validate.mjs create mode 100644 src/lib/iconify-lucide-subset.generated.json diff --git a/package-lock.json b/package-lock.json index 7a4ac21..b26cc87 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "sharp": "^0.34.5" }, "devDependencies": { + "@iconify-json/lucide": "^1.2.114", "@iconify-json/mdi": "^1.2.3", "@sveltejs/adapter-node": "^5.2.12", "@sveltejs/kit": "^2.65.0", @@ -610,6 +611,16 @@ "url": "https://github.com/sponsors/ayuhito" } }, + "node_modules/@iconify-json/lucide": { + "version": "1.2.114", + "resolved": "https://registry.npmjs.org/@iconify-json/lucide/-/lucide-1.2.114.tgz", + "integrity": "sha512-NbvH3B1BYo6wBtS7joLi7f2UVQOqK2dtZodMFf3kkBs+Tnh9TkRuy8oVHr1RM8UK6bUtvAXxfNlGAah0CuvPCw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@iconify/types": "*" + } + }, "node_modules/@iconify-json/mdi": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/@iconify-json/mdi/-/mdi-1.2.3.tgz", diff --git a/package.json b/package.json index 3dd42eb..11278f3 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "prepare": "svelte-kit sync || true" }, "devDependencies": { + "@iconify-json/lucide": "^1.2.114", "@iconify-json/mdi": "^1.2.3", "@sveltejs/adapter-node": "^5.2.12", "@sveltejs/kit": "^2.65.0", diff --git a/scripts/generate-icons.mjs b/scripts/generate-icons.mjs index 31d2ef7..b9a285c 100644 --- a/scripts/generate-icons.mjs +++ b/scripts/generate-icons.mjs @@ -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(); diff --git a/scripts/icon-apply.mjs b/scripts/icon-apply.mjs new file mode 100644 index 0000000..295e036 --- /dev/null +++ b/scripts/icon-apply.mjs @@ -0,0 +1,55 @@ +// Rewrite `mdi:NAME` -> `lucide:MAPPED` across frontend src + CMS content. +// KEEP_MDI names are left untouched (brand/language logos with no lucide pendant). +import { MAP, KEEP_MDI } from './icon-map.mjs'; +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const WEB_ROOT = path.resolve(ROOT, '..'); + +const TARGETS = [ + { dir: path.join(ROOT, 'src'), exts: /\.(svelte|ts|tsx|js|mjs)$/ }, + { dir: path.join(WEB_ROOT, 'cms_content', 'windwiderstand'), exts: /\.json5$/ }, +]; +const SKIP_DIR = /(node_modules|\.history|\.search|_legacy|\.svelte-kit)/; + +const stats = { files: 0, repl: 0, kept: {} }; + +function walk(dir, exts, acc) { + for (const e of fs.readdirSync(dir, { withFileTypes: true })) { + if (e.name.startsWith('.') && e.name !== '.') continue; + const full = path.join(dir, e.name); + if (SKIP_DIR.test(full)) continue; + if (e.isDirectory()) walk(full, exts, acc); + else if (exts.test(e.name)) acc.push(full); + } + return acc; +} + +const RE = /mdi:([a-z0-9-]+)/g; +for (const { dir, exts } of TARGETS) { + if (!fs.existsSync(dir)) continue; + for (const file of walk(dir, exts, [])) { + const src = fs.readFileSync(file, 'utf8'); + let n = 0; + const out = src.replace(RE, (m, name) => { + if (KEEP_MDI.has(name)) { + stats.kept[name] = (stats.kept[name] || 0) + 1; + return m; + } + const l = MAP[name]; + if (!l) return m; // unknown -> leave, validator should have caught + n++; + return `lucide:${l}`; + }); + if (n > 0) { + fs.writeFileSync(file, out); + stats.files++; + stats.repl += n; + } + } +} + +console.log(`rewrote ${stats.repl} refs in ${stats.files} files`); +console.log('kept on mdi:', JSON.stringify(stats.kept)); diff --git a/scripts/icon-map.mjs b/scripts/icon-map.mjs new file mode 100644 index 0000000..c45ed8e --- /dev/null +++ b/scripts/icon-map.mjs @@ -0,0 +1,104 @@ +// mdi -> lucide migration map. Names without a sensible lucide pendant +// (brand/language logos) intentionally stay on mdi — listed in KEEP_MDI. +export const KEEP_MDI = new Set([ + 'google', 'whatsapp', 'mastodon', // brands missing in lucide + 'language-javascript', 'language-python', 'language-rust', // language logos +]); + +export const MAP = { + 'account-group-outline': 'users', + 'alert-circle': 'circle-alert', + 'alert-circle-outline': 'circle-alert', + 'arrow-right': 'arrow-right', + 'arrow-top-right': 'arrow-up-right', + 'calendar': 'calendar', + 'calendar-blank': 'calendar', + 'calendar-clock': 'calendar-clock', + 'calendar-export': 'calendar-arrow-up', + 'calendar-month-outline': 'calendar', + 'calendar-plus': 'calendar-plus', + 'calendar-search': 'calendar-search', + 'card-account-details-outline': 'id-card', + 'check': 'check', + 'check-circle': 'circle-check', + 'check-decagram': 'badge-check', + 'chevron-down': 'chevron-down', + 'chevron-left': 'chevron-left', + 'chevron-right': 'chevron-right', + 'chevron-up': 'chevron-up', + 'clock-alert-outline': 'alarm-clock', + 'clock-outline': 'clock', + 'clock-remove-outline': 'clock-fading', + 'close': 'x', + 'cloud': 'cloud', + 'cloud-off-outline': 'cloud-off', + 'code-json': 'braces', + 'comment-outline': 'message-circle', + 'comment-text-outline': 'message-square-text', + 'console': 'terminal', + 'content-copy': 'copy', + 'content-save-outline': 'save', + 'cursor-default-click': 'mouse-pointer-click', + 'cursor-default-click-outline': 'mouse-pointer-click', + 'database-outline': 'database', + 'domain': 'building-2', + 'download': 'download', + 'download-outline': 'download', + 'drag-vertical': 'grip-vertical', + 'email': 'mail', + 'email-outline': 'mail', + 'facebook': 'facebook', + 'file-document-edit-outline': 'file-pen', + 'file-document-outline': 'file-text', + 'file-download': 'file-down', + 'file-excel-box': 'file-spreadsheet', + 'file-image': 'file-image', + 'file-outline': 'file', + 'file-pdf-box': 'file-text', + 'file-powerpoint-box': 'presentation', + 'file-word-box': 'file-text', + 'flag-outline': 'flag', + 'folder-zip': 'folder-archive', + 'format-text-variant-outline': 'type', + 'home': 'house', + 'home-group': 'house', + 'home-outline': 'house', + 'image-outline': 'image', + 'information-outline': 'info', + 'link': 'link', + 'link-variant': 'link', + 'loading': 'loader-circle', + 'magnify': 'search', + 'magnify-plus': 'zoom-in', + 'map': 'map', + 'map-marker': 'map-pin', + 'map-marker-outline': 'map-pin', + 'map-outline': 'map', + 'menu': 'menu', + 'open-in-new': 'external-link', + 'pencil-outline': 'pencil', + 'phone': 'phone', + 'phone-outline': 'phone', + 'play': 'play', + 'printer': 'printer', + 'printer-outline': 'printer', + 'qrcode': 'qr-code', + 'refresh': 'refresh-cw', + 'reply': 'reply', + 'robot-outline': 'bot', + 'rss': 'rss', + 'ruler-square': 'ruler', + 'share-variant': 'share-2', + 'share-variant-outline': 'share-2', + 'shield-check-outline': 'shield-check', + 'sort-alphabetical-ascending': 'arrow-down-a-z', + 'transmission-tower': 'radio-tower', + 'trash-can-outline': 'trash-2', + 'tune': 'sliders-horizontal', + 'tune-variant': 'sliders-horizontal', + 'vector-square': 'square-dashed', + 'video': 'video', + 'wind-turbine': 'fan', + 'xml': 'code', + 'youtube': 'youtube', +}; diff --git a/scripts/icon-validate.mjs b/scripts/icon-validate.mjs new file mode 100644 index 0000000..381a63d --- /dev/null +++ b/scripts/icon-validate.mjs @@ -0,0 +1,33 @@ +import { MAP, KEEP_MDI } from './icon-map.mjs'; +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const lucide = JSON.parse( + fs.readFileSync(path.join(ROOT, 'node_modules/@iconify-json/lucide/icons.json'), 'utf8'), +); +const has = (n) => Boolean(lucide.icons[n] || (lucide.aliases && lucide.aliases[n])); + +const all = fs + .readFileSync(process.env.TMPDIR + '/mdi_all.txt', 'utf8') + .trim() + .split('\n') + .map((s) => s.replace('mdi:', '')); +const cms = ['cloud', 'email', 'facebook', 'map', 'video', 'whatsapp', 'youtube']; +const set = [...new Set([...all, ...cms])].sort(); + +const bad = []; +const unmapped = []; +for (const m of set) { + if (KEEP_MDI.has(m)) continue; + const l = MAP[m]; + if (l === undefined) { + unmapped.push(m); + continue; + } + if (has(l) === false) bad.push(`${m} -> lucide:${l}`); +} +console.log('total mdi:', set.length, '| keep-mdi:', [...KEEP_MDI].filter((k) => set.includes(k)).length); +console.log('UNMAPPED:', unmapped.length ? unmapped.join(', ') : 'none'); +console.log('INVALID lucide names:', bad.length ? '\n ' + bad.join('\n ') : 'none'); diff --git a/src/lib/cms-api.generated.ts b/src/lib/cms-api.generated.ts index 4503247..fc943ff 100644 --- a/src/lib/cms-api.generated.ts +++ b/src/lib/cms-api.generated.ts @@ -4024,7 +4024,7 @@ export interface components { * @default false */ external: boolean; - /** @description Icon identifier (e.g. Iconify). Used for social links; fallback 'mdi:link'. */ + /** @description Icon identifier (e.g. Iconify). Used for social links; fallback 'lucide:link'. */ icon?: string; /** @description Internal key. Not used in app. */ internal?: string; @@ -4065,7 +4065,7 @@ export interface components { * @default false */ external: boolean; - /** @description Icon identifier (e.g. Iconify). Used for social links; fallback 'mdi:link'. */ + /** @description Icon identifier (e.g. Iconify). Used for social links; fallback 'lucide:link'. */ icon?: string; /** @description Internal key. Not used in app. */ internal?: string; @@ -5400,7 +5400,7 @@ export interface components { readonly _slug?: string; /** @description Optional pill color (CSS, e.g. #2d5a27 or rgb()). Admin: color picker + text field. */ color?: string; - /** @description Optional Iconify icon id (e.g. mdi:map-marker, same as link.icon). */ + /** @description Optional Iconify icon id (e.g. lucide:map-pin, same as link.icon). */ icon?: string; /** @description Tag name (e.g. politik, termin). Must be unique. */ name: string; @@ -5410,7 +5410,7 @@ export interface components { _slug: string; /** @description Optional pill color (CSS, e.g. #2d5a27 or rgb()). Admin: color picker + text field. */ color?: string; - /** @description Optional Iconify icon id (e.g. mdi:map-marker, same as link.icon). */ + /** @description Optional Iconify icon id (e.g. lucide:map-pin, same as link.icon). */ icon?: string; /** @description Tag name (e.g. politik, termin). Must be unique. */ name: string; diff --git a/src/lib/components/Accordion.svelte b/src/lib/components/Accordion.svelte index 411580d..7dcc2e4 100644 --- a/src/lib/components/Accordion.svelte +++ b/src/lib/components/Accordion.svelte @@ -34,7 +34,7 @@
- + {label}
diff --git a/src/lib/components/BlogOverview.svelte b/src/lib/components/BlogOverview.svelte index 95c96f8..f2580e2 100644 --- a/src/lib/components/BlogOverview.svelte +++ b/src/lib/components/BlogOverview.svelte @@ -133,7 +133,7 @@ - + - + {/if} @@ -158,7 +158,7 @@ - + - + {/if} @@ -138,7 +138,7 @@ /> {:else} - + {/if}
diff --git a/src/lib/components/SocialImageModal.svelte b/src/lib/components/SocialImageModal.svelte index d0b024e..604af8a 100644 --- a/src/lib/components/SocialImageModal.svelte +++ b/src/lib/components/SocialImageModal.svelte @@ -100,7 +100,7 @@ title="Direktlink zu dieser Vorschau kopieren" aria-label="Direktlink kopieren" > - + {linkCopied ? "Kopiert" : "Direktlink"}
@@ -170,7 +170,7 @@ /> - + Quer herunterladen @@ -189,7 +189,7 @@ /> - + Quadrat herunterladen @@ -208,7 +208,7 @@ /> - + Story herunterladen diff --git a/src/lib/components/WindAreaPanel.svelte b/src/lib/components/WindAreaPanel.svelte index 912c3af..93d59e7 100644 --- a/src/lib/components/WindAreaPanel.svelte +++ b/src/lib/components/WindAreaPanel.svelte @@ -110,7 +110,7 @@ class="shrink-0 rounded-md p-1.5 text-stein-400 hover:bg-stein-100 hover:text-stein-700 transition-colors" aria-label={t(T.windkarte_panel_close)} > - + @@ -121,7 +121,7 @@ {#if area.flaeche_ha != null}
- + {t(T.windkarte_label_flaeche)} {area.flaeche_ha.toFixed(1)} ha @@ -130,13 +130,13 @@ {#if area.anlagen_geplant != null}
- + {t(T.windkarte_label_anlagen_geplant)} {area.anlagen_geplant} {#if area.anlagen_geplant_hinweis} - + {/if}
@@ -144,7 +144,7 @@ {#if area.investor}
- + {t(T.windkarte_label_investor)} {area.investor} @@ -164,7 +164,7 @@ {#if area.gemeinden && area.gemeinden.length > 0}

- + {t(T.windkarte_label_gemeinden)}

@@ -186,7 +186,7 @@ {#if loadingKriterien}
- + {t(T.windkarte_kriterien_loading)}
{:else if kriterien.length > 0} @@ -201,7 +201,7 @@ > {k.title ?? k._slug} @@ -222,7 +222,7 @@ href={area.stellungnahmeUrl} class="flex items-center gap-2 rounded-lg bg-wald-700 px-4 py-3 text-sm font-semibold text-white no-underline hover:bg-wald-800 transition-colors" > - + Einwendung erstellen {/if} @@ -233,7 +233,7 @@ href={stellungnahmeHref(area.stellungnahme)} class="flex items-center gap-2 rounded-lg border border-wald-300 bg-wald-50 px-4 py-3 text-sm font-medium text-wald-700 hover:bg-wald-100 transition-colors" > - + {t(T.windkarte_stellungnahme_link)} {/if} @@ -244,7 +244,7 @@ onclick={copyLink} class="flex flex-1 items-center justify-center gap-1.5 rounded-lg border border-stein-200 px-2 py-2 text-xs font-medium text-stein-600 hover:bg-stein-50 transition-colors whitespace-nowrap" > - + {copied ? t(T.windkarte_action_copied) : t(T.windkarte_action_copy_link)} {#if osmHref(center)} @@ -254,7 +254,7 @@ rel="noopener noreferrer" class="flex flex-1 items-center justify-center gap-1.5 rounded-lg border border-stein-200 px-2 py-2 text-xs font-medium text-stein-600 hover:bg-stein-50 transition-colors whitespace-nowrap" > - + {t(T.windkarte_action_osm)} {/if} @@ -270,7 +270,7 @@ title={`${t(T.windkarte_action_download_geojson)}\n${exportInfo}`} class="flex flex-1 items-center justify-center gap-1.5 rounded-lg border border-stein-200 px-2 py-2 text-xs font-medium text-stein-600 no-underline hover:bg-stein-50 transition-colors whitespace-nowrap" > - + GeoJSON - + KML
diff --git a/src/lib/components/blocks/AdressbuchBlock.svelte b/src/lib/components/blocks/AdressbuchBlock.svelte index 7971286..c360ce4 100644 --- a/src/lib/components/blocks/AdressbuchBlock.svelte +++ b/src/lib/components/blocks/AdressbuchBlock.svelte @@ -122,7 +122,7 @@
- + - +
@@ -224,7 +224,7 @@ onclick={() => downloadVCardAll(filtered())} class="no-print btn-outline btn-sm" > - + Alle exportieren (.vcf)

{filtered().length} Kontakt{filtered().length !== 1 ? "e" : ""}

@@ -266,13 +266,13 @@ onclick={() => downloadVCard(contact)} class="text-stein-300 transition-colors hover:text-wald-600" > - +
{#if contact.note}
- + {contact.note}
{/if} @@ -281,7 +281,7 @@
{#if contact.phone}
- + {contact.phone} - +
{/if} {#if contact.email}
- + {contact.email} - +
{/if} diff --git a/src/lib/components/blocks/CalendarBlock.svelte b/src/lib/components/blocks/CalendarBlock.svelte index e72d0f8..526ede5 100644 --- a/src/lib/components/blocks/CalendarBlock.svelte +++ b/src/lib/components/blocks/CalendarBlock.svelte @@ -635,7 +635,7 @@ class="inline-flex items-center gap-1 tabular-nums" >
@@ -905,7 +905,7 @@ title={t(T.calendar_jump_to_month)} aria-label={t(T.calendar_jump_to_month)} > - + Im Grid
@@ -970,7 +970,7 @@ class="btn-primary btn-sm inline-flex items-center gap-1.5 min-h-[36px]" >
- {/if} diff --git a/src/lib/components/blocks/DeadlineBannerBlock.svelte b/src/lib/components/blocks/DeadlineBannerBlock.svelte index 9895190..2fdfc73 100644 --- a/src/lib/components/blocks/DeadlineBannerBlock.svelte +++ b/src/lib/components/blocks/DeadlineBannerBlock.svelte @@ -130,7 +130,7 @@ class="font-semibold whitespace-nowrap text-gray-900 hover:text-gray-700" > {ctaLabel}