Skip to content

Commit fa4b054

Browse files
committed
feat(components,configurator): per-size .sf-btn padding + min-height knobs; move flatten-all to Advanced
Extends the per-size button model (added for label font-size in v0.7.14) to padding and height, and reworks the configurator button panel around it. Framework: - Add 15 PUBLIC per-size knobs — --sf-btn-{xs,s,m,l,xl}-{padding-block, padding-inline,min-height} — each `initial`, wired as the first arg of the matching --sf-btn-{prop}--size tier so an unset knob falls through to that rung's scale default. Retune one rung's font/padding/height independently without flattening the --xs…xl ladder. - Re-tier the flatten-all overrides (--sf-btn-font-size, --sf-btn-padding-block, --sf-btn-padding-inline, --sf-btn-min-height) to PUBLIC-ADVANCED — same SemVer guarantee, surfaced as advanced (per-size knobs are the everyday path). Configurator: - ComponentsPanel: the per-size editor now tunes font-size / padding-block / padding-inline / min-height for the selected rung, and selecting a rung drives the live preview (fixes "preview doesn't react" to per-rung edits). The flatten-all "Advanced" disclosure is removed from the panel — those knobs now live only in the All-tokens tab, where the tier dividers group them under Advanced. Regenerate registry / docs / annotations / demos / audit artifacts; extend the token-api snapshot, empty-token coverage list, per-size browser test (now also asserts padding + min-height per rung), and CHANGELOG. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GnwqYHgHNAWPbsUJWUeY1w
1 parent 11441ea commit fa4b054

25 files changed

Lines changed: 1616 additions & 250 deletions

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
44

55
## Unreleased
66

7+
### Features
8+
- **components:** extend the per-size `.sf-btn` knobs to padding + height — `--sf-btn-{xs,s,m,l,xl}-{padding-block,padding-inline,min-height}` (15 new PUBLIC knobs) join the per-size `font-size` knobs, so each rung's font/padding/height can be retuned independently without flattening the `.sf-btn--xs…xl` ladder
9+
- **configurator:** the button panel's per-size editor now tunes font-size / padding / min-height per rung (and drives the live preview); the flatten-all knobs move out of the panel into the All-tokens tab's Advanced group
10+
11+
### Changed
12+
- **tokens:** re-tier the flatten-all `.sf-btn` overrides (`--sf-btn-font-size`, `--sf-btn-padding-block`, `--sf-btn-padding-inline`, `--sf-btn-min-height`) to PUBLIC-ADVANCED — same SemVer guarantee, surfaced as advanced (the per-size knobs are the everyday path)
13+
714
## [0.7.14] - 2026-07-10
815

916
### Features

configurator/src/components/panels/ComponentsPanel.svelte

Lines changed: 81 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -46,37 +46,45 @@
4646
{ label: "Border width", token: "--sf-btn-border-width", unit: "px", min: 0, max: 4, step: 0.5, default: 1, rawDefault: "var(--sf-border-width-1)", variableOptions: BORDER_WIDTH_SCALE },
4747
];
4848
49-
// Advanced flatten-all knobs — each shadows the per-size scale (.sf-btn--xs…xl)
50-
// on EVERY size once set, so they live behind the panel's "Advanced" toggle
51-
// rather than the primary surface. (For everyday sizing use the label-size
52-
// multiplier + per-size knobs above.)
53-
const BUTTON_ADVANCED_TOKENS: Array<{ label: string; token: string; unit: string; min: number; max: number; step: number; default: number; rawDefault: string; variableOptions: VarOption[] }> = [
54-
{ label: "Padding block", token: "--sf-btn-padding-block", unit: "rem", min: 0, max: 1, step: 0.025, default: 0.375, rawDefault: "var(--sf-space-xs)", variableOptions: SPACE_SCALE },
55-
{ label: "Padding inline", token: "--sf-btn-padding-inline", unit: "rem", min: 0, max: 2, step: 0.025, default: 1, rawDefault: "var(--sf-space-m)", variableOptions: SPACE_SCALE },
56-
// Unset, --sf-btn-min-height falls through to the per-size tier
57-
// (--sf-size-* via .sf-btn--xs…xl), whose base is --sf-size-m (40px) — so
58-
// THAT is the real default, not --sf-touch-target. Presenting touch-target
59-
// (44px) as the default silently pinned every size to 44px and collapsed
60-
// the XS–XL scale. touch-target stays available as an explicit choice via
61-
// the variable dropdown for consumers who want the WCAG AAA target.
62-
{ label: "Min height", token: "--sf-btn-min-height", unit: "rem", min: 1, max: 4, step: 0.125, default: 2.5, rawDefault: "var(--sf-size-m)", variableOptions: SIZE_SCALE },
63-
];
64-
6549
// Everyday label-size multiplier — scales every button's font-size by one
6650
// factor while KEEPING the xs…xl ladder intact (unlike the flatten-all
67-
// --sf-btn-font-size override, which lives in Advanced).
51+
// --sf-btn-font-size override, which now lives only in the All-tokens tab).
6852
const BTN_FONT_SCALE = { token: "--sf-btn-font-scale", default: 1, min: 0.75, max: 1.5, step: 0.05 };
6953
70-
// Per-size label-size knobs — one public knob per rung, the button counterpart
71-
// of the per-heading size knobs. Each rung's default step matches its own name
72-
// (xs→--sf-text-xs …); unset falls through to that scale default.
73-
const BTN_FONT_SIZES = [
74-
{ rung: "xs", token: "--sf-btn-xs-font-size" },
75-
{ rung: "s", token: "--sf-btn-s-font-size" },
76-
{ rung: "m", token: "--sf-btn-m-font-size" },
77-
{ rung: "l", token: "--sf-btn-l-font-size" },
78-
{ rung: "xl", token: "--sf-btn-xl-font-size" },
79-
];
54+
// Per-size editor — one PUBLIC knob per rung for each sizing property, the
55+
// button counterpart of the per-heading typography editor. Defaults mirror the
56+
// framework's --sf-btn-*--size tier; unset falls through to that scale default.
57+
const BTN_SIZE_RUNGS = ["xs", "s", "m", "l", "xl"];
58+
type RungSizing = {
59+
fontStep: string;
60+
pb: { raw: string; num: number };
61+
pi: { raw: string; num: number };
62+
mh: { raw: string; num: number };
63+
};
64+
const RUNG_SIZING: Record<string, RungSizing> = {
65+
xs: { fontStep: "xs", pb: { raw: "0.125rem", num: 0.125 }, pi: { raw: "var(--sf-space-xs)", num: 0.375 }, mh: { raw: "var(--sf-size-xs)", num: 2 } },
66+
s: { fontStep: "s", pb: { raw: "var(--sf-space-2xs)", num: 0.25 }, pi: { raw: "var(--sf-space-s)", num: 0.5 }, mh: { raw: "var(--sf-size-s)", num: 2.25 } },
67+
m: { fontStep: "m", pb: { raw: "var(--sf-space-xs)", num: 0.375 }, pi: { raw: "var(--sf-space-m)", num: 0.75 }, mh: { raw: "var(--sf-size-m)", num: 2.5 } },
68+
l: { fontStep: "l", pb: { raw: "var(--sf-space-s)", num: 0.5 }, pi: { raw: "var(--sf-space-l)", num: 1 }, mh: { raw: "var(--sf-size-l)", num: 3 } },
69+
xl: { fontStep: "xl", pb: { raw: "var(--sf-space-m)", num: 0.75 }, pi: { raw: "var(--sf-space-xl)", num: 1.5 }, mh: { raw: "var(--sf-size-xl)", num: 3.5 } },
70+
};
71+
// Per-rung SliderRow config for a padding / min-height property.
72+
function rungRow(rung: string, prop: "pb" | "pi" | "mh") {
73+
const d = RUNG_SIZING[rung][prop];
74+
const suffix = prop === "pb" ? "padding-block" : prop === "pi" ? "padding-inline" : "min-height";
75+
const isHeight = prop === "mh";
76+
return {
77+
token: `--sf-btn-${rung}-${suffix}`,
78+
rawDefault: d.raw,
79+
default: d.num,
80+
unit: "rem",
81+
min: 0,
82+
max: isHeight ? 4 : 2,
83+
step: isHeight ? 0.125 : 0.025,
84+
variableOptions: isHeight ? SIZE_SCALE : SPACE_SCALE,
85+
label: prop === "pb" ? "Padding block" : prop === "pi" ? "Padding inline" : "Min height",
86+
};
87+
}
8088
8189
const CARD_TOKENS: Array<{ label: string; token: string; unit: string; min: number; max: number; step: number; default: number; rawDefault: string; variableOptions: VarOption[] }> = [
8290
{ label: "Padding", token: "--sf-card-padding", unit: "rem", min: 0, max: 3, step: 0.05, default: 1.5, rawDefault: "var(--sf-space-l)", variableOptions: SPACE_SCALE },
@@ -86,26 +94,16 @@
8694
{ label: "Media radius", token: "--sf-card-media-radius", unit: "rem", min: 0, max: 2, step: 0.05, default: 0.5, rawDefault: "var(--sf-card-radius, var(--sf-radius-m))", variableOptions: RADIUS_SCALE },
8795
];
8896
89-
// Global knobs that, once set, shadow the per-size scale (.sf-btn--xs…xl) on
90-
// EVERY size — the framework reads the public knob before the size tier. The
91-
// UI flags these so a global tweak that flattens the size scale isn't a
92-
// surprise. (Label size / --sf-btn-font-size behaves the same way.)
93-
const SCALE_SHADOWING = new Set([
94-
"--sf-btn-padding-block",
95-
"--sf-btn-padding-inline",
96-
"--sf-btn-min-height",
97-
]);
98-
9997
function resetButtonTokens() {
10098
for (const k of Object.keys(overrides)) {
10199
if (k.startsWith("--sf-btn-")) onReset(k);
102100
}
103101
}
104102
105103
let showButton = $state(false);
106-
let showBtnAdvanced = $state(false);
107104
let activeBtnRung = $state("m");
108-
let activeBtnSize = $derived(BTN_FONT_SIZES.find((r) => r.rung === activeBtnRung) ?? BTN_FONT_SIZES[2]);
105+
let activeFontToken = $derived(`--sf-btn-${activeBtnRung}-font-size`);
106+
let activeFontDef = $derived(RUNG_SIZING[activeBtnRung].fontStep);
109107
let showCard = $state(false);
110108
111109
// Preview-only state — never touches overrides, just picks which real
@@ -232,7 +230,7 @@
232230
<div class="flex flex-wrap gap-1">
233231
{#each BTN_SIZES as s (s)}
234232
<button
235-
onclick={() => { btnSize = s; }}
233+
onclick={() => { btnSize = s; activeBtnRung = s; }}
236234
class={`px-2.5 py-1 rounded-lg text-[10px] border transition-all cursor-pointer uppercase ${
237235
btnSize === s
238236
? "bg-indigo-500/15 border-indigo-500/40 text-indigo-800 dark:text-indigo-200"
@@ -332,95 +330,62 @@
332330
</div>
333331
</div>
334332

335-
<!-- Per-size label size — one rung at a time (parity with per-heading
336-
size knobs). Retunes --sf-btn-{rung}-font-size for a single rung. -->
333+
<!-- Per-size editor — one rung at a time (parity with the per-heading
334+
typography editor). Retunes font-size / padding / min-height for a
335+
single rung; picking a rung also drives the live preview above. The
336+
flatten-all globals live in the All-tokens tab (Advanced group). -->
337337
<div>
338-
<div class="text-[9px] text-slate-500 mb-1">Per-size label — {activeBtnSize.token}</div>
339-
<div class="flex gap-0.5 mb-1.5 bg-black/5 dark:bg-white/5 rounded-lg p-0.5">
340-
{#each BTN_FONT_SIZES as r (r.rung)}
338+
<div class="text-[9px] font-bold text-slate-500 uppercase tracking-wider mb-1">Per-size ({activeBtnRung.toUpperCase()})</div>
339+
<div class="flex gap-0.5 mb-2 bg-black/5 dark:bg-white/5 rounded-lg p-0.5">
340+
{#each BTN_SIZE_RUNGS as rung (rung)}
341+
{@const touched = [`--sf-btn-${rung}-font-size`, `--sf-btn-${rung}-padding-block`, `--sf-btn-${rung}-padding-inline`, `--sf-btn-${rung}-min-height`].some((k) => k in overrides)}
341342
<button
342-
onclick={() => { activeBtnRung = r.rung; }}
343+
onclick={() => { activeBtnRung = rung; btnSize = rung; }}
343344
class={`relative flex-1 py-1 rounded-md text-[10px] font-bold uppercase transition-all cursor-pointer ${
344-
activeBtnRung === r.rung
345+
activeBtnRung === rung
345346
? "bg-black/12 dark:bg-white/12 text-slate-900 dark:text-white"
346347
: "text-slate-500 hover:text-slate-700 dark:hover:text-slate-300"
347348
}`}
348349
>
349-
{r.rung}
350-
{#if r.token in overrides}<span class="absolute top-0.5 right-0.5 w-1 h-1 rounded-full bg-indigo-400"></span>{/if}
350+
{rung}
351+
{#if touched}<span class="absolute top-0.5 right-0.5 w-1 h-1 rounded-full bg-indigo-400"></span>{/if}
351352
</button>
352353
{/each}
353354
</div>
354-
<div class="flex flex-wrap gap-1">
355-
{#each TEXT_STEPS as step (step)}
356-
{@const cur = currentStep(activeBtnSize.token, "text", TEXT_STEPS, activeBtnRung)}
357-
<button
358-
onclick={() => setStep(activeBtnSize.token, "text", step, activeBtnRung)}
359-
class={`px-2 py-1 rounded-lg text-[10px] border transition-all cursor-pointer ${
360-
cur === step
361-
? "bg-indigo-500/15 border-indigo-500/40 text-indigo-800 dark:text-indigo-200"
362-
: "border-black/8 dark:border-white/8 text-slate-600 dark:text-slate-400 hover:bg-black/5 dark:hover:bg-white/5"
363-
}`}
364-
>{step}</button>
365-
{/each}
366-
</div>
367-
</div>
368355

369-
<!-- Advanced — flatten-all knobs that override the whole size ladder. -->
370-
<div class="pt-1">
371-
<button
372-
onclick={() => { showBtnAdvanced = !showBtnAdvanced; }}
373-
aria-expanded={showBtnAdvanced}
374-
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"
375-
>
376-
<div class="text-[10px] font-semibold uppercase tracking-widest">Advanced</div>
377-
<span class="text-[10px]">{showBtnAdvanced ? "" : ""}</span>
378-
</button>
379-
{#if showBtnAdvanced}
380-
<div class="mt-2 space-y-2">
381-
<p class="text-[9px] text-slate-400 dark:text-slate-600 leading-snug">
382-
These <b>flatten every size</b>: once set they override the
383-
<code class="text-slate-500 dark:text-slate-400">.sf-btn--xs…xl</code> scale on all buttons.
384-
For proportional sizing use the multiplier + per-size knobs above.
385-
</p>
386-
{#each BUTTON_ADVANCED_TOKENS as t (t.token)}
387-
<SliderRow
388-
label={t.label} value={getVal(t)} min={t.min} max={t.max} step={t.step} unit={t.unit}
389-
help={t.token}
390-
overridden={t.token in overrides}
391-
onChange={(v) => onSet(t.token, `${v}${t.unit}`)}
392-
onReset={() => onReset(t.token)}
393-
rawDefault={t.rawDefault}
394-
variableOptions={t.variableOptions}
395-
currentRaw={overrides[t.token]}
396-
onRawSet={(v) => onSet(t.token, v)}
397-
/>
398-
{#if SCALE_SHADOWING.has(t.token) && t.token in overrides}
399-
<div class="text-[9px] text-amber-600 dark:text-amber-400 -mt-1 pl-0.5">↕ overrides every size (.sf-btn--xs…xl)</div>
400-
{/if}
356+
<!-- Label size for the active rung (text scale steps) -->
357+
<div class="mb-2">
358+
<div class="text-[9px] text-slate-500 mb-1">Label size — {activeFontToken}</div>
359+
<div class="flex flex-wrap gap-1">
360+
{#each TEXT_STEPS as step (step)}
361+
{@const cur = currentStep(activeFontToken, "text", TEXT_STEPS, activeFontDef)}
362+
<button
363+
onclick={() => setStep(activeFontToken, "text", step, activeFontDef)}
364+
class={`px-2 py-1 rounded-lg text-[10px] border transition-all cursor-pointer ${
365+
cur === step
366+
? "bg-indigo-500/15 border-indigo-500/40 text-indigo-800 dark:text-indigo-200"
367+
: "border-black/8 dark:border-white/8 text-slate-600 dark:text-slate-400 hover:bg-black/5 dark:hover:bg-white/5"
368+
}`}
369+
>{step}</button>
401370
{/each}
402-
403-
<div>
404-
<div class="text-[9px] text-slate-500 mb-1">Label size — flatten all — --sf-btn-font-size</div>
405-
<div class="flex flex-wrap gap-1">
406-
{#each TEXT_STEPS as step (step)}
407-
{@const cur = currentStep("--sf-btn-font-size", "text", TEXT_STEPS, "m")}
408-
<button
409-
onclick={() => setStep("--sf-btn-font-size", "text", step, "m")}
410-
class={`px-2 py-1 rounded-lg text-[10px] border transition-all cursor-pointer ${
411-
cur === step
412-
? "bg-indigo-500/15 border-indigo-500/40 text-indigo-800 dark:text-indigo-200"
413-
: "border-black/8 dark:border-white/8 text-slate-600 dark:text-slate-400 hover:bg-black/5 dark:hover:bg-white/5"
414-
}`}
415-
>{step}</button>
416-
{/each}
417-
</div>
418-
{#if "--sf-btn-font-size" in overrides}
419-
<div class="text-[9px] text-amber-600 dark:text-amber-400 mt-1 pl-0.5">↕ overrides every size (.sf-btn--xs…xl)</div>
420-
{/if}
421-
</div>
422371
</div>
423-
{/if}
372+
</div>
373+
374+
<!-- Padding + min-height sliders for the active rung -->
375+
{#each (["pb", "pi", "mh"] as const) as prop (prop)}
376+
{@const r = rungRow(activeBtnRung, prop)}
377+
<SliderRow
378+
label={r.label} value={getVal(r)} min={r.min} max={r.max} step={r.step} unit={r.unit}
379+
help={r.token}
380+
overridden={r.token in overrides}
381+
onChange={(v) => onSet(r.token, `${v}${r.unit}`)}
382+
onReset={() => onReset(r.token)}
383+
rawDefault={r.rawDefault}
384+
variableOptions={r.variableOptions}
385+
currentRaw={overrides[r.token]}
386+
onRawSet={(v) => onSet(r.token, v)}
387+
/>
388+
{/each}
424389
</div>
425390

426391
{#if Object.keys(overrides).some((k) => k.startsWith("--sf-btn-"))}

0 commit comments

Comments
 (0)