@remcostoeten/use-shortcut 2.0.1 → 2.1.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
@@ -18,6 +18,8 @@ WIP keyboard shortcut library for React with a chainable API.
18
18
  - Runtime controls: `setScopes`, `enableScope`, `disableScope`, `getScopes`, `isScopeActive`
19
19
  - Exception predicates/presets with `.except(...)`
20
20
  - Recording mode: `$.record({ timeoutMs })`
21
+ - Structured debug stream: `$.onDebug(...)` for every keypress
22
+ - Per-shortcut attempt inspection: `result.onAttempt((matched, event, details) => ...)`
21
23
  - Conflict detection (`exact`, `sequence-prefix`)
22
24
  - Priority ordering and `stopOnMatch`
23
25
  - Global guard/filter support via `eventFilter`
@@ -68,6 +70,31 @@ function App() {
68
70
 
69
71
  If you already have a builder from `useShortcut()`, you can bulk register with `registerShortcutMap($, shortcutMap)` and unbind the returned handles on cleanup.
70
72
 
73
+ ## Debug Example
74
+
75
+ ```tsx
76
+ const $ = useShortcut({
77
+ debug: {
78
+ console: true,
79
+ includeCode: true,
80
+ includeLocation: true,
81
+ includeKeyCode: true,
82
+ },
83
+ })
84
+
85
+ const unsubscribeDebug = $.onDebug((event) => {
86
+ console.log("key", event.input.combo, event.attempts)
87
+ })
88
+
89
+ const result = $.shift.key("e").then("e").on(runProbe, {
90
+ description: "sequence probe",
91
+ })
92
+
93
+ const unsubscribeAttempt = result.onAttempt?.((matched, _event, details) => {
94
+ console.log(matched ? "matched" : details?.status, details?.steps)
95
+ })
96
+ ```
97
+
71
98
  ## Architecture Notes
72
99
 
73
100
  - Core runtime lives in `src/builder.ts`
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import{existsSync as h,mkdirSync as f,readFileSync as w,writeFileSync as x}from"fs";import{join as l,dirname as B}from"path";import{fileURLToPath as I}from"url";function m(t){return{"scopes.ts":`/** App-defined scope catalog used by the scaffolded provider. */
2
+ import{existsSync as h,mkdirSync as f,readFileSync as A,writeFileSync as x}from"fs";import{join as l,dirname as B}from"path";import{fileURLToPath as I}from"url";function m(t){return{"scopes.ts":`/** App-defined scope catalog used by the scaffolded provider. */
3
3
  export const shortcutScopes = ["global", "navigation", "editor", "modal"] as const
4
4
 
5
5
  export type ShortcutScope = (typeof shortcutScopes)[number]
@@ -82,7 +82,7 @@ export type ShortcutContextValue = {
82
82
  actions: ShortcutActions
83
83
  meta: ShortcutMeta
84
84
  }
85
- `,"runtime.ts":`import type { ShortcutMap } from "@remcostoeten/use-shortcut"
85
+ `,"runtime.ts":`import type { HandlerOptions, ShortcutMap } from "@remcostoeten/use-shortcut"
86
86
  import { shortcutRegistry, type ShortcutActionId, type ShortcutBindings } from "./registry"
87
87
  import type { ShortcutHandlers } from "./types"
88
88
 
@@ -120,11 +120,15 @@ export function buildShortcutMap(bindings: ShortcutBindings, handlers: ShortcutH
120
120
  continue
121
121
  }
122
122
 
123
+ const definitionOptions: Partial<HandlerOptions> = ("options" in definition && definition.options)
124
+ ? definition.options
125
+ : {}
126
+
123
127
  map[actionId] = {
124
128
  keys: bindings[actionId],
125
129
  handler,
126
130
  options: {
127
- ...definition.options,
131
+ ...definitionOptions,
128
132
  scopes: definition.scopes,
129
133
  description: definition.description,
130
134
  },
@@ -431,6 +435,16 @@ It follows a scalable architecture with a strict split between:
431
435
  3. Optionally expose a user-configurable key in your settings UI through \`useShortcutManager().actions.setBinding\`.
432
436
  4. Activate scopes from feature boundaries (for example editor route enters \`editor\` scope).
433
437
 
438
+ ## Wiring A React App
439
+
440
+ Scaffold the shortcut files into a React app shell:
441
+
442
+ \`\`\`bash
443
+ npx @remcostoeten/use-shortcut scaffold --framework react --target apps/shortcut-playground/src
444
+ \`\`\`
445
+
446
+ Then wire those generated files into your app root by wrapping your app in \`<ShortcutProvider handlers={...} />\`.
447
+
434
448
  ${t==="next"?"## Next.js Integration\n\n1. Create a client provider wrapper at `app/shortcut-provider.tsx` and render `<ShortcutProvider />` there.\n2. Render that provider inside `app/layout.tsx` around your app shell.\n3. Keep page/server components pure; shortcut handlers stay in client components.\n":"## React Integration\n\n1. Wrap your app root (for example in `main.tsx`) with `<ShortcutProvider />`.\n2. Keep handlers in a top-level client component and pass them to the provider via `handlers`.\n3. Use `useShortcutManager()` inside feature components to toggle scopes and bindings.\n"}
435
449
  ## Rules For Scale
436
450
 
@@ -438,16 +452,16 @@ ${t==="next"?"## Next.js Integration\n\n1. Create a client provider wrapper at `
438
452
  - Use scopes instead of conditionals in handlers.
439
453
  - Persist only key bindings, not executable handlers.
440
454
  - Treat \`registry.ts\` as your architectural contract.
441
- `}}var k=I(import.meta.url),_=B(k),e={reset:"\x1B[0m",green:"\x1B[32m",cyan:"\x1B[36m",yellow:"\x1B[33m",dim:"\x1B[2m",red:"\x1B[31m"};function r(t,o=e.reset){console.log(`${o}${t}${e.reset}`)}function O(){return l(_,"..","src")}function R(t){return l(process.cwd(),t,"use-shortcut")}function C(t,o){return l(process.cwd(),t,o)}function p(t,o,a){let s=t.indexOf(o);if(s===-1)return a;let n=t[s+1];return!n||n.startsWith("--")?a:n}function g(t,o){return t.includes(o)}var P=["index.ts","hook.ts","builder.ts","types.ts","parser.ts","constants.ts","formatter.ts","runtime/types.ts","runtime/binding.ts","runtime/conflicts.ts","runtime/debug.ts","runtime/guards.ts","runtime/keys.ts","runtime/listener.ts","runtime/recording.ts"];function E(t="hooks",o=!1){let a=O(),s=R(t);if(r(`
455
+ `}}var k=I(import.meta.url),O=B(k),e={reset:"\x1B[0m",green:"\x1B[32m",cyan:"\x1B[36m",yellow:"\x1B[33m",dim:"\x1B[2m",red:"\x1B[31m"};function r(t,o=e.reset){console.log(`${o}${t}${e.reset}`)}function _(){return l(O,"..","src")}function R(t){return l(process.cwd(),t,"use-shortcut")}function C(t,o){return l(process.cwd(),t,o)}function p(t,o,a){let s=t.indexOf(o);if(s===-1)return a;let n=t[s+1];return!n||n.startsWith("--")?a:n}function g(t,o){return t.includes(o)}var P=["index.ts","hook.ts","builder.ts","types.ts","parser.ts","constants.ts","formatter.ts","runtime/types.ts","runtime/binding.ts","runtime/conflicts.ts","runtime/debug.ts","runtime/guards.ts","runtime/keys.ts","runtime/listener.ts","runtime/recording.ts"];function E(t="hooks",o=!1){let a=_(),s=R(t);if(r(`
442
456
  use-shortcut CLI
443
457
  `,e.cyan),h(s)&&!o){r(`Directory already exists: ${s}`,e.yellow),r(`Use --force to overwrite existing files
444
- `,e.dim);return}f(s,{recursive:!0});let n=0;for(let i of P){let c=l(a,i),d=l(s,i);if(f(B(d),{recursive:!0}),!h(c)){r(`Source file not found: ${i}`,e.yellow);continue}let u=w(c,"utf-8");x(d,u),n+=1,r(` wrote ${i}`,e.green)}r(`
458
+ `,e.dim);return}f(s,{recursive:!0});let n=0;for(let i of P){let c=l(a,i),d=l(s,i);if(f(B(d),{recursive:!0}),!h(c)){r(`Source file not found: ${i}`,e.yellow);continue}let u=A(c,"utf-8");x(d,u),n+=1,r(` wrote ${i}`,e.green)}r(`
445
459
  Copied ${n} files to:`,e.green),r(` ${s}
446
460
  `,e.dim),r("Usage:",e.cyan),r(` import { useShortcut } from "@/${t}/use-shortcut"`,e.dim),r(" const $ = useShortcut()",e.dim),r(` $.mod.key("k").on(() => console.log("Search"))
447
461
  `,e.dim)}function y(t,o="src",a="shortcuts",s=!1){let n=C(o,a),i=m(t);r(`
448
462
  use-shortcut CLI
449
463
  `,e.cyan),r(`Scaffolding ${t} architecture in ${n}
450
- `,e.dim),f(n,{recursive:!0});let c=0,d=0;for(let[u,A]of Object.entries(i)){let S=l(n,u);if(h(S)&&!s){d+=1,r(` skipped ${u} (already exists)`,e.yellow);continue}x(S,A),c+=1,r(` wrote ${u}`,e.green)}r("",e.reset),r(`Architecture scaffold complete: ${c} written, ${d} skipped.`,e.green),r(`Location: ${n}
464
+ `,e.dim),f(n,{recursive:!0});let c=0,d=0;for(let[u,w]of Object.entries(i)){let S=l(n,u);if(h(S)&&!s){d+=1,r(` skipped ${u} (already exists)`,e.yellow);continue}x(S,w),c+=1,r(` wrote ${u}`,e.green)}r("",e.reset),r(`Architecture scaffold complete: ${c} written, ${d} skipped.`,e.green),r(`Location: ${n}
451
465
  `,e.dim),r("Next steps:",e.cyan),r(` 1. Open ${l(o,a,"registry.ts")} and define your action catalog`,e.dim),r(" 2. Wire app handlers into <ShortcutProvider handlers={...} />",e.dim),r(" 3. Toggle scopes from feature boundaries via useShortcutManager()",e.dim),r(` 4. Optionally expose setBinding/resetBinding in your settings UI
452
466
  `,e.dim)}function b(){r(`
453
467
  use-shortcut CLI
package/dist/index.d.ts CHANGED
@@ -101,6 +101,57 @@ type ShortcutConflict = {
101
101
  existingCombo: string;
102
102
  reason: "exact" | "sequence-prefix";
103
103
  };
104
+ type ShortcutAttemptStatus = "matched" | "partial" | "wrong-order" | "mismatch";
105
+ type ShortcutDebugTokenStatus = "match" | "wrong-order" | "mismatch";
106
+ type ShortcutDebugToken = {
107
+ token: string;
108
+ kind: "modifier" | "key";
109
+ status: ShortcutDebugTokenStatus;
110
+ };
111
+ type ShortcutDebugStep = {
112
+ index: number;
113
+ expected: string;
114
+ actual?: string;
115
+ status: "match" | "partial" | "pending" | "wrong-order" | "mismatch";
116
+ tokens: ShortcutDebugToken[];
117
+ };
118
+ type ShortcutDebugInput = {
119
+ key: string;
120
+ code: string;
121
+ location: number;
122
+ repeat: boolean;
123
+ keyCode?: number;
124
+ which?: number;
125
+ combo: string;
126
+ modifiers: ModifierState;
127
+ };
128
+ type ShortcutAttemptDebugEvent = {
129
+ combo: string;
130
+ display: string;
131
+ description?: string;
132
+ status: ShortcutAttemptStatus;
133
+ matched: boolean;
134
+ progress: number;
135
+ expectedSteps: string[];
136
+ actualSteps: string[];
137
+ stepIndex: number;
138
+ input: ShortcutDebugInput;
139
+ steps: ShortcutDebugStep[];
140
+ };
141
+ type ShortcutDebugEvent = {
142
+ input: ShortcutDebugInput;
143
+ attempts: ShortcutAttemptDebugEvent[];
144
+ };
145
+ type ShortcutDebugOptions = {
146
+ /** Log shortcut attempts to the console (default: true) */
147
+ console?: boolean;
148
+ /** Include `KeyboardEvent.code` in console output */
149
+ includeCode?: boolean;
150
+ /** Include `KeyboardEvent.location` in console output */
151
+ includeLocation?: boolean;
152
+ /** Include deprecated numeric key metadata in console output when available */
153
+ includeKeyCode?: boolean;
154
+ };
104
155
  /**
105
156
  * Options for shortcut handler registration
106
157
  */
@@ -146,7 +197,7 @@ type ShortcutResult = {
146
197
  /** Temporarily disable the shortcut */
147
198
  disable: () => void;
148
199
  /** Subscribe to shortcut attempt events (useful for visual feedback) */
149
- onAttempt?: (callback: (matched: boolean, event: KeyboardEvent) => void) => () => void;
200
+ onAttempt?: (callback: (matched: boolean, event: KeyboardEvent, details?: ShortcutAttemptDebugEvent) => void) => () => void;
150
201
  };
151
202
  /**
152
203
  * Chainable modifier builder with type-safe exhaustion
@@ -234,6 +285,8 @@ type ShortcutBuilder = ModifierChain<EmptyModifiers> & {
234
285
  getScopes: () => string[];
235
286
  /** Check if a scope is active */
236
287
  isScopeActive: (scope: string) => boolean;
288
+ /** Subscribe to every keyboard input evaluated by this shortcut registry */
289
+ onDebug: (callback: (event: ShortcutDebugEvent) => void) => () => void;
237
290
  /** Record the next key combo */
238
291
  record: (options?: ShortcutRecordingOptions) => Promise<string>;
239
292
  };
@@ -241,8 +294,8 @@ type ShortcutBuilder = ModifierChain<EmptyModifiers> & {
241
294
  * Options for the `useShortcut` hook
242
295
  */
243
296
  type UseShortcutOptions = {
244
- /** Enable debug logging to console */
245
- debug?: boolean;
297
+ /** Enable debug logging to console or configure structured debug output */
298
+ debug?: boolean | ShortcutDebugOptions;
246
299
  /** Global delay for all handlers in milliseconds */
247
300
  delay?: number;
248
301
  /** Skip shortcuts when focused on input elements (default: `true`) */
@@ -412,4 +465,4 @@ declare function createShortcutGroup(): ShortcutGroup;
412
465
  */
413
466
  declare function useShortcutGroup(): ShortcutGroup;
414
467
 
415
- export { type ActionKey, type AlphaKey, type ExceptPredicate, type ExceptPreset, type FunctionKey, type HandlerOptions, type KeyChain, ModifierAliases, type ModifierChain, ModifierDisplayOrder, ModifierDisplaySymbols, type ModifierFlags, ModifierKey, type ModifierName, type ModifierState, type NavigationKey, type NumericKey, type ParsedShortcut, Platform, type ShortcutBuilder, type ShortcutConflict, type ShortcutGroup, type ShortcutHandler, type ShortcutMap, type ShortcutMapEntry, type ShortcutMapResult, type ShortcutRecordingOptions, type ShortcutResult, type ShortcutScope, type SpecialKey, SpecialKeyMap, type SymbolKey, type UseShortcutOptions, createShortcutGroup, detectPlatform, formatShortcut, matchesAnyShortcut, matchesShortcut, parseShortcut, parseShortcuts, registerShortcutMap, useShortcut, useShortcutGroup, useShortcutMap };
468
+ export { type ActionKey, type AlphaKey, type ExceptPredicate, type ExceptPreset, type FunctionKey, type HandlerOptions, type KeyChain, ModifierAliases, type ModifierChain, ModifierDisplayOrder, ModifierDisplaySymbols, type ModifierFlags, ModifierKey, type ModifierName, type ModifierState, type NavigationKey, type NumericKey, type ParsedShortcut, Platform, type ShortcutAttemptDebugEvent, type ShortcutAttemptStatus, type ShortcutBuilder, type ShortcutConflict, type ShortcutDebugEvent, type ShortcutDebugInput, type ShortcutDebugOptions, type ShortcutDebugStep, type ShortcutDebugToken, type ShortcutDebugTokenStatus, type ShortcutGroup, type ShortcutHandler, type ShortcutMap, type ShortcutMapEntry, type ShortcutMapResult, type ShortcutRecordingOptions, type ShortcutResult, type ShortcutScope, type SpecialKey, SpecialKeyMap, type SymbolKey, type UseShortcutOptions, createShortcutGroup, detectPlatform, formatShortcut, matchesAnyShortcut, matchesShortcut, parseShortcut, parseShortcuts, registerShortcutMap, useShortcut, useShortcutGroup, useShortcutMap };