import { useEffect } from 'react'; export interface ShortcutDef { /** Key to match (e.g. 'k', 'n', 'Escape', '/') */ key: string; /** Require Cmd (Mac) or Ctrl (Windows/Linux) — default: false */ meta?: boolean; /** Require Shift — default: false */ shift?: boolean; /** Require Alt/Option — default: false */ alt?: boolean; /** Handler to invoke when shortcut fires */ handler: () => void; /** Allow firing when focus is in input/textarea — default: false */ allowInInput?: boolean; /** Human-readable description for help overlay / accessibility */ description?: string; } function isInputElement(el: EventTarget | null): boolean { if (!el || !(el instanceof HTMLElement)) { return false; } const tag = el.tagName; return tag === 'INPUT' || tag === 'TEXTAREA' || el.isContentEditable; } export function useKeyboardShortcuts(shortcuts: ShortcutDef[]): void { useEffect(() => { if (shortcuts.length === 0) { return; } function handleKeyDown(e: KeyboardEvent) { if (!e.key) return; const inInput = isInputElement(e.target); for (const shortcut of shortcuts) { if (!shortcut.key) continue; const metaMatch = shortcut.meta ? e.metaKey || e.ctrlKey : !(e.metaKey || e.ctrlKey); const shiftMatch = shortcut.shift ? e.shiftKey : !e.shiftKey; const altMatch = shortcut.alt ? e.altKey : !e.altKey; // Normalize key comparison (case-insensitive for letters) const keyMatch = e.key.toLowerCase() === shortcut.key.toLowerCase(); if (keyMatch && metaMatch && shiftMatch && altMatch) { if (inInput && !shortcut.allowInInput) { continue; } e.preventDefault(); shortcut.handler(); return; } } } window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [shortcuts]); }