'use client'
import { useState } from 'react'
import { Bookmark, Home, Library, LogOut, Moon, PenLine, Search, Settings } from 'lucide-react'
import { Button, CommandPalette, Kbd, Stack, Text, type CommandItems } from '@hina-ui/react'
const items: CommandItems = [
{
label: '页面',
items: [
{ id: 'home', label: '首页', icon: Home, keywords: ['home'] },
{ id: 'library', label: '书架', icon: Library, keywords: ['library'] },
{ id: 'bookmarks', label: '收藏', icon: Bookmark, keywords: ['bookmark'] },
{ id: 'settings', label: '设置', icon: Settings, keywords: ['settings'], kbd: ['⌘', ','] },
],
},
{
label: '操作',
items: [
{ id: 'review', label: '新建书评', icon: PenLine, description: '记录一本刚读完的书' },
{ id: 'theme', label: '切换主题', icon: Moon, kbd: ['⌘', 'D'] },
{ id: 'logout', label: '退出登录', icon: LogOut },
],
},
]
export default function Demo() {
const [picked, setPicked] = useState('')
return (
<Stack gap="md" align="start">
<CommandPalette items={items} hotkey="mod+j" onSelect={item => setPicked(item.label)}>
<Button variant="outline" tone="neutral" icon={<Search />}>
搜索
<Kbd>⌘J</Kbd>
</Button>
</CommandPalette>
{picked && (
<Text size="sm" tone="muted">
已选择:{picked}
</Text>
)}
</Stack>
)
}
用法
import { CommandPalette } from '@hina-ui/react'
items 是条目列表。每个条目至少有 id 与 label,可以带 description、keywords、icon、kbd 与 onSelect;带有 label 与 items 的对象是一个分组。children 是触发器。选中条目时先调用该条目的 onSelect,再调用组件的 onSelect,然后关闭面板。
'use client'
import { useState } from 'react'
import { Button, CommandPalette, Stack, Text, type CommandItems } from '@hina-ui/react'
const items: CommandItems = [
{ id: 'home', label: '首页' },
{ id: 'library', label: '书架' },
{ id: 'bookmarks', label: '收藏' },
{ id: 'settings', label: '设置' },
]
export default function Demo() {
const [picked, setPicked] = useState('')
return (
<Stack gap="md" align="start">
<CommandPalette items={items} onSelect={item => setPicked(item.label)}>
<Button variant="outline" tone="neutral">
打开面板
</Button>
</CommandPalette>
{picked && (
<Text size="sm" tone="muted">
已选择:{picked}
</Text>
)}
</Stack>
)
}
示例
分组与说明
分组各有标题,条目的 description 显示在标签下方。没有查询时分组按给定顺序排列,有查询时含最佳匹配的分组靠前,没有匹配条目的分组不显示。
import { Button, CommandPalette, type CommandItems } from '@hina-ui/react'
const items: CommandItems = [
{
label: '最近阅读',
items: [
{ id: 'book-1', label: '星之继承者', description: '詹姆斯·P·霍根' },
{ id: 'book-2', label: '海伯利安', description: '丹·西蒙斯' },
],
},
{
label: '书单',
items: [
{ id: 'list-1', label: '今年想读', description: '12 本' },
{ id: 'list-2', label: '硬科幻入门', description: '8 本' },
],
},
]
export default function Demo() {
return (
<CommandPalette items={items} placeholder="搜索书与书单">
<Button variant="outline" tone="neutral">
搜索
</Button>
</CommandPalette>
)
}
图标与按键提示
icon 显示在标签前,kbd 是显示在行末的按键提示,只用于提示,面板不会替你绑定这些按键。
'use client'
import { Copy, Moon, PenLine, Trash2 } from 'lucide-react'
import { Button, CommandPalette, type CommandItems } from '@hina-ui/react'
const items: CommandItems = [
{ id: 'review', label: '新建书评', icon: PenLine, kbd: ['⌘', 'N'] },
{ id: 'copy', label: '复制链接', icon: Copy, kbd: ['⌘', 'C'] },
{ id: 'theme', label: '切换主题', icon: Moon, kbd: ['⌘', 'D'] },
{ id: 'delete', label: '删除书评', icon: Trash2, disabled: true },
]
export default function Demo() {
return (
<CommandPalette items={items}>
<Button variant="outline" tone="neutral">
操作
</Button>
</CommandPalette>
)
}
全局快捷键
hotkey 接受 mod+k 这样的组合,mod 对应 Mac 的 ⌘ 与其他平台的 Ctrl,还可以加上 shift 与 alt。面板打开时再按一次会关闭。
也可以按⌘⇧P打开
import { Button, CommandPalette, Inline, Kbd, Text, type CommandItems } from '@hina-ui/react'
const items: CommandItems = [
{ id: 'home', label: '首页' },
{ id: 'library', label: '书架' },
{ id: 'settings', label: '设置' },
]
export default function Demo() {
return (
<Inline gap="md" align="center">
<CommandPalette items={items} hotkey="mod+shift+p">
<Button variant="outline" tone="neutral">
搜索
</Button>
</CommandPalette>
<Text size="sm" tone="muted">
也可以按
<Kbd>⌘</Kbd>
<Kbd>⇧</Kbd>
<Kbd>P</Kbd>
打开
</Text>
</Inline>
)
}
受控
open 可受控。省略 children 时不渲染触发器,面板只能从外部打开。
已关闭
'use client'
import { useState } from 'react'
import { Button, CommandPalette, Inline, Text, type CommandItems } from '@hina-ui/react'
const items: CommandItems = [
{ id: 'home', label: '首页' },
{ id: 'library', label: '书架' },
{ id: 'settings', label: '设置' },
]
export default function Demo() {
const [open, setOpen] = useState(false)
return (
<Inline gap="md" align="center">
<Button variant="outline" tone="neutral" onClick={() => setOpen(true)}>
从外部打开
</Button>
<Text size="sm" tone="muted">
{open ? '已打开' : '已关闭'}
</Text>
<CommandPalette open={open} onOpenChange={setOpen} items={items} />
</Inline>
)
}
自定义过滤
search 可受控。设置 ignoreFilter 后面板不再自行过滤,条目列表完全由调用方决定,例如向服务端搜索。
'use client'
import { useState } from 'react'
import { Button, CommandPalette, type CommandItems } from '@hina-ui/react'
const books = [
{ id: 'b1', label: '星之继承者', tags: ['科幻', '硬科幻'] },
{ id: 'b2', label: '海伯利安', tags: ['科幻', '太空歌剧'] },
{ id: 'b3', label: '基地', tags: ['科幻', '经典'] },
{ id: 'b4', label: '三体', tags: ['科幻', '中文'] },
]
export default function Demo() {
const [search, setSearch] = useState('')
const query = search.trim()
const items: CommandItems = books
.filter(book => !query || book.tags.some(tag => tag.includes(query)))
.map(book => ({ id: book.id, label: book.label, description: book.tags.join('、') }))
return (
<CommandPalette
search={search}
onSearchChange={setSearch}
items={items}
ignoreFilter
placeholder="按标签搜索"
>
<Button variant="outline" tone="neutral">
按标签搜索
</Button>
</CommandPalette>
)
}
内联
设置 inline 后面板不再包进浮层,直接渲染在文档流里,适合嵌在页面中而不是由快捷键唤起。内联形态不注册全局快捷键,选中条目之后面板停在原地。
'use client'
import { Bookmark, Home, Library, Moon, PenLine, Settings } from 'lucide-react'
import { CommandPalette, type CommandItems } from '@hina-ui/react'
const items: CommandItems = [
{
label: '页面',
items: [
{ id: 'home', label: '首页', icon: Home },
{ id: 'library', label: '书架', icon: Library },
{ id: 'bookmarks', label: '收藏', icon: Bookmark },
{ id: 'settings', label: '设置', icon: Settings, kbd: ['⌘', ','] },
],
},
{
label: '操作',
items: [
{ id: 'review', label: '新建书评', icon: PenLine, description: '记录一本刚读完的书' },
{ id: 'theme', label: '切换主题', icon: Moon, kbd: ['⌘', 'D'] },
],
},
]
export default function Demo() {
return <CommandPalette inline items={items} className="max-w-sm" />
}
虚拟滚动
virtualize 按需渲染可见范围附近的条目,与 VirtualList 共用测量与滚动底层。默认关闭;可传 { estimateSize, overscan } 调整预估行高和两侧预渲染数量,行高会按实际内容测量。键盘导航覆盖完整数据,禁用项会跳过。 搜索仍处理完整数据。 命令离开渲染范围后会卸载,持久状态应按命令 id 保存在外部。
已选: —
'use client'
import { useState } from 'react'
import { CommandPalette, Stack, Text } from '@hina-ui/react'
const items = Array.from({ length: 10000 }, (_, index) => ({
id: String(index),
label: `条目 ${String(index + 1).padStart(5, '0')}`,
keywords: [`id-${index}`],
}))
export default function Demo() {
const [selected, setSelected] = useState('—')
return (
<Stack gap="sm" className="w-96 max-w-full">
<CommandPalette
items={items}
virtualize={{ estimateSize: 36, overscan: 6 }}
inline
aria-label="一万项"
onSelect={item => setSelected(item.label)}
/>
<Text size="sm" tone="muted">
已选: {selected}
</Text>
</Stack>
)
}
行为
- 打开后焦点落在输入框,首个条目自动高亮;输入时列表即时过滤,标签中匹配的片段以强调色标出。
- 匹配按标签全等、标签开头、标签包含、关键词、说明的顺序排列;有查询时,含最佳匹配的分组排在前面。超出可见高度的条目在列表内滚动。
- 方向键移动高亮,回车选中高亮项,鼠标悬停也会移动高亮。
- 按 Esc、点击遮罩或选中条目都会关闭面板,关闭时清空搜索内容。
- 面板打开期间页面停止滚动,焦点限制在面板内,关闭后回到触发器。
- 宽屏上面板停靠在视口上部,窄屏上贴顶并占满宽度。
无障碍
- 面板是对话框,无障碍名取
label,默认为界面语言中的“命令面板”。 - 输入框通过
aria-activedescendant指向当前高亮的条目;列表使用listbox与option角色,分组带有各自的名称。 - 图标对辅助技术隐藏,按键提示以
kbd元素呈现。
API
Props
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
items | CommandItems | — | 必填。条目与分组 |
virtualize | VirtualizeOptions | false | 虚拟滚动;预估行高按内容,overscan 6 |
placeholder | string | 取自界面语言 | 输入框的占位文字 |
label | string | 取自界面语言 | 面板的无障碍名 |
hotkey | string | — | 全局快捷键,例如 mod+k |
ignoreFilter | boolean | false | 不自行过滤,条目列表由调用方决定 |
inline | boolean | false | 渲染为内联面板,不使用浮层 |
className | string | — | 追加至面板的类名 |
受控状态
| 名称 | 类型 | 说明 |
|---|---|---|
open | boolean | 面板是否打开 |
search | string | 输入框中的搜索词 |
回调
| 回调 | 参数 | 说明 |
|---|---|---|
onSelect | (item: CommandItem) | 选中条目时触发 |
内容属性
| 属性 | 说明 |
|---|---|
children | 触发器 |
类型
| 字段 | 类型 | 说明 |
|---|---|---|
id | string | 必填。条目的唯一标识 |
label | string | 必填。标签 |
description | string | 标签下方的说明,也参与匹配 |
keywords | string[] | 参与匹配但不显示的关键词 |
icon | Component | 标签前的图标 |
kbd | string[] | 行末的按键提示 |
disabled | boolean | 不可选中 |
onSelect | () => void | 选中时调用 |
分组是 { label: string; items: CommandItem[] },CommandItems 是条目与分组的数组。
type VirtualizeOptions = boolean | { estimateSize?: number; overscan?: number }