import { useState } from "react";
import { CommandPalette, Button } from "@cloudflare/kumo";
export function CommandPaletteBasicDemo() {
const [open, setOpen] = useState(false);
const [search, setSearch] = useState("");
const [selectedItem, setSelectedItem] = useState<string | null>(null);
const handleSelect = (item: CommandItem) => {
setSelectedItem(item.title);
setOpen(false);
setSearch("");
};
// Filter groups based on search
const filteredGroups = filterGroupsWithItems(sampleGroups, search);
return (
<div className="flex flex-col items-start gap-4">
<Button onClick={() => setOpen(true)}>Open Command Palette</Button>
{selectedItem && (
<p className="text-sm text-kumo-subtle">
Last selected:{" "}
<span className="text-kumo-default">{selectedItem}</span>
</p>
)}
<CommandPalette.Root
open={open}
onOpenChange={setOpen}
items={filteredGroups}
value={search}
onValueChange={setSearch}
itemToStringValue={(group) => group.label}
onSelect={(item, { newTab }) => {
console.log("Selected:", item.title, newTab ? "(new tab)" : "");
handleSelect(item);
}}
getSelectableItems={getSelectableItems}
>
<CommandPalette.Input placeholder="Type a command or search..." />
<CommandPalette.List>
<CommandPalette.Results>
{(group: CommandGroup) => (
<CommandPalette.Group key={group.id} items={group.items}>
<CommandPalette.GroupLabel>
{group.label}
</CommandPalette.GroupLabel>
<CommandPalette.Items>
{(item: CommandItem) => (
<CommandPalette.Item
key={item.id}
value={item}
onClick={() => handleSelect(item)}
>
<span className="flex items-center gap-3">
{item.icon && (
<span className="text-kumo-subtle">{item.icon}</span>
)}
<span>{item.title}</span>
</span>
</CommandPalette.Item>
)}
</CommandPalette.Items>
</CommandPalette.Group>
)}
</CommandPalette.Results>
<CommandPalette.Empty>No commands found</CommandPalette.Empty>
</CommandPalette.List>
<CommandPalette.Footer>
<span className="flex items-center gap-2">
<kbd className="rounded border border-kumo-hairline bg-kumo-base px-1.5 py-0.5 text-[10px]">
↑↓
</kbd>
<span>Navigate</span>
</span>
<span className="flex items-center gap-2">
<kbd className="rounded border border-kumo-hairline bg-kumo-base px-1.5 py-0.5 text-[10px]">
↵
</kbd>
<span>Select</span>
</span>
</CommandPalette.Footer>
</CommandPalette.Root>
</div>
);
}安装
批量导入
import { CommandPalette } from "@cloudflare/kumo";按需导入
import { CommandPalette } from "@cloudflare/kumo/components/command-palette";用法
CommandPalette 是一个复合组件,构建于 Base UI 的 Autocomplete 原语之上。它为命令面板界面提供无障碍键盘导航和可自定义的样式。
import { useState } from "react";
import { CommandPalette } from "@cloudflare/kumo";
interface Item {
id: string;
title: string;
}
const items: Item[] = [
{ id: "1", title: "Create Project" },
{ id: "2", title: "Open Settings" },
];
export default function Example() {
const [open, setOpen] = useState(false);
const [search, setSearch] = useState("");
return (
<>
<button onClick={() => setOpen(true)}>Open</button>
<CommandPalette.Root
open={open}
onOpenChange={setOpen}
items={items}
value={search}
onValueChange={setSearch}
itemToStringValue={(item) => item.title}
getSelectableItems={(items) => items}
>
<CommandPalette.Input placeholder="Search..." />
<CommandPalette.List>
<CommandPalette.Results>
{(item) => (
<CommandPalette.Item
key={item.id}
value={item}
onClick={() => setOpen(false)}
>
{item.title}
</CommandPalette.Item>
)}
</CommandPalette.Results>
<CommandPalette.Empty>No results</CommandPalette.Empty>
</CommandPalette.List>
</CommandPalette.Root>
</>
);
}键盘导航
内置键盘导航会自动生效:
↑+↓在条目之间移动高亮
Enter选中高亮的条目
⌘/Ctrl+Enter以 newTab: true 方式选中
Escape关闭对话框
示例
分组条目
使用标签将相关命令分组在一起。
import { useState } from "react";
import { CommandPalette, Button } from "@cloudflare/kumo";
export function CommandPaletteBasicDemo() {
const [open, setOpen] = useState(false);
const [search, setSearch] = useState("");
const [selectedItem, setSelectedItem] = useState<string | null>(null);
const handleSelect = (item: CommandItem) => {
setSelectedItem(item.title);
setOpen(false);
setSearch("");
};
// Filter groups based on search
const filteredGroups = filterGroupsWithItems(sampleGroups, search);
return (
<div className="flex flex-col items-start gap-4">
<Button onClick={() => setOpen(true)}>Open Command Palette</Button>
{selectedItem && (
<p className="text-sm text-kumo-subtle">
Last selected:{" "}
<span className="text-kumo-default">{selectedItem}</span>
</p>
)}
<CommandPalette.Root
open={open}
onOpenChange={setOpen}
items={filteredGroups}
value={search}
onValueChange={setSearch}
itemToStringValue={(group) => group.label}
onSelect={(item, { newTab }) => {
console.log("Selected:", item.title, newTab ? "(new tab)" : "");
handleSelect(item);
}}
getSelectableItems={getSelectableItems}
>
<CommandPalette.Input placeholder="Type a command or search..." />
<CommandPalette.List>
<CommandPalette.Results>
{(group: CommandGroup) => (
<CommandPalette.Group key={group.id} items={group.items}>
<CommandPalette.GroupLabel>
{group.label}
</CommandPalette.GroupLabel>
<CommandPalette.Items>
{(item: CommandItem) => (
<CommandPalette.Item
key={item.id}
value={item}
onClick={() => handleSelect(item)}
>
<span className="flex items-center gap-3">
{item.icon && (
<span className="text-kumo-subtle">{item.icon}</span>
)}
<span>{item.title}</span>
</span>
</CommandPalette.Item>
)}
</CommandPalette.Items>
</CommandPalette.Group>
)}
</CommandPalette.Results>
<CommandPalette.Empty>No commands found</CommandPalette.Empty>
</CommandPalette.List>
<CommandPalette.Footer>
<span className="flex items-center gap-2">
<kbd className="rounded border border-kumo-hairline bg-kumo-base px-1.5 py-0.5 text-[10px]">
↑↓
</kbd>
<span>Navigate</span>
</span>
<span className="flex items-center gap-2">
<kbd className="rounded border border-kumo-hairline bg-kumo-base px-1.5 py-0.5 text-[10px]">
↵
</kbd>
<span>Select</span>
</span>
</CommandPalette.Footer>
</CommandPalette.Root>
</div>
);
}简单平铺列表
对于更简单的场景,使用不带分组的平铺条目数组。
import { useState } from "react";
import { CommandPalette, Button } from "@cloudflare/kumo";
export function CommandPaletteSimpleDemo() {
const [open, setOpen] = useState(false);
const [search, setSearch] = useState("");
return (
<div>
<Button onClick={() => setOpen(true)}>Open Simple Palette</Button>
<CommandPalette.Root
open={open}
onOpenChange={setOpen}
items={simpleItems}
value={search}
onValueChange={setSearch}
itemToStringValue={(item) => item.title}
onSelect={(item) => {
console.log("Selected:", item.title);
setOpen(false);
}}
getSelectableItems={(items) => items}
>
<CommandPalette.Input placeholder="Search actions..." />
<CommandPalette.List>
<CommandPalette.Results>
{(item: SimpleItem) => (
<CommandPalette.Item
key={item.id}
value={item}
onClick={() => {
console.log("Clicked:", item.title);
setOpen(false);
}}
>
{item.title}
</CommandPalette.Item>
)}
</CommandPalette.Results>
<CommandPalette.Empty>No actions found</CommandPalette.Empty>
</CommandPalette.List>
</CommandPalette.Root>
</div>
);
}加载状态
在获取结果时显示加载微调器。
import { useState } from "react";
import { CommandPalette, Button } from "@cloudflare/kumo";
// With loading state
export function CommandPaletteLoadingDemo() {
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
const [search, setSearch] = useState("");
const handleOpen = () => {
setOpen(true);
setLoading(true);
// Simulate loading
setTimeout(() => setLoading(false), 1500);
};
// Filter groups based on search
const filteredGroups = filterGroupsWithItems(sampleGroups, search);
return (
<div>
<Button onClick={handleOpen}>Open with Loading</Button>
<CommandPalette.Root
open={open}
onOpenChange={setOpen}
items={loading ? [] : filteredGroups}
value={search}
onValueChange={setSearch}
itemToStringValue={(group) => group.label}
getSelectableItems={getSelectableItems}
>
<CommandPalette.Input placeholder="Search..." />
<CommandPalette.List>
{loading ? (
<CommandPalette.Loading />
) : (
<>
<CommandPalette.Results>
{(group: CommandGroup) => (
<CommandPalette.Group key={group.id} items={group.items}>
<CommandPalette.GroupLabel>
{group.label}
</CommandPalette.GroupLabel>
<CommandPalette.Items>
{(item: CommandItem) => (
<CommandPalette.Item
key={item.id}
value={item}
onClick={() => setOpen(false)}
>
<span className="flex items-center gap-3">
{item.icon && (
<span className="text-kumo-subtle">
{item.icon}
</span>
)}
<span>{item.title}</span>
</span>
</CommandPalette.Item>
)}
</CommandPalette.Items>
</CommandPalette.Group>
)}
</CommandPalette.Results>
<CommandPalette.Empty>No results found</CommandPalette.Empty>
</>
)}
</CommandPalette.List>
</CommandPalette.Root>
</div>
);
}禁用浏览器自动补全
传入 autoComplete、autoCorrect、spellCheck 和 data-* 等标准 HTML 输入属性,可抑制浏览器和密码管理器自动补全覆盖层。
import { useState } from "react";
import { CommandPalette, Button } from "@cloudflare/kumo";
/** Demonstrates disabling browser autocomplete and spellcheck on the command palette input. */
export function CommandPaletteNoAutocompleteDemo() {
const [open, setOpen] = useState(false);
const [search, setSearch] = useState("");
const filteredGroups = filterGroupsWithItems(sampleGroups, search);
return (
<div className="flex flex-col items-start gap-4">
<Button onClick={() => setOpen(true)}>
Open Palette (No Autocomplete)
</Button>
<CommandPalette.Root
open={open}
onOpenChange={setOpen}
items={filteredGroups}
value={search}
onValueChange={setSearch}
itemToStringValue={(group) => group.label}
onSelect={(item) => {
console.log("Selected:", item.title);
setOpen(false);
setSearch("");
}}
getSelectableItems={getSelectableItems}
>
<CommandPalette.Input
placeholder="Search commands..."
autoComplete="off"
autoCorrect="off"
autoCapitalize="none"
spellCheck={false}
data-1p-ignore="true"
data-lpignore="true"
/>
<CommandPalette.List>
<CommandPalette.Results>
{(group: CommandGroup) => (
<CommandPalette.Group key={group.id} items={group.items}>
<CommandPalette.GroupLabel>
{group.label}
</CommandPalette.GroupLabel>
<CommandPalette.Items>
{(item: CommandItem) => (
<CommandPalette.Item
key={item.id}
value={item}
onClick={() => {
setOpen(false);
setSearch("");
}}
>
<span className="flex items-center gap-3">
{item.icon && (
<span className="text-kumo-subtle">{item.icon}</span>
)}
<span>{item.title}</span>
</span>
</CommandPalette.Item>
)}
</CommandPalette.Items>
</CommandPalette.Group>
)}
</CommandPalette.Results>
<CommandPalette.Empty>No commands found</CommandPalette.Empty>
</CommandPalette.List>
</CommandPalette.Root>
</div>
);
}带面包屑的 ResultItem
使用 ResultItem 构建丰富的条目,支持
面包屑、图标和可选文本高亮。
import { useState } from "react";
import { CommandPalette, Button } from "@cloudflare/kumo";
export function CommandPaletteResultItemDemo() {
const [open, setOpen] = useState(false);
const [search, setSearch] = useState("");
return (
<div>
<Button onClick={() => setOpen(true)}>Open with ResultItem</Button>
<CommandPalette.Root
open={open}
onOpenChange={setOpen}
items={searchResults}
value={search}
onValueChange={setSearch}
itemToStringValue={(item) => item.title}
getSelectableItems={(items) => items}
>
<CommandPalette.Input placeholder="Search documentation..." />
<CommandPalette.List>
<CommandPalette.Results>
{(item: SearchResult) => (
<CommandPalette.ResultItem
key={item.id}
value={item}
title={item.title}
breadcrumbs={item.breadcrumbs}
icon={item.icon}
onClick={() => {
console.log("Navigate to:", item.title);
setOpen(false);
}}
/>
)}
</CommandPalette.Results>
<CommandPalette.Empty>No pages found</CommandPalette.Empty>
</CommandPalette.List>
<CommandPalette.Footer>
<span className="flex items-center gap-2">
<kbd className="rounded border border-kumo-hairline bg-kumo-base px-1.5 py-0.5 text-[10px]">
↑↓
</kbd>
<span>Navigate</span>
</span>
<span className="flex items-center gap-2">
<kbd className="rounded border border-kumo-hairline bg-kumo-base px-1.5 py-0.5 text-[10px]">
⌘↵
</kbd>
<span>Open in new tab</span>
</span>
</CommandPalette.Footer>
</CommandPalette.Root>
</div>
);
}组件组成
CommandPalette.Root
组合 Dialog 与 Panel 的主包装组件。管理打开状态和 Autocomplete 功能。
CommandPalette.Dialog
模态对话框包装。与 Panel 搭配使用可实现可替换内容(例如 下钻式导航)。
CommandPalette.Panel
不含对话框的 Autocomplete 面板。在 Dialog 内使用,用于容纳无需 重新挂载即可切换的内容。
CommandPalette.Input
带自动聚焦和键盘处理的搜索输入框。
CommandPalette.List
结果的滚动容器。
CommandPalette.Results
条目/分组的 render 属性迭代器。
CommandPalette.Group
类别分组容器。
CommandPalette.GroupLabel
分组内的小节标题文本。
CommandPalette.Items
组内条目的 render 属性迭代器。
CommandPalette.Item
基础可选条目。
CommandPalette.ResultItem
带面包屑、图标和文本高亮的丰富条目。
CommandPalette.HighlightedText
根据匹配索引渲染带高亮部分的文本。
CommandPalette.Empty
无匹配结果时的空状态。
CommandPalette.Loading
加载微调器状态。
CommandPalette.Footer
用于存放键盘提示或其他内容的底部区域。
API 参考
CommandPalette.Root 属性
interface CommandPaletteRootProps<TGroup, TItem> {
// Dialog state
open: boolean;
onOpenChange: (open: boolean) => void;
onBackdropClick?: (e: React.MouseEvent) => void;
// Autocomplete
items: TGroup[];
value?: string;
onValueChange?: (value: string) => void;
itemToStringValue?: (item: TGroup) => string;
filter?: (item: TGroup, query: string) => boolean;
onItemHighlighted?: (item: TGroup | undefined, details: {...}) => void;
// Selection
onSelect?: (item: TItem, options: { newTab: boolean }) => void;
getSelectableItems?: (items: TGroup[]) => TItem[];
children: React.ReactNode;
}CommandPalette.ResultItem 属性
interface CommandPaletteResultItemProps<T> {
value: T;
title: string;
breadcrumbs?: string[];
titleHighlights?: [number, number][];
breadcrumbHighlights?: [number, number][][];
description?: string;
icon?: React.ReactNode;
onClick: (e: React.MouseEvent) => void;
showArrow?: boolean; // default: true
external?: boolean; // shows external link icon
nonInteractive?: boolean;
}