@manohub/ui 0.10.0 → 0.10.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -22,6 +22,11 @@ export interface TextareaAutoSize {
22
22
  * 附加位:`prefix` / `suffix` **插槽**里放什么由你决定(图标 / 单位 / 域名 / 你自己的可点元素),
23
23
  * 本件不内置标签件 —— 纯装饰形态无底色、不参与 Tab 序列,可点形态的热区与底色归消费方。
24
24
  *
25
+ * **属性落点**(用错位置就会「样式在壳上、属性在组件上被丢掉」):
26
+ * `class` / `style` → 表壳 `.mh-input__wrap`(整件的宽度与显式锚);**其余 attrs 一律透传到控件**
27
+ * (`id` / `name` / `aria-*` / `data-*` / `title` / 原生事件)—— 壳只是排版容器,属性落它上面等于丢掉
28
+ * —— 见 `forwardAttrs`;触发器形态另有 `controlClass` / `controlAttrs` 两个**专口子**(同名键以它为准)。
29
+ *
25
30
  * @example 受控 + 可清除
26
31
  * <Input modelValue={kw} placeholder="搜索" clearable prefix={<Icon name="search" />} onChange={setKw} />
27
32
  *
@@ -115,7 +120,7 @@ export declare const Input: import("vue").DefineComponent<import("vue").ExtractP
115
120
  type: NumberConstructor;
116
121
  default: undefined;
117
122
  };
118
- /** 右侧字数统计(单行须给 `maxlength` 才有意义) */
123
+ /** 右侧字数统计(**须给 `maxlength` 才渲染**:没有分母的 `n/0` 是坏值) */
119
124
  showCount: {
120
125
  type: BooleanConstructor;
121
126
  default: boolean;
@@ -145,7 +150,8 @@ export declare const Input: import("vue").DefineComponent<import("vue").ExtractP
145
150
  type: StringConstructor;
146
151
  default: undefined;
147
152
  };
148
- /** 展开到**控件元素**上的属性(触发器形态要 `popovertarget` / `aria-*` / `onKeydown` 落到控件上) */
153
+ /** 展开到**控件元素**上的属性(触发器形态要 `popovertarget` / `aria-*` / `onKeydown` 落到控件上);
154
+ * 与透传的 attrs 撞名时以本 prop 为准 */
149
155
  controlAttrs: {
150
156
  type: PropType<Record<string, unknown>>;
151
157
  default: undefined;
@@ -233,7 +239,7 @@ export declare const Input: import("vue").DefineComponent<import("vue").ExtractP
233
239
  type: NumberConstructor;
234
240
  default: undefined;
235
241
  };
236
- /** 右侧字数统计(单行须给 `maxlength` 才有意义) */
242
+ /** 右侧字数统计(**须给 `maxlength` 才渲染**:没有分母的 `n/0` 是坏值) */
237
243
  showCount: {
238
244
  type: BooleanConstructor;
239
245
  default: boolean;
@@ -263,7 +269,8 @@ export declare const Input: import("vue").DefineComponent<import("vue").ExtractP
263
269
  type: StringConstructor;
264
270
  default: undefined;
265
271
  };
266
- /** 展开到**控件元素**上的属性(触发器形态要 `popovertarget` / `aria-*` / `onKeydown` 落到控件上) */
272
+ /** 展开到**控件元素**上的属性(触发器形态要 `popovertarget` / `aria-*` / `onKeydown` 落到控件上);
273
+ * 与透传的 attrs 撞名时以本 prop 为准 */
267
274
  controlAttrs: {
268
275
  type: PropType<Record<string, unknown>>;
269
276
  default: undefined;
@@ -324,7 +331,7 @@ export interface TextareaProps {
324
331
  /** 是否带边框 */
325
332
  bordered?: boolean;
326
333
  maxlength?: number;
327
- /** 右下角字数统计(须给 `maxlength` 才有意义) */
334
+ /** 右下角字数统计(须给 `maxlength` 才渲染) */
328
335
  showCount?: boolean;
329
336
  autofocus?: boolean;
330
337
  onChange?: (v: string) => void;
@@ -332,8 +339,9 @@ export interface TextareaProps {
332
339
  /**
333
340
  * 多行输入(同一份面;结构与单行不同,故单列一个件)。
334
341
  *
335
- * ⚠️ 本件**没有** `mono` 档(`Input` 有)。要等宽的输入框别拿 `Text mono` 去包 —— 那会丢掉输入能力;
336
- * 该写法目前只能保留一条带 `font-family: var(--ui-font-mono)` 的应用侧样式登记(缺口见包内 `docs/kit-gaps.md`)。
342
+ * ⚠️ **输入族没有 `mono` 档**(`Input` 与 `Textarea` 都没有)。要等宽的输入框别拿 `Text mono` 去包
343
+ * —— 那会丢掉输入能力;目前只能由应用侧保留一条带 `font-family: var(--ui-font-mono)` 的样式登记
344
+ * (缺件登记见下游仓 `aihub-frontend/docs/kit-gaps.md`,本仓不复制那份台账)。
337
345
  *
338
346
  * @example 备注框:自适应 2~6 行 + 限长计数
339
347
  * <Textarea modelValue={note} autoSize={{ minRows: 2, maxRows: 6 }} maxlength={500} showCount onChange={setNote} />
@@ -388,6 +396,7 @@ export declare const Textarea: import("vue").DefineComponent<import("vue").Extra
388
396
  type: NumberConstructor;
389
397
  default: undefined;
390
398
  };
399
+ /** 右下角字数统计(**须给 `maxlength` 才渲染**:没有分母的 `n/0` 是坏值) */
391
400
  showCount: {
392
401
  type: BooleanConstructor;
393
402
  default: boolean;
@@ -452,6 +461,7 @@ export declare const Textarea: import("vue").DefineComponent<import("vue").Extra
452
461
  type: NumberConstructor;
453
462
  default: undefined;
454
463
  };
464
+ /** 右下角字数统计(**须给 `maxlength` 才渲染**:没有分母的 `n/0` 是坏值) */
455
465
  showCount: {
456
466
  type: BooleanConstructor;
457
467
  default: boolean;
@@ -559,7 +569,7 @@ export declare const InputCompound: {
559
569
  type: NumberConstructor;
560
570
  default: undefined;
561
571
  };
562
- /** 右侧字数统计(单行须给 `maxlength` 才有意义) */
572
+ /** 右侧字数统计(**须给 `maxlength` 才渲染**:没有分母的 `n/0` 是坏值) */
563
573
  showCount: {
564
574
  type: BooleanConstructor;
565
575
  default: boolean;
@@ -589,7 +599,8 @@ export declare const InputCompound: {
589
599
  type: StringConstructor;
590
600
  default: undefined;
591
601
  };
592
- /** 展开到**控件元素**上的属性(触发器形态要 `popovertarget` / `aria-*` / `onKeydown` 落到控件上) */
602
+ /** 展开到**控件元素**上的属性(触发器形态要 `popovertarget` / `aria-*` / `onKeydown` 落到控件上);
603
+ * 与透传的 attrs 撞名时以本 prop 为准 */
593
604
  controlAttrs: {
594
605
  type: PropType<Record<string, unknown>>;
595
606
  default: undefined;
@@ -711,7 +722,7 @@ export declare const InputCompound: {
711
722
  type: NumberConstructor;
712
723
  default: undefined;
713
724
  };
714
- /** 右侧字数统计(单行须给 `maxlength` 才有意义) */
725
+ /** 右侧字数统计(**须给 `maxlength` 才渲染**:没有分母的 `n/0` 是坏值) */
715
726
  showCount: {
716
727
  type: BooleanConstructor;
717
728
  default: boolean;
@@ -741,7 +752,8 @@ export declare const InputCompound: {
741
752
  type: StringConstructor;
742
753
  default: undefined;
743
754
  };
744
- /** 展开到**控件元素**上的属性(触发器形态要 `popovertarget` / `aria-*` / `onKeydown` 落到控件上) */
755
+ /** 展开到**控件元素**上的属性(触发器形态要 `popovertarget` / `aria-*` / `onKeydown` 落到控件上);
756
+ * 与透传的 attrs 撞名时以本 prop 为准 */
745
757
  controlAttrs: {
746
758
  type: PropType<Record<string, unknown>>;
747
759
  default: undefined;
@@ -854,7 +866,7 @@ export declare const InputCompound: {
854
866
  type: NumberConstructor;
855
867
  default: undefined;
856
868
  };
857
- /** 右侧字数统计(单行须给 `maxlength` 才有意义) */
869
+ /** 右侧字数统计(**须给 `maxlength` 才渲染**:没有分母的 `n/0` 是坏值) */
858
870
  showCount: {
859
871
  type: BooleanConstructor;
860
872
  default: boolean;
@@ -884,7 +896,8 @@ export declare const InputCompound: {
884
896
  type: StringConstructor;
885
897
  default: undefined;
886
898
  };
887
- /** 展开到**控件元素**上的属性(触发器形态要 `popovertarget` / `aria-*` / `onKeydown` 落到控件上) */
899
+ /** 展开到**控件元素**上的属性(触发器形态要 `popovertarget` / `aria-*` / `onKeydown` 落到控件上);
900
+ * 与透传的 attrs 撞名时以本 prop 为准 */
888
901
  controlAttrs: {
889
902
  type: PropType<Record<string, unknown>>;
890
903
  default: undefined;
@@ -975,6 +988,7 @@ export declare const InputCompound: {
975
988
  type: NumberConstructor;
976
989
  default: undefined;
977
990
  };
991
+ /** 右下角字数统计(**须给 `maxlength` 才渲染**:没有分母的 `n/0` 是坏值) */
978
992
  showCount: {
979
993
  type: BooleanConstructor;
980
994
  default: boolean;
@@ -1039,6 +1053,7 @@ export declare const InputCompound: {
1039
1053
  type: NumberConstructor;
1040
1054
  default: undefined;
1041
1055
  };
1056
+ /** 右下角字数统计(**须给 `maxlength` 才渲染**:没有分母的 `n/0` 是坏值) */
1042
1057
  showCount: {
1043
1058
  type: BooleanConstructor;
1044
1059
  default: boolean;
@@ -78,7 +78,7 @@ const Input = /* @__PURE__ */ defineComponent({
78
78
  type: Number,
79
79
  default: void 0
80
80
  },
81
- /** 右侧字数统计(单行须给 `maxlength` 才有意义) */
81
+ /** 右侧字数统计(**须给 `maxlength` 才渲染**:没有分母的 `n/0` 是坏值) */
82
82
  showCount: {
83
83
  type: Boolean,
84
84
  default: false
@@ -104,6 +104,10 @@ const Input = /* @__PURE__ */ defineComponent({
104
104
  * 原生 `<button>` 才有的语义(如 `popovertarget` 触发 popover —— 该属性只认 button)。
105
105
  * 缺省 `as="input"` 的渲染路径除本组 prop 外零变化。
106
106
  *
107
+ * **单行契约(与输入框同源,面层承担)**:控件是单行盒,值再长也不换行(超出走省略号)
108
+ * —— 见 `styles/components/input.css` 的 `.mh-input--trigger`。故 `default` 插槽里给**裸文本**
109
+ * 也安全,不需要自己再包一层截断 span。
110
+ *
107
111
  * **无值时的占位**:`<button>` 没有 `::placeholder` 伪元素可用,故 `placeholder` 在这个形态下
108
112
  * 由本件渲染成一个 span(`.mh-input__placeholder`,色档与原生占位一致);判据是
109
113
  * `hasValue === false && placeholder` **同时**成立 —— 两者缺一即沿用消费方给的 `default` 插槽。
@@ -119,7 +123,8 @@ const Input = /* @__PURE__ */ defineComponent({
119
123
  type: String,
120
124
  default: void 0
121
125
  },
122
- /** 展开到**控件元素**上的属性(触发器形态要 `popovertarget` / `aria-*` / `onKeydown` 落到控件上) */
126
+ /** 展开到**控件元素**上的属性(触发器形态要 `popovertarget` / `aria-*` / `onKeydown` 落到控件上);
127
+ * 与透传的 attrs 撞名时以本 prop 为准 */
123
128
  controlAttrs: {
124
129
  type: Object,
125
130
  default: void 0
@@ -143,6 +148,13 @@ const Input = /* @__PURE__ */ defineComponent({
143
148
  const inner = ref(props.defaultValue);
144
149
  const current = computed(() => props.modelValue ?? inner.value);
145
150
  const revealed = ref(false);
151
+ const forwardAttrs = computed(() => {
152
+ const rest = {};
153
+ for (const [key, value] of Object.entries(attrs)) {
154
+ if (key !== "class" && key !== "style") rest[key] = value;
155
+ }
156
+ return rest;
157
+ });
146
158
  const visualStatus = computed(() => props.status ?? (props.invalid ? "error" : void 0));
147
159
  const isInvalid = computed(() => visualStatus.value === "error");
148
160
  const commit = (value) => {
@@ -163,6 +175,7 @@ const Input = /* @__PURE__ */ defineComponent({
163
175
  const isTrigger = computed(() => props.as === "button");
164
176
  const controlClasses = computed(() => ["mh-input", isTrigger.value ? "mh-input--trigger" : "", props.size === "md" ? "" : `mh-input--${props.size}`, props.bordered ? "" : "mh-input--borderless", visualStatus.value ? `is-${visualStatus.value}` : "", props.disabled ? "is-disabled" : "", props.controlClass].filter(Boolean).join(" "));
165
177
  const showClear = computed(() => props.clearable && (props.hasValue ?? !!current.value));
178
+ const showCounter = computed(() => !isTrigger.value && props.showCount && props.maxlength !== void 0);
166
179
  const showPlaceholder = computed(() => isTrigger.value && props.hasValue === false && !!props.placeholder);
167
180
  return () => createVNode("div", {
168
181
  "class": ["mh-input__wrap", attrs.class].filter(Boolean).join(" "),
@@ -175,7 +188,7 @@ const Input = /* @__PURE__ */ defineComponent({
175
188
  本件的 Enter / Delete 是**输入框**语义,触发器有自己的一套(如 `↓` 开面板),不在此套用。 */
176
189
  createVNode("button", mergeProps({
177
190
  "type": "button"
178
- }, props.controlAttrs, {
191
+ }, forwardAttrs.value, props.controlAttrs, {
179
192
  "class": controlClasses.value,
180
193
  "disabled": props.disabled,
181
194
  "aria-invalid": isInvalid.value ? "true" : void 0
@@ -185,7 +198,7 @@ const Input = /* @__PURE__ */ defineComponent({
185
198
  "class": "mh-input__placeholder"
186
199
  }, [props.placeholder])
187
200
  ) : slots.default ? slots.default() : null])
188
- ) : createVNode("input", {
201
+ ) : createVNode("input", mergeProps(forwardAttrs.value, {
189
202
  "class": controlClasses.value,
190
203
  "type": inputType.value,
191
204
  "value": current.value,
@@ -197,7 +210,7 @@ const Input = /* @__PURE__ */ defineComponent({
197
210
  "aria-invalid": isInvalid.value ? "true" : void 0,
198
211
  "onInput": onInput,
199
212
  "onKeydown": onKeydown
200
- }, null), showClear.value ? createVNode("button", {
213
+ }), null), showClear.value ? createVNode("button", {
201
214
  "type": "button",
202
215
  "class": "mh-input__clear",
203
216
  "aria-label": "清除",
@@ -219,9 +232,9 @@ const Input = /* @__PURE__ */ defineComponent({
219
232
  "class": "mh-input__eye-icon"
220
233
  }, null)]) : null, slots.suffix ? createVNode("span", {
221
234
  "class": "mh-input__suffix"
222
- }, [slots.suffix()]) : null, !isTrigger.value && props.showCount ? createVNode("span", {
235
+ }, [slots.suffix()]) : null, showCounter.value ? createVNode("span", {
223
236
  "class": "mh-input__counter"
224
- }, [current.value?.length ?? 0, createTextVNode("/"), props.maxlength ?? 0]) : null]);
237
+ }, [current.value?.length ?? 0, createTextVNode("/"), props.maxlength]) : null]);
225
238
  }
226
239
  });
227
240
  const Textarea = /* @__PURE__ */ defineComponent({
@@ -277,6 +290,7 @@ const Textarea = /* @__PURE__ */ defineComponent({
277
290
  type: Number,
278
291
  default: void 0
279
292
  },
293
+ /** 右下角字数统计(**须给 `maxlength` 才渲染**:没有分母的 `n/0` 是坏值) */
280
294
  showCount: {
281
295
  type: Boolean,
282
296
  default: false
@@ -299,6 +313,14 @@ const Textarea = /* @__PURE__ */ defineComponent({
299
313
  }) {
300
314
  const inner = ref(props.defaultValue);
301
315
  const current = computed(() => props.modelValue ?? inner.value);
316
+ const forwardAttrs = computed(() => {
317
+ const rest = {};
318
+ for (const [key, value] of Object.entries(attrs)) {
319
+ if (key !== "class" && key !== "style") rest[key] = value;
320
+ }
321
+ return rest;
322
+ });
323
+ const showCounter = computed(() => props.showCount && props.maxlength !== void 0);
302
324
  const visualStatus = computed(() => props.status ?? (props.invalid ? "error" : void 0));
303
325
  const isInvalid = computed(() => visualStatus.value === "error");
304
326
  const autoRows = computed(() => {
@@ -320,7 +342,7 @@ const Textarea = /* @__PURE__ */ defineComponent({
320
342
  return () => createVNode("div", {
321
343
  "class": ["mh-input__wrap", attrs.class].filter(Boolean).join(" "),
322
344
  "style": attrs.style
323
- }, [createVNode("textarea", {
345
+ }, [createVNode("textarea", mergeProps(forwardAttrs.value, {
324
346
  "class": ["mh-input", "mh-input--textarea", props.size === "md" ? "" : `mh-input--${props.size}`, props.bordered ? "" : "mh-input--borderless", visualStatus.value ? `is-${visualStatus.value}` : "", props.disabled ? "is-disabled" : ""].filter(Boolean).join(" "),
325
347
  "rows": autoRows.value,
326
348
  "value": current.value,
@@ -331,9 +353,9 @@ const Textarea = /* @__PURE__ */ defineComponent({
331
353
  "autofocus": props.autofocus,
332
354
  "aria-invalid": isInvalid.value ? "true" : void 0,
333
355
  "onInput": onInput
334
- }, null), props.showCount ? createVNode("span", {
356
+ }), null), showCounter.value ? createVNode("span", {
335
357
  "class": "mh-input__counter"
336
- }, [current.value?.length ?? 0, createTextVNode("/"), props.maxlength ?? 0]) : null]);
358
+ }, [current.value?.length ?? 0, createTextVNode("/"), props.maxlength]) : null]);
337
359
  }
338
360
  });
339
361
  const InputDisplay = /* @__PURE__ */ defineComponent({
@@ -350,10 +372,17 @@ const InputDisplay = /* @__PURE__ */ defineComponent({
350
372
  slots,
351
373
  attrs
352
374
  }) {
353
- return () => createVNode("span", {
375
+ const forwardAttrs = computed(() => {
376
+ const rest = {};
377
+ for (const [key, value] of Object.entries(attrs)) {
378
+ if (key !== "class" && key !== "style") rest[key] = value;
379
+ }
380
+ return rest;
381
+ });
382
+ return () => createVNode("span", mergeProps(forwardAttrs.value, {
354
383
  "class": ["mh-input--display", attrs.class].filter(Boolean).join(" "),
355
384
  "style": attrs.style
356
- }, [slots.default ? slots.default() : props.value]);
385
+ }), [slots.default ? slots.default() : props.value]);
357
386
  }
358
387
  });
359
388
  Object.assign(Input, {
@@ -32,6 +32,9 @@
32
32
  * (不做 JS 定位、不做 resize 监听)。本件只补原生做不到的:选中值回填、多选数组的增删与收纳、
33
33
  * 搜索过滤与「创建」项、以及键盘的下拉导航(↑↓ 移动高亮行)。
34
34
  *
35
+ * **面板有高度上限**:`--ui-select-panel-max-height`(默认 320px,钉在 `select.css` 的面板根上)——
36
+ * 超出即面板内容区(`.mh-select__body`)滚动、搜索行常驻;列表与 `SelectTree` 两形态共用这一份。
37
+ *
35
38
  * ## 状态三源等价
36
39
  *
37
40
  * 选中行 = `[data-state='selected']`(组件下发)/ 原生 `input:checked`(静态页零 JS 路径)/
@@ -87,7 +90,7 @@ export declare const Select: import("vue").DefineComponent<import("vue").Extract
87
90
  type: StringConstructor;
88
91
  default: undefined;
89
92
  };
90
- /** 搜索:`true` 强制开;不给时选项多于 8 条自动开 */
93
+ /** 搜索行是否出现(**只认显式开启**;缺省不出 —— 不在面板里凭空多出一行) */
91
94
  searchable: {
92
95
  type: BooleanConstructor;
93
96
  default: boolean;
@@ -176,7 +179,7 @@ export declare const Select: import("vue").DefineComponent<import("vue").Extract
176
179
  type: StringConstructor;
177
180
  default: undefined;
178
181
  };
179
- /** 搜索:`true` 强制开;不给时选项多于 8 条自动开 */
182
+ /** 搜索行是否出现(**只认显式开启**;缺省不出 —— 不在面板里凭空多出一行) */
180
183
  searchable: {
181
184
  type: BooleanConstructor;
182
185
  default: boolean;
@@ -41,7 +41,7 @@ const Select = /* @__PURE__ */ defineComponent({
41
41
  type: String,
42
42
  default: void 0
43
43
  },
44
- /** 搜索:`true` 强制开;不给时选项多于 8 条自动开 */
44
+ /** 搜索行是否出现(**只认显式开启**;缺省不出 —— 不在面板里凭空多出一行) */
45
45
  searchable: {
46
46
  type: Boolean,
47
47
  default: false
@@ -122,7 +122,6 @@ const Select = /* @__PURE__ */ defineComponent({
122
122
  return list;
123
123
  });
124
124
  const selectedOptions = computed(() => flatOptions.value.filter((option) => selectedValues.value.includes(option.value)));
125
- const showSearch = computed(() => props.searchable || flatOptions.value.length > 8);
126
125
  const matched = (option, kw) => props.filterOption ? props.filterOption(option, kw) : option.label.includes(kw);
127
126
  const visibleOptions = computed(() => {
128
127
  const kw = keyword.value;
@@ -215,7 +214,7 @@ const Select = /* @__PURE__ */ defineComponent({
215
214
  }, null), createVNode(SelectPanel, {
216
215
  "id": panelId,
217
216
  "elementRef": panelRef,
218
- "searchable": showSearch.value,
217
+ "searchable": props.searchable,
219
218
  "keyword": keyword.value,
220
219
  "onKeyword": (value) => {
221
220
  keyword.value = value;
@@ -4,6 +4,8 @@
4
4
  * 它拥有「面板里那一撮选项」的全部形态与状态行:平铺选项 / 分组(组标题不可点)/ 「创建」行 /
5
5
  * 空态行 / 加载行。**不含面板壳与搜索行**(那是 `SelectPanel` 的事),也不持有任何状态 ——
6
6
  * 过滤结果、选中值、是否可创建统统由消费方算好喂进来,本件只渲染。
7
+ * 行由面板壳装在**内容区**(`.mh-select__body`,面板里唯一的滚动容器)里,限高与滚动都归面层,
8
+ * 本件不写第二个滚动容器。
7
9
  *
8
10
  * 选项是真实 `<button role="option">`(不是 div + JS 高亮):键盘可达、`:focus-visible` 可直接
9
11
  * 承载「高亮行」这层语义,于是不需要再维护一份 JS 高亮下标 —— **焦点即高亮**,
@@ -1,10 +1,15 @@
1
1
  /**
2
2
  * SelectPanel —— 选择器族的**面板壳内件**(`Select` 与 `SelectTree` 共用,不对外导出)。
3
3
  *
4
- * 它只拥有三样东西:**原生 popover 元素**(开合 / Esc / 点外部关闭 / 顶层渲染 / 定位全归浏览器)、
5
- * **搜索行**(面板顶部那一个输入框)、以及**内容槽**(列表体或树体)。
4
+ * 它只拥有四样东西:**原生 popover 元素**(开合 / Esc / 点外部关闭 / 顶层渲染 / 定位全归浏览器)、
5
+ * **搜索行**(面板顶部那一个输入框)、**内容区**(`.mh-select__body`,面板里唯一的滚动容器)
6
+ * 与**内容槽**(列表体或树体)。
6
7
  * 「面板里装什么」不在本件 —— 这正是让列表与树共用同一份面板皮肤的原因。
7
8
  *
9
+ * **限高在面层**:面板根上钉着 `--ui-select-panel-max-height`(`select.css`),超出即内容区滚动、
10
+ * 搜索行常驻不滚。内容区这一层因此是**结构性的**(不是可有可无的包裹):少了它,长列表 / 长树
11
+ * 会把面板一路撑高(实测 60 条选项 2212px),搜索行也会跟着滚走。本件只负责把它渲染出来。
12
+ *
8
13
  * 组合类名与服务端三条属性都是**操作层的组合**,不是本件新造的浮层:
9
14
  * `class="mh-popup mh-select__panel mh-popup--anchored"`(状态根 + 本族皮肤 + 显式锚档)、
10
15
  * `data-side` / `data-align`(`popup.css` 的定位档)、`popover="auto"`(原生轻 dismiss)。
@@ -27,7 +32,7 @@ export declare const SelectPanel: import("vue").DefineComponent<import("vue").Ex
27
32
  type: PropType<ElementRef>;
28
33
  default: undefined;
29
34
  };
30
- /** 是否渲染搜索行(列表形态:`searchable` 或选项多于 8 条;树形态同口径) */
35
+ /** 是否渲染搜索行(**只认消费方显式开启**:列表形态与树形态同口径,件不按数据量自动决断) */
31
36
  searchable: {
32
37
  type: BooleanConstructor;
33
38
  default: boolean;
@@ -66,7 +71,7 @@ export declare const SelectPanel: import("vue").DefineComponent<import("vue").Ex
66
71
  type: PropType<ElementRef>;
67
72
  default: undefined;
68
73
  };
69
- /** 是否渲染搜索行(列表形态:`searchable` 或选项多于 8 条;树形态同口径) */
74
+ /** 是否渲染搜索行(**只认消费方显式开启**:列表形态与树形态同口径,件不按数据量自动决断) */
70
75
  searchable: {
71
76
  type: BooleanConstructor;
72
77
  default: boolean;
@@ -13,7 +13,7 @@ const SelectPanel = /* @__PURE__ */ defineComponent({
13
13
  type: Object,
14
14
  default: void 0
15
15
  },
16
- /** 是否渲染搜索行(列表形态:`searchable` 或选项多于 8 条;树形态同口径) */
16
+ /** 是否渲染搜索行(**只认消费方显式开启**:列表形态与树形态同口径,件不按数据量自动决断) */
17
17
  searchable: {
18
18
  type: Boolean,
19
19
  default: false
@@ -64,7 +64,9 @@ const SelectPanel = /* @__PURE__ */ defineComponent({
64
64
  "value": props.keyword,
65
65
  "placeholder": props.searchPlaceholder,
66
66
  "onInput": (event) => props.onKeyword?.(event.target.value)
67
- }, null)]) : null, slots.default?.()]);
67
+ }, null)]) : null, createVNode("div", {
68
+ "class": "mh-select__body"
69
+ }, [slots.default?.()])]);
68
70
  }
69
71
  });
70
72
  export {
@@ -10,7 +10,9 @@
10
10
  * 2. **本地过滤树**:搜索行在场时按关键词过滤,命中节点的**祖先链保留**、父节点自身命中则
11
11
  * **整支保留**,并强制全展到命中层(算法与切分都是纯函数,见 `select-tree-filter.ts`);
12
12
  * 命中文本按面层的 `.mh-select__hit` 高亮(靠 `Tree` 的可选 `renderLabel` 钩子落进标签槽)。
13
- * 3. **面板内限高滚动**:树体包在 `.mh-select__tree` 里(内距 + 限高 + 溢出滚动),长树不撑破面板。
13
+ * 3. **面板内限高滚动**:限高归面板(`.mh-select__panel` 的 `--ui-select-panel-max-height`)、
14
+ * 溢出由面板内容区(`.mh-select__body`)承担;本件只把树包在 `.mh-select__tree` 里给内距
15
+ * —— 限高不由形态自持,列表形态共用同一份。
14
16
  *
15
17
  * ## 与 `Select` 的三处有意差异(都是「树不是列表」决定的)
16
18
  *
@@ -65,7 +67,7 @@ export declare const SelectTree: import("vue").DefineComponent<import("vue").Ext
65
67
  type: StringConstructor;
66
68
  default: string;
67
69
  };
68
- /** 搜索:`true` 强制开;不给时节点总数多于 8 自动开(与 `Select` 同口径) */
70
+ /** 搜索行是否出现(**只认显式开启**;缺省不出 —— 不在面板里凭空多出一行) */
69
71
  searchable: {
70
72
  type: BooleanConstructor;
71
73
  default: boolean;
@@ -174,7 +176,7 @@ export declare const SelectTree: import("vue").DefineComponent<import("vue").Ext
174
176
  type: StringConstructor;
175
177
  default: string;
176
178
  };
177
- /** 搜索:`true` 强制开;不给时节点总数多于 8 自动开(与 `Select` 同口径) */
179
+ /** 搜索行是否出现(**只认显式开启**;缺省不出 —— 不在面板里凭空多出一行) */
178
180
  searchable: {
179
181
  type: BooleanConstructor;
180
182
  default: boolean;
@@ -3,7 +3,7 @@ import { Tree } from "../tree/index.js";
3
3
  import { SelectPanel } from "./select-panel.js";
4
4
  import { SelectTrigger } from "./select-trigger.js";
5
5
  import { createSelectPanelId, createTriggerKeydown, showPanel, hidePanel } from "./select-shared.js";
6
- import { filterTreeNodes, findTreeNode, treeLabel, splitHighlight, treeChildren, treeNodeKey } from "./select-tree-filter.js";
6
+ import { filterTreeNodes, findTreeNode, treeLabel, splitHighlight, treeNodeKey, treeChildren } from "./select-tree-filter.js";
7
7
  const SelectTree = /* @__PURE__ */ defineComponent({
8
8
  name: "SelectTree",
9
9
  inheritAttrs: false,
@@ -42,7 +42,7 @@ const SelectTree = /* @__PURE__ */ defineComponent({
42
42
  type: String,
43
43
  default: "请选择"
44
44
  },
45
- /** 搜索:`true` 强制开;不给时节点总数多于 8 自动开(与 `Select` 同口径) */
45
+ /** 搜索行是否出现(**只认显式开启**;缺省不出 —— 不在面板里凭空多出一行) */
46
46
  searchable: {
47
47
  type: Boolean,
48
48
  default: false
@@ -132,18 +132,6 @@ const SelectTree = /* @__PURE__ */ defineComponent({
132
132
  const childrenOf = (node) => treeChildren(node, props.childrenKey);
133
133
  const current = computed(() => props.modelValue ?? inner.value);
134
134
  const hasValue = computed(() => current.value !== void 0 && current.value !== "");
135
- const nodeCount = computed(() => {
136
- let count = 0;
137
- const walk = (list) => {
138
- for (const node of list) {
139
- count += 1;
140
- walk(childrenOf(node));
141
- }
142
- };
143
- walk(props.nodes);
144
- return count;
145
- });
146
- const showSearch = computed(() => props.searchable || nodeCount.value > 8);
147
135
  const collectKeys = (list) => list.flatMap((node) => [keyOf(node), ...collectKeys(childrenOf(node))]);
148
136
  const innerExpanded = ref(props.defaultExpandAll ? collectKeys(props.nodes) : [...props.defaultExpandedKeys]);
149
137
  watch(() => props.defaultExpandedKeys, (next) => {
@@ -221,7 +209,7 @@ const SelectTree = /* @__PURE__ */ defineComponent({
221
209
  }, null), createVNode(SelectPanel, {
222
210
  "id": panelId,
223
211
  "elementRef": panelRef,
224
- "searchable": showSearch.value,
212
+ "searchable": props.searchable,
225
213
  "keyword": keyword.value,
226
214
  "onKeyword": (value) => {
227
215
  keyword.value = value;
@@ -28,16 +28,18 @@
28
28
  }
29
29
 
30
30
  /* 尺寸档的圆角吃**体量档**(`xs` / `sm`),不吃容器档 `md` —— 容器档一改(卡片圆角调整)
31
- 徽标会跟着变,那是两件无关的事。20 档有设计实证(表格内标签 r3),24 档无实证、按体量就近升一档。 */
31
+ 徽标会跟着变,那是两件无关的事。20 档有设计实证(表格内标签 r3),24 档无实证、按体量就近升一档。
32
+ 高度取**标记体量档**(全局令牌 `--ui-mark-height-sm/md`):与 `Tag` 的 20 / 24、`Select` 多选
33
+ 标签片的 24 同源 —— 这三处以前各写一份字面量,改一次档要改三处(2026-09-30 收敛)。 */
32
34
  .mh-badge--sm {
33
- height: 20px;
35
+ height: var(--ui-mark-height-sm);
34
36
  padding: 0 6px;
35
37
  min-width: 36px;
36
38
  border-radius: var(--ui-radius-xs);
37
39
  }
38
40
 
39
41
  .mh-badge--md {
40
- height: 24px;
42
+ height: var(--ui-mark-height-md);
41
43
  padding: 0 8px;
42
44
  border-radius: var(--ui-radius-sm);
43
45
  }
@@ -89,13 +91,20 @@
89
91
  border-color: transparent;
90
92
  }
91
93
 
94
+ /* 中性软底取 `base-200`(中性面)—— 改前是 `base-50`:那档只比内容底低 1.7 L*,
95
+ 软底淡到与背景不可分(同族 `--primary` 软底也走「弱底」档,此处与之等形)。 */
92
96
  .mh-badge--soft.mh-badge--default {
93
- --mh-badge-bg: var(--ui-base-50);
97
+ --mh-badge-bg: var(--ui-base-200);
94
98
  --mh-badge-fg: var(--ui-base-content-muted);
95
99
  }
96
100
 
101
+ /* ⚠️ 主色软底取**浅底档** `--ui-primary-weak`(同一槽位的其他消费者:`Tag` 默认底、表壳滑过底、
102
+ 聚焦环);**不取** `--ui-primary-subtle` —— 那是**选中态底**(树行 / 表格行 / 卡片选中 / 单选卡),
103
+ 两档在 default 主题里差一整档(#EFF5FF → #DBE9FF),取错会让徽标比同排的标签深一档,
104
+ 而徽标表达的是「标记」不是「选中」(2026-09-30 收敛,槽位口径见
105
+ `@manohub/theme` 的 `default/colors.css` 品牌槽位两条注释)。 */
97
106
  .mh-badge--soft.mh-badge--primary {
98
- --mh-badge-bg: var(--ui-primary-subtle);
107
+ --mh-badge-bg: var(--ui-primary-weak);
99
108
  }
100
109
 
101
110
  /* 描边:浅底 + 同色描边;default 退化为白底 + 中性描边 + 正文色(状态胶囊) */
@@ -165,10 +165,13 @@
165
165
  color: var(--ui-button-accent);
166
166
  }
167
167
 
168
- /* ===== 虚线:区域内添加内容 ===== */
168
+ /* ===== 虚线:区域内添加内容 =====
169
+ * 虚线也是「框」→ 取 `--ui-base-300`(全库唯一的「线 / 框」色,出处 `@manohub/theme` 的
170
+ * `default/colors.css` 头「三条口径」第 2 条;`base-400` 只服务勾选类小控件框体)。
171
+ * 改前取的是 `base-400`("虚线要深一档才看得见"),2026-09-30 按库级口径收敛。 */
169
172
  .mh-button--dashed {
170
173
  background: var(--ui-base-100);
171
- border: 1px dashed var(--ui-base-400);
174
+ border: 1px dashed var(--ui-base-300);
172
175
  color: var(--ui-button-accent);
173
176
  }
174
177
  .mh-button--dashed:hover,
@@ -25,6 +25,19 @@
25
25
  (特异性 0-3-0,高于本行)—— **那条兜底不能删**。 */
26
26
  display: flex;
27
27
  flex-direction: column;
28
+ /**
29
+ * **`margin: auto` 必须写**(靠 `inset: 0` + `margin: auto` 居中)。
30
+ *
31
+ * 不能依赖 UA 那条 `dialog { margin: auto }`:作者源无条件压过 UA 源(与特异性无关),
32
+ * 而**几乎每个宿主的 reset 都有 `* { margin: 0 }`**(Tailwind preflight / 本仓 ui reset 都会发下),
33
+ * 一旦命中就把 UA 的 `auto` 归零 ⇒ 弹窗**贴上贴左**(`inset: 0` + 无 margin 的必然结果)。
34
+ * 2026-09-30 实测:在 `apps/skill`(还带 Tailwind)里 `getComputedStyle` 读到 `margin: 0px`、
35
+ * `rect = (0, 0)`;显式补 `auto` 后立刻回到 `(432, 43)` 的精确居中。
36
+ *
37
+ * 与 `Drawer` 那条 `margin: 0` 是**同一件事的两面**:那边要贴侧、必须把 `auto` 归零,
38
+ * 这边要居中、必须把 `auto` 写实 —— 两边都**不能交给 UA**。`Toast` 同(它也显式归零)。
39
+ */
40
+ margin: auto;
28
41
  width: var(--ui-dialog-width);
29
42
  max-width: var(--ui-dialog-width);
30
43
  padding: 0; /* UA 给 <dialog> 的内距一律归零:内距由页头 / 内容 / 底栏各自拥有 */
@@ -109,15 +109,27 @@
109
109
  * 这里只补**作者源没覆盖、会被 `<button>` 的原生 UA 样式漏进来的四项**(实测 Chrome 的 html.css:
110
110
  * `button { align-items: flex-start; text-align: center; appearance: auto; font: 400 13.3333px Arial }`):
111
111
  *
112
- * - `align-items: center` —— 控件是 flex 行,UA 的 `flex-start` 会把值 / 标签顶到上沿;
112
+ * - `align-items: center` —— 消费方把控件开成 flex 行时(如 `Select` 的 `.mh-select`)拦住 UA 的
113
+ * `flex-start`(它会把值 / 标签顶到上沿)。本件缺省**不给 `display`**,控件是 inline-block,此时
114
+ * 垂直居中来自浏览器对 `<button>` 内容的默认居中 —— 这条声明在那个形态下用不到,留着是与复合件同一份规格;
113
115
  * - `text-align: inherit` —— UA 的 `center` 会**继承**给值文本;
114
116
  * - `font-weight` / `line-height: inherit` —— UA 的 `font` 简写会把这两项从继承链上摘下来,
115
117
  * 而作者源只写了 `font-family` 与 `font-size`(对比:`<input>` 的 UA 不设这两项,故输入框形态天然一致);
116
118
  * - `appearance: none` —— 防仍画原生按钮皮肤的引擎(Safari 一类)。
117
119
  *
120
+ * 外加一条**单行契约**(`white-space: nowrap` + `overflow: hidden` + `text-overflow: ellipsis`):
121
+ * 触发器与输入框同为**单行**控件,值再长也不换行。少了它,长值会在定高框里折成两行 ——
122
+ * 第二行溢出到框的下沿之外、右端图标也被挤走(实测:消费方把值当**裸文本**子节点渲染时必现,
123
+ * 如 `<Input as="button">{{ 部门名 }}</Input>` 放进窄表格单元格)。截断口径与
124
+ * `.mh-input__placeholder` / `.mh-select__value` 同源,这里补的是**控件本身**那一层
125
+ * —— 那两处只管各自的子节点,落在控件上的裸文本没有它们兜。
126
+ *
118
127
  * 禁写:本块不碰框 / 色 / 尺寸 / 内距 —— 那些归 `.mh-input` 与表壳,重复声明就是又开一份会漂的规格。 */
119
128
  .mh-input--trigger {
120
129
  appearance: none;
130
+ white-space: nowrap;
131
+ overflow: hidden;
132
+ text-overflow: ellipsis;
121
133
  align-items: center;
122
134
  font-weight: inherit;
123
135
  line-height: inherit;
@@ -28,9 +28,11 @@
28
28
  --ui-input-font-size: var(--ui-font-body); /* 14:控件内容走正文档 */
29
29
  --ui-input-font-size-sm: var(--ui-font-caption); /* 13 */
30
30
  --ui-input-font-size-lg: var(--ui-font-heading); /* 16 */
31
- /* 描边取 base-300(= 设计 `0:3946` 搜索框的 `#e6e6e6`),与卡片 / 表格 / 按钮 / 分页同一条线。
32
- ⚠️ 改前是 base-400,而 `filter.css` 的既有裁决早已写明「控件框边 = --ui-base-300」——
33
- 本件与它是同一条规格,之前的 base-400 属自相矛盾。base-400 从此只服务勾选类小控件框体。 */
31
+ /* 描边取 base-300 —— 这**不是本件自选**,是库级口径:`base-300` 是全库唯一的「线 / 框」色
32
+ (输入框、按钮、表格、分页项、卡片、面板、分隔线一律取它),`base-400` 只服务勾选类小控件框体。
33
+ 权威出处是 `@manohub/theme` 的 `default/colors.css` 头「三条口径」第 2 条(改值前先读那三条)。
34
+ 括注的 `#e6e6e6` 是 **default** 套该槽位的值;**farris** 套同槽位取 Ne08(`#e8ebf2`)——
35
+ 两套值不同、槽位语义相同,别按规范原文给输入框的 Ne04 来反推(那正是本件改前的 base-400)。 */
34
36
  --ui-input-line: var(--ui-base-300);
35
37
  --ui-input-line-hover: var(--ui-primary-hover); /* 滑过 / 聚焦转主色(规范 Th08) */
36
38
  --ui-input-textarea-min-height: 56px; /* 多行最小高(两行正文 + 上下内距) */
@@ -253,7 +253,13 @@
253
253
  --ui-pagination-font-size: var(--ui-font-body);
254
254
  }
255
255
 
256
- /* ===== 跳页(showQuickJumper) ===== */
256
+ /* ===== 跳页(showQuickJumper) =====
257
+ *
258
+ * 跳页框是**输入框**,画线取 `--ui-base-300` —— 与 `Input` 同一条口径(`base-300` 是全库唯一的
259
+ * 「线 / 框」色,出处 `@manohub/theme` 的 `default/colors.css` 头「三条口径」第 2 条;
260
+ * `base-400` 只服务勾选类小控件框体)。**改前取的是 `base-400`**:同一个「输入框」在库里因此
261
+ * 有过两种线色,属跨件口径漏改(2026-09-30 收敛)。
262
+ * 禁用档与 `Input` 同构:底转 `base-200`、线仍 `base-300` —— 即**靠底色区分禁用**,不再靠线深。 */
257
263
  .mh-pagination__jump {
258
264
  display: inline-flex;
259
265
  align-items: center;
@@ -265,7 +271,7 @@
265
271
  width: 56px;
266
272
  height: var(--ui-pagination-item-size);
267
273
  padding: 0 var(--ui-space-2);
268
- border: 1px solid var(--ui-base-400);
274
+ border: 1px solid var(--ui-base-300);
269
275
  border-radius: var(--ui-pagination-item-radius);
270
276
  background: var(--ui-base-100);
271
277
  font-family: inherit;
@@ -23,6 +23,9 @@
23
23
  * 表壳挂 `.mh-popup-anchor`(`anchor-name`)、面板挂 `.mh-popup--anchored`(`position-anchor`),
24
24
  * 锚点即整件;非锚定场景回落到 240px 兜底值。
25
25
  *
26
+ * **面板有高度上限**:`--ui-select-panel-max-height`(默认 320px)钉在面板根上,超出即内容区
27
+ * (`.mh-select__body`)滚动、搜索行常驻 —— 列表与树两形态共用这一份,形态里不许各写限高。
28
+ *
26
29
  * **一处有色差异**:规范的选中行是灰底(Ne10),本仓的「选中」统一走**主色浅底**
27
30
  * `--ui-primary-subtle`(与列表 / 树的选中态同一语义槽位),否则悬停(Ne11)与选中(Ne10)两档灰
28
31
  * 在本仓有限的灰阶里挤成同色、看不出选中。
@@ -106,10 +109,25 @@
106
109
  background: var(--ui-base-200);
107
110
  color: var(--ui-base-content-muted);
108
111
  }
109
- /* ===== 面板皮肤(与操作层并列组合:`class="mh-popup mh-select__panel mh-popup--anchored"`) ===== */
112
+ /* ===== 面板皮肤(与操作层并列组合:`class="mh-popup mh-select__panel mh-popup--anchored"`) =====
113
+ **面板是列容器 + 限高**:搜索行常驻(不滚),下一条的 `.mh-select__body` 承担滚动 ——
114
+ 列表与树两形态因此共用同一份「长内容不撑破面板」的规格。
115
+ (历史症状:限高原先只写在树体 `.mh-select__tree` 上,而列表形态的选项是面板的**直接子节点**,
116
+ 面板会被一路撑高;实测 60 条选项的面板高 2212px、视口只有 925px。) */
110
117
  .mh-select__panel {
111
118
  /* 锚 = `.mh-popup-anchor`(挂在表壳上)→ 面板与**整件**同宽;非锚定场景回落 240 */
112
119
  width: anchor-size(width, 240px);
120
+ /* 列容器:搜索行 `flex: none` + 内容区 `flex: 1 / min-height: 0`。
121
+ ⚠️ 加 `display: flex` 会以作者源压过 UA 给**收起态**定的 `display: none`,收起态靠操作层
122
+ `operations/popup.css` 里那条 `:is()` 兜底(把收起的浮层重新钉成 `display: none`,
123
+ 特异性 0-3-0、高于本行)—— **那条兜底不能删**(与 `dialog.css` 同一处坑)。 */
124
+ display: flex;
125
+ flex-direction: column;
126
+ /* 限高(**含搜索行**):超出即内容区滚动。面板仍贴锚点,只是不再无限长 */
127
+ max-height: var(--ui-select-panel-max-height);
128
+ /* UA 给 `[popover]` 的是 `overflow: auto`,必须归零:内容区已是滚动容器,
129
+ 留一层就会与它叠成两条滚动条(同一个面板上实测会同时出现)。 */
130
+ overflow: hidden;
113
131
  padding: var(--ui-space-1) 0;
114
132
  border-radius: var(--ui-select-radius);
115
133
  background: var(--ui-base-100);
@@ -119,6 +137,15 @@
119
137
  box-shadow: var(--ui-shadow-popover);
120
138
  }
121
139
 
140
+ /* 内容区:**面板里唯一的滚动容器**(列表体的选项行 / 树体的树都装在它里面)。
141
+ `min-height: 0` 是这一档生效的前提:列容器里弹性项的可收缩下限默认取内容高,
142
+ 不给它,内容就永远压不下去、上面的限高形同虚设(与 `panel.css` 的 `.mh-panel__body` 同款)。 */
143
+ .mh-select__body {
144
+ flex: 1;
145
+ min-height: 0;
146
+ overflow-y: auto;
147
+ }
148
+
122
149
  /* 选项行:滑过 / 选中 / 禁用 / 提示 / 空态 */
123
150
  .mh-select__option {
124
151
  position: relative;
@@ -213,21 +240,24 @@
213
240
  cursor: default;
214
241
  }
215
242
 
216
- /* 搜索行(选项多于 8 条时由组件渲染) */
243
+ /* 搜索行(**消费方显式 `searchable` 时才由组件渲染**,件不按选项条数自动决断)。
244
+ `flex: none`:它是面板的**常驻**行 —— 面板限高时收缩与滚动都归内容区,
245
+ 少了它,搜索行会被一起压扁(列容器里它默认是可收缩的)。 */
217
246
  .mh-select__search {
247
+ flex: none;
218
248
  padding: var(--ui-space-2) var(--ui-space-2) var(--ui-space-1);
219
249
  }
220
250
 
221
251
  /* ===== 树体(树选择 `SelectTree` 的面板内容)=====
222
252
  边界要认清楚:**面板皮肤归本文件**(上一节),**树的行态归 tree 面**(`.mh-tree__*` 那一族,
223
253
  本文件一行都不碰 —— 跨族类名须先登记跨面豁免,这里不走那条路)。
224
- 故本件只做「把树安放进面板」这一件事:内距 + 限高 + 溢出滚动。
225
- 行高 / 缩进 / 滑过与选中底色全部沿用 tree 面,两处形态不会漂移。 */
254
+ 故本件只做「把树安放进面板」这一件事:内距。
255
+ 行高 / 缩进 / 滑过与选中底色全部沿用 tree 面,两处形态不会漂移;
256
+ **限高与滚动也不在这里** —— 那是面板(`.mh-select__panel`)与面板内容区(`.mh-select__body`)的事,
257
+ 列表与树两形态共用同一份(形态里各写一条就会漂移,本件此前正是自己写了一条)。 */
226
258
  .mh-select__tree {
227
259
  /* 行底满宽、只有内容缩进(缩进公式在 tree 面),故左右各留一个最小呼吸位 */
228
260
  padding: 0 var(--ui-space-1) var(--ui-space-1);
229
- max-height: var(--ui-select-tree-max-height);
230
- overflow-y: auto;
231
261
  }
232
262
 
233
263
  /* 命中高亮(搜索命中的那段文字,由组件的 `renderLabel` 落进 `.mh-tree__label` 槽)。
@@ -28,9 +28,10 @@
28
28
  --ui-select-option-height: 36px; /* 设计 `0:4024`:选项行 144×36(原为 34) */
29
29
  --ui-select-option-hover: var(--ui-base-200); /* 设计滑过底 #f2f2f2 */
30
30
  --ui-select-option-selected: var(--ui-primary-subtle); /* 见头部「有色差异」 */
31
- --ui-select-chip-height: 24px; /* 多选标签(规范复用标签族 24 档) */
31
+ --ui-select-chip-height: var(--ui-mark-height-md); /* 多选标签(规范复用标签族 24 档)⇒ 标记体量档,与徽标 / 标签同源 */
32
32
  --ui-select-icon-size: var(--ui-font-icon); /* 下拉箭头:常规图标档 */
33
- /* 树形态(SelectTree):面板内树体的限高 —— 约等于 9 行(行高 34 + 行距 6 的节距 40 见 tree.css),
34
- 超过即滚动,长树不把面板撑成整屏 */
35
- --ui-select-tree-max-height: 320px;
33
+ /* 面板限高(**列表与树两形态共用**):面板整体(含搜索行)不超此高,超出即内容区滚动。
34
+ 320px ≈ 8 行选项(选项行 36 + 面板上下内距 8);树形态可见行数按 tree 面的节距 40 折算
35
+ (行高 34 + 行距 6),有无搜索行会差一行上下。要改高度改这枚令牌,不要在业务侧压 max-height。 */
36
+ --ui-select-panel-max-height: 320px;
36
37
  }
@@ -27,9 +27,11 @@
27
27
  --ui-steps-connector-length: 58.383px; /* 规范实测最小段长(其余由 flex 撑开) */
28
28
  --ui-steps-connector-width: 2px;
29
29
  --ui-steps-connector-arrow: 6px;
30
- /* 连接线沿用 base-400:设计稿里**只有三个向导页**画了连接线(`#cdd2de`),而向导页已判定为旧稿;
31
- 四个品牌页没有步骤条样张 → 按「未覆盖槽位沿用现值并注明」处理,不做推测性改动。 */
32
- --ui-steps-connector-line: var(--ui-base-400);
30
+ /* 连接线取 base-300(全库唯一的「线 / 框」色,见 `@manohub/theme` 的 `default/colors.css`
31
+ 头「三条口径」第 2 条)—— 改前取的是 base-400。设计稿里**只有三个向导页**画了连接线
32
+ (`#cdd2de`)而向导页已判定为旧稿,四个品牌页没有步骤条样张 → 按库级口径取线色,
33
+ 不做推测性改动(这枚令牌的档位语义与连接线宽度 / 燕尾尺寸一样,只在「线」这一族内变)。 */
34
+ --ui-steps-connector-line: var(--ui-base-300);
33
35
  --ui-steps-connector-line-active: var(--ui-primary); /* 规范 #4999fe */
34
36
  --ui-steps-title-gap: var(--ui-space-3); /* 节点与标题间距 12 */
35
37
  --ui-steps-seg-width: 140px;
@@ -132,8 +132,10 @@
132
132
  appearance: none;
133
133
  pointer-events: none;
134
134
  }
135
+ /* 悬停底取 `base-200`(中性面)—— 改前是 `base-50`:那档只比内容底低 1.7 L*,
136
+ 在多数屏幕上看不出悬停反馈(`base-50` 不再作为「更轻底」的默认去处,见 colors 口径第 1 条)。 */
135
137
  .mh-tag--select:hover {
136
- background: var(--ui-base-50);
138
+ background: var(--ui-base-200);
137
139
  border: 1px solid var(--ui-base-300);
138
140
  }
139
141
  .mh-tag--select:has(input:focus-visible) {
@@ -160,11 +162,13 @@
160
162
  cursor: not-allowed;
161
163
  }
162
164
 
163
- /* ===== 新增档(虚线框 + 加号由 @manohub/icon 的 plus 渲染)===== */
165
+ /* ===== 新增档(虚线框 + 加号由 @manohub/icon 的 plus 渲染)=====
166
+ * 虚线框取 `--ui-base-300`(全库唯一的「线 / 框」色,见 `@manohub/theme` 的 `default/colors.css`
167
+ * 头「三条口径」第 2 条);改前取 `base-400`,2026-09-30 按口径收敛。 */
164
168
  .mh-tag--add {
165
169
  height: var(--ui-tag-height);
166
170
  padding: 0 var(--ui-space-2);
167
- border: 1px dashed var(--ui-base-400);
171
+ border: 1px dashed var(--ui-base-300);
168
172
  border-radius: var(--ui-radius-full);
169
173
  background: transparent;
170
174
  color: var(--ui-base-content-muted);
@@ -19,8 +19,11 @@
19
19
  `--ui-primary-weak`(#EFF5FF) 差 ≤ 6/255 —— 三个近值并存的典型症状,本轮并入主题档。
20
20
  设计实证:表格内标签 `0:2723` 底 `#eaefff`。 */
21
21
  --ui-tag-bg: var(--ui-primary-weak);
22
- --ui-tag-height: 24px;
23
- --ui-tag-height-sm: 20px;
22
+ /* 20 / 24 两档是**跨件共用**的「标记体量档」(徽标 / 标签 / 多选标签片同源)⇒ 全局值在
23
+ `@manohub/theme` 的 `spacing.css`,本件只做别名:改档只改主题那一处。
24
+ 28 / 22 两档只服务本件(大号标签 / 状态标签),故字面量留在本文件。 */
25
+ --ui-tag-height: var(--ui-mark-height-md);
26
+ --ui-tag-height-sm: var(--ui-mark-height-sm);
24
27
  --ui-tag-height-lg: 28px;
25
28
  --ui-tag-height-status: 22px;
26
29
  --ui-tag-font-size: var(--ui-font-caption);
@@ -18,7 +18,7 @@
18
18
  --ui-upload-padding: 32px; /* 规范间距档 8(本仓间距刻度最大 24,故按实测值给) */
19
19
  --ui-upload-zone-max-width: 500px; /* 规范拖拽区帧宽 */
20
20
  --ui-upload-radius: var(--ui-radius-control);
21
- --ui-upload-line: var(--ui-base-400); /* 虚线边 Ne05 邻档 */
21
+ --ui-upload-line: var(--ui-base-300); /* 虚线边:全库唯一的「线 / 框」色(`base-400` 只服务勾选类小控件) */
22
22
  --ui-upload-text: var(--ui-base-content-muted); /* 主提示 Te05 */
23
23
  --ui-upload-hint: var(--ui-base-content-subtle); /* 副提示 Te07 */
24
24
  --ui-upload-meta-size: var(--ui-font-caption); /* 大小/缩略图文字:规范 12,本仓字阶最小 13 */
@@ -12,7 +12,7 @@
12
12
  *
13
13
  * | 段 | 命名 | 落点 | 锚 |
14
14
  * |---|---|---|---|
15
- * | 全局令牌(跨件共用的值) | `--ui-{primary,base-*,space-*,radius-*,shadow-*,font-*}` | `@manohub/theme` | `[data-manohub-ui]` |
15
+ * | 全局令牌(跨件共用的值) | `--ui-{primary,base-*,space-*,radius-*,shadow-*,font-*}`、尺寸档(`--ui-control-height-*` / `--ui-mark-height-*`) | `@manohub/theme` | `[data-manohub-ui]` |
16
16
  * | 组件令牌**基础值** | `--ui-<件>-*` | 本目录同名 `<件>.tokens.css` | `[data-manohub-ui]` |
17
17
  * | 组件令牌**档位改写** | `--ui-<件>-*` | 面文件 `<件>.css` 的**修饰符块** | `.mh-<件>--<档>` 等 |
18
18
  * | 内部状态变量 | `--mh-<件>-*` | 面文件(修饰符机制本身) | 组件根类 / 修饰符类 |
@@ -32,7 +32,11 @@
32
32
  * 四、零 JS 锚定 —— CSS Anchor Positioning:`position-area` 定位档(`data-side` / `data-align`)
33
33
  * + `position-try-fallbacks` 自动翻面;锚点走 `popovertarget` 的**隐式锚**,一个 `anchor-name`
34
34
  * 都不用写(隐式锚够不着时才用 `.mh-popup-anchor` / `.mh-popup--anchored` 那一档)。
35
- * 无 `data-side` 时不落定位档 —— 模态与无锚点浮层退回 UA 的视口居中。
35
+ * 无 `data-side` 时不落定位档 —— 模态与无锚点浮层退回**件自己声明的**视口居中
36
+ * (`inset: 0` + `margin: auto`)。⚠️ 这里**不能指望 UA**:UA 确有 `dialog { margin: auto }`,
37
+ * 但作者源无条件压过 UA 源,而宿主的 `* { margin: 0 }` reset 会把它归零 ⇒ 弹窗贴上贴左。
38
+ * 故居中必须由**件**写实(见 `components/dialog.css` 的 `.mh-dialog { margin: auto }`,
39
+ * 2026-09-30 在带 Tailwind 的宿主里实测踩到)。
36
40
  *
37
41
  * 五、展开态派生 —— 箭头转向:原生路径由宿主 `:has` 反推,组件路径由触发器自带的
38
42
  * `aria-expanded` 反推。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@manohub/ui",
3
- "version": "0.10.0",
3
+ "version": "0.10.2",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "AIHub 自绘组件层:不依赖 farris 的手写 Vue TSX 组件 + 自带令牌层(Farris 规范视觉 · daisyUI 式语义令牌 · shadcn 式 API 词汇)。",
@@ -46,7 +46,7 @@
46
46
  "vue": "^3.5.0"
47
47
  },
48
48
  "dependencies": {
49
- "@manohub/icon": "^0.10.0"
49
+ "@manohub/icon": "^0.10.2"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@types/node": "^24.0.0",