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:
@@ -143,11 +143,15 @@ describe("dispatch", () => {
|
||||
const calls: string[] = [];
|
||||
// The handler map is the global scope's routable ids only — `contextMenu` is absent from it,
|
||||
// which is the compile-time half of the same claim; tsc rejects passing one.
|
||||
const handlers = { help: () => calls.push("help") };
|
||||
const handlers = {
|
||||
help: () => void calls.push("help"),
|
||||
palette: () => void calls.push("palette"),
|
||||
};
|
||||
expect(dispatch(press({ key: "F10", shiftKey: true }), "global", handlers)).toBe(false);
|
||||
expect(dispatch(press({ key: "ContextMenu" }), "global", handlers)).toBe(false);
|
||||
expect(dispatch(press({ key: "h" }), "global", handlers)).toBe(true);
|
||||
expect(calls).toEqual(["help"]);
|
||||
expect(dispatch(press({ key: "k", ctrlKey: true }), "global", handlers)).toBe(true);
|
||||
expect(calls).toEqual(["help", "palette"]);
|
||||
});
|
||||
|
||||
it("ignores keys belonging to another scope", () => {
|
||||
@@ -203,7 +207,7 @@ describe("helpLines", () => {
|
||||
it("orders scopes as asked, not as tabled", () => {
|
||||
const first = (scopes: Parameters<typeof helpLines>[0]) => helpLines(scopes)[0]?.labelKey;
|
||||
expect(first(["plexBrowse", "global"])).toBe("keys.browseBack");
|
||||
expect(first(["global", "plexBrowse"])).toBe("keys.help");
|
||||
expect(first(["global", "plexBrowse"])).toBe("keys.palette");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user