oficina / docs Public

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
Paul Campbell pushed by paul@keymail.dev f05c041a3ffa4c46d1f1686b51ad726051e6876e parent d53d726
3 files changed, +109 −10
  • fixtures/browser-drive.mjs +66 −0
  • internal/docs/views/list.html +27 −10
  • internal/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);