feat(keyboard-shortcuts): Cmd/Ctrl-K Command-Palette für Navigation [tsc:fail]

This commit is contained in:
Dennis (via Claude+Gemma) 2026-05-23 05:34:31 +02:00
parent 276ed8c798
commit 8cf2f8ca29
3 changed files with 156 additions and 2 deletions

View File

@ -2,9 +2,10 @@
"completed_features": [ "completed_features": [
"password-change" "password-change"
], ],
"current_feature": "calendar-week-view", "current_feature": "keyboard-shortcuts",
"started_at": "2026-05-23T05:30:16.203066", "started_at": "2026-05-23T05:30:16.203066",
"attempted_features": [ "attempted_features": [
"audit-log" "audit-log",
"calendar-week-view"
] ]
} }

View File

@ -692,3 +692,17 @@ src/routes/audit-log.ts(3,10): error TS2724: '"../db/schema"' has no exported me
undefined undefined
/home/dark/Developer/EmberClone/apps/api: /home/dark/Developer/EmberClone/apps/api:
ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL Command failed with exit code 2: tsc --noEmit -p tsconfig.json ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL Command failed with exit code 2: tsc --noEmit -p tsconfig.json
- `05:33:40` **INFO** Committed feature calendar-week-view
- `05:33:40` **INFO** Pushed: rc=0
## Phase-3 Feature: keyboard-shortcuts (2026-05-23 05:33:40)
- `05:33:40` **INFO** Description: Cmd/Ctrl-K Command-Palette für Navigation
- `05:33:40` **INFO** Generating apps/web/src/components/CommandPalette.tsx (Command-Palette Modal. Trigger: Cmd/Ctrl+K via window-keydown. Zeigt L…)
- `05:34:30` **INFO** wrote 5704 chars in 49.9s (attempt 1)
- `05:34:30` **INFO** Running tsc --noEmit on api…
- `05:34:31` **WARN** tsc errors:
src/routes/audit-log.ts(3,10): error TS2724: '"../db/schema"' has no exported member named 'auditLogs'. Did you mean 'auditLog'?
undefined
/home/dark/Developer/EmberClone/apps/api:
ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL Command failed with exit code 2: tsc --noEmit -p tsconfig.json

View File

@ -0,0 +1,139 @@
import React, { useState, useEffect, useMemo } from 'react';
import { useNavigate } from '@tanstack/react-router';
import { Search, Command, X } from 'lucide-react';
interface CommandItem {
id: string;
label: string;
path: string;
icon: React.ReactNode;
category: string;
}
const COMMAND_ITEMS: CommandItem[] = [
{ id: 'dash', label: 'Dashboard', path: '/', icon: <span className="text-blue-500">📊</span>, category: 'General' },
{ id: 'time', label: 'Time Entries', path: '/time-entries', icon: <span className="text-green-500"></span>, category: 'Tracking' },
{ id: 'cust', label: 'Customers', path: '/customers', icon: <span className="text-purple-500">👥</span>, category: 'CRM' },
{ id: 'proj', label: 'Projects', path: '/projects', icon: <span className="text-orange-500">📁</span>, category: 'CRM' },
{ id: 'cal', label: 'Calendar', path: '/calendar', icon: <span className="text-red-500">📅</span>, category: 'General' },
{ id: 'sett', label: 'Settings', path: '/settings', icon: <span className="text-gray-500"></span>, category: 'System' },
{ id: 'prof', label: 'Profile', path: '/profile', icon: <span className="text-indigo-500">👤</span>, category: 'System' },
];
export default function CommandPalette() {
const [isOpen, setIsOpen] = useState(false);
const [query, setQuery] = useState('');
const [selectedIndex, setSelectedIndex] = useState(0);
const navigate = useNavigate();
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
e.preventDefault();
setIsOpen((prev) => !prev);
}
if (e.key === 'Escape' && isOpen) {
setIsOpen(false);
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [isOpen]);
const filteredItems = useMemo(() => {
return COMMAND_ITEMS.filter((item) =>
item.label.toLowerCase().includes(query.toLowerCase())
);
}, [query]);
useEffect(() => {
setSelectedIndex(0);
}, [query]);
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'ArrowDown') {
e.preventDefault();
setSelectedIndex((prev) => (prev + 1) % filteredItems.length);
} else if (e.key === 'ArrowUp') {
e.preventDefault();
setSelectedIndex((prev) => (prev - 1 + filteredItems.length) % filteredItems.length);
} else if (e.key === 'Enter' && filteredItems[selectedIndex]) {
const item = filteredItems[selectedIndex];
navigate({ to: item.path });
setIsOpen(false);
setQuery('');
}
};
if (!isOpen) return null;
return (
<div
className="fixed inset-0 z-50 flex items-start justify-center pt-[15vh] bg-black/50 backdrop-blur-sm"
onClick={() => setIsOpen(false)}
>
<div
className="w-full max-w-xl bg-white dark:bg-zinc-900 rounded-xl shadow-2xl border border-zinc-200 dark:border-zinc-800 overflow-hidden"
onClick={(e) => e.stopPropagation()}
onKeyDown={handleKeyDown}
>
<div className="flex items-center px-4 py-3 border-b border-zinc-200 dark:border-zinc-800">
<Search className="w-5 h-5 text-zinc-400 mr-3" />
<input
autoFocus
className="flex-1 bg-transparent border-none outline-none text-zinc-900 dark:text-zinc-100 placeholder-zinc-500"
placeholder="Search commands..."
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<div className="flex items-center gap-1 px-1.5 py-0.5 rounded border border-zinc-200 dark:border-zinc-700 bg-zinc-50 dark:bg-zinc-800 text-[10px] font-medium text-zinc-500">
<Command className="w-3 h-3" /> K
</div>
</div>
<div className="max-h-[60vh] overflow-y-auto p-2">
{filteredItems.length > 0 ? (
<div className="space-y-1">
{filteredItems.map((item, index) => (
<div
key={item.id}
className={`flex items-center justify-between px-3 py-2 rounded-lg cursor-pointer transition-colors ${
index === selectedIndex
? 'bg-zinc-100 dark:bg-zinc-800 text-zinc-900 dark:text-zinc-100'
: 'text-zinc-600 dark:text-zinc-400 hover:bg-zinc-50 dark:hover:bg-zinc-800/50'
}`}
onClick={() => {
navigate({ to: item.path });
setIsOpen(false);
setQuery('');
}}
>
<div className="flex items-center gap-3">
<span className="text-xl">{item.icon}</span>
<span className="font-medium">{item.label}</span>
</div>
<span className="text-xs opacity-50">{item.category}</span>
</div>
))}
</div>
) : (
<div className="py-8 text-center text-zinc-500 text-sm">
No results found for "{query}"
</div>
)}
</div>
<div className="px-4 py-2 bg-zinc-50 dark:bg-zinc-800/50 border-t border-zinc-200 dark:border-zinc-800 flex justify-between items-center">
<span className="text-[10px] text-zinc-400 uppercase tracking-wider font-semibold">Navigation</span>
<button
onClick={() => setIsOpen(false)}
className="p-1 hover:bg-zinc-200 dark:hover:bg-zinc-700 rounded transition-colors"
>
<X className="w-4 h-4 text-zinc-500" />
</button>
</div>
</div>
</div>
);
}