@lensui/lens-table 0.1.2 → 0.1.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.
package/README.md CHANGED
@@ -88,16 +88,17 @@ export function Example() {
88
88
  | `rows` | `Row[]` | 必填 | 表格数据。组件不会直接修改数据,编辑后通过事件通知外部更新。 |
89
89
  | `rowKey` | `keyof Row \| ((row, index) => string \| number)` | `id` | 行唯一标识。默认读取每行的`id` 字段;如果数据不是 `id` 主键,请显式传入字段名或函数。 |
90
90
  | `width` | `number \| string` | `100%` | 表格宽度。数字按像素处理,字符串可传`100%`、`80vw` 等。 |
91
- | `height` | `number \| string` | `100%` | 表格高度。默认继承父容器高度;如果父容器没有可计算高度,则回落到`480px`。 |
92
- | `autoHeight` | `boolean` | `false` | 是否按真实内容高度收缩表格。开启后数据较少时使用 `表头 + 行数 * 行高 + 合计行` 的高度,数据较多时不超过 `height`。 |
93
- | `rowHeight` | `number` | `40` | 每行高度,固定行高用于高性能虚拟滚动。 |
94
- | `headerHeight` | `number` | `44` | 表头高度,单位 px。 |
91
+ | `height` | `number \| string` | `100%` | 表格最大高度。默认继承父容器高度;如果父容器没有可计算高度,则回落到`480px`。数据较少时默认按真实内容高度收缩,数据较多时不超过该高度。 |
92
+ | `layout` | `{ rowHeight?: number; headerHeight?: number }` | `{ rowHeight: 36, headerHeight: 40 }` | 表格布局尺寸配置。推荐把行高、表头高度等样式相关参数放在这里;自定义多行表头会以 `headerHeight` 为最小高度自动撑开。 |
93
+ | `rowHeight` | `number` | `36` | 已兼容保留,建议改用 `layout.rowHeight`。 |
94
+ | `headerHeight` | `number` | `40` | 已兼容保留,建议改用 `layout.headerHeight`。 |
95
95
  | `fixedHeader` | `boolean` | `true` | 表头是否固定在顶部。 |
96
96
  | `locale` | `'zh-CN' \| 'en-US' \| LocaleConfig` | `'zh-CN'` | 国际化配置。传字符串时使用内置中英文文案;传对象时可通过`language` 指定语言,并通过 `labels` 覆盖文案。 |
97
- | `verticalBorderless` | `boolean` | `false` | 是否弱化竖向边框。开启后隐藏单元格之间的竖线,但保留横向分隔线。 |
97
+ | `borderless` | `boolean` | `false` | 是否弱化竖向边框。开启后隐藏单元格之间的竖线,但保留横向分隔线。 |
98
+ | `verticalBorderless` | `boolean` | `false` | 已兼容保留,建议改用 `borderless`。 |
98
99
  | `striped` | `boolean \| string` | `false` | 是否显示斑马纹行背景。传`true` 使用主题变量,传颜色字符串可自定义斑马纹颜色,例如 `'#f6faff'`。 |
99
100
  | `highlight` | `{ editedCells?: boolean \| string; insertedRows?: boolean \| string }` | `{}` | 高亮反馈配置。`editedCells` 控制编辑过的单元格底色,`insertedRows` 控制乐观插入行底色;传 `true` 使用默认色,传颜色字符串可自定义。 |
100
- | `cellSpans` | `Array<{ rowKey?: GridKey; rowIndex?: number; columnKey: string; rowSpan?: number; colSpan?: number }>` | `[]` | 合并主体单元格配置。推荐使用 `rowKey` 定位;横向合并会限制在同一个固定列分区内,工具列不会参与合并。 |
101
+ | `cellSpans` | `TableCellSpan[] \| ((ctx) => TableCellSpan \| { rowSpan?: number; colSpan?: number } \| null)` | `[]` | 合并主体单元格配置。可传数组,也可传函数按单元格生成规则。推荐使用 `rowKey` 定位;横向合并会限制在同一个固定列分区内,工具列不会参与合并。 |
101
102
  | `loading` | `boolean` | `false` | 显示加载态。 |
102
103
  | `loadingContent` | `ReactNode` | - | 自定义加载内容。传入后,首次加载和刷新加载不再区分,都会统一展示这个内容。 |
103
104
  | `virtualized` | `boolean \| { enabled?: boolean; overscan?: number }` | `true` | 虚拟渲染配置。传`false` 时渲染全部行列;传对象时可通过 `enabled` 开关,并用 `overscan` 配置可见范围外的缓冲行列数量,默认 `4`。 |
@@ -169,6 +170,19 @@ export function Example() {
169
170
  />
170
171
  ```
171
172
 
173
+ 也可以传函数按单元格声明规则。函数返回值会默认使用当前单元格作为锚点,因此只需要返回跨度:
174
+
175
+ ```tsx
176
+ <Table
177
+ columns={columns}
178
+ rows={rows}
179
+ cellSpans={({ rowIndex, column }) => {
180
+ if (column.key === 'department' && rowIndex % 3 === 0) return { rowSpan: 2 };
181
+ return null;
182
+ }}
183
+ />
184
+ ```
185
+
172
186
  `rowKey` 优先于 `rowIndex`,在排序、过滤或插入删除行后更稳定。被合并覆盖的单元格不会单独绘制或命中,点击覆盖区域会选中合并区域的锚点单元格。横向合并不会跨越固定左列、滚动列、固定右列这三个区域;如果声明跨区,组件会自动收缩到当前区域内。
173
187
 
174
188
  ## Column 参数
@@ -177,6 +191,7 @@ export function Example() {
177
191
  | -------------- | -------------------------------------------------------------------------- | -------------------- | ------------------------------------------------------------------------------------------- |
178
192
  | `key` | `string` | 必填 | 列唯一标识。排序、筛选、选择、编辑回调都使用它。 |
179
193
  | `title` | `string` | 必填 | 表头展示文本。 |
194
+ | `renderHeader` | `(column) => ReactNode` | - | 自定义表头 React 内容。多行内容会自动撑开表头高度;`title` 仍用于 tooltip、拖拽预览和回退文本。 |
180
195
  | `dataIndex` | `keyof Row` | - | 从行数据中读取和写入的字段。工具列可不传。 |
181
196
  | `width` | `number` | `140` | 列宽,单位 px。最小宽度由内部布局保护。 |
182
197
  | `fixed` | `'left' \| 'right'` | - | 固定列位置。左/右固定列会覆盖滚动列并显示阴影。 |
package/dist/index.cjs CHANGED
@@ -814,8 +814,9 @@ function buildColumnMetrics(columns, columnDraggableOrOptions = false) {
814
814
  });
815
815
  const viewportWidth = options.viewportWidth ?? 0;
816
816
  if (viewportWidth <= left || columns.length === 0) return metrics;
817
- const stretchable = columns.map((column, index) => ({ column, index })).filter(({ column }) => column.fixed !== "right" && !column.rowSelection && !column.rowDragHandle && !column.rowNumber);
818
- const targets = stretchable.length > 0 ? stretchable : columns.map((column, index) => ({ column, index })).filter(({ column }) => column.fixed !== "right");
817
+ const unsetWidthStretchable = columns.map((column, index) => ({ column, index })).filter(({ column }) => column.width === void 0 && column.fixed === void 0 && !column.rowSelection && !column.rowDragHandle && !column.rowNumber);
818
+ const dataStretchable = columns.map((column, index) => ({ column, index })).filter(({ column }) => column.fixed === void 0 && !column.rowSelection && !column.rowDragHandle && !column.rowNumber);
819
+ const targets = unsetWidthStretchable.length > 0 ? unsetWidthStretchable : dataStretchable.length > 0 ? dataStretchable : [];
819
820
  if (targets.length === 0) return metrics;
820
821
  const extra = viewportWidth - left;
821
822
  const baseTotal = targets.reduce((sum, { index }) => sum + metrics[index].width, 0);
@@ -1069,7 +1070,7 @@ function paintGrid(options) {
1069
1070
  if (!verticalBorderless) ctx.fillRect(x + metric.width - 1, headerY, 1, headerHeight);
1070
1071
  ctx.fillRect(x, headerY + headerHeight - 1, metric.width, 1);
1071
1072
  const column = columns[columnIndex];
1072
- if (!column.rowSelection && !column.rowDragHandle && !column.rowNumber && column.title) {
1073
+ if (!column.rowSelection && !column.rowDragHandle && !column.rowNumber && !column.renderHeader && column.title) {
1073
1074
  ctx.font = "600 13px Inter, ui-sans-serif, system-ui, sans-serif";
1074
1075
  const titleWidth = ctx.measureText(column.title).width;
1075
1076
  const visibleActions = getVisibleHeaderActions(column, metric.width, columnDraggable, titleWidth);
@@ -1328,12 +1329,14 @@ function Table({
1328
1329
  rowKey,
1329
1330
  width = "100%",
1330
1331
  height = "100%",
1331
- autoHeight = false,
1332
- rowHeight = 36,
1333
- headerHeight = 40,
1332
+ autoHeight = true,
1333
+ layout,
1334
+ rowHeight: rowHeightProp,
1335
+ headerHeight: headerHeightProp,
1334
1336
  fixedHeader = true,
1335
1337
  locale = "zh-CN",
1336
- verticalBorderless = false,
1338
+ borderless,
1339
+ verticalBorderless,
1337
1340
  striped = false,
1338
1341
  highlight,
1339
1342
  cellSpans = [],
@@ -1375,8 +1378,13 @@ function Table({
1375
1378
  style,
1376
1379
  ariaLabel = "Data grid"
1377
1380
  }) {
1381
+ const rowHeight = layout?.rowHeight ?? rowHeightProp ?? 36;
1382
+ const baseHeaderHeight = layout?.headerHeight ?? headerHeightProp ?? 40;
1378
1383
  const { language, labels } = (0, import_react5.useMemo)(() => resolveGridLocale(locale), [locale]);
1379
- const hasVerticalBorders = !verticalBorderless;
1384
+ const customHeaderContentRefs = (0, import_react5.useRef)(/* @__PURE__ */ new Map());
1385
+ const [measuredHeaderHeight, setMeasuredHeaderHeight] = (0, import_react5.useState)(0);
1386
+ const headerHeight = Math.max(baseHeaderHeight, measuredHeaderHeight);
1387
+ const hasVerticalBorders = !(borderless ?? verticalBorderless ?? false);
1380
1388
  const hasStripedRows = Boolean(striped);
1381
1389
  const stripedColor = typeof striped === "string" ? striped : void 0;
1382
1390
  const editedCellsHighlight = highlight?.editedCells ?? false;
@@ -1482,6 +1490,28 @@ function Table({
1482
1490
  if (headerTooltipTimerRef.current !== null) clearTimeout(headerTooltipTimerRef.current);
1483
1491
  if (cellTooltipTimerRef.current !== null) clearTimeout(cellTooltipTimerRef.current);
1484
1492
  }, []);
1493
+ const measureCustomHeaderHeight = (0, import_react5.useCallback)(() => {
1494
+ let nextHeight = 0;
1495
+ customHeaderContentRefs.current.forEach((node) => {
1496
+ nextHeight = Math.max(nextHeight, Math.ceil(node.scrollHeight) + 16);
1497
+ });
1498
+ setMeasuredHeaderHeight((current) => current === nextHeight ? current : nextHeight);
1499
+ }, []);
1500
+ (0, import_react5.useLayoutEffect)(() => {
1501
+ const activeCustomHeaderKeys = new Set(columns.filter((column) => column.renderHeader).map((column) => column.key));
1502
+ customHeaderContentRefs.current.forEach((_, key) => {
1503
+ if (!activeCustomHeaderKeys.has(key)) customHeaderContentRefs.current.delete(key);
1504
+ });
1505
+ if (customHeaderContentRefs.current.size === 0) {
1506
+ setMeasuredHeaderHeight(0);
1507
+ return;
1508
+ }
1509
+ measureCustomHeaderHeight();
1510
+ if (typeof ResizeObserver === "undefined") return;
1511
+ const observer = new ResizeObserver(measureCustomHeaderHeight);
1512
+ customHeaderContentRefs.current.forEach((node) => observer.observe(node));
1513
+ return () => observer.disconnect();
1514
+ }, [columns, measureCustomHeaderHeight, viewport.width]);
1485
1515
  const [filterEditor, setFilterEditor] = (0, import_react5.useState)(null);
1486
1516
  const [contextMenu, setContextMenu] = (0, import_react5.useState)(null);
1487
1517
  const [insertCounts, setInsertCounts] = (0, import_react5.useState)({ before: 1, after: 1 });
@@ -1490,6 +1520,7 @@ function Table({
1490
1520
  const [insertBusy, setInsertBusy] = (0, import_react5.useState)(false);
1491
1521
  const [deleteBusy, setDeleteBusy] = (0, import_react5.useState)(false);
1492
1522
  const [toast, setToast] = (0, import_react5.useState)(null);
1523
+ const [resizeGuideX, setResizeGuideX] = (0, import_react5.useState)(null);
1493
1524
  const [hoveredHeaderAction, setHoveredHeaderAction] = (0, import_react5.useState)(null);
1494
1525
  const [visibleHeaderTooltip, setVisibleHeaderTooltip] = (0, import_react5.useState)(null);
1495
1526
  const [hoveredCellTooltip, setHoveredCellTooltip] = (0, import_react5.useState)(null);
@@ -1643,14 +1674,30 @@ function Table({
1643
1674
  rowIndexByKeyRef.current.set(key, index);
1644
1675
  return key;
1645
1676
  }, [getDataRowKey]);
1677
+ const resolvedCellSpans = (0, import_react5.useMemo)(() => {
1678
+ if (Array.isArray(cellSpans)) return cellSpans;
1679
+ const spans = [];
1680
+ rows.forEach((row, rowIndex) => {
1681
+ columns.forEach((column, columnIndex) => {
1682
+ const span = cellSpans({ row, rowIndex, column, columnIndex, rows, columns });
1683
+ if (!span) return;
1684
+ spans.push({
1685
+ rowIndex,
1686
+ columnKey: column.key,
1687
+ ...span
1688
+ });
1689
+ });
1690
+ });
1691
+ return spans;
1692
+ }, [cellSpans, columns, rows]);
1646
1693
  const cellSpanLookup = (0, import_react5.useMemo)(() => {
1647
- if (cellSpans.length === 0) return createEmptyCellSpanLookup();
1694
+ if (resolvedCellSpans.length === 0) return createEmptyCellSpanLookup();
1648
1695
  const rowIndexByKey = /* @__PURE__ */ new Map();
1649
1696
  rows.forEach((row, index) => rowIndexByKey.set(getRowKey(row, index), index));
1650
1697
  const columnIndexByKey = new Map(columns.map((column, index) => [column.key, index]));
1651
1698
  const lookup = createEmptyCellSpanLookup();
1652
1699
  const occupied = /* @__PURE__ */ new Set();
1653
- for (const span of cellSpans) {
1700
+ for (const span of resolvedCellSpans) {
1654
1701
  const rowIndex = span.rowKey !== void 0 ? rowIndexByKey.get(span.rowKey) : span.rowIndex;
1655
1702
  const columnIndex = columnIndexByKey.get(span.columnKey);
1656
1703
  if (rowIndex === void 0 || columnIndex === void 0) continue;
@@ -1686,7 +1733,7 @@ function Table({
1686
1733
  }
1687
1734
  }
1688
1735
  return lookup;
1689
- }, [cellSpans, columns, getRowKey, rows]);
1736
+ }, [columns, getRowKey, resolvedCellSpans, rows]);
1690
1737
  const selectedRowKeySet = (0, import_react5.useMemo)(() => new Set(rowKeys), [rowKeys]);
1691
1738
  const selectedColumnKeySet = (0, import_react5.useMemo)(() => new Set(columnKeys), [columnKeys]);
1692
1739
  const insertedRowKeySet = (0, import_react5.useMemo)(() => insertedRowKeys, [insertedRowKeys]);
@@ -1939,6 +1986,16 @@ function Table({
1939
1986
  }
1940
1987
  return closest;
1941
1988
  }, [columnResizable, columns, getDisplayedColumnLeft, metrics]);
1989
+ const findResizeNeighbor = (0, import_react5.useCallback)((columnIndex) => {
1990
+ const resizedColumn = columns[columnIndex];
1991
+ for (let index = columnIndex + 1; index < columns.length; index += 1) {
1992
+ const column = columns[index];
1993
+ if (column.rowSelection || column.rowDragHandle || column.rowNumber) continue;
1994
+ if (resizedColumn?.fixed && column.fixed !== resizedColumn.fixed) return -1;
1995
+ return index;
1996
+ }
1997
+ return -1;
1998
+ }, [columns]);
1942
1999
  const locateHeaderAction = (0, import_react5.useCallback)((clientX, columnIndex) => {
1943
2000
  if (columnIndex < 0 || !canvasRef.current) return null;
1944
2001
  const column = columns[columnIndex];
@@ -2517,26 +2574,48 @@ function Table({
2517
2574
  if (resizeIndex >= 0) {
2518
2575
  event.preventDefault();
2519
2576
  event.currentTarget.setPointerCapture(event.pointerId);
2520
- columnDragRef.current = { type: "resize", columnIndex: resizeIndex, startX: event.clientX, startWidth: metrics[resizeIndex].width };
2577
+ const neighborIndex = findResizeNeighbor(resizeIndex);
2578
+ const startGuideX = getDisplayedColumnLeft(resizeIndex) + metrics[resizeIndex].width;
2579
+ columnDragRef.current = {
2580
+ type: "resize",
2581
+ columnIndex: resizeIndex,
2582
+ startX: event.clientX,
2583
+ startWidth: metrics[resizeIndex].width,
2584
+ startGuideX,
2585
+ neighborIndex,
2586
+ startNeighborWidth: neighborIndex >= 0 ? metrics[neighborIndex].width : 0
2587
+ };
2521
2588
  suppressClickRef.current = true;
2522
- setDragGuide(getDisplayedColumnLeft(resizeIndex) + metrics[resizeIndex].width);
2589
+ setResizeGuideX(startGuideX);
2523
2590
  return;
2524
2591
  }
2525
- }, [fixedHeader, getDisplayedColumnLeft, headerHeight, locateResizeHandle, metrics, setDragGuide]);
2592
+ }, [findResizeNeighbor, fixedHeader, getDisplayedColumnLeft, headerHeight, locateResizeHandle, metrics]);
2526
2593
  const handleColumnPointerMove = (0, import_react5.useCallback)((event) => {
2527
2594
  const drag = columnDragRef.current;
2528
2595
  if (!drag) return;
2529
- const width2 = Math.max(getMinimumColumnWidth(columns[drag.columnIndex], columnDraggable), Math.round(drag.startWidth + event.clientX - drag.startX));
2530
- onColumnResize?.(columns[drag.columnIndex].key, width2);
2531
- setDragGuide(getDisplayedColumnLeft(drag.columnIndex) + width2);
2532
- }, [columnDraggable, columns, getDisplayedColumnLeft, onColumnResize, setDragGuide]);
2596
+ const column = columns[drag.columnIndex];
2597
+ const minWidth = getMinimumColumnWidth(column, columnDraggable);
2598
+ let delta = Math.round(event.clientX - drag.startX);
2599
+ if (drag.neighborIndex >= 0) {
2600
+ const neighbor = columns[drag.neighborIndex];
2601
+ const minNeighborWidth = getMinimumColumnWidth(neighbor, columnDraggable);
2602
+ delta = Math.max(minWidth - drag.startWidth, Math.min(drag.startNeighborWidth - minNeighborWidth, delta));
2603
+ onColumnResize?.(column.key, drag.startWidth + delta);
2604
+ onColumnResize?.(neighbor.key, drag.startNeighborWidth - delta);
2605
+ setResizeGuideX(drag.startGuideX + delta);
2606
+ return;
2607
+ }
2608
+ const width2 = Math.max(minWidth, Math.round(drag.startWidth + delta));
2609
+ onColumnResize?.(column.key, width2);
2610
+ setResizeGuideX(drag.startGuideX + width2 - drag.startWidth);
2611
+ }, [columnDraggable, columns, onColumnResize]);
2533
2612
  const handleColumnPointerUp = (0, import_react5.useCallback)((event) => {
2534
2613
  const drag = columnDragRef.current;
2535
2614
  if (!drag) return;
2536
2615
  if (event.currentTarget.hasPointerCapture(event.pointerId)) event.currentTarget.releasePointerCapture(event.pointerId);
2537
2616
  columnDragRef.current = null;
2538
- setDragGuide(null);
2539
- }, [setDragGuide]);
2617
+ setResizeGuideX(null);
2618
+ }, []);
2540
2619
  const queueSortStateChange = (0, import_react5.useCallback)((next) => {
2541
2620
  setDisplaySortState(next);
2542
2621
  if (sortDebounceRef.current !== null) clearTimeout(sortDebounceRef.current);
@@ -2943,6 +3022,35 @@ function Table({
2943
3022
  column.key
2944
3023
  );
2945
3024
  }
3025
+ if (column.renderHeader) {
3026
+ const visibleActions = getVisibleHeaderActions(column, metrics[columnIndex].width, columnDraggable, measureHeaderTitleWidth(columnIndex));
3027
+ const actionWidth = (Number(visibleActions.drag) + Number(visibleActions.filter) + Number(visibleActions.sort)) * 18;
3028
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3029
+ "div",
3030
+ {
3031
+ className: "rvg-header-title is-custom",
3032
+ style: {
3033
+ left,
3034
+ top: fixedHeader ? 0 : -currentScrollTop,
3035
+ width: Math.max(0, metrics[columnIndex].width - actionWidth),
3036
+ height: headerHeight,
3037
+ justifyContent: column.align === "right" ? "flex-end" : column.align === "center" ? "center" : "flex-start",
3038
+ textAlign: column.align === "right" ? "right" : column.align === "center" ? "center" : "left"
3039
+ },
3040
+ children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3041
+ "span",
3042
+ {
3043
+ ref: (node) => {
3044
+ if (node) customHeaderContentRefs.current.set(column.key, node);
3045
+ else customHeaderContentRefs.current.delete(column.key);
3046
+ },
3047
+ children: column.renderHeader(column)
3048
+ }
3049
+ )
3050
+ },
3051
+ column.key
3052
+ );
3053
+ }
2946
3054
  return null;
2947
3055
  };
2948
3056
  const rangeHandlePosition = (() => {
@@ -3646,6 +3754,13 @@ function Table({
3646
3754
  ] }) }),
3647
3755
  toast && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: `rvg-toast is-${toast.type}`, role: "status", children: toast.message }),
3648
3756
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { ref: dragGuideRef, className: "rvg-column-guide" }),
3757
+ resizeGuideX !== null && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3758
+ "div",
3759
+ {
3760
+ className: "rvg-column-guide is-resizing",
3761
+ style: { display: "block", transform: `translateX(${Math.round(resizeGuideX)}px)` }
3762
+ }
3763
+ ),
3649
3764
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { ref: rowDragGuideRef, className: "rvg-row-guide" }),
3650
3765
  filterEditor && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { ref: filterRef, className: "rvg-filter", style: { left: filterEditor.left, top: filterEditor.top }, children: [
3651
3766
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
package/dist/index.css CHANGED
@@ -150,6 +150,15 @@
150
150
  white-space: nowrap;
151
151
  user-select: text;
152
152
  }
153
+ .rvg-header-title.is-custom {
154
+ overflow: visible;
155
+ white-space: normal;
156
+ }
157
+ .rvg-header-title.is-custom > span {
158
+ overflow: visible;
159
+ white-space: normal;
160
+ text-overflow: clip;
161
+ }
153
162
  .rvg-row-drag-icon {
154
163
  position: absolute;
155
164
  display: grid;
@@ -1154,14 +1163,18 @@
1154
1163
  z-index: 6;
1155
1164
  background: var(--rvg-color-bg);
1156
1165
  }
1166
+ .rvg-choice-editor {
1167
+ line-height: 1;
1168
+ }
1157
1169
  .rvg-time-editor {
1158
1170
  position: absolute;
1159
1171
  z-index: 6;
1160
1172
  display: flex;
1161
- padding: 0 28px 0 8px;
1173
+ padding: 0 28px 0 9px;
1162
1174
  align-items: center;
1163
1175
  gap: 3px;
1164
1176
  background: var(--rvg-color-bg);
1177
+ line-height: 1;
1165
1178
  }
1166
1179
  .rvg-time-field-group {
1167
1180
  display: contents;
@@ -1169,6 +1182,7 @@
1169
1182
  .rvg-time-editor input {
1170
1183
  min-width: 0;
1171
1184
  height: 100%;
1185
+ transform: translateY(-1px);
1172
1186
  flex: 1;
1173
1187
  padding: 0;
1174
1188
  border: 0;
@@ -1180,17 +1194,18 @@
1180
1194
  ui-sans-serif,
1181
1195
  system-ui,
1182
1196
  sans-serif;
1183
- line-height: 1;
1197
+ line-height: normal;
1184
1198
  }
1185
1199
  .rvg-native-editor {
1186
1200
  position: absolute;
1187
1201
  z-index: 6;
1188
1202
  display: flex;
1189
- padding: 0 8px;
1203
+ padding: 0 9px;
1190
1204
  align-items: center;
1191
1205
  gap: 6px;
1192
1206
  overflow: hidden;
1193
1207
  background: var(--rvg-color-bg);
1208
+ line-height: 1;
1194
1209
  }
1195
1210
  .rvg-native-editor.has-clear {
1196
1211
  padding-right: 32px;
@@ -1198,18 +1213,20 @@
1198
1213
  .rvg-native-editor input {
1199
1214
  min-width: 0;
1200
1215
  width: 100%;
1201
- height: 20px;
1216
+ height: 100%;
1202
1217
  padding: 0;
1218
+ transform: translateY(-1px);
1203
1219
  flex: 1;
1204
1220
  border: 0;
1205
1221
  outline: 0;
1206
1222
  background: transparent;
1207
1223
  color: var(--rvg-color-text);
1208
1224
  font:
1209
- 13px/20px Inter,
1225
+ 13px Inter,
1210
1226
  ui-sans-serif,
1211
1227
  system-ui,
1212
1228
  sans-serif;
1229
+ line-height: normal;
1213
1230
  box-sizing: border-box;
1214
1231
  }
1215
1232
  .rvg-native-editor span {
@@ -1448,13 +1465,14 @@
1448
1465
  }
1449
1466
  .rvg-choice-editor [data-part=trigger] {
1450
1467
  padding: 0 28px 0 9px;
1468
+ line-height: 1;
1451
1469
  }
1452
1470
  .rvg-date-value {
1453
- padding-top: 0;
1454
- padding-bottom: 2px;
1471
+ padding: 0 8px 2px 9px;
1455
1472
  }
1456
1473
  .rvg-choice-editor [data-part=value-text] {
1457
1474
  min-width: 0;
1475
+ transform: translateY(-1px);
1458
1476
  overflow: hidden;
1459
1477
  font: inherit;
1460
1478
  line-height: normal;
package/dist/index.d.cts CHANGED
@@ -116,6 +116,12 @@ interface TableHighlightConfig {
116
116
  /** Highlight rows inserted optimistically through the built-in insert action. Pass a color string to customize the fill. Defaults to false. */
117
117
  insertedRows?: boolean | string;
118
118
  }
119
+ interface TableLayoutConfig {
120
+ /** Fixed body row height in pixels. Defaults to 36. */
121
+ rowHeight?: number;
122
+ /** Header height in pixels. Defaults to 40. */
123
+ headerHeight?: number;
124
+ }
119
125
  interface TableCellSpan {
120
126
  /** Row index in the current rows array. Prefer rowKey when row order can change. */
121
127
  rowIndex?: number;
@@ -128,6 +134,16 @@ interface TableCellSpan {
128
134
  /** Number of columns covered by the merged cell. Defaults to 1. */
129
135
  colSpan?: number;
130
136
  }
137
+ interface TableCellSpanRuleContext<Row extends object> {
138
+ row: Row;
139
+ rowIndex: number;
140
+ column: GridColumn<Row>;
141
+ columnIndex: number;
142
+ rows: Row[];
143
+ columns: GridColumn<Row>[];
144
+ }
145
+ type TableCellSpanRule<Row extends object> = (context: TableCellSpanRuleContext<Row>) => TableCellSpan | Omit<TableCellSpan, 'rowIndex' | 'rowKey' | 'columnKey'> | false | null | undefined;
146
+ type TableCellSpans<Row extends object> = TableCellSpan[] | TableCellSpanRule<Row>;
131
147
  interface TableResolvedCellSpan {
132
148
  rowIndex: number;
133
149
  columnIndex: number;
@@ -149,6 +165,8 @@ interface GridColumn<Row> {
149
165
  key: string;
150
166
  /** Header label painted in the text layer. */
151
167
  title: string;
168
+ /** Custom React content rendered in the header DOM overlay. */
169
+ renderHeader?: (column: GridColumn<Row>) => ReactNode;
152
170
  /** Row field used as this column's cell value. Omit for utility columns. */
153
171
  dataIndex?: keyof Row;
154
172
  /** Preferred column width in pixels. Layout helpers enforce minimums. */
@@ -282,20 +300,26 @@ interface TableProps<Row extends object> {
282
300
  width?: number | string;
283
301
  /** Grid height. Defaults to 100%; falls back to 480px when the parent has no height. */
284
302
  height?: number | string;
285
- /** Shrink the grid to its real content height when there are fewer rows than the configured height can display. */
303
+ /** Shrink the grid to its real content height when there are fewer rows than the configured max height can display. Defaults to true. */
286
304
  autoHeight?: boolean;
305
+ /** Layout and sizing configuration. Prefer this over rowHeight/headerHeight for new code. */
306
+ layout?: TableLayoutConfig;
307
+ /** @deprecated Use layout.rowHeight instead. */
287
308
  rowHeight?: number;
309
+ /** @deprecated Use layout.headerHeight instead. */
288
310
  headerHeight?: number;
289
311
  fixedHeader?: boolean;
290
312
  locale?: TableLocaleConfig;
291
- /** Hide vertical grid lines while keeping horizontal row separators. */
313
+ /** Hide vertical grid lines while keeping horizontal row separators. Prefer borderless for new code. */
314
+ borderless?: boolean;
315
+ /** @deprecated Use borderless instead. */
292
316
  verticalBorderless?: boolean;
293
317
  /** Paint alternating row backgrounds. Pass a color string to customize the stripe color. */
294
318
  striped?: boolean | string;
295
319
  /** Visual highlight feedback configuration. All entries default to false. */
296
320
  highlight?: TableHighlightConfig;
297
321
  /** Merged body cell configuration. Utility columns are ignored and horizontal spans are limited to the same fixed column region. */
298
- cellSpans?: TableCellSpan[];
322
+ cellSpans?: TableCellSpans<Row>;
299
323
  loading?: boolean;
300
324
  /** Custom loading content. When provided, it is used for both initial loading and refresh loading. */
301
325
  loadingContent?: ReactNode;
@@ -348,7 +372,7 @@ interface ViewportRange {
348
372
  columnEnd: number;
349
373
  }
350
374
 
351
- declare function Table<Row extends object>({ columns: sourceColumns, rows: sourceRows, rowKey, width, height, autoHeight, rowHeight, headerHeight, fixedHeader, locale, verticalBorderless, striped, highlight, cellSpans, loading, loadingContent, virtualized, tooltip, summary, rowNumber, selectedCell, defaultSelectedCell, onSelectedCellChange, rangeSelection, rowSelection, selectedRowKeys, defaultSelectedRowKeys, onSelectedRowKeysChange, columnSelection, selectedColumnKeys, defaultSelectedColumnKeys, onSelectedColumnKeysChange, columnDraggable, columnResizable, onColumnOrderChange, onColumnResize, rowDraggable, onRowOrderChange, onInsertRows, onDeleteRows, sortState, onSortStateChange, filterValues, onFilterValuesChange, onCellChange, onCellContextMenu, contextMenu: contextMenuConfig, emptyContent, className, style, ariaLabel, }: TableProps<Row>): react.JSX.Element;
375
+ declare function Table<Row extends object>({ columns: sourceColumns, rows: sourceRows, rowKey, width, height, autoHeight, layout, rowHeight: rowHeightProp, headerHeight: headerHeightProp, fixedHeader, locale, borderless, verticalBorderless, striped, highlight, cellSpans, loading, loadingContent, virtualized, tooltip, summary, rowNumber, selectedCell, defaultSelectedCell, onSelectedCellChange, rangeSelection, rowSelection, selectedRowKeys, defaultSelectedRowKeys, onSelectedRowKeysChange, columnSelection, selectedColumnKeys, defaultSelectedColumnKeys, onSelectedColumnKeysChange, columnDraggable, columnResizable, onColumnOrderChange, onColumnResize, rowDraggable, onRowOrderChange, onInsertRows, onDeleteRows, sortState, onSortStateChange, filterValues, onFilterValuesChange, onCellChange, onCellContextMenu, contextMenu: contextMenuConfig, emptyContent, className, style, ariaLabel, }: TableProps<Row>): react.JSX.Element;
352
376
 
353
377
  type LayoutColumn<Row> = GridColumn<Row> & {
354
378
  rowSelection?: boolean;
@@ -395,4 +419,4 @@ declare function hitTestColumn(metrics: ColumnMetric[], contentX: number): numbe
395
419
 
396
420
  type TableFilterValues = Record<string, string>;
397
421
 
398
- export { type AxisSelectionConfig, type CellChange, type ContextMenuEvent, type DeleteRowsEvent, type GridCellPosition, type GridColumn, type GridKey, type GridSelection, type GridSortState, type InsertRowsEvent, type SelectionMode, type SortDirection, Table, type CellChange as TableCellChange, type GridCellPosition as TableCellPosition, type TableCellSpan, type GridColumn as TableColumn, type TableContextMenu, type TableContextMenuConfig, type ContextMenuEvent as TableContextMenuEvent, type DeleteRowsEvent as TableDeleteRowsEvent, type GridSelection as TableEditingCell, type TableFilterValues, type TableHighlightConfig, type InsertRowsEvent as TableInsertRowsEvent, type GridKey as TableKey, type TableLabels, type TableLocaleCode, type TableLocaleConfig, type TableProps, type TableResolvedCellSpan, type GridSortState as TableSorter, type TableSummaryConfig, type TableTooltipConfig, type ViewportRange as TableViewportRange, type TableVirtualizedConfig, Table as VelocityGrid, type TableCellSpan as VelocityGridCellSpan, type TableContextMenu as VelocityGridContextMenu, type TableContextMenuConfig as VelocityGridContextMenuConfig, type TableHighlightConfig as VelocityGridHighlightConfig, type TableLabels as VelocityGridLabels, type TableLocaleCode as VelocityGridLocaleCode, type TableLocaleConfig as VelocityGridLocaleConfig, type TableProps as VelocityGridProps, type TableResolvedCellSpan as VelocityGridResolvedCellSpan, type TableSummaryConfig as VelocityGridSummaryConfig, type TableTooltipConfig as VelocityGridTooltipConfig, type TableVirtualizedConfig as VelocityGridVirtualizedConfig, type ViewportRange, buildColumnMetrics, getViewportRange, hitTestColumn };
422
+ export { type AxisSelectionConfig, type CellChange, type ContextMenuEvent, type DeleteRowsEvent, type GridCellPosition, type GridColumn, type GridKey, type GridSelection, type GridSortState, type InsertRowsEvent, type SelectionMode, type SortDirection, Table, type CellChange as TableCellChange, type GridCellPosition as TableCellPosition, type TableCellSpan, type TableCellSpanRule, type TableCellSpanRuleContext, type TableCellSpans, type GridColumn as TableColumn, type TableContextMenu, type TableContextMenuConfig, type ContextMenuEvent as TableContextMenuEvent, type DeleteRowsEvent as TableDeleteRowsEvent, type GridSelection as TableEditingCell, type TableFilterValues, type TableHighlightConfig, type InsertRowsEvent as TableInsertRowsEvent, type GridKey as TableKey, type TableLabels, type TableLayoutConfig, type TableLocaleCode, type TableLocaleConfig, type TableProps, type TableResolvedCellSpan, type GridSortState as TableSorter, type TableSummaryConfig, type TableTooltipConfig, type ViewportRange as TableViewportRange, type TableVirtualizedConfig, Table as VelocityGrid, type TableCellSpan as VelocityGridCellSpan, type TableCellSpanRule as VelocityGridCellSpanRule, type TableCellSpanRuleContext as VelocityGridCellSpanRuleContext, type TableCellSpans as VelocityGridCellSpans, type TableContextMenu as VelocityGridContextMenu, type TableContextMenuConfig as VelocityGridContextMenuConfig, type TableHighlightConfig as VelocityGridHighlightConfig, type TableLabels as VelocityGridLabels, type TableLayoutConfig as VelocityGridLayoutConfig, type TableLocaleCode as VelocityGridLocaleCode, type TableLocaleConfig as VelocityGridLocaleConfig, type TableProps as VelocityGridProps, type TableResolvedCellSpan as VelocityGridResolvedCellSpan, type TableSummaryConfig as VelocityGridSummaryConfig, type TableTooltipConfig as VelocityGridTooltipConfig, type TableVirtualizedConfig as VelocityGridVirtualizedConfig, type ViewportRange, buildColumnMetrics, getViewportRange, hitTestColumn };
package/dist/index.d.ts CHANGED
@@ -116,6 +116,12 @@ interface TableHighlightConfig {
116
116
  /** Highlight rows inserted optimistically through the built-in insert action. Pass a color string to customize the fill. Defaults to false. */
117
117
  insertedRows?: boolean | string;
118
118
  }
119
+ interface TableLayoutConfig {
120
+ /** Fixed body row height in pixels. Defaults to 36. */
121
+ rowHeight?: number;
122
+ /** Header height in pixels. Defaults to 40. */
123
+ headerHeight?: number;
124
+ }
119
125
  interface TableCellSpan {
120
126
  /** Row index in the current rows array. Prefer rowKey when row order can change. */
121
127
  rowIndex?: number;
@@ -128,6 +134,16 @@ interface TableCellSpan {
128
134
  /** Number of columns covered by the merged cell. Defaults to 1. */
129
135
  colSpan?: number;
130
136
  }
137
+ interface TableCellSpanRuleContext<Row extends object> {
138
+ row: Row;
139
+ rowIndex: number;
140
+ column: GridColumn<Row>;
141
+ columnIndex: number;
142
+ rows: Row[];
143
+ columns: GridColumn<Row>[];
144
+ }
145
+ type TableCellSpanRule<Row extends object> = (context: TableCellSpanRuleContext<Row>) => TableCellSpan | Omit<TableCellSpan, 'rowIndex' | 'rowKey' | 'columnKey'> | false | null | undefined;
146
+ type TableCellSpans<Row extends object> = TableCellSpan[] | TableCellSpanRule<Row>;
131
147
  interface TableResolvedCellSpan {
132
148
  rowIndex: number;
133
149
  columnIndex: number;
@@ -149,6 +165,8 @@ interface GridColumn<Row> {
149
165
  key: string;
150
166
  /** Header label painted in the text layer. */
151
167
  title: string;
168
+ /** Custom React content rendered in the header DOM overlay. */
169
+ renderHeader?: (column: GridColumn<Row>) => ReactNode;
152
170
  /** Row field used as this column's cell value. Omit for utility columns. */
153
171
  dataIndex?: keyof Row;
154
172
  /** Preferred column width in pixels. Layout helpers enforce minimums. */
@@ -282,20 +300,26 @@ interface TableProps<Row extends object> {
282
300
  width?: number | string;
283
301
  /** Grid height. Defaults to 100%; falls back to 480px when the parent has no height. */
284
302
  height?: number | string;
285
- /** Shrink the grid to its real content height when there are fewer rows than the configured height can display. */
303
+ /** Shrink the grid to its real content height when there are fewer rows than the configured max height can display. Defaults to true. */
286
304
  autoHeight?: boolean;
305
+ /** Layout and sizing configuration. Prefer this over rowHeight/headerHeight for new code. */
306
+ layout?: TableLayoutConfig;
307
+ /** @deprecated Use layout.rowHeight instead. */
287
308
  rowHeight?: number;
309
+ /** @deprecated Use layout.headerHeight instead. */
288
310
  headerHeight?: number;
289
311
  fixedHeader?: boolean;
290
312
  locale?: TableLocaleConfig;
291
- /** Hide vertical grid lines while keeping horizontal row separators. */
313
+ /** Hide vertical grid lines while keeping horizontal row separators. Prefer borderless for new code. */
314
+ borderless?: boolean;
315
+ /** @deprecated Use borderless instead. */
292
316
  verticalBorderless?: boolean;
293
317
  /** Paint alternating row backgrounds. Pass a color string to customize the stripe color. */
294
318
  striped?: boolean | string;
295
319
  /** Visual highlight feedback configuration. All entries default to false. */
296
320
  highlight?: TableHighlightConfig;
297
321
  /** Merged body cell configuration. Utility columns are ignored and horizontal spans are limited to the same fixed column region. */
298
- cellSpans?: TableCellSpan[];
322
+ cellSpans?: TableCellSpans<Row>;
299
323
  loading?: boolean;
300
324
  /** Custom loading content. When provided, it is used for both initial loading and refresh loading. */
301
325
  loadingContent?: ReactNode;
@@ -348,7 +372,7 @@ interface ViewportRange {
348
372
  columnEnd: number;
349
373
  }
350
374
 
351
- declare function Table<Row extends object>({ columns: sourceColumns, rows: sourceRows, rowKey, width, height, autoHeight, rowHeight, headerHeight, fixedHeader, locale, verticalBorderless, striped, highlight, cellSpans, loading, loadingContent, virtualized, tooltip, summary, rowNumber, selectedCell, defaultSelectedCell, onSelectedCellChange, rangeSelection, rowSelection, selectedRowKeys, defaultSelectedRowKeys, onSelectedRowKeysChange, columnSelection, selectedColumnKeys, defaultSelectedColumnKeys, onSelectedColumnKeysChange, columnDraggable, columnResizable, onColumnOrderChange, onColumnResize, rowDraggable, onRowOrderChange, onInsertRows, onDeleteRows, sortState, onSortStateChange, filterValues, onFilterValuesChange, onCellChange, onCellContextMenu, contextMenu: contextMenuConfig, emptyContent, className, style, ariaLabel, }: TableProps<Row>): react.JSX.Element;
375
+ declare function Table<Row extends object>({ columns: sourceColumns, rows: sourceRows, rowKey, width, height, autoHeight, layout, rowHeight: rowHeightProp, headerHeight: headerHeightProp, fixedHeader, locale, borderless, verticalBorderless, striped, highlight, cellSpans, loading, loadingContent, virtualized, tooltip, summary, rowNumber, selectedCell, defaultSelectedCell, onSelectedCellChange, rangeSelection, rowSelection, selectedRowKeys, defaultSelectedRowKeys, onSelectedRowKeysChange, columnSelection, selectedColumnKeys, defaultSelectedColumnKeys, onSelectedColumnKeysChange, columnDraggable, columnResizable, onColumnOrderChange, onColumnResize, rowDraggable, onRowOrderChange, onInsertRows, onDeleteRows, sortState, onSortStateChange, filterValues, onFilterValuesChange, onCellChange, onCellContextMenu, contextMenu: contextMenuConfig, emptyContent, className, style, ariaLabel, }: TableProps<Row>): react.JSX.Element;
352
376
 
353
377
  type LayoutColumn<Row> = GridColumn<Row> & {
354
378
  rowSelection?: boolean;
@@ -395,4 +419,4 @@ declare function hitTestColumn(metrics: ColumnMetric[], contentX: number): numbe
395
419
 
396
420
  type TableFilterValues = Record<string, string>;
397
421
 
398
- export { type AxisSelectionConfig, type CellChange, type ContextMenuEvent, type DeleteRowsEvent, type GridCellPosition, type GridColumn, type GridKey, type GridSelection, type GridSortState, type InsertRowsEvent, type SelectionMode, type SortDirection, Table, type CellChange as TableCellChange, type GridCellPosition as TableCellPosition, type TableCellSpan, type GridColumn as TableColumn, type TableContextMenu, type TableContextMenuConfig, type ContextMenuEvent as TableContextMenuEvent, type DeleteRowsEvent as TableDeleteRowsEvent, type GridSelection as TableEditingCell, type TableFilterValues, type TableHighlightConfig, type InsertRowsEvent as TableInsertRowsEvent, type GridKey as TableKey, type TableLabels, type TableLocaleCode, type TableLocaleConfig, type TableProps, type TableResolvedCellSpan, type GridSortState as TableSorter, type TableSummaryConfig, type TableTooltipConfig, type ViewportRange as TableViewportRange, type TableVirtualizedConfig, Table as VelocityGrid, type TableCellSpan as VelocityGridCellSpan, type TableContextMenu as VelocityGridContextMenu, type TableContextMenuConfig as VelocityGridContextMenuConfig, type TableHighlightConfig as VelocityGridHighlightConfig, type TableLabels as VelocityGridLabels, type TableLocaleCode as VelocityGridLocaleCode, type TableLocaleConfig as VelocityGridLocaleConfig, type TableProps as VelocityGridProps, type TableResolvedCellSpan as VelocityGridResolvedCellSpan, type TableSummaryConfig as VelocityGridSummaryConfig, type TableTooltipConfig as VelocityGridTooltipConfig, type TableVirtualizedConfig as VelocityGridVirtualizedConfig, type ViewportRange, buildColumnMetrics, getViewportRange, hitTestColumn };
422
+ export { type AxisSelectionConfig, type CellChange, type ContextMenuEvent, type DeleteRowsEvent, type GridCellPosition, type GridColumn, type GridKey, type GridSelection, type GridSortState, type InsertRowsEvent, type SelectionMode, type SortDirection, Table, type CellChange as TableCellChange, type GridCellPosition as TableCellPosition, type TableCellSpan, type TableCellSpanRule, type TableCellSpanRuleContext, type TableCellSpans, type GridColumn as TableColumn, type TableContextMenu, type TableContextMenuConfig, type ContextMenuEvent as TableContextMenuEvent, type DeleteRowsEvent as TableDeleteRowsEvent, type GridSelection as TableEditingCell, type TableFilterValues, type TableHighlightConfig, type InsertRowsEvent as TableInsertRowsEvent, type GridKey as TableKey, type TableLabels, type TableLayoutConfig, type TableLocaleCode, type TableLocaleConfig, type TableProps, type TableResolvedCellSpan, type GridSortState as TableSorter, type TableSummaryConfig, type TableTooltipConfig, type ViewportRange as TableViewportRange, type TableVirtualizedConfig, Table as VelocityGrid, type TableCellSpan as VelocityGridCellSpan, type TableCellSpanRule as VelocityGridCellSpanRule, type TableCellSpanRuleContext as VelocityGridCellSpanRuleContext, type TableCellSpans as VelocityGridCellSpans, type TableContextMenu as VelocityGridContextMenu, type TableContextMenuConfig as VelocityGridContextMenuConfig, type TableHighlightConfig as VelocityGridHighlightConfig, type TableLabels as VelocityGridLabels, type TableLayoutConfig as VelocityGridLayoutConfig, type TableLocaleCode as VelocityGridLocaleCode, type TableLocaleConfig as VelocityGridLocaleConfig, type TableProps as VelocityGridProps, type TableResolvedCellSpan as VelocityGridResolvedCellSpan, type TableSummaryConfig as VelocityGridSummaryConfig, type TableTooltipConfig as VelocityGridTooltipConfig, type TableVirtualizedConfig as VelocityGridVirtualizedConfig, type ViewportRange, buildColumnMetrics, getViewportRange, hitTestColumn };
package/dist/index.js CHANGED
@@ -784,8 +784,9 @@ function buildColumnMetrics(columns, columnDraggableOrOptions = false) {
784
784
  });
785
785
  const viewportWidth = options.viewportWidth ?? 0;
786
786
  if (viewportWidth <= left || columns.length === 0) return metrics;
787
- const stretchable = columns.map((column, index) => ({ column, index })).filter(({ column }) => column.fixed !== "right" && !column.rowSelection && !column.rowDragHandle && !column.rowNumber);
788
- const targets = stretchable.length > 0 ? stretchable : columns.map((column, index) => ({ column, index })).filter(({ column }) => column.fixed !== "right");
787
+ const unsetWidthStretchable = columns.map((column, index) => ({ column, index })).filter(({ column }) => column.width === void 0 && column.fixed === void 0 && !column.rowSelection && !column.rowDragHandle && !column.rowNumber);
788
+ const dataStretchable = columns.map((column, index) => ({ column, index })).filter(({ column }) => column.fixed === void 0 && !column.rowSelection && !column.rowDragHandle && !column.rowNumber);
789
+ const targets = unsetWidthStretchable.length > 0 ? unsetWidthStretchable : dataStretchable.length > 0 ? dataStretchable : [];
789
790
  if (targets.length === 0) return metrics;
790
791
  const extra = viewportWidth - left;
791
792
  const baseTotal = targets.reduce((sum, { index }) => sum + metrics[index].width, 0);
@@ -1039,7 +1040,7 @@ function paintGrid(options) {
1039
1040
  if (!verticalBorderless) ctx.fillRect(x + metric.width - 1, headerY, 1, headerHeight);
1040
1041
  ctx.fillRect(x, headerY + headerHeight - 1, metric.width, 1);
1041
1042
  const column = columns[columnIndex];
1042
- if (!column.rowSelection && !column.rowDragHandle && !column.rowNumber && column.title) {
1043
+ if (!column.rowSelection && !column.rowDragHandle && !column.rowNumber && !column.renderHeader && column.title) {
1043
1044
  ctx.font = "600 13px Inter, ui-sans-serif, system-ui, sans-serif";
1044
1045
  const titleWidth = ctx.measureText(column.title).width;
1045
1046
  const visibleActions = getVisibleHeaderActions(column, metric.width, columnDraggable, titleWidth);
@@ -1298,12 +1299,14 @@ function Table({
1298
1299
  rowKey,
1299
1300
  width = "100%",
1300
1301
  height = "100%",
1301
- autoHeight = false,
1302
- rowHeight = 36,
1303
- headerHeight = 40,
1302
+ autoHeight = true,
1303
+ layout,
1304
+ rowHeight: rowHeightProp,
1305
+ headerHeight: headerHeightProp,
1304
1306
  fixedHeader = true,
1305
1307
  locale = "zh-CN",
1306
- verticalBorderless = false,
1308
+ borderless,
1309
+ verticalBorderless,
1307
1310
  striped = false,
1308
1311
  highlight,
1309
1312
  cellSpans = [],
@@ -1345,8 +1348,13 @@ function Table({
1345
1348
  style,
1346
1349
  ariaLabel = "Data grid"
1347
1350
  }) {
1351
+ const rowHeight = layout?.rowHeight ?? rowHeightProp ?? 36;
1352
+ const baseHeaderHeight = layout?.headerHeight ?? headerHeightProp ?? 40;
1348
1353
  const { language, labels } = useMemo(() => resolveGridLocale(locale), [locale]);
1349
- const hasVerticalBorders = !verticalBorderless;
1354
+ const customHeaderContentRefs = useRef2(/* @__PURE__ */ new Map());
1355
+ const [measuredHeaderHeight, setMeasuredHeaderHeight] = useState4(0);
1356
+ const headerHeight = Math.max(baseHeaderHeight, measuredHeaderHeight);
1357
+ const hasVerticalBorders = !(borderless ?? verticalBorderless ?? false);
1350
1358
  const hasStripedRows = Boolean(striped);
1351
1359
  const stripedColor = typeof striped === "string" ? striped : void 0;
1352
1360
  const editedCellsHighlight = highlight?.editedCells ?? false;
@@ -1452,6 +1460,28 @@ function Table({
1452
1460
  if (headerTooltipTimerRef.current !== null) clearTimeout(headerTooltipTimerRef.current);
1453
1461
  if (cellTooltipTimerRef.current !== null) clearTimeout(cellTooltipTimerRef.current);
1454
1462
  }, []);
1463
+ const measureCustomHeaderHeight = useCallback2(() => {
1464
+ let nextHeight = 0;
1465
+ customHeaderContentRefs.current.forEach((node) => {
1466
+ nextHeight = Math.max(nextHeight, Math.ceil(node.scrollHeight) + 16);
1467
+ });
1468
+ setMeasuredHeaderHeight((current) => current === nextHeight ? current : nextHeight);
1469
+ }, []);
1470
+ useLayoutEffect2(() => {
1471
+ const activeCustomHeaderKeys = new Set(columns.filter((column) => column.renderHeader).map((column) => column.key));
1472
+ customHeaderContentRefs.current.forEach((_, key) => {
1473
+ if (!activeCustomHeaderKeys.has(key)) customHeaderContentRefs.current.delete(key);
1474
+ });
1475
+ if (customHeaderContentRefs.current.size === 0) {
1476
+ setMeasuredHeaderHeight(0);
1477
+ return;
1478
+ }
1479
+ measureCustomHeaderHeight();
1480
+ if (typeof ResizeObserver === "undefined") return;
1481
+ const observer = new ResizeObserver(measureCustomHeaderHeight);
1482
+ customHeaderContentRefs.current.forEach((node) => observer.observe(node));
1483
+ return () => observer.disconnect();
1484
+ }, [columns, measureCustomHeaderHeight, viewport.width]);
1455
1485
  const [filterEditor, setFilterEditor] = useState4(null);
1456
1486
  const [contextMenu, setContextMenu] = useState4(null);
1457
1487
  const [insertCounts, setInsertCounts] = useState4({ before: 1, after: 1 });
@@ -1460,6 +1490,7 @@ function Table({
1460
1490
  const [insertBusy, setInsertBusy] = useState4(false);
1461
1491
  const [deleteBusy, setDeleteBusy] = useState4(false);
1462
1492
  const [toast, setToast] = useState4(null);
1493
+ const [resizeGuideX, setResizeGuideX] = useState4(null);
1463
1494
  const [hoveredHeaderAction, setHoveredHeaderAction] = useState4(null);
1464
1495
  const [visibleHeaderTooltip, setVisibleHeaderTooltip] = useState4(null);
1465
1496
  const [hoveredCellTooltip, setHoveredCellTooltip] = useState4(null);
@@ -1613,14 +1644,30 @@ function Table({
1613
1644
  rowIndexByKeyRef.current.set(key, index);
1614
1645
  return key;
1615
1646
  }, [getDataRowKey]);
1647
+ const resolvedCellSpans = useMemo(() => {
1648
+ if (Array.isArray(cellSpans)) return cellSpans;
1649
+ const spans = [];
1650
+ rows.forEach((row, rowIndex) => {
1651
+ columns.forEach((column, columnIndex) => {
1652
+ const span = cellSpans({ row, rowIndex, column, columnIndex, rows, columns });
1653
+ if (!span) return;
1654
+ spans.push({
1655
+ rowIndex,
1656
+ columnKey: column.key,
1657
+ ...span
1658
+ });
1659
+ });
1660
+ });
1661
+ return spans;
1662
+ }, [cellSpans, columns, rows]);
1616
1663
  const cellSpanLookup = useMemo(() => {
1617
- if (cellSpans.length === 0) return createEmptyCellSpanLookup();
1664
+ if (resolvedCellSpans.length === 0) return createEmptyCellSpanLookup();
1618
1665
  const rowIndexByKey = /* @__PURE__ */ new Map();
1619
1666
  rows.forEach((row, index) => rowIndexByKey.set(getRowKey(row, index), index));
1620
1667
  const columnIndexByKey = new Map(columns.map((column, index) => [column.key, index]));
1621
1668
  const lookup = createEmptyCellSpanLookup();
1622
1669
  const occupied = /* @__PURE__ */ new Set();
1623
- for (const span of cellSpans) {
1670
+ for (const span of resolvedCellSpans) {
1624
1671
  const rowIndex = span.rowKey !== void 0 ? rowIndexByKey.get(span.rowKey) : span.rowIndex;
1625
1672
  const columnIndex = columnIndexByKey.get(span.columnKey);
1626
1673
  if (rowIndex === void 0 || columnIndex === void 0) continue;
@@ -1656,7 +1703,7 @@ function Table({
1656
1703
  }
1657
1704
  }
1658
1705
  return lookup;
1659
- }, [cellSpans, columns, getRowKey, rows]);
1706
+ }, [columns, getRowKey, resolvedCellSpans, rows]);
1660
1707
  const selectedRowKeySet = useMemo(() => new Set(rowKeys), [rowKeys]);
1661
1708
  const selectedColumnKeySet = useMemo(() => new Set(columnKeys), [columnKeys]);
1662
1709
  const insertedRowKeySet = useMemo(() => insertedRowKeys, [insertedRowKeys]);
@@ -1909,6 +1956,16 @@ function Table({
1909
1956
  }
1910
1957
  return closest;
1911
1958
  }, [columnResizable, columns, getDisplayedColumnLeft, metrics]);
1959
+ const findResizeNeighbor = useCallback2((columnIndex) => {
1960
+ const resizedColumn = columns[columnIndex];
1961
+ for (let index = columnIndex + 1; index < columns.length; index += 1) {
1962
+ const column = columns[index];
1963
+ if (column.rowSelection || column.rowDragHandle || column.rowNumber) continue;
1964
+ if (resizedColumn?.fixed && column.fixed !== resizedColumn.fixed) return -1;
1965
+ return index;
1966
+ }
1967
+ return -1;
1968
+ }, [columns]);
1912
1969
  const locateHeaderAction = useCallback2((clientX, columnIndex) => {
1913
1970
  if (columnIndex < 0 || !canvasRef.current) return null;
1914
1971
  const column = columns[columnIndex];
@@ -2487,26 +2544,48 @@ function Table({
2487
2544
  if (resizeIndex >= 0) {
2488
2545
  event.preventDefault();
2489
2546
  event.currentTarget.setPointerCapture(event.pointerId);
2490
- columnDragRef.current = { type: "resize", columnIndex: resizeIndex, startX: event.clientX, startWidth: metrics[resizeIndex].width };
2547
+ const neighborIndex = findResizeNeighbor(resizeIndex);
2548
+ const startGuideX = getDisplayedColumnLeft(resizeIndex) + metrics[resizeIndex].width;
2549
+ columnDragRef.current = {
2550
+ type: "resize",
2551
+ columnIndex: resizeIndex,
2552
+ startX: event.clientX,
2553
+ startWidth: metrics[resizeIndex].width,
2554
+ startGuideX,
2555
+ neighborIndex,
2556
+ startNeighborWidth: neighborIndex >= 0 ? metrics[neighborIndex].width : 0
2557
+ };
2491
2558
  suppressClickRef.current = true;
2492
- setDragGuide(getDisplayedColumnLeft(resizeIndex) + metrics[resizeIndex].width);
2559
+ setResizeGuideX(startGuideX);
2493
2560
  return;
2494
2561
  }
2495
- }, [fixedHeader, getDisplayedColumnLeft, headerHeight, locateResizeHandle, metrics, setDragGuide]);
2562
+ }, [findResizeNeighbor, fixedHeader, getDisplayedColumnLeft, headerHeight, locateResizeHandle, metrics]);
2496
2563
  const handleColumnPointerMove = useCallback2((event) => {
2497
2564
  const drag = columnDragRef.current;
2498
2565
  if (!drag) return;
2499
- const width2 = Math.max(getMinimumColumnWidth(columns[drag.columnIndex], columnDraggable), Math.round(drag.startWidth + event.clientX - drag.startX));
2500
- onColumnResize?.(columns[drag.columnIndex].key, width2);
2501
- setDragGuide(getDisplayedColumnLeft(drag.columnIndex) + width2);
2502
- }, [columnDraggable, columns, getDisplayedColumnLeft, onColumnResize, setDragGuide]);
2566
+ const column = columns[drag.columnIndex];
2567
+ const minWidth = getMinimumColumnWidth(column, columnDraggable);
2568
+ let delta = Math.round(event.clientX - drag.startX);
2569
+ if (drag.neighborIndex >= 0) {
2570
+ const neighbor = columns[drag.neighborIndex];
2571
+ const minNeighborWidth = getMinimumColumnWidth(neighbor, columnDraggable);
2572
+ delta = Math.max(minWidth - drag.startWidth, Math.min(drag.startNeighborWidth - minNeighborWidth, delta));
2573
+ onColumnResize?.(column.key, drag.startWidth + delta);
2574
+ onColumnResize?.(neighbor.key, drag.startNeighborWidth - delta);
2575
+ setResizeGuideX(drag.startGuideX + delta);
2576
+ return;
2577
+ }
2578
+ const width2 = Math.max(minWidth, Math.round(drag.startWidth + delta));
2579
+ onColumnResize?.(column.key, width2);
2580
+ setResizeGuideX(drag.startGuideX + width2 - drag.startWidth);
2581
+ }, [columnDraggable, columns, onColumnResize]);
2503
2582
  const handleColumnPointerUp = useCallback2((event) => {
2504
2583
  const drag = columnDragRef.current;
2505
2584
  if (!drag) return;
2506
2585
  if (event.currentTarget.hasPointerCapture(event.pointerId)) event.currentTarget.releasePointerCapture(event.pointerId);
2507
2586
  columnDragRef.current = null;
2508
- setDragGuide(null);
2509
- }, [setDragGuide]);
2587
+ setResizeGuideX(null);
2588
+ }, []);
2510
2589
  const queueSortStateChange = useCallback2((next) => {
2511
2590
  setDisplaySortState(next);
2512
2591
  if (sortDebounceRef.current !== null) clearTimeout(sortDebounceRef.current);
@@ -2913,6 +2992,35 @@ function Table({
2913
2992
  column.key
2914
2993
  );
2915
2994
  }
2995
+ if (column.renderHeader) {
2996
+ const visibleActions = getVisibleHeaderActions(column, metrics[columnIndex].width, columnDraggable, measureHeaderTitleWidth(columnIndex));
2997
+ const actionWidth = (Number(visibleActions.drag) + Number(visibleActions.filter) + Number(visibleActions.sort)) * 18;
2998
+ return /* @__PURE__ */ jsx7(
2999
+ "div",
3000
+ {
3001
+ className: "rvg-header-title is-custom",
3002
+ style: {
3003
+ left,
3004
+ top: fixedHeader ? 0 : -currentScrollTop,
3005
+ width: Math.max(0, metrics[columnIndex].width - actionWidth),
3006
+ height: headerHeight,
3007
+ justifyContent: column.align === "right" ? "flex-end" : column.align === "center" ? "center" : "flex-start",
3008
+ textAlign: column.align === "right" ? "right" : column.align === "center" ? "center" : "left"
3009
+ },
3010
+ children: /* @__PURE__ */ jsx7(
3011
+ "span",
3012
+ {
3013
+ ref: (node) => {
3014
+ if (node) customHeaderContentRefs.current.set(column.key, node);
3015
+ else customHeaderContentRefs.current.delete(column.key);
3016
+ },
3017
+ children: column.renderHeader(column)
3018
+ }
3019
+ )
3020
+ },
3021
+ column.key
3022
+ );
3023
+ }
2916
3024
  return null;
2917
3025
  };
2918
3026
  const rangeHandlePosition = (() => {
@@ -3616,6 +3724,13 @@ function Table({
3616
3724
  ] }) }),
3617
3725
  toast && /* @__PURE__ */ jsx7("div", { className: `rvg-toast is-${toast.type}`, role: "status", children: toast.message }),
3618
3726
  /* @__PURE__ */ jsx7("div", { ref: dragGuideRef, className: "rvg-column-guide" }),
3727
+ resizeGuideX !== null && /* @__PURE__ */ jsx7(
3728
+ "div",
3729
+ {
3730
+ className: "rvg-column-guide is-resizing",
3731
+ style: { display: "block", transform: `translateX(${Math.round(resizeGuideX)}px)` }
3732
+ }
3733
+ ),
3619
3734
  /* @__PURE__ */ jsx7("div", { ref: rowDragGuideRef, className: "rvg-row-guide" }),
3620
3735
  filterEditor && /* @__PURE__ */ jsxs7("div", { ref: filterRef, className: "rvg-filter", style: { left: filterEditor.left, top: filterEditor.top }, children: [
3621
3736
  /* @__PURE__ */ jsx7(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lensui/lens-table",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "A lightweight, canvas-rendered React table component for LensUI.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",