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": [
|
"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"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -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
|
||||||
|
|||||||
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