Skip to content

Cap the sheet's content column, and answer a mouse - #98

Merged
EnesYilmazcode merged 4 commits into
mainfrom
claude/open-issues-eymdb2-93
Sep 2, 2026
Merged

Cap the sheet's content column, and answer a mouse#98
EnesYilmazcode merged 4 commits into
mainfrom
claude/open-issues-eymdb2-93

Conversation

@EnesYilmazcode

Copy link
Copy Markdown
Owner

Closes #93

Width caps and pointer affordances only. No breakpoint-driven relayout, no second design: the one-question flow, the single list, the peeked sheet and the zero-input open are untouched, and grep -c "@media (min-width" index.html still returns 0.

How this was measured

Every number below is from a real browser. scripts/shoot.mjs cannot run in this container (Chromium refuses to start as root and the shooter does not pass --no-sandbox), so the same CDP plumbing was reused in a throwaway rig — pinned clock, pinned position at the Thompson Library steps, real taps and real mouse drags — driving the app at seven viewports, once against origin/main and once against this branch. The rig is not committed. Nothing here is computed from the stylesheet.

What changed

One rule, three controls. .row, #chips and #origin are all flex: 1 1 auto inside a box that is left: 0; right: 0 with no cap, so one #sheet > * cap fixes all three. Measured at 1900x1000 on the ranked list, before → after:

before after
.row name column 1777.2px 389.2px
"Psychology Building 115" / "3 min" x=20.6 / x=1813.8 x=714.6 / x=1119.8
the four chips 459.3 / 445.5 / 445.5 / 508.2 112.3 / 98.5 / 98.5 / 161.2
#origin-where 1823.4px 435.4px
its ✕ x=1842.6 x=1148.6
#back x=9.6 x=694
#ask h1 54.4px 80px

The cap is a single --col: 32rem. No per-control rules were needed — #chips and #origin are siblings of the pane, not children of it, and the cap reaches them because it is on #sheet > *.

#ask h1 grows again. 13vw still governs the phone band; a max(…, min(6vw, 5rem)) term takes over, and 6vw only passes the old 3.4rem ceiling at 906.7px — wider than any phone in portrait and wider than a 430x932 phone turned sideways. Ceiling 5rem, reached at 1333px, chosen because "Vacant." measures 3.40× its font size, so 80px is 272px of wordmark inside the 24rem .opts column beneath it.

:hover for .opt, .opt.primary, .row, .row.on, .bar-btn (which is what #origin-where is), .chip, .chip[aria-checked], .dstep and #back. :active untouched. Guarded by (hover: hover) and (pointer: fine) and (forced-colors: none) so a phone does not get sticky hover and a high-contrast reader does not get an author colour on top of ButtonFace. Three new tokens, contrast checked with the same ratio the existing tokens were picked with: --fg on --card-hi 14.09:1, --dim on the row-hover surface 7.42:1 (over the 7:1 this screen is held to), near-black on --accent-hi 6.71:1.

A plain click on the grip toggles peek/full — see the decision below.

sw.js's shell gzip figure is recomputed because index.html grew: 84,201 → 86,442, using the same routine scripts/test/sw.test.mjs uses to check it. SHELL_CACHE is deliberately left alone for the stamper that runs in the job that commits.

Nothing moved at phone widths

The full before/after geometry dump is byte-identical at 320x568, 390x844, 430x932, and 393x852 with the root forced to 53px (the AX5 large-text case, where the @container list (max-width: 18em) reflow fires — it still fires, unchanged). Every box: sheet, list, row, name, walk, all four chips, origin bar, origin ✕, back arrow, wordmark. The only difference at any phone width in the whole diff is the deliberate click-toggle.

The decisions the issue asked for

Sheet or content? Content. The sheet keeps the window's width; only the column inside it narrows. The sheet's top border is a horizon line across the map, and body.has-bar stands it on an install rail that is the full width of the window — a narrowed sheet on a full-width rail reads as misaligned, and a card floating over the map is the second design docs/BLUEPRINT.md:408 rules out. #back is anchored to the column's left edge for the same reason: it moves toward what it acts on without becoming part of a panel.

pixelsPerGridForcap wins, so item 8 goes away. Measured against the committed data/campus.json (grid 4096, aspect() 1.20871, SETTLED_SPAN 0.28), MAX_RASTER_PX gives cap = 0.543946:

viewport ideal winner raster
390x844 dpr 2 0.680106 cap 2693x2229
1440x900 dpr 2 1.569475 cap 2693x2229
1920x1080 dpr 1 0.941685 cap 2693x2229
3840x2160 dpr 2 3.766741 cap 2693x2229

The raster is already at the cap at every one of these, including the smallest phone, so pixelsPerGridFor returns the same constant at every size and rebuilding state.basemap on resize would produce a byte-identical canvas. No change made. One honest edge: ideal only wins when shorter × dpr < 623.8, i.e. a dpr-1 window under 624px on its short axis. A 1200x600 window on a 1× monitor is in that band and would still get a raster sized for boot. That is narrow enough not to justify touching the map this PR does not own, and it is written down here rather than fixed silently.

#origin-where span's ellipsis. Live again, checked with the longest name in the index, "The Well - Hilliard Recreation and Wellness Center". At 1900px on main the span wanted 418px and had 418px, so it never truncated; it has 383px now and does. At 390px it wanted 418 and had 261 before and after — unchanged, still truncating where it did.

Mouse drag on #handle: confirmed working. Dispatched as real Input.dispatchMouseEvent press/move/release, the sheet went 380 → 780px at 1900x1000 and 321 → 658px at 390x844, on main and on this branch. The handlers are pointer events, so a mouse was never excluded — it just had to know to drag. A plain click now toggles, because that was the only way out of PEEK. Measured on this branch: rest 380 → click 780 → click 380 at 1900x1000, and 321 → 658 → 321 at 390x844; on main a click was a no-op at every size. The rule is "open unless already at full", so a click on the room screen (which rests at ROOM_SHEET, 0.72) opens rather than collapsing.

Not done, deliberately

  • scripts/shoot.mjs and docs/media/*.webp stay phone-only. The README frames are of the app on the device it is for. Adding a desktop frame means new committed binaries and a change to the shooter, which is not a CSS PR, and scripts/test/readme.test.mjs holds the frames and their alt text together. Worth its own issue if a desktop frame is wanted.
  • PEEK = 0.38 against window.innerHeight is untouched. The issue names the 1900x~360px letterbox but does not ask for it, and changing the rest heights changes bandFor, which composes the map — the render loop this PR is not allowed near.
  • #back moved horizontally, not vertically. It is now at the column's left edge but still top: .6rem, so at desktop heights it is still well above the sheet. Moving it down is a layout decision, not a cap.
  • The grip is still not keyboard-focusable. The click toggle is a pointer affordance; it does not give a keyboard user a way out of PEEK. That gap predates this PR and is not made worse by it.

Tests

npm test: 669 tests, 665 pass, 0 fail, 4 skipped. Baseline on main is 666/662/0/4 — the three new tests are the layout contracts for the cap, the hover pass and the click toggle, added to scripts/test/screens.test.mjs beside the two large-text ones. Every existing test is green, including the container-query source-order contract and install.test.mjs's rail-and-sheet rule.

Note on #85

Another lane is removing #chips from the list screen entirely. If that lands, the chip half of the cap becomes moot — but the cap is a single #sheet > * rule with no chip-specific selector in it, so #chips simply stops existing and nothing here needs unpicking. This was built against current main; both branches touch index.html and sw.js's gzip figure, so expect a merge conflict on those and resolve by keeping both edits and recomputing the figure.


🤖 Generated with Claude Code

https://claude.ai/code/session_017xiX95r6nnUUmhD3KwMnWM


Generated by Claude Code

Three broken controls, one cause: .row, #chips and #origin are all
`flex: 1 1 auto` inside a box that is left:0 right:0 with no cap. One
`#sheet > *` rule fixes all three, measured in headless Chrome at
1900x1000: the row's name column 1777.2 -> 389.2px, the four duration
chips 459.3/445.5/445.5/508.2 -> 112.3/98.5/98.5/161.2, #origin-where
1823.4 -> 435.4px with its ✕ from x=1842.6 to x=1148.6. That last one
also puts the ellipsis on #origin-where span back in reach: the longest
name in the index wants 418px in a span that had 418 and now has 383.

The SHEET keeps the window's width. Only the column inside it narrows:
the sheet's top border is a horizon line across the map and it stands on
a full-width install rail, so a floating card would be the second design
docs/BLUEPRINT.md rules out.

The cap is 32rem, which is 512px at the default root. Measured before and
after at 320, 390 and 430px, and at 393px with the root at 53px: every
box came back to the pixel.

#back rides the same column rather than the window corner. `grep -c
":hover"` returned 0, so every control the issue names has one now, under
hover/pointer/forced-colors so a phone and a high-contrast reader keep
what they had. #ask h1 grows above 906.7px, a width no phone reaches in
either orientation, to a 5rem ceiling at 1333px.

A plain click on the grip toggles peek and full. Dragging is a thumb
gesture and it was the only way out of peek.

sw.js's shell figure is recomputed because index.html grew: 84,201 ->
86,442 gzipped. SHELL_CACHE is left to the stamper that runs in the job
that commits.

Closes #93

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017xiX95r6nnUUmhD3KwMnWM
Review drove this branch in Chromium and found the click toggle firing on
gestures that were never clicks. `moved` read `drag.lastY - drag.y0`, and
lastY is the LAST pointermove sample -- it has to be, the velocity term
above it needs it -- so the expression measures net displacement. Any
gesture that comes back to where it started reads as a press.

Measured at 390x844 with real pointer events, both no-ops before this
screen learned to toggle:

  grip, 60px down then 60px back, release   321 -> 658   (snaps open)
  row,  40px down then 40px back, release   780 -> 321   (collapses)

There is now a `travelled` flag latched the first time the pointer is 8px
from its origin and never cleared. The comment claiming a pane gesture could
not reach that line without having moved was wrong in the same way: a pane
gesture clears 8px once to become a sheet drag, and nothing holds it
cleared.

pointercancel was bound to the same handler as pointerup and took the toggle
branch with it. A cancel is the platform taking the gesture away, not the
user letting go of one, so the branch now requires a release.

Three smaller things the same review found:

The landscape claim was false. 6vw was said to pass 3.4rem "wider than a
430x932 turned sideways"; 932 is wider than 906.7, so at 932x430 the
wordmark grows to 55.92px and the column cap binds, taking the sheet's
children from 932px to 512. That is the better answer in a 163px-tall
peeked landscape sheet, so the comment now says the rule bends there and
why, instead of claiming it does not.

.opt:hover had no :enabled guard, so it lit the four duration buttons while
they were still disabled and #ask:not(.ready) was dimming them to .45 --
the interface promising a control it has not got.

.b-row and .pick-row had no hover at all, though index.html:84 lists them
beside .row and .opt, and they are the whole of the nearest-buildings and
picker screens. A test named "every control answers a mouse" checked six of
eight; it checks eight.

670 tests, 666 pass, 0 fail. The new toggle test and the updated sheet test
both fail against this branch's previous app.js. Shell gzip re-measured
through the documented pipe: 87,332 on gzip 1.12, which is the version here
rather than the 1.14 the line named.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017xiX95r6nnUUmhD3KwMnWM
Found by merging all six lanes into one tree and running the suite there:
this branch and #85 are green apart and red together. This block was written
against a main that still had the duration chips; #85 removes them in the
same batch, so `.chip:hover` and `.chip[aria-checked="true"]:hover` become
style for a node nothing renders -- which is what #85's own guard exists to
forbid, and it fires:

  not ok - the duration chips are gone from the page, not merely hidden on it
    a chip selector survives in the stylesheet

Neither branch's tests could see it alone. Removing the rules here rather
than loosening that guard keeps main green through the merge in either
order.

670 tests, 666 pass, 0 fail. Shell gzip re-measured through the documented
pipe.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017xiX95r6nnUUmhD3KwMnWM
…mdb2-93

# Conflicts:
#	scripts/test/screens.test.mjs
#	sw.js
@EnesYilmazcode
EnesYilmazcode merged commit 8323b68 into main Sep 2, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

There is no width breakpoint anywhere, so on a laptop a room row puts ~1,400px between the name and its walk time

2 participants