diff --git a/package.json b/package.json index 5bbe9e136..2c157137b 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,7 @@ "@tanstack/markdown": "^1.0.0", "@tanstack/pacer": "^0.21.1", "@tanstack/react-db": "0.4.1", - "@tanstack/react-hotkeys": "^0.10.0", + "@tanstack/react-hotkeys": "^0.14.0", "@tanstack/react-pacer": "^0.22.1", "@tanstack/react-query": "^5.102.8", "@tanstack/react-router": "1.170.41", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c2bb127a4..be1b01d31 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -182,8 +182,8 @@ importers: specifier: 0.4.1 version: 0.4.1(react@19.2.3)(typescript@6.0.2) '@tanstack/react-hotkeys': - specifier: ^0.10.0 - version: 0.10.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + specifier: ^0.14.0 + version: 0.14.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@tanstack/react-pacer': specifier: ^0.22.1 version: 0.22.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -4066,9 +4066,9 @@ packages: resolution: {integrity: sha512-utTS5L2OkeYUzXGohL1Z8sefu1GLNOJcxe8Hd6iIdc/Xo1K1nDB2JEp4iSFhvYh33xKC9V91TxrS8qfrpoKobQ==} engines: {node: '>=20.19'} - '@tanstack/hotkeys@0.8.0': - resolution: {integrity: sha512-vqH7X9nb0MTJ/O08++dB5bP9jgj4+BIPOUu/U+6myG86lDsirZSVSobpq5UQpE7nBuk62i8eIYeOhd+OMl/UrA==} - engines: {node: '>=18'} + '@tanstack/hotkeys@0.12.0': + resolution: {integrity: sha512-DvVl1VMEFQbhOJPBlIouieJPq6Kq+ZuDOe/FC7c1rkEq5d7ZSnn32yDL6L7hlp5n4Z1Cj4F6cEcbcB7lIUC7KQ==} + engines: {node: '>=20'} '@tanstack/markdown@0.0.13': resolution: {integrity: sha512-ZSQP2ulBvlPzdpZeRheBJnaELo7e4ET/D/XrfgweHyqhhLMN5vUJJAUl7ufSRd7GlfO/vwdzjKPh0E0JwqJkDA==} @@ -4130,9 +4130,9 @@ packages: react: '>=16.8' react-dom: '>=16.8' - '@tanstack/react-hotkeys@0.10.0': - resolution: {integrity: sha512-GwOSndI5j3qBVYTmgP1mYyRTnlxb2MS17cwGlsavSxMQPSnmDf+m3LzMIpRMs+3zzQMjg3cYhHsFYizYlFI2tw==} - engines: {node: '>=18'} + '@tanstack/react-hotkeys@0.14.0': + resolution: {integrity: sha512-LrjU1wCT6VFaoZd99KuisrhN0OOqoM512BFE2UNRHQCtGU6cYNufDKadYypCOiSq+pqvyLIxWEIpHykYgjePlQ==} + engines: {node: '>=20'} peerDependencies: react: '>=16.8' react-dom: '>=16.8' @@ -11738,9 +11738,9 @@ snapshots: '@tanstack/history@1.162.4': {} - '@tanstack/hotkeys@0.8.0': + '@tanstack/hotkeys@0.12.0': dependencies: - '@tanstack/store': 0.11.0 + '@tanstack/store': 0.11.2 '@tanstack/markdown@0.0.13(octane@0.1.13(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite@8.0.16(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0)))(react@19.2.3)': optionalDependencies: @@ -11808,10 +11808,10 @@ snapshots: - solid-js - utf-8-validate - '@tanstack/react-hotkeys@0.10.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@tanstack/react-hotkeys@0.14.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@tanstack/hotkeys': 0.8.0 - '@tanstack/react-store': 0.11.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@tanstack/hotkeys': 0.12.0 + '@tanstack/react-store': 0.11.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) diff --git a/src/components/landing/HotkeysKeycap.tsx b/src/components/landing/HotkeysKeycap.tsx new file mode 100644 index 000000000..c8f843df8 --- /dev/null +++ b/src/components/landing/HotkeysKeycap.tsx @@ -0,0 +1,47 @@ +export function Keycap({ + children, + pressed = false, + size = 'md', +}: { + children: React.ReactNode + pressed?: boolean + size?: 'sm' | 'md' | 'lg' +}) { + const sizeClassName = { + sm: 'h-7 min-w-7 rounded-md px-1.5 text-ds-label-sm', + md: 'h-10 min-w-10 rounded-lg px-2.5 text-ds-label-lg', + lg: 'h-16 min-w-16 rounded-xl px-4 text-ds-heading-3', + }[size] + + return ( + + {children} + + ) +} + +export function KeyCombo({ + keys, + pressed = false, + size = 'md', +}: { + keys: ReadonlyArray + pressed?: boolean + size?: 'sm' | 'md' | 'lg' +}) { + return ( + + {keys.map((key, index) => ( + + {key} + + ))} + + ) +} diff --git a/src/components/landing/HotkeysLanding.tsx b/src/components/landing/HotkeysLanding.tsx index 36e489441..b80b77f88 100644 --- a/src/components/landing/HotkeysLanding.tsx +++ b/src/components/landing/HotkeysLanding.tsx @@ -1,438 +1,264 @@ import * as React from 'react' -import { - ArrowsLeftRightIcon, - ClockIcon, - CommandIcon, - CrosshairIcon, - KeyboardIcon, - StackIcon, - WarningCircleIcon, -} from '@phosphor-icons/react' +import { ClientOnly } from '@tanstack/react-router' +import { createIsomorphicFn } from '@tanstack/react-start' import { - LandingEyebrow, LandingSection, - LandingSectionIntro, LandingWindow, LibraryLandingShell, } from './LibraryLanding' +// Live demos register real shortcuts, so their module only loads in the browser. +const loadDemos = createIsomorphicFn() + .client(() => import('./HotkeysLandingDemos.client')) + .server(async () => { + throw new Error('Hotkeys landing demos only load in the browser.') + }) +const ShortcutLab = React.lazy(() => + loadDemos().then((m) => ({ default: m.ShortcutLab })), +) +const FormatLab = React.lazy(() => + loadDemos().then((m) => ({ default: m.FormatLab })), +) +const DefaultsLab = React.lazy(() => + loadDemos().then((m) => ({ default: m.DefaultsLab })), +) +const RebindLab = React.lazy(() => + loadDemos().then((m) => ({ default: m.RebindLab })), +) + const hotkeysPrompt = - 'Build a command system with TanStack Hotkeys. Define typed shortcuts, portable Mod bindings, scopes, input filtering, conflict handling, sequences, held keys, recording, and platform-aware display. Keep commands separate from their bindings so users can customize them.' + 'Add keyboard shortcuts with TanStack Hotkeys. Register commands with useHotkey using Mod bindings so they work on macOS, Windows, and Linux, and use physical [Code] bindings only where key position matters. Pass an array of keys such as G G to useHotkey for a sequence. Show platform-correct labels with formatForDisplay and reveal hints with useHotkeyHint while modifiers are held. Declare app hotkeys with defineAppHotkeys and pass them to HotkeysProvider as appHotkeys, with customHotkeys and onCustomHotkeysChange to persist user rebinding. Use useHotkeyRecorder with detectConflicts and onReject to record new bindings.' -const commands = [ +const edgeCases = [ { - name: 'Open search', - binding: 'Mod+K', - scope: 'workspace', - inputs: 'blocked', + title: 'Text fields', + tag: 'j in a textarea', + body: "Pressing J in a comment box types a J. It doesn't jump to the next item.", }, { - name: 'Show shortcuts', - binding: 'Shift+?', - scope: 'global', - inputs: 'allowed', + title: 'Browser defaults', + tag: '⌘S', + body: '⌘S saves your doc, not the web page.', }, { - name: 'Archive card', - binding: 'E', - scope: 'board', - inputs: 'blocked', + title: 'macOS, Windows, and Linux', + tag: 'Mod+S', + body: 'Write Mod once. Mac users get ⌘, everyone else gets Ctrl.', }, -] as const - -const scopes = [ { - name: 'global', - body: 'Help, navigation, and app-wide commands', - keys: ['Shift+?', 'Mod+K'], + title: 'Sequences', + tag: 'G then I', + body: "G then I for the inbox. The Konami code, if you're feeling it.", }, { - name: 'workspace', - body: 'Commands that only make sense inside a project', - keys: ['G then D', 'Mod+P'], + title: 'Conflicts', + tag: '⌘K twice', + body: 'Two features grab the same key? You decide who wins.', }, { - name: 'modal', - body: 'A temporary scope that wins while a dialog is open', - keys: ['Enter', 'Escape'], - }, -] as const - -const gestures = [ - { - icon: CommandIcon, - label: 'Chord', - binding: 'Mod + Shift + P', - body: 'Several keys resolve as one command.', + title: 'Cleanup', + tag: 'unmount', + body: 'Shortcuts go away with the component that added them.', }, { - icon: ArrowsLeftRightIcon, - label: 'Sequence', - binding: 'G then D', - body: 'Ordered keys create a tiny command language.', + title: 'Keyboard layouts', + tag: '⌥S → "ß"', + body: 'German, French, Russian, Dvorak. Your shortcuts still work.', }, { - icon: ClockIcon, - label: 'Held key', - binding: 'Space · 400ms', - body: 'Press duration can become part of the gesture.', + title: 'Input methods', + tag: 'IME', + body: "People typing Japanese or Chinese won't trigger shortcuts by accident.", }, ] as const +// Real completions from the Hotkey type for 'Mod+Alt+K. +const completions = [ + ['Mod+Alt+K', ''], + ['Mod+Alt+K', 'anaMode'], + ['Mod+Alt+K', 'atakana'], + ['Mod+Alt+[K', 'anaMode]'], + ['Mod+Alt+[K', 'atakana]'], + ['Mod+Alt+[K', 'eyA]'], +] as const + export default function HotkeysLanding() { return ( } + description="Type-safe keyboard shortcuts for any framework. They stay out of the way while people type, show the right keys on every platform, and let users pick their own." + headline="Great defaults for everyone. Superpowers for your power users." + hero={ + + + + } libraryId="hotkeys" prompt={hotkeysPrompt} promptLabel="Copy Hotkeys prompt" > - -
-
-
- -