From 4fdf71f2c86689f33f8d7e5d353b23c7f3908ac9 Mon Sep 17 00:00:00 2001 From: Kevin Van Cott Date: Mon, 5 Oct 2026 18:05:54 -0500 Subject: [PATCH 1/2] feat(hotkeys): Refresh landing page with live demos Co-Authored-By: Claude Opus 5.5 (1M context) --- package.json | 2 +- pnpm-lock.yaml | 26 +- src/components/landing/HotkeysKeycap.tsx | 47 ++ src/components/landing/HotkeysLanding.tsx | 569 ++++++------------ .../landing/HotkeysLandingDemos.client.tsx | 493 +++++++++++++++ 5 files changed, 752 insertions(+), 385 deletions(-) create mode 100644 src/components/landing/HotkeysKeycap.tsx create mode 100644 src/components/landing/HotkeysLandingDemos.client.tsx diff --git a/package.json b/package.json index c42e37dfe..d0f7377fa 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,7 @@ "@tanstack/markdown": "^0.0.16", "@tanstack/pacer": "^0.21.1", "@tanstack/react-db": "0.4.1", - "@tanstack/react-hotkeys": "^0.10.0", + "@tanstack/react-hotkeys": "^0.13.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 20ff1f131..0baff8261 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -185,8 +185,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.13.0 + version: 0.13.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) @@ -4084,9 +4084,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.11.0': + resolution: {integrity: sha512-uFKTuknj3sNp5ERoxsI0ISMMOVw3KmSkgy2iIcp19QR/zheYm7bkwzNndMv5NDAqhXpavOn6h4A6HPVJFCBXPg==} + engines: {node: '>=20'} '@tanstack/markdown@0.0.13': resolution: {integrity: sha512-ZSQP2ulBvlPzdpZeRheBJnaELo7e4ET/D/XrfgweHyqhhLMN5vUJJAUl7ufSRd7GlfO/vwdzjKPh0E0JwqJkDA==} @@ -4148,9 +4148,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.13.0': + resolution: {integrity: sha512-+iF7bQe1YegbMa/mL7Zl490OXjSuGLlu2IPYd2YcvdeI8OcTHfdENPrRNk7PklWQ2WAIJjCVxMXiQ/Uiq8fzIQ==} + engines: {node: '>=20'} peerDependencies: react: '>=16.8' react-dom: '>=16.8' @@ -11785,9 +11785,9 @@ snapshots: '@tanstack/history@1.162.4': {} - '@tanstack/hotkeys@0.8.0': + '@tanstack/hotkeys@0.11.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: @@ -11855,10 +11855,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.13.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.11.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..0fa635a95 100644 --- a/src/components/landing/HotkeysLanding.tsx +++ b/src/components/landing/HotkeysLanding.tsx @@ -1,438 +1,265 @@ 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. Add a G G sequence with useHotkeySequence. Show platform-correct labels with formatForDisplay and reveal hints with useHotkeyHint while modifiers are held. Let users rebind commands with useHotkeyRecorder using detectConflicts and onReject, and store the recorded bindings as plain strings in app state." -const commands = [ +const edgeCases = [ { - name: 'Open search', - binding: 'Mod+K', - scope: 'workspace', - inputs: 'blocked', + title: 'Text fields', + tag: 'j in a textarea', + body: "Single-key shortcuts don't fire while focus is in a text field. Mod shortcuts and Escape still do.", }, { - name: 'Show shortcuts', - binding: 'Shift+?', - scope: 'global', - inputs: 'allowed', + title: 'Browser defaults', + tag: '⌘S', + body: "Matched shortcuts call preventDefault, so ⌘S runs your save instead of the browser's.", }, { - name: 'Archive card', - binding: 'E', - scope: 'board', - inputs: 'blocked', + title: 'macOS, Windows, and Linux', + tag: 'Mod+S', + body: 'Mod is ⌘ on macOS and Ctrl on Windows and Linux, for both matching and display.', }, -] as const - -const scopes = [ { - name: 'global', - body: 'Help, navigation, and app-wide commands', - keys: ['Shift+?', 'Mod+K'], + title: 'Sequences', + tag: 'G then I', + body: "Multi-key sequences like G then I for the inbox, or the Konami code. Each has a timeout, and modifier presses or key repeats don't advance them.", }, { - name: 'workspace', - body: 'Commands that only make sense inside a project', - keys: ['G then D', 'Mod+P'], + title: 'Conflicts', + tag: '⌘K twice', + body: 'Choose what happens when two registrations share a key. Warn, throw, replace, or allow.', }, { - 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: 'Framework adapters register on mount, update when your state changes, and unregister on unmount.', }, { - icon: ArrowsLeftRightIcon, - label: 'Sequence', - binding: 'G then D', - body: 'Ordered keys create a tiny command language.', + title: 'Keyboard layouts', + tag: '⌥S → "ß"', + body: 'Option, dead keys, AltGr, and non-Latin layouts change what the browser reports. Letter shortcuts still match.', }, { - icon: ClockIcon, - label: 'Held key', - binding: 'Space · 400ms', - body: 'Press duration can become part of the gesture.', + title: 'Input methods', + tag: 'IME', + body: "Shortcuts like / don't fire while an IME is composing Japanese or Chinese text.", }, ] 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 and sequences for any framework. Hotkeys skips them while people type, shows the right keys on each platform, and records new bindings when users want to change them." + headline="Great defaults for everyone. Superpowers for your power users." + hero={ + + + + } libraryId="hotkeys" prompt={hotkeysPrompt} promptLabel="Copy Hotkeys prompt" > - -
-
-
- -