iter(g244): 新增 inputTag 标签输入组件,回车成标签并支持满额禁输

This commit is contained in:
HIsheR
2026-10-26 21:57:52 +08:00
parent cd8a8e9b12
commit ea5149338a
12 changed files with 288 additions and 10 deletions
+66 -2
View File
@@ -1,6 +1,6 @@
{
"$schema": "ui-arch-registry-index/0.1",
"generatedAt": "2026-10-26T13:53:12.662Z",
"generatedAt": "2026-10-26T13:57:11.667Z",
"repoRoot": ".",
"packages": [
{
@@ -218,7 +218,8 @@
"copyButton(props):复制按钮,点击把 text 写入剪贴板并短暂显示已复制,text 可传函数取实时值,clipboard API 不可用时降级 execCommand",
"countdown(props):倒计时,秒级递减到 0 自动停表并触发 onFinish,缺省 mm:ss 展示,返回句柄 start/stop/reset",
"kbd(props):键盘按键标记,键帽样式展示单个按键或组合键(多键以 + 连接),用于快捷键提示",
"highlight(props):关键词高亮,命中片段包 mark(大小写不敏感全部命中),text / keyword 支持信号响应式刷新"
"highlight(props):关键词高亮,命中片段包 mark(大小写不敏感全部命中),text / keyword 支持信号响应式刷新",
"inputTag(props):标签输入,输入后回车 / 逗号成标签、chip 可单个删除、空框退格删末个,value 信号双向绑定,max 满额自动禁输"
],
"keyTypes": [
"ButtonProps",
@@ -4027,6 +4028,69 @@
"两个入参都接受信号:搜索框输入实时联动高亮,无需手动重渲染",
"大小写不敏感且全部命中(非只第一处);关键词两侧空白自动忽略"
]
},
{
"id": "inputTag",
"title": "标签输入 InputTag",
"demo": "sec-inputtag",
"props": [
{
"n": "value",
"t": "Signal<string[]>",
"r": 1,
"d": "已选标签集合,双向绑定:成标签与删 chip 都写回信号"
},
{
"n": "placeholder",
"t": "string",
"r": 0,
"d": "未满时的占位文案;缺省「输入后回车添加」"
},
{
"n": "max",
"t": "number",
"r": 0,
"d": "最多允许的标签条数;缺省 0 表示不限,满额后输入框禁用并提示上限"
},
{
"n": "maxLength",
"t": "number",
"r": 0,
"d": "单个标签最大字符数;缺省 24,超出直接截断"
},
{
"n": "allowDuplicate",
"t": "boolean",
"r": 0,
"d": "是否允许重复标签;缺省 false(重复的静默丢弃)"
},
{
"n": "disabled",
"t": "boolean | Signal<boolean>",
"r": 0,
"d": "禁用态:布尔或信号,true 时不可输入也不可删"
},
{
"n": "ariaLabel",
"t": "string",
"r": 0,
"d": "输入框无障碍标签;缺省「标签输入」"
},
{
"n": "onChange",
"t": "(tags: string[]) => void",
"r": 0,
"d": "标签集合变化回调,参数是变化后的完整集合"
}
],
"example": "const tags = signal(['前端', '设计']);\ninputTag({ value: tags, max: 5, onChange: (list) => console.log(list.length) })",
"steps": [
"传一个 signal<string[]> 作为 value:回车或逗号把当前输入转成 chip,写回信号后立即重画",
"点 chip 上的 × 删除单个标签;输入框为空时按退格键删掉最后一个(手不离键盘也能改)",
"输入后直接失焦也会成标签,不必强行回车;max 满额后输入框禁用,占位文案变成「最多 N 个标签」",
"要区分大小写重复判断就传 allowDuplicate: true;默认按原样去重",
"样式复用输入组与标签:外框聚焦光环、chip 圆角与语义色都跟既有组件一致,无新增视觉字面量"
]
}
]
},
+1 -1
View File
@@ -21,7 +21,7 @@
- `packages/core/table-core`(@ui-arch/table-core):Headless 表格逻辑内核
- `packages/core/frame`(@ui-arch/frame):signal + hyperscript 微框架
- `packages/skin/ui`(@ui-arch/ui):75 个 DOM 组件(表单 / 反馈 / 浮层 / 导航 / 表格),样式只消费令牌
- `packages/skin/ui`(@ui-arch/ui):76 个 DOM 组件(表单 / 反馈 / 浮层 / 导航 / 表格),样式只消费令牌
- `packages/design/tokens`(@ui-arch/tokens):设计令牌
- `packages/tools/cli`(@ui-arch/cli):`ua` 命令行 + MCP 注册表服务(private)
- `apps/demo`(@ui-arch/demo):交互演示,产物 apps/demo/dist/demo.html
+1 -1
View File
@@ -34,7 +34,7 @@ ui-arch-mvp/
│ │ ├── table-core/ # 表格状态内核
│ │ └── frame/ # 信号 + hyperscript 微框架
│ ├── skin/ # 皮肤层(DOM 组件,可发布)
│ │ └── ui/ # 75 个组件 + ui.css
│ │ └── ui/ # 76 个组件 + ui.css
│ ├── design/ # 资产层(可发布)
│ │ └── tokens/ # --ua-* 设计令牌
│ └── tools/ # 工程层(private,不发布)
+4 -4
View File
@@ -12,13 +12,13 @@ npm run docs # 生成人类文档站 apps/docs/index.html + Agent 全
npm run changelog # 依据 git 提交史重写 CHANGELOG.md(发版前跑;changelog:check 可查漂移)
```
## 组件(共 75 个)
## 组件(共 76 个)
<!-- ua:components -->
accordion · affix · alert · anchor · autoComplete · avatar · avatarGroup · backTop · badge · breadcrumb · button · calendar · carousel · card · cascader · checkbox · checkboxGroup · colorPicker · commandPalette · copyButton · countdown · dataTable · datePicker · descriptions · divider · drawer · empty · floatButton · form · grid · highlight · image · inputNumber · kbd · link · list · menu · modal · mentions · multiSelect · notification · pagination · pinInput · popover · popconfirm · progress · radioGroup · rate · result · segmented · select · skeleton · slider · space · spinner · splitter · statistic · stepper · switchButton · tabs · tag · textField · textarea · timeline · timePicker · toast · tooltip · tour · transfer · tree · treeSelect · typography · upload · virtualList · watermark
accordion · affix · alert · anchor · autoComplete · avatar · avatarGroup · backTop · badge · breadcrumb · button · calendar · carousel · card · cascader · checkbox · checkboxGroup · colorPicker · commandPalette · copyButton · countdown · dataTable · datePicker · descriptions · divider · drawer · empty · floatButton · form · grid · highlight · image · inputNumber · inputTag · kbd · link · list · menu · modal · mentions · multiSelect · notification · pagination · pinInput · popover · popconfirm · progress · radioGroup · rate · result · segmented · select · skeleton · slider · space · spinner · splitter · statistic · stepper · switchButton · tabs · tag · textField · textarea · timeline · timePicker · toast · tooltip · tour · transfer · tree · treeSelect · typography · upload · virtualList · watermark
<!-- /ua:components -->
分组:表单(button / copyButton / textField / inputNumber / textarea / checkbox / checkboxGroup / colorPicker / switchButton / select / mentions / multiSelect / radioGroup / slider / rate / autoComplete / datePicker / timePicker / pinInput / segmented / upload / form + formItem / transfer / cascader)、反馈(alert / toast / notification / result / spinner / progress / skeleton / badge / empty / statistic / countdown)、浮层(tooltip / popover / popconfirm / modal / drawer / menu / commandPalette)、导航(tabs / breadcrumb / pagination / accordion / stepper / tree / treeSelect / anchor)、数据展示(dataTable / descriptions / card / list / kbd / highlight / timeline / tag / avatar / avatarGroup / image / carousel / calendar / typography / virtualList)、其他(floatButton / grid / link / space / divider / splitter / backTop / affix / tour / watermark)。
分组:表单(button / copyButton / textField / inputNumber / inputTag / textarea / checkbox / checkboxGroup / colorPicker / switchButton / select / mentions / multiSelect / radioGroup / slider / rate / autoComplete / datePicker / timePicker / pinInput / segmented / upload / form + formItem / transfer / cascader)、反馈(alert / toast / notification / result / spinner / progress / skeleton / badge / empty / statistic / countdown)、浮层(tooltip / popover / popconfirm / modal / drawer / menu / commandPalette)、导航(tabs / breadcrumb / pagination / accordion / stepper / tree / treeSelect / anchor)、数据展示(dataTable / descriptions / card / list / kbd / highlight / timeline / tag / avatar / avatarGroup / image / carousel / calendar / typography / virtualList)、其他(floatButton / grid / link / space / divider / splitter / backTop / affix / tour / watermark)。
## 可访问性约定
@@ -32,7 +32,7 @@ accordion · affix · alert · anchor · autoComplete · avatar · avatarGroup
|---|---|
| `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:75 个 DOM 组件 + ui.css(只消费 `--ua-*` 令牌) |
| `packages/skin/ui` | @ui-arch/ui:76 个 DOM 组件 + ui.css(只消费 `--ua-*` 令牌) |
| `packages/design/tokens` | @ui-arch/tokens:设计令牌(暗色只换值不换名) |
| `packages/tools/cli` | @ui-arch/cli:`ua` 命令行与 stdio MCP server |
| `apps/demo` | 交互演示(esbuild 打包成单文件) |
+13
View File
@@ -43,6 +43,7 @@ import {
form,
formItem,
inputNumber,
inputTag,
grid,
link,
list,
@@ -258,6 +259,7 @@ const pinMask = signal(''); // 密文验证码输入值(mask 演示)
const resendCd = countdown({ seconds: 90, running: false, format: (s) => `重发(${s}s)` }); // 重发倒计时(手动启动演示)
const highlightText = signal('数据不出端、预测低于 50ms、习惯本地学习是输入法增强系统的三条底线'); // 高亮演示原文
const highlightKw = signal('本地'); // 高亮演示关键词(与搜索框信号联动)
const demoTags = signal<string[]>(['本地推理', '隐私优先']); // 标签输入演示集合(成标签 / 删 chip 双向写回)
const treeChecked = signal<string[]>(['core']); // 多选树勾选集合(外部受控演示)
const treePick = signal('core'); // 树形选中节点 key
const transferKeys = signal<string[]>(['a11y']); // 穿梭框右侧集合(外部受控演示)
@@ -1156,6 +1158,17 @@ const content = h(
textField({ placeholder: '输入关键词试试实时联动', value: highlightKw, size: 'sm' }),
),
),
section(
'sec-inputtag',
'标签输入',
'inputTag:回车 / 逗号成标签,chip 可单个删除,空框退格删末个,失焦自动入列;max 满额自动禁输。',
h(
'div',
{ class: 'row' },
inputTag({ value: demoTags, max: 5, onChange: (list) => toast.info(`当前 ${list.length} 个标签`) }),
h('span', { class: 'out' }, () => `已选:${demoTags().join(' / ') || '(空)'}`),
),
),
section(
'sec-affix',
'固钉',
+32 -1
View File
@@ -1,6 +1,6 @@
# @ui-arch 组件库 Agent 文档(无图形版)
> 自动生成于 2026-10-26T13:53:12.662Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。
> 自动生成于 2026-10-26T13:57:11.667Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。
> 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。
## 快速上手
@@ -2140,6 +2140,36 @@ highlight({ text: 'Alpha Beta', keyword: 'beta' })
现场演示:apps/docs/demo.html#sec-highlight
### inputTag — 标签输入 InputTag
参数:
- value(必填):Signal<string[]> —— 已选标签集合,双向绑定:成标签与删 chip 都写回信号
- placeholder:string —— 未满时的占位文案;缺省「输入后回车添加」
- max:number —— 最多允许的标签条数;缺省 0 表示不限,满额后输入框禁用并提示上限
- maxLength:number —— 单个标签最大字符数;缺省 24,超出直接截断
- allowDuplicate:boolean —— 是否允许重复标签;缺省 false(重复的静默丢弃)
- disabled:boolean | Signal<boolean> —— 禁用态:布尔或信号,true 时不可输入也不可删
- ariaLabel:string —— 输入框无障碍标签;缺省「标签输入」
- onChange:(tags: string[]) => void —— 标签集合变化回调,参数是变化后的完整集合
示例:
```
const tags = signal(['前端', '设计']);
inputTag({ value: tags, max: 5, onChange: (list) => console.log(list.length) })
```
手把手:
1. 传一个 signal<string[]> 作为 value:回车或逗号把当前输入转成 chip,写回信号后立即重画
2. 点 chip 上的 × 删除单个标签;输入框为空时按退格键删掉最后一个(手不离键盘也能改)
3. 输入后直接失焦也会成标签,不必强行回车;max 满额后输入框禁用,占位文案变成「最多 N 个标签」
4. 要区分大小写重复判断就传 allowDuplicate: true;默认按原样去重
5. 样式复用输入组与标签:外框聚焦光环、chip 圆角与语义色都跟既有组件一致,无新增视觉字面量
现场演示:apps/docs/demo.html#sec-inputtag
API 速览:
- button(props):按钮,variant 三档视觉强度,disabled / loading 支持布尔或信号动态同步(loading 转圈并禁点),onClick 点击回调
@@ -2219,6 +2249,7 @@ API 速览:
- countdown(props):倒计时,秒级递减到 0 自动停表并触发 onFinish,缺省 mm:ss 展示,返回句柄 start/stop/reset
- kbd(props):键盘按键标记,键帽样式展示单个按键或组合键(多键以 + 连接),用于快捷键提示
- highlight(props):关键词高亮,命中片段包 mark(大小写不敏感全部命中),text / keyword 支持信号响应式刷新
- inputTag(props):标签输入,输入后回车 / 逗号成标签、chip 可单个删除、空框退格删末个,value 信号双向绑定,max 满额自动禁输
一行用法:
+1
View File
@@ -124,6 +124,7 @@
- countdown(props):倒计时,秒级递减到 0 自动停表并触发 onFinish,缺省 mm:ss 展示,返回句柄 start/stop/reset
- kbd(props):键盘按键标记,键帽样式展示单个按键或组合键(多键以 + 连接),用于快捷键提示
- highlight(props):关键词高亮,命中片段包 mark(大小写不敏感全部命中),text / keyword 支持信号响应式刷新
- inputTag(props):标签输入,输入后回车 / 逗号成标签、chip 可单个删除、空框退格删末个,value 信号双向绑定,max 满额自动禁输
- 用法: `const checked = signal(false); checkbox({ label: '同意', checked }); const open = signal(false); modal({ title: '确认', open }, '内容')`
- 约定: 皮肤不持有业务状态,一律委托 headless 内核或调用方信号
- 约定: 表单类组件统一约定:*Props.value/checked/active 为 Signal 双向绑定,onChange/onInput 为回调
+1
View File
@@ -75,3 +75,4 @@ export { copyButton } from './copyButton.js';
export { countdown } from './countdown.js';
export { kbd } from './kbd.js';
export { highlight } from './highlight.js';
export { inputTag } from './inputTag.js';
+68
View File
@@ -0,0 +1,68 @@
import { effect, h, type Signal } from '@ui-arch/frame';
import type { InputTagProps } from './types.js';
/*** 作用:把布尔或信号口径的禁用态读成当前值(供渲染时统一判断,天然跟随信号) ***/
const off = (v: boolean | Signal<boolean> | undefined): boolean => (typeof v === 'function' ? v() : Boolean(v));
/*** 作用:标签输入——回车 / 逗号成标签、chip 逐个删、空框退格删末个、失焦自动入列;外框与 chip 复用现有类,零新增视觉字面量 ***/
export function inputTag(props: InputTagProps): HTMLElement {
const root = h('div', { class: 'ua-input-group ua-input-tag' }) as HTMLElement;
const hint = props.placeholder ?? '输入后回车添加'; // 未满时的占位文案
const input = h('input', { class: 'ua-input', placeholder: hint, 'aria-label': props.ariaLabel ?? '标签输入' }) as HTMLInputElement;
const limit = props.max ?? 0; // 0 = 不限条数
const maxLength = props.maxLength ?? 24; // 单个标签超长直接截断,防止一个标签撑爆整行
const allowDuplicate = props.allowDuplicate ?? false;
root.append(input);
/*** 作用:写回标签集合(单一出口:信号与 onChange 永远同源,避免两处各写一次) ***/
const put = (next: string[]): void => {
props.value(next);
props.onChange?.(next);
};
/*** 作用:追加一个标签:截断超长、丢弃重复与超限,并把输入框清空 ***/
const commit = (raw: string): void => {
const text = raw.trim().slice(0, maxLength);
input.value = '';
if (!text) return;
const list = props.value();
if (limit > 0 && list.length >= limit) return; // 已达上限:静默丢弃,输入框已清空
if (!allowDuplicate && list.includes(text)) return; // 重复标签:不重复入列
put([...list, text]);
};
/*** 作用:按信号重画 chip 与输入框状态(chip 全量重建,input 始终留在末尾) ***/
const render = (): void => {
const list = props.value();
root.querySelectorAll('.ua-tag').forEach((node) => node.remove()); // 先清后建:条数少,重建比 diff 稳
for (const tag of list) {
const close = h('button', {
type: 'button',
class: 'ua-tag-close',
'aria-label': `移除标签 ${tag}`,
onClick: () => put(props.value().filter((item) => item !== tag)), // 按值删,不用索引(外部可能已改序)
}, '×');
root.insertBefore(h('span', { class: 'ua-tag' }, h('span', {}, tag), close), input);
}
const full = limit > 0 && list.length >= limit;
input.disabled = off(props.disabled) || full; // 满额即禁输:占位文案同时改成「已达上限」
input.placeholder = full ? `最多 ${limit} 个标签` : hint;
};
input.addEventListener('keydown', (event) => {
if (event.key === 'Enter' || event.key === ',' || event.key === ',') {
event.preventDefault(); // 回车不提交外层表单;逗号不留进输入值
commit(input.value);
return;
}
if (event.key === 'Backspace' && input.value === '') {
const list = props.value();
if (list.length) put(list.slice(0, -1)); // 空框退格:删掉最后一个 chip
}
});
input.addEventListener('blur', () => commit(input.value)); // 失焦即入列:手输后忘了回车也不会丢
root.addEventListener('click', () => input.focus()); // 点外框任意位置都聚焦输入框
effect(render);
return root;
}
+12
View File
@@ -985,3 +985,15 @@ export interface CommandPaletteHandle {
open: () => void; // 打开:清空搜索词并聚焦输入框
close: () => void; // 关闭:归还打开前焦点
}
/*** 标签输入入参 ***/
export interface InputTagProps {
value: Signal<string[]>; // 已选标签集合,双向绑定(成标签 / 删 chip 都写回)
placeholder?: string; // 未满时的占位文案;缺省「输入后回车添加」
max?: number; // 最多允许的标签条数;缺省 0 表示不限
maxLength?: number; // 单个标签最大字符数;缺省 24(超出截断)
allowDuplicate?: boolean; // 是否允许重复标签;缺省 false
disabled?: boolean | Signal<boolean>; // 禁用态:布尔或信号,满额时也会禁用输入
ariaLabel?: string; // 输入框无障碍标签;缺省「标签输入」
onChange?: (tags: string[]) => void; // 标签集合变化回调(参数是变化后的完整集合)
}
+65 -1
View File
@@ -89,7 +89,8 @@
"copyButton(props):复制按钮,点击把 text 写入剪贴板并短暂显示已复制,text 可传函数取实时值,clipboard API 不可用时降级 execCommand",
"countdown(props):倒计时,秒级递减到 0 自动停表并触发 onFinish,缺省 mm:ss 展示,返回句柄 start/stop/reset",
"kbd(props):键盘按键标记,键帽样式展示单个按键或组合键(多键以 + 连接),用于快捷键提示",
"highlight(props):关键词高亮,命中片段包 mark(大小写不敏感全部命中),text / keyword 支持信号响应式刷新"
"highlight(props):关键词高亮,命中片段包 mark(大小写不敏感全部命中),text / keyword 支持信号响应式刷新",
"inputTag(props):标签输入,输入后回车 / 逗号成标签、chip 可单个删除、空框退格删末个,value 信号双向绑定,max 满额自动禁输"
],
"keyTypes": [
"ButtonProps",
@@ -3898,6 +3899,69 @@
"两个入参都接受信号:搜索框输入实时联动高亮,无需手动重渲染",
"大小写不敏感且全部命中(非只第一处);关键词两侧空白自动忽略"
]
},
{
"id": "inputTag",
"title": "标签输入 InputTag",
"demo": "sec-inputtag",
"props": [
{
"n": "value",
"t": "Signal<string[]>",
"r": 1,
"d": "已选标签集合,双向绑定:成标签与删 chip 都写回信号"
},
{
"n": "placeholder",
"t": "string",
"r": 0,
"d": "未满时的占位文案;缺省「输入后回车添加」"
},
{
"n": "max",
"t": "number",
"r": 0,
"d": "最多允许的标签条数;缺省 0 表示不限,满额后输入框禁用并提示上限"
},
{
"n": "maxLength",
"t": "number",
"r": 0,
"d": "单个标签最大字符数;缺省 24,超出直接截断"
},
{
"n": "allowDuplicate",
"t": "boolean",
"r": 0,
"d": "是否允许重复标签;缺省 false(重复的静默丢弃)"
},
{
"n": "disabled",
"t": "boolean | Signal<boolean>",
"r": 0,
"d": "禁用态:布尔或信号,true 时不可输入也不可删"
},
{
"n": "ariaLabel",
"t": "string",
"r": 0,
"d": "输入框无障碍标签;缺省「标签输入」"
},
{
"n": "onChange",
"t": "(tags: string[]) => void",
"r": 0,
"d": "标签集合变化回调,参数是变化后的完整集合"
}
],
"example": "const tags = signal(['前端', '设计']);\ninputTag({ value: tags, max: 5, onChange: (list) => console.log(list.length) })",
"steps": [
"传一个 signal<string[]> 作为 value:回车或逗号把当前输入转成 chip,写回信号后立即重画",
"点 chip 上的 × 删除单个标签;输入框为空时按退格键删掉最后一个(手不离键盘也能改)",
"输入后直接失焦也会成标签,不必强行回车;max 满额后输入框禁用,占位文案变成「最多 N 个标签」",
"要区分大小写重复判断就传 allowDuplicate: true;默认按原样去重",
"样式复用输入组与标签:外框聚焦光环、chip 圆角与语义色都跟既有组件一致,无新增视觉字面量"
]
}
]
}
+24
View File
@@ -5259,3 +5259,27 @@ h5.ua-type-title { font-size: var(--ua-font-size-md); }
animation: none; /* 降动效:转圈静态化为实色环 */
}
}
/* ---- 标签输入:外框复用输入组(边框 / 聚焦光环),chip 复用 .ua-tag,这里只排布 ----
多标签必须换行:单行 flex 会把 chip 挤成省略号,用户看不出已选几个 */
.ua-input-tag {
display: flex;
flex-wrap: wrap;
gap: var(--ua-space-1);
padding: var(--ua-space-1) var(--ua-space-2);
min-height: var(--ua-control-h-md);
cursor: text; /* 点框内任意处都能输入 */
}
.ua-input-tag .ua-input {
flex: 1 1 auto;
min-width: 0; /* 允许收缩:否则长 chip 会把输入框顶出容器 */
padding: var(--ua-space-05) 0; /* 纵向内距交给外框,输入框自带内距会让 chip 与文字错行 */
height: auto;
}
.ua-input-tag .ua-input:disabled {
background: transparent; /* 满额 / 禁用时外框仍显示 chip,内层不叠一层灰底 */
}
.ua-input-tag .ua-tag {
max-width: 100%;
overflow-wrap: anywhere;
}