Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 2 additions & 31 deletions src/renderer/components/custom/documentation-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -143,35 +142,7 @@ export default function DocumentationDialog({ open, onOpenChange }: Documentatio
<AccordionItem value="hotkeys">
<AccordionTrigger className="text-sm font-semibold">Hotkeys</AccordionTrigger>
<AccordionContent>
{HOTKEY_GROUPS.map((group) => (
<div key={group.label} className="mb-4 last:mb-0">
<h4 className="text-xs font-medium mb-1">{group.label}</h4>
<div className="grid grid-cols-3 gap-2">
{group.keys.map((hk) => {
const info = HOTKEYS[hk];
return (
<React.Fragment key={hk}>
<div className="col-span-1">
<div
className={cn(
'px-2 py-1 rounded text-[11px] font-semibold min-w-22.5',
hk === Hotkey.StopAll
? 'bg-destructive/80 text-primary-foreground'
: hk === Hotkey.ToggleStealth
? 'bg-primary/80 text-primary-foreground'
: 'bg-muted'
)}
>
{info.combo}
</div>
</div>
<div className="col-span-2 text-sm">{info.description}</div>
</React.Fragment>
);
})}
</div>
</div>
))}
<HotkeyCheatsheet />
</AccordionContent>
</AccordionItem>
</Accordion>
Expand Down
75 changes: 75 additions & 0 deletions src/renderer/components/custom/hotkey-cheatsheet.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="space-y-4">
{HOTKEY_GROUPS.map((group) => (
<div key={group.label}>
<h4 className="text-xs font-semibold uppercase text-muted-foreground mb-2">
{group.label}
</h4>
<div className="space-y-2">
{group.keys.map((hk) => {
const info = HOTKEYS[hk];
return (
<div key={hk} className="flex items-start gap-2">
<div className={comboClass(hk)}>{info.combo}</div>
<div className="min-w-0">
<p className="text-sm font-medium leading-tight">{info.title}</p>
<p className="text-xs text-muted-foreground leading-tight mt-0.5">
{info.description}
</p>
</div>
</div>
);
})}
</div>
</div>
))}
</div>
);
}

interface HotkeyCheatsheetDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
}

export function HotkeyCheatsheetDialog({ open, onOpenChange }: HotkeyCheatsheetDialogProps) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-lg w-full max-h-[80vh] flex flex-col">
<DialogHeader>
<DialogTitle>Keyboard Shortcuts</DialogTitle>
<DialogDescription>Press ? anytime to reopen this list.</DialogDescription>
</DialogHeader>
<div className="overflow-auto flex-1">
<HotkeyCheatsheet />
</div>
</DialogContent>
</Dialog>
);
}
80 changes: 33 additions & 47 deletions src/renderer/components/custom/status-panel.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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 (
<div id="status-panel" className="flex items-center justify-between text-muted-foreground p-1">
Expand Down Expand Up @@ -79,52 +102,15 @@ export default function StatusPanel({
</TooltipContent>
</Tooltip>
<div className="flex-1" />
<Tooltip>
<TooltipTrigger asChild>
<button
className="h-6 flex items-center justify-center rounded hover:bg-muted text-xs font-medium gap-1 px-2"
aria-label="Hotkeys"
title="Show keyboard shortcuts"
>
<Keyboard className="h-4 w-4" /> Show Hotkeys
</button>
</TooltipTrigger>
<TooltipContent sideOffset={4} className="w-2xl rounded-md p-2">
<div className="space-y-2">
{HOTKEY_GROUPS.map((group) => (
<div key={group.label}>
<div className="text-[10px] font-semibold text-foreground mb-1 uppercase">
{group.label}
</div>
<div className="grid grid-cols-3 gap-1">
{group.keys.map((hk) => {
const info = HOTKEYS[hk];
return (
<div key={hk} className="flex items-center gap-1">
<div
className={cn(
'px-1 py-0.5 rounded text-[11px] font-semibold',
hk === Hotkey.StopAll
? 'bg-destructive/80 text-destructive-foreground'
: hk === Hotkey.ToggleStealth
? 'bg-primary/80 text-primary-foreground'
: 'bg-muted text-foreground'
)}
>
{info.combo}
</div>
<div className="text-[11px] font-semibold text-foreground">
{info.title}
</div>
</div>
);
})}
</div>
</div>
))}
</div>
</TooltipContent>
</Tooltip>
<button
className="h-6 flex items-center justify-center rounded hover:bg-muted text-xs font-medium gap-1 px-2"
aria-label="Show keyboard shortcuts"
title="Show keyboard shortcuts (?)"
onClick={() => setHotkeysOpen(true)}
>
<Keyboard className="h-4 w-4" /> Show Hotkeys
</button>
<HotkeyCheatsheetDialog open={hotkeysOpen} onOpenChange={setHotkeysOpen} />
</div>
);
}