Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions configurator/src/App.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts">
import { onMount } from 'svelte';
import { onMount, untrack } from 'svelte';
import type { PreviewTemplate, PresetTheme, SlashedToken } from './types';
import StudioHeader from './components/shell/StudioHeader.svelte';
import SidebarNav from './components/shell/SidebarNav.svelte';
Expand Down Expand Up @@ -56,7 +56,7 @@
}

// Save state — hasPendingChanges is derived so undo/redo update it automatically.
let lastSavedOverrides = $state<Record<string, string>>({ ...overrides });
let lastSavedOverrides = $state<Record<string, string>>(untrack(() => ({ ...overrides })));
let saveState = $state<'idle' | 'saving' | 'saved'>('idle');
let hasPendingChanges = $derived(!shallowEq(overrides, lastSavedOverrides));
let saveStateTimer: ReturnType<typeof setTimeout> | null = null;
Expand Down
1 change: 1 addition & 0 deletions configurator/src/components/CommandPalette.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
role="dialog"
aria-modal="true"
aria-label="Token search"
tabindex="-1"
onmousedown={(e) => { if (e.target === e.currentTarget) onClose(); }}
>
<!-- Panel -->
Expand Down
83 changes: 49 additions & 34 deletions configurator/src/components/inputs/ClampField.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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";
Expand All @@ -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
);
</script>

<div class={`rounded-xl border p-3 ${overridden ? "bg-indigo-500/8 border-indigo-500/25" : "bg-white/4 border-white/8"}`}>
Expand Down Expand Up @@ -128,38 +137,44 @@
</div>
{/if}

<!-- Ratio presets (optional) -->
<!-- Modular-scale ratio (optional) — independent mobile & desktop ratios.
Each breakpoint has its own preset dropdown plus an always-visible custom
number input, so a preset can be picked and then fine-tuned per side. -->
{#if ratioPresets}
<div class="mt-3 pt-3 border-t border-white/6">
<div class="text-[9px] font-semibold text-slate-500 mb-1.5">Modular scale ratio</div>
<div class="grid grid-cols-2 gap-1">
{#each ratioPresets as p (p.value)}
<button
onclick={() => onRatioPreset?.(p.value)}
class={`px-2 py-1 rounded-md text-[10px] border transition-all cursor-pointer text-left ${
activeRatioValue === p.value
? "bg-indigo-500/15 border-indigo-500/40 text-indigo-200"
: "border-white/8 text-slate-400 hover:bg-white/5 hover:text-slate-200"
}`}
>{p.label}</button>
<div class="text-[9px] font-semibold text-slate-500 mb-2">Modular scale ratio</div>
<div class="space-y-2">
{#each [
{ side: minLabel, value: ratioMin, active: activeRatioMin, onChange: onRatioMinChange },
{ side: maxLabel, value: ratioMax, active: activeRatioMax, onChange: onRatioMaxChange },
] as row (row.side)}
<div class="flex items-center gap-2">
<span class="text-[9px] text-slate-500 w-14 shrink-0">{row.side}</span>
<select
aria-label={`${row.side} modular scale ratio preset`}
value={row.active !== undefined ? String(row.active) : ""}
onchange={(e) => {
const v = parseFloat((e.target as HTMLSelectElement).value);
if (Number.isFinite(v)) row.onChange?.(clampRatio(v));
}}
class="flex-1 min-w-0 bg-white/5 border border-white/10 rounded text-[10px] text-slate-200 px-1.5 py-1 focus:outline-none focus:border-indigo-500 cursor-pointer"
>
{#if row.active === undefined}
<option value="" style="background:#16161e;">Custom</option>
{/if}
{#each ratioPresets as p (p.value)}
<option value={String(p.value)} style="background:#16161e;">{p.label}</option>
{/each}
</select>
<input
aria-label={`${row.side} modular scale custom ratio`}
type="number" min={ratioMin_bound} max={ratioMax_bound} step={0.001} value={row.value}
onchange={(e) => { 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"
/>
</div>
{/each}
</div>
{#if customRatioOpen}
<div class="grid grid-cols-2 gap-2 mt-2 pl-2 border-l border-amber-500/25">
<label class="flex items-center gap-1.5">
<span class="text-[9px] text-slate-500 shrink-0">{minLabel.toLowerCase()}</span>
<input type="number" min={ratioMin_bound} max={ratioMax_bound} step={0.001} value={ratioMin}
onchange={(e) => { const n = parseFloat((e.target as HTMLInputElement).value); if (Number.isFinite(n)) onRatioMinChange?.(Math.min(ratioMax_bound, Math.max(ratioMin_bound, n))); }}
class="w-full 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" />
</label>
<label class="flex items-center gap-1.5">
<span class="text-[9px] text-slate-500 shrink-0">{maxLabel.toLowerCase()}</span>
<input type="number" min={ratioMin_bound} max={ratioMax_bound} step={0.001} value={ratioMax}
onchange={(e) => { const n = parseFloat((e.target as HTMLInputElement).value); if (Number.isFinite(n)) onRatioMaxChange?.(Math.min(ratioMax_bound, Math.max(ratioMin_bound, n))); }}
class="w-full 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" />
</label>
</div>
{/if}
</div>
{/if}

Expand Down
10 changes: 9 additions & 1 deletion configurator/src/components/inputs/ColorInput.svelte
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<script lang="ts">
import { tick } from 'svelte';
import { resolveColor, previewVersion } from '../../lib/previewResolver.svelte';

let {
Expand All @@ -19,6 +20,13 @@

let editing = $state(false);
let cancelBlur = $state(false);
let editInput = $state<HTMLInputElement | null>(null);

$effect(() => {
if (editing) {
tick().then(() => editInput?.focus());
}
});

// Bare "--token" is a UI shorthand; normalize to "var(--token)" before resolving or storing.
function normalize(v: string): string {
Expand Down Expand Up @@ -69,8 +77,8 @@
<!-- Text display / editable input -->
{#if editing}
<input
bind:this={editInput}
value={value}
autofocus
onblur={(e) => {
if (cancelBlur) { cancelBlur = false; editing = false; return; }
const v = normalize((e.target as HTMLInputElement).value);
Expand Down
22 changes: 11 additions & 11 deletions configurator/src/components/inputs/SliderRow.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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('');
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(() => {
Expand Down Expand Up @@ -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"
/>
Expand Down
6 changes: 4 additions & 2 deletions configurator/src/components/panels/BordersPanel.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
</script>

Expand Down
1 change: 1 addition & 0 deletions configurator/src/components/panels/ShadowsPanel.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@
<div class="text-[9px] text-slate-500 mt-0.5">Ambient glow on elevated surfaces</div>
</div>
<button
aria-label={glowDisabled ? "Enable shadow glow" : "Disable shadow glow"}
onclick={() => {
if (glowDisabled) onReset("--sf-shadow-glow");
else onSet("--sf-shadow-glow", "none");
Expand Down
6 changes: 0 additions & 6 deletions configurator/src/components/panels/SpacingPanel.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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))}
/>
Expand Down
6 changes: 0 additions & 6 deletions configurator/src/components/panels/TypographyPanel.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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))}
/>
Expand Down
13 changes: 10 additions & 3 deletions configurator/src/components/shell/PreviewPanel.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { Sun, Moon, Smartphone, Tablet, Monitor, RefreshCw, ExternalLink, Columns2 } from 'lucide-svelte';
import type { PreviewTemplate } from '../../types';
import { fa } from '../../lib/codec';
import { computeDerivedOverrides } from '../../lib/persistence';
import { registerPreviewDoc, bumpPreviewVersion } from '../../lib/previewResolver.svelte';
import { lumlockerPreview } from '../../lib/lumlockerPreview.svelte';
// Import the built framework CSS at Vite compile time — always in sync with badges/.
Expand All @@ -21,6 +22,12 @@
onTemplateChange: (t: PreviewTemplate) => void;
} = $props();

function withDerivedOverrides(ov: Record<string, string>): Record<string, string> {
const reduceMotion = typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
const derived = computeDerivedOverrides(ov, { reduceMotion });
return Object.keys(derived).length > 0 ? { ...derived, ...ov } : ov;
}

const TEMPLATES: { id: PreviewTemplate; label: string }[] = [
{ id: "marketing", label: "Marketing" },
{ id: "docs", label: "Docs" },
Expand Down Expand Up @@ -468,7 +475,7 @@
template: PreviewTemplate,
frameworkCSS: string,
): string {
const css = fa(ov, { mode: "root", banner: false });
const css = fa(withDerivedOverrides(ov), { mode: "root", banner: false });
const motionCSS =
motion === "slow"
? "*, *::before, *::after { transition-duration: 200% !important; animation-duration: 200% !important; }"
Expand Down Expand Up @@ -549,7 +556,7 @@ ${BODIES[template]}

const styleEl = doc.getElementById("slashed-overrides");
if (styleEl) {
styleEl.textContent = fa(_ov, { mode: "root", banner: false });
styleEl.textContent = fa(withDerivedOverrides(_ov), { mode: "root", banner: false });
}

injectFontsIntoDoc(doc, _ov);
Expand All @@ -572,7 +579,7 @@ ${BODIES[template]}
const _lightCount = splitLightLoadCount;
const _darkCount = splitDarkLoadCount;
const _lock = lumlockerPreview.value;
const css = fa(_ov, { mode: "root", banner: false });
const css = fa(withDerivedOverrides(_ov), { mode: "root", banner: false });

const applyLock = (doc: Document) => {
if (_lock) doc.documentElement.setAttribute("data-lumlocker", "");
Expand Down
Loading