import { useEffect, useMemo, useRef, useState } from "react"; import { Search, CornerDownLeft, type LucideIcon } from "lucide-react"; import { cn } from "../ui"; export interface Command { id: string; label: string; icon: LucideIcon; group?: string; keywords?: string; run: () => void; } function match(c: Command, q: string): boolean { if (!q.trim()) return true; const hay = (c.label + " " + (c.keywords ?? "") + " " + (c.group ?? "")).toLowerCase(); return q .toLowerCase() .split(/\s+/) .every((t) => hay.includes(t)); } // CommandPalette 全局命令面板(⌘K):搜索 + 键盘上下选择 + Enter 执行 + Esc 关闭。 // 纯前端、两种运行模式通用,是"键盘优先工作站"的入口。 export function CommandPalette({ open, onClose, commands }: { open: boolean; onClose: () => void; commands: Command[] }) { const [q, setQ] = useState(""); const [sel, setSel] = useState(0); const inputRef = useRef(null); const listRef = useRef(null); const filtered = useMemo(() => commands.filter((c) => match(c, q)), [commands, q]); useEffect(() => { if (open) { setQ(""); setSel(0); const t = setTimeout(() => inputRef.current?.focus(), 0); return () => clearTimeout(t); } }, [open]); useEffect(() => setSel(0), [q]); useEffect(() => { listRef.current?.querySelector('[data-sel="1"]')?.scrollIntoView({ block: "nearest" }); }, [sel]); if (!open) return null; const onKey = (e: React.KeyboardEvent) => { if (e.key === "ArrowDown") { e.preventDefault(); setSel((s) => Math.min(s + 1, filtered.length - 1)); } else if (e.key === "ArrowUp") { e.preventDefault(); setSel((s) => Math.max(s - 1, 0)); } else if (e.key === "Enter") { e.preventDefault(); const c = filtered[sel]; if (c) { c.run(); onClose(); } } else if (e.key === "Escape") { onClose(); } }; return (
e.stopPropagation()}>
setQ(e.target.value)} onKeyDown={onKey} placeholder="跳转 · 搜索 · 执行动作…" className="h-11 w-full bg-transparent text-sm text-slate-200 placeholder:text-slate-600 focus:outline-none" /> esc
    {filtered.length === 0 &&
  • 无匹配命令
  • } {filtered.map((c, i) => { const Icon = c.icon; const on = i === sel; return (
  • setSel(i)} onClick={() => { c.run(); onClose(); }} className={cn("flex cursor-pointer items-center gap-3 rounded-md px-3 py-2 text-sm", on ? "bg-brand/15 text-brand-400" : "text-slate-300 hover:bg-ink-800")} > {c.label} {c.group && {c.group}} {on && }
  • ); })}
); }