@lensui/lens-table 0.1.1 → 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;
@@ -1440,7 +1448,7 @@ function Table({
1440
1448
  const hoveredRowIndexRef = (0, import_react5.useRef)(null);
1441
1449
  const [viewport, setViewport] = (0, import_react5.useState)({ width: 0, height: typeof height === "number" ? height : FALLBACK_HEIGHT });
1442
1450
  const [horizontalScrollbarHeight, setHorizontalScrollbarHeight] = (0, import_react5.useState)(0);
1443
- const resolvedHeight = viewport.height || FALLBACK_HEIGHT;
1451
+ const resolvedHeight = typeof height === "number" ? height : viewport.height || FALLBACK_HEIGHT;
1444
1452
  const [scrollPosition, setScrollPosition] = (0, import_react5.useState)({ left: 0, top: 0 });
1445
1453
  const scrollRef = (0, import_react5.useRef)({ left: 0, top: 0 });
1446
1454
  const [selection, setSelection] = useControllableValue(selectedCell, defaultSelectedCell, onSelectedCellChange);
@@ -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);
@@ -1588,14 +1619,15 @@ function Table({
1588
1619
  }, [columns, metrics]);
1589
1620
  const hasRowSelectionColumn = (0, import_react5.useMemo)(() => columns.some((column) => column.rowSelection), [columns]);
1590
1621
  const rowSelectionMode = typeof rowSelection === "object" ? rowSelection.mode ?? "multiple" : "multiple";
1591
- const hasSummaryRow = (0, import_react5.useMemo)(() => summaryEnabled && columns.some((column) => Boolean(column.summary)), [columns, summaryEnabled]);
1622
+ const hasSummaryRow = (0, import_react5.useMemo)(() => rows.length > 0 && summaryEnabled && columns.some((column) => Boolean(column.summary)), [columns, rows.length, summaryEnabled]);
1592
1623
  const topSummaryHeight = hasSummaryRow && summaryPosition === "top" ? rowHeight : 0;
1593
1624
  const bottomSummaryHeight = hasSummaryRow && summaryPosition === "bottom" ? rowHeight : 0;
1594
1625
  const bodyTop = headerHeight + topSummaryHeight;
1595
1626
  const bodyContentHeight = rows.length === 0 ? EMPTY_BODY_HEIGHT : contentHeight;
1596
1627
  const realContentHeight = bodyTop + bodyContentHeight + bottomSummaryHeight + (contentWidth > viewport.width ? horizontalScrollbarHeight : 0);
1597
1628
  const effectiveHeight = autoHeight ? Math.max(bodyTop + bottomSummaryHeight, Math.min(resolvedHeight, realContentHeight)) : resolvedHeight;
1598
- const bodyViewportHeight = Math.max(0, effectiveHeight - (fixedHeader ? headerHeight : 0) - topSummaryHeight);
1629
+ const renderHeight = Math.max(0, effectiveHeight - bottomSummaryHeight);
1630
+ const bodyViewportHeight = Math.max(0, renderHeight - (fixedHeader ? headerHeight : 0) - topSummaryHeight);
1599
1631
  const [summaryState, setSummaryState] = (0, import_react5.useState)(() => ({ columns: null, rows: null, values: /* @__PURE__ */ new Map(), pending: false }));
1600
1632
  const summaryPending = hasSummaryRow && (loading || summaryState.pending || summaryState.columns !== columns || summaryState.rows !== rows);
1601
1633
  const summaryValues = hasSummaryRow && summaryState.columns === columns && summaryState.rows === rows ? summaryState.values : /* @__PURE__ */ new Map();
@@ -1642,14 +1674,30 @@ function Table({
1642
1674
  rowIndexByKeyRef.current.set(key, index);
1643
1675
  return key;
1644
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]);
1645
1693
  const cellSpanLookup = (0, import_react5.useMemo)(() => {
1646
- if (cellSpans.length === 0) return createEmptyCellSpanLookup();
1694
+ if (resolvedCellSpans.length === 0) return createEmptyCellSpanLookup();
1647
1695
  const rowIndexByKey = /* @__PURE__ */ new Map();
1648
1696
  rows.forEach((row, index) => rowIndexByKey.set(getRowKey(row, index), index));
1649
1697
  const columnIndexByKey = new Map(columns.map((column, index) => [column.key, index]));
1650
1698
  const lookup = createEmptyCellSpanLookup();
1651
1699
  const occupied = /* @__PURE__ */ new Set();
1652
- for (const span of cellSpans) {
1700
+ for (const span of resolvedCellSpans) {
1653
1701
  const rowIndex = span.rowKey !== void 0 ? rowIndexByKey.get(span.rowKey) : span.rowIndex;
1654
1702
  const columnIndex = columnIndexByKey.get(span.columnKey);
1655
1703
  if (rowIndex === void 0 || columnIndex === void 0) continue;
@@ -1685,7 +1733,7 @@ function Table({
1685
1733
  }
1686
1734
  }
1687
1735
  return lookup;
1688
- }, [cellSpans, columns, getRowKey, rows]);
1736
+ }, [columns, getRowKey, resolvedCellSpans, rows]);
1689
1737
  const selectedRowKeySet = (0, import_react5.useMemo)(() => new Set(rowKeys), [rowKeys]);
1690
1738
  const selectedColumnKeySet = (0, import_react5.useMemo)(() => new Set(columnKeys), [columnKeys]);
1691
1739
  const insertedRowKeySet = (0, import_react5.useMemo)(() => insertedRowKeys, [insertedRowKeys]);
@@ -1732,7 +1780,7 @@ function Table({
1732
1780
  if (!canvas || viewport.width <= 0) return;
1733
1781
  const ratio = Math.min(window.devicePixelRatio || 1, 2);
1734
1782
  const physicalWidth = Math.round(viewport.width * ratio);
1735
- const physicalHeight = Math.round(effectiveHeight * ratio);
1783
+ const physicalHeight = Math.round(renderHeight * ratio);
1736
1784
  if (canvas.width !== physicalWidth) canvas.width = physicalWidth;
1737
1785
  if (canvas.height !== physicalHeight) canvas.height = physicalHeight;
1738
1786
  const context = canvas.getContext("2d");
@@ -1755,8 +1803,8 @@ function Table({
1755
1803
  const scroll = scrollRef.current;
1756
1804
  const rowScrollTop = fixedHeader ? scroll.top : Math.max(0, scroll.top - bodyTop);
1757
1805
  const range = isVirtualized ? getViewportRange(scroll.left, rowScrollTop, viewport.width, bodyViewportHeight, rows.length, rowHeight, metrics, virtualOverscan) : { rowStart: 0, rowEnd: rows.length, columnStart: 0, columnEnd: columns.length };
1758
- paintGrid({ context, width: viewport.width, height: effectiveHeight, pixelRatio: ratio, scrollLeft: scroll.left, scrollTop: scroll.top, rowHeight, headerHeight, bodyTop, suppressLastRowBottomBorder: bottomSummaryHeight > 0, suppressFrameBottomBorder: bottomSummaryHeight > 0, fixedHeader, verticalBorderless: !hasVerticalBorders, striped: hasStripedRows, columnDraggable, sortState, filterValues, hoveredHeaderAction: hoveredHeaderActionRef.current, rows, columns, metrics, range, selection, editing, hoveredRowIndex: hoveredRowIndexRef.current, selectionRange, selectedRowKeys: selectedRowKeySet, selectedColumnKeys: selectedColumnKeySet, cellSpans: cellSpanLookup.covered, maxRowSpan: cellSpanLookup.maxRowSpan, highlightEditedCells: highlightsEditedCells, highlightInsertedRows: highlightsInsertedRows, insertedRowKeys: insertedRowKeySet, editedCellKeys, cellAnnotations, getRowKey, rowDragPreview, colors: themeColors });
1759
- }, [bodyTop, bodyViewportHeight, bottomSummaryHeight, cellAnnotations, cellSpanLookup, columnDraggable, columns, editedCellHighlightColor, editedCellKeys, editing, effectiveHeight, filterValues, fixedHeader, getRowKey, hasStripedRows, hasVerticalBorders, headerHeight, highlightsEditedCells, highlightsInsertedRows, insertedRowHighlightColor, insertedRowKeySet, isVirtualized, metrics, rowDragPreview, rowHeight, rows, selectedColumnKeySet, selectedRowKeySet, selection, selectionRange, sortState, stripedColor, viewport.width, virtualOverscan]);
1806
+ paintGrid({ context, width: viewport.width, height: renderHeight, pixelRatio: ratio, scrollLeft: scroll.left, scrollTop: scroll.top, rowHeight, headerHeight, bodyTop, suppressLastRowBottomBorder: bottomSummaryHeight > 0, suppressFrameBottomBorder: bottomSummaryHeight > 0, fixedHeader, verticalBorderless: !hasVerticalBorders, striped: hasStripedRows, columnDraggable, sortState, filterValues, hoveredHeaderAction: hoveredHeaderActionRef.current, rows, columns, metrics, range, selection, editing, hoveredRowIndex: hoveredRowIndexRef.current, selectionRange, selectedRowKeys: selectedRowKeySet, selectedColumnKeys: selectedColumnKeySet, cellSpans: cellSpanLookup.covered, maxRowSpan: cellSpanLookup.maxRowSpan, highlightEditedCells: highlightsEditedCells, highlightInsertedRows: highlightsInsertedRows, insertedRowKeys: insertedRowKeySet, editedCellKeys, cellAnnotations, getRowKey, rowDragPreview, colors: themeColors });
1807
+ }, [bodyTop, bodyViewportHeight, bottomSummaryHeight, cellAnnotations, cellSpanLookup, columnDraggable, columns, editedCellHighlightColor, editedCellKeys, editing, renderHeight, filterValues, fixedHeader, getRowKey, hasStripedRows, hasVerticalBorders, headerHeight, highlightsEditedCells, highlightsInsertedRows, insertedRowHighlightColor, insertedRowKeySet, isVirtualized, metrics, rowDragPreview, rowHeight, rows, selectedColumnKeySet, selectedRowKeySet, selection, selectionRange, sortState, stripedColor, viewport.width, virtualOverscan]);
1760
1808
  const scheduleDraw = (0, import_react5.useCallback)(() => {
1761
1809
  if (frameRef.current === null) frameRef.current = requestAnimationFrame(draw);
1762
1810
  }, [draw]);
@@ -1865,14 +1913,14 @@ function Table({
1865
1913
  const horizontalStart = column.fixed === void 0 ? fixedWidth : 0;
1866
1914
  const horizontalEnd = column.fixed === void 0 ? viewport.width - rightFixedWidth : viewport.width;
1867
1915
  const verticalStart = fixedHeader ? bodyTop : 0;
1868
- if (rawRight <= horizontalStart || rawLeft >= horizontalEnd || rawBottom <= verticalStart || rawTop >= effectiveHeight) {
1916
+ if (rawRight <= horizontalStart || rawLeft >= horizontalEnd || rawBottom <= verticalStart || rawTop >= renderHeight) {
1869
1917
  focus.style.display = "none";
1870
1918
  } else {
1871
1919
  const borderWidth = 2;
1872
1920
  const left = Math.round(Math.max(rawLeft - borderWidth, horizontalStart - 1));
1873
1921
  const right = Math.round(Math.min(rawRight + 1, horizontalEnd, viewport.width));
1874
1922
  const top = Math.round(Math.max(rawTop - borderWidth, verticalStart - 1));
1875
- const bottom = Math.round(Math.min(rawBottom + 1, effectiveHeight));
1923
+ const bottom = Math.round(Math.min(rawBottom + 1, renderHeight));
1876
1924
  focus.style.display = "block";
1877
1925
  focus.style.left = `${left}px`;
1878
1926
  focus.style.top = `${top}px`;
@@ -1897,7 +1945,7 @@ function Table({
1897
1945
  setHoveredCellTooltip(null);
1898
1946
  scheduleDraw();
1899
1947
  setEditing(null);
1900
- }, [bodyTop, columns, effectiveHeight, fixedHeader, fixedWidth, getCellDisplayWidth, getCellSpan, getRowKey, metrics, rightFixedOffsets, rightFixedWidth, rowHeight, rows, scheduleDraw, selection, selectionRange, viewport.width]);
1948
+ }, [bodyTop, columns, renderHeight, fixedHeader, fixedWidth, getCellDisplayWidth, getCellSpan, getRowKey, metrics, rightFixedOffsets, rightFixedWidth, rowHeight, rows, scheduleDraw, selection, selectionRange, viewport.width]);
1901
1949
  const locateColumn = (0, import_react5.useCallback)((clientX) => {
1902
1950
  const canvas = canvasRef.current;
1903
1951
  if (!canvas) return -1;
@@ -1938,6 +1986,16 @@ function Table({
1938
1986
  }
1939
1987
  return closest;
1940
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]);
1941
1999
  const locateHeaderAction = (0, import_react5.useCallback)((clientX, columnIndex) => {
1942
2000
  if (columnIndex < 0 || !canvasRef.current) return null;
1943
2001
  const column = columns[columnIndex];
@@ -2485,13 +2543,13 @@ function Table({
2485
2543
  const rowTop = (fixedHeader ? 0 : bodyTop) + rowIndex * rowHeight;
2486
2544
  const rowBottom = rowTop + rowHeight * (span?.rowSpan ?? 1);
2487
2545
  const cellWidth = getCellDisplayWidth(columnIndex, span?.colSpan ?? 1);
2488
- const visibleRowHeight = fixedHeader ? effectiveHeight - bodyTop : effectiveHeight;
2546
+ const visibleRowHeight = fixedHeader ? renderHeight - bodyTop : renderHeight;
2489
2547
  if (rowTop < scroller.scrollTop) scroller.scrollTop = rowTop;
2490
2548
  if (rowBottom > scroller.scrollTop + visibleRowHeight) scroller.scrollTop = rowBottom - visibleRowHeight;
2491
2549
  if (columns[columnIndex].fixed === void 0 && metric.left < scroller.scrollLeft + fixedWidth) scroller.scrollLeft = Math.max(0, metric.left - fixedWidth);
2492
2550
  if (columns[columnIndex].fixed === void 0 && metric.left + cellWidth > scroller.scrollLeft + viewport.width - rightFixedWidth) scroller.scrollLeft = metric.left + cellWidth - viewport.width + rightFixedWidth;
2493
2551
  }
2494
- }, [beginEdit, bodyTop, columns, editing, effectiveHeight, fixedHeader, fixedWidth, getCellDisplayWidth, getCellSpan, getRowKey, metrics, rightFixedWidth, rowHeight, rows, selection, setSelection, viewport.width]);
2552
+ }, [beginEdit, bodyTop, columns, editing, renderHeight, fixedHeader, fixedWidth, getCellDisplayWidth, getCellSpan, getRowKey, metrics, rightFixedWidth, rowHeight, rows, selection, setSelection, viewport.width]);
2495
2553
  const editorStyle = (0, import_react5.useMemo)(() => {
2496
2554
  if (!editing) return void 0;
2497
2555
  const metric = metrics[editing.columnIndex];
@@ -2516,26 +2574,48 @@ function Table({
2516
2574
  if (resizeIndex >= 0) {
2517
2575
  event.preventDefault();
2518
2576
  event.currentTarget.setPointerCapture(event.pointerId);
2519
- 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
+ };
2520
2588
  suppressClickRef.current = true;
2521
- setDragGuide(getDisplayedColumnLeft(resizeIndex) + metrics[resizeIndex].width);
2589
+ setResizeGuideX(startGuideX);
2522
2590
  return;
2523
2591
  }
2524
- }, [fixedHeader, getDisplayedColumnLeft, headerHeight, locateResizeHandle, metrics, setDragGuide]);
2592
+ }, [findResizeNeighbor, fixedHeader, getDisplayedColumnLeft, headerHeight, locateResizeHandle, metrics]);
2525
2593
  const handleColumnPointerMove = (0, import_react5.useCallback)((event) => {
2526
2594
  const drag = columnDragRef.current;
2527
2595
  if (!drag) return;
2528
- const width2 = Math.max(getMinimumColumnWidth(columns[drag.columnIndex], columnDraggable), Math.round(drag.startWidth + event.clientX - drag.startX));
2529
- onColumnResize?.(columns[drag.columnIndex].key, width2);
2530
- setDragGuide(getDisplayedColumnLeft(drag.columnIndex) + width2);
2531
- }, [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]);
2532
2612
  const handleColumnPointerUp = (0, import_react5.useCallback)((event) => {
2533
2613
  const drag = columnDragRef.current;
2534
2614
  if (!drag) return;
2535
2615
  if (event.currentTarget.hasPointerCapture(event.pointerId)) event.currentTarget.releasePointerCapture(event.pointerId);
2536
2616
  columnDragRef.current = null;
2537
- setDragGuide(null);
2538
- }, [setDragGuide]);
2617
+ setResizeGuideX(null);
2618
+ }, []);
2539
2619
  const queueSortStateChange = (0, import_react5.useCallback)((next) => {
2540
2620
  setDisplaySortState(next);
2541
2621
  if (sortDebounceRef.current !== null) clearTimeout(sortDebounceRef.current);
@@ -2715,7 +2795,7 @@ function Table({
2715
2795
  type: "cell",
2716
2796
  ...cell,
2717
2797
  left: Math.min(Math.max(0, clientX - root.left), Math.max(0, viewport.width - 190)),
2718
- top: Math.min(Math.max(0, clientY - root.top), Math.max(0, effectiveHeight - 350))
2798
+ top: Math.min(Math.max(0, clientY - root.top), Math.max(0, renderHeight - 350))
2719
2799
  });
2720
2800
  onCellContextMenu?.({ ...cell, row: rows[cell.rowIndex], clientX, clientY });
2721
2801
  };
@@ -2726,7 +2806,7 @@ function Table({
2726
2806
  type: "header",
2727
2807
  columnIndex,
2728
2808
  left: Math.min(Math.max(0, clientX - root.left), Math.max(0, viewport.width - 180)),
2729
- top: Math.min(Math.max(0, clientY - root.top), Math.max(0, effectiveHeight - 68))
2809
+ top: Math.min(Math.max(0, clientY - root.top), Math.max(0, renderHeight - 68))
2730
2810
  });
2731
2811
  };
2732
2812
  const getRowDragOffset = (rowIndex) => {
@@ -2942,6 +3022,35 @@ function Table({
2942
3022
  column.key
2943
3023
  );
2944
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
+ }
2945
3054
  return null;
2946
3055
  };
2947
3056
  const rangeHandlePosition = (() => {
@@ -2956,7 +3065,7 @@ function Table({
2956
3065
  const horizontalStart = column.fixed === void 0 ? fixedWidth : 0;
2957
3066
  const horizontalEnd = column.fixed === void 0 ? viewport.width - rightFixedWidth : viewport.width;
2958
3067
  const verticalStart = fixedHeader ? bodyTop : 0;
2959
- if (rawLeft <= horizontalStart || rawLeft > horizontalEnd || rawTop <= verticalStart || rawTop > effectiveHeight) return null;
3068
+ if (rawLeft <= horizontalStart || rawLeft > horizontalEnd || rawTop <= verticalStart || rawTop > renderHeight) return null;
2960
3069
  return { left: rawLeft, top: rawTop, bounds };
2961
3070
  })();
2962
3071
  const selectionOutlineStyle = (() => {
@@ -2973,12 +3082,12 @@ function Table({
2973
3082
  const horizontalStart = column.fixed === void 0 ? fixedWidth : 0;
2974
3083
  const horizontalEnd = column.fixed === void 0 ? viewport.width - rightFixedWidth : viewport.width;
2975
3084
  const verticalStart = fixedHeader ? bodyTop : 0;
2976
- if (rawRight <= horizontalStart || rawLeft >= horizontalEnd || rawBottom <= verticalStart || rawTop >= effectiveHeight) return null;
3085
+ if (rawRight <= horizontalStart || rawLeft >= horizontalEnd || rawBottom <= verticalStart || rawTop >= renderHeight) return null;
2977
3086
  const borderWidth = 2;
2978
3087
  const left = Math.round(Math.max(rawLeft - borderWidth, horizontalStart - 1));
2979
3088
  const right = Math.round(Math.min(rawRight + 1, horizontalEnd, viewport.width));
2980
3089
  const top = Math.round(Math.max(rawTop - borderWidth, verticalStart - 1));
2981
- const bottom = Math.round(Math.min(rawBottom + 1, effectiveHeight));
3090
+ const bottom = Math.round(Math.min(rawBottom + 1, renderHeight));
2982
3091
  return {
2983
3092
  left,
2984
3093
  top,
@@ -3093,8 +3202,10 @@ function Table({
3093
3202
  const renderSummaryCell = (columnIndex, left) => {
3094
3203
  const column = columns[columnIndex];
3095
3204
  const isUtility = column.rowSelection || column.rowDragHandle || column.rowNumber;
3096
- const summaryContent = summaryValues.has(column.key) ? summaryValues.get(column.key) : summaryEmptyValue;
3097
- const content = column.rowNumber ? language === "zh-CN" ? "\u5408\u8BA1" : "Total" : summaryPending ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "rvg-summary-skeleton" }) : summaryContent;
3205
+ const hasSummaryValue = summaryValues.has(column.key);
3206
+ const summaryContent = hasSummaryValue ? summaryValues.get(column.key) : summaryEmptyValue;
3207
+ const showSummarySkeleton = Boolean(column.summary) && summaryPending;
3208
+ const content = columnIndex === 0 ? language === "zh-CN" ? "\u5408\u8BA1" : "Total" : showSummarySkeleton ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "rvg-summary-skeleton" }) : summaryContent;
3098
3209
  return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3099
3210
  "div",
3100
3211
  {
@@ -3125,7 +3236,7 @@ function Table({
3125
3236
  {
3126
3237
  ref: canvasRef,
3127
3238
  className: "rvg-canvas",
3128
- style: { width: viewport.width, height: effectiveHeight },
3239
+ style: { width: viewport.width, height: renderHeight },
3129
3240
  onMouseMove: (event) => {
3130
3241
  if (columnDragRef.current) return;
3131
3242
  const rect = event.currentTarget.getBoundingClientRect();
@@ -3303,7 +3414,7 @@ function Table({
3303
3414
  onContextMenu: handleContextMenu
3304
3415
  }
3305
3416
  ),
3306
- /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "rvg-text-layer", style: { width: viewport.width, height: effectiveHeight, marginTop: -effectiveHeight }, onContextMenu: handleContextMenu, children: [
3417
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "rvg-text-layer", style: { width: viewport.width, height: renderHeight, marginTop: -renderHeight }, onContextMenu: handleContextMenu, children: [
3307
3418
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "rvg-text-scroll-clip", style: { left: fixedWidth, right: rightFixedWidth }, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
3308
3419
  "div",
3309
3420
  {
@@ -3327,7 +3438,7 @@ function Table({
3327
3438
  columns.map((column, columnIndex) => column.fixed === "left" ? renderHeaderTitle(columnIndex, metrics[columnIndex].left) : null),
3328
3439
  columns.map((column, columnIndex) => column.fixed === "right" ? renderHeaderTitle(columnIndex, viewport.width - (rightFixedOffsets.get(columnIndex) ?? 0) - metrics[columnIndex].width) : null)
3329
3440
  ] }),
3330
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "rvg-spacer", style: { width: contentWidth, height: contentHeight + bodyTop, marginTop: -effectiveHeight } })
3441
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "rvg-spacer", style: { width: contentWidth, height: contentHeight + bodyTop, marginTop: -renderHeight } })
3331
3442
  ] }),
3332
3443
  hasSummaryRow && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
3333
3444
  "div",
@@ -3643,6 +3754,13 @@ function Table({
3643
3754
  ] }) }),
3644
3755
  toast && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: `rvg-toast is-${toast.type}`, role: "status", children: toast.message }),
3645
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
+ ),
3646
3764
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { ref: rowDragGuideRef, className: "rvg-row-guide" }),
3647
3765
  filterEditor && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { ref: filterRef, className: "rvg-filter", style: { left: filterEditor.left, top: filterEditor.top }, children: [
3648
3766
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
@@ -3691,7 +3809,7 @@ function Table({
3691
3809
  rows.length === 0 && !loading && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "rvg-empty", style: { top: bodyTop }, children: emptyContent ?? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(DefaultEmptyState, { label: labels.empty }) }),
3692
3810
  loading && hasCustomLoading && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "rvg-custom-loading", "aria-label": labels.refreshing, children: loadingContent }),
3693
3811
  loading && !hasCustomLoading && !hasCompletedLoadRef.current && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "rvg-initial-loading", style: { top: headerHeight }, "aria-label": labels.initialLoading, children: [
3694
- Array.from({ length: Math.ceil((effectiveHeight - headerHeight) / rowHeight) }, (_, rowIndex) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "rvg-loading-row", style: { height: rowHeight }, children: columns.map((column, columnIndex) => {
3812
+ Array.from({ length: Math.ceil((renderHeight - headerHeight) / rowHeight) }, (_, rowIndex) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "rvg-loading-row", style: { height: rowHeight }, children: columns.map((column, columnIndex) => {
3695
3813
  const left = getDisplayedColumnLeft(columnIndex);
3696
3814
  const columnWidth = metrics[columnIndex].width;
3697
3815
  if (left + columnWidth <= 0 || left >= viewport.width) return null;
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;
@@ -1410,7 +1418,7 @@ function Table({
1410
1418
  const hoveredRowIndexRef = useRef2(null);
1411
1419
  const [viewport, setViewport] = useState4({ width: 0, height: typeof height === "number" ? height : FALLBACK_HEIGHT });
1412
1420
  const [horizontalScrollbarHeight, setHorizontalScrollbarHeight] = useState4(0);
1413
- const resolvedHeight = viewport.height || FALLBACK_HEIGHT;
1421
+ const resolvedHeight = typeof height === "number" ? height : viewport.height || FALLBACK_HEIGHT;
1414
1422
  const [scrollPosition, setScrollPosition] = useState4({ left: 0, top: 0 });
1415
1423
  const scrollRef = useRef2({ left: 0, top: 0 });
1416
1424
  const [selection, setSelection] = useControllableValue(selectedCell, defaultSelectedCell, onSelectedCellChange);
@@ -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);
@@ -1558,14 +1589,15 @@ function Table({
1558
1589
  }, [columns, metrics]);
1559
1590
  const hasRowSelectionColumn = useMemo(() => columns.some((column) => column.rowSelection), [columns]);
1560
1591
  const rowSelectionMode = typeof rowSelection === "object" ? rowSelection.mode ?? "multiple" : "multiple";
1561
- const hasSummaryRow = useMemo(() => summaryEnabled && columns.some((column) => Boolean(column.summary)), [columns, summaryEnabled]);
1592
+ const hasSummaryRow = useMemo(() => rows.length > 0 && summaryEnabled && columns.some((column) => Boolean(column.summary)), [columns, rows.length, summaryEnabled]);
1562
1593
  const topSummaryHeight = hasSummaryRow && summaryPosition === "top" ? rowHeight : 0;
1563
1594
  const bottomSummaryHeight = hasSummaryRow && summaryPosition === "bottom" ? rowHeight : 0;
1564
1595
  const bodyTop = headerHeight + topSummaryHeight;
1565
1596
  const bodyContentHeight = rows.length === 0 ? EMPTY_BODY_HEIGHT : contentHeight;
1566
1597
  const realContentHeight = bodyTop + bodyContentHeight + bottomSummaryHeight + (contentWidth > viewport.width ? horizontalScrollbarHeight : 0);
1567
1598
  const effectiveHeight = autoHeight ? Math.max(bodyTop + bottomSummaryHeight, Math.min(resolvedHeight, realContentHeight)) : resolvedHeight;
1568
- const bodyViewportHeight = Math.max(0, effectiveHeight - (fixedHeader ? headerHeight : 0) - topSummaryHeight);
1599
+ const renderHeight = Math.max(0, effectiveHeight - bottomSummaryHeight);
1600
+ const bodyViewportHeight = Math.max(0, renderHeight - (fixedHeader ? headerHeight : 0) - topSummaryHeight);
1569
1601
  const [summaryState, setSummaryState] = useState4(() => ({ columns: null, rows: null, values: /* @__PURE__ */ new Map(), pending: false }));
1570
1602
  const summaryPending = hasSummaryRow && (loading || summaryState.pending || summaryState.columns !== columns || summaryState.rows !== rows);
1571
1603
  const summaryValues = hasSummaryRow && summaryState.columns === columns && summaryState.rows === rows ? summaryState.values : /* @__PURE__ */ new Map();
@@ -1612,14 +1644,30 @@ function Table({
1612
1644
  rowIndexByKeyRef.current.set(key, index);
1613
1645
  return key;
1614
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]);
1615
1663
  const cellSpanLookup = useMemo(() => {
1616
- if (cellSpans.length === 0) return createEmptyCellSpanLookup();
1664
+ if (resolvedCellSpans.length === 0) return createEmptyCellSpanLookup();
1617
1665
  const rowIndexByKey = /* @__PURE__ */ new Map();
1618
1666
  rows.forEach((row, index) => rowIndexByKey.set(getRowKey(row, index), index));
1619
1667
  const columnIndexByKey = new Map(columns.map((column, index) => [column.key, index]));
1620
1668
  const lookup = createEmptyCellSpanLookup();
1621
1669
  const occupied = /* @__PURE__ */ new Set();
1622
- for (const span of cellSpans) {
1670
+ for (const span of resolvedCellSpans) {
1623
1671
  const rowIndex = span.rowKey !== void 0 ? rowIndexByKey.get(span.rowKey) : span.rowIndex;
1624
1672
  const columnIndex = columnIndexByKey.get(span.columnKey);
1625
1673
  if (rowIndex === void 0 || columnIndex === void 0) continue;
@@ -1655,7 +1703,7 @@ function Table({
1655
1703
  }
1656
1704
  }
1657
1705
  return lookup;
1658
- }, [cellSpans, columns, getRowKey, rows]);
1706
+ }, [columns, getRowKey, resolvedCellSpans, rows]);
1659
1707
  const selectedRowKeySet = useMemo(() => new Set(rowKeys), [rowKeys]);
1660
1708
  const selectedColumnKeySet = useMemo(() => new Set(columnKeys), [columnKeys]);
1661
1709
  const insertedRowKeySet = useMemo(() => insertedRowKeys, [insertedRowKeys]);
@@ -1702,7 +1750,7 @@ function Table({
1702
1750
  if (!canvas || viewport.width <= 0) return;
1703
1751
  const ratio = Math.min(window.devicePixelRatio || 1, 2);
1704
1752
  const physicalWidth = Math.round(viewport.width * ratio);
1705
- const physicalHeight = Math.round(effectiveHeight * ratio);
1753
+ const physicalHeight = Math.round(renderHeight * ratio);
1706
1754
  if (canvas.width !== physicalWidth) canvas.width = physicalWidth;
1707
1755
  if (canvas.height !== physicalHeight) canvas.height = physicalHeight;
1708
1756
  const context = canvas.getContext("2d");
@@ -1725,8 +1773,8 @@ function Table({
1725
1773
  const scroll = scrollRef.current;
1726
1774
  const rowScrollTop = fixedHeader ? scroll.top : Math.max(0, scroll.top - bodyTop);
1727
1775
  const range = isVirtualized ? getViewportRange(scroll.left, rowScrollTop, viewport.width, bodyViewportHeight, rows.length, rowHeight, metrics, virtualOverscan) : { rowStart: 0, rowEnd: rows.length, columnStart: 0, columnEnd: columns.length };
1728
- paintGrid({ context, width: viewport.width, height: effectiveHeight, pixelRatio: ratio, scrollLeft: scroll.left, scrollTop: scroll.top, rowHeight, headerHeight, bodyTop, suppressLastRowBottomBorder: bottomSummaryHeight > 0, suppressFrameBottomBorder: bottomSummaryHeight > 0, fixedHeader, verticalBorderless: !hasVerticalBorders, striped: hasStripedRows, columnDraggable, sortState, filterValues, hoveredHeaderAction: hoveredHeaderActionRef.current, rows, columns, metrics, range, selection, editing, hoveredRowIndex: hoveredRowIndexRef.current, selectionRange, selectedRowKeys: selectedRowKeySet, selectedColumnKeys: selectedColumnKeySet, cellSpans: cellSpanLookup.covered, maxRowSpan: cellSpanLookup.maxRowSpan, highlightEditedCells: highlightsEditedCells, highlightInsertedRows: highlightsInsertedRows, insertedRowKeys: insertedRowKeySet, editedCellKeys, cellAnnotations, getRowKey, rowDragPreview, colors: themeColors });
1729
- }, [bodyTop, bodyViewportHeight, bottomSummaryHeight, cellAnnotations, cellSpanLookup, columnDraggable, columns, editedCellHighlightColor, editedCellKeys, editing, effectiveHeight, filterValues, fixedHeader, getRowKey, hasStripedRows, hasVerticalBorders, headerHeight, highlightsEditedCells, highlightsInsertedRows, insertedRowHighlightColor, insertedRowKeySet, isVirtualized, metrics, rowDragPreview, rowHeight, rows, selectedColumnKeySet, selectedRowKeySet, selection, selectionRange, sortState, stripedColor, viewport.width, virtualOverscan]);
1776
+ paintGrid({ context, width: viewport.width, height: renderHeight, pixelRatio: ratio, scrollLeft: scroll.left, scrollTop: scroll.top, rowHeight, headerHeight, bodyTop, suppressLastRowBottomBorder: bottomSummaryHeight > 0, suppressFrameBottomBorder: bottomSummaryHeight > 0, fixedHeader, verticalBorderless: !hasVerticalBorders, striped: hasStripedRows, columnDraggable, sortState, filterValues, hoveredHeaderAction: hoveredHeaderActionRef.current, rows, columns, metrics, range, selection, editing, hoveredRowIndex: hoveredRowIndexRef.current, selectionRange, selectedRowKeys: selectedRowKeySet, selectedColumnKeys: selectedColumnKeySet, cellSpans: cellSpanLookup.covered, maxRowSpan: cellSpanLookup.maxRowSpan, highlightEditedCells: highlightsEditedCells, highlightInsertedRows: highlightsInsertedRows, insertedRowKeys: insertedRowKeySet, editedCellKeys, cellAnnotations, getRowKey, rowDragPreview, colors: themeColors });
1777
+ }, [bodyTop, bodyViewportHeight, bottomSummaryHeight, cellAnnotations, cellSpanLookup, columnDraggable, columns, editedCellHighlightColor, editedCellKeys, editing, renderHeight, filterValues, fixedHeader, getRowKey, hasStripedRows, hasVerticalBorders, headerHeight, highlightsEditedCells, highlightsInsertedRows, insertedRowHighlightColor, insertedRowKeySet, isVirtualized, metrics, rowDragPreview, rowHeight, rows, selectedColumnKeySet, selectedRowKeySet, selection, selectionRange, sortState, stripedColor, viewport.width, virtualOverscan]);
1730
1778
  const scheduleDraw = useCallback2(() => {
1731
1779
  if (frameRef.current === null) frameRef.current = requestAnimationFrame(draw);
1732
1780
  }, [draw]);
@@ -1835,14 +1883,14 @@ function Table({
1835
1883
  const horizontalStart = column.fixed === void 0 ? fixedWidth : 0;
1836
1884
  const horizontalEnd = column.fixed === void 0 ? viewport.width - rightFixedWidth : viewport.width;
1837
1885
  const verticalStart = fixedHeader ? bodyTop : 0;
1838
- if (rawRight <= horizontalStart || rawLeft >= horizontalEnd || rawBottom <= verticalStart || rawTop >= effectiveHeight) {
1886
+ if (rawRight <= horizontalStart || rawLeft >= horizontalEnd || rawBottom <= verticalStart || rawTop >= renderHeight) {
1839
1887
  focus.style.display = "none";
1840
1888
  } else {
1841
1889
  const borderWidth = 2;
1842
1890
  const left = Math.round(Math.max(rawLeft - borderWidth, horizontalStart - 1));
1843
1891
  const right = Math.round(Math.min(rawRight + 1, horizontalEnd, viewport.width));
1844
1892
  const top = Math.round(Math.max(rawTop - borderWidth, verticalStart - 1));
1845
- const bottom = Math.round(Math.min(rawBottom + 1, effectiveHeight));
1893
+ const bottom = Math.round(Math.min(rawBottom + 1, renderHeight));
1846
1894
  focus.style.display = "block";
1847
1895
  focus.style.left = `${left}px`;
1848
1896
  focus.style.top = `${top}px`;
@@ -1867,7 +1915,7 @@ function Table({
1867
1915
  setHoveredCellTooltip(null);
1868
1916
  scheduleDraw();
1869
1917
  setEditing(null);
1870
- }, [bodyTop, columns, effectiveHeight, fixedHeader, fixedWidth, getCellDisplayWidth, getCellSpan, getRowKey, metrics, rightFixedOffsets, rightFixedWidth, rowHeight, rows, scheduleDraw, selection, selectionRange, viewport.width]);
1918
+ }, [bodyTop, columns, renderHeight, fixedHeader, fixedWidth, getCellDisplayWidth, getCellSpan, getRowKey, metrics, rightFixedOffsets, rightFixedWidth, rowHeight, rows, scheduleDraw, selection, selectionRange, viewport.width]);
1871
1919
  const locateColumn = useCallback2((clientX) => {
1872
1920
  const canvas = canvasRef.current;
1873
1921
  if (!canvas) return -1;
@@ -1908,6 +1956,16 @@ function Table({
1908
1956
  }
1909
1957
  return closest;
1910
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]);
1911
1969
  const locateHeaderAction = useCallback2((clientX, columnIndex) => {
1912
1970
  if (columnIndex < 0 || !canvasRef.current) return null;
1913
1971
  const column = columns[columnIndex];
@@ -2455,13 +2513,13 @@ function Table({
2455
2513
  const rowTop = (fixedHeader ? 0 : bodyTop) + rowIndex * rowHeight;
2456
2514
  const rowBottom = rowTop + rowHeight * (span?.rowSpan ?? 1);
2457
2515
  const cellWidth = getCellDisplayWidth(columnIndex, span?.colSpan ?? 1);
2458
- const visibleRowHeight = fixedHeader ? effectiveHeight - bodyTop : effectiveHeight;
2516
+ const visibleRowHeight = fixedHeader ? renderHeight - bodyTop : renderHeight;
2459
2517
  if (rowTop < scroller.scrollTop) scroller.scrollTop = rowTop;
2460
2518
  if (rowBottom > scroller.scrollTop + visibleRowHeight) scroller.scrollTop = rowBottom - visibleRowHeight;
2461
2519
  if (columns[columnIndex].fixed === void 0 && metric.left < scroller.scrollLeft + fixedWidth) scroller.scrollLeft = Math.max(0, metric.left - fixedWidth);
2462
2520
  if (columns[columnIndex].fixed === void 0 && metric.left + cellWidth > scroller.scrollLeft + viewport.width - rightFixedWidth) scroller.scrollLeft = metric.left + cellWidth - viewport.width + rightFixedWidth;
2463
2521
  }
2464
- }, [beginEdit, bodyTop, columns, editing, effectiveHeight, fixedHeader, fixedWidth, getCellDisplayWidth, getCellSpan, getRowKey, metrics, rightFixedWidth, rowHeight, rows, selection, setSelection, viewport.width]);
2522
+ }, [beginEdit, bodyTop, columns, editing, renderHeight, fixedHeader, fixedWidth, getCellDisplayWidth, getCellSpan, getRowKey, metrics, rightFixedWidth, rowHeight, rows, selection, setSelection, viewport.width]);
2465
2523
  const editorStyle = useMemo(() => {
2466
2524
  if (!editing) return void 0;
2467
2525
  const metric = metrics[editing.columnIndex];
@@ -2486,26 +2544,48 @@ function Table({
2486
2544
  if (resizeIndex >= 0) {
2487
2545
  event.preventDefault();
2488
2546
  event.currentTarget.setPointerCapture(event.pointerId);
2489
- 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
+ };
2490
2558
  suppressClickRef.current = true;
2491
- setDragGuide(getDisplayedColumnLeft(resizeIndex) + metrics[resizeIndex].width);
2559
+ setResizeGuideX(startGuideX);
2492
2560
  return;
2493
2561
  }
2494
- }, [fixedHeader, getDisplayedColumnLeft, headerHeight, locateResizeHandle, metrics, setDragGuide]);
2562
+ }, [findResizeNeighbor, fixedHeader, getDisplayedColumnLeft, headerHeight, locateResizeHandle, metrics]);
2495
2563
  const handleColumnPointerMove = useCallback2((event) => {
2496
2564
  const drag = columnDragRef.current;
2497
2565
  if (!drag) return;
2498
- const width2 = Math.max(getMinimumColumnWidth(columns[drag.columnIndex], columnDraggable), Math.round(drag.startWidth + event.clientX - drag.startX));
2499
- onColumnResize?.(columns[drag.columnIndex].key, width2);
2500
- setDragGuide(getDisplayedColumnLeft(drag.columnIndex) + width2);
2501
- }, [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]);
2502
2582
  const handleColumnPointerUp = useCallback2((event) => {
2503
2583
  const drag = columnDragRef.current;
2504
2584
  if (!drag) return;
2505
2585
  if (event.currentTarget.hasPointerCapture(event.pointerId)) event.currentTarget.releasePointerCapture(event.pointerId);
2506
2586
  columnDragRef.current = null;
2507
- setDragGuide(null);
2508
- }, [setDragGuide]);
2587
+ setResizeGuideX(null);
2588
+ }, []);
2509
2589
  const queueSortStateChange = useCallback2((next) => {
2510
2590
  setDisplaySortState(next);
2511
2591
  if (sortDebounceRef.current !== null) clearTimeout(sortDebounceRef.current);
@@ -2685,7 +2765,7 @@ function Table({
2685
2765
  type: "cell",
2686
2766
  ...cell,
2687
2767
  left: Math.min(Math.max(0, clientX - root.left), Math.max(0, viewport.width - 190)),
2688
- top: Math.min(Math.max(0, clientY - root.top), Math.max(0, effectiveHeight - 350))
2768
+ top: Math.min(Math.max(0, clientY - root.top), Math.max(0, renderHeight - 350))
2689
2769
  });
2690
2770
  onCellContextMenu?.({ ...cell, row: rows[cell.rowIndex], clientX, clientY });
2691
2771
  };
@@ -2696,7 +2776,7 @@ function Table({
2696
2776
  type: "header",
2697
2777
  columnIndex,
2698
2778
  left: Math.min(Math.max(0, clientX - root.left), Math.max(0, viewport.width - 180)),
2699
- top: Math.min(Math.max(0, clientY - root.top), Math.max(0, effectiveHeight - 68))
2779
+ top: Math.min(Math.max(0, clientY - root.top), Math.max(0, renderHeight - 68))
2700
2780
  });
2701
2781
  };
2702
2782
  const getRowDragOffset = (rowIndex) => {
@@ -2912,6 +2992,35 @@ function Table({
2912
2992
  column.key
2913
2993
  );
2914
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
+ }
2915
3024
  return null;
2916
3025
  };
2917
3026
  const rangeHandlePosition = (() => {
@@ -2926,7 +3035,7 @@ function Table({
2926
3035
  const horizontalStart = column.fixed === void 0 ? fixedWidth : 0;
2927
3036
  const horizontalEnd = column.fixed === void 0 ? viewport.width - rightFixedWidth : viewport.width;
2928
3037
  const verticalStart = fixedHeader ? bodyTop : 0;
2929
- if (rawLeft <= horizontalStart || rawLeft > horizontalEnd || rawTop <= verticalStart || rawTop > effectiveHeight) return null;
3038
+ if (rawLeft <= horizontalStart || rawLeft > horizontalEnd || rawTop <= verticalStart || rawTop > renderHeight) return null;
2930
3039
  return { left: rawLeft, top: rawTop, bounds };
2931
3040
  })();
2932
3041
  const selectionOutlineStyle = (() => {
@@ -2943,12 +3052,12 @@ function Table({
2943
3052
  const horizontalStart = column.fixed === void 0 ? fixedWidth : 0;
2944
3053
  const horizontalEnd = column.fixed === void 0 ? viewport.width - rightFixedWidth : viewport.width;
2945
3054
  const verticalStart = fixedHeader ? bodyTop : 0;
2946
- if (rawRight <= horizontalStart || rawLeft >= horizontalEnd || rawBottom <= verticalStart || rawTop >= effectiveHeight) return null;
3055
+ if (rawRight <= horizontalStart || rawLeft >= horizontalEnd || rawBottom <= verticalStart || rawTop >= renderHeight) return null;
2947
3056
  const borderWidth = 2;
2948
3057
  const left = Math.round(Math.max(rawLeft - borderWidth, horizontalStart - 1));
2949
3058
  const right = Math.round(Math.min(rawRight + 1, horizontalEnd, viewport.width));
2950
3059
  const top = Math.round(Math.max(rawTop - borderWidth, verticalStart - 1));
2951
- const bottom = Math.round(Math.min(rawBottom + 1, effectiveHeight));
3060
+ const bottom = Math.round(Math.min(rawBottom + 1, renderHeight));
2952
3061
  return {
2953
3062
  left,
2954
3063
  top,
@@ -3063,8 +3172,10 @@ function Table({
3063
3172
  const renderSummaryCell = (columnIndex, left) => {
3064
3173
  const column = columns[columnIndex];
3065
3174
  const isUtility = column.rowSelection || column.rowDragHandle || column.rowNumber;
3066
- const summaryContent = summaryValues.has(column.key) ? summaryValues.get(column.key) : summaryEmptyValue;
3067
- const content = column.rowNumber ? language === "zh-CN" ? "\u5408\u8BA1" : "Total" : summaryPending ? /* @__PURE__ */ jsx7("span", { className: "rvg-summary-skeleton" }) : summaryContent;
3175
+ const hasSummaryValue = summaryValues.has(column.key);
3176
+ const summaryContent = hasSummaryValue ? summaryValues.get(column.key) : summaryEmptyValue;
3177
+ const showSummarySkeleton = Boolean(column.summary) && summaryPending;
3178
+ const content = columnIndex === 0 ? language === "zh-CN" ? "\u5408\u8BA1" : "Total" : showSummarySkeleton ? /* @__PURE__ */ jsx7("span", { className: "rvg-summary-skeleton" }) : summaryContent;
3068
3179
  return /* @__PURE__ */ jsx7(
3069
3180
  "div",
3070
3181
  {
@@ -3095,7 +3206,7 @@ function Table({
3095
3206
  {
3096
3207
  ref: canvasRef,
3097
3208
  className: "rvg-canvas",
3098
- style: { width: viewport.width, height: effectiveHeight },
3209
+ style: { width: viewport.width, height: renderHeight },
3099
3210
  onMouseMove: (event) => {
3100
3211
  if (columnDragRef.current) return;
3101
3212
  const rect = event.currentTarget.getBoundingClientRect();
@@ -3273,7 +3384,7 @@ function Table({
3273
3384
  onContextMenu: handleContextMenu
3274
3385
  }
3275
3386
  ),
3276
- /* @__PURE__ */ jsxs7("div", { className: "rvg-text-layer", style: { width: viewport.width, height: effectiveHeight, marginTop: -effectiveHeight }, onContextMenu: handleContextMenu, children: [
3387
+ /* @__PURE__ */ jsxs7("div", { className: "rvg-text-layer", style: { width: viewport.width, height: renderHeight, marginTop: -renderHeight }, onContextMenu: handleContextMenu, children: [
3277
3388
  /* @__PURE__ */ jsx7("div", { className: "rvg-text-scroll-clip", style: { left: fixedWidth, right: rightFixedWidth }, children: /* @__PURE__ */ jsxs7(
3278
3389
  "div",
3279
3390
  {
@@ -3297,7 +3408,7 @@ function Table({
3297
3408
  columns.map((column, columnIndex) => column.fixed === "left" ? renderHeaderTitle(columnIndex, metrics[columnIndex].left) : null),
3298
3409
  columns.map((column, columnIndex) => column.fixed === "right" ? renderHeaderTitle(columnIndex, viewport.width - (rightFixedOffsets.get(columnIndex) ?? 0) - metrics[columnIndex].width) : null)
3299
3410
  ] }),
3300
- /* @__PURE__ */ jsx7("div", { className: "rvg-spacer", style: { width: contentWidth, height: contentHeight + bodyTop, marginTop: -effectiveHeight } })
3411
+ /* @__PURE__ */ jsx7("div", { className: "rvg-spacer", style: { width: contentWidth, height: contentHeight + bodyTop, marginTop: -renderHeight } })
3301
3412
  ] }),
3302
3413
  hasSummaryRow && /* @__PURE__ */ jsxs7(
3303
3414
  "div",
@@ -3613,6 +3724,13 @@ function Table({
3613
3724
  ] }) }),
3614
3725
  toast && /* @__PURE__ */ jsx7("div", { className: `rvg-toast is-${toast.type}`, role: "status", children: toast.message }),
3615
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
+ ),
3616
3734
  /* @__PURE__ */ jsx7("div", { ref: rowDragGuideRef, className: "rvg-row-guide" }),
3617
3735
  filterEditor && /* @__PURE__ */ jsxs7("div", { ref: filterRef, className: "rvg-filter", style: { left: filterEditor.left, top: filterEditor.top }, children: [
3618
3736
  /* @__PURE__ */ jsx7(
@@ -3661,7 +3779,7 @@ function Table({
3661
3779
  rows.length === 0 && !loading && /* @__PURE__ */ jsx7("div", { className: "rvg-empty", style: { top: bodyTop }, children: emptyContent ?? /* @__PURE__ */ jsx7(DefaultEmptyState, { label: labels.empty }) }),
3662
3780
  loading && hasCustomLoading && /* @__PURE__ */ jsx7("div", { className: "rvg-custom-loading", "aria-label": labels.refreshing, children: loadingContent }),
3663
3781
  loading && !hasCustomLoading && !hasCompletedLoadRef.current && /* @__PURE__ */ jsxs7("div", { className: "rvg-initial-loading", style: { top: headerHeight }, "aria-label": labels.initialLoading, children: [
3664
- Array.from({ length: Math.ceil((effectiveHeight - headerHeight) / rowHeight) }, (_, rowIndex) => /* @__PURE__ */ jsx7("div", { className: "rvg-loading-row", style: { height: rowHeight }, children: columns.map((column, columnIndex) => {
3782
+ Array.from({ length: Math.ceil((renderHeight - headerHeight) / rowHeight) }, (_, rowIndex) => /* @__PURE__ */ jsx7("div", { className: "rvg-loading-row", style: { height: rowHeight }, children: columns.map((column, columnIndex) => {
3665
3783
  const left = getDisplayedColumnLeft(columnIndex);
3666
3784
  const columnWidth = metrics[columnIndex].width;
3667
3785
  if (left + columnWidth <= 0 || left >= viewport.width) return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lensui/lens-table",
3
- "version": "0.1.1",
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",