diff --git a/configurator/src/App.svelte b/configurator/src/App.svelte index 33bde295..38f054ce 100644 --- a/configurator/src/App.svelte +++ b/configurator/src/App.svelte @@ -11,15 +11,17 @@ 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", - layout: "Layout", borders: "Shape", shadows: "Shadows", motion: "Motion", - effects: "Effects", macros: "Macros", misc: "Misc", components: "Components", + layout: "Layout", borders: "Shape", depth: "Depth", motion: "Motion", + macros: "Macros", misc: "Misc", components: "Components", changes: "Changes", themes: "Presets", wcag: "Accessibility", setup: "Install & export", cheatsheet: "Reference", }; @@ -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/CommandPalette.svelte b/configurator/src/components/CommandPalette.svelte index 5406e1fb..38ab1aaf 100644 --- a/configurator/src/components/CommandPalette.svelte +++ b/configurator/src/components/CommandPalette.svelte @@ -16,8 +16,8 @@ const DOMAIN_LABELS: Record = { home: "Home", colors: "Colors", typography: "Typography", spacing: "Spacing", - layout: "Layout", borders: "Shape", shadows: "Shadows", motion: "Motion", - effects: "Effects", macros: "Macros", misc: "Misc", components: "Components", + layout: "Layout", borders: "Shape", depth: "Depth", motion: "Motion", + macros: "Macros", misc: "Misc", components: "Components", changes: "Changes", wcag: "Accessibility", themes: "Presets", setup: "Install & export", cheatsheet: "Reference", }; @@ -32,7 +32,7 @@ }; const NAV = [ "home", "colors", "typography", "spacing", "borders", "motion", - "layout", "shadows", "effects", "macros", "components", "misc", + "layout", "depth", "macros", "components", "misc", "changes", "wcag", "themes", "setup", "cheatsheet", ].map((id) => ({ id, label: DOMAIN_LABELS[id] ?? id, terms: [id, ...(NAV_ALIASES[id] ?? [])] })); diff --git a/configurator/src/components/DomainPanel.svelte b/configurator/src/components/DomainPanel.svelte index d6cc67d4..1884bff0 100644 --- a/configurator/src/components/DomainPanel.svelte +++ b/configurator/src/components/DomainPanel.svelte @@ -8,9 +8,8 @@ import SpacingPanel from './panels/SpacingPanel.svelte'; import LayoutPanel from './panels/LayoutPanel.svelte'; import BordersPanel from './panels/BordersPanel.svelte'; - import ShadowsPanel from './panels/ShadowsPanel.svelte'; + import DepthPanel from './panels/DepthPanel.svelte'; import MotionPanel from './panels/MotionPanel.svelte'; - import EffectsPanel from './panels/EffectsPanel.svelte'; import MacrosPanel from './panels/MacrosPanel.svelte'; import MiscPanel from './panels/MiscPanel.svelte'; import ComponentsPanel from './panels/ComponentsPanel.svelte'; @@ -20,7 +19,7 @@ import ChangesPanel from './panels/ChangesPanel.svelte'; import GenericTokenPanel from './panels/GenericTokenPanel.svelte'; import AllTokensTab from './panels/AllTokensTab.svelte'; - import WcagPanel from './panels/WcagPanel.svelte'; + import AccessibilityPanel from './panels/AccessibilityPanel.svelte'; let { domain, tokens, overrides, focusToken = null, focusNonce = 0, onSet, onReset, onBulkChange, onApplyTheme, onSelectDomain, onResetAll }: { domain: string; @@ -39,7 +38,7 @@ // Domains that skip the two-tab treatment - const NO_CONTROLS_TAB = new Set(["home", "changes", "themes", "wcag", "setup", "cheatsheet"]); + const NO_CONTROLS_TAB = new Set(["home", "changes", "themes", "setup", "cheatsheet"]); let view = $state<"controls" | "tokens">("controls"); @@ -72,8 +71,6 @@ {:else if domain === "themes"} - {:else if domain === "wcag"} - {:else if domain === "setup"} {:else if domain === "cheatsheet"} @@ -95,18 +92,18 @@ {:else if domain === "borders"} - {:else if domain === "shadows"} - + {:else if domain === "depth"} + {:else if domain === "motion"} - {:else if domain === "effects"} - {:else if domain === "macros"} {:else if domain === "misc"} {:else if domain === "components"} + {:else if domain === "wcag"} + {:else} {/if} diff --git a/configurator/src/components/panels/AccessibilityPanel.svelte b/configurator/src/components/panels/AccessibilityPanel.svelte new file mode 100644 index 00000000..44f517b4 --- /dev/null +++ b/configurator/src/components/panels/AccessibilityPanel.svelte @@ -0,0 +1,124 @@ + + +
+ +
+ onSet("--sf-focus-ring-width", `${v}px`)} + onReset={() => onReset("--sf-focus-ring-width")} + /> + onSet("--sf-focus-ring-offset", `${v}px`)} + onReset={() => onReset("--sf-focus-ring-offset")} + /> +
+
Ring color
+ onSet("--sf-focus-ring-color", v)} + onReset={() => onReset("--sf-focus-ring-color")} + /> +
+
+
Ring style
+
+ {#each BORDER_STYLES as style (style)} + {@const current = overrides["--sf-focus-ring-style"] ?? "solid"} + + {/each} +
+
+ +
+
+ Focus ring · {overrides["--sf-focus-ring-style"] ?? "solid"} +
+
+
+ +
+ + +
+ onSet("--sf-touch-target", `${v}px`)} + onReset={() => onReset("--sf-touch-target")} + rawDefault="2.75rem" + currentRaw={overrides["--sf-touch-target"]} + onRawSet={(v) => onSet("--sf-touch-target", v)} + /> +
+
+

Minimum interactive area — ensures accessibility on touch devices.

+
+
+ +
+ + +
Colour contrast
+ +
diff --git a/configurator/src/components/panels/BordersPanel.svelte b/configurator/src/components/panels/BordersPanel.svelte index f59bc1cc..52c8bbb9 100644 --- a/configurator/src/components/panels/BordersPanel.svelte +++ b/configurator/src/components/panels/BordersPanel.svelte @@ -36,7 +36,6 @@ let showBorderWidths = $state(false); let showLineStyles = $state(false); let showDividers = $state(false); - let showFocusRing = $state(false); let showRadiusScale = $state(false); let showRadiusPreview = $state(false); let showMediaRadius = $state(false); @@ -50,13 +49,10 @@ } let borderScale = $derived(parseNum(overrides["--sf-border-scale"], 1)); - let focusWidth = $derived(parseNum(overrides["--sf-focus-ring-width"], 2, "px")); - let focusOffset = $derived(parseNum(overrides["--sf-focus-ring-offset"], 2, "px")); let dividerWidth = $derived(parseNum(overrides["--sf-divider-width"]?.replace("px",""), 1)); let dividerGap = $derived(parseNum(overrides["--sf-divider-gap"]?.replace("rem",""), 1)); // ~1rem ≈ var(--sf-space-m) at default scale let borderColor = $derived(overrides["--sf-color-border"] ?? ""); let borderStyle = $derived(overrides["--sf-border-style"] ?? "solid"); - let focusRingColor = $derived(overrides["--sf-focus-ring-color"] ?? ""); let dividerColor = $derived(overrides["--sf-divider-color"] ?? ""); let mediaRadius = $derived(parseNum(overrides["--sf-media-radius"]?.replace("rem",""), 0)); @@ -222,63 +218,8 @@
- -
- onSet("--sf-focus-ring-width", `${v}px`)} - onReset={() => onReset("--sf-focus-ring-width")} - /> - onSet("--sf-focus-ring-offset", `${v}px`)} - onReset={() => onReset("--sf-focus-ring-offset")} - /> -
-
Ring color
- onSet("--sf-focus-ring-color", v)} - onReset={() => onReset("--sf-focus-ring-color")} - /> -
-
-
Ring style
-
- {#each BORDER_STYLES as style (style)} - {@const current = overrides["--sf-focus-ring-style"] ?? "solid"} - - {/each} -
-
- -
-
- Focus ring · {overrides["--sf-focus-ring-style"] ?? "solid"} -
-
-
- -
+
Radius
diff --git a/configurator/src/components/panels/ChangesPanel.svelte b/configurator/src/components/panels/ChangesPanel.svelte index f9bfe89f..5c3d991b 100644 --- a/configurator/src/components/panels/ChangesPanel.svelte +++ b/configurator/src/components/panels/ChangesPanel.svelte @@ -17,7 +17,7 @@ const DOMAIN_LABEL: Record = { colors: "Colors", typography: "Typography", spacing: "Spacing", layout: "Layout", - borders: "Borders", shadows: "Shadows", motion: "Motion", effects: "Effects", + borders: "Shape", depth: "Depth", motion: "Motion", macros: "Macros", components: "Components", misc: "Misc", }; diff --git a/configurator/src/components/panels/DepthPanel.svelte b/configurator/src/components/panels/DepthPanel.svelte new file mode 100644 index 00000000..99049b92 --- /dev/null +++ b/configurator/src/components/panels/DepthPanel.svelte @@ -0,0 +1,31 @@ + + +
+
+ Elevation & shadows +
+ + +
+ +
+ Effects +
+ +
diff --git a/configurator/src/components/panels/HomePanel.svelte b/configurator/src/components/panels/HomePanel.svelte index 846a6419..5913d5fe 100644 --- a/configurator/src/components/panels/HomePanel.svelte +++ b/configurator/src/components/panels/HomePanel.svelte @@ -31,8 +31,7 @@ label: "Composition", items: [ { id: "layout", icon: Layout, label: "Layout", desc: "Containers, grids & primitives" }, - { id: "shadows", icon: Layers, label: "Shadows", desc: "Elevation & depth control" }, - { id: "effects", icon: Sparkles, label: "Effects", desc: "Blur, opacity & scrollbars" }, + { id: "depth", icon: Layers, label: "Depth", desc: "Shadows, glow, blur & opacity" }, { id: "macros", icon: Blocks, label: "Macros", desc: "Flow, prose, aspect & scrim" }, { id: "components", icon: Component, label: "Components", desc: "Button & card component tokens" }, { id: "misc", icon: Puzzle, label: "Misc", desc: "Z-index & remaining tokens" }, @@ -42,7 +41,7 @@ label: "Quality", items: [ { id: "changes", icon: ListChecks, label: "Changes", desc: "Review every active override" }, - { id: "wcag", icon: ShieldCheck, label: "Accessibility", desc: "Contrast checker & fixes" }, + { id: "wcag", icon: ShieldCheck, label: "Accessibility", desc: "Focus ring, touch target & contrast" }, ], }, { @@ -60,7 +59,7 @@ // Per-destination override count — the same canonical domainOf() classifier // the sidebar and Reset use, so counts never disagree. `changes` shows the // total; non-token tools show nothing. - const NON_TOKEN_IDS = new Set(["changes", "wcag", "themes", "setup", "cheatsheet"]); + const NON_TOKEN_IDS = new Set(["changes", "themes", "setup", "cheatsheet"]); const TOKEN_DOMAIN_IDS = new Set( GROUPS.flatMap((g) => g.items.map((i) => i.id as string)).filter((id) => !NON_TOKEN_IDS.has(id)), ); diff --git a/configurator/src/components/panels/MiscPanel.svelte b/configurator/src/components/panels/MiscPanel.svelte index bdcde2a8..a281fbf1 100644 --- a/configurator/src/components/panels/MiscPanel.svelte +++ b/configurator/src/components/panels/MiscPanel.svelte @@ -36,13 +36,11 @@ return isNaN(v) ? fallback : v; } - let touchTarget = $derived(parseNum(overrides["--sf-touch-target"], 44, "px")); let zBaseOffset = $derived(parseNum(overrides["--sf-z-base"], 0)); let caretColor = $derived(overrides["--sf-color-caret"] ?? ""); let underlineOffset = $derived(parseNum(overrides["--sf-link-underline-offset"]?.replace("em",""), 0.15)); let underlineThickness = $derived(overrides["--sf-link-underline-thickness"] ?? "auto"); - let showTouchTarget = $state(false); let showZIndex = $state(false); let showTextSelection = $state(false); let showComponentSizes = $state(false); @@ -59,29 +57,8 @@
- -
- onSet("--sf-touch-target", `${v}px`)} - onReset={() => onReset("--sf-touch-target")} - rawDefault="2.75rem" - currentRaw={overrides["--sf-touch-target"]} - onRawSet={(v) => onSet("--sf-touch-target", v)} - /> - -
-
-

Minimum interactive area — ensures accessibility on touch devices.

-
-
- -
+
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)}
- -
+ + +