diff --git a/src/App.tsx b/src/App.tsx index 95f4371..a81157f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,6 +5,7 @@ import { Sidebar } from './components/Sidebar'; import { Chat } from './components/Chat'; import { LoginScreen } from './components/LoginScreen'; import { ConnectionBanner } from './components/ConnectionBanner'; +import { KeyboardShortcuts } from './components/KeyboardShortcuts'; export default function App() { const { @@ -13,13 +14,21 @@ export default function App() { authenticated, login, logout, connectError, isConnecting, } = useGateway(); const [sidebarOpen, setSidebarOpen] = useState(false); + const [shortcutsOpen, setShortcutsOpen] = useState(false); - // Close sidebar on Escape key + // Close sidebar on Escape key, open shortcuts on ? const handleKeyDown = useCallback((e: KeyboardEvent) => { if (e.key === 'Escape' && sidebarOpen) { setSidebarOpen(false); } - }, [sidebarOpen]); + // Open shortcuts help with ? (only when not typing in an input) + if (e.key === '?' && !shortcutsOpen) { + const tag = (e.target as HTMLElement)?.tagName; + if (tag === 'INPUT' || tag === 'TEXTAREA' || (e.target as HTMLElement)?.isContentEditable) return; + e.preventDefault(); + setShortcutsOpen(true); + } + }, [sidebarOpen, shortcutsOpen]); useEffect(() => { document.addEventListener('keydown', handleKeyDown); @@ -54,6 +63,7 @@ export default function App() { + setShortcutsOpen(false)} /> ); } diff --git a/src/components/KeyboardShortcuts.tsx b/src/components/KeyboardShortcuts.tsx new file mode 100644 index 0000000..71fa834 --- /dev/null +++ b/src/components/KeyboardShortcuts.tsx @@ -0,0 +1,118 @@ +import { useEffect } from 'react'; +import { X, Keyboard } from 'lucide-react'; +import { useT } from '../hooks/useLocale'; + +interface Props { + open: boolean; + onClose: () => void; +} + +function Kbd({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} + +function ShortcutRow({ keys, label }: { keys: React.ReactNode; label: string }) { + return ( +
+ {label} +
{keys}
+
+ ); +} + +function SectionTitle({ children }: { children: React.ReactNode }) { + return ( +
+ {children} +
+ ); +} + +const isMac = typeof navigator !== 'undefined' && /Mac|iPod|iPhone|iPad/.test(navigator.userAgent); +const mod = isMac ? '⌘' : 'Ctrl'; + +export function KeyboardShortcuts({ open, onClose }: Props) { + const t = useT(); + + useEffect(() => { + if (!open) return; + const handler = (e: KeyboardEvent) => { + if (e.key === 'Escape') onClose(); + }; + window.addEventListener('keydown', handler); + return () => window.removeEventListener('keydown', handler); + }, [open, onClose]); + + if (!open) return null; + + return ( +
+ {/* Backdrop */} +
+ + {/* Modal */} +
e.stopPropagation()} + > + {/* Header */} +
+
+ +

{t('shortcuts.title')}

+
+ +
+ + {/* Body */} +
+
+ {t('shortcuts.chatSection')} + Enter} + label={t('shortcuts.send')} + /> + Shift+Enter} + label={t('shortcuts.newline')} + /> + Esc} + label={t('shortcuts.stop')} + /> +
+ +
+ {t('shortcuts.navigationSection')} + {mod}+K} + label={t('shortcuts.search')} + /> + Esc} + label={t('shortcuts.closeSidebar')} + /> +
+ +
+ {t('shortcuts.generalSection')} + ?} + label={t('shortcuts.help')} + /> +
+
+
+
+ ); +} diff --git a/src/lib/i18n.ts b/src/lib/i18n.ts index 26da876..dc6d94a 100644 --- a/src/lib/i18n.ts +++ b/src/lib/i18n.ts @@ -62,6 +62,19 @@ const en = { // Timestamps 'time.yesterday': 'Yesterday', + + // Keyboard shortcuts + 'shortcuts.title': 'Keyboard Shortcuts', + 'shortcuts.send': 'Send message', + 'shortcuts.newline': 'New line', + 'shortcuts.search': 'Search sessions', + 'shortcuts.closeSidebar': 'Close sidebar / search', + 'shortcuts.stop': 'Stop generation', + 'shortcuts.help': 'Show shortcuts', + 'shortcuts.close': 'Close', + 'shortcuts.chatSection': 'Chat', + 'shortcuts.navigationSection': 'Navigation', + 'shortcuts.generalSection': 'General', } as const; const fr: Record = { @@ -110,6 +123,18 @@ const fr: Record = { 'message.copied': 'Copié !', 'time.yesterday': 'Hier', + + 'shortcuts.title': 'Raccourcis clavier', + 'shortcuts.send': 'Envoyer le message', + 'shortcuts.newline': 'Nouvelle ligne', + 'shortcuts.search': 'Rechercher des sessions', + 'shortcuts.closeSidebar': 'Fermer la barre / recherche', + 'shortcuts.stop': 'Arrêter la génération', + 'shortcuts.help': 'Afficher les raccourcis', + 'shortcuts.close': 'Fermer', + 'shortcuts.chatSection': 'Chat', + 'shortcuts.navigationSection': 'Navigation', + 'shortcuts.generalSection': 'Général', }; export type TranslationKey = keyof typeof en;