feat(audit): Batch 17 — screen scroll contract + History virtualization (L161, L104)

L161: <main> is now a non-scrolling flex slot; each screen owns its scroll
via shared shells in ui/Screen.tsx — 'page' (Library/Settings/empty History
scroll the shell) and 'fill' (Downloads/Terminal/History scroll their own
list/log, shell scrolls only as short-window fallback). height:100%
couplings replaced with flexGrow+minHeight:0; list regions floored at
160px so they can't collapse on short windows.

L104: HistoryView renders through VirtualList; Ctrl+A re-homed onto a
wrapper around the scroller, per-row Delete unchanged on rows.

Verified with an Electron probe against the vite preview at 1100x720 and
800x520 (probe metrics + screenshots): one active scroller per screen,
main/body never scroll, floors hold. Adds .claude/launch.json for the
preview server.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-02 11:43:34 -04:00
parent dbbb0c70f9
commit 17d42ed4fe
9 changed files with 755 additions and 642 deletions
+18 -12
View File
@@ -992,13 +992,13 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n
`<Spinner size="tiny">` (Fetch/Index/Check-for-new/yt-dlp update/app-update/PO-mint/version-check); the only
remaining `<Spinner>`s are standalone status-row indicators (the "Fetching…"/"Waiting to start…"/indexing
rows), not a button + adjacent spinner. No separate-adjacent-spinner pattern survives.*
- [ ] **L104 — History isn't virtualized.** It renders all filtered rows, while Downloads and Library use `VirtualList`.
*Deferred (bounded; needs the L161 layout rework + live verification): History is capped at 500 rows (L141),
so the un-virtualized render is bounded — not a real perf problem. Virtualizing it requires giving the list a
fixed-height internal scroll container (the same `height:100%`/`overflow` rework L161 flags as needing
live verification at multiple window sizes) *and* re-homing its container-level Ctrl+A and per-row Delete
keyboard handling onto the virtualized scroller. That's a layout+interaction change best done watched, so it
rides with L161 rather than shipping unattended for a list that's already capped.*
- [x] **L104 — History isn't virtualized.** It renders all filtered rows, while Downloads and Library use `VirtualList`.
*Fixed (Batch 17, with L161): HistoryView renders through the shared `VirtualList` inside the new `fill`
screen shell — the list owns an internal scroll region (flex + 160px floor, DownloadsView's contract). The
container-level Ctrl+A moved onto a wrapper around the virtualized scroller (key events bubble up from the
rows), and per-row Delete stays on each row so it works wherever the row mounts. Verified live in an
Electron probe against the vite preview at 1100×720 and 800×520: rows render via `[data-index]` virtual
wrappers, the list scrolls internally, and `<main>`/body never scroll (probe metrics + screenshots).*
- [x] **L105 — `MediaKind` declared twice.** In both [ipc.ts](src/shared/ipc.ts) and
[store/downloads.ts](src/renderer/src/store/downloads.ts); components import it from both. Single source it.
*Fixed: the store now imports `MediaKind` from `@shared/ipc` and re-exports it, so the duplicate local
@@ -1301,12 +1301,18 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n
8px) instead of style classes (extends L5). *Fixed: the margin literals now draw from `SPACE`
(`hairline`/`xtight`/`tight`) — same values, one source. (The broader inline-`style` vs makeStyles split is
still L5.)*
- [ ] **L161 — `height: 100%` screens inside a padded scroll container.** Downloads/Terminal set
- [x] **L161 — `height: 100%` screens inside a padded scroll container.** Downloads/Terminal set
`height: 100%` while App's `<main>` is `overflowY: auto` + 24/28px padding — a fragile coupling that can
double-scroll or misalign the virtualized list's viewport. *Deferred (layout, needs live verification): it
works today (the virtualized queue scrolls correctly), and reworking the `<main>` scroll/height contract is
a layout change best validated with the app open at several window sizes — a watched pass, not an unattended
edit.*
double-scroll or misalign the virtualized list's viewport. *Fixed (Batch 17): `<main>` no longer scrolls or
pads — it's a plain flex slot, and each screen owns its scrolling through two shared shells in
[ui/Screen.tsx](src/renderer/src/components/ui/Screen.tsx): `page` (document screens — Library, Settings,
empty History: the shell scrolls) and `fill` (internally-scrolling screens — Downloads, Terminal, History:
the screen's list/log is the scroller, with the shell scrolling only as the short-window fallback). The
`height:100%` couplings are gone (`flexGrow:1; minHeight:0` inside the shell instead). The rework surfaced a
real pre-existing edge: on a very short window the flexed list collapsed to 0px — now floored at 160px with
the shell absorbing the overflow. Verified with an Electron probe at 1100×720 and 800×520 per screen:
exactly one active scroller per screen in the normal case, `<main>`/body never scroll, floors hold
(probe metrics + screenshots).*
- [ ] **L162 — Per-thumbnail store subscription.** Every `MediaThumb` calls `useResolvedDark()` (two store
subscriptions) instead of receiving a resolved theme prop — N subscriptions per list. *Deferred — same as
PERF6: virtualization/the 500-cap bounds the mounted count, and a prop conflicts with PERF5's hoisted