From bbbe2a28f5ec074787a6d8dded98c94ad8f2510d Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 28 Jun 2026 07:27:36 +0000 Subject: [PATCH 1/3] fix(configurator): resolve color swatches from preview and accept CSS variables Replace all occurrences across Effects, Misc, Borders, Shadows, and Colors panels with a new ColorInput component that: - Resolves the actual displayed color via resolveColor() from the live preview iframe so swatches show the real computed value instead of hardcoded fallbacks - Accepts CSS variable syntax (var(--token)) as text input; hides the native color picker when a variable is entered since type="color" can't handle them - Clicking the swatch still opens the native picker for concrete color values, seeded with the resolved hex Also fixes TokenRow swatch to use resolveColor() so tokens whose values reference CSS variables (not in the configurator DOM scope) render correctly. Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01EyxX99kENNBAiWeZvLQNBT --- .../src/components/inputs/ColorInput.svelte | 89 +++++++++++++++++++ .../src/components/inputs/TokenRow.svelte | 9 +- .../src/components/panels/BordersPanel.svelte | 53 +++++------ .../src/components/panels/ColorsPanel.svelte | 26 ++---- .../src/components/panels/EffectsPanel.svelte | 37 ++++---- .../src/components/panels/MiscPanel.svelte | 61 ++++++------- .../src/components/panels/ShadowsPanel.svelte | 41 ++++----- 7 files changed, 188 insertions(+), 128 deletions(-) create mode 100644 configurator/src/components/inputs/ColorInput.svelte diff --git a/configurator/src/components/inputs/ColorInput.svelte b/configurator/src/components/inputs/ColorInput.svelte new file mode 100644 index 00000000..e001c0b5 --- /dev/null +++ b/configurator/src/components/inputs/ColorInput.svelte @@ -0,0 +1,89 @@ + + +
+ +
+
+ {#if !isVar} + onSet((e.target as HTMLInputElement).value)} + class="absolute inset-0 w-full h-full opacity-0 cursor-pointer" + tabindex="-1" + /> + {/if} +
+ + + {#if editing} + { + const v = (e.target as HTMLInputElement).value.trim(); + if (!v) onReset(); + else onSet(v); + editing = false; + }} + onkeydown={(e) => { + if (e.key === "Enter") (e.currentTarget as HTMLInputElement).blur(); + if (e.key === "Escape") editing = false; + }} + placeholder={placeholder ?? "default"} + class="flex-1 bg-white/8 border border-indigo-500/50 rounded px-1.5 py-0.5 text-[10px] font-mono text-slate-200 focus:outline-none" + /> + {:else} + + {/if} + + {#if isOverridden} + + {/if} +
diff --git a/configurator/src/components/inputs/TokenRow.svelte b/configurator/src/components/inputs/TokenRow.svelte index a198012b..c7becf3a 100644 --- a/configurator/src/components/inputs/TokenRow.svelte +++ b/configurator/src/components/inputs/TokenRow.svelte @@ -1,5 +1,6 @@ @@ -31,7 +38,7 @@ {#if type === "color"}
{:else}
diff --git a/configurator/src/components/panels/BordersPanel.svelte b/configurator/src/components/panels/BordersPanel.svelte index 750cacf6..b4adda51 100644 --- a/configurator/src/components/panels/BordersPanel.svelte +++ b/configurator/src/components/panels/BordersPanel.svelte @@ -4,6 +4,7 @@ import PowerKnobRow from '../inputs/PowerKnobRow.svelte'; import StylePresetCards from '../inputs/StylePresetCards.svelte'; import SliderRow from '../inputs/SliderRow.svelte'; + import ColorInput from '../inputs/ColorInput.svelte'; let { overrides, onSet, onReset, onBulkChange }: { overrides: Record; @@ -103,18 +104,14 @@ {showBorderColor ? "▲" : "▼"} {#if showBorderColor} -
- onSet("--sf-color-border", (e.target as HTMLInputElement).value)} - class="w-8 h-8 rounded border border-white/10 bg-transparent cursor-pointer" - /> - {borderColor || "auto-derived"} - {#if "--sf-color-border" in overrides} - - {/if} -
+ onSet("--sf-color-border", v)} + onReset={() => onReset("--sf-color-border")} + />

Drives --sf-color-border--strong, --subtle, --translucent automatically.

@@ -242,16 +239,14 @@ {#if showDividers}
Color
- onSet("--sf-divider-color", (e.target as HTMLInputElement).value)} - class="w-7 h-7 rounded border border-white/10 bg-transparent cursor-pointer" + onSet("--sf-divider-color", v)} + onReset={() => onReset("--sf-divider-color")} /> - {dividerColor || "inherits border"} - {#if "--sf-divider-color" in overrides} - - {/if}
Ring color
- onSet("--sf-focus-ring-color", (e.target as HTMLInputElement).value)} - class="w-7 h-7 rounded border border-white/10 bg-transparent cursor-pointer" + onSet("--sf-focus-ring-color", v)} + onReset={() => onReset("--sf-focus-ring-color")} /> - {focusRingColor || "default (action)"} - {#if "--sf-focus-ring-color" in overrides} - - {/if}
diff --git a/configurator/src/components/panels/ColorsPanel.svelte b/configurator/src/components/panels/ColorsPanel.svelte index 6cffc7c0..b473d2bd 100644 --- a/configurator/src/components/panels/ColorsPanel.svelte +++ b/configurator/src/components/panels/ColorsPanel.svelte @@ -7,6 +7,7 @@ import OklchColorDesk from '../inputs/OklchColorDesk.svelte'; import PowerKnobRow from '../inputs/PowerKnobRow.svelte'; import SliderRow from '../inputs/SliderRow.svelte'; + import ColorInput from '../inputs/ColorInput.svelte'; let { tokens, overrides, onSet, onReset, onBulkChange, onSelectDomain }: { tokens: SlashedToken[]; @@ -956,23 +957,14 @@ {@const resolved = paint(`var(${s.name})`, "")}
{s.label}
-
- - onSet(s.name, (e.target as HTMLInputElement).value)} - class="absolute inset-0 w-full h-full opacity-0 cursor-pointer" - /> - {#if s.name in overrides} - - {/if} - - {overrides[s.name] ?? (resolved ? `auto-derived · ${resolved}` : "auto-derived")} - {#if s.name in overrides} - - {/if} -
+ onSet(s.name, v)} + onReset={() => onReset(s.name)} + />
{/each}
diff --git a/configurator/src/components/panels/EffectsPanel.svelte b/configurator/src/components/panels/EffectsPanel.svelte index aa56fc59..41e0e12f 100644 --- a/configurator/src/components/panels/EffectsPanel.svelte +++ b/configurator/src/components/panels/EffectsPanel.svelte @@ -1,5 +1,6 @@ @@ -58,7 +71,7 @@ value={value} autofocus onblur={(e) => { - const v = (e.target as HTMLInputElement).value.trim(); + const v = normalize((e.target as HTMLInputElement).value); if (!v) onReset(); else onSet(v); editing = false; diff --git a/configurator/src/components/inputs/TokenRow.svelte b/configurator/src/components/inputs/TokenRow.svelte index c7becf3a..fb90767d 100644 --- a/configurator/src/components/inputs/TokenRow.svelte +++ b/configurator/src/components/inputs/TokenRow.svelte @@ -22,14 +22,15 @@ let displayValue = $derived(overrideValue ?? token.value); let isOverridden = $derived(overrideValue !== undefined); + let type = $derived(guessType(token)); + let shortName = $derived(token.name.replace("--sf-", "")); function paintSwatch(expr: string): string { void previewVersion.value; return resolveColor(expr) || resolveColor(`var(${token.name})`) || expr; } - let swatchColor = $derived(paintSwatch(displayValue)); - let type = $derived(guessType(token)); - let shortName = $derived(token.name.replace("--sf-", "")); + // Only subscribe to previewVersion for color tokens to avoid unnecessary recomputation. + let swatchColor = $derived(type === "color" ? paintSwatch(displayValue) : "");
From 0c2d8d69af2675a635e2cdcd32bd76f39ca04cc1 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 28 Jun 2026 07:37:02 +0000 Subject: [PATCH 3/3] fix(configurator): escape key in ColorInput should cancel, not commit Setting editing = false on Escape caused the input to unmount, firing blur and committing the draft value. Use a cancelBlur flag to suppress the blur handler when Escape is pressed. Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01EyxX99kENNBAiWeZvLQNBT --- configurator/src/components/inputs/ColorInput.svelte | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/configurator/src/components/inputs/ColorInput.svelte b/configurator/src/components/inputs/ColorInput.svelte index 55836046..87af50b3 100644 --- a/configurator/src/components/inputs/ColorInput.svelte +++ b/configurator/src/components/inputs/ColorInput.svelte @@ -18,6 +18,7 @@ } = $props(); let editing = $state(false); + let cancelBlur = $state(false); // Bare "--token" is a UI shorthand; normalize to "var(--token)" before resolving or storing. function normalize(v: string): string { @@ -71,6 +72,7 @@ value={value} autofocus onblur={(e) => { + if (cancelBlur) { cancelBlur = false; editing = false; return; } const v = normalize((e.target as HTMLInputElement).value); if (!v) onReset(); else onSet(v); @@ -78,7 +80,7 @@ }} onkeydown={(e) => { if (e.key === "Enter") (e.currentTarget as HTMLInputElement).blur(); - if (e.key === "Escape") editing = false; + if (e.key === "Escape") { cancelBlur = true; editing = false; } }} placeholder={placeholder ?? "default"} class="flex-1 bg-white/8 border border-indigo-500/50 rounded px-1.5 py-0.5 text-[10px] font-mono text-slate-200 focus:outline-none"