diff --git a/README.md b/README.md
index 2da2e21..4609ad9 100644
--- a/README.md
+++ b/README.md
@@ -54,7 +54,8 @@ Type-safe keyboard shortcuts for the web. Template-string bindings, parsed objec
> You may know **TanStack Hotkeys** by our adapter names, too!
>
> - [**React Hotkeys**](https://tanstack.com/hotkeys/latest/docs/framework/react/react-hotkeys)
-> - Solid Hotkeys – needs a contributor!
+> - [**Preact Hotkeys**](https://tanstack.com/hotkeys/latest/docs/framework/preact/preact-hotkeys)
+> - [**Solid Hotkeys**](https://tanstack.com/hotkeys/latest/docs/framework/solid/solid-hotkeys)
> - Angular Hotkeys – needs a contributor!
> - Svelte Hotkeys – needs a contributor!
> - Vue Hotkeys – needs a contributor!
diff --git a/docs/config.json b/docs/config.json
index b2dc055..92e9bf8 100644
--- a/docs/config.json
+++ b/docs/config.json
@@ -31,6 +31,24 @@
"to": "framework/react/quick-start"
}
]
+ },
+ {
+ "label": "preact",
+ "children": [
+ {
+ "label": "Preact Hooks",
+ "to": "framework/preact/reference/index"
+ }
+ ]
+ },
+ {
+ "label": "solid",
+ "children": [
+ {
+ "label": "Solid Primitives",
+ "to": "framework/solid/reference/index"
+ }
+ ]
}
]
},
@@ -62,6 +80,56 @@
"to": "framework/react/guides/formatting-display"
}
]
+ },
+ {
+ "label": "preact",
+ "children": [
+ {
+ "label": "Hotkeys",
+ "to": "framework/preact/guides/hotkeys"
+ },
+ {
+ "label": "Sequences",
+ "to": "framework/preact/guides/sequences"
+ },
+ {
+ "label": "Hotkey Recording",
+ "to": "framework/preact/guides/hotkey-recording"
+ },
+ {
+ "label": "Key State Tracking",
+ "to": "framework/preact/guides/key-state-tracking"
+ },
+ {
+ "label": "Formatting & Display",
+ "to": "framework/preact/guides/formatting-display"
+ }
+ ]
+ },
+ {
+ "label": "solid",
+ "children": [
+ {
+ "label": "Hotkeys",
+ "to": "framework/solid/guides/hotkeys"
+ },
+ {
+ "label": "Sequences",
+ "to": "framework/solid/guides/sequences"
+ },
+ {
+ "label": "Hotkey Recording",
+ "to": "framework/solid/guides/hotkey-recording"
+ },
+ {
+ "label": "Key State Tracking",
+ "to": "framework/solid/guides/key-state-tracking"
+ },
+ {
+ "label": "Formatting & Display",
+ "to": "framework/solid/guides/formatting-display"
+ }
+ ]
}
]
},
@@ -82,6 +150,24 @@
"to": "framework/react/reference/index"
}
]
+ },
+ {
+ "label": "preact",
+ "children": [
+ {
+ "label": "Preact Hooks",
+ "to": "framework/preact/reference/index"
+ }
+ ]
+ },
+ {
+ "label": "solid",
+ "children": [
+ {
+ "label": "Solid Primitives",
+ "to": "framework/solid/reference/index"
+ }
+ ]
}
]
},
@@ -136,6 +222,32 @@
"to": "framework/react/reference/interfaces/UseHotkeyOptions"
}
]
+ },
+ {
+ "label": "preact",
+ "children": [
+ {
+ "label": "useHotkey",
+ "to": "framework/preact/reference/functions/useHotkey"
+ },
+ {
+ "label": "UseHotkeyOptions",
+ "to": "framework/preact/reference/interfaces/UseHotkeyOptions"
+ }
+ ]
+ },
+ {
+ "label": "solid",
+ "children": [
+ {
+ "label": "createHotkey",
+ "to": "framework/solid/reference/functions/createHotkey"
+ },
+ {
+ "label": "CreateHotkeyOptions",
+ "to": "framework/solid/reference/interfaces/CreateHotkeyOptions"
+ }
+ ]
}
]
},
@@ -178,6 +290,32 @@
"to": "framework/react/reference/interfaces/UseHotkeySequenceOptions"
}
]
+ },
+ {
+ "label": "preact",
+ "children": [
+ {
+ "label": "useHotkeySequence",
+ "to": "framework/preact/reference/functions/useHotkeySequence"
+ },
+ {
+ "label": "UseHotkeySequenceOptions",
+ "to": "framework/preact/reference/interfaces/UseHotkeySequenceOptions"
+ }
+ ]
+ },
+ {
+ "label": "solid",
+ "children": [
+ {
+ "label": "createHotkeySequence",
+ "to": "framework/solid/reference/functions/createHotkeySequence"
+ },
+ {
+ "label": "CreateHotkeySequenceOptions",
+ "to": "framework/solid/reference/interfaces/CreateHotkeySequenceOptions"
+ }
+ ]
}
]
},
@@ -212,6 +350,24 @@
"to": "framework/react/reference/functions/useKeyHold"
}
]
+ },
+ {
+ "label": "preact",
+ "children": [
+ {
+ "label": "useKeyHold",
+ "to": "framework/preact/reference/functions/useKeyHold"
+ }
+ ]
+ },
+ {
+ "label": "solid",
+ "children": [
+ {
+ "label": "createKeyHold",
+ "to": "framework/solid/reference/functions/createKeyHold"
+ }
+ ]
}
]
},
@@ -250,6 +406,32 @@
"to": "framework/react/reference/functions/useHeldKeyCodes"
}
]
+ },
+ {
+ "label": "preact",
+ "children": [
+ {
+ "label": "useHeldKeys",
+ "to": "framework/preact/reference/functions/useHeldKeys"
+ },
+ {
+ "label": "useHeldKeyCodes",
+ "to": "framework/preact/reference/functions/useHeldKeyCodes"
+ }
+ ]
+ },
+ {
+ "label": "solid",
+ "children": [
+ {
+ "label": "createHeldKeys",
+ "to": "framework/solid/reference/functions/createHeldKeys"
+ },
+ {
+ "label": "createHeldKeyCodes",
+ "to": "framework/solid/reference/functions/createHeldKeyCodes"
+ }
+ ]
}
]
},
@@ -284,6 +466,32 @@
"to": "framework/react/reference/interfaces/ReactHotkeyRecorder"
}
]
+ },
+ {
+ "label": "preact",
+ "children": [
+ {
+ "label": "useHotkeyRecorder",
+ "to": "framework/preact/reference/functions/useHotkeyRecorder"
+ },
+ {
+ "label": "PreactHotkeyRecorder",
+ "to": "framework/preact/reference/interfaces/PreactHotkeyRecorder"
+ }
+ ]
+ },
+ {
+ "label": "solid",
+ "children": [
+ {
+ "label": "createHotkeyRecorder",
+ "to": "framework/solid/reference/functions/createHotkeyRecorder"
+ },
+ {
+ "label": "SolidHotkeyRecorder",
+ "to": "framework/solid/reference/interfaces/SolidHotkeyRecorder"
+ }
+ ]
}
]
},
@@ -374,6 +582,56 @@
"to": "framework/react/examples/useKeyhold"
}
]
+ },
+ {
+ "label": "preact",
+ "children": [
+ {
+ "label": "useHotkey",
+ "to": "framework/preact/examples/useHotkey"
+ },
+ {
+ "label": "useHotkeySequence",
+ "to": "framework/preact/examples/useHotkeySequence"
+ },
+ {
+ "label": "useHotkeyRecorder",
+ "to": "framework/preact/examples/useHotkeyRecorder"
+ },
+ {
+ "label": "useHeldKeys",
+ "to": "framework/preact/examples/useHeldKeys"
+ },
+ {
+ "label": "useKeyHold",
+ "to": "framework/preact/examples/useKeyhold"
+ }
+ ]
+ },
+ {
+ "label": "solid",
+ "children": [
+ {
+ "label": "createHotkey",
+ "to": "framework/solid/examples/createHotkey"
+ },
+ {
+ "label": "createHotkeySequence",
+ "to": "framework/solid/examples/createHotkeySequence"
+ },
+ {
+ "label": "createHotkeyRecorder",
+ "to": "framework/solid/examples/createHotkeyRecorder"
+ },
+ {
+ "label": "createHeldKeys",
+ "to": "framework/solid/examples/createHeldKeys"
+ },
+ {
+ "label": "createKeyHold",
+ "to": "framework/solid/examples/createKeyHold"
+ }
+ ]
}
]
}
diff --git a/docs/framework/preact/guides/formatting-display.md b/docs/framework/preact/guides/formatting-display.md
new file mode 100644
index 0000000..741686a
--- /dev/null
+++ b/docs/framework/preact/guides/formatting-display.md
@@ -0,0 +1,216 @@
+---
+title: Formatting & Display Guide
+id: formatting-display
+---
+
+TanStack Hotkeys provides several utilities for formatting hotkey strings into human-readable display text. These utilities handle platform differences automatically, so your UI shows the right symbols and labels for each operating system.
+
+## `formatForDisplay`
+
+The primary formatting function. Returns a platform-aware string using symbols on macOS and text labels on Windows/Linux.
+
+```tsx
+import { formatForDisplay } from '@tanstack/preact-hotkeys'
+
+// On macOS:
+formatForDisplay('Mod+S') // "⌘S"
+formatForDisplay('Mod+Shift+Z') // "⇧⌘Z"
+formatForDisplay('Control+Alt+D') // "⌃⌥D"
+
+// On Windows/Linux:
+formatForDisplay('Mod+S') // "Ctrl+S"
+formatForDisplay('Mod+Shift+Z') // "Ctrl+Shift+Z"
+formatForDisplay('Control+Alt+D') // "Ctrl+Alt+D"
+```
+
+### Options
+
+```ts
+formatForDisplay('Mod+S', {
+ platform: 'mac', // Override platform detection ('mac' | 'windows' | 'linux')
+})
+```
+
+On macOS, modifiers are joined without a separator (e.g., `⇧⌘Z`). On Windows and Linux, modifiers are joined with `+` (e.g., `Ctrl+Shift+Z`). This behavior is automatic and not configurable.
+
+## `formatWithLabels`
+
+Returns human-readable text labels (e.g., "Cmd" instead of the symbol). Useful when you want readable text rather than symbols.
+
+```tsx
+import { formatWithLabels } from '@tanstack/preact-hotkeys'
+
+// On macOS:
+formatWithLabels('Mod+S') // "Cmd+S"
+formatWithLabels('Mod+Shift+Z') // "Cmd+Shift+Z"
+
+// On Windows/Linux:
+formatWithLabels('Mod+S') // "Ctrl+S"
+formatWithLabels('Mod+Shift+Z') // "Ctrl+Shift+Z"
+```
+
+## `formatKeyForDebuggingDisplay`
+
+Returns a rich label intended for devtools and debugging. Includes both the symbol and a descriptive label.
+
+```tsx
+import { formatKeyForDebuggingDisplay } from '@tanstack/preact-hotkeys'
+
+// On macOS:
+formatKeyForDebuggingDisplay('Meta') // "⌘ Mod (Cmd)"
+formatKeyForDebuggingDisplay('Shift') // "⇧ Shift"
+formatKeyForDebuggingDisplay('Control') // "⌃ Control"
+```
+
+## Using Formatted Hotkeys in Preact
+
+### Keyboard Shortcut Badges
+
+```tsx
+import { formatForDisplay } from '@tanstack/preact-hotkeys'
+
+function ShortcutBadge({ hotkey }: { hotkey: string }) {
+ return {formatForDisplay(hotkey)}
+}
+
+// Usage
+ // Renders: ⌘S (Mac) or Ctrl+S (Windows)
+ // Renders: ⇧⌘P (Mac) or Ctrl+Shift+P (Windows)
+```
+
+### Menu Items with Hotkeys
+
+```tsx
+import { useHotkey, formatForDisplay } from '@tanstack/preact-hotkeys'
+
+function MenuItem({
+ label,
+ hotkey,
+ onAction,
+}: {
+ label: string
+ hotkey: string
+ onAction: () => void
+}) {
+ useHotkey(hotkey, () => onAction())
+
+ return (
+
+ {label}
+ {formatForDisplay(hotkey)}
+
+ )
+}
+
+// Usage
+
+
+
+```
+
+### Command Palette Items
+
+```tsx
+import { formatForDisplay } from '@tanstack/preact-hotkeys'
+import type { Hotkey } from '@tanstack/preact-hotkeys'
+
+interface Command {
+ id: string
+ label: string
+ hotkey?: Hotkey
+ action: () => void
+}
+
+function CommandPaletteItem({ command }: { command: Command }) {
+ return (
+
+ {command.label}
+ {command.hotkey && (
+ {formatForDisplay(command.hotkey)}
+ )}
+
+ )
+}
+```
+
+## Platform Symbols Reference
+
+On macOS, modifiers are displayed as symbols:
+
+| Modifier | Mac Symbol | Windows/Linux Label |
+|----------|-----------|-------------------|
+| Meta (Cmd) | `⌘` | `Win` / `Super` |
+| Control | `⌃` | `Ctrl` |
+| Alt/Option | `⌥` | `Alt` |
+| Shift | `⇧` | `Shift` |
+
+Special keys also have display symbols:
+
+| Key | Display |
+|-----|---------|
+| Escape | `Esc` |
+| Backspace | `⌫` (Mac) / `Backspace` |
+| Delete | `⌦` (Mac) / `Del` |
+| Enter | `↵` |
+| Tab | `⇥` |
+| ArrowUp | `↑` |
+| ArrowDown | `↓` |
+| ArrowLeft | `←` |
+| ArrowRight | `→` |
+| Space | `Space` |
+
+## Parsing and Normalization
+
+TanStack Hotkeys also provides utilities for parsing and normalizing hotkey strings:
+
+### `parseHotkey`
+
+Parse a hotkey string into its component parts:
+
+```ts
+import { parseHotkey } from '@tanstack/preact-hotkeys'
+
+const parsed = parseHotkey('Mod+Shift+S')
+// {
+// key: 'S',
+// ctrl: false, // true on Windows/Linux
+// shift: true,
+// alt: false,
+// meta: true, // true on Mac
+// modifiers: ['Shift', 'Meta'] // or ['Control', 'Shift'] on Windows
+// }
+```
+
+### `normalizeHotkey`
+
+Normalize a hotkey string to its canonical form:
+
+```ts
+import { normalizeHotkey } from '@tanstack/preact-hotkeys'
+
+normalizeHotkey('Cmd+S') // 'Meta+S' (on Mac)
+normalizeHotkey('Ctrl+Shift+s') // 'Control+Shift+S'
+normalizeHotkey('Mod+S') // 'Meta+S' (on Mac) or 'Control+S' (on Windows)
+```
+
+## Validation
+
+Use `validateHotkey` to check if a hotkey string is valid and get warnings about potential platform issues:
+
+```ts
+import { validateHotkey } from '@tanstack/preact-hotkeys'
+
+const result = validateHotkey('Alt+A')
+// {
+// valid: true,
+// warnings: ['Alt+letter combinations may not work on macOS due to special characters'],
+// errors: []
+// }
+
+const result2 = validateHotkey('InvalidKey+S')
+// {
+// valid: false,
+// warnings: [],
+// errors: ['Unknown key: InvalidKey']
+// }
+```
diff --git a/docs/framework/preact/guides/hotkey-recording.md b/docs/framework/preact/guides/hotkey-recording.md
new file mode 100644
index 0000000..75c54ec
--- /dev/null
+++ b/docs/framework/preact/guides/hotkey-recording.md
@@ -0,0 +1,168 @@
+---
+title: Hotkey Recording Guide
+id: hotkey-recording
+---
+
+TanStack Hotkeys provides the `useHotkeyRecorder` hook for building keyboard shortcut customization UIs. This lets users record their own shortcuts by pressing the desired key combination, similar to how system preferences or IDE shortcut editors work.
+
+## Basic Usage
+
+```tsx
+import { useHotkeyRecorder, formatForDisplay } from '@tanstack/preact-hotkeys'
+
+function ShortcutRecorder() {
+ const { isRecording, recordedHotkey, startRecording, stopRecording, cancelRecording } =
+ useHotkeyRecorder({
+ onRecord: (hotkey) => {
+ console.log('Recorded:', hotkey) // e.g., "Mod+Shift+S"
+ },
+ })
+
+ return (
+
+
+ {isRecording
+ ? 'Press a key combination...'
+ : recordedHotkey
+ ? formatForDisplay(recordedHotkey)
+ : 'Click to record'}
+
+ {isRecording && (
+ Cancel
+ )}
+
+ )
+}
+```
+
+## Return Value
+
+The `useHotkeyRecorder` hook returns an object with:
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `isRecording` | `boolean` | Whether the recorder is currently listening for key presses |
+| `recordedHotkey` | `Hotkey \| null` | The last recorded hotkey string, or `null` if nothing recorded |
+| `startRecording` | `() => void` | Start listening for key presses |
+| `stopRecording` | `() => void` | Stop listening and keep the recorded hotkey |
+| `cancelRecording` | `() => void` | Stop listening and discard any recorded hotkey |
+
+## Options
+
+```tsx
+useHotkeyRecorder({
+ onRecord: (hotkey) => { /* called when a hotkey is recorded */ },
+ onCancel: () => { /* called when recording is cancelled */ },
+ onClear: () => { /* called when the recorded hotkey is cleared */ },
+})
+```
+
+### `onRecord`
+
+Called when the user presses a valid key combination (a modifier + a non-modifier key, or a single non-modifier key). Receives the recorded `Hotkey` string.
+
+### `onCancel`
+
+Called when recording is cancelled (either by pressing Escape or calling `cancelRecording()`).
+
+### `onClear`
+
+Called when the recorded hotkey is cleared (by pressing Backspace or Delete during recording).
+
+### Global Default Options via Provider
+
+You can set default options for all `useHotkeyRecorder` calls by wrapping your component tree with `HotkeysProvider`. Per-hook options will override the provider defaults.
+
+```tsx
+import { HotkeysProvider } from '@tanstack/preact-hotkeys'
+
+ console.log('Recording cancelled'),
+ },
+ }}
+>
+
+
+```
+
+## Recording Behavior
+
+The recorder has specific behavior for different keys:
+
+| Key | Behavior |
+|-----|----------|
+| **Modifier only** (Shift, Ctrl, etc.) | Waits for a non-modifier key -- modifier-only presses don't complete a recording |
+| **Modifier + key** (e.g., Ctrl+S) | Records the full combination |
+| **Single key** (e.g., Escape, F1) | Records the single key |
+| **Escape** | Cancels the recording |
+| **Backspace / Delete** | Clears the currently recorded hotkey |
+
+### Mod Auto-Conversion
+
+Recorded hotkeys automatically use the portable `Mod` format. If a user on macOS presses Command+S, the recorded hotkey will be `Mod+S` rather than `Meta+S`. This ensures shortcuts are portable across platforms.
+
+## Building a Shortcut Settings UI
+
+Here's a more complete example of a shortcut customization panel:
+
+```tsx
+import { useState } from 'preact/hooks'
+import {
+ useHotkey,
+ useHotkeyRecorder,
+ formatForDisplay,
+} from '@tanstack/preact-hotkeys'
+import type { Hotkey } from '@tanstack/preact-hotkeys'
+
+function ShortcutSettings() {
+ const [shortcuts, setShortcuts] = useState>({
+ save: 'Mod+S',
+ undo: 'Mod+Z',
+ search: 'Mod+K',
+ })
+
+ const [editingAction, setEditingAction] = useState(null)
+
+ const recorder = useHotkeyRecorder({
+ onRecord: (hotkey) => {
+ if (editingAction) {
+ setShortcuts((prev) => ({ ...prev, [editingAction]: hotkey }))
+ setEditingAction(null)
+ }
+ },
+ onCancel: () => setEditingAction(null),
+ })
+
+ // Register the actual hotkeys with their current bindings
+ useHotkey(shortcuts.save, () => save())
+ useHotkey(shortcuts.undo, () => undo())
+ useHotkey(shortcuts.search, () => openSearch())
+
+ return (
+
+
Keyboard Shortcuts
+ {Object.entries(shortcuts).map(([action, hotkey]) => (
+
+ {action}
+ {
+ setEditingAction(action)
+ recorder.startRecording()
+ }}
+ >
+ {editingAction === action && recorder.isRecording
+ ? 'Press keys...'
+ : formatForDisplay(hotkey)}
+
+
+ ))}
+
+ )
+}
+```
+
+## Under the Hood
+
+The `useHotkeyRecorder` hook creates a `HotkeyRecorder` class instance and subscribes to its reactive state via `@tanstack/preact-store`. The class manages its own keyboard event listeners and state, and the hook handles cleanup on unmount.
diff --git a/docs/framework/preact/guides/hotkeys.md b/docs/framework/preact/guides/hotkeys.md
new file mode 100644
index 0000000..ad7aa8a
--- /dev/null
+++ b/docs/framework/preact/guides/hotkeys.md
@@ -0,0 +1,252 @@
+---
+title: Hotkeys Guide
+id: hotkeys
+---
+
+The `useHotkey` hook is the primary way to register keyboard shortcuts in Preact applications. It wraps the singleton `HotkeyManager` with automatic lifecycle management, stale-closure prevention, and Preact ref support.
+
+## Basic Usage
+
+```tsx
+import { useHotkey } from '@tanstack/preact-hotkeys'
+
+function App() {
+ useHotkey('Mod+S', () => {
+ saveDocument()
+ }, {
+ // override the default options here
+ })
+}
+```
+
+The callback receives the original `KeyboardEvent` as the first argument and a `HotkeyCallbackContext` as the second:
+
+```tsx
+useHotkey('Mod+S', (event, context) => {
+ console.log(context.hotkey) // 'Mod+S'
+ console.log(context.parsedHotkey) // { key: 'S', ctrl: false, shift: false, alt: false, meta: true, modifiers: ['Meta'] }
+})
+```
+
+You can pass a hotkey as a string or as a `RawHotkey` object (modifier booleans optional). Use `mod` for cross-platform shortcuts (Command on Mac, Control elsewhere):
+
+```tsx
+useHotkey('Mod+S', () => save())
+useHotkey({ key: 'S', mod: true }, () => save()) // Same as above
+useHotkey({ key: 'Escape' }, () => closeModal())
+useHotkey({ key: 'S', ctrl: true, shift: true }, () => saveAs())
+useHotkey({ key: 'S', mod: true, shift: true }, () => saveAs())
+```
+
+## Default Options
+
+When you register a hotkey without passing options, or when you omit specific options, the following defaults apply:
+
+```tsx
+useHotkey('Mod+S', callback, {
+ enabled: true,
+ preventDefault: true,
+ stopPropagation: true,
+ eventType: 'keydown',
+ requireReset: false,
+ ignoreInputs: undefined, // smart default: false for Mod+S, true for single keys
+ target: document,
+ platform: undefined, // auto-detected
+ conflictBehavior: 'warn',
+})
+```
+
+### Why These Defaults?
+
+Most hotkey registrations are intended to override default browser behavior—such as using `Mod+S` to save a document instead of showing the browser's "Save Page" dialog. To make this easy and consistent, the library sets `preventDefault` and `stopPropagation` to `true` by default, ensuring your hotkey handlers take precedence and reducing the amount of repetitive boilerplate code required.
+
+#### Smart Input Handling: `ignoreInputs`
+
+The `ignoreInputs` option is designed to strike a balance between accessibility and usability. By default, hotkeys involving `Ctrl`/`Meta` modifiers (like `Mod+S`) and the `Escape` key are allowed to fire even when the focus is inside input elements (such as text fields or text areas), and when focused on button-type inputs (`type="button"`, `"submit"`, or `"reset"`). This allows shortcuts like save or close to work wherever the user is focused. On the other hand, single key shortcuts or those using only `Shift`/`Alt` are ignored within non-button inputs to prevent interference with normal typing.
+
+#### Hotkey Conflicts: `conflictBehavior`
+
+When you attempt to register a hotkey that is already registered (possibly in another part of your app), the library logs a warning by default using the `conflictBehavior: 'warn'` setting. This helps you catch accidental duplicate bindings during development so they can be resolved before reaching production.
+
+
+### Global Default Options via Provider
+
+You can change the default options for all `useHotkey` calls in your app by wrapping your component tree with `HotkeysProvider`. Per-hook options will override the provider defaults.
+
+```tsx
+import { HotkeysProvider } from '@tanstack/preact-hotkeys'
+
+
+
+
+```
+
+## Hotkey Options
+
+### `enabled`
+
+Controls whether the hotkey is active. Defaults to `true`.
+
+```tsx
+const [isEditing, setIsEditing] = useState(false)
+
+// Only active when editing
+useHotkey('Mod+S', () => save(), { enabled: isEditing })
+```
+
+### `preventDefault`
+
+Automatically calls `event.preventDefault()` when the hotkey fires. Defaults to `true`.
+
+```tsx
+// Browser default is prevented by default
+useHotkey('Mod+S', () => save())
+
+// Opt out when you want the browser's default behavior
+useHotkey('Mod+S', () => save(), { preventDefault: false })
+```
+
+### `stopPropagation`
+
+Calls `event.stopPropagation()` when the hotkey fires. Defaults to `true`.
+
+```tsx
+// Event propagation is stopped by default
+useHotkey('Escape', () => closeModal())
+
+// Opt out when you need the event to bubble
+useHotkey('Escape', () => closeModal(), { stopPropagation: false })
+```
+
+### `eventType`
+
+Whether to listen on `keydown` (default) or `keyup`.
+
+```tsx
+// Fire when the key is released
+useHotkey('Shift', () => deactivateMode(), { eventType: 'keyup' })
+```
+
+### `requireReset`
+
+When `true`, the hotkey will only fire once per key press. The key must be released and pressed again to fire again. Defaults to `false`.
+
+```tsx
+// Only fires once per Escape press, not on key repeat
+useHotkey('Escape', () => closePanel(), { requireReset: true })
+```
+
+### `ignoreInputs`
+
+When `true`, the hotkey will not fire when the user is focused on a text input, textarea, select, or contentEditable element. Button-type inputs (`type="button"`, `"submit"`, `"reset"`) are not ignored, so shortcuts like Mod+S work when the user has tabbed to a form button. When unset, a smart default applies: `Ctrl`/`Meta` shortcuts and `Escape` fire in inputs; single keys and `Shift`/`Alt` combos are ignored.
+
+```tsx
+// Single key - ignored in inputs by default (smart default)
+useHotkey('K', () => openSearch())
+
+// Mod+S and Escape - fire in inputs by default (smart default)
+useHotkey('Mod+S', () => save())
+useHotkey('Escape', () => closeDialog())
+
+// Override: force a single key to fire in inputs
+useHotkey('Enter', () => submit(), { ignoreInputs: false })
+```
+
+Set `ignoreInputs: false` or `true` explicitly to override the smart default.
+
+### `target`
+
+The DOM element to attach the event listener to. Defaults to `document`. Can be a DOM element, `document`, `window`, or a Preact ref.
+
+```tsx
+import { useRef } from 'preact/hooks'
+
+function Panel() {
+ const panelRef = useRef(null)
+
+ // Only listens for events on this specific element
+ useHotkey('Escape', () => closePanel(), { target: panelRef })
+
+ return (
+
+ )
+}
+```
+
+> [!NOTE]
+> When using a ref as the target, make sure the element is focusable (has `tabIndex`) so it can receive keyboard events.
+
+### `conflictBehavior`
+
+Controls what happens when you register a hotkey that's already registered. Options:
+
+- `'warn'` (default) - Logs a warning but allows the registration
+- `'error'` - Throws an error
+- `'replace'` - Replaces the existing registration
+- `'allow'` - Allows multiple registrations silently
+
+```tsx
+useHotkey('Mod+S', () => save(), { conflictBehavior: 'replace' })
+```
+
+### `platform`
+
+Override the auto-detected platform. Useful for testing or for applications that need to force a specific platform behavior.
+
+```tsx
+useHotkey('Mod+S', () => save(), { platform: 'mac' })
+```
+
+## Stale Closure Prevention
+
+The `useHotkey` hook automatically syncs the callback on every render, so you never need to worry about stale closures:
+
+```tsx
+function Counter() {
+ const [count, setCount] = useState(0)
+
+ // This callback always has access to the latest `count` value
+ useHotkey('Mod+Shift+C', () => {
+ console.log('Current count:', count)
+ })
+
+ return setCount(count + 1)}>Count: {count}
+}
+```
+
+## Automatic Cleanup
+
+The hook automatically unregisters the hotkey when the component unmounts:
+
+```tsx
+function TemporaryPanel() {
+ // Automatically cleaned up when this component unmounts
+ useHotkey('Escape', () => closePanel())
+
+ return Panel content
+}
+```
+
+## The Hotkey Manager
+
+Under the hood, `useHotkey` uses the singleton `HotkeyManager`. You can also access the manager directly if needed:
+
+```tsx
+import { getHotkeyManager } from '@tanstack/preact-hotkeys'
+
+const manager = getHotkeyManager()
+
+// Check if a hotkey is registered
+manager.isRegistered('Mod+S')
+
+// Get total number of registrations
+manager.getRegistrationCount()
+```
+
+The manager attaches event listeners per target element, so only elements that have registered hotkeys receive listeners. This is more efficient than a single global listener.
diff --git a/docs/framework/preact/guides/key-state-tracking.md b/docs/framework/preact/guides/key-state-tracking.md
new file mode 100644
index 0000000..ca082ce
--- /dev/null
+++ b/docs/framework/preact/guides/key-state-tracking.md
@@ -0,0 +1,186 @@
+---
+title: Key State Tracking Guide
+id: key-state-tracking
+---
+
+TanStack Hotkeys provides three hooks for tracking the real-time state of keyboard keys. These are useful for building UIs that respond to modifier keys being held, displaying active key states, or implementing hold-to-activate features.
+
+## `useHeldKeys`
+
+Returns a reactive array of all currently held key names.
+
+```tsx
+import { useHeldKeys } from '@tanstack/preact-hotkeys'
+
+function KeyDisplay() {
+ const heldKeys = useHeldKeys()
+
+ return (
+
+ {heldKeys.length > 0
+ ? `Held: ${heldKeys.join(' + ')}`
+ : 'No keys held'}
+
+ )
+}
+```
+
+The returned array contains key names like `'Shift'`, `'Control'`, `'Meta'`, `'A'`, `'ArrowUp'`, etc. Keys appear in the order they were pressed.
+
+## `useHeldKeyCodes`
+
+Returns a reactive object mapping held key names to their physical key codes (`event.code` values). This is useful when you need to distinguish between left and right modifiers.
+
+```tsx
+import { useHeldKeyCodes } from '@tanstack/preact-hotkeys'
+
+function KeyCodeDisplay() {
+ const heldCodes = useHeldKeyCodes()
+ // Example: { Shift: "ShiftLeft", Control: "ControlRight" }
+
+ return (
+
+ {Object.entries(heldCodes).map(([key, code]) => (
+
+ {key}: {code}
+
+ ))}
+
+ )
+}
+```
+
+## `useKeyHold`
+
+Checks whether a specific key is currently held. This hook is optimized to only trigger re-renders when the specified key's held state changes, not when other keys are pressed or released.
+
+```tsx
+import { useKeyHold } from '@tanstack/preact-hotkeys'
+
+function ModifierIndicators() {
+ const isShiftHeld = useKeyHold('Shift')
+ const isCtrlHeld = useKeyHold('Control')
+ const isAltHeld = useKeyHold('Alt')
+ const isMetaHeld = useKeyHold('Meta')
+
+ return (
+
+ Shift
+ Ctrl
+ Alt
+ Meta
+
+ )
+}
+```
+
+## Common Patterns
+
+### Hold-to-Reveal UI
+
+Show additional options while a modifier is held:
+
+```tsx
+import { useKeyHold } from '@tanstack/preact-hotkeys'
+
+function FileItem({ file }: { file: File }) {
+ const isShiftHeld = useKeyHold('Shift')
+
+ return (
+
+ {file.name}
+ {isShiftHeld && (
+ permanentlyDelete(file)}>
+ Permanently Delete
+
+ )}
+ {!isShiftHeld && (
+ moveToTrash(file)}>
+ Move to Trash
+
+ )}
+
+ )
+}
+```
+
+### Keyboard Shortcut Hints
+
+Display different shortcut hints based on which modifiers are held:
+
+```tsx
+import { useKeyHold } from '@tanstack/preact-hotkeys'
+
+function ShortcutHints() {
+ const isModHeld = useKeyHold('Meta') // or 'Control' on Windows
+
+ if (!isModHeld) return null
+
+ return (
+
+
S - Save
+
Z - Undo
+
Shift+Z - Redo
+
K - Command Palette
+
+ )
+}
+```
+
+### Debugging Key Display
+
+Combine hooks with formatting utilities for a rich debugging display:
+
+```tsx
+import {
+ useHeldKeys,
+ useHeldKeyCodes,
+ formatKeyForDebuggingDisplay,
+} from '@tanstack/preact-hotkeys'
+
+function KeyDebugger() {
+ const heldKeys = useHeldKeys()
+ const heldCodes = useHeldKeyCodes()
+
+ return (
+
+
Active Keys
+ {heldKeys.map((key) => (
+
+ {formatKeyForDebuggingDisplay(key)}
+ {heldCodes[key]}
+
+ ))}
+ {heldKeys.length === 0 &&
Press any key...
}
+
+ )
+}
+```
+
+## Platform Quirks
+
+The underlying `KeyStateTracker` handles several platform-specific issues:
+
+### macOS Modifier Key Behavior
+
+On macOS, when a modifier key is held and a non-modifier key is pressed, the OS sometimes swallows the `keyup` event for the non-modifier key. TanStack Hotkeys detects and handles this automatically so held key state stays accurate.
+
+### Window Blur
+
+When the browser window loses focus, all held keys are automatically cleared. This prevents "stuck" keys that would otherwise appear held even after the user tabs away and releases them.
+
+## Under the Hood
+
+All three hooks subscribe to the singleton `KeyStateTracker` via `@tanstack/preact-store`. The tracker manages its own event listeners on `document` and maintains state in a TanStack Store, which the hooks subscribe to reactively.
+
+```tsx
+import { getKeyStateTracker } from '@tanstack/preact-hotkeys'
+
+const tracker = getKeyStateTracker()
+
+// Imperative access (outside of Preact)
+tracker.getHeldKeys() // string[]
+tracker.isKeyHeld('Shift') // boolean
+tracker.isAnyKeyHeld(['Shift', 'Control']) // boolean
+tracker.areAllKeysHeld(['Shift', 'Control']) // boolean
+```
diff --git a/docs/framework/preact/guides/sequences.md b/docs/framework/preact/guides/sequences.md
new file mode 100644
index 0000000..a06a08c
--- /dev/null
+++ b/docs/framework/preact/guides/sequences.md
@@ -0,0 +1,171 @@
+---
+title: Sequences Guide
+id: sequences
+---
+
+TanStack Hotkeys supports multi-key sequences -- shortcuts where you press keys one after another rather than simultaneously. This is commonly used for Vim-style navigation, cheat codes, or multi-step commands.
+
+## Basic Usage
+
+Use the `useHotkeySequence` hook to register a key sequence:
+
+```tsx
+import { useHotkeySequence } from '@tanstack/preact-hotkeys'
+
+function App() {
+ // Vim-style: press g then g to scroll to top
+ useHotkeySequence(['G', 'G'], () => {
+ window.scrollTo({ top: 0, behavior: 'smooth' })
+ })
+}
+```
+
+The first argument is an array of `Hotkey` strings representing each step in the sequence. The user must press them in order within the timeout window.
+
+## Sequence Options
+
+The third argument is an options object:
+
+```tsx
+useHotkeySequence(['G', 'G'], callback, {
+ timeout: 1000, // Time allowed between keys (ms)
+ enabled: true, // Whether the sequence is active
+ target: document, // Or a Preact ref for scoped sequences
+})
+```
+
+### `timeout`
+
+The maximum time (in milliseconds) allowed between consecutive key presses. If the user takes longer than this between any two keys, the sequence resets. Defaults to `1000` (1 second).
+
+```tsx
+// Fast sequence - user must type quickly
+useHotkeySequence(['D', 'D'], () => deleteLine(), { timeout: 500 })
+
+// Slow sequence - user has more time between keys
+useHotkeySequence(['Shift+Z', 'Shift+Z'], () => forceQuit(), { timeout: 2000 })
+```
+
+### `enabled`
+
+Controls whether the sequence is active. Defaults to `true`.
+
+```tsx
+const [isVimMode, setIsVimMode] = useState(true)
+
+useHotkeySequence(['G', 'G'], () => scrollToTop(), { enabled: isVimMode })
+```
+
+### `target`
+
+The DOM element to attach the sequence listener to. Defaults to `document`. Can be a Preact ref, DOM element, `document`, or `window` for scoped sequences.
+
+### Global Default Options via Provider
+
+You can set default options for all `useHotkeySequence` calls by wrapping your component tree with `HotkeysProvider`. Per-hook options will override the provider defaults.
+
+```tsx
+import { HotkeysProvider } from '@tanstack/preact-hotkeys'
+
+
+
+
+```
+
+## Sequences with Modifiers
+
+Each step in a sequence can include modifiers:
+
+```tsx
+// Ctrl+K followed by Ctrl+C (VS Code-style comment)
+useHotkeySequence(['Mod+K', 'Mod+C'], () => {
+ commentSelection()
+})
+
+// g then Shift+G (go to bottom, Vim-style)
+useHotkeySequence(['G', 'Shift+G'], () => {
+ scrollToBottom()
+})
+```
+
+## Common Sequence Patterns
+
+### Vim-Style Navigation
+
+```tsx
+function VimNavigation() {
+ useHotkeySequence(['G', 'G'], () => scrollToTop())
+ useHotkeySequence(['G', 'Shift+G'], () => scrollToBottom())
+ useHotkeySequence(['D', 'D'], () => deleteLine())
+ useHotkeySequence(['D', 'W'], () => deleteWord())
+ useHotkeySequence(['C', 'I', 'W'], () => changeInnerWord())
+}
+```
+
+### Konami Code
+
+```tsx
+useHotkeySequence(
+ [
+ 'ArrowUp', 'ArrowUp',
+ 'ArrowDown', 'ArrowDown',
+ 'ArrowLeft', 'ArrowRight',
+ 'ArrowLeft', 'ArrowRight',
+ 'B', 'A',
+ ],
+ () => enableEasterEgg(),
+ { timeout: 2000 },
+)
+```
+
+### Multi-Step Commands
+
+```tsx
+// Press "h", "e", "l", "p" to open help
+useHotkeySequence(['H', 'E', 'L', 'P'], () => openHelp())
+```
+
+## How Sequences Work
+
+The `SequenceManager` (singleton) handles all sequence registrations. When a key is pressed:
+
+1. It checks if the key matches the next expected step in any registered sequence
+2. If it matches, the sequence advances to the next step
+3. If the timeout expires between steps, the sequence resets
+4. When all steps are completed, the callback fires
+
+### Overlapping Sequences
+
+Multiple sequences can share the same prefix. The manager tracks progress for each sequence independently:
+
+```tsx
+// Both share the 'D' prefix
+useHotkeySequence(['D', 'D'], () => deleteLine()) // dd
+useHotkeySequence(['D', 'W'], () => deleteWord()) // dw
+useHotkeySequence(['D', 'I', 'W'], () => deleteInnerWord()) // diw
+```
+
+After pressing `D`, the manager waits for the next key to determine which sequence to complete.
+
+## The Sequence Manager
+
+Under the hood, `useHotkeySequence` uses the singleton `SequenceManager`. You can also use the core `createSequenceMatcher` function for standalone sequence matching without the singleton:
+
+```tsx
+import { createSequenceMatcher } from '@tanstack/preact-hotkeys'
+
+const matcher = createSequenceMatcher(['G', 'G'], {
+ timeout: 1000,
+})
+
+document.addEventListener('keydown', (e) => {
+ if (matcher.match(e)) {
+ console.log('Sequence completed!')
+ }
+ console.log('Progress:', matcher.getProgress()) // e.g., 1/2
+})
+```
diff --git a/docs/framework/preact/reference/functions/HotkeysProvider.md b/docs/framework/preact/reference/functions/HotkeysProvider.md
new file mode 100644
index 0000000..ceb8849
--- /dev/null
+++ b/docs/framework/preact/reference/functions/HotkeysProvider.md
@@ -0,0 +1,22 @@
+---
+id: HotkeysProvider
+title: HotkeysProvider
+---
+
+# Function: HotkeysProvider()
+
+```ts
+function HotkeysProvider(__namedParameters): Element;
+```
+
+Defined in: [HotkeysProvider.tsx:27](https://github.com/TanStack/hotkeys/blob/main/packages/preact-hotkeys/src/HotkeysProvider.tsx#L27)
+
+## Parameters
+
+### \_\_namedParameters
+
+[`HotkeysProviderProps`](../interfaces/HotkeysProviderProps.md)
+
+## Returns
+
+`Element`
diff --git a/docs/framework/preact/reference/functions/useDefaultHotkeysOptions.md b/docs/framework/preact/reference/functions/useDefaultHotkeysOptions.md
new file mode 100644
index 0000000..9cc15af
--- /dev/null
+++ b/docs/framework/preact/reference/functions/useDefaultHotkeysOptions.md
@@ -0,0 +1,16 @@
+---
+id: useDefaultHotkeysOptions
+title: useDefaultHotkeysOptions
+---
+
+# Function: useDefaultHotkeysOptions()
+
+```ts
+function useDefaultHotkeysOptions(): HotkeysProviderOptions;
+```
+
+Defined in: [HotkeysProvider.tsx:49](https://github.com/TanStack/hotkeys/blob/main/packages/preact-hotkeys/src/HotkeysProvider.tsx#L49)
+
+## Returns
+
+[`HotkeysProviderOptions`](../interfaces/HotkeysProviderOptions.md)
diff --git a/docs/framework/preact/reference/functions/useHeldKeyCodes.md b/docs/framework/preact/reference/functions/useHeldKeyCodes.md
new file mode 100644
index 0000000..586b59c
--- /dev/null
+++ b/docs/framework/preact/reference/functions/useHeldKeyCodes.md
@@ -0,0 +1,42 @@
+---
+id: useHeldKeyCodes
+title: useHeldKeyCodes
+---
+
+# Function: useHeldKeyCodes()
+
+```ts
+function useHeldKeyCodes(): Record;
+```
+
+Defined in: [useHeldKeyCodes.ts:30](https://github.com/TanStack/hotkeys/blob/main/packages/preact-hotkeys/src/useHeldKeyCodes.ts#L30)
+
+Preact hook that returns a map of currently held key names to their physical `event.code` values.
+
+This is useful for debugging which physical key was pressed (e.g. distinguishing
+left vs right Shift via "ShiftLeft" / "ShiftRight").
+
+## Returns
+
+`Record`\<`string`, `string`\>
+
+Record mapping normalized key names to their `event.code` values
+
+## Example
+
+```tsx
+function KeyDebugDisplay() {
+ const heldKeys = useHeldKeys()
+ const heldCodes = useHeldKeyCodes()
+
+ return (
+
+ {heldKeys.map((key) => (
+
+ {key} {heldCodes[key]}
+
+ ))}
+
+ )
+}
+```
diff --git a/docs/framework/preact/reference/functions/useHeldKeys.md b/docs/framework/preact/reference/functions/useHeldKeys.md
new file mode 100644
index 0000000..dc68478
--- /dev/null
+++ b/docs/framework/preact/reference/functions/useHeldKeys.md
@@ -0,0 +1,38 @@
+---
+id: useHeldKeys
+title: useHeldKeys
+---
+
+# Function: useHeldKeys()
+
+```ts
+function useHeldKeys(): string[];
+```
+
+Defined in: [useHeldKeys.ts:26](https://github.com/TanStack/hotkeys/blob/main/packages/preact-hotkeys/src/useHeldKeys.ts#L26)
+
+Preact hook that returns an array of currently held keyboard keys.
+
+This hook uses `useStore` from `@tanstack/preact-store` to subscribe
+to the global KeyStateTracker and updates whenever keys are pressed
+or released.
+
+## Returns
+
+`string`[]
+
+Array of currently held key names
+
+## Example
+
+```tsx
+function KeyDisplay() {
+ const heldKeys = useHeldKeys()
+
+ return (
+
+ Currently pressed: {heldKeys.join(' + ') || 'None'}
+
+ )
+}
+```
diff --git a/docs/framework/preact/reference/functions/useHotkey.md b/docs/framework/preact/reference/functions/useHotkey.md
new file mode 100644
index 0000000..7d47393
--- /dev/null
+++ b/docs/framework/preact/reference/functions/useHotkey.md
@@ -0,0 +1,91 @@
+---
+id: useHotkey
+title: useHotkey
+---
+
+# Function: useHotkey()
+
+```ts
+function useHotkey(
+ hotkey,
+ callback,
+ options): void;
+```
+
+Defined in: [useHotkey.ts:90](https://github.com/TanStack/hotkeys/blob/main/packages/preact-hotkeys/src/useHotkey.ts#L90)
+
+Preact hook for registering a keyboard hotkey.
+
+Uses the singleton HotkeyManager for efficient event handling.
+The callback receives both the keyboard event and a context object
+containing the hotkey string and parsed hotkey.
+
+This hook syncs the callback and options on every render to avoid
+stale closures. This means
+callbacks that reference Preact state will always have access to
+the latest values.
+
+## Parameters
+
+### hotkey
+
+`RegisterableHotkey`
+
+The hotkey string (e.g., 'Mod+S', 'Escape') or RawHotkey object (supports `mod` for cross-platform)
+
+### callback
+
+`HotkeyCallback`
+
+The function to call when the hotkey is pressed
+
+### options
+
+[`UseHotkeyOptions`](../interfaces/UseHotkeyOptions.md) = `{}`
+
+Options for the hotkey behavior
+
+## Returns
+
+`void`
+
+## Examples
+
+```tsx
+function SaveButton() {
+ const [count, setCount] = useState(0)
+
+ // Callback always has access to latest count value
+ useHotkey('Mod+S', (event, { hotkey }) => {
+ console.log(`Save triggered, count is ${count}`)
+ handleSave()
+ })
+
+ return setCount(c => c + 1)}>Count: {count}
+}
+```
+
+```tsx
+function Modal({ isOpen, onClose }) {
+ // enabled option is synced on every render
+ useHotkey('Escape', () => {
+ onClose()
+ }, { enabled: isOpen })
+
+ if (!isOpen) return null
+ return ...
+}
+```
+
+```tsx
+function Editor() {
+ const editorRef = useRef(null)
+
+ // Scoped to a specific element
+ useHotkey('Mod+S', () => {
+ save()
+ }, { target: editorRef })
+
+ return ...
+}
+```
diff --git a/docs/framework/preact/reference/functions/useHotkeyRecorder.md b/docs/framework/preact/reference/functions/useHotkeyRecorder.md
new file mode 100644
index 0000000..7d5ff10
--- /dev/null
+++ b/docs/framework/preact/reference/functions/useHotkeyRecorder.md
@@ -0,0 +1,61 @@
+---
+id: useHotkeyRecorder
+title: useHotkeyRecorder
+---
+
+# Function: useHotkeyRecorder()
+
+```ts
+function useHotkeyRecorder(options): PreactHotkeyRecorder;
+```
+
+Defined in: [useHotkeyRecorder.ts:58](https://github.com/TanStack/hotkeys/blob/main/packages/preact-hotkeys/src/useHotkeyRecorder.ts#L58)
+
+Preact hook for recording keyboard shortcuts.
+
+This hook provides a thin wrapper around the framework-agnostic `HotkeyRecorder`
+class, managing all the complexity of capturing keyboard events, converting them
+to hotkey strings, and handling edge cases like Escape to cancel or Backspace/Delete
+to clear.
+
+## Parameters
+
+### options
+
+`HotkeyRecorderOptions`
+
+Configuration options for the recorder
+
+## Returns
+
+[`PreactHotkeyRecorder`](../interfaces/PreactHotkeyRecorder.md)
+
+An object with recording state and control functions
+
+## Example
+
+```tsx
+function ShortcutSettings() {
+ const [shortcut, setShortcut] = useState('Mod+S')
+
+ const recorder = useHotkeyRecorder({
+ onRecord: (hotkey) => {
+ setShortcut(hotkey)
+ },
+ onCancel: () => {
+ console.log('Recording cancelled')
+ },
+ })
+
+ return (
+
+
+ {recorder.isRecording ? 'Recording...' : 'Edit Shortcut'}
+
+ {recorder.recordedHotkey && (
+
Recording: {recorder.recordedHotkey}
+ )}
+
+ )
+}
+```
diff --git a/docs/framework/preact/reference/functions/useHotkeySequence.md b/docs/framework/preact/reference/functions/useHotkeySequence.md
new file mode 100644
index 0000000..4a6592c
--- /dev/null
+++ b/docs/framework/preact/reference/functions/useHotkeySequence.md
@@ -0,0 +1,67 @@
+---
+id: useHotkeySequence
+title: useHotkeySequence
+---
+
+# Function: useHotkeySequence()
+
+```ts
+function useHotkeySequence(
+ sequence,
+ callback,
+ options): void;
+```
+
+Defined in: [useHotkeySequence.ts:62](https://github.com/TanStack/hotkeys/blob/main/packages/preact-hotkeys/src/useHotkeySequence.ts#L62)
+
+Preact hook for registering a keyboard shortcut sequence (Vim-style).
+
+This hook allows you to register multi-key sequences like 'g g' or 'd d'
+that trigger when the full sequence is pressed within a timeout.
+
+## Parameters
+
+### sequence
+
+`HotkeySequence`
+
+Array of hotkey strings that form the sequence
+
+### callback
+
+`HotkeyCallback`
+
+Function to call when the sequence is completed
+
+### options
+
+[`UseHotkeySequenceOptions`](../interfaces/UseHotkeySequenceOptions.md) = `{}`
+
+Options for the sequence behavior
+
+## Returns
+
+`void`
+
+## Example
+
+```tsx
+function VimEditor() {
+ // 'g g' to go to top
+ useHotkeySequence(['G', 'G'], () => {
+ scrollToTop()
+ })
+
+ // 'd d' to delete line
+ useHotkeySequence(['D', 'D'], () => {
+ deleteLine()
+ })
+
+ // 'd i w' to delete inner word
+ useHotkeySequence(['D', 'I', 'W'], () => {
+ deleteInnerWord()
+ }, { timeout: 500 })
+
+ return ...
+}
+```
diff --git a/docs/framework/preact/reference/functions/useHotkeysContext.md b/docs/framework/preact/reference/functions/useHotkeysContext.md
new file mode 100644
index 0000000..5f10038
--- /dev/null
+++ b/docs/framework/preact/reference/functions/useHotkeysContext.md
@@ -0,0 +1,16 @@
+---
+id: useHotkeysContext
+title: useHotkeysContext
+---
+
+# Function: useHotkeysContext()
+
+```ts
+function useHotkeysContext(): HotkeysContextValue | null;
+```
+
+Defined in: [HotkeysProvider.tsx:45](https://github.com/TanStack/hotkeys/blob/main/packages/preact-hotkeys/src/HotkeysProvider.tsx#L45)
+
+## Returns
+
+`HotkeysContextValue` \| `null`
diff --git a/docs/framework/preact/reference/functions/useKeyHold.md b/docs/framework/preact/reference/functions/useKeyHold.md
new file mode 100644
index 0000000..cdbb832
--- /dev/null
+++ b/docs/framework/preact/reference/functions/useKeyHold.md
@@ -0,0 +1,62 @@
+---
+id: useKeyHold
+title: useKeyHold
+---
+
+# Function: useKeyHold()
+
+```ts
+function useKeyHold(key): boolean;
+```
+
+Defined in: [useKeyHold.ts:45](https://github.com/TanStack/hotkeys/blob/main/packages/preact-hotkeys/src/useKeyHold.ts#L45)
+
+Preact hook that returns whether a specific key is currently being held.
+
+This hook uses `useStore` from `@tanstack/preact-store` to subscribe
+to the global KeyStateTracker and uses a selector to determine if
+the specified key is held.
+
+## Parameters
+
+### key
+
+`HeldKey`
+
+The key to check (e.g., 'Shift', 'Control', 'A')
+
+## Returns
+
+`boolean`
+
+True if the key is currently held down
+
+## Examples
+
+```tsx
+function ShiftIndicator() {
+ const isShiftHeld = useKeyHold('Shift')
+
+ return (
+
+ {isShiftHeld ? 'Shift is pressed!' : 'Press Shift'}
+
+ )
+}
+```
+
+```tsx
+function ModifierIndicators() {
+ const ctrl = useKeyHold('Control')
+ const shift = useKeyHold('Shift')
+ const alt = useKeyHold('Alt')
+
+ return (
+
+ Ctrl
+ Shift
+ Alt
+
+ )
+}
+```
diff --git a/docs/framework/preact/reference/index.md b/docs/framework/preact/reference/index.md
new file mode 100644
index 0000000..2667f3d
--- /dev/null
+++ b/docs/framework/preact/reference/index.md
@@ -0,0 +1,26 @@
+---
+id: "@tanstack/preact-hotkeys"
+title: "@tanstack/preact-hotkeys"
+---
+
+# @tanstack/preact-hotkeys
+
+## Interfaces
+
+- [HotkeysProviderOptions](interfaces/HotkeysProviderOptions.md)
+- [HotkeysProviderProps](interfaces/HotkeysProviderProps.md)
+- [PreactHotkeyRecorder](interfaces/PreactHotkeyRecorder.md)
+- [UseHotkeyOptions](interfaces/UseHotkeyOptions.md)
+- [UseHotkeySequenceOptions](interfaces/UseHotkeySequenceOptions.md)
+
+## Functions
+
+- [HotkeysProvider](functions/HotkeysProvider.md)
+- [useDefaultHotkeysOptions](functions/useDefaultHotkeysOptions.md)
+- [useHeldKeyCodes](functions/useHeldKeyCodes.md)
+- [useHeldKeys](functions/useHeldKeys.md)
+- [useHotkey](functions/useHotkey.md)
+- [useHotkeyRecorder](functions/useHotkeyRecorder.md)
+- [useHotkeysContext](functions/useHotkeysContext.md)
+- [useHotkeySequence](functions/useHotkeySequence.md)
+- [useKeyHold](functions/useKeyHold.md)
diff --git a/docs/framework/preact/reference/interfaces/HotkeysProviderOptions.md b/docs/framework/preact/reference/interfaces/HotkeysProviderOptions.md
new file mode 100644
index 0000000..f1f454f
--- /dev/null
+++ b/docs/framework/preact/reference/interfaces/HotkeysProviderOptions.md
@@ -0,0 +1,38 @@
+---
+id: HotkeysProviderOptions
+title: HotkeysProviderOptions
+---
+
+# Interface: HotkeysProviderOptions
+
+Defined in: [HotkeysProvider.tsx:8](https://github.com/TanStack/hotkeys/blob/main/packages/preact-hotkeys/src/HotkeysProvider.tsx#L8)
+
+## Properties
+
+### hotkey?
+
+```ts
+optional hotkey: Partial;
+```
+
+Defined in: [HotkeysProvider.tsx:9](https://github.com/TanStack/hotkeys/blob/main/packages/preact-hotkeys/src/HotkeysProvider.tsx#L9)
+
+***
+
+### hotkeyRecorder?
+
+```ts
+optional hotkeyRecorder: Partial;
+```
+
+Defined in: [HotkeysProvider.tsx:10](https://github.com/TanStack/hotkeys/blob/main/packages/preact-hotkeys/src/HotkeysProvider.tsx#L10)
+
+***
+
+### hotkeySequence?
+
+```ts
+optional hotkeySequence: Partial;
+```
+
+Defined in: [HotkeysProvider.tsx:11](https://github.com/TanStack/hotkeys/blob/main/packages/preact-hotkeys/src/HotkeysProvider.tsx#L11)
diff --git a/docs/framework/preact/reference/interfaces/HotkeysProviderProps.md b/docs/framework/preact/reference/interfaces/HotkeysProviderProps.md
new file mode 100644
index 0000000..2a6e8cc
--- /dev/null
+++ b/docs/framework/preact/reference/interfaces/HotkeysProviderProps.md
@@ -0,0 +1,28 @@
+---
+id: HotkeysProviderProps
+title: HotkeysProviderProps
+---
+
+# Interface: HotkeysProviderProps
+
+Defined in: [HotkeysProvider.tsx:20](https://github.com/TanStack/hotkeys/blob/main/packages/preact-hotkeys/src/HotkeysProvider.tsx#L20)
+
+## Properties
+
+### children
+
+```ts
+children: ComponentChildren;
+```
+
+Defined in: [HotkeysProvider.tsx:21](https://github.com/TanStack/hotkeys/blob/main/packages/preact-hotkeys/src/HotkeysProvider.tsx#L21)
+
+***
+
+### defaultOptions?
+
+```ts
+optional defaultOptions: HotkeysProviderOptions;
+```
+
+Defined in: [HotkeysProvider.tsx:22](https://github.com/TanStack/hotkeys/blob/main/packages/preact-hotkeys/src/HotkeysProvider.tsx#L22)
diff --git a/docs/framework/preact/reference/interfaces/PreactHotkeyRecorder.md b/docs/framework/preact/reference/interfaces/PreactHotkeyRecorder.md
new file mode 100644
index 0000000..d4d369d
--- /dev/null
+++ b/docs/framework/preact/reference/interfaces/PreactHotkeyRecorder.md
@@ -0,0 +1,80 @@
+---
+id: PreactHotkeyRecorder
+title: PreactHotkeyRecorder
+---
+
+# Interface: PreactHotkeyRecorder
+
+Defined in: [useHotkeyRecorder.ts:7](https://github.com/TanStack/hotkeys/blob/main/packages/preact-hotkeys/src/useHotkeyRecorder.ts#L7)
+
+## Properties
+
+### cancelRecording()
+
+```ts
+cancelRecording: () => void;
+```
+
+Defined in: [useHotkeyRecorder.ts:17](https://github.com/TanStack/hotkeys/blob/main/packages/preact-hotkeys/src/useHotkeyRecorder.ts#L17)
+
+Cancel recording without saving
+
+#### Returns
+
+`void`
+
+***
+
+### isRecording
+
+```ts
+isRecording: boolean;
+```
+
+Defined in: [useHotkeyRecorder.ts:9](https://github.com/TanStack/hotkeys/blob/main/packages/preact-hotkeys/src/useHotkeyRecorder.ts#L9)
+
+Whether recording is currently active
+
+***
+
+### recordedHotkey
+
+```ts
+recordedHotkey: Hotkey | null;
+```
+
+Defined in: [useHotkeyRecorder.ts:11](https://github.com/TanStack/hotkeys/blob/main/packages/preact-hotkeys/src/useHotkeyRecorder.ts#L11)
+
+The currently recorded hotkey (for live preview)
+
+***
+
+### startRecording()
+
+```ts
+startRecording: () => void;
+```
+
+Defined in: [useHotkeyRecorder.ts:13](https://github.com/TanStack/hotkeys/blob/main/packages/preact-hotkeys/src/useHotkeyRecorder.ts#L13)
+
+Start recording a new hotkey
+
+#### Returns
+
+`void`
+
+***
+
+### stopRecording()
+
+```ts
+stopRecording: () => void;
+```
+
+Defined in: [useHotkeyRecorder.ts:15](https://github.com/TanStack/hotkeys/blob/main/packages/preact-hotkeys/src/useHotkeyRecorder.ts#L15)
+
+Stop recording (same as cancel)
+
+#### Returns
+
+`void`
diff --git a/docs/framework/preact/reference/interfaces/UseHotkeyOptions.md b/docs/framework/preact/reference/interfaces/UseHotkeyOptions.md
new file mode 100644
index 0000000..649ac84
--- /dev/null
+++ b/docs/framework/preact/reference/interfaces/UseHotkeyOptions.md
@@ -0,0 +1,31 @@
+---
+id: UseHotkeyOptions
+title: UseHotkeyOptions
+---
+
+# Interface: UseHotkeyOptions
+
+Defined in: [useHotkey.ts:18](https://github.com/TanStack/hotkeys/blob/main/packages/preact-hotkeys/src/useHotkey.ts#L18)
+
+## Extends
+
+- `Omit`\<`HotkeyOptions`, `"target"`\>
+
+## Properties
+
+### target?
+
+```ts
+optional target:
+ | HTMLElement
+ | Document
+ | Window
+ | RefObject
+ | null;
+```
+
+Defined in: [useHotkey.ts:24](https://github.com/TanStack/hotkeys/blob/main/packages/preact-hotkeys/src/useHotkey.ts#L24)
+
+The DOM element to attach the event listener to.
+Can be a Preact ref, direct DOM element, or null.
+Defaults to document.
diff --git a/docs/framework/preact/reference/interfaces/UseHotkeySequenceOptions.md b/docs/framework/preact/reference/interfaces/UseHotkeySequenceOptions.md
new file mode 100644
index 0000000..4811d38
--- /dev/null
+++ b/docs/framework/preact/reference/interfaces/UseHotkeySequenceOptions.md
@@ -0,0 +1,31 @@
+---
+id: UseHotkeySequenceOptions
+title: UseHotkeySequenceOptions
+---
+
+# Interface: UseHotkeySequenceOptions
+
+Defined in: [useHotkeySequence.ts:13](https://github.com/TanStack/hotkeys/blob/main/packages/preact-hotkeys/src/useHotkeySequence.ts#L13)
+
+## Extends
+
+- `Omit`\<`SequenceOptions`, `"target"`\>
+
+## Properties
+
+### target?
+
+```ts
+optional target:
+ | HTMLElement
+ | Document
+ | Window
+ | RefObject
+ | null;
+```
+
+Defined in: [useHotkeySequence.ts:22](https://github.com/TanStack/hotkeys/blob/main/packages/preact-hotkeys/src/useHotkeySequence.ts#L22)
+
+The DOM element to attach the event listener to.
+Can be a Preact ref, direct DOM element, or null.
+Defaults to document.
diff --git a/docs/framework/solid/guides/formatting-display.md b/docs/framework/solid/guides/formatting-display.md
new file mode 100644
index 0000000..37b8f99
--- /dev/null
+++ b/docs/framework/solid/guides/formatting-display.md
@@ -0,0 +1,154 @@
+---
+title: Formatting & Display Guide
+id: formatting-display
+---
+
+TanStack Hotkeys provides several utilities for formatting hotkey strings into human-readable display text. These utilities handle platform differences automatically, so your UI shows the right symbols and labels for each operating system.
+
+## `formatForDisplay`
+
+The primary formatting function. Returns a platform-aware string using symbols on macOS and text labels on Windows/Linux.
+
+```tsx
+import { formatForDisplay } from '@tanstack/solid-hotkeys'
+
+// On macOS:
+formatForDisplay('Mod+S') // "⌘S"
+formatForDisplay('Mod+Shift+Z') // "⇧⌘Z"
+formatForDisplay('Control+Alt+D') // "⌃⌥D"
+
+// On Windows/Linux:
+formatForDisplay('Mod+S') // "Ctrl+S"
+formatForDisplay('Mod+Shift+Z') // "Ctrl+Shift+Z"
+formatForDisplay('Control+Alt+D') // "Ctrl+Alt+D"
+```
+
+### Options
+
+```ts
+formatForDisplay('Mod+S', {
+ platform: 'mac', // Override platform detection ('mac' | 'windows' | 'linux')
+})
+```
+
+## `formatWithLabels`
+
+Returns human-readable text labels (e.g., "Cmd" instead of the symbol).
+
+```tsx
+import { formatWithLabels } from '@tanstack/solid-hotkeys'
+
+formatWithLabels('Mod+S') // "Cmd+S" (Mac) / "Ctrl+S" (Windows)
+formatWithLabels('Mod+Shift+Z') // "Cmd+Shift+Z" (Mac) / "Ctrl+Shift+Z" (Windows)
+```
+
+## `formatKeyForDebuggingDisplay`
+
+Returns a rich label for devtools and debugging.
+
+```tsx
+import { formatKeyForDebuggingDisplay } from '@tanstack/solid-hotkeys'
+
+formatKeyForDebuggingDisplay('Meta') // "⌘ Mod (Cmd)" (Mac)
+formatKeyForDebuggingDisplay('Shift') // "⇧ Shift"
+formatKeyForDebuggingDisplay('Control') // "⌃ Control"
+```
+
+## Using Formatted Hotkeys in Solid
+
+### Keyboard Shortcut Badges
+
+```tsx
+import { formatForDisplay } from '@tanstack/solid-hotkeys'
+
+function ShortcutBadge(props: { hotkey: string }) {
+ return {formatForDisplay(props.hotkey)}
+}
+```
+
+### Menu Items with Hotkeys
+
+```tsx
+import { createHotkey, formatForDisplay } from '@tanstack/solid-hotkeys'
+
+function MenuItem(props: {
+ label: string
+ hotkey: string
+ onAction: () => void
+}) {
+ createHotkey(props.hotkey, () => props.onAction())
+
+ return (
+
+ )
+}
+```
+
+### Command Palette Items
+
+```tsx
+import { formatForDisplay } from '@tanstack/solid-hotkeys'
+import type { Hotkey } from '@tanstack/solid-hotkeys'
+
+interface Command {
+ id: string
+ label: string
+ hotkey?: Hotkey
+ action: () => void
+}
+
+function CommandPaletteItem(props: { command: Command }) {
+ return (
+
+ {props.command.label}
+
+ {formatForDisplay(props.command.hotkey!)}
+
+
+ )
+}
+```
+
+## Platform Symbols Reference
+
+| Modifier | Mac Symbol | Windows/Linux Label |
+|----------|-----------|-------------------|
+| Meta (Cmd) | `⌘` | `Win` / `Super` |
+| Control | `⌃` | `Ctrl` |
+| Alt/Option | `⌥` | `Alt` |
+| Shift | `⇧` | `Shift` |
+
+## Parsing and Normalization
+
+### `parseHotkey`
+
+```ts
+import { parseHotkey } from '@tanstack/solid-hotkeys'
+
+const parsed = parseHotkey('Mod+Shift+S')
+// { key: 'S', ctrl: false, shift: true, alt: false, meta: true, modifiers: [...] }
+```
+
+### `normalizeHotkey`
+
+```ts
+import { normalizeHotkey } from '@tanstack/solid-hotkeys'
+
+normalizeHotkey('Cmd+S') // 'Meta+S' (on Mac)
+normalizeHotkey('Ctrl+Shift+s') // 'Control+Shift+S'
+```
+
+## Validation
+
+```ts
+import { validateHotkey } from '@tanstack/solid-hotkeys'
+
+const result = validateHotkey('Alt+A')
+// { valid: true, warnings: [...], errors: [] }
+
+const result2 = validateHotkey('InvalidKey+S')
+// { valid: false, warnings: [], errors: ['Unknown key: InvalidKey'] }
+```
diff --git a/docs/framework/solid/guides/hotkey-recording.md b/docs/framework/solid/guides/hotkey-recording.md
new file mode 100644
index 0000000..91b5c3c
--- /dev/null
+++ b/docs/framework/solid/guides/hotkey-recording.md
@@ -0,0 +1,169 @@
+---
+title: Hotkey Recording Guide
+id: hotkey-recording
+---
+
+TanStack Hotkeys provides the `createHotkeyRecorder` primitive for building keyboard shortcut customization UIs. This lets users record their own shortcuts by pressing the desired key combination, similar to how system preferences or IDE shortcut editors work.
+
+## Basic Usage
+
+```tsx
+import { createHotkeyRecorder, formatForDisplay } from '@tanstack/solid-hotkeys'
+
+function ShortcutRecorder() {
+ const recorder = createHotkeyRecorder({
+ onRecord: (hotkey) => {
+ console.log('Recorded:', hotkey) // e.g., "Mod+Shift+S"
+ },
+ })
+
+ return (
+
+ recorder.isRecording() ? recorder.stopRecording() : recorder.startRecording()}>
+ {recorder.isRecording()
+ ? 'Press a key combination...'
+ : recorder.recordedHotkey()
+ ? formatForDisplay(recorder.recordedHotkey()!)
+ : 'Click to record'}
+
+
+ Cancel
+
+
+ )
+}
+```
+
+> [!NOTE]
+> In Solid, `isRecording` and `recordedHotkey` are **accessors** (signal getters). You must call them with `()` to read the value: `recorder.isRecording()`, `recorder.recordedHotkey()`.
+
+## Return Value
+
+The `createHotkeyRecorder` primitive returns an object with:
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `isRecording` | `() => boolean` | Accessor returning whether the recorder is currently listening |
+| `recordedHotkey` | `() => Hotkey \| null` | Accessor returning the last recorded hotkey, or `null` |
+| `startRecording` | `() => void` | Start listening for key presses |
+| `stopRecording` | `() => void` | Stop listening and keep the recorded hotkey |
+| `cancelRecording` | `() => void` | Stop listening and discard any recorded hotkey |
+
+## Options
+
+```tsx
+createHotkeyRecorder({
+ onRecord: (hotkey) => { /* called when a hotkey is recorded */ },
+ onCancel: () => { /* called when recording is cancelled */ },
+ onClear: () => { /* called when the recorded hotkey is cleared */ },
+})
+```
+
+Options can also be passed as an accessor function for reactive configuration.
+
+### `onRecord`
+
+Called when the user presses a valid key combination. Receives the recorded `Hotkey` string.
+
+### `onCancel`
+
+Called when recording is cancelled (Escape or `cancelRecording()`).
+
+### `onClear`
+
+Called when the recorded hotkey is cleared (Backspace or Delete during recording).
+
+### Global Default Options via Provider
+
+```tsx
+import { HotkeysProvider } from '@tanstack/solid-hotkeys'
+
+ console.log('Recording cancelled'),
+ },
+ }}
+>
+
+
+```
+
+## Recording Behavior
+
+| Key | Behavior |
+|-----|----------|
+| **Modifier only** | Waits for a non-modifier key |
+| **Modifier + key** | Records the full combination |
+| **Single key** | Records the single key |
+| **Escape** | Cancels the recording |
+| **Backspace / Delete** | Clears the currently recorded hotkey |
+
+### Mod Auto-Conversion
+
+Recorded hotkeys automatically use the portable `Mod` format (Command on Mac, Control elsewhere).
+
+## Building a Shortcut Settings UI
+
+```tsx
+import { createSignal } from 'solid-js'
+import {
+ createHotkey,
+ createHotkeyRecorder,
+ formatForDisplay,
+} from '@tanstack/solid-hotkeys'
+import type { Hotkey } from '@tanstack/solid-hotkeys'
+
+function ShortcutSettings() {
+ const [shortcuts, setShortcuts] = createSignal>({
+ save: 'Mod+S',
+ undo: 'Mod+Z',
+ search: 'Mod+K',
+ })
+
+ const [editingAction, setEditingAction] = createSignal(null)
+
+ const recorder = createHotkeyRecorder({
+ onRecord: (hotkey) => {
+ const action = editingAction()
+ if (action) {
+ setShortcuts((prev) => ({ ...prev, [action]: hotkey }))
+ setEditingAction(null)
+ }
+ },
+ onCancel: () => setEditingAction(null),
+ })
+
+ // Register the actual hotkeys with their current bindings
+ createHotkey(() => shortcuts().save, () => save())
+ createHotkey(() => shortcuts().undo, () => undo())
+ createHotkey(() => shortcuts().search, () => openSearch())
+
+ return (
+
+
Keyboard Shortcuts
+
+ {([action, hotkey]) => (
+
+ {action}
+ {
+ setEditingAction(action)
+ recorder.startRecording()
+ }}
+ >
+ {editingAction() === action && recorder.isRecording()
+ ? 'Press keys...'
+ : formatForDisplay(hotkey)}
+
+
+ )}
+
+
+ )
+}
+```
+
+## Under the Hood
+
+The `createHotkeyRecorder` primitive creates a `HotkeyRecorder` class instance and subscribes to its reactive state via `@tanstack/solid-store`. The class manages its own keyboard event listeners and state, and the primitive handles cleanup when the component is disposed.
diff --git a/docs/framework/solid/guides/hotkeys.md b/docs/framework/solid/guides/hotkeys.md
new file mode 100644
index 0000000..e84b8fd
--- /dev/null
+++ b/docs/framework/solid/guides/hotkeys.md
@@ -0,0 +1,241 @@
+---
+title: Hotkeys Guide
+id: hotkeys
+---
+
+The `createHotkey` primitive is the primary way to register keyboard shortcuts in SolidJS applications. It wraps the singleton `HotkeyManager` with automatic lifecycle management and reactive option support.
+
+## Basic Usage
+
+```tsx
+import { createHotkey } from '@tanstack/solid-hotkeys'
+
+function App() {
+ createHotkey('Mod+S', () => {
+ saveDocument()
+ }, {
+ // override the default options here
+ })
+}
+```
+
+The callback receives the original `KeyboardEvent` as the first argument and a `HotkeyCallbackContext` as the second:
+
+```tsx
+createHotkey('Mod+S', (event, context) => {
+ console.log(context.hotkey) // 'Mod+S'
+ console.log(context.parsedHotkey) // { key: 'S', ctrl: false, shift: false, alt: false, meta: true, modifiers: ['Meta'] }
+})
+```
+
+You can pass a hotkey as a string or as a `RawHotkey` object (modifier booleans optional). Use `mod` for cross-platform shortcuts (Command on Mac, Control elsewhere):
+
+```tsx
+createHotkey('Mod+S', () => save())
+createHotkey({ key: 'S', mod: true }, () => save()) // Same as above
+createHotkey({ key: 'Escape' }, () => closeModal())
+createHotkey({ key: 'S', ctrl: true, shift: true }, () => saveAs())
+createHotkey({ key: 'S', mod: true, shift: true }, () => saveAs())
+```
+
+## Reactive Options
+
+Unlike React/Preact hooks, Solid primitives accept **accessor functions** for reactive options. Pass a function that returns the options object to have the hotkey automatically update when dependencies change:
+
+```tsx
+function Modal(props) {
+ createHotkey('Escape', () => props.onClose(), () => ({
+ enabled: props.isOpen,
+ }))
+
+ return (
+
+ ...
+
+ )
+}
+```
+
+For scoped targets, use an accessor so the hotkey waits for the element to be attached:
+
+```tsx
+function Editor() {
+ const [editorRef, setEditorRef] = createSignal(null)
+
+ createHotkey('Mod+S', save, () => ({ target: editorRef() }))
+
+ return ...
+}
+```
+
+## Default Options
+
+When you register a hotkey without passing options, or when you omit specific options, the following defaults apply:
+
+```tsx
+createHotkey('Mod+S', callback, {
+ enabled: true,
+ preventDefault: true,
+ stopPropagation: true,
+ eventType: 'keydown',
+ requireReset: false,
+ ignoreInputs: undefined, // smart default: false for Mod+S, true for single keys
+ target: document,
+ platform: undefined, // auto-detected
+ conflictBehavior: 'warn',
+})
+```
+
+### Why These Defaults?
+
+Most hotkey registrations are intended to override default browser behavior—such as using `Mod+S` to save a document instead of showing the browser's "Save Page" dialog. To make this easy and consistent, the library sets `preventDefault` and `stopPropagation` to `true` by default, ensuring your hotkey handlers take precedence and reducing the amount of repetitive boilerplate code required.
+
+#### Smart Input Handling: `ignoreInputs`
+
+The `ignoreInputs` option is designed to strike a balance between accessibility and usability. By default, hotkeys involving `Ctrl`/`Meta` modifiers (like `Mod+S`) and the `Escape` key are allowed to fire even when the focus is inside input elements (such as text fields or text areas), and when focused on button-type inputs (`type="button"`, `"submit"`, or `"reset"`). This allows shortcuts like save or close to work wherever the user is focused. On the other hand, single key shortcuts or those using only `Shift`/`Alt` are ignored within non-button inputs to prevent interference with normal typing.
+
+#### Hotkey Conflicts: `conflictBehavior`
+
+When you attempt to register a hotkey that is already registered (possibly in another part of your app), the library logs a warning by default using the `conflictBehavior: 'warn'` setting. This helps you catch accidental duplicate bindings during development so they can be resolved before reaching production.
+
+### Global Default Options via Provider
+
+You can change the default options for all `createHotkey` calls in your app by wrapping your component tree with `HotkeysProvider`. Per-primitive options will override the provider defaults.
+
+```tsx
+import { HotkeysProvider } from '@tanstack/solid-hotkeys'
+
+
+
+
+```
+
+## Hotkey Options
+
+### `enabled`
+
+Controls whether the hotkey is active. Defaults to `true`. Use an accessor for reactive control.
+
+```tsx
+const [isEditing, setIsEditing] = createSignal(false)
+
+createHotkey('Mod+S', () => save(), () => ({ enabled: isEditing() }))
+```
+
+### `preventDefault`
+
+Automatically calls `event.preventDefault()` when the hotkey fires. Defaults to `true`.
+
+```tsx
+createHotkey('Mod+S', () => save())
+createHotkey('Mod+S', () => save(), { preventDefault: false })
+```
+
+### `stopPropagation`
+
+Calls `event.stopPropagation()` when the hotkey fires. Defaults to `true`.
+
+```tsx
+createHotkey('Escape', () => closeModal())
+createHotkey('Escape', () => closeModal(), { stopPropagation: false })
+```
+
+### `eventType`
+
+Whether to listen on `keydown` (default) or `keyup`.
+
+```tsx
+createHotkey('Shift', () => deactivateMode(), { eventType: 'keyup' })
+```
+
+### `requireReset`
+
+When `true`, the hotkey will only fire once per key press. The key must be released and pressed again to fire again. Defaults to `false`.
+
+```tsx
+createHotkey('Escape', () => closePanel(), { requireReset: true })
+```
+
+### `ignoreInputs`
+
+When `true`, the hotkey will not fire when the user is focused on a text input, textarea, select, or contentEditable element. When unset, a smart default applies based on the hotkey type.
+
+```tsx
+createHotkey('K', () => openSearch()) // Smart default: ignored in inputs
+createHotkey('Mod+S', () => save()) // Smart default: fires in inputs
+createHotkey('Enter', () => submit(), { ignoreInputs: false })
+```
+
+### `target`
+
+The DOM element to attach the event listener to. Defaults to `document`. Can be a DOM element, `document`, `window`, or from an accessor for reactive targets.
+
+```tsx
+const [panelRef, setPanelRef] = createSignal(null)
+
+createHotkey('Escape', () => closePanel(), () => ({ target: panelRef() }))
+
+return ...
+```
+
+> [!NOTE]
+> When using an accessor for the target, the primitive waits for the element to be available before registering. Ensure the element is focusable (has `tabIndex`) so it can receive keyboard events.
+
+### `conflictBehavior`
+
+Controls what happens when you register a hotkey that's already registered. Options: `'warn'`, `'error'`, `'replace'`, `'allow'`.
+
+```tsx
+createHotkey('Mod+S', () => save(), { conflictBehavior: 'replace' })
+```
+
+### `platform`
+
+Override the auto-detected platform.
+
+```tsx
+createHotkey('Mod+S', () => save(), { platform: 'mac' })
+```
+
+## Automatic Dependency Tracking
+
+Solid's fine-grained reactivity means `createHotkey` automatically tracks reactive dependencies. The callback always has access to the latest signal values:
+
+```tsx
+function Counter() {
+ const [count, setCount] = createSignal(0)
+
+ createHotkey('Mod+Shift+C', () => {
+ console.log('Current count:', count())
+ })
+
+ return setCount(c => c + 1)}>Count: {count()}
+}
+```
+
+## Automatic Cleanup
+
+The primitive automatically unregisters the hotkey when the component unmounts (when the owning reactive scope is disposed):
+
+```tsx
+function TemporaryPanel() {
+ createHotkey('Escape', () => closePanel())
+ return Panel content
+}
+```
+
+## The Hotkey Manager
+
+Under the hood, `createHotkey` uses the singleton `HotkeyManager`. You can also access the manager directly if needed:
+
+```tsx
+import { getHotkeyManager } from '@tanstack/solid-hotkeys'
+
+const manager = getHotkeyManager()
+manager.isRegistered('Mod+S')
+manager.getRegistrationCount()
+```
diff --git a/docs/framework/solid/guides/key-state-tracking.md b/docs/framework/solid/guides/key-state-tracking.md
new file mode 100644
index 0000000..92768f2
--- /dev/null
+++ b/docs/framework/solid/guides/key-state-tracking.md
@@ -0,0 +1,188 @@
+---
+title: Key State Tracking Guide
+id: key-state-tracking
+---
+
+TanStack Hotkeys provides three primitives for tracking the real-time state of keyboard keys. These are useful for building UIs that respond to modifier keys being held, displaying active key states, or implementing hold-to-activate features.
+
+## `createHeldKeys`
+
+Returns an accessor that yields an array of all currently held key names.
+
+```tsx
+import { createHeldKeys } from '@tanstack/solid-hotkeys'
+
+function KeyDisplay() {
+ const heldKeys = createHeldKeys()
+
+ return (
+
+ {heldKeys().length > 0
+ ? `Held: ${heldKeys().join(' + ')}`
+ : 'No keys held'}
+
+ )
+}
+```
+
+> [!NOTE]
+> In Solid, `createHeldKeys()` returns an **accessor function**. Call it with `()` to read the current value: `heldKeys()`.
+
+The returned array contains key names like `'Shift'`, `'Control'`, `'Meta'`, `'A'`, `'ArrowUp'`, etc. Keys appear in the order they were pressed.
+
+## `createHeldKeyCodes`
+
+Returns an accessor that yields a reactive object mapping held key names to their physical key codes (`event.code` values). Useful for distinguishing between left and right modifiers.
+
+```tsx
+import { createHeldKeyCodes } from '@tanstack/solid-hotkeys'
+
+function KeyCodeDisplay() {
+ const heldCodes = createHeldKeyCodes()
+ // Example: { Shift: "ShiftLeft", Control: "ControlRight" }
+
+ return (
+
+
+ {([key, code]) => (
+
+ {key}: {code}
+
+ )}
+
+
+ )
+}
+```
+
+## `createKeyHold`
+
+Returns an accessor that indicates whether a specific key is currently held. Optimized to only trigger updates when the specified key's held state changes. The `key` argument can be a string or an accessor for reactive keys.
+
+```tsx
+import { createKeyHold } from '@tanstack/solid-hotkeys'
+
+function ModifierIndicators() {
+ const isShiftHeld = createKeyHold('Shift')
+ const isCtrlHeld = createKeyHold('Control')
+ const isAltHeld = createKeyHold('Alt')
+ const isMetaHeld = createKeyHold('Meta')
+
+ return (
+
+ Shift
+ Ctrl
+ Alt
+ Meta
+
+ )
+}
+```
+
+## Common Patterns
+
+### Hold-to-Reveal UI
+
+```tsx
+import { createKeyHold } from '@tanstack/solid-hotkeys'
+
+function FileItem(props: { file: File }) {
+ const isShiftHeld = createKeyHold('Shift')
+
+ return (
+
+ {props.file.name}
+
+ permanentlyDelete(props.file)}>
+ Permanently Delete
+
+
+
+ moveToTrash(props.file)}>
+ Move to Trash
+
+
+
+ )
+}
+```
+
+### Keyboard Shortcut Hints
+
+```tsx
+import { createKeyHold } from '@tanstack/solid-hotkeys'
+
+function ShortcutHints() {
+ const isModHeld = createKeyHold('Meta')
+
+ return (
+
+
+
S - Save
+
Z - Undo
+
Shift+Z - Redo
+
K - Command Palette
+
+
+ )
+}
+```
+
+### Debugging Key Display
+
+```tsx
+import {
+ createHeldKeys,
+ createHeldKeyCodes,
+ formatKeyForDebuggingDisplay,
+} from '@tanstack/solid-hotkeys'
+
+function KeyDebugger() {
+ const heldKeys = createHeldKeys()
+ const heldCodes = createHeldKeyCodes()
+
+ return (
+
+
Active Keys
+
+ {(key) => (
+
+ {formatKeyForDebuggingDisplay(key)}
+ {heldCodes()[key]}
+
+ )}
+
+
+ Press any key...
+
+
+ )
+}
+```
+
+## Platform Quirks
+
+The underlying `KeyStateTracker` handles several platform-specific issues:
+
+### macOS Modifier Key Behavior
+
+On macOS, when a modifier key is held and a non-modifier key is pressed, the OS sometimes swallows the `keyup` event. TanStack Hotkeys detects and handles this automatically.
+
+### Window Blur
+
+When the browser window loses focus, all held keys are automatically cleared.
+
+## Under the Hood
+
+All three primitives subscribe to the singleton `KeyStateTracker` via `@tanstack/solid-store`. The tracker manages its own event listeners on `document` and maintains state in a TanStack Store.
+
+```tsx
+import { getKeyStateTracker } from '@tanstack/solid-hotkeys'
+
+const tracker = getKeyStateTracker()
+
+tracker.getHeldKeys() // string[]
+tracker.isKeyHeld('Shift') // boolean
+tracker.isAnyKeyHeld(['Shift', 'Control']) // boolean
+tracker.areAllKeysHeld(['Shift', 'Control']) // boolean
+```
diff --git a/docs/framework/solid/guides/sequences.md b/docs/framework/solid/guides/sequences.md
new file mode 100644
index 0000000..cf5c583
--- /dev/null
+++ b/docs/framework/solid/guides/sequences.md
@@ -0,0 +1,170 @@
+---
+title: Sequences Guide
+id: sequences
+---
+
+TanStack Hotkeys supports multi-key sequences -- shortcuts where you press keys one after another rather than simultaneously. This is commonly used for Vim-style navigation, cheat codes, or multi-step commands.
+
+## Basic Usage
+
+Use the `createHotkeySequence` primitive to register a key sequence:
+
+```tsx
+import { createHotkeySequence } from '@tanstack/solid-hotkeys'
+
+function App() {
+ // Vim-style: press g then g to scroll to top
+ createHotkeySequence(['G', 'G'], () => {
+ window.scrollTo({ top: 0, behavior: 'smooth' })
+ })
+}
+```
+
+The first argument is an array of `Hotkey` strings representing each step in the sequence. The user must press them in order within the timeout window.
+
+## Reactive Options
+
+Solid's `createHotkeySequence` accepts **accessor functions** for reactive sequence and options:
+
+```tsx
+const [isVimMode, setIsVimMode] = createSignal(true)
+const [sequence] = createSignal(['G', 'G'] as const)
+
+createHotkeySequence(
+ sequence,
+ () => scrollToTop(),
+ () => ({ enabled: isVimMode(), timeout: 1500 }),
+)
+```
+
+## Sequence Options
+
+The third argument is an options object (or accessor returning options):
+
+```tsx
+createHotkeySequence(['G', 'G'], callback, {
+ timeout: 1000, // Time allowed between keys (ms)
+ enabled: true, // Whether the sequence is active
+ target: document, // Or from an accessor for scoped sequences
+})
+```
+
+### `timeout`
+
+The maximum time (in milliseconds) allowed between consecutive key presses. Defaults to `1000` (1 second).
+
+```tsx
+createHotkeySequence(['D', 'D'], () => deleteLine(), { timeout: 500 })
+createHotkeySequence(['Shift+Z', 'Shift+Z'], () => forceQuit(), { timeout: 2000 })
+```
+
+### `enabled`
+
+Controls whether the sequence is active. Defaults to `true`. Use an accessor for reactive control.
+
+```tsx
+const [isVimMode, setIsVimMode] = createSignal(true)
+
+createHotkeySequence(['G', 'G'], () => scrollToTop(), () => ({
+ enabled: isVimMode(),
+}))
+```
+
+### `target`
+
+The DOM element to attach the sequence listener to. Defaults to `document`. Can be from an accessor when the target becomes available after mount.
+
+### Global Default Options via Provider
+
+```tsx
+import { HotkeysProvider } from '@tanstack/solid-hotkeys'
+
+
+
+
+```
+
+## Sequences with Modifiers
+
+Each step in a sequence can include modifiers:
+
+```tsx
+createHotkeySequence(['Mod+K', 'Mod+C'], () => commentSelection())
+createHotkeySequence(['G', 'Shift+G'], () => scrollToBottom())
+```
+
+## Common Sequence Patterns
+
+### Vim-Style Navigation
+
+```tsx
+function VimNavigation() {
+ createHotkeySequence(['G', 'G'], () => scrollToTop())
+ createHotkeySequence(['G', 'Shift+G'], () => scrollToBottom())
+ createHotkeySequence(['D', 'D'], () => deleteLine())
+ createHotkeySequence(['D', 'W'], () => deleteWord())
+ createHotkeySequence(['C', 'I', 'W'], () => changeInnerWord())
+}
+```
+
+### Konami Code
+
+```tsx
+createHotkeySequence(
+ [
+ 'ArrowUp', 'ArrowUp',
+ 'ArrowDown', 'ArrowDown',
+ 'ArrowLeft', 'ArrowRight',
+ 'ArrowLeft', 'ArrowRight',
+ 'B', 'A',
+ ],
+ () => enableEasterEgg(),
+ { timeout: 2000 },
+)
+```
+
+### Multi-Step Commands
+
+```tsx
+createHotkeySequence(['H', 'E', 'L', 'P'], () => openHelp())
+```
+
+## How Sequences Work
+
+The `SequenceManager` (singleton) handles all sequence registrations. When a key is pressed:
+
+1. It checks if the key matches the next expected step in any registered sequence
+2. If it matches, the sequence advances to the next step
+3. If the timeout expires between steps, the sequence resets
+4. When all steps are completed, the callback fires
+
+### Overlapping Sequences
+
+Multiple sequences can share the same prefix. The manager tracks progress for each sequence independently:
+
+```tsx
+createHotkeySequence(['D', 'D'], () => deleteLine())
+createHotkeySequence(['D', 'W'], () => deleteWord())
+createHotkeySequence(['D', 'I', 'W'], () => deleteInnerWord())
+```
+
+## The Sequence Manager
+
+Under the hood, `createHotkeySequence` uses the singleton `SequenceManager`. You can also use the core `createSequenceMatcher` function for standalone sequence matching:
+
+```tsx
+import { createSequenceMatcher } from '@tanstack/solid-hotkeys'
+
+const matcher = createSequenceMatcher(['G', 'G'], { timeout: 1000 })
+
+document.addEventListener('keydown', (e) => {
+ if (matcher.match(e)) {
+ console.log('Sequence completed!')
+ }
+ console.log('Progress:', matcher.getProgress())
+})
+```
diff --git a/docs/installation.md b/docs/installation.md
index b92c872..0053f4c 100644
--- a/docs/installation.md
+++ b/docs/installation.md
@@ -8,6 +8,7 @@ TanStack Hotkeys is compatible with various front-end frameworks. Install the co
react: @tanstack/react-hotkeys
+preact: @tanstack/preact-hotkeys
@@ -40,3 +41,4 @@ react: @tanstack/react-hotkeys-devtools
See the [devtools](./devtools) documentation for more information on how to set up and use the Hotkeys devtools.
+
diff --git a/examples/preact/useHeldKeys/eslint.config.js b/examples/preact/useHeldKeys/eslint.config.js
new file mode 100644
index 0000000..3fd4ac4
--- /dev/null
+++ b/examples/preact/useHeldKeys/eslint.config.js
@@ -0,0 +1,11 @@
+// @ts-check
+
+import rootConfig from '../../../eslint.config.js'
+
+/** @type {import('eslint').Linter.Config[]} */
+export default [
+ {
+ ignores: ['eslint.config.js'],
+ },
+ ...rootConfig,
+]
diff --git a/examples/preact/useHeldKeys/index.html b/examples/preact/useHeldKeys/index.html
new file mode 100644
index 0000000..6c2cce1
--- /dev/null
+++ b/examples/preact/useHeldKeys/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+ useHeldKeys - TanStack Hotkeys React Example
+
+
+ You need to enable JavaScript to run this app.
+
+
+
+
diff --git a/examples/preact/useHeldKeys/package.json b/examples/preact/useHeldKeys/package.json
new file mode 100644
index 0000000..0f88274
--- /dev/null
+++ b/examples/preact/useHeldKeys/package.json
@@ -0,0 +1,24 @@
+{
+ "name": "@tanstack/hotkeys-example-preact-use-held-hotkeys",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vite --port=3069",
+ "build": "vite build",
+ "preview": "vite preview",
+ "lint": "eslint .",
+ "lint:fix": "eslint . --fix",
+ "test:types": "tsc"
+ },
+ "dependencies": {
+ "@tanstack/preact-hotkeys": "^0.3.0",
+ "preact": "^10.27.2"
+ },
+ "devDependencies": {
+ "@preact/preset-vite": "^2.10.2",
+ "@tanstack/preact-devtools": "0.9.6",
+ "@tanstack/preact-hotkeys-devtools": "^0.3.0",
+ "typescript": "5.9.3",
+ "vite": "^7.3.1"
+ }
+}
diff --git a/examples/preact/useHeldKeys/src/index.css b/examples/preact/useHeldKeys/src/index.css
new file mode 100644
index 0000000..5f83d60
--- /dev/null
+++ b/examples/preact/useHeldKeys/src/index.css
@@ -0,0 +1,131 @@
+* {
+ box-sizing: border-box;
+}
+body {
+ margin: 0;
+ font-family:
+ system-ui,
+ -apple-system,
+ sans-serif;
+ background: #f5f5f5;
+ color: #333;
+}
+.app {
+ max-width: 800px;
+ margin: 0 auto;
+ padding: 20px;
+}
+header {
+ text-align: center;
+ margin-bottom: 40px;
+}
+header h1 {
+ margin: 0 0 10px;
+ color: #0066cc;
+}
+header p {
+ color: #666;
+ margin: 0;
+ max-width: 500px;
+ margin: 0 auto;
+}
+.demo-section {
+ background: white;
+ border-radius: 12px;
+ padding: 24px;
+ margin-bottom: 24px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+}
+.demo-section h2 {
+ margin: 0 0 16px;
+ font-size: 20px;
+}
+.demo-section ul {
+ margin: 0;
+ padding-left: 20px;
+}
+.demo-section li {
+ margin-bottom: 8px;
+}
+kbd {
+ background: linear-gradient(180deg, #f8f8f8 0%, #e8e8e8 100%);
+ border: 1px solid #ccc;
+ border-bottom-width: 2px;
+ border-radius: 4px;
+ padding: 2px 8px;
+ font-family: monospace;
+ font-size: 13px;
+}
+kbd.large {
+ font-size: 24px;
+ padding: 8px 16px;
+ display: inline-flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 2px;
+}
+kbd.large .code-label {
+ display: block;
+ font-size: 11px;
+ color: #888;
+ font-weight: normal;
+}
+.key-display {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 12px;
+ min-height: 80px;
+ flex-wrap: wrap;
+ background: #f8f9fa;
+ border-radius: 8px;
+ padding: 20px;
+}
+.key-display .plus {
+ font-size: 24px;
+ color: #666;
+}
+.placeholder {
+ color: #999;
+ font-style: italic;
+}
+.stats {
+ text-align: center;
+ margin-top: 16px;
+ font-size: 16px;
+ color: #666;
+}
+.code-block {
+ background: #1e1e1e;
+ color: #d4d4d4;
+ padding: 16px;
+ border-radius: 8px;
+ overflow-x: auto;
+ font-size: 13px;
+ line-height: 1.5;
+}
+.history-list {
+ list-style: none;
+ padding: 0;
+ margin: 0 0 16px;
+}
+.history-list li {
+ padding: 8px 12px;
+ background: #f0f0f0;
+ border-radius: 4px;
+ margin-bottom: 4px;
+ font-family: monospace;
+ font-size: 14px;
+}
+button {
+ background: #0066cc;
+ color: white;
+ border: none;
+ padding: 10px 20px;
+ border-radius: 6px;
+ cursor: pointer;
+ font-size: 14px;
+}
+button:hover {
+ background: #0052a3;
+}
diff --git a/examples/preact/useHeldKeys/src/index.tsx b/examples/preact/useHeldKeys/src/index.tsx
new file mode 100644
index 0000000..9ddaf6c
--- /dev/null
+++ b/examples/preact/useHeldKeys/src/index.tsx
@@ -0,0 +1,150 @@
+import React from 'preact/compat'
+import { render } from 'preact'
+import {
+ formatKeyForDebuggingDisplay,
+ useHeldKeys,
+ useHeldKeyCodes,
+} from '@tanstack/preact-hotkeys'
+import { HotkeysProvider } from '@tanstack/preact-hotkeys'
+import { hotkeysDevtoolsPlugin } from '@tanstack/preact-hotkeys-devtools'
+import { TanStackDevtools } from '@tanstack/preact-devtools'
+import './index.css'
+
+function App() {
+ const heldKeys = useHeldKeys()
+ const heldCodes = useHeldKeyCodes()
+
+ // Track history of key combinations
+ const [history, setHistory] = React.useState>([])
+
+ React.useEffect(() => {
+ if (heldKeys.length > 0) {
+ const combo = heldKeys
+ .map((k) => formatKeyForDebuggingDisplay(k))
+ .join(' + ')
+ setHistory((h) => {
+ // Only add if different from last entry
+ if (h[h.length - 1] !== combo) {
+ return [...h.slice(-9), combo]
+ }
+ return h
+ })
+ }
+ }, [heldKeys])
+
+ console.log('heldKeys', heldKeys)
+
+ return (
+
+
+
+
+
+ Currently Held Keys
+
+ {heldKeys.length > 0 ? (
+ heldKeys.map((key, index) => {
+ const code = heldCodes[key]
+ return (
+
+ {index > 0 && + }
+
+ {formatKeyForDebuggingDisplay(key)}
+ {code && code !== key && (
+
+ {formatKeyForDebuggingDisplay(code, {
+ source: 'code',
+ })}
+
+ )}
+
+
+ )
+ })
+ ) : (
+ Press any keys...
+ )}
+
+
+ Keys held: {heldKeys.length}
+
+
+
+
+ Usage
+ {`import { useHeldKeys } from '@tanstack/preact-hotkeys'
+
+function KeyDisplay() {
+ const heldKeys = useHeldKeys()
+
+ return (
+
+ Currently pressed: {heldKeys.join(' + ') || 'None'}
+
+ )
+}`}
+
+
+
+ Try These Combinations
+
+
+ Hold Shift + Control + A
+
+ Press multiple letter keys at once
+ Hold modifiers and watch them appear
+ Release keys one by one
+
+
+
+
+ Recent Combinations
+ {history.length > 0 ? (
+
+ {history.map((combo, i) => (
+ {combo}
+ ))}
+
+ ) : (
+ Press some key combinations...
+ )}
+ setHistory([])}>Clear History
+
+
+
+ Use Cases
+
+ Building a keyboard shortcut recorder
+ Displaying currently held keys to users
+ Debugging keyboard input
+ Creating key combination tutorials
+
+
+
+
+ )
+}
+
+// TanStackDevtools as sibling of App to avoid Preact hook errors when hotkeys update state
+const devtoolsPlugins = [hotkeysDevtoolsPlugin()]
+
+render(
+ // optionally, provide default options to an optional HotkeysProvider
+
+
+
+ ,
+ document.getElementById('root')!,
+)
diff --git a/examples/preact/useHeldKeys/tsconfig.json b/examples/preact/useHeldKeys/tsconfig.json
new file mode 100644
index 0000000..faa3381
--- /dev/null
+++ b/examples/preact/useHeldKeys/tsconfig.json
@@ -0,0 +1,20 @@
+{
+ "compilerOptions": {
+ "target": "ESNext",
+ "lib": ["DOM", "DOM.Iterable", "ESNext"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+ "moduleResolution": "Bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+ "jsxImportSource": "preact"
+ },
+ "include": ["src", "vite.config.ts"]
+}
diff --git a/examples/preact/useHeldKeys/vite.config.ts b/examples/preact/useHeldKeys/vite.config.ts
new file mode 100644
index 0000000..bfe110c
--- /dev/null
+++ b/examples/preact/useHeldKeys/vite.config.ts
@@ -0,0 +1,6 @@
+import { defineConfig } from 'vite'
+import preact from '@preact/preset-vite'
+
+export default defineConfig({
+ plugins: [preact()],
+})
diff --git a/examples/preact/useHotkey/eslint.config.js b/examples/preact/useHotkey/eslint.config.js
new file mode 100644
index 0000000..3fd4ac4
--- /dev/null
+++ b/examples/preact/useHotkey/eslint.config.js
@@ -0,0 +1,11 @@
+// @ts-check
+
+import rootConfig from '../../../eslint.config.js'
+
+/** @type {import('eslint').Linter.Config[]} */
+export default [
+ {
+ ignores: ['eslint.config.js'],
+ },
+ ...rootConfig,
+]
diff --git a/examples/preact/useHotkey/index.html b/examples/preact/useHotkey/index.html
new file mode 100644
index 0000000..10fc3f3
--- /dev/null
+++ b/examples/preact/useHotkey/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+ useHotkey - TanStack Hotkeys React Example
+
+
+ You need to enable JavaScript to run this app.
+
+
+
+
diff --git a/examples/preact/useHotkey/package.json b/examples/preact/useHotkey/package.json
new file mode 100644
index 0000000..1c27c64
--- /dev/null
+++ b/examples/preact/useHotkey/package.json
@@ -0,0 +1,24 @@
+{
+ "name": "@tanstack/hotkeys-example-preact-use-hotkey",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vite --port=3069",
+ "build": "vite build",
+ "preview": "vite preview",
+ "lint": "eslint .",
+ "lint:fix": "eslint . --fix",
+ "test:types": "tsc"
+ },
+ "dependencies": {
+ "@tanstack/preact-hotkeys": "^0.3.0",
+ "preact": "^10.27.2"
+ },
+ "devDependencies": {
+ "@preact/preset-vite": "^2.10.2",
+ "@tanstack/preact-devtools": "0.9.6",
+ "@tanstack/preact-hotkeys-devtools": "^0.3.0",
+ "typescript": "5.9.3",
+ "vite": "^7.3.1"
+ }
+}
diff --git a/examples/preact/useHotkey/src/index.css b/examples/preact/useHotkey/src/index.css
new file mode 100644
index 0000000..e9f3ca9
--- /dev/null
+++ b/examples/preact/useHotkey/src/index.css
@@ -0,0 +1,212 @@
+* {
+ box-sizing: border-box;
+}
+body {
+ margin: 0;
+ font-family:
+ system-ui,
+ -apple-system,
+ sans-serif;
+ background: #f5f5f5;
+ color: #333;
+}
+.app {
+ max-width: 800px;
+ margin: 0 auto;
+ padding: 20px;
+}
+header {
+ text-align: center;
+ margin-bottom: 40px;
+}
+header h1 {
+ margin: 0 0 10px;
+ color: #0066cc;
+}
+header p {
+ color: #666;
+ margin: 0;
+}
+.demo-section {
+ background: white;
+ border-radius: 12px;
+ padding: 24px;
+ margin-bottom: 24px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+}
+.demo-section h2 {
+ margin: 0 0 12px;
+ font-size: 20px;
+}
+.demo-section p {
+ margin: 0 0 12px;
+}
+kbd {
+ background: linear-gradient(180deg, #f8f8f8 0%, #e8e8e8 100%);
+ border: 1px solid #ccc;
+ border-bottom-width: 2px;
+ border-radius: 4px;
+ padding: 2px 8px;
+ font-family: monospace;
+ font-size: 13px;
+}
+.counter {
+ font-size: 28px;
+ font-weight: bold;
+ color: #0066cc;
+ margin: 16px 0;
+}
+.hint {
+ font-size: 13px;
+ color: #888;
+ font-style: italic;
+}
+.info-box {
+ background: #e3f2fd;
+ border-radius: 8px;
+ padding: 12px 16px;
+ margin: 20px 0;
+}
+button {
+ background: #0066cc;
+ color: white;
+ border: none;
+ padding: 10px 20px;
+ border-radius: 6px;
+ cursor: pointer;
+ font-size: 14px;
+}
+button:hover {
+ background: #0052a3;
+}
+.code-block {
+ background: #1e1e1e;
+ color: #d4d4d4;
+ padding: 16px;
+ border-radius: 8px;
+ overflow-x: auto;
+ font-size: 13px;
+ line-height: 1.5;
+ margin-top: 16px;
+}
+.hotkey-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: 12px;
+ margin: 16px 0;
+}
+.hotkey-grid > div {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 8px 12px;
+ background: #f8f9fa;
+ border-radius: 6px;
+ font-size: 14px;
+}
+.hotkey-grid kbd {
+ flex-shrink: 0;
+}
+
+/* Scoped shortcuts section */
+.scoped-section {
+ margin-top: 40px;
+}
+
+.scoped-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+ gap: 24px;
+ margin: 24px 0;
+}
+
+.scoped-area {
+ background: #f8f9fa;
+ border: 2px dashed #0066cc;
+ border-radius: 8px;
+ padding: 20px;
+ position: relative;
+}
+
+.scoped-area:focus-within {
+ border-color: #0052a3;
+ border-style: solid;
+ background: #f0f7ff;
+ box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.1);
+}
+
+.scoped-area h3 {
+ margin: 0 0 12px;
+ font-size: 18px;
+ color: #0066cc;
+}
+
+.scoped-area .hotkey-list {
+ margin: 12px 0;
+}
+
+.scoped-area .hotkey-list > div {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 6px 0;
+ font-size: 14px;
+}
+
+.scoped-editor {
+ width: 100%;
+ margin: 12px 0;
+ padding: 12px;
+ border: 1px solid #ddd;
+ border-radius: 6px;
+ font-family: 'Courier New', monospace;
+ font-size: 14px;
+ resize: vertical;
+ min-height: 120px;
+}
+
+.scoped-editor:focus {
+ outline: 2px solid #0066cc;
+ outline-offset: 2px;
+ border-color: #0066cc;
+}
+
+/* Modal styles */
+.modal-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0, 0, 0, 0.5);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 1000;
+}
+
+.modal-content {
+ background: white;
+ border-radius: 12px;
+ padding: 24px;
+ max-width: 500px;
+ width: 90%;
+ max-height: 80vh;
+ overflow-y: auto;
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
+}
+
+.modal-content:focus {
+ outline: 3px solid #0066cc;
+ outline-offset: 2px;
+}
+
+.modal-content h3 {
+ margin: 0 0 16px;
+ font-size: 20px;
+ color: #0066cc;
+}
+
+.modal-content button {
+ margin-top: 16px;
+}
diff --git a/examples/preact/useHotkey/src/index.tsx b/examples/preact/useHotkey/src/index.tsx
new file mode 100644
index 0000000..6941eee
--- /dev/null
+++ b/examples/preact/useHotkey/src/index.tsx
@@ -0,0 +1,749 @@
+import React from 'preact/compat'
+import { render } from 'preact'
+import { formatForDisplay, useHotkey } from '@tanstack/preact-hotkeys'
+import { HotkeysProvider } from '@tanstack/preact-hotkeys'
+import { hotkeysDevtoolsPlugin } from '@tanstack/preact-hotkeys-devtools'
+import { TanStackDevtools } from '@tanstack/preact-devtools'
+import type { Hotkey } from '@tanstack/preact-hotkeys'
+import './index.css'
+
+function App() {
+ const [lastHotkey, setLastHotkey] = React.useState(null)
+ const [saveCount, setSaveCount] = React.useState(0)
+ const [incrementCount, setIncrementCount] = React.useState(0)
+ const [enabled, setEnabled] = React.useState(true)
+ const [activeTab, setActiveTab] = React.useState(1)
+ const [navigationCount, setNavigationCount] = React.useState(0)
+ const [functionKeyCount, setFunctionKeyCount] = React.useState(0)
+ const [multiModifierCount, setMultiModifierCount] = React.useState(0)
+ const [editingKeyCount, setEditingKeyCount] = React.useState(0)
+
+ // Scoped shortcuts state
+ const [modalOpen, setModalOpen] = React.useState(false)
+ const [editorContent, setEditorContent] = React.useState('')
+ const [sidebarShortcutCount, setSidebarShortcutCount] = React.useState(0)
+ const [modalShortcutCount, setModalShortcutCount] = React.useState(0)
+ const [editorShortcutCount, setEditorShortcutCount] = React.useState(0)
+
+ // Refs for scoped shortcuts
+ const sidebarRef = React.useRef(null)
+ const modalRef = React.useRef(null)
+ const editorRef = React.useRef(null)
+
+ // Type-safe refs for useHotkey (HTMLTextAreaElement extends HTMLElement)
+ const editorRefForHotkey = editorRef as React.RefObject
+
+ // ============================================================================
+ // Basic Hotkeys
+ // ============================================================================
+
+ // Browser default: Save page (downloads the current page)
+ // Basic hotkey with callback context
+ useHotkey('Mod+S', (_event, { hotkey, parsedHotkey }) => {
+ setLastHotkey(hotkey)
+ setSaveCount((c) => c + 1)
+ console.log('Hotkey triggered:', hotkey)
+ console.log('Parsed hotkey:', parsedHotkey)
+ })
+
+ // requireReset prevents repeated triggering while holding keys
+ useHotkey(
+ 'Mod+K',
+ (_event, { hotkey }) => {
+ setLastHotkey(hotkey)
+ setIncrementCount((c) => c + 1)
+ },
+ { requireReset: true },
+ )
+
+ // Conditional hotkey (enabled/disabled)
+ useHotkey(
+ 'Mod+E',
+ (_event, { hotkey }) => {
+ setLastHotkey(hotkey)
+ alert('This hotkey can be toggled!')
+ },
+ { enabled },
+ )
+
+ // ============================================================================
+ // Number Key Combinations (Tab/Section Switching)
+ // ============================================================================
+
+ // Browser default: Switch to tab 1
+ useHotkey('Mod+1', () => {
+ setLastHotkey('Mod+1')
+ setActiveTab(1)
+ })
+
+ useHotkey('Mod+2', () => {
+ setLastHotkey('Mod+2')
+ setActiveTab(2)
+ })
+
+ useHotkey('Mod+3', () => {
+ setLastHotkey('Mod+3')
+ setActiveTab(3)
+ })
+
+ useHotkey('Mod+4', () => {
+ setLastHotkey('Mod+4')
+ setActiveTab(4)
+ })
+
+ useHotkey('Mod+5', () => {
+ setLastHotkey('Mod+5')
+ setActiveTab(5)
+ })
+
+ // ============================================================================
+ // Navigation Key Combinations
+ // ============================================================================
+
+ useHotkey('Shift+ArrowUp', () => {
+ setLastHotkey('Shift+ArrowUp')
+ setNavigationCount((c) => c + 1)
+ })
+
+ useHotkey('Shift+ArrowDown', () => {
+ setLastHotkey('Shift+ArrowDown')
+ setNavigationCount((c) => c + 1)
+ })
+
+ useHotkey('Alt+ArrowLeft', () => {
+ setLastHotkey('Alt+ArrowLeft')
+ setNavigationCount((c) => c + 1)
+ })
+
+ useHotkey('Alt+ArrowRight', () => {
+ setLastHotkey('Alt+ArrowRight')
+ setNavigationCount((c) => c + 1)
+ })
+
+ useHotkey('Mod+Home', () => {
+ setLastHotkey('Mod+Home')
+ setNavigationCount((c) => c + 1)
+ })
+
+ useHotkey('Mod+End', () => {
+ setLastHotkey('Mod+End')
+ setNavigationCount((c) => c + 1)
+ })
+
+ useHotkey('Control+PageUp', () => {
+ setLastHotkey('Control+PageUp')
+ setNavigationCount((c) => c + 1)
+ })
+
+ useHotkey('Control+PageDown', () => {
+ setLastHotkey('Control+PageDown')
+ setNavigationCount((c) => c + 1)
+ })
+
+ // ============================================================================
+ // Function Key Combinations
+ // ============================================================================
+
+ useHotkey('Meta+F4', () => {
+ setLastHotkey('Alt+F4')
+ setFunctionKeyCount((c) => c + 1)
+ alert('Alt+F4 pressed (normally closes window)')
+ })
+
+ useHotkey('Control+F5', () => {
+ setLastHotkey('Control+F5')
+ setFunctionKeyCount((c) => c + 1)
+ })
+
+ useHotkey('Mod+F1', () => {
+ setLastHotkey('Mod+F1')
+ setFunctionKeyCount((c) => c + 1)
+ })
+
+ useHotkey('Shift+F10', () => {
+ setLastHotkey('Shift+F10')
+ setFunctionKeyCount((c) => c + 1)
+ })
+
+ // ============================================================================
+ // Multi-Modifier Combinations
+ // ============================================================================
+
+ useHotkey('Mod+Shift+S', () => {
+ setLastHotkey('Mod+Shift+S')
+ setMultiModifierCount((c) => c + 1)
+ })
+
+ useHotkey('Mod+Shift+Z', () => {
+ setLastHotkey('Mod+Shift+Z')
+ setMultiModifierCount((c) => c + 1)
+ })
+
+ useHotkey({ key: 'A', ctrl: true, alt: true }, () => {
+ setLastHotkey('Control+Alt+A')
+ setMultiModifierCount((c) => c + 1)
+ })
+
+ useHotkey('Control+Shift+N', () => {
+ setLastHotkey('Control+Shift+N')
+ setMultiModifierCount((c) => c + 1)
+ })
+
+ useHotkey('Mod+Alt+T', () => {
+ setLastHotkey('Mod+Alt+T')
+ setMultiModifierCount((c) => c + 1)
+ })
+
+ useHotkey('Control+Alt+Shift+X', () => {
+ setLastHotkey('Control+Alt+Shift+X')
+ setMultiModifierCount((c) => c + 1)
+ })
+
+ // ============================================================================
+ // Editing Key Combinations
+ // ============================================================================
+
+ useHotkey('Mod+Enter', () => {
+ setLastHotkey('Mod+Enter')
+ setEditingKeyCount((c) => c + 1)
+ })
+
+ useHotkey('Shift+Enter', () => {
+ setLastHotkey('Shift+Enter')
+ setEditingKeyCount((c) => c + 1)
+ })
+
+ useHotkey('Mod+Backspace', () => {
+ setLastHotkey('Mod+Backspace')
+ setEditingKeyCount((c) => c + 1)
+ })
+
+ useHotkey('Mod+Delete', () => {
+ setLastHotkey('Mod+Delete')
+ setEditingKeyCount((c) => c + 1)
+ })
+
+ useHotkey('Control+Tab', () => {
+ setLastHotkey('Control+Tab')
+ setEditingKeyCount((c) => c + 1)
+ })
+
+ useHotkey('Shift+Tab', () => {
+ setLastHotkey('Shift+Tab')
+ setEditingKeyCount((c) => c + 1)
+ })
+
+ useHotkey('Mod+Space', () => {
+ setLastHotkey('Mod+Space')
+ setEditingKeyCount((c) => c + 1)
+ })
+
+ // ============================================================================
+ // Single Keys
+ // ============================================================================
+
+ // Clear with Escape (RawHotkey object form)
+ useHotkey({ key: 'Escape' }, () => {
+ setLastHotkey(null)
+ setSaveCount(0)
+ setIncrementCount(0)
+ setNavigationCount(0)
+ setFunctionKeyCount(0)
+ setMultiModifierCount(0)
+ setEditingKeyCount(0)
+ setActiveTab(1)
+ })
+
+ useHotkey('F12', () => {
+ setLastHotkey('F12')
+ setFunctionKeyCount((c) => c + 1)
+ })
+
+ // ============================================================================
+ // Scoped Keyboard Shortcuts
+ // ============================================================================
+
+ // Scoped to sidebar - only works when sidebar is focused or contains focus
+ // Auto-focus modal when opened so scoped shortcuts work immediately
+ React.useEffect(() => {
+ if (modalOpen) {
+ modalRef.current?.focus()
+ }
+ }, [modalOpen])
+
+ useHotkey(
+ 'Mod+B',
+ () => {
+ setLastHotkey('Mod+B')
+ setSidebarShortcutCount((c) => c + 1)
+ alert(
+ 'Sidebar shortcut triggered! This only works when the sidebar area is focused.',
+ )
+ },
+ { target: sidebarRef },
+ )
+
+ useHotkey(
+ 'Mod+N',
+ () => {
+ setLastHotkey('Mod+N')
+ setSidebarShortcutCount((c) => c + 1)
+ },
+ { target: sidebarRef },
+ )
+
+ // Scoped to modal - only works when modal is open and focused
+ useHotkey(
+ 'Escape',
+ () => {
+ setLastHotkey('Escape')
+ setModalShortcutCount((c) => c + 1)
+ setModalOpen(false)
+ },
+ { target: modalRef, enabled: modalOpen },
+ )
+
+ useHotkey(
+ 'Mod+Enter',
+ () => {
+ setLastHotkey('Mod+Enter')
+ setModalShortcutCount((c) => c + 1)
+ alert('Modal submit shortcut!')
+ },
+ { target: modalRef, enabled: modalOpen },
+ )
+
+ // Scoped to editor - only works when editor is focused
+ useHotkey(
+ 'Mod+S',
+ () => {
+ setLastHotkey('Mod+S')
+ setEditorShortcutCount((c) => c + 1)
+ alert(
+ `Editor content saved: "${editorContent.substring(0, 50)}${editorContent.length > 50 ? '...' : ''}"`,
+ )
+ },
+ { target: editorRefForHotkey },
+ )
+
+ useHotkey(
+ 'Mod+/',
+ () => {
+ setLastHotkey('Mod+/')
+ setEditorShortcutCount((c) => c + 1)
+ setEditorContent((prev) => prev + '\n// Comment added via shortcut')
+ },
+ { target: editorRefForHotkey },
+ )
+
+ useHotkey(
+ 'Mod+K',
+ () => {
+ setLastHotkey('Mod+K')
+ setEditorShortcutCount((c) => c + 1)
+ setEditorContent('')
+ },
+ { target: editorRefForHotkey },
+ )
+
+ return (
+
+
+
+
+
+ Basic Hotkey
+
+ Press {formatForDisplay('Mod+S')} to trigger
+
+ Save triggered: {saveCount}x
+ {`useHotkey('Mod+S', (_event, { hotkey, parsedHotkey }) => {
+ console.log('Hotkey:', hotkey)
+ console.log('Parsed:', parsedHotkey)
+})`}
+
+
+
+ With requireReset
+
+ Hold {formatForDisplay('Mod+K')} — only increments once
+ until you release all keys
+
+ Increment: {incrementCount}
+
+ This prevents repeated triggering while holding the keys down.
+ Release all keys to allow re-triggering.
+
+ {`useHotkey(
+ 'Mod+K',
+ (event, { hotkey }) => {
+ setCount(c => c + 1)
+ },
+ { requireReset: true }
+)`}
+
+
+
+ Conditional Hotkey
+
+ {formatForDisplay('Mod+E')} is currently{' '}
+ {enabled ? 'enabled' : 'disabled'}
+
+ setEnabled(!enabled)}>
+ {enabled ? 'Disable' : 'Enable'} Hotkey
+
+ {`const [enabled, setEnabled] = useState(true)
+
+useHotkey(
+ 'Mod+E',
+ (event, { hotkey }) => {
+ alert('Triggered!')
+ },
+ { enabled }
+)`}
+
+
+
+ Number Key Combinations
+ Common for tab/section switching:
+
+
+ {formatForDisplay('Mod+1')} → Tab 1
+
+
+ {formatForDisplay('Mod+2')} → Tab 2
+
+
+ {formatForDisplay('Mod+3')} → Tab 3
+
+
+ {formatForDisplay('Mod+4')} → Tab 4
+
+
+ {formatForDisplay('Mod+5')} → Tab 5
+
+
+ Active Tab: {activeTab}
+ {`useHotkey('Mod+1', () => setActiveTab(1))
+useHotkey('Mod+2', () => setActiveTab(2))
+`}
+
+
+
+ Navigation Key Combinations
+ Selection and navigation shortcuts:
+
+
+ {formatForDisplay('Shift+ArrowUp')} — Select up
+
+
+ {formatForDisplay('Shift+ArrowDown')} — Select down
+
+
+ {formatForDisplay('Alt+ArrowLeft')} — Navigate back
+
+
+ {formatForDisplay('Alt+ArrowRight')} — Navigate forward
+
+
+ {formatForDisplay('Mod+Home')} — Go to start
+
+
+ {formatForDisplay('Mod+End')} — Go to end
+
+
+ {formatForDisplay('Control+PageUp')} — Previous page
+
+
+ {formatForDisplay('Control+PageDown')} — Next page
+
+
+
+ Navigation triggered: {navigationCount}x
+
+ {`useHotkey('Shift+ArrowUp', () => selectUp())
+useHotkey('Alt+ArrowLeft', () => navigateBack())
+useHotkey('Mod+Home', () => goToStart())
+useHotkey('Control+PageUp', () => previousPage())`}
+
+
+
+ Function Key Combinations
+ System and application shortcuts:
+
+
+ {formatForDisplay('Alt+F4')} — Close window
+
+
+ {formatForDisplay('Control+F5')} — Hard refresh
+
+
+ {formatForDisplay('Mod+F1')} — Help
+
+
+ {formatForDisplay('Shift+F10')} — Context menu
+
+
+ {formatForDisplay('F12')} — DevTools
+
+
+
+ Function keys triggered: {functionKeyCount}x
+
+ {`useHotkey('Alt+F4', () => closeWindow())
+useHotkey('Control+F5', () => hardRefresh())
+useHotkey('Mod+F1', () => showHelp())
+useHotkey('F12', () => openDevTools())`}
+
+
+
+ Multi-Modifier Combinations
+ Complex shortcuts with multiple modifiers:
+
+
+ {formatForDisplay('Mod+Shift+S')} — Save As
+
+
+ {formatForDisplay('Mod+Shift+Z')} — Redo
+
+
+ {formatForDisplay('Control+Alt+A')} — Special action
+
+
+ {formatForDisplay('Control+Shift+N')} — New incognito
+
+
+ {formatForDisplay('Mod+Alt+T')} — Toggle theme
+
+
+ {formatForDisplay('Control+Alt+Shift+X')} — Triple
+ modifier
+
+
+
+ Multi-modifier triggered: {multiModifierCount}x
+
+ {`useHotkey('Mod+Shift+S', () => saveAs())
+useHotkey('Mod+Shift+Z', () => redo())
+useHotkey('Control+Alt+A', () => specialAction())
+useHotkey('Control+Alt+Shift+X', () => complexAction())`}
+
+
+
+ Editing Key Combinations
+ Text editing and form shortcuts:
+
+
+ {formatForDisplay('Mod+Enter')} — Submit form
+
+
+ {formatForDisplay('Shift+Enter')} — New line
+
+
+ {formatForDisplay('Mod+Backspace')} — Delete word
+
+
+ {formatForDisplay('Mod+Delete')} — Delete forward
+
+
+ {formatForDisplay('Control+Tab')} — Next tab
+
+
+ {formatForDisplay('Shift+Tab')} — Previous field
+
+
+ {formatForDisplay('Mod+Space')} — Toggle
+
+
+
+ Editing keys triggered: {editingKeyCount}x
+
+ {`useHotkey('Mod+Enter', () => submitForm())
+useHotkey('Shift+Enter', () => insertNewline())
+useHotkey('Mod+Backspace', () => deleteWord())
+useHotkey('Control+Tab', () => nextTab())
+useHotkey('Mod+Space', () => toggle())`}
+
+
+ {lastHotkey && (
+
+ Last triggered: {formatForDisplay(lastHotkey)}
+
+ )}
+
+
+ Press Escape to reset all counters
+
+
+ {/* ==================================================================== */}
+ {/* Scoped Keyboard Shortcuts Section */}
+ {/* ==================================================================== */}
+
+ Scoped Keyboard Shortcuts
+
+ Shortcuts can be scoped to specific DOM elements using the{' '}
+ target option. This allows different shortcuts to work
+ in different parts of your application.
+
+
+
+ {/* Sidebar Example */}
+
+
Sidebar (Scoped Area)
+
Click here to focus, then try:
+
+
+ {formatForDisplay('Mod+B')} — Trigger sidebar
+ action
+
+
+ {formatForDisplay('Mod+N')} — New item
+
+
+
+ Sidebar shortcuts: {sidebarShortcutCount}x
+
+
+ These shortcuts only work when this sidebar area is focused or
+ contains focus.
+
+
+
+ {/* Modal Example */}
+
+
Modal Dialog
+
setModalOpen(true)}>Open Modal
+ {modalOpen && (
+
setModalOpen(false)}
+ >
+
e.stopPropagation()}
+ >
+
Modal Dialog (Scoped)
+
Try these shortcuts while modal is open:
+
+
+ {formatForDisplay('Escape')} — Close modal
+
+
+ {formatForDisplay('Mod+Enter')} — Submit
+
+
+
+ Modal shortcuts: {modalShortcutCount}x
+
+
+ These shortcuts only work when the modal is open and
+ focused. The Escape key here won't conflict with the
+ global Escape handler.
+
+
setModalOpen(false)}>Close
+
+
+ )}
+
+
+ {/* Editor Example */}
+
+
Text Editor (Scoped)
+
Focus the editor below and try:
+
+
+ {formatForDisplay('Mod+S')} — Save editor content
+
+
+ {formatForDisplay('Mod+/')} — Add comment
+
+
+ {formatForDisplay('Mod+K')} — Clear editor
+
+
+
+
+
+ {`// Scoped to a ref
+const sidebarRef = useRef(null)
+
+useHotkey(
+ 'Mod+B',
+ () => {
+ console.log('Sidebar shortcut!')
+ },
+ { target: sidebarRef }
+)
+
+// Scoped to a modal (only when open)
+const modalRef = useRef(null)
+const [isOpen, setIsOpen] = useState(false)
+
+useHotkey(
+ 'Escape',
+ () => setIsOpen(false),
+ { target: modalRef, enabled: isOpen }
+)
+
+// Scoped to an editor
+const editorRef = useRef(null)
+
+useHotkey(
+ 'Mod+S',
+ () => saveEditorContent(),
+ { target: editorRef }
+)`}
+
+
+
+ )
+}
+
+// TanStackDevtools must be a sibling of App, not inside it, to avoid Preact
+// "Hook can only be invoked from render methods" when hotkeys trigger state updates.
+// See: https://github.com/preactjs/preact/issues/2798
+const devtoolsPlugins = [hotkeysDevtoolsPlugin()]
+
+render(
+ // optionally, provide default options to an optional HotkeysProvider
+
+
+
+ ,
+ document.getElementById('root')!,
+)
diff --git a/examples/preact/useHotkey/tsconfig.json b/examples/preact/useHotkey/tsconfig.json
new file mode 100644
index 0000000..faa3381
--- /dev/null
+++ b/examples/preact/useHotkey/tsconfig.json
@@ -0,0 +1,20 @@
+{
+ "compilerOptions": {
+ "target": "ESNext",
+ "lib": ["DOM", "DOM.Iterable", "ESNext"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+ "moduleResolution": "Bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+ "jsxImportSource": "preact"
+ },
+ "include": ["src", "vite.config.ts"]
+}
diff --git a/examples/preact/useHotkey/vite.config.ts b/examples/preact/useHotkey/vite.config.ts
new file mode 100644
index 0000000..bfe110c
--- /dev/null
+++ b/examples/preact/useHotkey/vite.config.ts
@@ -0,0 +1,6 @@
+import { defineConfig } from 'vite'
+import preact from '@preact/preset-vite'
+
+export default defineConfig({
+ plugins: [preact()],
+})
diff --git a/examples/preact/useHotkeyRecorder/eslint.config.js b/examples/preact/useHotkeyRecorder/eslint.config.js
new file mode 100644
index 0000000..3fd4ac4
--- /dev/null
+++ b/examples/preact/useHotkeyRecorder/eslint.config.js
@@ -0,0 +1,11 @@
+// @ts-check
+
+import rootConfig from '../../../eslint.config.js'
+
+/** @type {import('eslint').Linter.Config[]} */
+export default [
+ {
+ ignores: ['eslint.config.js'],
+ },
+ ...rootConfig,
+]
diff --git a/examples/preact/useHotkeyRecorder/index.html b/examples/preact/useHotkeyRecorder/index.html
new file mode 100644
index 0000000..729d9b6
--- /dev/null
+++ b/examples/preact/useHotkeyRecorder/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+ Shortcut Settings - TanStack Hotkeys React Example
+
+
+ You need to enable JavaScript to run this app.
+
+
+
+
diff --git a/examples/preact/useHotkeyRecorder/package.json b/examples/preact/useHotkeyRecorder/package.json
new file mode 100644
index 0000000..e001405
--- /dev/null
+++ b/examples/preact/useHotkeyRecorder/package.json
@@ -0,0 +1,24 @@
+{
+ "name": "@tanstack/hotkeys-example-preact-shortcut-settings",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vite --port=3069",
+ "build": "vite build",
+ "preview": "vite preview",
+ "lint": "eslint .",
+ "lint:fix": "eslint . --fix",
+ "test:types": "tsc"
+ },
+ "dependencies": {
+ "@tanstack/preact-hotkeys": "^0.3.0",
+ "preact": "^10.27.2"
+ },
+ "devDependencies": {
+ "@preact/preset-vite": "^2.10.2",
+ "@tanstack/preact-devtools": "0.9.6",
+ "@tanstack/preact-hotkeys-devtools": "^0.3.0",
+ "typescript": "5.9.3",
+ "vite": "^7.3.1"
+ }
+}
diff --git a/examples/preact/useHotkeyRecorder/src/index.css b/examples/preact/useHotkeyRecorder/src/index.css
new file mode 100644
index 0000000..77dfb2f
--- /dev/null
+++ b/examples/preact/useHotkeyRecorder/src/index.css
@@ -0,0 +1,256 @@
+* {
+ box-sizing: border-box;
+}
+body {
+ margin: 0;
+ font-family:
+ system-ui,
+ -apple-system,
+ sans-serif;
+ background: #f5f5f5;
+ color: #333;
+}
+.app {
+ max-width: 900px;
+ margin: 0 auto;
+ padding: 20px;
+}
+header {
+ text-align: center;
+ margin-bottom: 40px;
+}
+header h1 {
+ margin: 0 0 10px;
+ color: #0066cc;
+}
+header p {
+ color: #666;
+ margin: 0;
+ max-width: 600px;
+ margin: 0 auto;
+}
+.demo-section {
+ background: white;
+ border-radius: 12px;
+ padding: 24px;
+ margin-bottom: 24px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+}
+.demo-section h2 {
+ margin: 0 0 16px;
+ font-size: 20px;
+}
+.demo-section p {
+ margin: 0 0 16px;
+}
+kbd {
+ background: linear-gradient(180deg, #f8f8f8 0%, #e8e8e8 100%);
+ border: 1px solid #ccc;
+ border-bottom-width: 2px;
+ border-radius: 4px;
+ padding: 2px 8px;
+ font-family: monospace;
+ font-size: 13px;
+}
+button {
+ background: #0066cc;
+ color: white;
+ border: none;
+ padding: 8px 16px;
+ border-radius: 6px;
+ cursor: pointer;
+ font-size: 14px;
+ transition: background 0.2s;
+}
+button:hover {
+ background: #0052a3;
+}
+button:active {
+ background: #004080;
+}
+.cancel-button {
+ background: #dc3545;
+}
+.cancel-button:hover {
+ background: #c82333;
+}
+.edit-button {
+ background: #28a745;
+}
+.edit-button:hover {
+ background: #218838;
+}
+.code-block {
+ background: #1e1e1e;
+ color: #d4d4d4;
+ padding: 16px;
+ border-radius: 8px;
+ overflow-x: auto;
+ font-size: 13px;
+ line-height: 1.5;
+ margin-top: 16px;
+}
+.info-box {
+ background: #e3f2fd;
+ border-radius: 8px;
+ padding: 12px 16px;
+ margin: 20px 0;
+}
+.recording-notice {
+ background: #fff3cd;
+ border: 2px solid #ffc107;
+ animation: pulse 2s ease-in-out infinite;
+}
+@keyframes pulse {
+ 0%,
+ 100% {
+ opacity: 1;
+ }
+ 50% {
+ opacity: 0.8;
+ }
+}
+
+/* Shortcuts List */
+.shortcuts-list {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+}
+.shortcut-item {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 16px;
+ background: #f8f9fa;
+ border: 2px solid transparent;
+ border-radius: 8px;
+ transition: all 0.2s;
+}
+.shortcut-item:hover {
+ background: #f0f0f0;
+}
+.shortcut-item.recording {
+ background: #fff3cd;
+ border-color: #ffc107;
+ box-shadow: 0 0 0 3px rgba(255, 193, 7, 0.2);
+ animation: recordingPulse 1.5s ease-in-out infinite;
+}
+@keyframes recordingPulse {
+ 0%,
+ 100% {
+ box-shadow: 0 0 0 3px rgba(255, 193, 7, 0.2);
+ }
+ 50% {
+ box-shadow: 0 0 0 6px rgba(255, 193, 7, 0.1);
+ }
+}
+.shortcut-item-content {
+ display: flex;
+ align-items: center;
+ gap: 24px;
+ flex: 1;
+}
+.shortcut-action {
+ font-weight: 500;
+ min-width: 80px;
+ font-size: 15px;
+}
+.shortcut-hotkey {
+ display: flex;
+ align-items: center;
+ min-height: 32px;
+}
+.shortcut-hotkey kbd {
+ font-size: 14px;
+}
+.no-shortcut {
+ color: #999;
+ font-style: italic;
+ font-size: 14px;
+}
+.shortcut-actions {
+ display: flex;
+ gap: 8px;
+}
+
+/* Recording Indicator */
+.recording-indicator {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+.recording-text {
+ color: #856404;
+ font-style: italic;
+ font-size: 14px;
+}
+.held-hotkeys {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+}
+.held-hotkeys .plus {
+ color: #856404;
+ font-size: 16px;
+ margin: 0 4px;
+}
+.held-hotkeys kbd {
+ background: #ffc107;
+ border-color: #ff9800;
+ color: #856404;
+ font-weight: 600;
+}
+
+/* Demo Stats */
+.demo-stats {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
+ gap: 16px;
+ margin-top: 20px;
+}
+.stat-item {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 16px;
+ background: #f8f9fa;
+ border-radius: 8px;
+ gap: 8px;
+}
+.stat-label {
+ font-size: 13px;
+ color: #666;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+.stat-value {
+ font-size: 32px;
+ font-weight: bold;
+ color: #0066cc;
+}
+.stat-item kbd {
+ margin-top: 4px;
+}
+
+/* Responsive */
+@media (max-width: 600px) {
+ .shortcut-item {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 12px;
+ }
+ .shortcut-item-content {
+ width: 100%;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 8px;
+ }
+ .shortcut-actions {
+ width: 100%;
+ justify-content: flex-end;
+ }
+ .demo-stats {
+ grid-template-columns: repeat(2, 1fr);
+ }
+}
diff --git a/examples/preact/useHotkeyRecorder/src/index.tsx b/examples/preact/useHotkeyRecorder/src/index.tsx
new file mode 100644
index 0000000..b3c9758
--- /dev/null
+++ b/examples/preact/useHotkeyRecorder/src/index.tsx
@@ -0,0 +1,374 @@
+import React from 'preact/compat'
+import { render } from 'preact'
+import {
+ formatForDisplay,
+ useHotkey,
+ useHeldKeys,
+ useHotkeyRecorder,
+ type Hotkey,
+} from '@tanstack/preact-hotkeys'
+import { HotkeysProvider } from '@tanstack/preact-hotkeys'
+import { hotkeysDevtoolsPlugin } from '@tanstack/preact-hotkeys-devtools'
+import { TanStackDevtools } from '@tanstack/preact-devtools'
+import './index.css'
+
+interface ShortcutActions {
+ [key: string]: {
+ name: string
+ defaultHotkey: Hotkey
+ }
+}
+
+const DEFAULT_SHORTCUT_ACTIONS: ShortcutActions = {
+ save: {
+ name: 'Save',
+ defaultHotkey: 'Mod+K',
+ },
+ open: {
+ name: 'Open',
+ defaultHotkey: 'Mod+E',
+ },
+ new: {
+ name: 'New',
+ defaultHotkey: 'Mod+G',
+ },
+ close: {
+ name: 'Close',
+ defaultHotkey: 'Mod+Shift+K',
+ },
+ undo: {
+ name: 'Undo',
+ defaultHotkey: 'Mod+Shift+E',
+ },
+ redo: {
+ name: 'Redo',
+ defaultHotkey: 'Mod+Shift+G',
+ },
+}
+
+function App() {
+ // State for shortcuts
+ const [shortcuts, setShortcuts] = React.useState>(
+ () => {
+ const defaults: Record = {}
+ for (const [id, action] of Object.entries(DEFAULT_SHORTCUT_ACTIONS)) {
+ defaults[id] = action.defaultHotkey
+ }
+ return defaults
+ },
+ )
+
+ // Simple counters for each action
+ const [saveCount, setSaveCount] = React.useState(0)
+ const [openCount, setOpenCount] = React.useState(0)
+ const [newCount, setNewCount] = React.useState(0)
+ const [closeCount, setCloseCount] = React.useState(0)
+ const [undoCount, setUndoCount] = React.useState(0)
+ const [redoCount, setRedoCount] = React.useState(0)
+
+ // Track which action is being recorded
+ const [recordingActionId, setRecordingActionId] = React.useState<
+ string | null
+ >(null)
+
+ // Use the library's useHotkeyRecorder hook
+ const recorder = useHotkeyRecorder({
+ onRecord: (hotkey: Hotkey) => {
+ if (recordingActionId) {
+ setShortcuts((prev) => ({
+ ...prev,
+ [recordingActionId]: hotkey || ('' as Hotkey | ''),
+ }))
+ setRecordingActionId(null)
+ }
+ },
+ onCancel: () => {
+ setRecordingActionId(null)
+ },
+ onClear: () => {
+ if (recordingActionId) {
+ setShortcuts((prev) => ({
+ ...prev,
+ [recordingActionId]: '' as Hotkey | '',
+ }))
+ setRecordingActionId(null)
+ }
+ },
+ })
+
+ // Register shortcuts using useHotkey
+ const isRecording = recorder.isRecording
+
+ // Get actual hotkey values (use defaults if empty)
+ const saveHotkey: Hotkey =
+ shortcuts.save || DEFAULT_SHORTCUT_ACTIONS.save.defaultHotkey
+ const openHotkey: Hotkey =
+ shortcuts.open || DEFAULT_SHORTCUT_ACTIONS.open.defaultHotkey
+ const newHotkey: Hotkey =
+ shortcuts.new || DEFAULT_SHORTCUT_ACTIONS.new.defaultHotkey
+ const closeHotkey: Hotkey =
+ shortcuts.close || DEFAULT_SHORTCUT_ACTIONS.close.defaultHotkey
+ const undoHotkey: Hotkey =
+ shortcuts.undo || DEFAULT_SHORTCUT_ACTIONS.undo.defaultHotkey
+ const redoHotkey: Hotkey =
+ shortcuts.redo || DEFAULT_SHORTCUT_ACTIONS.redo.defaultHotkey
+
+ // Register each shortcut - only enable if shortcut is actually set (not empty)
+ useHotkey(
+ saveHotkey,
+ () => {
+ console.log('Save triggered:', saveHotkey)
+ setSaveCount((c) => c + 1)
+ },
+ {
+ enabled: !isRecording && shortcuts.save !== '',
+ },
+ )
+
+ useHotkey(
+ openHotkey,
+ () => {
+ console.log('Open triggered:', openHotkey)
+ setOpenCount((c) => c + 1)
+ },
+ {
+ enabled: !isRecording && shortcuts.open !== '',
+ },
+ )
+
+ useHotkey(
+ newHotkey,
+ () => {
+ console.log('New triggered:', newHotkey)
+ setNewCount((c) => c + 1)
+ },
+ {
+ enabled: !isRecording && shortcuts.new !== '',
+ },
+ )
+
+ useHotkey(
+ closeHotkey,
+ () => {
+ console.log('Close triggered:', closeHotkey)
+ setCloseCount((c) => c + 1)
+ },
+ {
+ enabled: !isRecording && shortcuts.close !== '',
+ },
+ )
+
+ useHotkey(
+ undoHotkey,
+ () => {
+ console.log('Undo triggered:', undoHotkey)
+ setUndoCount((c) => c + 1)
+ },
+ {
+ enabled: !isRecording && shortcuts.undo !== '',
+ },
+ )
+
+ useHotkey(
+ redoHotkey,
+ () => {
+ console.log('Redo triggered:', redoHotkey)
+ setRedoCount((c) => c + 1)
+ },
+ {
+ enabled: !isRecording && shortcuts.redo !== '',
+ },
+ )
+
+ const handleEdit = (actionId: string) => {
+ setRecordingActionId(actionId)
+ recorder.startRecording()
+ }
+
+ const handleCancel = () => {
+ recorder.cancelRecording()
+ setRecordingActionId(null)
+ }
+
+ return (
+
+
+
+
+
+ Shortcuts
+
+ {Object.entries(DEFAULT_SHORTCUT_ACTIONS).map(
+ ([actionId, action]) => (
+ handleEdit(actionId)}
+ onCancel={handleCancel}
+ />
+ ),
+ )}
+
+
+
+
+ Demo Actions
+ Try your shortcuts! Actions will trigger when you press them.
+
+
+
Save
+
{saveCount}
+
{formatForDisplay(shortcuts.save || 'Mod+K')}
+
+
+
Open
+
{openCount}
+
{formatForDisplay(shortcuts.open || 'Mod+E')}
+
+
+
New
+
{newCount}
+
{formatForDisplay(shortcuts.new || 'Mod+G')}
+
+
+
Close
+
{closeCount}
+
{formatForDisplay(shortcuts.close || 'Mod+Shift+K')}
+
+
+
Undo
+
{undoCount}
+
{formatForDisplay(shortcuts.undo || 'Mod+Shift+E')}
+
+
+
Redo
+
{redoCount}
+
{formatForDisplay(shortcuts.redo || 'Mod+Shift+G')}
+
+
+
+
+ {recorder.isRecording && (
+
+ Recording shortcut... Press any key combination or
+ Escape to cancel. Press Backspace/Delete to clear the shortcut.
+
+ )}
+
+
+ Usage
+ {`import { useHotkey, formatForDisplay } from '@tanstack/preact-hotkeys'
+
+function App() {
+ const [shortcuts, setShortcuts] = useState({
+ save: 'Mod+K',
+ open: 'Mod+E',
+ })
+
+ // Register shortcuts dynamically
+ useHotkey(
+ shortcuts.save,
+ () => handleSave(),
+ { enabled: !isRecording }
+ )
+
+ return (
+
+ {formatForDisplay(shortcuts.save)}
+
+ )
+}`}
+
+
+
+ )
+}
+
+// TanStackDevtools as sibling of App to avoid Preact hook errors when hotkeys update state
+const devtoolsPlugins = [hotkeysDevtoolsPlugin()]
+
+interface ShortcutListItemProps {
+ actionName: string
+ hotkey: string
+ isRecording: boolean
+ onEdit: () => void
+ onCancel: () => void
+}
+
+function ShortcutListItem({
+ actionName,
+ hotkey,
+ isRecording,
+ onEdit,
+ onCancel,
+}: ShortcutListItemProps) {
+ const heldKeys = useHeldKeys()
+
+ return (
+
+
+
{actionName}
+
+ {isRecording ? (
+
+ {heldKeys.length > 0 ? (
+
+ {heldKeys.map((key, index) => (
+
+ {index > 0 && + }
+ {key}
+
+ ))}
+
+ ) : (
+
+ Press any key combination...
+
+ )}
+
+ ) : hotkey ? (
+
{formatForDisplay(hotkey as Hotkey)}
+ ) : (
+
No shortcut
+ )}
+
+
+
+ {isRecording ? (
+
+ Cancel
+
+ ) : (
+
+ Edit
+
+ )}
+
+
+ )
+}
+
+render(
+ // optionally, provide default options to an optional HotkeysProvider
+
+
+
+ ,
+ document.getElementById('root')!,
+)
diff --git a/examples/preact/useHotkeyRecorder/tsconfig.json b/examples/preact/useHotkeyRecorder/tsconfig.json
new file mode 100644
index 0000000..faa3381
--- /dev/null
+++ b/examples/preact/useHotkeyRecorder/tsconfig.json
@@ -0,0 +1,20 @@
+{
+ "compilerOptions": {
+ "target": "ESNext",
+ "lib": ["DOM", "DOM.Iterable", "ESNext"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+ "moduleResolution": "Bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+ "jsxImportSource": "preact"
+ },
+ "include": ["src", "vite.config.ts"]
+}
diff --git a/examples/preact/useHotkeyRecorder/vite.config.ts b/examples/preact/useHotkeyRecorder/vite.config.ts
new file mode 100644
index 0000000..bfe110c
--- /dev/null
+++ b/examples/preact/useHotkeyRecorder/vite.config.ts
@@ -0,0 +1,6 @@
+import { defineConfig } from 'vite'
+import preact from '@preact/preset-vite'
+
+export default defineConfig({
+ plugins: [preact()],
+})
diff --git a/examples/preact/useHotkeySequence/eslint.config.js b/examples/preact/useHotkeySequence/eslint.config.js
new file mode 100644
index 0000000..3fd4ac4
--- /dev/null
+++ b/examples/preact/useHotkeySequence/eslint.config.js
@@ -0,0 +1,11 @@
+// @ts-check
+
+import rootConfig from '../../../eslint.config.js'
+
+/** @type {import('eslint').Linter.Config[]} */
+export default [
+ {
+ ignores: ['eslint.config.js'],
+ },
+ ...rootConfig,
+]
diff --git a/examples/preact/useHotkeySequence/index.html b/examples/preact/useHotkeySequence/index.html
new file mode 100644
index 0000000..69c5063
--- /dev/null
+++ b/examples/preact/useHotkeySequence/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+ useHotkeySequence - TanStack Hotkeys React Example
+
+
+ You need to enable JavaScript to run this app.
+
+
+
+
diff --git a/examples/preact/useHotkeySequence/package.json b/examples/preact/useHotkeySequence/package.json
new file mode 100644
index 0000000..adf94b1
--- /dev/null
+++ b/examples/preact/useHotkeySequence/package.json
@@ -0,0 +1,24 @@
+{
+ "name": "@tanstack/hotkeys-example-preact-use-hotkey-sequence",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vite --port=3069",
+ "build": "vite build",
+ "preview": "vite preview",
+ "lint": "eslint .",
+ "lint:fix": "eslint . --fix",
+ "test:types": "tsc"
+ },
+ "dependencies": {
+ "@tanstack/preact-hotkeys": "^0.3.0",
+ "preact": "^10.27.2"
+ },
+ "devDependencies": {
+ "@preact/preset-vite": "^2.10.2",
+ "@tanstack/preact-devtools": "0.9.6",
+ "@tanstack/preact-hotkeys-devtools": "^0.3.0",
+ "typescript": "5.9.3",
+ "vite": "^7.3.1"
+ }
+}
diff --git a/examples/preact/useHotkeySequence/src/index.css b/examples/preact/useHotkeySequence/src/index.css
new file mode 100644
index 0000000..0fe2ade
--- /dev/null
+++ b/examples/preact/useHotkeySequence/src/index.css
@@ -0,0 +1,135 @@
+* {
+ box-sizing: border-box;
+}
+body {
+ margin: 0;
+ font-family:
+ system-ui,
+ -apple-system,
+ sans-serif;
+ background: #f5f5f5;
+ color: #333;
+}
+.app {
+ max-width: 800px;
+ margin: 0 auto;
+ padding: 20px;
+}
+header {
+ text-align: center;
+ margin-bottom: 40px;
+}
+header h1 {
+ margin: 0 0 10px;
+ color: #0066cc;
+}
+header p {
+ color: #666;
+ margin: 0;
+ max-width: 500px;
+ margin: 0 auto;
+}
+.demo-section {
+ background: white;
+ border-radius: 12px;
+ padding: 24px;
+ margin-bottom: 24px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+}
+.demo-section h2 {
+ margin: 0 0 16px;
+ font-size: 20px;
+}
+kbd {
+ background: linear-gradient(180deg, #f8f8f8 0%, #e8e8e8 100%);
+ border: 1px solid #ccc;
+ border-bottom-width: 2px;
+ border-radius: 4px;
+ padding: 2px 8px;
+ font-family: monospace;
+ font-size: 13px;
+ margin-right: 4px;
+}
+.sequence-table {
+ width: 100%;
+ border-collapse: collapse;
+}
+.sequence-table th,
+.sequence-table td {
+ padding: 12px;
+ text-align: left;
+ border-bottom: 1px solid #eee;
+}
+.sequence-table th {
+ font-weight: 600;
+ color: #666;
+ font-size: 14px;
+}
+.fun-sequences {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: 16px;
+}
+.sequence-card {
+ background: #f8f9fa;
+ border-radius: 8px;
+ padding: 16px;
+ text-align: center;
+}
+.sequence-card h3 {
+ margin: 0 0 12px;
+ font-size: 16px;
+}
+.sequence-card p {
+ margin: 0 0 8px;
+}
+.hint {
+ font-size: 12px;
+ color: #888;
+ font-style: italic;
+}
+.info-box {
+ background: #e3f2fd;
+ border-radius: 8px;
+ padding: 16px 20px;
+ margin-bottom: 24px;
+ font-size: 18px;
+}
+.info-box.success {
+ background: #e8f5e9;
+ color: #2e7d32;
+}
+.code-block {
+ background: #1e1e1e;
+ color: #d4d4d4;
+ padding: 16px;
+ border-radius: 8px;
+ overflow-x: auto;
+ font-size: 13px;
+ line-height: 1.5;
+}
+.history-list {
+ list-style: none;
+ padding: 0;
+ margin: 0 0 16px;
+}
+.history-list li {
+ padding: 10px 14px;
+ background: #f0f0f0;
+ border-radius: 6px;
+ margin-bottom: 6px;
+ font-family: monospace;
+ font-size: 14px;
+}
+button {
+ background: #0066cc;
+ color: white;
+ border: none;
+ padding: 10px 20px;
+ border-radius: 6px;
+ cursor: pointer;
+ font-size: 14px;
+}
+button:hover {
+ background: #0052a3;
+}
diff --git a/examples/preact/useHotkeySequence/src/index.tsx b/examples/preact/useHotkeySequence/src/index.tsx
new file mode 100644
index 0000000..17b83e9
--- /dev/null
+++ b/examples/preact/useHotkeySequence/src/index.tsx
@@ -0,0 +1,223 @@
+import React from 'preact/compat'
+import { render } from 'preact'
+import { useHotkey, useHotkeySequence } from '@tanstack/preact-hotkeys'
+import { HotkeysProvider } from '@tanstack/preact-hotkeys'
+import { hotkeysDevtoolsPlugin } from '@tanstack/preact-hotkeys-devtools'
+import { TanStackDevtools } from '@tanstack/preact-devtools'
+import './index.css'
+
+function App() {
+ const [lastSequence, setLastSequence] = React.useState(null)
+ const [history, setHistory] = React.useState>([])
+
+ const addToHistory = (action: string) => {
+ setLastSequence(action)
+ setHistory((h) => [...h.slice(-9), action])
+ }
+
+ useHotkeySequence(['G', 'G'], () => addToHistory('gg → Go to top'))
+ useHotkeySequence(['Shift+G'], () => addToHistory('G → Go to bottom'))
+ useHotkeySequence(['D', 'D'], () => addToHistory('dd → Delete line'))
+ useHotkeySequence(['Y', 'Y'], () => addToHistory('yy → Yank (copy) line'))
+ useHotkeySequence(['D', 'W'], () => addToHistory('dw → Delete word'))
+ useHotkeySequence(['C', 'I', 'W'], () =>
+ addToHistory('ciw → Change inner word'),
+ )
+
+ useHotkeySequence(
+ ['ArrowUp', 'ArrowUp', 'ArrowDown', 'ArrowDown'],
+ () => addToHistory('↑↑↓↓ → Konami code (partial)'),
+ { timeout: 1500 },
+ )
+
+ useHotkeySequence(
+ ['ArrowLeft', 'ArrowRight', 'ArrowLeft', 'ArrowRight'],
+ () => addToHistory('←→←→ → Side to side!'),
+ { timeout: 1500 },
+ )
+
+ useHotkeySequence(['H', 'E', 'L', 'L', 'O'], () =>
+ addToHistory('hello → Hello World!'),
+ )
+
+ // Clear history with Escape
+ useHotkey('Escape', () => {
+ setLastSequence(null)
+ setHistory([])
+ })
+
+ return (
+
+
+
+
+
+ Vim-Style Commands
+
+
+
+ Sequence
+ Action
+
+
+
+
+
+ g g
+
+ Go to top
+
+
+
+ G (Shift+G)
+
+ Go to bottom
+
+
+
+ d d
+
+ Delete line
+
+
+
+ y y
+
+ Yank (copy) line
+
+
+
+ d w
+
+ Delete word
+
+
+
+ c i w
+
+ Change inner word
+
+
+
+
+
+
+ Fun Sequences
+
+
+
Konami Code (Partial)
+
+ ↑ ↑ ↓ ↓
+
+
Use arrow keys within 1.5 seconds
+
+
+
Side to Side
+
+ ← → ← →
+
+
Arrow keys within 1.5 seconds
+
+
+
Spell It Out
+
+ h e l l o
+
+
Type "hello" quickly
+
+
+
+
+ {lastSequence && (
+
+ Triggered: {lastSequence}
+
+ )}
+
+
+
+
+ Usage
+ {`import { useHotkeySequence } from '@tanstack/preact-hotkeys'
+
+function VimEditor() {
+ // Basic sequence
+ useHotkeySequence(['G', 'G'], () => {
+ scrollToTop()
+ })
+
+ // With custom timeout (1.5 seconds)
+ useHotkeySequence(
+ ['ArrowUp', 'ArrowUp', 'ArrowDown', 'ArrowDown'],
+ () => activateCheatMode(),
+ { timeout: 1500 }
+ )
+
+ // Three-key sequence
+ useHotkeySequence(['C', 'I', 'W'], () => {
+ changeInnerWord()
+ })
+}`}
+
+
+ {history.length > 0 && (
+
+ History
+
+ {history.map((item, i) => (
+ {item}
+ ))}
+
+ setHistory([])}>Clear History
+
+ )}
+
+
+ Press Escape to clear history
+
+
+
+ )
+}
+
+// TanStackDevtools as sibling of App to avoid Preact hook errors when hotkeys update state
+const devtoolsPlugins = [hotkeysDevtoolsPlugin()]
+
+render(
+ // optionally, provide default options to an optional HotkeysProvider
+
+
+
+ ,
+ document.getElementById('root')!,
+)
diff --git a/examples/preact/useHotkeySequence/tsconfig.json b/examples/preact/useHotkeySequence/tsconfig.json
new file mode 100644
index 0000000..faa3381
--- /dev/null
+++ b/examples/preact/useHotkeySequence/tsconfig.json
@@ -0,0 +1,20 @@
+{
+ "compilerOptions": {
+ "target": "ESNext",
+ "lib": ["DOM", "DOM.Iterable", "ESNext"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+ "moduleResolution": "Bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+ "jsxImportSource": "preact"
+ },
+ "include": ["src", "vite.config.ts"]
+}
diff --git a/examples/preact/useHotkeySequence/vite.config.ts b/examples/preact/useHotkeySequence/vite.config.ts
new file mode 100644
index 0000000..bfe110c
--- /dev/null
+++ b/examples/preact/useHotkeySequence/vite.config.ts
@@ -0,0 +1,6 @@
+import { defineConfig } from 'vite'
+import preact from '@preact/preset-vite'
+
+export default defineConfig({
+ plugins: [preact()],
+})
diff --git a/examples/preact/useKeyhold/eslint.config.js b/examples/preact/useKeyhold/eslint.config.js
new file mode 100644
index 0000000..3fd4ac4
--- /dev/null
+++ b/examples/preact/useKeyhold/eslint.config.js
@@ -0,0 +1,11 @@
+// @ts-check
+
+import rootConfig from '../../../eslint.config.js'
+
+/** @type {import('eslint').Linter.Config[]} */
+export default [
+ {
+ ignores: ['eslint.config.js'],
+ },
+ ...rootConfig,
+]
diff --git a/examples/preact/useKeyhold/index.html b/examples/preact/useKeyhold/index.html
new file mode 100644
index 0000000..987e606
--- /dev/null
+++ b/examples/preact/useKeyhold/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+ useKeyHold - TanStack Hotkeys React Example
+
+
+ You need to enable JavaScript to run this app.
+
+
+
+
diff --git a/examples/preact/useKeyhold/package.json b/examples/preact/useKeyhold/package.json
new file mode 100644
index 0000000..2f863ce
--- /dev/null
+++ b/examples/preact/useKeyhold/package.json
@@ -0,0 +1,24 @@
+{
+ "name": "@tanstack/hotkeys-example-preact-use-key-hold",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vite --port=3069",
+ "build": "vite build",
+ "preview": "vite preview",
+ "lint": "eslint .",
+ "lint:fix": "eslint . --fix",
+ "test:types": "tsc"
+ },
+ "dependencies": {
+ "@tanstack/preact-hotkeys": "^0.3.0",
+ "preact": "^10.27.2"
+ },
+ "devDependencies": {
+ "@preact/preset-vite": "^2.10.2",
+ "@tanstack/preact-devtools": "0.9.6",
+ "@tanstack/preact-hotkeys-devtools": "^0.3.0",
+ "typescript": "5.9.3",
+ "vite": "^7.3.1"
+ }
+}
diff --git a/examples/preact/useKeyhold/src/index.css b/examples/preact/useKeyhold/src/index.css
new file mode 100644
index 0000000..0cbb5ae
--- /dev/null
+++ b/examples/preact/useKeyhold/src/index.css
@@ -0,0 +1,127 @@
+* {
+ box-sizing: border-box;
+}
+body {
+ margin: 0;
+ font-family:
+ system-ui,
+ -apple-system,
+ sans-serif;
+ background: #f5f5f5;
+ color: #333;
+}
+.app {
+ max-width: 800px;
+ margin: 0 auto;
+ padding: 20px;
+}
+header {
+ text-align: center;
+ margin-bottom: 40px;
+}
+header h1 {
+ margin: 0 0 10px;
+ color: #0066cc;
+}
+header p {
+ color: #666;
+ margin: 0;
+ max-width: 500px;
+ margin: 0 auto;
+}
+.demo-section {
+ background: white;
+ border-radius: 12px;
+ padding: 24px;
+ margin-bottom: 24px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+}
+.demo-section h2 {
+ margin: 0 0 16px;
+ font-size: 20px;
+}
+.demo-section p {
+ margin: 0 0 12px;
+}
+.demo-section ul {
+ margin: 0;
+ padding-left: 20px;
+}
+.demo-section li {
+ margin-bottom: 8px;
+}
+kbd {
+ background: linear-gradient(180deg, #f8f8f8 0%, #e8e8e8 100%);
+ border: 1px solid #ccc;
+ border-bottom-width: 2px;
+ border-radius: 4px;
+ padding: 2px 8px;
+ font-family: monospace;
+ font-size: 13px;
+}
+.modifier-grid {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: 16px;
+}
+.modifier-indicator {
+ background: #f0f0f0;
+ border: 2px solid #ddd;
+ border-radius: 12px;
+ padding: 20px;
+ text-align: center;
+ transition: all 0.15s ease;
+}
+.modifier-indicator.active {
+ background: #4caf50;
+ border-color: #388e3c;
+ color: white;
+ transform: scale(1.02);
+}
+.modifier-indicator .key-name {
+ display: block;
+ font-weight: bold;
+ font-size: 18px;
+ margin-bottom: 8px;
+}
+.modifier-indicator .status {
+ font-size: 14px;
+ opacity: 0.8;
+}
+.space-indicator {
+ background: #f0f0f0;
+ border: 3px solid #ddd;
+ border-radius: 16px;
+ padding: 40px;
+ text-align: center;
+ font-size: 24px;
+ transition: all 0.15s ease;
+}
+.space-indicator.active {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ border-color: #5a67d8;
+ color: white;
+ transform: scale(1.02);
+}
+.secret-box {
+ background: #f0f0f0;
+ border-radius: 8px;
+ padding: 20px;
+ text-align: center;
+ font-family: monospace;
+ font-size: 16px;
+ transition: all 0.3s ease;
+}
+.secret-box.revealed {
+ background: #e8f5e9;
+ color: #2e7d32;
+}
+.code-block {
+ background: #1e1e1e;
+ color: #d4d4d4;
+ padding: 16px;
+ border-radius: 8px;
+ overflow-x: auto;
+ font-size: 13px;
+ line-height: 1.5;
+}
diff --git a/examples/preact/useKeyhold/src/index.tsx b/examples/preact/useKeyhold/src/index.tsx
new file mode 100644
index 0000000..25e9393
--- /dev/null
+++ b/examples/preact/useKeyhold/src/index.tsx
@@ -0,0 +1,124 @@
+import { render } from 'preact'
+import { useKeyHold } from '@tanstack/preact-hotkeys'
+import { HotkeysProvider } from '@tanstack/preact-hotkeys'
+import { hotkeysDevtoolsPlugin } from '@tanstack/preact-hotkeys-devtools'
+import { TanStackDevtools } from '@tanstack/preact-devtools'
+import './index.css'
+
+function App() {
+ // const isModHeld = useKeyHold('')
+ const isShiftHeld = useKeyHold('Shift')
+ const isControlHeld = useKeyHold('Control')
+ const isAltHeld = useKeyHold('Alt')
+ const isMetaHeld = useKeyHold('Meta')
+ const isSpaceHeld = useKeyHold('Space')
+
+ return (
+
+
+
+
+
+ Modifier Key States
+
+
+ Shift
+
+ {isShiftHeld ? 'HELD' : 'Released'}
+
+
+
+ Control
+
+ {isControlHeld ? 'HELD' : 'Released'}
+
+
+
+ Alt / Option
+ {isAltHeld ? 'HELD' : 'Released'}
+
+
+ Meta (⌘ / ⊞)
+ {isMetaHeld ? 'HELD' : 'Released'}
+
+
+
+
+
+ Space Bar Demo
+
+ {isSpaceHeld ? '🚀 SPACE HELD!' : 'Hold Space Bar'}
+
+
+
+
+ Usage
+ {`import { useKeyHold } from '@tanstack/preact-hotkeys'
+
+function ShiftIndicator() {
+ const isShiftHeld = useKeyHold('Shift')
+
+ return (
+
+ {isShiftHeld ? 'Shift is pressed!' : 'Press Shift'}
+
+ )
+}`}
+
+
+
+ Conditional UI Example
+
+ Hold Shift to reveal the secret message:
+
+
+ {isShiftHeld ? (
+ 🎉 The secret password is: tanstack-hotkeys-rocks!
+ ) : (
+ ••••••••••••••••••••••••••
+ )}
+
+
+
+
+ Use Cases
+
+ Show different UI based on modifier state
+ Enable "power user" mode while holding a key
+ Hold-to-reveal sensitive information
+ Drag-and-drop with modifier behaviors
+ Show additional options on hover + modifier
+
+
+
+
+ )
+}
+
+// TanStackDevtools as sibling of App to avoid Preact hook errors when hotkeys update state
+const devtoolsPlugins = [hotkeysDevtoolsPlugin()]
+
+render(
+ // optionally, provide default options to an optional HotkeysProvider
+
+
+
+ ,
+ document.getElementById('root')!,
+)
diff --git a/examples/preact/useKeyhold/tsconfig.json b/examples/preact/useKeyhold/tsconfig.json
new file mode 100644
index 0000000..faa3381
--- /dev/null
+++ b/examples/preact/useKeyhold/tsconfig.json
@@ -0,0 +1,20 @@
+{
+ "compilerOptions": {
+ "target": "ESNext",
+ "lib": ["DOM", "DOM.Iterable", "ESNext"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+ "moduleResolution": "Bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+ "jsxImportSource": "preact"
+ },
+ "include": ["src", "vite.config.ts"]
+}
diff --git a/examples/preact/useKeyhold/vite.config.ts b/examples/preact/useKeyhold/vite.config.ts
new file mode 100644
index 0000000..bfe110c
--- /dev/null
+++ b/examples/preact/useKeyhold/vite.config.ts
@@ -0,0 +1,6 @@
+import { defineConfig } from 'vite'
+import preact from '@preact/preset-vite'
+
+export default defineConfig({
+ plugins: [preact()],
+})
diff --git a/examples/solid/createHeldKeys/package.json b/examples/solid/createHeldKeys/package.json
index af47229..e721502 100644
--- a/examples/solid/createHeldKeys/package.json
+++ b/examples/solid/createHeldKeys/package.json
@@ -10,8 +10,8 @@
"dependencies": {
"@tanstack/devtools-utils": "^0.3.0",
"@tanstack/solid-devtools": "0.7.26",
- "@tanstack/solid-hotkeys": "workspace:*",
- "@tanstack/solid-hotkeys-devtools": "workspace:*",
+ "@tanstack/solid-hotkeys": "^0.3.0",
+ "@tanstack/solid-hotkeys-devtools": "^0.3.0",
"solid-js": "^1.9.11"
},
"devDependencies": {
diff --git a/examples/solid/createHotkey/package.json b/examples/solid/createHotkey/package.json
index ae5eb9b..8ec486f 100644
--- a/examples/solid/createHotkey/package.json
+++ b/examples/solid/createHotkey/package.json
@@ -13,8 +13,8 @@
"dependencies": {
"@tanstack/devtools-utils": "^0.3.0",
"@tanstack/solid-devtools": "0.7.26",
- "@tanstack/solid-hotkeys": "workspace:*",
- "@tanstack/solid-hotkeys-devtools": "workspace:*",
+ "@tanstack/solid-hotkeys": "^0.3.0",
+ "@tanstack/solid-hotkeys-devtools": "^0.3.0",
"solid-js": "^1.9.11"
},
"devDependencies": {
diff --git a/examples/solid/createHotkeyRecorder/package.json b/examples/solid/createHotkeyRecorder/package.json
index 27b4f9e..9a06dcb 100644
--- a/examples/solid/createHotkeyRecorder/package.json
+++ b/examples/solid/createHotkeyRecorder/package.json
@@ -10,8 +10,8 @@
"dependencies": {
"@tanstack/devtools-utils": "^0.3.0",
"@tanstack/solid-devtools": "0.7.26",
- "@tanstack/solid-hotkeys": "workspace:*",
- "@tanstack/solid-hotkeys-devtools": "workspace:*",
+ "@tanstack/solid-hotkeys": "^0.3.0",
+ "@tanstack/solid-hotkeys-devtools": "^0.3.0",
"solid-js": "^1.9.11"
},
"devDependencies": {
diff --git a/examples/solid/createHotkeySequence/package.json b/examples/solid/createHotkeySequence/package.json
index 12561b3..349fa4e 100644
--- a/examples/solid/createHotkeySequence/package.json
+++ b/examples/solid/createHotkeySequence/package.json
@@ -10,8 +10,8 @@
"dependencies": {
"@tanstack/devtools-utils": "^0.3.0",
"@tanstack/solid-devtools": "0.7.26",
- "@tanstack/solid-hotkeys": "workspace:*",
- "@tanstack/solid-hotkeys-devtools": "workspace:*",
+ "@tanstack/solid-hotkeys": "^0.3.0",
+ "@tanstack/solid-hotkeys-devtools": "^0.3.0",
"solid-js": "^1.9.11"
},
"devDependencies": {
diff --git a/examples/solid/createKeyHold/package.json b/examples/solid/createKeyHold/package.json
index 24d9aea..f087382 100644
--- a/examples/solid/createKeyHold/package.json
+++ b/examples/solid/createKeyHold/package.json
@@ -10,8 +10,8 @@
"dependencies": {
"@tanstack/devtools-utils": "^0.3.0",
"@tanstack/solid-devtools": "0.7.26",
- "@tanstack/solid-hotkeys": "workspace:*",
- "@tanstack/solid-hotkeys-devtools": "workspace:*",
+ "@tanstack/solid-hotkeys": "^0.3.0",
+ "@tanstack/solid-hotkeys-devtools": "^0.3.0",
"solid-js": "^1.9.11"
},
"devDependencies": {
diff --git a/package.json b/package.json
index 611c681..2c58956 100644
--- a/package.json
+++ b/package.json
@@ -17,7 +17,7 @@
"clean": "find . -name 'dist' -type d -prune -exec rm -rf {} +",
"clean:node_modules": "find . -name 'node_modules' -type d -prune -exec rm -rf {} +",
"clean:all": "pnpm run clean && pnpm run clean:node_modules",
- "copy:readme": "cp README.md packages/hotkeys/README.md && cp README.md packages/hotkeys-devtools/README.md && cp README.md packages/react-hotkeys/README.md && cp README.md packages/react-hotkeys-devtools/README.md",
+ "copy:readme": "cp README.md packages/hotkeys/README.md && cp README.md packages/hotkeys-devtools/README.md && cp README.md packages/react-hotkeys/README.md && cp README.md packages/react-hotkeys-devtools/README.md && cp README.md packages/preact-hotkeys/README.md && cp README.md packages/preact-hotkeys-devtools/README.md",
"dev": "pnpm run watch",
"format": "prettier --experimental-cli --ignore-unknown '**/*' --write",
"generate-docs": "node scripts/generate-docs.ts && pnpm run copy:readme",
@@ -78,6 +78,8 @@
"overrides": {
"@tanstack/hotkeys": "workspace:*",
"@tanstack/hotkeys-devtools": "workspace:*",
+ "@tanstack/preact-hotkeys": "workspace:*",
+ "@tanstack/preact-hotkeys-devtools": "workspace:*",
"@tanstack/react-hotkeys": "workspace:*",
"@tanstack/react-hotkeys-devtools": "workspace:*"
}
diff --git a/packages/hotkeys-devtools/README.md b/packages/hotkeys-devtools/README.md
index 2da2e21..4609ad9 100644
--- a/packages/hotkeys-devtools/README.md
+++ b/packages/hotkeys-devtools/README.md
@@ -54,7 +54,8 @@ Type-safe keyboard shortcuts for the web. Template-string bindings, parsed objec
> You may know **TanStack Hotkeys** by our adapter names, too!
>
> - [**React Hotkeys**](https://tanstack.com/hotkeys/latest/docs/framework/react/react-hotkeys)
-> - Solid Hotkeys – needs a contributor!
+> - [**Preact Hotkeys**](https://tanstack.com/hotkeys/latest/docs/framework/preact/preact-hotkeys)
+> - [**Solid Hotkeys**](https://tanstack.com/hotkeys/latest/docs/framework/solid/solid-hotkeys)
> - Angular Hotkeys – needs a contributor!
> - Svelte Hotkeys – needs a contributor!
> - Vue Hotkeys – needs a contributor!
diff --git a/packages/hotkeys/README.md b/packages/hotkeys/README.md
index 2da2e21..4609ad9 100644
--- a/packages/hotkeys/README.md
+++ b/packages/hotkeys/README.md
@@ -54,7 +54,8 @@ Type-safe keyboard shortcuts for the web. Template-string bindings, parsed objec
> You may know **TanStack Hotkeys** by our adapter names, too!
>
> - [**React Hotkeys**](https://tanstack.com/hotkeys/latest/docs/framework/react/react-hotkeys)
-> - Solid Hotkeys – needs a contributor!
+> - [**Preact Hotkeys**](https://tanstack.com/hotkeys/latest/docs/framework/preact/preact-hotkeys)
+> - [**Solid Hotkeys**](https://tanstack.com/hotkeys/latest/docs/framework/solid/solid-hotkeys)
> - Angular Hotkeys – needs a contributor!
> - Svelte Hotkeys – needs a contributor!
> - Vue Hotkeys – needs a contributor!
diff --git a/packages/preact-hotkeys-devtools/README.md b/packages/preact-hotkeys-devtools/README.md
new file mode 100644
index 0000000..4609ad9
--- /dev/null
+++ b/packages/preact-hotkeys-devtools/README.md
@@ -0,0 +1,122 @@
+
+
+
+
+
+
+
+
+
+
+
+
+### [Become a Sponsor!](https://github.com/sponsors/tannerlinsley/)
+
+
+
+# TanStack Hotkeys
+
+> [!NOTE]
+> TanStack Hotkeys is pre-alpha (prototyping phase). We are actively developing the library and are open to feedback and contributions.
+
+Type-safe keyboard shortcuts for the web. Template-string bindings, parsed objects, a cross-platform `Mod` key, a singleton Hotkey Manager, and utilities for cheatsheet UIs—built to stay SSR-friendly.
+
+- Type-safe bindings — template strings (`Mod+Shift+S`, `Escape`) or parsed objects for full control
+- Flexible options — `keydown`/`keyup`, `preventDefault`, `stopPropagation`, conditional enabled, `requireReset`
+- Cross-platform Mod — maps to Cmd on macOS and Ctrl on Windows/Linux
+- Batteries included — validation + matching, sequences (Vim-style), key-state tracking, recorder UI helpers, React hooks, and devtools (in progress)
+
+### Read the docs →
+
+
+
+> [!NOTE]
+> You may know **TanStack Hotkeys** by our adapter names, too!
+>
+> - [**React Hotkeys**](https://tanstack.com/hotkeys/latest/docs/framework/react/react-hotkeys)
+> - [**Preact Hotkeys**](https://tanstack.com/hotkeys/latest/docs/framework/preact/preact-hotkeys)
+> - [**Solid Hotkeys**](https://tanstack.com/hotkeys/latest/docs/framework/solid/solid-hotkeys)
+> - Angular Hotkeys – needs a contributor!
+> - Svelte Hotkeys – needs a contributor!
+> - Vue Hotkeys – needs a contributor!
+
+## Get Involved
+
+- We welcome issues and pull requests!
+- Participate in [GitHub discussions](https://github.com/TanStack/hotkeys/discussions)
+- Chat with the community on [Discord](https://discord.com/invite/WrRKjPJ)
+- See [CONTRIBUTING.md](./CONTRIBUTING.md) for setup instructions
+
+## Partners
+
+
+
+
+
+
+
+
+We're looking for TanStack Hotkeys Partners to join our mission! Partner with us to push the boundaries of TanStack Hotkeys and build amazing things together.
+
+
LET'S CHAT
+
+
+
+
+## Explore the TanStack Ecosystem
+
+- TanStack Config – Tooling for JS/TS packages
+- TanStack DB – Reactive sync client store
+- TanStack DevTools – Unified devtools panel
+- TanStack Form – Type‑safe form state
+- TanStack Hotkeys – Type‑safe keyboard shortcuts
+- TanStack Query – Async state & caching
+- TanStack Ranger – Range & slider primitives
+- TanStack Router – Type‑safe routing, caching & URL state
+- TanStack Start – Full‑stack SSR & streaming
+- TanStack Store – Reactive data store
+- TanStack Table – Headless datagrids
+- TanStack Virtual – Virtualized rendering
+
+… and more at TanStack.com »
diff --git a/packages/preact-hotkeys-devtools/eslint.config.js b/packages/preact-hotkeys-devtools/eslint.config.js
new file mode 100644
index 0000000..3345087
--- /dev/null
+++ b/packages/preact-hotkeys-devtools/eslint.config.js
@@ -0,0 +1,8 @@
+// @ts-check
+
+import rootConfig from '../../eslint.config.js'
+
+/** @type {import('eslint').Linter.Config[]} */
+const config = [...rootConfig]
+
+export default config
diff --git a/packages/preact-hotkeys-devtools/package.json b/packages/preact-hotkeys-devtools/package.json
new file mode 100644
index 0000000..d4f461f
--- /dev/null
+++ b/packages/preact-hotkeys-devtools/package.json
@@ -0,0 +1,60 @@
+{
+ "name": "@tanstack/preact-hotkeys-devtools",
+ "version": "0.3.0",
+ "description": "Preact devtools for TanStack Hotkeys",
+ "author": "Tanner Linsley",
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/TanStack/hotkeys.git",
+ "directory": "packages/preact-hotkeys-devtools"
+ },
+ "homepage": "https://tanstack.com/hotkeys",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "keywords": [
+ "preact",
+ "tanstack",
+ "keys",
+ "devtools",
+ "hotkeys",
+ "keyboard"
+ ],
+ "scripts": {
+ "clean": "premove ./build ./dist",
+ "lint": "eslint ./src",
+ "lint:fix": "eslint ./src --fix",
+ "test:eslint": "eslint ./src",
+ "test:lib": "vitest --passWithNoTests",
+ "test:lib:dev": "pnpm test:lib --watch",
+ "test:types": "tsc",
+ "build": "tsdown"
+ },
+ "type": "module",
+ "types": "./dist/index.d.ts",
+ "exports": {
+ ".": "./dist/index.js",
+ "./package.json": "./package.json"
+ },
+ "sideEffects": false,
+ "engines": {
+ "node": ">=18"
+ },
+ "files": [
+ "dist/",
+ "src"
+ ],
+ "peerDependencies": {
+ "preact": ">=10.0.0"
+ },
+ "dependencies": {
+ "@tanstack/devtools-utils": "^0.3.0",
+ "@tanstack/hotkeys-devtools": "workspace:*"
+ },
+ "devDependencies": {
+ "@preact/preset-vite": "^2.10.2",
+ "preact": "^10.27.2"
+ }
+}
diff --git a/packages/preact-hotkeys-devtools/src/PreactHotkeysDevtools.tsx b/packages/preact-hotkeys-devtools/src/PreactHotkeysDevtools.tsx
new file mode 100644
index 0000000..9f5c90d
--- /dev/null
+++ b/packages/preact-hotkeys-devtools/src/PreactHotkeysDevtools.tsx
@@ -0,0 +1,10 @@
+import { createPreactPanel } from '@tanstack/devtools-utils/preact'
+import { HotkeysDevtoolsCore } from '@tanstack/hotkeys-devtools'
+import type { DevtoolsPanelProps } from '@tanstack/devtools-utils/preact'
+
+export interface HotkeysDevtoolsPreactInit extends DevtoolsPanelProps {}
+
+const [HotkeysDevtoolsPanel, HotkeysDevtoolsPanelNoOp] =
+ createPreactPanel(HotkeysDevtoolsCore)
+
+export { HotkeysDevtoolsPanel, HotkeysDevtoolsPanelNoOp }
diff --git a/packages/preact-hotkeys-devtools/src/index.ts b/packages/preact-hotkeys-devtools/src/index.ts
new file mode 100644
index 0000000..48d1293
--- /dev/null
+++ b/packages/preact-hotkeys-devtools/src/index.ts
@@ -0,0 +1,14 @@
+import * as Devtools from './PreactHotkeysDevtools'
+import * as plugin from './plugin'
+
+export const HotkeysDevtoolsPanel =
+ process.env.NODE_ENV !== 'development'
+ ? Devtools.HotkeysDevtoolsPanelNoOp
+ : Devtools.HotkeysDevtoolsPanel
+
+export const hotkeysDevtoolsPlugin =
+ process.env.NODE_ENV !== 'development'
+ ? plugin.hotkeysDevtoolsNoOpPlugin
+ : plugin.hotkeysDevtoolsPlugin
+
+export type { HotkeysDevtoolsPreactInit } from './PreactHotkeysDevtools'
diff --git a/packages/preact-hotkeys-devtools/src/plugin.tsx b/packages/preact-hotkeys-devtools/src/plugin.tsx
new file mode 100644
index 0000000..3618ee8
--- /dev/null
+++ b/packages/preact-hotkeys-devtools/src/plugin.tsx
@@ -0,0 +1,9 @@
+import { createPreactPlugin } from '@tanstack/devtools-utils/preact'
+import { HotkeysDevtoolsPanel } from './PreactHotkeysDevtools'
+
+const [hotkeysDevtoolsPlugin, hotkeysDevtoolsNoOpPlugin] = createPreactPlugin({
+ name: 'TanStack Hotkeys',
+ Component: HotkeysDevtoolsPanel,
+})
+
+export { hotkeysDevtoolsPlugin, hotkeysDevtoolsNoOpPlugin }
diff --git a/packages/preact-hotkeys-devtools/tsconfig.json b/packages/preact-hotkeys-devtools/tsconfig.json
new file mode 100644
index 0000000..5f2c38a
--- /dev/null
+++ b/packages/preact-hotkeys-devtools/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "extends": "../../tsconfig.json",
+ "include": ["src", "vitest.config.ts", "tests"],
+ "exclude": ["eslint.config.js"],
+ "compilerOptions": {
+ "jsx": "react-jsx",
+ "jsxImportSource": "preact"
+ }
+}
diff --git a/packages/preact-hotkeys-devtools/tsdown.config.ts b/packages/preact-hotkeys-devtools/tsdown.config.ts
new file mode 100644
index 0000000..178eef2
--- /dev/null
+++ b/packages/preact-hotkeys-devtools/tsdown.config.ts
@@ -0,0 +1,18 @@
+import { defineConfig } from 'tsdown'
+import preact from '@preact/preset-vite'
+
+export default defineConfig({
+ plugins: [preact()],
+ entry: ['./src/index.ts'],
+ format: ['esm'],
+ unbundle: true,
+ dts: true,
+ sourcemap: true,
+ clean: true,
+ minify: false,
+ fixedExtension: false,
+ exports: true,
+ publint: {
+ strict: true,
+ },
+})
diff --git a/packages/preact-hotkeys-devtools/vitest.config.ts b/packages/preact-hotkeys-devtools/vitest.config.ts
new file mode 100644
index 0000000..4fe5f0c
--- /dev/null
+++ b/packages/preact-hotkeys-devtools/vitest.config.ts
@@ -0,0 +1,14 @@
+import { defineConfig } from 'vitest/config'
+import preact from '@preact/preset-vite'
+import packageJson from './package.json' with { type: 'json' }
+
+export default defineConfig({
+ plugins: [preact()],
+ test: {
+ name: packageJson.name,
+ dir: './tests',
+ watch: false,
+ environment: 'happy-dom',
+ globals: true,
+ },
+})
diff --git a/packages/preact-hotkeys/README.md b/packages/preact-hotkeys/README.md
new file mode 100644
index 0000000..4609ad9
--- /dev/null
+++ b/packages/preact-hotkeys/README.md
@@ -0,0 +1,122 @@
+
+
+
+
+
+
+
+
+
+
+
+
+### [Become a Sponsor!](https://github.com/sponsors/tannerlinsley/)
+
+
+
+# TanStack Hotkeys
+
+> [!NOTE]
+> TanStack Hotkeys is pre-alpha (prototyping phase). We are actively developing the library and are open to feedback and contributions.
+
+Type-safe keyboard shortcuts for the web. Template-string bindings, parsed objects, a cross-platform `Mod` key, a singleton Hotkey Manager, and utilities for cheatsheet UIs—built to stay SSR-friendly.
+
+- Type-safe bindings — template strings (`Mod+Shift+S`, `Escape`) or parsed objects for full control
+- Flexible options — `keydown`/`keyup`, `preventDefault`, `stopPropagation`, conditional enabled, `requireReset`
+- Cross-platform Mod — maps to Cmd on macOS and Ctrl on Windows/Linux
+- Batteries included — validation + matching, sequences (Vim-style), key-state tracking, recorder UI helpers, React hooks, and devtools (in progress)
+
+### Read the docs →
+
+
+
+> [!NOTE]
+> You may know **TanStack Hotkeys** by our adapter names, too!
+>
+> - [**React Hotkeys**](https://tanstack.com/hotkeys/latest/docs/framework/react/react-hotkeys)
+> - [**Preact Hotkeys**](https://tanstack.com/hotkeys/latest/docs/framework/preact/preact-hotkeys)
+> - [**Solid Hotkeys**](https://tanstack.com/hotkeys/latest/docs/framework/solid/solid-hotkeys)
+> - Angular Hotkeys – needs a contributor!
+> - Svelte Hotkeys – needs a contributor!
+> - Vue Hotkeys – needs a contributor!
+
+## Get Involved
+
+- We welcome issues and pull requests!
+- Participate in [GitHub discussions](https://github.com/TanStack/hotkeys/discussions)
+- Chat with the community on [Discord](https://discord.com/invite/WrRKjPJ)
+- See [CONTRIBUTING.md](./CONTRIBUTING.md) for setup instructions
+
+## Partners
+
+
+
+
+
+
+
+
+We're looking for TanStack Hotkeys Partners to join our mission! Partner with us to push the boundaries of TanStack Hotkeys and build amazing things together.
+
+
LET'S CHAT
+
+
+
+
+## Explore the TanStack Ecosystem
+
+- TanStack Config – Tooling for JS/TS packages
+- TanStack DB – Reactive sync client store
+- TanStack DevTools – Unified devtools panel
+- TanStack Form – Type‑safe form state
+- TanStack Hotkeys – Type‑safe keyboard shortcuts
+- TanStack Query – Async state & caching
+- TanStack Ranger – Range & slider primitives
+- TanStack Router – Type‑safe routing, caching & URL state
+- TanStack Start – Full‑stack SSR & streaming
+- TanStack Store – Reactive data store
+- TanStack Table – Headless datagrids
+- TanStack Virtual – Virtualized rendering
+
+… and more at TanStack.com »
diff --git a/packages/preact-hotkeys/eslint.config.js b/packages/preact-hotkeys/eslint.config.js
new file mode 100644
index 0000000..3345087
--- /dev/null
+++ b/packages/preact-hotkeys/eslint.config.js
@@ -0,0 +1,8 @@
+// @ts-check
+
+import rootConfig from '../../eslint.config.js'
+
+/** @type {import('eslint').Linter.Config[]} */
+const config = [...rootConfig]
+
+export default config
diff --git a/packages/preact-hotkeys/package.json b/packages/preact-hotkeys/package.json
new file mode 100644
index 0000000..8f3e34a
--- /dev/null
+++ b/packages/preact-hotkeys/package.json
@@ -0,0 +1,63 @@
+{
+ "name": "@tanstack/preact-hotkeys",
+ "version": "0.3.0",
+ "description": "Preact adapter for TanStack Hotkeys",
+ "author": "Tanner Linsley",
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/TanStack/hotkeys.git",
+ "directory": "packages/preact-hotkeys"
+ },
+ "homepage": "https://tanstack.com/hotkeys",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "keywords": [
+ "preact",
+ "tanstack",
+ "keys"
+ ],
+ "scripts": {
+ "clean": "premove ./build ./dist",
+ "lint": "eslint ./src",
+ "lint:fix": "eslint ./src --fix",
+ "test:eslint": "eslint ./src",
+ "test:lib": "vitest --passWithNoTests",
+ "test:lib:dev": "pnpm test:lib --watch",
+ "test:types": "tsc",
+ "build": "tsdown"
+ },
+ "type": "module",
+ "main": "./dist/index.cjs",
+ "module": "./dist/index.js",
+ "types": "./dist/index.d.cts",
+ "exports": {
+ ".": {
+ "import": "./dist/index.js",
+ "require": "./dist/index.cjs"
+ },
+ "./package.json": "./package.json"
+ },
+ "sideEffects": false,
+ "engines": {
+ "node": ">=18"
+ },
+ "files": [
+ "dist",
+ "src"
+ ],
+ "dependencies": {
+ "@tanstack/hotkeys": "workspace:*",
+ "@tanstack/preact-store": "^0.11.0"
+ },
+ "devDependencies": {
+ "@preact/preset-vite": "^2.10.2",
+ "@testing-library/preact": "^3.2.4",
+ "preact": "^10.27.2"
+ },
+ "peerDependencies": {
+ "preact": ">=10.0.0"
+ }
+}
diff --git a/packages/preact-hotkeys/src/HotkeysProvider.tsx b/packages/preact-hotkeys/src/HotkeysProvider.tsx
new file mode 100644
index 0000000..71c3246
--- /dev/null
+++ b/packages/preact-hotkeys/src/HotkeysProvider.tsx
@@ -0,0 +1,52 @@
+import { createContext } from 'preact'
+import { useContext, useMemo } from 'preact/hooks'
+import type { ComponentChildren } from 'preact'
+import type { HotkeyRecorderOptions } from '@tanstack/hotkeys'
+import type { UseHotkeyOptions } from './useHotkey'
+import type { UseHotkeySequenceOptions } from './useHotkeySequence'
+
+export interface HotkeysProviderOptions {
+ hotkey?: Partial
+ hotkeyRecorder?: Partial
+ hotkeySequence?: Partial
+}
+
+interface HotkeysContextValue {
+ defaultOptions: HotkeysProviderOptions
+}
+
+const HotkeysContext = createContext(null)
+
+export interface HotkeysProviderProps {
+ children: ComponentChildren
+ defaultOptions?: HotkeysProviderOptions
+}
+
+const DEFAULT_OPTIONS: HotkeysProviderOptions = {}
+
+export function HotkeysProvider({
+ children,
+ defaultOptions = DEFAULT_OPTIONS,
+}: HotkeysProviderProps) {
+ const contextValue: HotkeysContextValue = useMemo(
+ () => ({
+ defaultOptions,
+ }),
+ [defaultOptions],
+ )
+
+ return (
+
+ {children}
+
+ )
+}
+
+export function useHotkeysContext() {
+ return useContext(HotkeysContext)
+}
+
+export function useDefaultHotkeysOptions() {
+ const context = useContext(HotkeysContext)
+ return context?.defaultOptions ?? {}
+}
diff --git a/packages/preact-hotkeys/src/index.ts b/packages/preact-hotkeys/src/index.ts
new file mode 100644
index 0000000..5d8a2ee
--- /dev/null
+++ b/packages/preact-hotkeys/src/index.ts
@@ -0,0 +1,13 @@
+// Re-export everything from the core package
+export * from '@tanstack/hotkeys'
+
+// provider
+export * from './HotkeysProvider'
+
+// Preact-specific exports
+export * from './useHotkey'
+export * from './useHeldKeys'
+export * from './useHeldKeyCodes'
+export * from './useKeyHold'
+export * from './useHotkeySequence'
+export * from './useHotkeyRecorder'
diff --git a/packages/preact-hotkeys/src/useHeldKeyCodes.ts b/packages/preact-hotkeys/src/useHeldKeyCodes.ts
new file mode 100644
index 0000000..e6b1b45
--- /dev/null
+++ b/packages/preact-hotkeys/src/useHeldKeyCodes.ts
@@ -0,0 +1,33 @@
+import { useStore } from '@tanstack/preact-store'
+import { getKeyStateTracker } from '@tanstack/hotkeys'
+
+/**
+ * Preact hook that returns a map of currently held key names to their physical `event.code` values.
+ *
+ * This is useful for debugging which physical key was pressed (e.g. distinguishing
+ * left vs right Shift via "ShiftLeft" / "ShiftRight").
+ *
+ * @returns Record mapping normalized key names to their `event.code` values
+ *
+ * @example
+ * ```tsx
+ * function KeyDebugDisplay() {
+ * const heldKeys = useHeldKeys()
+ * const heldCodes = useHeldKeyCodes()
+ *
+ * return (
+ *
+ * {heldKeys.map((key) => (
+ *
+ * {key} {heldCodes[key]}
+ *
+ * ))}
+ *
+ * )
+ * }
+ * ```
+ */
+export function useHeldKeyCodes(): Record {
+ const tracker = getKeyStateTracker()
+ return useStore(tracker.store, (state) => state.heldCodes)
+}
diff --git a/packages/preact-hotkeys/src/useHeldKeys.ts b/packages/preact-hotkeys/src/useHeldKeys.ts
new file mode 100644
index 0000000..98f27fb
--- /dev/null
+++ b/packages/preact-hotkeys/src/useHeldKeys.ts
@@ -0,0 +1,29 @@
+import { useStore } from '@tanstack/preact-store'
+import { getKeyStateTracker } from '@tanstack/hotkeys'
+
+/**
+ * Preact hook that returns an array of currently held keyboard keys.
+ *
+ * This hook uses `useStore` from `@tanstack/preact-store` to subscribe
+ * to the global KeyStateTracker and updates whenever keys are pressed
+ * or released.
+ *
+ * @returns Array of currently held key names
+ *
+ * @example
+ * ```tsx
+ * function KeyDisplay() {
+ * const heldKeys = useHeldKeys()
+ *
+ * return (
+ *
+ * Currently pressed: {heldKeys.join(' + ') || 'None'}
+ *
+ * )
+ * }
+ * ```
+ */
+export function useHeldKeys(): Array {
+ const tracker = getKeyStateTracker()
+ return useStore(tracker.store, (state) => state.heldKeys)
+}
diff --git a/packages/preact-hotkeys/src/useHotkey.ts b/packages/preact-hotkeys/src/useHotkey.ts
new file mode 100644
index 0000000..08eb40f
--- /dev/null
+++ b/packages/preact-hotkeys/src/useHotkey.ts
@@ -0,0 +1,192 @@
+import { useEffect, useRef } from 'preact/hooks'
+import {
+ detectPlatform,
+ formatHotkey,
+ getHotkeyManager,
+ rawHotkeyToParsedHotkey,
+} from '@tanstack/hotkeys'
+import { useDefaultHotkeysOptions } from './HotkeysProvider'
+import type { RefObject } from 'preact'
+import type {
+ Hotkey,
+ HotkeyCallback,
+ HotkeyOptions,
+ HotkeyRegistrationHandle,
+ RegisterableHotkey,
+} from '@tanstack/hotkeys'
+
+export interface UseHotkeyOptions extends Omit {
+ /**
+ * The DOM element to attach the event listener to.
+ * Can be a Preact ref, direct DOM element, or null.
+ * Defaults to document.
+ */
+ target?:
+ | RefObject
+ | HTMLElement
+ | Document
+ | Window
+ | null
+}
+
+/**
+ * Preact hook for registering a keyboard hotkey.
+ *
+ * Uses the singleton HotkeyManager for efficient event handling.
+ * The callback receives both the keyboard event and a context object
+ * containing the hotkey string and parsed hotkey.
+ *
+ * This hook syncs the callback and options on every render to avoid
+ * stale closures. This means
+ * callbacks that reference Preact state will always have access to
+ * the latest values.
+ *
+ * @param hotkey - The hotkey string (e.g., 'Mod+S', 'Escape') or RawHotkey object (supports `mod` for cross-platform)
+ * @param callback - The function to call when the hotkey is pressed
+ * @param options - Options for the hotkey behavior
+ *
+ * @example
+ * ```tsx
+ * function SaveButton() {
+ * const [count, setCount] = useState(0)
+ *
+ * // Callback always has access to latest count value
+ * useHotkey('Mod+S', (event, { hotkey }) => {
+ * console.log(`Save triggered, count is ${count}`)
+ * handleSave()
+ * })
+ *
+ * return setCount(c => c + 1)}>Count: {count}
+ * }
+ * ```
+ *
+ * @example
+ * ```tsx
+ * function Modal({ isOpen, onClose }) {
+ * // enabled option is synced on every render
+ * useHotkey('Escape', () => {
+ * onClose()
+ * }, { enabled: isOpen })
+ *
+ * if (!isOpen) return null
+ * return ...
+ * }
+ * ```
+ *
+ * @example
+ * ```tsx
+ * function Editor() {
+ * const editorRef = useRef(null)
+ *
+ * // Scoped to a specific element
+ * useHotkey('Mod+S', () => {
+ * save()
+ * }, { target: editorRef })
+ *
+ * return ...
+ * }
+ * ```
+ */
+export function useHotkey(
+ hotkey: RegisterableHotkey,
+ callback: HotkeyCallback,
+ options: UseHotkeyOptions = {},
+): void {
+ const mergedOptions = {
+ ...useDefaultHotkeysOptions().hotkey,
+ ...options,
+ } as UseHotkeyOptions
+
+ const manager = getHotkeyManager()
+
+ // Stable ref for registration handle
+ const registrationRef = useRef(null)
+
+ // Refs to capture current values for use in effect without adding dependencies
+ const callbackRef = useRef(callback)
+ const optionsRef = useRef(mergedOptions)
+ const managerRef = useRef(manager)
+
+ // Update refs on every render
+ callbackRef.current = callback
+ optionsRef.current = mergedOptions
+ managerRef.current = manager
+
+ // Track previous target and hotkey to detect changes requiring re-registration
+ const prevTargetRef = useRef(null)
+ const prevHotkeyRef = useRef(null)
+
+ // Normalize to hotkey string
+ const platform = mergedOptions.platform ?? detectPlatform()
+ const hotkeyString: Hotkey =
+ typeof hotkey === 'string'
+ ? hotkey
+ : (formatHotkey(rawHotkeyToParsedHotkey(hotkey, platform)) as Hotkey)
+
+ // Extract options without target (target is handled separately)
+ const { target: _target, ...optionsWithoutTarget } = mergedOptions
+
+ useEffect(() => {
+ // Resolve target inside the effect so refs are already attached after mount
+ const resolvedTarget = isRef(optionsRef.current.target)
+ ? optionsRef.current.target.current
+ : (optionsRef.current.target ??
+ (typeof document !== 'undefined' ? document : null))
+
+ // Skip if no valid target (SSR or ref still null)
+ if (!resolvedTarget) {
+ return
+ }
+
+ // Check if we need to re-register (target or hotkey changed)
+ const targetChanged =
+ prevTargetRef.current !== null && prevTargetRef.current !== resolvedTarget
+ const hotkeyChanged =
+ prevHotkeyRef.current !== null && prevHotkeyRef.current !== hotkeyString
+
+ // If we have an active registration and target/hotkey changed, unregister first
+ if (registrationRef.current?.isActive && (targetChanged || hotkeyChanged)) {
+ registrationRef.current.unregister()
+ registrationRef.current = null
+ }
+
+ // Register if needed (no active registration)
+ // Use refs to access current values without adding them to dependencies
+ if (!registrationRef.current || !registrationRef.current.isActive) {
+ registrationRef.current = managerRef.current.register(
+ hotkeyString,
+ callbackRef.current,
+ {
+ ...optionsRef.current,
+ target: resolvedTarget,
+ },
+ )
+ }
+
+ // Update tracking refs
+ prevTargetRef.current = resolvedTarget
+ prevHotkeyRef.current = hotkeyString
+
+ // Cleanup on unmount
+ return () => {
+ if (registrationRef.current?.isActive) {
+ registrationRef.current.unregister()
+ registrationRef.current = null
+ }
+ }
+ }, [hotkeyString, options.enabled])
+
+ // Sync callback and options on EVERY render (outside useEffect)
+ // This avoids stale closures - the callback always has access to latest state
+ if (registrationRef.current?.isActive) {
+ registrationRef.current.callback = callback
+ registrationRef.current.setOptions(optionsWithoutTarget)
+ }
+}
+
+/**
+ * Type guard to check if a value is a Preact ref-like object.
+ */
+function isRef(value: unknown): value is RefObject {
+ return value !== null && typeof value === 'object' && 'current' in value
+}
diff --git a/packages/preact-hotkeys/src/useHotkeyRecorder.ts b/packages/preact-hotkeys/src/useHotkeyRecorder.ts
new file mode 100644
index 0000000..5b4bba3
--- /dev/null
+++ b/packages/preact-hotkeys/src/useHotkeyRecorder.ts
@@ -0,0 +1,101 @@
+import { useEffect, useRef } from 'preact/hooks'
+import { useStore } from '@tanstack/preact-store'
+import { HotkeyRecorder } from '@tanstack/hotkeys'
+import { useDefaultHotkeysOptions } from './HotkeysProvider'
+import type { Hotkey, HotkeyRecorderOptions } from '@tanstack/hotkeys'
+
+export interface PreactHotkeyRecorder {
+ /** Whether recording is currently active */
+ isRecording: boolean
+ /** The currently recorded hotkey (for live preview) */
+ recordedHotkey: Hotkey | null
+ /** Start recording a new hotkey */
+ startRecording: () => void
+ /** Stop recording (same as cancel) */
+ stopRecording: () => void
+ /** Cancel recording without saving */
+ cancelRecording: () => void
+}
+
+/**
+ * Preact hook for recording keyboard shortcuts.
+ *
+ * This hook provides a thin wrapper around the framework-agnostic `HotkeyRecorder`
+ * class, managing all the complexity of capturing keyboard events, converting them
+ * to hotkey strings, and handling edge cases like Escape to cancel or Backspace/Delete
+ * to clear.
+ *
+ * @param options - Configuration options for the recorder
+ * @returns An object with recording state and control functions
+ *
+ * @example
+ * ```tsx
+ * function ShortcutSettings() {
+ * const [shortcut, setShortcut] = useState('Mod+S')
+ *
+ * const recorder = useHotkeyRecorder({
+ * onRecord: (hotkey) => {
+ * setShortcut(hotkey)
+ * },
+ * onCancel: () => {
+ * console.log('Recording cancelled')
+ * },
+ * })
+ *
+ * return (
+ *
+ *
+ * {recorder.isRecording ? 'Recording...' : 'Edit Shortcut'}
+ *
+ * {recorder.recordedHotkey && (
+ *
Recording: {recorder.recordedHotkey}
+ * )}
+ *
+ * )
+ * }
+ * ```
+ */
+export function useHotkeyRecorder(
+ options: HotkeyRecorderOptions,
+): PreactHotkeyRecorder {
+ const mergedOptions = {
+ ...useDefaultHotkeysOptions().hotkeyRecorder,
+ ...options,
+ } as HotkeyRecorderOptions
+
+ const recorderRef = useRef(null)
+
+ // Create recorder instance once
+ if (!recorderRef.current) {
+ recorderRef.current = new HotkeyRecorder(mergedOptions)
+ }
+
+ // Sync options on every render (same pattern as useHotkey)
+ // This ensures callbacks always have access to latest values
+ recorderRef.current.setOptions(mergedOptions)
+
+ // Subscribe to recorder state using useStore (same pattern as useHeldKeys)
+ const isRecording = useStore(
+ recorderRef.current.store,
+ (state) => state.isRecording,
+ )
+ const recordedHotkey = useStore(
+ recorderRef.current.store,
+ (state) => state.recordedHotkey,
+ )
+
+ // Cleanup on unmount
+ useEffect(() => {
+ return () => {
+ recorderRef.current?.destroy()
+ }
+ }, [])
+
+ return {
+ isRecording,
+ recordedHotkey,
+ startRecording: () => recorderRef.current?.start(),
+ stopRecording: () => recorderRef.current?.stop(),
+ cancelRecording: () => recorderRef.current?.cancel(),
+ }
+}
diff --git a/packages/preact-hotkeys/src/useHotkeySequence.ts b/packages/preact-hotkeys/src/useHotkeySequence.ts
new file mode 100644
index 0000000..7e34991
--- /dev/null
+++ b/packages/preact-hotkeys/src/useHotkeySequence.ts
@@ -0,0 +1,169 @@
+import { useEffect, useRef } from 'preact/hooks'
+import { formatHotkeySequence, getSequenceManager } from '@tanstack/hotkeys'
+import { useDefaultHotkeysOptions } from './HotkeysProvider'
+import type { RefObject } from 'preact'
+import type {
+ HotkeyCallback,
+ HotkeyCallbackContext,
+ HotkeySequence,
+ SequenceOptions,
+ SequenceRegistrationHandle,
+} from '@tanstack/hotkeys'
+
+export interface UseHotkeySequenceOptions extends Omit<
+ SequenceOptions,
+ 'target'
+> {
+ /**
+ * The DOM element to attach the event listener to.
+ * Can be a Preact ref, direct DOM element, or null.
+ * Defaults to document.
+ */
+ target?:
+ | RefObject
+ | HTMLElement
+ | Document
+ | Window
+ | null
+}
+
+/**
+ * Preact hook for registering a keyboard shortcut sequence (Vim-style).
+ *
+ * This hook allows you to register multi-key sequences like 'g g' or 'd d'
+ * that trigger when the full sequence is pressed within a timeout.
+ *
+ * @param sequence - Array of hotkey strings that form the sequence
+ * @param callback - Function to call when the sequence is completed
+ * @param options - Options for the sequence behavior
+ *
+ * @example
+ * ```tsx
+ * function VimEditor() {
+ * // 'g g' to go to top
+ * useHotkeySequence(['G', 'G'], () => {
+ * scrollToTop()
+ * })
+ *
+ * // 'd d' to delete line
+ * useHotkeySequence(['D', 'D'], () => {
+ * deleteLine()
+ * })
+ *
+ * // 'd i w' to delete inner word
+ * useHotkeySequence(['D', 'I', 'W'], () => {
+ * deleteInnerWord()
+ * }, { timeout: 500 })
+ *
+ * return ...
+ * }
+ * ```
+ */
+export function useHotkeySequence(
+ sequence: HotkeySequence,
+ callback: HotkeyCallback,
+ options: UseHotkeySequenceOptions = {},
+): void {
+ const mergedOptions = {
+ ...useDefaultHotkeysOptions().hotkeySequence,
+ ...options,
+ } as UseHotkeySequenceOptions
+
+ const manager = getSequenceManager()
+
+ // Stable ref for registration handle
+ const registrationRef = useRef(null)
+
+ // Refs to capture current values for use in effect without adding dependencies
+ const callbackRef = useRef(callback)
+ const optionsRef = useRef(mergedOptions)
+ const managerRef = useRef(manager)
+
+ // Update refs on every render
+ callbackRef.current = callback
+ optionsRef.current = mergedOptions
+ managerRef.current = manager
+
+ // Track previous target and sequence to detect changes requiring re-registration
+ const prevTargetRef = useRef(null)
+ const prevSequenceRef = useRef(null)
+
+ // Normalize to hotkey sequence string (join with spaces)
+ const hotkeySequenceString = formatHotkeySequence(sequence)
+
+ // Extract options without target (target is handled separately)
+ const { target: _target, ...optionsWithoutTarget } = mergedOptions
+
+ useEffect(() => {
+ if (sequence.length === 0) {
+ return
+ }
+
+ // Resolve target inside the effect so refs are already attached after mount
+ const resolvedTarget = isRef(optionsRef.current.target)
+ ? optionsRef.current.target.current
+ : (optionsRef.current.target ??
+ (typeof document !== 'undefined' ? document : null))
+
+ // Skip if no valid target (SSR or ref still null)
+ if (!resolvedTarget) {
+ return
+ }
+
+ // Check if we need to re-register (target or sequence changed)
+ const targetChanged =
+ prevTargetRef.current !== null && prevTargetRef.current !== resolvedTarget
+ const sequenceChanged =
+ prevSequenceRef.current !== null &&
+ prevSequenceRef.current !== hotkeySequenceString
+
+ // If we have an active registration and target/sequence changed, unregister first
+ if (
+ registrationRef.current?.isActive &&
+ (targetChanged || sequenceChanged)
+ ) {
+ registrationRef.current.unregister()
+ registrationRef.current = null
+ }
+
+ // Register if needed (no active registration)
+ if (!registrationRef.current || !registrationRef.current.isActive) {
+ registrationRef.current = managerRef.current.register(
+ sequence,
+ (event, context) => callbackRef.current(event, context),
+ {
+ ...optionsRef.current,
+ target: resolvedTarget,
+ },
+ )
+ }
+
+ // Update tracking refs
+ prevTargetRef.current = resolvedTarget
+ prevSequenceRef.current = hotkeySequenceString
+
+ // Cleanup on unmount
+ return () => {
+ if (registrationRef.current?.isActive) {
+ registrationRef.current.unregister()
+ registrationRef.current = null
+ }
+ }
+ }, [hotkeySequenceString, mergedOptions.enabled, sequence])
+
+ // Sync callback and options on EVERY render (outside useEffect)
+ if (registrationRef.current?.isActive) {
+ registrationRef.current.callback = (
+ event: KeyboardEvent,
+ context: HotkeyCallbackContext,
+ ) => callbackRef.current(event, context)
+ registrationRef.current.setOptions(optionsWithoutTarget)
+ }
+}
+
+/**
+ * Type guard to check if a value is a Preact ref-like object.
+ */
+function isRef(value: unknown): value is RefObject {
+ return value !== null && typeof value === 'object' && 'current' in value
+}
diff --git a/packages/preact-hotkeys/src/useKeyHold.ts b/packages/preact-hotkeys/src/useKeyHold.ts
new file mode 100644
index 0000000..1bc38cf
--- /dev/null
+++ b/packages/preact-hotkeys/src/useKeyHold.ts
@@ -0,0 +1,52 @@
+import { useStore } from '@tanstack/preact-store'
+import { getKeyStateTracker } from '@tanstack/hotkeys'
+import type { HeldKey } from '@tanstack/hotkeys'
+
+/**
+ * Preact hook that returns whether a specific key is currently being held.
+ *
+ * This hook uses `useStore` from `@tanstack/preact-store` to subscribe
+ * to the global KeyStateTracker and uses a selector to determine if
+ * the specified key is held.
+ *
+ * @param key - The key to check (e.g., 'Shift', 'Control', 'A')
+ * @returns True if the key is currently held down
+ *
+ * @example
+ * ```tsx
+ * function ShiftIndicator() {
+ * const isShiftHeld = useKeyHold('Shift')
+ *
+ * return (
+ *
+ * {isShiftHeld ? 'Shift is pressed!' : 'Press Shift'}
+ *
+ * )
+ * }
+ * ```
+ *
+ * @example
+ * ```tsx
+ * function ModifierIndicators() {
+ * const ctrl = useKeyHold('Control')
+ * const shift = useKeyHold('Shift')
+ * const alt = useKeyHold('Alt')
+ *
+ * return (
+ *
+ * Ctrl
+ * Shift
+ * Alt
+ *
+ * )
+ * }
+ * ```
+ */
+export function useKeyHold(key: HeldKey): boolean {
+ const tracker = getKeyStateTracker()
+ const normalizedKey = key.toLowerCase()
+
+ return useStore(tracker.store, (state) =>
+ state.heldKeys.some((heldKey) => heldKey.toLowerCase() === normalizedKey),
+ )
+}
diff --git a/packages/preact-hotkeys/tests/useHotkey.test.tsx b/packages/preact-hotkeys/tests/useHotkey.test.tsx
new file mode 100644
index 0000000..bcee773
--- /dev/null
+++ b/packages/preact-hotkeys/tests/useHotkey.test.tsx
@@ -0,0 +1,224 @@
+// @vitest-environment happy-dom
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
+import { cleanup, render } from '@testing-library/preact'
+import { useRef } from 'preact/hooks'
+import { HotkeyManager } from '@tanstack/hotkeys'
+import { useHotkey } from '../src/useHotkey'
+import type { HotkeyCallback } from '@tanstack/hotkeys'
+
+function HotkeyTestComponent({
+ callback,
+ enabled = true,
+ eventType = 'keydown',
+}: {
+ callback: HotkeyCallback
+ enabled?: boolean
+ eventType?: 'keydown' | 'keyup'
+}) {
+ useHotkey('Mod+S', callback, { platform: 'mac', enabled, eventType })
+ return null
+}
+
+describe('useHotkey', () => {
+ beforeEach(() => {
+ HotkeyManager.resetInstance()
+ })
+
+ afterEach(() => {
+ HotkeyManager.resetInstance()
+ cleanup()
+ })
+
+ it('should register a hotkey handler', () => {
+ const callback = vi.fn()
+ const addEventListenerSpy = vi.spyOn(document, 'addEventListener')
+
+ render( )
+
+ expect(addEventListenerSpy).toHaveBeenCalledWith(
+ 'keydown',
+ expect.any(Function),
+ )
+
+ addEventListenerSpy.mockRestore()
+ })
+
+ it('should remove handler on unmount', () => {
+ const callback = vi.fn()
+ const removeEventListenerSpy = vi.spyOn(document, 'removeEventListener')
+
+ const { unmount } = render( )
+ unmount()
+
+ expect(removeEventListenerSpy).toHaveBeenCalledWith(
+ 'keydown',
+ expect.any(Function),
+ )
+
+ removeEventListenerSpy.mockRestore()
+ })
+
+ it('should call callback when hotkey matches', () => {
+ const callback = vi.fn()
+ render( )
+
+ document.dispatchEvent(
+ new KeyboardEvent('keydown', {
+ key: 's',
+ metaKey: true,
+ bubbles: true,
+ }),
+ )
+
+ expect(callback).toHaveBeenCalled()
+ })
+
+ it('should not call callback when hotkey does not match', () => {
+ const callback = vi.fn()
+ render( )
+
+ document.dispatchEvent(
+ new KeyboardEvent('keydown', {
+ key: 'a',
+ metaKey: true,
+ bubbles: true,
+ }),
+ )
+
+ expect(callback).not.toHaveBeenCalled()
+ })
+
+ it('should use keyup event when specified', () => {
+ const callback = vi.fn()
+ const addEventListenerSpy = vi.spyOn(document, 'addEventListener')
+
+ render(
+ ,
+ )
+
+ expect(addEventListenerSpy).toHaveBeenCalledWith(
+ 'keyup',
+ expect.any(Function),
+ )
+
+ addEventListenerSpy.mockRestore()
+ })
+
+ describe('stale closure prevention', () => {
+ function ClosureComponent({
+ count,
+ capturedValues,
+ }: {
+ count: number
+ capturedValues: Array
+ }) {
+ useHotkey(
+ 'Mod+S',
+ () => {
+ capturedValues.push(count)
+ },
+ { platform: 'mac' },
+ )
+ return null
+ }
+
+ it('should have access to latest state values in callback', () => {
+ const capturedValues: Array = []
+ const { rerender } = render(
+ ,
+ )
+
+ document.dispatchEvent(
+ new KeyboardEvent('keydown', {
+ key: 's',
+ metaKey: true,
+ bubbles: true,
+ }),
+ )
+ expect(capturedValues).toEqual([0])
+
+ rerender( )
+ document.dispatchEvent(
+ new KeyboardEvent('keydown', {
+ key: 's',
+ metaKey: true,
+ bubbles: true,
+ }),
+ )
+ expect(capturedValues).toEqual([0, 5])
+
+ rerender( )
+ document.dispatchEvent(
+ new KeyboardEvent('keydown', {
+ key: 's',
+ metaKey: true,
+ bubbles: true,
+ }),
+ )
+ expect(capturedValues).toEqual([0, 5, 10])
+ })
+
+ it('should sync enabled option on every render', () => {
+ const callback = vi.fn()
+ const { rerender } = render(
+ ,
+ )
+
+ document.dispatchEvent(
+ new KeyboardEvent('keydown', {
+ key: 's',
+ metaKey: true,
+ bubbles: true,
+ }),
+ )
+ expect(callback).toHaveBeenCalledTimes(1)
+
+ rerender( )
+ document.dispatchEvent(
+ new KeyboardEvent('keydown', {
+ key: 's',
+ metaKey: true,
+ bubbles: true,
+ }),
+ )
+ expect(callback).toHaveBeenCalledTimes(1)
+
+ rerender( )
+ document.dispatchEvent(
+ new KeyboardEvent('keydown', {
+ key: 's',
+ metaKey: true,
+ bubbles: true,
+ }),
+ )
+ expect(callback).toHaveBeenCalledTimes(2)
+ })
+ })
+
+ describe('target handling', () => {
+ function RefTargetComponent({ callback }: { callback: HotkeyCallback }) {
+ const ref = useRef(null)
+ useHotkey('Mod+S', callback, { target: ref, platform: 'mac' })
+ return null
+ }
+
+ it('should wait for ref to be attached', () => {
+ const callback = vi.fn()
+ render( )
+
+ document.dispatchEvent(
+ new KeyboardEvent('keydown', {
+ key: 's',
+ metaKey: true,
+ bubbles: true,
+ }),
+ )
+
+ expect(callback).not.toHaveBeenCalled()
+ })
+ })
+})
diff --git a/packages/preact-hotkeys/tsconfig.docs.json b/packages/preact-hotkeys/tsconfig.docs.json
new file mode 100644
index 0000000..08866d6
--- /dev/null
+++ b/packages/preact-hotkeys/tsconfig.docs.json
@@ -0,0 +1,9 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "paths": {
+ "@tanstack/hotkeys": ["../hotkeys/src"]
+ }
+ },
+ "include": ["src"]
+}
diff --git a/packages/preact-hotkeys/tsconfig.json b/packages/preact-hotkeys/tsconfig.json
new file mode 100644
index 0000000..40dbbc8
--- /dev/null
+++ b/packages/preact-hotkeys/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "jsx": "react-jsx",
+ "jsxImportSource": "preact"
+ },
+ "include": ["src", "vitest.config.ts", "tests"],
+ "exclude": ["eslint.config.js"]
+}
diff --git a/packages/preact-hotkeys/tsdown.config.ts b/packages/preact-hotkeys/tsdown.config.ts
new file mode 100644
index 0000000..71071cb
--- /dev/null
+++ b/packages/preact-hotkeys/tsdown.config.ts
@@ -0,0 +1,16 @@
+import { defineConfig } from 'tsdown'
+
+export default defineConfig({
+ entry: ['./src/index.ts'],
+ format: ['esm', 'cjs'],
+ unbundle: true,
+ dts: true,
+ sourcemap: true,
+ clean: true,
+ minify: false,
+ fixedExtension: false,
+ exports: true,
+ publint: {
+ strict: true,
+ },
+})
diff --git a/packages/preact-hotkeys/vitest.config.ts b/packages/preact-hotkeys/vitest.config.ts
new file mode 100644
index 0000000..743da04
--- /dev/null
+++ b/packages/preact-hotkeys/vitest.config.ts
@@ -0,0 +1,15 @@
+import { defineConfig } from 'vitest/config'
+import preact from '@preact/preset-vite'
+import packageJson from './package.json' with { type: 'json' }
+
+export default defineConfig({
+ plugins: [preact()],
+ test: {
+ name: packageJson.name,
+ dir: './tests',
+ watch: false,
+ environment: 'happy-dom',
+ // setupFiles: ['./tests/test-setup.ts'],
+ globals: true,
+ },
+})
diff --git a/packages/react-hotkeys-devtools/README.md b/packages/react-hotkeys-devtools/README.md
index 2da2e21..4609ad9 100644
--- a/packages/react-hotkeys-devtools/README.md
+++ b/packages/react-hotkeys-devtools/README.md
@@ -54,7 +54,8 @@ Type-safe keyboard shortcuts for the web. Template-string bindings, parsed objec
> You may know **TanStack Hotkeys** by our adapter names, too!
>
> - [**React Hotkeys**](https://tanstack.com/hotkeys/latest/docs/framework/react/react-hotkeys)
-> - Solid Hotkeys – needs a contributor!
+> - [**Preact Hotkeys**](https://tanstack.com/hotkeys/latest/docs/framework/preact/preact-hotkeys)
+> - [**Solid Hotkeys**](https://tanstack.com/hotkeys/latest/docs/framework/solid/solid-hotkeys)
> - Angular Hotkeys – needs a contributor!
> - Svelte Hotkeys – needs a contributor!
> - Vue Hotkeys – needs a contributor!
diff --git a/packages/react-hotkeys/README.md b/packages/react-hotkeys/README.md
index 2da2e21..4609ad9 100644
--- a/packages/react-hotkeys/README.md
+++ b/packages/react-hotkeys/README.md
@@ -54,7 +54,8 @@ Type-safe keyboard shortcuts for the web. Template-string bindings, parsed objec
> You may know **TanStack Hotkeys** by our adapter names, too!
>
> - [**React Hotkeys**](https://tanstack.com/hotkeys/latest/docs/framework/react/react-hotkeys)
-> - Solid Hotkeys – needs a contributor!
+> - [**Preact Hotkeys**](https://tanstack.com/hotkeys/latest/docs/framework/preact/preact-hotkeys)
+> - [**Solid Hotkeys**](https://tanstack.com/hotkeys/latest/docs/framework/solid/solid-hotkeys)
> - Angular Hotkeys – needs a contributor!
> - Svelte Hotkeys – needs a contributor!
> - Vue Hotkeys – needs a contributor!
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 72153f4..a663c03 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -81,6 +81,131 @@ importers:
specifier: ^4.0.18
version: 4.0.18(@types/node@25.3.0)(happy-dom@20.7.0)(jiti@2.6.1)(yaml@2.8.2)
+ examples/preact/useHeldKeys:
+ dependencies:
+ '@tanstack/preact-hotkeys':
+ specifier: ^0.3.0
+ version: link:../../../packages/preact-hotkeys
+ preact:
+ specifier: ^10.27.2
+ version: 10.28.4
+ devDependencies:
+ '@preact/preset-vite':
+ specifier: ^2.10.2
+ version: 2.10.3(@babel/core@7.29.0)(preact@10.28.4)(rollup@4.58.0)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(yaml@2.8.2))
+ '@tanstack/preact-devtools':
+ specifier: 0.9.6
+ version: 0.9.6(csstype@3.2.3)(preact@10.28.4)(solid-js@1.9.11)
+ '@tanstack/preact-hotkeys-devtools':
+ specifier: ^0.3.0
+ version: link:../../../packages/preact-hotkeys-devtools
+ typescript:
+ specifier: 5.9.3
+ version: 5.9.3
+ vite:
+ specifier: ^7.3.1
+ version: 7.3.1(@types/node@25.3.0)(jiti@2.6.1)(yaml@2.8.2)
+
+ examples/preact/useHotkey:
+ dependencies:
+ '@tanstack/preact-hotkeys':
+ specifier: ^0.3.0
+ version: link:../../../packages/preact-hotkeys
+ preact:
+ specifier: ^10.27.2
+ version: 10.28.4
+ devDependencies:
+ '@preact/preset-vite':
+ specifier: ^2.10.2
+ version: 2.10.3(@babel/core@7.29.0)(preact@10.28.4)(rollup@4.58.0)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(yaml@2.8.2))
+ '@tanstack/preact-devtools':
+ specifier: 0.9.6
+ version: 0.9.6(csstype@3.2.3)(preact@10.28.4)(solid-js@1.9.11)
+ '@tanstack/preact-hotkeys-devtools':
+ specifier: ^0.3.0
+ version: link:../../../packages/preact-hotkeys-devtools
+ typescript:
+ specifier: 5.9.3
+ version: 5.9.3
+ vite:
+ specifier: ^7.3.1
+ version: 7.3.1(@types/node@25.3.0)(jiti@2.6.1)(yaml@2.8.2)
+
+ examples/preact/useHotkeyRecorder:
+ dependencies:
+ '@tanstack/preact-hotkeys':
+ specifier: ^0.3.0
+ version: link:../../../packages/preact-hotkeys
+ preact:
+ specifier: ^10.27.2
+ version: 10.28.4
+ devDependencies:
+ '@preact/preset-vite':
+ specifier: ^2.10.2
+ version: 2.10.3(@babel/core@7.29.0)(preact@10.28.4)(rollup@4.58.0)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(yaml@2.8.2))
+ '@tanstack/preact-devtools':
+ specifier: 0.9.6
+ version: 0.9.6(csstype@3.2.3)(preact@10.28.4)(solid-js@1.9.11)
+ '@tanstack/preact-hotkeys-devtools':
+ specifier: ^0.3.0
+ version: link:../../../packages/preact-hotkeys-devtools
+ typescript:
+ specifier: 5.9.3
+ version: 5.9.3
+ vite:
+ specifier: ^7.3.1
+ version: 7.3.1(@types/node@25.3.0)(jiti@2.6.1)(yaml@2.8.2)
+
+ examples/preact/useHotkeySequence:
+ dependencies:
+ '@tanstack/preact-hotkeys':
+ specifier: ^0.3.0
+ version: link:../../../packages/preact-hotkeys
+ preact:
+ specifier: ^10.27.2
+ version: 10.28.4
+ devDependencies:
+ '@preact/preset-vite':
+ specifier: ^2.10.2
+ version: 2.10.3(@babel/core@7.29.0)(preact@10.28.4)(rollup@4.58.0)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(yaml@2.8.2))
+ '@tanstack/preact-devtools':
+ specifier: 0.9.6
+ version: 0.9.6(csstype@3.2.3)(preact@10.28.4)(solid-js@1.9.11)
+ '@tanstack/preact-hotkeys-devtools':
+ specifier: ^0.3.0
+ version: link:../../../packages/preact-hotkeys-devtools
+ typescript:
+ specifier: 5.9.3
+ version: 5.9.3
+ vite:
+ specifier: ^7.3.1
+ version: 7.3.1(@types/node@25.3.0)(jiti@2.6.1)(yaml@2.8.2)
+
+ examples/preact/useKeyhold:
+ dependencies:
+ '@tanstack/preact-hotkeys':
+ specifier: ^0.3.0
+ version: link:../../../packages/preact-hotkeys
+ preact:
+ specifier: ^10.27.2
+ version: 10.28.4
+ devDependencies:
+ '@preact/preset-vite':
+ specifier: ^2.10.2
+ version: 2.10.3(@babel/core@7.29.0)(preact@10.28.4)(rollup@4.58.0)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(yaml@2.8.2))
+ '@tanstack/preact-devtools':
+ specifier: 0.9.6
+ version: 0.9.6(csstype@3.2.3)(preact@10.28.4)(solid-js@1.9.11)
+ '@tanstack/preact-hotkeys-devtools':
+ specifier: ^0.3.0
+ version: link:../../../packages/preact-hotkeys-devtools
+ typescript:
+ specifier: 5.9.3
+ version: 5.9.3
+ vite:
+ specifier: ^7.3.1
+ version: 7.3.1(@types/node@25.3.0)(jiti@2.6.1)(yaml@2.8.2)
+
examples/react/useHeldKeys:
dependencies:
'@tanstack/react-hotkeys':
@@ -255,15 +380,15 @@ importers:
dependencies:
'@tanstack/devtools-utils':
specifier: ^0.3.0
- version: 0.3.0(@types/react@19.2.14)(csstype@3.2.3)(react@19.2.4)(solid-js@1.9.11)
+ version: 0.3.0(@types/react@19.2.14)(csstype@3.2.3)(preact@10.28.4)(react@19.2.4)(solid-js@1.9.11)
'@tanstack/solid-devtools':
specifier: 0.7.26
version: 0.7.26(csstype@3.2.3)(solid-js@1.9.11)
'@tanstack/solid-hotkeys':
- specifier: workspace:*
+ specifier: ^0.3.0
version: link:../../../packages/solid-hotkeys
'@tanstack/solid-hotkeys-devtools':
- specifier: workspace:*
+ specifier: ^0.3.0
version: link:../../../packages/solid-hotkeys-devtools
solid-js:
specifier: ^1.9.11
@@ -280,15 +405,15 @@ importers:
dependencies:
'@tanstack/devtools-utils':
specifier: ^0.3.0
- version: 0.3.0(@types/react@19.2.14)(csstype@3.2.3)(react@19.2.4)(solid-js@1.9.11)
+ version: 0.3.0(@types/react@19.2.14)(csstype@3.2.3)(preact@10.28.4)(react@19.2.4)(solid-js@1.9.11)
'@tanstack/solid-devtools':
specifier: 0.7.26
version: 0.7.26(csstype@3.2.3)(solid-js@1.9.11)
'@tanstack/solid-hotkeys':
- specifier: workspace:*
+ specifier: ^0.3.0
version: link:../../../packages/solid-hotkeys
'@tanstack/solid-hotkeys-devtools':
- specifier: workspace:*
+ specifier: ^0.3.0
version: link:../../../packages/solid-hotkeys-devtools
solid-js:
specifier: ^1.9.11
@@ -311,15 +436,15 @@ importers:
dependencies:
'@tanstack/devtools-utils':
specifier: ^0.3.0
- version: 0.3.0(@types/react@19.2.14)(csstype@3.2.3)(react@19.2.4)(solid-js@1.9.11)
+ version: 0.3.0(@types/react@19.2.14)(csstype@3.2.3)(preact@10.28.4)(react@19.2.4)(solid-js@1.9.11)
'@tanstack/solid-devtools':
specifier: 0.7.26
version: 0.7.26(csstype@3.2.3)(solid-js@1.9.11)
'@tanstack/solid-hotkeys':
- specifier: workspace:*
+ specifier: ^0.3.0
version: link:../../../packages/solid-hotkeys
'@tanstack/solid-hotkeys-devtools':
- specifier: workspace:*
+ specifier: ^0.3.0
version: link:../../../packages/solid-hotkeys-devtools
solid-js:
specifier: ^1.9.11
@@ -336,15 +461,15 @@ importers:
dependencies:
'@tanstack/devtools-utils':
specifier: ^0.3.0
- version: 0.3.0(@types/react@19.2.14)(csstype@3.2.3)(react@19.2.4)(solid-js@1.9.11)
+ version: 0.3.0(@types/react@19.2.14)(csstype@3.2.3)(preact@10.28.4)(react@19.2.4)(solid-js@1.9.11)
'@tanstack/solid-devtools':
specifier: 0.7.26
version: 0.7.26(csstype@3.2.3)(solid-js@1.9.11)
'@tanstack/solid-hotkeys':
- specifier: workspace:*
+ specifier: ^0.3.0
version: link:../../../packages/solid-hotkeys
'@tanstack/solid-hotkeys-devtools':
- specifier: workspace:*
+ specifier: ^0.3.0
version: link:../../../packages/solid-hotkeys-devtools
solid-js:
specifier: ^1.9.11
@@ -361,15 +486,15 @@ importers:
dependencies:
'@tanstack/devtools-utils':
specifier: ^0.3.0
- version: 0.3.0(@types/react@19.2.14)(csstype@3.2.3)(react@19.2.4)(solid-js@1.9.11)
+ version: 0.3.0(@types/react@19.2.14)(csstype@3.2.3)(preact@10.28.4)(react@19.2.4)(solid-js@1.9.11)
'@tanstack/solid-devtools':
specifier: 0.7.26
version: 0.7.26(csstype@3.2.3)(solid-js@1.9.11)
'@tanstack/solid-hotkeys':
- specifier: workspace:*
+ specifier: ^0.3.0
version: link:../../../packages/solid-hotkeys
'@tanstack/solid-hotkeys-devtools':
- specifier: workspace:*
+ specifier: ^0.3.0
version: link:../../../packages/solid-hotkeys-devtools
solid-js:
specifier: ^1.9.11
@@ -395,7 +520,7 @@ importers:
version: 0.4.4(csstype@3.2.3)(solid-js@1.9.11)
'@tanstack/devtools-utils':
specifier: ^0.3.0
- version: 0.3.0(@types/react@19.2.14)(csstype@3.2.3)(react@19.2.4)(solid-js@1.9.11)
+ version: 0.3.0(@types/react@19.2.14)(csstype@3.2.3)(preact@10.28.4)(react@19.2.4)(solid-js@1.9.11)
'@tanstack/hotkeys':
specifier: workspace:*
version: link:../hotkeys
@@ -413,6 +538,41 @@ importers:
specifier: ^2.11.10
version: 2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.11)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(yaml@2.8.2))
+ packages/preact-hotkeys:
+ dependencies:
+ '@tanstack/hotkeys':
+ specifier: workspace:*
+ version: link:../hotkeys
+ '@tanstack/preact-store':
+ specifier: ^0.11.0
+ version: 0.11.1(preact@10.28.4)
+ devDependencies:
+ '@preact/preset-vite':
+ specifier: ^2.10.2
+ version: 2.10.3(@babel/core@7.29.0)(preact@10.28.4)(rollup@4.58.0)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(yaml@2.8.2))
+ '@testing-library/preact':
+ specifier: ^3.2.4
+ version: 3.2.4(preact@10.28.4)
+ preact:
+ specifier: ^10.27.2
+ version: 10.28.4
+
+ packages/preact-hotkeys-devtools:
+ dependencies:
+ '@tanstack/devtools-utils':
+ specifier: ^0.3.0
+ version: 0.3.0(@types/react@19.2.14)(csstype@3.2.3)(preact@10.28.4)(react@19.2.4)(solid-js@1.9.11)
+ '@tanstack/hotkeys-devtools':
+ specifier: workspace:*
+ version: link:../hotkeys-devtools
+ devDependencies:
+ '@preact/preset-vite':
+ specifier: ^2.10.2
+ version: 2.10.3(@babel/core@7.29.0)(preact@10.28.4)(rollup@4.58.0)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(yaml@2.8.2))
+ preact:
+ specifier: ^10.27.2
+ version: 10.28.4
+
packages/react-hotkeys:
dependencies:
'@tanstack/hotkeys':
@@ -451,7 +611,7 @@ importers:
dependencies:
'@tanstack/devtools-utils':
specifier: ^0.3.0
- version: 0.3.0(@types/react@19.2.14)(csstype@3.2.3)(react@19.2.4)(solid-js@1.9.11)
+ version: 0.3.0(@types/react@19.2.14)(csstype@3.2.3)(preact@10.28.4)(react@19.2.4)(solid-js@1.9.11)
'@tanstack/hotkeys-devtools':
specifier: workspace:*
version: link:../hotkeys-devtools
@@ -507,7 +667,7 @@ importers:
dependencies:
'@tanstack/devtools-utils':
specifier: ^0.3.0
- version: 0.3.0(@types/react@19.2.14)(csstype@3.2.3)(react@19.2.4)(solid-js@1.9.11)
+ version: 0.3.0(@types/react@19.2.14)(csstype@3.2.3)(preact@10.28.4)(react@19.2.4)(solid-js@1.9.11)
'@tanstack/hotkeys-devtools':
specifier: workspace:*
version: link:../hotkeys-devtools
@@ -645,6 +805,12 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0-0
+ '@babel/plugin-transform-react-jsx-development@7.27.1':
+ resolution: {integrity: sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
'@babel/plugin-transform-react-jsx-self@7.27.1':
resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==}
engines: {node: '>=6.9.0'}
@@ -657,6 +823,12 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0-0
+ '@babel/plugin-transform-react-jsx@7.28.6':
+ resolution: {integrity: sha512-61bxqhiRfAACulXSLd/GxqmAedUSrRZIu/cbaT18T1CetkTmtDN15it7i80ru4DVqRK1WMxQhXs+Lf9kajm5Ow==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
'@babel/runtime@7.28.6':
resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==}
engines: {node: '>=6.9.0'}
@@ -1246,6 +1418,29 @@ packages:
cpu: [x64]
os: [win32]
+ '@preact/preset-vite@2.10.3':
+ resolution: {integrity: sha512-1SiS+vFItpkNdBs7q585PSAIln0wBeBdcpJYbzPs1qipsb/FssnkUioNXuRsb8ZnU8YEQHr+3v8+/mzWSnTQmg==}
+ peerDependencies:
+ '@babel/core': 7.x
+ vite: 2.x || 3.x || 4.x || 5.x || 6.x || 7.x
+
+ '@prefresh/babel-plugin@0.5.3':
+ resolution: {integrity: sha512-57LX2SHs4BX2s1IwCjNzTE2OJeEepRCNf1VTEpbNcUyHfMO68eeOWGDIt4ob9aYlW6PEWZ1SuwNikuoIXANDtQ==}
+
+ '@prefresh/core@1.5.9':
+ resolution: {integrity: sha512-IKBKCPaz34OFVC+adiQ2qaTF5qdztO2/4ZPf4KsRTgjKosWqxVXmEbxCiUydYZRY8GVie+DQlKzQr9gt6HQ+EQ==}
+ peerDependencies:
+ preact: ^10.0.0 || ^11.0.0-0
+
+ '@prefresh/utils@1.2.1':
+ resolution: {integrity: sha512-vq/sIuN5nYfYzvyayXI4C2QkprfNaHUQ9ZX+3xLD8nL3rWyzpxOm1+K7RtMbhd+66QcaISViK7amjnheQ/4WZw==}
+
+ '@prefresh/vite@2.4.12':
+ resolution: {integrity: sha512-FY1fzXpUjiuosznMV0YM7XAOPZjB5FIdWS0W24+XnlxYkt9hNAwwsiKYn+cuTEoMtD/ZVazS5QVssBr9YhpCQA==}
+ peerDependencies:
+ preact: ^10.4.0 || ^11.0.0-0
+ vite: '>=2.0.0'
+
'@publint/pack@0.1.4':
resolution: {integrity: sha512-HDVTWq3H0uTXiU0eeSQntcVUTPP3GamzeXI41+x7uU9J65JgWQh3qWZHblR1i0npXfFtF+mxBiU2nJH8znxWnQ==}
engines: {node: '>=18'}
@@ -1337,6 +1532,19 @@ packages:
'@rolldown/pluginutils@1.0.0-rc.3':
resolution: {integrity: sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==}
+ '@rollup/pluginutils@4.2.1':
+ resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==}
+ engines: {node: '>= 8.0.0'}
+
+ '@rollup/pluginutils@5.3.0':
+ resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==}
+ engines: {node: '>=14.0.0'}
+ peerDependencies:
+ rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0
+ peerDependenciesMeta:
+ rollup:
+ optional: true
+
'@rollup/rollup-android-arm-eabi@4.58.0':
resolution: {integrity: sha512-mr0tmS/4FoVk1cnaeN244A/wjvGDNItZKR8hRhnmCzygyRXYtKF5jVDSIILR1U97CTzAYmbgIj/Dukg62ggG5w==}
cpu: [arm]
@@ -1572,6 +1780,10 @@ packages:
resolution: {integrity: sha512-hsNDE3iu4frt9cC2ppn1mNRnLKo2uc1/1hXAyY9z4UYb+o40M2clFAhiFoo4HngjfGJDV3x18KVVIq7W4Un+zA==}
engines: {node: '>=18'}
+ '@tanstack/devtools-event-bus@0.4.0':
+ resolution: {integrity: sha512-1t+/csFuDzi+miDxAOh6Xv7VDE80gJEItkTcAZLjV5MRulbO/W8ocjHLI2Do/p2r2/FBU0eKCRTpdqvXaYoHpQ==}
+ engines: {node: '>=18'}
+
'@tanstack/devtools-event-bus@0.4.1':
resolution: {integrity: sha512-cNnJ89Q021Zf883rlbBTfsaxTfi2r73/qejGtyTa7ksErF3hyDyAq1aTbo5crK9dAL7zSHh9viKY1BtMls1QOA==}
engines: {node: '>=18'}
@@ -1607,6 +1819,12 @@ packages:
vue:
optional: true
+ '@tanstack/devtools@0.10.2':
+ resolution: {integrity: sha512-6TPNl3jTrCFpyV3m9lBeHxum6btmiihbv+A3xkDpt3JScRcWP1a8G5rZzKhlOtikzG1QSiceRrbckKnIAvZ7FQ==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ solid-js: '>=1.9.7'
+
'@tanstack/devtools@0.10.7':
resolution: {integrity: sha512-ScwnFjJTMRUd6miQax7sEhq9winalQIVhm0MTX3YfjoGjMzB/jzjzYlLOraen8hcxMHH9CifTjio8ZVdqSRBRg==}
engines: {node: '>=18'}
@@ -1619,6 +1837,17 @@ packages:
peerDependencies:
eslint: ^9.0.0 || ^10.0.0
+ '@tanstack/preact-devtools@0.9.6':
+ resolution: {integrity: sha512-eOvLrKYtst1NGXGr0TrQgjT0/kweMmVfEigEDs1GreCjFK+F1YHH+bP5nvM55Y/P8p3gUwv8cWFH3HsmA8Q2rA==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ preact: '>=10.0.0'
+
+ '@tanstack/preact-store@0.11.1':
+ resolution: {integrity: sha512-dbaXYZX2YVtxVRcGJsSUCTH8wfAB+xFkKfnRGyEI1Z4s4js6HHbUK7GtMTTo6MCyNHYZqDI1w49Dj8Gj+IbwEA==}
+ peerDependencies:
+ preact: ^10.0.0
+
'@tanstack/react-devtools@0.9.6':
resolution: {integrity: sha512-4wnhqQ1o5PnmEDV8L3yLWaE2ZWD2xjdUw1X8Uv5NK9Ekrz/Qr6iuYl+X4Kq9+Ix2luVGMqd3toFvEwkr3uMFBw==}
engines: {node: '>=18'}
@@ -1656,10 +1885,20 @@ packages:
resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==}
engines: {node: '>=18'}
+ '@testing-library/dom@8.20.1':
+ resolution: {integrity: sha512-/DiOQ5xBxgdYRC8LNk7U+RWat0S3qRLeIw3ZIkMQ9kkVlRmwD/Eg8k8CqIpD6GW7u20JIUOfMKbxtiLutpjQ4g==}
+ engines: {node: '>=12'}
+
'@testing-library/jest-dom@6.9.1':
resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==}
engines: {node: '>=14', npm: '>=6', yarn: '>=1'}
+ '@testing-library/preact@3.2.4':
+ resolution: {integrity: sha512-F+kJ243LP6VmEK1M809unzTE/ijg+bsMNuiRN0JEDIJBELKKDNhdgC/WrUSZ7klwJvtlO3wQZ9ix+jhObG07Fg==}
+ engines: {node: '>= 12'}
+ peerDependencies:
+ preact: '>=10 || ^10.0.0-alpha.0 || ^10.0.0-beta.0'
+
'@testing-library/react@16.3.2':
resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==}
engines: {node: '>=18'}
@@ -1990,6 +2229,9 @@ packages:
argparse@2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
+ aria-query@5.1.3:
+ resolution: {integrity: sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==}
+
aria-query@5.3.0:
resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==}
@@ -1997,6 +2239,10 @@ packages:
resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==}
engines: {node: '>= 0.4'}
+ array-buffer-byte-length@1.0.2:
+ resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==}
+ engines: {node: '>= 0.4'}
+
array-union@2.1.0:
resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==}
engines: {node: '>=8'}
@@ -2015,6 +2261,10 @@ packages:
asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
+ available-typed-arrays@1.0.7:
+ resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
+ engines: {node: '>= 0.4'}
+
axios@1.13.5:
resolution: {integrity: sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==}
@@ -2027,6 +2277,11 @@ packages:
peerDependencies:
'@babel/core': ^7.20.12
+ babel-plugin-transform-hook-names@1.0.2:
+ resolution: {integrity: sha512-5gafyjyyBTTdX/tQQ0hRgu4AhNHG/hqWi0ZZmg2xvs2FgRkJXzDNKBZCyoYqgFkovfDrgM8OoKg8karoUvWeCw==}
+ peerDependencies:
+ '@babel/core': ^7.12.10
+
babel-preset-solid@1.9.10:
resolution: {integrity: sha512-HCelrgua/Y+kqO8RyL04JBWS/cVdrtUv/h45GntgQY+cJl4eBcKkCDV3TdMjtKx1nXwRaR9QXslM/Npm1dxdZQ==}
peerDependencies:
@@ -2101,6 +2356,14 @@ packages:
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
engines: {node: '>= 0.4'}
+ call-bind@1.0.8:
+ resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==}
+ engines: {node: '>= 0.4'}
+
+ call-bound@1.0.4:
+ resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==}
+ engines: {node: '>= 0.4'}
+
callsites@3.1.0:
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
engines: {node: '>=6'}
@@ -2203,16 +2466,28 @@ packages:
supports-color:
optional: true
+ deep-equal@2.2.3:
+ resolution: {integrity: sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==}
+ engines: {node: '>= 0.4'}
+
deep-is@0.1.4:
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
defaults@1.0.4:
resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==}
+ define-data-property@1.1.4:
+ resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==}
+ engines: {node: '>= 0.4'}
+
define-lazy-prop@2.0.0:
resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==}
engines: {node: '>=8'}
+ define-properties@1.2.1:
+ resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==}
+ engines: {node: '>= 0.4'}
+
defu@6.1.4:
resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==}
@@ -2332,6 +2607,9 @@ packages:
resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
engines: {node: '>= 0.4'}
+ es-get-iterator@1.1.3:
+ resolution: {integrity: sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==}
+
es-module-lexer@1.7.0:
resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==}
@@ -2524,6 +2802,9 @@ packages:
resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
engines: {node: '>=4.0'}
+ estree-walker@2.0.2:
+ resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
+
estree-walker@3.0.3:
resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
@@ -2609,6 +2890,10 @@ packages:
debug:
optional: true
+ for-each@0.3.5:
+ resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
+ engines: {node: '>= 0.4'}
+
form-data@4.0.5:
resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
engines: {node: '>= 6'}
@@ -2640,6 +2925,9 @@ packages:
function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
+ functions-have-names@1.2.3:
+ resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==}
+
gensync@1.0.0-beta.2:
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
engines: {node: '>=6.9.0'}
@@ -2702,10 +2990,17 @@ packages:
resolution: {integrity: sha512-hR/uLYQdngTyEfxnOoa+e6KTcfBFyc1hgFj/Cc144A5JJUuHFYqIEBDcD4FeGqUeKLRZqJ9eN9u7/GDjYEgS1g==}
engines: {node: '>=20.0.0'}
+ has-bigints@1.1.0:
+ resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==}
+ engines: {node: '>= 0.4'}
+
has-flag@4.0.0:
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
engines: {node: '>=8'}
+ has-property-descriptors@1.0.2:
+ resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==}
+
has-symbols@1.1.0:
resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
engines: {node: '>= 0.4'}
@@ -2718,6 +3013,10 @@ packages:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
+ he@1.2.0:
+ resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
+ hasBin: true
+
hermes-estree@0.25.1:
resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==}
@@ -2778,6 +3077,34 @@ packages:
inherits@2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
+ internal-slot@1.1.0:
+ resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==}
+ engines: {node: '>= 0.4'}
+
+ is-arguments@1.2.0:
+ resolution: {integrity: sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==}
+ engines: {node: '>= 0.4'}
+
+ is-array-buffer@3.0.5:
+ resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==}
+ engines: {node: '>= 0.4'}
+
+ is-bigint@1.1.0:
+ resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==}
+ engines: {node: '>= 0.4'}
+
+ is-boolean-object@1.2.2:
+ resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==}
+ engines: {node: '>= 0.4'}
+
+ is-callable@1.2.7:
+ resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==}
+ engines: {node: '>= 0.4'}
+
+ is-date-object@1.1.0:
+ resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==}
+ engines: {node: '>= 0.4'}
+
is-docker@2.2.1:
resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==}
engines: {node: '>=8'}
@@ -2805,6 +3132,14 @@ packages:
resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==}
engines: {node: '>=8'}
+ is-map@2.0.3:
+ resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==}
+ engines: {node: '>= 0.4'}
+
+ is-number-object@1.1.1:
+ resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==}
+ engines: {node: '>= 0.4'}
+
is-number@7.0.0:
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
engines: {node: '>=0.12.0'}
@@ -2812,14 +3147,42 @@ packages:
is-reference@3.0.3:
resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==}
+ is-regex@1.2.1:
+ resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==}
+ engines: {node: '>= 0.4'}
+
+ is-set@2.0.3:
+ resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==}
+ engines: {node: '>= 0.4'}
+
+ is-shared-array-buffer@1.0.4:
+ resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==}
+ engines: {node: '>= 0.4'}
+
+ is-string@1.1.1:
+ resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==}
+ engines: {node: '>= 0.4'}
+
is-subdir@1.2.0:
resolution: {integrity: sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==}
engines: {node: '>=4'}
+ is-symbol@1.1.1:
+ resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==}
+ engines: {node: '>= 0.4'}
+
is-unicode-supported@0.1.0:
resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==}
engines: {node: '>=10'}
+ is-weakmap@2.0.2:
+ resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==}
+ engines: {node: '>= 0.4'}
+
+ is-weakset@2.0.4:
+ resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==}
+ engines: {node: '>= 0.4'}
+
is-what@4.1.16:
resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==}
engines: {node: '>=12.13'}
@@ -2832,6 +3195,9 @@ packages:
resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==}
engines: {node: '>=8'}
+ isarray@2.0.5:
+ resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==}
+
isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
@@ -2895,6 +3261,9 @@ packages:
'@types/node': '>=18'
typescript: '>=5.0.4 <7'
+ kolorist@1.8.0:
+ resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==}
+
levn@0.4.1:
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
engines: {node: '>= 0.8.0'}
@@ -3050,6 +3419,9 @@ packages:
encoding:
optional: true
+ node-html-parser@6.1.13:
+ resolution: {integrity: sha512-qIsTMOY4C/dAa5Q5vsobRpOOvPfC4pB61UVW2uSwZNUp0QU/jCekTal1vMmbO0DgdHeLUJpv/ARmDqErVxA3Sg==}
+
node-machine-id@1.1.12:
resolution: {integrity: sha512-QNABxbrPa3qEIfrE6GOJ7BYIuignnJw7iQ2YPbc3Nla1HzRJjXzZOiikfF8m7eAMfichLt3M4VgLOetqgDmgGQ==}
@@ -3075,6 +3447,22 @@ packages:
'@swc/core':
optional: true
+ object-inspect@1.13.4:
+ resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
+ engines: {node: '>= 0.4'}
+
+ object-is@1.1.6:
+ resolution: {integrity: sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==}
+ engines: {node: '>= 0.4'}
+
+ object-keys@1.1.1:
+ resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==}
+ engines: {node: '>= 0.4'}
+
+ object.assign@4.1.7:
+ resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==}
+ engines: {node: '>= 0.4'}
+
obug@2.1.1:
resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==}
@@ -3180,10 +3568,17 @@ packages:
resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==}
engines: {node: '>=6'}
+ possible-typed-array-names@1.1.0:
+ resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
+ engines: {node: '>= 0.4'}
+
postcss@8.5.6:
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
engines: {node: ^10 || ^12 || >=14}
+ preact@10.28.4:
+ resolution: {integrity: sha512-uKFfOHWuSNpRFVTnljsCluEFq57OKT+0QdOiQo8XWnQ/pSvg7OpX5eNOejELXJMWy+BwM2nobz0FkvzmnpCNsQ==}
+
prelude-ls@1.2.1:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'}
@@ -3273,6 +3668,10 @@ packages:
resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==}
engines: {node: '>=8'}
+ regexp.prototype.flags@1.5.4:
+ resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==}
+ engines: {node: '>= 0.4'}
+
require-directory@2.1.1:
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
engines: {node: '>=0.10.0'}
@@ -3339,6 +3738,10 @@ packages:
safe-buffer@5.2.1:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
+ safe-regex-test@1.1.0:
+ resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==}
+ engines: {node: '>= 0.4'}
+
safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
@@ -3364,6 +3767,14 @@ packages:
resolution: {integrity: sha512-OE4cvmJ1uSPrKorFIH9/w/Qwuvi/IMcGbv5RKgcJ/zjA/IohDLU6SVaxFN9FwajbP7nsX0dQqMDes1whk3y+yw==}
engines: {node: '>=10'}
+ set-function-length@1.2.2:
+ resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
+ engines: {node: '>= 0.4'}
+
+ set-function-name@2.0.2:
+ resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==}
+ engines: {node: '>= 0.4'}
+
shebang-command@2.0.0:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
engines: {node: '>=8'}
@@ -3416,6 +3827,22 @@ packages:
resolution: {integrity: sha512-DHg6+Pj7ORhYyC+CaSAr8DeRxqf9GXB90yqLmUILPtY7WhZuJatMir3id2MNjuF5I/1313SbrTTItIDu//G4jg==}
hasBin: true
+ side-channel-list@1.0.0:
+ resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==}
+ engines: {node: '>= 0.4'}
+
+ side-channel-map@1.0.1:
+ resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==}
+ engines: {node: '>= 0.4'}
+
+ side-channel-weakmap@1.0.2:
+ resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==}
+ engines: {node: '>= 0.4'}
+
+ side-channel@1.1.0:
+ resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==}
+ engines: {node: '>= 0.4'}
+
siginfo@2.0.0:
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
@@ -3426,6 +3853,9 @@ packages:
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
engines: {node: '>=14'}
+ simple-code-frame@1.3.0:
+ resolution: {integrity: sha512-MB4pQmETUBlNs62BBeRjIFGeuy/x6gGKh7+eRUemn1rCFhqo7K+4slPqsyizCbcbYLnaYqaoZ2FWsZ/jN06D8w==}
+
size-limit@12.0.0:
resolution: {integrity: sha512-JBG8dioIs0m2kHOhs9jD6E/tZKD08vmbf2bfqj/rJyNWqJxk/ZcakixjhYtsqdbi+AKVbfPkt3g2RRZiKaizYA==}
engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0}
@@ -3456,6 +3886,10 @@ packages:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
+ source-map@0.7.6:
+ resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==}
+ engines: {node: '>= 12'}
+
spawndamnit@3.0.1:
resolution: {integrity: sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==}
@@ -3466,12 +3900,20 @@ packages:
resolution: {integrity: sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==}
engines: {node: '>=12.0.0'}
+ stack-trace@1.0.0-pre2:
+ resolution: {integrity: sha512-2ztBJRek8IVofG9DBJqdy2N5kulaacX30Nz7xmkYF6ale9WBVmIy6mFBchvGX7Vx/MyjBhx+Rcxqrj+dbOnQ6A==}
+ engines: {node: '>=16'}
+
stackback@0.0.2:
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
std-env@3.10.0:
resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==}
+ stop-iteration-iterator@1.1.0:
+ resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==}
+ engines: {node: '>= 0.4'}
+
string-ts@2.3.1:
resolution: {integrity: sha512-xSJq+BS52SaFFAVxuStmx6n5aYZU571uYUnUrPXkPFCfdHyZMMlbP2v2Wx5sNBnAVzq/2+0+mcBLBa3Xa5ubYw==}
@@ -3689,6 +4131,11 @@ packages:
'@testing-library/jest-dom':
optional: true
+ vite-prerender-plugin@0.5.12:
+ resolution: {integrity: sha512-EiwhbMn+flg14EysbLTmZSzq8NGTxhytgK3bf4aGRF1evWLGwZiHiUJ1KZDvbxgKbMf2pG6fJWGEa3UZXOnR1g==}
+ peerDependencies:
+ vite: 5.x || 6.x || 7.x
+
vite@7.3.1:
resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -3803,6 +4250,18 @@ packages:
whatwg-url@5.0.0:
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
+ which-boxed-primitive@1.1.1:
+ resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==}
+ engines: {node: '>= 0.4'}
+
+ which-collection@1.0.2:
+ resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==}
+ engines: {node: '>= 0.4'}
+
+ which-typed-array@1.1.20:
+ resolution: {integrity: sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==}
+ engines: {node: '>= 0.4'}
+
which@2.0.2:
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
engines: {node: '>= 8'}
@@ -4042,6 +4501,13 @@ snapshots:
'@babel/core': 7.29.0
'@babel/helper-plugin-utils': 7.28.6
+ '@babel/plugin-transform-react-jsx-development@7.27.1(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/plugin-transform-react-jsx': 7.28.6(@babel/core@7.29.0)
+ transitivePeerDependencies:
+ - supports-color
+
'@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.29.0)':
dependencies:
'@babel/core': 7.29.0
@@ -4052,6 +4518,17 @@ snapshots:
'@babel/core': 7.29.0
'@babel/helper-plugin-utils': 7.28.6
+ '@babel/plugin-transform-react-jsx@7.28.6(@babel/core@7.29.0)':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/helper-annotate-as-pure': 7.27.3
+ '@babel/helper-module-imports': 7.28.6
+ '@babel/helper-plugin-utils': 7.28.6
+ '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0)
+ '@babel/types': 7.29.0
+ transitivePeerDependencies:
+ - supports-color
+
'@babel/runtime@7.28.6': {}
'@babel/template@7.28.6':
@@ -4651,6 +5128,43 @@ snapshots:
'@oxc-resolver/binding-win32-x64-msvc@11.18.0':
optional: true
+ '@preact/preset-vite@2.10.3(@babel/core@7.29.0)(preact@10.28.4)(rollup@4.58.0)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(yaml@2.8.2))':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@babel/plugin-transform-react-jsx': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-react-jsx-development': 7.27.1(@babel/core@7.29.0)
+ '@prefresh/vite': 2.4.12(preact@10.28.4)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(yaml@2.8.2))
+ '@rollup/pluginutils': 5.3.0(rollup@4.58.0)
+ babel-plugin-transform-hook-names: 1.0.2(@babel/core@7.29.0)
+ debug: 4.4.3
+ picocolors: 1.1.1
+ vite: 7.3.1(@types/node@25.3.0)(jiti@2.6.1)(yaml@2.8.2)
+ vite-prerender-plugin: 0.5.12(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(yaml@2.8.2))
+ transitivePeerDependencies:
+ - preact
+ - rollup
+ - supports-color
+
+ '@prefresh/babel-plugin@0.5.3': {}
+
+ '@prefresh/core@1.5.9(preact@10.28.4)':
+ dependencies:
+ preact: 10.28.4
+
+ '@prefresh/utils@1.2.1': {}
+
+ '@prefresh/vite@2.4.12(preact@10.28.4)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(yaml@2.8.2))':
+ dependencies:
+ '@babel/core': 7.29.0
+ '@prefresh/babel-plugin': 0.5.3
+ '@prefresh/core': 1.5.9(preact@10.28.4)
+ '@prefresh/utils': 1.2.1
+ '@rollup/pluginutils': 4.2.1
+ preact: 10.28.4
+ vite: 7.3.1(@types/node@25.3.0)(jiti@2.6.1)(yaml@2.8.2)
+ transitivePeerDependencies:
+ - supports-color
+
'@publint/pack@0.1.4': {}
'@quansync/fs@1.0.0':
@@ -4700,6 +5214,19 @@ snapshots:
'@rolldown/pluginutils@1.0.0-rc.3': {}
+ '@rollup/pluginutils@4.2.1':
+ dependencies:
+ estree-walker: 2.0.2
+ picomatch: 2.3.1
+
+ '@rollup/pluginutils@5.3.0(rollup@4.58.0)':
+ dependencies:
+ '@types/estree': 1.0.8
+ estree-walker: 2.0.2
+ picomatch: 4.0.3
+ optionalDependencies:
+ rollup: 4.58.0
+
'@rollup/rollup-android-arm-eabi@4.58.0':
optional: true
@@ -4879,6 +5406,13 @@ snapshots:
dependencies:
'@tanstack/devtools-event-client': 0.4.0
+ '@tanstack/devtools-event-bus@0.4.0':
+ dependencies:
+ ws: 8.19.0
+ transitivePeerDependencies:
+ - bufferutil
+ - utf-8-validate
+
'@tanstack/devtools-event-bus@0.4.1':
dependencies:
ws: 8.19.0
@@ -4896,16 +5430,33 @@ snapshots:
transitivePeerDependencies:
- csstype
- '@tanstack/devtools-utils@0.3.0(@types/react@19.2.14)(csstype@3.2.3)(react@19.2.4)(solid-js@1.9.11)':
+ '@tanstack/devtools-utils@0.3.0(@types/react@19.2.14)(csstype@3.2.3)(preact@10.28.4)(react@19.2.4)(solid-js@1.9.11)':
dependencies:
'@tanstack/devtools-ui': 0.4.4(csstype@3.2.3)(solid-js@1.9.11)
optionalDependencies:
'@types/react': 19.2.14
+ preact: 10.28.4
react: 19.2.4
solid-js: 1.9.11
transitivePeerDependencies:
- csstype
+ '@tanstack/devtools@0.10.2(csstype@3.2.3)(solid-js@1.9.11)':
+ dependencies:
+ '@solid-primitives/event-listener': 2.4.3(solid-js@1.9.11)
+ '@solid-primitives/keyboard': 1.3.3(solid-js@1.9.11)
+ '@solid-primitives/resize-observer': 2.1.3(solid-js@1.9.11)
+ '@tanstack/devtools-client': 0.0.5
+ '@tanstack/devtools-event-bus': 0.4.0
+ '@tanstack/devtools-ui': 0.4.4(csstype@3.2.3)(solid-js@1.9.11)
+ clsx: 2.1.1
+ goober: 2.1.18(csstype@3.2.3)
+ solid-js: 1.9.11
+ transitivePeerDependencies:
+ - bufferutil
+ - csstype
+ - utf-8-validate
+
'@tanstack/devtools@0.10.7(csstype@3.2.3)(solid-js@1.9.11)':
dependencies:
'@solid-primitives/event-listener': 2.4.3(solid-js@1.9.11)
@@ -4938,6 +5489,21 @@ snapshots:
- supports-color
- typescript
+ '@tanstack/preact-devtools@0.9.6(csstype@3.2.3)(preact@10.28.4)(solid-js@1.9.11)':
+ dependencies:
+ '@tanstack/devtools': 0.10.2(csstype@3.2.3)(solid-js@1.9.11)
+ preact: 10.28.4
+ transitivePeerDependencies:
+ - bufferutil
+ - csstype
+ - solid-js
+ - utf-8-validate
+
+ '@tanstack/preact-store@0.11.1(preact@10.28.4)':
+ dependencies:
+ '@tanstack/store': 0.9.1
+ preact: 10.28.4
+
'@tanstack/react-devtools@0.9.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(csstype@3.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(solid-js@1.9.11)':
dependencies:
'@tanstack/devtools': 0.10.7(csstype@3.2.3)(solid-js@1.9.11)
@@ -4993,6 +5559,17 @@ snapshots:
picocolors: 1.1.1
pretty-format: 27.5.1
+ '@testing-library/dom@8.20.1':
+ dependencies:
+ '@babel/code-frame': 7.29.0
+ '@babel/runtime': 7.28.6
+ '@types/aria-query': 5.0.4
+ aria-query: 5.1.3
+ chalk: 4.1.2
+ dom-accessibility-api: 0.5.16
+ lz-string: 1.5.0
+ pretty-format: 27.5.1
+
'@testing-library/jest-dom@6.9.1':
dependencies:
'@adobe/css-tools': 4.4.4
@@ -5002,6 +5579,11 @@ snapshots:
picocolors: 1.1.1
redent: 3.0.0
+ '@testing-library/preact@3.2.4(preact@10.28.4)':
+ dependencies:
+ '@testing-library/dom': 8.20.1
+ preact: 10.28.4
+
'@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
'@babel/runtime': 7.28.6
@@ -5330,12 +5912,21 @@ snapshots:
argparse@2.0.1: {}
+ aria-query@5.1.3:
+ dependencies:
+ deep-equal: 2.2.3
+
aria-query@5.3.0:
dependencies:
dequal: 2.0.3
aria-query@5.3.2: {}
+ array-buffer-byte-length@1.0.2:
+ dependencies:
+ call-bound: 1.0.4
+ is-array-buffer: 3.0.5
+
array-union@2.1.0: {}
assertion-error@2.0.1: {}
@@ -5350,6 +5941,10 @@ snapshots:
asynckit@0.4.0: {}
+ available-typed-arrays@1.0.7:
+ dependencies:
+ possible-typed-array-names: 1.1.0
+
axios@1.13.5:
dependencies:
follow-redirects: 1.15.11
@@ -5369,6 +5964,10 @@ snapshots:
html-entities: 2.3.3
parse5: 7.3.0
+ babel-plugin-transform-hook-names@1.0.2(@babel/core@7.29.0):
+ dependencies:
+ '@babel/core': 7.29.0
+
babel-preset-solid@1.9.10(@babel/core@7.29.0)(solid-js@1.9.11):
dependencies:
'@babel/core': 7.29.0
@@ -5439,6 +6038,18 @@ snapshots:
es-errors: 1.3.0
function-bind: 1.1.2
+ call-bind@1.0.8:
+ dependencies:
+ call-bind-apply-helpers: 1.0.2
+ es-define-property: 1.0.1
+ get-intrinsic: 1.3.0
+ set-function-length: 1.2.2
+
+ call-bound@1.0.4:
+ dependencies:
+ call-bind-apply-helpers: 1.0.2
+ get-intrinsic: 1.3.0
+
callsites@3.1.0: {}
caniuse-lite@1.0.30001770: {}
@@ -5537,14 +6148,47 @@ snapshots:
dependencies:
ms: 2.1.3
+ deep-equal@2.2.3:
+ dependencies:
+ array-buffer-byte-length: 1.0.2
+ call-bind: 1.0.8
+ es-get-iterator: 1.1.3
+ get-intrinsic: 1.3.0
+ is-arguments: 1.2.0
+ is-array-buffer: 3.0.5
+ is-date-object: 1.1.0
+ is-regex: 1.2.1
+ is-shared-array-buffer: 1.0.4
+ isarray: 2.0.5
+ object-is: 1.1.6
+ object-keys: 1.1.1
+ object.assign: 4.1.7
+ regexp.prototype.flags: 1.5.4
+ side-channel: 1.1.0
+ which-boxed-primitive: 1.1.1
+ which-collection: 1.0.2
+ which-typed-array: 1.1.20
+
deep-is@0.1.4: {}
defaults@1.0.4:
dependencies:
clone: 1.0.4
+ define-data-property@1.1.4:
+ dependencies:
+ es-define-property: 1.0.1
+ es-errors: 1.3.0
+ gopd: 1.2.0
+
define-lazy-prop@2.0.0: {}
+ define-properties@1.2.1:
+ dependencies:
+ define-data-property: 1.1.4
+ has-property-descriptors: 1.0.2
+ object-keys: 1.1.1
+
defu@6.1.4: {}
delayed-stream@1.0.0: {}
@@ -5642,6 +6286,18 @@ snapshots:
es-errors@1.3.0: {}
+ es-get-iterator@1.1.3:
+ dependencies:
+ call-bind: 1.0.8
+ get-intrinsic: 1.3.0
+ has-symbols: 1.1.0
+ is-arguments: 1.2.0
+ is-map: 2.0.3
+ is-set: 2.0.3
+ is-string: 1.1.1
+ isarray: 2.0.5
+ stop-iteration-iterator: 1.1.0
+
es-module-lexer@1.7.0: {}
es-object-atoms@1.1.1:
@@ -5962,6 +6618,8 @@ snapshots:
estraverse@5.3.0: {}
+ estree-walker@2.0.2: {}
+
estree-walker@3.0.3:
dependencies:
'@types/estree': 1.0.8
@@ -6035,6 +6693,10 @@ snapshots:
follow-redirects@1.15.11: {}
+ for-each@0.3.5:
+ dependencies:
+ is-callable: 1.2.7
+
form-data@4.0.5:
dependencies:
asynckit: 0.4.0
@@ -6070,6 +6732,8 @@ snapshots:
function-bind@1.1.2: {}
+ functions-have-names@1.2.3: {}
+
gensync@1.0.0-beta.2: {}
get-caller-file@2.0.5: {}
@@ -6141,8 +6805,14 @@ snapshots:
- bufferutil
- utf-8-validate
+ has-bigints@1.1.0: {}
+
has-flag@4.0.0: {}
+ has-property-descriptors@1.0.2:
+ dependencies:
+ es-define-property: 1.0.1
+
has-symbols@1.1.0: {}
has-tostringtag@1.0.2:
@@ -6153,6 +6823,8 @@ snapshots:
dependencies:
function-bind: 1.1.2
+ he@1.2.0: {}
+
hermes-estree@0.25.1: {}
hermes-parser@0.25.1:
@@ -6203,6 +6875,39 @@ snapshots:
inherits@2.0.4: {}
+ internal-slot@1.1.0:
+ dependencies:
+ es-errors: 1.3.0
+ hasown: 2.0.2
+ side-channel: 1.1.0
+
+ is-arguments@1.2.0:
+ dependencies:
+ call-bound: 1.0.4
+ has-tostringtag: 1.0.2
+
+ is-array-buffer@3.0.5:
+ dependencies:
+ call-bind: 1.0.8
+ call-bound: 1.0.4
+ get-intrinsic: 1.3.0
+
+ is-bigint@1.1.0:
+ dependencies:
+ has-bigints: 1.1.0
+
+ is-boolean-object@1.2.2:
+ dependencies:
+ call-bound: 1.0.4
+ has-tostringtag: 1.0.2
+
+ is-callable@1.2.7: {}
+
+ is-date-object@1.1.0:
+ dependencies:
+ call-bound: 1.0.4
+ has-tostringtag: 1.0.2
+
is-docker@2.2.1: {}
is-extglob@2.1.1: {}
@@ -6225,18 +6930,56 @@ snapshots:
is-interactive@1.0.0: {}
+ is-map@2.0.3: {}
+
+ is-number-object@1.1.1:
+ dependencies:
+ call-bound: 1.0.4
+ has-tostringtag: 1.0.2
+
is-number@7.0.0: {}
is-reference@3.0.3:
dependencies:
'@types/estree': 1.0.8
+ is-regex@1.2.1:
+ dependencies:
+ call-bound: 1.0.4
+ gopd: 1.2.0
+ has-tostringtag: 1.0.2
+ hasown: 2.0.2
+
+ is-set@2.0.3: {}
+
+ is-shared-array-buffer@1.0.4:
+ dependencies:
+ call-bound: 1.0.4
+
+ is-string@1.1.1:
+ dependencies:
+ call-bound: 1.0.4
+ has-tostringtag: 1.0.2
+
is-subdir@1.2.0:
dependencies:
better-path-resolve: 1.0.0
+ is-symbol@1.1.1:
+ dependencies:
+ call-bound: 1.0.4
+ has-symbols: 1.1.0
+ safe-regex-test: 1.1.0
+
is-unicode-supported@0.1.0: {}
+ is-weakmap@2.0.2: {}
+
+ is-weakset@2.0.4:
+ dependencies:
+ call-bound: 1.0.4
+ get-intrinsic: 1.3.0
+
is-what@4.1.16: {}
is-windows@1.0.2: {}
@@ -6245,6 +6988,8 @@ snapshots:
dependencies:
is-docker: 2.2.1
+ isarray@2.0.5: {}
+
isexe@2.0.0: {}
jake@10.9.4:
@@ -6310,6 +7055,8 @@ snapshots:
typescript: 5.9.3
zod: 4.3.6
+ kolorist@1.8.0: {}
+
levn@0.4.1:
dependencies:
prelude-ls: 1.2.1
@@ -6437,6 +7184,11 @@ snapshots:
dependencies:
whatwg-url: 5.0.0
+ node-html-parser@6.1.13:
+ dependencies:
+ css-select: 5.2.2
+ he: 1.2.0
+
node-machine-id@1.1.12: {}
node-releases@2.0.27: {}
@@ -6501,6 +7253,24 @@ snapshots:
transitivePeerDependencies:
- debug
+ object-inspect@1.13.4: {}
+
+ object-is@1.1.6:
+ dependencies:
+ call-bind: 1.0.8
+ define-properties: 1.2.1
+
+ object-keys@1.1.1: {}
+
+ object.assign@4.1.7:
+ dependencies:
+ call-bind: 1.0.8
+ call-bound: 1.0.4
+ define-properties: 1.2.1
+ es-object-atoms: 1.1.1
+ has-symbols: 1.1.0
+ object-keys: 1.1.1
+
obug@2.1.1: {}
once@1.4.0:
@@ -6625,12 +7395,16 @@ snapshots:
pify@4.0.1: {}
+ possible-typed-array-names@1.1.0: {}
+
postcss@8.5.6:
dependencies:
nanoid: 3.3.11
picocolors: 1.1.1
source-map-js: 1.2.1
+ preact@10.28.4: {}
+
prelude-ls@1.2.1: {}
premove@4.0.0: {}
@@ -6706,6 +7480,15 @@ snapshots:
indent-string: 4.0.0
strip-indent: 3.0.0
+ regexp.prototype.flags@1.5.4:
+ dependencies:
+ call-bind: 1.0.8
+ define-properties: 1.2.1
+ es-errors: 1.3.0
+ get-proto: 1.0.1
+ gopd: 1.2.0
+ set-function-name: 2.0.2
+
require-directory@2.1.1: {}
resolve-from@4.0.0: {}
@@ -6800,6 +7583,12 @@ snapshots:
safe-buffer@5.2.1: {}
+ safe-regex-test@1.1.0:
+ dependencies:
+ call-bound: 1.0.4
+ es-errors: 1.3.0
+ is-regex: 1.2.1
+
safer-buffer@2.1.2: {}
scheduler@0.27.0: {}
@@ -6814,6 +7603,22 @@ snapshots:
seroval@1.5.0: {}
+ set-function-length@1.2.2:
+ dependencies:
+ define-data-property: 1.1.4
+ es-errors: 1.3.0
+ function-bind: 1.1.2
+ get-intrinsic: 1.3.0
+ gopd: 1.2.0
+ has-property-descriptors: 1.0.2
+
+ set-function-name@2.0.2:
+ dependencies:
+ define-data-property: 1.1.4
+ es-errors: 1.3.0
+ functions-have-names: 1.2.3
+ has-property-descriptors: 1.0.2
+
shebang-command@2.0.0:
dependencies:
shebang-regex: 3.0.0
@@ -6855,12 +7660,44 @@ snapshots:
sherif-windows-arm64: 1.10.0
sherif-windows-x64: 1.10.0
+ side-channel-list@1.0.0:
+ dependencies:
+ es-errors: 1.3.0
+ object-inspect: 1.13.4
+
+ side-channel-map@1.0.1:
+ dependencies:
+ call-bound: 1.0.4
+ es-errors: 1.3.0
+ get-intrinsic: 1.3.0
+ object-inspect: 1.13.4
+
+ side-channel-weakmap@1.0.2:
+ dependencies:
+ call-bound: 1.0.4
+ es-errors: 1.3.0
+ get-intrinsic: 1.3.0
+ object-inspect: 1.13.4
+ side-channel-map: 1.0.1
+
+ side-channel@1.1.0:
+ dependencies:
+ es-errors: 1.3.0
+ object-inspect: 1.13.4
+ side-channel-list: 1.0.0
+ side-channel-map: 1.0.1
+ side-channel-weakmap: 1.0.2
+
siginfo@2.0.0: {}
signal-exit@3.0.7: {}
signal-exit@4.1.0: {}
+ simple-code-frame@1.3.0:
+ dependencies:
+ kolorist: 1.8.0
+
size-limit@12.0.0(jiti@2.6.1):
dependencies:
bytes-iec: 3.1.1
@@ -6892,6 +7729,8 @@ snapshots:
source-map-js@1.2.1: {}
+ source-map@0.7.6: {}
+
spawndamnit@3.0.1:
dependencies:
cross-spawn: 7.0.6
@@ -6901,10 +7740,17 @@ snapshots:
stable-hash-x@0.2.0: {}
+ stack-trace@1.0.0-pre2: {}
+
stackback@0.0.2: {}
std-env@3.10.0: {}
+ stop-iteration-iterator@1.1.0:
+ dependencies:
+ es-errors: 1.3.0
+ internal-slot: 1.1.0
+
string-ts@2.3.1: {}
string-width@4.2.3:
@@ -7141,6 +7987,16 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ vite-prerender-plugin@0.5.12(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(yaml@2.8.2)):
+ dependencies:
+ kolorist: 1.8.0
+ magic-string: 0.30.21
+ node-html-parser: 6.1.13
+ simple-code-frame: 1.3.0
+ source-map: 0.7.6
+ stack-trace: 1.0.0-pre2
+ vite: 7.3.1(@types/node@25.3.0)(jiti@2.6.1)(yaml@2.8.2)
+
vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(yaml@2.8.2):
dependencies:
esbuild: 0.27.3
@@ -7230,6 +8086,31 @@ snapshots:
tr46: 0.0.3
webidl-conversions: 3.0.1
+ which-boxed-primitive@1.1.1:
+ dependencies:
+ is-bigint: 1.1.0
+ is-boolean-object: 1.2.2
+ is-number-object: 1.1.1
+ is-string: 1.1.1
+ is-symbol: 1.1.1
+
+ which-collection@1.0.2:
+ dependencies:
+ is-map: 2.0.3
+ is-set: 2.0.3
+ is-weakmap: 2.0.2
+ is-weakset: 2.0.4
+
+ which-typed-array@1.1.20:
+ dependencies:
+ available-typed-arrays: 1.0.7
+ call-bind: 1.0.8
+ call-bound: 1.0.4
+ for-each: 0.3.5
+ get-proto: 1.0.1
+ gopd: 1.2.0
+ has-tostringtag: 1.0.2
+
which@2.0.2:
dependencies:
isexe: 2.0.0
diff --git a/scripts/generate-docs.ts b/scripts/generate-docs.ts
index 8617e2e..fc379be 100644
--- a/scripts/generate-docs.ts
+++ b/scripts/generate-docs.ts
@@ -12,6 +12,18 @@ await generateReferenceDocs({
tsconfig: resolve(__dirname, '../packages/hotkeys/tsconfig.docs.json'),
outputDir: resolve(__dirname, '../docs/reference'),
},
+ {
+ name: 'preact-hotkeys',
+ entryPoints: [
+ resolve(__dirname, '../packages/preact-hotkeys/src/index.ts'),
+ ],
+ tsconfig: resolve(
+ __dirname,
+ '../packages/preact-hotkeys/tsconfig.docs.json',
+ ),
+ outputDir: resolve(__dirname, '../docs/framework/preact/reference'),
+ exclude: ['packages/hotkeys/**/*'],
+ },
{
name: 'react-hotkeys',
entryPoints: [
diff --git a/vitest.workspace.ts b/vitest.workspace.ts
index a43fccc..f6ddff8 100644
--- a/vitest.workspace.ts
+++ b/vitest.workspace.ts
@@ -3,12 +3,14 @@ import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
projects: [
- './packages/hotkeys/vitest.config.ts',
'./packages/hotkeys-devtools/vitest.config.ts',
- './packages/react-hotkeys/vitest.config.ts',
+ './packages/hotkeys/vitest.config.ts',
+ './packages/preact-hotkeys-devtools/vitest.config.ts',
+ './packages/preact-hotkeys/vitest.config.ts',
'./packages/react-hotkeys-devtools/vitest.config.ts',
- './packages/solid-hotkeys/vitest.config.ts',
+ './packages/react-hotkeys/vitest.config.ts',
'./packages/solid-hotkeys-devtools/vitest.config.ts',
+ './packages/solid-hotkeys/vitest.config.ts',
],
},
})