@tanstack/hotkeys-devtools 1.3.5 → 1.4.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
 
@@ -7,6 +7,12 @@ import { HotkeyManager, KeyStateTracker, toHotkeyRegistrationView } from "@tanst
7
7
  const HotkeysDevtoolsContext = createContext({
8
8
  registrations: () => [],
9
9
  appHotkeys: () => [],
10
+ scopes: () => ({
11
+ active: [],
12
+ exclusive: null,
13
+ allowed: [],
14
+ suspended: []
15
+ }),
10
16
  heldKeys: () => [],
11
17
  heldCodes: () => ({}),
12
18
  sequenceProgressNow: () => Date.now()
@@ -20,6 +26,7 @@ function HotkeysContextProvider(props) {
20
26
  const [registrations, setRegistrations] = createSignal(readRegistrations(manager));
21
27
  const [appHotkeysStore, setAppHotkeysStore] = createSignal(manager.appHotkeys.state);
22
28
  const [appHotkeysVersion, setAppHotkeysVersion] = createSignal(0);
29
+ const [scopes, setScopes] = createSignal(manager.scopes.state);
23
30
  const [heldKeys, setHeldKeys] = createSignal(tracker.store.state.heldKeys);
24
31
  const [heldCodes, setHeldCodes] = createSignal(tracker.store.state.heldCodes);
25
32
  createEffect(() => {
@@ -28,6 +35,12 @@ function HotkeysContextProvider(props) {
28
35
  }).unsubscribe;
29
36
  onCleanup(() => unsubscribe());
30
37
  });
38
+ createEffect(() => {
39
+ const unsubscribe = manager.scopes.subscribe(() => {
40
+ setScopes(manager.scopes.state);
41
+ }).unsubscribe;
42
+ onCleanup(() => unsubscribe());
43
+ });
31
44
  createEffect(() => {
32
45
  const unsubscribe = manager.appHotkeys.subscribe(() => {
33
46
  setAppHotkeysStore(manager.appHotkeys.state);
@@ -77,6 +90,7 @@ function HotkeysContextProvider(props) {
77
90
  value: {
78
91
  registrations,
79
92
  appHotkeys,
93
+ scopes,
80
94
  heldKeys,
81
95
  heldCodes,
82
96
  sequenceProgressNow
@@ -20,7 +20,8 @@ function useRegistrationConflicts(registration) {
20
20
  eventType: reg.options.eventType,
21
21
  excludeIds: [reg.id]
22
22
  };
23
- const overlapping = findHotkeyConflicts(binding, options).map((c) => c.registration);
23
+ const scope = reg.options.scope ?? "global";
24
+ const overlapping = findHotkeyConflicts(binding, options).map((c) => c.registration).filter((r) => (r.options.scope ?? "global") === scope);
24
25
  const ids = new Set(overlapping.map((r) => r.id));
25
26
  return {
26
27
  overlapping,
@@ -40,7 +41,7 @@ function formatRegistration(reg) {
40
41
  }
41
42
  /** Sequences may mix logical keys and physical codes, so report both when needed. */
42
43
  function getBindingKind(reg) {
43
- const parsed = reg.kind === "sequence" ? reg.parsedSequence : [reg.parsedHotkey];
44
+ const parsed = reg.kind === "sequence" ? reg.parsedSequence : [reg.parsedChord];
44
45
  const codes = parsed.filter((step) => step.code !== void 0).length;
45
46
  return codes === 0 ? "Key" : codes === parsed.length ? "Code" : "Mixed";
46
47
  }
@@ -0,0 +1,41 @@
1
+ //#region src/_scopes.ts
2
+ const scopeStatusDescriptions = {
3
+ exclusive: "Exclusive: only hotkeys in this scope, scopes nested in it, and scopes it allows fire",
4
+ allowed: "Allowed: the exclusive scope lets hotkeys in this scope fire",
5
+ active: "Active: hotkeys in this scope fire",
6
+ suspended: "Suspended: another scope is exclusive",
7
+ inactive: "Inactive: nothing has activated this scope"
8
+ };
9
+ function getScopeStatus(scope, state) {
10
+ if (scope === state.exclusive) return "exclusive";
11
+ if (state.allowed.includes(scope)) return "allowed";
12
+ if (state.suspended.includes(scope)) return "suspended";
13
+ return scope === "global" || state.active.includes(scope) ? "active" : "inactive";
14
+ }
15
+ /**
16
+ * Groups registrations by scope, highest precedence first: the exclusive
17
+ * scope, other activated scopes from most to least recent, `'global'`, then
18
+ * inactive scopes by name.
19
+ */
20
+ function groupByScope(registrations, state) {
21
+ const byScope = /* @__PURE__ */ new Map();
22
+ for (const registration of registrations) {
23
+ const scope = registration.options.scope ?? "global";
24
+ const list = byScope.get(scope);
25
+ if (list) list.push(registration);
26
+ else byScope.set(scope, [registration]);
27
+ }
28
+ const rank = (scope) => {
29
+ if (scope === state.exclusive) return Infinity;
30
+ if (scope === "global") return 0;
31
+ return state.active.indexOf(scope) + 1 || -1;
32
+ };
33
+ return [...byScope.keys()].sort((a, b) => rank(b) - rank(a) || a.localeCompare(b)).map((scope) => ({
34
+ scope,
35
+ status: getScopeStatus(scope, state),
36
+ registrations: byScope.get(scope)
37
+ }));
38
+ }
39
+
40
+ //#endregion
41
+ export { getScopeStatus, groupByScope, scopeStatusDescriptions };