import { createContext, useContext, ReactNode, useCallback, useEffect, useMemo, useState, } from "react"; import { ShortcutDefinition } from "../hooks/useKeyboardShortcuts"; export interface ShortcutCategory { name: string; shortcuts: { [id: string]: ShortcutDefinition }; } interface ShortcutContextValue { categories: ShortcutCategory[]; registerCategory: (category: ShortcutCategory) => void; unregisterCategory: (categoryName: string) => void; showHelp: boolean; setShowHelp: (show: boolean) => void; } const ShortcutContext = createContext(undefined); export function ShortcutProvider({ children }: { children: ReactNode }) { const [categories, setCategories] = useState([]); const [showHelp, setShowHelp] = useState(false); useEffect(() => { const handler = (e: KeyboardEvent) => { const target = e.target as HTMLElement | null; const isEditableTarget = !!target && (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.tagName === "SELECT" || target.isContentEditable); // Don't steal regular typing inside form fields. // Allow modified shortcuts (cmd/ctrl/alt) to keep working. if (isEditableTarget && !e.metaKey && !e.ctrlKey && !e.altKey) { return; } if (showHelp && e.key !== "Escape") return; for (const category of categories) { for (const shortcut of Object.values(category.shortcuts)) { if (shortcut.enabled === false) continue; if (!!shortcut.meta !== e.metaKey) continue; if (!!shortcut.ctrl !== e.ctrlKey) continue; if (!!shortcut.alt !== e.altKey) continue; if (!!shortcut.shift !== e.shiftKey) continue; const keyMatches = e.key === shortcut.key || e.key.toLowerCase() === shortcut.key.toLowerCase(); if (!keyMatches) continue; e.preventDefault(); e.stopPropagation(); shortcut.handler(); return; } } }; window.addEventListener("keydown", handler); return () => window.removeEventListener("keydown", handler); }, [categories, showHelp]); const registerCategory = useCallback((category: ShortcutCategory) => { setCategories((prev) => { // Replace if exists, otherwise add const existing = prev.findIndex((c) => c.name === category.name); if (existing >= 0) { const updated = [...prev]; updated[existing] = category; return updated; } return [...prev, category]; }); }, []); const unregisterCategory = useCallback((categoryName: string) => { setCategories((prev) => prev.filter((c) => c.name !== categoryName)); }, []); const value = useMemo( () => ({ categories, registerCategory, unregisterCategory, showHelp, setShowHelp }), [categories, registerCategory, unregisterCategory, showHelp] ); return {children}; } export function useShortcutContext() { const context = useContext(ShortcutContext); if (!context) { throw new Error("useShortcutContext must be used within ShortcutProvider"); } return context; }