@manohub/ui 0.9.0 → 0.10.0

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 (32) 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 +48 -7
  9. package/dist/components/input/index.js +23 -3
  10. package/dist/components/radio/index.js +29 -3
  11. package/dist/components/search/index.js +37 -35
  12. package/dist/components/select/index.js +1 -3
  13. package/dist/components/select/select-trigger.d.ts +4 -2
  14. package/dist/components/select/select-trigger.js +9 -4
  15. package/dist/components/table/index.d.ts +19 -8
  16. package/dist/components/table/index.js +3 -3
  17. package/dist/components/text/index.d.ts +4 -3
  18. package/dist/components/toast/index.js +16 -1
  19. package/dist/styles/components/avatar.css +4 -1
  20. package/dist/styles/components/button.css +2 -0
  21. package/dist/styles/components/filter.css +22 -34
  22. package/dist/styles/components/input.css +24 -0
  23. package/dist/styles/components/select.css +3 -4
  24. package/dist/styles/components/table.css +33 -22
  25. package/dist/styles/components/table.tokens.css +6 -6
  26. package/dist/styles/components/text.css +10 -0
  27. package/dist/styles/components/toast.css +35 -6
  28. package/dist/styles/components/upload.css +1 -0
  29. package/dist/styles/index.css +5 -4
  30. package/dist/styles/reset.css +52 -167
  31. package/package.json +2 -2
  32. package/dist/styles/components/filter.tokens.css +0 -14
package/README.md CHANGED
@@ -7,7 +7,7 @@ AIHub **自绘组件层**:不依赖 farris 的手写 Vue TSX 组件 + 自带
7
7
  | 层 | 入口 | 内容 |
8
8
  |---|---|---|
9
9
  | ① 主题 | `@manohub/theme/default.css`(兜底)/ `@manohub/theme/farris.css`(Farris 规范) | **全局**令牌(值)—— 随主题包独立发布,换主题只换这一行 |
10
- | ② 组件面 | `@manohub/ui/styles.css` | **元素级 reset**(盒模型 / 按钮外观 / 列表序号 / 链接下划线 / 标题字阶归零)+ 类与状态协议 + 组件令牌基础值,**无需 JS** 即可使用 |
10
+ | ② 组件面 | `@manohub/ui/styles.css` | **元素级 reset**(只有盒模型 + 容器基座两面,见下)+ 类与状态协议 + 组件令牌基础值,**无需 JS** 即可使用 |
11
11
  | ③ Vue 层 | `@manohub/ui` | Vue 组件:渲染 ② 的类 + 行为增强 |
12
12
  | ④ 服务层 | `@manohub/ui` | 命令式反馈:`toast()` / `confirm()` / `alert()` / `showLoading()`(宿主 + 队列 + 生命周期,共用 ③ 的组件) |
13
13
 
@@ -37,13 +37,19 @@ pnpm add @manohub/ui @manohub/theme
37
37
 
38
38
  只写静态页(不装 Vue)时引这两行即可 —— 类名与 `data-*` 状态协议直接可用。
39
39
 
40
- **② 的第一条 import 就是元素级 reset**(随包内置,不需要第三行):盒模型、按钮外观、
41
- `ul` / `ol` 的序号与缩进、`a` 的下划线、标题的默认字阶一并归零。作用域锚 `[data-manohub-ui]`
42
- 且特异性为 0 —— 应用侧写任何元素选择器都能盖过它,所以**不必再自己写一份全局 reset**。
40
+ **② 的第一条 import 就是元素级 reset**(随包内置,不需要第三行),它只有两面:
43
41
 
44
- 唯一需要留意的是**富文本**:reset 之后 `ul` / `li` 没有圆点、`a` 没有下划线也没有链接色,
45
- 需要富文本排版的页面请给自己的富文本容器类补回 `list-style` 与链接样式
46
- (富文本排版暂无合规归属,见 `@manohub/kit/CONTRACT.md` §10)。
42
+ 1. **盒模型**:`box-sizing: border-box` 覆盖容器、全部后代与伪元素(本包所有面的几何前提);
43
+ 2. **容器基座**:`[data-manohub-ui]` 上的字族 / 字号 / 行高 / 字色 / 底色 / 整链高度(令牌的落点)。
44
+
45
+ 作用域锚 `[data-manohub-ui]` 且特异性为 0 —— 应用侧写任何元素选择器都能盖过它。
46
+ **它有意不再归零标题字阶、列表序号、链接下划线、控件外观**:那些规则在 Tailwind 的层外
47
+ (未分层 > 分层),会把消费方的工具类整片压住(`border*`、`img` 上的 `h-*`、`hidden` …);
48
+ 需要归零的元素各自归到渲染它的组件面上(`img` 在 Avatar / Upload、`a` 在 Button、
49
+ `textarea` 的 `resize` 在 `.mh-input--textarea`、`h1`-`h6` 在 `.mh-text`)。
50
+ Tailwind 消费方另有 preflight 兜底,两者不冲突。
51
+
52
+ 所以**不必再自己写一份全局 reset**;也**不要**用元素级选择器去补(那正是被压住的成因)。
47
53
 
48
54
  本包**可独立发布**:组件与类型一律从 `@manohub/ui` 直接引;下游(如 `@manohub/kit`)若转出口,是下游自己的选择。
49
55
 
@@ -1,4 +1,4 @@
1
- import { defineComponent, ref, computed, watchEffect, createVNode, mergeProps } from "vue";
1
+ import { defineComponent, ref, computed, watchEffect, createVNode, mergeProps, nextTick } from "vue";
2
2
  import { Icon } from "@manohub/icon";
3
3
  const Checkbox = /* @__PURE__ */ defineComponent({
4
4
  name: "Checkbox",
@@ -58,6 +58,10 @@ const Checkbox = /* @__PURE__ */ defineComponent({
58
58
  const next = event.target.checked;
59
59
  emit("update:modelValue", next);
60
60
  props.onChange?.(next);
61
+ void nextTick(() => {
62
+ const el = inputRef.value;
63
+ if (el && el.checked !== !!props.modelValue) el.checked = !!props.modelValue;
64
+ });
61
65
  };
62
66
  return () => {
63
67
  const state = props.indeterminate ? "indeterminate" : props.modelValue ? "checked" : "unchecked";
@@ -1,5 +1,8 @@
1
- import { defineComponent, createVNode, mergeProps, ref, computed, onBeforeUnmount } from "vue";
1
+ import { defineComponent, createVNode, mergeProps, ref, computed, onBeforeUnmount, isVNode } from "vue";
2
2
  import { Icon } from "@manohub/icon";
3
+ function _isSlot(s) {
4
+ return typeof s === "function" || Object.prototype.toString.call(s) === "[object Object]" && !isVNode(s);
5
+ }
3
6
  const CodeView = /* @__PURE__ */ defineComponent({
4
7
  name: "CodeView",
5
8
  inheritAttrs: false,
@@ -285,6 +288,7 @@ const CodeBlockBase = /* @__PURE__ */ defineComponent({
285
288
  attrs
286
289
  }) {
287
290
  return () => {
291
+ let _slot;
288
292
  const shared = {
289
293
  language: props.language,
290
294
  maxHeight: props.maxHeight,
@@ -304,16 +308,19 @@ const CodeBlockBase = /* @__PURE__ */ defineComponent({
304
308
  "onUpdate:modelValue": (v) => emit("update:modelValue", v)
305
309
  }), null);
306
310
  }
307
- return createVNode(CodeView, mergeProps(shared, {
308
- "code": props.code,
309
- "inline": props.inline,
310
- "plain": props.plain,
311
- "copyable": props.copyable,
312
- "wrap": props.wrap,
313
- "onCopy": props.onCopy
314
- }), {
315
- default: () => [slots.default?.()]
316
- });
311
+ const view = {
312
+ ...shared,
313
+ code: props.code,
314
+ inline: props.inline,
315
+ plain: props.plain,
316
+ copyable: props.copyable,
317
+ wrap: props.wrap,
318
+ onCopy: props.onCopy
319
+ };
320
+ const body = slots.default;
321
+ return body ? createVNode(CodeView, view, _isSlot(_slot = body()) ? _slot : {
322
+ default: () => [_slot]
323
+ }) : createVNode(CodeView, view, null);
317
324
  };
318
325
  }
319
326
  });
@@ -41,7 +41,12 @@ export interface FilterProps {
41
41
  title?: string;
42
42
  /** 标题下的说明小字(透传给 `Form.Header`) */
43
43
  description?: string;
44
- /** label 列宽(px);缺省 `56`(见 `FILTER_LABEL_WIDTH`),透传给 `Form` */
44
+ /**
45
+ * label 列宽(px),透传给 `Form`。
46
+ *
47
+ * **缺省不设**:不传即与 `Form` 一致(取令牌 `--ui-form-label-width`,120)。
48
+ * 想收窄(筛选条的标签多为四字)就显式传,例如 `labelWidth={68}`。
49
+ */
45
50
  labelWidth?: number;
46
51
  /** 受控条件快照(`条件名 → 值`);不传即非受控(由 `defaultValue` 起) */
47
52
  modelValue?: FilterValues;
@@ -79,7 +84,8 @@ export interface FilterItemProps {
79
84
  * 增量只有一处 —— **读写的值都是归一后的**(`'all'` / 空数组 / 空值 → 空串),
80
85
  * 于是消费方直接把 `value` 绑给控件,不必自己处理哨兵。
81
86
  *
82
- * 字段宽**自适应**:容器够宽一行排下,窄了整体换行(列距 24 / 行距 16)。
87
+ * 字段宽**自适应**:字段区是 `Form` 的多列动态档(`Filter` 传 `columns="auto"`),
88
+ * 故本件只输出 `Form.Item` 自己的类 —— 列宽由那条网格轨道算,换行下来的字段与上一行逐列对齐。
83
89
  *
84
90
  * @example
85
91
  * <Filter.Item code="status" label="授权状态">
@@ -2,8 +2,6 @@ import { defineComponent, createVNode, ref, h, Fragment } from "vue";
2
2
  import { Button } from "../button/index.js";
3
3
  import { FormItem, Form } from "../form/index.js";
4
4
  const FILTER_ALL = "all";
5
- const FILTER_LABEL_WIDTH = 68;
6
- const FILTER_GAP = 16;
7
5
  function normalizeFilterValue(value) {
8
6
  if (Array.isArray(value)) {
9
7
  const kept = value.map((item) => normalizeFilterValue(item)).filter((item) => item !== "");
@@ -96,7 +94,7 @@ const FilterRoot = /* @__PURE__ */ defineComponent({
96
94
  emit("update:modelValue", next);
97
95
  props.onReset?.(next);
98
96
  };
99
- const onFieldsKeydown = (event) => {
97
+ const onFormKeydown = (event) => {
100
98
  if (event.key !== "Enter" || event.isComposing) return;
101
99
  const target = event.target;
102
100
  if (target?.tagName !== "INPUT" || target.closest("[popover]")) return;
@@ -118,30 +116,23 @@ const FilterRoot = /* @__PURE__ */ defineComponent({
118
116
  "ref": formRef,
119
117
  "class": ["mh-filter__form", attrs.class].filter(Boolean).join(" "),
120
118
  "style": attrs.style,
121
- "labelWidth": props.labelWidth ?? FILTER_LABEL_WIDTH,
122
- "gap": FILTER_GAP,
119
+ "labelWidth": props.labelWidth,
120
+ "columns": "auto",
121
+ "onKeydown": onFormKeydown,
123
122
  "modelValue": props.modelValue,
124
123
  "defaultValue": props.defaultValue,
125
124
  "onChange": onFormChange
126
125
  }, {
127
- default: () => [
128
- /* 插槽一律走对象形态:TSX 的 `v-slots={{…}}` 会被当**普通属性**丢掉
129
- (见 `list-view/index.tsx` 同款记录),而多子节点直传会触发
130
- 「Non-function value encountered for default slot」告警(同 `page/index.tsx`)。 */
131
- h(Form.Header, {
126
+ default: () => {
127
+ const raw = slots.default?.();
128
+ const fields = Array.isArray(raw) ? raw : raw == null ? [] : [raw];
129
+ return [h(Form.Header, {
132
130
  title: props.title,
133
131
  description: props.description
134
132
  }, {
135
133
  extra: renderActions
136
- }),
137
- // 字段区也用插槽对象形态传子节点:`h()` 的第三参不收 `null`(`slots.default?.()` 的返回可能是 null)
138
- h("div", {
139
- class: "mh-filter__fields",
140
- onKeydown: onFieldsKeydown
141
- }, {
142
- default: () => slots.default?.()
143
- })
144
- ]
134
+ }), ...fields];
135
+ }
145
136
  });
146
137
  }
147
138
  });
@@ -181,7 +172,7 @@ const FilterItem = /* @__PURE__ */ defineComponent({
181
172
  "required": props.required,
182
173
  "error": props.error,
183
174
  "hint": props.hint,
184
- "class": ["mh-filter__item", attrs.class].filter(Boolean).join(" "),
175
+ "class": attrs.class,
185
176
  "style": attrs.style
186
177
  }, {
187
178
  default: (scope) => slots.default?.({
@@ -99,6 +99,18 @@ export interface FormProps {
99
99
  defaultValue?: FormValues;
100
100
  /** 值变更(快照含**全部已登记字段**,未设值的字段为 `undefined`) */
101
101
  onChange?: (values: FormValues) => void;
102
+ /**
103
+ * 根节点的键盘事件(**转发到表单根**)。
104
+ *
105
+ * 本件是 `inheritAttrs: false`,根节点只显式绑 `class` / `style` —— 不声明的监听器会被**静默丢掉**
106
+ * (与该文件里已修过的「消费方 style 被静默丢弃」同款前科)。而根节点就是「全部字段行的容器」,
107
+ * 故它是「字段区层面的委托」的落点:本包 `Filter` 的「字段区回车 = 查询」就搭在这条通道上
108
+ * (构造 `Filter` 时因此不必再套一层只为挂事件的壳 —— 那会改变 `class` / `style` 的落点)。
109
+ *
110
+ * **有意只开这一条**:其它监听器(如 `onClick`)不转发;要把某一条也接进来,先当一次公开行为变更裁决。
111
+ * 要不要拦、拦哪些按键(本包 `Filter` 收窄到「`<input>` 上的回车」)由调用方自己判断,本件只转发。
112
+ */
113
+ onKeydown?: (event: KeyboardEvent) => void;
102
114
  }
103
115
  export interface FormItemProps {
104
116
  /**
@@ -193,6 +205,15 @@ export interface FormItemProps {
193
205
  * `{ y, x }` 上下 / 左右分别给,缺省 0(贴容器边)。页面里的表单在 `Panel.Body` 里、内距由那块给;
194
206
  * **弹窗 / 抽屉里没有别的容器**给内距,就用表单自带的 `padding={{ y: 16, x: 20 }}`。
195
207
  *
208
+ * ## 键盘事件(`onKeydown` 转发到根节点)
209
+ *
210
+ * `onKeydown` 是本件的**声明式入口**(缺省不绑):传进来的处理器绑在根节点上 ——
211
+ * 本件是 `inheritAttrs: false`,根节点只显式绑 `class` / `style`,不声明的监听器会被静默丢掉。
212
+ * 根节点即「全部字段行的容器」,因此它是「字段区层面的委托」的落点:本包 `Filter` 的
213
+ * 「字段区回车 = 查询」就搭在这里,构造 `Filter` 时**不必**再套一层只为挂事件的壳
214
+ * (那会改变 `class` / `style` 的落点)。要不要拦、拦哪些按键由调用方自己判断,本件只转发。
215
+ * **有意只开这一条**:其它监听器不转发,扩张要先当一次公开行为变更裁决。
216
+ *
196
217
  * ## 只读值(摘要 / 详情)
197
218
  *
198
219
  * 「向导最后一步的确认摘要」「详情弹窗」不需要另起一套描述列表件 —— 只用表单行本身,
@@ -455,6 +476,11 @@ export declare const Form: {
455
476
  type: PropType<(values: FormValues) => void>;
456
477
  default: undefined;
457
478
  };
479
+ /** 根节点的键盘事件(转发到表单根;缺省不绑)—— 用途与边界见 `FormProps` 的同名注释 */
480
+ onKeydown: {
481
+ type: PropType<(event: KeyboardEvent) => void>;
482
+ default: undefined;
483
+ };
458
484
  }>> & Readonly<{
459
485
  "onUpdate:modelValue"?: ((values: FormValues) => any) | undefined;
460
486
  }>, () => import("vue/jsx-runtime").JSX.Element, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
@@ -463,6 +489,7 @@ export declare const Form: {
463
489
  padding: Padding;
464
490
  layout: "horizontal" | "vertical";
465
491
  onChange: (values: FormValues) => void;
492
+ onKeydown: (event: KeyboardEvent) => void;
466
493
  defaultValue: FormValues;
467
494
  gap: number;
468
495
  columns: FormColumns;
@@ -544,12 +571,18 @@ export declare const Form: {
544
571
  type: PropType<(values: FormValues) => void>;
545
572
  default: undefined;
546
573
  };
574
+ /** 根节点的键盘事件(转发到表单根;缺省不绑)—— 用途与边界见 `FormProps` 的同名注释 */
575
+ onKeydown: {
576
+ type: PropType<(event: KeyboardEvent) => void>;
577
+ default: undefined;
578
+ };
547
579
  }>> & Readonly<{
548
580
  "onUpdate:modelValue"?: ((values: FormValues) => any) | undefined;
549
581
  }>, () => import("vue/jsx-runtime").JSX.Element, {}, {}, {}, {
550
582
  padding: Padding;
551
583
  layout: "horizontal" | "vertical";
552
584
  onChange: (values: FormValues) => void;
585
+ onKeydown: (event: KeyboardEvent) => void;
553
586
  defaultValue: FormValues;
554
587
  gap: number;
555
588
  columns: FormColumns;
@@ -626,6 +659,11 @@ export declare const Form: {
626
659
  type: PropType<(values: FormValues) => void>;
627
660
  default: undefined;
628
661
  };
662
+ /** 根节点的键盘事件(转发到表单根;缺省不绑)—— 用途与边界见 `FormProps` 的同名注释 */
663
+ onKeydown: {
664
+ type: PropType<(event: KeyboardEvent) => void>;
665
+ default: undefined;
666
+ };
629
667
  }>> & Readonly<{
630
668
  "onUpdate:modelValue"?: ((values: FormValues) => any) | undefined;
631
669
  }>, () => import("vue/jsx-runtime").JSX.Element, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
@@ -634,6 +672,7 @@ export declare const Form: {
634
672
  padding: Padding;
635
673
  layout: "horizontal" | "vertical";
636
674
  onChange: (values: FormValues) => void;
675
+ onKeydown: (event: KeyboardEvent) => void;
637
676
  defaultValue: FormValues;
638
677
  gap: number;
639
678
  columns: FormColumns;
@@ -236,6 +236,11 @@ const FormRoot = /* @__PURE__ */ defineComponent({
236
236
  onChange: {
237
237
  type: Function,
238
238
  default: void 0
239
+ },
240
+ /** 根节点的键盘事件(转发到表单根;缺省不绑)—— 用途与边界见 `FormProps` 的同名注释 */
241
+ onKeydown: {
242
+ type: Function,
243
+ default: void 0
239
244
  }
240
245
  },
241
246
  emits: {
@@ -313,6 +318,7 @@ const FormRoot = /* @__PURE__ */ defineComponent({
313
318
  } = splitFormHeader(list);
314
319
  return createVNode("div", {
315
320
  "class": ["mh-form", columnsClass(), props.layout === "vertical" ? "mh-form--vertical" : "", attrs.class].filter(Boolean).join(" "),
321
+ "onKeydown": props.onKeydown,
316
322
  "style": [
317
323
  mergePartStyle(attrs.style, props.padding),
318
324
  /* 行距同时下发实体 `gap` 与变量:多列的轨道下限公式要用**同一个**行距值算 ——
@@ -35,8 +35,9 @@ export interface TextareaAutoSize {
35
35
  * <Textarea modelValue={note} autoSize={{ minRows: 2, maxRows: 6 }} maxlength={500} showCount onChange={setNote} />
36
36
  *
37
37
  * @example 触发器形态(复合件用:控件换成 `<button>`,表壳 / 清除位 / 右侧图标位全部复用)
38
- * <Input as="button" clearable hasValue={!!value} controlClass="my-picker" controlAttrs={{ popovertarget: id }}>
39
- * {{ default: () => value ?? '请选择', suffix: () => <Icon name="chevron-down" /> }}
38
+ * <Input as="button" clearable hasValue={!!value} placeholder="请选择" controlClass="my-picker"
39
+ * controlAttrs={{ popovertarget: id }}>
40
+ * {{ default: () => value, suffix: () => <Icon name="chevron-down" /> }}
40
41
  * </Input>
41
42
  */
42
43
  export declare const Input: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
@@ -50,6 +51,13 @@ export declare const Input: import("vue").DefineComponent<import("vue").ExtractP
50
51
  type: StringConstructor;
51
52
  default: string;
52
53
  };
54
+ /**
55
+ * 占位文案。两种形态都消费它:
56
+ * - 缺省(`as="input"`)走原生 `::placeholder` 伪元素;
57
+ * - 触发器形态(`as="button"`)**没有 `::placeholder` 可用**,改由一个 span 承载
58
+ * (`.mh-input__placeholder`)—— 当且仅当 `hasValue === false` 且本 prop 有值时渲染它,
59
+ * 并**取代** `default` 插槽(见上面触发器一节)。没传 `hasValue` 的调用方行为不变。
60
+ */
53
61
  placeholder: {
54
62
  type: StringConstructor;
55
63
  default: undefined;
@@ -142,7 +150,8 @@ export declare const Input: import("vue").DefineComponent<import("vue").ExtractP
142
150
  type: PropType<Record<string, unknown>>;
143
151
  default: undefined;
144
152
  };
145
- /** 显式覆盖清除位显隐;不给时仍按「有内容」判定(多选类复合件没有单值可判) */
153
+ /** 显式覆盖清除位显隐;不给时仍按「有内容」判定(多选类复合件没有单值可判)。
154
+ * 同时是触发器形态「显示值还是显示占位」的判据(仅当它为 `false` 且传了 `placeholder` 时接管) */
146
155
  hasValue: {
147
156
  type: BooleanConstructor;
148
157
  default: undefined;
@@ -160,6 +169,13 @@ export declare const Input: import("vue").DefineComponent<import("vue").ExtractP
160
169
  type: StringConstructor;
161
170
  default: string;
162
171
  };
172
+ /**
173
+ * 占位文案。两种形态都消费它:
174
+ * - 缺省(`as="input"`)走原生 `::placeholder` 伪元素;
175
+ * - 触发器形态(`as="button"`)**没有 `::placeholder` 可用**,改由一个 span 承载
176
+ * (`.mh-input__placeholder`)—— 当且仅当 `hasValue === false` 且本 prop 有值时渲染它,
177
+ * 并**取代** `default` 插槽(见上面触发器一节)。没传 `hasValue` 的调用方行为不变。
178
+ */
163
179
  placeholder: {
164
180
  type: StringConstructor;
165
181
  default: undefined;
@@ -252,7 +268,8 @@ export declare const Input: import("vue").DefineComponent<import("vue").ExtractP
252
268
  type: PropType<Record<string, unknown>>;
253
269
  default: undefined;
254
270
  };
255
- /** 显式覆盖清除位显隐;不给时仍按「有内容」判定(多选类复合件没有单值可判) */
271
+ /** 显式覆盖清除位显隐;不给时仍按「有内容」判定(多选类复合件没有单值可判)。
272
+ * 同时是触发器形态「显示值还是显示占位」的判据(仅当它为 `false` 且传了 `placeholder` 时接管) */
256
273
  hasValue: {
257
274
  type: BooleanConstructor;
258
275
  default: undefined;
@@ -478,6 +495,13 @@ export declare const InputCompound: {
478
495
  type: StringConstructor;
479
496
  default: string;
480
497
  };
498
+ /**
499
+ * 占位文案。两种形态都消费它:
500
+ * - 缺省(`as="input"`)走原生 `::placeholder` 伪元素;
501
+ * - 触发器形态(`as="button"`)**没有 `::placeholder` 可用**,改由一个 span 承载
502
+ * (`.mh-input__placeholder`)—— 当且仅当 `hasValue === false` 且本 prop 有值时渲染它,
503
+ * 并**取代** `default` 插槽(见上面触发器一节)。没传 `hasValue` 的调用方行为不变。
504
+ */
481
505
  placeholder: {
482
506
  type: StringConstructor;
483
507
  default: undefined;
@@ -570,7 +594,8 @@ export declare const InputCompound: {
570
594
  type: PropType<Record<string, unknown>>;
571
595
  default: undefined;
572
596
  };
573
- /** 显式覆盖清除位显隐;不给时仍按「有内容」判定(多选类复合件没有单值可判) */
597
+ /** 显式覆盖清除位显隐;不给时仍按「有内容」判定(多选类复合件没有单值可判)。
598
+ * 同时是触发器形态「显示值还是显示占位」的判据(仅当它为 `false` 且传了 `placeholder` 时接管) */
574
599
  hasValue: {
575
600
  type: BooleanConstructor;
576
601
  default: undefined;
@@ -622,6 +647,13 @@ export declare const InputCompound: {
622
647
  type: StringConstructor;
623
648
  default: string;
624
649
  };
650
+ /**
651
+ * 占位文案。两种形态都消费它:
652
+ * - 缺省(`as="input"`)走原生 `::placeholder` 伪元素;
653
+ * - 触发器形态(`as="button"`)**没有 `::placeholder` 可用**,改由一个 span 承载
654
+ * (`.mh-input__placeholder`)—— 当且仅当 `hasValue === false` 且本 prop 有值时渲染它,
655
+ * 并**取代** `default` 插槽(见上面触发器一节)。没传 `hasValue` 的调用方行为不变。
656
+ */
625
657
  placeholder: {
626
658
  type: StringConstructor;
627
659
  default: undefined;
@@ -714,7 +746,8 @@ export declare const InputCompound: {
714
746
  type: PropType<Record<string, unknown>>;
715
747
  default: undefined;
716
748
  };
717
- /** 显式覆盖清除位显隐;不给时仍按「有内容」判定(多选类复合件没有单值可判) */
749
+ /** 显式覆盖清除位显隐;不给时仍按「有内容」判定(多选类复合件没有单值可判)。
750
+ * 同时是触发器形态「显示值还是显示占位」的判据(仅当它为 `false` 且传了 `placeholder` 时接管) */
718
751
  hasValue: {
719
752
  type: BooleanConstructor;
720
753
  default: undefined;
@@ -757,6 +790,13 @@ export declare const InputCompound: {
757
790
  type: StringConstructor;
758
791
  default: string;
759
792
  };
793
+ /**
794
+ * 占位文案。两种形态都消费它:
795
+ * - 缺省(`as="input"`)走原生 `::placeholder` 伪元素;
796
+ * - 触发器形态(`as="button"`)**没有 `::placeholder` 可用**,改由一个 span 承载
797
+ * (`.mh-input__placeholder`)—— 当且仅当 `hasValue === false` 且本 prop 有值时渲染它,
798
+ * 并**取代** `default` 插槽(见上面触发器一节)。没传 `hasValue` 的调用方行为不变。
799
+ */
760
800
  placeholder: {
761
801
  type: StringConstructor;
762
802
  default: undefined;
@@ -849,7 +889,8 @@ export declare const InputCompound: {
849
889
  type: PropType<Record<string, unknown>>;
850
890
  default: undefined;
851
891
  };
852
- /** 显式覆盖清除位显隐;不给时仍按「有内容」判定(多选类复合件没有单值可判) */
892
+ /** 显式覆盖清除位显隐;不给时仍按「有内容」判定(多选类复合件没有单值可判)。
893
+ * 同时是触发器形态「显示值还是显示占位」的判据(仅当它为 `false` 且传了 `placeholder` 时接管) */
853
894
  hasValue: {
854
895
  type: BooleanConstructor;
855
896
  default: undefined;
@@ -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
@@ -95,7 +102,13 @@ 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
+ * **无值时的占位**:`<button>` 没有 `::placeholder` 伪元素可用,故 `placeholder` 在这个形态下
108
+ * 由本件渲染成一个 span(`.mh-input__placeholder`,色档与原生占位一致);判据是
109
+ * `hasValue === false && placeholder` **同时**成立 —— 两者缺一即沿用消费方给的 `default` 插槽。
110
+ * 这样「有值渲染值、无值渲染占位」这件例行事的文案与切换都由输入面统一,复合件不必各画一遍
111
+ * (`Select` 早期为自己的值区单画过一份,见 `select-trigger.tsx`)。 */
99
112
  /** 控件元素形态:`input` 文本输入(缺省)/ `button` 触发器 */
100
113
  as: {
101
114
  type: String,
@@ -111,7 +124,8 @@ const Input = /* @__PURE__ */ defineComponent({
111
124
  type: Object,
112
125
  default: void 0
113
126
  },
114
- /** 显式覆盖清除位显隐;不给时仍按「有内容」判定(多选类复合件没有单值可判) */
127
+ /** 显式覆盖清除位显隐;不给时仍按「有内容」判定(多选类复合件没有单值可判)。
128
+ * 同时是触发器形态「显示值还是显示占位」的判据(仅当它为 `false` 且传了 `placeholder` 时接管) */
115
129
  hasValue: {
116
130
  type: Boolean,
117
131
  default: void 0
@@ -149,6 +163,7 @@ const Input = /* @__PURE__ */ defineComponent({
149
163
  const isTrigger = computed(() => props.as === "button");
150
164
  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
165
  const showClear = computed(() => props.clearable && (props.hasValue ?? !!current.value));
166
+ const showPlaceholder = computed(() => isTrigger.value && props.hasValue === false && !!props.placeholder);
152
167
  return () => createVNode("div", {
153
168
  "class": ["mh-input__wrap", attrs.class].filter(Boolean).join(" "),
154
169
  "style": attrs.style
@@ -164,7 +179,12 @@ const Input = /* @__PURE__ */ defineComponent({
164
179
  "class": controlClasses.value,
165
180
  "disabled": props.disabled,
166
181
  "aria-invalid": isInvalid.value ? "true" : void 0
167
- }), [slots.default ? slots.default() : null])
182
+ }), [showPlaceholder.value ? (
183
+ /* 触发器没有 `::placeholder`,占位文案由本件渲染(色见 `.mh-input__placeholder`) */
184
+ createVNode("span", {
185
+ "class": "mh-input__placeholder"
186
+ }, [props.placeholder])
187
+ ) : slots.default ? slots.default() : null])
168
188
  ) : createVNode("input", {
169
189
  "class": controlClasses.value,
170
190
  "type": inputType.value,
@@ -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 {