PB05▸🪟 Overlay · 覆蓋層▸命令
Command Palette
命令面板
難度 ★★★★★標籤 ⌘K · 命令 · 搜尋4 min read
模式簡述
Command Palette:⌘K 召喚的全局搜尋與命令面板。Linear、Raycast、VS Code 都有。所有功能在一個輸入框內可達。
Claude Code Prompt
參考:PB05 command-palette 組件:CommandPalette.tsx Command Palette:⌘K 召喚的全局搜尋與命令面板。Linear、Raycast、VS Code 都有。所有功能在一個輸入框內可達。 限制: - 模糊搜尋分數沒調好 - 沒處理 esc 兩次的行為 - 沒做 recent commands
完整程式碼
"use client";
import { useEffect, useState } from "react";
import { Search, ArrowRight } from "lucide-react";
type State = "closed" | "open" | "filtered" | "no-results";
const COMMANDS = [
{ icon: "📁", title: "新增專案", shortcut: "N" },
{ icon: "🔍", title: "搜尋檔案", shortcut: "F" },
{ icon: "⚙️", title: "開啟設定", shortcut: "," },
{ icon: "🎨", title: "切換主題", shortcut: "T" },
{ icon: "🚀", title: "Deploy 到 production", shortcut: "D" },
];
export function CommandPalette({ state = "open" }: { state?: State }) {
const [open, setOpen] = useState(state !== "closed");
const [query, setQuery] = useState("");
useEffect(() => {
setOpen(state !== "closed");
if (state === "filtered") setQuery("deploy");
else if (state === "no-results") setQuery("xyzzy");
else setQuery("");
}, [state]);
const filtered = query
? COMMANDS.filter((c) => c.title.toLowerCase().includes(query.toLowerCase()))
: COMMANDS;
return (
<div className="relative grid h-[320px] w-full max-w-md place-items-center rounded-lg border border-border bg-background/30">
{!open && (
<button
onClick={() => setOpen(true)}
className="rounded-md border border-border bg-background/50 px-3 py-1.5 font-mono text-xs hover:border-lab-accent"
>
⌘K 開啟
</button>
)}
{open && (
<>
<div onClick={() => setOpen(false)} className="absolute inset-0 bg-black/60 backdrop-blur-sm" />
<div className="absolute left-1/2 top-8 w-[88%] -translate-x-1/2 overflow-hidden rounded-xl border border-border bg-background shadow-2xl animate-in fade-in zoom-in-95 duration-150">
<div className="flex items-center gap-2 border-b border-border px-3 py-2.5">
<Search className="h-4 w-4 text-muted-foreground" />
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="輸入命令⋯"
className="flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground"
autoFocus
/>
<kbd className="rounded border border-border bg-background/50 px-1.5 py-0.5 font-mono text-[10px] text-muted-foreground">
ESC
</kbd>
</div>
<div className="max-h-56 overflow-y-auto p-1">
{filtered.length === 0 ? (
<p className="py-6 text-center font-mono text-xs text-muted-foreground">
找不到命令「{query}」
</p>
) : (
filtered.map((c, i) => (
<button
key={i}
className="flex w-full items-center gap-2 rounded-md px-2 py-2 text-left text-sm hover:bg-lab-accent/10"
>
<span>{c.icon}</span>
<span className="flex-1">{c.title}</span>
<kbd className="font-mono text-[10px] text-muted-foreground">⌘{c.shortcut}</kbd>
<ArrowRight className="h-3 w-3 text-muted-foreground/40" />
</button>
))
)}
</div>
</div>
</>
)}
</div>
);
}何時用
- ✅ 功能多到藏在五六層選單裡
- ✅ 進階使用者需要鍵盤導航全站
- ✅ 想模仿 IDE 體驗的工具型產品
何時別用
- ❌ 一般大眾用的消費型 app(多數人不會用 ⌘K)
- ❌ 功能少於 10 個(直接做按鈕)
- ❌ 搜尋資料而非執行命令(用一般搜尋框)
我踩過的坑
-
模糊搜尋分數沒調好:使用者輸入 ‘pay’ 但 ‘playlist’ 排在 ‘payment’ 前面。Fuse 的 threshold 要降到 0.3 以下。
-
沒處理 esc 兩次的行為:第一次 esc 應該清空輸入、第二次才關面板。
-
沒做 recent commands:使用者 ⌘K 打開後不知道從哪開始。第一頁顯示最近 5 個用過的命令。
相關模式
- PA01
- PB07