Mantine Spotlight 实战指南:为 React 应用打造 Overlay 命令中心
【免费下载链接】mantineA fully featured React components library项目地址: https://gitcode.com/GitHub_Trending/ma/mantine
Spotlight 是 Mantine 提供的全屏覆盖式命令中心组件(Overlay command center),它基于 Modal 实现,支持快捷键唤起、即时过滤、键盘导航,可直接为应用注入类似 macOS Spotlight / VS Code 命令面板的交互体验。本文将基于 packages/@mantine/spotlight 的源码,完整讲解安装、用法、数据模型、状态管理、过滤机制与样式定制,帮助你快速集成并深度定制。
什么是 Mantine Spotlight
Spotlight 是 Mantine 生态中的独立包,README 将其定位为 "Overlay command center for your application"(应用的全屏命令中心)。它解决的问题很具体:当应用功能越来越多,用户难以通过菜单层级找到想要的操作时,提供一个以键盘驱动的搜索面板,输入关键词即可命中并触发任意动作。
从源码结构看,@mantine/spotlight是一组可组合的复合组件(compound components)外加一个全局状态 store:
- 组件:
Spotlight、SpotlightRoot、SpotlightSearch、SpotlightActionsList、SpotlightAction、SpotlightActionsGroup、SpotlightEmpty、SpotlightFooter(导出见 index.ts); - 状态与动作:
spotlight、createSpotlight、createSpotlightStore、useSpotlight、openSpotlight、closeSpotlight、toggleSpotlight; - 包依赖:
@mantine/core、@mantine/hooks、@mantine/store(见 package.json)。
安装与基础环境
安装命令
README 给出的安装方式如下(README.md):
# With yarn yarn add @mantine/spotlight @mantine/core @mantine/hooks # With npm npm install @mantine/spotlight @mantine/core @mantine/hooks版本与依赖说明
- 当前仓库中
@mantine/spotlight版本为 9.6.0,peerDependencies要求@mantine/core与@mantine/hooks同为 9.6.0,React 需为^19.2.0、react-dom 同理(见 package.json),安装时注意版本对齐; - 它内部依赖
@mantine/store(9.6.0),用于状态管理(见 package.json); - 包同时提供 ESM(
esm/index.mjs)、CJS(cjs/index.cjs)与类型声明(lib/index.d.ts),并单独导出样式文件./styles.css与./styles.layer.css(见 package.json); - 使用 CSS Modules 构建,需要你的构建工具链支持 CSS Modules 与 Mantine 的 PostCSS 配置(仓库根目录的 postcss.config.cjs 即此类配置示例)。
基础用法:声明式 actions + 快捷键唤起
Spotlight 提供两种主要集成方式:声明式(通过actions数组声明命令)与命令式(通过 store 的 open/close/toggle 控制开关)。
方式一:声明式(推荐)
以复合组件形式声明搜索框、动作列表、空态与页脚,动作通过actions数组传入:
import { Spotlight } from '@mantine/spotlight'; import { IconSearch, IconHome, IconUser } from '@tabler/icons-react'; function App() { return ( <Spotlight actions={[ { id: 'home', label: 'Go to home', description: 'Navigate to the home page', leftSection: <IconHome size={18} />, onClick: () => navigate('/') }, { id: 'profile', label: 'Open profile', description: 'View your profile', leftSection: <IconUser size={18} />, onClick: () => navigate('/profile') }, ]} nothingFound="No results found" > <Spotlight.Search placeholder="Search actions..." /> <Spotlight.ActionsList /> <Spotlight.Empty /> <Spotlight.Footer>Press Enter to select</Spotlight.Footer> </Spotlight> ); }Spotlight组件内部逻辑(见 Spotlight.tsx)大致为:将query交给filter过滤,再交给limitActions截断,把每一项渲染为SpotlightAction;若某项带group,则包一层SpotlightActionsGroup。过滤后无结果且传了nothingFound时,渲染SpotlightEmpty。
方式二:命令式
通过Spotlight.open()/Spotlight.close()/Spotlight.toggle()或全局导出的openSpotlight/closeSpotlight/toggleSpotlight手动控制开关:
import { Button } from '@mantine/core'; import { openSpotlight, Spotlight } from '@mantine/spotlight'; function App() { return ( <> <Button onClick={openSpotlight}>Open spotlight</Button> <Spotlight actions={actions} nothingFound="Nothing found" /> </> ); }这些静态方法与全局函数都来自 store 层(见 Spotlight.tsx 与 spotlight.store.ts)。
组件 API 全景
Spotlight是一个复合组件,其静态子组件与函数(见 Spotlight.tsx 的Factory定义与 Spotlight.test.tsx 的测试断言):
| 静态成员 | 对应组件/函数 | 作用 |
|---|---|---|
Spotlight.Search | SpotlightSearch | 搜索输入框,负责过滤与键盘事件 |
Spotlight.ActionsList | SpotlightActionsList | 动作列表容器,内部使用ScrollArea.Autosize |
Spotlight.Action | SpotlightAction | 单个动作按钮 |
Spotlight.ActionsGroup | SpotlightActionsGroup | 动作分组(带组标题) |
Spotlight.Empty | SpotlightEmpty | 无结果时的空态提示 |
Spotlight.Footer | SpotlightFooter | 底部区域(如快捷键提示) |
Spotlight.Root | SpotlightRoot | 根容器(基于 Modal) |
Spotlight.open/close/toggle | store 动作 | 程序化开关 |
Spotlight 主组件 Props
定义见 Spotlight.tsx,同时继承SpotlightRootProps(Modal 相关 props):
| Prop | 类型 | 默认值 | 说明 |
|---|---|---|---|
actions | SpotlightActions[] | 必填 | 动作数据,见下文"数据模型" |
filter | SpotlightFilterFunction | defaultSpotlightFilter | 自定义过滤函数 |
nothingFound | React.ReactNode | - | 无匹配结果时的提示内容 |
highlightQuery | boolean | false | 是否高亮动作 label 中的匹配文本 |
limit | number | Infinity | 单次最多显示的动作数量 |
searchProps | SpotlightSearchProps | - | 透传给Spotlight.Search的 props |
scrollAreaProps | Partial<ScrollAreaAutosizeProps> | - | 透传给列表内部ScrollArea的 props |
query/onQueryChange | string/ 回调 | - | 受控查询词(来自 Root) |
shortcut | string \| string[] \| null | 'mod + K' | 唤起快捷键(来自 Root) |
SpotlightRoot Props(继承自 Modal)
定义见 SpotlightRoot.tsx:
| Prop | 类型 | 默认值 | 说明 |
|---|---|---|---|
store | SpotlightStore | 全局spotlightStore | 指定 store,用于多实例场景 |
clearQueryOnClose | boolean | true | 关闭时是否清空查询词 |
closeOnActionTrigger | boolean | true | 触发动作后是否自动关闭 |
shortcut | string \| string[] \| null | 'mod + K' | 快捷键,传null可禁用 |
tagsToIgnore | string[] | ['input','textarea','select'] | 焦点在这些标签内时忽略快捷键 |
triggerOnContentEditable | boolean | false | contentEditable 区域是否触发快捷键 |
disabled | boolean | false | 为 true 时不渲染 Spotlight |
onSpotlightOpen/onSpotlightClose | 回调 | - | 打开/关闭回调(由useDidUpdate触发) |
forceOpened | boolean | - | 强制打开,常用于测试 |
maxHeight | CSSmaxHeight | 400 | 内容最大高度(需配合scrollable) |
scrollable | boolean | false | 是否让动作列表可滚动 |
此外还继承size(默认 600)、yOffset(默认 80)、zIndex(默认getDefaultZIndex('max'))、overlayProps(默认{ backgroundOpacity: 0.35, blur: 7 })、transitionProps(默认{ duration: 200, transition: 'pop' })等 Modal props,默认值汇总见 SpotlightRoot.tsx。
SpotlightAction Props
定义见 SpotlightAction.tsx:
| Prop | 类型 | 默认值 | 说明 |
|---|---|---|---|
label | string | - | 动作标题,参与默认过滤 |
description | string | - | 动作描述,参与默认过滤 |
leftSection/rightSection | React.ReactNode | - | 左侧(图标)/右侧(快捷键提示)区块 |
children | React.ReactNode | - | 自定义内容,覆盖默认 label/描述/区块 |
dimmedSections | boolean | true | 左右区块是否使用弱化样式 |
highlightQuery | boolean | false | 是否高亮匹配文本 |
highlightColor | MantineColor | 'yellow' | 高亮颜色(theme.colors键或任意 CSS 颜色) |
closeSpotlightOnTrigger | boolean | - | 触发后是否关闭,覆盖根组件的closeOnActionTrigger |
keywords | string \| string[] | - | 隐藏关键词,参与过滤但不展示,如"react,router,javascript" |
数据模型:ActionData 与 ActionsGroup
类型定义见 Spotlight.tsx:
interface SpotlightActionData extends SpotlightActionProps { id: string; // 唯一标识,用作 React key group?: string; // 分组名,存在时按组渲染 } interface SpotlightActionGroupData { group: string; actions: SpotlightActionData[]; } type SpotlightActions = SpotlightActionData | SpotlightActionGroupData;两种组织方式示例:
const actions = [ { id: 'home', label: 'Home', group: 'Navigation', onClick: () => go('/') }, { group: 'Actions', actions: [ { id: 'new-file', label: 'New file', keywords: ['create', 'document'], onClick: createFile }, { id: 'save', label: 'Save', onClick: saveFile }, ], }, ];isActionsGroup通过group !== undefined && Array.isArray(item.actions)判定条目是否为分组(is-actions-group.ts),渲染逻辑见 Spotlight.tsx。
键盘交互与默认快捷键
SpotlightRoot使用useHotkeys注册快捷键(SpotlightRoot.tsx),getHotkeys会把字符串或数组转换为[hotkey, open]形式的快捷键项(get-hotkeys.ts):
// 单个快捷键 <Spotlight shortcut="mod + K" ... /> // 多个快捷键 <Spotlight shortcut={['mod + K', 'mod + P']} ... /> // 禁用快捷键 <Spotlight shortcut={null} ... />SpotlightSearch处理输入框内的键盘导航(SpotlightSearch.tsx):
ArrowDown:selectNextAction,选中下一个动作;ArrowUp:selectPreviousAction,选中上一个动作;Enter/NumpadEnter:triggerSelectedAction,触发当前选中的动作;- 支持 IME 输入法合成(
onCompositionStart/onCompositionEnd),中文输入法候选状态下手势事件会被跳过,避免误触发。
选中与滚动逻辑在 store 层实现(spotlight.store.ts):selectAction通过#listId定位动作列表,使用[data-action]收集动作按钮、[data-selected]标记当前项,并调用scrollIntoView({ block: 'nearest' })保证选中项可见;triggerSelectedAction则对[data-selected]元素执行.click()。注意selectAction支持 Shadow DOM 递归查找元素(spotlight.store.ts)。
过滤机制与自定义 filter
默认过滤由defaultSpotlightFilter实现(default-spotlight-filter.ts),匹配规则:
- 查询词先
trim().toLowerCase()归一化; - 优先级矩阵:
label包含查询词的动作进入第一优先级;description或keywords包含查询词的动作进入第二优先级; - 结果先按优先级排序,再按原顺序保留(
flatActionsToGroups会重新聚合成组); - 分组内的动作仍按顺序排列,组间顺序保持不变。
keywords支持字符串(如"react,router,javascript")或数组(如['react', 'router', 'javascript']),统一转为小写后参与匹配(default-spotlight-filter.ts)。
自定义 filter 只需实现(query, actions) => actions签名:
const fuzzyFilter: SpotlightFilterFunction = (query, actions) => { // 使用你自己的模糊匹配算法(如 Fuse.js)过滤 actions return fuzzySearch(query, actions); }; <Spotlight filter={fuzzyFilter} actions={actions} />;过滤后的结果还会经过limitActions截断(limit-actions.ts),它按顺序累计动作数量,达到limit后停止,分组内部也会递归截断,保证总显示数不超过limit。
状态管理:SpotlightStore 与多实例
Spotlight 的状态基于@mantine/store的createStore,SpotlightState包含opened、selected(当前选中索引)、listId(动作列表 DOM id)、query、empty(空态标记)、registeredActions(已注册动作的 Set)(spotlight.store.ts)。
全局单例
默认导出的spotlightStore与spotlight由createSpotlight()生成(spotlight.store.ts),openSpotlight/closeSpotlight/toggleSpotlight直接操作该全局实例。全局单例适合大多数应用只有一个命令中心的场景。
多实例与 createSpotlight
需要多个独立命令中心时,使用createSpotlight或createSpotlightStore创建独立 store:
import { createSpotlight, Spotlight } from '@mantine/spotlight'; // 创建独立的 store 与命令 const [store, spotlight] = createSpotlight(); function App() { return ( <> <Spotlight store={store} actions={actions} /> <button onClick={spotlight.open}>Open</button> </> ); }Store 关键动作
open/close/toggle:开关面板(toggle 时重置选中索引,见 spotlight.store.ts);setQuery:更新查询词,异步重置选中到第一项,并根据"查询非空但已注册动作数为 0"计算empty状态(spotlight.store.ts);clearSpotlightState:关闭时按clearQueryOnClose决定是否清空查询词与空态(spotlight.store.ts);registerAction:注册/注销动作 id,供空态判断使用(spotlight.store.ts)。
样式定制
样式 API(Styles API)
Spotlight的样式名(SpotlightStylesNames)包括 Modal 的全部样式名(root、content、body、inner、overlay等)外加search、actionsList、action、empty、footer、actionBody、actionLabel、actionDescription、actionSection、actionsGroup(SpotlightRoot.tsx),因此可以通过classNames/styles精确覆盖任意层级。
测试中验证了完整的样式选择器列表,包括root、action、actionBody、actionDescription、actionLabel、actionSection、actionsList、actionsGroup、body、content、inner、overlay、search(Spotlight.test.tsx)。
<Spotlight actions={actions} classNames={{ search: 'my-search', action: 'my-action' }} styles={{ actionLabel: { fontWeight: 600 } }} />关键 CSS 实现
核心样式见 Spotlight.module.css:
.content通过 CSS 变量控制高度:height: var(--spotlight-content-height, auto)、max-height: var(--spotlight-max-height),scrollable时由 Root 注入--spotlight-max-height(SpotlightRoot.tsx);.actionsList使用--spotlight-actions-list-padding: 4px作为滚动条偏移量,并设置max-height: calc(100vh - 15rem);.action[data-selected]使用主题主色var(--mantine-primary-color-filled)高亮选中项,描述文本通过--action-description-color/--action-description-opacity变量做降级显示;.actionsGroup通过--spotlight-labelCSS 变量渲染组标题(content: var(--spotlight-label)),组标题由 SpotlightActionsGroup.tsx 注入并转义引号;- 深色/浅色模式分别通过
@mixin where-light/@mixin where-dark适配边框与 hover 背景色。
无障碍与测试要点
- 每个动作渲染为
UnstyledButton,带data-action属性,tabIndex={-1},由 store 通过data-selected属性管理选中态(SpotlightAction.tsx); - 搜索框基于 Mantine
Input构建,键盘事件完整支持方向键与回车导航; - 测试用例覆盖了:系统 props 与样式 API 选择器、静态成员暴露、无动作时不渲染列表容器(仅渲染
nothingFound)、triggerSelectedAction在listId为空时不抛异常(Spotlight.test.tsx); - 需要打开面板做测试时,可组合
forceOpened、withinPortal={false}与transitionProps={{ duration: 0 }}(见测试的defaultProps,Spotlight.test.tsx)。
典型使用场景
- 全局命令面板:注册所有页面跳转、创建/保存等高频操作,
mod + K唤起,配合keywords提供语义别名; - 导航替代方案:通过
group将"导航""操作""设置"分组展示,减少鼠标点击层级; - 多实例业务场景:例如页面内搜索(
mod + K)与主题切换(mod + T)分别使用createSpotlight创建的独立 store; - 自定义过滤引擎:替换
filter接入模糊匹配或拼音检索,提升中文/复杂关键词的命中体验; - 快捷键提示可视化:利用
rightSection显示每个动作的快捷键(如⌘N),配合底部Spotlight.Footer给出操作指引。
总结
Mantine Spotlight 用约十个文件实现了一个功能完整的命令中心:数据驱动渲染(actions数组)、可插拔过滤(filter+limitActions)、事件驱动状态(@mantine/store)、复合组件组合(Spotlight.*静态成员)与完整样式 API。通过本文的 Props 表格、源码路径与数据模型说明,你可以快速集成默认行为,也可以在需要时替换过滤逻辑、扩展多实例或精细定制样式。
【免费下载链接】mantineA fully featured React components library项目地址: https://gitcode.com/GitHub_Trending/ma/mantine
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考