Compare commits
1 Commits
d7226f452d
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 42248bf4bb |
@@ -5,5 +5,3 @@ dist/
|
|||||||
apps/docs/*.html
|
apps/docs/*.html
|
||||||
.workbuddy/
|
.workbuddy/
|
||||||
.gitea-token
|
.gitea-token
|
||||||
.ua/loop.lock
|
|
||||||
.ua/heartbeat*
|
|
||||||
|
|||||||
+10
-246
@@ -6,276 +6,40 @@
|
|||||||
"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": "done",
|
"status": "open",
|
||||||
"notes": "API 定为 tooltip(props, trigger) 包裹式;delay 默认 150ms,Esc 立即收起,气泡 pointer-events:none 防抖,aria-describedby + role=tooltip 保证读屏可读;demo 新增 sec-tooltip 区块。顺带修复 ua style 对 https:// 的误判(先剥离字面量再定位 //)"
|
"notes": ""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"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": "done",
|
"status": "open",
|
||||||
"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 区块"
|
"notes": ""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"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": "done",
|
"status": "open",
|
||||||
"notes": "skeleton({variant, rows}):text(多行,末行缩短)/ rect / circle 三形状,扫光动画(::after 位移 + 渐变),reduced-motion 下静态灰块(动画列表已补 .ua-skeleton 与 ::after);demo 新增 sec-skeleton 卡片组合演示"
|
"notes": ""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"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": "done",
|
"status": "open",
|
||||||
"notes": "pagination(props):page 信号双向绑定(受控),≤7 页全展开、更多时首尾 + 当前页 ±1 折叠省略号(renderList 重建),原生 button 键盘可达,aria-current / aria-label 完整,首末页禁用 ‹ ›;dataTable 新增可选 page 信号桥接(双向同步 + 越界钳制,防无效派发);demo sec-table 新增 pageSize 3 × 24 行的联动卡片"
|
"notes": ""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"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",
|
"status": "open",
|
||||||
"notes": "核查确认 props 表/示例/步骤/演示 iframe 早已由 ua docs 从 manifest docs.components 单一来源驱动;真正的缺口是信息量不含默认值——本轮 CLI propsTable 新增「默认值」列(props 条目可选 df 字段,缺省渲染 —),llms-full.txt 参数行同步追加「默认 x」标注;16 处可机读默认值从描述文案提取进 df;check 全绿 + 文档站结构完整性校验通过"
|
"notes": ""
|
||||||
},
|
|
||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
+37
-718
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "ui-arch-registry-index/0.1",
|
"$schema": "ui-arch-registry-index/0.1",
|
||||||
"generatedAt": "2026-10-23T09:12:34.151Z",
|
"generatedAt": "2026-10-23T06:57:51.085Z",
|
||||||
"repoRoot": ".",
|
"repoRoot": ".",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
@@ -130,7 +130,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@ui-arch/ui",
|
"name": "@ui-arch/ui",
|
||||||
"type": "skin-dom",
|
"type": "skin-dom",
|
||||||
"description": "DOM 皮肤组件库:button / textField / textarea / slider / checkbox / switch / select / menu / radioGroup / accordion / breadcrumb / card / badge / tag / avatar / alert / toast / tooltip / popover / skeleton / spinner / progress / tabs / pagination / modal / drawer / form / formItem / dataTable,构建在 @ui-arch/frame 与 @ui-arch/table-core 之上,样式只消费 --ua-* 令牌。",
|
"description": "DOM 皮肤组件库:button / textField / textarea / checkbox / switch / select / radioGroup / card / badge / alert / spinner / progress / tabs / modal / form / formItem / dataTable,构建在 @ui-arch/frame 与 @ui-arch/table-core 之上,样式只消费 --ua-* 令牌。",
|
||||||
"tags": [
|
"tags": [
|
||||||
"components",
|
"components",
|
||||||
"dom",
|
"dom",
|
||||||
@@ -142,35 +142,23 @@
|
|||||||
"api": {
|
"api": {
|
||||||
"entry": "src/index.ts",
|
"entry": "src/index.ts",
|
||||||
"exports": [
|
"exports": [
|
||||||
"button(props):按钮,variant = primary | ghost | danger,disabled 支持布尔或信号禁用",
|
"button(props):按钮,variant = primary | ghost | danger",
|
||||||
"textField(props):文本输入,value 信号双向绑定,disabled 支持布尔或信号禁用",
|
"textField(props):文本输入,value 信号双向绑定",
|
||||||
"textarea(props):多行输入,value 信号双向绑定,disabled 支持布尔或信号禁用",
|
"textarea(props):多行输入,value 信号双向绑定",
|
||||||
"slider(props):滑块,value 信号双向绑定,min/max/step 可配,原生 range 承载键盘交互,右侧可显示当前值",
|
"checkbox(props):复选框,checked 信号双向绑定",
|
||||||
"checkbox(props):复选框,checked 信号双向绑定,disabled 支持布尔或信号禁用",
|
"switchButton(props):开关,checked 信号双向绑定",
|
||||||
"switchButton(props):开关,checked 信号双向绑定,disabled 支持布尔或信号禁用",
|
"select(props):自绘下拉选择(非原生 select),value 信号双向绑定,点外部 / Esc 关闭,选项弹层带微动画",
|
||||||
"select(props):自绘下拉选择(非原生 select),value 信号双向绑定,点外部 / Esc 关闭,选项弹层带微动画,disabled 支持布尔或信号禁用",
|
"radioGroup(props):单选组,options + value 信号双向绑定",
|
||||||
"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",
|
||||||
"tag(props):可关闭标签,variant 五档语义色,closable + onClose 支持从数据源移除(纯展示请用 badge)",
|
"alert(props):提示条,variant = info | success | warning | danger",
|
||||||
"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 色);不传 value 退化为不确定态无限往复",
|
"progress(props):进度条,轨道 + 渐变流动填充,value 信号驱动 0-100,满格切 success 色",
|
||||||
"tabs(props):标签页,active 信号双向绑定,滑动指示条 + 方向键环形导航,支持禁用页签",
|
"tabs(props):标签页,active 信号双向绑定,滑动指示条 + 方向键环形导航,支持禁用页签",
|
||||||
"modal(props, ...children):弹窗,open 信号驱动显隐,Esc / 遮罩 / × 关闭,size 三档宽度,footer 提供底部操作区",
|
"modal(props, ...children):弹窗,open 信号驱动显隐,Esc / 遮罩 / × 关闭",
|
||||||
"drawer(props, ...children):抽屉,open 信号驱动侧滑面板,placement 左右可配、width 可调、footer 操作区,遮罩 / × / Esc 关闭",
|
|
||||||
"form(props, items):表单容器,布局 + 聚合校验,全部通过才触发 onSubmit",
|
"form(props, items):表单容器,布局 + 聚合校验,全部通过才触发 onSubmit",
|
||||||
"formItem(props, control):表单字段,label + 必填红星 + 规则校验 + 错误区,返回 { el, validate } 句柄",
|
"formItem(props, control):表单字段,label + 必填红星 + 规则校验 + 错误区,返回 { el, validate } 句柄",
|
||||||
"dataTable(props):数据表格,粘性表头 / 行选择(全选三态)/ 排序 / 筛选 / 分页 / 空状态;loading 为真时数据行换骨架行且筛选/翻页禁用,内核为 table-core"
|
"dataTable(props):数据表格,粘性表头 / 行选择(全选三态)/ 排序 / 筛选 / 分页 / 空状态,内核为 table-core"
|
||||||
],
|
],
|
||||||
"keyTypes": [
|
"keyTypes": [
|
||||||
"ButtonProps",
|
"ButtonProps",
|
||||||
@@ -180,20 +168,10 @@
|
|||||||
"SwitchProps",
|
"SwitchProps",
|
||||||
"SelectProps",
|
"SelectProps",
|
||||||
"RadioGroupProps",
|
"RadioGroupProps",
|
||||||
"MenuItem",
|
|
||||||
"MenuProps",
|
|
||||||
"AccordionItem",
|
|
||||||
"AccordionProps",
|
|
||||||
"BreadcrumbItem",
|
|
||||||
"BreadcrumbProps",
|
|
||||||
"OptionItem",
|
"OptionItem",
|
||||||
"CardProps",
|
"CardProps",
|
||||||
"BadgeProps",
|
"BadgeProps",
|
||||||
"AlertProps",
|
"AlertProps",
|
||||||
"ToastProps",
|
|
||||||
"TooltipProps",
|
|
||||||
"SkeletonProps",
|
|
||||||
"PaginationProps",
|
|
||||||
"SpinnerProps",
|
"SpinnerProps",
|
||||||
"ProgressProps",
|
"ProgressProps",
|
||||||
"TabsProps",
|
"TabsProps",
|
||||||
@@ -202,23 +180,17 @@
|
|||||||
"FormItemProps",
|
"FormItemProps",
|
||||||
"FormItemHandle",
|
"FormItemHandle",
|
||||||
"FormProps",
|
"FormProps",
|
||||||
"DataTableProps",
|
"DataTableProps"
|
||||||
"PopoverProps",
|
|
||||||
"SliderProps",
|
|
||||||
"TagProps",
|
|
||||||
"DrawerProps",
|
|
||||||
"AvatarProps"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"usage": "const checked = signal(false); checkbox({ label: '同意', checked }); const open = signal(false); modal({ title: '确认', open }, '内容')",
|
"usage": "const checked = signal(false); checkbox({ label: '同意', checked }); const open = signal(false); modal({ title: '确认', open }, '内容')",
|
||||||
"conventions": [
|
"conventions": [
|
||||||
"皮肤不持有业务状态,一律委托 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-*"
|
|
||||||
],
|
],
|
||||||
"docs": {
|
"docs": {
|
||||||
"intro": "28 个 DOM 组件,统一用法:组件是普通函数,传入 props 返回 DOM 节点;需要状态时先创建 signal 再双向绑定。引入 CSS:import '@ui-arch/ui/ui.css'(内部已消费 tokens)。",
|
"intro": "16 个 DOM 组件,统一用法:组件是普通函数,传入 props 返回 DOM 节点;需要状态时先创建 signal 再双向绑定。引入 CSS:import '@ui-arch/ui/ui.css'(内部已消费 tokens)。",
|
||||||
"steps": [
|
"steps": [
|
||||||
"安装依赖:pnpm add @ui-arch/ui @ui-arch/frame(workspace 内自动链接)",
|
"安装依赖:pnpm add @ui-arch/ui @ui-arch/frame(workspace 内自动链接)",
|
||||||
"引入样式:在应用入口 import '@ui-arch/ui/ui.css',暗色主题在根元素设 data-theme='dark'",
|
"引入样式:在应用入口 import '@ui-arch/ui/ui.css',暗色主题在根元素设 data-theme='dark'",
|
||||||
@@ -241,13 +213,7 @@
|
|||||||
{
|
{
|
||||||
"n": "variant",
|
"n": "variant",
|
||||||
"t": "'primary' | 'ghost' | 'danger'",
|
"t": "'primary' | 'ghost' | 'danger'",
|
||||||
"d": "视觉强度;缺省 primary",
|
"d": "视觉强度;缺省 primary"
|
||||||
"df": "primary"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "disabled",
|
|
||||||
"t": "boolean | Signal<boolean>",
|
|
||||||
"d": "禁用态:布尔直接置位,传信号则动态同步"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"n": "onClick",
|
"n": "onClick",
|
||||||
@@ -278,11 +244,6 @@
|
|||||||
"t": "string",
|
"t": "string",
|
||||||
"d": "占位文案"
|
"d": "占位文案"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"n": "disabled",
|
|
||||||
"t": "boolean | Signal<boolean>",
|
|
||||||
"d": "禁用态:布尔直接置位,传信号则动态同步"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"n": "onInput",
|
"n": "onInput",
|
||||||
"t": "(value: string) => void",
|
"t": "(value: string) => void",
|
||||||
@@ -310,19 +271,13 @@
|
|||||||
{
|
{
|
||||||
"n": "rows",
|
"n": "rows",
|
||||||
"t": "number",
|
"t": "number",
|
||||||
"d": "初始行数;缺省 3",
|
"d": "初始行数;缺省 3"
|
||||||
"df": "3"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"n": "placeholder",
|
"n": "placeholder",
|
||||||
"t": "string",
|
"t": "string",
|
||||||
"d": "占位文案"
|
"d": "占位文案"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"n": "disabled",
|
|
||||||
"t": "boolean | Signal<boolean>",
|
|
||||||
"d": "禁用态:布尔直接置位,传信号则动态同步"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"n": "onInput",
|
"n": "onInput",
|
||||||
"t": "(value: string) => void",
|
"t": "(value: string) => void",
|
||||||
@@ -337,62 +292,6 @@
|
|||||||
"字数统计等派生逻辑直接在响应式文本里读 bio().length"
|
"字数统计等派生逻辑直接在响应式文本里读 bio().length"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "slider",
|
|
||||||
"title": "滑块 Slider",
|
|
||||||
"demo": "sec-slider",
|
|
||||||
"props": [
|
|
||||||
{
|
|
||||||
"n": "value",
|
|
||||||
"t": "Signal<number>",
|
|
||||||
"r": 1,
|
|
||||||
"d": "当前值信号,双向绑定:拖动写回,外部赋值同步位置"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "min",
|
|
||||||
"t": "number",
|
|
||||||
"d": "最小值;缺省 0",
|
|
||||||
"df": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "max",
|
|
||||||
"t": "number",
|
|
||||||
"d": "最大值;缺省 100",
|
|
||||||
"df": "100"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "step",
|
|
||||||
"t": "number",
|
|
||||||
"d": "步长;缺省 1",
|
|
||||||
"df": "1"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "disabled",
|
|
||||||
"t": "boolean | Signal<boolean>",
|
|
||||||
"d": "禁用态:布尔直接置位,传信号则动态同步"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "showValue",
|
|
||||||
"t": "boolean",
|
|
||||||
"d": "是否右侧显示当前值;缺省 true",
|
|
||||||
"df": "true"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "onChange",
|
|
||||||
"t": "(value: number) => void",
|
|
||||||
"k": 1,
|
|
||||||
"d": "拖动回调"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"example": "const vol = signal(40); slider({ value: vol, min: 0, max: 100, step: 5, onChange: (v) => console.log(v) })",
|
|
||||||
"steps": [
|
|
||||||
"创建数值信号(初值在 min/max 区间内),传入 value 完成双向绑定",
|
|
||||||
"用 min / max / step 限定范围与粒度,如 step: 5 只取 5 的倍数",
|
|
||||||
"拖动或按 ←→ 键改变值:原生 range 自带键盘与触屏交互,无需额外处理",
|
|
||||||
"只想看拖动条不显示数字时传 showValue: false;需要自定义展示可自行读 signal",
|
|
||||||
"禁用场景传 disabled(布尔或信号),置灰且不可拖"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "checkbox",
|
"id": "checkbox",
|
||||||
"title": "复选框 Checkbox",
|
"title": "复选框 Checkbox",
|
||||||
@@ -408,11 +307,6 @@
|
|||||||
"t": "string",
|
"t": "string",
|
||||||
"d": "右侧文案;缺省只渲染勾选框"
|
"d": "右侧文案;缺省只渲染勾选框"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"n": "disabled",
|
|
||||||
"t": "boolean | Signal<boolean>",
|
|
||||||
"d": "禁用态:布尔直接置位,传信号则动态同步"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"n": "onChange",
|
"n": "onChange",
|
||||||
"t": "(checked: boolean) => void",
|
"t": "(checked: boolean) => void",
|
||||||
@@ -442,11 +336,6 @@
|
|||||||
"t": "string",
|
"t": "string",
|
||||||
"d": "左侧文案"
|
"d": "左侧文案"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"n": "disabled",
|
|
||||||
"t": "boolean | Signal<boolean>",
|
|
||||||
"d": "禁用态:布尔直接置位,传信号则动态同步"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"n": "onChange",
|
"n": "onChange",
|
||||||
"t": "(checked: boolean) => void",
|
"t": "(checked: boolean) => void",
|
||||||
@@ -482,11 +371,6 @@
|
|||||||
"t": "string",
|
"t": "string",
|
||||||
"d": "未选中时触发器显示的占位文案"
|
"d": "未选中时触发器显示的占位文案"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"n": "disabled",
|
|
||||||
"t": "boolean | Signal<boolean>",
|
|
||||||
"d": "禁用态:布尔直接置位,传信号则动态同步;禁用时触发器置灰且不展开弹层"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"n": "onChange",
|
"n": "onChange",
|
||||||
"t": "(value: string) => void",
|
"t": "(value: string) => void",
|
||||||
@@ -502,52 +386,6 @@
|
|||||||
"选中项在弹层中用淡色弱化标记,不抢视觉焦点"
|
"选中项在弹层中用淡色弱化标记,不抢视觉焦点"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "menu",
|
|
||||||
"title": "下拉菜单 Menu",
|
|
||||||
"demo": "sec-menu",
|
|
||||||
"props": [
|
|
||||||
{
|
|
||||||
"n": "label",
|
|
||||||
"t": "string",
|
|
||||||
"r": 1,
|
|
||||||
"d": "触发器文案"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "items",
|
|
||||||
"t": "MenuItem[]",
|
|
||||||
"r": 1,
|
|
||||||
"d": "菜单条目:{ value, label, disabled?, danger?, divider?, shortcut? }"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "variant",
|
|
||||||
"t": "'primary' | 'ghost'",
|
|
||||||
"d": "触发器视觉强度;缺省 ghost",
|
|
||||||
"df": "ghost"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "align",
|
|
||||||
"t": "'start' | 'end'",
|
|
||||||
"d": "弹层对齐方向;缺省 start(与触发器左对齐)",
|
|
||||||
"df": "start"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "onSelect",
|
|
||||||
"t": "(value: string) => void",
|
|
||||||
"k": 1,
|
|
||||||
"d": "选中回调,禁用项不触发"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"example": "menu({ label: '操作', items: [{ value: 'edit', label: '编辑' }, { value: 'del', label: '删除', danger: true }], onSelect: (v) => console.log(v) })",
|
|
||||||
"steps": [
|
|
||||||
"准备 items 数组:value 是回调标识、label 是展示文案,插一条 { divider: true } 即可分出条目组",
|
|
||||||
"传 label 决定触发器文案;默认 ghost 轻量样式,主操作用 variant: 'primary'",
|
|
||||||
"点触发器展开弹层,点条目收起并触发 onSelect;点外部或按 Esc 也会收起(Esc 后焦点回到触发器)",
|
|
||||||
"条目要给快捷键提示时传 shortcut(如 '⌘K'):右侧灰字展示,组件不绑定按键,实际快捷键由调用方在全局监听",
|
|
||||||
"键盘:Tab 聚焦触发器,↑↓ 展开并在可用条目间环形移动,Enter / Space 选中;disabled 项自动跳过",
|
|
||||||
"靠右对齐避免溢出时传 align: 'end';危险操作给条目加 danger: true"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "radioGroup",
|
"id": "radioGroup",
|
||||||
"title": "单选组 RadioGroup",
|
"title": "单选组 RadioGroup",
|
||||||
@@ -564,11 +402,6 @@
|
|||||||
"t": "Signal<string>",
|
"t": "Signal<string>",
|
||||||
"d": "选中值信号,双向绑定"
|
"d": "选中值信号,双向绑定"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"n": "disabled",
|
|
||||||
"t": "boolean | Signal<boolean>",
|
|
||||||
"d": "整组禁用:全部选项与文案置灰且不可选"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"n": "onChange",
|
"n": "onChange",
|
||||||
"t": "(value: string) => void",
|
"t": "(value: string) => void",
|
||||||
@@ -580,78 +413,7 @@
|
|||||||
"steps": [
|
"steps": [
|
||||||
"创建字符串信号,初值设为默认选项的 value",
|
"创建字符串信号,初值设为默认选项的 value",
|
||||||
"传入 options 与 value,组内自动互斥",
|
"传入 options 与 value,组内自动互斥",
|
||||||
"读取 size() 获取当前选中值",
|
"读取 size() 获取当前选中值"
|
||||||
"需要整组禁用时传 disabled:布尔值静态禁用,也可传信号(如 locked)实现动态锁定"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "accordion",
|
|
||||||
"title": "折叠面板 Accordion",
|
|
||||||
"demo": "sec-accordion",
|
|
||||||
"props": [
|
|
||||||
{
|
|
||||||
"n": "items",
|
|
||||||
"t": "AccordionItem[]",
|
|
||||||
"r": 1,
|
|
||||||
"d": "面板列表:{ value, title, content, disabled? }"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "active",
|
|
||||||
"t": "Signal<string>",
|
|
||||||
"d": "受控展开面板信号(单开模式);缺省由组件内部持有状态"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "multiple",
|
|
||||||
"t": "boolean",
|
|
||||||
"d": "允许多个面板同时展开;缺省 false(手风琴,单开)",
|
|
||||||
"df": "false"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "onChange",
|
|
||||||
"t": "(active: string) => void",
|
|
||||||
"k": 1,
|
|
||||||
"d": "展开变化回调:收起时传空串"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"example": "accordion({ items: [{ value: 'a', title: '账户设置', content: '内容…' }, { value: 'b', title: '通知', content: h('p', null, '…') }] })",
|
|
||||||
"steps": [
|
|
||||||
"准备 items:value 作标识、title 作标题、content 可为字符串 / DOM 节点 / 响应式函数",
|
|
||||||
"默认手风琴模式:展开一个自动收起其他;需要同时展开多个时传 multiple: true",
|
|
||||||
"需要外部控制或读取展开项时传 active 信号(受控单开),否则组件内部自持状态",
|
|
||||||
"标题是原生 button:Tab 聚焦、Enter / Space 切换,展开态带 aria-expanded + region 关联",
|
|
||||||
"内容区用 grid 行高 0fr → 1fr 过渡,无需测量高度;reduced-motion 用户自动静态切换"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "breadcrumb",
|
|
||||||
"title": "面包屑 Breadcrumb",
|
|
||||||
"demo": "sec-breadcrumb",
|
|
||||||
"props": [
|
|
||||||
{
|
|
||||||
"n": "items",
|
|
||||||
"t": "BreadcrumbItem[]",
|
|
||||||
"r": 1,
|
|
||||||
"d": "路径条目:{ label, href? },末项自动视为当前页"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "separator",
|
|
||||||
"t": "string",
|
|
||||||
"d": "分隔符;缺省 '/'",
|
|
||||||
"df": "/"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "onNavigate",
|
|
||||||
"t": "(item, index) => void",
|
|
||||||
"k": 1,
|
|
||||||
"d": "无 href 条目的点击回调"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"example": "breadcrumb({ items: [{ label: '首页', href: '/' }, { label: '设置' }, { label: '账户' }], onNavigate: (it) => console.log(it.label) })",
|
|
||||||
"steps": [
|
|
||||||
"按层级顺序准备 items,最后一项即当前页,自动渲染为不可点的灰色文字并带 aria-current='page'",
|
|
||||||
"需要真实跳转就给条目加 href(渲染 <a>);单页应用想自行接管时不给 href,改用 onNavigate 回调",
|
|
||||||
"改分隔符传 separator,如 '›';分隔符对读屏隐藏",
|
|
||||||
"语义结构为 nav + ol + li,可直接放进页头或卡片标题栏"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -691,91 +453,13 @@
|
|||||||
{
|
{
|
||||||
"n": "variant",
|
"n": "variant",
|
||||||
"t": "'default' | 'success' | 'warning' | 'danger'",
|
"t": "'default' | 'success' | 'warning' | 'danger'",
|
||||||
"d": "语义色;缺省 default",
|
"d": "语义色;缺省 default"
|
||||||
"df": "default"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"example": "badge({ label: '成功', variant: 'success' })",
|
"example": "badge({ label: '成功', variant: 'success' })",
|
||||||
"steps": [
|
"steps": [
|
||||||
"选择语义色:default 中性、success 成功、warning 警告、danger 危险",
|
"选择语义色:default 中性、success 成功、warning 警告、danger 危险",
|
||||||
"传入 label 即可,纯展示组件无状态",
|
"传入 label 即可,纯展示组件无状态"
|
||||||
"状态语义靠 variant 承载,不要自造颜色;纯展示不可交互,需要操作请配 button"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "tag",
|
|
||||||
"title": "标签 Tag",
|
|
||||||
"demo": "sec-tag",
|
|
||||||
"props": [
|
|
||||||
{
|
|
||||||
"n": "label",
|
|
||||||
"t": "string",
|
|
||||||
"r": 1,
|
|
||||||
"d": "标签文案"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "variant",
|
|
||||||
"t": "'default' | 'success' | 'warning' | 'danger' | 'info'",
|
|
||||||
"d": "语义色;缺省 default",
|
|
||||||
"df": "default"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "closable",
|
|
||||||
"t": "boolean",
|
|
||||||
"d": "显示 × 关闭按钮;缺省 false(纯展示)",
|
|
||||||
"df": "false"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "onClose",
|
|
||||||
"t": "() => void",
|
|
||||||
"k": 1,
|
|
||||||
"d": "关闭回调:调用方从数据源移除该项后重新渲染"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"example": "tag({ label: '前端', variant: 'info', closable: true, onClose: () => remove('前端') })",
|
|
||||||
"steps": [
|
|
||||||
"纯展示用 badge,需要可移除的标签(筛选项 / 已选条件)用 tag",
|
|
||||||
"按语义选 variant:info 中性标记、success 通过、warning 待处理、danger 已失效",
|
|
||||||
"closable: true 时必须配 onClose:把数据源里的该项删掉再重渲染(组件无状态,不自行摘除节点)",
|
|
||||||
"组合用法:容器里用 renderList 按信号数组渲染 tag 列表,删除即写回信号",
|
|
||||||
"标签是内联元素,可直接排进 .row 等容器,自动换行由容器决定"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "avatar",
|
|
||||||
"title": "头像 Avatar",
|
|
||||||
"demo": "sec-avatar",
|
|
||||||
"props": [
|
|
||||||
{
|
|
||||||
"n": "name",
|
|
||||||
"t": "string",
|
|
||||||
"r": 1,
|
|
||||||
"d": "名称:生成占位首字,并作为图片 alt 与悬停标题"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "src",
|
|
||||||
"t": "string",
|
|
||||||
"d": "图片地址;缺省用首字占位"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "size",
|
|
||||||
"t": "number",
|
|
||||||
"d": "直径像素;缺省 32",
|
|
||||||
"df": "32"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "variant",
|
|
||||||
"t": "'default' | 'success' | 'warning' | 'danger'",
|
|
||||||
"d": "占位底色语义档;缺省 default",
|
|
||||||
"df": "default"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"example": "avatar({ name: '张三' }); avatar({ name: 'Alice', size: 40, variant: 'success' })",
|
|
||||||
"steps": [
|
|
||||||
"只传 name:用名称首字做占位(中文取首字、英文大写),底色走 accent 淡化,暗色自动适配",
|
|
||||||
"有图片就传 src:渲染 img 并用 name 作 alt,读屏语义完整;图片缺失时由浏览器 alt 兜底",
|
|
||||||
"尺寸用 size(像素)控制,头像不参与布局拉伸(flex:none),可安全排进 .row 与表格单元格",
|
|
||||||
"需要区分类别(在线/告警)时用 variant 换底色档,颜色一律来自令牌"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -789,198 +473,16 @@
|
|||||||
"r": 1,
|
"r": 1,
|
||||||
"d": "提示内容"
|
"d": "提示内容"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"n": "title",
|
|
||||||
"t": "string",
|
|
||||||
"d": "加粗标题行;缺省只渲染正文"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"n": "variant",
|
"n": "variant",
|
||||||
"t": "'info' | 'success' | 'warning' | 'danger'",
|
"t": "'info' | 'success' | 'warning' | 'danger'",
|
||||||
"d": "语义色;缺省 info",
|
"d": "语义色;缺省 info"
|
||||||
"df": "info"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "closable",
|
|
||||||
"t": "boolean",
|
|
||||||
"d": "显示 × 关闭按钮,退场后从 DOM 移除;缺省 false",
|
|
||||||
"df": "false"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "onClose",
|
|
||||||
"t": "() => void",
|
|
||||||
"k": 1,
|
|
||||||
"d": "关闭回调,供调用方同步业务状态"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"example": "alert({ message: '操作可能存在风险', variant: 'warning' })",
|
"example": "alert({ message: '操作可能存在风险', variant: 'warning' })",
|
||||||
"steps": [
|
"steps": [
|
||||||
"按信息级别选 variant:info 提示、success 成功、warning 警告、danger 错误",
|
"按信息级别选 variant:info 提示、success 成功、warning 警告、danger 错误",
|
||||||
"传入 message 即可,入场自带下滑动画",
|
"传入 message 即可,入场自带下滑动画"
|
||||||
"需要可关闭或带操作按钮时,外层用 h() 组合 button 自行拼装,alert 只负责语义底色",
|
|
||||||
"需要用户可自行消除的提示传 closable: true:点 × 淡出并从 DOM 移除,配合 onClose 同步业务状态",
|
|
||||||
"有标题与正文两层信息时传 title(加粗首行),正文仍走 message"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "skeleton",
|
|
||||||
"title": "骨架屏 Skeleton",
|
|
||||||
"demo": "sec-skeleton",
|
|
||||||
"props": [
|
|
||||||
{
|
|
||||||
"n": "variant",
|
|
||||||
"t": "'text' | 'rect' | 'circle'",
|
|
||||||
"d": "占位形状;缺省 rect(整块矩形)",
|
|
||||||
"df": "rect"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "rows",
|
|
||||||
"t": "number",
|
|
||||||
"d": "text 形状的行数;缺省 3,末行自动缩短模拟段落",
|
|
||||||
"df": "3"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"example": "skeleton({ variant: 'text', rows: 3 }); skeleton({ variant: 'circle' }); skeleton({ variant: 'rect' })",
|
|
||||||
"steps": [
|
|
||||||
"数据加载期间用 skeleton 顶替真实内容,加载完成后整体替换",
|
|
||||||
"文本段落用 variant: 'text' + rows 控制行数,头像用 'circle',卡片/图片区域用 'rect'",
|
|
||||||
"占位块自带扫光动画,reduced-motion 用户自动静态化为纯灰块",
|
|
||||||
"组合布局:skeleton 是普通 DOM,可放进 card / row 等任意容器"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "toast",
|
|
||||||
"title": "消息提示 Toast",
|
|
||||||
"demo": "sec-toast",
|
|
||||||
"props": [
|
|
||||||
{
|
|
||||||
"n": "message",
|
|
||||||
"t": "string",
|
|
||||||
"r": 1,
|
|
||||||
"d": "提示文案"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "duration",
|
|
||||||
"t": "number",
|
|
||||||
"d": "自动消失毫秒;缺省 3000",
|
|
||||||
"df": "3000"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "closable",
|
|
||||||
"t": "boolean",
|
|
||||||
"d": "显示关闭按钮,点 × 立即收起;缺省 false(仅可关闭的 toast 接收指针事件,其余保持穿透)",
|
|
||||||
"df": "false"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "variant",
|
|
||||||
"t": "'info' | 'success' | 'warning' | 'danger'",
|
|
||||||
"d": "语义色;缺省 info(toast.error 对应 danger)",
|
|
||||||
"df": "info"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"example": "toast.success('保存成功'); toast.error('网络请求失败', 5000)",
|
|
||||||
"steps": [
|
|
||||||
"直接调用语义工厂:toast.info / success / warning / error(message, duration?),无需先建容器",
|
|
||||||
"不传 duration 时默认停留 3 秒后自动淡出并移除节点",
|
|
||||||
"多条消息在右上角自动纵向堆叠,互不遮挡",
|
|
||||||
"需要完整入参(含 variant)时用 showToast(props);组件挂载容器由内部懒创建,样式随 ui.css 一起引入",
|
|
||||||
"同屏最多 4 条,超出自动挤掉最早一条;需要用户主动确认的消息用 showToast({ closable: true }) 给个 ×"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "tooltip",
|
|
||||||
"title": "悬浮提示 Tooltip",
|
|
||||||
"demo": "sec-tooltip",
|
|
||||||
"props": [
|
|
||||||
{
|
|
||||||
"n": "content",
|
|
||||||
"t": "string",
|
|
||||||
"r": 1,
|
|
||||||
"d": "提示文案(纯文本)"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "placement",
|
|
||||||
"t": "'top' | 'bottom'",
|
|
||||||
"d": "气泡方位;缺省 top(触发器上方)",
|
|
||||||
"df": "top"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "delay",
|
|
||||||
"t": "number",
|
|
||||||
"d": "出场延迟毫秒;缺省 150,传 0 立即显示",
|
|
||||||
"df": "150"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "trigger",
|
|
||||||
"t": "HTMLElement",
|
|
||||||
"r": 1,
|
|
||||||
"d": "第二个参数:被包裹的触发器元素(按钮 / 输入框等任意 DOM)"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"example": "tooltip({ content: '保存后立即生效' }, button({ label: '保存', onClick: save }))",
|
|
||||||
"steps": [
|
|
||||||
"先创建触发器元素,如 const btn = button({ label: '保存' })",
|
|
||||||
"用 tooltip(props, trigger) 包一层:返回带气泡的包裹元素,原触发器仍是独立节点",
|
|
||||||
"鼠标移入或键盘聚焦约 150ms 后出场,移出 / 失焦立即收起;按 Esc 可立刻关闭",
|
|
||||||
"气泡默认在上方,空间不足或需要避让时传 placement: 'bottom'",
|
|
||||||
"气泡 pointer-events: none 不抢指针;判读依赖 aria-describedby + role='tooltip',读屏可读",
|
|
||||||
"只做展示:不要在气泡里放可点击元素,需要交互请用 modal 或自绘弹层"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "popover",
|
|
||||||
"title": "气泡卡片 Popover",
|
|
||||||
"demo": "sec-popover",
|
|
||||||
"props": [
|
|
||||||
{
|
|
||||||
"n": "content",
|
|
||||||
"t": "unknown",
|
|
||||||
"r": 1,
|
|
||||||
"d": "弹层内容:字符串 / DOM 节点 / 响应式函数,可放按钮等交互元素"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "title",
|
|
||||||
"t": "string",
|
|
||||||
"d": "弹层标题;缺省不渲染标题行"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "placement",
|
|
||||||
"t": "'top' | 'bottom'",
|
|
||||||
"d": "弹层方位;缺省 bottom(触发器下方)",
|
|
||||||
"df": "bottom"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "trigger",
|
|
||||||
"t": "'click' | 'hover'",
|
|
||||||
"d": "触发方式;缺省 click",
|
|
||||||
"df": "click"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "delay",
|
|
||||||
"t": "number",
|
|
||||||
"d": "hover 模式出场延迟毫秒;缺省 150",
|
|
||||||
"df": "150"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "onOpenChange",
|
|
||||||
"t": "(open: boolean) => void",
|
|
||||||
"k": 1,
|
|
||||||
"d": "开合回调"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "triggerEl(第二个参数)",
|
|
||||||
"t": "HTMLElement",
|
|
||||||
"r": 1,
|
|
||||||
"d": "第二个参数:被包裹的触发器元素"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"example": "popover({ title: '筛选条件', content: h('div', null, '任意可交互内容') }, button({ label: '筛选' }))",
|
|
||||||
"steps": [
|
|
||||||
"创建触发器元素,再用 popover(props, trigger) 包一层:返回带弹层的包裹元素",
|
|
||||||
"点击触发器开合;点弹层外部或按 Esc 收起(Esc 后焦点回触发器)",
|
|
||||||
"content 里可以放 button / checkbox 等交互元素(tooltip 只能纯文本),点击弹层内部不会误关",
|
|
||||||
"hover 场景传 trigger: 'hover' + delay 控制多快出场;placement: 'top' 让弹层往上翻",
|
|
||||||
"需要感知开合做副作用(如懒加载数据)时用 onOpenChange"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -991,15 +493,13 @@
|
|||||||
{
|
{
|
||||||
"n": "size",
|
"n": "size",
|
||||||
"t": "number",
|
"t": "number",
|
||||||
"d": "直径像素;缺省 18",
|
"d": "直径像素;缺省 18"
|
||||||
"df": "18"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"example": "spinner(); spinner({ size: 26 })",
|
"example": "spinner(); spinner({ size: 26 })",
|
||||||
"steps": [
|
"steps": [
|
||||||
"数据加载时插入 spinner(),加载完成移除",
|
"数据加载时插入 spinner(),加载完成移除",
|
||||||
"用 size 匹配上下文尺寸(按钮内用小号,整页 loading 用大号)",
|
"用 size 匹配上下文尺寸(按钮内用小号,整页 loading 用大号)"
|
||||||
"与 skeleton 分工:已知内容结构用 skeleton 占位,只有『在跑』的短等待用 spinner"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1010,23 +510,21 @@
|
|||||||
{
|
{
|
||||||
"n": "value",
|
"n": "value",
|
||||||
"t": "Signal<number>",
|
"t": "Signal<number>",
|
||||||
"d": "当前进度 0-100 信号,变化自动平滑过渡(越界钳制)"
|
"r": 1,
|
||||||
|
"d": "进度 0-100 信号,变化自动平滑过渡"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"n": "showText",
|
"n": "showText",
|
||||||
"t": "boolean",
|
"t": "boolean",
|
||||||
"d": "是否显示百分比文字;缺省 true",
|
"d": "是否显示百分比文字;缺省 true"
|
||||||
"df": "true"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"example": "const pct = signal(0); progress({ value: pct }); setInterval(() => pct((pct() + 5) % 105), 400)",
|
"example": "const pct = signal(0); progress({ value: pct }); setInterval(() => pct((pct() + 5) % 105), 400)",
|
||||||
"steps": [
|
"steps": [
|
||||||
"时长未知的加载直接 progress() 不传 value:填充块匀速往复,不显示百分比,读屏读作进行中",
|
|
||||||
"创建数值信号 pct = signal(0),范围 0-100(越界自动钳制)",
|
"创建数值信号 pct = signal(0),范围 0-100(越界自动钳制)",
|
||||||
"传入 value,填充宽度与百分比文字随信号平滑过渡",
|
"传入 value,填充宽度与百分比文字随信号平滑过渡",
|
||||||
"进行中填充带流动条纹,到 100% 自动变 success 色并停止条纹",
|
"进行中填充带流动条纹,到 100% 自动变 success 色并停止条纹",
|
||||||
"不需要百分比文字时传 showText: false",
|
"不需要百分比文字时传 showText: false"
|
||||||
"定量进度用 value 信号:满格自动切 success 色并停止条纹,role=progressbar + aria-valuenow 同步"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1060,45 +558,6 @@
|
|||||||
"键盘可访问:Tab 聚焦激活页签,←→ 在启用页签间环形移动"
|
"键盘可访问:Tab 聚焦激活页签,←→ 在启用页签间环形移动"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "pagination",
|
|
||||||
"title": "分页器 Pagination",
|
|
||||||
"demo": "sec-table",
|
|
||||||
"props": [
|
|
||||||
{
|
|
||||||
"n": "page",
|
|
||||||
"t": "Signal<number>",
|
|
||||||
"r": 1,
|
|
||||||
"d": "当前页码信号,双向绑定:外部赋值自动翻页,点击页码写回信号"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "pageSize",
|
|
||||||
"t": "number",
|
|
||||||
"r": 1,
|
|
||||||
"d": "每页条数"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "total",
|
|
||||||
"t": "number",
|
|
||||||
"r": 1,
|
|
||||||
"d": "总条数"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "onChange",
|
|
||||||
"t": "(page: number) => void",
|
|
||||||
"k": 1,
|
|
||||||
"d": "翻页回调"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"example": "const page = signal(1); pagination({ page, pageSize: 3, total: 24, onChange: (p) => console.log(p) })",
|
|
||||||
"steps": [
|
|
||||||
"创建页码信号 page = signal(1),与 pageSize / total 一起传入",
|
|
||||||
"页码 ≤7 全部展开;更多时只保留首尾与当前页 ±1,缺口折叠为省略号",
|
|
||||||
"首末页自动禁用 ‹ › 翻页按钮,当前页禁点并标记 aria-current",
|
|
||||||
"与 dataTable 联动:把同一个 page 信号传给 dataTable 的 page 参数,两处翻页自动同步",
|
|
||||||
"键盘可达:全部是原生 button,Tab 聚焦后 Enter / Space 触发"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "modal",
|
"id": "modal",
|
||||||
"title": "弹窗 Modal",
|
"title": "弹窗 Modal",
|
||||||
@@ -1116,17 +575,6 @@
|
|||||||
"r": 1,
|
"r": 1,
|
||||||
"d": "标题"
|
"d": "标题"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"n": "size",
|
|
||||||
"t": "'sm' | 'md' | 'lg'",
|
|
||||||
"d": "宽度档位:sm 340px / md 420px / lg 640px;缺省 md",
|
|
||||||
"df": "md"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "footer",
|
|
||||||
"t": "unknown",
|
|
||||||
"d": "底部操作区内容(常见为按钮组,右对齐);缺省不渲染"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"n": "onClose",
|
"n": "onClose",
|
||||||
"t": "() => void",
|
"t": "() => void",
|
||||||
@@ -1144,63 +592,7 @@
|
|||||||
"创建布尔信号 open = signal(false),弹窗初始隐藏",
|
"创建布尔信号 open = signal(false),弹窗初始隐藏",
|
||||||
"把 open 传给 modal,其余参数为弹窗内容子节点",
|
"把 open 传给 modal,其余参数为弹窗内容子节点",
|
||||||
"打开:open(true);关闭:open(false),点遮罩或 × 会自动写回 false",
|
"打开:open(true);关闭:open(false),点遮罩或 × 会自动写回 false",
|
||||||
"入场自带遮罩淡入 + 弹窗上浮动画",
|
"入场自带遮罩淡入 + 弹窗上浮动画"
|
||||||
"底部操作按钮放进 footer(如取消/确定),需要更宽内容用 size: 'lg',窄确认框用 'sm'"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "drawer",
|
|
||||||
"title": "抽屉 Drawer",
|
|
||||||
"demo": "sec-drawer",
|
|
||||||
"props": [
|
|
||||||
{
|
|
||||||
"n": "title",
|
|
||||||
"t": "string",
|
|
||||||
"r": 1,
|
|
||||||
"d": "标题"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "open",
|
|
||||||
"t": "Signal<boolean>",
|
|
||||||
"r": 1,
|
|
||||||
"d": "开关信号:true 滑出 / false 收起"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "placement",
|
|
||||||
"t": "'left' | 'right'",
|
|
||||||
"d": "滑出方向;缺省 right",
|
|
||||||
"df": "right"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "width",
|
|
||||||
"t": "number",
|
|
||||||
"d": "面板宽度像素(窄屏最多 92vw);缺省 320",
|
|
||||||
"df": "320"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "footer",
|
|
||||||
"t": "unknown",
|
|
||||||
"d": "底部操作区内容;缺省不渲染"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "onClose",
|
|
||||||
"t": "() => void",
|
|
||||||
"k": 1,
|
|
||||||
"d": "关闭回调(点遮罩 / × / Esc 触发)"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "...children",
|
|
||||||
"t": "unknown[]",
|
|
||||||
"d": "面板内容子节点"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"example": "const open = signal(false); drawer({ title: '详情', open, width: 360, onClose: () => open(false) }, h('p', null, '内容'))",
|
|
||||||
"steps": [
|
|
||||||
"创建布尔信号 open = signal(false),传给 drawer;其余参数为面板内容子节点",
|
|
||||||
"打开后从侧边滑出:placement: 'right'(默认)或 'left',width 控制宽度(窄屏自动收到 92vw)",
|
|
||||||
"点遮罩、× 或按 Esc 关闭并写回 false;打开时焦点落在面板首个可交互元素",
|
|
||||||
"底部操作按钮放进 footer;与 modal 的分工:modal 是强打断的确认,drawer 保留主视图上下文做详情/编辑",
|
|
||||||
"面板内容与 modal 一样接受任意 DOM 与响应式函数"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1211,8 +603,7 @@
|
|||||||
{
|
{
|
||||||
"n": "layout",
|
"n": "layout",
|
||||||
"t": "'vertical' | 'horizontal'",
|
"t": "'vertical' | 'horizontal'",
|
||||||
"d": "布局方向;缺省 vertical(horizontal 为标签左置)",
|
"d": "布局方向;缺省 vertical(horizontal 为标签左置)"
|
||||||
"df": "vertical"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"n": "submitText",
|
"n": "submitText",
|
||||||
@@ -1255,8 +646,7 @@
|
|||||||
{
|
{
|
||||||
"n": "(formItem)validateOn",
|
"n": "(formItem)validateOn",
|
||||||
"t": "'input' | 'blur' | 'submit'",
|
"t": "'input' | 'blur' | 'submit'",
|
||||||
"d": "校验时机;缺省 blur(输入时自动清已报错误)",
|
"d": "校验时机;缺省 blur(输入时自动清已报错误)"
|
||||||
"df": "blur"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"example": "const name = signal(''); const f = form({ onSubmit: submit }, [formItem({ label: '用户名', required: true, value: name, rules: [{ min: 2, max: 12 }] }, textField({ value: name }))])",
|
"example": "const name = signal(''); const f = form({ onSubmit: submit }, [formItem({ label: '用户名', required: true, value: name, rules: [{ min: 2, max: 12 }] }, textField({ value: name }))])",
|
||||||
@@ -1266,9 +656,7 @@
|
|||||||
"把句柄数组交给 form:form({ onSubmit }, items),form 渲染全部字段 + 提交/重置按钮",
|
"把句柄数组交给 form:form({ onSubmit }, items),form 渲染全部字段 + 提交/重置按钮",
|
||||||
"提交时逐项校验:全过触发 onSubmit;有错则标红控件、显示错误文案并聚焦首个出错项",
|
"提交时逐项校验:全过触发 onSubmit;有错则标红控件、显示错误文案并聚焦首个出错项",
|
||||||
"规则语义:required 非空(trim)、min/max 长度、pattern 正则、validator 自定义(true 通过 / 字符串为错误)",
|
"规则语义:required 非空(trim)、min/max 长度、pattern 正则、validator 自定义(true 通过 / 字符串为错误)",
|
||||||
"非必填字段为空时跳过其余规则;Enter 键触发表单原生 submit(已拦截走校验流程)",
|
"非必填字段为空时跳过其余规则;Enter 键触发表单原生 submit(已拦截走校验流程)"
|
||||||
"错误态覆盖包裹式控件:标红作用于控件根与内部原生控件(含自绘 select 的触发器),并同步 aria-invalid,读屏与视觉一致",
|
|
||||||
"触发时机用 input / change / focusout(均会冒泡),因此 checkbox / select / 单选组这类包裹式控件同样能失焦校验"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1286,7 +674,7 @@
|
|||||||
"n": "columns",
|
"n": "columns",
|
||||||
"t": "ColumnDef<T>[]",
|
"t": "ColumnDef<T>[]",
|
||||||
"r": 1,
|
"r": 1,
|
||||||
"d": "列定义:内核字段 { key, header?, sortable?, width? } + 皮肤字段 { align?, format? };width 数字为固定像素,'flex' 自适应"
|
"d": "列定义:{ key, header?, sortable? }"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"n": "filterKey",
|
"n": "filterKey",
|
||||||
@@ -1296,97 +684,28 @@
|
|||||||
{
|
{
|
||||||
"n": "pageSize",
|
"n": "pageSize",
|
||||||
"t": "number",
|
"t": "number",
|
||||||
"d": "每页行数;缺省 5",
|
"d": "每页行数;缺省 5"
|
||||||
"df": "5"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"n": "selectable",
|
"n": "selectable",
|
||||||
"t": "boolean",
|
"t": "boolean",
|
||||||
"d": "启用行选择列:全选三态(含半选)、行 checkbox、已选计数;缺省 false",
|
"d": "启用行选择列:全选三态(含半选)、行 checkbox、已选计数;缺省 false"
|
||||||
"df": "false"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "page",
|
|
||||||
"t": "Signal<number>",
|
|
||||||
"d": "受控页码信号:与内核分页双向同步,传同一个信号给独立 pagination 组件即可联动"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "loading",
|
|
||||||
"t": "boolean | Signal<boolean>",
|
|
||||||
"d": "加载态:为真时数据行换成骨架行(行数随 pageSize),筛选输入与翻页按钮临时禁用,容器带 aria-busy"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "selected",
|
|
||||||
"t": "Signal<RowKey[]>",
|
|
||||||
"d": "受控选中行信号:与内核选择双向同步(内容比较,不来回派发),供批量操作读取/改写"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"example": "dataTable({ data: users, selectable: true, columns: [{ key: 'name', header: '姓名', sortable: true }], pageSize: 5 })",
|
"example": "dataTable({ data: users, selectable: true, columns: [{ key: 'name', header: '姓名', sortable: true }], pageSize: 5 })",
|
||||||
"steps": [
|
"steps": [
|
||||||
"准备行数据,确保每行有唯一 id(跨页选择与刷新不丢行靠它)",
|
"准备行数据,确保每行有唯一 id(跨页选择与刷新不丢行靠它)",
|
||||||
"定义 columns:sortable: true 的列表头可点击排序(升 → 降 → 取消循环),标记箭头 ↑↓↕",
|
"定义 columns:sortable: true 的列表头可点击排序(升 → 降 → 取消循环),标记箭头 ↑↓↕",
|
||||||
"列展示细节用皮肤字段:align 控制表头与单元格对齐,format(value, row) 返回纯文本(走 textContent,不需要转义)",
|
|
||||||
"列宽用内核字段 width:数字(像素)固定该列,'flex' 或不设则按内容自适应",
|
|
||||||
"selectable: true 开启行选择:表头全选框带 indeterminate 半选态,工具栏显示已选计数",
|
"selectable: true 开启行选择:表头全选框带 indeterminate 半选态,工具栏显示已选计数",
|
||||||
"选中行用低视觉比重样式(淡色底 + 弱化文字),选择状态存 rowKey,跨页不丢失",
|
"选中行用低视觉比重样式(淡色底 + 弱化文字),选择状态存 rowKey,跨页不丢失",
|
||||||
"工具栏输入框即筛选,输入即时过滤并回第 1 页;无匹配时显示空状态行",
|
"工具栏输入框即筛选,输入即时过滤并回第 1 页;无匹配时显示空状态行",
|
||||||
"表头粘性固定(滚动容器 max-height 420px),斑马纹 + 悬停高亮由 CSS 提供",
|
"表头粘性固定(滚动容器 max-height 420px),斑马纹 + 悬停高亮由 CSS 提供"
|
||||||
"数据在途时传 loading(布尔或信号):表格体换成骨架行、筛选与翻页禁用,返回后置回 false 即恢复",
|
|
||||||
"需要独立分页器联动时传 page 信号:const page = signal(1),dataTable({ page }) 与 pagination({ page }) 共用",
|
|
||||||
"需要批量操作时传 selected 信号:内部勾选实时写回,外部置空或赋值即同步整表(跨页保留,按 rowKey 存)"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dir": "packages/skin/ui"
|
"dir": "packages/skin/ui"
|
||||||
},
|
|
||||||
{
|
|
||||||
"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 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|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": []
|
|
||||||
},
|
|
||||||
"version": "0.1.0",
|
|
||||||
"dir": "packages/tools/cli"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
{"time":"2026-10-23T15:47:00+08:00","id":"b01","result":"done","summary":"新增 tooltip 悬浮提示组件(包裹式 API / 150ms 延迟 / Esc 收起 / reduced-motion 兜底),同步 manifest + llms-full.txt + demo 区块,并修复 ua style 对 URL 中 // 的误判;npm run check 全绿"}
|
|
||||||
{"time":"2026-10-23T15:50:03+08:00","id":"b02","result":"done","summary":"新增 toast 消息提示组件:四语义工厂 + showToast,右上角堆叠,默认 3s 自动消失,微动画 + reduced-motion 兜底;manifest/docs/llms 同步,check 全绿"}
|
|
||||||
{"time":"2026-10-23T15:53:33+08:00","id":"b03","result":"done","summary":"新增 skeleton 骨架屏组件:text/rect/circle 三形状 + 扫光微动画 + reduced-motion 静态化;manifest/docs/llms/demo 同步,check 全绿"}
|
|
||||||
{"time":"2026-10-23T16:00:11+08:00","id":"b04","result":"done","summary":"新增独立 pagination 分页器(page 信号双向绑定 + 省略号折叠 + 键盘可达),dataTable 新增 page 受控桥接实现联动;demo 联动卡片,check 全绿"}
|
|
||||||
{"time":"2026-10-23T16:03:46+08:00","id":"b05","result":"done","summary":"文档站核查确认已 manifest 驱动;补齐默认值信息量缺口:CLI propsTable 新增默认值列(df 字段)+ llms-full 默认标注,16 处默认值提取;check 全绿"}
|
|
||||||
{"time":"2026-10-23T16:08:17+08:00","id":"c01","result":"done","summary":"button/checkbox/switchButton 补 disabled(boolean|Signal),复用 h() 布尔/信号既有语义零辅助代码;CSS 通用禁用态 + demo 信号联动示例;check 全绿"}
|
|
||||||
{"time":"2026-10-23T16:10:31+08:00","id":"c02","result":"done","summary":"textField/textarea 补 disabled(boolean|Signal),与 c01 同模式;禁用态 CSS 覆盖输入类;demo locked 信号联动;check 全绿"}
|
|
||||||
{"time": "2026-10-23T16:55:00+08:00", "id": "d01", "result": "done", "summary": "新增下拉菜单 menu:触发器 + 自绘弹层,disabled/danger/divider 条目,↑↓ 环形导航 + Esc 回焦,reduced-motion 兜底;manifest/llms-full/demo 同步,check 全绿"}
|
|
||||||
{"time": "2026-10-23T17:02:00+08:00", "id": "d02", "result": "done", "summary": "select/radioGroup 补 disabled(布尔|信号),并修复 select 根节点缺 ua-open 导致箭头不旋转的既有缺陷;CSS :has() 行级弱化;demo/manifest/llms 同步,check 全绿"}
|
|
||||||
{"time": "2026-10-23T16:47:56+08:00", "id": "d03", "result": "done", "summary": "新增折叠面板 accordion:单开/多开 + active 受控 + 禁用面板,grid 行高 0fr→1fr 过渡免测量,aria 关联完整;manifest/llms/demo 同步,check 全绿"}
|
|
||||||
{"time": "2026-10-23T16:48:36+08:00", "id": "d04", "result": "done", "summary": "dataTable 新增 loading(布尔|信号):表体换骨架行、筛选与翻页禁用、aria-busy;demo 加开关,manifest/llms 同步,check 全绿"}
|
|
||||||
{"time": "2026-10-23T16:49:45+08:00", "id": "d05", "result": "done", "summary": "新增面包屑 breadcrumb:nav/ol 语义结构 + 末项 aria-current,href 走 <a>、无 href 走 onNavigate,分隔符可配;manifest/llms/demo 同步,check 全绿(中途被 style 门禁拦下多行注释,已改单行)"}
|
|
||||||
{"time": "2026-10-23T16:50:49+08:00", "id": "d06", "result": "done", "summary": "新增 ua verify 门禁:清单 id/exports/props/steps/example 与 demo 锚点一致性,纳入 npm run check;顺带补齐 badge/alert/spinner 手把手第三条,check 全绿"}
|
|
||||||
{"time": "2026-10-23T16:51:48+08:00", "id": "d07", "result": "done", "summary": "新增可交互气泡卡片 popover:点击/hover 触发、点外部与 Esc 收起、内容可交互且内部点击不误关;manifest/llms/demo 同步,check 全绿"}
|
|
||||||
{"time": "2026-10-23T16:52:28+08:00", "id": "d08", "result": "done", "summary": "演示页改双栏 shell:sticky 侧边目录由 .sec 自动反推 + IntersectionObserver 滚动高亮,窄屏隐藏、reduced-motion 关平滑滚动;check 全绿"}
|
|
||||||
{"time": "2026-10-23T16:53:06+08:00", "id": "d09", "result": "done", "summary": "新增滑块 slider:value 信号双向绑定 + min/max/step + 布尔|信号禁用 + showValue,原生 range 承载键盘与触屏,accent-color 令牌化;manifest/llms/demo 同步,check 全绿"}
|
|
||||||
{"time": "2026-10-23T16:53:44+08:00", "id": "d10", "result": "done", "summary": "toast 新增 closable(× 手动关闭)与同屏上限 4 条,退场统一走 dismiss() 双兜底;is-closable 才接收指针;demo/manifest/llms 同步,check 全绿"}
|
|
||||||
{"time": "2026-10-23T16:54:36+08:00", "id": "d11", "result": "done", "summary": "modal 新增 size 三档(sm/md/lg)与 footer 操作区(右对齐 + 上分割线),缺省行为不变;demo/manifest/llms 同步,check 全绿"}
|
|
||||||
{"time": "2026-10-23T16:55:10+08:00", "id": "d12", "result": "done", "summary": "新增可关闭标签 tag(五档语义色 + closable/onClose,无状态委托调用方重渲染);demo 用 renderList 演示真实删除;manifest/llms 同步,check 全绿"}
|
|
||||||
{"time": "2026-10-23T16:55:55+08:00", "id": "d13", "result": "done", "summary": "dataTable 列定义新增皮肤层 TableColumn(align + format),内核 ColumnDef 不变;表头/单元格共用对齐类,format 走 textContent;demo/manifest/llms 同步,check 全绿"}
|
|
||||||
{"time": "2026-10-23T16:56:36+08:00", "id": "d14", "result": "done", "summary": "新增抽屉 drawer:open 信号驱动侧滑、placement/width 可配、footer 操作区、遮罩/×/Esc 关闭并聚焦首元素;demo/manifest/llms 同步,check 全绿"}
|
|
||||||
{"time": "2026-10-23T16:57:22+08:00", "id": "d15", "result": "done", "summary": "CLI 包补 ua.manifest.json(type: tooling,七个子命令 + 约定),注册表 5 包;修正 AGENTS/ARCHITECTURE 过期组件数 17→27;check 全绿"}
|
|
||||||
{"time": "2026-10-23T16:58:51+08:00", "id": "d16", "result": "done", "summary": "dataTable 消费内核列宽 width:colgroup 固定像素 / flex 自适应,选择列 44px;demo 三列演示宽档;manifest/llms 同步,check 全绿"}
|
|
||||||
{"time": "2026-10-23T16:59:46+08:00", "id": "d17", "result": "done", "summary": "表单错误态打通包裹式控件:focusout/change 取代 blur + aria-invalid + .ua-invalid 后代选择器覆盖 select/radio/slider;demo 加必填 select;check 全绿"}
|
|
||||||
{"time": "2026-10-23T17:00:55+08:00", "id": "d18", "result": "done", "summary": "ua verify 新增皮肤硬编码颜色门禁(#hex 禁止,rgba 仅限阴影/遮罩),清掉 ui.css 5 处 #fff 改令牌;负向测试确认能拦;check 全绿"}
|
|
||||||
{"time": "2026-10-23T17:01:46+08:00", "id": "d19", "result": "done", "summary": "dataTable 新增 selected 受控信号:与内核选择双向同步(内容比较 + 拷贝保护),demo 展示已选计数与清空;check 全绿"}
|
|
||||||
{"time": "2026-10-23T17:02:24+08:00", "id": "d20", "result": "done", "summary": "progress 支持不确定态:value 可选,缺省时 35% 填充块往复 + aria-label 进行中;定量态补 aria-valuenow;demo/manifest/llms 同步,check 全绿"}
|
|
||||||
{"time": "2026-10-23T17:03:02+08:00", "id": "d21", "result": "done", "summary": "无障碍收尾:modal × 补 aria-label/type、spinner role=status、skeleton aria-hidden、tablist aria-label + 禁用页签 aria-disabled;conventions 记录基线;check 全绿"}
|
|
||||||
{"time": "2026-10-23T17:03:49+08:00", "id": "d22", "result": "done", "summary": "新增头像 avatar(src 图片 / 首字占位 + size + 四档语义底色),demo 独立 sec-avatar;中途被 verify 的 demo 锚点检查拦下已补;check 全绿"}
|
|
||||||
{"time": "2026-10-23T17:04:27+08:00", "id": "d23", "result": "done", "summary": "alert 新增 title / closable / onClose 与 role=alert,关闭走 200ms 退场后摘除;demo 独立 sec-alert;manifest/llms 同步,check 全绿"}
|
|
||||||
{"time": "2026-10-23T17:05:46+08:00", "id": "d24", "result": "done", "summary": "重写 README(定位/快速开始/28 组件/目录/门禁/Agent 接入/流水线),组件清单做成标记区块并由 ua verify 校验防漂移;负向测试通过,check 全绿"}
|
|
||||||
{"time": "2026-10-23T17:06:17+08:00", "id": "d25", "result": "done", "summary": "menu 条目新增 shortcut 快捷键提示(右侧 kbd 灰字,两端对齐 + 单行省略,不绑按键);demo/manifest/llms 同步,check 全绿"}
|
|
||||||
{"ts": "2026-10-23T09:09:16+00:00", "round": 26, "id": "d26", "result": "done", "commit": "82f48aa", "summary": "select 键盘导航:↑↓ 环形移动、Home/End 跳转、Esc 回焦触发器"}
|
|
||||||
{"ts": "2026-10-23T09:11:42+00:00", "round": 27, "id": "d27", "result": "done", "commit": "fac74a3", "summary": "modal 焦点陷阱 + dialog 语义"}
|
|
||||||
{"ts": "2026-10-23T09:11:42+00:00", "round": 28, "id": "d28", "result": "done", "commit": "", "summary": "drawer 焦点陷阱对齐 modal"}
|
|
||||||
{"ts": "2026-10-23T09:12:49+00:00", "round": 29, "id": "d29", "result": "done", "commit": "", "summary": "menu Home/End 跳转对齐 select"}
|
|
||||||
@@ -21,13 +21,13 @@
|
|||||||
|
|
||||||
- `packages/core/table-core`(@ui-arch/table-core):Headless 表格逻辑内核
|
- `packages/core/table-core`(@ui-arch/table-core):Headless 表格逻辑内核
|
||||||
- `packages/core/frame`(@ui-arch/frame):signal + hyperscript 微框架
|
- `packages/core/frame`(@ui-arch/frame):signal + hyperscript 微框架
|
||||||
- `packages/skin/ui`(@ui-arch/ui):27 个 DOM 组件(表单 / 反馈 / 浮层 / 导航 / 表格),样式只消费令牌
|
- `packages/skin/ui`(@ui-arch/ui):16 个 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` = build + style + typecheck + smoke + index + verify,提交前必过。`ua verify` 校验清单 ↔ 文档 ↔ 演示一致(docs.components 的 id 必须在 api.exports 有对应条目、props 必须带 n/t/d、demo 锚点必须在 `apps/demo/main.ts` 中存在),检查皮肤包样式表无硬编码颜色(`#hex` 一律禁止,必须走 `--ua-*` 令牌),并核对 README 的 `<!-- ua:components -->` 区块与清单组件集合完全一致。
|
**门禁一条龙**:`npm run check` = style + typecheck + smoke + index,提交前必过。
|
||||||
|
|
||||||
## Agent 查询组件库的三种方式
|
## Agent 查询组件库的三种方式
|
||||||
|
|
||||||
@@ -46,7 +46,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 | docs | style | verify`
|
2. **CLI**:`node packages/tools/cli/bin/ua.js list | info <name> | index`
|
||||||
|
|
||||||
3. **静态索引**:`.ua/index.json`(机器可读索引)、`llms.txt`(速览)或 **`llms-full.txt`(全量 Agent 文档:逐组件参数 / 回调 / 手把手步骤 / 令牌,无图形纯文本,喂给 Agent 首选)**。
|
3. **静态索引**:`.ua/index.json`(机器可读索引)、`llms.txt`(速览)或 **`llms-full.txt`(全量 Agent 文档:逐组件参数 / 回调 / 手把手步骤 / 令牌,无图形纯文本,喂给 Agent 首选)**。
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -34,7 +34,7 @@ ui-arch-mvp/
|
|||||||
│ │ ├── table-core/ # 表格状态内核
|
│ │ ├── table-core/ # 表格状态内核
|
||||||
│ │ └── frame/ # 信号 + hyperscript 微框架
|
│ │ └── frame/ # 信号 + hyperscript 微框架
|
||||||
│ ├── skin/ # 皮肤层(DOM 组件,可发布)
|
│ ├── skin/ # 皮肤层(DOM 组件,可发布)
|
||||||
│ │ └── ui/ # 27 个组件 + ui.css
|
│ │ └── ui/ # 15 个组件 + ui.css
|
||||||
│ ├── design/ # 资产层(可发布)
|
│ ├── design/ # 资产层(可发布)
|
||||||
│ │ └── tokens/ # --ua-* 设计令牌
|
│ │ └── tokens/ # --ua-* 设计令牌
|
||||||
│ └── tools/ # 工程层(private,不发布)
|
│ └── tools/ # 工程层(private,不发布)
|
||||||
|
|||||||
+4
-11
@@ -1,16 +1,7 @@
|
|||||||
## 无人值守自我迭代协议(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 分钟视为循环中断(崩溃/超时),调度器删旧锁、重新点火。
|
|
||||||
|
|
||||||
## 分支流水线
|
## 分支流水线
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -25,13 +16,15 @@ 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)。
|
||||||
- **通过**:提交到 `qa/probe`(commit message 以 `iter:` 开头,注明 backlog 条目 id),推送 `qa/probe`,fast-forward 合并到 `qa/stable` 并推送;将 backlog 条目 `status` 置为 `done` 并随迭代提交。
|
- **通过**:提交到 `qa/probe`(commit message 以 `iter:` 开头,注明 backlog 条目 id),推送 `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,**字段统一**:`{"ts":"本地ISO时间","id":"条目id","result":"done|aborted","commit":"短哈希","summary":"一句话"}`。硬性规范:`ts` 必须来自 `date` 命令实测(本地时间,带 +08:00 偏移,**禁止手写或用 UTC 换算**);时长结论必须来自两次时间查询之差,禁止使用「大约/接近」等估算词;并发实例格式不得自创变体。随本轮提交。
|
5. **留痕**:在 `.ua/iterations.log` 追加一行 JSON:`{时间, 条目id, 结果, 摘要}`,随本轮提交。
|
||||||
6. **边界**:不改 `main` 分支保护、不删分支、不动 `.workbuddy/`、不重写历史、不 force push 到 `qa/stable` 与 `main`。
|
6. **边界**:不改 `main` 分支保护、不删分支、不动 `.workbuddy/`、不重写历史、不 force push 到 `qa/stable` 与 `main`。
|
||||||
|
|
||||||
## 选题规则(自主补点子时)
|
## 选题规则(自主补点子时)
|
||||||
|
|||||||
@@ -1,54 +1,2 @@
|
|||||||
# @ui-arch · 私人组件库
|
# UI
|
||||||
|
|
||||||
自研、零依赖、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
|
|
||||||
```
|
|
||||||
|
|
||||||
## 组件(共 28 个)
|
|
||||||
|
|
||||||
<!-- ua:components -->
|
|
||||||
accordion · alert · avatar · badge · breadcrumb · button · card · checkbox · dataTable · drawer · form · menu · modal · pagination · popover · progress · radioGroup · select · skeleton · slider · spinner · switchButton · tabs · tag · textField · textarea · toast · tooltip
|
|
||||||
<!-- /ua:components -->
|
|
||||||
|
|
||||||
分组:表单(button / textField / textarea / checkbox / switchButton / select / radioGroup / slider / form / formItem)、反馈(alert / toast / spinner / progress / skeleton / badge)、浮层(tooltip / popover / modal / drawer / menu)、导航(tabs / breadcrumb / pagination / accordion)、数据(dataTable / tag / avatar / card)。
|
|
||||||
|
|
||||||
## 目录
|
|
||||||
|
|
||||||
| 路径 | 说明 |
|
|
||||||
|---|---|
|
|
||||||
| `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:28 个 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` 仅由人审后合入。
|
|
||||||
|
|||||||
+1
-10
@@ -45,16 +45,7 @@ ${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}
|
||||||
.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:880px;margin:0 auto;padding:36px 20px 80px}
|
||||||
.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}
|
||||||
|
|||||||
+10
-302
@@ -1,58 +1,23 @@
|
|||||||
import { h, mount, renderList, signal } from '@ui-arch/frame';
|
import { h, mount, signal } from '@ui-arch/frame';
|
||||||
import {
|
import {
|
||||||
accordion,
|
|
||||||
alert,
|
alert,
|
||||||
avatar,
|
|
||||||
badge,
|
badge,
|
||||||
breadcrumb,
|
|
||||||
button,
|
button,
|
||||||
card,
|
card,
|
||||||
checkbox,
|
checkbox,
|
||||||
dataTable,
|
dataTable,
|
||||||
drawer,
|
|
||||||
form,
|
form,
|
||||||
formItem,
|
formItem,
|
||||||
menu,
|
|
||||||
modal,
|
modal,
|
||||||
pagination,
|
|
||||||
popover,
|
|
||||||
progress,
|
progress,
|
||||||
radioGroup,
|
radioGroup,
|
||||||
select,
|
select,
|
||||||
showToast,
|
|
||||||
skeleton,
|
|
||||||
slider,
|
|
||||||
spinner,
|
spinner,
|
||||||
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 {
|
||||||
@@ -99,31 +64,7 @@ const username = signal('');
|
|||||||
const email = signal('');
|
const email = signal('');
|
||||||
const formResult = signal('');
|
const formResult = signal('');
|
||||||
|
|
||||||
// —— 分页联动状态:dataTable 与独立 pagination 共用同一页码信号 ——
|
const app = h(
|
||||||
const tablePage = signal(1);
|
|
||||||
|
|
||||||
// —— 抽屉开关 ——
|
|
||||||
const drawerOpen = signal(false);
|
|
||||||
|
|
||||||
// —— 标签列表:关闭即写回信号,容器按信号重渲染 ——
|
|
||||||
const tags = signal(['设计', '前端', '组件库']);
|
|
||||||
|
|
||||||
// —— 滑块:音量与禁用态演示 ——
|
|
||||||
const volume = signal(40);
|
|
||||||
|
|
||||||
// —— 受控选中: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(
|
||||||
@@ -157,80 +98,6 @@ const content = 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(
|
section(
|
||||||
'sec-feedback',
|
'sec-feedback',
|
||||||
@@ -244,61 +111,16 @@ const content = 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()} / ${city()}`) },
|
{ submitText: '注册', onSubmit: () => formResult(`提交成功:${username()} / ${email()}`) },
|
||||||
[
|
[
|
||||||
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() || '等待提交…'),
|
||||||
@@ -311,99 +133,11 @@ const content = 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) },
|
||||||
title: '确认操作',
|
h('p', null, '这是一个由信号驱动的弹窗组件。'),
|
||||||
open: modalOpen,
|
h('div', { class: 'row' }, button({ label: '知道了', onClick: () => modalOpen(false) }), badge({ label: 'modal', variant: 'success' })),
|
||||||
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-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',
|
||||||
'数据表格',
|
'数据表格',
|
||||||
@@ -411,35 +145,13 @@ const content = h(
|
|||||||
card({ title: '用户表' }, dataTable({
|
card({ title: '用户表' }, dataTable({
|
||||||
data: users,
|
data: users,
|
||||||
selectable: true,
|
selectable: true,
|
||||||
loading: tableLoading,
|
|
||||||
columns: [
|
columns: [
|
||||||
{ key: 'name', header: '姓名', sortable: true, width: 160 },
|
{ key: 'name', header: '姓名', sortable: true },
|
||||||
{ key: 'age', header: '年龄', sortable: true, align: 'right', width: 100, format: (v) => `${v} 岁` },
|
{ key: 'age', header: '年龄', sortable: true },
|
||||||
{ key: 'city', header: '城市', align: 'center', width: 'flex' },
|
{ key: 'city', header: '城市' },
|
||||||
],
|
],
|
||||||
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',
|
||||||
@@ -449,8 +161,4 @@ const content = 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);
|
|
||||||
|
|||||||
+33
-419
@@ -1,6 +1,6 @@
|
|||||||
# @ui-arch 组件库 Agent 文档(无图形版)
|
# @ui-arch 组件库 Agent 文档(无图形版)
|
||||||
|
|
||||||
> 自动生成于 2026-10-23T09:06:10.925Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。
|
> 自动生成于 2026-10-23T06:57:51.085Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。
|
||||||
> 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。
|
> 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。
|
||||||
|
|
||||||
## 快速上手
|
## 快速上手
|
||||||
@@ -38,25 +38,16 @@
|
|||||||
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
|
||||||
|
|
||||||
28 个 DOM 组件,统一用法:组件是普通函数,传入 props 返回 DOM 节点;需要状态时先创建 signal 再双向绑定。引入 CSS:import '@ui-arch/ui/ui.css'(内部已消费 tokens)。
|
16 个 DOM 组件,统一用法:组件是普通函数,传入 props 返回 DOM 节点;需要状态时先创建 signal 再双向绑定。引入 CSS:import '@ui-arch/ui/ui.css'(内部已消费 tokens)。
|
||||||
|
|
||||||
### button — 按钮 Button
|
### button — 按钮 Button
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
|
|
||||||
- label(必填):string —— 按钮文案
|
- label(必填):string —— 按钮文案
|
||||||
- variant(默认 primary):'primary' | 'ghost' | 'danger' —— 视觉强度;缺省 primary
|
- variant:'primary' | 'ghost' | 'danger' —— 视觉强度;缺省 primary
|
||||||
- disabled:boolean | Signal<boolean> —— 禁用态:布尔直接置位,传信号则动态同步
|
|
||||||
- onClick(回调):() => void —— 点击回调
|
- onClick(回调):() => void —— 点击回调
|
||||||
|
|
||||||
示例:
|
示例:
|
||||||
@@ -79,7 +70,6 @@ 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 —— 输入回调,每次按键触发
|
||||||
|
|
||||||
示例:
|
示例:
|
||||||
@@ -101,9 +91,8 @@ const name = signal(''); const el = textField({ value: name, placeholder: '输
|
|||||||
参数:
|
参数:
|
||||||
|
|
||||||
- value:Signal<string> —— 双向绑定信号
|
- value:Signal<string> —— 双向绑定信号
|
||||||
- rows(默认 3):number —— 初始行数;缺省 3
|
- rows:number —— 初始行数;缺省 3
|
||||||
- placeholder:string —— 占位文案
|
- placeholder:string —— 占位文案
|
||||||
- disabled:boolean | Signal<boolean> —— 禁用态:布尔直接置位,传信号则动态同步
|
|
||||||
- onInput(回调):(value: string) => void —— 输入回调
|
- onInput(回调):(value: string) => void —— 输入回调
|
||||||
|
|
||||||
示例:
|
示例:
|
||||||
@@ -120,41 +109,12 @@ const bio = signal(''); textarea({ value: bio, rows: 4, placeholder: '一句话
|
|||||||
|
|
||||||
现场演示: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 —— 变更回调
|
||||||
|
|
||||||
示例:
|
示例:
|
||||||
@@ -177,7 +137,6 @@ 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 —— 变更回调
|
||||||
|
|
||||||
示例:
|
示例:
|
||||||
@@ -201,7 +160,6 @@ 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 —— 选中回调
|
||||||
|
|
||||||
示例:
|
示例:
|
||||||
@@ -219,40 +177,12 @@ 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 —— 变更回调
|
||||||
|
|
||||||
示例:
|
示例:
|
||||||
@@ -266,58 +196,9 @@ 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
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
@@ -344,7 +225,7 @@ card({ title: '用户表' }, dataTable({ data, columns }))
|
|||||||
参数:
|
参数:
|
||||||
|
|
||||||
- label(必填):string —— 徽标文案
|
- label(必填):string —— 徽标文案
|
||||||
- variant(默认 default):'default' | 'success' | 'warning' | 'danger' —— 语义色;缺省 default
|
- variant:'default' | 'success' | 'warning' | 'danger' —— 语义色;缺省 default
|
||||||
|
|
||||||
示例:
|
示例:
|
||||||
|
|
||||||
@@ -356,68 +237,15 @@ 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 —— 提示内容
|
||||||
- title:string —— 加粗标题行;缺省只渲染正文
|
- variant:'info' | 'success' | 'warning' | 'danger' —— 语义色;缺省 info
|
||||||
- variant(默认 info):'info' | 'success' | 'warning' | 'danger' —— 语义色;缺省 info
|
|
||||||
- closable(默认 false):boolean —— 显示 × 关闭按钮,退场后从 DOM 移除;缺省 false
|
|
||||||
- onClose(回调):() => void —— 关闭回调,供调用方同步业务状态
|
|
||||||
|
|
||||||
示例:
|
示例:
|
||||||
|
|
||||||
@@ -429,118 +257,14 @@ 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(默认 18):number —— 直径像素;缺省 18
|
- size:number —— 直径像素;缺省 18
|
||||||
|
|
||||||
示例:
|
示例:
|
||||||
|
|
||||||
@@ -552,7 +276,6 @@ 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
|
||||||
|
|
||||||
@@ -560,8 +283,8 @@ spinner(); spinner({ size: 26 })
|
|||||||
|
|
||||||
参数:
|
参数:
|
||||||
|
|
||||||
- value:Signal<number> —— 当前进度 0-100 信号,变化自动平滑过渡(越界钳制)
|
- value(必填):Signal<number> —— 进度 0-100 信号,变化自动平滑过渡
|
||||||
- showText(默认 true):boolean —— 是否显示百分比文字;缺省 true
|
- showText:boolean —— 是否显示百分比文字;缺省 true
|
||||||
|
|
||||||
示例:
|
示例:
|
||||||
|
|
||||||
@@ -571,12 +294,10 @@ const pct = signal(0); progress({ value: pct }); setInterval(() => pct((pct() +
|
|||||||
|
|
||||||
手把手:
|
手把手:
|
||||||
|
|
||||||
1. 时长未知的加载直接 progress() 不传 value:填充块匀速往复,不显示百分比,读屏读作进行中
|
1. 创建数值信号 pct = signal(0),范围 0-100(越界自动钳制)
|
||||||
2. 创建数值信号 pct = signal(0),范围 0-100(越界自动钳制)
|
2. 传入 value,填充宽度与百分比文字随信号平滑过渡
|
||||||
3. 传入 value,填充宽度与百分比文字随信号平滑过渡
|
3. 进行中填充带流动条纹,到 100% 自动变 success 色并停止条纹
|
||||||
4. 进行中填充带流动条纹,到 100% 自动变 success 色并停止条纹
|
4. 不需要百分比文字时传 showText: false
|
||||||
5. 不需要百分比文字时传 showText: false
|
|
||||||
6. 定量进度用 value 信号:满格自动切 success 色并停止条纹,role=progressbar + aria-valuenow 同步
|
|
||||||
|
|
||||||
现场演示:apps/docs/demo.html#sec-feedback
|
现场演示:apps/docs/demo.html#sec-feedback
|
||||||
|
|
||||||
@@ -603,39 +324,12 @@ const tab = signal('a'); tabs({ tabs: [{ value: 'a', label: '概览' }, { value:
|
|||||||
|
|
||||||
现场演示:apps/docs/demo.html#sec-tabs-modal
|
现场演示:apps/docs/demo.html#sec-tabs-modal
|
||||||
|
|
||||||
### 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[] —— 弹窗内容子节点
|
||||||
|
|
||||||
@@ -651,43 +345,14 @@ 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):'vertical' | 'horizontal' —— 布局方向;缺省 vertical(horizontal 为标签左置)
|
- layout:'vertical' | 'horizontal' —— 布局方向;缺省 vertical(horizontal 为标签左置)
|
||||||
- submitText:string —— 提交按钮文案;缺省「提交」
|
- submitText:string —— 提交按钮文案;缺省「提交」
|
||||||
- resetText:string —— 重置按钮文案;缺省不渲染
|
- resetText:string —— 重置按钮文案;缺省不渲染
|
||||||
- onSubmit(回调):() => void —— 全部字段校验通过后的提交回调
|
- onSubmit(回调):() => void —— 全部字段校验通过后的提交回调
|
||||||
@@ -695,7 +360,7 @@ const open = signal(false); drawer({ title: '详情', open, width: 360, 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(默认 blur):'input' | 'blur' | 'submit' —— 校验时机;缺省 blur(输入时自动清已报错误)
|
- (formItem)validateOn:'input' | 'blur' | 'submit' —— 校验时机;缺省 blur(输入时自动清已报错误)
|
||||||
|
|
||||||
示例:
|
示例:
|
||||||
|
|
||||||
@@ -711,8 +376,6 @@ 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
|
||||||
|
|
||||||
@@ -721,13 +384,10 @@ const name = signal(''); const f = form({ onSubmit: submit }, [formItem({ label:
|
|||||||
参数:
|
参数:
|
||||||
|
|
||||||
- data(必填):T[] —— 全量行数据,每行须含 id 字段作 rowKey
|
- data(必填):T[] —— 全量行数据,每行须含 id 字段作 rowKey
|
||||||
- columns(必填):ColumnDef<T>[] —— 列定义:内核字段 { key, header?, sortable?, width? } + 皮肤字段 { align?, format? };width 数字为固定像素,'flex' 自适应
|
- columns(必填):ColumnDef<T>[] —— 列定义:{ key, header?, sortable? }
|
||||||
- filterKey:string —— 工具栏筛选的列;缺省取第一列
|
- filterKey:string —— 工具栏筛选的列;缺省取第一列
|
||||||
- pageSize(默认 5):number —— 每页行数;缺省 5
|
- pageSize:number —— 每页行数;缺省 5
|
||||||
- selectable(默认 false):boolean —— 启用行选择列:全选三态(含半选)、行 checkbox、已选计数;缺省 false
|
- selectable:boolean —— 启用行选择列:全选三态(含半选)、行 checkbox、已选计数;缺省 false
|
||||||
- page:Signal<number> —— 受控页码信号:与内核分页双向同步,传同一个信号给独立 pagination 组件即可联动
|
|
||||||
- loading:boolean | Signal<boolean> —— 加载态:为真时数据行换成骨架行(行数随 pageSize),筛选输入与翻页按钮临时禁用,容器带 aria-busy
|
|
||||||
- selected:Signal<RowKey[]> —— 受控选中行信号:与内核选择双向同步(内容比较,不来回派发),供批量操作读取/改写
|
|
||||||
|
|
||||||
示例:
|
示例:
|
||||||
|
|
||||||
@@ -739,49 +399,32 @@ dataTable({ data: users, selectable: true, columns: [{ key: 'name', header: '姓
|
|||||||
|
|
||||||
1. 准备行数据,确保每行有唯一 id(跨页选择与刷新不丢行靠它)
|
1. 准备行数据,确保每行有唯一 id(跨页选择与刷新不丢行靠它)
|
||||||
2. 定义 columns:sortable: true 的列表头可点击排序(升 → 降 → 取消循环),标记箭头 ↑↓↕
|
2. 定义 columns:sortable: true 的列表头可点击排序(升 → 降 → 取消循环),标记箭头 ↑↓↕
|
||||||
3. 列展示细节用皮肤字段:align 控制表头与单元格对齐,format(value, row) 返回纯文本(走 textContent,不需要转义)
|
3. selectable: true 开启行选择:表头全选框带 indeterminate 半选态,工具栏显示已选计数
|
||||||
4. 列宽用内核字段 width:数字(像素)固定该列,'flex' 或不设则按内容自适应
|
4. 选中行用低视觉比重样式(淡色底 + 弱化文字),选择状态存 rowKey,跨页不丢失
|
||||||
5. selectable: true 开启行选择:表头全选框带 indeterminate 半选态,工具栏显示已选计数
|
5. 工具栏输入框即筛选,输入即时过滤并回第 1 页;无匹配时显示空状态行
|
||||||
6. 选中行用低视觉比重样式(淡色底 + 弱化文字),选择状态存 rowKey,跨页不丢失
|
6. 表头粘性固定(滚动容器 max-height 420px),斑马纹 + 悬停高亮由 CSS 提供
|
||||||
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,disabled 支持布尔或信号禁用
|
- button(props):按钮,variant = primary | ghost | danger
|
||||||
- textField(props):文本输入,value 信号双向绑定,disabled 支持布尔或信号禁用
|
- textField(props):文本输入,value 信号双向绑定
|
||||||
- textarea(props):多行输入,value 信号双向绑定,disabled 支持布尔或信号禁用
|
- textarea(props):多行输入,value 信号双向绑定
|
||||||
- slider(props):滑块,value 信号双向绑定,min/max/step 可配,原生 range 承载键盘交互,右侧可显示当前值
|
- checkbox(props):复选框,checked 信号双向绑定
|
||||||
- checkbox(props):复选框,checked 信号双向绑定,disabled 支持布尔或信号禁用
|
- switchButton(props):开关,checked 信号双向绑定
|
||||||
- switchButton(props):开关,checked 信号双向绑定,disabled 支持布尔或信号禁用
|
- select(props):自绘下拉选择(非原生 select),value 信号双向绑定,点外部 / Esc 关闭,选项弹层带微动画
|
||||||
- select(props):自绘下拉选择(非原生 select),value 信号双向绑定,点外部 / Esc 关闭,选项弹层带微动画,disabled 支持布尔或信号禁用
|
- radioGroup(props):单选组,options + value 信号双向绑定
|
||||||
- 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
|
||||||
- tag(props):可关闭标签,variant 五档语义色,closable + onClose 支持从数据源移除(纯展示请用 badge)
|
- alert(props):提示条,variant = info | success | warning | danger
|
||||||
- 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 色);不传 value 退化为不确定态无限往复
|
- progress(props):进度条,轨道 + 渐变流动填充,value 信号驱动 0-100,满格切 success 色
|
||||||
- tabs(props):标签页,active 信号双向绑定,滑动指示条 + 方向键环形导航,支持禁用页签
|
- tabs(props):标签页,active 信号双向绑定,滑动指示条 + 方向键环形导航,支持禁用页签
|
||||||
- modal(props, ...children):弹窗,open 信号驱动显隐,Esc / 遮罩 / × 关闭,size 三档宽度,footer 提供底部操作区
|
- modal(props, ...children):弹窗,open 信号驱动显隐,Esc / 遮罩 / × 关闭
|
||||||
- drawer(props, ...children):抽屉,open 信号驱动侧滑面板,placement 左右可配、width 可调、footer 操作区,遮罩 / × / Esc 关闭
|
|
||||||
- form(props, items):表单容器,布局 + 聚合校验,全部通过才触发 onSubmit
|
- form(props, items):表单容器,布局 + 聚合校验,全部通过才触发 onSubmit
|
||||||
- formItem(props, control):表单字段,label + 必填红星 + 规则校验 + 错误区,返回 { el, validate } 句柄
|
- formItem(props, control):表单字段,label + 必填红星 + 规则校验 + 错误区,返回 { el, validate } 句柄
|
||||||
- dataTable(props):数据表格,粘性表头 / 行选择(全选三态)/ 排序 / 筛选 / 分页 / 空状态;loading 为真时数据行换骨架行且筛选/翻页禁用,内核为 table-core
|
- dataTable(props):数据表格,粘性表头 / 行选择(全选三态)/ 排序 / 筛选 / 分页 / 空状态,内核为 table-core
|
||||||
|
|
||||||
一行用法:
|
一行用法:
|
||||||
|
|
||||||
@@ -794,7 +437,6 @@ 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-*
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -867,34 +509,6 @@ 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 mcp:零依赖 stdio MCP server,暴露 list_components / get_component / search_components
|
|
||||||
|
|
||||||
一行用法:
|
|
||||||
|
|
||||||
```
|
|
||||||
node packages/tools/cli/bin/ua.js <index|list|info <name>|docs|style|verify|mcp>;仓库内等价于 npm run index / docs / style / verify
|
|
||||||
```
|
|
||||||
|
|
||||||
约定:
|
|
||||||
|
|
||||||
- 零依赖:只用 node 标准库,Node >= 18
|
|
||||||
- 清单是单一来源:代码注释、文档站、llms-full.txt、MCP 全部从 ua.manifest.json 派生
|
|
||||||
- 门禁只增不改:新增校验必须能被人手复现,报错要指出文件与字段
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 设计令牌(--ua-*)
|
## 设计令牌(--ua-*)
|
||||||
|
|
||||||
- --ua-color-bg: #ffffff(暗色 #101216)
|
- --ua-color-bg: #ffffff(暗色 #101216)
|
||||||
|
|||||||
@@ -45,56 +45,26 @@
|
|||||||
## @ui-arch/ui (v0.1.0)
|
## @ui-arch/ui (v0.1.0)
|
||||||
|
|
||||||
- 类型: skin-dom · 目录: packages/skin/ui
|
- 类型: skin-dom · 目录: packages/skin/ui
|
||||||
- 描述: DOM 皮肤组件库:button / textField / textarea / slider / checkbox / switch / select / menu / radioGroup / accordion / breadcrumb / card / badge / tag / avatar / alert / toast / tooltip / popover / skeleton / spinner / progress / tabs / pagination / modal / drawer / form / formItem / dataTable,构建在 @ui-arch/frame 与 @ui-arch/table-core 之上,样式只消费 --ua-* 令牌。
|
- 描述: DOM 皮肤组件库:button / textField / textarea / checkbox / switch / select / radioGroup / card / badge / alert / spinner / progress / tabs / modal / form / formItem / dataTable,构建在 @ui-arch/frame 与 @ui-arch/table-core 之上,样式只消费 --ua-* 令牌。
|
||||||
- API:
|
- API:
|
||||||
- button(props):按钮,variant = primary | ghost | danger,disabled 支持布尔或信号禁用
|
- button(props):按钮,variant = primary | ghost | danger
|
||||||
- textField(props):文本输入,value 信号双向绑定,disabled 支持布尔或信号禁用
|
- textField(props):文本输入,value 信号双向绑定
|
||||||
- textarea(props):多行输入,value 信号双向绑定,disabled 支持布尔或信号禁用
|
- textarea(props):多行输入,value 信号双向绑定
|
||||||
- slider(props):滑块,value 信号双向绑定,min/max/step 可配,原生 range 承载键盘交互,右侧可显示当前值
|
- checkbox(props):复选框,checked 信号双向绑定
|
||||||
- checkbox(props):复选框,checked 信号双向绑定,disabled 支持布尔或信号禁用
|
- switchButton(props):开关,checked 信号双向绑定
|
||||||
- switchButton(props):开关,checked 信号双向绑定,disabled 支持布尔或信号禁用
|
- select(props):自绘下拉选择(非原生 select),value 信号双向绑定,点外部 / Esc 关闭,选项弹层带微动画
|
||||||
- select(props):自绘下拉选择(非原生 select),value 信号双向绑定,点外部 / Esc 关闭,选项弹层带微动画,disabled 支持布尔或信号禁用
|
- radioGroup(props):单选组,options + value 信号双向绑定
|
||||||
- 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
|
||||||
- tag(props):可关闭标签,variant 五档语义色,closable + onClose 支持从数据源移除(纯展示请用 badge)
|
- alert(props):提示条,variant = info | success | warning | danger
|
||||||
- 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 色);不传 value 退化为不确定态无限往复
|
- progress(props):进度条,轨道 + 渐变流动填充,value 信号驱动 0-100,满格切 success 色
|
||||||
- tabs(props):标签页,active 信号双向绑定,滑动指示条 + 方向键环形导航,支持禁用页签
|
- tabs(props):标签页,active 信号双向绑定,滑动指示条 + 方向键环形导航,支持禁用页签
|
||||||
- modal(props, ...children):弹窗,open 信号驱动显隐,Esc / 遮罩 / × 关闭,size 三档宽度,footer 提供底部操作区
|
- modal(props, ...children):弹窗,open 信号驱动显隐,Esc / 遮罩 / × 关闭
|
||||||
- drawer(props, ...children):抽屉,open 信号驱动侧滑面板,placement 左右可配、width 可调、footer 操作区,遮罩 / × / Esc 关闭
|
|
||||||
- form(props, items):表单容器,布局 + 聚合校验,全部通过才触发 onSubmit
|
- form(props, items):表单容器,布局 + 聚合校验,全部通过才触发 onSubmit
|
||||||
- formItem(props, control):表单字段,label + 必填红星 + 规则校验 + 错误区,返回 { el, validate } 句柄
|
- formItem(props, control):表单字段,label + 必填红星 + 规则校验 + 错误区,返回 { el, validate } 句柄
|
||||||
- dataTable(props):数据表格,粘性表头 / 行选择(全选三态)/ 排序 / 筛选 / 分页 / 空状态;loading 为真时数据行换骨架行且筛选/翻页禁用,内核为 table-core
|
- dataTable(props):数据表格,粘性表头 / 行选择(全选三态)/ 排序 / 筛选 / 分页 / 空状态,内核为 table-core
|
||||||
- 用法: `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 mcp:零依赖 stdio MCP server,暴露 list_components / get_component / search_components
|
|
||||||
- 用法: `node packages/tools/cli/bin/ua.js <index|list|info <name>|docs|style|verify|mcp>;仓库内等价于 npm run index / docs / style / verify`
|
|
||||||
- 约定: 零依赖:只用 node 标准库,Node >= 18
|
|
||||||
- 约定: 清单是单一来源:代码注释、文档站、llms-full.txt、MCP 全部从 ua.manifest.json 派生
|
|
||||||
- 约定: 门禁只增不改:新增校验必须能被人手复现,报错要指出文件与字段
|
|
||||||
|
|||||||
+2
-3
@@ -12,9 +12,8 @@
|
|||||||
"style": "node packages/tools/cli/bin/ua.js style",
|
"style": "node packages/tools/cli/bin/ua.js style",
|
||||||
"index": "node packages/tools/cli/bin/ua.js index",
|
"index": "node packages/tools/cli/bin/ua.js index",
|
||||||
"docs": "node packages/tools/cli/bin/ua.js index && node packages/tools/cli/bin/ua.js docs",
|
"docs": "node packages/tools/cli/bin/ua.js index && node packages/tools/cli/bin/ua.js docs",
|
||||||
"check": "npm run build && npm run style && npm run typecheck && npm run smoke && npm run index && npm run verify",
|
"check": "npm run build && npm run style && npm run typecheck && npm run smoke && npm run index",
|
||||||
"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"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5.6.0"
|
"typescript": "^5.6.0"
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
@@ -1,32 +1,7 @@
|
|||||||
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 决定语义色,可选标题与关闭按钮;关闭即退场并从 DOM 移除(内容由调用方持有) ***/
|
/*** 提示条:variant 决定语义色,纯展示不持有状态 ***/
|
||||||
export function alert(props: AlertProps): HTMLElement {
|
export function alert(props: AlertProps): HTMLElement {
|
||||||
const body = h(
|
return h('div', { class: `ua-alert ua-alert-${props.variant ?? 'info'}` }, props.message);
|
||||||
'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;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
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() : '?'; // 空名兜底问号,避免空白圆
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
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 控制视觉强度,disabled 支持布尔或信号禁用,onClick 为点击回调 ***/
|
/*** 按钮:variant 控制视觉强度,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'}`, disabled: props.disabled, onClick: props.onClick }, // false/undefined 移除属性,Signal 走响应式
|
{ class: `ua-btn ua-btn-${props.variant ?? 'primary'}`, onClick: props.onClick },
|
||||||
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 信号双向绑定,disabled 支持布尔或信号禁用,onChange 为变更回调 ***/
|
/*** 复选框:checked 信号双向绑定,onChange 为变更回调 ***/
|
||||||
export function checkbox(props: CheckboxProps): HTMLElement {
|
export function checkbox(props: CheckboxProps): HTMLElement {
|
||||||
const el = h('input', { type: 'checkbox', class: 'ua-checkbox', disabled: props.disabled }) as HTMLInputElement; // false/undefined 移除属性,Signal 走响应式
|
const el = h('input', { type: 'checkbox', class: 'ua-checkbox' }) as HTMLInputElement;
|
||||||
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,7 +8,6 @@ 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';
|
||||||
|
|
||||||
@@ -44,35 +43,6 @@ 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);
|
||||||
@@ -106,23 +76,15 @@ export function dataTable<T extends { id: RowKey }>(props: DataTableProps<T>): H
|
|||||||
h(
|
h(
|
||||||
'th',
|
'th',
|
||||||
{
|
{
|
||||||
class: `${col.sortable ? 'ua-sortable' : ''}${col.align ? ` ua-align-${col.align}` : ''}`.trim(),
|
class: () => (col.sortable ? 'ua-sortable' : ''),
|
||||||
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) {
|
||||||
@@ -131,11 +93,7 @@ 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) => {
|
const cells = props.columns.map((col) => h('td', null, String(table.getCellValue(row, col.key))));
|
||||||
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;
|
||||||
@@ -146,17 +104,8 @@ 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}…`,
|
||||||
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 页
|
||||||
@@ -165,20 +114,20 @@ export function dataTable<T extends { id: RowKey }>(props: DataTableProps<T>): H
|
|||||||
|
|
||||||
return h(
|
return h(
|
||||||
'section',
|
'section',
|
||||||
{ class: 'ua-table', 'aria-busy': () => String(busy()) },
|
{ class: 'ua-table' },
|
||||||
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, colgroup, h('thead', null, h('tr', null, selectable ? headCheck : null, ...props.columns.map(th))), tbody)),
|
h('div', { class: 'ua-table-scroll' }, h('table', null, 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', disabled: props.loading, onClick: () => table.dispatch({ type: 'setPage', page: info().page - 1 }) }),
|
button({ label: '上一页', variant: 'ghost', 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', disabled: props.loading, onClick: () => table.dispatch({ type: 'setPage', page: info().page + 1 }) }),
|
button({ label: '下一页', variant: 'ghost', onClick: () => table.dispatch({ type: 'setPage', page: info().page + 1 }) }),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,60 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
@@ -26,13 +26,7 @@ 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;
|
||||||
@@ -42,18 +36,14 @@ 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('change', () => {
|
control.addEventListener('blur', () => {
|
||||||
if (mode === 'input') validate(); // select / checkbox / radio 变更即校验(原生 input 事件不冒泡)
|
if (mode !== 'submit') validate();
|
||||||
});
|
|
||||||
control.addEventListener('focusout', () => {
|
|
||||||
if (mode !== 'submit') validate(); // 用会冒泡的 focusout 取代 blur,包裹式控件同样生效
|
|
||||||
});
|
});
|
||||||
return { el, validate };
|
return { el, validate };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,28 +2,16 @@ 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 { 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 { 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 { 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';
|
||||||
|
|||||||
@@ -1,85 +0,0 @@
|
|||||||
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;Tab 焦点陷阱 + 关闭归还焦点 ***/
|
/*** 弹窗:open 信号驱动显隐,点遮罩、× 或 Esc 触发 onClose ***/
|
||||||
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,38 +16,15 @@ export function modal(props: ModalProps, ...children: unknown[]): HTMLElement {
|
|||||||
});
|
});
|
||||||
const box = h(
|
const box = h(
|
||||||
'div',
|
'div',
|
||||||
{ class: `ua-modal ua-modal-${props.size ?? 'md'}`, role: 'dialog', 'aria-modal': 'true' }, // 对话框语义,供读屏识别
|
{ class: 'ua-modal' },
|
||||||
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-head' }, h('h3', { class: 'ua-modal-title' }, props.title), h('button', { class: 'ua-modal-close', 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) {
|
if (open) (box.querySelector('button, input, textarea') as HTMLElement | null)?.focus(); // 打开时聚焦首个可交互元素
|
||||||
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 关闭当前打开的弹窗
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
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,37 +1,18 @@
|
|||||||
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 定量态,缺省 value 则退化为不确定态(无限往复);满格切 success 色并停条纹 ***/
|
/*** 进度条:轨道 + 渐变流动填充,value 信号驱动 0-100;满格切换 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(value()); // 依赖收集:信号变化自动写回
|
const v = clamp(props.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(
|
return h('div', { class: 'ua-progress' }, track, props.showText === false ? null : text);
|
||||||
'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,12 +6,7 @@ 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', {
|
const input = h('input', { type: 'radio', name: groupName, class: 'ua-radio' }) as HTMLInputElement;
|
||||||
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', () => {
|
||||||
|
|||||||
@@ -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 信号双向绑定;支持 ↑↓/Home/End 键盘导航,点选项 / 点外部 / Esc 关闭 ***/
|
/*** 下拉选择:自绘弹层替代原生 select,value 信号双向绑定;点选项 / 点外部 / Esc 关闭 ***/
|
||||||
export function select(props: SelectProps): HTMLElement {
|
export function select(props: SelectProps): HTMLElement {
|
||||||
// —— 状态集中声明 ——
|
// —— 状态集中声明 ——
|
||||||
const open = signal(false); // 弹层开合
|
const open = signal(false); // 弹层开合
|
||||||
@@ -24,7 +24,6 @@ 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 || ''),
|
||||||
@@ -47,21 +46,10 @@ 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${open() ? ' ua-open' : ''}` }, trigger, pop); // 根带开合类,供箭头旋转
|
const root = h('div', { class: 'ua-select' }, trigger, pop);
|
||||||
|
|
||||||
root.addEventListener('keydown', (e: KeyboardEvent) => {
|
root.addEventListener('keydown', (e: KeyboardEvent) => {
|
||||||
if (e.key === 'Escape') { open(false); trigger.focus(); return; } // Esc 关闭并回焦触发器
|
if (e.key === 'Escape') open(false); // 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; // 卫语句:组件内点击不处理
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
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); // 整组装饰性占位
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
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', role: 'status', 'aria-label': '加载中' }); // 读屏播报加载状态
|
const el = h('span', { class: 'ua-spinner' });
|
||||||
el.style.width = size;
|
el.style.width = size;
|
||||||
el.style.height = size;
|
el.style.height = size;
|
||||||
return el;
|
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 信号双向绑定,disabled 支持布尔或信号禁用,轨道 + 滑块的纯 CSS 实现 ***/
|
/*** 开关:checked 信号双向绑定,轨道 + 滑块的纯 CSS 实现 ***/
|
||||||
export function switchButton(props: SwitchProps): HTMLElement {
|
export function switchButton(props: SwitchProps): HTMLElement {
|
||||||
const input = h('input', { type: 'checkbox', class: 'ua-switch-input', disabled: props.disabled }) as HTMLInputElement; // false/undefined 移除属性,Signal 走响应式
|
const input = h('input', { type: 'checkbox', class: 'ua-switch-input' }) as HTMLInputElement;
|
||||||
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', 'aria-label': '标签页' }, indicator);
|
const head = h('div', { class: 'ua-tabs-head', role: 'tablist' }, indicator);
|
||||||
|
|
||||||
/*** 同步激活态与指示条几何位置 ***/
|
/*** 同步激活态与指示条几何位置 ***/
|
||||||
const sync = () => {
|
const sync = () => {
|
||||||
@@ -27,7 +27,6 @@ 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); // 用户切换写回信号
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
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,9 @@
|
|||||||
import { effect, h } from '@ui-arch/frame';
|
import { effect, h } from '@ui-arch/frame';
|
||||||
import type { TextFieldProps } from './types.js';
|
import type { TextFieldProps } from './types.js';
|
||||||
|
|
||||||
/*** 文本输入:value 信号双向绑定,disabled 支持布尔或信号禁用,onInput 为输入回调 ***/
|
/*** 文本输入:value 信号双向绑定,onInput 为输入回调 ***/
|
||||||
export function textField(props: TextFieldProps): HTMLInputElement {
|
export function textField(props: TextFieldProps): HTMLInputElement {
|
||||||
const el = h('input', { class: 'ua-input', placeholder: props.placeholder ?? '', disabled: props.disabled }) as HTMLInputElement; // false/undefined 移除属性,Signal 走响应式
|
const el = h('input', { class: 'ua-input', placeholder: props.placeholder ?? '' }) as HTMLInputElement;
|
||||||
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)); // 用户输入写回信号
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { effect, h } from '@ui-arch/frame';
|
import { effect, h } from '@ui-arch/frame';
|
||||||
import type { TextareaProps } from './types.js';
|
import type { TextareaProps } from './types.js';
|
||||||
|
|
||||||
/*** 多行输入:value 信号双向绑定,disabled 支持布尔或信号禁用,onInput 为输入回调 ***/
|
/*** 多行输入:value 信号双向绑定,onInput 为输入回调 ***/
|
||||||
export function textarea(props: TextareaProps): HTMLTextAreaElement {
|
export function textarea(props: TextareaProps): HTMLTextAreaElement {
|
||||||
const el = h('textarea', { class: 'ua-textarea', rows: props.rows ?? 3, placeholder: props.placeholder ?? '', disabled: props.disabled }) as HTMLTextAreaElement; // false/undefined 移除属性,Signal 走响应式
|
const el = h('textarea', { class: 'ua-textarea', rows: props.rows ?? 3, placeholder: props.placeholder ?? '' }) as HTMLTextAreaElement;
|
||||||
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)); // 用户输入写回信号
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
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' }), // 错误
|
|
||||||
};
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
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,28 +1,10 @@
|
|||||||
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; // 点击回调
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,7 +12,6 @@ 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; // 输入回调
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,22 +20,13 @@ 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: TableColumn<T>[]; // 列定义:内核字段 + align / format
|
columns: ColumnDef<T>[]; // 列定义
|
||||||
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 共用的选项形状 ***/
|
||||||
@@ -68,7 +40,6 @@ 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; // 变更回调
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,7 +47,6 @@ 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; // 变更回调
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,7 +55,6 @@ 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; // 变更回调
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,75 +62,14 @@ 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; // 输入回调
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,50 +79,9 @@ 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 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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,8 +92,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
|
||||||
}
|
}
|
||||||
|
|
||||||
/*** 标签页入参 ***/
|
/*** 标签页入参 ***/
|
||||||
@@ -240,21 +107,9 @@ 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 后判定)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@ui-arch/ui",
|
"name": "@ui-arch/ui",
|
||||||
"type": "skin-dom",
|
"type": "skin-dom",
|
||||||
"description": "DOM 皮肤组件库:button / textField / textarea / slider / checkbox / switch / select / menu / radioGroup / accordion / breadcrumb / card / badge / tag / avatar / alert / toast / tooltip / popover / skeleton / spinner / progress / tabs / pagination / modal / drawer / form / formItem / dataTable,构建在 @ui-arch/frame 与 @ui-arch/table-core 之上,样式只消费 --ua-* 令牌。",
|
"description": "DOM 皮肤组件库:button / textField / textarea / checkbox / switch / select / radioGroup / card / badge / alert / spinner / progress / tabs / modal / form / formItem / dataTable,构建在 @ui-arch/frame 与 @ui-arch/table-core 之上,样式只消费 --ua-* 令牌。",
|
||||||
"tags": [
|
"tags": [
|
||||||
"components",
|
"components",
|
||||||
"dom",
|
"dom",
|
||||||
@@ -13,35 +13,23 @@
|
|||||||
"api": {
|
"api": {
|
||||||
"entry": "src/index.ts",
|
"entry": "src/index.ts",
|
||||||
"exports": [
|
"exports": [
|
||||||
"button(props):按钮,variant = primary | ghost | danger,disabled 支持布尔或信号禁用",
|
"button(props):按钮,variant = primary | ghost | danger",
|
||||||
"textField(props):文本输入,value 信号双向绑定,disabled 支持布尔或信号禁用",
|
"textField(props):文本输入,value 信号双向绑定",
|
||||||
"textarea(props):多行输入,value 信号双向绑定,disabled 支持布尔或信号禁用",
|
"textarea(props):多行输入,value 信号双向绑定",
|
||||||
"slider(props):滑块,value 信号双向绑定,min/max/step 可配,原生 range 承载键盘交互,右侧可显示当前值",
|
"checkbox(props):复选框,checked 信号双向绑定",
|
||||||
"checkbox(props):复选框,checked 信号双向绑定,disabled 支持布尔或信号禁用",
|
"switchButton(props):开关,checked 信号双向绑定",
|
||||||
"switchButton(props):开关,checked 信号双向绑定,disabled 支持布尔或信号禁用",
|
"select(props):自绘下拉选择(非原生 select),value 信号双向绑定,点外部 / Esc 关闭,选项弹层带微动画",
|
||||||
"select(props):自绘下拉选择(非原生 select),value 信号双向绑定,点外部 / Esc 关闭,选项弹层带微动画,disabled 支持布尔或信号禁用",
|
"radioGroup(props):单选组,options + value 信号双向绑定",
|
||||||
"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",
|
||||||
"tag(props):可关闭标签,variant 五档语义色,closable + onClose 支持从数据源移除(纯展示请用 badge)",
|
"alert(props):提示条,variant = info | success | warning | danger",
|
||||||
"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 色);不传 value 退化为不确定态无限往复",
|
"progress(props):进度条,轨道 + 渐变流动填充,value 信号驱动 0-100,满格切 success 色",
|
||||||
"tabs(props):标签页,active 信号双向绑定,滑动指示条 + 方向键环形导航,支持禁用页签",
|
"tabs(props):标签页,active 信号双向绑定,滑动指示条 + 方向键环形导航,支持禁用页签",
|
||||||
"modal(props, ...children):弹窗,open 信号驱动显隐,Esc / 遮罩 / × 关闭,size 三档宽度,footer 提供底部操作区",
|
"modal(props, ...children):弹窗,open 信号驱动显隐,Esc / 遮罩 / × 关闭",
|
||||||
"drawer(props, ...children):抽屉,open 信号驱动侧滑面板,placement 左右可配、width 可调、footer 操作区,遮罩 / × / Esc 关闭",
|
|
||||||
"form(props, items):表单容器,布局 + 聚合校验,全部通过才触发 onSubmit",
|
"form(props, items):表单容器,布局 + 聚合校验,全部通过才触发 onSubmit",
|
||||||
"formItem(props, control):表单字段,label + 必填红星 + 规则校验 + 错误区,返回 { el, validate } 句柄",
|
"formItem(props, control):表单字段,label + 必填红星 + 规则校验 + 错误区,返回 { el, validate } 句柄",
|
||||||
"dataTable(props):数据表格,粘性表头 / 行选择(全选三态)/ 排序 / 筛选 / 分页 / 空状态;loading 为真时数据行换骨架行且筛选/翻页禁用,内核为 table-core"
|
"dataTable(props):数据表格,粘性表头 / 行选择(全选三态)/ 排序 / 筛选 / 分页 / 空状态,内核为 table-core"
|
||||||
],
|
],
|
||||||
"keyTypes": [
|
"keyTypes": [
|
||||||
"ButtonProps",
|
"ButtonProps",
|
||||||
@@ -51,20 +39,10 @@
|
|||||||
"SwitchProps",
|
"SwitchProps",
|
||||||
"SelectProps",
|
"SelectProps",
|
||||||
"RadioGroupProps",
|
"RadioGroupProps",
|
||||||
"MenuItem",
|
|
||||||
"MenuProps",
|
|
||||||
"AccordionItem",
|
|
||||||
"AccordionProps",
|
|
||||||
"BreadcrumbItem",
|
|
||||||
"BreadcrumbProps",
|
|
||||||
"OptionItem",
|
"OptionItem",
|
||||||
"CardProps",
|
"CardProps",
|
||||||
"BadgeProps",
|
"BadgeProps",
|
||||||
"AlertProps",
|
"AlertProps",
|
||||||
"ToastProps",
|
|
||||||
"TooltipProps",
|
|
||||||
"SkeletonProps",
|
|
||||||
"PaginationProps",
|
|
||||||
"SpinnerProps",
|
"SpinnerProps",
|
||||||
"ProgressProps",
|
"ProgressProps",
|
||||||
"TabsProps",
|
"TabsProps",
|
||||||
@@ -73,23 +51,17 @@
|
|||||||
"FormItemProps",
|
"FormItemProps",
|
||||||
"FormItemHandle",
|
"FormItemHandle",
|
||||||
"FormProps",
|
"FormProps",
|
||||||
"DataTableProps",
|
"DataTableProps"
|
||||||
"PopoverProps",
|
|
||||||
"SliderProps",
|
|
||||||
"TagProps",
|
|
||||||
"DrawerProps",
|
|
||||||
"AvatarProps"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"usage": "const checked = signal(false); checkbox({ label: '同意', checked }); const open = signal(false); modal({ title: '确认', open }, '内容')",
|
"usage": "const checked = signal(false); checkbox({ label: '同意', checked }); const open = signal(false); modal({ title: '确认', open }, '内容')",
|
||||||
"conventions": [
|
"conventions": [
|
||||||
"皮肤不持有业务状态,一律委托 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-*"
|
|
||||||
],
|
],
|
||||||
"docs": {
|
"docs": {
|
||||||
"intro": "28 个 DOM 组件,统一用法:组件是普通函数,传入 props 返回 DOM 节点;需要状态时先创建 signal 再双向绑定。引入 CSS:import '@ui-arch/ui/ui.css'(内部已消费 tokens)。",
|
"intro": "16 个 DOM 组件,统一用法:组件是普通函数,传入 props 返回 DOM 节点;需要状态时先创建 signal 再双向绑定。引入 CSS:import '@ui-arch/ui/ui.css'(内部已消费 tokens)。",
|
||||||
"steps": [
|
"steps": [
|
||||||
"安装依赖:pnpm add @ui-arch/ui @ui-arch/frame(workspace 内自动链接)",
|
"安装依赖:pnpm add @ui-arch/ui @ui-arch/frame(workspace 内自动链接)",
|
||||||
"引入样式:在应用入口 import '@ui-arch/ui/ui.css',暗色主题在根元素设 data-theme='dark'",
|
"引入样式:在应用入口 import '@ui-arch/ui/ui.css',暗色主题在根元素设 data-theme='dark'",
|
||||||
@@ -112,13 +84,7 @@
|
|||||||
{
|
{
|
||||||
"n": "variant",
|
"n": "variant",
|
||||||
"t": "'primary' | 'ghost' | 'danger'",
|
"t": "'primary' | 'ghost' | 'danger'",
|
||||||
"d": "视觉强度;缺省 primary",
|
"d": "视觉强度;缺省 primary"
|
||||||
"df": "primary"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "disabled",
|
|
||||||
"t": "boolean | Signal<boolean>",
|
|
||||||
"d": "禁用态:布尔直接置位,传信号则动态同步"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"n": "onClick",
|
"n": "onClick",
|
||||||
@@ -149,11 +115,6 @@
|
|||||||
"t": "string",
|
"t": "string",
|
||||||
"d": "占位文案"
|
"d": "占位文案"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"n": "disabled",
|
|
||||||
"t": "boolean | Signal<boolean>",
|
|
||||||
"d": "禁用态:布尔直接置位,传信号则动态同步"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"n": "onInput",
|
"n": "onInput",
|
||||||
"t": "(value: string) => void",
|
"t": "(value: string) => void",
|
||||||
@@ -181,19 +142,13 @@
|
|||||||
{
|
{
|
||||||
"n": "rows",
|
"n": "rows",
|
||||||
"t": "number",
|
"t": "number",
|
||||||
"d": "初始行数;缺省 3",
|
"d": "初始行数;缺省 3"
|
||||||
"df": "3"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"n": "placeholder",
|
"n": "placeholder",
|
||||||
"t": "string",
|
"t": "string",
|
||||||
"d": "占位文案"
|
"d": "占位文案"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"n": "disabled",
|
|
||||||
"t": "boolean | Signal<boolean>",
|
|
||||||
"d": "禁用态:布尔直接置位,传信号则动态同步"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"n": "onInput",
|
"n": "onInput",
|
||||||
"t": "(value: string) => void",
|
"t": "(value: string) => void",
|
||||||
@@ -208,62 +163,6 @@
|
|||||||
"字数统计等派生逻辑直接在响应式文本里读 bio().length"
|
"字数统计等派生逻辑直接在响应式文本里读 bio().length"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "slider",
|
|
||||||
"title": "滑块 Slider",
|
|
||||||
"demo": "sec-slider",
|
|
||||||
"props": [
|
|
||||||
{
|
|
||||||
"n": "value",
|
|
||||||
"t": "Signal<number>",
|
|
||||||
"r": 1,
|
|
||||||
"d": "当前值信号,双向绑定:拖动写回,外部赋值同步位置"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "min",
|
|
||||||
"t": "number",
|
|
||||||
"d": "最小值;缺省 0",
|
|
||||||
"df": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "max",
|
|
||||||
"t": "number",
|
|
||||||
"d": "最大值;缺省 100",
|
|
||||||
"df": "100"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "step",
|
|
||||||
"t": "number",
|
|
||||||
"d": "步长;缺省 1",
|
|
||||||
"df": "1"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "disabled",
|
|
||||||
"t": "boolean | Signal<boolean>",
|
|
||||||
"d": "禁用态:布尔直接置位,传信号则动态同步"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "showValue",
|
|
||||||
"t": "boolean",
|
|
||||||
"d": "是否右侧显示当前值;缺省 true",
|
|
||||||
"df": "true"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "onChange",
|
|
||||||
"t": "(value: number) => void",
|
|
||||||
"k": 1,
|
|
||||||
"d": "拖动回调"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"example": "const vol = signal(40); slider({ value: vol, min: 0, max: 100, step: 5, onChange: (v) => console.log(v) })",
|
|
||||||
"steps": [
|
|
||||||
"创建数值信号(初值在 min/max 区间内),传入 value 完成双向绑定",
|
|
||||||
"用 min / max / step 限定范围与粒度,如 step: 5 只取 5 的倍数",
|
|
||||||
"拖动或按 ←→ 键改变值:原生 range 自带键盘与触屏交互,无需额外处理",
|
|
||||||
"只想看拖动条不显示数字时传 showValue: false;需要自定义展示可自行读 signal",
|
|
||||||
"禁用场景传 disabled(布尔或信号),置灰且不可拖"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "checkbox",
|
"id": "checkbox",
|
||||||
"title": "复选框 Checkbox",
|
"title": "复选框 Checkbox",
|
||||||
@@ -279,11 +178,6 @@
|
|||||||
"t": "string",
|
"t": "string",
|
||||||
"d": "右侧文案;缺省只渲染勾选框"
|
"d": "右侧文案;缺省只渲染勾选框"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"n": "disabled",
|
|
||||||
"t": "boolean | Signal<boolean>",
|
|
||||||
"d": "禁用态:布尔直接置位,传信号则动态同步"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"n": "onChange",
|
"n": "onChange",
|
||||||
"t": "(checked: boolean) => void",
|
"t": "(checked: boolean) => void",
|
||||||
@@ -313,11 +207,6 @@
|
|||||||
"t": "string",
|
"t": "string",
|
||||||
"d": "左侧文案"
|
"d": "左侧文案"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"n": "disabled",
|
|
||||||
"t": "boolean | Signal<boolean>",
|
|
||||||
"d": "禁用态:布尔直接置位,传信号则动态同步"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"n": "onChange",
|
"n": "onChange",
|
||||||
"t": "(checked: boolean) => void",
|
"t": "(checked: boolean) => void",
|
||||||
@@ -353,11 +242,6 @@
|
|||||||
"t": "string",
|
"t": "string",
|
||||||
"d": "未选中时触发器显示的占位文案"
|
"d": "未选中时触发器显示的占位文案"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"n": "disabled",
|
|
||||||
"t": "boolean | Signal<boolean>",
|
|
||||||
"d": "禁用态:布尔直接置位,传信号则动态同步;禁用时触发器置灰且不展开弹层"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"n": "onChange",
|
"n": "onChange",
|
||||||
"t": "(value: string) => void",
|
"t": "(value: string) => void",
|
||||||
@@ -373,52 +257,6 @@
|
|||||||
"选中项在弹层中用淡色弱化标记,不抢视觉焦点"
|
"选中项在弹层中用淡色弱化标记,不抢视觉焦点"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "menu",
|
|
||||||
"title": "下拉菜单 Menu",
|
|
||||||
"demo": "sec-menu",
|
|
||||||
"props": [
|
|
||||||
{
|
|
||||||
"n": "label",
|
|
||||||
"t": "string",
|
|
||||||
"r": 1,
|
|
||||||
"d": "触发器文案"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "items",
|
|
||||||
"t": "MenuItem[]",
|
|
||||||
"r": 1,
|
|
||||||
"d": "菜单条目:{ value, label, disabled?, danger?, divider?, shortcut? }"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "variant",
|
|
||||||
"t": "'primary' | 'ghost'",
|
|
||||||
"d": "触发器视觉强度;缺省 ghost",
|
|
||||||
"df": "ghost"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "align",
|
|
||||||
"t": "'start' | 'end'",
|
|
||||||
"d": "弹层对齐方向;缺省 start(与触发器左对齐)",
|
|
||||||
"df": "start"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "onSelect",
|
|
||||||
"t": "(value: string) => void",
|
|
||||||
"k": 1,
|
|
||||||
"d": "选中回调,禁用项不触发"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"example": "menu({ label: '操作', items: [{ value: 'edit', label: '编辑' }, { value: 'del', label: '删除', danger: true }], onSelect: (v) => console.log(v) })",
|
|
||||||
"steps": [
|
|
||||||
"准备 items 数组:value 是回调标识、label 是展示文案,插一条 { divider: true } 即可分出条目组",
|
|
||||||
"传 label 决定触发器文案;默认 ghost 轻量样式,主操作用 variant: 'primary'",
|
|
||||||
"点触发器展开弹层,点条目收起并触发 onSelect;点外部或按 Esc 也会收起(Esc 后焦点回到触发器)",
|
|
||||||
"条目要给快捷键提示时传 shortcut(如 '⌘K'):右侧灰字展示,组件不绑定按键,实际快捷键由调用方在全局监听",
|
|
||||||
"键盘:Tab 聚焦触发器,↑↓ 展开并在可用条目间环形移动,Enter / Space 选中;disabled 项自动跳过",
|
|
||||||
"靠右对齐避免溢出时传 align: 'end';危险操作给条目加 danger: true"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "radioGroup",
|
"id": "radioGroup",
|
||||||
"title": "单选组 RadioGroup",
|
"title": "单选组 RadioGroup",
|
||||||
@@ -435,11 +273,6 @@
|
|||||||
"t": "Signal<string>",
|
"t": "Signal<string>",
|
||||||
"d": "选中值信号,双向绑定"
|
"d": "选中值信号,双向绑定"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"n": "disabled",
|
|
||||||
"t": "boolean | Signal<boolean>",
|
|
||||||
"d": "整组禁用:全部选项与文案置灰且不可选"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"n": "onChange",
|
"n": "onChange",
|
||||||
"t": "(value: string) => void",
|
"t": "(value: string) => void",
|
||||||
@@ -451,78 +284,7 @@
|
|||||||
"steps": [
|
"steps": [
|
||||||
"创建字符串信号,初值设为默认选项的 value",
|
"创建字符串信号,初值设为默认选项的 value",
|
||||||
"传入 options 与 value,组内自动互斥",
|
"传入 options 与 value,组内自动互斥",
|
||||||
"读取 size() 获取当前选中值",
|
"读取 size() 获取当前选中值"
|
||||||
"需要整组禁用时传 disabled:布尔值静态禁用,也可传信号(如 locked)实现动态锁定"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "accordion",
|
|
||||||
"title": "折叠面板 Accordion",
|
|
||||||
"demo": "sec-accordion",
|
|
||||||
"props": [
|
|
||||||
{
|
|
||||||
"n": "items",
|
|
||||||
"t": "AccordionItem[]",
|
|
||||||
"r": 1,
|
|
||||||
"d": "面板列表:{ value, title, content, disabled? }"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "active",
|
|
||||||
"t": "Signal<string>",
|
|
||||||
"d": "受控展开面板信号(单开模式);缺省由组件内部持有状态"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "multiple",
|
|
||||||
"t": "boolean",
|
|
||||||
"d": "允许多个面板同时展开;缺省 false(手风琴,单开)",
|
|
||||||
"df": "false"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "onChange",
|
|
||||||
"t": "(active: string) => void",
|
|
||||||
"k": 1,
|
|
||||||
"d": "展开变化回调:收起时传空串"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"example": "accordion({ items: [{ value: 'a', title: '账户设置', content: '内容…' }, { value: 'b', title: '通知', content: h('p', null, '…') }] })",
|
|
||||||
"steps": [
|
|
||||||
"准备 items:value 作标识、title 作标题、content 可为字符串 / DOM 节点 / 响应式函数",
|
|
||||||
"默认手风琴模式:展开一个自动收起其他;需要同时展开多个时传 multiple: true",
|
|
||||||
"需要外部控制或读取展开项时传 active 信号(受控单开),否则组件内部自持状态",
|
|
||||||
"标题是原生 button:Tab 聚焦、Enter / Space 切换,展开态带 aria-expanded + region 关联",
|
|
||||||
"内容区用 grid 行高 0fr → 1fr 过渡,无需测量高度;reduced-motion 用户自动静态切换"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "breadcrumb",
|
|
||||||
"title": "面包屑 Breadcrumb",
|
|
||||||
"demo": "sec-breadcrumb",
|
|
||||||
"props": [
|
|
||||||
{
|
|
||||||
"n": "items",
|
|
||||||
"t": "BreadcrumbItem[]",
|
|
||||||
"r": 1,
|
|
||||||
"d": "路径条目:{ label, href? },末项自动视为当前页"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "separator",
|
|
||||||
"t": "string",
|
|
||||||
"d": "分隔符;缺省 '/'",
|
|
||||||
"df": "/"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "onNavigate",
|
|
||||||
"t": "(item, index) => void",
|
|
||||||
"k": 1,
|
|
||||||
"d": "无 href 条目的点击回调"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"example": "breadcrumb({ items: [{ label: '首页', href: '/' }, { label: '设置' }, { label: '账户' }], onNavigate: (it) => console.log(it.label) })",
|
|
||||||
"steps": [
|
|
||||||
"按层级顺序准备 items,最后一项即当前页,自动渲染为不可点的灰色文字并带 aria-current='page'",
|
|
||||||
"需要真实跳转就给条目加 href(渲染 <a>);单页应用想自行接管时不给 href,改用 onNavigate 回调",
|
|
||||||
"改分隔符传 separator,如 '›';分隔符对读屏隐藏",
|
|
||||||
"语义结构为 nav + ol + li,可直接放进页头或卡片标题栏"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -562,91 +324,13 @@
|
|||||||
{
|
{
|
||||||
"n": "variant",
|
"n": "variant",
|
||||||
"t": "'default' | 'success' | 'warning' | 'danger'",
|
"t": "'default' | 'success' | 'warning' | 'danger'",
|
||||||
"d": "语义色;缺省 default",
|
"d": "语义色;缺省 default"
|
||||||
"df": "default"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"example": "badge({ label: '成功', variant: 'success' })",
|
"example": "badge({ label: '成功', variant: 'success' })",
|
||||||
"steps": [
|
"steps": [
|
||||||
"选择语义色:default 中性、success 成功、warning 警告、danger 危险",
|
"选择语义色:default 中性、success 成功、warning 警告、danger 危险",
|
||||||
"传入 label 即可,纯展示组件无状态",
|
"传入 label 即可,纯展示组件无状态"
|
||||||
"状态语义靠 variant 承载,不要自造颜色;纯展示不可交互,需要操作请配 button"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "tag",
|
|
||||||
"title": "标签 Tag",
|
|
||||||
"demo": "sec-tag",
|
|
||||||
"props": [
|
|
||||||
{
|
|
||||||
"n": "label",
|
|
||||||
"t": "string",
|
|
||||||
"r": 1,
|
|
||||||
"d": "标签文案"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "variant",
|
|
||||||
"t": "'default' | 'success' | 'warning' | 'danger' | 'info'",
|
|
||||||
"d": "语义色;缺省 default",
|
|
||||||
"df": "default"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "closable",
|
|
||||||
"t": "boolean",
|
|
||||||
"d": "显示 × 关闭按钮;缺省 false(纯展示)",
|
|
||||||
"df": "false"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "onClose",
|
|
||||||
"t": "() => void",
|
|
||||||
"k": 1,
|
|
||||||
"d": "关闭回调:调用方从数据源移除该项后重新渲染"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"example": "tag({ label: '前端', variant: 'info', closable: true, onClose: () => remove('前端') })",
|
|
||||||
"steps": [
|
|
||||||
"纯展示用 badge,需要可移除的标签(筛选项 / 已选条件)用 tag",
|
|
||||||
"按语义选 variant:info 中性标记、success 通过、warning 待处理、danger 已失效",
|
|
||||||
"closable: true 时必须配 onClose:把数据源里的该项删掉再重渲染(组件无状态,不自行摘除节点)",
|
|
||||||
"组合用法:容器里用 renderList 按信号数组渲染 tag 列表,删除即写回信号",
|
|
||||||
"标签是内联元素,可直接排进 .row 等容器,自动换行由容器决定"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "avatar",
|
|
||||||
"title": "头像 Avatar",
|
|
||||||
"demo": "sec-avatar",
|
|
||||||
"props": [
|
|
||||||
{
|
|
||||||
"n": "name",
|
|
||||||
"t": "string",
|
|
||||||
"r": 1,
|
|
||||||
"d": "名称:生成占位首字,并作为图片 alt 与悬停标题"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "src",
|
|
||||||
"t": "string",
|
|
||||||
"d": "图片地址;缺省用首字占位"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "size",
|
|
||||||
"t": "number",
|
|
||||||
"d": "直径像素;缺省 32",
|
|
||||||
"df": "32"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "variant",
|
|
||||||
"t": "'default' | 'success' | 'warning' | 'danger'",
|
|
||||||
"d": "占位底色语义档;缺省 default",
|
|
||||||
"df": "default"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"example": "avatar({ name: '张三' }); avatar({ name: 'Alice', size: 40, variant: 'success' })",
|
|
||||||
"steps": [
|
|
||||||
"只传 name:用名称首字做占位(中文取首字、英文大写),底色走 accent 淡化,暗色自动适配",
|
|
||||||
"有图片就传 src:渲染 img 并用 name 作 alt,读屏语义完整;图片缺失时由浏览器 alt 兜底",
|
|
||||||
"尺寸用 size(像素)控制,头像不参与布局拉伸(flex:none),可安全排进 .row 与表格单元格",
|
|
||||||
"需要区分类别(在线/告警)时用 variant 换底色档,颜色一律来自令牌"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -660,198 +344,16 @@
|
|||||||
"r": 1,
|
"r": 1,
|
||||||
"d": "提示内容"
|
"d": "提示内容"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"n": "title",
|
|
||||||
"t": "string",
|
|
||||||
"d": "加粗标题行;缺省只渲染正文"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"n": "variant",
|
"n": "variant",
|
||||||
"t": "'info' | 'success' | 'warning' | 'danger'",
|
"t": "'info' | 'success' | 'warning' | 'danger'",
|
||||||
"d": "语义色;缺省 info",
|
"d": "语义色;缺省 info"
|
||||||
"df": "info"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "closable",
|
|
||||||
"t": "boolean",
|
|
||||||
"d": "显示 × 关闭按钮,退场后从 DOM 移除;缺省 false",
|
|
||||||
"df": "false"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "onClose",
|
|
||||||
"t": "() => void",
|
|
||||||
"k": 1,
|
|
||||||
"d": "关闭回调,供调用方同步业务状态"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"example": "alert({ message: '操作可能存在风险', variant: 'warning' })",
|
"example": "alert({ message: '操作可能存在风险', variant: 'warning' })",
|
||||||
"steps": [
|
"steps": [
|
||||||
"按信息级别选 variant:info 提示、success 成功、warning 警告、danger 错误",
|
"按信息级别选 variant:info 提示、success 成功、warning 警告、danger 错误",
|
||||||
"传入 message 即可,入场自带下滑动画",
|
"传入 message 即可,入场自带下滑动画"
|
||||||
"需要可关闭或带操作按钮时,外层用 h() 组合 button 自行拼装,alert 只负责语义底色",
|
|
||||||
"需要用户可自行消除的提示传 closable: true:点 × 淡出并从 DOM 移除,配合 onClose 同步业务状态",
|
|
||||||
"有标题与正文两层信息时传 title(加粗首行),正文仍走 message"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "skeleton",
|
|
||||||
"title": "骨架屏 Skeleton",
|
|
||||||
"demo": "sec-skeleton",
|
|
||||||
"props": [
|
|
||||||
{
|
|
||||||
"n": "variant",
|
|
||||||
"t": "'text' | 'rect' | 'circle'",
|
|
||||||
"d": "占位形状;缺省 rect(整块矩形)",
|
|
||||||
"df": "rect"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "rows",
|
|
||||||
"t": "number",
|
|
||||||
"d": "text 形状的行数;缺省 3,末行自动缩短模拟段落",
|
|
||||||
"df": "3"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"example": "skeleton({ variant: 'text', rows: 3 }); skeleton({ variant: 'circle' }); skeleton({ variant: 'rect' })",
|
|
||||||
"steps": [
|
|
||||||
"数据加载期间用 skeleton 顶替真实内容,加载完成后整体替换",
|
|
||||||
"文本段落用 variant: 'text' + rows 控制行数,头像用 'circle',卡片/图片区域用 'rect'",
|
|
||||||
"占位块自带扫光动画,reduced-motion 用户自动静态化为纯灰块",
|
|
||||||
"组合布局:skeleton 是普通 DOM,可放进 card / row 等任意容器"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "toast",
|
|
||||||
"title": "消息提示 Toast",
|
|
||||||
"demo": "sec-toast",
|
|
||||||
"props": [
|
|
||||||
{
|
|
||||||
"n": "message",
|
|
||||||
"t": "string",
|
|
||||||
"r": 1,
|
|
||||||
"d": "提示文案"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "duration",
|
|
||||||
"t": "number",
|
|
||||||
"d": "自动消失毫秒;缺省 3000",
|
|
||||||
"df": "3000"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "closable",
|
|
||||||
"t": "boolean",
|
|
||||||
"d": "显示关闭按钮,点 × 立即收起;缺省 false(仅可关闭的 toast 接收指针事件,其余保持穿透)",
|
|
||||||
"df": "false"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "variant",
|
|
||||||
"t": "'info' | 'success' | 'warning' | 'danger'",
|
|
||||||
"d": "语义色;缺省 info(toast.error 对应 danger)",
|
|
||||||
"df": "info"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"example": "toast.success('保存成功'); toast.error('网络请求失败', 5000)",
|
|
||||||
"steps": [
|
|
||||||
"直接调用语义工厂:toast.info / success / warning / error(message, duration?),无需先建容器",
|
|
||||||
"不传 duration 时默认停留 3 秒后自动淡出并移除节点",
|
|
||||||
"多条消息在右上角自动纵向堆叠,互不遮挡",
|
|
||||||
"需要完整入参(含 variant)时用 showToast(props);组件挂载容器由内部懒创建,样式随 ui.css 一起引入",
|
|
||||||
"同屏最多 4 条,超出自动挤掉最早一条;需要用户主动确认的消息用 showToast({ closable: true }) 给个 ×"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "tooltip",
|
|
||||||
"title": "悬浮提示 Tooltip",
|
|
||||||
"demo": "sec-tooltip",
|
|
||||||
"props": [
|
|
||||||
{
|
|
||||||
"n": "content",
|
|
||||||
"t": "string",
|
|
||||||
"r": 1,
|
|
||||||
"d": "提示文案(纯文本)"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "placement",
|
|
||||||
"t": "'top' | 'bottom'",
|
|
||||||
"d": "气泡方位;缺省 top(触发器上方)",
|
|
||||||
"df": "top"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "delay",
|
|
||||||
"t": "number",
|
|
||||||
"d": "出场延迟毫秒;缺省 150,传 0 立即显示",
|
|
||||||
"df": "150"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "trigger",
|
|
||||||
"t": "HTMLElement",
|
|
||||||
"r": 1,
|
|
||||||
"d": "第二个参数:被包裹的触发器元素(按钮 / 输入框等任意 DOM)"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"example": "tooltip({ content: '保存后立即生效' }, button({ label: '保存', onClick: save }))",
|
|
||||||
"steps": [
|
|
||||||
"先创建触发器元素,如 const btn = button({ label: '保存' })",
|
|
||||||
"用 tooltip(props, trigger) 包一层:返回带气泡的包裹元素,原触发器仍是独立节点",
|
|
||||||
"鼠标移入或键盘聚焦约 150ms 后出场,移出 / 失焦立即收起;按 Esc 可立刻关闭",
|
|
||||||
"气泡默认在上方,空间不足或需要避让时传 placement: 'bottom'",
|
|
||||||
"气泡 pointer-events: none 不抢指针;判读依赖 aria-describedby + role='tooltip',读屏可读",
|
|
||||||
"只做展示:不要在气泡里放可点击元素,需要交互请用 modal 或自绘弹层"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "popover",
|
|
||||||
"title": "气泡卡片 Popover",
|
|
||||||
"demo": "sec-popover",
|
|
||||||
"props": [
|
|
||||||
{
|
|
||||||
"n": "content",
|
|
||||||
"t": "unknown",
|
|
||||||
"r": 1,
|
|
||||||
"d": "弹层内容:字符串 / DOM 节点 / 响应式函数,可放按钮等交互元素"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "title",
|
|
||||||
"t": "string",
|
|
||||||
"d": "弹层标题;缺省不渲染标题行"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "placement",
|
|
||||||
"t": "'top' | 'bottom'",
|
|
||||||
"d": "弹层方位;缺省 bottom(触发器下方)",
|
|
||||||
"df": "bottom"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "trigger",
|
|
||||||
"t": "'click' | 'hover'",
|
|
||||||
"d": "触发方式;缺省 click",
|
|
||||||
"df": "click"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "delay",
|
|
||||||
"t": "number",
|
|
||||||
"d": "hover 模式出场延迟毫秒;缺省 150",
|
|
||||||
"df": "150"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "onOpenChange",
|
|
||||||
"t": "(open: boolean) => void",
|
|
||||||
"k": 1,
|
|
||||||
"d": "开合回调"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "triggerEl(第二个参数)",
|
|
||||||
"t": "HTMLElement",
|
|
||||||
"r": 1,
|
|
||||||
"d": "第二个参数:被包裹的触发器元素"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"example": "popover({ title: '筛选条件', content: h('div', null, '任意可交互内容') }, button({ label: '筛选' }))",
|
|
||||||
"steps": [
|
|
||||||
"创建触发器元素,再用 popover(props, trigger) 包一层:返回带弹层的包裹元素",
|
|
||||||
"点击触发器开合;点弹层外部或按 Esc 收起(Esc 后焦点回触发器)",
|
|
||||||
"content 里可以放 button / checkbox 等交互元素(tooltip 只能纯文本),点击弹层内部不会误关",
|
|
||||||
"hover 场景传 trigger: 'hover' + delay 控制多快出场;placement: 'top' 让弹层往上翻",
|
|
||||||
"需要感知开合做副作用(如懒加载数据)时用 onOpenChange"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -862,15 +364,13 @@
|
|||||||
{
|
{
|
||||||
"n": "size",
|
"n": "size",
|
||||||
"t": "number",
|
"t": "number",
|
||||||
"d": "直径像素;缺省 18",
|
"d": "直径像素;缺省 18"
|
||||||
"df": "18"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"example": "spinner(); spinner({ size: 26 })",
|
"example": "spinner(); spinner({ size: 26 })",
|
||||||
"steps": [
|
"steps": [
|
||||||
"数据加载时插入 spinner(),加载完成移除",
|
"数据加载时插入 spinner(),加载完成移除",
|
||||||
"用 size 匹配上下文尺寸(按钮内用小号,整页 loading 用大号)",
|
"用 size 匹配上下文尺寸(按钮内用小号,整页 loading 用大号)"
|
||||||
"与 skeleton 分工:已知内容结构用 skeleton 占位,只有『在跑』的短等待用 spinner"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -881,23 +381,21 @@
|
|||||||
{
|
{
|
||||||
"n": "value",
|
"n": "value",
|
||||||
"t": "Signal<number>",
|
"t": "Signal<number>",
|
||||||
"d": "当前进度 0-100 信号,变化自动平滑过渡(越界钳制)"
|
"r": 1,
|
||||||
|
"d": "进度 0-100 信号,变化自动平滑过渡"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"n": "showText",
|
"n": "showText",
|
||||||
"t": "boolean",
|
"t": "boolean",
|
||||||
"d": "是否显示百分比文字;缺省 true",
|
"d": "是否显示百分比文字;缺省 true"
|
||||||
"df": "true"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"example": "const pct = signal(0); progress({ value: pct }); setInterval(() => pct((pct() + 5) % 105), 400)",
|
"example": "const pct = signal(0); progress({ value: pct }); setInterval(() => pct((pct() + 5) % 105), 400)",
|
||||||
"steps": [
|
"steps": [
|
||||||
"时长未知的加载直接 progress() 不传 value:填充块匀速往复,不显示百分比,读屏读作进行中",
|
|
||||||
"创建数值信号 pct = signal(0),范围 0-100(越界自动钳制)",
|
"创建数值信号 pct = signal(0),范围 0-100(越界自动钳制)",
|
||||||
"传入 value,填充宽度与百分比文字随信号平滑过渡",
|
"传入 value,填充宽度与百分比文字随信号平滑过渡",
|
||||||
"进行中填充带流动条纹,到 100% 自动变 success 色并停止条纹",
|
"进行中填充带流动条纹,到 100% 自动变 success 色并停止条纹",
|
||||||
"不需要百分比文字时传 showText: false",
|
"不需要百分比文字时传 showText: false"
|
||||||
"定量进度用 value 信号:满格自动切 success 色并停止条纹,role=progressbar + aria-valuenow 同步"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -931,45 +429,6 @@
|
|||||||
"键盘可访问:Tab 聚焦激活页签,←→ 在启用页签间环形移动"
|
"键盘可访问:Tab 聚焦激活页签,←→ 在启用页签间环形移动"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "pagination",
|
|
||||||
"title": "分页器 Pagination",
|
|
||||||
"demo": "sec-table",
|
|
||||||
"props": [
|
|
||||||
{
|
|
||||||
"n": "page",
|
|
||||||
"t": "Signal<number>",
|
|
||||||
"r": 1,
|
|
||||||
"d": "当前页码信号,双向绑定:外部赋值自动翻页,点击页码写回信号"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "pageSize",
|
|
||||||
"t": "number",
|
|
||||||
"r": 1,
|
|
||||||
"d": "每页条数"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "total",
|
|
||||||
"t": "number",
|
|
||||||
"r": 1,
|
|
||||||
"d": "总条数"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "onChange",
|
|
||||||
"t": "(page: number) => void",
|
|
||||||
"k": 1,
|
|
||||||
"d": "翻页回调"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"example": "const page = signal(1); pagination({ page, pageSize: 3, total: 24, onChange: (p) => console.log(p) })",
|
|
||||||
"steps": [
|
|
||||||
"创建页码信号 page = signal(1),与 pageSize / total 一起传入",
|
|
||||||
"页码 ≤7 全部展开;更多时只保留首尾与当前页 ±1,缺口折叠为省略号",
|
|
||||||
"首末页自动禁用 ‹ › 翻页按钮,当前页禁点并标记 aria-current",
|
|
||||||
"与 dataTable 联动:把同一个 page 信号传给 dataTable 的 page 参数,两处翻页自动同步",
|
|
||||||
"键盘可达:全部是原生 button,Tab 聚焦后 Enter / Space 触发"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "modal",
|
"id": "modal",
|
||||||
"title": "弹窗 Modal",
|
"title": "弹窗 Modal",
|
||||||
@@ -987,17 +446,6 @@
|
|||||||
"r": 1,
|
"r": 1,
|
||||||
"d": "标题"
|
"d": "标题"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"n": "size",
|
|
||||||
"t": "'sm' | 'md' | 'lg'",
|
|
||||||
"d": "宽度档位:sm 340px / md 420px / lg 640px;缺省 md",
|
|
||||||
"df": "md"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "footer",
|
|
||||||
"t": "unknown",
|
|
||||||
"d": "底部操作区内容(常见为按钮组,右对齐);缺省不渲染"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"n": "onClose",
|
"n": "onClose",
|
||||||
"t": "() => void",
|
"t": "() => void",
|
||||||
@@ -1015,63 +463,7 @@
|
|||||||
"创建布尔信号 open = signal(false),弹窗初始隐藏",
|
"创建布尔信号 open = signal(false),弹窗初始隐藏",
|
||||||
"把 open 传给 modal,其余参数为弹窗内容子节点",
|
"把 open 传给 modal,其余参数为弹窗内容子节点",
|
||||||
"打开:open(true);关闭:open(false),点遮罩或 × 会自动写回 false",
|
"打开:open(true);关闭:open(false),点遮罩或 × 会自动写回 false",
|
||||||
"入场自带遮罩淡入 + 弹窗上浮动画",
|
"入场自带遮罩淡入 + 弹窗上浮动画"
|
||||||
"底部操作按钮放进 footer(如取消/确定),需要更宽内容用 size: 'lg',窄确认框用 'sm'"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "drawer",
|
|
||||||
"title": "抽屉 Drawer",
|
|
||||||
"demo": "sec-drawer",
|
|
||||||
"props": [
|
|
||||||
{
|
|
||||||
"n": "title",
|
|
||||||
"t": "string",
|
|
||||||
"r": 1,
|
|
||||||
"d": "标题"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "open",
|
|
||||||
"t": "Signal<boolean>",
|
|
||||||
"r": 1,
|
|
||||||
"d": "开关信号:true 滑出 / false 收起"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "placement",
|
|
||||||
"t": "'left' | 'right'",
|
|
||||||
"d": "滑出方向;缺省 right",
|
|
||||||
"df": "right"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "width",
|
|
||||||
"t": "number",
|
|
||||||
"d": "面板宽度像素(窄屏最多 92vw);缺省 320",
|
|
||||||
"df": "320"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "footer",
|
|
||||||
"t": "unknown",
|
|
||||||
"d": "底部操作区内容;缺省不渲染"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "onClose",
|
|
||||||
"t": "() => void",
|
|
||||||
"k": 1,
|
|
||||||
"d": "关闭回调(点遮罩 / × / Esc 触发)"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "...children",
|
|
||||||
"t": "unknown[]",
|
|
||||||
"d": "面板内容子节点"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"example": "const open = signal(false); drawer({ title: '详情', open, width: 360, onClose: () => open(false) }, h('p', null, '内容'))",
|
|
||||||
"steps": [
|
|
||||||
"创建布尔信号 open = signal(false),传给 drawer;其余参数为面板内容子节点",
|
|
||||||
"打开后从侧边滑出:placement: 'right'(默认)或 'left',width 控制宽度(窄屏自动收到 92vw)",
|
|
||||||
"点遮罩、× 或按 Esc 关闭并写回 false;打开时焦点落在面板首个可交互元素",
|
|
||||||
"底部操作按钮放进 footer;与 modal 的分工:modal 是强打断的确认,drawer 保留主视图上下文做详情/编辑",
|
|
||||||
"面板内容与 modal 一样接受任意 DOM 与响应式函数"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1082,8 +474,7 @@
|
|||||||
{
|
{
|
||||||
"n": "layout",
|
"n": "layout",
|
||||||
"t": "'vertical' | 'horizontal'",
|
"t": "'vertical' | 'horizontal'",
|
||||||
"d": "布局方向;缺省 vertical(horizontal 为标签左置)",
|
"d": "布局方向;缺省 vertical(horizontal 为标签左置)"
|
||||||
"df": "vertical"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"n": "submitText",
|
"n": "submitText",
|
||||||
@@ -1126,8 +517,7 @@
|
|||||||
{
|
{
|
||||||
"n": "(formItem)validateOn",
|
"n": "(formItem)validateOn",
|
||||||
"t": "'input' | 'blur' | 'submit'",
|
"t": "'input' | 'blur' | 'submit'",
|
||||||
"d": "校验时机;缺省 blur(输入时自动清已报错误)",
|
"d": "校验时机;缺省 blur(输入时自动清已报错误)"
|
||||||
"df": "blur"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"example": "const name = signal(''); const f = form({ onSubmit: submit }, [formItem({ label: '用户名', required: true, value: name, rules: [{ min: 2, max: 12 }] }, textField({ value: name }))])",
|
"example": "const name = signal(''); const f = form({ onSubmit: submit }, [formItem({ label: '用户名', required: true, value: name, rules: [{ min: 2, max: 12 }] }, textField({ value: name }))])",
|
||||||
@@ -1137,9 +527,7 @@
|
|||||||
"把句柄数组交给 form:form({ onSubmit }, items),form 渲染全部字段 + 提交/重置按钮",
|
"把句柄数组交给 form:form({ onSubmit }, items),form 渲染全部字段 + 提交/重置按钮",
|
||||||
"提交时逐项校验:全过触发 onSubmit;有错则标红控件、显示错误文案并聚焦首个出错项",
|
"提交时逐项校验:全过触发 onSubmit;有错则标红控件、显示错误文案并聚焦首个出错项",
|
||||||
"规则语义:required 非空(trim)、min/max 长度、pattern 正则、validator 自定义(true 通过 / 字符串为错误)",
|
"规则语义:required 非空(trim)、min/max 长度、pattern 正则、validator 自定义(true 通过 / 字符串为错误)",
|
||||||
"非必填字段为空时跳过其余规则;Enter 键触发表单原生 submit(已拦截走校验流程)",
|
"非必填字段为空时跳过其余规则;Enter 键触发表单原生 submit(已拦截走校验流程)"
|
||||||
"错误态覆盖包裹式控件:标红作用于控件根与内部原生控件(含自绘 select 的触发器),并同步 aria-invalid,读屏与视觉一致",
|
|
||||||
"触发时机用 input / change / focusout(均会冒泡),因此 checkbox / select / 单选组这类包裹式控件同样能失焦校验"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1157,7 +545,7 @@
|
|||||||
"n": "columns",
|
"n": "columns",
|
||||||
"t": "ColumnDef<T>[]",
|
"t": "ColumnDef<T>[]",
|
||||||
"r": 1,
|
"r": 1,
|
||||||
"d": "列定义:内核字段 { key, header?, sortable?, width? } + 皮肤字段 { align?, format? };width 数字为固定像素,'flex' 自适应"
|
"d": "列定义:{ key, header?, sortable? }"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"n": "filterKey",
|
"n": "filterKey",
|
||||||
@@ -1167,44 +555,22 @@
|
|||||||
{
|
{
|
||||||
"n": "pageSize",
|
"n": "pageSize",
|
||||||
"t": "number",
|
"t": "number",
|
||||||
"d": "每页行数;缺省 5",
|
"d": "每页行数;缺省 5"
|
||||||
"df": "5"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"n": "selectable",
|
"n": "selectable",
|
||||||
"t": "boolean",
|
"t": "boolean",
|
||||||
"d": "启用行选择列:全选三态(含半选)、行 checkbox、已选计数;缺省 false",
|
"d": "启用行选择列:全选三态(含半选)、行 checkbox、已选计数;缺省 false"
|
||||||
"df": "false"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "page",
|
|
||||||
"t": "Signal<number>",
|
|
||||||
"d": "受控页码信号:与内核分页双向同步,传同一个信号给独立 pagination 组件即可联动"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "loading",
|
|
||||||
"t": "boolean | Signal<boolean>",
|
|
||||||
"d": "加载态:为真时数据行换成骨架行(行数随 pageSize),筛选输入与翻页按钮临时禁用,容器带 aria-busy"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"n": "selected",
|
|
||||||
"t": "Signal<RowKey[]>",
|
|
||||||
"d": "受控选中行信号:与内核选择双向同步(内容比较,不来回派发),供批量操作读取/改写"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"example": "dataTable({ data: users, selectable: true, columns: [{ key: 'name', header: '姓名', sortable: true }], pageSize: 5 })",
|
"example": "dataTable({ data: users, selectable: true, columns: [{ key: 'name', header: '姓名', sortable: true }], pageSize: 5 })",
|
||||||
"steps": [
|
"steps": [
|
||||||
"准备行数据,确保每行有唯一 id(跨页选择与刷新不丢行靠它)",
|
"准备行数据,确保每行有唯一 id(跨页选择与刷新不丢行靠它)",
|
||||||
"定义 columns:sortable: true 的列表头可点击排序(升 → 降 → 取消循环),标记箭头 ↑↓↕",
|
"定义 columns:sortable: true 的列表头可点击排序(升 → 降 → 取消循环),标记箭头 ↑↓↕",
|
||||||
"列展示细节用皮肤字段:align 控制表头与单元格对齐,format(value, row) 返回纯文本(走 textContent,不需要转义)",
|
|
||||||
"列宽用内核字段 width:数字(像素)固定该列,'flex' 或不设则按内容自适应",
|
|
||||||
"selectable: true 开启行选择:表头全选框带 indeterminate 半选态,工具栏显示已选计数",
|
"selectable: true 开启行选择:表头全选框带 indeterminate 半选态,工具栏显示已选计数",
|
||||||
"选中行用低视觉比重样式(淡色底 + 弱化文字),选择状态存 rowKey,跨页不丢失",
|
"选中行用低视觉比重样式(淡色底 + 弱化文字),选择状态存 rowKey,跨页不丢失",
|
||||||
"工具栏输入框即筛选,输入即时过滤并回第 1 页;无匹配时显示空状态行",
|
"工具栏输入框即筛选,输入即时过滤并回第 1 页;无匹配时显示空状态行",
|
||||||
"表头粘性固定(滚动容器 max-height 420px),斑马纹 + 悬停高亮由 CSS 提供",
|
"表头粘性固定(滚动容器 max-height 420px),斑马纹 + 悬停高亮由 CSS 提供"
|
||||||
"数据在途时传 loading(布尔或信号):表格体换成骨架行、筛选与翻页禁用,返回后置回 false 即恢复",
|
|
||||||
"需要独立分页器联动时传 page 信号:const page = signal(1),dataTable({ page }) 与 pagination({ page }) 共用",
|
|
||||||
"需要批量操作时传 selected 信号:内部勾选实时写回,外部置空或赋值即同步整表(跨页保留,按 rowKey 存)"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
+8
-738
@@ -28,7 +28,7 @@
|
|||||||
.ua-btn-danger {
|
.ua-btn-danger {
|
||||||
background: var(--ua-color-danger);
|
background: var(--ua-color-danger);
|
||||||
border-color: var(--ua-color-danger);
|
border-color: var(--ua-color-danger);
|
||||||
color: var(--ua-color-bg);
|
color: #fff;
|
||||||
}
|
}
|
||||||
.ua-input {
|
.ua-input {
|
||||||
min-width: 200px;
|
min-width: 200px;
|
||||||
@@ -113,13 +113,6 @@
|
|||||||
width: 36px;
|
width: 36px;
|
||||||
text-align: center !important;
|
text-align: center !important;
|
||||||
}
|
}
|
||||||
/* 列对齐:表头与单元格共用,由列定义的 align 决定 */
|
|
||||||
.ua-align-center {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.ua-align-right {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
.ua-empty-row td {
|
.ua-empty-row td {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: var(--ua-color-text-muted);
|
color: var(--ua-color-text-muted);
|
||||||
@@ -207,7 +200,7 @@
|
|||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: var(--ua-color-bg);
|
background: #fff;
|
||||||
transition: left 0.2s;
|
transition: left 0.2s;
|
||||||
}
|
}
|
||||||
.ua-switch-input:checked + .ua-switch-track {
|
.ua-switch-input:checked + .ua-switch-track {
|
||||||
@@ -315,364 +308,6 @@
|
|||||||
background: color-mix(in srgb, var(--ua-color-accent) 14%, transparent);
|
background: color-mix(in srgb, var(--ua-color-accent) 14%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* —— 下拉菜单 —— */
|
|
||||||
.ua-menu {
|
|
||||||
position: relative;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
.ua-menu-trigger {
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
.ua-menu-caret {
|
|
||||||
font-size: 10px;
|
|
||||||
opacity: 0.7;
|
|
||||||
transition: transform 0.2s;
|
|
||||||
}
|
|
||||||
.ua-menu.ua-open .ua-menu-caret {
|
|
||||||
transform: rotate(180deg);
|
|
||||||
}
|
|
||||||
.ua-menu-pop {
|
|
||||||
position: absolute;
|
|
||||||
z-index: 20;
|
|
||||||
top: calc(100% + 4px);
|
|
||||||
left: 0;
|
|
||||||
min-width: 160px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 2px;
|
|
||||||
padding: 4px;
|
|
||||||
border: 1px solid var(--ua-color-border);
|
|
||||||
border-radius: var(--ua-radius-md);
|
|
||||||
background: var(--ua-color-surface);
|
|
||||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.14);
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(-4px);
|
|
||||||
visibility: hidden;
|
|
||||||
transition: opacity 0.15s ease, transform 0.15s ease, visibility 0.15s;
|
|
||||||
}
|
|
||||||
.ua-menu-pop.is-end {
|
|
||||||
left: auto;
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
.ua-menu-pop.ua-open {
|
|
||||||
opacity: 1;
|
|
||||||
transform: none;
|
|
||||||
visibility: visible;
|
|
||||||
}
|
|
||||||
.ua-menu-item {
|
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: var(--ua-space-4);
|
|
||||||
text-align: left;
|
|
||||||
padding: 6px 10px;
|
|
||||||
border: none;
|
|
||||||
border-radius: var(--ua-radius-sm);
|
|
||||||
background: none;
|
|
||||||
color: var(--ua-color-text);
|
|
||||||
font-size: var(--ua-font-size-md);
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background 0.12s;
|
|
||||||
}
|
|
||||||
.ua-menu-item:hover:not(:disabled) {
|
|
||||||
background: color-mix(in srgb, var(--ua-color-accent) 8%, transparent);
|
|
||||||
}
|
|
||||||
.ua-menu-item.is-danger {
|
|
||||||
color: var(--ua-color-danger);
|
|
||||||
}
|
|
||||||
.ua-menu-item.is-danger:hover:not(:disabled) {
|
|
||||||
background: color-mix(in srgb, var(--ua-color-danger) 10%, transparent);
|
|
||||||
}
|
|
||||||
.ua-menu-label {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
.ua-menu-key {
|
|
||||||
flex: none;
|
|
||||||
color: var(--ua-color-text-muted);
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: var(--ua-font-size-sm);
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
.ua-menu-item:disabled {
|
|
||||||
opacity: 0.45;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
.ua-menu-divider {
|
|
||||||
height: 1px;
|
|
||||||
margin: 4px 2px;
|
|
||||||
background: var(--ua-color-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* —— 面包屑 —— */
|
|
||||||
.ua-breadcrumb {
|
|
||||||
font-size: var(--ua-font-size-md);
|
|
||||||
}
|
|
||||||
.ua-crumb-list {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--ua-space-2);
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
.ua-crumb-item {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--ua-space-2);
|
|
||||||
}
|
|
||||||
.ua-crumb-link {
|
|
||||||
padding: 0;
|
|
||||||
border: none;
|
|
||||||
background: none;
|
|
||||||
color: var(--ua-color-accent);
|
|
||||||
font-size: inherit;
|
|
||||||
text-decoration: none;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: opacity 0.15s;
|
|
||||||
}
|
|
||||||
.ua-crumb-link:hover {
|
|
||||||
opacity: 0.75;
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
.ua-crumb-link:focus-visible {
|
|
||||||
outline: none;
|
|
||||||
border-radius: var(--ua-radius-sm);
|
|
||||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--ua-color-accent) 18%, transparent);
|
|
||||||
}
|
|
||||||
.ua-crumb-current {
|
|
||||||
color: var(--ua-color-text-muted);
|
|
||||||
}
|
|
||||||
.ua-crumb-sep {
|
|
||||||
color: var(--ua-color-text-muted);
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* —— 气泡卡片(可交互弹层,与 tooltip 的分工是 content 里能点) —— */
|
|
||||||
.ua-pop-wrap {
|
|
||||||
position: relative;
|
|
||||||
display: inline-flex;
|
|
||||||
}
|
|
||||||
.ua-pop {
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
z-index: 30;
|
|
||||||
min-width: 200px;
|
|
||||||
max-width: 280px;
|
|
||||||
padding: var(--ua-space-3);
|
|
||||||
border: 1px solid var(--ua-color-border);
|
|
||||||
border-radius: var(--ua-radius-md);
|
|
||||||
background: var(--ua-color-bg);
|
|
||||||
box-shadow: 0 10px 28px rgba(0, 0, 0, 0.16);
|
|
||||||
opacity: 0;
|
|
||||||
visibility: hidden;
|
|
||||||
transition: opacity 0.16s ease, transform 0.16s ease, visibility 0.16s;
|
|
||||||
}
|
|
||||||
.ua-pop-top {
|
|
||||||
bottom: calc(100% + 8px);
|
|
||||||
transform: translateY(4px);
|
|
||||||
}
|
|
||||||
.ua-pop-bottom {
|
|
||||||
top: calc(100% + 8px);
|
|
||||||
transform: translateY(-4px);
|
|
||||||
}
|
|
||||||
.ua-pop.ua-open {
|
|
||||||
opacity: 1;
|
|
||||||
transform: none;
|
|
||||||
visibility: visible;
|
|
||||||
}
|
|
||||||
.ua-pop-title {
|
|
||||||
margin-bottom: var(--ua-space-2);
|
|
||||||
color: var(--ua-color-text);
|
|
||||||
font-size: var(--ua-font-size-lg);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
.ua-pop-body {
|
|
||||||
color: var(--ua-color-text-muted);
|
|
||||||
font-size: var(--ua-font-size-md);
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* —— 折叠面板 —— */
|
|
||||||
.ua-accordion {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
border: 1px solid var(--ua-color-border);
|
|
||||||
border-radius: var(--ua-radius-md);
|
|
||||||
overflow: hidden;
|
|
||||||
background: var(--ua-color-bg);
|
|
||||||
}
|
|
||||||
.ua-acc-item + .ua-acc-item {
|
|
||||||
border-top: 1px solid var(--ua-color-border);
|
|
||||||
}
|
|
||||||
.ua-acc-head {
|
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 10px 12px;
|
|
||||||
border: none;
|
|
||||||
background: none;
|
|
||||||
color: var(--ua-color-text);
|
|
||||||
font-size: var(--ua-font-size-lg);
|
|
||||||
text-align: left;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background 0.15s;
|
|
||||||
}
|
|
||||||
.ua-acc-head:hover:not(:disabled) {
|
|
||||||
background: color-mix(in srgb, var(--ua-color-accent) 6%, transparent);
|
|
||||||
}
|
|
||||||
.ua-acc-head:focus-visible {
|
|
||||||
outline: none;
|
|
||||||
box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--ua-color-accent) 45%, transparent);
|
|
||||||
}
|
|
||||||
.ua-acc-head:disabled {
|
|
||||||
opacity: 0.45;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
.ua-acc-caret {
|
|
||||||
font-size: 10px;
|
|
||||||
color: var(--ua-color-text-muted);
|
|
||||||
transition: transform 0.22s ease;
|
|
||||||
}
|
|
||||||
.ua-acc-item.ua-open .ua-acc-caret {
|
|
||||||
transform: rotate(90deg);
|
|
||||||
}
|
|
||||||
/* 内容区用 grid 行高过渡:0fr → 1fr,无需测量高度 */
|
|
||||||
.ua-acc-body {
|
|
||||||
display: grid;
|
|
||||||
grid-template-rows: 0fr;
|
|
||||||
transition: grid-template-rows 0.24s ease;
|
|
||||||
}
|
|
||||||
.ua-acc-item.ua-open .ua-acc-body {
|
|
||||||
grid-template-rows: 1fr;
|
|
||||||
}
|
|
||||||
.ua-acc-inner {
|
|
||||||
overflow: hidden;
|
|
||||||
padding: 0 12px;
|
|
||||||
color: var(--ua-color-text-muted);
|
|
||||||
font-size: var(--ua-font-size-md);
|
|
||||||
}
|
|
||||||
.ua-acc-item.ua-open .ua-acc-inner {
|
|
||||||
padding-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* —— 头像 —— */
|
|
||||||
.ua-avatar {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
flex: none;
|
|
||||||
border-radius: 50%;
|
|
||||||
overflow: hidden;
|
|
||||||
background: color-mix(in srgb, var(--ua-color-accent) 14%, transparent);
|
|
||||||
color: var(--ua-color-accent);
|
|
||||||
font-size: var(--ua-font-size-md);
|
|
||||||
font-weight: 500;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
.ua-avatar-img {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
.ua-avatar-success {
|
|
||||||
background: color-mix(in srgb, var(--ua-color-success) 16%, transparent);
|
|
||||||
color: var(--ua-color-success);
|
|
||||||
}
|
|
||||||
.ua-avatar-warning {
|
|
||||||
background: color-mix(in srgb, var(--ua-color-warning) 18%, transparent);
|
|
||||||
color: var(--ua-color-warning);
|
|
||||||
}
|
|
||||||
.ua-avatar-danger {
|
|
||||||
background: color-mix(in srgb, var(--ua-color-danger) 16%, transparent);
|
|
||||||
color: var(--ua-color-danger);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* —— 标签(可关闭) —— */
|
|
||||||
.ua-tag {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
padding: 2px 8px;
|
|
||||||
border: 1px solid var(--ua-color-border);
|
|
||||||
border-radius: 999px;
|
|
||||||
background: var(--ua-color-surface);
|
|
||||||
color: var(--ua-color-text);
|
|
||||||
font-size: var(--ua-font-size-sm);
|
|
||||||
line-height: 1.6;
|
|
||||||
transition: border-color 0.15s, background 0.15s;
|
|
||||||
}
|
|
||||||
.ua-tag-success {
|
|
||||||
border-color: color-mix(in srgb, var(--ua-color-success) 55%, transparent);
|
|
||||||
background: color-mix(in srgb, var(--ua-color-success) 10%, var(--ua-color-bg));
|
|
||||||
}
|
|
||||||
.ua-tag-warning {
|
|
||||||
border-color: color-mix(in srgb, var(--ua-color-warning) 55%, transparent);
|
|
||||||
background: color-mix(in srgb, var(--ua-color-warning) 12%, var(--ua-color-bg));
|
|
||||||
}
|
|
||||||
.ua-tag-danger {
|
|
||||||
border-color: color-mix(in srgb, var(--ua-color-danger) 55%, transparent);
|
|
||||||
background: color-mix(in srgb, var(--ua-color-danger) 10%, var(--ua-color-bg));
|
|
||||||
}
|
|
||||||
.ua-tag-info {
|
|
||||||
border-color: color-mix(in srgb, var(--ua-color-accent) 55%, transparent);
|
|
||||||
background: color-mix(in srgb, var(--ua-color-accent) 10%, var(--ua-color-bg));
|
|
||||||
}
|
|
||||||
.ua-tag-close {
|
|
||||||
padding: 0;
|
|
||||||
border: none;
|
|
||||||
background: none;
|
|
||||||
color: var(--ua-color-text-muted);
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 1;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: color 0.15s, transform 0.15s;
|
|
||||||
}
|
|
||||||
.ua-tag-close:hover {
|
|
||||||
color: var(--ua-color-text);
|
|
||||||
transform: scale(1.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* —— 滑块 —— */
|
|
||||||
.ua-slider {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--ua-space-3);
|
|
||||||
min-width: 180px;
|
|
||||||
}
|
|
||||||
.ua-slider-input {
|
|
||||||
flex: 1;
|
|
||||||
height: 4px;
|
|
||||||
margin: 0;
|
|
||||||
border-radius: var(--ua-radius-sm);
|
|
||||||
background: color-mix(in srgb, var(--ua-color-border) 70%, transparent);
|
|
||||||
accent-color: var(--ua-color-accent);
|
|
||||||
cursor: pointer;
|
|
||||||
transition: opacity 0.15s;
|
|
||||||
}
|
|
||||||
.ua-slider-input:focus-visible {
|
|
||||||
outline: none;
|
|
||||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--ua-color-accent) 20%, transparent);
|
|
||||||
}
|
|
||||||
.ua-slider-input:disabled {
|
|
||||||
opacity: 0.45;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
.ua-slider-value {
|
|
||||||
min-width: 32px;
|
|
||||||
color: var(--ua-color-text-muted);
|
|
||||||
font-size: var(--ua-font-size-md);
|
|
||||||
font-variant-numeric: tabular-nums;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* —— 多行输入 —— */
|
/* —— 多行输入 —— */
|
||||||
.ua-textarea {
|
.ua-textarea {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -701,25 +336,21 @@
|
|||||||
}
|
}
|
||||||
.ua-badge-success {
|
.ua-badge-success {
|
||||||
background: var(--ua-color-success);
|
background: var(--ua-color-success);
|
||||||
color: var(--ua-color-bg);
|
color: #fff;
|
||||||
}
|
}
|
||||||
.ua-badge-warning {
|
.ua-badge-warning {
|
||||||
background: var(--ua-color-warning);
|
background: var(--ua-color-warning);
|
||||||
color: var(--ua-color-bg);
|
color: #fff;
|
||||||
}
|
}
|
||||||
.ua-badge-danger {
|
.ua-badge-danger {
|
||||||
background: var(--ua-color-danger);
|
background: var(--ua-color-danger);
|
||||||
color: var(--ua-color-bg);
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* —— 提示条 —— */
|
/* —— 提示条 —— */
|
||||||
.ua-alert {
|
.ua-alert {
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: var(--ua-space-2);
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
transition: opacity 0.2s ease, transform 0.2s ease;
|
|
||||||
border-radius: var(--ua-radius-md);
|
border-radius: var(--ua-radius-md);
|
||||||
border: 1px solid;
|
border: 1px solid;
|
||||||
font-size: var(--ua-font-size-md);
|
font-size: var(--ua-font-size-md);
|
||||||
@@ -731,33 +362,6 @@
|
|||||||
transform: translateY(-6px);
|
transform: translateY(-6px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.ua-alert-body {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
.ua-alert-title {
|
|
||||||
margin-bottom: 2px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
.ua-alert-close {
|
|
||||||
flex: none;
|
|
||||||
padding: 0 2px;
|
|
||||||
border: none;
|
|
||||||
background: none;
|
|
||||||
color: var(--ua-color-text-muted);
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.4;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: color 0.15s;
|
|
||||||
}
|
|
||||||
.ua-alert-close:hover {
|
|
||||||
color: var(--ua-color-text);
|
|
||||||
}
|
|
||||||
/* 关闭退场:淡出 + 轻微上移,配合 200ms 后摘除节点 */
|
|
||||||
.ua-alert-out {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(-4px);
|
|
||||||
}
|
|
||||||
.ua-alert-info {
|
.ua-alert-info {
|
||||||
border-color: var(--ua-color-accent);
|
border-color: var(--ua-color-accent);
|
||||||
background: color-mix(in srgb, var(--ua-color-accent) 10%, transparent);
|
background: color-mix(in srgb, var(--ua-color-accent) 10%, transparent);
|
||||||
@@ -775,153 +379,6 @@
|
|||||||
background: color-mix(in srgb, var(--ua-color-danger) 12%, transparent);
|
background: color-mix(in srgb, var(--ua-color-danger) 12%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* —— 独立分页器 —— */
|
|
||||||
.ua-pagination {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--ua-space-1);
|
|
||||||
}
|
|
||||||
.ua-page-btn {
|
|
||||||
min-width: 28px;
|
|
||||||
height: 28px;
|
|
||||||
padding: 0 var(--ua-space-2);
|
|
||||||
border: 1px solid var(--ua-color-border);
|
|
||||||
border-radius: var(--ua-radius-sm);
|
|
||||||
background: var(--ua-color-bg);
|
|
||||||
color: var(--ua-color-text);
|
|
||||||
font-size: var(--ua-font-size-md);
|
|
||||||
cursor: pointer;
|
|
||||||
font-variant-numeric: tabular-nums;
|
|
||||||
transition: border-color 0.15s ease, box-shadow 0.15s ease, transform 0.15s ease;
|
|
||||||
}
|
|
||||||
.ua-page-btn:hover:not(:disabled) {
|
|
||||||
border-color: var(--ua-color-accent);
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
|
||||||
.ua-page-btn:focus-visible {
|
|
||||||
outline: none;
|
|
||||||
border-color: var(--ua-color-accent);
|
|
||||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--ua-color-accent) 18%, transparent);
|
|
||||||
}
|
|
||||||
.ua-page-btn:disabled {
|
|
||||||
opacity: 0.45;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
.ua-page-btn.ua-active {
|
|
||||||
border-color: var(--ua-color-accent);
|
|
||||||
background: color-mix(in srgb, var(--ua-color-accent) 10%, transparent); /* 选中态低视觉比重 */
|
|
||||||
color: var(--ua-color-text-muted);
|
|
||||||
}
|
|
||||||
.ua-page-gap {
|
|
||||||
color: var(--ua-color-text-muted);
|
|
||||||
padding: 0 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* —— 骨架屏:占位灰块 + 扫光 —— */
|
|
||||||
.ua-skeleton {
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
border-radius: var(--ua-radius-sm);
|
|
||||||
background: color-mix(in srgb, var(--ua-color-border) 45%, transparent);
|
|
||||||
}
|
|
||||||
.ua-skeleton::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
transform: translateX(-100%);
|
|
||||||
background: linear-gradient(90deg, transparent, color-mix(in srgb, var(--ua-color-bg) 65%, transparent), transparent);
|
|
||||||
animation: ua-shimmer 1.4s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
@keyframes ua-shimmer {
|
|
||||||
to {
|
|
||||||
transform: translateX(100%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.ua-skeleton-text {
|
|
||||||
height: 12px;
|
|
||||||
margin-bottom: var(--ua-space-2);
|
|
||||||
}
|
|
||||||
.ua-skeleton-text.ua-last {
|
|
||||||
width: 60%;
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
.ua-skeleton-rect {
|
|
||||||
width: 100%;
|
|
||||||
height: 80px;
|
|
||||||
border-radius: var(--ua-radius-md);
|
|
||||||
}
|
|
||||||
.ua-skeleton-circle {
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
border-radius: 50%;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
.ua-skeleton-group {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* —— toast 消息提示:右上角堆叠,转瞬即逝 —— */
|
|
||||||
.ua-toast-host {
|
|
||||||
position: fixed;
|
|
||||||
top: var(--ua-space-4);
|
|
||||||
right: var(--ua-space-4);
|
|
||||||
z-index: 50;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-end;
|
|
||||||
gap: var(--ua-space-2);
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
.ua-toast {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--ua-space-3);
|
|
||||||
padding: var(--ua-space-2) var(--ua-space-3);
|
|
||||||
border-radius: var(--ua-radius-md);
|
|
||||||
border: 1px solid;
|
|
||||||
font-size: var(--ua-font-size-md);
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateX(12px);
|
|
||||||
transition: opacity 0.22s ease-out, transform 0.22s ease-out;
|
|
||||||
}
|
|
||||||
.ua-toast.ua-open {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateX(0);
|
|
||||||
}
|
|
||||||
/* 仅带关闭按钮的 toast 接收指针,其余保持穿透不挡页面 */
|
|
||||||
.ua-toast.is-closable {
|
|
||||||
pointer-events: auto;
|
|
||||||
}
|
|
||||||
.ua-toast-close {
|
|
||||||
padding: 0 2px;
|
|
||||||
border: none;
|
|
||||||
background: none;
|
|
||||||
color: var(--ua-color-text-muted);
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: color 0.15s;
|
|
||||||
}
|
|
||||||
.ua-toast-close:hover {
|
|
||||||
color: var(--ua-color-text);
|
|
||||||
}
|
|
||||||
.ua-toast-info {
|
|
||||||
border-color: var(--ua-color-accent);
|
|
||||||
background: color-mix(in srgb, var(--ua-color-accent) 8%, var(--ua-color-bg));
|
|
||||||
}
|
|
||||||
.ua-toast-success {
|
|
||||||
border-color: var(--ua-color-success);
|
|
||||||
background: color-mix(in srgb, var(--ua-color-success) 10%, var(--ua-color-bg));
|
|
||||||
}
|
|
||||||
.ua-toast-warning {
|
|
||||||
border-color: var(--ua-color-warning);
|
|
||||||
background: color-mix(in srgb, var(--ua-color-warning) 10%, var(--ua-color-bg));
|
|
||||||
}
|
|
||||||
.ua-toast-danger {
|
|
||||||
border-color: var(--ua-color-danger);
|
|
||||||
background: color-mix(in srgb, var(--ua-color-danger) 10%, var(--ua-color-bg));
|
|
||||||
}
|
|
||||||
|
|
||||||
/* —— 加载指示器 —— */
|
/* —— 加载指示器 —— */
|
||||||
.ua-spinner {
|
.ua-spinner {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
@@ -973,23 +430,6 @@
|
|||||||
animation: none;
|
animation: none;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
/* 不确定态:固定比例填充块匀速往复,宽度与位移都由动画控制 */
|
|
||||||
.ua-progress-fill.ua-indeterminate {
|
|
||||||
width: 35%;
|
|
||||||
transition: none;
|
|
||||||
animation: ua-indeterminate 1.2s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
.ua-progress-fill.ua-indeterminate::after {
|
|
||||||
animation: none;
|
|
||||||
}
|
|
||||||
@keyframes ua-indeterminate {
|
|
||||||
from {
|
|
||||||
transform: translateX(-100%);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: translateX(285%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@keyframes ua-stripe {
|
@keyframes ua-stripe {
|
||||||
to {
|
to {
|
||||||
background-position: 17px 0;
|
background-position: 17px 0;
|
||||||
@@ -1100,13 +540,8 @@
|
|||||||
.ua-form-error:empty {
|
.ua-form-error:empty {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
/* 校验失败:控件描红 + 危险色光环(root 与包裹式控件的内部触发器一并覆盖) */
|
/* 校验失败:控件描红 + 危险色光环 */
|
||||||
.ua-invalid,
|
.ua-invalid {
|
||||||
.ua-invalid .ua-input,
|
|
||||||
.ua-invalid .ua-textarea,
|
|
||||||
.ua-invalid .ua-select-trigger,
|
|
||||||
.ua-invalid .ua-radio,
|
|
||||||
.ua-invalid .ua-slider-input {
|
|
||||||
border-color: var(--ua-color-danger) !important;
|
border-color: var(--ua-color-danger) !important;
|
||||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--ua-color-danger) 15%, transparent) !important;
|
box-shadow: 0 0 0 3px color-mix(in srgb, var(--ua-color-danger) 15%, transparent) !important;
|
||||||
}
|
}
|
||||||
@@ -1116,82 +551,6 @@
|
|||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* —— 抽屉(侧滑面板) —— */
|
|
||||||
.ua-drawer-overlay {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
z-index: 90;
|
|
||||||
background: rgba(0, 0, 0, 0.36);
|
|
||||||
opacity: 0;
|
|
||||||
visibility: hidden;
|
|
||||||
transition: opacity 0.22s ease, visibility 0.22s;
|
|
||||||
}
|
|
||||||
.ua-drawer-overlay.ua-open {
|
|
||||||
opacity: 1;
|
|
||||||
visibility: visible;
|
|
||||||
}
|
|
||||||
.ua-drawer {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
max-width: 92vw;
|
|
||||||
background: var(--ua-color-surface);
|
|
||||||
border-left: 1px solid var(--ua-color-border);
|
|
||||||
box-shadow: 0 0 40px rgba(0, 0, 0, 0.22);
|
|
||||||
transition: transform 0.24s ease;
|
|
||||||
}
|
|
||||||
.ua-drawer-right {
|
|
||||||
right: 0;
|
|
||||||
transform: translateX(100%);
|
|
||||||
}
|
|
||||||
.ua-drawer-left {
|
|
||||||
left: 0;
|
|
||||||
border-left: none;
|
|
||||||
border-right: 1px solid var(--ua-color-border);
|
|
||||||
transform: translateX(-100%);
|
|
||||||
}
|
|
||||||
.ua-drawer-overlay.ua-open .ua-drawer-right,
|
|
||||||
.ua-drawer-overlay.ua-open .ua-drawer-left {
|
|
||||||
transform: none;
|
|
||||||
}
|
|
||||||
.ua-drawer-head {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: var(--ua-space-2);
|
|
||||||
padding: 12px 16px;
|
|
||||||
border-bottom: 1px solid var(--ua-color-border);
|
|
||||||
}
|
|
||||||
.ua-drawer-title {
|
|
||||||
margin: 0;
|
|
||||||
font-size: var(--ua-font-size-lg);
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
.ua-drawer-close {
|
|
||||||
border: none;
|
|
||||||
background: none;
|
|
||||||
color: var(--ua-color-text-muted);
|
|
||||||
font-size: 18px;
|
|
||||||
line-height: 1;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.ua-drawer-body {
|
|
||||||
flex: 1;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding: 16px;
|
|
||||||
color: var(--ua-color-text);
|
|
||||||
font-size: var(--ua-font-size-md);
|
|
||||||
}
|
|
||||||
.ua-drawer-foot {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: var(--ua-space-2);
|
|
||||||
padding: 12px 16px;
|
|
||||||
border-top: 1px solid var(--ua-color-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* —— 弹窗 —— */
|
/* —— 弹窗 —— */
|
||||||
.ua-modal-overlay {
|
.ua-modal-overlay {
|
||||||
display: none;
|
display: none;
|
||||||
@@ -1214,20 +573,6 @@
|
|||||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
|
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
|
||||||
animation: ua-pop-in 0.22s cubic-bezier(0.2, 0.9, 0.3, 1.2);
|
animation: ua-pop-in 0.22s cubic-bezier(0.2, 0.9, 0.3, 1.2);
|
||||||
}
|
}
|
||||||
/* 宽度档位:md 为基准,sm / lg 覆盖 */
|
|
||||||
.ua-modal-sm {
|
|
||||||
width: min(340px, 90vw);
|
|
||||||
}
|
|
||||||
.ua-modal-lg {
|
|
||||||
width: min(640px, 92vw);
|
|
||||||
}
|
|
||||||
.ua-modal-foot {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: var(--ua-space-2);
|
|
||||||
padding: 12px 16px;
|
|
||||||
border-top: 1px solid var(--ua-color-border);
|
|
||||||
}
|
|
||||||
@keyframes ua-fade {
|
@keyframes ua-fade {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
@@ -1268,63 +613,6 @@
|
|||||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--ua-color-accent) 18%, transparent);
|
box-shadow: 0 0 0 3px color-mix(in srgb, var(--ua-color-accent) 18%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* —— 悬浮提示 —— */
|
|
||||||
.ua-tip-wrap {
|
|
||||||
position: relative;
|
|
||||||
display: inline-flex;
|
|
||||||
}
|
|
||||||
.ua-tip {
|
|
||||||
position: absolute;
|
|
||||||
left: 50%;
|
|
||||||
z-index: 30;
|
|
||||||
max-width: 220px;
|
|
||||||
padding: var(--ua-space-1) var(--ua-space-2);
|
|
||||||
border-radius: var(--ua-radius-sm);
|
|
||||||
background: var(--ua-color-text);
|
|
||||||
color: var(--ua-color-bg);
|
|
||||||
font-size: var(--ua-font-size-sm);
|
|
||||||
line-height: 1.5;
|
|
||||||
pointer-events: none;
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.16s ease-out, transform 0.16s ease-out;
|
|
||||||
}
|
|
||||||
.ua-tip-top {
|
|
||||||
bottom: calc(100% + 6px);
|
|
||||||
transform: translateX(-50%) translateY(3px);
|
|
||||||
}
|
|
||||||
.ua-tip-bottom {
|
|
||||||
top: calc(100% + 6px);
|
|
||||||
transform: translateX(-50%) translateY(-3px);
|
|
||||||
}
|
|
||||||
.ua-tip.ua-open {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateX(-50%) translateY(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* —— 通用禁用态:置灰 + 禁用光标 —— */
|
|
||||||
.ua-btn:disabled,
|
|
||||||
.ua-checkbox:disabled,
|
|
||||||
.ua-radio:disabled,
|
|
||||||
.ua-switch-input:disabled,
|
|
||||||
.ua-input:disabled,
|
|
||||||
.ua-textarea:disabled,
|
|
||||||
.ua-select-trigger:disabled {
|
|
||||||
opacity: 0.45;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
/* 禁用时悬停不改边框色,避免误以为可交互 */
|
|
||||||
.ua-select-trigger:disabled:hover {
|
|
||||||
border-color: var(--ua-color-border);
|
|
||||||
}
|
|
||||||
/* 单选组禁用:整行文案同步弱化 */
|
|
||||||
.ua-check-row:has(.ua-radio:disabled) {
|
|
||||||
opacity: 0.45;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
.ua-switch-input:disabled + .ua-switch-track {
|
|
||||||
opacity: 0.45;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* —— 无障碍:用户偏好减少动效时关闭全部动画 —— */
|
/* —— 无障碍:用户偏好减少动效时关闭全部动画 —— */
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
.ua-btn,
|
.ua-btn,
|
||||||
@@ -1334,35 +622,17 @@
|
|||||||
.ua-select-arrow,
|
.ua-select-arrow,
|
||||||
.ua-select-pop,
|
.ua-select-pop,
|
||||||
.ua-option,
|
.ua-option,
|
||||||
.ua-menu-pop,
|
|
||||||
.ua-menu-caret,
|
|
||||||
.ua-menu-item,
|
|
||||||
.ua-acc-head,
|
|
||||||
.ua-acc-caret,
|
|
||||||
.ua-acc-body,
|
|
||||||
.ua-crumb-link,
|
|
||||||
.ua-pop,
|
|
||||||
.ua-slider-input,
|
|
||||||
.ua-tag,
|
|
||||||
.ua-tag-close,
|
|
||||||
.ua-switch-track,
|
.ua-switch-track,
|
||||||
.ua-switch-thumb,
|
.ua-switch-thumb,
|
||||||
.ua-tab,
|
.ua-tab,
|
||||||
.ua-tabs-indicator,
|
.ua-tabs-indicator,
|
||||||
.ua-page-btn,
|
|
||||||
.ua-table tbody tr,
|
.ua-table tbody tr,
|
||||||
.ua-progress-fill,
|
.ua-progress-fill,
|
||||||
.ua-progress-fill::after,
|
.ua-progress-fill::after,
|
||||||
.ua-alert,
|
.ua-alert,
|
||||||
.ua-tip,
|
|
||||||
.ua-toast,
|
|
||||||
.ua-skeleton,
|
|
||||||
.ua-skeleton::after,
|
|
||||||
.ua-form-error,
|
.ua-form-error,
|
||||||
.ua-modal,
|
.ua-modal,
|
||||||
.ua-modal-overlay,
|
.ua-modal-overlay {
|
||||||
.ua-drawer,
|
|
||||||
.ua-drawer-overlay {
|
|
||||||
animation: none !important;
|
animation: none !important;
|
||||||
transition: none !important;
|
transition: none !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -191,9 +191,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>'+(p.df?'<code>'+esc(p.df)+'</code>':'—')+'</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>'+esc(p.d||'')+(p.k?'(回调)':'')+'</td></tr>';
|
||||||
}).join('');
|
}).join('');
|
||||||
return '<table class="props"><tr><th>参数</th><th>类型</th><th>必填</th><th>默认值</th><th>说明</th></tr>'+rows+'</table>';
|
return '<table class="props"><tr><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 +289,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 ? '回调' : '', prop.df ? `默认 ${prop.df}` : ''].filter(Boolean).join(',');
|
const marks = [prop.r ? '必填' : '', prop.k ? '回调' : ''].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('');
|
||||||
@@ -491,11 +491,6 @@ 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 = [];
|
||||||
@@ -533,7 +528,7 @@ function styleCheckFile(file, src) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const hash = blankStrings(raw).indexOf('//'); // 注释定位:先剥离字面量,'https://' 不算注释
|
const hash = raw.indexOf('//');
|
||||||
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);
|
||||||
@@ -560,84 +555,6 @@ 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*** 清单一致性校验:组件文档块的 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)`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 组件清单同步)');
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- 入口 ----
|
// ---- 入口 ----
|
||||||
|
|
||||||
const [cmd, ...args] = process.argv.slice(2);
|
const [cmd, ...args] = process.argv.slice(2);
|
||||||
@@ -661,13 +578,10 @@ switch (cmd) {
|
|||||||
case 'style':
|
case 'style':
|
||||||
cmdStyle();
|
cmdStyle();
|
||||||
break;
|
break;
|
||||||
case 'verify':
|
|
||||||
cmdVerify();
|
|
||||||
break;
|
|
||||||
case 'mcp':
|
case 'mcp':
|
||||||
cmdMcp();
|
cmdMcp();
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
console.log('usage: ua <index|list|info <name>|docs|style|verify|mcp>');
|
console.log('usage: ua <index|list|info <name>|docs|style|mcp>');
|
||||||
process.exit(cmd ? 1 : 0);
|
process.exit(cmd ? 1 : 0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
{
|
|
||||||
"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 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|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": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
/*** 作用: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