Solid Example: CreateHotkeySequences

/* @refresh reload */
import { render } from 'solid-js/web'
import { For, Show, createSignal } from 'solid-js'
import {
  HotkeysProvider,
  createHotkey,
  createHotkeyRegistrations,
  createHotkeySequences,
  formatForDisplay,
} from '@tanstack/solid-hotkeys'
import { hotkeysDevtoolsPlugin } from '@tanstack/solid-hotkeys-devtools'
import { TanStackDevtools } from '@tanstack/solid-devtools'
import './index.css'

function App() {
  const [lastSequence, setLastSequence] = createSignal<string | null>(null)
  const [history, setHistory] = createSignal<Array<string>>([])
  const [helloSequenceEnabled, setHelloSequenceEnabled] = createSignal(true)
  const addToHistory = (action: string) => {
    setLastSequence(action)
    setHistory((h) => [...h.slice(-9), action])
  }

  createHotkeySequences([
    {
      sequence: ['G', 'G'],
      callback: () => addToHistory('gg \u2192 Go to top'),
      options: {
        meta: {
          name: 'Go to top',
          description: 'Scroll to the beginning of the document',
        },
      },
    },
    {
      sequence: ['Shift+G'],
      callback: () => addToHistory('G \u2192 Go to bottom'),
      options: {
        meta: {
          name: 'Go to bottom',
          description: 'Scroll to the end of the document',
        },
      },
    },
    {
      sequence: ['D', 'D'],
      callback: () => addToHistory('dd \u2192 Delete line'),
      options: {
        meta: {
          name: 'Delete line',
          description: 'Delete the current line',
        },
      },
    },
    {
      sequence: ['Y', 'Y'],
      callback: () => addToHistory('yy \u2192 Yank (copy) line'),
      options: {
        meta: {
          name: 'Yank line',
          description: 'Copy the current line to clipboard',
        },
      },
    },
    {
      sequence: ['D', 'W'],
      callback: () => addToHistory('dw \u2192 Delete word'),
      options: {
        meta: {
          name: 'Delete word',
          description: 'Delete from cursor to end of word',
        },
      },
    },
    {
      sequence: ['C', 'I', 'W'],
      callback: () => addToHistory('ciw \u2192 Change inner word'),
      options: {
        meta: {
          name: 'Change inner word',
          description: 'Delete word under cursor and enter insert mode',
        },
      },
    },
    {
      sequence: ['ArrowUp', 'ArrowUp', 'ArrowDown', 'ArrowDown'],
      callback: () =>
        addToHistory('\u2191\u2191\u2193\u2193 \u2192 Konami code (partial)'),
      options: {
        timeout: 1500,
        meta: {
          name: 'Konami code',
          description: 'Partial Konami code using arrow keys',
        },
      },
    },
    {
      sequence: ['ArrowLeft', 'ArrowRight', 'ArrowLeft', 'ArrowRight'],
      callback: () =>
        addToHistory('\u2190\u2192\u2190\u2192 \u2192 Side to side!'),
      options: {
        timeout: 1500,
        meta: {
          name: 'Side to side',
          description: 'Left-right-left-right arrow pattern',
        },
      },
    },
    {
      sequence: ['H', 'E', 'L', 'L', 'O'],
      callback: () => addToHistory('hello \u2192 Hello World!'),
      options: {
        get enabled() {
          return helloSequenceEnabled()
        },
        meta: {
          name: 'Hello',
          description: 'Spell out hello to trigger',
        },
      },
    },
    {
      sequence: ['Shift+R', 'Shift+T'],
      callback: () =>
        addToHistory('\u21E7R \u21E7T \u2192 Chained Shift+letter (2 steps)'),
      options: {
        meta: {
          name: 'Chained Shift',
          description: 'Two consecutive Shift+letter chords',
        },
      },
    },
  ])

  // Clear history with Escape
  createHotkey('Escape', () => {
    setLastSequence(null)
    setHistory([])
  })

  return (
    <div class="app">
      <header>
        <h1>createHotkeySequences</h1>
        <p>
          Register many multi-key sequences in one primitive (like Vim
          commands). Keys must be pressed within the timeout window (default:
          1000ms).
        </p>
      </header>

      <main>
        <section class="demo-section">
          <h2>Vim-Style Commands</h2>
          <table class="sequence-table">
            <thead>
              <tr>
                <th>Sequence</th>
                <th>Action</th>
              </tr>
            </thead>
            <tbody>
              <tr>
                <td>
                  <kbd>g</kbd> <kbd>g</kbd>
                </td>
                <td>Go to top</td>
              </tr>
              <tr>
                <td>
                  <kbd>G</kbd> (Shift+G)
                </td>
                <td>Go to bottom</td>
              </tr>
              <tr>
                <td>
                  <kbd>d</kbd> <kbd>d</kbd>
                </td>
                <td>Delete line</td>
              </tr>
              <tr>
                <td>
                  <kbd>y</kbd> <kbd>y</kbd>
                </td>
                <td>Yank (copy) line</td>
              </tr>
              <tr>
                <td>
                  <kbd>d</kbd> <kbd>w</kbd>
                </td>
                <td>Delete word</td>
              </tr>
              <tr>
                <td>
                  <kbd>c</kbd> <kbd>i</kbd> <kbd>w</kbd>
                </td>
                <td>Change inner word</td>
              </tr>
            </tbody>
          </table>
        </section>

        <section class="demo-section">
          <h2>Fun Sequences</h2>
          <div class="fun-sequences">
            <div class="sequence-card">
              <h3>Konami Code (Partial)</h3>
              <p>
                <kbd>{'\u2191'}</kbd> <kbd>{'\u2191'}</kbd>{' '}
                <kbd>{'\u2193'}</kbd> <kbd>{'\u2193'}</kbd>
              </p>
              <span class="hint">Use arrow keys within 1.5 seconds</span>
            </div>
            <div class="sequence-card">
              <h3>Side to Side</h3>
              <p>
                <kbd>{'\u2190'}</kbd> <kbd>{'\u2192'}</kbd>{' '}
                <kbd>{'\u2190'}</kbd> <kbd>{'\u2192'}</kbd>
              </p>
              <span class="hint">Arrow keys within 1.5 seconds</span>
            </div>
            <div class="sequence-card">
              <h3>Spell It Out</h3>
              <p>
                <kbd>h</kbd> <kbd>e</kbd> <kbd>l</kbd> <kbd>l</kbd> <kbd>o</kbd>
              </p>
              <span class="hint">Type "hello" quickly</span>
              <p class="sequence-toggle-status">
                This sequence is{' '}
                <strong>
                  {helloSequenceEnabled() ? 'enabled' : 'disabled'}
                </strong>
                .
              </p>
              <button
                type="button"
                onClick={() => setHelloSequenceEnabled(!helloSequenceEnabled())}
              >
                {helloSequenceEnabled() ? 'Disable' : 'Enable'} sequence
              </button>
            </div>
          </div>
        </section>

        <Show when={lastSequence()}>
          <div class="info-box success">
            <strong>Triggered:</strong> {lastSequence()}
          </div>
        </Show>

        <section class="demo-section">
          <h2>Input handling</h2>
          <p>
            Sequences are not detected when typing in text inputs, textareas,
            selects, or contenteditable elements. Button-type inputs (
            <code>type="button"</code>, <code>submit</code>, <code>reset</code>)
            still receive sequences. Focus the input below and try <kbd>g</kbd>{' '}
            <kbd>g</kbd> or <kbd>h</kbd>
            <kbd>e</kbd>
            <kbd>l</kbd>
            <kbd>l</kbd>
            <kbd>o</kbd> — nothing will trigger. Click outside to try again.
          </p>
          <input
            type="text"
            class="demo-input"
            placeholder="Focus here \u2013 sequences won't trigger while typing..."
          />
        </section>

        <section class="demo-section">
          <h2>Chained Shift+letter sequences</h2>
          <p>
            Each step is a chord: hold <kbd>Shift</kbd> and press a letter. You
            can press <kbd>Shift</kbd> alone between steps—those modifier-only
            presses do not reset progress, so the next chord still counts.
          </p>
          <table class="sequence-table">
            <thead>
              <tr>
                <th>Sequence</th>
                <th>Action</th>
              </tr>
            </thead>
            <tbody>
              <tr>
                <td>
                  <kbd>Shift</kbd>+<kbd>r</kbd> then <kbd>Shift</kbd>+
                  <kbd>t</kbd>
                </td>
                <td>Chained Shift+letter (2 steps)</td>
              </tr>
            </tbody>
          </table>
        </section>

        <RegistrationsViewer />

        <section class="demo-section">
          <h2>Usage</h2>
          <pre class="code-block">{`import { createHotkeySequences, createHotkeyRegistrations } from '@tanstack/solid-hotkeys'

function VimEditor() {
  createHotkeySequences([
    {
      sequence: ['G', 'G'],
      callback: () => scrollToTop(),
      options: { meta: { name: 'Go to top', description: 'Scroll to top' } },
    },
    {
      sequence: ['C', 'I', 'W'],
      callback: () => changeInnerWord(),
      options: { meta: { name: 'Change inner word' } },
    },
  ])

  // Introspect all registrations
  const { sequences } = createHotkeyRegistrations()
  // sequences()[0].options.meta?.name → 'Go to top'
  // sequences()[0].triggerCount → 3
}`}</pre>
        </section>

        <Show when={history().length > 0}>
          <section class="demo-section">
            <h2>History</h2>
            <ul class="history-list">
              <For each={history()}>{(item) => <li>{item}</li>}</For>
            </ul>
            <button onClick={() => setHistory([])}>Clear History</button>
          </section>
        </Show>

        <p class="hint">
          Press <kbd>Escape</kbd> to clear history
        </p>
      </main>

      <TanStackDevtools plugins={[hotkeysDevtoolsPlugin()]} />
    </div>
  )
}

// ---------------------------------------------------------------------------
// Live registrations viewer using createHotkeyRegistrations
// ---------------------------------------------------------------------------

function RegistrationsViewer() {
  const { hotkeys, sequences } = createHotkeyRegistrations()

  return (
    <section class="demo-section">
      <h2>Live Registrations (createHotkeyRegistrations)</h2>
      <p>
        This table is rendered from <code>createHotkeyRegistrations()</code> — a
        reactive view of all registered hotkeys and sequences. Trigger counts
        update in real-time.
      </p>

      <Show when={hotkeys().length > 0}>
        <h3>Hotkeys</h3>
        <table class="registrations-table">
          <thead>
            <tr>
              <th>Hotkey</th>
              <th>Name</th>
              <th>Description</th>
              <th>Triggers</th>
            </tr>
          </thead>
          <tbody>
            <For each={hotkeys()}>
              {(reg) => (
                <tr>
                  <td>
                    <kbd>{formatForDisplay(reg.hotkey)}</kbd>
                  </td>
                  <td>{reg.options.meta?.name ?? '\u2014'}</td>
                  <td class="description-cell">
                    {reg.options.meta?.description ?? '\u2014'}
                  </td>
                  <td class="trigger-count">{reg.triggerCount}</td>
                </tr>
              )}
            </For>
          </tbody>
        </table>
      </Show>

      <h3>Sequences ({sequences().length})</h3>
      <table class="registrations-table">
        <thead>
          <tr>
            <th>Sequence</th>
            <th>Name</th>
            <th>Description</th>
            <th>Enabled</th>
            <th>Triggers</th>
          </tr>
        </thead>
        <tbody>
          <For each={sequences()}>
            {(reg) => (
              <tr>
                <td>
                  <For each={reg.sequence}>
                    {(s, i) => (
                      <>
                        {i() > 0 && ' '}
                        <kbd>{formatForDisplay(s)}</kbd>
                      </>
                    )}
                  </For>
                </td>
                <td>{reg.options.meta?.name ?? '\u2014'}</td>
                <td class="description-cell">
                  {reg.options.meta?.description ?? '\u2014'}
                </td>
                <td>
                  <span
                    class={
                      reg.options.enabled !== false ? 'status-on' : 'status-off'
                    }
                  >
                    {reg.options.enabled !== false ? 'yes' : 'no'}
                  </span>
                </td>
                <td class="trigger-count">{reg.triggerCount}</td>
              </tr>
            )}
          </For>
          <Show when={sequences().length === 0}>
            <tr>
              <td colSpan={5} class="hint">
                No sequences registered
              </td>
            </tr>
          </Show>
        </tbody>
      </table>
    </section>
  )
}

const root = document.getElementById('root')!
render(
  () => (
    <HotkeysProvider>
      <App />
    </HotkeysProvider>
  ),
  root,
)
/* @refresh reload */
import { render } from 'solid-js/web'
import { For, Show, createSignal } from 'solid-js'
import {
  HotkeysProvider,
  createHotkey,
  createHotkeyRegistrations,
  createHotkeySequences,
  formatForDisplay,
} from '@tanstack/solid-hotkeys'
import { hotkeysDevtoolsPlugin } from '@tanstack/solid-hotkeys-devtools'
import { TanStackDevtools } from '@tanstack/solid-devtools'
import './index.css'

function App() {
  const [lastSequence, setLastSequence] = createSignal<string | null>(null)
  const [history, setHistory] = createSignal<Array<string>>([])
  const [helloSequenceEnabled, setHelloSequenceEnabled] = createSignal(true)
  const addToHistory = (action: string) => {
    setLastSequence(action)
    setHistory((h) => [...h.slice(-9), action])
  }

  createHotkeySequences([
    {
      sequence: ['G', 'G'],
      callback: () => addToHistory('gg \u2192 Go to top'),
      options: {
        meta: {
          name: 'Go to top',
          description: 'Scroll to the beginning of the document',
        },
      },
    },
    {
      sequence: ['Shift+G'],
      callback: () => addToHistory('G \u2192 Go to bottom'),
      options: {
        meta: {
          name: 'Go to bottom',
          description: 'Scroll to the end of the document',
        },
      },
    },
    {
      sequence: ['D', 'D'],
      callback: () => addToHistory('dd \u2192 Delete line'),
      options: {
        meta: {
          name: 'Delete line',
          description: 'Delete the current line',
        },
      },
    },
    {
      sequence: ['Y', 'Y'],
      callback: () => addToHistory('yy \u2192 Yank (copy) line'),
      options: {
        meta: {
          name: 'Yank line',
          description: 'Copy the current line to clipboard',
        },
      },
    },
    {
      sequence: ['D', 'W'],
      callback: () => addToHistory('dw \u2192 Delete word'),
      options: {
        meta: {
          name: 'Delete word',
          description: 'Delete from cursor to end of word',
        },
      },
    },
    {
      sequence: ['C', 'I', 'W'],
      callback: () => addToHistory('ciw \u2192 Change inner word'),
      options: {
        meta: {
          name: 'Change inner word',
          description: 'Delete word under cursor and enter insert mode',
        },
      },
    },
    {
      sequence: ['ArrowUp', 'ArrowUp', 'ArrowDown', 'ArrowDown'],
      callback: () =>
        addToHistory('\u2191\u2191\u2193\u2193 \u2192 Konami code (partial)'),
      options: {
        timeout: 1500,
        meta: {
          name: 'Konami code',
          description: 'Partial Konami code using arrow keys',
        },
      },
    },
    {
      sequence: ['ArrowLeft', 'ArrowRight', 'ArrowLeft', 'ArrowRight'],
      callback: () =>
        addToHistory('\u2190\u2192\u2190\u2192 \u2192 Side to side!'),
      options: {
        timeout: 1500,
        meta: {
          name: 'Side to side',
          description: 'Left-right-left-right arrow pattern',
        },
      },
    },
    {
      sequence: ['H', 'E', 'L', 'L', 'O'],
      callback: () => addToHistory('hello \u2192 Hello World!'),
      options: {
        get enabled() {
          return helloSequenceEnabled()
        },
        meta: {
          name: 'Hello',
          description: 'Spell out hello to trigger',
        },
      },
    },
    {
      sequence: ['Shift+R', 'Shift+T'],
      callback: () =>
        addToHistory('\u21E7R \u21E7T \u2192 Chained Shift+letter (2 steps)'),
      options: {
        meta: {
          name: 'Chained Shift',
          description: 'Two consecutive Shift+letter chords',
        },
      },
    },
  ])

  // Clear history with Escape
  createHotkey('Escape', () => {
    setLastSequence(null)
    setHistory([])
  })

  return (
    <div class="app">
      <header>
        <h1>createHotkeySequences</h1>
        <p>
          Register many multi-key sequences in one primitive (like Vim
          commands). Keys must be pressed within the timeout window (default:
          1000ms).
        </p>
      </header>

      <main>
        <section class="demo-section">
          <h2>Vim-Style Commands</h2>
          <table class="sequence-table">
            <thead>
              <tr>
                <th>Sequence</th>
                <th>Action</th>
              </tr>
            </thead>
            <tbody>
              <tr>
                <td>
                  <kbd>g</kbd> <kbd>g</kbd>
                </td>
                <td>Go to top</td>
              </tr>
              <tr>
                <td>
                  <kbd>G</kbd> (Shift+G)
                </td>
                <td>Go to bottom</td>
              </tr>
              <tr>
                <td>
                  <kbd>d</kbd> <kbd>d</kbd>
                </td>
                <td>Delete line</td>
              </tr>
              <tr>
                <td>
                  <kbd>y</kbd> <kbd>y</kbd>
                </td>
                <td>Yank (copy) line</td>
              </tr>
              <tr>
                <td>
                  <kbd>d</kbd> <kbd>w</kbd>
                </td>
                <td>Delete word</td>
              </tr>
              <tr>
                <td>
                  <kbd>c</kbd> <kbd>i</kbd> <kbd>w</kbd>
                </td>
                <td>Change inner word</td>
              </tr>
            </tbody>
          </table>
        </section>

        <section class="demo-section">
          <h2>Fun Sequences</h2>
          <div class="fun-sequences">
            <div class="sequence-card">
              <h3>Konami Code (Partial)</h3>
              <p>
                <kbd>{'\u2191'}</kbd> <kbd>{'\u2191'}</kbd>{' '}
                <kbd>{'\u2193'}</kbd> <kbd>{'\u2193'}</kbd>
              </p>
              <span class="hint">Use arrow keys within 1.5 seconds</span>
            </div>
            <div class="sequence-card">
              <h3>Side to Side</h3>
              <p>
                <kbd>{'\u2190'}</kbd> <kbd>{'\u2192'}</kbd>{' '}
                <kbd>{'\u2190'}</kbd> <kbd>{'\u2192'}</kbd>
              </p>
              <span class="hint">Arrow keys within 1.5 seconds</span>
            </div>
            <div class="sequence-card">
              <h3>Spell It Out</h3>
              <p>
                <kbd>h</kbd> <kbd>e</kbd> <kbd>l</kbd> <kbd>l</kbd> <kbd>o</kbd>
              </p>
              <span class="hint">Type "hello" quickly</span>
              <p class="sequence-toggle-status">
                This sequence is{' '}
                <strong>
                  {helloSequenceEnabled() ? 'enabled' : 'disabled'}
                </strong>
                .
              </p>
              <button
                type="button"
                onClick={() => setHelloSequenceEnabled(!helloSequenceEnabled())}
              >
                {helloSequenceEnabled() ? 'Disable' : 'Enable'} sequence
              </button>
            </div>
          </div>
        </section>

        <Show when={lastSequence()}>
          <div class="info-box success">
            <strong>Triggered:</strong> {lastSequence()}
          </div>
        </Show>

        <section class="demo-section">
          <h2>Input handling</h2>
          <p>
            Sequences are not detected when typing in text inputs, textareas,
            selects, or contenteditable elements. Button-type inputs (
            <code>type="button"</code>, <code>submit</code>, <code>reset</code>)
            still receive sequences. Focus the input below and try <kbd>g</kbd>{' '}
            <kbd>g</kbd> or <kbd>h</kbd>
            <kbd>e</kbd>
            <kbd>l</kbd>
            <kbd>l</kbd>
            <kbd>o</kbd> — nothing will trigger. Click outside to try again.
          </p>
          <input
            type="text"
            class="demo-input"
            placeholder="Focus here \u2013 sequences won't trigger while typing..."
          />
        </section>

        <section class="demo-section">
          <h2>Chained Shift+letter sequences</h2>
          <p>
            Each step is a chord: hold <kbd>Shift</kbd> and press a letter. You
            can press <kbd>Shift</kbd> alone between steps—those modifier-only
            presses do not reset progress, so the next chord still counts.
          </p>
          <table class="sequence-table">
            <thead>
              <tr>
                <th>Sequence</th>
                <th>Action</th>
              </tr>
            </thead>
            <tbody>
              <tr>
                <td>
                  <kbd>Shift</kbd>+<kbd>r</kbd> then <kbd>Shift</kbd>+
                  <kbd>t</kbd>
                </td>
                <td>Chained Shift+letter (2 steps)</td>
              </tr>
            </tbody>
          </table>
        </section>

        <RegistrationsViewer />

        <section class="demo-section">
          <h2>Usage</h2>
          <pre class="code-block">{`import { createHotkeySequences, createHotkeyRegistrations } from '@tanstack/solid-hotkeys'

function VimEditor() {
  createHotkeySequences([
    {
      sequence: ['G', 'G'],
      callback: () => scrollToTop(),
      options: { meta: { name: 'Go to top', description: 'Scroll to top' } },
    },
    {
      sequence: ['C', 'I', 'W'],
      callback: () => changeInnerWord(),
      options: { meta: { name: 'Change inner word' } },
    },
  ])

  // Introspect all registrations
  const { sequences } = createHotkeyRegistrations()
  // sequences()[0].options.meta?.name → 'Go to top'
  // sequences()[0].triggerCount → 3
}`}</pre>
        </section>

        <Show when={history().length > 0}>
          <section class="demo-section">
            <h2>History</h2>
            <ul class="history-list">
              <For each={history()}>{(item) => <li>{item}</li>}</For>
            </ul>
            <button onClick={() => setHistory([])}>Clear History</button>
          </section>
        </Show>

        <p class="hint">
          Press <kbd>Escape</kbd> to clear history
        </p>
      </main>

      <TanStackDevtools plugins={[hotkeysDevtoolsPlugin()]} />
    </div>
  )
}

// ---------------------------------------------------------------------------
// Live registrations viewer using createHotkeyRegistrations
// ---------------------------------------------------------------------------

function RegistrationsViewer() {
  const { hotkeys, sequences } = createHotkeyRegistrations()

  return (
    <section class="demo-section">
      <h2>Live Registrations (createHotkeyRegistrations)</h2>
      <p>
        This table is rendered from <code>createHotkeyRegistrations()</code> — a
        reactive view of all registered hotkeys and sequences. Trigger counts
        update in real-time.
      </p>

      <Show when={hotkeys().length > 0}>
        <h3>Hotkeys</h3>
        <table class="registrations-table">
          <thead>
            <tr>
              <th>Hotkey</th>
              <th>Name</th>
              <th>Description</th>
              <th>Triggers</th>
            </tr>
          </thead>
          <tbody>
            <For each={hotkeys()}>
              {(reg) => (
                <tr>
                  <td>
                    <kbd>{formatForDisplay(reg.hotkey)}</kbd>
                  </td>
                  <td>{reg.options.meta?.name ?? '\u2014'}</td>
                  <td class="description-cell">
                    {reg.options.meta?.description ?? '\u2014'}
                  </td>
                  <td class="trigger-count">{reg.triggerCount}</td>
                </tr>
              )}
            </For>
          </tbody>
        </table>
      </Show>

      <h3>Sequences ({sequences().length})</h3>
      <table class="registrations-table">
        <thead>
          <tr>
            <th>Sequence</th>
            <th>Name</th>
            <th>Description</th>
            <th>Enabled</th>
            <th>Triggers</th>
          </tr>
        </thead>
        <tbody>
          <For each={sequences()}>
            {(reg) => (
              <tr>
                <td>
                  <For each={reg.sequence}>
                    {(s, i) => (
                      <>
                        {i() > 0 && ' '}
                        <kbd>{formatForDisplay(s)}</kbd>
                      </>
                    )}
                  </For>
                </td>
                <td>{reg.options.meta?.name ?? '\u2014'}</td>
                <td class="description-cell">
                  {reg.options.meta?.description ?? '\u2014'}
                </td>
                <td>
                  <span
                    class={
                      reg.options.enabled !== false ? 'status-on' : 'status-off'
                    }
                  >
                    {reg.options.enabled !== false ? 'yes' : 'no'}
                  </span>
                </td>
                <td class="trigger-count">{reg.triggerCount}</td>
              </tr>
            )}
          </For>
          <Show when={sequences().length === 0}>
            <tr>
              <td colSpan={5} class="hint">
                No sequences registered
              </td>
            </tr>
          </Show>
        </tbody>
      </table>
    </section>
  )
}

const root = document.getElementById('root')!
render(
  () => (
    <HotkeysProvider>
      <App />
    </HotkeysProvider>
  ),
  root,
)