@pyreon/hotkeys 0.7.0 → 0.9.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/README.md ADDED
@@ -0,0 +1,64 @@
1
+ # @pyreon/hotkeys
2
+
3
+ Reactive keyboard shortcut management for Pyreon. Scope-aware, modifier keys, conflict detection, automatic lifecycle cleanup.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ bun add @pyreon/hotkeys
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```tsx
14
+ import { useHotkey, useHotkeyScope } from '@pyreon/hotkeys'
15
+
16
+ // Global shortcut — auto-unregisters on unmount
17
+ useHotkey('mod+s', () => save(), { description: 'Save document' })
18
+ useHotkey('mod+k', () => openCommandPalette())
19
+
20
+ // Scoped shortcuts — only active when scope is enabled
21
+ useHotkeyScope('editor')
22
+ useHotkey('ctrl+z', () => undo(), { scope: 'editor' })
23
+ useHotkey('escape', () => closeModal(), { scope: 'modal' })
24
+ ```
25
+
26
+ `mod` = Command on Mac, Ctrl elsewhere. Shortcuts are ignored in input elements by default.
27
+
28
+ ## API
29
+
30
+ ### `useHotkey(shortcut, handler, options?)`
31
+
32
+ Component-scoped keyboard shortcut. Automatically unregisters on unmount.
33
+
34
+ Options: `scope`, `description`, `preventDefault` (default: true), `enableInInputs` (default: false).
35
+
36
+ ### `useHotkeyScope(scope)`
37
+
38
+ Activate a scope for the component's lifetime. Deactivates on unmount.
39
+
40
+ ### `registerHotkey(shortcut, handler, options?)`
41
+
42
+ Imperative registration. Returns an unregister function.
43
+
44
+ ### `enableScope(scope)` / `disableScope(scope)`
45
+
46
+ Manually control which scopes are active.
47
+
48
+ ### `getRegisteredHotkeys()`
49
+
50
+ List all registered hotkeys — useful for help dialogs.
51
+
52
+ ### `getActiveScopes()`
53
+
54
+ List currently active scopes.
55
+
56
+ ### Utilities
57
+
58
+ - `parseShortcut(str)` — parse shortcut string into `KeyCombo`
59
+ - `formatCombo(combo)` — format `KeyCombo` as display string
60
+ - `matchesCombo(event, combo)` — check if a keyboard event matches a combo
61
+
62
+ ## License
63
+
64
+ MIT
@@ -5386,7 +5386,7 @@ var drawChart = (function (exports) {
5386
5386
  </script>
5387
5387
  <script>
5388
5388
  /*<!--*/
5389
- const data = {"version":2,"tree":{"name":"root","children":[{"name":"index.js","children":[{"name":"src","children":[{"uid":"1e5277a7-1","name":"parse.ts"},{"uid":"1e5277a7-3","name":"registry.ts"},{"uid":"1e5277a7-5","name":"use-hotkey.ts"},{"uid":"1e5277a7-7","name":"use-hotkey-scope.ts"},{"uid":"1e5277a7-9","name":"index.ts"}]}]}],"isRoot":true},"nodeParts":{"1e5277a7-1":{"renderedLength":1941,"gzipLength":838,"brotliLength":0,"metaUid":"1e5277a7-0"},"1e5277a7-3":{"renderedLength":2832,"gzipLength":1057,"brotliLength":0,"metaUid":"1e5277a7-2"},"1e5277a7-5":{"renderedLength":496,"gzipLength":295,"brotliLength":0,"metaUid":"1e5277a7-4"},"1e5277a7-7":{"renderedLength":421,"gzipLength":264,"brotliLength":0,"metaUid":"1e5277a7-6"},"1e5277a7-9":{"renderedLength":0,"gzipLength":0,"brotliLength":0,"metaUid":"1e5277a7-8"}},"nodeMetas":{"1e5277a7-0":{"id":"/src/parse.ts","moduleParts":{"index.js":"1e5277a7-1"},"imported":[],"importedBy":[{"uid":"1e5277a7-8"},{"uid":"1e5277a7-2"}]},"1e5277a7-2":{"id":"/src/registry.ts","moduleParts":{"index.js":"1e5277a7-3"},"imported":[{"uid":"1e5277a7-11"},{"uid":"1e5277a7-0"}],"importedBy":[{"uid":"1e5277a7-8"},{"uid":"1e5277a7-4"},{"uid":"1e5277a7-6"}]},"1e5277a7-4":{"id":"/src/use-hotkey.ts","moduleParts":{"index.js":"1e5277a7-5"},"imported":[{"uid":"1e5277a7-10"},{"uid":"1e5277a7-2"}],"importedBy":[{"uid":"1e5277a7-8"}]},"1e5277a7-6":{"id":"/src/use-hotkey-scope.ts","moduleParts":{"index.js":"1e5277a7-7"},"imported":[{"uid":"1e5277a7-10"},{"uid":"1e5277a7-2"}],"importedBy":[{"uid":"1e5277a7-8"}]},"1e5277a7-8":{"id":"/src/index.ts","moduleParts":{"index.js":"1e5277a7-9"},"imported":[{"uid":"1e5277a7-4"},{"uid":"1e5277a7-6"},{"uid":"1e5277a7-2"},{"uid":"1e5277a7-0"}],"importedBy":[],"isEntry":true},"1e5277a7-10":{"id":"@pyreon/core","moduleParts":{},"imported":[],"importedBy":[{"uid":"1e5277a7-4"},{"uid":"1e5277a7-6"}]},"1e5277a7-11":{"id":"@pyreon/reactivity","moduleParts":{},"imported":[],"importedBy":[{"uid":"1e5277a7-2"}]}},"env":{"rollup":"4.23.0"},"options":{"gzip":true,"brotli":false,"sourcemap":false}};
5389
+ const data = {"version":2,"tree":{"name":"root","children":[{"name":"index.js","children":[{"name":"src","children":[{"uid":"b111ad13-1","name":"parse.ts"},{"uid":"b111ad13-3","name":"registry.ts"},{"uid":"b111ad13-5","name":"use-hotkey.ts"},{"uid":"b111ad13-7","name":"use-hotkey-scope.ts"},{"uid":"b111ad13-9","name":"index.ts"}]}]}],"isRoot":true},"nodeParts":{"b111ad13-1":{"renderedLength":1941,"gzipLength":838,"brotliLength":0,"metaUid":"b111ad13-0"},"b111ad13-3":{"renderedLength":2918,"gzipLength":1080,"brotliLength":0,"metaUid":"b111ad13-2"},"b111ad13-5":{"renderedLength":496,"gzipLength":295,"brotliLength":0,"metaUid":"b111ad13-4"},"b111ad13-7":{"renderedLength":421,"gzipLength":264,"brotliLength":0,"metaUid":"b111ad13-6"},"b111ad13-9":{"renderedLength":0,"gzipLength":0,"brotliLength":0,"metaUid":"b111ad13-8"}},"nodeMetas":{"b111ad13-0":{"id":"/src/parse.ts","moduleParts":{"index.js":"b111ad13-1"},"imported":[],"importedBy":[{"uid":"b111ad13-8"},{"uid":"b111ad13-2"}]},"b111ad13-2":{"id":"/src/registry.ts","moduleParts":{"index.js":"b111ad13-3"},"imported":[{"uid":"b111ad13-11"},{"uid":"b111ad13-0"}],"importedBy":[{"uid":"b111ad13-8"},{"uid":"b111ad13-4"},{"uid":"b111ad13-6"}]},"b111ad13-4":{"id":"/src/use-hotkey.ts","moduleParts":{"index.js":"b111ad13-5"},"imported":[{"uid":"b111ad13-10"},{"uid":"b111ad13-2"}],"importedBy":[{"uid":"b111ad13-8"}]},"b111ad13-6":{"id":"/src/use-hotkey-scope.ts","moduleParts":{"index.js":"b111ad13-7"},"imported":[{"uid":"b111ad13-10"},{"uid":"b111ad13-2"}],"importedBy":[{"uid":"b111ad13-8"}]},"b111ad13-8":{"id":"/src/index.ts","moduleParts":{"index.js":"b111ad13-9"},"imported":[{"uid":"b111ad13-4"},{"uid":"b111ad13-6"},{"uid":"b111ad13-2"},{"uid":"b111ad13-0"}],"importedBy":[],"isEntry":true},"b111ad13-10":{"id":"@pyreon/core","moduleParts":{},"imported":[],"importedBy":[{"uid":"b111ad13-4"},{"uid":"b111ad13-6"}]},"b111ad13-11":{"id":"@pyreon/reactivity","moduleParts":{},"imported":[],"importedBy":[{"uid":"b111ad13-2"}]}},"env":{"rollup":"4.23.0"},"options":{"gzip":true,"brotli":false,"sourcemap":false}};
5390
5390
 
5391
5391
  const run = () => {
5392
5392
  const width = window.innerWidth;
package/lib/index.js CHANGED
@@ -125,7 +125,7 @@ function registerHotkey(shortcut, handler, options) {
125
125
  stopPropagation: options?.stopPropagation === true,
126
126
  enableOnInputs: options?.enableOnInputs === true,
127
127
  enabled: options?.enabled ?? true,
128
- description: options?.description
128
+ ...options?.description != null ? { description: options.description } : {}
129
129
  }
130
130
  };
131
131
  entries.push(entry);
@@ -168,7 +168,7 @@ function getRegisteredHotkeys() {
168
168
  return entries.map((e) => ({
169
169
  shortcut: e.shortcut,
170
170
  scope: e.options.scope,
171
- description: e.options.description
171
+ ...e.options.description != null ? { description: e.options.description } : {}
172
172
  }));
173
173
  }
174
174
  function _resetHotkeys() {
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 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 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":"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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(\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 +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;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"}
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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pyreon/hotkeys",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "description": "Reactive keyboard shortcut management for Pyreon — scope-aware, conflict detection",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -40,7 +40,7 @@
40
40
  "typecheck": "tsc --noEmit"
41
41
  },
42
42
  "peerDependencies": {
43
- "@pyreon/core": ">=0.5.0 <1.0.0",
44
- "@pyreon/reactivity": ">=0.5.0 <1.0.0"
43
+ "@pyreon/core": ">=0.7.0 <0.8.0",
44
+ "@pyreon/reactivity": ">=0.7.0 <0.8.0"
45
45
  }
46
46
  }
package/src/registry.ts CHANGED
@@ -84,7 +84,9 @@ export function registerHotkey(
84
84
  stopPropagation: options?.stopPropagation === true,
85
85
  enableOnInputs: options?.enableOnInputs === true,
86
86
  enabled: options?.enabled ?? true,
87
- description: options?.description,
87
+ ...(options?.description != null
88
+ ? { description: options.description }
89
+ : {}),
88
90
  },
89
91
  }
90
92
 
@@ -139,7 +141,9 @@ export function getRegisteredHotkeys(): ReadonlyArray<{
139
141
  return entries.map((e) => ({
140
142
  shortcut: e.shortcut,
141
143
  scope: e.options.scope,
142
- description: e.options.description,
144
+ ...(e.options.description != null
145
+ ? { description: e.options.description }
146
+ : {}),
143
147
  }))
144
148
  }
145
149