Compare commits

...

3 Commits

13 changed files with 206 additions and 8 deletions
+8
View File
@@ -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 组件"
}
]
}
+48 -1
View File
@@ -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<number>",
"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<boolean>",
"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",
+1
View File
@@ -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":"20d7b94","summary":"新增 inputNumber 数字输入:−/+ 按钮 + 键盘上下步进,信号双向绑定,min/max/step 夹取,门禁全绿"}
+2 -2
View File
@@ -7,7 +7,7 @@
调度器(每小时唤起)**不是**迭代器,只做两件事:健康检查 + 中断重启。真正的迭代是单次唤起内的**连续循环**——反复执行《每轮迭代流程》,直到 backlog 清空或时间预算耗尽,中间不停歇。
- **并发锁**:`.ua/loop.lock`(含时间戳,不入库)。唤起时若锁存在且心跳在 15 分钟内 → 已有实例在跑,直接退出;否则写锁开始循环。每轮开始前刷新锁内时间戳作心跳。
- **时间预算**:单次唤起最多连续迭代约 50 分钟,到点在完成当前轮后停止(防止与下次唤起重叠)。
- **时间预算**:单次唤起最多连续迭代约 50 分钟,到点在完成当前轮后停止(防止与下次唤起重叠)。**预算判断必须基于客观时钟**:点火时用 `date +%s` 记录起点并写入 `.ua/loop.lock`,每轮开始时重新取 `date +%s` 与起点求差,差值 ≥ 2700 秒(45 分钟)即到点;**禁止凭感觉估算时长**——LLM 无内部时钟,自估时长系统性偏高(实测自报 40+ 分钟而实际约 30 分钟)。
- **熔断**:同一 backlog 条目连续 2 轮失败 → 停止循环,锁照常释放,等待下次唤起。
- **重启语义**:锁心跳超过 15 分钟视为循环中断(崩溃/超时),调度器删旧锁、重新点火。
@@ -31,7 +31,7 @@ qa/probe ──门禁通过──▶ qa/stable ──人工审阅──▶ main
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` 直接重置回 `qa/stable`,backlog 条目在 `notes` 里追加失败原因、`status` 保持 `open`,提交并推送该元数据变更(仅改 `.ua/backlog.json`,不碰代码)。
5. **留痕**:在 `.ua/iterations.log` 追加一行 JSON:`{时间, 条目id, 结果, 摘要}`,随本轮提交。
5. **留痕**:在 `.ua/iterations.log` 追加一行 JSON,**字段统一**:`{"ts":"本地ISO时间","id":"条目id","result":"done|aborted","commit":"短哈希","summary":"一句话"}`。硬性规范:`ts` 必须来自 `date` 命令实测(本地时间,带 +08:00 偏移,**禁止手写或用 UTC 换算**);时长结论必须来自两次时间查询之差,禁止使用「大约/接近」等估算词;并发实例格式不得自创变体。随本轮提交。
6. **边界**:不改 `main` 分支保护、不删分支、不动 `.workbuddy/`、不重写历史、不 force push 到 `qa/stable` 与 `main`。
## 选题规则(自主补点子时)
+4 -4
View File
@@ -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 个)
<!-- 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
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
<!-- /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)。
分组:表单(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 打包成单文件) |
+15
View File
@@ -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<RowKey[]>([]);
@@ -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',
'反馈与提示',
+27 -1
View File
@@ -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<number> —— 当前值信号,双向绑定(按钮/键盘写回、外部赋值同步)
- min:number —— 最小值;缺省无下限
- max:number —— 最大值;缺省无上限
- step(默认 1):number —— 步长;缺省 1
- disabled:boolean | Signal<boolean> —— 禁用态:布尔直接置位,传信号则动态同步
- 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 支持布尔或信号禁用
+1
View File
@@ -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 支持布尔或信号禁用
+1
View File
@@ -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';
+26
View File
@@ -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;
}
+10
View File
@@ -165,6 +165,16 @@ export interface TextareaProps {
onInput?: (value: string) => void; // 输入回调
}
/*** 数字输入入参 ***/
export interface InputNumberProps {
value: Signal<number>; // 当前值信号,双向绑定(按钮/键盘写回、外部赋值同步)
min?: number; // 最小值;缺省无下限
max?: number; // 最大值;缺省无上限
step?: number; // 步长;缺省 1
disabled?: boolean | Signal<boolean>; // 禁用态:布尔直接置位,传信号则动态同步
onChange?: (value: number) => void; // 值变化回调(已夹取到合法区间)
}
/*** 徽标入参 ***/
export interface BadgeProps {
label: string; // 徽标文案
+47
View File
@@ -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<number>",
"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<boolean>",
"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",
+16
View File
@@ -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;