Skip to content
Merged
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
93 changes: 73 additions & 20 deletions configurator/src/lib/preview/sections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -325,48 +325,101 @@ export function layout(): string {

// ── Components ─────────────────────────────────────────────────────────────
const BTN_FAMILIES = ["primary", "secondary", "tertiary", "action", "base", "neutral", "success", "warning", "danger", "info"];
const BTN_SIZES: [string, string][] = [["xs", "XS"], ["s", "Small"], ["", "Default"], ["l", "Large"], ["xl", "XL"]];
// [modifier suffix, visible label] — "" is the default (medium) rung.
const BTN_SIZES: [string, string][] = [["xs", "XS"], ["s", "S"], ["", "M"], ["l", "L"], ["xl", "XL"]];

export function components(): string {
const btn = (mods: string, label: string) => `<button class="sf-btn ${mods}">${esc(label)}</button>`;

const families = well(cluster(...BTN_FAMILIES.map((f) => btn(`sf-btn--${f}`, f[0].toUpperCase() + f.slice(1)))));
const stylesRow = well(cluster(
btn("sf-btn--primary", "Fill"),
btn("sf-btn--primary sf-btn--soft", "Soft"),
btn("sf-btn--primary sf-btn--outline", "Outline"),
btn("sf-btn--primary sf-btn--gradient", "Gradient"),
// Families — every shipped colour family as a fill button.
const families = well(cluster(
...BTN_FAMILIES.map((f) => btn(`sf-btn--${f}`, f[0].toUpperCase() + f.slice(1))),
));
const sizes = well(`<div class="sf-cluster sf-cluster--s" style="align-items:center">${BTN_SIZES.map(
([s, label]) => btn(`sf-btn--primary${s ? ` sf-btn--${s}` : ""}`, label),
).join("")}</div>`);

// Styles — fill · soft · outline · gradient, on a brand family and a neutral.
const styleRow = (family: string, withGradient: boolean) => cluster(
btn(`sf-btn--${family}`, "Fill"),
btn(`sf-btn--${family} sf-btn--soft`, "Soft"),
btn(`sf-btn--${family} sf-btn--outline`, "Outline"),
...(withGradient ? [btn(`sf-btn--${family} sf-btn--gradient`, "Gradient")] : []),
);
const styles = well(stack("s", styleRow("primary", true), styleRow("neutral", false)));

// Size scale — the hero. Aligned to a common baseline so the per-rung
// font-size / padding / min-height ladder reads as a clear staircase.
const sizes = well(
`<div class="sf-cluster sf-cluster--s" style="align-items:flex-end">${BTN_SIZES.map(
([s, label]) => btn(`sf-btn--primary${s ? ` sf-btn--${s}` : ""}`, label),
).join("")}</div>`,
);

// Full width — --block stretches unconditionally; --block-cq only inside a
// query container narrower than 20rem, so it's shown inside a 16rem container
// (dashed) where it actually fills its width.
// align-items:flex-start so buttons size to content — only --block (explicit
// 100%) and the block-cq inside the 16rem query container actually stretch.
const widths = well(`<div class="sf-stack sf-stack--s" style="align-items:flex-start">
<button class="sf-btn sf-btn--primary sf-btn--block">Block — full width</button>
<button class="sf-btn sf-btn--neutral sf-btn--outline sf-btn--block">Block — outline</button>
<button class="sf-btn sf-btn--primary sf-btn--block-cq">Block-cq — auto here (wide container)</button>
<div style="container-type:inline-size;inline-size:16rem;padding:var(--sf-space-s);border:1px dashed var(--sf-color-border);border-radius:var(--sf-radius-s)">
<button class="sf-btn sf-btn--primary sf-btn--block-cq">Block-cq — fills</button>
</div>
</div>`);

// States — default, disabled, loading.
const states = well(cluster(
btn("sf-btn--primary", "Default"),
`<button class="sf-btn sf-btn--primary" disabled>Disabled</button>`,
`<button class="sf-btn sf-btn--primary sf-is-loading">Loading</button>`,
));

// A rich card that exercises the full slot API: media, avatar header,
// title, body and a footer action row. Media/avatar use a token-driven
// gradient (no external asset) so they re-tint live with the colour panel.
const swatch = "var(--sf-gradient-primary, var(--sf-color-primary))";
const mediaCard = `<article class="sf-card sf-card--elevated" style="max-inline-size:22rem">
<div class="sf-card__media" style="background:${swatch}"></div>
<div class="sf-card__header">
<div class="sf-cluster sf-cluster--s" style="align-items:center;flex-wrap:nowrap">
<div class="sf-card__avatar" style="background:${swatch}"></div>
<div class="sf-stack sf-stack--xs">
<h3 class="sf-card__title" style="margin:0">Media card</h3>
<span class="pv-secondary">__media · __avatar · __title</span>
</div>
</div>
</div>
<div class="sf-card__body"><p class="pv-secondary">Composed from the full card slot API. Every dimension — padding, radius, gap, media ratio, avatar size — is token-driven and reacts live.</p></div>
<div class="sf-card__footer">${cluster(
btn("sf-btn--primary sf-btn--s", "Open"),
btn("sf-btn--outline sf-btn--s", "Share"),
)}</div>
</article>`;

// The three shipped card modifiers, side by side.
const card = (mods: string, title: string, body: string, btnMods: string, action: string) =>
`<article class="sf-card ${mods}">
<div class="sf-card__header"><h3 class="sf-card__title">${esc(title)}</h3></div>
<div class="sf-card__body"><p class="pv-secondary">${esc(body)}</p></div>
<div class="sf-card__footer">${btn(btnMods, action)}</div>
</article>`;
const cards = grid(
16,
card("", "Base card", "Default surface, radius, border and text tokens.", "sf-btn--primary sf-btn--s", "Open"),
card("sf-card--bordered", "Bordered card", "The shipped bordered modifier for clearer structure.", "sf-btn--outline sf-btn--s", "Details"),
card("sf-card--elevated sf-card--interactive", "Interactive card", "Elevation and interaction states react to tokens.", "sf-btn--soft sf-btn--s", "Select"),
const cardModifiers = grid(
15,
card("", "Base", "Default surface, radius, border and shadow.", "sf-btn--primary sf-btn--s", "Open"),
card("sf-card--bordered", "Bordered", "Keeps the border, drops the shadow.", "sf-btn--outline sf-btn--s", "Details"),
card("sf-card--elevated sf-card--interactive", "Interactive", "Elevated, with a hover/focus lift.", "sf-btn--soft sf-btn--s", "Select"),
);

return page(
"Components",
"Buttons and cards — every family, style, the full XS–XL size scale and states. This is where the size-scale knobs in the Components panel show their effect immediately.",
section("Button families", families),
section("Styles", stylesRow),
section("Size scale (xs · s · default · l · xl)", sizes, "Global Padding / Min-height / Label-size knobs override this whole scale."),
"Buttons and cards built from the real .sf-btn / .sf-card classes — every family and style, the full XS–XL size ladder, states, full-width and the card slot API. Edit the Components panel and each specimen reacts live.",
section("Button families", families, `${BTN_FAMILIES.length} shipped colour families`),
section("Styles", styles, "fill · soft · outline · gradient"),
section("Size scale (xs · s · default · l · xl)", sizes, "Each rung has its own font-size, padding and min-height knob (Components panel ▸ Per-size); the Label-size multiplier scales the whole ladder proportionally."),
section("Full width", widths, "sf-btn--block stretches to the container; sf-btn--block-cq does so only in a narrow query container."),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 sf-btn--block-cq mentioned but never shown

The section note promises that sf-btn--block-cq "does so only in a narrow query container," yet neither button in widths carries that class. A developer inspecting the preview to understand how sf-btn--block-cq differs from sf-btn--block gets no visual example — just a prose claim they cannot verify in-page.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

section("States", states),
section("Cards", cards),
section("Card — full slot API", mediaCard),
section("Card modifiers", cardModifiers, "base · bordered · elevated + interactive"),
);
}

Expand Down