@manohub/ui 1.0.6 → 1.0.8

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.
@@ -111,9 +111,9 @@ export declare const CodeView: import("vue").DefineComponent<import("vue").Extra
111
111
  plain: boolean;
112
112
  code: string;
113
113
  onCopy: (ok: boolean) => void;
114
+ maxHeight: string | number;
114
115
  language: string;
115
116
  copyable: boolean;
116
- maxHeight: string | number;
117
117
  }, SlotsType<{
118
118
  default?: () => VNodeChild;
119
119
  }>, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
@@ -260,8 +260,8 @@ export declare const CodeEditor: import("vue").DefineComponent<import("vue").Ext
260
260
  modelValue: string;
261
261
  readonly: boolean;
262
262
  autoSize: boolean | CodeEditorAutoSize;
263
- language: string;
264
263
  maxHeight: string | number;
264
+ language: string;
265
265
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
266
266
  /** `CodeBlock` 的 props:**两形态的并集** + 切换开关(按 `editable` 分两组,见各条说明) */
267
267
  export interface CodeBlockProps extends CodeViewProps, CodeEditorProps {
@@ -366,9 +366,9 @@ export declare const CodeBlock: {
366
366
  modelValue: string;
367
367
  readonly: boolean;
368
368
  autoSize: boolean | CodeEditorAutoSize;
369
+ maxHeight: string | number;
369
370
  language: string;
370
371
  copyable: boolean;
371
- maxHeight: string | number;
372
372
  editable: boolean;
373
373
  }, true, {}, SlotsType<{
374
374
  default?: () => VNodeChild;
@@ -464,9 +464,9 @@ export declare const CodeBlock: {
464
464
  modelValue: string;
465
465
  readonly: boolean;
466
466
  autoSize: boolean | CodeEditorAutoSize;
467
+ maxHeight: string | number;
467
468
  language: string;
468
469
  copyable: boolean;
469
- maxHeight: string | number;
470
470
  editable: boolean;
471
471
  }>;
472
472
  __isFragment?: never;
@@ -559,9 +559,9 @@ export declare const CodeBlock: {
559
559
  modelValue: string;
560
560
  readonly: boolean;
561
561
  autoSize: boolean | CodeEditorAutoSize;
562
+ maxHeight: string | number;
562
563
  language: string;
563
564
  copyable: boolean;
564
- maxHeight: string | number;
565
565
  editable: boolean;
566
566
  }, {}, string, SlotsType<{
567
567
  default?: () => VNodeChild;
@@ -638,9 +638,9 @@ export declare const CodeBlock: {
638
638
  plain: boolean;
639
639
  code: string;
640
640
  onCopy: (ok: boolean) => void;
641
+ maxHeight: string | number;
641
642
  language: string;
642
643
  copyable: boolean;
643
- maxHeight: string | number;
644
644
  }, SlotsType<{
645
645
  default?: () => VNodeChild;
646
646
  }>, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
@@ -739,8 +739,8 @@ export declare const CodeBlock: {
739
739
  modelValue: string;
740
740
  readonly: boolean;
741
741
  autoSize: boolean | CodeEditorAutoSize;
742
- language: string;
743
742
  maxHeight: string | number;
743
+ language: string;
744
744
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
745
745
  };
746
746
  /** 含成员的 `CodeBlock` 类型(与 `CodeBlockCompoundType` 同义,两个名字都为对齐本仓既有的 `XxxCompound` 口径) */
@@ -1,4 +1,4 @@
1
- import { type PropType, type SlotsType, type VNodeChild } from 'vue';
1
+ import { type HTMLAttributes, type PropType, type SlotsType, type VNodeChild } from 'vue';
2
2
  import type { ControlSize } from '../control-size';
3
3
  /** 校验状态(`error` 红、`warning` 黄;缺省无校验态) */
4
4
  export type InputStatus = 'error' | 'warning';
@@ -26,10 +26,11 @@ export interface TextareaAutoSize {
26
26
  * `class` / `style` → 表壳 `.mh-input__wrap`(整件的宽度与显式锚);**其余 attrs 一律透传到控件**
27
27
  * (`id` / `name` / `aria-*` / `data-*` / `title` / 原生事件)—— 壳只是排版容器,属性落它上面等于丢掉
28
28
  * —— 见 `forwardAttrs`;触发器形态另有 `controlClass` / `controlAttrs` 两个**专口子**(同名键以它为准)。
29
+ * 这组属性在**类型面**同步放开(两形态共认的键):`onClick` / `title` / `data-*` / `aria-*` 直接写在组件上即可。
29
30
  *
30
31
  * 用法见 `docs/demos/input/`(每个维度一份可运行样例;文档页 `docs/components/input.md`)。
31
32
  */
32
- export declare const Input: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
33
+ declare const InputBase: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
33
34
  /** 受控值 */
34
35
  modelValue: {
35
36
  type: StringConstructor;
@@ -299,6 +300,16 @@ export declare const Input: import("vue").DefineComponent<import("vue").ExtractP
299
300
  prefix?: () => VNodeChild;
300
301
  suffix?: () => VNodeChild;
301
302
  }>, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
303
+ /**
304
+ * 控件元素上的原生属性 / 事件(`HTMLAttributes` 即输入框与触发器共有的那张基表):
305
+ * 本件不登记它们,运行时按 attrs 透传到控件,这里只把类型面同步放开。
306
+ */
307
+ type InputControlAttrs = Omit<HTMLAttributes, keyof InstanceType<typeof InputBase>['$props']>;
308
+ export declare const Input: typeof InputBase & {
309
+ new (): {
310
+ $props: InstanceType<typeof InputBase>["$props"] & InputControlAttrs;
311
+ };
312
+ };
302
313
  export interface TextareaProps {
303
314
  /** 受控值 */
304
315
  modelValue?: string;
@@ -933,7 +944,9 @@ export declare const InputCompound: {
933
944
  default?: () => VNodeChild;
934
945
  prefix?: () => VNodeChild;
935
946
  suffix?: () => VNodeChild;
936
- }>, import("vue").GlobalComponents, import("vue").GlobalDirectives, string, import("vue").ComponentProvideOptions> & import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps & {
947
+ }>, import("vue").GlobalComponents, import("vue").GlobalDirectives, string, import("vue").ComponentProvideOptions> & import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps & (new () => {
948
+ $props: InstanceType<typeof InputBase>["$props"] & InputControlAttrs;
949
+ }) & {
937
950
  Textarea: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
938
951
  modelValue: {
939
952
  type: StringConstructor;
@@ -1,7 +1,7 @@
1
1
  import { defineComponent, ref, computed, createVNode, mergeProps, createTextVNode } from "vue";
2
2
  import { Icon } from "@manohub/icon";
3
3
  import { borderedClassOf } from "./bordered.js";
4
- const Input = /* @__PURE__ */ defineComponent({
4
+ const InputBase = /* @__PURE__ */ defineComponent({
5
5
  name: "Input",
6
6
  inheritAttrs: false,
7
7
  props: {
@@ -242,6 +242,7 @@ const Input = /* @__PURE__ */ defineComponent({
242
242
  }, [current.value?.length ?? 0, createTextVNode("/"), props.maxlength]) : null]);
243
243
  }
244
244
  });
245
+ const Input = InputBase;
245
246
  const Textarea = /* @__PURE__ */ defineComponent({
246
247
  name: "Textarea",
247
248
  inheritAttrs: false,
@@ -43,6 +43,8 @@ export interface TableColumn {
43
43
  sortable?: boolean;
44
44
  }
45
45
  export type TableSortOrder = 'asc' | 'desc' | null;
46
+ /** 满高档取值:不传 / `false` = 高度由内容撑起;`true` / `'parent'` = 占满父级;`number` = 表体滚动上限(px) */
47
+ export type TableFill = boolean | 'parent' | number;
46
48
  /** 行数据:至少能用 `rowKey` 取到稳定标识 */
47
49
  export type TableRow = Record<string, unknown>;
48
50
  /** 行级事件(`onRow` 的返回):给整行挂点击与类,省掉「每列都写一次 onClick」 */
@@ -73,23 +75,15 @@ export interface TableCellSlotScope {
73
75
  }
74
76
  /** 行级可勾选判定(`selectable` 的函数形态):返回 `false` 的行渲染**禁用态**勾选框,且不进表头全选的增删范围;签名与 `onRow` / `rowClass` 一致(都给 `index`)。 */
75
77
  export type TableRowSelectable = (row: TableRow, index: number) => boolean;
76
- /** 分页档:`false` 不渲染;给对象就渲染分页条(**本件不切片,`data` 是当前页数据**) */
77
- export interface TablePagination {
78
- total: number;
79
- current?: number;
80
- pageSize?: number;
81
- pageSizeOptions?: number[];
82
- onChange?: (page: number, pageSize: number) => void;
83
- }
84
78
  /**
85
- * 表格(**自绘**;列 × 数据的表格骨架 + 选择列 + 展开行 + 排序事件 + 分页 + 三态)。
79
+ * 表格(**自绘**;列 × 数据的表格骨架 + 选择列 + 展开行 + 排序事件 + 三态)。
86
80
  *
87
81
  * ## 这个组件负责什么、不负责什么(都是刻意的)
88
82
  *
89
83
  * **负责**:把 `columns` × `data` 渲成表格;列对齐 / 宽度 / 省略 / 必填;行选中(勾选列复用本包的
90
84
  * `Checkbox` 组件 —— 它内部才是真实 `<input type="checkbox">`,故面上的
91
85
  * `tr:has(.mh-table__col-check input:checked)` 零 JS 路径仍成立;选择器**限定在勾选列内**,否则功能列的
92
- * `Switch` 也会把整行点亮);展开行;排序按钮的**状态与事件**;加载态与空态;分页条的排布与事件透传;
86
+ * `Switch` 也会把整行点亮);展开行;排序按钮的**状态与事件**;加载态与空态;
93
87
  * `rowClass` / `onRow` 让调用方标分组行、禁用行与挂行级点击。
94
88
  *
95
89
  * **不负责**:
@@ -97,8 +91,7 @@ export interface TablePagination {
97
91
  * - **排序算法归调用方**(列宽拖拽同理):规范只给了 `↕` 字形与拖拽线两处视觉,而 `↕` 是文字符号(本包禁
98
92
  * 拿文字符号冒充图标),故朝向改用现成字形 `chevron-up` / `chevron-down` 表达 —— 本件发 `sort` 事件、
99
93
  * 画态,真正排序 / 改列宽由调用方决定(它有数据、也知道业务口径);
100
- * - **分页切片**:`pagination` 只渲染分页条并透传页码事件,**`data` 始终是当前页数据** —— 组件自己切片会
101
- * 让「服务端分页」与「客户端分页」两套口径藏在同一个 prop 里;
94
+ * - **分页条**:本件不内建(`data` 始终是给它的那页数据),页码状态、切片与分页条的摆放都归调用方;
102
95
  * - **错误态**:归 `QueryState`(表格只管列数据)。
103
96
  */
104
97
  export declare const Table: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
@@ -172,9 +165,19 @@ export declare const Table: import("vue").DefineComponent<import("vue").ExtractP
172
165
  type: BooleanConstructor;
173
166
  default: boolean;
174
167
  };
175
- /** 外框交给包裹层(满高表格用) */
168
+ /**
169
+ * 满高档(高度与滚动归属;详见 `docs/components/table.md` 的「满高与滚动」):
170
+ *
171
+ * - 不传 / `false` —— 高度由内容撑起(单表结构,与不带本 prop 时完全一致);
172
+ * - `true` / `'parent'` —— 占满父级高度(`height: 100%`),**表体滚、表头固定在表体上方**,
173
+ * 外框与圆角归包裹层;父级需可确定高度(`auto` 父级下 `100%` 解析不出来);
174
+ * - `number` —— 表体滚动上限 = 该像素值,**父级高度不确定时的显式兜底档**
175
+ * (CSS 无法探测父级是否定高,故兜底只能由调用方声明)。
176
+ *
177
+ * 满高档一律按 `table-layout: fixed` 渲染(表头与表体是两个表格盒,`auto` 各自算宽必然错列)。
178
+ */
176
179
  fill: {
177
- type: BooleanConstructor;
180
+ type: PropType<TableFill>;
178
181
  default: boolean;
179
182
  };
180
183
  /**
@@ -190,10 +193,6 @@ export declare const Table: import("vue").DefineComponent<import("vue").ExtractP
190
193
  type: PropType<"auto" | "fixed">;
191
194
  default: string;
192
195
  };
193
- stickyHead: {
194
- type: BooleanConstructor;
195
- default: boolean;
196
- };
197
196
  stickyCol: {
198
197
  type: BooleanConstructor;
199
198
  default: boolean;
@@ -252,11 +251,6 @@ export declare const Table: import("vue").DefineComponent<import("vue").ExtractP
252
251
  type: PropType<(keys: Array<string | number>) => void>;
253
252
  default: undefined;
254
253
  };
255
- /** 分页档:`false` 不渲染底部分页条 */
256
- pagination: {
257
- type: PropType<false | TablePagination>;
258
- default: boolean;
259
- };
260
254
  }>, () => import("vue/jsx-runtime").JSX.Element, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
261
255
  /** 选中行 key 列表(`v-model`) */
262
256
  'update:modelValue': (keys: Array<string | number>) => boolean;
@@ -338,9 +332,19 @@ export declare const Table: import("vue").DefineComponent<import("vue").ExtractP
338
332
  type: BooleanConstructor;
339
333
  default: boolean;
340
334
  };
341
- /** 外框交给包裹层(满高表格用) */
335
+ /**
336
+ * 满高档(高度与滚动归属;详见 `docs/components/table.md` 的「满高与滚动」):
337
+ *
338
+ * - 不传 / `false` —— 高度由内容撑起(单表结构,与不带本 prop 时完全一致);
339
+ * - `true` / `'parent'` —— 占满父级高度(`height: 100%`),**表体滚、表头固定在表体上方**,
340
+ * 外框与圆角归包裹层;父级需可确定高度(`auto` 父级下 `100%` 解析不出来);
341
+ * - `number` —— 表体滚动上限 = 该像素值,**父级高度不确定时的显式兜底档**
342
+ * (CSS 无法探测父级是否定高,故兜底只能由调用方声明)。
343
+ *
344
+ * 满高档一律按 `table-layout: fixed` 渲染(表头与表体是两个表格盒,`auto` 各自算宽必然错列)。
345
+ */
342
346
  fill: {
343
- type: BooleanConstructor;
347
+ type: PropType<TableFill>;
344
348
  default: boolean;
345
349
  };
346
350
  /**
@@ -356,10 +360,6 @@ export declare const Table: import("vue").DefineComponent<import("vue").ExtractP
356
360
  type: PropType<"auto" | "fixed">;
357
361
  default: string;
358
362
  };
359
- stickyHead: {
360
- type: BooleanConstructor;
361
- default: boolean;
362
- };
363
363
  stickyCol: {
364
364
  type: BooleanConstructor;
365
365
  default: boolean;
@@ -418,11 +418,6 @@ export declare const Table: import("vue").DefineComponent<import("vue").ExtractP
418
418
  type: PropType<(keys: Array<string | number>) => void>;
419
419
  default: undefined;
420
420
  };
421
- /** 分页档:`false` 不渲染底部分页条 */
422
- pagination: {
423
- type: PropType<false | TablePagination>;
424
- default: boolean;
425
- };
426
421
  }>> & Readonly<{
427
422
  onSort?: ((payload: {
428
423
  key: string;
@@ -431,7 +426,7 @@ export declare const Table: import("vue").DefineComponent<import("vue").ExtractP
431
426
  onChange?: ((keys: (string | number)[]) => any) | undefined;
432
427
  "onUpdate:modelValue"?: ((keys: (string | number)[]) => any) | undefined;
433
428
  }>, {
434
- fill: boolean;
429
+ fill: TableFill;
435
430
  layout: "fixed" | "auto";
436
431
  size: ControlSize;
437
432
  compact: boolean;
@@ -447,7 +442,6 @@ export declare const Table: import("vue").DefineComponent<import("vue").ExtractP
447
442
  selectable: boolean | TableRowSelectable;
448
443
  striped: boolean;
449
444
  onCellChange: (row: TableRow, key: string, value: unknown, index: number) => void;
450
- stickyHead: boolean;
451
445
  stickyCol: boolean;
452
446
  showHeader: boolean;
453
447
  sortKey: string;
@@ -455,19 +449,15 @@ export declare const Table: import("vue").DefineComponent<import("vue").ExtractP
455
449
  rowClass: (row: TableRow, index: number) => string;
456
450
  onRow: (row: TableRow, index: number) => TableRowProps;
457
451
  expandable: (row: TableRow, index: number) => VNodeChild;
458
- pagination: false | TablePagination;
459
452
  }, SlotsType<{
460
453
  /** 单元格插槽:`{ row, column, value, index, setValue }`(不给则直接输出原值)。**编辑格也走这里** —— 本件不替调用方渲染控件,只给值、给位置、给一个受控的回写口。 */
461
454
  cell?: (context: TableCellSlotScope) => VNodeChild;
462
- /** 行尾操作区:`{ row, index }`(面里用 `align: 'right'` 的列承载) */
463
- actions?: (context: {
464
- row: TableRow;
465
- index: number;
466
- }) => VNodeChild;
467
455
  /** 批量栏:选中非空时出现在表格上方(`{ selected }`) */
468
456
  batch?: (context: {
469
457
  selected: Array<string | number>;
470
458
  }) => VNodeChild;
471
459
  /** 空态自定义(插画 + 操作按钮);不给则渲染 `emptyText` 一行 */
472
460
  empty?: () => VNodeChild;
461
+ /** 合计行:渲染在滚动区之外(表格下方),容器与标签 / 数值子样式见 `.mh-table__summary*` */
462
+ summary?: () => VNodeChild;
473
463
  }>, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
@@ -1,7 +1,6 @@
1
- import { defineComponent, computed, ref, createVNode } from "vue";
1
+ import { defineComponent, computed, ref, onMounted, onUpdated, createVNode } from "vue";
2
2
  import { Icon } from "@manohub/icon";
3
3
  import { Checkbox } from "../checkbox/index.js";
4
- import { Pagination } from "../pagination/index.js";
5
4
  const Table = /* @__PURE__ */ defineComponent({
6
5
  name: "Table",
7
6
  inheritAttrs: false,
@@ -76,9 +75,19 @@ const Table = /* @__PURE__ */ defineComponent({
76
75
  type: Boolean,
77
76
  default: true
78
77
  },
79
- /** 外框交给包裹层(满高表格用) */
78
+ /**
79
+ * 满高档(高度与滚动归属;详见 `docs/components/table.md` 的「满高与滚动」):
80
+ *
81
+ * - 不传 / `false` —— 高度由内容撑起(单表结构,与不带本 prop 时完全一致);
82
+ * - `true` / `'parent'` —— 占满父级高度(`height: 100%`),**表体滚、表头固定在表体上方**,
83
+ * 外框与圆角归包裹层;父级需可确定高度(`auto` 父级下 `100%` 解析不出来);
84
+ * - `number` —— 表体滚动上限 = 该像素值,**父级高度不确定时的显式兜底档**
85
+ * (CSS 无法探测父级是否定高,故兜底只能由调用方声明)。
86
+ *
87
+ * 满高档一律按 `table-layout: fixed` 渲染(表头与表体是两个表格盒,`auto` 各自算宽必然错列)。
88
+ */
80
89
  fill: {
81
- type: Boolean,
90
+ type: [Boolean, String, Number],
82
91
  default: false
83
92
  },
84
93
  /**
@@ -94,10 +103,6 @@ const Table = /* @__PURE__ */ defineComponent({
94
103
  type: String,
95
104
  default: "auto"
96
105
  },
97
- stickyHead: {
98
- type: Boolean,
99
- default: false
100
- },
101
106
  stickyCol: {
102
107
  type: Boolean,
103
108
  default: false
@@ -155,11 +160,6 @@ const Table = /* @__PURE__ */ defineComponent({
155
160
  onExpandChange: {
156
161
  type: Function,
157
162
  default: void 0
158
- },
159
- /** 分页档:`false` 不渲染底部分页条 */
160
- pagination: {
161
- type: [Boolean, Object],
162
- default: false
163
163
  }
164
164
  },
165
165
  emits: {
@@ -242,120 +242,165 @@ const Table = /* @__PURE__ */ defineComponent({
242
242
  return "asc";
243
243
  };
244
244
  const sortGlyph = (key) => props.sortKey === key && props.sortOrder === "asc" ? "chevron-up" : "chevron-down";
245
- return () => {
246
- const tableClass = ["mh-table", resolvedSize.value === "sm" ? "mh-table--xs" : "", resolvedSize.value === "lg" ? "mh-table--lg" : "", props.bordered ? "" : "mh-table--borderless", props.fill ? "mh-table--fill" : "", props.layout === "fixed" ? "mh-table--fixed" : "", props.stickyHead ? "mh-table--sticky-head" : "", props.stickyCol ? "mh-table--sticky-col" : "", attrs.class].filter(Boolean).join(" ");
247
- const pager = typeof props.pagination === "object" ? props.pagination : null;
248
- return createVNode("div", {
249
- "class": "mh-table-wrapper",
250
- "style": attrs.style
251
- }, [slots.batch && selected.value.length > 0 ? createVNode("div", {
252
- "class": "mh-table__batch"
253
- }, [slots.batch({
254
- selected: selected.value
255
- })]) : null, createVNode("table", {
256
- "class": tableClass
257
- }, [colElements(), props.showHeader ? createVNode("thead", null, [createVNode("tr", null, [props.expandable ? createVNode("th", {
258
- "class": "mh-table__col-expand",
259
- "aria-label": "展开"
260
- }, null) : null, hasSelectColumn.value ? createVNode("th", {
261
- "class": "mh-table__col-check"
262
- }, [createVNode(Checkbox, {
263
- "aria-label": "全选",
264
- "modelValue": allSelected.value,
265
- "indeterminate": someSelected.value && !allSelected.value,
266
- "disabled": headerCheckboxDisabled.value,
267
- "onChange": toggleAll
268
- }, null)]) : null, props.columns.map((column) => createVNode("th", {
269
- "key": column.key,
270
- "class": [column.required ? "mh-table__cell-required" : "", column.align === "center" ? "mh-table__cell-center" : "", column.align === "right" ? "mh-table__col-actions" : ""].filter(Boolean).join(" ")
271
- }, [column.title, column.sortable ? createVNode("button", {
245
+ const renderHead = () => createVNode("thead", null, [createVNode("tr", null, [props.expandable ? createVNode("th", {
246
+ "class": "mh-table__col-expand",
247
+ "aria-label": "展开"
248
+ }, null) : null, hasSelectColumn.value ? createVNode("th", {
249
+ "class": "mh-table__col-check"
250
+ }, [createVNode(Checkbox, {
251
+ "aria-label": "全选",
252
+ "modelValue": allSelected.value,
253
+ "indeterminate": someSelected.value && !allSelected.value,
254
+ "disabled": headerCheckboxDisabled.value,
255
+ "onChange": toggleAll
256
+ }, null)]) : null, props.columns.map((column) => createVNode("th", {
257
+ "key": column.key,
258
+ "class": [column.required ? "mh-table__cell-required" : "", column.align === "center" ? "mh-table__cell-center" : "", column.align === "right" ? "mh-table__col-right" : ""].filter(Boolean).join(" ")
259
+ }, [column.title, column.sortable ? createVNode("button", {
260
+ "type": "button",
261
+ "class": ["mh-table__sort", props.sortKey === column.key && props.sortOrder ? "mh-table__sort--active" : ""].filter(Boolean).join(" "),
262
+ "aria-label": `按${column.title}排序`,
263
+ "onClick": () => emit("sort", {
264
+ key: column.key,
265
+ order: nextOrder(column.key)
266
+ })
267
+ }, [createVNode(Icon, {
268
+ "name": sortGlyph(column.key),
269
+ "class": ["mh-table__sort-icon", props.sortKey === column.key && props.sortOrder === "asc" ? "mh-table__sort-icon--asc" : "", props.sortKey === column.key && props.sortOrder === "desc" ? "mh-table__sort-icon--desc" : ""].filter(Boolean).join(" ")
270
+ }, null)]) : null]))])]);
271
+ const renderBody = () => createVNode("tbody", null, [isEmpty.value ? createVNode("tr", {
272
+ "class": "mh-table__empty-row"
273
+ }, [createVNode("td", {
274
+ "class": "mh-table__empty",
275
+ "colspan": columnCount.value
276
+ }, [slots.empty ? slots.empty() : props.emptyText])]) : null, props.data.map((row, index) => {
277
+ const key = row[props.rowKey];
278
+ const rowProps = props.onRow?.(row, index);
279
+ const isExpanded = expanded.value.includes(key);
280
+ const rowState = [props.striped && index % 2 === 1 ? "is-striped" : "", selected.value.includes(key) ? "is-selected" : "", rowProps?.onClick ? "is-clickable" : "", rowProps?.class ?? "", props.rowClass?.(row, index) ?? ""].filter(Boolean).join(" ");
281
+ return [createVNode("tr", {
282
+ "key": key,
283
+ "class": rowState,
284
+ "data-state": selected.value.includes(key) ? "selected" : "idle",
285
+ "onClick": rowProps?.onClick ? (event) => rowProps.onClick?.(row, index, event) : void 0
286
+ }, [props.expandable ? createVNode("td", {
287
+ "class": "mh-table__col-expand"
288
+ }, [createVNode("button", {
272
289
  "type": "button",
273
- "class": ["mh-table__sort", props.sortKey === column.key && props.sortOrder ? "mh-table__sort--active" : ""].filter(Boolean).join(" "),
274
- "aria-label": `按${column.title}排序`,
275
- "onClick": () => emit("sort", {
276
- key: column.key,
277
- order: nextOrder(column.key)
278
- })
290
+ "class": "mh-table__expand",
291
+ "aria-label": isExpanded ? "收起" : "展开",
292
+ "aria-expanded": isExpanded ? "true" : "false",
293
+ "onClick": (event) => {
294
+ event.stopPropagation();
295
+ toggleExpand(key);
296
+ }
279
297
  }, [createVNode(Icon, {
280
- "name": sortGlyph(column.key),
281
- "class": ["mh-table__sort-icon", props.sortKey === column.key && props.sortOrder === "asc" ? "mh-table__sort-icon--asc" : "", props.sortKey === column.key && props.sortOrder === "desc" ? "mh-table__sort-icon--desc" : ""].filter(Boolean).join(" ")
282
- }, null)]) : null]))])]) : null, createVNode("tbody", null, [isEmpty.value ? createVNode("tr", {
283
- "class": "mh-table__empty-row"
298
+ "name": "chevron-down",
299
+ "class": "mh-table__expand-icon"
300
+ }, null)])]) : null, hasSelectColumn.value ? (
301
+ /* 勾选列复用 `Checkbox`(不手写原生 input)。行本身可能带 `onRow` 的 onClick:勾选不该顺带触发行
302
+ 点击 —— 阻断挂在**单元格**上(`Checkbox` 的根是 `label`,点它会冒泡两次,只挡 input 挡不住)。
303
+ `selectable` 为函数时逐行判定:返回 `false` 的行给禁用态(置灰 + not-allowed)。 */
304
+ createVNode("td", {
305
+ "class": "mh-table__col-check",
306
+ "onClick": (event) => event.stopPropagation()
307
+ }, [createVNode(Checkbox, {
308
+ "aria-label": "选择本行",
309
+ "modelValue": selected.value.includes(key),
310
+ "disabled": !isRowSelectable(row, index),
311
+ "onChange": () => toggleRow(key)
312
+ }, null)])
313
+ ) : null, props.columns.map((column) => createVNode("td", {
314
+ "key": column.key,
315
+ "class": cellClass(column).join(" ")
316
+ }, [slots.cell ? (
317
+ /* 展示格与编辑格都走这条(含行尾操作列):值、位置、回写口一并交给调用方 */
318
+ slots.cell({
319
+ row,
320
+ column,
321
+ value: row[column.key],
322
+ index,
323
+ setValue: cellSetValue(row, column, index)
324
+ })
325
+ ) : String(row[column.key] ?? "")]))]), isExpanded ? createVNode("tr", {
326
+ "key": `${key}-expand`,
327
+ "class": "mh-table__expand-row"
284
328
  }, [createVNode("td", {
285
- "class": "mh-table__empty",
286
329
  "colspan": columnCount.value
287
- }, [slots.empty ? slots.empty() : props.emptyText])]) : null, props.data.map((row, index) => {
288
- const key = row[props.rowKey];
289
- const rowProps = props.onRow?.(row, index);
290
- const isExpanded = expanded.value.includes(key);
291
- const rowState = [props.striped && index % 2 === 1 ? "is-striped" : "", selected.value.includes(key) ? "is-selected" : "", rowProps?.onClick ? "is-clickable" : "", rowProps?.class ?? "", props.rowClass?.(row, index) ?? ""].filter(Boolean).join(" ");
292
- return [createVNode("tr", {
293
- "key": key,
294
- "class": rowState,
295
- "data-state": selected.value.includes(key) ? "selected" : "idle",
296
- "onClick": rowProps?.onClick ? (event) => rowProps.onClick?.(row, index, event) : void 0
297
- }, [props.expandable ? createVNode("td", {
298
- "class": "mh-table__col-expand"
299
- }, [createVNode("button", {
300
- "type": "button",
301
- "class": "mh-table__expand",
302
- "aria-label": isExpanded ? "收起" : "展开",
303
- "aria-expanded": isExpanded ? "true" : "false",
304
- "onClick": (event) => {
305
- event.stopPropagation();
306
- toggleExpand(key);
307
- }
308
- }, [createVNode(Icon, {
309
- "name": "chevron-down",
310
- "class": "mh-table__expand-icon"
311
- }, null)])]) : null, hasSelectColumn.value ? (
312
- /* 勾选列复用 `Checkbox`(不手写原生 input)。行本身可能带 `onRow` 的 onClick:勾选不该顺带触发行
313
- 点击 —— 阻断挂在**单元格**上(`Checkbox` 的根是 `label`,点它会冒泡两次,只挡 input 挡不住)。
314
- `selectable` 为函数时逐行判定:返回 `false` 的行给禁用态(置灰 + not-allowed)。 */
315
- createVNode("td", {
316
- "class": "mh-table__col-check",
317
- "onClick": (event) => event.stopPropagation()
318
- }, [createVNode(Checkbox, {
319
- "aria-label": "选择本行",
320
- "modelValue": selected.value.includes(key),
321
- "disabled": !isRowSelectable(row, index),
322
- "onChange": () => toggleRow(key)
323
- }, null)])
324
- ) : null, props.columns.map((column) => createVNode("td", {
325
- "key": column.key,
326
- "class": cellClass(column).join(" ")
327
- }, [slots.cell ? (
328
- /* 编辑格与展示格都走这条:值、位置、回写口一并交给调用方 */
329
- slots.cell({
330
- row,
331
- column,
332
- value: row[column.key],
333
- index,
334
- setValue: cellSetValue(row, column, index)
335
- })
336
- ) : slots.actions && column.align === "right" ? slots.actions({
337
- row,
338
- index
339
- }) : String(row[column.key] ?? "")]))]), isExpanded ? createVNode("tr", {
340
- "key": `${key}-expand`,
341
- "class": "mh-table__expand-row"
342
- }, [createVNode("td", {
343
- "colspan": columnCount.value
344
- }, [props.expandable?.(row, index)])]) : null];
345
- })])]), props.loading ? createVNode("div", {
330
+ }, [props.expandable?.(row, index)])]) : null];
331
+ })]);
332
+ const headBoxRef = ref(null);
333
+ const headTableRef = ref(null);
334
+ const scrollBoxRef = ref(null);
335
+ const syncHead = () => {
336
+ const box = scrollBoxRef.value;
337
+ const head = headBoxRef.value;
338
+ if (!box) return;
339
+ if (headTableRef.value) headTableRef.value.style.transform = `translateX(${-box.scrollLeft}px)`;
340
+ if (head) {
341
+ const gutter = `${box.offsetWidth - box.clientWidth}px`;
342
+ if (head.style.paddingRight !== gutter) head.style.paddingRight = gutter;
343
+ }
344
+ };
345
+ onMounted(syncHead);
346
+ onUpdated(syncHead);
347
+ return () => {
348
+ const fill = props.fill !== false;
349
+ const tableClass = ["mh-table", resolvedSize.value === "sm" ? "mh-table--xs" : "", resolvedSize.value === "lg" ? "mh-table--lg" : "", props.bordered ? "" : "mh-table--borderless", fill ? "mh-table--fill" : "", fill || props.layout === "fixed" ? "mh-table--fixed" : "", attrs.class].filter(Boolean).join(" ");
350
+ const bodyTableClass = props.stickyCol ? `${tableClass} mh-table--sticky-col` : tableClass;
351
+ const cellTokens = fill || props.layout === "fixed" ? {
352
+ "--ui-control-min-width": "0"
353
+ } : void 0;
354
+ const batchBar = slots.batch && selected.value.length > 0 ? createVNode("div", {
355
+ "class": "mh-table__batch"
356
+ }, [slots.batch({
357
+ selected: selected.value
358
+ })]) : null;
359
+ const summaryBar = slots.summary ? createVNode("div", {
360
+ "class": "mh-table__summary"
361
+ }, [slots.summary()]) : null;
362
+ const loadingOverlay = props.loading ? createVNode("div", {
346
363
  "class": "mh-table__loading",
347
364
  "aria-busy": "true"
348
365
  }, [createVNode("span", {
349
366
  "class": "mh-table__spinner"
350
- }, null)]) : null, pager ? createVNode("div", {
351
- "class": "mh-table__pagination"
352
- }, [createVNode(Pagination, {
353
- "total": pager.total,
354
- "modelValue": pager.current ?? 1,
355
- "pageSize": pager.pageSize ?? 10,
356
- "pageSizeOptions": pager.pageSizeOptions,
357
- "onChange": (payload) => pager.onChange?.(payload.page, payload.pageSize)
358
- }, null)]) : null]);
367
+ }, null)]) : null;
368
+ if (!fill) {
369
+ return createVNode("div", {
370
+ "class": "mh-table-wrapper",
371
+ "style": attrs.style
372
+ }, [batchBar, createVNode("table", {
373
+ "class": bodyTableClass,
374
+ "style": cellTokens
375
+ }, [colElements(), props.showHeader ? renderHead() : null, renderBody()]), summaryBar, loadingOverlay]);
376
+ }
377
+ const wrapperStyle = props.fill === true || props.fill === "parent" ? [{
378
+ height: "100%"
379
+ }, attrs.style] : attrs.style;
380
+ const scrollStyle = typeof props.fill === "number" ? {
381
+ maxHeight: `${props.fill}px`
382
+ } : void 0;
383
+ return createVNode("div", {
384
+ "class": "mh-table-wrapper mh-table-wrapper--fill",
385
+ "style": wrapperStyle
386
+ }, [batchBar, props.showHeader ? createVNode("div", {
387
+ "class": "mh-table__head",
388
+ "ref": headBoxRef
389
+ }, [createVNode("table", {
390
+ "class": tableClass,
391
+ "ref": headTableRef,
392
+ "style": cellTokens
393
+ }, [colElements(), renderHead()])]) : null, createVNode("div", {
394
+ "class": "mh-table__body"
395
+ }, [createVNode("div", {
396
+ "class": "mh-table-scroll",
397
+ "ref": scrollBoxRef,
398
+ "onScroll": syncHead,
399
+ "style": scrollStyle
400
+ }, [createVNode("table", {
401
+ "class": bodyTableClass,
402
+ "style": cellTokens
403
+ }, [colElements(), renderBody()])]), loadingOverlay]), summaryBar]);
359
404
  };
360
405
  }
361
406
  });
package/dist/index.d.ts CHANGED
@@ -45,7 +45,7 @@ export { Notification, type NotificationTone, } from './components/notification'
45
45
  export { Dialog, type DialogWidth } from './components/dialog';
46
46
  export { Drawer, type DrawerSide } from './components/drawer';
47
47
  export { Tooltip, type TooltipSide, type TooltipAlign, } from './components/tooltip';
48
- export { Table, type TableColumn, type TableCellSlotScope, type TableRow, type TableRowSelectable, type TableSortOrder, } from './components/table';
48
+ export { Table, type TableColumn, type TableCellSlotScope, type TableFill, type TableRow, type TableRowSelectable, type TableSortOrder, } from './components/table';
49
49
  export { Pagination, type PaginationPageItem } from './components/pagination';
50
50
  export { TabBar, type TabBarOption, type TabBarValue } from './components/tabbar';
51
51
  export { Steps, type CanJump, type StepsItem, type StepsVariant, type StepsCompound, type StepsCompoundType, } from './components/steps';
@@ -30,14 +30,41 @@
30
30
  .mh-table-wrapper {
31
31
  overflow: auto;
32
32
  }
33
- /* `--fill` 档把外框从表移交给包裹层(表自己不再画框,改由包裹层画满高框) */
34
- .mh-table-wrapper:has(> .mh-table--fill) {
33
+ /* ===== 满高档(两表结构:表头盒 + 表体滚动盒)=====
34
+ * 外框从表移交给包裹层(表自己不再画框),**表头不在滚动盒里** —— 表头固定是结构给的,不靠 sticky。 */
35
+ .mh-table-wrapper--fill {
36
+ display: flex;
37
+ flex-direction: column;
38
+ overflow: hidden; /* 表头底 / 末行跟着外框圆角收边 */
35
39
  border: 1px solid var(--ui-table-border);
36
40
  border-radius: var(--ui-table-radius);
37
41
  }
42
+ /* 滚动区外的常驻块不被压缩(表体盒吃剩余高度) */
43
+ .mh-table-wrapper--fill > .mh-table__batch,
44
+ .mh-table-wrapper--fill > .mh-table__summary {
45
+ flex: none;
46
+ }
47
+ /* 表头盒:裁切横向平移的表头(平移由组件同步,不在这里做) */
48
+ .mh-table__head {
49
+ flex: none;
50
+ overflow: hidden;
51
+ }
52
+ /* 表体盒:表体的定位上下文(满高档的加载罩按它铺满,不再盖到表头) */
53
+ .mh-table__body {
54
+ position: relative;
55
+ flex: 1 1 auto;
56
+ min-height: 0;
57
+ }
58
+ /* 表体滚动盒:满高档唯一的滚动落点 */
38
59
  .mh-table-scroll {
39
- max-height: var(--ui-table-scroll-height); /* 规范样张 132(实际用按需覆盖) */
60
+ height: 100%;
61
+ max-height: var(--ui-table-scroll-height); /* 基础上限;「占满父级」档由下面那条解除 */
40
62
  overflow: auto;
63
+ scrollbar-gutter: stable; /* 滚动条槽常驻:表体宽度不随内容长短跳变 */
64
+ }
65
+ /* 满高档:滚动范围由包裹层高度(或调用方给的内联上限)决定 */
66
+ .mh-table-wrapper--fill > .mh-table__body > .mh-table-scroll {
67
+ max-height: none;
41
68
  }
42
69
 
43
70
  /* 几何只挂在 `<table>` 上:令牌在同名 `.tokens.css`(锚 `[data-manohub-ui]`),几何若跟着挂会给包裹层加边框与圆角 */
@@ -195,7 +222,7 @@
195
222
  width: var(--ui-table-col-check);
196
223
  text-align: center;
197
224
  }
198
- .mh-table__col-actions {
225
+ .mh-table__col-right {
199
226
  text-align: right;
200
227
  }
201
228
 
@@ -207,7 +234,7 @@
207
234
  .mh-table thead th.mh-table__cell-center {
208
235
  text-align: center;
209
236
  }
210
- .mh-table thead th.mh-table__col-actions {
237
+ .mh-table thead th.mh-table__col-right {
211
238
  text-align: right;
212
239
  }
213
240
  /* `--sticky-col`:首列钉住(必须自带底,见文件头第 4 条) */
@@ -222,14 +249,8 @@
222
249
  z-index: 2; /* 表头既钉顶又钉左,压住表头其余列 */
223
250
  background: var(--ui-table-head-bg);
224
251
  }
225
- /* `--sticky-head`:表头钉顶。⚠️ 选择器必须带 `tr`(0-1-3):文件后段「拖拽线的定位上下文」那条
226
- * `.mh-table thead th` 是 **0-1-2**,与不写 `tr` 的写法同特异性且在后 ⇒ 会覆盖 `position: sticky`,
227
- * `stickyHead` 静默失效。`sticky` 同样是定位元素,照样给表头里的拖拽线当包含块,故加 `tr` 不损失原能力。 */
228
- .mh-table--sticky-head thead tr th {
229
- position: sticky;
230
- top: 0;
231
- z-index: 1;
232
- }
252
+ /* `--sticky-head` 已退场(2026-10-10):表头固定改由满高档的两表结构承担 —— 表头不在滚动盒里,
253
+ * 不需要 sticky,也就没有「特异性压不过 `position: relative`」那类静默失效。 */
233
254
 
234
255
  /* ===== 单元格类型 ===== */
235
256
  .mh-table__cell-center {
@@ -416,6 +437,13 @@
416
437
  text-align: center;
417
438
  color: var(--ui-base-content-muted);
418
439
  }
440
+ /* 满高档的空态:空态格撑满表体可见高度(按行选择 —— 空数据时才有那一行,有数据时行高不受影响) */
441
+ .mh-table-wrapper--fill .mh-table-scroll > .mh-table:has(.mh-table__empty-row) {
442
+ height: 100%;
443
+ }
444
+ .mh-table-wrapper--fill .mh-table__empty {
445
+ height: 100%;
446
+ }
419
447
  .mh-table__empty-row:hover td {
420
448
  background: transparent;
421
449
  }
@@ -460,10 +488,3 @@
460
488
  padding: var(--ui-space-4) var(--ui-table-padding-x);
461
489
  background: var(--ui-base-200);
462
490
  }
463
-
464
- /* ===== 分页条(贴在表格下方、右对齐;分页件自身的面归 pagination.css) ===== */
465
- .mh-table__pagination {
466
- display: flex;
467
- justify-content: flex-end;
468
- padding: var(--ui-space-4) 0 0;
469
- }
@@ -39,5 +39,5 @@
39
39
  --ui-table-summary-height: 40px;
40
40
  --ui-table-batch-height: 34px;
41
41
  --ui-table-tree-indent: 38px;
42
- --ui-table-scroll-height: 132px; /* 规范样张值 */
42
+ --ui-table-scroll-height: 132px; /* 表体滚动盒的基础上限(满高「占满父级」档由面解除、数值档由组件内联覆盖) */
43
43
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@manohub/ui",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "AIHub 自绘组件层:不依赖 farris 的手写 Vue TSX 组件 + 自带令牌层(Farris 规范视觉 · daisyUI 式语义令牌 · shadcn 式 API 词汇)。",
@@ -35,7 +35,7 @@
35
35
  "vue": "^3.5.0"
36
36
  },
37
37
  "dependencies": {
38
- "@manohub/icon": "^1.0.6"
38
+ "@manohub/icon": "^1.0.8"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@types/node": "^24.0.0",