asdf98 commited on
Commit
6313940
·
verified ·
1 Parent(s): 16aca1c

feat: Phase 4 - Command Palette (Ctrl+K) for quick navigation and actions

Browse files
Files changed (1) hide show
  1. src/components/CommandPalette.tsx +69 -0
src/components/CommandPalette.tsx ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { motion, AnimatePresence } from 'motion/react';
2
+ import { Search, Home, Folder, LayoutGrid, Download, ListOrdered, Settings, Timer, Palette, Globe } from 'lucide-react';
3
+ import { useState, useEffect, useRef } from 'react';
4
+ import { cn } from '../../lib/utils';
5
+
6
+ interface CommandItem { id: string; label: string; icon: React.ReactNode; action: () => void; section?: string; keywords?: string; }
7
+
8
+ export function CommandPalette({ open, onClose, onNavigate }: { open: boolean; onClose: () => void; onNavigate: (view: string) => void }) {
9
+ const [query, setQuery] = useState('');
10
+ const [selectedIdx, setSelectedIdx] = useState(0);
11
+ const inputRef = useRef<HTMLInputElement>(null);
12
+
13
+ const commands: CommandItem[] = [
14
+ { id: 'home', label: 'Go to Home', icon: <Home className="w-4 h-4" />, action: () => onNavigate('home'), section: 'Navigate', keywords: 'home dashboard start' },
15
+ { id: 'web', label: 'Open Browser', icon: <Globe className="w-4 h-4" />, action: () => onNavigate('web'), section: 'Navigate', keywords: 'browser web tab browse' },
16
+ { id: 'library', label: 'Open Library', icon: <Folder className="w-4 h-4" />, action: () => onNavigate('library'), section: 'Navigate', keywords: 'library references images' },
17
+ { id: 'board', label: 'Open Board', icon: <LayoutGrid className="w-4 h-4" />, action: () => onNavigate('board'), section: 'Navigate', keywords: 'board canvas pureref reference' },
18
+ { id: 'study', label: 'Quick Study Mode', icon: <Timer className="w-4 h-4" />, action: () => onNavigate('study'), section: 'Navigate', keywords: 'study timer practice draw' },
19
+ { id: 'downloads', label: 'Downloads', icon: <Download className="w-4 h-4" />, action: () => onNavigate('downloads'), section: 'Navigate', keywords: 'downloads files' },
20
+ { id: 'sessions', label: 'Sessions', icon: <ListOrdered className="w-4 h-4" />, action: () => onNavigate('sessions'), section: 'Navigate', keywords: 'sessions tabs workspace' },
21
+ { id: 'settings', label: 'Settings', icon: <Settings className="w-4 h-4" />, action: () => onNavigate('settings'), section: 'Navigate', keywords: 'settings preferences theme' },
22
+ { id: 'palette', label: 'Color Tools', icon: <Palette className="w-4 h-4" />, action: () => onNavigate('colors'), section: 'Tools', keywords: 'color palette export extract' },
23
+ ];
24
+
25
+ const filtered = query.trim() ? commands.filter(c => c.label.toLowerCase().includes(query.toLowerCase()) || c.keywords?.toLowerCase().includes(query.toLowerCase())) : commands;
26
+
27
+ useEffect(() => { if (open) { setQuery(''); setSelectedIdx(0); setTimeout(() => inputRef.current?.focus(), 50); } }, [open]);
28
+ useEffect(() => { setSelectedIdx(0); }, [query]);
29
+
30
+ useEffect(() => {
31
+ if (!open) return;
32
+ const handler = (e: KeyboardEvent) => {
33
+ if (e.key === 'Escape') { onClose(); return; }
34
+ if (e.key === 'ArrowDown') { e.preventDefault(); setSelectedIdx(i => Math.min(i + 1, filtered.length - 1)); }
35
+ if (e.key === 'ArrowUp') { e.preventDefault(); setSelectedIdx(i => Math.max(i - 1, 0)); }
36
+ if (e.key === 'Enter' && filtered[selectedIdx]) { filtered[selectedIdx].action(); onClose(); }
37
+ };
38
+ window.addEventListener('keydown', handler);
39
+ return () => window.removeEventListener('keydown', handler);
40
+ }, [open, filtered, selectedIdx, onClose]);
41
+
42
+ if (!open) return null;
43
+
44
+ return (
45
+ <AnimatePresence>
46
+ <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} className="fixed inset-0 z-[200] flex items-start justify-center pt-[15vh] bg-black/40 backdrop-blur-sm" onClick={onClose}>
47
+ <motion.div initial={{ opacity: 0, y: -20, scale: 0.95 }} animate={{ opacity: 1, y: 0, scale: 1 }} exit={{ opacity: 0, y: -20, scale: 0.95 }} transition={{ duration: 0.15 }} onClick={e => e.stopPropagation()} className="w-[520px] bg-dusk-surface border border-dusk-border rounded-2xl shadow-2xl overflow-hidden">
48
+ {/* Search Input */}
49
+ <div className="flex items-center gap-3 px-5 h-14 border-b border-dusk-border/60">
50
+ <Search className="w-5 h-5 text-dusk-text-muted shrink-0" />
51
+ <input ref={inputRef} value={query} onChange={e => setQuery(e.target.value)} placeholder="Type a command or search..." className="flex-1 bg-transparent text-[15px] text-dusk-text placeholder:text-dusk-text-muted/60 outline-none" autoFocus />
52
+ <kbd className="text-[10px] font-bold text-dusk-text-muted bg-dusk-bg px-2 py-0.5 rounded border border-dusk-border">ESC</kbd>
53
+ </div>
54
+ {/* Results */}
55
+ <div className="max-h-[360px] overflow-auto py-2">
56
+ {filtered.length === 0 && <div className="text-center text-dusk-text-muted py-8 text-sm">No matching commands</div>}
57
+ {filtered.map((cmd, idx) => (
58
+ <button key={cmd.id} onClick={() => { cmd.action(); onClose(); }} onMouseEnter={() => setSelectedIdx(idx)} className={cn('w-full flex items-center gap-3 px-5 py-2.5 text-left transition-colors', idx === selectedIdx ? 'bg-dusk-accent/10 text-dusk-accent' : 'text-dusk-text hover:bg-dusk-surface-hover')}>
59
+ <span className={cn('shrink-0', idx === selectedIdx ? 'text-dusk-accent' : 'text-dusk-text-muted')}>{cmd.icon}</span>
60
+ <span className="text-[14px] font-medium">{cmd.label}</span>
61
+ {cmd.section && <span className="ml-auto text-[11px] text-dusk-text-muted">{cmd.section}</span>}
62
+ </button>
63
+ ))}
64
+ </div>
65
+ </motion.div>
66
+ </motion.div>
67
+ </AnimatePresence>
68
+ );
69
+ }