@manohub/ui 0.9.0 → 0.10.1

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.
Files changed (45) hide show
  1. package/README.md +13 -7
  2. package/dist/components/checkbox/index.js +5 -1
  3. package/dist/components/code-block/index.js +18 -11
  4. package/dist/components/filter/index.d.ts +8 -2
  5. package/dist/components/filter/index.js +11 -20
  6. package/dist/components/form/index.d.ts +39 -0
  7. package/dist/components/form/index.js +6 -0
  8. package/dist/components/input/index.d.ts +76 -20
  9. package/dist/components/input/index.js +64 -15
  10. package/dist/components/radio/index.js +29 -3
  11. package/dist/components/search/index.js +37 -35
  12. package/dist/components/select/index.d.ts +5 -2
  13. package/dist/components/select/index.js +3 -6
  14. package/dist/components/select/select-list.d.ts +2 -0
  15. package/dist/components/select/select-panel.d.ts +9 -4
  16. package/dist/components/select/select-panel.js +4 -2
  17. package/dist/components/select/select-tree.d.ts +5 -3
  18. package/dist/components/select/select-tree.js +3 -15
  19. package/dist/components/select/select-trigger.d.ts +4 -2
  20. package/dist/components/select/select-trigger.js +9 -4
  21. package/dist/components/table/index.d.ts +19 -8
  22. package/dist/components/table/index.js +3 -3
  23. package/dist/components/text/index.d.ts +4 -3
  24. package/dist/components/toast/index.js +16 -1
  25. package/dist/styles/components/avatar.css +4 -1
  26. package/dist/styles/components/badge.css +3 -1
  27. package/dist/styles/components/button.css +7 -2
  28. package/dist/styles/components/filter.css +22 -34
  29. package/dist/styles/components/input.css +37 -1
  30. package/dist/styles/components/input.tokens.css +5 -3
  31. package/dist/styles/components/pagination.css +8 -2
  32. package/dist/styles/components/select.css +39 -10
  33. package/dist/styles/components/select.tokens.css +4 -3
  34. package/dist/styles/components/steps.tokens.css +5 -3
  35. package/dist/styles/components/table.css +33 -22
  36. package/dist/styles/components/table.tokens.css +6 -6
  37. package/dist/styles/components/tag.css +7 -3
  38. package/dist/styles/components/text.css +10 -0
  39. package/dist/styles/components/toast.css +35 -6
  40. package/dist/styles/components/upload.css +1 -0
  41. package/dist/styles/components/upload.tokens.css +1 -1
  42. package/dist/styles/index.css +5 -4
  43. package/dist/styles/reset.css +52 -167
  44. package/package.json +2 -2
  45. package/dist/styles/components/filter.tokens.css +0 -14
@@ -14,6 +14,13 @@ const Input = /* @__PURE__ */ defineComponent({
14
14
  type: String,
15
15
  default: ""
16
16
  },
17
+ /**
18
+ * 占位文案。两种形态都消费它:
19
+ * - 缺省(`as="input"`)走原生 `::placeholder` 伪元素;
20
+ * - 触发器形态(`as="button"`)**没有 `::placeholder` 可用**,改由一个 span 承载
21
+ * (`.mh-input__placeholder`)—— 当且仅当 `hasValue === false` 且本 prop 有值时渲染它,
22
+ * 并**取代** `default` 插槽(见上面触发器一节)。没传 `hasValue` 的调用方行为不变。
23
+ */
17
24
  placeholder: {
18
25
  type: String,
19
26
  default: void 0
@@ -71,7 +78,7 @@ const Input = /* @__PURE__ */ defineComponent({
71
78
  type: Number,
72
79
  default: void 0
73
80
  },
74
- /** 右侧字数统计(单行须给 `maxlength` 才有意义) */
81
+ /** 右侧字数统计(**须给 `maxlength` 才渲染**:没有分母的 `n/0` 是坏值) */
75
82
  showCount: {
76
83
  type: Boolean,
77
84
  default: false
@@ -95,7 +102,17 @@ const Input = /* @__PURE__ */ defineComponent({
95
102
  * 整件的**表壳、附加位与状态规则一并复用**,只把「控件元素」从 `<input>` 换成 `<button>`:
96
103
  * 于是 `Select` 这类件能拿到输入框的框与内距、内建清除位、右侧图标位与三态反馈,同时保留
97
104
  * 原生 `<button>` 才有的语义(如 `popovertarget` 触发 popover —— 该属性只认 button)。
98
- * 缺省 `as="input"` 的渲染路径除本组 prop 外零变化。 */
105
+ * 缺省 `as="input"` 的渲染路径除本组 prop 外零变化。
106
+ *
107
+ * **单行契约(与输入框同源,面层承担)**:控件是单行盒,值再长也不换行(超出走省略号)
108
+ * —— 见 `styles/components/input.css` 的 `.mh-input--trigger`。故 `default` 插槽里给**裸文本**
109
+ * 也安全,不需要自己再包一层截断 span。
110
+ *
111
+ * **无值时的占位**:`<button>` 没有 `::placeholder` 伪元素可用,故 `placeholder` 在这个形态下
112
+ * 由本件渲染成一个 span(`.mh-input__placeholder`,色档与原生占位一致);判据是
113
+ * `hasValue === false && placeholder` **同时**成立 —— 两者缺一即沿用消费方给的 `default` 插槽。
114
+ * 这样「有值渲染值、无值渲染占位」这件例行事的文案与切换都由输入面统一,复合件不必各画一遍
115
+ * (`Select` 早期为自己的值区单画过一份,见 `select-trigger.tsx`)。 */
99
116
  /** 控件元素形态:`input` 文本输入(缺省)/ `button` 触发器 */
100
117
  as: {
101
118
  type: String,
@@ -106,12 +123,14 @@ const Input = /* @__PURE__ */ defineComponent({
106
123
  type: String,
107
124
  default: void 0
108
125
  },
109
- /** 展开到**控件元素**上的属性(触发器形态要 `popovertarget` / `aria-*` / `onKeydown` 落到控件上) */
126
+ /** 展开到**控件元素**上的属性(触发器形态要 `popovertarget` / `aria-*` / `onKeydown` 落到控件上);
127
+ * 与透传的 attrs 撞名时以本 prop 为准 */
110
128
  controlAttrs: {
111
129
  type: Object,
112
130
  default: void 0
113
131
  },
114
- /** 显式覆盖清除位显隐;不给时仍按「有内容」判定(多选类复合件没有单值可判) */
132
+ /** 显式覆盖清除位显隐;不给时仍按「有内容」判定(多选类复合件没有单值可判)。
133
+ * 同时是触发器形态「显示值还是显示占位」的判据(仅当它为 `false` 且传了 `placeholder` 时接管) */
115
134
  hasValue: {
116
135
  type: Boolean,
117
136
  default: void 0
@@ -129,6 +148,13 @@ const Input = /* @__PURE__ */ defineComponent({
129
148
  const inner = ref(props.defaultValue);
130
149
  const current = computed(() => props.modelValue ?? inner.value);
131
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
+ });
132
158
  const visualStatus = computed(() => props.status ?? (props.invalid ? "error" : void 0));
133
159
  const isInvalid = computed(() => visualStatus.value === "error");
134
160
  const commit = (value) => {
@@ -149,6 +175,8 @@ const Input = /* @__PURE__ */ defineComponent({
149
175
  const isTrigger = computed(() => props.as === "button");
150
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(" "));
151
177
  const showClear = computed(() => props.clearable && (props.hasValue ?? !!current.value));
178
+ const showCounter = computed(() => !isTrigger.value && props.showCount && props.maxlength !== void 0);
179
+ const showPlaceholder = computed(() => isTrigger.value && props.hasValue === false && !!props.placeholder);
152
180
  return () => createVNode("div", {
153
181
  "class": ["mh-input__wrap", attrs.class].filter(Boolean).join(" "),
154
182
  "style": attrs.style
@@ -160,12 +188,17 @@ const Input = /* @__PURE__ */ defineComponent({
160
188
  本件的 Enter / Delete 是**输入框**语义,触发器有自己的一套(如 `↓` 开面板),不在此套用。 */
161
189
  createVNode("button", mergeProps({
162
190
  "type": "button"
163
- }, props.controlAttrs, {
191
+ }, forwardAttrs.value, props.controlAttrs, {
164
192
  "class": controlClasses.value,
165
193
  "disabled": props.disabled,
166
194
  "aria-invalid": isInvalid.value ? "true" : void 0
167
- }), [slots.default ? slots.default() : null])
168
- ) : createVNode("input", {
195
+ }), [showPlaceholder.value ? (
196
+ /* 触发器没有 `::placeholder`,占位文案由本件渲染(色见 `.mh-input__placeholder`) */
197
+ createVNode("span", {
198
+ "class": "mh-input__placeholder"
199
+ }, [props.placeholder])
200
+ ) : slots.default ? slots.default() : null])
201
+ ) : createVNode("input", mergeProps(forwardAttrs.value, {
169
202
  "class": controlClasses.value,
170
203
  "type": inputType.value,
171
204
  "value": current.value,
@@ -177,7 +210,7 @@ const Input = /* @__PURE__ */ defineComponent({
177
210
  "aria-invalid": isInvalid.value ? "true" : void 0,
178
211
  "onInput": onInput,
179
212
  "onKeydown": onKeydown
180
- }, null), showClear.value ? createVNode("button", {
213
+ }), null), showClear.value ? createVNode("button", {
181
214
  "type": "button",
182
215
  "class": "mh-input__clear",
183
216
  "aria-label": "清除",
@@ -199,9 +232,9 @@ const Input = /* @__PURE__ */ defineComponent({
199
232
  "class": "mh-input__eye-icon"
200
233
  }, null)]) : null, slots.suffix ? createVNode("span", {
201
234
  "class": "mh-input__suffix"
202
- }, [slots.suffix()]) : null, !isTrigger.value && props.showCount ? createVNode("span", {
235
+ }, [slots.suffix()]) : null, showCounter.value ? createVNode("span", {
203
236
  "class": "mh-input__counter"
204
- }, [current.value?.length ?? 0, createTextVNode("/"), props.maxlength ?? 0]) : null]);
237
+ }, [current.value?.length ?? 0, createTextVNode("/"), props.maxlength]) : null]);
205
238
  }
206
239
  });
207
240
  const Textarea = /* @__PURE__ */ defineComponent({
@@ -257,6 +290,7 @@ const Textarea = /* @__PURE__ */ defineComponent({
257
290
  type: Number,
258
291
  default: void 0
259
292
  },
293
+ /** 右下角字数统计(**须给 `maxlength` 才渲染**:没有分母的 `n/0` 是坏值) */
260
294
  showCount: {
261
295
  type: Boolean,
262
296
  default: false
@@ -279,6 +313,14 @@ const Textarea = /* @__PURE__ */ defineComponent({
279
313
  }) {
280
314
  const inner = ref(props.defaultValue);
281
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);
282
324
  const visualStatus = computed(() => props.status ?? (props.invalid ? "error" : void 0));
283
325
  const isInvalid = computed(() => visualStatus.value === "error");
284
326
  const autoRows = computed(() => {
@@ -300,7 +342,7 @@ const Textarea = /* @__PURE__ */ defineComponent({
300
342
  return () => createVNode("div", {
301
343
  "class": ["mh-input__wrap", attrs.class].filter(Boolean).join(" "),
302
344
  "style": attrs.style
303
- }, [createVNode("textarea", {
345
+ }, [createVNode("textarea", mergeProps(forwardAttrs.value, {
304
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(" "),
305
347
  "rows": autoRows.value,
306
348
  "value": current.value,
@@ -311,9 +353,9 @@ const Textarea = /* @__PURE__ */ defineComponent({
311
353
  "autofocus": props.autofocus,
312
354
  "aria-invalid": isInvalid.value ? "true" : void 0,
313
355
  "onInput": onInput
314
- }, null), props.showCount ? createVNode("span", {
356
+ }), null), showCounter.value ? createVNode("span", {
315
357
  "class": "mh-input__counter"
316
- }, [current.value?.length ?? 0, createTextVNode("/"), props.maxlength ?? 0]) : null]);
358
+ }, [current.value?.length ?? 0, createTextVNode("/"), props.maxlength]) : null]);
317
359
  }
318
360
  });
319
361
  const InputDisplay = /* @__PURE__ */ defineComponent({
@@ -330,10 +372,17 @@ const InputDisplay = /* @__PURE__ */ defineComponent({
330
372
  slots,
331
373
  attrs
332
374
  }) {
333
- 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, {
334
383
  "class": ["mh-input--display", attrs.class].filter(Boolean).join(" "),
335
384
  "style": attrs.style
336
- }, [slots.default ? slots.default() : props.value]);
385
+ }), [slots.default ? slots.default() : props.value]);
337
386
  }
338
387
  });
339
388
  Object.assign(Input, {
@@ -1,4 +1,4 @@
1
- import { defineComponent, createVNode, ref, computed } from "vue";
1
+ import { defineComponent, createVNode, ref, nextTick, computed } from "vue";
2
2
  const Radio = /* @__PURE__ */ defineComponent({
3
3
  name: "Radio",
4
4
  inheritAttrs: false,
@@ -136,7 +136,17 @@ const RadioGroup = /* @__PURE__ */ defineComponent({
136
136
  props.onChange?.(value);
137
137
  }
138
138
  );
139
+ const rootRef = ref(null);
140
+ const syncNative = () => {
141
+ const root = rootRef.value;
142
+ if (!root) return;
143
+ root.querySelectorAll(".mh-radio__input").forEach((el) => {
144
+ const want = String(current.value) === el.value;
145
+ if (el.checked !== want) el.checked = want;
146
+ });
147
+ };
139
148
  return () => createVNode("div", {
149
+ "ref": rootRef,
140
150
  "class": ["mh-radio-group", props.vertical ? "mh-radio-group--vertical" : void 0, attrs.class].filter(Boolean).join(" "),
141
151
  "style": attrs.style
142
152
  }, [slots.default ? slots.default({
@@ -150,7 +160,10 @@ const RadioGroup = /* @__PURE__ */ defineComponent({
150
160
  "checked": option.value === current.value,
151
161
  "disabled": props.disabled || option.disabled,
152
162
  "size": props.size,
153
- "onChange": select
163
+ "onChange": (value) => {
164
+ select(value);
165
+ void nextTick(syncNative);
166
+ }
154
167
  }, null))]);
155
168
  }
156
169
  });
@@ -206,7 +219,17 @@ const RadioCard = /* @__PURE__ */ defineComponent({
206
219
  props.onChange?.(value);
207
220
  }
208
221
  );
222
+ const rootRef = ref(null);
223
+ const syncNative = () => {
224
+ const root = rootRef.value;
225
+ if (!root) return;
226
+ root.querySelectorAll(".mh-radio-card__input").forEach((el) => {
227
+ const want = current.value === el.value;
228
+ if (el.checked !== want) el.checked = want;
229
+ });
230
+ };
209
231
  return () => createVNode("div", {
232
+ "ref": rootRef,
210
233
  "class": ["mh-radio-card", props.columns > 1 ? "mh-radio-card--grid" : "", attrs.class].filter(Boolean).join(" "),
211
234
  "style": props.columns > 1 ? {
212
235
  ...attrs.style,
@@ -228,7 +251,10 @@ const RadioCard = /* @__PURE__ */ defineComponent({
228
251
  "value": String(item.value),
229
252
  "checked": isChecked,
230
253
  "disabled": isDisabled,
231
- "onChange": () => select(item.value)
254
+ "onChange": () => {
255
+ select(item.value);
256
+ void nextTick(syncNative);
257
+ }
232
258
  }, null), createVNode("span", {
233
259
  "class": "mh-radio-card__radio",
234
260
  "aria-hidden": "true"
@@ -76,44 +76,46 @@ const Search = /* @__PURE__ */ defineComponent({
76
76
  const iconAction = computed(() => props.action === "icon");
77
77
  const showAction = computed(() => iconAction.value || props.searchText !== void 0);
78
78
  const actionLabel = computed(() => props.searchText ?? "搜索");
79
- return () => createVNode("div", {
80
- "class": ["mh-search", attrs.class].filter(Boolean).join(" "),
81
- "style": attrs.style
82
- }, [createVNode(Input, {
83
- "class": "mh-search__input",
84
- "modelValue": current.value,
85
- "size": props.size,
86
- "disabled": props.disabled,
87
- "placeholder": props.placeholder,
88
- "clearable": props.clearable,
89
- "onChange": commit,
90
- "onEnter": search
91
- }, {
92
- default: () => [iconAction.value ? {} : {
79
+ return () => {
80
+ const field = {
81
+ class: "mh-search__input",
82
+ modelValue: current.value,
83
+ size: props.size,
84
+ disabled: props.disabled,
85
+ placeholder: props.placeholder,
86
+ clearable: props.clearable,
87
+ onChange: commit,
88
+ onEnter: search
89
+ };
90
+ const fieldNode = iconAction.value ? createVNode(Input, field, null) : createVNode(Input, field, {
93
91
  prefix: () => createVNode(Icon, {
94
92
  "name": "search"
95
93
  }, null)
96
- }]
97
- }), showAction.value ? iconAction.value ? createVNode(Button, {
98
- "class": "mh-search__btn",
99
- "variant": "primary",
100
- "size": props.size,
101
- "icon": "search",
102
- "title": actionLabel.value,
103
- "disabled": props.disabled,
104
- "loading": props.loading,
105
- "onClick": search
106
- }, null) : createVNode(Button, {
107
- "class": "mh-search__btn",
108
- "variant": "primary",
109
- "size": props.size,
110
- "icon": "search",
111
- "disabled": props.disabled,
112
- "loading": props.loading,
113
- "onClick": search
114
- }, {
115
- default: () => props.searchText
116
- }) : null]);
94
+ });
95
+ return createVNode("div", {
96
+ "class": ["mh-search", attrs.class].filter(Boolean).join(" "),
97
+ "style": attrs.style
98
+ }, [fieldNode, showAction.value ? iconAction.value ? createVNode(Button, {
99
+ "class": "mh-search__btn",
100
+ "variant": "primary",
101
+ "size": props.size,
102
+ "icon": "search",
103
+ "title": actionLabel.value,
104
+ "disabled": props.disabled,
105
+ "loading": props.loading,
106
+ "onClick": search
107
+ }, null) : createVNode(Button, {
108
+ "class": "mh-search__btn",
109
+ "variant": "primary",
110
+ "size": props.size,
111
+ "icon": "search",
112
+ "disabled": props.disabled,
113
+ "loading": props.loading,
114
+ "onClick": search
115
+ }, {
116
+ default: () => props.searchText
117
+ }) : null]);
118
+ };
117
119
  }
118
120
  });
119
121
  export {
@@ -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;
@@ -211,13 +210,11 @@ const Select = /* @__PURE__ */ defineComponent({
211
210
  "class": "mh-select__chip"
212
211
  }, [option.label])), foldedCount.value > 0 ? createVNode("span", {
213
212
  "class": "mh-select__chip mh-select__chip--fold"
214
- }, [foldText.value]) : null, !hasValue ? createVNode("span", {
215
- "class": "mh-select__value mh-select__value--placeholder"
216
- }, [props.placeholder]) : null]) : void 0
213
+ }, [foldText.value]) : null]) : void 0
217
214
  }, null), createVNode(SelectPanel, {
218
215
  "id": panelId,
219
216
  "elementRef": panelRef,
220
- "searchable": showSearch.value,
217
+ "searchable": props.searchable,
221
218
  "keyword": keyword.value,
222
219
  "onKeyword": (value) => {
223
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;
@@ -35,11 +35,12 @@ export declare const SelectTrigger: import("vue").DefineComponent<import("vue").
35
35
  type: BooleanConstructor;
36
36
  default: boolean;
37
37
  };
38
+ /** 无值时的占位文案(**透传给输入面**:`hasValue === false` 时由它渲染并取代 `default` 插槽) */
38
39
  placeholder: {
39
40
  type: StringConstructor;
40
41
  default: string;
41
42
  };
42
- /** 单选显示文本(无值时不渲染它,改渲染 `placeholder`) */
43
+ /** 单选显示文本(只在**有值**时渲染;无值态由输入面的占位承担) */
43
44
  label: {
44
45
  type: StringConstructor;
45
46
  default: undefined;
@@ -93,11 +94,12 @@ export declare const SelectTrigger: import("vue").DefineComponent<import("vue").
93
94
  type: BooleanConstructor;
94
95
  default: boolean;
95
96
  };
97
+ /** 无值时的占位文案(**透传给输入面**:`hasValue === false` 时由它渲染并取代 `default` 插槽) */
96
98
  placeholder: {
97
99
  type: StringConstructor;
98
100
  default: string;
99
101
  };
100
- /** 单选显示文本(无值时不渲染它,改渲染 `placeholder`) */
102
+ /** 单选显示文本(只在**有值**时渲染;无值态由输入面的占位承担) */
101
103
  label: {
102
104
  type: StringConstructor;
103
105
  default: undefined;
@@ -38,11 +38,12 @@ const SelectTrigger = /* @__PURE__ */ defineComponent({
38
38
  type: Boolean,
39
39
  default: false
40
40
  },
41
+ /** 无值时的占位文案(**透传给输入面**:`hasValue === false` 时由它渲染并取代 `default` 插槽) */
41
42
  placeholder: {
42
43
  type: String,
43
44
  default: "请选择"
44
45
  },
45
- /** 单选显示文本(无值时不渲染它,改渲染 `placeholder`) */
46
+ /** 单选显示文本(只在**有值**时渲染;无值态由输入面的占位承担) */
46
47
  label: {
47
48
  type: String,
48
49
  default: void 0
@@ -72,6 +73,7 @@ const SelectTrigger = /* @__PURE__ */ defineComponent({
72
73
  "disabled": props.disabled,
73
74
  "clearable": props.clearable,
74
75
  "hasValue": props.hasValue,
76
+ "placeholder": props.placeholder,
75
77
  "class": "mh-popup-anchor mh-select-host",
76
78
  "controlClass": controlClass(),
77
79
  "controlAttrs": {
@@ -84,9 +86,12 @@ const SelectTrigger = /* @__PURE__ */ defineComponent({
84
86
  },
85
87
  "onChange": () => props.onClear?.()
86
88
  }, {
87
- default: () => props.renderValue ? props.renderValue() : createVNode("span", {
88
- "class": ["mh-select__value", props.hasValue ? "" : "mh-select__value--placeholder"].filter(Boolean).join(" ")
89
- }, [props.hasValue ? props.label : props.placeholder]),
89
+ default: () => props.renderValue ? props.renderValue() : (
90
+ /* 只画**有值**时的那一行;无值态在上面被输入面的占位取代(`default` 不会被调用) */
91
+ createVNode("span", {
92
+ "class": "mh-select__value"
93
+ }, [props.label])
94
+ ),
90
95
  /* 箭头沿用输入框的右侧图标位(槽内尺寸收敛见 `select.css` 的 `.mh-select-host` 一节);
91
96
  展开转向由 `popup.css` 的 `.mh-popup-caret` 承担(含触发器进表壳后的兄弟档) */
92
97
  suffix: () => createVNode(Icon, {