E32 review round 1 (F3–F6).
- Channels' open-row store and the chat dock use lib/store's createStore instead of a hand-rolled
listener set (the chat dock gets a test of its own).
- The Plex backdrop reads prefers-reduced-motion through useMediaQuery; the query string is one
constant (REDUCED_MOTION) for the hook and the carousel's one-off read.
- A link-restored collection filter showed its raw rating_key in the picker while the titles
loaded: usePlexCollectionTitles now says it is pending, the picker draws a placeholder
(labelsPending, as the channel picker does) and so does the chip row.
- The "deleted tag" rule was written twice with two i18n keys and had drifted: the channel manager
showed NO chip for a pick while the tag list loaded, or ever after a failed load, while the pick
still narrowed the table. One rule now (lib/tagPicks), one key (common.deletedTag); the manager's
row shows every pick, as a placeholder while loading and as its id after a failure.
- ui/MediaCard + mediaCardStyles: one frame for every video, Plex title and
Home tile; the meta line pinned to the bottom edge, the length a badge on
the picture, the media Play only where the click plays, the hover row at
the picture's bottom-right (the owner's pick C).
- Shelf density: on any strip a one-line title over one meta line; mixed
Home shelves are landscape; rails turn content-visibility off (a card's
reserved height had become the strip's). Home slot 232px, so the Play
and the hover row no longer touch; a landscape shelf takes the small Play.
- Home hero is a card (hover row, the whole menu, a touch "…"), three
quarters wide and centred; channel / playlist tiles get Play, open and
"…"; a show tile acts on its next episode, ⓘ opens the show.
- Plex preview: a ~0.9 s mouse rest opens a passive panel with the whole
title, facts and synopsis (popover `pointerOnly`); the synopsis and title
tooltips are gone.
- Compact list row B: the title leads, "…" at the end, toggles over the
date on hover.
- Media-info layout B (collections, crew, cast beside the facts sidebar;
an empty column is not drawn); filtering values are chips there and on
the show page; "Cast" over the cast-only strips.
- Browse tiles hover like a card; prose capped by the `measure` utility.
- In-place editors (Playlists heading + rail rename, Plex collection + playlist rename, PriorityStepper,
Scheduler interval): a real Enter closed the editor, returned the focus to the trigger, and the same
keystroke's activation pressed that trigger again - re-opening the editor, or in the rail selecting
the renamed playlist. useReturnFocus.arm(e) now cancels the key's default (type-enforced); the rail and
Scheduler editors do the same. The earlier tests dispatched a synthetic keydown, which has no default.
- Plex playlist rename: an unchanged or empty title closes with no write, as the collection rename does.
- Switch: a 36x24 box around the track (the 24 floor), 44 on a coarse pointer; `label` is required, and
the Plex two-way-sync switch had no name.
- z-index: numeric z-10/z-20 and a literal 40 moved onto the named layer scale; a lint rule + contract.
- The /watch file card's Download is the filled primary: the page's one action.
- Stale comments: the 1024x768 floor, the retired glass tuner.
The second half of the button sweep: the remaining hand-rolled buttons in
feature components, the design lab and the components/ui composites now
stand on Button, IconButton, Pressable, Chip or DragHandle.
- Chip + DismissibleChip (filter / token / tag / suggest / ghost, 24px
floor, 44 on touch) replace five chip geometries; FilterChipGroup
renders Chips; overflow lanes measure a mirror painted by chipClass.
- The Plex control row is IconButton's player rung (36, 20px glyph,
round, overlay; 44 on touch); the YT modal bar is on the sm rung with
pressed cycle toggles and names that survive a folded label.
- New variants: IconButton ghost-accent, danger-soft; Button warn on a
new state-warn utility. The .btn-{tone} recipes and btnCls are gone.
- Fixed on the way: membership rows (collection editor, Plex playlist
add) announce checked/mixed; a one-letter tag chip was 21px wide;
descriptionLinks moved out of lib/ (it renders components).
- local/button-primitive: a raw <button>/<FocusSafeButton> outside the
control-primitive files is a lint error (contract-tested, seen red).
Second slice of the E11 keyboard layer (S4b). S4a gave the feed, the library and the channel page
`j`/`k`, the arrows and Enter; Home's shelves and the whole Plex module were still Tab-stop country —
a Plex poster is its own Tab stop, and a rail's cards sat behind every control of the cards before.
- `lib/listNav`: a list that mounts every item no longer declares a count or columns — both are
measured off the DOM when a key is pressed, so an auto-fill grid's columns follow its width and a
rail is one row. ↑ on a list's top row and ↓ on its bottom row go on to the neighbouring list, onto
the item horizontally nearest the one left (owner decision D4: ←→ within a rail, ↑↓ to the rail
above or below). The first `j` skips cards a rail has scrolled out to the left.
- `Carousel keyboardNav` registers the scroller; the scroller gets a 4px inset (against a -4px margin,
layout unchanged) so the focus ring is not clipped, and `[data-nav-index]` a 2.5rem inline scroll
margin so a card stepped into view clears the rail's edge fade and paging arrow.
- `ui/NavGrid`: the plain, fully mounted grid that registers itself.
- `navIndex` on PlexPosterCard / SeasonCard / EpisodeCard (the card root is the item, the poster the
open element — the ring goes round poster and title, D2), PosterTile and Home's ShelfTile (the button
is the item); cast, related and collection-strip buttons carry the attrs directly.
- Wired: Home's comfort and resume rails; Plex Browse titles + episodes, Discover rails + genres,
collection view, playlists/collections browse, person known-for + filmography, show seasons, season
episodes, cast/related strips, info-page cast + collection strips.
- `listNav.wiring.contract.test`: every `Carousel` says `keyboardNav`, every card says `navIndex`, and a
card with `navIndex` sits inside a NavGrid / Carousel / VirtualGrid — both halves are opt-in, and
either one missing leaves a list the keyboard silently skips.
The app HAS a platform layer -- AnchoredPopover (22 importers, with
arrows/typeahead/Escape/anchor-flip), QueryState (21), Modal (17),
PageShell, DataTable, Tabs, Tooltip and ~26 more -- but it sat flat among
~160 feature components with nothing marking it as one. Several of those
files describe themselves as 'the one place' or 'canonical' and were
still bypassed by hand-rolled markup, because a layer nobody can see is a
layer nobody uses; three separate inventory passes of this codebase
misread it the same way.
Moves the 33 domain-free modules into components/ui/, rewrites the
imports in 89 files, and makes the boundary a lint rule rather than a
convention: inside ui/ a one-level-up import (a feature component) and
any lib/api slice are errors. The rule was verified to fire on both and
to leave generic lib/ helpers alone. README.md states which side of the
line a component belongs on and why.
No behaviour change: 3x tsc, eslint, knip, prettier and 687 vitest tests
all green; no backend file touched.