mirror of
https://github.com/davidkaya/aryx.git
synced 2026-07-30 08:28:48 +02:00
feat: add rich keyboard shortcuts with cheat sheet overlay
Comprehensive keybindings for power-user navigation: - Ctrl+N: new session, Ctrl+W: archive session - Ctrl+Tab / Ctrl+Shift+Tab: cycle between sessions - Ctrl+,: open settings, Ctrl+/: shortcuts cheat sheet - Ctrl+.: quick-approve pending tool call - Ctrl+L: focus composer, Escape: cancel turn / close overlay Centralized shortcut registry in lib/keyboardShortcuts.ts drives both the cheat sheet panel and command palette shortcut badges. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import {
|
||||
Copy,
|
||||
FolderOpen,
|
||||
FolderPlus,
|
||||
Keyboard,
|
||||
MessageSquare,
|
||||
Monitor,
|
||||
Moon,
|
||||
@@ -20,6 +21,7 @@ import {
|
||||
import type { AppearanceTheme } from '@shared/domain/tooling';
|
||||
import { isScratchpadProject } from '@shared/domain/project';
|
||||
import type { WorkspaceState } from '@shared/domain/workspace';
|
||||
import { shortcutKeys } from '@renderer/lib/keyboardShortcuts';
|
||||
|
||||
interface PaletteCommand {
|
||||
id: string;
|
||||
@@ -47,6 +49,7 @@ export interface CommandPaletteProps {
|
||||
onArchiveSession: (sessionId: string, isArchived: boolean) => void;
|
||||
onAddProject: () => void;
|
||||
onOpenAppDataFolder: () => void;
|
||||
onShowShortcuts: () => void;
|
||||
}
|
||||
|
||||
/** Score how well `query` matches `text` (and optional `keywords`). 0 = no match. */
|
||||
@@ -86,6 +89,7 @@ export function CommandPalette({
|
||||
onArchiveSession,
|
||||
onAddProject,
|
||||
onOpenAppDataFolder,
|
||||
onShowShortcuts,
|
||||
}: CommandPaletteProps) {
|
||||
const [query, setQuery] = useState('');
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
@@ -151,6 +155,7 @@ export function CommandPalette({
|
||||
label: 'New Session',
|
||||
category: 'Actions',
|
||||
keywords: 'create start',
|
||||
shortcut: shortcutKeys('new-session'),
|
||||
icon: <Plus className={ICON} />,
|
||||
action: () => onNewSession(defaultProjectId),
|
||||
});
|
||||
@@ -187,7 +192,8 @@ export function CommandPalette({
|
||||
id: 'archive-session',
|
||||
label: 'Archive Session',
|
||||
category: 'Session',
|
||||
keywords: 'archive hide remove',
|
||||
keywords: 'archive hide remove close',
|
||||
shortcut: shortcutKeys('close-session'),
|
||||
icon: <Archive className={ICON} />,
|
||||
action: () => onArchiveSession(selectedSession.id, true),
|
||||
});
|
||||
@@ -221,6 +227,7 @@ export function CommandPalette({
|
||||
label: 'Open Settings',
|
||||
category: 'General',
|
||||
keywords: 'preferences config options',
|
||||
shortcut: shortcutKeys('settings'),
|
||||
icon: <Settings className={ICON} />,
|
||||
action: onOpenSettings,
|
||||
});
|
||||
@@ -241,7 +248,7 @@ export function CommandPalette({
|
||||
label: 'Toggle Terminal',
|
||||
category: 'General',
|
||||
keywords: 'terminal console shell command',
|
||||
shortcut: 'Ctrl+`',
|
||||
shortcut: shortcutKeys('toggle-terminal'),
|
||||
icon: <Terminal className={ICON} />,
|
||||
action: onToggleTerminal,
|
||||
});
|
||||
@@ -255,6 +262,16 @@ export function CommandPalette({
|
||||
action: onOpenAppDataFolder,
|
||||
});
|
||||
|
||||
cmds.push({
|
||||
id: 'keyboard-shortcuts',
|
||||
label: 'Keyboard Shortcuts',
|
||||
category: 'General',
|
||||
keywords: 'keys keybindings hotkeys help cheatsheet',
|
||||
shortcut: shortcutKeys('shortcut-help'),
|
||||
icon: <Keyboard className={ICON} />,
|
||||
action: onShowShortcuts,
|
||||
});
|
||||
|
||||
// ── Theme ──
|
||||
cmds.push({
|
||||
id: 'theme-dark',
|
||||
@@ -287,7 +304,7 @@ export function CommandPalette({
|
||||
onSelectSession, onSelectProject, onNewSession, onCreateScratchpad,
|
||||
onOpenSettings, onOpenProjectSettings, onToggleTerminal, onSetTheme,
|
||||
onDuplicateSession, onPinSession, onArchiveSession, onAddProject,
|
||||
onOpenAppDataFolder,
|
||||
onOpenAppDataFolder, onShowShortcuts,
|
||||
]);
|
||||
|
||||
const filteredCommands = useMemo(() => {
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
import { useEffect } from 'react';
|
||||
import { Keyboard } from 'lucide-react';
|
||||
|
||||
import { shortcuts, type ShortcutDefinition } from '@renderer/lib/keyboardShortcuts';
|
||||
|
||||
interface KeyboardShortcutsPanelProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const categoryOrder = ['Navigation', 'Sessions', 'Workspace', 'General'] as const;
|
||||
|
||||
function groupByCategory(defs: ShortcutDefinition[]): Map<string, ShortcutDefinition[]> {
|
||||
const groups = new Map<string, ShortcutDefinition[]>();
|
||||
for (const def of defs) {
|
||||
const list = groups.get(def.category) ?? [];
|
||||
list.push(def);
|
||||
groups.set(def.category, list);
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
export function KeyboardShortcutsPanel({ onClose }: KeyboardShortcutsPanelProps) {
|
||||
// Escape to close — capture phase so it doesn't propagate
|
||||
useEffect(() => {
|
||||
const handleEscape = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', handleEscape, true);
|
||||
return () => document.removeEventListener('keydown', handleEscape, true);
|
||||
}, [onClose]);
|
||||
|
||||
const grouped = groupByCategory(shortcuts);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="palette-backdrop-enter fixed inset-0 z-[70] flex items-center justify-center bg-[#07080e]/80 backdrop-blur-sm"
|
||||
onClick={onClose}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="shortcuts-title"
|
||||
>
|
||||
<div
|
||||
className="palette-enter w-full max-w-lg overflow-hidden rounded-xl border border-[var(--color-border)] bg-[var(--color-surface-1)] shadow-[0_24px_80px_rgba(0,0,0,0.55)]"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-3 border-b border-[var(--color-border)] px-5 py-3.5">
|
||||
<Keyboard className="size-4 text-[var(--color-text-accent)]" />
|
||||
<h2
|
||||
id="shortcuts-title"
|
||||
className="font-display text-[14px] font-semibold text-[var(--color-text-primary)]"
|
||||
>
|
||||
Keyboard Shortcuts
|
||||
</h2>
|
||||
<span className="ml-auto text-[11px] text-[var(--color-text-muted)]">
|
||||
Press <Kbd>Esc</Kbd> to close
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Body — two-column grid of categories */}
|
||||
<div className="grid grid-cols-2 gap-x-6 gap-y-5 px-5 py-4">
|
||||
{categoryOrder.map((cat) => {
|
||||
const items = grouped.get(cat);
|
||||
if (!items?.length) return null;
|
||||
return (
|
||||
<div key={cat}>
|
||||
<h3 className="mb-2 text-[11px] font-medium uppercase tracking-wider text-[var(--color-text-muted)]">
|
||||
{cat}
|
||||
</h3>
|
||||
<ul className="space-y-1.5">
|
||||
{items.map((item) => (
|
||||
<li
|
||||
key={item.id}
|
||||
className="flex items-center justify-between gap-3 text-[12.5px]"
|
||||
>
|
||||
<span className="truncate text-[var(--color-text-secondary)]">
|
||||
{item.label}
|
||||
</span>
|
||||
<ShortcutBadge keys={item.keys} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="border-t border-[var(--color-border)] px-5 py-2.5 text-[11px] text-[var(--color-text-muted)]">
|
||||
Tip: Use the command palette for even more actions
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** Inline keyboard key cap. */
|
||||
function Kbd({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<kbd className="rounded border border-[var(--color-border)] bg-[var(--color-surface-0)] px-1.5 py-0.5 font-mono text-[10px] text-[var(--color-text-muted)]">
|
||||
{children}
|
||||
</kbd>
|
||||
);
|
||||
}
|
||||
|
||||
/** Renders a compound shortcut like "Ctrl+Shift+Tab" as joined key caps. */
|
||||
function ShortcutBadge({ keys }: { keys: string }) {
|
||||
const parts = keys.split('+');
|
||||
return (
|
||||
<span className="flex shrink-0 items-center gap-0.5">
|
||||
{parts.map((part, i) => (
|
||||
<Kbd key={i}>{part}</Kbd>
|
||||
))}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user