Frontend Lab
← 回到所有模式
PB05🪟 Overlay · 覆蓋層命令

Command Palette

命令面板

難度 ★★★★標籤 ⌘K · 命令 · 搜尋4 min read

模式簡述

Command Palette:⌘K 召喚的全局搜尋與命令面板。Linear、Raycast、VS Code 都有。所有功能在一個輸入框內可達。

關閉|預設關閉

Claude Code Prompt

💬 Claude Code Prompt
參考:PB05 command-palette
組件:CommandPalette.tsx

Command Palette:⌘K 召喚的全局搜尋與命令面板。Linear、Raycast、VS Code 都有。所有功能在一個輸入框內可達。

限制:
- 模糊搜尋分數沒調好
- 沒處理 esc 兩次的行為
- 沒做 recent commands

完整程式碼

CommandPalette.tsx
"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 個(直接做按鈕)
  • ❌ 搜尋資料而非執行命令(用一般搜尋框)

我踩過的坑

  1. 模糊搜尋分數沒調好:使用者輸入 ‘pay’ 但 ‘playlist’ 排在 ‘payment’ 前面。Fuse 的 threshold 要降到 0.3 以下。

  2. 沒處理 esc 兩次的行為:第一次 esc 應該清空輸入、第二次才關面板。

  3. 沒做 recent commands:使用者 ⌘K 打開後不知道從哪開始。第一頁顯示最近 5 個用過的命令。

相關模式

  • PA01
  • PB07

搜尋

按 ⌘K 隨時開啟