{"version":3,"file":"hotkey-sequence-recorder.cjs","names":["Store","#options","#platform","detectPlatform","#idleTimer","#clearIdleTimer","#keydownHandler","isInputElement","#finishWithoutCommitCallback","#patchSteps","#scheduleIdleTimer","hotkeyChordFromKeydown","#addListener","#removeListener"],"sources":["../src/hotkey-sequence-recorder.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { detectPlatform } from './constants'\nimport { isInputElement } from './manager.utils'\nimport { hotkeyChordFromKeydown } from './recorder-chord'\nimport type { HotkeySequence } from './sequence-manager'\n\n/**\n * How the user can commit a recorded sequence from the keyboard.\n * - `'enter'`: plain Enter (no modifiers) commits when at least one step exists.\n * - `'none'`: only {@link HotkeySequenceRecorder.commit} finishes recording (or idle timeout if set).\n */\nexport type HotkeySequenceRecorderCommitKeys = 'enter' | 'none'\n\n/**\n * State interface for the HotkeySequenceRecorder.\n */\nexport interface HotkeySequenceRecorderState {\n  /** Whether recording is currently active */\n  isRecording: boolean\n  /** Chords captured so far in the current recording session */\n  steps: HotkeySequence\n  /** The last successfully committed sequence, or null if none / after starting a new session */\n  recordedSequence: HotkeySequence | null\n}\n\n/**\n * Options for configuring a HotkeySequenceRecorder instance.\n */\nexport interface HotkeySequenceRecorderOptions {\n  /** Callback when a sequence is successfully recorded (including empty array when cleared) */\n  onRecord: (sequence: HotkeySequence) => void\n  /** Optional callback when recording is cancelled (Escape pressed) */\n  onCancel?: () => void\n  /** Optional callback when the sequence is cleared (Backspace/Delete with no steps) */\n  onClear?: () => void\n  /**\n   * Whether plain Enter commits the current steps. Ignored when {@link commitKeys} is `'none'`.\n   * @default true\n   */\n  commitOnEnter?: boolean\n  /**\n   * Keyboard commit mode. When `'none'`, use {@link HotkeySequenceRecorder.commit} (and optional idle timeout).\n   * @default 'enter'\n   */\n  commitKeys?: HotkeySequenceRecorderCommitKeys\n  /**\n   * Milliseconds of inactivity after the **last completed chord** before auto-committing.\n   * The timer does not run while waiting for the first chord (`steps.length === 0`).\n   */\n  idleTimeoutMs?: number\n  /**\n   * Whether to ignore keyboard events from input-like elements (text inputs,\n   * textarea, select, contenteditable). When true, typing in inputs passes\n   * through normally instead of being captured as a sequence recording.\n   * Escape always works regardless of this setting.\n   * @default true\n   */\n  ignoreInputs?: boolean\n}\n\nconst defaultHotkeySequenceRecorderOptions: Pick<\n  HotkeySequenceRecorderOptions,\n  'commitOnEnter' | 'commitKeys'\n> = {\n  commitOnEnter: true,\n  commitKeys: 'enter',\n}\n\nfunction resolvedCommitOnEnter(\n  options: HotkeySequenceRecorderOptions,\n): boolean {\n  if (options.commitKeys === 'none') {\n    return false\n  }\n  return options.commitOnEnter !== false\n}\n\n/**\n * Framework-agnostic class for recording multi-chord sequences (Vim-style shortcuts).\n *\n * Each step is captured like a single hotkey chord. Press **Enter** (no modifiers) to commit\n * when {@link HotkeySequenceRecorderOptions.commitKeys} is `'enter'` (default), **Escape** to cancel,\n * **Backspace/Delete** to remove the last step or clear when empty.\n */\nexport class HotkeySequenceRecorder {\n  readonly store: Store<HotkeySequenceRecorderState> =\n    new Store<HotkeySequenceRecorderState>({\n      isRecording: false,\n      steps: [],\n      recordedSequence: null,\n    })\n\n  #keydownHandler: ((event: KeyboardEvent) => void) | null = null\n  #options: HotkeySequenceRecorderOptions\n  #platform: 'mac' | 'windows' | 'linux'\n  #idleTimer: ReturnType<typeof setTimeout> | null = null\n\n  constructor(options: HotkeySequenceRecorderOptions) {\n    this.#options = {\n      ...defaultHotkeySequenceRecorderOptions,\n      ...options,\n    }\n    this.#platform = detectPlatform()\n  }\n\n  setOptions(options: Partial<HotkeySequenceRecorderOptions>): void {\n    this.#options = {\n      ...defaultHotkeySequenceRecorderOptions,\n      ...this.#options,\n      ...options,\n    }\n  }\n\n  #clearIdleTimer(): void {\n    if (this.#idleTimer !== null) {\n      clearTimeout(this.#idleTimer)\n      this.#idleTimer = null\n    }\n  }\n\n  #scheduleIdleTimer(): void {\n    this.#clearIdleTimer()\n    const ms = this.#options.idleTimeoutMs\n    if (ms === undefined || ms <= 0) {\n      return\n    }\n    const steps = this.store.state.steps\n    if (steps.length === 0) {\n      return\n    }\n    this.#idleTimer = setTimeout(() => {\n      this.#idleTimer = null\n      if (!this.#keydownHandler) {\n        return\n      }\n      if (this.store.state.steps.length >= 1) {\n        this.commit()\n      }\n    }, ms)\n  }\n\n  #patchSteps(updater: (prev: HotkeySequence) => HotkeySequence): void {\n    this.store.setState((s) => ({\n      ...s,\n      steps: updater(s.steps),\n    }))\n  }\n\n  start(): void {\n    if (this.#keydownHandler) {\n      return\n    }\n\n    this.#clearIdleTimer()\n    this.store.setState(() => ({\n      isRecording: true,\n      steps: [],\n      recordedSequence: null,\n    }))\n\n    const handler = (event: KeyboardEvent) => {\n      if (!this.#keydownHandler) {\n        return\n      }\n\n      // If ignoreInputs is enabled (default) and focus is in an input element,\n      // let the event pass through so the user can type normally.\n      // Escape is the exception — it should always cancel recording.\n      if (this.#options.ignoreInputs !== false) {\n        const activeEl =\n          typeof document !== 'undefined' ? document.activeElement : null\n        if (isInputElement(activeEl) && event.key !== 'Escape') {\n          return\n        }\n      }\n\n      event.preventDefault()\n      event.stopPropagation()\n\n      if (event.key === 'Escape') {\n        this.cancel()\n        return\n      }\n\n      if (event.key === 'Backspace' || event.key === 'Delete') {\n        if (\n          !event.ctrlKey &&\n          !event.shiftKey &&\n          !event.altKey &&\n          !event.metaKey\n        ) {\n          const steps = this.store.state.steps\n          if (steps.length === 0) {\n            this.#options.onClear?.()\n            this.#options.onRecord([])\n            this.#finishWithoutCommitCallback()\n            return\n          }\n          this.#patchSteps((prev) => prev.slice(0, -1))\n          const next = this.store.state.steps\n          if (next.length === 0) {\n            this.#clearIdleTimer()\n          } else {\n            this.#scheduleIdleTimer()\n          }\n          return\n        }\n      }\n\n      const enterCommits =\n        resolvedCommitOnEnter(this.#options) &&\n        event.key === 'Enter' &&\n        !event.ctrlKey &&\n        !event.shiftKey &&\n        !event.altKey &&\n        !event.metaKey\n\n      if (enterCommits && this.store.state.steps.length >= 1) {\n        this.commit()\n        return\n      }\n\n      if (enterCommits && this.store.state.steps.length === 0) {\n        return\n      }\n\n      const finalHotkey = hotkeyChordFromKeydown(event, this.#platform)\n      if (finalHotkey === null) {\n        return\n      }\n\n      this.#patchSteps((prev) => [...prev, finalHotkey])\n      this.#scheduleIdleTimer()\n    }\n\n    this.#keydownHandler = handler\n    this.#addListener(handler)\n  }\n\n  /**\n   * Commit the current steps as a sequence. No-op if fewer than one step.\n   */\n  commit(): void {\n    const steps = this.store.state.steps\n    if (steps.length < 1) {\n      return\n    }\n\n    const sequence = [...steps]\n\n    if (this.#keydownHandler) {\n      this.#removeListener(this.#keydownHandler)\n      this.#keydownHandler = null\n    }\n    this.#clearIdleTimer()\n\n    this.store.setState(() => ({\n      isRecording: false,\n      steps: [],\n      recordedSequence: sequence,\n    }))\n\n    this.#options.onRecord(sequence)\n  }\n\n  #finishWithoutCommitCallback(): void {\n    if (this.#keydownHandler) {\n      this.#removeListener(this.#keydownHandler)\n      this.#keydownHandler = null\n    }\n    this.#clearIdleTimer()\n    this.store.setState(() => ({\n      isRecording: false,\n      steps: [],\n      recordedSequence: null,\n    }))\n  }\n\n  stop(): void {\n    if (this.#keydownHandler) {\n      this.#removeListener(this.#keydownHandler)\n      this.#keydownHandler = null\n    }\n    this.#clearIdleTimer()\n    this.store.setState(() => ({\n      isRecording: false,\n      steps: [],\n      recordedSequence: null,\n    }))\n  }\n\n  cancel(): void {\n    if (this.#keydownHandler) {\n      this.#removeListener(this.#keydownHandler)\n      this.#keydownHandler = null\n    }\n    this.#clearIdleTimer()\n    this.store.setState(() => ({\n      isRecording: false,\n      steps: [],\n      recordedSequence: null,\n    }))\n    this.#options.onCancel?.()\n  }\n\n  #addListener(handler: (event: KeyboardEvent) => void): void {\n    if (typeof document === 'undefined') {\n      return\n    }\n    document.addEventListener('keydown', handler, true)\n  }\n\n  #removeListener(handler: (event: KeyboardEvent) => void): void {\n    if (typeof document === 'undefined') {\n      return\n    }\n    document.removeEventListener('keydown', handler, true)\n  }\n\n  destroy(): void {\n    this.stop()\n    this.store.setState(() => ({\n      isRecording: false,\n      steps: [],\n      recordedSequence: null,\n    }))\n  }\n}\n"],"mappings":"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"}