# @ui-arch 组件库 Agent 文档(无图形版) > 自动生成于 2026-10-26T18:28:42.413Z,数据源:各包 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,再重新生成文档站 5. 选区:库内文字选区 ::selection 统一为主题色淡底(皮肤包已全局处理),应用侧不必再写 ### @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 —— 禁用态:布尔直接置位,传信号则动态同步 - onClick(回调):() => void —— 点击回调 - size:'sm' | 'md' | 'lg' —— 高度档位:sm 紧凑 / md 默认 / lg 大触控目标,走 --ua-control-h-* 三档;缺省 md - loading(默认 false):boolean | Signal —— 加载态:转圈并禁点(aria-busy 播报忙碌);布尔直接置位,传信号则动态同步 示例: ``` button({ label: '提交', variant: 'primary', onClick: () => console.log('clicked') }) ``` 手把手: 1. 确定语义强度:主操作用 primary(默认),次要操作用 ghost,危险操作用 danger 2. 绑定 onClick 回调处理点击逻辑 3. 需要禁用态时传 disabled(布尔或信号);异步提交用 loading:开始时置 true,完成后置 false,按钮转圈并自动禁点 4. 放进
里需要提交时显式传 type: "submit";缺省 button 不会触发原生提交 现场演示:apps/docs/demo.html#sec-counter ### textField — 文本输入 TextField 参数: - value:Signal —— 双向绑定信号:输入写回信号,信号变化写回输入框 - placeholder:string —— 占位文案 - disabled:boolean | Signal —— 禁用态:布尔直接置位,传信号则动态同步 - 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 同步);缺省普通文本 - ariaLabel:string —— 无可见 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 —— 当前值信号,双向绑定(按钮/键盘写回、外部赋值同步) - min:number —— 最小值;缺省无下限 - max:number —— 最大值;缺省无上限 - step(默认 1):number —— 步长;缺省 1 - precision:number —— 小数位数:写回与显示都按 toFixed(p) 归一(消除步进浮点尾差,step 0.1 也干净);缺省不处理 - disabled:boolean | Signal —— 禁用态:布尔直接置位,传信号则动态同步 - 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 - ariaLabel:string —— 无可见 label 时的可访问名(如「数量」);缺省「数字输入」 示例: ``` 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 —— 双向绑定信号 - rows(默认 3):number —— 初始行数;缺省 3 - placeholder:string —— 占位文案 - disabled:boolean | Signal —— 禁用态:布尔直接置位,传信号则动态同步 - 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 内部滚动;缺省固定行数 - ariaLabel:string —— 无可见 label 时的可访问名(如「备注」);缺省「多行文本」 示例: ``` 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 —— 当前值信号,双向绑定:拖动写回,外部赋值同步位置 - min(默认 0):number —— 最小值;缺省 0 - max(默认 100):number —— 最大值;缺省 100 - step(默认 1):number —— 步长;缺省 1 - disabled:boolean | Signal —— 禁用态:布尔直接置位,传信号则动态同步 - showValue(默认 true):boolean —— 是否右侧显示当前值;缺省 true - marks:{ value: number; label?: string }[] —— 刻度标注:按值定位在轨道下方,label 缺省显示数值;纯装饰不进读屏(数值由原生 input 播报) - range(默认 false):boolean —— 双滑块区间模式:value 当下限、end 当上限,两拇指之间用强调色连成已选段 - end:Signal —— 区间上限信号;仅 range 模式使用,不传则组件内部维护 - onRangeChange(回调):(low: number, high: number) => void —— 区间两端变更回调,参数已保证 low ≤ high(越界那一端会被压平) - onChange(回调):(value: number) => void —— 拖动回调 - ariaLabel:string —— 无可见 label 时的可访问名(如「音量」);缺省「数值」,区间模式自动带「下限 / 上限」 示例: ``` 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 —— 勾选状态信号,双向绑定 - label:string —— 右侧文案;缺省只渲染勾选框 - indeterminate:boolean | Signal —— 半选态(批量选择时部分子项选中):仅影响视觉与读屏 mixed 播报,不改变 checked 值;点一下即回到全选 - disabled:boolean | Signal —— 禁用态:布尔直接置位,传信号则动态同步 - 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 —— 开关状态信号,双向绑定 - ariaLabel:string —— 无可见文案时的可访问名(如纯图标开关);缺省取 label - label:string —— 左侧文案 - loading:boolean | Signal —— 加载中:锁定交互、滑块换成转圈并挂 aria-busy(异步提交防误点);与 disabled 同时生效 - disabled:boolean | Signal —— 禁用态:布尔直接置位,传信号则动态同步 - 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 —— 选中值信号,双向绑定 - placeholder:string —— 未选中时触发器显示的占位文案 - searchable:boolean —— 弹层顶部带搜索框,按标签包含匹配过滤;缺省 false - clearable:boolean —— 有值时触发器右缘出现清空按钮(收起态按 Delete / Backspace 也能清);缺省 false,用于「可选筛选项」 - disabled:boolean | Signal —— 禁用态:布尔直接置位,传信号则动态同步;禁用时触发器置灰且不展开弹层 - 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 —— 选中值信号,双向绑定 - disabled:boolean | Signal —— 整组禁用:全部选项与文案置灰且不可选 - 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 —— 受控展开面板信号(单开模式);缺省由组件内部持有状态 - 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(渲染 );单页应用想自行接管时不给 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 —— 选中态信号,双向绑定(点击写回、外部赋值同步);不传则组件内部自持 - 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 换底色档,颜色一律来自令牌 5. src 加载失败(404 / 断网)时自动回退为文字头像:首字占位、role=img 与 aria-label 不变,尺寸与语义底色保持,不会露出浏览器碎图标 现场演示: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 等任意容器 5. 行高按「正文字号 × 正文档行高」算,与真实文字行盒等高:表格 / 列表的加载态换成数据时不会整体窜一下(历史写死 12px,比正文矮约 8px,五行就差 40px) 现场演示: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. 鼠标悬停或键盘 Tab 进卡片时倒计时暂停,移出 / 焦点离开后按剩余时长续走(剩余不足 400ms 时补到 400ms,避免松手即消失);宿主容器仍穿透,只有卡片本体这一小块可命中 4. 多条消息在右上角自动纵向堆叠,互不遮挡 5. 需要完整入参(含 variant)时用 showToast(props);组件挂载容器由内部懒创建,样式随 ui.css 一起引入 6. 同屏最多 4 条,超出自动挤掉最早一条;需要用户主动确认的消息用 showToast({ closable: true }) 给个 × 7. 要「撤销」这类操作时给 showToast 传 actionLabel 与 onAction:按钮出现在文案右侧,点完先收起再回调,duration 建议放到 5000 让用户来得及点 8. 同屏最多 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 —— 受控开合信号;不传则内部维护,传了外部可编程开合(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 —— 当前进度 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 —— 当前激活页签 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?, status? } - status:'error' —— 写在 steps 的每一项里:该项标为失败(红点 + 感叹号 + 读屏「第 N 步:xx(失败)」),优先级高于进度态,也不参与回跳 - active(必填):Signal —— 当前步索引(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 同步业务状态;点圆点(键盘可 Tab)或标题文字都算,一次点击只回调一次 4. 某步失败时给该项加 status: 'error':圆点变红加感叹号、标题标红、读屏读「第 N 步:xx(失败)」;它既不进 done 也不占 current,因此不参与回跳——「失败」和「还没走到」只能是两种视觉 5. 侧栏或移动端传 direction: 'vertical':圆点成一列、说明文字紧贴右侧,步与步之间由圆点下方的竖线相连(末步不画线,已完成的段用主色标进度) 现场演示:apps/docs/demo.html#sec-stepper ### rate — 评分 Rate 参数: - value(必填):Signal —— 当前评分(1 基,allowHalf 时 0.5 步进)信号,双向绑定:点选/键盘写回、外部赋值同步 - max(默认 5):number —— 星标总数;缺省 5 - allowHalf(默认 false):boolean —— 允许半星:点击星标左半区记 0.5 分,键盘步进同为 0.5;缺省整数 - disabled:boolean | Signal —— 禁用态:布尔直接置位,传信号则动态同步 - 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 —— 当前页码信号,双向绑定:外部赋值自动翻页,点击页码写回信号 - pageSize(必填):number —— 每页条数 - total(必填):number | Signal —— 总条数;传信号则总量变化时自动重算页数(并把越界页码钳回并回调 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 —— 开关信号: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 —— 内置页脚:确认回调;返回 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 —— 开关信号: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 —— 提交按钮文案;缺省「提交」。异步规则校验期间按钮转圈并禁点(aria-busy),跑 400ms 的远程校验也看得见在等 - resetText:string —— 重置按钮文案;缺省不渲染 - onSubmit(回调):() => void —— 全部字段校验通过后的提交回调 - (formItem)label(必填):string —— 字段标签 - (formItem)value(必填):Signal —— 字段值信号,校验数据源 - (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 —— 条件显示信号:为假时整项隐藏且不参与校验(未显示的字段不该拦住提交) - (formItem)reset:() => void —— 自绘控件的复位钩子:select / dataTable 这类没有原生 input 的控件拿不到值,由调用方在这里回写信号;原生输入框由 formItem 自己还原,不用管 - 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 数组里增删 16. 校验失败必定中止提交:判据是错误条数而不是「有没有找到焦点目标」,所以只有自绘控件(非搜索式下拉 / 评分 / 开关,容器里没有原生 input)的字段也能拦住;焦点定位顺序是控件 → 触发器 → 字段容器(后者临时 tabindex=-1,只接受程序聚焦,不污染 Tab 序) 现场演示:apps/docs/demo.html#sec-formlab ### dataTable — 数据表格 DataTable 参数: - data(必填):T[] | Signal —— 全量行数据(行须含 id 作 rowKey);传信号或 computed 时数据变化自动换源,排序 / 筛选 / 页码保持用户原意 - columns(必填):ColumnDef[] —— 列定义:内核字段 { key, header?, sortable?, width? } + 皮肤字段 { align?, format?, render? };width 数字为固定像素,'flex' 自适应 - filterKey:string —— 工具栏筛选的列;缺省取第一列 - pageSize(默认 5):number —— 每页行数;缺省 5 - pageSizes(默认 [5, 10, 20]):number[] —— 分页条「每页条数」候选档;缺省 [5, 10, 20],当前 pageSize 不在候选里会自动补进去 - emptyText(默认 暂无数据):string —— 无数据时的空状态文案;有筛选词时仍显示「没有符合「x」的行」并给一键清除(两种语义不混用) - emptyAction:unknown —— 真没数据时占位区右侧的操作(如「新建」按钮);有筛选词时这一位置换成「清除筛选」,两者互斥 - selectable(默认 false):boolean —— 启用行选择列:全选三态(含半选)、行 checkbox、已选计数;缺省 false - page:Signal —— 受控页码信号:与内核分页双向同步,传同一个信号给独立 pagination 组件即可联动 - loading:boolean | Signal —— 加载态:为真时数据行换成骨架行(行数随 pageSize),筛选输入与翻页按钮临时禁用,容器带 aria-busy - selected:Signal —— 受控选中行信号:与内核选择双向同步(内容比较,不来回派发),供批量操作读取/改写 - exportable:boolean —— 工具栏显示「导出 CSV」:导出筛选 + 排序后的全量行(不分页),列跟随当前可见列;缺省 false - exportName:string —— 导出文件名;缺省 table.csv - onRowClick(回调):(row: T) => void —— 整行点击 / Enter / 空格的回调;传了行才可聚焦可点(未传时行保持纯展示,不抢 Tab 焦点) - columnToggle(默认 false):boolean —— 工具栏显示「列」按钮:弹层内勾选控制列显隐,至少保留一列(最后一列的勾选框锁死),缺省 false - hiddenColumns:Signal —— 受控隐藏列键集:与「列」面板双向同步,外部可据此持久化用户偏好;隐藏列不渲染、不进导出 - renderDetail(回调):(row: T) => unknown —— 传了才多一列展开钮:点开在该行下铺一条详情行(内容由这里给,展开态按 rowKey 记,筛选翻页不丢) 示例: ``` dataTable({ data: users, selectable: true, columns: [{ key: 'name', header: '姓名', sortable: true }], pageSize: 5 }) ``` 手把手: 1. 准备行数据,确保每行有唯一 id(跨页选择与刷新不丢行靠它) 2. 定义 columns:sortable: true 的列表头可点击排序(升 → 降 → 取消循环),标记箭头 ↑↓↕ 3. 多列排序:按住 Shift 点第二个可排序列即叠加——内核按 rules 顺序逐列比较,表头标记带优先级(↑1 / ↓2),不按 Shift 则回到单列语义 4. 列展示细节用皮肤字段:align 控制表头与单元格对齐,format(value, row) 返回纯文本(走 textContent,不需要转义) 5. 数据源可以给信号或 computed(如筛选结果):变化时内核整体换源,选择状态按 rowKey 剔除已消失的行,排序与页码不被打断 6. 列宽用内核字段 width:数字(像素)固定该列,'flex' 或不设则按内容自适应 7. selectable: true 开启行选择:表头全选框带 indeterminate 半选态(aria-label「全选本页」),每行复选框读「选择第 N 行」、选中行标 aria-selected(淡底只是视觉),工具栏显示已选计数 8. 选中行用低视觉比重样式(淡色底 + 弱化文字),选择状态存 rowKey,跨页不丢失 9. 工具栏输入框即筛选,输入即时过滤并回第 1 页;无匹配时显示空状态行 10. 表头粘性固定(滚动容器 max-height 420px),斑马纹 + 悬停高亮由 CSS 提供 11. 数据在途时传 loading(布尔或信号):表格体换成骨架行、筛选与翻页禁用,返回后置回 false 即恢复 12. 分页条自带每页条数下拉(5 / 10 / 20 条,调用方传的 pageSize 若不在候选里会自动补进去):切换即改页大小并回第 1 页,首末页时上/下一页置灰不再派发无效页码 13. 需要独立分页器联动时传 page 信号:const page = signal(1),dataTable({ page }) 与 pagination({ page }) 共用 14. 需要批量操作时传 selected 信号:内部勾选实时写回,外部置空或赋值即同步整表(跨页保留,按 rowKey 存) 15. 可排序表头的 aria-sort 跟随排序态同步(ascending / descending / none),读屏能直接播报当前列序,箭头只是视觉补充 16. 需要导出时开 exportable:按钮导的是筛选+排序后的全量行,与当前页无关,单元格按 format 输出 17. 整行可点(进详情、选中一行做后续操作)传 onRowClick:行会变成可聚焦项,Enter / 空格与点击等价;不传时行不进 Tab 序列,纯展示表不会被读屏当成按钮 18. 需要让用户自己调列时开 columnToggle:工具栏多一个「列」按钮,弹层勾选即刻重绘表头与单元格(隐藏列不占 colspan,空状态与骨架行同步收窄) 19. 想记住用户偏好就传 hiddenColumns 信号:面板勾选实时写回,外部赋值即整表跟随;隐藏筛选列后筛选条件仍然生效,属预期而非失效 20. 表里本来就空(不是筛掉的)时传 emptyAction 给个「新建」之类的出口:它只在「真没数据」时出现,一旦有筛选词就换成「清除筛选」——先让用户清条件,而不是急着让他新建 21. 表里塞不下的长内容(备注、标签、二级操作)交给 renderDetail:多出一列 ▸,点开在该行下方铺一条凹陷底的详情行,按钮自己拦冒泡所以不会顺带触发 onRowClick 现场演示: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 —— 当前选中值信号,双向绑定(点击写回,外部赋值即同步滑块) - size(默认 md):'sm' | 'md' —— 尺寸档位;缺省 md - disabled:boolean | Signal —— 整组禁用:布尔直接置位,传信号则动态同步;单项禁用走 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 —— 数值信号:变化时滚动过渡到新值;倒计时模式可不传 - 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 —— 当前下标信号;不传则内部维护 - 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 —— 展开的 key 集合;不传则内部维护 - selected:Signal —— 选中 key 信号;不传则内部维护 - defaultExpandAll:boolean —— 首屏展开全部分支;缺省 false - searchable:boolean —— 顶部搜索框:按标签过滤,命中节点保留父链并自动展开;缺省 false - onSelect(回调):(node: TreeNode) => void —— 选中回调:点击标签或 Enter/空格触发 - multi:boolean —— 是否显示勾选框并开启多选;缺省 false - checked:Signal —— 勾选 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 —— 右侧集合信号;不传则组件内部维护,传了即外部受控 - 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 —— 选中路径信号;不传则内部维护,传了即外部受控 - placeholder:string —— 未选时占位文案;缺省「请选择」 - separator:string —— 路径分隔符;缺省「 / 」 - changeOnSelect:boolean —— 允许停在中间层;缺省 false 必须选到叶子才提交 - disabled:boolean | Signal —— 禁用触发器;支持信号响应式禁用 - 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 —— 各栏百分比数组(和为 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 —— 输入值信号;不传则内部维护,传了即外部受控 - placeholder:string —— 占位文案,同时作为无障碍名称 - emptyText:string —— 无匹配时的提示文案;缺省「无匹配项」 - maxItems:number —— 最多展示条数;缺省 8 - disabled:boolean | Signal —— 禁用输入与弹层 - 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 —— 选中日期(YYYY-MM-DD)信号;区间模式下是起始日,不传则内部维护 - range(默认 false):boolean —— 区间模式:首次点击定起点(弹层不收起),再次点击定终点;点早了自动交换先后 - end:Signal —— 区间模式的结束日信号;不传则内部维护 - onRangeChange(回调):(start: string, end: string) => void —— 区间两端都选定后的回调,参数已按先后排序(单日模式不触发) - placeholder:string —— 未选时占位文案;缺省「请选择日期」(区间模式缺省「请选择日期范围」) - min:string —— 最早可选日期(含,YYYY-MM-DD);更早的日置灰不可点 - max:string —— 最晚可选日期(含,YYYY-MM-DD);更晚的日置灰不可点 - disabled:boolean | Signal —— 禁用触发器 - 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 —— 颜色值信号(CSS 颜色串,支持 var(--ua-*));不传则内部维护 - presets:string[] —— 预设色板;缺省取语义令牌色(accent/success/warning/danger/text/border) - placeholder:string —— 未选时占位文案;缺省「请选择颜色」 - disabled:boolean | Signal —— 禁用触发器 - 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 —— 选中值数组信号;不传则内部维护 - min:number —— 最少选几项:到下限后已选项禁用,防止取消 - max:number —— 最多选几项:到上限后未选项禁用 - checkAll:boolean —— 是否显示全选行(含半选);缺省 false - direction:'horizontal' | 'vertical' —— 排列方向;缺省 horizontal - disabled:boolean | Signal —— 整组禁用 - 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 —— 选中值数组信号;不传则内部维护 - placeholder:string —— 未选时占位文案;已选时让位给标签 - max:number —— 最多选几项:到上限后未选项自动禁用 - maxTagCount:number —— 标签区最多显示几个,超出的折成一枚「+N」(悬停看全量、点击展开弹层);缺省全部显示 - searchable:boolean —— 是否可输入过滤;缺省 true - disabled:boolean | Signal —— 禁用整个下拉 - onChange:(values: string[]) => void —— 变更回调(传副本) - clearable:boolean —— 开启后有选中值时在右缘显示一键清空按钮;缺省不渲染 - selectAll(默认 false):boolean —— 弹层顶部给「全选」行(只作用于当前过滤结果,半选显示 –,到 max 补满即停);缺省 false 示例: ``` 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(不占键盘高亮下标),选中后的标签顺序仍按点击顺序而非分组顺序 8. 候选多到「一个个点太慢」时开 selectAll:弹层顶部多一行「全选(3/8)」,半选显 –、全选再点即清空这批;它只作用于当前过滤结果,先搜再全选就是「按条件批量选」 现场演示: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 —— 受控开合信号;不传则内部维护,传了可由外部编程开合 示例: ``` 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 —— 选中日期(YYYY-MM-DD)信号;不传则内部维护 - month:Signal —— 展示月份(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 —— 受控文本信号;不传则组件内部维护(拿不到外部读取时不要传) - 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 —— 单选选中 key;不传则内部维护 - checked:Signal —— 多选勾选 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 —— 显隐信号:外部写 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 —— 选中时间信号(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 —— 禁用触发器 - 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 —— 当前验证码(数字拼接串)信号,双向绑定:格子输入写回、外部赋值同步回格子 - length(默认 6):number —— 格数;缺省 6 - mask(默认 false):boolean —— 密文显示(type=password,并声明 one-time-code 便于短信验证码自动填充);缺省明文 - disabled:boolean | Signal —— 禁用态:布尔直接置位,传信号则动态同步 - 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 —— 禁用态:布尔直接置位,传信号则动态同步 - 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 —— 原文;传信号则随信号变化响应式重画 - keyword(必填):string | Signal —— 关键词(大小写不敏感、全部命中,首尾空白忽略);空串不高亮、原样展示 示例: ``` highlight({ text: 'Alpha Beta', keyword: 'beta' }) ``` 手把手: 1. 传 text + keyword 即可,命中片段以淡黄 mark 底展示,适合搜索结果 / 补全列表的匹配说明 2. 两个入参都接受信号:搜索框输入实时联动高亮,无需手动重渲染 3. 大小写不敏感且全部命中(非只第一处);关键词两侧空白自动忽略 4. keyword 为空或纯空白时原样返回文本,既不抛错也不全高亮:搜索框清空那一刻不用特判 现场演示:apps/docs/demo.html#sec-highlight ### inputTag — 标签输入 InputTag 参数: - value(必填):Signal —— 已选标签集合,双向绑定:成标签与删 chip 都写回信号 - placeholder:string —— 未满时的占位文案;缺省「输入后回车添加」 - max:number —— 最多允许的标签条数;缺省 0 表示不限,满额后输入框禁用并提示上限 - maxLength:number —— 单个标签最大字符数;缺省 24,超出直接截断 - allowDuplicate:boolean —— 是否允许重复标签;缺省 false(重复的静默丢弃) - disabled:boolean | Signal —— 禁用态:布尔或信号,true 时不可输入也不可删 - ariaLabel:string —— 输入框无障碍标签;缺省「标签输入」 - onChange:(tags: string[]) => void —— 标签集合变化回调,参数是变化后的完整集合 示例: ``` const tags = signal(['前端', '设计']); inputTag({ value: tags, max: 5, onChange: (list) => console.log(list.length) }) ``` 手把手: 1. 传一个 signal 作为 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 —— 折叠态信号;传入则双向绑定(外部可读数),缺省内部自管 - 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 —— 签字结果 dataURL 信号,双向绑定;清空写空串 - height:number —— 画布高度(px);缺省 160 - penWidth:number —— 笔宽(px);缺省 2 - hint:string —— 底部提示文案;缺省「在上方区域手写签名」 - disabled:boolean | Signal —— 禁用态:布尔或信号,禁用时不响应手写 - 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 —— 是否加载中:布尔直接置位,传信号则随状态显隐 - 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 —— 当前进度 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 ### sortableList — 拖拽排序列表 SortableList 参数: - items(必填):string[] —— 全部条目文案;文案即排序键,要求唯一(重名会让顺序回读撞车) - value:Signal —— 受控顺序信号;传了则内部改动写回,外部改动即重排(拖拽中跳过) - onChange(回调):(next: string[]) => void —— 顺序变化回调,拖放落定 / 键盘搬位 / move 调用后各触发一次 - disabled(默认 false):boolean | Signal —— 禁用拖拽与键盘搬位,同时整体置灰退到不可交互那一档(光标与悬停都不再给反馈);缺省 false - emptyText:string —— 无条目占位文案;缺省「暂无可排序项」 示例: ``` const sort = sortableList({ items: ['需求评审', '接口联调', '灰度发布'], onChange: (next) => console.log(next) }); container.append(sort.el); ``` 手把手: 1. items 传全部条目文案、顺序即当前顺序:文案同时是排序键,所以必须唯一(重名会分不清谁被搬动) 2. 鼠标 / 触摸按住任意一行拖动,指针越过邻行中线就换位;松手后写回 value 信号并触发 onChange,拖拽期间不重排避免手势中断 3. 键盘等价:Tab 聚焦某行后按 Alt+↑ / Alt+↓ 上下搬一格、Alt+Home / Alt+End 搬到首尾(裸 ↑↓ 留给页面滚动) 4. 被拖的行以虚线边 + 抬升投影标示(不用整行降透明度:那会把正文对比压到 4.86,低于正文 7:1 口径),落点由指针位置决定,不会出现「拖了三格只动一格」的错觉 5. 外部要按代码调整顺序用句柄 move(from, to),下标越界或禁用时静默忽略;order() 以 DOM 为唯一真相回读当前顺序 6. 条目超过一屏也能拖:指针进入最近滚动祖先(overflow-y 为 auto/scroll 且内容确已溢出)上下 48px 感应带即按帧自动滚,松手或离开感应带立刻停;找不到滚动祖先(整页跟随滚动)时不接管滚动,交给浏览器 7. 什么时候别用:需要严格的两级联动排序(树 / 表格分组)时它不够用,得自己在 onChange 里按父节点重排;跨列表搬运用两个实例各自持有顺序信号 8. 只看不改时传 disabled:光标回到默认、文字退到次级色、悬停与按下都不再变色,读屏播报 aria-disabled——禁用态最忌「看着能拖、拖了没反应」 现场演示:apps/docs/demo.html#sec-sortable ### masonry — 瀑布流 Masonry 参数: - items(必填):unknown[] —— 每个卡片内容(字符串 / DOM / 响应式值);数组顺序即铺放顺序,列高不等由内容自己决定 - columns:number —— 固定列数;传了即不随容器宽度变化,与 minColumnWidth 互斥 - minColumnWidth(默认 240):number —— 自适应列宽下限(像素);缺省 240,容器越宽列数越多 - gap(默认 var(--ua-space-3)):string —— 列间距与行间距(CSS 长度或令牌值);缺省 var(--ua-space-3) 示例: ``` masonry({ minColumnWidth: 220, items: shots.map((s) => card({ title: s.title }, image({ src: s.url }))) }) ``` 手把手: 1. items 里直接放 card / image / 任意 DOM:每项由组件包一层 .ua-masonry-item,只有这一层带 break-inside: avoid,卡片才不会在列边界断成两截 2. 不传 columns 就按 minColumnWidth 自适应:容器变宽自动加列、窄屏自动降到一列,不需要自己监听 window.resize 3. 要固定几列(比如大屏看板恒 4 列)就传 columns,此时 minColumnWidth 不再生效——两者互斥,别同时传指望它俩协商 4. 间距传一次就够:gap 同时给 column-gap 与每项的 margin-bottom,列间距和行间距天然一致;改间距只改这一处 5. 列高不等才是瀑布流的价值所在(图片墙、评论流、看板):等高内容请用 grid,瀑布流会把等高卡片铺成奇怪的错位 6. 什么时候别用:需要严格的行对齐(表格化数据)、需要按列独立滚动(用 splitter / 多个 virtualList),以及首屏内容需要按阅读顺序严格横向(多列布局是竖向填充,读屏顺序与视觉列序一致但不等同于行序) 现场演示:apps/docs/demo.html#sec-masonry ### editable — 行内编辑 Editable 参数: - value(必填):Signal —— 展示值,双向绑定:只有提交才写回,Esc 取消绝不落值 - placeholder(默认 '点击填写'):string —— 空值时的占位文案,颜色走辅助文字档 - label(默认 '编辑'):string —— 读屏用的字段名,作静止态按钮 aria-label 的前缀(如「标题:季度目标」) - multiline:boolean —— 多行模式(textarea):Enter 换行,Ctrl / Cmd + Enter 才提交;缺省单行 - disabled:boolean | Signal —— 禁用只读,布尔与信号双口径;点了也不进编辑态 - validate(回调):(v: string) => string | null —— 提交校验:返回文案即拒绝提交、保持编辑态并标红(aria-invalid + role=alert),返回 null 表示通过 - onCommit(回调):(v: string) => void —— 提交回调,值已写回信号之后触发;取消不触发 示例: ``` const title = signal('季度目标'); editable({ value: title, label: '标题', validate: (v) => (v ? null : '标题不能为空'), onCommit: (v) => toast.success('已保存:' + v) }) ``` 手把手: 1. 传一个 Signal 作唯一数据源:组件只在提交时写回,取消路径一个字都不改,「改了没保存」的中间态不会污染业务状态 2. 点文字进编辑态:Enter 提交、Esc 取消、点别处(失焦)提交;多行模式要 Ctrl / Cmd + Enter 才提交,Enter 留给换行——否则多行里连换行都写不了 3. validate 返回文案时保留编辑态并标红:控件带 aria-invalid、下方 role=alert 出文案,与 form 的出错口径一致,用户改对再提交即可 4. disabled 接受布尔或信号:禁用时静止态是灰字、点了不进编辑,适合权限只读的场景(要完全不给「可改」暗示就直接渲染文字) 5. 单元格里要塞组件(editable 就地编辑、tag 状态标记、avatar 头像、progress 完成度)时给列传 render: (row) => 节点:它直接取代 format 决定这一格画什么,排序与筛选仍按 dataKey 的原值走;格内的点击会被拦下,不会顺带触发 onRowClick(点单元格里的按钮不该等于点整行) 6. 什么时候别用:长表单(用 form + textField,editable 没有 label 排版与必填标记)、需要撤销栈的富文本,以及值会被频繁外部改写的地方(编辑中不跟随外部更新,是有意的取舍) 现场演示:apps/docs/demo.html#sec-editable 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 用首字占位,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):单行通知条,语义色 + 左侧符号 + 可选跑马灯与关闭,适合页顶常驻播报 - sortableList(props):拖拽排序列表,整行可拖 + Alt+↑↓ 键盘搬位,返回 { el, move, order } 句柄 - masonry(props):瀑布流多列布局,列宽自适应或固定列数,卡片不被拆到两列 - editable(props):行内编辑,静止态是文字、点一下就地变输入框,Enter 提交 / Esc 取消 / 失焦提交 - theme(options):主题工具(非组件),URL 参数 > 本地记忆 > 跟随系统,写 html[data-theme] 换令牌,返回 { mode, dark, set, toggle, dispose } 一行用法: ``` 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:无参读值并收集依赖,传参写值并触发订阅 - 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 audit:surface [--json|--list]:表面阶梯门禁,逐档核对底色对底色的亮度比(页面底 → 表面 → 悬停底 → 按下底 ≥1.04、凹陷底 ≥1.02),外加两档半透明淡底压在表面上的可见度(选中底 ≥1.04、悬停底 ≥1.04),亮/暗 × 常规/高对比四套主题各算一遍,不达标退出码 1;对比度只管文字读不读得清,管不到「悬停有没有反馈」(实测过 1.008 的悬停底) - ua mcp:零依赖 stdio MCP server,暴露 list_components / get_component / search_components / get_tokens / list_recipes / get_recipe;get_component 支持 format: md 直接取可粘贴的 Markdown 片段 - ua loop [--json]:迭代循环诊断,一眼看清锁心跳(是否 15 分钟内)、上次留痕、backlog 待办与视觉欠账数,并给出本轮该不该抢锁、下一条该选谁、以及自补条目可用的下一个编号;多实例错位接力时不必翻调度器数据库;--json 里的 nextId 可直接给脚本用 - ua iter done [notes]:一轮迭代收尾——backlog 条目置 done、按固定字段顺序追加 .ua/iterations.log 一行(ts 由本机时钟生成带 +08:00,commit 校验 7-40 位哈希);ua iter add <条目json文件> 补一条自选题(id 唯一 + 五字段齐全校验) 一行用法: ``` node packages/tools/cli/bin/ua.js [--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) 6%, var(--ua-color-surface))(暗色 color-mix(in srgb, var(--ua-color-text) 7%, var(--ua-color-surface))) - --ua-color-surface-3: color-mix(in srgb, var(--ua-color-text) 14%, var(--ua-color-surface))(暗色 color-mix(in srgb, var(--ua-color-text) 17%, var(--ua-color-surface))) - --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: #8a5d15(暗色 #d9a94f) - --ua-color-tint-danger: color-mix(in srgb, var(--ua-color-danger) var(--ua-alpha-soft), transparent)(暗色 color-mix(in srgb, var(--ua-color-danger) 14%, transparent)) - --ua-color-tint-success: color-mix(in srgb, var(--ua-color-success) var(--ua-alpha-soft), transparent)(暗色 color-mix(in srgb, var(--ua-color-success) 14%, transparent)) - --ua-color-tint-warning: color-mix(in srgb, var(--ua-color-warning) var(--ua-alpha-soft), transparent)(暗色 color-mix(in srgb, var(--ua-color-warning) 14%, transparent)) - --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-raised: 10 - --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([]); 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([]); 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(['本地推理']); // 条件条:可增删的多个标签(「且」关系) 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` 同样是 `Signal`)换源时内核保留排序、每页行数、当前页,并按 `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` 外层限定高度(或放进抽屉),否则长差异会把审阅按钮挤出视口。 ## 配方 19:可排序的检查单(拖拽 + 键盘搬位 + 顺序回写) ```ts const steps = ['需求评审', '接口联调', '灰度发布', '线上回归']; const order = signal(steps); // 顺序唯一真相:组件与下方摘要读同一个信号 const list = sortableList({ items: steps, // 文案即排序键,要求唯一 value: order, onChange: (next) => toast.success(`已调整:${next[0]} 置顶`), }); card({ title: '发布检查单(可拖动,Alt+↑↓ 键盘搬位)' }, list.el, // 返回的是句柄不是节点:忘了 append(el) 页面上一片空白 h('p', { class: 'desc' }, () => order().map((t, i) => `${i + 1}. ${t}`).join(' ')), button({ label: '把「灰度发布」置顶', variant: 'ghost', onClick: () => list.move(2, 0) }), ); ``` 要点: - `sortableList` 返回 `{ el, move, order }` 句柄(与 `form` / `virtualList` 同类):节点在 `el` 上,业务侧要改顺序用 `move(from, to)`,读当前顺序用 `order()`。 - 顺序只存一份:`value` 信号同时喂组件与摘要 / 上报逻辑,否则会出现「界面拖了、提交的仍是老顺序」。 - `move()` 与拖拽走同一条提交路径(都会触发 `onChange`),所以「置顶 / 上移」按钮不必另写状态同步;越界下标静默忽略,不用自己判边界。 - 拖到最近滚动容器的上下沿会按帧自动滚(感应带 48px),长清单一屏装不下也能拖到位;容器是整页滚动时组件不接管,交给浏览器。跨列表搬运用两个实例各持一个顺序信号。 - 条目文案要唯一——重名会让「谁被搬走了」失去参照,需要重复内容时把序号拼进文案(`'确认条款 #1'`)。 ## 配方 20:分步表单向导(步进条 + 按步校验 + 完成反馈) ```ts const step = signal(0); // 当前步索引:stepper 与内容区共用一份信号 const name = signal(''); const phone = signal(''); const remark = signal(''); const account = formItem({ label: '账号名', required: true, value: name, rules: [{ min: 2, max: 12 }] }, textField({ value: name })); const contact = formItem({ label: '手机号', required: true, value: phone, rules: [{ pattern: /^1\d{10}$/, message: '请输入 11 位手机号' }] }, textField({ value: phone })); const note = formItem({ label: '备注', value: remark }, textarea({ value: remark })); const pages = [[account], [contact, note], []]; // 每步的字段句柄,末页是完成页:推进校验与回跳重放都读这一份 const body = h('div', {}); effect(() => { body.replaceChildren(...pages[step()].map((f) => f.el)); }); // 步进即换页:只挂当前步的字段,末页自然留白 const last = pages.length - 1; // 末步下标:步进条、边界钳制与提交判断共用 const go = (delta) => { // delta>0 是「下一步」:先过当前步校验,不过就不换页 if (delta > 0 && pages[step()].some((f) => !f.validate())) return; step(Math.min(Math.max(step() + delta, 0), last)); }; card({ title: '开通向导' }, stepper({ steps: [{ label: '账号' }, { label: '联系' }, { label: '完成' }], active: step, clickable: true }), body, button({ label: '上一步', variant: 'ghost', disabled: computed(() => step() === 0), onClick: () => go(-1) }), button({ label: '下一步', onClick: () => { if (step() === last) { toast.success('已提交开通申请'); step(0); return; } // 末步即提交:同步校验已在前两步拦过,直接收尾归零 go(1); } }), ); ``` 要点: - `stepper` 的 `active` 与字段容器读同一个 `step` 信号:点步骤条回跳、点「上一步」、提交后归零都只改这一个信号,不存在两份步号。 - 「下一步」先对当前步的 formItem 句柄逐个 `validate()`,有失败项就停在原地标红——把校验拦在步与步之间,不要攒到最后一步一起报错,用户会迷失在哪一步填错。 - `clickable: true` 只放行**已完成**步骤的回跳,点不存在的未来步骤不会生效,所以回跳不会绕过校验;回跳后再次「下一步」会重放该步校验,已填过的值不会丢(信号还在)。 - 换页用 `body.replaceChildren()` 重挂当前步字段:formItem 句柄是稳定对象,重复挂载不会重建控件,信号值与错误态都保留;末页句柄数组为空,`replaceChildren()` 空参即清空,不用特判。 - 「下一步」按钮的 label 是静态的,想在末步显示「提交」就用两个按钮按 `step()` 条件渲染(`h` 里读信号的函数会跟随重渲染);上面的示例为省篇幅在 onClick 里判末步。 - 末步提交前若还有异步规则(如邀请码查重),改用 formItem 的 `validateAsync` 聚合 await,校验期间禁用提交按钮;纯同步表单 `validate()` 已足够。 - 提交成功给 `toast.success` 反馈并把 `step` 归零,向导可以立即重开;要保留数据就别归零,改跳一个只读的完成页。 ## 配方 21:消息中心(铃铛角标 + 抽屉通知流) ```ts const open = signal(false); // 抽屉开关:铃铛与抽屉共用一份 const notices = signal([ // 通知数据唯一真相:未读角标、列表、按钮禁用态都从它派生 { title: '审批通过', description: '报销单 RB-2048 已打款', time: Date.now() - 5 * 60_000, read: false }, { title: '库存预警', description: 'SKU-88 低于安全库存线', time: Date.now() - 26 * 3600_000, read: false }, ]); const unread = computed(() => notices().filter((n) => !n.read).length); // 未读数单一来源,不另立计数变量 const markRead = (title) => notices(notices().map((n) => (n.title === title ? { ...n, read: true } : n))); // 点单条置已读:只写数据源 const bellSlot = h('span'); // badge 的 count 是静态快照:未读数变化时重挂新实例 effect(() => bellSlot.replaceChildren(badge({ count: unread() }))); const bell = h('button', { class: 'ua-btn', onclick: () => open(true) }, '🔔 消息', bellSlot); // badge 不是按钮:点进中心的语义与键盘可达性交给外层 button const feed = h('div'); effect(() => { // list 同样是静态快照组件:数据一变整个重建 feed.replaceChildren(list({ header: `未读 ${unread()}`, items: notices().map((n) => ({ title: n.title, description: n.description, extra: h('span', { class: 'desc' }, relativeTime({ value: n.time, ariaLabel: '通知时间' }), n.read ? '' : badge({ label: '未读', variant: 'danger' })), })), onItemClick: (item) => markRead(item.title), })); }); card({ title: '消息中心' }, bell, drawer({ title: '消息中心', open, width: 380 }, feed, button({ label: '全部已读', variant: 'ghost', disabled: computed(() => unread() === 0), onClick: () => notices(notices().map((n) => ({ ...n, read: true }))) }), ), ); ``` 要点: - 未读数只有一个来源:`unread` 从 `notices` 派生,铃铛角标、列表头、按钮禁用态都读它——任何一处「自己再记一份未读数」都会在已读回写时漂移。 - `list` / `badge` 返回 DOM 而非句柄,是静态快照组件,不自己订信号:接信号统一用「effect + `replaceChildren` 重挂」这一招;句柄式组件(drawer 的 `open`、button 的 `disabled` 收 Signal)才直接传信号。 - badge 不是按钮(无 tabindex 也无键盘响应):「点角标进消息中心」把手势交给外层 `button`,角标只负责报数——这正是 badge 手把手第 4 条的落点。 - 已读回写只改数据源:`markRead` 与「全部已读」都写 `notices`,角标、列表、按钮态一处写处处更新,不要在回调里手动改 DOM。 - `relativeTime` 默认每 30 秒自刷新,「3 分钟前」自己会走字;列表重建后新实例接着算,不用手动更新时间文案。条数极多时把 `list` 换 `virtualList`,并把 `refresh` 调大或传 0。 - 通知条目要可操作时在 list 的 `onItemClick` 里跳详情或调 `notification`(右下角)补一条带按钮的强提醒;轻提示走 `toast`,别在同一屏堆两层浮层。 ## 配方 22:表格里就地改数据(editable + dataTable) 场景:后台列表要改一个预算、改一个区域名,不想让用户点进详情页再回来。 ```ts type Row = { id: string; name: string; budget: string }; const rows = signal([ { id: 'r1', name: '华东区', budget: '120' }, { id: 'r2', name: '华南区', budget: '95' }, ]); /** 回写单格:只有 editable 的 onCommit 会走到这里,Esc 取消根本不触发,所以取消不会污染数据 */ const patch = (id: string, key: 'name' | 'budget', value: string): void => { rows(rows().map((r) => (r.id === id ? { ...r, [key]: value } : r))); }; // 只改这一行这一列 /** 一格的草稿信号:初值取自行数据,与 rows 解耦——编辑中不会被别处的刷新冲掉 */ const cell = (row: Row, key: 'name' | 'budget', title: string) => editable({ value: signal(row[key]), // 草稿缓冲,不是数据源 label: title, validate: (v) => (v.trim() ? null : `${title}不能为空`), // 与 form 同口径:返回文案即不提交 onCommit: (v) => { patch(row.id, key, v); toast.success(`${title}已保存`); }, }); dataTable({ data: rows, // 传信号:提交回写后内核整表换源,重建出的格子拿到新草稿 columns: [ { key: 'name', header: '区域', width: 200, render: (row) => cell(row, 'name', '区域') }, // render 决定这一格画什么 { key: 'budget', header: '预算(万)', align: 'right', render: (row) => cell(row, 'budget', '预算') }, ], pageSize: 5, }); ``` 要点: - 唯一数据源仍是 `rows`:`editable` 里的信号只是草稿缓冲,提交才回写。反过来把草稿当数据源,Esc 取消后表格显示的就是「没保存的值」。 - 列的 `render` 只负责画什么,排序 / 筛选 / 导出仍按 `data` 里的原值走——所以行数据保持原始字符串,别为了显示把数据换成 DOM。 - 提交会让内核整表换源,正在编辑的另一格草稿会被重建冲掉:一次只编辑一格是行内编辑的合理约束,要并行编辑就把草稿提到 `rows` 之外的状态里自己管。 - 校验口径与 `form` 一致:`validate` 返回文案则不提交、保持编辑态并标红(`aria-invalid` + `role=alert`),用户改对再提交即可,不要在回调里弹 toast 了事。 - 格内的点击会被拦下,不会触发 `onRowClick`:点输入框不该等于「打开详情」。反过来说,想整行进详情就别把可交互控件铺满整行。 - 纯文本列继续用 `format`:它走 `textContent`,天然免注入;`render` 只在真要塞组件(tag / avatar / progress / editable)时才用。 ## 通用约定 - 状态一律放信号(`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 的双源一致性校验会直接报错。