Compare commits

...

18 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
22 changed files with 534 additions and 49 deletions
+51 -3
View File
@@ -346,7 +346,7 @@
"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": "open",
"status": "done",
"notes": "来源:审计应修 #4;历史 iter 缺 scope 的提交不回改(已发布历史不动),只管增量"
},
{
@@ -354,7 +354,7 @@
"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": "open",
"status": "done",
"notes": "来源:审计应修 #5"
},
{
@@ -362,8 +362,56 @@
"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": "open",
"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 收尾"
}
]
}
+74 -9
View File
@@ -1,6 +1,6 @@
{
"$schema": "ui-arch-registry-index/0.1",
"generatedAt": "2026-10-23T10:41:39.918Z",
"generatedAt": "2026-10-23T11:03:12.689Z",
"repoRoot": ".",
"packages": [
{
@@ -130,7 +130,7 @@
{
"name": "@ui-arch/ui",
"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": [
"components",
"dom",
@@ -143,9 +143,9 @@
"entry": "src/index.ts",
"exports": [
"button(props):按钮,variant = primary | ghost | danger,disabled 支持布尔或信号禁用",
"textField(props):文本输入,value 信号双向绑定,disabled 支持布尔或信号禁用",
"inputNumber(props):数字输入,−/+ 按钮 + 键盘 ↑↓ 调节,value 信号双向绑定,min/max/step 超界自动夹取",
"textarea(props):多行输入,value 信号双向绑定,disabled 支持布尔或信号禁用",
"textField(props):文本输入,value 信号双向绑定,disabled 支持布尔或信号,onInput 输入回调;prefix/suffix 可选渲染为输入框内侧附加块",
"inputNumber(props):数字输入,−/+ 按钮与键盘 ↑↓ 调节,min/max/step 夹取;prefix/suffix 可选渲染为整组前后附加块",
"textarea(props):多行输入,value 信号双向绑定,disabled 支持布尔或信号,rows 控行数;prefix/suffix 可选渲染为上下附加块",
"slider(props):滑块,value 信号双向绑定,min/max/step 可配,原生 range 承载键盘交互,右侧可显示当前值",
"checkbox(props):复选框,checked 信号双向绑定,disabled 支持布尔或信号禁用",
"switchButton(props):开关,checked 信号双向绑定,disabled 支持布尔或信号禁用",
@@ -175,7 +175,8 @@
"drawer(props, ...children):抽屉,open 信号驱动侧滑面板,placement 左右可配、width 可调、footer 操作区,遮罩 / × / Esc 关闭",
"form(props, items):表单容器,布局 + 聚合校验,全部通过才触发 onSubmit",
"formItem(props, control):表单字段,label + 必填红星 + 规则校验 + 错误区,返回 { el, validate } 句柄",
"dataTable(props):数据表格,粘性表头 / 行选择(全选三态)/ 排序 / 筛选 / 分页 / 空状态;loading 为真时数据行换骨架行且筛选/翻页禁用,内核为 table-core"
"dataTable(props):数据表格,粘性表头 / 行选择(全选三态)/ 排序 / 筛选 / 分页 / 空状态;loading 为真时数据行换骨架行且筛选/翻页禁用,内核为 table-core",
"image(props):图片,缩略图点击 / 回车开遮罩大图预览(Esc 或点击遮罩关闭),width 控制缩略宽"
],
"keyTypes": [
"ButtonProps",
@@ -232,6 +233,37 @@
"读写状态:name() 读、name('张三') 写,界面自动同步,无需手动操作 DOM"
],
"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",
"title": "按钮 Button",
@@ -293,13 +325,24 @@
"t": "(value: string) => void",
"k": 1,
"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) })",
"steps": [
"先用 signal('') 创建字符串信号",
"传入 value 完成双向绑定:用户输入自动写回,外部赋值自动回显",
"在任意响应式位置用 name() 读取最新值,如 () => `你好,${name()}`"
"在任意响应式位置用 name() 读取最新值,如 () => `你好,${name()}`",
"需要图标、货币符、单位等前后缀时传 prefix/suffix(字符串或 DOM),控件会包进带边框的附加组,聚焦光环由组统一承载"
]
},
{
@@ -339,13 +382,24 @@
"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 支持布尔或信号禁用"
"非法输入自动回滚为当前值;min/max 超界自动夹取,disabled 支持布尔或信号禁用",
"需要图标、货币符、单位等前后缀时传 prefix/suffix(字符串或 DOM),控件会包进带边框的附加组,聚焦光环由组统一承载"
]
},
{
@@ -379,13 +433,24 @@
"t": "(value: string) => void",
"k": 1,
"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: '一句话介绍…' })",
"steps": [
"创建信号并传入 value",
"用 rows 控制初始高度,用户可纵向拉伸",
"字数统计等派生逻辑直接在响应式文本里读 bio().length"
"字数统计等派生逻辑直接在响应式文本里读 bio().length",
"需要图标、货币符、单位等前后缀时传 prefix/suffix(字符串或 DOM),控件会包进带边框的附加组,聚焦光环由组统一承载"
]
},
{
+9
View File
@@ -42,3 +42,12 @@
{"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,三份文档组件计数文本全部纳入漂移门禁"}
+1 -1
View File
@@ -21,7 +21,7 @@
- `packages/core/table-core`(@ui-arch/table-core):Headless 表格逻辑内核
- `packages/core/frame`(@ui-arch/frame):signal + hyperscript 微框架
- `packages/skin/ui`(@ui-arch/ui):33 个 DOM 组件(表单 / 反馈 / 浮层 / 导航 / 表格),样式只消费令牌
- `packages/skin/ui`(@ui-arch/ui):34 个 DOM 组件(表单 / 反馈 / 浮层 / 导航 / 表格),样式只消费令牌
- `packages/design/tokens`(@ui-arch/tokens):设计令牌
- `packages/tools/cli`(@ui-arch/cli):`ua` 命令行 + MCP 注册表服务(private)
- `apps/demo`(@ui-arch/demo):交互演示,产物 apps/demo/dist/demo.html
+10 -2
View File
@@ -34,16 +34,24 @@ ui-arch-mvp/
│ │ ├── table-core/ # 表格状态内核
│ │ └── frame/ # 信号 + hyperscript 微框架
│ ├── skin/ # 皮肤层(DOM 组件,可发布)
│ │ └── ui/ # 33 个组件 + ui.css
│ │ └── ui/ # 34 个组件 + ui.css
│ ├── design/ # 资产层(可发布)
│ │ └── tokens/ # --ua-* 设计令牌
│ └── tools/ # 工程层(private,不发布)
│ └── cli/ # ua 命令行 + MCP 注册表服务
├── scripts/ # 工程脚本:gitea.mjs(git 推送封装)、gitea-api.mjs(Gitea REST API)
├── ARCHITECTURE.md # 本文件:结构规范
├── AGENTS.md # Agent 接入手册(兼项目入口文档)
├── CODING.md # 编码规范
├── COMMITTING.md # 提交信息规范(commit-lint 机器校验)
├── ITERATION.md # 无人值守自我迭代协议(自动化 Agent 执行)
├── pnpm-workspace.yaml # pnpm 工作区定义
├── pnpm-lock.yaml # 依赖锁定(入库,保证依赖树可复现)
├── 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 机器校验)
```
## 包规范(每包强制)
+1
View File
@@ -31,6 +31,7 @@
- 应用:`demo` / `docs`
- 元数据:`backlog` / `repo` / `protocol`
- `iter` 提交的 scope 固定为条目 id:`b02`、`c01`、`d29` …
- **scope 强制**:除 Merge/Revert 自动信息外,所有 type 必须带 scope;`ua commit-lint` 已机器校验,裸 type(`chore:` / `docs:`)一律拒绝
## subject 规则
+3 -3
View File
@@ -11,10 +11,10 @@ npm run demo # 打开演示:apps/demo/dist/demo.html
npm run docs # 生成人类文档站 apps/docs/index.html + Agent 全量文档 llms-full.txt
```
## 组件(共 33 个)
## 组件(共 34 个)
<!-- ua:components -->
accordion · alert · avatar · badge · breadcrumb · button · card · checkbox · dataTable · divider · drawer · empty · form · inputNumber · menu · modal · pagination · popover · progress · radioGroup · rate · select · skeleton · slider · spinner · stepper · 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 -->
分组:表单(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/table-core` | @ui-arch/table-core:headless 表格(filter → sort → paginate 派生管线) |
| `packages/skin/ui` | @ui-arch/ui:33 个 DOM 组件 + ui.css(只消费 `--ua-*` 令牌) |
| `packages/skin/ui` | @ui-arch/ui:34 个 DOM 组件 + ui.css(只消费 `--ua-*` 令牌) |
| `packages/design/tokens` | @ui-arch/tokens:设计令牌(暗色只换值不换名) |
| `packages/tools/cli` | @ui-arch/cli:`ua` 命令行与 stdio MCP server |
| `apps/demo` | 交互演示(esbuild 打包成单文件) |
+26 -1
View File
@@ -1,4 +1,12 @@
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 {
accordion,
alert,
@@ -12,6 +20,7 @@ import {
divider,
drawer,
empty,
image,
form,
formItem,
inputNumber,
@@ -118,6 +127,8 @@ const volume = signal(40);
// —— 数字输入:购买数量演示,与禁用态实例共用同一信号 ——
const qty = signal(1);
const price = signal('9.9'); // 演示 suffix 附加块
const amount = signal(2); // 演示 inputNumber 前后缀
// —— 步骤条:下单流程演示,按钮推进、点已完成步骤回跳 ——
const stepIdx = signal(0);
@@ -160,8 +171,10 @@ const content = h(
section(
'sec-bind',
'双向绑定',
'textField 与信号双向同步:输入即回显。',
'textField 与信号双向同步:输入即回显;prefix/suffix 渲染为输入框内侧附加块。',
textField({ placeholder: '输入名字…', value: name }),
textField({ placeholder: '搜索…', value: name, prefix: '🔍' }),
textField({ value: price, suffix: '元' }),
h('span', { class: 'out' }, () => (name() ? `你好,${name()}!` : '等待输入…')),
),
section(
@@ -258,6 +271,7 @@ const content = h(
{ 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(
@@ -384,6 +398,17 @@ const content = h(
),
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',
'空状态',
+37 -4
View File
@@ -1,6 +1,6 @@
# @ui-arch 组件库 Agent 文档(无图形版)
> 自动生成于 2026-10-23T10:14:36.972Z,数据源:各包 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)。
## 快速上手
@@ -50,6 +50,29 @@
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
参数:
@@ -81,6 +104,8 @@ button({ label: '提交', variant: 'primary', onClick: () => console.log('clicke
- placeholder:string —— 占位文案
- disabled:boolean | Signal<boolean> —— 禁用态:布尔直接置位,传信号则动态同步
- onInput(回调):(value: string) => void —— 输入回调,每次按键触发
- prefix:string | HTMLElement —— 前置附加块(textField/inputNumber 在输入框内侧前、textarea 在上方);缺省不渲染
- suffix:string | HTMLElement —— 后置附加块(textField/inputNumber 在输入框内侧后、textarea 在下方);缺省不渲染
示例:
@@ -93,6 +118,7 @@ const name = signal(''); const el = textField({ value: name, placeholder: '输
1. 先用 signal('') 创建字符串信号
2. 传入 value 完成双向绑定:用户输入自动写回,外部赋值自动回显
3. 在任意响应式位置用 name() 读取最新值,如 () => `你好,${name()}`
4. 需要图标、货币符、单位等前后缀时传 prefix/suffix(字符串或 DOM),控件会包进带边框的附加组,聚焦光环由组统一承载
现场演示:apps/docs/demo.html#sec-bind
@@ -106,6 +132,8 @@ const name = signal(''); const el = textField({ value: name, placeholder: '输
- step(默认 1):number —— 步长;缺省 1
- disabled:boolean | Signal<boolean> —— 禁用态:布尔直接置位,传信号则动态同步
- onChange(回调):(value: number) => void —— 值变化回调(已夹取到合法区间)
- prefix:string | HTMLElement —— 前置附加块(textField/inputNumber 在输入框内侧前、textarea 在上方);缺省不渲染
- suffix:string | HTMLElement —— 后置附加块(textField/inputNumber 在输入框内侧后、textarea 在下方);缺省不渲染
示例:
@@ -118,6 +146,7 @@ const qty = signal(1); const el = inputNumber({ value: qty, min: 1, max: 99, onC
1. 用 signal(1) 创建数字信号并传入 value 完成双向绑定
2. 点击 −/+ 按钮或在输入框聚焦时按 ↑↓ 以 step 调节;直接输入在失焦或回车时解析
3. 非法输入自动回滚为当前值;min/max 超界自动夹取,disabled 支持布尔或信号禁用
4. 需要图标、货币符、单位等前后缀时传 prefix/suffix(字符串或 DOM),控件会包进带边框的附加组,聚焦光环由组统一承载
现场演示:apps/docs/demo.html#sec-inputnumber
@@ -130,6 +159,8 @@ const qty = signal(1); const el = inputNumber({ value: qty, min: 1, max: 99, onC
- placeholder:string —— 占位文案
- disabled:boolean | Signal<boolean> —— 禁用态:布尔直接置位,传信号则动态同步
- onInput(回调):(value: string) => void —— 输入回调
- prefix:string | HTMLElement —— 前置附加块(textField/inputNumber 在输入框内侧前、textarea 在上方);缺省不渲染
- suffix:string | HTMLElement —— 后置附加块(textField/inputNumber 在输入框内侧后、textarea 在下方);缺省不渲染
示例:
@@ -142,6 +173,7 @@ const bio = signal(''); textarea({ value: bio, rows: 4, placeholder: '一句话
1. 创建信号并传入 value
2. 用 rows 控制初始高度,用户可纵向拉伸
3. 字数统计等派生逻辑直接在响应式文本里读 bio().length
4. 需要图标、货币符、单位等前后缀时传 prefix/suffix(字符串或 DOM),控件会包进带边框的附加组,聚焦光环由组统一承载
现场演示:apps/docs/demo.html#sec-form
@@ -869,9 +901,9 @@ dataTable({ data: users, selectable: true, columns: [{ key: 'name', header: '姓
API 速览:
- button(props):按钮,variant = primary | ghost | danger,disabled 支持布尔或信号禁用
- textField(props):文本输入,value 信号双向绑定,disabled 支持布尔或信号禁用
- inputNumber(props):数字输入,−/+ 按钮 + 键盘 ↑↓ 调节,value 信号双向绑定,min/max/step 超界自动夹取
- textarea(props):多行输入,value 信号双向绑定,disabled 支持布尔或信号禁用
- textField(props):文本输入,value 信号双向绑定,disabled 支持布尔或信号,onInput 输入回调;prefix/suffix 可选渲染为输入框内侧附加块
- inputNumber(props):数字输入,−/+ 按钮与键盘 ↑↓ 调节,min/max/step 夹取;prefix/suffix 可选渲染为整组前后附加块
- textarea(props):多行输入,value 信号双向绑定,disabled 支持布尔或信号,rows 控行数;prefix/suffix 可选渲染为上下附加块
- slider(props):滑块,value 信号双向绑定,min/max/step 可配,原生 range 承载键盘交互,右侧可显示当前值
- checkbox(props):复选框,checked 信号双向绑定,disabled 支持布尔或信号禁用
- switchButton(props):开关,checked 信号双向绑定,disabled 支持布尔或信号禁用
@@ -902,6 +934,7 @@ API 速览:
- form(props, items):表单容器,布局 + 聚合校验,全部通过才触发 onSubmit
- formItem(props, control):表单字段,label + 必填红星 + 规则校验 + 错误区,返回 { el, validate } 句柄
- dataTable(props):数据表格,粘性表头 / 行选择(全选三态)/ 排序 / 筛选 / 分页 / 空状态;loading 为真时数据行换骨架行且筛选/翻页禁用,内核为 table-core
- image(props):图片,缩略图点击 / 回车开遮罩大图预览(Esc 或点击遮罩关闭),width 控制缩略宽
一行用法:
+5 -4
View File
@@ -45,12 +45,12 @@
## @ui-arch/ui (v0.1.0)
- 类型: 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:
- button(props):按钮,variant = primary | ghost | danger,disabled 支持布尔或信号禁用
- textField(props):文本输入,value 信号双向绑定,disabled 支持布尔或信号禁用
- inputNumber(props):数字输入,−/+ 按钮 + 键盘 ↑↓ 调节,value 信号双向绑定,min/max/step 超界自动夹取
- textarea(props):多行输入,value 信号双向绑定,disabled 支持布尔或信号禁用
- textField(props):文本输入,value 信号双向绑定,disabled 支持布尔或信号,onInput 输入回调;prefix/suffix 可选渲染为输入框内侧附加块
- inputNumber(props):数字输入,−/+ 按钮与键盘 ↑↓ 调节,min/max/step 夹取;prefix/suffix 可选渲染为整组前后附加块
- textarea(props):多行输入,value 信号双向绑定,disabled 支持布尔或信号,rows 控行数;prefix/suffix 可选渲染为上下附加块
- slider(props):滑块,value 信号双向绑定,min/max/step 可配,原生 range 承载键盘交互,右侧可显示当前值
- checkbox(props):复选框,checked 信号双向绑定,disabled 支持布尔或信号禁用
- switchButton(props):开关,checked 信号双向绑定,disabled 支持布尔或信号禁用
@@ -81,6 +81,7 @@
- form(props, items):表单容器,布局 + 聚合校验,全部通过才触发 onSubmit
- formItem(props, control):表单字段,label + 必填红星 + 规则校验 + 错误区,返回 { el, validate } 句柄
- dataTable(props):数据表格,粘性表头 / 行选择(全选三态)/ 排序 / 筛选 / 分页 / 空状态;loading 为真时数据行换骨架行且筛选/翻页禁用,内核为 table-core
- image(props):图片,缩略图点击 / 回车开遮罩大图预览(Esc 或点击遮罩关闭),width 控制缩略宽
- 用法: `const checked = signal(false); checkbox({ label: '同意', checked }); const open = signal(false); modal({ title: '确认', open }, '内容')`
- 约定: 皮肤不持有业务状态,一律委托 headless 内核或调用方信号
- 约定: 表单类组件统一约定:*Props.value/checked/active 为 Signal 双向绑定,onChange/onInput 为回调
+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({
placeholder: `筛选 ${filterKey}…`,
prefix: '🔍',
disabled: props.loading, // 加载中禁止筛选,避免与在途结果竞争
onInput: (value) => {
table.dispatch({ type: 'setFilter', key: filterKey, value });
+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);
}
+1
View File
@@ -15,6 +15,7 @@ export { card } from './card.js';
export { badge } from './badge.js';
export { tag } from './tag.js';
export { avatar } from './avatar.js';
export { image } from './image.js';
export { alert } from './alert.js';
export { toast, showToast } from './toast.js';
export { tooltip } from './tooltip.js';
+3 -2
View File
@@ -1,9 +1,10 @@
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): HTMLDivElement {
export function inputNumber(props: InputNumberProps): HTMLElement {
const min = props.min ?? -Infinity; // 无下限缺省
const max = props.max ?? Infinity; // 无上限缺省
const step = props.step ?? 1; // 步长缺省 1
@@ -22,5 +23,5 @@ export function inputNumber(props: InputNumberProps): HTMLDivElement {
const inc = button({ label: '+', variant: 'ghost', disabled: props.disabled, onClick: () => emit(props.value() + step) });
dec.setAttribute('aria-label', '减少');
inc.setAttribute('aria-label', '增加');
return h('div', { class: 'ua-input-number', role: 'group' }, dec, input, inc) as HTMLDivElement;
return addonWrap(h('div', { class: 'ua-input-number', role: 'group' }, dec, input, inc) as HTMLDivElement, props.prefix, props.suffix);
}
+4 -3
View File
@@ -1,8 +1,9 @@
import { effect, h } from '@ui-arch/frame';
import { addonWrap } from './addons.js';
import type { TextFieldProps } from './types.js';
/*** 文本输入:value 信号双向绑定,disabled 支持布尔或信号禁用,onInput 为输入回调 ***/
export function textField(props: TextFieldProps): HTMLInputElement {
/*** 文本输入:value 信号双向绑定,disabled 支持布尔或信号禁用,onInput 为输入回调;prefix/suffix 包附加组 ***/
export function textField(props: TextFieldProps): HTMLElement {
const el = h('input', { class: 'ua-input', placeholder: props.placeholder ?? '', disabled: props.disabled }) as HTMLInputElement; // false/undefined 移除属性,Signal 走响应式
if (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));
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 { addonWrap } from './addons.js';
import type { TextareaProps } from './types.js';
/*** 多行输入:value 信号双向绑定,disabled 支持布尔或信号禁用,onInput 为输入回调 ***/
export function textarea(props: TextareaProps): HTMLTextAreaElement {
/*** 多行输入:value 信号双向绑定,disabled 支持布尔或信号禁用,onInput 为输入回调;prefix/suffix 包纵向附加组 ***/
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 走响应式
if (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));
return el;
return addonWrap(el, props.prefix, props.suffix, true);
}
+13
View File
@@ -32,6 +32,8 @@ export interface TextFieldProps {
value?: Signal<string>; // 双向绑定的信号
disabled?: boolean | Signal<boolean>; // 禁用态:布尔直接置位,传信号则动态同步
onInput?: (value: string) => void; // 输入回调
prefix?: string | HTMLElement; // 输入框内侧前置附加块(如图标、货币符);缺省不渲染
suffix?: string | HTMLElement; // 输入框内侧后置附加块(如单位);缺省不渲染
}
/*** 卡片入参 ***/
@@ -163,6 +165,8 @@ export interface TextareaProps {
value?: Signal<string>; // 双向绑定的信号
disabled?: boolean | Signal<boolean>; // 禁用态:布尔直接置位,传信号则动态同步
onInput?: (value: string) => void; // 输入回调
prefix?: string | HTMLElement; // 上方附加块(如标题、计数说明);缺省不渲染
suffix?: string | HTMLElement; // 下方附加块(如字数提示);缺省不渲染
}
/*** 数字输入入参 ***/
@@ -173,6 +177,8 @@ export interface InputNumberProps {
step?: number; // 步长;缺省 1
disabled?: boolean | Signal<boolean>; // 禁用态:布尔直接置位,传信号则动态同步
onChange?: (value: number) => void; // 值变化回调(已夹取到合法区间)
prefix?: string | HTMLElement; // −/+ 按钮前的附加块(如货币符);缺省不渲染
suffix?: string | HTMLElement; // +/+ 按钮后的附加块(如单位);缺省不渲染
}
/*** 步骤条条目 ***/
@@ -233,6 +239,13 @@ export interface TagProps {
onClose?: () => void; // 关闭回调(调用方自行从数据源移除该项)
}
/*** 图片入参 ***/
export interface ImageProps {
src: string; // 图片地址(缩略图与大图共用)
alt?: string; // 替代文本;缺省空串
width?: number; // 缩略图宽度像素,高度自适应;缺省走样式默认
}
/*** 提示条入参 ***/
export interface AlertProps {
message: string; // 提示内容
+73 -8
View File
@@ -1,7 +1,7 @@
{
"name": "@ui-arch/ui",
"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": [
"components",
"dom",
@@ -14,9 +14,9 @@
"entry": "src/index.ts",
"exports": [
"button(props):按钮,variant = primary | ghost | danger,disabled 支持布尔或信号禁用",
"textField(props):文本输入,value 信号双向绑定,disabled 支持布尔或信号禁用",
"inputNumber(props):数字输入,−/+ 按钮 + 键盘 ↑↓ 调节,value 信号双向绑定,min/max/step 超界自动夹取",
"textarea(props):多行输入,value 信号双向绑定,disabled 支持布尔或信号禁用",
"textField(props):文本输入,value 信号双向绑定,disabled 支持布尔或信号,onInput 输入回调;prefix/suffix 可选渲染为输入框内侧附加块",
"inputNumber(props):数字输入,−/+ 按钮与键盘 ↑↓ 调节,min/max/step 夹取;prefix/suffix 可选渲染为整组前后附加块",
"textarea(props):多行输入,value 信号双向绑定,disabled 支持布尔或信号,rows 控行数;prefix/suffix 可选渲染为上下附加块",
"slider(props):滑块,value 信号双向绑定,min/max/step 可配,原生 range 承载键盘交互,右侧可显示当前值",
"checkbox(props):复选框,checked 信号双向绑定,disabled 支持布尔或信号禁用",
"switchButton(props):开关,checked 信号双向绑定,disabled 支持布尔或信号禁用",
@@ -46,7 +46,8 @@
"drawer(props, ...children):抽屉,open 信号驱动侧滑面板,placement 左右可配、width 可调、footer 操作区,遮罩 / × / Esc 关闭",
"form(props, items):表单容器,布局 + 聚合校验,全部通过才触发 onSubmit",
"formItem(props, control):表单字段,label + 必填红星 + 规则校验 + 错误区,返回 { el, validate } 句柄",
"dataTable(props):数据表格,粘性表头 / 行选择(全选三态)/ 排序 / 筛选 / 分页 / 空状态;loading 为真时数据行换骨架行且筛选/翻页禁用,内核为 table-core"
"dataTable(props):数据表格,粘性表头 / 行选择(全选三态)/ 排序 / 筛选 / 分页 / 空状态;loading 为真时数据行换骨架行且筛选/翻页禁用,内核为 table-core",
"image(props):图片,缩略图点击 / 回车开遮罩大图预览(Esc 或点击遮罩关闭),width 控制缩略宽"
],
"keyTypes": [
"ButtonProps",
@@ -103,6 +104,37 @@
"读写状态:name() 读、name('张三') 写,界面自动同步,无需手动操作 DOM"
],
"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",
"title": "按钮 Button",
@@ -164,13 +196,24 @@
"t": "(value: string) => void",
"k": 1,
"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) })",
"steps": [
"先用 signal('') 创建字符串信号",
"传入 value 完成双向绑定:用户输入自动写回,外部赋值自动回显",
"在任意响应式位置用 name() 读取最新值,如 () => `你好,${name()}`"
"在任意响应式位置用 name() 读取最新值,如 () => `你好,${name()}`",
"需要图标、货币符、单位等前后缀时传 prefix/suffix(字符串或 DOM),控件会包进带边框的附加组,聚焦光环由组统一承载"
]
},
{
@@ -210,13 +253,24 @@
"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 支持布尔或信号禁用"
"非法输入自动回滚为当前值;min/max 超界自动夹取,disabled 支持布尔或信号禁用",
"需要图标、货币符、单位等前后缀时传 prefix/suffix(字符串或 DOM),控件会包进带边框的附加组,聚焦光环由组统一承载"
]
},
{
@@ -250,13 +304,24 @@
"t": "(value: string) => void",
"k": 1,
"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: '一句话介绍…' })",
"steps": [
"创建信号并传入 value",
"用 rows 控制初始高度,用户可纵向拉伸",
"字数统计等派生逻辑直接在响应式文本里读 bio().length"
"字数统计等派生逻辑直接在响应式文本里读 bio().length",
"需要图标、货币符、单位等前后缀时传 prefix/suffix(字符串或 DOM),控件会包进带边框的附加组,聚焦光环由组统一承载"
]
},
{
+82
View File
@@ -1565,3 +1565,85 @@ button.ua-step-dot:hover {
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;
}
+33 -6
View File
@@ -56,6 +56,13 @@ function collectManifests() {
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 ***/
function cmdIndex() {
const packages = collectManifests();
@@ -66,7 +73,11 @@ function cmdIndex() {
repoRoot: '.',
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 = [
'# @ui-arch 私人组件库索引',
@@ -87,8 +98,8 @@ function cmdIndex() {
for (const c of p.conventions ?? []) lines.push(`- 约定: ${c}`);
lines.push('');
}
fs.writeFileSync(LLMS_FILE, lines.join('\n'));
console.log(`indexed ${packages.length} package(s) -> .ua/index.json, llms.txt`);
writeIfChanged(LLMS_FILE, lines.join('\n'));
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));
const demoSrc = path.join(ROOT, 'apps', 'demo', 'dist', 'demo.html'); // 演示页拷入文档站供 iframe
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(
`docs generated -> apps/docs/index.html + demo.html, llms-full.txt (${index.packages.length} packages)`,
);
@@ -597,7 +610,7 @@ function checkReadme(pkgs) {
function checkCountDocs(actual) {
const issues = [];
const re = /(\d+)\s*个(?:\s*DOM\s*)?组件/;
for (const name of ['AGENTS.md', 'ARCHITECTURE.md']) {
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) => {
@@ -610,6 +623,18 @@ function checkCountDocs(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 锚点必须与代码和演示产物对齐 ***/
function cmdVerify() {
const issues = [];
@@ -650,6 +675,7 @@ function cmdVerify() {
.flatMap((p) => p.docs?.components ?? []).length;
issues.push(...checkCountDocs(skinTotal));
issues.push(...checkReadme(pkgs));
checkLlmsFull(pkgs, issues);
if (issues.length) {
for (const it of issues) console.error('verify: ' + it);
@@ -680,7 +706,8 @@ function cmdCommitLint(args) {
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 仅允许小写字母与连字符
// 非 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})`);
+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: {}