// 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}-*`,
`
`,
);
// 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)",
`
`);
// 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 = () =>
`
`);
// 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))),
);
}