/* @refresh reload */
import { render } from 'solid-js/web'
import { createSignal, Show, For } from 'solid-js'
import {
formatForDisplay,
createHotkey,
createHeldKeys,
createHotkeyRecorder,
HotkeysProvider,
} from '@tanstack/solid-hotkeys'
import { hotkeysDevtoolsPlugin } from '@tanstack/solid-hotkeys-devtools'
import { TanStackDevtools } from '@tanstack/solid-devtools'
import type { Hotkey } from '@tanstack/solid-hotkeys'
import './index.css'
const DEFAULT_SHORTCUT_ACTIONS: Record<
string,
{ name: string; defaultHotkey: Hotkey }
> = {
save: { name: 'Save', defaultHotkey: 'Mod+K' },
open: { name: 'Open', defaultHotkey: 'Mod+E' },
new: { name: 'New', defaultHotkey: 'Mod+G' },
close: { name: 'Close', defaultHotkey: 'Mod+Shift+K' },
undo: { name: 'Undo', defaultHotkey: 'Mod+Shift+E' },
redo: { name: 'Redo', defaultHotkey: 'Mod+Shift+G' },
}
const ACTION_ENTRIES = Object.entries(DEFAULT_SHORTCUT_ACTIONS)
function ShortcutListItem(props: {
actionName: string
hotkey: string
isRecording: boolean
onEdit: () => void
onCancel: () => void
}) {
const heldKeys = createHeldKeys()
return (
<div class={`shortcut-item ${props.isRecording ? 'recording' : ''}`}>
<div class="shortcut-item-content">
<div class="shortcut-action">{props.actionName}</div>
<div class="shortcut-hotkey">
{props.isRecording ? (
<div class="recording-indicator">
{heldKeys().length > 0 ? (
<div class="held-hotkeys">
{heldKeys().flatMap((key, index) =>
index > 0
? [<span class="plus">+</span>, <kbd>{key}</kbd>]
: [<kbd>{key}</kbd>],
)}
</div>
) : (
<span class="recording-text">Press any key combination...</span>
)}
</div>
) : props.hotkey ? (
<kbd>{formatForDisplay(props.hotkey as Hotkey)}</kbd>
) : (
<span class="no-shortcut">No shortcut</span>
)}
</div>
</div>
<div class="shortcut-actions">
{props.isRecording ? (
<button onClick={props.onCancel} class="cancel-button">
Cancel
</button>
) : (
<button onClick={props.onEdit} class="edit-button">
Edit
</button>
)}
</div>
</div>
)
}
function App() {
const [shortcuts, setShortcuts] = createSignal<Record<string, Hotkey | ''>>(
() => {
const defaults: Record<string, Hotkey> = {}
for (const [id, action] of Object.entries(DEFAULT_SHORTCUT_ACTIONS)) {
defaults[id] = action.defaultHotkey
}
return defaults
},
)
const [saveCount, setSaveCount] = createSignal(0)
const [openCount, setOpenCount] = createSignal(0)
const [newCount, setNewCount] = createSignal(0)
const [closeCount, setCloseCount] = createSignal(0)
const [undoCount, setUndoCount] = createSignal(0)
const [redoCount, setRedoCount] = createSignal(0)
const [recordingActionId, setRecordingActionId] = createSignal<string | null>(
null,
)
const recorder = createHotkeyRecorder({
onRecord: (hotkey: Hotkey) => {
const id = recordingActionId()
if (id) {
setShortcuts((prev) => ({
...prev,
[id]: hotkey || ('' as Hotkey | ''),
}))
setRecordingActionId(null)
}
},
onCancel: () => setRecordingActionId(null),
onClear: () => {
const id = recordingActionId()
if (id) {
setShortcuts((prev) => ({
...prev,
[id]: '' as Hotkey | '',
}))
setRecordingActionId(null)
}
},
})
const saveHotkey = () =>
shortcuts().save || DEFAULT_SHORTCUT_ACTIONS.save.defaultHotkey
const openHotkey = () =>
shortcuts().open || DEFAULT_SHORTCUT_ACTIONS.open.defaultHotkey
const newHotkey = () =>
shortcuts().new || DEFAULT_SHORTCUT_ACTIONS.new.defaultHotkey
const closeHotkey = () =>
shortcuts().close || DEFAULT_SHORTCUT_ACTIONS.close.defaultHotkey
const undoHotkey = () =>
shortcuts().undo || DEFAULT_SHORTCUT_ACTIONS.undo.defaultHotkey
const redoHotkey = () =>
shortcuts().redo || DEFAULT_SHORTCUT_ACTIONS.redo.defaultHotkey
createHotkey(
saveHotkey,
() => {
setSaveCount((c) => c + 1)
},
() => ({
enabled: !recorder.isRecording() && shortcuts().save !== '',
}),
)
createHotkey(
openHotkey,
() => setOpenCount((c) => c + 1),
() => ({
enabled: !recorder.isRecording() && shortcuts().open !== '',
}),
)
createHotkey(
newHotkey,
() => setNewCount((c) => c + 1),
() => ({
enabled: !recorder.isRecording() && shortcuts().new !== '',
}),
)
createHotkey(
closeHotkey,
() => setCloseCount((c) => c + 1),
() => ({
enabled: !recorder.isRecording() && shortcuts().close !== '',
}),
)
createHotkey(
undoHotkey,
() => setUndoCount((c) => c + 1),
() => ({
enabled: !recorder.isRecording() && shortcuts().undo !== '',
}),
)
createHotkey(
redoHotkey,
() => setRedoCount((c) => c + 1),
() => ({
enabled: !recorder.isRecording() && shortcuts().redo !== '',
}),
)
const handleEdit = (actionId: string) => {
setRecordingActionId(actionId)
recorder.startRecording()
}
const handleCancel = () => {
recorder.cancelRecording()
setRecordingActionId(null)
}
return (
<div class="app">
<header>
<h1>Keyboard Shortcuts Settings</h1>
<p>
Customize your keyboard shortcuts. Click "Edit" to record a new
shortcut, or press Escape to cancel.
</p>
</header>
<main>
<section class="demo-section">
<h2>Shortcuts</h2>
<div class="shortcuts-list">
<For each={ACTION_ENTRIES}>
{([actionId, action]) => (
<ShortcutListItem
actionName={action.name}
hotkey={shortcuts()[actionId] || ''}
isRecording={
recorder.isRecording() && recordingActionId() === actionId
}
onEdit={() => handleEdit(actionId)}
onCancel={handleCancel}
/>
)}
</For>
</div>
</section>
<section class="demo-section">
<h2>Demo Actions</h2>
<p>Try your shortcuts! Actions will trigger when you press them.</p>
<div class="demo-stats">
<div class="stat-item">
<div class="stat-label">Save</div>
<div class="stat-value">{saveCount()}</div>
<kbd>{formatForDisplay(shortcuts().save || 'Mod+K')}</kbd>
</div>
<div class="stat-item">
<div class="stat-label">Open</div>
<div class="stat-value">{openCount()}</div>
<kbd>{formatForDisplay(shortcuts().open || 'Mod+E')}</kbd>
</div>
<div class="stat-item">
<div class="stat-label">New</div>
<div class="stat-value">{newCount()}</div>
<kbd>{formatForDisplay(shortcuts().new || 'Mod+G')}</kbd>
</div>
<div class="stat-item">
<div class="stat-label">Close</div>
<div class="stat-value">{closeCount()}</div>
<kbd>{formatForDisplay(shortcuts().close || 'Mod+Shift+K')}</kbd>
</div>
<div class="stat-item">
<div class="stat-label">Undo</div>
<div class="stat-value">{undoCount()}</div>
<kbd>{formatForDisplay(shortcuts().undo || 'Mod+Shift+E')}</kbd>
</div>
<div class="stat-item">
<div class="stat-label">Redo</div>
<div class="stat-value">{redoCount()}</div>
<kbd>{formatForDisplay(shortcuts().redo || 'Mod+Shift+G')}</kbd>
</div>
</div>
</section>
<Show when={recorder.isRecording()}>
<div class="info-box recording-notice">
<strong>Recording shortcut...</strong> Press any key combination or
Escape to cancel. Press Backspace/Delete to clear the shortcut.
</div>
</Show>
<section class="demo-section">
<h2>Usage</h2>
<pre class="code-block">{`import { createHotkey, createHotkeyRecorder } from '@tanstack/solid-hotkeys'
function App() {
const [shortcuts, setShortcuts] = createSignal({
save: 'Mod+K',
open: 'Mod+E',
})
const recorder = createHotkeyRecorder({
onRecord: (hotkey) => setShortcuts(prev => ({ ...prev, save: hotkey }))
})
createHotkey(
() => shortcuts().save,
() => handleSave(),
() => ({ enabled: !recorder.isRecording() })
)
}`}</pre>
</section>
</main>
<TanStackDevtools plugins={[hotkeysDevtoolsPlugin()]} />
</div>
)
}
const root = document.getElementById('root')!
render(
() => (
<HotkeysProvider>
<App />
</HotkeysProvider>
),
root,
)