"use client"; // ShortcutsRoot — global key listener + command palette + help overlay. // Mount once in app/layout.tsx. Pages register actions via useAction(). import { useEffect, useState, useSyncExternalStore } from "react"; import { useRouter } from "next/navigation"; import { NAV } from "@/lib/nav"; import { shortcuts, eventToCombo, prettyCombo, shouldIgnore, type Action, } from "@/lib/shortcuts"; export function ShortcutsRoot() { const router = useRouter(); const [paletteOpen, setPaletteOpen] = useState(false); const [helpOpen, setHelpOpen] = useState(false); const [searchOpen, setSearchOpen] = useState(false); // Built-in actions: nav jumps, palette, search, help. useEffect(() => { const unsubs: Array<() => void> = []; // Nav jumps use mod+shift+N — browser owns plain mod+1..9 (tab switching). NAV.slice(0, 9).forEach((item, i) => { unsubs.push( shortcuts.register({ id: `nav-${i}`, label: `Go to ${item.label}`, combo: `mod+shift+${i + 1}`, group: "Navigation", run: () => router.push(item.href), }), ); }); unsubs.push( // Palette uses mod+shift+p — browser owns plain mod+p (print). shortcuts.register({ id: "palette", label: "Open command palette", combo: "mod+shift+p", group: "Help", run: () => setPaletteOpen(true), }), shortcuts.register({ id: "search", label: "Search", combo: "mod+k", group: "Actions", run: () => setSearchOpen(true), }), shortcuts.register({ id: "help", label: "Show keyboard shortcuts", combo: "?", group: "Help", run: () => setHelpOpen(true), }), ); return () => { for (const u of unsubs) u(); }; }, [router]); // Global key listener. useEffect(() => { const handler = (e: KeyboardEvent) => { // Always allow Escape to close overlays even from inputs. if (e.key === "Escape") { if (paletteOpen) { setPaletteOpen(false); e.preventDefault(); return; } if (helpOpen) { setHelpOpen(false); e.preventDefault(); return; } if (searchOpen) { setSearchOpen(false); e.preventDefault(); return; } } if (shouldIgnore(e)) return; const combo = eventToCombo(e); const action = shortcuts.byCombo(combo); if (action) { e.preventDefault(); void action.run(); } }; window.addEventListener("keydown", handler); return () => window.removeEventListener("keydown", handler); }, [paletteOpen, helpOpen, searchOpen]); return ( <> {paletteOpen && setPaletteOpen(false)} />} {helpOpen && setHelpOpen(false)} />} {searchOpen && setSearchOpen(false)} />} ); } // ─── Command palette ────────────────────────────────────────────────── function useShortcutsList(): Action[] { return useSyncExternalStore( (cb) => shortcuts.subscribe(cb), () => shortcuts.list(), () => shortcuts.list(), ); } function CommandPalette({ onClose }: { onClose: () => void }) { const all = useShortcutsList(); const [q, setQ] = useState(""); const filtered = q ? all.filter((a) => (a.label + " " + a.group + " " + (a.hint ?? "")).toLowerCase().includes(q.toLowerCase()), ) : all; return (
e.stopPropagation()}> setQ(e.target.value)} placeholder="Type a command…" className="sc-palette-input" />
    {filtered.length === 0 &&
  • No matches
  • } {filtered.map((a) => (
  • ))}
); } // ─── Help overlay ───────────────────────────────────────────────────── function ShortcutsHelp({ onClose }: { onClose: () => void }) { const all = useShortcutsList().filter((a) => a.combo); const groups: Record = {}; for (const a of all) (groups[a.group] ||= []).push(a); return (
e.stopPropagation()}>

Keyboard shortcuts

{Object.entries(groups).map(([group, actions]) => (

{group}

    {actions.map((a) => (
  • {a.label} {prettyCombo(a.combo!)}
  • ))}
))}
); } // ─── Search modal (placeholder) ─────────────────────────────────────── function SearchModal({ onClose }: { onClose: () => void }) { return (
e.stopPropagation()}>

TODO: wire global search. Esc to close.

); } function Overlay({ children, onClose }: { children: React.ReactNode; onClose: () => void }) { return (
{children}
); }