diff --git a/configurator/src/App.svelte b/configurator/src/App.svelte index cfac27d9..38ec8a9d 100644 --- a/configurator/src/App.svelte +++ b/configurator/src/App.svelte @@ -11,7 +11,7 @@ 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 { parseImport, summarizeImport, type ImportReport } from './lib/importOverrides'; import tokensRaw from './data/api-index.generated.json'; import CommandPalette from './components/CommandPalette.svelte'; @@ -85,6 +85,19 @@ // Transient feedback after an import (the old flow failed silently). let importStatus = $state(null); let importStatusTimer: ReturnType | null = null; + // Parsed-but-not-yet-applied import, awaiting a Merge/Replace choice. + let importPreview = $state<{ overrides: Record; report: ImportReport; filename: string } | null>(null); + + function navigateTo(domainId: string, token?: string) { + domain = domainId; + if (token) { + focusNonce += 1; + focusRequest = { token, nonce: focusNonce }; + } else { + focusRequest = null; + } + mobileView = "controls"; + } // On narrow screens the controls panel and the live preview can't both fit, so // we show one at a time and let the user fold between them (desktop shows both). let mobileView = $state<"controls" | "preview">("controls"); @@ -246,6 +259,14 @@ importStatusTimer = setTimeout(() => { importStatus = null; importStatusTimer = null; }, 6000); } + function applyImport(mode: "merge" | "replace") { + if (!importPreview) return; + const { overrides: imported, report } = importPreview; + setOverrides(mode === "replace" ? { ...imported } : (prev) => ({ ...prev, ...imported })); + showImportStatus(`${mode === "replace" ? "Replaced" : "Merged"} · ${summarizeImport(report)}`); + importPreview = null; + } + function handleImport() { const input = document.createElement("input"); input.type = "file"; @@ -260,10 +281,13 @@ // 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 })); + // Nothing usable → just report; otherwise open the Merge/Replace chooser + // so the user reviews what will change before it's applied. + if (report.malformed || Object.keys(imported).length === 0) { + showImportStatus(summarizeImport(report)); + return; } - showImportStatus(summarizeImport(report)); + importPreview = { overrides: imported, report, filename: file.name }; }; reader.onerror = () => { showImportStatus("Import failed — the selected file could not be read."); }; reader.readAsText(file); @@ -375,7 +399,7 @@ @@ -432,7 +456,7 @@ onReset={handleReset} onBulkChange={handleBulkChange} onApplyTheme={handleApplyTheme} - onSelectDomain={(d) => { domain = d; focusRequest = null; }} + onSelectDomain={(d) => { navigateTo(d); }} onResetAll={handleResetAll} /> @@ -480,7 +504,7 @@ { domain = d; navDrawerOpen = false; mobileView = "controls"; focusRequest = null; }} + onSelect={(d) => { domain = d; navDrawerOpen = false; mobileView = "controls"; }} overridesByDomain={domainBadges} /> @@ -492,17 +516,61 @@ {/if} + + {#if importPreview} + {@const r = importPreview.report} + + {/if} + {#if showPalette} { - domain = d; - if (token) { focusNonce += 1; focusRequest = { token, nonce: focusNonce }; } - else focusRequest = null; - // On mobile, deep-linking into a token means we want the controls side. - mobileView = "controls"; + navigateTo(d, token); }} onClose={() => { showPalette = false; }} /> diff --git a/configurator/src/components/CommandPalette.svelte b/configurator/src/components/CommandPalette.svelte index d78767d6..5b6c3684 100644 --- a/configurator/src/components/CommandPalette.svelte +++ b/configurator/src/components/CommandPalette.svelte @@ -24,11 +24,17 @@ // Navigation destinations — makes this a real command palette (jump to any // panel/tool), not just a token search. + const NAV_ALIASES: Record = { + borders: ["border", "radius", "shape"], shadows: ["shadow", "depth"], + effects: ["effect"], wcag: ["accessibility", "contrast"], + themes: ["theme", "preset"], setup: ["install", "export"], + cheatsheet: ["reference", "classes"], misc: ["system"], + }; const NAV = [ "home", "colors", "typography", "spacing", "borders", "motion", "layout", "depth", "macros", "components", "misc", "changes", "wcag", "themes", "setup", "cheatsheet", - ].map((id) => ({ id, label: DOMAIN_LABELS[id] ?? id })); + ].map((id) => ({ id, label: DOMAIN_LABELS[id] ?? id, terms: [id, ...(NAV_ALIASES[id] ?? [])] })); type Result = | { kind: "nav"; id: string; label: string } @@ -38,7 +44,9 @@ const q = query.trim().toLowerCase(); // Navigation matches (all destinations when empty, so the palette is useful // before typing). - const nav: Result[] = (q ? NAV.filter((n) => n.label.toLowerCase().includes(q)) : NAV) + const nav: Result[] = (q ? NAV.filter((n) => + n.label.toLowerCase().includes(q) || n.terms.some((term) => term.includes(q)) + ) : NAV) .map((n) => ({ kind: "nav", id: n.id, label: n.label })); const tokenMatches: Result[] = []; diff --git a/configurator/src/components/shell/StudioHeader.svelte b/configurator/src/components/shell/StudioHeader.svelte index 3755130b..8184b3dd 100644 --- a/configurator/src/components/shell/StudioHeader.svelte +++ b/configurator/src/components/shell/StudioHeader.svelte @@ -80,6 +80,7 @@