diff --git a/.ua/backlog.json b/.ua/backlog.json index 1c68655..40e80b5 100644 --- a/.ua/backlog.json +++ b/.ua/backlog.json @@ -3401,6 +3401,15 @@ "motivation": "对比度门禁有「正文 × 强调淡底」,却没有「正文 / 语义色 × 语义三色淡底」——而提示条、失败步骤、删除悬停底的文字恰恰压在这些淡底上。补上配对后立刻抓到一处真实不达标:亮色警告色 #9a6b1f 压在自家 10% 淡底上只有 4.12(AA 要 4.5),也就是最需要被读清的警告条文字反而不合格。", "acceptance": "① contrast-audit 的 PAIRS 增加 6 组:正文与语义色各自 × 危险 / 成功 / 警告淡底(阈值 4.5),淡底先压到页面底再求值;② 亮色警告色加深到 #8a5d15(页面底 5.74、警告淡底 5.01),tokens.css 与 tokens.json 双源同步(verify 的名字与值校验要过);③ audit:contrast 全部 24 组配对在亮 / 暗 × 常规 / 高对比四套主题下达标,audit:surface 阶梯仍全部达标(淡底变深只会变明显);④ AGENTS 的门禁说明同步到 24 组;⑤ npm run check 全绿。", "notes": "" + }, + { + "id": "g335", + "domain": "tune", + "status": "open", + "title": "输入类组件补可访问名:读屏不再只念「编辑框 / 滑块」", + "motivation": "库内 13 个组件都有 ariaLabel 入参(switchButton / pagination / jsonViewer / codeBlock …),偏偏最常用的四个输入件没有:textField 与 textarea 的控件上没有可访问名(只有清空 / 显隐按钮有),读屏进去只念「编辑框」;slider 的原生 range 无名,区间模式两个滑块更是完全同名,读屏分不出上下限;inputNumber 的「数字输入」写死,业务上没法改成「数量 / 单价」。", + "acceptance": "① TextFieldProps / TextareaProps / InputNumberProps / SliderProps 增可选 ariaLabel,落成控件 aria-label(缺省沿用「文本输入」「多行文本」「数字输入」「数值」);② slider 区间模式自动拼「下限 / 上限」,两个 range 各有名字;③ inputNumber 的写死字符串改成可覆盖,与其余组件同一口径;④ 四份清单文档补 ariaLabel 参数行(verify 的双向字段校验要过);⑤ npm run check 全绿。", + "notes": "" } ] } diff --git a/.ua/index.json b/.ua/index.json index f8a6f34..7ccf304 100644 --- a/.ua/index.json +++ b/.ua/index.json @@ -1,6 +1,6 @@ { "$schema": "ui-arch-registry-index/0.1", - "generatedAt": "2026-10-26T18:05:09.130Z", + "generatedAt": "2026-10-26T18:08:56.364Z", "repoRoot": ".", "packages": [ { @@ -444,6 +444,11 @@ "n": "password", "t": "boolean", "d": "密码框:原生打点渲染,内置「显示/隐藏」切换按钮(aria-label 同步);缺省普通文本" + }, + { + "n": "ariaLabel", + "t": "string", + "d": "无可见 label 时的可访问名(如「收件人」);缺省「文本输入」" } ], "example": "const name = signal(''); const el = textField({ value: name, placeholder: '输入名字…', onInput: (v) => console.log(v) })", @@ -512,6 +517,11 @@ "n": "size", "t": "'sm' | 'md' | 'lg'", "d": "高度档位:sm 紧凑 / md 默认 / lg 大触控目标,走 --ua-control-h-* 三档;缺省 md" + }, + { + "n": "ariaLabel", + "t": "string", + "d": "无可见 label 时的可访问名(如「数量」);缺省「数字输入」" } ], "example": "const qty = signal(1); const el = inputNumber({ value: qty, min: 1, max: 99, onChange: (v) => console.log(v) })", @@ -574,6 +584,11 @@ "n": "autosize", "t": "boolean | { minRows?, maxRows? }", "d": "自适应内容高度:true 跟随内容涨落,对象态钳最小/最大行数,超出 maxRows 内部滚动;缺省固定行数" + }, + { + "n": "ariaLabel", + "t": "string", + "d": "无可见 label 时的可访问名(如「备注」);缺省「多行文本」" } ], "example": "const bio = signal(''); textarea({ value: bio, rows: 4, placeholder: '一句话介绍…' })", @@ -651,6 +666,11 @@ "t": "(value: number) => void", "k": 1, "d": "拖动回调" + }, + { + "n": "ariaLabel", + "t": "string", + "d": "无可见 label 时的可访问名(如「音量」);缺省「数值」,区间模式自动带「下限 / 上限」" } ], "example": "const vol = signal(40); slider({ value: vol, min: 0, max: 100, step: 5, onChange: (v) => console.log(v) })", diff --git a/llms-full.txt b/llms-full.txt index 49572f6..d6e4bed 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -1,6 +1,6 @@ # @ui-arch 组件库 Agent 文档(无图形版) -> 自动生成于 2026-10-26T18:05:09.130Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。 +> 自动生成于 2026-10-26T18:08:56.364Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。 > 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。 ## 快速上手 @@ -125,6 +125,7 @@ button({ label: '提交', variant: 'primary', onClick: () => console.log('clicke - maxCount:number —— 字数上限:同时设原生 maxlength,并在输入框内侧显示实时计数;缺省不限 - size:'sm' | 'md' | 'lg' —— 高度档位:sm 紧凑 / md 默认 / lg 大触控目标,走 --ua-control-h-* 三档;缺省 md - password:boolean —— 密码框:原生打点渲染,内置「显示/隐藏」切换按钮(aria-label 同步);缺省普通文本 +- ariaLabel:string —— 无可见 label 时的可访问名(如「收件人」);缺省「文本输入」 示例: @@ -156,6 +157,7 @@ const name = signal(''); const el = textField({ value: name, placeholder: '输 - prefix:string | HTMLElement —— 前置附加块(textField/inputNumber 在输入框内侧前、textarea 在上方);缺省不渲染 - suffix:string | HTMLElement —— 后置附加块(textField/inputNumber 在输入框内侧后、textarea 在下方);缺省不渲染 - size:'sm' | 'md' | 'lg' —— 高度档位:sm 紧凑 / md 默认 / lg 大触控目标,走 --ua-control-h-* 三档;缺省 md +- ariaLabel:string —— 无可见 label 时的可访问名(如「数量」);缺省「数字输入」 示例: @@ -186,6 +188,7 @@ const qty = signal(1); const el = inputNumber({ value: qty, min: 1, max: 99, onC - suffix:string | HTMLElement —— 后置附加块(textField/inputNumber 在输入框内侧后、textarea 在下方);缺省不渲染 - maxCount:number —— 字数上限:同时设原生 maxlength,并在下方附加块显示实时计数;缺省不限 - autosize:boolean | { minRows?, maxRows? } —— 自适应内容高度:true 跟随内容涨落,对象态钳最小/最大行数,超出 maxRows 内部滚动;缺省固定行数 +- ariaLabel:string —— 无可见 label 时的可访问名(如「备注」);缺省「多行文本」 示例: @@ -217,6 +220,7 @@ const bio = signal(''); textarea({ value: bio, rows: 4, placeholder: '一句话 - end:Signal —— 区间上限信号;仅 range 模式使用,不传则组件内部维护 - onRangeChange(回调):(low: number, high: number) => void —— 区间两端变更回调,参数已保证 low ≤ high(越界那一端会被压平) - onChange(回调):(value: number) => void —— 拖动回调 +- ariaLabel:string —— 无可见 label 时的可访问名(如「音量」);缺省「数值」,区间模式自动带「下限 / 上限」 示例: diff --git a/packages/skin/ui/src/inputNumber.ts b/packages/skin/ui/src/inputNumber.ts index 57e2607..55f5b47 100644 --- a/packages/skin/ui/src/inputNumber.ts +++ b/packages/skin/ui/src/inputNumber.ts @@ -11,7 +11,7 @@ export function inputNumber(props: InputNumberProps): HTMLElement { const clamp = (v: number): number => Math.min(max, Math.max(min, v)); // 任何来源的值都夹回合法区间 const quantize = (n: number): number => (props.precision === undefined ? n : Number(n.toFixed(props.precision))); // precision 归一(顺带消除步进浮点尾差) const emit = (v: number): void => { const n = quantize(clamp(v)); props.value(n); if (props.onChange) props.onChange(n); }; // 写回信号并回调 - const input = h('input', { class: `ua-input ua-input-number-field${props.size && props.size !== 'md' ? ` is-${props.size}` : ''}`, inputmode: 'decimal', disabled: props.disabled, 'aria-label': '数字输入' }) as HTMLInputElement; + const input = h('input', { class: `ua-input ua-input-number-field${props.size && props.size !== 'md' ? ` is-${props.size}` : ''}`, inputmode: 'decimal', disabled: props.disabled, 'aria-label': props.ariaLabel ?? '数字输入' }) as HTMLInputElement; const sync = (): void => { const text = props.precision === undefined ? String(props.value()) : props.value().toFixed(props.precision); if (input.value !== text) input.value = text; }; // 外部变化写回输入框(precision 定长显示) effect(sync); input.addEventListener('change', () => { const n = parseFloat(input.value); if (Number.isNaN(n)) sync(); else emit(n); }); // 非法输入回滚当前值 diff --git a/packages/skin/ui/src/slider.ts b/packages/skin/ui/src/slider.ts index e05b488..1645bd3 100644 --- a/packages/skin/ui/src/slider.ts +++ b/packages/skin/ui/src/slider.ts @@ -13,8 +13,8 @@ export function slider(props: SliderProps): HTMLElement { const pct = (v: number): number => ((v - min) / (max - min || 1)) * 100; // 分母兜底,避免 min=max 时除零 /*** 建一条原生 range:区间模式靠 CSS 叠在同一格,只有拇指可命中 ***/ - const track = (sig: typeof value, onInput: (n: number) => void): HTMLInputElement => { - const input = h('input', { type: 'range', class: 'ua-slider-input', min, max, step, disabled }) as HTMLInputElement; + const track = (sig: typeof value, onInput: (n: number) => void, label: string): HTMLInputElement => { + const input = h('input', { type: 'range', class: 'ua-slider-input', min, max, step, disabled, 'aria-label': label }) as HTMLInputElement; // 原生 range 不自带名字,读屏只念「滑块」 input.addEventListener('input', () => onInput(Number(input.value))); effect(() => { const next = String(sig()); @@ -30,7 +30,7 @@ export function slider(props: SliderProps): HTMLElement { : null; if (!props.range) { // —— 单值:一条原生轨,行为与旧版完全一致 —— - const input = track(value, (next) => { value(next); props.onChange?.(next); }); + const input = track(value, (next) => { value(next); props.onChange?.(next); }, props.ariaLabel ?? '数值'); return h( 'div', { class: 'ua-slider' }, @@ -42,8 +42,8 @@ export function slider(props: SliderProps): HTMLElement { // —— 区间:两条原生轨叠放,rail 打底、seg 画出两拇指之间的已选段 —— const rail = h('div', { class: 'ua-slider-rail', 'aria-hidden': 'true' }); const seg = h('div', { class: 'ua-slider-seg', 'aria-hidden': 'true' }) as HTMLElement; - const lowIn = track(value, (next) => { value(Math.min(next, high())); props.onRangeChange?.(value(), high()); }); // 下限越过上限就压平 - const highIn = track(high, (next) => { high(Math.max(next, value())); props.onRangeChange?.(value(), high()); }); // 上限低于下限就压平 + const lowIn = track(value, (next) => { value(Math.min(next, high())); props.onRangeChange?.(value(), high()); }, `${props.ariaLabel ?? '数值'}下限`); // 下限越过上限就压平;区间两端各有名字 + const highIn = track(high, (next) => { high(Math.max(next, value())); props.onRangeChange?.(value(), high()); }, `${props.ariaLabel ?? '数值'}上限`); // 上限低于下限就压平 lowIn.classList.add('ua-slider-input-low'); // 供 CSS 区分上下层与拇指命中区 highIn.classList.add('ua-slider-input-high'); effect(() => { seg.style.left = `${pct(value())}%`; seg.style.width = `${pct(high()) - pct(value())}%`; }); // 已选段随两端重算 diff --git a/packages/skin/ui/src/textField.ts b/packages/skin/ui/src/textField.ts index 461bd3b..92f7858 100644 --- a/packages/skin/ui/src/textField.ts +++ b/packages/skin/ui/src/textField.ts @@ -4,7 +4,7 @@ import type { TextFieldProps } from './types.js'; /*** 文本输入:value 信号双向绑定,disabled 支持布尔或信号禁用,onInput 为输入回调;prefix/suffix 包附加组,clearable 一键清空,password 密码显隐切换,maxCount 显示字数统计 ***/ export function textField(props: TextFieldProps): HTMLElement { - const el = h('input', { class: `ua-input${props.size && props.size !== 'md' ? ` is-${props.size}` : ''}`, placeholder: props.placeholder ?? '', disabled: props.disabled }) as HTMLInputElement; // false/undefined 移除属性,Signal 走响应式 + const el = h('input', { class: `ua-input${props.size && props.size !== 'md' ? ` is-${props.size}` : ''}`, placeholder: props.placeholder ?? '', disabled: props.disabled, 'aria-label': props.ariaLabel ?? '文本输入' }) as HTMLInputElement; // false/undefined 移除属性,Signal 走响应式 if (props.value) { const signal = props.value; el.addEventListener('input', () => signal(el.value)); // 用户输入写回信号 diff --git a/packages/skin/ui/src/textarea.ts b/packages/skin/ui/src/textarea.ts index 8244c33..281dc1a 100644 --- a/packages/skin/ui/src/textarea.ts +++ b/packages/skin/ui/src/textarea.ts @@ -4,7 +4,7 @@ import type { TextareaProps } from './types.js'; /*** 多行输入:value 信号双向绑定,disabled 支持布尔或信号禁用,onInput 为输入回调;prefix/suffix 包纵向附加组,maxCount 显示字数统计 ***/ export function textarea(props: TextareaProps): HTMLElement { - const el = h('textarea', { class: 'ua-textarea', rows: props.rows ?? 3, placeholder: props.placeholder ?? '', disabled: props.disabled }) as HTMLTextAreaElement; // false/undefined 移除属性,Signal 走响应式 + const el = h('textarea', { class: 'ua-textarea', rows: props.rows ?? 3, placeholder: props.placeholder ?? '', disabled: props.disabled, 'aria-label': props.ariaLabel ?? '多行文本' }) as HTMLTextAreaElement; // false/undefined 移除属性,Signal 走响应式 if (props.value) { const signal = props.value; el.addEventListener('input', () => signal(el.value)); // 用户输入写回信号 diff --git a/packages/skin/ui/src/types.ts b/packages/skin/ui/src/types.ts index c387ee1..828deec 100644 --- a/packages/skin/ui/src/types.ts +++ b/packages/skin/ui/src/types.ts @@ -32,6 +32,7 @@ export interface ButtonProps { /*** 文本输入入参 ***/ export interface TextFieldProps { + ariaLabel?: string; // 读屏用的字段名(如「收件人」);缺省「文本输入」 size?: 'sm' | 'md' | 'lg'; // 高度档位:走 --ua-control-h-* 三档;缺省 md placeholder?: string; // 占位文案 value?: Signal; // 双向绑定的信号 @@ -189,6 +190,7 @@ export interface AccordionProps { /*** 滑块入参 ***/ export interface SliderProps { + ariaLabel?: string; // 读屏用的字段名(如「音量」);缺省「数值」,区间模式自动带上限/下限 value: Signal; // 当前值信号,双向绑定(拖动写回、外部赋值同步) min?: number; // 最小值;缺省 0 max?: number; // 最大值;缺省 100 @@ -204,6 +206,7 @@ export interface SliderProps { /*** 多行输入入参 ***/ export interface TextareaProps { + ariaLabel?: string; // 读屏用的字段名(如「备注」);缺省「多行文本」 placeholder?: string; // 占位文案 rows?: number; // 行数;缺省 3 value?: Signal; // 双向绑定的信号 @@ -217,6 +220,7 @@ export interface TextareaProps { /*** 数字输入入参 ***/ export interface InputNumberProps { + ariaLabel?: string; // 读屏用的字段名(如「数量」);缺省「数字输入」 size?: 'sm' | 'md' | 'lg'; // 高度档位:走 --ua-control-h-* 三档;缺省 md value: Signal; // 当前值信号,双向绑定(按钮/键盘写回、外部赋值同步) min?: number; // 最小值;缺省无下限 diff --git a/packages/skin/ui/ua.manifest.json b/packages/skin/ui/ua.manifest.json index e229ff1..4bbf0c2 100644 --- a/packages/skin/ui/ua.manifest.json +++ b/packages/skin/ui/ua.manifest.json @@ -315,6 +315,12 @@ "t": "boolean", "d": "密码框:原生打点渲染,内置「显示/隐藏」切换按钮(aria-label 同步);缺省普通文本" } + , + { + "n": "ariaLabel", + "t": "string", + "d": "无可见 label 时的可访问名(如「收件人」);缺省「文本输入」" + } ], "example": "const name = signal(''); const el = textField({ value: name, placeholder: '输入名字…', onInput: (v) => console.log(v) })", "steps": [ @@ -383,6 +389,12 @@ "t": "'sm' | 'md' | 'lg'", "d": "高度档位:sm 紧凑 / md 默认 / lg 大触控目标,走 --ua-control-h-* 三档;缺省 md" } + , + { + "n": "ariaLabel", + "t": "string", + "d": "无可见 label 时的可访问名(如「数量」);缺省「数字输入」" + } ], "example": "const qty = signal(1); const el = inputNumber({ value: qty, min: 1, max: 99, onChange: (v) => console.log(v) })", "steps": [ @@ -445,6 +457,12 @@ "t": "boolean | { minRows?, maxRows? }", "d": "自适应内容高度:true 跟随内容涨落,对象态钳最小/最大行数,超出 maxRows 内部滚动;缺省固定行数" } + , + { + "n": "ariaLabel", + "t": "string", + "d": "无可见 label 时的可访问名(如「备注」);缺省「多行文本」" + } ], "example": "const bio = signal(''); textarea({ value: bio, rows: 4, placeholder: '一句话介绍…' })", "steps": [ @@ -522,6 +540,12 @@ "k": 1, "d": "拖动回调" } + , + { + "n": "ariaLabel", + "t": "string", + "d": "无可见 label 时的可访问名(如「音量」);缺省「数值」,区间模式自动带「下限 / 上限」" + } ], "example": "const vol = signal(40); slider({ value: vol, min: 0, max: 100, step: 5, onChange: (v) => console.log(v) })", "steps": [