diff --git a/.ua/backlog.json b/.ua/backlog.json index 31b4a7d..e1d5fc0 100644 --- a/.ua/backlog.json +++ b/.ua/backlog.json @@ -276,6 +276,14 @@ "id": "d29", "title": "menu 补 Home/End 跳转,与 select 键盘导航对称", "status": "done" + }, + { + "id": "e01", + "title": "新增 inputNumber 数字输入组件", + "motivation": "数量、份数等离散数值输入是表单常见场景,纯 textField 缺少步进语义;加减按钮 + 键盘调节可复用既有 button 与信号双向绑定模式", + "acceptance": "packages/skin/ui 新增 inputNumber.ts:−/+ 按钮 + 输入框三段式,value 信号双向绑定,min/max/step 超界自动夹取,键盘 ↑↓ 调节,非法输入回滚,disabled 支持布尔或信号;只消费 --ua-* 令牌;导出、manifest、文档、llms-full.txt 同步;demo 加一节演示;npm run check 全绿", + "status": "done", + "notes": "inputNumber(props):−/+ 复用 ghost button(自带微动画与 disabled 透传),输入框 inputmode=decimal,change 时 parseFloat 解析、非法回滚当前值;↑↓ 步进并 preventDefault;clamp 统一夹取 min/max(缺省 ±Infinity);aria-label 三件套 + role=group;CSS 步进按钮收窄、tabular-nums;demo 新增 sec-inputnumber(与禁用态实例共用 qty 信号);manifest/docs/llms-full/README 同步为 29 组件" } ] } \ No newline at end of file diff --git a/.ua/index.json b/.ua/index.json index 15948a0..e25b76a 100644 --- a/.ua/index.json +++ b/.ua/index.json @@ -1,6 +1,6 @@ { "$schema": "ui-arch-registry-index/0.1", - "generatedAt": "2026-10-23T09:12:34.151Z", + "generatedAt": "2026-10-23T09:49:54.480Z", "repoRoot": ".", "packages": [ { @@ -144,6 +144,7 @@ "exports": [ "button(props):按钮,variant = primary | ghost | danger,disabled 支持布尔或信号禁用", "textField(props):文本输入,value 信号双向绑定,disabled 支持布尔或信号禁用", + "inputNumber(props):数字输入,−/+ 按钮 + 键盘 ↑↓ 调节,value 信号双向绑定,min/max/step 超界自动夹取", "textarea(props):多行输入,value 信号双向绑定,disabled 支持布尔或信号禁用", "slider(props):滑块,value 信号双向绑定,min/max/step 可配,原生 range 承载键盘交互,右侧可显示当前值", "checkbox(props):复选框,checked 信号双向绑定,disabled 支持布尔或信号禁用", @@ -297,6 +298,52 @@ "在任意响应式位置用 name() 读取最新值,如 () => `你好,${name()}`" ] }, + { + "id": "inputNumber", + "title": "数字输入 InputNumber", + "demo": "sec-inputnumber", + "props": [ + { + "n": "value", + "t": "Signal", + "r": 1, + "d": "当前值信号,双向绑定(按钮/键盘写回、外部赋值同步)" + }, + { + "n": "min", + "t": "number", + "d": "最小值;缺省无下限" + }, + { + "n": "max", + "t": "number", + "d": "最大值;缺省无上限" + }, + { + "n": "step", + "t": "number", + "d": "步长;缺省 1", + "df": "1" + }, + { + "n": "disabled", + "t": "boolean | Signal", + "d": "禁用态:布尔直接置位,传信号则动态同步" + }, + { + "n": "onChange", + "t": "(value: number) => void", + "k": 1, + "d": "值变化回调(已夹取到合法区间)" + } + ], + "example": "const qty = signal(1); const el = inputNumber({ value: qty, min: 1, max: 99, onChange: (v) => console.log(v) })", + "steps": [ + "用 signal(1) 创建数字信号并传入 value 完成双向绑定", + "点击 −/+ 按钮或在输入框聚焦时按 ↑↓ 以 step 调节;直接输入在失焦或回车时解析", + "非法输入自动回滚为当前值;min/max 超界自动夹取,disabled 支持布尔或信号禁用" + ] + }, { "id": "textarea", "title": "多行输入 Textarea", diff --git a/.ua/iterations.log b/.ua/iterations.log index 79c14b5..0d4472f 100644 --- a/.ua/iterations.log +++ b/.ua/iterations.log @@ -34,3 +34,4 @@ {"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"} +{"ts":"2026-10-23T17:50:14+08:00","id":"e01","result":"done","commit":"pending","summary":"新增 inputNumber 数字输入:−/+ 按钮 + 键盘上下步进,信号双向绑定,min/max/step 夹取,门禁全绿"} diff --git a/README.md b/README.md index dc0eded..a924e54 100644 --- a/README.md +++ b/README.md @@ -11,13 +11,13 @@ npm run demo # 打开演示:apps/demo/dist/demo.html npm run docs # 生成人类文档站 apps/docs/index.html + Agent 全量文档 llms-full.txt ``` -## 组件(共 28 个) +## 组件(共 29 个) -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 +accordion · alert · avatar · badge · breadcrumb · button · card · checkbox · dataTable · drawer · form · inputNumber · menu · modal · pagination · popover · progress · radioGroup · select · skeleton · slider · spinner · switchButton · tabs · tag · textField · textarea · toast · tooltip -分组:表单(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)。 +分组:表单(button / textField / inputNumber / 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)。 ## 目录 @@ -25,7 +25,7 @@ accordion · alert · avatar · badge · breadcrumb · button · card · checkbo |---|---| | `packages/core/frame` | @ui-arch/frame:signal / computed / effect + hyperscript(无虚拟 DOM) | | `packages/core/table-core` | @ui-arch/table-core:headless 表格(filter → sort → paginate 派生管线) | -| `packages/skin/ui` | @ui-arch/ui:28 个 DOM 组件 + ui.css(只消费 `--ua-*` 令牌) | +| `packages/skin/ui` | @ui-arch/ui:29 个 DOM 组件 + ui.css(只消费 `--ua-*` 令牌) | | `packages/design/tokens` | @ui-arch/tokens:设计令牌(暗色只换值不换名) | | `packages/tools/cli` | @ui-arch/cli:`ua` 命令行与 stdio MCP server | | `apps/demo` | 交互演示(esbuild 打包成单文件) | diff --git a/apps/demo/main.ts b/apps/demo/main.ts index 8083d6b..4c73164 100644 --- a/apps/demo/main.ts +++ b/apps/demo/main.ts @@ -12,6 +12,7 @@ import { drawer, form, formItem, + inputNumber, menu, modal, pagination, @@ -111,6 +112,9 @@ const tags = signal(['设计', '前端', '组件库']); // —— 滑块:音量与禁用态演示 —— const volume = signal(40); +// —— 数字输入:购买数量演示,与禁用态实例共用同一信号 —— +const qty = signal(1); + // —— 受控选中:dataTable 内部勾选实时写回,外部可清空 —— const picked = signal([]); @@ -232,6 +236,17 @@ const content = h( slider({ value: volume, disabled: locked }), ), ), + section( + 'sec-inputnumber', + '数字输入', + 'inputNumber:−/+ 按钮与键盘 ↑↓ 调节,value 信号双向绑定,min/max/step 超界自动夹取。', + h( + 'div', + { class: 'row' }, + inputNumber({ value: qty, min: 1, max: 99, onChange: (v) => toast.info(`数量 ${v}`) }), + inputNumber({ value: qty, disabled: locked }), + ), + ), section( 'sec-feedback', '反馈与提示', diff --git a/llms-full.txt b/llms-full.txt index c59cbfc..4c9fbf4 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -1,6 +1,6 @@ # @ui-arch 组件库 Agent 文档(无图形版) -> 自动生成于 2026-10-23T09:06:10.925Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。 +> 自动生成于 2026-10-23T09:49:54.480Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。 > 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。 ## 快速上手 @@ -96,6 +96,31 @@ const name = signal(''); const el = textField({ value: name, placeholder: '输 现场演示:apps/docs/demo.html#sec-bind +### inputNumber — 数字输入 InputNumber + +参数: + +- value(必填):Signal —— 当前值信号,双向绑定(按钮/键盘写回、外部赋值同步) +- min:number —— 最小值;缺省无下限 +- max:number —— 最大值;缺省无上限 +- step(默认 1):number —— 步长;缺省 1 +- disabled:boolean | Signal —— 禁用态:布尔直接置位,传信号则动态同步 +- onChange(回调):(value: number) => void —— 值变化回调(已夹取到合法区间) + +示例: + +``` +const qty = signal(1); const el = inputNumber({ value: qty, min: 1, max: 99, onChange: (v) => console.log(v) }) +``` + +手把手: + +1. 用 signal(1) 创建数字信号并传入 value 完成双向绑定 +2. 点击 −/+ 按钮或在输入框聚焦时按 ↑↓ 以 step 调节;直接输入在失焦或回车时解析 +3. 非法输入自动回滚为当前值;min/max 超界自动夹取,disabled 支持布尔或信号禁用 + +现场演示:apps/docs/demo.html#sec-inputnumber + ### textarea — 多行输入 Textarea 参数: @@ -755,6 +780,7 @@ API 速览: - button(props):按钮,variant = primary | ghost | danger,disabled 支持布尔或信号禁用 - textField(props):文本输入,value 信号双向绑定,disabled 支持布尔或信号禁用 +- inputNumber(props):数字输入,−/+ 按钮 + 键盘 ↑↓ 调节,value 信号双向绑定,min/max/step 超界自动夹取 - textarea(props):多行输入,value 信号双向绑定,disabled 支持布尔或信号禁用 - slider(props):滑块,value 信号双向绑定,min/max/step 可配,原生 range 承载键盘交互,右侧可显示当前值 - checkbox(props):复选框,checked 信号双向绑定,disabled 支持布尔或信号禁用 diff --git a/llms.txt b/llms.txt index c35fba0..c2d4a7c 100644 --- a/llms.txt +++ b/llms.txt @@ -49,6 +49,7 @@ - API: - button(props):按钮,variant = primary | ghost | danger,disabled 支持布尔或信号禁用 - textField(props):文本输入,value 信号双向绑定,disabled 支持布尔或信号禁用 + - inputNumber(props):数字输入,−/+ 按钮 + 键盘 ↑↓ 调节,value 信号双向绑定,min/max/step 超界自动夹取 - textarea(props):多行输入,value 信号双向绑定,disabled 支持布尔或信号禁用 - slider(props):滑块,value 信号双向绑定,min/max/step 可配,原生 range 承载键盘交互,右侧可显示当前值 - checkbox(props):复选框,checked 信号双向绑定,disabled 支持布尔或信号禁用 diff --git a/packages/skin/ui/src/index.ts b/packages/skin/ui/src/index.ts index 10b2194..efcfa07 100644 --- a/packages/skin/ui/src/index.ts +++ b/packages/skin/ui/src/index.ts @@ -1,6 +1,7 @@ export * from './types.js'; export { button } from './button.js'; export { textField } from './textField.js'; +export { inputNumber } from './inputNumber.js'; export { textarea } from './textarea.js'; export { slider } from './slider.js'; export { checkbox } from './checkbox.js'; diff --git a/packages/skin/ui/src/inputNumber.ts b/packages/skin/ui/src/inputNumber.ts new file mode 100644 index 0000000..7b94b7c --- /dev/null +++ b/packages/skin/ui/src/inputNumber.ts @@ -0,0 +1,26 @@ +import { effect, h } from '@ui-arch/frame'; +import { button } from './button.js'; +import type { InputNumberProps } from './types.js'; + +/*** 数字输入:−/+ 按钮 + 输入框三段式,value 信号双向绑定,min/max/step 超界自动夹取,键盘 ↑↓ 调节 ***/ +export function inputNumber(props: InputNumberProps): HTMLDivElement { + const min = props.min ?? -Infinity; // 无下限缺省 + const max = props.max ?? Infinity; // 无上限缺省 + const step = props.step ?? 1; // 步长缺省 1 + const clamp = (v: number): number => Math.min(max, Math.max(min, v)); // 任何来源的值都夹回合法区间 + const emit = (v: number): void => { const n = clamp(v); props.value(n); if (props.onChange) props.onChange(n); }; // 写回信号并回调 + const input = h('input', { class: 'ua-input ua-input-number-field', inputmode: 'decimal', disabled: props.disabled, 'aria-label': '数字输入' }) as HTMLInputElement; + const sync = (): void => { const text = String(props.value()); if (input.value !== text) input.value = text; }; // 外部变化写回输入框 + effect(sync); + input.addEventListener('change', () => { const n = parseFloat(input.value); if (Number.isNaN(n)) sync(); else emit(n); }); // 非法输入回滚当前值 + input.addEventListener('keydown', (e: KeyboardEvent) => { + if (e.key !== 'ArrowUp' && e.key !== 'ArrowDown') return; // 卫语句:只拦截上下方向键 + e.preventDefault(); + emit(props.value() + (e.key === 'ArrowUp' ? step : -step)); // ↑ 加步长 / ↓ 减步长 + }); + const dec = button({ label: '−', variant: 'ghost', disabled: props.disabled, onClick: () => emit(props.value() - step) }); + const inc = button({ label: '+', variant: 'ghost', disabled: props.disabled, onClick: () => emit(props.value() + step) }); + dec.setAttribute('aria-label', '减少'); + inc.setAttribute('aria-label', '增加'); + return h('div', { class: 'ua-input-number', role: 'group' }, dec, input, inc) as HTMLDivElement; +} diff --git a/packages/skin/ui/src/types.ts b/packages/skin/ui/src/types.ts index 29aab78..9c02216 100644 --- a/packages/skin/ui/src/types.ts +++ b/packages/skin/ui/src/types.ts @@ -165,6 +165,16 @@ export interface TextareaProps { onInput?: (value: string) => void; // 输入回调 } +/*** 数字输入入参 ***/ +export interface InputNumberProps { + value: Signal; // 当前值信号,双向绑定(按钮/键盘写回、外部赋值同步) + min?: number; // 最小值;缺省无下限 + max?: number; // 最大值;缺省无上限 + step?: number; // 步长;缺省 1 + disabled?: boolean | Signal; // 禁用态:布尔直接置位,传信号则动态同步 + onChange?: (value: number) => void; // 值变化回调(已夹取到合法区间) +} + /*** 徽标入参 ***/ export interface BadgeProps { label: string; // 徽标文案 diff --git a/packages/skin/ui/ua.manifest.json b/packages/skin/ui/ua.manifest.json index 3db9bc9..58d572c 100644 --- a/packages/skin/ui/ua.manifest.json +++ b/packages/skin/ui/ua.manifest.json @@ -15,6 +15,7 @@ "exports": [ "button(props):按钮,variant = primary | ghost | danger,disabled 支持布尔或信号禁用", "textField(props):文本输入,value 信号双向绑定,disabled 支持布尔或信号禁用", + "inputNumber(props):数字输入,−/+ 按钮 + 键盘 ↑↓ 调节,value 信号双向绑定,min/max/step 超界自动夹取", "textarea(props):多行输入,value 信号双向绑定,disabled 支持布尔或信号禁用", "slider(props):滑块,value 信号双向绑定,min/max/step 可配,原生 range 承载键盘交互,右侧可显示当前值", "checkbox(props):复选框,checked 信号双向绑定,disabled 支持布尔或信号禁用", @@ -168,6 +169,52 @@ "在任意响应式位置用 name() 读取最新值,如 () => `你好,${name()}`" ] }, + { + "id": "inputNumber", + "title": "数字输入 InputNumber", + "demo": "sec-inputnumber", + "props": [ + { + "n": "value", + "t": "Signal", + "r": 1, + "d": "当前值信号,双向绑定(按钮/键盘写回、外部赋值同步)" + }, + { + "n": "min", + "t": "number", + "d": "最小值;缺省无下限" + }, + { + "n": "max", + "t": "number", + "d": "最大值;缺省无上限" + }, + { + "n": "step", + "t": "number", + "d": "步长;缺省 1", + "df": "1" + }, + { + "n": "disabled", + "t": "boolean | Signal", + "d": "禁用态:布尔直接置位,传信号则动态同步" + }, + { + "n": "onChange", + "t": "(value: number) => void", + "k": 1, + "d": "值变化回调(已夹取到合法区间)" + } + ], + "example": "const qty = signal(1); const el = inputNumber({ value: qty, min: 1, max: 99, onChange: (v) => console.log(v) })", + "steps": [ + "用 signal(1) 创建数字信号并传入 value 完成双向绑定", + "点击 −/+ 按钮或在输入框聚焦时按 ↑↓ 以 step 调节;直接输入在失焦或回车时解析", + "非法输入自动回滚为当前值;min/max 超界自动夹取,disabled 支持布尔或信号禁用" + ] + }, { "id": "textarea", "title": "多行输入 Textarea", diff --git a/packages/skin/ui/ui.css b/packages/skin/ui/ui.css index 52627e2..d6c8d55 100644 --- a/packages/skin/ui/ui.css +++ b/packages/skin/ui/ui.css @@ -687,6 +687,22 @@ resize: vertical; } +/* —— 数字输入 —— */ +.ua-input-number { + display: inline-flex; + align-items: center; + gap: 4px; +} +.ua-input-number .ua-btn { + padding: 5px 12px; /* 收窄为步进按钮 */ + line-height: 1.4; +} +.ua-input-number-field { + width: 76px; + text-align: center; + font-variant-numeric: tabular-nums; +} + /* —— 徽标 —— */ .ua-badge { display: inline-block;