feat(keyboard-shortcuts): Cmd/Ctrl-K Command-Palette für Navigation [tsc:fail]
This commit is contained in:
parent
276ed8c798
commit
8cf2f8ca29
@ -2,9 +2,10 @@
|
||||
"completed_features": [
|
||||
"password-change"
|
||||
],
|
||||
"current_feature": "calendar-week-view",
|
||||
"current_feature": "keyboard-shortcuts",
|
||||
"started_at": "2026-05-23T05:30:16.203066",
|
||||
"attempted_features": [
|
||||
"audit-log"
|
||||
"audit-log",
|
||||
"calendar-week-view"
|
||||
]
|
||||
}
|
||||
@ -692,3 +692,17 @@ src/routes/audit-log.ts(3,10): error TS2724: '"../db/schema"' has no exported me
|
||||
undefined
|
||||
/home/dark/Developer/EmberClone/apps/api:
|
||||
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
|
||||
|
||||
139
apps/web/src/components/CommandPalette.tsx
Normal file
139
apps/web/src/components/CommandPalette.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user