@tanstack/solid-hotkeys 0.6.0 → 0.7.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.
@@ -62,6 +62,16 @@ function createHotkey(hotkey, callback, options = {}) {
62
62
  const defaultOptions = require_HotkeysProvider.useDefaultHotkeysOptions();
63
63
  const manager = (0, _tanstack_hotkeys.getHotkeyManager)();
64
64
  let registration = null;
65
+ let lastHotkeyString = null;
66
+ let lastTarget = null;
67
+ (0, solid_js.onCleanup)(() => {
68
+ if (registration?.isActive) {
69
+ registration.unregister();
70
+ registration = null;
71
+ }
72
+ lastHotkeyString = null;
73
+ lastTarget = null;
74
+ });
65
75
  (0, solid_js.createEffect)(() => {
66
76
  const resolvedHotkey = typeof hotkey === "function" ? hotkey() : hotkey;
67
77
  const resolvedOptions = typeof options === "function" ? options() : options;
@@ -72,12 +82,25 @@ function createHotkey(hotkey, callback, options = {}) {
72
82
  const platform = mergedOptions.platform ?? (0, _tanstack_hotkeys.detectPlatform)();
73
83
  const hotkeyString = typeof resolvedHotkey === "string" ? resolvedHotkey : (0, _tanstack_hotkeys.formatHotkey)((0, _tanstack_hotkeys.rawHotkeyToParsedHotkey)(resolvedHotkey, platform));
74
84
  const resolvedTarget = "target" in mergedOptions ? mergedOptions.target ?? null : typeof document !== "undefined" ? document : null;
75
- if (!resolvedTarget) return;
85
+ if (!resolvedTarget) {
86
+ if (registration?.isActive) {
87
+ registration.unregister();
88
+ registration = null;
89
+ }
90
+ lastHotkeyString = null;
91
+ lastTarget = null;
92
+ return;
93
+ }
94
+ const { target: _target, ...optionsWithoutTarget } = mergedOptions;
95
+ if (registration?.isActive && lastHotkeyString === hotkeyString && lastTarget === resolvedTarget) {
96
+ registration.callback = callback;
97
+ registration.setOptions(optionsWithoutTarget);
98
+ return;
99
+ }
76
100
  if (registration?.isActive) {
77
101
  registration.unregister();
78
102
  registration = null;
79
103
  }
80
- const { target: _target, ...optionsWithoutTarget } = mergedOptions;
81
104
  registration = manager.register(hotkeyString, callback, {
82
105
  ...optionsWithoutTarget,
83
106
  target: resolvedTarget
@@ -86,12 +109,8 @@ function createHotkey(hotkey, callback, options = {}) {
86
109
  registration.callback = callback;
87
110
  registration.setOptions(optionsWithoutTarget);
88
111
  }
89
- (0, solid_js.onCleanup)(() => {
90
- if (registration?.isActive) {
91
- registration.unregister();
92
- registration = null;
93
- }
94
- });
112
+ lastHotkeyString = hotkeyString;
113
+ lastTarget = resolvedTarget;
95
114
  });
96
115
  }
97
116
 
@@ -1 +1 @@
1
- {"version":3,"file":"createHotkey.cjs","names":["useDefaultHotkeysOptions"],"sources":["../src/createHotkey.ts"],"sourcesContent":["import { createEffect, onCleanup } from 'solid-js'\nimport {\n detectPlatform,\n formatHotkey,\n getHotkeyManager,\n rawHotkeyToParsedHotkey,\n} from '@tanstack/hotkeys'\nimport { useDefaultHotkeysOptions } from './HotkeysProvider'\nimport type {\n Hotkey,\n HotkeyCallback,\n HotkeyOptions,\n HotkeyRegistrationHandle,\n RegisterableHotkey,\n} from '@tanstack/hotkeys'\n\nexport interface CreateHotkeyOptions extends Omit<HotkeyOptions, 'target'> {\n /**\n * The DOM element to attach the event listener to.\n * Can be a direct DOM element, an accessor (for reactive targets that become\n * available after mount), or null. Defaults to document.\n * When using scoped targets, pass an accessor: () => ({ target: elementSignal() })\n * so the hotkey waits for the element to be attached before registering.\n */\n target?: HTMLElement | Document | Window | null\n}\n\n/**\n * SolidJS primitive for registering a keyboard hotkey.\n *\n * Uses the singleton HotkeyManager for efficient event handling.\n * The callback receives both the keyboard event and a context object\n * containing the hotkey string and parsed hotkey.\n *\n * This primitive automatically tracks reactive dependencies and updates\n * the registration when options or the callback change.\n *\n * @param hotkey - The hotkey string (e.g., 'Mod+S', 'Escape') or RawHotkey object (supports `mod` for cross-platform)\n * @param callback - The function to call when the hotkey is pressed\n * @param options - Options for the hotkey behavior\n *\n * @example\n * ```tsx\n * function SaveButton() {\n * const [count, setCount] = createSignal(0)\n *\n * // Callback always has access to latest count value\n * createHotkey('Mod+S', (event, { hotkey }) => {\n * console.log(`Save triggered, count is ${count()}`)\n * handleSave()\n * })\n *\n * return <button onClick={() => setCount(c => c + 1)}>Count: {count()}</button>\n * }\n * ```\n *\n * @example\n * ```tsx\n * function Modal(props) {\n * // enabled option is reactive\n * createHotkey('Escape', () => {\n * props.onClose()\n * }, () => ({ enabled: props.isOpen }))\n *\n * return <Show when={props.isOpen}>\n * <div class=\"modal\">...</div>\n * </Show>\n * }\n * ```\n *\n * @example\n * ```tsx\n * function Editor() {\n * const [editorRef, setEditorRef] = createSignal<HTMLDivElement | null>(null)\n *\n * // Scoped to a specific element - use accessor so hotkey waits for ref\n * createHotkey('Mod+S', save, () => ({ target: editorRef() }))\n *\n * return <div ref={setEditorRef}>...</div>\n * }\n * ```\n */\nexport function createHotkey(\n hotkey: RegisterableHotkey | (() => RegisterableHotkey),\n callback: HotkeyCallback,\n options: CreateHotkeyOptions | (() => CreateHotkeyOptions) = {},\n): void {\n const defaultOptions = useDefaultHotkeysOptions()\n const manager = getHotkeyManager()\n\n let registration: HotkeyRegistrationHandle | null = null\n\n createEffect(() => {\n // Resolve reactive values\n const resolvedHotkey = typeof hotkey === 'function' ? hotkey() : hotkey\n const resolvedOptions = typeof options === 'function' ? options() : options\n\n const mergedOptions = {\n ...defaultOptions.hotkey,\n ...resolvedOptions,\n } as CreateHotkeyOptions\n\n // Normalize to hotkey string\n const platform = mergedOptions.platform ?? detectPlatform()\n const hotkeyString: Hotkey =\n typeof resolvedHotkey === 'string'\n ? resolvedHotkey\n : (formatHotkey(\n rawHotkeyToParsedHotkey(resolvedHotkey, platform),\n ) as Hotkey)\n\n // Resolve target: when explicitly provided (even as null), use it and skip if null.\n // When not provided, default to document. Matches React's ref handling.\n const resolvedTarget =\n 'target' in mergedOptions\n ? (mergedOptions.target ?? null)\n : typeof document !== 'undefined'\n ? document\n : null\n\n if (!resolvedTarget) {\n return\n }\n\n // Unregister previous registration if it exists\n if (registration?.isActive) {\n registration.unregister()\n registration = null\n }\n\n // Extract options without target (target is handled separately)\n const { target: _target, ...optionsWithoutTarget } = mergedOptions\n\n // Register the hotkey\n registration = manager.register(hotkeyString, callback, {\n ...optionsWithoutTarget,\n target: resolvedTarget,\n })\n\n // Update callback and options on every effect run\n if (registration.isActive) {\n registration.callback = callback\n registration.setOptions(optionsWithoutTarget)\n }\n\n // Cleanup on disposal\n onCleanup(() => {\n if (registration?.isActive) {\n registration.unregister()\n registration = null\n }\n })\n })\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkFA,SAAgB,aACd,QACA,UACA,UAA6D,EAAE,EACzD;CACN,MAAM,iBAAiBA,kDAA0B;CACjD,MAAM,mDAA4B;CAElC,IAAI,eAAgD;AAEpD,kCAAmB;EAEjB,MAAM,iBAAiB,OAAO,WAAW,aAAa,QAAQ,GAAG;EACjE,MAAM,kBAAkB,OAAO,YAAY,aAAa,SAAS,GAAG;EAEpE,MAAM,gBAAgB;GACpB,GAAG,eAAe;GAClB,GAAG;GACJ;EAGD,MAAM,WAAW,cAAc,mDAA4B;EAC3D,MAAM,eACJ,OAAO,mBAAmB,WACtB,oGAE0B,gBAAgB,SAAS,CAClD;EAIP,MAAM,iBACJ,YAAY,gBACP,cAAc,UAAU,OACzB,OAAO,aAAa,cAClB,WACA;AAER,MAAI,CAAC,eACH;AAIF,MAAI,cAAc,UAAU;AAC1B,gBAAa,YAAY;AACzB,kBAAe;;EAIjB,MAAM,EAAE,QAAQ,SAAS,GAAG,yBAAyB;AAGrD,iBAAe,QAAQ,SAAS,cAAc,UAAU;GACtD,GAAG;GACH,QAAQ;GACT,CAAC;AAGF,MAAI,aAAa,UAAU;AACzB,gBAAa,WAAW;AACxB,gBAAa,WAAW,qBAAqB;;AAI/C,gCAAgB;AACd,OAAI,cAAc,UAAU;AAC1B,iBAAa,YAAY;AACzB,mBAAe;;IAEjB;GACF"}
1
+ {"version":3,"file":"createHotkey.cjs","names":["useDefaultHotkeysOptions"],"sources":["../src/createHotkey.ts"],"sourcesContent":["import { createEffect, onCleanup } from 'solid-js'\nimport {\n detectPlatform,\n formatHotkey,\n getHotkeyManager,\n rawHotkeyToParsedHotkey,\n} from '@tanstack/hotkeys'\nimport { useDefaultHotkeysOptions } from './HotkeysProvider'\nimport type {\n Hotkey,\n HotkeyCallback,\n HotkeyOptions,\n HotkeyRegistrationHandle,\n RegisterableHotkey,\n} from '@tanstack/hotkeys'\n\nexport interface CreateHotkeyOptions extends Omit<HotkeyOptions, 'target'> {\n /**\n * The DOM element to attach the event listener to.\n * Can be a direct DOM element, an accessor (for reactive targets that become\n * available after mount), or null. Defaults to document.\n * When using scoped targets, pass an accessor: () => ({ target: elementSignal() })\n * so the hotkey waits for the element to be attached before registering.\n */\n target?: HTMLElement | Document | Window | null\n}\n\n/**\n * SolidJS primitive for registering a keyboard hotkey.\n *\n * Uses the singleton HotkeyManager for efficient event handling.\n * The callback receives both the keyboard event and a context object\n * containing the hotkey string and parsed hotkey.\n *\n * This primitive automatically tracks reactive dependencies and updates\n * the registration when options or the callback change.\n *\n * @param hotkey - The hotkey string (e.g., 'Mod+S', 'Escape') or RawHotkey object (supports `mod` for cross-platform)\n * @param callback - The function to call when the hotkey is pressed\n * @param options - Options for the hotkey behavior\n *\n * @example\n * ```tsx\n * function SaveButton() {\n * const [count, setCount] = createSignal(0)\n *\n * // Callback always has access to latest count value\n * createHotkey('Mod+S', (event, { hotkey }) => {\n * console.log(`Save triggered, count is ${count()}`)\n * handleSave()\n * })\n *\n * return <button onClick={() => setCount(c => c + 1)}>Count: {count()}</button>\n * }\n * ```\n *\n * @example\n * ```tsx\n * function Modal(props) {\n * // enabled option is reactive\n * createHotkey('Escape', () => {\n * props.onClose()\n * }, () => ({ enabled: props.isOpen }))\n *\n * return <Show when={props.isOpen}>\n * <div class=\"modal\">...</div>\n * </Show>\n * }\n * ```\n *\n * @example\n * ```tsx\n * function Editor() {\n * const [editorRef, setEditorRef] = createSignal<HTMLDivElement | null>(null)\n *\n * // Scoped to a specific element - use accessor so hotkey waits for ref\n * createHotkey('Mod+S', save, () => ({ target: editorRef() }))\n *\n * return <div ref={setEditorRef}>...</div>\n * }\n * ```\n */\nexport function createHotkey(\n hotkey: RegisterableHotkey | (() => RegisterableHotkey),\n callback: HotkeyCallback,\n options: CreateHotkeyOptions | (() => CreateHotkeyOptions) = {},\n): void {\n const defaultOptions = useDefaultHotkeysOptions()\n const manager = getHotkeyManager()\n\n let registration: HotkeyRegistrationHandle | null = null\n let lastHotkeyString: Hotkey | null = null\n let lastTarget: HTMLElement | Document | Window | null = null\n\n onCleanup(() => {\n if (registration?.isActive) {\n registration.unregister()\n registration = null\n }\n lastHotkeyString = null\n lastTarget = null\n })\n\n createEffect(() => {\n // Resolve reactive values\n const resolvedHotkey = typeof hotkey === 'function' ? hotkey() : hotkey\n const resolvedOptions = typeof options === 'function' ? options() : options\n\n const mergedOptions = {\n ...defaultOptions.hotkey,\n ...resolvedOptions,\n } as CreateHotkeyOptions\n\n // Normalize to hotkey string\n const platform = mergedOptions.platform ?? detectPlatform()\n const hotkeyString: Hotkey =\n typeof resolvedHotkey === 'string'\n ? resolvedHotkey\n : (formatHotkey(\n rawHotkeyToParsedHotkey(resolvedHotkey, platform),\n ) as Hotkey)\n\n // Resolve target: when explicitly provided (even as null), use it and skip if null.\n // When not provided, default to document. Matches React's ref handling.\n const resolvedTarget =\n 'target' in mergedOptions\n ? (mergedOptions.target ?? null)\n : typeof document !== 'undefined'\n ? document\n : null\n\n if (!resolvedTarget) {\n if (registration?.isActive) {\n registration.unregister()\n registration = null\n }\n lastHotkeyString = null\n lastTarget = null\n return\n }\n\n // Extract options without target (target is handled separately)\n const { target: _target, ...optionsWithoutTarget } = mergedOptions\n\n if (\n registration?.isActive &&\n lastHotkeyString === hotkeyString &&\n lastTarget === resolvedTarget\n ) {\n registration.callback = callback\n registration.setOptions(optionsWithoutTarget)\n return\n }\n\n if (registration?.isActive) {\n registration.unregister()\n registration = null\n }\n\n registration = manager.register(hotkeyString, callback, {\n ...optionsWithoutTarget,\n target: resolvedTarget,\n })\n\n if (registration.isActive) {\n registration.callback = callback\n registration.setOptions(optionsWithoutTarget)\n }\n\n lastHotkeyString = hotkeyString\n lastTarget = resolvedTarget\n })\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkFA,SAAgB,aACd,QACA,UACA,UAA6D,EAAE,EACzD;CACN,MAAM,iBAAiBA,kDAA0B;CACjD,MAAM,mDAA4B;CAElC,IAAI,eAAgD;CACpD,IAAI,mBAAkC;CACtC,IAAI,aAAqD;AAEzD,+BAAgB;AACd,MAAI,cAAc,UAAU;AAC1B,gBAAa,YAAY;AACzB,kBAAe;;AAEjB,qBAAmB;AACnB,eAAa;GACb;AAEF,kCAAmB;EAEjB,MAAM,iBAAiB,OAAO,WAAW,aAAa,QAAQ,GAAG;EACjE,MAAM,kBAAkB,OAAO,YAAY,aAAa,SAAS,GAAG;EAEpE,MAAM,gBAAgB;GACpB,GAAG,eAAe;GAClB,GAAG;GACJ;EAGD,MAAM,WAAW,cAAc,mDAA4B;EAC3D,MAAM,eACJ,OAAO,mBAAmB,WACtB,oGAE0B,gBAAgB,SAAS,CAClD;EAIP,MAAM,iBACJ,YAAY,gBACP,cAAc,UAAU,OACzB,OAAO,aAAa,cAClB,WACA;AAER,MAAI,CAAC,gBAAgB;AACnB,OAAI,cAAc,UAAU;AAC1B,iBAAa,YAAY;AACzB,mBAAe;;AAEjB,sBAAmB;AACnB,gBAAa;AACb;;EAIF,MAAM,EAAE,QAAQ,SAAS,GAAG,yBAAyB;AAErD,MACE,cAAc,YACd,qBAAqB,gBACrB,eAAe,gBACf;AACA,gBAAa,WAAW;AACxB,gBAAa,WAAW,qBAAqB;AAC7C;;AAGF,MAAI,cAAc,UAAU;AAC1B,gBAAa,YAAY;AACzB,kBAAe;;AAGjB,iBAAe,QAAQ,SAAS,cAAc,UAAU;GACtD,GAAG;GACH,QAAQ;GACT,CAAC;AAEF,MAAI,aAAa,UAAU;AACzB,gBAAa,WAAW;AACxB,gBAAa,WAAW,qBAAqB;;AAG/C,qBAAmB;AACnB,eAAa;GACb"}
@@ -62,6 +62,16 @@ function createHotkey(hotkey, callback, options = {}) {
62
62
  const defaultOptions = useDefaultHotkeysOptions();
63
63
  const manager = getHotkeyManager();
64
64
  let registration = null;
65
+ let lastHotkeyString = null;
66
+ let lastTarget = null;
67
+ onCleanup(() => {
68
+ if (registration?.isActive) {
69
+ registration.unregister();
70
+ registration = null;
71
+ }
72
+ lastHotkeyString = null;
73
+ lastTarget = null;
74
+ });
65
75
  createEffect(() => {
66
76
  const resolvedHotkey = typeof hotkey === "function" ? hotkey() : hotkey;
67
77
  const resolvedOptions = typeof options === "function" ? options() : options;
@@ -72,12 +82,25 @@ function createHotkey(hotkey, callback, options = {}) {
72
82
  const platform = mergedOptions.platform ?? detectPlatform();
73
83
  const hotkeyString = typeof resolvedHotkey === "string" ? resolvedHotkey : formatHotkey(rawHotkeyToParsedHotkey(resolvedHotkey, platform));
74
84
  const resolvedTarget = "target" in mergedOptions ? mergedOptions.target ?? null : typeof document !== "undefined" ? document : null;
75
- if (!resolvedTarget) return;
85
+ if (!resolvedTarget) {
86
+ if (registration?.isActive) {
87
+ registration.unregister();
88
+ registration = null;
89
+ }
90
+ lastHotkeyString = null;
91
+ lastTarget = null;
92
+ return;
93
+ }
94
+ const { target: _target, ...optionsWithoutTarget } = mergedOptions;
95
+ if (registration?.isActive && lastHotkeyString === hotkeyString && lastTarget === resolvedTarget) {
96
+ registration.callback = callback;
97
+ registration.setOptions(optionsWithoutTarget);
98
+ return;
99
+ }
76
100
  if (registration?.isActive) {
77
101
  registration.unregister();
78
102
  registration = null;
79
103
  }
80
- const { target: _target, ...optionsWithoutTarget } = mergedOptions;
81
104
  registration = manager.register(hotkeyString, callback, {
82
105
  ...optionsWithoutTarget,
83
106
  target: resolvedTarget
@@ -86,12 +109,8 @@ function createHotkey(hotkey, callback, options = {}) {
86
109
  registration.callback = callback;
87
110
  registration.setOptions(optionsWithoutTarget);
88
111
  }
89
- onCleanup(() => {
90
- if (registration?.isActive) {
91
- registration.unregister();
92
- registration = null;
93
- }
94
- });
112
+ lastHotkeyString = hotkeyString;
113
+ lastTarget = resolvedTarget;
95
114
  });
96
115
  }
97
116
 
@@ -1 +1 @@
1
- {"version":3,"file":"createHotkey.js","names":[],"sources":["../src/createHotkey.ts"],"sourcesContent":["import { createEffect, onCleanup } from 'solid-js'\nimport {\n detectPlatform,\n formatHotkey,\n getHotkeyManager,\n rawHotkeyToParsedHotkey,\n} from '@tanstack/hotkeys'\nimport { useDefaultHotkeysOptions } from './HotkeysProvider'\nimport type {\n Hotkey,\n HotkeyCallback,\n HotkeyOptions,\n HotkeyRegistrationHandle,\n RegisterableHotkey,\n} from '@tanstack/hotkeys'\n\nexport interface CreateHotkeyOptions extends Omit<HotkeyOptions, 'target'> {\n /**\n * The DOM element to attach the event listener to.\n * Can be a direct DOM element, an accessor (for reactive targets that become\n * available after mount), or null. Defaults to document.\n * When using scoped targets, pass an accessor: () => ({ target: elementSignal() })\n * so the hotkey waits for the element to be attached before registering.\n */\n target?: HTMLElement | Document | Window | null\n}\n\n/**\n * SolidJS primitive for registering a keyboard hotkey.\n *\n * Uses the singleton HotkeyManager for efficient event handling.\n * The callback receives both the keyboard event and a context object\n * containing the hotkey string and parsed hotkey.\n *\n * This primitive automatically tracks reactive dependencies and updates\n * the registration when options or the callback change.\n *\n * @param hotkey - The hotkey string (e.g., 'Mod+S', 'Escape') or RawHotkey object (supports `mod` for cross-platform)\n * @param callback - The function to call when the hotkey is pressed\n * @param options - Options for the hotkey behavior\n *\n * @example\n * ```tsx\n * function SaveButton() {\n * const [count, setCount] = createSignal(0)\n *\n * // Callback always has access to latest count value\n * createHotkey('Mod+S', (event, { hotkey }) => {\n * console.log(`Save triggered, count is ${count()}`)\n * handleSave()\n * })\n *\n * return <button onClick={() => setCount(c => c + 1)}>Count: {count()}</button>\n * }\n * ```\n *\n * @example\n * ```tsx\n * function Modal(props) {\n * // enabled option is reactive\n * createHotkey('Escape', () => {\n * props.onClose()\n * }, () => ({ enabled: props.isOpen }))\n *\n * return <Show when={props.isOpen}>\n * <div class=\"modal\">...</div>\n * </Show>\n * }\n * ```\n *\n * @example\n * ```tsx\n * function Editor() {\n * const [editorRef, setEditorRef] = createSignal<HTMLDivElement | null>(null)\n *\n * // Scoped to a specific element - use accessor so hotkey waits for ref\n * createHotkey('Mod+S', save, () => ({ target: editorRef() }))\n *\n * return <div ref={setEditorRef}>...</div>\n * }\n * ```\n */\nexport function createHotkey(\n hotkey: RegisterableHotkey | (() => RegisterableHotkey),\n callback: HotkeyCallback,\n options: CreateHotkeyOptions | (() => CreateHotkeyOptions) = {},\n): void {\n const defaultOptions = useDefaultHotkeysOptions()\n const manager = getHotkeyManager()\n\n let registration: HotkeyRegistrationHandle | null = null\n\n createEffect(() => {\n // Resolve reactive values\n const resolvedHotkey = typeof hotkey === 'function' ? hotkey() : hotkey\n const resolvedOptions = typeof options === 'function' ? options() : options\n\n const mergedOptions = {\n ...defaultOptions.hotkey,\n ...resolvedOptions,\n } as CreateHotkeyOptions\n\n // Normalize to hotkey string\n const platform = mergedOptions.platform ?? detectPlatform()\n const hotkeyString: Hotkey =\n typeof resolvedHotkey === 'string'\n ? resolvedHotkey\n : (formatHotkey(\n rawHotkeyToParsedHotkey(resolvedHotkey, platform),\n ) as Hotkey)\n\n // Resolve target: when explicitly provided (even as null), use it and skip if null.\n // When not provided, default to document. Matches React's ref handling.\n const resolvedTarget =\n 'target' in mergedOptions\n ? (mergedOptions.target ?? null)\n : typeof document !== 'undefined'\n ? document\n : null\n\n if (!resolvedTarget) {\n return\n }\n\n // Unregister previous registration if it exists\n if (registration?.isActive) {\n registration.unregister()\n registration = null\n }\n\n // Extract options without target (target is handled separately)\n const { target: _target, ...optionsWithoutTarget } = mergedOptions\n\n // Register the hotkey\n registration = manager.register(hotkeyString, callback, {\n ...optionsWithoutTarget,\n target: resolvedTarget,\n })\n\n // Update callback and options on every effect run\n if (registration.isActive) {\n registration.callback = callback\n registration.setOptions(optionsWithoutTarget)\n }\n\n // Cleanup on disposal\n onCleanup(() => {\n if (registration?.isActive) {\n registration.unregister()\n registration = null\n }\n })\n })\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkFA,SAAgB,aACd,QACA,UACA,UAA6D,EAAE,EACzD;CACN,MAAM,iBAAiB,0BAA0B;CACjD,MAAM,UAAU,kBAAkB;CAElC,IAAI,eAAgD;AAEpD,oBAAmB;EAEjB,MAAM,iBAAiB,OAAO,WAAW,aAAa,QAAQ,GAAG;EACjE,MAAM,kBAAkB,OAAO,YAAY,aAAa,SAAS,GAAG;EAEpE,MAAM,gBAAgB;GACpB,GAAG,eAAe;GAClB,GAAG;GACJ;EAGD,MAAM,WAAW,cAAc,YAAY,gBAAgB;EAC3D,MAAM,eACJ,OAAO,mBAAmB,WACtB,iBACC,aACC,wBAAwB,gBAAgB,SAAS,CAClD;EAIP,MAAM,iBACJ,YAAY,gBACP,cAAc,UAAU,OACzB,OAAO,aAAa,cAClB,WACA;AAER,MAAI,CAAC,eACH;AAIF,MAAI,cAAc,UAAU;AAC1B,gBAAa,YAAY;AACzB,kBAAe;;EAIjB,MAAM,EAAE,QAAQ,SAAS,GAAG,yBAAyB;AAGrD,iBAAe,QAAQ,SAAS,cAAc,UAAU;GACtD,GAAG;GACH,QAAQ;GACT,CAAC;AAGF,MAAI,aAAa,UAAU;AACzB,gBAAa,WAAW;AACxB,gBAAa,WAAW,qBAAqB;;AAI/C,kBAAgB;AACd,OAAI,cAAc,UAAU;AAC1B,iBAAa,YAAY;AACzB,mBAAe;;IAEjB;GACF"}
1
+ {"version":3,"file":"createHotkey.js","names":[],"sources":["../src/createHotkey.ts"],"sourcesContent":["import { createEffect, onCleanup } from 'solid-js'\nimport {\n detectPlatform,\n formatHotkey,\n getHotkeyManager,\n rawHotkeyToParsedHotkey,\n} from '@tanstack/hotkeys'\nimport { useDefaultHotkeysOptions } from './HotkeysProvider'\nimport type {\n Hotkey,\n HotkeyCallback,\n HotkeyOptions,\n HotkeyRegistrationHandle,\n RegisterableHotkey,\n} from '@tanstack/hotkeys'\n\nexport interface CreateHotkeyOptions extends Omit<HotkeyOptions, 'target'> {\n /**\n * The DOM element to attach the event listener to.\n * Can be a direct DOM element, an accessor (for reactive targets that become\n * available after mount), or null. Defaults to document.\n * When using scoped targets, pass an accessor: () => ({ target: elementSignal() })\n * so the hotkey waits for the element to be attached before registering.\n */\n target?: HTMLElement | Document | Window | null\n}\n\n/**\n * SolidJS primitive for registering a keyboard hotkey.\n *\n * Uses the singleton HotkeyManager for efficient event handling.\n * The callback receives both the keyboard event and a context object\n * containing the hotkey string and parsed hotkey.\n *\n * This primitive automatically tracks reactive dependencies and updates\n * the registration when options or the callback change.\n *\n * @param hotkey - The hotkey string (e.g., 'Mod+S', 'Escape') or RawHotkey object (supports `mod` for cross-platform)\n * @param callback - The function to call when the hotkey is pressed\n * @param options - Options for the hotkey behavior\n *\n * @example\n * ```tsx\n * function SaveButton() {\n * const [count, setCount] = createSignal(0)\n *\n * // Callback always has access to latest count value\n * createHotkey('Mod+S', (event, { hotkey }) => {\n * console.log(`Save triggered, count is ${count()}`)\n * handleSave()\n * })\n *\n * return <button onClick={() => setCount(c => c + 1)}>Count: {count()}</button>\n * }\n * ```\n *\n * @example\n * ```tsx\n * function Modal(props) {\n * // enabled option is reactive\n * createHotkey('Escape', () => {\n * props.onClose()\n * }, () => ({ enabled: props.isOpen }))\n *\n * return <Show when={props.isOpen}>\n * <div class=\"modal\">...</div>\n * </Show>\n * }\n * ```\n *\n * @example\n * ```tsx\n * function Editor() {\n * const [editorRef, setEditorRef] = createSignal<HTMLDivElement | null>(null)\n *\n * // Scoped to a specific element - use accessor so hotkey waits for ref\n * createHotkey('Mod+S', save, () => ({ target: editorRef() }))\n *\n * return <div ref={setEditorRef}>...</div>\n * }\n * ```\n */\nexport function createHotkey(\n hotkey: RegisterableHotkey | (() => RegisterableHotkey),\n callback: HotkeyCallback,\n options: CreateHotkeyOptions | (() => CreateHotkeyOptions) = {},\n): void {\n const defaultOptions = useDefaultHotkeysOptions()\n const manager = getHotkeyManager()\n\n let registration: HotkeyRegistrationHandle | null = null\n let lastHotkeyString: Hotkey | null = null\n let lastTarget: HTMLElement | Document | Window | null = null\n\n onCleanup(() => {\n if (registration?.isActive) {\n registration.unregister()\n registration = null\n }\n lastHotkeyString = null\n lastTarget = null\n })\n\n createEffect(() => {\n // Resolve reactive values\n const resolvedHotkey = typeof hotkey === 'function' ? hotkey() : hotkey\n const resolvedOptions = typeof options === 'function' ? options() : options\n\n const mergedOptions = {\n ...defaultOptions.hotkey,\n ...resolvedOptions,\n } as CreateHotkeyOptions\n\n // Normalize to hotkey string\n const platform = mergedOptions.platform ?? detectPlatform()\n const hotkeyString: Hotkey =\n typeof resolvedHotkey === 'string'\n ? resolvedHotkey\n : (formatHotkey(\n rawHotkeyToParsedHotkey(resolvedHotkey, platform),\n ) as Hotkey)\n\n // Resolve target: when explicitly provided (even as null), use it and skip if null.\n // When not provided, default to document. Matches React's ref handling.\n const resolvedTarget =\n 'target' in mergedOptions\n ? (mergedOptions.target ?? null)\n : typeof document !== 'undefined'\n ? document\n : null\n\n if (!resolvedTarget) {\n if (registration?.isActive) {\n registration.unregister()\n registration = null\n }\n lastHotkeyString = null\n lastTarget = null\n return\n }\n\n // Extract options without target (target is handled separately)\n const { target: _target, ...optionsWithoutTarget } = mergedOptions\n\n if (\n registration?.isActive &&\n lastHotkeyString === hotkeyString &&\n lastTarget === resolvedTarget\n ) {\n registration.callback = callback\n registration.setOptions(optionsWithoutTarget)\n return\n }\n\n if (registration?.isActive) {\n registration.unregister()\n registration = null\n }\n\n registration = manager.register(hotkeyString, callback, {\n ...optionsWithoutTarget,\n target: resolvedTarget,\n })\n\n if (registration.isActive) {\n registration.callback = callback\n registration.setOptions(optionsWithoutTarget)\n }\n\n lastHotkeyString = hotkeyString\n lastTarget = resolvedTarget\n })\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkFA,SAAgB,aACd,QACA,UACA,UAA6D,EAAE,EACzD;CACN,MAAM,iBAAiB,0BAA0B;CACjD,MAAM,UAAU,kBAAkB;CAElC,IAAI,eAAgD;CACpD,IAAI,mBAAkC;CACtC,IAAI,aAAqD;AAEzD,iBAAgB;AACd,MAAI,cAAc,UAAU;AAC1B,gBAAa,YAAY;AACzB,kBAAe;;AAEjB,qBAAmB;AACnB,eAAa;GACb;AAEF,oBAAmB;EAEjB,MAAM,iBAAiB,OAAO,WAAW,aAAa,QAAQ,GAAG;EACjE,MAAM,kBAAkB,OAAO,YAAY,aAAa,SAAS,GAAG;EAEpE,MAAM,gBAAgB;GACpB,GAAG,eAAe;GAClB,GAAG;GACJ;EAGD,MAAM,WAAW,cAAc,YAAY,gBAAgB;EAC3D,MAAM,eACJ,OAAO,mBAAmB,WACtB,iBACC,aACC,wBAAwB,gBAAgB,SAAS,CAClD;EAIP,MAAM,iBACJ,YAAY,gBACP,cAAc,UAAU,OACzB,OAAO,aAAa,cAClB,WACA;AAER,MAAI,CAAC,gBAAgB;AACnB,OAAI,cAAc,UAAU;AAC1B,iBAAa,YAAY;AACzB,mBAAe;;AAEjB,sBAAmB;AACnB,gBAAa;AACb;;EAIF,MAAM,EAAE,QAAQ,SAAS,GAAG,yBAAyB;AAErD,MACE,cAAc,YACd,qBAAqB,gBACrB,eAAe,gBACf;AACA,gBAAa,WAAW;AACxB,gBAAa,WAAW,qBAAqB;AAC7C;;AAGF,MAAI,cAAc,UAAU;AAC1B,gBAAa,YAAY;AACzB,kBAAe;;AAGjB,iBAAe,QAAQ,SAAS,cAAc,UAAU;GACtD,GAAG;GACH,QAAQ;GACT,CAAC;AAEF,MAAI,aAAa,UAAU;AACzB,gBAAa,WAAW;AACxB,gBAAa,WAAW,qBAAqB;;AAG/C,qBAAmB;AACnB,eAAa;GACb"}
@@ -49,6 +49,16 @@ function createHotkeySequence(sequence, callback, options = {}) {
49
49
  const defaultOptions = require_HotkeysProvider.useDefaultHotkeysOptions();
50
50
  const manager = (0, _tanstack_hotkeys.getSequenceManager)();
51
51
  let registration = null;
52
+ let lastSequenceKey = null;
53
+ let lastTarget = null;
54
+ (0, solid_js.onCleanup)(() => {
55
+ if (registration?.isActive) {
56
+ registration.unregister();
57
+ registration = null;
58
+ }
59
+ lastSequenceKey = null;
60
+ lastTarget = null;
61
+ });
52
62
  (0, solid_js.createEffect)(() => {
53
63
  const resolvedSequence = typeof sequence === "function" ? sequence() : sequence;
54
64
  const resolvedOptions = typeof options === "function" ? options() : options;
@@ -57,9 +67,22 @@ function createHotkeySequence(sequence, callback, options = {}) {
57
67
  ...resolvedOptions
58
68
  };
59
69
  const { target: _target, ...optionsWithoutTarget } = mergedOptions;
60
- if (resolvedSequence.length === 0) return;
61
70
  const resolvedTarget = "target" in mergedOptions ? mergedOptions.target ?? null : typeof document !== "undefined" ? document : null;
62
- if (!resolvedTarget) return;
71
+ if (resolvedSequence.length === 0 || !resolvedTarget) {
72
+ if (registration?.isActive) {
73
+ registration.unregister();
74
+ registration = null;
75
+ }
76
+ lastSequenceKey = null;
77
+ lastTarget = null;
78
+ return;
79
+ }
80
+ const sequenceKey = (0, _tanstack_hotkeys.formatHotkeySequence)(resolvedSequence);
81
+ if (registration?.isActive && lastSequenceKey === sequenceKey && lastTarget === resolvedTarget) {
82
+ registration.callback = callback;
83
+ registration.setOptions(optionsWithoutTarget);
84
+ return;
85
+ }
63
86
  if (registration?.isActive) {
64
87
  registration.unregister();
65
88
  registration = null;
@@ -72,12 +95,8 @@ function createHotkeySequence(sequence, callback, options = {}) {
72
95
  registration.callback = callback;
73
96
  registration.setOptions(optionsWithoutTarget);
74
97
  }
75
- (0, solid_js.onCleanup)(() => {
76
- if (registration?.isActive) {
77
- registration.unregister();
78
- registration = null;
79
- }
80
- });
98
+ lastSequenceKey = sequenceKey;
99
+ lastTarget = resolvedTarget;
81
100
  });
82
101
  }
83
102
 
@@ -1 +1 @@
1
- {"version":3,"file":"createHotkeySequence.cjs","names":["useDefaultHotkeysOptions"],"sources":["../src/createHotkeySequence.ts"],"sourcesContent":["import { createEffect, onCleanup } from 'solid-js'\nimport { getSequenceManager } from '@tanstack/hotkeys'\nimport { useDefaultHotkeysOptions } from './HotkeysProvider'\nimport type {\n HotkeyCallback,\n HotkeySequence,\n SequenceOptions,\n SequenceRegistrationHandle,\n} from '@tanstack/hotkeys'\n\nexport interface CreateHotkeySequenceOptions extends Omit<\n SequenceOptions,\n 'target'\n> {\n /**\n * The DOM element to attach the event listener to.\n * Can be a direct DOM element, an accessor, or null. Defaults to document.\n */\n target?: HTMLElement | Document | Window | null\n}\n\n/**\n * SolidJS primitive for registering a keyboard shortcut sequence (Vim-style).\n *\n * This primitive allows you to register multi-key sequences like 'g g' or 'd d'\n * that trigger when the full sequence is pressed within a timeout.\n *\n * Each step may include modifiers. You can chain the same modifier across\n * steps (e.g. `Shift+R` then `Shift+T`). Modifier-only keydown events (Shift,\n * Control, Alt, or Meta pressed alone) are ignored while matching—they do not\n * advance the sequence or reset progress.\n *\n * @param sequence - Array of hotkey strings that form the sequence (or accessor function)\n * @param callback - Function to call when the sequence is completed\n * @param options - Options for the sequence behavior (or accessor function)\n *\n * @example\n * ```tsx\n * function VimEditor() {\n * // 'g g' to go to top\n * createHotkeySequence(['G', 'G'], () => {\n * scrollToTop()\n * })\n *\n * // 'd d' to delete line\n * createHotkeySequence(['D', 'D'], () => {\n * deleteLine()\n * })\n *\n * // 'd i w' to delete inner word\n * createHotkeySequence(['D', 'I', 'W'], () => {\n * deleteInnerWord()\n * }, { timeout: 500 })\n *\n * // Same modifier on consecutive steps (bare Shift between chords is ignored)\n * createHotkeySequence(['Shift+R', 'Shift+T'], () => {\n * nextAction()\n * })\n *\n * return <div>...</div>\n * }\n * ```\n */\nexport function createHotkeySequence(\n sequence: HotkeySequence | (() => HotkeySequence),\n callback: HotkeyCallback,\n options:\n | CreateHotkeySequenceOptions\n | (() => CreateHotkeySequenceOptions) = {},\n): void {\n const defaultOptions = useDefaultHotkeysOptions()\n const manager = getSequenceManager()\n\n let registration: SequenceRegistrationHandle | null = null\n\n createEffect(() => {\n // Resolve reactive values\n const resolvedSequence =\n typeof sequence === 'function' ? sequence() : sequence\n const resolvedOptions = typeof options === 'function' ? options() : options\n\n const mergedOptions = {\n ...defaultOptions.hotkeySequence,\n ...resolvedOptions,\n } as CreateHotkeySequenceOptions\n\n // Extract options without target (target is handled separately)\n const { target: _target, ...optionsWithoutTarget } = mergedOptions\n\n if (resolvedSequence.length === 0) {\n return\n }\n\n // Resolve target: when explicitly provided (even as null), use it and skip if null.\n // When not provided, default to document. Matches createHotkey.\n const resolvedTarget =\n 'target' in mergedOptions\n ? (mergedOptions.target ?? null)\n : typeof document !== 'undefined'\n ? document\n : null\n\n if (!resolvedTarget) {\n return\n }\n\n // Unregister previous registration if it exists\n if (registration?.isActive) {\n registration.unregister()\n registration = null\n }\n\n // Register the sequence\n registration = manager.register(resolvedSequence, callback, {\n ...mergedOptions,\n target: resolvedTarget,\n })\n\n // Sync callback and options on every effect run\n if (registration.isActive) {\n registration.callback = callback\n registration.setOptions(optionsWithoutTarget)\n }\n\n // Cleanup on disposal\n onCleanup(() => {\n if (registration?.isActive) {\n registration.unregister()\n registration = null\n }\n })\n })\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+DA,SAAgB,qBACd,UACA,UACA,UAE0C,EAAE,EACtC;CACN,MAAM,iBAAiBA,kDAA0B;CACjD,MAAM,qDAA8B;CAEpC,IAAI,eAAkD;AAEtD,kCAAmB;EAEjB,MAAM,mBACJ,OAAO,aAAa,aAAa,UAAU,GAAG;EAChD,MAAM,kBAAkB,OAAO,YAAY,aAAa,SAAS,GAAG;EAEpE,MAAM,gBAAgB;GACpB,GAAG,eAAe;GAClB,GAAG;GACJ;EAGD,MAAM,EAAE,QAAQ,SAAS,GAAG,yBAAyB;AAErD,MAAI,iBAAiB,WAAW,EAC9B;EAKF,MAAM,iBACJ,YAAY,gBACP,cAAc,UAAU,OACzB,OAAO,aAAa,cAClB,WACA;AAER,MAAI,CAAC,eACH;AAIF,MAAI,cAAc,UAAU;AAC1B,gBAAa,YAAY;AACzB,kBAAe;;AAIjB,iBAAe,QAAQ,SAAS,kBAAkB,UAAU;GAC1D,GAAG;GACH,QAAQ;GACT,CAAC;AAGF,MAAI,aAAa,UAAU;AACzB,gBAAa,WAAW;AACxB,gBAAa,WAAW,qBAAqB;;AAI/C,gCAAgB;AACd,OAAI,cAAc,UAAU;AAC1B,iBAAa,YAAY;AACzB,mBAAe;;IAEjB;GACF"}
1
+ {"version":3,"file":"createHotkeySequence.cjs","names":["useDefaultHotkeysOptions"],"sources":["../src/createHotkeySequence.ts"],"sourcesContent":["import { createEffect, onCleanup } from 'solid-js'\nimport { formatHotkeySequence, getSequenceManager } from '@tanstack/hotkeys'\nimport { useDefaultHotkeysOptions } from './HotkeysProvider'\nimport type {\n HotkeyCallback,\n HotkeySequence,\n SequenceOptions,\n SequenceRegistrationHandle,\n} from '@tanstack/hotkeys'\n\nexport interface CreateHotkeySequenceOptions extends Omit<\n SequenceOptions,\n 'target'\n> {\n /**\n * The DOM element to attach the event listener to.\n * Can be a direct DOM element, an accessor, or null. Defaults to document.\n */\n target?: HTMLElement | Document | Window | null\n}\n\n/**\n * SolidJS primitive for registering a keyboard shortcut sequence (Vim-style).\n *\n * This primitive allows you to register multi-key sequences like 'g g' or 'd d'\n * that trigger when the full sequence is pressed within a timeout.\n *\n * Each step may include modifiers. You can chain the same modifier across\n * steps (e.g. `Shift+R` then `Shift+T`). Modifier-only keydown events (Shift,\n * Control, Alt, or Meta pressed alone) are ignored while matching—they do not\n * advance the sequence or reset progress.\n *\n * @param sequence - Array of hotkey strings that form the sequence (or accessor function)\n * @param callback - Function to call when the sequence is completed\n * @param options - Options for the sequence behavior (or accessor function)\n *\n * @example\n * ```tsx\n * function VimEditor() {\n * // 'g g' to go to top\n * createHotkeySequence(['G', 'G'], () => {\n * scrollToTop()\n * })\n *\n * // 'd d' to delete line\n * createHotkeySequence(['D', 'D'], () => {\n * deleteLine()\n * })\n *\n * // 'd i w' to delete inner word\n * createHotkeySequence(['D', 'I', 'W'], () => {\n * deleteInnerWord()\n * }, { timeout: 500 })\n *\n * // Same modifier on consecutive steps (bare Shift between chords is ignored)\n * createHotkeySequence(['Shift+R', 'Shift+T'], () => {\n * nextAction()\n * })\n *\n * return <div>...</div>\n * }\n * ```\n */\nexport function createHotkeySequence(\n sequence: HotkeySequence | (() => HotkeySequence),\n callback: HotkeyCallback,\n options:\n | CreateHotkeySequenceOptions\n | (() => CreateHotkeySequenceOptions) = {},\n): void {\n const defaultOptions = useDefaultHotkeysOptions()\n const manager = getSequenceManager()\n\n let registration: SequenceRegistrationHandle | null = null\n let lastSequenceKey: string | null = null\n let lastTarget: HTMLElement | Document | Window | null = null\n\n onCleanup(() => {\n if (registration?.isActive) {\n registration.unregister()\n registration = null\n }\n lastSequenceKey = null\n lastTarget = null\n })\n\n createEffect(() => {\n // Resolve reactive values\n const resolvedSequence =\n typeof sequence === 'function' ? sequence() : sequence\n const resolvedOptions = typeof options === 'function' ? options() : options\n\n const mergedOptions = {\n ...defaultOptions.hotkeySequence,\n ...resolvedOptions,\n } as CreateHotkeySequenceOptions\n\n // Extract options without target (target is handled separately)\n const { target: _target, ...optionsWithoutTarget } = mergedOptions\n\n // Resolve target: when explicitly provided (even as null), use it and skip if null.\n // When not provided, default to document. Matches createHotkey.\n const resolvedTarget =\n 'target' in mergedOptions\n ? (mergedOptions.target ?? null)\n : typeof document !== 'undefined'\n ? document\n : null\n\n if (resolvedSequence.length === 0 || !resolvedTarget) {\n if (registration?.isActive) {\n registration.unregister()\n registration = null\n }\n lastSequenceKey = null\n lastTarget = null\n return\n }\n\n const sequenceKey = formatHotkeySequence(resolvedSequence)\n\n if (\n registration?.isActive &&\n lastSequenceKey === sequenceKey &&\n lastTarget === resolvedTarget\n ) {\n registration.callback = callback\n registration.setOptions(optionsWithoutTarget)\n return\n }\n\n if (registration?.isActive) {\n registration.unregister()\n registration = null\n }\n\n registration = manager.register(resolvedSequence, callback, {\n ...mergedOptions,\n target: resolvedTarget,\n })\n\n if (registration.isActive) {\n registration.callback = callback\n registration.setOptions(optionsWithoutTarget)\n }\n\n lastSequenceKey = sequenceKey\n lastTarget = resolvedTarget\n })\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+DA,SAAgB,qBACd,UACA,UACA,UAE0C,EAAE,EACtC;CACN,MAAM,iBAAiBA,kDAA0B;CACjD,MAAM,qDAA8B;CAEpC,IAAI,eAAkD;CACtD,IAAI,kBAAiC;CACrC,IAAI,aAAqD;AAEzD,+BAAgB;AACd,MAAI,cAAc,UAAU;AAC1B,gBAAa,YAAY;AACzB,kBAAe;;AAEjB,oBAAkB;AAClB,eAAa;GACb;AAEF,kCAAmB;EAEjB,MAAM,mBACJ,OAAO,aAAa,aAAa,UAAU,GAAG;EAChD,MAAM,kBAAkB,OAAO,YAAY,aAAa,SAAS,GAAG;EAEpE,MAAM,gBAAgB;GACpB,GAAG,eAAe;GAClB,GAAG;GACJ;EAGD,MAAM,EAAE,QAAQ,SAAS,GAAG,yBAAyB;EAIrD,MAAM,iBACJ,YAAY,gBACP,cAAc,UAAU,OACzB,OAAO,aAAa,cAClB,WACA;AAER,MAAI,iBAAiB,WAAW,KAAK,CAAC,gBAAgB;AACpD,OAAI,cAAc,UAAU;AAC1B,iBAAa,YAAY;AACzB,mBAAe;;AAEjB,qBAAkB;AAClB,gBAAa;AACb;;EAGF,MAAM,0DAAmC,iBAAiB;AAE1D,MACE,cAAc,YACd,oBAAoB,eACpB,eAAe,gBACf;AACA,gBAAa,WAAW;AACxB,gBAAa,WAAW,qBAAqB;AAC7C;;AAGF,MAAI,cAAc,UAAU;AAC1B,gBAAa,YAAY;AACzB,kBAAe;;AAGjB,iBAAe,QAAQ,SAAS,kBAAkB,UAAU;GAC1D,GAAG;GACH,QAAQ;GACT,CAAC;AAEF,MAAI,aAAa,UAAU;AACzB,gBAAa,WAAW;AACxB,gBAAa,WAAW,qBAAqB;;AAG/C,oBAAkB;AAClB,eAAa;GACb"}
@@ -1,5 +1,5 @@
1
1
  import { useDefaultHotkeysOptions } from "./HotkeysProvider.js";
2
- import { getSequenceManager } from "@tanstack/hotkeys";
2
+ import { formatHotkeySequence, getSequenceManager } from "@tanstack/hotkeys";
3
3
  import { createEffect, onCleanup } from "solid-js";
4
4
 
5
5
  //#region src/createHotkeySequence.ts
@@ -49,6 +49,16 @@ function createHotkeySequence(sequence, callback, options = {}) {
49
49
  const defaultOptions = useDefaultHotkeysOptions();
50
50
  const manager = getSequenceManager();
51
51
  let registration = null;
52
+ let lastSequenceKey = null;
53
+ let lastTarget = null;
54
+ onCleanup(() => {
55
+ if (registration?.isActive) {
56
+ registration.unregister();
57
+ registration = null;
58
+ }
59
+ lastSequenceKey = null;
60
+ lastTarget = null;
61
+ });
52
62
  createEffect(() => {
53
63
  const resolvedSequence = typeof sequence === "function" ? sequence() : sequence;
54
64
  const resolvedOptions = typeof options === "function" ? options() : options;
@@ -57,9 +67,22 @@ function createHotkeySequence(sequence, callback, options = {}) {
57
67
  ...resolvedOptions
58
68
  };
59
69
  const { target: _target, ...optionsWithoutTarget } = mergedOptions;
60
- if (resolvedSequence.length === 0) return;
61
70
  const resolvedTarget = "target" in mergedOptions ? mergedOptions.target ?? null : typeof document !== "undefined" ? document : null;
62
- if (!resolvedTarget) return;
71
+ if (resolvedSequence.length === 0 || !resolvedTarget) {
72
+ if (registration?.isActive) {
73
+ registration.unregister();
74
+ registration = null;
75
+ }
76
+ lastSequenceKey = null;
77
+ lastTarget = null;
78
+ return;
79
+ }
80
+ const sequenceKey = formatHotkeySequence(resolvedSequence);
81
+ if (registration?.isActive && lastSequenceKey === sequenceKey && lastTarget === resolvedTarget) {
82
+ registration.callback = callback;
83
+ registration.setOptions(optionsWithoutTarget);
84
+ return;
85
+ }
63
86
  if (registration?.isActive) {
64
87
  registration.unregister();
65
88
  registration = null;
@@ -72,12 +95,8 @@ function createHotkeySequence(sequence, callback, options = {}) {
72
95
  registration.callback = callback;
73
96
  registration.setOptions(optionsWithoutTarget);
74
97
  }
75
- onCleanup(() => {
76
- if (registration?.isActive) {
77
- registration.unregister();
78
- registration = null;
79
- }
80
- });
98
+ lastSequenceKey = sequenceKey;
99
+ lastTarget = resolvedTarget;
81
100
  });
82
101
  }
83
102
 
@@ -1 +1 @@
1
- {"version":3,"file":"createHotkeySequence.js","names":[],"sources":["../src/createHotkeySequence.ts"],"sourcesContent":["import { createEffect, onCleanup } from 'solid-js'\nimport { getSequenceManager } from '@tanstack/hotkeys'\nimport { useDefaultHotkeysOptions } from './HotkeysProvider'\nimport type {\n HotkeyCallback,\n HotkeySequence,\n SequenceOptions,\n SequenceRegistrationHandle,\n} from '@tanstack/hotkeys'\n\nexport interface CreateHotkeySequenceOptions extends Omit<\n SequenceOptions,\n 'target'\n> {\n /**\n * The DOM element to attach the event listener to.\n * Can be a direct DOM element, an accessor, or null. Defaults to document.\n */\n target?: HTMLElement | Document | Window | null\n}\n\n/**\n * SolidJS primitive for registering a keyboard shortcut sequence (Vim-style).\n *\n * This primitive allows you to register multi-key sequences like 'g g' or 'd d'\n * that trigger when the full sequence is pressed within a timeout.\n *\n * Each step may include modifiers. You can chain the same modifier across\n * steps (e.g. `Shift+R` then `Shift+T`). Modifier-only keydown events (Shift,\n * Control, Alt, or Meta pressed alone) are ignored while matching—they do not\n * advance the sequence or reset progress.\n *\n * @param sequence - Array of hotkey strings that form the sequence (or accessor function)\n * @param callback - Function to call when the sequence is completed\n * @param options - Options for the sequence behavior (or accessor function)\n *\n * @example\n * ```tsx\n * function VimEditor() {\n * // 'g g' to go to top\n * createHotkeySequence(['G', 'G'], () => {\n * scrollToTop()\n * })\n *\n * // 'd d' to delete line\n * createHotkeySequence(['D', 'D'], () => {\n * deleteLine()\n * })\n *\n * // 'd i w' to delete inner word\n * createHotkeySequence(['D', 'I', 'W'], () => {\n * deleteInnerWord()\n * }, { timeout: 500 })\n *\n * // Same modifier on consecutive steps (bare Shift between chords is ignored)\n * createHotkeySequence(['Shift+R', 'Shift+T'], () => {\n * nextAction()\n * })\n *\n * return <div>...</div>\n * }\n * ```\n */\nexport function createHotkeySequence(\n sequence: HotkeySequence | (() => HotkeySequence),\n callback: HotkeyCallback,\n options:\n | CreateHotkeySequenceOptions\n | (() => CreateHotkeySequenceOptions) = {},\n): void {\n const defaultOptions = useDefaultHotkeysOptions()\n const manager = getSequenceManager()\n\n let registration: SequenceRegistrationHandle | null = null\n\n createEffect(() => {\n // Resolve reactive values\n const resolvedSequence =\n typeof sequence === 'function' ? sequence() : sequence\n const resolvedOptions = typeof options === 'function' ? options() : options\n\n const mergedOptions = {\n ...defaultOptions.hotkeySequence,\n ...resolvedOptions,\n } as CreateHotkeySequenceOptions\n\n // Extract options without target (target is handled separately)\n const { target: _target, ...optionsWithoutTarget } = mergedOptions\n\n if (resolvedSequence.length === 0) {\n return\n }\n\n // Resolve target: when explicitly provided (even as null), use it and skip if null.\n // When not provided, default to document. Matches createHotkey.\n const resolvedTarget =\n 'target' in mergedOptions\n ? (mergedOptions.target ?? null)\n : typeof document !== 'undefined'\n ? document\n : null\n\n if (!resolvedTarget) {\n return\n }\n\n // Unregister previous registration if it exists\n if (registration?.isActive) {\n registration.unregister()\n registration = null\n }\n\n // Register the sequence\n registration = manager.register(resolvedSequence, callback, {\n ...mergedOptions,\n target: resolvedTarget,\n })\n\n // Sync callback and options on every effect run\n if (registration.isActive) {\n registration.callback = callback\n registration.setOptions(optionsWithoutTarget)\n }\n\n // Cleanup on disposal\n onCleanup(() => {\n if (registration?.isActive) {\n registration.unregister()\n registration = null\n }\n })\n })\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+DA,SAAgB,qBACd,UACA,UACA,UAE0C,EAAE,EACtC;CACN,MAAM,iBAAiB,0BAA0B;CACjD,MAAM,UAAU,oBAAoB;CAEpC,IAAI,eAAkD;AAEtD,oBAAmB;EAEjB,MAAM,mBACJ,OAAO,aAAa,aAAa,UAAU,GAAG;EAChD,MAAM,kBAAkB,OAAO,YAAY,aAAa,SAAS,GAAG;EAEpE,MAAM,gBAAgB;GACpB,GAAG,eAAe;GAClB,GAAG;GACJ;EAGD,MAAM,EAAE,QAAQ,SAAS,GAAG,yBAAyB;AAErD,MAAI,iBAAiB,WAAW,EAC9B;EAKF,MAAM,iBACJ,YAAY,gBACP,cAAc,UAAU,OACzB,OAAO,aAAa,cAClB,WACA;AAER,MAAI,CAAC,eACH;AAIF,MAAI,cAAc,UAAU;AAC1B,gBAAa,YAAY;AACzB,kBAAe;;AAIjB,iBAAe,QAAQ,SAAS,kBAAkB,UAAU;GAC1D,GAAG;GACH,QAAQ;GACT,CAAC;AAGF,MAAI,aAAa,UAAU;AACzB,gBAAa,WAAW;AACxB,gBAAa,WAAW,qBAAqB;;AAI/C,kBAAgB;AACd,OAAI,cAAc,UAAU;AAC1B,iBAAa,YAAY;AACzB,mBAAe;;IAEjB;GACF"}
1
+ {"version":3,"file":"createHotkeySequence.js","names":[],"sources":["../src/createHotkeySequence.ts"],"sourcesContent":["import { createEffect, onCleanup } from 'solid-js'\nimport { formatHotkeySequence, getSequenceManager } from '@tanstack/hotkeys'\nimport { useDefaultHotkeysOptions } from './HotkeysProvider'\nimport type {\n HotkeyCallback,\n HotkeySequence,\n SequenceOptions,\n SequenceRegistrationHandle,\n} from '@tanstack/hotkeys'\n\nexport interface CreateHotkeySequenceOptions extends Omit<\n SequenceOptions,\n 'target'\n> {\n /**\n * The DOM element to attach the event listener to.\n * Can be a direct DOM element, an accessor, or null. Defaults to document.\n */\n target?: HTMLElement | Document | Window | null\n}\n\n/**\n * SolidJS primitive for registering a keyboard shortcut sequence (Vim-style).\n *\n * This primitive allows you to register multi-key sequences like 'g g' or 'd d'\n * that trigger when the full sequence is pressed within a timeout.\n *\n * Each step may include modifiers. You can chain the same modifier across\n * steps (e.g. `Shift+R` then `Shift+T`). Modifier-only keydown events (Shift,\n * Control, Alt, or Meta pressed alone) are ignored while matching—they do not\n * advance the sequence or reset progress.\n *\n * @param sequence - Array of hotkey strings that form the sequence (or accessor function)\n * @param callback - Function to call when the sequence is completed\n * @param options - Options for the sequence behavior (or accessor function)\n *\n * @example\n * ```tsx\n * function VimEditor() {\n * // 'g g' to go to top\n * createHotkeySequence(['G', 'G'], () => {\n * scrollToTop()\n * })\n *\n * // 'd d' to delete line\n * createHotkeySequence(['D', 'D'], () => {\n * deleteLine()\n * })\n *\n * // 'd i w' to delete inner word\n * createHotkeySequence(['D', 'I', 'W'], () => {\n * deleteInnerWord()\n * }, { timeout: 500 })\n *\n * // Same modifier on consecutive steps (bare Shift between chords is ignored)\n * createHotkeySequence(['Shift+R', 'Shift+T'], () => {\n * nextAction()\n * })\n *\n * return <div>...</div>\n * }\n * ```\n */\nexport function createHotkeySequence(\n sequence: HotkeySequence | (() => HotkeySequence),\n callback: HotkeyCallback,\n options:\n | CreateHotkeySequenceOptions\n | (() => CreateHotkeySequenceOptions) = {},\n): void {\n const defaultOptions = useDefaultHotkeysOptions()\n const manager = getSequenceManager()\n\n let registration: SequenceRegistrationHandle | null = null\n let lastSequenceKey: string | null = null\n let lastTarget: HTMLElement | Document | Window | null = null\n\n onCleanup(() => {\n if (registration?.isActive) {\n registration.unregister()\n registration = null\n }\n lastSequenceKey = null\n lastTarget = null\n })\n\n createEffect(() => {\n // Resolve reactive values\n const resolvedSequence =\n typeof sequence === 'function' ? sequence() : sequence\n const resolvedOptions = typeof options === 'function' ? options() : options\n\n const mergedOptions = {\n ...defaultOptions.hotkeySequence,\n ...resolvedOptions,\n } as CreateHotkeySequenceOptions\n\n // Extract options without target (target is handled separately)\n const { target: _target, ...optionsWithoutTarget } = mergedOptions\n\n // Resolve target: when explicitly provided (even as null), use it and skip if null.\n // When not provided, default to document. Matches createHotkey.\n const resolvedTarget =\n 'target' in mergedOptions\n ? (mergedOptions.target ?? null)\n : typeof document !== 'undefined'\n ? document\n : null\n\n if (resolvedSequence.length === 0 || !resolvedTarget) {\n if (registration?.isActive) {\n registration.unregister()\n registration = null\n }\n lastSequenceKey = null\n lastTarget = null\n return\n }\n\n const sequenceKey = formatHotkeySequence(resolvedSequence)\n\n if (\n registration?.isActive &&\n lastSequenceKey === sequenceKey &&\n lastTarget === resolvedTarget\n ) {\n registration.callback = callback\n registration.setOptions(optionsWithoutTarget)\n return\n }\n\n if (registration?.isActive) {\n registration.unregister()\n registration = null\n }\n\n registration = manager.register(resolvedSequence, callback, {\n ...mergedOptions,\n target: resolvedTarget,\n })\n\n if (registration.isActive) {\n registration.callback = callback\n registration.setOptions(optionsWithoutTarget)\n }\n\n lastSequenceKey = sequenceKey\n lastTarget = resolvedTarget\n })\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+DA,SAAgB,qBACd,UACA,UACA,UAE0C,EAAE,EACtC;CACN,MAAM,iBAAiB,0BAA0B;CACjD,MAAM,UAAU,oBAAoB;CAEpC,IAAI,eAAkD;CACtD,IAAI,kBAAiC;CACrC,IAAI,aAAqD;AAEzD,iBAAgB;AACd,MAAI,cAAc,UAAU;AAC1B,gBAAa,YAAY;AACzB,kBAAe;;AAEjB,oBAAkB;AAClB,eAAa;GACb;AAEF,oBAAmB;EAEjB,MAAM,mBACJ,OAAO,aAAa,aAAa,UAAU,GAAG;EAChD,MAAM,kBAAkB,OAAO,YAAY,aAAa,SAAS,GAAG;EAEpE,MAAM,gBAAgB;GACpB,GAAG,eAAe;GAClB,GAAG;GACJ;EAGD,MAAM,EAAE,QAAQ,SAAS,GAAG,yBAAyB;EAIrD,MAAM,iBACJ,YAAY,gBACP,cAAc,UAAU,OACzB,OAAO,aAAa,cAClB,WACA;AAER,MAAI,iBAAiB,WAAW,KAAK,CAAC,gBAAgB;AACpD,OAAI,cAAc,UAAU;AAC1B,iBAAa,YAAY;AACzB,mBAAe;;AAEjB,qBAAkB;AAClB,gBAAa;AACb;;EAGF,MAAM,cAAc,qBAAqB,iBAAiB;AAE1D,MACE,cAAc,YACd,oBAAoB,eACpB,eAAe,gBACf;AACA,gBAAa,WAAW;AACxB,gBAAa,WAAW,qBAAqB;AAC7C;;AAGF,MAAI,cAAc,UAAU;AAC1B,gBAAa,YAAY;AACzB,kBAAe;;AAGjB,iBAAe,QAAQ,SAAS,kBAAkB,UAAU;GAC1D,GAAG;GACH,QAAQ;GACT,CAAC;AAEF,MAAI,aAAa,UAAU;AACzB,gBAAa,WAAW;AACxB,gBAAa,WAAW,qBAAqB;;AAG/C,oBAAkB;AAClB,eAAa;GACb"}
@@ -0,0 +1,110 @@
1
+ const require_HotkeysProvider = require('./HotkeysProvider.cjs');
2
+ let _tanstack_hotkeys = require("@tanstack/hotkeys");
3
+ let solid_js = require("solid-js");
4
+
5
+ //#region src/createHotkeySequences.ts
6
+ /**
7
+ * SolidJS primitive for registering multiple keyboard shortcut sequences at once (Vim-style).
8
+ *
9
+ * Uses the singleton SequenceManager. Accepts a dynamic array of definitions, or accessors,
10
+ * so you can react to variable-length lists.
11
+ *
12
+ * Options are merged in this order:
13
+ * HotkeysProvider defaults < commonOptions < per-definition options
14
+ *
15
+ * Definitions with an empty `sequence` are skipped (no registration).
16
+ *
17
+ * @param sequences - Array of sequence definitions, or accessor returning them
18
+ * @param commonOptions - Shared options for all sequences, or accessor
19
+ *
20
+ * @example
21
+ * ```tsx
22
+ * function VimPalette() {
23
+ * createHotkeySequences([
24
+ * { sequence: ['G', 'G'], callback: () => scrollToTop() },
25
+ * { sequence: ['D', 'D'], callback: () => deleteLine() },
26
+ * ])
27
+ * }
28
+ * ```
29
+ *
30
+ * @example
31
+ * ```tsx
32
+ * function Dynamic(props) {
33
+ * createHotkeySequences(
34
+ * () => props.items.map((item) => ({
35
+ * sequence: item.chords,
36
+ * callback: item.action,
37
+ * options: { enabled: item.enabled },
38
+ * })),
39
+ * { preventDefault: true },
40
+ * )
41
+ * }
42
+ * ```
43
+ */
44
+ function createHotkeySequences(sequences, commonOptions = {}) {
45
+ const defaultOptions = require_HotkeysProvider.useDefaultHotkeysOptions();
46
+ const manager = (0, _tanstack_hotkeys.getSequenceManager)();
47
+ const registrations = /* @__PURE__ */ new Map();
48
+ (0, solid_js.onCleanup)(() => {
49
+ for (const { handle } of registrations.values()) if (handle.isActive) handle.unregister();
50
+ registrations.clear();
51
+ });
52
+ (0, solid_js.createEffect)(() => {
53
+ const resolved = typeof sequences === "function" ? sequences() : sequences;
54
+ const resolvedCommonOptions = typeof commonOptions === "function" ? commonOptions() : commonOptions;
55
+ const nextKeys = /* @__PURE__ */ new Set();
56
+ const prepared = [];
57
+ for (let i = 0; i < resolved.length; i++) {
58
+ const def = resolved[i];
59
+ const resolvedDefOptions = def.options ?? {};
60
+ const mergedOptions = {
61
+ ...defaultOptions.hotkeySequence,
62
+ ...resolvedCommonOptions,
63
+ ...resolvedDefOptions
64
+ };
65
+ if (def.sequence.length === 0) continue;
66
+ const sequenceString = (0, _tanstack_hotkeys.formatHotkeySequence)(def.sequence);
67
+ const resolvedTarget = "target" in mergedOptions ? mergedOptions.target ?? null : typeof document !== "undefined" ? document : null;
68
+ if (!resolvedTarget) continue;
69
+ const registrationKey = `${i}:${sequenceString}`;
70
+ nextKeys.add(registrationKey);
71
+ prepared.push({
72
+ registrationKey,
73
+ def,
74
+ mergedOptions,
75
+ sequenceString,
76
+ resolvedTarget
77
+ });
78
+ }
79
+ for (const [key, record] of [...registrations.entries()]) if (!nextKeys.has(key)) {
80
+ if (record.handle.isActive) record.handle.unregister();
81
+ registrations.delete(key);
82
+ }
83
+ for (const p of prepared) {
84
+ const existing = registrations.get(p.registrationKey);
85
+ if (existing?.handle.isActive && existing.target === p.resolvedTarget) {
86
+ existing.handle.callback = p.def.callback;
87
+ const { target: _target, ...optionsWithoutTarget } = p.mergedOptions;
88
+ existing.handle.setOptions(optionsWithoutTarget);
89
+ continue;
90
+ }
91
+ if (existing) {
92
+ if (existing.handle.isActive) existing.handle.unregister();
93
+ registrations.delete(p.registrationKey);
94
+ }
95
+ const { target: _target, ...optionsWithoutTarget } = p.mergedOptions;
96
+ const handle = manager.register(p.def.sequence, p.def.callback, {
97
+ ...optionsWithoutTarget,
98
+ target: p.resolvedTarget
99
+ });
100
+ registrations.set(p.registrationKey, {
101
+ handle,
102
+ target: p.resolvedTarget
103
+ });
104
+ }
105
+ });
106
+ }
107
+
108
+ //#endregion
109
+ exports.createHotkeySequences = createHotkeySequences;
110
+ //# sourceMappingURL=createHotkeySequences.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createHotkeySequences.cjs","names":["useDefaultHotkeysOptions"],"sources":["../src/createHotkeySequences.ts"],"sourcesContent":["import { createEffect, onCleanup } from 'solid-js'\nimport { formatHotkeySequence, getSequenceManager } from '@tanstack/hotkeys'\nimport { useDefaultHotkeysOptions } from './HotkeysProvider'\nimport type { CreateHotkeySequenceOptions } from './createHotkeySequence'\nimport type {\n HotkeyCallback,\n HotkeySequence,\n SequenceRegistrationHandle,\n} from '@tanstack/hotkeys'\n\n/**\n * A single sequence definition for use with `createHotkeySequences`.\n */\nexport interface CreateHotkeySequenceDefinition {\n /** Array of hotkey strings that form the sequence */\n sequence: HotkeySequence\n /** The function to call when the sequence is completed */\n callback: HotkeyCallback\n /** Per-sequence options (merged on top of commonOptions) */\n options?: CreateHotkeySequenceOptions\n}\n\n/**\n * SolidJS primitive for registering multiple keyboard shortcut sequences at once (Vim-style).\n *\n * Uses the singleton SequenceManager. Accepts a dynamic array of definitions, or accessors,\n * so you can react to variable-length lists.\n *\n * Options are merged in this order:\n * HotkeysProvider defaults < commonOptions < per-definition options\n *\n * Definitions with an empty `sequence` are skipped (no registration).\n *\n * @param sequences - Array of sequence definitions, or accessor returning them\n * @param commonOptions - Shared options for all sequences, or accessor\n *\n * @example\n * ```tsx\n * function VimPalette() {\n * createHotkeySequences([\n * { sequence: ['G', 'G'], callback: () => scrollToTop() },\n * { sequence: ['D', 'D'], callback: () => deleteLine() },\n * ])\n * }\n * ```\n *\n * @example\n * ```tsx\n * function Dynamic(props) {\n * createHotkeySequences(\n * () => props.items.map((item) => ({\n * sequence: item.chords,\n * callback: item.action,\n * options: { enabled: item.enabled },\n * })),\n * { preventDefault: true },\n * )\n * }\n * ```\n */\nexport function createHotkeySequences(\n sequences:\n | Array<CreateHotkeySequenceDefinition>\n | (() => Array<CreateHotkeySequenceDefinition>),\n commonOptions:\n | CreateHotkeySequenceOptions\n | (() => CreateHotkeySequenceOptions) = {},\n): void {\n type RegistrationRecord = {\n handle: SequenceRegistrationHandle\n target: Document | HTMLElement | Window\n }\n\n const defaultOptions = useDefaultHotkeysOptions()\n const manager = getSequenceManager()\n\n const registrations = new Map<string, RegistrationRecord>()\n\n onCleanup(() => {\n for (const { handle } of registrations.values()) {\n if (handle.isActive) {\n handle.unregister()\n }\n }\n registrations.clear()\n })\n\n createEffect(() => {\n const resolved = typeof sequences === 'function' ? sequences() : sequences\n const resolvedCommonOptions =\n typeof commonOptions === 'function' ? commonOptions() : commonOptions\n\n const nextKeys = new Set<string>()\n const prepared: Array<{\n registrationKey: string\n def: CreateHotkeySequenceDefinition\n mergedOptions: CreateHotkeySequenceOptions\n sequenceString: string\n resolvedTarget: Document | HTMLElement | Window\n }> = []\n\n for (let i = 0; i < resolved.length; i++) {\n const def = resolved[i]!\n const resolvedDefOptions = def.options ?? {}\n\n const mergedOptions = {\n ...defaultOptions.hotkeySequence,\n ...resolvedCommonOptions,\n ...resolvedDefOptions,\n } as CreateHotkeySequenceOptions\n\n if (def.sequence.length === 0) {\n continue\n }\n\n const sequenceString = formatHotkeySequence(def.sequence)\n\n const resolvedTarget =\n 'target' in mergedOptions\n ? (mergedOptions.target ?? null)\n : typeof document !== 'undefined'\n ? document\n : null\n\n if (!resolvedTarget) {\n continue\n }\n\n const registrationKey = `${i}:${sequenceString}`\n nextKeys.add(registrationKey)\n prepared.push({\n registrationKey,\n def,\n mergedOptions,\n sequenceString,\n resolvedTarget,\n })\n }\n\n for (const [key, record] of [...registrations.entries()]) {\n if (!nextKeys.has(key)) {\n if (record.handle.isActive) {\n record.handle.unregister()\n }\n registrations.delete(key)\n }\n }\n\n for (const p of prepared) {\n const existing = registrations.get(p.registrationKey)\n if (existing?.handle.isActive && existing.target === p.resolvedTarget) {\n existing.handle.callback = p.def.callback\n const { target: _target, ...optionsWithoutTarget } = p.mergedOptions\n existing.handle.setOptions(optionsWithoutTarget)\n continue\n }\n\n if (existing) {\n if (existing.handle.isActive) {\n existing.handle.unregister()\n }\n registrations.delete(p.registrationKey)\n }\n\n const { target: _target, ...optionsWithoutTarget } = p.mergedOptions\n const handle = manager.register(p.def.sequence, p.def.callback, {\n ...optionsWithoutTarget,\n target: p.resolvedTarget,\n })\n registrations.set(p.registrationKey, {\n handle,\n target: p.resolvedTarget,\n })\n }\n })\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4DA,SAAgB,sBACd,WAGA,gBAE0C,EAAE,EACtC;CAMN,MAAM,iBAAiBA,kDAA0B;CACjD,MAAM,qDAA8B;CAEpC,MAAM,gCAAgB,IAAI,KAAiC;AAE3D,+BAAgB;AACd,OAAK,MAAM,EAAE,YAAY,cAAc,QAAQ,CAC7C,KAAI,OAAO,SACT,QAAO,YAAY;AAGvB,gBAAc,OAAO;GACrB;AAEF,kCAAmB;EACjB,MAAM,WAAW,OAAO,cAAc,aAAa,WAAW,GAAG;EACjE,MAAM,wBACJ,OAAO,kBAAkB,aAAa,eAAe,GAAG;EAE1D,MAAM,2BAAW,IAAI,KAAa;EAClC,MAAM,WAMD,EAAE;AAEP,OAAK,IAAI,IAAI,GAAG,IAAI,SAAS,QAAQ,KAAK;GACxC,MAAM,MAAM,SAAS;GACrB,MAAM,qBAAqB,IAAI,WAAW,EAAE;GAE5C,MAAM,gBAAgB;IACpB,GAAG,eAAe;IAClB,GAAG;IACH,GAAG;IACJ;AAED,OAAI,IAAI,SAAS,WAAW,EAC1B;GAGF,MAAM,6DAAsC,IAAI,SAAS;GAEzD,MAAM,iBACJ,YAAY,gBACP,cAAc,UAAU,OACzB,OAAO,aAAa,cAClB,WACA;AAER,OAAI,CAAC,eACH;GAGF,MAAM,kBAAkB,GAAG,EAAE,GAAG;AAChC,YAAS,IAAI,gBAAgB;AAC7B,YAAS,KAAK;IACZ;IACA;IACA;IACA;IACA;IACD,CAAC;;AAGJ,OAAK,MAAM,CAAC,KAAK,WAAW,CAAC,GAAG,cAAc,SAAS,CAAC,CACtD,KAAI,CAAC,SAAS,IAAI,IAAI,EAAE;AACtB,OAAI,OAAO,OAAO,SAChB,QAAO,OAAO,YAAY;AAE5B,iBAAc,OAAO,IAAI;;AAI7B,OAAK,MAAM,KAAK,UAAU;GACxB,MAAM,WAAW,cAAc,IAAI,EAAE,gBAAgB;AACrD,OAAI,UAAU,OAAO,YAAY,SAAS,WAAW,EAAE,gBAAgB;AACrE,aAAS,OAAO,WAAW,EAAE,IAAI;IACjC,MAAM,EAAE,QAAQ,SAAS,GAAG,yBAAyB,EAAE;AACvD,aAAS,OAAO,WAAW,qBAAqB;AAChD;;AAGF,OAAI,UAAU;AACZ,QAAI,SAAS,OAAO,SAClB,UAAS,OAAO,YAAY;AAE9B,kBAAc,OAAO,EAAE,gBAAgB;;GAGzC,MAAM,EAAE,QAAQ,SAAS,GAAG,yBAAyB,EAAE;GACvD,MAAM,SAAS,QAAQ,SAAS,EAAE,IAAI,UAAU,EAAE,IAAI,UAAU;IAC9D,GAAG;IACH,QAAQ,EAAE;IACX,CAAC;AACF,iBAAc,IAAI,EAAE,iBAAiB;IACnC;IACA,QAAQ,EAAE;IACX,CAAC;;GAEJ"}
@@ -0,0 +1,57 @@
1
+ import { CreateHotkeySequenceOptions } from "./createHotkeySequence.cjs";
2
+ import { HotkeyCallback, HotkeySequence } from "@tanstack/hotkeys";
3
+
4
+ //#region src/createHotkeySequences.d.ts
5
+ /**
6
+ * A single sequence definition for use with `createHotkeySequences`.
7
+ */
8
+ interface CreateHotkeySequenceDefinition {
9
+ /** Array of hotkey strings that form the sequence */
10
+ sequence: HotkeySequence;
11
+ /** The function to call when the sequence is completed */
12
+ callback: HotkeyCallback;
13
+ /** Per-sequence options (merged on top of commonOptions) */
14
+ options?: CreateHotkeySequenceOptions;
15
+ }
16
+ /**
17
+ * SolidJS primitive for registering multiple keyboard shortcut sequences at once (Vim-style).
18
+ *
19
+ * Uses the singleton SequenceManager. Accepts a dynamic array of definitions, or accessors,
20
+ * so you can react to variable-length lists.
21
+ *
22
+ * Options are merged in this order:
23
+ * HotkeysProvider defaults < commonOptions < per-definition options
24
+ *
25
+ * Definitions with an empty `sequence` are skipped (no registration).
26
+ *
27
+ * @param sequences - Array of sequence definitions, or accessor returning them
28
+ * @param commonOptions - Shared options for all sequences, or accessor
29
+ *
30
+ * @example
31
+ * ```tsx
32
+ * function VimPalette() {
33
+ * createHotkeySequences([
34
+ * { sequence: ['G', 'G'], callback: () => scrollToTop() },
35
+ * { sequence: ['D', 'D'], callback: () => deleteLine() },
36
+ * ])
37
+ * }
38
+ * ```
39
+ *
40
+ * @example
41
+ * ```tsx
42
+ * function Dynamic(props) {
43
+ * createHotkeySequences(
44
+ * () => props.items.map((item) => ({
45
+ * sequence: item.chords,
46
+ * callback: item.action,
47
+ * options: { enabled: item.enabled },
48
+ * })),
49
+ * { preventDefault: true },
50
+ * )
51
+ * }
52
+ * ```
53
+ */
54
+ declare function createHotkeySequences(sequences: Array<CreateHotkeySequenceDefinition> | (() => Array<CreateHotkeySequenceDefinition>), commonOptions?: CreateHotkeySequenceOptions | (() => CreateHotkeySequenceOptions)): void;
55
+ //#endregion
56
+ export { CreateHotkeySequenceDefinition, createHotkeySequences };
57
+ //# sourceMappingURL=createHotkeySequences.d.cts.map
@@ -0,0 +1,57 @@
1
+ import { CreateHotkeySequenceOptions } from "./createHotkeySequence.js";
2
+ import { HotkeyCallback, HotkeySequence } from "@tanstack/hotkeys";
3
+
4
+ //#region src/createHotkeySequences.d.ts
5
+ /**
6
+ * A single sequence definition for use with `createHotkeySequences`.
7
+ */
8
+ interface CreateHotkeySequenceDefinition {
9
+ /** Array of hotkey strings that form the sequence */
10
+ sequence: HotkeySequence;
11
+ /** The function to call when the sequence is completed */
12
+ callback: HotkeyCallback;
13
+ /** Per-sequence options (merged on top of commonOptions) */
14
+ options?: CreateHotkeySequenceOptions;
15
+ }
16
+ /**
17
+ * SolidJS primitive for registering multiple keyboard shortcut sequences at once (Vim-style).
18
+ *
19
+ * Uses the singleton SequenceManager. Accepts a dynamic array of definitions, or accessors,
20
+ * so you can react to variable-length lists.
21
+ *
22
+ * Options are merged in this order:
23
+ * HotkeysProvider defaults < commonOptions < per-definition options
24
+ *
25
+ * Definitions with an empty `sequence` are skipped (no registration).
26
+ *
27
+ * @param sequences - Array of sequence definitions, or accessor returning them
28
+ * @param commonOptions - Shared options for all sequences, or accessor
29
+ *
30
+ * @example
31
+ * ```tsx
32
+ * function VimPalette() {
33
+ * createHotkeySequences([
34
+ * { sequence: ['G', 'G'], callback: () => scrollToTop() },
35
+ * { sequence: ['D', 'D'], callback: () => deleteLine() },
36
+ * ])
37
+ * }
38
+ * ```
39
+ *
40
+ * @example
41
+ * ```tsx
42
+ * function Dynamic(props) {
43
+ * createHotkeySequences(
44
+ * () => props.items.map((item) => ({
45
+ * sequence: item.chords,
46
+ * callback: item.action,
47
+ * options: { enabled: item.enabled },
48
+ * })),
49
+ * { preventDefault: true },
50
+ * )
51
+ * }
52
+ * ```
53
+ */
54
+ declare function createHotkeySequences(sequences: Array<CreateHotkeySequenceDefinition> | (() => Array<CreateHotkeySequenceDefinition>), commonOptions?: CreateHotkeySequenceOptions | (() => CreateHotkeySequenceOptions)): void;
55
+ //#endregion
56
+ export { CreateHotkeySequenceDefinition, createHotkeySequences };
57
+ //# sourceMappingURL=createHotkeySequences.d.ts.map
@@ -0,0 +1,110 @@
1
+ import { useDefaultHotkeysOptions } from "./HotkeysProvider.js";
2
+ import { formatHotkeySequence, getSequenceManager } from "@tanstack/hotkeys";
3
+ import { createEffect, onCleanup } from "solid-js";
4
+
5
+ //#region src/createHotkeySequences.ts
6
+ /**
7
+ * SolidJS primitive for registering multiple keyboard shortcut sequences at once (Vim-style).
8
+ *
9
+ * Uses the singleton SequenceManager. Accepts a dynamic array of definitions, or accessors,
10
+ * so you can react to variable-length lists.
11
+ *
12
+ * Options are merged in this order:
13
+ * HotkeysProvider defaults < commonOptions < per-definition options
14
+ *
15
+ * Definitions with an empty `sequence` are skipped (no registration).
16
+ *
17
+ * @param sequences - Array of sequence definitions, or accessor returning them
18
+ * @param commonOptions - Shared options for all sequences, or accessor
19
+ *
20
+ * @example
21
+ * ```tsx
22
+ * function VimPalette() {
23
+ * createHotkeySequences([
24
+ * { sequence: ['G', 'G'], callback: () => scrollToTop() },
25
+ * { sequence: ['D', 'D'], callback: () => deleteLine() },
26
+ * ])
27
+ * }
28
+ * ```
29
+ *
30
+ * @example
31
+ * ```tsx
32
+ * function Dynamic(props) {
33
+ * createHotkeySequences(
34
+ * () => props.items.map((item) => ({
35
+ * sequence: item.chords,
36
+ * callback: item.action,
37
+ * options: { enabled: item.enabled },
38
+ * })),
39
+ * { preventDefault: true },
40
+ * )
41
+ * }
42
+ * ```
43
+ */
44
+ function createHotkeySequences(sequences, commonOptions = {}) {
45
+ const defaultOptions = useDefaultHotkeysOptions();
46
+ const manager = getSequenceManager();
47
+ const registrations = /* @__PURE__ */ new Map();
48
+ onCleanup(() => {
49
+ for (const { handle } of registrations.values()) if (handle.isActive) handle.unregister();
50
+ registrations.clear();
51
+ });
52
+ createEffect(() => {
53
+ const resolved = typeof sequences === "function" ? sequences() : sequences;
54
+ const resolvedCommonOptions = typeof commonOptions === "function" ? commonOptions() : commonOptions;
55
+ const nextKeys = /* @__PURE__ */ new Set();
56
+ const prepared = [];
57
+ for (let i = 0; i < resolved.length; i++) {
58
+ const def = resolved[i];
59
+ const resolvedDefOptions = def.options ?? {};
60
+ const mergedOptions = {
61
+ ...defaultOptions.hotkeySequence,
62
+ ...resolvedCommonOptions,
63
+ ...resolvedDefOptions
64
+ };
65
+ if (def.sequence.length === 0) continue;
66
+ const sequenceString = formatHotkeySequence(def.sequence);
67
+ const resolvedTarget = "target" in mergedOptions ? mergedOptions.target ?? null : typeof document !== "undefined" ? document : null;
68
+ if (!resolvedTarget) continue;
69
+ const registrationKey = `${i}:${sequenceString}`;
70
+ nextKeys.add(registrationKey);
71
+ prepared.push({
72
+ registrationKey,
73
+ def,
74
+ mergedOptions,
75
+ sequenceString,
76
+ resolvedTarget
77
+ });
78
+ }
79
+ for (const [key, record] of [...registrations.entries()]) if (!nextKeys.has(key)) {
80
+ if (record.handle.isActive) record.handle.unregister();
81
+ registrations.delete(key);
82
+ }
83
+ for (const p of prepared) {
84
+ const existing = registrations.get(p.registrationKey);
85
+ if (existing?.handle.isActive && existing.target === p.resolvedTarget) {
86
+ existing.handle.callback = p.def.callback;
87
+ const { target: _target, ...optionsWithoutTarget } = p.mergedOptions;
88
+ existing.handle.setOptions(optionsWithoutTarget);
89
+ continue;
90
+ }
91
+ if (existing) {
92
+ if (existing.handle.isActive) existing.handle.unregister();
93
+ registrations.delete(p.registrationKey);
94
+ }
95
+ const { target: _target, ...optionsWithoutTarget } = p.mergedOptions;
96
+ const handle = manager.register(p.def.sequence, p.def.callback, {
97
+ ...optionsWithoutTarget,
98
+ target: p.resolvedTarget
99
+ });
100
+ registrations.set(p.registrationKey, {
101
+ handle,
102
+ target: p.resolvedTarget
103
+ });
104
+ }
105
+ });
106
+ }
107
+
108
+ //#endregion
109
+ export { createHotkeySequences };
110
+ //# sourceMappingURL=createHotkeySequences.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createHotkeySequences.js","names":[],"sources":["../src/createHotkeySequences.ts"],"sourcesContent":["import { createEffect, onCleanup } from 'solid-js'\nimport { formatHotkeySequence, getSequenceManager } from '@tanstack/hotkeys'\nimport { useDefaultHotkeysOptions } from './HotkeysProvider'\nimport type { CreateHotkeySequenceOptions } from './createHotkeySequence'\nimport type {\n HotkeyCallback,\n HotkeySequence,\n SequenceRegistrationHandle,\n} from '@tanstack/hotkeys'\n\n/**\n * A single sequence definition for use with `createHotkeySequences`.\n */\nexport interface CreateHotkeySequenceDefinition {\n /** Array of hotkey strings that form the sequence */\n sequence: HotkeySequence\n /** The function to call when the sequence is completed */\n callback: HotkeyCallback\n /** Per-sequence options (merged on top of commonOptions) */\n options?: CreateHotkeySequenceOptions\n}\n\n/**\n * SolidJS primitive for registering multiple keyboard shortcut sequences at once (Vim-style).\n *\n * Uses the singleton SequenceManager. Accepts a dynamic array of definitions, or accessors,\n * so you can react to variable-length lists.\n *\n * Options are merged in this order:\n * HotkeysProvider defaults < commonOptions < per-definition options\n *\n * Definitions with an empty `sequence` are skipped (no registration).\n *\n * @param sequences - Array of sequence definitions, or accessor returning them\n * @param commonOptions - Shared options for all sequences, or accessor\n *\n * @example\n * ```tsx\n * function VimPalette() {\n * createHotkeySequences([\n * { sequence: ['G', 'G'], callback: () => scrollToTop() },\n * { sequence: ['D', 'D'], callback: () => deleteLine() },\n * ])\n * }\n * ```\n *\n * @example\n * ```tsx\n * function Dynamic(props) {\n * createHotkeySequences(\n * () => props.items.map((item) => ({\n * sequence: item.chords,\n * callback: item.action,\n * options: { enabled: item.enabled },\n * })),\n * { preventDefault: true },\n * )\n * }\n * ```\n */\nexport function createHotkeySequences(\n sequences:\n | Array<CreateHotkeySequenceDefinition>\n | (() => Array<CreateHotkeySequenceDefinition>),\n commonOptions:\n | CreateHotkeySequenceOptions\n | (() => CreateHotkeySequenceOptions) = {},\n): void {\n type RegistrationRecord = {\n handle: SequenceRegistrationHandle\n target: Document | HTMLElement | Window\n }\n\n const defaultOptions = useDefaultHotkeysOptions()\n const manager = getSequenceManager()\n\n const registrations = new Map<string, RegistrationRecord>()\n\n onCleanup(() => {\n for (const { handle } of registrations.values()) {\n if (handle.isActive) {\n handle.unregister()\n }\n }\n registrations.clear()\n })\n\n createEffect(() => {\n const resolved = typeof sequences === 'function' ? sequences() : sequences\n const resolvedCommonOptions =\n typeof commonOptions === 'function' ? commonOptions() : commonOptions\n\n const nextKeys = new Set<string>()\n const prepared: Array<{\n registrationKey: string\n def: CreateHotkeySequenceDefinition\n mergedOptions: CreateHotkeySequenceOptions\n sequenceString: string\n resolvedTarget: Document | HTMLElement | Window\n }> = []\n\n for (let i = 0; i < resolved.length; i++) {\n const def = resolved[i]!\n const resolvedDefOptions = def.options ?? {}\n\n const mergedOptions = {\n ...defaultOptions.hotkeySequence,\n ...resolvedCommonOptions,\n ...resolvedDefOptions,\n } as CreateHotkeySequenceOptions\n\n if (def.sequence.length === 0) {\n continue\n }\n\n const sequenceString = formatHotkeySequence(def.sequence)\n\n const resolvedTarget =\n 'target' in mergedOptions\n ? (mergedOptions.target ?? null)\n : typeof document !== 'undefined'\n ? document\n : null\n\n if (!resolvedTarget) {\n continue\n }\n\n const registrationKey = `${i}:${sequenceString}`\n nextKeys.add(registrationKey)\n prepared.push({\n registrationKey,\n def,\n mergedOptions,\n sequenceString,\n resolvedTarget,\n })\n }\n\n for (const [key, record] of [...registrations.entries()]) {\n if (!nextKeys.has(key)) {\n if (record.handle.isActive) {\n record.handle.unregister()\n }\n registrations.delete(key)\n }\n }\n\n for (const p of prepared) {\n const existing = registrations.get(p.registrationKey)\n if (existing?.handle.isActive && existing.target === p.resolvedTarget) {\n existing.handle.callback = p.def.callback\n const { target: _target, ...optionsWithoutTarget } = p.mergedOptions\n existing.handle.setOptions(optionsWithoutTarget)\n continue\n }\n\n if (existing) {\n if (existing.handle.isActive) {\n existing.handle.unregister()\n }\n registrations.delete(p.registrationKey)\n }\n\n const { target: _target, ...optionsWithoutTarget } = p.mergedOptions\n const handle = manager.register(p.def.sequence, p.def.callback, {\n ...optionsWithoutTarget,\n target: p.resolvedTarget,\n })\n registrations.set(p.registrationKey, {\n handle,\n target: p.resolvedTarget,\n })\n }\n })\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4DA,SAAgB,sBACd,WAGA,gBAE0C,EAAE,EACtC;CAMN,MAAM,iBAAiB,0BAA0B;CACjD,MAAM,UAAU,oBAAoB;CAEpC,MAAM,gCAAgB,IAAI,KAAiC;AAE3D,iBAAgB;AACd,OAAK,MAAM,EAAE,YAAY,cAAc,QAAQ,CAC7C,KAAI,OAAO,SACT,QAAO,YAAY;AAGvB,gBAAc,OAAO;GACrB;AAEF,oBAAmB;EACjB,MAAM,WAAW,OAAO,cAAc,aAAa,WAAW,GAAG;EACjE,MAAM,wBACJ,OAAO,kBAAkB,aAAa,eAAe,GAAG;EAE1D,MAAM,2BAAW,IAAI,KAAa;EAClC,MAAM,WAMD,EAAE;AAEP,OAAK,IAAI,IAAI,GAAG,IAAI,SAAS,QAAQ,KAAK;GACxC,MAAM,MAAM,SAAS;GACrB,MAAM,qBAAqB,IAAI,WAAW,EAAE;GAE5C,MAAM,gBAAgB;IACpB,GAAG,eAAe;IAClB,GAAG;IACH,GAAG;IACJ;AAED,OAAI,IAAI,SAAS,WAAW,EAC1B;GAGF,MAAM,iBAAiB,qBAAqB,IAAI,SAAS;GAEzD,MAAM,iBACJ,YAAY,gBACP,cAAc,UAAU,OACzB,OAAO,aAAa,cAClB,WACA;AAER,OAAI,CAAC,eACH;GAGF,MAAM,kBAAkB,GAAG,EAAE,GAAG;AAChC,YAAS,IAAI,gBAAgB;AAC7B,YAAS,KAAK;IACZ;IACA;IACA;IACA;IACA;IACD,CAAC;;AAGJ,OAAK,MAAM,CAAC,KAAK,WAAW,CAAC,GAAG,cAAc,SAAS,CAAC,CACtD,KAAI,CAAC,SAAS,IAAI,IAAI,EAAE;AACtB,OAAI,OAAO,OAAO,SAChB,QAAO,OAAO,YAAY;AAE5B,iBAAc,OAAO,IAAI;;AAI7B,OAAK,MAAM,KAAK,UAAU;GACxB,MAAM,WAAW,cAAc,IAAI,EAAE,gBAAgB;AACrD,OAAI,UAAU,OAAO,YAAY,SAAS,WAAW,EAAE,gBAAgB;AACrE,aAAS,OAAO,WAAW,EAAE,IAAI;IACjC,MAAM,EAAE,QAAQ,SAAS,GAAG,yBAAyB,EAAE;AACvD,aAAS,OAAO,WAAW,qBAAqB;AAChD;;AAGF,OAAI,UAAU;AACZ,QAAI,SAAS,OAAO,SAClB,UAAS,OAAO,YAAY;AAE9B,kBAAc,OAAO,EAAE,gBAAgB;;GAGzC,MAAM,EAAE,QAAQ,SAAS,GAAG,yBAAyB,EAAE;GACvD,MAAM,SAAS,QAAQ,SAAS,EAAE,IAAI,UAAU,EAAE,IAAI,UAAU;IAC9D,GAAG;IACH,QAAQ,EAAE;IACX,CAAC;AACF,iBAAc,IAAI,EAAE,iBAAiB;IACnC;IACA,QAAQ,EAAE;IACX,CAAC;;GAEJ"}
package/dist/index.cjs CHANGED
@@ -6,6 +6,7 @@ const require_createHeldKeys = require('./createHeldKeys.cjs');
6
6
  const require_createHeldKeyCodes = require('./createHeldKeyCodes.cjs');
7
7
  const require_createKeyHold = require('./createKeyHold.cjs');
8
8
  const require_createHotkeySequence = require('./createHotkeySequence.cjs');
9
+ const require_createHotkeySequences = require('./createHotkeySequences.cjs');
9
10
  const require_createHotkeyRecorder = require('./createHotkeyRecorder.cjs');
10
11
  const require_createHotkeySequenceRecorder = require('./createHotkeySequenceRecorder.cjs');
11
12
 
@@ -16,6 +17,7 @@ exports.createHotkey = require_createHotkey.createHotkey;
16
17
  exports.createHotkeyRecorder = require_createHotkeyRecorder.createHotkeyRecorder;
17
18
  exports.createHotkeySequence = require_createHotkeySequence.createHotkeySequence;
18
19
  exports.createHotkeySequenceRecorder = require_createHotkeySequenceRecorder.createHotkeySequenceRecorder;
20
+ exports.createHotkeySequences = require_createHotkeySequences.createHotkeySequences;
19
21
  exports.createHotkeys = require_createHotkeys.createHotkeys;
20
22
  exports.createKeyHold = require_createKeyHold.createKeyHold;
21
23
  exports.useDefaultHotkeysOptions = require_HotkeysProvider.useDefaultHotkeysOptions;
package/dist/index.d.cts CHANGED
@@ -5,7 +5,8 @@ import { CreateHotkeyDefinition, createHotkeys } from "./createHotkeys.cjs";
5
5
  import { createHeldKeys } from "./createHeldKeys.cjs";
6
6
  import { createHeldKeyCodes } from "./createHeldKeyCodes.cjs";
7
7
  import { createKeyHold } from "./createKeyHold.cjs";
8
+ import { CreateHotkeySequenceDefinition, createHotkeySequences } from "./createHotkeySequences.cjs";
8
9
  import { SolidHotkeyRecorder, createHotkeyRecorder } from "./createHotkeyRecorder.cjs";
9
10
  import { SolidHotkeySequenceRecorder, createHotkeySequenceRecorder } from "./createHotkeySequenceRecorder.cjs";
10
11
  export * from "@tanstack/hotkeys";
11
- export { CreateHotkeyDefinition, CreateHotkeyOptions, CreateHotkeySequenceOptions, HotkeysProvider, HotkeysProviderOptions, HotkeysProviderProps, SolidHotkeyRecorder, SolidHotkeySequenceRecorder, createHeldKeyCodes, createHeldKeys, createHotkey, createHotkeyRecorder, createHotkeySequence, createHotkeySequenceRecorder, createHotkeys, createKeyHold, useDefaultHotkeysOptions, useHotkeysContext };
12
+ export { CreateHotkeyDefinition, CreateHotkeyOptions, CreateHotkeySequenceDefinition, CreateHotkeySequenceOptions, HotkeysProvider, HotkeysProviderOptions, HotkeysProviderProps, SolidHotkeyRecorder, SolidHotkeySequenceRecorder, createHeldKeyCodes, createHeldKeys, createHotkey, createHotkeyRecorder, createHotkeySequence, createHotkeySequenceRecorder, createHotkeySequences, createHotkeys, createKeyHold, useDefaultHotkeysOptions, useHotkeysContext };
package/dist/index.d.ts CHANGED
@@ -5,7 +5,8 @@ import { CreateHotkeyDefinition, createHotkeys } from "./createHotkeys.js";
5
5
  import { createHeldKeys } from "./createHeldKeys.js";
6
6
  import { createHeldKeyCodes } from "./createHeldKeyCodes.js";
7
7
  import { createKeyHold } from "./createKeyHold.js";
8
+ import { CreateHotkeySequenceDefinition, createHotkeySequences } from "./createHotkeySequences.js";
8
9
  import { SolidHotkeyRecorder, createHotkeyRecorder } from "./createHotkeyRecorder.js";
9
10
  import { SolidHotkeySequenceRecorder, createHotkeySequenceRecorder } from "./createHotkeySequenceRecorder.js";
10
11
  export * from "@tanstack/hotkeys";
11
- export { CreateHotkeyDefinition, CreateHotkeyOptions, CreateHotkeySequenceOptions, HotkeysProvider, HotkeysProviderOptions, HotkeysProviderProps, SolidHotkeyRecorder, SolidHotkeySequenceRecorder, createHeldKeyCodes, createHeldKeys, createHotkey, createHotkeyRecorder, createHotkeySequence, createHotkeySequenceRecorder, createHotkeys, createKeyHold, useDefaultHotkeysOptions, useHotkeysContext };
12
+ export { CreateHotkeyDefinition, CreateHotkeyOptions, CreateHotkeySequenceDefinition, CreateHotkeySequenceOptions, HotkeysProvider, HotkeysProviderOptions, HotkeysProviderProps, SolidHotkeyRecorder, SolidHotkeySequenceRecorder, createHeldKeyCodes, createHeldKeys, createHotkey, createHotkeyRecorder, createHotkeySequence, createHotkeySequenceRecorder, createHotkeySequences, createHotkeys, createKeyHold, useDefaultHotkeysOptions, useHotkeysContext };
package/dist/index.js CHANGED
@@ -5,9 +5,10 @@ import { createHeldKeys } from "./createHeldKeys.js";
5
5
  import { createHeldKeyCodes } from "./createHeldKeyCodes.js";
6
6
  import { createKeyHold } from "./createKeyHold.js";
7
7
  import { createHotkeySequence } from "./createHotkeySequence.js";
8
+ import { createHotkeySequences } from "./createHotkeySequences.js";
8
9
  import { createHotkeyRecorder } from "./createHotkeyRecorder.js";
9
10
  import { createHotkeySequenceRecorder } from "./createHotkeySequenceRecorder.js";
10
11
 
11
12
  export * from "@tanstack/hotkeys"
12
13
 
13
- export { HotkeysProvider, createHeldKeyCodes, createHeldKeys, createHotkey, createHotkeyRecorder, createHotkeySequence, createHotkeySequenceRecorder, createHotkeys, createKeyHold, useDefaultHotkeysOptions, useHotkeysContext };
14
+ export { HotkeysProvider, createHeldKeyCodes, createHeldKeys, createHotkey, createHotkeyRecorder, createHotkeySequence, createHotkeySequenceRecorder, createHotkeySequences, createHotkeys, createKeyHold, useDefaultHotkeysOptions, useHotkeysContext };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/solid-hotkeys",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "SolidJS adapter for TanStack Hotkeys",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
@@ -89,6 +89,17 @@ export function createHotkey(
89
89
  const manager = getHotkeyManager()
90
90
 
91
91
  let registration: HotkeyRegistrationHandle | null = null
92
+ let lastHotkeyString: Hotkey | null = null
93
+ let lastTarget: HTMLElement | Document | Window | null = null
94
+
95
+ onCleanup(() => {
96
+ if (registration?.isActive) {
97
+ registration.unregister()
98
+ registration = null
99
+ }
100
+ lastHotkeyString = null
101
+ lastTarget = null
102
+ })
92
103
 
93
104
  createEffect(() => {
94
105
  // Resolve reactive values
@@ -119,36 +130,44 @@ export function createHotkey(
119
130
  : null
120
131
 
121
132
  if (!resolvedTarget) {
133
+ if (registration?.isActive) {
134
+ registration.unregister()
135
+ registration = null
136
+ }
137
+ lastHotkeyString = null
138
+ lastTarget = null
139
+ return
140
+ }
141
+
142
+ // Extract options without target (target is handled separately)
143
+ const { target: _target, ...optionsWithoutTarget } = mergedOptions
144
+
145
+ if (
146
+ registration?.isActive &&
147
+ lastHotkeyString === hotkeyString &&
148
+ lastTarget === resolvedTarget
149
+ ) {
150
+ registration.callback = callback
151
+ registration.setOptions(optionsWithoutTarget)
122
152
  return
123
153
  }
124
154
 
125
- // Unregister previous registration if it exists
126
155
  if (registration?.isActive) {
127
156
  registration.unregister()
128
157
  registration = null
129
158
  }
130
159
 
131
- // Extract options without target (target is handled separately)
132
- const { target: _target, ...optionsWithoutTarget } = mergedOptions
133
-
134
- // Register the hotkey
135
160
  registration = manager.register(hotkeyString, callback, {
136
161
  ...optionsWithoutTarget,
137
162
  target: resolvedTarget,
138
163
  })
139
164
 
140
- // Update callback and options on every effect run
141
165
  if (registration.isActive) {
142
166
  registration.callback = callback
143
167
  registration.setOptions(optionsWithoutTarget)
144
168
  }
145
169
 
146
- // Cleanup on disposal
147
- onCleanup(() => {
148
- if (registration?.isActive) {
149
- registration.unregister()
150
- registration = null
151
- }
152
- })
170
+ lastHotkeyString = hotkeyString
171
+ lastTarget = resolvedTarget
153
172
  })
154
173
  }
@@ -1,5 +1,5 @@
1
1
  import { createEffect, onCleanup } from 'solid-js'
2
- import { getSequenceManager } from '@tanstack/hotkeys'
2
+ import { formatHotkeySequence, getSequenceManager } from '@tanstack/hotkeys'
3
3
  import { useDefaultHotkeysOptions } from './HotkeysProvider'
4
4
  import type {
5
5
  HotkeyCallback,
@@ -72,6 +72,17 @@ export function createHotkeySequence(
72
72
  const manager = getSequenceManager()
73
73
 
74
74
  let registration: SequenceRegistrationHandle | null = null
75
+ let lastSequenceKey: string | null = null
76
+ let lastTarget: HTMLElement | Document | Window | null = null
77
+
78
+ onCleanup(() => {
79
+ if (registration?.isActive) {
80
+ registration.unregister()
81
+ registration = null
82
+ }
83
+ lastSequenceKey = null
84
+ lastTarget = null
85
+ })
75
86
 
76
87
  createEffect(() => {
77
88
  // Resolve reactive values
@@ -87,10 +98,6 @@ export function createHotkeySequence(
87
98
  // Extract options without target (target is handled separately)
88
99
  const { target: _target, ...optionsWithoutTarget } = mergedOptions
89
100
 
90
- if (resolvedSequence.length === 0) {
91
- return
92
- }
93
-
94
101
  // Resolve target: when explicitly provided (even as null), use it and skip if null.
95
102
  // When not provided, default to document. Matches createHotkey.
96
103
  const resolvedTarget =
@@ -100,34 +107,44 @@ export function createHotkeySequence(
100
107
  ? document
101
108
  : null
102
109
 
103
- if (!resolvedTarget) {
110
+ if (resolvedSequence.length === 0 || !resolvedTarget) {
111
+ if (registration?.isActive) {
112
+ registration.unregister()
113
+ registration = null
114
+ }
115
+ lastSequenceKey = null
116
+ lastTarget = null
117
+ return
118
+ }
119
+
120
+ const sequenceKey = formatHotkeySequence(resolvedSequence)
121
+
122
+ if (
123
+ registration?.isActive &&
124
+ lastSequenceKey === sequenceKey &&
125
+ lastTarget === resolvedTarget
126
+ ) {
127
+ registration.callback = callback
128
+ registration.setOptions(optionsWithoutTarget)
104
129
  return
105
130
  }
106
131
 
107
- // Unregister previous registration if it exists
108
132
  if (registration?.isActive) {
109
133
  registration.unregister()
110
134
  registration = null
111
135
  }
112
136
 
113
- // Register the sequence
114
137
  registration = manager.register(resolvedSequence, callback, {
115
138
  ...mergedOptions,
116
139
  target: resolvedTarget,
117
140
  })
118
141
 
119
- // Sync callback and options on every effect run
120
142
  if (registration.isActive) {
121
143
  registration.callback = callback
122
144
  registration.setOptions(optionsWithoutTarget)
123
145
  }
124
146
 
125
- // Cleanup on disposal
126
- onCleanup(() => {
127
- if (registration?.isActive) {
128
- registration.unregister()
129
- registration = null
130
- }
131
- })
147
+ lastSequenceKey = sequenceKey
148
+ lastTarget = resolvedTarget
132
149
  })
133
150
  }
@@ -0,0 +1,176 @@
1
+ import { createEffect, onCleanup } from 'solid-js'
2
+ import { formatHotkeySequence, getSequenceManager } from '@tanstack/hotkeys'
3
+ import { useDefaultHotkeysOptions } from './HotkeysProvider'
4
+ import type { CreateHotkeySequenceOptions } from './createHotkeySequence'
5
+ import type {
6
+ HotkeyCallback,
7
+ HotkeySequence,
8
+ SequenceRegistrationHandle,
9
+ } from '@tanstack/hotkeys'
10
+
11
+ /**
12
+ * A single sequence definition for use with `createHotkeySequences`.
13
+ */
14
+ export interface CreateHotkeySequenceDefinition {
15
+ /** Array of hotkey strings that form the sequence */
16
+ sequence: HotkeySequence
17
+ /** The function to call when the sequence is completed */
18
+ callback: HotkeyCallback
19
+ /** Per-sequence options (merged on top of commonOptions) */
20
+ options?: CreateHotkeySequenceOptions
21
+ }
22
+
23
+ /**
24
+ * SolidJS primitive for registering multiple keyboard shortcut sequences at once (Vim-style).
25
+ *
26
+ * Uses the singleton SequenceManager. Accepts a dynamic array of definitions, or accessors,
27
+ * so you can react to variable-length lists.
28
+ *
29
+ * Options are merged in this order:
30
+ * HotkeysProvider defaults < commonOptions < per-definition options
31
+ *
32
+ * Definitions with an empty `sequence` are skipped (no registration).
33
+ *
34
+ * @param sequences - Array of sequence definitions, or accessor returning them
35
+ * @param commonOptions - Shared options for all sequences, or accessor
36
+ *
37
+ * @example
38
+ * ```tsx
39
+ * function VimPalette() {
40
+ * createHotkeySequences([
41
+ * { sequence: ['G', 'G'], callback: () => scrollToTop() },
42
+ * { sequence: ['D', 'D'], callback: () => deleteLine() },
43
+ * ])
44
+ * }
45
+ * ```
46
+ *
47
+ * @example
48
+ * ```tsx
49
+ * function Dynamic(props) {
50
+ * createHotkeySequences(
51
+ * () => props.items.map((item) => ({
52
+ * sequence: item.chords,
53
+ * callback: item.action,
54
+ * options: { enabled: item.enabled },
55
+ * })),
56
+ * { preventDefault: true },
57
+ * )
58
+ * }
59
+ * ```
60
+ */
61
+ export function createHotkeySequences(
62
+ sequences:
63
+ | Array<CreateHotkeySequenceDefinition>
64
+ | (() => Array<CreateHotkeySequenceDefinition>),
65
+ commonOptions:
66
+ | CreateHotkeySequenceOptions
67
+ | (() => CreateHotkeySequenceOptions) = {},
68
+ ): void {
69
+ type RegistrationRecord = {
70
+ handle: SequenceRegistrationHandle
71
+ target: Document | HTMLElement | Window
72
+ }
73
+
74
+ const defaultOptions = useDefaultHotkeysOptions()
75
+ const manager = getSequenceManager()
76
+
77
+ const registrations = new Map<string, RegistrationRecord>()
78
+
79
+ onCleanup(() => {
80
+ for (const { handle } of registrations.values()) {
81
+ if (handle.isActive) {
82
+ handle.unregister()
83
+ }
84
+ }
85
+ registrations.clear()
86
+ })
87
+
88
+ createEffect(() => {
89
+ const resolved = typeof sequences === 'function' ? sequences() : sequences
90
+ const resolvedCommonOptions =
91
+ typeof commonOptions === 'function' ? commonOptions() : commonOptions
92
+
93
+ const nextKeys = new Set<string>()
94
+ const prepared: Array<{
95
+ registrationKey: string
96
+ def: CreateHotkeySequenceDefinition
97
+ mergedOptions: CreateHotkeySequenceOptions
98
+ sequenceString: string
99
+ resolvedTarget: Document | HTMLElement | Window
100
+ }> = []
101
+
102
+ for (let i = 0; i < resolved.length; i++) {
103
+ const def = resolved[i]!
104
+ const resolvedDefOptions = def.options ?? {}
105
+
106
+ const mergedOptions = {
107
+ ...defaultOptions.hotkeySequence,
108
+ ...resolvedCommonOptions,
109
+ ...resolvedDefOptions,
110
+ } as CreateHotkeySequenceOptions
111
+
112
+ if (def.sequence.length === 0) {
113
+ continue
114
+ }
115
+
116
+ const sequenceString = formatHotkeySequence(def.sequence)
117
+
118
+ const resolvedTarget =
119
+ 'target' in mergedOptions
120
+ ? (mergedOptions.target ?? null)
121
+ : typeof document !== 'undefined'
122
+ ? document
123
+ : null
124
+
125
+ if (!resolvedTarget) {
126
+ continue
127
+ }
128
+
129
+ const registrationKey = `${i}:${sequenceString}`
130
+ nextKeys.add(registrationKey)
131
+ prepared.push({
132
+ registrationKey,
133
+ def,
134
+ mergedOptions,
135
+ sequenceString,
136
+ resolvedTarget,
137
+ })
138
+ }
139
+
140
+ for (const [key, record] of [...registrations.entries()]) {
141
+ if (!nextKeys.has(key)) {
142
+ if (record.handle.isActive) {
143
+ record.handle.unregister()
144
+ }
145
+ registrations.delete(key)
146
+ }
147
+ }
148
+
149
+ for (const p of prepared) {
150
+ const existing = registrations.get(p.registrationKey)
151
+ if (existing?.handle.isActive && existing.target === p.resolvedTarget) {
152
+ existing.handle.callback = p.def.callback
153
+ const { target: _target, ...optionsWithoutTarget } = p.mergedOptions
154
+ existing.handle.setOptions(optionsWithoutTarget)
155
+ continue
156
+ }
157
+
158
+ if (existing) {
159
+ if (existing.handle.isActive) {
160
+ existing.handle.unregister()
161
+ }
162
+ registrations.delete(p.registrationKey)
163
+ }
164
+
165
+ const { target: _target, ...optionsWithoutTarget } = p.mergedOptions
166
+ const handle = manager.register(p.def.sequence, p.def.callback, {
167
+ ...optionsWithoutTarget,
168
+ target: p.resolvedTarget,
169
+ })
170
+ registrations.set(p.registrationKey, {
171
+ handle,
172
+ target: p.resolvedTarget,
173
+ })
174
+ }
175
+ })
176
+ }
package/src/index.ts CHANGED
@@ -11,5 +11,6 @@ export * from './createHeldKeys'
11
11
  export * from './createHeldKeyCodes'
12
12
  export * from './createKeyHold'
13
13
  export * from './createHotkeySequence'
14
+ export * from './createHotkeySequences'
14
15
  export * from './createHotkeyRecorder'
15
16
  export * from './createHotkeySequenceRecorder'