"use client"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useRouter } from "next/navigation"; import { filterCommands, globalCommands, moveHighlight, projectCommands, type Command, } from "@/lib/command/palette"; import { handleTabTrap } from "@/lib/a11y/focusTrap"; // 从 pathname 抽取当前 projectId(/projects//...)。 function projectIdFromPath(pathname: string): string | null { const m = pathname.match(/^\/projects\/([^/]+)/); return m?.[1] ?? null; } interface CommandPaletteProps { pathname: string; } // 命令面板(⌘K,UX §7):键盘触发的快速导航/动作。 // 焦点管理(打开自动聚焦输入框、Esc 关闭、上下选择、回车执行)+ a11y(role=dialog/listbox)。 export function CommandPalette({ pathname }: CommandPaletteProps) { const router = useRouter(); const [open, setOpen] = useState(false); const [query, setQuery] = useState(""); const [highlight, setHighlight] = useState(0); const inputRef = useRef(null); const dialogRef = useRef(null); const projectId = projectIdFromPath(pathname); const commands = useMemo( () => projectId ? [...projectCommands(projectId), ...globalCommands()] : globalCommands(), [projectId], ); const results = useMemo( () => filterCommands(commands, query), [commands, query], ); const close = useCallback((): void => { setOpen(false); setQuery(""); setHighlight(0); }, []); const run = useCallback( (cmd: Command | undefined): void => { if (!cmd) return; close(); if (cmd.href) router.push(cmd.href); }, [close, router], ); // ⌘K / Ctrl+K 全局开关。 useEffect(() => { const onKey = (e: KeyboardEvent): void => { if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") { e.preventDefault(); setOpen((prev) => !prev); } }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, []); // 打开时聚焦输入框;切查询重置高亮。 useEffect(() => { if (open) inputRef.current?.focus(); }, [open]); useEffect(() => { setHighlight(0); }, [query]); if (!open) return null; const onInputKey = (e: React.KeyboardEvent): void => { if (e.key === "Escape") { e.preventDefault(); close(); } else if (e.key === "ArrowDown") { e.preventDefault(); setHighlight((h) => moveHighlight(h, 1, results.length)); } else if (e.key === "ArrowUp") { e.preventDefault(); setHighlight((h) => moveHighlight(h, -1, results.length)); } else if (e.key === "Enter") { e.preventDefault(); run(results[highlight]); } }; return (
e.stopPropagation()} onKeyDown={(e) => { // focus trap:Tab/Shift+Tab 在对话框内循环,不逃逸到背景(WCAG 2.1.2)。 if (dialogRef.current) handleTabTrap(dialogRef.current, e); }} > setQuery(e.target.value)} onKeyDown={onInputKey} placeholder="搜索命令或页面…(写本章 / 审稿 / 生成角色 / 跳转伏笔 / 搜设定)" aria-label="命令搜索" aria-controls="command-list" className="w-full border-b border-line bg-bg px-4 py-3 text-sm text-ink outline-none" />
    {results.length === 0 ? (
  • 无匹配命令
  • ) : ( results.map((cmd, i) => (
  • setHighlight(i)} onClick={() => run(cmd)} className={`flex cursor-pointer items-center justify-between px-4 py-2 text-sm ${ i === highlight ? "bg-[var(--color-cinnabar-wash)] text-cinnabar" : "text-ink" }`} > {cmd.title} {cmd.group}
  • )) )}
); }