From 8712cbd0344ecfcd7d07429c3e685cd959609729 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 12 Jul 2026 21:05:33 +0000 Subject: [PATCH 1/4] fix(preview): repair invisible dividers, gradients, motion tiles and aspect frames in the live-preview gallery MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Audited every live-preview tab in the configurator by rendering it in a real browser instead of just reading the generated markup. Several specimens had zero visible pixels despite correct HTML/tokens: - core/layout.css: `.sf-divider` collapsed to 0 inline-size inside a flex column (e.g. `.sf-stack`) because `
` doesn't participate in flex stretch sizing the way block `
`s do — a real framework bug, not just a preview issue. Give it an explicit `inline-size: 100%`. - preview/skin.ts: `.pv-grad` (gradient swatches) and the Motion tab's `.pv-motion-tile` had the same 0-width flex-stretch problem, plus the GRADS list only had 4 of the 6 solid-colour gradient tokens (missing --sf-gradient-secondary / --sf-gradient-action), so those two rendered `background-image: none`. - preview/sections.ts: the Color tab's Surfaces demo and Borders demo, and the Layout tab's aspect-ratio Frame demo, all relied on flex stretch for their width and rendered as a collapsed circle / thin line / nothing at all — now given explicit inline-size. - Motion tab's exit/fade-out/scale-down tiles play once and legitimately end at opacity:0 (correct real behaviour), which read as "broken" in a static gallery — force infinite alternate playback for the demo tiles only, unlayered so it beats @layer slashed.motion. Verified visually tab-by-tab (Color, Type, Spacing, Borders, Shadows, Effects, Motion, Layout, Components, Macros) before and after. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_0113XBTydmejAnYJt9CTyYRc --- badges/badge-optimal.json | 2 +- configurator/src/lib/preview/sections.ts | 8 ++++---- configurator/src/lib/preview/skin.ts | 13 +++++++++---- core/layout.css | 1 + 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/badges/badge-optimal.json b/badges/badge-optimal.json index 99f96683..46b854cc 100644 --- a/badges/badge-optimal.json +++ b/badges/badge-optimal.json @@ -1,7 +1,7 @@ { "schemaVersion": 1, "label": "optimal", - "message": "19.4 kB gzip", + "message": "19.5 kB gzip", "color": "brightgreen", "namedLogo": "css3" } diff --git a/configurator/src/lib/preview/sections.ts b/configurator/src/lib/preview/sections.ts index 273aa5bd..440e5a16 100644 --- a/configurator/src/lib/preview/sections.ts +++ b/configurator/src/lib/preview/sections.ts @@ -9,7 +9,7 @@ import { familySteps, classesOfKind, tokensMatching, tokensByGroup, classesByKin 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 GRADS = ["primary", "brand", "tertiary", "surface"]; +const GRADS = ["primary", "secondary", "tertiary", "action", "brand", "surface"]; const rampRow = (family: string) => specimen( @@ -35,7 +35,7 @@ export function color(): string { const surfaces = grid( 10, ...surfaceTokens.map((name) => - specimen(name, `
`), + specimen(name, `
`), ), ); const onColor = grid( @@ -59,7 +59,7 @@ export function color(): string { ...["border", "border--subtle", "border--strong"].map((b) => specimen( `--sf-color-${b}`, - `
`, + `
`, ), ), ); @@ -307,7 +307,7 @@ export function layout(): string { ...(["video", "square", "portrait"] as const).map((r) => specimen( `sf-frame--${r}`, - `
`, + `
`, ), ), ); diff --git a/configurator/src/lib/preview/skin.ts b/configurator/src/lib/preview/skin.ts index 1f58e478..358ab294 100644 --- a/configurator/src/lib/preview/skin.ts +++ b/configurator/src/lib/preview/skin.ts @@ -12,7 +12,7 @@ const RADII = ["xs", "s", "m", "l", "xl", "2xl", "full"]; const SHADOWS = ["xs", "s", "m", "l", "xl", "2xl"]; const SPACES = ["3xs", "2xs", "xs", "s", "m", "l", "xl", "2xl", "3xl", "4xl"]; const TYPE_STEPS = ["display-l", "display-m", "display-s", "2xl", "xl", "l", "m", "s", "xs"]; -const GRADS = ["primary", "brand", "tertiary", "surface"]; +const GRADS = ["primary", "secondary", "tertiary", "action", "brand", "surface"]; export function previewSkinCSS(): string { const rampClass = (family: string) => @@ -45,7 +45,7 @@ export function previewSkinCSS(): string { /* Status chips + gradients */ .pv-chip{display:block;inline-size:100%;block-size:44px;border-radius:var(--sf-radius-s);} ${STATUS.map((s) => `.pv-chip--${s}{background:var(--sf-color-${s});}`).join("")} - .pv-grad{block-size:44px;border-radius:var(--sf-radius-m);border:var(--sf-border-width-1) var(--sf-border-style) var(--sf-color-border);} + .pv-grad{inline-size:100%;block-size:44px;border-radius:var(--sf-radius-m);border:var(--sf-border-width-1) var(--sf-border-style) var(--sf-color-border);} ${GRADS.map((g) => `.pv-grad--${g}{background:var(--sf-gradient-${g});}`).join("")} /* Radius + shadow demo boxes */ .pv-demo-box{inline-size:56px;block-size:56px;background:var(--sf-color-primary-100);border:2px solid var(--sf-color-primary-300);} @@ -62,8 +62,13 @@ export function previewSkinCSS(): string { /* Generic filled block for layout-primitive demos (stack/cluster/grid gaps) */ .pv-box{background:var(--sf-color-primary-100);border:var(--sf-border-width-1) var(--sf-border-style) var(--sf-color-primary-300);border-radius:var(--sf-radius-s);padding:var(--sf-space-s);color:var(--sf-color-primary-700);font-size:var(--sf-text-s);font-family:var(--sf-font-mono);text-align:center;min-inline-size:3rem;} .pv-box--tall{min-block-size:4rem;display:flex;align-items:center;justify-content:center;} - /* Motion demo tile */ - .pv-motion-tile{inline-size:100%;block-size:56px;background:var(--sf-gradient-primary);border-radius:var(--sf-radius-m);} + /* Motion demo tile — "exit"/fade-out/scale-down presets end their real, + one-shot animation at opacity:0 (animation-fill-mode:both), which is + correct for actual exit transitions but would leave the tile blank + forever in a static gallery. Force infinite alternate playback here + (unlayered, so it beats @layer slashed.motion) purely so every tile + stays visible and keeps demonstrating its motion. */ + .pv-motion-tile{inline-size:100%;block-size:56px;background:var(--sf-gradient-primary);border-radius:var(--sf-radius-m);animation-iteration-count:infinite !important;animation-direction:alternate !important;} /* Tinted card variants (demo-only; unlayered so they beat .sf-card's layer) */ .pv-card--primary{background:var(--sf-color-primary);color:var(--sf-color-text--on-primary);} .pv-card--soft{background:var(--sf-color-primary-100);color:var(--sf-color-primary-700);}`; diff --git a/core/layout.css b/core/layout.css index 6d6e1f9f..eced9411 100644 --- a/core/layout.css +++ b/core/layout.css @@ -43,6 +43,7 @@ } .sf-divider { + inline-size: 100%; border: none; border-block-start: var(--sf-divider-width) var(--sf-divider-style) var(--sf-divider-color); margin-block: var(--sf-divider-gap); From 08834cf78445328c4c9ee40eaa1ec4fb93f0541a Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 13 Jul 2026 05:22:19 +0000 Subject: [PATCH 2/4] fix(preview): make the Macros tab render every macro instead of listing class names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Macros tab only rendered .sf-surface--* and then dumped the other 47 macro classes (.sf-prose, .sf-scrim, .sf-corner-scoop, .sf-overlap, .sf-overflow-fade, drop-shadows, line-clamp, etc.) as inert text chips — never actually applied to an element. Replaced that with a real specimen for every one of the 58 macro classes: prose/flow rhythm, truncation and line-clamp on real copy, equal-height, aspect-ratio, tabular numbers, links, alpha-following drop-shadows, scroll-shadow/snap, every overflow-fade edge, scrim/surface-bg/text-protect over a token-driven gradient backdrop, text-gradient, corner-scoop, and overlap. Also fixed the Layout tab's bento grid, which rendered 5 uniform cells and never exercised the .sf-bento-featured/-wide/-tall span modifiers that are the whole point of a bento layout. Two real usage bugs turned up while building this and got corrected in the demo (not the framework, which behaves correctly): - .sf-overlap pulls an element UP onto whatever precedes it; overlapping a card that follows requires .sf-overlap--down. The demo used the wrong variant and showed no overlap at all. - .sf-overflow-fade's row used .sf-cluster (wraps by default), so the "overflowing" content just wrapped onto new lines instead of overflowing — fixed with an explicit nowrap row, plus a separate overflowing column for the top/bottom/block variants (a single-line row can't demonstrate a vertical fade). Also bumped .sf-card's border-radius down in the corner-scoop demo — the card's own default oversized radius nearly circularized a 6rem swatch, making the scoop notch imperceptible. Verified every new specimen renders correctly in a real browser (scroll-shadow fade, scroll-snap sections, corner-scoop notches on all 4 corners, overlap actually overlapping, aspect-ratio boxes at their real ratios) before committing. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_0113XBTydmejAnYJt9CTyYRc --- configurator/src/lib/preview/sections.ts | 239 ++++++++++++++++++++++- 1 file changed, 229 insertions(+), 10 deletions(-) diff --git a/configurator/src/lib/preview/sections.ts b/configurator/src/lib/preview/sections.ts index 440e5a16..7eccff74 100644 --- a/configurator/src/lib/preview/sections.ts +++ b/configurator/src/lib/preview/sections.ts @@ -299,8 +299,17 @@ export function layout(): string { ), ); const bentoDemo = specimen( - "sf-bento", - `
${["a", "b", "c", "d", "e"].map((t) => `
${t}
`).join("")}
`, + "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, @@ -429,27 +438,237 @@ export function components(): string { } // ── 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, - ...["primary", "secondary", "tertiary", "action", "neutral", "success", "warning", "info"].map((s) => + specimen( + "sf-surface (custom --sf-surface-color)", + `
Aa
`, + ), + ...surfaceNamed.map((s) => specimen( `sf-surface--${s}`, `
Aa
`, ), ), ); - const macroClasses = classesOfKind("macro").filter((c) => c.selector.startsWith(".sf-")); - const list = well( - `
${macroClasses - .map((c) => `${esc(c.selector)}`) + + // 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.

+
  • First point
  • Second point
+
+

.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(``); + + // 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

`); + + // Corner scoop — the notch reveals whatever sits behind the element, so + // the card sits on a contrasting frame. + const scoopCard = (mods: string) => + `
`; + const cornerScoop = frame( + `
${[ + ["sf-corner-scoop", "default (top-right)"], + ["sf-corner-scoop sf-corner-scoop--top-left", "top-left"], + ["sf-corner-scoop sf-corner-scoop--bottom-left", "bottom-left"], + ["sf-corner-scoop sf-corner-scoop--bottom-right", "bottom-right"], + ] + .map(([mods, label]) => specimen(label, scoopCard(mods))) + .join("")}
`, + ); + + // 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 + // remaining 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)"], + ["--start", "sf-overlap--start"], + ["--end", "sf-overlap--end"], + ] 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", - "Surface macros and the full set of shipped macro utility classes.", - section("Surfaces (sf-surface--*)", surfaces), - section("All macro classes", list, `${macroClasses.length} classes in @layer slashed.macros`), + "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("Corner scoop (sf-corner-scoop + corner modifiers)", cornerScoop), + section("Overlap (sf-overlap, sf-overlap-host)", well(stack("m", overlapHost, overlapDirections))), + section("No tap highlight (sf-no-tap-highlight)", noTapHighlight), ); } From b9badcfaed2341bd937222d7f4afec53b258d0df Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 13 Jul 2026 05:53:49 +0000 Subject: [PATCH 3/4] fix(preview): reset inline-size on vertical dividers; dedupe GRADS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review feedback on PR #605 (Greptile + CodeRabbit, both independently caught the same issue): - core/layout.css: the inline-size:100% just added to .sf-divider bled into .sf-divider--vertical, which doesn't reset it — a vertical divider (used inside a row flex container, e.g. .sf-cluster) would inflate its flex-basis to the full row width instead of staying a narrow rule. Reset it with inline-size:auto on the modifier. Verified in-browser: vertical divider is back to a 1px-wide rule; the horizontal divider still spans its container. - GRADS was duplicated in sections.ts and skin.ts — the exact kind of drift that caused the blank-gradient bug fixed earlier in this PR. Extracted a single export from catalog.ts and imported it in both. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_0113XBTydmejAnYJt9CTyYRc --- configurator/src/lib/preview/catalog.ts | 7 +++++++ configurator/src/lib/preview/sections.ts | 3 +-- configurator/src/lib/preview/skin.ts | 3 ++- core/layout.css | 11 ++++++----- 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/configurator/src/lib/preview/catalog.ts b/configurator/src/lib/preview/catalog.ts index a48e6f60..939d32bf 100644 --- a/configurator/src/lib/preview/catalog.ts +++ b/configurator/src/lib/preview/catalog.ts @@ -32,6 +32,13 @@ export interface ApiClass { export const TOKENS: ApiToken[] = (apiIndex as { tokens: ApiToken[] }).tokens; export const CLASSES: ApiClass[] = (classIndex as { classes: ApiClass[] }).classes; +/** Solid-colour gradient families (--sf-gradient-*, excluding the directional + * --sf-gradient-fade--* and --sf-scrim-gradient). Shared between sections.ts + * (which renders the swatches) and skin.ts (which generates the + * .pv-grad--{family} rules they use) — the two must stay in lockstep or a + * swatch silently renders blank. */ +export const GRADS = ["primary", "secondary", "tertiary", "action", "brand", "surface"]; + export const isPublic = (t: ApiToken) => t.tier === "PUBLIC"; export const isAdvanced = (t: ApiToken) => t.tier === "PUBLIC-ADVANCED"; diff --git a/configurator/src/lib/preview/sections.ts b/configurator/src/lib/preview/sections.ts index 7eccff74..47cbda6c 100644 --- a/configurator/src/lib/preview/sections.ts +++ b/configurator/src/lib/preview/sections.ts @@ -4,12 +4,11 @@ // Foundations enumerate the real API via ./catalog; components are curated. import { page, section, specimen, tag, cluster, grid, stack, well, frame, esc } from "./specimen"; -import { familySteps, classesOfKind, tokensMatching, tokensByGroup, classesByKind } from "./catalog"; +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 GRADS = ["primary", "secondary", "tertiary", "action", "brand", "surface"]; const rampRow = (family: string) => specimen( diff --git a/configurator/src/lib/preview/skin.ts b/configurator/src/lib/preview/skin.ts index 358ab294..f451ce0f 100644 --- a/configurator/src/lib/preview/skin.ts +++ b/configurator/src/lib/preview/skin.ts @@ -5,6 +5,8 @@ // references a live --sf-* token, so configurator overrides drive these exactly // like the framework's own classes — the whole gallery reacts to every change. +import { 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"]; @@ -12,7 +14,6 @@ const RADII = ["xs", "s", "m", "l", "xl", "2xl", "full"]; const SHADOWS = ["xs", "s", "m", "l", "xl", "2xl"]; const SPACES = ["3xs", "2xs", "xs", "s", "m", "l", "xl", "2xl", "3xl", "4xl"]; const TYPE_STEPS = ["display-l", "display-m", "display-s", "2xl", "xl", "l", "m", "s", "xs"]; -const GRADS = ["primary", "secondary", "tertiary", "action", "brand", "surface"]; export function previewSkinCSS(): string { const rampClass = (family: string) => diff --git a/core/layout.css b/core/layout.css index eced9411..8fff2eeb 100644 --- a/core/layout.css +++ b/core/layout.css @@ -49,11 +49,12 @@ margin-block: var(--sf-divider-gap); } .sf-divider--vertical { - border-block-start: none; - border-inline-start: var(--sf-divider-width) var(--sf-divider-style) var(--sf-divider-color); - margin-block: 0; - margin-inline: var(--sf-divider-gap); - align-self: stretch; + inline-size: auto; + border-block-start: none; + border-inline-start: var(--sf-divider-width) var(--sf-divider-style) var(--sf-divider-color); + margin-block: 0; + margin-inline: var(--sf-divider-gap); + align-self: stretch; } .sf-divider--soft { --sf-divider-color: var(--sf-color-border--subtle); } From 88a10b5e308b5e8e7a83d49864061e4a18385a78 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 13 Jul 2026 05:56:48 +0000 Subject: [PATCH 4/4] test(layout): add geometry coverage for .sf-divider width behavior MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CodeRabbit's review of the .sf-divider--vertical fix asked for a regression test, since the existing suite only checked border sides, not the inline-size bug that actually shipped. Adds two cases: horizontal .sf-divider spans its container width, and .sf-divider--vertical stays a narrow rule inside a flex row instead of stretching to the row's full width. Verified against the built dist/slashed.optimal.css bundle directly (pre-installed chromium executable, since this sandbox lacks the headless-shell binary the project's own Playwright config expects) — all four assertions in this describe block pass. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_0113XBTydmejAnYJt9CTyYRc --- tests/layout.spec.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/layout.spec.js b/tests/layout.spec.js index b8f67d83..a6d220f3 100644 --- a/tests/layout.spec.js +++ b/tests/layout.spec.js @@ -534,6 +534,24 @@ test.describe('layout: .sf-divider', () => { expect(cs.inline).toBeGreaterThan(0); expect(cs.block).toBe(0); }); + + test('spans its container width in normal (block) flow', async ({ page }) => { + await setup(page, `

`); + const w = await page.locator('#t').evaluate(el => el.getBoundingClientRect().width); + expect(w).toBeGreaterThan(300); + }); + + test('--vertical stays a narrow rule inside a flex row, not stretched to the row width', async ({ page }) => { + await setup(page, ` +
+ Left +
+ Right +
+ `); + const w = await page.locator('#t').evaluate(el => el.getBoundingClientRect().width); + expect(w).toBeLessThan(20); + }); }); // ── .sf-divide ──────────────────────────────────────────────────