@tanstack/hotkeys 0.11.0 → 0.12.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.
@@ -0,0 +1,295 @@
1
+ import { detectPlatform } from "./platform.js";
2
+ import { isHotkeySequence, simplifyHotkey } from "./parse.js";
3
+ import { validateHotkey } from "./validate.js";
4
+ import { areHotkeysEqual } from "./match.js";
5
+ import { getHotkeyManager } from "./hotkey-manager.js";
6
+ import { Store } from "@tanstack/store";
7
+
8
+ //#region src/app-hotkeys.ts
9
+ /**
10
+ * Declares every app hotkey up front, without registering any, so cheat
11
+ * sheets and settings screens can list hotkeys whose components have not
12
+ * mounted. Pass the result to `HotkeysProvider` as `appHotkeys`.
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * export const appHotkeys = defineAppHotkeys({
17
+ * 'doc.save': { hotkey: 'Mod+S', meta: { name: 'Save', group: 'Document' } },
18
+ * 'nav.home': { hotkey: ['G', 'H'], meta: { name: 'Go home' }, options: { timeout: 800 } },
19
+ * 'palette.open': { hotkey: null, meta: { name: 'Command palette' } },
20
+ * })
21
+ * ```
22
+ */
23
+ function defineAppHotkeys(definitions) {
24
+ const result = {};
25
+ for (const [id, input] of Object.entries(definitions)) {
26
+ const hotkey = input.hotkey === null ? null : simplifyHotkey(input.hotkey);
27
+ result[id] = {
28
+ "~appHotkey": true,
29
+ id,
30
+ hotkey,
31
+ defaultHotkey: hotkey,
32
+ meta: input.meta,
33
+ context: input.context,
34
+ options: input.options
35
+ };
36
+ }
37
+ return result;
38
+ }
39
+ /** Whether a value is an app hotkey created by `defineAppHotkeys`. */
40
+ function isAppHotkey(value) {
41
+ return typeof value === "object" && value !== null && value["~appHotkey"] === true;
42
+ }
43
+ function hotkeysEqual(left, right, platform) {
44
+ if (left === null || right === null) return left === right;
45
+ return areHotkeysEqual(left, right, platform);
46
+ }
47
+ /** Whether an app hotkey's effective hotkey differs from its default. */
48
+ function isCustomHotkey(appHotkey, platform = detectPlatform()) {
49
+ return !hotkeysEqual(appHotkey.hotkey, appHotkey.defaultHotkey, platform);
50
+ }
51
+ /** Parses a saved custom hotkey. Returns `undefined` when it is not a valid hotkey. */
52
+ function parseCustomHotkey(value) {
53
+ if (value === null) return null;
54
+ return (typeof value === "string" || Array.isArray(value) && value.every((step) => typeof step === "string")) && validateHotkey(value).valid ? simplifyHotkey(value) : void 0;
55
+ }
56
+ /** Applies custom hotkeys to declarations. Unchanged entries keep their identity. */
57
+ function applyCustomHotkeys(appHotkeys, customHotkeys) {
58
+ const custom = customHotkeys;
59
+ const result = {};
60
+ for (const [id, appHotkey] of Object.entries(appHotkeys)) {
61
+ const hotkey = custom[id] === void 0 ? appHotkey.defaultHotkey : custom[id];
62
+ result[id] = hotkey === appHotkey.hotkey ? appHotkey : {
63
+ ...appHotkey,
64
+ hotkey
65
+ };
66
+ }
67
+ return result;
68
+ }
69
+ /** Keeps only valid custom hotkeys for known ids, warning about the rest. */
70
+ function sanitizeCustomHotkeys(appHotkeys, saved) {
71
+ const result = {};
72
+ if (typeof saved !== "object" || saved === null) return result;
73
+ for (const [id, value] of Object.entries(saved)) {
74
+ if (value === void 0) continue;
75
+ if (!(id in appHotkeys)) {
76
+ console.warn(`Ignoring custom hotkey for unknown app hotkey '${id}'`);
77
+ continue;
78
+ }
79
+ const hotkey = parseCustomHotkey(value);
80
+ if (hotkey === void 0) {
81
+ console.warn(`Ignoring invalid custom hotkey for app hotkey '${id}':`, value);
82
+ continue;
83
+ }
84
+ result[id] = hotkey;
85
+ }
86
+ return result;
87
+ }
88
+ /** 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;
91
+ }
92
+ /** Whether two hotkeys collide: equal chords, or one sequence starts with the other. */
93
+ function hotkeysOverlap(left, right, platform) {
94
+ const a = isHotkeySequence(left) ? left : [left];
95
+ const b = isHotkeySequence(right) ? right : [right];
96
+ const length = Math.min(a.length, b.length);
97
+ for (let i = 0; i < length; i++) if (!areHotkeysEqual(a[i], b[i], platform)) return false;
98
+ return true;
99
+ }
100
+ /**
101
+ * Holds declared app hotkeys and the user's custom hotkeys. Framework
102
+ * providers create one and expose it through hooks; devtools read it from
103
+ * `HotkeyManager.appHotkeys`.
104
+ *
105
+ * @example
106
+ * ```ts
107
+ * const store = new AppHotkeysStore(appHotkeys, {
108
+ * customHotkeys: JSON.parse(localStorage.getItem('hotkeys') ?? '{}'),
109
+ * onCustomHotkeysChange: (custom) =>
110
+ * localStorage.setItem('hotkeys', JSON.stringify(custom)),
111
+ * })
112
+ * store.setCustomHotkey('doc.save', 'Mod+Shift+S')
113
+ * ```
114
+ */
115
+ var AppHotkeysStore = class {
116
+ store;
117
+ #declarations;
118
+ #options;
119
+ constructor(appHotkeys, options = {}) {
120
+ this.#declarations = appHotkeys;
121
+ this.#options = options;
122
+ const customHotkeys = sanitizeCustomHotkeys(appHotkeys, options.customHotkeys);
123
+ this.store = new Store({
124
+ appHotkeys: applyCustomHotkeys(appHotkeys, customHotkeys),
125
+ customHotkeys
126
+ });
127
+ }
128
+ get #platform() {
129
+ return this.#options.platform ?? detectPlatform();
130
+ }
131
+ /** Effective app hotkeys. */
132
+ get appHotkeys() {
133
+ return this.store.state.appHotkeys;
134
+ }
135
+ /** The user's hotkeys that differ from the defaults. */
136
+ get customHotkeys() {
137
+ return this.store.state.customHotkeys;
138
+ }
139
+ /** Updates callbacks and other options without resetting custom hotkeys. */
140
+ setOptions(options) {
141
+ this.#options = {
142
+ ...this.#options,
143
+ ...options
144
+ };
145
+ }
146
+ /**
147
+ * Replaces the declarations, for example after hot module replacement.
148
+ * Custom hotkeys for ids that still exist are kept.
149
+ */
150
+ setAppHotkeys(appHotkeys) {
151
+ if (appHotkeys === this.#declarations) return;
152
+ this.#declarations = appHotkeys;
153
+ const customHotkeys = sanitizeCustomHotkeys(appHotkeys, this.store.state.customHotkeys);
154
+ this.store.setState(() => ({
155
+ appHotkeys: applyCustomHotkeys(appHotkeys, customHotkeys),
156
+ customHotkeys
157
+ }));
158
+ }
159
+ /** Effective app hotkey for an id, or `undefined` when it is not declared. */
160
+ get(id) {
161
+ return this.store.state.appHotkeys[id];
162
+ }
163
+ /**
164
+ * 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
166
+ * hotkeys are equal or one sequence starts with the other.
167
+ */
168
+ findConflicts(id, hotkey) {
169
+ const self = this.store.state.appHotkeys[id];
170
+ if (!self) return [];
171
+ 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));
173
+ }
174
+ /**
175
+ * Sets the user's hotkey for an app hotkey. `null` unbinds it. A hotkey
176
+ * equal to the default removes the customization.
177
+ *
178
+ * @returns Conflicting app hotkeys. When any exist, nothing changes.
179
+ */
180
+ setCustomHotkey(id, hotkey) {
181
+ const appHotkey = this.store.state.appHotkeys[id];
182
+ if (!appHotkey) {
183
+ console.warn(`Cannot set a custom hotkey for unknown app hotkey '${id}'`);
184
+ return [];
185
+ }
186
+ const next = hotkey === null ? null : simplifyHotkey(hotkey);
187
+ if (next !== null) {
188
+ const conflicts = this.findConflicts(id, next);
189
+ if (conflicts.length > 0) return conflicts;
190
+ }
191
+ const { [id]: _previous, ...rest } = this.store.state.customHotkeys;
192
+ this.#commit(hotkeysEqual(next, appHotkey.defaultHotkey, this.#platform) ? rest : {
193
+ ...rest,
194
+ [id]: next
195
+ });
196
+ return [];
197
+ }
198
+ /** Restores the default hotkey for an app hotkey. */
199
+ resetCustomHotkey(id) {
200
+ const { [id]: _previous, ...rest } = this.store.state.customHotkeys;
201
+ this.#commit(rest);
202
+ }
203
+ /** Restores every default hotkey. */
204
+ resetCustomHotkeys() {
205
+ this.#commit({});
206
+ }
207
+ #commit(customHotkeys) {
208
+ this.store.setState(() => ({
209
+ appHotkeys: applyCustomHotkeys(this.#declarations, customHotkeys),
210
+ customHotkeys
211
+ }));
212
+ this.#options.onCustomHotkeysChange?.(customHotkeys);
213
+ }
214
+ /**
215
+ * Joins app hotkeys with live registrations, in declaration order.
216
+ * Adapters call this with the manager's registration views.
217
+ */
218
+ getList(registrations) {
219
+ const byId = /* @__PURE__ */ new Map();
220
+ for (const registration of registrations) {
221
+ const id = registration.options.appHotkeyId;
222
+ if (id === void 0) continue;
223
+ const list = byId.get(id);
224
+ if (list) list.push(registration);
225
+ else byId.set(id, [registration]);
226
+ }
227
+ return Object.values(this.store.state.appHotkeys).map((appHotkey) => {
228
+ const registered = byId.get(appHotkey.id) ?? [];
229
+ return {
230
+ appHotkey,
231
+ status: appHotkey.hotkey === null ? "unbound" : registered.length === 0 ? "inactive" : registered.some((r) => r.options.enabled !== false) ? "active" : "disabled",
232
+ isCustom: isCustomHotkey(appHotkey, this.#platform),
233
+ registrations: registered
234
+ };
235
+ });
236
+ }
237
+ };
238
+ /**
239
+ * Returns the provider's version of an app hotkey, so custom hotkeys apply
240
+ * even when a component imports the declaration directly. Falls back to the
241
+ * given app hotkey when no store declares it.
242
+ */
243
+ function resolveAppHotkey(appHotkey, store) {
244
+ return store?.get(appHotkey.id) ?? appHotkey;
245
+ }
246
+ /**
247
+ * 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
249
+ * that do not apply to the effective hotkey's kind.
250
+ */
251
+ function getAppHotkeyRegistrationOptions(appHotkey) {
252
+ return {
253
+ ...appHotkey.options,
254
+ meta: appHotkey.meta,
255
+ appHotkeyId: appHotkey.id
256
+ };
257
+ }
258
+ /**
259
+ * Publishes a provider's store on the HotkeyManager so devtools can list
260
+ * declared hotkeys. Returns a cleanup function that unpublishes it if it is
261
+ * still the published store.
262
+ */
263
+ function publishAppHotkeysStore(store) {
264
+ const manager = getHotkeyManager();
265
+ manager.appHotkeys.setState(() => store);
266
+ return () => {
267
+ if (manager.appHotkeys.state === store) manager.appHotkeys.setState(() => null);
268
+ };
269
+ }
270
+ /**
271
+ * Creates an app hotkeys store and publishes it on the HotkeyManager, for
272
+ * adapters without a provider component (such as Lit and Alpine). Hotkey
273
+ * APIs in those adapters resolve app hotkeys and custom hotkeys from the
274
+ * published store. Call it once at startup. Returns the store and a function
275
+ * that unpublishes it.
276
+ *
277
+ * @example
278
+ * ```ts
279
+ * provideAppHotkeys(appHotkeys, {
280
+ * customHotkeys: JSON.parse(localStorage.getItem('hotkeys') ?? '{}'),
281
+ * onCustomHotkeysChange: (custom) =>
282
+ * localStorage.setItem('hotkeys', JSON.stringify(custom)),
283
+ * })
284
+ * ```
285
+ */
286
+ function provideAppHotkeys(appHotkeys, options = {}) {
287
+ const store = new AppHotkeysStore(appHotkeys, options);
288
+ return {
289
+ store,
290
+ dispose: publishAppHotkeysStore(store)
291
+ };
292
+ }
293
+
294
+ //#endregion
295
+ export { AppHotkeysStore, defineAppHotkeys, getAppHotkeyRegistrationOptions, isAppHotkey, isCustomHotkey, provideAppHotkeys, publishAppHotkeysStore, resolveAppHotkey };
@@ -1,14 +1,10 @@
1
1
  import { RegisterableHotkey } from "./hotkey.types.js";
2
2
  import { HotkeyRegistrationView } from "./hotkey-manager.js";
3
- import { SequenceRegistrationView } from "./sequence-manager.js";
4
3
  //#region src/conflicts.d.ts
5
- export type HotkeyConflict = {
6
- type: 'hotkey';
4
+ /** A live registration that overlaps a candidate hotkey. */
5
+ export interface HotkeyConflict {
7
6
  registration: HotkeyRegistrationView;
8
- } | {
9
- type: 'sequence';
10
- registration: SequenceRegistrationView;
11
- };
7
+ }
12
8
  export interface HotkeyConflictOptions {
13
9
  /** Intended registration target. Defaults to document; disjoint targets are excluded. */
14
10
  target?: HTMLElement | Document | Window;
@@ -21,7 +17,7 @@ export interface HotkeyConflictOptions {
21
17
  /** IDs of registrations being edited. */
22
18
  excludeIds?: ReadonlyArray<string>;
23
19
  /** Additional app-specific exclusions, such as all instances of one action. */
24
- exclude?: (registration: HotkeyRegistrationView | SequenceRegistrationView) => boolean;
20
+ exclude?: (registration: HotkeyRegistrationView) => boolean;
25
21
  /** Platform used to resolve Mod in the candidate. Registrations retain their own platform. */
26
22
  platform?: 'mac' | 'windows' | 'linux';
27
23
  /** Source events allow detecting physical/logical overlap on the recorded layout. */
@@ -32,5 +28,5 @@ export interface HotkeyConflictOptions {
32
28
  * With source events, also checks observed logical/physical overlap. Does not
33
29
  * discover unmounted handlers, external listeners, or other keyboard layouts.
34
30
  */
35
- export declare function findHotkeyConflicts(candidate: RegisterableHotkey | ReadonlyArray<RegisterableHotkey>, options?: HotkeyConflictOptions): Array<HotkeyConflict>;
31
+ export declare function findHotkeyConflicts(candidate: RegisterableHotkey, options?: HotkeyConflictOptions): Array<HotkeyConflict>;
36
32
  //#endregion
package/dist/conflicts.js CHANGED
@@ -1,9 +1,8 @@
1
1
  import { detectPlatform } from "./platform.js";
2
2
  import { keysEqual } from "./_keyboard-event.js";
3
- import { parseRegisterableHotkey } from "./parse.js";
3
+ import { isHotkeySequence, parseRegisterableHotkey } from "./parse.js";
4
4
  import { matchesKeyboardEvent } from "./match.js";
5
5
  import { getHotkeyManager, toHotkeyRegistrationView } from "./hotkey-manager.js";
6
- import { getSequenceManager } from "./sequence-manager.js";
7
6
 
8
7
  //#region src/conflicts.ts
9
8
  /** Whether two targets share a document and one contains the other (or is its window). */
@@ -24,7 +23,7 @@ function targetsOverlap(a, b) {
24
23
  * discover unmounted handlers, external listeners, or other keyboard layouts.
25
24
  */
26
25
  function findHotkeyConflicts(candidate, options = {}) {
27
- const steps = Array.isArray(candidate) ? candidate : [candidate];
26
+ const steps = isHotkeySequence(candidate) ? candidate : [candidate];
28
27
  if (steps.length === 0) return [];
29
28
  const platform = options.platform ?? detectPlatform();
30
29
  const parsed = steps.map((step) => parseRegisterableHotkey(step, platform));
@@ -39,14 +38,10 @@ function findHotkeyConflicts(candidate, options = {}) {
39
38
  return same || event !== void 0 && matchesKeyboardEvent(event, a, platform) && matchesKeyboardEvent(event, b, registration.options.platform);
40
39
  });
41
40
  };
42
- for (const registration of getHotkeyManager().registrations.state.values()) if (matches(registration, [registration.hotkey])) conflicts.push({
43
- type: "hotkey",
44
- registration: toHotkeyRegistrationView(registration)
45
- });
46
- for (const registration of getSequenceManager().registrations.state.values()) if (matches(registration, registration.sequence)) conflicts.push({
47
- type: "sequence",
48
- registration
49
- });
41
+ for (const registration of getHotkeyManager().registrations.state.values()) {
42
+ const view = toHotkeyRegistrationView(registration);
43
+ if (matches(view, view.kind === "sequence" ? view.hotkey : [view.hotkey])) conflicts.push({ registration: view });
44
+ }
50
45
  return conflicts;
51
46
  }
52
47
 
package/dist/format.d.ts CHANGED
@@ -1,19 +1,5 @@
1
- import { Hotkey, ParsedHotkey, RegisterableHotkey } from "./hotkey.types.js";
1
+ import { ParsedHotkey, RegisterableHotkeyChord } from "./hotkey.types.js";
2
2
  //#region src/format.d.ts
3
- /**
4
- * Joins stored sequence steps with spaces, preserving physical code tokens.
5
- * For user-facing labels, call formatForDisplay on each step instead.
6
- *
7
- * @param sequence - Array of hotkey strings that form the sequence
8
- * @returns A space-separated string (e.g. ['G','G'] → 'G G')
9
- *
10
- * @example
11
- * ```ts
12
- * formatHotkeySequence(['G', 'G']) // 'G G'
13
- * formatHotkeySequence(['D', 'I', 'W']) // 'D I W'
14
- * ```
15
- */
16
- export declare function formatHotkeySequence(sequence: Array<Hotkey>): string;
17
3
  /**
18
4
  * Converts a ParsedHotkey back to a hotkey string.
19
5
  *
@@ -27,7 +13,10 @@ export declare function formatHotkeySequence(sequence: Array<Hotkey>): string;
27
13
  * ```
28
14
  */
29
15
  export declare function formatHotkey(parsed: ParsedHotkey): string;
30
- export type DisplayHotkey = RegisterableHotkey | ParsedHotkey | (string & {});
16
+ /** A single chord to display: a string, raw object, or parsed hotkey. */
17
+ export type DisplayHotkeyChord = RegisterableHotkeyChord | ParsedHotkey | (string & {});
18
+ /** A hotkey to display: a chord or a sequence of chords. */
19
+ export type DisplayHotkey = DisplayHotkeyChord | ReadonlyArray<DisplayHotkeyChord>;
31
20
  /**
32
21
  * Formats a hotkey for display in a user interface.
33
22
  *
@@ -39,10 +28,12 @@ export type DisplayHotkey = RegisterableHotkey | ParsedHotkey | (string & {});
39
28
  * `[Digit2]` becomes `2`. These labels do not infer the active keyboard layout
40
29
  * or change matching. Supply a resolved `layoutMap` for layout-aware labels;
41
30
  * `keyLabels` overrides both layout entries and fallback labels.
31
+ * Sequence steps are formatted individually and joined with `sequenceSeparator`
32
+ * (a space by default).
42
33
  *
43
- * @param hotkey - A logical or physical string, raw binding, or ParsedHotkey
34
+ * @param hotkey - A logical or physical string, raw object, ParsedHotkey, or sequence
44
35
  * @param options - Formatting options
45
- * @returns Display text, or individual key labels when parts is true
36
+ * @returns Display text, or individual key labels when parts is true (one array per step for sequences)
46
37
  *
47
38
  * @example
48
39
  * ```ts
@@ -54,19 +45,21 @@ export type DisplayHotkey = RegisterableHotkey | ParsedHotkey | (string & {});
54
45
  *
55
46
  * formatForDisplay('Escape')
56
47
  * // Returns: 'Esc' (on all platforms)
48
+ *
49
+ * formatForDisplay(['G', 'H'])
50
+ * // Returns: 'G H'
57
51
  * ```
58
52
  */
59
- export declare function formatForDisplay(hotkey: DisplayHotkey, options: FormatDisplayOptions & {
53
+ export declare function formatForDisplay(hotkey: ReadonlyArray<DisplayHotkeyChord>, options: FormatDisplayOptions & {
54
+ parts: true;
55
+ }): Array<Array<string>>;
56
+ export declare function formatForDisplay(hotkey: DisplayHotkeyChord, options: FormatDisplayOptions & {
60
57
  parts: true;
61
58
  }): Array<string>;
62
59
  export declare function formatForDisplay(hotkey: DisplayHotkey, options?: FormatDisplayOptions & {
63
60
  parts?: false;
64
61
  }): string;
65
- export declare function formatForDisplay(hotkey: DisplayHotkey, options: FormatDisplayOptions): string | Array<string>;
66
- /**
67
- * @deprecated Use {@link formatForDisplay} instead with `useSymbols: false` option.
68
- */
69
- export declare function formatWithLabels(hotkey: RegisterableHotkey, options?: Omit<FormatDisplayOptions, 'useSymbols' | 'parts'>): string;
62
+ export declare function formatForDisplay(hotkey: DisplayHotkey, options: FormatDisplayOptions): string | Array<string> | Array<Array<string>>;
70
63
  /**
71
64
  * Options for formatting hotkeys for display.
72
65
  */
@@ -93,5 +86,7 @@ export interface FormatDisplayOptions {
93
86
  keyLabels?: Readonly<Record<string, string>>;
94
87
  /** Override the separator between display tokens. Defaults to platform-specific formatting when null/undefined. */
95
88
  separatorToken?: string | null;
89
+ /** Separator between sequence steps. Default: a space. */
90
+ sequenceSeparator?: string;
96
91
  }
97
92
  //#endregion
package/dist/format.js CHANGED
@@ -6,22 +6,6 @@ import { isModifierKey, normalizeHotkeyFromParsed, normalizeRegisterableHotkey }
6
6
 
7
7
  //#region src/format.ts
8
8
  /**
9
- * Joins stored sequence steps with spaces, preserving physical code tokens.
10
- * For user-facing labels, call formatForDisplay on each step instead.
11
- *
12
- * @param sequence - Array of hotkey strings that form the sequence
13
- * @returns A space-separated string (e.g. ['G','G'] → 'G G')
14
- *
15
- * @example
16
- * ```ts
17
- * formatHotkeySequence(['G', 'G']) // 'G G'
18
- * formatHotkeySequence(['D', 'I', 'W']) // 'D I W'
19
- * ```
20
- */
21
- function formatHotkeySequence(sequence) {
22
- return sequence.join(" ");
23
- }
24
- /**
25
9
  * Converts a ParsedHotkey back to a hotkey string.
26
10
  *
27
11
  * @param parsed - The parsed hotkey object
@@ -41,11 +25,26 @@ function formatHotkey(parsed) {
41
25
  return parts.join("+");
42
26
  }
43
27
  function formatForDisplay(hotkey, options = {}) {
28
+ if (Array.isArray(hotkey)) {
29
+ const steps = hotkey.map((step) => formatChordParts(step, options));
30
+ return options.parts ? steps : steps.map((parts) => parts.join(chordSeparator(options))).join(options.sequenceSeparator ?? " ");
31
+ }
32
+ const parts = formatChordParts(hotkey, options);
33
+ return options.parts ? parts : parts.join(chordSeparator(options));
34
+ }
35
+ function modifierSymbolsEnabled(options) {
36
+ const symbols = options.useSymbols ?? true;
37
+ return typeof symbols === "boolean" ? symbols : symbols.modifiers ?? true;
38
+ }
39
+ function chordSeparator(options) {
40
+ const platform = options.platform ?? detectPlatform();
41
+ return options.separatorToken ?? (platform === "mac" && modifierSymbolsEnabled(options) ? " " : "+");
42
+ }
43
+ function formatChordParts(hotkey, options) {
44
44
  const platform = options.platform ?? detectPlatform();
45
45
  const symbols = options.useSymbols ?? true;
46
- const modifierSymbols = typeof symbols === "boolean" ? symbols : symbols.modifiers ?? true;
46
+ const modifierSymbols = modifierSymbolsEnabled(options);
47
47
  const keySymbols = typeof symbols === "boolean" ? symbols : symbols.keys ?? true;
48
- const separator = options.separatorToken ?? (platform === "mac" && modifierSymbols ? " " : "+");
49
48
  const normalized = typeof hotkey === "object" && "modifiers" in hotkey ? normalizeHotkeyFromParsed(hotkey, platform) : normalizeRegisterableHotkey(hotkey, platform);
50
49
  const segments = splitHotkeyParts(normalized);
51
50
  if (platform === "mac") {
@@ -55,7 +54,7 @@ function formatForDisplay(hotkey, options = {}) {
55
54
  };
56
55
  segments.sort((a, b) => modifierIndex(a) - modifierIndex(b));
57
56
  }
58
- const parts = segments.map((segment) => {
57
+ return segments.map((segment) => {
59
58
  if (isModifierKey(segment)) {
60
59
  const modifier = MODIFIER_ALIASES[segment] ?? MODIFIER_ALIASES[segment.toLowerCase()];
61
60
  return platform === "mac" ? modifierSymbols ? MAC_MODIFIER_SYMBOLS[modifier] : MAC_MODIFIER_LABELS[modifier] : platform === "windows" ? WINDOWS_MODIFIER_LABELS[modifier] : LINUX_MODIFIER_LABELS[modifier];
@@ -76,17 +75,7 @@ function formatForDisplay(hotkey, options = {}) {
76
75
  const display = (keySymbols ? KEY_DISPLAY_SYMBOLS : PUNCTUATION_KEY_DISPLAY_LABELS)[displayKey] ?? displayKey;
77
76
  return numpad ? `Numpad ${display.replace(/([a-z])([A-Z])/g, "$1 $2")}` : display;
78
77
  });
79
- return options.parts ? parts : parts.join(separator);
80
- }
81
- /**
82
- * @deprecated Use {@link formatForDisplay} instead with `useSymbols: false` option.
83
- */
84
- function formatWithLabels(hotkey, options = {}) {
85
- return formatForDisplay(hotkey, {
86
- ...options,
87
- useSymbols: false
88
- });
89
78
  }
90
79
 
91
80
  //#endregion
92
- export { formatForDisplay, formatHotkey, formatHotkeySequence, formatWithLabels };
81
+ export { formatForDisplay, formatHotkey };
package/dist/hint.d.ts CHANGED
@@ -11,6 +11,7 @@ export interface HeldModifierOptions {
11
11
  * By default, any nonempty subset of the binding's modifiers qualifies; an
12
12
  * unrelated modifier hides the hint. `exact` requires every binding modifier.
13
13
  * Nonmodifier keys are ignored. This does not check registration or focus.
14
+ * A sequence reveals its hint from its first step.
14
15
  *
15
16
  * @example
16
17
  * matchesHeldModifiers('Alt+Shift+[KeyK]', ['Alt']) // true
package/dist/hint.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { MODIFIER_KEYS, normalizeKeyName } from "./constants.js";
2
- import { parseRegisterableHotkey } from "./parse.js";
2
+ import { isHotkeySequence, parseRegisterableHotkey } from "./parse.js";
3
3
 
4
4
  //#region src/hint.ts
5
5
  /**
@@ -7,6 +7,7 @@ import { parseRegisterableHotkey } from "./parse.js";
7
7
  * By default, any nonempty subset of the binding's modifiers qualifies; an
8
8
  * unrelated modifier hides the hint. `exact` requires every binding modifier.
9
9
  * Nonmodifier keys are ignored. This does not check registration or focus.
10
+ * A sequence reveals its hint from its first step.
10
11
  *
11
12
  * @example
12
13
  * matchesHeldModifiers('Alt+Shift+[KeyK]', ['Alt']) // true
@@ -15,7 +16,9 @@ import { parseRegisterableHotkey } from "./parse.js";
15
16
  function matchesHeldModifiers(hotkey, heldKeys, options = {}) {
16
17
  if (heldKeys.includes("AltGraph")) return false;
17
18
  const held = [...new Set(heldKeys.map(normalizeKeyName).filter((key) => MODIFIER_KEYS.has(key)))];
18
- const { modifiers } = parseRegisterableHotkey(hotkey, options.platform);
19
+ const first = isHotkeySequence(hotkey) ? hotkey[0] : hotkey;
20
+ if (first === void 0) return false;
21
+ const { modifiers } = parseRegisterableHotkey(first, options.platform);
19
22
  return held.length > 0 && held.every((key) => modifiers.includes(key)) && (!options.exact || held.length === modifiers.length);
20
23
  }
21
24