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:
@@ -187,6 +187,27 @@ function ensureOverlayListener(): void {
|
||||
});
|
||||
}
|
||||
|
||||
/** Close an overlay that holds a `useBackToClose` entry and THEN navigate — in that order, waiting
|
||||
* for the browser to confirm it.
|
||||
*
|
||||
* The trap: closing unmounts the overlay, whose cleanup reconciles the history by issuing a
|
||||
* programmatic `history.back()`. A `pushState` made before that lands (i.e. anything `setPage` /
|
||||
* `openChannel` does synchronously) is simply undone by it, so the navigation silently does
|
||||
* nothing. PlayerModal hit this first — clicking a video's channel name only closed the player —
|
||||
* and the command palette hit it again the day it was written, which is why the fix lives here
|
||||
* instead of being re-derived at each call site.
|
||||
*
|
||||
* Only call this from INSIDE an overlay that uses `useBackToClose`: the popstate we wait for is
|
||||
* that overlay's own entry being reclaimed, and without one it never arrives. */
|
||||
export function closeThenNavigate(close: () => void, navigate: () => void): void {
|
||||
const once = () => {
|
||||
window.removeEventListener("popstate", once);
|
||||
navigate();
|
||||
};
|
||||
window.addEventListener("popstate", once);
|
||||
close();
|
||||
}
|
||||
|
||||
/** While the calling component is mounted, Back (or the browser/mouse back button) closes it via
|
||||
* `onClose` instead of navigating away. Mount the component only while the overlay is open. */
|
||||
export function useBackToClose(onClose: () => void): void {
|
||||
|
||||
Reference in New Issue
Block a user