feat(e10): a jump-to palette on Ctrl/Cmd+K
Reaching a channel three modules away cost a rail, a page and a scroll. Now it costs a keystroke and half a word: Mod+K searches modules, channels, playlists, the saved views applicable where you stand, and the app-wide actions. Scope agreed with the product owner: navigation + actions, but only the reversible local ones (the shortcuts sheet, the theme). "Sync from YouTube" is deliberately absent — it is a long-running job against a quota, it wants the progress and error surface the Channels page already gives it, and one mistyped Enter is a worse failure than the walk it would have saved. Matching is word-wise and accent-folded, because this is a bilingual app and nobody reaches for the accent keys in a hurry: "pod thevr" finds TheVR Podcast and "penzugy" finds pénzügy. The arrows walk one flat result list so crossing a group heading is invisible, and the field carries the combobox/listbox aria tree that virtual selection needs. ui/CommandPalette is domain-free (items are a prop, per the ui/ import rule); AppCommandPalette supplies them. The shell is Modal again, with a new `top` placement so a search-first dialog grows downwards instead of jumping as results change. Also fixes a bug the palette hit on its first run, and that PlayerModal had already hit once: closing a dialog reclaims its Back-to-close history entry with a programmatic history.back(), which undoes any pushState made before it lands — so a command that navigates only closed the palette. The rule is now closeThenNavigate() in history.ts with a test, and PlayerModal's inline copy is gone. The global `h` moves out of KeyboardHelp into useGlobalKeymap beside Mod+K, so one listener owns the whole `global` scope and dispatch's exhaustiveness keeps meaning what it says.
This commit is contained in:
@@ -51,6 +51,7 @@ export const SHORTCUTS = [
|
||||
// --- global ---------------------------------------------------------------------------------
|
||||
// `h`, not `?`: on a Hungarian layout `?` is a Shift chord, and the one help key people already
|
||||
// have in their fingers here is the Plex player's `h`. Making it app-wide costs no relearning.
|
||||
{ id: "palette", scope: "global", keys: ["Mod+k"], labelKey: "keys.palette" },
|
||||
{ id: "help", scope: "global", keys: ["h"], labelKey: "keys.help" },
|
||||
// Matched by `useContextMenu` on the focused row, not by a window-level dispatch: which row the
|
||||
// menu belongs to is a property of focus, not of the app.
|
||||
@@ -242,6 +243,36 @@ export const toggleKeyboardHelp = (): void => helpOpen.set((v) => !v);
|
||||
/** Subscribe to whether the sheet is showing (the Plex player suppresses wheel-volume under it). */
|
||||
export const useKeyboardHelpOpen = (): boolean => helpOpen.use();
|
||||
|
||||
// ── The command palette ───────────────────────────────────────────────────────────────────────
|
||||
const paletteOpen = createStore(false);
|
||||
|
||||
export const closePalette = (): void => paletteOpen.set(false);
|
||||
export const usePaletteOpen = (): boolean => paletteOpen.use();
|
||||
|
||||
/** Bind the `global` scope, once, for the whole app. It lives here rather than inside either
|
||||
* overlay because both are opened by it: a listener that sat in one of them would have to route a
|
||||
* key belonging to the other, and `dispatch`'s exhaustiveness would stop meaning anything. App
|
||||
* calls this; the overlays are pure renderers of their store. */
|
||||
export function useGlobalKeymap(): void {
|
||||
useEffect(() => {
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
dispatch(e, "global", {
|
||||
help: (ev) => {
|
||||
ev.preventDefault();
|
||||
helpOpen.set((v) => !v);
|
||||
},
|
||||
palette: (ev) => {
|
||||
// Mod+K is the browser's "focus the search bar" in a few builds; we want it.
|
||||
ev.preventDefault();
|
||||
paletteOpen.set((v) => !v);
|
||||
},
|
||||
});
|
||||
};
|
||||
window.addEventListener("keydown", onKey);
|
||||
return () => window.removeEventListener("keydown", onKey);
|
||||
}, []);
|
||||
}
|
||||
|
||||
/** Answers for the `when` conditions, published by the surface that knows them. */
|
||||
export type HelpConditions = { readonly [K in ShortcutWhen]?: boolean };
|
||||
const helpConditions = createStore<HelpConditions>({});
|
||||
|
||||
Reference in New Issue
Block a user