diff --git a/configurator/src/App.svelte b/configurator/src/App.svelte index 33bde295..c1dcfdaf 100644 --- a/configurator/src/App.svelte +++ b/configurator/src/App.svelte @@ -11,10 +11,12 @@ import { generateCSS } from './lib/codec'; import { loadInitialOverrides, injectLivePreview, saveOverrides, hasWpBoot } from './lib/persistence'; import { domainOf } from './lib/domains'; + import { parseImport, summarizeImport } from './lib/importOverrides'; import tokensRaw from './data/api-index.generated.json'; import CommandPalette from './components/CommandPalette.svelte'; const ALL_TOKENS = ((tokensRaw as ApiIndex).tokens ?? tokensRaw) as SlashedToken[]; + const LIVE_TOKEN_NAMES = new Set(ALL_TOKENS.map((t) => t.name)); const DOMAIN_LABELS: Record = { home: "Home", colors: "Colors", typography: "Typography", spacing: "Spacing", @@ -54,6 +56,9 @@ // be re-focused (a second search for it still scrolls/highlights). let focusRequest = $state<{ token: string; nonce: number } | null>(null); let focusNonce = 0; + // Transient feedback after an import (the old flow failed silently). + let importStatus = $state(null); + let importStatusTimer: ReturnType | null = null; function navigateTo(domainId: string, token?: string) { domain = domainId; @@ -210,6 +215,12 @@ overrides = next; } + function showImportStatus(msg: string) { + importStatus = msg; + if (importStatusTimer) clearTimeout(importStatusTimer); + importStatusTimer = setTimeout(() => { importStatus = null; importStatusTimer = null; }, 6000); + } + function handleImport() { const input = document.createElement("input"); input.type = "file"; @@ -220,32 +231,16 @@ const reader = new FileReader(); reader.onload = (ev) => { const text = ev.target?.result as string; - if (!text) return; - if (file.name.endsWith(".json")) { - try { - const data = JSON.parse(text); - if (data !== null && typeof data === "object" && !Array.isArray(data)) { - // Restrict to real token-name keys too, not just string values — an - // imported JSON file is untrusted input and its keys end up as - // object property names downstream (CodeQL: remote-property-injection). - const safe = Object.fromEntries( - Object.entries(data as Record).filter( - ([k, v]) => typeof v === "string" && /^--sf-[\w-]+$/.test(k) - ) - ) as Record; - if (Object.keys(safe).length > 0) setOverrides(safe); - } - } catch {} - } else { - const parsed: Record = {}; - const re = /(--sf-[\w-]+)\s*:\s*([^;]+);/g; - let m; - while ((m = re.exec(text)) !== null) { - parsed[m[1].trim()] = m[2].trim(); - } - if (Object.keys(parsed).length > 0) setOverrides((prev) => ({ ...prev, ...parsed })); + if (!text) { showImportStatus("Nothing imported — the selected file is empty."); return; } + // One validated pipeline for both CSS and JSON: sanitised, migrated, + // merged (non-destructive), and always reported — no more silent no-ops. + const { overrides: imported, report } = parseImport(text, file.name, LIVE_TOKEN_NAMES); + if (Object.keys(imported).length > 0) { + setOverrides((prev) => ({ ...prev, ...imported })); } + showImportStatus(summarizeImport(report)); }; + reader.onerror = () => { showImportStatus("Import failed — the selected file could not be read."); }; reader.readAsText(file); }; input.click(); @@ -285,6 +280,7 @@ return () => { window.removeEventListener("keydown", handler); if (saveStateTimer) clearTimeout(saveStateTimer); + if (importStatusTimer) clearTimeout(importStatusTimer); }; }); @@ -322,6 +318,14 @@ onOpenSearch={() => { showPalette = true; }} /> + + {#if importStatus} +
+ {importStatus} + +
+ {/if} + diff --git a/configurator/src/components/shell/PreviewPanel.svelte b/configurator/src/components/shell/PreviewPanel.svelte index 18503e50..bbdfe847 100644 --- a/configurator/src/components/shell/PreviewPanel.svelte +++ b/configurator/src/components/shell/PreviewPanel.svelte @@ -258,8 +258,19 @@ ${buildTab(template)}
- -
+ + +