From c19591c86a2f01b47972b087406ac6c24dd9668d Mon Sep 17 00:00:00 2001 From: alpha Date: Fri, 4 Sep 2026 13:38:18 -0400 Subject: [PATCH] fix(hotkeys): make the shortcut cheat-sheet click-to-open, not hover-only status-panel.tsx's "Show Hotkeys" control only ever revealed the list on hover - nothing visible without pointing at it first, the exact antipattern the rest of the control bar has. Converts it to a proper dialog opened by click or a new '?' shortcut (local to this window, not a registered global hotkey - it only needs focus, not stealth-mode reach). Extracts the hotkey list itself into hotkey-cheatsheet.tsx so status-panel.tsx and documentation-dialog.tsx render the same data instead of two independently-drifting copies. --- .../custom/documentation-dialog.tsx | 33 +------- .../components/custom/hotkey-cheatsheet.tsx | 75 +++++++++++++++++ .../components/custom/status-panel.tsx | 80 ++++++++----------- 3 files changed, 110 insertions(+), 78 deletions(-) create mode 100644 src/renderer/components/custom/hotkey-cheatsheet.tsx diff --git a/src/renderer/components/custom/documentation-dialog.tsx b/src/renderer/components/custom/documentation-dialog.tsx index ba185737..1e0b5c41 100644 --- a/src/renderer/components/custom/documentation-dialog.tsx +++ b/src/renderer/components/custom/documentation-dialog.tsx @@ -16,11 +16,10 @@ import { DialogTitle, } from '@/components/ui/dialog'; import { APP_NAME } from '@/lib/consts'; -import { Hotkey, HOTKEY_GROUPS, HOTKEYS } from '@/lib/hotkeys'; -import { cn } from '@/lib/utils'; import { LANGUAGES } from '@/types/language'; import ExternalLink from './external-link'; +import { HotkeyCheatsheet } from './hotkey-cheatsheet'; interface DocumentationDialogProps { open: boolean; @@ -143,35 +142,7 @@ export default function DocumentationDialog({ open, onOpenChange }: Documentatio Hotkeys - {HOTKEY_GROUPS.map((group) => ( -
-

{group.label}

-
- {group.keys.map((hk) => { - const info = HOTKEYS[hk]; - return ( - -
-
- {info.combo} -
-
-
{info.description}
-
- ); - })} -
-
- ))} +
diff --git a/src/renderer/components/custom/hotkey-cheatsheet.tsx b/src/renderer/components/custom/hotkey-cheatsheet.tsx new file mode 100644 index 00000000..4e65e7bc --- /dev/null +++ b/src/renderer/components/custom/hotkey-cheatsheet.tsx @@ -0,0 +1,75 @@ +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { Hotkey, HOTKEY_GROUPS, HOTKEYS } from '@/lib/hotkeys'; +import { cn } from '@/lib/utils'; + +const comboClass = (hk: Hotkey) => + cn( + 'shrink-0 px-2 py-1 rounded text-[11px] font-semibold whitespace-nowrap', + hk === Hotkey.StopAll + ? 'bg-destructive/80 text-destructive-foreground' + : hk === Hotkey.ToggleStealth + ? 'bg-primary/80 text-primary-foreground' + : 'bg-muted text-foreground' + ); + +/** + * The full hotkey reference, grouped and described. Shared so the control bar's status panel and + * the documentation dialog can't drift out of sync the way the two independent copies they + * replace already had. + */ +export function HotkeyCheatsheet() { + return ( +
+ {HOTKEY_GROUPS.map((group) => ( +
+

+ {group.label} +

+
+ {group.keys.map((hk) => { + const info = HOTKEYS[hk]; + return ( +
+
{info.combo}
+
+

{info.title}

+

+ {info.description} +

+
+
+ ); + })} +
+
+ ))} +
+ ); +} + +interface HotkeyCheatsheetDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; +} + +export function HotkeyCheatsheetDialog({ open, onOpenChange }: HotkeyCheatsheetDialogProps) { + return ( + + + + Keyboard Shortcuts + Press ? anytime to reopen this list. + +
+ +
+
+
+ ); +} diff --git a/src/renderer/components/custom/status-panel.tsx b/src/renderer/components/custom/status-panel.tsx index dea729c1..a3b6356d 100644 --- a/src/renderer/components/custom/status-panel.tsx +++ b/src/renderer/components/custom/status-panel.tsx @@ -1,15 +1,36 @@ import { Captions, CaptionsOff, Keyboard, ListChecks, Route } from 'lucide-react'; +import { useEffect, useState } from 'react'; import CreditsDisplay from '@/components/custom/credits-display'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import { useProfessionalMode } from '@/hooks/use-professional-mode'; import { useTranscriptPanel } from '@/hooks/use-transcript-panel'; -import { Hotkey, HOTKEY_GROUPS, HOTKEYS } from '@/lib/hotkeys'; +import { Hotkey, HOTKEYS } from '@/lib/hotkeys'; import { cn } from '@/lib/utils'; import { RunningState, UserRole } from '@/types/app-state'; +import { HotkeyCheatsheetDialog } from './hotkey-cheatsheet'; import { RunningIndicator } from './running-indicator'; +/** + * Renderer-local, not a registered Hotkey: it only needs to work while this window has focus, + * unlike the globalShortcut-backed ones in lib/hotkeys.ts that must also fire in stealth mode. + */ +function useHotkeyCheatsheetShortcut(onOpen: () => void) { + useEffect(() => { + const handler = (e: KeyboardEvent) => { + if (e.key !== '?' || e.metaKey || e.ctrlKey || e.altKey) return; + const target = e.target as HTMLElement | null; + const tag = target?.tagName; + if (tag === 'INPUT' || tag === 'TEXTAREA' || target?.isContentEditable) return; + e.preventDefault(); + onOpen(); + }; + window.addEventListener('keydown', handler); + return () => window.removeEventListener('keydown', handler); + }, [onOpen]); +} + interface StatusPanelProps { runningState: RunningState; credits: number; @@ -34,6 +55,8 @@ export default function StatusPanel({ // calculate and formatting handled by CreditsDisplay component const { enabled: professionalMode } = useProfessionalMode(); const { visible: transcriptVisible } = useTranscriptPanel(); + const [hotkeysOpen, setHotkeysOpen] = useState(false); + useHotkeyCheatsheetShortcut(() => setHotkeysOpen(true)); return (
@@ -79,52 +102,15 @@ export default function StatusPanel({
- - - - - -
- {HOTKEY_GROUPS.map((group) => ( -
-
- {group.label} -
-
- {group.keys.map((hk) => { - const info = HOTKEYS[hk]; - return ( -
-
- {info.combo} -
-
- {info.title} -
-
- ); - })} -
-
- ))} -
-
-
+ +
); }