The bottom sheet's body has scrolled since #60 and said nothing about
it. Measured at 424x439 with one row selected, the track list's menu ran
to eight items ending at y=470 against a fold at 439. It was never unreachable — the body is overflow-y: auto and 39px of scroll brings
the last item into view — which is why it was Priority/Medium. What was
missing is the affordance, and the worst case is the one that reproduces
on main: where the cut lands on a row boundary the sheet ends in a
clean edge that reads as the end of the list.
What changed and why
One rule, wa-dialog::part(body) in components/menu-surface/.
The 85vh cap stays — the component's own comment says a surface covering
the whole screen is a page, not a sheet — so the affordance is what
changes and not the height.
The fade is two background layers, and the pair of attachments is the
feature: a shadow pinned to the box (scroll) under a cover of the
sheet's own colour painted at the end of the content (local), which
scrolls up over the shadow exactly when there is nothing more to see. So
it is absent on a menu that fits, present the moment one does not, and
gone again at the end of the list — with no scroll listener, no
measurement and nothing reaching into wa-dialog's shadow root for the
scroller. background-attachment is Chrome 4; the reference device is
Chrome 113.
The report's other two options — a shortened last row, or a max height
that makes the cut obvious — both need height mod 48, which CSS cannot
express; and the observed case is already a cut on a row boundary.
The curve is steep rather than linear because the rows under it stay
live. A scrim over a menu item is that item's text surface, so this
app's 4.5:1 rule reaches it, and the light ramp (bgElevated#e9ecef)
is what makes that real rather than theoretical. A first draft of 48px
linear at 0.8 put ~0.375 on the last label — 5.0:1, passing but visibly
greyed. The shipped 32px, already down to a quarter strength at 14px,
measures 9.9:1 there and spends its weight on the strip below it.
Verification
Reproduced on main first. At 424x360 (see below for why not 439): scrollHeight 364 against clientHeight 306, last item's bottom at
y=410 in a 360px viewport, and background-image: none — no affordance
at all.
Pixel measurements, x=300, dark ramp, against the running app:
y
before
more below
scrolled to the end
330
52,58,64
50,56,62
52,58,64
340
52,58,64
43,48,53
52,58,64
350
52,58,64
33,37,40
52,58,64
359
52,58,64
22,24,27
52,58,64
At 424x439, where main's seven-item menu fits exactly
(scrollHeight 364 == clientHeight 364), the sheet is flat 52,58,64
to its bottom edge — nothing is drawn. Light ramp checked by overriding
the two custom properties on :root.
Tiers run:
make ui-test — 1093 passed, including one new case in test/components/menu-surface.test.ts. Proved non-vacuous: with
the rule reverted to its pre-fix form it fails on expected 'scroll' to be 'local, scroll'.
make e2e — 239 passed against make dev-headless SEED=default
(chromium; WebKit is CI-only). The sheet is shared by fourteen menus,
so this tier is the regression check rather than the assertion.
npx tsc --noEmit, make css-check — clean.
Screenshots at 424x360 on both ramps, cropped and scaled, since a dark
scrim on a dark surface is invisible in a shrunk preview while being
measurably present.
No Go, no .sql and no .templ changed, so make lint, make test, make generate and make bindings were not run.
Deliberately not done
No e2e spec. The conditionality is a property of background-attachment and the component tier asserts exactly that;
the rendered result needs pixels, and the only viewport on main
where a menu overflows is 424x360 — contrived enough that the spec
would be about the fixture rather than the app. Once #67 merges, the
track list's menu overflows at the real 424x439 and a spec becomes
worth writing.
The cap was not raised and no item was removed from any menu; the
report rules the second out and menu-surface rules the first out.
No top-edge affordance. The report is about the last item; a
symmetric shadow is two more layers for a question nobody asked.
CLAUDE.md's menu-surface section grows a seventh load-bearing point,
and the measurements are in .planning/NOTES.md.
## What the issue was
The bottom sheet's body has scrolled since #60 and said nothing about
it. Measured at 424x439 with one row selected, the track list's menu ran
to eight items ending at y=470 against a fold at 439. It was never
*unreachable* — the body is `overflow-y: auto` and 39px of scroll brings
the last item into view — which is why it was `Priority/Medium`. What was
missing is the affordance, and the worst case is the one that reproduces
on `main`: where the cut lands on a row boundary the sheet ends in a
clean edge that reads as the end of the list.
## What changed and why
One rule, `wa-dialog::part(body)` in `components/menu-surface/`.
The 85vh cap stays — the component's own comment says a surface covering
the whole screen is a page, not a sheet — so the affordance is what
changes and not the height.
The fade is two background layers, and the *pair of attachments* is the
feature: a shadow pinned to the box (`scroll`) under a cover of the
sheet's own colour painted at the end of the *content* (`local`), which
scrolls up over the shadow exactly when there is nothing more to see. So
it is **absent on a menu that fits, present the moment one does not, and
gone again at the end of the list** — with no scroll listener, no
measurement and nothing reaching into `wa-dialog`'s shadow root for the
scroller. `background-attachment` is Chrome 4; the reference device is
Chrome 113.
The report's other two options — a shortened last row, or a max height
that makes the cut obvious — both need `height mod 48`, which CSS cannot
express; and the observed case is already a cut on a row boundary.
**The curve is steep rather than linear because the rows under it stay
live.** A scrim over a menu item is that item's text surface, so this
app's 4.5:1 rule reaches it, and the light ramp (`bgElevated` `#e9ecef`)
is what makes that real rather than theoretical. A first draft of 48px
linear at 0.8 put ~0.375 on the last label — 5.0:1, passing but visibly
greyed. The shipped 32px, already down to a quarter strength at 14px,
measures **9.9:1** there and spends its weight on the strip below it.
## Verification
**Reproduced on `main` first.** At 424x360 (see below for why not 439):
`scrollHeight` 364 against `clientHeight` 306, last item's bottom at
y=410 in a 360px viewport, and `background-image: none` — no affordance
at all.
**Pixel measurements**, x=300, dark ramp, against the running app:
| y | before | more below | scrolled to the end |
|---|---|---|---|
| 330 | 52,58,64 | 50,56,62 | 52,58,64 |
| 340 | 52,58,64 | 43,48,53 | 52,58,64 |
| 350 | 52,58,64 | 33,37,40 | 52,58,64 |
| 359 | 52,58,64 | 22,24,27 | 52,58,64 |
At 424x439, where `main`'s seven-item menu fits exactly
(`scrollHeight` 364 == `clientHeight` 364), the sheet is flat 52,58,64
to its bottom edge — nothing is drawn. Light ramp checked by overriding
the two custom properties on `:root`.
**Tiers run:**
- `make ui-test` — **1093 passed**, including one new case in
`test/components/menu-surface.test.ts`. **Proved non-vacuous**: with
the rule reverted to its pre-fix form it fails on
`expected 'scroll' to be 'local, scroll'`.
- `make e2e` — **239 passed** against `make dev-headless SEED=default`
(chromium; WebKit is CI-only). The sheet is shared by fourteen menus,
so this tier is the regression check rather than the assertion.
- `npx tsc --noEmit`, `make css-check` — clean.
- Screenshots at 424x360 on both ramps, cropped and scaled, since a dark
scrim on a dark surface is invisible in a shrunk preview while being
measurably present.
No Go, no `.sql` and no `.templ` changed, so `make lint`, `make test`,
`make generate` and `make bindings` were **not** run.
## Deliberately not done
- **No e2e spec.** The conditionality is a property of
`background-attachment` and the component tier asserts exactly that;
the rendered result needs pixels, and the only viewport on `main`
where a menu overflows is 424x360 — contrived enough that the spec
would be about the fixture rather than the app. Once #67 merges, the
track list's menu overflows at the real 424x439 and a spec becomes
worth writing.
- **The cap was not raised** and no item was removed from any menu; the
report rules the second out and `menu-surface` rules the first out.
- **No top-edge affordance.** The report is about the last item; a
symmetric shadow is two more layers for a question nobody asked.
`CLAUDE.md`'s `menu-surface` section grows a seventh load-bearing point,
and the measurements are in `.planning/NOTES.md`.
Closes #207
The bottom sheet's body has scrolled since #60 and said nothing about
it. Measured at 424x439, the track list's menu ended at y=470 with the
fold at 439 — reachable, since the body is `overflow-y: auto`, but with
no affordance saying so, and worst where the cut lands on a row
boundary and the sheet ends in a clean edge that reads as the end of
the list.
The cap stays: `menu-surface`'s own comment says a surface covering the
whole screen is a page, not a sheet. What changes is that the body
draws a fade, from two background layers whose *attachments* are the
feature — a shadow pinned to the box (`scroll`) under a cover of the
sheet's own colour painted at the end of the content (`local`), which
scrolls up over the shadow exactly when there is nothing more to see.
So the fade is absent on a menu that fits, present the moment one does
not, and gone again at the end of the list, with no scroll listener and
nothing reaching into `wa-dialog`'s shadow root for the scroller.
`background-attachment` is Chrome 4; the reference device is Chrome 113.
The other two options in the report — a shortened last row, or a max
height that makes the cut obvious — both need `height mod 48`, which
CSS cannot express, and the observed case is exactly the one where the
cut already lands on a row boundary.
The curve is steep rather than linear because the rows under it stay
live: a scrim over a menu item is that item's text surface, so the
4.5:1 rule reaches it, and the light ramp is what makes that real.
A 48px linear scrim at 0.8 greyed the last label to 5.0:1; 32px already
down to a quarter strength at 14px measures 9.9:1 there and spends its
weight on the strip below it.
The test asserts the pair of attachments rather than the pixels, on
this file's existing grounds that no tier here renders like the device
— it fails on the pre-fix stylesheet with `expected 'scroll' to be
'local, scroll'`. The rendered result was measured in the harness and
is recorded in `.planning/NOTES.md`.
Closes#207
CI is green — run 17668, check and e2e both success (chromium
and WebKit).
The first run failed on #138, not on this change.check reported test/components/play-in-context.test.ts > "queues the list as
displayed and starts on that row" with
which is #138's reported assertion byte-for-byte — a known flake at
~1 in 3 full-suite runs on pristine main, Reviewed/Confirmed. This
branch touches components/menu-surface/ and its test only, and menu-surface.test.ts passed in that same run (✓ 9 tests). Re-ran the
workflow with no change to the branch; check passed and e2e followed.
Recorded here rather than silently re-run, since "a tier failed" and "a
tier failed for a reason unrelated to the diff" are different claims.
**CI is green** — run 17668, `check` and `e2e` both success (chromium
and WebKit).
**The first run failed on #138, not on this change.** `check` reported
`test/components/play-in-context.test.ts` > "queues the list as
displayed and starts on that row" with
```
AssertionError: expected [ 12, '/music/track-11.mp3' ]
to deeply equal [ 12, '/music/track-3.mp3' ]
```
which is #138's reported assertion byte-for-byte — a known flake at
~1 in 3 full-suite runs on pristine `main`, `Reviewed/Confirmed`. This
branch touches `components/menu-surface/` and its test only, and
`menu-surface.test.ts` passed in that same run (`✓ 9 tests`). Re-ran the
workflow with no change to the branch; `check` passed and `e2e` followed.
Recorded here rather than silently re-run, since "a tier failed" and "a
tier failed for a reason unrelated to the diff" are different claims.
The comment on `wa-dialog::part(body)` carried bottom-edge pixels from
an intermediate probe (29,33,36) while `.planning/NOTES.md` recorded
the final sample against the shipped rule (22,24,27) — the same
gradient, read a few pixels higher up the box. A measurement written in
two places has to agree, or neither can be trusted.
logan
merged commit aeb173c684 into main2026-08-25 16:39:44 +00:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
What the issue was
The bottom sheet's body has scrolled since #60 and said nothing about
it. Measured at 424x439 with one row selected, the track list's menu ran
to eight items ending at y=470 against a fold at 439. It was never
unreachable — the body is
overflow-y: autoand 39px of scroll bringsthe last item into view — which is why it was
Priority/Medium. What wasmissing is the affordance, and the worst case is the one that reproduces
on
main: where the cut lands on a row boundary the sheet ends in aclean edge that reads as the end of the list.
What changed and why
One rule,
wa-dialog::part(body)incomponents/menu-surface/.The 85vh cap stays — the component's own comment says a surface covering
the whole screen is a page, not a sheet — so the affordance is what
changes and not the height.
The fade is two background layers, and the pair of attachments is the
feature: a shadow pinned to the box (
scroll) under a cover of thesheet's own colour painted at the end of the content (
local), whichscrolls up over the shadow exactly when there is nothing more to see. So
it is absent on a menu that fits, present the moment one does not, and
gone again at the end of the list — with no scroll listener, no
measurement and nothing reaching into
wa-dialog's shadow root for thescroller.
background-attachmentis Chrome 4; the reference device isChrome 113.
The report's other two options — a shortened last row, or a max height
that makes the cut obvious — both need
height mod 48, which CSS cannotexpress; and the observed case is already a cut on a row boundary.
The curve is steep rather than linear because the rows under it stay
live. A scrim over a menu item is that item's text surface, so this
app's 4.5:1 rule reaches it, and the light ramp (
bgElevated#e9ecef)is what makes that real rather than theoretical. A first draft of 48px
linear at 0.8 put ~0.375 on the last label — 5.0:1, passing but visibly
greyed. The shipped 32px, already down to a quarter strength at 14px,
measures 9.9:1 there and spends its weight on the strip below it.
Verification
Reproduced on
mainfirst. At 424x360 (see below for why not 439):scrollHeight364 againstclientHeight306, last item's bottom aty=410 in a 360px viewport, and
background-image: none— no affordanceat all.
Pixel measurements, x=300, dark ramp, against the running app:
At 424x439, where
main's seven-item menu fits exactly(
scrollHeight364 ==clientHeight364), the sheet is flat 52,58,64to its bottom edge — nothing is drawn. Light ramp checked by overriding
the two custom properties on
:root.Tiers run:
make ui-test— 1093 passed, including one new case intest/components/menu-surface.test.ts. Proved non-vacuous: withthe rule reverted to its pre-fix form it fails on
expected 'scroll' to be 'local, scroll'.make e2e— 239 passed againstmake dev-headless SEED=default(chromium; WebKit is CI-only). The sheet is shared by fourteen menus,
so this tier is the regression check rather than the assertion.
npx tsc --noEmit,make css-check— clean.scrim on a dark surface is invisible in a shrunk preview while being
measurably present.
No Go, no
.sqland no.templchanged, somake lint,make test,make generateandmake bindingswere not run.Deliberately not done
background-attachmentand the component tier asserts exactly that;the rendered result needs pixels, and the only viewport on
mainwhere a menu overflows is 424x360 — contrived enough that the spec
would be about the fixture rather than the app. Once #67 merges, the
track list's menu overflows at the real 424x439 and a spec becomes
worth writing.
report rules the second out and
menu-surfacerules the first out.symmetric shadow is two more layers for a question nobody asked.
CLAUDE.md'smenu-surfacesection grows a seventh load-bearing point,and the measurements are in
.planning/NOTES.md.Closes #207
CI is green — run 17668,
checkande2eboth success (chromiumand WebKit).
The first run failed on #138, not on this change.
checkreportedtest/components/play-in-context.test.ts> "queues the list asdisplayed and starts on that row" with
which is #138's reported assertion byte-for-byte — a known flake at
~1 in 3 full-suite runs on pristine
main,Reviewed/Confirmed. Thisbranch touches
components/menu-surface/and its test only, andmenu-surface.test.tspassed in that same run (✓ 9 tests). Re-ran theworkflow with no change to the branch;
checkpassed ande2efollowed.Recorded here rather than silently re-run, since "a tier failed" and "a
tier failed for a reason unrelated to the diff" are different claims.