@tanstack/hotkeys 0.18.0 → 0.20.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.
@@ -1,5 +1,5 @@
1
1
  import { MODIFIER_KEYS, normalizeKeyName } from "./constants.js";
2
- import { Store } from "@tanstack/store";
2
+ import { createStore } from "./_store.js";
3
3
 
4
4
  //#region src/key-state-tracker.ts
5
5
  /**
@@ -51,7 +51,7 @@ var KeyStateTracker = class KeyStateTracker {
51
51
  * The TanStack Store instance containing the tracker state.
52
52
  * Use this to subscribe to state changes or access current state.
53
53
  */
54
- store = new Store(getDefaultKeyStateTrackerState());
54
+ store = createStore(getDefaultKeyStateTrackerState());
55
55
  #heldEntries = /* @__PURE__ */ new Map();
56
56
  /** Window listeners as `[type, listener, capture]`, removed together on destroy. */
57
57
  #listeners = [];
@@ -42,8 +42,8 @@ export declare function validateHotkey(hotkey: ValidatableHotkey): ValidationRes
42
42
  */
43
43
  export declare function assertValidHotkey(hotkey: ValidatableHotkey): void;
44
44
  /**
45
- * Validates a hotkey and logs warnings to the console.
46
- * Useful for development-time feedback.
45
+ * Validates a hotkey and logs errors and warnings to the console.
46
+ * Useful for development-time feedback. Logs nothing in production builds.
47
47
  *
48
48
  * @param hotkey - The hotkey string to validate
49
49
  * @returns True if the hotkey is valid (may still have warnings)
package/dist/validate.js CHANGED
@@ -1,5 +1,4 @@
1
- import { ALL_KEYS, normalizeKeyName } from "./constants.js";
2
- import { modifierAlias, physicalCodeOf, splitChordParts } from "./_keyboard-event.js";
1
+ import { isKeyNameShape, modifierAlias, physicalCodeOf, splitChordParts } from "./_keyboard-event.js";
3
2
 
4
3
  //#region src/validate.ts
5
4
  /**
@@ -62,17 +61,13 @@ function validateChord(hotkey) {
62
61
  const keyPart = parts[parts.length - 1];
63
62
  for (const modifier of modifierParts) if (!modifierAlias(modifier)) errors.push(`Unknown modifier: '${modifier}'`);
64
63
  if (keyPart.length > 1 && (keyPart.startsWith("[") || keyPart.endsWith("]")) && physicalCodeOf(keyPart) === void 0) errors.push("Invalid physical key: use a bracketed KeyboardEvent.code, such as [KeyS]");
65
- if (!isKnownKey(normalizeKeyName(keyPart))) warnings.push(`Unknown key: '${keyPart}'. This may still work but won't have type-safe autocomplete.`);
64
+ if (physicalCodeOf(keyPart) === void 0 && !isKeyNameShape(keyPart)) warnings.push(`Unknown key: '${keyPart}'. Key names are case-sensitive, such as 'Escape' and 'F1'.`);
66
65
  return {
67
66
  valid: errors.length === 0,
68
67
  warnings,
69
68
  errors
70
69
  };
71
70
  }
72
- /** Physical codes, single characters, and named keys are known. */
73
- function isKnownKey(key) {
74
- return physicalCodeOf(key) !== void 0 || Array.from(key).length === 1 || ALL_KEYS.has(key);
75
- }
76
71
  /**
77
72
  * Validates a hotkey and throws an error if invalid.
78
73
  * Useful for development-time validation.
@@ -91,8 +86,8 @@ function assertValidHotkey(hotkey) {
91
86
  if (!result.valid) throw new Error(`Invalid hotkey '${String(hotkey)}': ${result.errors.join(", ")}`);
92
87
  }
93
88
  /**
94
- * Validates a hotkey and logs warnings to the console.
95
- * Useful for development-time feedback.
89
+ * Validates a hotkey and logs errors and warnings to the console.
90
+ * Useful for development-time feedback. Logs nothing in production builds.
96
91
  *
97
92
  * @param hotkey - The hotkey string to validate
98
93
  * @returns True if the hotkey is valid (may still have warnings)
@@ -106,8 +101,10 @@ function assertValidHotkey(hotkey) {
106
101
  */
107
102
  function checkHotkey(hotkey) {
108
103
  const result = validateHotkey(hotkey);
109
- if (result.errors.length > 0) console.error(`Hotkey '${String(hotkey)}' errors:`, result.errors.join("; "));
110
- if (result.warnings.length > 0) console.warn(`Hotkey '${String(hotkey)}' warnings:`, result.warnings.join("; "));
104
+ if (process.env.NODE_ENV !== "production") {
105
+ if (result.errors.length > 0) console.error(`Hotkey '${String(hotkey)}' errors:`, result.errors.join("; "));
106
+ if (result.warnings.length > 0) console.warn(`Hotkey '${String(hotkey)}' warnings:`, result.warnings.join("; "));
107
+ }
111
108
  return result.valid;
112
109
  }
113
110
 
@@ -0,0 +1,23 @@
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
+ //#region src/store.d.ts
4
+ type StoreAction = (...args: Array<any>) => any;
5
+ type StoreActionMap = Record<string, StoreAction>;
6
+ type StoreActionsFactory<T, TActions extends StoreActionMap> = (store: {
7
+ setState: Store<T>['setState'];
8
+ get: Store<T>['get'];
9
+ }) => TActions;
10
+ type NonFunction<T> = T extends ((...args: Array<any>) => any) ? never : T;
11
+ declare class Store<T, TActions extends StoreActionMap = never> {
12
+ private atom;
13
+ readonly actions: TActions;
14
+ constructor(getValue: (prev?: NoInfer<T>) => T);
15
+ constructor(initialValue: T);
16
+ constructor(initialValue: NonFunction<T>, actionsFactory: StoreActionsFactory<T, TActions>);
17
+ setState(updater: (prev: T) => T): void;
18
+ get state(): T;
19
+ get(): T;
20
+ subscribe(observerOrFn: Observer<T> | ((value: T) => void)): Subscription;
21
+ }
22
+ //#endregion
23
+ export { Store, StoreAction, StoreActionMap, StoreActionsFactory };
@@ -0,0 +1,11 @@
1
+ //#region ../../node_modules/.pnpm/@tanstack+store@0.11.2/node_modules/@tanstack/store/dist/types.d.ts
2
+ type Observer<T> = {
3
+ next?: (value: T) => void;
4
+ error?: (err: unknown) => void;
5
+ complete?: () => void;
6
+ };
7
+ interface Subscription {
8
+ unsubscribe: () => void;
9
+ }
10
+ //#endregion
11
+ export { Observer, Subscription };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/hotkeys",
3
- "version": "0.18.0",
3
+ "version": "0.20.0",
4
4
  "description": "Type-safe, framework-agnostic keyboard hotkey management for the browser",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
@@ -39,9 +39,12 @@
39
39
  "dist",
40
40
  "skills"
41
41
  ],
42
- "dependencies": {
42
+ "devDependencies": {
43
43
  "@tanstack/store": "^0.11.2"
44
44
  },
45
+ "inlinedDependencies": {
46
+ "@tanstack/store": "0.11.2"
47
+ },
45
48
  "scripts": {
46
49
  "clean": "premove ./build ./dist",
47
50
  "lint": "eslint ./src",
@@ -33,7 +33,7 @@ The library owns the keyboard. Register every shortcut with your framework adapt
33
33
  | Instead of | Use |
34
34
  | --------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
35
35
  | A `keydown` listener that checks `event.key`, `metaKey`, or `ctrlKey` | A registered hotkey such as `'Mod+S'`. Inside a widget's own key handler, `matchesKeyboardEvent(event, 'Mod+Enter')`. |
36
- | `isMac` checks and `⌘`/`Ctrl` label maps | `Mod` in the hotkey, and `formatForDisplay(hotkey)` for labels |
36
+ | `isMac` checks and `⌘`/`Ctrl` label maps | `Mod` in the hotkey, `formatForDisplay(hotkey)` for labels, or `formatForDisplay({ key })` for held keys |
37
37
  | Skipping events from text fields, calling `preventDefault` | The defaults: single keys and Shift/Alt chords are skipped while typing, `Mod` chords and `Escape` still fire, and matched keys are prevented |
38
38
  | A shortcut registry, service, context, or wrapper hook | The adapter API, called where each action lives. For one list of every shortcut, load `@tanstack/hotkeys#app-hotkeys`. |
39
39
  | A "shortcuts enabled" flag or topmost-dialog tracking | Scopes, with `exclusive` for dialogs |
@@ -59,6 +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
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`.
63
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.
64
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.