From 5818e246caad47552ae261c1a53945f7e2fa8722 Mon Sep 17 00:00:00 2001 From: jdecroock Date: Fri, 20 Feb 2026 17:34:08 +0100 Subject: [PATCH 1/7] Implement preact packages --- README.md | 1 + package.json | 4 +- packages/preact-hotkeys-devtools/README.md | 46 ++ .../preact-hotkeys-devtools/eslint.config.js | 8 + packages/preact-hotkeys-devtools/package.json | 60 ++ .../src/PreactHotkeysDevtools.tsx | 10 + packages/preact-hotkeys-devtools/src/index.ts | 14 + .../preact-hotkeys-devtools/src/plugin.tsx | 9 + .../preact-hotkeys-devtools/tsconfig.json | 9 + .../preact-hotkeys-devtools/tsdown.config.ts | 18 + .../preact-hotkeys-devtools/vitest.config.ts | 14 + packages/preact-hotkeys/README.md | 121 +++ packages/preact-hotkeys/eslint.config.js | 8 + packages/preact-hotkeys/package.json | 63 ++ .../preact-hotkeys/src/HotkeysProvider.tsx | 52 ++ packages/preact-hotkeys/src/index.ts | 13 + .../preact-hotkeys/src/useHeldKeyCodes.ts | 33 + packages/preact-hotkeys/src/useHeldKeys.ts | 29 + packages/preact-hotkeys/src/useHotkey.ts | 192 +++++ .../preact-hotkeys/src/useHotkeyRecorder.ts | 101 +++ .../preact-hotkeys/src/useHotkeySequence.ts | 92 +++ packages/preact-hotkeys/src/useKeyHold.ts | 52 ++ packages/preact-hotkeys/src/useStoreState.ts | 47 ++ .../preact-hotkeys/tests/useHotkey.test.tsx | 220 ++++++ packages/preact-hotkeys/tsconfig.json | 9 + packages/preact-hotkeys/tsdown.config.ts | 16 + packages/preact-hotkeys/vitest.config.ts | 15 + pnpm-lock.yaml | 730 +++++++++++++++++- vitest.workspace.js | 1 + 29 files changed, 1977 insertions(+), 10 deletions(-) create mode 100644 packages/preact-hotkeys-devtools/README.md create mode 100644 packages/preact-hotkeys-devtools/eslint.config.js create mode 100644 packages/preact-hotkeys-devtools/package.json create mode 100644 packages/preact-hotkeys-devtools/src/PreactHotkeysDevtools.tsx create mode 100644 packages/preact-hotkeys-devtools/src/index.ts create mode 100644 packages/preact-hotkeys-devtools/src/plugin.tsx create mode 100644 packages/preact-hotkeys-devtools/tsconfig.json create mode 100644 packages/preact-hotkeys-devtools/tsdown.config.ts create mode 100644 packages/preact-hotkeys-devtools/vitest.config.ts create mode 100644 packages/preact-hotkeys/README.md create mode 100644 packages/preact-hotkeys/eslint.config.js create mode 100644 packages/preact-hotkeys/package.json create mode 100644 packages/preact-hotkeys/src/HotkeysProvider.tsx create mode 100644 packages/preact-hotkeys/src/index.ts create mode 100644 packages/preact-hotkeys/src/useHeldKeyCodes.ts create mode 100644 packages/preact-hotkeys/src/useHeldKeys.ts create mode 100644 packages/preact-hotkeys/src/useHotkey.ts create mode 100644 packages/preact-hotkeys/src/useHotkeyRecorder.ts create mode 100644 packages/preact-hotkeys/src/useHotkeySequence.ts create mode 100644 packages/preact-hotkeys/src/useKeyHold.ts create mode 100644 packages/preact-hotkeys/src/useStoreState.ts create mode 100644 packages/preact-hotkeys/tests/useHotkey.test.tsx create mode 100644 packages/preact-hotkeys/tsconfig.json create mode 100644 packages/preact-hotkeys/tsdown.config.ts create mode 100644 packages/preact-hotkeys/vitest.config.ts diff --git a/README.md b/README.md index 2da2e21..e593ec4 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,7 @@ 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) +> - Preact Hotkeys > - Solid Hotkeys – needs a contributor! > - Angular Hotkeys – needs a contributor! > - Svelte Hotkeys – needs a contributor! diff --git a/package.json b/package.json index 671e9c2..730faac 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/preact-hotkeys-devtools/README.md b/packages/preact-hotkeys-devtools/README.md new file mode 100644 index 0000000..607e516 --- /dev/null +++ b/packages/preact-hotkeys-devtools/README.md @@ -0,0 +1,46 @@ +# @tanstack/preact-hotkeys-devtools + +> Preact devtools for [TanStack Hotkeys](https://tanstack.com/hotkeys) + +## Installation + +```bash +npm install @tanstack/preact-hotkeys-devtools @tanstack/preact-hotkeys +# or +bun add @tanstack/preact-hotkeys-devtools @tanstack/preact-hotkeys +# or +pnpm add @tanstack/preact-hotkeys-devtools @tanstack/preact-hotkeys +``` + +## Usage + +```tsx +import { + HotkeysDevtoolsPanel, + hotkeysDevtoolsPlugin, +} from '@tanstack/preact-hotkeys-devtools' +import { useHotkey } from '@tanstack/preact-hotkeys' + +function App() { + // Register the devtools plugin + useHotkey( + 'Mod+S', + (event) => { + event.preventDefault() + console.log('Save!') + }, + { plugins: [hotkeysDevtoolsPlugin] }, + ) + + return ( +
+ + {/* Your app */} +
+ ) +} +``` + +## License + +MIT 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..e92f6e9 --- /dev/null +++ b/packages/preact-hotkeys-devtools/package.json @@ -0,0 +1,60 @@ +{ + "name": "@tanstack/preact-hotkeys-devtools", + "version": "0.1.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..59b97df --- /dev/null +++ b/packages/preact-hotkeys/README.md @@ -0,0 +1,121 @@ +
+ TanStack Hotkeys +
+ +
+ +
+ + + + + + + + + +
+ +
+ + semantic-release + + + Release + + + Follow @TanStack + +
+ +
+ +### [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, Preact 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) +> - Solid Hotkeys – needs a contributor! +> - 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 + +
+ + + + + + +
+ + + + + CodeRabbit + + + + + + + + Cloudflare + + +
+ +
+Keys & you? +

+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..ec930c1 --- /dev/null +++ b/packages/preact-hotkeys/package.json @@ -0,0 +1,63 @@ +{ + "name": "@tanstack/preact-hotkeys", + "version": "0.1.3", + "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..b6ea0ab --- /dev/null +++ b/packages/preact-hotkeys/src/useHeldKeyCodes.ts @@ -0,0 +1,33 @@ +import { getKeyStateTracker } from '@tanstack/hotkeys' +import { useStoreState } from './useStoreState' + +/** + * 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 useStoreState(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..a27baf8 --- /dev/null +++ b/packages/preact-hotkeys/src/useHeldKeys.ts @@ -0,0 +1,29 @@ +import { getKeyStateTracker } from '@tanstack/hotkeys' +import { useStoreState } from './useStoreState' + +/** + * 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 useStoreState(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 + * } + * ``` + * + * @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..332f0fd --- /dev/null +++ b/packages/preact-hotkeys/src/useHotkeyRecorder.ts @@ -0,0 +1,101 @@ +import { useEffect, useRef } from 'preact/hooks' +import { HotkeyRecorder } from '@tanstack/hotkeys' +import { useDefaultHotkeysOptions } from './HotkeysProvider' +import { useStoreState } from './useStoreState' +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.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 = useStoreState( + recorderRef.current.store, + (state) => state.isRecording, + ) + const recordedHotkey = useStoreState( + 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..20de119 --- /dev/null +++ b/packages/preact-hotkeys/src/useHotkeySequence.ts @@ -0,0 +1,92 @@ +import { useEffect, useRef } from 'preact/hooks' +import { getSequenceManager } from '@tanstack/hotkeys' +import { useDefaultHotkeysOptions } from './HotkeysProvider' +import type { + HotkeyCallback, + HotkeySequence, + SequenceOptions, +} from '@tanstack/hotkeys' + +export interface UseHotkeySequenceOptions extends Omit< + SequenceOptions, + 'enabled' +> { + /** Whether the sequence is enabled. Defaults to true. */ + enabled?: boolean +} + +/** + * 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 { enabled = true, ...sequenceOptions } = mergedOptions + + // Extract options for stable dependencies + const { timeout, platform } = sequenceOptions + + // Use refs to keep callback stable + const callbackRef = useRef(callback) + callbackRef.current = callback + + // Serialize sequence for dependency comparison + const sequenceKey = sequence.join('|') + + useEffect(() => { + if (!enabled || sequence.length === 0) { + return + } + + const manager = getSequenceManager() + + // Build options object conditionally to avoid overwriting manager defaults with undefined + const registerOptions: SequenceOptions = { enabled: true } + if (timeout !== undefined) registerOptions.timeout = timeout + if (platform !== undefined) registerOptions.platform = platform + + const unregister = manager.register( + sequence, + (event, context) => callbackRef.current(event, context), + registerOptions, + ) + + return unregister + }, [enabled, sequence, sequenceKey, timeout, platform]) +} diff --git a/packages/preact-hotkeys/src/useKeyHold.ts b/packages/preact-hotkeys/src/useKeyHold.ts new file mode 100644 index 0000000..e720f38 --- /dev/null +++ b/packages/preact-hotkeys/src/useKeyHold.ts @@ -0,0 +1,52 @@ +import { getKeyStateTracker } from '@tanstack/hotkeys' +import { useStoreState } from './useStoreState' +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 useStoreState(tracker.store, (state) => + state.heldKeys.some((heldKey) => heldKey.toLowerCase() === normalizedKey), + ) +} diff --git a/packages/preact-hotkeys/src/useStoreState.ts b/packages/preact-hotkeys/src/useStoreState.ts new file mode 100644 index 0000000..ab35861 --- /dev/null +++ b/packages/preact-hotkeys/src/useStoreState.ts @@ -0,0 +1,47 @@ +import { useEffect, useRef, useState } from 'preact/hooks' + +type Unsubscribe = { unsubscribe: () => void } | (() => void) + +interface CompatibleStore { + state: TState + subscribe: (listener: (...args: Array) => void) => Unsubscribe +} + +/** + * Lightweight store subscription helper compatible with the store API + * exposed by @tanstack/hotkeys. + */ +export function useStoreState( + store: CompatibleStore, + selector: (state: TState) => TSelected = (state) => + state as unknown as TSelected, +): TSelected { + const selectorRef = useRef(selector) + selectorRef.current = selector + + const [selected, setSelected] = useState(() => + selectorRef.current(store.state), + ) + const selectedRef = useRef(selected) + selectedRef.current = selected + + useEffect(() => { + const updateSelected = () => { + const nextSelected = selectorRef.current(store.state) + if (!Object.is(selectedRef.current, nextSelected)) { + selectedRef.current = nextSelected + setSelected(nextSelected) + } + } + + updateSelected() + + const subscription = store.subscribe(updateSelected) + + return typeof subscription === 'function' + ? subscription + : () => subscription.unsubscribe() + }, [store]) + + return selected +} diff --git a/packages/preact-hotkeys/tests/useHotkey.test.tsx b/packages/preact-hotkeys/tests/useHotkey.test.tsx new file mode 100644 index 0000000..a23b2bb --- /dev/null +++ b/packages/preact-hotkeys/tests/useHotkey.test.tsx @@ -0,0 +1,220 @@ +// @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.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/pnpm-lock.yaml b/pnpm-lock.yaml index 0ced805..7b2ef47 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -255,7 +255,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/solid-devtools': specifier: 0.7.25 version: 0.7.25(csstype@3.2.3)(solid-js@1.9.11) @@ -280,7 +280,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/solid-devtools': specifier: 0.7.25 version: 0.7.25(csstype@3.2.3)(solid-js@1.9.11) @@ -311,7 +311,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/solid-devtools': specifier: 0.7.25 version: 0.7.25(csstype@3.2.3)(solid-js@1.9.11) @@ -336,7 +336,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/solid-devtools': specifier: 0.7.25 version: 0.7.25(csstype@3.2.3)(solid-js@1.9.11) @@ -361,7 +361,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/solid-devtools': specifier: 0.7.25 version: 0.7.25(csstype@3.2.3)(solid-js@1.9.11) @@ -395,7 +395,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 +413,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.2.3)(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.57.1)(vite@7.3.1(@types/node@25.2.3)(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.57.1)(vite@7.3.1(@types/node@25.2.3)(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 +486,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 +542,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 +680,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 +698,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'} @@ -1241,6 +1288,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'} @@ -1332,6 +1402,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.57.1': resolution: {integrity: sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==} cpu: [arm] @@ -1614,6 +1697,11 @@ packages: peerDependencies: eslint: ^8.0.0 || ^9.0.0 + '@tanstack/preact-store@0.11.1': + resolution: {integrity: sha512-dbaXYZX2YVtxVRcGJsSUCTH8wfAB+xFkKfnRGyEI1Z4s4js6HHbUK7GtMTTo6MCyNHYZqDI1w49Dj8Gj+IbwEA==} + peerDependencies: + preact: ^10.0.0 + '@tanstack/react-devtools@0.9.5': resolution: {integrity: sha512-/YsSSobbWfSZ0khLZ5n4cz/isa8Ac21PAVdgrX0qOEkPkS6J63JTEgFR0Ch2n2ka511dm2pIEuTvCsL7WVu1XQ==} engines: {node: '>=18'} @@ -1643,6 +1731,9 @@ packages: '@tanstack/store@0.8.0': resolution: {integrity: sha512-Om+BO0YfMZe//X2z0uLF2j+75nQga6TpTJgLJQBiq85aOyZNIhkCgleNcud2KQg4k4v9Y9l+Uhru3qWMPGTOzQ==} + '@tanstack/store@0.9.1': + resolution: {integrity: sha512-+qcNkOy0N1qSGsP7omVCW0SDrXtaDcycPqBDE726yryiA5eTDFpjBReaYjghVJwNf1pcPMyzIwTGlYjCSQR0Fg==} + '@tanstack/typedoc-config@0.3.3': resolution: {integrity: sha512-wVT2YfKDSpd+4f7fk6UaPIP3a2J7LSovlyVuFF1PH2yQb7gjqehod5zdFiwFyEXgvI9XGuFvvs1OehkKNYcr6A==} engines: {node: '>=18'} @@ -1651,10 +1742,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'} @@ -1979,6 +2080,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==} @@ -1986,6 +2090,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'} @@ -2004,6 +2112,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==} @@ -2016,6 +2128,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: @@ -2089,6 +2206,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'} @@ -2191,16 +2316,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==} @@ -2320,6 +2457,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==} @@ -2500,6 +2640,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==} @@ -2585,6 +2728,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'} @@ -2616,6 +2763,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'} @@ -2678,10 +2828,17 @@ packages: resolution: {integrity: sha512-+0vhESXXhFwkdjZnJ5DlmJIfUYGgIEEjzIjB+aKJbFuqlvvKyOi+XkI1fYbgYR9QCxG5T08koxsQ6HrQfa5gCQ==} 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'} @@ -2694,6 +2851,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==} @@ -2754,6 +2915,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'} @@ -2781,6 +2970,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'} @@ -2788,14 +2985,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'} @@ -2808,6 +3033,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==} @@ -2875,6 +3103,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'} @@ -3030,6 +3261,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==} @@ -3055,6 +3289,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==} @@ -3160,10 +3410,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'} @@ -3253,6 +3510,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'} @@ -3319,6 +3580,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==} @@ -3344,6 +3609,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'} @@ -3396,6 +3669,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==} @@ -3406,6 +3695,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} @@ -3436,6 +3728,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==} @@ -3446,12 +3742,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==} @@ -3669,6 +3973,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} @@ -3783,6 +4092,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'} @@ -4022,6 +4343,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 @@ -4032,6 +4360,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': @@ -4629,6 +4968,43 @@ snapshots: '@oxc-resolver/binding-win32-x64-msvc@11.17.1': optional: true + '@preact/preset-vite@2.10.3(@babel/core@7.29.0)(preact@10.28.4)(rollup@4.57.1)(vite@7.3.1(@types/node@25.2.3)(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.2.3)(jiti@2.6.1)(yaml@2.8.2)) + '@rollup/pluginutils': 5.3.0(rollup@4.57.1) + 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.2.3)(jiti@2.6.1)(yaml@2.8.2) + vite-prerender-plugin: 0.5.12(vite@7.3.1(@types/node@25.2.3)(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.2.3)(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.2.3)(jiti@2.6.1)(yaml@2.8.2) + transitivePeerDependencies: + - supports-color + '@publint/pack@0.1.4': {} '@quansync/fs@1.0.0': @@ -4678,6 +5054,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.57.1)': + dependencies: + '@types/estree': 1.0.8 + estree-walker: 2.0.2 + picomatch: 4.0.3 + optionalDependencies: + rollup: 4.57.1 + '@rollup/rollup-android-arm-eabi@4.57.1': optional: true @@ -4874,11 +5263,12 @@ 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: @@ -4916,6 +5306,11 @@ snapshots: - supports-color - typescript + '@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.5(@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.6(csstype@3.2.3)(solid-js@1.9.11) @@ -4952,6 +5347,8 @@ snapshots: '@tanstack/store@0.8.0': {} + '@tanstack/store@0.9.1': {} + '@tanstack/typedoc-config@0.3.3(typescript@5.9.3)': dependencies: typedoc: 0.28.14(typescript@5.9.3) @@ -4971,6 +5368,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 @@ -4980,6 +5388,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 @@ -5304,12 +5717,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: {} @@ -5324,6 +5746,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 @@ -5343,6 +5769,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 @@ -5415,6 +5845,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.30001769: {} @@ -5513,14 +5955,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: {} @@ -5618,6 +6093,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: @@ -5923,6 +6410,8 @@ snapshots: estraverse@5.3.0: {} + estree-walker@2.0.2: {} + estree-walker@3.0.3: dependencies: '@types/estree': 1.0.8 @@ -5996,6 +6485,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 @@ -6031,6 +6524,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: {} @@ -6102,8 +6597,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: @@ -6114,6 +6615,8 @@ snapshots: dependencies: function-bind: 1.1.2 + he@1.2.0: {} + hermes-estree@0.25.1: {} hermes-parser@0.25.1: @@ -6164,6 +6667,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: {} @@ -6186,18 +6722,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: {} @@ -6206,6 +6780,8 @@ snapshots: dependencies: is-docker: 2.2.1 + isarray@2.0.5: {} + isexe@2.0.0: {} jackspeak@4.2.3: @@ -6275,6 +6851,8 @@ snapshots: typescript: 5.9.3 zod: 4.3.6 + kolorist@1.8.0: {} + levn@0.4.1: dependencies: prelude-ls: 1.2.1 @@ -6402,6 +6980,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: {} @@ -6466,6 +7049,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: @@ -6590,12 +7191,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: {} @@ -6671,6 +7276,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: {} @@ -6765,6 +7379,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: {} @@ -6779,6 +7399,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 @@ -6820,12 +7456,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 @@ -6857,6 +7525,8 @@ snapshots: source-map-js@1.2.1: {} + source-map@0.7.6: {} + spawndamnit@3.0.1: dependencies: cross-spawn: 7.0.6 @@ -6866,10 +7536,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: @@ -7105,6 +7782,16 @@ snapshots: transitivePeerDependencies: - supports-color + vite-prerender-plugin@0.5.12(vite@7.3.1(@types/node@25.2.3)(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.2.3)(jiti@2.6.1)(yaml@2.8.2) + vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(yaml@2.8.2): dependencies: esbuild: 0.27.3 @@ -7194,6 +7881,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/vitest.workspace.js b/vitest.workspace.js index a43fccc..b7e138b 100644 --- a/vitest.workspace.js +++ b/vitest.workspace.js @@ -5,6 +5,7 @@ export default defineConfig({ projects: [ './packages/hotkeys/vitest.config.ts', './packages/hotkeys-devtools/vitest.config.ts', + './packages/preact-hotkeys-devtools/vitest.config.ts', './packages/react-hotkeys/vitest.config.ts', './packages/react-hotkeys-devtools/vitest.config.ts', './packages/solid-hotkeys/vitest.config.ts', From fb2336d40c0cf29ac954102aee4bbdaf635153c7 Mon Sep 17 00:00:00 2001 From: jdecroock Date: Fri, 20 Feb 2026 17:34:15 +0100 Subject: [PATCH 2/7] Add preact examples --- examples/preact/useHeldKeys/eslint.config.js | 11 + examples/preact/useHeldKeys/index.html | 14 + examples/preact/useHeldKeys/package.json | 23 + examples/preact/useHeldKeys/src/index.css | 131 ++++ examples/preact/useHeldKeys/src/index.tsx | 145 ++++ examples/preact/useHeldKeys/tsconfig.json | 21 + examples/preact/useHeldKeys/vite.config.ts | 7 + examples/preact/useHotkey/eslint.config.js | 11 + examples/preact/useHotkey/index.html | 14 + examples/preact/useHotkey/package.json | 23 + examples/preact/useHotkey/src/index.css | 212 +++++ examples/preact/useHotkey/src/index.tsx | 738 ++++++++++++++++++ examples/preact/useHotkey/tsconfig.json | 21 + examples/preact/useHotkey/vite.config.ts | 7 + .../preact/useHotkeyRecorder/eslint.config.js | 11 + examples/preact/useHotkeyRecorder/index.html | 14 + .../preact/useHotkeyRecorder/package.json | 23 + .../preact/useHotkeyRecorder/src/index.css | 256 ++++++ .../preact/useHotkeyRecorder/src/index.tsx | 369 +++++++++ .../preact/useHotkeyRecorder/tsconfig.json | 21 + .../preact/useHotkeyRecorder/vite.config.ts | 7 + .../preact/useHotkeySequence/eslint.config.js | 11 + examples/preact/useHotkeySequence/index.html | 14 + .../preact/useHotkeySequence/package.json | 23 + .../preact/useHotkeySequence/src/index.css | 135 ++++ .../preact/useHotkeySequence/src/index.tsx | 201 +++++ .../preact/useHotkeySequence/tsconfig.json | 21 + .../preact/useHotkeySequence/vite.config.ts | 7 + examples/preact/useKeyhold/eslint.config.js | 11 + examples/preact/useKeyhold/index.html | 14 + examples/preact/useKeyhold/package.json | 23 + examples/preact/useKeyhold/src/index.css | 127 +++ examples/preact/useKeyhold/src/index.tsx | 119 +++ examples/preact/useKeyhold/tsconfig.json | 21 + examples/preact/useKeyhold/vite.config.ts | 7 + 35 files changed, 2813 insertions(+) create mode 100644 examples/preact/useHeldKeys/eslint.config.js create mode 100644 examples/preact/useHeldKeys/index.html create mode 100644 examples/preact/useHeldKeys/package.json create mode 100644 examples/preact/useHeldKeys/src/index.css create mode 100644 examples/preact/useHeldKeys/src/index.tsx create mode 100644 examples/preact/useHeldKeys/tsconfig.json create mode 100644 examples/preact/useHeldKeys/vite.config.ts create mode 100644 examples/preact/useHotkey/eslint.config.js create mode 100644 examples/preact/useHotkey/index.html create mode 100644 examples/preact/useHotkey/package.json create mode 100644 examples/preact/useHotkey/src/index.css create mode 100644 examples/preact/useHotkey/src/index.tsx create mode 100644 examples/preact/useHotkey/tsconfig.json create mode 100644 examples/preact/useHotkey/vite.config.ts create mode 100644 examples/preact/useHotkeyRecorder/eslint.config.js create mode 100644 examples/preact/useHotkeyRecorder/index.html create mode 100644 examples/preact/useHotkeyRecorder/package.json create mode 100644 examples/preact/useHotkeyRecorder/src/index.css create mode 100644 examples/preact/useHotkeyRecorder/src/index.tsx create mode 100644 examples/preact/useHotkeyRecorder/tsconfig.json create mode 100644 examples/preact/useHotkeyRecorder/vite.config.ts create mode 100644 examples/preact/useHotkeySequence/eslint.config.js create mode 100644 examples/preact/useHotkeySequence/index.html create mode 100644 examples/preact/useHotkeySequence/package.json create mode 100644 examples/preact/useHotkeySequence/src/index.css create mode 100644 examples/preact/useHotkeySequence/src/index.tsx create mode 100644 examples/preact/useHotkeySequence/tsconfig.json create mode 100644 examples/preact/useHotkeySequence/vite.config.ts create mode 100644 examples/preact/useKeyhold/eslint.config.js create mode 100644 examples/preact/useKeyhold/index.html create mode 100644 examples/preact/useKeyhold/package.json create mode 100644 examples/preact/useKeyhold/src/index.css create mode 100644 examples/preact/useKeyhold/src/index.tsx create mode 100644 examples/preact/useKeyhold/tsconfig.json create mode 100644 examples/preact/useKeyhold/vite.config.ts 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 + + + +
+ + + diff --git a/examples/preact/useHeldKeys/package.json b/examples/preact/useHeldKeys/package.json new file mode 100644 index 0000000..1319351 --- /dev/null +++ b/examples/preact/useHeldKeys/package.json @@ -0,0 +1,23 @@ +{ + "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.1.3", + "preact": "^10.27.2" + }, + "devDependencies": { + "@preact/preset-vite": "^2.10.2", + "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..7263999 --- /dev/null +++ b/examples/preact/useHeldKeys/src/index.tsx @@ -0,0 +1,145 @@ +import React from 'preact/compat' +import { render } from 'preact' +import { + formatKeyForDebuggingDisplay, + useHeldKeys, + useHeldKeyCodes, +} from '@tanstack/preact-hotkeys' +import { HotkeysProvider } from '@tanstack/preact-hotkeys' +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 ( +
+
+

useHeldKeys

+

+ Returns an array of all currently pressed keys. Useful for displaying + key combinations or building custom shortcut recording. +

+
+ +
+
+

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...

+ )} + +
+ +
+

Use Cases

+
    +
  • Building a keyboard shortcut recorder
  • +
  • Displaying currently held keys to users
  • +
  • Debugging keyboard input
  • +
  • Creating key combination tutorials
  • +
+
+
+
+ ) +} + +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..eea3d2e --- /dev/null +++ b/examples/preact/useHeldKeys/tsconfig.json @@ -0,0 +1,21 @@ +{ + "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..cb76bf8 --- /dev/null +++ b/examples/preact/useHeldKeys/vite.config.ts @@ -0,0 +1,7 @@ +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 + + + +
+ + + diff --git a/examples/preact/useHotkey/package.json b/examples/preact/useHotkey/package.json new file mode 100644 index 0000000..8f485db --- /dev/null +++ b/examples/preact/useHotkey/package.json @@ -0,0 +1,23 @@ +{ + "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.1.3", + "preact": "^10.27.2" + }, + "devDependencies": { + "@preact/preset-vite": "^2.10.2", + "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..208951c --- /dev/null +++ b/examples/preact/useHotkey/src/index.tsx @@ -0,0 +1,738 @@ +import React from 'preact/compat' +import { render } from 'preact' +import { formatForDisplay, useHotkey } from '@tanstack/preact-hotkeys' +import { HotkeysProvider } from '@tanstack/preact-hotkeys' +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 ( +
+
+

useHotkey

+

+ Register keyboard shortcuts with callback context containing the + hotkey and parsed hotkey information. +

+
+ +
+
+

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'} +

+ +
{`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

+ + {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. +

+ +
+
+ )} +
+ + {/* 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 +
+
+