The toolbar and the search form fit in 320px
Two WCAG 2.2 SC 1.4.10 failures on main, both mine, both shipped past a review and a browser drive because nothing was measuring the one width AGENTS.md commits to without exemption. The criterion is binary: six pixels of sideways scroll fails it exactly as a hundred would. The document page overflowed by 6px. The toolbar wraps, so a group that does not fit moves to its own line — but a group WIDER than a line then runs off the viewport, and three selects side by side are exactly that at 320px. The group now wraps inside itself, and a select may shrink below its own content width (a flex item's automatic minimum is its content, and a select's content is its widest option, so "Extra large" set a floor nothing else could get under). Reported by the live-sync session with the element named and the numbers measured, which is why this took minutes rather than an evening. The list page overflowed by 36px, which they had not looked at and I had not either. That one is worse than a miss: the search form carried flex-wrap: nowrap to stop its button dropping onto its own line in the desktop header, under a comment asserting that "a 320px viewport puts each form on its own line intact rather than breaking one of them in half". That was never measured and was wrong — the group wrapped, but the form itself was 340px against 288px of available width. A desktop nicety traded for an accessibility failure, by a comment that sounded like it had been checked. The field now takes what is left after the button instead of claiming a fixed width, and the button keeps its whole label, since that label is also the field's accessible name. The drive gained the check that was missing. It sets a real 320px viewport, walks every element on the list and on a document, and names the innermost one whose box crosses the edge — because "something overflows" sends the next person hunting through a stylesheet and "select[docs-tool-select] ends at 326" does not. Confirmed red against the unfixed CSS first, on both pages, naming exactly the elements above; 24/24 green with the fix.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YYB9fUAfxXNzkeC3fsFYzR
3 files changed,
+109
−10
fixtures/browser-drive.mjs+66 −0internal/docs/views/list.html+27 −10internal/docs/views/show.html+16 −0
CI passed — run details
Intent
Attributed to this commit: 398 g CO₂e (43379755 tokens). This is an attribution, not a measurement — usage counted from this commit's own marker forward to the next one on reflow-320, never backward from it. A turn that produced more than one commit books all of its tokens to the last one, and a concurrent session on this branch can add to the same figure. Some of it was priced using the shared default rate, because no model-specific factor row exists yet for at least one model involved. The Carbon view has the repo total, which is never derived from this attribution.
diff --git a/fixtures/browser-drive.mjs b/fixtures/browser-drive.mjs| index d5d5750..1a4a4db 100644 |
| --- a/fixtures/browser-drive.mjs |
| +++ b/fixtures/browser-drive.mjs |
| @@ -354,9 +354,75 @@ async function run() { |
| await caretChecks() |
| + await reflowChecks() |
| + |
| check('the page reported no JavaScript errors', pageProblems.length === 0, pageProblems.join(' | ')) |
| } |
| +// 320px reflow — WCAG 2.2 SC 1.4.10, which AGENTS.md takes with no |
| +// exemptions. |
| +// |
| +// Last, and it changes the viewport, so nothing above it may depend on |
| +// the window size afterwards. The criterion is binary: six pixels of |
| +// horizontal scroll fails it exactly as a hundred would, and six is |
| +// what the formatting bar actually shipped — three selects in a group |
| +// that could not break inside itself, ending at x=326 in a 320px |
| +// viewport. It reached main, because nothing here was looking. |
| +// |
| +// The check names the offending element rather than reporting a |
| +// number, because "something overflows" sends the next person hunting |
| +// through a stylesheet and "select[docs-tool-select] ends at 326" |
| +// does not. |
| +async function reflowChecks() { |
| + await send('Emulation.setDeviceMetricsOverride', { |
| + width: 320, height: 720, deviceScaleFactor: 1, mobile: true, |
| + }) |
| + try { |
| + for (const [name, path] of [['the document list', '/'], ['a document', null]]) { |
| + if (path) { |
| + await send('Page.navigate', { url: base + path }) |
| + await sleep(1500) |
| + } else { |
| + const href = await evaluate( |
| + `(()=>{const a=document.querySelector('a[href^="/d/"]');return a?a.getAttribute('href'):null})()`, |
| + ) |
| + await send('Page.navigate', { url: base + href }) |
| + await sleep(2500) |
| + } |
| + const found = await evaluate(`(() => { |
| + const vw = document.documentElement.clientWidth |
| + const over = [] |
| + for (const el of document.querySelectorAll('*')) { |
| + const r = el.getBoundingClientRect() |
| + if (r.width === 0 && r.height === 0) continue |
| + if (r.right > vw + 0.5 || r.left < -0.5) { |
| + let depth = 0 |
| + for (let n = el; n.parentElement; n = n.parentElement) depth++ |
| + over.push({ |
| + d: depth, |
| + what: el.tagName.toLowerCase() + |
| + (el.getAttributeNames().filter(a => a.startsWith('docs-')).map(a => '[' + a + ']').join('') || ''), |
| + right: Math.round(r.right), |
| + }) |
| + } |
| + } |
| + over.sort((a, b) => b.d - a.d) |
| + return { scrollWidth: document.documentElement.scrollWidth, viewport: vw, worst: over.slice(0, 3) } |
| + })()`) |
| + check( |
| + `${name} does not scroll sideways at 320px`, |
| + found.scrollWidth <= found.viewport && found.worst.length === 0, |
| + `scrollWidth ${found.scrollWidth} vs viewport ${found.viewport}; ` + |
| + found.worst.map((o) => `${o.what} ends at ${o.right}`).join(', '), |
| + ) |
| + } |
| + } finally { |
| + // Always put the viewport back, even if a check threw — a later |
| + // reader running this by hand should not inherit a phone. |
| + await send('Emulation.clearDeviceMetricsOverride') |
| + } |
| +} |
| + |
| // The caret-placement checks. |
| // |
| // These cover fd1d21a (clicking the page margins places the caret, and |
diff --git a/internal/docs/views/list.html b/internal/docs/views/list.html| index e755010..30a6448 100644 |
| --- a/internal/docs/views/list.html |
| +++ b/internal/docs/views/list.html |
| @@ -148,23 +148,40 @@ |
| max-inline-size: 14rem; |
| } |
| /* [docs-search-form] is the same inline row as the create form, with |
| - one difference: it does NOT wrap. The header now carries five |
| - controls, and with wrapping allowed the search button dropped onto |
| - a line of its own under its own field while the create form stayed |
| - whole — which reads as two rows of unrelated chrome rather than as |
| - one field and its button. The whole group still wraps at |
| - [docs-header-actions] above, so a 320px viewport (§10.3's reflow |
| - width) puts each form on its own line intact rather than breaking |
| - one of them in half. */ |
| + one difference: it does NOT wrap. The header carries five controls, |
| + and with wrapping allowed the search button dropped onto a line of |
| + its own under its own field while the create form stayed whole — |
| + which reads as two rows of unrelated chrome rather than as one |
| + field and its button. |
| + Staying on one row is only safe because the FIELD shrinks. This |
| + rule previously carried a comment asserting that a 320px viewport |
| + "puts each form on its own line intact rather than breaking one of |
| + them in half" — which was never measured and was wrong: the group |
| + did wrap, but the form itself was 340px wide against 288px of |
| + available width, so the page scrolled sideways. That is a WCAG 2.2 |
| + SC 1.4.10 failure, which AGENTS.md allows no exemption from, traded |
| + for a desktop layout nicety by a comment that sounded like it had |
| + been checked. |
| + Now the field takes what is left after the button rather than |
| + claiming a fixed width: flex 1 1 with a small basis, and |
| + min-inline-size 0 so it can go below its own content width, which |
| + is a flex item's default floor. */ |
| [docs-search-form] { |
| display: flex; |
| flex-wrap: nowrap; |
| align-items: center; |
| gap: var(--rst-sp-2); |
| + max-inline-size: 100%; |
| } |
| [docs-search-form] [rst-input] { |
| - inline-size: 11rem; |
| - max-inline-size: 100%; |
| + flex: 1 1 6rem; |
| + min-inline-size: 0; |
| + max-inline-size: 11rem; |
| + } |
| + /* The button keeps its whole label — it is the field's accessible |
| + name too — so it is the field that gives way, not the words. */ |
| + [docs-search-form] button { |
| + flex: 0 0 auto; |
| } |
| /* The submit button is for a pointer and for a screen reader; a |
| keyboard user submits with Enter from the field, which a |
diff --git a/internal/docs/views/show.html b/internal/docs/views/show.html| index 2d2b75c..8fee9a1 100644 |
| --- a/internal/docs/views/show.html |
| +++ b/internal/docs/views/show.html |
| @@ -374,8 +374,17 @@ |
| background: var(--rst-surface); |
| border-block-end: 1px solid var(--rst-line); |
| } |
| + /* flex-wrap: wrap, and it is load-bearing rather than tidy. The |
| + toolbar itself wraps, so a group that does not fit moves to its |
| + own line — but a group WIDER than one line then overflows the |
| + viewport, and the three selects together are exactly that at |
| + 320px. The group has to be able to break inside itself too. |
| + Measured: without this, [docs-tool-select] ended at x=326 in a |
| + 320px viewport, which is a WCAG 2.2 SC 1.4.10 failure and |
| + AGENTS.md allows no exemptions. */ |
| [docs-toolbar-group] { |
| display: flex; |
| + flex-wrap: wrap; |
| align-items: center; |
| gap: 2px; |
| } |
| @@ -449,8 +458,15 @@ |
| font-family: "Docs Mono", Consolas, monospace; |
| font-size: 13px; |
| } |
| + /* min-inline-size: 0 lets a select actually shrink. A flex item's |
| + default minimum is its content size, and a <select>'s content is |
| + its widest option — so "Extra large" set a floor no amount of |
| + wrapping could get under. Belt and braces with the group's wrap |
| + above: the group breaks between selects, and each select can also |
| + narrow to fit what is left. */ |
| [docs-tool-select] { |
| min-block-size: 28px; |
| + min-inline-size: 0; |
| max-inline-size: 11rem; |
| padding: 0 var(--rst-sp-1); |
| border: 1px solid var(--rst-line); |