feat(e10): open the app's menu with a right-click, starting with playlist rows

S2's headline was never a new Menu: `ActionMenu` already sits on `AnchoredPopover`
(arrows, typeahead, Escape, anchor-flip). What was missing is an INPUT PATH into it, so
this adds `useContextMenu` + `<ContextMenu>` over the same `ActionMenuItem[]` and the same
`MenuItemList` — a row cannot grow a right-click menu that looks or behaves unlike its own
⋯ menu. Three ways in: right-click anywhere on the row, long-press on touch/pen, and
Shift+F10 / the Menu key. The LEFT button is untouched and keeps the row's own action.

One popover per list, anchored to a virtual element at the pointer (native behaviour, and
a table of 200 rows would otherwise mount 200 floating contexts to keep 199 shut).

Platform bits it needed: `hoverAlso` on AnchoredPopover (a submenu falls open under a
mouse and is tapped by a finger — mouse-only, so a tap's synthetic mouseenter can't race
the click toggle) and `elementRef` on PopoverItem (the submenu anchors to the very
menuitem that opens it, where aria-haspopup/expanded belong). `ActionMenuItem` becomes a
discriminated union — a row either fires or opens a submenu — plus `danger` and
`separatorBefore`.

First consumers, both in Playlists:
- item rows: play · move to another list (a submenu of every other playlist, one gesture)
  · to top / up / down / bottom · remove. Reordering goes through the same undoable store
  as the drag handle and is disabled under a view for the same reason, with the same
  explanation rather than silence.
- rail rows: rename (in the row that was right-clicked, keeping its cover so the rail
  doesn't jump) · delete. Both now run through `usePlaylistActions`, which also takes over
  the detail header's pencil and trash — the delete dialog's YouTube branch and the
  "what gets selected next" rule exist once, not twice.
- the rail gains a "recently changed" sort key off the new `updated_at`. NOT the default:
  a rail that reshuffles itself while you look at it costs more than it saves.
This commit is contained in:
2026-09-05 19:59:56 +02:00
parent b28be682fe
commit 928eac4c89
11 changed files with 997 additions and 111 deletions
+16
View File
@@ -559,6 +559,22 @@ body {
min-height: 2.75rem;
}
}
/* ===== E10 S2 · rows that answer a right-click / long-press (useContextMenu) =====
The long-press path competes with two browser behaviours that fire at the same gesture:
iOS Safari's own callout (Copy / Share over a link or image) and the text-selection handles.
Both would appear ON TOP of our menu, so the attribute the hook stamps on the row turns them
off. Deliberately NOT `user-select: none` on a precise pointer — selecting a video title with
the mouse is legitimate, and only the touch path has the conflict. */
[data-context-menu-target] {
-webkit-touch-callout: none;
}
@media (pointer: coarse) {
[data-context-menu-target] {
-webkit-user-select: none;
user-select: none;
}
}
}
.glass-hover:hover {
border-color: color-mix(in srgb, var(--accent) 55%, transparent);