|
46 | 46 | { 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 }, |
47 | 47 | ]; |
48 | 48 |
|
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 | | -
|
65 | 49 | // Everyday label-size multiplier — scales every button's font-size by one |
66 | 50 | // 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). |
68 | 52 | const BTN_FONT_SCALE = { token: "--sf-btn-font-scale", default: 1, min: 0.75, max: 1.5, step: 0.05 }; |
69 | 53 |
|
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 | + } |
80 | 88 |
|
81 | 89 | const CARD_TOKENS: Array<{ label: string; token: string; unit: string; min: number; max: number; step: number; default: number; rawDefault: string; variableOptions: VarOption[] }> = [ |
82 | 90 | { 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 | 94 | { 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 }, |
87 | 95 | ]; |
88 | 96 |
|
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 | | -
|
99 | 97 | function resetButtonTokens() { |
100 | 98 | for (const k of Object.keys(overrides)) { |
101 | 99 | if (k.startsWith("--sf-btn-")) onReset(k); |
102 | 100 | } |
103 | 101 | } |
104 | 102 |
|
105 | 103 | let showButton = $state(false); |
106 | | - let showBtnAdvanced = $state(false); |
107 | 104 | 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); |
109 | 107 | let showCard = $state(false); |
110 | 108 |
|
111 | 109 | // Preview-only state — never touches overrides, just picks which real |
|
232 | 230 | <div class="flex flex-wrap gap-1"> |
233 | 231 | {#each BTN_SIZES as s (s)} |
234 | 232 | <button |
235 | | - onclick={() => { btnSize = s; }} |
| 233 | + onclick={() => { btnSize = s; activeBtnRung = s; }} |
236 | 234 | class={`px-2.5 py-1 rounded-lg text-[10px] border transition-all cursor-pointer uppercase ${ |
237 | 235 | btnSize === s |
238 | 236 | ? "bg-indigo-500/15 border-indigo-500/40 text-indigo-800 dark:text-indigo-200" |
|
332 | 330 | </div> |
333 | 331 | </div> |
334 | 332 |
|
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). --> |
337 | 337 | <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)} |
341 | 342 | <button |
342 | | - onclick={() => { activeBtnRung = r.rung; }} |
| 343 | + onclick={() => { activeBtnRung = rung; btnSize = rung; }} |
343 | 344 | class={`relative flex-1 py-1 rounded-md text-[10px] font-bold uppercase transition-all cursor-pointer ${ |
344 | | - activeBtnRung === r.rung |
| 345 | + activeBtnRung === rung |
345 | 346 | ? "bg-black/12 dark:bg-white/12 text-slate-900 dark:text-white" |
346 | 347 | : "text-slate-500 hover:text-slate-700 dark:hover:text-slate-300" |
347 | 348 | }`} |
348 | 349 | > |
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} |
351 | 352 | </button> |
352 | 353 | {/each} |
353 | 354 | </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> |
368 | 355 |
|
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> |
401 | 370 | {/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> |
422 | 371 | </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} |
424 | 389 | </div> |
425 | 390 |
|
426 | 391 | {#if Object.keys(overrides).some((k) => k.startsWith("--sf-btn-"))} |
|
0 commit comments