@manohub/ui 0.10.1 → 0.10.3

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 (51) hide show
  1. package/README.md +1 -1
  2. package/dist/components/badge/index.d.ts +46 -1
  3. package/dist/components/badge/index.js +42 -9
  4. package/dist/components/card/index.d.ts +1 -1
  5. package/dist/components/drawer/index.d.ts +1 -1
  6. package/dist/components/form/index.d.ts +50 -1
  7. package/dist/components/form/index.js +20 -2
  8. package/dist/components/input/index.js +1 -0
  9. package/dist/components/nav/index.d.ts +27 -0
  10. package/dist/components/nav/index.js +12 -0
  11. package/dist/components/notice/index.d.ts +1 -1
  12. package/dist/components/notification/index.d.ts +2 -2
  13. package/dist/components/select/index.d.ts +9 -0
  14. package/dist/components/select/index.js +57 -12
  15. package/dist/components/select/select-list.d.ts +42 -8
  16. package/dist/components/select/select-list.js +36 -11
  17. package/dist/components/select/select-panel.d.ts +36 -8
  18. package/dist/components/select/select-panel.js +18 -4
  19. package/dist/components/select/select-shared.d.ts +51 -10
  20. package/dist/components/select/select-shared.js +12 -2
  21. package/dist/components/select/select-tree.d.ts +25 -3
  22. package/dist/components/select/select-tree.js +17 -6
  23. package/dist/components/select/select-trigger.d.ts +21 -0
  24. package/dist/components/select/select-trigger.js +15 -1
  25. package/dist/components/tag/index.d.ts +32 -153
  26. package/dist/components/tag/index.js +102 -101
  27. package/dist/components/text/index.d.ts +1 -1
  28. package/dist/components/tooltip/index.d.ts +1 -1
  29. package/dist/components/tree/index.d.ts +65 -0
  30. package/dist/components/tree/index.js +29 -2
  31. package/dist/index.d.ts +1 -1
  32. package/dist/styles/components/avatar.css +2 -2
  33. package/dist/styles/components/badge.css +31 -4
  34. package/dist/styles/components/badge.tokens.css +24 -0
  35. package/dist/styles/components/button.css +7 -6
  36. package/dist/styles/components/button.tokens.css +5 -2
  37. package/dist/styles/components/dialog.css +13 -0
  38. package/dist/styles/components/divider.css +8 -4
  39. package/dist/styles/components/divider.tokens.css +2 -1
  40. package/dist/styles/components/form.css +51 -0
  41. package/dist/styles/components/notification.css +1 -1
  42. package/dist/styles/components/select.css +62 -39
  43. package/dist/styles/components/select.tokens.css +16 -9
  44. package/dist/styles/components/tag.css +77 -154
  45. package/dist/styles/components/tag.tokens.css +13 -19
  46. package/dist/styles/components/tree.css +37 -3
  47. package/dist/styles/components/tree.tokens.css +4 -3
  48. package/dist/styles/components/upload.tokens.css +1 -1
  49. package/dist/styles/index.css +2 -1
  50. package/dist/styles/operations/popup.css +5 -1
  51. package/package.json +2 -2
package/README.md CHANGED
@@ -62,7 +62,7 @@ import { Card, Form, Tree, Checkbox } from '@manohub/ui'
62
62
  | 分组 | 组件 |
63
63
  |---|---|
64
64
  | 容器与分区 | `Page`(页面级骨架:`.Header` / `.Body` / `.Footer` / `.Filter` / `.Toolbar` / `.Split`)、`Panel`(+ `.Header` / `.Body` / `.Footer`)、`Card`(+ 三件)、`Nav`、`Layout`(`Layout.Row` 排「行」/ `Layout.Column` 排「列」:轴向 × 间距档 × 对齐) |
65
- | 数据展示 | `Tree`、`ListView`(+ `.CardItem`)、`Badge`、`Tag`(常规 / 状态 / 可选 / 新增 / 筛选片) |
65
+ | 数据展示 | `Tree`、`ListView`(+ `.CardItem`)、`Badge`、`Tag`(`Badge` 的可关闭 / 可新增封装) |
66
66
  | 展示 | `Avatar`(+ `.Group`)、`Skeleton`(行 / 列表 / 块状) |
67
67
  | 表单基础(**自绘**) | `Button`(+ `.Group` / `.Divider`;五型 / 小号 / 加载 / 菜单按钮 / 连体切换)、`Input`(+ `.Group` / `.Chip` / `.Display` / `.Textarea`;清除 / 可见性切换 / 校验)、`Search`(搜索框:值 / 搜索 / 清空三条回调)、`Select`(单选 / 多选 / 搜索 / 加载;面板用原生 popover)、`SelectTree`(树形态:仅叶子可选 + 本地过滤 + 命中高亮;与 `Select` 平级导出、共用触发器与面板)、`Switch`(表格档 / 表单档) |
68
68
  | 表单与选择 | `Form`(+ `.Header` / `.Item`;标签体系、成员归位、`v-model` 值收集与命令式 `clear()`)、`Filter`(+ `.Item`;`Form` 的壳:条件字段写成子件、控件自放、查询/重置与快照归一)、`Checkbox`、`Radio`(+ `.Group` / `.Card`;卡片式单选)、`Capsule`(胶囊选择器;胶囊 / 方形两档外形,与 `Tabs` 组合即页签) |
@@ -19,6 +19,13 @@ export type BadgeStatus = 'success' | 'processing' | 'default' | 'warning' | 'er
19
19
  * - `outline`:浅底 + 同色描边;`tone="default"` 时退化为白底 + 中性描边 + 正文色(状态胶囊)
20
20
  * - `plain`:无底无框,靠圆点表达状态
21
21
  *
22
+ * **本件是「只读标记」的唯一样式源**:`Tag`(可关闭 / 可新增)只是在本件之上加交互位,
23
+ * 几何与色彩一行都不重写 —— 改本件的档位,两件一起走(见 `components/tag/index.tsx`)。
24
+ *
25
+ * **属性落点**:`class` / `style` 有专口(并入档位类、随档位内联),**其余 attrs 一律透传到根元素**
26
+ * (`id` / `aria-*` / `data-*` / `title` / 原生事件)—— 停在组件上等于丢掉(`Tag` 的「新增」
27
+ * 就靠这条把 `onClick` 落到那颗按钮上),见 `forwardAttrs`。
28
+ *
22
29
  * @example 内容徽标
23
30
  * <Badge tone="success" shape="outline" size="md" dot="filled">运行中</Badge>
24
31
  *
@@ -28,6 +35,9 @@ export type BadgeStatus = 'success' | 'processing' | 'default' | 'warning' | 'er
28
35
  *
29
36
  * @example 状态点
30
37
  * <Badge status="processing" text="运行中" />
38
+ *
39
+ * @example 可点形态(复合件用;`Tag` 的「新增」即此形 + 本件自己的遮罩)
40
+ * <Badge as="button" tone="warning" onClick={() => add()}>新增</Badge>
31
41
  */
32
42
  export declare const Badge: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
33
43
  tone: {
@@ -73,6 +83,23 @@ export declare const Badge: import("vue").DefineComponent<import("vue").ExtractP
73
83
  type: StringConstructor;
74
84
  default: undefined;
75
85
  };
86
+ /**
87
+ * 根元素形态:`span`(缺省,纯展示)/ `button`(可点形态,给**复合件**用)。
88
+ *
89
+ * `Tag` 就是靠它拿到「可点」的:徽标是**只读标记**的唯一样式源,`Tag` 只往上加
90
+ * 关闭位与新增位,不再自己画一遍框(见 `components/tag/index.tsx`)。
91
+ * `as="button"` 会渲染真实 `<button type="button">`(`type` 不能省:缺省是 `submit`,
92
+ * 放进表单会提交),并保留本件全部档位类与几何 —— 按钮的外观缺三项由本件面自带
93
+ * (`border` / `background` / `padding`,见 `contract-reset.spec.ts` 的第三组),
94
+ * `font-family: inherit` 在 `badge.css` 的基块补(UA 会把按钮字形换成 Arial)。
95
+ *
96
+ * **只对「内容徽标」形态生效**:`count` / `status` 两种形态是装饰(角标、状态点 + 文字),
97
+ * 传了它们就忽略本 prop(那时走各自的分支,根仍是 `<span>`)。
98
+ */
99
+ as: {
100
+ type: PropType<"span" | "button">;
101
+ default: string;
102
+ };
76
103
  }>, () => import("vue/jsx-runtime").JSX.Element | null, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
77
104
  tone: {
78
105
  type: PropType<BadgeTone>;
@@ -117,16 +144,34 @@ export declare const Badge: import("vue").DefineComponent<import("vue").ExtractP
117
144
  type: StringConstructor;
118
145
  default: undefined;
119
146
  };
147
+ /**
148
+ * 根元素形态:`span`(缺省,纯展示)/ `button`(可点形态,给**复合件**用)。
149
+ *
150
+ * `Tag` 就是靠它拿到「可点」的:徽标是**只读标记**的唯一样式源,`Tag` 只往上加
151
+ * 关闭位与新增位,不再自己画一遍框(见 `components/tag/index.tsx`)。
152
+ * `as="button"` 会渲染真实 `<button type="button">`(`type` 不能省:缺省是 `submit`,
153
+ * 放进表单会提交),并保留本件全部档位类与几何 —— 按钮的外观缺三项由本件面自带
154
+ * (`border` / `background` / `padding`,见 `contract-reset.spec.ts` 的第三组),
155
+ * `font-family: inherit` 在 `badge.css` 的基块补(UA 会把按钮字形换成 Arial)。
156
+ *
157
+ * **只对「内容徽标」形态生效**:`count` / `status` 两种形态是装饰(角标、状态点 + 文字),
158
+ * 传了它们就忽略本 prop(那时走各自的分支,根仍是 `<span>`)。
159
+ */
160
+ as: {
161
+ type: PropType<"span" | "button">;
162
+ default: string;
163
+ };
120
164
  }>> & Readonly<{}>, {
121
165
  text: string;
122
166
  size: BadgeSize;
123
167
  dot: boolean | BadgeDot;
124
168
  shape: BadgeShape;
125
169
  count: number;
126
- status: BadgeStatus;
127
170
  tone: BadgeTone;
128
171
  overflowCount: number;
129
172
  showZero: boolean;
173
+ status: BadgeStatus;
174
+ as: "button" | "span";
130
175
  }, SlotsType<{
131
176
  default?: () => VNodeChild;
132
177
  }>, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
@@ -1,4 +1,4 @@
1
- import { defineComponent, computed, createVNode } from "vue";
1
+ import { defineComponent, computed, createVNode, mergeProps, Fragment } from "vue";
2
2
  const STATUS_TONE = {
3
3
  success: "success",
4
4
  processing: "primary",
@@ -52,6 +52,23 @@ const Badge = /* @__PURE__ */ defineComponent({
52
52
  text: {
53
53
  type: String,
54
54
  default: void 0
55
+ },
56
+ /**
57
+ * 根元素形态:`span`(缺省,纯展示)/ `button`(可点形态,给**复合件**用)。
58
+ *
59
+ * `Tag` 就是靠它拿到「可点」的:徽标是**只读标记**的唯一样式源,`Tag` 只往上加
60
+ * 关闭位与新增位,不再自己画一遍框(见 `components/tag/index.tsx`)。
61
+ * `as="button"` 会渲染真实 `<button type="button">`(`type` 不能省:缺省是 `submit`,
62
+ * 放进表单会提交),并保留本件全部档位类与几何 —— 按钮的外观缺三项由本件面自带
63
+ * (`border` / `background` / `padding`,见 `contract-reset.spec.ts` 的第三组),
64
+ * `font-family: inherit` 在 `badge.css` 的基块补(UA 会把按钮字形换成 Arial)。
65
+ *
66
+ * **只对「内容徽标」形态生效**:`count` / `status` 两种形态是装饰(角标、状态点 + 文字),
67
+ * 传了它们就忽略本 prop(那时走各自的分支,根仍是 `<span>`)。
68
+ */
69
+ as: {
70
+ type: String,
71
+ default: "span"
55
72
  }
56
73
  },
57
74
  slots: Object,
@@ -59,6 +76,13 @@ const Badge = /* @__PURE__ */ defineComponent({
59
76
  slots,
60
77
  attrs
61
78
  }) {
79
+ const forwardAttrs = computed(() => {
80
+ const rest = {};
81
+ for (const [key, value] of Object.entries(attrs)) {
82
+ if (key !== "class" && key !== "style") rest[key] = value;
83
+ }
84
+ return rest;
85
+ });
62
86
  const countText = computed(() => {
63
87
  if (props.count === void 0) return null;
64
88
  if (props.count === 0 && !props.showZero) return null;
@@ -69,28 +93,37 @@ const Badge = /* @__PURE__ */ defineComponent({
69
93
  const style = attrs.style;
70
94
  const extra = attrs.class;
71
95
  if (props.status) {
72
- return createVNode("span", {
96
+ return createVNode("span", mergeProps(forwardAttrs.value, {
73
97
  "class": ["mh-badge--status", `mh-badge--${STATUS_TONE[props.status]}`, extra].filter(Boolean).join(" "),
74
98
  "style": style
75
- }, [createVNode("span", {
99
+ }), [createVNode("span", {
76
100
  "class": "mh-badge__status-dot"
77
101
  }, null), props.text ?? slots.default?.()]);
78
102
  }
79
103
  if (props.count !== void 0) {
80
104
  if (countText.value === null && !slots.default) return null;
81
- return createVNode("span", {
105
+ return createVNode("span", mergeProps(forwardAttrs.value, {
82
106
  "class": ["mh-badge--count", extra].filter(Boolean).join(" "),
83
107
  "style": style
84
- }, [slots.default?.(), countText.value !== null ? createVNode("sup", {
108
+ }), [slots.default?.(), countText.value !== null ? createVNode("sup", {
85
109
  "class": ["mh-badge__count", `mh-badge--${props.tone}`].join(" ")
86
110
  }, [countText.value]) : null]);
87
111
  }
88
- return createVNode("span", {
89
- "class": ["mh-badge", `mh-badge--${props.tone}`, `mh-badge--${props.shape}`, `mh-badge--${props.size}`, dotKind.value ? `mh-badge--dot-${dotKind.value}` : "", extra].filter(Boolean).join(" "),
90
- "style": style
91
- }, [dotKind.value ? createVNode("span", {
112
+ const classes = ["mh-badge", `mh-badge--${props.tone}`, `mh-badge--${props.shape}`, `mh-badge--${props.size}`, dotKind.value ? `mh-badge--dot-${dotKind.value}` : "", extra].filter(Boolean).join(" ");
113
+ const content = createVNode(Fragment, null, [dotKind.value ? createVNode("span", {
92
114
  "class": "mh-badge__dot"
93
115
  }, null) : null, slots.default?.()]);
116
+ if (props.as === "button") {
117
+ return createVNode("button", mergeProps(forwardAttrs.value, {
118
+ "type": "button",
119
+ "class": classes,
120
+ "style": style
121
+ }), [content]);
122
+ }
123
+ return createVNode("span", mergeProps(forwardAttrs.value, {
124
+ "class": classes,
125
+ "style": style
126
+ }), [content]);
94
127
  };
95
128
  }
96
129
  });
@@ -76,7 +76,7 @@ export type CardHeaderIcon = string | (() => VNodeChild);
76
76
  * @example 技能卡(图标 + 标题/副标题 + 右端状态 + 正文 + 操作行)
77
77
  * <Card>
78
78
  * <Card.Header icon={skill.icon} title={skill.name} subtitle={`V${skill.version}`}>
79
- * <template #extra><Tag shape="status" tone="success">已发布</Tag></template>
79
+ * <template #extra><Badge shape="soft" tone="success">已发布</Badge></template>
80
80
  * </Card.Header>
81
81
  * <Card.Body>{skill.description}</Card.Body>
82
82
  * <template #actions><Button size="sm">编辑</Button></template>
@@ -221,6 +221,7 @@ export declare const Drawer: import("vue").DefineComponent<import("vue").Extract
221
221
  onClose: () => void;
222
222
  width: string | number;
223
223
  closable: boolean;
224
+ side: DrawerSide;
224
225
  closeOnBackdrop: boolean;
225
226
  maskClosable: boolean;
226
227
  keyboard: boolean;
@@ -230,7 +231,6 @@ export declare const Drawer: import("vue").DefineComponent<import("vue").Extract
230
231
  confirmLoading: boolean;
231
232
  showFooter: boolean | undefined;
232
233
  onOk: () => unknown;
233
- side: DrawerSide;
234
234
  }, SlotsType<{
235
235
  default?: () => VNodeChild;
236
236
  title?: () => VNodeChild;
@@ -133,6 +133,21 @@ export interface FormItemProps {
133
133
  error?: string;
134
134
  /** 字段说明文案(常驻,排在错误之后) */
135
135
  hint?: string;
136
+ /**
137
+ * **label 旁的提示**(问号图标 + 悬停气泡,仅文本)。
138
+ *
139
+ * 与 `hint` 分工不同:`hint` 是**常驻**在控件下方的说明文案,`tip` 是**按需展开**的补充解释 ——
140
+ * 不占版面,只在悬停(或键盘聚焦)问号图标时出现,两者可以同时给。
141
+ *
142
+ * 图标排在 label 文本与必填星号**之后**(`服务名称*ⓘ`);只给 `tip` 而不给 `label` 时同样保留
143
+ * label 列(与「只给 required」同一判据)—— 否则图标没有列可落、静默消失。
144
+ *
145
+ * 用的是 `Tooltip` 的 **hover 档**(零 JS):显隐与定位全在 CSS,移入 / 聚焦即显、移开即隐,
146
+ * 因此**不吃点击、不抢焦点**。代价一条:hover 档不进浏览器顶层浮层,会被祖先 `overflow` 裁切
147
+ * —— 弹窗 / 抽屉里贴着裁剪边的字段可能看不全(要顶层浮层就得换 `Tooltip trigger="click"`,
148
+ * 本件有意不暴露该维度,见文件头「label 提示」)。
149
+ */
150
+ tip?: string;
136
151
  /** 本行 label 列宽(px);覆盖 `Form` 的表单级设置 */
137
152
  labelWidth?: number;
138
153
  /** 本行 label 对齐;覆盖 `Form` 的表单级设置 */
@@ -205,6 +220,18 @@ export interface FormItemProps {
205
220
  * `{ y, x }` 上下 / 左右分别给,缺省 0(贴容器边)。页面里的表单在 `Panel.Body` 里、内距由那块给;
206
221
  * **弹窗 / 抽屉里没有别的容器**给内距,就用表单自带的 `padding={{ y: 16, x: 20 }}`。
207
222
  *
223
+ * ## label 提示(`tip`)
224
+ *
225
+ * 字段名(与必填星号)之后挂一枚问号图标,悬停 / 键盘聚焦弹出说明气泡。它是**按需展开**的补充
226
+ * 解释,不占版面;常驻说明仍归 `hint`(排在控件下方)。两者分工不同,也**不互相替代**。
227
+ *
228
+ * - 图标取 `@manohub/icon` 的 `help`(带圈问号),尺寸随主题图标档(`--ui-font-icon`);
229
+ * - 图标**排在星号之后**(`服务名称*ⓘ`)—— 与「星号跟着字段名走」同一口径;
230
+ * - 只给 `tip`(无 `label`、非必填)时**同样保留 label 列**(与「只给 required」同一判据),
231
+ * 否则图标没有列可落、静默消失;
232
+ * - 气泡用 `Tooltip` 的 **hover 档**(零 JS):不吃点击、不抢焦点;代价是 hover 档不进浏览器
233
+ * 顶层浮层,会被祖先 `overflow` 裁切(弹窗 / 抽屉里贴着裁剪边的字段可能看不全)。
234
+ *
208
235
  * ## 键盘事件(`onKeydown` 转发到根节点)
209
236
  *
210
237
  * `onKeydown` 是本件的**声明式入口**(缺省不绑):传进来的处理器绑在根节点上 ——
@@ -238,7 +265,7 @@ export interface FormItemProps {
238
265
  * <Form.Item label="编码" required error={errors.code} hint="保存后不可修改">
239
266
  * <Input modelValue={form.code} onChange={(v) => (form.code = v)} />
240
267
  * </Form.Item>
241
- * <Form.Item label="缓存策略" controlWidth={360}>
268
+ * <Form.Item label="缓存策略" controlWidth={360} tip="命中即不请求后端">
242
269
  * <Select options={cacheOptions} modelValue={form.cache} onChange={…} />
243
270
  * </Form.Item>
244
271
  * <Form.Item label="过滤条件" fullWidth>
@@ -268,6 +295,11 @@ export declare const FormItem: import("vue").DefineComponent<import("vue").Extra
268
295
  type: StringConstructor;
269
296
  default: string;
270
297
  };
298
+ /** label 旁的提示文案(问号图标 + 悬停气泡);口径见 `FormItemProps.tip` */
299
+ tip: {
300
+ type: StringConstructor;
301
+ default: string;
302
+ };
271
303
  labelWidth: {
272
304
  type: NumberConstructor;
273
305
  default: undefined;
@@ -316,6 +348,11 @@ export declare const FormItem: import("vue").DefineComponent<import("vue").Extra
316
348
  type: StringConstructor;
317
349
  default: string;
318
350
  };
351
+ /** label 旁的提示文案(问号图标 + 悬停气泡);口径见 `FormItemProps.tip` */
352
+ tip: {
353
+ type: StringConstructor;
354
+ default: string;
355
+ };
319
356
  labelWidth: {
320
357
  type: NumberConstructor;
321
358
  default: undefined;
@@ -350,6 +387,7 @@ export declare const FormItem: import("vue").DefineComponent<import("vue").Extra
350
387
  align: FormItemAlign;
351
388
  readonly: boolean;
352
389
  hint: string;
390
+ tip: string;
353
391
  labelWidth: number;
354
392
  labelAlign: FormLabelAlign;
355
393
  controlWidth: number;
@@ -730,6 +768,11 @@ export declare const Form: {
730
768
  type: StringConstructor;
731
769
  default: string;
732
770
  };
771
+ /** label 旁的提示文案(问号图标 + 悬停气泡);口径见 `FormItemProps.tip` */
772
+ tip: {
773
+ type: StringConstructor;
774
+ default: string;
775
+ };
733
776
  labelWidth: {
734
777
  type: NumberConstructor;
735
778
  default: undefined;
@@ -778,6 +821,11 @@ export declare const Form: {
778
821
  type: StringConstructor;
779
822
  default: string;
780
823
  };
824
+ /** label 旁的提示文案(问号图标 + 悬停气泡);口径见 `FormItemProps.tip` */
825
+ tip: {
826
+ type: StringConstructor;
827
+ default: string;
828
+ };
781
829
  labelWidth: {
782
830
  type: NumberConstructor;
783
831
  default: undefined;
@@ -812,6 +860,7 @@ export declare const Form: {
812
860
  align: FormItemAlign;
813
861
  readonly: boolean;
814
862
  hint: string;
863
+ tip: string;
815
864
  labelWidth: number;
816
865
  labelAlign: FormLabelAlign;
817
866
  controlWidth: number;
@@ -1,5 +1,7 @@
1
1
  import { defineComponent, inject, watch, createVNode, createTextVNode, provide, toRef, ref, computed, isVNode } from "vue";
2
+ import { Icon } from "@manohub/icon";
2
3
  import { mergePartStyle } from "../part-padding.js";
4
+ import { Tooltip } from "../tooltip/index.js";
3
5
  const FORM_LABEL_WIDTH = /* @__PURE__ */ Symbol("mh-form-label-width");
4
6
  const FORM_CONTROL_WIDTH = /* @__PURE__ */ Symbol("mh-form-control-width");
5
7
  const FORM_LABEL_ALIGN = /* @__PURE__ */ Symbol("mh-form-label-align");
@@ -29,6 +31,11 @@ const FormItem = /* @__PURE__ */ defineComponent({
29
31
  type: String,
30
32
  default: ""
31
33
  },
34
+ /** label 旁的提示文案(问号图标 + 悬停气泡);口径见 `FormItemProps.tip` */
35
+ tip: {
36
+ type: String,
37
+ default: ""
38
+ },
32
39
  labelWidth: {
33
40
  type: Number,
34
41
  default: void 0
@@ -85,7 +92,7 @@ const FormItem = /* @__PURE__ */ defineComponent({
85
92
  const styles = [attrs.style, labelWidth ? {
86
93
  "--mh-form-label-width": `${labelWidth}px`
87
94
  } : null].filter(Boolean);
88
- const showLabel = !!props.label || props.required;
95
+ const showLabel = !!props.label || props.required || !!props.tip;
89
96
  return createVNode("div", {
90
97
  "class": [
91
98
  "mh-form-item",
@@ -109,7 +116,18 @@ const FormItem = /* @__PURE__ */ defineComponent({
109
116
  "class": "mh-form-item__label-text"
110
117
  }, [props.label]), props.required ? createVNode("span", {
111
118
  "class": "mh-form-item__required"
112
- }, [createTextVNode("*")]) : null]) : null, createVNode("div", {
119
+ }, [createTextVNode("*")]) : null, props.tip ? createVNode(Tooltip, {
120
+ "class": "mh-form-item__tip",
121
+ "content": props.tip
122
+ }, {
123
+ default: () => [createVNode("button", {
124
+ "type": "button",
125
+ "class": "mh-form-item__tip-btn",
126
+ "aria-label": props.tip
127
+ }, [createVNode(Icon, {
128
+ "name": "help"
129
+ }, null)])]
130
+ }) : null]) : null, createVNode("div", {
113
131
  "class": "mh-form-item__control",
114
132
  "style": controlWidth ? {
115
133
  maxWidth: `${controlWidth}px`
@@ -168,6 +168,7 @@ const Input = /* @__PURE__ */ defineComponent({
168
168
  if (props.modelValue !== void 0 && props.modelValue !== el.value) el.value = props.modelValue;
169
169
  };
170
170
  const onKeydown = (event) => {
171
+ if (event.isComposing) return;
171
172
  if (event.key === "Enter") props.onEnter?.(current.value ?? "");
172
173
  if (event.key === "Delete" && props.clearable) commit("");
173
174
  };
@@ -27,6 +27,11 @@ export type NavVariant = 'tree' | 'list';
27
27
  * `renderActions` 直接透传给 `Tree`:默认隐藏、行悬浮或行内获得焦点时显形、点击不冒泡(§4.16)。
28
28
  * 应用侧只给节点,不要自己写显隐 CSS,也不要把它塞进 `renderNode`(那是常驻附加内容)。
29
29
  *
30
+ * ## 图标
31
+ *
32
+ * 导航项图标走 `renderIcon`(标签前的一位图标,口径同 `Tree.renderIcon`)—— **不要**用
33
+ * `renderLabel` 拼「图标 + 文字」:标签槽承担省略号与行内对齐,图标进去会被裁掉,也不成列。
34
+ *
30
35
  * @example 分类导航树(选中驱动右侧 + 行尾管理操作)
31
36
  * <Nav variant="tree" nodes={categoryNodes.value} rowKey="id" labelKey="name" width={220}
32
37
  * selected={categoryId.value} onSelect={(node) => (categoryId.value = node.id)}
@@ -89,6 +94,16 @@ export declare const Nav: import("vue").DefineComponent<import("vue").ExtractPro
89
94
  type: PropType<(node: TreeNode) => unknown>;
90
95
  default: undefined;
91
96
  };
97
+ /** 导航项图标(标签前的一位图标):见 `Tree.renderIcon`(侧栏图标走这一位,不要塞进 `renderLabel`) */
98
+ renderIcon: {
99
+ type: PropType<(node: TreeNode) => unknown>;
100
+ default: undefined;
101
+ };
102
+ /** 展开 / 收起箭头字形:见 `Tree.renderCaret`(`list` 态箭头位被隐藏,传了也不显形) */
103
+ renderCaret: {
104
+ type: PropType<(node: TreeNode, expanded: boolean) => unknown>;
105
+ default: undefined;
106
+ };
92
107
  /** 侧栏标题(可选;不传则只有导航体) */
93
108
  title: {
94
109
  type: StringConstructor;
@@ -206,6 +221,16 @@ export declare const Nav: import("vue").DefineComponent<import("vue").ExtractPro
206
221
  type: PropType<(node: TreeNode) => unknown>;
207
222
  default: undefined;
208
223
  };
224
+ /** 导航项图标(标签前的一位图标):见 `Tree.renderIcon`(侧栏图标走这一位,不要塞进 `renderLabel`) */
225
+ renderIcon: {
226
+ type: PropType<(node: TreeNode) => unknown>;
227
+ default: undefined;
228
+ };
229
+ /** 展开 / 收起箭头字形:见 `Tree.renderCaret`(`list` 态箭头位被隐藏,传了也不显形) */
230
+ renderCaret: {
231
+ type: PropType<(node: TreeNode, expanded: boolean) => unknown>;
232
+ default: undefined;
233
+ };
209
234
  /** 侧栏标题(可选;不传则只有导航体) */
210
235
  title: {
211
236
  type: StringConstructor;
@@ -285,6 +310,8 @@ export declare const Nav: import("vue").DefineComponent<import("vue").ExtractPro
285
310
  onErrorAction: () => void;
286
311
  labelKey: string;
287
312
  childrenKey: string;
313
+ renderCaret: (node: TreeNode, expanded: boolean) => unknown;
314
+ renderIcon: (node: TreeNode) => unknown;
288
315
  renderActions: (node: TreeNode) => unknown;
289
316
  expandedKeys: string[];
290
317
  defaultExpandedKeys: string[];
@@ -58,6 +58,16 @@ const Nav = /* @__PURE__ */ defineComponent({
58
58
  type: Function,
59
59
  default: void 0
60
60
  },
61
+ /** 导航项图标(标签前的一位图标):见 `Tree.renderIcon`(侧栏图标走这一位,不要塞进 `renderLabel`) */
62
+ renderIcon: {
63
+ type: Function,
64
+ default: void 0
65
+ },
66
+ /** 展开 / 收起箭头字形:见 `Tree.renderCaret`(`list` 态箭头位被隐藏,传了也不显形) */
67
+ renderCaret: {
68
+ type: Function,
69
+ default: void 0
70
+ },
61
71
  /** 侧栏标题(可选;不传则只有导航体) */
62
72
  title: {
63
73
  type: String,
@@ -146,6 +156,8 @@ const Nav = /* @__PURE__ */ defineComponent({
146
156
  "defaultExpandedKeys": props.defaultExpandedKeys,
147
157
  "onExpandChange": props.onExpandChange,
148
158
  "renderActions": props.renderActions,
159
+ "renderIcon": props.renderIcon,
160
+ "renderCaret": props.renderCaret,
149
161
  "size": props.size,
150
162
  "loading": props.loading,
151
163
  "empty": props.empty,
@@ -143,8 +143,8 @@ export declare const Notice: import("vue").DefineComponent<import("vue").Extract
143
143
  tone: NoticeTone;
144
144
  description: string;
145
145
  action: string;
146
- closeText: string;
147
146
  onAction: () => void;
147
+ closeText: string;
148
148
  }, SlotsType<{
149
149
  default?: () => VNodeChild;
150
150
  }>, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
@@ -16,7 +16,7 @@ export type NotificationTone = 'info' | 'success' | 'warning' | 'error' | 'submi
16
16
  *
17
17
  * - 图标:`icon` 布尔(按 `tone` 映射)或直接给图标名;
18
18
  * - 头像:给 `avatar` 插槽 → 原样渲染(推荐塞 `<Avatar>`);给 `avatar` 字符串 → 渲染字标兜底盒;
19
- * - 标签:`tags` 插槽里放标签族的 `<Tag shape="status" tone="error">`;
19
+ * - 标签:`tags` 插槽里放标记族(只读 `<Badge shape="soft" tone="error">`,要可关闭用 `<Tag>`);
20
20
  * - 操作:`actions` 插槽里放按钮族的 `<Button size="sm" variant="primary">`。
21
21
  *
22
22
  * 关闭是**受控**的:本件只回调 `onClose`,不自己隐藏(可见性归消费方,避免「点了没反应」与
@@ -27,7 +27,7 @@ export type NotificationTone = 'info' | 'success' | 'warning' | 'error' | 'submi
27
27
  *
28
28
  * @example 图标 + 自动消失 + 头像 + 标签
29
29
  * <Notification icon duration={4000} tone="info" title="审批通知" avatar="张" @close="dismiss">
30
- * <Tag shape="status" tone="error">逾期</Tag>
30
+ * <Badge shape="soft" tone="error">逾期</Badge>
31
31
  * {{ actions: () => <Button size="sm" variant="primary">去处理</Button> }}
32
32
  * </Notification>
33
33
  */
@@ -35,6 +35,15 @@
35
35
  * **面板有高度上限**:`--ui-select-panel-max-height`(默认 320px,钉在 `select.css` 的面板根上)——
36
36
  * 超出即面板内容区(`.mh-select__body`)滚动、搜索行常驻;列表与 `SelectTree` 两形态共用这一份。
37
37
  *
38
+ * ## 键盘与高亮(焦点**留在触发器上**)
39
+ *
40
+ * 面板里的选项是 `div[role='option']`、**不是 button**、也不可聚焦:高亮位由本件持有
41
+ * (可激活项里的格号),列表体据此下发 `data-active`,触发器上的 `aria-activedescendant`
42
+ * 指到那一行的 id —— 原生 popover 下的「虚拟焦点」标准表达。两条入口共用同一份模型:
43
+ * 触发器(`↑` / `↓` 开面板并平移、`Enter` / `Space` 提交高亮项)与面板内的搜索行
44
+ * (`↑` / `↓` / `Enter` 冒泡到面板)。可激活项 = 摊平 + 跳过禁用项 + (可创建时)末尾那一格
45
+ * 「创建 xxx」行,顺序由 `activatableOptions()` 一处给。
46
+ *
38
47
  * ## 状态三源等价
39
48
  *
40
49
  * 选中行 = `[data-state='selected']`(组件下发)/ 原生 `input:checked`(静态页零 JS 路径)/
@@ -1,8 +1,8 @@
1
- import { defineComponent, ref, computed, createVNode, Fragment } from "vue";
1
+ import { defineComponent, ref, computed, watch, createVNode, Fragment } from "vue";
2
2
  import { SelectList } from "./select-list.js";
3
3
  import { SelectPanel } from "./select-panel.js";
4
4
  import { SelectTrigger } from "./select-trigger.js";
5
- import { createSelectPanelId, isGroup, createTriggerKeydown, showPanel, hidePanel } from "./select-shared.js";
5
+ import { createSelectPanelId, isGroup, activatableOptions, activatableCount, createTriggerKeydown, optionDomId, showPanel, hidePanel } from "./select-shared.js";
6
6
  const Select = /* @__PURE__ */ defineComponent({
7
7
  name: "Select",
8
8
  inheritAttrs: false,
@@ -167,27 +167,68 @@ const Select = /* @__PURE__ */ defineComponent({
167
167
  keyword.value = "";
168
168
  };
169
169
  const clear = () => commit(props.multiple ? [] : "");
170
- const optionEls = () => panelRef.value ? [...panelRef.value.querySelectorAll(".mh-select__option")].filter((el) => !el.disabled) : [];
171
- const moveFocus = (step) => {
172
- const items = optionEls();
173
- if (!items.length) return;
174
- const at = items.indexOf(document.activeElement);
175
- const next = at < 0 ? step > 0 ? 0 : items.length - 1 : (at + step + items.length) % items.length;
176
- items[next].focus();
170
+ const activatable = computed(() => activatableOptions(visibleOptions.value));
171
+ const slotCount = computed(() => activatableCount(visibleOptions.value, creatable.value));
172
+ const activeIndex = ref(void 0);
173
+ const activeEl = () => panelRef.value?.querySelector('[data-active="true"]') ?? null;
174
+ const scrollActiveIntoView = () => {
175
+ requestAnimationFrame(() => activeEl()?.scrollIntoView?.({
176
+ block: "nearest"
177
+ }));
178
+ };
179
+ const moveActive = (direction) => {
180
+ const count = slotCount.value;
181
+ if (!count) return;
182
+ const at = activeIndex.value ?? -1;
183
+ activeIndex.value = at < 0 ? direction > 0 ? 0 : count - 1 : (at + direction + count) % count;
184
+ scrollActiveIntoView();
185
+ };
186
+ const commitActive = () => {
187
+ const slot = activeIndex.value;
188
+ if (slot === void 0) return false;
189
+ if (creatable.value && slot === activatable.value.length) {
190
+ createFromKeyword();
191
+ return true;
192
+ }
193
+ const option = activatable.value[slot];
194
+ if (!option) return false;
195
+ pick(option);
196
+ if (!props.multiple) activeIndex.value = void 0;
197
+ return true;
198
+ };
199
+ watch(keyword, () => {
200
+ activeIndex.value = void 0;
201
+ });
202
+ const panelOpen = ref(false);
203
+ const isPanelOpen = () => panelOpen.value;
204
+ const openPanel = () => {
205
+ if (!panelOpen.value) activeIndex.value = void 0;
206
+ panelOpen.value = true;
207
+ showPanel(panelRef.value);
208
+ };
209
+ const onPanelToggle = (event) => {
210
+ if (event.newState === "open") return;
211
+ panelOpen.value = false;
212
+ activeIndex.value = void 0;
177
213
  };
178
214
  const onTriggerKeydown = createTriggerKeydown({
179
215
  clearable: props.clearable,
180
216
  disabled: props.disabled,
181
217
  clear,
182
- openPanel: () => showPanel(panelRef.value),
183
- focusFirst: (direction) => moveFocus(direction)
218
+ openPanel,
219
+ isPanelOpen,
220
+ moveActive,
221
+ commitActive
184
222
  });
185
223
  const onPanelKeydown = (event) => {
186
224
  if (event.key === "ArrowDown" || event.key === "ArrowUp") {
187
225
  event.preventDefault();
188
- moveFocus(event.key === "ArrowDown" ? 1 : -1);
226
+ moveActive(event.key === "ArrowDown" ? 1 : -1);
227
+ return;
189
228
  }
229
+ if (event.key === "Enter" && commitActive()) event.preventDefault();
190
230
  };
231
+ const activeDescendant = computed(() => activeIndex.value === void 0 ? void 0 : optionDomId(panelId, activeIndex.value));
191
232
  return () => {
192
233
  const hasValue = selectedValues.value.length > 0;
193
234
  return createVNode("div", {
@@ -203,6 +244,7 @@ const Select = /* @__PURE__ */ defineComponent({
203
244
  "multiple": props.multiple,
204
245
  "placeholder": props.placeholder,
205
246
  "label": selectedOptions.value[0]?.label,
247
+ "activeDescendant": activeDescendant.value,
206
248
  "onKeydown": onTriggerKeydown,
207
249
  "onClear": clear,
208
250
  "renderValue": props.multiple ? () => createVNode(Fragment, null, [visibleTags.value.map((option) => createVNode("span", {
@@ -215,6 +257,7 @@ const Select = /* @__PURE__ */ defineComponent({
215
257
  "id": panelId,
216
258
  "elementRef": panelRef,
217
259
  "searchable": props.searchable,
260
+ "onToggle": onPanelToggle,
218
261
  "keyword": keyword.value,
219
262
  "onKeyword": (value) => {
220
263
  keyword.value = value;
@@ -225,6 +268,8 @@ const Select = /* @__PURE__ */ defineComponent({
225
268
  default: () => createVNode(SelectList, {
226
269
  "options": visibleOptions.value,
227
270
  "selectedValues": selectedValues.value,
271
+ "activeIndex": activeIndex.value,
272
+ "idBase": panelId,
228
273
  "loading": props.loading,
229
274
  "emptyText": props.emptyText,
230
275
  "creatable": creatable.value,