#301: stop /settings scrolling horizontally at 320px #309

Merged
opus-5 merged 8 commits from issue-301-settings-reflow into main 2026-08-01 13:34:38 +02:00
Contributor

Closes #301.

The layout skeleton was already hardened for narrow viewports in #165 (min-width: 0 on the content column, stacking below 60rem, the nav strip scrolling inside itself). The overflow therefore had to come from content inside a section, and two candidates could not shrink:

  • The sessions table. A table does not shrink below its min-content width, width: 100% or not — four columns, one carrying the full user-agent string. It now scrolls inside its own container.
  • The .settings-checkbox rows. display: flex with no flex-wrap, and the accent swatches are fixed-size circles that cannot shrink. An unwrappable row sets a floor for the entire page width.

The scroll container is keyboard-operable

overflow-x: auto alone produces a region only a mouse or trackpad can pan. The wrapper therefore carries tabIndex={0}, role="region" and a name, so keyboard users can reach and scroll it. Fixing a reflow failure by creating a keyboard trap would be a poor trade.

overflow: hidden was not used: it removes the scrollbar by clipping the content, which hides data rather than making it reachable.

The guard names the culprit

axe does not cover SC 1.4.10 — it is not derivable from the DOM — so the a11y pack gained a separate reflow check. It asserts scrollWidth === clientWidth at 320×800 and, when it trips, reports each overflowing element with its class list, right edge and width.

That matters for the next time: without it a failure tells you only that the page overflows, leaving you to bisect in devtools. It went into a11y.spec.ts rather than a new file because the CI job invokes specs by name — a new reflow.spec.ts would not have run without a workflow change.

Honest limits

I did not reproduce the original ~23px in a browser; the fixes come from reading the layout, and this PR's CI run is the actual measurement. Both changes are correct independently of what the exact culprit was — a wide table belongs in a scroll container, and those rows should wrap — but if the guard still trips, its output will name what is left, and I will follow up in the same PR.

The a11y pack gains one more login (fixture-user). If the login rate limit trips before the later packs, a reset step in ci.yml is the fix.

Checked: pnpm typecheck and full pnpm lint green.

Closes #301. The layout skeleton was already hardened for narrow viewports in #165 (`min-width: 0` on the content column, stacking below 60rem, the nav strip scrolling inside itself). The overflow therefore had to come from content inside a section, and two candidates could not shrink: - **The sessions table.** A table does not shrink below its min-content width, `width: 100%` or not — four columns, one carrying the full user-agent string. It now scrolls inside its own container. - **The `.settings-checkbox` rows.** `display: flex` with no `flex-wrap`, and the accent swatches are fixed-size circles that cannot shrink. An unwrappable row sets a floor for the entire page width. ## The scroll container is keyboard-operable `overflow-x: auto` alone produces a region only a mouse or trackpad can pan. The wrapper therefore carries `tabIndex={0}`, `role="region"` and a name, so keyboard users can reach and scroll it. Fixing a reflow failure by creating a keyboard trap would be a poor trade. `overflow: hidden` was not used: it removes the scrollbar by clipping the content, which hides data rather than making it reachable. ## The guard names the culprit axe does not cover SC 1.4.10 — it is not derivable from the DOM — so the a11y pack gained a separate reflow check. It asserts `scrollWidth === clientWidth` at 320×800 and, when it trips, reports each overflowing element with its class list, right edge and width. That matters for the next time: without it a failure tells you only *that* the page overflows, leaving you to bisect in devtools. It went into `a11y.spec.ts` rather than a new file because the CI job invokes specs by name — a new `reflow.spec.ts` would not have run without a workflow change. ## Honest limits I did not reproduce the original ~23px in a browser; the fixes come from reading the layout, and **this PR's CI run is the actual measurement**. Both changes are correct independently of what the exact culprit was — a wide table belongs in a scroll container, and those rows should wrap — but if the guard still trips, its output will name what is left, and I will follow up in the same PR. The a11y pack gains one more login (`fixture-user`). If the login rate limit trips before the later packs, a reset step in `ci.yml` is the fix. **Checked**: `pnpm typecheck` and full `pnpm lint` green.
opus-5 added this to the M33 — Tweaks & Feinschliff milestone 2026-08-01 07:17:45 +02:00
opus-5 added 1 commit 2026-08-01 07:17:45 +02:00
#301: stop /settings scrolling horizontally at 320px
Some checks failed
CI / Lint, typecheck, test (pull_request) Successful in 6m22s
CI / Auth e2e pack (pull_request) Failing after 8m10s
CI / Import/export fidelity gate (pull_request) Has been skipped
CI / Build container images (pull_request) Successful in 1m11s
e48b9dd7df
WCAG 2.1 SC 1.4.10 asks for no two-dimensional scrolling down to 320px,
which is also what 400% zoom on a 1280px screen produces. The layout
skeleton was already hardened for this in #165; the overflow came from
content inside the sections.

- The sessions table cannot shrink below its min-content width — four
  columns, one of them the full user-agent string. It now scrolls inside
  its own container rather than pushing the page. The container is
  focusable with a role and a name, because a scroll area that only a
  mouse can reach trades one barrier for another.
- `.settings-checkbox` rows may wrap. The accent swatches have a fixed
  size and cannot shrink, so an unwrappable row set a floor for the whole
  page width.

Adds a reflow guard to the a11y pack. axe does not cover 1.4.10 — the
criterion is not derivable from the DOM — so this is a separate check,
and it names the overflowing elements when it trips instead of only
reporting that something overflows.
fable-5 added 1 commit 2026-08-01 07:37:07 +02:00
#301: the real culprit was the jump nav, not the wide content
Some checks failed
CI / Build container images (pull_request) Successful in 1m12s
CI / Auth e2e pack (pull_request) Failing after 8m10s
CI / Import/export fidelity gate (pull_request) Has been skipped
CI / Lint, typecheck, test (pull_request) Successful in 6m22s
3c1f211f44
The first attempt fixed plausible suspects. CI measured the actual page
and named something else: six `.settings-nav__link` buttons, 417px of
page-level overflow at 320px.

`.settings-nav` already had `overflow-x: auto`, but as a flex child it
also had the default `min-width: auto` — the min-content width of the
whole jump strip. That forced the column wider than the viewport, so its
own overflow rule never had anything to scroll. `min-width: 0` is exactly
the case CLAUDE.md warns about under Reflow.

The guard now ignores elements that sit inside a scroll container. Such
content is *meant* to be wider than the viewport — reporting it buried
the one finding that mattered under twelve lines of noise, and the cap
truncated the list before it could show anything else.

The table wrapper and the wrapping settings rows from the first commit
stay. Neither was the cause here, but a table cannot shrink below its
min-content width and those rows cannot wrap on their own, so both are
hardening that holds regardless of content.
fable-5 added 1 commit 2026-08-01 07:53:36 +02:00
#301: make the reflow guard report the ancestor chain
Some checks failed
CI / Lint, typecheck, test (pull_request) Successful in 6m22s
CI / Build container images (pull_request) Successful in 1m13s
CI / Auth e2e pack (pull_request) Failing after 8m14s
CI / Import/export fidelity gate (pull_request) Has been skipped
55932b0828
The previous run came back with an empty offender list and an unchanged
417px overflow: the filter treated everything under a scroll container as
innocent, including the container that was itself too wide. A scroller
only absolves its children when the scroller fits.

It now reports the chain from body down to the widest offender with each
box's width, so the first element wider than the viewport is visible
instead of inferred.
fable-5 added 1 commit 2026-08-01 08:10:33 +02:00
#301: dump raw box metrics from the reflow guard
Some checks failed
CI / Lint, typecheck, test (pull_request) Successful in 6m33s
CI / Auth e2e pack (pull_request) Failing after 8m16s
CI / Import/export fidelity gate (pull_request) Has been skipped
CI / Build container images (pull_request) Successful in 1m15s
9c87a14f51
Two rounds now reported no element past the viewport edge while the
document still claimed 417px of overflow — a combination that rules out
every hypothesis I had, including my own filter.

So stop inferring. The guard now prints the html/body metrics, every
element whose own content is wider than its box (with its overflow-x, so
the intentional scrollers are distinguishable), and every box reaching
past the edge with no filtering at all. Diagnostics ride in the assertion
message, not the compared value, so they show up even when they match.
fable-5 added 1 commit 2026-08-01 08:50:04 +02:00
#301: the overflow was an escaping visually-hidden heading
Some checks failed
CI / Build container images (pull_request) Successful in 1m13s
CI / Auth e2e pack (pull_request) Failing after 8m14s
CI / Import/export fidelity gate (pull_request) Has been skipped
CI / Lint, typecheck, test (pull_request) Successful in 6m21s
27038a1f27
Found by standing up the local stack instead of guessing through CI.
The DOM tree under `.app-body` shows it in one line:

  span.visually-hidden rect=[342,343] pos=absolute

Its right edge is 343, and `.app-body` reports scrollWidth 343 against a
320 client. The table's actions column carries a `.visually-hidden`
heading, which is `position: absolute`. `.table-scroll` was `position:
static`, so it was NOT that span's containing block — the span escaped
the scroller's clipping, kept its static position out at the table's
right edge, and pushed the page.

`position: relative` on the wrapper makes it the containing block, and
the span is clipped like the rest of the table.

This is one cause behind both numbers: 23px locally, matching the
original report, and 417px in CI, where different font metrics make the
table wider and carry the span further out. Chasing them as separate
problems is what cost three CI rounds.

Verified locally against a real stack: the reflow guard passes and the
whole a11y pack is green, 11 tests in both colour schemes.
fable-5 added 1 commit 2026-08-01 11:15:38 +02:00
#301: sort the reflow report so the culprit cannot be buried
Some checks failed
CI / Build container images (pull_request) Successful in 1m13s
CI / Auth e2e pack (pull_request) Failing after 8m18s
CI / Import/export fidelity gate (pull_request) Has been skipped
CI / Lint, typecheck, test (pull_request) Successful in 6m23s
0420f97c42
CI still reports 737 while the local stack is now clean, and the box list
was capped at 15 entries — all of them nav links clipped by their own
scroller. Whatever pushes the page in CI sits past that cap.

The list is now sorted by reach, marks each entry as either clipped by a
fitting scroller or actually pushing the page, and shows 40.
fable-5 added 1 commit 2026-08-01 11:34:15 +02:00
#301: the token tables need the same scroll wrapper
Some checks failed
CI / Lint, typecheck, test (pull_request) Successful in 6m26s
CI / Build container images (pull_request) Successful in 1m13s
CI / Auth e2e pack (pull_request) Failing after 8m24s
CI / Import/export fidelity gate (pull_request) Has been skipped
70685968fc
The sorted report finally named it: `table.api-tokens__table` at 833px
wide, with its `.visually-hidden` heading reaching right=737 — exactly
the document's scrollWidth. Same mechanism as the sessions table, a
second table I had not wrapped.

Locally the API-tokens table was empty and therefore narrow, which is why
this only ever appeared in CI. With a token present it reproduces:
without the wrapper 345px of page overflow, with it none.

The feed-token table gets the same treatment — it is built the same way
and would fail as soon as someone holds a feed token with a long name.

The "[in fitting scroller]" marker in the report is misleading for these:
`main.main` is a scroller, but it is `position: static`, so it never
clipped the absolutely positioned heading. Only a positioned ancestor
does — which is what `.table-scroll` now is.

Verified locally against a real stack, with a wide token table present:
reflow guard green, whole a11y pack green in both colour schemes.
fable-5 added 1 commit 2026-08-01 12:14:19 +02:00
#301: reset the login rate limit before the VS-NfD packs
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 6m45s
CI / Build container images (pull_request) Successful in 1m18s
CI / Auth e2e pack (pull_request) Successful in 8m37s
CI / Import/export fidelity gate (pull_request) Successful in 1m0s
ef5f570dbf
CI 665: the reflow guard itself passed; the run died two packs later on
`fixture login for fixture-admin failed: 429`.

The a11y pack costs one more login since this branch added the reflow
test, and that was enough to exhaust the budget before the VS-NfD packs.
Same trap the workflow already documents for the content and collab
packs — it just needed one more reset, in the place the extra login
pushed it over.
fable-5 force-pushed issue-301-settings-reflow from ef5f570dbf to 5164801676 2026-08-01 13:02:34 +02:00 Compare
opus-5 merged commit 5164801676 into main 2026-08-01 13:34:38 +02:00
Sign in to join this conversation.
No description provided.