+
+
+
+
+ {#if type === "color"}
+
+ {/if}
+
{shortName}
- {#if token.description}
-
{token.description}
+ {#if isOverridden}
+
+ reset
+
{/if}
- {#if showScalePicker && scaleOpts}
-
{
- 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)}
- {token.value} (default)
- {/if}
- {#each scaleOpts as o (o.value)}
- {o.label}
- {/each}
- Custom…
-
- {:else if expanded}
-
{
- const v = (e.target as HTMLInputElement).value.trim();
- if (v && v !== token.value) onSet(v);
- else if (!v || v === token.value) onReset();
- expanded = false;
- }}
- onkeydown={(e) => {
- if (e.key === "Enter") (e.currentTarget as HTMLInputElement).blur();
- if (e.key === "Escape") { expanded = false; }
- }}
- class="w-28 bg-black/8 dark:bg-white/8 border border-indigo-500/50 rounded px-1.5 py-0.5 text-[10px] font-mono text-slate-800 dark:text-slate-200 focus:outline-none text-right"
- />
- {:else}
-
{ expanded = true; }}
- class="text-[10px] font-mono text-slate-500 hover:text-slate-800 dark:hover:text-slate-200 truncate max-w-28 text-right cursor-pointer transition-colors"
- title={displayValue}
- >
- {#if isOverridden}
- {displayValue}
- {:else}
- {displayValue}
- {/if}
-
+ {#if token.description}
+
{token.description}
{/if}
- {#if isOverridden}
-
- ✕
-
- {/if}
+
+ {#if showScalePicker && scaleOpts}
+ {
+ const v = (e.target as HTMLSelectElement).value;
+ if (v === CUSTOM) { expanded = true; return; }
+ if (v === token.value) onReset(); else onSet(v);
+ }}
+ class="w-full bg-black/5 dark:bg-white/5 border border-black/10 dark:border-white/10 rounded px-1.5 py-1 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)}
+ {token.value} (default)
+ {/if}
+ {#each scaleOpts as o (o.value)}
+ {o.label}
+ {/each}
+ Custom…
+
+ {:else if expanded}
+ {
+ const v = (e.target as HTMLInputElement).value.trim();
+ if (v && v !== token.value) onSet(v);
+ else if (!v || v === token.value) onReset();
+ expanded = false;
+ }}
+ onkeydown={(e) => {
+ if (e.key === "Enter") (e.currentTarget as HTMLInputElement).blur();
+ if (e.key === "Escape") { expanded = false; }
+ }}
+ class="w-full bg-black/8 dark:bg-white/8 border border-indigo-500/50 rounded px-1.5 py-1 text-[10px] font-mono text-slate-800 dark:text-slate-200 focus:outline-none"
+ />
+ {:else}
+ { expanded = true; }}
+ class="w-full text-left text-[10px] font-mono text-slate-500 hover:text-slate-800 dark:hover:text-slate-200 truncate cursor-pointer transition-colors"
+ title={displayValue}
+ >
+ {#if isOverridden}
+ {displayValue}
+ {:else}
+ {displayValue}
+ {/if}
+
+ {/if}
+
diff --git a/configurator/src/components/inputs/TypeSpecimenRow.svelte b/configurator/src/components/inputs/TypeSpecimenRow.svelte
new file mode 100644
index 00000000..a1554f01
--- /dev/null
+++ b/configurator/src/components/inputs/TypeSpecimenRow.svelte
@@ -0,0 +1,43 @@
+
+
+
+ {label}
+ Aa
+ {px ? `${(px / rootPx).toFixed(2)}rem` : ""}
+
diff --git a/configurator/src/components/panels/BordersPanel.svelte b/configurator/src/components/panels/BordersPanel.svelte
index acb20af2..0c1e54b7 100644
--- a/configurator/src/components/panels/BordersPanel.svelte
+++ b/configurator/src/components/panels/BordersPanel.svelte
@@ -14,7 +14,6 @@
const BORDER_STYLES = ["solid", "dashed", "dotted"];
const RADIUS_STEPS = ["xs", "s", "m", "l", "xl", "2xl", "full"];
- const BASE_RADII: Record
= { xs: 2, s: 4, m: 8, l: 12, xl: 16, "2xl": 24, full: 9999 };
const RADIUS_FINE: Array<{ step: string; name: string; default: number; max: number; step_size: number; rawDefault: string }> = [
{ step: "xs", name: "--sf-radius-xs", default: 2, max: 16, step_size: 0.5, rawDefault: "calc(2px * var(--sf-radius-scale))" },
@@ -50,7 +49,6 @@
return isNaN(v) ? fallback : v;
}
- let radiusScale = $derived(parseNum(overrides["--sf-radius-scale"], 1));
let borderScale = $derived(parseNum(overrides["--sf-border-scale"], 1));
let focusWidth = $derived(parseNum(overrides["--sf-focus-ring-width"], 2, "px"));
let focusOffset = $derived(parseNum(overrides["--sf-focus-ring-offset"], 2, "px"));
@@ -130,14 +128,11 @@
{#each [1, 2, 3, 4] as base (base)}
-
{base}px →
+
width-{base}
-
- {(base * borderScale).toFixed(1)}px
-
{/each}
@@ -178,11 +173,11 @@
Border sample
-
{(1 * borderScale).toFixed(1)}px · {borderStyle}
+
width-1 · {borderStyle}
@@ -258,7 +253,7 @@
Focus preview
@@ -316,12 +311,10 @@
{#each RADIUS_STEPS as step (step)}
- {@const base = BASE_RADII[step] ?? 6}
- {@const computed = step === "full" ? 9999 : base * radiusScale}
@@ -345,7 +338,7 @@
overridden={"--sf-media-radius" in overrides}
onChange={(v) => onSet("--sf-media-radius", `${v}rem`)}
onReset={() => onReset("--sf-media-radius")}
- rawDefault="var(--sf-radius-m)"
+ rawDefault="0"
variableOptions={RADIUS_SCALE}
currentRaw={overrides["--sf-media-radius"]}
onRawSet={(v) => onSet("--sf-media-radius", v)}
@@ -353,7 +346,7 @@
diff --git a/configurator/src/components/panels/EffectsPanel.svelte b/configurator/src/components/panels/EffectsPanel.svelte
index 27d19086..58d5487c 100644
--- a/configurator/src/components/panels/EffectsPanel.svelte
+++ b/configurator/src/components/panels/EffectsPanel.svelte
@@ -59,7 +59,7 @@
{blur}px
@@ -94,13 +94,13 @@
{#each [
- { label: "muted", val: opacityMuted },
- { label: "disabled", val: opacityDisabled },
- { label: "pending", val: pendingOpacity },
+ { label: "muted", token: "--sf-opacity-muted", val: opacityMuted },
+ { label: "disabled", token: "--sf-opacity-disabled", val: opacityDisabled },
+ { label: "pending", token: "--sf-state-pending-opacity", val: pendingOpacity },
] as row (row.label)}
{row.label}
-
+
{row.val}
{/each}
@@ -129,8 +129,8 @@
{/each}
-
diff --git a/configurator/src/components/panels/MacrosPanel.svelte b/configurator/src/components/panels/MacrosPanel.svelte
index aa7134fa..122aeb61 100644
--- a/configurator/src/components/panels/MacrosPanel.svelte
+++ b/configurator/src/components/panels/MacrosPanel.svelte
@@ -131,7 +131,7 @@
currentRaw={overrides["--sf-flow-space"]}
onRawSet={(v) => onSet("--sf-flow-space", v)}
/>
-
+
{#each [0, 1, 2] as i (i)}
{/each}
@@ -228,7 +228,7 @@
.sf-surface-bg preview
diff --git a/configurator/src/components/panels/MiscPanel.svelte b/configurator/src/components/panels/MiscPanel.svelte
index 424f14d1..b09bc1f8 100644
--- a/configurator/src/components/panels/MiscPanel.svelte
+++ b/configurator/src/components/panels/MiscPanel.svelte
@@ -88,10 +88,8 @@
- {touchTarget}px
-
+ style={`width: var(--sf-touch-target, 2.75rem); height: var(--sf-touch-target, 2.75rem)`}
+ >
Minimum interactive area — ensures accessibility on touch devices.
@@ -240,7 +238,7 @@
Focus ring · {focusRingStyle}
@@ -308,7 +306,7 @@
Underline thickness
{#if "--sf-link-underline-thickness" in overrides}
- onReset("--sf-link-underline-thickness")} class="text-[9px] text-slate-500 hover:text-rose-600 dark:hover:text-rose-400 cursor-pointer opacity-100 sm:opacity-0 sm:group-hover:opacity-100 sm:group-focus-within:opacity-100 focus:opacity-100">reset
+ onReset("--sf-link-underline-thickness")} class="text-[9px] text-slate-500 hover:text-rose-600 dark:hover:text-rose-400 cursor-pointer ">reset
{/if}
diff --git a/configurator/src/components/panels/MotionPanel.svelte b/configurator/src/components/panels/MotionPanel.svelte
index f61cb56f..65e5ca74 100644
--- a/configurator/src/components/panels/MotionPanel.svelte
+++ b/configurator/src/components/panels/MotionPanel.svelte
@@ -258,7 +258,7 @@
Stagger base
{#if STAGGER_TOKENS.some(t => t in overrides)}
- reset
+ reset
{/if}
handleDelete(theme.id)}
- class="p-1.5 rounded-lg border border-black/8 dark:border-white/8 text-slate-500 hover:text-rose-600 dark:hover:text-rose-400 hover:border-rose-500/30 transition-all cursor-pointer opacity-100 sm:opacity-0 sm:group-hover:opacity-100 sm:group-focus-within:opacity-100 focus:opacity-100"
+ class="p-1.5 rounded-lg border border-black/8 dark:border-white/8 text-slate-500 hover:text-rose-600 dark:hover:text-rose-400 hover:border-rose-500/30 transition-all cursor-pointer "
>
diff --git a/configurator/src/components/panels/TypographyPanel.svelte b/configurator/src/components/panels/TypographyPanel.svelte
index c47741be..55db8ddf 100644
--- a/configurator/src/components/panels/TypographyPanel.svelte
+++ b/configurator/src/components/panels/TypographyPanel.svelte
@@ -1,11 +1,11 @@
-
+
{#snippet weightGrid(label: string, tokenName: string, defaultVal: string)}
{@const current = overrides[tokenName] ?? defaultVal}
{@const isOverridden = tokenName in overrides}
- {label} weight
+ {label}
{#if isOverridden}
- onReset(tokenName)} class="text-[9px] text-slate-500 hover:text-rose-600 dark:hover:text-rose-400 cursor-pointer opacity-100 sm:opacity-0 sm:group-hover:opacity-100 sm:group-focus-within:opacity-100 focus:opacity-100 transition-colors">reset
+ onReset(tokenName)} class="text-[9px] text-slate-500 hover:text-rose-600 dark:hover:text-rose-400 cursor-pointer transition-colors">reset
{/if}
@@ -283,8 +261,8 @@
-
-
+
+
{#each [
{ label: "Body", token: "--sf-font-body", current: currentBodyFont, opts: BODY_STACKS },
{ label: "Heading", token: "--sf-font-heading", current: currentHeadingFont, opts: HEADING_STACKS },
@@ -341,6 +319,7 @@
{ googleFontChoice = (e.target as HTMLInputElement).value; }}
value={googleFontChoice}
@@ -353,17 +332,288 @@
Load & apply to {customFontTarget}
+
+
+
+
OpenType
+
+
Numeric figures
+
+ {#each [["tabular-nums", "tabular-nums (default)"], ["proportional-nums", "proportional-nums"], ["normal", "normal"]] as [val, label] (val)}
+ {@const cur = overrides["--sf-font-numeric"] ?? "tabular-nums"}
+ val === "tabular-nums" ? onReset("--sf-font-numeric") : onSet("--sf-font-numeric", val)}
+ class={`flex-1 py-1.5 rounded-lg text-[9px] border transition-all cursor-pointer ${
+ cur === val
+ ? "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"
+ }`}
+ >{label}
+ {/each}
+
+
+
+
Optical sizing
+
+ {#each [["auto", "auto (default)"], ["none", "none"]] as [val, label] (val)}
+ {@const cur = overrides["--sf-optical-sizing"] ?? "auto"}
+ val === "auto" ? onReset("--sf-optical-sizing") : onSet("--sf-optical-sizing", val)}
+ class={`flex-1 py-2 rounded-lg text-[10px] border transition-all cursor-pointer ${
+ cur === val
+ ? "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"
+ }`}
+ >{label}
+ {/each}
+
+
+
+ Font features
+ {
+ const v = (e.target as HTMLInputElement).value.trim();
+ v ? onSet("--sf-font-features", v) : onReset("--sf-font-features");
+ }}
+ 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 "--sf-font-features" in overrides}
+ onReset("--sf-font-features")} class="text-[8px] text-slate-500 hover:text-rose-600 dark:hover:text-rose-400 cursor-pointer shrink-0">reset
+ {/if}
+
+
+ Variation
+ {
+ const v = (e.target as HTMLInputElement).value.trim();
+ v ? onSet("--sf-font-variation", v) : onReset("--sf-font-variation");
+ }}
+ 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 "--sf-font-variation" in overrides}
+ onReset("--sf-font-variation")} class="text-[8px] text-slate-500 hover:text-rose-600 dark:hover:text-rose-400 cursor-pointer shrink-0">reset
+ {/if}
+
+
-
-
+
+
+
+ Utopia-style fluid scale. The viewport range and the modular ratio are shared;
+ text and display each have their own base size and multiplier.
+
+
+
+ { onReset("--sf-fluid-min-vw"); onReset("--sf-fluid-max-vw"); }}
+ onMinChange={(v) => onSet("--sf-fluid-min-vw", String(v))}
+ onMaxChange={(v) => onSet("--sf-fluid-max-vw", String(v))}
+ />
+
+
+ { onReset("--sf-text-base-min"); onReset("--sf-text-base-max"); }}
+ onMinChange={(v) => onSet("--sf-text-base-min", String(v))}
+ onMaxChange={(v) => onSet("--sf-text-base-max", String(v))}
+ ratioPresets={RATIO_PRESETS}
+ ratioMin={ratioMin} ratioMax={ratioMax}
+ ratioMin_bound={1.05} ratioMax_bound={1.8}
+ onRatioMinChange={(v) => onSet("--sf-text-ratio-min", String(v))}
+ onRatioMaxChange={(v) => onSet("--sf-text-ratio-max", String(v))}
+ />
+ onSet("--sf-text-scale", String(v))}
+ onReset={() => onReset("--sf-text-scale")}
+ />
+
+
+ { onReset("--sf-text-display-base-min"); onReset("--sf-text-display-base-max"); }}
+ onMinChange={(v) => onSet("--sf-text-display-base-min", String(v))}
+ onMaxChange={(v) => onSet("--sf-text-display-base-max", String(v))}
+ ratioPresets={RATIO_PRESETS}
+ ratioMin={ratioMin} ratioMax={ratioMax}
+ ratioMin_bound={1.05} ratioMax_bound={1.8}
+ onRatioMinChange={(v) => onSet("--sf-text-ratio-min", String(v))}
+ onRatioMaxChange={(v) => onSet("--sf-text-ratio-max", String(v))}
+ />
+ onSet("--sf-text-display-scale", String(v))}
+ onReset={() => onReset("--sf-text-display-scale")}
+ />
+
+
+
+
Text
+ {#each [...TEXT_STEPS].reverse() as label (label)}
+
+ {/each}
+
Display
+ {#each ["l", "m", "s"] as label (label)}
+
+ {/each}
+
+
+
+
+
{ showScaleAdvanced = !showScaleAdvanced; }}
+ aria-expanded={showScaleAdvanced}
+ class="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"
+ >
+ Advanced
+ {showScaleAdvanced ? "▲" : "▼"}
+
+ {#if showScaleAdvanced}
+
+ {#each knobs as k (k.name)}
+
val === null ? onReset(name) : onSet(name, val)}
+ />
+ {/each}
+
+ {/if}
+
+
+
+
+
+
+
+
+
Leading scale
+
+ {#each LEADING_TOKENS as t (t.token)}
+ onSet(t.token, String(v))}
+ onReset={() => onReset(t.token)}
+ />
+ {/each}
+
+
+
+ onSet("--sf-leading-taper", String(v))}
+ onReset={() => onReset("--sf-leading-taper")}
+ />
+
+
+
Display line heights
+
+ {#each DISPLAY_LEADING as row (row.token)}
+ onSet(row.token, String(v))}
+ onReset={() => onReset(row.token)}
+ />
+ {/each}
+
+
+
+
+
+
+
+
+
+ Overriding these propagates to heading sizes that reference them.
+
+
+ {#each TRACKING_TOKENS as t (t.token)}
+
+
+ {t.label}
+ {#if t.token in overrides}
+ onReset(t.token)} class="text-[9px] text-slate-500 hover:text-rose-600 dark:hover:text-rose-400 cursor-pointer ">reset
+ {/if}
+
+
onSet(t.token, `${v}em`)}
+ />
+
+ {/each}
+
+
+
+
+
+
+
+ Weight scale
+ Named weight tokens. Override if your font uses non-standard axis values.
+ {@render weightGrid("Light", "--sf-font-weight-light", "300")}
+ {@render weightGrid("Normal", "--sf-font-weight-normal", "400")}
+ {@render weightGrid("Medium", "--sf-font-weight-medium", "500")}
+ {@render weightGrid("Semibold", "--sf-font-weight-semibold", "600")}
+ {@render weightGrid("Bold", "--sf-font-weight-bold", "700")}
+
+ Role weights
+ Semantic defaults the elements build on — reweight the whole site from here.
+ {@render weightGrid("Heading", "--sf-font-weight-heading", WEIGHT_DEFAULTS["--sf-font-weight-heading"] ?? "600")}
+ {@render weightGrid("Display", "--sf-font-weight-display", WEIGHT_DEFAULTS["--sf-font-weight-display"] ?? "700")}
+ {@render weightGrid("Body strong", "--sf-font-weight-strong", "700")}
+ {@render weightGrid("Interactive", "--sf-font-weight-interactive", WEIGHT_DEFAULTS["--sf-font-weight-interactive"] ?? "600")}
+
+
+
+
+
+
{#each TYPE_LEVELS as lvl (lvl.id)}
- {@const isOv = (lvl.size in overrides) || (lvl.lh in overrides) || (lvl.wt in overrides) || (!!lvl.ls && lvl.ls in overrides)}
+ {@const lvlMw = levelMaxWidthToken(lvl.id)}
+ {@const isOv = (lvl.size in overrides) || (lvl.lh in overrides) || (lvl.wt in overrides) || (!!lvl.ls && lvl.ls in overrides) || (!!lvlMw && lvlMw in overrides)}
{ activeLevelId = lvl.id; }}
class={`relative flex-1 py-1 rounded-md text-[10px] font-bold transition-all cursor-pointer ${
@@ -376,11 +626,13 @@
{/each}
-
+
@@ -456,13 +708,14 @@
{/if}
- {#if activeLevel.id !== "body"}
+ {#if activeLevel.id.startsWith("h")}
{@const maxWidthToken = `--sf-${activeLevel.id}-max-width`}
Max width
{
const v = (e.target as HTMLInputElement).value.trim();
@@ -477,381 +730,116 @@
{/if}
- {#if (activeLevel.size in overrides) || (activeLevel.lh in overrides) || (activeLevel.wt in overrides) || (!!activeLevel.ls && activeLevel.ls in overrides)}
+ {#if (activeLevel.size in overrides) || (activeLevel.lh in overrides) || (activeLevel.wt in overrides) || (!!activeLevel.ls && activeLevel.ls in overrides) || (!!activeMw && activeMw in overrides)}
{
const patch: Record = { [activeLevel.size]: null, [activeLevel.lh]: null, [activeLevel.wt]: null };
if (activeLevel.ls) patch[activeLevel.ls] = null;
+ if (activeMw) patch[activeMw] = null;
onBulkChange(patch);
}}
class="text-[9px] text-slate-500 hover:text-rose-600 dark:hover:text-rose-400 cursor-pointer"
>reset {activeLevel.label}
{/if}
-
-
-
-
-
-
-
- {@render weightGrid("Body strong", "--sf-font-weight-strong", WEIGHT_DEFAULTS["--sf-font-weight-strong"] ?? "600")}
- {@render weightGrid("Interactive", "--sf-font-weight-interactive", WEIGHT_DEFAULTS["--sf-font-weight-interactive"] ?? "600")}
+
+
+
Body & prose details
-
-
-
Emphasis style
-
- {#each [["Italic", "italic"], ["Normal", "normal"]] as [label, val] (val)}
- {@const current = overrides["--sf-body-em-style"] ?? "italic"}
- val === "italic" ? onReset("--sf-body-em-style") : onSet("--sf-body-em-style", val)}
- class={`flex-1 py-2 rounded-lg text-[10px] border transition-all cursor-pointer ${
- current === val
- ? "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"
- }`}
- >{label}
- {/each}
-
-
-
-
-
onSet("--sf-code-font-size", `${v}em`)}
- onReset={() => onReset("--sf-code-font-size")}
- />
-
-
- {#each [
- { label: "Heading wrap", token: "--sf-heading-text-wrap", defaultVal: "balance" },
- { label: "Body wrap", token: "--sf-body-text-wrap", defaultVal: "pretty" },
- ] as row (row.token)}
- {@const current = overrides[row.token] ?? row.defaultVal}
+
-
{row.label}
+
Emphasis style
- {#each WRAP_OPTIONS as o (o.value)}
+ {#each [["Italic", "italic"], ["Normal", "normal"]] as [label, val] (val)}
+ {@const current = overrides["--sf-body-em-style"] ?? "italic"}
o.value === row.defaultVal && !(row.token in overrides) ? undefined : (o.value === row.defaultVal ? onReset(row.token) : onSet(row.token, o.value))}
- class={`flex-1 px-2 py-1.5 rounded-lg text-[10px] border transition-all cursor-pointer ${
- current === o.value
+ onclick={() => val === "italic" ? onReset("--sf-body-em-style") : onSet("--sf-body-em-style", val)}
+ class={`flex-1 py-2 rounded-lg text-[10px] border transition-all cursor-pointer ${
+ current === val
? "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"
}`}
- >{o.label}
+ >{label}
{/each}
- {/each}
-
-
- External marker
- {
- const v = (e.target as HTMLInputElement).value;
- v ? onSet("--sf-link-external-marker", v) : onReset("--sf-link-external-marker");
- }}
- class="flex-1 min-w-0 bg-black/5 dark:bg-white/5 border border-black/10 dark:border-white/10 rounded-lg px-2 py-1.5 text-[11px] font-mono text-slate-700 dark:text-slate-300 placeholder:text-slate-600 focus:outline-none focus:border-indigo-500"
- />
- {#if "--sf-link-external-marker" in overrides}
- onReset("--sf-link-external-marker")} class="text-[9px] text-slate-500 hover:text-rose-600 dark:hover:text-rose-400 cursor-pointer shrink-0">reset
- {/if}
-
-
-
-
- External label
- {
- const v = (e.target as HTMLInputElement).value;
- v ? onSet("--sf-link-external-label", v) : onReset("--sf-link-external-label");
- }}
- class="flex-1 min-w-0 bg-black/5 dark:bg-white/5 border border-black/10 dark:border-white/10 rounded-lg px-2 py-1.5 text-[11px] font-mono text-slate-700 dark:text-slate-300 placeholder:text-slate-600 focus:outline-none focus:border-indigo-500"
+
+ onSet("--sf-code-font-size", `${v}em`)}
+ onReset={() => onReset("--sf-code-font-size")}
/>
- {#if "--sf-link-external-label" in overrides}
- onReset("--sf-link-external-label")} class="text-[9px] text-slate-500 hover:text-rose-600 dark:hover:text-rose-400 cursor-pointer shrink-0">reset
- {/if}
-
-
-
-
-
-
- Named weight tokens. Override if your font uses non-standard axis values.
+
{#each [
- { label: "Light", token: "--sf-font-weight-light", def: 300 },
- { label: "Normal", token: "--sf-font-weight-normal", def: 400 },
- { label: "Medium", token: "--sf-font-weight-medium", def: 500 },
- { label: "Semibold", token: "--sf-font-weight-semibold", def: 600 },
- { label: "Bold", token: "--sf-font-weight-bold", def: 700 },
+ { label: "Heading wrap", token: "--sf-heading-text-wrap", defaultVal: "balance" },
+ { label: "Body wrap", token: "--sf-body-text-wrap", defaultVal: "pretty" },
] as row (row.token)}
-
-
- {row.label}
- {#if row.token in overrides}
- onReset(row.token)} class="text-[9px] text-slate-500 hover:text-rose-600 dark:hover:text-rose-400 cursor-pointer opacity-100 sm:opacity-0 sm:group-hover:opacity-100 sm:group-focus-within:opacity-100 focus:opacity-100">reset
- {/if}
-
+ {@const current = overrides[row.token] ?? row.defaultVal}
+
+
{row.label}
- {#each WEIGHT_OPTIONS as w (w)}
- {@const cur = overrides[row.token] ?? String(row.def)}
+ {#each WRAP_OPTIONS as o (o.value)}
w === String(row.def) && !(row.token in overrides) ? undefined : (w === String(row.def) ? onReset(row.token) : onSet(row.token, w))}
- style={`font-weight:${parseInt(w)}`}
- class={`flex-1 py-1.5 rounded-lg text-[10px] border transition-all cursor-pointer font-mono ${
- cur === w
+ onclick={() => o.value === row.defaultVal && !(row.token in overrides) ? undefined : (o.value === row.defaultVal ? onReset(row.token) : onSet(row.token, o.value))}
+ class={`flex-1 px-2 py-1.5 rounded-lg text-[10px] border transition-all cursor-pointer ${
+ current === o.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"
}`}
- >{w}
+ >{o.label}
{/each}
{/each}
-
-
-
-
-
-
-
- { onReset("--sf-text-display-base-min"); onReset("--sf-text-display-base-max"); }}
- onMinChange={(v) => onSet("--sf-text-display-base-min", String(v))}
- onMaxChange={(v) => onSet("--sf-text-display-base-max", String(v))}
- />
-
-
- onSet("--sf-text-display-scale", String(v))}
- onReset={() => onReset("--sf-text-display-scale")}
- />
-
-
- Modular ratio is shared with the text scale (Mobile {ratioMin} → Desktop {ratioMax}) — edit it under “Modular scale”.
-
-
-
-
- {#each [
- { label: "Display S", token: "--sf-display-s-line-height", def: 1.1 },
- { label: "Display M", token: "--sf-display-m-line-height", def: 1.05 },
- { label: "Display L", token: "--sf-display-l-line-height", def: 1 },
- ] as row (row.token)}
-
-
{row.label} line-height
-
onSet(row.token, String(v))}
- onReset={() => onReset(row.token)}
- />
-
- {/each}
-
-
-
- {@render weightGrid("Display", "--sf-font-weight-display", WEIGHT_DEFAULTS["--sf-font-weight-display"] ?? "700")}
- {@render weightGrid("Heading", "--sf-font-weight-heading", WEIGHT_DEFAULTS["--sf-font-weight-heading"] ?? "700")}
-
-
-
-
-
-
-
- Utopia-style fluid scale. Each endpoint has three values — viewport width,
- base size, and modular ratio. Sizes interpolate fluidly between mobile and desktop.
-
-
-
- { onReset("--sf-fluid-min-vw"); onReset("--sf-fluid-max-vw"); }}
- onMinChange={(v) => onSet("--sf-fluid-min-vw", String(v))}
- onMaxChange={(v) => onSet("--sf-fluid-max-vw", String(v))}
- />
- { onReset("--sf-text-base-min"); onReset("--sf-text-base-max"); }}
- onMinChange={(v) => onSet("--sf-text-base-min", String(v))}
- onMaxChange={(v) => onSet("--sf-text-base-max", String(v))}
- ratioPresets={RATIO_PRESETS}
- ratioMin={ratioMin} ratioMax={ratioMax}
- ratioMin_bound={1.05} ratioMax_bound={1.8}
- onRatioMinChange={(v) => onSet("--sf-text-ratio-min", String(v))}
- onRatioMaxChange={(v) => onSet("--sf-text-ratio-max", String(v))}
- />
-
-
- onSet("--sf-text-scale", String(v))}
- onReset={() => onReset("--sf-text-scale")}
- />
-
-
-
-
Line height scale
-
-
onSet("--sf-leading-taper", String(v))}
- onReset={() => onReset("--sf-leading-taper")}
- />
-
-
-
-
Letter-spacing scale
-
- Overriding these propagates to heading sizes that reference them.
-
-
- {#each TRACKING_TOKENS as t (t.token)}
-
-
- {t.label}
- {#if t.token in overrides}
- onReset(t.token)} class="text-[9px] text-slate-500 hover:text-rose-600 dark:hover:text-rose-400 cursor-pointer opacity-100 sm:opacity-0 sm:group-hover:opacity-100 sm:group-focus-within:opacity-100 focus:opacity-100">reset
- {/if}
-
-
onSet(t.token, `${v}em`)}
- />
-
- {/each}
+
+
+ External label
+ {
+ const v = (e.target as HTMLInputElement).value;
+ v ? onSet("--sf-link-external-label", v) : onReset("--sf-link-external-label");
+ }}
+ class="flex-1 min-w-0 bg-black/5 dark:bg-white/5 border border-black/10 dark:border-white/10 rounded-lg px-2 py-1.5 text-[11px] font-mono text-slate-700 dark:text-slate-300 placeholder:text-slate-600 focus:outline-none focus:border-indigo-500"
+ />
+ {#if "--sf-link-external-label" in overrides}
+ onReset("--sf-link-external-label")} class="text-[9px] text-slate-500 hover:text-rose-600 dark:hover:text-rose-400 cursor-pointer shrink-0">reset
+ {/if}
-
-
-
-
{ showScaleAdvanced = !showScaleAdvanced; }}
- aria-expanded={showScaleAdvanced}
- class="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"
- >
- Advanced
- {showScaleAdvanced ? "▲" : "▼"}
-
- {#if showScaleAdvanced}
-
- {#each knobs as k (k.name)}
-
val === null ? onReset(name) : onSet(name, val)}
- />
- {/each}
-
- {/if}
-
-
-
- { showScalePreview = !showScalePreview; }}
- aria-expanded={showScalePreview}
- class="w-full flex items-center justify-between cursor-pointer mb-3"
- >
- Scale preview
- {showScalePreview ? "▲" : "▼"}
-
- {#if showScalePreview}
- {@const midBase = (baseMin + baseMax) / 2}
- {@const midDisp = (dispMin + dispMax) / 2}
- {@const midRatio = (ratioMin + ratioMax) / 2}
-
-
-
Text
- {#each [...TEXT_STEPS].reverse() as { label, factor } (label)}
- {@const size = midBase * factor * textScale}
-
- {label}
- Aa
- {size.toFixed(2)}rem
-
- {/each}
-
-
Display
- {#each [
- { label: "l", factor: midRatio * midRatio },
- { label: "m", factor: midRatio },
- { label: "s", factor: 1 },
- ] as { label, factor } (label)}
- {@const size = midDisp * factor * displayScale}
-
- {label}
- Aa
- {size.toFixed(2)}rem
-
- {/each}
-
- {/if}
-
-
-
-
-
+
-
-
-
-
-
- Advanced OpenType and variable-font axes. Leave as normal unless your font supports these features.
-
-
-
-
Numeric figures
-
- {#each [["tabular-nums", "tabular-nums (default)"], ["proportional-nums", "proportional-nums"], ["normal", "normal"]] as [val, label] (val)}
- {@const cur = overrides["--sf-font-numeric"] ?? "tabular-nums"}
- val === "tabular-nums" ? onReset("--sf-font-numeric") : onSet("--sf-font-numeric", val)}
- class={`flex-1 py-1.5 rounded-lg text-[9px] border transition-all cursor-pointer ${
- cur === val
- ? "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"
- }`}
- >{label}
- {/each}
-
-
-
-
-
-
Optical sizing
-
- {#each [["auto", "auto (default)"], ["none", "none"]] as [val, label] (val)}
- {@const cur = overrides["--sf-optical-sizing"] ?? "auto"}
- val === "auto" ? onReset("--sf-optical-sizing") : onSet("--sf-optical-sizing", val)}
- class={`flex-1 py-2 rounded-lg text-[10px] border transition-all cursor-pointer ${
- cur === val
- ? "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"
- }`}
- >{label}
- {/each}
-
-
-
-
-
- Font features
- {
- const v = (e.target as HTMLInputElement).value.trim();
- v ? onSet("--sf-font-features", v) : onReset("--sf-font-features");
- }}
- 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 "--sf-font-features" in overrides}
- onReset("--sf-font-features")} class="text-[8px] text-slate-500 hover:text-rose-600 dark:hover:text-rose-400 cursor-pointer shrink-0">reset
- {/if}
-
-
-
-
- Variation
- {
- const v = (e.target as HTMLInputElement).value.trim();
- v ? onSet("--sf-font-variation", v) : onReset("--sf-font-variation");
- }}
- 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 "--sf-font-variation" in overrides}
- onReset("--sf-font-variation")} class="text-[8px] text-slate-500 hover:text-rose-600 dark:hover:text-rose-400 cursor-pointer shrink-0">reset
- {/if}
-
-
diff --git a/configurator/tests-components/token-editing.test.js b/configurator/tests-components/token-editing.test.js
index 714e3f76..b8db28ce 100644
--- a/configurator/tests-components/token-editing.test.js
+++ b/configurator/tests-components/token-editing.test.js
@@ -106,13 +106,13 @@ describe('TokenRow', () => {
render(TokenRow, { props: { token, onSet: () => {}, onReset: () => {} } });
expect(screen.getByText('space-m')).toBeTruthy();
expect(screen.getByText('1rem')).toBeTruthy();
- expect(screen.queryByText('✕')).toBeNull();
+ expect(screen.queryByText('reset')).toBeNull();
});
test('bind: renders the override value and reset affordance when overridden', () => {
render(TokenRow, { props: { token, overrideValue: '2rem', onSet: () => {}, onReset: () => {} } });
expect(screen.getByText('2rem')).toBeTruthy();
- expect(screen.getByText('✕')).toBeTruthy();
+ expect(screen.getByText('reset')).toBeTruthy();
});
test('override-set: editing the value and blurring with a new value calls onSet', async () => {
@@ -143,10 +143,10 @@ describe('TokenRow', () => {
expect(onSet).not.toHaveBeenCalled();
});
- test('reset: clicking the ✕ button calls onReset directly', async () => {
+ test('reset: clicking the reset button calls onReset directly', async () => {
const onReset = vi.fn();
render(TokenRow, { props: { token, overrideValue: '2rem', onSet: () => {}, onReset } });
- await fireEvent.click(screen.getByText('✕'));
+ await fireEvent.click(screen.getByText('reset'));
expect(onReset).toHaveBeenCalledOnce();
});
});