diff --git a/configurator/src/components/DomainPanel.svelte b/configurator/src/components/DomainPanel.svelte index 37edf4b3..470d6176 100644 --- a/configurator/src/components/DomainPanel.svelte +++ b/configurator/src/components/DomainPanel.svelte @@ -77,7 +77,7 @@ {#if domain === "colors"} {:else if domain === "typography"} - + {:else if domain === "spacing"} {:else if domain === "layout"} diff --git a/configurator/src/components/inputs/PowerKnobRow.svelte b/configurator/src/components/inputs/PowerKnobRow.svelte index 6d156ca4..5ade0e70 100644 --- a/configurator/src/components/inputs/PowerKnobRow.svelte +++ b/configurator/src/components/inputs/PowerKnobRow.svelte @@ -36,7 +36,7 @@ {#if isOverridden} diff --git a/configurator/src/components/inputs/SliderRow.svelte b/configurator/src/components/inputs/SliderRow.svelte index b4207b11..7f656181 100644 --- a/configurator/src/components/inputs/SliderRow.svelte +++ b/configurator/src/components/inputs/SliderRow.svelte @@ -116,14 +116,14 @@ class={`text-[9px] font-mono cursor-pointer transition-all px-0.5 ${ showRawText ? 'text-indigo-600 dark:text-indigo-400' - : 'opacity-100 sm:opacity-0 sm:group-hover:opacity-100 sm:group-focus-within:opacity-100 focus:opacity-100 text-slate-500 hover:text-indigo-600 dark:hover:text-indigo-400' + : 'text-slate-500 hover:text-indigo-600 dark:hover:text-indigo-400' }`} ></> {/if} {#if overridden} {/if} diff --git a/configurator/src/components/inputs/TokenRow.svelte b/configurator/src/components/inputs/TokenRow.svelte index 70691ee5..a5ec5a7b 100644 --- a/configurator/src/components/inputs/TokenRow.svelte +++ b/configurator/src/components/inputs/TokenRow.svelte @@ -46,81 +46,83 @@ let swatchColor = $derived(type === "color" ? paintSwatch(displayValue) : ""); -
-
- - {#if type === "color"} -
- {:else} -
- {/if} - -
-
+ +
+
+
+ {#if type === "color"} +
+ {/if} +
{shortName}
- {#if token.description} -
{token.description}
+ {#if isOverridden} + {/if}
- {#if showScalePicker && scaleOpts} - - {: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} - + {#if token.description} +
{token.description}
{/if} - {#if isOverridden} - - {/if} +
+ {#if showScalePicker && scaleOpts} + + {: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} + + {/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}
{step}
@@ -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}
-
-
+
+
Scrollbar preview
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 @@
-
+
Caption over scrim
.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} - + {/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)} - + {/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} - + {/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"} + + {/each} +
+
+
+
Optical sizing
+
+ {#each [["auto", "auto (default)"], ["none", "none"]] as [val, label] (val)} + {@const cur = overrides["--sf-optical-sizing"] ?? "auto"} + + {/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} + + {/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} + + {/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} +
+ + +
+ + {#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} + + {/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)}
- +
The quick brown fox
@@ -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)} {/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"} - - {/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"} + >{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} - - {/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} - - {/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} - - {/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)} + >{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
-
- {#each LEADING_TOKENS as t (t.token)} - onSet(t.token, String(v))} - onReset={() => onReset(t.token)} - /> - {/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} + + {/if}
-
- 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} - - {/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} + + {/if}
- - -
- - {#if showScaleAdvanced} -
- {#each knobs as k (k.name)} - val === null ? onReset(name) : onSet(name, val)} - /> - {/each} -
- {/if} -
- -
- - {#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} -
- -
- - +

Per-text-size max-width caps on .sf-text-* elements. Set to none to remove the constraint; use ch units for character-based measures. @@ -873,6 +861,7 @@ { const v = (e.target as HTMLInputElement).value.trim(); @@ -887,83 +876,4 @@ {/each}

- -
- - -
-

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"} - - {/each} -
-
- - -
-
Optical sizing
-
- {#each [["auto", "auto (default)"], ["none", "none"]] as [val, label] (val)} - {@const cur = overrides["--sf-optical-sizing"] ?? "auto"} - - {/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} - - {/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} - - {/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(); }); });