diff --git a/.ua/index.json b/.ua/index.json index e5c254c..ef9b234 100644 --- a/.ua/index.json +++ b/.ua/index.json @@ -1,6 +1,6 @@ { "$schema": "ui-arch-registry-index/0.1", - "generatedAt": "2026-10-26T13:15:52.353Z", + "generatedAt": "2026-10-26T13:22:26.722Z", "repoRoot": ".", "packages": [ { @@ -213,7 +213,8 @@ "avatarGroup(props):头像组,横向叠放并在超出 max 时折叠成 +N,整组 title 列出全部姓名", "virtualList(props):虚拟列表,固定行高长列表只渲染可视窗口 + 上下缓冲条,万级数据滚动不随总量掉帧;返回 { el, scrollTo } 句柄", "commandPalette(props):命令面板,⌘K / Ctrl+K 唤起,输入即过滤(主文案/说明/关键词三层),↑↓ 环形选择 + 回车执行 + Esc 收起,选中与关闭都归还焦点;返回 { el, open, close } 句柄", - "timePicker(props):时间选择,时 / 分两列弹层,value 信号双向绑定 HH:mm,分钟可配步长,Esc 收起" + "timePicker(props):时间选择,时 / 分两列弹层,value 信号双向绑定 HH:mm,分钟可配步长,Esc 收起", + "pinInput(props):验证码输入,N 格单字符数字输入,输入自动进格、退格回删、方向键移格、粘贴自动分发,value 信号双向绑定,mask 可密文显示" ], "keyTypes": [ "ButtonProps", @@ -3837,6 +3838,49 @@ "收起态键盘微调:聚焦触发器按 ↑↓ 按步长加减(12:30 步长 15 → 12:45),Shift + ↑↓ 按小时加减,跨 00:00 / 23:xx 自动环形;改完同样触发 onChange", "打开弹层后焦点直接落在时列当前格(键盘用户不必再 Tab 找):列内 ↑↓ 立即生效,Esc 收起后焦点回到触发器" ] + }, + { + "id": "pinInput", + "title": "验证码输入 PinInput", + "demo": "sec-pininput", + "props": [ + { + "n": "value", + "t": "Signal", + "r": 1, + "d": "当前验证码(数字拼接串)信号,双向绑定:格子输入写回、外部赋值同步回格子" + }, + { + "n": "length", + "t": "number", + "df": "6", + "d": "格数;缺省 6" + }, + { + "n": "mask", + "t": "boolean", + "df": "false", + "d": "密文显示(type=password,并声明 one-time-code 便于短信验证码自动填充);缺省明文" + }, + { + "n": "disabled", + "t": "boolean | Signal", + "d": "禁用态:布尔直接置位,传信号则动态同步" + }, + { + "n": "onChange", + "t": "(value: string) => void", + "k": 1, + "d": "任一格变化回调(参数为当前已输入拼接串)" + } + ], + "example": "const code = signal(''); pinInput({ value: code, length: 6, onChange: (v) => console.log(v) })", + "steps": [ + "用 signal('') 创建验证码信号并传入 value;拼接串随输入实时更新,长度满格即验证码就绪", + "输入一位自动进下一格;退格在本格为空时回删上一格;←/→ 在格间移动;聚焦即全选,直接敲键覆盖", + "粘贴会自动分发:整段验证码(位数 ≥ 格数)从首格铺起,零散片段从当前格续填", + "mask: true 走密文显示并声明 one-time-code,短信验证码可自动填充;disabled 支持布尔或信号禁用" + ] } ] }, diff --git a/AGENTS.md b/AGENTS.md index a18b705..660fdd8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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):70 个 DOM 组件(表单 / 反馈 / 浮层 / 导航 / 表格),样式只消费令牌 +- `packages/skin/ui`(@ui-arch/ui):71 个 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 diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 2713e7f..a840282 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -34,7 +34,7 @@ ui-arch-mvp/ │ │ ├── table-core/ # 表格状态内核 │ │ └── frame/ # 信号 + hyperscript 微框架 │ ├── skin/ # 皮肤层(DOM 组件,可发布) -│ │ └── ui/ # 70 个组件 + ui.css +│ │ └── ui/ # 71 个组件 + ui.css │ ├── design/ # 资产层(可发布) │ │ └── tokens/ # --ua-* 设计令牌 │ └── tools/ # 工程层(private,不发布) diff --git a/README.md b/README.md index d0b49d6..ece22f5 100644 --- a/README.md +++ b/README.md @@ -12,13 +12,13 @@ npm run docs # 生成人类文档站 apps/docs/index.html + Agent 全 npm run changelog # 依据 git 提交史重写 CHANGELOG.md(发版前跑;changelog:check 可查漂移) ``` -## 组件(共 70 个) +## 组件(共 71 个) -accordion · affix · alert · anchor · autoComplete · avatar · avatarGroup · backTop · badge · breadcrumb · button · calendar · carousel · card · cascader · checkbox · checkboxGroup · colorPicker · commandPalette · dataTable · datePicker · descriptions · divider · drawer · empty · floatButton · form · grid · image · inputNumber · link · list · menu · modal · mentions · multiSelect · notification · pagination · 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 · dataTable · datePicker · descriptions · divider · drawer · empty · floatButton · form · grid · image · inputNumber · 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 -分组:表单(button / textField / inputNumber / textarea / checkbox / checkboxGroup / colorPicker / switchButton / select / mentions / multiSelect / radioGroup / slider / rate / autoComplete / datePicker / timePicker / segmented / upload / form + formItem / transfer / cascader)、反馈(alert / toast / notification / result / spinner / progress / skeleton / badge / empty / statistic)、浮层(tooltip / popover / popconfirm / modal / drawer / menu / commandPalette)、导航(tabs / breadcrumb / pagination / accordion / stepper / tree / treeSelect / anchor)、数据展示(dataTable / descriptions / card / list / timeline / tag / avatar / avatarGroup / image / carousel / calendar / typography / virtualList)、其他(floatButton / grid / link / space / divider / splitter / backTop / affix / tour / watermark)。 +分组:表单(button / 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)、浮层(tooltip / popover / popconfirm / modal / drawer / menu / commandPalette)、导航(tabs / breadcrumb / pagination / accordion / stepper / tree / treeSelect / anchor)、数据展示(dataTable / descriptions / card / list / 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:70 个 DOM 组件 + ui.css(只消费 `--ua-*` 令牌) | +| `packages/skin/ui` | @ui-arch/ui:71 个 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 9e7a2b6..9655084 100644 --- a/apps/demo/main.ts +++ b/apps/demo/main.ts @@ -54,6 +54,7 @@ import { mentions, multiSelect, pagination, + pinInput, popconfirm, popover, progress, @@ -247,6 +248,8 @@ const dateVal = signal('2026-10-26'); // 日期选择值(外部受控演示) const rangeFrom = signal(''); // 区间起点 const rangeTo = signal(''); // 区间终点 const acValue = signal(''); // 自动补全输入值(外部受控演示) +const pinCode = signal(''); // 验证码输入值(外部受控演示) +const pinMask = signal(''); // 密文验证码输入值(mask 演示) const treeChecked = signal(['core']); // 多选树勾选集合(外部受控演示) const treePick = signal('core'); // 树形选中节点 key const transferKeys = signal(['a11y']); // 穿梭框右侧集合(外部受控演示) @@ -1072,6 +1075,24 @@ const content = h( h('span', { class: 'out' }, () => `当前时间:${atTime() || '未选'}`), ), ), + section( + 'sec-pininput', + '验证码输入', + 'pinInput:N 格单字符数字输入,输入自动进格、退格回删、←/→ 移格、粘贴自动分发;mask 密文显示,disabled 禁用。', + h( + 'div', + { class: 'row' }, + pinInput({ value: pinCode, onChange: (v) => { if (v.length === 6) toast.success(`验证码 ${v} 已就绪`); } }), + h('span', { class: 'out' }, () => `当前:${pinCode() || '未输入'}(试试直接粘贴整段)`), + ), + h( + 'div', + { class: 'row' }, + pinInput({ value: pinMask, length: 4, mask: true }), + h('span', { class: 'out' }, () => `密文 4 格:${pinMask().length}/4`), + ), + h('div', { class: 'row' }, pinInput({ value: signal(''), length: 6, disabled: true })), + ), section( 'sec-affix', '固钉', diff --git a/llms-full.txt b/llms-full.txt index 8a23e8c..668802f 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -1,6 +1,6 @@ # @ui-arch 组件库 Agent 文档(无图形版) -> 自动生成于 2026-10-26T13:15:52.353Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。 +> 自动生成于 2026-10-26T13:22:26.722Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。 > 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。 ## 快速上手 @@ -2021,6 +2021,31 @@ const at = signal('09:30'); timePicker({ value: at, minuteStep: 15, onChange: (t 现场演示:apps/docs/demo.html#sec-timepicker +### pinInput — 验证码输入 PinInput + +参数: + +- value(必填):Signal —— 当前验证码(数字拼接串)信号,双向绑定:格子输入写回、外部赋值同步回格子 +- length(默认 6):number —— 格数;缺省 6 +- mask(默认 false):boolean —— 密文显示(type=password,并声明 one-time-code 便于短信验证码自动填充);缺省明文 +- disabled:boolean | Signal —— 禁用态:布尔直接置位,传信号则动态同步 +- onChange(回调):(value: string) => void —— 任一格变化回调(参数为当前已输入拼接串) + +示例: + +``` +const code = signal(''); pinInput({ value: code, length: 6, onChange: (v) => console.log(v) }) +``` + +手把手: + +1. 用 signal('') 创建验证码信号并传入 value;拼接串随输入实时更新,长度满格即验证码就绪 +2. 输入一位自动进下一格;退格在本格为空时回删上一格;←/→ 在格间移动;聚焦即全选,直接敲键覆盖 +3. 粘贴会自动分发:整段验证码(位数 ≥ 格数)从首格铺起,零散片段从当前格续填 +4. mask: true 走密文显示并声明 one-time-code,短信验证码可自动填充;disabled 支持布尔或信号禁用 + +现场演示:apps/docs/demo.html#sec-pininput + API 速览: - button(props):按钮,variant = primary | ghost | danger,disabled 支持布尔或信号禁用;type 缺省 button(表单内不隐式提交) @@ -2095,6 +2120,7 @@ API 速览: - virtualList(props):虚拟列表,固定行高长列表只渲染可视窗口 + 上下缓冲条,万级数据滚动不随总量掉帧;返回 { el, scrollTo } 句柄 - commandPalette(props):命令面板,⌘K / Ctrl+K 唤起,输入即过滤(主文案/说明/关键词三层),↑↓ 环形选择 + 回车执行 + Esc 收起,选中与关闭都归还焦点;返回 { el, open, close } 句柄 - timePicker(props):时间选择,时 / 分两列弹层,value 信号双向绑定 HH:mm,分钟可配步长,Esc 收起 +- pinInput(props):验证码输入,N 格单字符数字输入,输入自动进格、退格回删、方向键移格、粘贴自动分发,value 信号双向绑定,mask 可密文显示 一行用法: diff --git a/llms.txt b/llms.txt index e341a63..2b90a0f 100644 --- a/llms.txt +++ b/llms.txt @@ -119,6 +119,7 @@ - virtualList(props):虚拟列表,固定行高长列表只渲染可视窗口 + 上下缓冲条,万级数据滚动不随总量掉帧;返回 { el, scrollTo } 句柄 - commandPalette(props):命令面板,⌘K / Ctrl+K 唤起,输入即过滤(主文案/说明/关键词三层),↑↓ 环形选择 + 回车执行 + Esc 收起,选中与关闭都归还焦点;返回 { el, open, close } 句柄 - timePicker(props):时间选择,时 / 分两列弹层,value 信号双向绑定 HH:mm,分钟可配步长,Esc 收起 + - pinInput(props):验证码输入,N 格单字符数字输入,输入自动进格、退格回删、方向键移格、粘贴自动分发,value 信号双向绑定,mask 可密文显示 - 用法: `const checked = signal(false); checkbox({ label: '同意', checked }); const open = signal(false); modal({ title: '确认', open }, '内容')` - 约定: 皮肤不持有业务状态,一律委托 headless 内核或调用方信号 - 约定: 表单类组件统一约定:*Props.value/checked/active 为 Signal 双向绑定,onChange/onInput 为回调 diff --git a/packages/skin/ui/src/index.ts b/packages/skin/ui/src/index.ts index 0a9fadf..a720714 100644 --- a/packages/skin/ui/src/index.ts +++ b/packages/skin/ui/src/index.ts @@ -70,3 +70,4 @@ export { avatarGroup } from './avatarGroup.js'; export { virtualList } from './virtualList.js'; export { commandPalette } from './commandPalette.js'; export { timePicker } from './timePicker.js'; +export { pinInput } from './pinInput.js'; diff --git a/packages/skin/ui/src/pinInput.ts b/packages/skin/ui/src/pinInput.ts new file mode 100644 index 0000000..05dcb10 --- /dev/null +++ b/packages/skin/ui/src/pinInput.ts @@ -0,0 +1,62 @@ +import { effect, h } from '@ui-arch/frame'; +import type { PinInputProps } from './types.js'; + +/*** 验证码输入:N 格单字符数字输入,输入自动进格、退格回删、方向键移格、粘贴自动分发,value 信号双向绑定 ***/ +export function pinInput(props: PinInputProps): HTMLDivElement { + const length = props.length ?? 6; // 格数缺省 6 + const sanitize = (s: string): string => String(s).replace(/\D/g, '').slice(0, length); // 只留数字并截到格数 + const inputs: HTMLInputElement[] = []; // 格子引用(格子建一次不重渲染,焦点天然不丢) + const focusAt = (i: number): void => inputs[Math.max(0, Math.min(length - 1, i))]?.focus(); // 目标格钳制到首尾 + + /*** 写回单一来源:拼接各格内容赋回外部信号(同值短路防 effect 环),并回调当前拼接串 ***/ + const commit = (): void => { + const next = inputs.map((el) => el.value).join(''); + if (props.value() !== next) props.value(next); + if (props.onChange) props.onChange(next); + }; + + /*** 单格构造:输入进格 / 退格回删 / 方向键移格 / 粘贴分发,非数字键一律拦下 ***/ + const cell = (i: number): HTMLInputElement => h('input', { + class: 'ua-input ua-pin-cell', + type: props.mask ? 'password' : 'text', + inputmode: 'numeric', + autocomplete: props.mask ? 'one-time-code' : 'off', + maxlength: 1, + 'aria-label': `第 ${i + 1} 位`, + onFocus: () => inputs[i].select(), // 聚焦全选:直接敲键即覆盖旧值 + onInput: () => { + inputs[i].value = sanitize(inputs[i].value).slice(-1); // 单格只留最后敲入的一位 + if (inputs[i].value && i < length - 1) focusAt(i + 1); // 敲完进下一格(末格不进) + commit(); + }, + onKeydown: (e: KeyboardEvent) => { + if (e.key === 'Backspace') { e.preventDefault(); if (inputs[i].value) { inputs[i].value = ''; } else if (i > 0) { inputs[i - 1].value = ''; focusAt(i - 1); } commit(); return; } // 本格有值删本格,空格回删上一格并左移 + if (e.key === 'ArrowLeft') { e.preventDefault(); focusAt(i - 1); return; } + if (e.key === 'ArrowRight') { e.preventDefault(); focusAt(i + 1); return; } + if (e.key.length === 1 && !/\d/.test(e.key) && !e.ctrlKey && !e.metaKey) e.preventDefault(); // 只放行数字与控制键(粘贴由 onPaste 接管) + }, + onPaste: (e: ClipboardEvent) => { + const digits = sanitize(e.clipboardData?.getData('text') ?? ''); + if (!digits) return; + e.preventDefault(); + const from = digits.length >= length ? 0 : i; // 整段验证码从首格铺起,零散片段从当前格续 + for (let k = 0; k < digits.length && from + k < length; k++) inputs[from + k].value = digits[k]; // 逐格分发 + focusAt(Math.min(length - 1, from + digits.length)); // 落到末个空格,接着手输也顺手 + commit(); + }, + }) as HTMLInputElement; + + const root = h('div', { class: 'ua-pin-input', role: 'group' }, ...Array.from({ length }, (_, i) => cell(i))) as HTMLDivElement; + root.querySelectorAll('input').forEach((el) => inputs.push(el as HTMLInputElement)); + + effect(() => { // 外部信号 → 格子回写(同值不写避免打断输入法),禁用态支持布尔或信号 + const v = sanitize(props.value()); + const dis = typeof props.disabled === 'function' ? props.disabled() : !!props.disabled; + inputs.forEach((el, k) => { + const ch = v[k] ?? ''; + if (el.value !== ch) el.value = ch; + el.disabled = dis; + }); + }); + return root; +} diff --git a/packages/skin/ui/src/types.ts b/packages/skin/ui/src/types.ts index 05f2ff9..03eb6ec 100644 --- a/packages/skin/ui/src/types.ts +++ b/packages/skin/ui/src/types.ts @@ -245,6 +245,15 @@ export interface RateProps { onChange?: (value: number) => void; // 评分变化回调 } +/*** 验证码输入入参 ***/ +export interface PinInputProps { + value: Signal; // 当前验证码(数字拼接串),双向绑定(输入写回、外部赋值同步) + length?: number; // 格数;缺省 6 + mask?: boolean; // 密文显示(type=password + one-time-code 自动填充);缺省明文 + disabled?: boolean | Signal; // 禁用态:布尔直接置位,传信号则动态同步 + onChange?: (value: string) => void; // 任一格变化回调(参数为当前已输入拼接串) +} + /*** 空状态入参 ***/ export interface EmptyProps { title?: string; // 主标题;缺省不渲染 diff --git a/packages/skin/ui/ua.manifest.json b/packages/skin/ui/ua.manifest.json index 8e48be1..16a1cf4 100644 --- a/packages/skin/ui/ua.manifest.json +++ b/packages/skin/ui/ua.manifest.json @@ -84,7 +84,8 @@ "avatarGroup(props):头像组,横向叠放并在超出 max 时折叠成 +N,整组 title 列出全部姓名", "virtualList(props):虚拟列表,固定行高长列表只渲染可视窗口 + 上下缓冲条,万级数据滚动不随总量掉帧;返回 { el, scrollTo } 句柄", "commandPalette(props):命令面板,⌘K / Ctrl+K 唤起,输入即过滤(主文案/说明/关键词三层),↑↓ 环形选择 + 回车执行 + Esc 收起,选中与关闭都归还焦点;返回 { el, open, close } 句柄", - "timePicker(props):时间选择,时 / 分两列弹层,value 信号双向绑定 HH:mm,分钟可配步长,Esc 收起" + "timePicker(props):时间选择,时 / 分两列弹层,value 信号双向绑定 HH:mm,分钟可配步长,Esc 收起", + "pinInput(props):验证码输入,N 格单字符数字输入,输入自动进格、退格回删、方向键移格、粘贴自动分发,value 信号双向绑定,mask 可密文显示" ], "keyTypes": [ "ButtonProps", @@ -3708,6 +3709,49 @@ "收起态键盘微调:聚焦触发器按 ↑↓ 按步长加减(12:30 步长 15 → 12:45),Shift + ↑↓ 按小时加减,跨 00:00 / 23:xx 自动环形;改完同样触发 onChange", "打开弹层后焦点直接落在时列当前格(键盘用户不必再 Tab 找):列内 ↑↓ 立即生效,Esc 收起后焦点回到触发器" ] + }, + { + "id": "pinInput", + "title": "验证码输入 PinInput", + "demo": "sec-pininput", + "props": [ + { + "n": "value", + "t": "Signal", + "r": 1, + "d": "当前验证码(数字拼接串)信号,双向绑定:格子输入写回、外部赋值同步回格子" + }, + { + "n": "length", + "t": "number", + "df": "6", + "d": "格数;缺省 6" + }, + { + "n": "mask", + "t": "boolean", + "df": "false", + "d": "密文显示(type=password,并声明 one-time-code 便于短信验证码自动填充);缺省明文" + }, + { + "n": "disabled", + "t": "boolean | Signal", + "d": "禁用态:布尔直接置位,传信号则动态同步" + }, + { + "n": "onChange", + "t": "(value: string) => void", + "k": 1, + "d": "任一格变化回调(参数为当前已输入拼接串)" + } + ], + "example": "const code = signal(''); pinInput({ value: code, length: 6, onChange: (v) => console.log(v) })", + "steps": [ + "用 signal('') 创建验证码信号并传入 value;拼接串随输入实时更新,长度满格即验证码就绪", + "输入一位自动进下一格;退格在本格为空时回删上一格;←/→ 在格间移动;聚焦即全选,直接敲键覆盖", + "粘贴会自动分发:整段验证码(位数 ≥ 格数)从首格铺起,零散片段从当前格续填", + "mask: true 走密文显示并声明 one-time-code,短信验证码可自动填充;disabled 支持布尔或信号禁用" + ] } ] } diff --git a/packages/skin/ui/ui.css b/packages/skin/ui/ui.css index 5cd6f28..bb05203 100644 --- a/packages/skin/ui/ui.css +++ b/packages/skin/ui/ui.css @@ -5183,3 +5183,17 @@ h5.ua-type-title { font-size: var(--ua-font-size-md); } padding-left: var(--ua-space-1); border-left: 1px solid var(--ua-border-subtle); /* 时分分界:一条 1px 细线,比留白更能说明这是两列 */ } + +/* ---- 验证码输入:N 格单字符数字输入(pinInput) ---- */ +.ua-pin-input { + display: inline-flex; + gap: var(--ua-space-2); +} +.ua-pin-input .ua-input { + min-width: 0; + width: var(--ua-control-h-lg); /* 方格:宽高同档,正方形观感 */ + padding: 0; + text-align: center; + font-variant-numeric: tabular-nums; /* 数字等宽:换位不左右窜 */ + caret-color: var(--ua-color-accent); +}