From 2676c766a222b20ec97db16a7827d603301065aa Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 6 Jul 2026 10:33:09 +0000 Subject: [PATCH 1/2] fix(configurator): show variable-backed knob defaults as pickers, not resolved sliders MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Several dimension knobs (--sf-gap, --sf-gutter, --sf-btn-radius, --sf-touch-target, radius/space/border-width steps, ...) default to another design token via var(...), but SliderRow only surfaced that as a small "default: var(...)" caption while the slider itself showed a bare resolved number — hiding the fact that the value comes from the space/radius/border- width/size scale. SliderRow now renders a dropdown of the token's default plus its sibling scale steps whenever variable info is available, falling back to the numeric slider only behind an explicit "Custom value…" choice (or when the current override doesn't match any known option). Unrecognized CSS expressions still surface as editable raw text instead of a resolved number. Also wires up --sf-touch-target in MiscPanel, which was missing its rawDefault entirely despite aliasing --sf-size-l. configurator/scripts/check-curation.mjs already confirms every public knob has a home domain, so this is a display fix rather than a coverage gap. --- .../src/components/inputs/SliderRow.svelte | 119 ++++++++++++++---- .../src/components/panels/BordersPanel.svelte | 13 +- .../components/panels/ComponentsPanel.svelte | 29 +++-- .../src/components/panels/LayoutPanel.svelte | 7 ++ .../src/components/panels/MacrosPanel.svelte | 7 +- .../src/components/panels/MiscPanel.svelte | 5 + .../src/components/panels/SpacingPanel.svelte | 4 + configurator/src/lib/variableScales.ts | 40 ++++++ 8 files changed, 178 insertions(+), 46 deletions(-) create mode 100644 configurator/src/lib/variableScales.ts diff --git a/configurator/src/components/inputs/SliderRow.svelte b/configurator/src/components/inputs/SliderRow.svelte index 784d9060..d6edaabe 100644 --- a/configurator/src/components/inputs/SliderRow.svelte +++ b/configurator/src/components/inputs/SliderRow.svelte @@ -3,7 +3,7 @@ let { label, help, value, min, max, step, unit, overridden, onChange, onReset, - rawDefault, currentRaw, onRawSet + rawDefault, currentRaw, onRawSet, variableOptions }: { label?: string; help?: string; @@ -18,28 +18,80 @@ rawDefault?: string; currentRaw?: string; onRawSet?: (v: string) => void; + /** Sibling scale steps (e.g. the space or radius scale) offered alongside rawDefault. */ + variableOptions?: { label: string; value: string }[]; } = $props(); - let userRawMode = $state(false); + const CUSTOM = "__sf_custom__"; - // Local draft so typing is never interrupted by re-renders. Declared before - // the derived below so `isEditing` is in scope where `showRaw` reads it. - let rawDraft = $state(''); - let isEditing = $state(false); + function prettyVar(raw: string): string { + const m = raw.match(/^var\(\s*(--sf-[\w-]+)/); + return m ? m[1].replace(/^--sf-/, "") : raw; + } - // Auto raw mode when override value is a CSS expression - let isRawOverride = $derived( + // Dropdown options: the token's own default first, then any sibling scale steps. + let allOptions = $derived.by(() => { + const opts: { label: string; value: string }[] = []; + if (rawDefault) opts.push({ label: `${prettyVar(rawDefault)} (default)`, value: rawDefault }); + for (const o of variableOptions ?? []) { + if (o.value !== rawDefault) opts.push(o); + } + return opts; + }); + + let matchedOption = $derived( + allOptions.find((o) => o.value === (currentRaw ?? rawDefault)) + ); + + let hasVarInfo = $derived(!!rawDefault && !!onRawSet); + + // An override that isn't one of the known options but still looks like a + // CSS expression (var()/calc()/clamp()/…) — surface it as editable text + // rather than silently falling back to a resolved slider number. + let isExprShaped = $derived( !!currentRaw && /^(var|calc|clamp|min|max|env)\(/.test(currentRaw.trim()) ); - let showRaw = $derived(!!(rawDefault && onRawSet && (userRawMode || isRawOverride || isEditing))); + // User-driven view override, layered on top of the value-derived state above. + // 'none' = auto-detect from currentRaw; 'slider'/'raw' = user forced a view + // while outside the picker (via "Custom value…" or the toggle). + let manualView = $state<'none' | 'slider' | 'raw'>('none'); + + // The dropdown is shown whenever the current state maps to a known option + // (the default, or one of the sibling scale steps) and the user hasn't + // explicitly asked to go custom. + let showPicker = $derived( + hasVarInfo && manualView === 'none' && (currentRaw === undefined || !!matchedOption) + ); + + // Outside the picker, decide between the raw-CSS text box and the slider. + let showRawText = $derived( + hasVarInfo && !showPicker && (manualView === 'raw' || (manualView === 'none' && isExprShaped)) + ); + + let selectValue = $derived(matchedOption?.value ?? rawDefault ?? CUSTOM); + + let rawDraft = $state(''); + let isEditingRaw = $state(false); - // Sync draft from external currentRaw changes only when user is not actively editing $effect(() => { - if (!isEditing) { - rawDraft = currentRaw ?? ''; - } + if (!isEditingRaw) rawDraft = currentRaw ?? ''; }); + + function pickOption(v: string) { + if (v === CUSTOM) { + manualView = 'slider'; + return; + } + manualView = 'none'; + if (v === rawDefault) onReset(); + else onRawSet?.(v); + } + + function backToVariable() { + manualView = 'none'; + onReset(); + }
@@ -50,12 +102,12 @@ {/if}
- {#if rawDefault && onRawSet} + {#if hasVarInfo && !showPicker} {/if}
- {#if showRaw && rawDefault} + {#if showPicker} + + {:else if showRawText} { isEditing = true; }} + onfocus={() => { isEditingRaw = true; }} onblur={() => { - isEditing = false; - if (!rawDraft.trim()) onReset(); + isEditingRaw = false; + if (!rawDraft.trim()) backToVariable(); }} oninput={(e) => { rawDraft = (e.target as HTMLInputElement).value; const v = rawDraft.trim(); - if (v && onRawSet) onRawSet(v); + if (v) onRawSet?.(v); }} - class="w-full bg-black/5 dark:bg-white/5 border border-black/10 dark:border-white/10 rounded px-2 py-1.5 text-[11px] font-mono text-slate-700 dark:text-slate-300 placeholder:text-slate-500 focus:outline-none focus:border-indigo-500" + class="w-full bg-black/8 dark:bg-white/8 border border-indigo-500/50 rounded px-2 py-1.5 text-[11px] font-mono text-slate-700 dark:text-slate-300 placeholder:text-slate-500 focus:outline-none" /> {:else} - {#if rawDefault && !overridden} -

default: {rawDefault}

+ {#if hasVarInfo} + {/if} {/if} diff --git a/configurator/src/components/panels/BordersPanel.svelte b/configurator/src/components/panels/BordersPanel.svelte index 8904ad2d..d554100c 100644 --- a/configurator/src/components/panels/BordersPanel.svelte +++ b/configurator/src/components/panels/BordersPanel.svelte @@ -3,6 +3,7 @@ import PowerKnobRow from '../inputs/PowerKnobRow.svelte'; import SliderRow from '../inputs/SliderRow.svelte'; import ColorInput from '../inputs/ColorInput.svelte'; + import { SPACE_SCALE, RADIUS_SCALE, BORDER_WIDTH_SCALE, type VarOption } from '../../lib/variableScales'; let { overrides, onSet, onReset }: { overrides: Record; @@ -23,10 +24,10 @@ { step: "2xl", name: "--sf-radius-2xl", default: 24, max: 80, step_size: 2, rawDefault: "calc(24px * var(--sf-radius-scale))" }, ]; - const COMPONENT_TOKENS = [ - { label: "Field radius", token: "--sf-field-radius", unit: "rem", min: 0, max: 2, step: 0.05, default: 0.5, rawDefault: "var(--sf-radius-m)", help: "--sf-field-radius" }, - { label: "Field padding block", token: "--sf-field-padding-block", unit: "rem", min: 0, max: 1, step: 0.025, default: 0.375, rawDefault: "var(--sf-space-xs)", help: "--sf-field-padding-block" }, - { label: "Field padding inline", token: "--sf-field-padding-inline", unit: "rem", min: 0, max: 2, step: 0.025, default: 0.75, rawDefault: "var(--sf-space-s)", help: "--sf-field-padding-inline" }, + const COMPONENT_TOKENS: Array<{ label: string; token: string; unit: string; min: number; max: number; step: number; default: number; rawDefault: string; help: string; variableOptions: VarOption[] }> = [ + { label: "Field radius", token: "--sf-field-radius", unit: "rem", min: 0, max: 2, step: 0.05, default: 0.5, rawDefault: "var(--sf-radius-m)", help: "--sf-field-radius", variableOptions: RADIUS_SCALE }, + { label: "Field padding block", token: "--sf-field-padding-block", unit: "rem", min: 0, max: 1, step: 0.025, default: 0.375, rawDefault: "var(--sf-space-xs)", help: "--sf-field-padding-block", variableOptions: SPACE_SCALE }, + { label: "Field padding inline", token: "--sf-field-padding-inline", unit: "rem", min: 0, max: 2, step: 0.025, default: 0.75, rawDefault: "var(--sf-space-s)", help: "--sf-field-padding-inline", variableOptions: SPACE_SCALE }, ]; const knobs = KNOBS_BY_DOMAIN["borders"] ?? []; @@ -245,6 +246,7 @@ onChange={(v) => onSet("--sf-divider-width", `${v}px`)} onReset={() => onReset("--sf-divider-width")} rawDefault="var(--sf-border-width-1)" + variableOptions={BORDER_WIDTH_SCALE} currentRaw={overrides["--sf-divider-width"]} onRawSet={(v) => onSet("--sf-divider-width", v)} /> @@ -255,6 +257,7 @@ onChange={(v) => onSet("--sf-divider-gap", `${v}rem`)} onReset={() => onReset("--sf-divider-gap")} rawDefault="var(--sf-space-m)" + variableOptions={SPACE_SCALE} currentRaw={overrides["--sf-divider-gap"]} onRawSet={(v) => onSet("--sf-divider-gap", v)} /> @@ -401,6 +404,7 @@ onChange={(v) => onSet("--sf-media-radius", `${v}rem`)} onReset={() => onReset("--sf-media-radius")} rawDefault="var(--sf-radius-m)" + variableOptions={RADIUS_SCALE} currentRaw={overrides["--sf-media-radius"]} onRawSet={(v) => onSet("--sf-media-radius", v)} /> @@ -435,6 +439,7 @@ onChange={(v) => onSet(t.token, `${v}${t.unit}`)} onReset={() => onReset(t.token)} rawDefault={t.rawDefault} + variableOptions={t.variableOptions} currentRaw={overrides[t.token]} onRawSet={(v) => onSet(t.token, v)} /> diff --git a/configurator/src/components/panels/ComponentsPanel.svelte b/configurator/src/components/panels/ComponentsPanel.svelte index d53d2b97..36e75aa7 100644 --- a/configurator/src/components/panels/ComponentsPanel.svelte +++ b/configurator/src/components/panels/ComponentsPanel.svelte @@ -1,5 +1,6 @@