diff --git a/.ua/backlog.json b/.ua/backlog.json index cba646d..6a50d5f 100644 --- a/.ua/backlog.json +++ b/.ua/backlog.json @@ -216,6 +216,14 @@ "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)" } ] } diff --git a/.ua/index.json b/.ua/index.json index 910f9bd..50a0e98 100644 --- a/.ua/index.json +++ b/.ua/index.json @@ -1,6 +1,6 @@ { "$schema": "ui-arch-registry-index/0.1", - "generatedAt": "2026-10-23T09:02:18.149Z", + "generatedAt": "2026-10-23T09:03:01.680Z", "repoRoot": ".", "packages": [ { @@ -212,7 +212,8 @@ "conventions": [ "皮肤不持有业务状态,一律委托 headless 内核或调用方信号", "表单类组件统一约定:*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": { "intro": "27 个 DOM 组件,统一用法:组件是普通函数,传入 props 返回 DOM 节点;需要状态时先创建 signal 再双向绑定。引入 CSS:import '@ui-arch/ui/ui.css'(内部已消费 tokens)。", diff --git a/.ua/iterations.log b/.ua/iterations.log index 27ec38f..338d3b2 100644 --- a/.ua/iterations.log +++ b/.ua/iterations.log @@ -25,3 +25,4 @@ {"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 全绿"} diff --git a/llms-full.txt b/llms-full.txt index 488407e..e91b76d 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -1,6 +1,6 @@ # @ui-arch 组件库 Agent 文档(无图形版) -> 自动生成于 2026-10-23T09:02:18.149Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。 +> 自动生成于 2026-10-23T09:03:01.680Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。 > 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。 ## 快速上手 @@ -763,6 +763,7 @@ const checked = signal(false); checkbox({ label: '同意', checked }); const ope - 皮肤不持有业务状态,一律委托 headless 内核或调用方信号 - 表单类组件统一约定:*Props.value/checked/active 为 Signal 双向绑定,onChange/onInput 为回调 - 语义色 variant 四档:default|info / success / warning / danger,一律来自令牌,禁止硬编码颜色 +- 无障碍基线:装饰性占位(skeleton)aria-hidden、加载指示(spinner)role=status、关闭按钮必须 aria-label、弹层弹窗 aria-haspopup/expanded、读屏可见的状态一律同步 aria-* --- diff --git a/llms.txt b/llms.txt index 5eb43f5..e9ebc8c 100644 --- a/llms.txt +++ b/llms.txt @@ -79,6 +79,7 @@ - 约定: 皮肤不持有业务状态,一律委托 headless 内核或调用方信号 - 约定: 表单类组件统一约定:*Props.value/checked/active 为 Signal 双向绑定,onChange/onInput 为回调 - 约定: 语义色 variant 四档:default|info / success / warning / danger,一律来自令牌,禁止硬编码颜色 +- 约定: 无障碍基线:装饰性占位(skeleton)aria-hidden、加载指示(spinner)role=status、关闭按钮必须 aria-label、弹层弹窗 aria-haspopup/expanded、读屏可见的状态一律同步 aria-* ## @ui-arch/cli (v0.1.0) diff --git a/packages/skin/ui/src/modal.ts b/packages/skin/ui/src/modal.ts index 6f43582..3ced53c 100644 --- a/packages/skin/ui/src/modal.ts +++ b/packages/skin/ui/src/modal.ts @@ -17,7 +17,7 @@ export function modal(props: ModalProps, ...children: unknown[]): HTMLElement { const box = h( 'div', { class: `ua-modal ua-modal-${props.size ?? 'md'}` }, - h('div', { class: 'ua-modal-head' }, h('h3', { class: 'ua-modal-title' }, props.title), h('button', { class: 'ua-modal-close', onClick: close }, '×')), + h('div', { class: 'ua-modal-head' }, h('h3', { class: 'ua-modal-title' }, props.title), h('button', { type: 'button', class: 'ua-modal-close', 'aria-label': '关闭弹窗', onClick: close }, '×')), h('div', { class: 'ua-modal-body' }, ...children), props.footer ? h('div', { class: 'ua-modal-foot' }, props.footer) : null, // 底部操作区:右对齐,仅传了才渲染 ); diff --git a/packages/skin/ui/src/skeleton.ts b/packages/skin/ui/src/skeleton.ts index 4148c57..6099e7c 100644 --- a/packages/skin/ui/src/skeleton.ts +++ b/packages/skin/ui/src/skeleton.ts @@ -4,9 +4,9 @@ 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}` }); // 单形状直接返回 + 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' : ''}` })); - return h('div', { class: 'ua-skeleton-group' }, ...lines); + 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); // 整组装饰性占位 } diff --git a/packages/skin/ui/src/spinner.ts b/packages/skin/ui/src/spinner.ts index ffb12b7..190bf0f 100644 --- a/packages/skin/ui/src/spinner.ts +++ b/packages/skin/ui/src/spinner.ts @@ -4,7 +4,7 @@ import type { SpinnerProps } from './types.js'; /*** 加载指示器:纯 CSS 旋转动画,size 控制直径 ***/ export function spinner(props: SpinnerProps = {}): HTMLElement { const size = `${props.size ?? 18}px`; - const el = h('span', { class: 'ua-spinner' }); + const el = h('span', { class: 'ua-spinner', role: 'status', 'aria-label': '加载中' }); // 读屏播报加载状态 el.style.width = size; el.style.height = size; return el; diff --git a/packages/skin/ui/src/tabs.ts b/packages/skin/ui/src/tabs.ts index 82ab914..5f3f94a 100644 --- a/packages/skin/ui/src/tabs.ts +++ b/packages/skin/ui/src/tabs.ts @@ -6,7 +6,7 @@ export function tabs(props: TabsProps): HTMLElement { // —— 状态集中声明 —— const buttons: HTMLButtonElement[] = []; // 页签按钮,顺序与 tabs 一致 const indicator = h('span', { class: 'ua-tabs-indicator' }); - const head = h('div', { class: 'ua-tabs-head', role: 'tablist' }, indicator); + const head = h('div', { class: 'ua-tabs-head', role: 'tablist', 'aria-label': '标签页' }, indicator); /*** 同步激活态与指示条几何位置 ***/ const sync = () => { @@ -27,6 +27,7 @@ export function tabs(props: TabsProps): HTMLElement { const btn = h('button', { class: 'ua-tab', role: 'tab', type: 'button' }, tab.label) as HTMLButtonElement; btn.dataset.value = tab.value; btn.disabled = tab.disabled ?? false; + if (tab.disabled) btn.setAttribute('aria-disabled', 'true'); // 读屏提示不可选 btn.addEventListener('click', () => { if (btn.disabled) return; // 卫语句:禁用页签不响应 if (props.active) props.active(tab.value); // 用户切换写回信号 diff --git a/packages/skin/ui/ua.manifest.json b/packages/skin/ui/ua.manifest.json index 2f6ff04..3edeb01 100644 --- a/packages/skin/ui/ua.manifest.json +++ b/packages/skin/ui/ua.manifest.json @@ -83,7 +83,8 @@ "conventions": [ "皮肤不持有业务状态,一律委托 headless 内核或调用方信号", "表单类组件统一约定:*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": { "intro": "27 个 DOM 组件,统一用法:组件是普通函数,传入 props 返回 DOM 节点;需要状态时先创建 signal 再双向绑定。引入 CSS:import '@ui-arch/ui/ui.css'(内部已消费 tokens)。",