Compare commits

...

38 Commits

Author SHA1 Message Date
HIsheR 511a3901a7 chore(backlog): 记录 g06 完成 2026-10-23 19:08:38 +08:00
HIsheR b6c948998b iter(g06): 组件数门禁覆盖 README 2026-10-23 19:08:35 +08:00
HIsheR 8d439f068f chore(backlog): 记录 g05 完成 2026-10-23 19:07:37 +08:00
HIsheR 9651c39f83 iter(g05): verify 增加 llms-full 同步校验 2026-10-23 19:07:34 +08:00
HIsheR 86c72f7090 chore(backlog): 记录 g04 完成 2026-10-23 19:05:49 +08:00
HIsheR c0bb8d705d iter(g04): dataTable 筛选框加搜索图标前缀 2026-10-23 19:05:46 +08:00
HIsheR 5131977f54 chore(backlog): 记录 g03 完成 2026-10-23 19:04:29 +08:00
HIsheR bfa70132da iter(g03): 输入组件补 prefix/suffix 附加块 2026-10-23 19:04:27 +08:00
HIsheR d5ac26af31 chore(backlog): 记录 g02 完成 2026-10-23 18:59:35 +08:00
HIsheR 33979b23a0 iter(g02): 新增 image 图片组件 2026-10-23 18:59:32 +08:00
HIsheR 0c42c6f71e chore(backlog): 记录 g01 完成 2026-10-23 18:55:25 +08:00
HIsheR 98f557cba8 iter(g01): ua docs 幂等化防时间戳弄脏工作区 2026-10-23 18:55:22 +08:00
HIsheR 7ce8bf72d4 chore(backlog): 记录 f06 完成 2026-10-23 18:53:34 +08:00
HIsheR a5439fe4a6 iter(f06): 锁文件入库并补全目录树 2026-10-23 18:53:31 +08:00
HIsheR 35c9f00e26 chore(backlog): 记录 f05 完成 2026-10-23 18:52:13 +08:00
HIsheR 265060fdb3 iter(f05): index 幂等化防时间戳弄脏工作区 2026-10-23 18:52:09 +08:00
HIsheR fbe0348b47 chore(backlog): 记录 f04 完成 2026-10-23 18:48:37 +08:00
HIsheR 99ce0cdd63 iter(f04): commit-lint 收严强制 scope 2026-10-23 18:48:34 +08:00
HIsheR f1363e81f7 chore(backlog): 记录 f03 完成 2026-10-23 18:46:56 +08:00
HIsheR f57da4c74e iter(f03): 新增留痕格式门禁 lint-iterations 2026-10-23 18:46:53 +08:00
HIsheR 3fb3cd2395 chore(backlog): 记录 f02 完成 2026-10-23 18:44:09 +08:00
HIsheR f2fc286eeb iter(f02): check 链描述补 verify 第六步 2026-10-23 18:44:06 +08:00
HIsheR b9b05f53fb chore(backlog): 记录 f01 完成 2026-10-23 18:42:46 +08:00
HIsheR c19f83b413 iter(f01): 组件数漂移纳入 verify 门禁并修正文档 2026-10-23 18:42:43 +08:00
HIsheR 67374c78c3 chore(backlog): 灌入审计产出的 f01-f06 应修条目 2026-10-23 18:36:58 +08:00
HIsheR cbf1d4a248 chore: 留痕补 commit 哈希 2026-10-23 18:14:50 +08:00
HIsheR c457e0cc33 iter(e05): 新增 divider 分隔线组件 2026-10-23 18:14:47 +08:00
HIsheR 84d008b5fd chore: 留痕补 commit 哈希 2026-10-23 18:12:01 +08:00
HIsheR fd4ee018e6 iter(e04): 新增 empty 空状态组件 2026-10-23 18:11:47 +08:00
HIsheR 62d4ce648d chore: 留痕补 commit 哈希 2026-10-23 18:07:59 +08:00
HIsheR ee20322e3f iter(e03): 新增 rate 评分组件 2026-10-23 18:07:42 +08:00
HIsheR dc4bac4d17 docs: 同步提交规范到 AGENTS/ITERATION/cli 清单 2026-10-23 18:07:39 +08:00
HIsheR 7072f77f88 feat(cli): 新增 commit-lint 提交信息校验 2026-10-23 18:07:37 +08:00
HIsheR 4f7d1e86b3 chore: 留痕补 e02 commit 哈希 2026-10-23 17:57:46 +08:00
HIsheR fc7a1e8299 iter: e02 新增 stepper 步骤条组件(三态流转 + active 信号双向绑定 + clickable 回跳) 2026-10-23 17:57:44 +08:00
HIsheR 47bea0b69c chore: 留痕补 e01 commit 哈希 2026-10-23 17:50:46 +08:00
HIsheR 20d7b9440f iter: e01 新增 inputNumber 数字输入组件(−/+ 步进 + 键盘上下调节,信号双向绑定,min/max/step 夹取) 2026-10-23 17:50:44 +08:00
HIsheR d7226f452d iter: 协议加时钟硬规范——时长禁止自报估算,预算判断改 date 实测差值 2026-10-23 17:35:40 +08:00
29 changed files with 1768 additions and 88 deletions
+136
View File
@@ -276,6 +276,142 @@
"id": "d29", "id": "d29",
"title": "menu 补 Home/End 跳转,与 select 键盘导航对称", "title": "menu 补 Home/End 跳转,与 select 键盘导航对称",
"status": "done" "status": "done"
},
{
"id": "e01",
"title": "新增 inputNumber 数字输入组件",
"motivation": "数量、份数等离散数值输入是表单常见场景,纯 textField 缺少步进语义;加减按钮 + 键盘调节可复用既有 button 与信号双向绑定模式",
"acceptance": "packages/skin/ui 新增 inputNumber.ts:−/+ 按钮 + 输入框三段式,value 信号双向绑定,min/max/step 超界自动夹取,键盘 ↑↓ 调节,非法输入回滚,disabled 支持布尔或信号;只消费 --ua-* 令牌;导出、manifest、文档、llms-full.txt 同步;demo 加一节演示;npm run check 全绿",
"status": "done",
"notes": "inputNumber(props):−/+ 复用 ghost button(自带微动画与 disabled 透传),输入框 inputmode=decimal,change 时 parseFloat 解析、非法回滚当前值;↑↓ 步进并 preventDefault;clamp 统一夹取 min/max(缺省 ±Infinity);aria-label 三件套 + role=group;CSS 步进按钮收窄、tabular-nums;demo 新增 sec-inputnumber(与禁用态实例共用 qty 信号);manifest/docs/llms-full/README 同步为 29 组件"
},
{
"id": "e02",
"title": "新增 stepper 步骤条组件",
"motivation": "下单、注册等多步流程需要进度指示与回跳,现有导航类组件(tabs/breadcrumb/pagination)都不覆盖分步语义;纯展示 + 可选点击回跳,改动范围可控",
"acceptance": "packages/skin/ui 新增 stepper.ts:stepper({steps, active, clickable?, onChange?}),done/current/upcoming 三态视觉,clickable 时已完成步骤可点回跳并写回 active 信号,aria-current=step;只消费 --ua-* 令牌;导出、manifest、文档、llms-full.txt、README 清单同步;demo 加一节演示;npm run check 全绿",
"status": "done",
"notes": "stepper(props):active 信号变化整条重渲染(effect 首跑赋值 + replaceWith,条目少开销可忽略);done 显示 ✓、current 光环聚焦、upcoming 弱化,aria-current=step;clickable 时已完成步骤渲染为 button 回跳写回信号;连接线用 .ua-step + .ua-step::before 横向短线;demo 新增 sec-stepper(上一步/下一步按钮 computed 禁用联动);README 同步 30 组件"
},
{
"id": "e03",
"title": "新增 rate 评分组件",
"motivation": "评价、打分场景常用,现有组件无覆盖;星标交互可完整复用信号双向绑定与微动画审美约定,单文件可控",
"acceptance": "packages/skin/ui 新增 rate.ts:rate({value, max?, disabled?, onChange?}),max 缺省 5,hover 预览移出还原,键盘 ←→/↑↓ 调节,aria 星标语义;只消费 --ua-* 令牌;导出、manifest、文档、llms-full.txt、README 清单同步;demo 加一节演示;npm run check 全绿",
"status": "done",
"notes": "rate(props):hover 预览用组件内局部 hover 信号(容器 mouseleave 归零),shown = hover || value 决定点亮数;value/hover 变化整条重渲染(与 stepper 同款 effect + replaceWith 模式);键盘 →/↑ 加分 ←/↓ 减分并 clamp [1,max];role=slider + aria-valuenow;选中色取 --ua-color-warning 金黄;demo 新增 sec-rate(可交互 + 只读两实例共用 score 信号);README 同步 31 组件"
},
{
"id": "e04",
"title": "新增 empty 空状态组件",
"motivation": "列表、表格、搜索无结果时需要统一的占位提示,dataTable 内置空态样式但独立场景(卡片、面板、筛选结果)没有可复用组件",
"acceptance": "packages/skin/ui 新增 empty.ts:empty({title?, description?, action?}),居中排版,action 为可选 DOM 操作区(常见放按钮);只消费 --ua-* 令牌;导出、manifest、文档、llms-full.txt、README 清单同步;demo 加一节演示;npm run check 全绿",
"status": "done",
"notes": "empty(props):纯静态组件(无信号),role=status 由读屏播报;装饰符号 ⌀ 弱化为边框色且 aria-hidden;min-height 120px 撑留白(令牌只到 space-4);demo 新增 sec-empty(信号切换有数据/无结果对比,内容用响应式函数渲染);README 同步 32 组件"
},
{
"id": "e05",
"title": "新增 divider 分隔线组件",
"motivation": "表单分组、卡片内区块、工具栏之间需要统一分隔语义,目前只有 menu 内部私有分隔线,独立场景无组件可用",
"acceptance": "packages/skin/ui 新增 divider.ts:divider({label?, vertical?, dashed?}),水平/垂直两种方向,可带居中文案标签;只消费 --ua-* 令牌;导出、manifest、文档、llms-full.txt、README 清单同步;demo 加一节演示;npm run check 全绿",
"status": "done",
"notes": "divider(props):水平线用 ::before/::after 双线夹 label 居中文案;vertical 单线(伪元素清空,dashed 走 border-left);dashed 水平线 border-top 替代背景色;role=separator + aria-label;demo 新增 sec-divider(文案线/虚线/工具栏竖线三种用法);README 同步 33 组件"
},
{
"id": "f01",
"title": "组件数漂移治理:AGENTS/ARCHITECTURE 数字纳入 verify 门禁",
"motivation": "组件数第三次漂移(AGENTS.md 与 ARCHITECTURE.md 仍写 27,实际 33),ua verify 只锁 README 区块锁不住散文数字",
"acceptance": "ua verify 新增规则:扫描 AGENTS.md 与 ARCHITECTURE.md 中的「NN 个组件」模式数字,与 manifest docs.components 长度比对不一致即报错;当前两处 27 改为 33;npm run check 全绿",
"status": "done",
"notes": "来源:2026-10-23 第二次冷启动审计应修 #1"
},
{
"id": "f02",
"title": "check 链描述同步:AGENTS.md 与 ITERATION.md 补 verify 第六步",
"motivation": "AGENTS.md:10 与 ITERATION.md:31 写 5 步,package.json 实为 6 步(含 verify),文档与实际门禁不一致",
"acceptance": "两处描述改为 build → style → typecheck → smoke → index → verify 六步;npm run check 全绿",
"status": "done",
"notes": "来源:审计应修 #2"
},
{
"id": "f03",
"title": "新增 ua lint-iterations 门禁:留痕格式机器校验",
"motivation": "iterations.log 41 行中 36 行违规(time 字段混用、UTC 时间戳、缺 commit),靠人工反复修补,规范缺机器校验",
"acceptance": "ua 新增 lint-iterations 子命令:校验每行 JSON 字段恰为 {ts,id,result,commit,summary}、ts 为本地 +08:00 ISO 且可用 Date 解析、commit 为 7-40 位哈希或 aborted 时允许为空、result 枚举 done/aborted;并入 npm run check;历史 36 条违规行按规范修复(ts 以对应 git 提交的 author date 本地时区回填,commit 补哈希);npm run check 全绿",
"status": "done",
"notes": "来源:审计应修 #3;历史行修复用 git log --format=%cI 按条目 id 关联提交时间回填"
},
{
"id": "f04",
"title": "commit-lint 收严:chore/docs 等强制 scope,杜绝无 scope 放行",
"motivation": "COMMITTING.md 规定 <type>(<scope>) 格式,但 chore:、docs: 无 scope 被 lint 放行,历史还有 17 条 iter: 缺 scope",
"acceptance": "commit-lint 规则收严为:除 Merge 外所有 type 必须带 scope(scope 允许 cli/skin/core/backlog/留痕等枚举或条目 id);反向用例 chore: xxx / docs: xxx / iter: xxx 必须被拒;正例全部通过;npm run check 全绿",
"status": "done",
"notes": "来源:审计应修 #4;历史 iter 缺 scope 的提交不回改(已发布历史不动),只管增量"
},
{
"id": "f05",
"title": "ua index 幂等化:generatedAt 仅内容变化时更新",
"motivation": "ua index 每次重写 .ua/index.json 的 generatedAt,npm run check 必然弄脏工作区,与「提交前 check」工作流冲突",
"acceptance": "index 命令改为:若除 generatedAt 外内容与现有文件一致则不写入(或 generatedAt 改为内容哈希派生);连续两次 npm run check 后 git status 必须干净",
"status": "done",
"notes": "来源:审计应修 #5"
},
{
"id": "f06",
"title": "pnpm-lock.yaml 入库 + 目录树补全",
"motivation": "lockfile 缺失导致依赖树不可复现;ARCHITECTURE.md 目录树缺 scripts/、COMMITTING.md、ITERATION.md、.ua/ 清单等条目,新代理接手有盲区",
"acceptance": "pnpm-lock.yaml 提交入库;ARCHITECTURE.md 目录树补 scripts/(gitea.mjs/gitea-api.mjs 一句话说明)、COMMITTING.md、ITERATION.md、.ua/backlog.json、.ua/iterations.log、pnpm-workspace.yaml;npm run check 全绿",
"status": "done",
"notes": "来源:审计应修 #6 + 建议 #7"
},
{
"id": "g01",
"title": "ua docs 幂等化:llms-full.txt 时间戳不弄脏工作区",
"motivation": "llms-full.txt 头部含 generatedAt,每次 ua docs 重写必然弄脏工作区,与 index 幂等化(f05)同类问题",
"acceptance": "内容与现有文件仅时间戳之差时不写入;npm run docs 连跑两次 git status 干净",
"status": "done",
"notes": "自补:延续 f05 方案"
},
{
"id": "g02",
"title": "新增 image 图片组件(缩略图 + 点击预览)",
"motivation": "皮肤包缺常规媒体组件,img 缩略图 + 遮罩预览是常规表单/详情页刚需",
"acceptance": "image({src, alt, width?}) 返回缩略图,点击开遮罩大图(Esc/点击遮罩关闭,reduced-motion 兜底);manifest/docs/llms-full/README/demo 同步;npm run check 全绿",
"status": "done",
"notes": "自补:组件补充方向"
},
{
"id": "g03",
"title": "textField/textarea/inputNumber 补 prefix/suffix 附加内容槽",
"motivation": "搜索框放大镜、金额单位等前后缀是输入组件高频需求,目前只能外部拼装",
"acceptance": "三个输入组件新增 prefix/suffix(string|HTMLElement),渲染为输入框内侧前后附加块;manifest 参数表与 llms-full 同步;npm run check 全绿",
"status": "done",
"notes": "自补:可用性细节方向"
},
{
"id": "g04",
"title": "dataTable 筛选框用 prefix 放搜索图标",
"motivation": "g03 落地了 prefix 附加块,dataTable 工具栏筛选框是最佳实战落点,同时验证包装不影响事件冒泡",
"acceptance": "dataTable 筛选输入框带 🔍 prefix;筛选功能回归正常(smoke 不变绿转红);npm run check 全绿",
"status": "done",
"notes": "自补:g03 实战验证"
},
{
"id": "g05",
"title": "ua verify 增加 llms-full.txt 生成同步校验",
"motivation": "llms-full.txt 是手工触发 ua docs 生成的产物,改 manifest 后忘记跑 docs 会产生文档漂移,目前无门禁",
"acceptance": "verify 内联重渲染 Agent 文档(时间戳归一化)与根目录 llms-full.txt 比对,不一致即报错提示 npm run docs;npm run check 全绿",
"status": "done",
"notes": "自补:工具链防漂移方向"
},
{
"id": "g06",
"title": "组件数门禁扩展 README 计数文本",
"motivation": "f01 的计数漂移校验只覆盖 AGENTS/ARCHITECTURE,README 的「N 个 DOM 组件」文本仍可漂移",
"acceptance": "checkCountDocs 扫描列表加入 README.md,漂移即 verify 失败;npm run check 全绿",
"status": "done",
"notes": "自补:f01 收尾"
} }
] ]
} }
+253 -8
View File
@@ -1,6 +1,6 @@
{ {
"$schema": "ui-arch-registry-index/0.1", "$schema": "ui-arch-registry-index/0.1",
"generatedAt": "2026-10-23T09:12:34.151Z", "generatedAt": "2026-10-23T11:03:12.689Z",
"repoRoot": ".", "repoRoot": ".",
"packages": [ "packages": [
{ {
@@ -130,7 +130,7 @@
{ {
"name": "@ui-arch/ui", "name": "@ui-arch/ui",
"type": "skin-dom", "type": "skin-dom",
"description": "DOM 皮肤组件库:button / textField / textarea / slider / checkbox / switch / select / menu / radioGroup / accordion / breadcrumb / card / badge / tag / avatar / alert / toast / tooltip / popover / skeleton / spinner / progress / tabs / pagination / modal / drawer / form / formItem / dataTable,构建在 @ui-arch/frame 与 @ui-arch/table-core 之上,样式只消费 --ua-* 令牌。", "description": "DOM 皮肤组件库:button / textField / textarea / slider / checkbox / switch / select / menu / radioGroup / accordion / breadcrumb / card / badge / image / tag / avatar / alert / toast / tooltip / popover / skeleton / spinner / progress / tabs / pagination / modal / drawer / form / formItem / dataTable,构建在 @ui-arch/frame 与 @ui-arch/table-core 之上,样式只消费 --ua-* 令牌。",
"tags": [ "tags": [
"components", "components",
"dom", "dom",
@@ -143,8 +143,9 @@
"entry": "src/index.ts", "entry": "src/index.ts",
"exports": [ "exports": [
"button(props):按钮,variant = primary | ghost | danger,disabled 支持布尔或信号禁用", "button(props):按钮,variant = primary | ghost | danger,disabled 支持布尔或信号禁用",
"textField(props):文本输入,value 信号双向绑定,disabled 支持布尔或信号禁用", "textField(props):文本输入,value 信号双向绑定,disabled 支持布尔或信号,onInput 输入回调;prefix/suffix 可选渲染为输入框内侧附加块",
"textarea(props):多行输入,value 信号双向绑定,disabled 支持布尔或信号禁用", "inputNumber(props):数字输入,−/+ 按钮与键盘 ↑↓ 调节,min/max/step 夹取;prefix/suffix 可选渲染为整组前后附加块",
"textarea(props):多行输入,value 信号双向绑定,disabled 支持布尔或信号,rows 控行数;prefix/suffix 可选渲染为上下附加块",
"slider(props):滑块,value 信号双向绑定,min/max/step 可配,原生 range 承载键盘交互,右侧可显示当前值", "slider(props):滑块,value 信号双向绑定,min/max/step 可配,原生 range 承载键盘交互,右侧可显示当前值",
"checkbox(props):复选框,checked 信号双向绑定,disabled 支持布尔或信号禁用", "checkbox(props):复选框,checked 信号双向绑定,disabled 支持布尔或信号禁用",
"switchButton(props):开关,checked 信号双向绑定,disabled 支持布尔或信号禁用", "switchButton(props):开关,checked 信号双向绑定,disabled 支持布尔或信号禁用",
@@ -166,11 +167,16 @@
"spinner(props):加载指示器,size 控制直径", "spinner(props):加载指示器,size 控制直径",
"progress(props):进度条,value 信号驱动 0-100(满格切 success 色);不传 value 退化为不确定态无限往复", "progress(props):进度条,value 信号驱动 0-100(满格切 success 色);不传 value 退化为不确定态无限往复",
"tabs(props):标签页,active 信号双向绑定,滑动指示条 + 方向键环形导航,支持禁用页签", "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 提供底部操作区", "modal(props, ...children):弹窗,open 信号驱动显隐,Esc / 遮罩 / × 关闭,size 三档宽度,footer 提供底部操作区",
"drawer(props, ...children):抽屉,open 信号驱动侧滑面板,placement 左右可配、width 可调、footer 操作区,遮罩 / × / Esc 关闭", "drawer(props, ...children):抽屉,open 信号驱动侧滑面板,placement 左右可配、width 可调、footer 操作区,遮罩 / × / Esc 关闭",
"form(props, items):表单容器,布局 + 聚合校验,全部通过才触发 onSubmit", "form(props, items):表单容器,布局 + 聚合校验,全部通过才触发 onSubmit",
"formItem(props, control):表单字段,label + 必填红星 + 规则校验 + 错误区,返回 { el, validate } 句柄", "formItem(props, control):表单字段,label + 必填红星 + 规则校验 + 错误区,返回 { el, validate } 句柄",
"dataTable(props):数据表格,粘性表头 / 行选择(全选三态)/ 排序 / 筛选 / 分页 / 空状态;loading 为真时数据行换骨架行且筛选/翻页禁用,内核为 table-core" "dataTable(props):数据表格,粘性表头 / 行选择(全选三态)/ 排序 / 筛选 / 分页 / 空状态;loading 为真时数据行换骨架行且筛选/翻页禁用,内核为 table-core",
"image(props):图片,缩略图点击 / 回车开遮罩大图预览(Esc 或点击遮罩关闭),width 控制缩略宽"
], ],
"keyTypes": [ "keyTypes": [
"ButtonProps", "ButtonProps",
@@ -227,6 +233,37 @@
"读写状态:name() 读、name('张三') 写,界面自动同步,无需手动操作 DOM" "读写状态:name() 读、name('张三') 写,界面自动同步,无需手动操作 DOM"
], ],
"components": [ "components": [
{
"id": "image",
"title": "图片 Image",
"demo": "sec-image",
"props": [
{
"n": "src",
"t": "string",
"r": 1,
"d": "图片地址(缩略图与大图共用)"
},
{
"n": "alt",
"t": "string",
"d": "替代文本;缺省空串",
"df": "''"
},
{
"n": "width",
"t": "number",
"d": "缩略图宽度像素,高度自适应;缺省走样式默认"
}
],
"example": "image({ src: '/a.png', alt: '架构图', width: 160 })",
"steps": [
"基础用法只传 src:渲染缩略图,hover 上浮提亮,光标变放大镜",
"点击缩略图(或聚焦后回车)打开全屏遮罩大图,Esc 或点击遮罩任意处关闭,焦点归还缩略图",
"宽度用 width(像素)控制,高度按比例自适应,容器内不撑破 max-width",
"alt 尽量传有意义的描述,读屏与图片加载失败时都依赖它"
]
},
{ {
"id": "button", "id": "button",
"title": "按钮 Button", "title": "按钮 Button",
@@ -288,13 +325,81 @@
"t": "(value: string) => void", "t": "(value: string) => void",
"k": 1, "k": 1,
"d": "输入回调,每次按键触发" "d": "输入回调,每次按键触发"
},
{
"n": "prefix",
"t": "string | HTMLElement",
"d": "前置附加块(textField/inputNumber 在输入框内侧前、textarea 在上方);缺省不渲染"
},
{
"n": "suffix",
"t": "string | HTMLElement",
"d": "后置附加块(textField/inputNumber 在输入框内侧后、textarea 在下方);缺省不渲染"
} }
], ],
"example": "const name = signal(''); const el = textField({ value: name, placeholder: '输入名字…', onInput: (v) => console.log(v) })", "example": "const name = signal(''); const el = textField({ value: name, placeholder: '输入名字…', onInput: (v) => console.log(v) })",
"steps": [ "steps": [
"先用 signal('') 创建字符串信号", "先用 signal('') 创建字符串信号",
"传入 value 完成双向绑定:用户输入自动写回,外部赋值自动回显", "传入 value 完成双向绑定:用户输入自动写回,外部赋值自动回显",
"在任意响应式位置用 name() 读取最新值,如 () => `你好,${name()}`" "在任意响应式位置用 name() 读取最新值,如 () => `你好,${name()}`",
"需要图标、货币符、单位等前后缀时传 prefix/suffix(字符串或 DOM),控件会包进带边框的附加组,聚焦光环由组统一承载"
]
},
{
"id": "inputNumber",
"title": "数字输入 InputNumber",
"demo": "sec-inputnumber",
"props": [
{
"n": "value",
"t": "Signal<number>",
"r": 1,
"d": "当前值信号,双向绑定(按钮/键盘写回、外部赋值同步)"
},
{
"n": "min",
"t": "number",
"d": "最小值;缺省无下限"
},
{
"n": "max",
"t": "number",
"d": "最大值;缺省无上限"
},
{
"n": "step",
"t": "number",
"d": "步长;缺省 1",
"df": "1"
},
{
"n": "disabled",
"t": "boolean | Signal<boolean>",
"d": "禁用态:布尔直接置位,传信号则动态同步"
},
{
"n": "onChange",
"t": "(value: number) => void",
"k": 1,
"d": "值变化回调(已夹取到合法区间)"
},
{
"n": "prefix",
"t": "string | HTMLElement",
"d": "前置附加块(textField/inputNumber 在输入框内侧前、textarea 在上方);缺省不渲染"
},
{
"n": "suffix",
"t": "string | HTMLElement",
"d": "后置附加块(textField/inputNumber 在输入框内侧后、textarea 在下方);缺省不渲染"
}
],
"example": "const qty = signal(1); const el = inputNumber({ value: qty, min: 1, max: 99, onChange: (v) => console.log(v) })",
"steps": [
"用 signal(1) 创建数字信号并传入 value 完成双向绑定",
"点击 −/+ 按钮或在输入框聚焦时按 ↑↓ 以 step 调节;直接输入在失焦或回车时解析",
"非法输入自动回滚为当前值;min/max 超界自动夹取,disabled 支持布尔或信号禁用",
"需要图标、货币符、单位等前后缀时传 prefix/suffix(字符串或 DOM),控件会包进带边框的附加组,聚焦光环由组统一承载"
] ]
}, },
{ {
@@ -328,13 +433,24 @@
"t": "(value: string) => void", "t": "(value: string) => void",
"k": 1, "k": 1,
"d": "输入回调" "d": "输入回调"
},
{
"n": "prefix",
"t": "string | HTMLElement",
"d": "前置附加块(textField/inputNumber 在输入框内侧前、textarea 在上方);缺省不渲染"
},
{
"n": "suffix",
"t": "string | HTMLElement",
"d": "后置附加块(textField/inputNumber 在输入框内侧后、textarea 在下方);缺省不渲染"
} }
], ],
"example": "const bio = signal(''); textarea({ value: bio, rows: 4, placeholder: '一句话介绍…' })", "example": "const bio = signal(''); textarea({ value: bio, rows: 4, placeholder: '一句话介绍…' })",
"steps": [ "steps": [
"创建信号并传入 value", "创建信号并传入 value",
"用 rows 控制初始高度,用户可纵向拉伸", "用 rows 控制初始高度,用户可纵向拉伸",
"字数统计等派生逻辑直接在响应式文本里读 bio().length" "字数统计等派生逻辑直接在响应式文本里读 bio().length",
"需要图标、货币符、单位等前后缀时传 prefix/suffix(字符串或 DOM),控件会包进带边框的附加组,聚焦光环由组统一承载"
] ]
}, },
{ {
@@ -1060,6 +1176,134 @@
"键盘可访问:Tab 聚焦激活页签,←→ 在启用页签间环形移动" "键盘可访问:Tab 聚焦激活页签,←→ 在启用页签间环形移动"
] ]
}, },
{
"id": "stepper",
"title": "步骤条 Stepper",
"demo": "sec-stepper",
"props": [
{
"n": "steps",
"t": "StepperItem[]",
"r": 1,
"d": "步骤列表,每项 { label, description? }"
},
{
"n": "active",
"t": "Signal<number>",
"r": 1,
"d": "当前步索引(0 基)信号,双向绑定:回跳写回、外部赋值同步"
},
{
"n": "clickable",
"t": "boolean",
"d": "已完成步骤是否可点击回跳;缺省 false"
},
{
"n": "onChange",
"t": "(index: number) => void",
"k": 1,
"d": "步骤切换回调(点击回跳触发)"
}
],
"example": "const step = signal(1); stepper({ steps: [{ label: '下单' }, { label: '支付' }, { label: '完成' }], active: step, clickable: true })",
"steps": [
"用 signal(0) 创建当前步索引,传入 active;索引之前的步骤显示 ✓ 已完成态",
"流程推进时外部赋值即可:step(step() + 1),组件三态自动跟随",
"clickable: true 时已完成步骤可点击回跳并写回信号,配合 onChange 同步业务状态"
]
},
{
"id": "rate",
"title": "评分 Rate",
"demo": "sec-rate",
"props": [
{
"n": "value",
"t": "Signal<number>",
"r": 1,
"d": "当前评分(1 基整数值)信号,双向绑定:点选/键盘写回、外部赋值同步"
},
{
"n": "max",
"t": "number",
"d": "星标总数;缺省 5",
"df": "5"
},
{
"n": "disabled",
"t": "boolean | Signal<boolean>",
"d": "禁用态:布尔直接置位,传信号则动态同步"
},
{
"n": "onChange",
"t": "(value: number) => void",
"k": 1,
"d": "评分变化回调"
}
],
"example": "const score = signal(0); rate({ value: score, max: 5, onChange: (v) => console.log(v) })",
"steps": [
"用 signal(0) 创建评分信号并传入 value;0 表示未评分",
"点击星标或聚焦后按 →/↑ 加一分、←/↓ 减一分;悬停高亮预览、移出还原",
"max 可调整星标总数;disabled 支持布尔或信号禁用"
]
},
{
"id": "empty",
"title": "空状态 Empty",
"demo": "sec-empty",
"props": [
{
"n": "title",
"t": "string",
"d": "主标题;缺省不渲染"
},
{
"n": "description",
"t": "string",
"d": "补充说明;缺省不渲染"
},
{
"n": "action",
"t": "unknown",
"d": "操作区内容(常见为按钮);缺省不渲染"
}
],
"example": "empty({ title: '暂无数据', description: '调整筛选条件后再试', action: button({ label: '重置筛选', variant: 'ghost' }) })",
"steps": [
"在列表 / 表格 / 筛选结果为空时渲染 empty 占位",
"title 一句话说明状态,description 补充原因或建议",
"action 传入 DOM(如 ghost 按钮)引导用户下一步操作"
]
},
{
"id": "divider",
"title": "分隔线 Divider",
"demo": "sec-divider",
"props": [
{
"n": "label",
"t": "string",
"d": "居中文案标签;缺省纯线条"
},
{
"n": "vertical",
"t": "boolean",
"d": "是否垂直分隔线(用于行内工具栏);缺省水平"
},
{
"n": "dashed",
"t": "boolean",
"d": "是否虚线;缺省实线"
}
],
"example": "divider({ label: '更多信息' }) / divider({ vertical: true })",
"steps": [
"在区块之间插入 divider 划分内容层次",
"传 label 在水平线上显示居中文案,如「更多信息」",
"工具栏内用 vertical: true 的行内竖线,dashed: true 切虚线弱化分隔感"
]
},
{ {
"id": "pagination", "id": "pagination",
"title": "分页器 Pagination", "title": "分页器 Pagination",
@@ -1361,6 +1605,7 @@
"ua docs:生成人类文档站 apps/docs/index.html + demo.html 与 Agent 全量文档 llms-full.txt", "ua docs:生成人类文档站 apps/docs/index.html + demo.html 与 Agent 全量文档 llms-full.txt",
"ua style:注释规范门禁(导出必须 /*** ***/、接口字段行尾注释、行注释中文),违规退出码 1", "ua style:注释规范门禁(导出必须 /*** ***/、接口字段行尾注释、行注释中文),违规退出码 1",
"ua verify:清单一致性门禁(id 唯一 / exports 对应 / props 必带 n t d / steps≥3 / demo 锚点存在)", "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" "ua mcp:零依赖 stdio MCP server,暴露 list_components / get_component / search_components"
], ],
"keyTypes": [ "keyTypes": [
@@ -1368,7 +1613,7 @@
"MCP tool 入参 schema" "MCP tool 入参 schema"
] ]
}, },
"usage": "node packages/tools/cli/bin/ua.js <index|list|info <name>|docs|style|verify|mcp>;仓库内等价于 npm run index / docs / style / verify", "usage": "node packages/tools/cli/bin/ua.js <index|list|info <name>|docs|style|verify|commit-lint|mcp>;仓库内等价于 npm run index / docs / style / verify",
"conventions": [ "conventions": [
"零依赖:只用 node 标准库,Node >= 18", "零依赖:只用 node 标准库,Node >= 18",
"清单是单一来源:代码注释、文档站、llms-full.txt、MCP 全部从 ua.manifest.json 派生", "清单是单一来源:代码注释、文档站、llms-full.txt、MCP 全部从 ua.manifest.json 派生",
+53 -36
View File
@@ -1,36 +1,53 @@
{"time":"2026-10-23T15:47:00+08:00","id":"b01","result":"done","summary":"新增 tooltip 悬浮提示组件(包裹式 API / 150ms 延迟 / Esc 收起 / reduced-motion 兜底),同步 manifest + llms-full.txt + demo 区块,并修复 ua style 对 URL 中 // 的误判;npm run check 全绿"} {"ts": "2026-10-23T15:34:23+08:00", "id": "b01", "result": "done", "commit": "a57577a", "summary": "新增 tooltip 悬浮提示组件(包裹式 API / 150ms 延迟 / Esc 收起 / reduced-motion 兜底),同步 manifest + llms-full.txt + demo 区块,并修复 ua style 对 URL 中 // 的误判;npm run check 全绿"}
{"time":"2026-10-23T15:50:03+08:00","id":"b02","result":"done","summary":"新增 toast 消息提示组件:四语义工厂 + showToast,右上角堆叠,默认 3s 自动消失,微动画 + reduced-motion 兜底;manifest/docs/llms 同步,check 全绿"} {"ts": "2026-10-23T15:50:20+08:00", "id": "b02", "result": "done", "commit": "ece38bc", "summary": "新增 toast 消息提示组件:四语义工厂 + showToast,右上角堆叠,默认 3s 自动消失,微动画 + reduced-motion 兜底;manifest/docs/llms 同步,check 全绿"}
{"time":"2026-10-23T15:53:33+08:00","id":"b03","result":"done","summary":"新增 skeleton 骨架屏组件:text/rect/circle 三形状 + 扫光微动画 + reduced-motion 静态化;manifest/docs/llms/demo 同步,check 全绿"} {"ts": "2026-10-23T15:53:34+08:00", "id": "b03", "result": "done", "commit": "2882057", "summary": "新增 skeleton 骨架屏组件:text/rect/circle 三形状 + 扫光微动画 + reduced-motion 静态化;manifest/docs/llms/demo 同步,check 全绿"}
{"time":"2026-10-23T16:00:11+08:00","id":"b04","result":"done","summary":"新增独立 pagination 分页器(page 信号双向绑定 + 省略号折叠 + 键盘可达),dataTable 新增 page 受控桥接实现联动;demo 联动卡片,check 全绿"} {"ts": "2026-10-23T16:00:13+08:00", "id": "b04", "result": "done", "commit": "8c9a63b", "summary": "新增独立 pagination 分页器(page 信号双向绑定 + 省略号折叠 + 键盘可达),dataTable 新增 page 受控桥接实现联动;demo 联动卡片,check 全绿"}
{"time":"2026-10-23T16:03:46+08:00","id":"b05","result":"done","summary":"文档站核查确认已 manifest 驱动;补齐默认值信息量缺口:CLI propsTable 新增默认值列(df 字段)+ llms-full 默认标注,16 处默认值提取;check 全绿"} {"ts": "2026-10-23T16:03:47+08:00", "id": "b05", "result": "done", "commit": "83bc1e2", "summary": "文档站核查确认已 manifest 驱动;补齐默认值信息量缺口:CLI propsTable 新增默认值列(df 字段)+ llms-full 默认标注,16 处默认值提取;check 全绿"}
{"time":"2026-10-23T16:08:17+08:00","id":"c01","result":"done","summary":"button/checkbox/switchButton 补 disabled(boolean|Signal),复用 h() 布尔/信号既有语义零辅助代码;CSS 通用禁用态 + demo 信号联动示例;check 全绿"} {"ts": "2026-10-23T16:08:19+08:00", "id": "c01", "result": "done", "commit": "2a5892a", "summary": "button/checkbox/switchButton 补 disabled(boolean|Signal),复用 h() 布尔/信号既有语义零辅助代码;CSS 通用禁用态 + demo 信号联动示例;check 全绿"}
{"time":"2026-10-23T16:10:31+08:00","id":"c02","result":"done","summary":"textField/textarea 补 disabled(boolean|Signal),与 c01 同模式;禁用态 CSS 覆盖输入类;demo locked 信号联动;check 全绿"} {"ts": "2026-10-23T16:10:32+08:00", "id": "c02", "result": "done", "commit": "f9324dd", "summary": "textField/textarea 补 disabled(boolean|Signal),与 c01 同模式;禁用态 CSS 覆盖输入类;demo locked 信号联动;check 全绿"}
{"time": "2026-10-23T16:55:00+08:00", "id": "d01", "result": "done", "summary": "新增下拉菜单 menu:触发器 + 自绘弹层,disabled/danger/divider 条目,↑↓ 环形导航 + Esc 回焦,reduced-motion 兜底;manifest/llms-full/demo 同步,check 全绿"} {"ts": "2026-10-23T16:46:00+08:00", "id": "d01", "result": "done", "commit": "84277a7", "summary": "新增下拉菜单 menu:触发器 + 自绘弹层,disabled/danger/divider 条目,↑↓ 环形导航 + Esc 回焦,reduced-motion 兜底;manifest/llms-full/demo 同步,check 全绿"}
{"time": "2026-10-23T17:02:00+08:00", "id": "d02", "result": "done", "summary": "select/radioGroup 补 disabled(布尔|信号),并修复 select 根节点缺 ua-open 导致箭头不旋转的既有缺陷;CSS :has() 行级弱化;demo/manifest/llms 同步,check 全绿"} {"ts": "2026-10-23T16:47:03+08:00", "id": "d02", "result": "done", "commit": "6b9fa95", "summary": "select/radioGroup 补 disabled(布尔|信号),并修复 select 根节点缺 ua-open 导致箭头不旋转的既有缺陷;CSS :has() 行级弱化;demo/manifest/llms 同步,check 全绿"}
{"time": "2026-10-23T16:47:56+08:00", "id": "d03", "result": "done", "summary": "新增折叠面板 accordion:单开/多开 + active 受控 + 禁用面板,grid 行高 0fr→1fr 过渡免测量,aria 关联完整;manifest/llms/demo 同步,check 全绿"} {"ts": "2026-10-23T16:47:56+08:00", "id": "d03", "result": "done", "commit": "df2fb34", "summary": "新增折叠面板 accordion:单开/多开 + active 受控 + 禁用面板,grid 行高 0fr→1fr 过渡免测量,aria 关联完整;manifest/llms/demo 同步,check 全绿"}
{"time": "2026-10-23T16:48:36+08:00", "id": "d04", "result": "done", "summary": "dataTable 新增 loading(布尔|信号):表体换骨架行、筛选与翻页禁用、aria-busy;demo 加开关,manifest/llms 同步,check 全绿"} {"ts": "2026-10-23T16:48:36+08:00", "id": "d04", "result": "done", "commit": "ccbdcb9", "summary": "dataTable 新增 loading(布尔|信号):表体换骨架行、筛选与翻页禁用、aria-busy;demo 加开关,manifest/llms 同步,check 全绿"}
{"time": "2026-10-23T16:49:45+08:00", "id": "d05", "result": "done", "summary": "新增面包屑 breadcrumb:nav/ol 语义结构 + 末项 aria-current,href 走 <a>、无 href 走 onNavigate,分隔符可配;manifest/llms/demo 同步,check 全绿(中途被 style 门禁拦下多行注释,已改单行)"} {"ts": "2026-10-23T16:49:45+08:00", "id": "d05", "result": "done", "commit": "60c0524", "summary": "新增面包屑 breadcrumb:nav/ol 语义结构 + 末项 aria-current,href 走 <a>、无 href 走 onNavigate,分隔符可配;manifest/llms/demo 同步,check 全绿(中途被 style 门禁拦下多行注释,已改单行)"}
{"time": "2026-10-23T16:50:49+08:00", "id": "d06", "result": "done", "summary": "新增 ua verify 门禁:清单 id/exports/props/steps/example 与 demo 锚点一致性,纳入 npm run check;顺带补齐 badge/alert/spinner 手把手第三条,check 全绿"} {"ts": "2026-10-23T16:50:49+08:00", "id": "d06", "result": "done", "commit": "582318a", "summary": "新增 ua verify 门禁:清单 id/exports/props/steps/example 与 demo 锚点一致性,纳入 npm run check;顺带补齐 badge/alert/spinner 手把手第三条,check 全绿"}
{"time": "2026-10-23T16:51:48+08:00", "id": "d07", "result": "done", "summary": "新增可交互气泡卡片 popover:点击/hover 触发、点外部与 Esc 收起、内容可交互且内部点击不误关;manifest/llms/demo 同步,check 全绿"} {"ts": "2026-10-23T16:51:48+08:00", "id": "d07", "result": "done", "commit": "fb9ab88", "summary": "新增可交互气泡卡片 popover:点击/hover 触发、点外部与 Esc 收起、内容可交互且内部点击不误关;manifest/llms/demo 同步,check 全绿"}
{"time": "2026-10-23T16:52:28+08:00", "id": "d08", "result": "done", "summary": "演示页改双栏 shell:sticky 侧边目录由 .sec 自动反推 + IntersectionObserver 滚动高亮,窄屏隐藏、reduced-motion 关平滑滚动;check 全绿"} {"ts": "2026-10-23T16:52:28+08:00", "id": "d08", "result": "done", "commit": "1a4b2c1", "summary": "演示页改双栏 shell:sticky 侧边目录由 .sec 自动反推 + IntersectionObserver 滚动高亮,窄屏隐藏、reduced-motion 关平滑滚动;check 全绿"}
{"time": "2026-10-23T16:53:06+08:00", "id": "d09", "result": "done", "summary": "新增滑块 slider:value 信号双向绑定 + min/max/step + 布尔|信号禁用 + showValue,原生 range 承载键盘与触屏,accent-color 令牌化;manifest/llms/demo 同步,check 全绿"} {"ts": "2026-10-23T16:53:06+08:00", "id": "d09", "result": "done", "commit": "a11157b", "summary": "新增滑块 slider:value 信号双向绑定 + min/max/step + 布尔|信号禁用 + showValue,原生 range 承载键盘与触屏,accent-color 令牌化;manifest/llms/demo 同步,check 全绿"}
{"time": "2026-10-23T16:53:44+08:00", "id": "d10", "result": "done", "summary": "toast 新增 closable(× 手动关闭)与同屏上限 4 条,退场统一走 dismiss() 双兜底;is-closable 才接收指针;demo/manifest/llms 同步,check 全绿"} {"ts": "2026-10-23T16:53:44+08:00", "id": "d10", "result": "done", "commit": "a08f157", "summary": "toast 新增 closable(× 手动关闭)与同屏上限 4 条,退场统一走 dismiss() 双兜底;is-closable 才接收指针;demo/manifest/llms 同步,check 全绿"}
{"time": "2026-10-23T16:54:36+08:00", "id": "d11", "result": "done", "summary": "modal 新增 size 三档(sm/md/lg)与 footer 操作区(右对齐 + 上分割线),缺省行为不变;demo/manifest/llms 同步,check 全绿"} {"ts": "2026-10-23T16:54:36+08:00", "id": "d11", "result": "done", "commit": "d1a2864", "summary": "modal 新增 size 三档(sm/md/lg)与 footer 操作区(右对齐 + 上分割线),缺省行为不变;demo/manifest/llms 同步,check 全绿"}
{"time": "2026-10-23T16:55:10+08:00", "id": "d12", "result": "done", "summary": "新增可关闭标签 tag(五档语义色 + closable/onClose,无状态委托调用方重渲染);demo 用 renderList 演示真实删除;manifest/llms 同步,check 全绿"} {"ts": "2026-10-23T16:55:10+08:00", "id": "d12", "result": "done", "commit": "9eefdcb", "summary": "新增可关闭标签 tag(五档语义色 + closable/onClose,无状态委托调用方重渲染);demo 用 renderList 演示真实删除;manifest/llms 同步,check 全绿"}
{"time": "2026-10-23T16:55:55+08:00", "id": "d13", "result": "done", "summary": "dataTable 列定义新增皮肤层 TableColumn(align + format),内核 ColumnDef 不变;表头/单元格共用对齐类,format 走 textContent;demo/manifest/llms 同步,check 全绿"} {"ts": "2026-10-23T16:55:55+08:00", "id": "d13", "result": "done", "commit": "730ee1a", "summary": "dataTable 列定义新增皮肤层 TableColumn(align + format),内核 ColumnDef 不变;表头/单元格共用对齐类,format 走 textContent;demo/manifest/llms 同步,check 全绿"}
{"time": "2026-10-23T16:56:36+08:00", "id": "d14", "result": "done", "summary": "新增抽屉 drawer:open 信号驱动侧滑、placement/width 可配、footer 操作区、遮罩/×/Esc 关闭并聚焦首元素;demo/manifest/llms 同步,check 全绿"} {"ts": "2026-10-23T16:56:36+08:00", "id": "d14", "result": "done", "commit": "d3fc754", "summary": "新增抽屉 drawer:open 信号驱动侧滑、placement/width 可配、footer 操作区、遮罩/×/Esc 关闭并聚焦首元素;demo/manifest/llms 同步,check 全绿"}
{"time": "2026-10-23T16:57:22+08:00", "id": "d15", "result": "done", "summary": "CLI 包补 ua.manifest.json(type: tooling,七个子命令 + 约定),注册表 5 包;修正 AGENTS/ARCHITECTURE 过期组件数 17→27;check 全绿"} {"ts": "2026-10-23T16:57:22+08:00", "id": "d15", "result": "done", "commit": "1c03c81", "summary": "CLI 包补 ua.manifest.json(type: tooling,七个子命令 + 约定),注册表 5 包;修正 AGENTS/ARCHITECTURE 过期组件数 17→27;check 全绿"}
{"time": "2026-10-23T16:58:51+08:00", "id": "d16", "result": "done", "summary": "dataTable 消费内核列宽 width:colgroup 固定像素 / flex 自适应,选择列 44px;demo 三列演示宽档;manifest/llms 同步,check 全绿"} {"ts": "2026-10-23T16:58:52+08:00", "id": "d16", "result": "done", "commit": "fa168b9", "summary": "dataTable 消费内核列宽 width:colgroup 固定像素 / flex 自适应,选择列 44px;demo 三列演示宽档;manifest/llms 同步,check 全绿"}
{"time": "2026-10-23T16:59:46+08:00", "id": "d17", "result": "done", "summary": "表单错误态打通包裹式控件:focusout/change 取代 blur + aria-invalid + .ua-invalid 后代选择器覆盖 select/radio/slider;demo 加必填 select;check 全绿"} {"ts": "2026-10-23T16:59:46+08:00", "id": "d17", "result": "done", "commit": "bdc1fec", "summary": "表单错误态打通包裹式控件:focusout/change 取代 blur + aria-invalid + .ua-invalid 后代选择器覆盖 select/radio/slider;demo 加必填 select;check 全绿"}
{"time": "2026-10-23T17:00:55+08:00", "id": "d18", "result": "done", "summary": "ua verify 新增皮肤硬编码颜色门禁(#hex 禁止,rgba 仅限阴影/遮罩),清掉 ui.css 5 处 #fff 改令牌;负向测试确认能拦;check 全绿"} {"ts": "2026-10-23T17:00:55+08:00", "id": "d18", "result": "done", "commit": "4012923", "summary": "ua verify 新增皮肤硬编码颜色门禁(#hex 禁止,rgba 仅限阴影/遮罩),清掉 ui.css 5 处 #fff 改令牌;负向测试确认能拦;check 全绿"}
{"time": "2026-10-23T17:01:46+08:00", "id": "d19", "result": "done", "summary": "dataTable 新增 selected 受控信号:与内核选择双向同步(内容比较 + 拷贝保护),demo 展示已选计数与清空;check 全绿"} {"ts": "2026-10-23T17:01:46+08:00", "id": "d19", "result": "done", "commit": "4555542", "summary": "dataTable 新增 selected 受控信号:与内核选择双向同步(内容比较 + 拷贝保护),demo 展示已选计数与清空;check 全绿"}
{"time": "2026-10-23T17:02:24+08:00", "id": "d20", "result": "done", "summary": "progress 支持不确定态:value 可选,缺省时 35% 填充块往复 + aria-label 进行中;定量态补 aria-valuenow;demo/manifest/llms 同步,check 全绿"} {"ts": "2026-10-23T17:02:24+08:00", "id": "d20", "result": "done", "commit": "9cfdb43", "summary": "progress 支持不确定态:value 可选,缺省时 35% 填充块往复 + aria-label 进行中;定量态补 aria-valuenow;demo/manifest/llms 同步,check 全绿"}
{"time": "2026-10-23T17:03:02+08:00", "id": "d21", "result": "done", "summary": "无障碍收尾:modal × 补 aria-label/type、spinner role=status、skeleton aria-hidden、tablist aria-label + 禁用页签 aria-disabled;conventions 记录基线;check 全绿"} {"ts": "2026-10-23T17:03:02+08:00", "id": "d21", "result": "done", "commit": "00a6104", "summary": "无障碍收尾:modal × 补 aria-label/type、spinner role=status、skeleton aria-hidden、tablist aria-label + 禁用页签 aria-disabled;conventions 记录基线;check 全绿"}
{"time": "2026-10-23T17:03:49+08:00", "id": "d22", "result": "done", "summary": "新增头像 avatar(src 图片 / 首字占位 + size + 四档语义底色),demo 独立 sec-avatar;中途被 verify 的 demo 锚点检查拦下已补;check 全绿"} {"ts": "2026-10-23T17:03:49+08:00", "id": "d22", "result": "done", "commit": "f86da98", "summary": "新增头像 avatar(src 图片 / 首字占位 + size + 四档语义底色),demo 独立 sec-avatar;中途被 verify 的 demo 锚点检查拦下已补;check 全绿"}
{"time": "2026-10-23T17:04:27+08:00", "id": "d23", "result": "done", "summary": "alert 新增 title / closable / onClose 与 role=alert,关闭走 200ms 退场后摘除;demo 独立 sec-alert;manifest/llms 同步,check 全绿"} {"ts": "2026-10-23T17:04:28+08:00", "id": "d23", "result": "done", "commit": "f361a19", "summary": "alert 新增 title / closable / onClose 与 role=alert,关闭走 200ms 退场后摘除;demo 独立 sec-alert;manifest/llms 同步,check 全绿"}
{"time": "2026-10-23T17:05:46+08:00", "id": "d24", "result": "done", "summary": "重写 README(定位/快速开始/28 组件/目录/门禁/Agent 接入/流水线),组件清单做成标记区块并由 ua verify 校验防漂移;负向测试通过,check 全绿"} {"ts": "2026-10-23T17:05:46+08:00", "id": "d24", "result": "done", "commit": "6efe3ab", "summary": "重写 README(定位/快速开始/28 组件/目录/门禁/Agent 接入/流水线),组件清单做成标记区块并由 ua verify 校验防漂移;负向测试通过,check 全绿"}
{"time": "2026-10-23T17:06:17+08:00", "id": "d25", "result": "done", "summary": "menu 条目新增 shortcut 快捷键提示(右侧 kbd 灰字,两端对齐 + 单行省略,不绑按键);demo/manifest/llms 同步,check 全绿"} {"ts": "2026-10-23T17:06:17+08:00", "id": "d25", "result": "done", "commit": "6a5e662", "summary": "menu 条目新增 shortcut 快捷键提示(右侧 kbd 灰字,两端对齐 + 单行省略,不绑按键);demo/manifest/llms 同步,check 全绿"}
{"ts": "2026-10-23T09:09:16+00:00", "round": 26, "id": "d26", "result": "done", "commit": "82f48aa", "summary": "select 键盘导航:↑↓ 环形移动、Home/End 跳转、Esc 回焦触发器"} {"ts": "2026-10-23T17:08:53+08:00", "id": "d26", "result": "done", "commit": "82f48aa", "summary": "select 键盘导航:↑↓ 环形移动、Home/End 跳转、Esc 回焦触发器"}
{"ts": "2026-10-23T09:11:42+00:00", "round": 27, "id": "d27", "result": "done", "commit": "fac74a3", "summary": "modal 焦点陷阱 + dialog 语义"} {"ts": "2026-10-23T17:10:37+08:00", "id": "d27", "result": "done", "commit": "fac74a3", "summary": "modal 焦点陷阱 + dialog 语义"}
{"ts": "2026-10-23T09:11:42+00:00", "round": 28, "id": "d28", "result": "done", "commit": "", "summary": "drawer 焦点陷阱对齐 modal"} {"ts": "2026-10-23T17:11:31+08:00", "id": "d28", "result": "done", "commit": "8e16829", "summary": "drawer 焦点陷阱对齐 modal"}
{"ts": "2026-10-23T09:12:49+00:00", "round": 29, "id": "d29", "result": "done", "commit": "", "summary": "menu Home/End 跳转对齐 select"} {"ts": "2026-10-23T17:12:43+08:00", "id": "d29", "result": "done", "commit": "f8934ba", "summary": "menu Home/End 跳转对齐 select"}
{"ts":"2026-10-23T17:50:14+08:00","id":"e01","result":"done","commit":"20d7b94","summary":"新增 inputNumber 数字输入:−/+ 按钮 + 键盘上下步进,信号双向绑定,min/max/step 夹取,门禁全绿"}
{"ts":"2026-10-23T17:57:31+08:00","id":"e02","result":"done","commit":"fc7a1e8","summary":"新增 stepper 步骤条:三态流转 + active 信号双向绑定 + clickable 回跳,门禁全绿"}
{"ts":"2026-10-23T18:03:34+08:00","id":"e03","result":"done","commit":"ee20322","summary":"新增 rate 评分组件:星标点选 + hover 预览 + 键盘方向键调节,信号双向绑定,门禁全绿"}
{"ts":"2026-10-23T18:11:46+08:00","id":"e04","result":"done","commit":"fd4ee01","summary":"新增 empty 空状态组件:title/description/action 居中占位,静态零依赖,门禁全绿"}
{"ts":"2026-10-23T18:14:46+08:00","id":"e05","result":"done","commit":"c457e0c","summary":"新增 divider 分隔线组件:水平/垂直 + 居中文案 + 虚线,门禁全绿"}
{"ts":"2026-10-23T18:42:15+08:00","id":"f01","result":"done","commit":"c19f83b","summary":"组件数漂移纳入 ua verify 门禁:AGENTS/ARCHITECTURE 的「N 个组件」与 skin-dom 清单实际数比对,修正 27→33"}
{"ts":"2026-10-23T18:44:08+08:00","id":"f02","result":"done","commit":"f2fc286","summary":"AGENTS.md 与 ITERATION.md 的 npm run check 描述补上 verify,与实际六步链一致"}
{"ts":"2026-10-23T18:46:55+08:00","id":"f03","result":"done","commit":"f57da4c","summary":"ua 新增 lint-iterations 子命令校验留痕格式并并入 check 链,36 条历史违规行按 git author date 回填修复"}
{"ts":"2026-10-23T18:48:36+08:00","id":"f04","result":"done","commit":"99ce0cd","summary":"commit-lint 收严:非 iter type 必须带 scope,裸 chore:/docs:/iter: 反向用例全部拒绝,COMMITTING.md 同步"}
{"ts":"2026-10-23T18:52:12+08:00","id":"f05","result":"done","commit":"265060f","summary":"ua index 幂等化:剔除 generatedAt 比对且行尾归一化,内容无变化跳写,连续两次 check 后工作区干净"}
{"ts":"2026-10-23T18:53:33+08:00","id":"f06","result":"done","commit":"a5439fe","summary":"pnpm-lock.yaml 经 pnpm install --lockfile-only 生成入库;ARCHITECTURE.md 目录树补 scripts/、COMMITTING、ITERATION、pnpm-workspace、.ua 明细"}
{"ts":"2026-10-23T18:55:24+08:00","id":"g01","result":"done","commit":"98f557c","summary":"writeIfChanged 支持归一化比较,llms-full.txt 仅时间戳之差时跳写,docs 双跑工作区干净;backlog 灌入 g01-g03"}
{"ts":"2026-10-23T18:59:34+08:00","id":"g02","result":"done","commit":"33979b2","summary":"新增 image 组件:缩略图点击/回车开遮罩大图,Esc/点击遮罩关闭并归还焦点;组件 33→34,manifest/docs/README/demo 同步"}
{"ts":"2026-10-23T19:04:29+08:00","id":"g03","result":"done","commit":"bfa7013","summary":"textField/textarea/inputNumber 新增 prefix/suffix(addonWrap 零侵入包装,无附加不包裹),样式聚焦光环由组承载;demo 与 manifest 同步"}
{"ts":"2026-10-23T19:05:48+08:00","id":"g04","result":"done","commit":"c0bb8d7","summary":"dataTable 筛选输入框用 g03 的 prefix 放搜索图标,事件冒泡与筛选回归正常,门禁全绿"}
{"ts":"2026-10-23T19:07:37+08:00","id":"g05","result":"done","commit":"9651c39","summary":"verify 内联重渲染 Agent 文档与 llms-full.txt 归一化比对,漂移即拦截提示 npm run docs"}
{"ts":"2026-10-23T19:08:37+08:00","id":"g06","result":"done","commit":"b6c9489","summary":"checkCountDocs 扫描列表加入 README.md,三份文档组件计数文本全部纳入漂移门禁"}
+4 -2
View File
@@ -7,7 +7,7 @@
1. **包管理器必须用 pnpm**:包间依赖用 `workspace:*` 协议,npm 不支持。安装依赖:`pnpm install`。 1. **包管理器必须用 pnpm**:包间依赖用 `workspace:*` 协议,npm 不支持。安装依赖:`pnpm install`。
2. **构建工具**:TypeScript 5(`tsc`)。若环境未全局安装,可使用托管工作区的副本:`C:/Users/shawn/.workbuddy/binaries/node/workspace/node_modules/typescript`(本仓库历史即如此构建)。 2. **构建工具**:TypeScript 5(`tsc`)。若环境未全局安装,可使用托管工作区的副本:`C:/Users/shawn/.workbuddy/binaries/node/workspace/node_modules/typescript`(本仓库历史即如此构建)。
3. **Node**:>= 18,ESM(`"type": "module"`)。 3. **Node**:>= 18,ESM(`"type": "module"`)。
4. **验证自检**:装好后先跑 `npm run check`(build → style → typecheck → smoke → index)确认基线全绿,再开始改代码。 4. **验证自检**:装好后先跑 `npm run check`(build → style → typecheck → smoke → index → verify)确认基线全绿,再开始改代码。
## 编码规范(强制) ## 编码规范(强制)
@@ -21,7 +21,7 @@
- `packages/core/table-core`(@ui-arch/table-core):Headless 表格逻辑内核 - `packages/core/table-core`(@ui-arch/table-core):Headless 表格逻辑内核
- `packages/core/frame`(@ui-arch/frame):signal + hyperscript 微框架 - `packages/core/frame`(@ui-arch/frame):signal + hyperscript 微框架
- `packages/skin/ui`(@ui-arch/ui):27 个 DOM 组件(表单 / 反馈 / 浮层 / 导航 / 表格),样式只消费令牌 - `packages/skin/ui`(@ui-arch/ui):34 个 DOM 组件(表单 / 反馈 / 浮层 / 导航 / 表格),样式只消费令牌
- `packages/design/tokens`(@ui-arch/tokens):设计令牌 - `packages/design/tokens`(@ui-arch/tokens):设计令牌
- `packages/tools/cli`(@ui-arch/cli):`ua` 命令行 + MCP 注册表服务(private) - `packages/tools/cli`(@ui-arch/cli):`ua` 命令行 + MCP 注册表服务(private)
- `apps/demo`(@ui-arch/demo):交互演示,产物 apps/demo/dist/demo.html - `apps/demo`(@ui-arch/demo):交互演示,产物 apps/demo/dist/demo.html
@@ -29,6 +29,8 @@
**门禁一条龙**:`npm run check` = build + style + typecheck + smoke + index + verify,提交前必过。`ua verify` 校验清单 ↔ 文档 ↔ 演示一致(docs.components 的 id 必须在 api.exports 有对应条目、props 必须带 n/t/d、demo 锚点必须在 `apps/demo/main.ts` 中存在),检查皮肤包样式表无硬编码颜色(`#hex` 一律禁止,必须走 `--ua-*` 令牌),并核对 README 的 `<!-- ua:components -->` 区块与清单组件集合完全一致。 **门禁一条龙**:`npm run check` = build + style + typecheck + smoke + index + verify,提交前必过。`ua verify` 校验清单 ↔ 文档 ↔ 演示一致(docs.components 的 id 必须在 api.exports 有对应条目、props 必须带 n/t/d、demo 锚点必须在 `apps/demo/main.ts` 中存在),检查皮肤包样式表无硬编码颜色(`#hex` 一律禁止,必须走 `--ua-*` 令牌),并核对 README 的 `<!-- ua:components -->` 区块与清单组件集合完全一致。
**提交信息规范**:`COMMITTING.md`(`<type>(<scope>): <中文祈使句>`),`ua commit-lint` + commit-msg 钩子自动校验,迭代提交用 `iter(<条目id>): <subject>`。
## Agent 查询组件库的三种方式 ## Agent 查询组件库的三种方式
1. **MCP(推荐)**:在 `~/.workbuddy/mcp.json` 注册: 1. **MCP(推荐)**:在 `~/.workbuddy/mcp.json` 注册:
+10 -2
View File
@@ -34,16 +34,24 @@ ui-arch-mvp/
│ │ ├── table-core/ # 表格状态内核 │ │ ├── table-core/ # 表格状态内核
│ │ └── frame/ # 信号 + hyperscript 微框架 │ │ └── frame/ # 信号 + hyperscript 微框架
│ ├── skin/ # 皮肤层(DOM 组件,可发布) │ ├── skin/ # 皮肤层(DOM 组件,可发布)
│ │ └── ui/ # 27 个组件 + ui.css │ │ └── ui/ # 34 个组件 + ui.css
│ ├── design/ # 资产层(可发布) │ ├── design/ # 资产层(可发布)
│ │ └── tokens/ # --ua-* 设计令牌 │ │ └── tokens/ # --ua-* 设计令牌
│ └── tools/ # 工程层(private,不发布) │ └── tools/ # 工程层(private,不发布)
│ └── cli/ # ua 命令行 + MCP 注册表服务 │ └── cli/ # ua 命令行 + MCP 注册表服务
├── scripts/ # 工程脚本:gitea.mjs(git 推送封装)、gitea-api.mjs(Gitea REST API)
├── ARCHITECTURE.md # 本文件:结构规范 ├── ARCHITECTURE.md # 本文件:结构规范
├── AGENTS.md # Agent 接入手册(兼项目入口文档) ├── AGENTS.md # Agent 接入手册(兼项目入口文档)
├── CODING.md # 编码规范 ├── CODING.md # 编码规范
├── COMMITTING.md # 提交信息规范(commit-lint 机器校验)
├── ITERATION.md # 无人值守自我迭代协议(自动化 Agent 执行)
├── pnpm-workspace.yaml # pnpm 工作区定义
├── pnpm-lock.yaml # 依赖锁定(入库,保证依赖树可复现)
├── llms-full.txt # Agent 全量文档(ua docs 生成) ├── llms-full.txt # Agent 全量文档(ua docs 生成)
└── .ua/index.json + llms.txt # 注册表索引(ua index 生成,提交入库) └── .ua/ # 迭代元数据
├── index.json # 注册表索引(ua index 生成,提交入库)
├── backlog.json # 迭代待办清单(open/done)
└── iterations.log # 每轮迭代留痕(lint-iterations 机器校验)
``` ```
## 包规范(每包强制) ## 包规范(每包强制)
+67
View File
@@ -0,0 +1,67 @@
# Git 提交规范(COMMITTING.md)
> 适用于所有人工提交与自动化迭代提交。`ua commit-lint` 门禁与 `.git/hooks/commit-msg` 钩子按本文校验,违规提交被拒绝。
## 格式
```
<type>(<scope>): <subject>
[body:动机与要点,每行 ≤ 80 字符]
[footer:Refs: <编号> / BREAKING CHANGE: <说明>]
```
## type 枚举
| type | 用途 |
|---|---|
| `feat` | 新功能(组件、命令、插件、门禁) |
| `fix` | 缺陷修复 |
| `docs` | 纯文档变更 |
| `style` | 格式调整(不影响语义:空白、注释格式) |
| `refactor` | 重构(既非新增也非修复) |
| `test` | 测试相关 |
| `chore` | 构建、工具、元数据、依赖 |
| `iter` | 迭代流水线产物(自动化专用,scope 必须为 backlog 条目 id) |
## scope 枚举
- 包名:`table-core` / `frame` / `ui` / `tokens` / `cli`(跨包可省略)
- 应用:`demo` / `docs`
- 元数据:`backlog` / `repo` / `protocol`
- `iter` 提交的 scope 固定为条目 id:`b02`、`c01`、`d29` …
- **scope 强制**:除 Merge/Revert 自动信息外,所有 type 必须带 scope;`ua commit-lint` 已机器校验,裸 type(`chore:` / `docs:`)一律拒绝
## subject 规则
- 中文祈使句,动词开头(「新增…」「修复…」「补充…」);
- ≤ 50 字符;
- 结尾不加句号;
- 条目 id 不写入 subject(scope 已承载,方便 `git log --grep='iter(b02)'` 检索)。
## 示例
```
iter(b02): 新增 toast 消息提示组件
feat(ui): dataTable 新增 selected 受控信号
fix(cli): 修正 style 检查对 URL 中 // 的误判
chore(backlog): 记录 d29 完成
docs: 重写 README 组件清单
```
## 反例(会被门禁拒绝)
```
iter: b02 新增 toast 消息提示组件 ← iter 缺 scope(id)
iter(b02): 文档站 toast 组件的新增。 ← 句号结尾
feat(ui): 增加 dataTable 的受控选中信号并且支持与内核选择双向同步以及已选计数展示 ← 超 50 字符
update stuff ← 非 type 枚举、非中文祈使句
```
## 与流水线的关系
- 自动化每轮迭代的首个提交:`iter(<id>): <subject>`;
- 同轮的元数据变更(backlog 置 done):`chore(backlog): 记录 <id> 完成`;
- 合并提交(qa/stable 的 ff-only)不产生 message,无需遵守;
- 门禁来源:`ua commit-lint <文件|message>`,commit-msg 钩子自动调用;git 自动的 Merge/Revert/fixup 信息跳过校验。
+4 -4
View File
@@ -7,7 +7,7 @@
调度器(每小时唤起)**不是**迭代器,只做两件事:健康检查 + 中断重启。真正的迭代是单次唤起内的**连续循环**——反复执行《每轮迭代流程》,直到 backlog 清空或时间预算耗尽,中间不停歇。 调度器(每小时唤起)**不是**迭代器,只做两件事:健康检查 + 中断重启。真正的迭代是单次唤起内的**连续循环**——反复执行《每轮迭代流程》,直到 backlog 清空或时间预算耗尽,中间不停歇。
- **并发锁**:`.ua/loop.lock`(含时间戳,不入库)。唤起时若锁存在且心跳在 15 分钟内 → 已有实例在跑,直接退出;否则写锁开始循环。每轮开始前刷新锁内时间戳作心跳。 - **并发锁**:`.ua/loop.lock`(含时间戳,不入库)。唤起时若锁存在且心跳在 15 分钟内 → 已有实例在跑,直接退出;否则写锁开始循环。每轮开始前刷新锁内时间戳作心跳。
- **时间预算**:单次唤起最多连续迭代约 50 分钟,到点在完成当前轮后停止(防止与下次唤起重叠)。 - **时间预算**:单次唤起最多连续迭代约 50 分钟,到点在完成当前轮后停止(防止与下次唤起重叠)。**预算判断必须基于客观时钟**:点火时用 `date +%s` 记录起点并写入 `.ua/loop.lock`,每轮开始时重新取 `date +%s` 与起点求差,差值 ≥ 2700 秒(45 分钟)即到点;**禁止凭感觉估算时长**——LLM 无内部时钟,自估时长系统性偏高(实测自报 40+ 分钟而实际约 30 分钟)。
- **熔断**:同一 backlog 条目连续 2 轮失败 → 停止循环,锁照常释放,等待下次唤起。 - **熔断**:同一 backlog 条目连续 2 轮失败 → 停止循环,锁照常释放,等待下次唤起。
- **重启语义**:锁心跳超过 15 分钟视为循环中断(崩溃/超时),调度器删旧锁、重新点火。 - **重启语义**:锁心跳超过 15 分钟视为循环中断(崩溃/超时),调度器删旧锁、重新点火。
@@ -28,10 +28,10 @@ qa/probe ──门禁通过──▶ qa/stable ──人工审阅──▶ main
1. **同步**:`git fetch --all --prune`,切到 `qa/stable` 并 `git pull`;从其最新提交强制重建 `qa/probe` 并切换过去。 1. **同步**:`git fetch --all --prune`,切到 `qa/stable` 并 `git pull`;从其最新提交强制重建 `qa/probe` 并切换过去。
2. **选题**:读 `.ua/backlog.json`,取第一条 `status === "open"` 的条目;若全部关闭,按《选题规则》自行补充一条新点子后继续。 2. **选题**:读 `.ua/backlog.json`,取第一条 `status === "open"` 的条目;若全部关闭,按《选题规则》自行补充一条新点子后继续。
3. **实现**:按 `CODING.md` 编码规范实现,保持低侵入,单文件 < 200 行,涉及组件需同步 manifest/docs/llms-full.txt。 3. **实现**:按 `CODING.md` 编码规范实现,保持低侵入,单文件 < 200 行,涉及组件需同步 manifest/docs/llms-full.txt。
4. **门禁**:跑 `npm run check`(build → style → typecheck → smoke → index)。 4. **门禁**:跑 `npm run check`(build → style → typecheck → smoke → index → verify)。
- **通过**:提交到 `qa/probe`(commit message 以 `iter:` 开头,注明 backlog 条目 id),推送 `qa/probe`,fast-forward 合并到 `qa/stable` 并推送;将 backlog 条目 `status` 置为 `done` 并随迭代提交。 - **通过**:提交到 `qa/probe`,提交信息按 `COMMITTING.md` 规范(`iter(<条目id>): <中文祈使句>`,元数据变更用 `chore(backlog): 记录 <id> 完成`;commit-msg 钩子自动校验),推送 `qa/probe`,fast-forward 合并到 `qa/stable` 并推送;将 backlog 条目 `status` 置为 `done` 并随迭代提交。
- **失败**:尝试修复一次;仍失败则放弃本轮——`qa/probe` 直接重置回 `qa/stable`,backlog 条目在 `notes` 里追加失败原因、`status` 保持 `open`,提交并推送该元数据变更(仅改 `.ua/backlog.json`,不碰代码)。 - **失败**:尝试修复一次;仍失败则放弃本轮——`qa/probe` 直接重置回 `qa/stable`,backlog 条目在 `notes` 里追加失败原因、`status` 保持 `open`,提交并推送该元数据变更(仅改 `.ua/backlog.json`,不碰代码)。
5. **留痕**:在 `.ua/iterations.log` 追加一行 JSON:`{时间, 条目id, 结果, 摘要}`,随本轮提交。 5. **留痕**:在 `.ua/iterations.log` 追加一行 JSON,**字段统一**:`{"ts":"本地ISO时间","id":"条目id","result":"done|aborted","commit":"短哈希","summary":"一句话"}`。硬性规范:`ts` 必须来自 `date` 命令实测(本地时间,带 +08:00 偏移,**禁止手写或用 UTC 换算**);时长结论必须来自两次时间查询之差,禁止使用「大约/接近」等估算词;并发实例格式不得自创变体。随本轮提交。
6. **边界**:不改 `main` 分支保护、不删分支、不动 `.workbuddy/`、不重写历史、不 force push 到 `qa/stable` 与 `main`。 6. **边界**:不改 `main` 分支保护、不删分支、不动 `.workbuddy/`、不重写历史、不 force push 到 `qa/stable` 与 `main`。
## 选题规则(自主补点子时) ## 选题规则(自主补点子时)
+4 -4
View File
@@ -11,13 +11,13 @@ npm run demo # 打开演示:apps/demo/dist/demo.html
npm run docs # 生成人类文档站 apps/docs/index.html + Agent 全量文档 llms-full.txt npm run docs # 生成人类文档站 apps/docs/index.html + Agent 全量文档 llms-full.txt
``` ```
## 组件(共 28 个) ## 组件(共 34 个)
<!-- ua:components --> <!-- ua:components -->
accordion · alert · avatar · badge · breadcrumb · button · card · checkbox · dataTable · drawer · form · menu · modal · pagination · popover · progress · radioGroup · select · skeleton · slider · spinner · switchButton · tabs · tag · textField · textarea · toast · tooltip accordion · alert · avatar · badge · breadcrumb · button · card · checkbox · dataTable · divider · drawer · empty · form · image · inputNumber · menu · modal · pagination · popover · progress · radioGroup · rate · select · skeleton · slider · spinner · stepper · switchButton · tabs · tag · textField · textarea · toast · tooltip
<!-- /ua:components --> <!-- /ua:components -->
分组:表单(button / textField / textarea / checkbox / switchButton / select / radioGroup / slider / form / formItem)、反馈(alert / toast / spinner / progress / skeleton / badge)、浮层(tooltip / popover / modal / drawer / menu)、导航(tabs / breadcrumb / pagination / accordion)、数据(dataTable / tag / avatar / card)。 分组:表单(button / textField / inputNumber / textarea / checkbox / switchButton / select / radioGroup / slider / rate / form / formItem)、反馈(alert / toast / spinner / progress / skeleton / badge / empty)、浮层(tooltip / popover / modal / drawer / menu)、导航(tabs / breadcrumb / pagination / accordion / stepper)、数据(dataTable / tag / avatar / card)、其他(divider)。
## 目录 ## 目录
@@ -25,7 +25,7 @@ accordion · alert · avatar · badge · breadcrumb · button · card · checkbo
|---|---| |---|---|
| `packages/core/frame` | @ui-arch/frame:signal / computed / effect + hyperscript(无虚拟 DOM) | | `packages/core/frame` | @ui-arch/frame:signal / computed / effect + hyperscript(无虚拟 DOM) |
| `packages/core/table-core` | @ui-arch/table-core:headless 表格(filter → sort → paginate 派生管线) | | `packages/core/table-core` | @ui-arch/table-core:headless 表格(filter → sort → paginate 派生管线) |
| `packages/skin/ui` | @ui-arch/ui:28 个 DOM 组件 + ui.css(只消费 `--ua-*` 令牌) | | `packages/skin/ui` | @ui-arch/ui:34 个 DOM 组件 + ui.css(只消费 `--ua-*` 令牌) |
| `packages/design/tokens` | @ui-arch/tokens:设计令牌(暗色只换值不换名) | | `packages/design/tokens` | @ui-arch/tokens:设计令牌(暗色只换值不换名) |
| `packages/tools/cli` | @ui-arch/cli:`ua` 命令行与 stdio MCP server | | `packages/tools/cli` | @ui-arch/cli:`ua` 命令行与 stdio MCP server |
| `apps/demo` | 交互演示(esbuild 打包成单文件) | | `apps/demo` | 交互演示(esbuild 打包成单文件) |
+115 -2
View File
@@ -1,4 +1,12 @@
import { h, mount, renderList, signal } from '@ui-arch/frame'; import { computed, h, mount, renderList, signal } from '@ui-arch/frame';
/*** 演示用内联 SVG 图片(data URI,离线可用) ***/
const IMG_DEMO = 'data:image/svg+xml;utf8,' + encodeURIComponent(
'<svg xmlns="http://www.w3.org/2000/svg" width="480" height="320">' +
'<rect width="480" height="320" fill="#185fa5"/>' +
'<circle cx="240" cy="160" r="90" fill="#ffffff" opacity="0.85"/>' +
'<text x="240" y="170" font-size="28" text-anchor="middle" fill="#185fa5">IMAGE DEMO</text></svg>',
);
import { import {
accordion, accordion,
alert, alert,
@@ -9,20 +17,26 @@ import {
card, card,
checkbox, checkbox,
dataTable, dataTable,
divider,
drawer, drawer,
empty,
image,
form, form,
formItem, formItem,
inputNumber,
menu, menu,
modal, modal,
pagination, pagination,
popover, popover,
progress, progress,
radioGroup, radioGroup,
rate,
select, select,
showToast, showToast,
skeleton, skeleton,
slider, slider,
spinner, spinner,
stepper,
switchButton, switchButton,
tabs, tabs,
tag, tag,
@@ -111,6 +125,20 @@ const tags = signal(['设计', '前端', '组件库']);
// —— 滑块:音量与禁用态演示 —— // —— 滑块:音量与禁用态演示 ——
const volume = signal(40); const volume = signal(40);
// —— 数字输入:购买数量演示,与禁用态实例共用同一信号 ——
const qty = signal(1);
const price = signal('9.9'); // 演示 suffix 附加块
const amount = signal(2); // 演示 inputNumber 前后缀
// —— 步骤条:下单流程演示,按钮推进、点已完成步骤回跳 ——
const stepIdx = signal(0);
// —— 评分:满意度演示,与只读实例共用同一信号 ——
const score = signal(0);
// —— 空状态:搜索无结果演示 ——
const noResult = signal(true);
// —— 受控选中:dataTable 内部勾选实时写回,外部可清空 —— // —— 受控选中:dataTable 内部勾选实时写回,外部可清空 ——
const picked = signal<RowKey[]>([]); const picked = signal<RowKey[]>([]);
@@ -143,8 +171,10 @@ const content = h(
section( section(
'sec-bind', 'sec-bind',
'双向绑定', '双向绑定',
'textField 与信号双向同步:输入即回显。', 'textField 与信号双向同步:输入即回显;prefix/suffix 渲染为输入框内侧附加块。',
textField({ placeholder: '输入名字…', value: name }), textField({ placeholder: '输入名字…', value: name }),
textField({ placeholder: '搜索…', value: name, prefix: '🔍' }),
textField({ value: price, suffix: '元' }),
h('span', { class: 'out' }, () => (name() ? `你好,${name()}!` : '等待输入…')), h('span', { class: 'out' }, () => (name() ? `你好,${name()}!` : '等待输入…')),
), ),
section( section(
@@ -232,6 +262,18 @@ const content = h(
slider({ value: volume, disabled: locked }), slider({ value: volume, disabled: locked }),
), ),
), ),
section(
'sec-inputnumber',
'数字输入',
'inputNumber:−/+ 按钮与键盘 ↑↓ 调节,value 信号双向绑定,min/max/step 超界自动夹取。',
h(
'div',
{ class: 'row' },
inputNumber({ value: qty, min: 1, max: 99, onChange: (v) => toast.info(`数量 ${v}`) }),
inputNumber({ value: qty, disabled: locked }),
inputNumber({ value: amount, min: 0, prefix: '¥', suffix: '件' }),
),
),
section( section(
'sec-feedback', 'sec-feedback',
'反馈与提示', '反馈与提示',
@@ -327,6 +369,77 @@ const content = h(
h('div', { class: 'row' }, badge({ label: 'modal', variant: 'success' }), badge({ label: 'footer', variant: 'default' })), h('div', { class: 'row' }, badge({ label: 'modal', variant: 'success' }), badge({ label: 'footer', variant: 'default' })),
), ),
), ),
section(
'sec-stepper',
'步骤条',
'stepper:done/current/upcoming 三态随 active 信号流转,clickable 时点已完成步骤回跳。',
stepper({
steps: [{ label: '确认订单', description: '收货信息与商品清单' }, { label: '支付', description: '选择支付方式' }, { label: '完成', description: '等待发货' }],
active: stepIdx,
clickable: true,
onChange: (i) => toast.info(`回到第 ${i + 1} 步`),
}),
h(
'div',
{ class: 'row' },
button({ label: '上一步', variant: 'ghost', disabled: computed(() => stepIdx() === 0), onClick: () => stepIdx(Math.max(0, stepIdx() - 1)) }),
button({ label: '下一步', disabled: computed(() => stepIdx() === 2), onClick: () => stepIdx(Math.min(2, stepIdx() + 1)) }),
),
),
section(
'sec-rate',
'评分',
'rate:点选星标打分,悬停预览移出还原,键盘 →/↑ 加分 ←/↓ 减分,disabled 只读展示。',
h(
'div',
{ class: 'row' },
rate({ value: score, onChange: (v) => toast.info(`评分 ${v} 星`) }),
h('span', { class: 'out' }, () => `当前:${score() || '未评分'}`),
),
h('div', { class: 'row' }, rate({ value: score, disabled: true })),
),
section(
'sec-image',
'图片',
'image:缩略图点击 / 回车打开遮罩大图,Esc 或点击遮罩关闭,焦点归还缩略图。',
h(
'div',
{ class: 'row' },
image({ src: IMG_DEMO, alt: '演示图一', width: 180 }),
image({ src: IMG_DEMO, alt: '演示图二', width: 120 }),
),
),
section(
'sec-empty',
'空状态',
'empty:无数据 / 无结果的统一占位,action 放引导按钮,点击按钮切回有数据态可对比。',
h('div', { class: 'row' }, h('span', { class: 'out' }, () => (noResult() ? '当前:无结果' : '当前:有数据'))),
h(
'div',
{ class: 'row' },
button({ label: '切换数据态', onClick: () => noResult(!noResult()) }),
),
() => (noResult()
? empty({ title: '暂无匹配结果', description: '试试放宽筛选条件,或清空关键词重新搜索。', action: button({ label: '重置筛选', variant: 'ghost', onClick: () => noResult(false) }) })
: card({ title: '搜索结果' }, h('p', null, '这里展示命中的数据列表。'))),
),
section(
'sec-divider',
'分隔线',
'divider:水平线可带居中文案,dashed 切虚线;vertical 行内竖线用于工具栏。',
divider({ label: '更多信息' }),
h('p', null, '分割线上下的内容各自成组。'),
divider({ dashed: true }),
h(
'div',
{ class: 'row' },
button({ label: '保存', variant: 'ghost' }),
divider({ vertical: true }),
button({ label: '另存为', variant: 'ghost' }),
divider({ vertical: true, dashed: true }),
button({ label: '导出', variant: 'ghost' }),
),
),
section( section(
'sec-menu', 'sec-menu',
'下拉菜单', '下拉菜单',
+158 -4
View File
@@ -1,6 +1,6 @@
# @ui-arch 组件库 Agent 文档(无图形版) # @ui-arch 组件库 Agent 文档(无图形版)
> 自动生成于 2026-10-23T09:06:10.925Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。 > 自动生成于 2026-10-23T11:03:12.689Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。
> 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。 > 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。
## 快速上手 ## 快速上手
@@ -50,6 +50,29 @@
28 个 DOM 组件,统一用法:组件是普通函数,传入 props 返回 DOM 节点;需要状态时先创建 signal 再双向绑定。引入 CSS:import '@ui-arch/ui/ui.css'(内部已消费 tokens)。 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 ### button — 按钮 Button
参数: 参数:
@@ -81,6 +104,8 @@ button({ label: '提交', variant: 'primary', onClick: () => console.log('clicke
- placeholder:string —— 占位文案 - placeholder:string —— 占位文案
- disabled:boolean | Signal<boolean> —— 禁用态:布尔直接置位,传信号则动态同步 - disabled:boolean | Signal<boolean> —— 禁用态:布尔直接置位,传信号则动态同步
- onInput(回调):(value: string) => void —— 输入回调,每次按键触发 - onInput(回调):(value: string) => void —— 输入回调,每次按键触发
- prefix:string | HTMLElement —— 前置附加块(textField/inputNumber 在输入框内侧前、textarea 在上方);缺省不渲染
- suffix:string | HTMLElement —— 后置附加块(textField/inputNumber 在输入框内侧后、textarea 在下方);缺省不渲染
示例: 示例:
@@ -93,9 +118,38 @@ const name = signal(''); const el = textField({ value: name, placeholder: '输
1. 先用 signal('') 创建字符串信号 1. 先用 signal('') 创建字符串信号
2. 传入 value 完成双向绑定:用户输入自动写回,外部赋值自动回显 2. 传入 value 完成双向绑定:用户输入自动写回,外部赋值自动回显
3. 在任意响应式位置用 name() 读取最新值,如 () => `你好,${name()}` 3. 在任意响应式位置用 name() 读取最新值,如 () => `你好,${name()}`
4. 需要图标、货币符、单位等前后缀时传 prefix/suffix(字符串或 DOM),控件会包进带边框的附加组,聚焦光环由组统一承载
现场演示:apps/docs/demo.html#sec-bind 现场演示: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 —— 值变化回调(已夹取到合法区间)
- prefix:string | HTMLElement —— 前置附加块(textField/inputNumber 在输入框内侧前、textarea 在上方);缺省不渲染
- suffix:string | HTMLElement —— 后置附加块(textField/inputNumber 在输入框内侧后、textarea 在下方);缺省不渲染
示例:
```
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. 需要图标、货币符、单位等前后缀时传 prefix/suffix(字符串或 DOM),控件会包进带边框的附加组,聚焦光环由组统一承载
现场演示:apps/docs/demo.html#sec-inputnumber
### textarea — 多行输入 Textarea ### textarea — 多行输入 Textarea
参数: 参数:
@@ -105,6 +159,8 @@ const name = signal(''); const el = textField({ value: name, placeholder: '输
- placeholder:string —— 占位文案 - placeholder:string —— 占位文案
- disabled:boolean | Signal<boolean> —— 禁用态:布尔直接置位,传信号则动态同步 - disabled:boolean | Signal<boolean> —— 禁用态:布尔直接置位,传信号则动态同步
- onInput(回调):(value: string) => void —— 输入回调 - onInput(回调):(value: string) => void —— 输入回调
- prefix:string | HTMLElement —— 前置附加块(textField/inputNumber 在输入框内侧前、textarea 在上方);缺省不渲染
- suffix:string | HTMLElement —— 后置附加块(textField/inputNumber 在输入框内侧后、textarea 在下方);缺省不渲染
示例: 示例:
@@ -117,6 +173,7 @@ const bio = signal(''); textarea({ value: bio, rows: 4, placeholder: '一句话
1. 创建信号并传入 value 1. 创建信号并传入 value
2. 用 rows 控制初始高度,用户可纵向拉伸 2. 用 rows 控制初始高度,用户可纵向拉伸
3. 字数统计等派生逻辑直接在响应式文本里读 bio().length 3. 字数统计等派生逻辑直接在响应式文本里读 bio().length
4. 需要图标、货币符、单位等前后缀时传 prefix/suffix(字符串或 DOM),控件会包进带边框的附加组,聚焦光环由组统一承载
现场演示:apps/docs/demo.html#sec-form 现场演示:apps/docs/demo.html#sec-form
@@ -603,6 +660,96 @@ const tab = signal('a'); tabs({ tabs: [{ value: 'a', label: '概览' }, { value:
现场演示:apps/docs/demo.html#sec-tabs-modal 现场演示: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 ### pagination — 分页器 Pagination
参数: 参数:
@@ -754,8 +901,9 @@ dataTable({ data: users, selectable: true, columns: [{ key: 'name', header: '姓
API 速览: API 速览:
- button(props):按钮,variant = primary | ghost | danger,disabled 支持布尔或信号禁用 - button(props):按钮,variant = primary | ghost | danger,disabled 支持布尔或信号禁用
- textField(props):文本输入,value 信号双向绑定,disabled 支持布尔或信号禁用 - textField(props):文本输入,value 信号双向绑定,disabled 支持布尔或信号,onInput 输入回调;prefix/suffix 可选渲染为输入框内侧附加块
- textarea(props):多行输入,value 信号双向绑定,disabled 支持布尔或信号禁用 - inputNumber(props):数字输入,−/+ 按钮与键盘 ↑↓ 调节,min/max/step 夹取;prefix/suffix 可选渲染为整组前后附加块
- textarea(props):多行输入,value 信号双向绑定,disabled 支持布尔或信号,rows 控行数;prefix/suffix 可选渲染为上下附加块
- slider(props):滑块,value 信号双向绑定,min/max/step 可配,原生 range 承载键盘交互,右侧可显示当前值 - slider(props):滑块,value 信号双向绑定,min/max/step 可配,原生 range 承载键盘交互,右侧可显示当前值
- checkbox(props):复选框,checked 信号双向绑定,disabled 支持布尔或信号禁用 - checkbox(props):复选框,checked 信号双向绑定,disabled 支持布尔或信号禁用
- switchButton(props):开关,checked 信号双向绑定,disabled 支持布尔或信号禁用 - switchButton(props):开关,checked 信号双向绑定,disabled 支持布尔或信号禁用
@@ -777,11 +925,16 @@ API 速览:
- spinner(props):加载指示器,size 控制直径 - spinner(props):加载指示器,size 控制直径
- progress(props):进度条,value 信号驱动 0-100(满格切 success 色);不传 value 退化为不确定态无限往复 - progress(props):进度条,value 信号驱动 0-100(满格切 success 色);不传 value 退化为不确定态无限往复
- tabs(props):标签页,active 信号双向绑定,滑动指示条 + 方向键环形导航,支持禁用页签 - 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 提供底部操作区 - modal(props, ...children):弹窗,open 信号驱动显隐,Esc / 遮罩 / × 关闭,size 三档宽度,footer 提供底部操作区
- drawer(props, ...children):抽屉,open 信号驱动侧滑面板,placement 左右可配、width 可调、footer 操作区,遮罩 / × / Esc 关闭 - drawer(props, ...children):抽屉,open 信号驱动侧滑面板,placement 左右可配、width 可调、footer 操作区,遮罩 / × / Esc 关闭
- form(props, items):表单容器,布局 + 聚合校验,全部通过才触发 onSubmit - form(props, items):表单容器,布局 + 聚合校验,全部通过才触发 onSubmit
- formItem(props, control):表单字段,label + 必填红星 + 规则校验 + 错误区,返回 { el, validate } 句柄 - formItem(props, control):表单字段,label + 必填红星 + 规则校验 + 错误区,返回 { el, validate } 句柄
- dataTable(props):数据表格,粘性表头 / 行选择(全选三态)/ 排序 / 筛选 / 分页 / 空状态;loading 为真时数据行换骨架行且筛选/翻页禁用,内核为 table-core - dataTable(props):数据表格,粘性表头 / 行选择(全选三态)/ 排序 / 筛选 / 分页 / 空状态;loading 为真时数据行换骨架行且筛选/翻页禁用,内核为 table-core
- image(props):图片,缩略图点击 / 回车开遮罩大图预览(Esc 或点击遮罩关闭),width 控制缩略宽
一行用法: 一行用法:
@@ -879,12 +1032,13 @@ API 速览:
- ua docs:生成人类文档站 apps/docs/index.html + demo.html 与 Agent 全量文档 llms-full.txt - ua docs:生成人类文档站 apps/docs/index.html + demo.html 与 Agent 全量文档 llms-full.txt
- ua style:注释规范门禁(导出必须 /*** ***/、接口字段行尾注释、行注释中文),违规退出码 1 - ua style:注释规范门禁(导出必须 /*** ***/、接口字段行尾注释、行注释中文),违规退出码 1
- ua verify:清单一致性门禁(id 唯一 / exports 对应 / props 必带 n t d / steps≥3 / demo 锚点存在) - 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 - 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|mcp>;仓库内等价于 npm run index / docs / style / verify node packages/tools/cli/bin/ua.js <index|list|info <name>|docs|style|verify|commit-lint|mcp>;仓库内等价于 npm run index / docs / style / verify
``` ```
约定: 约定:
+11 -4
View File
@@ -45,11 +45,12 @@
## @ui-arch/ui (v0.1.0) ## @ui-arch/ui (v0.1.0)
- 类型: skin-dom · 目录: packages/skin/ui - 类型: skin-dom · 目录: packages/skin/ui
- 描述: DOM 皮肤组件库:button / textField / textarea / slider / checkbox / switch / select / menu / radioGroup / accordion / breadcrumb / card / badge / tag / avatar / alert / toast / tooltip / popover / skeleton / spinner / progress / tabs / pagination / modal / drawer / form / formItem / dataTable,构建在 @ui-arch/frame 与 @ui-arch/table-core 之上,样式只消费 --ua-* 令牌。 - 描述: DOM 皮肤组件库:button / textField / textarea / slider / checkbox / switch / select / menu / radioGroup / accordion / breadcrumb / card / badge / image / tag / avatar / alert / toast / tooltip / popover / skeleton / spinner / progress / tabs / pagination / modal / drawer / form / formItem / dataTable,构建在 @ui-arch/frame 与 @ui-arch/table-core 之上,样式只消费 --ua-* 令牌。
- API: - API:
- button(props):按钮,variant = primary | ghost | danger,disabled 支持布尔或信号禁用 - button(props):按钮,variant = primary | ghost | danger,disabled 支持布尔或信号禁用
- textField(props):文本输入,value 信号双向绑定,disabled 支持布尔或信号禁用 - textField(props):文本输入,value 信号双向绑定,disabled 支持布尔或信号,onInput 输入回调;prefix/suffix 可选渲染为输入框内侧附加块
- textarea(props):多行输入,value 信号双向绑定,disabled 支持布尔或信号禁用 - inputNumber(props):数字输入,−/+ 按钮与键盘 ↑↓ 调节,min/max/step 夹取;prefix/suffix 可选渲染为整组前后附加块
- textarea(props):多行输入,value 信号双向绑定,disabled 支持布尔或信号,rows 控行数;prefix/suffix 可选渲染为上下附加块
- slider(props):滑块,value 信号双向绑定,min/max/step 可配,原生 range 承载键盘交互,右侧可显示当前值 - slider(props):滑块,value 信号双向绑定,min/max/step 可配,原生 range 承载键盘交互,右侧可显示当前值
- checkbox(props):复选框,checked 信号双向绑定,disabled 支持布尔或信号禁用 - checkbox(props):复选框,checked 信号双向绑定,disabled 支持布尔或信号禁用
- switchButton(props):开关,checked 信号双向绑定,disabled 支持布尔或信号禁用 - switchButton(props):开关,checked 信号双向绑定,disabled 支持布尔或信号禁用
@@ -71,11 +72,16 @@
- spinner(props):加载指示器,size 控制直径 - spinner(props):加载指示器,size 控制直径
- progress(props):进度条,value 信号驱动 0-100(满格切 success 色);不传 value 退化为不确定态无限往复 - progress(props):进度条,value 信号驱动 0-100(满格切 success 色);不传 value 退化为不确定态无限往复
- tabs(props):标签页,active 信号双向绑定,滑动指示条 + 方向键环形导航,支持禁用页签 - 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 提供底部操作区 - modal(props, ...children):弹窗,open 信号驱动显隐,Esc / 遮罩 / × 关闭,size 三档宽度,footer 提供底部操作区
- drawer(props, ...children):抽屉,open 信号驱动侧滑面板,placement 左右可配、width 可调、footer 操作区,遮罩 / × / Esc 关闭 - drawer(props, ...children):抽屉,open 信号驱动侧滑面板,placement 左右可配、width 可调、footer 操作区,遮罩 / × / Esc 关闭
- form(props, items):表单容器,布局 + 聚合校验,全部通过才触发 onSubmit - form(props, items):表单容器,布局 + 聚合校验,全部通过才触发 onSubmit
- formItem(props, control):表单字段,label + 必填红星 + 规则校验 + 错误区,返回 { el, validate } 句柄 - formItem(props, control):表单字段,label + 必填红星 + 规则校验 + 错误区,返回 { el, validate } 句柄
- dataTable(props):数据表格,粘性表头 / 行选择(全选三态)/ 排序 / 筛选 / 分页 / 空状态;loading 为真时数据行换骨架行且筛选/翻页禁用,内核为 table-core - dataTable(props):数据表格,粘性表头 / 行选择(全选三态)/ 排序 / 筛选 / 分页 / 空状态;loading 为真时数据行换骨架行且筛选/翻页禁用,内核为 table-core
- image(props):图片,缩略图点击 / 回车开遮罩大图预览(Esc 或点击遮罩关闭),width 控制缩略宽
- 用法: `const checked = signal(false); checkbox({ label: '同意', checked }); const open = signal(false); modal({ title: '确认', open }, '内容')` - 用法: `const checked = signal(false); checkbox({ label: '同意', checked }); const open = signal(false); modal({ title: '确认', open }, '内容')`
- 约定: 皮肤不持有业务状态,一律委托 headless 内核或调用方信号 - 约定: 皮肤不持有业务状态,一律委托 headless 内核或调用方信号
- 约定: 表单类组件统一约定:*Props.value/checked/active 为 Signal 双向绑定,onChange/onInput 为回调 - 约定: 表单类组件统一约定:*Props.value/checked/active 为 Signal 双向绑定,onChange/onInput 为回调
@@ -93,8 +99,9 @@
- ua docs:生成人类文档站 apps/docs/index.html + demo.html 与 Agent 全量文档 llms-full.txt - ua docs:生成人类文档站 apps/docs/index.html + demo.html 与 Agent 全量文档 llms-full.txt
- ua style:注释规范门禁(导出必须 /*** ***/、接口字段行尾注释、行注释中文),违规退出码 1 - ua style:注释规范门禁(导出必须 /*** ***/、接口字段行尾注释、行注释中文),违规退出码 1
- ua verify:清单一致性门禁(id 唯一 / exports 对应 / props 必带 n t d / steps≥3 / demo 锚点存在) - 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 - 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|mcp>;仓库内等价于 npm run index / docs / style / verify` - 用法: `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 - 约定: 零依赖:只用 node 标准库,Node >= 18
- 约定: 清单是单一来源:代码注释、文档站、llms-full.txt、MCP 全部从 ua.manifest.json 派生 - 约定: 清单是单一来源:代码注释、文档站、llms-full.txt、MCP 全部从 ua.manifest.json 派生
- 约定: 门禁只增不改:新增校验必须能被人手复现,报错要指出文件与字段 - 约定: 门禁只增不改:新增校验必须能被人手复现,报错要指出文件与字段
+3 -2
View File
@@ -12,9 +12,10 @@
"style": "node packages/tools/cli/bin/ua.js style", "style": "node packages/tools/cli/bin/ua.js style",
"index": "node packages/tools/cli/bin/ua.js index", "index": "node packages/tools/cli/bin/ua.js index",
"docs": "node packages/tools/cli/bin/ua.js index && node packages/tools/cli/bin/ua.js docs", "docs": "node packages/tools/cli/bin/ua.js index && node packages/tools/cli/bin/ua.js docs",
"check": "npm run build && npm run style && npm run typecheck && npm run smoke && npm run index && npm run verify", "check": "npm run build && npm run style && npm run typecheck && npm run smoke && npm run index && npm run verify && npm run lint-iterations",
"ua": "node packages/tools/cli/bin/ua.js", "ua": "node packages/tools/cli/bin/ua.js",
"verify": "node packages/tools/cli/bin/ua.js verify" "verify": "node packages/tools/cli/bin/ua.js verify",
"lint-iterations": "node packages/tools/cli/bin/ua.js lint-iterations"
}, },
"devDependencies": { "devDependencies": {
"typescript": "^5.6.0" "typescript": "^5.6.0"
+13
View File
@@ -0,0 +1,13 @@
import { h } from '@ui-arch/frame';
/*** 输入附加块包装:prefix/suffix 存在时把控件包进附加组,否则原样返回(零侵入,结构不变) ***/
export function addonWrap(
el: HTMLElement,
prefix?: string | HTMLElement,
suffix?: string | HTMLElement,
column = false, // 多行控件用纵向布局:prefix 在上、suffix 在下
): HTMLElement {
if (!prefix && !suffix) return el; // 卫语句:无附加不包裹
const addon = (v?: string | HTMLElement) => (v == null ? null : h('span', { class: 'ua-input-addon' }, v));
return h('div', { class: column ? 'ua-input-group ua-input-group-column' : 'ua-input-group' }, addon(prefix), el, addon(suffix));
}
+1
View File
@@ -156,6 +156,7 @@ export function dataTable<T extends { id: RowKey }>(props: DataTableProps<T>): H
const filterInput = textField({ const filterInput = textField({
placeholder: `筛选 ${filterKey}…`, placeholder: `筛选 ${filterKey}…`,
prefix: '🔍',
disabled: props.loading, // 加载中禁止筛选,避免与在途结果竞争 disabled: props.loading, // 加载中禁止筛选,避免与在途结果竞争
onInput: (value) => { onInput: (value) => {
table.dispatch({ type: 'setFilter', key: filterKey, value }); table.dispatch({ type: 'setFilter', key: filterKey, value });
+11
View File
@@ -0,0 +1,11 @@
import { h } from '@ui-arch/frame';
import type { DividerProps } from './types.js';
/*** 分隔线:水平/垂直两种方向,可带居中文案标签,dashed 切虚线 ***/
export function divider(props: DividerProps): HTMLDivElement {
const cls = ['ua-divider', props.vertical ? 'ua-divider-vertical' : '', props.dashed ? 'ua-divider-dashed' : ''].filter(Boolean).join(' ');
const line = props.label && !props.vertical
? h('div', { class: cls, role: 'separator', 'aria-label': props.label }, h('span', { class: 'ua-divider-label' }, props.label))
: h('div', { class: cls, role: 'separator', 'aria-label': props.label }); // 垂直或无标签:纯线条
return line as HTMLDivElement;
}
+14
View File
@@ -0,0 +1,14 @@
import { h } from '@ui-arch/frame';
import type { EmptyProps } from './types.js';
/*** 空状态:居中占位提示,title/description 可选,action 放操作区(常见为按钮) ***/
export function empty(props: EmptyProps): HTMLDivElement {
return h(
'div',
{ class: 'ua-empty', role: 'status' }, // role=status 由读屏播报
h('div', { class: 'ua-empty-icon', 'aria-hidden': 'true' }, '⌀'), // 装饰符号,读屏跳过
props.title ? h('div', { class: 'ua-empty-title' }, props.title) : null,
props.description ? h('div', { class: 'ua-empty-desc' }, props.description) : null,
props.action ? h('div', { class: 'ua-empty-action' }, props.action) : null,
) as HTMLDivElement;
}
+37
View File
@@ -0,0 +1,37 @@
import { h } from '@ui-arch/frame';
import type { ImageProps } from './types.js';
/*** 图片:缩略图展示,点击 / 回车打开遮罩大图预览(Esc 或点击遮罩关闭),样式只消费 --ua-* 令牌 ***/
export function image(props: ImageProps): HTMLElement {
const style = props.width ? `width:${props.width}px` : '';
return h('img', {
class: 'ua-image',
src: props.src,
alt: props.alt ?? '',
style,
tabindex: '0', // 可聚焦,键盘用户回车预览
role: 'button',
'aria-label': '预览图片',
onclick: () => openViewer(props.src, props.alt),
onkeydown: (e: KeyboardEvent) => e.key === 'Enter' && openViewer(props.src, props.alt),
});
}
/*** 打开全屏遮罩预览层;关闭时自清理、解绑键盘并归还焦点 ***/
function openViewer(src: string, alt?: string): void {
const prev = document.activeElement as HTMLElement | null; // 焦点归还目标
const mask = h(
'div',
{ class: 'ua-image-mask', role: 'dialog', 'aria-modal': 'true', 'aria-label': '图片预览' },
h('img', { class: 'ua-image-full', src, alt: alt ?? '' }),
);
const close = () => {
mask.remove(); // 卫语句式自清理:遮罩移除 + 键盘解绑 + 焦点归还
document.removeEventListener('keydown', onKey);
prev?.focus?.();
};
const onKey = (e: KeyboardEvent) => e.key === 'Escape' && close();
mask.addEventListener('click', close);
document.addEventListener('keydown', onKey);
document.body.appendChild(mask);
}
+6
View File
@@ -1,6 +1,7 @@
export * from './types.js'; export * from './types.js';
export { button } from './button.js'; export { button } from './button.js';
export { textField } from './textField.js'; export { textField } from './textField.js';
export { inputNumber } from './inputNumber.js';
export { textarea } from './textarea.js'; export { textarea } from './textarea.js';
export { slider } from './slider.js'; export { slider } from './slider.js';
export { checkbox } from './checkbox.js'; export { checkbox } from './checkbox.js';
@@ -14,6 +15,7 @@ export { card } from './card.js';
export { badge } from './badge.js'; export { badge } from './badge.js';
export { tag } from './tag.js'; export { tag } from './tag.js';
export { avatar } from './avatar.js'; export { avatar } from './avatar.js';
export { image } from './image.js';
export { alert } from './alert.js'; export { alert } from './alert.js';
export { toast, showToast } from './toast.js'; export { toast, showToast } from './toast.js';
export { tooltip } from './tooltip.js'; export { tooltip } from './tooltip.js';
@@ -23,6 +25,10 @@ export { pagination } from './pagination.js';
export { spinner } from './spinner.js'; export { spinner } from './spinner.js';
export { progress } from './progress.js'; export { progress } from './progress.js';
export { tabs } from './tabs.js'; export { tabs } from './tabs.js';
export { stepper } from './stepper.js';
export { rate } from './rate.js';
export { empty } from './empty.js';
export { divider } from './divider.js';
export { modal } from './modal.js'; export { modal } from './modal.js';
export { drawer } from './drawer.js'; export { drawer } from './drawer.js';
export { form, formItem } from './form.js'; export { form, formItem } from './form.js';
+27
View File
@@ -0,0 +1,27 @@
import { effect, h } from '@ui-arch/frame';
import { button } from './button.js';
import { addonWrap } from './addons.js';
import type { InputNumberProps } from './types.js';
/*** 数字输入:−/+ 按钮 + 输入框三段式,value 信号双向绑定,min/max/step 超界自动夹取,键盘 ↑↓ 调节 ***/
export function inputNumber(props: InputNumberProps): HTMLElement {
const min = props.min ?? -Infinity; // 无下限缺省
const max = props.max ?? Infinity; // 无上限缺省
const step = props.step ?? 1; // 步长缺省 1
const clamp = (v: number): number => Math.min(max, Math.max(min, v)); // 任何来源的值都夹回合法区间
const emit = (v: number): void => { const n = clamp(v); props.value(n); if (props.onChange) props.onChange(n); }; // 写回信号并回调
const input = h('input', { class: 'ua-input ua-input-number-field', inputmode: 'decimal', disabled: props.disabled, 'aria-label': '数字输入' }) as HTMLInputElement;
const sync = (): void => { const text = String(props.value()); if (input.value !== text) input.value = text; }; // 外部变化写回输入框
effect(sync);
input.addEventListener('change', () => { const n = parseFloat(input.value); if (Number.isNaN(n)) sync(); else emit(n); }); // 非法输入回滚当前值
input.addEventListener('keydown', (e: KeyboardEvent) => {
if (e.key !== 'ArrowUp' && e.key !== 'ArrowDown') return; // 卫语句:只拦截上下方向键
e.preventDefault();
emit(props.value() + (e.key === 'ArrowUp' ? step : -step)); // ↑ 加步长 / ↓ 减步长
});
const dec = button({ label: '−', variant: 'ghost', disabled: props.disabled, onClick: () => emit(props.value() - step) });
const inc = button({ label: '+', variant: 'ghost', disabled: props.disabled, onClick: () => emit(props.value() + step) });
dec.setAttribute('aria-label', '减少');
inc.setAttribute('aria-label', '增加');
return addonWrap(h('div', { class: 'ua-input-number', role: 'group' }, dec, input, inc) as HTMLDivElement, props.prefix, props.suffix);
}
+34
View File
@@ -0,0 +1,34 @@
import { effect, h, signal } from '@ui-arch/frame';
import type { RateProps } from './types.js';
/*** 评分:星标点选,hover 预览移出还原,键盘 ←→/↑↓ 调节,value 信号双向绑定 ***/
export function rate(props: RateProps): HTMLDivElement {
const value = props.value;
const max = props.max ?? 5; // 星标总数缺省 5
const hover = signal(0); // 预览索引(1 基,0 表示无预览)
const emit = (v: number): void => { const n = Math.min(max, Math.max(1, v)); value(n); if (props.onChange) props.onChange(n); }; // 写回信号并回调
const render = (): HTMLElement => {
const shown = hover() || value(); // 预览优先,无预览显示当前值
const stars = [];
for (let i = 1; i <= max; i++) {
stars.push(h('button', {
class: `ua-rate-star${i <= shown ? ' is-on' : ''}`,
type: 'button',
disabled: props.disabled,
'aria-label': `${i} 星`,
onClick: () => emit(i),
onMouseEnter: () => hover(i),
}, '★'));
}
return h('div', { class: 'ua-rate', role: 'slider', 'aria-label': '评分', 'aria-valuemin': 1, 'aria-valuemax': max, 'aria-valuenow': value(), onMouseLeave: () => hover(0) }, ...stars);
};
let el!: HTMLElement; // 当前渲染产物(effect 首跑赋值)
effect(() => { const next = render(); if (el) el.replaceWith(next); el = next; }); // value/hover 变化整条重渲染(星标少,开销可忽略)
const keyNav = (e: KeyboardEvent): void => {
if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight' && e.key !== 'ArrowUp' && e.key !== 'ArrowDown') return; // 卫语句:只拦截方向键
e.preventDefault();
emit(value() + (e.key === 'ArrowRight' || e.key === 'ArrowUp' ? 1 : -1)); // →/↑ 加一分,←/↓ 减一分
};
el.addEventListener('keydown', keyNav);
return el as HTMLDivElement;
}
+28
View File
@@ -0,0 +1,28 @@
import { effect, h } from '@ui-arch/frame';
import type { StepperProps } from './types.js';
/*** 步骤条:done/current/upcoming 三态横向流程指示,clickable 时已完成步骤可点回跳并写回 active 信号 ***/
export function stepper(props: StepperProps): HTMLElement {
const active = props.active;
const clickStep = (i: number): void => { if (!props.clickable || i >= active()) return; // 卫语句:不可点或未完成不回跳
active(i); if (props.onChange) props.onChange(i); }; // 写回信号并回调
const render = (): HTMLElement => {
const cur = active();
const items = props.steps.map((s, i) => {
const state = i < cur ? 'done' : i === cur ? 'current' : 'upcoming'; // 三态:已完成 / 当前 / 未到
const mark = state === 'done' ? '✓' : String(i + 1); // 完成打勾,其余显示序号
const clickable = !!props.clickable && i < cur; // 仅已完成步骤可回跳
const head = clickable
? h('button', { class: 'ua-step-dot', type: 'button', onClick: () => clickStep(i) }, mark)
: h('span', { class: 'ua-step-dot' }, mark);
const body = h('div', { class: 'ua-step-text' }, h('span', { class: 'ua-step-label' }, s.label),
s.description ? h('span', { class: 'ua-step-desc' }, s.description) : null);
const li = h('li', { class: `ua-step ua-step-${state}`, 'aria-current': state === 'current' ? 'step' : null }, head, body);
return li;
});
return h('ol', { class: 'ua-stepper' }, ...items);
};
let el!: HTMLElement; // 当前渲染产物(effect 首跑赋值)
effect(() => { const next = render(); if (el) el.replaceWith(next); el = next; }); // active 变化整条重渲染(条目少,开销可忽略)
return el;
}
+4 -3
View File
@@ -1,8 +1,9 @@
import { effect, h } from '@ui-arch/frame'; import { effect, h } from '@ui-arch/frame';
import { addonWrap } from './addons.js';
import type { TextFieldProps } from './types.js'; import type { TextFieldProps } from './types.js';
/*** 文本输入:value 信号双向绑定,disabled 支持布尔或信号禁用,onInput 为输入回调 ***/ /*** 文本输入:value 信号双向绑定,disabled 支持布尔或信号禁用,onInput 为输入回调;prefix/suffix 包附加组 ***/
export function textField(props: TextFieldProps): HTMLInputElement { export function textField(props: TextFieldProps): HTMLElement {
const el = h('input', { class: 'ua-input', placeholder: props.placeholder ?? '', disabled: props.disabled }) as HTMLInputElement; // false/undefined 移除属性,Signal 走响应式 const el = h('input', { class: 'ua-input', placeholder: props.placeholder ?? '', disabled: props.disabled }) as HTMLInputElement; // false/undefined 移除属性,Signal 走响应式
if (props.value) { if (props.value) {
const signal = props.value; const signal = props.value;
@@ -12,5 +13,5 @@ export function textField(props: TextFieldProps): HTMLInputElement {
}); });
} }
if (props.onInput) el.addEventListener('input', () => props.onInput!(el.value)); if (props.onInput) el.addEventListener('input', () => props.onInput!(el.value));
return el; return addonWrap(el, props.prefix, props.suffix);
} }
+4 -3
View File
@@ -1,8 +1,9 @@
import { effect, h } from '@ui-arch/frame'; import { effect, h } from '@ui-arch/frame';
import { addonWrap } from './addons.js';
import type { TextareaProps } from './types.js'; import type { TextareaProps } from './types.js';
/*** 多行输入:value 信号双向绑定,disabled 支持布尔或信号禁用,onInput 为输入回调 ***/ /*** 多行输入:value 信号双向绑定,disabled 支持布尔或信号禁用,onInput 为输入回调;prefix/suffix 包纵向附加组 ***/
export function textarea(props: TextareaProps): HTMLTextAreaElement { export function textarea(props: TextareaProps): HTMLElement {
const el = h('textarea', { class: 'ua-textarea', rows: props.rows ?? 3, placeholder: props.placeholder ?? '', disabled: props.disabled }) as HTMLTextAreaElement; // false/undefined 移除属性,Signal 走响应式 const el = h('textarea', { class: 'ua-textarea', rows: props.rows ?? 3, placeholder: props.placeholder ?? '', disabled: props.disabled }) as HTMLTextAreaElement; // false/undefined 移除属性,Signal 走响应式
if (props.value) { if (props.value) {
const signal = props.value; const signal = props.value;
@@ -12,5 +13,5 @@ export function textarea(props: TextareaProps): HTMLTextAreaElement {
}); });
} }
if (props.onInput) el.addEventListener('input', () => props.onInput!(el.value)); if (props.onInput) el.addEventListener('input', () => props.onInput!(el.value));
return el; return addonWrap(el, props.prefix, props.suffix, true);
} }
+59
View File
@@ -32,6 +32,8 @@ export interface TextFieldProps {
value?: Signal<string>; // 双向绑定的信号 value?: Signal<string>; // 双向绑定的信号
disabled?: boolean | Signal<boolean>; // 禁用态:布尔直接置位,传信号则动态同步 disabled?: boolean | Signal<boolean>; // 禁用态:布尔直接置位,传信号则动态同步
onInput?: (value: string) => void; // 输入回调 onInput?: (value: string) => void; // 输入回调
prefix?: string | HTMLElement; // 输入框内侧前置附加块(如图标、货币符);缺省不渲染
suffix?: string | HTMLElement; // 输入框内侧后置附加块(如单位);缺省不渲染
} }
/*** 卡片入参 ***/ /*** 卡片入参 ***/
@@ -163,6 +165,56 @@ export interface TextareaProps {
value?: Signal<string>; // 双向绑定的信号 value?: Signal<string>; // 双向绑定的信号
disabled?: boolean | Signal<boolean>; // 禁用态:布尔直接置位,传信号则动态同步 disabled?: boolean | Signal<boolean>; // 禁用态:布尔直接置位,传信号则动态同步
onInput?: (value: string) => void; // 输入回调 onInput?: (value: string) => void; // 输入回调
prefix?: string | HTMLElement; // 上方附加块(如标题、计数说明);缺省不渲染
suffix?: string | HTMLElement; // 下方附加块(如字数提示);缺省不渲染
}
/*** 数字输入入参 ***/
export interface InputNumberProps {
value: Signal<number>; // 当前值信号,双向绑定(按钮/键盘写回、外部赋值同步)
min?: number; // 最小值;缺省无下限
max?: number; // 最大值;缺省无上限
step?: number; // 步长;缺省 1
disabled?: boolean | Signal<boolean>; // 禁用态:布尔直接置位,传信号则动态同步
onChange?: (value: number) => void; // 值变化回调(已夹取到合法区间)
prefix?: string | HTMLElement; // −/+ 按钮前的附加块(如货币符);缺省不渲染
suffix?: string | HTMLElement; // +/+ 按钮后的附加块(如单位);缺省不渲染
}
/*** 步骤条条目 ***/
export interface StepperItem {
label: string; // 步骤标题
description?: string; // 步骤补充说明;缺省不渲染
}
/*** 步骤条入参 ***/
export interface StepperProps {
steps: StepperItem[]; // 步骤列表(按顺序渲染)
active: Signal<number>; // 当前步索引(0 基),双向绑定(回跳写回、外部赋值同步)
clickable?: boolean; // 已完成步骤是否可点击回跳;缺省 false
onChange?: (index: number) => void; // 步骤切换回调(点击回跳触发)
}
/*** 评分入参 ***/
export interface RateProps {
value: Signal<number>; // 当前评分(1 基整数值),双向绑定(点击/键盘写回、外部赋值同步)
max?: number; // 星标总数;缺省 5
disabled?: boolean | Signal<boolean>; // 禁用态:布尔直接置位,传信号则动态同步
onChange?: (value: number) => void; // 评分变化回调
}
/*** 空状态入参 ***/
export interface EmptyProps {
title?: string; // 主标题;缺省不渲染
description?: string; // 补充说明;缺省不渲染
action?: unknown; // 操作区内容(常见为按钮);缺省不渲染
}
/*** 分隔线入参 ***/
export interface DividerProps {
label?: string; // 居中文案标签;缺省纯线条
vertical?: boolean; // 是否垂直分隔线(用于行内工具栏);缺省水平
dashed?: boolean; // 是否虚线;缺省实线
} }
/*** 徽标入参 ***/ /*** 徽标入参 ***/
@@ -187,6 +239,13 @@ export interface TagProps {
onClose?: () => void; // 关闭回调(调用方自行从数据源移除该项) onClose?: () => void; // 关闭回调(调用方自行从数据源移除该项)
} }
/*** 图片入参 ***/
export interface ImageProps {
src: string; // 图片地址(缩略图与大图共用)
alt?: string; // 替代文本;缺省空串
width?: number; // 缩略图宽度像素,高度自适应;缺省走样式默认
}
/*** 提示条入参 ***/ /*** 提示条入参 ***/
export interface AlertProps { export interface AlertProps {
message: string; // 提示内容 message: string; // 提示内容
+250 -6
View File
@@ -1,7 +1,7 @@
{ {
"name": "@ui-arch/ui", "name": "@ui-arch/ui",
"type": "skin-dom", "type": "skin-dom",
"description": "DOM 皮肤组件库:button / textField / textarea / slider / checkbox / switch / select / menu / radioGroup / accordion / breadcrumb / card / badge / tag / avatar / alert / toast / tooltip / popover / skeleton / spinner / progress / tabs / pagination / modal / drawer / form / formItem / dataTable,构建在 @ui-arch/frame 与 @ui-arch/table-core 之上,样式只消费 --ua-* 令牌。", "description": "DOM 皮肤组件库:button / textField / textarea / slider / checkbox / switch / select / menu / radioGroup / accordion / breadcrumb / card / badge / image / tag / avatar / alert / toast / tooltip / popover / skeleton / spinner / progress / tabs / pagination / modal / drawer / form / formItem / dataTable,构建在 @ui-arch/frame 与 @ui-arch/table-core 之上,样式只消费 --ua-* 令牌。",
"tags": [ "tags": [
"components", "components",
"dom", "dom",
@@ -14,8 +14,9 @@
"entry": "src/index.ts", "entry": "src/index.ts",
"exports": [ "exports": [
"button(props):按钮,variant = primary | ghost | danger,disabled 支持布尔或信号禁用", "button(props):按钮,variant = primary | ghost | danger,disabled 支持布尔或信号禁用",
"textField(props):文本输入,value 信号双向绑定,disabled 支持布尔或信号禁用", "textField(props):文本输入,value 信号双向绑定,disabled 支持布尔或信号,onInput 输入回调;prefix/suffix 可选渲染为输入框内侧附加块",
"textarea(props):多行输入,value 信号双向绑定,disabled 支持布尔或信号禁用", "inputNumber(props):数字输入,−/+ 按钮与键盘 ↑↓ 调节,min/max/step 夹取;prefix/suffix 可选渲染为整组前后附加块",
"textarea(props):多行输入,value 信号双向绑定,disabled 支持布尔或信号,rows 控行数;prefix/suffix 可选渲染为上下附加块",
"slider(props):滑块,value 信号双向绑定,min/max/step 可配,原生 range 承载键盘交互,右侧可显示当前值", "slider(props):滑块,value 信号双向绑定,min/max/step 可配,原生 range 承载键盘交互,右侧可显示当前值",
"checkbox(props):复选框,checked 信号双向绑定,disabled 支持布尔或信号禁用", "checkbox(props):复选框,checked 信号双向绑定,disabled 支持布尔或信号禁用",
"switchButton(props):开关,checked 信号双向绑定,disabled 支持布尔或信号禁用", "switchButton(props):开关,checked 信号双向绑定,disabled 支持布尔或信号禁用",
@@ -37,11 +38,16 @@
"spinner(props):加载指示器,size 控制直径", "spinner(props):加载指示器,size 控制直径",
"progress(props):进度条,value 信号驱动 0-100(满格切 success 色);不传 value 退化为不确定态无限往复", "progress(props):进度条,value 信号驱动 0-100(满格切 success 色);不传 value 退化为不确定态无限往复",
"tabs(props):标签页,active 信号双向绑定,滑动指示条 + 方向键环形导航,支持禁用页签", "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 提供底部操作区", "modal(props, ...children):弹窗,open 信号驱动显隐,Esc / 遮罩 / × 关闭,size 三档宽度,footer 提供底部操作区",
"drawer(props, ...children):抽屉,open 信号驱动侧滑面板,placement 左右可配、width 可调、footer 操作区,遮罩 / × / Esc 关闭", "drawer(props, ...children):抽屉,open 信号驱动侧滑面板,placement 左右可配、width 可调、footer 操作区,遮罩 / × / Esc 关闭",
"form(props, items):表单容器,布局 + 聚合校验,全部通过才触发 onSubmit", "form(props, items):表单容器,布局 + 聚合校验,全部通过才触发 onSubmit",
"formItem(props, control):表单字段,label + 必填红星 + 规则校验 + 错误区,返回 { el, validate } 句柄", "formItem(props, control):表单字段,label + 必填红星 + 规则校验 + 错误区,返回 { el, validate } 句柄",
"dataTable(props):数据表格,粘性表头 / 行选择(全选三态)/ 排序 / 筛选 / 分页 / 空状态;loading 为真时数据行换骨架行且筛选/翻页禁用,内核为 table-core" "dataTable(props):数据表格,粘性表头 / 行选择(全选三态)/ 排序 / 筛选 / 分页 / 空状态;loading 为真时数据行换骨架行且筛选/翻页禁用,内核为 table-core",
"image(props):图片,缩略图点击 / 回车开遮罩大图预览(Esc 或点击遮罩关闭),width 控制缩略宽"
], ],
"keyTypes": [ "keyTypes": [
"ButtonProps", "ButtonProps",
@@ -98,6 +104,37 @@
"读写状态:name() 读、name('张三') 写,界面自动同步,无需手动操作 DOM" "读写状态:name() 读、name('张三') 写,界面自动同步,无需手动操作 DOM"
], ],
"components": [ "components": [
{
"id": "image",
"title": "图片 Image",
"demo": "sec-image",
"props": [
{
"n": "src",
"t": "string",
"r": 1,
"d": "图片地址(缩略图与大图共用)"
},
{
"n": "alt",
"t": "string",
"d": "替代文本;缺省空串",
"df": "''"
},
{
"n": "width",
"t": "number",
"d": "缩略图宽度像素,高度自适应;缺省走样式默认"
}
],
"example": "image({ src: '/a.png', alt: '架构图', width: 160 })",
"steps": [
"基础用法只传 src:渲染缩略图,hover 上浮提亮,光标变放大镜",
"点击缩略图(或聚焦后回车)打开全屏遮罩大图,Esc 或点击遮罩任意处关闭,焦点归还缩略图",
"宽度用 width(像素)控制,高度按比例自适应,容器内不撑破 max-width",
"alt 尽量传有意义的描述,读屏与图片加载失败时都依赖它"
]
},
{ {
"id": "button", "id": "button",
"title": "按钮 Button", "title": "按钮 Button",
@@ -159,13 +196,81 @@
"t": "(value: string) => void", "t": "(value: string) => void",
"k": 1, "k": 1,
"d": "输入回调,每次按键触发" "d": "输入回调,每次按键触发"
},
{
"n": "prefix",
"t": "string | HTMLElement",
"d": "前置附加块(textField/inputNumber 在输入框内侧前、textarea 在上方);缺省不渲染"
},
{
"n": "suffix",
"t": "string | HTMLElement",
"d": "后置附加块(textField/inputNumber 在输入框内侧后、textarea 在下方);缺省不渲染"
} }
], ],
"example": "const name = signal(''); const el = textField({ value: name, placeholder: '输入名字…', onInput: (v) => console.log(v) })", "example": "const name = signal(''); const el = textField({ value: name, placeholder: '输入名字…', onInput: (v) => console.log(v) })",
"steps": [ "steps": [
"先用 signal('') 创建字符串信号", "先用 signal('') 创建字符串信号",
"传入 value 完成双向绑定:用户输入自动写回,外部赋值自动回显", "传入 value 完成双向绑定:用户输入自动写回,外部赋值自动回显",
"在任意响应式位置用 name() 读取最新值,如 () => `你好,${name()}`" "在任意响应式位置用 name() 读取最新值,如 () => `你好,${name()}`",
"需要图标、货币符、单位等前后缀时传 prefix/suffix(字符串或 DOM),控件会包进带边框的附加组,聚焦光环由组统一承载"
]
},
{
"id": "inputNumber",
"title": "数字输入 InputNumber",
"demo": "sec-inputnumber",
"props": [
{
"n": "value",
"t": "Signal<number>",
"r": 1,
"d": "当前值信号,双向绑定(按钮/键盘写回、外部赋值同步)"
},
{
"n": "min",
"t": "number",
"d": "最小值;缺省无下限"
},
{
"n": "max",
"t": "number",
"d": "最大值;缺省无上限"
},
{
"n": "step",
"t": "number",
"d": "步长;缺省 1",
"df": "1"
},
{
"n": "disabled",
"t": "boolean | Signal<boolean>",
"d": "禁用态:布尔直接置位,传信号则动态同步"
},
{
"n": "onChange",
"t": "(value: number) => void",
"k": 1,
"d": "值变化回调(已夹取到合法区间)"
},
{
"n": "prefix",
"t": "string | HTMLElement",
"d": "前置附加块(textField/inputNumber 在输入框内侧前、textarea 在上方);缺省不渲染"
},
{
"n": "suffix",
"t": "string | HTMLElement",
"d": "后置附加块(textField/inputNumber 在输入框内侧后、textarea 在下方);缺省不渲染"
}
],
"example": "const qty = signal(1); const el = inputNumber({ value: qty, min: 1, max: 99, onChange: (v) => console.log(v) })",
"steps": [
"用 signal(1) 创建数字信号并传入 value 完成双向绑定",
"点击 −/+ 按钮或在输入框聚焦时按 ↑↓ 以 step 调节;直接输入在失焦或回车时解析",
"非法输入自动回滚为当前值;min/max 超界自动夹取,disabled 支持布尔或信号禁用",
"需要图标、货币符、单位等前后缀时传 prefix/suffix(字符串或 DOM),控件会包进带边框的附加组,聚焦光环由组统一承载"
] ]
}, },
{ {
@@ -199,13 +304,24 @@
"t": "(value: string) => void", "t": "(value: string) => void",
"k": 1, "k": 1,
"d": "输入回调" "d": "输入回调"
},
{
"n": "prefix",
"t": "string | HTMLElement",
"d": "前置附加块(textField/inputNumber 在输入框内侧前、textarea 在上方);缺省不渲染"
},
{
"n": "suffix",
"t": "string | HTMLElement",
"d": "后置附加块(textField/inputNumber 在输入框内侧后、textarea 在下方);缺省不渲染"
} }
], ],
"example": "const bio = signal(''); textarea({ value: bio, rows: 4, placeholder: '一句话介绍…' })", "example": "const bio = signal(''); textarea({ value: bio, rows: 4, placeholder: '一句话介绍…' })",
"steps": [ "steps": [
"创建信号并传入 value", "创建信号并传入 value",
"用 rows 控制初始高度,用户可纵向拉伸", "用 rows 控制初始高度,用户可纵向拉伸",
"字数统计等派生逻辑直接在响应式文本里读 bio().length" "字数统计等派生逻辑直接在响应式文本里读 bio().length",
"需要图标、货币符、单位等前后缀时传 prefix/suffix(字符串或 DOM),控件会包进带边框的附加组,聚焦光环由组统一承载"
] ]
}, },
{ {
@@ -931,6 +1047,134 @@
"键盘可访问:Tab 聚焦激活页签,←→ 在启用页签间环形移动" "键盘可访问:Tab 聚焦激活页签,←→ 在启用页签间环形移动"
] ]
}, },
{
"id": "stepper",
"title": "步骤条 Stepper",
"demo": "sec-stepper",
"props": [
{
"n": "steps",
"t": "StepperItem[]",
"r": 1,
"d": "步骤列表,每项 { label, description? }"
},
{
"n": "active",
"t": "Signal<number>",
"r": 1,
"d": "当前步索引(0 基)信号,双向绑定:回跳写回、外部赋值同步"
},
{
"n": "clickable",
"t": "boolean",
"d": "已完成步骤是否可点击回跳;缺省 false"
},
{
"n": "onChange",
"t": "(index: number) => void",
"k": 1,
"d": "步骤切换回调(点击回跳触发)"
}
],
"example": "const step = signal(1); stepper({ steps: [{ label: '下单' }, { label: '支付' }, { label: '完成' }], active: step, clickable: true })",
"steps": [
"用 signal(0) 创建当前步索引,传入 active;索引之前的步骤显示 ✓ 已完成态",
"流程推进时外部赋值即可:step(step() + 1),组件三态自动跟随",
"clickable: true 时已完成步骤可点击回跳并写回信号,配合 onChange 同步业务状态"
]
},
{
"id": "rate",
"title": "评分 Rate",
"demo": "sec-rate",
"props": [
{
"n": "value",
"t": "Signal<number>",
"r": 1,
"d": "当前评分(1 基整数值)信号,双向绑定:点选/键盘写回、外部赋值同步"
},
{
"n": "max",
"t": "number",
"d": "星标总数;缺省 5",
"df": "5"
},
{
"n": "disabled",
"t": "boolean | Signal<boolean>",
"d": "禁用态:布尔直接置位,传信号则动态同步"
},
{
"n": "onChange",
"t": "(value: number) => void",
"k": 1,
"d": "评分变化回调"
}
],
"example": "const score = signal(0); rate({ value: score, max: 5, onChange: (v) => console.log(v) })",
"steps": [
"用 signal(0) 创建评分信号并传入 value;0 表示未评分",
"点击星标或聚焦后按 →/↑ 加一分、←/↓ 减一分;悬停高亮预览、移出还原",
"max 可调整星标总数;disabled 支持布尔或信号禁用"
]
},
{
"id": "empty",
"title": "空状态 Empty",
"demo": "sec-empty",
"props": [
{
"n": "title",
"t": "string",
"d": "主标题;缺省不渲染"
},
{
"n": "description",
"t": "string",
"d": "补充说明;缺省不渲染"
},
{
"n": "action",
"t": "unknown",
"d": "操作区内容(常见为按钮);缺省不渲染"
}
],
"example": "empty({ title: '暂无数据', description: '调整筛选条件后再试', action: button({ label: '重置筛选', variant: 'ghost' }) })",
"steps": [
"在列表 / 表格 / 筛选结果为空时渲染 empty 占位",
"title 一句话说明状态,description 补充原因或建议",
"action 传入 DOM(如 ghost 按钮)引导用户下一步操作"
]
},
{
"id": "divider",
"title": "分隔线 Divider",
"demo": "sec-divider",
"props": [
{
"n": "label",
"t": "string",
"d": "居中文案标签;缺省纯线条"
},
{
"n": "vertical",
"t": "boolean",
"d": "是否垂直分隔线(用于行内工具栏);缺省水平"
},
{
"n": "dashed",
"t": "boolean",
"d": "是否虚线;缺省实线"
}
],
"example": "divider({ label: '更多信息' }) / divider({ vertical: true })",
"steps": [
"在区块之间插入 divider 划分内容层次",
"传 label 在水平线上显示居中文案,如「更多信息」",
"工具栏内用 vertical: true 的行内竖线,dashed: true 切虚线弱化分隔感"
]
},
{ {
"id": "pagination", "id": "pagination",
"title": "分页器 Pagination", "title": "分页器 Pagination",
+281 -1
View File
@@ -687,6 +687,202 @@
resize: vertical; resize: vertical;
} }
/* —— 数字输入 —— */
.ua-input-number {
display: inline-flex;
align-items: center;
gap: 4px;
}
.ua-input-number .ua-btn {
padding: 5px 12px; /* 收窄为步进按钮 */
line-height: 1.4;
}
.ua-input-number-field {
width: 76px;
text-align: center;
font-variant-numeric: tabular-nums;
}
/* —— 步骤条 —— */
.ua-stepper {
display: flex;
margin: 0;
padding: 0;
list-style: none;
}
.ua-step {
display: flex;
align-items: flex-start;
gap: var(--ua-space-2);
flex: 1;
}
.ua-step + .ua-step {
padding-left: var(--ua-space-3);
}
.ua-step + .ua-step::before {
content: ''; /* 连接线:与前一节点之间的横向短线 */
width: 20px;
height: 1px;
margin-top: 13px;
margin-right: var(--ua-space-2);
background: var(--ua-color-border);
flex-shrink: 0;
}
.ua-step-dot {
display: inline-flex;
align-items: center;
justify-content: center;
width: 26px;
height: 26px;
border: 1px solid var(--ua-color-border);
border-radius: 999px;
background: var(--ua-color-surface);
color: var(--ua-color-text-muted);
font: inherit;
font-size: var(--ua-font-size-sm);
flex-shrink: 0;
transition: border-color 0.15s, background 0.15s, color 0.15s, transform 0.12s, box-shadow 0.15s;
}
button.ua-step-dot {
padding: 0; /* 可回跳步骤:重置原生按钮样式 */
cursor: pointer;
}
button.ua-step-dot:hover {
border-color: var(--ua-color-accent);
color: var(--ua-color-accent);
transform: translateY(-1px);
}
.ua-step-done .ua-step-dot {
border-color: color-mix(in srgb, var(--ua-color-accent) 55%, transparent);
background: color-mix(in srgb, var(--ua-color-accent) 10%, var(--ua-color-bg));
color: var(--ua-color-accent);
}
.ua-step-current .ua-step-dot {
border-color: var(--ua-color-accent);
background: var(--ua-color-bg);
color: var(--ua-color-accent);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--ua-color-accent) 15%, transparent);
}
.ua-step-text {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.ua-step-label {
font-size: var(--ua-font-size-md);
line-height: 1.4;
color: var(--ua-color-text-muted);
}
.ua-step-current .ua-step-label {
color: var(--ua-color-text);
font-weight: 600;
}
.ua-step-desc {
font-size: var(--ua-font-size-sm);
color: var(--ua-color-text-muted);
}
/* —— 评分 —— */
.ua-rate {
display: inline-flex;
gap: 2px;
}
.ua-rate-star {
padding: 0;
border: none;
background: none;
color: var(--ua-color-border); /* 未选中:弱化为边框色 */
font-size: 22px;
line-height: 1;
cursor: pointer;
transition: color 0.15s, transform 0.12s;
}
.ua-rate-star:hover {
transform: scale(1.15); /* 悬停微放大 */
}
.ua-rate-star.is-on {
color: var(--ua-color-warning); /* 选中:警示黄即星标金 */
}
.ua-rate-star:disabled {
cursor: not-allowed;
opacity: 0.45;
}
.ua-rate-star:focus-visible {
outline: none;
box-shadow: 0 0 0 3px color-mix(in srgb, var(--ua-color-warning) 25%, transparent);
border-radius: 4px;
}
/* —— 空状态 —— */
.ua-empty {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--ua-space-2);
padding: var(--ua-space-4);
min-height: 120px; /* 撑出足够留白感 */
justify-content: center;
text-align: center;
}
.ua-empty-icon {
font-size: 34px;
line-height: 1;
color: var(--ua-color-border); /* 装饰符号弱化为边框色 */
}
.ua-empty-title {
font-size: var(--ua-font-size-md);
font-weight: 600;
color: var(--ua-color-text);
}
.ua-empty-desc {
font-size: var(--ua-font-size-sm);
color: var(--ua-color-text-muted);
}
.ua-empty-action {
margin-top: var(--ua-space-1);
}
/* —— 分隔线 —— */
.ua-divider {
display: flex;
align-items: center;
gap: var(--ua-space-3);
margin: var(--ua-space-3) 0;
}
.ua-divider::before,
.ua-divider::after {
content: '';
flex: 1;
height: 1px;
background: var(--ua-color-border);
}
.ua-divider-dashed::before,
.ua-divider-dashed::after {
background: none;
border-top: 1px dashed var(--ua-color-border);
}
.ua-divider-label {
font-size: var(--ua-font-size-sm);
color: var(--ua-color-text-muted);
white-space: nowrap;
}
.ua-divider-vertical {
display: inline-block;
align-self: stretch;
width: 1px;
min-height: 20px;
margin: 0 var(--ua-space-2);
}
.ua-divider-vertical::before,
.ua-divider-vertical::after {
content: none; /* 垂直线:单线无伪元素 */
}
.ua-divider-vertical.ua-divider-dashed {
background: none;
border-left: 1px dashed var(--ua-color-border); /* 垂直虚线走边框 */
}
/* —— 徽标 —— */ /* —— 徽标 —— */
.ua-badge { .ua-badge {
display: inline-block; display: inline-block;
@@ -1362,8 +1558,92 @@
.ua-modal, .ua-modal,
.ua-modal-overlay, .ua-modal-overlay,
.ua-drawer, .ua-drawer,
.ua-drawer-overlay { .ua-drawer-overlay,
.ua-step-dot,
.ua-rate-star {
animation: none !important; animation: none !important;
transition: none !important; transition: none !important;
} }
} }
/* ---- image 图片(缩略图 + 遮罩大图预览) ---- */
.ua-image {
display: inline-block;
max-width: 100%;
border-radius: var(--ua-radius-md);
cursor: zoom-in;
transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.ua-image:hover,
.ua-image:focus-visible {
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.14);
outline: none;
}
.ua-image-mask {
position: fixed;
inset: 0;
z-index: 1100;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.6); /* 遮罩透明度叠加,非实体色 */
cursor: zoom-out;
animation: ua-image-in 0.18s ease;
}
.ua-image-full {
max-width: 90vw;
max-height: 90vh;
border-radius: var(--ua-radius-md);
box-shadow: 0 10px 28px rgba(0, 0, 0, 0.16);
}
@keyframes ua-image-in {
from { opacity: 0; transform: scale(0.96); }
to { opacity: 1; transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) {
.ua-image,
.ua-image-mask {
animation: none !important;
transition: none !important;
}
}
/* ---- 输入附加组(prefix/suffix) ---- */
.ua-input-group {
display: inline-flex;
align-items: center;
border: 1px solid var(--ua-color-border);
border-radius: var(--ua-radius-md);
background: var(--ua-color-bg);
transition: border-color 0.15s, box-shadow 0.15s;
}
.ua-input-group:focus-within {
border-color: var(--ua-color-accent);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--ua-color-accent) 18%, transparent);
}
.ua-input-group .ua-input,
.ua-input-group .ua-textarea {
border: none; /* 组已带边框,内层去边防双线 */
border-radius: 0;
min-width: 160px;
background: transparent;
}
.ua-input-group .ua-input:focus,
.ua-input-group .ua-textarea:focus {
box-shadow: none; /* 聚焦光环由组统一承载 */
outline: none;
}
.ua-input-group-column {
flex-direction: column;
align-items: stretch;
}
.ua-input-addon {
display: inline-flex;
align-items: center;
padding: 6px 10px;
color: var(--ua-color-text-muted);
background: color-mix(in srgb, var(--ua-color-text) 5%, transparent);
font-size: var(--ua-font-size-md);
user-select: none;
}
+125 -5
View File
@@ -56,6 +56,13 @@ function collectManifests() {
return out; return out;
} }
/*** 内容无变化则跳过写入(幂等,行尾归一化防 CRLF 误判;norm 可剔除时间戳后再比对),避免弄脏工作区 ***/
function writeIfChanged(file, content, norm = (s) => s) {
if (fs.existsSync(file) && norm(fs.readFileSync(file, 'utf8').replace(/\r\n/g, '\n')) === norm(content)) return false;
fs.writeFileSync(file, content);
return true;
}
/*** 聚合清单,生成 .ua/index.json 与 llms.txt ***/ /*** 聚合清单,生成 .ua/index.json 与 llms.txt ***/
function cmdIndex() { function cmdIndex() {
const packages = collectManifests(); const packages = collectManifests();
@@ -66,7 +73,11 @@ function cmdIndex() {
repoRoot: '.', repoRoot: '.',
packages, packages,
}; };
fs.writeFileSync(INDEX_FILE, JSON.stringify(index, null, 2) + '\n'); // 幂等:剔除 generatedAt 后内容一致即不写入,连续 check 不弄脏工作区
const json = JSON.stringify(index, null, 2) + '\n';
const stripTs = (s) => s.replace(/"generatedAt": "[^"]*"/, '"generatedAt": ""');
const changed = !fs.existsSync(INDEX_FILE) || stripTs(fs.readFileSync(INDEX_FILE, 'utf8').replace(/\r\n/g, '\n')) !== stripTs(json);
if (changed) fs.writeFileSync(INDEX_FILE, json);
const lines = [ const lines = [
'# @ui-arch 私人组件库索引', '# @ui-arch 私人组件库索引',
@@ -87,8 +98,8 @@ function cmdIndex() {
for (const c of p.conventions ?? []) lines.push(`- 约定: ${c}`); for (const c of p.conventions ?? []) lines.push(`- 约定: ${c}`);
lines.push(''); lines.push('');
} }
fs.writeFileSync(LLMS_FILE, lines.join('\n')); writeIfChanged(LLMS_FILE, lines.join('\n'));
console.log(`indexed ${packages.length} package(s) -> .ua/index.json, llms.txt`); console.log(`indexed ${packages.length} package(s) -> .ua/index.json, llms.txt${changed ? '' : ' (unchanged, skip)'}`);
} }
/*** 读取注册表索引;缺失则报错退出 ***/ /*** 读取注册表索引;缺失则报错退出 ***/
@@ -345,7 +356,9 @@ function cmdDocs() {
fs.writeFileSync(path.join(outDir, 'index.html'), renderDocs(index, tokens)); fs.writeFileSync(path.join(outDir, 'index.html'), renderDocs(index, tokens));
const demoSrc = path.join(ROOT, 'apps', 'demo', 'dist', 'demo.html'); // 演示页拷入文档站供 iframe const demoSrc = path.join(ROOT, 'apps', 'demo', 'dist', 'demo.html'); // 演示页拷入文档站供 iframe
if (fs.existsSync(demoSrc)) fs.copyFileSync(demoSrc, path.join(outDir, 'demo.html')); if (fs.existsSync(demoSrc)) fs.copyFileSync(demoSrc, path.join(outDir, 'demo.html'));
fs.writeFileSync(LLMS_FULL, renderAgentDocs(index, tokens)); // llms-full 头部时间戳仅生成时刻之差视为无变化(与 index 幂等化同方案)
const normTs = (s) => s.replace(/自动生成于 [^,]*/, '自动生成于 X');
writeIfChanged(LLMS_FULL, renderAgentDocs(index, tokens), normTs);
console.log( console.log(
`docs generated -> apps/docs/index.html + demo.html, llms-full.txt (${index.packages.length} packages)`, `docs generated -> apps/docs/index.html + demo.html, llms-full.txt (${index.packages.length} packages)`,
); );
@@ -593,6 +606,35 @@ function checkReadme(pkgs) {
return issues; return issues;
} }
/*** 组件数漂移校验:AGENTS.md / ARCHITECTURE.md 中的「N 个组件」必须与 skin-dom 清单实际总数一致 ***/
function checkCountDocs(actual) {
const issues = [];
const re = /(\d+)\s*个(?:\s*DOM\s*)?组件/;
for (const name of ['AGENTS.md', 'ARCHITECTURE.md', 'README.md']) {
const file = path.join(ROOT, name);
if (!fs.existsSync(file)) continue; // 卫语句:文档缺失不校验
fs.readFileSync(file, 'utf8').split(/\r?\n/).forEach((line, i) => {
const m = line.match(re);
if (m && Number(m[1]) !== actual) {
issues.push(`${name}:${i + 1} 组件数「${m[1]}」与清单实际 ${actual} 不一致,请同步`);
}
});
}
return issues;
}
/*** llms-full.txt 同步校验:内联重渲染 Agent 文档(时间戳归一化)与落盘产物比对,防 manifest 改后忘跑 docs ***/
function checkLlmsFull(pkgs, issues) {
if (!fs.existsSync(LLMS_FULL)) return; // 卫语句:产物缺失不算漂移(可能是新仓库)
const tokensPath = path.join(ROOT, 'packages', 'design', 'tokens', 'tokens.json');
const tokens = fs.existsSync(tokensPath) ? JSON.parse(fs.readFileSync(tokensPath, 'utf8')) : null;
const norm = (s) => s.replace(/\r\n/g, '\n').replace(/自动生成于 [^,]*/, '自动生成于 X');
const expected = renderAgentDocs({ generatedAt: '', packages: pkgs }, tokens);
if (norm(fs.readFileSync(LLMS_FULL, 'utf8')) !== norm(expected)) {
issues.push('llms-full.txt 与清单不一致,请运行 npm run docs 重新生成');
}
}
/*** 清单一致性校验:组件文档块的 id / props 说明 / demo 锚点必须与代码和演示产物对齐 ***/ /*** 清单一致性校验:组件文档块的 id / props 说明 / demo 锚点必须与代码和演示产物对齐 ***/
function cmdVerify() { function cmdVerify() {
const issues = []; const issues = [];
@@ -628,7 +670,12 @@ function cmdVerify() {
} }
} }
const skinTotal = pkgs // skin-dom 清单组件总数,供文档数字漂移校验
.filter((p) => p.type === 'skin-dom')
.flatMap((p) => p.docs?.components ?? []).length;
issues.push(...checkCountDocs(skinTotal));
issues.push(...checkReadme(pkgs)); issues.push(...checkReadme(pkgs));
checkLlmsFull(pkgs, issues);
if (issues.length) { if (issues.length) {
for (const it of issues) console.error('verify: ' + it); for (const it of issues) console.error('verify: ' + it);
@@ -638,6 +685,73 @@ function cmdVerify() {
console.log('verify OK (manifest ↔ docs ↔ demo 一致,皮肤无硬编码颜色,README 组件清单同步)'); console.log('verify OK (manifest ↔ docs ↔ demo 一致,皮肤无硬编码颜色,README 组件清单同步)');
} }
/*** 校验 git 提交信息是否符合 COMMITTING.md 规范(参数为 message 文件路径或直接 message 文本),违规退出码 1 ***/
function cmdCommitLint(args) {
// 输入来源:文件路径(commit-msg 钩子传参)或参数拼接
let msg = '';
if (args[0] && fs.existsSync(args[0])) msg = fs.readFileSync(args[0], 'utf8');
else msg = args.join(' ');
if (!msg.trim()) { console.error('commit-lint: 提交信息为空'); process.exit(1); }
const head = msg.split('\n')[0].trimEnd();
// git 自动生成的合并/回滚信息直接放行
if (/^(Merge |Revert |fixup!|squash!)/.test(head)) return console.log('commit-lint OK (git 自动信息,跳过校验)');
const TYPES = ['feat', 'fix', 'docs', 'style', 'refactor', 'test', 'chore', 'iter'];
const m = head.match(/^(\w+)(?:\(([^)]*)\))?: (.+)$/);
if (!m) { console.error('commit-lint: 首行须为 <type>(<scope>): <subject>,规范见 COMMITTING.md'); process.exit(1); }
const [, type, scope, subject] = m;
const problems = [];
// type 枚举
if (!TYPES.includes(type)) problems.push(`type「${type}」不在枚举:${TYPES.join('/')}`);
// iter 提交 scope 必须是 backlog 条目 id
if (type === 'iter' && !/^[a-z]\d{2,}$/.test(scope ?? '')) problems.push('iter 提交的 scope 必须是 backlog 条目 id(如 b02/e03)');
// 非 iter 提交 scope 必填(杜绝 chore:/docs: 裸 type 放行),且仅允许小写字母与连字符
if (type !== 'iter' && !scope) problems.push(`type「${type}」必须带 scope,如 chore(backlog):`);
if (type !== 'iter' && scope && !/^[a-z][a-z-]*$/.test(scope)) problems.push(`scope「${scope}」仅允许小写字母与连字符`);
// subject 长度与标点
if ([...subject].length > 50) problems.push(`subject 超过 50 字符(当前 ${[...subject].length})`);
if (/[。..!!??]$/.test(subject.trim())) problems.push('subject 结尾不加句号');
// body 行宽
for (const line of msg.split('\n').slice(1)) {
if (line.length > 80) problems.push(`body 行超过 80 字符:${line.slice(0, 20)}…`);
}
if (problems.length) { for (const p of problems) console.error('commit-lint: ' + p); process.exit(1); }
console.log(`commit-lint OK (${type}${scope ? `(${scope})` : ''})`);
}
/*** 迭代留痕校验:.ua/iterations.log 每行 JSON 字段与格式必须符合 ITERATION.md 规范 ***/
function cmdLintIterations() {
const file = path.join(INDEX_DIR, 'iterations.log');
if (!fs.existsSync(file)) return console.log('lint-iterations OK (no log)');
const lines = fs.readFileSync(file, 'utf8').split(/\r?\n/).filter((l) => l.trim());
const issues = [];
lines.forEach((line, i) => {
const tag = `iterations.log:${i + 1}`;
let o;
try { o = JSON.parse(line); } catch { issues.push(`${tag} 不是合法 JSON`); return; }
const keys = Object.keys(o);
if (keys.length !== 5 || !['ts', 'id', 'result', 'commit', 'summary'].every((k) => keys.includes(k))) {
issues.push(`${tag} 字段必须恰为 {ts,id,result,commit,summary}(当前 ${keys.join(',')})`);
}
if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\+08:00$/.test(String(o.ts ?? '')) || Number.isNaN(Date.parse(o.ts))) {
issues.push(`${tag} ts 须为可解析的本地 +08:00 ISO 时间`);
}
if (!/^[a-z]\d{2,}$/.test(String(o.id ?? ''))) issues.push(`${tag} id 须为 backlog 条目 id`);
if (o.result !== 'done' && o.result !== 'aborted') issues.push(`${tag} result 须为 done|aborted`);
const commit = String(o.commit ?? '');
if (o.result === 'done' && !/^[0-9a-f]{7,40}$/.test(commit)) issues.push(`${tag} done 条目 commit 须为 7-40 位哈希`);
if (o.result === 'aborted' && commit && !/^[0-9a-f]{7,40}$/.test(commit)) issues.push(`${tag} aborted 条目 commit 须为空或哈希`);
if (!String(o.summary ?? '').trim()) issues.push(`${tag} summary 不能为空`);
});
if (issues.length) {
for (const it of issues) console.error('lint-iterations: ' + it);
console.error(`${issues.length} violation(s)`);
process.exit(1);
}
console.log(`lint-iterations OK (${lines.length} entries)`);
}
// ---- 入口 ---- // ---- 入口 ----
const [cmd, ...args] = process.argv.slice(2); const [cmd, ...args] = process.argv.slice(2);
@@ -664,10 +778,16 @@ switch (cmd) {
case 'verify': case 'verify':
cmdVerify(); cmdVerify();
break; break;
case 'commit-lint':
cmdCommitLint(args);
break;
case 'lint-iterations':
cmdLintIterations();
break;
case 'mcp': case 'mcp':
cmdMcp(); cmdMcp();
break; break;
default: default:
console.log('usage: ua <index|list|info <name>|docs|style|verify|mcp>'); console.log('usage: ua <index|list|info <name>|docs|style|verify|commit-lint|lint-iterations|mcp>');
process.exit(cmd ? 1 : 0); process.exit(cmd ? 1 : 0);
} }
+2 -1
View File
@@ -12,11 +12,12 @@
"ua docs:生成人类文档站 apps/docs/index.html + demo.html 与 Agent 全量文档 llms-full.txt", "ua docs:生成人类文档站 apps/docs/index.html + demo.html 与 Agent 全量文档 llms-full.txt",
"ua style:注释规范门禁(导出必须 /*** ***/、接口字段行尾注释、行注释中文),违规退出码 1", "ua style:注释规范门禁(导出必须 /*** ***/、接口字段行尾注释、行注释中文),违规退出码 1",
"ua verify:清单一致性门禁(id 唯一 / exports 对应 / props 必带 n t d / steps≥3 / demo 锚点存在)", "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" "ua mcp:零依赖 stdio MCP server,暴露 list_components / get_component / search_components"
], ],
"keyTypes": ["ua.manifest.json 清单结构", "MCP tool 入参 schema"] "keyTypes": ["ua.manifest.json 清单结构", "MCP tool 入参 schema"]
}, },
"usage": "node packages/tools/cli/bin/ua.js <index|list|info <name>|docs|style|verify|mcp>;仓库内等价于 npm run index / docs / style / verify", "usage": "node packages/tools/cli/bin/ua.js <index|list|info <name>|docs|style|verify|commit-lint|mcp>;仓库内等价于 npm run index / docs / style / verify",
"conventions": [ "conventions": [
"零依赖:只用 node 标准库,Node >= 18", "零依赖:只用 node 标准库,Node >= 18",
"清单是单一来源:代码注释、文档站、llms-full.txt、MCP 全部从 ua.manifest.json 派生", "清单是单一来源:代码注释、文档站、llms-full.txt、MCP 全部从 ua.manifest.json 派生",
+53
View File
@@ -0,0 +1,53 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
devDependencies:
typescript:
specifier: ^5.6.0
version: 5.9.3
apps/demo:
dependencies:
'@ui-arch/frame':
specifier: workspace:*
version: link:../../packages/core/frame
'@ui-arch/ui':
specifier: workspace:*
version: link:../../packages/skin/ui
packages/core/frame: {}
packages/core/table-core: {}
packages/design/tokens: {}
packages/skin/ui:
dependencies:
'@ui-arch/frame':
specifier: workspace:*
version: link:../../core/frame
'@ui-arch/table-core':
specifier: workspace:*
version: link:../../core/table-core
'@ui-arch/tokens':
specifier: workspace:*
version: link:../../design/tokens
packages/tools/cli: {}
packages:
typescript@5.9.3:
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
engines: {node: '>=14.17'}
hasBin: true
snapshots:
typescript@5.9.3: {}