@lensui/lens-table 0.1.3 → 0.1.4

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/dist/index.cjs CHANGED
@@ -893,6 +893,8 @@ function ellipsizeText(context, text, maxWidth) {
893
893
  }
894
894
  function paintGrid(options) {
895
895
  const { context: ctx, width, height, pixelRatio, scrollLeft, scrollTop, rowHeight, headerHeight, fixedHeader, verticalBorderless, striped, columnDraggable, sortState, filterValues, hoveredHeaderAction, rows, columns, metrics, range, selection, editing, hoveredRowIndex, selectionRange, selectedRowKeys, selectedColumnKeys, highlightEditedCells, highlightInsertedRows, insertedRowKeys, editedCellKeys, cellAnnotations, getRowKey, rowDragPreview } = options;
896
+ const headerLeafTop = options.headerLeafTop ?? 0;
897
+ const headerLeafHeight = options.headerLeafHeight ?? headerHeight;
896
898
  const colors = { ...COLORS, ...options.colors };
897
899
  const bodyTop = options.bodyTop ?? headerHeight;
898
900
  const getCellSpan = (rowIndex, columnIndex) => options.cellSpans.get(getCellCoordKey(rowIndex, columnIndex));
@@ -909,9 +911,16 @@ function paintGrid(options) {
909
911
  ctx.fillRect(0, 0, width, height);
910
912
  ctx.font = "13px Inter, ui-sans-serif, system-ui, sans-serif";
911
913
  ctx.textBaseline = "middle";
912
- const leftFixedWidth = columns.reduce((value, column, index) => column.fixed === "left" ? Math.max(value, metrics[index].right) : value, 0);
914
+ const leftFixedWidth = columns.reduce((value, column, index) => column.fixed === "left" ? value + metrics[index].width : value, 0);
913
915
  const rightFixedWidth = columns.reduce((value, column, index) => column.fixed === "right" ? value + metrics[index].width : value, 0);
914
916
  const rightFixedLeft = width - rightFixedWidth;
917
+ const leftOffsets = /* @__PURE__ */ new Map();
918
+ let leftOffset = 0;
919
+ for (let index = 0; index < columns.length; index += 1) {
920
+ if (columns[index].fixed !== "left") continue;
921
+ leftOffsets.set(index, leftOffset);
922
+ leftOffset += metrics[index].width;
923
+ }
915
924
  const rightOffsets = /* @__PURE__ */ new Map();
916
925
  let rightOffset = 0;
917
926
  for (let index = columns.length - 1; index >= 0; index -= 1) {
@@ -919,8 +928,14 @@ function paintGrid(options) {
919
928
  rightOffsets.set(index, rightOffset);
920
929
  rightOffset += metrics[index].width;
921
930
  }
931
+ const scrollableLefts = /* @__PURE__ */ new Map();
932
+ let scrollableLeft = leftFixedWidth;
933
+ for (let index = 0; index < columns.length; index += 1) {
934
+ scrollableLefts.set(index, scrollableLeft);
935
+ if (columns[index].fixed === void 0) scrollableLeft += metrics[index].width;
936
+ }
922
937
  const getColumnX = (index) => {
923
- return columns[index].fixed === "left" ? metrics[index].left : columns[index].fixed === "right" ? width - (rightOffsets.get(index) ?? 0) - metrics[index].width : metrics[index].left - scrollLeft;
938
+ return columns[index].fixed === "left" ? leftOffsets.get(index) ?? 0 : columns[index].fixed === "right" ? width - (rightOffsets.get(index) ?? 0) - metrics[index].width : (scrollableLefts.get(index) ?? leftFixedWidth) - scrollLeft;
924
939
  };
925
940
  const paintBodyColumns = (layer) => {
926
941
  const rowStart = Math.max(0, range.rowStart - Math.max(0, options.maxRowSpan - 1));
@@ -1070,7 +1085,7 @@ function paintGrid(options) {
1070
1085
  if (!verticalBorderless) ctx.fillRect(x + metric.width - 1, headerY, 1, headerHeight);
1071
1086
  ctx.fillRect(x, headerY + headerHeight - 1, metric.width, 1);
1072
1087
  const column = columns[columnIndex];
1073
- if (!column.rowSelection && !column.rowDragHandle && !column.rowNumber && !column.renderHeader && column.title) {
1088
+ if (headerLeafTop === 0 && !column.rowSelection && !column.rowDragHandle && !column.rowNumber && !column.renderHeader && column.title) {
1074
1089
  ctx.font = "600 13px Inter, ui-sans-serif, system-ui, sans-serif";
1075
1090
  const titleWidth = ctx.measureText(column.title).width;
1076
1091
  const visibleActions = getVisibleHeaderActions(column, metric.width, columnDraggable, titleWidth);
@@ -1087,11 +1102,11 @@ function paintGrid(options) {
1087
1102
  const textX = titleAlign === "right" ? x + contentWidth - padding : titleAlign === "center" ? x + centeredTextX : x + padding;
1088
1103
  ctx.save();
1089
1104
  ctx.beginPath();
1090
- ctx.rect(x + 2, headerY + 1, Math.max(0, clipWidth - 4), headerHeight - 2);
1105
+ ctx.rect(x + 2, headerY + headerLeafTop + 1, Math.max(0, clipWidth - 4), headerLeafHeight - 2);
1091
1106
  ctx.clip();
1092
1107
  ctx.fillStyle = colors.muted;
1093
1108
  ctx.textAlign = titleAlign === "right" ? "right" : titleAlign === "center" ? "center" : "left";
1094
- ctx.fillText(title, textX, headerY + headerHeight / 2);
1109
+ ctx.fillText(title, textX, headerY + headerLeafTop + headerLeafHeight / 2);
1095
1110
  ctx.restore();
1096
1111
  }
1097
1112
  }
@@ -1099,22 +1114,6 @@ function paintGrid(options) {
1099
1114
  paintHeaderColumns("scroll");
1100
1115
  paintHeaderColumns("left");
1101
1116
  paintHeaderColumns("right");
1102
- if (leftFixedWidth > 0 && scrollLeft > 0) {
1103
- const shadowWidth = 10;
1104
- const shadow = ctx.createLinearGradient(leftFixedWidth, 0, leftFixedWidth + shadowWidth, 0);
1105
- shadow.addColorStop(0, "rgba(5, 5, 5, 0.06)");
1106
- shadow.addColorStop(1, "rgba(5, 5, 5, 0)");
1107
- ctx.fillStyle = shadow;
1108
- ctx.fillRect(leftFixedWidth, 0, shadowWidth, height);
1109
- }
1110
- if (rightFixedWidth > 0 && scrollLeft < (metrics[metrics.length - 1]?.right ?? 0) - width) {
1111
- const shadowWidth = 10;
1112
- const shadow = ctx.createLinearGradient(rightFixedLeft - shadowWidth, 0, rightFixedLeft, 0);
1113
- shadow.addColorStop(0, "rgba(5, 5, 5, 0)");
1114
- shadow.addColorStop(1, "rgba(5, 5, 5, 0.06)");
1115
- ctx.fillStyle = shadow;
1116
- ctx.fillRect(rightFixedLeft - shadowWidth, 0, shadowWidth, height);
1117
- }
1118
1117
  if (rightFixedWidth > 0 && !verticalBorderless) {
1119
1118
  ctx.fillStyle = colors.grid;
1120
1119
  ctx.fillRect(rightFixedLeft - 1, 0, 1, height);
@@ -1266,6 +1265,45 @@ var getRowNumberColumnWidth = (rowCount) => {
1266
1265
  const digits = String(Math.max(1, rowCount)).length;
1267
1266
  return digits <= 4 ? 44 : 44 + (digits - 4) * 8;
1268
1267
  };
1268
+ function getColumnDepth(column) {
1269
+ if (!column.children?.length) return 1;
1270
+ return 1 + Math.max(...column.children.map(getColumnDepth));
1271
+ }
1272
+ function flattenDataColumns(columns, inheritedFixed, insideGroup = false) {
1273
+ return columns.flatMap((column) => {
1274
+ if (!column.children?.length) {
1275
+ return insideGroup ? [{ ...column, fixed: inheritedFixed }] : [column];
1276
+ }
1277
+ return flattenDataColumns(column.children, column.fixed ?? inheritedFixed, true);
1278
+ });
1279
+ }
1280
+ function buildHeaderCells(sourceColumns, utilityColumnCount, depth) {
1281
+ const cells = [];
1282
+ let leafIndex = utilityColumnCount;
1283
+ const walk = (column, level, inheritedFixed, insideGroup, rootIndex, parentKey) => {
1284
+ const fixedColumn = insideGroup ? { ...column, fixed: inheritedFixed } : column;
1285
+ if (!fixedColumn.children?.length) {
1286
+ const startIndex2 = leafIndex;
1287
+ leafIndex += 1;
1288
+ cells.push({ key: fixedColumn.key, column: fixedColumn, level: depth - 1, startIndex: startIndex2, endIndex: startIndex2, leaf: true, parentKey, rootIndex });
1289
+ return { startIndex: startIndex2, endIndex: startIndex2 };
1290
+ }
1291
+ const startIndex = leafIndex;
1292
+ fixedColumn.children.forEach((child) => walk(child, level + 1, fixedColumn.fixed, true, rootIndex, fixedColumn.key));
1293
+ const endIndex = leafIndex - 1;
1294
+ cells.push({ key: fixedColumn.key, column: fixedColumn, level, startIndex, endIndex, leaf: false, parentKey, rootIndex });
1295
+ return { startIndex, endIndex };
1296
+ };
1297
+ sourceColumns.forEach((column, index) => walk(column, 0, void 0, false, index));
1298
+ return cells;
1299
+ }
1300
+ function hasNestedFixedColumns(columns, insideGroup = false) {
1301
+ return columns.some((column) => insideGroup && column.fixed !== void 0 || (column.children?.length ? hasNestedFixedColumns(column.children, true) : false));
1302
+ }
1303
+ function isProductionRuntime() {
1304
+ const env = globalThis.process?.env?.NODE_ENV;
1305
+ return env === "production";
1306
+ }
1269
1307
  var createPendingInsertId = () => `table-inserted-${globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(36).slice(2)}`}`;
1270
1308
  var waitForPaint = () => new Promise((resolve) => {
1271
1309
  requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
@@ -1380,10 +1418,24 @@ function Table({
1380
1418
  }) {
1381
1419
  const rowHeight = layout?.rowHeight ?? rowHeightProp ?? 36;
1382
1420
  const baseHeaderHeight = layout?.headerHeight ?? headerHeightProp ?? 40;
1421
+ const headerDepth = (0, import_react5.useMemo)(() => Math.max(1, ...sourceColumns.map(getColumnDepth)), [sourceColumns]);
1383
1422
  const { language, labels } = (0, import_react5.useMemo)(() => resolveGridLocale(locale), [locale]);
1384
1423
  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);
1424
+ const customHeaderMeasurementsRef = (0, import_react5.useRef)(/* @__PURE__ */ new Map());
1425
+ const [measuredHeaderRowHeights, setMeasuredHeaderRowHeights] = (0, import_react5.useState)([]);
1426
+ const headerRowHeights = (0, import_react5.useMemo)(() => Array.from({ length: headerDepth }, (_, level) => Math.max(baseHeaderHeight, measuredHeaderRowHeights[level] ?? 0)), [baseHeaderHeight, headerDepth, measuredHeaderRowHeights]);
1427
+ const headerRowOffsets = (0, import_react5.useMemo)(() => {
1428
+ const offsets = [];
1429
+ let top = 0;
1430
+ headerRowHeights.forEach((height2) => {
1431
+ offsets.push(top);
1432
+ top += height2;
1433
+ });
1434
+ return offsets;
1435
+ }, [headerRowHeights]);
1436
+ const headerHeight = headerRowHeights.reduce((total, height2) => total + height2, 0);
1437
+ const headerLeafTop = headerRowOffsets[headerDepth - 1] ?? 0;
1438
+ const headerLeafHeight = headerRowHeights[headerDepth - 1] ?? baseHeaderHeight;
1387
1439
  const hasVerticalBorders = !(borderless ?? verticalBorderless ?? false);
1388
1440
  const hasStripedRows = Boolean(striped);
1389
1441
  const stripedColor = typeof striped === "string" ? striped : void 0;
@@ -1403,6 +1455,13 @@ function Table({
1403
1455
  const summaryEmptyValue = typeof summary === "object" ? summary.emptyValue ?? "" : "";
1404
1456
  const contextMenuEnabled = contextMenuConfig !== false;
1405
1457
  const customContextMenuConfig = typeof contextMenuConfig === "object" ? contextMenuConfig : void 0;
1458
+ const [resizedColumnWidths, setResizedColumnWidths] = (0, import_react5.useState)({});
1459
+ const leafSourceColumns = (0, import_react5.useMemo)(() => flattenDataColumns(sourceColumns), [sourceColumns]);
1460
+ (0, import_react5.useEffect)(() => {
1461
+ if (!isProductionRuntime() && hasNestedFixedColumns(sourceColumns)) {
1462
+ console.warn("[Table] Multi-level headers only support fixed columns on top-level columns. Nested fixed values are ignored.");
1463
+ }
1464
+ }, [sourceColumns]);
1406
1465
  const columns = (0, import_react5.useMemo)(() => {
1407
1466
  const utilityColumns = [];
1408
1467
  if (rowDraggable) {
@@ -1414,9 +1473,24 @@ function Table({
1414
1473
  if (rowNumber) {
1415
1474
  utilityColumns.push({ key: "__rvg_row_number__", title: "#", width: getRowNumberColumnWidth(sourceRows.length), align: "center", fixed: "left", rowNumber: true });
1416
1475
  }
1417
- return [...utilityColumns, ...sourceColumns];
1418
- }, [rowDraggable, rowNumber, rowSelection, sourceColumns, sourceRows.length]);
1476
+ const dataColumns = leafSourceColumns.map((column) => {
1477
+ const resizedWidth = resizedColumnWidths[column.key];
1478
+ return resizedWidth === void 0 ? column : { ...column, width: resizedWidth };
1479
+ });
1480
+ return [...utilityColumns, ...dataColumns];
1481
+ }, [leafSourceColumns, resizedColumnWidths, rowDraggable, rowNumber, rowSelection, sourceRows.length]);
1419
1482
  const utilityColumnCount = (rowDraggable ? 1 : 0) + (rowSelection ? 1 : 0) + (rowNumber ? 1 : 0);
1483
+ const headerCells = (0, import_react5.useMemo)(() => buildHeaderCells(sourceColumns, utilityColumnCount, headerDepth), [headerDepth, sourceColumns, utilityColumnCount]);
1484
+ const activeCustomHeaderLevels = (0, import_react5.useMemo)(() => {
1485
+ const levels = /* @__PURE__ */ new Map();
1486
+ columns.forEach((column) => {
1487
+ if (column.renderHeader) levels.set(column.key, headerDepth - 1);
1488
+ });
1489
+ headerCells.forEach((cell) => {
1490
+ if (cell.column.renderHeader) levels.set(`group:${cell.key}:${cell.level}`, cell.level);
1491
+ });
1492
+ return levels;
1493
+ }, [columns, headerCells, headerDepth]);
1420
1494
  const canvasRef = (0, import_react5.useRef)(null);
1421
1495
  const scrollerRef = (0, import_react5.useRef)(null);
1422
1496
  const frameRef = (0, import_react5.useRef)(null);
@@ -1434,6 +1508,14 @@ function Table({
1434
1508
  const dragGuideRef = (0, import_react5.useRef)(null);
1435
1509
  const rowDragGuideRef = (0, import_react5.useRef)(null);
1436
1510
  const selectionFocusRef = (0, import_react5.useRef)(null);
1511
+ const verticalScrollbarRef = (0, import_react5.useRef)(null);
1512
+ const verticalScrollbarThumbRef = (0, import_react5.useRef)(null);
1513
+ const verticalScrollbarDragRef = (0, import_react5.useRef)(null);
1514
+ const horizontalScrollbarRef = (0, import_react5.useRef)(null);
1515
+ const horizontalScrollbarThumbRef = (0, import_react5.useRef)(null);
1516
+ const horizontalScrollbarDragRef = (0, import_react5.useRef)(null);
1517
+ const verticalScrollbarVisibilityTimerRef = (0, import_react5.useRef)(null);
1518
+ const horizontalScrollbarVisibilityTimerRef = (0, import_react5.useRef)(null);
1437
1519
  const rowByKeyRef = (0, import_react5.useRef)(/* @__PURE__ */ new Map());
1438
1520
  const rowIndexByKeyRef = (0, import_react5.useRef)(/* @__PURE__ */ new Map());
1439
1521
  const headerIconsRef = (0, import_react5.useRef)(null);
@@ -1486,24 +1568,57 @@ function Table({
1486
1568
  rangeDragRef.current = null;
1487
1569
  setSelectionRange(null);
1488
1570
  }, [rangeSelection]);
1571
+ (0, import_react5.useEffect)(() => {
1572
+ setResizedColumnWidths((current) => {
1573
+ const sourceColumnByKey = new Map(leafSourceColumns.map((column) => [column.key, column]));
1574
+ let changed = false;
1575
+ const next = {};
1576
+ Object.entries(current).forEach(([key, value]) => {
1577
+ const sourceColumn = sourceColumnByKey.get(key);
1578
+ if (!sourceColumn) {
1579
+ changed = true;
1580
+ return;
1581
+ }
1582
+ if (sourceColumn.width !== void 0 && sourceColumn.width !== value) {
1583
+ changed = true;
1584
+ return;
1585
+ }
1586
+ next[key] = value;
1587
+ });
1588
+ return changed ? next : current;
1589
+ });
1590
+ }, [leafSourceColumns]);
1489
1591
  (0, import_react5.useEffect)(() => () => {
1490
1592
  if (headerTooltipTimerRef.current !== null) clearTimeout(headerTooltipTimerRef.current);
1491
1593
  if (cellTooltipTimerRef.current !== null) clearTimeout(cellTooltipTimerRef.current);
1492
1594
  }, []);
1493
1595
  const measureCustomHeaderHeight = (0, import_react5.useCallback)(() => {
1494
- let nextHeight = 0;
1596
+ const nextHeights = Array.from({ length: headerDepth }, () => baseHeaderHeight);
1495
1597
  customHeaderContentRefs.current.forEach((node) => {
1496
- nextHeight = Math.max(nextHeight, Math.ceil(node.scrollHeight) + 16);
1598
+ const headerCell = node.closest(".rvg-header-title");
1599
+ const key = headerCell?.dataset.measureKey;
1600
+ const level = key ? activeCustomHeaderLevels.get(key) : void 0;
1601
+ if (!key || level === void 0 || level < 0 || level >= nextHeights.length) return;
1602
+ customHeaderMeasurementsRef.current.set(key, { level, height: Math.ceil(node.scrollHeight) + 16 });
1497
1603
  });
1498
- setMeasuredHeaderHeight((current) => current === nextHeight ? current : nextHeight);
1499
- }, []);
1604
+ customHeaderMeasurementsRef.current.forEach((measurement, key) => {
1605
+ if (!activeCustomHeaderLevels.has(key)) return;
1606
+ nextHeights[measurement.level] = Math.max(nextHeights[measurement.level], measurement.height);
1607
+ });
1608
+ setMeasuredHeaderRowHeights((current) => {
1609
+ const changed = nextHeights.length !== current.length || nextHeights.some((height2, index) => height2 !== current[index]);
1610
+ return changed ? nextHeights : current;
1611
+ });
1612
+ }, [activeCustomHeaderLevels, baseHeaderHeight, headerDepth]);
1500
1613
  (0, import_react5.useLayoutEffect)(() => {
1501
- const activeCustomHeaderKeys = new Set(columns.filter((column) => column.renderHeader).map((column) => column.key));
1502
1614
  customHeaderContentRefs.current.forEach((_, key) => {
1503
- if (!activeCustomHeaderKeys.has(key)) customHeaderContentRefs.current.delete(key);
1615
+ if (!activeCustomHeaderLevels.has(key)) customHeaderContentRefs.current.delete(key);
1616
+ });
1617
+ customHeaderMeasurementsRef.current.forEach((_, key) => {
1618
+ if (!activeCustomHeaderLevels.has(key)) customHeaderMeasurementsRef.current.delete(key);
1504
1619
  });
1505
- if (customHeaderContentRefs.current.size === 0) {
1506
- setMeasuredHeaderHeight(0);
1620
+ if (activeCustomHeaderLevels.size === 0) {
1621
+ setMeasuredHeaderRowHeights([]);
1507
1622
  return;
1508
1623
  }
1509
1624
  measureCustomHeaderHeight();
@@ -1511,7 +1626,7 @@ function Table({
1511
1626
  const observer = new ResizeObserver(measureCustomHeaderHeight);
1512
1627
  customHeaderContentRefs.current.forEach((node) => observer.observe(node));
1513
1628
  return () => observer.disconnect();
1514
- }, [columns, measureCustomHeaderHeight, viewport.width]);
1629
+ }, [activeCustomHeaderLevels, measureCustomHeaderHeight, viewport.width]);
1515
1630
  const [filterEditor, setFilterEditor] = (0, import_react5.useState)(null);
1516
1631
  const [contextMenu, setContextMenu] = (0, import_react5.useState)(null);
1517
1632
  const [insertCounts, setInsertCounts] = (0, import_react5.useState)({ before: 1, after: 1 });
@@ -1521,6 +1636,7 @@ function Table({
1521
1636
  const [deleteBusy, setDeleteBusy] = (0, import_react5.useState)(false);
1522
1637
  const [toast, setToast] = (0, import_react5.useState)(null);
1523
1638
  const [resizeGuideX, setResizeGuideX] = (0, import_react5.useState)(null);
1639
+ const [resizeGuideTop, setResizeGuideTop] = (0, import_react5.useState)(0);
1524
1640
  const [hoveredHeaderAction, setHoveredHeaderAction] = (0, import_react5.useState)(null);
1525
1641
  const [visibleHeaderTooltip, setVisibleHeaderTooltip] = (0, import_react5.useState)(null);
1526
1642
  const [hoveredCellTooltip, setHoveredCellTooltip] = (0, import_react5.useState)(null);
@@ -1605,7 +1721,17 @@ function Table({
1605
1721
  const metrics = (0, import_react5.useMemo)(() => buildColumnMetrics(columns, { columnDraggable, viewportWidth: viewport.width }), [columnDraggable, columns, viewport.width]);
1606
1722
  const contentWidth = metrics.length > 0 ? metrics[metrics.length - 1].right : 0;
1607
1723
  const contentHeight = rows.length * rowHeight;
1608
- const fixedWidth = (0, import_react5.useMemo)(() => columns.reduce((width2, column, index) => column.fixed === "left" ? Math.max(width2, metrics[index].right) : width2, 0), [columns, metrics]);
1724
+ const fixedWidth = (0, import_react5.useMemo)(() => columns.reduce((width2, column, index) => column.fixed === "left" ? width2 + metrics[index].width : width2, 0), [columns, metrics]);
1725
+ const leftFixedOffsets = (0, import_react5.useMemo)(() => {
1726
+ const offsets = /* @__PURE__ */ new Map();
1727
+ let offset = 0;
1728
+ for (let index = 0; index < columns.length; index += 1) {
1729
+ if (columns[index].fixed !== "left") continue;
1730
+ offsets.set(index, offset);
1731
+ offset += metrics[index].width;
1732
+ }
1733
+ return offsets;
1734
+ }, [columns, metrics]);
1609
1735
  const rightFixedWidth = (0, import_react5.useMemo)(() => columns.reduce((width2, column, index) => column.fixed === "right" ? width2 + metrics[index].width : width2, 0), [columns, metrics]);
1610
1736
  const rightFixedOffsets = (0, import_react5.useMemo)(() => {
1611
1737
  const offsets = /* @__PURE__ */ new Map();
@@ -1617,6 +1743,16 @@ function Table({
1617
1743
  }
1618
1744
  return offsets;
1619
1745
  }, [columns, metrics]);
1746
+ const scrollableColumnLefts = (0, import_react5.useMemo)(() => {
1747
+ const lefts = [];
1748
+ let width2 = fixedWidth;
1749
+ for (let index = 0; index < columns.length; index += 1) {
1750
+ lefts[index] = width2;
1751
+ if (columns[index].fixed === void 0) width2 += metrics[index].width;
1752
+ }
1753
+ return lefts;
1754
+ }, [columns, fixedWidth, metrics]);
1755
+ const getScrollableColumnLeft = (0, import_react5.useCallback)((columnIndex) => scrollableColumnLefts[columnIndex] ?? fixedWidth, [fixedWidth, scrollableColumnLefts]);
1620
1756
  const hasRowSelectionColumn = (0, import_react5.useMemo)(() => columns.some((column) => column.rowSelection), [columns]);
1621
1757
  const rowSelectionMode = typeof rowSelection === "object" ? rowSelection.mode ?? "multiple" : "multiple";
1622
1758
  const hasSummaryRow = (0, import_react5.useMemo)(() => rows.length > 0 && summaryEnabled && columns.some((column) => Boolean(column.summary)), [columns, rows.length, summaryEnabled]);
@@ -1627,7 +1763,7 @@ function Table({
1627
1763
  const realContentHeight = bodyTop + bodyContentHeight + bottomSummaryHeight + (contentWidth > viewport.width ? horizontalScrollbarHeight : 0);
1628
1764
  const effectiveHeight = autoHeight ? Math.max(bodyTop + bottomSummaryHeight, Math.min(resolvedHeight, realContentHeight)) : resolvedHeight;
1629
1765
  const renderHeight = Math.max(0, effectiveHeight - bottomSummaryHeight);
1630
- const bodyViewportHeight = Math.max(0, renderHeight - (fixedHeader ? headerHeight : 0) - topSummaryHeight);
1766
+ const bodyViewportHeight = Math.max(0, renderHeight - (fixedHeader ? headerHeight + topSummaryHeight : 0));
1631
1767
  const [summaryState, setSummaryState] = (0, import_react5.useState)(() => ({ columns: null, rows: null, values: /* @__PURE__ */ new Map(), pending: false }));
1632
1768
  const summaryPending = hasSummaryRow && (loading || summaryState.pending || summaryState.columns !== columns || summaryState.rows !== rows);
1633
1769
  const summaryValues = hasSummaryRow && summaryState.columns === columns && summaryState.rows === rows ? summaryState.values : /* @__PURE__ */ new Map();
@@ -1803,8 +1939,8 @@ function Table({
1803
1939
  const scroll = scrollRef.current;
1804
1940
  const rowScrollTop = fixedHeader ? scroll.top : Math.max(0, scroll.top - bodyTop);
1805
1941
  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 };
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]);
1942
+ paintGrid({ context, width: viewport.width, height: renderHeight, pixelRatio: ratio, scrollLeft: scroll.left, scrollTop: scroll.top, rowHeight, headerHeight, headerLeafTop, headerLeafHeight, 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 });
1943
+ }, [bodyTop, bodyViewportHeight, bottomSummaryHeight, cellAnnotations, cellSpanLookup, columnDraggable, columns, editedCellHighlightColor, editedCellKeys, editing, renderHeight, filterValues, fixedHeader, getRowKey, hasStripedRows, hasVerticalBorders, headerDepth, headerHeight, headerLeafHeight, headerLeafTop, highlightsEditedCells, highlightsInsertedRows, insertedRowHighlightColor, insertedRowKeySet, isVirtualized, metrics, rowDragPreview, rowHeight, rows, selectedColumnKeySet, selectedRowKeySet, selection, selectionRange, sortState, stripedColor, viewport.width, virtualOverscan]);
1808
1944
  const scheduleDraw = (0, import_react5.useCallback)(() => {
1809
1945
  if (frameRef.current === null) frameRef.current = requestAnimationFrame(draw);
1810
1946
  }, [draw]);
@@ -1841,6 +1977,8 @@ function Table({
1841
1977
  if (sortDebounceRef.current !== null) clearTimeout(sortDebounceRef.current);
1842
1978
  if (textFrameRef.current !== null) cancelAnimationFrame(textFrameRef.current);
1843
1979
  if (toastTimerRef.current !== null) clearTimeout(toastTimerRef.current);
1980
+ if (verticalScrollbarVisibilityTimerRef.current !== null) clearTimeout(verticalScrollbarVisibilityTimerRef.current);
1981
+ if (horizontalScrollbarVisibilityTimerRef.current !== null) clearTimeout(horizontalScrollbarVisibilityTimerRef.current);
1844
1982
  }, []);
1845
1983
  (0, import_react5.useEffect)(() => {
1846
1984
  const scroller = scrollerRef.current;
@@ -1892,9 +2030,204 @@ function Table({
1892
2030
  scroller.addEventListener("scroll", lockScroll);
1893
2031
  return () => scroller.removeEventListener("scroll", lockScroll);
1894
2032
  }, [insertBusy]);
2033
+ const showCustomScrollbar = (0, import_react5.useCallback)((axis, persist = false) => {
2034
+ const showVertical = axis === "vertical" || axis === "both";
2035
+ const showHorizontal = axis === "horizontal" || axis === "both";
2036
+ if (showVertical) {
2037
+ verticalScrollbarRef.current?.classList.add("is-active");
2038
+ if (verticalScrollbarVisibilityTimerRef.current !== null) clearTimeout(verticalScrollbarVisibilityTimerRef.current);
2039
+ if (!persist) {
2040
+ verticalScrollbarVisibilityTimerRef.current = setTimeout(() => {
2041
+ verticalScrollbarRef.current?.classList.remove("is-active");
2042
+ verticalScrollbarVisibilityTimerRef.current = null;
2043
+ }, 900);
2044
+ }
2045
+ }
2046
+ if (showHorizontal) {
2047
+ horizontalScrollbarRef.current?.classList.add("is-active");
2048
+ if (horizontalScrollbarVisibilityTimerRef.current !== null) clearTimeout(horizontalScrollbarVisibilityTimerRef.current);
2049
+ if (!persist) {
2050
+ horizontalScrollbarVisibilityTimerRef.current = setTimeout(() => {
2051
+ horizontalScrollbarRef.current?.classList.remove("is-active");
2052
+ horizontalScrollbarVisibilityTimerRef.current = null;
2053
+ }, 900);
2054
+ }
2055
+ }
2056
+ }, []);
2057
+ const keepCustomScrollbarVisible = (0, import_react5.useCallback)((axis) => {
2058
+ showCustomScrollbar(axis, true);
2059
+ }, [showCustomScrollbar]);
2060
+ const releaseCustomScrollbarVisible = (0, import_react5.useCallback)((axis) => {
2061
+ if (axis === "vertical" && verticalScrollbarDragRef.current) return;
2062
+ if (axis === "horizontal" && horizontalScrollbarDragRef.current) return;
2063
+ showCustomScrollbar(axis);
2064
+ }, [showCustomScrollbar]);
2065
+ const updateCustomScrollbars = (0, import_react5.useCallback)(() => {
2066
+ const scroller = scrollerRef.current;
2067
+ const verticalTrack = verticalScrollbarRef.current;
2068
+ const verticalThumb = verticalScrollbarThumbRef.current;
2069
+ const horizontalTrack = horizontalScrollbarRef.current;
2070
+ const horizontalThumb = horizontalScrollbarThumbRef.current;
2071
+ if (!scroller || !verticalTrack || !verticalThumb || !horizontalTrack || !horizontalThumb) return;
2072
+ const maxScrollTop = scroller.scrollHeight - scroller.clientHeight;
2073
+ if (maxScrollTop <= 1 || renderHeight <= 0) {
2074
+ verticalTrack.style.display = "none";
2075
+ } else {
2076
+ const top = fixedHeader ? bodyTop : 0;
2077
+ const bottom = bottomSummaryHeight;
2078
+ const trackHeight = Math.max(0, effectiveHeight - top - bottom);
2079
+ if (trackHeight <= 0) {
2080
+ verticalTrack.style.display = "none";
2081
+ } else {
2082
+ const thumbHeight = Math.max(28, Math.round(trackHeight * (scroller.clientHeight / scroller.scrollHeight)));
2083
+ const maxThumbTop = Math.max(0, trackHeight - thumbHeight);
2084
+ const thumbTop = maxScrollTop > 0 ? Math.round(scroller.scrollTop / maxScrollTop * maxThumbTop) : 0;
2085
+ verticalTrack.style.display = "block";
2086
+ verticalTrack.style.top = `${top}px`;
2087
+ verticalTrack.style.bottom = `${bottom}px`;
2088
+ verticalThumb.style.height = `${Math.min(trackHeight, thumbHeight)}px`;
2089
+ verticalThumb.style.transform = `translateY(${thumbTop}px)`;
2090
+ }
2091
+ }
2092
+ const maxScrollLeft = scroller.scrollWidth - scroller.clientWidth;
2093
+ if (maxScrollLeft <= 1 || viewport.width <= 0) {
2094
+ horizontalTrack.style.display = "none";
2095
+ return;
2096
+ }
2097
+ const trackLeft = fixedWidth;
2098
+ const trackRight = rightFixedWidth;
2099
+ const trackWidth = Math.max(0, viewport.width - trackLeft - trackRight);
2100
+ if (trackWidth <= 0) {
2101
+ horizontalTrack.style.display = "none";
2102
+ return;
2103
+ }
2104
+ const thumbWidth = Math.max(36, Math.round(trackWidth * (scroller.clientWidth / scroller.scrollWidth)));
2105
+ const maxThumbLeft = Math.max(0, trackWidth - thumbWidth);
2106
+ const thumbLeft = maxScrollLeft > 0 ? Math.round(scroller.scrollLeft / maxScrollLeft * maxThumbLeft) : 0;
2107
+ horizontalTrack.style.display = "block";
2108
+ horizontalTrack.style.left = `${trackLeft}px`;
2109
+ horizontalTrack.style.right = `${trackRight}px`;
2110
+ horizontalTrack.style.bottom = `${bottomSummaryHeight + 2}px`;
2111
+ horizontalThumb.style.width = `${Math.min(trackWidth, thumbWidth)}px`;
2112
+ horizontalThumb.style.transform = `translateX(${thumbLeft}px)`;
2113
+ }, [bodyTop, bottomSummaryHeight, effectiveHeight, fixedHeader, fixedWidth, renderHeight, rightFixedWidth, viewport.width]);
2114
+ (0, import_react5.useLayoutEffect)(() => {
2115
+ updateCustomScrollbars();
2116
+ }, [contentHeight, contentWidth, updateCustomScrollbars, viewport.height, viewport.width]);
2117
+ const handleVerticalScrollbarPointerDown = (0, import_react5.useCallback)((event) => {
2118
+ const scroller = scrollerRef.current;
2119
+ const track = verticalScrollbarRef.current;
2120
+ const thumb = verticalScrollbarThumbRef.current;
2121
+ if (!scroller || !track || !thumb || event.button !== 0) return;
2122
+ event.preventDefault();
2123
+ event.currentTarget.setPointerCapture(event.pointerId);
2124
+ const trackRect = track.getBoundingClientRect();
2125
+ const thumbRect = thumb.getBoundingClientRect();
2126
+ const maxScrollTop = scroller.scrollHeight - scroller.clientHeight;
2127
+ const trackHeight = trackRect.height;
2128
+ const thumbHeight = thumbRect.height;
2129
+ showCustomScrollbar("vertical", true);
2130
+ if (event.target === thumb) {
2131
+ verticalScrollbarDragRef.current = {
2132
+ pointerId: event.pointerId,
2133
+ startY: event.clientY,
2134
+ startTop: thumbRect.top - trackRect.top,
2135
+ trackHeight,
2136
+ thumbHeight,
2137
+ maxScrollTop
2138
+ };
2139
+ return;
2140
+ }
2141
+ const nextThumbTop = Math.max(0, Math.min(trackHeight - thumbHeight, event.clientY - trackRect.top - thumbHeight / 2));
2142
+ const maxThumbTop = Math.max(1, trackHeight - thumbHeight);
2143
+ scroller.scrollTop = nextThumbTop / maxThumbTop * maxScrollTop;
2144
+ updateCustomScrollbars();
2145
+ verticalScrollbarDragRef.current = {
2146
+ pointerId: event.pointerId,
2147
+ startY: event.clientY,
2148
+ startTop: nextThumbTop,
2149
+ trackHeight,
2150
+ thumbHeight,
2151
+ maxScrollTop
2152
+ };
2153
+ }, [showCustomScrollbar, updateCustomScrollbars]);
2154
+ const handleVerticalScrollbarPointerMove = (0, import_react5.useCallback)((event) => {
2155
+ const drag = verticalScrollbarDragRef.current;
2156
+ const scroller = scrollerRef.current;
2157
+ if (!drag || !scroller || drag.pointerId !== event.pointerId) return;
2158
+ event.preventDefault();
2159
+ const maxThumbTop = Math.max(1, drag.trackHeight - drag.thumbHeight);
2160
+ const nextThumbTop = Math.max(0, Math.min(maxThumbTop, drag.startTop + event.clientY - drag.startY));
2161
+ scroller.scrollTop = nextThumbTop / maxThumbTop * drag.maxScrollTop;
2162
+ updateCustomScrollbars();
2163
+ }, [updateCustomScrollbars]);
2164
+ const handleVerticalScrollbarPointerUp = (0, import_react5.useCallback)((event) => {
2165
+ const drag = verticalScrollbarDragRef.current;
2166
+ if (!drag || drag.pointerId !== event.pointerId) return;
2167
+ if (event.currentTarget.hasPointerCapture(event.pointerId)) event.currentTarget.releasePointerCapture(event.pointerId);
2168
+ verticalScrollbarDragRef.current = null;
2169
+ showCustomScrollbar("vertical");
2170
+ }, [showCustomScrollbar]);
2171
+ const handleHorizontalScrollbarPointerDown = (0, import_react5.useCallback)((event) => {
2172
+ const scroller = scrollerRef.current;
2173
+ const track = horizontalScrollbarRef.current;
2174
+ const thumb = horizontalScrollbarThumbRef.current;
2175
+ if (!scroller || !track || !thumb || event.button !== 0) return;
2176
+ event.preventDefault();
2177
+ event.currentTarget.setPointerCapture(event.pointerId);
2178
+ const trackRect = track.getBoundingClientRect();
2179
+ const thumbRect = thumb.getBoundingClientRect();
2180
+ const maxScrollLeft = scroller.scrollWidth - scroller.clientWidth;
2181
+ const trackWidth = trackRect.width;
2182
+ const thumbWidth = thumbRect.width;
2183
+ showCustomScrollbar("horizontal", true);
2184
+ if (event.target === thumb) {
2185
+ horizontalScrollbarDragRef.current = {
2186
+ pointerId: event.pointerId,
2187
+ startX: event.clientX,
2188
+ startLeft: thumbRect.left - trackRect.left,
2189
+ trackWidth,
2190
+ thumbWidth,
2191
+ maxScrollLeft
2192
+ };
2193
+ return;
2194
+ }
2195
+ const nextThumbLeft = Math.max(0, Math.min(trackWidth - thumbWidth, event.clientX - trackRect.left - thumbWidth / 2));
2196
+ const maxThumbLeft = Math.max(1, trackWidth - thumbWidth);
2197
+ scroller.scrollLeft = nextThumbLeft / maxThumbLeft * maxScrollLeft;
2198
+ updateCustomScrollbars();
2199
+ horizontalScrollbarDragRef.current = {
2200
+ pointerId: event.pointerId,
2201
+ startX: event.clientX,
2202
+ startLeft: nextThumbLeft,
2203
+ trackWidth,
2204
+ thumbWidth,
2205
+ maxScrollLeft
2206
+ };
2207
+ }, [showCustomScrollbar, updateCustomScrollbars]);
2208
+ const handleHorizontalScrollbarPointerMove = (0, import_react5.useCallback)((event) => {
2209
+ const drag = horizontalScrollbarDragRef.current;
2210
+ const scroller = scrollerRef.current;
2211
+ if (!drag || !scroller || drag.pointerId !== event.pointerId) return;
2212
+ event.preventDefault();
2213
+ const maxThumbLeft = Math.max(1, drag.trackWidth - drag.thumbWidth);
2214
+ const nextThumbLeft = Math.max(0, Math.min(maxThumbLeft, drag.startLeft + event.clientX - drag.startX));
2215
+ scroller.scrollLeft = nextThumbLeft / maxThumbLeft * drag.maxScrollLeft;
2216
+ updateCustomScrollbars();
2217
+ }, [updateCustomScrollbars]);
2218
+ const handleHorizontalScrollbarPointerUp = (0, import_react5.useCallback)((event) => {
2219
+ const drag = horizontalScrollbarDragRef.current;
2220
+ if (!drag || drag.pointerId !== event.pointerId) return;
2221
+ if (event.currentTarget.hasPointerCapture(event.pointerId)) event.currentTarget.releasePointerCapture(event.pointerId);
2222
+ horizontalScrollbarDragRef.current = null;
2223
+ showCustomScrollbar("horizontal");
2224
+ }, [showCustomScrollbar]);
1895
2225
  const handleScroll = (0, import_react5.useCallback)(() => {
1896
2226
  const element = scrollerRef.current;
1897
2227
  if (!element) return;
2228
+ const previousScroll = scrollRef.current;
2229
+ const scrollDeltaX = element.scrollLeft - previousScroll.left;
2230
+ const scrollDeltaY = element.scrollTop - previousScroll.top;
1898
2231
  scrollRef.current = { left: element.scrollLeft, top: element.scrollTop };
1899
2232
  const focus = selectionFocusRef.current;
1900
2233
  if (focus && selection && !selectionRange) {
@@ -1905,7 +2238,7 @@ function Table({
1905
2238
  if (!validSelection) {
1906
2239
  focus.style.display = "none";
1907
2240
  } else {
1908
- const rawLeft = column.fixed === "left" ? metric.left : column.fixed === "right" ? viewport.width - (rightFixedOffsets.get(selection.columnIndex) ?? 0) - metric.width : metric.left - element.scrollLeft;
2241
+ const rawLeft = column.fixed === "left" ? leftFixedOffsets.get(selection.columnIndex) ?? 0 : column.fixed === "right" ? viewport.width - (rightFixedOffsets.get(selection.columnIndex) ?? 0) - metric.width : getScrollableColumnLeft(selection.columnIndex) - element.scrollLeft;
1909
2242
  const rawTop = bodyTop + selection.rowIndex * rowHeight - element.scrollTop;
1910
2243
  const span = getCellSpan(selection.rowIndex, selection.columnIndex);
1911
2244
  const rawRight = rawLeft + getCellDisplayWidth(selection.columnIndex, span?.colSpan ?? 1);
@@ -1938,6 +2271,10 @@ function Table({
1938
2271
  if (scrollingTextRef.current) scrollingTextRef.current.style.transform = `translateX(${-element.scrollLeft}px)`;
1939
2272
  if (scrollingHeaderIconsRef.current) scrollingHeaderIconsRef.current.style.transform = `translateX(${-element.scrollLeft}px)`;
1940
2273
  if (headerIconsRef.current) headerIconsRef.current.style.transform = `translateY(${fixedHeader ? 0 : -element.scrollTop}px)`;
2274
+ updateCustomScrollbars();
2275
+ if (scrollDeltaX !== 0 || scrollDeltaY !== 0) {
2276
+ showCustomScrollbar(scrollDeltaX !== 0 && scrollDeltaY !== 0 ? "both" : scrollDeltaX !== 0 ? "horizontal" : "vertical");
2277
+ }
1941
2278
  if (hoveredHeaderActionRef.current) {
1942
2279
  hoveredHeaderActionRef.current = null;
1943
2280
  setHoveredHeaderAction(null);
@@ -1945,7 +2282,7 @@ function Table({
1945
2282
  setHoveredCellTooltip(null);
1946
2283
  scheduleDraw();
1947
2284
  setEditing(null);
1948
- }, [bodyTop, columns, renderHeight, fixedHeader, fixedWidth, getCellDisplayWidth, getCellSpan, getRowKey, metrics, rightFixedOffsets, rightFixedWidth, rowHeight, rows, scheduleDraw, selection, selectionRange, viewport.width]);
2285
+ }, [bodyTop, columns, renderHeight, fixedHeader, fixedWidth, getCellDisplayWidth, getCellSpan, getRowKey, getScrollableColumnLeft, metrics, rightFixedOffsets, rightFixedWidth, rowHeight, rows, scheduleDraw, selection, selectionRange, showCustomScrollbar, updateCustomScrollbars, viewport.width]);
1949
2286
  const locateColumn = (0, import_react5.useCallback)((clientX) => {
1950
2287
  const canvas = canvasRef.current;
1951
2288
  if (!canvas) return -1;
@@ -1953,7 +2290,11 @@ function Table({
1953
2290
  const localX = clientX - rect.left;
1954
2291
  let columnIndex;
1955
2292
  if (localX < fixedWidth) {
1956
- columnIndex = hitTestColumn(metrics, localX);
2293
+ columnIndex = columns.findIndex((column, index) => {
2294
+ if (column.fixed !== "left") return false;
2295
+ const left = leftFixedOffsets.get(index) ?? 0;
2296
+ return localX >= left && localX < left + metrics[index].width;
2297
+ });
1957
2298
  } else if (rightFixedWidth > 0 && localX >= viewport.width - rightFixedWidth) {
1958
2299
  columnIndex = columns.findIndex((column, index) => {
1959
2300
  if (column.fixed !== "right") return false;
@@ -1961,41 +2302,33 @@ function Table({
1961
2302
  return localX >= left && localX < left + metrics[index].width;
1962
2303
  });
1963
2304
  } else {
1964
- columnIndex = hitTestColumn(metrics, localX + scrollRef.current.left);
2305
+ const scrollX = localX + scrollRef.current.left;
2306
+ columnIndex = columns.findIndex((column, index) => {
2307
+ if (column.fixed !== void 0) return false;
2308
+ const left = getScrollableColumnLeft(index);
2309
+ return scrollX >= left && scrollX < left + metrics[index].width;
2310
+ });
1965
2311
  }
1966
2312
  return columnIndex;
1967
- }, [columns, fixedWidth, metrics, rightFixedOffsets, rightFixedWidth, viewport.width]);
2313
+ }, [columns, fixedWidth, getScrollableColumnLeft, leftFixedOffsets, metrics, rightFixedOffsets, rightFixedWidth, viewport.width]);
1968
2314
  const getDisplayedColumnLeft = (0, import_react5.useCallback)((columnIndex) => {
1969
2315
  const metric = metrics[columnIndex];
1970
- if (columns[columnIndex].fixed === "left") return metric.left;
2316
+ if (columns[columnIndex].fixed === "left") return leftFixedOffsets.get(columnIndex) ?? 0;
1971
2317
  if (columns[columnIndex].fixed === "right") return viewport.width - (rightFixedOffsets.get(columnIndex) ?? 0) - metric.width;
1972
- return metric.left - scrollRef.current.left;
1973
- }, [columns, metrics, rightFixedOffsets, viewport.width]);
1974
- const locateResizeHandle = (0, import_react5.useCallback)((clientX) => {
1975
- if (!columnResizable || !canvasRef.current) return -1;
1976
- const localX = clientX - canvasRef.current.getBoundingClientRect().left;
1977
- let closest = -1;
1978
- let distance = 6;
1979
- for (let index = 0; index < columns.length; index += 1) {
1980
- if (columns[index].rowSelection || columns[index].rowDragHandle || columns[index].rowNumber) continue;
1981
- const nextDistance = Math.abs(localX - (getDisplayedColumnLeft(index) + metrics[index].width));
1982
- if (nextDistance < distance) {
1983
- distance = nextDistance;
1984
- closest = index;
1985
- }
2318
+ return getScrollableColumnLeft(columnIndex) - scrollRef.current.left;
2319
+ }, [columns, getScrollableColumnLeft, leftFixedOffsets, metrics, rightFixedOffsets, viewport.width]);
2320
+ const getHeaderLevelAtY = (0, import_react5.useCallback)((localY, headerY) => {
2321
+ const y = localY - headerY;
2322
+ for (let level = 0; level < headerRowHeights.length; level += 1) {
2323
+ const top = headerRowOffsets[level] ?? 0;
2324
+ if (y >= top && y < top + headerRowHeights[level]) return level;
1986
2325
  }
1987
- return closest;
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]);
2326
+ return headerDepth - 1;
2327
+ }, [headerDepth, headerRowHeights, headerRowOffsets]);
2328
+ const resizeColumn = (0, import_react5.useCallback)((columnKey, width2) => {
2329
+ setResizedColumnWidths((current) => current[columnKey] === width2 ? current : { ...current, [columnKey]: width2 });
2330
+ onColumnResize?.(columnKey, width2);
2331
+ }, [onColumnResize]);
1999
2332
  const locateHeaderAction = (0, import_react5.useCallback)((clientX, columnIndex) => {
2000
2333
  if (columnIndex < 0 || !canvasRef.current) return null;
2001
2334
  const column = columns[columnIndex];
@@ -2030,11 +2363,70 @@ function Table({
2030
2363
  const columnRight = getDisplayedColumnLeft(columnIndex) + metrics[columnIndex].width;
2031
2364
  return localX >= columnRight - 16 && localX < columnRight - 5;
2032
2365
  }, [columnDraggable, columns, getDisplayedColumnLeft, measureHeaderTitleWidth, metrics]);
2033
- const setDragGuide = (0, import_react5.useCallback)((x) => {
2366
+ const getHeaderCellLeft = (0, import_react5.useCallback)((cell) => {
2367
+ const startMetric = metrics[cell.startIndex];
2368
+ const fixedSide = columns[cell.startIndex]?.fixed;
2369
+ if (!startMetric) return 0;
2370
+ return fixedSide === "left" ? leftFixedOffsets.get(cell.startIndex) ?? 0 : fixedSide === "right" ? viewport.width - (rightFixedOffsets.get(cell.startIndex) ?? 0) - startMetric.width : getScrollableColumnLeft(cell.startIndex) - scrollRef.current.left;
2371
+ }, [columns, getScrollableColumnLeft, leftFixedOffsets, metrics, rightFixedOffsets, viewport.width]);
2372
+ const getHeaderCellWidth = (0, import_react5.useCallback)((cell) => cell.leaf ? metrics[cell.startIndex]?.width ?? 0 : getCellDisplayWidth(cell.startIndex, cell.endIndex - cell.startIndex + 1), [getCellDisplayWidth, metrics]);
2373
+ const locateHeaderResizeHit = (0, import_react5.useCallback)((clientX, clientY) => {
2374
+ if (!columnResizable || !canvasRef.current) return null;
2375
+ const rect = canvasRef.current.getBoundingClientRect();
2376
+ const localX = clientX - rect.left;
2377
+ const localY = clientY - rect.top;
2378
+ const headerY = fixedHeader ? 0 : -scrollRef.current.top;
2379
+ if (localY < headerY || localY >= headerY + headerHeight) return null;
2380
+ const level = getHeaderLevelAtY(localY, headerY);
2381
+ let closest = null;
2382
+ let distance = 6;
2383
+ for (const cell of headerCells) {
2384
+ if (cell.level !== level) continue;
2385
+ if (columns[cell.startIndex]?.rowSelection || columns[cell.startIndex]?.rowDragHandle || columns[cell.startIndex]?.rowNumber) continue;
2386
+ const fixedSide = columns[cell.startIndex]?.fixed;
2387
+ const edge = fixedSide === "right" && cell.leaf ? "left" : "right";
2388
+ const edgeX = getHeaderCellLeft(cell) + (edge === "right" ? getHeaderCellWidth(cell) : 0);
2389
+ const nextDistance = Math.abs(localX - edgeX);
2390
+ if (nextDistance < distance) {
2391
+ distance = nextDistance;
2392
+ closest = { cell, edge };
2393
+ }
2394
+ }
2395
+ return closest;
2396
+ }, [columnResizable, columns, fixedHeader, getHeaderCellLeft, getHeaderCellWidth, getHeaderLevelAtY, headerCells, headerHeight]);
2397
+ const locateHeaderResizeCell = (0, import_react5.useCallback)((clientX, clientY) => locateHeaderResizeHit(clientX, clientY)?.cell ?? null, [locateHeaderResizeHit]);
2398
+ const locateHeaderCellDragHandle = (0, import_react5.useCallback)((clientX, cell) => {
2399
+ if (!columnDraggable || !canvasRef.current) return false;
2400
+ if (cell.leaf) return locateHeaderDragHandle(clientX, cell.startIndex);
2401
+ const localX = clientX - canvasRef.current.getBoundingClientRect().left;
2402
+ const right = getHeaderCellLeft(cell) + getHeaderCellWidth(cell);
2403
+ return localX >= right - 16 && localX < right - 5;
2404
+ }, [columnDraggable, getHeaderCellLeft, getHeaderCellWidth, locateHeaderDragHandle]);
2405
+ const locateHeaderCell = (0, import_react5.useCallback)((clientX, clientY) => {
2406
+ const canvas = canvasRef.current;
2407
+ if (!canvas) return null;
2408
+ const rect = canvas.getBoundingClientRect();
2409
+ const headerY = fixedHeader ? 0 : -scrollRef.current.top;
2410
+ const localX = clientX - rect.left;
2411
+ const localY = clientY - rect.top;
2412
+ if (localY < headerY || localY >= headerY + headerHeight) return null;
2413
+ const level = getHeaderLevelAtY(localY, headerY);
2414
+ for (const cell of headerCells) {
2415
+ if (cell.level !== level) continue;
2416
+ const left = getHeaderCellLeft(cell);
2417
+ const width2 = getHeaderCellWidth(cell);
2418
+ if (localX >= left && localX < left + width2) return cell;
2419
+ }
2420
+ return null;
2421
+ }, [fixedHeader, getHeaderCellLeft, getHeaderCellWidth, getHeaderLevelAtY, headerCells, headerHeight]);
2422
+ const setDragGuide = (0, import_react5.useCallback)((x, top = 0) => {
2034
2423
  const guide = dragGuideRef.current;
2035
2424
  if (!guide) return;
2036
2425
  guide.style.display = x === null ? "none" : "block";
2037
- if (x !== null) guide.style.transform = `translateX(${Math.round(x)}px)`;
2426
+ if (x !== null) {
2427
+ guide.style.top = `${top}px`;
2428
+ guide.style.transform = `translateX(${Math.round(x)}px)`;
2429
+ }
2038
2430
  }, []);
2039
2431
  const setRowDragGuide = (0, import_react5.useCallback)((y) => {
2040
2432
  const guide = rowDragGuideRef.current;
@@ -2052,6 +2444,15 @@ function Table({
2052
2444
  (0, import_react5.useEffect)(() => {
2053
2445
  const canvas = canvasRef.current;
2054
2446
  if (!canvas || !columnDraggable && !rowDraggable) return;
2447
+ const getColumnDropIndex = (sourceIndex, targetIndex, edge) => {
2448
+ if (edge === "right") return targetIndex;
2449
+ const destinationIndex = targetIndex - (sourceIndex < targetIndex ? 1 : 0);
2450
+ return Math.max(0, Math.min(columns.length - 1, destinationIndex));
2451
+ };
2452
+ const getColumnDropPlacement = (sourceIndex, targetIndex, edge) => {
2453
+ if (edge !== "right") return "before";
2454
+ return targetIndex < sourceIndex ? "before" : "after";
2455
+ };
2055
2456
  const getHeaderColumn = (clientX, clientY) => {
2056
2457
  const rect = canvas.getBoundingClientRect();
2057
2458
  const headerY = fixedHeader ? 0 : -scrollRef.current.top;
@@ -2059,22 +2460,35 @@ function Table({
2059
2460
  if (localY < headerY || localY >= headerY + headerHeight) return -1;
2060
2461
  return locateColumn(clientX);
2061
2462
  };
2463
+ const getHeaderDragCell = (clientX, clientY) => {
2464
+ const cell = locateHeaderCell(clientX, clientY);
2465
+ if (!cell) return null;
2466
+ return locateHeaderCellDragHandle(clientX, cell) && !locateHeaderResizeCell(clientX, clientY) ? cell : null;
2467
+ };
2062
2468
  const cleanupDrag = (0, import_adapter.draggable)({
2063
2469
  element: canvas,
2064
2470
  canDrag: ({ input }) => {
2065
- const index = getHeaderColumn(input.clientX, input.clientY);
2066
- if (columnDraggable && index >= 0) return locateHeaderDragHandle(input.clientX, index) && locateResizeHandle(input.clientX) < 0;
2471
+ const headerDragCell = columnDraggable ? getHeaderDragCell(input.clientX, input.clientY) : null;
2472
+ if (headerDragCell) return true;
2067
2473
  const columnIndex = locateColumn(input.clientX);
2068
2474
  return rowDraggable && columnIndex >= 0 && Boolean(columns[columnIndex].rowDragHandle) && locateRow(input.clientY) >= 0;
2069
2475
  },
2070
2476
  getInitialData: ({ input }) => {
2071
- const headerColumn = getHeaderColumn(input.clientX, input.clientY);
2072
- if (headerColumn < 0) {
2477
+ const headerDragCell = columnDraggable ? getHeaderDragCell(input.clientX, input.clientY) : null;
2478
+ if (!headerDragCell) {
2073
2479
  return { type: "table-row", sourceIndex: locateRow(input.clientY) };
2074
2480
  }
2075
- const sourceIndex = headerColumn;
2481
+ const sourceIndex = headerDragCell.startIndex;
2076
2482
  columnDragPreviewRef.current = { sourceIndex, targetIndex: sourceIndex };
2077
- return { type: "table-column", sourceIndex };
2483
+ return {
2484
+ type: "table-column",
2485
+ sourceIndex,
2486
+ sourceKey: headerDragCell.key,
2487
+ parentKey: headerDragCell.parentKey,
2488
+ headerLevel: headerDragCell.level,
2489
+ headerKind: headerDragCell.leaf ? "column" : "group",
2490
+ fixed: columns[headerDragCell.startIndex]?.fixed
2491
+ };
2078
2492
  },
2079
2493
  onGenerateDragPreview: ({ nativeSetDragImage, source }) => {
2080
2494
  const sourceIndex = Number(source.data.sourceIndex);
@@ -2177,31 +2591,31 @@ function Table({
2177
2591
  return;
2178
2592
  }
2179
2593
  const targetIndex = Number(target?.targetIndex);
2180
- if (!Number.isInteger(targetIndex) || columns[targetIndex]?.fixed !== columns[sourceIndex]?.fixed) {
2594
+ if (!Number.isInteger(targetIndex) || target?.headerKind !== source.data.headerKind || target?.parentKey !== source.data.parentKey || target?.fixed !== source.data.fixed) {
2181
2595
  setDragGuide(null);
2182
2596
  return;
2183
2597
  }
2184
2598
  const edge = target?.columnEdge;
2185
- let destinationIndex = targetIndex + (edge === "right" ? 1 : 0);
2186
- if (sourceIndex < destinationIndex) destinationIndex -= 1;
2187
- destinationIndex = Math.max(0, Math.min(columns.length - 1, destinationIndex));
2599
+ const destinationIndex = getColumnDropIndex(sourceIndex, targetIndex, edge);
2188
2600
  columnDragPreviewRef.current = { sourceIndex, targetIndex: destinationIndex };
2189
- const targetLeft = getDisplayedColumnLeft(targetIndex);
2190
- setDragGuide(edge === "right" ? targetLeft + metrics[targetIndex].width : targetLeft);
2601
+ const targetLeft = Number(target?.targetLeft);
2602
+ const targetWidth = Number(target?.targetWidth);
2603
+ const guideTop = headerRowOffsets[Number(source.data.headerLevel)] ?? 0;
2604
+ setDragGuide(edge === "right" ? targetLeft + targetWidth : targetLeft, guideTop);
2191
2605
  },
2192
2606
  onDrop: ({ source, location }) => {
2193
2607
  const target = location.current.dropTargets[0]?.data;
2194
2608
  const sourceIndex = Number(source.data.sourceIndex);
2195
2609
  if (source.data.type === "table-row") {
2196
- const rawTargetIndex = Number(target?.targetIndex);
2197
- let targetIndex2 = rawTargetIndex + (target?.rowEdge === "bottom" ? 1 : 0);
2610
+ const rawTargetIndex2 = Number(target?.targetIndex);
2611
+ let targetIndex2 = rawTargetIndex2 + (target?.rowEdge === "bottom" ? 1 : 0);
2198
2612
  if (sourceIndex < targetIndex2) targetIndex2 -= 1;
2199
2613
  targetIndex2 = Math.max(0, Math.min(rows.length - 1, targetIndex2));
2200
2614
  scrollerRef.current?.classList.add("rvg-row-drop-settling");
2201
2615
  setRowDragPreview(null);
2202
2616
  setRowDragGuide(null);
2203
2617
  canvas.style.cursor = "default";
2204
- if (Number.isInteger(rawTargetIndex) && sourceIndex !== targetIndex2) onRowOrderChange?.(sourceIndex, targetIndex2);
2618
+ if (Number.isInteger(rawTargetIndex2) && sourceIndex !== targetIndex2) onRowOrderChange?.(sourceIndex, targetIndex2);
2205
2619
  requestAnimationFrame(() => {
2206
2620
  requestAnimationFrame(() => scrollerRef.current?.classList.remove("rvg-row-drop-settling"));
2207
2621
  });
@@ -2210,12 +2624,23 @@ function Table({
2210
2624
  }, 0);
2211
2625
  return;
2212
2626
  }
2213
- const targetIndex = columnDragPreviewRef.current?.targetIndex ?? Number(target?.targetIndex);
2627
+ const rawTargetIndex = Number(target?.targetIndex);
2628
+ const targetIndex = columnDragPreviewRef.current?.targetIndex ?? (Number.isInteger(rawTargetIndex) ? getColumnDropIndex(sourceIndex, rawTargetIndex, target?.columnEdge) : rawTargetIndex);
2214
2629
  canvas.style.cursor = "default";
2215
2630
  setDragGuide(null);
2216
2631
  columnDragPreviewRef.current = null;
2217
- if (Number.isInteger(targetIndex) && sourceIndex !== targetIndex && columns[targetIndex]?.fixed === columns[sourceIndex]?.fixed) {
2218
- onColumnOrderChange?.(sourceIndex - utilityColumnCount, targetIndex - utilityColumnCount);
2632
+ if (Number.isInteger(rawTargetIndex) && Number.isInteger(targetIndex) && sourceIndex !== targetIndex && target?.headerKind === source.data.headerKind && target?.parentKey === source.data.parentKey && target?.fixed === source.data.fixed) {
2633
+ onColumnOrderChange?.(
2634
+ sourceIndex - utilityColumnCount,
2635
+ targetIndex - utilityColumnCount,
2636
+ {
2637
+ type: source.data.headerKind === "group" ? "group" : "column",
2638
+ sourceKey: String(source.data.sourceKey),
2639
+ targetKey: String(target.targetKey),
2640
+ parentKey: typeof source.data.parentKey === "string" ? source.data.parentKey : void 0,
2641
+ placement: getColumnDropPlacement(sourceIndex, rawTargetIndex, target.columnEdge)
2642
+ }
2643
+ );
2219
2644
  }
2220
2645
  window.setTimeout(() => {
2221
2646
  suppressClickRef.current = false;
@@ -2233,24 +2658,38 @@ function Table({
2233
2658
  const rowEdge = input.clientY - canvas.getBoundingClientRect().top < rowTop + rowHeight / 2 ? "top" : "bottom";
2234
2659
  return (0, import_closest_edge.attachClosestEdge)({ targetIndex: targetIndex2, rowEdge }, { element, input, allowedEdges: ["top", "bottom"] });
2235
2660
  }
2236
- let targetIndex = getHeaderColumn(input.clientX, input.clientY);
2237
- if (targetIndex < 0) return { targetIndex: -1 };
2661
+ const targetCell = locateHeaderCell(input.clientX, input.clientY);
2662
+ if (!targetCell) return { targetIndex: -1 };
2663
+ let targetIndex = targetCell.startIndex;
2664
+ const targetFixed = columns[targetCell.startIndex]?.fixed;
2665
+ const headerKind = targetCell.leaf ? "column" : "group";
2666
+ if (source.data.headerKind !== headerKind || source.data.parentKey !== targetCell.parentKey || source.data.fixed !== targetFixed) return { targetIndex: -1 };
2238
2667
  if (columns[targetIndex].rowSelection || columns[targetIndex].rowDragHandle || columns[targetIndex].rowNumber) {
2239
2668
  const fixedSide = columns[Number(source.data.sourceIndex)]?.fixed;
2240
2669
  targetIndex = columns.findIndex((column, index) => index > targetIndex && column.fixed === fixedSide && !column.rowSelection && !column.rowDragHandle && !column.rowNumber);
2241
2670
  if (targetIndex < 0) return { targetIndex: -1 };
2242
2671
  return { targetIndex, columnEdge: "left" };
2243
2672
  }
2244
- const left = getDisplayedColumnLeft(targetIndex);
2245
- const columnEdge = input.clientX - canvas.getBoundingClientRect().left < left + metrics[targetIndex].width / 2 ? "left" : "right";
2246
- return (0, import_closest_edge.attachClosestEdge)({ targetIndex, columnEdge }, { element, input, allowedEdges: ["left", "right"] });
2673
+ const left = getHeaderCellLeft(targetCell);
2674
+ const targetWidth = targetCell.leaf ? metrics[targetIndex].width : getHeaderCellWidth(targetCell);
2675
+ const columnEdge = input.clientX - canvas.getBoundingClientRect().left < left + targetWidth / 2 ? "left" : "right";
2676
+ return (0, import_closest_edge.attachClosestEdge)({
2677
+ targetIndex,
2678
+ targetKey: targetCell.key,
2679
+ parentKey: targetCell.parentKey,
2680
+ headerKind,
2681
+ fixed: targetFixed,
2682
+ targetLeft: left,
2683
+ targetWidth,
2684
+ columnEdge
2685
+ }, { element, input, allowedEdges: ["left", "right"] });
2247
2686
  }
2248
2687
  });
2249
2688
  return () => {
2250
2689
  cleanupDrag();
2251
2690
  cleanupDrop();
2252
2691
  };
2253
- }, [bodyTop, columnDraggable, columns, fixedHeader, getCellLabel, getDisplayedColumnLeft, getRowKey, headerHeight, locateColumn, locateHeaderDragHandle, locateResizeHandle, locateRow, metrics, onColumnOrderChange, onRowOrderChange, rowDraggable, rowHeight, rows, selectedRowKeySet, setDragGuide, setRowDragGuide, utilityColumnCount]);
2692
+ }, [bodyTop, columnDraggable, columns, fixedHeader, getCellLabel, getDisplayedColumnLeft, getHeaderCellWidth, getRowKey, headerHeight, locateColumn, locateHeaderCell, locateHeaderCellDragHandle, locateHeaderResizeCell, locateRow, metrics, onColumnOrderChange, onRowOrderChange, rowDraggable, rowHeight, rows, selectedRowKeySet, setDragGuide, setRowDragGuide, utilityColumnCount]);
2254
2693
  const locateCell = (0, import_react5.useCallback)((clientX, clientY) => {
2255
2694
  const canvas = canvasRef.current;
2256
2695
  if (!canvas) return null;
@@ -2357,8 +2796,9 @@ function Table({
2357
2796
  const horizontalFocusGap = 0;
2358
2797
  const verticalFocusGap = 0;
2359
2798
  if (column.fixed === void 0) {
2360
- if (metric.left - nextLeft < fixedWidth) nextLeft = Math.max(0, metric.left - fixedWidth - horizontalFocusGap);
2361
- if (metric.left + cellWidth - nextLeft > viewport.width - rightFixedWidth) nextLeft = metric.left + cellWidth - viewport.width + rightFixedWidth + horizontalFocusGap;
2799
+ const cellLeft = getScrollableColumnLeft(cell.columnIndex);
2800
+ if (cellLeft - nextLeft < fixedWidth) nextLeft = Math.max(0, cellLeft - fixedWidth - horizontalFocusGap);
2801
+ if (cellLeft + cellWidth - nextLeft > viewport.width - rightFixedWidth) nextLeft = cellLeft + cellWidth - viewport.width + rightFixedWidth + horizontalFocusGap;
2362
2802
  }
2363
2803
  const rowTop = (fixedHeader ? 0 : bodyTop) + cell.rowIndex * rowHeight;
2364
2804
  const rowBottom = rowTop + rowHeight * rowSpan;
@@ -2371,7 +2811,7 @@ function Table({
2371
2811
  scroller.scrollTop = nextTop;
2372
2812
  scrollRef.current = { left: scroller.scrollLeft, top: scroller.scrollTop };
2373
2813
  scheduleDraw();
2374
- }, [bodyTop, columns, fixedHeader, fixedWidth, getCellDisplayWidth, getCellSpan, metrics, rightFixedWidth, rowHeight, scheduleDraw, viewport.width]);
2814
+ }, [bodyTop, columns, fixedHeader, fixedWidth, getCellDisplayWidth, getCellSpan, getScrollableColumnLeft, metrics, rightFixedWidth, rowHeight, scheduleDraw, viewport.width]);
2375
2815
  const beginEdit = (0, import_react5.useCallback)((cell) => {
2376
2816
  const column = columns[cell.columnIndex];
2377
2817
  if (!column?.editable || column.dataIndex === void 0) return;
@@ -2392,8 +2832,9 @@ function Table({
2392
2832
  const horizontalFocusGap = 0;
2393
2833
  const verticalFocusGap = 0;
2394
2834
  if (column.fixed === void 0) {
2395
- if (metric.left - nextLeft < fixedWidth) nextLeft = Math.max(0, metric.left - fixedWidth - horizontalFocusGap);
2396
- if (metric.left + cellWidth - nextLeft > viewport.width - rightFixedWidth) nextLeft = metric.left + cellWidth - viewport.width + rightFixedWidth + horizontalFocusGap;
2835
+ const cellLeft = getScrollableColumnLeft(cell.columnIndex);
2836
+ if (cellLeft - nextLeft < fixedWidth) nextLeft = Math.max(0, cellLeft - fixedWidth - horizontalFocusGap);
2837
+ if (cellLeft + cellWidth - nextLeft > viewport.width - rightFixedWidth) nextLeft = cellLeft + cellWidth - viewport.width + rightFixedWidth + horizontalFocusGap;
2397
2838
  }
2398
2839
  const rowTop = (fixedHeader ? 0 : bodyTop) + cell.rowIndex * rowHeight;
2399
2840
  const rowBottom = rowTop + rowHeight * rowSpan;
@@ -2415,7 +2856,7 @@ function Table({
2415
2856
  return;
2416
2857
  }
2417
2858
  setEditing(cell);
2418
- }, [columns, fixedHeader, fixedWidth, getCellDisplayWidth, getCellSpan, metrics, rightFixedWidth, rowHeight, rows, scheduleDraw, viewport.width]);
2859
+ }, [columns, fixedHeader, fixedWidth, getCellDisplayWidth, getCellSpan, getScrollableColumnLeft, metrics, rightFixedWidth, rowHeight, rows, scheduleDraw, viewport.width]);
2419
2860
  const getCellEditKey = (cell) => `${typeof cell.rowKey}:${String(cell.rowKey)}\0${cell.columnKey}`;
2420
2861
  const markCellEdited = (cell, previousValue, nextValue) => {
2421
2862
  const key = getCellEditKey(cell);
@@ -2546,10 +2987,13 @@ function Table({
2546
2987
  const visibleRowHeight = fixedHeader ? renderHeight - bodyTop : renderHeight;
2547
2988
  if (rowTop < scroller.scrollTop) scroller.scrollTop = rowTop;
2548
2989
  if (rowBottom > scroller.scrollTop + visibleRowHeight) scroller.scrollTop = rowBottom - visibleRowHeight;
2549
- if (columns[columnIndex].fixed === void 0 && metric.left < scroller.scrollLeft + fixedWidth) scroller.scrollLeft = Math.max(0, metric.left - fixedWidth);
2550
- if (columns[columnIndex].fixed === void 0 && metric.left + cellWidth > scroller.scrollLeft + viewport.width - rightFixedWidth) scroller.scrollLeft = metric.left + cellWidth - viewport.width + rightFixedWidth;
2990
+ if (columns[columnIndex].fixed === void 0) {
2991
+ const cellLeft = getScrollableColumnLeft(columnIndex);
2992
+ if (cellLeft < scroller.scrollLeft + fixedWidth) scroller.scrollLeft = Math.max(0, cellLeft - fixedWidth);
2993
+ if (cellLeft + cellWidth > scroller.scrollLeft + viewport.width - rightFixedWidth) scroller.scrollLeft = cellLeft + cellWidth - viewport.width + rightFixedWidth;
2994
+ }
2551
2995
  }
2552
- }, [beginEdit, bodyTop, columns, editing, renderHeight, fixedHeader, fixedWidth, getCellDisplayWidth, getCellSpan, getRowKey, metrics, rightFixedWidth, rowHeight, rows, selection, setSelection, viewport.width]);
2996
+ }, [beginEdit, bodyTop, columns, editing, renderHeight, fixedHeader, fixedWidth, getCellDisplayWidth, getCellSpan, getRowKey, getScrollableColumnLeft, metrics, rightFixedWidth, rowHeight, rows, selection, setSelection, viewport.width]);
2553
2997
  const editorStyle = (0, import_react5.useMemo)(() => {
2554
2998
  if (!editing) return void 0;
2555
2999
  const metric = metrics[editing.columnIndex];
@@ -2557,64 +3001,82 @@ function Table({
2557
3001
  const span = getCellSpan(editing.rowIndex, editing.columnIndex);
2558
3002
  const cellWidth = getCellDisplayWidth(editing.columnIndex, span?.colSpan ?? 1);
2559
3003
  const cellHeight = rowHeight * (span?.rowSpan ?? 1);
2560
- const cellLeft = fixedSide === "left" ? metric.left : fixedSide === "right" ? viewport.width - (rightFixedOffsets.get(editing.columnIndex) ?? 0) - metric.width : metric.left - scrollRef.current.left;
3004
+ const cellLeft = fixedSide === "left" ? leftFixedOffsets.get(editing.columnIndex) ?? 0 : fixedSide === "right" ? viewport.width - (rightFixedOffsets.get(editing.columnIndex) ?? 0) - metric.width : getScrollableColumnLeft(editing.columnIndex) - scrollRef.current.left;
2561
3005
  return {
2562
3006
  left: (fixedSide ? cellLeft : Math.max(cellLeft, fixedWidth)) + 1,
2563
3007
  top: Math.max(fixedHeader ? bodyTop : 0, bodyTop + editing.rowIndex * rowHeight - scrollRef.current.top) + 1,
2564
3008
  width: cellWidth - 2,
2565
3009
  height: cellHeight - 2
2566
3010
  };
2567
- }, [bodyTop, columns, editing, fixedHeader, fixedWidth, getCellDisplayWidth, getCellSpan, metrics, rightFixedOffsets, rowHeight, viewport.width]);
3011
+ }, [bodyTop, columns, editing, fixedHeader, fixedWidth, getCellDisplayWidth, getCellSpan, getScrollableColumnLeft, leftFixedOffsets, metrics, rightFixedOffsets, rowHeight, viewport.width]);
2568
3012
  const handleColumnPointerDown = (0, import_react5.useCallback)((event) => {
2569
3013
  const rect = event.currentTarget.getBoundingClientRect();
2570
3014
  const localY = event.clientY - rect.top;
2571
3015
  const headerY = fixedHeader ? 0 : -scrollRef.current.top;
2572
3016
  if (localY < headerY || localY >= headerY + headerHeight) return;
2573
- const resizeIndex = locateResizeHandle(event.clientX);
2574
- if (resizeIndex >= 0) {
3017
+ const resizeHit = locateHeaderResizeHit(event.clientX, event.clientY);
3018
+ if (resizeHit) {
3019
+ const { cell: resizeCell, edge: resizeEdge } = resizeHit;
2575
3020
  event.preventDefault();
2576
3021
  event.currentTarget.setPointerCapture(event.pointerId);
2577
- const neighborIndex = findResizeNeighbor(resizeIndex);
2578
- const startGuideX = getDisplayedColumnLeft(resizeIndex) + metrics[resizeIndex].width;
3022
+ const resizeIndex = resizeCell.endIndex;
3023
+ const resizeIndices = resizeCell.leaf ? [resizeIndex] : Array.from({ length: resizeCell.endIndex - resizeCell.startIndex + 1 }, (_, index) => resizeCell.startIndex + index);
3024
+ const startWidths = resizeIndices.map((index) => metrics[index].width);
3025
+ const startGuideX = getHeaderCellLeft(resizeCell) + (resizeEdge === "right" ? getHeaderCellWidth(resizeCell) : 0);
2579
3026
  columnDragRef.current = {
2580
3027
  type: "resize",
2581
3028
  columnIndex: resizeIndex,
2582
3029
  startX: event.clientX,
2583
- startWidth: metrics[resizeIndex].width,
3030
+ startWidth: startWidths.reduce((total, width2) => total + width2, 0),
2584
3031
  startGuideX,
2585
- neighborIndex,
2586
- startNeighborWidth: neighborIndex >= 0 ? metrics[neighborIndex].width : 0
3032
+ guideTop: headerRowOffsets[resizeCell.level] ?? 0,
3033
+ resizeEdge,
3034
+ resizeIndices,
3035
+ startWidths
2587
3036
  };
2588
3037
  suppressClickRef.current = true;
3038
+ setResizeGuideTop(headerRowOffsets[resizeCell.level] ?? 0);
2589
3039
  setResizeGuideX(startGuideX);
2590
3040
  return;
2591
3041
  }
2592
- }, [findResizeNeighbor, fixedHeader, getDisplayedColumnLeft, headerHeight, locateResizeHandle, metrics]);
3042
+ }, [fixedHeader, getHeaderCellLeft, getHeaderCellWidth, headerHeight, headerRowOffsets, locateHeaderResizeHit, metrics]);
2593
3043
  const handleColumnPointerMove = (0, import_react5.useCallback)((event) => {
2594
3044
  const drag = columnDragRef.current;
2595
3045
  if (!drag) return;
2596
3046
  const column = columns[drag.columnIndex];
2597
3047
  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);
3048
+ const pointerDelta = Math.round(event.clientX - drag.startX);
3049
+ const delta = drag.resizeEdge === "left" ? -pointerDelta : pointerDelta;
3050
+ if (drag.resizeIndices.length > 1) {
3051
+ const minWidths = drag.resizeIndices.map((index) => getMinimumColumnWidth(columns[index], columnDraggable));
3052
+ const minTotalWidth = minWidths.reduce((total, width3) => total + width3, 0);
3053
+ const targetTotalWidth = Math.max(minTotalWidth, drag.startWidth + delta);
3054
+ const ratio = drag.startWidth > 0 ? targetTotalWidth / drag.startWidth : 1;
3055
+ const nextWidths = drag.startWidths.map((width3, index) => Math.max(minWidths[index], Math.round(width3 * ratio)));
3056
+ let remainder = targetTotalWidth - nextWidths.reduce((total, width3) => total + width3, 0);
3057
+ for (let index = nextWidths.length - 1; index >= 0 && remainder !== 0; index -= 1) {
3058
+ const next = nextWidths[index] + (remainder > 0 ? 1 : -1);
3059
+ if (next < minWidths[index]) continue;
3060
+ nextWidths[index] = next;
3061
+ remainder += remainder > 0 ? -1 : 1;
3062
+ }
3063
+ drag.resizeIndices.forEach((columnIndex, index) => {
3064
+ resizeColumn(columns[columnIndex].key, nextWidths[index]);
3065
+ });
3066
+ setResizeGuideX(drag.startGuideX + (drag.resizeEdge === "left" ? drag.startWidth - targetTotalWidth : targetTotalWidth - drag.startWidth));
2606
3067
  return;
2607
3068
  }
2608
3069
  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]);
3070
+ resizeColumn(column.key, width2);
3071
+ setResizeGuideX(drag.startGuideX + (drag.resizeEdge === "left" ? drag.startWidth - width2 : width2 - drag.startWidth));
3072
+ }, [columnDraggable, columns, resizeColumn]);
2612
3073
  const handleColumnPointerUp = (0, import_react5.useCallback)((event) => {
2613
3074
  const drag = columnDragRef.current;
2614
3075
  if (!drag) return;
2615
3076
  if (event.currentTarget.hasPointerCapture(event.pointerId)) event.currentTarget.releasePointerCapture(event.pointerId);
2616
3077
  columnDragRef.current = null;
2617
3078
  setResizeGuideX(null);
3079
+ setResizeGuideTop(0);
2618
3080
  }, []);
2619
3081
  const queueSortStateChange = (0, import_react5.useCallback)((next) => {
2620
3082
  setDisplaySortState(next);
@@ -2627,7 +3089,7 @@ function Table({
2627
3089
  const renderHeaderIcons = (columnIndex, left) => {
2628
3090
  const column = columns[columnIndex];
2629
3091
  if (column.rowSelection || column.rowDragHandle || column.rowNumber) return null;
2630
- const top = (headerHeight - 14) / 2;
3092
+ const top = headerLeafTop + (headerLeafHeight - 14) / 2;
2631
3093
  const visibleActions = getVisibleHeaderActions(column, metrics[columnIndex].width, columnDraggable, measureHeaderTitleWidth(columnIndex));
2632
3094
  let right = left + metrics[columnIndex].width - (visibleActions.drag ? 18 : 4);
2633
3095
  const icons = [];
@@ -2653,44 +3115,87 @@ function Table({
2653
3115
  }
2654
3116
  return icons;
2655
3117
  };
2656
- const headerTooltip = (() => {
2657
- if (!visibleHeaderTooltip) return null;
2658
- const { columnIndex, action } = visibleHeaderTooltip;
2659
- const column = columns[columnIndex];
3118
+ const createHeaderTooltip = (0, import_react5.useCallback)((action, columnIndex, cell) => {
3119
+ const column = cell?.column ?? columns[columnIndex];
2660
3120
  if (!column) return null;
2661
- const columnRight = getDisplayedColumnLeft(columnIndex) + metrics[columnIndex].width;
2662
- const visibleActions = getVisibleHeaderActions(column, metrics[columnIndex].width, columnDraggable, measureHeaderTitleWidth(columnIndex));
2663
- let anchorX = columnRight - (visibleActions.drag ? 18 : 4);
2664
- let anchorLeft = anchorX;
2665
- let label;
3121
+ const cellLeft = cell ? getHeaderCellLeft(cell) : getDisplayedColumnLeft(columnIndex);
3122
+ const cellWidth = cell ? getHeaderCellWidth(cell) : metrics[columnIndex]?.width ?? 0;
3123
+ const level = cell?.level ?? headerDepth - 1;
3124
+ const rowTop = (fixedHeader ? 0 : -scrollPosition.top) + (headerRowOffsets[level] ?? 0);
3125
+ const rowHeight2 = headerRowHeights[level] ?? baseHeaderHeight;
3126
+ let anchorX = cellLeft + cellWidth / 2;
3127
+ let anchorLeft = cellLeft;
3128
+ let label = column.title;
2666
3129
  if (action === "title") {
2667
- const titleWidth = measureHeaderTitleWidth(columnIndex);
2668
- const actionWidth = (Number(visibleActions.drag) + Number(visibleActions.filter) + Number(visibleActions.sort)) * 18;
2669
- const contentWidth2 = Math.max(0, metrics[columnIndex].width - actionWidth);
2670
- const textWidth = Math.min(titleWidth, Math.max(0, contentWidth2 - 20));
2671
- const columnLeft = getDisplayedColumnLeft(columnIndex);
2672
- anchorX = column.align === "right" ? columnLeft + contentWidth2 - 10 : column.align === "center" ? columnLeft + metrics[columnIndex].width / 2 + textWidth / 2 : columnLeft + 10 + textWidth;
2673
- anchorLeft = column.align === "right" ? anchorX - textWidth : column.align === "center" ? columnLeft + metrics[columnIndex].width / 2 - textWidth / 2 : columnLeft + 10;
2674
- label = column.title;
3130
+ const canvas = canvasRef.current;
3131
+ const context = canvas?.getContext("2d");
3132
+ if (context) context.font = "600 13px Inter, ui-sans-serif, system-ui, sans-serif";
3133
+ const titleWidth = context?.measureText(column.title).width ?? column.title.length * 13;
3134
+ const visibleActions = cell?.leaf || !cell ? getVisibleHeaderActions(column, cellWidth, columnDraggable, titleWidth) : null;
3135
+ const actionWidth = visibleActions ? (Number(visibleActions.drag) + Number(visibleActions.filter) + Number(visibleActions.sort)) * 18 : columnDraggable ? 18 : 0;
3136
+ const contentWidth2 = Math.max(0, cellWidth - actionWidth);
3137
+ const maxTextWidth = Math.max(0, contentWidth2 - 20);
3138
+ const textWidth = Math.min(titleWidth, maxTextWidth);
3139
+ if (column.align === "right") {
3140
+ anchorX = cellLeft + contentWidth2 - 10;
3141
+ anchorLeft = anchorX - textWidth;
3142
+ } else if (column.align === "center") {
3143
+ anchorX = cellLeft + contentWidth2 / 2 + textWidth / 2;
3144
+ anchorLeft = cellLeft + contentWidth2 / 2 - textWidth / 2;
3145
+ } else {
3146
+ anchorLeft = cellLeft + 10;
3147
+ anchorX = anchorLeft + textWidth;
3148
+ }
2675
3149
  } else if (action === "drag") {
2676
- anchorX = columnRight;
3150
+ anchorX = cellLeft + cellWidth;
2677
3151
  anchorLeft = anchorX;
2678
3152
  label = labels.dragColumn;
2679
3153
  } else if (action === "sort") {
2680
3154
  const direction = displaySortState?.columnKey === column.key ? displaySortState.direction : null;
2681
3155
  label = direction === "asc" ? labels.sortAsc : direction === "desc" ? labels.sortDesc : labels.sortBoth;
2682
- } else {
2683
- if (visibleActions.sort) anchorX -= 18;
3156
+ anchorX = cellLeft + cellWidth - 22;
3157
+ anchorLeft = anchorX;
3158
+ } else if (action === "filter") {
2684
3159
  const value = filterValues[column.key];
2685
3160
  label = value ? labels.filterWithValue(value) : labels.filter;
3161
+ anchorX = cellLeft + cellWidth - 40;
3162
+ anchorLeft = anchorX;
2686
3163
  }
2687
3164
  const placement = anchorX + 328 > viewport.width ? "left" : "right";
2688
3165
  return {
3166
+ key: `${action}:${cell ? `cell:${cell.key}:${cell.level}` : `column:${column.key}`}:${Math.round(anchorX)}:${Math.round(rowTop)}`,
3167
+ columnIndex,
3168
+ action,
2689
3169
  left: placement === "right" ? anchorX + 8 : Math.max(8, anchorLeft - 8),
2690
- top: (fixedHeader ? 0 : -scrollPosition.top) + headerHeight / 2,
3170
+ top: rowTop + rowHeight2 / 2,
2691
3171
  label,
2692
3172
  placement
2693
3173
  };
3174
+ }, [baseHeaderHeight, columnDraggable, columns, displaySortState, filterValues, fixedHeader, getDisplayedColumnLeft, getHeaderCellLeft, getHeaderCellWidth, headerDepth, headerRowHeights, headerRowOffsets, labels, metrics, scrollPosition.top, viewport.width]);
3175
+ const isPointerOnHeaderTitle = (0, import_react5.useCallback)((clientX, clientY, cell) => {
3176
+ const canvas = canvasRef.current;
3177
+ if (!canvas) return false;
3178
+ const rect = canvas.getBoundingClientRect();
3179
+ const localX = clientX - rect.left;
3180
+ const localY = clientY - rect.top;
3181
+ const column = cell.column;
3182
+ const cellLeft = getHeaderCellLeft(cell);
3183
+ const cellWidth = getHeaderCellWidth(cell);
3184
+ const rowTop = (fixedHeader ? 0 : -scrollRef.current.top) + (headerRowOffsets[cell.level] ?? 0);
3185
+ const context = canvas.getContext("2d");
3186
+ if (context) context.font = "600 13px Inter, ui-sans-serif, system-ui, sans-serif";
3187
+ const titleWidth = context?.measureText(column.title).width ?? column.title.length * 13;
3188
+ const visibleActions = cell.leaf ? getVisibleHeaderActions(column, cellWidth, columnDraggable, titleWidth) : null;
3189
+ const actionWidth = visibleActions ? (Number(visibleActions.drag) + Number(visibleActions.filter) + Number(visibleActions.sort)) * 18 : columnDraggable ? 18 : 0;
3190
+ const contentWidth2 = Math.max(0, cellWidth - actionWidth);
3191
+ const textWidth = Math.min(titleWidth, Math.max(0, contentWidth2 - 20));
3192
+ const textLeft = column.align === "right" ? cellLeft + contentWidth2 - 10 - textWidth : column.align === "center" ? cellLeft + contentWidth2 / 2 - textWidth / 2 : cellLeft + 10;
3193
+ const textTop = rowTop + 8;
3194
+ return localX >= textLeft - 4 && localX <= textLeft + textWidth + 4 && localY >= textTop - 2 && localY <= textTop + 18;
3195
+ }, [columnDraggable, fixedHeader, getHeaderCellLeft, getHeaderCellWidth, headerRowOffsets]);
3196
+ const headerTooltip = (() => {
3197
+ if (!visibleHeaderTooltip) return null;
3198
+ return visibleHeaderTooltip;
2694
3199
  })();
2695
3200
  const getRangeBounds = () => selectionRange ? {
2696
3201
  rowStart: Math.min(selectionRange.anchor.rowIndex, selectionRange.focus.rowIndex),
@@ -2749,7 +3254,7 @@ function Table({
2749
3254
  }, 1800);
2750
3255
  };
2751
3256
  const showHeaderTooltipAfterDelay = (next) => {
2752
- const nextKey = next ? `${next.columnIndex}:${next.action}` : "";
3257
+ const nextKey = next ? next.key : "";
2753
3258
  if (nextKey && pendingHeaderTooltipKeyRef.current === nextKey) return;
2754
3259
  pendingHeaderTooltipKeyRef.current = nextKey;
2755
3260
  if (headerTooltipTimerRef.current !== null) clearTimeout(headerTooltipTimerRef.current);
@@ -2997,12 +3502,14 @@ function Table({
2997
3502
  const renderHeaderTitle = (columnIndex, left) => {
2998
3503
  const column = columns[columnIndex];
2999
3504
  const currentScrollTop = scrollRef.current.top;
3505
+ const headerTop = fixedHeader ? 0 : -currentScrollTop;
3506
+ const leafTop = headerTop + headerLeafTop;
3000
3507
  if (column.rowNumber) {
3001
3508
  return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3002
3509
  "div",
3003
3510
  {
3004
3511
  className: "rvg-header-title",
3005
- style: { left, top: fixedHeader ? 0 : -currentScrollTop, width: metrics[columnIndex].width, height: headerHeight, justifyContent: "center", padding: 0, textAlign: "center" },
3512
+ style: { left, top: headerTop, width: metrics[columnIndex].width, height: headerHeight, justifyContent: "center", padding: 0, textAlign: "center" },
3006
3513
  children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { children: column.title })
3007
3514
  },
3008
3515
  column.key
@@ -3016,7 +3523,7 @@ function Table({
3016
3523
  "div",
3017
3524
  {
3018
3525
  className: "rvg-header-selection-icon",
3019
- style: { left, top: fixedHeader ? 0 : -currentScrollTop, width: metrics[columnIndex].width, height: headerHeight },
3526
+ style: { left, top: headerTop, width: metrics[columnIndex].width, height: headerHeight },
3020
3527
  children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(SelectionIcon, { checked: allSelected, indeterminate })
3021
3528
  },
3022
3529
  column.key
@@ -3029,11 +3536,13 @@ function Table({
3029
3536
  "div",
3030
3537
  {
3031
3538
  className: "rvg-header-title is-custom",
3539
+ "data-level": headerDepth - 1,
3540
+ "data-measure-key": column.key,
3032
3541
  style: {
3033
3542
  left,
3034
- top: fixedHeader ? 0 : -currentScrollTop,
3543
+ top: leafTop,
3035
3544
  width: Math.max(0, metrics[columnIndex].width - actionWidth),
3036
- height: headerHeight,
3545
+ height: headerLeafHeight,
3037
3546
  justifyContent: column.align === "right" ? "flex-end" : column.align === "center" ? "center" : "flex-start",
3038
3547
  textAlign: column.align === "right" ? "right" : column.align === "center" ? "center" : "left"
3039
3548
  },
@@ -3041,8 +3550,10 @@ function Table({
3041
3550
  "span",
3042
3551
  {
3043
3552
  ref: (node) => {
3044
- if (node) customHeaderContentRefs.current.set(column.key, node);
3045
- else customHeaderContentRefs.current.delete(column.key);
3553
+ if (node) {
3554
+ customHeaderContentRefs.current.set(column.key, node);
3555
+ requestAnimationFrame(measureCustomHeaderHeight);
3556
+ } else customHeaderContentRefs.current.delete(column.key);
3046
3557
  },
3047
3558
  children: column.renderHeader(column)
3048
3559
  }
@@ -3053,6 +3564,61 @@ function Table({
3053
3564
  }
3054
3565
  return null;
3055
3566
  };
3567
+ const renderGroupedHeaderCell = (cell) => {
3568
+ const startMetric = metrics[cell.startIndex];
3569
+ const endMetric = metrics[cell.endIndex];
3570
+ if (!startMetric || !endMetric) return null;
3571
+ const column = cell.column;
3572
+ const fixedSide = columns[cell.startIndex]?.fixed;
3573
+ const currentScrollTop = scrollRef.current.top;
3574
+ const rowHeightForLevel = headerRowHeights[cell.level] ?? baseHeaderHeight;
3575
+ const headerTop = (fixedHeader ? 0 : -currentScrollTop) + (headerRowOffsets[cell.level] ?? 0);
3576
+ const left = fixedSide === "left" ? leftFixedOffsets.get(cell.startIndex) ?? 0 : fixedSide === "right" ? viewport.width - (rightFixedOffsets.get(cell.startIndex) ?? 0) - startMetric.width : getScrollableColumnLeft(cell.startIndex);
3577
+ const width2 = cell.leaf ? startMetric.width : getCellDisplayWidth(cell.startIndex, cell.endIndex - cell.startIndex + 1);
3578
+ const visibleActions = cell.leaf ? getVisibleHeaderActions(column, startMetric.width, columnDraggable, measureHeaderTitleWidth(cell.startIndex)) : null;
3579
+ const actionWidth = visibleActions ? (Number(visibleActions.drag) + Number(visibleActions.filter) + Number(visibleActions.sort)) * 18 : columnDraggable && !cell.leaf ? 18 : 0;
3580
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
3581
+ "div",
3582
+ {
3583
+ className: `rvg-header-title rvg-header-group${column.renderHeader ? " is-custom" : ""}${cell.leaf ? " is-leaf" : ""}`,
3584
+ "data-level": cell.level,
3585
+ "data-measure-key": `group:${cell.key}:${cell.level}`,
3586
+ style: {
3587
+ left,
3588
+ top: headerTop,
3589
+ width: width2,
3590
+ height: rowHeightForLevel,
3591
+ paddingRight: cell.leaf ? actionWidth + 10 : 10,
3592
+ justifyContent: column.align === "right" ? "flex-end" : column.align === "center" ? "center" : "flex-start",
3593
+ textAlign: column.align === "right" ? "right" : column.align === "center" ? "center" : "left"
3594
+ },
3595
+ children: [
3596
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3597
+ "span",
3598
+ {
3599
+ ref: (node) => {
3600
+ if (!column.renderHeader) return;
3601
+ const key = `group:${cell.key}:${cell.level}`;
3602
+ if (node) {
3603
+ customHeaderContentRefs.current.set(key, node);
3604
+ requestAnimationFrame(measureCustomHeaderHeight);
3605
+ } else customHeaderContentRefs.current.delete(key);
3606
+ },
3607
+ children: column.renderHeader ? column.renderHeader(column) : column.title
3608
+ }
3609
+ ),
3610
+ !cell.leaf && columnDraggable && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3611
+ HeaderDragIcon,
3612
+ {
3613
+ className: `rvg-header-icon rvg-header-drag-icon${hoveredHeaderAction?.columnIndex === cell.startIndex && hoveredHeaderAction.action === "drag" ? " is-hovered" : ""}`,
3614
+ style: { left: width2 - 16, top: (rowHeightForLevel - 14) / 2 }
3615
+ }
3616
+ )
3617
+ ]
3618
+ },
3619
+ `group:${cell.key}:${cell.level}`
3620
+ );
3621
+ };
3056
3622
  const rangeHandlePosition = (() => {
3057
3623
  if (!rangeSelection) return null;
3058
3624
  const bounds = getRangeBounds();
@@ -3244,15 +3810,19 @@ function Table({
3244
3810
  const headerY = fixedHeader ? 0 : -scrollRef.current.top;
3245
3811
  const columnIndex = locateColumn(event.clientX);
3246
3812
  const inHeader = localY >= headerY && localY < headerY + headerHeight;
3247
- const hoveredAction = inHeader ? locateHeaderAction(event.clientX, columnIndex) : null;
3248
- const hoveredDragHandle = inHeader && locateHeaderDragHandle(event.clientX, columnIndex);
3249
- const hoveredTitle = inHeader && !hoveredAction && !hoveredDragHandle && columnIndex >= 0 && Boolean(columns[columnIndex]?.title) && (tooltipConfig.header || isHeaderTitleTruncated(columnIndex));
3250
- const nextHover = hoveredAction ? { columnIndex, action: hoveredAction } : hoveredDragHandle ? { columnIndex, action: "drag" } : hoveredTitle ? { columnIndex, action: "title" } : null;
3813
+ const inLeafHeader = inHeader && localY >= headerY + headerLeafTop;
3814
+ const hoveredHeaderCell = inHeader ? locateHeaderCell(event.clientX, event.clientY) : null;
3815
+ const hoveredAction = inLeafHeader ? locateHeaderAction(event.clientX, columnIndex) : null;
3816
+ const hoveredDragHandle = Boolean(hoveredHeaderCell && locateHeaderCellDragHandle(event.clientX, hoveredHeaderCell));
3817
+ const hoveredDragColumnIndex = hoveredHeaderCell?.startIndex ?? columnIndex;
3818
+ const hoveredTitle = inHeader && !hoveredAction && !hoveredDragHandle && hoveredHeaderCell && Boolean(hoveredHeaderCell.column.title) && isPointerOnHeaderTitle(event.clientX, event.clientY, hoveredHeaderCell) && (tooltipConfig.header || hoveredHeaderCell.leaf && isHeaderTitleTruncated(hoveredHeaderCell.startIndex));
3819
+ const nextHover = hoveredAction ? { columnIndex, action: hoveredAction } : hoveredDragHandle ? { columnIndex: hoveredDragColumnIndex, action: "drag" } : hoveredTitle ? { columnIndex: hoveredHeaderCell.startIndex, action: "title" } : null;
3820
+ const nextTooltip = hoveredAction && hoveredHeaderCell ? createHeaderTooltip(hoveredAction, columnIndex, hoveredHeaderCell) : hoveredDragHandle && hoveredHeaderCell ? createHeaderTooltip("drag", hoveredDragColumnIndex, hoveredHeaderCell) : hoveredTitle && hoveredHeaderCell ? createHeaderTooltip("title", hoveredHeaderCell.startIndex, hoveredHeaderCell) : null;
3251
3821
  const previousHover = hoveredHeaderActionRef.current;
3252
3822
  if (previousHover?.columnIndex !== nextHover?.columnIndex || previousHover?.action !== nextHover?.action) {
3253
3823
  hoveredHeaderActionRef.current = nextHover;
3254
3824
  setHoveredHeaderAction(nextHover);
3255
- showHeaderTooltipAfterDelay(nextHover);
3825
+ showHeaderTooltipAfterDelay(nextTooltip);
3256
3826
  scheduleDraw();
3257
3827
  }
3258
3828
  if (inHeader) {
@@ -3276,8 +3846,8 @@ function Table({
3276
3846
  if (annotation?.content.trim()) {
3277
3847
  const cellLeft = getDisplayedColumnLeft(cell.columnIndex);
3278
3848
  const cellTop = bodyTop + cell.rowIndex * rowHeight - scrollRef.current.top;
3279
- const nextTooltip = { rowIndex: cell.rowIndex, columnIndex: cell.columnIndex, left: cellLeft + metrics[cell.columnIndex].width + 6, top: cellTop + rowHeight / 2, label: annotation.content, color: annotation.color, annotation: true };
3280
- showCellTooltip(nextTooltip);
3849
+ const nextTooltip2 = { rowIndex: cell.rowIndex, columnIndex: cell.columnIndex, left: cellLeft + metrics[cell.columnIndex].width + 6, top: cellTop + rowHeight / 2, label: annotation.content, color: annotation.color, annotation: true };
3850
+ showCellTooltip(nextTooltip2);
3281
3851
  } else {
3282
3852
  const label = getCellLabel(cell.rowIndex, cell.columnIndex);
3283
3853
  const context = event.currentTarget.getContext("2d");
@@ -3304,7 +3874,7 @@ function Table({
3304
3874
  showCellTooltipAfterDelay(null);
3305
3875
  } else {
3306
3876
  const placement = textRight + 330 > viewport.width ? "left" : "right";
3307
- const nextTooltip = {
3877
+ const nextTooltip2 = {
3308
3878
  rowIndex: cell.rowIndex,
3309
3879
  columnIndex: cell.columnIndex,
3310
3880
  left: placement === "right" ? textRight + 16 : Math.max(8, textLeft - 16),
@@ -3312,13 +3882,13 @@ function Table({
3312
3882
  label,
3313
3883
  placement
3314
3884
  };
3315
- showCellTooltipAfterDelay(nextTooltip);
3885
+ showCellTooltipAfterDelay(nextTooltip2);
3316
3886
  }
3317
3887
  }
3318
3888
  }
3319
3889
  }
3320
3890
  }
3321
- const cursor = inHeader && locateResizeHandle(event.clientX) >= 0 ? "col-resize" : hoveredAction ? "pointer" : hoveredDragHandle ? "move" : columnIndex >= 0 && columns[columnIndex].rowSelection ? "pointer" : !inHeader && rowDraggable && columnIndex >= 0 && columns[columnIndex].rowDragHandle ? "move" : "default";
3891
+ const cursor = inHeader && locateHeaderResizeCell(event.clientX, event.clientY) ? "col-resize" : hoveredAction ? "pointer" : hoveredDragHandle ? "move" : columnIndex >= 0 && columns[columnIndex].rowSelection ? "pointer" : !inHeader && rowDraggable && columnIndex >= 0 && columns[columnIndex].rowDragHandle ? "move" : "default";
3322
3892
  if (event.currentTarget.style.cursor !== cursor) event.currentTarget.style.cursor = cursor;
3323
3893
  },
3324
3894
  onMouseLeave: (event) => {
@@ -3361,6 +3931,7 @@ function Table({
3361
3931
  const headerY = fixedHeader ? 0 : -scrollRef.current.top;
3362
3932
  if (localY >= headerY && localY < headerY + headerHeight) {
3363
3933
  clickedCellRef.current = null;
3934
+ if (localY < headerY + headerLeafTop) return;
3364
3935
  const columnIndex = locateColumn(event.clientX);
3365
3936
  const action = locateHeaderAction(event.clientX, columnIndex);
3366
3937
  if (action === "sort") {
@@ -3422,21 +3993,30 @@ function Table({
3422
3993
  className: "rvg-text-scroll-content",
3423
3994
  style: { left: -fixedWidth, width: contentWidth, transform: `translateX(${-currentScroll.left}px)` },
3424
3995
  children: [
3425
- columns.map((column, columnIndex) => column.fixed ? null : columnIndex >= domRange.columnStart && columnIndex < domRange.columnEnd ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { children: renderHeaderTitle(columnIndex, metrics[columnIndex].left) }, column.key) : null),
3996
+ columns.map((column, columnIndex) => column.fixed || columnIndex < utilityColumnCount || headerDepth > 1 ? null : columnIndex >= domRange.columnStart && columnIndex < domRange.columnEnd ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { children: renderHeaderTitle(columnIndex, getScrollableColumnLeft(columnIndex)) }, column.key) : null),
3997
+ headerCells.map((cell) => {
3998
+ if (columns[cell.startIndex]?.fixed) return null;
3999
+ if (cell.endIndex < domRange.columnStart || cell.startIndex >= domRange.columnEnd) return null;
4000
+ return renderGroupedHeaderCell(cell);
4001
+ }),
3426
4002
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "rvg-body-text-clip", style: { top: fixedHeader ? bodyTop : 0, bottom: 0 }, children: Array.from({ length: domRange.rowEnd - domRange.rowStart }, (_, offset) => domRange.rowStart + offset).flatMap((rowIndex) => columns.map((column, columnIndex) => {
3427
4003
  if (column.fixed || columnIndex < domRange.columnStart || columnIndex >= domRange.columnEnd) return null;
3428
- return renderCellText(rowIndex, columnIndex, metrics[columnIndex].left);
4004
+ return renderCellText(rowIndex, columnIndex, getScrollableColumnLeft(columnIndex));
3429
4005
  })) })
3430
4006
  ]
3431
4007
  }
3432
4008
  ) }),
3433
4009
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "rvg-body-text-clip", style: { top: fixedHeader ? bodyTop : 0, bottom: 0 }, children: Array.from({ length: domRange.rowEnd - domRange.rowStart }, (_, offset) => domRange.rowStart + offset).flatMap((rowIndex) => columns.map((column, columnIndex) => {
3434
4010
  if (!column.fixed) return null;
3435
- const left = column.fixed === "left" ? metrics[columnIndex].left : viewport.width - (rightFixedOffsets.get(columnIndex) ?? 0) - metrics[columnIndex].width;
4011
+ const left = column.fixed === "left" ? leftFixedOffsets.get(columnIndex) ?? 0 : viewport.width - (rightFixedOffsets.get(columnIndex) ?? 0) - metrics[columnIndex].width;
3436
4012
  return renderCellText(rowIndex, columnIndex, left);
3437
4013
  })) }),
3438
- columns.map((column, columnIndex) => column.fixed === "left" ? renderHeaderTitle(columnIndex, metrics[columnIndex].left) : null),
3439
- columns.map((column, columnIndex) => column.fixed === "right" ? renderHeaderTitle(columnIndex, viewport.width - (rightFixedOffsets.get(columnIndex) ?? 0) - metrics[columnIndex].width) : null)
4014
+ columns.map((column, columnIndex) => columnIndex < utilityColumnCount || headerDepth === 1 && (column.fixed === "left" || column.fixed === "right") ? renderHeaderTitle(columnIndex, column.fixed === "right" ? viewport.width - (rightFixedOffsets.get(columnIndex) ?? 0) - metrics[columnIndex].width : column.fixed === "left" ? leftFixedOffsets.get(columnIndex) ?? 0 : getScrollableColumnLeft(columnIndex)) : null),
4015
+ headerCells.map((cell) => columns[cell.startIndex]?.fixed ? renderGroupedHeaderCell(cell) : null),
4016
+ fixedWidth > 0 && currentScroll.left > 0 && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "rvg-text-fixed-shadow is-left", style: { left: fixedWidth } }),
4017
+ rightFixedWidth > 0 && currentScroll.left < contentWidth - viewport.width && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "rvg-text-fixed-shadow is-right", style: { right: rightFixedWidth } }),
4018
+ hasVerticalBorders && fixedWidth > 0 && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "rvg-text-fixed-border is-left", style: { left: fixedWidth - 1 } }),
4019
+ hasVerticalBorders && rightFixedWidth > 0 && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "rvg-text-fixed-border is-right", style: { right: rightFixedWidth } })
3440
4020
  ] }),
3441
4021
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "rvg-spacer", style: { width: contentWidth, height: contentHeight + bodyTop, marginTop: -renderHeight } })
3442
4022
  ] }),
@@ -3444,18 +4024,18 @@ function Table({
3444
4024
  "div",
3445
4025
  {
3446
4026
  className: `rvg-summary-row is-${summaryPosition}${hasVerticalBorders ? "" : " is-vertical-borderless"}`,
3447
- style: { height: rowHeight, top: summaryPosition === "top" ? headerHeight : void 0 },
4027
+ style: { height: rowHeight, top: summaryPosition === "top" ? fixedHeader ? headerHeight : headerHeight - currentScroll.top : void 0 },
3448
4028
  children: [
3449
4029
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "rvg-summary-scroll-clip", style: { left: fixedWidth, right: rightFixedWidth }, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3450
4030
  "div",
3451
4031
  {
3452
4032
  className: "rvg-summary-scroll-content",
3453
4033
  style: { left: -fixedWidth, width: contentWidth, transform: `translateX(${-currentScroll.left}px)` },
3454
- children: columns.map((column, columnIndex) => column.fixed ? null : renderSummaryCell(columnIndex, metrics[columnIndex].left))
4034
+ children: columns.map((column, columnIndex) => column.fixed ? null : renderSummaryCell(columnIndex, getScrollableColumnLeft(columnIndex)))
3455
4035
  }
3456
4036
  ) }),
3457
4037
  /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "rvg-summary-fixed-layer", children: [
3458
- columns.map((column, columnIndex) => column.fixed === "left" ? renderSummaryCell(columnIndex, metrics[columnIndex].left) : null),
4038
+ columns.map((column, columnIndex) => column.fixed === "left" ? renderSummaryCell(columnIndex, leftFixedOffsets.get(columnIndex) ?? 0) : null),
3459
4039
  columns.map((column, columnIndex) => column.fixed === "right" ? renderSummaryCell(columnIndex, viewport.width - (rightFixedOffsets.get(columnIndex) ?? 0) - metrics[columnIndex].width) : null)
3460
4040
  ] }),
3461
4041
  fixedWidth > 0 && currentScroll.left > 0 && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "rvg-summary-fixed-shadow is-left", style: { left: fixedWidth } }),
@@ -3472,11 +4052,11 @@ function Table({
3472
4052
  ref: scrollingHeaderIconsRef,
3473
4053
  className: "rvg-header-icon-layer rvg-header-icon-layer-scroll",
3474
4054
  style: { left: -fixedWidth, width: contentWidth, transform: `translateX(${-scrollRef.current.left}px)` },
3475
- children: columns.map((column, index) => column.fixed ? null : /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { children: renderHeaderIcons(index, metrics[index].left) }, column.key))
4055
+ children: columns.map((column, index) => column.fixed ? null : /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { children: renderHeaderIcons(index, getScrollableColumnLeft(index)) }, column.key))
3476
4056
  }
3477
4057
  ) }),
3478
4058
  /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "rvg-header-icon-layer", children: [
3479
- columns.map((column, index) => column.fixed === "left" ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { children: renderHeaderIcons(index, metrics[index].left) }, column.key) : null),
4059
+ columns.map((column, index) => column.fixed === "left" ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { children: renderHeaderIcons(index, leftFixedOffsets.get(index) ?? 0) }, column.key) : null),
3480
4060
  columns.map((column, index) => column.fixed === "right" ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { children: renderHeaderIcons(index, viewport.width - (rightFixedOffsets.get(index) ?? 0) - metrics[index].width) }, column.key) : null)
3481
4061
  ] })
3482
4062
  ] }),
@@ -3753,12 +4333,40 @@ function Table({
3753
4333
  ] })
3754
4334
  ] }) }),
3755
4335
  toast && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: `rvg-toast is-${toast.type}`, role: "status", children: toast.message }),
4336
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
4337
+ "div",
4338
+ {
4339
+ ref: verticalScrollbarRef,
4340
+ className: "rvg-vertical-scrollbar",
4341
+ onPointerEnter: () => keepCustomScrollbarVisible("vertical"),
4342
+ onPointerLeave: () => releaseCustomScrollbarVisible("vertical"),
4343
+ onPointerDown: handleVerticalScrollbarPointerDown,
4344
+ onPointerMove: handleVerticalScrollbarPointerMove,
4345
+ onPointerUp: handleVerticalScrollbarPointerUp,
4346
+ onPointerCancel: handleVerticalScrollbarPointerUp,
4347
+ children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { ref: verticalScrollbarThumbRef, className: "rvg-vertical-scrollbar-thumb" })
4348
+ }
4349
+ ),
4350
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
4351
+ "div",
4352
+ {
4353
+ ref: horizontalScrollbarRef,
4354
+ className: "rvg-horizontal-scrollbar",
4355
+ onPointerEnter: () => keepCustomScrollbarVisible("horizontal"),
4356
+ onPointerLeave: () => releaseCustomScrollbarVisible("horizontal"),
4357
+ onPointerDown: handleHorizontalScrollbarPointerDown,
4358
+ onPointerMove: handleHorizontalScrollbarPointerMove,
4359
+ onPointerUp: handleHorizontalScrollbarPointerUp,
4360
+ onPointerCancel: handleHorizontalScrollbarPointerUp,
4361
+ children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { ref: horizontalScrollbarThumbRef, className: "rvg-horizontal-scrollbar-thumb" })
4362
+ }
4363
+ ),
3756
4364
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { ref: dragGuideRef, className: "rvg-column-guide" }),
3757
4365
  resizeGuideX !== null && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3758
4366
  "div",
3759
4367
  {
3760
4368
  className: "rvg-column-guide is-resizing",
3761
- style: { display: "block", transform: `translateX(${Math.round(resizeGuideX)}px)` }
4369
+ style: { display: "block", top: resizeGuideTop, transform: `translateX(${Math.round(resizeGuideX)}px)` }
3762
4370
  }
3763
4371
  ),
3764
4372
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { ref: rowDragGuideRef, className: "rvg-row-guide" }),