fix(admin): size the row drawer to the scroll region, not to the table - #72
Merged
Conversation
📝 WalkthroughWalkthroughThe PR constrains account drawers to the scroll region, prevents nested crew content from widening the drawer, and adds 320px end-to-end checks for control reachability, horizontal scrolling, and pinned-cell overlap. ChangesAccounts drawer layout
Estimated code review effort: 3 (Moderate) | ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
✨ Simplify code
Comment |
The accounts drawer renders as its own row — `<tr class="drawer-row"><td
colSpan={8}>` — so `.drawer__controls` had a flex line box as wide as the
accounts table. It wraps, but a flex line box is as wide as its container, and
the container was eight columns of tabular data. At 320px that made the control
row 943.8px inside a 286px region: `save note` sat 332px of horizontal scroll
off to the right, in a note field with no tabular reason to be anywhere but on
screen.
Give the drawer an inline-size container on the scroller and size it to that
instead, pinned to the region's left edge so it holds at every offset.
- `- 2 * var(--s-3)` subtracts the drawer cell's own inline padding. This is not
cosmetic: a bare `100cqi` overflows the cell by 24px and invents horizontal
scroll on viewports that had none (1280px: table 1198 -> 1222, maxScrollLeft
0 -> 24). With it subtracted, desktop is byte-identical to before.
- The container goes on `.scroller`, not `.scroller-frame`, which is 2px wider
than the scrollport because `.scroller` carries the border.
- `.drawer__crew { min-width: 0 }` releases the grid `min-width: auto` floor.
The nested crew Scroller's 372.5px min-content was sizing the drawer's column,
which pushed `freeze` 4.3px outside the region.
Costs 141px of height at 320px only, where the groups now stack three deep, on
a page that scrolls vertically anyway. 768px and up are unchanged.
The new test measures what the pin does to each drawer control with a 2-D area
intersection, not an x-extent comparison: `freeze` sits 83% inside the pinned
column's x-band with 0% of its area covered, so an x-only measure would call it
occluded. `inRegion` is asserted beside every `covered` so a zero cannot pass
for want of anything to measure.
guarzo
force-pushed
the
fix/drawer-wraps-to-region
branch
from
August 4, 2026 13:19
3d59a54 to
e2bbe4f
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
The accounts drawer renders as its own row —
<tr class="drawer-row"><td colSpan={8}>(row-disclosure.tsx:63) — so.drawer__controlshad a flex line box as wide as the accounts table. It does setflex-wrap: wrap, but a flex line box is as wide as its container, and the container was eight columns of tabular data, so wrapping never fired.Measured at a 320px viewport:
.drawer__controlssave noteAn admin who opened a row to write a note had to scroll a table 332px sideways to reach a note field with no tabular reason to be there. Pre-existing, introduced by #59 when the drawer moved out of the name cell into its own row.
The fix
An inline-size container on the scroller, with the drawer sized to that and pinned to the region's left edge:
Three things worth reviewing, each measured rather than reasoned about:
- 2 * var(--s-3)is not cosmetic. It subtracts the drawer cell's own inline padding. A bare100cqioverflows the cell by 24px and invents horizontal scroll on viewports that had none — at 1280px the table went 1198 → 1222 andmaxScrollLeft0 → 24. With the padding subtracted, desktop is byte-identical to before..scroller, not.scroller-frame..scrollercarries the 1px border, so the frame is 2px wider than the scrollport; a frame-hosted container measured a 288px drawer inside a 286px region..drawer__crew { min-width: 0 }is load-bearing. Grid items floor atmin-width: auto, and that item holds the crew table's ownScroller, whose min-content is the crew table (372.5px). That floor sized the drawer's column and pushedfreeze4.3px outside the region. Caught during implementation — the prototype had missed it because its variant declaredcontainer-typeon every.scroller, including the nested crew one, which zeroed the contribution. Written up in the spec.What it costs
At 320px only, the groups stack three deep: controls height 76.7 → 217.6px, and the crew list sits 141px further from the row that opened it. That is the accepted trade — vertical distance on a page that scrolls vertically anyway, against 332px of horizontal scroll inside a nested scroll region. 768px and up are unchanged.
Testing
One new test at
e2e/admin.spec.ts:439, asserting properties rather than the numbers they currently produce.It measures the pin with a 2-D area intersection, not an x-extent comparison. This matters:
freezesits with 83% of its width inside the pinned column's x-band and 0% of its area covered, so an x-only measure would report it 83% occluded. A control scrolled off the region's left edge has the same x-relationship to the pin as one buried under it.Both vacuity modes are closed explicitly —
inRegionproves the control was on screen, and a separate assertion proves a pinned cell exists at all (with none found,coveredByPinsums to 0 and the whole loop would pass against a table that had lost its pin;inRegiondoes not catch that).Verification, all run on this branch:
npm run typecheck— cleannpm test—Test Files 52 passed (52),Tests 430 passed (430)npm run test:e2e—57 passednpm run lint— 0 errors (5 pre-existing<img>warnings in untouched files)npm run format:check—All matched files use Prettier code style!Coordination with #61 — resolved
#61 (
fix/pin-stays-with-drawer-open) merged while this was in flight, so this branch has been rebased ontoorigin/main(bb08765). What the verbatim-copy strategy actually bought, now that it has been tested rather than predicted:e2e/geometry.ts— no conflict, and no diff. This branch carried a byte-identical copy ofcoveredByPin, comment included, at the same position betweenpinGeometryandclearOfPin. Git collapsed it silently: the file no longer appears in this PR's diff at all, there is exactly one definition (geometry.ts:70), andadmin.spec.tsimports it once. Had the copy drifted by a comment word, this would have been a hand-resolved conflict inside a helper both PRs' tests depend on.e2e/admin.spec.ts— conflicted on the import block only (test(admin): measure what the pin costs an open drawer, and record it #61 addedBASE_URL). Kept it. Both sets of tests survive: test(admin): measure what the pin costs an open drawer, and record it #61's "an open drawer does not widen the shared first column" (:555) and this branch's 320px test (:619).src/app/globals.css— conflicted inside.drawer's leading comment.mainhad renamed the component reference while this branch rewrote the comment around it. Kept the expanded comment, tookmain's reference.The component was renamed upstream
row-disclosure.tsxis gone;disclosure.tsxreplaces it withas="details"(default, used by the sync page) andas="row"(accounts). That matters to.scroller:has(.drawer), which was written when only one component could produce a.drawer— so it was checked, not assumed: thedetailsbranch renders a bare<div id={id}>with no.drawerclass (disclosure.tsx:97-109), so the container rule still applies to exactly one scroller. If a future change gives that branch the class, the scoping needs revisiting.Re-verified after the rebase
Upstream also landed #63 (primitives), #65 (payouts) and #67 (admin denials), two of which touch
admin/accounts/page.tsx— so the spec's measured figures were re-probed rather than assumed to carry over. They reproduce exactly: region 286, table 967.8125,maxScrollLeft682, drawer 262, controls height 217.6.Where to look
src/app/globals.cssis the whole change; the two e2e files are the proof. The comment-to-declaration ratio in the CSS is high (58 comment lines to 6 declarations, against a 0.7:1 baseline for the file) — deliberate, since each declaration encodes a measured tradeoff that reads as arbitrary otherwise, but say the word if you'd rather they pointed at the spec instead of restating it.Noted, not fixed
.log--sticky-colis a descendant selector, so the crew table nested inside a drawer row picks up the sticky-first-column rule too — its Name column is sticky within its own scroller, with the hairline and opaque ground that come with it. Nothing chose this, and it already cost a round of debugging incoveredByPin. This work doesn't touch those selectors, so it stays unfixed.🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes
Tests