Files
ProjectE/apps/web-legacy/components/shortcuts-help.tsx
T
Hermes fca56ab77e T1/Phase 1: scaffold Vite SPA + Hono API + Bun worker
- apps/web: Vite + React 19 + TanStack Router/Query + shadcn/ui
   - apps/api: Hono + Bun on :3001 with /api/health, /api/auth/*, /mcp stubs
   - apps/worker: Bun worker stub, DB connection, graceful SIGTERM
   - apps/web-legacy/: old Next.js code moved aside (preserved for T2-T8 reference)
   - Dockerfiles: api (Bun), worker (Bun), spa (multi-stage Caddy)
   - Caddyfile: serves dist + reverse-proxies /api/* + /mcp to api
   - docker-compose.yml: 4-service target (api, spa, db, worker)
   - packages/db/src/client.ts: shared Drizzle client for api + worker
   - db/client.ts: root-level alias for convenience

   Parent: t_e1cbd87d -> t_24c9c3fd (T0)
2026-08-01 01:15:31 +00:00

163 lines
5.2 KiB
TypeScript

'use client';
import { useEffect, useState, useMemo } from 'react';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
} from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { ScrollArea } from '@/components/ui/scroll-area';
interface ShortcutGroup {
title: string;
shortcuts: { keys: string; description: string }[];
}
const shortcutGroups: ShortcutGroup[] = [
{
title: 'Global',
shortcuts: [
{ keys: '?', description: 'Open keyboard shortcuts help' },
{ keys: '⌘K', description: 'Open command palette' },
{ keys: '⌘⇧K', description: 'Deep search' },
{ keys: 'Esc', description: 'Close panel / dialog' },
],
},
{
title: 'Navigation',
shortcuts: [
{ keys: 'G then D', description: 'Go to Dashboard' },
{ keys: 'G then T', description: 'Go to Tasks' },
{ keys: 'G then H', description: 'Go to Habits' },
{ keys: 'G then P', description: 'Go to Projects' },
{ keys: 'G then N', description: 'Go to Notes' },
{ keys: 'G then G', description: 'Go to Graph' },
{ keys: 'G then C', description: 'Go to Calendar' },
{ keys: 'G then S', description: 'Go to Search' },
{ keys: 'G then A', description: 'Go to Analytics' },
],
},
{
title: 'Creation',
shortcuts: [
{ keys: 'C (on tasks page)', description: 'New task' },
{ keys: 'C (on habits page)', description: 'New habit' },
{ keys: 'C (on projects page)', description: 'New project' },
{ keys: 'C (on notes page)', description: 'New note' },
{ keys: 'C (on project detail)', description: 'New section' },
],
},
{
title: 'Entity Actions',
shortcuts: [
{ keys: 'Space (on tasks)', description: 'Open first task detail' },
{ keys: 'E', description: 'Edit focused task' },
{ keys: 'D', description: 'Delete focused task' },
{ keys: '1-4 (on tasks)', description: 'Filter kanban column (todo→cancelled)' },
],
},
];
export function ShortcutsHelp() {
const [open, setOpen] = useState(false);
const [search, setSearch] = useState('');
// Toggle with ? key
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
const target = e.target as HTMLElement;
if (
target.tagName === 'INPUT' ||
target.tagName === 'TEXTAREA' ||
target.tagName === 'SELECT' ||
target.isContentEditable
) {
return;
}
if (e.key === '?' && !e.metaKey && !e.ctrlKey && !e.altKey) {
e.preventDefault();
setOpen((prev) => !prev);
}
if (e.key === 'Escape' && open) {
setOpen(false);
}
};
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}, [open]);
const filteredGroups = useMemo(() => {
if (!search.trim()) return shortcutGroups;
const q = search.toLowerCase();
return shortcutGroups
.map((group) => ({
...group,
shortcuts: group.shortcuts.filter(
(s) =>
s.keys.toLowerCase().includes(q) ||
s.description.toLowerCase().includes(q)
),
}))
.filter((group) => group.shortcuts.length > 0);
}, [search]);
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="max-w-lg max-h-[80vh]">
<DialogHeader>
<DialogTitle>Keyboard Shortcuts</DialogTitle>
<DialogDescription>
All available keyboard shortcuts for Project E.
</DialogDescription>
</DialogHeader>
<Input
placeholder="Search shortcuts..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="mb-4"
aria-label="Search shortcuts"
/>
<ScrollArea className="flex-1 max-h-[50vh]">
{filteredGroups.length === 0 ? (
<p className="text-sm text-muted-foreground text-center py-8">
No shortcuts match your search.
</p>
) : (
filteredGroups.map((group) => (
<div key={group.title} className="mb-6">
<h3 className="text-sm font-semibold mb-2 text-muted-foreground uppercase tracking-wider">
{group.title}
</h3>
<div className="space-y-2">
{group.shortcuts.map((shortcut) => (
<div
key={shortcut.keys}
className="flex items-center justify-between text-sm"
>
<span>{shortcut.description}</span>
<kbd className="ml-4 inline-flex h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground whitespace-nowrap">
{shortcut.keys}
</kbd>
</div>
))}
</div>
</div>
))
)}
</ScrollArea>
<p className="text-xs text-muted-foreground text-center pt-2 border-t">
Press <kbd className="rounded border bg-muted px-1 font-mono">?</kbd> to toggle this overlay
</p>
</DialogContent>
</Dialog>
);
}