oficina / docs Public

The document list fits in 320px, and the drive is now looking

Discussion #16 reported two WCAG 2.2 SC 1.4.10 failures and shipped a
branch (reflow-320, f05c041) that never landed. Nine commits later that
branch is stale, so this re-measures rather than merges it.

Measured on origin/main at 7507a33 with scripts/browser-drive.sh, at a
real 320px viewport:

  - the document page is CLEAN — scrollWidth 320, zero offenders. The
    toolbar half of reflow-320 is moot; something in tighten-chrome
    fixed it independently, and merging that branch would have re-landed
    a fix for a bug that no longer exists.
  - the document list still scrolled sideways: scrollWidth 356 against a
    320 viewport, innermost offenders button at x=356, button at x=350,
    form[docs-search-form] at x=356.

So one of the two failures is real and live on main tonight, in the list
page header.

[docs-search-form] held its field at a fixed 11rem beside a button whose
label does not wrap, under a comment claiming the group wrapping at
[docs-header-actions] kept 320px safe. It never had: max-inline-size:
100% resolves against a form sized by its own content, so the form set a
min-content floor of about 334px and stood wider than the 288px column.
The field now takes what is left after the button (flex 1 1 6rem,
min-inline-size 0), and the button keeps its whole label because that
label is also the field's accessible name.

The second offender was not a second bug. [docs-create-form] is written
correctly and shrinks as it should; it overflowed because a wrapping
flex container is at least as wide as its widest item's min-content, so
the search form's floor widened the line the create form had to itself.
Both went green on the one fix, which is the evidence that they were one
bug. Recorded in the comment so the next reader does not "fix" the
create form too.

Also stale and now corrected: [docs-header-actions]'s comment still
named the sign-out form as one of its children, which moved into the
shell's account control.

The check that catches this did not exist, which is why the failure
reached main past a Fable whole-branch review and a 22-check drive.
fixtures/browser-drive.mjs now sets a real 320px viewport, walks every
element on both pages, and names the innermost one whose box crosses the
edge — "form[docs-search-form] ends at 356" rather than "something
overflows". Confirmed red against the unfixed CSS first (24/25) and
green after (25/25); a check never seen to fail cannot tell "no" from
"I cannot see it".

Verified: browser drive 25/25, and CGO_ENABLED=0 go test ./internal/...
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WxucgadpWLE9XNWvfHBsnb
Paul Campbell pushed by paul@keymail.dev a51de5e2b093f12123e60999c67ff24e5d76c738 parent 7507a33
2 files changed, +124 −10
  • fixtures/browser-drive.mjs +71 −0
  • internal/docs/views/list.html +53 −10
diff --git a/fixtures/browser-drive.mjs b/fixtures/browser-drive.mjs
index f9d1cf3..319205a 100644
--- a/fixtures/browser-drive.mjs
+++ b/fixtures/browser-drive.mjs
@@ -361,9 +361,80 @@ 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 shipped with — 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 "input[docs-search-input] ends at 356" does
+// not.
+//
+// Both pages, not just the document. The list page's own overflow —
+// 36px, from a search form that claimed a fixed field width inside a
+// row it had told not to wrap — was found only because this walks
+// every page rather than the one that was being worked on.
+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 5c997ae..40b60ed 100644
--- a/internal/docs/views/list.html
+++ b/internal/docs/views/list.html
@@ -112,8 +112,9 @@
/* [docs-header-actions]: the header's one flex child opposite the
titles — [rst-page-header]'s space-between rule places it there
with no layout rule of its own beyond this; everything it holds
- (the create form, the sign-out form) lays out as one inline
- group inside it. */
+ (the search form and the create form — sign-out moved into the
+ shell's account control, see the note at the top of this file)
+ lays out as one inline group inside it. */
[docs-header-actions] {
display: flex;
flex-wrap: wrap;
@@ -172,32 +173,74 @@
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
+ one difference: it does NOT wrap. The header carries several
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 field and its button.
+
+ Staying on one row is only safe because the FIELD shrinks, and
+ that is the part this rule got wrong for a while. It previously
+ carried a comment asserting that the group wrapping at
+ [docs-header-actions] meant "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 field held a fixed
+ 11rem, its button holds a label that does not wrap, and
+ max-inline-size: 100% resolves against a form whose own width is
+ its content — so the form set a min-content floor of about 334px
+ and simply stood wider than the 288px column. Measured on
+ origin/main at 7507a33 with scripts/browser-drive.sh: the list
+ page scrolled sideways at 320px, scrollWidth 356 against a 320
+ viewport, with this form's button the innermost offender at
+ x=356.
+
+ 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.
+
+ Note what the floor did to its NEIGHBOUR. [docs-create-form] was
+ measured over the edge too, at x=350, and it is correctly written
+ — it shrinks exactly as it should. It overflowed because a
+ wrapping flex container is at least as wide as its widest item's
+ min-content, so this form's 334px floor widened every line in the
+ group, including the one the create form had to itself. Fixing
+ the field below fixes both; there was never a second bug. Worth
+ knowing before someone "fixes" the create form as well.
+
+ The fix is that the field takes what is left after the button
+ rather than claiming a width: flex 1 1 with a small basis, and
+ min-inline-size 0 to lift a flex item's automatic minimum, which
+ is its content size and is what an <input> otherwise refuses to
+ go below. The 11rem cap keeps the desktop header exactly as it
+ was. */
[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 submit button is for a pointer and for a screen reader; a
keyboard user submits with Enter from the field, which a
single-input form does natively. It stays visible rather than
becoming an icon because "Search documents" is also the field's
accessible name and the two agreeing is worth more than the
- width. */
+ width.
+ flex: 0 0 auto for the same reason: the whole shortfall comes out
+ of the field, so the button keeps its whole label. It is the
+ words that are load-bearing here, not the box.
+ The same locale caveat [docs-create-form] carries applies here —
+ a catalog whose docs.list.search.label is wider than the 320px
+ column would overflow rather than wrap, and the fix then is a
+ wrap allowance keyed to the label, not a smaller field. */
[docs-search-form] button {
white-space: nowrap;
+ flex: 0 0 auto;
}
/* The way back out of a result page, under the results rather than
in the header: it is about the list being shown, not about the