@remcostoeten/use-shortcut 2.0.1 → 2.2.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,47 @@
1
+ import { P as ParsedShortcut, e as ModifierState } from './types-Cg7uyv1m.js';
2
+
3
+ /**
4
+ * Parse a shortcut string into its components
5
+ *
6
+ * @param shortcut - Shortcut string (e.g., "cmd+s", "ctrl+shift+p")
7
+ * @returns Parsed shortcut with modifiers, key, and original string
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * const parsed = parseShortcut("cmd+s")
12
+ * // { modifiers: { meta: true, ... }, key: "s", original: "cmd+s" }
13
+ * ```
14
+ */
15
+ declare function parseShortcut(shortcut: string): ParsedShortcut;
16
+ /**
17
+ * Parse multiple shortcut strings
18
+ *
19
+ * @param shortcuts - Single shortcut or array of shortcuts
20
+ * @returns Array of parsed shortcuts
21
+ */
22
+ declare function parseShortcuts(shortcuts: string | string[]): ParsedShortcut[];
23
+ /**
24
+ * Extract modifier state from a keyboard event
25
+ *
26
+ * @param event - The keyboard event
27
+ * @returns Object with meta, ctrl, alt, shift boolean flags
28
+ */
29
+ declare function getModifiersFromEvent(event: KeyboardEvent): ModifierState;
30
+ /**
31
+ * Check if a keyboard event matches a parsed shortcut
32
+ *
33
+ * @param event - The keyboard event to check
34
+ * @param parsed - The parsed shortcut to match against
35
+ * @returns `true` if the event matches the shortcut
36
+ */
37
+ declare function matchesShortcut(event: KeyboardEvent, parsed: ParsedShortcut): boolean;
38
+ /**
39
+ * Check if a keyboard event matches any of the parsed shortcuts
40
+ *
41
+ * @param event - The keyboard event to check
42
+ * @param parsedShortcuts - Array of parsed shortcuts to match against
43
+ * @returns `true` if the event matches any shortcut
44
+ */
45
+ declare function matchesAnyShortcut(event: KeyboardEvent, parsedShortcuts: ParsedShortcut[]): boolean;
46
+
47
+ export { getModifiersFromEvent, matchesAnyShortcut, matchesShortcut, parseShortcut, parseShortcuts };
package/dist/parser.js ADDED
@@ -0,0 +1 @@
1
+ 'use strict';var a={MAC:"mac",WINDOWS:"windows",LINUX:"linux"},d=a,o=null;function m(){if(o)return o;if(typeof navigator>"u")return o=a.WINDOWS,o;let r=(navigator.userAgentData?.platform?.toLowerCase()??navigator.platform??navigator.userAgent??"").toLowerCase();return r.includes("mac")||r.includes("iphone")||r.includes("ipad")||r.includes("ipod")?(o=a.MAC,o):r.includes("linux")||r.includes("android")?(o=a.LINUX,o):(r.includes("win"),o=a.WINDOWS,o)}var e={META:"meta",CTRL:"ctrl",ALT:"alt",SHIFT:"shift"},T={command:e.META,cmd:e.META,"\u2318":e.META,meta:e.META,win:e.META,windows:e.META,super:e.META,mod:e.META,control:e.CTRL,ctrl:e.CTRL,"\u2303":e.CTRL,ctl:e.CTRL,alt:e.ALT,option:e.ALT,opt:e.ALT,"\u2325":e.ALT,shift:e.SHIFT,"\u21E7":e.SHIFT,shft:e.SHIFT},u={up:"ArrowUp",down:"ArrowDown",left:"ArrowLeft",right:"ArrowRight",home:"Home",end:"End",pageup:"PageUp",pagedown:"PageDown",enter:"Enter",return:"Enter",space:" ",spacebar:" ",tab:"Tab",backspace:"Backspace",delete:"Delete",del:"Delete",escape:"Escape",esc:"Escape",f1:"F1",f2:"F2",f3:"F3",f4:"F4",f5:"F5",f6:"F6",f7:"F7",f8:"F8",f9:"F9",f10:"F10",f11:"F11",f12:"F12",plus:"+",minus:"-",comma:",",period:".",slash:"/",backslash:"\\",bracket:"[",closebracket:"]"};function y(t){return t===" "?"space":t.toLowerCase()}function A(t){let r=m(),f=t.toLowerCase().trim().split(/[\s+-]+/).filter(Boolean);if(f.length===0)throw new Error(`Invalid shortcut: "${t}"`);let n={meta:false,ctrl:false,alt:false,shift:false},s=f.pop();for(let l of f){let p=T[l];p?l==="mod"?r===d.MAC?n.meta=true:n.ctrl=true:n[p]=true:s=l+s;}let c=u[s]||s;return {modifiers:n,key:c.length===1?c.toLowerCase():c,original:t}}function C(t){return (Array.isArray(t)?t:[t]).map(A)}function M(t){return {meta:t.metaKey,ctrl:t.ctrlKey,alt:t.altKey,shift:t.shiftKey}}function S(t,r){let i=M(t),f=y(t.key),n=i.meta===r.modifiers.meta&&i.ctrl===r.modifiers.ctrl&&i.alt===r.modifiers.alt&&i.shift===r.modifiers.shift,s=f===y(r.key);return n&&s}function F(t,r){return r.some(i=>S(t,i))}exports.getModifiersFromEvent=M;exports.matchesAnyShortcut=F;exports.matchesShortcut=S;exports.parseShortcut=A;exports.parseShortcuts=C;
@@ -0,0 +1 @@
1
+ var a={MAC:"mac",WINDOWS:"windows",LINUX:"linux"},d=a,o=null;function m(){if(o)return o;if(typeof navigator>"u")return o=a.WINDOWS,o;let r=(navigator.userAgentData?.platform?.toLowerCase()??navigator.platform??navigator.userAgent??"").toLowerCase();return r.includes("mac")||r.includes("iphone")||r.includes("ipad")||r.includes("ipod")?(o=a.MAC,o):r.includes("linux")||r.includes("android")?(o=a.LINUX,o):(r.includes("win"),o=a.WINDOWS,o)}var e={META:"meta",CTRL:"ctrl",ALT:"alt",SHIFT:"shift"},T={command:e.META,cmd:e.META,"\u2318":e.META,meta:e.META,win:e.META,windows:e.META,super:e.META,mod:e.META,control:e.CTRL,ctrl:e.CTRL,"\u2303":e.CTRL,ctl:e.CTRL,alt:e.ALT,option:e.ALT,opt:e.ALT,"\u2325":e.ALT,shift:e.SHIFT,"\u21E7":e.SHIFT,shft:e.SHIFT},u={up:"ArrowUp",down:"ArrowDown",left:"ArrowLeft",right:"ArrowRight",home:"Home",end:"End",pageup:"PageUp",pagedown:"PageDown",enter:"Enter",return:"Enter",space:" ",spacebar:" ",tab:"Tab",backspace:"Backspace",delete:"Delete",del:"Delete",escape:"Escape",esc:"Escape",f1:"F1",f2:"F2",f3:"F3",f4:"F4",f5:"F5",f6:"F6",f7:"F7",f8:"F8",f9:"F9",f10:"F10",f11:"F11",f12:"F12",plus:"+",minus:"-",comma:",",period:".",slash:"/",backslash:"\\",bracket:"[",closebracket:"]"};function y(t){return t===" "?"space":t.toLowerCase()}function A(t){let r=m(),f=t.toLowerCase().trim().split(/[\s+-]+/).filter(Boolean);if(f.length===0)throw new Error(`Invalid shortcut: "${t}"`);let n={meta:false,ctrl:false,alt:false,shift:false},s=f.pop();for(let l of f){let p=T[l];p?l==="mod"?r===d.MAC?n.meta=true:n.ctrl=true:n[p]=true:s=l+s;}let c=u[s]||s;return {modifiers:n,key:c.length===1?c.toLowerCase():c,original:t}}function C(t){return (Array.isArray(t)?t:[t]).map(A)}function M(t){return {meta:t.metaKey,ctrl:t.ctrlKey,alt:t.altKey,shift:t.shiftKey}}function S(t,r){let i=M(t),f=y(t.key),n=i.meta===r.modifiers.meta&&i.ctrl===r.modifiers.ctrl&&i.alt===r.modifiers.alt&&i.shift===r.modifiers.shift,s=f===y(r.key);return n&&s}function F(t,r){return r.some(i=>S(t,i))}export{M as getModifiersFromEvent,F as matchesAnyShortcut,S as matchesShortcut,A as parseShortcut,C as parseShortcuts};
@@ -0,0 +1,79 @@
1
+ import { p as ShortcutGroup, r as ShortcutMap, h as ShortcutBuilder, t as ShortcutMapResult, U as UseShortcutOptions } from './types-Cg7uyv1m.js';
2
+ export { A as ActionKey, a as AlphaKey, E as ExceptPredicate, b as ExceptPreset, F as FunctionKey, H as HandlerOptions, K as KeyChain, M as ModifierChain, c as ModifierFlags, d as ModifierName, N as NavigationKey, f as NumericKey, S as ShortcutAttemptDebugEvent, g as ShortcutAttemptStatus, i as ShortcutConflict, j as ShortcutDebugEvent, k as ShortcutDebugInput, l as ShortcutDebugOptions, m as ShortcutDebugStep, n as ShortcutDebugToken, o as ShortcutDebugTokenStatus, q as ShortcutHandler, s as ShortcutMapEntry, u as ShortcutRecordingOptions, v as ShortcutResult, w as ShortcutScope, x as SpecialKey, y as SymbolKey } from './types-Cg7uyv1m.js';
3
+
4
+ /**
5
+ * Registers an object-based shortcut map in one call and returns per-action handles.
6
+ *
7
+ * @param builder - Builder returned by `useShortcut()`
8
+ * @param shortcutMap - Record of action ids to key bindings, handlers, and options
9
+ * @returns A result map with one `ShortcutResult` per shortcut id
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * const $ = useShortcut()
14
+ * const results = registerShortcutMap($, {
15
+ * save: { keys: "mod+s", handler: onSave },
16
+ * nav: { keys: ["g", "d"], handler: onGoDashboard },
17
+ * })
18
+ * ```
19
+ */
20
+ declare function registerShortcutMap<T extends ShortcutMap>(builder: ShortcutBuilder, shortcutMap: T): ShortcutMapResult<T>;
21
+ /**
22
+ * React hook for registering chainable keyboard shortcuts
23
+ *
24
+ * @param options - Configuration options for the hook
25
+ * @returns A chainable shortcut builder (`$`)
26
+ *
27
+ * @example
28
+ * ```ts
29
+ * const $ = useShortcut({ activeScopes: ["editor"] })
30
+ * $.mod.key("s").on((event) => {
31
+ * event.preventDefault()
32
+ * saveDocument()
33
+ * })
34
+ * ```
35
+ */
36
+ declare function useShortcut(options?: UseShortcutOptions): ShortcutBuilder;
37
+ /**
38
+ * React hook that registers a shortcut map and automatically unbinds on cleanup.
39
+ *
40
+ * @param shortcutMap - Record of action ids to key bindings, handlers, and options
41
+ * @param options - Same options as `useShortcut()`
42
+ * @returns A map of `ShortcutResult` keyed by your shortcut ids
43
+ *
44
+ * @example
45
+ * ```ts
46
+ * const mapResults = useShortcutMap({
47
+ * save: { keys: "mod+s", handler: onSave },
48
+ * close: { keys: "escape", handler: onClose },
49
+ * })
50
+ * ```
51
+ */
52
+ declare function useShortcutMap<T extends ShortcutMap>(shortcutMap: T, options?: UseShortcutOptions): ShortcutMapResult<T>;
53
+ /**
54
+ * Creates an imperative group controller for many shortcut registrations.
55
+ *
56
+ * @returns A `ShortcutGroup` that can add and unbind multiple shortcuts together
57
+ *
58
+ * @example
59
+ * ```ts
60
+ * const group = createShortcutGroup()
61
+ * group.add($.mod.key("s").on(onSave))
62
+ * group.add($.key("escape").on(onClose))
63
+ * group.unbindAll()
64
+ * ```
65
+ */
66
+ declare function createShortcutGroup(): ShortcutGroup;
67
+ /**
68
+ * React hook that returns a stable `ShortcutGroup` instance.
69
+ *
70
+ * @returns A memoized `ShortcutGroup` tied to the component lifecycle
71
+ *
72
+ * @example
73
+ * ```ts
74
+ * const group = useShortcutGroup()
75
+ * ```
76
+ */
77
+ declare function useShortcutGroup(): ShortcutGroup;
78
+
79
+ export { ShortcutBuilder, ShortcutGroup, ShortcutMap, ShortcutMapResult, UseShortcutOptions, createShortcutGroup, registerShortcutMap, useShortcut, useShortcutGroup, useShortcutMap };
package/dist/react.js ADDED
@@ -0,0 +1 @@
1
+ 'use strict';var react=require('react');var m={MAC:"mac",WINDOWS:"windows",LINUX:"linux"},R=m,y=null;function M(){if(y)return y;if(typeof navigator>"u")return y=m.WINDOWS,y;let e=(navigator.userAgentData?.platform?.toLowerCase()??navigator.platform??navigator.userAgent??"").toLowerCase();return e.includes("mac")||e.includes("iphone")||e.includes("ipad")||e.includes("ipod")?(y=m.MAC,y):e.includes("linux")||e.includes("android")?(y=m.LINUX,y):(e.includes("win"),y=m.WINDOWS,y)}var p={META:"meta",CTRL:"ctrl",ALT:"alt",SHIFT:"shift"},z={command:p.META,cmd:p.META,"\u2318":p.META,meta:p.META,win:p.META,windows:p.META,super:p.META,mod:p.META,control:p.CTRL,ctrl:p.CTRL,"\u2303":p.CTRL,ctl:p.CTRL,alt:p.ALT,option:p.ALT,opt:p.ALT,"\u2325":p.ALT,shift:p.SHIFT,"\u21E7":p.SHIFT,shft:p.SHIFT},$={up:"ArrowUp",down:"ArrowDown",left:"ArrowLeft",right:"ArrowRight",home:"Home",end:"End",pageup:"PageUp",pagedown:"PageDown",enter:"Enter",return:"Enter",space:" ",spacebar:" ",tab:"Tab",backspace:"Backspace",delete:"Delete",del:"Delete",escape:"Escape",esc:"Escape",f1:"F1",f2:"F2",f3:"F3",f4:"F4",f5:"F5",f6:"F6",f7:"F7",f8:"F8",f9:"F9",f10:"F10",f11:"F11",f12:"F12",plus:"+",minus:"-",comma:",",period:".",slash:"/",backslash:"\\",bracket:"[",closebracket:"]"},j={[m.MAC]:{[p.META]:"\u2318",[p.CTRL]:"\u2303",[p.ALT]:"\u2325",[p.SHIFT]:"\u21E7"},[m.WINDOWS]:{[p.META]:"Ctrl",[p.CTRL]:"Ctrl",[p.ALT]:"Alt",[p.SHIFT]:"Shift"},[m.LINUX]:{[p.META]:"Super",[p.CTRL]:"Ctrl",[p.ALT]:"Alt",[p.SHIFT]:"Shift"}},D={[m.MAC]:[p.CTRL,p.ALT,p.SHIFT,p.META],[m.WINDOWS]:[p.META,p.ALT,p.SHIFT,p.CTRL],[m.LINUX]:[p.META,p.ALT,p.SHIFT,p.CTRL]};function W(t){return t===" "?"space":t.toLowerCase()}function L(t){let e=M(),o=t.toLowerCase().trim().split(/[\s+-]+/).filter(Boolean);if(o.length===0)throw new Error(`Invalid shortcut: "${t}"`);let n={meta:false,ctrl:false,alt:false,shift:false},s=o.pop();for(let i of o){let u=z[i];u?i==="mod"?e===R.MAC?n.meta=true:n.ctrl=true:n[u]=true:s=i+s;}let c=$[s]||s;return {modifiers:n,key:c.length===1?c.toLowerCase():c,original:t}}function gt(t){return {meta:t.metaKey,ctrl:t.ctrlKey,alt:t.altKey,shift:t.shiftKey}}function B(t,e){let r=gt(t),o=W(t.key),n=r.meta===e.modifiers.meta&&r.ctrl===e.modifiers.ctrl&&r.alt===e.modifiers.alt&&r.shift===e.modifiers.shift,s=o===W(e.key);return n&&s}function yt(t){return t==="ctrl"||t==="alt"||t==="shift"||t==="cmd"}function F(t){return t.split("+").map(e=>e.trim()).filter(Boolean)}function G(t){if(t)return t===true?{console:true}:t}function v(t){let e=G(t);return e?e.console!==false:false}function h(t,...e){v(t)&&console.log("[useShortcut]",...e);}function X(t,e){return {key:t.key,code:t.code,location:t.location,repeat:t.repeat,keyCode:"keyCode"in t?t.keyCode:void 0,which:"which"in t?t.which:void 0,combo:e,modifiers:{meta:t.metaKey,ctrl:t.ctrlKey,alt:t.altKey,shift:t.shiftKey}}}function bt(t,e,r){return e.has(t)?"match":r.has(t)?"wrong-order":"mismatch"}function Y(t,e,r){return t.map((o,n)=>{let s=e[n];if(!s)return {index:n,expected:o,status:"pending",tokens:[]};let c=new Set(F(o)),i=new Set(t.slice(n+1).flatMap(F)),u=F(s).map((l,b,k)=>({token:l,kind:yt(l)||b<k.length-1?"modifier":"key",status:bt(l,c,i)}));if(s===o)return {index:n,expected:o,actual:s,status:r||n<e.length-1?"match":"partial",tokens:u};let f=t.slice(n+1).includes(s)?"wrong-order":"mismatch";return {index:n,expected:o,actual:s,status:f,tokens:u}})}function J(t,e,r,o){if(o)return "matched";let n=t.slice(0,r);return n.length>0&&n.every(s=>s.status==="match"||s.status==="partial")?r<e?"partial":"mismatch":n.some(s=>s.status==="wrong-order")?"wrong-order":"mismatch"}function Q(t,e){if(!v(t))return;let r=G(t),o=[];if(r?.includeCode&&e.input.code&&o.push(`code=${e.input.code}`),r?.includeLocation&&o.push(`location=${String(e.input.location)}`),r?.includeKeyCode&&(typeof e.input.keyCode=="number"&&o.push(`keyCode=${String(e.input.keyCode)}`),typeof e.input.which=="number"&&o.push(`which=${String(e.input.which)}`)),e.attempts.length===0){console.log("[useShortcut]","key",e.input.combo,...o);return}for(let n of e.attempts)console.log("[useShortcut]",n.status.toUpperCase(),`${e.input.combo} -> ${n.combo}`,...o);}var V={ArrowUp:"\u2191",ArrowDown:"\u2193",ArrowLeft:"\u2190",ArrowRight:"\u2192",Home:"Home",End:"End",PageUp:"PgUp",PageDown:"PgDn"},Et={...V,Enter:"\u21A9",Tab:"\u21E5",Escape:"\u238B",Backspace:"\u232B",Delete:"\u2326"," ":"\u2423"},Tt={...V,Enter:"Enter",Tab:"Tab",Escape:"Esc",Backspace:"Backspace",Delete:"Del"," ":"Space"};function Z(t,e){let r=M(),o=L(t),n=j[r],s=D[r],c=[];for(let f of s)o.modifiers[f]&&c.push(n[f]);let i=Mt(o.key,r);c.push(i);let u=r===m.MAC?"":"+";return c.join(u)}function Mt(t,e){return (e===m.MAC?Et:Tt)[t]||t.toUpperCase()}function At(t){let e=M(),r=D[e],o=[];for(let n of r)n===p.CTRL&&t.ctrl&&o.push("ctrl"),n===p.ALT&&t.alt&&o.push("alt"),n===p.SHIFT&&t.shift&&o.push("shift"),n===p.META&&t.cmd&&o.push("cmd");return o}function tt(t,e){return [...At(t),e].join("+")}function et(t){return t.map(e=>Z(e)).join(" then ")}function A(t){let e=[];t.modifiers.ctrl&&e.push("ctrl"),t.modifiers.alt&&e.push("alt"),t.modifiers.shift&&e.push("shift"),t.modifiers.meta&&e.push("cmd");let r=t.key===" "||t.key==="Spacebar"?"space":t.key.toLowerCase();return [...e,r].join("+")}function N(t){let e=[];t.ctrlKey&&e.push("ctrl"),t.altKey&&e.push("alt"),t.shiftKey&&e.push("shift"),t.metaKey&&e.push("cmd");let r=t.key===" "||t.key==="Spacebar"?"space":t.key.toLowerCase();return [...e,r].join("+")}function rt(t){return N(t)}function ot(t,e){if(t.length>e.length)return false;for(let r=0;r<t.length;r+=1)if(t[r]!==e[r])return false;return true}function nt(t,e){let r=t.map(A),o=e.map(A),n=r.join(" "),s=o.join(" ");return n===s?"exact":ot(r,o)||ot(o,r)?"sequence-prefix":null}function st(t,e){let r=t.options.conflictWarnings??true;if(t.options.onConflict){t.options.onConflict(e);return}r&&console.warn(`[useShortcut] Conflict detected (${e.reason}) between "${e.combo}" and "${e.existingCombo}"`);}var P=new Set(["INPUT","TEXTAREA","SELECT"]),it={input:t=>{if(!(t.target instanceof HTMLElement))return false;let e=t.target;return P.has(e.tagName)},editable:t=>t.target instanceof HTMLElement?t.target.isContentEditable:false,typing:t=>{if(!(t.target instanceof HTMLElement))return false;let e=t.target;return P.has(e.tagName)||e.isContentEditable},modal:()=>typeof document>"u"||typeof document.querySelector!="function"?false:document.querySelector('[data-modal="true"], [role="dialog"]')!==null,disabled:t=>{if(!(t.target instanceof HTMLElement))return false;let e=t.target;return e.hasAttribute("disabled")||e.getAttribute("aria-disabled")==="true"}};function ct(t,e){return e?typeof e=="function"?e(t):Array.isArray(e)?e.some(r=>it[r]?.(t)):it[e]?.(t)??false:false}function C(t){return t?(Array.isArray(t)?t:[t]).map(e=>e.trim()).filter(Boolean):[]}function ut(t,e){if(t.size===0)return true;for(let r of t)if(e.has(r))return true;return false}function at(t){let e=t.key.toLowerCase();return e==="shift"||e==="control"||e==="alt"||e==="meta"}function Ct(t){return t.length<=1?t:[...t].sort((e,r)=>r.priority!==e.priority?r.priority-e.priority:e.id-r.id)}function wt(t,e){let r=t.options;if(r.disabled||r.eventFilter&&!r.eventFilter(e))return;let o=rt(e),n=X(e,o),s=[],c=new Set,i=t.debugListeners.size>0||v(r.debug)||[...t.listeners.values()].some(l=>l.some(b=>b.attemptCallbacks.size>0)),u=t.firstStepIndex.get(o);if(u)for(let l of u)c.add(l);for(let l of t.activeSequenceCombos)c.add(l);if(i)for(let l of t.listeners.keys())c.add(l);for(let l of c){let b=t.listeners.get(l);if(!b)continue;let k=Ct(b);for(let a of k){if(!a.isEnabled||!ut(a.scopes,t.activeScopes))continue;if(r.ignoreInputs!==false&&!a.except){let S=e.target;if(S&&(P.has(S.tagName)||S.isContentEditable))continue}if(ct(e,a.except)){h(r.debug,"Skipped due to except condition:",l);continue}let I=a.parsedSteps[a.progress],E=Date.now();a.progress>0&&E-a.lastMatchedAt>a.sequenceTimeout&&(a.progress=0),a.debugHistory.length>0&&E-a.lastDebugAt>a.sequenceTimeout&&(a.debugHistory=[]);let H=a.progress,T=false;B(e,I)?(a.progress+=1,a.lastMatchedAt=E,a.progress===a.parsedSteps.length&&(T=true,a.progress=0)):a.progress>0&&B(e,a.parsedSteps[0])?(a.progress=1,a.lastMatchedAt=E):a.progress=0,a.lastDebugAt=E,a.debugHistory.push(o),a.debugHistory.length>a.expectedSteps.length&&a.debugHistory.shift();let g=a.debugHistory.slice(-a.expectedSteps.length),_=Y(a.expectedSteps,g,T),O={combo:a.combo,display:a.display,description:a.description,status:J(_,a.expectedSteps.length,g.length,T),matched:T,progress:a.progress,expectedSteps:a.expectedSteps,actualSteps:g,stepIndex:H,input:n,steps:_};s.push(O);for(let S of a.attemptCallbacks)S(T,e,O);if(!T)continue;h(r.debug,"MATCHED:",l),a.preventDefault&&e.preventDefault(),a.stopPropagation&&e.stopPropagation();let d=()=>a.userHandler(e);if(a.delay>0?(h(r.debug,"Delaying execution by",a.delay,"ms"),setTimeout(d,a.delay)):d(),a.stopOnMatch)break}b.some(a=>a.progress>0)?t.activeSequenceCombos.add(l):t.activeSequenceCombos.delete(l);}let f={input:n,attempts:s};if(t.debugListeners.size>0)for(let l of t.debugListeners)l(f);Q(r.debug,f);}function pt(t){if(t.listener)return;let e=t.options.target??(typeof window<"u"?window:null);if(!e)return;let r=t.options.eventType??"keydown",o=n=>wt(t,n);e.addEventListener(r,o),t.listener=o,t.listenerTarget=e,t.listenerEventType=r,h(t.options.debug,"Listener attached");}function lt(t){!t.listener||!t.listenerTarget||(t.listenerTarget.removeEventListener(t.listenerEventType,t.listener),t.listener=null,t.listenerTarget=null,h(t.options.debug,"Listener detached"));}function U(t,e,r={},o){let{options:n,except:s}=t,c=t.steps;if(c.length===0)throw new Error("[useShortcut] No key specified. Use .key() to set the action key.");let i=c.map(d=>L(d)),u=i.map(A).join(" "),f=et(c),l=n.debug??false,b=s??r.except;for(let[d,S]of o.listeners.entries())for(let w of S){if(d===u)continue;let x=nt(i,w.parsedSteps);x&&st(o,{combo:u,existingCombo:d,reason:x});}let k=!r.disabled&&!n.disabled,a=r.delay??n.delay??0,I=r.sequenceTimeout??n.sequenceTimeout??800,E=new Set(C(t.scopes??r.scopes)),H=i.map(A),T=new Set;h(l,"Registering:",u,"\u2192",f,{parsedSteps:i,except:!!b,scopes:[...E]});let g={id:o.nextId++,userHandler:e,isEnabled:k,combo:u,display:f,description:r.description,attemptCallbacks:T,parsedSteps:i,expectedSteps:H,scopes:E,progress:0,lastMatchedAt:0,debugHistory:[],lastDebugAt:0,except:b,delay:a,sequenceTimeout:I,preventDefault:r.preventDefault!==false,stopPropagation:r.stopPropagation??false,stopOnMatch:r.stopOnMatch??false,priority:r.priority??0},_=o.listeners.get(u);if(_)_.push(g);else {o.listeners.set(u,[g]);let d=A(i[0]),S=o.firstStepIndex.get(d);S?S.add(u):o.firstStepIndex.set(d,new Set([u]));}return pt(o),{unbind:()=>{let d=o.listeners.get(u);if(!d)return;let S=d.filter(w=>w.id!==g.id);if(S.length===0){o.listeners.delete(u),o.activeSequenceCombos.delete(u);let w=A(i[0]),x=o.firstStepIndex.get(w);x&&(x.delete(u),x.size===0&&o.firstStepIndex.delete(w)),h(l,"Unregistered:",u);}else o.listeners.set(u,S);o.listeners.size===0&&lt(o);},display:f,combo:u,trigger:()=>e(new KeyboardEvent(o.options.eventType??"keydown")),get isEnabled(){return g.isEnabled},enable:()=>{g.isEnabled=true;},disable:()=>{g.isEnabled=false;},onAttempt:d=>(g.attemptCallbacks.add(d),()=>g.attemptCallbacks.delete(d))}}function ft(t){return (e={})=>new Promise((r,o)=>{let n=e.target??t.target??(typeof window<"u"?window:null),s=e.eventType??t.eventType??"keydown";if(!n){o(new Error("[useShortcut] Cannot record shortcut without a target."));return}let c,i=f=>{let l=f;at(l)||(l.preventDefault(),n.removeEventListener(s,i),c&&clearTimeout(c),r(N(l)));};n.addEventListener(s,i);let u=e.timeoutMs;u&&u>0&&(c=setTimeout(()=>{n.removeEventListener(s,i),o(new Error(`[useShortcut] Recording timed out after ${u}ms.`));},u));})}var xt=new Set(["ctrl","shift","alt","cmd","mod"]);function dt(t={}){let e={listeners:new Map,firstStepIndex:new Map,activeSequenceCombos:new Set,options:t,activeScopes:new Set(C(t.activeScopes)),nextId:1,debugListeners:new Set,listener:null,listenerTarget:null,listenerEventType:t.eventType??"keydown"};h(t.debug,"Builder created with options:",t);function r(n){return new Proxy({},{get(s,c){if(c==="__debug")return n.options.debug;if(xt.has(c)){let i=M(),u=c==="mod"?i===R.MAC?"cmd":"ctrl":c,f={...n,modifiers:{...n.modifiers,[u]:true}};return h(n.options.debug,`Chain: +${c} \u2192`,f.modifiers),r(f)}if(c==="in")return i=>{let u=[...C(n.scopes),...C(i)],f={...n,scopes:u};return r(f)};if(c==="setScopes")return i=>{e.activeScopes=new Set(C(i));};if(c==="enableScope")return i=>{i?.trim()&&e.activeScopes.add(i.trim());};if(c==="disableScope")return i=>{i?.trim()&&e.activeScopes.delete(i.trim());};if(c==="getScopes")return ()=>[...e.activeScopes];if(c==="isScopeActive")return i=>e.activeScopes.has(i);if(c==="onDebug")return i=>(e.debugListeners.add(i),()=>e.debugListeners.delete(i));if(c==="record")return ft(e.options);if(c==="key")return i=>{let u=tt(n.modifiers,i),f={...n,modifiers:{},steps:[...n.steps,u]};return h(n.options.debug,`Chain: .key("${i}")`),r(f)};if(c==="then")return i=>{let u=String(i).trim().toLowerCase();if(!u)throw new Error("[useShortcut] .then() requires a non-empty key or shortcut step.");let f={...n,steps:[...n.steps,u]};return h(n.options.debug,`Chain: .then("${u}")`),r(f)};if(c==="except")return i=>{let u={...n,except:i};return h(n.options.debug,"Chain: .except()",i),r(u)};if(c==="on")return (i,u)=>U(n,i,u,e);if(c==="handle")return i=>{let{handler:u,...f}=i;return U(n,u,f,e)}}})}return {builder:r({modifiers:{},steps:[],options:t}),registry:e}}function _t(t,e){if(Array.isArray(t)&&Array.isArray(e)){if(t.length!==e.length)return false;for(let r=0;r<t.length;r+=1)if(t[r]!==e[r])return false;return true}return !Array.isArray(t)&&!Array.isArray(e)?t===e:false}function Rt(t,e){let r=Object.keys(t),o=Object.keys(e);if(r.length!==o.length)return false;for(let n of r){let s=t[n],c=e[n];if(!c||!_t(s.keys,c.keys)||s.handler!==c.handler||s.options!==c.options)return false}return true}function Dt(t){if(Array.isArray(t))return t.map(r=>r.trim()).filter(Boolean);let e=t.trim();return e?e.includes(" then ")?e.split(/\s+then\s+/i).map(r=>r.trim()).filter(Boolean):e.includes(" ")&&!e.includes("+")?e.split(/\s+/).map(r=>r.trim()).filter(Boolean):[e]:[]}function Lt(t,e){let r=e.toLowerCase().split("+").map(s=>s.trim()).filter(Boolean);if(r.length===0)throw new Error("[useShortcutMap] Invalid step: empty shortcut step");let o=r.pop(),n=t;for(let s of r){if(s==="ctrl"||s==="control"){n=n.ctrl;continue}if(s==="shift"){n=n.shift;continue}if(s==="alt"||s==="option"){n=n.alt;continue}if(s==="cmd"||s==="command"||s==="meta"){n=n.cmd;continue}if(s==="mod"){n=n.mod;continue}throw new Error(`[useShortcutMap] Unsupported modifier token "${s}" in step "${e}"`)}return n.key(o)}function mt(t,e){let r={};for(let o of Object.keys(e)){let n=e[o],s=Dt(n.keys);if(s.length===0)throw new Error(`[useShortcutMap] Shortcut "${String(o)}" has no key steps`);let c=Lt(t,s[0]);for(let i of s.slice(1))c=c.then(i);r[o]=c.on(n.handler,n.options);}return r}function ht(t={}){let e=react.useRef(t);e.current=t;let{builder:r,registry:o}=react.useMemo(()=>dt(e.current),[]);return react.useEffect(()=>{if(o.options=e.current,e.current.activeScopes!==void 0){let n=Array.isArray(e.current.activeScopes)?e.current.activeScopes:[e.current.activeScopes];o.activeScopes=new Set(n.map(s=>s.trim()).filter(Boolean));}},[o,t]),react.useEffect(()=>()=>{o.listeners.clear(),o.firstStepIndex.clear(),o.activeSequenceCombos.clear(),o.listener&&o.listenerTarget&&(o.listenerTarget.removeEventListener(o.listenerEventType,o.listener),o.listener=null,o.listenerTarget=null);},[o]),r}function vt(t,e={}){let r=ht(e),o=react.useRef(t);Rt(o.current,t)||(o.current=t);let n=o.current,s=react.useRef({});return react.useEffect(()=>{let c=mt(r,n),i=s.current;for(let u of Object.keys(i))delete i[u];return Object.assign(i,c),()=>{for(let u of Object.values(c))u.unbind();for(let u of Object.keys(i))delete i[u];}},[r,n]),s.current}function St(){let t=[];return {add:(...e)=>{t.push(...e);},addMany:e=>{if(Array.isArray(e)){t.push(...e);return}t.push(...Object.values(e));},unbindAll:()=>{for(let e of t)e.unbind();t.length=0;},clear:()=>{t.length=0;},getResults:()=>[...t]}}function Pt(){let t=react.useRef(null);return t.current||(t.current=St()),t.current}exports.createShortcutGroup=St;exports.registerShortcutMap=mt;exports.useShortcut=ht;exports.useShortcutGroup=Pt;exports.useShortcutMap=vt;
package/dist/react.mjs ADDED
@@ -0,0 +1 @@
1
+ import {useRef,useMemo,useEffect}from'react';var m={MAC:"mac",WINDOWS:"windows",LINUX:"linux"},R=m,y=null;function M(){if(y)return y;if(typeof navigator>"u")return y=m.WINDOWS,y;let e=(navigator.userAgentData?.platform?.toLowerCase()??navigator.platform??navigator.userAgent??"").toLowerCase();return e.includes("mac")||e.includes("iphone")||e.includes("ipad")||e.includes("ipod")?(y=m.MAC,y):e.includes("linux")||e.includes("android")?(y=m.LINUX,y):(e.includes("win"),y=m.WINDOWS,y)}var p={META:"meta",CTRL:"ctrl",ALT:"alt",SHIFT:"shift"},z={command:p.META,cmd:p.META,"\u2318":p.META,meta:p.META,win:p.META,windows:p.META,super:p.META,mod:p.META,control:p.CTRL,ctrl:p.CTRL,"\u2303":p.CTRL,ctl:p.CTRL,alt:p.ALT,option:p.ALT,opt:p.ALT,"\u2325":p.ALT,shift:p.SHIFT,"\u21E7":p.SHIFT,shft:p.SHIFT},$={up:"ArrowUp",down:"ArrowDown",left:"ArrowLeft",right:"ArrowRight",home:"Home",end:"End",pageup:"PageUp",pagedown:"PageDown",enter:"Enter",return:"Enter",space:" ",spacebar:" ",tab:"Tab",backspace:"Backspace",delete:"Delete",del:"Delete",escape:"Escape",esc:"Escape",f1:"F1",f2:"F2",f3:"F3",f4:"F4",f5:"F5",f6:"F6",f7:"F7",f8:"F8",f9:"F9",f10:"F10",f11:"F11",f12:"F12",plus:"+",minus:"-",comma:",",period:".",slash:"/",backslash:"\\",bracket:"[",closebracket:"]"},j={[m.MAC]:{[p.META]:"\u2318",[p.CTRL]:"\u2303",[p.ALT]:"\u2325",[p.SHIFT]:"\u21E7"},[m.WINDOWS]:{[p.META]:"Ctrl",[p.CTRL]:"Ctrl",[p.ALT]:"Alt",[p.SHIFT]:"Shift"},[m.LINUX]:{[p.META]:"Super",[p.CTRL]:"Ctrl",[p.ALT]:"Alt",[p.SHIFT]:"Shift"}},D={[m.MAC]:[p.CTRL,p.ALT,p.SHIFT,p.META],[m.WINDOWS]:[p.META,p.ALT,p.SHIFT,p.CTRL],[m.LINUX]:[p.META,p.ALT,p.SHIFT,p.CTRL]};function W(t){return t===" "?"space":t.toLowerCase()}function L(t){let e=M(),o=t.toLowerCase().trim().split(/[\s+-]+/).filter(Boolean);if(o.length===0)throw new Error(`Invalid shortcut: "${t}"`);let n={meta:false,ctrl:false,alt:false,shift:false},s=o.pop();for(let i of o){let u=z[i];u?i==="mod"?e===R.MAC?n.meta=true:n.ctrl=true:n[u]=true:s=i+s;}let c=$[s]||s;return {modifiers:n,key:c.length===1?c.toLowerCase():c,original:t}}function gt(t){return {meta:t.metaKey,ctrl:t.ctrlKey,alt:t.altKey,shift:t.shiftKey}}function B(t,e){let r=gt(t),o=W(t.key),n=r.meta===e.modifiers.meta&&r.ctrl===e.modifiers.ctrl&&r.alt===e.modifiers.alt&&r.shift===e.modifiers.shift,s=o===W(e.key);return n&&s}function yt(t){return t==="ctrl"||t==="alt"||t==="shift"||t==="cmd"}function F(t){return t.split("+").map(e=>e.trim()).filter(Boolean)}function G(t){if(t)return t===true?{console:true}:t}function v(t){let e=G(t);return e?e.console!==false:false}function h(t,...e){v(t)&&console.log("[useShortcut]",...e);}function X(t,e){return {key:t.key,code:t.code,location:t.location,repeat:t.repeat,keyCode:"keyCode"in t?t.keyCode:void 0,which:"which"in t?t.which:void 0,combo:e,modifiers:{meta:t.metaKey,ctrl:t.ctrlKey,alt:t.altKey,shift:t.shiftKey}}}function bt(t,e,r){return e.has(t)?"match":r.has(t)?"wrong-order":"mismatch"}function Y(t,e,r){return t.map((o,n)=>{let s=e[n];if(!s)return {index:n,expected:o,status:"pending",tokens:[]};let c=new Set(F(o)),i=new Set(t.slice(n+1).flatMap(F)),u=F(s).map((l,b,k)=>({token:l,kind:yt(l)||b<k.length-1?"modifier":"key",status:bt(l,c,i)}));if(s===o)return {index:n,expected:o,actual:s,status:r||n<e.length-1?"match":"partial",tokens:u};let f=t.slice(n+1).includes(s)?"wrong-order":"mismatch";return {index:n,expected:o,actual:s,status:f,tokens:u}})}function J(t,e,r,o){if(o)return "matched";let n=t.slice(0,r);return n.length>0&&n.every(s=>s.status==="match"||s.status==="partial")?r<e?"partial":"mismatch":n.some(s=>s.status==="wrong-order")?"wrong-order":"mismatch"}function Q(t,e){if(!v(t))return;let r=G(t),o=[];if(r?.includeCode&&e.input.code&&o.push(`code=${e.input.code}`),r?.includeLocation&&o.push(`location=${String(e.input.location)}`),r?.includeKeyCode&&(typeof e.input.keyCode=="number"&&o.push(`keyCode=${String(e.input.keyCode)}`),typeof e.input.which=="number"&&o.push(`which=${String(e.input.which)}`)),e.attempts.length===0){console.log("[useShortcut]","key",e.input.combo,...o);return}for(let n of e.attempts)console.log("[useShortcut]",n.status.toUpperCase(),`${e.input.combo} -> ${n.combo}`,...o);}var V={ArrowUp:"\u2191",ArrowDown:"\u2193",ArrowLeft:"\u2190",ArrowRight:"\u2192",Home:"Home",End:"End",PageUp:"PgUp",PageDown:"PgDn"},Et={...V,Enter:"\u21A9",Tab:"\u21E5",Escape:"\u238B",Backspace:"\u232B",Delete:"\u2326"," ":"\u2423"},Tt={...V,Enter:"Enter",Tab:"Tab",Escape:"Esc",Backspace:"Backspace",Delete:"Del"," ":"Space"};function Z(t,e){let r=M(),o=L(t),n=j[r],s=D[r],c=[];for(let f of s)o.modifiers[f]&&c.push(n[f]);let i=Mt(o.key,r);c.push(i);let u=r===m.MAC?"":"+";return c.join(u)}function Mt(t,e){return (e===m.MAC?Et:Tt)[t]||t.toUpperCase()}function At(t){let e=M(),r=D[e],o=[];for(let n of r)n===p.CTRL&&t.ctrl&&o.push("ctrl"),n===p.ALT&&t.alt&&o.push("alt"),n===p.SHIFT&&t.shift&&o.push("shift"),n===p.META&&t.cmd&&o.push("cmd");return o}function tt(t,e){return [...At(t),e].join("+")}function et(t){return t.map(e=>Z(e)).join(" then ")}function A(t){let e=[];t.modifiers.ctrl&&e.push("ctrl"),t.modifiers.alt&&e.push("alt"),t.modifiers.shift&&e.push("shift"),t.modifiers.meta&&e.push("cmd");let r=t.key===" "||t.key==="Spacebar"?"space":t.key.toLowerCase();return [...e,r].join("+")}function N(t){let e=[];t.ctrlKey&&e.push("ctrl"),t.altKey&&e.push("alt"),t.shiftKey&&e.push("shift"),t.metaKey&&e.push("cmd");let r=t.key===" "||t.key==="Spacebar"?"space":t.key.toLowerCase();return [...e,r].join("+")}function rt(t){return N(t)}function ot(t,e){if(t.length>e.length)return false;for(let r=0;r<t.length;r+=1)if(t[r]!==e[r])return false;return true}function nt(t,e){let r=t.map(A),o=e.map(A),n=r.join(" "),s=o.join(" ");return n===s?"exact":ot(r,o)||ot(o,r)?"sequence-prefix":null}function st(t,e){let r=t.options.conflictWarnings??true;if(t.options.onConflict){t.options.onConflict(e);return}r&&console.warn(`[useShortcut] Conflict detected (${e.reason}) between "${e.combo}" and "${e.existingCombo}"`);}var P=new Set(["INPUT","TEXTAREA","SELECT"]),it={input:t=>{if(!(t.target instanceof HTMLElement))return false;let e=t.target;return P.has(e.tagName)},editable:t=>t.target instanceof HTMLElement?t.target.isContentEditable:false,typing:t=>{if(!(t.target instanceof HTMLElement))return false;let e=t.target;return P.has(e.tagName)||e.isContentEditable},modal:()=>typeof document>"u"||typeof document.querySelector!="function"?false:document.querySelector('[data-modal="true"], [role="dialog"]')!==null,disabled:t=>{if(!(t.target instanceof HTMLElement))return false;let e=t.target;return e.hasAttribute("disabled")||e.getAttribute("aria-disabled")==="true"}};function ct(t,e){return e?typeof e=="function"?e(t):Array.isArray(e)?e.some(r=>it[r]?.(t)):it[e]?.(t)??false:false}function C(t){return t?(Array.isArray(t)?t:[t]).map(e=>e.trim()).filter(Boolean):[]}function ut(t,e){if(t.size===0)return true;for(let r of t)if(e.has(r))return true;return false}function at(t){let e=t.key.toLowerCase();return e==="shift"||e==="control"||e==="alt"||e==="meta"}function Ct(t){return t.length<=1?t:[...t].sort((e,r)=>r.priority!==e.priority?r.priority-e.priority:e.id-r.id)}function wt(t,e){let r=t.options;if(r.disabled||r.eventFilter&&!r.eventFilter(e))return;let o=rt(e),n=X(e,o),s=[],c=new Set,i=t.debugListeners.size>0||v(r.debug)||[...t.listeners.values()].some(l=>l.some(b=>b.attemptCallbacks.size>0)),u=t.firstStepIndex.get(o);if(u)for(let l of u)c.add(l);for(let l of t.activeSequenceCombos)c.add(l);if(i)for(let l of t.listeners.keys())c.add(l);for(let l of c){let b=t.listeners.get(l);if(!b)continue;let k=Ct(b);for(let a of k){if(!a.isEnabled||!ut(a.scopes,t.activeScopes))continue;if(r.ignoreInputs!==false&&!a.except){let S=e.target;if(S&&(P.has(S.tagName)||S.isContentEditable))continue}if(ct(e,a.except)){h(r.debug,"Skipped due to except condition:",l);continue}let I=a.parsedSteps[a.progress],E=Date.now();a.progress>0&&E-a.lastMatchedAt>a.sequenceTimeout&&(a.progress=0),a.debugHistory.length>0&&E-a.lastDebugAt>a.sequenceTimeout&&(a.debugHistory=[]);let H=a.progress,T=false;B(e,I)?(a.progress+=1,a.lastMatchedAt=E,a.progress===a.parsedSteps.length&&(T=true,a.progress=0)):a.progress>0&&B(e,a.parsedSteps[0])?(a.progress=1,a.lastMatchedAt=E):a.progress=0,a.lastDebugAt=E,a.debugHistory.push(o),a.debugHistory.length>a.expectedSteps.length&&a.debugHistory.shift();let g=a.debugHistory.slice(-a.expectedSteps.length),_=Y(a.expectedSteps,g,T),O={combo:a.combo,display:a.display,description:a.description,status:J(_,a.expectedSteps.length,g.length,T),matched:T,progress:a.progress,expectedSteps:a.expectedSteps,actualSteps:g,stepIndex:H,input:n,steps:_};s.push(O);for(let S of a.attemptCallbacks)S(T,e,O);if(!T)continue;h(r.debug,"MATCHED:",l),a.preventDefault&&e.preventDefault(),a.stopPropagation&&e.stopPropagation();let d=()=>a.userHandler(e);if(a.delay>0?(h(r.debug,"Delaying execution by",a.delay,"ms"),setTimeout(d,a.delay)):d(),a.stopOnMatch)break}b.some(a=>a.progress>0)?t.activeSequenceCombos.add(l):t.activeSequenceCombos.delete(l);}let f={input:n,attempts:s};if(t.debugListeners.size>0)for(let l of t.debugListeners)l(f);Q(r.debug,f);}function pt(t){if(t.listener)return;let e=t.options.target??(typeof window<"u"?window:null);if(!e)return;let r=t.options.eventType??"keydown",o=n=>wt(t,n);e.addEventListener(r,o),t.listener=o,t.listenerTarget=e,t.listenerEventType=r,h(t.options.debug,"Listener attached");}function lt(t){!t.listener||!t.listenerTarget||(t.listenerTarget.removeEventListener(t.listenerEventType,t.listener),t.listener=null,t.listenerTarget=null,h(t.options.debug,"Listener detached"));}function U(t,e,r={},o){let{options:n,except:s}=t,c=t.steps;if(c.length===0)throw new Error("[useShortcut] No key specified. Use .key() to set the action key.");let i=c.map(d=>L(d)),u=i.map(A).join(" "),f=et(c),l=n.debug??false,b=s??r.except;for(let[d,S]of o.listeners.entries())for(let w of S){if(d===u)continue;let x=nt(i,w.parsedSteps);x&&st(o,{combo:u,existingCombo:d,reason:x});}let k=!r.disabled&&!n.disabled,a=r.delay??n.delay??0,I=r.sequenceTimeout??n.sequenceTimeout??800,E=new Set(C(t.scopes??r.scopes)),H=i.map(A),T=new Set;h(l,"Registering:",u,"\u2192",f,{parsedSteps:i,except:!!b,scopes:[...E]});let g={id:o.nextId++,userHandler:e,isEnabled:k,combo:u,display:f,description:r.description,attemptCallbacks:T,parsedSteps:i,expectedSteps:H,scopes:E,progress:0,lastMatchedAt:0,debugHistory:[],lastDebugAt:0,except:b,delay:a,sequenceTimeout:I,preventDefault:r.preventDefault!==false,stopPropagation:r.stopPropagation??false,stopOnMatch:r.stopOnMatch??false,priority:r.priority??0},_=o.listeners.get(u);if(_)_.push(g);else {o.listeners.set(u,[g]);let d=A(i[0]),S=o.firstStepIndex.get(d);S?S.add(u):o.firstStepIndex.set(d,new Set([u]));}return pt(o),{unbind:()=>{let d=o.listeners.get(u);if(!d)return;let S=d.filter(w=>w.id!==g.id);if(S.length===0){o.listeners.delete(u),o.activeSequenceCombos.delete(u);let w=A(i[0]),x=o.firstStepIndex.get(w);x&&(x.delete(u),x.size===0&&o.firstStepIndex.delete(w)),h(l,"Unregistered:",u);}else o.listeners.set(u,S);o.listeners.size===0&&lt(o);},display:f,combo:u,trigger:()=>e(new KeyboardEvent(o.options.eventType??"keydown")),get isEnabled(){return g.isEnabled},enable:()=>{g.isEnabled=true;},disable:()=>{g.isEnabled=false;},onAttempt:d=>(g.attemptCallbacks.add(d),()=>g.attemptCallbacks.delete(d))}}function ft(t){return (e={})=>new Promise((r,o)=>{let n=e.target??t.target??(typeof window<"u"?window:null),s=e.eventType??t.eventType??"keydown";if(!n){o(new Error("[useShortcut] Cannot record shortcut without a target."));return}let c,i=f=>{let l=f;at(l)||(l.preventDefault(),n.removeEventListener(s,i),c&&clearTimeout(c),r(N(l)));};n.addEventListener(s,i);let u=e.timeoutMs;u&&u>0&&(c=setTimeout(()=>{n.removeEventListener(s,i),o(new Error(`[useShortcut] Recording timed out after ${u}ms.`));},u));})}var xt=new Set(["ctrl","shift","alt","cmd","mod"]);function dt(t={}){let e={listeners:new Map,firstStepIndex:new Map,activeSequenceCombos:new Set,options:t,activeScopes:new Set(C(t.activeScopes)),nextId:1,debugListeners:new Set,listener:null,listenerTarget:null,listenerEventType:t.eventType??"keydown"};h(t.debug,"Builder created with options:",t);function r(n){return new Proxy({},{get(s,c){if(c==="__debug")return n.options.debug;if(xt.has(c)){let i=M(),u=c==="mod"?i===R.MAC?"cmd":"ctrl":c,f={...n,modifiers:{...n.modifiers,[u]:true}};return h(n.options.debug,`Chain: +${c} \u2192`,f.modifiers),r(f)}if(c==="in")return i=>{let u=[...C(n.scopes),...C(i)],f={...n,scopes:u};return r(f)};if(c==="setScopes")return i=>{e.activeScopes=new Set(C(i));};if(c==="enableScope")return i=>{i?.trim()&&e.activeScopes.add(i.trim());};if(c==="disableScope")return i=>{i?.trim()&&e.activeScopes.delete(i.trim());};if(c==="getScopes")return ()=>[...e.activeScopes];if(c==="isScopeActive")return i=>e.activeScopes.has(i);if(c==="onDebug")return i=>(e.debugListeners.add(i),()=>e.debugListeners.delete(i));if(c==="record")return ft(e.options);if(c==="key")return i=>{let u=tt(n.modifiers,i),f={...n,modifiers:{},steps:[...n.steps,u]};return h(n.options.debug,`Chain: .key("${i}")`),r(f)};if(c==="then")return i=>{let u=String(i).trim().toLowerCase();if(!u)throw new Error("[useShortcut] .then() requires a non-empty key or shortcut step.");let f={...n,steps:[...n.steps,u]};return h(n.options.debug,`Chain: .then("${u}")`),r(f)};if(c==="except")return i=>{let u={...n,except:i};return h(n.options.debug,"Chain: .except()",i),r(u)};if(c==="on")return (i,u)=>U(n,i,u,e);if(c==="handle")return i=>{let{handler:u,...f}=i;return U(n,u,f,e)}}})}return {builder:r({modifiers:{},steps:[],options:t}),registry:e}}function _t(t,e){if(Array.isArray(t)&&Array.isArray(e)){if(t.length!==e.length)return false;for(let r=0;r<t.length;r+=1)if(t[r]!==e[r])return false;return true}return !Array.isArray(t)&&!Array.isArray(e)?t===e:false}function Rt(t,e){let r=Object.keys(t),o=Object.keys(e);if(r.length!==o.length)return false;for(let n of r){let s=t[n],c=e[n];if(!c||!_t(s.keys,c.keys)||s.handler!==c.handler||s.options!==c.options)return false}return true}function Dt(t){if(Array.isArray(t))return t.map(r=>r.trim()).filter(Boolean);let e=t.trim();return e?e.includes(" then ")?e.split(/\s+then\s+/i).map(r=>r.trim()).filter(Boolean):e.includes(" ")&&!e.includes("+")?e.split(/\s+/).map(r=>r.trim()).filter(Boolean):[e]:[]}function Lt(t,e){let r=e.toLowerCase().split("+").map(s=>s.trim()).filter(Boolean);if(r.length===0)throw new Error("[useShortcutMap] Invalid step: empty shortcut step");let o=r.pop(),n=t;for(let s of r){if(s==="ctrl"||s==="control"){n=n.ctrl;continue}if(s==="shift"){n=n.shift;continue}if(s==="alt"||s==="option"){n=n.alt;continue}if(s==="cmd"||s==="command"||s==="meta"){n=n.cmd;continue}if(s==="mod"){n=n.mod;continue}throw new Error(`[useShortcutMap] Unsupported modifier token "${s}" in step "${e}"`)}return n.key(o)}function mt(t,e){let r={};for(let o of Object.keys(e)){let n=e[o],s=Dt(n.keys);if(s.length===0)throw new Error(`[useShortcutMap] Shortcut "${String(o)}" has no key steps`);let c=Lt(t,s[0]);for(let i of s.slice(1))c=c.then(i);r[o]=c.on(n.handler,n.options);}return r}function ht(t={}){let e=useRef(t);e.current=t;let{builder:r,registry:o}=useMemo(()=>dt(e.current),[]);return useEffect(()=>{if(o.options=e.current,e.current.activeScopes!==void 0){let n=Array.isArray(e.current.activeScopes)?e.current.activeScopes:[e.current.activeScopes];o.activeScopes=new Set(n.map(s=>s.trim()).filter(Boolean));}},[o,t]),useEffect(()=>()=>{o.listeners.clear(),o.firstStepIndex.clear(),o.activeSequenceCombos.clear(),o.listener&&o.listenerTarget&&(o.listenerTarget.removeEventListener(o.listenerEventType,o.listener),o.listener=null,o.listenerTarget=null);},[o]),r}function vt(t,e={}){let r=ht(e),o=useRef(t);Rt(o.current,t)||(o.current=t);let n=o.current,s=useRef({});return useEffect(()=>{let c=mt(r,n),i=s.current;for(let u of Object.keys(i))delete i[u];return Object.assign(i,c),()=>{for(let u of Object.values(c))u.unbind();for(let u of Object.keys(i))delete i[u];}},[r,n]),s.current}function St(){let t=[];return {add:(...e)=>{t.push(...e);},addMany:e=>{if(Array.isArray(e)){t.push(...e);return}t.push(...Object.values(e));},unbindAll:()=>{for(let e of t)e.unbind();t.length=0;},clear:()=>{t.length=0;},getResults:()=>[...t]}}function Pt(){let t=useRef(null);return t.current||(t.current=St()),t.current}export{St as createShortcutGroup,mt as registerShortcutMap,ht as useShortcut,Pt as useShortcutGroup,vt as useShortcutMap};
@@ -1,38 +1,3 @@
1
- /** Supported runtime OS identifiers used by formatter and parser normalization. */
2
- declare const OS: {
3
- readonly MAC: "mac";
4
- readonly WINDOWS: "windows";
5
- readonly LINUX: "linux";
6
- };
7
- type PlatformType = (typeof OS)[keyof typeof OS];
8
- /** Public platform constant alias (`Platform.MAC`, `Platform.WINDOWS`, `Platform.LINUX`). */
9
- declare const Platform: {
10
- readonly MAC: "mac";
11
- readonly WINDOWS: "windows";
12
- readonly LINUX: "linux";
13
- };
14
- /**
15
- * Detect the current OS platform for modifier normalization and display formatting.
16
- * Result is memoized for the page lifecycle.
17
- */
18
- declare function detectPlatform(): PlatformType;
19
- /** Canonical modifier token names used internally across parsing/formatting. */
20
- declare const ModifierKey: {
21
- readonly META: "meta";
22
- readonly CTRL: "ctrl";
23
- readonly ALT: "alt";
24
- readonly SHIFT: "shift";
25
- };
26
- type ModifierKeyType = (typeof ModifierKey)[keyof typeof ModifierKey];
27
- /** Alias map from user-facing modifier tokens to canonical modifier keys. */
28
- declare const ModifierAliases: Record<string, ModifierKeyType>;
29
- /** Alias map from human shortcut key tokens to `KeyboardEvent.key`-compatible values. */
30
- declare const SpecialKeyMap: Record<string, string>;
31
- /** Platform-specific display labels/symbols for modifier keys. */
32
- declare const ModifierDisplaySymbols: Record<PlatformType, Record<ModifierKeyType, string>>;
33
- /** Platform-specific canonical order for modifier rendering and combo normalization. */
34
- declare const ModifierDisplayOrder: Record<PlatformType, ModifierKeyType[]>;
35
-
36
1
  /** Lowercase letter keys a-z */
37
2
  type AlphaKey = "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j" | "k" | "l" | "m" | "n" | "o" | "p" | "q" | "r" | "s" | "t" | "u" | "v" | "w" | "x" | "y" | "z";
38
3
  /** Number keys 0-9 */
@@ -101,6 +66,65 @@ type ShortcutConflict = {
101
66
  existingCombo: string;
102
67
  reason: "exact" | "sequence-prefix";
103
68
  };
69
+ /** High-level match status for one shortcut attempt against the current keyboard input. */
70
+ type ShortcutAttemptStatus = "matched" | "partial" | "wrong-order" | "mismatch";
71
+ /** Token-level verdict for modifiers and keys inside debug attempt payloads. */
72
+ type ShortcutDebugTokenStatus = "match" | "wrong-order" | "mismatch";
73
+ /** Debug metadata for one expected token in a shortcut step. */
74
+ type ShortcutDebugToken = {
75
+ token: string;
76
+ kind: "modifier" | "key";
77
+ status: ShortcutDebugTokenStatus;
78
+ };
79
+ /** Debug metadata for one step in a combo or multi-step shortcut sequence. */
80
+ type ShortcutDebugStep = {
81
+ index: number;
82
+ expected: string;
83
+ actual?: string;
84
+ status: "match" | "partial" | "pending" | "wrong-order" | "mismatch";
85
+ tokens: ShortcutDebugToken[];
86
+ };
87
+ /** Normalized view of the keyboard input that triggered debug processing. */
88
+ type ShortcutDebugInput = {
89
+ key: string;
90
+ code: string;
91
+ location: number;
92
+ repeat: boolean;
93
+ keyCode?: number;
94
+ which?: number;
95
+ combo: string;
96
+ modifiers: ModifierState;
97
+ };
98
+ /** Per-shortcut debug payload describing how one registered shortcut was evaluated. */
99
+ type ShortcutAttemptDebugEvent = {
100
+ combo: string;
101
+ display: string;
102
+ description?: string;
103
+ status: ShortcutAttemptStatus;
104
+ matched: boolean;
105
+ progress: number;
106
+ expectedSteps: string[];
107
+ actualSteps: string[];
108
+ stepIndex: number;
109
+ input: ShortcutDebugInput;
110
+ steps: ShortcutDebugStep[];
111
+ };
112
+ /** Global debug payload emitted for every processed keyboard event. */
113
+ type ShortcutDebugEvent = {
114
+ input: ShortcutDebugInput;
115
+ attempts: ShortcutAttemptDebugEvent[];
116
+ };
117
+ /** Runtime debug configuration for console/debug-stream metadata. */
118
+ type ShortcutDebugOptions = {
119
+ /** Log shortcut attempts to the console (default: true) */
120
+ console?: boolean;
121
+ /** Include `KeyboardEvent.code` in console output */
122
+ includeCode?: boolean;
123
+ /** Include `KeyboardEvent.location` in console output */
124
+ includeLocation?: boolean;
125
+ /** Include deprecated numeric key metadata in console output when available */
126
+ includeKeyCode?: boolean;
127
+ };
104
128
  /**
105
129
  * Options for shortcut handler registration
106
130
  */
@@ -146,7 +170,7 @@ type ShortcutResult = {
146
170
  /** Temporarily disable the shortcut */
147
171
  disable: () => void;
148
172
  /** Subscribe to shortcut attempt events (useful for visual feedback) */
149
- onAttempt?: (callback: (matched: boolean, event: KeyboardEvent) => void) => () => void;
173
+ onAttempt?: (callback: (matched: boolean, event: KeyboardEvent, details?: ShortcutAttemptDebugEvent) => void) => () => void;
150
174
  };
151
175
  /**
152
176
  * Chainable modifier builder with type-safe exhaustion
@@ -234,6 +258,8 @@ type ShortcutBuilder = ModifierChain<EmptyModifiers> & {
234
258
  getScopes: () => string[];
235
259
  /** Check if a scope is active */
236
260
  isScopeActive: (scope: string) => boolean;
261
+ /** Subscribe to every keyboard input evaluated by this shortcut registry */
262
+ onDebug: (callback: (event: ShortcutDebugEvent) => void) => () => void;
237
263
  /** Record the next key combo */
238
264
  record: (options?: ShortcutRecordingOptions) => Promise<string>;
239
265
  };
@@ -241,8 +267,8 @@ type ShortcutBuilder = ModifierChain<EmptyModifiers> & {
241
267
  * Options for the `useShortcut` hook
242
268
  */
243
269
  type UseShortcutOptions = {
244
- /** Enable debug logging to console */
245
- debug?: boolean;
270
+ /** Enable debug logging to console or configure structured debug output */
271
+ debug?: boolean | ShortcutDebugOptions;
246
272
  /** Global delay for all handlers in milliseconds */
247
273
  delay?: number;
248
274
  /** Skip shortcuts when focused on input elements (default: `true`) */
@@ -285,131 +311,4 @@ type ShortcutGroup = {
285
311
  getResults: () => ShortcutResult[];
286
312
  };
287
313
 
288
- /**
289
- * Parse a shortcut string into its components
290
- *
291
- * @param shortcut - Shortcut string (e.g., "cmd+s", "ctrl+shift+p")
292
- * @returns Parsed shortcut with modifiers, key, and original string
293
- *
294
- * @example
295
- * ```ts
296
- * const parsed = parseShortcut("cmd+s")
297
- * // { modifiers: { meta: true, ... }, key: "s", original: "cmd+s" }
298
- * ```
299
- */
300
- declare function parseShortcut(shortcut: string): ParsedShortcut;
301
- /**
302
- * Parse multiple shortcut strings
303
- *
304
- * @param shortcuts - Single shortcut or array of shortcuts
305
- * @returns Array of parsed shortcuts
306
- */
307
- declare function parseShortcuts(shortcuts: string | string[]): ParsedShortcut[];
308
- /**
309
- * Check if a keyboard event matches a parsed shortcut
310
- *
311
- * @param event - The keyboard event to check
312
- * @param parsed - The parsed shortcut to match against
313
- * @returns `true` if the event matches the shortcut
314
- */
315
- declare function matchesShortcut(event: KeyboardEvent, parsed: ParsedShortcut): boolean;
316
- /**
317
- * Check if a keyboard event matches any of the parsed shortcuts
318
- *
319
- * @param event - The keyboard event to check
320
- * @param parsedShortcuts - Array of parsed shortcuts to match against
321
- * @returns `true` if the event matches any shortcut
322
- */
323
- declare function matchesAnyShortcut(event: KeyboardEvent, parsedShortcuts: ParsedShortcut[]): boolean;
324
-
325
- /**
326
- * Format a shortcut string for display with platform-aware symbols
327
- *
328
- * @param shortcut - Shortcut string (e.g., "cmd+s")
329
- * @param platform - Optional platform override (default: auto-detect)
330
- * @returns Formatted display string (e.g., "⌘S" on Mac, "Ctrl+S" on Windows)
331
- *
332
- * @example
333
- * ```ts
334
- * formatShortcut("cmd+s") // "⌘S" on Mac, "Ctrl+S" on Windows
335
- * formatShortcut("ctrl+shift+p", "mac") // "⌃⇧P"
336
- * ```
337
- */
338
- declare function formatShortcut(shortcut: string, platform?: PlatformType): string;
339
-
340
- /**
341
- * Registers an object-based shortcut map in one call and returns per-action handles.
342
- *
343
- * @param builder - Builder returned by `useShortcut()`
344
- * @param shortcutMap - Record of action ids to key bindings, handlers, and options
345
- * @returns A result map with one `ShortcutResult` per shortcut id
346
- *
347
- * @example
348
- * ```ts
349
- * const $ = useShortcut()
350
- * const results = registerShortcutMap($, {
351
- * save: { keys: "mod+s", handler: onSave },
352
- * nav: { keys: ["g", "d"], handler: onGoDashboard },
353
- * })
354
- * ```
355
- */
356
- declare function registerShortcutMap<T extends ShortcutMap>(builder: ShortcutBuilder, shortcutMap: T): ShortcutMapResult<T>;
357
- /**
358
- * React hook for registering chainable keyboard shortcuts
359
- *
360
- * @param options - Configuration options for the hook
361
- * @returns A chainable shortcut builder (`$`)
362
- *
363
- * @example
364
- * ```ts
365
- * const $ = useShortcut({ activeScopes: ["editor"] })
366
- * $.mod.key("s").on((event) => {
367
- * event.preventDefault()
368
- * saveDocument()
369
- * })
370
- * ```
371
- */
372
- declare function useShortcut(options?: UseShortcutOptions): ShortcutBuilder;
373
- /**
374
- * React hook that registers a shortcut map and automatically unbinds on cleanup.
375
- *
376
- * @param shortcutMap - Record of action ids to key bindings, handlers, and options
377
- * @param options - Same options as `useShortcut()`
378
- * @returns A map of `ShortcutResult` keyed by your shortcut ids
379
- *
380
- * @example
381
- * ```ts
382
- * const mapResults = useShortcutMap({
383
- * save: { keys: "mod+s", handler: onSave },
384
- * close: { keys: "escape", handler: onClose },
385
- * })
386
- * ```
387
- */
388
- declare function useShortcutMap<T extends ShortcutMap>(shortcutMap: T, options?: UseShortcutOptions): ShortcutMapResult<T>;
389
- /**
390
- * Creates an imperative group controller for many shortcut registrations.
391
- *
392
- * @returns A `ShortcutGroup` that can add and unbind multiple shortcuts together
393
- *
394
- * @example
395
- * ```ts
396
- * const group = createShortcutGroup()
397
- * group.add($.mod.key("s").on(onSave))
398
- * group.add($.key("escape").on(onClose))
399
- * group.unbindAll()
400
- * ```
401
- */
402
- declare function createShortcutGroup(): ShortcutGroup;
403
- /**
404
- * React hook that returns a stable `ShortcutGroup` instance.
405
- *
406
- * @returns A memoized `ShortcutGroup` tied to the component lifecycle
407
- *
408
- * @example
409
- * ```ts
410
- * const group = useShortcutGroup()
411
- * ```
412
- */
413
- declare function useShortcutGroup(): ShortcutGroup;
414
-
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 };
314
+ export type { ActionKey as A, ExceptPredicate as E, FunctionKey as F, HandlerOptions as H, KeyChain as K, ModifierChain as M, NavigationKey as N, ParsedShortcut as P, ShortcutAttemptDebugEvent as S, UseShortcutOptions as U, AlphaKey as a, ExceptPreset as b, ModifierFlags as c, ModifierName as d, ModifierState as e, NumericKey as f, ShortcutAttemptStatus as g, ShortcutBuilder as h, ShortcutConflict as i, ShortcutDebugEvent as j, ShortcutDebugInput as k, ShortcutDebugOptions as l, ShortcutDebugStep as m, ShortcutDebugToken as n, ShortcutDebugTokenStatus as o, ShortcutGroup as p, ShortcutHandler as q, ShortcutMap as r, ShortcutMapEntry as s, ShortcutMapResult as t, ShortcutRecordingOptions as u, ShortcutResult as v, ShortcutScope as w, SpecialKey as x, SymbolKey as y };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@remcostoeten/use-shortcut",
3
- "version": "2.0.1",
4
- "description": "Beautiful, intuitive, chainable syntax for executing keyboard shortcuts in React and Next.js with perfect TypeScript IntelliSense.",
3
+ "version": "2.2.0",
4
+ "description": "Tiny, chainable React keyboard shortcuts with sequences, scopes, and typed debug hooks.",
5
5
  "author": "Remco Stoeten",
6
6
  "license": "MIT",
7
7
  "homepage": "https://github.com/remcostoeten/use-shortcut#readme",
@@ -23,29 +23,65 @@
23
23
  "chainable"
24
24
  ],
25
25
  "sideEffects": false,
26
- "bin": {
27
- "use-shortcut": "dist/cli/index.mjs"
28
- },
29
26
  "main": "./dist/index.js",
30
27
  "module": "./dist/index.mjs",
31
28
  "types": "./dist/index.d.ts",
32
29
  "exports": {
33
30
  ".": {
34
31
  "import": {
35
- "types": "./dist/index.d.mts",
32
+ "types": "./dist/index.d.ts",
36
33
  "default": "./dist/index.mjs"
37
34
  },
38
35
  "require": {
39
36
  "types": "./dist/index.d.ts",
40
37
  "default": "./dist/index.js"
41
38
  }
39
+ },
40
+ "./react": {
41
+ "import": {
42
+ "types": "./dist/react.d.ts",
43
+ "default": "./dist/react.mjs"
44
+ },
45
+ "require": {
46
+ "types": "./dist/react.d.ts",
47
+ "default": "./dist/react.js"
48
+ }
49
+ },
50
+ "./parser": {
51
+ "import": {
52
+ "types": "./dist/parser.d.ts",
53
+ "default": "./dist/parser.mjs"
54
+ },
55
+ "require": {
56
+ "types": "./dist/parser.d.ts",
57
+ "default": "./dist/parser.js"
58
+ }
59
+ },
60
+ "./formatter": {
61
+ "import": {
62
+ "types": "./dist/formatter.d.ts",
63
+ "default": "./dist/formatter.mjs"
64
+ },
65
+ "require": {
66
+ "types": "./dist/formatter.d.ts",
67
+ "default": "./dist/formatter.js"
68
+ }
69
+ },
70
+ "./constants": {
71
+ "import": {
72
+ "types": "./dist/constants.d.ts",
73
+ "default": "./dist/constants.mjs"
74
+ },
75
+ "require": {
76
+ "types": "./dist/constants.d.ts",
77
+ "default": "./dist/constants.js"
78
+ }
42
79
  }
43
80
  },
44
81
  "files": [
45
82
  "dist",
46
83
  "README.md",
47
- "LICENSE",
48
- "CHANGELOG.md"
84
+ "LICENSE"
49
85
  ],
50
86
  "engines": {
51
87
  "node": ">=18.0.0"
@@ -54,12 +90,16 @@
54
90
  "access": "public"
55
91
  },
56
92
  "scripts": {
57
- "build": "tsup",
93
+ "build": "tsup && node scripts/prune-dist.mjs",
58
94
  "dev": "tsup --watch",
59
95
  "test": "vitest run --environment jsdom",
96
+ "test:watch": "vitest --environment jsdom",
97
+ "test:entrypoints": "vitest run --environment jsdom src/__tests__/entrypoints.test.ts",
98
+ "test:features": "vitest run --environment jsdom src/__tests__/features.test.ts",
60
99
  "typecheck": "tsc --noEmit",
61
100
  "docs:api": "node scripts/generate-api-reference.mjs",
62
101
  "docs:check": "bun run docs:api && node scripts/docs-check.mjs",
102
+ "verify": "bun run typecheck && bun run test && bun run build && bun run docs:check",
63
103
  "clean": "rm -rf dist",
64
104
  "prepublishOnly": "npm run build"
65
105
  },