diff --git a/.planning/NOTES.md b/.planning/NOTES.md index f5b106b..28076da 100644 --- a/.planning/NOTES.md +++ b/.planning/NOTES.md @@ -3580,3 +3580,87 @@ per-card flag can answer at all. The general point: **two columns that agree today are not one column.** Which of them a new surface reads should be decided by which one has something that can un-set it. + +## The queue panel was a column that could not afford to be one (measured 2026-08-19) + +Plan 018, issue #24. Measured against the running app (`make +dev-headless SEED=default`, Chromium) on Playlists, sweeping the +viewport with the queue open and closed. Main panel width, and how much +of the page header survived: + +| viewport | sidebar | main (queue open) | actions clipped | +|---|---|---|---| +| 1280×800 | 200 | 759 | — | +| 1000×700 | 200 | 479 | 2 of 3 | +| **900×600** | 200 | **379** | all three | +| 800×600 | 56 | 423 | all three | +| 390×780 | — | **69** | all three | +| 320×600 | — | **0** | all three | +| 800×600 | 56 | 744 *(closed)* | New Smart Playlist, 158/162px | + +Five things came out of it that the issue did not say. + +- **The header clips at the enforced minimum with the queue closed.** + 800×600 is the only size this app promises, and "New Smart Playlist" + loses 4px of its 162 there. The queue makes it dramatic; it is not + the cause. +- **900×600 is worse than 800×600.** `AUTO_COLLAPSE_VIEWPORT` collapses + the sidebar *below* 900, so the main panel is 843px at 899 and 700px + at 900. **The worst desktop case is the top of the Compact band, not + the enforced floor** — so every viewport list that stopped at "the + minimum" was missing its own worst case. `layout-overflow.spec.ts` + carries 900 now. +- **At 320px the main panel was 0px.** The panel is `flex-shrink: 0` in + the flow of `.content-area`, so an open queue is paid for by the + content rather than covering it. Not degraded — gone. That is the + measurement #55 wanted and did not have. +- **Only Playlists overflows.** All ten primary views swept at 900×600 + and 390×780; every other header reports `scrollWidth == + clientWidth`, and Albums at 390 renders title, count and sort legibly + (checked on a screenshot, not just the number). So #69 is one view's + action set — three text buttons totalling 390px — and not a systemic + header failure. +- **Both reasons in `MinWidth`'s comment had expired.** The subtitle is + `display: none` from 899 down, and the sidebar host is + `overflow-y: auto` (at 600×460, `scrollHeight` 434 against a 332px + client, Settings reachable after scrolling). The floor is right; its + stated defence was two mechanisms that can no longer happen, which is + worse than either answer because nobody can argue with it. + +**A correction worth keeping, because it nearly went in the plan.** My +first probe for the sidebar's scroller searched +`shadowRoot.querySelectorAll('*')` and reported "no scroller — items +are unreachable", which reads exactly like a live Settings-unreachable +bug. The scroller is the **host**, and a host is not inside its own +shadow root. CLAUDE.md was right and the probe was wrong. + +**And one claim in the plan's first draft was too strong**: that the +overlay "removes the desktop half of #69". After phase 2, at 900×600, +open and closed are now *identical* (main 700, one action clipped) +where open used to be main 379 with all three clipped. The queue's +contribution is gone; the header's own overflow remains and is still a +live defect at a supported size. + +### The mode cannot be a media query + +The panel is drag-resizable 200–500px and persisted, so a viewport +breakpoint assumes the default 320 and is wrong by up to 180px for a +user who widened it — in the direction that hurts, since a wider queue +is exactly when the content can least afford it. It is computed from +`.content-area`'s width instead (which already accounts for the +sidebar's collapse), and the component test that matters widens the +panel at a *fixed* parent width and asserts the flip. + +The floor (480) is a judgement, and the measurement is why: there is no +cliff. The track list rescales its columns continuously — 213px down to +124px between main widths of 900 and 544, `rowOverflow=0` at every step +— and the album grid steps 3 columns to 2 somewhere between 564 and 644 +without breaking. So 480 is anchored at both ends instead: it keeps the +default 1100px window inline, and puts every measured-broken case on +the overlay side. + +The scrim is perceptible but subtle on a dark ramp, which is worth +knowing before someone "fixes" it as broken: sampled from screenshots at +900×600, the main panel's background goes 33,37,41 → 18,20,23 and a +row's text 242 → 133. It covers the content area only — not the sidebar +or the transport — because the queue is not modal. diff --git a/.planning/plans/active/018-supported-sizes-and-the-queue-model.md b/.planning/plans/active/018-supported-sizes-and-the-queue-model.md index caed6e6..7a134ef 100644 --- a/.planning/plans/active/018-supported-sizes-and-the-queue-model.md +++ b/.planning/plans/active/018-supported-sizes-and-the-queue-model.md @@ -183,6 +183,22 @@ follows immediately after this. What *this* plan owes it is the promise in the matrix — no action unreachable at any supported size — and the measurement that the only offender today is Playlists. +**And the promise is not kept yet, which is the honest version of a +claim this document made in its first draft.** "Decision 2 removes the +desktop half of #69's symptom" was too strong. Measured after phase 2, +at 900×600 on Playlists: + +| | before | after | +|---|---|---| +| queue open | main 379px, **all three** actions clipped | main 700px, **one** clipped | +| queue closed | main 700px, one clipped | unchanged | + +So the queue's *contribution* is gone — open and closed are now +identical, which is the whole of what this decision owed — and the +residual "New Smart Playlist: 114/162px" is the header overflowing on +its own, at a size the queue never touched. #69 is still a live defect +at a supported size, and the matrix's promise is what will close it. + --- ## Decision 4 — a very small window becomes the phone layout, not the mini-player @@ -219,16 +235,39 @@ correctness one. ## Phases 1. **This document**, linked from #24, with the matrix reported on the - issue and #55 told whether it is unblocked. *(no code)* + issue and #55 told whether it is unblocked. *(no code)* — **done** 2. **The queue's overlay mode** — computed mode attribute, scrim, Escape and scrim-click close, focus return. The inline path is - unchanged above the threshold. + unchanged above the threshold. — **done** 3. **The window minimum's comment** — replace both stale reasons with - the measured ones. No value change. + the measured ones. No value change. — **done** 4. **Verification**, below. Including the specs that must change - because they assert the old behaviour. + because they assert the old behaviour. — **done** -#69 follows as its own branch; #55 becomes unblocked at phase 2. +#69 follows as its own branch; #55 became unblocked at phase 2. + +## What landed, measured + +Main panel width with the queue open, before and after: + +| viewport | before | after | mode | +|---|---|---|---| +| 1280×800 | 759 | 759 | inline | +| 1100×720 (default window) | 579 | 579 | inline | +| 1024×768 | 503 | 503 | inline | +| 900×600 | **379** | **700** | overlay | +| 800×600 | 423 | 744 | overlay | +| 390×780 | **69** | **390** | overlay | +| 320×600 | **0** | **320** | overlay | + +The scrim is perceptible but subtle on a dark ramp, which is worth +knowing before someone "fixes" it: sampled from the screenshots at +900×600, the main panel's background goes 33,37,41 → 18,20,23 and a +row's text 242 → 133. It covers the **content area only** — not the +sidebar or the transport — on purpose: the queue is not modal, and +leaving the navigation live means the scrim reads as "this is over the +content" (which is what #24 asked for) without pretending the rest of +the app is unavailable. ## Verification, and what each tier cannot see diff --git a/CLAUDE.md b/CLAUDE.md index 9b408d5..a2b6346 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1328,6 +1328,68 @@ is 32px each. Which four is plan 016's committed subset, and everything else — Settings included, because a phone still needs it — is behind "More". +**There are three supported size bands, and the queue is part of the +promise.** Plan 018 (#24) wrote them down: **Phone** below 600 (bottom +nav, reflows, fits 320px exactly), **Compact** 600–899 (icon sidebar), +**Desktop** from 900 (labelled sidebar) — plus one sentence across all +three, *no action is ever unreachable at any supported size*. The bands +themselves already existed; what was new is that they are a promise and +that the queue panel is inside it. + +**900 is the worst desktop width, not the 800×600 minimum.** The +sidebar collapses to icons *below* 900, so the main panel is 843px at +899 and 700px at 900 — the narrowest content area any desktop width +produces is at the top of the Compact band, not at the enforced floor. +Every viewport list that stopped at "the minimum" was therefore missing +its own worst case, which is why `layout-overflow.spec.ts` carries 900 +now. And **both reasons in `MinWidth`'s comment had expired** — the +subtitle is `display: none` from 899 down and the sidebar host scrolls +(`overflow-y: auto`; at 600×460 its `scrollHeight` is 434 against a +332px client) — so 800×600 is a *comfort* floor for desktop chrome and +not a correctness one. Below it the phone layout takes over, which is +also why a very small window reflows rather than becoming a +mini-player: **#12 is a second always-on-top window, not a mode of this +one**, and making it a mode would discard navigation state on a resize +and put the process-level MPRIS question on a path a drag can trigger. + +**The queue panel is a column only while the content can spare the +width, and that cannot be a media query.** In flow the host is +`flex-shrink: 0`, so an open queue is paid for by the main panel: it +left 379px at 900×600 (with all three of the Playlists header's actions +clipped), 69px at 390, and **0px** at 320 — the content was not +degraded but gone. It goes to an overlay with a scrim when +`available - panelWidth < 480`, where `available` is +`.content-area`'s width and therefore already accounts for the +sidebar's collapse. + +Four things about it are load-bearing. **The mode is computed, not +breakpointed**, because the panel's width is user state — drag-resizable +200–500px and persisted — so a viewport breakpoint silently assumes the +default 320 and is wrong by up to 180px in the direction that hurts; +widening the panel at a fixed window size must flip it, and +`queue-overlay-mode.test.ts` is written around exactly that. **480 is a +judgement and says so**: there is no cliff to derive it from (the track +list rescales continuously, 213px to 124px columns with no row +overflow), so it is anchored to keep the default 1100px window inline +and put every measured-broken case on the overlay side. **The scrim +covers the content area only** — not the sidebar or the transport — +because the queue is not modal, and it is subtle on a dark ramp by +arithmetic rather than by accident (33,37,41 → 18,20,23). And **the +overlay is a presentation, not a fork**: #55 asks for one component +with two mount points, so the roving tab stop, Alt+Arrow reorder, drag +reorder, selection semantics and `virtualizer.requestUpdate()` all come +along untouched. Escape closes it and returns focus, and is attached +only while the overlay is up — it is a dismissal, not a shortcut, which +is why it is not a panel-scoped binding. + +What this does **not** fix is `page-header` overflowing on its own: +at 900×600 "New Smart Playlist" is still clipped to 114 of 162px with +the queue *closed*. That is #69, and it cannot be fixed in +`page-header` alone — actions arrive through `` as +arbitrary light-DOM markup with their own handlers, so collapsing them +into a "More actions" menu needs an actions *API* (data, not markup) +across all three hosts that slot them. + **The phone section of `index.css` is last on purpose.** A media query adds no specificity, so a `@media (max-width: 599px)` block placed above the plain rules it overrides loses to them — which is how phase 1