@pyreon/hotkeys 0.33.0 → 0.35.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/analysis/index.js.html +1 -1
- package/lib/index.js +6 -4
- package/lib/index.js.map +1 -1
- package/lib/types/index.d.ts.map +1 -1
- package/package.json +3 -3
|
@@ -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":[{"uid":"
|
|
5389
|
+
const data = {"version":2,"tree":{"name":"root","children":[{"name":"index.js","children":[{"uid":"6eaba0d3-1","name":"package.json"},{"name":"src","children":[{"uid":"6eaba0d3-3","name":"parse.ts"},{"uid":"6eaba0d3-5","name":"registry.ts"},{"uid":"6eaba0d3-7","name":"use-hotkey.ts"},{"uid":"6eaba0d3-9","name":"use-hotkey-scope.ts"},{"uid":"6eaba0d3-11","name":"index.ts"}]}]}],"isRoot":true},"nodeParts":{"6eaba0d3-1":{"renderedLength":90,"gzipLength":101,"brotliLength":0,"metaUid":"6eaba0d3-0"},"6eaba0d3-3":{"renderedLength":1941,"gzipLength":838,"brotliLength":0,"metaUid":"6eaba0d3-2"},"6eaba0d3-5":{"renderedLength":5093,"gzipLength":1657,"brotliLength":0,"metaUid":"6eaba0d3-4"},"6eaba0d3-7":{"renderedLength":496,"gzipLength":295,"brotliLength":0,"metaUid":"6eaba0d3-6"},"6eaba0d3-9":{"renderedLength":421,"gzipLength":264,"brotliLength":0,"metaUid":"6eaba0d3-8"},"6eaba0d3-11":{"renderedLength":760,"gzipLength":452,"brotliLength":0,"metaUid":"6eaba0d3-10"}},"nodeMetas":{"6eaba0d3-0":{"id":"/package.json","moduleParts":{"index.js":"6eaba0d3-1"},"imported":[],"importedBy":[{"uid":"6eaba0d3-10"}]},"6eaba0d3-2":{"id":"/src/parse.ts","moduleParts":{"index.js":"6eaba0d3-3"},"imported":[],"importedBy":[{"uid":"6eaba0d3-10"},{"uid":"6eaba0d3-4"}]},"6eaba0d3-4":{"id":"/src/registry.ts","moduleParts":{"index.js":"6eaba0d3-5"},"imported":[{"uid":"6eaba0d3-12"},{"uid":"6eaba0d3-2"}],"importedBy":[{"uid":"6eaba0d3-10"},{"uid":"6eaba0d3-6"},{"uid":"6eaba0d3-8"}]},"6eaba0d3-6":{"id":"/src/use-hotkey.ts","moduleParts":{"index.js":"6eaba0d3-7"},"imported":[{"uid":"6eaba0d3-13"},{"uid":"6eaba0d3-4"}],"importedBy":[{"uid":"6eaba0d3-10"}]},"6eaba0d3-8":{"id":"/src/use-hotkey-scope.ts","moduleParts":{"index.js":"6eaba0d3-9"},"imported":[{"uid":"6eaba0d3-13"},{"uid":"6eaba0d3-4"}],"importedBy":[{"uid":"6eaba0d3-10"}]},"6eaba0d3-10":{"id":"/src/index.ts","moduleParts":{"index.js":"6eaba0d3-11"},"imported":[{"uid":"6eaba0d3-0"},{"uid":"6eaba0d3-12"},{"uid":"6eaba0d3-6"},{"uid":"6eaba0d3-8"},{"uid":"6eaba0d3-4"},{"uid":"6eaba0d3-2"}],"importedBy":[],"isEntry":true},"6eaba0d3-12":{"id":"@pyreon/reactivity","moduleParts":{},"imported":[],"importedBy":[{"uid":"6eaba0d3-10"},{"uid":"6eaba0d3-4"}]},"6eaba0d3-13":{"id":"@pyreon/core","moduleParts":{},"imported":[],"importedBy":[{"uid":"6eaba0d3-6"},{"uid":"6eaba0d3-8"}]}},"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
|
@@ -3,7 +3,7 @@ import { onUnmount } from "@pyreon/core";
|
|
|
3
3
|
|
|
4
4
|
//#region package.json
|
|
5
5
|
var name = "@pyreon/hotkeys";
|
|
6
|
-
var version = "0.
|
|
6
|
+
var version = "0.35.0";
|
|
7
7
|
|
|
8
8
|
//#endregion
|
|
9
9
|
//#region src/parse.ts
|
|
@@ -78,7 +78,7 @@ function isMac() {
|
|
|
78
78
|
//#endregion
|
|
79
79
|
//#region src/registry.ts
|
|
80
80
|
const entries = [];
|
|
81
|
-
const activeScopes = signal(new Set(["global"]));
|
|
81
|
+
const activeScopes = signal(/* @__PURE__ */ new Set(["global"]));
|
|
82
82
|
let listenerAttached = false;
|
|
83
83
|
let keydownHandler = null;
|
|
84
84
|
const SEQUENCE_TIMEOUT_MS = 1e3;
|
|
@@ -95,7 +95,7 @@ function armSequenceTimeout() {
|
|
|
95
95
|
if (pendingTimer) clearTimeout(pendingTimer);
|
|
96
96
|
pendingTimer = setTimeout(clearPending, SEQUENCE_TIMEOUT_MS);
|
|
97
97
|
}
|
|
98
|
-
const INPUT_TAGS = new Set([
|
|
98
|
+
const INPUT_TAGS = /* @__PURE__ */ new Set([
|
|
99
99
|
"INPUT",
|
|
100
100
|
"TEXTAREA",
|
|
101
101
|
"SELECT"
|
|
@@ -118,6 +118,7 @@ function attachListener() {
|
|
|
118
118
|
let fired = false;
|
|
119
119
|
for (const p of pending) {
|
|
120
120
|
const expected = p.entry.sequence[p.next];
|
|
121
|
+
/* v8 ignore next */
|
|
121
122
|
if (!expected) continue;
|
|
122
123
|
if (!matchesCombo(event, expected)) continue;
|
|
123
124
|
if (p.next + 1 === p.entry.sequence.length) {
|
|
@@ -185,6 +186,7 @@ function registerHotkey(shortcut, handler, options) {
|
|
|
185
186
|
if (subShortcuts.length === 0) throw new Error(`[@pyreon/hotkeys] empty shortcut: ${JSON.stringify(shortcut)}`);
|
|
186
187
|
const combos = subShortcuts.map(parseShortcut);
|
|
187
188
|
const firstCombo = combos[0];
|
|
189
|
+
/* v8 ignore start */
|
|
188
190
|
if (!firstCombo) throw new Error(`[@pyreon/hotkeys] invalid shortcut: ${JSON.stringify(shortcut)}`);
|
|
189
191
|
const entry = {
|
|
190
192
|
shortcut,
|
|
@@ -246,7 +248,7 @@ function getRegisteredHotkeys() {
|
|
|
246
248
|
}
|
|
247
249
|
function _resetHotkeys() {
|
|
248
250
|
entries.length = 0;
|
|
249
|
-
activeScopes.set(new Set(["global"]));
|
|
251
|
+
activeScopes.set(/* @__PURE__ */ new Set(["global"]));
|
|
250
252
|
clearPending();
|
|
251
253
|
detachListener();
|
|
252
254
|
}
|
package/lib/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["__pkgName","__pkgVersion"],"sources":["../package.json","../src/parse.ts","../src/registry.ts","../src/use-hotkey.ts","../src/use-hotkey-scope.ts","../src/index.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 { isServer, signal } from '@pyreon/reactivity'\nimport { matchesCombo, parseShortcut } from './parse'\nimport type { HotkeyEntry, HotkeyOptions } from './types'\n\n// ─── State ───────────────────────────────────────────────────────────────────\n\n// Linear array on purpose. Per-keystroke dispatch (line 35) iterates entries\n// in registration order — O(n) where n = registered hotkeys. For real apps\n// (single-digit to low-tens) the cost is sub-microsecond and well under the\n// 16ms frame budget. Switching to a Map<comboKey, entries[]> would help only\n// past ~5,000 hotkeys, which is unrealistic. The array also preserves\n// registration order for the rare case where two hotkeys match the same\n// combo on different scopes — first-registered wins. Don't replace with a\n// Map without confirming a real app actually hits the perf wall.\nconst entries: HotkeyEntry[] = []\nconst activeScopes = signal<Set<string>>(new Set(['global']))\nlet listenerAttached = false\nlet keydownHandler: ((event: KeyboardEvent) => void) | null = null\n\n// ─── Sequence state ──────────────────────────────────────────────────────────\n\n/**\n * Active sequence-tracking state. When a user presses a key that matches the\n * FIRST combo of one-or-more sequential hotkeys (`'g t'`, `'g n'`, etc.), we\n * remember the matched prefix and wait for the next keystroke. If it matches\n * the next combo in any pending sequence, we keep narrowing. A full match\n * fires the handler; a non-match clears the pending state. The state also\n * times out after `SEQUENCE_TIMEOUT_MS` so a stranded prefix doesn't trap\n * the next single-key shortcut.\n */\ninterface PendingSequence {\n /** The entry whose remaining keys we're tracking. */\n entry: HotkeyEntry\n /** Index of the NEXT combo we expect to match in `entry.sequence`. */\n next: number\n}\n\nconst SEQUENCE_TIMEOUT_MS = 1000\nlet pending: PendingSequence[] = []\nlet pendingTimer: ReturnType<typeof setTimeout> | null = null\n\nfunction clearPending(): void {\n pending = []\n if (pendingTimer) {\n clearTimeout(pendingTimer)\n pendingTimer = null\n }\n}\n\nfunction armSequenceTimeout(): void {\n if (pendingTimer) clearTimeout(pendingTimer)\n pendingTimer = setTimeout(clearPending, SEQUENCE_TIMEOUT_MS)\n}\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 (isServer) return\n listenerAttached = true\n\n keydownHandler = (event) => {\n const scopes = activeScopes.peek()\n\n // ─── Stage 1: advance any pending sequences ──────────────────────────\n // If we're mid-sequence, the user's next keystroke must match the\n // NEXT combo of at least one pending entry. Non-matches drop that\n // pending entry; a full match (last combo) fires the handler and\n // clears all pending state. If anything advanced, return early so\n // single-key shortcuts (like 't' alone) don't ALSO fire on this\n // keystroke — sequence ownership of the keystroke beats fresh\n // single-combo matches.\n if (pending.length > 0) {\n const surviving: PendingSequence[] = []\n let fired = false\n for (const p of pending) {\n const expected = p.entry.sequence[p.next]\n if (!expected) continue // defensive — shouldn't happen\n if (!matchesCombo(event, expected)) continue\n // Advance\n if (p.next + 1 === p.entry.sequence.length) {\n // Full match — fire\n if (p.entry.options.preventDefault) event.preventDefault()\n if (p.entry.options.stopPropagation) event.stopPropagation()\n p.entry.handler(event)\n fired = true\n } else {\n surviving.push({ entry: p.entry, next: p.next + 1 })\n }\n }\n if (fired || surviving.length > 0) {\n pending = surviving\n if (surviving.length > 0) armSequenceTimeout()\n else clearPending()\n return\n }\n // No pending sequence matched — fall through to fresh dispatch\n // but clear the stale pending state.\n clearPending()\n }\n\n // ─── Stage 2: fresh dispatch ────────────────────────────────────────\n const newPending: PendingSequence[] = []\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 FIRST combo match\n if (!matchesCombo(event, entry.combo)) continue\n\n if (entry.sequence.length === 0) {\n // Single-combo hotkey — fire immediately\n if (entry.options.preventDefault) event.preventDefault()\n if (entry.options.stopPropagation) event.stopPropagation()\n entry.handler(event)\n } else {\n // Sequential hotkey — record as pending; consume the keystroke\n // (preventDefault so 'g' doesn't trigger a browser shortcut)\n // but DON'T fire the handler yet.\n if (entry.options.preventDefault) event.preventDefault()\n newPending.push({ entry, next: 0 })\n }\n }\n\n if (newPending.length > 0) {\n pending = newPending\n armSequenceTimeout()\n }\n }\n\n window.addEventListener('keydown', keydownHandler)\n}\n\nfunction detachListener(): void {\n if (isServer) return\n if (!listenerAttached || !keydownHandler) return\n window.removeEventListener('keydown', keydownHandler)\n listenerAttached = false\n keydownHandler = null\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 // Sequential combo support: space-separated combos like `'g t'` are\n // treated as ordered sequences — user presses `g`, then `t` within\n // `SEQUENCE_TIMEOUT_MS`, the handler fires. Each sub-combo is parsed\n // through the existing `parseShortcut` (so `'ctrl+k p'` works: `ctrl+k`\n // followed by `p`). Single-combo shortcuts have `sequence: []` and\n // behave identically to the pre-sequence code path.\n const subShortcuts = shortcut\n .split(/\\s+/)\n .map((s) => s.trim())\n .filter(Boolean)\n if (subShortcuts.length === 0) {\n throw new Error(`[@pyreon/hotkeys] empty shortcut: ${JSON.stringify(shortcut)}`)\n }\n const combos = subShortcuts.map(parseShortcut)\n const firstCombo = combos[0]\n if (!firstCombo) {\n // Unreachable given the length check above, but keeps TS happy.\n throw new Error(`[@pyreon/hotkeys] invalid shortcut: ${JSON.stringify(shortcut)}`)\n }\n const rest = combos.slice(1)\n\n const entry: HotkeyEntry = {\n shortcut,\n combo: firstCombo,\n sequence: rest,\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 // Detach listener if no more entries\n if (entries.length === 0) {\n detachListener()\n }\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 clearPending()\n detachListener()\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","/**\n * @pyreon/hotkeys — Reactive keyboard shortcut management for Pyreon.\n *\n * Register global or scoped keyboard shortcuts with automatic lifecycle\n * management. Supports modifier keys, aliases, input filtering, and\n * scope-based activation.\n *\n * @example\n * ```ts\n * import { useHotkey, useHotkeyScope } from '@pyreon/hotkeys'\n *\n * // Global shortcut\n * useHotkey('ctrl+s', () => save(), { description: 'Save' })\n *\n * // Scoped shortcut — only active when scope is enabled\n * useHotkeyScope('editor')\n * useHotkey('ctrl+z', () => undo(), { scope: 'editor' })\n *\n * // Platform-aware — mod = ⌘ on Mac, Ctrl elsewhere\n * useHotkey('mod+k', () => openCommandPalette())\n * ```\n */\n\nimport { name as __pkgName, version as __pkgVersion } from '../package.json' with { type: 'json' }\nimport { registerSingleton } from '@pyreon/reactivity'\n\n// Singleton sentinel — fail-loud detection of duplicate @pyreon/hotkeys\n// instances in the same heap. See @pyreon/reactivity/singleton-sentinel for\n// full rationale. Hardcoded version is acceptable here — it's a diagnostic\n// aid, not a load-bearing identity check.\nregisterSingleton(__pkgName, __pkgVersion, import.meta.url)\n\n// ─── Hooks ───────────────────────────────────────────────────────────────────\n\nexport { useHotkey } from './use-hotkey'\nexport { useHotkeyScope } from './use-hotkey-scope'\n\n// ─── Imperative API ──────────────────────────────────────────────────────────\n\nexport {\n disableScope,\n enableScope,\n getActiveScopes,\n getRegisteredHotkeys,\n registerHotkey,\n} from './registry'\n\n// ─── Utilities ───────────────────────────────────────────────────────────────\n\nexport { formatCombo, matchesCombo, parseShortcut } from './parse'\n\n// ─── Types ───────────────────────────────────────────────────────────────────\n\nexport type { HotkeyEntry, HotkeyOptions, KeyCombo } from './types'\n\n// ─── Testing ─────────────────────────────────────────────────────────────────\n\nexport { _resetHotkeys } from './registry'\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["__pkgName","__pkgVersion"],"sources":["../package.json","../src/parse.ts","../src/registry.ts","../src/use-hotkey.ts","../src/use-hotkey-scope.ts","../src/index.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 { isServer, signal } from '@pyreon/reactivity'\nimport { matchesCombo, parseShortcut } from './parse'\nimport type { HotkeyEntry, HotkeyOptions } from './types'\n\n// ─── State ───────────────────────────────────────────────────────────────────\n\n// Linear array on purpose. Per-keystroke dispatch (line 35) iterates entries\n// in registration order — O(n) where n = registered hotkeys. For real apps\n// (single-digit to low-tens) the cost is sub-microsecond and well under the\n// 16ms frame budget. Switching to a Map<comboKey, entries[]> would help only\n// past ~5,000 hotkeys, which is unrealistic. The array also preserves\n// registration order for the rare case where two hotkeys match the same\n// combo on different scopes — first-registered wins. Don't replace with a\n// Map without confirming a real app actually hits the perf wall.\nconst entries: HotkeyEntry[] = []\nconst activeScopes = signal<Set<string>>(new Set(['global']))\nlet listenerAttached = false\nlet keydownHandler: ((event: KeyboardEvent) => void) | null = null\n\n// ─── Sequence state ──────────────────────────────────────────────────────────\n\n/**\n * Active sequence-tracking state. When a user presses a key that matches the\n * FIRST combo of one-or-more sequential hotkeys (`'g t'`, `'g n'`, etc.), we\n * remember the matched prefix and wait for the next keystroke. If it matches\n * the next combo in any pending sequence, we keep narrowing. A full match\n * fires the handler; a non-match clears the pending state. The state also\n * times out after `SEQUENCE_TIMEOUT_MS` so a stranded prefix doesn't trap\n * the next single-key shortcut.\n */\ninterface PendingSequence {\n /** The entry whose remaining keys we're tracking. */\n entry: HotkeyEntry\n /** Index of the NEXT combo we expect to match in `entry.sequence`. */\n next: number\n}\n\nconst SEQUENCE_TIMEOUT_MS = 1000\nlet pending: PendingSequence[] = []\nlet pendingTimer: ReturnType<typeof setTimeout> | null = null\n\nfunction clearPending(): void {\n pending = []\n if (pendingTimer) {\n clearTimeout(pendingTimer)\n pendingTimer = null\n }\n}\n\nfunction armSequenceTimeout(): void {\n if (pendingTimer) clearTimeout(pendingTimer)\n pendingTimer = setTimeout(clearPending, SEQUENCE_TIMEOUT_MS)\n}\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 (isServer) return\n listenerAttached = true\n\n keydownHandler = (event) => {\n const scopes = activeScopes.peek()\n\n // ─── Stage 1: advance any pending sequences ──────────────────────────\n // If we're mid-sequence, the user's next keystroke must match the\n // NEXT combo of at least one pending entry. Non-matches drop that\n // pending entry; a full match (last combo) fires the handler and\n // clears all pending state. If anything advanced, return early so\n // single-key shortcuts (like 't' alone) don't ALSO fire on this\n // keystroke — sequence ownership of the keystroke beats fresh\n // single-combo matches.\n if (pending.length > 0) {\n const surviving: PendingSequence[] = []\n let fired = false\n for (const p of pending) {\n const expected = p.entry.sequence[p.next]\n // Defensive — unreachable. A PendingSequence is only created with a\n // `next` that points at a valid index: stage-1 survivors (L101) use\n // `p.next + 1` strictly below `sequence.length`, and fresh entries\n // (L144) use `next: 0` only when `sequence.length > 0`. So\n // `sequence[next]` is always defined.\n /* v8 ignore next */\n if (!expected) continue\n if (!matchesCombo(event, expected)) continue\n // Advance\n if (p.next + 1 === p.entry.sequence.length) {\n // Full match — fire\n if (p.entry.options.preventDefault) event.preventDefault()\n if (p.entry.options.stopPropagation) event.stopPropagation()\n p.entry.handler(event)\n fired = true\n } else {\n surviving.push({ entry: p.entry, next: p.next + 1 })\n }\n }\n if (fired || surviving.length > 0) {\n pending = surviving\n if (surviving.length > 0) armSequenceTimeout()\n else clearPending()\n return\n }\n // No pending sequence matched — fall through to fresh dispatch\n // but clear the stale pending state.\n clearPending()\n }\n\n // ─── Stage 2: fresh dispatch ────────────────────────────────────────\n const newPending: PendingSequence[] = []\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 FIRST combo match\n if (!matchesCombo(event, entry.combo)) continue\n\n if (entry.sequence.length === 0) {\n // Single-combo hotkey — fire immediately\n if (entry.options.preventDefault) event.preventDefault()\n if (entry.options.stopPropagation) event.stopPropagation()\n entry.handler(event)\n } else {\n // Sequential hotkey — record as pending; consume the keystroke\n // (preventDefault so 'g' doesn't trigger a browser shortcut)\n // but DON'T fire the handler yet.\n if (entry.options.preventDefault) event.preventDefault()\n newPending.push({ entry, next: 0 })\n }\n }\n\n if (newPending.length > 0) {\n pending = newPending\n armSequenceTimeout()\n }\n }\n\n window.addEventListener('keydown', keydownHandler)\n}\n\nfunction detachListener(): void {\n if (isServer) return\n if (!listenerAttached || !keydownHandler) return\n window.removeEventListener('keydown', keydownHandler)\n listenerAttached = false\n keydownHandler = null\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 // Sequential combo support: space-separated combos like `'g t'` are\n // treated as ordered sequences — user presses `g`, then `t` within\n // `SEQUENCE_TIMEOUT_MS`, the handler fires. Each sub-combo is parsed\n // through the existing `parseShortcut` (so `'ctrl+k p'` works: `ctrl+k`\n // followed by `p`). Single-combo shortcuts have `sequence: []` and\n // behave identically to the pre-sequence code path.\n const subShortcuts = shortcut\n .split(/\\s+/)\n .map((s) => s.trim())\n .filter(Boolean)\n if (subShortcuts.length === 0) {\n throw new Error(`[@pyreon/hotkeys] empty shortcut: ${JSON.stringify(shortcut)}`)\n }\n const combos = subShortcuts.map(parseShortcut)\n const firstCombo = combos[0]\n // Unreachable given the `subShortcuts.length === 0` throw above (combos is\n // 1:1 with the non-empty subShortcuts), but the index access is typed\n // `T | undefined`, so the guard keeps TS happy.\n /* v8 ignore start */\n if (!firstCombo) {\n throw new Error(`[@pyreon/hotkeys] invalid shortcut: ${JSON.stringify(shortcut)}`)\n }\n /* v8 ignore stop */\n const rest = combos.slice(1)\n\n const entry: HotkeyEntry = {\n shortcut,\n combo: firstCombo,\n sequence: rest,\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 // Detach listener if no more entries\n if (entries.length === 0) {\n detachListener()\n }\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 clearPending()\n detachListener()\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","/**\n * @pyreon/hotkeys — Reactive keyboard shortcut management for Pyreon.\n *\n * Register global or scoped keyboard shortcuts with automatic lifecycle\n * management. Supports modifier keys, aliases, input filtering, and\n * scope-based activation.\n *\n * @example\n * ```ts\n * import { useHotkey, useHotkeyScope } from '@pyreon/hotkeys'\n *\n * // Global shortcut\n * useHotkey('ctrl+s', () => save(), { description: 'Save' })\n *\n * // Scoped shortcut — only active when scope is enabled\n * useHotkeyScope('editor')\n * useHotkey('ctrl+z', () => undo(), { scope: 'editor' })\n *\n * // Platform-aware — mod = ⌘ on Mac, Ctrl elsewhere\n * useHotkey('mod+k', () => openCommandPalette())\n * ```\n */\n\nimport { name as __pkgName, version as __pkgVersion } from '../package.json' with { type: 'json' }\nimport { registerSingleton } from '@pyreon/reactivity'\n\n// Singleton sentinel — fail-loud detection of duplicate @pyreon/hotkeys\n// instances in the same heap. See @pyreon/reactivity/singleton-sentinel for\n// full rationale. Hardcoded version is acceptable here — it's a diagnostic\n// aid, not a load-bearing identity check.\nregisterSingleton(__pkgName, __pkgVersion, import.meta.url)\n\n// ─── Hooks ───────────────────────────────────────────────────────────────────\n\nexport { useHotkey } from './use-hotkey'\nexport { useHotkeyScope } from './use-hotkey-scope'\n\n// ─── Imperative API ──────────────────────────────────────────────────────────\n\nexport {\n disableScope,\n enableScope,\n getActiveScopes,\n getRegisteredHotkeys,\n registerHotkey,\n} from './registry'\n\n// ─── Utilities ───────────────────────────────────────────────────────────────\n\nexport { formatCombo, matchesCombo, parseShortcut } from './parse'\n\n// ─── Types ───────────────────────────────────────────────────────────────────\n\nexport type { HotkeyEntry, HotkeyOptions, KeyCombo } from './types'\n\n// ─── Testing ─────────────────────────────────────────────────────────────────\n\nexport { _resetHotkeys } from './registry'\n"],"mappings":"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"}
|
package/lib/types/index.d.ts.map
CHANGED
|
@@ -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;;;AAAG;UAMY,aAAA;EAAa;EAE5B,KAAA;EAF4B;EAI5B,cAAA;EAAA;EAEA,eAAA;EAEA;EAAA,cAAA;EAIA;EAFA,WAAA;EAEO;EAAP,OAAA;AAAA;;;;UAMe,WAAA;EAqBX;EAnBJ,QAAA;EAiBS;;;;;EAXT,KAAA,EAAO,QAAA;EAOP;;;;;;EAAA,QAAA,EAAU,QAAA;EAKR;EAHF,OAAA,GAAU,KAAA,EAAO,aAAA;EAOX;EALN,OAAA,EAAS,QAAA,CACP,IAAA,CACE,aAAA;IAGE,WAAA;EAAA;AAAA;;;;;AArDR;;;;;;;;;;AAKK;AAML;iBCCgB,SAAA,CACd,QAAA,UACA,OAAA,GAAU,KAAA,EAAO,aAAA,WACjB,OAAA,GAAU,aAAa;;;;;;ADfzB;;;;;;;;;;iBEUgB,cAAA,CAAe,KAAa;;;;AFV5C;;;;;;;;
|
|
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;;;AAAG;UAMY,aAAA;EAAa;EAE5B,KAAA;EAF4B;EAI5B,cAAA;EAAA;EAEA,eAAA;EAEA;EAAA,cAAA;EAIA;EAFA,WAAA;EAEO;EAAP,OAAA;AAAA;;;;UAMe,WAAA;EAqBX;EAnBJ,QAAA;EAiBS;;;;;EAXT,KAAA,EAAO,QAAA;EAOP;;;;;;EAAA,QAAA,EAAU,QAAA;EAKR;EAHF,OAAA,GAAU,KAAA,EAAO,aAAA;EAOX;EALN,OAAA,EAAS,QAAA,CACP,IAAA,CACE,aAAA;IAGE,WAAA;EAAA;AAAA;;;;;AArDR;;;;;;;;;;AAKK;AAML;iBCCgB,SAAA,CACd,QAAA,UACA,OAAA,GAAU,KAAA,EAAO,aAAA,WACjB,OAAA,GAAU,aAAa;;;;;;ADfzB;;;;;;;;;;iBEUgB,cAAA,CAAe,KAAa;;;;AFV5C;;;;;;;;iBG+KgB,cAAA,CACd,QAAA,UACA,OAAA,GAAU,KAAA,EAAO,aAAA,WACjB,OAAA,GAAU,aAAa;;AH7KpB;AAML;iBGqOgB,WAAA,CAAY,KAAa;;;;iBAWzB,YAAA,CAAa,KAAa;;;;iBAY1B,eAAA,IAAmB,MAAM,CAAC,GAAA;;AHhPjC;AAMT;iBGiPgB,oBAAA,IAAwB,aAAa;EACnD,QAAA;EACA,KAAA;EACA,WAAA;AAAA;AAAA,iBAWc,aAAA;;;;;AH5RhB;;iBIgBgB,aAAA,CAAc,QAAA,WAAmB,QAAQ;;;;iBAsCzC,YAAA,CAAa,KAAA,EAAO,aAAA,EAAe,KAAA,EAAO,QAAQ;;;;iBAalD,WAAA,CAAY,KAAe,EAAR,QAAQ"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pyreon/hotkeys",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.35.0",
|
|
4
4
|
"description": "Reactive keyboard shortcut management for Pyreon — scope-aware, conflict detection",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://github.com/pyreon/pyreon/tree/main/packages/fundamentals/hotkeys#readme",
|
|
@@ -36,8 +36,8 @@
|
|
|
36
36
|
"lint": "oxlint ."
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
|
-
"@pyreon/core": "^0.
|
|
40
|
-
"@pyreon/reactivity": "^0.
|
|
39
|
+
"@pyreon/core": "^0.35.0",
|
|
40
|
+
"@pyreon/reactivity": "^0.35.0"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@happy-dom/global-registrator": "^20.9.0",
|