@tanstack/hotkeys 0.8.0 → 0.10.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.
Files changed (109) hide show
  1. package/README.md +15 -1
  2. package/dist/{manager.utils.js → _event-target.js} +2 -41
  3. package/dist/_keyboard-event.js +75 -0
  4. package/dist/_match.js +48 -0
  5. package/dist/_named-keys.d.ts +6 -0
  6. package/dist/_named-keys.js +65 -0
  7. package/dist/_recorder-chord.js +42 -0
  8. package/dist/_recording-guard.js +55 -0
  9. package/dist/_registration.js +49 -0
  10. package/dist/conflicts.d.ts +36 -0
  11. package/dist/conflicts.js +54 -0
  12. package/dist/constants.d.ts +16 -144
  13. package/dist/constants.js +55 -172
  14. package/dist/display-labels.d.ts +90 -0
  15. package/dist/display-labels.js +112 -0
  16. package/dist/format.d.ts +48 -12
  17. package/dist/format.js +35 -39
  18. package/dist/hint.d.ts +20 -0
  19. package/dist/hint.js +23 -0
  20. package/dist/hotkey-manager.d.ts +14 -12
  21. package/dist/hotkey-manager.js +70 -21
  22. package/dist/hotkey-recorder.d.ts +8 -8
  23. package/dist/hotkey-recorder.js +74 -15
  24. package/dist/hotkey-sequence-recorder.d.ts +13 -9
  25. package/dist/hotkey-sequence-recorder.js +90 -28
  26. package/dist/{hotkey.d.ts → hotkey.types.d.ts} +58 -126
  27. package/dist/index.d.ts +14 -9
  28. package/dist/index.js +10 -6
  29. package/dist/key-state-tracker.d.ts +4 -7
  30. package/dist/key-state-tracker.js +30 -27
  31. package/dist/key.types.d.ts +79 -0
  32. package/dist/match.d.ts +24 -11
  33. package/dist/match.js +19 -30
  34. package/dist/parse.d.ts +18 -15
  35. package/dist/parse.js +39 -22
  36. package/dist/platform.d.ts +41 -0
  37. package/dist/platform.js +52 -0
  38. package/dist/recorder-options.d.ts +31 -0
  39. package/dist/sequence-manager.d.ts +27 -15
  40. package/dist/sequence-manager.js +61 -30
  41. package/dist/validate.d.ts +16 -8
  42. package/dist/validate.js +8 -13
  43. package/package.json +8 -13
  44. package/dist/constants.cjs +0 -535
  45. package/dist/constants.cjs.map +0 -1
  46. package/dist/constants.d.cts +0 -290
  47. package/dist/constants.js.map +0 -1
  48. package/dist/format.cjs +0 -91
  49. package/dist/format.cjs.map +0 -1
  50. package/dist/format.d.cts +0 -60
  51. package/dist/format.js.map +0 -1
  52. package/dist/hotkey-manager.cjs +0 -383
  53. package/dist/hotkey-manager.cjs.map +0 -1
  54. package/dist/hotkey-manager.d.cts +0 -230
  55. package/dist/hotkey-manager.js.map +0 -1
  56. package/dist/hotkey-recorder.cjs +0 -179
  57. package/dist/hotkey-recorder.cjs.map +0 -1
  58. package/dist/hotkey-recorder.d.cts +0 -116
  59. package/dist/hotkey-recorder.js.map +0 -1
  60. package/dist/hotkey-sequence-recorder.cjs +0 -192
  61. package/dist/hotkey-sequence-recorder.cjs.map +0 -1
  62. package/dist/hotkey-sequence-recorder.d.cts +0 -79
  63. package/dist/hotkey-sequence-recorder.js.map +0 -1
  64. package/dist/hotkey.d.cts +0 -301
  65. package/dist/index.cjs +0 -63
  66. package/dist/index.d.cts +0 -13
  67. package/dist/key-state-tracker.cjs +0 -197
  68. package/dist/key-state-tracker.cjs.map +0 -1
  69. package/dist/key-state-tracker.d.cts +0 -107
  70. package/dist/key-state-tracker.js.map +0 -1
  71. package/dist/manager.utils.cjs +0 -115
  72. package/dist/manager.utils.cjs.map +0 -1
  73. package/dist/manager.utils.d.cts +0 -13
  74. package/dist/manager.utils.d.ts +0 -13
  75. package/dist/manager.utils.js.map +0 -1
  76. package/dist/match.cjs +0 -153
  77. package/dist/match.cjs.map +0 -1
  78. package/dist/match.d.cts +0 -85
  79. package/dist/match.js.map +0 -1
  80. package/dist/parse.cjs +0 -228
  81. package/dist/parse.cjs.map +0 -1
  82. package/dist/parse.d.cts +0 -128
  83. package/dist/parse.js.map +0 -1
  84. package/dist/recorder-chord.cjs +0 -18
  85. package/dist/recorder-chord.cjs.map +0 -1
  86. package/dist/recorder-chord.js +0 -18
  87. package/dist/recorder-chord.js.map +0 -1
  88. package/dist/sequence-manager.cjs +0 -415
  89. package/dist/sequence-manager.cjs.map +0 -1
  90. package/dist/sequence-manager.d.cts +0 -157
  91. package/dist/sequence-manager.js.map +0 -1
  92. package/dist/validate.cjs +0 -116
  93. package/dist/validate.cjs.map +0 -1
  94. package/dist/validate.d.cts +0 -56
  95. package/dist/validate.js.map +0 -1
  96. package/src/constants.ts +0 -603
  97. package/src/format.ts +0 -142
  98. package/src/hotkey-manager.ts +0 -759
  99. package/src/hotkey-recorder.ts +0 -272
  100. package/src/hotkey-sequence-recorder.ts +0 -328
  101. package/src/hotkey.ts +0 -436
  102. package/src/index.ts +0 -11
  103. package/src/key-state-tracker.ts +0 -249
  104. package/src/manager.utils.ts +0 -231
  105. package/src/match.ts +0 -259
  106. package/src/parse.ts +0 -328
  107. package/src/recorder-chord.ts +0 -25
  108. package/src/sequence-manager.ts +0 -734
  109. package/src/validate.ts +0 -171
@@ -0,0 +1,112 @@
1
+ //#region src/display-labels.ts
2
+ /**
3
+ * Modifier key symbols for macOS display.
4
+ *
5
+ * Used by formatting functions to display hotkeys with macOS-style symbols
6
+ * (e.g., ⌘ for Command, ⌃ for Control) instead of text labels. This provides
7
+ * a native macOS look and feel in hotkey displays.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * MAC_MODIFIER_SYMBOLS['Meta'] // '⌘'
12
+ * MAC_MODIFIER_SYMBOLS['Control'] // '⌃'
13
+ * MAC_MODIFIER_SYMBOLS['Alt'] // '⌥'
14
+ * MAC_MODIFIER_SYMBOLS['Shift'] // '⇧'
15
+ * ```
16
+ */
17
+ const MAC_MODIFIER_SYMBOLS = {
18
+ Control: "⌃",
19
+ Alt: "⌥",
20
+ Shift: "⇧",
21
+ Meta: "⌘",
22
+ Mod: "⌘"
23
+ };
24
+ /**
25
+ * Modifier key labels for macOS display.
26
+ *
27
+ * Used by formatting functions to display hotkeys with macOS-style text labels
28
+ * (e.g., 'Control' for Control, 'Option' for Alt, 'Cmd' for Meta) instead of symbols.
29
+ * This provides a familiar macOS look and feel in hotkey displays.
30
+ *
31
+ * @example
32
+ * ```ts
33
+ * MAC_MODIFIER_LABELS['Control'] // 'control'
34
+ * MAC_MODIFIER_LABELS['Alt'] // 'option'
35
+ * MAC_MODIFIER_LABELS['Shift'] // 'shift'
36
+ * MAC_MODIFIER_LABELS['Meta'] // 'cmd'
37
+ * ```
38
+ */
39
+ const MAC_MODIFIER_LABELS = {
40
+ Control: "Control",
41
+ Alt: "Option",
42
+ Shift: "Shift",
43
+ Meta: "Cmd",
44
+ Mod: "Cmd"
45
+ };
46
+ /**
47
+ * Modifier key labels for Windows/Linux display.
48
+ *
49
+ * Used by formatting functions to display hotkeys with standard text labels
50
+ * (e.g., 'Ctrl' for Control, 'Win' for Meta/Windows key) instead of symbols.
51
+ * This provides a familiar Windows/Linux look and feel in hotkey displays.
52
+ *
53
+ * @example
54
+ * ```ts
55
+ * STANDARD_MODIFIER_LABELS['Control'] // 'Ctrl'
56
+ * STANDARD_MODIFIER_LABELS['Meta'] // 'Win'
57
+ * STANDARD_MODIFIER_LABELS['Alt'] // 'Alt'
58
+ * STANDARD_MODIFIER_LABELS['Shift'] // 'Shift'
59
+ * ```
60
+ */
61
+ const WINDOWS_MODIFIER_LABELS = {
62
+ Control: "Ctrl",
63
+ Alt: "Alt",
64
+ Shift: "Shift",
65
+ Meta: "Win",
66
+ Mod: "Ctrl"
67
+ };
68
+ const LINUX_MODIFIER_LABELS = {
69
+ ...WINDOWS_MODIFIER_LABELS,
70
+ Meta: "Super"
71
+ };
72
+ const PUNCTUATION_KEY_DISPLAY_LABELS = {
73
+ "`": "Backquote",
74
+ "\\": "Backslash",
75
+ "[": "Left Bracket",
76
+ "]": "Right Bracket",
77
+ ",": "Comma",
78
+ "=": "Equal",
79
+ "-": "Minus",
80
+ ".": "Period",
81
+ ";": "Semicolon"
82
+ };
83
+ /**
84
+ * Special key symbols for display formatting.
85
+ *
86
+ * Maps certain keys to their visual symbols for better readability in hotkey displays.
87
+ * Used by formatting functions to show symbols like ↑ for ArrowUp or ↵ for Enter
88
+ * instead of text labels.
89
+ *
90
+ * @example
91
+ * ```ts
92
+ * KEY_DISPLAY_SYMBOLS['ArrowUp'] // '↑'
93
+ * KEY_DISPLAY_SYMBOLS['Enter'] // '↵'
94
+ * KEY_DISPLAY_SYMBOLS['Escape'] // 'Esc'
95
+ * KEY_DISPLAY_SYMBOLS['Space'] // '␣'
96
+ * ```
97
+ */
98
+ const KEY_DISPLAY_SYMBOLS = {
99
+ ArrowUp: "↑",
100
+ ArrowDown: "↓",
101
+ ArrowLeft: "←",
102
+ ArrowRight: "→",
103
+ Enter: "↵",
104
+ Escape: "Esc",
105
+ Backspace: "⌫",
106
+ Delete: "⌦",
107
+ Tab: "⇥",
108
+ Space: "␣"
109
+ };
110
+
111
+ //#endregion
112
+ export { KEY_DISPLAY_SYMBOLS, LINUX_MODIFIER_LABELS, MAC_MODIFIER_LABELS, MAC_MODIFIER_SYMBOLS, PUNCTUATION_KEY_DISPLAY_LABELS, WINDOWS_MODIFIER_LABELS };
package/dist/format.d.ts CHANGED
@@ -1,8 +1,8 @@
1
- import { FormatDisplayOptions, Hotkey, ParsedHotkey, RegisterableHotkey } from "./hotkey.js";
2
-
1
+ import { Hotkey, ParsedHotkey, RegisterableHotkey } from "./hotkey.types.js";
3
2
  //#region src/format.d.ts
4
3
  /**
5
- * Converts a hotkey sequence array to a display string.
4
+ * Joins stored sequence steps with spaces, preserving physical code tokens.
5
+ * For user-facing labels, call formatForDisplay on each step instead.
6
6
  *
7
7
  * @param sequence - Array of hotkey strings that form the sequence
8
8
  * @returns A space-separated string (e.g. ['G','G'] → 'G G')
@@ -13,7 +13,7 @@ import { FormatDisplayOptions, Hotkey, ParsedHotkey, RegisterableHotkey } from "
13
13
  * formatHotkeySequence(['D', 'I', 'W']) // 'D I W'
14
14
  * ```
15
15
  */
16
- declare function formatHotkeySequence(sequence: Array<Hotkey>): string;
16
+ export declare function formatHotkeySequence(sequence: Array<Hotkey>): string;
17
17
  /**
18
18
  * Converts a ParsedHotkey back to a hotkey string.
19
19
  *
@@ -26,17 +26,22 @@ declare function formatHotkeySequence(sequence: Array<Hotkey>): string;
26
26
  * // Returns: 'Control+Shift+S'
27
27
  * ```
28
28
  */
29
- declare function formatHotkey(parsed: ParsedHotkey): string;
29
+ export declare function formatHotkey(parsed: ParsedHotkey): string;
30
+ export type DisplayHotkey = RegisterableHotkey | ParsedHotkey | (string & {});
30
31
  /**
31
32
  * Formats a hotkey for display in a user interface.
32
33
  *
33
34
  * On macOS, uses symbols (⌘⇧S) in the same modifier order as {@link normalizeHotkeyFromParsed}.
34
35
  * On Windows/Linux, uses text (Ctrl+Shift+S) with `+` separators.
35
36
  * The separator can be customized with `separatorToken`.
37
+ * Physical codes use conventional display labels: `[KeyS]` becomes `S` and
38
+ * `[Digit2]` becomes `2`. These labels do not infer the active keyboard layout
39
+ * or change matching. Supply a resolved `layoutMap` for layout-aware labels;
40
+ * `keyLabels` overrides both layout entries and fallback labels.
36
41
  *
37
- * @param hotkey - The hotkey string or ParsedHotkey to format
42
+ * @param hotkey - A logical or physical string, raw binding, or ParsedHotkey
38
43
  * @param options - Formatting options
39
- * @returns A formatted string suitable for display
44
+ * @returns Display text, or individual key labels when parts is true
40
45
  *
41
46
  * @example
42
47
  * ```ts
@@ -50,11 +55,42 @@ declare function formatHotkey(parsed: ParsedHotkey): string;
50
55
  * // Returns: 'Esc' (on all platforms)
51
56
  * ```
52
57
  */
53
- declare function formatForDisplay(hotkey: RegisterableHotkey | (string & {}), options?: FormatDisplayOptions): string;
58
+ export declare function formatForDisplay(hotkey: DisplayHotkey, options: FormatDisplayOptions & {
59
+ parts: true;
60
+ }): Array<string>;
61
+ export declare function formatForDisplay(hotkey: DisplayHotkey, options?: FormatDisplayOptions & {
62
+ parts?: false;
63
+ }): string;
64
+ export declare function formatForDisplay(hotkey: DisplayHotkey, options: FormatDisplayOptions): string | Array<string>;
54
65
  /**
55
66
  * @deprecated Use {@link formatForDisplay} instead with `useSymbols: false` option.
56
67
  */
57
- declare function formatWithLabels(hotkey: RegisterableHotkey, options?: Omit<FormatDisplayOptions, 'useSymbols'>): string;
58
- //#endregion
59
- export { formatForDisplay, formatHotkey, formatHotkeySequence, formatWithLabels };
60
- //# sourceMappingURL=format.d.ts.map
68
+ export declare function formatWithLabels(hotkey: RegisterableHotkey, options?: Omit<FormatDisplayOptions, 'useSymbols' | 'parts'>): string;
69
+ /**
70
+ * Options for formatting hotkeys for display.
71
+ */
72
+ export interface FormatDisplayOptions {
73
+ /** The target platform. Defaults to auto-detection. */
74
+ platform?: 'mac' | 'windows' | 'linux';
75
+ /** Whether to use symbols for the display. Defaults to true. */
76
+ useSymbols?: boolean | {
77
+ modifiers?: boolean;
78
+ keys?: boolean;
79
+ };
80
+ /** Return individual key labels instead of a joined string. Default: false. */
81
+ parts?: boolean;
82
+ /**
83
+ * An already-resolved code-to-key map, such as KeyboardLayoutMap or Map.
84
+ * Used only for physical bindings. Mapped keys receive normal display formatting;
85
+ * missing or empty entries use conventional fallback labels. The caller owns
86
+ * loading and refreshing the map; this formatter never calls browser APIs.
87
+ */
88
+ layoutMap?: {
89
+ get: (code: string) => string | undefined;
90
+ };
91
+ /** Final display overrides for logical keys or physical codes; take precedence over layoutMap. Never changes matching. */
92
+ keyLabels?: Readonly<Record<string, string>>;
93
+ /** Override the separator between display tokens. Defaults to platform-specific formatting when null/undefined. */
94
+ separatorToken?: string | null;
95
+ }
96
+ //#endregion
package/dist/format.js CHANGED
@@ -1,9 +1,13 @@
1
- import { KEY_DISPLAY_SYMBOLS, LINUX_MODIFIER_LABELS, MAC_MODIFIER_LABELS, MAC_MODIFIER_SYMBOLS, MODIFIER_ALIASES, MODIFIER_ORDER, PUNCTUATION_KEY_DISPLAY_LABELS, WINDOWS_MODIFIER_LABELS, detectPlatform } from "./constants.js";
2
- import { isModifierKey, normalizeRegisterableHotkey } from "./parse.js";
1
+ import { MODIFIER_ALIASES, MODIFIER_ORDER, PUNCTUATION_CODE_MAP, normalizeKeyName } from "./constants.js";
2
+ import { KEY_DISPLAY_SYMBOLS, LINUX_MODIFIER_LABELS, MAC_MODIFIER_LABELS, MAC_MODIFIER_SYMBOLS, PUNCTUATION_KEY_DISPLAY_LABELS, WINDOWS_MODIFIER_LABELS } from "./display-labels.js";
3
+ import { detectPlatform } from "./platform.js";
4
+ import { assertLogicalKey, splitHotkeyParts } from "./_keyboard-event.js";
5
+ import { isModifierKey, normalizeHotkeyFromParsed, normalizeRegisterableHotkey } from "./parse.js";
3
6
 
4
7
  //#region src/format.ts
5
8
  /**
6
- * Converts a hotkey sequence array to a display string.
9
+ * Joins stored sequence steps with spaces, preserving physical code tokens.
10
+ * For user-facing labels, call formatForDisplay on each step instead.
7
11
  *
8
12
  * @param sequence - Array of hotkey strings that form the sequence
9
13
  * @returns A space-separated string (e.g. ['G','G'] → 'G G')
@@ -30,48 +34,41 @@ function formatHotkeySequence(sequence) {
30
34
  * ```
31
35
  */
32
36
  function formatHotkey(parsed) {
37
+ if (parsed.code === void 0) assertLogicalKey(parsed.key);
33
38
  const parts = [];
34
39
  for (const modifier of MODIFIER_ORDER) if (parsed.modifiers.includes(modifier)) parts.push(modifier);
35
- parts.push(parsed.key);
40
+ parts.push(parsed.code !== void 0 ? `[${parsed.code}]` : parsed.key);
36
41
  return parts.join("+");
37
42
  }
38
- /**
39
- * Formats a hotkey for display in a user interface.
40
- *
41
- * On macOS, uses symbols (⌘⇧S) in the same modifier order as {@link normalizeHotkeyFromParsed}.
42
- * On Windows/Linux, uses text (Ctrl+Shift+S) with `+` separators.
43
- * The separator can be customized with `separatorToken`.
44
- *
45
- * @param hotkey - The hotkey string or ParsedHotkey to format
46
- * @param options - Formatting options
47
- * @returns A formatted string suitable for display
48
- *
49
- * @example
50
- * ```ts
51
- * formatForDisplay('Mod+Shift+S', { platform: 'mac' })
52
- * // Returns: '⌘ ⇧ S' (symbols separated by spaces on macOS)
53
- *
54
- * formatForDisplay('Mod+Shift+S', { platform: 'windows' })
55
- * // Returns: 'Ctrl+Shift+S'
56
- *
57
- * formatForDisplay('Escape')
58
- * // Returns: 'Esc' (on all platforms)
59
- * ```
60
- */
61
43
  function formatForDisplay(hotkey, options = {}) {
62
44
  const platform = options.platform ?? detectPlatform();
63
- const useSymbols = options.useSymbols ?? true;
64
- const separatorToken = options.separatorToken ?? (platform === "mac" && useSymbols ? " " : "+");
65
- return normalizeRegisterableHotkey(hotkey, platform).split("+").map((segment) => {
45
+ const symbols = options.useSymbols ?? true;
46
+ const modifierSymbols = typeof symbols === "boolean" ? symbols : symbols.modifiers ?? true;
47
+ const keySymbols = typeof symbols === "boolean" ? symbols : symbols.keys ?? true;
48
+ const separator = options.separatorToken ?? (platform === "mac" && modifierSymbols ? " " : "+");
49
+ const normalized = typeof hotkey === "object" && "modifiers" in hotkey ? normalizeHotkeyFromParsed(hotkey, platform) : normalizeRegisterableHotkey(hotkey, platform);
50
+ const parts = splitHotkeyParts(normalized).map((segment) => {
66
51
  if (isModifierKey(segment)) {
67
- const modifierToken = MODIFIER_ALIASES[segment] ?? MODIFIER_ALIASES[segment.toLowerCase()];
68
- return platform === "mac" ? useSymbols ? MAC_MODIFIER_SYMBOLS[modifierToken] : MAC_MODIFIER_LABELS[modifierToken] : platform === "windows" ? WINDOWS_MODIFIER_LABELS[modifierToken] : LINUX_MODIFIER_LABELS[modifierToken];
69
- } else {
70
- const keyDisplaySymbol = useSymbols && KEY_DISPLAY_SYMBOLS[segment];
71
- if (keyDisplaySymbol) return keyDisplaySymbol;
72
- return !useSymbols && PUNCTUATION_KEY_DISPLAY_LABELS[segment] || segment;
52
+ const modifier = MODIFIER_ALIASES[segment] ?? MODIFIER_ALIASES[segment.toLowerCase()];
53
+ return platform === "mac" ? modifierSymbols ? MAC_MODIFIER_SYMBOLS[modifier] : MAC_MODIFIER_LABELS[modifier] : platform === "windows" ? WINDOWS_MODIFIER_LABELS[modifier] : LINUX_MODIFIER_LABELS[modifier];
54
+ }
55
+ const code = /^\[([A-Za-z][A-Za-z0-9]*)\]$/.exec(segment)?.[1];
56
+ const label = options.keyLabels?.[code ?? segment];
57
+ if (label !== void 0) return label;
58
+ const numpad = code?.startsWith("Numpad") ?? false;
59
+ let displayKey = code ?? segment;
60
+ const mappedKey = code ? options.layoutMap?.get(code) : void 0;
61
+ if (mappedKey) displayKey = normalizeKeyName(mappedKey);
62
+ else if (code) {
63
+ if (/^Key[A-Z]$/.test(code)) displayKey = code.slice(3);
64
+ else if (/^Digit[0-9]$/.test(code)) displayKey = code.slice(5);
65
+ else if (numpad) displayKey = code.slice(6);
66
+ else displayKey = PUNCTUATION_CODE_MAP[code] ?? code;
73
67
  }
74
- }).join(separatorToken);
68
+ const display = (keySymbols ? KEY_DISPLAY_SYMBOLS : PUNCTUATION_KEY_DISPLAY_LABELS)[displayKey] ?? displayKey;
69
+ return numpad ? `Numpad ${display.replace(/([a-z])([A-Z])/g, "$1 $2")}` : display;
70
+ });
71
+ return options.parts ? parts : parts.join(separator);
75
72
  }
76
73
  /**
77
74
  * @deprecated Use {@link formatForDisplay} instead with `useSymbols: false` option.
@@ -84,5 +81,4 @@ function formatWithLabels(hotkey, options = {}) {
84
81
  }
85
82
 
86
83
  //#endregion
87
- export { formatForDisplay, formatHotkey, formatHotkeySequence, formatWithLabels };
88
- //# sourceMappingURL=format.js.map
84
+ export { formatForDisplay, formatHotkey, formatHotkeySequence, formatWithLabels };
package/dist/hint.d.ts ADDED
@@ -0,0 +1,20 @@
1
+ import { RegisterableHotkey } from "./hotkey.types.js";
2
+ //#region src/hint.d.ts
3
+ export interface HeldModifierOptions {
4
+ /** Resolve Mod for this platform. Defaults to platform detection. */
5
+ platform?: 'mac' | 'windows' | 'linux';
6
+ /** Require every binding modifier to be held. Default: false (nonempty subset). */
7
+ exact?: boolean;
8
+ }
9
+ /**
10
+ * Returns whether held modifiers should reveal a shortcut hint.
11
+ * By default, any nonempty subset of the binding's modifiers qualifies; an
12
+ * unrelated modifier hides the hint. `exact` requires every binding modifier.
13
+ * Nonmodifier keys are ignored. This does not check registration or focus.
14
+ *
15
+ * @example
16
+ * matchesHeldModifiers('Alt+Shift+[KeyK]', ['Alt']) // true
17
+ * matchesHeldModifiers('Alt+Shift+[KeyK]', ['Alt', 'Control']) // false
18
+ */
19
+ export declare function matchesHeldModifiers(hotkey: RegisterableHotkey, heldKeys: ReadonlyArray<string>, options?: HeldModifierOptions): boolean;
20
+ //#endregion
package/dist/hint.js ADDED
@@ -0,0 +1,23 @@
1
+ import { MODIFIER_KEYS, normalizeKeyName } from "./constants.js";
2
+ import { parseRegisterableHotkey } from "./parse.js";
3
+
4
+ //#region src/hint.ts
5
+ /**
6
+ * Returns whether held modifiers should reveal a shortcut hint.
7
+ * By default, any nonempty subset of the binding's modifiers qualifies; an
8
+ * unrelated modifier hides the hint. `exact` requires every binding modifier.
9
+ * Nonmodifier keys are ignored. This does not check registration or focus.
10
+ *
11
+ * @example
12
+ * matchesHeldModifiers('Alt+Shift+[KeyK]', ['Alt']) // true
13
+ * matchesHeldModifiers('Alt+Shift+[KeyK]', ['Alt', 'Control']) // false
14
+ */
15
+ function matchesHeldModifiers(hotkey, heldKeys, options = {}) {
16
+ if (heldKeys.includes("AltGraph")) return false;
17
+ const held = [...new Set(heldKeys.map(normalizeKeyName).filter((key) => MODIFIER_KEYS.has(key)))];
18
+ const { modifiers } = parseRegisterableHotkey(hotkey, options.platform);
19
+ return held.length > 0 && held.every((key) => modifiers.includes(key)) && (!options.exact || held.length === modifiers.length);
20
+ }
21
+
22
+ //#endregion
23
+ export { matchesHeldModifiers };
@@ -1,12 +1,10 @@
1
- import { Hotkey, HotkeyCallback, HotkeyMeta, ParsedHotkey, RegisterableHotkey } from "./hotkey.js";
2
- import { ConflictBehavior } from "./manager.utils.js";
1
+ import { ConflictBehavior, Hotkey, HotkeyCallback, HotkeyMeta, ParsedHotkey, RegisterableHotkey } from "./hotkey.types.js";
3
2
  import { Store } from "@tanstack/store";
4
-
5
3
  //#region src/hotkey-manager.d.ts
6
4
  /**
7
5
  * Options for registering a hotkey.
8
6
  */
9
- interface HotkeyOptions {
7
+ export interface HotkeyOptions {
10
8
  /** Behavior when this hotkey conflicts with an existing registration on the same target. Defaults to 'warn' */
11
9
  conflictBehavior?: ConflictBehavior;
12
10
  /**
@@ -35,11 +33,16 @@ interface HotkeyOptions {
35
33
  /**
36
34
  * A registered hotkey handler in the HotkeyManager.
37
35
  */
38
- interface HotkeyRegistration {
36
+ export interface HotkeyRegistration {
39
37
  /** The callback to invoke */
40
38
  callback: HotkeyCallback;
41
39
  /** Whether this registration has fired and needs reset (for requireReset) */
42
40
  hasFired: boolean;
41
+ /** The concrete key/code that activated a requireReset registration. */
42
+ activeMatch?: {
43
+ key: string;
44
+ code: string;
45
+ };
43
46
  /** The original hotkey string */
44
47
  hotkey: Hotkey;
45
48
  /** Unique identifier for this registration */
@@ -57,7 +60,7 @@ interface HotkeyRegistration {
57
60
  * Public view of a hotkey registration for display and introspection.
58
61
  * Omits the callback function which is an internal implementation detail.
59
62
  */
60
- interface HotkeyRegistrationView {
63
+ export interface HotkeyRegistrationView {
61
64
  /** The original hotkey string */
62
65
  hotkey: Hotkey;
63
66
  /** Unique identifier for this registration */
@@ -77,7 +80,7 @@ interface HotkeyRegistrationView {
77
80
  * Creates a public view from an internal registration,
78
81
  * stripping the callback function.
79
82
  */
80
- declare function toHotkeyRegistrationView(reg: HotkeyRegistration): HotkeyRegistrationView;
83
+ export declare function toHotkeyRegistrationView(reg: HotkeyRegistration): HotkeyRegistrationView;
81
84
  /**
82
85
  * A handle returned from HotkeyManager.register() that allows updating
83
86
  * the callback and options without re-registering the hotkey.
@@ -101,7 +104,7 @@ declare function toHotkeyRegistrationView(reg: HotkeyRegistration): HotkeyRegist
101
104
  * handle.unregister()
102
105
  * ```
103
106
  */
104
- interface HotkeyRegistrationHandle {
107
+ export interface HotkeyRegistrationHandle {
105
108
  /**
106
109
  * The callback function. Can be set directly to update without re-registering.
107
110
  * This avoids stale closures when the callback references React state.
@@ -138,7 +141,7 @@ interface HotkeyRegistrationHandle {
138
141
  * unregister()
139
142
  * ```
140
143
  */
141
- declare class HotkeyManager {
144
+ export declare class HotkeyManager {
142
145
  #private;
143
146
  /**
144
147
  * The TanStack Store containing all hotkey registrations.
@@ -224,7 +227,6 @@ declare class HotkeyManager {
224
227
  * Gets the singleton HotkeyManager instance.
225
228
  * Convenience function for accessing the manager.
226
229
  */
227
- declare function getHotkeyManager(): HotkeyManager;
230
+ export declare function getHotkeyManager(): HotkeyManager;
228
231
  //#endregion
229
- export { HotkeyManager, HotkeyOptions, HotkeyRegistration, HotkeyRegistrationHandle, HotkeyRegistrationView, getHotkeyManager, toHotkeyRegistrationView };
230
- //# sourceMappingURL=hotkey-manager.d.ts.map
232
+ export type { ConflictBehavior };
@@ -1,8 +1,12 @@
1
- import { detectPlatform, normalizeKeyName } from "./constants.js";
1
+ import { normalizeKeyName } from "./constants.js";
2
+ import { detectPlatform } from "./platform.js";
2
3
  import { parseHotkey, rawHotkeyToParsedHotkey } from "./parse.js";
3
4
  import { formatHotkey } from "./format.js";
4
- import { matchesKeyboardEvent } from "./match.js";
5
- import { defaultHotkeyOptions, getDefaultIgnoreInputs, handleConflict, isEventForTarget, shouldIgnoreInputEvent } from "./manager.utils.js";
5
+ import { matchKeyboardEvent } from "./_match.js";
6
+ import { areHotkeysEqual } from "./match.js";
7
+ import { isRecordingEvent } from "./_recording-guard.js";
8
+ import { defaultHotkeyOptions, getDefaultIgnoreInputs, handleConflict, optionsEqual } from "./_registration.js";
9
+ import { isEventForTarget, shouldIgnoreInputEvent } from "./_event-target.js";
6
10
  import { Store } from "@tanstack/store";
7
11
 
8
12
  //#region src/hotkey-manager.ts
@@ -49,11 +53,30 @@ function generateId() {
49
53
  */
50
54
  var HotkeyManager = class HotkeyManager {
51
55
  static #instance = null;
56
+ /**
57
+ * The TanStack Store containing all hotkey registrations.
58
+ * Use this to subscribe to registration changes or access current registrations.
59
+ *
60
+ * @example
61
+ * ```ts
62
+ * const manager = HotkeyManager.getInstance()
63
+ *
64
+ * // Subscribe to registration changes
65
+ * const unsubscribe = manager.registrations.subscribe(() => {
66
+ * console.log('Registrations changed:', manager.registrations.state.size)
67
+ * })
68
+ *
69
+ * // Access current registrations
70
+ * for (const [id, reg] of manager.registrations.state) {
71
+ * console.log(reg.hotkey, reg.options.enabled)
72
+ * }
73
+ * ```
74
+ */
75
+ registrations = new Store(/* @__PURE__ */ new Map());
52
76
  #platform;
53
77
  #targetListeners = /* @__PURE__ */ new Map();
54
78
  #targetRegistrations = /* @__PURE__ */ new Map();
55
79
  constructor() {
56
- this.registrations = new Store(/* @__PURE__ */ new Map());
57
80
  this.#platform = detectPlatform();
58
81
  }
59
82
  /**
@@ -162,7 +185,10 @@ var HotkeyManager = class HotkeyManager {
162
185
  setOptions: (newOptions) => {
163
186
  manager.registrations.setState((prev) => {
164
187
  const reg = prev.get(id);
165
- if (reg) {
188
+ if (reg && !optionsEqual(reg.options, {
189
+ ...reg.options,
190
+ ...newOptions
191
+ })) {
166
192
  const next = new Map(prev);
167
193
  next.set(id, {
168
194
  ...reg,
@@ -230,8 +256,27 @@ var HotkeyManager = class HotkeyManager {
230
256
  * Processes keyboard events for a specific target and event type.
231
257
  */
232
258
  #processTargetEvent(event, target, eventType) {
259
+ if (eventType === "keyup") for (const id of this.#targetRegistrations.get(target) ?? []) {
260
+ const registration = this.registrations.state.get(id);
261
+ if (registration?.hasFired && this.#shouldResetRegistration(registration, event)) {
262
+ registration.hasFired = false;
263
+ registration.activeMatch = void 0;
264
+ }
265
+ }
266
+ if (isRecordingEvent(event)) return;
233
267
  const targetRegs = this.#targetRegistrations.get(target);
234
268
  if (!targetRegs) return;
269
+ const matches = /* @__PURE__ */ new Map();
270
+ let bestScore = 0;
271
+ for (const id of targetRegs) {
272
+ const registration = this.registrations.state.get(id);
273
+ if (!registration || !registration.options.enabled || registration.options.eventType !== eventType || !isEventForTarget(event, target) || registration.options.ignoreInputs !== false && shouldIgnoreInputEvent(event, target, registration.target)) continue;
274
+ const result = matchKeyboardEvent(event, registration.parsedHotkey, registration.options.platform);
275
+ if (result.matched) {
276
+ matches.set(id, result);
277
+ bestScore = Math.max(bestScore, result.score);
278
+ }
279
+ }
235
280
  for (const id of targetRegs) {
236
281
  const registration = this.registrations.state.get(id);
237
282
  if (!registration) continue;
@@ -242,21 +287,21 @@ var HotkeyManager = class HotkeyManager {
242
287
  }
243
288
  if (eventType === "keydown") {
244
289
  if (registration.options.eventType !== "keydown") continue;
245
- if (matchesKeyboardEvent(event, registration.parsedHotkey, registration.options.platform)) {
290
+ const match = matches.get(id);
291
+ if (match?.matched && match.score === bestScore) {
246
292
  if (registration.options.preventDefault) event.preventDefault();
247
293
  if (registration.options.stopPropagation) event.stopPropagation();
248
294
  if (!registration.options.requireReset || !registration.hasFired) {
249
295
  this.#executeHotkeyCallback(registration, event);
250
- if (registration.options.requireReset) registration.hasFired = true;
296
+ if (registration.options.requireReset) {
297
+ registration.hasFired = true;
298
+ registration.activeMatch = match.identity;
299
+ }
251
300
  }
252
301
  }
253
- } else {
254
- if (registration.options.eventType === "keyup") {
255
- if (matchesKeyboardEvent(event, registration.parsedHotkey, registration.options.platform)) this.#executeHotkeyCallback(registration, event);
256
- }
257
- if (registration.options.requireReset && registration.hasFired) {
258
- if (this.#shouldResetRegistration(registration, event)) registration.hasFired = false;
259
- }
302
+ } else if (registration.options.eventType === "keyup") {
303
+ const match = matches.get(id);
304
+ if (match?.matched && match.score === bestScore) this.#executeHotkeyCallback(registration, event);
260
305
  }
261
306
  }
262
307
  }
@@ -294,7 +339,7 @@ var HotkeyManager = class HotkeyManager {
294
339
  * Finds an existing registration with the same hotkey and target.
295
340
  */
296
341
  #findConflictingRegistration(hotkey, target) {
297
- for (const registration of this.registrations.state.values()) if (registration.hotkey === hotkey && registration.target === target) return registration;
342
+ for (const registration of this.registrations.state.values()) if (areHotkeysEqual(registration.hotkey, hotkey, registration.options.platform) && registration.target === target) return registration;
298
343
  return null;
299
344
  }
300
345
  /**
@@ -303,8 +348,13 @@ var HotkeyManager = class HotkeyManager {
303
348
  #shouldResetRegistration(registration, event) {
304
349
  const parsed = registration.parsedHotkey;
305
350
  const releasedKey = normalizeKeyName(event.key);
306
- const parsedKeyNormalized = parsed.key.length === 1 ? parsed.key.toUpperCase() : parsed.key;
307
- if ((releasedKey.length === 1 ? releasedKey.toUpperCase() : releasedKey) === parsedKeyNormalized) return true;
351
+ if (registration.activeMatch?.code && event.code === registration.activeMatch.code) return true;
352
+ if (parsed.code !== void 0) {
353
+ if (event.code === parsed.code) return true;
354
+ } else {
355
+ const parsedKeyNormalized = parsed.key.length === 1 ? parsed.key.toUpperCase() : parsed.key;
356
+ if ((releasedKey.length === 1 ? releasedKey.toUpperCase() : releasedKey) === parsedKeyNormalized) return true;
357
+ }
308
358
  if (parsed.ctrl && releasedKey === "Control") return true;
309
359
  if (parsed.shift && releasedKey === "Shift") return true;
310
360
  if (parsed.alt && releasedKey === "Alt") return true;
@@ -323,7 +373,7 @@ var HotkeyManager = class HotkeyManager {
323
373
  if (!registration) return false;
324
374
  const parsed = registration.parsedHotkey;
325
375
  const syntheticEvent = new KeyboardEvent(registration.options.eventType ?? "keydown", {
326
- key: parsed.key,
376
+ ...parsed.code !== void 0 ? { code: parsed.code } : { key: parsed.key },
327
377
  ctrlKey: parsed.ctrl,
328
378
  shiftKey: parsed.shift,
329
379
  altKey: parsed.alt,
@@ -353,7 +403,7 @@ var HotkeyManager = class HotkeyManager {
353
403
  * @returns True if a matching registration exists
354
404
  */
355
405
  isRegistered(hotkey, target) {
356
- for (const registration of this.registrations.state.values()) if (registration.hotkey === hotkey) {
406
+ for (const registration of this.registrations.state.values()) if (areHotkeysEqual(registration.hotkey, hotkey, registration.options.platform)) {
357
407
  if (target === void 0 || registration.target === target) return true;
358
408
  }
359
409
  return false;
@@ -377,5 +427,4 @@ function getHotkeyManager() {
377
427
  }
378
428
 
379
429
  //#endregion
380
- export { HotkeyManager, getHotkeyManager, toHotkeyRegistrationView };
381
- //# sourceMappingURL=hotkey-manager.js.map
430
+ export { HotkeyManager, getHotkeyManager, toHotkeyRegistrationView };
@@ -1,11 +1,11 @@
1
- import { Hotkey } from "./hotkey.js";
1
+ import { Hotkey } from "./hotkey.types.js";
2
+ import { HotkeyRecorderValidationContext, RecorderOptions } from "./recorder-options.js";
2
3
  import { Store } from "@tanstack/store";
3
-
4
4
  //#region src/hotkey-recorder.d.ts
5
5
  /**
6
6
  * State interface for the HotkeyRecorder.
7
7
  */
8
- interface HotkeyRecorderState {
8
+ export interface HotkeyRecorderState {
9
9
  /** Whether recording is currently active */
10
10
  isRecording: boolean;
11
11
  /** The currently recorded hotkey (for live preview) */
@@ -14,7 +14,9 @@ interface HotkeyRecorderState {
14
14
  /**
15
15
  * Options for configuring a HotkeyRecorder instance.
16
16
  */
17
- interface HotkeyRecorderOptions {
17
+ export interface HotkeyRecorderOptions extends RecorderOptions {
18
+ /** Return true to accept, or false/a message to reject while staying in recording mode. */
19
+ validate?: (hotkey: Hotkey, context: HotkeyRecorderValidationContext) => boolean | string;
18
20
  /** Callback when a hotkey is successfully recorded */
19
21
  onRecord: (hotkey: Hotkey) => void;
20
22
  /** Optional callback when recording is cancelled (Escape pressed) */
@@ -69,7 +71,7 @@ interface HotkeyRecorderOptions {
69
71
  * unsubscribe()
70
72
  * ```
71
73
  */
72
- declare class HotkeyRecorder {
74
+ export declare class HotkeyRecorder {
73
75
  #private;
74
76
  /**
75
77
  * The TanStack Store instance containing the recorder state.
@@ -111,6 +113,4 @@ declare class HotkeyRecorder {
111
113
  */
112
114
  destroy(): void;
113
115
  }
114
- //#endregion
115
- export { HotkeyRecorder, HotkeyRecorderOptions, HotkeyRecorderState };
116
- //# sourceMappingURL=hotkey-recorder.d.ts.map
116
+ //#endregion