From cfdd702e86514bbef1b5526a5275e5a806447966 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 13 Jul 2026 08:54:34 +0000 Subject: [PATCH 1/4] fix(layout): make .sf-bento and .sf-grid-cols-N actually respond to their container MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bento grid on mobile wasn't collapsing to a single column, and the root cause is bigger than mobile: .sf-bento and .sf-grid-cols-2/3/4/6 each declared `container: / inline-size` on themselves and then used `@container` to change their OWN grid-template-columns. A size container can never be the subject of its own @container query — per spec, conditions only ever match descendants of the nearest ancestor container — so this was a silent no-op at every viewport, not just mobile. Confirmed empirically: .sf-grid-cols-4 rendered as a single 319px-wide column even inside a 2000px-wide ancestor. Fix: drop the self-established container from both primitives so their (already-unnamed) @container queries fall through to whatever ancestor container exists (.sf-container/.sf-cq/.sf-fluid-cq) — the same pattern .sf-alternate and .sf-grid-cols-1-2/2-1/1-3/3-1 already use correctly for restyling their children. This is what the framework's own docs describe ("layout primitives respond to @container") but the self-querying bug prevented in practice. Once the breakpoint actually fired, a second bug surfaced: .sf-bento-wide/-featured request `grid-column: span 2` unconditionally, which forces an implicit 2nd column even inside the 1-column mobile grid. Reset both to span 1 inside the same breakpoint (placed after the modifier declarations so it wins the equal-specificity/source-order tiebreak against them). .sf-grid-cols-2/3/4/6 had the identical self-query bug — fixed alongside .sf-bento since it's the exact same root cause and fix, one file, ~60 lines apart. Verified in a real browser at mobile/tablet/desktop container widths: single column + no overflow on mobile, 2 columns at the tablet breakpoint, full column count above it, spanning children never overflow. Added regression tests for the breakpoints themselves — the existing .sf-bento tests only checked --sf-bento-cols overrides, which route through the (unaffected) base rule and never exercised the broken @container path at all. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_0113XBTydmejAnYJt9CTyYRc --- core/layout.css | 26 ++++++++++++--- tests/layout.spec.js | 75 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 5 deletions(-) diff --git a/core/layout.css b/core/layout.css index 8fff2eeb..e1a4177e 100644 --- a/core/layout.css +++ b/core/layout.css @@ -5,7 +5,16 @@ SL-005: every @container query in this file hardcodes its breakpoint instead of referencing a --sf-* token — var() is not allowed inside an @container condition per the CSS spec. This applies framework-wide to - every @container site below; not re-explained at each one. */ + every @container site below; not re-explained at each one. + SL-034: a query container cannot be the subject of its own @container + query — conditions only ever match descendants of the nearest ancestor + container, never the element that establishes it (self-querying is a + silent no-op, not an error). Primitives that need to resize THEMSELVES + (.sf-grid-cols-2/3/4/6, .sf-bento) must therefore rely on an ancestor + container (.sf-container / .sf-cq / .sf-fluid-cq) rather than declaring + `container` on themselves — unlike .sf-alternate/.sf-grid-cols-1-2 etc., + which query a self-established container to restyle their CHILDREN and + are fine. Not re-explained at each site below. */ @layer slashed.layout { @@ -509,9 +518,8 @@ .sf-grid-cols-2, .sf-grid-cols-3, .sf-grid-cols-4, .sf-grid-cols-6 { - display: grid; - gap: var(--sf-grid-gap); - container: sf-grid / inline-size; + display: grid; + gap: var(--sf-grid-gap); } @container (min-width: 30em) { @@ -579,7 +587,6 @@ grid-auto-rows: minmax(var(--sf-bento-row, var(--sf-bento-row-default)), auto); grid-auto-flow: dense; gap: var(--sf-bento-gap); - container: sf-bento / inline-size; } @container (max-width: 29.99em) { @@ -605,6 +612,15 @@ .sf-bento-tall { grid-row: span 2; } .sf-bento-featured { grid-column: span 2; grid-row: span 2; } + /* A spanning child can't request more columns than the 1-column mobile + grid has, or CSS Grid adds an implicit column to satisfy the span, + breaking the single-column layout the query above exists to guarantee. + Placed after the modifiers above (equal specificity) so it wins on + source order regardless of query-match state. */ + @container (max-width: 29.99em) { + .sf-bento-wide, .sf-bento-featured { grid-column: span 1; } + } + .sf-subgrid { display: grid; grid-template-columns: subgrid; } .sf-subgrid-rows { display: grid; grid-template-rows: subgrid; } diff --git a/tests/layout.spec.js b/tests/layout.spec.js index a6d220f3..0a5f90ce 100644 --- a/tests/layout.spec.js +++ b/tests/layout.spec.js @@ -644,6 +644,81 @@ test.describe('layout: .sf-bento', () => { ); expect(cols).toBe(2); }); + + // .sf-bento's own @container breakpoints must resolve against an ANCESTOR + // container, not itself — a size container cannot be the subject of its + // own @container query (SL-034); .sf-bento used to declare `container` + // on itself, which made these breakpoints a permanent silent no-op at + // every viewport, mobile included. + test('collapses to 1 column below the mobile breakpoint (ancestor container)', async ({ page }) => { + await setup(page, ` +
+
A
B
+
+ `); + const cols = await page.locator('#g').evaluate(el => + getComputedStyle(el).gridTemplateColumns.split(' ').length + ); + expect(cols).toBe(1); + }); + + test('a spanning child does not force a phantom 2nd column at the mobile breakpoint', async ({ page }) => { + await setup(page, ` +
+
+
wide
+
B
+
+
+ `); + const [gridWidth, itemWidth] = await Promise.all([ + page.locator('.sf-bento').evaluate(el => el.getBoundingClientRect().width), + page.locator('#w').evaluate(el => el.getBoundingClientRect().width), + ]); + expect(itemWidth).toBeLessThanOrEqual(gridWidth + 1); + }); + + test('uses the wider column count once past the mobile breakpoint', async ({ page }) => { + await setup(page, ` +
+
A
B
+
+ `); + const cols = await page.locator('#g').evaluate(el => + getComputedStyle(el).gridTemplateColumns.split(' ').length + ); + expect(cols).toBeGreaterThan(1); + }); +}); + +// ── .sf-grid-cols-2/3/4/6 ────────────────────────────────────────── +test.describe('layout: .sf-grid-cols-2/3/4/6', () => { + // Same SL-034 constraint as .sf-bento above: these used to establish and + // then query their own container, so the breakpoint below was dead code + // at every viewport — only an ancestor container makes it fire. + test('resolves its column count against an ancestor container, not itself', async ({ page }) => { + await setup(page, ` +
+
1
2
3
4
+
+ `); + const cols = await page.locator('#g').evaluate(el => + getComputedStyle(el).gridTemplateColumns.split(' ').length + ); + expect(cols).toBe(4); + }); + + test('stays 1 column below the breakpoint', async ({ page }) => { + await setup(page, ` +
+
1
2
3
4
+
+ `); + const cols = await page.locator('#g').evaluate(el => + getComputedStyle(el).gridTemplateColumns.split(' ').length + ); + expect(cols).toBe(1); + }); }); // ── .sf-alternate ─────────────────────────────────────────────── From a3afdbe947185f032234746feb34a930a8bf0af9 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 13 Jul 2026 08:56:46 +0000 Subject: [PATCH 2/4] docs(changelog): note the .sf-bento / .sf-grid-cols-N container-query fix Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_0113XBTydmejAnYJt9CTyYRc --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a8e894d..62b0b9e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ## Unreleased +### Bug Fixes +- **layout:** `.sf-bento` and `.sf-grid-cols-2/3/4/6` now actually respond to their `@container` breakpoints — both used to declare a `container` on themselves and then query that same container to resize themselves, which is a no-op per spec (a container can't be the subject of its own `@container` query). Fixed by relying on an ancestor container (`.sf-container` / `.sf-cq` / `.sf-fluid-cq`) instead, matching how `.sf-alternate` and `.sf-grid-cols-1-2` etc. already do it. `.sf-bento-wide` / `.sf-bento-featured` also no longer force a phantom 2nd column once the grid collapses to 1 column at the mobile breakpoint. + ## [0.7.16] - 2026-07-13 ### Features From ac232f4272e1544f9be8255381b2d60c1f4131ea Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 13 Jul 2026 09:12:33 +0000 Subject: [PATCH 3/4] test(layout): cover .sf-bento-featured overflow and the grid-cols-4 mid breakpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Greptile review on PR #607 caught two real gaps in the new tests: - Only .sf-bento-wide was checked for the phantom-column overflow fix; .sf-bento-featured carries the same grid-column: span 2 (plus grid-row: span 2) and needs the identical assertion. - The "stays 1 column below the breakpoint" test for .sf-grid-cols-4 passed for the wrong reason: below 30em neither @container rule matches at all, so gridTemplateColumns falls back to the unset "none" — which also satisfies a naive length-1 check even with a completely broken ancestor container. Added a mid-range (30-48em) case that only passes if the container query genuinely fired. Verified both against the real built dist/slashed.optimal.css bundle before committing (same sandbox limitation as the prior commit — no chrome-headless-shell binary here for the project's own Playwright runner). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_0113XBTydmejAnYJt9CTyYRc --- tests/layout.spec.js | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/tests/layout.spec.js b/tests/layout.spec.js index 0a5f90ce..2c5bb6e4 100644 --- a/tests/layout.spec.js +++ b/tests/layout.spec.js @@ -678,6 +678,22 @@ test.describe('layout: .sf-bento', () => { expect(itemWidth).toBeLessThanOrEqual(gridWidth + 1); }); + test('--featured (span 2/2) also does not force a phantom 2nd column at the mobile breakpoint', async ({ page }) => { + await setup(page, ` +
+
+ +
B
+
+
+ `); + const [gridWidth, itemWidth] = await Promise.all([ + page.locator('.sf-bento').evaluate(el => el.getBoundingClientRect().width), + page.locator('#f').evaluate(el => el.getBoundingClientRect().width), + ]); + expect(itemWidth).toBeLessThanOrEqual(gridWidth + 1); + }); + test('uses the wider column count once past the mobile breakpoint', async ({ page }) => { await setup(page, `
@@ -719,6 +735,23 @@ test.describe('layout: .sf-grid-cols-2/3/4/6', () => { ); expect(cols).toBe(1); }); + + // Below 30em NEITHER @container rule matches, so gridTemplateColumns falls + // back to the unset "none" (still 1 "column" by construction) — the case + // above alone can't distinguish a working ancestor container from a + // completely absent one. This mid-range case only passes if the 30em + // breakpoint's ancestor-container query actually fired. + test('uses the 2-column mid breakpoint between 30em and 48em', async ({ page }) => { + await setup(page, ` +
+
1
2
3
4
+
+ `); + const cols = await page.locator('#g').evaluate(el => + getComputedStyle(el).gridTemplateColumns.split(' ').length + ); + expect(cols).toBe(2); + }); }); // ── .sf-alternate ─────────────────────────────────────────────── From 55b8e2cdaf81bb1073934a5ff2a9744b1cb376bf Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 13 Jul 2026 09:37:33 +0000 Subject: [PATCH 4/4] test(layout): fix cross-browser flaky container-query breakpoint widths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI failed on Firefox and WebKit: the "resolves its column count against an ancestor container" test for .sf-grid-cols-4 expected 4 columns at a 900px ancestor width but got 2. Root cause: `em` inside an @container condition resolves against the container's inherited font-size, and that's the fluid `--sf-text-m` here (~19px at this viewport), not a fixed 16px, because `body { font-size: var(--sf-text-m) }`. 900px sits close enough to the resulting ~48em boundary that Chromium and Firefox/WebKit disagreed on which side of it they landed. Not a product bug — just three engines evaluating a clamp()/pow() formula with slightly different floating-point precision right at a boundary. Fixed by widening the margins so the outcome is unambiguous regardless of engine: the "past the breakpoint" cases move from 900px to 1600px, the "mid-range" cases move from 600px to 750px (both grid-cols-4 and the new .sf-bento mid-range case CodeRabbit suggested). Verified all five breakpoint assertions against the real built bundle in Chromium before pushing (same sandbox limitation as prior commits — no Firefox/WebKit binaries here to reproduce the actual failure locally). Also tightened the SL-034 header comment per a CodeRabbit finding: it incorrectly grouped .sf-grid-cols-1-2 with .sf-alternate as "query a self-established container to restyle children" — .sf-grid-cols-1-2 never declared its own `container` at all (same ancestor-container pattern as the fixed primitives, just already correct), unlike .sf-alternate which genuinely does self-establish one for its children. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_0113XBTydmejAnYJt9CTyYRc --- core/layout.css | 9 ++++++--- tests/layout.spec.js | 32 +++++++++++++++++++++++++++++--- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/core/layout.css b/core/layout.css index e1a4177e..a60bc1ab 100644 --- a/core/layout.css +++ b/core/layout.css @@ -12,9 +12,12 @@ silent no-op, not an error). Primitives that need to resize THEMSELVES (.sf-grid-cols-2/3/4/6, .sf-bento) must therefore rely on an ancestor container (.sf-container / .sf-cq / .sf-fluid-cq) rather than declaring - `container` on themselves — unlike .sf-alternate/.sf-grid-cols-1-2 etc., - which query a self-established container to restyle their CHILDREN and - are fine. Not re-explained at each site below. */ + `container` on themselves. .sf-grid-cols-1-2 etc. already follow this — + they've never declared their own `container` — so they were unaffected. + .sf-alternate is a different, unrelated-but-similar-looking pattern: it + DOES declare `container` on itself, but only to restyle its CHILDREN + (`.sf-alternate > *`), which is not self-querying and is fine. Not + re-explained at each site below. */ @layer slashed.layout { diff --git a/tests/layout.spec.js b/tests/layout.spec.js index 2c5bb6e4..6a6460d2 100644 --- a/tests/layout.spec.js +++ b/tests/layout.spec.js @@ -696,7 +696,7 @@ test.describe('layout: .sf-bento', () => { test('uses the wider column count once past the mobile breakpoint', async ({ page }) => { await setup(page, ` -
+
A
B
`); @@ -705,6 +705,24 @@ test.describe('layout: .sf-bento', () => { ); expect(cols).toBeGreaterThan(1); }); + + // `em` inside an @container condition resolves against the container's + // (fluid, viewport-driven) inherited font-size here — not a fixed 16px — + // so the effective pixel breakpoint isn't identical across engines. 750px + // sits comfortably between every observed 30em/48em interpretation; 900px + // was measured to land right on that boundary and flipped between 2 and 4 + // columns depending on the browser (see the grid-cols-4 test below). + test('uses 2 columns in the mid-range between 30em and 48em', async ({ page }) => { + await setup(page, ` +
+
A
B
+
+ `); + const cols = await page.locator('#g').evaluate(el => + getComputedStyle(el).gridTemplateColumns.split(' ').length + ); + expect(cols).toBe(2); + }); }); // ── .sf-grid-cols-2/3/4/6 ────────────────────────────────────────── @@ -712,9 +730,17 @@ test.describe('layout: .sf-grid-cols-2/3/4/6', () => { // Same SL-034 constraint as .sf-bento above: these used to establish and // then query their own container, so the breakpoint below was dead code // at every viewport — only an ancestor container makes it fire. + // A width of 900px (56.25em at a fixed 16px basis) originally lived here + // and was flaky in CI: `em` inside an @container condition resolves + // against the container's inherited font-size, which here is the fluid + // `--sf-text-m` (~19px at this viewport, not 16px), and engines don't + // agree on the resulting effective breakpoint down to the pixel — 900px + // sat close enough to the 48em boundary that Chromium read it as past the + // breakpoint while Firefox/WebKit read it as short of it. 1600px clears + // every observed interpretation. test('resolves its column count against an ancestor container, not itself', async ({ page }) => { await setup(page, ` -
+
1
2
3
4
`); @@ -743,7 +769,7 @@ test.describe('layout: .sf-grid-cols-2/3/4/6', () => { // breakpoint's ancestor-container query actually fired. test('uses the 2-column mid breakpoint between 30em and 48em', async ({ page }) => { await setup(page, ` -
+
1
2
3
4
`);