- SelectTrigger lays its toolbar value out as a row; the saved-view chip passes its glyph and
modified dot through the icon/trailing slots (an <svg> is display:block and broke the chip in two).
- One Escape in a dialog's open list closed the list AND the dialog: the list's close flushes
synchronously and pops its layer before the dialog's window listener runs. takeEscape() is the one
close-on-Escape test (topmost AND not yet taken); Modal, the Welcome lightbox and useLayer use it,
and keymap.dispatch ignores a taken Escape.
- /watch: the Download sits on the toolbar rung, level with the Repeat chip.
Round 2 made the step prompt a blocking layer so that `j` and the `g` chords
would not reach the page behind it. The dialog it sits in gave up its Tab trap
because of that, while the prompt's own replacement trap covered only its own
two controls — so Tab from anywhere else in the dialog (the bar's Screenshot or
Stop, the dialog's own buttons) walked out behind the scrim. `layerStack` now
separates the two questions it had folded into one flag: a layer can own the
keyboard and still leave the Tab trap to the dialog below it. The mirror case,
a dialog opening over the prompt, falls out of the same split.
A send's claim is now a lease with an owner. Every progress report re-stamps
it, so a send slower than the lease cannot lose it while it is still running,
and only the send that took a claim can give it back. The issue_create audit
row is staged inside the send and rides the same checkpoint as `issue_url`, so
the trail cannot be missing for an issue that exists. The attachment-name
prefix has one definition, shared by the producer and both readers.
A running send is reported to the UI. A reload or a second tab used to render
it exactly like a send that STOPPED, and offered a retry the server was certain
to refuse; the composer now says the report is being sent, holds the draft, and
puts both the retry and Create issue out. The past-sessions list says the same
for the same reason — it is the list a user opens to check on a send, and both
"draft" and "half sent" describe one that died. `/preview` and the retry path
got a pace of their own, and the per-user byte cap took the user-scoped lock
that makes it a cap instead of a guess.
Verified in a real browser on localdev: Tab cycles the whole dialog with the
prompt open, the prompt still swallows the page's keys, and the issue link is
focusable where the composer puts it. All 24 regression tests mutation-checked.
Deep-review of E11 (19 raised, 14 confirmed; 2 refuted, 3 latent test-hardening notes dropped).
- Back from a sub-view (a Plex show page, a Messages thread) restores the item it was opened from
even when focus was still inside the view: the first attempt now waits one task, past the
sub-view's own popstate listener (measured in Chrome: it left focus on <body>).
- The restore listener is installed at load, so the first Back after a reload finds it; a push
cancels a restore still waiting for the last landing's lists.
- Navigating over an overlay asks it again, a render apart, while its entry stays: the Plex
player peels one panel per Back step, so Shift+W over info + stats closed one panel and dropped
the navigation.
- j/k: a native <button> item keeps its own Tab stop; landing twice on the same item no longer
leaks its tabindex; after the focused item is removed (a hidden card) the next j goes on from
its slot when that slot is on screen.
- An abandoned `g` no longer swallows a modifier chord (g, then Mod+K opened the browser search).
- A hover card above a dialog no longer switches its Tab trap off (player, Modal, wizard,
Lightbox): new layerStack `anyBlockingLayerAbove`.
- "Manage views…" closes its menu before opening the dialog, through a shared `closeThenAct`
that ActionMenu now uses too.
- Copy: the EN welcome says "Feed"; the EN unreachable reason no longer claims a change;
stale comments about PlayerModal's scroll lock and dialog contract.
A feed card cost ~10 Tab stops, so the tenth card sat ~135 Tabs from the top of the page, and the
only keyboard focus ring (a 60% accent outline on the thumbnail) all but vanished on the dark glass.
First slice of the E11 keyboard layer (S4a); Home, Plex and the tables follow in S4b/S4c.
- `lib/listNav`: one window listener over every registered list, walked in document order. `j`/`k`
step item to item (on into the next list — owner decision D4), the arrows move around a grid, Enter
clicks the item's own `data-nav-open` element, so it opens exactly like a click. An item root is
never a Tab stop: it gets `tabindex=-1` only while the keyboard is on it, and keeps it until the next
item takes it, so the player's focus restore lands back on the card. The first `j` takes the first
card you can see, not the top of the list.
- `VirtualGrid` takes part with `keyboardNav` (column count + scrolling an unmounted row in);
`VirtualFeed` turns it on, so the feed, the library and the channel page all have it.
- Keymap: two-step sequences (`g h`) whose pending step belongs to the scope that started it; the
page-level scopes (`page`, `search`, `list`) are quiet while a layer that owns the keyboard is open,
enforced in `dispatch`. A test pins the owner's rule that none of them needs a modifier on the
Hungarian layout.
- `g h/f/c/p/x/n/m/d/s/,` jump to a module this account can reach; `s` focuses the header search box
and selects the old query; Escape in it hands the keyboard back to the page (Plex's typeahead spends
its first Escape on its panel).
- Hover popovers are passive layers: an open tooltip still takes Escape first but no longer silences
the page keys.
- Focus ring (owner decision D2 = B): solid 2px accent around the whole item; the app-wide button/
link/row ring goes from 60% to solid too. The item keeps a transparent resting outline because the
video card transitions `all` — without it every `j` flashed a 3px white ring (measured in Chrome).
- Help sheet: the new keys, in a fixed order after the surface's own; a `g` chord only for a reachable
module; opened over a player or dialog it leaves out the page-level keys and the context-menu key,
which nothing can reach there (the latter pre-existing since E10).
- CLAUDE.md: a canonical-patterns row for list/grid keys (`useListNav` + `navItemAttrs` +
`data-nav-open`, not a private arrow-key handler).
UX-8: one Escape must close only the topmost layer, never both an open
popover AND the player beneath it. The popover's Escape listener is on
`document` and fires before the player's on `window`; closing the popover
flushes its layer off the shared stack synchronously, so the player's
`anyLayerAbove()` lookup read "nothing above" and closed too. Add a
consumed-event flag to layerStack (`markEscapeHandled`/`escapeHandled`):
the acting topmost layer stamps the shared KeyboardEvent, and both players
check the stamp before the (now-mutated) stack. Immune to listener order
and mid-dispatch stack mutation. Cover with layerStack.dom.test.ts.
UX-7: the E4 `.reveal` sweep already covered the card action rows; two
residual hover-only traps remained — invisible-but-tappable/Tab-focusable
on touch. Add a group-agnostic `.reveal-hover` recipe (display:none under
`(hover: none)`) paired with each site's own hover/focus-within reveal:
- VideoCard play/Continue/Restart overlay: also reveal on
group-focus-within (keyboard), and drop it on touch — the card tap
already plays/resumes, so no permanent scrim over every mobile thumbnail.
- Carousel prev/next arrows: gone on touch, where the strip swipes.
Replace the three mutually-unaware Escape systems — Modal's private modalStack,
the popovers' document listeners with a blanket stopPropagation, and the two
players' modalCount() checks — with one shared lib/layerStack.ts. Every open
dismissable layer registers in open order; a layer acts on Escape only when it
is topmost, and reads anyLayerAbove() to decide whether to defer. Coordination
is now a stack lookup, not event-propagation order, so it no longer depends on
which listener happens to fire first.
- Modal + useDismiss register via the stack; useDismiss drops the blanket
stopPropagation (its documented side effect: it swallowed a player's own
Escape while any popover was open).
- Both players register as a layer and defer via anyLayerAbove() instead of
modalCount(); PlayerModal checks it before its maximise/fullscreen steps.
A player MENU is a layer too, so it closes first and the next Escape reaches
the auto-skip-cancel — fixing the documented PlexPlayer bug.
- AnchoredPopover routes non-hover Escape through the stack (floating-ui's own
escapeKey off there) and registers as a layer, so a player/modal underneath
defers once S4 migrates the in-player menus. FloatingTree still owns
popover-vs-popover ordering; the stack adds the cross-type ordering.