import { useStore } from '@nanostores/react' import { Dialog as DialogPrimitive } from 'radix-ui' import { useState } from 'react' import { Button } from '@/components/ui/button' import { Codicon } from '@/components/ui/codicon' import { DisclosureCaret } from '@/components/ui/disclosure-caret' import { useI18n } from '@/i18n' import { KEYBIND_ACTIONS, KEYBIND_CATEGORIES, KEYBIND_PANEL_ACTION, KEYBIND_READONLY, type KeybindActionMeta, type KeybindReadonly } from '@/lib/keybinds/actions' import { formatCombo } from '@/lib/keybinds/combo' import { arraysEqual } from '@/lib/storage' import { $bindings, $capture, $keybindPanelOpen, beginCapture, closeKeybindPanel, conflictsFor, endCapture, resetAllBindings, resetBinding } from '@/store/keybinds' // The full hotkey map. Quiet popover, click a row's chip to rebind. export function KeybindPanel() { const { t } = useI18n() const open = useStore($keybindPanelOpen) const bindings = useStore($bindings) const k = t.keybinds const [collapsed, setCollapsed] = useState>(new Set()) const openCombo = bindings[KEYBIND_PANEL_ACTION]?.[0] const toggleCategory = (category: string) => setCollapsed(prev => { const next = new Set(prev) if (next.has(category)) { next.delete(category) } else { next.add(category) } return next }) return ( !next && closeKeybindPanel()} open={open}> {/* Header */}
{k.title} {k.subtitle(openCombo ? formatCombo(openCombo) : '')}
{/* Body */}
{KEYBIND_CATEGORIES.map(category => { const actions = KEYBIND_ACTIONS.filter( action => action.category === category && action.id !== KEYBIND_PANEL_ACTION ) const readonly = KEYBIND_READONLY.filter(shortcut => shortcut.category === category) if (actions.length === 0 && readonly.length === 0) { return null } const sectionOpen = !collapsed.has(category) return (
toggleCategory(category)} open={sectionOpen} /> {sectionOpen && actions.map(action => )} {sectionOpen && readonly.map(shortcut => )}
) })}
) } // Collapsible category header — chevron fades in on hover, rotates when open // (matches the sessions sidebar section pattern). function CategoryHeader({ label, onToggle, open }: { label: string; onToggle: () => void; open: boolean }) { return ( ) } function HeaderButton({ icon, label, onClick }: { icon: string; label: string; onClick: () => void }) { return ( ) } function KeybindRow({ action }: { action: KeybindActionMeta }) { const { t } = useI18n() const k = t.keybinds const bindings = useStore($bindings) const capture = useStore($capture) const combos = bindings[action.id] ?? [] const capturing = capture === action.id const label = k.actions[action.id] ?? action.id const isDefault = arraysEqual(combos, [...action.defaults]) const conflict = combos .flatMap(combo => conflictsFor(action.id, combo).map(other => k.actions[other] ?? other)) .find(Boolean) return (
{label} {conflict && ( )} {/* Click the caps to rebind — the on-screen editor does the same thing. */} {/* Reset only shows once a binding diverges from its default; the spacer holds the column otherwise so rows stay aligned. */} {isDefault ? ( ) : ( )}
) } // Fixed shortcut: same layout as KeybindRow but the caps aren't interactive and // the trailing reset slot stays empty (spacer keeps the columns aligned). function ReadonlyRow({ shortcut }: { shortcut: KeybindReadonly }) { const { t } = useI18n() const k = t.keybinds const label = k.actions[shortcut.id] ?? shortcut.id return (
{label}
{shortcut.keys.map(key => ( {formatCombo(key)} ))}
) }