@tanstack/angular-hotkeys 0.13.0 → 0.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,19 +1,58 @@
1
- import { InjectHotkeyOptions } from "./injectHotkey.js";
2
- import { InjectHotkeySequenceOptions } from "./injectHotkeySequence.js";
3
- import { HotkeyRecorderOptions, HotkeySequenceRecorderOptions } from "@tanstack/hotkeys";
4
- import { InjectionToken, StaticProvider } from "@angular/core";
1
+ import { InjectHotkeyCommonOptions } from "./injectHotkey.js";
2
+ import { AppHotkeys, AppHotkeysState, AppHotkeysStore, CustomHotkeys, HotkeyChordRecorderOptions, HotkeySequenceRecorderOptions } from "@tanstack/hotkeys";
3
+ import { InjectionToken, Provider, Signal } from "@angular/core";
5
4
  //#region src/hotkeys-provider.d.ts
6
5
  export interface HotkeysProviderOptions {
7
- hotkey?: Partial<InjectHotkeyOptions>;
8
- hotkeyRecorder?: Partial<HotkeyRecorderOptions>;
9
- hotkeySequenceRecorder?: Partial<HotkeySequenceRecorderOptions>;
10
- hotkeySequence?: Partial<InjectHotkeySequenceOptions>;
6
+ /**
7
+ * Defaults for every hotkey registered by `injectHotkey` and `injectHotkeys`.
8
+ * `requireReset` applies to chords and `timeout` applies to sequences.
9
+ */
10
+ hotkey?: InjectHotkeyCommonOptions;
11
+ /** Defaults for every `injectHotkeyRecorder`. */
12
+ hotkeyRecorder?: Partial<Omit<HotkeyChordRecorderOptions & HotkeySequenceRecorderOptions, 'maxSteps' | 'onRecord' | 'validate'>>;
13
+ }
14
+ /** Options for `provideHotkeys`. */
15
+ export interface ProvideHotkeysOptions {
16
+ defaultOptions?: HotkeysProviderOptions;
17
+ /**
18
+ * Every app hotkey, from `defineAppHotkeys`. Listed by
19
+ * `injectAppHotkeyList` whether or not they are registered.
20
+ */
21
+ appHotkeys?: AppHotkeys;
22
+ /**
23
+ * Starting custom hotkeys, such as saved data. Read once; the provider owns
24
+ * the state afterwards. Invalid entries are ignored with a warning.
25
+ */
26
+ customHotkeys?: CustomHotkeys | null;
27
+ /** Called with the custom hotkeys after every change, ready to save. */
28
+ onCustomHotkeysChange?: (customHotkeys: CustomHotkeys) => void;
11
29
  }
12
30
  export interface HotkeysContextValue {
13
31
  readonly defaultOptions: HotkeysProviderOptions;
32
+ readonly appHotkeysStore: AppHotkeysStore | null;
33
+ /** Tracks app hotkeys and custom hotkeys so signals and effects re-run. */
34
+ readonly appHotkeysState: Signal<AppHotkeysState | null>;
14
35
  }
15
36
  export declare const HOTKEYS_INJECTION_TOKEN: InjectionToken<HotkeysContextValue>;
16
- export declare function provideHotkeys(defaultOptions: HotkeysProviderOptions): StaticProvider;
37
+ /**
38
+ * Provides default options, app hotkeys, and the user's custom hotkeys to an
39
+ * injector, such as the application or a component.
40
+ *
41
+ * @example
42
+ * ```ts
43
+ * bootstrapApplication(App, {
44
+ * providers: [
45
+ * provideHotkeys({
46
+ * appHotkeys,
47
+ * customHotkeys: JSON.parse(localStorage.getItem('hotkeys') ?? '{}'),
48
+ * onCustomHotkeysChange: (custom) =>
49
+ * localStorage.setItem('hotkeys', JSON.stringify(custom)),
50
+ * }),
51
+ * ],
52
+ * })
53
+ * ```
54
+ */
55
+ export declare function provideHotkeys(options?: ProvideHotkeysOptions): Provider;
17
56
  export declare function injectHotkeysContext(): HotkeysContextValue;
18
57
  export declare function injectDefaultHotkeysOptions(): HotkeysProviderOptions;
19
58
  //#endregion
@@ -1,14 +1,59 @@
1
- import { InjectionToken, inject } from "@angular/core";
1
+ import { AppHotkeysStore, publishAppHotkeysStore } from "@tanstack/hotkeys";
2
+ import { DestroyRef, InjectionToken, inject, signal } from "@angular/core";
2
3
 
3
4
  //#region src/hotkeys-provider.ts
5
+ const NO_APP_HOTKEYS = signal(null).asReadonly();
4
6
  const HOTKEYS_INJECTION_TOKEN = new InjectionToken("HOTKEYS_INJECTION_TOKEN", {
5
7
  providedIn: "root",
6
- factory: () => ({ defaultOptions: {} })
8
+ factory: () => ({
9
+ defaultOptions: {},
10
+ appHotkeysStore: null,
11
+ appHotkeysState: NO_APP_HOTKEYS
12
+ })
7
13
  });
8
- function provideHotkeys(defaultOptions) {
14
+ /**
15
+ * Provides default options, app hotkeys, and the user's custom hotkeys to an
16
+ * injector, such as the application or a component.
17
+ *
18
+ * @example
19
+ * ```ts
20
+ * bootstrapApplication(App, {
21
+ * providers: [
22
+ * provideHotkeys({
23
+ * appHotkeys,
24
+ * customHotkeys: JSON.parse(localStorage.getItem('hotkeys') ?? '{}'),
25
+ * onCustomHotkeysChange: (custom) =>
26
+ * localStorage.setItem('hotkeys', JSON.stringify(custom)),
27
+ * }),
28
+ * ],
29
+ * })
30
+ * ```
31
+ */
32
+ function provideHotkeys(options = {}) {
9
33
  return {
10
34
  provide: HOTKEYS_INJECTION_TOKEN,
11
- useValue: { defaultOptions }
35
+ useFactory: () => {
36
+ const defaultOptions = options.defaultOptions ?? {};
37
+ if (!options.appHotkeys) return {
38
+ defaultOptions,
39
+ appHotkeysStore: null,
40
+ appHotkeysState: NO_APP_HOTKEYS
41
+ };
42
+ const store = new AppHotkeysStore(options.appHotkeys, {
43
+ customHotkeys: options.customHotkeys,
44
+ onCustomHotkeysChange: options.onCustomHotkeysChange
45
+ });
46
+ const state = signal(store.store.state);
47
+ const subscription = store.store.subscribe(() => state.set(store.store.state));
48
+ const destroyRef = inject(DestroyRef);
49
+ destroyRef.onDestroy(() => subscription.unsubscribe());
50
+ if (typeof document !== "undefined") destroyRef.onDestroy(publishAppHotkeysStore(store));
51
+ return {
52
+ defaultOptions,
53
+ appHotkeysStore: store,
54
+ appHotkeysState: state.asReadonly()
55
+ };
56
+ }
12
57
  };
13
58
  }
14
59
  function injectHotkeysContext() {
package/dist/index.d.ts CHANGED
@@ -1,14 +1,12 @@
1
- import { InjectHotkeyOptions, injectHotkey } from "./injectHotkey.js";
2
- import { InjectHotkeySequenceOptions, injectHotkeySequence } from "./injectHotkeySequence.js";
3
- import { HOTKEYS_INJECTION_TOKEN, HotkeysContextValue, HotkeysProviderOptions, injectDefaultHotkeysOptions, injectHotkeysContext, provideHotkeys } from "./hotkeys-provider.js";
1
+ import { InjectAppHotkeyOptions, InjectHotkeyChordOptions, InjectHotkeyCommonOptions, InjectHotkeyOptions, InjectHotkeyOptionsFor, InjectHotkeySequenceOptions, InjectHotkeyTarget, injectHotkey } from "./injectHotkey.js";
2
+ import { HOTKEYS_INJECTION_TOKEN, HotkeysContextValue, HotkeysProviderOptions, ProvideHotkeysOptions, injectDefaultHotkeysOptions, injectHotkeysContext, provideHotkeys } from "./hotkeys-provider.js";
4
3
  import { InjectHotkeyDefinition, injectHotkeys } from "./injectHotkeys.js";
5
- import { InjectHotkeySequenceDefinition, injectHotkeySequences } from "./injectHotkeySequences.js";
6
4
  import { injectHeldKeys } from "./injectHeldKeys.js";
7
5
  import { injectHeldKeyCodes } from "./injectHeldKeyCodes.js";
8
6
  import { injectKeyHold } from "./injectKeyHold.js";
9
7
  import { AngularHotkeyRecorder, injectHotkeyRecorder } from "./injectHotkeyRecorder.js";
10
- import { HotkeyRegistrationsResult, injectHotkeyRegistrations } from "./injectHotkeyRegistrations.js";
11
- import { AngularHotkeySequenceRecorder, injectHotkeySequenceRecorder } from "./injectHotkeySequenceRecorder.js";
8
+ import { injectHotkeyRegistrations } from "./injectHotkeyRegistrations.js";
12
9
  import { injectHotkeyHint } from "./injectHotkeyHint.js";
10
+ import { InjectCustomHotkeysResult, injectAppHotkeyList, injectAppHotkeys, injectCustomHotkeys } from "./injectAppHotkeys.js";
13
11
  export * from "@tanstack/hotkeys";
14
- export { AngularHotkeyRecorder, AngularHotkeySequenceRecorder, HOTKEYS_INJECTION_TOKEN, HotkeyRegistrationsResult, HotkeysContextValue, HotkeysProviderOptions, InjectHotkeyDefinition, InjectHotkeyOptions, InjectHotkeySequenceDefinition, InjectHotkeySequenceOptions, injectDefaultHotkeysOptions, injectHeldKeyCodes, injectHeldKeys, injectHotkey, injectHotkeyHint, injectHotkeyRecorder, injectHotkeyRegistrations, injectHotkeySequence, injectHotkeySequenceRecorder, injectHotkeySequences, injectHotkeys, injectHotkeysContext, injectKeyHold, provideHotkeys };
12
+ export { AngularHotkeyRecorder, HOTKEYS_INJECTION_TOKEN, HotkeysContextValue, HotkeysProviderOptions, InjectAppHotkeyOptions, InjectCustomHotkeysResult, InjectHotkeyChordOptions, InjectHotkeyCommonOptions, InjectHotkeyDefinition, InjectHotkeyOptions, InjectHotkeyOptionsFor, InjectHotkeySequenceOptions, InjectHotkeyTarget, ProvideHotkeysOptions, injectAppHotkeyList, injectAppHotkeys, injectCustomHotkeys, injectDefaultHotkeysOptions, injectHeldKeyCodes, injectHeldKeys, injectHotkey, injectHotkeyHint, injectHotkeyRecorder, injectHotkeyRegistrations, injectHotkeys, injectHotkeysContext, injectKeyHold, provideHotkeys };
package/dist/index.js CHANGED
@@ -1,16 +1,14 @@
1
1
  import { HOTKEYS_INJECTION_TOKEN, injectDefaultHotkeysOptions, injectHotkeysContext, provideHotkeys } from "./hotkeys-provider.js";
2
- import { injectHotkey } from "./injectHotkey.js";
3
2
  import { injectHotkeys } from "./injectHotkeys.js";
4
- import { injectHotkeySequence } from "./injectHotkeySequence.js";
5
- import { injectHotkeySequences } from "./injectHotkeySequences.js";
3
+ import { injectHotkey } from "./injectHotkey.js";
6
4
  import { injectHeldKeys } from "./injectHeldKeys.js";
7
5
  import { injectHeldKeyCodes } from "./injectHeldKeyCodes.js";
8
6
  import { injectKeyHold } from "./injectKeyHold.js";
9
7
  import { injectHotkeyRecorder } from "./injectHotkeyRecorder.js";
10
8
  import { injectHotkeyRegistrations } from "./injectHotkeyRegistrations.js";
11
- import { injectHotkeySequenceRecorder } from "./injectHotkeySequenceRecorder.js";
12
9
  import { injectHotkeyHint } from "./injectHotkeyHint.js";
10
+ import { injectAppHotkeyList, injectAppHotkeys, injectCustomHotkeys } from "./injectAppHotkeys.js";
13
11
 
14
12
  export * from "@tanstack/hotkeys"
15
13
 
16
- export { HOTKEYS_INJECTION_TOKEN, injectDefaultHotkeysOptions, injectHeldKeyCodes, injectHeldKeys, injectHotkey, injectHotkeyHint, injectHotkeyRecorder, injectHotkeyRegistrations, injectHotkeySequence, injectHotkeySequenceRecorder, injectHotkeySequences, injectHotkeys, injectHotkeysContext, injectKeyHold, provideHotkeys };
14
+ export { HOTKEYS_INJECTION_TOKEN, injectAppHotkeyList, injectAppHotkeys, injectCustomHotkeys, injectDefaultHotkeysOptions, injectHeldKeyCodes, injectHeldKeys, injectHotkey, injectHotkeyHint, injectHotkeyRecorder, injectHotkeyRegistrations, injectHotkeys, injectHotkeysContext, injectKeyHold, provideHotkeys };
@@ -0,0 +1,45 @@
1
+ import { AppHotkey, AppHotkeyListItem, CustomHotkeys, Hotkey, RegisteredAppHotkeys } from "@tanstack/hotkeys";
2
+ import { Signal } from "@angular/core";
3
+ //#region src/injectAppHotkeys.d.ts
4
+ /**
5
+ * Returns a signal of the effective app hotkeys from `provideHotkeys`, typed
6
+ * by `Register`: each declaration with the user's custom hotkey applied.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * hotkeys = injectAppHotkeys()
11
+ * constructor() {
12
+ * injectHotkey(() => this.hotkeys()['doc.save'], () => this.save())
13
+ * }
14
+ * ```
15
+ */
16
+ export declare function injectAppHotkeys(): Signal<RegisteredAppHotkeys>;
17
+ /**
18
+ * Lists every app hotkey with its live status, in declaration order, for
19
+ * cheat sheets and settings screens. App hotkeys whose components have not
20
+ * mounted are `'inactive'`; those without a hotkey are `'unbound'`.
21
+ */
22
+ export declare function injectAppHotkeyList(): Signal<Array<AppHotkeyListItem<RegisteredAppHotkeys[keyof RegisteredAppHotkeys]>>>;
23
+ /** Custom hotkey state and actions returned by `injectCustomHotkeys`. */
24
+ export interface InjectCustomHotkeysResult {
25
+ /** The user's hotkeys that differ from the defaults. */
26
+ readonly customHotkeys: Signal<CustomHotkeys>;
27
+ /**
28
+ * Sets the user's hotkey for an app hotkey. `null` unbinds it.
29
+ * Returns conflicting app hotkeys; when any exist, nothing changes.
30
+ */
31
+ readonly setCustomHotkey: (id: keyof RegisteredAppHotkeys & string, hotkey: Hotkey | null) => Array<AppHotkey>;
32
+ /** Restores the default hotkey for an app hotkey. */
33
+ readonly resetCustomHotkey: (id: keyof RegisteredAppHotkeys & string) => void;
34
+ /** Restores every default hotkey. */
35
+ readonly resetCustomHotkeys: () => void;
36
+ /** Other app hotkeys that would collide if `id` used `hotkey`. */
37
+ readonly findConflicts: (id: keyof RegisteredAppHotkeys & string, hotkey: Hotkey) => Array<AppHotkey>;
38
+ }
39
+ /**
40
+ * Reads and changes the user's custom hotkeys. Changes apply to every
41
+ * registered app hotkey immediately and are passed to `onCustomHotkeysChange`
42
+ * from `provideHotkeys`, ready to save.
43
+ */
44
+ export declare function injectCustomHotkeys(): InjectCustomHotkeysResult;
45
+ //#endregion
@@ -0,0 +1,66 @@
1
+ import { injectHotkeysContext } from "./hotkeys-provider.js";
2
+ import { injectHotkeyRegistrations } from "./injectHotkeyRegistrations.js";
3
+ import { computed } from "@angular/core";
4
+
5
+ //#region src/injectAppHotkeys.ts
6
+ function injectRequiredStore(name) {
7
+ const context = injectHotkeysContext();
8
+ if (!context.appHotkeysStore) throw new Error(`${name} requires provideHotkeys with appHotkeys`);
9
+ return {
10
+ store: context.appHotkeysStore,
11
+ state: context.appHotkeysState
12
+ };
13
+ }
14
+ /**
15
+ * Returns a signal of the effective app hotkeys from `provideHotkeys`, typed
16
+ * by `Register`: each declaration with the user's custom hotkey applied.
17
+ *
18
+ * @example
19
+ * ```ts
20
+ * hotkeys = injectAppHotkeys()
21
+ * constructor() {
22
+ * injectHotkey(() => this.hotkeys()['doc.save'], () => this.save())
23
+ * }
24
+ * ```
25
+ */
26
+ function injectAppHotkeys() {
27
+ const { store, state } = injectRequiredStore("injectAppHotkeys");
28
+ return computed(() => {
29
+ state();
30
+ return store.appHotkeys;
31
+ });
32
+ }
33
+ /**
34
+ * Lists every app hotkey with its live status, in declaration order, for
35
+ * cheat sheets and settings screens. App hotkeys whose components have not
36
+ * mounted are `'inactive'`; those without a hotkey are `'unbound'`.
37
+ */
38
+ function injectAppHotkeyList() {
39
+ const { store, state } = injectRequiredStore("injectAppHotkeyList");
40
+ const registrations = injectHotkeyRegistrations();
41
+ return computed(() => {
42
+ state();
43
+ return store.getList(registrations());
44
+ });
45
+ }
46
+ /**
47
+ * Reads and changes the user's custom hotkeys. Changes apply to every
48
+ * registered app hotkey immediately and are passed to `onCustomHotkeysChange`
49
+ * from `provideHotkeys`, ready to save.
50
+ */
51
+ function injectCustomHotkeys() {
52
+ const { store, state } = injectRequiredStore("injectCustomHotkeys");
53
+ return {
54
+ customHotkeys: computed(() => {
55
+ state();
56
+ return store.customHotkeys;
57
+ }),
58
+ setCustomHotkey: (id, hotkey) => store.setCustomHotkey(id, hotkey),
59
+ resetCustomHotkey: (id) => store.resetCustomHotkey(id),
60
+ resetCustomHotkeys: () => store.resetCustomHotkeys(),
61
+ findConflicts: (id, hotkey) => store.findConflicts(id, hotkey)
62
+ };
63
+ }
64
+
65
+ //#endregion
66
+ export { injectAppHotkeyList, injectAppHotkeys, injectCustomHotkeys };
@@ -1,72 +1,47 @@
1
- import { HotkeyCallback, HotkeyOptions, RegisterableHotkey } from "@tanstack/hotkeys";
1
+ import { AppHotkey, HotkeyCallback, RegisterableHotkey, Target } from "@tanstack/hotkeys";
2
+ import { AdapterAppHotkeyOptions, AdapterChordOptions, AdapterCommonOptions, AdapterHotkeyOptions, AdapterOptionsFor, AdapterSequenceOptions } from "@tanstack/hotkeys/adapter";
2
3
  //#region src/injectHotkey.d.ts
3
- export interface InjectHotkeyOptions extends Omit<HotkeyOptions, 'target'> {
4
- /**
5
- * The DOM element to attach the event listener to.
6
- * Can be a direct DOM element, an accessor (for reactive targets that become
7
- * available after mount), or null. Defaults to document.
8
- * When using scoped targets, pass an accessor: () => ({ target: elementSignal() })
9
- * so the hotkey waits for the element to be attached before registering.
10
- */
11
- target?: HTMLElement | Document | Window | null;
12
- }
13
4
  /**
14
- * Angular inject-based API for registering a keyboard hotkey.
15
- *
16
- * Uses the singleton HotkeyManager for efficient event handling.
17
- * The callback receives both the keyboard event and a context object
18
- * containing the hotkey string and parsed hotkey.
19
- *
20
- * Call in an injection context (e.g. constructor or field initializer).
21
- * Uses effect() to track reactive dependencies and update registration
22
- * when options or the callback change.
23
- * `enabled: false` keeps the registration (visible in devtools) and only suppresses firing; the same
24
- * handle is updated instead of unregistering and re-registering when identity is unchanged.
25
- *
26
- * @param hotkey - The hotkey string (e.g. 'Mod+S', 'Escape') or getter function
27
- * @param callback - The function to call when the hotkey is pressed
28
- * @param options - Options for the hotkey behavior, or getter for reactive options
29
- *
30
- * @example
31
- * ```ts
32
- * @Component({ ... })
33
- * export class SaveButtonComponent {
34
- * private readonly saveCount = signal(0)
35
- *
36
- * constructor() {
37
- * injectHotkey('Mod+S', (event, { hotkey }) => {
38
- * event.preventDefault()
39
- * this.saveCount.update(c => c + 1)
40
- * })
41
- * }
42
- * }
43
- * ```
44
- *
45
- * @example
46
- * ```ts
47
- * @Component({ ... })
48
- * export class ModalComponent {
49
- * isOpen = signal(true)
5
+ * The element to listen on. A `null` target skips registration, so pass a
6
+ * getter such as `() => ({ target: this.element() })` for elements that
7
+ * attach after the view renders.
8
+ */
9
+ export type InjectHotkeyTarget = Target | null;
10
+ /** Options shared by chords and sequences. */
11
+ export type InjectHotkeyOptions = AdapterHotkeyOptions<InjectHotkeyTarget>;
12
+ /** Options for a chord such as `'Mod+S'`. */
13
+ export type InjectHotkeyChordOptions = AdapterChordOptions<InjectHotkeyTarget>;
14
+ /** Options for a sequence such as `['G', 'G']`. */
15
+ export type InjectHotkeySequenceOptions = AdapterSequenceOptions<InjectHotkeyTarget>;
16
+ /** Options applied to every hotkey, such as provider defaults. Each applies to the kinds that support it. */
17
+ export type InjectHotkeyCommonOptions = AdapterCommonOptions<InjectHotkeyTarget>;
18
+ /** The options type for a hotkey: chord options for chords, sequence options for sequences. */
19
+ export type InjectHotkeyOptionsFor<T extends RegisterableHotkey> = AdapterOptionsFor<T, InjectHotkeyTarget>;
20
+ /** Call-site options for an app hotkey, typed by its declared default kind. */
21
+ export type InjectAppHotkeyOptions<T extends AppHotkey = AppHotkey> = AdapterAppHotkeyOptions<T, InjectHotkeyTarget>;
22
+ type MaybeGetter<T> = T | (() => T);
23
+ /**
24
+ * Angular inject-based API for registering a hotkey: a chord such as
25
+ * `'Mod+S'`, a sequence such as `['G', 'G']`, or an app hotkey from
26
+ * `defineAppHotkeys`. The hotkey and options may be getters that read signals.
50
27
  *
51
- * constructor() {
52
- * injectHotkey('Escape', () => this.close(), () => ({ enabled: this.isOpen() }))
53
- * }
54
- * }
55
- * ```
28
+ * Chords accept `requireReset`; sequences accept `timeout`. A one-step
29
+ * sequence registers as its chord. An app hotkey uses its effective hotkey
30
+ * from `provideHotkeys` (including the user's custom hotkey) and its declared
31
+ * meta and options, and registers nothing while unbound.
56
32
  *
57
33
  * @example
58
34
  * ```ts
59
35
  * @Component({ ... })
60
36
  * export class EditorComponent {
61
- * private readonly editorRef = viewChild<ElementRef<HTMLDivElement>>('editorRef')
62
- *
63
37
  * constructor() {
64
- * injectHotkey('Mod+B', () => this.toggleBold(), () => ({
65
- * target: this.editorRef()?.nativeElement ?? null,
66
- * }))
38
+ * injectHotkey('Mod+S', () => this.save())
39
+ * injectHotkey(['G', 'G'], () => this.scrollToTop(), { timeout: 500 })
40
+ * injectHotkey('Escape', () => this.close(), () => ({ enabled: this.isOpen() }))
67
41
  * }
68
42
  * }
69
43
  * ```
70
44
  */
71
- export declare function injectHotkey(hotkey: RegisterableHotkey | (() => RegisterableHotkey), callback: HotkeyCallback, options?: InjectHotkeyOptions | (() => InjectHotkeyOptions)): void;
45
+ export declare function injectHotkey<T extends AppHotkey>(hotkey: MaybeGetter<T>, callback: HotkeyCallback, options?: MaybeGetter<InjectAppHotkeyOptions<T>>): void;
46
+ export declare function injectHotkey<T extends RegisterableHotkey>(hotkey: MaybeGetter<T>, callback: HotkeyCallback, options?: MaybeGetter<InjectHotkeyOptionsFor<T>>): void;
72
47
  //#endregion
@@ -1,121 +1,12 @@
1
- import { injectDefaultHotkeysOptions } from "./hotkeys-provider.js";
2
- import { detectPlatform, getHotkeyManager, normalizeRegisterableHotkey } from "@tanstack/hotkeys";
3
- import { DestroyRef, effect, inject } from "@angular/core";
1
+ import { injectHotkeys } from "./injectHotkeys.js";
4
2
 
5
3
  //#region src/injectHotkey.ts
6
- /**
7
- * Angular inject-based API for registering a keyboard hotkey.
8
- *
9
- * Uses the singleton HotkeyManager for efficient event handling.
10
- * The callback receives both the keyboard event and a context object
11
- * containing the hotkey string and parsed hotkey.
12
- *
13
- * Call in an injection context (e.g. constructor or field initializer).
14
- * Uses effect() to track reactive dependencies and update registration
15
- * when options or the callback change.
16
- * `enabled: false` keeps the registration (visible in devtools) and only suppresses firing; the same
17
- * handle is updated instead of unregistering and re-registering when identity is unchanged.
18
- *
19
- * @param hotkey - The hotkey string (e.g. 'Mod+S', 'Escape') or getter function
20
- * @param callback - The function to call when the hotkey is pressed
21
- * @param options - Options for the hotkey behavior, or getter for reactive options
22
- *
23
- * @example
24
- * ```ts
25
- * @Component({ ... })
26
- * export class SaveButtonComponent {
27
- * private readonly saveCount = signal(0)
28
- *
29
- * constructor() {
30
- * injectHotkey('Mod+S', (event, { hotkey }) => {
31
- * event.preventDefault()
32
- * this.saveCount.update(c => c + 1)
33
- * })
34
- * }
35
- * }
36
- * ```
37
- *
38
- * @example
39
- * ```ts
40
- * @Component({ ... })
41
- * export class ModalComponent {
42
- * isOpen = signal(true)
43
- *
44
- * constructor() {
45
- * injectHotkey('Escape', () => this.close(), () => ({ enabled: this.isOpen() }))
46
- * }
47
- * }
48
- * ```
49
- *
50
- * @example
51
- * ```ts
52
- * @Component({ ... })
53
- * export class EditorComponent {
54
- * private readonly editorRef = viewChild<ElementRef<HTMLDivElement>>('editorRef')
55
- *
56
- * constructor() {
57
- * injectHotkey('Mod+B', () => this.toggleBold(), () => ({
58
- * target: this.editorRef()?.nativeElement ?? null,
59
- * }))
60
- * }
61
- * }
62
- * ```
63
- */
64
4
  function injectHotkey(hotkey, callback, options = {}) {
65
- const defaultOptions = injectDefaultHotkeysOptions();
66
- const manager = getHotkeyManager();
67
- const destroyRef = inject(DestroyRef);
68
- let registration = null;
69
- let lastHotkeyString = null;
70
- let lastTarget = null;
71
- destroyRef.onDestroy(() => {
72
- if (registration?.isActive) {
73
- registration.unregister();
74
- registration = null;
75
- }
76
- lastHotkeyString = null;
77
- lastTarget = null;
78
- });
79
- effect(() => {
80
- const resolvedHotkey = typeof hotkey === "function" ? hotkey() : hotkey;
81
- const resolvedOptions = typeof options === "function" ? options() : options;
82
- const mergedOptions = {
83
- ...defaultOptions.hotkey,
84
- ...resolvedOptions
85
- };
86
- const platform = mergedOptions.platform ?? detectPlatform();
87
- const hotkeyString = normalizeRegisterableHotkey(resolvedHotkey, platform);
88
- const resolvedTarget = "target" in mergedOptions ? mergedOptions.target ?? null : typeof document !== "undefined" ? document : null;
89
- if (!resolvedTarget) {
90
- if (registration?.isActive) {
91
- registration.unregister();
92
- registration = null;
93
- }
94
- lastHotkeyString = null;
95
- lastTarget = null;
96
- return;
97
- }
98
- const { target: _target, ...optionsWithoutTarget } = mergedOptions;
99
- if (registration?.isActive && lastHotkeyString === hotkeyString && lastTarget === resolvedTarget) {
100
- registration.callback = callback;
101
- registration.setOptions(optionsWithoutTarget);
102
- return;
103
- }
104
- if (registration?.isActive) {
105
- registration.unregister();
106
- registration = null;
107
- }
108
- registration = manager.register(hotkeyString, callback, {
109
- ...optionsWithoutTarget,
110
- target: resolvedTarget
111
- });
112
- if (registration.isActive) {
113
- registration.callback = callback;
114
- registration.setOptions(optionsWithoutTarget);
115
- }
116
- lastHotkeyString = hotkeyString;
117
- lastTarget = resolvedTarget;
118
- });
5
+ injectHotkeys(() => [{
6
+ hotkey: typeof hotkey === "function" ? hotkey() : hotkey,
7
+ callback,
8
+ options: typeof options === "function" ? options() : options
9
+ }]);
119
10
  }
120
11
 
121
12
  //#endregion
@@ -1,12 +1,12 @@
1
- import { HeldModifierOptions, RegisterableHotkey } from "@tanstack/hotkeys";
1
+ import { AppHotkey, HeldModifierOptions, RegisterableHotkey } from "@tanstack/hotkeys";
2
2
  import { Signal } from "@angular/core";
3
3
  //#region src/injectHotkeyHint.d.ts
4
4
  /**
5
- * Returns a computed signal indicating whether held modifiers reveal this shortcut.
6
- * Call in an injection context; pass signal getters for changing inputs.
7
- * Uses the nonempty-subset/AltGr rules of `matchesHeldModifiers`; `exact` requires
8
- * every modifier. Combine with the action's enabled state before showing a badge.
9
- * This helper does not register a shortcut or check whether its target is focused.
5
+ * Returns a signal indicating whether held modifiers reveal this shortcut.
6
+ * Pass getters to track changing hotkeys and options. A sequence reveals its
7
+ * hint from its first step, and an app hotkey uses its effective hotkey
8
+ * (false while unbound). This helper does not register a shortcut or check
9
+ * whether its target is focused.
10
10
  */
11
- export declare function injectHotkeyHint(hotkey: RegisterableHotkey | (() => RegisterableHotkey), options?: HeldModifierOptions | (() => HeldModifierOptions)): Signal<boolean>;
11
+ export declare function injectHotkeyHint(hotkey: RegisterableHotkey | AppHotkey | (() => RegisterableHotkey | AppHotkey), options?: HeldModifierOptions | (() => HeldModifierOptions)): Signal<boolean>;
12
12
  //#endregion
@@ -1,18 +1,25 @@
1
- import { getKeyStateTracker, matchesHeldModifiers } from "@tanstack/hotkeys";
1
+ import { injectHotkeysContext } from "./hotkeys-provider.js";
2
+ import { getKeyStateTracker, isAppHotkey, matchesHeldModifiers, resolveAppHotkey } from "@tanstack/hotkeys";
2
3
  import { computed } from "@angular/core";
3
4
  import { injectSelector } from "@tanstack/angular-store";
4
5
 
5
6
  //#region src/injectHotkeyHint.ts
6
7
  /**
7
- * Returns a computed signal indicating whether held modifiers reveal this shortcut.
8
- * Call in an injection context; pass signal getters for changing inputs.
9
- * Uses the nonempty-subset/AltGr rules of `matchesHeldModifiers`; `exact` requires
10
- * every modifier. Combine with the action's enabled state before showing a badge.
11
- * This helper does not register a shortcut or check whether its target is focused.
8
+ * Returns a signal indicating whether held modifiers reveal this shortcut.
9
+ * Pass getters to track changing hotkeys and options. A sequence reveals its
10
+ * hint from its first step, and an app hotkey uses its effective hotkey
11
+ * (false while unbound). This helper does not register a shortcut or check
12
+ * whether its target is focused.
12
13
  */
13
14
  function injectHotkeyHint(hotkey, options = {}) {
15
+ const context = injectHotkeysContext();
14
16
  const held = injectSelector(getKeyStateTracker().store, (state) => state.heldKeys);
15
- return computed(() => matchesHeldModifiers(typeof hotkey === "function" ? hotkey() : hotkey, held(), typeof options === "function" ? options() : options));
17
+ return computed(() => {
18
+ context.appHotkeysState();
19
+ const value = typeof hotkey === "function" ? hotkey() : hotkey;
20
+ const resolved = isAppHotkey(value) ? resolveAppHotkey(value, context.appHotkeysStore).hotkey : value;
21
+ return resolved !== null && matchesHeldModifiers(resolved, held(), typeof options === "function" ? options() : options);
22
+ });
16
23
  }
17
24
 
18
25
  //#endregion
@@ -1,23 +1,29 @@
1
- import { Hotkey, HotkeyRecorderOptions } from "@tanstack/hotkeys";
1
+ import { Hotkey, HotkeyChord, HotkeyChordRecorderOptions, HotkeyRecorderOptions, HotkeySequence, HotkeySequenceRecorderOptions } from "@tanstack/hotkeys";
2
2
  //#region src/injectHotkeyRecorder.d.ts
3
- export interface AngularHotkeyRecorder {
3
+ export interface AngularHotkeyRecorder<THotkey extends Hotkey = Hotkey> {
4
4
  /** Whether recording is currently active */
5
5
  readonly isRecording: () => boolean;
6
- /** The currently recorded hotkey (for live preview) */
7
- readonly recordedHotkey: () => Hotkey | null;
6
+ /** Chords recorded so far in a multi-step recording (for live preview) */
7
+ readonly steps: () => HotkeySequence;
8
+ /** The last committed hotkey */
9
+ readonly recordedHotkey: () => THotkey | null;
8
10
  /** Start recording a new hotkey */
9
11
  readonly startRecording: () => void;
10
- /** Stop recording (same as cancel) */
12
+ /** Stop recording without calling onCancel */
11
13
  readonly stopRecording: () => void;
12
- /** Cancel recording without saving */
14
+ /** Cancel recording and call onCancel */
13
15
  readonly cancelRecording: () => void;
16
+ /** Commit the recorded steps now (multi-step recordings) */
17
+ readonly commitRecording: () => void;
14
18
  }
15
19
  /**
16
20
  * Angular inject-based API for recording keyboard shortcuts.
17
21
  *
18
- * Thin wrapper around the framework-agnostic HotkeyRecorder class: captures
19
- * keyboard events, converts them to hotkey strings, and handles Escape to
20
- * cancel or Backspace/Delete to clear.
22
+ * With the default `maxSteps: 1`, the first valid chord is recorded. With a
23
+ * larger `maxSteps`, each chord adds a step; the recording commits on Enter
24
+ * (unless `commitKeys: 'none'`), after `idleTimeoutMs`, or at `maxSteps`, and
25
+ * `onRecord` receives a chord (one step) or a sequence. Escape cancels;
26
+ * Backspace/Delete removes the last step or clears.
21
27
  *
22
28
  * @param options - Configuration options for the recorder (or getter)
23
29
  * @returns Object with recording state signals and control functions
@@ -41,5 +47,9 @@ export interface AngularHotkeyRecorder {
41
47
  * }
42
48
  * ```
43
49
  */
44
- export declare function injectHotkeyRecorder(options: HotkeyRecorderOptions | (() => HotkeyRecorderOptions)): AngularHotkeyRecorder;
50
+ export declare function injectHotkeyRecorder(options: HotkeyChordRecorderOptions): AngularHotkeyRecorder<HotkeyChord>;
51
+ export declare function injectHotkeyRecorder(options: HotkeySequenceRecorderOptions): AngularHotkeyRecorder;
52
+ export declare function injectHotkeyRecorder(options: () => HotkeySequenceRecorderOptions): AngularHotkeyRecorder;
53
+ export declare function injectHotkeyRecorder(options: () => HotkeyChordRecorderOptions): AngularHotkeyRecorder<HotkeyChord>;
54
+ export declare function injectHotkeyRecorder(options: () => HotkeyRecorderOptions): AngularHotkeyRecorder;
45
55
  //#endregion