8.8 KiB
Tactile UI 组件库
Tactile UI 是 Planet 内部抽出的可移植 React 控件层。它来自 Admin 的按钮、开关、滚动条和 tooltip 收口,但组件本身不依赖 Admin、AntD、Radix、Tailwind 或 an-* class。目标是先在仓库内稳定使用,后续可以作为独立 npm 包发布。
设计目标
- 轻触感:默认控件使用白色或主题表面、细边框和外部投影,接近 Docs 主题滑块的轻微立体感,不使用大色块或发光效果。
- 可移植:组件 class 使用
tui-*前缀,样式集中在frontend/src/components/tactile-ui/styles.css。 - 低依赖:组件只假设 React/React DOM;图标预设当前使用
lucide-react,调用方也可以传自定义 React 节点。 - 主题友好:默认样式通过 CSS variables 暴露,Planet 可以在 Admin 或其它页面按主题覆盖 token。
- 语义清楚:无歧义操作优先 icon-only + tooltip;保存、确认、创建、执行这类强意图操作可以保留文字。
导入
仓库内使用:
import { TactileButton, TactileSwitch, ControlGroup } from '@/components/tactile-ui'
import '@/components/tactile-ui/styles.css'
未来拆包后建议导入形式:
import { TactileButton, TactileSwitch } from '@planet/tactile-ui'
import '@planet/tactile-ui/styles.css'
主题 Token
核心 token 在 CSS 中以 --tui-* 暴露。业务主题只需要覆盖变量,不应直接重写组件内部 class。
:root {
--tui-surface: #ffffff;
--tui-surface-raised-hover: #f8fbff;
--tui-border-soft: #d6dfeb;
--tui-border-hover: #b8c6d9;
--tui-text: #0f172a;
--tui-primary: #2563eb;
--tui-danger: #dc2626;
}
[data-theme='dark'] {
--tui-surface: #111827;
--tui-text: #e5edf8;
}
组件还支持通过 tactile prop 传入局部尺寸、圆角、背景、边框和阴影参数。局部参数适合少量特殊按钮;全局视觉应优先走 CSS variables。
Portal 与深色主题
TactileTooltip 和使用 Tactile UI 的 Admin Dialog、Select、Toast 都可能通过 portal 挂到 document.body。这类节点不在 .admin-theme-root[data-theme='dark'] 下面,不能只依赖局部祖先选择器读取深色 token。
Admin 的主题 provider 会把当前主题同步到 body[data-admin-theme]。共享样式必须同时支持两类选择器:
[data-theme='dark'] .tui-button,
body[data-admin-theme='dark'] .tui-button {
--tui-surface: #172033;
--tui-text: #e5edf8;
}
新增 portal 控件时,先确认它是否渲染到 body。如果是,就要在组件自己的样式入口补 body[data-admin-theme='dark'] 分支,或复用已经覆盖过的 --tui-* / --an-* token。不要在单个弹窗里手写固定深色,因为同一问题会在下拉菜单、tooltip、toast 和确认弹窗里重复出现。
TactileButton
按钮组件覆盖普通按钮、图标按钮、强意图按钮和链接式按钮。
常用 props:
| Prop | 说明 |
|---|---|
variant |
neutral、primary、danger、subtle、ghost |
size |
sm、md、lg、icon |
shape |
square、pill |
icon |
预设图标名称或自定义 React 节点 |
iconOnly |
图标按钮固定尺寸,并要求提供 tooltip 或 aria-label |
tooltip |
通过 portal 渲染,默认避开光标位置 |
loading |
禁用按钮并暴露 aria-disabled |
tactile |
覆盖宽高、圆角、阴影、背景和深色模式变量 |
<TactileButton icon="refresh" iconOnly tooltip="刷新" />
<TactileButton variant="primary" icon="save">保存</TactileButton>
<TactileButton variant="danger" icon="delete" iconOnly tooltip="删除" />
variant="neutral" 的默认按钮是白色触感按钮。彩色按钮仍应保留外部投影和统一高度,不应在业务 CSS 中手写新的阴影体系。
有色按钮的边框不能直接使用填充色本身。primary、danger 和后续新增的有色 variant 应使用同色系减淡边框,例如 color-mix(in srgb, var(--tui-danger) 64%, white)。这样边框仍然属于按钮色相,但视觉重量弱于填充面,避免红色/蓝色按钮看起来比默认按钮额外大一圈。hover 态应提亮而不是压暗,背景用当前色混入少量 white,边框继续比背景更轻。
图标预设
预设图标由 tactileIconPresets 统一维护,业务页面通过语义名称调用,避免每个页面随意选择图标。
常用语义:
| 名称 | 用途 |
|---|---|
refresh |
刷新数据 |
save |
保存 |
delete |
删除 |
trigger / collect |
触发采集 |
start / play |
启动 |
stop |
停止 |
connect / test |
连接或连通性测试 |
guide |
凭证教程 |
generate |
AI 生成 |
reset |
恢复默认 |
detail |
查看详情 |
copy |
复制 |
upload |
上传 |
如果预设不足,可以直接传 React 节点:
<TactileButton icon={<MyIcon aria-hidden="true" />} iconOnly tooltip="自定义动作" />
TactileSwitch
开关组件用于二元设置。它不是 iOS 风格大开关,而是与 Docs 主题滑块一致的小型轻触感控件。
<TactileSwitch
checked={enabled}
onCheckedChange={setEnabled}
label="启用 WebSearch"
tooltip="启用 WebSearch"
/>
文案默认进入 tooltip。需要显示文字时,由调用方在布局中单独放 label,不应把长文案塞进 switch 内部。
ControlGroup
ControlGroup 用于把一组按钮按统一间距和对齐方式排列。默认没有灰色底座;只有显式传 withBase 或 baseTactile 时才显示底座。
<ControlGroup align="end" gap={8}>
<TactileButton icon="refresh" iconOnly tooltip="刷新" />
<TactileButton variant="primary" icon="save">保存</TactileButton>
</ControlGroup>
用于列表底部时,按钮应随着列表滚动出现,而不是固定漂浮在列表中部。用于详情页工具栏时,按钮高度和阴影应由组件默认值统一。
TactileTooltip
Tooltip 使用 createPortal 挂到 document.body,支持 top、right、bottom、left 和 collision 修正。默认位置会相对触发元素向右下偏移,避免被鼠标指针挡住。
<TactileTooltip label="刷新" anchor={buttonRef.current} open={open} side="bottom" />
应用层通常不需要直接使用它,TactileButton 和 TactileSwitch 已经内置 tooltip。
Scrollbar
Scrollbar 包裹普通滚动容器。真实滚动仍由内部 viewport 承担,Tactile UI 只提供覆盖式 track/thumb。
<Scrollbar axis="both" minThumbSize={28} autoHide>
<LongContent />
</Scrollbar>
常用 props:
| Prop | 说明 |
|---|---|
axis |
x、y、both |
autoHide |
非 hover/拖拽时自动弱化 |
alwaysVisible |
始终显示滚动条 |
trackSize / thumbSize |
控制轨道和 thumb 尺寸 |
inset / radius |
控制贴边距离和圆角 |
thumbColor / trackColor |
局部颜色覆盖 |
viewportRef |
由外部提供真实滚动节点 |
Textarea 不建议强行套 overlay 滚动条,因为浏览器 resize grip 和文本选择需要原生交互。需要统一外观时,应使用专门的 resizable textarea 样式,让原生 grip 保留可点击区域。
ScrollbarOverlay
ScrollbarOverlay 适合已经存在滚动节点的区域,例如第三方表格或自定义 viewport。它不会创建新的滚动容器,只会监听目标节点并画覆盖式滚动条。
<div ref={hostRef}>
<ThirdPartyTable />
<ScrollbarOverlay containerRef={hostRef} targetSelector=".tui-scroll-target" />
</div>
TableScrollRegion
TableScrollRegion 是表格滚动区域的便利封装。默认目标选择器是 .tui-scroll-target,Admin 会显式传自己的 table viewport selector,避免组件库里出现业务命名。
<TableScrollRegion targetSelector=".table-viewport">
<EntityTable />
</TableScrollRegion>
可访问性
iconOnly按钮必须提供tooltip或aria-label。TactileSwitch使用role="switch"和aria-checked。- tooltip 只是说明,不承担唯一状态表达;状态仍应通过文本、badge 或
aria-*呈现。 - 禁用和 loading 状态会设置
aria-disabled,button 元素会同步disabled。
Admin 迁移约定
Admin 中所有全局工具按钮、详情页工具按钮和列表底部动作应使用 Tactile UI:
- 无歧义动作:
TactileButton iconOnly tooltip - 保存/创建/确认:
TactileButton variant="primary",通常保留文字 - 删除/停止:
TactileButton variant="danger",根据风险决定是否保留文字 - 开关:
TactileSwitch - 长列表、表格、日志、Markdown:
Scrollbar/ScrollbarOverlay/TableScrollRegion
这样做的边界是:组件负责触感、尺寸、阴影、tooltip 和滚动条行为;业务页面负责 API、状态机、权限和文案。