# @ui-arch 组件库 Agent 文档（无图形版）

> 自动生成于 2026-10-23T10:58:48.833Z，数据源：各包 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 / api 三钩子）传入 plugins 即插入管线

### @ui-arch/tokens

1. 引入：在应用入口 import '@ui-arch/tokens/tokens.css'
2. 消费：组件样式一律 var(--ua-color-accent) 这类变量，不写死色值
3. 暗色：根元素设 data-theme=dark，变量值自动切换
4. 扩展：新令牌先同时加进 tokens.css 与 tokens.json，再重新生成文档站

### @ui-arch/ui

1. 安装依赖：pnpm add @ui-arch/ui @ui-arch/frame（workspace 内自动链接）
2. 引入样式：在应用入口 import '@ui-arch/ui/ui.css'，暗色主题在根元素设 data-theme='dark'
3. 创建状态：import { signal } from '@ui-arch/frame'，如 const name = signal('')
4. 调用组件：const el = textField({ value: name })，把 el 挂到你的容器或配合 h() 组合
5. 读写状态：name() 读、name('张三') 写，界面自动同步，无需手动操作 DOM

### @ui-arch/cli

1. 装好依赖后先跑 npm run check 确认基线全绿（build → style → typecheck → smoke → index → verify）
2. 查组件：ua list 看全量，ua info <包名> 看单包 api 与约定
3. 改完清单跑 npm run docs：同时刷新人类文档站与 llms-full.txt（Agent 首选喂料）
4. 提交前 npm run check：style 查注释规范，verify 查清单 ↔ 文档 ↔ 演示锚点是否一致
5. 在 ~/.workbuddy/mcp.json 注册 ua mcp 后，Agent 可直接调 list_components / get_component / search_components

## @ui-arch/ui（skin-dom）v0.1.0

28 个 DOM 组件，统一用法：组件是普通函数，传入 props 返回 DOM 节点；需要状态时先创建 signal 再双向绑定。引入 CSS：import '@ui-arch/ui/ui.css'（内部已消费 tokens）。

### image — 图片 Image

参数：

- src（必填）：string —— 图片地址（缩略图与大图共用）
- alt（默认 ''）：string —— 替代文本；缺省空串
- width：number —— 缩略图宽度像素，高度自适应；缺省走样式默认

示例：

```
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
- disabled：boolean | Signal<boolean> —— 禁用态：布尔直接置位，传信号则动态同步
- onClick（回调）：() => void —— 点击回调

示例：

```
button({ label: '提交', variant: 'primary', onClick: () => console.log('clicked') })
```

手把手：

1. 确定语义强度：主操作用 primary（默认），次要操作用 ghost，危险操作用 danger
2. 绑定 onClick 回调处理点击逻辑
3. 需要禁用态时自行在回调首行卫语句判断，或外层包条件渲染

现场演示：apps/docs/demo.html#sec-counter

### textField — 文本输入 TextField

参数：

- value：Signal<string> —— 双向绑定信号：输入写回信号，信号变化写回输入框
- placeholder：string —— 占位文案
- disabled：boolean | Signal<boolean> —— 禁用态：布尔直接置位，传信号则动态同步
- onInput（回调）：(value: string) => void —— 输入回调，每次按键触发

示例：

```
const name = signal(''); const el = textField({ value: name, placeholder: '输入名字…', onInput: (v) => console.log(v) })
```

手把手：

1. 先用 signal('') 创建字符串信号
2. 传入 value 完成双向绑定：用户输入自动写回，外部赋值自动回显
3. 在任意响应式位置用 name() 读取最新值，如 () => `你好，${name()}`

现场演示：apps/docs/demo.html#sec-bind

### inputNumber — 数字输入 InputNumber

参数：

- value（必填）：Signal<number> —— 当前值信号，双向绑定（按钮/键盘写回、外部赋值同步）
- min：number —— 最小值；缺省无下限
- max：number —— 最大值；缺省无上限
- step（默认 1）：number —— 步长；缺省 1
- disabled：boolean | Signal<boolean> —— 禁用态：布尔直接置位，传信号则动态同步
- onChange（回调）：(value: number) => void —— 值变化回调（已夹取到合法区间）

示例：

```
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 支持布尔或信号禁用

现场演示：apps/docs/demo.html#sec-inputnumber

### textarea — 多行输入 Textarea

参数：

- value：Signal<string> —— 双向绑定信号
- rows（默认 3）：number —— 初始行数；缺省 3
- placeholder：string —— 占位文案
- disabled：boolean | Signal<boolean> —— 禁用态：布尔直接置位，传信号则动态同步
- onInput（回调）：(value: string) => void —— 输入回调

示例：

```
const bio = signal(''); textarea({ value: bio, rows: 4, placeholder: '一句话介绍…' })
```

手把手：

1. 创建信号并传入 value
2. 用 rows 控制初始高度，用户可纵向拉伸
3. 字数统计等派生逻辑直接在响应式文本里读 bio().length

现场演示：apps/docs/demo.html#sec-form

### slider — 滑块 Slider

参数：

- value（必填）：Signal<number> —— 当前值信号，双向绑定：拖动写回，外部赋值同步位置
- min（默认 0）：number —— 最小值；缺省 0
- max（默认 100）：number —— 最大值；缺省 100
- step（默认 1）：number —— 步长；缺省 1
- disabled：boolean | Signal<boolean> —— 禁用态：布尔直接置位，传信号则动态同步
- showValue（默认 true）：boolean —— 是否右侧显示当前值；缺省 true
- onChange（回调）：(value: number) => void —— 拖动回调

示例：

```
const vol = signal(40); slider({ value: vol, min: 0, max: 100, step: 5, onChange: (v) => console.log(v) })
```

手把手：

1. 创建数值信号（初值在 min/max 区间内），传入 value 完成双向绑定
2. 用 min / max / step 限定范围与粒度，如 step: 5 只取 5 的倍数
3. 拖动或按 ←→ 键改变值：原生 range 自带键盘与触屏交互，无需额外处理
4. 只想看拖动条不显示数字时传 showValue: false；需要自定义展示可自行读 signal
5. 禁用场景传 disabled（布尔或信号），置灰且不可拖

现场演示：apps/docs/demo.html#sec-slider

### checkbox — 复选框 Checkbox

参数：

- checked：Signal<boolean> —— 勾选状态信号，双向绑定
- label：string —— 右侧文案；缺省只渲染勾选框
- disabled：boolean | Signal<boolean> —— 禁用态：布尔直接置位，传信号则动态同步
- onChange（回调）：(checked: boolean) => void —— 变更回调

示例：

```
const agreed = signal(false); checkbox({ label: '同意条款', checked: agreed, onChange: (v) => console.log(v) })
```

手把手：

1. 创建布尔信号 agreed = signal(false)
2. 传入 checked 与 label，勾选即写回信号
3. 表单提交前用 agreed() 判断是否勾选

现场演示：apps/docs/demo.html#sec-form

### switchButton — 开关 Switch

参数：

- checked：Signal<boolean> —— 开关状态信号，双向绑定
- label：string —— 左侧文案
- disabled：boolean | Signal<boolean> —— 禁用态：布尔直接置位，传信号则动态同步
- onChange（回调）：(checked: boolean) => void —— 变更回调

示例：

```
const night = signal(true); switchButton({ label: '夜间模式', checked: night })
```

手把手：

1. 创建布尔信号并传入 checked
2. 轨道与滑块的动画由 CSS 完成，无需干预
3. 状态变化在 onChange 里做副作用（如切换主题）

现场演示：apps/docs/demo.html#sec-form

### select — 下拉选择 Select（自绘弹层）

参数：

- options（必填）：OptionItem[] —— 选项列表，每项 { value, label }
- value：Signal<string> —— 选中值信号，双向绑定
- placeholder：string —— 未选中时触发器显示的占位文案
- disabled：boolean | Signal<boolean> —— 禁用态：布尔直接置位，传信号则动态同步；禁用时触发器置灰且不展开弹层
- onChange（回调）：(value: string) => void —— 选中回调

示例：

```
const city = signal(''); select({ options: [{ value: 'bj', label: '北京' }], value: city, placeholder: '选择城市…' })
```

手把手：

1. 准备 options 数组，value 是提交用值、label 是展示文案
2. 创建信号并传入 value；未传时组件内部自持状态
3. 点击触发器展开弹层，点选项写回信号并收起；点外部或按 Esc 也会收起
4. 选中项在弹层中用淡色弱化标记，不抢视觉焦点

现场演示：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

现场演示：apps/docs/demo.html#sec-menu

### radioGroup — 单选组 RadioGroup

参数：

- options（必填）：OptionItem[] —— 选项列表
- value：Signal<string> —— 选中值信号，双向绑定
- disabled：boolean | Signal<boolean> —— 整组禁用：全部选项与文案置灰且不可选
- onChange（回调）：(value: string) => void —— 变更回调

示例：

```
const size = signal('m'); radioGroup({ options: [{ value: 's', label: '小' }, { value: 'm', label: '中' }], value: size })
```

手把手：

1. 创建字符串信号，初值设为默认选项的 value
2. 传入 options 与 value，组内自动互斥
3. 读取 size() 获取当前选中值
4. 需要整组禁用时传 disabled：布尔值静态禁用，也可传信号（如 locked）实现动态锁定

现场演示：apps/docs/demo.html#sec-form

### accordion — 折叠面板 Accordion

参数：

- items（必填）：AccordionItem[] —— 面板列表：{ value, title, content, disabled? }
- active：Signal<string> —— 受控展开面板信号（单开模式）；缺省由组件内部持有状态
- multiple（默认 false）：boolean —— 允许多个面板同时展开；缺省 false（手风琴，单开）
- onChange（回调）：(active: string) => void —— 展开变化回调：收起时传空串

示例：

```
accordion({ items: [{ value: 'a', title: '账户设置', content: '内容…' }, { value: 'b', title: '通知', content: h('p', null, '…') }] })
```

手把手：

1. 准备 items：value 作标识、title 作标题、content 可为字符串 / DOM 节点 / 响应式函数
2. 默认手风琴模式：展开一个自动收起其他；需要同时展开多个时传 multiple: true
3. 需要外部控制或读取展开项时传 active 信号（受控单开），否则组件内部自持状态
4. 标题是原生 button：Tab 聚焦、Enter / Space 切换，展开态带 aria-expanded + region 关联
5. 内容区用 grid 行高 0fr → 1fr 过渡，无需测量高度；reduced-motion 用户自动静态切换

现场演示：apps/docs/demo.html#sec-accordion

### breadcrumb — 面包屑 Breadcrumb

参数：

- items（必填）：BreadcrumbItem[] —— 路径条目：{ label, href? }，末项自动视为当前页
- separator（默认 /）：string —— 分隔符；缺省 '/'
- onNavigate（回调）：(item, index) => void —— 无 href 条目的点击回调

示例：

```
breadcrumb({ items: [{ label: '首页', href: '/' }, { label: '设置' }, { label: '账户' }], onNavigate: (it) => console.log(it.label) })
```

手把手：

1. 按层级顺序准备 items，最后一项即当前页，自动渲染为不可点的灰色文字并带 aria-current='page'
2. 需要真实跳转就给条目加 href（渲染 <a>）；单页应用想自行接管时不给 href，改用 onNavigate 回调
3. 改分隔符传 separator，如 '›'；分隔符对读屏隐藏
4. 语义结构为 nav + ol + li，可直接放进页头或卡片标题栏

现场演示：apps/docs/demo.html#sec-breadcrumb

### card — 卡片 Card

参数：

- title：string —— 标题；缺省不渲染标题行
- ...children：unknown[] —— 任意子节点：字符串 / DOM / 响应式函数

示例：

```
card({ title: '用户表' }, dataTable({ data, columns }))
```

手把手：

1. card 是容器组件：第一个参数是 props，其余参数是任意子节点
2. 传 title 显示标题行，否则只有内容区
3. 子节点可以混合字符串、DOM 节点与响应式函数

现场演示：apps/docs/demo.html#sec-card

### badge — 徽标 Badge

参数：

- label（必填）：string —— 徽标文案
- variant（默认 default）：'default' | 'success' | 'warning' | 'danger' —— 语义色；缺省 default

示例：

```
badge({ label: '成功', variant: 'success' })
```

手把手：

1. 选择语义色：default 中性、success 成功、warning 警告、danger 危险
2. 传入 label 即可，纯展示组件无状态
3. 状态语义靠 variant 承载，不要自造颜色；纯展示不可交互，需要操作请配 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 —— 关闭回调：调用方从数据源移除该项后重新渲染

示例：

```
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 等容器，自动换行由容器决定

现场演示：apps/docs/demo.html#sec-tag

### avatar — 头像 Avatar

参数：

- name（必填）：string —— 名称：生成占位首字，并作为图片 alt 与悬停标题
- src：string —— 图片地址；缺省用首字占位
- size（默认 32）：number —— 直径像素；缺省 32
- variant（默认 default）：'default' | 'success' | 'warning' | 'danger' —— 占位底色语义档；缺省 default

示例：

```
avatar({ name: '张三' }); avatar({ name: 'Alice', size: 40, variant: 'success' })
```

手把手：

1. 只传 name：用名称首字做占位（中文取首字、英文大写），底色走 accent 淡化，暗色自动适配
2. 有图片就传 src：渲染 img 并用 name 作 alt，读屏语义完整；图片缺失时由浏览器 alt 兜底
3. 尺寸用 size（像素）控制，头像不参与布局拉伸（flex:none），可安全排进 .row 与表格单元格
4. 需要区分类别（在线/告警）时用 variant 换底色档，颜色一律来自令牌

现场演示：apps/docs/demo.html#sec-avatar

### alert — 提示条 Alert

参数：

- message（必填）：string —— 提示内容
- title：string —— 加粗标题行；缺省只渲染正文
- variant（默认 info）：'info' | 'success' | 'warning' | 'danger' —— 语义色；缺省 info
- closable（默认 false）：boolean —— 显示 × 关闭按钮，退场后从 DOM 移除；缺省 false
- onClose（回调）：() => void —— 关闭回调，供调用方同步业务状态

示例：

```
alert({ message: '操作可能存在风险', variant: 'warning' })
```

手把手：

1. 按信息级别选 variant：info 提示、success 成功、warning 警告、danger 错误
2. 传入 message 即可，入场自带下滑动画
3. 需要可关闭或带操作按钮时，外层用 h() 组合 button 自行拼装，alert 只负责语义底色
4. 需要用户可自行消除的提示传 closable: true：点 × 淡出并从 DOM 移除，配合 onClose 同步业务状态
5. 有标题与正文两层信息时传 title（加粗首行），正文仍走 message

现场演示：apps/docs/demo.html#sec-feedback

### skeleton — 骨架屏 Skeleton

参数：

- variant（默认 rect）：'text' | 'rect' | 'circle' —— 占位形状；缺省 rect（整块矩形）
- rows（默认 3）：number —— text 形状的行数；缺省 3，末行自动缩短模拟段落

示例：

```
skeleton({ variant: 'text', rows: 3 }); skeleton({ variant: 'circle' }); skeleton({ variant: 'rect' })
```

手把手：

1. 数据加载期间用 skeleton 顶替真实内容，加载完成后整体替换
2. 文本段落用 variant: 'text' + rows 控制行数，头像用 'circle'，卡片/图片区域用 'rect'
3. 占位块自带扫光动画，reduced-motion 用户自动静态化为纯灰块
4. 组合布局：skeleton 是普通 DOM，可放进 card / row 等任意容器

现场演示：apps/docs/demo.html#sec-skeleton

### toast — 消息提示 Toast

参数：

- message（必填）：string —— 提示文案
- duration（默认 3000）：number —— 自动消失毫秒；缺省 3000
- closable（默认 false）：boolean —— 显示关闭按钮，点 × 立即收起；缺省 false（仅可关闭的 toast 接收指针事件，其余保持穿透）
- variant（默认 info）：'info' | 'success' | 'warning' | 'danger' —— 语义色；缺省 info（toast.error 对应 danger）

示例：

```
toast.success('保存成功'); toast.error('网络请求失败', 5000)
```

手把手：

1. 直接调用语义工厂：toast.info / success / warning / error(message, duration?)，无需先建容器
2. 不传 duration 时默认停留 3 秒后自动淡出并移除节点
3. 多条消息在右上角自动纵向堆叠，互不遮挡
4. 需要完整入参（含 variant）时用 showToast(props)；组件挂载容器由内部懒创建，样式随 ui.css 一起引入
5. 同屏最多 4 条，超出自动挤掉最早一条；需要用户主动确认的消息用 showToast({ closable: true }) 给个 ×

现场演示：apps/docs/demo.html#sec-toast

### tooltip — 悬浮提示 Tooltip

参数：

- content（必填）：string —— 提示文案（纯文本）
- placement（默认 top）：'top' | 'bottom' —— 气泡方位；缺省 top（触发器上方）
- delay（默认 150）：number —— 出场延迟毫秒；缺省 150，传 0 立即显示
- trigger（必填）：HTMLElement —— 第二个参数：被包裹的触发器元素（按钮 / 输入框等任意 DOM）

示例：

```
tooltip({ content: '保存后立即生效' }, button({ label: '保存', onClick: save }))
```

手把手：

1. 先创建触发器元素，如 const btn = button({ label: '保存' })
2. 用 tooltip(props, trigger) 包一层：返回带气泡的包裹元素，原触发器仍是独立节点
3. 鼠标移入或键盘聚焦约 150ms 后出场，移出 / 失焦立即收起；按 Esc 可立刻关闭
4. 气泡默认在上方，空间不足或需要避让时传 placement: 'bottom'
5. 气泡 pointer-events: none 不抢指针；判读依赖 aria-describedby + role='tooltip'，读屏可读
6. 只做展示：不要在气泡里放可点击元素，需要交互请用 modal 或自绘弹层

现场演示：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 —— 第二个参数：被包裹的触发器元素

示例：

```
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

现场演示：apps/docs/demo.html#sec-popover

### spinner — 加载指示器 Spinner

参数：

- size（默认 18）：number —— 直径像素；缺省 18

示例：

```
spinner(); spinner({ size: 26 })
```

手把手：

1. 数据加载时插入 spinner()，加载完成移除
2. 用 size 匹配上下文尺寸（按钮内用小号，整页 loading 用大号）
3. 与 skeleton 分工：已知内容结构用 skeleton 占位，只有『在跑』的短等待用 spinner

现场演示：apps/docs/demo.html#sec-feedback

### progress — 进度条 Progress

参数：

- value：Signal<number> —— 当前进度 0-100 信号，变化自动平滑过渡（越界钳制）
- showText（默认 true）：boolean —— 是否显示百分比文字；缺省 true

示例：

```
const pct = signal(0); progress({ value: pct }); setInterval(() => pct((pct() + 5) % 105), 400)
```

手把手：

1. 时长未知的加载直接 progress() 不传 value：填充块匀速往复，不显示百分比，读屏读作进行中
2. 创建数值信号 pct = signal(0)，范围 0-100（越界自动钳制）
3. 传入 value，填充宽度与百分比文字随信号平滑过渡
4. 进行中填充带流动条纹，到 100% 自动变 success 色并停止条纹
5. 不需要百分比文字时传 showText: false
6. 定量进度用 value 信号：满格自动切 success 色并停止条纹，role=progressbar + aria-valuenow 同步

现场演示：apps/docs/demo.html#sec-feedback

### tabs — 标签页 Tabs

参数：

- tabs（必填）：OptionItem[] —— 页签列表，每项 { value, label }
- active：Signal<string> —— 当前激活页签 value 信号，双向绑定
- onChange（回调）：(value: string) => void —— 切换回调

示例：

```
const tab = signal('a'); tabs({ tabs: [{ value: 'a', label: '概览' }, { value: 'b', label: '明细', disabled: true }], active: tab })
```

手把手：

1. 创建信号 tab = signal('a')，初值为默认页签
2. tabs 只渲染页签头；页签内容用响应式函数按信号切换，如 () => tab() === 'a' ? panelA : panelB
3. 点击或方向键（←→ 环形）切换，滑动指示条自动跟随；disabled: true 的页签置灰不可点
4. 键盘可访问：Tab 聚焦激活页签，←→ 在启用页签间环形移动

现场演示：apps/docs/demo.html#sec-tabs-modal

### stepper — 步骤条 Stepper

参数：

- steps（必填）：StepperItem[] —— 步骤列表，每项 { label, description? }
- active（必填）：Signal<number> —— 当前步索引（0 基）信号，双向绑定：回跳写回、外部赋值同步
- clickable：boolean —— 已完成步骤是否可点击回跳；缺省 false
- 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 同步业务状态

现场演示：apps/docs/demo.html#sec-stepper

### rate — 评分 Rate

参数：

- value（必填）：Signal<number> —— 当前评分（1 基整数值）信号，双向绑定：点选/键盘写回、外部赋值同步
- max（默认 5）：number —— 星标总数；缺省 5
- disabled：boolean | Signal<boolean> —— 禁用态：布尔直接置位，传信号则动态同步
- onChange（回调）：(value: number) => void —— 评分变化回调

示例：

```
const score = signal(0); rate({ value: score, max: 5, onChange: (v) => console.log(v) })
```

手把手：

1. 用 signal(0) 创建评分信号并传入 value；0 表示未评分
2. 点击星标或聚焦后按 →/↑ 加一分、←/↓ 减一分；悬停高亮预览、移出还原
3. max 可调整星标总数；disabled 支持布尔或信号禁用

现场演示：apps/docs/demo.html#sec-rate

### empty — 空状态 Empty

参数：

- title：string —— 主标题；缺省不渲染
- description：string —— 补充说明；缺省不渲染
- action：unknown —— 操作区内容（常见为按钮）；缺省不渲染

示例：

```
empty({ title: '暂无数据', description: '调整筛选条件后再试', action: button({ label: '重置筛选', variant: 'ghost' }) })
```

手把手：

1. 在列表 / 表格 / 筛选结果为空时渲染 empty 占位
2. title 一句话说明状态，description 补充原因或建议
3. action 传入 DOM（如 ghost 按钮）引导用户下一步操作

现场演示：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 切虚线弱化分隔感

现场演示：apps/docs/demo.html#sec-divider

### pagination — 分页器 Pagination

参数：

- page（必填）：Signal<number> —— 当前页码信号，双向绑定：外部赋值自动翻页，点击页码写回信号
- pageSize（必填）：number —— 每页条数
- total（必填）：number —— 总条数
- onChange（回调）：(page: number) => void —— 翻页回调

示例：

```
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 触发

现场演示：apps/docs/demo.html#sec-table

### modal — 弹窗 Modal

参数：

- open（必填）：Signal<boolean> —— 开关信号：true 显示 / false 隐藏
- title（必填）：string —— 标题
- size（默认 md）：'sm' | 'md' | 'lg' —— 宽度档位：sm 340px / md 420px / lg 640px；缺省 md
- footer：unknown —— 底部操作区内容（常见为按钮组，右对齐）；缺省不渲染
- 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)，点遮罩或 × 会自动写回 false
4. 入场自带遮罩淡入 + 弹窗上浮动画
5. 底部操作按钮放进 footer（如取消/确定），需要更宽内容用 size: 'lg'，窄确认框用 'sm'

现场演示：apps/docs/demo.html#sec-tabs-modal

### drawer — 抽屉 Drawer

参数：

- title（必填）：string —— 标题
- open（必填）：Signal<boolean> —— 开关信号：true 滑出 / false 收起
- placement（默认 right）：'left' | 'right' —— 滑出方向；缺省 right
- width（默认 320）：number —— 面板宽度像素（窄屏最多 92vw）；缺省 320
- footer：unknown —— 底部操作区内容；缺省不渲染
- 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；打开时焦点落在面板首个可交互元素
4. 底部操作按钮放进 footer；与 modal 的分工：modal 是强打断的确认，drawer 保留主视图上下文做详情/编辑
5. 面板内容与 modal 一样接受任意 DOM 与响应式函数

现场演示：apps/docs/demo.html#sec-drawer

### form — 表单 Form / 表单字段 FormItem

参数：

- layout（默认 vertical）：'vertical' | 'horizontal' —— 布局方向；缺省 vertical（horizontal 为标签左置）
- submitText：string —— 提交按钮文案；缺省「提交」
- resetText：string —— 重置按钮文案；缺省不渲染
- onSubmit（回调）：() => void —— 全部字段校验通过后的提交回调
- （formItem）label（必填）：string —— 字段标签
- （formItem）value（必填）：Signal<string> —— 字段值信号，校验数据源
- （formItem）required：boolean —— 必填：显示红星并自动追加非空规则
- （formItem）rules：FormRule[] —— 校验规则：required / min / max / pattern / validator，按序执行取首个失败
- （formItem）validateOn（默认 blur）：'input' | 'blur' | 'submit' —— 校验时机；缺省 blur（输入时自动清已报错误）

示例：

```
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 / 单选组这类包裹式控件同样能失焦校验

现场演示：apps/docs/demo.html#sec-formlab

### dataTable — 数据表格 DataTable

参数：

- data（必填）：T[] —— 全量行数据，每行须含 id 字段作 rowKey
- columns（必填）：ColumnDef<T>[] —— 列定义：内核字段 { key, header?, sortable?, width? } + 皮肤字段 { align?, format? }；width 数字为固定像素，'flex' 自适应
- filterKey：string —— 工具栏筛选的列；缺省取第一列
- pageSize（默认 5）：number —— 每页行数；缺省 5
- selectable（默认 false）：boolean —— 启用行选择列：全选三态（含半选）、行 checkbox、已选计数；缺省 false
- page：Signal<number> —— 受控页码信号：与内核分页双向同步，传同一个信号给独立 pagination 组件即可联动
- loading：boolean | Signal<boolean> —— 加载态：为真时数据行换成骨架行（行数随 pageSize），筛选输入与翻页按钮临时禁用，容器带 aria-busy
- selected：Signal<RowKey[]> —— 受控选中行信号：与内核选择双向同步（内容比较，不来回派发），供批量操作读取/改写

示例：

```
dataTable({ data: users, selectable: true, columns: [{ key: 'name', header: '姓名', sortable: true }], pageSize: 5 })
```

手把手：

1. 准备行数据，确保每行有唯一 id（跨页选择与刷新不丢行靠它）
2. 定义 columns：sortable: true 的列表头可点击排序（升 → 降 → 取消循环），标记箭头 ↑↓↕
3. 列展示细节用皮肤字段：align 控制表头与单元格对齐，format(value, row) 返回纯文本（走 textContent，不需要转义）
4. 列宽用内核字段 width：数字（像素）固定该列，'flex' 或不设则按内容自适应
5. selectable: true 开启行选择：表头全选框带 indeterminate 半选态，工具栏显示已选计数
6. 选中行用低视觉比重样式（淡色底 + 弱化文字），选择状态存 rowKey，跨页不丢失
7. 工具栏输入框即筛选，输入即时过滤并回第 1 页；无匹配时显示空状态行
8. 表头粘性固定（滚动容器 max-height 420px），斑马纹 + 悬停高亮由 CSS 提供
9. 数据在途时传 loading（布尔或信号）：表格体换成骨架行、筛选与翻页禁用，返回后置回 false 即恢复
10. 需要独立分页器联动时传 page 信号：const page = signal(1)，dataTable({ page }) 与 pagination({ page }) 共用
11. 需要批量操作时传 selected 信号：内部勾选实时写回，外部置空或赋值即同步整表（跨页保留，按 rowKey 存）

现场演示：apps/docs/demo.html#sec-table

API 速览：

- button(props)：按钮，variant = primary | ghost | danger，disabled 支持布尔或信号禁用
- textField(props)：文本输入，value 信号双向绑定，disabled 支持布尔或信号禁用
- inputNumber(props)：数字输入，−/+ 按钮 + 键盘 ↑↓ 调节，value 信号双向绑定，min/max/step 超界自动夹取
- textarea(props)：多行输入，value 信号双向绑定，disabled 支持布尔或信号禁用
- slider(props)：滑块，value 信号双向绑定，min/max/step 可配，原生 range 承载键盘交互，右侧可显示当前值
- checkbox(props)：复选框，checked 信号双向绑定，disabled 支持布尔或信号禁用
- switchButton(props)：开关，checked 信号双向绑定，disabled 支持布尔或信号禁用
- select(props)：自绘下拉选择（非原生 select），value 信号双向绑定，点外部 / Esc 关闭，选项弹层带微动画，disabled 支持布尔或信号禁用
- menu(props)：下拉菜单，触发器 + 自绘弹层，条目支持禁用 / 危险 / 分隔线，↑↓ 环形导航，点外部 / Esc 关闭
- radioGroup(props)：单选组，options + value 信号双向绑定，disabled 整组禁用
- accordion(props)：折叠面板，标题按钮 + grid 行高过渡，单开（手风琴）或多开，支持禁用面板与 active 信号受控
- breadcrumb(props)：面包屑，nav + ol 语义结构，末项为当前页（aria-current），有 href 走原生链接、无 href 交给 onNavigate
- card(props, ...children)：卡片容器
- badge(props)：徽标，variant = default | success | warning | danger
- tag(props)：可关闭标签，variant 五档语义色，closable + onClose 支持从数据源移除（纯展示请用 badge）
- avatar(props)：头像，有 src 渲染图片（alt 用名称）、无 src 用首字占位，size 可调 + 四档语义底色
- alert(props)：提示条，variant 四档语义色 + 可选 title 与 closable 关闭按钮，role=alert 读屏可播报
- toast.info/success/warning/error(message, duration?)：消息提示工厂，右上角堆叠，默认 3s 自动消失，出入场微动画；showToast(props) 为完整入参版本（closable 显示关闭按钮，同屏最多 4 条）
- tooltip(props, trigger)：悬浮提示，包裹触发器，hover / focus 延迟 150ms 出场，移出 / 失焦 / Esc 收起
- popover(props, trigger)：可交互气泡卡片，包裹触发器，默认点击开合 + 点外部 / Esc 收起，content 可放按钮等交互元素
- skeleton(props)：骨架屏占位，variant = text | rect | circle，text 支持 rows 多行，扫光微动画
- pagination(props)：独立分页器，page 信号双向绑定，页码过多折叠省略号，首末页禁用翻页，键盘可达
- spinner(props)：加载指示器，size 控制直径
- progress(props)：进度条，value 信号驱动 0-100（满格切 success 色）；不传 value 退化为不确定态无限往复
- tabs(props)：标签页，active 信号双向绑定，滑动指示条 + 方向键环形导航，支持禁用页签
- stepper(props)：步骤条，done/current/upcoming 三态，active 信号双向绑定，clickable 时已完成步骤可回跳
- rate(props)：评分星标，value 信号双向绑定，hover 预览移出还原，键盘方向键调节，max 可配
- empty(props)：空状态占位，title/description 可选，action 放操作区
- divider(props)：分隔线，水平/垂直两种方向，可带居中文案标签，dashed 切虚线
- 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 控制缩略宽

一行用法：

```
const checked = signal(false); checkbox({ label: '同意', checked }); const open = signal(false); modal({ title: '确认', open }, '内容')
```

约定：

- 皮肤不持有业务状态，一律委托 headless 内核或调用方信号
- 表单类组件统一约定：*Props.value/checked/active 为 Signal 双向绑定，onChange/onInput 为回调
- 语义色 variant 四档：default|info / success / warning / danger，一律来自令牌，禁止硬编码颜色
- 无障碍基线：装饰性占位（skeleton）aria-hidden、加载指示（spinner）role=status、关闭按钮必须 aria-label、弹层弹窗 aria-haspopup/expanded、读屏可见的状态一律同步 aria-*

---

## @ui-arch/frame（headless-core）v0.1.0

零依赖微框架：组件 = 返回 DOM 的普通函数。signal/effect/computed 管状态，h() 管 DOM，无虚拟 DOM 无生命周期抽象。

API 速览：

- signal(initial) -> Signal<T>：无参读值并收集依赖，传参写值并触发订阅
- effect(fn)：立即执行并追踪依赖，信号变化自动重跑
- computed(fn)：由信号派生的只读计算值
- h(tag, props, ...children)：hyperscript；on* 绑事件，函数值属性/子节点自动响应式
- renderList(el, fn)：信号变化时整体替换容器子节点
- mount(target, view)：挂载视图

一行用法：

```
const count = signal(0); mount('#app', () => h('button', { onClick: () => count(count() + 1) }, () => `count = ${count()}`))
```

约定：

- 组件 = 返回 DOM 节点的普通函数，不做类组件与生命周期抽象
- 跨系统状态（如 table-core）用桥接信号：subscribe 里递增 version 信号

---

## @ui-arch/table-core（headless-core）v0.1.0

Headless 表格内核：不渲染任何 DOM，只管理状态与派生。任何渲染层通过 getVisibleRows() 拿行模型，配 subscribe() 刷新。

API 速览：

- createTable(options) -> TableInstance: create headless table instance
- sortPlugin(options?) -> multi-column sorting plugin (asc -> desc -> off cycle)
- filterPlugin(options?) -> column filter plugin (default substring match, custom predicate supported)
- paginationPlugin() -> client-side pagination with page clamping, exposes setPage/nextPage/prevPage/getPageInfo
- selectionPlugin() -> rowKey-based selection, exposes selectAllVisible

一行用法：

```
const table = createTable({ data, columns, rowKey: 'id', plugins: [filterPlugin(), sortPlugin(), paginationPlugin()], initialState: { pagination: { page: 1, pageSize: 20 } } }); table.dispatch({ type: 'toggleSort', key: 'name' }); table.getVisibleRows()
```

约定：

- selection/expanded must store rowKey, never row index
- derive pipeline order is fixed: filter -> sort -> paginate
- actions are the only way to mutate state; state is immutable

---

## @ui-arch/tokens（design-tokens）v0.1.0

设计令牌唯一来源：CSS 变量 --ua-* + 同名 JSON。皮肤只允许消费变量，禁止硬编码颜色；暗色只换值不换名。

API 速览：

- tokens.css: import '@ui-arch/tokens/tokens.css' once at app root
- tokens.json: programmatic access for tools and agents

一行用法：

```
import '@ui-arch/tokens/tokens.css'; then use var(--ua-color-accent) etc. in any skin package
```

---

## @ui-arch/cli（tooling）v0.1.0

工程层工具包（private，不发布）。所有 Agent 与人都通过它读取组件库：清单驱动索引、文档、MCP 三件套，外加 style / verify 两道门禁。

API 速览：

- ua index：扫描 packages/ 两层目录的 ua.manifest.json，生成 .ua/index.json 与 llms.txt
- ua list：列出注册表内所有包与组件数
- ua info <name>：查看单个包的清单详情（api / 约定 / 文档步骤）
- 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 锚点存在）
- ua commit-lint <文件|message>：提交信息门禁（type 枚举 / scope 规则 / subject 中文祈使 ≤50 字符），规范见 COMMITTING.md，违规退出码 1
- ua mcp：零依赖 stdio MCP server，暴露 list_components / get_component / search_components

一行用法：

```
node packages/tools/cli/bin/ua.js <index|list|info <name>|docs|style|verify|commit-lint|mcp>；仓库内等价于 npm run index / docs / style / verify
```

约定：

- 零依赖：只用 node 标准库，Node >= 18
- 清单是单一来源：代码注释、文档站、llms-full.txt、MCP 全部从 ua.manifest.json 派生
- 门禁只增不改：新增校验必须能被人手复现，报错要指出文件与字段

---

## 设计令牌（--ua-*）

- --ua-color-bg: #ffffff（暗色 #101216）
- --ua-color-surface: #f6f7f9（暗色 #1a1c22）
- --ua-color-text: #1c1e21（暗色 #e7e9ec）
- --ua-color-textMuted: #6b7280（暗色 #888780）
- --ua-color-accent: #185fa5（暗色 #378add）
- --ua-color-danger: #a32d2d
- --ua-color-success: #0f6e56

### radius

- --ua-radius-sm: 4px
- --ua-radius-md: 8px
- --ua-radius-lg: 12px

### space

- --ua-space-1: 4px
- --ua-space-2: 8px
- --ua-space-3: 12px
- --ua-space-4: 16px
