@pyreon/hotkeys 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/parse.ts","../src/registry.ts","../src/use-hotkey.ts","../src/use-hotkey-scope.ts"],"sourcesContent":["import type { KeyCombo } from './types'\n\n// ─── Key aliases ─────────────────────────────────────────────────────────────\n\nconst KEY_ALIASES: Record<string, string> = {\n esc: 'escape',\n return: 'enter',\n del: 'delete',\n ins: 'insert',\n space: ' ',\n spacebar: ' ',\n up: 'arrowup',\n down: 'arrowdown',\n left: 'arrowleft',\n right: 'arrowright',\n plus: '+',\n}\n\n/**\n * Parse a shortcut string like 'ctrl+shift+s' into a KeyCombo.\n * Supports aliases (esc, del, space, etc.) and mod (ctrl on Windows/Linux, meta on Mac).\n */\nexport function parseShortcut(shortcut: string): KeyCombo {\n const parts = shortcut.toLowerCase().trim().split('+')\n const combo: KeyCombo = {\n ctrl: false,\n shift: false,\n alt: false,\n meta: false,\n key: '',\n }\n\n for (const part of parts) {\n const p = part.trim()\n if (p === 'ctrl' || p === 'control') {\n combo.ctrl = true\n } else if (p === 'shift') {\n combo.shift = true\n } else if (p === 'alt') {\n combo.alt = true\n } else if (p === 'meta' || p === 'cmd' || p === 'command') {\n combo.meta = true\n } else if (p === 'mod') {\n // mod = meta on Mac, ctrl elsewhere\n if (isMac()) {\n combo.meta = true\n } else {\n combo.ctrl = true\n }\n } else {\n combo.key = KEY_ALIASES[p] ?? p\n }\n }\n\n return combo\n}\n\n/**\n * Check if a KeyboardEvent matches a KeyCombo.\n */\nexport function matchesCombo(event: KeyboardEvent, combo: KeyCombo): boolean {\n if (event.ctrlKey !== combo.ctrl) return false\n if (event.shiftKey !== combo.shift) return false\n if (event.altKey !== combo.alt) return false\n if (event.metaKey !== combo.meta) return false\n\n const eventKey = event.key.toLowerCase()\n return eventKey === combo.key\n}\n\n/**\n * Format a KeyCombo back to a human-readable string.\n */\nexport function formatCombo(combo: KeyCombo): string {\n const parts: string[] = []\n if (combo.ctrl) parts.push('Ctrl')\n if (combo.shift) parts.push('Shift')\n if (combo.alt) parts.push('Alt')\n if (combo.meta) parts.push(isMac() ? '⌘' : 'Meta')\n parts.push(\n combo.key.length === 1 ? combo.key.toUpperCase() : capitalize(combo.key),\n )\n return parts.join('+')\n}\n\nfunction capitalize(s: string): string {\n return s.charAt(0).toUpperCase() + s.slice(1)\n}\n\nfunction isMac(): boolean {\n if (typeof navigator === 'undefined') return false\n return /mac|iphone|ipad|ipod/i.test(navigator.userAgent)\n}\n","import type { Signal } from '@pyreon/reactivity'\nimport { signal } from '@pyreon/reactivity'\nimport { matchesCombo, parseShortcut } from './parse'\nimport type { HotkeyEntry, HotkeyOptions } from './types'\n\n// ─── State ───────────────────────────────────────────────────────────────────\n\nconst entries: HotkeyEntry[] = []\nconst activeScopes = signal<Set<string>>(new Set(['global']))\nlet listenerAttached = false\n\n// ─── Input detection ─────────────────────────────────────────────────────────\n\nconst INPUT_TAGS = new Set(['INPUT', 'TEXTAREA', 'SELECT'])\n\nfunction isInputFocused(event: KeyboardEvent): boolean {\n const target = event.target as HTMLElement | null\n if (!target) return false\n if (INPUT_TAGS.has(target.tagName)) return true\n if (target.isContentEditable) return true\n return false\n}\n\n// ─── Global listener ─────────────────────────────────────────────────────────\n\nfunction attachListener(): void {\n if (listenerAttached) return\n if (typeof window === 'undefined') return\n listenerAttached = true\n\n window.addEventListener('keydown', (event) => {\n const scopes = activeScopes.peek()\n\n for (const entry of entries) {\n // Check scope\n if (!scopes.has(entry.options.scope)) continue\n\n // Check enabled\n const enabled =\n typeof entry.options.enabled === 'function'\n ? entry.options.enabled()\n : entry.options.enabled\n if (!enabled) continue\n\n // Check input focus\n if (!entry.options.enableOnInputs && isInputFocused(event)) continue\n\n // Check key match\n if (!matchesCombo(event, entry.combo)) continue\n\n // Match found\n if (entry.options.preventDefault) event.preventDefault()\n if (entry.options.stopPropagation) event.stopPropagation()\n entry.handler(event)\n }\n })\n}\n\n// ─── Registration ────────────────────────────────────────────────────────────\n\n/**\n * Register a keyboard shortcut. Returns an unregister function.\n *\n * @example\n * ```ts\n * const unregister = registerHotkey('ctrl+s', (e) => save(), { description: 'Save' })\n * // later: unregister()\n * ```\n */\nexport function registerHotkey(\n shortcut: string,\n handler: (event: KeyboardEvent) => void,\n options?: HotkeyOptions,\n): () => void {\n attachListener()\n\n const entry: HotkeyEntry = {\n shortcut,\n combo: parseShortcut(shortcut),\n handler,\n options: {\n scope: options?.scope ?? 'global',\n preventDefault: options?.preventDefault !== false,\n stopPropagation: options?.stopPropagation === true,\n enableOnInputs: options?.enableOnInputs === true,\n enabled: options?.enabled ?? true,\n ...(options?.description != null\n ? { description: options.description }\n : {}),\n },\n }\n\n entries.push(entry)\n\n return () => {\n const idx = entries.indexOf(entry)\n if (idx !== -1) entries.splice(idx, 1)\n }\n}\n\n// ─── Scope management ────────────────────────────────────────────────────────\n\n/**\n * Activate a hotkey scope. 'global' is always active.\n */\nexport function enableScope(scope: string): void {\n const current = activeScopes.peek()\n if (current.has(scope)) return\n const next = new Set(current)\n next.add(scope)\n activeScopes.set(next)\n}\n\n/**\n * Deactivate a hotkey scope. Cannot deactivate 'global'.\n */\nexport function disableScope(scope: string): void {\n if (scope === 'global') return\n const current = activeScopes.peek()\n if (!current.has(scope)) return\n const next = new Set(current)\n next.delete(scope)\n activeScopes.set(next)\n}\n\n/**\n * Get the currently active scopes as a reactive signal.\n */\nexport function getActiveScopes(): Signal<Set<string>> {\n return activeScopes\n}\n\n/**\n * Get all registered hotkeys (for building help dialogs).\n */\nexport function getRegisteredHotkeys(): ReadonlyArray<{\n shortcut: string\n scope: string\n description?: string\n}> {\n return entries.map((e) => ({\n shortcut: e.shortcut,\n scope: e.options.scope,\n ...(e.options.description != null\n ? { description: e.options.description }\n : {}),\n }))\n}\n\n// ─── Reset (for testing) ────────────────────────────────────────────────────\n\nexport function _resetHotkeys(): void {\n entries.length = 0\n activeScopes.set(new Set(['global']))\n}\n","import { onUnmount } from '@pyreon/core'\nimport { registerHotkey } from './registry'\nimport type { HotkeyOptions } from './types'\n\n/**\n * Register a keyboard shortcut scoped to a component's lifecycle.\n * Automatically unregisters when the component unmounts.\n *\n * @example\n * ```ts\n * function Editor() {\n * useHotkey('ctrl+s', () => save(), { description: 'Save document' })\n * useHotkey('ctrl+z', () => undo())\n * useHotkey('ctrl+shift+z', () => redo())\n * // ...\n * }\n * ```\n */\nexport function useHotkey(\n shortcut: string,\n handler: (event: KeyboardEvent) => void,\n options?: HotkeyOptions,\n): void {\n const unregister = registerHotkey(shortcut, handler, options)\n onUnmount(unregister)\n}\n","import { onUnmount } from '@pyreon/core'\nimport { disableScope, enableScope } from './registry'\n\n/**\n * Activate a hotkey scope for the lifetime of a component.\n * When the component unmounts, the scope is deactivated.\n *\n * @example\n * ```tsx\n * function Modal() {\n * useHotkeyScope('modal')\n * useHotkey('escape', () => closeModal(), { scope: 'modal' })\n * // ...\n * }\n * ```\n */\nexport function useHotkeyScope(scope: string): void {\n enableScope(scope)\n onUnmount(() => disableScope(scope))\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/parse.ts","../src/registry.ts","../src/use-hotkey.ts","../src/use-hotkey-scope.ts"],"sourcesContent":["import type { KeyCombo } from \"./types\"\n\n// ─── Key aliases ─────────────────────────────────────────────────────────────\n\nconst KEY_ALIASES: Record<string, string> = {\n esc: \"escape\",\n return: \"enter\",\n del: \"delete\",\n ins: \"insert\",\n space: \" \",\n spacebar: \" \",\n up: \"arrowup\",\n down: \"arrowdown\",\n left: \"arrowleft\",\n right: \"arrowright\",\n plus: \"+\",\n}\n\n/**\n * Parse a shortcut string like 'ctrl+shift+s' into a KeyCombo.\n * Supports aliases (esc, del, space, etc.) and mod (ctrl on Windows/Linux, meta on Mac).\n */\nexport function parseShortcut(shortcut: string): KeyCombo {\n const parts = shortcut.toLowerCase().trim().split(\"+\")\n const combo: KeyCombo = {\n ctrl: false,\n shift: false,\n alt: false,\n meta: false,\n key: \"\",\n }\n\n for (const part of parts) {\n const p = part.trim()\n if (p === \"ctrl\" || p === \"control\") {\n combo.ctrl = true\n } else if (p === \"shift\") {\n combo.shift = true\n } else if (p === \"alt\") {\n combo.alt = true\n } else if (p === \"meta\" || p === \"cmd\" || p === \"command\") {\n combo.meta = true\n } else if (p === \"mod\") {\n // mod = meta on Mac, ctrl elsewhere\n if (isMac()) {\n combo.meta = true\n } else {\n combo.ctrl = true\n }\n } else {\n combo.key = KEY_ALIASES[p] ?? p\n }\n }\n\n return combo\n}\n\n/**\n * Check if a KeyboardEvent matches a KeyCombo.\n */\nexport function matchesCombo(event: KeyboardEvent, combo: KeyCombo): boolean {\n if (event.ctrlKey !== combo.ctrl) return false\n if (event.shiftKey !== combo.shift) return false\n if (event.altKey !== combo.alt) return false\n if (event.metaKey !== combo.meta) return false\n\n const eventKey = event.key.toLowerCase()\n return eventKey === combo.key\n}\n\n/**\n * Format a KeyCombo back to a human-readable string.\n */\nexport function formatCombo(combo: KeyCombo): string {\n const parts: string[] = []\n if (combo.ctrl) parts.push(\"Ctrl\")\n if (combo.shift) parts.push(\"Shift\")\n if (combo.alt) parts.push(\"Alt\")\n if (combo.meta) parts.push(isMac() ? \"⌘\" : \"Meta\")\n parts.push(combo.key.length === 1 ? combo.key.toUpperCase() : capitalize(combo.key))\n return parts.join(\"+\")\n}\n\nfunction capitalize(s: string): string {\n return s.charAt(0).toUpperCase() + s.slice(1)\n}\n\nfunction isMac(): boolean {\n if (typeof navigator === \"undefined\") return false\n return /mac|iphone|ipad|ipod/i.test(navigator.userAgent)\n}\n","import type { Signal } from \"@pyreon/reactivity\"\nimport { signal } from \"@pyreon/reactivity\"\nimport { matchesCombo, parseShortcut } from \"./parse\"\nimport type { HotkeyEntry, HotkeyOptions } from \"./types\"\n\n// ─── State ───────────────────────────────────────────────────────────────────\n\nconst entries: HotkeyEntry[] = []\nconst activeScopes = signal<Set<string>>(new Set([\"global\"]))\nlet listenerAttached = false\n\n// ─── Input detection ─────────────────────────────────────────────────────────\n\nconst INPUT_TAGS = new Set([\"INPUT\", \"TEXTAREA\", \"SELECT\"])\n\nfunction isInputFocused(event: KeyboardEvent): boolean {\n const target = event.target as HTMLElement | null\n if (!target) return false\n if (INPUT_TAGS.has(target.tagName)) return true\n if (target.isContentEditable) return true\n return false\n}\n\n// ─── Global listener ─────────────────────────────────────────────────────────\n\nfunction attachListener(): void {\n if (listenerAttached) return\n if (typeof window === \"undefined\") return\n listenerAttached = true\n\n window.addEventListener(\"keydown\", (event) => {\n const scopes = activeScopes.peek()\n\n for (const entry of entries) {\n // Check scope\n if (!scopes.has(entry.options.scope)) continue\n\n // Check enabled\n const enabled =\n typeof entry.options.enabled === \"function\"\n ? entry.options.enabled()\n : entry.options.enabled\n if (!enabled) continue\n\n // Check input focus\n if (!entry.options.enableOnInputs && isInputFocused(event)) continue\n\n // Check key match\n if (!matchesCombo(event, entry.combo)) continue\n\n // Match found\n if (entry.options.preventDefault) event.preventDefault()\n if (entry.options.stopPropagation) event.stopPropagation()\n entry.handler(event)\n }\n })\n}\n\n// ─── Registration ────────────────────────────────────────────────────────────\n\n/**\n * Register a keyboard shortcut. Returns an unregister function.\n *\n * @example\n * ```ts\n * const unregister = registerHotkey('ctrl+s', (e) => save(), { description: 'Save' })\n * // later: unregister()\n * ```\n */\nexport function registerHotkey(\n shortcut: string,\n handler: (event: KeyboardEvent) => void,\n options?: HotkeyOptions,\n): () => void {\n attachListener()\n\n const entry: HotkeyEntry = {\n shortcut,\n combo: parseShortcut(shortcut),\n handler,\n options: {\n scope: options?.scope ?? \"global\",\n preventDefault: options?.preventDefault !== false,\n stopPropagation: options?.stopPropagation === true,\n enableOnInputs: options?.enableOnInputs === true,\n enabled: options?.enabled ?? true,\n ...(options?.description != null ? { description: options.description } : {}),\n },\n }\n\n entries.push(entry)\n\n return () => {\n const idx = entries.indexOf(entry)\n if (idx !== -1) entries.splice(idx, 1)\n }\n}\n\n// ─── Scope management ────────────────────────────────────────────────────────\n\n/**\n * Activate a hotkey scope. 'global' is always active.\n */\nexport function enableScope(scope: string): void {\n const current = activeScopes.peek()\n if (current.has(scope)) return\n const next = new Set(current)\n next.add(scope)\n activeScopes.set(next)\n}\n\n/**\n * Deactivate a hotkey scope. Cannot deactivate 'global'.\n */\nexport function disableScope(scope: string): void {\n if (scope === \"global\") return\n const current = activeScopes.peek()\n if (!current.has(scope)) return\n const next = new Set(current)\n next.delete(scope)\n activeScopes.set(next)\n}\n\n/**\n * Get the currently active scopes as a reactive signal.\n */\nexport function getActiveScopes(): Signal<Set<string>> {\n return activeScopes\n}\n\n/**\n * Get all registered hotkeys (for building help dialogs).\n */\nexport function getRegisteredHotkeys(): ReadonlyArray<{\n shortcut: string\n scope: string\n description?: string\n}> {\n return entries.map((e) => ({\n shortcut: e.shortcut,\n scope: e.options.scope,\n ...(e.options.description != null ? { description: e.options.description } : {}),\n }))\n}\n\n// ─── Reset (for testing) ────────────────────────────────────────────────────\n\nexport function _resetHotkeys(): void {\n entries.length = 0\n activeScopes.set(new Set([\"global\"]))\n}\n","import { onUnmount } from \"@pyreon/core\"\nimport { registerHotkey } from \"./registry\"\nimport type { HotkeyOptions } from \"./types\"\n\n/**\n * Register a keyboard shortcut scoped to a component's lifecycle.\n * Automatically unregisters when the component unmounts.\n *\n * @example\n * ```ts\n * function Editor() {\n * useHotkey('ctrl+s', () => save(), { description: 'Save document' })\n * useHotkey('ctrl+z', () => undo())\n * useHotkey('ctrl+shift+z', () => redo())\n * // ...\n * }\n * ```\n */\nexport function useHotkey(\n shortcut: string,\n handler: (event: KeyboardEvent) => void,\n options?: HotkeyOptions,\n): void {\n const unregister = registerHotkey(shortcut, handler, options)\n onUnmount(unregister)\n}\n","import { onUnmount } from \"@pyreon/core\"\nimport { disableScope, enableScope } from \"./registry\"\n\n/**\n * Activate a hotkey scope for the lifetime of a component.\n * When the component unmounts, the scope is deactivated.\n *\n * @example\n * ```tsx\n * function Modal() {\n * useHotkeyScope('modal')\n * useHotkey('escape', () => closeModal(), { scope: 'modal' })\n * // ...\n * }\n * ```\n */\nexport function useHotkeyScope(scope: string): void {\n enableScope(scope)\n onUnmount(() => disableScope(scope))\n}\n"],"mappings":";;;;AAIA,MAAM,cAAsC;CAC1C,KAAK;CACL,QAAQ;CACR,KAAK;CACL,KAAK;CACL,OAAO;CACP,UAAU;CACV,IAAI;CACJ,MAAM;CACN,MAAM;CACN,OAAO;CACP,MAAM;CACP;;;;;AAMD,SAAgB,cAAc,UAA4B;CACxD,MAAM,QAAQ,SAAS,aAAa,CAAC,MAAM,CAAC,MAAM,IAAI;CACtD,MAAM,QAAkB;EACtB,MAAM;EACN,OAAO;EACP,KAAK;EACL,MAAM;EACN,KAAK;EACN;AAED,MAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,IAAI,KAAK,MAAM;AACrB,MAAI,MAAM,UAAU,MAAM,UACxB,OAAM,OAAO;WACJ,MAAM,QACf,OAAM,QAAQ;WACL,MAAM,MACf,OAAM,MAAM;WACH,MAAM,UAAU,MAAM,SAAS,MAAM,UAC9C,OAAM,OAAO;WACJ,MAAM,MAEf,KAAI,OAAO,CACT,OAAM,OAAO;MAEb,OAAM,OAAO;MAGf,OAAM,MAAM,YAAY,MAAM;;AAIlC,QAAO;;;;;AAMT,SAAgB,aAAa,OAAsB,OAA0B;AAC3E,KAAI,MAAM,YAAY,MAAM,KAAM,QAAO;AACzC,KAAI,MAAM,aAAa,MAAM,MAAO,QAAO;AAC3C,KAAI,MAAM,WAAW,MAAM,IAAK,QAAO;AACvC,KAAI,MAAM,YAAY,MAAM,KAAM,QAAO;AAGzC,QADiB,MAAM,IAAI,aAAa,KACpB,MAAM;;;;;AAM5B,SAAgB,YAAY,OAAyB;CACnD,MAAM,QAAkB,EAAE;AAC1B,KAAI,MAAM,KAAM,OAAM,KAAK,OAAO;AAClC,KAAI,MAAM,MAAO,OAAM,KAAK,QAAQ;AACpC,KAAI,MAAM,IAAK,OAAM,KAAK,MAAM;AAChC,KAAI,MAAM,KAAM,OAAM,KAAK,OAAO,GAAG,MAAM,OAAO;AAClD,OAAM,KAAK,MAAM,IAAI,WAAW,IAAI,MAAM,IAAI,aAAa,GAAG,WAAW,MAAM,IAAI,CAAC;AACpF,QAAO,MAAM,KAAK,IAAI;;AAGxB,SAAS,WAAW,GAAmB;AACrC,QAAO,EAAE,OAAO,EAAE,CAAC,aAAa,GAAG,EAAE,MAAM,EAAE;;AAG/C,SAAS,QAAiB;AACxB,KAAI,OAAO,cAAc,YAAa,QAAO;AAC7C,QAAO,wBAAwB,KAAK,UAAU,UAAU;;;;;AClF1D,MAAM,UAAyB,EAAE;AACjC,MAAM,eAAe,OAAoB,IAAI,IAAI,CAAC,SAAS,CAAC,CAAC;AAC7D,IAAI,mBAAmB;AAIvB,MAAM,aAAa,IAAI,IAAI;CAAC;CAAS;CAAY;CAAS,CAAC;AAE3D,SAAS,eAAe,OAA+B;CACrD,MAAM,SAAS,MAAM;AACrB,KAAI,CAAC,OAAQ,QAAO;AACpB,KAAI,WAAW,IAAI,OAAO,QAAQ,CAAE,QAAO;AAC3C,KAAI,OAAO,kBAAmB,QAAO;AACrC,QAAO;;AAKT,SAAS,iBAAuB;AAC9B,KAAI,iBAAkB;AACtB,KAAI,OAAO,WAAW,YAAa;AACnC,oBAAmB;AAEnB,QAAO,iBAAiB,YAAY,UAAU;EAC5C,MAAM,SAAS,aAAa,MAAM;AAElC,OAAK,MAAM,SAAS,SAAS;AAE3B,OAAI,CAAC,OAAO,IAAI,MAAM,QAAQ,MAAM,CAAE;AAOtC,OAAI,EAHF,OAAO,MAAM,QAAQ,YAAY,aAC7B,MAAM,QAAQ,SAAS,GACvB,MAAM,QAAQ,SACN;AAGd,OAAI,CAAC,MAAM,QAAQ,kBAAkB,eAAe,MAAM,CAAE;AAG5D,OAAI,CAAC,aAAa,OAAO,MAAM,MAAM,CAAE;AAGvC,OAAI,MAAM,QAAQ,eAAgB,OAAM,gBAAgB;AACxD,OAAI,MAAM,QAAQ,gBAAiB,OAAM,iBAAiB;AAC1D,SAAM,QAAQ,MAAM;;GAEtB;;;;;;;;;;;AAcJ,SAAgB,eACd,UACA,SACA,SACY;AACZ,iBAAgB;CAEhB,MAAM,QAAqB;EACzB;EACA,OAAO,cAAc,SAAS;EAC9B;EACA,SAAS;GACP,OAAO,SAAS,SAAS;GACzB,gBAAgB,SAAS,mBAAmB;GAC5C,iBAAiB,SAAS,oBAAoB;GAC9C,gBAAgB,SAAS,mBAAmB;GAC5C,SAAS,SAAS,WAAW;GAC7B,GAAI,SAAS,eAAe,OAAO,EAAE,aAAa,QAAQ,aAAa,GAAG,EAAE;GAC7E;EACF;AAED,SAAQ,KAAK,MAAM;AAEnB,cAAa;EACX,MAAM,MAAM,QAAQ,QAAQ,MAAM;AAClC,MAAI,QAAQ,GAAI,SAAQ,OAAO,KAAK,EAAE;;;;;;AAS1C,SAAgB,YAAY,OAAqB;CAC/C,MAAM,UAAU,aAAa,MAAM;AACnC,KAAI,QAAQ,IAAI,MAAM,CAAE;CACxB,MAAM,OAAO,IAAI,IAAI,QAAQ;AAC7B,MAAK,IAAI,MAAM;AACf,cAAa,IAAI,KAAK;;;;;AAMxB,SAAgB,aAAa,OAAqB;AAChD,KAAI,UAAU,SAAU;CACxB,MAAM,UAAU,aAAa,MAAM;AACnC,KAAI,CAAC,QAAQ,IAAI,MAAM,CAAE;CACzB,MAAM,OAAO,IAAI,IAAI,QAAQ;AAC7B,MAAK,OAAO,MAAM;AAClB,cAAa,IAAI,KAAK;;;;;AAMxB,SAAgB,kBAAuC;AACrD,QAAO;;;;;AAMT,SAAgB,uBAIb;AACD,QAAO,QAAQ,KAAK,OAAO;EACzB,UAAU,EAAE;EACZ,OAAO,EAAE,QAAQ;EACjB,GAAI,EAAE,QAAQ,eAAe,OAAO,EAAE,aAAa,EAAE,QAAQ,aAAa,GAAG,EAAE;EAChF,EAAE;;AAKL,SAAgB,gBAAsB;AACpC,SAAQ,SAAS;AACjB,cAAa,IAAI,IAAI,IAAI,CAAC,SAAS,CAAC,CAAC;;;;;;;;;;;;;;;;;;;ACnIvC,SAAgB,UACd,UACA,SACA,SACM;AAEN,WADmB,eAAe,UAAU,SAAS,QAAQ,CACxC;;;;;;;;;;;;;;;;;;ACRvB,SAAgB,eAAe,OAAqB;AAClD,aAAY,MAAM;AAClB,iBAAgB,aAAa,MAAM,CAAC"}
@@ -40,7 +40,7 @@ interface HotkeyEntry {
40
40
  /** The callback to invoke */
41
41
  handler: (event: KeyboardEvent) => void;
42
42
  /** Options */
43
- options: Required<Pick<HotkeyOptions, 'scope' | 'preventDefault' | 'stopPropagation' | 'enableOnInputs' | 'enabled'>> & {
43
+ options: Required<Pick<HotkeyOptions, "scope" | "preventDefault" | "stopPropagation" | "enableOnInputs" | "enabled">> & {
44
44
  description?: string;
45
45
  };
46
46
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index2.d.ts","names":[],"sources":["../../../src/types.ts","../../../src/use-hotkey.ts","../../../src/use-hotkey-scope.ts","../../../src/registry.ts","../../../src/parse.ts"],"mappings":";;;;;;AAMA;UAAiB,QAAA;EACf,IAAA;EACA,KAAA;EACA,GAAA;EACA,IAAA;EACA,GAAA;AAAA;;;;UAMe,aAAA;EAAa;EAE5B,KAAA;EAF4B;EAI5B,cAAA;EAAA;EAEA,eAAA;EAEA;EAAA,cAAA;EAIA;EAFA,WAAA;EAEO;EAAP,OAAA;AAAA;;;;UAMe,WAAA;EASb;EAPF,QAAA;EAMiB;EAJjB,KAAA,EAAO,QAAA;EAFP;EAIA,OAAA,GAAU,KAAA,EAAO,aAAA;EAFV;EAIP,OAAA,EAAS,QAAA,CACP,IAAA,CACE,aAAA;IAOE,WAAA;EAAA;AAAA;;;;;AA9CR;;;;;;;;;;;AAWA;iBCCgB,SAAA,CACd,QAAA,UACA,OAAA,GAAU,KAAA,EAAO,aAAA,WACjB,OAAA,GAAU,aAAA;;;;;;ADfZ;;;;;;;;;;iBEUgB,cAAA,CAAe,KAAA;;;;AFV/B;;;;;;;;iBG+DgB,cAAA,CACd,QAAA,UACA,OAAA,GAAU,KAAA,EAAO,aAAA,WACjB,OAAA,GAAU,aAAA;;;AHvDZ;iBGwFgB,WAAA,CAAY,KAAA;;;;iBAWZ,YAAA,CAAa,KAAA;;;;iBAYb,eAAA,CAAA,GAAmB,MAAA,CAAO,GAAA;;;AH7F1C;iBGoGgB,oBAAA,CAAA,GAAwB,aAAA;EACtC,QAAA;EACA,KAAA;EACA,WAAA;AAAA;AAAA,iBAac,aAAA,CAAA;;;;;AHjJhB;;iBIgBgB,aAAA,CAAc,QAAA,WAAmB,QAAA;;;;iBAsCjC,YAAA,CAAa,KAAA,EAAO,aAAA,EAAe,KAAA,EAAO,QAAA;;;;iBAa1C,WAAA,CAAY,KAAA,EAAO,QAAA"}
1
+ {"version":3,"file":"index2.d.ts","names":[],"sources":["../../../src/types.ts","../../../src/use-hotkey.ts","../../../src/use-hotkey-scope.ts","../../../src/registry.ts","../../../src/parse.ts"],"mappings":";;;;;;AAMA;UAAiB,QAAA;EACf,IAAA;EACA,KAAA;EACA,GAAA;EACA,IAAA;EACA,GAAA;AAAA;;;;UAMe,aAAA;EAAa;EAE5B,KAAA;EAF4B;EAI5B,cAAA;EAAA;EAEA,eAAA;EAEA;EAAA,cAAA;EAIA;EAFA,WAAA;EAEO;EAAP,OAAA;AAAA;;;;UAMe,WAAA;EASb;EAPF,QAAA;EAMiB;EAJjB,KAAA,EAAO,QAAA;EAFP;EAIA,OAAA,GAAU,KAAA,EAAO,aAAA;EAFV;EAIP,OAAA,EAAS,QAAA,CACP,IAAA,CACE,aAAA;IAGE,WAAA;EAAA;AAAA;;;;;AA1CR;;;;;;;;;;;AAWA;iBCCgB,SAAA,CACd,QAAA,UACA,OAAA,GAAU,KAAA,EAAO,aAAA,WACjB,OAAA,GAAU,aAAA;;;;;;ADfZ;;;;;;;;;;iBEUgB,cAAA,CAAe,KAAA;;;;AFV/B;;;;;;;;iBG+DgB,cAAA,CACd,QAAA,UACA,OAAA,GAAU,KAAA,EAAO,aAAA,WACjB,OAAA,GAAU,aAAA;;;AHvDZ;iBGsFgB,WAAA,CAAY,KAAA;;;;iBAWZ,YAAA,CAAa,KAAA;;;;iBAYb,eAAA,CAAA,GAAmB,MAAA,CAAO,GAAA;;;AH3F1C;iBGkGgB,oBAAA,CAAA,GAAwB,aAAA;EACtC,QAAA;EACA,KAAA;EACA,WAAA;AAAA;AAAA,iBAWc,aAAA,CAAA;;;;;AH7IhB;;iBIgBgB,aAAA,CAAc,QAAA,WAAmB,QAAA;;;;iBAsCjC,YAAA,CAAa,KAAA,EAAO,aAAA,EAAe,KAAA,EAAO,QAAA;;;;iBAa1C,WAAA,CAAY,KAAA,EAAO,QAAA"}
package/package.json CHANGED
@@ -1,16 +1,16 @@
1
1
  {
2
2
  "name": "@pyreon/hotkeys",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Reactive keyboard shortcut management for Pyreon — scope-aware, conflict detection",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
8
- "url": "https://github.com/pyreon/fundamentals.git",
9
- "directory": "packages/hotkeys"
8
+ "url": "https://github.com/pyreon/pyreon.git",
9
+ "directory": "packages/fundamentals/hotkeys"
10
10
  },
11
11
  "homepage": "https://github.com/pyreon/fundamentals/tree/main/packages/hotkeys#readme",
12
12
  "bugs": {
13
- "url": "https://github.com/pyreon/fundamentals/issues"
13
+ "url": "https://github.com/pyreon/pyreon/issues"
14
14
  },
15
15
  "publishConfig": {
16
16
  "access": "public"
@@ -37,10 +37,17 @@
37
37
  "build": "vl_rolldown_build",
38
38
  "dev": "vl_rolldown_build-watch",
39
39
  "test": "vitest run",
40
- "typecheck": "tsc --noEmit"
40
+ "typecheck": "tsc --noEmit",
41
+ "lint": "biome check ."
41
42
  },
42
43
  "peerDependencies": {
43
- "@pyreon/core": ">=0.7.0 <0.8.0",
44
- "@pyreon/reactivity": ">=0.7.0 <0.8.0"
44
+ "@pyreon/core": "^0.11.0",
45
+ "@pyreon/reactivity": "^0.11.0"
46
+ },
47
+ "devDependencies": {
48
+ "@happy-dom/global-registrator": "^20.8.3",
49
+ "@pyreon/core": "^0.11.0",
50
+ "@pyreon/reactivity": "^0.11.0",
51
+ "@vitus-labs/tools-lint": "^1.11.0"
45
52
  }
46
53
  }
package/src/index.ts CHANGED
@@ -23,8 +23,8 @@
23
23
 
24
24
  // ─── Hooks ───────────────────────────────────────────────────────────────────
25
25
 
26
- export { useHotkey } from './use-hotkey'
27
- export { useHotkeyScope } from './use-hotkey-scope'
26
+ export { useHotkey } from "./use-hotkey"
27
+ export { useHotkeyScope } from "./use-hotkey-scope"
28
28
 
29
29
  // ─── Imperative API ──────────────────────────────────────────────────────────
30
30
 
@@ -34,16 +34,16 @@ export {
34
34
  getActiveScopes,
35
35
  getRegisteredHotkeys,
36
36
  registerHotkey,
37
- } from './registry'
37
+ } from "./registry"
38
38
 
39
39
  // ─── Utilities ───────────────────────────────────────────────────────────────
40
40
 
41
- export { formatCombo, matchesCombo, parseShortcut } from './parse'
41
+ export { formatCombo, matchesCombo, parseShortcut } from "./parse"
42
42
 
43
43
  // ─── Types ───────────────────────────────────────────────────────────────────
44
44
 
45
- export type { HotkeyEntry, HotkeyOptions, KeyCombo } from './types'
45
+ export type { HotkeyEntry, HotkeyOptions, KeyCombo } from "./types"
46
46
 
47
47
  // ─── Testing ─────────────────────────────────────────────────────────────────
48
48
 
49
- export { _resetHotkeys } from './registry'
49
+ export { _resetHotkeys } from "./registry"
package/src/parse.ts CHANGED
@@ -1,19 +1,19 @@
1
- import type { KeyCombo } from './types'
1
+ import type { KeyCombo } from "./types"
2
2
 
3
3
  // ─── Key aliases ─────────────────────────────────────────────────────────────
4
4
 
5
5
  const KEY_ALIASES: Record<string, string> = {
6
- esc: 'escape',
7
- return: 'enter',
8
- del: 'delete',
9
- ins: 'insert',
10
- space: ' ',
11
- spacebar: ' ',
12
- up: 'arrowup',
13
- down: 'arrowdown',
14
- left: 'arrowleft',
15
- right: 'arrowright',
16
- plus: '+',
6
+ esc: "escape",
7
+ return: "enter",
8
+ del: "delete",
9
+ ins: "insert",
10
+ space: " ",
11
+ spacebar: " ",
12
+ up: "arrowup",
13
+ down: "arrowdown",
14
+ left: "arrowleft",
15
+ right: "arrowright",
16
+ plus: "+",
17
17
  }
18
18
 
19
19
  /**
@@ -21,26 +21,26 @@ const KEY_ALIASES: Record<string, string> = {
21
21
  * Supports aliases (esc, del, space, etc.) and mod (ctrl on Windows/Linux, meta on Mac).
22
22
  */
23
23
  export function parseShortcut(shortcut: string): KeyCombo {
24
- const parts = shortcut.toLowerCase().trim().split('+')
24
+ const parts = shortcut.toLowerCase().trim().split("+")
25
25
  const combo: KeyCombo = {
26
26
  ctrl: false,
27
27
  shift: false,
28
28
  alt: false,
29
29
  meta: false,
30
- key: '',
30
+ key: "",
31
31
  }
32
32
 
33
33
  for (const part of parts) {
34
34
  const p = part.trim()
35
- if (p === 'ctrl' || p === 'control') {
35
+ if (p === "ctrl" || p === "control") {
36
36
  combo.ctrl = true
37
- } else if (p === 'shift') {
37
+ } else if (p === "shift") {
38
38
  combo.shift = true
39
- } else if (p === 'alt') {
39
+ } else if (p === "alt") {
40
40
  combo.alt = true
41
- } else if (p === 'meta' || p === 'cmd' || p === 'command') {
41
+ } else if (p === "meta" || p === "cmd" || p === "command") {
42
42
  combo.meta = true
43
- } else if (p === 'mod') {
43
+ } else if (p === "mod") {
44
44
  // mod = meta on Mac, ctrl elsewhere
45
45
  if (isMac()) {
46
46
  combo.meta = true
@@ -73,14 +73,12 @@ export function matchesCombo(event: KeyboardEvent, combo: KeyCombo): boolean {
73
73
  */
74
74
  export function formatCombo(combo: KeyCombo): string {
75
75
  const parts: string[] = []
76
- if (combo.ctrl) parts.push('Ctrl')
77
- if (combo.shift) parts.push('Shift')
78
- if (combo.alt) parts.push('Alt')
79
- if (combo.meta) parts.push(isMac() ? '' : 'Meta')
80
- parts.push(
81
- combo.key.length === 1 ? combo.key.toUpperCase() : capitalize(combo.key),
82
- )
83
- return parts.join('+')
76
+ if (combo.ctrl) parts.push("Ctrl")
77
+ if (combo.shift) parts.push("Shift")
78
+ if (combo.alt) parts.push("Alt")
79
+ if (combo.meta) parts.push(isMac() ? "" : "Meta")
80
+ parts.push(combo.key.length === 1 ? combo.key.toUpperCase() : capitalize(combo.key))
81
+ return parts.join("+")
84
82
  }
85
83
 
86
84
  function capitalize(s: string): string {
@@ -88,6 +86,6 @@ function capitalize(s: string): string {
88
86
  }
89
87
 
90
88
  function isMac(): boolean {
91
- if (typeof navigator === 'undefined') return false
89
+ if (typeof navigator === "undefined") return false
92
90
  return /mac|iphone|ipad|ipod/i.test(navigator.userAgent)
93
91
  }
package/src/registry.ts CHANGED
@@ -1,17 +1,17 @@
1
- import type { Signal } from '@pyreon/reactivity'
2
- import { signal } from '@pyreon/reactivity'
3
- import { matchesCombo, parseShortcut } from './parse'
4
- import type { HotkeyEntry, HotkeyOptions } from './types'
1
+ import type { Signal } from "@pyreon/reactivity"
2
+ import { signal } from "@pyreon/reactivity"
3
+ import { matchesCombo, parseShortcut } from "./parse"
4
+ import type { HotkeyEntry, HotkeyOptions } from "./types"
5
5
 
6
6
  // ─── State ───────────────────────────────────────────────────────────────────
7
7
 
8
8
  const entries: HotkeyEntry[] = []
9
- const activeScopes = signal<Set<string>>(new Set(['global']))
9
+ const activeScopes = signal<Set<string>>(new Set(["global"]))
10
10
  let listenerAttached = false
11
11
 
12
12
  // ─── Input detection ─────────────────────────────────────────────────────────
13
13
 
14
- const INPUT_TAGS = new Set(['INPUT', 'TEXTAREA', 'SELECT'])
14
+ const INPUT_TAGS = new Set(["INPUT", "TEXTAREA", "SELECT"])
15
15
 
16
16
  function isInputFocused(event: KeyboardEvent): boolean {
17
17
  const target = event.target as HTMLElement | null
@@ -25,10 +25,10 @@ function isInputFocused(event: KeyboardEvent): boolean {
25
25
 
26
26
  function attachListener(): void {
27
27
  if (listenerAttached) return
28
- if (typeof window === 'undefined') return
28
+ if (typeof window === "undefined") return
29
29
  listenerAttached = true
30
30
 
31
- window.addEventListener('keydown', (event) => {
31
+ window.addEventListener("keydown", (event) => {
32
32
  const scopes = activeScopes.peek()
33
33
 
34
34
  for (const entry of entries) {
@@ -37,7 +37,7 @@ function attachListener(): void {
37
37
 
38
38
  // Check enabled
39
39
  const enabled =
40
- typeof entry.options.enabled === 'function'
40
+ typeof entry.options.enabled === "function"
41
41
  ? entry.options.enabled()
42
42
  : entry.options.enabled
43
43
  if (!enabled) continue
@@ -79,14 +79,12 @@ export function registerHotkey(
79
79
  combo: parseShortcut(shortcut),
80
80
  handler,
81
81
  options: {
82
- scope: options?.scope ?? 'global',
82
+ scope: options?.scope ?? "global",
83
83
  preventDefault: options?.preventDefault !== false,
84
84
  stopPropagation: options?.stopPropagation === true,
85
85
  enableOnInputs: options?.enableOnInputs === true,
86
86
  enabled: options?.enabled ?? true,
87
- ...(options?.description != null
88
- ? { description: options.description }
89
- : {}),
87
+ ...(options?.description != null ? { description: options.description } : {}),
90
88
  },
91
89
  }
92
90
 
@@ -115,7 +113,7 @@ export function enableScope(scope: string): void {
115
113
  * Deactivate a hotkey scope. Cannot deactivate 'global'.
116
114
  */
117
115
  export function disableScope(scope: string): void {
118
- if (scope === 'global') return
116
+ if (scope === "global") return
119
117
  const current = activeScopes.peek()
120
118
  if (!current.has(scope)) return
121
119
  const next = new Set(current)
@@ -141,9 +139,7 @@ export function getRegisteredHotkeys(): ReadonlyArray<{
141
139
  return entries.map((e) => ({
142
140
  shortcut: e.shortcut,
143
141
  scope: e.options.scope,
144
- ...(e.options.description != null
145
- ? { description: e.options.description }
146
- : {}),
142
+ ...(e.options.description != null ? { description: e.options.description } : {}),
147
143
  }))
148
144
  }
149
145
 
@@ -151,5 +147,5 @@ export function getRegisteredHotkeys(): ReadonlyArray<{
151
147
 
152
148
  export function _resetHotkeys(): void {
153
149
  entries.length = 0
154
- activeScopes.set(new Set(['global']))
150
+ activeScopes.set(new Set(["global"]))
155
151
  }
@@ -1,4 +1,4 @@
1
- import { afterEach, beforeEach, describe, expect, it } from 'vitest'
1
+ import { afterEach, beforeEach, describe, expect, it } from "vitest"
2
2
  import {
3
3
  _resetHotkeys,
4
4
  disableScope,
@@ -9,7 +9,7 @@ import {
9
9
  matchesCombo,
10
10
  parseShortcut,
11
11
  registerHotkey,
12
- } from '../index'
12
+ } from "../index"
13
13
 
14
14
  // ─── Helpers ─────────────────────────────────────────────────────────────────
15
15
 
@@ -23,7 +23,7 @@ function fireKey(
23
23
  }> = {},
24
24
  target?: HTMLElement,
25
25
  ): KeyboardEvent {
26
- const event = new KeyboardEvent('keydown', {
26
+ const event = new KeyboardEvent("keydown", {
27
27
  key,
28
28
  ctrlKey: modifiers.ctrlKey ?? false,
29
29
  shiftKey: modifiers.shiftKey ?? false,
@@ -38,117 +38,117 @@ function fireKey(
38
38
 
39
39
  // ─── Tests ───────────────────────────────────────────────────────────────────
40
40
 
41
- describe('parseShortcut', () => {
42
- it('parses simple key', () => {
43
- const combo = parseShortcut('a')
41
+ describe("parseShortcut", () => {
42
+ it("parses simple key", () => {
43
+ const combo = parseShortcut("a")
44
44
  expect(combo).toEqual({
45
45
  ctrl: false,
46
46
  shift: false,
47
47
  alt: false,
48
48
  meta: false,
49
- key: 'a',
49
+ key: "a",
50
50
  })
51
51
  })
52
52
 
53
- it('parses ctrl+key', () => {
54
- const combo = parseShortcut('ctrl+s')
53
+ it("parses ctrl+key", () => {
54
+ const combo = parseShortcut("ctrl+s")
55
55
  expect(combo.ctrl).toBe(true)
56
- expect(combo.key).toBe('s')
56
+ expect(combo.key).toBe("s")
57
57
  })
58
58
 
59
- it('parses multiple modifiers', () => {
60
- const combo = parseShortcut('ctrl+shift+alt+k')
59
+ it("parses multiple modifiers", () => {
60
+ const combo = parseShortcut("ctrl+shift+alt+k")
61
61
  expect(combo.ctrl).toBe(true)
62
62
  expect(combo.shift).toBe(true)
63
63
  expect(combo.alt).toBe(true)
64
- expect(combo.key).toBe('k')
64
+ expect(combo.key).toBe("k")
65
65
  })
66
66
 
67
- it('parses meta/cmd/command as meta', () => {
68
- expect(parseShortcut('meta+k').meta).toBe(true)
69
- expect(parseShortcut('cmd+k').meta).toBe(true)
70
- expect(parseShortcut('command+k').meta).toBe(true)
67
+ it("parses meta/cmd/command as meta", () => {
68
+ expect(parseShortcut("meta+k").meta).toBe(true)
69
+ expect(parseShortcut("cmd+k").meta).toBe(true)
70
+ expect(parseShortcut("command+k").meta).toBe(true)
71
71
  })
72
72
 
73
- it('handles aliases', () => {
74
- expect(parseShortcut('esc').key).toBe('escape')
75
- expect(parseShortcut('return').key).toBe('enter')
76
- expect(parseShortcut('del').key).toBe('delete')
77
- expect(parseShortcut('space').key).toBe(' ')
73
+ it("handles aliases", () => {
74
+ expect(parseShortcut("esc").key).toBe("escape")
75
+ expect(parseShortcut("return").key).toBe("enter")
76
+ expect(parseShortcut("del").key).toBe("delete")
77
+ expect(parseShortcut("space").key).toBe(" ")
78
78
  })
79
79
 
80
- it('parses mod as ctrl on non-Mac', () => {
80
+ it("parses mod as ctrl on non-Mac", () => {
81
81
  // happy-dom doesn't simulate Mac, so mod should resolve to ctrl
82
- const combo = parseShortcut('mod+k')
82
+ const combo = parseShortcut("mod+k")
83
83
  expect(combo.ctrl || combo.meta).toBe(true)
84
- expect(combo.key).toBe('k')
84
+ expect(combo.key).toBe("k")
85
85
  })
86
86
 
87
- it('parses control as ctrl', () => {
88
- expect(parseShortcut('control+s').ctrl).toBe(true)
87
+ it("parses control as ctrl", () => {
88
+ expect(parseShortcut("control+s").ctrl).toBe(true)
89
89
  })
90
90
 
91
- it('is case-insensitive', () => {
92
- const combo = parseShortcut('Ctrl+Shift+S')
91
+ it("is case-insensitive", () => {
92
+ const combo = parseShortcut("Ctrl+Shift+S")
93
93
  expect(combo.ctrl).toBe(true)
94
94
  expect(combo.shift).toBe(true)
95
- expect(combo.key).toBe('s')
95
+ expect(combo.key).toBe("s")
96
96
  })
97
97
  })
98
98
 
99
- describe('matchesCombo', () => {
100
- it('matches simple key', () => {
101
- const combo = parseShortcut('a')
102
- const event = new KeyboardEvent('keydown', { key: 'a' })
99
+ describe("matchesCombo", () => {
100
+ it("matches simple key", () => {
101
+ const combo = parseShortcut("a")
102
+ const event = new KeyboardEvent("keydown", { key: "a" })
103
103
  expect(matchesCombo(event, combo)).toBe(true)
104
104
  })
105
105
 
106
- it('matches with modifiers', () => {
107
- const combo = parseShortcut('ctrl+s')
108
- const event = new KeyboardEvent('keydown', { key: 's', ctrlKey: true })
106
+ it("matches with modifiers", () => {
107
+ const combo = parseShortcut("ctrl+s")
108
+ const event = new KeyboardEvent("keydown", { key: "s", ctrlKey: true })
109
109
  expect(matchesCombo(event, combo)).toBe(true)
110
110
  })
111
111
 
112
- it('does not match when modifier is missing', () => {
113
- const combo = parseShortcut('ctrl+s')
114
- const event = new KeyboardEvent('keydown', { key: 's' })
112
+ it("does not match when modifier is missing", () => {
113
+ const combo = parseShortcut("ctrl+s")
114
+ const event = new KeyboardEvent("keydown", { key: "s" })
115
115
  expect(matchesCombo(event, combo)).toBe(false)
116
116
  })
117
117
 
118
- it('does not match when extra modifier is present', () => {
119
- const combo = parseShortcut('ctrl+s')
120
- const event = new KeyboardEvent('keydown', {
121
- key: 's',
118
+ it("does not match when extra modifier is present", () => {
119
+ const combo = parseShortcut("ctrl+s")
120
+ const event = new KeyboardEvent("keydown", {
121
+ key: "s",
122
122
  ctrlKey: true,
123
123
  shiftKey: true,
124
124
  })
125
125
  expect(matchesCombo(event, combo)).toBe(false)
126
126
  })
127
127
 
128
- it('does not match wrong key', () => {
129
- const combo = parseShortcut('ctrl+s')
130
- const event = new KeyboardEvent('keydown', { key: 'a', ctrlKey: true })
128
+ it("does not match wrong key", () => {
129
+ const combo = parseShortcut("ctrl+s")
130
+ const event = new KeyboardEvent("keydown", { key: "a", ctrlKey: true })
131
131
  expect(matchesCombo(event, combo)).toBe(false)
132
132
  })
133
133
  })
134
134
 
135
- describe('formatCombo', () => {
136
- it('formats simple key', () => {
137
- expect(formatCombo(parseShortcut('a'))).toBe('A')
135
+ describe("formatCombo", () => {
136
+ it("formats simple key", () => {
137
+ expect(formatCombo(parseShortcut("a"))).toBe("A")
138
138
  })
139
139
 
140
- it('formats with modifiers', () => {
141
- const result = formatCombo(parseShortcut('ctrl+shift+s'))
142
- expect(result).toBe('Ctrl+Shift+S')
140
+ it("formats with modifiers", () => {
141
+ const result = formatCombo(parseShortcut("ctrl+shift+s"))
142
+ expect(result).toBe("Ctrl+Shift+S")
143
143
  })
144
144
 
145
- it('capitalizes special keys', () => {
146
- expect(formatCombo(parseShortcut('escape'))).toBe('Escape')
147
- expect(formatCombo(parseShortcut('enter'))).toBe('Enter')
145
+ it("capitalizes special keys", () => {
146
+ expect(formatCombo(parseShortcut("escape"))).toBe("Escape")
147
+ expect(formatCombo(parseShortcut("enter"))).toBe("Enter")
148
148
  })
149
149
  })
150
150
 
151
- describe('registerHotkey', () => {
151
+ describe("registerHotkey", () => {
152
152
  beforeEach(() => {
153
153
  _resetHotkeys()
154
154
  })
@@ -157,184 +157,184 @@ describe('registerHotkey', () => {
157
157
  _resetHotkeys()
158
158
  })
159
159
 
160
- it('fires handler on matching keydown', () => {
160
+ it("fires handler on matching keydown", () => {
161
161
  let fired = false
162
- registerHotkey('ctrl+s', () => {
162
+ registerHotkey("ctrl+s", () => {
163
163
  fired = true
164
164
  })
165
- fireKey('s', { ctrlKey: true })
165
+ fireKey("s", { ctrlKey: true })
166
166
  expect(fired).toBe(true)
167
167
  })
168
168
 
169
- it('does not fire on non-matching key', () => {
169
+ it("does not fire on non-matching key", () => {
170
170
  let fired = false
171
- registerHotkey('ctrl+s', () => {
171
+ registerHotkey("ctrl+s", () => {
172
172
  fired = true
173
173
  })
174
- fireKey('a', { ctrlKey: true })
174
+ fireKey("a", { ctrlKey: true })
175
175
  expect(fired).toBe(false)
176
176
  })
177
177
 
178
- it('does not fire without required modifier', () => {
178
+ it("does not fire without required modifier", () => {
179
179
  let fired = false
180
- registerHotkey('ctrl+s', () => {
180
+ registerHotkey("ctrl+s", () => {
181
181
  fired = true
182
182
  })
183
- fireKey('s')
183
+ fireKey("s")
184
184
  expect(fired).toBe(false)
185
185
  })
186
186
 
187
- it('passes the event to the handler', () => {
187
+ it("passes the event to the handler", () => {
188
188
  let receivedEvent: KeyboardEvent | null = null
189
- registerHotkey('ctrl+s', (e) => {
189
+ registerHotkey("ctrl+s", (e) => {
190
190
  receivedEvent = e
191
191
  })
192
- fireKey('s', { ctrlKey: true })
192
+ fireKey("s", { ctrlKey: true })
193
193
  expect(receivedEvent).not.toBeNull()
194
- expect(receivedEvent!.key).toBe('s')
194
+ expect(receivedEvent!.key).toBe("s")
195
195
  })
196
196
 
197
- it('preventDefault is true by default', () => {
198
- registerHotkey('ctrl+s', () => {
197
+ it("preventDefault is true by default", () => {
198
+ registerHotkey("ctrl+s", () => {
199
199
  // handler
200
200
  })
201
- const event = fireKey('s', { ctrlKey: true })
201
+ const event = fireKey("s", { ctrlKey: true })
202
202
  expect(event.defaultPrevented).toBe(true)
203
203
  })
204
204
 
205
- it('preventDefault can be disabled', () => {
205
+ it("preventDefault can be disabled", () => {
206
206
  registerHotkey(
207
- 'ctrl+s',
207
+ "ctrl+s",
208
208
  () => {
209
209
  // handler
210
210
  },
211
211
  { preventDefault: false },
212
212
  )
213
- const event = fireKey('s', { ctrlKey: true })
213
+ const event = fireKey("s", { ctrlKey: true })
214
214
  expect(event.defaultPrevented).toBe(false)
215
215
  })
216
216
 
217
- it('unregister function removes the hotkey', () => {
217
+ it("unregister function removes the hotkey", () => {
218
218
  let count = 0
219
- const unregister = registerHotkey('ctrl+s', () => {
219
+ const unregister = registerHotkey("ctrl+s", () => {
220
220
  count++
221
221
  })
222
- fireKey('s', { ctrlKey: true })
222
+ fireKey("s", { ctrlKey: true })
223
223
  expect(count).toBe(1)
224
224
 
225
225
  unregister()
226
- fireKey('s', { ctrlKey: true })
226
+ fireKey("s", { ctrlKey: true })
227
227
  expect(count).toBe(1)
228
228
  })
229
229
 
230
- it('does not fire in input elements by default', () => {
230
+ it("does not fire in input elements by default", () => {
231
231
  let fired = false
232
- registerHotkey('ctrl+s', () => {
232
+ registerHotkey("ctrl+s", () => {
233
233
  fired = true
234
234
  })
235
235
 
236
- const input = document.createElement('input')
236
+ const input = document.createElement("input")
237
237
  document.body.appendChild(input)
238
- fireKey('s', { ctrlKey: true }, input)
238
+ fireKey("s", { ctrlKey: true }, input)
239
239
  input.remove()
240
240
 
241
241
  expect(fired).toBe(false)
242
242
  })
243
243
 
244
- it('fires in input elements when enableOnInputs is true', () => {
244
+ it("fires in input elements when enableOnInputs is true", () => {
245
245
  let fired = false
246
246
  registerHotkey(
247
- 'ctrl+s',
247
+ "ctrl+s",
248
248
  () => {
249
249
  fired = true
250
250
  },
251
251
  { enableOnInputs: true },
252
252
  )
253
253
 
254
- const input = document.createElement('input')
254
+ const input = document.createElement("input")
255
255
  document.body.appendChild(input)
256
- fireKey('s', { ctrlKey: true }, input)
256
+ fireKey("s", { ctrlKey: true }, input)
257
257
  input.remove()
258
258
 
259
259
  expect(fired).toBe(true)
260
260
  })
261
261
 
262
- it('does not fire in textarea by default', () => {
262
+ it("does not fire in textarea by default", () => {
263
263
  let fired = false
264
- registerHotkey('ctrl+s', () => {
264
+ registerHotkey("ctrl+s", () => {
265
265
  fired = true
266
266
  })
267
267
 
268
- const textarea = document.createElement('textarea')
268
+ const textarea = document.createElement("textarea")
269
269
  document.body.appendChild(textarea)
270
- fireKey('s', { ctrlKey: true }, textarea)
270
+ fireKey("s", { ctrlKey: true }, textarea)
271
271
  textarea.remove()
272
272
 
273
273
  expect(fired).toBe(false)
274
274
  })
275
275
 
276
- it('does not fire in contenteditable by default', () => {
276
+ it("does not fire in contenteditable by default", () => {
277
277
  let fired = false
278
- registerHotkey('ctrl+s', () => {
278
+ registerHotkey("ctrl+s", () => {
279
279
  fired = true
280
280
  })
281
281
 
282
- const div = document.createElement('div')
283
- div.contentEditable = 'true'
282
+ const div = document.createElement("div")
283
+ div.contentEditable = "true"
284
284
  document.body.appendChild(div)
285
- fireKey('s', { ctrlKey: true }, div)
285
+ fireKey("s", { ctrlKey: true }, div)
286
286
  div.remove()
287
287
 
288
288
  expect(fired).toBe(false)
289
289
  })
290
290
 
291
- it('enabled: false prevents firing', () => {
291
+ it("enabled: false prevents firing", () => {
292
292
  let fired = false
293
293
  registerHotkey(
294
- 'ctrl+s',
294
+ "ctrl+s",
295
295
  () => {
296
296
  fired = true
297
297
  },
298
298
  { enabled: false },
299
299
  )
300
- fireKey('s', { ctrlKey: true })
300
+ fireKey("s", { ctrlKey: true })
301
301
  expect(fired).toBe(false)
302
302
  })
303
303
 
304
- it('enabled as function controls firing dynamically', () => {
304
+ it("enabled as function controls firing dynamically", () => {
305
305
  let enabled = true
306
306
  let count = 0
307
307
  registerHotkey(
308
- 'ctrl+s',
308
+ "ctrl+s",
309
309
  () => {
310
310
  count++
311
311
  },
312
312
  { enabled: () => enabled },
313
313
  )
314
314
 
315
- fireKey('s', { ctrlKey: true })
315
+ fireKey("s", { ctrlKey: true })
316
316
  expect(count).toBe(1)
317
317
 
318
318
  enabled = false
319
- fireKey('s', { ctrlKey: true })
319
+ fireKey("s", { ctrlKey: true })
320
320
  expect(count).toBe(1)
321
321
  })
322
322
 
323
- it('multiple hotkeys can be registered', () => {
323
+ it("multiple hotkeys can be registered", () => {
324
324
  let saveCount = 0
325
325
  let undoCount = 0
326
- registerHotkey('ctrl+s', () => saveCount++)
327
- registerHotkey('ctrl+z', () => undoCount++)
326
+ registerHotkey("ctrl+s", () => saveCount++)
327
+ registerHotkey("ctrl+z", () => undoCount++)
328
328
 
329
- fireKey('s', { ctrlKey: true })
330
- fireKey('z', { ctrlKey: true })
329
+ fireKey("s", { ctrlKey: true })
330
+ fireKey("z", { ctrlKey: true })
331
331
 
332
332
  expect(saveCount).toBe(1)
333
333
  expect(undoCount).toBe(1)
334
334
  })
335
335
  })
336
336
 
337
- describe('scopes', () => {
337
+ describe("scopes", () => {
338
338
  beforeEach(() => {
339
339
  _resetHotkeys()
340
340
  })
@@ -343,85 +343,85 @@ describe('scopes', () => {
343
343
  _resetHotkeys()
344
344
  })
345
345
 
346
- it('global scope is active by default', () => {
346
+ it("global scope is active by default", () => {
347
347
  const scopes = getActiveScopes()
348
- expect(scopes.peek().has('global')).toBe(true)
348
+ expect(scopes.peek().has("global")).toBe(true)
349
349
  })
350
350
 
351
- it('global scope hotkeys fire by default', () => {
351
+ it("global scope hotkeys fire by default", () => {
352
352
  let fired = false
353
- registerHotkey('ctrl+s', () => {
353
+ registerHotkey("ctrl+s", () => {
354
354
  fired = true
355
355
  })
356
- fireKey('s', { ctrlKey: true })
356
+ fireKey("s", { ctrlKey: true })
357
357
  expect(fired).toBe(true)
358
358
  })
359
359
 
360
- it('non-global scope hotkeys do not fire by default', () => {
360
+ it("non-global scope hotkeys do not fire by default", () => {
361
361
  let fired = false
362
362
  registerHotkey(
363
- 'ctrl+s',
363
+ "ctrl+s",
364
364
  () => {
365
365
  fired = true
366
366
  },
367
- { scope: 'editor' },
367
+ { scope: "editor" },
368
368
  )
369
- fireKey('s', { ctrlKey: true })
369
+ fireKey("s", { ctrlKey: true })
370
370
  expect(fired).toBe(false)
371
371
  })
372
372
 
373
- it('enableScope activates a scope', () => {
373
+ it("enableScope activates a scope", () => {
374
374
  let fired = false
375
375
  registerHotkey(
376
- 'ctrl+s',
376
+ "ctrl+s",
377
377
  () => {
378
378
  fired = true
379
379
  },
380
- { scope: 'editor' },
380
+ { scope: "editor" },
381
381
  )
382
382
 
383
- enableScope('editor')
384
- fireKey('s', { ctrlKey: true })
383
+ enableScope("editor")
384
+ fireKey("s", { ctrlKey: true })
385
385
  expect(fired).toBe(true)
386
386
  })
387
387
 
388
- it('disableScope deactivates a scope', () => {
388
+ it("disableScope deactivates a scope", () => {
389
389
  let count = 0
390
390
  registerHotkey(
391
- 'ctrl+s',
391
+ "ctrl+s",
392
392
  () => {
393
393
  count++
394
394
  },
395
- { scope: 'editor' },
395
+ { scope: "editor" },
396
396
  )
397
397
 
398
- enableScope('editor')
399
- fireKey('s', { ctrlKey: true })
398
+ enableScope("editor")
399
+ fireKey("s", { ctrlKey: true })
400
400
  expect(count).toBe(1)
401
401
 
402
- disableScope('editor')
403
- fireKey('s', { ctrlKey: true })
402
+ disableScope("editor")
403
+ fireKey("s", { ctrlKey: true })
404
404
  expect(count).toBe(1)
405
405
  })
406
406
 
407
- it('cannot disable global scope', () => {
408
- disableScope('global')
409
- expect(getActiveScopes().peek().has('global')).toBe(true)
407
+ it("cannot disable global scope", () => {
408
+ disableScope("global")
409
+ expect(getActiveScopes().peek().has("global")).toBe(true)
410
410
  })
411
411
 
412
- it('enableScope is idempotent', () => {
413
- enableScope('editor')
414
- enableScope('editor')
412
+ it("enableScope is idempotent", () => {
413
+ enableScope("editor")
414
+ enableScope("editor")
415
415
  expect(getActiveScopes().peek().size).toBe(2) // global + editor
416
416
  })
417
417
 
418
- it('disableScope for non-active scope is no-op', () => {
419
- disableScope('nonexistent')
418
+ it("disableScope for non-active scope is no-op", () => {
419
+ disableScope("nonexistent")
420
420
  expect(getActiveScopes().peek().size).toBe(1)
421
421
  })
422
422
  })
423
423
 
424
- describe('getRegisteredHotkeys', () => {
424
+ describe("getRegisteredHotkeys", () => {
425
425
  beforeEach(() => {
426
426
  _resetHotkeys()
427
427
  })
@@ -430,30 +430,30 @@ describe('getRegisteredHotkeys', () => {
430
430
  _resetHotkeys()
431
431
  })
432
432
 
433
- it('returns all registered hotkeys', () => {
433
+ it("returns all registered hotkeys", () => {
434
434
  // biome-ignore lint/suspicious/noEmptyBlockStatements: intentional no-op handlers for registry test
435
435
  const noop = () => {}
436
- registerHotkey('ctrl+s', noop, { description: 'Save' })
437
- registerHotkey('ctrl+z', noop, { scope: 'editor', description: 'Undo' })
436
+ registerHotkey("ctrl+s", noop, { description: "Save" })
437
+ registerHotkey("ctrl+z", noop, { scope: "editor", description: "Undo" })
438
438
 
439
439
  const hotkeys = getRegisteredHotkeys()
440
440
  expect(hotkeys).toHaveLength(2)
441
441
  expect(hotkeys[0]).toEqual({
442
- shortcut: 'ctrl+s',
443
- scope: 'global',
444
- description: 'Save',
442
+ shortcut: "ctrl+s",
443
+ scope: "global",
444
+ description: "Save",
445
445
  })
446
446
  expect(hotkeys[1]).toEqual({
447
- shortcut: 'ctrl+z',
448
- scope: 'editor',
449
- description: 'Undo',
447
+ shortcut: "ctrl+z",
448
+ scope: "editor",
449
+ description: "Undo",
450
450
  })
451
451
  })
452
452
 
453
- it('reflects unregistered hotkeys', () => {
453
+ it("reflects unregistered hotkeys", () => {
454
454
  // biome-ignore lint/suspicious/noEmptyBlockStatements: intentional no-op handler
455
455
  const noop = () => {}
456
- const unsub = registerHotkey('ctrl+s', noop)
456
+ const unsub = registerHotkey("ctrl+s", noop)
457
457
  expect(getRegisteredHotkeys()).toHaveLength(1)
458
458
  unsub()
459
459
  expect(getRegisteredHotkeys()).toHaveLength(0)
package/src/types.ts CHANGED
@@ -44,11 +44,7 @@ export interface HotkeyEntry {
44
44
  options: Required<
45
45
  Pick<
46
46
  HotkeyOptions,
47
- | 'scope'
48
- | 'preventDefault'
49
- | 'stopPropagation'
50
- | 'enableOnInputs'
51
- | 'enabled'
47
+ "scope" | "preventDefault" | "stopPropagation" | "enableOnInputs" | "enabled"
52
48
  >
53
49
  > & { description?: string }
54
50
  }
@@ -1,5 +1,5 @@
1
- import { onUnmount } from '@pyreon/core'
2
- import { disableScope, enableScope } from './registry'
1
+ import { onUnmount } from "@pyreon/core"
2
+ import { disableScope, enableScope } from "./registry"
3
3
 
4
4
  /**
5
5
  * Activate a hotkey scope for the lifetime of a component.
package/src/use-hotkey.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { onUnmount } from '@pyreon/core'
2
- import { registerHotkey } from './registry'
3
- import type { HotkeyOptions } from './types'
1
+ import { onUnmount } from "@pyreon/core"
2
+ import { registerHotkey } from "./registry"
3
+ import type { HotkeyOptions } from "./types"
4
4
 
5
5
  /**
6
6
  * Register a keyboard shortcut scoped to a component's lifecycle.