Files
UI/llms-full.txt
T
2026-10-26 23:21:30 +08:00

3237 lines
187 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# @ui-arch 组件库 Agent 文档(无图形版)
> 自动生成于 2026-10-26T15:21:15.323Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。
> 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。
## 快速上手
### @ui-arch/frame
1. 引入:import { signal, computed, effect, h, mount } from '@ui-arch/frame'
2. 声明状态:const count = signal(0);无参调用读值 count(),传参写值 count(1)
3. 渲染视图:mount('#app', () => h('div', null, () => `count = ${count()}`))——函数型子节点即响应式文本
4. 绑定事件:props 里 on 开头且值为函数的键(onClick 等)自动 addEventListener
5. 派生数据:const double = computed(() => count() * 2),视图里直接调用 double(),依赖自动追踪
6. 列表渲染:renderList(tbody, () => rows.map(r => h('tr', null, r.name))),信号变化整体替换子节点
### @ui-arch/table-core
1. 创建实例:const table = createTable({ data, columns, rowKey: 'id', plugins: [filterPlugin(), sortPlugin(), paginationPlugin()] })
2. 取派生数据:table.getVisibleRows() 返回 { rows, total };total 语义固定 = 筛选后、分页前的行数
3. 变更状态:只能 dispatch action,如 table.dispatch({ type: 'toggleSort', key: 'name' });状态不可变
4. 订阅刷新:table.subscribe(fn),fn 里重新 getVisibleRows() 并交给渲染层
5. 分页信息:api.getPageInfo() 返回 { page, pageSize, totalPages };越界页码自动钳制
6. 扩展能力:实现 TablePlugin(derive / patchState / settle / api 四钩子)传入 plugins 即插入管线
### @ui-arch/tokens
1. 引入:在应用入口 import '@ui-arch/tokens/tokens.css'
2. 消费:组件样式一律 var(--ua-color-accent) 这类变量,不写死色值
3. 暗色:根元素设 data-theme=dark,变量值自动切换
4. 扩展:新令牌先同时加进 tokens.css 与 tokens.json,再重新生成文档站
### @ui-arch/ui
1. 安装依赖:pnpm add @ui-arch/ui @ui-arch/frame(workspace 内自动链接)
2. 引入样式:在应用入口 import '@ui-arch/ui/ui.css',暗色主题在根元素设 data-theme='dark'
3. 创建状态:import { signal } from '@ui-arch/frame',如 const name = signal('')
4. 调用组件:const el = textField({ value: name }),把 el 挂到你的容器或配合 h() 组合
5. 读写状态:name() 读、name('张三') 写,界面自动同步,无需手动操作 DOM
### @ui-arch/cli
1. 接手先跑 ua doctor(11 项:node / 依赖 / git 钩子与身份 / 元数据 / 命令面三方一致 / 门禁链完整,秒级),再跑 npm run check 确认基线全绿(build → demo → style → typecheck → smoke → index → verify → lint-iterations → audit:visual → audit:contrast → audit:motion)
2. 查组件:ua list 看全量,ua search <关键词> 按 id/标题/参数名/参数说明/步骤五层 + 配方三层(标题/配方组件/要点)模糊找(命中非 id 标题会标出命中字段,配方命中行直接给 ua recipes <编号> 指路;拼错会回相近 id),ua info <组件id> 看 props/步骤/示例(ua info <包名> 看单包 api 与约定,加 --json 出原始清单);要给文档 / issue / 回复贴组件说明时加 --md,直接拿到标题 + 参数表 + 用法 + 示例的 Markdown 片段
3. 查配方:ua recipes 列全量(编号 / 标题 / 用到的组件),ua recipes <编号> 看骨架代码与要点(--json 给脚本用;MCP 里对应 list_recipes / get_recipe)
4. 查令牌:ua tokens 打印全部 --ua-* 变量(颜色带暗色值),--json 输出结构化数组;写样式前先查,禁止硬编码。令牌分十三组:颜色 / 混色浓度 / 圆角 / 间距 / 字号 / 字重行高 / 边框 / 阴影 / 动效 / 位移 / 焦点环 / 控件高度 / 层级,color-mix 混入浓度用 --ua-alpha-soft/tint/line/mid/strong/heavy,弹层阴影用 --ua-shadow-1..4,过渡用 --ua-duration-* + --ua-ease,transform 位移用 --ua-translate-*(负号 calc(-1 * var(...))),焦点环用 --ua-focus-ring(_inset)
5. 收口令牌面用 ua tokens --unused:列出皮肤样式表与令牌自身都没引用的令牌(当前 3 个:surface-3 / text-secondary / font-size-xl),删之前先确认没有外部消费方在吃它们
6. 改完清单跑 npm run docs:同时刷新人类文档站与 llms-full.txt(Agent 首选喂料)
7. 改完样式跑 npm run audit:visual:各类硬编码计数不得高于 .ua/visual-baseline.json(棘轮只减不增);字面量可用 npm run visual:migrate 幂等改写成令牌
8. 改完颜色令牌跑 npm run audit:contrast:18 组前景/底色配对在亮/暗 × 常规/高对比四套主题下都必须达标(正文 7 / 常规 4.5 / 禁用 2.6 / 非文本 3),跌破即退出码 1——调一个色值就能让全站辅助文字掉到 4.4 或让焦点环糊进背景,人眼看不出但门禁看得出
9. 改完动效跑 npm run audit:motion:带 infinite 的动画必须在 prefers-reduced-motion 块里被关掉,悬停 / 按压的位移(translate)必须归零,缺一条即退出码 1——循环动画漏兜底会把「减少动效」用户整页交给抖动,位移漏兜底则每次悬停都在推搡前庭
10. 在 ~/.workbuddy/mcp.json 注册 ua mcp 后,Agent 可直接调 list_components / get_component / search_components / get_tokens / list_recipes / get_recipe;get_component 传 format: md 拿到与 ua info --md 同款的 Markdown 片段
## @ui-arch/ui(skin-dom)v0.1.0
70 个 DOM 组件,统一用法:组件是普通函数,传入 props 返回 DOM 节点;需要状态时先创建 signal 再双向绑定。引入 CSS:import '@ui-arch/ui/ui.css'(内部已消费 tokens)。
阴影对照表:下拉弹层=shadow-2 / 浮层与悬浮按钮=shadow-3 / 模态弹窗与抽屉及通知=shadow-4 / 卡片与输入组轻浮起=shadow-1;主按钮与强调件的主题色投影=shadow-accent / shadow-accent-strong,组件内不得再手写 box-shadow 字面值。
浮层入场统一:新弹层用 animation: ua-pop-drop var(--ua-duration-fast) var(--ua-ease-out) both(自上而下 4px 淡入),弹层翻到触发器上方时叠 .is-flip 覆盖成 ua-pop-rise(自下而上),不要再为单个组件自造 keyframes——方向与时长各写一份就会出现「翻转后还朝下入场」的错位。
### image — 图片 Image
参数:
- src(必填):string —— 图片地址(缩略图与大图共用)
- alt(默认 ''):string —— 替代文本;缺省空串
- width:number —— 缩略图宽度像素,高度自适应;缺省走样式默认
- fallback(默认 '图片加载失败'):string —— 加载失败时缩略图内显示的占位文案;缺省「图片加载失败」
示例:
```
image({ src: '/a.png', alt: '架构图', width: 160 })
```
手把手:
1. 基础用法只传 src:渲染缩略图,hover 上浮提亮,光标变放大镜
2. 点击缩略图(或聚焦后回车)打开全屏遮罩大图,Esc 或点击遮罩任意处关闭,焦点归还缩略图
3. 宽度用 width(像素)控制,高度按比例自适应,容器内不撑破 max-width
4. alt 尽量传有意义的描述,读屏与图片加载失败时都依赖它
现场演示:apps/docs/demo.html#sec-image
### button — 按钮 Button
参数:
- label(必填):string —— 按钮文案
- variant(默认 primary):'primary' | 'ghost' | 'danger' —— 视觉强度;缺省 primary
- type:'button' | 'submit' | 'reset' —— 原生类型;缺省 button,表单内不会意外提交
- disabled:boolean | Signal<boolean> —— 禁用态:布尔直接置位,传信号则动态同步
- onClick(回调):() => void —— 点击回调
- size:'sm' | 'md' | 'lg' —— 高度档位:sm 紧凑 / md 默认 / lg 大触控目标,走 --ua-control-h-* 三档;缺省 md
- loading(默认 false):boolean | Signal<boolean> —— 加载态:转圈并禁点(aria-busy 播报忙碌);布尔直接置位,传信号则动态同步
示例:
```
button({ label: '提交', variant: 'primary', onClick: () => console.log('clicked') })
```
手把手:
1. 确定语义强度:主操作用 primary(默认),次要操作用 ghost,危险操作用 danger
2. 绑定 onClick 回调处理点击逻辑
3. 需要禁用态时传 disabled(布尔或信号);异步提交用 loading:开始时置 true,完成后置 false,按钮转圈并自动禁点
4. 放进 <form> 里需要提交时显式传 type: "submit";缺省 button 不会触发原生提交
现场演示:apps/docs/demo.html#sec-counter
### textField — 文本输入 TextField
参数:
- value:Signal<string> —— 双向绑定信号:输入写回信号,信号变化写回输入框
- placeholder:string —— 占位文案
- disabled:boolean | Signal<boolean> —— 禁用态:布尔直接置位,传信号则动态同步
- onInput(回调):(value: string) => void —— 输入回调,每次按键触发
- prefix:string | HTMLElement —— 前置附加块(textField/inputNumber 在输入框内侧前、textarea 在上方);缺省不渲染
- suffix:string | HTMLElement —— 后置附加块(textField/inputNumber 在输入框内侧后、textarea 在下方);缺省不渲染
- clearable(默认 false):boolean —— 开启后有值即显示一键清空按钮;缺省 false
- maxCount:number —— 字数上限:同时设原生 maxlength,并在输入框内侧显示实时计数;缺省不限
- size:'sm' | 'md' | 'lg' —— 高度档位:sm 紧凑 / md 默认 / lg 大触控目标,走 --ua-control-h-* 三档;缺省 md
- password:boolean —— 密码框:原生打点渲染,内置「显示/隐藏」切换按钮(aria-label 同步);缺省普通文本
示例:
```
const name = signal(''); const el = textField({ value: name, placeholder: '输入名字…', onInput: (v) => console.log(v) })
```
手把手:
1. 先用 signal('') 创建字符串信号
2. 传入 value 完成双向绑定:用户输入自动写回,外部赋值自动回显
3. 在任意响应式位置用 name() 读取最新值,如 () => `你好,${name()}`
4. 需要图标、货币符、单位等前后缀时传 prefix/suffix(字符串或 DOM),控件会包进带边框的附加组,聚焦光环由组统一承载
5. 需要限制长度就给 maxCount:超限输入被原生拦截,输入框内侧实时显示 n/上限
现场演示:apps/docs/demo.html#sec-bind
### inputNumber — 数字输入 InputNumber
参数:
- value(必填):Signal<number> —— 当前值信号,双向绑定(按钮/键盘写回、外部赋值同步)
- min:number —— 最小值;缺省无下限
- max:number —— 最大值;缺省无上限
- step(默认 1):number —— 步长;缺省 1
- precision:number —— 小数位数:写回与显示都按 toFixed(p) 归一(消除步进浮点尾差,step 0.1 也干净);缺省不处理
- disabled:boolean | Signal<boolean> —— 禁用态:布尔直接置位,传信号则动态同步
- onChange(回调):(value: number) => void —— 值变化回调(已夹取到合法区间)
- prefix:string | HTMLElement —— 前置附加块(textField/inputNumber 在输入框内侧前、textarea 在上方);缺省不渲染
- suffix:string | HTMLElement —— 后置附加块(textField/inputNumber 在输入框内侧后、textarea 在下方);缺省不渲染
- size:'sm' | 'md' | 'lg' —— 高度档位:sm 紧凑 / md 默认 / lg 大触控目标,走 --ua-control-h-* 三档;缺省 md
示例:
```
const qty = signal(1); const el = inputNumber({ value: qty, min: 1, max: 99, onChange: (v) => console.log(v) })
```
手把手:
1. 用 signal(1) 创建数字信号并传入 value 完成双向绑定
2. 点击 −/+ 按钮或在输入框聚焦时按 ↑↓ 以 step 调节;直接输入在失焦或回车时解析
3. 非法输入自动回滚为当前值;min/max 超界自动夹取,disabled 支持布尔或信号禁用
4. 金额、比例这类小数场景传 precision:值与显示都固定小数位,步进不再出现 0.30000000000000004
5. 需要图标、货币符、单位等前后缀时传 prefix/suffix(字符串或 DOM),控件会包进带边框的附加组,聚焦光环由组统一承载
现场演示:apps/docs/demo.html#sec-inputnumber
### textarea — 多行输入 Textarea
参数:
- value:Signal<string> —— 双向绑定信号
- rows(默认 3):number —— 初始行数;缺省 3
- placeholder:string —— 占位文案
- disabled:boolean | Signal<boolean> —— 禁用态:布尔直接置位,传信号则动态同步
- onInput(回调):(value: string) => void —— 输入回调
- prefix:string | HTMLElement —— 前置附加块(textField/inputNumber 在输入框内侧前、textarea 在上方);缺省不渲染
- suffix:string | HTMLElement —— 后置附加块(textField/inputNumber 在输入框内侧后、textarea 在下方);缺省不渲染
- maxCount:number —— 字数上限:同时设原生 maxlength,并在下方附加块显示实时计数;缺省不限
- autosize:boolean | { minRows?, maxRows? } —— 自适应内容高度:true 跟随内容涨落,对象态钳最小/最大行数,超出 maxRows 内部滚动;缺省固定行数
示例:
```
const bio = signal(''); textarea({ value: bio, rows: 4, placeholder: '一句话介绍…' })
```
手把手:
1. 创建信号并传入 value
2. 用 rows 控制初始高度,用户可纵向拉伸
3. 字数统计等派生逻辑直接在响应式文本里读 bio().length
4. 需要图标、货币符、单位等前后缀时传 prefix/suffix(字符串或 DOM),控件会包进带边框的附加组,聚焦光环由组统一承载
现场演示:apps/docs/demo.html#sec-form
### slider — 滑块 Slider
参数:
- value(必填):Signal<number> —— 当前值信号,双向绑定:拖动写回,外部赋值同步位置
- min(默认 0):number —— 最小值;缺省 0
- max(默认 100):number —— 最大值;缺省 100
- step(默认 1):number —— 步长;缺省 1
- disabled:boolean | Signal<boolean> —— 禁用态:布尔直接置位,传信号则动态同步
- showValue(默认 true):boolean —— 是否右侧显示当前值;缺省 true
- marks:{ value: number; label?: string }[] —— 刻度标注:按值定位在轨道下方,label 缺省显示数值;纯装饰不进读屏(数值由原生 input 播报)
- range(默认 false):boolean —— 双滑块区间模式:value 当下限、end 当上限,两拇指之间用强调色连成已选段
- end:Signal<number> —— 区间上限信号;仅 range 模式使用,不传则组件内部维护
- onRangeChange(回调):(low: number, high: number) => void —— 区间两端变更回调,参数已保证 low ≤ high(越界那一端会被压平)
- onChange(回调):(value: number) => void —— 拖动回调
示例:
```
const vol = signal(40); slider({ value: vol, min: 0, max: 100, step: 5, onChange: (v) => console.log(v) })
```
手把手:
1. 创建数值信号(初值在 min/max 区间内),传入 value 完成双向绑定
2. 用 min / max / step 限定范围与粒度,如 step: 5 只取 5 的倍数
3. 拖动或按 ←→ 键改变值:原生 range 自带键盘与触屏交互,无需额外处理
4. 只想看拖动条不显示数字时传 showValue: false;需要自定义展示可自行读 signal
5. 禁用场景传 disabled(布尔或信号),置灰且不可拖
6. 需要在轨道下方标刻度(如 0 / 50 / 100 的档位含义)传 marks:[{ value, label }],按 min-max 换算百分比定位,容器宽度变化时自动跟随
7. 要选数值区间就传 range: true 并给 end 信号:两条原生 range 叠在同一轨道,只有拇指可拖;拖下限越过上限会自动压平,回调走 onRangeChange(low, high)
现场演示:apps/docs/demo.html#sec-slider
### checkbox — 复选框 Checkbox
参数:
- checked:Signal<boolean> —— 勾选状态信号,双向绑定
- label:string —— 右侧文案;缺省只渲染勾选框
- indeterminate:boolean | Signal<boolean> —— 半选态(批量选择时部分子项选中):仅影响视觉与读屏 mixed 播报,不改变 checked 值;点一下即回到全选
- disabled:boolean | Signal<boolean> —— 禁用态:布尔直接置位,传信号则动态同步
- onChange(回调):(checked: boolean) => void —— 变更回调
示例:
```
const agreed = signal(false); checkbox({ label: '同意条款', checked: agreed, onChange: (v) => console.log(v) })
```
手把手:
1. 创建布尔信号 agreed = signal(false)
2. 传入 checked 与 label,勾选即写回信号
3. 表单提交前用 agreed() 判断是否勾选
4. 做全选/子项联动时把「部分选中」折算成 indeterminate(布尔或信号):视觉显示横杠,读屏播报 mixed,点一下即全选
现场演示:apps/docs/demo.html#sec-form
### switchButton — 开关 Switch
参数:
- checked:Signal<boolean> —— 开关状态信号,双向绑定
- ariaLabel:string —— 无可见文案时的可访问名(如纯图标开关);缺省取 label
- label:string —— 左侧文案
- loading:boolean | Signal<boolean> —— 加载中:锁定交互、滑块换成转圈并挂 aria-busy(异步提交防误点);与 disabled 同时生效
- disabled:boolean | Signal<boolean> —— 禁用态:布尔直接置位,传信号则动态同步
- onChange(回调):(checked: boolean) => void —— 变更回调
示例:
```
const night = signal(true); switchButton({ label: '夜间模式', checked: night })
```
手把手:
1. 创建布尔信号并传入 checked
2. 轨道与滑块的动画由 CSS 完成,无需干预
3. 状态变化在 onChange 里做副作用(如切换主题)
4. 切换要等异步请求就传 loading(布尔或信号):请求期间滑块转圈、不可点,完成后自动恢复
现场演示:apps/docs/demo.html#sec-form
### select — 下拉选择 Select(自绘弹层)
参数:
- options(必填):OptionItem[] —— 选项列表,每项 { value, label };每项可加 group 指定分组小标题
- value:Signal<string> —— 选中值信号,双向绑定
- placeholder:string —— 未选中时触发器显示的占位文案
- searchable:boolean —— 弹层顶部带搜索框,按标签包含匹配过滤;缺省 false
- clearable:boolean —— 有值时触发器右缘出现清空按钮(收起态按 Delete / Backspace 也能清);缺省 false,用于「可选筛选项」
- disabled:boolean | Signal<boolean> —— 禁用态:布尔直接置位,传信号则动态同步;禁用时触发器置灰且不展开弹层
- ariaLabel:string —— 触发器的可访问名:没有可见 label 关联时(如表格分页条的「每页条数」)给读屏一个名字;缺省不落 aria-label
- onChange(回调):(value: string) => void —— 选中回调
- size:'sm' | 'md' | 'lg' —— 高度档位:sm 紧凑 / md 默认 / lg 大触控目标,走 --ua-control-h-* 三档;缺省 md
示例:
```
const city = signal(''); select({ options: [{ value: 'bj', label: '北京' }], value: city, placeholder: '选择城市…' })
```
手把手:
1. 准备 options 数组,value 是提交用值、label 是展示文案
2. 创建信号并传入 value;未传时组件内部自持状态
3. 点击触发器展开弹层,点选项写回信号并收起;点外部或按 Esc 也会收起
4. 选中项在弹层中用淡色弱化标记,不抢视觉焦点
5. 弹层展开时按 Esc 只收起下拉并回焦触发器;若下拉在弹窗/抽屉内,再按一次 Esc 才关外层(逐层关闭)
6. searchable: true 时打开即聚焦搜索框,输入实时过滤;↑↓/Home/End 只在可见项间移动,无命中显示「无匹配选项」
7. 需要允许「不选」的筛选项就开 clearable:有值时右缘出现 ×,点它把值写成空串并回调 onChange(''),键盘在收起态按 Delete / Backspace 等价
8. 候选多时给选项加 group:相邻同组合成一段、组首插灰色小标题(段间有分隔线),过滤后整组无命中时标题一起隐藏——搜索路径与键盘 ↑↓ 仍只走真实选项
9. 没有可见 label 的场合(工具栏、分页条)传 ariaLabel:触发器只显示当前值文案,读屏需要「这个下拉在选什么
10. 弹层展开期间滚动页面或任意滚动容器,会按视口重算上下翻转(与 tooltip 共用 position.ts 的 trackShift);收起即摘监听
现场演示:apps/docs/demo.html#sec-form
### menu — 下拉菜单 Menu
参数:
- label(必填):string —— 触发器文案
- items(必填):MenuItem[] —— 菜单条目:{ value, label, disabled?, danger?, divider?, shortcut? }
- variant(默认 ghost):'primary' | 'ghost' —— 触发器视觉强度;缺省 ghost
- align(默认 start):'start' | 'end' —— 弹层对齐方向;缺省 start(与触发器左对齐)
- onSelect(回调):(value: string) => void —— 选中回调,禁用项不触发
示例:
```
menu({ label: '操作', items: [{ value: 'edit', label: '编辑' }, { value: 'del', label: '删除', danger: true }], onSelect: (v) => console.log(v) })
```
手把手:
1. 准备 items 数组:value 是回调标识、label 是展示文案,插一条 { divider: true } 即可分出条目组
2. 传 label 决定触发器文案;默认 ghost 轻量样式,主操作用 variant: 'primary'
3. 点触发器展开弹层,点条目收起并触发 onSelect;点外部或按 Esc 也会收起(Esc 后焦点回到触发器)
4. 条目要给快捷键提示时传 shortcut(如 '⌘K'):右侧灰字展示,组件不绑定按键,实际快捷键由调用方在全局监听
5. 键盘:Tab 聚焦触发器,↑↓ 展开并在可用条目间环形移动,Enter / Space 选中;disabled 项自动跳过
6. 靠右对齐避免溢出时传 align: 'end';危险操作给条目加 danger: true
7. 展开时按 Esc 只收起菜单并回焦触发器,不会连带关闭所在弹窗(逐层关闭)
现场演示:apps/docs/demo.html#sec-menu
### radioGroup — 单选组 RadioGroup
参数:
- options(必填):OptionItem[] —— 选项列表
- value:Signal<string> —— 选中值信号,双向绑定
- disabled:boolean | Signal<boolean> —— 整组禁用:全部选项与文案置灰且不可选
- onChange(回调):(value: string) => void —— 变更回调
示例:
```
const size = signal('m'); radioGroup({ options: [{ value: 's', label: '小' }, { value: 'm', label: '中' }], value: size })
```
手把手:
1. 创建字符串信号,初值设为默认选项的 value
2. 传入 options 与 value,组内自动互斥
3. 读取 size() 获取当前选中值
4. 需要整组禁用时传 disabled:布尔值静态禁用,也可传信号(如 locked)实现动态锁定
现场演示:apps/docs/demo.html#sec-form
### accordion — 折叠面板 Accordion
参数:
- items(必填):AccordionItem[] —— 面板列表:{ value, title, content, disabled? }
- active:Signal<string> —— 受控展开面板信号(单开模式);缺省由组件内部持有状态
- multiple(默认 false):boolean —— 允许多个面板同时展开;缺省 false(手风琴,单开)
- onChange(回调):(active: string) => void —— 展开变化回调:收起时传空串
示例:
```
accordion({ items: [{ value: 'a', title: '账户设置', content: '内容…' }, { value: 'b', title: '通知', content: h('p', null, '…') }] })
```
手把手:
1. 准备 items:value 作标识、title 作标题、content 可为字符串 / DOM 节点 / 响应式函数
2. 默认手风琴模式:展开一个自动收起其他;需要同时展开多个时传 multiple: true
3. 需要外部控制或读取展开项时传 active 信号(受控单开),否则组件内部自持状态
4. 标题是原生 button:Tab 聚焦、Enter / Space 切换,展开态带 aria-expanded + region 关联
5. 内容区用 grid 行高 0fr → 1fr 过渡,无需测量高度;reduced-motion 用户自动静态切换
现场演示:apps/docs/demo.html#sec-accordion
### breadcrumb — 面包屑 Breadcrumb
参数:
- items(必填):BreadcrumbItem[] —— 路径条目:{ label, href? },末项自动视为当前页
- separator(默认 /):string —— 分隔符;缺省 '/'
- onNavigate(回调):(item, index) => void —— 无 href 条目的点击回调
- maxItems:number —— 最多展示几项:超出时首项之后折叠成「…」按钮(aria-label 报出折叠条数),点开即完整展开;缺省不折叠
示例:
```
breadcrumb({ items: [{ label: '首页', href: '/' }, { label: '设置' }, { label: '账户' }], onNavigate: (it) => console.log(it.label) })
```
手把手:
1. 按层级顺序准备 items,最后一项即当前页,自动渲染为不可点的灰色文字并带 aria-current='page'
2. 需要真实跳转就给条目加 href(渲染 <a>);单页应用想自行接管时不给 href,改用 onNavigate 回调
3. 改分隔符传 separator,如 '›';分隔符对读屏隐藏
4. 语义结构为 nav + ol + li,可直接放进页头或卡片标题栏
5. 路径过深时传 maxItems(如 4):首项 + 「…」+ 末几项,点「…」原地展开全部,末项始终是当前页
现场演示:apps/docs/demo.html#sec-breadcrumb
### card — 卡片 Card
参数:
- title:string —— 标题;缺省不渲染标题行
- ...children:unknown[] —— 任意子节点:字符串 / DOM / 响应式函数
- extra:unknown —— 标题行右侧操作区(按钮 / 统计文案);缺省不渲染
- footer:unknown —— 底部操作区;缺省不渲染
- bordered:boolean —— 是否显示边框;缺省 true
- hoverable:boolean —— 悬停浮起(可点击卡片);缺省 false
示例:
```
card({ title: '用户表' }, dataTable({ data, columns }))
```
手把手:
1. card 是容器组件:第一个参数是 props,其余参数是任意子节点
2. 传 title 显示标题行,否则只有内容区
3. 子节点可以混合字符串、DOM 节点与响应式函数
4. title 与 extra 同处标题行:有 extra 时右侧对齐,没标题也能只放操作区
5. footer 渲染在内容之后的底部区,右对齐并带上分隔线,适合放主次按钮
6. bordered:false 去边框(嵌在已有边界的容器里),hoverable 让整卡悬停浮起(自带 reduced-motion 兜底)
现场演示:apps/docs/demo.html#sec-card
### badge — 徽标 Badge
参数:
- label:string —— 徽标文案;与 count / dot 三选一,dot 时作可访问名
- variant:'default' | 'success' | 'warning' | 'danger' —— 语义色;缺省 default
- count:number —— 数字角标:超过 max 显示 N+,负数归零
- max:number —— 数字上限;缺省 99
- dot:boolean —— 只显示小红点(无文字),role=img + label 作名称
示例:
```
badge({ label: '成功', variant: 'success' })
```
手把手:
1. 纯状态文案直接传 label,配 variant 定语义色
2. 要显示数量传 count(如未读数),超过 max(缺省 99)自动渲染成 99+
3. 只提示「有更新」用 dot: true,配合 label 给出读屏名(如「有未读消息」)
4. 不要给 badge 挂点击:它不是按钮(无 tabindex 也无键盘响应);「点角标进消息中心」应当把 badge 塞进 button 里,让按钮承担语义
现场演示:apps/docs/demo.html#sec-feedback
### tag — 标签 Tag
参数:
- label(必填):string —— 标签文案
- variant(默认 default):'default' | 'success' | 'warning' | 'danger' | 'info' —— 语义色;缺省 default
- closable(默认 false):boolean —— 显示 × 关闭按钮;缺省 false(纯展示)
- onClose(回调):() => void —— 关闭回调:调用方从数据源移除该项后重新渲染
- checkable(默认 false):boolean —— 可选中:渲染为筛选片(button + aria-pressed);与 closable 互斥,同时传以 checkable 为准
- checked:Signal<boolean> —— 选中态信号,双向绑定(点击写回、外部赋值同步);不传则组件内部自持
- onChange:(checked: boolean) => void —— 选中态变化回调,参数是点击后的新值
示例:
```
tag({ label: '前端', variant: 'info', closable: true, onClose: () => remove('前端') })
```
手把手:
1. 纯展示用 badge,需要可移除的标签(筛选项 / 已选条件)用 tag
2. 按语义选 variant:info 中性标记、success 通过、warning 待处理、danger 已失效
3. closable: true 时必须配 onClose:把数据源里的该项删掉再重渲染(组件无状态,不自行摘除节点)
4. 组合用法:容器里用 renderList 按信号数组渲染 tag 列表,删除即写回信号
5. 标签是内联元素,可直接排进 .row 等容器,自动换行由容器决定
6. 需要可点的筛选片时传 checkable + checked:点击写回信号,视觉与 aria-pressed 同步,不做整块重渲染(焦点不丢)
现场演示:apps/docs/demo.html#sec-tag
### avatar — 头像 Avatar
参数:
- name(必填):string —— 名称:生成占位首字,并作为图片 alt 与悬停标题
- src:string —— 图片地址;缺省用首字占位
- size(默认 32):number —— 直径像素;缺省 32
- variant(默认 default):'default' | 'success' | 'warning' | 'danger' —— 占位底色语义档;缺省 default
示例:
```
avatar({ name: '张三' }); avatar({ name: 'Alice', size: 40, variant: 'success' })
```
手把手:
1. 只传 name:用名称首字做占位(中文取首字、英文大写),底色走 accent 淡化,暗色自动适配
2. 有图片就传 src:渲染 img 并用 name 作 alt,读屏语义完整;图片缺失时由浏览器 alt 兜底
3. 尺寸用 size(像素)控制,头像不参与布局拉伸(flex:none),可安全排进 .row 与表格单元格
4. 需要区分类别(在线/告警)时用 variant 换底色档,颜色一律来自令牌
现场演示:apps/docs/demo.html#sec-avatar
### alert — 提示条 Alert
参数:
- message(必填):string —— 提示内容
- title:string —— 加粗标题行;缺省只渲染正文
- variant(默认 info):'info' | 'success' | 'warning' | 'danger' —— 语义色;缺省 info
- closable(默认 false):boolean —— 显示 × 关闭按钮,退场后从 DOM 移除;缺省 false
- onClose(回调):() => void —— 关闭回调,供调用方同步业务状态
- banner(默认 false):boolean —— 通栏形态:去圆角、只保留下边框、左右撑满,适合页面顶部通知
- showIcon(默认 false):boolean —— 显示语义图标(info ℹ / success ✓ / warning ! / danger ✕),纯装饰不进读屏
- icon:string | HTMLElement —— 自定义图标节点,传了即覆盖语义图标
示例:
```
alert({ message: '操作可能存在风险', variant: 'warning' })
```
手把手:
1. 按信息级别选 variant:info 提示、success 成功、warning 警告、danger 错误
2. 传入 message 即可,入场自带下滑动画
3. 需要可关闭或带操作按钮时,外层用 h() 组合 button 自行拼装,alert 只负责语义底色
4. 需要用户可自行消除的提示传 closable: true:点 × 淡出并从 DOM 移除,配合 onClose 同步业务状态
5. 有标题与正文两层信息时传 title(加粗首行),正文仍走 message
6. banner: true 出通栏形态(圆角与左右边框归零),页面顶部通知直接铺满容器即可,不需要额外包裹层
现场演示:apps/docs/demo.html#sec-alert
### skeleton — 骨架屏 Skeleton
参数:
- variant(默认 rect):'text' | 'rect' | 'circle' —— 占位形状;缺省 rect(整块矩形)
- rows(默认 3):number —— text 形状的行数;缺省 3,末行自动缩短模拟段落
示例:
```
skeleton({ variant: 'text', rows: 3 }); skeleton({ variant: 'circle' }); skeleton({ variant: 'rect' })
```
手把手:
1. 数据加载期间用 skeleton 顶替真实内容,加载完成后整体替换
2. 文本段落用 variant: 'text' + rows 控制行数,头像用 'circle',卡片/图片区域用 'rect'
3. 占位块自带扫光动画,reduced-motion 用户自动静态化为纯灰块
4. 组合布局:skeleton 是普通 DOM,可放进 card / row 等任意容器
现场演示:apps/docs/demo.html#sec-skeleton
### toast — 消息提示 Toast
参数:
- message(必填):string —— 提示文案
- duration(默认 3000):number —— 自动消失毫秒;缺省 3000
- closable(默认 false):boolean —— 显示关闭按钮,点 × 立即收起;缺省 false(仅可关闭的 toast 接收指针事件,其余保持穿透)
- variant(默认 info):'info' | 'success' | 'warning' | 'danger' —— 语义色;缺省 info(toast.error 对应 danger)
- actionLabel:string —— 操作按钮文案(如「撤销」);传了才渲染,点后 toast 立即收起
- onAction(回调):() => void —— 操作按钮回调,在 toast 收起之后触发(撤销删除之类)
示例:
```
toast.success('保存成功'); toast.error('网络请求失败', 5000)
```
手把手:
1. 直接调用语义工厂:toast.info / success / warning / error(message, duration?),无需先建容器
2. 不传 duration 时默认停留 3 秒后自动淡出并移除节点
3. 多条消息在右上角自动纵向堆叠,互不遮挡
4. 需要完整入参(含 variant)时用 showToast(props);组件挂载容器由内部懒创建,样式随 ui.css 一起引入
5. 同屏最多 4 条,超出自动挤掉最早一条;需要用户主动确认的消息用 showToast({ closable: true }) 给个 ×
6. 要「撤销」这类操作时给 showToast 传 actionLabel 与 onAction:按钮出现在文案右侧,点完先收起再回调,duration 建议放到 5000 让用户来得及点
7. 同屏最多 4 条:第 5 条进来时最早一条走正常退场(淡出后移除)而不是瞬间消失;要不被挤掉就传 duration: 0 让它常驻,靠 closable 手动关
现场演示:apps/docs/demo.html#sec-toast
### tooltip — 悬浮提示 Tooltip
参数:
- content(必填):string —— 提示文案(纯文本)
- placement(默认 top):'top' | 'bottom' | 'left' | 'right' —— 气泡方位;缺省 top,出场时按视口剩余空间自动翻到对侧(水平方位按宽度判左右)
- delay(默认 150):number —— 出场延迟毫秒;缺省 150,传 0 立即显示
- trigger(必填):HTMLElement —— 第二个参数:被包裹的触发器元素(按钮 / 输入框等任意 DOM)
示例:
```
tooltip({ content: '保存后立即生效' }, button({ label: '保存', onClick: save }))
```
手把手:
1. 把提示要挂在的元素作为第二个参数传入,返回的包裹节点替换原位置(气泡绝对定位,不占布局)
2. 鼠标悬停或键盘聚焦后延迟 delay 毫秒出场,移出 / 失焦 / Esc 收起;空间不足时自动翻到另一侧
3. 触摸屏没有 hover:手指按下即出场,抬手后停留 1.2 秒再收起,免得手指正挡着就消失
4. 气泡设了 pointer-events: none,不会挡住下面的点击;aria-describedby 已关联,读屏能念出提示文案
5. 方位支持上下左右四向:贴边或工具栏竖排时用 left / right,出场时按该方向剩余空间自动翻到对侧
6. 出场期间滚动页面或任意滚动容器,气泡会按视口重算上下翻转(rAF 节流,与 select 等浮层共用同一套 trackShift);收起后监听立即摘掉,不在关闭态空转
现场演示:apps/docs/demo.html#sec-tooltip
### popover — 气泡卡片 Popover
参数:
- content(必填):unknown —— 弹层内容:字符串 / DOM 节点 / 响应式函数,可放按钮等交互元素
- title:string —— 弹层标题;缺省不渲染标题行
- placement(默认 bottom):'top' | 'bottom' —— 弹层方位;缺省 bottom(触发器下方)
- trigger(默认 click):'click' | 'hover' —— 触发方式;缺省 click
- delay(默认 150):number —— hover 模式出场延迟毫秒;缺省 150
- onOpenChange(回调):(open: boolean) => void —— 开合回调
- triggerEl(第二个参数)(必填):HTMLElement —— 第二个参数:被包裹的触发器元素
- open:Signal<boolean> —— 受控开合信号;不传则内部维护,传了外部可编程开合(popconfirm 即基于它)
示例:
```
popover({ title: '筛选条件', content: h('div', null, '任意可交互内容') }, button({ label: '筛选' }))
```
手把手:
1. 创建触发器元素,再用 popover(props, trigger) 包一层:返回带弹层的包裹元素
2. 点击触发器开合;点弹层外部或按 Esc 收起(Esc 后焦点回触发器)
3. content 里可以放 button / checkbox 等交互元素(tooltip 只能纯文本),点击弹层内部不会误关
4. hover 场景传 trigger: 'hover' + delay 控制多快出场;placement: 'top' 让弹层往上翻
5. 需要感知开合做副作用(如懒加载数据)时用 onOpenChange
6. 展开时按 Esc 只收起气泡并回焦触发器,不会连带关闭所在弹窗(逐层关闭)
现场演示:apps/docs/demo.html#sec-popover
### spinner — 加载指示器 Spinner
参数:
- size(默认 18):number —— 直径像素;缺省 18
- label:string —— 读屏播报文案,如「正在加载订单」;缺省「加载中」
示例:
```
spinner(); spinner({ size: 26 })
```
手把手:
1. 数据加载时插入 spinner(),加载完成移除
2. 用 size 匹配上下文尺寸(按钮内用小号,整页 loading 用大号)
3. 与 skeleton 分工:已知内容结构用 skeleton 占位,只有『在跑』的短等待用 spinner
4. 区块级加载给 spinner 传 label 说明在加载什么,读屏才不会只念一句「加载中」
现场演示:apps/docs/demo.html#sec-feedback
### progress — 进度条 Progress
参数:
- value:Signal<number> —— 当前进度 0-100 的信号;不传即不确定态(未知时长的加载)
- type(默认 line):'line' | 'circle' —— 形态:线性条或环形;环形需同时传 value(不确定态仍走线性条)
- showText:boolean —— 是否右侧显示百分比;缺省 true(不确定态忽略)
- label:string —— 可访问名,如「上传进度」;缺省不确定态念「进行中」
- variant:'default' | 'success' | 'warning' | 'danger' —— 填充语义色;缺省 default(满格自动切 success)
- format(回调):(v: number) => string —— 文本自定义:传入则线/环形的进度文本用其返回值替代默认「N%」;缺省取整百分比
示例:
```
const pct = signal(0); progress({ value: pct }); setInterval(() => pct((pct() + 5) % 105), 400)
```
手把手:
1. 传 value 信号给定量进度,不传则退化为往复的不确定态
2. label 写清这是什么进度,读屏会把名称与百分比一起念出来
3. 失败或告警用 variant: 'danger' / 'warning',该颜色在满格时也不被 success 覆盖
4. 看板 / 上传卡片要环形进度就传 type: 'circle'(同时必须传 value):SVG 双圆按 dashoffset 出弧,中间自动显示百分比,语义色同 variant
现场演示:apps/docs/demo.html#sec-feedback
### tabs — 标签页 Tabs
参数:
- tabs(必填):TabItem[] —— 页签列表,每项 { value, label, disabled?, closable? }
- active:Signal<string> —— 当前激活页签 value 信号,双向绑定
- onChange(回调):(value: string) => void —— 切换回调
- onClose(回调):(value: string) => void —— 关闭页签回调:组件不自行移除,凭 value 从数据源里摘掉后重绘
- extra:unknown —— 页签栏右侧操作区内容(按钮 / 链接):与页签同一行、共用一条分隔线;缺省不渲染且页签栏结构不变
- closable:boolean —— 页签项上的开关:true 时渲染关闭按钮(×),关闭按钮不进 Tab 序但可用鼠标点;缺省不可关
示例:
```
const tab = signal('a'); tabs({ tabs: [{ value: 'a', label: '概览', closable: true }, { value: 'b', label: '明细', disabled: true }], active: tab, onClose: (v) => toast.info(`关闭 ${v}`) })
```
手把手:
1. 创建信号 tab = signal('a'),初值为默认页签
2. tabs 只渲染页签头;页签内容用响应式函数按信号切换,如 () => tab() === 'a' ? panelA : panelB
3. 点击或方向键(←→ 环形)、Home/End(直达首/末页签)切换,滑动指示条自动跟随;disabled: true 的页签置灰不可点
4. 页签多到一屏放不下(页签条横向溢出)时,切到被裁掉的页签会自动滚回可视区:只滚页签条本身、不带动页面,系统开启「减弱动效」时直接跳位;已完整可见则不动,不打断用户手动滚动
5. 需要可关闭页签时给该项加 closable: true,× 或聚焦后按 Delete 触发 onClose(value);组件不改数据源,调用方从数组里摘掉该项再重绘
6. 键盘可访问:Tab 聚焦激活页签,←→ 在启用页签间环形移动,Home/End 跳首个/末个启用页签
7. 页签栏右侧要放「新建 / 设置」这类操作就传 extra(按钮或链接):它与页签同一行共用一条分隔线,不传时 DOM 结构保持不变
现场演示:apps/docs/demo.html#sec-tabs-modal
### stepper — 步骤条 Stepper
参数:
- steps(必填):StepperItem[] —— 步骤列表,每项 { label, description? }
- active(必填):Signal<number> —— 当前步索引(0 基)信号,双向绑定:回跳写回、外部赋值同步
- clickable:boolean —— 已完成步骤是否可点击回跳;缺省 false
- direction(默认 'horizontal'):'horizontal' | 'vertical' —— 排列方向:horizontal 横排连接线在节点之间;vertical 竖排、连接线落在圆点下方,已走完的一段用主色
- onChange(回调):(index: number) => void —— 步骤切换回调(点击回跳触发)
示例:
```
const step = signal(1); stepper({ steps: [{ label: '下单' }, { label: '支付' }, { label: '完成' }], active: step, clickable: true })
```
手把手:
1. 用 signal(0) 创建当前步索引,传入 active;索引之前的步骤显示 ✓ 已完成态
2. 流程推进时外部赋值即可:step(step() + 1),组件三态自动跟随
3. clickable: true 时已完成步骤可点击回跳并写回信号,配合 onChange 同步业务状态
4. 侧栏或移动端传 direction: 'vertical':圆点成一列、说明文字紧贴右侧,步与步之间由圆点下方的竖线相连(末步不画线,已完成的段用主色标进度)
现场演示:apps/docs/demo.html#sec-stepper
### rate — 评分 Rate
参数:
- value(必填):Signal<number> —— 当前评分(1 基,allowHalf 时 0.5 步进)信号,双向绑定:点选/键盘写回、外部赋值同步
- max(默认 5):number —— 星标总数;缺省 5
- allowHalf(默认 false):boolean —— 允许半星:点击星标左半区记 0.5 分,键盘步进同为 0.5;缺省整数
- disabled:boolean | Signal<boolean> —— 禁用态:布尔直接置位,传信号则动态同步
- onChange(回调):(value: number) => void —— 评分变化回调
- allowClear:boolean —— 允许清除:再次点击当前分值(半星须精确相等)清零;缺省不可清
示例:
```
const score = signal(0); rate({ value: score, max: 5, onChange: (v) => console.log(v) })
```
手把手:
1. 用 signal(0) 创建评分信号并传入 value;0 表示未评分
2. 点击星标或聚焦后按 →/↑ 加一分、←/↓ 减一分;悬停高亮预览、移出还原
3. max 可调整星标总数;disabled 支持布尔或信号禁用
4. 要支持半星就传 allowHalf:点星标左半区记 0.5 分,悬停位置实时预览半格填充
现场演示:apps/docs/demo.html#sec-rate
### empty — 空状态 Empty
参数:
- title:string —— 主标题;缺省不渲染
- description:string —— 补充说明;缺省不渲染
- icon:unknown —— 图标区内容(字符串或图标 DOM);缺省渲染 ⌀ 占位符号,装饰性元素不进无障碍树
- action:unknown —— 操作区内容(常见为按钮);缺省不渲染
示例:
```
empty({ title: '暂无数据', description: '调整筛选条件后再试', action: button({ label: '重置筛选', variant: 'ghost' }) })
```
手把手:
1. 在列表 / 表格 / 筛选结果为空时渲染 empty 占位
2. title 一句话说明状态,description 补充原因或建议
3. action 传入 DOM(如 ghost 按钮)引导用户下一步操作
4. 要换成自家插图就传 icon(字符串或 SVG 节点):它固定 aria-hidden,文案仍由 title / description 承担
现场演示:apps/docs/demo.html#sec-empty
### divider — 分隔线 Divider
参数:
- label:string —— 居中文案标签;缺省纯线条
- vertical:boolean —— 是否垂直分隔线(用于行内工具栏);缺省水平
- dashed:boolean —— 是否虚线;缺省实线
示例:
```
divider({ label: '更多信息' }) / divider({ vertical: true })
```
手把手:
1. 在区块之间插入 divider 划分内容层次
2. 传 label 在水平线上显示居中文案,如「更多信息」
3. 工具栏内用 vertical: true 的行内竖线,dashed: true 切虚线弱化分隔感
4. 分割线是纯视觉件:没有 role 也没有焦点,读屏不会播报它。真要让读屏理解分组,用 heading 或 section,别指望横线
现场演示:apps/docs/demo.html#sec-divider
### space — 间距容器 Space
参数:
- direction:'horizontal' | 'vertical' —— 主轴方向;缺省 horizontal 横排
- size:'sm' | 'md' | 'lg' | number —— 子元素间距:三档走 --ua-space-* 令牌,数字按像素;缺省 md
- align:'start' | 'center' | 'end' | 'baseline' —— 交叉轴对齐;缺省 center,按钮与文字混排不错位
- justify:'start' | 'center' | 'end' | 'between' —— 主轴分布;缺省 start,between 两端撑开
- wrap:boolean —— 空间不足是否换行;缺省 false
- block:boolean —— 是否占满父容器宽度;缺省按内容收缩
- split:string —— 子元素之间的分隔符(如 · / |):纯装饰、aria-hidden,仅 2 个以上子元素时插入
示例:
```
space({ size: 'lg' }, button({ label: '保存' }), button({ label: '取消', variant: 'ghost' })) / space({ direction: 'vertical', align: 'start' }, ...)
```
手把手:
1. 把一排需要统一间距的元素交给 space 托管,不再各自写 margin
2. 调间距:size 传 sm/md/lg 走令牌,需要精确像素时直接给数字
3. 竖排用 direction: 'vertical',配 align: 'start' 让子元素左对齐
4. 工具栏两端撑开用 justify: 'between' + block: true;窄屏放不下时加 wrap
5. 一行里要塞分隔符(面包屑式「A · B · C」)传 split: '·':分隔符由容器插入且读屏跳过,不要手写 span 再逐个调 margin
现场演示:apps/docs/demo.html#sec-space
### pagination — 分页器 Pagination
参数:
- page(必填):Signal<number> —— 当前页码信号,双向绑定:外部赋值自动翻页,点击页码写回信号
- pageSize(必填):number —— 每页条数
- total(必填):number | Signal<number> —— 总条数;传信号则总量变化时自动重算页数(并把越界页码钳回并回调 onChange)
- simple(默认 false):boolean —— 极简模式:只留上一页 / 下一页与「当前 / 总页」文字,适合表格工具栏等窄位
- hideOnSinglePage(默认 false):boolean —— 只有一页时整个分页器隐藏(不占布局位)
- onChange(回调):(page: number) => void —— 翻页回调
- jumpable(默认 false):boolean —— 末尾显示跳页输入框(总页数 > 1 才出现);缺省 false
- pageSizes:number[] —— 每页条数候选(如 [10, 20, 50]);传了才渲染条数切换器
- onPageSizeChange(回调):(size: number) => void —— 切换每页条数回调,同时自动回到第 1 页
示例:
```
const page = signal(1); pagination({ page, pageSize: 3, total: 24, onChange: (p) => console.log(p) })
```
手把手:
1. 创建页码信号 page = signal(1),与 pageSize / total 一起传入
2. 页码 ≤7 全部展开;更多时只保留首尾与当前页 ±1,缺口折叠为省略号
3. 首末页自动禁用 ‹ › 翻页按钮,当前页禁点并标记 aria-current
4. 与 dataTable 联动:把同一个 page 信号传给 dataTable 的 page 参数,两处翻页自动同步
5. 键盘可达:全部是原生 button,Tab 聚焦后 Enter / Space 触发
6. pageSizes 传候选数组即出现「N 条/页」切换器,切换后自动回第 1 页;onPageSizeChange 拿新条数重算数据
7. 空间紧的地方传 simple:页码区换成「3 / 5」文字 + 两个箭头(文字带 aria-live,翻页会被读屏播报);数据少到只有一页时再传 hideOnSinglePage,整块自动消失而不是留个灰掉的 1
8. 总量会变(筛选 / 删除后)就把 total 传信号:页码越界时组件自动钳到最后一页并回调 onChange,父级 state 不会停在空页
现场演示:apps/docs/demo.html#sec-pagination
### modal — 弹窗 Modal
参数:
- open(必填):Signal<boolean> —— 开关信号:true 显示 / false 隐藏
- title(必填):string —— 标题
- size(默认 md):'sm' | 'md' | 'lg' —— 宽度档位:sm 340px / md 420px / lg 640px;缺省 md
- footer:unknown —— 底部操作区内容(常见为按钮组,右对齐);缺省不渲染;给了它就用自定义页脚,内置按钮组不再渲染
- confirmText:string —— 内置页脚:确认按钮文案;缺省「确定」(不传 footer 且有 onConfirm 时也渲染内置页脚)
- cancelText:string —— 内置页脚:取消按钮文案;缺省「取消」,点击走 close 流程
- onConfirm(回调):() => void | Promise<void> —— 内置页脚:确认回调;返回 Promise 时按钮变「处理中…」并禁用,成功自动收窗、失败留在窗口重试
- closeOnOverlay:boolean —— 点遮罩是否关闭弹窗;缺省 true,表单类可传 false 防误关(× 与 Esc 仍可用)
- onClose(回调):() => void —— 关闭回调(点遮罩、× 或 Esc 触发)
- ...children:unknown[] —— 弹窗内容子节点
示例:
```
const open = signal(false); modal({ title: '确认操作', open, onClose: () => open(false) }, h('p', null, '内容'))
```
手把手:
1. 创建布尔信号 open = signal(false),弹窗初始隐藏
2. 把 open 传给 modal,其余参数为弹窗内容子节点
3. 打开:open(true);关闭:open(false),点遮罩、× 或 Esc 会自动写回 false;多弹层叠加时 Esc 只关最上层(后开先关)
4. 入场自带遮罩淡入 + 弹窗上浮动画
5. 底部操作按钮放进 footer(如取消/确定),需要更宽内容用 size: 'lg',窄确认框用 'sm'
6. 打开后 Tab 焦点锁在弹窗内,关闭自动把焦点还给打开前的元素
7. 普通确认框不必手拼 footer:传 confirmText / onConfirm 即得「取消 + 确认」按钮组;onConfirm 写成 async,组件会在等待期间把按钮置灰改文案,resolve 后自己收窗、reject 时保持打开供重试
现场演示:apps/docs/demo.html#sec-tabs-modal
### drawer — 抽屉 Drawer
参数:
- title(必填):string —— 标题
- open(必填):Signal<boolean> —— 开关信号:true 滑出 / false 收起
- placement(默认 right):'left' | 'right' | 'top' | 'bottom' —— 滑出方向;缺省 right
- width(默认 320):number —— 面板尺寸(像素):左右滑出为宽度,上下滑出为高度;缺省 320
- footer:unknown —— 底部操作区内容;缺省不渲染
- closeOnOverlay:boolean —— 点遮罩是否关闭抽屉;缺省 true(× 与 Esc 始终可用)
- modal:boolean —— 是否模态:false 时无遮罩、不锁背景滚动、不困焦点,页面仍可操作(检查器 / 详情面板);缺省 true
- onClose(回调):() => void —— 关闭回调(点遮罩 / × / Esc 触发)
- ...children:unknown[] —— 面板内容子节点
示例:
```
const open = signal(false); drawer({ title: '详情', open, width: 360, onClose: () => open(false) }, h('p', null, '内容'))
```
手把手:
1. 创建布尔信号 open = signal(false),传给 drawer;其余参数为面板内容子节点
2. 打开后从侧边滑出:placement: 'right'(默认)或 'left',width 控制宽度(窄屏自动收到 92vw)
3. 点遮罩、× 或按 Esc 关闭并写回 false;打开时焦点落在面板首个可交互元素,Esc 只关最上层弹层
4. 底部操作按钮放进 footer;与 modal 的分工:modal 是强打断的确认,drawer 保留主视图上下文做详情/编辑
5. 面板内容与 modal 一样接受任意 DOM 与响应式函数
6. 要「一边看一边改」就把 modal 设成 false:遮罩透明且不吃点击、背景滚动不锁、Tab 不被困在面板里,× 与 Esc 仍能关
现场演示:apps/docs/demo.html#sec-drawer
### form — 表单 Form / 表单字段 FormItem
参数:
- layout(默认 vertical):'vertical' | 'horizontal' —— 布局方向;缺省 vertical(horizontal 为标签左置)
- columns(默认 1):1 | 2 | 3 —— 字段列数:>1 时字段走栅格铺列(沿用 grid 的窄屏单列),按钮区始终整行;缺省 1
- submitText:string —— 提交按钮文案;缺省「提交」
- resetText:string —— 重置按钮文案;缺省不渲染
- onSubmit(回调):() => void —— 全部字段校验通过后的提交回调
- (formItem)label(必填):string —— 字段标签
- (formItem)value(必填):Signal<string> —— 字段值信号,校验数据源
- (formItem)required:boolean —— 必填:显示红星并自动追加非空规则
- (formItem)rules:FormRule[] —— 校验规则:required / min / max / pattern / validator,按序执行取首个失败
- (formItem)validateOn(默认 blur):'input' | 'blur' | 'submit' —— 校验时机;缺省 blur(输入时自动清已报错误)
- (formItem)hint:string —— 控件下方辅助说明(格式提示等);报错时让位给错误文案
- (formItem)span:number —— 多列表单里本字段占几列(如整宽的多行备注);单列布局或窄屏下自动退回整行
- (formItem)visible:Signal<boolean> —— 条件显示信号:为假时整项隐藏且不参与校验(未显示的字段不该拦住提交)
- onReset:() => void —— 重置按钮点击回调;组件不自行清空字段,由调用方重置信号;不传 resetText 不渲染按钮
- onError(回调):(count: number) => void —— 校验不通过时的回调,参数为不通字段数(适合埋点或顶部提示)
- errorSummary:boolean —— 是否在表单顶部显示「有 N 项需要修改」汇总条;缺省 true,自带错误 UI 时传 false
示例:
```
const name = signal(''); const f = form({ onSubmit: submit }, [formItem({ label: '用户名', required: true, value: name, rules: [{ min: 2, max: 12 }] }, textField({ value: name }))])
```
手把手:
1. 为每个字段创建 signal 与控件,如 const name = signal(''); textField({ value: name })
2. 用 formItem 包住控件:formItem({ label, required, value, rules }, control),返回 { el, validate } 句柄
3. 把句柄数组交给 form:form({ onSubmit }, items),form 渲染全部字段 + 提交/重置按钮
4. 提交时逐项校验:全过触发 onSubmit;有错则标红控件、显示错误文案并聚焦首个出错项
5. 规则语义:required 非空(trim)、min/max 长度、pattern 正则、validator 自定义(true 通过 / 字符串为错误)
6. 非必填字段为空时跳过其余规则;Enter 键触发表单原生 submit(已拦截走校验流程)
7. 错误态覆盖包裹式控件:标红作用于控件根与内部原生控件(含自绘 select 的触发器),并同步 aria-invalid,读屏与视觉一致
8. 触发时机用 input / change / focusout(均会冒泡),因此 checkbox / select / 单选组这类包裹式控件同样能失焦校验
9. 单行输入框内按 Enter 等同点击提交(textarea 内仍是换行);提交按钮为 type="submit",不会与点击回调重复触发
10. rules.validator 返回 Promise 即为异步规则(如「邀请码是否已被使用」远程校验):提交时逐项 await,校验期间提交按钮禁用,结论按轮次取舍避免乱序覆盖
11. formItem 返回 { el, validate, validateAsync } 句柄:validate 只跑同步规则(供 blur/input 即时反馈),validateAsync 逐条 await 供 form 聚合提交
12. 需要给字段加格式说明就传 hint:平时显示在控件下方,校验失败时自动让位给错误文案,同一位置不堆两行
13. 字段多的编辑页传 columns: 2 或 3 走多列栅格:列间距与窄屏堆叠沿用 grid,按钮区始终整行;单列表单不要传(默认 1)
14. 多列表单里想整宽就传 span:formItem({ span: 2 }, textarea(...)) 会占满两列,窄屏单列时自动忽略跨列(否则会撑出横向滚动)
15. 字段要按另一个字段的值决定是否出现(选了「其他」才出现输入框),给 formItem 传 visible 信号:隐藏项自动清掉错误态并退出校验,不用自己在 items 数组里增删
现场演示:apps/docs/demo.html#sec-formlab
### dataTable — 数据表格 DataTable
参数:
- data(必填):T[] | Signal<T[]> —— 全量行数据(行须含 id 作 rowKey);传信号或 computed 时数据变化自动换源,排序 / 筛选 / 页码保持用户原意
- columns(必填):ColumnDef<T>[] —— 列定义:内核字段 { key, header?, sortable?, width? } + 皮肤字段 { align?, format? };width 数字为固定像素,'flex' 自适应
- filterKey:string —— 工具栏筛选的列;缺省取第一列
- pageSize(默认 5):number —— 每页行数;缺省 5
- pageSizes(默认 [5, 10, 20]):number[] —— 分页条「每页条数」候选档;缺省 [5, 10, 20],当前 pageSize 不在候选里会自动补进去
- emptyText(默认 暂无数据):string —— 无数据时的空状态文案;有筛选词时仍显示「没有符合「x」的行」并给一键清除(两种语义不混用)
- selectable(默认 false):boolean —— 启用行选择列:全选三态(含半选)、行 checkbox、已选计数;缺省 false
- page:Signal<number> —— 受控页码信号:与内核分页双向同步,传同一个信号给独立 pagination 组件即可联动
- loading:boolean | Signal<boolean> —— 加载态:为真时数据行换成骨架行(行数随 pageSize),筛选输入与翻页按钮临时禁用,容器带 aria-busy
- selected:Signal<RowKey[]> —— 受控选中行信号:与内核选择双向同步(内容比较,不来回派发),供批量操作读取/改写
- exportable:boolean —— 工具栏显示「导出 CSV」:导出筛选 + 排序后的全量行(不分页);缺省 false
- exportName:string —— 导出文件名;缺省 table.csv
- onRowClick(回调):(row: T) => void —— 整行点击 / Enter / 空格的回调;传了行才可聚焦可点(未传时行保持纯展示,不抢 Tab 焦点)
示例:
```
dataTable({ data: users, selectable: true, columns: [{ key: 'name', header: '姓名', sortable: true }], pageSize: 5 })
```
手把手:
1. 准备行数据,确保每行有唯一 id(跨页选择与刷新不丢行靠它)
2. 定义 columns:sortable: true 的列表头可点击排序(升 → 降 → 取消循环),标记箭头 ↑↓↕
3. 列展示细节用皮肤字段:align 控制表头与单元格对齐,format(value, row) 返回纯文本(走 textContent,不需要转义)
4. 数据源可以给信号或 computed(如筛选结果):变化时内核整体换源,选择状态按 rowKey 剔除已消失的行,排序与页码不被打断
5. 列宽用内核字段 width:数字(像素)固定该列,'flex' 或不设则按内容自适应
6. selectable: true 开启行选择:表头全选框带 indeterminate 半选态,工具栏显示已选计数
7. 选中行用低视觉比重样式(淡色底 + 弱化文字),选择状态存 rowKey,跨页不丢失
8. 工具栏输入框即筛选,输入即时过滤并回第 1 页;无匹配时显示空状态行
9. 表头粘性固定(滚动容器 max-height 420px),斑马纹 + 悬停高亮由 CSS 提供
10. 数据在途时传 loading(布尔或信号):表格体换成骨架行、筛选与翻页禁用,返回后置回 false 即恢复
11. 分页条自带每页条数下拉(5 / 10 / 20 条,调用方传的 pageSize 若不在候选里会自动补进去):切换即改页大小并回第 1 页,首末页时上/下一页置灰不再派发无效页码
12. 需要独立分页器联动时传 page 信号:const page = signal(1),dataTable({ page }) 与 pagination({ page }) 共用
13. 需要批量操作时传 selected 信号:内部勾选实时写回,外部置空或赋值即同步整表(跨页保留,按 rowKey 存)
14. 可排序表头的 aria-sort 跟随排序态同步(ascending / descending / none),读屏能直接播报当前列序,箭头只是视觉补充
15. 需要导出时开 exportable:按钮导的是筛选+排序后的全量行,与当前页无关,单元格按 format 输出
16. 整行可点(进详情、选中一行做后续操作)传 onRowClick:行会变成可聚焦项,Enter / 空格与点击等价;不传时行不进 Tab 序列,纯展示表不会被读屏当成按钮
现场演示:apps/docs/demo.html#sec-table
### timeline — 时间线 Timeline
参数:
- items(必填):TimelineItem[] —— 条目列表,按时间顺序自上而下;每项含 title(必填)、content、time、variant
- mode(默认 left):'left' | 'right' | 'alternate' —— 轴线位置:left / right 单侧排布;alternate 双侧交替(轴线居中、奇偶左右分列,窄屏自动退回左侧)
- pending(默认 false):boolean —— 末尾追加虚线「进行中…」节点,表示流程尚未结束;缺省 false
示例:
```
timeline({ items: [{ title: '提交审批', time: '09:12', variant: 'success' }, { title: '财务复核', time: '10:30', content: '等待张工处理' }], pending: true })
```
手把手:
1. 事件流按时间顺序排(组件不做排序):较早的条目放前面,最新的放最后
2. variant 只承载语义:success 完成、warning 待处理、danger 失败、default 普通记录(节点色走 --ua-* 令牌,随暗色主题变化)
3. time 是纯展示文案,格式自由('09:12' / '10-23 18:00'),组件不做日期解析
4. 流程未结束时传 pending: true,末尾自动补一个虚线脉冲节点,完成后置回 false 即消失
5. 需要轴线居右(如右侧栏时间流)时传 mode: 'right',条目与节点整体镜像
6. content 为纯文本;需要按钮等交互内容时改用 accordion / card 组合
7. 要做里程碑式双侧时间线就传 mode: 'alternate':轴线居中、条目按奇偶分列左右并在窄屏退回单侧,pending 虚节点同样参与交替计数
现场演示:apps/docs/demo.html#sec-timeline
### segmented — 分段控制器 Segmented
参数:
- options(必填):OptionItem[] —— 分段选项列表(互斥),每项含 value / label / disabled
- value(必填):Signal<string> —— 当前选中值信号,双向绑定(点击写回,外部赋值即同步滑块)
- size(默认 md):'sm' | 'md' —— 尺寸档位;缺省 md
- disabled:boolean | Signal<boolean> —— 整组禁用:布尔直接置位,传信号则动态同步;单项禁用走 options[].disabled
- onChange(回调):(value: string) => void —— 切换回调(禁用项与重复点击当前项都不触发)
示例:
```
segmented({ options: [{ value: 'day', label: '日' }, { value: 'week', label: '周' }], value: range })
```
手把手:
1. 和 tabs 的分工:tabs 是页签(下划线指示条 + 面板语义),segmented 是紧凑互斥切换(视图 / 单位 / 时间粒度)
2. value 必须传信号:点击写回信号,外部改信号滑块自动平移,不要在回调里另存一份状态
3. options[].disabled 置灰单项;disabled 传布尔或信号则整组失效(连滑块一起置灰)
4. 键盘:←→ 在可用项间环形移动并选中,Tab 只落在当前项上(漫游 tabindex)
5. 滑块几何由组件测量按钮位置得到,不要在外部改按钮宽度 / 内边距,否则需自行重渲染
6. 需要带图标或更复杂布局的分段,改用 radioGroup + 自定义样式
现场演示:apps/docs/demo.html#sec-segmented
### descriptions — 描述列表 Descriptions
参数:
- items(必填):DescriptionItem[] —— 描述项列表:每项含 label(字段名)、value(字符串 / DOM / 响应式函数)、span(跨列数)
- column(默认 3):1 | 2 | 3 | 4 —— 桌面列数;缺省 3,窄屏(≤640px)由样式表降为单列
- bordered(默认 false):boolean —— 表格形态(带分隔线与字段名底纹);缺省 false 纯网格
- size(默认 md):'sm' | 'md' —— 尺寸档位;缺省 md
- title:string —— 分组标题;缺省不渲染标题行
- layout(默认 'horizontal'):'horizontal' | 'vertical' —— 字段名位置:horizontal 在值左侧,vertical 在值上方(窄栏 / 长文本更耐读,配 bordered 时去掉标签区的分隔线)
- extra(回调):unknown —— 标题行右侧内容(如更新时间、刷新按钮);仅在传了 title 时渲染
示例:
```
descriptions({ title: '订单信息', column: 2, bordered: true, items: [{ label: '订单号', value: 'RB-2048' }, { label: '金额', value: '¥1,280.00' }, { label: '备注', value: '加急', span: 2 }] })
```
手把手:
1. 详情页 / 抽屉里展示只读字段用它,不要用 form(form 承载输入与校验)
2. column 按信息密度定:字段多且短用 3-4 列,字段长(地址、备注)用 2 列并配 span 占整行
3. value 支持任意 DOM:放 tag、badge、button 都可以;要给值做响应式更新就传函数 () => 内容
4. bordered: true 是表格形态(适合打印与密集信息),纯网格形态更轻,适合卡片内嵌
5. 窄屏自动降为单列,无需在调用方写媒体查询
6. 纯展示组件,无状态无回调:值变了由调用方重新渲染或传函数式 value
7. 窄栏或长文本场景把 layout 设成 'vertical':字段名落到值上方,bordered 形态会自动去掉标签右侧的分隔线与底纹;标题行要放操作就传 extra(数组可放多个节点)
现场演示:apps/docs/demo.html#sec-descriptions
### statistic — 统计数值 Statistic
参数:
- title:string —— 指标名(小字灰标题);缺省不渲染
- value(必填):Signal<number> —— 数值信号:变化时滚动过渡到新值;倒计时模式可不传
- countdown:number —— 目标时间戳(毫秒):传了即倒计时模式,每秒显示 DD 天 HH:MM:SS,忽略 value
- precision(默认 0):number —— 小数位,千分位分隔自动带;缺省 0
- prefix:string —— 数字前缀(如 ¥);缺省不渲染
- suffix:string —— 数字后缀(如 元 / %);缺省不渲染
- trend:'up' | 'down' —— 环比方向:up 红 / down 绿(A 股涨红跌绿口径);缺省不渲染
示例:
```
const gmv = signal(128000); statistic({ title: '本月 GMV', value: gmv, prefix: '¥', precision: 2, trend: 'up' })
```
手把手:
1. value 必须传信号:组件用 effect 监听,值变了自动滚动过渡,不要在外部手动改文本
2. 首跑也会从 0 滚到目标值(入场动画);不想要就初始化信号为终值后在挂载后再改
3. precision 控制小数位,千分位由 toLocaleString 自动加,不要自己拼格式化
4. trend 只表达方向与颜色(涨红跌绿),数值本身与环比幅度仍由调用方组织
5. 多条指标并排时用 descriptions 或 flex 容器排列,本组件不自带网格
6. 用户开启「减弱动效」时组件直接落终值,无需调用方判断
7. 做倒计时(活动剩余时间等)传 countdown: 目标时间戳,组件每秒刷新并在到点/卸载时自动停表
现场演示:apps/docs/demo.html#sec-statistic
### list — 列表 List
参数:
- items(必填):ListItem[] —— 条目列表,每项 { title, description?, avatar?, extra? }
- header:string —— 列表头标题;缺省不渲染
- footer:string —— 列表尾说明;缺省不渲染
- bordered:boolean —— 是否带外边框与分隔线;缺省 false
- emptyText:string —— 无数据占位文案;缺省「暂无数据」
- onItemClick(回调):(item: ListItem, index: number) => void —— 条目点击回调;提供后条目可点击并带 hover 反馈
示例:
```
list({ header: '待办', bordered: true, items: [{ title: '报销单 RB-2048', description: '等待复核', avatar: avatar({ name: '王' }) }], onItemClick: (item) => console.log(item.title) })
```
手把手:
1. 准备 items 数组,每项至少给 title,description 作次要说明
2. avatar 传头像组件(如 avatar({ name }))、extra 传右侧附加(如 tag(...)),不传该侧不留位
3. 需要点击时传 onItemClick(item, index),条目自动加 hover 底色;bordered: true 给外边框与分隔线
4. items 为空数组时自动渲染 empty 占位,文案可用 emptyText 覆盖
现场演示:apps/docs/demo.html#sec-list
### carousel — 轮播 Carousel
参数:
- items(必填):unknown[] —— 每张幻灯片内容,顺序即播放顺序
- index:Signal<number> —— 当前下标信号;不传则内部维护
- autoplay:number —— 自动前进间隔(毫秒);缺省不自动播放
- height:number —— 固定高度像素;缺省由内容撑开
- label:string —— 区域无障碍名称;缺省「轮播」
- onChange(回调):(index: number) => void —— 切换回调:点击指示点/箭头/键盘/自动前进都会触发
示例:
```
const i = signal(0); carousel({ items: [h('div', null, 'A'), h('div', null, 'B')], index: i, height: 150, autoplay: 4000, onChange: (k) => console.log(k) })
```
手把手:
1. items 每项是任意节点(图、卡片、文字均可),顺序即播放顺序
2. index 传自己的信号即可外部受控(外部改信号轮播会跟),不传则由组件内部维护
3. height 控固定高度(缺省随内容撑开);autoplay 传毫秒数自动前进,鼠标悬停自动暂停
4. 键盘:聚焦轮播后 ←→ 切换,事件不冒泡避免连带切换外层 tabs/menu
5. 指示点与箭头都可点击,末张下一张环形回到首张
现场演示:apps/docs/demo.html#sec-carousel
### tree — 树形 Tree
参数:
- nodes(必填):TreeNode[] —— 根节点列表,每项 { key, label, children?, disabled? }
- expanded:Signal<string[]> —— 展开的 key 集合;不传则内部维护
- selected:Signal<string> —— 选中 key 信号;不传则内部维护
- defaultExpandAll:boolean —— 首屏展开全部分支;缺省 false
- searchable:boolean —— 顶部搜索框:按标签过滤,命中节点保留父链并自动展开;缺省 false
- onSelect(回调):(node: TreeNode) => void —— 选中回调:点击标签或 Enter/空格触发
- multi:boolean —— 是否显示勾选框并开启多选;缺省 false
- checked:Signal<string[]> —— 勾选 key 集合(multi 时生效);不传则内部维护
- onCheck:(keys: string[]) => void —— 勾选变更回调(父子已级联,传副本)
示例:
```
const sel = signal(''); tree({ nodes: [{ key: 'src', label: 'src', children: [{ key: 'ui', label: 'ui' }] }], selected: sel, onSelect: (n) => console.log(n.key) })
```
手把手:
1. 节点树用 { key, label, children } 描述,key 唯一即可(选中与展开都按 key 记)
2. 传 selected 信号即可外部受控;不传则组件内部维护选中态
3. 点箭头只展开/收起,点标签才选中;叶子节点箭头位置留空以对齐
4. 键盘:↑↓ 在可见节点间移动、Home/End 到首末行(只移动焦点),Enter/空格 选中、→ 展开、← 收起;disabled 节点点击与键盘都不响应
5. defaultExpandAll 首屏展开全部分支(外部已给 expanded 初值时以外部为准)
6. multi: true 时每行前置勾选框:勾父级下钻全选子孙(跳过禁用项),子孙全选后父级自动上勾、部分勾选显示半选
7. 勾选框点击不触发选中(stopPropagation),键盘在多选模式下 Enter/空格改为切换勾选
8. 节点多时开 searchable:输入即过滤,命中节点连同父链一起显示,隐藏的枝不参与方向键遍历
现场演示:apps/docs/demo.html#sec-tree
### upload — 文件选择 Upload
参数:
- accept:string —— 接受的类型(同原生 accept),如 'image/*,.pdf'
- multiple:boolean —— 允许多选;缺省 false(单选时新选择覆盖旧的)
- hint:string —— 提示文案;缺省「点击或拖拽文件到此处」
- maxSize:number —— 单文件体积上限(字节);超限丢弃并在提示行说明,缺省不限
- maxCount:number —— multiple 时的文件数上限;超出部分丢弃并提示,缺省不限
- disabled:boolean —— 禁用:不可点选、不接受拖拽
- onChange(回调):(files: File[]) => void —— 文件变化回调:选择/拖拽/移除后都触发,回传副本
示例:
```
upload({ multiple: true, accept: 'image/*', hint: '拖拽图片到此处', onChange: (files) => console.log(files.map((f) => f.name)) })
```
手把手:
1. 把 upload 放进表单,accept 限定类型、multiple 允许多选
2. 需要拦体积就给 maxSize(字节),限制数量给 maxCount
3. 被拒的文件不会进清单,原因显示在下方提示行(拖拽与点选走同一套校验)
4. onChange 拿到的 File[] 是副本,自行发请求上传(组件不内置网络层)
5. 选中的图片会带 28px 缩略图(对象 URL,随文件离场即 revoke,不会泄漏);非图片只显示文件名与体积,点 × 移除
现场演示:apps/docs/demo.html#sec-upload
### notification — 通知卡片 Notification
参数:
- title(必填):string —— 通知标题
- description:string —— 补充说明;缺省不渲染
- variant:'info' | 'success' | 'warning' | 'danger' —— 语义色(左侧条与圆点);缺省 info
- duration:number —— 自动关闭毫秒;缺省 0 = 常驻不自动关,非 0 时悬停 / 聚焦暂停计时
- closable:boolean —— 是否显示关闭按钮;缺省 true
- action:unknown —— 操作区内容(如按钮);缺省不渲染
- placement(默认 bottom-right):'top-right' | 'bottom-right' | 'top-left' | 'bottom-left' —— 停靠角落;每个角落独立堆叠(同角最多 3 条),右上与 toast 同侧,建议次要信息放左侧
- key:string —— 同 key 复用同一张卡:再次调用只原地改标题 / 描述 / 语义色,不再堆新条(返回的句柄带 update 方法可主动改)
示例:
```
const { close } = notification({ title: '导出完成', description: 'report.csv 已开始下载', variant: 'success', duration: 4000 }); // close() 手动关闭
```
手把手:
1. 调用即入列:右下角堆叠,同屏最多 3 条,超出自动挤掉最早一条
2. 缺省常驻(duration 0),需要自动消失就传毫秒数,如 duration: 4000
3. 返回 { el, close } 句柄:程序化关闭用 close(),需要自己挂交互就用 el
4. action 放操作区(常见为按钮);variant 决定左侧条与圆点语义色
5. 与 toast 的分工:toast 右上角轻量瞬时,notification 右下角可长读、可操作
6. 停靠角落用 placement 调:四个角各自独立堆叠与节流,顶部角落新通知从屏幕边缘长出、旧的往下让
7. 进度类通知传 key: 'sync-job':第一次返回句柄,后续 job.update({ title, description, variant }) 就地把同一条卡改成 60% / 完成,不会像以前那样堆出三条;job.close() 收掉时同时释放 key 登记
现场演示:apps/docs/demo.html#sec-notification
### result — 结果页 Result
参数:
- status:'success' | 'warning' | 'danger' | 'info' —— 终态语义色(图标底色);缺省 info
- title(必填):string —— 主标题
- description:string —— 补充说明;缺省不渲染
- extra:unknown —— 操作区内容(常见为主次按钮);缺省不渲染
示例:
```
result({ status: 'success', title: '提交成功', description: '工单 #2049 已受理', extra: button({ label: '查看工单', variant: 'primary', onClick: () => go() }) })
```
手把手:
1. status 决定图标底色(success 绿 / warning 黄 / danger 红 / info 蓝),整体居中排布
2. title 必填;description 用于说明原因或下一步,超过一行自动居中换行
3. extra 放操作区,常见为主按钮 + 幽灵按钮;不传则不渲染
4. 组件自带 role="status",插入页面后读屏会播报终态文案
现场演示:apps/docs/demo.html#sec-result
### transfer — 穿梭框 Transfer
参数:
- dataSource(必填):TransferItem[] —— 全量条目,左右两栏都按是否在 targetKeys 里派生
- targetKeys:Signal<string[]> —— 右侧集合信号;不传则组件内部维护,传了即外部受控
- titles:[string, string] —— 左右栏标题;缺省「待选列表 / 已选列表」
- searchable:boolean —— 是否显示搜索框;缺省 true,逐栏独立过滤
- height:number —— 列表区固定高度像素;缺省走样式表 220,超出滚动
- onChange:(keys: string[]) => void —— 右侧集合变化回调(搬运后触发,传副本)
示例:
```
transfer({ targetKeys: enabled, titles: ['待选', '已启用'], onChange: (keys) => toast.info(`已启用 ${keys.length} 项`), dataSource: [{ key: 'a11y', label: '无障碍语义' }, { key: 'ssr', label: 'SSR(暂不可用)', disabled: true }] })
```
手把手:
1. dataSource 是唯一数据源,条目归属由 targetKeys 决定;搬运只改 targetKeys,不删条目
2. 两栏独立勾选,勾选后点中间 → / ← 整体搬运,搬完两栏勾选自动清空;无勾选时按钮自动禁用
3. 每栏均有搜索框与全选:全选只覆盖当前搜索结果里未禁用的条目,部分勾选时全选框显示半选
4. disabled 条目不可勾选、不计入全选基数、也不会被搬运
5. 传入 targetKeys 信号即为受控用法,外部写入即可程序化搬运(如「一键全选」)
现场演示:apps/docs/demo.html#sec-transfer
### cascader — 级联选择 Cascader
参数:
- options(必填):CascaderOption[] —— 根级选项,children 递归出列,value 逐级拼成路径
- value:Signal<string[]> —— 选中路径信号;不传则内部维护,传了即外部受控
- placeholder:string —— 未选时占位文案;缺省「请选择」
- separator:string —— 路径分隔符;缺省「 / 」
- changeOnSelect:boolean —— 允许停在中间层;缺省 false 必须选到叶子才提交
- disabled:boolean | Signal<boolean> —— 禁用触发器;支持信号响应式禁用
- onChange:(path: string[]) => void —— 选中回调(叶子或中间层,传副本)
示例:
```
cascader({ value: areaPath, changeOnSelect: true, options: [{ value: 'east', label: '华东', children: [{ value: 'sh', label: '上海' }] }] })
```
手把手:
1. 传入嵌套 options,第一列是根级选项,悬停带 children 的项即展开下一列
2. 点击叶子提交;changeOnSelect 为 true 时点中间层也提交(适合「省/市」这类粗粒度就够用的场景)
3. value 是路径数组(逐级 value),传信号即受控;触发器文案按 separator 拼接已提交路径
4. 键盘:↑↓ 同列移动、→ 下钻、← 回退、Enter/空格 提交、Esc 收起(与弹窗共用一个 Esc 栈)
5. 面板空间不足时自动向上翻转;disabled 选项不可点也不会展开下一列
现场演示:apps/docs/demo.html#sec-cascader
### splitter — 可拖拽分栏 Splitter
参数:
- panes(必填):unknown[] —— 各栏内容(节点或纯文本),顺序即排列顺序
- direction:'horizontal' | 'vertical' —— 分割方向;缺省 horizontal(左右)
- sizes:Signal<number[]> —— 各栏百分比数组(和为 100);不传则内部均分
- min:number —— 每栏最小百分比;缺省 10,拖到边界即停
- onChange:(sizes: number[]) => void —— 拖拽 / 键盘调整后回调(传副本)
示例:
```
splitter({ sizes: ratio, min: 15, panes: [leftPanel, rightPanel], onChange: (s) => ratio([...s]) })
```
手把手:
1. panes 长度决定栏数与分隔条数(n 栏对应 n-1 条),栏宽由 sizes 百分比驱动
2. 指针按住分隔条拖动即可改比例,拖动过程两栏之和恒定、各自不低于 min
3. 分隔条可 Tab 聚焦,← → / ↑ ↓ 每次调 2%,按住 Shift 调 10%;双击复位为均分
4. direction 传 vertical 变为上下分栏,键盘方向键与光标同步切换
5. 传入 sizes 信号即受控,外部写入可做「预设布局」切换(如 1:1 / 2:1)
现场演示:apps/docs/demo.html#sec-splitter
### autoComplete — 自动补全 AutoComplete
参数:
- options(必填):AutoCompleteOption[] | ((q: string) => AutoCompleteOption[]) —— 候选项:静态列表或按关键字计算的函数(可远程联想)
- value:Signal<string> —— 输入值信号;不传则内部维护,传了即外部受控
- placeholder:string —— 占位文案,同时作为无障碍名称
- emptyText:string —— 无匹配时的提示文案;缺省「无匹配项」
- maxItems:number —— 最多展示条数;缺省 8
- disabled:boolean | Signal<boolean> —— 禁用输入与弹层
- onInput:(query: string) => void —— 每次输入回调,适合触发远程请求
- onSelect:(option: AutoCompleteOption) => void —— 选中回调(点击或 Enter)
示例:
```
autoComplete({ value: acValue, placeholder: '输入组件名', options: [{ value: 'dataTable', hint: '表格' }], onSelect: (o) => toast.success(o.value) })
```
手把手:
1. 输入任何字符即按包含匹配(忽略大小写)过滤候选,空串时弹层不展开
2. ↑↓ 在候选间环形移动并高亮,Enter 选中,Esc 收起,Tab 失焦也会收起
3. 候选行支持右侧 hint(分类 / 拼音),选中后值写回输入框并触发 onSelect
4. options 传函数时每次输入都会用当前关键字调用,可在此发起远程请求并返回新数组
5. value 传信号即受控:外部写入会同步回填输入框内容
现场演示:apps/docs/demo.html#sec-autocomplete
### datePicker — 日期选择 DatePicker
参数:
- value:Signal<string> —— 选中日期(YYYY-MM-DD)信号;区间模式下是起始日,不传则内部维护
- range(默认 false):boolean —— 区间模式:首次点击定起点(弹层不收起),再次点击定终点;点早了自动交换先后
- end:Signal<string> —— 区间模式的结束日信号;不传则内部维护
- onRangeChange(回调):(start: string, end: string) => void —— 区间两端都选定后的回调,参数已按先后排序(单日模式不触发)
- placeholder:string —— 未选时占位文案;缺省「请选择日期」(区间模式缺省「请选择日期范围」)
- min:string —— 最早可选日期(含,YYYY-MM-DD);更早的日置灰不可点
- max:string —— 最晚可选日期(含,YYYY-MM-DD);更晚的日置灰不可点
- disabled:boolean | Signal<boolean> —— 禁用触发器
- presets:DatePreset[] —— 快捷区间按钮组(仅区间模式渲染):每项给 label + days(相对今天含今天),或 label + from/to 绝对日期;越界结果按 min/max 钳制,点击即写入 value/end 并收起
- onChange:(date: string) => void —— 选中回调(点击日期或 Enter)
示例:
```
datePicker({ value: dateVal, min: '2026-10-20', max: '2026-10-31', onChange: (d) => toast.success(d) })
```
手把手:
1. 点触发器展开当月网格,打开时视图自动对准已选日期(无选中则今天)
2. ‹ › 翻月,底部「今天」一键跳今天并提交;今天用描边标记、选中用实心底色
3. 键盘:←→ 逐日、↑↓ 逐周、PageUp/PageDown 逐月、Enter 提交,跨月会自动翻页
4. min / max 为闭区间,越界日期置灰不可点也不可键盘选中
5. value 传信号即受控;Esc 收起(与弹窗/抽屉共用逐层关闭栈)
6. 选日期区间传 range: true:第一次点击只定起点(触发器显示「起 ~ …」),第二次点击补终点并收起,两端之间用低比重底色连成一条
7. 区间回调走 onRangeChange(start, end),参数已排序——用户先点晚的日期也不会出现 start > end;只需检视当前值就同时读 value 与 end 两个信号
8. 传 presets 在弹层顶部加一排快捷区间按钮:{ label: '最近 7 天', days: 7 } 走相对今天,{ label: '本月', from: '2026-10-01', to: '2026-10-31' } 走绝对日期(两者都给时 days 优先),点一下即写入并收起,回调照旧走 onRangeChange
现场演示:apps/docs/demo.html#sec-datepicker
### backTop — 返回顶部 BackTop
参数:
- target:HTMLElement | Window —— 监听的滚动容器;缺省 window(整页)
- threshold:number —— 滚动超过多少像素才浮现;缺省 300
- progress:boolean —— 是否显示滚动进度环;缺省 true
- onClick:() => void —— 回顶之后的回调(适合埋点)
示例:
```
backTop({ threshold: 300, onClick: () => toast.info('已回到顶部') })
```
手把手:
1. 把返回的元素挂到页面任意位置即可,组件自身 position: fixed 定位在右下角
2. 滚动越过 threshold 才浮现(淡入 + 上移微动画),回到阈值内自动隐藏
3. progress 为 true 时外环弧长等于已滚比例;内滚容器用 target 传入该元素
4. 点击平滑回顶;系统开启「减少动效」时改为瞬间跳转
现场演示:apps/docs/demo.html#sec-backtop
### colorPicker — 颜色选择 ColorPicker
参数:
- value:Signal<string> —— 颜色值信号(CSS 颜色串,支持 var(--ua-*));不传则内部维护
- presets:string[] —— 预设色板;缺省取语义令牌色(accent/success/warning/danger/text/border)
- placeholder:string —— 未选时占位文案;缺省「请选择颜色」
- disabled:boolean | Signal<boolean> —— 禁用触发器
- onChange:(color: string) => void —— 选定回调(点色板、取色器或手输回车)
示例:
```
colorPicker({ value: colorVal, onChange: (c) => toast.success(c) })
```
手把手:
1. 点触发器展开弹层:上方是预设色板,下方是原生取色器 + 手输框
2. 预设缺省直接用 --ua-* 令牌色,所以暗色主题切换后色块自动跟着变,无需维护两套色值
3. 点色块即提交;手输框支持任意 CSS 颜色串(含 var(--ua-color-accent)),回车提交
4. 触发器左侧色块始终预览当前值,点击外部或 Esc 收起
现场演示:apps/docs/demo.html#sec-colorpicker
### tour — 引导漫游 Tour
参数:
- steps(必填):TourStep[] —— 步骤序列:每步含 target 选择器、title、可选 description
- padding:number —— 聚光区相对目标的外扩像素;缺省 6
- onFinish:() => void —— 走完最后一步的回调
- onClose:() => void —— 中途跳过(× 或 Esc)的回调
示例:
```
const t = tour({ steps: [{ target: '#sec-tour', title: '引导漫游', description: '按选择器定位' }], onFinish: () => toast.success('完成') }); document.body.append(t.el); t.start();
```
手把手:
1. tour() 返回 { el, start, close } 句柄:el 必须挂到 body(fixed 定位,不占布局)
2. start() 从第一步播放,可重复调用重播;close() 立即退出并按跳过处理
3. 聚光框按 target 选择器定位并外扩 padding,其余区域由超大 box-shadow 压暗(省掉独立遮罩层)
4. 卡片优先落在目标下方,空间不足自动翻到上方,左右夹在视口内;滚动 / 缩放会实时重算
5. 末步按钮文案变「完成」并触发 onFinish;× 或 Esc 触发 onClose,焦点归还开启前的元素
现场演示:apps/docs/demo.html#sec-tour
### watermark — 水印 Watermark
参数:
- text(必填):string —— 水印文字,支持 \n 多行
- opacity:number —— 透明度;缺省 0.12
- rotate:number —— 旋转角度(度);缺省 -20
- gap:number —— 平铺间距像素;缺省 140
- fontSize:number —— 字号像素;缺省 14
- color:string —— 颜色(CSS 串或 var(--ua-*) 令牌);缺省取 --ua-color-text-muted 的实色
- children:...unknown[] —— 被水印覆盖的内容,作为第二个及之后的实参传入
示例:
```
watermark({ text: '内部资料\n仅限内网', gap: 160 }, card({ title: '受保护内容' }, h('p', null, '正文')))
```
手把手:
1. 第一个参数是配置,其后所有实参都被当作内容渲染在水印层下方
2. 组件用一块 gap 见方的 canvas 画单个旋转单元,再 background-repeat 铺满,因此不需要监听尺寸变化
3. color 传 var(--ua-*) 时会先解析成计算值(canvas 不认 CSS 变量),所以暗色主题下颜色会自动跟随
4. 覆盖层为 pointer-events:none,下方按钮/输入框照常可点;watermark 根节点自带圆角与裁剪
现场演示:apps/docs/demo.html#sec-watermark
### checkboxGroup — 多选组 CheckboxGroup
参数:
- options(必填):CheckOption[] —— 选项集合:value / label / 可选 disabled
- value:Signal<string[]> —— 选中值数组信号;不传则内部维护
- min:number —— 最少选几项:到下限后已选项禁用,防止取消
- max:number —— 最多选几项:到上限后未选项禁用
- checkAll:boolean —— 是否显示全选行(含半选);缺省 false
- direction:'horizontal' | 'vertical' —— 排列方向;缺省 horizontal
- disabled:boolean | Signal<boolean> —— 整组禁用
- onChange:(values: string[]) => void —— 变更回调(传副本)
示例:
```
checkboxGroup({ value: permPick, min: 1, max: 3, checkAll: true, options: [{ value: 'read', label: '读取' }, { value: 'write', label: '写入' }] })
```
手把手:
1. options 顺序即渲染顺序;勾选后把整份数组写回 value 信号并触发 onChange
2. max 生效时未选项自动禁用,min 生效时已选项自动禁用——用禁用表达边界,不抛错也不静默回滚
3. checkAll 行的选中/半选只统计未禁用项,禁用项不计入基数
4. direction 传 vertical 改为纵向排列;disabled 支持信号,整组响应式禁用
现场演示:apps/docs/demo.html#sec-checkgroup
### affix — 固钉 Affix
参数:
- offset:number —— 距视口顶部多少像素后转为固定;缺省 0
- position:'top' | 'bottom' —— 固定边:贴顶放头部工具栏,贴底放底部操作条;缺省 top
- target:HTMLElement | Window —— 监听的滚动容器;缺省 window(整页)
- children:...unknown[] —— 被固定的内容,作为第二个及之后的实参传入
示例:
```
affix({ offset: 8 }, card({ title: '滚动时我会贴顶' }, h('p', null, '正文')))
```
手把手:
1. 把要贴住的内容作为实参传入;组件内部包两层:外层占位、内层负责 fixed
2. 滚动到 offset 以内(默认贴视口顶)时内层转 fixed,外层同步撑出等高占位,页面不会塌跳
3. 固定期间宽度与横向位置跟随占位容器,窗口缩放或父容器变宽都自动贴合
4. 内滚容器场景把该元素传给 target,判定改为基于它的滚动位置
5. 底部操作条传 position: 'bottom':占位容器底边越过「视口底 - offset」时才固定,对应的内联边距写 bottom
现场演示:apps/docs/demo.html#sec-affix
### multiSelect — 多选下拉 MultiSelect
参数:
- options(必填):CheckOption[] —— 选项集合:value / label / 可选 disabled;每项可加 group 指定分组小标题
- value:Signal<string[]> —— 选中值数组信号;不传则内部维护
- placeholder:string —— 未选时占位文案;已选时让位给标签
- max:number —— 最多选几项:到上限后未选项自动禁用
- maxTagCount:number —— 标签区最多显示几个,超出的折成一枚「+N」(悬停看全量、点击展开弹层);缺省全部显示
- searchable:boolean —— 是否可输入过滤;缺省 true
- disabled:boolean | Signal<boolean> —— 禁用整个下拉
- onChange:(values: string[]) => void —— 变更回调(传副本)
- clearable:boolean —— 开启后有选中值时在右缘显示一键清空按钮;缺省不渲染
示例:
```
multiSelect({ value: tagPick, max: 3, placeholder: '选择技术栈', options: [{ value: 'ts', label: 'TypeScript' }, { value: 'css', label: 'CSS 令牌' }] })
```
手把手:
1. 选中项以标签形式回显,点标签上的 × 或空输入时按退格即可删除末个标签
2. 点输入区展开弹层并聚焦输入框,输入字符即时过滤(大小写不敏感)
3. 点选项只切换该项、弹层不收起,适合连续多选;到 max 后未选项置灰不可点
4. ↑↓ 环形移动高亮、Enter 切换当前项、Esc 收起(与其它弹层共用一个 Esc 栈)
5. value 传数组信号即受控,外部写入会同步标签区
6. 选中项多时传 maxTagCount: 2 折叠:前 2 枚标签照常逐个删除,其余折成虚线「+N」,悬停 title 给出被折叠的项名,点它直接展开弹层;受控数组一变折叠数就重算
7. 候选分组同样用 group 字段:小标题是不可点击的 li(不占键盘高亮下标),选中后的标签顺序仍按点击顺序而非分组顺序
现场演示:apps/docs/demo.html#sec-multiselect
### anchor — 锚点导航 Anchor
参数:
- items:AnchorItem[] —— 锚点项集合,每项含 href(形如 #sec-x)、title、可选 disabled
- offset:number —— 定位与高亮判定预留的顶部距离,适合有吸顶栏的页面;缺省 0
- container:HTMLElement | Window —— 监听的滚动容器;缺省 window(整页滚动)
- label:string —— nav 的无障碍名称;缺省「锚点导航」
- onChange:(href: string) => void —— 跳转后的回调,参数为命中的 href(适合埋点或联动)
示例:
```
anchor({ items: [{ href: '#sec-button', title: '按钮' }, { href: '#sec-card', title: '卡片' }], offset: 72 })
```
手把手:
1. 在页面任意位置放一个容器,调用 anchor 并把返回的 nav 塞进去(固定侧栏或吸顶栏均可)
2. items 里每条 href 指向页面上真实存在的区块 id,标题即链接文案;目标缺失的项在滚动判定时自动跳过
3. 页面有吸顶导航时把它的高度传给 offset,跳转与高亮判定都会预留这段距离
4. 滚动容器不是整页时用 container 传入该元素;点击走平滑滚动,系统开启「减少动效」时改为瞬移,hash 用 replaceState 回写不堆历史
现场演示:apps/docs/demo.html#sec-anchor
### popconfirm — 二次确认 Popconfirm
参数:
- title(必填):string —— 气泡标题,通常写成一句问句(如「删除这条记录?」)
- description:string —— 补充说明:影响范围、是否可撤销;缺省给一句通用提示
- confirmText:string —— 确认按钮文案;缺省「确定」
- cancelText:string —— 取消按钮文案;缺省「取消」
- variant:'default' | 'danger' —— 确认按钮语义色;删除类操作传 danger,缺省 default
- placement:'top' | 'bottom' —— 气泡方位;缺省 top,压在触发器上方不盖住被操作的行
- onConfirm(回调):() => void —— 点确定后的回调:普通回调先收起再执行;返回 Promise 时按钮转「处理中…」并禁用,成功才收起、失败留在气泡里可重试
- onCancel(回调):() => void —— 取消 / 点外部 / Esc 收起时的回调
- open:Signal<boolean> —— 受控开合信号;不传则内部维护,传了可由外部编程开合
示例:
```
popconfirm({ title: '删除这条记录?', description: '删除后无法恢复', variant: 'danger', confirmText: '删除', onConfirm: () => toast.success('已删除') }, button({ label: '删除', variant: 'danger' }))
```
手把手:
1. 把要保护的触发器元素(按钮 / 图标)作为第二个参数传进去,返回的包裹节点替换原位置
2. 写清 title(问句)与 description(影响范围),删除类操作把 variant 设成 danger,确认按钮会自动变成危险色
3. 点确定先收起弹层再触发 onConfirm,回调里可以放心同步改数据;取消、点外部、Esc 一律走 onCancel
4. 展开时焦点自动落在「取消」上,误按回车不会执行危险操作;需要外部控制开合时传 open 信号
5. 删除 / 归档这类要打后端的操作把 onConfirm 写成 async:气泡不会立刻收起,确认按钮变「处理中…」并禁用,resolve 后才关,reject 时保持展开供重试
现场演示:apps/docs/demo.html#sec-popconfirm
### calendar — 月历 Calendar
参数:
- value:Signal<string> —— 选中日期(YYYY-MM-DD)信号;不传则内部维护
- month:Signal<string> —— 展示月份(YYYY-MM);不传则内部维护,初始跟随 value 或今天
- marker(回调):(date: string) => string | null —— 每日标记:返回文案即渲染小徽标(如「3 项」),同时写进 aria-label;返回 null 不标记
- onSelect(回调):(date: string) => void —— 选中日期回调(点击或 Enter / 空格)
- onMonthChange(回调):(month: string) => void —— 翻月回调,参数为 YYYY-MM(可在此拉取当月数据)
示例:
```
const day = signal(''); calendar({ value: day, marker: (d) => (d === '2026-10-26' ? '2 项' : null), onSelect: (d) => toast.info(d) })
```
手把手:
1. 把 calendar 放在页面容器里即可,它自带标题行(月份 + 上下月箭头)、星期表头与 6 行网格
2. 选中日期用 value 信号双向绑定;不传则组件内部维护,onSelect 里拿字符串自己处理
3. 需要显示每天有几件事就传 marker:返回非空文案即渲染小徽标,并拼进 aria-label 让读屏也听得到
4. 键盘:Tab 进入网格一次,←→ 逐日、↑↓ 逐周、Enter / 空格 选中;跨月会自动翻页并交接焦点
现场演示:apps/docs/demo.html#sec-calendar
### typography — 排版文本 Typography
参数:
- content(必填):string | Node —— 文本内容:字符串按纯文本渲染;传 DOM 则原样插入(此时不参与截断与复制)
- variant(默认 text):'title' | 'text' | 'paragraph' —— 排版形态,决定语义标签与字号:title 出 h1-h5、paragraph 出 p、text 出 span
- level(默认 3):1 | 2 | 3 | 4 | 5 —— 标题层级,仅 variant=title 生效;越界值会被夹到 1-5
- type:'secondary' | 'success' | 'warning' | 'danger' —— 语义色调;缺省主文本色
- strong(默认 false):boolean —— 加粗强调
- underline(默认 false):boolean —— 下划线(可为链接式文案保留指引视觉)
- delete(默认 false):boolean —— 删除线,用于作废文案;会同时弱化文字颜色
- mark(默认 false):boolean —— 高亮底纹(警示色 24% 混透明),用于搜索命中片段
- code(默认 false):boolean —— 等宽代码样式,用于行内字段名 / 命令
- align:'left' | 'center' | 'right' —— 水平对齐;缺省左对齐
- ellipsis:number —— 截断行数(1 起):超出显示省略号,纯文本自动挂 title 供悬停看全文;缺省不截断
- copyable(默认 false):boolean —— 右侧附复制按钮:点击写剪贴板并回显「已复制 / 复制失败」,1.2 秒后复原
示例:
```
typography({ variant: 'title', level: 2, content: '部署说明' }); typography({ content: 'RB-2048', code: true, copyable: true }); typography({ variant: 'paragraph', ellipsis: 2, content: '一段很长的说明……' })
```
手把手:
1. 正文一律用 variant='text' 或 'paragraph',标题用 variant='title' + level,标签自动出 h1-h5,别自己写 h 标签再叠样式
2. 文内强调用装饰位组合:strong 加粗、mark 高亮、code 等宽、underline / delete 表示链接与作废,一个组件内可叠加
3. 长描述(卡片摘要、表格说明)用 ellipsis 给行数:超出显示省略号并自动挂 title,悬停即看全文,不必额外做展开按钮
4. 订单号 / 密钥 / 命令这类要复制的内容传 copyable:按钮独立于文本节点,复制失败会回显「复制失败」而不是静默
5. content 传 DOM 时组件只负责语义标签与类名:截断与复制对 DOM 内容不生效(取不到稳定文本)
6. 段落连续排列时相邻 .ua-type-paragraph 自动留出间距,单个段落不额外撑高,嵌进卡片也不会有空隙
现场演示:apps/docs/demo.html#sec-typography
### grid — 栅格容器 Grid
参数:
- columns(默认 3):number —— 固定列数(按等宽铺开);窄屏(≤640px)由样式表统一降为单列
- min:string —— 自适应最小列宽(如 '220px'):给了就按 auto-fit 铺列,列数随可用宽度自然增减,此时忽略 columns
- gutter(默认 md):SpaceSize —— 列间距:sm/md/lg 走令牌,数字按像素
- align(默认 stretch):'start' | 'center' | 'end' | 'stretch' —— 交叉轴对齐:同行子项高度不一时决定各自贴哪边或拉满
- dense(默认 false):boolean —— 紧凑填充:小项回填前面留下的空洞(瀑布式排布)
示例:
```
grid({ columns: 3, gutter: 'md' }, card({ title: '实例数' }, statistic({ value: total })), card({ title: '错误率' }, statistic({ value: errRate }))); grid({ min: '240px', align: 'start' }, ...items)
```
手把手:
1. 页面骨架用 grid({ columns: n }) 铺等宽卡片:子项宽度由列模板算,别在子项上写 width,窄屏会自动变成单列
2. 卡片宽度要求「不窄于 X」时用 min(如 '240px'):auto-fit 会按可用宽度决定列数,比手写断点稳,此时不要再传 columns
3. 列间距统一走 gutter(sm/md/lg 令牌或像素),不要在子项上写 margin——相邻 margin 会叠加成双倍间距
4. 同行卡片高度参差时:要等高(默认 stretch),要各自顶端对齐传 align: 'start'
5. 子项必须是单个元素(card / div / 自定义块),grid 只负责排布且已对子项设 min-width: 0,长文本不会撑破列宽
6. 只要横向一排展示且元素不等宽,用 space 更轻;grid 面向二维对齐
现场演示:apps/docs/demo.html#sec-grid
### floatButton — 悬浮操作按钮 FloatButton
参数:
- icon:string | Node —— 图标:字符串按字符(emoji / 符号)渲染,传 DOM 原样插入
- label:string —— 文字标签;给了就出「图标 + 文字」胶囊,缺省为纯图标圆形
- badge:string | number —— 角标(数字或短文案,如 3 / '新');缺省不渲染
- tooltip:string —— 悬停提示,同时用作 aria-label;纯图标时必传,否则读屏听到默认「快捷操作」
- position(默认 right-bottom):'right-bottom' | 'left-bottom' —— 停靠角落;右下与 backTop 同侧,需同时出现时错开一左一右
- offset(默认 24):number —— 距边距离(px);同角落堆叠多个时按倍数递增
- onClick(回调):() => void —— 点击回调
示例:
```
document.body.append(floatButton({ icon: '+', label: '新建工单', position: 'left-bottom', onClick: () => toast.success('已新建') })); document.body.append(floatButton({ icon: '?', tooltip: '帮助中心', badge: 3 }))
```
手把手:
1. 把返回节点挂到 document.body(不是页面容器内):它是 fixed 定位,挂在容器里会被 overflow: hidden 裁剪
2. 只有图标时务必传 tooltip:它同时是 aria-label,读屏用户靠它知道按钮干什么;有 label 时 tooltip 可省
3. label 存在即「图标 + 文字」胶囊,只有图标即 44px 圆形;两种形态都保证触摸目标不小于 44px
4. 同一角落要放多个按钮时递增 offset(如 24 / 80),或把它们并进同一个 flex 容器统一排布——组件自身不做堆叠计算
5. 右下角已被 backTop 占用,页面同时有返回顶部时用 position: 'left-bottom'
6. 纯展示 + 单回调,没有选中态与开合状态;需要多级操作请用 menu / popover 挂在 onClick 上
现场演示:apps/docs/demo.html#sec-floatbutton
### mentions — @提及输入 Mentions
参数:
- items(必填):MentionItem[] —— 候选池:每项含 name(插入正文的名字)、label(显示名)、description(说明)、keywords(只参与过滤的额外匹配词)
- value:Signal<string> —— 受控文本信号;不传则组件内部维护(拿不到外部读取时不要传)
- rows(默认 3):number —— 输入框行数
- placeholder:string —— 占位文案
- onSelect(回调):(item: MentionItem) => void —— 选中某人后的回调(弹层已收起,正文已替换)
- onInput(回调):(text: string) => void —— 每次输入的回调,参数为当前全文(已写回 value 信号)
示例:
```
const text = signal(''); mentions({ items: [{ name: 'zhangsan', label: '张三', description: '前端', keywords: ['zs'] }], value: text, placeholder: '输入 @ 叫人', onSelect: (it) => toast.info(it.label) })
```
手把手:
1. 在输入框里键入 @ 触发弹层,继续打字按 name / keywords 过滤(拼音、工号放 keywords,不出现在候选行里)
2. 键盘:↑↓ 环形移动高亮、Enter 或 Tab 选中、Esc 只收这一层弹层(stopPropagation,不会连带关掉外层弹窗)
3. 选中后组件按光标位置把 @片段替换成「@名字 + 空格」,光标落在其后,可以接着打字
4. 受控用法:传 value 信号并在 onSelect / onInput 里读最新全文;外部改信号会同步回输入框,别同时再手写 input 事件
5. 弹层贴在输入框下沿而非跟随光标行(多行输入里更稳定,也不会被行高算错),需要跟随光标请自绘
6. 要限制可选人范围就在 items 上做过滤,组件不做远程搜索;远程场景用 onInput 拉数据后重建 items
现场演示:apps/docs/demo.html#sec-mentions
### treeSelect — 树形选择 TreeSelect
参数:
- nodes(必填):TreeNode[] —— 根节点列表,结构同 tree:key / label / children / disabled
- value:Signal<string> —— 单选选中 key;不传则内部维护
- checked:Signal<string[]> —— 多选勾选 key 集合(multi 时生效);不传则内部维护
- multi(默认 false):boolean —— 多选模式:显示勾选框、父子级联、选中后弹层不收起
- searchable(默认 false):boolean —— 弹层内带搜索框(按标签过滤并保留父链)
- placeholder(默认 请选择):string —— 未选中时触发器文案
- disabled(默认 false):boolean —— 禁用整个触发器(不响应点击)
- onSelect(回调):(node: TreeNode) => void —— 单选选中回调(此时弹层已收起)
- onChange(回调):(keys: string[]) => void —— 多选勾选变更回调,传副本且父子已级联
示例:
```
const dept = signal(''); treeSelect({ nodes: [{ key: 'tech', label: '技术中心', children: [{ key: 'fe', label: '前端组' }] }], value: dept, searchable: true, onSelect: (n) => toast.info(n.label) })
```
手把手:
1. 表单里选层级归属(部门 / 分类 / 区域)用它,不要用 cascader——cascader 要求逐级选到叶子,treeSelect 允许选中间节点,也允许任意深度
2. 单选默认选中即收起;要连续看结构就传 multi,勾选后弹层保持展开,触发器回显「已选 N 项」
3. 需要在大树里找人/找分类就开 searchable:搜索按标签过滤并保留命中节点的父链,路径不会断
4. Esc 与其他弹层共享键盘栈(只关最上层),点外部也会收起;展开时焦点自动落到搜索框
5. 选中值只存 key(单选 value / 多选 checked 数组),显示名由 nodes 反查——改 label 不需要迁移数据
6. 禁用单个节点用 nodes 里的 disabled(该节点不可选,子节点仍可选);禁用整个控件用 disabled
现场演示:apps/docs/demo.html#sec-treeselect
### link — 链接 Link
参数:
- href(必填):string —— 目标地址:站内相对路径或绝对 URL
- label(必填):string —— 链接文案
- target:'_self' | '_blank' | '_parent' | '_top' —— 打开方式;缺省同窗口。传 _blank 会自动按站外处理(补 rel 与图标)
- external(默认 false):boolean —— 站外链接:补 ↗ 图标、强制新窗口、加 rel=noopener noreferrer,并给读屏播报「在新窗口打开」
- muted(默认 false):boolean —— 弱化色,用于次要链接与页脚
- disabled(默认 false):boolean —— 禁用:去掉 href、拦点击,保留可聚焦与 aria-disabled(仍能被 Tab 到,读屏能听出不可用)
- onClick(回调):(e: Event) => void —— 点击回调(禁用时不触发;可用于埋点,returnValue 不影响默认跳转)
示例:
```
link({ href: '/docs', label: '查看文档' }); link({ href: 'https://example.com', label: '外部站点', external: true }); link({ href: '#', label: '说明页', muted: true })
```
手把手:
1. 站内跳转用相对路径、站外用 external: true:会自动补 ↗、target=_blank、rel=noopener noreferrer,并给读屏播报开了新窗口
2. 页脚 / 次要说明用 muted 弱化;正文里的行内链接保持默认主色,与 typography 的 underline 区分用途(后者不跳转)
3. 需要「点了先确认再跳」这类拦截,在 onClick 里做判断后自行处理,组件不接管默认行为(除禁用外都放行)
4. 禁用态只是去掉 href 并保持 aria-disabled,不做视觉上的「像普通文字」处理——需要完全不可聚焦请在外层控制渲染
5. 纯样式的「看起来像链接」用 typography({ underline: true }),不要为了样式引入带 href 的 a 标签
现场演示:apps/docs/demo.html#sec-link
### avatarGroup — 头像组 AvatarGroup
参数:
- items(必填):AvatarItem[] —— 成员列表:每项含 name(姓名)、src(图片地址)、variant(首字底色语义档)
- max(默认 4):number —— 最多展示几个,超出折叠成 +N(至少展示 1 个)
- size(默认 32):number —— 头像直径像素,与 avatar 的 size 同义
示例:
```
avatarGroup({ items: [{ name: '张三' }, { name: '李四', variant: 'success' }, { name: '王五' }], max: 4 })
```
手把手:
1. 协作列表 / 评论头部展示参与人用它,比并排 avatar 省横向空间:叠放用负边距 + 底色描边切出入口
2. 成员多时设 max:前 max 个照常显示,其余折叠成一个 +N,悬停任意头像都能看到全部姓名
3. 只传 name 即首字占位,有图片就传 src;底色语义档用 variant(default / success / warning / danger)
4. 整组是展示型元素:要有点击行为就在外面包 button 或 link,不要给单个头像加交互
5. size 与单个 avatar 一致,混排时把并排 avatar 与 avatarGroup 传同一个 size,视觉才对得上
现场演示:apps/docs/demo.html#sec-avatar
### virtualList — 虚拟列表 VirtualList
参数:
- items(必填):unknown[] —— 全量数据(不切片,由组件按滚动位置取窗口)
- itemHeight(必填):number —— 每行固定高度像素;虚拟化前提是行高一致
- height(必填):number —— 容器可视高度像素,超出即容器内滚动
- overscan(默认 5):number —— 上下各多渲染的缓冲条数;缺省 5,滚动更平滑
- renderItem(必填):(item, index) => unknown —— 行内容渲染器:返回字符串 / DOM / 响应式值
示例:
```
const vlist = virtualList({ items: users, itemHeight: 44, height: 320, renderItem: (u) => u.name }); vlist.scrollTo(500);
```
手把手:
1. itemHeight 必须与每行真实高度一致:行高由组件钉死,行内容超高会溢出,请保持单行或内部滚动
2. height 是容器可视高度(像素),列表在容器内滚动,不会撑开页面
3. renderItem 第二参是原始下标(全量数据中的位置),不是窗口内序号
4. 滚回某条用句柄 scrollTo(index),越界自动钳制;DOM 里只存在可视窗口的行,勿按全量下标查行节点
5. 数据整体变化(换数据源)请销毁重建实例:items 是创建时快照,不响应外部数组突变
6. 滚动事件内部已 rAF 合帧,滚动回调无需节流
现场演示:apps/docs/demo.html#sec-vlist
### commandPalette — 命令面板 CommandPalette
参数:
- open(必填):Signal<boolean> —— 显隐信号:外部写 true 唤起,组件内部关闭也会写回
- items(必填):CommandPaletteItem[] —— 全量命令,创建时快照;含 id / label / group / hint / keywords
- placeholder:string —— 搜索框占位文案;缺省「搜索命令…」
- emptyText:string —— 无匹配时的空状态文案;缺省「没有匹配的命令」
- hotkey(默认 true):boolean —— 是否注册 ⌘K / Ctrl+K 全局唤起;缺省 true,多实例场景可关掉自己绑
- onSelect(必填):(item) => void —— 回车或点击选中后回调:先收起并归还焦点,再回调
示例:
```
const cmdk = commandPalette({ open: signal(false), items: [{ id: 'save', label: '保存当前页', group: '文件', hint: '⌘S' }, { id: 'invite', label: '邀请成员', group: '成员' }], onSelect: (it) => toast.info(`执行 ${it.label}`) }); document.body.append(cmdk.el); cmdk.open();
```
手把手:
1. 把返回的 el 挂到 body(面板是 fixed + 遮罩,挂在 section 里也定位正确),之后用 open 信号或句柄 open()/close() 控制显隐
2. items 是创建时快照:命令表变化(权限变化后菜单增减)请重建实例,不要直接改数组
3. 过滤看 label / hint / keywords 三层:中文别名与拼音首字母塞进 keywords 即可支持,不必改组件
4. 同组连续排列才合并成一段并画组标题;分组顺序按 items 顺序,组件不排序
5. 键盘:↑↓ 环形选择、Home/End 跳首尾、回车执行、Esc 交给弹层栈(嵌套弹窗时只关最上层);鼠标划过即高亮
6. hotkey 缺省注册 ⌘K / Ctrl+K,同一页面多个面板请只留一个开 hotkey,其余传 hotkey: false
现场演示:apps/docs/demo.html#sec-cmdk
### timePicker — 时间选择 TimePicker
参数:
- value:Signal<string> —— 选中时间信号(HH:mm);不传则内部维护
- minuteStep:number —— 分钟步长;缺省 5,超过 30 按 30 处理(如 15 只有 00/15/30/45)
- clearable:boolean —— 显示清空按钮 ×(仅在有值时出现);缺省 false,键盘 Delete / Backspace 等价
- size:'sm' | 'md' | 'lg' —— 触发器高度档位,与 textField / select 同口径;缺省 md
- placeholder:string —— 未选时占位文案;缺省「请选择时间」
- disabled:boolean | Signal<boolean> —— 禁用触发器
- onChange:(time: string) => void —— 写回回调:选时即触发一次(分默认 00),选完分再触发一次
示例:
```
const at = signal('09:30'); timePicker({ value: at, minuteStep: 15, onChange: (t) => toast.info(`定在 ${t}`) })
```
手把手:
1. 创建信号 at = signal('09:30') 并传入 value:组件读它显示、选完写回,双向绑定
2. 点触发器展开时 / 分两列:当前值所在格高亮并自动滚到列中部;还没选过时停在此刻(当前小时 / 当前分钟档),打开即可就近选
3. 先点时列定小时(分默认落到首个步长值,值立刻可用),再点分列即完成并收起,焦点回到触发器
4. 列内可用 ↑↓ 环形移动、Home/End 直达首末;Tab、Esc、点击外部都能正常进出与收起
5. 需要 15 分钟一档就传 minuteStep: 15,步长只影响分列的可选值,时列始终 24 档
6. 需要允许回到「未选」态就传 clearable: true:有值时触发器右侧出现 ×(不进 Tab 序),聚焦触发器按 Delete / Backspace 也能清空,回调收到空串
7. 要和输入框并排对齐就传 size(sm / md / lg):高度走 --ua-control-h-* 三档,与 textField、select 的档位完全同源,不会出现「旁边的输入框矮 6px」
8. 传入值不在步长档上时(如 09:07 + 15 分钟档)不会「没有高亮」:分列按最近档高亮并滚到位,点它即落到 09:15;原值在触发器上照常显示,直到用户重新选
9. 收起态键盘微调:聚焦触发器按 ↑↓ 按步长加减(12:30 步长 15 → 12:45),Shift + ↑↓ 按小时加减,跨 00:00 / 23:xx 自动环形;改完同样触发 onChange
10. 打开弹层后焦点直接落在时列当前格(键盘用户不必再 Tab 找):列内 ↑↓ 立即生效,Esc 收起后焦点回到触发器
现场演示:apps/docs/demo.html#sec-timepicker
### pinInput — 验证码输入 PinInput
参数:
- value(必填):Signal<string> —— 当前验证码(数字拼接串)信号,双向绑定:格子输入写回、外部赋值同步回格子
- length(默认 6):number —— 格数;缺省 6
- mask(默认 false):boolean —— 密文显示(type=password,并声明 one-time-code 便于短信验证码自动填充);缺省明文
- disabled:boolean | Signal<boolean> —— 禁用态:布尔直接置位,传信号则动态同步
- onChange(回调):(value: string) => void —— 任一格变化回调(参数为当前已输入拼接串)
示例:
```
const code = signal(''); pinInput({ value: code, length: 6, onChange: (v) => console.log(v) })
```
手把手:
1. 用 signal('') 创建验证码信号并传入 value;拼接串随输入实时更新,长度满格即验证码就绪
2. 输入一位自动进下一格;退格在本格为空时回删上一格;←/→ 在格间移动;聚焦即全选,直接敲键覆盖
3. 粘贴会自动分发:整段验证码(位数 ≥ 格数)从首格铺起,零散片段从当前格续填
4. mask: true 走密文显示并声明 one-time-code,短信验证码可自动填充;disabled 支持布尔或信号禁用
现场演示:apps/docs/demo.html#sec-pininput
### copyButton — 复制按钮 CopyButton
参数:
- text(必填):string | (() => string) —— 待复制内容;传函数则在点击瞬间取实时值(如输入框当前值)
- label(默认 复制):string —— 按钮文案
- copiedLabel(默认 已复制):string —— 复制成功后的短暂文案(2s 后还原为 label)
- variant(默认 ghost):'primary' | 'ghost' | 'danger' —— 视觉强度,沿用按钮三档
- size(默认 md):'sm' | 'md' | 'lg' —— 高度档位
- disabled:boolean | Signal<boolean> —— 禁用态:布尔直接置位,传信号则动态同步
- onCopied(回调):(text: string) => void —— 复制成功回调(参数为实际写入的内容)
示例:
```
copyButton({ text: 'npm install @ui-arch/ui', onCopied: (t) => console.log(t) })
```
手把手:
1. 传 text 即可用,缺省幽灵按钮、文案「复制」;复制成功后短暂换成「已复制」,2s 自动还原
2. 内容需要实时取就传函数:text: () => textFieldValue(),点击瞬间才求值
3. label / copiedLabel 可改文案;variant / size 透传给内部 button,视觉与普通按钮完全一致
4. 非安全上下文(http)或旧浏览器自动降级为隐藏 textarea + execCommand,无需业务处理兼容
现场演示:apps/docs/demo.html#sec-copybutton
### countdown — 倒计时 Countdown
参数:
- seconds(必填):number —— 总时长(秒),> 0
- running(默认 true):boolean —— 创建即开始倒数;传 false 用句柄 start 手动启动
- format:(seconds: number) => string —— 展示格式化;缺省 mm:ss 两位补零
- onFinish(回调):() => void —— 归零回调(自动停表后触发)
示例:
```
const cd = countdown({ seconds: 60, onFinish: () => console.log('结束') }); // cd.start / stop / reset
```
手把手:
1. 传 seconds 即可用,缺省挂载即开始、mm:ss 展示;返回句柄 { el, start, stop, reset }
2. running: false 先摆好再手动 start,典型场景是「发送验证码」后才开始 60s 重发倒计时
3. 归零自动停表并触发 onFinish;之后 start() 会重开一轮,reset() 复位到总时长
4. format 自定义展示:如只显示秒 (s) => `${s}s`,或带上小时位
现场演示:apps/docs/demo.html#sec-countdown
### kbd — 键盘按键 Kbd
参数:
- keys(必填):string | string[] —— 按键内容:单键字符串或多键组合数组(组合以 + 连接展示)
示例:
```
kbd({ keys: 'Esc' })、kbd({ keys: ['Ctrl', 'K'] })
```
手把手:
1. 传字符串展示单键帽:kbd({ keys: "Esc" }),适合文档里说明单个按键
2. 传数组展示组合键:kbd({ keys: ["Ctrl", "K"] }),键帽之间以弱化的 + 连接(+ 对读屏隐藏)
3. 常与提示文案混排:行内 baseline 对齐,键帽底边加厚一线出立体感
4. 组件只画键帽,不做平台映射:Ctrl 与 ⌘ 的替换由调用方按平台判断后传进来(写死 Ctrl 在 Mac 上就是错的)
现场演示:apps/docs/demo.html#sec-kbd
### highlight — 关键词高亮 Highlight
参数:
- text(必填):string | Signal<string> —— 原文;传信号则随信号变化响应式重画
- keyword(必填):string | Signal<string> —— 关键词(大小写不敏感、全部命中,首尾空白忽略);空串不高亮、原样展示
示例:
```
highlight({ text: 'Alpha Beta', keyword: 'beta' })
```
手把手:
1. 传 text + keyword 即可,命中片段以淡黄 mark 底展示,适合搜索结果 / 补全列表的匹配说明
2. 两个入参都接受信号:搜索框输入实时联动高亮,无需手动重渲染
3. 大小写不敏感且全部命中(非只第一处);关键词两侧空白自动忽略
4. keyword 为空或纯空白时原样返回文本,既不抛错也不全高亮:搜索框清空那一刻不用特判
现场演示:apps/docs/demo.html#sec-highlight
### inputTag — 标签输入 InputTag
参数:
- value(必填):Signal<string[]> —— 已选标签集合,双向绑定:成标签与删 chip 都写回信号
- placeholder:string —— 未满时的占位文案;缺省「输入后回车添加」
- max:number —— 最多允许的标签条数;缺省 0 表示不限,满额后输入框禁用并提示上限
- maxLength:number —— 单个标签最大字符数;缺省 24,超出直接截断
- allowDuplicate:boolean —— 是否允许重复标签;缺省 false(重复的静默丢弃)
- disabled:boolean | Signal<boolean> —— 禁用态:布尔或信号,true 时不可输入也不可删
- ariaLabel:string —— 输入框无障碍标签;缺省「标签输入」
- onChange:(tags: string[]) => void —— 标签集合变化回调,参数是变化后的完整集合
示例:
```
const tags = signal(['前端', '设计']);
inputTag({ value: tags, max: 5, onChange: (list) => console.log(list.length) })
```
手把手:
1. 传一个 signal<string[]> 作为 value:回车或逗号把当前输入转成 chip,写回信号后立即重画
2. 点 chip 上的 × 删除单个标签,删完焦点交回输入框(chip 一删按钮就消失,不接住焦点键盘用户要从头 Tab);输入框为空时按退格键删掉最后一个(手不离键盘也能改)
3. 输入后直接失焦也会成标签,不必强行回车;max 满额后输入框禁用,占位文案变成「最多 N 个标签」
4. 整段粘贴「上海,北京,深圳」这类带分隔符的文字会自动拆成多个标签(半角/全角逗号、换行、制表符都认),单个值仍走浏览器默认粘贴
5. 要区分大小写重复判断就传 allowDuplicate: true;默认按原样去重
6. 样式复用输入组与标签:外框聚焦光环、chip 圆角与语义色都跟既有组件一致,无新增视觉字面量
现场演示:apps/docs/demo.html#sec-inputtag
### layout — 布局骨架 Layout
参数:
- header:unknown —— 页头内容(文案 / 节点);缺省不渲染页头
- sider:unknown —— 侧栏内容(通常是 menu);缺省则内容区独占整行
- footer:unknown —— 页脚内容;缺省不渲染页脚
- siderWidth:number —— 侧栏展开宽度(px);缺省 220
- collapsible:boolean —— 是否可折叠:true 时页头左侧出现折叠开关
- collapsed:Signal<boolean> —— 折叠态信号;传入则双向绑定(外部可读数),缺省内部自管
- sticky:boolean —— 页头是否吸顶;缺省 false(长内容里建议打开)
示例:
```
layout({ header: '控制台', sider: menu({ items }), footer: '© 2026', collapsible: true, sticky: true }, card({ title: '内容区' }, ...))
```
手把手:
1. 把整页交给 layout:children 就是内容区,页头 / 侧栏 / 页脚用 header / sider / footer 三个入参一次给全
2. 侧栏通常放 menu 或 anchor,宽度用 siderWidth 调(缺省 220px,够放四项中文菜单)
3. 需要折叠时传 collapsible: true,页头左侧自动出现开关;想把折叠态接到别处(比如只显示图标栏)就传 collapsed 信号读它的值
4. 长内容页面打开 sticky,页头吸顶;内容区自己滚动,不会把整页撑高
5. 只要内容不要骨架时省略 header / sider / footer:不套多余容器,内容区独占整行
现场演示:apps/docs/demo.html#sec-layout
### jsonViewer — JSON 树查看 JsonViewer
参数:
- value:unknown —— 任意可序列化数据:对象 / 数组 / 标量都行
- depth:number —— 默认展开层数;缺省 2(看得到一级结构又不至于刷屏)
- rootLabel:string —— 根节点键名;缺省 root
- maxHeight:number —— 最高高度(px),超出在框内滚动;缺省不限高
- ariaLabel:string —— 可访问名;缺省「JSON 数据」
示例:
```
jsonViewer({ value: payload, depth: 2, rootLabel: 'response', maxHeight: 320 })
```
手把手:
1. 把接口返回的原始对象直接交给 value:不用自己写 JSON.stringify 再塞进 pre,组件会按层折叠
2. 默认展开两层;数据层级深时调小 depth(1 层只看顶层键),扁平数据可调大
3. 长数据给 maxHeight,超出在框内滚动,滚动条已是统一细条(暗色下不会露出原生亮条)
4. 数组下标与对象键都走同一套行渲染,收起时右侧显示「3 项 / 2 个键」,先看规模再决定要不要展开
5. 要展示编辑结果时不要用它(只读):输入侧请用 textarea,展示侧再交给 jsonViewer
现场演示:apps/docs/demo.html#sec-jsonviewer
### diffView — 文本差异对比 DiffView
参数:
- before:string —— 旧文本(按行比对,CRLF 与 LF 等价)
- after:string —— 新文本
- showStats:boolean —— 是否在顶部显示 +N / −N 统计条;缺省 true
- ariaLabel:string —— 可访问名;缺省自动播报「新增 N 行,删除 M 行」
- ignoreWhitespace:boolean —— 比较时忽略行内空白差异(格式化提交不再淹没真实改动);缺省 false
示例:
```
diffView({ before: oldText, after: newText })
```
手把手:
1. 把改前 / 改后两份文本整串交给 before 与 after:不用先切行,CRLF 与 LF 视为同一行
2. 行级对齐先剪公共前后缀、再对中间段做 LCS:改两行不会把整篇标成差异
3. 增删行同时有底色与行首 + / − 符号两条通道,色盲用户也能分辨;行号列不参与复制
4. 顶部统计条给 +N / −N,读屏在容器上直接播报同一组数字,不必逐行听
5. 超大文本(中间段行数乘积超过 40 万)会退化成「整段替换」——语义仍对,只是不再逐行对齐;要逐行对齐就先分段比对
6. 格式化提交制造的大量「只差缩进 / 行尾空格」噪音,打开 ignoreWhitespace 后再看:显示仍是原文缩进,只是比对时把行内空白归一
现场演示:apps/docs/demo.html#sec-diff
### codeBlock — 代码块 CodeBlock
参数:
- code:string —— 代码原文:显示与复制都用它(不读 DOM,避免行号混进剪贴板)
- lang:string —— 语言名(如 ts / css);无 title 时作头部标题
- title:string —— 头部标题(如文件名);优先于 lang
- lineNumbers:boolean —— 是否显示行号列;缺省 true(单行代码自动省略)
- wrap:boolean —— 长行是否折行(窄屏 / 长日志场景);缺省 false(横向滚动,行号保持对齐)
- maxHeight:number —— 代码区最高高度(px),超出在框内滚动;缺省不限高
- copyable:boolean —— 右上角是否出现复制按钮;缺省 true
- onCopy:(text: string) => void —— 复制成功回调(参数为实际写入剪贴板的内容)
- ariaLabel:string —— 可访问名;缺省「代码块」
示例:
```
codeBlock({ code: source, title: 'main.ts', lang: 'ts', maxHeight: 320 })
```
手把手:
1. 把代码原文交给 code:显示与复制都读这一份,不要从 DOM 里取文本(行号会污染剪贴板)
2. 头部标题用 title(文件名)或 lang(语言名),两者都给时 title 优先
3. 长代码给 maxHeight,超出在框内滚动;滚动条已是统一细条,暗色下不会露原生亮条
4. 单行代码自动省掉行号列;想彻底不要行号传 lineNumbers: false
5. 不需要复制按钮(例如代码本身是操作步骤)传 copyable: false;连标题都没有时头部整条省掉
6. 窄屏或长日志要折行就传 wrap: true:长行按容器宽度换行,行号仍只占首行左侧;对齐要求高的代码别开,折行会让缩进失去参照
现场演示:apps/docs/demo.html#sec-code
### signaturePad — 手写签名 SignaturePad
参数:
- value:Signal<string> —— 签字结果 dataURL 信号,双向绑定;清空写空串
- height:number —— 画布高度(px);缺省 160
- penWidth:number —— 笔宽(px);缺省 2
- hint:string —— 底部提示文案;缺省「在上方区域手写签名」
- disabled:boolean | Signal<boolean> —— 禁用态:布尔或信号,禁用时不响应手写
- ariaLabel:string —— 画布可访问名;缺省「手写签名区」
- onChange:(dataUrl: string) => void —— 收笔 / 清空时回调(清空回调空串)
示例:
```
const pad = signaturePad({ value: sign, height: 140 }); mount('#box', () => pad.el); pad.toDataURL(); pad.clear();
```
手把手:
1. 把画布挂上去即可手写:宽度跟随容器、高度由 height 给(缺省 160),不用自己算 DP R
2. 签字结果在收笔瞬间写回 value 信号(PNG dataURL);要提交表单就把它塞进字段值,清空会写回空串
3. 底部「清空」按钮在未落笔或禁用时自动不可点,避免空操作;也能用返回的 handle.clear() 外部触发
4. 需要判空用 handle.isEmpty(),取图用 handle.toDataURL();两者与信号同源,不会出现「有图但信号是空」
5. 禁用传布尔或信号:布尔直接置位,信号随状态变灰并挡住手写;移动端画布已关掉浏览器手势,不会边签边滚页
现场演示:apps/docs/demo.html#sec-sign
### loading — 区域加载遮罩 Loading
参数:
- active:boolean | Signal<boolean> —— 是否加载中:布尔直接置位,传信号则随状态显隐
- text:string —— 遮罩文案;缺省只显示转圈(读屏仍报「内容加载中」)
- delay:number —— 出场延迟(ms);缺省 120,快请求不闪遮罩
- spinnerSize:number —— 转圈直径(px);缺省 20
- minHeight:number —— 最小高度(px);内容为空时兜住遮罩高度
示例:
```
loading({ active: busy, text: '正在刷新订单' }, dataTable({ ... }))
```
手把手:
1. 把要遮挡的那块内容作为 children 交进来,active 传布尔或信号:刷新时盖上遮罩,数据回来后自动收起
2. 内容不隐藏、只降透明度——布局与尺寸原地保留,遮罩收起时不会跳一下
3. 默认延迟 120ms 出场:接口快于这个时间就不闪遮罩;想立刻显示传 delay: 0
4. 遮罩文案写明在等什么(「正在刷新订单」),读屏同一句也会播报;只要转圈就省略 text
5. 内容可能为空时给 minHeight,否则高度塌成 0 遮罩无处可盖
现场演示:apps/docs/demo.html#sec-loading
### relativeTime — 相对时间 RelativeTime
参数:
- value:string | number | Date —— 目标时间:ISO 串 / 时间戳(10 位按秒、13 位按毫秒)/ Date
- refresh:number —— 自动刷新间隔(ms);缺省 30000,传 0 不刷新
- threshold:number —— 超过该秒数改用绝对日期;缺省 7 天
- fallback:string —— 时间非法时的占位文案;缺省「—」
- ariaLabel:string —— 可访问名(如「下单时间」),让读屏知道这个时间是什么
示例:
```
relativeTime({ value: row.createdAt, ariaLabel: '下单时间' })
```
手把手:
1. 把后端给的时间原样交进来:ISO 串、10 位秒级戳、13 位毫秒戳、Date 都认,不用自己 new Date
2. 默认每 30 秒刷新一次文案(「3 分钟前」会在不刷新页面的情况下变「4 分钟前」);列表里条数极多时可把 refresh 调大或传 0
3. 超过 7 天自动改显示绝对日期(YYYY-MM-DD HH:mm),想更早切换就调小 threshold
4. 精确时间放在 title:鼠标悬停可看,不必为「想知道具体哪天」再点开详情
5. 时间非法(空串 / 格式错)不猜也不抛,显示 fallback(缺省「—」),列表不会因一条脏数据空白
现场演示:apps/docs/demo.html#sec-reltime
### sparkline — 迷你趋势线 Sparkline
参数:
- values:number[] —— 数值序列(按时间顺序);非有限数会被剔除
- width:number —— 宽度(px);缺省 120
- height:number —— 高度(px);缺省 32
- area:boolean —— 是否填充折线下方面积;缺省 true
- marker:boolean —— 是否标记末点;缺省 true(一眼看出当前值位置)
- ariaLabel:string —— 可访问名;缺省「趋势图」
示例:
```
sparkline({ values: last7Days, width: 140, height: 36 })
```
手把手:
1. 把按时间顺序的数值数组交给 values:不用自己算最大最小与像素坐标,组件按 viewBox 等比缩放
2. 和 statistic 并排用:主数值走 statistic、走向走 sparkline,趋势线不画坐标轴也不带交互
3. 不要面积就传 area: false,不要末点圆点传 marker: false(极窄的表格单元格里圆点会糊成一团)
4. 改颜色在父级设 color:线、面积、圆点都读 currentColor,放进 card 或表格行自动跟随文字色
5. 少于两个点时画不出趋势,组件返回空图——请在外面判空并给「暂无数据」,别让它空着占位
现场演示:apps/docs/demo.html#sec-spark
### heatmap — 日历热力图 Heatmap
参数:
- values:{ date: string; count: number }[] —— 每一项 = 某天与当天数值;date 用 YYYY-MM-DD,同日多条自动求和
- weeks:number —— 显示最近多少周;缺省 12
- cellSize:number —— 格子边长(px);缺省 11
- legendText:string —— 图例左端文案;缺省「少」(右端固定「多」)
- ariaLabel:string —— 可访问名;缺省「最近 N 周活跃度」
- formatValue:(count: number, date: string) => string —— 悬停提示文案;缺省「日期 · 数量」
示例:
```
heatmap({ values: activity, weeks: 12, formatValue: (n, d) => d + ' 提交 ' + n + ' 次' })
```
手把手:
1. values 只给「有数据的那几天」即可:缺失的日期自动按 0 渲染,不必把 84 天全部补齐
2. 末列永远对齐到本周(周一到周日完整七行),所以数据是滚动的,不会出现半截列
3. 同一天多条记录会求和(合并成一个格子),上游按天预聚合也行,两种都吃
4. 格子颜色是相对深浅:按序列最大值归一成四档——所以它说的是「哪天更密」,不是绝对值高低
5. 默认 12 周、11px 格子;嵌进窄卡片就调小 weeks,看不出格子边界就调大 cellSize
现场演示:apps/docs/demo.html#sec-heat
### progressRing — 环形进度 ProgressRing
参数:
- value:number | Signal<number> —— 当前进度 0-100;超出范围自动夹取,信号变化时自动重绘
- size:number —— 外径(px);缺省 96
- thickness:number —— 环宽(px);缺省 8
- showText:boolean —— 是否显示中心文字;缺省 true(关掉时请自行保证 aria 可读)
- label:string —— 中心文字;缺省「N%」
示例:
```
progressRing({ value: pct, size: 96 })
```
手把手:
1. value 给数字就是静态环,给信号就是活环:不用自己算弧长,组件按 0-100 换算 dasharray
2. 0 点固定在 12 点方向顺时针走,和进度条的心理模型一致,不需要额外传旋转参数
3. 要放进表格或卡片角落就用 size: 56 + showText: false 的小环,文字由附近的主体数字承担
4. 换色在父级设 color:进度弧读 currentColor,底环固定描边色(--ua-color-border),不引入第二个色源
5. 这是个「环形进度条」,不是仪表盘:只表达 0-100 单一进度,多段占比请用饼图类组件
现场演示:apps/docs/demo.html#sec-ring
### noticeBar — 通知条 NoticeBar
参数:
- text:string —— 通知正文;单行不换行,超宽需配合 scroll
- variant:'info' | 'success' | 'warning' | 'danger' —— 语义色;缺省 info
- icon:string —— 左侧符号;缺省按 variant 取(ℹ / ✓ / ! / ×)
- scroll:boolean —— 正文超宽时跑马灯滚动;缺省 false(静态,超出裁切)
- closable:boolean —— 是否显示关闭按钮;缺省 true
- onClose:() => void —— 关闭回调
示例:
```
noticeBar({ text: '系统将于今晚 23:00 维护 10 分钟', scroll: true })
```
手把手:
1. 和 alert 的分工:alert 可多行带标题、占块;通知条只占一行,适合页顶常驻播报(维护公告、试用期提醒)
2. 文案比容器宽就传 scroll: true:正文从右向左匀速走,悬停自动暂停,读得完不用追
3. 只想播报不需要关闭就传 closable: false:右侧不再留 × 位,横条更紧凑
4. 颜色只是第二线索:左侧符号按 variant 自动配(ℹ / ✓ / ! / ×),色弱或灰度打印也能分辨
5. 跑马灯在减少动效偏好下自动停成静态首屏——完整原文始终在 title 里,不必为可访问性另写一份
6. 点 × 是先淡出再隐藏(200ms,和 alert 同一档),回调 onClose 立刻触发:想同步清理外部状态就在回调里做,别等隐藏
现场演示:apps/docs/demo.html#sec-notice
API 速览:
- button(props):按钮,variant 三档视觉强度,disabled / loading 支持布尔或信号动态同步(loading 转圈并禁点),onClick 点击回调
- textField(props):文本输入,value 信号双向绑定,disabled 支持布尔或信号,onInput 输入回调;prefix/suffix 可选渲染为输入框内侧附加块,clearable 开启后有值即显一键清空按钮,maxCount 设原生上限并实时显示字数
- inputNumber(props):数字输入,−/+ 按钮与键盘 ↑↓ 调节(长按连发步进),min/max/step 夹取,precision 小数位归一;prefix/suffix 可选渲染为整组前后附加块
- textarea(props):多行输入,value 信号双向绑定,disabled 支持布尔或信号,rows 控行数;prefix/suffix 可选渲染为上下附加块,maxCount 显示实时字数并设原生上限
- slider(props):滑块,value 信号双向绑定,min/max/step 可配,原生 range 承载键盘交互,右侧可显示当前值
- checkbox(props):复选框,checked 信号双向绑定,indeterminate 半选态(读屏播报 mixed),disabled 支持布尔或信号禁用
- switchButton(props):开关,checked 信号双向绑定,loading 加载锁(滑块转圈 + aria-busy),disabled 支持布尔或信号禁用
- select(props):自绘下拉选择(非原生 select),value 信号双向绑定,点外部 / Esc 关闭,选项弹层带微动画,disabled 支持布尔或信号禁用,空间不足自动翻转,键入字母快跳选项;searchable 开启弹层搜索框,clearable 有值时给清空按钮
- menu(props):下拉菜单,触发器 + 自绘弹层,条目支持禁用 / 危险 / 分隔线,↑↓ 环形导航,点外部 / Esc 关闭,空间不足自动翻转,键入字母快跳菜单项
- flip(trigger, panel, preferred):浮层上下翻转判定,按触发器与面板的视口几何决定 preferred 方位放不下时是否翻转,返回实际生效方位 'top' | 'bottom'
- radioGroup(props):单选组,options + value 信号双向绑定,disabled 整组禁用
- accordion(props):折叠面板,标题按钮 + grid 行高过渡,单开(手风琴)或多开,支持禁用面板与 active 信号受控
- breadcrumb(props):面包屑,nav + ol 语义结构,末项为当前页(aria-current),有 href 走原生链接、无 href 交给 onNavigate
- card(props, ...children):卡片容器
- badge(props):徽标,variant = default | success | warning | danger
- tag(props):可关闭标签,variant 五档语义色,closable + onClose 支持从数据源移除(纯展示请用 badge)
- avatar(props):头像,有 src 渲染图片(alt 用名称)、无 src 用首字占位,size 可调 + 四档语义底色
- alert(props):提示条,variant 四档语义色 + 可选 title 与 closable 关闭按钮,role=alert 读屏可播报
- toast.info/success/warning/error(message, duration?):消息提示工厂,右上角堆叠,默认 3s 自动消失(悬停暂停、移出续时),出入场微动画;showToast(props) 为完整入参版本(closable 显示关闭按钮,同屏最多 4 条)
- tooltip(props, trigger):包裹式悬浮提示,hover / 键盘聚焦延迟出场,Esc 收起;触摸屏按下即出、抬手延迟 1.2s 收起,气泡 pointer-events: none 不挡点击
- popover(props, trigger):可交互气泡卡片,包裹触发器,默认点击开合 + 点外部 / Esc 收起,content 可放按钮等交互元素;视口一侧空间不足时自动翻转
- skeleton(props):骨架屏占位,variant = text | rect | circle,text 支持 rows 多行,扫光微动画
- pagination(props):独立分页器,page 信号双向绑定,页码过多折叠省略号,首末页禁用翻页,jumpable 时末尾可输入页码直接跳转,键盘可达;pageSizes 可切每页条数
- spinner(props):加载指示器,size 控制直径
- progress(props):进度条,value 信号驱动 0-100(满格切 success 色);不传 value 退化为不确定态无限往复
- tabs(props):标签页,active 信号双向绑定,滑动指示条 + 方向键环形导航,支持禁用页签;页签项 closable 时带关闭按钮(聚焦后 Delete 同样可关),关闭只回调 onClose 由调用方摘数据
- stepper(props):步骤条,done/current/upcoming 三态,active 信号双向绑定,clickable 时已完成步骤可回跳
- rate(props):评分星标,value 信号双向绑定,hover 预览移出还原,键盘方向键调节,max 可配,allowHalf 开启半星(左半区 0.5 分)
- empty(props):空状态占位,title/description 可选,action 放操作区
- divider(props):分隔线,水平/垂直两种方向,可带居中文案标签,dashed 切虚线
- space(props):间距容器,flex gap 统一子元素间距,direction / size / align / justify / wrap / block 可调
- modal(props, ...children):弹窗,open 信号驱动显隐,Esc / 遮罩 / × 关闭,size 三档宽度,footer 提供底部操作区
- drawer(props, ...children):抽屉,open 信号驱动侧滑面板,placement 左右可配、width 可调、footer 操作区,遮罩 / × / Esc 关闭
- form(props, items):表单容器,布局 + 聚合校验,全部通过才触发 onSubmit
- formItem(props, control):表单字段,label + 必填红星 + 规则校验 + 错误区,返回 { el, validate } 句柄
- dataTable(props):数据表格,粘性表头 / 行选择(全选三态)/ 排序 / 筛选 / 分页 / 空状态;loading 为真时数据行换骨架行且筛选/翻页禁用,内核为 table-core
- image(props):图片,缩略图点击 / 回车开遮罩大图预览(Esc 或点击遮罩关闭),width 控制缩略宽,加载失败自动落占位(fallback 自定义文案)
- timeline(props):时间线,items 按序渲染纵向事件流(标题 / 时间 / 正文 + 语义色节点),mode 控制轴线左右,pending 追加进行中节点
- segmented(props):分段控制器,胶囊分段 + 滑动滑块,value 信号双向绑定,←→ 环形导航,整组或单项可禁用
- descriptions(props):描述列表,items(label/value/span) 铺进 column 列网格,bordered 切表格形态,窄屏自动单列
- statistic(props):统计数值,标题 + 大号数字 + 前后缀 + 环比趋势,value 变化时 rAF 滚动过渡(400ms easeOutCubic),减弱动效偏好下直落终值
- list(props):列表,header/footer 外壳 + 条目 avatar/title/description/extra,onItemClick 让条目可点击,items 为空回落空状态
- carousel(props):轮播,items 横排位移,指示点/箭头/←→ 键切换,autoplay 毫秒自动前进且悬停暂停,index 信号可外部受控
- tree(props):树形,递归节点 + 箭头展开/收起 + 点选节点,键盘 Enter 选中、→ 展开、← 收起,禁用节点不响应
- upload(props):文件选择,点击或拖拽入列,清单可逐个移除,onChange 回传文件数组(不内置上传请求)
- notification(props):右下角通知卡片(标题 + 描述 + 操作区),返回 { el, close } 句柄,缺省常驻需手动关闭
- result(props):结果页,status 四档语义色 + 标题 + 说明 + 操作区,用于提交完成/校验失败等终态
- transfer(props):穿梭框,左右两栏勾选后整体搬运,targetKeys 信号双向绑定,两栏各自搜索与全选(含半选),禁用条目不参与搬运也不可勾选
- cascader(props):级联选择,多列联动面板(悬停下钻 / 点击提交),value 为路径数组信号,changeOnSelect 允许停在中间层,全键盘可操作
- splitter(props):可拖拽分栏容器,分隔条支持指针拖拽 / 方向键微调 / 双击复位,sizes 为百分比数组信号
- autoComplete(props):输入联想,输入即过滤候选、↑↓ 环形高亮 / Enter 选中 / Esc 收起,options 可传函数做远程联想
- datePicker(props):日期选择,单月网格 + 翻月 / 今天快捷键,value 为 YYYY-MM-DD 信号,min/max 闭区间限制,方向键逐日逐周可导航
- backTop(props):返回顶部悬浮按钮,越过阈值才浮现,外环 conic-gradient 显示已滚比例,点击平滑回顶
- colorPicker(props):颜色选择,预设色板(缺省语义令牌色)+ 原生取色器 + 手输 CSS 颜色串,value 为颜色串信号
- tour(props):引导漫游,返回 { el, start, close } 句柄;聚光框按 target 选择器定位,卡片自动选方位,Esc / 跳过走 onClose
- watermark(props, ...children):内容容器 + 平铺水印覆盖层,canvas 生成单元、颜色取自令牌,覆盖层不挡交互
- checkboxGroup(props):多选组,value 为数组信号,min/max 用禁用边界项实现(不抛错),checkAll 支持半选且只统计未禁用项
- affix(props, ...children):固钉,滚过 offset 后内容转 fixed 贴顶、外层保留占位高度,固定后宽度随容器自适应
- multiSelect(props):多选下拉,标签式回显 + 可搜索弹层,点选不收起便于连续多选,max 到限后未选项禁用
- anchor(props):锚点导航,竖排区块链接,滚动时按目标位置高亮当前区块(aria-current 同步),点击按 offset 偏移量平滑定位并回写 hash;container 可指定内滚容器
- popconfirm(props, trigger):二次确认气泡,挂在任意触发器上,就地确认危险操作;确定后先收起再回调,取消 / 点外部 / Esc 走 onCancel,展开即聚焦「取消」
- calendar(props):月历,整月网格内嵌页面(排期 / 值班 / 日程),每日可带 marker 标记文案,方向键漫游 + Enter 选中,翻月走 onMonthChange
- typography(props):排版文本,variant 决定语义(标题出 h1-h5 / 段落出 p / 行内出 span),支持语义色、装饰位、对齐、多行截断与复制按钮
- grid(props, ...children):栅格容器,columns 固定列数或 min 走 auto-fit 自适应列宽,gutter 统一列间距,窄屏自动降为单列
- floatButton(props):悬浮操作按钮,固定停靠左下 / 右下角,label 缺省为纯图标圆形,badge 走角标,tooltip 兼作 aria-label
- mentions(props):@提及输入,textarea 内键入 @ 弹候选(名字 / 关键词过滤),↑↓ 环形移动、Enter/Tab 选中、Esc 只收本层,选中按光标替换并写回 value
- treeSelect(props):树形选择,按钮触发弹层并复用 tree(搜索 / 多选 / 级联),单选选中即收起、多选保持展开,Esc 与点外收起
- link(props):链接,站内跳转与站外跳转统一入口,external 自动补 ↗ 与 rel=noopener noreferrer 并给读屏播报
- avatarGroup(props):头像组,横向叠放并在超出 max 时折叠成 +N,整组 title 列出全部姓名
- virtualList(props):虚拟列表,固定行高长列表只渲染可视窗口 + 上下缓冲条,万级数据滚动不随总量掉帧;返回 { el, scrollTo } 句柄
- commandPalette(props):命令面板,⌘K / Ctrl+K 唤起,输入即过滤(主文案/说明/关键词三层),↑↓ 环形选择 + 回车执行 + Esc 收起,选中与关闭都归还焦点;返回 { el, open, close } 句柄
- timePicker(props):时间选择,时 / 分两列弹层,value 信号双向绑定 HH:mm,分钟可配步长,Esc 收起
- pinInput(props):验证码输入,N 格单字符数字输入,输入自动进格、退格回删、方向键移格、粘贴自动分发,value 信号双向绑定,mask 可密文显示
- copyButton(props):复制按钮,点击把 text 写入剪贴板并短暂显示已复制,text 可传函数取实时值,clipboard API 不可用时降级 execCommand
- countdown(props):倒计时,秒级递减到 0 自动停表并触发 onFinish,缺省 mm:ss 展示,返回句柄 start/stop/reset
- kbd(props):键盘按键标记,键帽样式展示单个按键或组合键(多键以 + 连接),用于快捷键提示
- highlight(props):关键词高亮,命中片段包 mark(大小写不敏感全部命中),text / keyword 支持信号响应式刷新
- inputTag(props):标签输入,输入后回车 / 逗号成标签、chip 可单个删除、空框退格删末个,value 信号双向绑定,max 满额自动禁输
- layout(props, ...children):布局骨架,页头 / 侧栏 / 内容 / 页脚四区一次成型,侧栏可折叠(collapsed 信号双向绑定)
- jsonViewer(props):JSON 树查看,按层折叠、分支带计数摘要、叶子按类型着色,只读不编辑
- diffView(props):文本差异对比,行级 LCS 对齐 + 增删着色 + 双行号列,顶部带 +N / −N 统计
- codeBlock(props):代码块,等宽多行 + 行号列 + 标题行 + 一键复制原文,只展示不高亮
- signaturePad(props):手写签名板,指针绘制 + DPR 位图,返回 handle(el / clear / toDataURL / isEmpty)
- loading(props, ...children):区域加载遮罩,内容原地降透明度 + 转圈文案,带延迟出场避免快请求闪烁
- relativeTime(props):把时间戳渲染成「3 分钟前」并自动刷新,超阈值改显示绝对日期,title 恒给精确值
- sparkline(props):迷你趋势线,把数值序列压成折线(可填充 + 末点标记),无坐标轴无交互
- heatmap(props):日历热力图,按周列 × 星期行铺格子、四档浓度表示当天数量,悬停看明细
- progressRing(props):环形进度,SVG 双圆靠 dasharray 画弧 + 中心百分比,value 可传信号自动重绘
- noticeBar(props):单行通知条,语义色 + 左侧符号 + 可选跑马灯与关闭,适合页顶常驻播报
一行用法:
```
const checked = signal(false); checkbox({ label: '同意', checked }); const open = signal(false); modal({ title: '确认', open }, '内容')
```
约定:
- 皮肤不持有业务状态,一律委托 headless 内核或调用方信号
- 表单类组件统一约定:*Props.value/checked/active 为 Signal 双向绑定,onChange/onInput 为回调
- 语义色 variant 四档:default|info / success / warning / danger,一律来自令牌,禁止硬编码颜色
- 无障碍基线:装饰性占位(skeleton)aria-hidden、加载指示(spinner)role=status、关闭按钮必须 aria-label、弹层弹窗 aria-haspopup/expanded、读屏可见的状态一律同步 aria-*
---
## @ui-arch/frame(headless-core)v0.1.0
零依赖微框架:组件 = 返回 DOM 的普通函数。signal/effect/computed 管状态,h() 管 DOM,无虚拟 DOM 无生命周期抽象。
API 速览:
- signal(initial) -> Signal<T>:无参读值并收集依赖,传参写值并触发订阅
- effect(fn):立即执行并追踪依赖,信号变化自动重跑
- computed(fn):由信号派生的只读计算值
- h(tag, props, ...children):hyperscript;on* 绑事件,函数值属性/子节点自动响应式
- renderList(el, fn):信号变化时整体替换容器子节点
- mount(target, view):挂载视图
一行用法:
```
const count = signal(0); mount('#app', () => h('button', { onClick: () => count(count() + 1) }, () => `count = ${count()}`))
```
约定:
- 组件 = 返回 DOM 节点的普通函数,不做类组件与生命周期抽象
- 跨系统状态(如 table-core)用桥接信号:subscribe 里递增 version 信号
---
## @ui-arch/table-core(headless-core)v0.1.0
Headless 表格内核:不渲染任何 DOM,只管理状态与派生。任何渲染层通过 getVisibleRows() 拿行模型,配 subscribe() 刷新。
API 速览:
- createTable(options) -> TableInstance: create headless table instance
- sortPlugin(options?) -> multi-column sorting plugin (asc -> desc -> off cycle)
- filterPlugin(options?) -> column filter plugin (default substring match, custom predicate supported)
- paginationPlugin() -> client-side pagination with page clamping, exposes setPage/nextPage/prevPage/getPageInfo
- selectionPlugin() -> rowKey-based selection, exposes selectAllVisible
一行用法:
```
const table = createTable({ data, columns, rowKey: 'id', plugins: [filterPlugin(), sortPlugin(), paginationPlugin()], initialState: { pagination: { page: 1, pageSize: 20 } } }); table.dispatch({ type: 'toggleSort', key: 'name' }); table.getVisibleRows()
```
约定:
- selection/expanded must store rowKey, never row index
- derive pipeline order is fixed: filter -> sort -> paginate
- actions are the only way to mutate state; state is immutable
---
## @ui-arch/tokens(design-tokens)v0.1.0
设计令牌唯一来源:CSS 变量 --ua-* + 同名 JSON。皮肤只允许消费变量,禁止硬编码颜色;暗色只换值不换名。
API 速览:
- tokens.css: import '@ui-arch/tokens/tokens.css' once at app root
- tokens.json: programmatic access for tools and agents
一行用法:
```
import '@ui-arch/tokens/tokens.css'; then use var(--ua-color-accent) etc. in any skin package
```
---
## @ui-arch/cli(tooling)v0.1.0
工程层工具包(private,不发布)。所有 Agent 与人都通过它读取组件库:清单驱动索引、文档、MCP 三件套,外加 style / verify 两道门禁。
API 速览:
- ua doctor:11 项环境与自洽预检(node / 依赖 / git 钩子与身份 / .ua 元数据可解析 / usage 串 ↔ dispatch 一致 / check 链覆盖全部门禁步骤 / CLI 清单声明的命令都有实现),任一失败退出码 1
- ua index:扫描 packages/ 两层目录的 ua.manifest.json,生成 .ua/index.json 与 llms.txt
- ua list:列出注册表内所有包与组件数;--json 输出「包 + 组件 id」结构化数组,--components 逐组件展开
- ua info <包名|组件id>:包名返回摘要,组件 id 返回 props 表 + 步骤 + 示例;--json 输出原始清单,--md 直接给可粘贴的 Markdown 片段(标题 + 参数表 + 用法 + 示例)
- ua tokens [关键词] [--json|--unused]:打印 --ua-* 令牌清单(颜色带暗色值);--unused 反向列出皮肤包与令牌自身都没引用的令牌,用来收口令牌面
- ua docs:生成人类文档站 apps/docs/index.html + demo.html 与 Agent 全量文档 llms-full.txt
- ua style:注释规范门禁(导出必须 /*** ***/、接口字段行尾注释、行注释中文),违规退出码 1
- ua verify:清单一致性门禁(id 唯一 / exports 对应 / props 必带 n t d / steps≥3 / demo 锚点存在 / 视觉回归抽查清单六组件各 ≥3 条且五维度齐全)
- ua commit-lint <文件|message>:提交信息门禁(type 枚举 / scope 规则 / subject 中文祈使 ≤50 字符),规范见 COMMITTING.md,违规退出码 1
- ua audit:visual [--json|--baseline]:视觉漂移门禁,统计皮肤包里各类硬编码视觉字面量(字号/动效时长/阴影/圆角/焦点环/内距/控件高度/字重/行高/层级/边框色)并与 .ua/visual-baseline.json 棘轮比对,超基线退出码 1;--baseline 重写基线,--json 出机器可读计数与每类命中行号(line / prop / value / hint,Agent 可直接定位)
- ua audit:contrast [--json|--list]:对比度门禁,把令牌里 18 组「前景色 × 底色」配对按 WCAG 公式算成对比度(求值支持 var() 与 color-mix,半透明底先压到页面底色),亮/暗 × 常规/高对比四套分算,低于阈值(正文 7 / 常规 4.5 / 禁用 2.6 / 非文本 3)退出码 1;--json 出逐对明细,--list 只列配对表
- ua audit:motion [--json]:动效兜底门禁,两类必查——① 带 infinite 的动画逐个核对是否在 prefers-reduced-motion 块里 animation: none;② 悬停 / 按压的位移(translate)是否在减少动效下归零(认 ua- 类通配的全局规则)。缺兜底退出码 1(--json 出逐条明细)
- ua mcp:零依赖 stdio MCP server,暴露 list_components / get_component / search_components / get_tokens / list_recipes / get_recipe;get_component 支持 format: md 直接取可粘贴的 Markdown 片段
一行用法:
```
node packages/tools/cli/bin/ua.js <index|list|info <包名|组件id> [--json|--md]|docs|tokens [--json|--unused]|style|verify|commit-lint|audit:visual|audit:contrast|audit:motion|doctor|mcp>;仓库内等价于 npm run index / docs / style / verify / audit:visual / audit:contrast / audit:motion / doctor
```
约定:
- 零依赖:只用 node 标准库,Node >= 18
- 清单是单一来源:代码注释、文档站、llms-full.txt、MCP 全部从 ua.manifest.json 派生
- 门禁只增不改:新增校验必须能被人手复现,报错要指出文件与字段
---
## 设计令牌(--ua-*)
- --ua-color-bg: #ffffff(暗色 #101216)
- --ua-color-surface: #f6f7f9(暗色 #1a1c22)
- --ua-color-surface-2: color-mix(in srgb, var(--ua-color-text) 4%, var(--ua-color-bg))(暗色 color-mix(in srgb, var(--ua-color-text) 5%, var(--ua-color-bg)))
- --ua-color-surface-3: color-mix(in srgb, var(--ua-color-text) 8%, var(--ua-color-bg))(暗色 color-mix(in srgb, var(--ua-color-text) 10%, var(--ua-color-bg)))
- --ua-color-surface-inset: color-mix(in srgb, var(--ua-color-text) 5%, var(--ua-color-bg))(暗色 color-mix(in srgb, var(--ua-color-text) 6%, var(--ua-color-bg)))
- --ua-color-text: #1c1e21(暗色 #e7e9ec)
- --ua-color-text-muted: #676d78(暗色 #888780)
- --ua-color-text-secondary: #4b5158(暗色 #b4bac2)
- --ua-color-text-faint: #9aa0a8(暗色 #62676f)
- --ua-color-overlay: rgba(0, 0, 0, 0.45)(暗色 rgba(0, 0, 0, 0.6))
- --ua-color-overlay-strong: rgba(0, 0, 0, 0.6)(暗色 rgba(0, 0, 0, 0.7))
- --ua-color-border: rgba(0, 0, 0, 0.15)(暗色 rgba(255, 255, 255, 0.15))
- --ua-color-scrollbar: rgba(0, 0, 0, 0.22)(暗色 rgba(255, 255, 255, 0.24))
- --ua-color-scrollbar-hover: rgba(0, 0, 0, 0.38)(暗色 rgba(255, 255, 255, 0.42))
- --ua-color-highlight: rgba(255, 255, 255, 0.5)(暗色 rgba(255, 255, 255, 0.08))
- --ua-color-accent: #185fa5(暗色 #4a9aeb)
- --ua-color-accent-contrast: #ffffff(暗色 #042c53)
- --ua-color-accent-soft: color-mix(in srgb, var(--ua-color-accent) 72%, var(--ua-color-bg))(暗色 color-mix(in srgb, var(--ua-color-accent) 82%, var(--ua-color-bg)))
- --ua-color-tint-accent: color-mix(in srgb, var(--ua-color-accent) var(--ua-alpha-soft), transparent)(暗色 color-mix(in srgb, var(--ua-color-accent) 14%, transparent))
- --ua-color-tint-accent-weak: color-mix(in srgb, var(--ua-color-accent) 5%, transparent)(暗色 color-mix(in srgb, var(--ua-color-accent) 8%, transparent))
- --ua-color-danger: #a32d2d(暗色 #d96a6a)
- --ua-color-success: #0f6e56(暗色 #4fc39e)
- --ua-color-warning: #9a6b1f(暗色 #d9a94f)
- --ua-alpha-soft: 10%
- --ua-alpha-tint: 18%
- --ua-alpha-line: 30%
- --ua-alpha-mid: 45%
- --ua-alpha-strong: 55%
- --ua-alpha-heavy: 70%
- --ua-radius-sm: 4px
- --ua-radius-md: 8px
- --ua-radius-lg: 12px
- --ua-radius-md-inset: calc(var(--ua-radius-md) - 1px)
- --ua-space-1: 4px
- --ua-space-2: 8px
- --ua-space-3: 12px
- --ua-space-4: 16px
- --ua-space-05: 2px
- --ua-space-15: 6px
- --ua-space-25: 10px
- --ua-space-35: 14px
- --ua-space-table-cell: var(--ua-space-2) var(--ua-space-3)
- --ua-space-6: 24px
- --ua-space-indent: 22px
- --ua-font-size-xs: 11px
- --ua-font-size-sm: 12px
- --ua-font-size-md: 13px
- --ua-font-size-lg: 14px
- --ua-font-size-xl: 16px
- --ua-font-size-2xl: 18px
- --ua-font-size-3xl: 22px
- --ua-font-size-4xl: 26px
- --ua-font-size-5xl: 34px
- --ua-font-size-scale-md: 0.92em
- --ua-font-size-scale-sm: 0.85em
- --ua-tracking-wide: 0.04em
- --ua-tracking-tight: -0.01em
- --ua-weight-normal: 400
- --ua-weight-medium: 500
- --ua-weight-semibold: 600
- --ua-weight-bold: 700
- --ua-leading-none: 1
- --ua-leading-tight: 1.35
- --ua-leading-normal: 1.55
- --ua-border-subtle: color-mix(in srgb, var(--ua-color-border) 60%, transparent)
- --ua-shadow-1: 0 1px 2px rgba(0, 0, 0, 0.06)(暗色 0 1px 2px rgba(0, 0, 0, 0.3))
- --ua-shadow-2: 0 4px 12px rgba(0, 0, 0, 0.1)(暗色 0 4px 12px rgba(0, 0, 0, 0.4))
- --ua-shadow-3: 0 10px 28px rgba(0, 0, 0, 0.12)(暗色 0 10px 28px rgba(0, 0, 0, 0.45))
- --ua-shadow-4: 0 18px 44px rgba(0, 0, 0, 0.14)(暗色 0 18px 44px rgba(0, 0, 0, 0.5))
- --ua-shadow-accent: 0 3px 10px color-mix(in srgb, var(--ua-color-accent) 25%, transparent)(暗色 0 3px 10px color-mix(in srgb, var(--ua-color-accent) 30%, transparent))
- --ua-shadow-accent-strong: 0 8px 20px color-mix(in srgb, var(--ua-color-accent) 35%, transparent)(暗色 0 8px 20px color-mix(in srgb, var(--ua-color-accent) 40%, transparent))
- --ua-shadow-highlight: inset 0 1px 0 var(--ua-color-highlight)
- --ua-shadow-hairline: inset 0 -1px 0 var(--ua-border-subtle)
- --ua-shadow-inset-border: inset 0 0 0 1px var(--ua-color-border)
- --ua-shadow-inset-accent: inset 0 0 0 1px var(--ua-color-accent)
- --ua-shadow-gap: 0 0 0 2px var(--ua-color-bg)
- --ua-duration-fast: 0.15s
- --ua-duration-base: 0.2s
- --ua-duration-slow: 0.25s
- --ua-duration-slower: 0.35s
- --ua-duration-loop-fast: 0.6s
- --ua-duration-loop-mid: 0.8s
- --ua-duration-loop-slow: 1.2s
- --ua-duration-loop-long: 1.6s
- --ua-duration-step: 0.02s
- --ua-ease: cubic-bezier(0.2, 0.8, 0.2, 1)
- --ua-ease-out: cubic-bezier(0.16, 1, 0.3, 1)
- --ua-ease-spring: cubic-bezier(0.34, 1.3, 0.5, 1)
- --ua-translate-1: 1px
- --ua-translate-2: 2px
- --ua-translate-3: 3px
- --ua-translate-4: 4px
- --ua-translate-6: 6px
- --ua-translate-8: 8px
- --ua-translate-12: 12px
- --ua-focus-ring: 0 0 0 3px color-mix(in srgb, var(--ua-color-accent) 22%, transparent)(暗色 0 0 0 3px color-mix(in srgb, var(--ua-color-accent) 34%, transparent))
- --ua-focus-ring-inset: inset 0 0 0 2px color-mix(in srgb, var(--ua-color-accent) 40%, transparent)(暗色 inset 0 0 0 2px color-mix(in srgb, var(--ua-color-accent) 52%, transparent))
- --ua-focus-ring-warning: 0 0 0 3px color-mix(in srgb, var(--ua-color-warning) 25%, transparent)(暗色 0 0 0 3px color-mix(in srgb, var(--ua-color-warning) 34%, transparent))
- --ua-focus-ring-danger: 0 0 0 3px color-mix(in srgb, var(--ua-color-danger) 18%, transparent)(暗色 0 0 0 3px color-mix(in srgb, var(--ua-color-danger) 30%, transparent))
- --ua-focus-ring-pulse: 0 0 0 5px color-mix(in srgb, var(--ua-color-accent) 4%, transparent)(暗色 0 0 0 5px color-mix(in srgb, var(--ua-color-accent) 8%, transparent))
- --ua-focus-ring-selected: 0 0 0 1.5px var(--ua-color-accent-soft)
- --ua-focus-outline: 2px solid var(--ua-color-accent)
- --ua-focus-outline-inset: 1.5px solid var(--ua-color-accent-soft)
- --ua-focus-outline-soft: 2px solid color-mix(in srgb, var(--ua-color-accent) var(--ua-alpha-mid), transparent)
- --ua-control-h-sm: 28px
- --ua-control-h-md: 34px
- --ua-control-h-lg: 40px
- --ua-z-dropdown: 100
- --ua-z-sticky: 200
- --ua-z-overlay: 300
- --ua-z-modal: 400
- --ua-z-toast: 500
皮肤包只允许消费这些变量:写死 #hex 会被 verify 拦截,拼错变量名会被令牌引用校验拦截。
# 组合配方(Recipes)
> 单个组件的参数表说明「怎么用」,这些配方说明「怎么拼」。所有配方都只消费组件对外 API,不依赖内部文件。
## 配方 1:表单校验 + 提交反馈
```ts
const name = signal('');
const email = signal('');
const formRef = form({ onSubmit: (ok) => (ok ? result({ status: 'success', title: '已保存' }) : toast.error('请检查表单')) }, [
formItem({ label: '名称', value: name, required: true, rules: [{ min: 2 }] }, textField({ value: name })),
formItem({ label: '邮箱', value: email, required: true, rules: [{ pattern: /^\S+@\S+$/ }] }, textField({ value: email })),
]);
```
要点:
- 校验规则挂在 `formItem`,值信号只有一个来源(`value` 同时给 `formItem` 与控件),避免两处各存一份。
- 提交结果用 `toast`(轻量、瞬时)或 `result`(整页终态)表达,不要两者同时弹。
## 配方 2:危险操作二次确认
```ts
const open = signal(false);
const dangerOpen = (key: string) => {
open(true);
return modal({
title: `确认删除 ${key}?`,
open,
size: 'sm',
onClose: () => open(false),
footer: h('div', { class: 'row' },
button({ label: '取消', variant: 'ghost', onClick: () => open(false) }),
button({ label: '删除', variant: 'danger', onClick: () => { open(false); toast.success('已删除'); } })),
}, h('p', null, '删除后不可恢复。'));
};
```
要点:
- `modal` 自带焦点陷阱与 Esc 逐层关闭,不要在业务层再挂 Esc 监听。
- 确认按钮用 `variant: 'danger'`,取消用 `ghost`,主次分明。
## 配方 3:表格筛选 + 分页 + 批量操作
```ts
const picked = signal<string[]>([]);
const page = signal(1);
dataTable({
data: rows,
columns: [{ key: 'name', header: '名称', sortable: true }, { key: 'city', header: '城市' }],
pageSize: 10,
page, // 同一 page 信号也给 pagination,二者自动联动
selectable: true,
selected: picked,
loading: saving,
});
pagination({ page, pageSize: 10, total: rows.length, jumpable: true, onChange: (p) => page(p) });
```
要点:
- `page` 信号是表格与分页器的唯一共享状态;外部改页码前自己钳制范围(core 的 `setPage` 不做钳制)。
- 选中一律存 `rowKey`,翻页/筛选后仍然稳定;全选只作用于当前可见行。
- 批量操作按钮在 `picked().length === 0` 时禁用,避免空提交。
## 配方 4:长列表数据加载与空态
```ts
const loading = signal(true);
const items = signal<ListItem[]>([]);
list({ items: items(), bordered: true, emptyText: loading() ? '加载中…' : '暂无数据' });
```
要点:
- 空态文案随加载态切换;列表本身会在 `items` 为空时渲染 `empty`,不需要业务层再判断一次。
- 需要骨架屏就换成 `skeleton`,加载完成再 `renderList` 成真实行。
## 配方 5:数据表 + 独立分页器 + 导出
```ts
const page = signal(1);
const table = dataTable({
data: rows,
columns: [
{ key: 'name', header: '姓名', sortable: true, width: 160 },
{ key: 'city', header: '城市', align: 'center', width: 'flex' },
],
page, // 受控页码:与外部 pagination 双向联动
selectable: true,
exportable: true, // 导出筛选 + 排序后的全量行(不是当前页)
exportName: '用户列表.csv',
});
const pager = pagination({ page, pageSize: 5, total: rows.length, jumpable: true, pageSizes: [5, 10, 20] });
```
要点:
- 页码只保留一个来源:`dataTable` 与 `pagination` 共传同一个 `page` 信号,翻页由内核写回,桥接层会钳制越界值。
- `exportable` 导出的是「筛选 + 排序后、未分页」的全量行,列序与表头一致、单元格走 `format`;想导出当前页请自行读 `getVisibleRows()`。
- 列宽拖拽只对 `width` 给数字的列生效(`'flex'` 列交给浏览器自适应);双击手柄回到声明宽度。
- 行选择一律按 rowKey 记账,筛选换掉可见行后已选仍在,批量操作前用已选计数确认。
## 配方 6:长表单编辑页(吸底操作条 + 提交锁定)
```ts
const saving = signal(false);
form(
{
onSubmit: (ok) => {
if (!ok) return; // 失败时汇总条已定位到第一处错误,无需自己数红框
saving(true);
save().then(() => toast.success('已保存')).finally(() => saving(false));
},
},
[
formItem({ label: '标题', value: title, required: true, rules: [{ max: 20 }] }, textField({ value: title, maxCount: 20 })),
formItem({ label: '权重', value: weight, hint: '0–100%,一位小数' }, inputNumber({ value: weight, min: 0, max: 100, step: 0.1, precision: 1 })),
formItem({ label: '上架', value: online }, switchButton({ label: '保存后自动上架', checked: online, loading: saving })),
],
);
affix({ position: 'bottom', offset: 0 }, h('div', { class: 'row' },
button({ label: '保存', type: 'submit', disabled: saving }),
));
```
要点:
- 长表单的操作条用 `affix({ position: 'bottom' })` 贴底固定,翻到表单底部按钮也不丢;提交按钮 `type: 'submit'` 走 `form` 聚合校验。
- 校验失败时 `form` 顶部的汇总条会播报「有 N 项需要修改」并聚焦第一处(`errorSummary: false` 可关);`hint` 写格式说明,报错时自动让位。
- 提交期间用同一个 `saving` 信号锁三处:按钮 `disabled`、开关 `loading`(转圈 + aria-busy)、业务请求出口,防重复提交与中途改状态。
- 长度约束一处写够:`textField` 的 `maxCount` 设原生上限并实时计数,`formItem` 的 `rules: [{ max }]` 只做提交兜底,两处数字保持一致。
## 配方 7:筛选栏(层级归属 + 日期区间)
```ts
const dept = signal('');
const from = signal('');
const to = signal('');
space({ size: 'sm', wrap: true },
treeSelect({ nodes: orgTree, value: dept, searchable: true, placeholder: '归属部门' }),
datePicker({ range: true, value: from, end: to, onRangeChange: (a, b) => reload(a, b) }),
button({ label: '重置', variant: 'ghost', onClick: () => { dept(''); from(''); to(''); } }),
);
```
要点:
- 层级用 `treeSelect`(允许选中间节点、可搜索),只有「必须逐级选到叶子」时才用 `cascader`。
- 日期区间传 `range: true`:组件内部保证先后顺序(用户点早了会自动交换),`onRangeChange` 拿到的就是排好序的两端。
- 重置把三个信号清空即可:组件都是受控的,值一变界面自己回到占位态。
## 配方 8:评论输入(@ 提及 + 参与人头像组)
```ts
const text = signal('');
const members = [{ name: 'zhangsan', label: '张三', description: '前端', keywords: ['zs'] }];
card({ title: '评审讨论', extra: avatarGroup({ items: members, max: 4 }) },
mentions({ items: members, value: text, placeholder: '输入 @ 叫人评审…', onSelect: (m) => toast.info(`已提及 ${m.label}`) }),
space({ size: 'sm', justify: 'end', block: true },
button({ label: '发送', onClick: () => { toast.success('已发送'); text(''); } })),
);
```
要点:
- 参与人头像用 `avatarGroup`:超出 `max` 自动折叠成 `+N`,整组 `title` 列出全部姓名,比并排 `avatar` 省一半横向空间。
- `mentions` 的 `value` 是受控信号:发送后 `text('')` 即清空,不要在组件外部再改 textarea。
- 候选人来源由调用方决定(`items` 可随 `onInput` 远程拉取后重建),组件不做远程搜索。
## 配方 9:详情页排版(栅格 + 排版文本 + 描述列表)
```ts
grid({ columns: 2, gutter: 'lg' },
card({ title: '基本信息' },
typography({ variant: 'title', level: 4, content: '订单 RB-2048' }),
descriptions({ column: 2, items: [
{ label: '金额', value: '¥1,280.00' },
{ label: '状态', value: tag({ label: '待发货', variant: 'warning' }) },
] }),
),
card({ title: '备注' },
typography({ variant: 'paragraph', ellipsis: 3, content: longNote }),
typography({ content: 'RB-2048-0917', code: true, copyable: true }),
),
);
```
要点:
- 页面骨架用 `grid`(`columns` 固定列 / `min` 走 auto-fit),窄屏自动单列,不要在调用方写媒体查询。
- 标题一律 `typography({ variant: 'title', level })` 出语义 `h1-h5`;正文长文本用 `ellipsis` 截断并自动挂 `title` 看全文。
- 只读字段用 `descriptions`(不要用 `form`):`form` 承载输入与校验,`descriptions` 只负责 label:value 对齐。
## 配方 10:暗色主题切换(只切 data-theme,不改令牌名)
```ts
const saved = localStorage.getItem('ua-theme');
const dark = signal(saved ? saved === 'dark' : matchMedia('(prefers-color-scheme: dark)').matches);
effect(() => {
document.documentElement.dataset.theme = dark() ? 'dark' : 'light';
document.documentElement.style.colorScheme = dark() ? 'dark' : 'light'; // 原生控件(date/滚动条)跟着变
localStorage.setItem('ua-theme', dark() ? 'dark' : 'light');
});
floatButton({ label: '切换主题', onClick: () => dark(!dark()) });
```
要点:
- 只改 `[data-theme]` 的取值:暗色是令牌**值**的另一套,名字永远不变,组件样式一律不写主题判断。
- 初值顺序是「先 localStorage、再 prefers-color-scheme」;反过来会让用户的手动选择被系统偏好覆盖。
- 同步设置 `color-scheme`,否则原生控件(日期选择、滚动条)在暗色下仍是白底。
## 配方 11:数据看板筛选区(区间滑块 + 日期预设 + 分组下拉)
```ts
const price = signal(200); // 价格下限
const priceTop = signal(800); // 价格上限
const from = signal(''); // 日期区间起点
const to = signal(''); // 日期区间终点
const stack = signal(''); // 技术栈(分组下拉)
grid({ columns: 3, gutter: 'md' },
slider({ value: price, end: priceTop, range: true, min: 0, max: 2000, step: 50 }), // 双滑块:两端都受控
datePicker({ range: true, value: from, end: to, presets: [{ label: '最近 7 天', days: 7 }, { label: '最近 30 天', days: 30 }] }),
select({ value: stack, options: [
{ value: 'vue', label: 'Vue 3', group: '前端框架' },
{ value: 'react', label: 'React 19', group: '前端框架' },
{ value: 'node', label: 'Node.js 22', group: '服务端运行时' },
{ value: 'deno', label: 'Deno 2', group: '服务端运行时' },
] }),
);
dataTable({ columns, data: rows, selectable: true }); // 表格只吃派生出来的查询条件
```
要点:
- 三个筛选控件全部受控于信号:查询参数从信号派生(`computed`),表格不另存一份筛选状态,否则「重置」要逐处清。
- 区间类控件都自带先后与越界修正:`slider` 的 `value`/`end` 越界会压平,`datePicker` 点早了会自动交换两端——调用方不要再写 clamp。
- 长候选集给 `option.group` 分块、常用时间段给 `presets`(`days` 相对今天,跨月跨年由组件算),比让用户逐行扫列表更快。
## 配方 12:命令面板(⌘K 全局操作入口)
```ts
const cmdkOpen = signal(false); // 面板显隐:快捷键与按钮共用同一份状态
const cmdk = commandPalette({
open: cmdkOpen,
items: [
{ id: 'new-order', label: '新建工单', group: '快捷操作', hint: '⌘N' },
{ id: 'export', label: '导出当前列表', group: '快捷操作' },
{ id: 'invite', label: '邀请成员', group: '协作', keywords: ['yaoqing', 'yao qing'] },
{ id: 'theme', label: '切换暗色主题', group: '偏好', keywords: ['dark'] },
],
onSelect: (item) => {
if (item.id === 'theme') return toggleTheme(); // 立即型命令:自己带副作用
if (item.id === 'invite') return inviteModal.open(); // 需要表单的交给已有弹窗,面板只做入口
toast.success(`已执行:${item.label}`); // 其余执行完给一次轻反馈
},
});
document.body.append(cmdk.el); // 面板是 fixed + 遮罩,挂 body 最省事
```
要点:
- 用 `id` 做命令分发键、`label` 只负责显示:以后改文案不会打断路由;同一 `id` 也是埋点的事件名。
- 过滤看三层 `label` / `hint` / `keywords`:拼音与别名塞 `keywords`(如 `yaoqing`),用户没切输入法也能命中。
- 同组必须**连续排列**才会合并成一段标题,`items` 顺序即展示顺序,组件不排序——按「快捷操作 / 跳转 / 偏好」排好再传。
- 需要二次确认的命令在 `onSelect` 里开 `modal` / `popconfirm`,别在面板内部再叠一层确认,否则 Esc 栈会有两层。
- 一页只留一个实例用缺省 `hotkey`(⌘K / Ctrl+K),其余传 `hotkey: false` 自己绑,否则一次按键会同时开两个面板。
- 命令集随权限变化时**重建实例**(`items` 是创建时快照),不要往原数组里 push。
## 配方 13:预约时点(日期 + 时间两段拼装)
```ts
import { computed, signal } from '@ui-arch/frame';
import { datePicker, form, formItem, timePicker, toast } from '@ui-arch/ui';
const day = signal(''); // 日期段:YYYY-MM-DD
const clock = signal('09:00'); // 时间段:HH:mm(给初值,用户只需要改自己想改的那半)
const at = computed(() => (day() ? `${day()} ${clock()}` : '')); // 拼装交给业务层:两个组件各管一段,互不知道对方
const dateField = formItem(
{ label: '预约日期', value: day, required: true },
datePicker({ value: day, min: new Date().toISOString().slice(0, 10) }), // 早于今天的日期直接置灰,比事后校验更省事
);
const timeField = formItem(
{ label: '预约时间', value: clock, required: true, hint: '每 15 分钟一档' },
timePicker({ value: clock, minuteStep: 15 }), // 步长只影响分列,时列始终 24 档
);
form({
layout: 'horizontal',
submitText: '确认预约',
items: [dateField, timeField],
onSubmit: () => toast.success(`已预约 ${at()}`), // 两段都非空才会走到这里:required 已经拦住半截状态
});
```
要点:
- **两段各管一段**:`datePicker` 只写 `YYYY-MM-DD`、`timePicker` 只写 `HH:mm`,拼成完整时点交给业务 `computed`——不要试图让某个组件同时产出日期与时间,否则一半值缺失时格式全靠猜。
- **时间给初值**:`clock` 预置 `09:00` 这类合理默认,用户多数只想改小时;日期保持空值,避免「默认今天」被误提交。
- **约束优先于校验**:日期上限下限用 `datePicker` 的 `min` / `max` 置灰,时间粒度用 `minuteStep` 收窄,`required` 只兜底空值——能点不出来的选项比报错文案更友好。
- **半截状态别提交**:`form` 的 `required` 依赖各自 `value` 信号,日期没选时提交会被拦下并定位到该字段;不要自己写 `if (!day()) return` 之类的旁路判断。
- 需要区间就用 `datePicker({ range: true })` + 两个 `timePicker`(起 / 止),拼装时各自独立,别指望一个信号同时表达两端。
## 配方 14:标签筛选条 → 表格联动
```ts
import { computed, signal } from '@ui-arch/frame';
import { button, dataTable, inputTag, select, space } from '@ui-arch/ui';
const tags = signal<string[]>(['本地推理']); // 条件条:可增删的多个标签(「且」关系)
const city = signal('all'); // 地区:单值下拉,all 表示不限
const rows = signal(listFromApi()); // 全量数据集(轮询 / 搜索时整体替换)
// 派生行集合:信号一变就重算,直接喂给表格的信号数据源
const matched = computed(() =>
rows().filter((row) =>
(city() === 'all' || row.city === city()) &&
tags().every((need) => row.tags.includes(need)), // 多标签是「且」:逐条都要命中
),
);
space({ gap: 'sm' }, [
inputTag({ value: tags, max: 5, placeholder: '按标签筛选,回车添加' }),
select({ value: city, options: [{ value: 'all', label: '全部地区' }, { value: 'sh', label: '上海' }] }),
button({ label: '重置', variant: 'ghost', onClick: () => { tags([]); city('all'); } }),
]);
const table = dataTable({ data: matched, columns, pageSize: 10 }); // 信号数据源:换源不重置排序与页码
```
要点:
- **筛选在信号层做,不在表格里做**:`dataTable` 的工具栏筛选是「关键字单列」的轻量搜索,多条件组合交给 `computed` 派生行集合,表格只负责渲染当前结果。
- **给表格传信号而不是数组**:`data: matched`(`Computed<T[]>` 同样是 `Signal<T[]>`)换源时内核保留排序、每页行数、当前页,并按 `rowKey` 剔除已消失行的选中态——手写 `replaceChildren(dataTable(...))` 会把这些状态全部丢掉。
- **条件条本身就是一个受控信号集合**:`inputTag` 的 `value` 就是唯一数据源,删除 chip 与增删条件走同一出口,别在业务层再存一份「当前条件」。
- **「且」还是「或」写在派生里**:`every` 是且(收窄)、`some` 是或(放大),两种语义只差一个方法调用,但决定了空条件时的行为——空数组 `every` 恒为真(等于不限),`some` 恒为假(等于无结果),重置逻辑据此写。
- 结果为空时不要另写空状态:`dataTable` 已内置空行提示,筛选无命中会自己显示「没有匹配的行」。
## 配方 15:表格分页与独立分页器联动(每页条数 + 受控页码)
表格自带的分页条(上一页 / 页码播报 / 下一页 / 每页条数)只够单表自用;需要把页码提到页面上方、或让多个表格共用一个分页器时,用 `page` 信号把两边的页码接起来,条数候选交给 `pageSizes`。
```ts
const page = signal(1); // 唯一页码来源:表格与独立分页器共读共写
const size = signal(10); // 每页条数由业务决定,表格只负责渲染
const table = dataTable({
data: rows, // 数组或信号均可;换源保留排序、页码与选中态
columns,
page, // 受控页码:内部翻页实时写回,外部置值即跳页
pageSize: size(),
pageSizes: [10, 25, 50], // 分页条候选档;当前值不在候选里会自动补进去
selectable: true,
});
pagination({ page, total: () => rows().length, pageSize: size() }); // 独立分页器与表格共用同一页码信号
```
要点:
- **一个页码只有一个来源**:`page` 信号既是表格的内部页码,也是独立分页器的输入;两边各自派发、等值写回短路,不会互相推着翻页。
- **改页大小要回到第 1 页**:`pageSize` 变化时表格内部按内核语义回第 1 页,外部只需把 `size` 改掉,别在业务层再手动 `page(1)`(顺序错了会出现「先跳到 25 条的第 3 页再被推回第 1 页」的闪动)。
- **越界页码由内核收敛**:数据缩水后 `state` 里的页码会被回写成实际生效页,数据恢复也不会突然跳回旧页码——所以外部依赖这个信号做联动时不需要自己钳制。
- **每页条数下拉要给名字**:无可见 label 的场合(分页条、工具栏)传 `ariaLabel`,否则读屏只念出一句「10 条/页」而没有主语。
## 配方 16:后台页骨架
```ts
layout(
{
header: h('div', { class: 'row' }, breadcrumb({ items: [{ label: '订单' }, { label: '列表' }] })),
sider: menu({ items: [{ key: 'overview', label: '概览' }, { key: 'orders', label: '订单' }] }),
footer: '© 2026 内部系统',
collapsible: true, // 页头左侧出现折叠开关
sticky: true, // 长内容页让页头吸顶
},
card({ title: '本月订单' }, dataTable({ data: rows, columns })),
)
```
要点:
- 页头给 `breadcrumb`、侧栏给 `menu`、主体给 `card`:`layout` 只负责四区位置与页头吸顶,导航与内容各归其组件,别在 `layout` 上塞交互。
- 折叠态想接到别处(例如只显示图标栏)就传 `collapsed` 信号读值;不给信号则内部自管,不必自己再 `signal(false)`。
- 内容区自带滚动与统一细条,不要再套一层 `overflow: auto`——两层滚动容器会让吸顶与滚动条重复出现。
## 配方 17:表格刷新遮罩
```ts
const busy = signal(false);
const table = dataTable({ data: rows, columns, page, selectable: true });
const refresh = async () => {
busy(true);
try { await reload(); } finally { busy(false); } // 遮罩只跟 busy 走,异常也要收
};
loading({ active: busy, text: '正在刷新订单', minHeight: 240 }, table);
```
要点:
- 遮罩包住表格而不是整页:用户看得见「哪一块在等」,其余区域仍可操作。
- `loading` 默认延迟 120ms 出场——接口快于此不闪;别为了「一定看见」传 `delay: 0`,那会让每次快请求都闪一下。
- 刷新期间不要把 `data` 换成空数组占位:`loading` 已负责等待感,清空数据会让页码与选中态无谓地收敛一次。
## 配方 18:变更审阅面板
```ts
const panel = h('div', { class: 'review' },
tag({ label: '待审阅', variant: 'warning' }),
codeBlock({ code: before, title: '旧版本', lang: 'ts', maxHeight: 220 }),
diffView({ before, after }),
copyButton({ text: after, label: '复制新版本' }),
);
```
要点:
- 三段各司其职:`codeBlock` 看全文(带行号与复制)、`diffView` 只看变化、`copyButton` 把新版本带走——不要用 `diffView` 代替全文展示。
- `codeBlock` 的行号不参与复制,`copyButton` 直接读原文,所以「复制出来的代码」永远干净。
- 差异行多了给 `diffView` 外层限定高度(或放进抽屉),否则长差异会把审阅按钮挤出视口。
## 通用约定
- 状态一律放信号(`signal` / `computed`),组件只读写信号,不持有业务数据副本。
- 反馈分层:`toast` 右上角瞬时、`notification` 右下角可长读、`result` 整页终态、`alert` 页内常驻。
- 样式不要写颜色与间距字面值,先跑 `ua tokens` 查 `--ua-*`(verify 会拦截未定义令牌与 `#hex`)。
- 动效与弹层方向不要各写一份:入场用 `ua-pop-drop`、翻到触发器上方用 `ua-pop-rise`(`.is-flip`),时长缓动走 `--ua-duration-*` / `--ua-ease-*`;verify 会拦「帧体完全相同但名字不同」的复制粘贴。
- 新增可滚动容器(自绘列表 / 代码块 / 面板)时,把类名加进 `ui.css` 顶部那条 `scrollbar-width` 统一规则里,否则暗色下会露出系统原生亮条;verify 会逐个核对 `overflow: auto|scroll` 的容器是否都在名单内。
- 新增颜色令牌要同时改 `tokens.css`(`:root` + `[data-theme='dark']`)与 `tokens.json`(`color.*.light/dark`):只改一处时文档色板会凭空多/少一个色块,verify 的双源一致性校验会直接报错。