feat(design): a playlist's one action list (C1) — the row's "…", Delete + undo, the header's "…" (E28 S3e2)
- Playlist rows: the X column became the row's "…" (play, pin, copy title/link, move, order,
remove). Remove runs at once, with no confirm; its toast's Undo puts the row back at its index.
The Delete key removes a keyboard-walked row, and focus moves on to the next.
- POST /api/playlists/{id}/items takes an optional `position`, so an undo is one request that
renumbers the list around the row.
- A new `listDelete` key scope. Only a list with `onDelete` registers it, so the help sheet does not
offer Delete where nothing handles it.
- A playlist has one list, `usePlaylistActions.items`: rename, pin, YouTube open/push/revert,
delete. The rail row's "…" and the page header's "…" both open it. Push and revert moved into
the hook; the header keeps Play all.
- Phone-width playlist table: "#" and Channel are hidden below sm (`Column.phoneHidden`). Their
fixed widths had left the title no width.
This commit is contained in:
@@ -31,13 +31,19 @@ export type KeyScope =
|
||||
| "page"
|
||||
| "search"
|
||||
| "list"
|
||||
| "listDelete"
|
||||
| "undo"
|
||||
| "contextMenu"
|
||||
| "ytPlayer"
|
||||
| "plexPlayer"
|
||||
| "plexBrowse";
|
||||
|
||||
const PAGE_LEVEL: ReadonlySet<KeyScope> = new Set<KeyScope>(["page", "search", "list"]);
|
||||
const PAGE_LEVEL: ReadonlySet<KeyScope> = new Set<KeyScope>([
|
||||
"page",
|
||||
"search",
|
||||
"list",
|
||||
"listDelete",
|
||||
]);
|
||||
/** True for a scope that acts on the page beneath the overlays (see `KeyScope`). */
|
||||
export const isPageLevelScope = (scope: KeyScope): boolean => PAGE_LEVEL.has(scope);
|
||||
|
||||
@@ -132,6 +138,9 @@ export const SHORTCUTS = [
|
||||
{ id: "up", scope: "list", keys: ["ArrowUp"], labelKey: "keys.gridMove" },
|
||||
{ id: "down", scope: "list", keys: ["ArrowDown"], labelKey: "keys.gridMove" },
|
||||
{ id: "open", scope: "list", keys: ["Enter"], labelKey: "keys.open" },
|
||||
// Its own scope beside `list`: only a list that can take an item out (a playlist's rows) registers
|
||||
// it, so the sheet does not promise Delete on the feed or a Plex grid, where nothing answers it.
|
||||
{ id: "remove", scope: "listDelete", keys: ["Delete"], labelKey: "keys.listRemove" },
|
||||
// --- the row context menu (wherever useContextMenu is wired) ---------------------------------
|
||||
// Its OWN scope, not `global`: only the surfaces that actually call `useContextMenu` register it,
|
||||
// so the sheet stops advertising "Open the context menu" on the feed, Plex, Downloads and Settings
|
||||
@@ -412,7 +421,9 @@ export function useActiveScopes(): KeyScope[] {
|
||||
// registers `page` in its own effect, which React runs after every child's — so by mount order
|
||||
// the module jumps would head the sheet, above the list keys of the page you are looking at.
|
||||
const surfaces = live.filter((s) => !isPageLevelScope(s));
|
||||
const pageLevel = (["list", "search", "page"] as const).filter((s) => live.includes(s));
|
||||
const pageLevel = (["list", "listDelete", "search", "page"] as const).filter((s) =>
|
||||
live.includes(s),
|
||||
);
|
||||
return [...surfaces, ...pageLevel, "global"];
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user