Compare commits
76 Commits
main
..
86c72f7090
| Author | SHA1 | Date | |
|---|---|---|---|
| 86c72f7090 | |||
| c0bb8d705d | |||
| 5131977f54 | |||
| bfa70132da | |||
| d5ac26af31 | |||
| 33979b23a0 | |||
| 0c42c6f71e | |||
| 98f557cba8 | |||
| 7ce8bf72d4 | |||
| a5439fe4a6 | |||
| 35c9f00e26 | |||
| 265060fdb3 | |||
| fbe0348b47 | |||
| 99ce0cdd63 | |||
| f1363e81f7 | |||
| f57da4c74e | |||
| 3fb3cd2395 | |||
| f2fc286eeb | |||
| b9b05f53fb | |||
| c19f83b413 | |||
| 67374c78c3 | |||
| cbf1d4a248 | |||
| c457e0cc33 | |||
| 84d008b5fd | |||
| fd4ee018e6 | |||
| 62d4ce648d | |||
| ee20322e3f | |||
| dc4bac4d17 | |||
| 7072f77f88 | |||
| 4f7d1e86b3 | |||
| fc7a1e8299 | |||
| 47bea0b69c | |||
| 20d7b9440f | |||
| d7226f452d | |||
| b89d1cd801 | |||
| f8934ba84a | |||
| c8154b4099 | |||
| 8e168293ec | |||
| fac74a31e8 | |||
| 734017a627 | |||
| 82f48aaca1 | |||
| 6a5e6623fe | |||
| 6efe3ab57b | |||
| f361a1933d | |||
| f86da9866c | |||
| 00a6104f84 | |||
| 9cfdb43a4f | |||
| 4555542811 | |||
| 4012923ff1 | |||
| bdc1fec3f9 | |||
| fa168b9a1c | |||
| 1c03c8113a | |||
| d3fc754b33 | |||
| 730ee1abde | |||
| 9eefdcbd75 | |||
| d1a2864470 | |||
| a08f157e5e | |||
| a11157b80a | |||
| 1a4b2c1fed | |||
| fb9ab88feb | |||
| 582318adf1 | |||
| 60c052439f | |||
| ccbdcb9271 | |||
| df2fb34e63 | |||
| 6b9fa95d1e | |||
| 84277a744e | |||
| f9324ddc6a | |||
| 2a5892a88c | |||
| 83bc1e241c | |||
| 8c9a63b5d2 | |||
| 28820574e8 | |||
| ece38bcac9 | |||
| aaf6fdc5ac | |||
| a57577a995 | |||
| e9165ead18 | |||
| 17a4fb171b |
@@ -5,3 +5,5 @@ dist/
|
|||||||
apps/docs/*.html
|
apps/docs/*.html
|
||||||
.workbuddy/
|
.workbuddy/
|
||||||
.gitea-token
|
.gitea-token
|
||||||
|
.ua/loop.lock
|
||||||
|
.ua/heartbeat*
|
||||||
|
|||||||
+373
-9
@@ -6,40 +6,404 @@
|
|||||||
"title": "新增 tooltip 组件",
|
"title": "新增 tooltip 组件",
|
||||||
"motivation": "皮肤包 16 组件缺少悬浮提示,表单与工具栏场景常用;可复用自绘弹层与微动画既有模式",
|
"motivation": "皮肤包 16 组件缺少悬浮提示,表单与工具栏场景常用;可复用自绘弹层与微动画既有模式",
|
||||||
"acceptance": "packages/skin/ui 新增 tooltip.ts:content/message 简单 API,hover/focus 触发,延迟约 150ms 出场,带出入场微动画与 prefers-reduced-motion 兜底,只消费 --ua-* 令牌;导出并注册进 ua.manifest.json,文档站 props 表与 llms-full.txt 同步更新;npm run check 全绿",
|
"acceptance": "packages/skin/ui 新增 tooltip.ts:content/message 简单 API,hover/focus 触发,延迟约 150ms 出场,带出入场微动画与 prefers-reduced-motion 兜底,只消费 --ua-* 令牌;导出并注册进 ua.manifest.json,文档站 props 表与 llms-full.txt 同步更新;npm run check 全绿",
|
||||||
"status": "open",
|
"status": "done",
|
||||||
"notes": ""
|
"notes": "API 定为 tooltip(props, trigger) 包裹式;delay 默认 150ms,Esc 立即收起,气泡 pointer-events:none 防抖,aria-describedby + role=tooltip 保证读屏可读;demo 新增 sec-tooltip 区块。顺带修复 ua style 对 https:// 的误判(先剥离字面量再定位 //)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "b02",
|
"id": "b02",
|
||||||
"title": "新增 toast 消息提示组件",
|
"title": "新增 toast 消息提示组件",
|
||||||
"motivation": "alert 是常驻块级元素,缺少轻量转瞬即逝的全局反馈;操作成功/失败场景刚需",
|
"motivation": "alert 是常驻块级元素,缺少轻量转瞬即逝的全局反馈;操作成功/失败场景刚需",
|
||||||
"acceptance": "新增 toast.ts:toast.success/error/warning/info 四个工厂函数,入参 {message, duration?},默认 3s 自动消失,右上角堆叠,语义色复用四档 variant,出入场微动画 + prefers-reduced-motion 兜底;导出、manifest、文档、llms-full.txt 同步;npm run check 全绿",
|
"acceptance": "新增 toast.ts:toast.success/error/warning/info 四个工厂函数,入参 {message, duration?},默认 3s 自动消失,右上角堆叠,语义色复用四档 variant,出入场微动画 + prefers-reduced-motion 兜底;导出、manifest、文档、llms-full.txt 同步;npm run check 全绿",
|
||||||
"status": "open",
|
"status": "done",
|
||||||
"notes": ""
|
"notes": "对外暴露 toast.info/success/warning/error(message, duration?) 四工厂 + showToast(props) 完整入参版;全局堆叠容器懒创建,role=status 无障碍;退场用 transitionend 移除并带 320ms 兜底(覆盖 reduced-motion 下 transition 不触发的情况);样式复用 alert 语义色 color-mix 模式,reduced-motion 列表已补 .ua-toast;demo 新增 sec-toast 区块"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "b03",
|
"id": "b03",
|
||||||
"title": "新增 skeleton 骨架屏组件",
|
"title": "新增 skeleton 骨架屏组件",
|
||||||
"motivation": "异步加载占位是数据表格与卡片的标配,配合 dataTable 空状态形成完整加载体验",
|
"motivation": "异步加载占位是数据表格与卡片的标配,配合 dataTable 空状态形成完整加载体验",
|
||||||
"acceptance": "新增 skeleton.ts:skeleton({variant: 'text'|'rect'|'circle', rows?}),呼吸/扫光微动画 + prefers-reduced-motion 静态化,只消费 --ua-* 令牌;导出、manifest、文档、llms-full.txt 同步;demo 页加一节演示;npm run check 全绿",
|
"acceptance": "新增 skeleton.ts:skeleton({variant: 'text'|'rect'|'circle', rows?}),呼吸/扫光微动画 + prefers-reduced-motion 静态化,只消费 --ua-* 令牌;导出、manifest、文档、llms-full.txt 同步;demo 页加一节演示;npm run check 全绿",
|
||||||
"status": "open",
|
"status": "done",
|
||||||
"notes": ""
|
"notes": "skeleton({variant, rows}):text(多行,末行缩短)/ rect / circle 三形状,扫光动画(::after 位移 + 渐变),reduced-motion 下静态灰块(动画列表已补 .ua-skeleton 与 ::after);demo 新增 sec-skeleton 卡片组合演示"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "b04",
|
"id": "b04",
|
||||||
"title": "新增独立 pagination 分页器组件并与 dataTable 演示联动",
|
"title": "新增独立 pagination 分页器组件并与 dataTable 演示联动",
|
||||||
"motivation": "table-core 已有 pagination 插件,但皮肤包没有独立分页器;dataTable 演示也未展示分页联动",
|
"motivation": "table-core 已有 pagination 插件,但皮肤包没有独立分页器;dataTable 演示也未展示分页联动",
|
||||||
"acceptance": "新增 pagination.ts:受控 API {page, pageSize, total, onChange},页码过多时折叠省略号,键盘可达,禁用态完整;demo 页 dataTable 一节增加分页联动演示;导出、manifest、文档、llms-full.txt 同步;npm run check 全绿",
|
"acceptance": "新增 pagination.ts:受控 API {page, pageSize, total, onChange},页码过多时折叠省略号,键盘可达,禁用态完整;demo 页 dataTable 一节增加分页联动演示;导出、manifest、文档、llms-full.txt 同步;npm run check 全绿",
|
||||||
"status": "open",
|
"status": "done",
|
||||||
"notes": ""
|
"notes": "pagination(props):page 信号双向绑定(受控),≤7 页全展开、更多时首尾 + 当前页 ±1 折叠省略号(renderList 重建),原生 button 键盘可达,aria-current / aria-label 完整,首末页禁用 ‹ ›;dataTable 新增可选 page 信号桥接(双向同步 + 越界钳制,防无效派发);demo sec-table 新增 pageSize 3 × 24 行的联动卡片"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "b05",
|
"id": "b05",
|
||||||
"title": "文档站演示区由 manifest 驱动自动生成",
|
"title": "文档站演示区由 manifest 驱动自动生成",
|
||||||
"motivation": "目前 apps/docs/index.html 手写 props 表与演示,每加组件都要人肉同步;由 manifest 单一来源驱动可消除重复维护",
|
"motivation": "目前 apps/docs/index.html 手写 props 表与演示,每加组件都要人肉同步;由 manifest 单一来源驱动可消除重复维护",
|
||||||
"acceptance": "ua CLI 新增 docs-gen 逻辑:读取各组件 ua.manifest.json 中的 components 参数表,自动生成 props 表 HTML 片段注入文档站模板;生成结果与现有手写版信息量对齐(props 名/类型/默认值/说明);npm run check 全绿且文档站可正常打开",
|
"acceptance": "ua CLI 新增 docs-gen 逻辑:读取各组件 ua.manifest.json 中的 components 参数表,自动生成 props 表 HTML 片段注入文档站模板;生成结果与现有手写版信息量对齐(props 名/类型/默认值/说明);npm run check 全绿且文档站可正常打开",
|
||||||
|
"status": "done",
|
||||||
|
"notes": "核查确认 props 表/示例/步骤/演示 iframe 早已由 ua docs 从 manifest docs.components 单一来源驱动;真正的缺口是信息量不含默认值——本轮 CLI propsTable 新增「默认值」列(props 条目可选 df 字段,缺省渲染 —),llms-full.txt 参数行同步追加「默认 x」标注;16 处可机读默认值从描述文案提取进 df;check 全绿 + 文档站结构完整性校验通过"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "c01",
|
||||||
|
"title": "button / checkbox / switch 补 disabled 禁用态",
|
||||||
|
"motivation": "三个基础交互组件缺禁用能力,表单只读/权限控制场景刚需",
|
||||||
|
"acceptance": "ButtonProps / CheckboxProps / SwitchProps 新增可选 disabled(boolean | Signal<boolean>):静态布尔与信号绑定都支持,禁用时事件不触发、置灰 + not-allowed 光标;demo 表单区加静态禁用与信号联动示例;manifest/文档/llms-full.txt 同步;npm run check 全绿",
|
||||||
|
"status": "done",
|
||||||
|
"notes": "零新增辅助代码:复用 frame h() 对 false/undefined 移除属性、Signal 函数走响应式绑定的既有语义,disabled 透传即可;CSS 补通用禁用态(opacity 0.45 + not-allowed,开关轨道同级置灰);demo 用同一 locked 信号绑定 switch 的 checked 与 checkbox 的 disabled 展示联动"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "c02",
|
||||||
|
"title": "textField / textarea 补 disabled 禁用态",
|
||||||
|
"motivation": "输入类组件与 c01 的交互组件对齐,表单只读场景闭环",
|
||||||
|
"acceptance": "TextFieldProps / TextareaProps 新增可选 disabled(boolean | Signal<boolean>),禁用时不响应输入、置灰 + not-allowed 光标;demo 禁用示例区补一个信号绑定输入框;manifest/文档/llms-full.txt 同步;npm run check 全绿",
|
||||||
|
"status": "done",
|
||||||
|
"notes": "与 c01 同模式:disabled 透传 h() 即得布尔/信号双支持;禁用态 CSS 追加 .ua-input / .ua-textarea;demo 复用 locked 信号联动开关与输入框"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "d01",
|
||||||
|
"title": "新增下拉菜单 menu 组件",
|
||||||
|
"motivation": "工具栏/行操作场景缺省可折叠的动作集合,select 只能选值不能派发动作;可复用自绘弹层与微动画既有模式",
|
||||||
|
"acceptance": "packages/skin/ui 新增 menu.ts:触发器 + 自绘弹层,items 支持 disabled / danger / divider,点条目 / 点外部 / Esc 关闭,↑↓ 环形导航(禁用项跳过),Enter/Space 选中,Esc 收起后焦点回触发器;只消费 --ua-* 令牌并带 prefers-reduced-motion 兜底;导出 + manifest + 文档 + llms-full.txt + demo 同步;npm run check 全绿",
|
||||||
|
"status": "done",
|
||||||
|
"notes": "API menu({ label, items, variant, align, onSelect });MenuItem = { value, label, disabled?, danger?, divider? };弹层 ua-open 同时挂在根与弹层(根管箭头旋转、弹层管出入场),危险条目取 danger 色;键盘 ↑↓ 环形导航 + Esc 回焦触发器;demo 新增 sec-menu 双示例(含禁用/危险/分隔线与右对齐)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "d02",
|
||||||
|
"title": "select / radioGroup 补 disabled,修复 select 箭头旋转失效",
|
||||||
|
"motivation": "c01/c02 已覆盖 button/checkbox/switch/输入类,选择类仍缺禁用;同时发现 select 的 .ua-select.ua-open 选择器永不命中(ua-open 只挂在弹层),展开时箭头不旋转",
|
||||||
|
"acceptance": "SelectProps / RadioGroupProps 新增可选 disabled(boolean | Signal<boolean>);禁用时 select 触发器置灰且不展开弹层,radioGroup 整组置灰不可选;select 箭头在展开时旋转 180°;demo 加信号禁用与静态禁用示例;manifest/文档/llms-full.txt 同步;npm run check 全绿",
|
||||||
|
"status": "done",
|
||||||
|
"notes": "disabled 仍走 h() 透传(原生 button/input 拦截交互)零辅助代码;select 根节点补 class 函数加 ua-open,箭头旋转选择器命中;CSS 通用禁用态列表并入 .ua-select-trigger 与 .ua-radio,另加 :has() 让单选行整行弱化、禁用触发器 hover 不改边框色"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "d03",
|
||||||
|
"title": "新增折叠面板 accordion 组件",
|
||||||
|
"motivation": "长表单/设置页需要分区收起,皮肤包无折叠容器;grid 行高过渡可零测量实现展开动画",
|
||||||
|
"acceptance": "新增 accordion.ts:items(value/title/content/disabled)+ 单开(手风琴)与 multiple 多开,active 信号可选受控,标题为原生 button(aria-expanded/controls + region 关联),内容区 0fr→1fr 过渡且 reduced-motion 静态化;导出 / manifest / 文档 / llms-full.txt / demo 同步;npm run check 全绿",
|
||||||
|
"status": "done",
|
||||||
|
"notes": "contents 走 appendChild 既有语义,字符串/DOM/响应式函数通吃;未传 active 时内部 signal<string[]> 持有展开集合,受控模式只认 active 单开;CSS 用 grid-template-rows 0fr→1fr 过渡免测量,reduced-motion 列表补 ua-acc-*;demo 新增 sec-accordion(多开 + 禁用面板)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "d04",
|
||||||
|
"title": "dataTable 接加载态(骨架行 + 交互禁用)",
|
||||||
|
"motivation": "异步取数时表格直接闪空状态,缺少 loading 反馈;skeleton 已就位但未与表格打通",
|
||||||
|
"acceptance": "DataTableProps 新增 loading?: boolean | Signal<boolean>;为真时表体渲染骨架行(行数随 pageSize,列数含选择列)、筛选输入与上下页按钮禁用、容器 aria-busy=true;demo 加开关演示;manifest/文档/llms-full.txt 同步;npm run check 全绿",
|
||||||
|
"status": "done",
|
||||||
|
"notes": "busy() 读取器兼顾布尔与信号(信号在 renderList 内调用可被依赖收集);骨架行数量 Math.min(pageSize,6) 防过长;加载态优先于数据判断,避免闪出旧行;disabled 仍走 props 透传,零新增辅助组件"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "d05",
|
||||||
|
"title": "新增面包屑 breadcrumb 组件",
|
||||||
|
"motivation": "多层级页面的位置指示缺失;顺带补齐导航类组件的语义结构(nav/ol/li + aria-current)",
|
||||||
|
"acceptance": "新增 breadcrumb.ts:nav + ol 语义结构,末项自动为当前页(不可点、aria-current=page),条目有 href 渲染 <a>、无 href 渲染 button 走 onNavigate,分隔符可配;只消费 --ua-* 令牌;导出 / manifest / 文档 / llms-full.txt / demo 同步;npm run check 全绿",
|
||||||
|
"status": "done",
|
||||||
|
"notes": "API breadcrumb({ items: {label, href?}[], separator?, onNavigate? });分隔符 aria-hidden;reduced-motion 列表补 .ua-crumb-link;demo 新增 sec-breadcrumb('›' 分隔 + toast 反馈跳转);首轮 style 门禁拦下多行 /*** 注释(检查器只认紧邻单行块),已改单行"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "d06",
|
||||||
|
"title": "门禁新增 ua verify:清单 ↔ 文档 ↔ 演示锚点一致性校验",
|
||||||
|
"motivation": "新增组件靠人工同步 manifest/docs/demo,易漏(本轮已出现 badge/alert/spinner 手把手只有 2 步);把不变量机器化,防回归",
|
||||||
|
"acceptance": "CLI 新增 verify 子命令:docs.components 的 id 唯一且 api.exports 有 `${id}(` 或以 `${id}.` 开头条目、props 条目必备 n/t/d、steps ≥3、example 必填、demo 锚点必须存在于 apps/demo/main.ts;纳入 npm run check 链条;AGENTS.md 同步;npm run check 全绿",
|
||||||
|
"status": "done",
|
||||||
|
"notes": "锚点校验改看演示源码而非打包产物(demo.html 是 esbuild 产物,锚点在运行时生成);顺带补齐 badge/alert/spinner 的第 3 条手把手(说明与 button/skeleton 的分工);check 链变为 build→style→typecheck→smoke→index→verify"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "d07",
|
||||||
|
"title": "新增可交互气泡卡片 popover 组件",
|
||||||
|
"motivation": "tooltip 只做纯展示(pointer-events:none),筛选面板/快捷操作需要可点击的浮层;与 tooltip 形成展示/交互分工",
|
||||||
|
"acceptance": "新增 popover.ts:popover(props, trigger) 包裹式,默认点击开合,trigger:'hover' 走延迟出场,点外部 / Esc 收起(Esc 回焦触发器),content 支持任意 DOM 且内部点击不误关,onOpenChange 回调;aria-haspopup/aria-expanded 正确;只消费 --ua-* 令牌 + reduced-motion 兜底;导出 / manifest / 文档 / llms-full.txt / demo 同步;npm run check 全绿",
|
||||||
|
"status": "done",
|
||||||
|
"notes": "触发器 click 内 stopPropagation,避免刚开就被 document 的外部点击关掉;aria-expanded 用 effect 写回(setAttribute 不吃函数值);弹层点击已阻止冒泡;demo 新增 sec-popover(含 checkbox+button 的筛选面板与 hover 触发示例)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "d08",
|
||||||
|
"title": "演示页加侧边目录与滚动高亮",
|
||||||
|
"motivation": "演示页已 14 个区块,长页滚动找区块靠手滑;目录同时给文档站 iframe 外的直接访问提供导航",
|
||||||
|
"acceptance": "demo 页改为 shell 双栏布局(左 sticky 目录 + 右内容),目录项挂载后从 .sec 自动反推(不手写清单),IntersectionObserver 高亮当前区块,窄屏(≤820px)隐藏目录,reduced-motion 下关平滑滚动;npm run check 全绿、demo 可正常构建",
|
||||||
|
"status": "done",
|
||||||
|
"notes": "目录由 buildToc 在 mount 后扫描 section.sec 生成(id + h2 文案),新增区块自动入目录无需维护;rootMargin -10%/-80% 让高亮更跟手;.page 去掉 max-width 由 .shell 统一控宽 1160"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "d09",
|
||||||
|
"title": "新增滑块 slider 组件",
|
||||||
|
"motivation": "设置类场景(音量/阈值/预算)需要范围输入,皮肤包只有文本类输入;原生 range 自带键盘与触屏,接入成本低",
|
||||||
|
"acceptance": "新增 slider.ts:value 信号双向绑定(拖动写回 + 外部赋值同步),min/max/step 可配,disabled 支持布尔或信号,showValue 可关,onChange 回调;样式只消费 --ua-* 令牌(accent-color)且带聚焦光环与禁用态;导出 / manifest / 文档 / llms-full.txt / demo 同步;npm run check 全绿",
|
||||||
|
"status": "done",
|
||||||
|
"notes": "组件不持有数值,一律委托调用方信号;effect 内等值不重写 input.value,避免拖动过程被打断;轨道配色用 accent-color 令牌化,无需自绘 thumb;demo 新增 sec-slider(音量 step 5 + 与 locked 信号联动的禁用滑块)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "d10",
|
||||||
|
"title": "toast 补手动关闭与同屏堆叠上限",
|
||||||
|
"motivation": "自动消失前用户无法提前关掉(如已看清错误),且连续触发会无限堆叠淹没页面",
|
||||||
|
"acceptance": "ToastProps 新增 closable(显示 × 立即退场);同屏最多 4 条,超出挤掉最早一条;退场逻辑抽为内部 dismiss() 供自动/手动共用;is-closable 才接收指针事件,其余保持 pointer-events:none;manifest/文档/llms-full.txt/demo 同步;npm run check 全绿",
|
||||||
|
"status": "done",
|
||||||
|
"notes": "dismiss() 统一 transitionend + 320ms 双保险;超限用 firstElementChild.remove() 立即腾位(不走动画,保证硬上限);demo sec-toast 增加「可关闭」按钮演示 showToast({closable:true})"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "d11",
|
||||||
|
"title": "modal 补 size 档位与 footer 操作区",
|
||||||
|
"motivation": "弹窗内容宽窄差异大(确认框 vs 表单),底部操作按钮只能塞进 body 导致分割线语义缺失",
|
||||||
|
"acceptance": "ModalProps 新增 size?: 'sm'|'md'|'lg'(340/420/640)与 footer?: unknown;footer 渲染右对齐操作区带上分割线;缺省行为与旧版一致;manifest/文档/llms-full.txt/demo 同步;npm run check 全绿",
|
||||||
|
"status": "done",
|
||||||
|
"notes": "宽度档位用 .ua-modal-sm/lg 覆盖基准 md,基础宽 min(420px,90vw) 不动;footer 仅在传入时渲染(null 分支不进 DOM);demo 的弹窗改为 size:'lg' + 取消/确定 footer 按钮"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "d12",
|
||||||
|
"title": "新增可关闭标签 tag 组件",
|
||||||
|
"motivation": "badge 纯展示无法移除,筛选条件/已选关键字需要带 × 的标签;补齐 badge 的交互版",
|
||||||
|
"acceptance": "新增 tag.ts:label + variant 五档 + closable/onClose;closable 时渲染带 aria-label 的 × 按钮;只消费 --ua-* 令牌;导出 / manifest / 文档 / llms-full.txt / demo 同步;npm run check 全绿",
|
||||||
|
"status": "done",
|
||||||
|
"notes": "组件无状态:onClose 只通知,节点由调用方按信号重渲染移除(与 form/formItem 返回句柄、dataTable 委托内核的哲学一致);demo 用 signal<string[]> + renderList 演示真实删除链路,另有四档语义色静态展示"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "d13",
|
||||||
|
"title": "dataTable 列定义支持对齐与单元格格式化",
|
||||||
|
"motivation": "皮肤层只能 String() 原始值,数字列无法右对齐、年龄/金额无法加单位;内核不关心展示,字段应留在皮肤层",
|
||||||
|
"acceptance": "新增皮肤层 TableColumn<T> extends ColumnDef<T>,追加 align?: left|center|right 与 format?: (value,row)=>string;DataTableProps.columns 改用它;表头与单元格共用对齐类;format 走 textContent 天然免注入;demo 表格演示年龄右对齐+单位、城市居中;manifest/文档/llms-full.txt 同步;npm run check 全绿",
|
||||||
|
"status": "done",
|
||||||
|
"notes": "严格分层:内核 ColumnDef 不动,展示字段只在 skin 的 TableColumn 上扩展(extends 而非改核心);对齐类同时作用于 th(取代原 class 函数,合并 ua-sortable);demo 首表展示 align:right + format(v=>`${v} 岁`)、城市居中"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "d14",
|
||||||
|
"title": "新增抽屉 drawer 组件",
|
||||||
|
"motivation": "详情查看/编辑需要不打断主视图的侧滑容器,modal 是强打断确认;移动端尤其常用",
|
||||||
|
"acceptance": "新增 drawer.ts:open 信号驱动,placement 左右、width 可调(窄屏 92vw 上限)、footer 操作区、遮罩 / × / Esc 关闭、打开聚焦首个可交互元素;只消费 --ua-* 令牌 + reduced-motion 兜底;导出 / manifest / 文档 / llms-full.txt / demo 同步;npm run check 全绿",
|
||||||
|
"status": "done",
|
||||||
|
"notes": "遮罩开合用 opacity+visibility(与 tooltip/popover 同模式),面板靠 transform:translateX 滑入;width 走 inline style(调用方数据驱动的表现值不进 CSS);demo 新增 sec-drawer(订单详情 + footer 关闭 + tag/badge 组合)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "d15",
|
||||||
|
"title": "CLI 包补 ua.manifest.json 并修正文档里的过期组件数",
|
||||||
|
"motivation": "工具链自身不在注册表里,Agent 无法通过 MCP/llms 发现 ua 的 6 个子命令;AGENTS.md 与 ARCHITECTURE.md 仍写 17 个组件(实际 27)",
|
||||||
|
"acceptance": "packages/tools/cli/ua.manifest.json(type: tooling)描述 index/list/info/docs/style/verify/mcp 七个子命令与约定,索引包数变 5;AGENTS.md 与 ARCHITECTURE.md 的组件数改成 27;llms.txt / llms-full.txt 重新生成且 CLI 段落可读;npm run check 全绿",
|
||||||
|
"status": "done",
|
||||||
|
"notes": "tooling 类型在 ORDER 表外,排序自然落到末尾(??9 兜底),无需改 CLI 排序逻辑;CLI 清单 docs.components 留空,故不参与 verify 的组件级校验;顺带确认 docs 生成对无组件包不报错"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "d16",
|
||||||
|
"title": "dataTable 落地内核已定义的列宽 width",
|
||||||
|
"motivation": "table-core 的 ColumnDef 声明了 width?: number | 'flex',但皮肤层从未消费,列宽完全靠浏览器猜;契约与实现脱节",
|
||||||
|
"acceptance": "dataTable 渲染 colgroup:数字 width 落成固定像素 <col>,'flex' 或不设交给自适应,选择列固定 44px;demo 表格演示 name 160 / age 100 / city flex;manifest 列定义说明与手把手同步;npm run check 全绿",
|
||||||
|
"status": "done",
|
||||||
|
"notes": "用 colgroup 而非逐 th 设宽,保证表头与表体一致且不入侵单元格渲染;未开 table-layout:fixed(nowrap 内容会被裁),宽高仍由内容兜底;pinned 固定列仍未消费(多列 sticky 偏移需测量),留作后续条目"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "d17",
|
||||||
|
"title": "表单错误态覆盖包裹式控件并修正校验时机事件",
|
||||||
|
"motivation": "① 现有 blur 不冒泡,checkbox/select/radioGroup 这类包裹式控件失焦永远不校验;② ua-invalid 只加在控件根,自绘 select 的边框在内部触发器上,视觉上无错误提示;③ 缺 aria-invalid,读屏感知不到",
|
||||||
|
"acceptance": "formItem 用 focusout 取代 blur、补 change(select/checkbox 变更即校验);错误态同时落到控件根与内部原生控件,并同步 aria-invalid;CSS 覆盖 .ua-invalid 后代 .ua-input/.ua-textarea/.ua-select-trigger/.ua-radio/.ua-slider-input;demo 表单加必填 select 演示;manifest/文档/llms-full.txt 同步;npm run check 全绿",
|
||||||
|
"status": "done",
|
||||||
|
"notes": "marked() 统一收集控件根 + 内部 input/textarea/select/.ua-select-trigger 两处落点;.ua-invalid 后代选择器让包裹式控件描红,无需组件各自改签名(低侵入);demo formlab 新增城市必填 select,提交结果显示城市"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "d18",
|
||||||
|
"title": "把「皮肤包禁止硬编码颜色」做成 ua verify 门禁",
|
||||||
|
"motivation": "AGENTS.md 写着皮肤禁硬编码颜色,但一直靠自觉;ui.css 实际残留 5 处 #fff(按钮/徽标/开关滑块),暗色下对比也不理想",
|
||||||
|
"acceptance": "checkSkinCss():skin-dom 类型包里所有 .css 出现 #hex 字面量即报错并指出文件行号;5 处 #fff 改为 var(--ua-color-bg)(信号色底上的反色,暗色自动翻转);负向测试(临时加 .ua-tmp{color:#fff})能拦下,移除后回归全绿;AGENTS.md 门禁说明同步;npm run check 全绿",
|
||||||
|
"status": "done",
|
||||||
|
"notes": "rgba(...) 未禁:仅用于阴影与遮罩透明度叠加,不承载品牌色,禁掉会逼出更难读的写法(已在函数注释写明边界);实现上先随 python 写文件把正则转义搞坏两次,改用编辑工具直写并做负向验证确认门禁真实生效"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "d19",
|
||||||
|
"title": "dataTable 选中态受控(selected 信号)",
|
||||||
|
"motivation": "选中集只在内核里,外部拿不到也无法预设,批量操作/清空选择只能靠内部工具栏;与 page 受控桥接形成对称能力",
|
||||||
|
"acceptance": "DataTableProps 新增 selected?: Signal<RowKey[]>,与内核选择双向同步(内容比较避免来回派发,外部数组拷贝保护内核);demo 首表传 picked 信号,展示已选计数与「清空选择」按钮;manifest/文档/llms-full.txt 同步;npm run check 全绿",
|
||||||
|
"status": "done",
|
||||||
|
"notes": "复用 page 桥接的同一套思路:两个 effect 各管一向,sameKeys 内容比较收敛;内部写回拷贝 [...internal],防外部改动内核数组;demo 需 import type { RowKey } 才能对齐 Signal 的逆变"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "d20",
|
||||||
|
"title": "progress 支持不确定态(未知时长加载)",
|
||||||
|
"motivation": "请求时长未知时只能硬编造一个假百分比,或用 spinner 但失去条状形态;不确定态是进度条的标准形态",
|
||||||
|
"acceptance": "ProgressProps.value 改为可选;缺省时渲染不确定态(35% 填充块 1.2s 往复)、不显示百分比、role=progressbar + aria-label=进行中;定量态补 aria-valuemin/max/now;reduced-motion 静态化;demo 加不确定态示例;manifest/文档/llms-full.txt 同步;npm run check 全绿",
|
||||||
|
"status": "done",
|
||||||
|
"notes": "以「不传 value」作为不确定态的开关,不新增布尔参数(少一个可矛盾的状态);定量态的 aria-valuenow 用函数属性随信号更新;reduced-motion 下靠既有 .ua-progress-fill 规则把动画归零,退化为静态 35% 条"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "d21",
|
||||||
|
"title": "无障碍收尾:× 按钮 aria-label、spinner/skeleton 语义、tablist 标签",
|
||||||
|
"motivation": "审计发现 modal 的 × 按钮无 aria-label(读屏只念「×」)、spinner 无 role=status(不播报加载)、skeleton 未 aria-hidden(占位文本被读出来)、tablist 缺 aria-label 与 aria-disabled",
|
||||||
|
"acceptance": "modal 关闭按钮补 type/aria-label、spinner 补 role=status + aria-label、skeleton 全形态 aria-hidden、tabs 的 tablist 补 aria-label 且禁用页签标 aria-disabled;manifest conventions 记录无障碍基线;npm run check 全绿",
|
||||||
|
"status": "done",
|
||||||
|
"notes": "纯属性级修复,不动结构与样式;tabIndex 漫游(仅激活页签可 Tab)此前已实现,本次只补语义;分页器审计后无需改动(已有 nav aria-label、页码 aria-current、‹› aria-label)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "d22",
|
||||||
|
"title": "新增头像 avatar 组件",
|
||||||
|
"motivation": "用户列表/评论区需要头像位,缺 component 只能手拼 img + 圆角;首字占位是缺图时的标准兜底",
|
||||||
|
"acceptance": "新增 avatar.ts:name 必填(alt / title / 首字来源),src 可选(有则图片、无则首字占位),size 可调,variant 四档语义底色;只消费 --ua-* 令牌;导出 / manifest / 文档 / llms-full.txt / demo 同步;npm run check 全绿",
|
||||||
|
"status": "done",
|
||||||
|
"notes": "首字占位对中文取首字、英文大写,空名兜底 ?;flex:none 保证不被挤压;首次跑 verify 被自身新门禁拦下(声明 sec-avatar 但 demo 缺区块),补独立区块后通过——门禁有效性再次得到验证"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "d23",
|
||||||
|
"title": "alert 补标题行与可关闭,并补 role=alert",
|
||||||
|
"motivation": "常驻提示无法消除(错误已处理完仍占版面),且只有单层正文放不下「结论 + 详情」;role=alert 缺失导致读屏不播报",
|
||||||
|
"acceptance": "AlertProps 新增 title? / closable? / onClose?;关闭时先退场(opacity+上移,200ms)再从 DOM 移除;容器 role=alert;只消费 --ua-* 令牌;demo 新增 sec-alert 展示无标题与可关闭带标题两种;manifest/文档/llms-full.txt 同步;npm run check 全绿",
|
||||||
|
"status": "done",
|
||||||
|
"notes": "与 toast 的分工保持:alert 常驻在页面流里、toast 全局浮动;关闭后由调用方自行决定是否重新渲染;body/title/message 三层结构让 flex 布局能把 × 对齐到首行;对现有 alert({message}) 调用完全兼容"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "d24",
|
||||||
|
"title": "重写 README 并把组件清单纳入 verify 防漂移",
|
||||||
|
"motivation": "README 只有标题一行,新人/Agent 从仓库首页拿不到任何信息;而文档里的组件数此前已经漂移过(17 vs 27)",
|
||||||
|
"acceptance": "README 覆盖定位 / 快速开始 / 28 组件清单 / 目录表 / 三道门禁 / Agent 接入 / 分支流水线;新增 <!-- ua:components --> 标记区块,ua verify 校验区块 id 集合与全部清单 docs.components 完全一致(缺/多都报);负向测试(删 avatar)能拦下;AGENTS.md 同步;npm run check 全绿",
|
||||||
|
"status": "done",
|
||||||
|
"notes": "不做自动写入(避免工具偷偷改文档),只做一致性校验:新增组件时 verify 报「README 组件区块缺少 xxx」,由人补一行;标记区块用 · 分隔便于人读与机器切分"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "d25",
|
||||||
|
"title": "menu 条目支持快捷键提示",
|
||||||
|
"motivation": "菜单里展示快捷键是桌面级体验的标配;缺字段时调用方只能把快捷键拼进 label,文案与对齐都失控",
|
||||||
|
"acceptance": "MenuItem 新增 shortcut?: string;条目改为 flex 两端对齐,label 单行省略,右侧 kbd 灰字提示(不绑定按键);demo 文件操作菜单加 F2 / ⌘D;manifest/文档/llms-full.txt 同步;npm run check 全绿",
|
||||||
|
"status": "done",
|
||||||
|
"notes": "明确边界:快捷键只做展示,实际按键仍需调用方全局监听(组件不做全局键盘陷阱);两端对齐让长文案截断而快捷键始终可见;kbd 用 inherit 字体避免等宽字跳脱整体排版"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "d26",
|
||||||
|
"title": "select 键盘导航:↑↓ 环形移动、Home/End 跳转、Esc 回焦触发器",
|
||||||
|
"status": "done"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "d27",
|
||||||
|
"title": "modal 焦点陷阱:Tab/Shift+Tab 回绕、role=dialog/aria-modal、关闭归还焦点",
|
||||||
|
"status": "done"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "d28",
|
||||||
|
"title": "drawer 焦点陷阱:Tab/Shift+Tab 回绕、关闭归还焦点(对齐 modal)",
|
||||||
|
"status": "done"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "d29",
|
||||||
|
"title": "menu 补 Home/End 跳转,与 select 键盘导航对称",
|
||||||
|
"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": "open",
|
"status": "open",
|
||||||
"notes": ""
|
"notes": "自补:工具链防漂移方向"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
+965
-39
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,51 @@
|
|||||||
|
{"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 全绿"}
|
||||||
|
{"ts": "2026-10-23T15:50:20+08:00", "id": "b02", "result": "done", "commit": "ece38bc", "summary": "新增 toast 消息提示组件:四语义工厂 + showToast,右上角堆叠,默认 3s 自动消失,微动画 + reduced-motion 兜底;manifest/docs/llms 同步,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 全绿"}
|
||||||
|
{"ts": "2026-10-23T16:00:13+08:00", "id": "b04", "result": "done", "commit": "8c9a63b", "summary": "新增独立 pagination 分页器(page 信号双向绑定 + 省略号折叠 + 键盘可达),dataTable 新增 page 受控桥接实现联动;demo 联动卡片,check 全绿"}
|
||||||
|
{"ts": "2026-10-23T16:03:47+08:00", "id": "b05", "result": "done", "commit": "83bc1e2", "summary": "文档站核查确认已 manifest 驱动;补齐默认值信息量缺口:CLI propsTable 新增默认值列(df 字段)+ llms-full 默认标注,16 处默认值提取;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 全绿"}
|
||||||
|
{"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 全绿"}
|
||||||
|
{"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 全绿"}
|
||||||
|
{"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 全绿"}
|
||||||
|
{"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 全绿"}
|
||||||
|
{"ts": "2026-10-23T16:48:36+08:00", "id": "d04", "result": "done", "commit": "ccbdcb9", "summary": "dataTable 新增 loading(布尔|信号):表体换骨架行、筛选与翻页禁用、aria-busy;demo 加开关,manifest/llms 同步,check 全绿"}
|
||||||
|
{"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 门禁拦下多行注释,已改单行)"}
|
||||||
|
{"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 全绿"}
|
||||||
|
{"ts": "2026-10-23T16:51:48+08:00", "id": "d07", "result": "done", "commit": "fb9ab88", "summary": "新增可交互气泡卡片 popover:点击/hover 触发、点外部与 Esc 收起、内容可交互且内部点击不误关;manifest/llms/demo 同步,check 全绿"}
|
||||||
|
{"ts": "2026-10-23T16:52:28+08:00", "id": "d08", "result": "done", "commit": "1a4b2c1", "summary": "演示页改双栏 shell:sticky 侧边目录由 .sec 自动反推 + IntersectionObserver 滚动高亮,窄屏隐藏、reduced-motion 关平滑滚动;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 全绿"}
|
||||||
|
{"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 全绿"}
|
||||||
|
{"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 全绿"}
|
||||||
|
{"ts": "2026-10-23T16:55:10+08:00", "id": "d12", "result": "done", "commit": "9eefdcb", "summary": "新增可关闭标签 tag(五档语义色 + closable/onClose,无状态委托调用方重渲染);demo 用 renderList 演示真实删除;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 全绿"}
|
||||||
|
{"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 全绿"}
|
||||||
|
{"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 全绿"}
|
||||||
|
{"ts": "2026-10-23T16:58:52+08:00", "id": "d16", "result": "done", "commit": "fa168b9", "summary": "dataTable 消费内核列宽 width:colgroup 固定像素 / flex 自适应,选择列 44px;demo 三列演示宽档;manifest/llms 同步,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 全绿"}
|
||||||
|
{"ts": "2026-10-23T17:00:55+08:00", "id": "d18", "result": "done", "commit": "4012923", "summary": "ua verify 新增皮肤硬编码颜色门禁(#hex 禁止,rgba 仅限阴影/遮罩),清掉 ui.css 5 处 #fff 改令牌;负向测试确认能拦;check 全绿"}
|
||||||
|
{"ts": "2026-10-23T17:01:46+08:00", "id": "d19", "result": "done", "commit": "4555542", "summary": "dataTable 新增 selected 受控信号:与内核选择双向同步(内容比较 + 拷贝保护),demo 展示已选计数与清空;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 全绿"}
|
||||||
|
{"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 全绿"}
|
||||||
|
{"ts": "2026-10-23T17:03:49+08:00", "id": "d22", "result": "done", "commit": "f86da98", "summary": "新增头像 avatar(src 图片 / 首字占位 + size + 四档语义底色),demo 独立 sec-avatar;中途被 verify 的 demo 锚点检查拦下已补;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 全绿"}
|
||||||
|
{"ts": "2026-10-23T17:05:46+08:00", "id": "d24", "result": "done", "commit": "6efe3ab", "summary": "重写 README(定位/快速开始/28 组件/目录/门禁/Agent 接入/流水线),组件清单做成标记区块并由 ua verify 校验防漂移;负向测试通过,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-23T17:08:53+08:00", "id": "d26", "result": "done", "commit": "82f48aa", "summary": "select 键盘导航:↑↓ 环形移动、Home/End 跳转、Esc 回焦触发器"}
|
||||||
|
{"ts": "2026-10-23T17:10:37+08:00", "id": "d27", "result": "done", "commit": "fac74a3", "summary": "modal 焦点陷阱 + dialog 语义"}
|
||||||
|
{"ts": "2026-10-23T17:11:31+08:00", "id": "d28", "result": "done", "commit": "8e16829", "summary": "drawer 焦点陷阱对齐 modal"}
|
||||||
|
{"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 放搜索图标,事件冒泡与筛选回归正常,门禁全绿"}
|
||||||
@@ -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,13 +21,15 @@
|
|||||||
|
|
||||||
- `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):16 个 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
|
||||||
- `apps/docs`:文档站,纯产物(ua docs 生成)
|
- `apps/docs`:文档站,纯产物(ua docs 生成)
|
||||||
|
|
||||||
**门禁一条龙**:`npm run check` = style + typecheck + smoke + index,提交前必过。
|
**门禁一条龙**:`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 查询组件库的三种方式
|
||||||
|
|
||||||
@@ -46,7 +48,7 @@
|
|||||||
|
|
||||||
可用工具:`list_components` / `get_component` / `search_components`。
|
可用工具:`list_components` / `get_component` / `search_components`。
|
||||||
|
|
||||||
2. **CLI**:`node packages/tools/cli/bin/ua.js list | info <name> | index`
|
2. **CLI**:`node packages/tools/cli/bin/ua.js list | info <name> | index | docs | style | verify`
|
||||||
|
|
||||||
3. **静态索引**:`.ua/index.json`(机器可读索引)、`llms.txt`(速览)或 **`llms-full.txt`(全量 Agent 文档:逐组件参数 / 回调 / 手把手步骤 / 令牌,无图形纯文本,喂给 Agent 首选)**。
|
3. **静态索引**:`.ua/index.json`(机器可读索引)、`llms.txt`(速览)或 **`llms-full.txt`(全量 Agent 文档:逐组件参数 / 回调 / 手把手步骤 / 令牌,无图形纯文本,喂给 Agent 首选)**。
|
||||||
|
|
||||||
|
|||||||
+10
-2
@@ -34,16 +34,24 @@ ui-arch-mvp/
|
|||||||
│ │ ├── table-core/ # 表格状态内核
|
│ │ ├── table-core/ # 表格状态内核
|
||||||
│ │ └── frame/ # 信号 + hyperscript 微框架
|
│ │ └── frame/ # 信号 + hyperscript 微框架
|
||||||
│ ├── skin/ # 皮肤层(DOM 组件,可发布)
|
│ ├── skin/ # 皮肤层(DOM 组件,可发布)
|
||||||
│ │ └── ui/ # 15 个组件 + 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 机器校验)
|
||||||
```
|
```
|
||||||
|
|
||||||
## 包规范(每包强制)
|
## 包规范(每包强制)
|
||||||
|
|||||||
@@ -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 信息跳过校验。
|
||||||
+13
-6
@@ -1,7 +1,16 @@
|
|||||||
# 无人值守自我迭代协议(ITERATION.md)
|
## 无人值守自我迭代协议(ITERATION.md)
|
||||||
|
|
||||||
> 本协议由自动化 Agent 执行,也可供人工参考。任何迭代必须遵守 `CODING.md`、`AGENTS.md`、`ARCHITECTURE.md` 三份规范,门禁 `npm run check` 不通过的一切产出作废。
|
> 本协议由自动化 Agent 执行,也可供人工参考。任何迭代必须遵守 `CODING.md`、`AGENTS.md`、`ARCHITECTURE.md` 三份规范,门禁 `npm run check` 不通过的一切产出作废。
|
||||||
|
|
||||||
|
## 运行模式:连续迭代 + 心跳监护
|
||||||
|
|
||||||
|
调度器(每小时唤起)**不是**迭代器,只做两件事:健康检查 + 中断重启。真正的迭代是单次唤起内的**连续循环**——反复执行《每轮迭代流程》,直到 backlog 清空或时间预算耗尽,中间不停歇。
|
||||||
|
|
||||||
|
- **并发锁**:`.ua/loop.lock`(含时间戳,不入库)。唤起时若锁存在且心跳在 15 分钟内 → 已有实例在跑,直接退出;否则写锁开始循环。每轮开始前刷新锁内时间戳作心跳。
|
||||||
|
- **时间预算**:单次唤起最多连续迭代约 50 分钟,到点在完成当前轮后停止(防止与下次唤起重叠)。**预算判断必须基于客观时钟**:点火时用 `date +%s` 记录起点并写入 `.ua/loop.lock`,每轮开始时重新取 `date +%s` 与起点求差,差值 ≥ 2700 秒(45 分钟)即到点;**禁止凭感觉估算时长**——LLM 无内部时钟,自估时长系统性偏高(实测自报 40+ 分钟而实际约 30 分钟)。
|
||||||
|
- **熔断**:同一 backlog 条目连续 2 轮失败 → 停止循环,锁照常释放,等待下次唤起。
|
||||||
|
- **重启语义**:锁心跳超过 15 分钟视为循环中断(崩溃/超时),调度器删旧锁、重新点火。
|
||||||
|
|
||||||
## 分支流水线
|
## 分支流水线
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -16,15 +25,13 @@ qa/probe ──门禁通过──▶ qa/stable ──人工审阅──▶ main
|
|||||||
|
|
||||||
## 每轮迭代流程(自动化 Agent 执行)
|
## 每轮迭代流程(自动化 Agent 执行)
|
||||||
|
|
||||||
> 所有远程推送一律使用认证包装脚本:`node scripts/gitea.mjs push <remote> <分支>`(令牌读自本地 `.gitea-token`,不入库;凭据管理器不可用)。
|
|
||||||
|
|
||||||
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`。
|
||||||
|
|
||||||
## 选题规则(自主补点子时)
|
## 选题规则(自主补点子时)
|
||||||
|
|||||||
@@ -1,2 +1,54 @@
|
|||||||
# UI
|
# @ui-arch · 私人组件库
|
||||||
|
|
||||||
|
自研、零依赖、Agent 友好的 DOM 组件库单体仓:**signal 微框架 + headless 表格内核 + 令牌化皮肤组件**,所有文档与 MCP 注册表都从一个清单派生。
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm install # 包间依赖用 workspace:*,npm 不支持
|
||||||
|
npm run check # 门禁一条龙:build → style → typecheck → smoke → index → verify
|
||||||
|
npm run demo # 打开演示:apps/demo/dist/demo.html
|
||||||
|
npm run docs # 生成人类文档站 apps/docs/index.html + Agent 全量文档 llms-full.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
## 组件(共 34 个)
|
||||||
|
|
||||||
|
<!-- ua:components -->
|
||||||
|
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)。
|
||||||
|
|
||||||
|
## 目录
|
||||||
|
|
||||||
|
| 路径 | 说明 |
|
||||||
|
|---|---|
|
||||||
|
| `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:34 个 DOM 组件 + ui.css(只消费 `--ua-*` 令牌) |
|
||||||
|
| `packages/design/tokens` | @ui-arch/tokens:设计令牌(暗色只换值不换名) |
|
||||||
|
| `packages/tools/cli` | @ui-arch/cli:`ua` 命令行与 stdio MCP server |
|
||||||
|
| `apps/demo` | 交互演示(esbuild 打包成单文件) |
|
||||||
|
| `apps/docs` | 文档站纯产物(`ua docs` 生成) |
|
||||||
|
|
||||||
|
## 三道门禁
|
||||||
|
|
||||||
|
1. `ua style`:注释规范——导出必须有 `/*** 作用 ***/`、接口字段行尾注释、行注释用中文。
|
||||||
|
2. `ua verify`:清单 ↔ 文档 ↔ 演示一致(id 唯一、exports 对应、props 带 n/t/d、steps ≥ 3、demo 锚点存在),皮肤样式表禁止 `#hex` 硬编码颜色。
|
||||||
|
3. `ua index`:刷新 `.ua/index.json` / `llms.txt`,供 MCP 与 Agent 查询。
|
||||||
|
|
||||||
|
## Agent 接入
|
||||||
|
|
||||||
|
- 静态:`llms.txt`(速览)/ **`llms-full.txt`(全量:逐组件参数 / 回调 / 手把手 / 令牌)**
|
||||||
|
- 命令行:`node packages/tools/cli/bin/ua.js list | info <name> | docs | verify | mcp`
|
||||||
|
- MCP:在 `~/.workbuddy/mcp.json` 注册 `ua mcp`,可用 `list_components` / `get_component` / `search_components`
|
||||||
|
|
||||||
|
规范见 `AGENTS.md`(接入手册)、`ARCHITECTURE.md`(分层与依赖方向)、`CODING.md`(编码规范)、`ITERATION.md`(自动迭代协议)。
|
||||||
|
|
||||||
|
## 分支流水线
|
||||||
|
|
||||||
|
```
|
||||||
|
qa/probe ──门禁通过──▶ qa/stable ──人工审阅──▶ main(受保护)
|
||||||
|
```
|
||||||
|
|
||||||
|
自动化只在 `qa/stable` 做 fast-forward 合并,`main` 仅由人审后合入。
|
||||||
|
|||||||
+10
-1
@@ -45,7 +45,16 @@ ${css}
|
|||||||
@media (prefers-color-scheme:dark){:root{--page-bg:#101216;--page-text:#e7e9ec;--page-muted:#888780;--page-border:rgba(255,255,255,.15)}}
|
@media (prefers-color-scheme:dark){:root{--page-bg:#101216;--page-text:#e7e9ec;--page-muted:#888780;--page-border:rgba(255,255,255,.15)}}
|
||||||
*{box-sizing:border-box}
|
*{box-sizing:border-box}
|
||||||
body{margin:0;background:var(--page-bg);color:var(--page-text);font:14px/1.7 -apple-system,"Segoe UI","Microsoft YaHei",sans-serif}
|
body{margin:0;background:var(--page-bg);color:var(--page-text);font:14px/1.7 -apple-system,"Segoe UI","Microsoft YaHei",sans-serif}
|
||||||
.page{max-width:880px;margin:0 auto;padding:36px 20px 80px}
|
.shell{max-width:1160px;margin:0 auto;padding:36px 20px 80px;display:grid;grid-template-columns:190px minmax(0,1fr);gap:36px}
|
||||||
|
.page{max-width:900px;margin:0;padding:0}
|
||||||
|
.toc{position:sticky;top:24px;align-self:start;max-height:calc(100vh - 48px);overflow:auto;font-size:13px}
|
||||||
|
.toc-title{color:var(--page-muted);font-size:11px;letter-spacing:.08em;margin-bottom:8px}
|
||||||
|
.toc-link{display:block;padding:3px 8px;border-left:2px solid transparent;border-radius:0 4px 4px 0;color:var(--page-muted);text-decoration:none;transition:color .15s,background .15s,border-color .15s}
|
||||||
|
.toc-link:hover{color:var(--page-text);background:rgba(127,127,127,.08)}
|
||||||
|
.toc-link.is-active{color:var(--ua-color-accent);border-left-color:var(--ua-color-accent);background:color-mix(in srgb,var(--ua-color-accent) 10%,transparent)}
|
||||||
|
@media (max-width:820px){.shell{grid-template-columns:1fr;gap:0}.toc{display:none}}
|
||||||
|
html{scroll-behavior:smooth}
|
||||||
|
@media (prefers-reduced-motion:reduce){html{scroll-behavior:auto}}
|
||||||
header{margin-bottom:30px}
|
header{margin-bottom:30px}
|
||||||
h1{font-size:20px;font-weight:500;margin:0}
|
h1{font-size:20px;font-weight:500;margin:0}
|
||||||
.sub{color:var(--page-muted);font-size:13px;margin:8px 0 0}
|
.sub{color:var(--page-muted);font-size:13px;margin:8px 0 0}
|
||||||
|
|||||||
+416
-11
@@ -1,23 +1,72 @@
|
|||||||
import { h, mount, 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,
|
||||||
alert,
|
alert,
|
||||||
|
avatar,
|
||||||
badge,
|
badge,
|
||||||
|
breadcrumb,
|
||||||
button,
|
button,
|
||||||
card,
|
card,
|
||||||
checkbox,
|
checkbox,
|
||||||
dataTable,
|
dataTable,
|
||||||
|
divider,
|
||||||
|
drawer,
|
||||||
|
empty,
|
||||||
|
image,
|
||||||
form,
|
form,
|
||||||
formItem,
|
formItem,
|
||||||
|
inputNumber,
|
||||||
|
menu,
|
||||||
modal,
|
modal,
|
||||||
|
pagination,
|
||||||
|
popover,
|
||||||
progress,
|
progress,
|
||||||
radioGroup,
|
radioGroup,
|
||||||
|
rate,
|
||||||
select,
|
select,
|
||||||
|
showToast,
|
||||||
|
skeleton,
|
||||||
|
slider,
|
||||||
spinner,
|
spinner,
|
||||||
|
stepper,
|
||||||
switchButton,
|
switchButton,
|
||||||
tabs,
|
tabs,
|
||||||
|
tag,
|
||||||
textarea,
|
textarea,
|
||||||
textField,
|
textField,
|
||||||
|
toast,
|
||||||
|
tooltip,
|
||||||
} from '@ui-arch/ui';
|
} from '@ui-arch/ui';
|
||||||
|
import type { RowKey } from '@ui-arch/ui';
|
||||||
|
|
||||||
|
/*** 侧边目录:挂载后从 .sec 反推锚点,IntersectionObserver 高亮视口内区块(清单改动自动跟随) ***/
|
||||||
|
function buildToc(toc: HTMLElement, content: HTMLElement): void {
|
||||||
|
const secs = Array.from(content.querySelectorAll<HTMLElement>('section.sec'));
|
||||||
|
if (!secs.length) return; // 卫语句:无区块不建目录
|
||||||
|
const links = secs.map((sec) =>
|
||||||
|
h('a', { class: 'toc-link', href: `#${sec.id}`, 'data-for': sec.id }, sec.querySelector('h2')?.textContent ?? sec.id),
|
||||||
|
);
|
||||||
|
toc.append(...links);
|
||||||
|
const io = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
for (const en of entries) {
|
||||||
|
if (!en.isIntersecting) continue; // 只处理刚进入视口的区块
|
||||||
|
const id = (en.target as HTMLElement).id;
|
||||||
|
for (const link of links) link.classList.toggle('is-active', link.getAttribute('data-for') === id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ rootMargin: '-10% 0px -80% 0px' }, // 顶部窄条判定,滚动时高亮更跟手
|
||||||
|
);
|
||||||
|
for (const sec of secs) io.observe(sec);
|
||||||
|
}
|
||||||
|
|
||||||
/*** 生成演示区块;id 供文档站 iframe 锚点定位 ***/
|
/*** 生成演示区块;id 供文档站 iframe 锚点定位 ***/
|
||||||
function section(id: string, title: string, desc: string, ...body: unknown[]): HTMLElement {
|
function section(id: string, title: string, desc: string, ...body: unknown[]): HTMLElement {
|
||||||
@@ -64,7 +113,45 @@ const username = signal('');
|
|||||||
const email = signal('');
|
const email = signal('');
|
||||||
const formResult = signal('');
|
const formResult = signal('');
|
||||||
|
|
||||||
const app = h(
|
// —— 分页联动状态:dataTable 与独立 pagination 共用同一页码信号 ——
|
||||||
|
const tablePage = signal(1);
|
||||||
|
|
||||||
|
// —— 抽屉开关 ——
|
||||||
|
const drawerOpen = signal(false);
|
||||||
|
|
||||||
|
// —— 标签列表:关闭即写回信号,容器按信号重渲染 ——
|
||||||
|
const tags = signal(['设计', '前端', '组件库']);
|
||||||
|
|
||||||
|
// —— 滑块:音量与禁用态演示 ——
|
||||||
|
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 内部勾选实时写回,外部可清空 ——
|
||||||
|
const picked = signal<RowKey[]>([]);
|
||||||
|
|
||||||
|
// —— 表格加载态:切换后数据行变骨架行,筛选与翻页临时禁用 ——
|
||||||
|
const tableLoading = signal(false);
|
||||||
|
|
||||||
|
// —— 菜单选中结果:演示 onSelect 回调 ——
|
||||||
|
const menuPick = signal('');
|
||||||
|
|
||||||
|
// —— 禁用态演示:开关信号同时绑定 switch 的 checked 与 checkbox 的 disabled ——
|
||||||
|
const locked = signal(true);
|
||||||
|
|
||||||
|
const content = h(
|
||||||
'div',
|
'div',
|
||||||
{ class: 'page' },
|
{ class: 'page' },
|
||||||
h(
|
h(
|
||||||
@@ -84,8 +171,10 @@ const app = 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(
|
||||||
@@ -98,6 +187,92 @@ const app = h(
|
|||||||
h('div', { class: 'row' }, radioGroup({ options: [{ value: 's', label: '小' }, { value: 'm', label: '中' }, { value: 'l', label: '大' }], value: size })),
|
h('div', { class: 'row' }, radioGroup({ options: [{ value: 's', label: '小' }, { value: 'm', label: '中' }, { value: 'l', label: '大' }], value: size })),
|
||||||
textarea({ placeholder: '一句话介绍…', value: bio, rows: 2 }),
|
textarea({ placeholder: '一句话介绍…', value: bio, rows: 2 }),
|
||||||
h('span', { class: 'out' }, () => `简介 ${bio().length} 字`),
|
h('span', { class: 'out' }, () => `简介 ${bio().length} 字`),
|
||||||
|
h(
|
||||||
|
'div',
|
||||||
|
{ class: 'row' },
|
||||||
|
button({ label: '禁用按钮', variant: 'ghost', disabled: true }),
|
||||||
|
checkbox({ label: '信号禁用的勾选', disabled: locked }),
|
||||||
|
switchButton({ label: '锁定', checked: locked }), // 关掉开关即解锁上面的勾选框与输入框
|
||||||
|
textField({ placeholder: '锁定后不可输入…', disabled: locked }),
|
||||||
|
),
|
||||||
|
h(
|
||||||
|
'div',
|
||||||
|
{ class: 'row' },
|
||||||
|
select({ options: cities.map((c) => ({ value: c, label: c })), value: city, placeholder: '信号禁用的下拉…', disabled: locked }),
|
||||||
|
radioGroup({ options: [{ value: 's', label: '小' }, { value: 'm', label: '中' }], value: size, disabled: locked }),
|
||||||
|
radioGroup({ options: [{ value: 'x', label: '静态禁用' }], value: size, disabled: true }),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
section(
|
||||||
|
'sec-drawer',
|
||||||
|
'抽屉',
|
||||||
|
'drawer:open 信号驱动侧滑面板,保留主视图上下文;遮罩 / × / Esc 关闭。',
|
||||||
|
button({ label: '打开抽屉', onClick: () => drawerOpen(true) }),
|
||||||
|
drawer(
|
||||||
|
{
|
||||||
|
title: '订单详情',
|
||||||
|
open: drawerOpen,
|
||||||
|
width: 360,
|
||||||
|
onClose: () => drawerOpen(false),
|
||||||
|
footer: h('div', { class: 'row' }, button({ label: '关闭', variant: 'ghost', onClick: () => drawerOpen(false) })),
|
||||||
|
},
|
||||||
|
h('p', null, '这里是抽屉内容,可放任意 DOM。'),
|
||||||
|
h('div', { class: 'row' }, tag({ label: '#A1024', variant: 'info' }), badge({ label: '已发货', variant: 'success' })),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
section(
|
||||||
|
'sec-tag',
|
||||||
|
'标签',
|
||||||
|
'tag:可关闭标签(badge 的交互版),关闭回调里改信号即从列表移除。',
|
||||||
|
h(
|
||||||
|
'div',
|
||||||
|
{ class: 'row' },
|
||||||
|
tag({ label: '纯展示', variant: 'default' }),
|
||||||
|
tag({ label: '通过', variant: 'success' }),
|
||||||
|
tag({ label: '待处理', variant: 'warning' }),
|
||||||
|
tag({ label: '已失效', variant: 'danger' }),
|
||||||
|
),
|
||||||
|
(() => {
|
||||||
|
const box = h('div', { class: 'row' });
|
||||||
|
renderList(box, () => tags().map((t) => tag({ label: t, variant: 'info', closable: true, onClose: () => tags(tags().filter((x) => x !== t)) })));
|
||||||
|
return box;
|
||||||
|
})(),
|
||||||
|
),
|
||||||
|
section(
|
||||||
|
'sec-avatar',
|
||||||
|
'头像',
|
||||||
|
'avatar:有 src 走图片(alt 用名称),无 src 用首字占位;size 与语义底色都可调。',
|
||||||
|
h(
|
||||||
|
'div',
|
||||||
|
{ class: 'row' },
|
||||||
|
avatar({ name: '张三' }),
|
||||||
|
avatar({ name: 'Alice', size: 40, variant: 'success' }),
|
||||||
|
avatar({ name: 'Bob', size: 24, variant: 'warning' }),
|
||||||
|
avatar({ name: '李四', size: 36, variant: 'danger' }),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
section(
|
||||||
|
'sec-slider',
|
||||||
|
'滑块',
|
||||||
|
'slider:原生 range 承载交互(含键盘 ←→),value 信号双向绑定,右侧可显示当前值。',
|
||||||
|
h(
|
||||||
|
'div',
|
||||||
|
{ class: 'row' },
|
||||||
|
slider({ value: volume, min: 0, max: 100, step: 5, onChange: (v) => toast.info(`音量 ${v}`) }),
|
||||||
|
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',
|
||||||
@@ -111,16 +286,61 @@ const app = h(
|
|||||||
alert({ message: '警告提示:操作可能存在风险。', variant: 'warning' }),
|
alert({ message: '警告提示:操作可能存在风险。', variant: 'warning' }),
|
||||||
h('div', { class: 'row' }, spinner(), spinner({ size: 26 })),
|
h('div', { class: 'row' }, spinner(), spinner({ size: 26 })),
|
||||||
progress({ value: percent }),
|
progress({ value: percent }),
|
||||||
|
h('div', { class: 'row' }, h('span', { class: 'out' }, '不确定态:'), progress()),
|
||||||
|
),
|
||||||
|
section(
|
||||||
|
'sec-alert',
|
||||||
|
'提示条',
|
||||||
|
'alert:四档语义色 + 可选标题与关闭按钮,role=alert 由读屏播报。',
|
||||||
|
alert({ message: '这是一条中性提示(无标题)', variant: 'info' }),
|
||||||
|
alert({ title: '保存失败', message: '网络波动,请稍后重试;本次修改已保留在本地草稿。', variant: 'danger', closable: true, onClose: () => toast.warning('提示已关闭') }),
|
||||||
|
),
|
||||||
|
section(
|
||||||
|
'sec-tooltip',
|
||||||
|
'悬浮提示',
|
||||||
|
'tooltip 包裹触发器:hover / 聚焦延迟 150ms 出场,移出、失焦或 Esc 收起,气泡不抢指针。',
|
||||||
|
h(
|
||||||
|
'div',
|
||||||
|
{ class: 'row' },
|
||||||
|
tooltip({ content: '保存后立即生效' }, button({ label: '保存' })),
|
||||||
|
tooltip({ content: '该操作不可撤销', placement: 'bottom' }, button({ label: '删除', variant: 'danger' })),
|
||||||
|
tooltip({ content: '聚焦输入框同样会显示' }, textField({ placeholder: '聚焦我…' })),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
section(
|
||||||
|
'sec-toast',
|
||||||
|
'消息提示',
|
||||||
|
'toast 右上角堆叠弹出,默认 3s 自动淡出移除;语义色四档,出入场微动画。',
|
||||||
|
h(
|
||||||
|
'div',
|
||||||
|
{ class: 'row' },
|
||||||
|
button({ label: '成功', onClick: () => toast.success('保存成功') }),
|
||||||
|
button({ label: '错误', variant: 'danger', onClick: () => toast.error('网络请求失败') }),
|
||||||
|
button({ label: '警告', onClick: () => toast.warning('磁盘空间不足') }),
|
||||||
|
button({ label: '信息(1.5s)', onClick: () => toast.info('已同步云端', 1500) }),
|
||||||
|
button({ label: '可关闭', variant: 'ghost', onClick: () => showToast({ message: '这条可以手动关掉', variant: 'warning', closable: true }) }),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
section(
|
||||||
|
'sec-skeleton',
|
||||||
|
'骨架屏',
|
||||||
|
'skeleton 占位:text 多行段落 / rect 块 / circle 圆,扫光微动画,reduced-motion 自动静态化。',
|
||||||
|
card(
|
||||||
|
{ title: '加载中的用户资料' },
|
||||||
|
h('div', { class: 'row' }, skeleton({ variant: 'circle' }), skeleton({ variant: 'text', rows: 2 })),
|
||||||
|
skeleton({ variant: 'rect' }),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
section(
|
section(
|
||||||
'sec-formlab',
|
'sec-formlab',
|
||||||
'表单校验',
|
'表单校验',
|
||||||
'form + formItem:必填 / 长度 / 正则校验,全部通过才触发提交;失焦校验、输入清错。',
|
'form + formItem:必填 / 长度 / 正则校验,全部通过才触发提交;失焦校验、输入清错。',
|
||||||
form(
|
form(
|
||||||
{ submitText: '注册', onSubmit: () => formResult(`提交成功:${username()} / ${email()}`) },
|
{ submitText: '注册', onSubmit: () => formResult(`提交成功:${username()} / ${email()} / ${city()}`) },
|
||||||
[
|
[
|
||||||
formItem({ label: '用户名', required: true, value: username, rules: [{ min: 2, max: 12, message: '用户名需 2-12 个字符' }] }, textField({ placeholder: '2-12 个字符', value: username })),
|
formItem({ label: '用户名', required: true, value: username, rules: [{ min: 2, max: 12, message: '用户名需 2-12 个字符' }] }, textField({ placeholder: '2-12 个字符', value: username })),
|
||||||
formItem({ label: '邮箱', required: true, value: email, rules: [{ pattern: /^[\w.-]+@[\w-]+\.[\w.]+$/, message: '邮箱格式不正确' }] }, textField({ placeholder: 'you@example.com', value: email })),
|
formItem({ label: '邮箱', required: true, value: email, rules: [{ pattern: /^[\w.-]+@[\w-]+\.[\w.]+$/, message: '邮箱格式不正确' }] }, textField({ placeholder: 'you@example.com', value: email })),
|
||||||
|
formItem({ label: '城市', required: true, value: city }, select({ options: cities.map((c) => ({ value: c, label: c })), value: city, placeholder: '请选择城市…' })),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
h('span', { class: 'out' }, () => formResult() || '等待提交…'),
|
h('span', { class: 'out' }, () => formResult() || '等待提交…'),
|
||||||
@@ -133,11 +353,170 @@ const app = h(
|
|||||||
h('span', { class: 'out' }, () => `当前页签:${activeTab()}`),
|
h('span', { class: 'out' }, () => `当前页签:${activeTab()}`),
|
||||||
button({ label: '打开弹窗', onClick: () => modalOpen(true) }),
|
button({ label: '打开弹窗', onClick: () => modalOpen(true) }),
|
||||||
modal(
|
modal(
|
||||||
{ title: '确认操作', open: modalOpen, onClose: () => modalOpen(false) },
|
{
|
||||||
h('p', null, '这是一个由信号驱动的弹窗组件。'),
|
title: '确认操作',
|
||||||
h('div', { class: 'row' }, button({ label: '知道了', onClick: () => modalOpen(false) }), badge({ label: 'modal', variant: 'success' })),
|
open: modalOpen,
|
||||||
|
size: 'lg',
|
||||||
|
onClose: () => modalOpen(false),
|
||||||
|
footer: h(
|
||||||
|
'div',
|
||||||
|
{ class: 'row' },
|
||||||
|
button({ label: '取消', variant: 'ghost', onClick: () => modalOpen(false) }),
|
||||||
|
button({ label: '确定', onClick: () => { modalOpen(false); toast.success('已确认'); } }),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
h('p', null, '这是一个由信号驱动的弹窗组件(size: lg + footer 操作区)。'),
|
||||||
|
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(
|
||||||
|
'sec-menu',
|
||||||
|
'下拉菜单',
|
||||||
|
'menu 触发器 + 自绘弹层:条目支持禁用 / 危险 / 分隔线,↑↓ 环形导航,点外部或 Esc 收起。',
|
||||||
|
h(
|
||||||
|
'div',
|
||||||
|
{ class: 'row' },
|
||||||
|
menu({
|
||||||
|
label: '文件操作',
|
||||||
|
items: [
|
||||||
|
{ value: 'rename', label: '重命名', shortcut: 'F2' },
|
||||||
|
{ value: 'copy', label: '创建副本', shortcut: '⌘D' },
|
||||||
|
{ value: 'archive', label: '归档', disabled: true },
|
||||||
|
{ divider: true, value: '', label: '' },
|
||||||
|
{ value: 'delete', label: '删除', danger: true },
|
||||||
|
],
|
||||||
|
onSelect: (v) => menuPick(v),
|
||||||
|
}),
|
||||||
|
menu({
|
||||||
|
label: '右对齐',
|
||||||
|
align: 'end',
|
||||||
|
variant: 'primary',
|
||||||
|
items: [
|
||||||
|
{ value: 'profile', label: '个人资料' },
|
||||||
|
{ value: 'settings', label: '偏好设置' },
|
||||||
|
],
|
||||||
|
onSelect: (v) => menuPick(v),
|
||||||
|
}),
|
||||||
|
h('span', { class: 'out' }, () => (menuPick() ? `已选:${menuPick()}` : '未选择')),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
section(
|
||||||
|
'sec-popover',
|
||||||
|
'气泡卡片',
|
||||||
|
'popover:可交互弹层(tooltip 只展示,popover 里能点),点外部 / Esc 收起。',
|
||||||
|
h(
|
||||||
|
'div',
|
||||||
|
{ class: 'row' },
|
||||||
|
popover(
|
||||||
|
{
|
||||||
|
title: '筛选条件',
|
||||||
|
content: h(
|
||||||
|
'div',
|
||||||
|
null,
|
||||||
|
h('div', { class: 'row' }, checkbox({ label: '只看我的', checked: agreed })),
|
||||||
|
h('div', { class: 'row' }, button({ label: '应用', onClick: () => toast.success('已应用筛选') })),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
button({ label: '打开气泡卡片' }),
|
||||||
|
),
|
||||||
|
popover({ placement: 'top', trigger: 'hover', content: '悬停出场,移出收起(可放链接)' }, badge({ label: 'hover 触发' })),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
section(
|
||||||
|
'sec-breadcrumb',
|
||||||
|
'面包屑',
|
||||||
|
'breadcrumb:nav + ol 语义结构,末项自动标为当前页,无 href 的条目走 onNavigate。',
|
||||||
|
breadcrumb({
|
||||||
|
items: [{ label: '首页', href: '#sec-counter' }, { label: '组件库' }, { label: '导航类' }, { label: '面包屑' }],
|
||||||
|
separator: '›',
|
||||||
|
onNavigate: (item) => toast.info(`跳转到 ${item.label}`),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
section(
|
||||||
|
'sec-accordion',
|
||||||
|
'折叠面板',
|
||||||
|
'accordion:标题按钮 + grid 行高过渡,默认手风琴单开,multiple 可多开,支持禁用面板。',
|
||||||
|
accordion({
|
||||||
|
multiple: true,
|
||||||
|
items: [
|
||||||
|
{ value: 'a', title: '账户设置', content: '修改昵称、头像与绑定邮箱。' },
|
||||||
|
{ value: 'b', title: '通知偏好', content: alert({ message: '通知偏好:每日摘要已开启', variant: 'info' }) },
|
||||||
|
{ value: 'c', title: '危险操作', content: '删除账号后数据不可恢复。' },
|
||||||
|
{ value: 'd', title: '企业版专属(禁用)', content: '升级后可配置 SSO。', disabled: true },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
),
|
||||||
section(
|
section(
|
||||||
'sec-table',
|
'sec-table',
|
||||||
'数据表格',
|
'数据表格',
|
||||||
@@ -145,13 +524,35 @@ const app = h(
|
|||||||
card({ title: '用户表' }, dataTable({
|
card({ title: '用户表' }, dataTable({
|
||||||
data: users,
|
data: users,
|
||||||
selectable: true,
|
selectable: true,
|
||||||
|
loading: tableLoading,
|
||||||
columns: [
|
columns: [
|
||||||
{ key: 'name', header: '姓名', sortable: true },
|
{ key: 'name', header: '姓名', sortable: true, width: 160 },
|
||||||
{ key: 'age', header: '年龄', sortable: true },
|
{ key: 'age', header: '年龄', sortable: true, align: 'right', width: 100, format: (v) => `${v} 岁` },
|
||||||
{ key: 'city', header: '城市' },
|
{ key: 'city', header: '城市', align: 'center', width: 'flex' },
|
||||||
],
|
],
|
||||||
pageSize: 5,
|
pageSize: 5,
|
||||||
})),
|
selected: picked,
|
||||||
|
}), h(
|
||||||
|
'div',
|
||||||
|
{ class: 'row' },
|
||||||
|
switchButton({ label: '加载态', checked: tableLoading }),
|
||||||
|
badge({ label: '受控选中', variant: 'default' }),
|
||||||
|
h('span', { class: 'out' }, () => `已选 ${picked().length} 行`),
|
||||||
|
button({ label: '清空选择', variant: 'ghost', onClick: () => picked([]) }),
|
||||||
|
)),
|
||||||
|
card(
|
||||||
|
{ title: '独立分页器联动(pageSize 3,共用页码信号)' },
|
||||||
|
dataTable({
|
||||||
|
data: users,
|
||||||
|
columns: [
|
||||||
|
{ key: 'name', header: '姓名' },
|
||||||
|
{ key: 'city', header: '城市' },
|
||||||
|
],
|
||||||
|
pageSize: 3,
|
||||||
|
page: tablePage,
|
||||||
|
}),
|
||||||
|
pagination({ page: tablePage, pageSize: 3, total: users.length, onChange: (p) => tablePage(p) }),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
section(
|
section(
|
||||||
'sec-card',
|
'sec-card',
|
||||||
@@ -161,4 +562,8 @@ const app = h(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const toc = h('aside', { class: 'toc' }, h('div', { class: 'toc-title' }, '目录')); // 目录项由 buildToc 填充
|
||||||
|
const app = h('div', { class: 'shell' }, toc, content);
|
||||||
|
|
||||||
mount('#app', () => app);
|
mount('#app', () => app);
|
||||||
|
buildToc(toc, content);
|
||||||
|
|||||||
+573
-33
@@ -1,6 +1,6 @@
|
|||||||
# @ui-arch 组件库 Agent 文档(无图形版)
|
# @ui-arch 组件库 Agent 文档(无图形版)
|
||||||
|
|
||||||
> 自动生成于 2026-10-23T06:57:51.085Z,数据源:各包 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)。
|
||||||
|
|
||||||
## 快速上手
|
## 快速上手
|
||||||
@@ -38,16 +38,48 @@
|
|||||||
4. 调用组件:const el = textField({ value: name }),把 el 挂到你的容器或配合 h() 组合
|
4. 调用组件:const el = textField({ value: name }),把 el 挂到你的容器或配合 h() 组合
|
||||||
5. 读写状态:name() 读、name('张三') 写,界面自动同步,无需手动操作 DOM
|
5. 读写状态:name() 读、name('张三') 写,界面自动同步,无需手动操作 DOM
|
||||||
|
|
||||||
|
### @ui-arch/cli
|
||||||
|
|
||||||
|
1. 装好依赖后先跑 npm run check 确认基线全绿(build → style → typecheck → smoke → index → verify)
|
||||||
|
2. 查组件:ua list 看全量,ua info <包名> 看单包 api 与约定
|
||||||
|
3. 改完清单跑 npm run docs:同时刷新人类文档站与 llms-full.txt(Agent 首选喂料)
|
||||||
|
4. 提交前 npm run check:style 查注释规范,verify 查清单 ↔ 文档 ↔ 演示锚点是否一致
|
||||||
|
5. 在 ~/.workbuddy/mcp.json 注册 ua mcp 后,Agent 可直接调 list_components / get_component / search_components
|
||||||
|
|
||||||
## @ui-arch/ui(skin-dom)v0.1.0
|
## @ui-arch/ui(skin-dom)v0.1.0
|
||||||
|
|
||||||
16 个 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
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
|
|
||||||
- label(必填):string —— 按钮文案
|
- label(必填):string —— 按钮文案
|
||||||
- variant:'primary' | 'ghost' | 'danger' —— 视觉强度;缺省 primary
|
- variant(默认 primary):'primary' | 'ghost' | 'danger' —— 视觉强度;缺省 primary
|
||||||
|
- disabled:boolean | Signal<boolean> —— 禁用态:布尔直接置位,传信号则动态同步
|
||||||
- onClick(回调):() => void —— 点击回调
|
- onClick(回调):() => void —— 点击回调
|
||||||
|
|
||||||
示例:
|
示例:
|
||||||
@@ -70,7 +102,10 @@ button({ label: '提交', variant: 'primary', onClick: () => console.log('clicke
|
|||||||
|
|
||||||
- value:Signal<string> —— 双向绑定信号:输入写回信号,信号变化写回输入框
|
- value:Signal<string> —— 双向绑定信号:输入写回信号,信号变化写回输入框
|
||||||
- placeholder:string —— 占位文案
|
- placeholder:string —— 占位文案
|
||||||
|
- disabled:boolean | Signal<boolean> —— 禁用态:布尔直接置位,传信号则动态同步
|
||||||
- onInput(回调):(value: string) => void —— 输入回调,每次按键触发
|
- onInput(回调):(value: string) => void —— 输入回调,每次按键触发
|
||||||
|
- prefix:string | HTMLElement —— 前置附加块(textField/inputNumber 在输入框内侧前、textarea 在上方);缺省不渲染
|
||||||
|
- suffix:string | HTMLElement —— 后置附加块(textField/inputNumber 在输入框内侧后、textarea 在下方);缺省不渲染
|
||||||
|
|
||||||
示例:
|
示例:
|
||||||
|
|
||||||
@@ -83,17 +118,49 @@ 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
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
|
|
||||||
- value:Signal<string> —— 双向绑定信号
|
- value:Signal<string> —— 双向绑定信号
|
||||||
- rows:number —— 初始行数;缺省 3
|
- rows(默认 3):number —— 初始行数;缺省 3
|
||||||
- placeholder:string —— 占位文案
|
- placeholder:string —— 占位文案
|
||||||
|
- disabled:boolean | Signal<boolean> —— 禁用态:布尔直接置位,传信号则动态同步
|
||||||
- onInput(回调):(value: string) => void —— 输入回调
|
- onInput(回调):(value: string) => void —— 输入回调
|
||||||
|
- prefix:string | HTMLElement —— 前置附加块(textField/inputNumber 在输入框内侧前、textarea 在上方);缺省不渲染
|
||||||
|
- suffix:string | HTMLElement —— 后置附加块(textField/inputNumber 在输入框内侧后、textarea 在下方);缺省不渲染
|
||||||
|
|
||||||
示例:
|
示例:
|
||||||
|
|
||||||
@@ -106,15 +173,45 @@ 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
|
||||||
|
|
||||||
|
### slider — 滑块 Slider
|
||||||
|
|
||||||
|
参数:
|
||||||
|
|
||||||
|
- value(必填):Signal<number> —— 当前值信号,双向绑定:拖动写回,外部赋值同步位置
|
||||||
|
- min(默认 0):number —— 最小值;缺省 0
|
||||||
|
- max(默认 100):number —— 最大值;缺省 100
|
||||||
|
- step(默认 1):number —— 步长;缺省 1
|
||||||
|
- disabled:boolean | Signal<boolean> —— 禁用态:布尔直接置位,传信号则动态同步
|
||||||
|
- showValue(默认 true):boolean —— 是否右侧显示当前值;缺省 true
|
||||||
|
- onChange(回调):(value: number) => void —— 拖动回调
|
||||||
|
|
||||||
|
示例:
|
||||||
|
|
||||||
|
```
|
||||||
|
const vol = signal(40); slider({ value: vol, min: 0, max: 100, step: 5, onChange: (v) => console.log(v) })
|
||||||
|
```
|
||||||
|
|
||||||
|
手把手:
|
||||||
|
|
||||||
|
1. 创建数值信号(初值在 min/max 区间内),传入 value 完成双向绑定
|
||||||
|
2. 用 min / max / step 限定范围与粒度,如 step: 5 只取 5 的倍数
|
||||||
|
3. 拖动或按 ←→ 键改变值:原生 range 自带键盘与触屏交互,无需额外处理
|
||||||
|
4. 只想看拖动条不显示数字时传 showValue: false;需要自定义展示可自行读 signal
|
||||||
|
5. 禁用场景传 disabled(布尔或信号),置灰且不可拖
|
||||||
|
|
||||||
|
现场演示:apps/docs/demo.html#sec-slider
|
||||||
|
|
||||||
### checkbox — 复选框 Checkbox
|
### checkbox — 复选框 Checkbox
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
|
|
||||||
- checked:Signal<boolean> —— 勾选状态信号,双向绑定
|
- checked:Signal<boolean> —— 勾选状态信号,双向绑定
|
||||||
- label:string —— 右侧文案;缺省只渲染勾选框
|
- label:string —— 右侧文案;缺省只渲染勾选框
|
||||||
|
- disabled:boolean | Signal<boolean> —— 禁用态:布尔直接置位,传信号则动态同步
|
||||||
- onChange(回调):(checked: boolean) => void —— 变更回调
|
- onChange(回调):(checked: boolean) => void —— 变更回调
|
||||||
|
|
||||||
示例:
|
示例:
|
||||||
@@ -137,6 +234,7 @@ const agreed = signal(false); checkbox({ label: '同意条款', checked: agreed,
|
|||||||
|
|
||||||
- checked:Signal<boolean> —— 开关状态信号,双向绑定
|
- checked:Signal<boolean> —— 开关状态信号,双向绑定
|
||||||
- label:string —— 左侧文案
|
- label:string —— 左侧文案
|
||||||
|
- disabled:boolean | Signal<boolean> —— 禁用态:布尔直接置位,传信号则动态同步
|
||||||
- onChange(回调):(checked: boolean) => void —— 变更回调
|
- onChange(回调):(checked: boolean) => void —— 变更回调
|
||||||
|
|
||||||
示例:
|
示例:
|
||||||
@@ -160,6 +258,7 @@ const night = signal(true); switchButton({ label: '夜间模式', checked: night
|
|||||||
- options(必填):OptionItem[] —— 选项列表,每项 { value, label }
|
- options(必填):OptionItem[] —— 选项列表,每项 { value, label }
|
||||||
- value:Signal<string> —— 选中值信号,双向绑定
|
- value:Signal<string> —— 选中值信号,双向绑定
|
||||||
- placeholder:string —— 未选中时触发器显示的占位文案
|
- placeholder:string —— 未选中时触发器显示的占位文案
|
||||||
|
- disabled:boolean | Signal<boolean> —— 禁用态:布尔直接置位,传信号则动态同步;禁用时触发器置灰且不展开弹层
|
||||||
- onChange(回调):(value: string) => void —— 选中回调
|
- onChange(回调):(value: string) => void —— 选中回调
|
||||||
|
|
||||||
示例:
|
示例:
|
||||||
@@ -177,12 +276,40 @@ const city = signal(''); select({ options: [{ value: 'bj', label: '北京' }], v
|
|||||||
|
|
||||||
现场演示:apps/docs/demo.html#sec-form
|
现场演示:apps/docs/demo.html#sec-form
|
||||||
|
|
||||||
|
### menu — 下拉菜单 Menu
|
||||||
|
|
||||||
|
参数:
|
||||||
|
|
||||||
|
- label(必填):string —— 触发器文案
|
||||||
|
- items(必填):MenuItem[] —— 菜单条目:{ value, label, disabled?, danger?, divider?, shortcut? }
|
||||||
|
- variant(默认 ghost):'primary' | 'ghost' —— 触发器视觉强度;缺省 ghost
|
||||||
|
- align(默认 start):'start' | 'end' —— 弹层对齐方向;缺省 start(与触发器左对齐)
|
||||||
|
- onSelect(回调):(value: string) => void —— 选中回调,禁用项不触发
|
||||||
|
|
||||||
|
示例:
|
||||||
|
|
||||||
|
```
|
||||||
|
menu({ label: '操作', items: [{ value: 'edit', label: '编辑' }, { value: 'del', label: '删除', danger: true }], onSelect: (v) => console.log(v) })
|
||||||
|
```
|
||||||
|
|
||||||
|
手把手:
|
||||||
|
|
||||||
|
1. 准备 items 数组:value 是回调标识、label 是展示文案,插一条 { divider: true } 即可分出条目组
|
||||||
|
2. 传 label 决定触发器文案;默认 ghost 轻量样式,主操作用 variant: 'primary'
|
||||||
|
3. 点触发器展开弹层,点条目收起并触发 onSelect;点外部或按 Esc 也会收起(Esc 后焦点回到触发器)
|
||||||
|
4. 条目要给快捷键提示时传 shortcut(如 '⌘K'):右侧灰字展示,组件不绑定按键,实际快捷键由调用方在全局监听
|
||||||
|
5. 键盘:Tab 聚焦触发器,↑↓ 展开并在可用条目间环形移动,Enter / Space 选中;disabled 项自动跳过
|
||||||
|
6. 靠右对齐避免溢出时传 align: 'end';危险操作给条目加 danger: true
|
||||||
|
|
||||||
|
现场演示:apps/docs/demo.html#sec-menu
|
||||||
|
|
||||||
### radioGroup — 单选组 RadioGroup
|
### radioGroup — 单选组 RadioGroup
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
|
|
||||||
- options(必填):OptionItem[] —— 选项列表
|
- options(必填):OptionItem[] —— 选项列表
|
||||||
- value:Signal<string> —— 选中值信号,双向绑定
|
- value:Signal<string> —— 选中值信号,双向绑定
|
||||||
|
- disabled:boolean | Signal<boolean> —— 整组禁用:全部选项与文案置灰且不可选
|
||||||
- onChange(回调):(value: string) => void —— 变更回调
|
- onChange(回调):(value: string) => void —— 变更回调
|
||||||
|
|
||||||
示例:
|
示例:
|
||||||
@@ -196,9 +323,58 @@ const size = signal('m'); radioGroup({ options: [{ value: 's', label: '小' }, {
|
|||||||
1. 创建字符串信号,初值设为默认选项的 value
|
1. 创建字符串信号,初值设为默认选项的 value
|
||||||
2. 传入 options 与 value,组内自动互斥
|
2. 传入 options 与 value,组内自动互斥
|
||||||
3. 读取 size() 获取当前选中值
|
3. 读取 size() 获取当前选中值
|
||||||
|
4. 需要整组禁用时传 disabled:布尔值静态禁用,也可传信号(如 locked)实现动态锁定
|
||||||
|
|
||||||
现场演示:apps/docs/demo.html#sec-form
|
现场演示:apps/docs/demo.html#sec-form
|
||||||
|
|
||||||
|
### accordion — 折叠面板 Accordion
|
||||||
|
|
||||||
|
参数:
|
||||||
|
|
||||||
|
- items(必填):AccordionItem[] —— 面板列表:{ value, title, content, disabled? }
|
||||||
|
- active:Signal<string> —— 受控展开面板信号(单开模式);缺省由组件内部持有状态
|
||||||
|
- multiple(默认 false):boolean —— 允许多个面板同时展开;缺省 false(手风琴,单开)
|
||||||
|
- onChange(回调):(active: string) => void —— 展开变化回调:收起时传空串
|
||||||
|
|
||||||
|
示例:
|
||||||
|
|
||||||
|
```
|
||||||
|
accordion({ items: [{ value: 'a', title: '账户设置', content: '内容…' }, { value: 'b', title: '通知', content: h('p', null, '…') }] })
|
||||||
|
```
|
||||||
|
|
||||||
|
手把手:
|
||||||
|
|
||||||
|
1. 准备 items:value 作标识、title 作标题、content 可为字符串 / DOM 节点 / 响应式函数
|
||||||
|
2. 默认手风琴模式:展开一个自动收起其他;需要同时展开多个时传 multiple: true
|
||||||
|
3. 需要外部控制或读取展开项时传 active 信号(受控单开),否则组件内部自持状态
|
||||||
|
4. 标题是原生 button:Tab 聚焦、Enter / Space 切换,展开态带 aria-expanded + region 关联
|
||||||
|
5. 内容区用 grid 行高 0fr → 1fr 过渡,无需测量高度;reduced-motion 用户自动静态切换
|
||||||
|
|
||||||
|
现场演示:apps/docs/demo.html#sec-accordion
|
||||||
|
|
||||||
|
### breadcrumb — 面包屑 Breadcrumb
|
||||||
|
|
||||||
|
参数:
|
||||||
|
|
||||||
|
- items(必填):BreadcrumbItem[] —— 路径条目:{ label, href? },末项自动视为当前页
|
||||||
|
- separator(默认 /):string —— 分隔符;缺省 '/'
|
||||||
|
- onNavigate(回调):(item, index) => void —— 无 href 条目的点击回调
|
||||||
|
|
||||||
|
示例:
|
||||||
|
|
||||||
|
```
|
||||||
|
breadcrumb({ items: [{ label: '首页', href: '/' }, { label: '设置' }, { label: '账户' }], onNavigate: (it) => console.log(it.label) })
|
||||||
|
```
|
||||||
|
|
||||||
|
手把手:
|
||||||
|
|
||||||
|
1. 按层级顺序准备 items,最后一项即当前页,自动渲染为不可点的灰色文字并带 aria-current='page'
|
||||||
|
2. 需要真实跳转就给条目加 href(渲染 <a>);单页应用想自行接管时不给 href,改用 onNavigate 回调
|
||||||
|
3. 改分隔符传 separator,如 '›';分隔符对读屏隐藏
|
||||||
|
4. 语义结构为 nav + ol + li,可直接放进页头或卡片标题栏
|
||||||
|
|
||||||
|
现场演示:apps/docs/demo.html#sec-breadcrumb
|
||||||
|
|
||||||
### card — 卡片 Card
|
### card — 卡片 Card
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
@@ -225,7 +401,7 @@ card({ title: '用户表' }, dataTable({ data, columns }))
|
|||||||
参数:
|
参数:
|
||||||
|
|
||||||
- label(必填):string —— 徽标文案
|
- label(必填):string —— 徽标文案
|
||||||
- variant:'default' | 'success' | 'warning' | 'danger' —— 语义色;缺省 default
|
- variant(默认 default):'default' | 'success' | 'warning' | 'danger' —— 语义色;缺省 default
|
||||||
|
|
||||||
示例:
|
示例:
|
||||||
|
|
||||||
@@ -237,15 +413,68 @@ badge({ label: '成功', variant: 'success' })
|
|||||||
|
|
||||||
1. 选择语义色:default 中性、success 成功、warning 警告、danger 危险
|
1. 选择语义色:default 中性、success 成功、warning 警告、danger 危险
|
||||||
2. 传入 label 即可,纯展示组件无状态
|
2. 传入 label 即可,纯展示组件无状态
|
||||||
|
3. 状态语义靠 variant 承载,不要自造颜色;纯展示不可交互,需要操作请配 button
|
||||||
|
|
||||||
现场演示:apps/docs/demo.html#sec-feedback
|
现场演示:apps/docs/demo.html#sec-feedback
|
||||||
|
|
||||||
|
### tag — 标签 Tag
|
||||||
|
|
||||||
|
参数:
|
||||||
|
|
||||||
|
- label(必填):string —— 标签文案
|
||||||
|
- variant(默认 default):'default' | 'success' | 'warning' | 'danger' | 'info' —— 语义色;缺省 default
|
||||||
|
- closable(默认 false):boolean —— 显示 × 关闭按钮;缺省 false(纯展示)
|
||||||
|
- onClose(回调):() => void —— 关闭回调:调用方从数据源移除该项后重新渲染
|
||||||
|
|
||||||
|
示例:
|
||||||
|
|
||||||
|
```
|
||||||
|
tag({ label: '前端', variant: 'info', closable: true, onClose: () => remove('前端') })
|
||||||
|
```
|
||||||
|
|
||||||
|
手把手:
|
||||||
|
|
||||||
|
1. 纯展示用 badge,需要可移除的标签(筛选项 / 已选条件)用 tag
|
||||||
|
2. 按语义选 variant:info 中性标记、success 通过、warning 待处理、danger 已失效
|
||||||
|
3. closable: true 时必须配 onClose:把数据源里的该项删掉再重渲染(组件无状态,不自行摘除节点)
|
||||||
|
4. 组合用法:容器里用 renderList 按信号数组渲染 tag 列表,删除即写回信号
|
||||||
|
5. 标签是内联元素,可直接排进 .row 等容器,自动换行由容器决定
|
||||||
|
|
||||||
|
现场演示:apps/docs/demo.html#sec-tag
|
||||||
|
|
||||||
|
### avatar — 头像 Avatar
|
||||||
|
|
||||||
|
参数:
|
||||||
|
|
||||||
|
- name(必填):string —— 名称:生成占位首字,并作为图片 alt 与悬停标题
|
||||||
|
- src:string —— 图片地址;缺省用首字占位
|
||||||
|
- size(默认 32):number —— 直径像素;缺省 32
|
||||||
|
- variant(默认 default):'default' | 'success' | 'warning' | 'danger' —— 占位底色语义档;缺省 default
|
||||||
|
|
||||||
|
示例:
|
||||||
|
|
||||||
|
```
|
||||||
|
avatar({ name: '张三' }); avatar({ name: 'Alice', size: 40, variant: 'success' })
|
||||||
|
```
|
||||||
|
|
||||||
|
手把手:
|
||||||
|
|
||||||
|
1. 只传 name:用名称首字做占位(中文取首字、英文大写),底色走 accent 淡化,暗色自动适配
|
||||||
|
2. 有图片就传 src:渲染 img 并用 name 作 alt,读屏语义完整;图片缺失时由浏览器 alt 兜底
|
||||||
|
3. 尺寸用 size(像素)控制,头像不参与布局拉伸(flex:none),可安全排进 .row 与表格单元格
|
||||||
|
4. 需要区分类别(在线/告警)时用 variant 换底色档,颜色一律来自令牌
|
||||||
|
|
||||||
|
现场演示:apps/docs/demo.html#sec-avatar
|
||||||
|
|
||||||
### alert — 提示条 Alert
|
### alert — 提示条 Alert
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
|
|
||||||
- message(必填):string —— 提示内容
|
- message(必填):string —— 提示内容
|
||||||
- variant:'info' | 'success' | 'warning' | 'danger' —— 语义色;缺省 info
|
- title:string —— 加粗标题行;缺省只渲染正文
|
||||||
|
- variant(默认 info):'info' | 'success' | 'warning' | 'danger' —— 语义色;缺省 info
|
||||||
|
- closable(默认 false):boolean —— 显示 × 关闭按钮,退场后从 DOM 移除;缺省 false
|
||||||
|
- onClose(回调):() => void —— 关闭回调,供调用方同步业务状态
|
||||||
|
|
||||||
示例:
|
示例:
|
||||||
|
|
||||||
@@ -257,14 +486,118 @@ alert({ message: '操作可能存在风险', variant: 'warning' })
|
|||||||
|
|
||||||
1. 按信息级别选 variant:info 提示、success 成功、warning 警告、danger 错误
|
1. 按信息级别选 variant:info 提示、success 成功、warning 警告、danger 错误
|
||||||
2. 传入 message 即可,入场自带下滑动画
|
2. 传入 message 即可,入场自带下滑动画
|
||||||
|
3. 需要可关闭或带操作按钮时,外层用 h() 组合 button 自行拼装,alert 只负责语义底色
|
||||||
|
4. 需要用户可自行消除的提示传 closable: true:点 × 淡出并从 DOM 移除,配合 onClose 同步业务状态
|
||||||
|
5. 有标题与正文两层信息时传 title(加粗首行),正文仍走 message
|
||||||
|
|
||||||
现场演示:apps/docs/demo.html#sec-feedback
|
现场演示:apps/docs/demo.html#sec-feedback
|
||||||
|
|
||||||
|
### skeleton — 骨架屏 Skeleton
|
||||||
|
|
||||||
|
参数:
|
||||||
|
|
||||||
|
- variant(默认 rect):'text' | 'rect' | 'circle' —— 占位形状;缺省 rect(整块矩形)
|
||||||
|
- rows(默认 3):number —— text 形状的行数;缺省 3,末行自动缩短模拟段落
|
||||||
|
|
||||||
|
示例:
|
||||||
|
|
||||||
|
```
|
||||||
|
skeleton({ variant: 'text', rows: 3 }); skeleton({ variant: 'circle' }); skeleton({ variant: 'rect' })
|
||||||
|
```
|
||||||
|
|
||||||
|
手把手:
|
||||||
|
|
||||||
|
1. 数据加载期间用 skeleton 顶替真实内容,加载完成后整体替换
|
||||||
|
2. 文本段落用 variant: 'text' + rows 控制行数,头像用 'circle',卡片/图片区域用 'rect'
|
||||||
|
3. 占位块自带扫光动画,reduced-motion 用户自动静态化为纯灰块
|
||||||
|
4. 组合布局:skeleton 是普通 DOM,可放进 card / row 等任意容器
|
||||||
|
|
||||||
|
现场演示:apps/docs/demo.html#sec-skeleton
|
||||||
|
|
||||||
|
### toast — 消息提示 Toast
|
||||||
|
|
||||||
|
参数:
|
||||||
|
|
||||||
|
- message(必填):string —— 提示文案
|
||||||
|
- duration(默认 3000):number —— 自动消失毫秒;缺省 3000
|
||||||
|
- closable(默认 false):boolean —— 显示关闭按钮,点 × 立即收起;缺省 false(仅可关闭的 toast 接收指针事件,其余保持穿透)
|
||||||
|
- variant(默认 info):'info' | 'success' | 'warning' | 'danger' —— 语义色;缺省 info(toast.error 对应 danger)
|
||||||
|
|
||||||
|
示例:
|
||||||
|
|
||||||
|
```
|
||||||
|
toast.success('保存成功'); toast.error('网络请求失败', 5000)
|
||||||
|
```
|
||||||
|
|
||||||
|
手把手:
|
||||||
|
|
||||||
|
1. 直接调用语义工厂:toast.info / success / warning / error(message, duration?),无需先建容器
|
||||||
|
2. 不传 duration 时默认停留 3 秒后自动淡出并移除节点
|
||||||
|
3. 多条消息在右上角自动纵向堆叠,互不遮挡
|
||||||
|
4. 需要完整入参(含 variant)时用 showToast(props);组件挂载容器由内部懒创建,样式随 ui.css 一起引入
|
||||||
|
5. 同屏最多 4 条,超出自动挤掉最早一条;需要用户主动确认的消息用 showToast({ closable: true }) 给个 ×
|
||||||
|
|
||||||
|
现场演示:apps/docs/demo.html#sec-toast
|
||||||
|
|
||||||
|
### tooltip — 悬浮提示 Tooltip
|
||||||
|
|
||||||
|
参数:
|
||||||
|
|
||||||
|
- content(必填):string —— 提示文案(纯文本)
|
||||||
|
- placement(默认 top):'top' | 'bottom' —— 气泡方位;缺省 top(触发器上方)
|
||||||
|
- delay(默认 150):number —— 出场延迟毫秒;缺省 150,传 0 立即显示
|
||||||
|
- trigger(必填):HTMLElement —— 第二个参数:被包裹的触发器元素(按钮 / 输入框等任意 DOM)
|
||||||
|
|
||||||
|
示例:
|
||||||
|
|
||||||
|
```
|
||||||
|
tooltip({ content: '保存后立即生效' }, button({ label: '保存', onClick: save }))
|
||||||
|
```
|
||||||
|
|
||||||
|
手把手:
|
||||||
|
|
||||||
|
1. 先创建触发器元素,如 const btn = button({ label: '保存' })
|
||||||
|
2. 用 tooltip(props, trigger) 包一层:返回带气泡的包裹元素,原触发器仍是独立节点
|
||||||
|
3. 鼠标移入或键盘聚焦约 150ms 后出场,移出 / 失焦立即收起;按 Esc 可立刻关闭
|
||||||
|
4. 气泡默认在上方,空间不足或需要避让时传 placement: 'bottom'
|
||||||
|
5. 气泡 pointer-events: none 不抢指针;判读依赖 aria-describedby + role='tooltip',读屏可读
|
||||||
|
6. 只做展示:不要在气泡里放可点击元素,需要交互请用 modal 或自绘弹层
|
||||||
|
|
||||||
|
现场演示:apps/docs/demo.html#sec-tooltip
|
||||||
|
|
||||||
|
### popover — 气泡卡片 Popover
|
||||||
|
|
||||||
|
参数:
|
||||||
|
|
||||||
|
- content(必填):unknown —— 弹层内容:字符串 / DOM 节点 / 响应式函数,可放按钮等交互元素
|
||||||
|
- title:string —— 弹层标题;缺省不渲染标题行
|
||||||
|
- placement(默认 bottom):'top' | 'bottom' —— 弹层方位;缺省 bottom(触发器下方)
|
||||||
|
- trigger(默认 click):'click' | 'hover' —— 触发方式;缺省 click
|
||||||
|
- delay(默认 150):number —— hover 模式出场延迟毫秒;缺省 150
|
||||||
|
- onOpenChange(回调):(open: boolean) => void —— 开合回调
|
||||||
|
- triggerEl(第二个参数)(必填):HTMLElement —— 第二个参数:被包裹的触发器元素
|
||||||
|
|
||||||
|
示例:
|
||||||
|
|
||||||
|
```
|
||||||
|
popover({ title: '筛选条件', content: h('div', null, '任意可交互内容') }, button({ label: '筛选' }))
|
||||||
|
```
|
||||||
|
|
||||||
|
手把手:
|
||||||
|
|
||||||
|
1. 创建触发器元素,再用 popover(props, trigger) 包一层:返回带弹层的包裹元素
|
||||||
|
2. 点击触发器开合;点弹层外部或按 Esc 收起(Esc 后焦点回触发器)
|
||||||
|
3. content 里可以放 button / checkbox 等交互元素(tooltip 只能纯文本),点击弹层内部不会误关
|
||||||
|
4. hover 场景传 trigger: 'hover' + delay 控制多快出场;placement: 'top' 让弹层往上翻
|
||||||
|
5. 需要感知开合做副作用(如懒加载数据)时用 onOpenChange
|
||||||
|
|
||||||
|
现场演示:apps/docs/demo.html#sec-popover
|
||||||
|
|
||||||
### spinner — 加载指示器 Spinner
|
### spinner — 加载指示器 Spinner
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
|
|
||||||
- size:number —— 直径像素;缺省 18
|
- size(默认 18):number —— 直径像素;缺省 18
|
||||||
|
|
||||||
示例:
|
示例:
|
||||||
|
|
||||||
@@ -276,6 +609,7 @@ spinner(); spinner({ size: 26 })
|
|||||||
|
|
||||||
1. 数据加载时插入 spinner(),加载完成移除
|
1. 数据加载时插入 spinner(),加载完成移除
|
||||||
2. 用 size 匹配上下文尺寸(按钮内用小号,整页 loading 用大号)
|
2. 用 size 匹配上下文尺寸(按钮内用小号,整页 loading 用大号)
|
||||||
|
3. 与 skeleton 分工:已知内容结构用 skeleton 占位,只有『在跑』的短等待用 spinner
|
||||||
|
|
||||||
现场演示:apps/docs/demo.html#sec-feedback
|
现场演示:apps/docs/demo.html#sec-feedback
|
||||||
|
|
||||||
@@ -283,8 +617,8 @@ spinner(); spinner({ size: 26 })
|
|||||||
|
|
||||||
参数:
|
参数:
|
||||||
|
|
||||||
- value(必填):Signal<number> —— 进度 0-100 信号,变化自动平滑过渡
|
- value:Signal<number> —— 当前进度 0-100 信号,变化自动平滑过渡(越界钳制)
|
||||||
- showText:boolean —— 是否显示百分比文字;缺省 true
|
- showText(默认 true):boolean —— 是否显示百分比文字;缺省 true
|
||||||
|
|
||||||
示例:
|
示例:
|
||||||
|
|
||||||
@@ -294,10 +628,12 @@ const pct = signal(0); progress({ value: pct }); setInterval(() => pct((pct() +
|
|||||||
|
|
||||||
手把手:
|
手把手:
|
||||||
|
|
||||||
1. 创建数值信号 pct = signal(0),范围 0-100(越界自动钳制)
|
1. 时长未知的加载直接 progress() 不传 value:填充块匀速往复,不显示百分比,读屏读作进行中
|
||||||
2. 传入 value,填充宽度与百分比文字随信号平滑过渡
|
2. 创建数值信号 pct = signal(0),范围 0-100(越界自动钳制)
|
||||||
3. 进行中填充带流动条纹,到 100% 自动变 success 色并停止条纹
|
3. 传入 value,填充宽度与百分比文字随信号平滑过渡
|
||||||
4. 不需要百分比文字时传 showText: false
|
4. 进行中填充带流动条纹,到 100% 自动变 success 色并停止条纹
|
||||||
|
5. 不需要百分比文字时传 showText: false
|
||||||
|
6. 定量进度用 value 信号:满格自动切 success 色并停止条纹,role=progressbar + aria-valuenow 同步
|
||||||
|
|
||||||
现场演示:apps/docs/demo.html#sec-feedback
|
现场演示:apps/docs/demo.html#sec-feedback
|
||||||
|
|
||||||
@@ -324,12 +660,129 @@ 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
|
||||||
|
|
||||||
|
参数:
|
||||||
|
|
||||||
|
- page(必填):Signal<number> —— 当前页码信号,双向绑定:外部赋值自动翻页,点击页码写回信号
|
||||||
|
- pageSize(必填):number —— 每页条数
|
||||||
|
- total(必填):number —— 总条数
|
||||||
|
- onChange(回调):(page: number) => void —— 翻页回调
|
||||||
|
|
||||||
|
示例:
|
||||||
|
|
||||||
|
```
|
||||||
|
const page = signal(1); pagination({ page, pageSize: 3, total: 24, onChange: (p) => console.log(p) })
|
||||||
|
```
|
||||||
|
|
||||||
|
手把手:
|
||||||
|
|
||||||
|
1. 创建页码信号 page = signal(1),与 pageSize / total 一起传入
|
||||||
|
2. 页码 ≤7 全部展开;更多时只保留首尾与当前页 ±1,缺口折叠为省略号
|
||||||
|
3. 首末页自动禁用 ‹ › 翻页按钮,当前页禁点并标记 aria-current
|
||||||
|
4. 与 dataTable 联动:把同一个 page 信号传给 dataTable 的 page 参数,两处翻页自动同步
|
||||||
|
5. 键盘可达:全部是原生 button,Tab 聚焦后 Enter / Space 触发
|
||||||
|
|
||||||
|
现场演示:apps/docs/demo.html#sec-table
|
||||||
|
|
||||||
### modal — 弹窗 Modal
|
### modal — 弹窗 Modal
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
|
|
||||||
- open(必填):Signal<boolean> —— 开关信号:true 显示 / false 隐藏
|
- open(必填):Signal<boolean> —— 开关信号:true 显示 / false 隐藏
|
||||||
- title(必填):string —— 标题
|
- title(必填):string —— 标题
|
||||||
|
- size(默认 md):'sm' | 'md' | 'lg' —— 宽度档位:sm 340px / md 420px / lg 640px;缺省 md
|
||||||
|
- footer:unknown —— 底部操作区内容(常见为按钮组,右对齐);缺省不渲染
|
||||||
- onClose(回调):() => void —— 关闭回调(点遮罩、× 或 Esc 触发)
|
- onClose(回调):() => void —— 关闭回调(点遮罩、× 或 Esc 触发)
|
||||||
- ...children:unknown[] —— 弹窗内容子节点
|
- ...children:unknown[] —— 弹窗内容子节点
|
||||||
|
|
||||||
@@ -345,14 +798,43 @@ const open = signal(false); modal({ title: '确认操作', open, onClose: () =>
|
|||||||
2. 把 open 传给 modal,其余参数为弹窗内容子节点
|
2. 把 open 传给 modal,其余参数为弹窗内容子节点
|
||||||
3. 打开:open(true);关闭:open(false),点遮罩或 × 会自动写回 false
|
3. 打开:open(true);关闭:open(false),点遮罩或 × 会自动写回 false
|
||||||
4. 入场自带遮罩淡入 + 弹窗上浮动画
|
4. 入场自带遮罩淡入 + 弹窗上浮动画
|
||||||
|
5. 底部操作按钮放进 footer(如取消/确定),需要更宽内容用 size: 'lg',窄确认框用 'sm'
|
||||||
|
|
||||||
现场演示:apps/docs/demo.html#sec-tabs-modal
|
现场演示:apps/docs/demo.html#sec-tabs-modal
|
||||||
|
|
||||||
|
### drawer — 抽屉 Drawer
|
||||||
|
|
||||||
|
参数:
|
||||||
|
|
||||||
|
- title(必填):string —— 标题
|
||||||
|
- open(必填):Signal<boolean> —— 开关信号:true 滑出 / false 收起
|
||||||
|
- placement(默认 right):'left' | 'right' —— 滑出方向;缺省 right
|
||||||
|
- width(默认 320):number —— 面板宽度像素(窄屏最多 92vw);缺省 320
|
||||||
|
- footer:unknown —— 底部操作区内容;缺省不渲染
|
||||||
|
- onClose(回调):() => void —— 关闭回调(点遮罩 / × / Esc 触发)
|
||||||
|
- ...children:unknown[] —— 面板内容子节点
|
||||||
|
|
||||||
|
示例:
|
||||||
|
|
||||||
|
```
|
||||||
|
const open = signal(false); drawer({ title: '详情', open, width: 360, onClose: () => open(false) }, h('p', null, '内容'))
|
||||||
|
```
|
||||||
|
|
||||||
|
手把手:
|
||||||
|
|
||||||
|
1. 创建布尔信号 open = signal(false),传给 drawer;其余参数为面板内容子节点
|
||||||
|
2. 打开后从侧边滑出:placement: 'right'(默认)或 'left',width 控制宽度(窄屏自动收到 92vw)
|
||||||
|
3. 点遮罩、× 或按 Esc 关闭并写回 false;打开时焦点落在面板首个可交互元素
|
||||||
|
4. 底部操作按钮放进 footer;与 modal 的分工:modal 是强打断的确认,drawer 保留主视图上下文做详情/编辑
|
||||||
|
5. 面板内容与 modal 一样接受任意 DOM 与响应式函数
|
||||||
|
|
||||||
|
现场演示:apps/docs/demo.html#sec-drawer
|
||||||
|
|
||||||
### form — 表单 Form / 表单字段 FormItem
|
### form — 表单 Form / 表单字段 FormItem
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
|
|
||||||
- layout:'vertical' | 'horizontal' —— 布局方向;缺省 vertical(horizontal 为标签左置)
|
- layout(默认 vertical):'vertical' | 'horizontal' —— 布局方向;缺省 vertical(horizontal 为标签左置)
|
||||||
- submitText:string —— 提交按钮文案;缺省「提交」
|
- submitText:string —— 提交按钮文案;缺省「提交」
|
||||||
- resetText:string —— 重置按钮文案;缺省不渲染
|
- resetText:string —— 重置按钮文案;缺省不渲染
|
||||||
- onSubmit(回调):() => void —— 全部字段校验通过后的提交回调
|
- onSubmit(回调):() => void —— 全部字段校验通过后的提交回调
|
||||||
@@ -360,7 +842,7 @@ const open = signal(false); modal({ title: '确认操作', open, onClose: () =>
|
|||||||
- (formItem)value(必填):Signal<string> —— 字段值信号,校验数据源
|
- (formItem)value(必填):Signal<string> —— 字段值信号,校验数据源
|
||||||
- (formItem)required:boolean —— 必填:显示红星并自动追加非空规则
|
- (formItem)required:boolean —— 必填:显示红星并自动追加非空规则
|
||||||
- (formItem)rules:FormRule[] —— 校验规则:required / min / max / pattern / validator,按序执行取首个失败
|
- (formItem)rules:FormRule[] —— 校验规则:required / min / max / pattern / validator,按序执行取首个失败
|
||||||
- (formItem)validateOn:'input' | 'blur' | 'submit' —— 校验时机;缺省 blur(输入时自动清已报错误)
|
- (formItem)validateOn(默认 blur):'input' | 'blur' | 'submit' —— 校验时机;缺省 blur(输入时自动清已报错误)
|
||||||
|
|
||||||
示例:
|
示例:
|
||||||
|
|
||||||
@@ -376,6 +858,8 @@ const name = signal(''); const f = form({ onSubmit: submit }, [formItem({ label:
|
|||||||
4. 提交时逐项校验:全过触发 onSubmit;有错则标红控件、显示错误文案并聚焦首个出错项
|
4. 提交时逐项校验:全过触发 onSubmit;有错则标红控件、显示错误文案并聚焦首个出错项
|
||||||
5. 规则语义:required 非空(trim)、min/max 长度、pattern 正则、validator 自定义(true 通过 / 字符串为错误)
|
5. 规则语义:required 非空(trim)、min/max 长度、pattern 正则、validator 自定义(true 通过 / 字符串为错误)
|
||||||
6. 非必填字段为空时跳过其余规则;Enter 键触发表单原生 submit(已拦截走校验流程)
|
6. 非必填字段为空时跳过其余规则;Enter 键触发表单原生 submit(已拦截走校验流程)
|
||||||
|
7. 错误态覆盖包裹式控件:标红作用于控件根与内部原生控件(含自绘 select 的触发器),并同步 aria-invalid,读屏与视觉一致
|
||||||
|
8. 触发时机用 input / change / focusout(均会冒泡),因此 checkbox / select / 单选组这类包裹式控件同样能失焦校验
|
||||||
|
|
||||||
现场演示:apps/docs/demo.html#sec-formlab
|
现场演示:apps/docs/demo.html#sec-formlab
|
||||||
|
|
||||||
@@ -384,10 +868,13 @@ const name = signal(''); const f = form({ onSubmit: submit }, [formItem({ label:
|
|||||||
参数:
|
参数:
|
||||||
|
|
||||||
- data(必填):T[] —— 全量行数据,每行须含 id 字段作 rowKey
|
- data(必填):T[] —— 全量行数据,每行须含 id 字段作 rowKey
|
||||||
- columns(必填):ColumnDef<T>[] —— 列定义:{ key, header?, sortable? }
|
- columns(必填):ColumnDef<T>[] —— 列定义:内核字段 { key, header?, sortable?, width? } + 皮肤字段 { align?, format? };width 数字为固定像素,'flex' 自适应
|
||||||
- filterKey:string —— 工具栏筛选的列;缺省取第一列
|
- filterKey:string —— 工具栏筛选的列;缺省取第一列
|
||||||
- pageSize:number —— 每页行数;缺省 5
|
- pageSize(默认 5):number —— 每页行数;缺省 5
|
||||||
- selectable:boolean —— 启用行选择列:全选三态(含半选)、行 checkbox、已选计数;缺省 false
|
- selectable(默认 false):boolean —— 启用行选择列:全选三态(含半选)、行 checkbox、已选计数;缺省 false
|
||||||
|
- page:Signal<number> —— 受控页码信号:与内核分页双向同步,传同一个信号给独立 pagination 组件即可联动
|
||||||
|
- loading:boolean | Signal<boolean> —— 加载态:为真时数据行换成骨架行(行数随 pageSize),筛选输入与翻页按钮临时禁用,容器带 aria-busy
|
||||||
|
- selected:Signal<RowKey[]> —— 受控选中行信号:与内核选择双向同步(内容比较,不来回派发),供批量操作读取/改写
|
||||||
|
|
||||||
示例:
|
示例:
|
||||||
|
|
||||||
@@ -399,32 +886,55 @@ dataTable({ data: users, selectable: true, columns: [{ key: 'name', header: '姓
|
|||||||
|
|
||||||
1. 准备行数据,确保每行有唯一 id(跨页选择与刷新不丢行靠它)
|
1. 准备行数据,确保每行有唯一 id(跨页选择与刷新不丢行靠它)
|
||||||
2. 定义 columns:sortable: true 的列表头可点击排序(升 → 降 → 取消循环),标记箭头 ↑↓↕
|
2. 定义 columns:sortable: true 的列表头可点击排序(升 → 降 → 取消循环),标记箭头 ↑↓↕
|
||||||
3. selectable: true 开启行选择:表头全选框带 indeterminate 半选态,工具栏显示已选计数
|
3. 列展示细节用皮肤字段:align 控制表头与单元格对齐,format(value, row) 返回纯文本(走 textContent,不需要转义)
|
||||||
4. 选中行用低视觉比重样式(淡色底 + 弱化文字),选择状态存 rowKey,跨页不丢失
|
4. 列宽用内核字段 width:数字(像素)固定该列,'flex' 或不设则按内容自适应
|
||||||
5. 工具栏输入框即筛选,输入即时过滤并回第 1 页;无匹配时显示空状态行
|
5. selectable: true 开启行选择:表头全选框带 indeterminate 半选态,工具栏显示已选计数
|
||||||
6. 表头粘性固定(滚动容器 max-height 420px),斑马纹 + 悬停高亮由 CSS 提供
|
6. 选中行用低视觉比重样式(淡色底 + 弱化文字),选择状态存 rowKey,跨页不丢失
|
||||||
|
7. 工具栏输入框即筛选,输入即时过滤并回第 1 页;无匹配时显示空状态行
|
||||||
|
8. 表头粘性固定(滚动容器 max-height 420px),斑马纹 + 悬停高亮由 CSS 提供
|
||||||
|
9. 数据在途时传 loading(布尔或信号):表格体换成骨架行、筛选与翻页禁用,返回后置回 false 即恢复
|
||||||
|
10. 需要独立分页器联动时传 page 信号:const page = signal(1),dataTable({ page }) 与 pagination({ page }) 共用
|
||||||
|
11. 需要批量操作时传 selected 信号:内部勾选实时写回,外部置空或赋值即同步整表(跨页保留,按 rowKey 存)
|
||||||
|
|
||||||
现场演示:apps/docs/demo.html#sec-table
|
现场演示:apps/docs/demo.html#sec-table
|
||||||
|
|
||||||
API 速览:
|
API 速览:
|
||||||
|
|
||||||
- button(props):按钮,variant = primary | ghost | danger
|
- button(props):按钮,variant = primary | ghost | danger,disabled 支持布尔或信号禁用
|
||||||
- textField(props):文本输入,value 信号双向绑定
|
- textField(props):文本输入,value 信号双向绑定,disabled 支持布尔或信号,onInput 输入回调;prefix/suffix 可选渲染为输入框内侧附加块
|
||||||
- textarea(props):多行输入,value 信号双向绑定
|
- inputNumber(props):数字输入,−/+ 按钮与键盘 ↑↓ 调节,min/max/step 夹取;prefix/suffix 可选渲染为整组前后附加块
|
||||||
- checkbox(props):复选框,checked 信号双向绑定
|
- textarea(props):多行输入,value 信号双向绑定,disabled 支持布尔或信号,rows 控行数;prefix/suffix 可选渲染为上下附加块
|
||||||
- switchButton(props):开关,checked 信号双向绑定
|
- slider(props):滑块,value 信号双向绑定,min/max/step 可配,原生 range 承载键盘交互,右侧可显示当前值
|
||||||
- select(props):自绘下拉选择(非原生 select),value 信号双向绑定,点外部 / Esc 关闭,选项弹层带微动画
|
- checkbox(props):复选框,checked 信号双向绑定,disabled 支持布尔或信号禁用
|
||||||
- radioGroup(props):单选组,options + value 信号双向绑定
|
- switchButton(props):开关,checked 信号双向绑定,disabled 支持布尔或信号禁用
|
||||||
|
- select(props):自绘下拉选择(非原生 select),value 信号双向绑定,点外部 / Esc 关闭,选项弹层带微动画,disabled 支持布尔或信号禁用
|
||||||
|
- menu(props):下拉菜单,触发器 + 自绘弹层,条目支持禁用 / 危险 / 分隔线,↑↓ 环形导航,点外部 / Esc 关闭
|
||||||
|
- radioGroup(props):单选组,options + value 信号双向绑定,disabled 整组禁用
|
||||||
|
- accordion(props):折叠面板,标题按钮 + grid 行高过渡,单开(手风琴)或多开,支持禁用面板与 active 信号受控
|
||||||
|
- breadcrumb(props):面包屑,nav + ol 语义结构,末项为当前页(aria-current),有 href 走原生链接、无 href 交给 onNavigate
|
||||||
- card(props, ...children):卡片容器
|
- card(props, ...children):卡片容器
|
||||||
- badge(props):徽标,variant = default | success | warning | danger
|
- badge(props):徽标,variant = default | success | warning | danger
|
||||||
- alert(props):提示条,variant = info | success | warning | danger
|
- tag(props):可关闭标签,variant 五档语义色,closable + onClose 支持从数据源移除(纯展示请用 badge)
|
||||||
|
- avatar(props):头像,有 src 渲染图片(alt 用名称)、无 src 用首字占位,size 可调 + 四档语义底色
|
||||||
|
- alert(props):提示条,variant 四档语义色 + 可选 title 与 closable 关闭按钮,role=alert 读屏可播报
|
||||||
|
- toast.info/success/warning/error(message, duration?):消息提示工厂,右上角堆叠,默认 3s 自动消失,出入场微动画;showToast(props) 为完整入参版本(closable 显示关闭按钮,同屏最多 4 条)
|
||||||
|
- tooltip(props, trigger):悬浮提示,包裹触发器,hover / focus 延迟 150ms 出场,移出 / 失焦 / Esc 收起
|
||||||
|
- popover(props, trigger):可交互气泡卡片,包裹触发器,默认点击开合 + 点外部 / Esc 收起,content 可放按钮等交互元素
|
||||||
|
- skeleton(props):骨架屏占位,variant = text | rect | circle,text 支持 rows 多行,扫光微动画
|
||||||
|
- pagination(props):独立分页器,page 信号双向绑定,页码过多折叠省略号,首末页禁用翻页,键盘可达
|
||||||
- spinner(props):加载指示器,size 控制直径
|
- spinner(props):加载指示器,size 控制直径
|
||||||
- progress(props):进度条,轨道 + 渐变流动填充,value 信号驱动 0-100,满格切 success 色
|
- progress(props):进度条,value 信号驱动 0-100(满格切 success 色);不传 value 退化为不确定态无限往复
|
||||||
- tabs(props):标签页,active 信号双向绑定,滑动指示条 + 方向键环形导航,支持禁用页签
|
- tabs(props):标签页,active 信号双向绑定,滑动指示条 + 方向键环形导航,支持禁用页签
|
||||||
- modal(props, ...children):弹窗,open 信号驱动显隐,Esc / 遮罩 / × 关闭
|
- stepper(props):步骤条,done/current/upcoming 三态,active 信号双向绑定,clickable 时已完成步骤可回跳
|
||||||
|
- rate(props):评分星标,value 信号双向绑定,hover 预览移出还原,键盘方向键调节,max 可配
|
||||||
|
- empty(props):空状态占位,title/description 可选,action 放操作区
|
||||||
|
- divider(props):分隔线,水平/垂直两种方向,可带居中文案标签,dashed 切虚线
|
||||||
|
- modal(props, ...children):弹窗,open 信号驱动显隐,Esc / 遮罩 / × 关闭,size 三档宽度,footer 提供底部操作区
|
||||||
|
- drawer(props, ...children):抽屉,open 信号驱动侧滑面板,placement 左右可配、width 可调、footer 操作区,遮罩 / × / Esc 关闭
|
||||||
- form(props, items):表单容器,布局 + 聚合校验,全部通过才触发 onSubmit
|
- form(props, items):表单容器,布局 + 聚合校验,全部通过才触发 onSubmit
|
||||||
- formItem(props, control):表单字段,label + 必填红星 + 规则校验 + 错误区,返回 { el, validate } 句柄
|
- formItem(props, control):表单字段,label + 必填红星 + 规则校验 + 错误区,返回 { el, validate } 句柄
|
||||||
- dataTable(props):数据表格,粘性表头 / 行选择(全选三态)/ 排序 / 筛选 / 分页 / 空状态,内核为 table-core
|
- dataTable(props):数据表格,粘性表头 / 行选择(全选三态)/ 排序 / 筛选 / 分页 / 空状态;loading 为真时数据行换骨架行且筛选/翻页禁用,内核为 table-core
|
||||||
|
- image(props):图片,缩略图点击 / 回车开遮罩大图预览(Esc 或点击遮罩关闭),width 控制缩略宽
|
||||||
|
|
||||||
一行用法:
|
一行用法:
|
||||||
|
|
||||||
@@ -437,6 +947,7 @@ const checked = signal(false); checkbox({ label: '同意', checked }); const ope
|
|||||||
- 皮肤不持有业务状态,一律委托 headless 内核或调用方信号
|
- 皮肤不持有业务状态,一律委托 headless 内核或调用方信号
|
||||||
- 表单类组件统一约定:*Props.value/checked/active 为 Signal 双向绑定,onChange/onInput 为回调
|
- 表单类组件统一约定:*Props.value/checked/active 为 Signal 双向绑定,onChange/onInput 为回调
|
||||||
- 语义色 variant 四档:default|info / success / warning / danger,一律来自令牌,禁止硬编码颜色
|
- 语义色 variant 四档:default|info / success / warning / danger,一律来自令牌,禁止硬编码颜色
|
||||||
|
- 无障碍基线:装饰性占位(skeleton)aria-hidden、加载指示(spinner)role=status、关闭按钮必须 aria-label、弹层弹窗 aria-haspopup/expanded、读屏可见的状态一律同步 aria-*
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -509,6 +1020,35 @@ import '@ui-arch/tokens/tokens.css'; then use var(--ua-color-accent) etc. in any
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## @ui-arch/cli(tooling)v0.1.0
|
||||||
|
|
||||||
|
工程层工具包(private,不发布)。所有 Agent 与人都通过它读取组件库:清单驱动索引、文档、MCP 三件套,外加 style / verify 两道门禁。
|
||||||
|
|
||||||
|
API 速览:
|
||||||
|
|
||||||
|
- ua index:扫描 packages/ 两层目录的 ua.manifest.json,生成 .ua/index.json 与 llms.txt
|
||||||
|
- ua list:列出注册表内所有包与组件数
|
||||||
|
- ua info <name>:查看单个包的清单详情(api / 约定 / 文档步骤)
|
||||||
|
- ua docs:生成人类文档站 apps/docs/index.html + demo.html 与 Agent 全量文档 llms-full.txt
|
||||||
|
- ua style:注释规范门禁(导出必须 /*** ***/、接口字段行尾注释、行注释中文),违规退出码 1
|
||||||
|
- ua verify:清单一致性门禁(id 唯一 / exports 对应 / props 必带 n t d / steps≥3 / demo 锚点存在)
|
||||||
|
- ua commit-lint <文件|message>:提交信息门禁(type 枚举 / scope 规则 / subject 中文祈使 ≤50 字符),规范见 COMMITTING.md,违规退出码 1
|
||||||
|
- ua mcp:零依赖 stdio MCP server,暴露 list_components / get_component / search_components
|
||||||
|
|
||||||
|
一行用法:
|
||||||
|
|
||||||
|
```
|
||||||
|
node packages/tools/cli/bin/ua.js <index|list|info <name>|docs|style|verify|commit-lint|mcp>;仓库内等价于 npm run index / docs / style / verify
|
||||||
|
```
|
||||||
|
|
||||||
|
约定:
|
||||||
|
|
||||||
|
- 零依赖:只用 node 标准库,Node >= 18
|
||||||
|
- 清单是单一来源:代码注释、文档站、llms-full.txt、MCP 全部从 ua.manifest.json 派生
|
||||||
|
- 门禁只增不改:新增校验必须能被人手复现,报错要指出文件与字段
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 设计令牌(--ua-*)
|
## 设计令牌(--ua-*)
|
||||||
|
|
||||||
- --ua-color-bg: #ffffff(暗色 #101216)
|
- --ua-color-bg: #ffffff(暗色 #101216)
|
||||||
|
|||||||
@@ -45,26 +45,63 @@
|
|||||||
## @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 / checkbox / switch / select / radioGroup / card / badge / alert / spinner / progress / tabs / modal / 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
|
- button(props):按钮,variant = primary | ghost | danger,disabled 支持布尔或信号禁用
|
||||||
- textField(props):文本输入,value 信号双向绑定
|
- textField(props):文本输入,value 信号双向绑定,disabled 支持布尔或信号,onInput 输入回调;prefix/suffix 可选渲染为输入框内侧附加块
|
||||||
- textarea(props):多行输入,value 信号双向绑定
|
- inputNumber(props):数字输入,−/+ 按钮与键盘 ↑↓ 调节,min/max/step 夹取;prefix/suffix 可选渲染为整组前后附加块
|
||||||
- checkbox(props):复选框,checked 信号双向绑定
|
- textarea(props):多行输入,value 信号双向绑定,disabled 支持布尔或信号,rows 控行数;prefix/suffix 可选渲染为上下附加块
|
||||||
- switchButton(props):开关,checked 信号双向绑定
|
- slider(props):滑块,value 信号双向绑定,min/max/step 可配,原生 range 承载键盘交互,右侧可显示当前值
|
||||||
- select(props):自绘下拉选择(非原生 select),value 信号双向绑定,点外部 / Esc 关闭,选项弹层带微动画
|
- checkbox(props):复选框,checked 信号双向绑定,disabled 支持布尔或信号禁用
|
||||||
- radioGroup(props):单选组,options + value 信号双向绑定
|
- switchButton(props):开关,checked 信号双向绑定,disabled 支持布尔或信号禁用
|
||||||
|
- select(props):自绘下拉选择(非原生 select),value 信号双向绑定,点外部 / Esc 关闭,选项弹层带微动画,disabled 支持布尔或信号禁用
|
||||||
|
- menu(props):下拉菜单,触发器 + 自绘弹层,条目支持禁用 / 危险 / 分隔线,↑↓ 环形导航,点外部 / Esc 关闭
|
||||||
|
- radioGroup(props):单选组,options + value 信号双向绑定,disabled 整组禁用
|
||||||
|
- accordion(props):折叠面板,标题按钮 + grid 行高过渡,单开(手风琴)或多开,支持禁用面板与 active 信号受控
|
||||||
|
- breadcrumb(props):面包屑,nav + ol 语义结构,末项为当前页(aria-current),有 href 走原生链接、无 href 交给 onNavigate
|
||||||
- card(props, ...children):卡片容器
|
- card(props, ...children):卡片容器
|
||||||
- badge(props):徽标,variant = default | success | warning | danger
|
- badge(props):徽标,variant = default | success | warning | danger
|
||||||
- alert(props):提示条,variant = info | success | warning | danger
|
- tag(props):可关闭标签,variant 五档语义色,closable + onClose 支持从数据源移除(纯展示请用 badge)
|
||||||
|
- avatar(props):头像,有 src 渲染图片(alt 用名称)、无 src 用首字占位,size 可调 + 四档语义底色
|
||||||
|
- alert(props):提示条,variant 四档语义色 + 可选 title 与 closable 关闭按钮,role=alert 读屏可播报
|
||||||
|
- toast.info/success/warning/error(message, duration?):消息提示工厂,右上角堆叠,默认 3s 自动消失,出入场微动画;showToast(props) 为完整入参版本(closable 显示关闭按钮,同屏最多 4 条)
|
||||||
|
- tooltip(props, trigger):悬浮提示,包裹触发器,hover / focus 延迟 150ms 出场,移出 / 失焦 / Esc 收起
|
||||||
|
- popover(props, trigger):可交互气泡卡片,包裹触发器,默认点击开合 + 点外部 / Esc 收起,content 可放按钮等交互元素
|
||||||
|
- skeleton(props):骨架屏占位,variant = text | rect | circle,text 支持 rows 多行,扫光微动画
|
||||||
|
- pagination(props):独立分页器,page 信号双向绑定,页码过多折叠省略号,首末页禁用翻页,键盘可达
|
||||||
- spinner(props):加载指示器,size 控制直径
|
- spinner(props):加载指示器,size 控制直径
|
||||||
- progress(props):进度条,轨道 + 渐变流动填充,value 信号驱动 0-100,满格切 success 色
|
- progress(props):进度条,value 信号驱动 0-100(满格切 success 色);不传 value 退化为不确定态无限往复
|
||||||
- tabs(props):标签页,active 信号双向绑定,滑动指示条 + 方向键环形导航,支持禁用页签
|
- tabs(props):标签页,active 信号双向绑定,滑动指示条 + 方向键环形导航,支持禁用页签
|
||||||
- modal(props, ...children):弹窗,open 信号驱动显隐,Esc / 遮罩 / × 关闭
|
- stepper(props):步骤条,done/current/upcoming 三态,active 信号双向绑定,clickable 时已完成步骤可回跳
|
||||||
|
- rate(props):评分星标,value 信号双向绑定,hover 预览移出还原,键盘方向键调节,max 可配
|
||||||
|
- empty(props):空状态占位,title/description 可选,action 放操作区
|
||||||
|
- divider(props):分隔线,水平/垂直两种方向,可带居中文案标签,dashed 切虚线
|
||||||
|
- modal(props, ...children):弹窗,open 信号驱动显隐,Esc / 遮罩 / × 关闭,size 三档宽度,footer 提供底部操作区
|
||||||
|
- drawer(props, ...children):抽屉,open 信号驱动侧滑面板,placement 左右可配、width 可调、footer 操作区,遮罩 / × / Esc 关闭
|
||||||
- form(props, items):表单容器,布局 + 聚合校验,全部通过才触发 onSubmit
|
- form(props, items):表单容器,布局 + 聚合校验,全部通过才触发 onSubmit
|
||||||
- formItem(props, control):表单字段,label + 必填红星 + 规则校验 + 错误区,返回 { el, validate } 句柄
|
- formItem(props, control):表单字段,label + 必填红星 + 规则校验 + 错误区,返回 { el, validate } 句柄
|
||||||
- dataTable(props):数据表格,粘性表头 / 行选择(全选三态)/ 排序 / 筛选 / 分页 / 空状态,内核为 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 为回调
|
||||||
- 约定: 语义色 variant 四档:default|info / success / warning / danger,一律来自令牌,禁止硬编码颜色
|
- 约定: 语义色 variant 四档:default|info / success / warning / danger,一律来自令牌,禁止硬编码颜色
|
||||||
|
- 约定: 无障碍基线:装饰性占位(skeleton)aria-hidden、加载指示(spinner)role=status、关闭按钮必须 aria-label、弹层弹窗 aria-haspopup/expanded、读屏可见的状态一律同步 aria-*
|
||||||
|
|
||||||
|
## @ui-arch/cli (v0.1.0)
|
||||||
|
|
||||||
|
- 类型: tooling · 目录: packages/tools/cli
|
||||||
|
- 描述: ua 命令行与 MCP 注册表服务:把各包 ua.manifest.json 汇总成机器可读索引(.ua/index.json)、Agent 全量文档(llms-full.txt)与人类文档站,并提供清单一致性门禁。
|
||||||
|
- API:
|
||||||
|
- ua index:扫描 packages/ 两层目录的 ua.manifest.json,生成 .ua/index.json 与 llms.txt
|
||||||
|
- ua list:列出注册表内所有包与组件数
|
||||||
|
- ua info <name>:查看单个包的清单详情(api / 约定 / 文档步骤)
|
||||||
|
- ua docs:生成人类文档站 apps/docs/index.html + demo.html 与 Agent 全量文档 llms-full.txt
|
||||||
|
- ua style:注释规范门禁(导出必须 /*** ***/、接口字段行尾注释、行注释中文),违规退出码 1
|
||||||
|
- ua verify:清单一致性门禁(id 唯一 / exports 对应 / props 必带 n t d / steps≥3 / demo 锚点存在)
|
||||||
|
- ua commit-lint <文件|message>:提交信息门禁(type 枚举 / scope 规则 / subject 中文祈使 ≤50 字符),规范见 COMMITTING.md,违规退出码 1
|
||||||
|
- ua mcp:零依赖 stdio MCP server,暴露 list_components / get_component / search_components
|
||||||
|
- 用法: `node packages/tools/cli/bin/ua.js <index|list|info <name>|docs|style|verify|commit-lint|mcp>;仓库内等价于 npm run index / docs / style / verify`
|
||||||
|
- 约定: 零依赖:只用 node 标准库,Node >= 18
|
||||||
|
- 约定: 清单是单一来源:代码注释、文档站、llms-full.txt、MCP 全部从 ua.manifest.json 派生
|
||||||
|
- 约定: 门禁只增不改:新增校验必须能被人手复现,报错要指出文件与字段
|
||||||
|
|||||||
+4
-2
@@ -12,8 +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",
|
"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",
|
||||||
|
"lint-iterations": "node packages/tools/cli/bin/ua.js lint-iterations"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5.6.0"
|
"typescript": "^5.6.0"
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { h, signal } from '@ui-arch/frame';
|
||||||
|
import type { AccordionItem, AccordionProps } from './types.js';
|
||||||
|
|
||||||
|
/*** 折叠面板:标题按钮 + grid 行高过渡;单开(手风琴)或多开,展开态由 active 信号驱动或内部持有 ***/
|
||||||
|
export function accordion(props: AccordionProps): HTMLElement {
|
||||||
|
// —— 状态集中声明 ——
|
||||||
|
const internal = signal<string[]>([]); // 内部展开集合(未传 active 时使用)
|
||||||
|
const opened = (v: string): boolean => (props.active ? props.active() === v : internal().includes(v));
|
||||||
|
|
||||||
|
const toggle = (item: AccordionItem): void => {
|
||||||
|
if (props.active) {
|
||||||
|
const next = props.active() === item.value ? '' : item.value; // 单开:再点收起
|
||||||
|
props.active(next);
|
||||||
|
props.onChange?.(next);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const cur = internal();
|
||||||
|
const next = cur.includes(item.value)
|
||||||
|
? cur.filter((v) => v !== item.value)
|
||||||
|
: props.multiple
|
||||||
|
? [...cur, item.value]
|
||||||
|
: [item.value];
|
||||||
|
internal(next);
|
||||||
|
props.onChange?.(next[0] ?? '');
|
||||||
|
};
|
||||||
|
|
||||||
|
// —— 面板:标题为原生 button(键盘可达),内容包一层 overflow 容器供过渡裁剪 ——
|
||||||
|
const rows = props.items.map((item, i) =>
|
||||||
|
h(
|
||||||
|
'div',
|
||||||
|
{ class: () => `ua-acc-item${opened(item.value) ? ' ua-open' : ''}` },
|
||||||
|
h(
|
||||||
|
'button',
|
||||||
|
{
|
||||||
|
type: 'button',
|
||||||
|
class: 'ua-acc-head',
|
||||||
|
id: `ua-acc-h-${i}`,
|
||||||
|
'aria-expanded': () => String(opened(item.value)),
|
||||||
|
'aria-controls': `ua-acc-b-${i}`,
|
||||||
|
disabled: item.disabled ?? false, // 布尔透传即得禁用态
|
||||||
|
onClick: () => toggle(item),
|
||||||
|
},
|
||||||
|
h('span', { class: 'ua-acc-title' }, item.title),
|
||||||
|
h('span', { class: 'ua-acc-caret' }, '▸'),
|
||||||
|
),
|
||||||
|
h(
|
||||||
|
'div',
|
||||||
|
{ class: 'ua-acc-body', id: `ua-acc-b-${i}`, role: 'region', 'aria-labelledby': `ua-acc-h-${i}` },
|
||||||
|
h('div', { class: 'ua-acc-inner' }, item.content),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return h('div', { class: 'ua-accordion' }, ...rows);
|
||||||
|
}
|
||||||
@@ -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,7 +1,32 @@
|
|||||||
import { h } from '@ui-arch/frame';
|
import { h } from '@ui-arch/frame';
|
||||||
import type { AlertProps } from './types.js';
|
import type { AlertProps } from './types.js';
|
||||||
|
|
||||||
/*** 提示条:variant 决定语义色,纯展示不持有状态 ***/
|
/*** 提示条:variant 决定语义色,可选标题与关闭按钮;关闭即退场并从 DOM 移除(内容由调用方持有) ***/
|
||||||
export function alert(props: AlertProps): HTMLElement {
|
export function alert(props: AlertProps): HTMLElement {
|
||||||
return h('div', { class: `ua-alert ua-alert-${props.variant ?? 'info'}` }, props.message);
|
const body = h(
|
||||||
|
'div',
|
||||||
|
{ class: 'ua-alert-body' },
|
||||||
|
props.title ? h('div', { class: 'ua-alert-title' }, props.title) : null,
|
||||||
|
h('div', { class: 'ua-alert-message' }, props.message),
|
||||||
|
);
|
||||||
|
const el = h('div', { class: `ua-alert ua-alert-${props.variant ?? 'info'}`, role: 'alert' }, body);
|
||||||
|
|
||||||
|
if (!props.closable) return el; // 纯展示形态
|
||||||
|
el.append(
|
||||||
|
h(
|
||||||
|
'button',
|
||||||
|
{
|
||||||
|
type: 'button',
|
||||||
|
class: 'ua-alert-close',
|
||||||
|
'aria-label': '关闭提示',
|
||||||
|
onClick: () => {
|
||||||
|
el.classList.add('ua-alert-out'); // 先退场
|
||||||
|
window.setTimeout(() => el.isConnected && el.remove(), 200); // 过渡结束再摘除
|
||||||
|
props.onClose?.();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'×',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return el;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { h } from '@ui-arch/frame';
|
||||||
|
import type { AvatarProps } from './types.js';
|
||||||
|
|
||||||
|
/*** 头像:有 src 渲染图片(alt 用名称),否则用名称首字做占位;尺寸与语义底色由 props 决定 ***/
|
||||||
|
export function avatar(props: AvatarProps): HTMLElement {
|
||||||
|
const size = `${props.size ?? 32}px`;
|
||||||
|
return h(
|
||||||
|
'span',
|
||||||
|
{ class: `ua-avatar ua-avatar-${props.variant ?? 'default'}`, style: `width:${size};height:${size}`, title: props.name },
|
||||||
|
props.src
|
||||||
|
? h('img', { class: 'ua-avatar-img', src: props.src, alt: props.name })
|
||||||
|
: h('span', { class: 'ua-avatar-text', 'aria-label': props.name }, firstChar(props.name)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*** 取首个可见字符(中文取首字、英文取首字母大写) ***/
|
||||||
|
function firstChar(name: string): string {
|
||||||
|
const c = name.trim().charAt(0);
|
||||||
|
return c ? c.toUpperCase() : '?'; // 空名兜底问号,避免空白圆
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { h } from '@ui-arch/frame';
|
||||||
|
import type { BreadcrumbProps } from './types.js';
|
||||||
|
|
||||||
|
/*** 面包屑:nav + ol 语义结构,末项为当前页(不可点、aria-current);有 href 渲染 <a>,无 href 渲染 button 交 onNavigate ***/
|
||||||
|
export function breadcrumb(props: BreadcrumbProps): HTMLElement {
|
||||||
|
const items = props.items;
|
||||||
|
const sep = props.separator ?? '/';
|
||||||
|
|
||||||
|
const entries = items.map((item, i) => {
|
||||||
|
const last = i === items.length - 1; // 末项即当前页
|
||||||
|
const node = last
|
||||||
|
? h('span', { class: 'ua-crumb-current', 'aria-current': 'page' }, item.label)
|
||||||
|
: item.href
|
||||||
|
? h('a', { class: 'ua-crumb-link', href: item.href }, item.label)
|
||||||
|
: h('button', { type: 'button', class: 'ua-crumb-link', onClick: () => props.onNavigate?.(item, i) }, item.label);
|
||||||
|
return h('li', { class: 'ua-crumb-item' }, node, last ? null : h('span', { class: 'ua-crumb-sep', 'aria-hidden': 'true' }, sep));
|
||||||
|
});
|
||||||
|
|
||||||
|
return h('nav', { class: 'ua-breadcrumb', 'aria-label': '面包屑导航' }, h('ol', { class: 'ua-crumb-list' }, ...entries));
|
||||||
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import { h } from '@ui-arch/frame';
|
import { h } from '@ui-arch/frame';
|
||||||
import type { ButtonProps } from './types.js';
|
import type { ButtonProps } from './types.js';
|
||||||
|
|
||||||
/*** 按钮:variant 控制视觉强度,onClick 为点击回调 ***/
|
/*** 按钮:variant 控制视觉强度,disabled 支持布尔或信号禁用,onClick 为点击回调 ***/
|
||||||
export function button(props: ButtonProps): HTMLElement {
|
export function button(props: ButtonProps): HTMLElement {
|
||||||
return h(
|
return h(
|
||||||
'button',
|
'button',
|
||||||
{ class: `ua-btn ua-btn-${props.variant ?? 'primary'}`, onClick: props.onClick },
|
{ class: `ua-btn ua-btn-${props.variant ?? 'primary'}`, disabled: props.disabled, onClick: props.onClick }, // false/undefined 移除属性,Signal 走响应式
|
||||||
props.label,
|
props.label,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { effect, h } from '@ui-arch/frame';
|
import { effect, h } from '@ui-arch/frame';
|
||||||
import type { CheckboxProps } from './types.js';
|
import type { CheckboxProps } from './types.js';
|
||||||
|
|
||||||
/*** 复选框:checked 信号双向绑定,onChange 为变更回调 ***/
|
/*** 复选框:checked 信号双向绑定,disabled 支持布尔或信号禁用,onChange 为变更回调 ***/
|
||||||
export function checkbox(props: CheckboxProps): HTMLElement {
|
export function checkbox(props: CheckboxProps): HTMLElement {
|
||||||
const el = h('input', { type: 'checkbox', class: 'ua-checkbox' }) as HTMLInputElement;
|
const el = h('input', { type: 'checkbox', class: 'ua-checkbox', disabled: props.disabled }) as HTMLInputElement; // false/undefined 移除属性,Signal 走响应式
|
||||||
if (props.checked) {
|
if (props.checked) {
|
||||||
const checked = props.checked;
|
const checked = props.checked;
|
||||||
el.addEventListener('change', () => checked(el.checked)); // 用户勾选写回信号
|
el.addEventListener('change', () => checked(el.checked)); // 用户勾选写回信号
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
import type { PageInfo, RowKey, TableInstance } from '@ui-arch/table-core';
|
import type { PageInfo, RowKey, TableInstance } from '@ui-arch/table-core';
|
||||||
import { computed, effect, h, renderList, signal } from '@ui-arch/frame';
|
import { computed, effect, h, renderList, signal } from '@ui-arch/frame';
|
||||||
import { button } from './button.js';
|
import { button } from './button.js';
|
||||||
|
import { skeleton } from './skeleton.js';
|
||||||
import { textField } from './textField.js';
|
import { textField } from './textField.js';
|
||||||
import type { DataTableProps } from './types.js';
|
import type { DataTableProps } from './types.js';
|
||||||
|
|
||||||
@@ -43,6 +44,35 @@ export function dataTable<T extends { id: RowKey }>(props: DataTableProps<T>): H
|
|||||||
return api.getPageInfo();
|
return api.getPageInfo();
|
||||||
});
|
});
|
||||||
const selectedCount = computed(() => state().selection.length);
|
const selectedCount = computed(() => state().selection.length);
|
||||||
|
// 加载态读取器:布尔直接读,信号则保持响应性(供 renderList 依赖收集)
|
||||||
|
const busy = (): boolean => (typeof props.loading === 'function' ? props.loading() : props.loading === true);
|
||||||
|
|
||||||
|
// —— 受控页码桥接:外部信号 ↔ 内核分页;外部越界先钳制,signal 等值写回不触发,双向同步收敛安全 ——
|
||||||
|
if (props.page) {
|
||||||
|
const pageSig = props.page;
|
||||||
|
effect(() => {
|
||||||
|
const external = Math.min(api.getPageInfo().totalPages, Math.max(1, pageSig())); // 越界钳制,防无效派发
|
||||||
|
if (external !== info().page) table.dispatch({ type: 'setPage', page: external }); // 外部翻页 → 内核
|
||||||
|
});
|
||||||
|
effect(() => {
|
||||||
|
const internal = info().page;
|
||||||
|
if (internal !== pageSig()) pageSig(internal); // 内部翻页(筛选/内置翻页器)→ 写回外部
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// —— 受控选中桥接:外部信号 ↔ 内核选择集;比较内容而非引用,避免来回派发 ——
|
||||||
|
if (props.selected) {
|
||||||
|
const selSig = props.selected;
|
||||||
|
const sameKeys = (a: RowKey[], b: RowKey[]): boolean => a.length === b.length && a.every((k, i) => k === b[i]);
|
||||||
|
effect(() => {
|
||||||
|
const external = selSig();
|
||||||
|
if (!sameKeys(external, state().selection)) table.dispatch({ type: 'setSelection', keys: external }); // 外部改写 → 内核
|
||||||
|
});
|
||||||
|
effect(() => {
|
||||||
|
const internal = state().selection;
|
||||||
|
if (!sameKeys(internal, selSig())) selSig([...internal]); // 内部勾选 → 写回外部(拷贝避免外部改到内核数组)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const filterKey = props.filterKey ?? props.columns[0].key;
|
const filterKey = props.filterKey ?? props.columns[0].key;
|
||||||
const colspan = props.columns.length + (selectable ? 1 : 0);
|
const colspan = props.columns.length + (selectable ? 1 : 0);
|
||||||
@@ -76,15 +106,23 @@ export function dataTable<T extends { id: RowKey }>(props: DataTableProps<T>): H
|
|||||||
h(
|
h(
|
||||||
'th',
|
'th',
|
||||||
{
|
{
|
||||||
class: () => (col.sortable ? 'ua-sortable' : ''),
|
class: `${col.sortable ? 'ua-sortable' : ''}${col.align ? ` ua-align-${col.align}` : ''}`.trim(),
|
||||||
onClick: () => col.sortable && table.dispatch({ type: 'toggleSort', key: col.key }),
|
onClick: () => col.sortable && table.dispatch({ type: 'toggleSort', key: col.key }),
|
||||||
},
|
},
|
||||||
col.header ?? col.key,
|
col.header ?? col.key,
|
||||||
col.sortable ? sortMark(col.key) : null,
|
col.sortable ? sortMark(col.key) : null,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/*** 骨架行:列宽与表头对齐,行数取当前每页条数(上限 6 行防过长) ***/
|
||||||
|
const skeletonRows = (): Node[] => {
|
||||||
|
const count = Math.min(props.pageSize ?? 5, 6);
|
||||||
|
const cell = () => h('td', null, skeleton({ variant: 'text', rows: 1 }));
|
||||||
|
return Array.from({ length: count }, () => h('tr', { class: 'ua-loading-row' }, ...Array.from({ length: colspan }, cell)));
|
||||||
|
};
|
||||||
|
|
||||||
const tbody = h('tbody');
|
const tbody = h('tbody');
|
||||||
renderList(tbody, () => {
|
renderList(tbody, () => {
|
||||||
|
if (busy()) return skeletonRows(); // 加载态优先:不看数据,避免闪出旧行
|
||||||
const sel = state().selection; // 读入依赖:选择变化即整表重绘(行数 = pageSize,代价可控)
|
const sel = state().selection; // 读入依赖:选择变化即整表重绘(行数 = pageSize,代价可控)
|
||||||
const rows = visible().rows;
|
const rows = visible().rows;
|
||||||
if (!rows.length) {
|
if (!rows.length) {
|
||||||
@@ -93,7 +131,11 @@ export function dataTable<T extends { id: RowKey }>(props: DataTableProps<T>): H
|
|||||||
return rows.map((row, i) => {
|
return rows.map((row, i) => {
|
||||||
const key = visible().keys[i];
|
const key = visible().keys[i];
|
||||||
const selected = sel.includes(key);
|
const selected = sel.includes(key);
|
||||||
const cells = props.columns.map((col) => h('td', null, String(table.getCellValue(row, col.key))));
|
const cells = props.columns.map((col) => {
|
||||||
|
const raw = table.getCellValue(row, col.key);
|
||||||
|
const text = col.format ? col.format(raw, row) : String(raw); // format 只产出纯文本,天然免注入
|
||||||
|
return h('td', col.align ? { class: `ua-align-${col.align}` } : null, text);
|
||||||
|
});
|
||||||
if (selectable) {
|
if (selectable) {
|
||||||
const check = h('input', { type: 'checkbox', class: 'ua-checkbox' }) as HTMLInputElement;
|
const check = h('input', { type: 'checkbox', class: 'ua-checkbox' }) as HTMLInputElement;
|
||||||
check.checked = selected;
|
check.checked = selected;
|
||||||
@@ -104,8 +146,18 @@ export function dataTable<T extends { id: RowKey }>(props: DataTableProps<T>): H
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// —— 列宽:数字落成 <col> 固定宽度,'flex' 或未设置交给浏览器自适应 ——
|
||||||
|
const colgroup = h(
|
||||||
|
'colgroup',
|
||||||
|
null,
|
||||||
|
selectable ? h('col', { style: 'width:44px' }) : null,
|
||||||
|
...props.columns.map((col) => (typeof col.width === 'number' ? h('col', { style: `width:${col.width}px` }) : h('col'))),
|
||||||
|
);
|
||||||
|
|
||||||
const filterInput = textField({
|
const filterInput = textField({
|
||||||
placeholder: `筛选 ${filterKey}…`,
|
placeholder: `筛选 ${filterKey}…`,
|
||||||
|
prefix: '🔍',
|
||||||
|
disabled: props.loading, // 加载中禁止筛选,避免与在途结果竞争
|
||||||
onInput: (value) => {
|
onInput: (value) => {
|
||||||
table.dispatch({ type: 'setFilter', key: filterKey, value });
|
table.dispatch({ type: 'setFilter', key: filterKey, value });
|
||||||
table.dispatch({ type: 'setPage', page: 1 }); // 筛选变化回第 1 页
|
table.dispatch({ type: 'setPage', page: 1 }); // 筛选变化回第 1 页
|
||||||
@@ -114,20 +166,20 @@ export function dataTable<T extends { id: RowKey }>(props: DataTableProps<T>): H
|
|||||||
|
|
||||||
return h(
|
return h(
|
||||||
'section',
|
'section',
|
||||||
{ class: 'ua-table' },
|
{ class: 'ua-table', 'aria-busy': () => String(busy()) },
|
||||||
h(
|
h(
|
||||||
'div',
|
'div',
|
||||||
{ class: 'ua-table-toolbar' },
|
{ class: 'ua-table-toolbar' },
|
||||||
filterInput,
|
filterInput,
|
||||||
selectable ? h('span', { class: 'ua-sel-count' }, () => (selectedCount() ? `已选 ${selectedCount()} 行` : '')) : null,
|
selectable ? h('span', { class: 'ua-sel-count' }, () => (selectedCount() ? `已选 ${selectedCount()} 行` : '')) : null,
|
||||||
),
|
),
|
||||||
h('div', { class: 'ua-table-scroll' }, h('table', null, h('thead', null, h('tr', null, selectable ? headCheck : null, ...props.columns.map(th))), tbody)),
|
h('div', { class: 'ua-table-scroll' }, h('table', null, colgroup, h('thead', null, h('tr', null, selectable ? headCheck : null, ...props.columns.map(th))), tbody)),
|
||||||
h(
|
h(
|
||||||
'div',
|
'div',
|
||||||
{ class: 'ua-pager' },
|
{ class: 'ua-pager' },
|
||||||
button({ label: '上一页', variant: 'ghost', onClick: () => table.dispatch({ type: 'setPage', page: info().page - 1 }) }),
|
button({ label: '上一页', variant: 'ghost', disabled: props.loading, onClick: () => table.dispatch({ type: 'setPage', page: info().page - 1 }) }),
|
||||||
h('span', { class: 'ua-page-info' }, () => `第 ${info().page} / ${info().totalPages} 页 · 共 ${visible().total} 行`),
|
h('span', { class: 'ua-page-info' }, () => `第 ${info().page} / ${info().totalPages} 页 · 共 ${visible().total} 行`),
|
||||||
button({ label: '下一页', variant: 'ghost', onClick: () => table.dispatch({ type: 'setPage', page: info().page + 1 }) }),
|
button({ label: '下一页', variant: 'ghost', disabled: props.loading, onClick: () => table.dispatch({ type: 'setPage', page: info().page + 1 }) }),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { effect, h } from '@ui-arch/frame';
|
||||||
|
import type { DrawerProps } from './types.js';
|
||||||
|
|
||||||
|
/*** 抽屉:open 信号驱动侧滑面板,点遮罩 / × / Esc 关闭;Tab 焦点陷阱 + 关闭归还焦点,与 modal 的分工是不打断主视图、可从侧边保留上下文 ***/
|
||||||
|
export function drawer(props: DrawerProps, ...children: unknown[]): HTMLElement {
|
||||||
|
const close = (): void => {
|
||||||
|
if (!props.open()) return; // 卫语句:已关闭不重复触发回调
|
||||||
|
props.open(false);
|
||||||
|
props.onClose?.();
|
||||||
|
};
|
||||||
|
const overlay = h('div', {
|
||||||
|
class: () => `ua-drawer-overlay${props.open() ? ' ua-open' : ''}`,
|
||||||
|
onClick: (e: Event) => {
|
||||||
|
if (e.target === overlay) close(); // 卫语句:仅点遮罩本身关闭
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const panel = h(
|
||||||
|
'aside',
|
||||||
|
{
|
||||||
|
class: `ua-drawer ua-drawer-${props.placement ?? 'right'}`,
|
||||||
|
style: `width:${props.width ?? 320}px`,
|
||||||
|
role: 'dialog',
|
||||||
|
'aria-hidden': () => String(!props.open()),
|
||||||
|
},
|
||||||
|
h('div', { class: 'ua-drawer-head' }, h('h3', { class: 'ua-drawer-title' }, props.title), h('button', { class: 'ua-drawer-close', 'aria-label': '关闭抽屉', onClick: close }, '×')),
|
||||||
|
h('div', { class: 'ua-drawer-body' }, ...children),
|
||||||
|
props.footer ? h('div', { class: 'ua-drawer-foot' }, props.footer) : null,
|
||||||
|
);
|
||||||
|
overlay.append(panel);
|
||||||
|
|
||||||
|
let lastFocus: HTMLElement | null = null; // 打开前焦点宿主,关闭时归还
|
||||||
|
const focusables = (): HTMLElement[] =>
|
||||||
|
Array.from(panel.querySelectorAll<HTMLElement>('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'))
|
||||||
|
.filter((el) => !el.hasAttribute('disabled')); // 面板内可聚焦元素(排除禁用)
|
||||||
|
effect(() => {
|
||||||
|
if (props.open()) {
|
||||||
|
lastFocus = document.activeElement as HTMLElement | null; // 记录打开前焦点
|
||||||
|
(panel.querySelector('button, input, textarea') as HTMLElement | null)?.focus(); // 打开时聚焦首个可交互元素
|
||||||
|
} else if (lastFocus) {
|
||||||
|
lastFocus.focus(); // 关闭时归还焦点
|
||||||
|
lastFocus = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
overlay.addEventListener('keydown', (e: KeyboardEvent) => {
|
||||||
|
if (e.key !== 'Tab' || !props.open()) return; // 仅处理打开中的 Tab
|
||||||
|
const els = focusables();
|
||||||
|
if (els.length === 0) { e.preventDefault(); return; } // 无可聚焦元素:拦截 Tab 防焦点逃逸
|
||||||
|
const first = els[0];
|
||||||
|
const last = els[els.length - 1];
|
||||||
|
if (e.shiftKey && (document.activeElement === first || !panel.contains(document.activeElement))) {
|
||||||
|
e.preventDefault(); last.focus(); // Shift+Tab 到首位时回绕到末尾
|
||||||
|
} else if (!e.shiftKey && (document.activeElement === last || !panel.contains(document.activeElement))) {
|
||||||
|
e.preventDefault(); first.focus(); // Tab 到末位时回绕到开头
|
||||||
|
}
|
||||||
|
});
|
||||||
|
document.addEventListener('keydown', (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Escape' && props.open()) close();
|
||||||
|
});
|
||||||
|
return overlay;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -26,7 +26,13 @@ export function formItem(props: FormItemProps, control: HTMLElement): FormItemHa
|
|||||||
const label = h('label', { class: 'ua-form-label' }, props.label, props.required ? h('i', { class: 'ua-form-req' }, '*') : null);
|
const label = h('label', { class: 'ua-form-label' }, props.label, props.required ? h('i', { class: 'ua-form-req' }, '*') : null);
|
||||||
const el = h('div', { class: 'ua-form-item' }, label, h('div', { class: 'ua-form-control' }, control, error));
|
const el = h('div', { class: 'ua-form-item' }, label, h('div', { class: 'ua-form-control' }, control, error));
|
||||||
|
|
||||||
/*** 执行全部规则:返回首个错误消息,同时同步错误区与控件态 ***/
|
/*** 错误态落点:控件根 + 内部原生控件,保证 select / checkbox 这类包裹式控件也能标红 ***/
|
||||||
|
const marked = (): HTMLElement[] => [
|
||||||
|
control,
|
||||||
|
...Array.from(control.querySelectorAll<HTMLElement>('input, textarea, select, .ua-select-trigger')),
|
||||||
|
];
|
||||||
|
|
||||||
|
/*** 执行全部规则:返回首个错误消息,同时同步错误区、控件态与 aria-invalid ***/
|
||||||
const validate = () => {
|
const validate = () => {
|
||||||
const value = props.value();
|
const value = props.value();
|
||||||
let msg: string | null = null;
|
let msg: string | null = null;
|
||||||
@@ -36,14 +42,18 @@ export function formItem(props: FormItemProps, control: HTMLElement): FormItemHa
|
|||||||
}
|
}
|
||||||
error.textContent = msg ?? '';
|
error.textContent = msg ?? '';
|
||||||
control.classList.toggle('ua-invalid', !!msg);
|
control.classList.toggle('ua-invalid', !!msg);
|
||||||
|
for (const node of marked()) node.setAttribute('aria-invalid', String(!!msg)); // 读屏同步错误态
|
||||||
return msg;
|
return msg;
|
||||||
};
|
};
|
||||||
|
|
||||||
control.addEventListener('input', () => {
|
control.addEventListener('input', () => {
|
||||||
if (mode === 'input' || error.textContent) validate(); // input 模式即时校验;其余模式仅清已报错误
|
if (mode === 'input' || error.textContent) validate(); // input 模式即时校验;其余模式仅清已报错误
|
||||||
});
|
});
|
||||||
control.addEventListener('blur', () => {
|
control.addEventListener('change', () => {
|
||||||
if (mode !== 'submit') validate();
|
if (mode === 'input') validate(); // select / checkbox / radio 变更即校验(原生 input 事件不冒泡)
|
||||||
|
});
|
||||||
|
control.addEventListener('focusout', () => {
|
||||||
|
if (mode !== 'submit') validate(); // 用会冒泡的 focusout 取代 blur,包裹式控件同样生效
|
||||||
});
|
});
|
||||||
return { el, validate };
|
return { el, validate };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,17 +1,35 @@
|
|||||||
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 { checkbox } from './checkbox.js';
|
export { checkbox } from './checkbox.js';
|
||||||
export { switchButton } from './switch.js';
|
export { switchButton } from './switch.js';
|
||||||
export { select } from './select.js';
|
export { select } from './select.js';
|
||||||
|
export { menu } from './menu.js';
|
||||||
export { radioGroup } from './radioGroup.js';
|
export { radioGroup } from './radioGroup.js';
|
||||||
|
export { accordion } from './accordion.js';
|
||||||
|
export { breadcrumb } from './breadcrumb.js';
|
||||||
export { card } from './card.js';
|
export { card } from './card.js';
|
||||||
export { badge } from './badge.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 { alert } from './alert.js';
|
||||||
|
export { toast, showToast } from './toast.js';
|
||||||
|
export { tooltip } from './tooltip.js';
|
||||||
|
export { popover } from './popover.js';
|
||||||
|
export { skeleton } from './skeleton.js';
|
||||||
|
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 { form, formItem } from './form.js';
|
export { form, formItem } from './form.js';
|
||||||
export { dataTable } from './dataTable.js';
|
export { dataTable } from './dataTable.js';
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import { effect, h, signal } from '@ui-arch/frame';
|
||||||
|
import type { MenuItem, MenuProps } from './types.js';
|
||||||
|
|
||||||
|
/*** 下拉菜单:触发器 + 自绘弹层,项支持禁用 / 危险 / 分隔线;点项 / 点外部 / Esc 关闭,↑↓ 环形导航 + Home/End 跳转 ***/
|
||||||
|
export function menu(props: MenuProps): HTMLElement {
|
||||||
|
// —— 状态集中声明 ——
|
||||||
|
const open = signal(false); // 弹层开合
|
||||||
|
const focusables: HTMLButtonElement[] = []; // 可聚焦项(跳过分隔线与禁用项),供键盘导航
|
||||||
|
|
||||||
|
const pick = (item: MenuItem): void => {
|
||||||
|
open(false); // 选中即收起
|
||||||
|
props.onSelect?.(item.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
// —— 菜单项:分隔线走独立分支,其余为原生 button 保证键盘可达 ——
|
||||||
|
const rows = props.items.map((item) => {
|
||||||
|
if (item.divider) return h('div', { class: 'ua-menu-divider', role: 'separator' });
|
||||||
|
const btn = h(
|
||||||
|
'button',
|
||||||
|
{
|
||||||
|
type: 'button',
|
||||||
|
class: `ua-menu-item${item.danger ? ' is-danger' : ''}`,
|
||||||
|
role: 'menuitem',
|
||||||
|
disabled: item.disabled ? true : undefined,
|
||||||
|
onClick: () => pick(item),
|
||||||
|
},
|
||||||
|
h('span', { class: 'ua-menu-label' }, item.label),
|
||||||
|
item.shortcut ? h('kbd', { class: 'ua-menu-key' }, item.shortcut) : null, // 快捷键只做提示,不拦截按键
|
||||||
|
) as HTMLButtonElement;
|
||||||
|
if (!item.disabled) focusables.push(btn); // 禁用项不参与导航
|
||||||
|
return btn;
|
||||||
|
});
|
||||||
|
|
||||||
|
const trigger = h(
|
||||||
|
'button',
|
||||||
|
{
|
||||||
|
type: 'button',
|
||||||
|
class: `ua-btn ua-btn-${props.variant ?? 'ghost'} ua-menu-trigger`,
|
||||||
|
'aria-haspopup': 'menu',
|
||||||
|
'aria-expanded': () => String(open()),
|
||||||
|
onClick: () => open(!open()),
|
||||||
|
},
|
||||||
|
props.label,
|
||||||
|
h('span', { class: 'ua-menu-caret' }, '▾'),
|
||||||
|
);
|
||||||
|
|
||||||
|
const pop = h(
|
||||||
|
'div',
|
||||||
|
{
|
||||||
|
class: () => `ua-menu-pop${open() ? ' ua-open' : ''}${props.align === 'end' ? ' is-end' : ''}`,
|
||||||
|
role: 'menu',
|
||||||
|
},
|
||||||
|
...rows,
|
||||||
|
);
|
||||||
|
|
||||||
|
const root = h('div', { class: () => `ua-menu${open() ? ' ua-open' : ''}` }, trigger, pop);
|
||||||
|
|
||||||
|
root.addEventListener('keydown', (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
open(false);
|
||||||
|
trigger.focus(); // 收起后焦点回到触发器,键盘流不断
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (e.key === 'Home' && open()) { e.preventDefault(); focusables[0]?.focus(); return; } // Home 跳首项
|
||||||
|
if (e.key === 'End' && open()) { e.preventDefault(); focusables[focusables.length - 1]?.focus(); return; } // End 跳末项
|
||||||
|
if (e.key !== 'ArrowDown' && e.key !== 'ArrowUp') return; // 卫语句:只接管上下键
|
||||||
|
e.preventDefault();
|
||||||
|
if (!open()) {
|
||||||
|
open(true);
|
||||||
|
focusables[0]?.focus(); // 首次按下即展开并进首项
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const at = focusables.indexOf(document.activeElement as HTMLButtonElement);
|
||||||
|
const step = e.key === 'ArrowDown' ? 1 : -1;
|
||||||
|
focusables[(at + step + focusables.length) % focusables.length]?.focus(); // 环形移动
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('click', (e) => {
|
||||||
|
if (root.contains(e.target as Node)) return; // 卫语句:组件内点击不处理
|
||||||
|
open(false); // 点外部收起
|
||||||
|
});
|
||||||
|
|
||||||
|
effect(() => open()); // 建立依赖,保证开合信号生效
|
||||||
|
return root;
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { effect, h } from '@ui-arch/frame';
|
import { effect, h } from '@ui-arch/frame';
|
||||||
import type { ModalProps } from './types.js';
|
import type { ModalProps } from './types.js';
|
||||||
|
|
||||||
/*** 弹窗:open 信号驱动显隐,点遮罩、× 或 Esc 触发 onClose ***/
|
/*** 弹窗:open 信号驱动显隐,点遮罩、× 或 Esc 触发 onClose;Tab 焦点陷阱 + 关闭归还焦点 ***/
|
||||||
export function modal(props: ModalProps, ...children: unknown[]): HTMLElement {
|
export function modal(props: ModalProps, ...children: unknown[]): HTMLElement {
|
||||||
const close = () => {
|
const close = () => {
|
||||||
if (!props.open()) return; // 卫语句:已关闭不重复触发回调
|
if (!props.open()) return; // 卫语句:已关闭不重复触发回调
|
||||||
@@ -16,15 +16,38 @@ export function modal(props: ModalProps, ...children: unknown[]): HTMLElement {
|
|||||||
});
|
});
|
||||||
const box = h(
|
const box = h(
|
||||||
'div',
|
'div',
|
||||||
{ class: 'ua-modal' },
|
{ class: `ua-modal ua-modal-${props.size ?? 'md'}`, role: 'dialog', 'aria-modal': 'true' }, // 对话框语义,供读屏识别
|
||||||
h('div', { class: 'ua-modal-head' }, h('h3', { class: 'ua-modal-title' }, props.title), h('button', { class: 'ua-modal-close', onClick: close }, '×')),
|
h('div', { class: 'ua-modal-head' }, h('h3', { class: 'ua-modal-title' }, props.title), h('button', { type: 'button', class: 'ua-modal-close', 'aria-label': '关闭弹窗', onClick: close }, '×')),
|
||||||
h('div', { class: 'ua-modal-body' }, ...children),
|
h('div', { class: 'ua-modal-body' }, ...children),
|
||||||
|
props.footer ? h('div', { class: 'ua-modal-foot' }, props.footer) : null, // 底部操作区:右对齐,仅传了才渲染
|
||||||
);
|
);
|
||||||
overlay.append(box);
|
overlay.append(box);
|
||||||
|
let lastFocus: HTMLElement | null = null; // 打开前焦点宿主,关闭时归还
|
||||||
|
const focusables = (): HTMLElement[] =>
|
||||||
|
Array.from(box.querySelectorAll<HTMLElement>('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'))
|
||||||
|
.filter((el) => !el.hasAttribute('disabled')); // 弹窗内可聚焦元素(排除禁用)
|
||||||
effect(() => {
|
effect(() => {
|
||||||
const open = props.open();
|
const open = props.open();
|
||||||
overlay.classList.toggle('ua-open', open); // 显隐跟随信号
|
overlay.classList.toggle('ua-open', open); // 显隐跟随信号
|
||||||
if (open) (box.querySelector('button, input, textarea') as HTMLElement | null)?.focus(); // 打开时聚焦首个可交互元素
|
if (open) {
|
||||||
|
lastFocus = document.activeElement as HTMLElement | null; // 记录打开前焦点
|
||||||
|
(box.querySelector('button, input, textarea') as HTMLElement | null)?.focus(); // 打开时聚焦首个可交互元素
|
||||||
|
} else if (lastFocus) {
|
||||||
|
lastFocus.focus(); // 关闭时归还焦点
|
||||||
|
lastFocus = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
overlay.addEventListener('keydown', (e: KeyboardEvent) => {
|
||||||
|
if (e.key !== 'Tab' || !props.open()) return; // 仅处理打开中的 Tab
|
||||||
|
const els = focusables();
|
||||||
|
if (els.length === 0) { e.preventDefault(); return; } // 无可聚焦元素:拦截 Tab 防焦点逃逸
|
||||||
|
const first = els[0];
|
||||||
|
const last = els[els.length - 1];
|
||||||
|
if (e.shiftKey && (document.activeElement === first || !box.contains(document.activeElement))) {
|
||||||
|
e.preventDefault(); last.focus(); // Shift+Tab 到首位时回绕到末尾
|
||||||
|
} else if (!e.shiftKey && (document.activeElement === last || !box.contains(document.activeElement))) {
|
||||||
|
e.preventDefault(); first.focus(); // Tab 到末位时回绕到开头
|
||||||
|
}
|
||||||
});
|
});
|
||||||
document.addEventListener('keydown', (e: KeyboardEvent) => {
|
document.addEventListener('keydown', (e: KeyboardEvent) => {
|
||||||
if (e.key === 'Escape' && props.open()) close(); // Esc 关闭当前打开的弹窗
|
if (e.key === 'Escape' && props.open()) close(); // Esc 关闭当前打开的弹窗
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { h, renderList } from '@ui-arch/frame';
|
||||||
|
import type { PaginationProps } from './types.js';
|
||||||
|
|
||||||
|
/*** 页码序列:≤7 页全展开;否则保留首尾与当前页 ±1 窗口,缺口折叠为省略号(null 占位) ***/
|
||||||
|
function pageSequence(page: number, totalPages: number): (number | null)[] {
|
||||||
|
if (totalPages <= 7) return Array.from({ length: totalPages }, (_, i) => i + 1);
|
||||||
|
const marks = [1, 2, page - 1, page, page + 1, totalPages - 1, totalPages].filter((p) => p >= 1 && p <= totalPages);
|
||||||
|
const seq: (number | null)[] = [];
|
||||||
|
let prev = 0;
|
||||||
|
for (const p of [...new Set(marks)].sort((a, b) => a - b)) {
|
||||||
|
if (p - prev > 1) seq.push(null); // 缺口 → 省略号
|
||||||
|
seq.push(p);
|
||||||
|
prev = p;
|
||||||
|
}
|
||||||
|
return seq;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*** 独立分页器:受控组件,page 为双向绑定信号;页码过多折叠省略号,首末页禁用翻页,原生 button 保证键盘可达 ***/
|
||||||
|
export function pagination(props: PaginationProps): HTMLElement {
|
||||||
|
const totalPages = Math.max(1, Math.ceil(props.total / props.pageSize)); // 总页数
|
||||||
|
const go = (p: number) => {
|
||||||
|
const next = Math.min(totalPages, Math.max(1, p)); // 钳制到有效页码范围
|
||||||
|
props.page(next);
|
||||||
|
props.onChange?.(next);
|
||||||
|
};
|
||||||
|
|
||||||
|
const nav = h('nav', { class: 'ua-pagination', 'aria-label': '分页' });
|
||||||
|
renderList(nav, () => {
|
||||||
|
const page = Math.min(totalPages, Math.max(1, props.page()));
|
||||||
|
const items: HTMLElement[] = [h('button', { class: 'ua-page-btn', disabled: page <= 1, 'aria-label': '上一页', onClick: () => go(page - 1) }, '‹')];
|
||||||
|
for (const it of pageSequence(page, totalPages)) {
|
||||||
|
items.push(
|
||||||
|
it == null
|
||||||
|
? h('span', { class: 'ua-page-gap', 'aria-hidden': 'true' }, '…')
|
||||||
|
: h('button', { class: `ua-page-btn${it === page ? ' ua-active' : ''}`, disabled: it === page, 'aria-current': it === page ? 'page' : null, 'aria-label': `第 ${it} 页`, onClick: () => go(it) }, String(it)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
items.push(h('button', { class: 'ua-page-btn', disabled: page >= totalPages, 'aria-label': '下一页', onClick: () => go(page + 1) }, '›'));
|
||||||
|
return items;
|
||||||
|
});
|
||||||
|
return nav;
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { effect, h, signal } from '@ui-arch/frame';
|
||||||
|
import type { PopoverProps } from './types.js';
|
||||||
|
|
||||||
|
let seq = 0; // 全局自增序号:保证每个弹层 id 唯一,供 aria-controls 关联
|
||||||
|
|
||||||
|
/*** 气泡卡片:包裹触发器,默认点击开合;内容可交互,点外部 / Esc / 再次点触发器收起 ***/
|
||||||
|
export function popover(props: PopoverProps, trigger: HTMLElement): HTMLElement {
|
||||||
|
// —— 状态集中声明 ——
|
||||||
|
const open = signal(false); // 弹层显隐
|
||||||
|
const id = `ua-pop-${++seq}`; // 弹层唯一 id
|
||||||
|
let timer: number | null = null; // hover 模式下的出入场计时器
|
||||||
|
|
||||||
|
const setOpen = (next: boolean): void => {
|
||||||
|
if (open() === next) return; // 幂等:状态未变不派发回调
|
||||||
|
open(next);
|
||||||
|
props.onOpenChange?.(next);
|
||||||
|
};
|
||||||
|
const toggle = (): void => setOpen(!open());
|
||||||
|
|
||||||
|
// —— 触发器与弹层 ——
|
||||||
|
trigger.setAttribute('aria-haspopup', 'dialog');
|
||||||
|
effect(() => trigger.setAttribute('aria-expanded', String(open()))); // 展开态跟随信号写回
|
||||||
|
const panel = h(
|
||||||
|
'div',
|
||||||
|
{
|
||||||
|
id,
|
||||||
|
class: () => `ua-pop ua-pop-${props.placement ?? 'bottom'}${open() ? ' ua-open' : ''}`,
|
||||||
|
role: 'dialog',
|
||||||
|
'aria-hidden': () => String(!open()),
|
||||||
|
onClick: (e: Event) => e.stopPropagation(), // 弹层内点击不触发外部关闭
|
||||||
|
},
|
||||||
|
props.title ? h('div', { class: 'ua-pop-title' }, props.title) : null,
|
||||||
|
h('div', { class: 'ua-pop-body' }, props.content),
|
||||||
|
);
|
||||||
|
const root = h('span', { class: 'ua-pop-wrap' }, trigger, panel);
|
||||||
|
|
||||||
|
if (props.trigger === 'hover') {
|
||||||
|
root.addEventListener('pointerenter', () => {
|
||||||
|
if (timer != null) window.clearTimeout(timer);
|
||||||
|
timer = window.setTimeout(() => setOpen(true), props.delay ?? 150);
|
||||||
|
});
|
||||||
|
root.addEventListener('pointerleave', () => {
|
||||||
|
if (timer != null) window.clearTimeout(timer); // 未出场就离开:取消计时
|
||||||
|
setOpen(false);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
trigger.addEventListener('click', (e: Event) => {
|
||||||
|
e.stopPropagation(); // 阻止冒泡到 document,否则刚打开就被外部点击关掉
|
||||||
|
toggle();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
root.addEventListener('keydown', (e: KeyboardEvent) => {
|
||||||
|
if (e.key !== 'Escape' || !open()) return; // 卫语句:未展开时 Esc 不处理
|
||||||
|
setOpen(false);
|
||||||
|
trigger.focus(); // 收起后焦点回触发器
|
||||||
|
});
|
||||||
|
document.addEventListener('click', () => setOpen(false)); // 点外部收起(弹层内已阻止冒泡)
|
||||||
|
|
||||||
|
return root;
|
||||||
|
}
|
||||||
@@ -1,18 +1,37 @@
|
|||||||
import { effect, h } from '@ui-arch/frame';
|
import { effect, h } from '@ui-arch/frame';
|
||||||
import type { ProgressProps } from './types.js';
|
import type { ProgressProps } from './types.js';
|
||||||
|
|
||||||
/*** 进度条:轨道 + 渐变流动填充,value 信号驱动 0-100;满格切换 success 色并停止条纹 ***/
|
/*** 进度条:value 信号驱动 0-100 定量态,缺省 value 则退化为不确定态(无限往复);满格切 success 色并停条纹 ***/
|
||||||
export function progress(props: ProgressProps): HTMLElement {
|
export function progress(props: ProgressProps = {}): HTMLElement {
|
||||||
|
// —— 状态集中声明 ——
|
||||||
|
const value = props.value; // 可选:不传即不确定态
|
||||||
const fill = h('div', { class: 'ua-progress-fill' });
|
const fill = h('div', { class: 'ua-progress-fill' });
|
||||||
const track = h('div', { class: 'ua-progress-track' }, fill);
|
const track = h('div', { class: 'ua-progress-track' }, fill);
|
||||||
const text = h('span', { class: 'ua-progress-text' });
|
const text = h('span', { class: 'ua-progress-text' });
|
||||||
|
const withText = props.showText !== false && !!value; // 不确定态没有百分比可显示
|
||||||
|
|
||||||
|
if (!value) {
|
||||||
|
fill.classList.add('ua-indeterminate'); // 宽度与往复动画交给 CSS
|
||||||
|
return h('div', { class: 'ua-progress', role: 'progressbar', 'aria-label': '进行中' }, track);
|
||||||
|
}
|
||||||
effect(() => {
|
effect(() => {
|
||||||
const v = clamp(props.value()); // 依赖收集:信号变化自动写回
|
const v = clamp(value()); // 依赖收集:信号变化自动写回
|
||||||
fill.style.width = `${v}%`;
|
fill.style.width = `${v}%`;
|
||||||
fill.classList.toggle('ua-done', v >= 100); // 满格切语义色
|
fill.classList.toggle('ua-done', v >= 100); // 满格切语义色
|
||||||
text.textContent = `${Math.round(v)}%`;
|
text.textContent = `${Math.round(v)}%`;
|
||||||
});
|
});
|
||||||
return h('div', { class: 'ua-progress' }, track, props.showText === false ? null : text);
|
return h(
|
||||||
|
'div',
|
||||||
|
{
|
||||||
|
class: 'ua-progress',
|
||||||
|
role: 'progressbar',
|
||||||
|
'aria-valuemin': '0',
|
||||||
|
'aria-valuemax': '100',
|
||||||
|
'aria-valuenow': () => String(Math.round(clamp(value()))),
|
||||||
|
},
|
||||||
|
track,
|
||||||
|
withText ? text : null,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*** 进度钳制到 0-100 ***/
|
/*** 进度钳制到 0-100 ***/
|
||||||
|
|||||||
@@ -6,7 +6,12 @@ export function radioGroup(props: RadioGroupProps): HTMLElement {
|
|||||||
const groupName = `ua-radio-${Math.random().toString(36).slice(2, 8)}`; // 组内唯一 name
|
const groupName = `ua-radio-${Math.random().toString(36).slice(2, 8)}`; // 组内唯一 name
|
||||||
const radios: HTMLInputElement[] = [];
|
const radios: HTMLInputElement[] = [];
|
||||||
for (const opt of props.options) {
|
for (const opt of props.options) {
|
||||||
const input = h('input', { type: 'radio', name: groupName, class: 'ua-radio' }) as HTMLInputElement;
|
const input = h('input', {
|
||||||
|
type: 'radio',
|
||||||
|
name: groupName,
|
||||||
|
class: 'ua-radio',
|
||||||
|
disabled: props.disabled, // 布尔或信号禁用,h() 直接透传
|
||||||
|
}) as HTMLInputElement;
|
||||||
input.value = opt.value;
|
input.value = opt.value;
|
||||||
radios.push(input);
|
radios.push(input);
|
||||||
input.addEventListener('change', () => {
|
input.addEventListener('change', () => {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { effect, h, signal } from '@ui-arch/frame';
|
import { effect, h, signal } from '@ui-arch/frame';
|
||||||
import type { SelectProps } from './types.js';
|
import type { SelectProps } from './types.js';
|
||||||
|
|
||||||
/*** 下拉选择:自绘弹层替代原生 select,value 信号双向绑定;点选项 / 点外部 / Esc 关闭 ***/
|
/*** 下拉选择:自绘弹层替代原生 select,value 信号双向绑定;支持 ↑↓/Home/End 键盘导航,点选项 / 点外部 / Esc 关闭 ***/
|
||||||
export function select(props: SelectProps): HTMLElement {
|
export function select(props: SelectProps): HTMLElement {
|
||||||
// —— 状态集中声明 ——
|
// —— 状态集中声明 ——
|
||||||
const open = signal(false); // 弹层开合
|
const open = signal(false); // 弹层开合
|
||||||
@@ -24,6 +24,7 @@ export function select(props: SelectProps): HTMLElement {
|
|||||||
class: 'ua-select-trigger',
|
class: 'ua-select-trigger',
|
||||||
'aria-haspopup': 'listbox',
|
'aria-haspopup': 'listbox',
|
||||||
'aria-expanded': () => String(open()),
|
'aria-expanded': () => String(open()),
|
||||||
|
disabled: props.disabled, // 布尔或信号禁用,h() 直接透传(原生 button 拦截点击)
|
||||||
onClick: () => open(!open()),
|
onClick: () => open(!open()),
|
||||||
},
|
},
|
||||||
h('span', { class: () => `ua-select-value${value() ? '' : ' is-empty'}` }, () => current() || props.placeholder || ''),
|
h('span', { class: () => `ua-select-value${value() ? '' : ' is-empty'}` }, () => current() || props.placeholder || ''),
|
||||||
@@ -46,10 +47,21 @@ export function select(props: SelectProps): HTMLElement {
|
|||||||
);
|
);
|
||||||
const pop = h('div', { class: () => `ua-select-pop${open() ? ' ua-open' : ''}`, role: 'listbox' }, ...options);
|
const pop = h('div', { class: () => `ua-select-pop${open() ? ' ua-open' : ''}`, role: 'listbox' }, ...options);
|
||||||
|
|
||||||
const root = h('div', { class: 'ua-select' }, trigger, pop);
|
const root = h('div', { class: () => `ua-select${open() ? ' ua-open' : ''}` }, trigger, pop); // 根带开合类,供箭头旋转
|
||||||
|
|
||||||
root.addEventListener('keydown', (e: KeyboardEvent) => {
|
root.addEventListener('keydown', (e: KeyboardEvent) => {
|
||||||
if (e.key === 'Escape') open(false); // Esc 关闭弹层
|
if (e.key === 'Escape') { open(false); trigger.focus(); return; } // Esc 关闭并回焦触发器
|
||||||
|
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
||||||
|
e.preventDefault(); // 阻止页面滚动
|
||||||
|
if (!open()) { open(true); options[0]?.focus(); return; } // 未展开:展开并聚焦首个选项
|
||||||
|
const idx = options.indexOf(document.activeElement as HTMLElement); // 展开中:在选项间环形移动
|
||||||
|
const dir = e.key === 'ArrowDown' ? 1 : -1;
|
||||||
|
const next = (idx + dir + options.length) % options.length;
|
||||||
|
options[next]?.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (e.key === 'Home' && open()) { e.preventDefault(); options[0]?.focus(); return; } // Home 跳首项
|
||||||
|
if (e.key === 'End' && open()) { e.preventDefault(); options[options.length - 1]?.focus(); } // End 跳末项
|
||||||
});
|
});
|
||||||
document.addEventListener('click', (e) => {
|
document.addEventListener('click', (e) => {
|
||||||
if (root.contains(e.target as Node)) return; // 卫语句:组件内点击不处理
|
if (root.contains(e.target as Node)) return; // 卫语句:组件内点击不处理
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { h } from '@ui-arch/frame';
|
||||||
|
import type { SkeletonProps } from './types.js';
|
||||||
|
|
||||||
|
/*** 骨架屏:variant 决定占位形状(text 多行 / rect 块 / circle 圆),纯展示无状态 ***/
|
||||||
|
export function skeleton(props: SkeletonProps): HTMLElement {
|
||||||
|
const variant = props.variant ?? 'rect'; // 缺省整块矩形
|
||||||
|
if (variant !== 'text') return h('div', { class: `ua-skeleton ua-skeleton-${variant}`, 'aria-hidden': 'true' }); // 纯装饰,读屏跳过
|
||||||
|
const rows = props.rows ?? 3; // text 行数,末行缩短模拟段落收尾
|
||||||
|
const lines: HTMLElement[] = [];
|
||||||
|
for (let i = 0; i < rows; i++) lines.push(h('div', { class: `ua-skeleton ua-skeleton-text${i === rows - 1 ? ' ua-last' : ''}`, 'aria-hidden': 'true' }));
|
||||||
|
return h('div', { class: 'ua-skeleton-group', 'aria-hidden': 'true' }, ...lines); // 整组装饰性占位
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { effect, h } from '@ui-arch/frame';
|
||||||
|
import type { SliderProps } from './types.js';
|
||||||
|
|
||||||
|
/*** 滑块:原生 range 承载交互与键盘,value 信号双向绑定;轨道配色走 accent-color 令牌 ***/
|
||||||
|
export function slider(props: SliderProps): HTMLElement {
|
||||||
|
const value = props.value; // 必需信号:组件不持有数值,一律委托调用方
|
||||||
|
const input = h('input', {
|
||||||
|
type: 'range',
|
||||||
|
class: 'ua-slider-input',
|
||||||
|
min: props.min ?? 0,
|
||||||
|
max: props.max ?? 100,
|
||||||
|
step: props.step ?? 1,
|
||||||
|
disabled: props.disabled, // 布尔或信号禁用,h() 直接透传
|
||||||
|
}) as HTMLInputElement;
|
||||||
|
|
||||||
|
input.addEventListener('input', () => {
|
||||||
|
const next = Number(input.value);
|
||||||
|
value(next); // 用户拖动写回信号
|
||||||
|
props.onChange?.(next);
|
||||||
|
});
|
||||||
|
effect(() => {
|
||||||
|
const next = String(value());
|
||||||
|
if (input.value !== next) input.value = next; // 外部赋值同步滑块位置(等值不重写,避免打断拖动)
|
||||||
|
});
|
||||||
|
|
||||||
|
return h(
|
||||||
|
'div',
|
||||||
|
{ class: 'ua-slider' },
|
||||||
|
input,
|
||||||
|
props.showValue === false ? null : h('span', { class: 'ua-slider-value' }, () => String(value())),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ import type { SpinnerProps } from './types.js';
|
|||||||
/*** 加载指示器:纯 CSS 旋转动画,size 控制直径 ***/
|
/*** 加载指示器:纯 CSS 旋转动画,size 控制直径 ***/
|
||||||
export function spinner(props: SpinnerProps = {}): HTMLElement {
|
export function spinner(props: SpinnerProps = {}): HTMLElement {
|
||||||
const size = `${props.size ?? 18}px`;
|
const size = `${props.size ?? 18}px`;
|
||||||
const el = h('span', { class: 'ua-spinner' });
|
const el = h('span', { class: 'ua-spinner', role: 'status', 'aria-label': '加载中' }); // 读屏播报加载状态
|
||||||
el.style.width = size;
|
el.style.width = size;
|
||||||
el.style.height = size;
|
el.style.height = size;
|
||||||
return el;
|
return el;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import { effect, h } from '@ui-arch/frame';
|
import { effect, h } from '@ui-arch/frame';
|
||||||
import type { SwitchProps } from './types.js';
|
import type { SwitchProps } from './types.js';
|
||||||
|
|
||||||
/*** 开关:checked 信号双向绑定,轨道 + 滑块的纯 CSS 实现 ***/
|
/*** 开关:checked 信号双向绑定,disabled 支持布尔或信号禁用,轨道 + 滑块的纯 CSS 实现 ***/
|
||||||
export function switchButton(props: SwitchProps): HTMLElement {
|
export function switchButton(props: SwitchProps): HTMLElement {
|
||||||
const input = h('input', { type: 'checkbox', class: 'ua-switch-input' }) as HTMLInputElement;
|
const input = h('input', { type: 'checkbox', class: 'ua-switch-input', disabled: props.disabled }) as HTMLInputElement; // false/undefined 移除属性,Signal 走响应式
|
||||||
if (props.checked) {
|
if (props.checked) {
|
||||||
const checked = props.checked;
|
const checked = props.checked;
|
||||||
input.addEventListener('change', () => checked(input.checked)); // 用户切换写回信号
|
input.addEventListener('change', () => checked(input.checked)); // 用户切换写回信号
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export function tabs(props: TabsProps): HTMLElement {
|
|||||||
// —— 状态集中声明 ——
|
// —— 状态集中声明 ——
|
||||||
const buttons: HTMLButtonElement[] = []; // 页签按钮,顺序与 tabs 一致
|
const buttons: HTMLButtonElement[] = []; // 页签按钮,顺序与 tabs 一致
|
||||||
const indicator = h('span', { class: 'ua-tabs-indicator' });
|
const indicator = h('span', { class: 'ua-tabs-indicator' });
|
||||||
const head = h('div', { class: 'ua-tabs-head', role: 'tablist' }, indicator);
|
const head = h('div', { class: 'ua-tabs-head', role: 'tablist', 'aria-label': '标签页' }, indicator);
|
||||||
|
|
||||||
/*** 同步激活态与指示条几何位置 ***/
|
/*** 同步激活态与指示条几何位置 ***/
|
||||||
const sync = () => {
|
const sync = () => {
|
||||||
@@ -27,6 +27,7 @@ export function tabs(props: TabsProps): HTMLElement {
|
|||||||
const btn = h('button', { class: 'ua-tab', role: 'tab', type: 'button' }, tab.label) as HTMLButtonElement;
|
const btn = h('button', { class: 'ua-tab', role: 'tab', type: 'button' }, tab.label) as HTMLButtonElement;
|
||||||
btn.dataset.value = tab.value;
|
btn.dataset.value = tab.value;
|
||||||
btn.disabled = tab.disabled ?? false;
|
btn.disabled = tab.disabled ?? false;
|
||||||
|
if (tab.disabled) btn.setAttribute('aria-disabled', 'true'); // 读屏提示不可选
|
||||||
btn.addEventListener('click', () => {
|
btn.addEventListener('click', () => {
|
||||||
if (btn.disabled) return; // 卫语句:禁用页签不响应
|
if (btn.disabled) return; // 卫语句:禁用页签不响应
|
||||||
if (props.active) props.active(tab.value); // 用户切换写回信号
|
if (props.active) props.active(tab.value); // 用户切换写回信号
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { h } from '@ui-arch/frame';
|
||||||
|
import type { TagProps } from './types.js';
|
||||||
|
|
||||||
|
/*** 标签:语义色小胶囊,可选 × 关闭;与 badge 的分工是 tag 可交互(带关闭回调) ***/
|
||||||
|
export function tag(props: TagProps): HTMLElement {
|
||||||
|
const label = h('span', { class: 'ua-tag-label' }, props.label);
|
||||||
|
if (!props.closable) {
|
||||||
|
return h('span', { class: `ua-tag ua-tag-${props.variant ?? 'default'}` }, label); // 纯展示形态
|
||||||
|
}
|
||||||
|
return h(
|
||||||
|
'span',
|
||||||
|
{ class: `ua-tag is-closable ua-tag-${props.variant ?? 'default'}` },
|
||||||
|
label,
|
||||||
|
h('button', { type: 'button', class: 'ua-tag-close', 'aria-label': `移除 ${props.label}`, onClick: () => props.onClose?.() }, '×'),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
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 信号双向绑定,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 ?? '' }) as HTMLInputElement;
|
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;
|
||||||
el.addEventListener('input', () => signal(el.value)); // 用户输入写回信号
|
el.addEventListener('input', () => signal(el.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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
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 信号双向绑定,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 ?? '' }) as HTMLTextAreaElement;
|
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;
|
||||||
el.addEventListener('input', () => signal(el.value)); // 用户输入写回信号
|
el.addEventListener('input', () => signal(el.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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { h } from '@ui-arch/frame';
|
||||||
|
import type { ToastProps } from './types.js';
|
||||||
|
|
||||||
|
let host: HTMLElement | null = null; // 全局右上角堆叠容器:首条消息出现时懒创建
|
||||||
|
let seq = 0; // 自增序号:为每条 toast 生成唯一 id,辅助无障碍定位
|
||||||
|
const MAX_STACK = 4; // 同屏最多条数:超出即挤掉最早一条,避免刷屏淹页面
|
||||||
|
|
||||||
|
/*** 确保堆叠容器存在:fixed 挂在 body 末尾,后续消息依次向上堆叠 ***/
|
||||||
|
function ensureHost(): HTMLElement {
|
||||||
|
if (host) return host;
|
||||||
|
host = h('div', { class: 'ua-toast-host' });
|
||||||
|
document.body.appendChild(host);
|
||||||
|
return host;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*** 退场并移除:先摘 ua-open 触发过渡,transitionend 与 320ms 超时兜底二选一 ***/
|
||||||
|
function dismiss(item: HTMLElement): void {
|
||||||
|
item.classList.remove('ua-open');
|
||||||
|
item.addEventListener('transitionend', () => item.remove(), { once: true });
|
||||||
|
window.setTimeout(() => item.isConnected && item.remove(), 320); // reduced-motion 下 transition 不触发
|
||||||
|
}
|
||||||
|
|
||||||
|
/*** 弹出一条 toast:语义色由 variant 决定,duration 毫秒后自动淡出并移除节点 ***/
|
||||||
|
export function showToast(props: ToastProps): HTMLElement {
|
||||||
|
// —— 状态集中声明 ——
|
||||||
|
const duration = props.duration ?? 3000; // 停留时长毫秒
|
||||||
|
const item = h(
|
||||||
|
'div',
|
||||||
|
{ class: `ua-toast ua-toast-${props.variant ?? 'info'}${props.closable ? ' is-closable' : ''}`, role: 'status', id: `ua-toast-${++seq}` },
|
||||||
|
h('span', { class: 'ua-toast-msg' }, props.message),
|
||||||
|
props.closable ? h('button', { type: 'button', class: 'ua-toast-close', 'aria-label': '关闭提示', onClick: () => dismiss(item) }, '×') : null,
|
||||||
|
);
|
||||||
|
const stack = ensureHost();
|
||||||
|
if (stack.childElementCount >= MAX_STACK) stack.firstElementChild?.remove(); // 超过上限直接挤掉最早一条
|
||||||
|
|
||||||
|
stack.appendChild(item);
|
||||||
|
requestAnimationFrame(() => item.classList.add('ua-open')); // 下一帧入场,保证过渡生效
|
||||||
|
window.setTimeout(() => dismiss(item), duration);
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*** toast 语义快捷工厂:toast.info / success / warning / error(message, duration?) ***/
|
||||||
|
export const toast = {
|
||||||
|
info: (message: string, duration?: number): HTMLElement => showToast({ message, duration, variant: 'info' }), // 中性信息
|
||||||
|
success: (message: string, duration?: number): HTMLElement => showToast({ message, duration, variant: 'success' }), // 成功反馈
|
||||||
|
warning: (message: string, duration?: number): HTMLElement => showToast({ message, duration, variant: 'warning' }), // 警告
|
||||||
|
error: (message: string, duration?: number): HTMLElement => showToast({ message, duration, variant: 'danger' }), // 错误
|
||||||
|
};
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { h, signal } from '@ui-arch/frame';
|
||||||
|
import type { TooltipProps } from './types.js';
|
||||||
|
|
||||||
|
let seq = 0; // 全局自增序号:保证每个气泡 id 唯一,供 aria-describedby 关联
|
||||||
|
|
||||||
|
/*** 悬浮提示:包裹触发器,hover / focus 延迟出场,移出 / 失焦 / Esc 收起,纯展示不拦截指针 ***/
|
||||||
|
export function tooltip(props: TooltipProps, trigger: HTMLElement): HTMLElement {
|
||||||
|
// —— 状态集中声明 ——
|
||||||
|
const open = signal(false); // 气泡显隐
|
||||||
|
const id = `ua-tip-${++seq}`; // 气泡唯一 id
|
||||||
|
const delay = props.delay ?? 150; // 出场延迟毫秒;<=0 视为立即显示
|
||||||
|
let timer: number | null = null; // 出场延迟计时器句柄
|
||||||
|
|
||||||
|
const show = () => {
|
||||||
|
if (timer != null) window.clearTimeout(timer); // 重复进入先清旧计时
|
||||||
|
if (delay <= 0) return open(true);
|
||||||
|
timer = window.setTimeout(() => {
|
||||||
|
timer = null;
|
||||||
|
open(true);
|
||||||
|
}, delay);
|
||||||
|
};
|
||||||
|
const hide = () => {
|
||||||
|
if (timer != null) { // 还没出场就离开:直接取消计时,避免闪一下
|
||||||
|
window.clearTimeout(timer);
|
||||||
|
timer = null;
|
||||||
|
}
|
||||||
|
open(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// —— 触发器与气泡 ——
|
||||||
|
trigger.setAttribute('aria-describedby', id); // 无障碍:读屏把提示文案关联到触发器
|
||||||
|
const bubble = h(
|
||||||
|
'span',
|
||||||
|
{
|
||||||
|
id,
|
||||||
|
class: () => `ua-tip ua-tip-${props.placement ?? 'top'}${open() ? ' ua-open' : ''}`,
|
||||||
|
role: 'tooltip',
|
||||||
|
'aria-hidden': () => String(!open()),
|
||||||
|
},
|
||||||
|
props.content,
|
||||||
|
);
|
||||||
|
const root = h('span', { class: 'ua-tip-wrap' }, trigger, bubble);
|
||||||
|
|
||||||
|
root.addEventListener('pointerenter', show); // 鼠标进入
|
||||||
|
root.addEventListener('pointerleave', hide); // 鼠标离开
|
||||||
|
root.addEventListener('focusin', show); // 键盘聚焦(focusin 会冒泡)
|
||||||
|
root.addEventListener('focusout', hide); // 失焦
|
||||||
|
root.addEventListener('keydown', (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Escape') hide(); // Esc 立即收起,不必等延迟
|
||||||
|
});
|
||||||
|
return root;
|
||||||
|
}
|
||||||
@@ -1,10 +1,28 @@
|
|||||||
import type { ColumnDef, RowKey } from '@ui-arch/table-core';
|
import type { ColumnDef, RowKey } from '@ui-arch/table-core';
|
||||||
import type { Signal } from '@ui-arch/frame';
|
import type { Signal } from '@ui-arch/frame';
|
||||||
|
|
||||||
|
/*** 悬浮提示入参 ***/
|
||||||
|
export interface TooltipProps {
|
||||||
|
content: string; // 提示文案(纯文本)
|
||||||
|
placement?: 'top' | 'bottom'; // 气泡方位;缺省 top(触发器上方)
|
||||||
|
delay?: number; // 出场延迟毫秒;缺省 150,传 0 立即显示
|
||||||
|
}
|
||||||
|
|
||||||
|
/*** 气泡卡片入参 ***/
|
||||||
|
export interface PopoverProps {
|
||||||
|
content: unknown; // 弹层内容:字符串 / DOM 节点 / 响应式函数(可放交互元素)
|
||||||
|
title?: string; // 弹层标题;缺省不渲染标题行
|
||||||
|
placement?: 'top' | 'bottom'; // 弹层方位;缺省 bottom(触发器下方)
|
||||||
|
trigger?: 'click' | 'hover'; // 触发方式;缺省 click(hover 模式带 delay 延迟)
|
||||||
|
delay?: number; // hover 模式出场延迟毫秒;缺省 150
|
||||||
|
onOpenChange?: (open: boolean) => void; // 开合回调
|
||||||
|
}
|
||||||
|
|
||||||
/*** 按钮入参 ***/
|
/*** 按钮入参 ***/
|
||||||
export interface ButtonProps {
|
export interface ButtonProps {
|
||||||
label: string; // 按钮文案
|
label: string; // 按钮文案
|
||||||
variant?: 'primary' | 'ghost' | 'danger'; // 视觉强度;缺省 primary
|
variant?: 'primary' | 'ghost' | 'danger'; // 视觉强度;缺省 primary
|
||||||
|
disabled?: boolean | Signal<boolean>; // 禁用态:布尔直接置位,传信号则动态同步
|
||||||
onClick?: () => void; // 点击回调
|
onClick?: () => void; // 点击回调
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -12,7 +30,10 @@ export interface ButtonProps {
|
|||||||
export interface TextFieldProps {
|
export interface TextFieldProps {
|
||||||
placeholder?: string; // 占位文案
|
placeholder?: string; // 占位文案
|
||||||
value?: Signal<string>; // 双向绑定的信号
|
value?: Signal<string>; // 双向绑定的信号
|
||||||
|
disabled?: boolean | Signal<boolean>; // 禁用态:布尔直接置位,传信号则动态同步
|
||||||
onInput?: (value: string) => void; // 输入回调
|
onInput?: (value: string) => void; // 输入回调
|
||||||
|
prefix?: string | HTMLElement; // 输入框内侧前置附加块(如图标、货币符);缺省不渲染
|
||||||
|
suffix?: string | HTMLElement; // 输入框内侧后置附加块(如单位);缺省不渲染
|
||||||
}
|
}
|
||||||
|
|
||||||
/*** 卡片入参 ***/
|
/*** 卡片入参 ***/
|
||||||
@@ -20,13 +41,22 @@ export interface CardProps {
|
|||||||
title?: string; // 标题;缺省不渲染标题行
|
title?: string; // 标题;缺省不渲染标题行
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*** 皮肤层列定义:内核列定义 + 纯渲染字段(对齐 / 文案格式化),不参与排序筛选语义 ***/
|
||||||
|
export interface TableColumn<T = unknown> extends ColumnDef<T> {
|
||||||
|
align?: 'left' | 'center' | 'right'; // 表头与单元格对齐;缺省 left
|
||||||
|
format?: (value: unknown, row: T) => string; // 单元格文案格式化(返回纯文本,走 textContent 渲染)
|
||||||
|
}
|
||||||
|
|
||||||
/*** 数据表格入参 ***/
|
/*** 数据表格入参 ***/
|
||||||
export interface DataTableProps<T> {
|
export interface DataTableProps<T> {
|
||||||
data: T[]; // 全量行数据,行须含 id 字段作 rowKey
|
data: T[]; // 全量行数据,行须含 id 字段作 rowKey
|
||||||
columns: ColumnDef<T>[]; // 列定义
|
columns: TableColumn<T>[]; // 列定义:内核字段 + align / format
|
||||||
filterKey?: string; // 工具栏筛选的列;缺省取第一列
|
filterKey?: string; // 工具栏筛选的列;缺省取第一列
|
||||||
pageSize?: number; // 每页行数;缺省 5
|
pageSize?: number; // 每页行数;缺省 5
|
||||||
selectable?: boolean; // 是否启用行选择列(含全选三态与已选计数);缺省 false
|
selectable?: boolean; // 是否启用行选择列(含全选三态与已选计数);缺省 false
|
||||||
|
page?: Signal<number>; // 受控页码信号:与内核分页双向同步,可接独立 pagination 组件联动
|
||||||
|
loading?: boolean | Signal<boolean>; // 加载态:为真时数据行换成骨架行,筛选与翻页禁用
|
||||||
|
selected?: Signal<RowKey[]>; // 受控选中行信号:与内核选择双向同步,供批量操作读取
|
||||||
}
|
}
|
||||||
|
|
||||||
/*** 选项条目:select / radioGroup / tabs 共用的选项形状 ***/
|
/*** 选项条目:select / radioGroup / tabs 共用的选项形状 ***/
|
||||||
@@ -40,6 +70,7 @@ export interface OptionItem {
|
|||||||
export interface CheckboxProps {
|
export interface CheckboxProps {
|
||||||
label?: string; // 右侧文案;缺省不渲染
|
label?: string; // 右侧文案;缺省不渲染
|
||||||
checked?: Signal<boolean>; // 勾选状态信号,双向绑定
|
checked?: Signal<boolean>; // 勾选状态信号,双向绑定
|
||||||
|
disabled?: boolean | Signal<boolean>; // 禁用态:布尔直接置位,传信号则动态同步
|
||||||
onChange?: (checked: boolean) => void; // 变更回调
|
onChange?: (checked: boolean) => void; // 变更回调
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,6 +78,7 @@ export interface CheckboxProps {
|
|||||||
export interface SwitchProps {
|
export interface SwitchProps {
|
||||||
label?: string; // 左侧文案;缺省不渲染
|
label?: string; // 左侧文案;缺省不渲染
|
||||||
checked?: Signal<boolean>; // 开关状态信号,双向绑定
|
checked?: Signal<boolean>; // 开关状态信号,双向绑定
|
||||||
|
disabled?: boolean | Signal<boolean>; // 禁用态:布尔直接置位,传信号则动态同步
|
||||||
onChange?: (checked: boolean) => void; // 变更回调
|
onChange?: (checked: boolean) => void; // 变更回调
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,6 +87,7 @@ export interface SelectProps {
|
|||||||
options: OptionItem[]; // 选项列表
|
options: OptionItem[]; // 选项列表
|
||||||
value?: Signal<string>; // 选中值信号,双向绑定
|
value?: Signal<string>; // 选中值信号,双向绑定
|
||||||
placeholder?: string; // 未选中时的占位项文案
|
placeholder?: string; // 未选中时的占位项文案
|
||||||
|
disabled?: boolean | Signal<boolean>; // 禁用态:布尔直接置位,传信号则动态同步
|
||||||
onChange?: (value: string) => void; // 变更回调
|
onChange?: (value: string) => void; // 变更回调
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,15 +95,126 @@ export interface SelectProps {
|
|||||||
export interface RadioGroupProps {
|
export interface RadioGroupProps {
|
||||||
options: OptionItem[]; // 选项列表
|
options: OptionItem[]; // 选项列表
|
||||||
value?: Signal<string>; // 选中值信号,双向绑定
|
value?: Signal<string>; // 选中值信号,双向绑定
|
||||||
|
disabled?: boolean | Signal<boolean>; // 禁用态:整组置灰且不可选
|
||||||
onChange?: (value: string) => void; // 变更回调
|
onChange?: (value: string) => void; // 变更回调
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*** 菜单项:divider 渲染成分隔线,其余为可选条目 ***/
|
||||||
|
export interface MenuItem {
|
||||||
|
value: string; // 条目标识(经 onSelect 透传)
|
||||||
|
label: string; // 条目文案
|
||||||
|
disabled?: boolean; // 禁用该条目:置灰且不参与键盘导航
|
||||||
|
danger?: boolean; // 危险操作条目:文案取 danger 语义色
|
||||||
|
shortcut?: string; // 右侧快捷键提示(仅展示,不绑定按键),如 '⌘K'
|
||||||
|
divider?: boolean; // 该条目渲染为分隔线(忽略 value / label)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*** 下拉菜单入参 ***/
|
||||||
|
export interface MenuProps {
|
||||||
|
label: string; // 触发器文案
|
||||||
|
items: MenuItem[]; // 菜单条目列表
|
||||||
|
variant?: 'primary' | 'ghost'; // 触发器视觉强度;缺省 ghost
|
||||||
|
align?: 'start' | 'end'; // 弹层对齐方向;缺省 start(与触发器左对齐)
|
||||||
|
onSelect?: (value: string) => void; // 选中回调(禁用项不触发)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*** 面包屑条目 ***/
|
||||||
|
export interface BreadcrumbItem {
|
||||||
|
label: string; // 条目文案
|
||||||
|
href?: string; // 跳转地址;缺省时渲染 button 交给 onNavigate
|
||||||
|
}
|
||||||
|
|
||||||
|
/*** 面包屑入参 ***/
|
||||||
|
export interface BreadcrumbProps {
|
||||||
|
items: BreadcrumbItem[]; // 路径条目,末项视为当前页(不可点)
|
||||||
|
separator?: string; // 分隔符;缺省 '/'
|
||||||
|
onNavigate?: (item: BreadcrumbItem, index: number) => void; // 无 href 条目的点击回调
|
||||||
|
}
|
||||||
|
|
||||||
|
/*** 折叠面板条目 ***/
|
||||||
|
export interface AccordionItem {
|
||||||
|
value: string; // 面板标识(回调与展开集合用)
|
||||||
|
title: string; // 标题文案(同时作为触发器)
|
||||||
|
content: unknown; // 面板内容:字符串 / DOM 节点 / 响应式函数
|
||||||
|
disabled?: boolean; // 禁用该面板:标题置灰且不可展开
|
||||||
|
}
|
||||||
|
|
||||||
|
/*** 折叠面板入参 ***/
|
||||||
|
export interface AccordionProps {
|
||||||
|
items: AccordionItem[]; // 面板列表
|
||||||
|
active?: Signal<string>; // 展开面板信号(单开模式);缺省由组件内部持有状态
|
||||||
|
multiple?: boolean; // 是否允许多个面板同时展开;缺省 false(手风琴)
|
||||||
|
onChange?: (active: string) => void; // 展开变化回调:收起时传空串
|
||||||
|
}
|
||||||
|
|
||||||
|
/*** 滑块入参 ***/
|
||||||
|
export interface SliderProps {
|
||||||
|
value: Signal<number>; // 当前值信号,双向绑定(拖动写回、外部赋值同步)
|
||||||
|
min?: number; // 最小值;缺省 0
|
||||||
|
max?: number; // 最大值;缺省 100
|
||||||
|
step?: number; // 步长;缺省 1
|
||||||
|
disabled?: boolean | Signal<boolean>; // 禁用态:布尔直接置位,传信号则动态同步
|
||||||
|
showValue?: boolean; // 是否右侧显示当前值;缺省 true
|
||||||
|
onChange?: (value: number) => void; // 拖动回调
|
||||||
|
}
|
||||||
|
|
||||||
/*** 多行输入入参 ***/
|
/*** 多行输入入参 ***/
|
||||||
export interface TextareaProps {
|
export interface TextareaProps {
|
||||||
placeholder?: string; // 占位文案
|
placeholder?: string; // 占位文案
|
||||||
rows?: number; // 行数;缺省 3
|
rows?: number; // 行数;缺省 3
|
||||||
value?: Signal<string>; // 双向绑定的信号
|
value?: Signal<string>; // 双向绑定的信号
|
||||||
|
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; // 是否虚线;缺省实线
|
||||||
}
|
}
|
||||||
|
|
||||||
/*** 徽标入参 ***/
|
/*** 徽标入参 ***/
|
||||||
@@ -79,9 +223,57 @@ export interface BadgeProps {
|
|||||||
variant?: 'default' | 'success' | 'warning' | 'danger'; // 语义色;缺省 default
|
variant?: 'default' | 'success' | 'warning' | 'danger'; // 语义色;缺省 default
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*** 头像入参 ***/
|
||||||
|
export interface AvatarProps {
|
||||||
|
name: string; // 名称:生成占位首字,并作为图片 alt 与悬停标题
|
||||||
|
src?: string; // 图片地址;缺省用首字占位
|
||||||
|
size?: number; // 直径像素;缺省 32
|
||||||
|
variant?: 'default' | 'success' | 'warning' | 'danger'; // 占位底色语义档;缺省 default
|
||||||
|
}
|
||||||
|
|
||||||
|
/*** 标签入参(可关闭,与纯展示的 badge 区分) ***/
|
||||||
|
export interface TagProps {
|
||||||
|
label: string; // 标签文案
|
||||||
|
variant?: 'default' | 'success' | 'warning' | 'danger' | 'info'; // 语义色;缺省 default
|
||||||
|
closable?: boolean; // 显示 × 关闭按钮;缺省 false
|
||||||
|
onClose?: () => void; // 关闭回调(调用方自行从数据源移除该项)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*** 图片入参 ***/
|
||||||
|
export interface ImageProps {
|
||||||
|
src: string; // 图片地址(缩略图与大图共用)
|
||||||
|
alt?: string; // 替代文本;缺省空串
|
||||||
|
width?: number; // 缩略图宽度像素,高度自适应;缺省走样式默认
|
||||||
|
}
|
||||||
|
|
||||||
/*** 提示条入参 ***/
|
/*** 提示条入参 ***/
|
||||||
export interface AlertProps {
|
export interface AlertProps {
|
||||||
message: string; // 提示内容
|
message: string; // 提示内容
|
||||||
|
title?: string; // 可选的加粗标题行;缺省只渲染正文
|
||||||
|
variant?: 'info' | 'success' | 'warning' | 'danger'; // 语义色;缺省 info
|
||||||
|
closable?: boolean; // 显示 × 关闭按钮(退场后从 DOM 移除);缺省 false
|
||||||
|
onClose?: () => void; // 关闭回调,供调用方同步业务状态
|
||||||
|
}
|
||||||
|
|
||||||
|
/*** 骨架屏入参 ***/
|
||||||
|
export interface SkeletonProps {
|
||||||
|
variant?: 'text' | 'rect' | 'circle'; // 占位形状;缺省 rect
|
||||||
|
rows?: number; // text 形状的行数;缺省 3
|
||||||
|
}
|
||||||
|
|
||||||
|
/*** 分页器入参 ***/
|
||||||
|
export interface PaginationProps {
|
||||||
|
page: Signal<number>; // 当前页码信号,双向绑定(外部赋值自动翻页)
|
||||||
|
pageSize: number; // 每页条数
|
||||||
|
total: number; // 总条数
|
||||||
|
onChange?: (page: number) => void; // 翻页回调
|
||||||
|
}
|
||||||
|
|
||||||
|
/*** toast 消息提示入参 ***/
|
||||||
|
export interface ToastProps {
|
||||||
|
message: string; // 提示文案
|
||||||
|
duration?: number; // 自动消失毫秒;缺省 3000
|
||||||
|
closable?: boolean; // 是否显示关闭按钮(手动立即收起);缺省 false
|
||||||
variant?: 'info' | 'success' | 'warning' | 'danger'; // 语义色;缺省 info
|
variant?: 'info' | 'success' | 'warning' | 'danger'; // 语义色;缺省 info
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,8 +284,8 @@ export interface SpinnerProps {
|
|||||||
|
|
||||||
/*** 进度条入参 ***/
|
/*** 进度条入参 ***/
|
||||||
export interface ProgressProps {
|
export interface ProgressProps {
|
||||||
value: Signal<number>; // 当前进度 0-100 的信号
|
value?: Signal<number>; // 当前进度 0-100 的信号;不传即不确定态(未知时长的加载)
|
||||||
showText?: boolean; // 是否右侧显示百分比;缺省 true
|
showText?: boolean; // 是否右侧显示百分比;缺省 true(不确定态忽略)
|
||||||
}
|
}
|
||||||
|
|
||||||
/*** 标签页入参 ***/
|
/*** 标签页入参 ***/
|
||||||
@@ -107,9 +299,21 @@ export interface TabsProps {
|
|||||||
export interface ModalProps {
|
export interface ModalProps {
|
||||||
title: string; // 标题
|
title: string; // 标题
|
||||||
open: Signal<boolean>; // 开关信号:true 显示 / false 隐藏
|
open: Signal<boolean>; // 开关信号:true 显示 / false 隐藏
|
||||||
|
size?: 'sm' | 'md' | 'lg'; // 宽度档位;缺省 md(420px),sm 340px / lg 640px
|
||||||
|
footer?: unknown; // 底部操作区内容(常见为按钮组);缺省不渲染
|
||||||
onClose?: () => void; // 关闭回调(点遮罩、× 或 Esc 触发)
|
onClose?: () => void; // 关闭回调(点遮罩、× 或 Esc 触发)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*** 抽屉入参 ***/
|
||||||
|
export interface DrawerProps {
|
||||||
|
title: string; // 标题
|
||||||
|
open: Signal<boolean>; // 开关信号:true 滑出 / false 收起
|
||||||
|
placement?: 'left' | 'right'; // 滑出方向;缺省 right
|
||||||
|
width?: number; // 面板宽度像素;缺省 320
|
||||||
|
footer?: unknown; // 底部操作区内容;缺省不渲染
|
||||||
|
onClose?: () => void; // 关闭回调(遮罩 / × / Esc 触发)
|
||||||
|
}
|
||||||
|
|
||||||
/*** 表单校验规则:按序执行,首个失败即停止 ***/
|
/*** 表单校验规则:按序执行,首个失败即停止 ***/
|
||||||
export interface FormRule {
|
export interface FormRule {
|
||||||
required?: boolean; // 非空校验(trim 后判定)
|
required?: boolean; // 非空校验(trim 后判定)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
+1018
-8
File diff suppressed because it is too large
Load Diff
@@ -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)'}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*** 读取注册表索引;缺失则报错退出 ***/
|
/*** 读取注册表索引;缺失则报错退出 ***/
|
||||||
@@ -191,9 +202,9 @@ var ORDER={skin-dom:0,'headless-core':1,'design-tokens':2};
|
|||||||
function esc(s){return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')}
|
function esc(s){return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')}
|
||||||
function propsTable(c){
|
function propsTable(c){
|
||||||
var rows=(c.props||[]).map(function(p){
|
var rows=(c.props||[]).map(function(p){
|
||||||
return '<tr><td><code>'+esc(p.n)+'</code></td><td><code>'+esc(p.t)+'</code></td><td>'+(p.r?'必填':'否')+'</td><td>'+esc(p.d||'')+(p.k?'(回调)':'')+'</td></tr>';
|
return '<tr><td><code>'+esc(p.n)+'</code></td><td><code>'+esc(p.t)+'</code></td><td>'+(p.r?'必填':'否')+'</td><td>'+(p.df?'<code>'+esc(p.df)+'</code>':'—')+'</td><td>'+esc(p.d||'')+(p.k?'(回调)':'')+'</td></tr>';
|
||||||
}).join('');
|
}).join('');
|
||||||
return '<table class="props"><tr><th>参数</th><th>类型</th><th>必填</th><th>说明</th></tr>'+rows+'</table>';
|
return '<table class="props"><tr><th>参数</th><th>类型</th><th>必填</th><th>默认值</th><th>说明</th></tr>'+rows+'</table>';
|
||||||
}
|
}
|
||||||
function compBlock(c){
|
function compBlock(c){
|
||||||
var steps=(c.steps||[]).map(function(s){return '<li>'+esc(s)+'</li>'}).join('');
|
var steps=(c.steps||[]).map(function(s){return '<li>'+esc(s)+'</li>'}).join('');
|
||||||
@@ -289,7 +300,7 @@ function renderAgentDocs(index, tokens) {
|
|||||||
if (c.props?.length) {
|
if (c.props?.length) {
|
||||||
lines.push('参数:', '');
|
lines.push('参数:', '');
|
||||||
for (const prop of c.props) {
|
for (const prop of c.props) {
|
||||||
const marks = [prop.r ? '必填' : '', prop.k ? '回调' : ''].filter(Boolean).join(',');
|
const marks = [prop.r ? '必填' : '', prop.k ? '回调' : '', prop.df ? `默认 ${prop.df}` : ''].filter(Boolean).join(',');
|
||||||
lines.push(`- ${prop.n}${marks ? `(${marks})` : ''}:${prop.t}${prop.d ? ` —— ${prop.d}` : ''}`);
|
lines.push(`- ${prop.n}${marks ? `(${marks})` : ''}:${prop.t}${prop.d ? ` —— ${prop.d}` : ''}`);
|
||||||
}
|
}
|
||||||
lines.push('');
|
lines.push('');
|
||||||
@@ -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)`,
|
||||||
);
|
);
|
||||||
@@ -491,6 +504,11 @@ function collectSourceFiles() {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*** 把字符串 / 模板字面量整体替换为等长空格,便于按位置定位真实注释(避开 URL、正则里的 //) ***/
|
||||||
|
function blankStrings(line) {
|
||||||
|
return line.replace(/(['"`])(?:\\.|(?!\1)[^\\])*\1/g, (m) => ' '.repeat(m.length));
|
||||||
|
}
|
||||||
|
|
||||||
/*** 检查单文件:导出缺注释块、接口字段缺行尾注释、行注释非中文 ***/
|
/*** 检查单文件:导出缺注释块、接口字段缺行尾注释、行注释非中文 ***/
|
||||||
function styleCheckFile(file, src) {
|
function styleCheckFile(file, src) {
|
||||||
const issues = [];
|
const issues = [];
|
||||||
@@ -528,7 +546,7 @@ function styleCheckFile(file, src) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const hash = raw.indexOf('//');
|
const hash = blankStrings(raw).indexOf('//'); // 注释定位:先剥离字面量,'https://' 不算注释
|
||||||
if (hash >= 0) {
|
if (hash >= 0) {
|
||||||
const text = raw.slice(hash + 2);
|
const text = raw.slice(hash + 2);
|
||||||
const isPragma = /^[\s*/]*(eslint|@ts-|prettier-ignore|istanbul|c8|node:)/i.test(text);
|
const isPragma = /^[\s*/]*(eslint|@ts-|prettier-ignore|istanbul|c8|node:)/i.test(text);
|
||||||
@@ -555,6 +573,172 @@ function cmdStyle() {
|
|||||||
console.log(`style OK (${files.length} files)`);
|
console.log(`style OK (${files.length} files)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*** 皮肤包硬编码颜色检查:#hex 字面量一律禁止,颜色必须走 --ua-* 令牌(rgba 仅限阴影/遮罩的透明度叠加) ***/
|
||||||
|
function checkSkinCss(pkg) {
|
||||||
|
const issues = [];
|
||||||
|
const dir = path.join(ROOT, pkg.dir);
|
||||||
|
if (!fs.existsSync(dir)) return issues;
|
||||||
|
for (const name of fs.readdirSync(dir)) {
|
||||||
|
if (!name.endsWith('.css')) continue; // 只查样式表
|
||||||
|
const file = path.join(dir, name);
|
||||||
|
const lines = fs.readFileSync(file, 'utf8').split(/\r?\n/);
|
||||||
|
lines.forEach((line, i) => {
|
||||||
|
if (/#[0-9a-fA-F]{3,8}\b/.test(line)) issues.push(`${pkg.dir}/${name}:${i + 1} 皮肤包禁止硬编码颜色,改用 --ua-* 令牌`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return issues;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*** README 组件区块校验:marker 之间的 id 集合必须与全部清单的 docs.components 完全一致(防文档漂移) ***/
|
||||||
|
function checkReadme(pkgs) {
|
||||||
|
const issues = [];
|
||||||
|
const file = path.join(ROOT, 'README.md');
|
||||||
|
if (!fs.existsSync(file)) return issues; // 卫语句:没有 README 就不校验
|
||||||
|
const block = fs.readFileSync(file, 'utf8').match(/<!-- ua:components -->([\s\S]*?)<!-- \/ua:components -->/);
|
||||||
|
if (!block) {
|
||||||
|
issues.push('README.md 缺少组件区块标记(<!-- ua:components --> … <!-- /ua:components -->)');
|
||||||
|
return issues;
|
||||||
|
}
|
||||||
|
const listed = new Set(block[1].split('·').map((s) => s.trim()).filter(Boolean));
|
||||||
|
const actual = new Set(pkgs.flatMap((p) => (p.docs?.components ?? []).map((c) => c.id)));
|
||||||
|
for (const id of actual) if (!listed.has(id)) issues.push(`README.md 组件区块缺少 ${id}`);
|
||||||
|
for (const id of listed) if (!actual.has(id)) issues.push(`README.md 组件区块多出 ${id}(清单里没有)`);
|
||||||
|
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']) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*** 清单一致性校验:组件文档块的 id / props 说明 / demo 锚点必须与代码和演示产物对齐 ***/
|
||||||
|
function cmdVerify() {
|
||||||
|
const issues = [];
|
||||||
|
const pkgs = collectManifests(); // 一次收集,供组件校验与 README 区块校验共用
|
||||||
|
const demoPath = path.join(ROOT, 'apps', 'demo', 'main.ts'); // 演示区块定义在源码里(产物是打包后的 JS)
|
||||||
|
const demoSource = fs.existsSync(demoPath) ? fs.readFileSync(demoPath, 'utf8') : '';
|
||||||
|
const anchors = new Set(demoSource.match(/sec-[a-z0-9-]+/g) ?? []); // 已定义的区块锚点
|
||||||
|
if (!demoSource) console.log('verify: 未找到 apps/demo/main.ts,跳过 demo 锚点校验');
|
||||||
|
|
||||||
|
for (const pkg of pkgs) {
|
||||||
|
const comps = pkg.docs?.components ?? []; // 清单已被 collectManifests 摊平
|
||||||
|
const exports = pkg.api?.exports ?? [];
|
||||||
|
if (pkg.type === 'skin-dom') issues.push(...checkSkinCss(pkg));
|
||||||
|
if (comps.length && !exports.length) issues.push(`${pkg.name}: docs.components 有 ${comps.length} 项但 api.exports 为空`);
|
||||||
|
|
||||||
|
const seen = new Set(); // 组件 id 去重(同一清单内不允许重复)
|
||||||
|
for (const c of comps) {
|
||||||
|
const tag = `${pkg.name}#${c.id ?? '?'}`;
|
||||||
|
if (!c.id || !c.title) issues.push(`${tag}: 缺少 id 或 title`);
|
||||||
|
if (seen.has(c.id)) issues.push(`${tag}: id 重复`);
|
||||||
|
seen.add(c.id);
|
||||||
|
if (!c.example) issues.push(`${tag}: 缺少 example`);
|
||||||
|
if ((c.steps ?? []).length < 3) issues.push(`${tag}: steps 少于 3 条`);
|
||||||
|
if (!exports.some((e) => e.startsWith(`${c.id}(`) || e.startsWith(`${c.id}.`))) {
|
||||||
|
issues.push(`${tag}: api.exports 中没有以「${c.id}(」或「${c.id}.」开头的条目`);
|
||||||
|
}
|
||||||
|
for (const prop of c.props ?? []) {
|
||||||
|
if (!prop.n || !prop.t || !prop.d) issues.push(`${tag}: props 条目 ${prop.n ?? '?'} 缺 n/t/d`);
|
||||||
|
}
|
||||||
|
if (c.demo && demoSource && !anchors.has(c.demo)) {
|
||||||
|
issues.push(`${tag}: demo 锚点 ${c.demo} 在 apps/demo/main.ts 中不存在(缺对应 section)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
|
||||||
|
if (issues.length) {
|
||||||
|
for (const it of issues) console.error('verify: ' + it);
|
||||||
|
console.error(`${issues.length} violation(s)`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
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);
|
||||||
@@ -578,10 +762,19 @@ switch (cmd) {
|
|||||||
case 'style':
|
case 'style':
|
||||||
cmdStyle();
|
cmdStyle();
|
||||||
break;
|
break;
|
||||||
|
case 'verify':
|
||||||
|
cmdVerify();
|
||||||
|
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|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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"name": "@ui-arch/cli",
|
||||||
|
"type": "tooling",
|
||||||
|
"description": "ua 命令行与 MCP 注册表服务:把各包 ua.manifest.json 汇总成机器可读索引(.ua/index.json)、Agent 全量文档(llms-full.txt)与人类文档站,并提供清单一致性门禁。",
|
||||||
|
"tags": ["cli", "tooling", "mcp", "docs", "gate"],
|
||||||
|
"api": {
|
||||||
|
"entry": "bin/ua.js",
|
||||||
|
"exports": [
|
||||||
|
"ua index:扫描 packages/ 两层目录的 ua.manifest.json,生成 .ua/index.json 与 llms.txt",
|
||||||
|
"ua list:列出注册表内所有包与组件数",
|
||||||
|
"ua info <name>:查看单个包的清单详情(api / 约定 / 文档步骤)",
|
||||||
|
"ua docs:生成人类文档站 apps/docs/index.html + demo.html 与 Agent 全量文档 llms-full.txt",
|
||||||
|
"ua style:注释规范门禁(导出必须 /*** ***/、接口字段行尾注释、行注释中文),违规退出码 1",
|
||||||
|
"ua verify:清单一致性门禁(id 唯一 / exports 对应 / props 必带 n t d / steps≥3 / demo 锚点存在)",
|
||||||
|
"ua commit-lint <文件|message>:提交信息门禁(type 枚举 / scope 规则 / subject 中文祈使 ≤50 字符),规范见 COMMITTING.md,违规退出码 1",
|
||||||
|
"ua mcp:零依赖 stdio MCP server,暴露 list_components / get_component / search_components"
|
||||||
|
],
|
||||||
|
"keyTypes": ["ua.manifest.json 清单结构", "MCP tool 入参 schema"]
|
||||||
|
},
|
||||||
|
"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": [
|
||||||
|
"零依赖:只用 node 标准库,Node >= 18",
|
||||||
|
"清单是单一来源:代码注释、文档站、llms-full.txt、MCP 全部从 ua.manifest.json 派生",
|
||||||
|
"门禁只增不改:新增校验必须能被人手复现,报错要指出文件与字段"
|
||||||
|
],
|
||||||
|
"docs": {
|
||||||
|
"intro": "工程层工具包(private,不发布)。所有 Agent 与人都通过它读取组件库:清单驱动索引、文档、MCP 三件套,外加 style / verify 两道门禁。",
|
||||||
|
"steps": [
|
||||||
|
"装好依赖后先跑 npm run check 确认基线全绿(build → style → typecheck → smoke → index → verify)",
|
||||||
|
"查组件:ua list 看全量,ua info <包名> 看单包 api 与约定",
|
||||||
|
"改完清单跑 npm run docs:同时刷新人类文档站与 llms-full.txt(Agent 首选喂料)",
|
||||||
|
"提交前 npm run check:style 查注释规范,verify 查清单 ↔ 文档 ↔ 演示锚点是否一致",
|
||||||
|
"在 ~/.workbuddy/mcp.json 注册 ua mcp 后,Agent 可直接调 list_components / get_component / search_components"
|
||||||
|
],
|
||||||
|
"components": []
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+53
@@ -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: {}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
/*** 作用:Gitea REST API 调用包装器,用法 node scripts/gitea-api.mjs <METHOD> <path不带前导斜杠> [jsonBody],令牌读自 .gitea-token ***/
|
||||||
|
import { readFileSync } from 'node:fs';
|
||||||
|
|
||||||
|
// 仓库与令牌
|
||||||
|
const base = 'https://gitea.yinyuanjingling.icu/api/v1';
|
||||||
|
const token = readFileSync(new URL('../.gitea-token', import.meta.url), 'utf8').trim();
|
||||||
|
const [method = 'GET', path = '', body = ''] = process.argv.slice(2);
|
||||||
|
// 卫语句:缺参数退出
|
||||||
|
if (!method || !path) { console.error('用法: node scripts/gitea-api.mjs <METHOD> <path> [jsonBody]'); process.exit(2); }
|
||||||
|
|
||||||
|
// 发起请求并输出结果(path 不带前导斜杠,规避 Git Bash MSYS 路径转换篡改)
|
||||||
|
const res = await fetch(`${base}/${path.replace(/^\//, '')}`, {
|
||||||
|
method: method.toUpperCase(),
|
||||||
|
headers: { 'Authorization': `token ${token}`, 'Content-Type': 'application/json' },
|
||||||
|
body: body || undefined,
|
||||||
|
});
|
||||||
|
const text = await res.text();
|
||||||
|
console.log(`HTTP ${res.status}\n${text}`);
|
||||||
|
process.exit(res.ok ? 0 : 1);
|
||||||
Reference in New Issue
Block a user