@manohub/ui 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -50,6 +50,36 @@ export interface TableRowProps {
50
50
  class?: string;
51
51
  onClick?: (row: TableRow, index: number, event: MouseEvent) => void;
52
52
  }
53
+ /**
54
+ * `#cell` 插槽的作用域 —— **单元格编辑走这条**。
55
+ *
56
+ * 表格**不替调用方渲染控件**:编辑格要摆什么(`Input` / `Select` / `NumberInput`、外层要不要套
57
+ * `Form.Item` 拿 label 位与错误文案位、只读态摆什么)全由调用方在插槽里决定。口径与 `Form.Item`
58
+ * 同款 —— 它的默认插槽也是「你自己放控件」;骨架层因此不必猜每种业务格子的排版,
59
+ * 也不会把「哪一格长什么样」的开关堆成一张越来越长的 prop 表。
60
+ *
61
+ * **回写是受控的**:`setValue(next)` 只把「这一格变成了什么」上报出去
62
+ * (= 发一次 `onCellChange(row, column.key, next, index)`)。本件**从不改 `data`** ——
63
+ * 新数组与「这次改动该不该被接受」都由调用方决定,与行选中同一套受控口径。
64
+ *
65
+ * @example 编辑格:套 `Form.Item` 拿错误位,控件自己绑值、回写走 `setValue`
66
+ * <Table :columns="columns" :data="rows" row-key="id" layout="fixed" :on-cell-change="write">
67
+ * <template #cell="{ row, column, value, index, setValue }">
68
+ * <!-- 格子里的框由控件自己带:默认有框 / `:bordered="false"` 无框 / `bordered="focus"` 聚焦才显框 -->
69
+ * <Input v-if="column.key === 'name'" :model-value="String(value ?? '')" @change="setValue" />
70
+ * <template v-else>{{ value }}</template>
71
+ * </template>
72
+ * </Table>
73
+ */
74
+ export interface TableCellSlotScope {
75
+ row: TableRow;
76
+ column: TableColumn;
77
+ /** 该格当前值(`row[column.key]` 的原值;表格不改写它,也不做任何格式化) */
78
+ value: unknown;
79
+ index: number;
80
+ /** 上报该格的新值(= 发一次 `onCellChange`);调用方没接 `onCellChange` 时是空操作 */
81
+ setValue: (next: unknown) => void;
82
+ }
53
83
  /**
54
84
  * 行级可勾选判定(`selectable` 的函数形态):返回 `false` 的行渲染**禁用态**勾选框,
55
85
  * 且不进表头全选的增删范围。签名与 `onRow` / `rowClass` 一致(都给 `index`)。
@@ -150,6 +180,17 @@ export declare const Table: import("vue").DefineComponent<import("vue").ExtractP
150
180
  type: BooleanConstructor;
151
181
  default: boolean;
152
182
  };
183
+ /**
184
+ * 单元格值变更 —— 由 `#cell` 插槽里的 `setValue` 触发,是**唯一的回写通道**。
185
+ *
186
+ * 本件**不改 `data`**(与本仓表格一贯的克制口径一致:全受控、只发事件、不排序、不切片);
187
+ * 要不要接受、以及新数组怎么产生,都由调用方决定。
188
+ * 没接这个回调时 `setValue` 是空操作(插槽里的控件照旧可绑,不会报错)。
189
+ */
190
+ onCellChange: {
191
+ type: PropType<(row: TableRow, key: string, value: unknown, index: number) => void>;
192
+ default: undefined;
193
+ };
153
194
  /** 尺寸档:`sm` 紧凑(表格嵌在卡片里)/ `md` 标准 / `lg` 宽松(详情页) */
154
195
  size: {
155
196
  type: PropType<ControlSize>;
@@ -160,7 +201,18 @@ export declare const Table: import("vue").DefineComponent<import("vue").ExtractP
160
201
  type: BooleanConstructor;
161
202
  default: boolean;
162
203
  };
163
- borderless: {
204
+ /**
205
+ * 有无外框(缺省**有**框):去外框写 `:bordered="false"`。
206
+ *
207
+ * **极性口径**:本库的「有无外框」一律用**肯定式** `bordered`(`Input` / `Textarea` /
208
+ * `NumberInput` / `Nav` 同款,见 `docs/guide/naming-and-defaults.md` §2.1)。本件原名
209
+ * `borderless`(否定式、默认 `false`),2026-10 按同名同义统一改成 `bordered` —— 默认行为不变
210
+ * (都是「缺省有框」),只是名字的极性换到与其余件一致,调用方不必再记「哪件用否定式」。
211
+ *
212
+ * 类名仍是 `.mh-table--borderless`:面层词根**统一为否定式**(输入族关框时也输出它),
213
+ * 与 prop 的极性相反 —— 两套各自内部一致,`Input` 的文档页记着同一处口径。
214
+ */
215
+ bordered: {
164
216
  type: BooleanConstructor;
165
217
  default: boolean;
166
218
  };
@@ -297,6 +349,17 @@ export declare const Table: import("vue").DefineComponent<import("vue").ExtractP
297
349
  type: BooleanConstructor;
298
350
  default: boolean;
299
351
  };
352
+ /**
353
+ * 单元格值变更 —— 由 `#cell` 插槽里的 `setValue` 触发,是**唯一的回写通道**。
354
+ *
355
+ * 本件**不改 `data`**(与本仓表格一贯的克制口径一致:全受控、只发事件、不排序、不切片);
356
+ * 要不要接受、以及新数组怎么产生,都由调用方决定。
357
+ * 没接这个回调时 `setValue` 是空操作(插槽里的控件照旧可绑,不会报错)。
358
+ */
359
+ onCellChange: {
360
+ type: PropType<(row: TableRow, key: string, value: unknown, index: number) => void>;
361
+ default: undefined;
362
+ };
300
363
  /** 尺寸档:`sm` 紧凑(表格嵌在卡片里)/ `md` 标准 / `lg` 宽松(详情页) */
301
364
  size: {
302
365
  type: PropType<ControlSize>;
@@ -307,7 +370,18 @@ export declare const Table: import("vue").DefineComponent<import("vue").ExtractP
307
370
  type: BooleanConstructor;
308
371
  default: boolean;
309
372
  };
310
- borderless: {
373
+ /**
374
+ * 有无外框(缺省**有**框):去外框写 `:bordered="false"`。
375
+ *
376
+ * **极性口径**:本库的「有无外框」一律用**肯定式** `bordered`(`Input` / `Textarea` /
377
+ * `NumberInput` / `Nav` 同款,见 `docs/guide/naming-and-defaults.md` §2.1)。本件原名
378
+ * `borderless`(否定式、默认 `false`),2026-10 按同名同义统一改成 `bordered` —— 默认行为不变
379
+ * (都是「缺省有框」),只是名字的极性换到与其余件一致,调用方不必再记「哪件用否定式」。
380
+ *
381
+ * 类名仍是 `.mh-table--borderless`:面层词根**统一为否定式**(输入族关框时也输出它),
382
+ * 与 prop 的极性相反 —— 两套各自内部一致,`Input` 的文档页记着同一处口径。
383
+ */
384
+ bordered: {
311
385
  type: BooleanConstructor;
312
386
  default: boolean;
313
387
  };
@@ -414,11 +488,12 @@ export declare const Table: import("vue").DefineComponent<import("vue").ExtractP
414
488
  expandedKeys: (string | number)[];
415
489
  defaultExpandedKeys: (string | number)[];
416
490
  onExpandChange: (keys: Array<string | number>) => void;
491
+ bordered: boolean;
417
492
  modelValue: (string | number)[];
418
493
  emptyText: string;
419
494
  selectable: boolean | TableRowSelectable;
420
495
  striped: boolean;
421
- borderless: boolean;
496
+ onCellChange: (row: TableRow, key: string, value: unknown, index: number) => void;
422
497
  stickyHead: boolean;
423
498
  stickyCol: boolean;
424
499
  showHeader: boolean;
@@ -429,13 +504,13 @@ export declare const Table: import("vue").DefineComponent<import("vue").ExtractP
429
504
  expandable: (row: TableRow, index: number) => VNodeChild;
430
505
  pagination: false | TablePagination;
431
506
  }, SlotsType<{
432
- /** 单元格:`{ row, column, value, index }`(不给则直接输出原值) */
433
- cell?: (context: {
434
- row: TableRow;
435
- column: TableColumn;
436
- value: unknown;
437
- index: number;
438
- }) => VNodeChild;
507
+ /**
508
+ * 单元格:`{ row, column, value, index, setValue }`(不给则直接输出原值)。
509
+ *
510
+ * **编辑格也走这里**(见 `TableCellSlotScope` 的说明):本件不替调用方渲染控件,
511
+ * 只给值、给位置、给一个受控的回写口 `setValue`。
512
+ */
513
+ cell?: (context: TableCellSlotScope) => VNodeChild;
439
514
  /** 行尾操作区:`{ row, index }`(面里用 `align: 'right'` 的列承载) */
440
515
  actions?: (context: {
441
516
  row: TableRow;
@@ -43,6 +43,17 @@ const Table = /* @__PURE__ */ defineComponent({
43
43
  type: Boolean,
44
44
  default: false
45
45
  },
46
+ /**
47
+ * 单元格值变更 —— 由 `#cell` 插槽里的 `setValue` 触发,是**唯一的回写通道**。
48
+ *
49
+ * 本件**不改 `data`**(与本仓表格一贯的克制口径一致:全受控、只发事件、不排序、不切片);
50
+ * 要不要接受、以及新数组怎么产生,都由调用方决定。
51
+ * 没接这个回调时 `setValue` 是空操作(插槽里的控件照旧可绑,不会报错)。
52
+ */
53
+ onCellChange: {
54
+ type: Function,
55
+ default: void 0
56
+ },
46
57
  /** 尺寸档:`sm` 紧凑(表格嵌在卡片里)/ `md` 标准 / `lg` 宽松(详情页) */
47
58
  size: {
48
59
  type: String,
@@ -53,9 +64,20 @@ const Table = /* @__PURE__ */ defineComponent({
53
64
  type: Boolean,
54
65
  default: false
55
66
  },
56
- borderless: {
67
+ /**
68
+ * 有无外框(缺省**有**框):去外框写 `:bordered="false"`。
69
+ *
70
+ * **极性口径**:本库的「有无外框」一律用**肯定式** `bordered`(`Input` / `Textarea` /
71
+ * `NumberInput` / `Nav` 同款,见 `docs/guide/naming-and-defaults.md` §2.1)。本件原名
72
+ * `borderless`(否定式、默认 `false`),2026-10 按同名同义统一改成 `bordered` —— 默认行为不变
73
+ * (都是「缺省有框」),只是名字的极性换到与其余件一致,调用方不必再记「哪件用否定式」。
74
+ *
75
+ * 类名仍是 `.mh-table--borderless`:面层词根**统一为否定式**(输入族关框时也输出它),
76
+ * 与 prop 的极性相反 —— 两套各自内部一致,`Input` 的文档页记着同一处口径。
77
+ */
78
+ bordered: {
57
79
  type: Boolean,
58
- default: false
80
+ default: true
59
81
  },
60
82
  /** 外框交给包裹层(满高表格用) */
61
83
  fill: {
@@ -178,6 +200,9 @@ const Table = /* @__PURE__ */ defineComponent({
178
200
  const columnCount = computed(() => leadColumns.value + props.columns.length);
179
201
  const isEmpty = computed(() => props.data.length === 0);
180
202
  const cellClass = (column) => [column.align === "center" ? "mh-table__cell-center" : "", column.align === "right" ? "mh-table__cell-amount" : "", column.wrap ? "mh-table__cell-wrap" : ""].filter(Boolean);
203
+ const cellSetValue = (row, column, index) => (next) => {
204
+ props.onCellChange?.(row, column.key, next, index);
205
+ };
181
206
  const colStyles = computed(() => {
182
207
  const weightSum = props.columns.reduce((sum, item) => typeof item.width === "number" ? sum + item.width : sum, 0);
183
208
  return props.columns.map((column) => {
@@ -221,7 +246,7 @@ const Table = /* @__PURE__ */ defineComponent({
221
246
  };
222
247
  const sortGlyph = (key) => props.sortKey === key && props.sortOrder === "asc" ? "chevron-up" : "chevron-down";
223
248
  return () => {
224
- const tableClass = ["mh-table", resolvedSize.value === "sm" ? "mh-table--xs" : "", resolvedSize.value === "lg" ? "mh-table--lg" : "", props.borderless ? "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(" ");
249
+ 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(" ");
225
250
  const pager = typeof props.pagination === "object" ? props.pagination : null;
226
251
  return createVNode("div", {
227
252
  "class": "mh-table-wrapper",
@@ -303,12 +328,16 @@ const Table = /* @__PURE__ */ defineComponent({
303
328
  ) : null, props.columns.map((column) => createVNode("td", {
304
329
  "key": column.key,
305
330
  "class": cellClass(column).join(" ")
306
- }, [slots.cell ? slots.cell({
307
- row,
308
- column,
309
- value: row[column.key],
310
- index
311
- }) : slots.actions && column.align === "right" ? slots.actions({
331
+ }, [slots.cell ? (
332
+ /* 编辑格与展示格都走这条:值、位置、回写口一并交给调用方 */
333
+ slots.cell({
334
+ row,
335
+ column,
336
+ value: row[column.key],
337
+ index,
338
+ setValue: cellSetValue(row, column, index)
339
+ })
340
+ ) : slots.actions && column.align === "right" ? slots.actions({
312
341
  row,
313
342
  index
314
343
  }) : String(row[column.key] ?? "")]))]), isExpanded ? createVNode("tr", {
@@ -99,7 +99,7 @@ export declare const Toast: import("vue").DefineComponent<import("vue").ExtractP
99
99
  onClose?: (() => any) | undefined;
100
100
  }>, {
101
101
  text: string;
102
- icon: boolean | "search" | "link" | "filter" | "copy" | "help" | "table" | "menu" | "delete" | "image" | "close" | "key" | "page-title" | "loading" | "info" | "alert" | "chevron-left" | "chevron-right" | "chevron-down" | "chevron-up" | "check" | "user" | "download" | "back" | "folder" | "folder-open" | "file" | "file-code" | "file-json" | "file-text" | "terminal" | "shield" | "tool" | "plus" | "notification" | "settings" | "logout" | "home" | "workbench" | "apps" | "favorite" | "favorite-filled" | "drag-handle" | "edit" | "refresh" | "upload" | "save" | "more" | "preview" | "external-link" | "minus" | "plus-circle" | "close-circle" | "info-circle" | "check-circle" | "alert-circle" | "list" | "calendar" | "clock" | "lock" | "unlock" | "users" | "archive" | "package" | "plug" | "branch" | "tag" | "ai" | "shield-check" | "shield-alert" | "history";
102
+ icon: boolean | "search" | "link" | "filter" | "copy" | "help" | "table" | "menu" | "delete" | "image" | "close" | "key" | "application" | "client" | "service" | "schema" | "compose" | "log" | "mcp" | "agent" | "server" | "exposure" | "openapi" | "loading" | "info" | "alert" | "chevron-left" | "chevron-right" | "chevron-down" | "chevron-up" | "check" | "user" | "download" | "back" | "folder" | "folder-open" | "file" | "file-code" | "file-json" | "file-text" | "terminal" | "shield" | "tool" | "plus" | "notification" | "settings" | "logout" | "home" | "workbench" | "apps" | "favorite" | "page-title" | "favorite-filled" | "drag-handle" | "edit" | "refresh" | "upload" | "save" | "more" | "preview" | "external-link" | "minus" | "plus-circle" | "close-circle" | "info-circle" | "check-circle" | "alert-circle" | "list" | "calendar" | "clock" | "lock" | "unlock" | "users" | "archive" | "package" | "plug" | "branch" | "tag" | "ai" | "shield-check" | "shield-alert" | "history";
103
103
  closable: boolean;
104
104
  tone: ToastTone;
105
105
  action: string;
@@ -151,7 +151,7 @@ export declare const ToastCompound: {
151
151
  close: () => true;
152
152
  }, import("vue").PublicProps, {
153
153
  text: string;
154
- icon: boolean | "search" | "link" | "filter" | "copy" | "help" | "table" | "menu" | "delete" | "image" | "close" | "key" | "page-title" | "loading" | "info" | "alert" | "chevron-left" | "chevron-right" | "chevron-down" | "chevron-up" | "check" | "user" | "download" | "back" | "folder" | "folder-open" | "file" | "file-code" | "file-json" | "file-text" | "terminal" | "shield" | "tool" | "plus" | "notification" | "settings" | "logout" | "home" | "workbench" | "apps" | "favorite" | "favorite-filled" | "drag-handle" | "edit" | "refresh" | "upload" | "save" | "more" | "preview" | "external-link" | "minus" | "plus-circle" | "close-circle" | "info-circle" | "check-circle" | "alert-circle" | "list" | "calendar" | "clock" | "lock" | "unlock" | "users" | "archive" | "package" | "plug" | "branch" | "tag" | "ai" | "shield-check" | "shield-alert" | "history";
154
+ icon: boolean | "search" | "link" | "filter" | "copy" | "help" | "table" | "menu" | "delete" | "image" | "close" | "key" | "application" | "client" | "service" | "schema" | "compose" | "log" | "mcp" | "agent" | "server" | "exposure" | "openapi" | "loading" | "info" | "alert" | "chevron-left" | "chevron-right" | "chevron-down" | "chevron-up" | "check" | "user" | "download" | "back" | "folder" | "folder-open" | "file" | "file-code" | "file-json" | "file-text" | "terminal" | "shield" | "tool" | "plus" | "notification" | "settings" | "logout" | "home" | "workbench" | "apps" | "favorite" | "page-title" | "favorite-filled" | "drag-handle" | "edit" | "refresh" | "upload" | "save" | "more" | "preview" | "external-link" | "minus" | "plus-circle" | "close-circle" | "info-circle" | "check-circle" | "alert-circle" | "list" | "calendar" | "clock" | "lock" | "unlock" | "users" | "archive" | "package" | "plug" | "branch" | "tag" | "ai" | "shield-check" | "shield-alert" | "history";
155
155
  closable: boolean;
156
156
  tone: ToastTone;
157
157
  action: string;
@@ -205,7 +205,7 @@ export declare const ToastCompound: {
205
205
  onClose?: (() => any) | undefined;
206
206
  }>, () => import("vue/jsx-runtime").JSX.Element, {}, {}, {}, {
207
207
  text: string;
208
- icon: boolean | "search" | "link" | "filter" | "copy" | "help" | "table" | "menu" | "delete" | "image" | "close" | "key" | "page-title" | "loading" | "info" | "alert" | "chevron-left" | "chevron-right" | "chevron-down" | "chevron-up" | "check" | "user" | "download" | "back" | "folder" | "folder-open" | "file" | "file-code" | "file-json" | "file-text" | "terminal" | "shield" | "tool" | "plus" | "notification" | "settings" | "logout" | "home" | "workbench" | "apps" | "favorite" | "favorite-filled" | "drag-handle" | "edit" | "refresh" | "upload" | "save" | "more" | "preview" | "external-link" | "minus" | "plus-circle" | "close-circle" | "info-circle" | "check-circle" | "alert-circle" | "list" | "calendar" | "clock" | "lock" | "unlock" | "users" | "archive" | "package" | "plug" | "branch" | "tag" | "ai" | "shield-check" | "shield-alert" | "history";
208
+ icon: boolean | "search" | "link" | "filter" | "copy" | "help" | "table" | "menu" | "delete" | "image" | "close" | "key" | "application" | "client" | "service" | "schema" | "compose" | "log" | "mcp" | "agent" | "server" | "exposure" | "openapi" | "loading" | "info" | "alert" | "chevron-left" | "chevron-right" | "chevron-down" | "chevron-up" | "check" | "user" | "download" | "back" | "folder" | "folder-open" | "file" | "file-code" | "file-json" | "file-text" | "terminal" | "shield" | "tool" | "plus" | "notification" | "settings" | "logout" | "home" | "workbench" | "apps" | "favorite" | "page-title" | "favorite-filled" | "drag-handle" | "edit" | "refresh" | "upload" | "save" | "more" | "preview" | "external-link" | "minus" | "plus-circle" | "close-circle" | "info-circle" | "check-circle" | "alert-circle" | "list" | "calendar" | "clock" | "lock" | "unlock" | "users" | "archive" | "package" | "plug" | "branch" | "tag" | "ai" | "shield-check" | "shield-alert" | "history";
209
209
  closable: boolean;
210
210
  tone: ToastTone;
211
211
  action: string;
@@ -256,7 +256,7 @@ export declare const ToastCompound: {
256
256
  close: () => true;
257
257
  }, string, {
258
258
  text: string;
259
- icon: boolean | "search" | "link" | "filter" | "copy" | "help" | "table" | "menu" | "delete" | "image" | "close" | "key" | "page-title" | "loading" | "info" | "alert" | "chevron-left" | "chevron-right" | "chevron-down" | "chevron-up" | "check" | "user" | "download" | "back" | "folder" | "folder-open" | "file" | "file-code" | "file-json" | "file-text" | "terminal" | "shield" | "tool" | "plus" | "notification" | "settings" | "logout" | "home" | "workbench" | "apps" | "favorite" | "favorite-filled" | "drag-handle" | "edit" | "refresh" | "upload" | "save" | "more" | "preview" | "external-link" | "minus" | "plus-circle" | "close-circle" | "info-circle" | "check-circle" | "alert-circle" | "list" | "calendar" | "clock" | "lock" | "unlock" | "users" | "archive" | "package" | "plug" | "branch" | "tag" | "ai" | "shield-check" | "shield-alert" | "history";
259
+ icon: boolean | "search" | "link" | "filter" | "copy" | "help" | "table" | "menu" | "delete" | "image" | "close" | "key" | "application" | "client" | "service" | "schema" | "compose" | "log" | "mcp" | "agent" | "server" | "exposure" | "openapi" | "loading" | "info" | "alert" | "chevron-left" | "chevron-right" | "chevron-down" | "chevron-up" | "check" | "user" | "download" | "back" | "folder" | "folder-open" | "file" | "file-code" | "file-json" | "file-text" | "terminal" | "shield" | "tool" | "plus" | "notification" | "settings" | "logout" | "home" | "workbench" | "apps" | "favorite" | "page-title" | "favorite-filled" | "drag-handle" | "edit" | "refresh" | "upload" | "save" | "more" | "preview" | "external-link" | "minus" | "plus-circle" | "close-circle" | "info-circle" | "check-circle" | "alert-circle" | "list" | "calendar" | "clock" | "lock" | "unlock" | "users" | "archive" | "package" | "plug" | "branch" | "tag" | "ai" | "shield-check" | "shield-alert" | "history";
260
260
  closable: boolean;
261
261
  tone: ToastTone;
262
262
  action: string;
package/dist/index.d.ts CHANGED
@@ -32,7 +32,7 @@ export { Tree, type TreeNode } from './components/tree';
32
32
  export { ListView, type ListViewProps, type ListViewStatus, type ListViewStatusTone, type ListViewCardItemProps, type ListViewCompound, } from './components/list-view';
33
33
  export { Badge, type BadgeTone, type BadgeShape, type BadgeSize, type BadgeDot, type BadgeStatus, } from './components/badge';
34
34
  export { Button, type ButtonVariant, type ButtonCompound, type ButtonCompoundType, } from './components/button';
35
- export { Input, Textarea, type TextareaProps, type InputCompound, type InputCompoundType, } from './components/input';
35
+ export { Input, Textarea, NumberInput, type TextareaProps, type NumberInputProps, type InputCompound, type InputCompoundType, } from './components/input';
36
36
  export { Search, type SearchProps } from './components/search';
37
37
  export { Select, SelectTree, type SelectOption, type SelectValue } from './components/select';
38
38
  export { Switch } from './components/switch';
@@ -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 TableRow, type TableRowSelectable, type TableSortOrder, } from './components/table';
48
+ export { Table, type TableColumn, type TableCellSlotScope, 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';
package/dist/index.js CHANGED
@@ -38,6 +38,7 @@ import { Upload, formatFileSize } from "./components/upload/index.js";
38
38
  import { QueryState } from "./components/query-state/index.js";
39
39
  import { Loading } from "./components/loading/index.js";
40
40
  import { mergePartStyle, partPaddingStyle } from "./components/part-padding.js";
41
+ import { NumberInput } from "./components/input/number-input.js";
41
42
  import { SelectTree } from "./components/select/select-tree.js";
42
43
  import { alert, closeAllDialogs, confirm } from "./services/dialog.js";
43
44
  import { clearToasts, toast } from "./services/toast.js";
@@ -66,6 +67,7 @@ export {
66
67
  Nav,
67
68
  Notice,
68
69
  Notification,
70
+ NumberInput,
69
71
  Page,
70
72
  Pagination,
71
73
  Panel,
@@ -415,6 +415,20 @@
415
415
  color: var(--ui-base-content);
416
416
  }
417
417
 
418
+ /* 标题左端的品牌色竖标(主色胶囊,值与文档站示例标题同一套:见 `form.tokens.css`)。
419
+ * **只在有标题文本时出现**:这一行也会在「只给 `extra`」时渲染(筛选条那种),
420
+ * 那时凭空多一条竖标就成了坏的排版 —— 用 `:has()` 守住真正有标题的那一种。
421
+ * `flex: none`:标题换行时竖标不许被挤扁。竖标与文本之间的间距由标题行既有的 `gap` 给,
422
+ * 本句不另写间距(两处各写一遍必然漂)。 */
423
+ .mh-form__title:has(> .mh-form__title-text)::before {
424
+ content: '';
425
+ flex: none;
426
+ width: var(--ui-form-title-bar-width);
427
+ height: var(--ui-form-title-bar-height);
428
+ border-radius: var(--ui-radius-full);
429
+ background: var(--ui-primary);
430
+ }
431
+
418
432
  /* 标题文本允许被挤窄(右端操作位不参与收缩);长标题**换行**而不是截断
419
433
  —— 截断会让这一块读不出是什么。 */
420
434
 
@@ -12,4 +12,8 @@
12
12
  [data-manohub-ui] {
13
13
  --ui-form-label-width: 120px;
14
14
  --ui-form-column-min: 320px;
15
+ /* 标题行左端的品牌色竖标(3×12 胶囊)。与文档站示例标题(`.mh-demo__title::before`)
16
+ 是同一套值:「页面里的一块标题」这一语言在库内与文档站只留一份规格。 */
17
+ --ui-form-title-bar-width: 3px;
18
+ --ui-form-title-bar-height: 12px;
15
19
  }
@@ -188,34 +188,45 @@
188
188
  inset: 0;
189
189
  }
190
190
 
191
- /* ===== 无边框档(表格内编辑 / 行内编辑:没有框也没有底,靠悬停给反馈) ===== */
191
+ /* ===== 无边框档(`bordered={false}`:框与底全退,**任何状态都不再出现**) =====
192
+ * 本档的效果**只有一种**:无框。悬停不亮底、聚焦不显框也不留环 —— 「常态无框、聚焦才显框」
193
+ * 是**另一档** `bordered="focus"`(见下一节),不要把两种效果叠在同一档上。
194
+ *
195
+ * **只读值档**(`readonly` + `bordered={false}`):几何与可编辑输入框**完全一致**(同高 / 同内距 /
196
+ * 值文字同左缘),仍可聚焦、可选中复制(只读 ≠ 禁用:要的就是能选、能复制),
197
+ * 但「可编辑」的线索本来就一条都没有 —— 它是摘要 / 详情里值回显的推荐写法,
198
+ * 也是 `Form.Item` 的 `readonly` 行配的那一档。
199
+ *
200
+ * 中和句与通用状态规则**同为 0-2-0**(`.mh-input:hover` / `.mh-input:focus`),靠写在它们之后取胜。 */
192
201
  .mh-input--borderless {
193
202
  border-color: transparent;
194
203
  background: transparent;
195
204
  border-radius: 0;
196
205
  }
197
206
  .mh-input--borderless:hover,
198
- .mh-input--borderless.is-hover {
199
- border-color: transparent;
200
- background: var(--ui-base-200);
201
- }
207
+ .mh-input--borderless.is-hover,
202
208
  .mh-input--borderless:focus,
203
209
  .mh-input--borderless.is-focus {
204
- border-color: var(--ui-input-line-hover);
205
- background: var(--ui-base-100);
206
- box-shadow: 0 0 0 2px var(--ui-primary-weak);
210
+ border-color: transparent;
211
+ background: transparent;
212
+ box-shadow: none;
207
213
  }
208
- /* **只读值档**(`readonly` + `bordered={false}`):几何照旧(与可编辑输入框同高、同内距),
209
- 但把「可编辑」的线索全退掉 —— 只读值不该在滑过时亮起浅底、被点中时亮出边框与主色环。
210
- 仍然可聚焦、可选中复制(只读 ≠ 禁用:要的就是能选、能复制)。
211
- 它是摘要 / 详情里值回显的推荐写法,也是 `Form.Item` 的 `readonly` 行配的那一档。
212
- 特异性 0-3-0,压过上面两条(0-2-0);带壳时框主是壳,同款规则见文件下半部分。 */
213
- .mh-input--borderless[readonly]:hover,
214
- .mh-input--borderless[readonly].is-hover,
215
- .mh-input--borderless[readonly]:focus,
216
- .mh-input--borderless[readonly].is-focus {
214
+
215
+ /* ===== 聚焦显框档(`bordered="focus"`:行内编辑那档)=====
216
+ * 常态无框无底;**悬停与聚焦复用有框档那两条**(`.mh-input:hover` / `.mh-input:focus`:框转 hover
217
+ * 线色、聚焦再加主色弱底环)—— 本档**不另写任何状态描写**,于是「聚焦 = 有框档的样子」这句话
218
+ * 在面上只有一份。
219
+ * 那两条是 0-2-0、本档常态是 0-1-0 ⇒ 悬停 / 聚焦必定压过常态,**与顺序无关**(裸控件这一层)。
220
+ * 配 `readonly`:把复用的线索中和掉(只读值不许看起来能编辑;推荐写法是 `readonly` + `false`)。 */
221
+ .mh-input--border-on-focus {
217
222
  border-color: transparent;
218
223
  background: transparent;
224
+ }
225
+ .mh-input--border-on-focus[readonly]:hover,
226
+ .mh-input--border-on-focus[readonly].is-hover,
227
+ .mh-input--border-on-focus[readonly]:focus,
228
+ .mh-input--border-on-focus[readonly].is-focus {
229
+ border-color: transparent;
219
230
  box-shadow: none;
220
231
  }
221
232
 
@@ -262,13 +273,17 @@
262
273
  * **裸 `.mh-input`(不带壳)仍由控件自己画框**(Select 的搜索行就靠它),见上一节。
263
274
  */
264
275
  .mh-input__wrap {
276
+ /* 左右内距走**内部变量**再吃令牌:壳是框与内距的主,而档位类挂在控件上(壳是它的父),
277
+ 档位规则要能改到壳的内距 —— 见下面两条 `:has()`。写成 `--mh-*` 是刻意的(内部中转),
278
+ 消费方要改内距请重设 `--ui-input-padding-x*` 那三个令牌。 */
279
+ --mh-input-padding-x: var(--ui-input-padding-x);
265
280
  position: relative;
266
281
  display: inline-flex;
267
282
  align-items: center;
268
283
  gap: var(--ui-input-affix-gap);
269
284
  width: 100%;
270
285
  height: var(--ui-input-height);
271
- padding: 0 var(--ui-input-padding-x);
286
+ padding: 0 var(--mh-input-padding-x);
272
287
  border: 1px solid var(--ui-input-line);
273
288
  border-radius: var(--ui-input-radius);
274
289
  background: var(--ui-base-100);
@@ -276,16 +291,21 @@
276
291
  transition-duration: 0.15s;
277
292
  transition-timing-function: ease;
278
293
  }
294
+ /* ⚠️ 档位**必须连内距一起换**,不能只换高度:壳内控件的内距被归零(见 `.mh-input__wrap > .mh-input`),
295
+ 壳的 12px 若原样留着,sm(24 高)就成了「矮框里塞着标准档的内距」—— 左右各 12 对 13 号字太挤,
296
+ 看起来像内距坏了。2026-10-08 修:三档的高度与左右内距成对下发。 */
279
297
  .mh-input__wrap:has(> .mh-input--sm) {
298
+ --mh-input-padding-x: var(--ui-input-padding-x-sm);
280
299
  height: var(--ui-input-height-sm);
281
300
  }
282
301
  .mh-input__wrap:has(> .mh-input--lg) {
302
+ --mh-input-padding-x: var(--ui-input-padding-x-lg);
283
303
  height: var(--ui-input-height-lg);
284
304
  }
285
- /* 多行:高归内容(`resize` 要能改高),框仍归壳 */
305
+ /* 多行:高归内容(`resize` 要能改高),框仍归壳;左右内距同样随档(走同一个变量) */
286
306
  .mh-input__wrap:has(> .mh-input--textarea) {
287
307
  height: auto;
288
- padding: var(--ui-space-2) var(--ui-input-padding-x);
308
+ padding: var(--ui-space-2) var(--mh-input-padding-x);
289
309
  }
290
310
  /* 壳内控件占满剩余宽度,且允许被压缩(min-width:0);附加位增减时输入区自动伸缩 */
291
311
  .mh-input__wrap > .mh-input {
@@ -309,6 +329,15 @@
309
329
  .mh-input__wrap > .mh-input.is-focus {
310
330
  box-shadow: none;
311
331
  }
332
+ /* **聚焦显框档(`bordered="focus"`)的常态**:框与底全退。
333
+ * ⚠️ 这个位置是**语义要求**,不是排版偏好:本句与下面「悬停 / 聚焦」两条**同为 0-2-0**
334
+ * (`:has()` 按参数里最具体的那个算,这里是一个类),同特异性时 CSS 取**后写者** ——
335
+ * 本句若写在那两条之后,聚焦时框永远显不出来。本档的悬停 / 聚焦表现**完全复用**下面那两条
336
+ * (框转 hover 线色 + 主色弱底环),故本档在这里只有「常态」这一句,不另写状态描写。 */
337
+ .mh-input__wrap:has(> .mh-input--border-on-focus) {
338
+ border-color: transparent;
339
+ background: transparent;
340
+ }
312
341
  /* 悬停:反馈画在壳上(壳铺满整件,附加位区域也一起响应) */
313
342
  .mh-input__wrap:hover {
314
343
  border-color: var(--ui-input-line-hover);
@@ -344,29 +373,23 @@
344
373
  background: var(--ui-base-200);
345
374
  border-color: var(--ui-base-300);
346
375
  }
347
- /* 无边框档(表格内编辑 / 行内编辑):框与底全退,悬停给浅底、聚焦才显出框 */
376
+ /* 无边框档(带壳时框主是壳,`bordered={false}`):框与底全退,**任何状态都不再出现**
377
+ * —— 口径与裸控件那节完全一致(本档的效果只有一种)。
378
+ * 中和句 0-3-0 压过通用悬停 / 聚焦(0-2-0),故写在本节(它们之后)也成立;
379
+ * 配 `readonly` 即**只读值档**,几何照旧、线索全无(说明见裸控件那节)。
380
+ * 带壳的 `bordered="focus"` 档只有「常态」那一句,在文件上半部分(与通用悬停 / 聚焦同段,位置有讲究)。 */
348
381
  .mh-input__wrap:has(> .mh-input--borderless) {
349
382
  border-color: transparent;
350
383
  border-radius: 0;
351
384
  background: transparent;
352
385
  }
353
- .mh-input__wrap:has(> .mh-input--borderless):hover {
354
- border-color: transparent;
355
- background: var(--ui-base-200);
356
- }
357
- .mh-input__wrap:has(> .mh-input--borderless):focus-within {
358
- border-color: var(--ui-input-line-hover);
359
- background: var(--ui-base-100);
360
- box-shadow: 0 0 0 2px var(--ui-primary-weak);
361
- }
362
- /* **只读值档**同款(带壳时框主是壳):退掉悬停底与聚焦的边框 / 底色 / 主色环。
363
- 特异性 0-4-0,压过上面两条(0-3-0),并且写在它们之后。 */
364
- .mh-input__wrap:has(> .mh-input--borderless[readonly]):hover,
365
- .mh-input__wrap:has(> .mh-input--borderless[readonly]).is-hover {
366
- border-color: transparent;
367
- background: transparent;
368
- }
369
- .mh-input__wrap:has(> .mh-input--borderless[readonly]):focus-within {
386
+ .mh-input__wrap:has(> .mh-input--borderless):hover,
387
+ .mh-input__wrap:has(> .mh-input--borderless).is-hover,
388
+ .mh-input__wrap:has(> .mh-input--borderless):focus-within,
389
+ /* 静态档(`.is-hover` / `.is-focus` 挂在**控件**上,与通用的 `:has(> .mh-input.is-focus)` 同源)——
390
+ 漏了这两条,截图 / 单测走静态档时框就冒出来了(`:focus-within` 那条盖不住它,两者都是 0-3-0) */
391
+ .mh-input__wrap:has(> .mh-input--borderless.is-hover),
392
+ .mh-input__wrap:has(> .mh-input--borderless.is-focus) {
370
393
  border-color: transparent;
371
394
  background: transparent;
372
395
  box-shadow: none;
@@ -488,6 +511,104 @@
488
511
  bottom: var(--ui-space-1);
489
512
  }
490
513
 
514
+ /* ===== 数字输入(NumberInput;与 Input 同族同面) =====
515
+ *
516
+ * 表壳 / 内距 / 状态协议全部复用上面几节 —— 本件只多两样:控件上的 `.mh-input--number`
517
+ * 与右端的一对步进按钮。故**没有**独立的面文件,也不进 `styles/index.css` 的登记清单
518
+ * (那是「一组件一面」的清单,本件与 `Input` 共用 `input.css`)。
519
+ *
520
+ * 类名**与面文件同根**(`mh-input*`),口径与 `Textarea` 的 `.mh-input--textarea` 一致:
521
+ * 同族两件共用一份面时,族根就是 `mh-input`,另开 `mh-number-*` 会被
522
+ * `__tests__/contract-class-surface.spec.ts` 的「与面文件名同根」那条判据拦下 —— 那条判据
523
+ * 正是为了让「共用一份面」这件事在命名上看得见。
524
+ *
525
+ * `.mh-input--number` 只声明 `font-variant-numeric`:数值等宽,编辑时数字不会因字形宽度
526
+ * 变化而左右跳(比例数字下 `1111` 比 `0000` 窄一截,逐位输入时值区会抖)。它**不承载几何** ——
527
+ * 框与内距归 `.mh-input__wrap` / `.mh-input`,重复声明就是又开一份会漂的规格。 */
528
+ .mh-input--number {
529
+ font-variant-numeric: tabular-nums;
530
+ }
531
+
532
+ /* 步进位:右端的一对上下箭头(列排、整条居中)。
533
+ * 取值(两条约束一起解出来的):md 16 / 压 4(32 − 4 = 28 ≤ 30)、lg 16 / 压 0(32 ≤ 38)、
534
+ * sm 13 / 压 4(26 − 4 = 22 ≤ 22;13 是「放得下 **且** 压不过字形」的最大值 —— 14 的两条约束互相打架)。
535
+ * 尺寸声明落在**步进位自己**上(基准),两档改写靠表壳的 `:has()` 选到它 —— 档位类挂在控件上
536
+ * (`.mh-input--sm`),而箭头是控件的**兄弟**:自定义属性只向下继承,把变量声明在控件上,
537
+ * 箭头取不到值(与 `input.tokens.css` 头注释记的同一个坑)。 */
538
+ .mh-input__step {
539
+ /* md 基准:箭头盒取**常规图标档 16**(一对 32 比内容高 30 多 2,故两枚之间压 4 → 一对 28 落进 30) */
540
+ --mh-input-step-icon: 16px;
541
+ --mh-input-step-overlap: 4px;
542
+ flex: none;
543
+ display: inline-flex;
544
+ flex-direction: column;
545
+ align-self: stretch;
546
+ justify-content: center;
547
+ /* **默认隐形且不可点**:步进位是「聚焦后调值」的辅助位,常显会让页面上每个数字框的右端
548
+ 都挂着一对箭头(密集表单里那一列箭头比数字本身还抢眼)。
549
+ 为什么用 `opacity` 而不是 `display`:① 它仍在流中**占位** —— 显隐时值区宽度不变,
550
+ 否则聚焦的一瞬间数字会横向跳一格;② `pointer-events: none` 让隐形时的热区一并失效,
551
+ 避免「点了看不见的箭头、值莫名其妙变了」。读屏仍能读到这两个按钮(不丢可达性)。 */
552
+ opacity: 0;
553
+ pointer-events: none;
554
+ transition-property: opacity;
555
+ transition-duration: 0.15s;
556
+ transition-timing-function: ease;
557
+ }
558
+ /* 显形:**控件拿到焦点时**(点箭头本身焦点也不会掉出壳 —— 按钮在壳内,`:focus-within` 照旧成立) */
559
+ .mh-input__wrap:focus-within > .mh-input__step {
560
+ opacity: 1;
561
+ pointer-events: auto;
562
+ }
563
+ /* sm:控件只 24 高(内容高 22),16 的一对(32)放不下、14 也放不下(压到安全上限 4.67 时还有 23.3 > 22)。
564
+ 13 是**两条约束的唯一交集**:一对 26 − 4 = 22(正好落进),压缩 4 ≤ 13/3(不侵可点区)。 */
565
+ .mh-input__wrap:has(> .mh-input--sm) > .mh-input__step {
566
+ --mh-input-step-icon: 13px;
567
+ --mh-input-step-overlap: 4px;
568
+ }
569
+ /* lg:内容高 38 放得下 16 的一对,不必压(上下各留 3,字形离边框 9) */
570
+ .mh-input__wrap:has(> .mh-input--lg) > .mh-input__step {
571
+ --mh-input-step-icon: 16px;
572
+ --mh-input-step-overlap: 0px;
573
+ }
574
+
575
+ /* 步进按钮:**裸按钮必须自带 border / background / padding**(基线 reset 不再代消费方归零,
576
+ 见 `styles/reset.css` 与 `__tests__/contract-reset.spec.ts` 的第三组)。
577
+ 盒宽取附加位档(16)—— 热区比字形大一档;常态同附加位色档,滑过转主色;到界即 `:disabled`(不回绕)。 */
578
+ .mh-input__step-btn {
579
+ flex: none;
580
+ display: inline-flex;
581
+ align-items: center;
582
+ justify-content: center;
583
+ width: var(--ui-input-affix-size);
584
+ min-height: 0;
585
+ padding: 0;
586
+ border: none;
587
+ background: transparent;
588
+ color: var(--ui-base-content-subtle);
589
+ cursor: pointer;
590
+ }
591
+ .mh-input__step-btn:hover:not(:disabled) {
592
+ color: var(--ui-primary);
593
+ }
594
+ .mh-input__step-btn:disabled {
595
+ color: var(--ui-base-content-muted);
596
+ cursor: not-allowed;
597
+ }
598
+ /* 第二枚往上**压一档**:一对箭头(两个常规图标档的盒)比控件内容高还高,靠负 margin 挤进去
599
+ (`gap` 不接受负值,故用 `margin-top` 的负值;量由 `--mh-input-step-overlap` 给,改档只动一个数)。
600
+ ⚠️ 压缩量有上限(**盒 / 3**):chevron 的笔画含描边占盒高 1/3,压过它,上箭头字形的下半截
601
+ 就落进下箭头的可点区 —— 用户点「增」会被判成「减」。 */
602
+ .mh-input__step-btn + .mh-input__step-btn {
603
+ margin-top: calc(-1 * var(--mh-input-step-overlap));
604
+ }
605
+ /* 箭头图标:尺寸随档(见上面的 `--mh-input-step-icon`),**不在组件里写死 `size`** */
606
+ .mh-input__step-btn > svg {
607
+ flex: none;
608
+ width: var(--mh-input-step-icon);
609
+ height: var(--mh-input-step-icon);
610
+ }
611
+
491
612
  /* 附加位里下拉件的面板皮肤(与操作层并列组合:`class="mh-popup mh-input__menu"`) */
492
613
  .mh-input__menu {
493
614
  width: 96px;
@@ -313,6 +313,12 @@
313
313
  color: var(--ui-error);
314
314
  }
315
315
 
316
+ /* 单元格里的编辑控件:**表格面不为它写规则**。
317
+ * 编辑格由调用方在 `#cell` 插槽里自己摆(`Input` / `Select` / `NumberInput`,外层可套 `Form.Item`),
318
+ * 框、内距、错误档、只读档全部沿用 form / input 两族既有口径 —— 表格侧再写一份就是开第二套会漂的规格。
319
+ * 唯一要提醒的是**列宽**:`table-layout: auto` 下「输入框的最小内容宽」会把窄列顶开,
320
+ * 列宽要守得住就开 `layout="fixed"`(列宽由 `<colgroup>` 决定,不再被内容撑破)。 */
321
+
316
322
  /* 状态点(语义四档) */
317
323
  .mh-table__status-dot {
318
324
  display: inline-block;