@tanstack/hotkeys 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -67,6 +67,9 @@ Type-safe keyboard shortcuts for the web. Template-string bindings, parsed objec
67
67
  > [!NOTE]
68
68
  > You may know **TanStack Hotkeys** by our adapter names, too!
69
69
  >
70
+ > - [**Alpine Hotkeys**](https://tanstack.com/hotkeys/latest/docs/framework/alpine/quick-start)
71
+ > - [**Ember Hotkeys**](https://tanstack.com/hotkeys/latest/docs/framework/ember/quick-start)
72
+ > - [**Octane Hotkeys**](https://tanstack.com/hotkeys/latest/docs/framework/octane/quick-start)
70
73
  > - [**React Hotkeys**](https://tanstack.com/hotkeys/latest/docs/framework/react/react-hotkeys)
71
74
  > - [**Preact Hotkeys**](https://tanstack.com/hotkeys/latest/docs/framework/preact/preact-hotkeys)
72
75
  > - [**Solid Hotkeys**](https://tanstack.com/hotkeys/latest/docs/framework/solid/reference)
@@ -0,0 +1,27 @@
1
+ import { HotkeyCallback, RegisterableHotkey } from "./hotkey.types.js";
2
+ import { HotkeyOptions } from "./hotkey-manager.js";
3
+ import { HotkeySequence, SequenceOptions } from "./sequence-manager.js";
4
+ //#region src/adapter.d.ts
5
+ /** A hotkey and its callback, with optional per-binding options. */
6
+ export interface HotkeyDefinition {
7
+ hotkey: RegisterableHotkey;
8
+ callback: HotkeyCallback;
9
+ options?: HotkeyOptions;
10
+ }
11
+ /** A sequence and its callback, with optional per-binding options. */
12
+ export interface HotkeySequenceDefinition {
13
+ sequence: HotkeySequence;
14
+ callback: HotkeyCallback;
15
+ options?: SequenceOptions;
16
+ }
17
+ /** Reconciles adapter registrations while retaining handles for option updates. */
18
+ export declare function createHotkeyBindings(): {
19
+ update(definitions: Array<HotkeyDefinition>, commonOptions?: HotkeyOptions): void;
20
+ destroy: () => void;
21
+ };
22
+ /** Reconciles sequence bindings; empty sequences and explicit null targets are skipped. */
23
+ export declare function createHotkeySequenceBindings(): {
24
+ update(definitions: Array<HotkeySequenceDefinition>, commonOptions?: SequenceOptions): void;
25
+ destroy: () => void;
26
+ };
27
+ //#endregion
@@ -0,0 +1,109 @@
1
+ import { detectPlatform } from "./platform.js";
2
+ import { normalizeRegisterableHotkey, parseHotkey } from "./parse.js";
3
+ import { defaultHotkeyOptions, getDefaultIgnoreInputs } from "./_registration.js";
4
+ import { getHotkeyManager } from "./hotkey-manager.js";
5
+ import { DEFAULT_SEQUENCE_TIMEOUT, getSequenceManager } from "./sequence-manager.js";
6
+
7
+ //#region src/adapter.ts
8
+ function createBindings() {
9
+ const registrations = /* @__PURE__ */ new Map();
10
+ return {
11
+ update(bindings) {
12
+ const keys = new Set(bindings.map((binding) => binding.key));
13
+ for (const [key, registration] of registrations) if (!keys.has(key)) {
14
+ registration.handle.unregister();
15
+ registrations.delete(key);
16
+ }
17
+ for (const binding of bindings) {
18
+ const previous = registrations.get(binding.key);
19
+ const { target, platform } = binding.options;
20
+ if (previous?.handle.isActive && previous.target === target && previous.platform === platform) {
21
+ previous.handle.callback = binding.callback;
22
+ const reset = Object.fromEntries(Object.keys(previous.options).map((key) => [key, void 0]));
23
+ previous.handle.setOptions({
24
+ ...reset,
25
+ ...binding.options
26
+ });
27
+ previous.options = binding.options;
28
+ } else {
29
+ previous?.handle.unregister();
30
+ registrations.set(binding.key, {
31
+ handle: binding.register(),
32
+ target,
33
+ platform,
34
+ options: binding.options
35
+ });
36
+ }
37
+ }
38
+ },
39
+ destroy() {
40
+ for (const { handle } of registrations.values()) handle.unregister();
41
+ registrations.clear();
42
+ }
43
+ };
44
+ }
45
+ function resolveOptions(options) {
46
+ return {
47
+ ...defaultHotkeyOptions,
48
+ requireReset: false,
49
+ ...options,
50
+ ignoreInputs: options.ignoreInputs,
51
+ target: "target" in options ? options.target ?? null : typeof document === "undefined" ? null : document,
52
+ platform: options.platform ?? detectPlatform()
53
+ };
54
+ }
55
+ /** Reconciles adapter registrations while retaining handles for option updates. */
56
+ function createHotkeyBindings() {
57
+ const bindings = createBindings();
58
+ return {
59
+ update(definitions, commonOptions = {}) {
60
+ const prepared = [];
61
+ definitions.forEach((definition, index) => {
62
+ const options = resolveOptions({
63
+ ...commonOptions,
64
+ ...definition.options
65
+ });
66
+ if (!options.target) return;
67
+ const hotkey = normalizeRegisterableHotkey(definition.hotkey, options.platform);
68
+ options.ignoreInputs ??= getDefaultIgnoreInputs(parseHotkey(hotkey, options.platform));
69
+ prepared.push({
70
+ key: `${index}:${hotkey}`,
71
+ callback: definition.callback,
72
+ options,
73
+ register: () => getHotkeyManager().register(hotkey, definition.callback, options)
74
+ });
75
+ });
76
+ bindings.update(prepared);
77
+ },
78
+ destroy: bindings.destroy
79
+ };
80
+ }
81
+ /** Reconciles sequence bindings; empty sequences and explicit null targets are skipped. */
82
+ function createHotkeySequenceBindings() {
83
+ const bindings = createBindings();
84
+ return {
85
+ update(definitions, commonOptions = {}) {
86
+ const prepared = [];
87
+ definitions.forEach((definition, index) => {
88
+ const options = resolveOptions({
89
+ timeout: DEFAULT_SEQUENCE_TIMEOUT,
90
+ ...commonOptions,
91
+ ...definition.options
92
+ });
93
+ if (!options.target || definition.sequence.length === 0) return;
94
+ options.ignoreInputs ??= getDefaultIgnoreInputs(parseHotkey(definition.sequence[0], options.platform));
95
+ prepared.push({
96
+ key: `${index}:${JSON.stringify(definition.sequence)}`,
97
+ callback: definition.callback,
98
+ options,
99
+ register: () => getSequenceManager().register(definition.sequence, definition.callback, options)
100
+ });
101
+ });
102
+ bindings.update(prepared);
103
+ },
104
+ destroy: bindings.destroy
105
+ };
106
+ }
107
+
108
+ //#endregion
109
+ export { createHotkeyBindings, createHotkeySequenceBindings };
@@ -118,7 +118,7 @@ export declare const PUNCTUATION_CODE_MAP: Record<string, string>;
118
118
  * @see {@link EDITING_KEYS}
119
119
  * @see {@link PUNCTUATION_KEYS}
120
120
  */
121
- export declare const ALL_KEYS: Set<LetterKey | NumberKey | FunctionKey | NavigationKey | EditingKey | PunctuationKey | "CapsLock" | "ContextMenu" | "Convert" | "KanaMode" | "NonConvert" | "Help" | "Insert" | "NumLock" | "Fn" | "FnLock" | "PrintScreen" | "ScrollLock" | "Pause" | "BrowserBack" | "BrowserFavorites" | "BrowserForward" | "BrowserHome" | "BrowserRefresh" | "BrowserSearch" | "BrowserStop" | "Eject" | "LaunchMail" | "MediaPlayPause" | "MediaStop" | "MediaTrackNext" | "MediaTrackPrevious" | "Power" | "AudioVolumeDown" | "AudioVolumeMute" | "AudioVolumeUp" | "WakeUp" | "Hyper" | "Super" | "Again" | "Copy" | "Cut" | "Find" | "Open" | "Paste" | "Props" | "Select" | "Undo" | "Hiragana" | "Katakana" | "Clear" | "Cancel" | "Standby" | "LaunchApplication1" | "LaunchApplication2" | "LaunchMediaPlayer" | "HangulMode" | "HanjaMode" | "Zenkaku" | "Hankaku" | "ZenkakuHankaku">;
121
+ export declare const ALL_KEYS: Set<"CapsLock" | "ContextMenu" | "Convert" | "KanaMode" | "NonConvert" | "Help" | "Insert" | "NumLock" | "Fn" | "FnLock" | "PrintScreen" | "ScrollLock" | "Pause" | "BrowserBack" | "BrowserFavorites" | "BrowserForward" | "BrowserHome" | "BrowserRefresh" | "BrowserSearch" | "BrowserStop" | "Eject" | "LaunchMail" | "MediaPlayPause" | "MediaStop" | "MediaTrackNext" | "MediaTrackPrevious" | "Power" | "AudioVolumeDown" | "AudioVolumeMute" | "AudioVolumeUp" | "WakeUp" | "Hyper" | "Super" | "Again" | "Copy" | "Cut" | "Find" | "Open" | "Paste" | "Props" | "Select" | "Undo" | "Hiragana" | "Katakana" | "Clear" | "Cancel" | "Standby" | "LaunchApplication1" | "LaunchApplication2" | "LaunchMediaPlayer" | "HangulMode" | "HanjaMode" | "Zenkaku" | "Hankaku" | "ZenkakuHankaku" | LetterKey | NumberKey | FunctionKey | NavigationKey | EditingKey | PunctuationKey>;
122
122
  /**
123
123
  * Normalizes a key name to its canonical form.
124
124
  *
package/dist/format.d.ts CHANGED
@@ -31,7 +31,8 @@ export type DisplayHotkey = RegisterableHotkey | ParsedHotkey | (string & {});
31
31
  /**
32
32
  * Formats a hotkey for display in a user interface.
33
33
  *
34
- * On macOS, uses symbols (⌘⇧S) in the same modifier order as {@link normalizeHotkeyFromParsed}.
34
+ * On macOS, uses symbols (⇧⌘S) in Control, Option, Shift, Command order.
35
+ * Display ordering does not change the normalized hotkey used for registration.
35
36
  * On Windows/Linux, uses text (Ctrl+Shift+S) with `+` separators.
36
37
  * The separator can be customized with `separatorToken`.
37
38
  * Physical codes use conventional display labels: `[KeyS]` becomes `S` and
@@ -46,7 +47,7 @@ export type DisplayHotkey = RegisterableHotkey | ParsedHotkey | (string & {});
46
47
  * @example
47
48
  * ```ts
48
49
  * formatForDisplay('Mod+Shift+S', { platform: 'mac' })
49
- * // Returns: '⌘ ⇧ S' (symbols separated by spaces on macOS)
50
+ * // Returns: '⇧ ⌘ S' (symbols separated by spaces on macOS)
50
51
  *
51
52
  * formatForDisplay('Mod+Shift+S', { platform: 'windows' })
52
53
  * // Returns: 'Ctrl+Shift+S'
package/dist/format.js CHANGED
@@ -47,7 +47,15 @@ function formatForDisplay(hotkey, options = {}) {
47
47
  const keySymbols = typeof symbols === "boolean" ? symbols : symbols.keys ?? true;
48
48
  const separator = options.separatorToken ?? (platform === "mac" && modifierSymbols ? " " : "+");
49
49
  const normalized = typeof hotkey === "object" && "modifiers" in hotkey ? normalizeHotkeyFromParsed(hotkey, platform) : normalizeRegisterableHotkey(hotkey, platform);
50
- const parts = splitHotkeyParts(normalized).map((segment) => {
50
+ const segments = splitHotkeyParts(normalized);
51
+ if (platform === "mac") {
52
+ const modifierIndex = (segment) => {
53
+ const index = MODIFIER_ORDER.indexOf(segment === "Mod" ? "Meta" : segment);
54
+ return index === -1 ? MODIFIER_ORDER.length : index;
55
+ };
56
+ segments.sort((a, b) => modifierIndex(a) - modifierIndex(b));
57
+ }
58
+ const parts = segments.map((segment) => {
51
59
  if (isModifierKey(segment)) {
52
60
  const modifier = MODIFIER_ALIASES[segment] ?? MODIFIER_ALIASES[segment.toLowerCase()];
53
61
  return platform === "mac" ? modifierSymbols ? MAC_MODIFIER_SYMBOLS[modifier] : MAC_MODIFIER_LABELS[modifier] : platform === "windows" ? WINDOWS_MODIFIER_LABELS[modifier] : LINUX_MODIFIER_LABELS[modifier];
@@ -78,7 +78,8 @@ export declare class HotkeyRecorder {
78
78
  * Use this to subscribe to state changes or access current state.
79
79
  */
80
80
  readonly store: Store<HotkeyRecorderState>;
81
- constructor(options: HotkeyRecorderOptions);
81
+ /** Options may be a getter to read current configuration during an active session. */
82
+ constructor(options: HotkeyRecorderOptions | (() => HotkeyRecorderOptions));
82
83
  /**
83
84
  * Updates the recorder options, including callbacks.
84
85
  * This allows framework adapters to sync callback changes without recreating the recorder.
@@ -58,10 +58,18 @@ var HotkeyRecorder = class {
58
58
  recordedHotkey: null
59
59
  });
60
60
  #keydownHandler = null;
61
- #options;
61
+ #optionsSource;
62
+ #optionOverrides = {};
63
+ get #options() {
64
+ return {
65
+ ...typeof this.#optionsSource === "function" ? this.#optionsSource() : this.#optionsSource,
66
+ ...this.#optionOverrides
67
+ };
68
+ }
62
69
  #platform;
70
+ /** Options may be a getter to read current configuration during an active session. */
63
71
  constructor(options) {
64
- this.#options = options;
72
+ this.#optionsSource = options;
65
73
  this.#platform = detectPlatform();
66
74
  }
67
75
  /**
@@ -69,8 +77,8 @@ var HotkeyRecorder = class {
69
77
  * This allows framework adapters to sync callback changes without recreating the recorder.
70
78
  */
71
79
  setOptions(options) {
72
- this.#options = {
73
- ...this.#options,
80
+ this.#optionOverrides = {
81
+ ...this.#optionOverrides,
74
82
  ...options
75
83
  };
76
84
  }
@@ -65,7 +65,8 @@ export interface HotkeySequenceRecorderOptions extends RecorderOptions {
65
65
  export declare class HotkeySequenceRecorder {
66
66
  #private;
67
67
  readonly store: Store<HotkeySequenceRecorderState>;
68
- constructor(options: HotkeySequenceRecorderOptions);
68
+ /** Options may be a getter to read current configuration during an active session. */
69
+ constructor(options: HotkeySequenceRecorderOptions | (() => HotkeySequenceRecorderOptions));
69
70
  /** Merges current callbacks and options without discarding recorded steps. */
70
71
  setOptions(options: Partial<HotkeySequenceRecorderOptions>): void;
71
72
  /** Starts a fresh recording; repeated starts during an active session are ignored. */
@@ -32,22 +32,27 @@ var HotkeySequenceRecorder = class {
32
32
  recordedSequence: null
33
33
  });
34
34
  #keydownHandler = null;
35
- #options;
35
+ #optionsSource;
36
+ #optionOverrides = {};
37
+ get #options() {
38
+ return {
39
+ ...defaultHotkeySequenceRecorderOptions,
40
+ ...typeof this.#optionsSource === "function" ? this.#optionsSource() : this.#optionsSource,
41
+ ...this.#optionOverrides
42
+ };
43
+ }
36
44
  #platform;
37
45
  #events = [];
38
46
  #idleTimer = null;
47
+ /** Options may be a getter to read current configuration during an active session. */
39
48
  constructor(options) {
40
- this.#options = {
41
- ...defaultHotkeySequenceRecorderOptions,
42
- ...options
43
- };
49
+ this.#optionsSource = options;
44
50
  this.#platform = detectPlatform();
45
51
  }
46
52
  /** Merges current callbacks and options without discarding recorded steps. */
47
53
  setOptions(options) {
48
- this.#options = {
49
- ...defaultHotkeySequenceRecorderOptions,
50
- ...this.#options,
54
+ this.#optionOverrides = {
55
+ ...this.#optionOverrides,
51
56
  ...options
52
57
  };
53
58
  }
package/dist/index.d.ts CHANGED
@@ -1,15 +1,15 @@
1
1
  import { CanonicalModifier, EditingKey, FunctionKey, IndividualKey, Key, LetterKey, LogicalKey, Modifier, NamedKey, NavigationKey, NonPunctuationKey, NormalizedKeyboardEvent, NumberKey, PhysicalKey, PhysicalKeyCode, PunctuationKey } from "./key.types.js";
2
+ import { ConflictBehavior, FourModifierHotkey, Hotkey, HotkeyCallback, HotkeyCallbackContext, HotkeyMeta, ParsedHotkey, ParsedModifiers, RawHotkey, RawModifiers, RegisterableHotkey, SingleModifierHotkey, ThreeModifierHotkey, TwoModifierHotkey } from "./hotkey.types.js";
3
+ import { HotkeyManager, HotkeyOptions, HotkeyRegistration, HotkeyRegistrationHandle, HotkeyRegistrationView, getHotkeyManager, toHotkeyRegistrationView } from "./hotkey-manager.js";
4
+ import { DEFAULT_SEQUENCE_TIMEOUT, HotkeySequence, SequenceManager, SequenceOptions, SequenceRegistration, SequenceRegistrationHandle, SequenceRegistrationView, Target, createSequenceMatcher, getSequenceManager } from "./sequence-manager.js";
2
5
  import { ALL_KEYS, EDITING_KEYS, FUNCTION_KEYS, LETTER_KEYS, MODIFIER_ALIASES, MODIFIER_KEYS, MODIFIER_ORDER, NAVIGATION_KEYS, NUMBER_KEYS, PUNCTUATION_CODE_MAP, PUNCTUATION_KEYS, isSingleLetterKey, normalizeKeyName } from "./constants.js";
3
6
  import { KEY_DISPLAY_SYMBOLS, LINUX_MODIFIER_LABELS, MAC_MODIFIER_LABELS, MAC_MODIFIER_SYMBOLS, PUNCTUATION_KEY_DISPLAY_LABELS, WINDOWS_MODIFIER_LABELS } from "./display-labels.js";
4
7
  import { detectPlatform, resolveModifier } from "./platform.js";
5
- import { ConflictBehavior, FourModifierHotkey, Hotkey, HotkeyCallback, HotkeyCallbackContext, HotkeyMeta, ParsedHotkey, ParsedModifiers, RawHotkey, RawModifiers, RegisterableHotkey, SingleModifierHotkey, ThreeModifierHotkey, TwoModifierHotkey } from "./hotkey.types.js";
6
8
  import { DisplayHotkey, FormatDisplayOptions, formatForDisplay, formatHotkey, formatHotkeySequence, formatWithLabels } from "./format.js";
7
9
  import { hasNonModifierKey, isModifierKey, normalizeHotkey, normalizeHotkeyFromEvent, normalizeHotkeyFromParsed, normalizeRegisterableHotkey, parseHotkey, parseKeyboardEvent, parseRegisterableHotkey, rawHotkeyToParsedHotkey } from "./parse.js";
8
10
  import { ValidationResult, assertValidHotkey, checkHotkey, validateHotkey } from "./validate.js";
9
11
  import { CreateHotkeyHandlerOptions, KeyboardEventMatch, MultiHotkeyHandler, areHotkeysEqual, createHotkeyHandler, createMultiHotkeyHandler, matchesKeyboardEvent } from "./match.js";
10
12
  import { HeldModifierOptions, matchesHeldModifiers } from "./hint.js";
11
- import { HotkeyManager, HotkeyOptions, HotkeyRegistration, HotkeyRegistrationHandle, HotkeyRegistrationView, getHotkeyManager, toHotkeyRegistrationView } from "./hotkey-manager.js";
12
- import { DEFAULT_SEQUENCE_TIMEOUT, HotkeySequence, SequenceManager, SequenceOptions, SequenceRegistration, SequenceRegistrationHandle, SequenceRegistrationView, Target, createSequenceMatcher, getSequenceManager } from "./sequence-manager.js";
13
13
  import { HotkeyConflict, HotkeyConflictOptions, findHotkeyConflicts } from "./conflicts.js";
14
14
  import { KeyStateTracker, KeyStateTrackerState, getKeyStateTracker } from "./key-state-tracker.js";
15
15
  import { HotkeyRecorderValidationContext, HotkeySequenceRecorderValidationContext, RecorderKeyMode, RecorderOptions, RecorderRejection } from "./recorder-options.js";
package/dist/parse.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Key } from "./key.types.js";
2
- import { MODIFIER_ALIASES } from "./constants.js";
3
2
  import { Hotkey, ParsedHotkey, RawHotkey, RegisterableHotkey } from "./hotkey.types.js";
3
+ import { MODIFIER_ALIASES } from "./constants.js";
4
4
  //#region src/parse.d.ts
5
5
  /**
6
6
  * Parses a hotkey string into its component parts.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/hotkeys",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Type-safe, framework-agnostic keyboard hotkey management for the browser",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
@@ -27,6 +27,7 @@
27
27
  "types": "./dist/index.d.ts",
28
28
  "exports": {
29
29
  ".": "./dist/index.js",
30
+ "./adapter": "./dist/adapter.js",
30
31
  "./package.json": "./package.json"
31
32
  },
32
33
  "sideEffects": false,
@@ -37,7 +38,7 @@
37
38
  "dist"
38
39
  ],
39
40
  "dependencies": {
40
- "@tanstack/store": "^0.11.1"
41
+ "@tanstack/store": "^0.11.2"
41
42
  },
42
43
  "scripts": {
43
44
  "clean": "premove ./build ./dist",