From 85f009dff867f44e57a36bee9244b308445498c4 Mon Sep 17 00:00:00 2001 From: HIsheR Date: Mon, 26 Oct 2026 17:30:00 +0800 Subject: [PATCH] =?UTF-8?q?iter(g181):=20=E6=8E=A7=E4=BB=B6=E9=AB=98?= =?UTF-8?q?=E5=BA=A6=E7=BB=9F=E4=B8=80=E4=B8=89=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .ua/index.json | 22 ++++++++++++++- apps/demo/main.ts | 11 ++++++++ llms-full.txt | 6 ++++- packages/skin/ui/src/button.ts | 2 +- packages/skin/ui/src/inputNumber.ts | 2 +- packages/skin/ui/src/select.ts | 2 +- packages/skin/ui/src/textField.ts | 2 +- packages/skin/ui/src/types.ts | 4 +++ packages/skin/ui/ua.manifest.json | 20 ++++++++++++++ packages/skin/ui/ui.css | 36 ++++++++++++++++++++----- packages/tools/cli/lib/visual-rules.mjs | 30 ++++++++++++++++++++- 11 files changed, 123 insertions(+), 14 deletions(-) diff --git a/.ua/index.json b/.ua/index.json index 9d7d4e8..4a5488f 100644 --- a/.ua/index.json +++ b/.ua/index.json @@ -1,6 +1,6 @@ { "$schema": "ui-arch-registry-index/0.1", - "generatedAt": "2026-10-26T09:17:08.488Z", + "generatedAt": "2026-10-26T09:29:34.397Z", "repoRoot": ".", "packages": [ { @@ -343,6 +343,11 @@ "t": "() => void", "k": 1, "d": "点击回调" + }, + { + "n": "size", + "t": "'sm' | 'md' | 'lg'", + "d": "高度档位:sm 紧凑 / md 默认 / lg 大触控目标,走 --ua-control-h-* 三档;缺省 md" } ], "example": "button({ label: '提交', variant: 'primary', onClick: () => console.log('clicked') })", @@ -399,6 +404,11 @@ "n": "maxCount", "t": "number", "d": "字数上限:同时设原生 maxlength,并在输入框内侧显示实时计数;缺省不限" + }, + { + "n": "size", + "t": "'sm' | 'md' | 'lg'", + "d": "高度档位:sm 紧凑 / md 默认 / lg 大触控目标,走 --ua-control-h-* 三档;缺省 md" } ], "example": "const name = signal(''); const el = textField({ value: name, placeholder: '输入名字…', onInput: (v) => console.log(v) })", @@ -462,6 +472,11 @@ "n": "suffix", "t": "string | HTMLElement", "d": "后置附加块(textField/inputNumber 在输入框内侧后、textarea 在下方);缺省不渲染" + }, + { + "n": "size", + "t": "'sm' | 'md' | 'lg'", + "d": "高度档位:sm 紧凑 / md 默认 / lg 大触控目标,走 --ua-control-h-* 三档;缺省 md" } ], "example": "const qty = signal(1); const el = inputNumber({ value: qty, min: 1, max: 99, onChange: (v) => console.log(v) })", @@ -736,6 +751,11 @@ "t": "(value: string) => void", "k": 1, "d": "选中回调" + }, + { + "n": "size", + "t": "'sm' | 'md' | 'lg'", + "d": "高度档位:sm 紧凑 / md 默认 / lg 大触控目标,走 --ua-control-h-* 三档;缺省 md" } ], "example": "const city = signal(''); select({ options: [{ value: 'bj', label: '北京' }], value: city, placeholder: '选择城市…' })", diff --git a/apps/demo/main.ts b/apps/demo/main.ts index 6d5ebf3..2d76129 100644 --- a/apps/demo/main.ts +++ b/apps/demo/main.ts @@ -456,6 +456,17 @@ const content = h( inputNumber({ value: qty, disabled: locked }), inputNumber({ value: amount, min: 0, prefix: '¥', suffix: '件' }), ), + h('div', { class: 'out' }, '↓ 控件高度三档并排(sm / md / lg):button / textField / select / inputNumber 统一走 --ua-control-h-*'), + h( + 'div', + { class: 'row' }, + button({ label: '小号', size: 'sm' }), button({ label: '默认' }), button({ label: '大号', size: 'lg' }), + textField({ placeholder: 'sm', size: 'sm' }), textField({ placeholder: 'md' }), textField({ placeholder: 'lg', size: 'lg' }), + select({ options: [{ value: 'sm', label: 'sm' }, { value: 'md', label: 'md' }, { value: 'lg', label: 'lg' }], value: signal('md'), size: 'sm' }), + select({ options: [{ value: 'a', label: '选项 A' }, { value: 'b', label: '选项 B' }], value: signal('a') }), + select({ options: [{ value: 'a', label: '选项 A' }, { value: 'b', label: '选项 B' }], value: signal('a'), size: 'lg' }), + inputNumber({ value: qty, size: 'sm' }), inputNumber({ value: qty }), inputNumber({ value: qty, size: 'lg' }), + ), ), section( 'sec-feedback', diff --git a/llms-full.txt b/llms-full.txt index 0fdb675..cab587c 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -1,6 +1,6 @@ # @ui-arch 组件库 Agent 文档(无图形版) -> 自动生成于 2026-10-26T09:17:08.488Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。 +> 自动生成于 2026-10-26T09:29:34.397Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。 > 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。 ## 快速上手 @@ -87,6 +87,7 @@ image({ src: '/a.png', alt: '架构图', width: 160 }) - type:'button' | 'submit' | 'reset' —— 原生类型;缺省 button,表单内不会意外提交 - disabled:boolean | Signal —— 禁用态:布尔直接置位,传信号则动态同步 - onClick(回调):() => void —— 点击回调 +- size:'sm' | 'md' | 'lg' —— 高度档位:sm 紧凑 / md 默认 / lg 大触控目标,走 --ua-control-h-* 三档;缺省 md 示例: @@ -115,6 +116,7 @@ button({ label: '提交', variant: 'primary', onClick: () => console.log('clicke - suffix:string | HTMLElement —— 后置附加块(textField/inputNumber 在输入框内侧后、textarea 在下方);缺省不渲染 - clearable(默认 false):boolean —— 开启后有值即显示一键清空按钮;缺省 false - maxCount:number —— 字数上限:同时设原生 maxlength,并在输入框内侧显示实时计数;缺省不限 +- size:'sm' | 'md' | 'lg' —— 高度档位:sm 紧凑 / md 默认 / lg 大触控目标,走 --ua-control-h-* 三档;缺省 md 示例: @@ -145,6 +147,7 @@ const name = signal(''); const el = textField({ value: name, placeholder: '输 - onChange(回调):(value: number) => void —— 值变化回调(已夹取到合法区间) - prefix:string | HTMLElement —— 前置附加块(textField/inputNumber 在输入框内侧前、textarea 在上方);缺省不渲染 - suffix:string | HTMLElement —— 后置附加块(textField/inputNumber 在输入框内侧后、textarea 在下方);缺省不渲染 +- size:'sm' | 'md' | 'lg' —— 高度档位:sm 紧凑 / md 默认 / lg 大触控目标,走 --ua-control-h-* 三档;缺省 md 示例: @@ -286,6 +289,7 @@ const night = signal(true); switchButton({ label: '夜间模式', checked: night - clearable:boolean —— 有值时触发器右缘出现清空按钮(收起态按 Delete / Backspace 也能清);缺省 false,用于「可选筛选项」 - disabled:boolean | Signal —— 禁用态:布尔直接置位,传信号则动态同步;禁用时触发器置灰且不展开弹层 - onChange(回调):(value: string) => void —— 选中回调 +- size:'sm' | 'md' | 'lg' —— 高度档位:sm 紧凑 / md 默认 / lg 大触控目标,走 --ua-control-h-* 三档;缺省 md 示例: diff --git a/packages/skin/ui/src/button.ts b/packages/skin/ui/src/button.ts index 3c31d34..8f60320 100644 --- a/packages/skin/ui/src/button.ts +++ b/packages/skin/ui/src/button.ts @@ -5,7 +5,7 @@ import type { ButtonProps } from './types.js'; export function button(props: ButtonProps): HTMLElement { return h( 'button', - { class: `ua-btn ua-btn-${props.variant ?? 'primary'}`, type: props.type ?? 'button', disabled: props.disabled, onClick: props.onClick }, // 缺省 button:表单内不隐式提交 + { class: `ua-btn ua-btn-${props.variant ?? 'primary'}${props.size && props.size !== 'md' ? ` is-${props.size}` : ''}`, type: props.type ?? 'button', disabled: props.disabled, onClick: props.onClick }, // 缺省 button:表单内不隐式提交 props.label, ); } diff --git a/packages/skin/ui/src/inputNumber.ts b/packages/skin/ui/src/inputNumber.ts index adff4bc..57e2607 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', 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': '数字输入' }) 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/select.ts b/packages/skin/ui/src/select.ts index d504bd3..d66b106 100644 --- a/packages/skin/ui/src/select.ts +++ b/packages/skin/ui/src/select.ts @@ -36,7 +36,7 @@ export function select(props: SelectProps): HTMLElement { 'button', { type: 'button', - class: 'ua-select-trigger', + class: `ua-select-trigger${props.size && props.size !== 'md' ? ` is-${props.size}` : ''}`, 'aria-haspopup': 'listbox', 'aria-expanded': () => String(open()), disabled: props.disabled, // 布尔或信号禁用,h() 直接透传(原生 button 拦截点击) diff --git a/packages/skin/ui/src/textField.ts b/packages/skin/ui/src/textField.ts index f3d209d..a9836c9 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 一键清空,maxCount 显示字数统计 ***/ export function textField(props: TextFieldProps): HTMLElement { - const el = h('input', { class: 'ua-input', 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 }) 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/types.ts b/packages/skin/ui/src/types.ts index 1fe08bd..56f1c49 100644 --- a/packages/skin/ui/src/types.ts +++ b/packages/skin/ui/src/types.ts @@ -21,6 +21,7 @@ export interface PopoverProps { /*** 按钮入参 ***/ export interface ButtonProps { + size?: 'sm' | 'md' | 'lg'; // 高度档位:走 --ua-control-h-* 三档;缺省 md label: string; // 按钮文案 variant?: 'primary' | 'ghost' | 'danger'; // 视觉强度;缺省 primary type?: 'button' | 'submit' | 'reset'; // 原生类型;缺省 button(表单内不会意外触发提交) @@ -30,6 +31,7 @@ export interface ButtonProps { /*** 文本输入入参 ***/ export interface TextFieldProps { + size?: 'sm' | 'md' | 'lg'; // 高度档位:走 --ua-control-h-* 三档;缺省 md placeholder?: string; // 占位文案 value?: Signal; // 双向绑定的信号 disabled?: boolean | Signal; // 禁用态:布尔直接置位,传信号则动态同步 @@ -108,6 +110,7 @@ export interface SwitchProps { /*** 下拉选择入参 ***/ export interface SelectProps { + size?: 'sm' | 'md' | 'lg'; // 高度档位:走 --ua-control-h-* 三档;缺省 md options: OptionItem[]; // 选项列表 value?: Signal; // 选中值信号,双向绑定 placeholder?: string; // 未选中时的占位项文案 @@ -203,6 +206,7 @@ export interface TextareaProps { /*** 数字输入入参 ***/ export interface InputNumberProps { + size?: 'sm' | 'md' | 'lg'; // 高度档位:走 --ua-control-h-* 三档;缺省 md value: Signal; // 当前值信号,双向绑定(按钮/键盘写回、外部赋值同步) min?: number; // 最小值;缺省无下限 max?: number; // 最大值;缺省无上限 diff --git a/packages/skin/ui/ua.manifest.json b/packages/skin/ui/ua.manifest.json index d784c2c..0c1b0db 100644 --- a/packages/skin/ui/ua.manifest.json +++ b/packages/skin/ui/ua.manifest.json @@ -214,6 +214,11 @@ "t": "() => void", "k": 1, "d": "点击回调" + }, + { + "n": "size", + "t": "'sm' | 'md' | 'lg'", + "d": "高度档位:sm 紧凑 / md 默认 / lg 大触控目标,走 --ua-control-h-* 三档;缺省 md" } ], "example": "button({ label: '提交', variant: 'primary', onClick: () => console.log('clicked') })", @@ -270,6 +275,11 @@ "n": "maxCount", "t": "number", "d": "字数上限:同时设原生 maxlength,并在输入框内侧显示实时计数;缺省不限" + }, + { + "n": "size", + "t": "'sm' | 'md' | 'lg'", + "d": "高度档位:sm 紧凑 / md 默认 / lg 大触控目标,走 --ua-control-h-* 三档;缺省 md" } ], "example": "const name = signal(''); const el = textField({ value: name, placeholder: '输入名字…', onInput: (v) => console.log(v) })", @@ -333,6 +343,11 @@ "n": "suffix", "t": "string | HTMLElement", "d": "后置附加块(textField/inputNumber 在输入框内侧后、textarea 在下方);缺省不渲染" + }, + { + "n": "size", + "t": "'sm' | 'md' | 'lg'", + "d": "高度档位:sm 紧凑 / md 默认 / lg 大触控目标,走 --ua-control-h-* 三档;缺省 md" } ], "example": "const qty = signal(1); const el = inputNumber({ value: qty, min: 1, max: 99, onChange: (v) => console.log(v) })", @@ -607,6 +622,11 @@ "t": "(value: string) => void", "k": 1, "d": "选中回调" + }, + { + "n": "size", + "t": "'sm' | 'md' | 'lg'", + "d": "高度档位:sm 紧凑 / md 默认 / lg 大触控目标,走 --ua-control-h-* 三档;缺省 md" } ], "example": "const city = signal(''); select({ options: [{ value: 'bj', label: '北京' }], value: city, placeholder: '选择城市…' })", diff --git a/packages/skin/ui/ui.css b/packages/skin/ui/ui.css index e3bde7f..7b8fefc 100644 --- a/packages/skin/ui/ui.css +++ b/packages/skin/ui/ui.css @@ -6,6 +6,27 @@ outline: 2px solid var(--ua-color-accent); outline-offset: 2px; } +/* —— 控件高度三档:所有可交互控件统一走 --ua-control-h-*,size 档只改高度 —— */ +.ua-btn, +.ua-input, +.ua-select-trigger, +.ua-input-number-field { + box-sizing: border-box; + height: var(--ua-control-h-md); +} +.ua-btn.is-sm, +.ua-input.is-sm, +.ua-select-trigger.is-sm, +.ua-input-number-field.is-sm { + height: var(--ua-control-h-sm); +} +.ua-btn.is-lg, +.ua-input.is-lg, +.ua-select-trigger.is-lg, +.ua-input-number-field.is-lg { + height: var(--ua-control-h-lg); +} + .ua-btn { padding: var(--ua-space-15) var(--ua-space-35); border: 1px solid var(--ua-color-accent); @@ -913,6 +934,7 @@ body.ua-col-resizing { gap: var(--ua-space-1); } .ua-input-number .ua-btn { + height: auto; /* 步进器是图标按钮:不吃控件统一高度 */ padding: 5px var(--ua-space-3); /* 收窄为步进按钮 */ line-height: 1.4; } @@ -1311,7 +1333,7 @@ button.ua-step-dot:hover { } .ua-page-btn { min-width: 28px; - height: 28px; + height: var(--ua-control-h-sm); padding: 0 var(--ua-space-2); border: 1px solid var(--ua-color-border); border-radius: var(--ua-radius-sm); @@ -1380,7 +1402,7 @@ button.ua-step-dot:hover { } .ua-skeleton-circle { width: 40px; - height: 40px; + height: var(--ua-control-h-lg); border-radius: 50%; flex-shrink: 0; } @@ -2554,7 +2576,7 @@ button.ua-step-dot:hover { top: 50%; transform: translateY(-50%); width: 28px; - height: 28px; + height: var(--ua-control-h-sm); border: 1px solid var(--ua-color-border); border-radius: 50%; background: var(--ua-color-bg); @@ -2737,7 +2759,7 @@ button.ua-step-dot:hover { } .ua-upload-thumb { width: 28px; - height: 28px; + height: var(--ua-control-h-sm); object-fit: cover; border: 1px solid var(--ua-color-border); border-radius: var(--ua-radius-sm); @@ -2869,7 +2891,7 @@ button.ua-step-dot:hover { align-items: center; justify-content: center; width: 40px; - height: 40px; + height: var(--ua-control-h-lg); border-radius: 50%; font-size: var(--ua-font-size-lg); font-weight: 700; @@ -3364,10 +3386,10 @@ button.ua-step-dot:hover { margin-top: var(--ua-space-05); } .ua-datepicker-blank { - height: 28px; + height: var(--ua-control-h-sm); } .ua-datepicker-day { - height: 28px; + height: var(--ua-control-h-sm); font-size: var(--ua-font-size-sm); color: var(--ua-color-text); background: transparent; diff --git a/packages/tools/cli/lib/visual-rules.mjs b/packages/tools/cli/lib/visual-rules.mjs index 505c640..806d5da 100644 --- a/packages/tools/cli/lib/visual-rules.mjs +++ b/packages/tools/cli/lib/visual-rules.mjs @@ -41,6 +41,11 @@ export const KIND_META = [ label: '内距字面值', hint: 'padding/gap/margin 写死 px,应改用 --ua-space-*', }, + { + key: 'control-h-literal', + label: '控件高度字面值', + hint: 'height 写死控件档位值(28/34/40px),应改用 --ua-control-h-*', + }, ]; /*** 作用:需要扫描的 CSS 属性白名单(其余属性本轮不动;用白名单顺带避开伪类选择器误判) ***/ @@ -66,6 +71,9 @@ const SCAN_PROPS = new Set([ 'gap', 'row-gap', 'column-gap', + 'min-height', + 'max-height', + 'height', ]); /*** 作用:内距类属性集合(迁移与统计共用同一判定) ***/ @@ -136,6 +144,13 @@ const DURATION_STEPS = [ { max: 0.2501, token: 'var(--ua-duration-slow)' }, ]; +/*** 作用:控件高度三档的字面值 → 令牌(28/34/40px 三个值保留给控件高度档位,其余尺寸不入此口径) ***/ +const CONTROL_H_TOKENS = new Map([ + ['28px', 'var(--ua-control-h-sm)'], + ['34px', 'var(--ua-control-h-md)'], + ['40px', 'var(--ua-control-h-lg)'], +]); + /*** 作用:压缩空白,便于用稳定字符串比对(不改语义) ***/ export function squash(value) { return String(value).replace(/\s+/g, ' ').trim(); @@ -161,7 +176,7 @@ export function blankComments(src) { /*** 作用:逐条产出「属性 + 值」及其在原文中的偏移;只认白名单属性且必须以分号收尾(避开伪类选择器) ***/ export function eachDeclaration(code) { - const re = /(padding-top|padding-right|padding-bottom|padding-left|padding|margin-top|margin-right|margin-bottom|margin-left|margin|row-gap|column-gap|gap|transition-duration|animation-duration|transition|animation|font-size|box-shadow|border-radius|outline)\s*:\s*([^;{}]*);/gi; + const re = /(padding-top|padding-right|padding-bottom|padding-left|padding|margin-top|margin-right|margin-bottom|margin-left|margin|row-gap|column-gap|gap|min-height|max-height|height|transition-duration|animation-duration|transition|animation|font-size|box-shadow|border-radius|outline)\s*:\s*([^;{}]*);/gi; const list = []; for (const m of code.matchAll(re)) { const prop = m[1].toLowerCase(); @@ -266,6 +281,13 @@ function migrateSpace(core) { return { kind: 'space-literal', value: next }; } +/*** 作用:迁移单条控件高度:整值命中三档即换令牌(其余 height 值不动),返回 null 表示无需改动 ***/ +function migrateControlH(core) { + const token = CONTROL_H_TOKENS.get(squash(core)); + if (!token) return null; + return { kind: 'control-h-literal', value: token }; +} + /*** 作用:判定单条声明是否需要迁移,返回 { kind, value } 或 null(幂等:已是令牌就返回 null) ***/ export function migrateDeclaration(prop, rawValue) { const { core, tail } = splitImportant(rawValue); @@ -282,6 +304,8 @@ export function migrateDeclaration(prop, rawValue) { result = migrateShadow(core); } else if (SPACE_PROPS.has(prop)) { result = migrateSpace(core); + } else if (prop === 'height') { + result = migrateControlH(core); } if (!result) return null; return { kind: result.kind, value: result.value + tail }; @@ -328,6 +352,10 @@ function detect(prop, rawValue, key) { const tokens = String(value).match(/(^|[\s(,])(\d+(?:\.\d+)?)px\b/gi) ?? []; return tokens.filter((t) => !/(^|[\s(,])1px$/i.test(t)).length; } + if (key === 'control-h-literal') { + if (prop !== 'height') return 0; // min/max-height 是约束不是控件档位,不入此口径 + return CONTROL_H_TOKENS.has(squash(core)) ? 1 : 0; + } return 0; }