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
49 changes: 49 additions & 0 deletions configurator/src/components/inputs/AspectRatioInput.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<script lang="ts">
/**
* AspectRatioInput — one shared preset grid for "pick a raw aspect ratio for
* a single token" controls (Macros --sf-aspect, Layout --sf-frame-ratio).
* Clicking the default preset resets the override. Optional live preview box.
*/
type Preset = string | { label: string; value: string };

let { token, value, defaultValue, presets, columns = 3, dense = false, showPreview = false, onSet, onReset }: {
token: string;
value: string;
defaultValue: string;
presets: Preset[];
columns?: number;
dense?: boolean;
showPreview?: boolean;
onSet: (name: string, value: string) => void;
onReset: (name: string) => void;
} = $props();

const COLS: Record<number, string> = {
2: "grid-cols-2", 3: "grid-cols-3", 4: "grid-cols-4", 5: "grid-cols-5", 6: "grid-cols-6",
};
const norm = (s: string) => s.replace(/\s/g, "");
let items = $derived(presets.map((p) => (typeof p === "string" ? { label: p, value: p } : p)));
let colClass = $derived(COLS[columns] ?? "grid-cols-3");
</script>

<div class={`grid ${colClass} ${dense ? "gap-1" : "gap-1.5"}`}>
{#each items as p (p.value)}
<button
onclick={() => p.value === defaultValue ? onReset(token) : onSet(token, p.value)}
class={`${dense ? "py-1 text-[9px]" : "py-1.5 text-[10px]"} rounded-lg border transition-all cursor-pointer font-mono ${
norm(value) === norm(p.value)
? "bg-indigo-500/15 border-indigo-500/40 text-indigo-800 dark:text-indigo-200"
: "border-black/8 dark:border-white/8 text-slate-600 dark:text-slate-400 hover:bg-black/5 dark:hover:bg-white/5 hover:text-slate-800 dark:hover:text-slate-200"
}`}
>{p.label}</button>
{/each}
</div>

{#if showPreview}
<div class="bg-black/4 dark:bg-white/4 rounded-xl border border-black/8 dark:border-white/8 p-3 flex justify-center mt-2">
<div
class="bg-indigo-500/30 border border-indigo-500/30 rounded-lg flex items-center justify-center text-[9px] font-mono text-indigo-800 dark:text-indigo-200"
style={`aspect-ratio:${value};max-height:120px;max-width:100%;width:auto;height:120px`}
>{value}</div>
</div>
{/if}
34 changes: 34 additions & 0 deletions configurator/src/components/inputs/RawTokenRow.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<script lang="ts">
/**
* RawTokenRow — a labelled free-text field for arbitrary CSS token values
* (ratios, shadows, urls, keyframes…) with an inline reset. Replaces the
* hand-rolled "text input + reset" blocks duplicated across panels. Empty
* input (after trim) resets the override; the raw value is stored verbatim
* so intentional leading whitespace (e.g. markers) is preserved.
*/
let { label, labelWidth = "w-20", value, placeholder, overridden, onSet, onReset }: {
label?: string;
labelWidth?: string;
value: string;
placeholder?: string;
overridden: boolean;
onSet: (value: string) => void;
onReset: () => void;
} = $props();
</script>

<div class="flex items-center gap-2">
{#if label}
<span class={`text-[10px] font-semibold text-slate-600 dark:text-slate-400 ${labelWidth} shrink-0`}>{label}</span>
{/if}
<input
type="text"
{value}
{placeholder}
oninput={(e) => { const v = (e.target as HTMLInputElement).value; v.trim() ? onSet(v) : onReset(); }}
class="flex-1 min-w-0 bg-black/5 dark:bg-white/5 border border-black/10 dark:border-white/10 rounded px-1.5 py-1 text-[9px] font-mono text-slate-700 dark:text-slate-300 placeholder:text-slate-600 focus:outline-none focus:border-indigo-500"
/>
{#if overridden}
<button onclick={onReset} class="text-[8px] text-slate-500 hover:text-rose-600 dark:hover:text-rose-400 cursor-pointer shrink-0">reset</button>
{/if}
</div>
36 changes: 36 additions & 0 deletions configurator/src/components/inputs/Section.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<script lang="ts">
/**
* Section — the shared collapsible panel section. Replaces the
* button[aria-expanded] + {#if} header block that was hand-copied ~50 times
* across every panel. `open` is bindable so panels keep their own $state.
* `variant="advanced"` renders the de-emphasised sub-section header used for
* "Advanced" power-knob groups.
*/
import type { Snippet } from 'svelte';

let { title, open = $bindable(false), variant = 'default', spacing = 'space-y-3', children }: {
title: string;
open?: boolean;
variant?: 'default' | 'advanced';
spacing?: string;
children: Snippet;
} = $props();
</script>

<section class={spacing}>
<button
onclick={() => open = !open}
aria-expanded={open}
class={variant === 'advanced'
? "w-full flex items-center justify-between text-slate-400 dark:text-slate-600 hover:text-slate-600 dark:hover:text-slate-400 transition-colors cursor-pointer"
: "w-full flex items-center justify-between cursor-pointer"}
>
<div class={variant === 'advanced'
? "text-[10px] font-semibold uppercase tracking-widest"
: "text-[10px] font-bold text-slate-500 uppercase tracking-widest"}>{title}</div>
<span class={variant === 'advanced' ? "text-[10px]" : "text-[10px] text-slate-500"}>{open ? "▲" : "▼"}</span>
</button>
{#if open}
{@render children()}
{/if}
</section>
29 changes: 29 additions & 0 deletions configurator/src/components/inputs/Toggle.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<script lang="ts">
/**
* Toggle — the shared pill switch used across panels (shadow glow, disable
* motion, boolean state flags…). Previously hand-rolled inline in three
* panels with slightly different markup; this unifies them. Two sizes:
* `md` (default) and `sm`.
*/
let { checked, onToggle, size = "md", ariaLabel, title }: {
checked: boolean;
onToggle: () => void;
size?: "sm" | "md";
ariaLabel?: string;
title?: string;
} = $props();

let track = $derived(size === "sm" ? "w-8 h-4" : "w-9 h-5");
let thumb = $derived(size === "sm" ? "w-3 h-3" : "w-4 h-4");
</script>

<button
type="button"
aria-label={ariaLabel}
aria-pressed={checked}
{title}
onclick={onToggle}
class={`${track} rounded-full transition-colors relative cursor-pointer shrink-0 ${checked ? "bg-indigo-600" : "bg-black/10 dark:bg-white/10"}`}
>
<div class={`absolute top-0.5 ${thumb} bg-white rounded-full shadow transition-transform ${checked ? "translate-x-4" : "translate-x-0.5"}`}></div>
</button>
34 changes: 33 additions & 1 deletion configurator/src/components/inputs/TokenRow.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<script lang="ts">
import type { SlashedToken } from '../../types';
import { resolveColor, previewVersion } from '../../lib/previewResolver.svelte';
import { scaleForValue } from '../../lib/variableScales';

let { token, overrideValue, onSet, onReset }: {
token: SlashedToken;
Expand All @@ -9,8 +10,20 @@
onReset: () => void;
} = $props();

const CUSTOM = "__sf_custom__";
let expanded = $state(false);

// When a token's default is itself a scale variable (e.g. var(--sf-space-m)),
// offer that scale's steps as a dropdown — variable-first, with the raw text
// box available via "Custom…". Mirrors SliderRow's picker for the generic row.
let scaleOpts = $derived(scaleForValue(token.value));
let matchedScale = $derived(
!!scaleOpts && (scaleOpts.some((o) => o.value === (overrideValue ?? token.value)))
);
let showScalePicker = $derived(
!!scaleOpts && !expanded && (overrideValue === undefined || matchedScale)
);

function guessType(t: SlashedToken): "color" | "font" | "number" | "text" {
const n = t.name;
if (n.includes("color") || n.includes("source") || n.includes("-bg") || n.includes("-border")) return "color";
Expand Down Expand Up @@ -54,7 +67,26 @@
{/if}
</div>

{#if expanded}
{#if showScalePicker && scaleOpts}
<select
value={displayValue}
aria-label={`${shortName} value`}
onchange={(e) => {
const v = (e.target as HTMLSelectElement).value;
if (v === CUSTOM) { expanded = true; return; }
if (v === token.value) onReset(); else onSet(v);
}}
class="w-32 shrink-0 bg-black/5 dark:bg-white/5 border border-black/10 dark:border-white/10 rounded px-1.5 py-0.5 text-[10px] font-mono text-slate-700 dark:text-slate-300 focus:outline-none focus:border-indigo-500 cursor-pointer"
>
{#if !scaleOpts.some((o) => o.value === token.value)}
<option value={token.value}>{token.value} (default)</option>
{/if}
{#each scaleOpts as o (o.value)}
<option value={o.value}>{o.label}</option>
{/each}
<option value={CUSTOM}>Custom…</option>
</select>
{:else if expanded}
<input
value={displayValue}
onblur={(e) => {
Expand Down
85 changes: 13 additions & 72 deletions configurator/src/components/panels/BordersPanel.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import PowerKnobRow from '../inputs/PowerKnobRow.svelte';
import SliderRow from '../inputs/SliderRow.svelte';
import ColorInput from '../inputs/ColorInput.svelte';
import Section from '../inputs/Section.svelte';
import { SPACE_SCALE, RADIUS_SCALE, BORDER_WIDTH_SCALE, type VarOption } from '../../lib/variableScales';

let { overrides, onSet, onReset }: {
Expand Down Expand Up @@ -85,16 +86,7 @@
<div class="p-4 space-y-5">

<!-- BORDER COLOR -->
<section class="space-y-3">
<button
onclick={() => { showBorderColor = !showBorderColor; }}
aria-expanded={showBorderColor}
class="w-full flex items-center justify-between cursor-pointer"
>
<div class="text-[10px] font-bold text-slate-500 uppercase tracking-widest">Border color</div>
<span class="text-[10px] text-slate-500">{showBorderColor ? "▲" : "▼"}</span>
</button>
{#if showBorderColor}
<Section title="Border color" bind:open={showBorderColor}>
<ColorInput
token="--sf-color-border"
value={borderColor}
Expand All @@ -106,22 +98,12 @@
<p class="text-[9px] text-slate-400 dark:text-slate-600">
Drives --sf-color-border--strong, --subtle, --translucent automatically.
</p>
{/if}
</section>
</Section>

<div class="h-px bg-black/6 dark:bg-white/6"></div>

<!-- BORDER SCALE -->
<section class="space-y-3">
<button
onclick={() => { showBorderWidths = !showBorderWidths; }}
aria-expanded={showBorderWidths}
class="w-full flex items-center justify-between cursor-pointer"
>
<div class="text-[10px] font-bold text-slate-500 uppercase tracking-widest">Border widths</div>
<span class="text-[10px] text-slate-500">{showBorderWidths ? "▲" : "▼"}</span>
</button>
{#if showBorderWidths}
<Section title="Border widths" bind:open={showBorderWidths}>
<SliderRow
label="Border scale" value={borderScale} min={0} max={4} step={0.25}
help="Multiplier applied to all border widths. 0 hides all borders."
Expand Down Expand Up @@ -159,22 +141,12 @@
</div>
{/each}
</div>
{/if}
</section>
</Section>

<div class="h-px bg-black/6 dark:bg-white/6"></div>

<!-- LINE STYLES -->
<section class="space-y-4">
<button
onclick={() => { showLineStyles = !showLineStyles; }}
aria-expanded={showLineStyles}
class="w-full flex items-center justify-between cursor-pointer"
>
<div class="text-[10px] font-bold text-slate-500 uppercase tracking-widest">Line styles</div>
<span class="text-[10px] text-slate-500">{showLineStyles ? "▲" : "▼"}</span>
</button>
{#if showLineStyles}
<Section title="Line styles" spacing="space-y-4" bind:open={showLineStyles}>
<div class="grid grid-cols-2 gap-3">
{#each [
{ label: "Border", token: "--sf-border-style", default: "solid" },
Expand Down Expand Up @@ -212,22 +184,12 @@
</div>
<span class="text-[9px] font-mono text-slate-400 dark:text-slate-600">{(1 * borderScale).toFixed(1)}px · {borderStyle}</span>
</div>
{/if}
</section>
</Section>

<div class="h-px bg-black/6 dark:bg-white/6"></div>

<!-- DIVIDERS -->
<section class="space-y-3">
<button
onclick={() => { showDividers = !showDividers; }}
aria-expanded={showDividers}
class="w-full flex items-center justify-between cursor-pointer"
>
<div class="text-[10px] font-bold text-slate-500 uppercase tracking-widest">Dividers</div>
<span class="text-[10px] text-slate-500">{showDividers ? "▲" : "▼"}</span>
</button>
{#if showDividers}
<Section title="Dividers" bind:open={showDividers}>
<div class="flex items-center gap-2">
<div class="text-[10px] font-semibold text-slate-600 dark:text-slate-400 w-24 shrink-0">Color</div>
<ColorInput
Expand Down Expand Up @@ -261,22 +223,12 @@
currentRaw={overrides["--sf-divider-gap"]}
onRawSet={(v) => onSet("--sf-divider-gap", v)}
/>
{/if}
</section>
</Section>

<div class="h-px bg-black/6 dark:bg-white/6"></div>

<!-- FOCUS RING -->
<section class="space-y-3">
<button
onclick={() => { showFocusRing = !showFocusRing; }}
aria-expanded={showFocusRing}
class="w-full flex items-center justify-between cursor-pointer"
>
<div class="text-[10px] font-bold text-slate-500 uppercase tracking-widest">Focus ring</div>
<span class="text-[10px] text-slate-500">{showFocusRing ? "▲" : "▼"}</span>
</button>
{#if showFocusRing}
<Section title="Focus ring" bind:open={showFocusRing}>
<SliderRow
label="Ring width" value={focusWidth} min={0} max={6} step={0.5} unit="px"
help="Thickness of the keyboard focus indicator"
Expand Down Expand Up @@ -311,8 +263,7 @@
Focus preview
</div>
</div>
{/if}
</section>
</Section>

<div class="h-px bg-black/6 dark:bg-white/6"></div>

Expand Down Expand Up @@ -383,16 +334,7 @@
<div class="h-px bg-black/6 dark:bg-white/6"></div>

<!-- GLOBAL MEDIA RADIUS -->
<section class="space-y-3">
<button
onclick={() => { showMediaRadius = !showMediaRadius; }}
aria-expanded={showMediaRadius}
class="w-full flex items-center justify-between cursor-pointer"
>
<div class="text-[10px] font-bold text-slate-500 uppercase tracking-widest">Global media radius</div>
<span class="text-[10px] text-slate-500">{showMediaRadius ? "▲" : "▼"}</span>
</button>
{#if showMediaRadius}
<Section title="Global media radius" bind:open={showMediaRadius}>
<p class="text-[9px] text-slate-400 dark:text-slate-600">
--sf-media-radius — 0 by default (off). Rounds every &lt;img&gt;/&lt;figure&gt; globally via a
zero-specificity :where() rule; .sf-bg-layer and component-level radii still win.
Expand All @@ -414,8 +356,7 @@
style={`border-radius: ${mediaRadius}rem`}
></div>
</div>
{/if}
</section>
</Section>

<div class="h-px bg-black/6 dark:bg-white/6"></div>

Expand Down
Loading