@tanstack/hotkeys 0.15.1 → 0.16.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
@@ -51,7 +51,7 @@
51
51
  # TanStack Hotkeys
52
52
 
53
53
  > [!NOTE]
54
- > TanStack Hotkeys is alpha. We are actively developing the library and are open to feedback and contributions.
54
+ > TanStack Hotkeys is in beta. We are actively developing the library and are open to feedback and contributions.
55
55
 
56
56
  Type-safe keyboard shortcuts for the web. Template-string bindings, parsed objects, a cross-platform `Mod` key, a singleton Hotkey Manager, and utilities for cheatsheet UIs—built to stay SSR-friendly.
57
57
 
@@ -62,7 +62,7 @@ function physicalCodeOf(token) {
62
62
  return token.startsWith("[") && token.endsWith("]") && isCodeName(token.slice(1, -1)) ? token.slice(1, -1) : void 0;
63
63
  }
64
64
  /** Splits modifier tokens while retaining a trailing literal plus: `Control++` → `['Control', '+']`. */
65
- function splitHotkeyParts(hotkey) {
65
+ function splitChordParts(hotkey) {
66
66
  if (!hotkey.endsWith("+")) return hotkey.split("+");
67
67
  const parts = hotkey.slice(0, -1).split("+");
68
68
  if (parts.at(-1) === "") parts.pop();
@@ -88,4 +88,4 @@ function assertLogicalKey(key) {
88
88
  }
89
89
 
90
90
  //#endregion
91
- export { assertLogicalKey, codeToLogicalKey, isAsciiLetter, isCodeName, isLatinLetter, isPrintableKey, keysEqual, modifierAlias, normalizeKeyboardEvent, physicalCodeOf, splitHotkeyParts };
91
+ export { assertLogicalKey, codeToLogicalKey, isAsciiLetter, isCodeName, isLatinLetter, isPrintableKey, keysEqual, modifierAlias, normalizeKeyboardEvent, physicalCodeOf, splitChordParts };
package/dist/_match.js CHANGED
@@ -50,9 +50,9 @@ function modifiersMatch(event, parsed, allowImplicitShift) {
50
50
  * same physical code or (case-insensitively) the same logical key. A code and
51
51
  * a key never match, even when one keyboard event could produce both.
52
52
  */
53
- function parsedHotkeysEqual(a, b) {
53
+ function parsedChordsEqual(a, b) {
54
54
  return a.ctrl === b.ctrl && a.alt === b.alt && a.shift === b.shift && a.meta === b.meta && (a.code !== void 0 || b.code !== void 0 ? a.code === b.code : keysEqual(a.key, b.key));
55
55
  }
56
56
 
57
57
  //#endregion
58
- export { matchKeyboardEvent, parsedHotkeysEqual };
58
+ export { matchKeyboardEvent, parsedChordsEqual };
@@ -1,6 +1,6 @@
1
1
  import { normalizeKeyName } from "./constants.js";
2
2
  import { isCodeName, normalizeKeyboardEvent } from "./_keyboard-event.js";
3
- import { isModifierKey, normalizeHotkeyFromParsed, parseKeyboardEvent } from "./parse.js";
3
+ import { isModifierKey, normalizeHotkey, parseKeyboardEvent } from "./parse.js";
4
4
 
5
5
  //#region src/_recorder-chord.ts
6
6
  /**
@@ -15,12 +15,12 @@ function hotkeyChordFromKeydown(event, platform, recordBy = "code") {
15
15
  const parsed = parseKeyboardEvent(event, platform);
16
16
  if (recordBy === "code") {
17
17
  const { key: _key, code: _code, ...modifiers } = parsed;
18
- return normalizeHotkeyFromParsed({
18
+ return normalizeHotkey({
19
19
  ...modifiers,
20
20
  code: event.code
21
21
  }, platform);
22
22
  }
23
- return parsed.key ? normalizeHotkeyFromParsed(parsed, platform) : null;
23
+ return parsed.key ? normalizeHotkey(parsed, platform) : null;
24
24
  }
25
25
  /**
26
26
  * Explains why a physical chord cannot be recorded before syntax/policy checks.
@@ -1,4 +1,6 @@
1
1
  //#region src/_registration.ts
2
+ /** The scope of hotkeys that do not name one. It is always active unless an exclusive scope is active. */
3
+ const GLOBAL_SCOPE = "global";
2
4
  /**
3
5
  * Default options for hotkey/sequence registration.
4
6
  * Omitted: platform, target (resolved at registration), requireReset (HotkeyManager only).
@@ -10,15 +12,16 @@ const defaultHotkeyOptions = {
10
12
  eventType: "keydown",
11
13
  enabled: true,
12
14
  ignoreInputs: "auto",
13
- conflictBehavior: "warn"
15
+ conflictBehavior: "warn",
16
+ scope: GLOBAL_SCOPE
14
17
  };
15
18
  /**
16
19
  * Resolves `ignoreInputs: 'auto'` from the hotkey (the first step of a sequence).
17
20
  * Ctrl/Meta shortcuts and Escape fire in inputs; single keys and Shift/Alt combos are ignored.
18
21
  */
19
- function getDefaultIgnoreInputs(parsedHotkey) {
20
- if (parsedHotkey.ctrl || parsedHotkey.meta) return false;
21
- if (parsedHotkey.key === "Escape" || parsedHotkey.code === "Escape") return false;
22
+ function getDefaultIgnoreInputs(parsedChord) {
23
+ if (parsedChord.ctrl || parsedChord.meta) return false;
24
+ if (parsedChord.key === "Escape" || parsedChord.code === "Escape") return false;
22
25
  return true;
23
26
  }
24
27
  /**
@@ -47,4 +50,4 @@ function optionsEqual(left, right) {
47
50
  }
48
51
 
49
52
  //#endregion
50
- export { defaultHotkeyOptions, getDefaultIgnoreInputs, handleConflict, optionsEqual };
53
+ export { GLOBAL_SCOPE, defaultHotkeyOptions, getDefaultIgnoreInputs, handleConflict, optionsEqual };
package/dist/adapter.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { HotkeyCallback, HotkeyChord, HotkeySequence, RegisterableHotkey, RegisterableHotkeyChord } from "./hotkey.types.js";
2
- import { HotkeyChordOptions, HotkeyOptions, HotkeySequenceOptions, Target } from "./hotkey-manager.js";
1
+ import { HotkeyCallback, HotkeyChord, HotkeySequence, RegisterableHotkey, RegisterableHotkeyChord, RegisterableHotkeySequence } from "./hotkey.types.js";
2
+ import { ActivateHotkeyScopeOptions, HotkeyChordOptions, HotkeyOptions, HotkeySequenceOptions, Target } from "./hotkey-manager.js";
3
3
  import { AppHotkey, AppHotkeysStore } from "./app-hotkeys.js";
4
4
  //#region src/adapter.d.ts
5
5
  /**
@@ -11,7 +11,7 @@ export type HotkeyDefinition = {
11
11
  callback: HotkeyCallback;
12
12
  options?: HotkeyChordOptions;
13
13
  } | {
14
- hotkey: HotkeySequence;
14
+ hotkey: RegisterableHotkeySequence;
15
15
  callback: HotkeyCallback;
16
16
  options?: HotkeySequenceOptions;
17
17
  };
@@ -55,15 +55,15 @@ export type AdapterAppHotkeyOptions<T extends AppHotkey, TTarget> = Omit<[T['def
55
55
  * accept only shared options.
56
56
  */
57
57
  export type AdapterHotkeyDefinition<TTarget> = {
58
- /** A chord string such as `'Mod+S'`, or a RawHotkey object */
58
+ /** A chord string such as `'Mod+S'`, or a chord object */
59
59
  hotkey: RegisterableHotkeyChord;
60
60
  /** The function to call when the hotkey is pressed */
61
61
  callback: HotkeyCallback;
62
62
  /** Per-hotkey options (merged on top of common options) */
63
63
  options?: AdapterChordOptions<TTarget>;
64
64
  } | {
65
- /** A sequence such as `['G', 'G']` */
66
- hotkey: HotkeySequence;
65
+ /** A sequence such as `['G', 'G']`, with each step a string or an object */
66
+ hotkey: RegisterableHotkeySequence;
67
67
  callback: HotkeyCallback;
68
68
  options?: AdapterSequenceOptions<TTarget>;
69
69
  } | {
@@ -113,4 +113,51 @@ export declare function prepareHotkeyDefinitions<TTarget>(definitions: ReadonlyA
113
113
  * unsubscribe function.
114
114
  */
115
115
  export declare function subscribePublishedAppHotkeys(onChange: () => void): () => void;
116
+ /** Scope props shared by framework providers. */
117
+ export interface HotkeysProviderScopeProps {
118
+ /**
119
+ * Scope for hotkeys registered inside this provider, active while it is
120
+ * mounted and enabled. Defaults to the parent provider's scope, or
121
+ * `'global'` at the root. App hotkeys that declare a scope keep it.
122
+ */
123
+ scope?: string;
124
+ /**
125
+ * While mounted and enabled, suspend hotkeys in every other scope,
126
+ * including `'global'`, except scopes of nested providers. Pass
127
+ * `{ allow: [...] }` to keep other scopes active, such as `'global'`. Use
128
+ * with `scope`, for dialogs and other overlays.
129
+ */
130
+ exclusive?: ActivateHotkeyScopeOptions['exclusive'];
131
+ /**
132
+ * Set false to deactivate this provider's scope, and the scopes of
133
+ * providers nested in it, without unmounting. Defaults to true.
134
+ */
135
+ enabled?: boolean;
136
+ }
137
+ /**
138
+ * Merges a provider's default options over its parent's. A provider with a
139
+ * `scope` sets `hotkey.scope`, so hotkeys registered inside it use that scope.
140
+ */
141
+ export declare function mergeHotkeysProviderDefaults<T extends {
142
+ hotkey?: object;
143
+ hotkeyRecorder?: object;
144
+ }>(parent: T | undefined, own: T | undefined, scope: string | undefined): T;
145
+ /**
146
+ * A key for a provider's scope activation, or `null` when the scope should not
147
+ * be active. Equal keys mean the same activation, so adapters reactivate only
148
+ * when the key changes, not when an inline `exclusive` object is recreated.
149
+ */
150
+ export declare function getHotkeysProviderScopeKey(options: {
151
+ scope: string | undefined;
152
+ exclusive: ActivateHotkeyScopeOptions['exclusive'];
153
+ /** Whether this provider and every ancestor are enabled. */
154
+ active: boolean;
155
+ /** The parent provider's scope. */
156
+ parent: string | undefined;
157
+ }): string | null;
158
+ /**
159
+ * Activates the scope described by a key from `getHotkeysProviderScopeKey`.
160
+ * Returns a function that releases the activation.
161
+ */
162
+ export declare function activateHotkeysProviderScope(key: string): () => void;
116
163
  //#endregion
package/dist/adapter.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { detectPlatform } from "./platform.js";
2
- import { isHotkeySequence, normalizeRegisterableHotkey } from "./parse.js";
2
+ import { isHotkeySequence, normalizeHotkey } from "./parse.js";
3
3
  import { defaultHotkeyOptions } from "./_registration.js";
4
4
  import { getHotkeyManager } from "./hotkey-manager.js";
5
5
  import { getAppHotkeyRegistrationOptions, isAppHotkey, resolveAppHotkey } from "./app-hotkeys.js";
@@ -28,7 +28,7 @@ function createHotkeyBindings() {
28
28
  platform: merged.platform ?? detectPlatform()
29
29
  };
30
30
  if (!options.target || isHotkeySequence(definition.hotkey) && definition.hotkey.length === 0) return;
31
- const hotkey = normalizeRegisterableHotkey(definition.hotkey, options.platform);
31
+ const hotkey = normalizeHotkey(definition.hotkey, options.platform);
32
32
  if (isHotkeySequence(hotkey)) delete options.requireReset;
33
33
  else delete options.timeout;
34
34
  prepared.push({
@@ -127,6 +127,48 @@ function subscribePublishedAppHotkeys(onChange) {
127
127
  inner?.unsubscribe();
128
128
  };
129
129
  }
130
+ /**
131
+ * Merges a provider's default options over its parent's. A provider with a
132
+ * `scope` sets `hotkey.scope`, so hotkeys registered inside it use that scope.
133
+ */
134
+ function mergeHotkeysProviderDefaults(parent, own, scope) {
135
+ return {
136
+ hotkey: {
137
+ ...parent?.hotkey,
138
+ ...own?.hotkey,
139
+ ...scope && { scope }
140
+ },
141
+ hotkeyRecorder: {
142
+ ...parent?.hotkeyRecorder,
143
+ ...own?.hotkeyRecorder
144
+ }
145
+ };
146
+ }
147
+ /**
148
+ * A key for a provider's scope activation, or `null` when the scope should not
149
+ * be active. Equal keys mean the same activation, so adapters reactivate only
150
+ * when the key changes, not when an inline `exclusive` object is recreated.
151
+ */
152
+ function getHotkeysProviderScopeKey(options) {
153
+ const { scope, exclusive, active, parent } = options;
154
+ if (!scope || !active) return null;
155
+ return JSON.stringify([
156
+ scope,
157
+ typeof exclusive === "object" ? exclusive.allow : !!exclusive,
158
+ parent ?? null
159
+ ]);
160
+ }
161
+ /**
162
+ * Activates the scope described by a key from `getHotkeysProviderScopeKey`.
163
+ * Returns a function that releases the activation.
164
+ */
165
+ function activateHotkeysProviderScope(key) {
166
+ const [scope, exclusive, parent] = JSON.parse(key);
167
+ return getHotkeyManager().activateScope(scope, {
168
+ exclusive: Array.isArray(exclusive) ? { allow: exclusive } : exclusive,
169
+ parent: parent ?? void 0
170
+ });
171
+ }
130
172
 
131
173
  //#endregion
132
- export { createHotkeyBindings, prepareHotkeyDefinitions, subscribePublishedAppHotkeys };
174
+ export { activateHotkeysProviderScope, createHotkeyBindings, getHotkeysProviderScopeKey, mergeHotkeysProviderDefaults, prepareHotkeyDefinitions, subscribePublishedAppHotkeys };
@@ -17,11 +17,12 @@ export interface AppHotkeyInput {
17
17
  /** Display metadata for cheat sheets and settings screens. */
18
18
  meta?: HotkeyMeta;
19
19
  /**
20
- * Label for where this hotkey applies, such as `'editor'`. App hotkeys with
21
- * different contexts may share a hotkey; one without a context conflicts with
22
- * any app hotkey that shares its hotkey. It does not affect matching.
20
+ * Scope this hotkey fires in, such as `'editor'`. Overrides the scope of the
21
+ * provider it registers under. App hotkeys in different scopes may share a
22
+ * hotkey; one in the `'global'` scope conflicts with any app hotkey that
23
+ * shares its hotkey.
23
24
  */
24
- context?: string;
25
+ scope?: string;
25
26
  /**
26
27
  * Options that describe the hotkey itself. Chord defaults accept
27
28
  * `requireReset`; sequence defaults accept `timeout`.
@@ -45,7 +46,7 @@ export interface AppHotkey<TId extends string = string, TDefault extends HotkeyC
45
46
  /** Hotkey declared in `defineAppHotkeys`. */
46
47
  readonly defaultHotkey: TDefault;
47
48
  readonly meta?: HotkeyMeta;
48
- readonly context?: string;
49
+ readonly scope?: string;
49
50
  readonly options?: AppHotkeyOptions;
50
51
  }
51
52
  /** A map of app hotkeys keyed by id. */
@@ -110,11 +111,12 @@ export declare function isCustomHotkey(appHotkey: AppHotkey, platform?: Platform
110
111
  /**
111
112
  * Status of an app hotkey in the running app.
112
113
  * - `'active'`: at least one enabled registration
114
+ * - `'suspended'`: has an enabled registration, but none in an active scope
113
115
  * - `'disabled'`: registered, but every registration has `enabled: false`
114
116
  * - `'inactive'`: has a hotkey, but nothing has registered it
115
117
  * - `'unbound'`: no hotkey assigned
116
118
  */
117
- export type AppHotkeyStatus = 'active' | 'disabled' | 'inactive' | 'unbound';
119
+ export type AppHotkeyStatus = 'active' | 'suspended' | 'disabled' | 'inactive' | 'unbound';
118
120
  /** One row of an app hotkey cheat sheet. */
119
121
  export interface AppHotkeyListItem<T extends AppHotkey = AppHotkey> {
120
122
  appHotkey: T;
@@ -174,7 +176,7 @@ export declare class AppHotkeysStore<T extends AppHotkeys = AppHotkeys> {
174
176
  get(id: string): AppHotkey | undefined;
175
177
  /**
176
178
  * Other app hotkeys that would collide if `id` used `hotkey`. App hotkeys
177
- * collide when their contexts match or either has no context, and their
179
+ * collide when their scopes match or either is `'global'`, and their
178
180
  * hotkeys are equal or one sequence starts with the other.
179
181
  */
180
182
  findConflicts(id: keyof T & string, hotkey: Hotkey): Array<AppHotkey>;
@@ -203,12 +205,13 @@ export declare class AppHotkeysStore<T extends AppHotkeys = AppHotkeys> {
203
205
  export declare function resolveAppHotkey(appHotkey: AppHotkey, store: AppHotkeysStore | null | undefined): AppHotkey;
204
206
  /**
205
207
  * Options that adapters pass to the manager when registering an app hotkey:
206
- * declared options, meta, and the app hotkey id. The manager drops options
208
+ * declared options, scope, meta, and the app hotkey id. The manager drops options
207
209
  * that do not apply to the effective hotkey's kind.
208
210
  */
209
211
  export declare function getAppHotkeyRegistrationOptions(appHotkey: AppHotkey): AppHotkeyOptions & {
210
212
  meta?: HotkeyMeta;
211
213
  appHotkeyId: string;
214
+ scope?: string;
212
215
  };
213
216
  /**
214
217
  * Publishes a provider's store on the HotkeyManager so devtools can list
@@ -2,6 +2,7 @@ import { detectPlatform } from "./platform.js";
2
2
  import { isHotkeySequence, simplifyHotkey } from "./parse.js";
3
3
  import { validateHotkey } from "./validate.js";
4
4
  import { areHotkeysEqual } from "./match.js";
5
+ import { GLOBAL_SCOPE } from "./_registration.js";
5
6
  import { getHotkeyManager } from "./hotkey-manager.js";
6
7
  import { Store } from "@tanstack/store";
7
8
 
@@ -30,7 +31,7 @@ function defineAppHotkeys(definitions) {
30
31
  hotkey,
31
32
  defaultHotkey: hotkey,
32
33
  meta: input.meta,
33
- context: input.context,
34
+ scope: input.scope,
34
35
  options: input.options
35
36
  };
36
37
  }
@@ -86,8 +87,10 @@ function sanitizeCustomHotkeys(appHotkeys, saved) {
86
87
  return result;
87
88
  }
88
89
  /** Whether two app hotkeys can apply at the same time. */
89
- function contextsOverlap(a, b) {
90
- return a.context === void 0 || b.context === void 0 || a.context === b.context;
90
+ function scopesOverlap(a, b) {
91
+ const left = a.scope ?? "global";
92
+ const right = b.scope ?? "global";
93
+ return left === "global" || right === "global" || left === right;
91
94
  }
92
95
  /** Whether two hotkeys collide: equal chords, or one sequence starts with the other. */
93
96
  function hotkeysOverlap(left, right, platform) {
@@ -162,14 +165,14 @@ var AppHotkeysStore = class {
162
165
  }
163
166
  /**
164
167
  * Other app hotkeys that would collide if `id` used `hotkey`. App hotkeys
165
- * collide when their contexts match or either has no context, and their
168
+ * collide when their scopes match or either is `'global'`, and their
166
169
  * hotkeys are equal or one sequence starts with the other.
167
170
  */
168
171
  findConflicts(id, hotkey) {
169
172
  const self = this.store.state.appHotkeys[id];
170
173
  if (!self) return [];
171
174
  const candidate = simplifyHotkey(hotkey);
172
- return Object.values(this.store.state.appHotkeys).filter((other) => other.id !== id && other.hotkey !== null && contextsOverlap(self, other) && hotkeysOverlap(candidate, other.hotkey, this.#platform));
175
+ return Object.values(this.store.state.appHotkeys).filter((other) => other.id !== id && other.hotkey !== null && scopesOverlap(self, other) && hotkeysOverlap(candidate, other.hotkey, this.#platform));
173
176
  }
174
177
  /**
175
178
  * Sets the user's hotkey for an app hotkey. `null` unbinds it. A hotkey
@@ -224,11 +227,13 @@ var AppHotkeysStore = class {
224
227
  if (list) list.push(registration);
225
228
  else byId.set(id, [registration]);
226
229
  }
230
+ const manager = getHotkeyManager();
227
231
  return Object.values(this.store.state.appHotkeys).map((appHotkey) => {
228
232
  const registered = byId.get(appHotkey.id) ?? [];
233
+ const enabled = registered.filter((r) => r.options.enabled !== false);
229
234
  return {
230
235
  appHotkey,
231
- status: appHotkey.hotkey === null ? "unbound" : registered.length === 0 ? "inactive" : registered.some((r) => r.options.enabled !== false) ? "active" : "disabled",
236
+ status: appHotkey.hotkey === null ? "unbound" : registered.length === 0 ? "inactive" : enabled.length === 0 ? "disabled" : enabled.some((r) => manager.isScopeActive(r.options.scope)) ? "active" : "suspended",
232
237
  isCustom: isCustomHotkey(appHotkey, this.#platform),
233
238
  registrations: registered
234
239
  };
@@ -245,12 +250,13 @@ function resolveAppHotkey(appHotkey, store) {
245
250
  }
246
251
  /**
247
252
  * Options that adapters pass to the manager when registering an app hotkey:
248
- * declared options, meta, and the app hotkey id. The manager drops options
253
+ * declared options, scope, meta, and the app hotkey id. The manager drops options
249
254
  * that do not apply to the effective hotkey's kind.
250
255
  */
251
256
  function getAppHotkeyRegistrationOptions(appHotkey) {
252
257
  return {
253
258
  ...appHotkey.options,
259
+ ...appHotkey.scope !== void 0 && { scope: appHotkey.scope },
254
260
  meta: appHotkey.meta,
255
261
  appHotkeyId: appHotkey.id
256
262
  };
package/dist/conflicts.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { detectPlatform } from "./platform.js";
2
- import { isHotkeySequence, parseRegisterableHotkey } from "./parse.js";
3
- import { parsedHotkeysEqual } from "./_match.js";
2
+ import { isHotkeySequence, parseHotkey } from "./parse.js";
3
+ import { parsedChordsEqual } from "./_match.js";
4
4
  import { matchesKeyboardEvent } from "./match.js";
5
5
  import { getTargetDocument } from "./_event-target.js";
6
6
  import { managerRef } from "./_manager-ref.js";
@@ -23,15 +23,15 @@ function findHotkeyConflicts(candidate, options = {}) {
23
23
  const steps = isHotkeySequence(candidate) ? candidate : [candidate];
24
24
  if (steps.length === 0) return [];
25
25
  const platform = options.platform ?? detectPlatform();
26
- const parsed = steps.map((step) => parseRegisterableHotkey(step, platform));
26
+ const parsed = steps.map((step) => parseHotkey(step, platform));
27
27
  const target = options.target ?? (typeof document === "undefined" ? void 0 : document);
28
28
  const conflicts = [];
29
29
  const matches = (registration, bindings) => {
30
30
  if (!options.includeDisabled && registration.options.enabled === false || (registration.options.eventType ?? "keydown") !== (options.eventType ?? "keydown") || options.excludeIds?.includes(registration.id) || options.exclude?.(registration) || options.scope !== "all" && target && !targetsOverlap(target, registration.target)) return false;
31
31
  return parsed.slice(0, Math.min(parsed.length, bindings.length)).every((a, index) => {
32
- const b = parseRegisterableHotkey(bindings[index], registration.options.platform);
32
+ const b = parseHotkey(bindings[index], registration.options.platform);
33
33
  const event = options.events?.[index];
34
- return parsedHotkeysEqual(a, b) || event !== void 0 && matchesKeyboardEvent(event, a, platform) && matchesKeyboardEvent(event, b, registration.options.platform);
34
+ return parsedChordsEqual(a, b) || event !== void 0 && matchesKeyboardEvent(event, a, platform) && matchesKeyboardEvent(event, b, registration.options.platform);
35
35
  });
36
36
  };
37
37
  const registrations = managerRef.current?.registrations.state.values() ?? [];
package/dist/format.d.ts CHANGED
@@ -1,20 +1,24 @@
1
- import { ParsedHotkey, RegisterableHotkeyChord } from "./hotkey.types.js";
1
+ import { ParsedHotkeyChord, RegisterableHotkeyChord } from "./hotkey.types.js";
2
2
  //#region src/format.d.ts
3
3
  /**
4
- * Converts a ParsedHotkey back to a hotkey string.
4
+ * Converts a parsed hotkey back to a string, with each platform modifier
5
+ * spelled out, such as `Control+Shift+S`. A parsed chord returns a string, and
6
+ * a parsed sequence returns one string per step. To get `Mod` back where the
7
+ * platform allows it, use `normalizeHotkey` instead.
5
8
  *
6
- * @param parsed - The parsed hotkey object
7
- * @returns A hotkey string in canonical form
9
+ * @param parsed - A parsed chord, or the parsed steps of a sequence
8
10
  *
9
11
  * @example
10
12
  * ```ts
11
- * formatHotkey({ key: 'S', ctrl: true, shift: true, alt: false, meta: false, modifiers: ['Control', 'Shift'] })
12
- * // Returns: 'Control+Shift+S'
13
+ * formatHotkey(parseHotkey('Mod+Shift+S', 'windows')) // 'Control+Shift+S'
14
+ * formatHotkey(parseHotkey(['G', 'Mod+K'], 'mac')) // ['G', 'Meta+K']
13
15
  * ```
14
16
  */
15
- export declare function formatHotkey(parsed: ParsedHotkey): string;
17
+ export declare function formatHotkey(parsed: ParsedHotkeyChord): string;
18
+ export declare function formatHotkey(parsed: ReadonlyArray<ParsedHotkeyChord>): Array<string>;
19
+ export declare function formatHotkey(parsed: ParsedHotkeyChord | ReadonlyArray<ParsedHotkeyChord>): string | Array<string>;
16
20
  /** A single chord to display: a string, raw object, or parsed hotkey. */
17
- export type DisplayHotkeyChord = RegisterableHotkeyChord | ParsedHotkey | (string & {});
21
+ export type DisplayHotkeyChord = RegisterableHotkeyChord | ParsedHotkeyChord | (string & {});
18
22
  /** A hotkey to display: a chord or a sequence of chords. */
19
23
  export type DisplayHotkey = DisplayHotkeyChord | ReadonlyArray<DisplayHotkeyChord>;
20
24
  /**
@@ -31,7 +35,7 @@ export type DisplayHotkey = DisplayHotkeyChord | ReadonlyArray<DisplayHotkeyChor
31
35
  * Sequence steps are formatted individually and joined with `sequenceSeparator`
32
36
  * (a space by default).
33
37
  *
34
- * @param hotkey - A logical or physical string, raw object, ParsedHotkey, or sequence
38
+ * @param hotkey - A logical or physical string, raw object, ParsedHotkeyChord, or sequence
35
39
  * @param options - Formatting options
36
40
  * @returns Display text, or individual key labels when parts is true (one array per step for sequences)
37
41
  *
package/dist/format.js CHANGED
@@ -1,23 +1,14 @@
1
1
  import { MODIFIER_ORDER, normalizeKeyName } from "./constants.js";
2
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
3
  import { detectPlatform } from "./platform.js";
4
- import { assertLogicalKey, codeToLogicalKey, modifierAlias, physicalCodeOf, splitHotkeyParts } from "./_keyboard-event.js";
5
- import { normalizeHotkeyFromParsed, normalizeRegisterableHotkey } from "./parse.js";
4
+ import { assertLogicalKey, codeToLogicalKey, modifierAlias, physicalCodeOf, splitChordParts } from "./_keyboard-event.js";
5
+ import { normalizeHotkey } from "./parse.js";
6
6
 
7
7
  //#region src/format.ts
8
- /**
9
- * Converts a ParsedHotkey back to a hotkey string.
10
- *
11
- * @param parsed - The parsed hotkey object
12
- * @returns A hotkey string in canonical form
13
- *
14
- * @example
15
- * ```ts
16
- * formatHotkey({ key: 'S', ctrl: true, shift: true, alt: false, meta: false, modifiers: ['Control', 'Shift'] })
17
- * // Returns: 'Control+Shift+S'
18
- * ```
19
- */
20
8
  function formatHotkey(parsed) {
9
+ return Array.isArray(parsed) ? parsed.map((step) => formatParsedChord(step)) : formatParsedChord(parsed);
10
+ }
11
+ function formatParsedChord(parsed) {
21
12
  if (parsed.code === void 0) assertLogicalKey(parsed.key);
22
13
  const parts = [];
23
14
  for (const modifier of MODIFIER_ORDER) if (parsed.modifiers.includes(modifier)) parts.push(modifier);
@@ -45,8 +36,8 @@ function formatChordParts(hotkey, options) {
45
36
  const symbols = options.useSymbols ?? true;
46
37
  const modifierSymbols = modifierSymbolsEnabled(options);
47
38
  const keySymbols = typeof symbols === "boolean" ? symbols : symbols.keys ?? true;
48
- const normalized = typeof hotkey === "object" && "modifiers" in hotkey ? normalizeHotkeyFromParsed(hotkey, platform) : normalizeRegisterableHotkey(hotkey, platform);
49
- const segments = splitHotkeyParts(normalized);
39
+ const normalized = normalizeHotkey(hotkey, platform);
40
+ const segments = splitChordParts(normalized);
50
41
  if (platform === "mac") {
51
42
  const modifierIndex = (segment) => {
52
43
  const index = MODIFIER_ORDER.indexOf(segment === "Mod" ? "Meta" : segment);
package/dist/hint.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { MODIFIER_KEYS, normalizeKeyName } from "./constants.js";
2
- import { isHotkeySequence, parseRegisterableHotkey } from "./parse.js";
2
+ import { isHotkeySequence, parseHotkey } from "./parse.js";
3
3
 
4
4
  //#region src/hint.ts
5
5
  /**
@@ -18,7 +18,7 @@ function matchesHeldModifiers(hotkey, heldKeys, options = {}) {
18
18
  const held = [...new Set(heldKeys.map(normalizeKeyName).filter((key) => MODIFIER_KEYS.has(key)))];
19
19
  const first = isHotkeySequence(hotkey) ? hotkey[0] : hotkey;
20
20
  if (first === void 0) return false;
21
- const { modifiers } = parseRegisterableHotkey(first, options.platform);
21
+ const { modifiers } = parseHotkey(first, options.platform);
22
22
  return held.length > 0 && held.every((key) => modifiers.includes(key)) && (!options.exact || held.length === modifiers.length);
23
23
  }
24
24