aboutsummaryrefslogtreecommitdiff
path: root/src/components/CommandPalette
diff options
context:
space:
mode:
authorzwlucas <lucas.fariamo08@gmail.com>2026-06-05 20:52:54 +0000
committerzwlucas <lucas.fariamo08@gmail.com>2026-06-05 20:52:54 +0000
commit09f964451d7d92e9891430ec4595c1276d486aab (patch)
tree28da4483f5c28924a8c47fceb648b1baebe88224 /src/components/CommandPalette
downloadyace-09f964451d7d92e9891430ec4595c1276d486aab.tar.gz
yace-09f964451d7d92e9891430ec4595c1276d486aab.zip
feat: uploadHEADmaster
Signed-off-by: zwlucas <lucas.fariamo08@gmail.com>
Diffstat (limited to 'src/components/CommandPalette')
-rw-r--r--src/components/CommandPalette/CommandPalette.tsx201
1 files changed, 201 insertions, 0 deletions
diff --git a/src/components/CommandPalette/CommandPalette.tsx b/src/components/CommandPalette/CommandPalette.tsx
new file mode 100644
index 0000000..eba6221
--- /dev/null
+++ b/src/components/CommandPalette/CommandPalette.tsx
@@ -0,0 +1,201 @@
+import { useEffect, useRef, useMemo } from 'react'
+import { useCommandStore } from '@/stores/useCommandStore'
+
+function SearchIcon() {
+ return (
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
+ <circle cx="11" cy="11" r="8" />
+ <line x1="21" y1="21" x2="16.65" y2="16.65" />
+ </svg>
+ )
+}
+
+function Kbd({ keys }: { keys: string }) {
+ return (
+ <span className="flex items-center gap-0.5 text-ui-xs">
+ {keys.split('+').map((k) => (
+ <kbd
+ key={k}
+ className="px-1 py-[1px] rounded text-[10px] leading-none font-mono"
+ style={{
+ backgroundColor: 'var(--color-panel-hover)',
+ color: 'var(--color-text-muted)',
+ border: '1px solid var(--color-panel-border)',
+ minWidth: '16px',
+ textAlign: 'center',
+ }}
+ >
+ {k === 'Ctrl' ? '⌃' : k === 'Shift' ? '⇧' : k === 'Cmd' ? '⌘' : k}
+ </kbd>
+ ))}
+ </span>
+ )
+}
+
+function CommandGroup({ category, children }: { category: string; children: React.ReactNode }) {
+ return (
+ <div>
+ <div className="px-4 py-1.5 text-ui-xs font-medium tracking-wider uppercase text-text-muted">{category}</div>
+ {children}
+ </div>
+ )
+}
+
+export default function CommandPalette() {
+ const isOpen = useCommandStore((s) => s.isOpen)
+ const query = useCommandStore((s) => s.query)
+ const selectedIndex = useCommandStore((s) => s.selectedIndex)
+ const commands = useCommandStore((s) => s.commands)
+ const setQuery = useCommandStore((s) => s.setQuery)
+ const setSelectedIndex = useCommandStore((s) => s.setSelectedIndex)
+ const executeSelected = useCommandStore((s) => s.executeSelected)
+ const close = useCommandStore((s) => s.close)
+
+ const inputRef = useRef<HTMLInputElement>(null)
+
+ const filtered = useMemo(
+ () => commands.filter(
+ (c) =>
+ c.label.toLowerCase().includes(query.toLowerCase()) ||
+ c.category.toLowerCase().includes(query.toLowerCase())
+ ),
+ [commands, query]
+ )
+
+ const grouped = useMemo(() => {
+ const map = new Map<string, typeof filtered>()
+ filtered.forEach((c) => {
+ const group = map.get(c.category) ?? []
+ group.push(c)
+ map.set(c.category, group)
+ })
+ return Array.from(map.entries())
+ }, [filtered])
+
+ useEffect(() => {
+ if (isOpen) {
+ inputRef.current?.focus()
+ }
+ }, [isOpen])
+
+ useEffect(() => {
+ if (!isOpen) return
+
+ function handleKeyDown(e: KeyboardEvent) {
+ if (e.key === 'Escape') {
+ e.preventDefault()
+ close()
+ return
+ }
+
+ if (e.key === 'Enter') {
+ e.preventDefault()
+ executeSelected()
+ return
+ }
+
+ if (e.key === 'ArrowDown') {
+ e.preventDefault()
+ setSelectedIndex(Math.min(selectedIndex + 1, filtered.length - 1))
+ return
+ }
+
+ if (e.key === 'ArrowUp') {
+ e.preventDefault()
+ setSelectedIndex(Math.max(selectedIndex - 1, 0))
+ return
+ }
+ }
+
+ window.addEventListener('keydown', handleKeyDown)
+ return () => window.removeEventListener('keydown', handleKeyDown)
+ }, [isOpen, filtered.length, selectedIndex, setSelectedIndex, executeSelected, close])
+
+ if (!isOpen) return null
+
+ return (
+ <div
+ className="fixed inset-0 z-50 flex justify-center"
+ style={{ paddingTop: '12vh' }}
+ onClick={close}
+ role="dialog"
+ aria-label="Command palette"
+ >
+ <div className="absolute inset-0 backdrop-glass" />
+
+ <div
+ className="relative w-full max-w-[520px] rounded-xl shadow-glass overflow-hidden"
+ style={{
+ backgroundColor: 'var(--color-panel-bg)',
+ border: '1px solid var(--color-panel-border)',
+ maxHeight: '60vh',
+ display: 'flex',
+ flexDirection: 'column',
+ }}
+ onClick={(e) => e.stopPropagation()}
+ >
+ <div className="flex items-center gap-3 px-4 h-11 shrink-0" style={{ borderBottom: '1px solid var(--color-panel-border)' }}>
+ <span className="text-text-muted shrink-0"><SearchIcon /></span>
+ <input
+ ref={inputRef}
+ type="text"
+ value={query}
+ onChange={(e) => setQuery(e.target.value)}
+ placeholder="Type a command..."
+ className="flex-1 bg-transparent text-ui-base text-text-primary outline-none placeholder:text-text-muted"
+ />
+ </div>
+
+ <div className="flex-1 overflow-y-auto py-2" role="listbox">
+ {filtered.length === 0 && (
+ <div className="px-4 py-6 text-ui-sm text-text-muted text-center">
+ No results found
+ </div>
+ )}
+
+ {grouped.map(([category, items]) => (
+ <CommandGroup key={category} category={category}>
+ {items.map((cmd) => {
+ const idx = filtered.indexOf(cmd)
+ const isSelected = idx === selectedIndex
+
+ return (
+ <button
+ key={cmd.id}
+ role="option"
+ aria-selected={isSelected ? 'true' : 'false'}
+ className="w-full flex items-center justify-between px-4 py-1.5 text-left transition-colors duration-fast"
+ style={{
+ backgroundColor: isSelected ? 'var(--color-panel-hover)' : 'transparent',
+ color: isSelected ? 'var(--color-text-primary)' : 'var(--color-text-secondary)',
+ }}
+ onMouseEnter={() => setSelectedIndex(idx)}
+ onClick={() => {
+ cmd.action()
+ close()
+ }}
+ >
+ <span className="text-ui-sm truncate">{cmd.label}</span>
+ {cmd.shortcut && <Kbd keys={cmd.shortcut} />}
+ </button>
+ )
+ })}
+ </CommandGroup>
+ ))}
+
+ <div className="px-4 pt-2 pb-1 flex items-center gap-3 text-ui-xs text-text-muted border-t border-panel-border mt-2">
+ <span className="flex items-center gap-1">
+ <Kbd keys="↑↓" /> navigate
+ </span>
+ <span className="flex items-center gap-1">
+ <Kbd keys="↵" /> select
+ </span>
+ <span className="flex items-center gap-1">
+ <Kbd keys="Esc" /> close
+ </span>
+ </div>
+ </div>
+ </div>
+ </div>
+ )
+}