From 9c148cc6541363a66125a93813b12190d7003765 Mon Sep 17 00:00:00 2001 From: Kiro Agent <244629292+kiro-agent@users.noreply.github.com> Date: Thu, 20 Aug 2026 00:01:25 +0000 Subject: [PATCH 1/4] feat(configurator): merge-vs-replace import chooser with report preview Import applied silently and always merged. Now selecting a file opens a chooser that previews the parsed report before anything changes: - Shows the filename and counts: tokens to import, migrated (renamed), dropped (removed by the framework), unknown in this build, skipped (invalid). - Explains the two actions and lets the user pick: Merge (keep current overrides and add on top) or Replace (discard the current set first); Cancel/Escape aborts. - A malformed or empty file skips the dialog and just reports (nothing to apply). Builds on the existing parseImport/summarizeImport (no parser changes). check, lint, 286 unit tests and shell e2e pass; screenshot + e2e verified the dialog and that Merge keeps existing overrides while Replace discards them. --- configurator/src/App.svelte | 69 ++++++++++++++++++++++++++++++++++--- 1 file changed, 65 insertions(+), 4 deletions(-) diff --git a/configurator/src/App.svelte b/configurator/src/App.svelte index cfac27d9..7439a2df 100644 --- a/configurator/src/App.svelte +++ b/configurator/src/App.svelte @@ -11,7 +11,8 @@ 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 { changedKeys, shouldCoalesce, NO_COALESCE, type CoalesceState } from './lib/history'; + import { parseImport, summarizeImport, type ImportReport } from './lib/importOverrides'; import tokensRaw from './data/api-index.generated.json'; import CommandPalette from './components/CommandPalette.svelte'; @@ -85,6 +86,8 @@ // 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); // 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 +249,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 +271,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); @@ -492,6 +506,53 @@ {/if} + + {#if importPreview} + {@const r = importPreview.report} + + {/if} + {#if showPalette} Date: Thu, 20 Aug 2026 05:05:28 +0000 Subject: [PATCH 2/4] fix(configurator): preserve editor intent and undo steps --- configurator/src/App.svelte | 1 - 1 file changed, 1 deletion(-) diff --git a/configurator/src/App.svelte b/configurator/src/App.svelte index 7439a2df..ab18389e 100644 --- a/configurator/src/App.svelte +++ b/configurator/src/App.svelte @@ -11,7 +11,6 @@ import { generateCSS } from './lib/codec'; import { loadInitialOverrides, injectLivePreview, saveOverrides, hasWpBoot } from './lib/persistence'; import { domainOf } from './lib/domains'; - import { changedKeys, shouldCoalesce, NO_COALESCE, type CoalesceState } from './lib/history'; import { parseImport, summarizeImport, type ImportReport } from './lib/importOverrides'; import tokensRaw from './data/api-index.generated.json'; import CommandPalette from './components/CommandPalette.svelte'; From 68cf0f6618756ec87d8aa3303463b1d35ac7701d Mon Sep 17 00:00:00 2001 From: Kiro Agent <244629292+kiro-agent@users.noreply.github.com> Date: Thu, 20 Aug 2026 05:06:06 +0000 Subject: [PATCH 3/4] fix(configurator): clear stale search focus --- configurator/src/App.svelte | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/configurator/src/App.svelte b/configurator/src/App.svelte index ab18389e..698ba936 100644 --- a/configurator/src/App.svelte +++ b/configurator/src/App.svelte @@ -87,6 +87,17 @@ 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"); From 5b8378b848ceb35dd39f82edb258279b4019e9b6 Mon Sep 17 00:00:00 2001 From: Kiro Agent <244629292+kiro-agent@users.noreply.github.com> Date: Thu, 20 Aug 2026 05:08:18 +0000 Subject: [PATCH 4/4] fix(configurator): complete import and drawer feedback --- configurator/src/App.svelte | 13 +++++-------- configurator/src/components/CommandPalette.svelte | 12 ++++++++++-- .../src/components/shell/StudioHeader.svelte | 1 + 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/configurator/src/App.svelte b/configurator/src/App.svelte index 698ba936..38ec8a9d 100644 --- a/configurator/src/App.svelte +++ b/configurator/src/App.svelte @@ -399,7 +399,7 @@ @@ -456,7 +456,7 @@ onReset={handleReset} onBulkChange={handleBulkChange} onApplyTheme={handleApplyTheme} - onSelectDomain={(d) => { domain = d; focusRequest = null; }} + onSelectDomain={(d) => { navigateTo(d); }} onResetAll={handleResetAll} /> @@ -504,7 +504,7 @@ { domain = d; navDrawerOpen = false; mobileView = "controls"; focusRequest = null; }} + onSelect={(d) => { domain = d; navDrawerOpen = false; mobileView = "controls"; }} overridesByDomain={domainBadges} /> @@ -539,6 +539,7 @@ {#if r.removed > 0}
Dropped (removed by framework){r.removed}
{/if} {#if r.unknown > 0}
Unknown in this build{r.unknown}
{/if} {#if r.invalid.length > 0}
Skipped (invalid){r.invalid.length}
{/if} + {#if r.collisions > 0}
Skipped (migration collision){r.collisions}
{/if}

Merge keeps your current {overridesCount} override{overridesCount !== 1 ? "s" : ""} and adds these on top. Replace discards the current set first. @@ -569,11 +570,7 @@ tokens={ALL_TOKENS} {overrides} onNavigate={(d, token) => { - 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 @@