// Section builders for the live-preview gallery. Each returns an HTML string // built from real framework classes + the pv-* skin, arranged so every // configurable token/class is visible and reacts live to the configurator. // Foundations enumerate the real API via ./catalog; components are curated. import { page, section, specimen, specimenFull, tag, cluster, grid, stack, well, frame, esc } from "./specimen"; import { familySteps, classesOfKind, tokensMatching, tokensByGroup, classesByKind, GRADS } from "./catalog"; const RAMP_STEPS = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950]; const RAMP_FAMILIES = ["primary", "secondary", "tertiary", "action", "base", "neutral"]; const STATUS = ["success", "warning", "danger", "info"]; const rampRow = (family: string) => specimen( `--sf-color-${family}-*`, `
${RAMP_STEPS.map( (s) => ``, ).join("")}
`, ); // ── Color ──────────────────────────────────────────────────────────────── export function color(): string { const ramps = stack("s", ...RAMP_FAMILIES.map(rampRow)); const status = grid( 8, ...STATUS.map((s) => specimen(`--sf-color-${s}`, ``)), ); const grads = grid( 9, ...GRADS.map((g) => specimen(`--sf-gradient-${g}`, `
`)), ); const surfaceTokens = tokensMatching("--sf-color-surface", "--sf-color-inset", "--sf-color-raised") .map((t) => t.name); const surfaces = grid( 10, ...surfaceTokens.map((name) => specimen(name, `
`), ), ); const onColor = grid( 9, ...["primary", "secondary", "action", "success", "warning", "danger", "info"].map((f) => specimen( `text--on-${f}`, `
Aa
`, ), ), ); const textRoles = well(`
Heading — --sf-color-heading Body text — --sf-color-text Secondary — --sf-color-text--subtle Muted — --sf-color-text--muted Link — --sf-color-link
`); const borders = grid( 10, ...["border", "border--subtle", "border--strong"].map((b) => specimen( `--sf-color-${b}`, `
`, ), ), ); return page( "Color", "Every brand family ramp (50–950), status colours, gradients, surfaces, text roles, borders and readable text-on-colour pairs. Change any colour token in the Colors panel and watch the whole system re-tint.", section("Brand family ramps", ramps, "primary · secondary · tertiary · action · base · neutral"), section("Status colours", status), section("Gradients", grads), section("Surfaces", surfaces), section("Text roles & links", textRoles), section("Borders", borders), section("Text on colour (contrast pairs)", onColor), ); } // ── Typography ───────────────────────────────────────────────────────────── export function type(): string { const display = well(stack( "xs", `Display L`, `Display M`, `Display S`, )); const headings = stack( "xs", ...["2xl", "xl", "l", "m", "s", "xs"].map( (s) => `Heading ${s.toUpperCase()} — --sf-text-${s}`, ), ); const prose = `

Body — the quick brown fox jumps over the lazy dog. Good typography builds hierarchy and trust.

Secondary — supporting copy that helps without competing.

Muted / caption — timestamps, tags, helper text.

const tokens = design()bold, italic, underline, a link.

`; const families = grid( 12, ...["body", "heading", "mono"].map((f) => specimen( `--sf-font-${f}`, `
Ag 123
`, ), ), ); const weights = familySteps("--sf-font-weight-"); const weightRow = weights.length ? well(cluster( ...weights.map((w) => specimen( `weight-${w}`, `Aa`, ), ), )) : ""; const leading = well(`

Tight leading — --sf-leading-tight. Two or three lines of copy to show the rhythm.

Normal leading — --sf-leading-normal. Two or three lines of copy to show the rhythm.

Relaxed leading — --sf-leading-relaxed. Two or three lines of copy to show the rhythm.

`); return page( "Typography", "Display and heading scale, body prose, font families, weights and leading — all fluid via --sf-text-* and the type-ratio knobs.", section("Display scale", display), section("Heading scale", headings), section("Font families", well(families)), ...(weightRow ? [section("Font weights", weightRow)] : []), section("Line height", leading), section("Prose, inline & links", well(prose)), ); } // ── Spacing ──────────────────────────────────────────────────────────────── export function space(): string { const steps = familySteps("--sf-space-"); const bars = well( `
${steps .map( (s) => `
${tag(s)}
`, ) .join("")}
`, ); const gapDemo = stack( "s", ...(["xs", "s", "m", "l"] as const).map((g) => specimen( `sf-cluster--${g}`, `
ABC
`, ), ), ); const sizeSteps = familySteps("--sf-size-"); const sizes = sizeSteps.length ? well(`
${sizeSteps .map( (s) => `
${tag(`size-${s}`)}
`, ) .join("")}
`) : ""; return page( "Spacing", "The fluid spacing scale (--sf-space-*), the control-size scale (--sf-size-*), and how they drive real gaps. Adjust the base/scale in the Spacing panel and every bar and gap rescales together.", section("Spacing scale", bars), ...(sizes ? [section("Size scale (--sf-size-*)", sizes)] : []), section("Gaps in context (sf-cluster)", gapDemo), ); } // ── Borders & radius ────────────────────────────────────────────────────── export function borders(): string { const radii = ["xs", "s", "m", "l", "xl", "2xl", "full"]; const radiusRow = well(cluster( ...radii.map((r) => specimen(`--sf-radius-${r}`, `
`)), )); const widthSteps = familySteps("--sf-border-width-"); const widths = well(cluster( ...widthSteps.map((w) => specimen( `--sf-border-width-${w}`, `
`, ), ), )); const styles = well(cluster( ...["solid", "dashed", "dotted"].map((s) => specimen( s, `
`, ), ), )); const divider = well(`
Above
Dashed
Below
`); return page( "Borders & radius", "Radius scale, border widths, styles and the divider — driven by --sf-radius-* and --sf-border-*.", section("Radius scale", radiusRow), section("Border widths", widths), section("Border styles", styles), section("Dividers (.sf-divider)", divider), ); } // ── Shadows ──────────────────────────────────────────────────────────────── export function shadows(): string { const steps = ["xs", "s", "m", "l", "xl", "2xl"]; const row = well(cluster( ...steps.map((s) => specimen(`--sf-shadow-${s}`, `
`)), )); return page( "Shadows & elevation", "The elevation scale (--sf-shadow-*). Tune shadow colour/opacity/spread in the Shadows panel to restyle every level at once.", section("Elevation scale", row), ); } // ── Effects ──────────────────────────────────────────────────────────────── export function effects(): string { const grads = grid( 9, ...GRADS.map((g) => specimen(`--sf-gradient-${g}`, `
`)), ); const util = classesOfKind("utility").slice(0, 24); const utilList = util.length ? well(`
${util.map((c) => `${esc(c.selector)}`).join("")}
`) : ""; return page( "Effects", "Gradients, blur/opacity and background-layer utilities.", section("Gradients", grads), ...(utilList ? [section("Utility classes", utilList)] : []), ); } // ── Motion ───────────────────────────────────────────────────────────────── export function motion(): string { const anim = classesOfKind("motion").filter((c) => c.selector.startsWith(".sf-")); const tiles = grid( 10, ...anim.slice(0, 24).map((c) => { const cls = c.selector.replace(/^\./, "").split(/[ :>]/)[0]; return specimen(c.selector, `
`); }), ); return page( "Motion", "Animation presets and transition tokens. Switch the preview motion mode (top-right) to preview slow / reduced motion.", section("Animation presets", tiles, "Tiles replay their animation on load / tab switch."), ); } // ── Layout ───────────────────────────────────────────────────────────────── export function layout(): string { const box = (t: string) => `${esc(t)}`; const stackDemo = specimen( "sf-stack (vertical rhythm)", `
${box("1")}${box("2")}${box("3")}
`, ); const clusterDemo = specimen( "sf-cluster (wrap + gap)", `
${box("one")}${box("two")}${box("three")}${box("four")}
`, ); const gridCols = stack( "s", ...([2, 3, 4] as const).map((n) => specimenFull( `sf-grid-cols-${n}`, `
${Array.from({ length: n }, (_, i) => box(String(i + 1))).join("")}
`, ), ), ); const switcherDemo = specimenFull( "sf-switcher (auto row→stack)", `
${box("a")}${box("b")}${box("c")}
`, ); const sidebarDemo = specimenFull( "sf-sidebar", `
aside
main content area
`, ); const centerDemo = specimenFull( "sf-center", `
${box("centered")}
`, ); const asymGrid = stack( "s", ...(["1-2", "2-1", "1-3"] as const).map((c) => specimenFull( `sf-grid-cols-${c}`, `
${box("a")}${box("b")}
`, ), ), ); const bentoDemo = specimenFull( "sf-bento — sf-bento-featured / -wide / -tall span modifiers", `
${[ ["sf-bento-featured", "featured (2×2)"], ["", "b"], ["", "c"], ["sf-bento-wide", "wide (2×1)"], ["sf-bento-tall", "tall (1×2)"], ["", "f"], ] .map(([mods, t]) => `
${esc(t)}
`) .join("")}
`, ); const frameDemo = grid( 9, ...(["video", "square", "portrait"] as const).map((r) => specimen( `sf-frame--${r}`, `
`, ), ), ); return page( "Layout", "The token-driven layout primitives — stack, cluster, grid, switcher, sidebar, center, bento and aspect-ratio frames. Boxes make the gaps and columns visible; every gap is a --sf-space-* token.", section("Stack & cluster", well(stack("m", stackDemo, clusterDemo))), section("Grid column presets", well(gridCols)), section("Asymmetric grids", well(asymGrid)), section("Bento grid", well(bentoDemo)), section("Aspect-ratio frames", well(frameDemo)), section("Switcher, sidebar & center", well(stack("m", switcherDemo, sidebarDemo, centerDemo))), ); } // ── Components ───────────────────────────────────────────────────────────── const BTN_FAMILIES = ["primary", "secondary", "tertiary", "action", "base", "neutral", "success", "warning", "danger", "info"]; // [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 rows use frame() (a non-card backdrop), NOT well(): the framework's // `.sf-card .sf-btn` rule pins a nested button's label to --sf-text-s, which // would flatten the per-rung font ladder in the Size-scale demo (padding and // min-height still grow, but the label wouldn't). Cards below keep .sf-card. // Families — every shipped colour family as a fill button. const families = frame(cluster( ...BTN_FAMILIES.map((f) => btn(`sf-btn--${f}`, f[0].toUpperCase() + f.slice(1))), )); // 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 = frame(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 = frame( `
${BTN_SIZES.map( ([s, label]) => btn(`sf-btn--primary${s ? ` sf-btn--${s}` : ""}`, label), ).join("")}
`, ); // 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 = frame(`
`); // States — default, disabled, loading. const states = frame(cluster( btn("sf-btn--primary", "Default"), ``, ``, )); // 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 = `

Media card

__media · __avatar · __title

Composed from the full card slot API. Every dimension — padding, radius, gap, media ratio, avatar size — is token-driven and reacts live.

`; // The three shipped card modifiers, side by side. const card = (mods: string, title: string, body: string, btnMods: string, action: string) => `

${esc(title)}

${esc(body)}

`; 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 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."), section("States", states), section("Card — full slot API", mediaCard), section("Card modifiers", cardModifiers, "base · bordered · elevated + interactive"), ); } // ── Macros & utilities ───────────────────────────────────────────────────── // Every one of the 58 shipped macro classes is applied here for real — this // gallery is a coverage floor (tests/preview-coverage.test.ts), not just a // reference; a bare class-name chip doesn't satisfy that spirit, so each // macro gets an actual specimen instead of only appearing in a text list. const LOREM_MACRO = "SLASHED ships a token-driven CSS framework with no build step and no JavaScript required to render a page."; export function macros(): string { // Surfaces — the generic form (any colour via --sf-surface-color) plus // all 10 precomputed named variants. const surfaceNamed = ["primary", "secondary", "tertiary", "action", "neutral", "inverse", "success", "warning", "info", "danger"]; const surfaces = grid( 12, specimen( "sf-surface (custom --sf-surface-color)", `
Aa
`, ), ...surfaceNamed.map((s) => specimen( `sf-surface--${s}`, `
Aa
`, ), ), ); // Prose, not-prose & flow — real rhythm, not a swatch. const prose = well(`

Article heading

Paragraphs get automatic vertical rhythm from .sf-prose — no utility classes needed between them.

.sf-not-prose opts this box out of the prose rhythm above.

…and rhythm resumes after it.

`); const flow = well(`
One
Two — margin-block-start
Three — margin-block-start
`); // Truncation — one line, then 2/3/N-line clamps, all on the same copy. const truncation = grid( 14, specimen("sf-truncate", `
${LOREM_MACRO}
`), specimen("sf-line-clamp-2", `

${LOREM_MACRO}

`), specimen("sf-line-clamp-3", `

${LOREM_MACRO}

`), specimen( "sf-line-clamp-N (--sf-line-clamp:5)", `

${LOREM_MACRO} ${LOREM_MACRO}

`, ), ); // Equal height — three unevenly-tall children forced level. const equalHeight = well( `
Short
Two
lines
Three
lines
here
`, ); // Aspect ratio — content-agnostic, distinct from the .sf-frame primitive. const aspect = well( `
${["16 / 9", "1 / 1", "4 / 3"] .map( (r) => `
${tag(`--sf-aspect:${r}`)}
`, ) .join("")}
`, ); // Tabular numbers — a real numeric column so digits visibly align. const tabularNums = well(`
Invoice #11,024.50
Invoice #82398.00
Invoice #91312,760.75
`); // Links — external marker + underline affordances, as real anchors. const links = well(`
sf-link-external sf-link--subtle (hover me) sf-link--reverse (hover me)
`); // Drop shadows — filter: drop-shadow() follows alpha shape, so a ring // (mask-cut circle) shows the hole surviving the shadow, unlike box-shadow. const dropShadows = grid( 9, ...(["xs", "s", "m", "l", "xl"] as const).map((s) => specimen( `sf-drop-shadow-${s}`, `
`, ), ), ); // Scrolling — shadow mask, snap, and content-visibility, all with real // overflowing content so the effect is actually observable. const scrollShadow = well( `
${Array.from( { length: 8 }, (_, i) => `
Row ${i + 1}
`, ).join("")}
`, ); const scrollSnap = well( `
${["a", "b", "c"] .map( (l, i) => `
${tag(`section ${l}`)}
`, ) .join("")}
`, ); const contentAuto = well( `

content-visibility:auto — off-screen instances of this section skip layout/paint until scrolled near.

`, ); // Overflow fade — genuinely overflowing content in the axis each variant // fades: a single-line nowrap row for the horizontal/inline variants, and // a fixed-height column for the vertical/block variants. flex-wrap must // be forced off on the row: .sf-cluster wraps by default, which would // just break it onto multiple lines instead of overflowing it. const fadeRow = () => `
${Array.from( { length: 10 }, (_, i) => `${i + 1}`, ).join("")}
`; const fadeColumn = () => `
${Array.from( { length: 6 }, (_, i) => `${i + 1}`, ).join("")}
`; const overflowFade = grid( 11, ...([ ["", fadeRow], ["--right", fadeRow], ["--left", fadeRow], ["--inline", fadeRow], ["--top", fadeColumn], ["--bottom", fadeColumn], ["--block", fadeColumn], ] as const).map(([m, shape]) => specimen(`sf-overflow-fade${m}`, `
${shape()}
`)), ); // Scrim, surface-bg & text-protect — text over a token-driven gradient // "photo" (no external asset needed). const photo = "var(--sf-gradient-brand, var(--sf-color-primary))"; const scrimCard = (mods: string, label: string) => `
${esc(label)}
`; const scrims = grid( 12, specimen("sf-scrim sf-scrim--bottom", scrimCard("sf-scrim--bottom", "Legible headline")), specimen("sf-scrim sf-scrim--top", scrimCard("sf-scrim--top", "Legible headline")), specimen("sf-scrim sf-scrim--full", scrimCard("sf-scrim--full", "Legible headline")), ); const surfaceBg = well( `
sf-surface-bg — named background preset
`, ); const textProtect = `

Readable over a photo

`; // Text gradient. const textGradient = well(`

Gradient headline

`); // Overlap — an avatar pulled down over the card that follows it // (.sf-overlap--down; the plain .sf-overlap pulls UP onto whatever // precedes it instead — not what a leading avatar needs here), plus the // two block-axis directional variants on their own swatches. const avatar = `
`; const overlapHost = frame(`
${avatar}

.sf-overlap-host reserves space so in-flow content clears the intruding avatar above.

`); const overlapDirections = grid( 10, ...([ ["", "sf-overlap (pulls up)"], ["--down", "sf-overlap--down (pulls down)"], ] as const).map(([m, label]) => specimen(label, `
`), ), ); // No-tap-highlight — no visual difference to show (it only suppresses the // mobile tap-highlight overlay); still applied to a real element for // coverage rather than left to a text mention. const noTapHighlight = well( `sf-no-tap-highlight — suppresses the mobile tap-highlight overlay (no on-screen difference here)`, ); return page( "Macros & utilities", "Every shipped macro (@layer slashed.macros), applied for real — not just named.", section("Surfaces (sf-surface, sf-surface--*)", surfaces), section("Prose, not-prose & flow", well(stack("m", prose, flow))), section("Truncation (sf-truncate, sf-line-clamp-*)", truncation), section("Equal height (sf-equal-height)", equalHeight), section("Aspect ratio (sf-aspect)", aspect), section("Tabular numbers (sf-tabular-nums)", tabularNums), section("Links (sf-link-external, sf-link--subtle, sf-link--reverse)", links), section("Drop shadows (sf-drop-shadow-*)", dropShadows, "Alpha-following filter: drop-shadow() — note the shadow survives the masked hole."), section("Scroll shadow (sf-scroll-shadow)", scrollShadow), section("Scroll snap (sf-scroll-snap)", scrollSnap), section("Content-visibility (sf-content-auto)", contentAuto), section("Overflow fade (sf-overflow-fade + edge/axis modifiers)", overflowFade), section("Scrim over media (sf-scrim, sf-scrim--*, sf-scrim__content)", scrims), section("Named background preset (sf-surface-bg)", surfaceBg), section("Text protect (sf-text-protect)", well(textProtect)), section("Text gradient (sf-text-gradient)", textGradient), section("Overlap (sf-overlap, sf-overlap-host)", well(stack("m", overlapHost, overlapDirections))), section("No tap highlight (sf-no-tap-highlight)", noTapHighlight), ); } // ── Advanced / All (exhaustive, searchable reference) ────────────────────── const chips = (items: string[]) => `
${items .map((s) => `${esc(s)}`) .join("")}
`; export function advanced(): string { // Every class, grouped by kind — guarantees the whole class API is at least // listed even where a rich specimen would be noise (state/print/a11y). const classSections = classesByKind().map(({ kind, classes }) => section(`${kind} · ${classes.length}`, well(chips(classes.map((c) => c.name)))) ); // Every public token, grouped — the full variable reference the domain tabs // visualise. Kept exhaustive so nothing configurable is undocumented here. const tokenSections = tokensByGroup().map(({ group, tokens }) => section(`${group} · ${tokens.length}`, well(chips(tokens.map((t) => t.name)))) ); return page( "Advanced / All", "The exhaustive reference: every class and every variable in the framework, grouped. The domain tabs visualise these live — this tab is the complete searchable index (including advanced and draft surface).", section("Classes", stack("s", ...classSections.map((s) => s))), section("Variables (tokens)", stack("s", ...tokenSections.map((s) => s))), ); }