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:
@@ -32,11 +32,13 @@ export default function Modal({
|
||||
onClose: () => void;
|
||||
children: ReactNode;
|
||||
maxWidth?: string;
|
||||
/** `drawer` slides in from the right edge and runs full height; everything else about the shell —
|
||||
* the layer stack, ESC, the focus trap, Back-to-close, the scroll lock — is identical. A drawer
|
||||
* is a placement, not a second dialog implementation: duplicating that behaviour is exactly how
|
||||
* two shells drift apart. */
|
||||
placement?: "center" | "drawer";
|
||||
/** `drawer` slides in from the right edge and runs full height; `top` pins the panel near the top
|
||||
* of the viewport (where a search-first dialog like the command palette belongs, so the list grows
|
||||
* downwards instead of the whole thing jumping as results change). Everything else about the shell
|
||||
* — the layer stack, ESC, the focus trap, Back-to-close, the scroll lock — is identical. A
|
||||
* placement is not a second dialog implementation: duplicating that behaviour is exactly how two
|
||||
* shells drift apart. */
|
||||
placement?: "center" | "drawer" | "top";
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
// Browser/mouse Back closes the topmost modal instead of navigating away.
|
||||
@@ -114,10 +116,15 @@ export default function Modal({
|
||||
}, [restoreTo]);
|
||||
|
||||
const drawer = placement === "drawer";
|
||||
const top = placement === "top";
|
||||
return createPortal(
|
||||
<div
|
||||
className={`fixed inset-0 z-overlay flex bg-black/70 overlay-backdrop ${
|
||||
drawer ? "justify-end" : "items-center justify-center p-4 sm:p-6"
|
||||
drawer
|
||||
? "justify-end"
|
||||
: top
|
||||
? "items-start justify-center p-4 pt-[8vh] sm:p-6 sm:pt-[10vh]"
|
||||
: "items-center justify-center p-4 sm:p-6"
|
||||
}`}
|
||||
onMouseDown={(e) => {
|
||||
pressedOnBackdrop.current = e.target === e.currentTarget;
|
||||
|
||||
Reference in New Issue
Block a user