From e8b2e7571da7de5fcbf0a45b7d7fab1b2a4cc7db Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 30 Jun 2026 08:54:46 +0000 Subject: [PATCH 1/3] fix(configurator): back-port panel fixes from WP plugin to framework Syncs configurator UI fixes that were made in the SLASHED-Plugins repo back to the framework source so they survive future sync-core.mjs runs. Changes ported from plugin commits (c6b0c8a..HEAD in admin-app/src/): - ClampField: redesign modular scale ratio to independent mobile/desktop selectors + always-visible custom inputs; remove shared activeRatioValue and onRatioPreset props - SliderRow: fix raw input clearing while typing; keep raw mode visible while editing; reset on blur when empty instead of on every keystroke - BordersPanel / SpacingPanel / TypographyPanel: drop activeRatioValue + onRatioPreset call-sites after ClampField API change; fix parseFloat returning 0 for falsy values (use explicit isNaN guard) - persistence: fix derived/ov merge order in injectLivePreview so user overrides win; add same-origin guard to wpSave REST URL Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_0124dAf4BHiKD1uESapMZdnK --- .../src/components/inputs/ClampField.svelte | 83 +++++++++++-------- .../src/components/inputs/SliderRow.svelte | 22 ++--- .../src/components/panels/BordersPanel.svelte | 6 +- .../src/components/panels/SpacingPanel.svelte | 6 -- .../components/panels/TypographyPanel.svelte | 6 -- configurator/src/lib/persistence.ts | 7 +- 6 files changed, 70 insertions(+), 60 deletions(-) diff --git a/configurator/src/components/inputs/ClampField.svelte b/configurator/src/components/inputs/ClampField.svelte index dfab4656..a1d88bd6 100644 --- a/configurator/src/components/inputs/ClampField.svelte +++ b/configurator/src/components/inputs/ClampField.svelte @@ -22,14 +22,14 @@ onMaxChange, overridden = false, onReset, - // Optional ratio block + // Optional ratio block — independent mobile (ratioMin) / desktop (ratioMax) + // modular-scale ratios. Each breakpoint gets its own preset selector and an + // always-visible custom number input. ratioPresets, - activeRatioValue, ratioMin, ratioMax, ratioMin_bound = 1.05, ratioMax_bound = 1.8, - onRatioPreset, onRatioMinChange, onRatioMaxChange, // Optional preview renderer (e.g. type sample / spacing block) @@ -49,12 +49,10 @@ overridden?: boolean; onReset?: () => void; ratioPresets?: RatioPreset[]; - activeRatioValue?: number | undefined; ratioMin?: number; ratioMax?: number; ratioMin_bound?: number; ratioMax_bound?: number; - onRatioPreset?: (v: number) => void; onRatioMinChange?: (v: number) => void; onRatioMaxChange?: (v: number) => void; previewKind?: "none" | "type" | "space"; @@ -65,7 +63,18 @@ } let minPct = $derived(((minValue - min) / (max - min)) * 100); let maxPct = $derived(((maxValue - min) / (max - min)) * 100); - let customRatioOpen = $derived(!!ratioPresets && activeRatioValue === undefined); + + function clampRatio(v: number) { + return Math.min(ratioMax_bound, Math.max(ratioMin_bound, v)); + } + // Which preset (if any) each breakpoint currently matches, computed per side + // so mobile and desktop highlight independently. + let activeRatioMin = $derived( + ratioPresets?.find((p) => ratioMin !== undefined && Math.abs(p.value - ratioMin) < 0.0015)?.value + ); + let activeRatioMax = $derived( + ratioPresets?.find((p) => ratioMax !== undefined && Math.abs(p.value - ratioMax) < 0.0015)?.value + );
@@ -128,38 +137,44 @@
{/if} - + {#if ratioPresets}
-
Modular scale ratio
-
- {#each ratioPresets as p (p.value)} - +
Modular scale ratio
+
+ {#each [ + { side: minLabel, value: ratioMin, active: activeRatioMin, onChange: onRatioMinChange }, + { side: maxLabel, value: ratioMax, active: activeRatioMax, onChange: onRatioMaxChange }, + ] as row (row.side)} +
+ {row.side} + + { const n = parseFloat((e.target as HTMLInputElement).value); if (Number.isFinite(n)) row.onChange?.(clampRatio(n)); }} + class="w-16 shrink-0 bg-white/5 border border-white/10 rounded text-[11px] font-mono text-slate-200 text-right px-1.5 py-0.5 focus:outline-none focus:border-indigo-500" + /> +
{/each}
- {#if customRatioOpen} -
- - -
- {/if}
{/if} diff --git a/configurator/src/components/inputs/SliderRow.svelte b/configurator/src/components/inputs/SliderRow.svelte index 35a6a730..778ed41a 100644 --- a/configurator/src/components/inputs/SliderRow.svelte +++ b/configurator/src/components/inputs/SliderRow.svelte @@ -22,16 +22,17 @@ let userRawMode = $state(false); + // 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(currentRaw ?? ''); + let isEditing = $state(false); + // Auto raw mode when override value is a CSS expression let isRawOverride = $derived( !!currentRaw && /^(var|calc|clamp|min|max|env)\(/.test(currentRaw.trim()) ); - let showRaw = $derived(!!(rawDefault && onRawSet && (userRawMode || isRawOverride))); - - // Local draft so typing is never interrupted by re-renders - let rawDraft = $state(currentRaw ?? ''); - let isEditing = $state(false); + let showRaw = $derived(!!(rawDefault && onRawSet && (userRawMode || isRawOverride || isEditing))); // Sync draft from external currentRaw changes only when user is not actively editing $effect(() => { @@ -75,15 +76,14 @@ value={rawDraft} placeholder={rawDefault} onfocus={() => { isEditing = true; }} - onblur={() => { isEditing = false; }} + onblur={() => { + isEditing = false; + if (!rawDraft.trim()) onReset(); + }} oninput={(e) => { rawDraft = (e.target as HTMLInputElement).value; const v = rawDraft.trim(); - if (!v) { - onReset(); - } else if (onRawSet) { - onRawSet(v); - } + if (v && onRawSet) onRawSet(v); }} class="w-full bg-white/5 border border-white/10 rounded px-2 py-1.5 text-[11px] font-mono text-slate-300 placeholder:text-slate-500 focus:outline-none focus:border-indigo-500" /> diff --git a/configurator/src/components/panels/BordersPanel.svelte b/configurator/src/components/panels/BordersPanel.svelte index d7f2fcc8..292efcb9 100644 --- a/configurator/src/components/panels/BordersPanel.svelte +++ b/configurator/src/components/panels/BordersPanel.svelte @@ -72,14 +72,16 @@ const raw = overrides[r.name]; if (!raw) return r.default; if (/^(var|calc|clamp)\(/.test(raw.trim())) return r.default; - return parseFloat(raw) || r.default; + const parsed = parseFloat(raw); + return isNaN(parsed) ? r.default : parsed; } function getComponentVal(t: typeof COMPONENT_TOKENS[0]): number { const raw = overrides[t.token]; if (!raw) return t.default; if (/^(var|calc|clamp)\(/.test(raw.trim())) return t.default; - return parseFloat(raw) || t.default; + const parsed = parseFloat(raw); + return isNaN(parsed) ? t.default : parsed; } diff --git a/configurator/src/components/panels/SpacingPanel.svelte b/configurator/src/components/panels/SpacingPanel.svelte index cc754e16..8a2945f1 100644 --- a/configurator/src/components/panels/SpacingPanel.svelte +++ b/configurator/src/components/panels/SpacingPanel.svelte @@ -48,10 +48,6 @@ let vwMin = $derived(num("--sf-fluid-min-vw", 22.5)); let vwMax = $derived(num("--sf-fluid-max-vw", 90)); - let activeRatio = $derived(RATIO_PRESETS.find( - (p) => Math.abs(p.value - ratioMin) < 0.0015 && Math.abs(p.value - ratioMax) < 0.0015 - )); - let activeDensity = $derived(DENSITY_PRESETS.find((d) => Object.entries(d.patch).every(([k, v]) => v === null ? !(k in overrides) : overrides[k] === v @@ -184,10 +180,8 @@ onMinChange={(v) => onSet("--sf-space-base-min", String(v))} onMaxChange={(v) => onSet("--sf-space-base-max", String(v))} ratioPresets={RATIO_PRESETS} - activeRatioValue={activeRatio?.value} ratioMin={ratioMin} ratioMax={ratioMax} ratioMin_bound={1.1} ratioMax_bound={1.8} - onRatioPreset={(v) => onBulkChange({ "--sf-space-ratio-min": String(v), "--sf-space-ratio-max": String(v) })} onRatioMinChange={(v) => onSet("--sf-space-ratio-min", String(v))} onRatioMaxChange={(v) => onSet("--sf-space-ratio-max", String(v))} /> diff --git a/configurator/src/components/panels/TypographyPanel.svelte b/configurator/src/components/panels/TypographyPanel.svelte index 6fc95439..8d1fe2da 100644 --- a/configurator/src/components/panels/TypographyPanel.svelte +++ b/configurator/src/components/panels/TypographyPanel.svelte @@ -161,10 +161,6 @@ let vwMin = $derived(num("--sf-fluid-min-vw", 22.5)); let vwMax = $derived(num("--sf-fluid-max-vw", 90)); - let activeRatio = $derived(RATIO_PRESETS.find( - (p) => Math.abs(p.value - ratioMin) < 0.0015 && Math.abs(p.value - ratioMax) < 0.0015 - )); - let showFontFamilies = $state(false); let showPerType = $state(false); let showBodyText = $state(false); @@ -757,10 +753,8 @@ onMinChange={(v) => onSet("--sf-text-base-min", String(v))} onMaxChange={(v) => onSet("--sf-text-base-max", String(v))} ratioPresets={RATIO_PRESETS} - activeRatioValue={activeRatio?.value} ratioMin={ratioMin} ratioMax={ratioMax} ratioMin_bound={1.05} ratioMax_bound={1.8} - onRatioPreset={(v) => onBulkChange({ "--sf-text-ratio-min": String(v), "--sf-text-ratio-max": String(v) })} onRatioMinChange={(v) => onSet("--sf-text-ratio-min", String(v))} onRatioMaxChange={(v) => onSet("--sf-text-ratio-max", String(v))} /> diff --git a/configurator/src/lib/persistence.ts b/configurator/src/lib/persistence.ts index b89fde33..f5d6fd9a 100644 --- a/configurator/src/lib/persistence.ts +++ b/configurator/src/lib/persistence.ts @@ -72,7 +72,12 @@ export function injectLivePreview(ov: Record): void { } async function wpSave(rest: { url?: string; nonce?: string }, ov: Record): Promise { - const url = (rest.url ?? "").replace(/\/$/, "") + "/tokens/overrides"; + const base = (rest.url ?? "").replace(/\/$/, ""); + const parsed = new URL(base + "/tokens/overrides", window.location.href); + if (parsed.origin !== window.location.origin) { + throw new Error("slashed: REST URL must be same-origin"); + } + const url = parsed.href; const res = await fetch(url, { method: "POST", credentials: "same-origin", From 023f26dd5601fa028bced15c4ff30065176e5f08 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 30 Jun 2026 09:01:18 +0000 Subject: [PATCH 2/3] =?UTF-8?q?feat(configurator):=20port=20PR=20#109=20?= =?UTF-8?q?=E2=80=94=20derived=20scale=20tokens=20for=20radius/border/moti?= =?UTF-8?q?on?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ports all configurator-side changes from SLASHED-Plugins PR #109 to the framework so they survive future sync-core.mjs runs. persistence.ts — full computeDerivedOverrides() engine (exported): - Adds radius/border/motion derived-token computation alongside existing text/space logic (RADIUS_STEPS, BORDER_WIDTH_STEPS, DURATION_STEPS) - injectLivePreview() now injects derived tokens as unlayered :root CSS so scale knobs are reflected immediately without waiting for a save PreviewPanel.svelte — withDerivedOverrides() applied in all three preview CSS injection paths (buildIframeHTML + both live-update calls) so preview iframes reflect scale knobs immediately App.svelte — untrack() wraps lastSavedOverrides initialiser to avoid spurious dirty-state on mount CommandPalette.svelte — tabindex="-1" on dialog root for keyboard focus ColorInput.svelte — replace autofocus with tick()+bind:this $effect to avoid SSR warnings and race conditions on re-mount SliderRow.svelte — init rawDraft as '' instead of currentRaw to prevent stale value flashing before the sync $effect runs ShadowsPanel.svelte — aria-label on glow toggle button Not ported (plugin-specific): AppOverlay.svelte changes, class-css-generator.php Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_0124dAf4BHiKD1uESapMZdnK --- configurator/src/App.svelte | 4 +- .../src/components/CommandPalette.svelte | 1 + .../src/components/inputs/ColorInput.svelte | 10 +- .../src/components/inputs/SliderRow.svelte | 2 +- .../src/components/panels/ShadowsPanel.svelte | 1 + .../src/components/shell/PreviewPanel.svelte | 12 +- configurator/src/lib/persistence.ts | 152 +++++++++++++++++- 7 files changed, 174 insertions(+), 8 deletions(-) diff --git a/configurator/src/App.svelte b/configurator/src/App.svelte index 49aca477..888f8b1c 100644 --- a/configurator/src/App.svelte +++ b/configurator/src/App.svelte @@ -1,5 +1,5 @@