@tanstack/hotkeys 0.24.0 → 0.25.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/dist/_app-hotkeys-factory.d.ts +1 -3
- package/dist/_app-hotkeys-factory.js +2 -3
- package/dist/_event-target.js +25 -15
- package/dist/_keyboard-event.js +8 -5
- package/dist/_manager-ref.js +1 -2
- package/dist/_match.js +5 -3
- package/dist/_named-keys.d.ts +1 -3
- package/dist/_named-keys.js +1 -2
- package/dist/_recorder-chord.js +7 -13
- package/dist/_recording-guard.js +1 -2
- package/dist/_registration.d.ts +1 -3
- package/dist/_registration.js +11 -6
- package/dist/_server.js +2 -3
- package/dist/_store.d.ts +1 -3
- package/dist/_store.js +1 -2
- package/dist/_target-listeners.js +1 -2
- package/dist/adapter.d.ts +3 -5
- package/dist/adapter.js +2 -3
- package/dist/app-hotkeys.d.ts +4 -5
- package/dist/app-hotkeys.js +10 -21
- package/dist/conflicts.d.ts +2 -4
- package/dist/conflicts.js +5 -6
- package/dist/constants.d.ts +2 -4
- package/dist/constants.js +1 -2
- package/dist/display-labels.d.ts +1 -3
- package/dist/display-labels.js +1 -2
- package/dist/format.d.ts +2 -4
- package/dist/format.js +4 -5
- package/dist/hint.d.ts +1 -3
- package/dist/hint.js +1 -2
- package/dist/hotkey-manager.d.ts +1 -3
- package/dist/hotkey-manager.js +12 -13
- package/dist/hotkey-recorder.d.ts +19 -7
- package/dist/hotkey-recorder.js +8 -11
- package/dist/hotkey.types.d.ts +1 -3
- package/dist/index.d.ts +8 -8
- package/dist/index.js +3 -3
- package/dist/key-state-tracker.d.ts +1 -3
- package/dist/key-state-tracker.js +2 -3
- package/dist/key.types.d.ts +5 -6
- package/dist/match.d.ts +4 -5
- package/dist/match.js +7 -7
- package/dist/parse.d.ts +2 -3
- package/dist/parse.js +10 -19
- package/dist/platform.d.ts +1 -3
- package/dist/platform.js +1 -2
- package/dist/validate.d.ts +33 -13
- package/dist/validate.js +51 -34
- package/dist/vendor/@tanstack/store/dist/store.d.ts +0 -2
- package/dist/vendor/@tanstack/store/dist/types.d.ts +0 -2
- package/package.json +4 -4
- package/skills/app-hotkeys/SKILL.md +1 -1
- package/skills/hotkeys-core/SKILL.md +1 -1
package/dist/validate.d.ts
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { Hotkey } from "./hotkey.types.js";
|
|
2
|
-
//#region src/validate.d.ts
|
|
3
2
|
/** A hotkey to validate: a chord string, a sequence, or untyped input such as saved data. */
|
|
4
3
|
export type ValidatableHotkey = Hotkey | (string & {}) | ReadonlyArray<string>;
|
|
5
4
|
/**
|
|
6
5
|
* Validates a hotkey and returns any warnings or errors.
|
|
7
|
-
*
|
|
6
|
+
* Each issue has a `reason` code, so apps can write their own messages.
|
|
7
|
+
* Issues in a sequence carry the index of their `step`.
|
|
8
8
|
*
|
|
9
9
|
* Checks for:
|
|
10
10
|
* - Valid syntax (modifier+...+key format)
|
|
11
11
|
* - Known modifiers
|
|
12
|
+
* - A key that isn't a modifier
|
|
12
13
|
* - Known keys
|
|
13
14
|
*
|
|
14
15
|
* @param hotkey - The hotkey string to validate
|
|
@@ -20,10 +21,10 @@ export type ValidatableHotkey = Hotkey | (string & {}) | ReadonlyArray<string>;
|
|
|
20
21
|
* // { valid: true, warnings: [], errors: [] }
|
|
21
22
|
*
|
|
22
23
|
* validateHotkey('')
|
|
23
|
-
* // { valid: false, warnings: [], errors: [
|
|
24
|
+
* // { valid: false, warnings: [], errors: [{ reason: 'empty' }] }
|
|
24
25
|
*
|
|
25
26
|
* validateHotkey(['G', 'Foo+H'])
|
|
26
|
-
* // { valid: false, warnings: [], errors: [
|
|
27
|
+
* // { valid: false, warnings: [], errors: [{ reason: 'unknown-modifier', part: 'Foo', step: 1 }] }
|
|
27
28
|
* ```
|
|
28
29
|
*/
|
|
29
30
|
export declare function validateHotkey(hotkey: ValidatableHotkey): ValidationResult;
|
|
@@ -37,7 +38,7 @@ export declare function validateHotkey(hotkey: ValidatableHotkey): ValidationRes
|
|
|
37
38
|
* @example
|
|
38
39
|
* ```ts
|
|
39
40
|
* assertValidHotkey('Mod+S') // OK
|
|
40
|
-
* assertValidHotkey('') // Throws Error: Invalid hotkey:
|
|
41
|
+
* assertValidHotkey('') // Throws Error: [hotkeys] Invalid hotkey '': empty
|
|
41
42
|
* ```
|
|
42
43
|
*/
|
|
43
44
|
export declare function assertValidHotkey(hotkey: ValidatableHotkey): void;
|
|
@@ -51,24 +52,43 @@ export declare function assertValidHotkey(hotkey: ValidatableHotkey): void;
|
|
|
51
52
|
* @example
|
|
52
53
|
* ```ts
|
|
53
54
|
* checkHotkey('Control+escape')
|
|
54
|
-
* // Console: Hotkey 'Control+escape'
|
|
55
|
+
* // Console: [hotkeys] Hotkey 'Control+escape' may never fire: unknown-key 'escape'
|
|
55
56
|
* // Returns: true
|
|
56
57
|
*
|
|
57
58
|
* checkHotkey('Ctl+K')
|
|
58
|
-
* // Console:
|
|
59
|
+
* // Console: [hotkeys] Invalid hotkey 'Ctl+K': unknown-modifier 'Ctl'
|
|
59
60
|
* // Returns: false
|
|
60
61
|
* ```
|
|
61
62
|
*/
|
|
62
63
|
export declare function checkHotkey(hotkey: ValidatableHotkey): boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Why a hotkey is invalid or may never fire:
|
|
66
|
+
*
|
|
67
|
+
* - `'empty'`: the hotkey or sequence is empty.
|
|
68
|
+
* - `'empty-part'`: a part between `+` signs is empty or whitespace.
|
|
69
|
+
* - `'unknown-modifier'`: a modifier isn't one the library knows, such as `Ctl`.
|
|
70
|
+
* - `'invalid-physical-key'`: a bracketed key isn't a `KeyboardEvent.code`.
|
|
71
|
+
* - `'modifier-only'`: the key is a modifier, such as `Shift` or `[ShiftLeft]`.
|
|
72
|
+
* - `'unknown-key'` (warning): the key isn't written like a known key name.
|
|
73
|
+
* Key names are case-sensitive, so `escape` never matches `Escape`.
|
|
74
|
+
*/
|
|
75
|
+
export type HotkeyIssueReason = 'empty' | 'empty-part' | 'unknown-modifier' | 'invalid-physical-key' | 'modifier-only' | 'unknown-key';
|
|
76
|
+
/** One problem found by `validateHotkey`. */
|
|
77
|
+
export interface HotkeyIssue {
|
|
78
|
+
reason: HotkeyIssueReason;
|
|
79
|
+
/** The part of the chord with the problem, such as `'Ctl'` in `'Ctl+K'`. */
|
|
80
|
+
part?: string;
|
|
81
|
+
/** For a sequence, the index of the step with the problem. */
|
|
82
|
+
step?: number;
|
|
83
|
+
}
|
|
63
84
|
/**
|
|
64
85
|
* Result of validating a hotkey string.
|
|
65
86
|
*/
|
|
66
87
|
export interface ValidationResult {
|
|
67
88
|
/** Whether the hotkey is valid (can still have warnings) */
|
|
68
89
|
valid: boolean;
|
|
69
|
-
/**
|
|
70
|
-
warnings: Array<
|
|
71
|
-
/**
|
|
72
|
-
errors: Array<
|
|
73
|
-
}
|
|
74
|
-
//#endregion
|
|
90
|
+
/** Problems that may stop the hotkey from firing, such as an unknown key */
|
|
91
|
+
warnings: Array<HotkeyIssue>;
|
|
92
|
+
/** Problems that stop the hotkey from registering */
|
|
93
|
+
errors: Array<HotkeyIssue>;
|
|
94
|
+
}
|
package/dist/validate.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import { isKeyNameShape, modifierAlias, physicalCodeOf, splitChordParts } from "./_keyboard-event.js";
|
|
1
|
+
import { isKeyNameShape, isModifierCode, modifierAlias, physicalCodeOf, splitChordParts } from "./_keyboard-event.js";
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
// src/validate.ts
|
|
4
4
|
/**
|
|
5
5
|
* Validates a hotkey and returns any warnings or errors.
|
|
6
|
-
*
|
|
6
|
+
* Each issue has a `reason` code, so apps can write their own messages.
|
|
7
|
+
* Issues in a sequence carry the index of their `step`.
|
|
7
8
|
*
|
|
8
9
|
* Checks for:
|
|
9
10
|
* - Valid syntax (modifier+...+key format)
|
|
10
11
|
* - Known modifiers
|
|
12
|
+
* - A key that isn't a modifier
|
|
11
13
|
* - Known keys
|
|
12
14
|
*
|
|
13
15
|
* @param hotkey - The hotkey string to validate
|
|
@@ -19,55 +21,71 @@ import { isKeyNameShape, modifierAlias, physicalCodeOf, splitChordParts } from "
|
|
|
19
21
|
* // { valid: true, warnings: [], errors: [] }
|
|
20
22
|
*
|
|
21
23
|
* validateHotkey('')
|
|
22
|
-
* // { valid: false, warnings: [], errors: [
|
|
24
|
+
* // { valid: false, warnings: [], errors: [{ reason: 'empty' }] }
|
|
23
25
|
*
|
|
24
26
|
* validateHotkey(['G', 'Foo+H'])
|
|
25
|
-
* // { valid: false, warnings: [], errors: [
|
|
27
|
+
* // { valid: false, warnings: [], errors: [{ reason: 'unknown-modifier', part: 'Foo', step: 1 }] }
|
|
26
28
|
* ```
|
|
27
29
|
*/
|
|
28
30
|
function validateHotkey(hotkey) {
|
|
29
31
|
if (typeof hotkey !== "string") {
|
|
30
|
-
if (hotkey.length === 0) return
|
|
31
|
-
valid: false,
|
|
32
|
-
warnings: [],
|
|
33
|
-
errors: ["Sequence cannot be empty"]
|
|
34
|
-
};
|
|
32
|
+
if (hotkey.length === 0) return invalid("empty");
|
|
35
33
|
if (hotkey.length === 1) return validateChord(hotkey[0]);
|
|
36
34
|
const results = hotkey.map(validateChord);
|
|
37
|
-
const
|
|
35
|
+
const withStep = (key) => results.flatMap((result, step) => result[key].map((issue) => ({
|
|
36
|
+
...issue,
|
|
37
|
+
step
|
|
38
|
+
})));
|
|
38
39
|
return {
|
|
39
40
|
valid: results.every((result) => result.valid),
|
|
40
|
-
warnings:
|
|
41
|
-
errors:
|
|
41
|
+
warnings: withStep("warnings"),
|
|
42
|
+
errors: withStep("errors")
|
|
42
43
|
};
|
|
43
44
|
}
|
|
44
45
|
return validateChord(hotkey);
|
|
45
46
|
}
|
|
46
|
-
function
|
|
47
|
-
|
|
48
|
-
const errors = [];
|
|
49
|
-
if (!hotkey || hotkey.trim() === "") return {
|
|
47
|
+
function invalid(reason) {
|
|
48
|
+
return {
|
|
50
49
|
valid: false,
|
|
51
50
|
warnings: [],
|
|
52
|
-
errors: [
|
|
51
|
+
errors: [{ reason }]
|
|
53
52
|
};
|
|
53
|
+
}
|
|
54
|
+
function validateChord(hotkey) {
|
|
55
|
+
const warnings = [];
|
|
56
|
+
const errors = [];
|
|
57
|
+
if (!hotkey || hotkey.trim() === "") return invalid("empty");
|
|
54
58
|
const parts = splitChordParts(hotkey).map((p) => p.trim());
|
|
55
|
-
if (parts.length === 0 || parts.some((p) => p === "")) return
|
|
56
|
-
valid: false,
|
|
57
|
-
warnings: [],
|
|
58
|
-
errors: ["Invalid hotkey format: empty parts detected"]
|
|
59
|
-
};
|
|
59
|
+
if (parts.length === 0 || parts.some((p) => p === "")) return invalid("empty-part");
|
|
60
60
|
const modifierParts = parts.slice(0, -1);
|
|
61
61
|
const keyPart = parts[parts.length - 1];
|
|
62
|
-
for (const modifier of modifierParts) if (!modifierAlias(modifier)) errors.push(
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
for (const modifier of modifierParts) if (!modifierAlias(modifier)) errors.push({
|
|
63
|
+
reason: "unknown-modifier",
|
|
64
|
+
part: modifier
|
|
65
|
+
});
|
|
66
|
+
const code = physicalCodeOf(keyPart);
|
|
67
|
+
if (keyPart.length > 1 && (keyPart.startsWith("[") || keyPart.endsWith("]")) && code === void 0) errors.push({
|
|
68
|
+
reason: "invalid-physical-key",
|
|
69
|
+
part: keyPart
|
|
70
|
+
});
|
|
71
|
+
if (code !== void 0 ? isModifierCode(code) : modifierAlias(keyPart)) errors.push({
|
|
72
|
+
reason: "modifier-only",
|
|
73
|
+
part: keyPart
|
|
74
|
+
});
|
|
75
|
+
if (code === void 0 && !isKeyNameShape(keyPart)) warnings.push({
|
|
76
|
+
reason: "unknown-key",
|
|
77
|
+
part: keyPart
|
|
78
|
+
});
|
|
65
79
|
return {
|
|
66
80
|
valid: errors.length === 0,
|
|
67
81
|
warnings,
|
|
68
82
|
errors
|
|
69
83
|
};
|
|
70
84
|
}
|
|
85
|
+
/** Lists issues for a console message, such as `unknown-modifier 'Ctl'`. */
|
|
86
|
+
function describeIssues(issues) {
|
|
87
|
+
return issues.map(({ reason, part, step }) => (step === void 0 ? "" : `step ${step + 1} `) + reason + (part === void 0 ? "" : ` '${part}'`)).join(", ");
|
|
88
|
+
}
|
|
71
89
|
/**
|
|
72
90
|
* Validates a hotkey and throws an error if invalid.
|
|
73
91
|
* Useful for development-time validation.
|
|
@@ -78,12 +96,12 @@ function validateChord(hotkey) {
|
|
|
78
96
|
* @example
|
|
79
97
|
* ```ts
|
|
80
98
|
* assertValidHotkey('Mod+S') // OK
|
|
81
|
-
* assertValidHotkey('') // Throws Error: Invalid hotkey:
|
|
99
|
+
* assertValidHotkey('') // Throws Error: [hotkeys] Invalid hotkey '': empty
|
|
82
100
|
* ```
|
|
83
101
|
*/
|
|
84
102
|
function assertValidHotkey(hotkey) {
|
|
85
103
|
const result = validateHotkey(hotkey);
|
|
86
|
-
if (!result.valid) throw new Error(`Invalid hotkey '${String(hotkey)}': ${result.errors
|
|
104
|
+
if (!result.valid) throw new Error(`[hotkeys] Invalid hotkey '${String(hotkey)}': ${describeIssues(result.errors)}`);
|
|
87
105
|
}
|
|
88
106
|
/**
|
|
89
107
|
* Validates a hotkey and logs errors and warnings to the console.
|
|
@@ -95,22 +113,21 @@ function assertValidHotkey(hotkey) {
|
|
|
95
113
|
* @example
|
|
96
114
|
* ```ts
|
|
97
115
|
* checkHotkey('Control+escape')
|
|
98
|
-
* // Console: Hotkey 'Control+escape'
|
|
116
|
+
* // Console: [hotkeys] Hotkey 'Control+escape' may never fire: unknown-key 'escape'
|
|
99
117
|
* // Returns: true
|
|
100
118
|
*
|
|
101
119
|
* checkHotkey('Ctl+K')
|
|
102
|
-
* // Console:
|
|
120
|
+
* // Console: [hotkeys] Invalid hotkey 'Ctl+K': unknown-modifier 'Ctl'
|
|
103
121
|
* // Returns: false
|
|
104
122
|
* ```
|
|
105
123
|
*/
|
|
106
124
|
function checkHotkey(hotkey) {
|
|
107
125
|
const result = validateHotkey(hotkey);
|
|
108
|
-
if (process.env.NODE_ENV !== "production") {
|
|
109
|
-
if (result.errors.length > 0) console.error(`
|
|
110
|
-
if (result.warnings.length > 0) console.warn(`Hotkey '${String(hotkey)}'
|
|
126
|
+
if (typeof process !== "undefined" && process.env.NODE_ENV !== "production") {
|
|
127
|
+
if (result.errors.length > 0) console.error(`[hotkeys] Invalid hotkey '${String(hotkey)}': ${describeIssues(result.errors)}`);
|
|
128
|
+
if (result.warnings.length > 0) console.warn(`[hotkeys] Hotkey '${String(hotkey)}' may never fire: ${describeIssues(result.warnings)}`);
|
|
111
129
|
}
|
|
112
130
|
return result.valid;
|
|
113
131
|
}
|
|
114
132
|
|
|
115
|
-
//#endregion
|
|
116
133
|
export { assertValidHotkey, checkHotkey, validateHotkey };
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { Observer, Subscription } from "./types.js";
|
|
2
|
-
//#region ../../node_modules/.pnpm/@tanstack+store@0.11.2/node_modules/@tanstack/store/dist/store.d.ts
|
|
3
2
|
//#region src/store.d.ts
|
|
4
3
|
type StoreAction = (...args: Array<any>) => any;
|
|
5
4
|
type StoreActionMap = Record<string, StoreAction>;
|
|
@@ -19,5 +18,4 @@ declare class Store<T, TActions extends StoreActionMap = never> {
|
|
|
19
18
|
get(): T;
|
|
20
19
|
subscribe(observerOrFn: Observer<T> | ((value: T) => void)): Subscription;
|
|
21
20
|
}
|
|
22
|
-
//#endregion
|
|
23
21
|
export { Store, StoreAction, StoreActionMap, StoreActionsFactory };
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
//#region ../../node_modules/.pnpm/@tanstack+store@0.11.2/node_modules/@tanstack/store/dist/types.d.ts
|
|
2
1
|
type Observer<T> = {
|
|
3
2
|
next?: (value: T) => void;
|
|
4
3
|
error?: (err: unknown) => void;
|
|
@@ -7,5 +6,4 @@ type Observer<T> = {
|
|
|
7
6
|
interface Subscription {
|
|
8
7
|
unsubscribe: () => void;
|
|
9
8
|
}
|
|
10
|
-
//#endregion
|
|
11
9
|
export { Observer, Subscription };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/hotkeys",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.0",
|
|
4
4
|
"description": "Type-safe, framework-agnostic keyboard hotkey management for the browser",
|
|
5
5
|
"author": "Tanner Linsley",
|
|
6
6
|
"license": "MIT",
|
|
@@ -47,9 +47,9 @@
|
|
|
47
47
|
},
|
|
48
48
|
"scripts": {
|
|
49
49
|
"clean": "premove ./build ./dist",
|
|
50
|
-
"lint": "
|
|
51
|
-
"lint:fix": "
|
|
52
|
-
"test:
|
|
50
|
+
"lint": "oxlint ./src",
|
|
51
|
+
"lint:fix": "oxlint ./src --fix",
|
|
52
|
+
"test:oxlint": "oxlint ./src",
|
|
53
53
|
"test:lib": "vitest",
|
|
54
54
|
"test:lib:dev": "pnpm test:lib --watch",
|
|
55
55
|
"test:types": "tsc",
|
|
@@ -52,7 +52,7 @@ Augment `Register` in the adapter module you import from, such as `declare modul
|
|
|
52
52
|
|
|
53
53
|
- **Register the app hotkey, not its value.** Pass `appHotkeys['doc.save']`. Passing `.hotkey` registers the declared default, so the user's rebinding never applies, and the list reports the shortcut as `'inactive'`.
|
|
54
54
|
- **Save what `onCustomHotkeysChange` receives.** It holds only the shortcuts the user changed, so users still get new defaults. Saving every effective hotkey freezes today's defaults into their data.
|
|
55
|
-
- **Ids are a storage contract.** Saved data is keyed by id, and unknown ids are dropped
|
|
55
|
+
- **Ids are a storage contract.** Saved data is keyed by id, and unknown ids are silently dropped, so renaming an id discards users' customizations.
|
|
56
56
|
- **`setCustomHotkey` returns conflicts and changes nothing when there are any.** Check the result before reporting success, or reject the key press earlier by calling `findConflicts` in the recorder's `validate`.
|
|
57
57
|
- **Declare scopes on app hotkeys that share keys.** An app hotkey without a scope, or in `'global'`, conflicts with every other app hotkey on the same keys, so rebinding is rejected.
|
|
58
58
|
- **Conflict checks treat physical and logical bindings as different.** A recorded `Mod+[KeyP]` doesn't conflict with a declared `Mod+P`, although Command+P runs both. Declaring rebindable defaults in the recorder's form, such as `Mod+[KeyP]`, makes them comparable.
|
|
@@ -59,7 +59,7 @@ When nesting scopes, stacking dialogs, or keeping some hotkeys working during an
|
|
|
59
59
|
- **A disabled hotkey doesn't block the browser.** With `enabled: false`, `Mod+S` opens the Save Page dialog. Add `preventDefault: 'always'` to keep the action blocked while the hotkey can't fire, whether it's disabled or its scope is off. Skip it for hotkeys turned off to give the key back to the browser, such as `Mod+C`, `Mod+Z`, `Tab`, or single keys behind a user's off switch. Don't keep the hotkey enabled and return early from the callback; the hotkey would also stay `active` in app hotkey lists and show its hint.
|
|
60
60
|
- **Modifiers match exactly.** `ArrowDown` never fires while Shift is held, so register `Shift+ArrowDown` instead of checking `event.shiftKey`. Shift that a symbol needs is the exception: `?` matches Shift+/.
|
|
61
61
|
- **Chords repeat while held.** Set `requireReset: true` on toggles, or holding the keys flickers them.
|
|
62
|
-
- **Key names are case-sensitive.** Write `Escape`, `ArrowUp`, `F1`, and `+` as the types spell them; `escape`, `Esc`, `f1`, and `Plus` never match. Only letters match in any case. Hotkeys from plain JavaScript or saved data skip the types, so check them with `validateHotkey`.
|
|
62
|
+
- **Key names are case-sensitive.** Write `Escape`, `ArrowUp`, `F1`, and `+` as the types spell them; `escape`, `Esc`, `f1`, and `Plus` never match. Only letters match in any case. Hotkeys from plain JavaScript or saved data skip the types, so check them with `validateHotkey`. Its errors and warnings are reason codes such as `{ reason: 'unknown-key', part: 'escape' }`, not text, so map them to your own messages. A modifier-only hotkey, or one with an unknown modifier such as `Ctl+K`, registers nothing.
|
|
63
63
|
- **Letters follow the typed character.** For position-based controls such as WASD, use physical bindings such as `'[KeyW]'`. Keep letters for mnemonic commands such as `Mod+S`.
|
|
64
64
|
- **Each open exclusive scope needs its own name.** A reusable dialog with a fixed scope name breaks when one dialog opens another: both share one scope and both handle `Escape`. Build the name per instance.
|
|
65
65
|
- **A chord on a sequence's first key runs every time the sequence starts.** With `G` and `['G', 'G']`, the first `G` runs the chord. Later keys belong to the sequence. To show "waiting for the next key", read `matchedStepCount` from the adapter's registrations list.
|