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
不带对话框的自动补全面板。放在 Dialog 内部,可在不重新挂载的情况下切换内容。
CommandPalette.Input
带自动聚焦和键盘处理的搜索输入框。
CommandPalette.List
可滚动的结果容器。
CommandPalette.Results
用于项/分组的 render prop 迭代器。
CommandPalette.Group
分类分组容器。
CommandPalette.GroupLabel
分组内的节标题文本。
CommandPalette.Items
用于分组内各项的 render prop 迭代器。
CommandPalette.Item
基础的可选项。
CommandPalette.ResultItem
带面包屑、图标和文本高亮的富内容项。
CommandPalette.HighlightedText
根据匹配索引渲染带高亮部分的文本。
CommandPalette.Empty
没有匹配结果时的空状态。
CommandPalette.Loading
加载指示器状态。
CommandPalette.Footer
用于键盘提示或其他内容的页脚。
API 参考
CommandPalette.Root 属性
interface CommandPaletteRootProps<TGroup, TItem> {
// 对话框状态
open: boolean;
onOpenChange: (open: boolean) => void;
onBackdropClick?: (e: React.MouseEvent) => void;
// 自动补全
items: TGroup[];
value?: string;
onValueChange?: (value: string) => void;
itemToStringValue?: (item: TGroup) => string;
filter?: (item: TGroup, query: string) => boolean;
onItemHighlighted?: (item: TGroup | undefined, details: {...}) => void;
// 选择
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; // 默认:true
external?: boolean; // 显示外部链接图标
nonInteractive?: boolean;
}