@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.js CHANGED
@@ -863,6 +863,8 @@ function ellipsizeText(context, text, maxWidth) {
863
863
  }
864
864
  function paintGrid(options) {
865
865
  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;
866
+ const headerLeafTop = options.headerLeafTop ?? 0;
867
+ const headerLeafHeight = options.headerLeafHeight ?? headerHeight;
866
868
  const colors = { ...COLORS, ...options.colors };
867
869
  const bodyTop = options.bodyTop ?? headerHeight;
868
870
  const getCellSpan = (rowIndex, columnIndex) => options.cellSpans.get(getCellCoordKey(rowIndex, columnIndex));
@@ -879,9 +881,16 @@ function paintGrid(options) {
879
881
  ctx.fillRect(0, 0, width, height);
880
882
  ctx.font = "13px Inter, ui-sans-serif, system-ui, sans-serif";
881
883
  ctx.textBaseline = "middle";
882
- const leftFixedWidth = columns.reduce((value, column, index) => column.fixed === "left" ? Math.max(value, metrics[index].right) : value, 0);
884
+ const leftFixedWidth = columns.reduce((value, column, index) => column.fixed === "left" ? value + metrics[index].width : value, 0);
883
885
  const rightFixedWidth = columns.reduce((value, column, index) => column.fixed === "right" ? value + metrics[index].width : value, 0);
884
886
  const rightFixedLeft = width - rightFixedWidth;
887
+ const leftOffsets = /* @__PURE__ */ new Map();
888
+ let leftOffset = 0;
889
+ for (let index = 0; index < columns.length; index += 1) {
890
+ if (columns[index].fixed !== "left") continue;
891
+ leftOffsets.set(index, leftOffset);
892
+ leftOffset += metrics[index].width;
893
+ }
885
894
  const rightOffsets = /* @__PURE__ */ new Map();
886
895
  let rightOffset = 0;
887
896
  for (let index = columns.length - 1; index >= 0; index -= 1) {
@@ -889,8 +898,14 @@ function paintGrid(options) {
889
898
  rightOffsets.set(index, rightOffset);
890
899
  rightOffset += metrics[index].width;
891
900
  }
901
+ const scrollableLefts = /* @__PURE__ */ new Map();
902
+ let scrollableLeft = leftFixedWidth;
903
+ for (let index = 0; index < columns.length; index += 1) {
904
+ scrollableLefts.set(index, scrollableLeft);
905
+ if (columns[index].fixed === void 0) scrollableLeft += metrics[index].width;
906
+ }
892
907
  const getColumnX = (index) => {
893
- return columns[index].fixed === "left" ? metrics[index].left : columns[index].fixed === "right" ? width - (rightOffsets.get(index) ?? 0) - metrics[index].width : metrics[index].left - scrollLeft;
908
+ 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;
894
909
  };
895
910
  const paintBodyColumns = (layer) => {
896
911
  const rowStart = Math.max(0, range.rowStart - Math.max(0, options.maxRowSpan - 1));
@@ -1040,7 +1055,7 @@ function paintGrid(options) {
1040
1055
  if (!verticalBorderless) ctx.fillRect(x + metric.width - 1, headerY, 1, headerHeight);
1041
1056
  ctx.fillRect(x, headerY + headerHeight - 1, metric.width, 1);
1042
1057
  const column = columns[columnIndex];
1043
- if (!column.rowSelection && !column.rowDragHandle && !column.rowNumber && !column.renderHeader && column.title) {
1058
+ if (headerLeafTop === 0 && !column.rowSelection && !column.rowDragHandle && !column.rowNumber && !column.renderHeader && column.title) {
1044
1059
  ctx.font = "600 13px Inter, ui-sans-serif, system-ui, sans-serif";
1045
1060
  const titleWidth = ctx.measureText(column.title).width;
1046
1061
  const visibleActions = getVisibleHeaderActions(column, metric.width, columnDraggable, titleWidth);
@@ -1057,11 +1072,11 @@ function paintGrid(options) {
1057
1072
  const textX = titleAlign === "right" ? x + contentWidth - padding : titleAlign === "center" ? x + centeredTextX : x + padding;
1058
1073
  ctx.save();
1059
1074
  ctx.beginPath();
1060
- ctx.rect(x + 2, headerY + 1, Math.max(0, clipWidth - 4), headerHeight - 2);
1075
+ ctx.rect(x + 2, headerY + headerLeafTop + 1, Math.max(0, clipWidth - 4), headerLeafHeight - 2);
1061
1076
  ctx.clip();
1062
1077
  ctx.fillStyle = colors.muted;
1063
1078
  ctx.textAlign = titleAlign === "right" ? "right" : titleAlign === "center" ? "center" : "left";
1064
- ctx.fillText(title, textX, headerY + headerHeight / 2);
1079
+ ctx.fillText(title, textX, headerY + headerLeafTop + headerLeafHeight / 2);
1065
1080
  ctx.restore();
1066
1081
  }
1067
1082
  }
@@ -1069,22 +1084,6 @@ function paintGrid(options) {
1069
1084
  paintHeaderColumns("scroll");
1070
1085
  paintHeaderColumns("left");
1071
1086
  paintHeaderColumns("right");
1072
- if (leftFixedWidth > 0 && scrollLeft > 0) {
1073
- const shadowWidth = 10;
1074
- const shadow = ctx.createLinearGradient(leftFixedWidth, 0, leftFixedWidth + shadowWidth, 0);
1075
- shadow.addColorStop(0, "rgba(5, 5, 5, 0.06)");
1076
- shadow.addColorStop(1, "rgba(5, 5, 5, 0)");
1077
- ctx.fillStyle = shadow;
1078
- ctx.fillRect(leftFixedWidth, 0, shadowWidth, height);
1079
- }
1080
- if (rightFixedWidth > 0 && scrollLeft < (metrics[metrics.length - 1]?.right ?? 0) - width) {
1081
- const shadowWidth = 10;
1082
- const shadow = ctx.createLinearGradient(rightFixedLeft - shadowWidth, 0, rightFixedLeft, 0);
1083
- shadow.addColorStop(0, "rgba(5, 5, 5, 0)");
1084
- shadow.addColorStop(1, "rgba(5, 5, 5, 0.06)");
1085
- ctx.fillStyle = shadow;
1086
- ctx.fillRect(rightFixedLeft - shadowWidth, 0, shadowWidth, height);
1087
- }
1088
1087
  if (rightFixedWidth > 0 && !verticalBorderless) {
1089
1088
  ctx.fillStyle = colors.grid;
1090
1089
  ctx.fillRect(rightFixedLeft - 1, 0, 1, height);
@@ -1236,6 +1235,45 @@ var getRowNumberColumnWidth = (rowCount) => {
1236
1235
  const digits = String(Math.max(1, rowCount)).length;
1237
1236
  return digits <= 4 ? 44 : 44 + (digits - 4) * 8;
1238
1237
  };
1238
+ function getColumnDepth(column) {
1239
+ if (!column.children?.length) return 1;
1240
+ return 1 + Math.max(...column.children.map(getColumnDepth));
1241
+ }
1242
+ function flattenDataColumns(columns, inheritedFixed, insideGroup = false) {
1243
+ return columns.flatMap((column) => {
1244
+ if (!column.children?.length) {
1245
+ return insideGroup ? [{ ...column, fixed: inheritedFixed }] : [column];
1246
+ }
1247
+ return flattenDataColumns(column.children, column.fixed ?? inheritedFixed, true);
1248
+ });
1249
+ }
1250
+ function buildHeaderCells(sourceColumns, utilityColumnCount, depth) {
1251
+ const cells = [];
1252
+ let leafIndex = utilityColumnCount;
1253
+ const walk = (column, level, inheritedFixed, insideGroup, rootIndex, parentKey) => {
1254
+ const fixedColumn = insideGroup ? { ...column, fixed: inheritedFixed } : column;
1255
+ if (!fixedColumn.children?.length) {
1256
+ const startIndex2 = leafIndex;
1257
+ leafIndex += 1;
1258
+ cells.push({ key: fixedColumn.key, column: fixedColumn, level: depth - 1, startIndex: startIndex2, endIndex: startIndex2, leaf: true, parentKey, rootIndex });
1259
+ return { startIndex: startIndex2, endIndex: startIndex2 };
1260
+ }
1261
+ const startIndex = leafIndex;
1262
+ fixedColumn.children.forEach((child) => walk(child, level + 1, fixedColumn.fixed, true, rootIndex, fixedColumn.key));
1263
+ const endIndex = leafIndex - 1;
1264
+ cells.push({ key: fixedColumn.key, column: fixedColumn, level, startIndex, endIndex, leaf: false, parentKey, rootIndex });
1265
+ return { startIndex, endIndex };
1266
+ };
1267
+ sourceColumns.forEach((column, index) => walk(column, 0, void 0, false, index));
1268
+ return cells;
1269
+ }
1270
+ function hasNestedFixedColumns(columns, insideGroup = false) {
1271
+ return columns.some((column) => insideGroup && column.fixed !== void 0 || (column.children?.length ? hasNestedFixedColumns(column.children, true) : false));
1272
+ }
1273
+ function isProductionRuntime() {
1274
+ const env = globalThis.process?.env?.NODE_ENV;
1275
+ return env === "production";
1276
+ }
1239
1277
  var createPendingInsertId = () => `table-inserted-${globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(36).slice(2)}`}`;
1240
1278
  var waitForPaint = () => new Promise((resolve) => {
1241
1279
  requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
@@ -1350,10 +1388,24 @@ function Table({
1350
1388
  }) {
1351
1389
  const rowHeight = layout?.rowHeight ?? rowHeightProp ?? 36;
1352
1390
  const baseHeaderHeight = layout?.headerHeight ?? headerHeightProp ?? 40;
1391
+ const headerDepth = useMemo(() => Math.max(1, ...sourceColumns.map(getColumnDepth)), [sourceColumns]);
1353
1392
  const { language, labels } = useMemo(() => resolveGridLocale(locale), [locale]);
1354
1393
  const customHeaderContentRefs = useRef2(/* @__PURE__ */ new Map());
1355
- const [measuredHeaderHeight, setMeasuredHeaderHeight] = useState4(0);
1356
- const headerHeight = Math.max(baseHeaderHeight, measuredHeaderHeight);
1394
+ const customHeaderMeasurementsRef = useRef2(/* @__PURE__ */ new Map());
1395
+ const [measuredHeaderRowHeights, setMeasuredHeaderRowHeights] = useState4([]);
1396
+ const headerRowHeights = useMemo(() => Array.from({ length: headerDepth }, (_, level) => Math.max(baseHeaderHeight, measuredHeaderRowHeights[level] ?? 0)), [baseHeaderHeight, headerDepth, measuredHeaderRowHeights]);
1397
+ const headerRowOffsets = useMemo(() => {
1398
+ const offsets = [];
1399
+ let top = 0;
1400
+ headerRowHeights.forEach((height2) => {
1401
+ offsets.push(top);
1402
+ top += height2;
1403
+ });
1404
+ return offsets;
1405
+ }, [headerRowHeights]);
1406
+ const headerHeight = headerRowHeights.reduce((total, height2) => total + height2, 0);
1407
+ const headerLeafTop = headerRowOffsets[headerDepth - 1] ?? 0;
1408
+ const headerLeafHeight = headerRowHeights[headerDepth - 1] ?? baseHeaderHeight;
1357
1409
  const hasVerticalBorders = !(borderless ?? verticalBorderless ?? false);
1358
1410
  const hasStripedRows = Boolean(striped);
1359
1411
  const stripedColor = typeof striped === "string" ? striped : void 0;
@@ -1373,6 +1425,13 @@ function Table({
1373
1425
  const summaryEmptyValue = typeof summary === "object" ? summary.emptyValue ?? "" : "";
1374
1426
  const contextMenuEnabled = contextMenuConfig !== false;
1375
1427
  const customContextMenuConfig = typeof contextMenuConfig === "object" ? contextMenuConfig : void 0;
1428
+ const [resizedColumnWidths, setResizedColumnWidths] = useState4({});
1429
+ const leafSourceColumns = useMemo(() => flattenDataColumns(sourceColumns), [sourceColumns]);
1430
+ useEffect2(() => {
1431
+ if (!isProductionRuntime() && hasNestedFixedColumns(sourceColumns)) {
1432
+ console.warn("[Table] Multi-level headers only support fixed columns on top-level columns. Nested fixed values are ignored.");
1433
+ }
1434
+ }, [sourceColumns]);
1376
1435
  const columns = useMemo(() => {
1377
1436
  const utilityColumns = [];
1378
1437
  if (rowDraggable) {
@@ -1384,9 +1443,24 @@ function Table({
1384
1443
  if (rowNumber) {
1385
1444
  utilityColumns.push({ key: "__rvg_row_number__", title: "#", width: getRowNumberColumnWidth(sourceRows.length), align: "center", fixed: "left", rowNumber: true });
1386
1445
  }
1387
- return [...utilityColumns, ...sourceColumns];
1388
- }, [rowDraggable, rowNumber, rowSelection, sourceColumns, sourceRows.length]);
1446
+ const dataColumns = leafSourceColumns.map((column) => {
1447
+ const resizedWidth = resizedColumnWidths[column.key];
1448
+ return resizedWidth === void 0 ? column : { ...column, width: resizedWidth };
1449
+ });
1450
+ return [...utilityColumns, ...dataColumns];
1451
+ }, [leafSourceColumns, resizedColumnWidths, rowDraggable, rowNumber, rowSelection, sourceRows.length]);
1389
1452
  const utilityColumnCount = (rowDraggable ? 1 : 0) + (rowSelection ? 1 : 0) + (rowNumber ? 1 : 0);
1453
+ const headerCells = useMemo(() => buildHeaderCells(sourceColumns, utilityColumnCount, headerDepth), [headerDepth, sourceColumns, utilityColumnCount]);
1454
+ const activeCustomHeaderLevels = useMemo(() => {
1455
+ const levels = /* @__PURE__ */ new Map();
1456
+ columns.forEach((column) => {
1457
+ if (column.renderHeader) levels.set(column.key, headerDepth - 1);
1458
+ });
1459
+ headerCells.forEach((cell) => {
1460
+ if (cell.column.renderHeader) levels.set(`group:${cell.key}:${cell.level}`, cell.level);
1461
+ });
1462
+ return levels;
1463
+ }, [columns, headerCells, headerDepth]);
1390
1464
  const canvasRef = useRef2(null);
1391
1465
  const scrollerRef = useRef2(null);
1392
1466
  const frameRef = useRef2(null);
@@ -1404,6 +1478,14 @@ function Table({
1404
1478
  const dragGuideRef = useRef2(null);
1405
1479
  const rowDragGuideRef = useRef2(null);
1406
1480
  const selectionFocusRef = useRef2(null);
1481
+ const verticalScrollbarRef = useRef2(null);
1482
+ const verticalScrollbarThumbRef = useRef2(null);
1483
+ const verticalScrollbarDragRef = useRef2(null);
1484
+ const horizontalScrollbarRef = useRef2(null);
1485
+ const horizontalScrollbarThumbRef = useRef2(null);
1486
+ const horizontalScrollbarDragRef = useRef2(null);
1487
+ const verticalScrollbarVisibilityTimerRef = useRef2(null);
1488
+ const horizontalScrollbarVisibilityTimerRef = useRef2(null);
1407
1489
  const rowByKeyRef = useRef2(/* @__PURE__ */ new Map());
1408
1490
  const rowIndexByKeyRef = useRef2(/* @__PURE__ */ new Map());
1409
1491
  const headerIconsRef = useRef2(null);
@@ -1456,24 +1538,57 @@ function Table({
1456
1538
  rangeDragRef.current = null;
1457
1539
  setSelectionRange(null);
1458
1540
  }, [rangeSelection]);
1541
+ useEffect2(() => {
1542
+ setResizedColumnWidths((current) => {
1543
+ const sourceColumnByKey = new Map(leafSourceColumns.map((column) => [column.key, column]));
1544
+ let changed = false;
1545
+ const next = {};
1546
+ Object.entries(current).forEach(([key, value]) => {
1547
+ const sourceColumn = sourceColumnByKey.get(key);
1548
+ if (!sourceColumn) {
1549
+ changed = true;
1550
+ return;
1551
+ }
1552
+ if (sourceColumn.width !== void 0 && sourceColumn.width !== value) {
1553
+ changed = true;
1554
+ return;
1555
+ }
1556
+ next[key] = value;
1557
+ });
1558
+ return changed ? next : current;
1559
+ });
1560
+ }, [leafSourceColumns]);
1459
1561
  useEffect2(() => () => {
1460
1562
  if (headerTooltipTimerRef.current !== null) clearTimeout(headerTooltipTimerRef.current);
1461
1563
  if (cellTooltipTimerRef.current !== null) clearTimeout(cellTooltipTimerRef.current);
1462
1564
  }, []);
1463
1565
  const measureCustomHeaderHeight = useCallback2(() => {
1464
- let nextHeight = 0;
1566
+ const nextHeights = Array.from({ length: headerDepth }, () => baseHeaderHeight);
1465
1567
  customHeaderContentRefs.current.forEach((node) => {
1466
- nextHeight = Math.max(nextHeight, Math.ceil(node.scrollHeight) + 16);
1568
+ const headerCell = node.closest(".rvg-header-title");
1569
+ const key = headerCell?.dataset.measureKey;
1570
+ const level = key ? activeCustomHeaderLevels.get(key) : void 0;
1571
+ if (!key || level === void 0 || level < 0 || level >= nextHeights.length) return;
1572
+ customHeaderMeasurementsRef.current.set(key, { level, height: Math.ceil(node.scrollHeight) + 16 });
1467
1573
  });
1468
- setMeasuredHeaderHeight((current) => current === nextHeight ? current : nextHeight);
1469
- }, []);
1574
+ customHeaderMeasurementsRef.current.forEach((measurement, key) => {
1575
+ if (!activeCustomHeaderLevels.has(key)) return;
1576
+ nextHeights[measurement.level] = Math.max(nextHeights[measurement.level], measurement.height);
1577
+ });
1578
+ setMeasuredHeaderRowHeights((current) => {
1579
+ const changed = nextHeights.length !== current.length || nextHeights.some((height2, index) => height2 !== current[index]);
1580
+ return changed ? nextHeights : current;
1581
+ });
1582
+ }, [activeCustomHeaderLevels, baseHeaderHeight, headerDepth]);
1470
1583
  useLayoutEffect2(() => {
1471
- const activeCustomHeaderKeys = new Set(columns.filter((column) => column.renderHeader).map((column) => column.key));
1472
1584
  customHeaderContentRefs.current.forEach((_, key) => {
1473
- if (!activeCustomHeaderKeys.has(key)) customHeaderContentRefs.current.delete(key);
1585
+ if (!activeCustomHeaderLevels.has(key)) customHeaderContentRefs.current.delete(key);
1586
+ });
1587
+ customHeaderMeasurementsRef.current.forEach((_, key) => {
1588
+ if (!activeCustomHeaderLevels.has(key)) customHeaderMeasurementsRef.current.delete(key);
1474
1589
  });
1475
- if (customHeaderContentRefs.current.size === 0) {
1476
- setMeasuredHeaderHeight(0);
1590
+ if (activeCustomHeaderLevels.size === 0) {
1591
+ setMeasuredHeaderRowHeights([]);
1477
1592
  return;
1478
1593
  }
1479
1594
  measureCustomHeaderHeight();
@@ -1481,7 +1596,7 @@ function Table({
1481
1596
  const observer = new ResizeObserver(measureCustomHeaderHeight);
1482
1597
  customHeaderContentRefs.current.forEach((node) => observer.observe(node));
1483
1598
  return () => observer.disconnect();
1484
- }, [columns, measureCustomHeaderHeight, viewport.width]);
1599
+ }, [activeCustomHeaderLevels, measureCustomHeaderHeight, viewport.width]);
1485
1600
  const [filterEditor, setFilterEditor] = useState4(null);
1486
1601
  const [contextMenu, setContextMenu] = useState4(null);
1487
1602
  const [insertCounts, setInsertCounts] = useState4({ before: 1, after: 1 });
@@ -1491,6 +1606,7 @@ function Table({
1491
1606
  const [deleteBusy, setDeleteBusy] = useState4(false);
1492
1607
  const [toast, setToast] = useState4(null);
1493
1608
  const [resizeGuideX, setResizeGuideX] = useState4(null);
1609
+ const [resizeGuideTop, setResizeGuideTop] = useState4(0);
1494
1610
  const [hoveredHeaderAction, setHoveredHeaderAction] = useState4(null);
1495
1611
  const [visibleHeaderTooltip, setVisibleHeaderTooltip] = useState4(null);
1496
1612
  const [hoveredCellTooltip, setHoveredCellTooltip] = useState4(null);
@@ -1575,7 +1691,17 @@ function Table({
1575
1691
  const metrics = useMemo(() => buildColumnMetrics(columns, { columnDraggable, viewportWidth: viewport.width }), [columnDraggable, columns, viewport.width]);
1576
1692
  const contentWidth = metrics.length > 0 ? metrics[metrics.length - 1].right : 0;
1577
1693
  const contentHeight = rows.length * rowHeight;
1578
- const fixedWidth = useMemo(() => columns.reduce((width2, column, index) => column.fixed === "left" ? Math.max(width2, metrics[index].right) : width2, 0), [columns, metrics]);
1694
+ const fixedWidth = useMemo(() => columns.reduce((width2, column, index) => column.fixed === "left" ? width2 + metrics[index].width : width2, 0), [columns, metrics]);
1695
+ const leftFixedOffsets = useMemo(() => {
1696
+ const offsets = /* @__PURE__ */ new Map();
1697
+ let offset = 0;
1698
+ for (let index = 0; index < columns.length; index += 1) {
1699
+ if (columns[index].fixed !== "left") continue;
1700
+ offsets.set(index, offset);
1701
+ offset += metrics[index].width;
1702
+ }
1703
+ return offsets;
1704
+ }, [columns, metrics]);
1579
1705
  const rightFixedWidth = useMemo(() => columns.reduce((width2, column, index) => column.fixed === "right" ? width2 + metrics[index].width : width2, 0), [columns, metrics]);
1580
1706
  const rightFixedOffsets = useMemo(() => {
1581
1707
  const offsets = /* @__PURE__ */ new Map();
@@ -1587,6 +1713,16 @@ function Table({
1587
1713
  }
1588
1714
  return offsets;
1589
1715
  }, [columns, metrics]);
1716
+ const scrollableColumnLefts = useMemo(() => {
1717
+ const lefts = [];
1718
+ let width2 = fixedWidth;
1719
+ for (let index = 0; index < columns.length; index += 1) {
1720
+ lefts[index] = width2;
1721
+ if (columns[index].fixed === void 0) width2 += metrics[index].width;
1722
+ }
1723
+ return lefts;
1724
+ }, [columns, fixedWidth, metrics]);
1725
+ const getScrollableColumnLeft = useCallback2((columnIndex) => scrollableColumnLefts[columnIndex] ?? fixedWidth, [fixedWidth, scrollableColumnLefts]);
1590
1726
  const hasRowSelectionColumn = useMemo(() => columns.some((column) => column.rowSelection), [columns]);
1591
1727
  const rowSelectionMode = typeof rowSelection === "object" ? rowSelection.mode ?? "multiple" : "multiple";
1592
1728
  const hasSummaryRow = useMemo(() => rows.length > 0 && summaryEnabled && columns.some((column) => Boolean(column.summary)), [columns, rows.length, summaryEnabled]);
@@ -1597,7 +1733,7 @@ function Table({
1597
1733
  const realContentHeight = bodyTop + bodyContentHeight + bottomSummaryHeight + (contentWidth > viewport.width ? horizontalScrollbarHeight : 0);
1598
1734
  const effectiveHeight = autoHeight ? Math.max(bodyTop + bottomSummaryHeight, Math.min(resolvedHeight, realContentHeight)) : resolvedHeight;
1599
1735
  const renderHeight = Math.max(0, effectiveHeight - bottomSummaryHeight);
1600
- const bodyViewportHeight = Math.max(0, renderHeight - (fixedHeader ? headerHeight : 0) - topSummaryHeight);
1736
+ const bodyViewportHeight = Math.max(0, renderHeight - (fixedHeader ? headerHeight + topSummaryHeight : 0));
1601
1737
  const [summaryState, setSummaryState] = useState4(() => ({ columns: null, rows: null, values: /* @__PURE__ */ new Map(), pending: false }));
1602
1738
  const summaryPending = hasSummaryRow && (loading || summaryState.pending || summaryState.columns !== columns || summaryState.rows !== rows);
1603
1739
  const summaryValues = hasSummaryRow && summaryState.columns === columns && summaryState.rows === rows ? summaryState.values : /* @__PURE__ */ new Map();
@@ -1773,8 +1909,8 @@ function Table({
1773
1909
  const scroll = scrollRef.current;
1774
1910
  const rowScrollTop = fixedHeader ? scroll.top : Math.max(0, scroll.top - bodyTop);
1775
1911
  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 };
1776
- paintGrid({ context, width: viewport.width, height: renderHeight, pixelRatio: ratio, scrollLeft: scroll.left, scrollTop: scroll.top, rowHeight, headerHeight, bodyTop, suppressLastRowBottomBorder: bottomSummaryHeight > 0, suppressFrameBottomBorder: bottomSummaryHeight > 0, fixedHeader, verticalBorderless: !hasVerticalBorders, striped: hasStripedRows, columnDraggable, sortState, filterValues, hoveredHeaderAction: hoveredHeaderActionRef.current, rows, columns, metrics, range, selection, editing, hoveredRowIndex: hoveredRowIndexRef.current, selectionRange, selectedRowKeys: selectedRowKeySet, selectedColumnKeys: selectedColumnKeySet, cellSpans: cellSpanLookup.covered, maxRowSpan: cellSpanLookup.maxRowSpan, highlightEditedCells: highlightsEditedCells, highlightInsertedRows: highlightsInsertedRows, insertedRowKeys: insertedRowKeySet, editedCellKeys, cellAnnotations, getRowKey, rowDragPreview, colors: themeColors });
1777
- }, [bodyTop, bodyViewportHeight, bottomSummaryHeight, cellAnnotations, cellSpanLookup, columnDraggable, columns, editedCellHighlightColor, editedCellKeys, editing, renderHeight, filterValues, fixedHeader, getRowKey, hasStripedRows, hasVerticalBorders, headerHeight, highlightsEditedCells, highlightsInsertedRows, insertedRowHighlightColor, insertedRowKeySet, isVirtualized, metrics, rowDragPreview, rowHeight, rows, selectedColumnKeySet, selectedRowKeySet, selection, selectionRange, sortState, stripedColor, viewport.width, virtualOverscan]);
1912
+ 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 });
1913
+ }, [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]);
1778
1914
  const scheduleDraw = useCallback2(() => {
1779
1915
  if (frameRef.current === null) frameRef.current = requestAnimationFrame(draw);
1780
1916
  }, [draw]);
@@ -1811,6 +1947,8 @@ function Table({
1811
1947
  if (sortDebounceRef.current !== null) clearTimeout(sortDebounceRef.current);
1812
1948
  if (textFrameRef.current !== null) cancelAnimationFrame(textFrameRef.current);
1813
1949
  if (toastTimerRef.current !== null) clearTimeout(toastTimerRef.current);
1950
+ if (verticalScrollbarVisibilityTimerRef.current !== null) clearTimeout(verticalScrollbarVisibilityTimerRef.current);
1951
+ if (horizontalScrollbarVisibilityTimerRef.current !== null) clearTimeout(horizontalScrollbarVisibilityTimerRef.current);
1814
1952
  }, []);
1815
1953
  useEffect2(() => {
1816
1954
  const scroller = scrollerRef.current;
@@ -1862,9 +2000,204 @@ function Table({
1862
2000
  scroller.addEventListener("scroll", lockScroll);
1863
2001
  return () => scroller.removeEventListener("scroll", lockScroll);
1864
2002
  }, [insertBusy]);
2003
+ const showCustomScrollbar = useCallback2((axis, persist = false) => {
2004
+ const showVertical = axis === "vertical" || axis === "both";
2005
+ const showHorizontal = axis === "horizontal" || axis === "both";
2006
+ if (showVertical) {
2007
+ verticalScrollbarRef.current?.classList.add("is-active");
2008
+ if (verticalScrollbarVisibilityTimerRef.current !== null) clearTimeout(verticalScrollbarVisibilityTimerRef.current);
2009
+ if (!persist) {
2010
+ verticalScrollbarVisibilityTimerRef.current = setTimeout(() => {
2011
+ verticalScrollbarRef.current?.classList.remove("is-active");
2012
+ verticalScrollbarVisibilityTimerRef.current = null;
2013
+ }, 900);
2014
+ }
2015
+ }
2016
+ if (showHorizontal) {
2017
+ horizontalScrollbarRef.current?.classList.add("is-active");
2018
+ if (horizontalScrollbarVisibilityTimerRef.current !== null) clearTimeout(horizontalScrollbarVisibilityTimerRef.current);
2019
+ if (!persist) {
2020
+ horizontalScrollbarVisibilityTimerRef.current = setTimeout(() => {
2021
+ horizontalScrollbarRef.current?.classList.remove("is-active");
2022
+ horizontalScrollbarVisibilityTimerRef.current = null;
2023
+ }, 900);
2024
+ }
2025
+ }
2026
+ }, []);
2027
+ const keepCustomScrollbarVisible = useCallback2((axis) => {
2028
+ showCustomScrollbar(axis, true);
2029
+ }, [showCustomScrollbar]);
2030
+ const releaseCustomScrollbarVisible = useCallback2((axis) => {
2031
+ if (axis === "vertical" && verticalScrollbarDragRef.current) return;
2032
+ if (axis === "horizontal" && horizontalScrollbarDragRef.current) return;
2033
+ showCustomScrollbar(axis);
2034
+ }, [showCustomScrollbar]);
2035
+ const updateCustomScrollbars = useCallback2(() => {
2036
+ const scroller = scrollerRef.current;
2037
+ const verticalTrack = verticalScrollbarRef.current;
2038
+ const verticalThumb = verticalScrollbarThumbRef.current;
2039
+ const horizontalTrack = horizontalScrollbarRef.current;
2040
+ const horizontalThumb = horizontalScrollbarThumbRef.current;
2041
+ if (!scroller || !verticalTrack || !verticalThumb || !horizontalTrack || !horizontalThumb) return;
2042
+ const maxScrollTop = scroller.scrollHeight - scroller.clientHeight;
2043
+ if (maxScrollTop <= 1 || renderHeight <= 0) {
2044
+ verticalTrack.style.display = "none";
2045
+ } else {
2046
+ const top = fixedHeader ? bodyTop : 0;
2047
+ const bottom = bottomSummaryHeight;
2048
+ const trackHeight = Math.max(0, effectiveHeight - top - bottom);
2049
+ if (trackHeight <= 0) {
2050
+ verticalTrack.style.display = "none";
2051
+ } else {
2052
+ const thumbHeight = Math.max(28, Math.round(trackHeight * (scroller.clientHeight / scroller.scrollHeight)));
2053
+ const maxThumbTop = Math.max(0, trackHeight - thumbHeight);
2054
+ const thumbTop = maxScrollTop > 0 ? Math.round(scroller.scrollTop / maxScrollTop * maxThumbTop) : 0;
2055
+ verticalTrack.style.display = "block";
2056
+ verticalTrack.style.top = `${top}px`;
2057
+ verticalTrack.style.bottom = `${bottom}px`;
2058
+ verticalThumb.style.height = `${Math.min(trackHeight, thumbHeight)}px`;
2059
+ verticalThumb.style.transform = `translateY(${thumbTop}px)`;
2060
+ }
2061
+ }
2062
+ const maxScrollLeft = scroller.scrollWidth - scroller.clientWidth;
2063
+ if (maxScrollLeft <= 1 || viewport.width <= 0) {
2064
+ horizontalTrack.style.display = "none";
2065
+ return;
2066
+ }
2067
+ const trackLeft = fixedWidth;
2068
+ const trackRight = rightFixedWidth;
2069
+ const trackWidth = Math.max(0, viewport.width - trackLeft - trackRight);
2070
+ if (trackWidth <= 0) {
2071
+ horizontalTrack.style.display = "none";
2072
+ return;
2073
+ }
2074
+ const thumbWidth = Math.max(36, Math.round(trackWidth * (scroller.clientWidth / scroller.scrollWidth)));
2075
+ const maxThumbLeft = Math.max(0, trackWidth - thumbWidth);
2076
+ const thumbLeft = maxScrollLeft > 0 ? Math.round(scroller.scrollLeft / maxScrollLeft * maxThumbLeft) : 0;
2077
+ horizontalTrack.style.display = "block";
2078
+ horizontalTrack.style.left = `${trackLeft}px`;
2079
+ horizontalTrack.style.right = `${trackRight}px`;
2080
+ horizontalTrack.style.bottom = `${bottomSummaryHeight + 2}px`;
2081
+ horizontalThumb.style.width = `${Math.min(trackWidth, thumbWidth)}px`;
2082
+ horizontalThumb.style.transform = `translateX(${thumbLeft}px)`;
2083
+ }, [bodyTop, bottomSummaryHeight, effectiveHeight, fixedHeader, fixedWidth, renderHeight, rightFixedWidth, viewport.width]);
2084
+ useLayoutEffect2(() => {
2085
+ updateCustomScrollbars();
2086
+ }, [contentHeight, contentWidth, updateCustomScrollbars, viewport.height, viewport.width]);
2087
+ const handleVerticalScrollbarPointerDown = useCallback2((event) => {
2088
+ const scroller = scrollerRef.current;
2089
+ const track = verticalScrollbarRef.current;
2090
+ const thumb = verticalScrollbarThumbRef.current;
2091
+ if (!scroller || !track || !thumb || event.button !== 0) return;
2092
+ event.preventDefault();
2093
+ event.currentTarget.setPointerCapture(event.pointerId);
2094
+ const trackRect = track.getBoundingClientRect();
2095
+ const thumbRect = thumb.getBoundingClientRect();
2096
+ const maxScrollTop = scroller.scrollHeight - scroller.clientHeight;
2097
+ const trackHeight = trackRect.height;
2098
+ const thumbHeight = thumbRect.height;
2099
+ showCustomScrollbar("vertical", true);
2100
+ if (event.target === thumb) {
2101
+ verticalScrollbarDragRef.current = {
2102
+ pointerId: event.pointerId,
2103
+ startY: event.clientY,
2104
+ startTop: thumbRect.top - trackRect.top,
2105
+ trackHeight,
2106
+ thumbHeight,
2107
+ maxScrollTop
2108
+ };
2109
+ return;
2110
+ }
2111
+ const nextThumbTop = Math.max(0, Math.min(trackHeight - thumbHeight, event.clientY - trackRect.top - thumbHeight / 2));
2112
+ const maxThumbTop = Math.max(1, trackHeight - thumbHeight);
2113
+ scroller.scrollTop = nextThumbTop / maxThumbTop * maxScrollTop;
2114
+ updateCustomScrollbars();
2115
+ verticalScrollbarDragRef.current = {
2116
+ pointerId: event.pointerId,
2117
+ startY: event.clientY,
2118
+ startTop: nextThumbTop,
2119
+ trackHeight,
2120
+ thumbHeight,
2121
+ maxScrollTop
2122
+ };
2123
+ }, [showCustomScrollbar, updateCustomScrollbars]);
2124
+ const handleVerticalScrollbarPointerMove = useCallback2((event) => {
2125
+ const drag = verticalScrollbarDragRef.current;
2126
+ const scroller = scrollerRef.current;
2127
+ if (!drag || !scroller || drag.pointerId !== event.pointerId) return;
2128
+ event.preventDefault();
2129
+ const maxThumbTop = Math.max(1, drag.trackHeight - drag.thumbHeight);
2130
+ const nextThumbTop = Math.max(0, Math.min(maxThumbTop, drag.startTop + event.clientY - drag.startY));
2131
+ scroller.scrollTop = nextThumbTop / maxThumbTop * drag.maxScrollTop;
2132
+ updateCustomScrollbars();
2133
+ }, [updateCustomScrollbars]);
2134
+ const handleVerticalScrollbarPointerUp = useCallback2((event) => {
2135
+ const drag = verticalScrollbarDragRef.current;
2136
+ if (!drag || drag.pointerId !== event.pointerId) return;
2137
+ if (event.currentTarget.hasPointerCapture(event.pointerId)) event.currentTarget.releasePointerCapture(event.pointerId);
2138
+ verticalScrollbarDragRef.current = null;
2139
+ showCustomScrollbar("vertical");
2140
+ }, [showCustomScrollbar]);
2141
+ const handleHorizontalScrollbarPointerDown = useCallback2((event) => {
2142
+ const scroller = scrollerRef.current;
2143
+ const track = horizontalScrollbarRef.current;
2144
+ const thumb = horizontalScrollbarThumbRef.current;
2145
+ if (!scroller || !track || !thumb || event.button !== 0) return;
2146
+ event.preventDefault();
2147
+ event.currentTarget.setPointerCapture(event.pointerId);
2148
+ const trackRect = track.getBoundingClientRect();
2149
+ const thumbRect = thumb.getBoundingClientRect();
2150
+ const maxScrollLeft = scroller.scrollWidth - scroller.clientWidth;
2151
+ const trackWidth = trackRect.width;
2152
+ const thumbWidth = thumbRect.width;
2153
+ showCustomScrollbar("horizontal", true);
2154
+ if (event.target === thumb) {
2155
+ horizontalScrollbarDragRef.current = {
2156
+ pointerId: event.pointerId,
2157
+ startX: event.clientX,
2158
+ startLeft: thumbRect.left - trackRect.left,
2159
+ trackWidth,
2160
+ thumbWidth,
2161
+ maxScrollLeft
2162
+ };
2163
+ return;
2164
+ }
2165
+ const nextThumbLeft = Math.max(0, Math.min(trackWidth - thumbWidth, event.clientX - trackRect.left - thumbWidth / 2));
2166
+ const maxThumbLeft = Math.max(1, trackWidth - thumbWidth);
2167
+ scroller.scrollLeft = nextThumbLeft / maxThumbLeft * maxScrollLeft;
2168
+ updateCustomScrollbars();
2169
+ horizontalScrollbarDragRef.current = {
2170
+ pointerId: event.pointerId,
2171
+ startX: event.clientX,
2172
+ startLeft: nextThumbLeft,
2173
+ trackWidth,
2174
+ thumbWidth,
2175
+ maxScrollLeft
2176
+ };
2177
+ }, [showCustomScrollbar, updateCustomScrollbars]);
2178
+ const handleHorizontalScrollbarPointerMove = useCallback2((event) => {
2179
+ const drag = horizontalScrollbarDragRef.current;
2180
+ const scroller = scrollerRef.current;
2181
+ if (!drag || !scroller || drag.pointerId !== event.pointerId) return;
2182
+ event.preventDefault();
2183
+ const maxThumbLeft = Math.max(1, drag.trackWidth - drag.thumbWidth);
2184
+ const nextThumbLeft = Math.max(0, Math.min(maxThumbLeft, drag.startLeft + event.clientX - drag.startX));
2185
+ scroller.scrollLeft = nextThumbLeft / maxThumbLeft * drag.maxScrollLeft;
2186
+ updateCustomScrollbars();
2187
+ }, [updateCustomScrollbars]);
2188
+ const handleHorizontalScrollbarPointerUp = useCallback2((event) => {
2189
+ const drag = horizontalScrollbarDragRef.current;
2190
+ if (!drag || drag.pointerId !== event.pointerId) return;
2191
+ if (event.currentTarget.hasPointerCapture(event.pointerId)) event.currentTarget.releasePointerCapture(event.pointerId);
2192
+ horizontalScrollbarDragRef.current = null;
2193
+ showCustomScrollbar("horizontal");
2194
+ }, [showCustomScrollbar]);
1865
2195
  const handleScroll = useCallback2(() => {
1866
2196
  const element = scrollerRef.current;
1867
2197
  if (!element) return;
2198
+ const previousScroll = scrollRef.current;
2199
+ const scrollDeltaX = element.scrollLeft - previousScroll.left;
2200
+ const scrollDeltaY = element.scrollTop - previousScroll.top;
1868
2201
  scrollRef.current = { left: element.scrollLeft, top: element.scrollTop };
1869
2202
  const focus = selectionFocusRef.current;
1870
2203
  if (focus && selection && !selectionRange) {
@@ -1875,7 +2208,7 @@ function Table({
1875
2208
  if (!validSelection) {
1876
2209
  focus.style.display = "none";
1877
2210
  } else {
1878
- const rawLeft = column.fixed === "left" ? metric.left : column.fixed === "right" ? viewport.width - (rightFixedOffsets.get(selection.columnIndex) ?? 0) - metric.width : metric.left - element.scrollLeft;
2211
+ 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;
1879
2212
  const rawTop = bodyTop + selection.rowIndex * rowHeight - element.scrollTop;
1880
2213
  const span = getCellSpan(selection.rowIndex, selection.columnIndex);
1881
2214
  const rawRight = rawLeft + getCellDisplayWidth(selection.columnIndex, span?.colSpan ?? 1);
@@ -1908,6 +2241,10 @@ function Table({
1908
2241
  if (scrollingTextRef.current) scrollingTextRef.current.style.transform = `translateX(${-element.scrollLeft}px)`;
1909
2242
  if (scrollingHeaderIconsRef.current) scrollingHeaderIconsRef.current.style.transform = `translateX(${-element.scrollLeft}px)`;
1910
2243
  if (headerIconsRef.current) headerIconsRef.current.style.transform = `translateY(${fixedHeader ? 0 : -element.scrollTop}px)`;
2244
+ updateCustomScrollbars();
2245
+ if (scrollDeltaX !== 0 || scrollDeltaY !== 0) {
2246
+ showCustomScrollbar(scrollDeltaX !== 0 && scrollDeltaY !== 0 ? "both" : scrollDeltaX !== 0 ? "horizontal" : "vertical");
2247
+ }
1911
2248
  if (hoveredHeaderActionRef.current) {
1912
2249
  hoveredHeaderActionRef.current = null;
1913
2250
  setHoveredHeaderAction(null);
@@ -1915,7 +2252,7 @@ function Table({
1915
2252
  setHoveredCellTooltip(null);
1916
2253
  scheduleDraw();
1917
2254
  setEditing(null);
1918
- }, [bodyTop, columns, renderHeight, fixedHeader, fixedWidth, getCellDisplayWidth, getCellSpan, getRowKey, metrics, rightFixedOffsets, rightFixedWidth, rowHeight, rows, scheduleDraw, selection, selectionRange, viewport.width]);
2255
+ }, [bodyTop, columns, renderHeight, fixedHeader, fixedWidth, getCellDisplayWidth, getCellSpan, getRowKey, getScrollableColumnLeft, metrics, rightFixedOffsets, rightFixedWidth, rowHeight, rows, scheduleDraw, selection, selectionRange, showCustomScrollbar, updateCustomScrollbars, viewport.width]);
1919
2256
  const locateColumn = useCallback2((clientX) => {
1920
2257
  const canvas = canvasRef.current;
1921
2258
  if (!canvas) return -1;
@@ -1923,7 +2260,11 @@ function Table({
1923
2260
  const localX = clientX - rect.left;
1924
2261
  let columnIndex;
1925
2262
  if (localX < fixedWidth) {
1926
- columnIndex = hitTestColumn(metrics, localX);
2263
+ columnIndex = columns.findIndex((column, index) => {
2264
+ if (column.fixed !== "left") return false;
2265
+ const left = leftFixedOffsets.get(index) ?? 0;
2266
+ return localX >= left && localX < left + metrics[index].width;
2267
+ });
1927
2268
  } else if (rightFixedWidth > 0 && localX >= viewport.width - rightFixedWidth) {
1928
2269
  columnIndex = columns.findIndex((column, index) => {
1929
2270
  if (column.fixed !== "right") return false;
@@ -1931,41 +2272,33 @@ function Table({
1931
2272
  return localX >= left && localX < left + metrics[index].width;
1932
2273
  });
1933
2274
  } else {
1934
- columnIndex = hitTestColumn(metrics, localX + scrollRef.current.left);
2275
+ const scrollX = localX + scrollRef.current.left;
2276
+ columnIndex = columns.findIndex((column, index) => {
2277
+ if (column.fixed !== void 0) return false;
2278
+ const left = getScrollableColumnLeft(index);
2279
+ return scrollX >= left && scrollX < left + metrics[index].width;
2280
+ });
1935
2281
  }
1936
2282
  return columnIndex;
1937
- }, [columns, fixedWidth, metrics, rightFixedOffsets, rightFixedWidth, viewport.width]);
2283
+ }, [columns, fixedWidth, getScrollableColumnLeft, leftFixedOffsets, metrics, rightFixedOffsets, rightFixedWidth, viewport.width]);
1938
2284
  const getDisplayedColumnLeft = useCallback2((columnIndex) => {
1939
2285
  const metric = metrics[columnIndex];
1940
- if (columns[columnIndex].fixed === "left") return metric.left;
2286
+ if (columns[columnIndex].fixed === "left") return leftFixedOffsets.get(columnIndex) ?? 0;
1941
2287
  if (columns[columnIndex].fixed === "right") return viewport.width - (rightFixedOffsets.get(columnIndex) ?? 0) - metric.width;
1942
- return metric.left - scrollRef.current.left;
1943
- }, [columns, metrics, rightFixedOffsets, viewport.width]);
1944
- const locateResizeHandle = useCallback2((clientX) => {
1945
- if (!columnResizable || !canvasRef.current) return -1;
1946
- const localX = clientX - canvasRef.current.getBoundingClientRect().left;
1947
- let closest = -1;
1948
- let distance = 6;
1949
- for (let index = 0; index < columns.length; index += 1) {
1950
- if (columns[index].rowSelection || columns[index].rowDragHandle || columns[index].rowNumber) continue;
1951
- const nextDistance = Math.abs(localX - (getDisplayedColumnLeft(index) + metrics[index].width));
1952
- if (nextDistance < distance) {
1953
- distance = nextDistance;
1954
- closest = index;
1955
- }
2288
+ return getScrollableColumnLeft(columnIndex) - scrollRef.current.left;
2289
+ }, [columns, getScrollableColumnLeft, leftFixedOffsets, metrics, rightFixedOffsets, viewport.width]);
2290
+ const getHeaderLevelAtY = useCallback2((localY, headerY) => {
2291
+ const y = localY - headerY;
2292
+ for (let level = 0; level < headerRowHeights.length; level += 1) {
2293
+ const top = headerRowOffsets[level] ?? 0;
2294
+ if (y >= top && y < top + headerRowHeights[level]) return level;
1956
2295
  }
1957
- return closest;
1958
- }, [columnResizable, columns, getDisplayedColumnLeft, metrics]);
1959
- const findResizeNeighbor = useCallback2((columnIndex) => {
1960
- const resizedColumn = columns[columnIndex];
1961
- for (let index = columnIndex + 1; index < columns.length; index += 1) {
1962
- const column = columns[index];
1963
- if (column.rowSelection || column.rowDragHandle || column.rowNumber) continue;
1964
- if (resizedColumn?.fixed && column.fixed !== resizedColumn.fixed) return -1;
1965
- return index;
1966
- }
1967
- return -1;
1968
- }, [columns]);
2296
+ return headerDepth - 1;
2297
+ }, [headerDepth, headerRowHeights, headerRowOffsets]);
2298
+ const resizeColumn = useCallback2((columnKey, width2) => {
2299
+ setResizedColumnWidths((current) => current[columnKey] === width2 ? current : { ...current, [columnKey]: width2 });
2300
+ onColumnResize?.(columnKey, width2);
2301
+ }, [onColumnResize]);
1969
2302
  const locateHeaderAction = useCallback2((clientX, columnIndex) => {
1970
2303
  if (columnIndex < 0 || !canvasRef.current) return null;
1971
2304
  const column = columns[columnIndex];
@@ -2000,11 +2333,70 @@ function Table({
2000
2333
  const columnRight = getDisplayedColumnLeft(columnIndex) + metrics[columnIndex].width;
2001
2334
  return localX >= columnRight - 16 && localX < columnRight - 5;
2002
2335
  }, [columnDraggable, columns, getDisplayedColumnLeft, measureHeaderTitleWidth, metrics]);
2003
- const setDragGuide = useCallback2((x) => {
2336
+ const getHeaderCellLeft = useCallback2((cell) => {
2337
+ const startMetric = metrics[cell.startIndex];
2338
+ const fixedSide = columns[cell.startIndex]?.fixed;
2339
+ if (!startMetric) return 0;
2340
+ 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;
2341
+ }, [columns, getScrollableColumnLeft, leftFixedOffsets, metrics, rightFixedOffsets, viewport.width]);
2342
+ const getHeaderCellWidth = useCallback2((cell) => cell.leaf ? metrics[cell.startIndex]?.width ?? 0 : getCellDisplayWidth(cell.startIndex, cell.endIndex - cell.startIndex + 1), [getCellDisplayWidth, metrics]);
2343
+ const locateHeaderResizeHit = useCallback2((clientX, clientY) => {
2344
+ if (!columnResizable || !canvasRef.current) return null;
2345
+ const rect = canvasRef.current.getBoundingClientRect();
2346
+ const localX = clientX - rect.left;
2347
+ const localY = clientY - rect.top;
2348
+ const headerY = fixedHeader ? 0 : -scrollRef.current.top;
2349
+ if (localY < headerY || localY >= headerY + headerHeight) return null;
2350
+ const level = getHeaderLevelAtY(localY, headerY);
2351
+ let closest = null;
2352
+ let distance = 6;
2353
+ for (const cell of headerCells) {
2354
+ if (cell.level !== level) continue;
2355
+ if (columns[cell.startIndex]?.rowSelection || columns[cell.startIndex]?.rowDragHandle || columns[cell.startIndex]?.rowNumber) continue;
2356
+ const fixedSide = columns[cell.startIndex]?.fixed;
2357
+ const edge = fixedSide === "right" && cell.leaf ? "left" : "right";
2358
+ const edgeX = getHeaderCellLeft(cell) + (edge === "right" ? getHeaderCellWidth(cell) : 0);
2359
+ const nextDistance = Math.abs(localX - edgeX);
2360
+ if (nextDistance < distance) {
2361
+ distance = nextDistance;
2362
+ closest = { cell, edge };
2363
+ }
2364
+ }
2365
+ return closest;
2366
+ }, [columnResizable, columns, fixedHeader, getHeaderCellLeft, getHeaderCellWidth, getHeaderLevelAtY, headerCells, headerHeight]);
2367
+ const locateHeaderResizeCell = useCallback2((clientX, clientY) => locateHeaderResizeHit(clientX, clientY)?.cell ?? null, [locateHeaderResizeHit]);
2368
+ const locateHeaderCellDragHandle = useCallback2((clientX, cell) => {
2369
+ if (!columnDraggable || !canvasRef.current) return false;
2370
+ if (cell.leaf) return locateHeaderDragHandle(clientX, cell.startIndex);
2371
+ const localX = clientX - canvasRef.current.getBoundingClientRect().left;
2372
+ const right = getHeaderCellLeft(cell) + getHeaderCellWidth(cell);
2373
+ return localX >= right - 16 && localX < right - 5;
2374
+ }, [columnDraggable, getHeaderCellLeft, getHeaderCellWidth, locateHeaderDragHandle]);
2375
+ const locateHeaderCell = useCallback2((clientX, clientY) => {
2376
+ const canvas = canvasRef.current;
2377
+ if (!canvas) return null;
2378
+ const rect = canvas.getBoundingClientRect();
2379
+ const headerY = fixedHeader ? 0 : -scrollRef.current.top;
2380
+ const localX = clientX - rect.left;
2381
+ const localY = clientY - rect.top;
2382
+ if (localY < headerY || localY >= headerY + headerHeight) return null;
2383
+ const level = getHeaderLevelAtY(localY, headerY);
2384
+ for (const cell of headerCells) {
2385
+ if (cell.level !== level) continue;
2386
+ const left = getHeaderCellLeft(cell);
2387
+ const width2 = getHeaderCellWidth(cell);
2388
+ if (localX >= left && localX < left + width2) return cell;
2389
+ }
2390
+ return null;
2391
+ }, [fixedHeader, getHeaderCellLeft, getHeaderCellWidth, getHeaderLevelAtY, headerCells, headerHeight]);
2392
+ const setDragGuide = useCallback2((x, top = 0) => {
2004
2393
  const guide = dragGuideRef.current;
2005
2394
  if (!guide) return;
2006
2395
  guide.style.display = x === null ? "none" : "block";
2007
- if (x !== null) guide.style.transform = `translateX(${Math.round(x)}px)`;
2396
+ if (x !== null) {
2397
+ guide.style.top = `${top}px`;
2398
+ guide.style.transform = `translateX(${Math.round(x)}px)`;
2399
+ }
2008
2400
  }, []);
2009
2401
  const setRowDragGuide = useCallback2((y) => {
2010
2402
  const guide = rowDragGuideRef.current;
@@ -2022,6 +2414,15 @@ function Table({
2022
2414
  useEffect2(() => {
2023
2415
  const canvas = canvasRef.current;
2024
2416
  if (!canvas || !columnDraggable && !rowDraggable) return;
2417
+ const getColumnDropIndex = (sourceIndex, targetIndex, edge) => {
2418
+ if (edge === "right") return targetIndex;
2419
+ const destinationIndex = targetIndex - (sourceIndex < targetIndex ? 1 : 0);
2420
+ return Math.max(0, Math.min(columns.length - 1, destinationIndex));
2421
+ };
2422
+ const getColumnDropPlacement = (sourceIndex, targetIndex, edge) => {
2423
+ if (edge !== "right") return "before";
2424
+ return targetIndex < sourceIndex ? "before" : "after";
2425
+ };
2025
2426
  const getHeaderColumn = (clientX, clientY) => {
2026
2427
  const rect = canvas.getBoundingClientRect();
2027
2428
  const headerY = fixedHeader ? 0 : -scrollRef.current.top;
@@ -2029,22 +2430,35 @@ function Table({
2029
2430
  if (localY < headerY || localY >= headerY + headerHeight) return -1;
2030
2431
  return locateColumn(clientX);
2031
2432
  };
2433
+ const getHeaderDragCell = (clientX, clientY) => {
2434
+ const cell = locateHeaderCell(clientX, clientY);
2435
+ if (!cell) return null;
2436
+ return locateHeaderCellDragHandle(clientX, cell) && !locateHeaderResizeCell(clientX, clientY) ? cell : null;
2437
+ };
2032
2438
  const cleanupDrag = draggable({
2033
2439
  element: canvas,
2034
2440
  canDrag: ({ input }) => {
2035
- const index = getHeaderColumn(input.clientX, input.clientY);
2036
- if (columnDraggable && index >= 0) return locateHeaderDragHandle(input.clientX, index) && locateResizeHandle(input.clientX) < 0;
2441
+ const headerDragCell = columnDraggable ? getHeaderDragCell(input.clientX, input.clientY) : null;
2442
+ if (headerDragCell) return true;
2037
2443
  const columnIndex = locateColumn(input.clientX);
2038
2444
  return rowDraggable && columnIndex >= 0 && Boolean(columns[columnIndex].rowDragHandle) && locateRow(input.clientY) >= 0;
2039
2445
  },
2040
2446
  getInitialData: ({ input }) => {
2041
- const headerColumn = getHeaderColumn(input.clientX, input.clientY);
2042
- if (headerColumn < 0) {
2447
+ const headerDragCell = columnDraggable ? getHeaderDragCell(input.clientX, input.clientY) : null;
2448
+ if (!headerDragCell) {
2043
2449
  return { type: "table-row", sourceIndex: locateRow(input.clientY) };
2044
2450
  }
2045
- const sourceIndex = headerColumn;
2451
+ const sourceIndex = headerDragCell.startIndex;
2046
2452
  columnDragPreviewRef.current = { sourceIndex, targetIndex: sourceIndex };
2047
- return { type: "table-column", sourceIndex };
2453
+ return {
2454
+ type: "table-column",
2455
+ sourceIndex,
2456
+ sourceKey: headerDragCell.key,
2457
+ parentKey: headerDragCell.parentKey,
2458
+ headerLevel: headerDragCell.level,
2459
+ headerKind: headerDragCell.leaf ? "column" : "group",
2460
+ fixed: columns[headerDragCell.startIndex]?.fixed
2461
+ };
2048
2462
  },
2049
2463
  onGenerateDragPreview: ({ nativeSetDragImage, source }) => {
2050
2464
  const sourceIndex = Number(source.data.sourceIndex);
@@ -2147,31 +2561,31 @@ function Table({
2147
2561
  return;
2148
2562
  }
2149
2563
  const targetIndex = Number(target?.targetIndex);
2150
- if (!Number.isInteger(targetIndex) || columns[targetIndex]?.fixed !== columns[sourceIndex]?.fixed) {
2564
+ if (!Number.isInteger(targetIndex) || target?.headerKind !== source.data.headerKind || target?.parentKey !== source.data.parentKey || target?.fixed !== source.data.fixed) {
2151
2565
  setDragGuide(null);
2152
2566
  return;
2153
2567
  }
2154
2568
  const edge = target?.columnEdge;
2155
- let destinationIndex = targetIndex + (edge === "right" ? 1 : 0);
2156
- if (sourceIndex < destinationIndex) destinationIndex -= 1;
2157
- destinationIndex = Math.max(0, Math.min(columns.length - 1, destinationIndex));
2569
+ const destinationIndex = getColumnDropIndex(sourceIndex, targetIndex, edge);
2158
2570
  columnDragPreviewRef.current = { sourceIndex, targetIndex: destinationIndex };
2159
- const targetLeft = getDisplayedColumnLeft(targetIndex);
2160
- setDragGuide(edge === "right" ? targetLeft + metrics[targetIndex].width : targetLeft);
2571
+ const targetLeft = Number(target?.targetLeft);
2572
+ const targetWidth = Number(target?.targetWidth);
2573
+ const guideTop = headerRowOffsets[Number(source.data.headerLevel)] ?? 0;
2574
+ setDragGuide(edge === "right" ? targetLeft + targetWidth : targetLeft, guideTop);
2161
2575
  },
2162
2576
  onDrop: ({ source, location }) => {
2163
2577
  const target = location.current.dropTargets[0]?.data;
2164
2578
  const sourceIndex = Number(source.data.sourceIndex);
2165
2579
  if (source.data.type === "table-row") {
2166
- const rawTargetIndex = Number(target?.targetIndex);
2167
- let targetIndex2 = rawTargetIndex + (target?.rowEdge === "bottom" ? 1 : 0);
2580
+ const rawTargetIndex2 = Number(target?.targetIndex);
2581
+ let targetIndex2 = rawTargetIndex2 + (target?.rowEdge === "bottom" ? 1 : 0);
2168
2582
  if (sourceIndex < targetIndex2) targetIndex2 -= 1;
2169
2583
  targetIndex2 = Math.max(0, Math.min(rows.length - 1, targetIndex2));
2170
2584
  scrollerRef.current?.classList.add("rvg-row-drop-settling");
2171
2585
  setRowDragPreview(null);
2172
2586
  setRowDragGuide(null);
2173
2587
  canvas.style.cursor = "default";
2174
- if (Number.isInteger(rawTargetIndex) && sourceIndex !== targetIndex2) onRowOrderChange?.(sourceIndex, targetIndex2);
2588
+ if (Number.isInteger(rawTargetIndex2) && sourceIndex !== targetIndex2) onRowOrderChange?.(sourceIndex, targetIndex2);
2175
2589
  requestAnimationFrame(() => {
2176
2590
  requestAnimationFrame(() => scrollerRef.current?.classList.remove("rvg-row-drop-settling"));
2177
2591
  });
@@ -2180,12 +2594,23 @@ function Table({
2180
2594
  }, 0);
2181
2595
  return;
2182
2596
  }
2183
- const targetIndex = columnDragPreviewRef.current?.targetIndex ?? Number(target?.targetIndex);
2597
+ const rawTargetIndex = Number(target?.targetIndex);
2598
+ const targetIndex = columnDragPreviewRef.current?.targetIndex ?? (Number.isInteger(rawTargetIndex) ? getColumnDropIndex(sourceIndex, rawTargetIndex, target?.columnEdge) : rawTargetIndex);
2184
2599
  canvas.style.cursor = "default";
2185
2600
  setDragGuide(null);
2186
2601
  columnDragPreviewRef.current = null;
2187
- if (Number.isInteger(targetIndex) && sourceIndex !== targetIndex && columns[targetIndex]?.fixed === columns[sourceIndex]?.fixed) {
2188
- onColumnOrderChange?.(sourceIndex - utilityColumnCount, targetIndex - utilityColumnCount);
2602
+ if (Number.isInteger(rawTargetIndex) && Number.isInteger(targetIndex) && sourceIndex !== targetIndex && target?.headerKind === source.data.headerKind && target?.parentKey === source.data.parentKey && target?.fixed === source.data.fixed) {
2603
+ onColumnOrderChange?.(
2604
+ sourceIndex - utilityColumnCount,
2605
+ targetIndex - utilityColumnCount,
2606
+ {
2607
+ type: source.data.headerKind === "group" ? "group" : "column",
2608
+ sourceKey: String(source.data.sourceKey),
2609
+ targetKey: String(target.targetKey),
2610
+ parentKey: typeof source.data.parentKey === "string" ? source.data.parentKey : void 0,
2611
+ placement: getColumnDropPlacement(sourceIndex, rawTargetIndex, target.columnEdge)
2612
+ }
2613
+ );
2189
2614
  }
2190
2615
  window.setTimeout(() => {
2191
2616
  suppressClickRef.current = false;
@@ -2203,24 +2628,38 @@ function Table({
2203
2628
  const rowEdge = input.clientY - canvas.getBoundingClientRect().top < rowTop + rowHeight / 2 ? "top" : "bottom";
2204
2629
  return attachClosestEdge({ targetIndex: targetIndex2, rowEdge }, { element, input, allowedEdges: ["top", "bottom"] });
2205
2630
  }
2206
- let targetIndex = getHeaderColumn(input.clientX, input.clientY);
2207
- if (targetIndex < 0) return { targetIndex: -1 };
2631
+ const targetCell = locateHeaderCell(input.clientX, input.clientY);
2632
+ if (!targetCell) return { targetIndex: -1 };
2633
+ let targetIndex = targetCell.startIndex;
2634
+ const targetFixed = columns[targetCell.startIndex]?.fixed;
2635
+ const headerKind = targetCell.leaf ? "column" : "group";
2636
+ if (source.data.headerKind !== headerKind || source.data.parentKey !== targetCell.parentKey || source.data.fixed !== targetFixed) return { targetIndex: -1 };
2208
2637
  if (columns[targetIndex].rowSelection || columns[targetIndex].rowDragHandle || columns[targetIndex].rowNumber) {
2209
2638
  const fixedSide = columns[Number(source.data.sourceIndex)]?.fixed;
2210
2639
  targetIndex = columns.findIndex((column, index) => index > targetIndex && column.fixed === fixedSide && !column.rowSelection && !column.rowDragHandle && !column.rowNumber);
2211
2640
  if (targetIndex < 0) return { targetIndex: -1 };
2212
2641
  return { targetIndex, columnEdge: "left" };
2213
2642
  }
2214
- const left = getDisplayedColumnLeft(targetIndex);
2215
- const columnEdge = input.clientX - canvas.getBoundingClientRect().left < left + metrics[targetIndex].width / 2 ? "left" : "right";
2216
- return attachClosestEdge({ targetIndex, columnEdge }, { element, input, allowedEdges: ["left", "right"] });
2643
+ const left = getHeaderCellLeft(targetCell);
2644
+ const targetWidth = targetCell.leaf ? metrics[targetIndex].width : getHeaderCellWidth(targetCell);
2645
+ const columnEdge = input.clientX - canvas.getBoundingClientRect().left < left + targetWidth / 2 ? "left" : "right";
2646
+ return attachClosestEdge({
2647
+ targetIndex,
2648
+ targetKey: targetCell.key,
2649
+ parentKey: targetCell.parentKey,
2650
+ headerKind,
2651
+ fixed: targetFixed,
2652
+ targetLeft: left,
2653
+ targetWidth,
2654
+ columnEdge
2655
+ }, { element, input, allowedEdges: ["left", "right"] });
2217
2656
  }
2218
2657
  });
2219
2658
  return () => {
2220
2659
  cleanupDrag();
2221
2660
  cleanupDrop();
2222
2661
  };
2223
- }, [bodyTop, columnDraggable, columns, fixedHeader, getCellLabel, getDisplayedColumnLeft, getRowKey, headerHeight, locateColumn, locateHeaderDragHandle, locateResizeHandle, locateRow, metrics, onColumnOrderChange, onRowOrderChange, rowDraggable, rowHeight, rows, selectedRowKeySet, setDragGuide, setRowDragGuide, utilityColumnCount]);
2662
+ }, [bodyTop, columnDraggable, columns, fixedHeader, getCellLabel, getDisplayedColumnLeft, getHeaderCellWidth, getRowKey, headerHeight, locateColumn, locateHeaderCell, locateHeaderCellDragHandle, locateHeaderResizeCell, locateRow, metrics, onColumnOrderChange, onRowOrderChange, rowDraggable, rowHeight, rows, selectedRowKeySet, setDragGuide, setRowDragGuide, utilityColumnCount]);
2224
2663
  const locateCell = useCallback2((clientX, clientY) => {
2225
2664
  const canvas = canvasRef.current;
2226
2665
  if (!canvas) return null;
@@ -2327,8 +2766,9 @@ function Table({
2327
2766
  const horizontalFocusGap = 0;
2328
2767
  const verticalFocusGap = 0;
2329
2768
  if (column.fixed === void 0) {
2330
- if (metric.left - nextLeft < fixedWidth) nextLeft = Math.max(0, metric.left - fixedWidth - horizontalFocusGap);
2331
- if (metric.left + cellWidth - nextLeft > viewport.width - rightFixedWidth) nextLeft = metric.left + cellWidth - viewport.width + rightFixedWidth + horizontalFocusGap;
2769
+ const cellLeft = getScrollableColumnLeft(cell.columnIndex);
2770
+ if (cellLeft - nextLeft < fixedWidth) nextLeft = Math.max(0, cellLeft - fixedWidth - horizontalFocusGap);
2771
+ if (cellLeft + cellWidth - nextLeft > viewport.width - rightFixedWidth) nextLeft = cellLeft + cellWidth - viewport.width + rightFixedWidth + horizontalFocusGap;
2332
2772
  }
2333
2773
  const rowTop = (fixedHeader ? 0 : bodyTop) + cell.rowIndex * rowHeight;
2334
2774
  const rowBottom = rowTop + rowHeight * rowSpan;
@@ -2341,7 +2781,7 @@ function Table({
2341
2781
  scroller.scrollTop = nextTop;
2342
2782
  scrollRef.current = { left: scroller.scrollLeft, top: scroller.scrollTop };
2343
2783
  scheduleDraw();
2344
- }, [bodyTop, columns, fixedHeader, fixedWidth, getCellDisplayWidth, getCellSpan, metrics, rightFixedWidth, rowHeight, scheduleDraw, viewport.width]);
2784
+ }, [bodyTop, columns, fixedHeader, fixedWidth, getCellDisplayWidth, getCellSpan, getScrollableColumnLeft, metrics, rightFixedWidth, rowHeight, scheduleDraw, viewport.width]);
2345
2785
  const beginEdit = useCallback2((cell) => {
2346
2786
  const column = columns[cell.columnIndex];
2347
2787
  if (!column?.editable || column.dataIndex === void 0) return;
@@ -2362,8 +2802,9 @@ function Table({
2362
2802
  const horizontalFocusGap = 0;
2363
2803
  const verticalFocusGap = 0;
2364
2804
  if (column.fixed === void 0) {
2365
- if (metric.left - nextLeft < fixedWidth) nextLeft = Math.max(0, metric.left - fixedWidth - horizontalFocusGap);
2366
- if (metric.left + cellWidth - nextLeft > viewport.width - rightFixedWidth) nextLeft = metric.left + cellWidth - viewport.width + rightFixedWidth + horizontalFocusGap;
2805
+ const cellLeft = getScrollableColumnLeft(cell.columnIndex);
2806
+ if (cellLeft - nextLeft < fixedWidth) nextLeft = Math.max(0, cellLeft - fixedWidth - horizontalFocusGap);
2807
+ if (cellLeft + cellWidth - nextLeft > viewport.width - rightFixedWidth) nextLeft = cellLeft + cellWidth - viewport.width + rightFixedWidth + horizontalFocusGap;
2367
2808
  }
2368
2809
  const rowTop = (fixedHeader ? 0 : bodyTop) + cell.rowIndex * rowHeight;
2369
2810
  const rowBottom = rowTop + rowHeight * rowSpan;
@@ -2385,7 +2826,7 @@ function Table({
2385
2826
  return;
2386
2827
  }
2387
2828
  setEditing(cell);
2388
- }, [columns, fixedHeader, fixedWidth, getCellDisplayWidth, getCellSpan, metrics, rightFixedWidth, rowHeight, rows, scheduleDraw, viewport.width]);
2829
+ }, [columns, fixedHeader, fixedWidth, getCellDisplayWidth, getCellSpan, getScrollableColumnLeft, metrics, rightFixedWidth, rowHeight, rows, scheduleDraw, viewport.width]);
2389
2830
  const getCellEditKey = (cell) => `${typeof cell.rowKey}:${String(cell.rowKey)}\0${cell.columnKey}`;
2390
2831
  const markCellEdited = (cell, previousValue, nextValue) => {
2391
2832
  const key = getCellEditKey(cell);
@@ -2516,10 +2957,13 @@ function Table({
2516
2957
  const visibleRowHeight = fixedHeader ? renderHeight - bodyTop : renderHeight;
2517
2958
  if (rowTop < scroller.scrollTop) scroller.scrollTop = rowTop;
2518
2959
  if (rowBottom > scroller.scrollTop + visibleRowHeight) scroller.scrollTop = rowBottom - visibleRowHeight;
2519
- if (columns[columnIndex].fixed === void 0 && metric.left < scroller.scrollLeft + fixedWidth) scroller.scrollLeft = Math.max(0, metric.left - fixedWidth);
2520
- if (columns[columnIndex].fixed === void 0 && metric.left + cellWidth > scroller.scrollLeft + viewport.width - rightFixedWidth) scroller.scrollLeft = metric.left + cellWidth - viewport.width + rightFixedWidth;
2960
+ if (columns[columnIndex].fixed === void 0) {
2961
+ const cellLeft = getScrollableColumnLeft(columnIndex);
2962
+ if (cellLeft < scroller.scrollLeft + fixedWidth) scroller.scrollLeft = Math.max(0, cellLeft - fixedWidth);
2963
+ if (cellLeft + cellWidth > scroller.scrollLeft + viewport.width - rightFixedWidth) scroller.scrollLeft = cellLeft + cellWidth - viewport.width + rightFixedWidth;
2964
+ }
2521
2965
  }
2522
- }, [beginEdit, bodyTop, columns, editing, renderHeight, fixedHeader, fixedWidth, getCellDisplayWidth, getCellSpan, getRowKey, metrics, rightFixedWidth, rowHeight, rows, selection, setSelection, viewport.width]);
2966
+ }, [beginEdit, bodyTop, columns, editing, renderHeight, fixedHeader, fixedWidth, getCellDisplayWidth, getCellSpan, getRowKey, getScrollableColumnLeft, metrics, rightFixedWidth, rowHeight, rows, selection, setSelection, viewport.width]);
2523
2967
  const editorStyle = useMemo(() => {
2524
2968
  if (!editing) return void 0;
2525
2969
  const metric = metrics[editing.columnIndex];
@@ -2527,64 +2971,82 @@ function Table({
2527
2971
  const span = getCellSpan(editing.rowIndex, editing.columnIndex);
2528
2972
  const cellWidth = getCellDisplayWidth(editing.columnIndex, span?.colSpan ?? 1);
2529
2973
  const cellHeight = rowHeight * (span?.rowSpan ?? 1);
2530
- const cellLeft = fixedSide === "left" ? metric.left : fixedSide === "right" ? viewport.width - (rightFixedOffsets.get(editing.columnIndex) ?? 0) - metric.width : metric.left - scrollRef.current.left;
2974
+ 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;
2531
2975
  return {
2532
2976
  left: (fixedSide ? cellLeft : Math.max(cellLeft, fixedWidth)) + 1,
2533
2977
  top: Math.max(fixedHeader ? bodyTop : 0, bodyTop + editing.rowIndex * rowHeight - scrollRef.current.top) + 1,
2534
2978
  width: cellWidth - 2,
2535
2979
  height: cellHeight - 2
2536
2980
  };
2537
- }, [bodyTop, columns, editing, fixedHeader, fixedWidth, getCellDisplayWidth, getCellSpan, metrics, rightFixedOffsets, rowHeight, viewport.width]);
2981
+ }, [bodyTop, columns, editing, fixedHeader, fixedWidth, getCellDisplayWidth, getCellSpan, getScrollableColumnLeft, leftFixedOffsets, metrics, rightFixedOffsets, rowHeight, viewport.width]);
2538
2982
  const handleColumnPointerDown = useCallback2((event) => {
2539
2983
  const rect = event.currentTarget.getBoundingClientRect();
2540
2984
  const localY = event.clientY - rect.top;
2541
2985
  const headerY = fixedHeader ? 0 : -scrollRef.current.top;
2542
2986
  if (localY < headerY || localY >= headerY + headerHeight) return;
2543
- const resizeIndex = locateResizeHandle(event.clientX);
2544
- if (resizeIndex >= 0) {
2987
+ const resizeHit = locateHeaderResizeHit(event.clientX, event.clientY);
2988
+ if (resizeHit) {
2989
+ const { cell: resizeCell, edge: resizeEdge } = resizeHit;
2545
2990
  event.preventDefault();
2546
2991
  event.currentTarget.setPointerCapture(event.pointerId);
2547
- const neighborIndex = findResizeNeighbor(resizeIndex);
2548
- const startGuideX = getDisplayedColumnLeft(resizeIndex) + metrics[resizeIndex].width;
2992
+ const resizeIndex = resizeCell.endIndex;
2993
+ const resizeIndices = resizeCell.leaf ? [resizeIndex] : Array.from({ length: resizeCell.endIndex - resizeCell.startIndex + 1 }, (_, index) => resizeCell.startIndex + index);
2994
+ const startWidths = resizeIndices.map((index) => metrics[index].width);
2995
+ const startGuideX = getHeaderCellLeft(resizeCell) + (resizeEdge === "right" ? getHeaderCellWidth(resizeCell) : 0);
2549
2996
  columnDragRef.current = {
2550
2997
  type: "resize",
2551
2998
  columnIndex: resizeIndex,
2552
2999
  startX: event.clientX,
2553
- startWidth: metrics[resizeIndex].width,
3000
+ startWidth: startWidths.reduce((total, width2) => total + width2, 0),
2554
3001
  startGuideX,
2555
- neighborIndex,
2556
- startNeighborWidth: neighborIndex >= 0 ? metrics[neighborIndex].width : 0
3002
+ guideTop: headerRowOffsets[resizeCell.level] ?? 0,
3003
+ resizeEdge,
3004
+ resizeIndices,
3005
+ startWidths
2557
3006
  };
2558
3007
  suppressClickRef.current = true;
3008
+ setResizeGuideTop(headerRowOffsets[resizeCell.level] ?? 0);
2559
3009
  setResizeGuideX(startGuideX);
2560
3010
  return;
2561
3011
  }
2562
- }, [findResizeNeighbor, fixedHeader, getDisplayedColumnLeft, headerHeight, locateResizeHandle, metrics]);
3012
+ }, [fixedHeader, getHeaderCellLeft, getHeaderCellWidth, headerHeight, headerRowOffsets, locateHeaderResizeHit, metrics]);
2563
3013
  const handleColumnPointerMove = useCallback2((event) => {
2564
3014
  const drag = columnDragRef.current;
2565
3015
  if (!drag) return;
2566
3016
  const column = columns[drag.columnIndex];
2567
3017
  const minWidth = getMinimumColumnWidth(column, columnDraggable);
2568
- let delta = Math.round(event.clientX - drag.startX);
2569
- if (drag.neighborIndex >= 0) {
2570
- const neighbor = columns[drag.neighborIndex];
2571
- const minNeighborWidth = getMinimumColumnWidth(neighbor, columnDraggable);
2572
- delta = Math.max(minWidth - drag.startWidth, Math.min(drag.startNeighborWidth - minNeighborWidth, delta));
2573
- onColumnResize?.(column.key, drag.startWidth + delta);
2574
- onColumnResize?.(neighbor.key, drag.startNeighborWidth - delta);
2575
- setResizeGuideX(drag.startGuideX + delta);
3018
+ const pointerDelta = Math.round(event.clientX - drag.startX);
3019
+ const delta = drag.resizeEdge === "left" ? -pointerDelta : pointerDelta;
3020
+ if (drag.resizeIndices.length > 1) {
3021
+ const minWidths = drag.resizeIndices.map((index) => getMinimumColumnWidth(columns[index], columnDraggable));
3022
+ const minTotalWidth = minWidths.reduce((total, width3) => total + width3, 0);
3023
+ const targetTotalWidth = Math.max(minTotalWidth, drag.startWidth + delta);
3024
+ const ratio = drag.startWidth > 0 ? targetTotalWidth / drag.startWidth : 1;
3025
+ const nextWidths = drag.startWidths.map((width3, index) => Math.max(minWidths[index], Math.round(width3 * ratio)));
3026
+ let remainder = targetTotalWidth - nextWidths.reduce((total, width3) => total + width3, 0);
3027
+ for (let index = nextWidths.length - 1; index >= 0 && remainder !== 0; index -= 1) {
3028
+ const next = nextWidths[index] + (remainder > 0 ? 1 : -1);
3029
+ if (next < minWidths[index]) continue;
3030
+ nextWidths[index] = next;
3031
+ remainder += remainder > 0 ? -1 : 1;
3032
+ }
3033
+ drag.resizeIndices.forEach((columnIndex, index) => {
3034
+ resizeColumn(columns[columnIndex].key, nextWidths[index]);
3035
+ });
3036
+ setResizeGuideX(drag.startGuideX + (drag.resizeEdge === "left" ? drag.startWidth - targetTotalWidth : targetTotalWidth - drag.startWidth));
2576
3037
  return;
2577
3038
  }
2578
3039
  const width2 = Math.max(minWidth, Math.round(drag.startWidth + delta));
2579
- onColumnResize?.(column.key, width2);
2580
- setResizeGuideX(drag.startGuideX + width2 - drag.startWidth);
2581
- }, [columnDraggable, columns, onColumnResize]);
3040
+ resizeColumn(column.key, width2);
3041
+ setResizeGuideX(drag.startGuideX + (drag.resizeEdge === "left" ? drag.startWidth - width2 : width2 - drag.startWidth));
3042
+ }, [columnDraggable, columns, resizeColumn]);
2582
3043
  const handleColumnPointerUp = useCallback2((event) => {
2583
3044
  const drag = columnDragRef.current;
2584
3045
  if (!drag) return;
2585
3046
  if (event.currentTarget.hasPointerCapture(event.pointerId)) event.currentTarget.releasePointerCapture(event.pointerId);
2586
3047
  columnDragRef.current = null;
2587
3048
  setResizeGuideX(null);
3049
+ setResizeGuideTop(0);
2588
3050
  }, []);
2589
3051
  const queueSortStateChange = useCallback2((next) => {
2590
3052
  setDisplaySortState(next);
@@ -2597,7 +3059,7 @@ function Table({
2597
3059
  const renderHeaderIcons = (columnIndex, left) => {
2598
3060
  const column = columns[columnIndex];
2599
3061
  if (column.rowSelection || column.rowDragHandle || column.rowNumber) return null;
2600
- const top = (headerHeight - 14) / 2;
3062
+ const top = headerLeafTop + (headerLeafHeight - 14) / 2;
2601
3063
  const visibleActions = getVisibleHeaderActions(column, metrics[columnIndex].width, columnDraggable, measureHeaderTitleWidth(columnIndex));
2602
3064
  let right = left + metrics[columnIndex].width - (visibleActions.drag ? 18 : 4);
2603
3065
  const icons = [];
@@ -2623,44 +3085,87 @@ function Table({
2623
3085
  }
2624
3086
  return icons;
2625
3087
  };
2626
- const headerTooltip = (() => {
2627
- if (!visibleHeaderTooltip) return null;
2628
- const { columnIndex, action } = visibleHeaderTooltip;
2629
- const column = columns[columnIndex];
3088
+ const createHeaderTooltip = useCallback2((action, columnIndex, cell) => {
3089
+ const column = cell?.column ?? columns[columnIndex];
2630
3090
  if (!column) return null;
2631
- const columnRight = getDisplayedColumnLeft(columnIndex) + metrics[columnIndex].width;
2632
- const visibleActions = getVisibleHeaderActions(column, metrics[columnIndex].width, columnDraggable, measureHeaderTitleWidth(columnIndex));
2633
- let anchorX = columnRight - (visibleActions.drag ? 18 : 4);
2634
- let anchorLeft = anchorX;
2635
- let label;
3091
+ const cellLeft = cell ? getHeaderCellLeft(cell) : getDisplayedColumnLeft(columnIndex);
3092
+ const cellWidth = cell ? getHeaderCellWidth(cell) : metrics[columnIndex]?.width ?? 0;
3093
+ const level = cell?.level ?? headerDepth - 1;
3094
+ const rowTop = (fixedHeader ? 0 : -scrollPosition.top) + (headerRowOffsets[level] ?? 0);
3095
+ const rowHeight2 = headerRowHeights[level] ?? baseHeaderHeight;
3096
+ let anchorX = cellLeft + cellWidth / 2;
3097
+ let anchorLeft = cellLeft;
3098
+ let label = column.title;
2636
3099
  if (action === "title") {
2637
- const titleWidth = measureHeaderTitleWidth(columnIndex);
2638
- const actionWidth = (Number(visibleActions.drag) + Number(visibleActions.filter) + Number(visibleActions.sort)) * 18;
2639
- const contentWidth2 = Math.max(0, metrics[columnIndex].width - actionWidth);
2640
- const textWidth = Math.min(titleWidth, Math.max(0, contentWidth2 - 20));
2641
- const columnLeft = getDisplayedColumnLeft(columnIndex);
2642
- anchorX = column.align === "right" ? columnLeft + contentWidth2 - 10 : column.align === "center" ? columnLeft + metrics[columnIndex].width / 2 + textWidth / 2 : columnLeft + 10 + textWidth;
2643
- anchorLeft = column.align === "right" ? anchorX - textWidth : column.align === "center" ? columnLeft + metrics[columnIndex].width / 2 - textWidth / 2 : columnLeft + 10;
2644
- label = column.title;
3100
+ const canvas = canvasRef.current;
3101
+ const context = canvas?.getContext("2d");
3102
+ if (context) context.font = "600 13px Inter, ui-sans-serif, system-ui, sans-serif";
3103
+ const titleWidth = context?.measureText(column.title).width ?? column.title.length * 13;
3104
+ const visibleActions = cell?.leaf || !cell ? getVisibleHeaderActions(column, cellWidth, columnDraggable, titleWidth) : null;
3105
+ const actionWidth = visibleActions ? (Number(visibleActions.drag) + Number(visibleActions.filter) + Number(visibleActions.sort)) * 18 : columnDraggable ? 18 : 0;
3106
+ const contentWidth2 = Math.max(0, cellWidth - actionWidth);
3107
+ const maxTextWidth = Math.max(0, contentWidth2 - 20);
3108
+ const textWidth = Math.min(titleWidth, maxTextWidth);
3109
+ if (column.align === "right") {
3110
+ anchorX = cellLeft + contentWidth2 - 10;
3111
+ anchorLeft = anchorX - textWidth;
3112
+ } else if (column.align === "center") {
3113
+ anchorX = cellLeft + contentWidth2 / 2 + textWidth / 2;
3114
+ anchorLeft = cellLeft + contentWidth2 / 2 - textWidth / 2;
3115
+ } else {
3116
+ anchorLeft = cellLeft + 10;
3117
+ anchorX = anchorLeft + textWidth;
3118
+ }
2645
3119
  } else if (action === "drag") {
2646
- anchorX = columnRight;
3120
+ anchorX = cellLeft + cellWidth;
2647
3121
  anchorLeft = anchorX;
2648
3122
  label = labels.dragColumn;
2649
3123
  } else if (action === "sort") {
2650
3124
  const direction = displaySortState?.columnKey === column.key ? displaySortState.direction : null;
2651
3125
  label = direction === "asc" ? labels.sortAsc : direction === "desc" ? labels.sortDesc : labels.sortBoth;
2652
- } else {
2653
- if (visibleActions.sort) anchorX -= 18;
3126
+ anchorX = cellLeft + cellWidth - 22;
3127
+ anchorLeft = anchorX;
3128
+ } else if (action === "filter") {
2654
3129
  const value = filterValues[column.key];
2655
3130
  label = value ? labels.filterWithValue(value) : labels.filter;
3131
+ anchorX = cellLeft + cellWidth - 40;
3132
+ anchorLeft = anchorX;
2656
3133
  }
2657
3134
  const placement = anchorX + 328 > viewport.width ? "left" : "right";
2658
3135
  return {
3136
+ key: `${action}:${cell ? `cell:${cell.key}:${cell.level}` : `column:${column.key}`}:${Math.round(anchorX)}:${Math.round(rowTop)}`,
3137
+ columnIndex,
3138
+ action,
2659
3139
  left: placement === "right" ? anchorX + 8 : Math.max(8, anchorLeft - 8),
2660
- top: (fixedHeader ? 0 : -scrollPosition.top) + headerHeight / 2,
3140
+ top: rowTop + rowHeight2 / 2,
2661
3141
  label,
2662
3142
  placement
2663
3143
  };
3144
+ }, [baseHeaderHeight, columnDraggable, columns, displaySortState, filterValues, fixedHeader, getDisplayedColumnLeft, getHeaderCellLeft, getHeaderCellWidth, headerDepth, headerRowHeights, headerRowOffsets, labels, metrics, scrollPosition.top, viewport.width]);
3145
+ const isPointerOnHeaderTitle = useCallback2((clientX, clientY, cell) => {
3146
+ const canvas = canvasRef.current;
3147
+ if (!canvas) return false;
3148
+ const rect = canvas.getBoundingClientRect();
3149
+ const localX = clientX - rect.left;
3150
+ const localY = clientY - rect.top;
3151
+ const column = cell.column;
3152
+ const cellLeft = getHeaderCellLeft(cell);
3153
+ const cellWidth = getHeaderCellWidth(cell);
3154
+ const rowTop = (fixedHeader ? 0 : -scrollRef.current.top) + (headerRowOffsets[cell.level] ?? 0);
3155
+ const context = canvas.getContext("2d");
3156
+ if (context) context.font = "600 13px Inter, ui-sans-serif, system-ui, sans-serif";
3157
+ const titleWidth = context?.measureText(column.title).width ?? column.title.length * 13;
3158
+ const visibleActions = cell.leaf ? getVisibleHeaderActions(column, cellWidth, columnDraggable, titleWidth) : null;
3159
+ const actionWidth = visibleActions ? (Number(visibleActions.drag) + Number(visibleActions.filter) + Number(visibleActions.sort)) * 18 : columnDraggable ? 18 : 0;
3160
+ const contentWidth2 = Math.max(0, cellWidth - actionWidth);
3161
+ const textWidth = Math.min(titleWidth, Math.max(0, contentWidth2 - 20));
3162
+ const textLeft = column.align === "right" ? cellLeft + contentWidth2 - 10 - textWidth : column.align === "center" ? cellLeft + contentWidth2 / 2 - textWidth / 2 : cellLeft + 10;
3163
+ const textTop = rowTop + 8;
3164
+ return localX >= textLeft - 4 && localX <= textLeft + textWidth + 4 && localY >= textTop - 2 && localY <= textTop + 18;
3165
+ }, [columnDraggable, fixedHeader, getHeaderCellLeft, getHeaderCellWidth, headerRowOffsets]);
3166
+ const headerTooltip = (() => {
3167
+ if (!visibleHeaderTooltip) return null;
3168
+ return visibleHeaderTooltip;
2664
3169
  })();
2665
3170
  const getRangeBounds = () => selectionRange ? {
2666
3171
  rowStart: Math.min(selectionRange.anchor.rowIndex, selectionRange.focus.rowIndex),
@@ -2719,7 +3224,7 @@ function Table({
2719
3224
  }, 1800);
2720
3225
  };
2721
3226
  const showHeaderTooltipAfterDelay = (next) => {
2722
- const nextKey = next ? `${next.columnIndex}:${next.action}` : "";
3227
+ const nextKey = next ? next.key : "";
2723
3228
  if (nextKey && pendingHeaderTooltipKeyRef.current === nextKey) return;
2724
3229
  pendingHeaderTooltipKeyRef.current = nextKey;
2725
3230
  if (headerTooltipTimerRef.current !== null) clearTimeout(headerTooltipTimerRef.current);
@@ -2967,12 +3472,14 @@ function Table({
2967
3472
  const renderHeaderTitle = (columnIndex, left) => {
2968
3473
  const column = columns[columnIndex];
2969
3474
  const currentScrollTop = scrollRef.current.top;
3475
+ const headerTop = fixedHeader ? 0 : -currentScrollTop;
3476
+ const leafTop = headerTop + headerLeafTop;
2970
3477
  if (column.rowNumber) {
2971
3478
  return /* @__PURE__ */ jsx7(
2972
3479
  "div",
2973
3480
  {
2974
3481
  className: "rvg-header-title",
2975
- style: { left, top: fixedHeader ? 0 : -currentScrollTop, width: metrics[columnIndex].width, height: headerHeight, justifyContent: "center", padding: 0, textAlign: "center" },
3482
+ style: { left, top: headerTop, width: metrics[columnIndex].width, height: headerHeight, justifyContent: "center", padding: 0, textAlign: "center" },
2976
3483
  children: /* @__PURE__ */ jsx7("span", { children: column.title })
2977
3484
  },
2978
3485
  column.key
@@ -2986,7 +3493,7 @@ function Table({
2986
3493
  "div",
2987
3494
  {
2988
3495
  className: "rvg-header-selection-icon",
2989
- style: { left, top: fixedHeader ? 0 : -currentScrollTop, width: metrics[columnIndex].width, height: headerHeight },
3496
+ style: { left, top: headerTop, width: metrics[columnIndex].width, height: headerHeight },
2990
3497
  children: /* @__PURE__ */ jsx7(SelectionIcon, { checked: allSelected, indeterminate })
2991
3498
  },
2992
3499
  column.key
@@ -2999,11 +3506,13 @@ function Table({
2999
3506
  "div",
3000
3507
  {
3001
3508
  className: "rvg-header-title is-custom",
3509
+ "data-level": headerDepth - 1,
3510
+ "data-measure-key": column.key,
3002
3511
  style: {
3003
3512
  left,
3004
- top: fixedHeader ? 0 : -currentScrollTop,
3513
+ top: leafTop,
3005
3514
  width: Math.max(0, metrics[columnIndex].width - actionWidth),
3006
- height: headerHeight,
3515
+ height: headerLeafHeight,
3007
3516
  justifyContent: column.align === "right" ? "flex-end" : column.align === "center" ? "center" : "flex-start",
3008
3517
  textAlign: column.align === "right" ? "right" : column.align === "center" ? "center" : "left"
3009
3518
  },
@@ -3011,8 +3520,10 @@ function Table({
3011
3520
  "span",
3012
3521
  {
3013
3522
  ref: (node) => {
3014
- if (node) customHeaderContentRefs.current.set(column.key, node);
3015
- else customHeaderContentRefs.current.delete(column.key);
3523
+ if (node) {
3524
+ customHeaderContentRefs.current.set(column.key, node);
3525
+ requestAnimationFrame(measureCustomHeaderHeight);
3526
+ } else customHeaderContentRefs.current.delete(column.key);
3016
3527
  },
3017
3528
  children: column.renderHeader(column)
3018
3529
  }
@@ -3023,6 +3534,61 @@ function Table({
3023
3534
  }
3024
3535
  return null;
3025
3536
  };
3537
+ const renderGroupedHeaderCell = (cell) => {
3538
+ const startMetric = metrics[cell.startIndex];
3539
+ const endMetric = metrics[cell.endIndex];
3540
+ if (!startMetric || !endMetric) return null;
3541
+ const column = cell.column;
3542
+ const fixedSide = columns[cell.startIndex]?.fixed;
3543
+ const currentScrollTop = scrollRef.current.top;
3544
+ const rowHeightForLevel = headerRowHeights[cell.level] ?? baseHeaderHeight;
3545
+ const headerTop = (fixedHeader ? 0 : -currentScrollTop) + (headerRowOffsets[cell.level] ?? 0);
3546
+ const left = fixedSide === "left" ? leftFixedOffsets.get(cell.startIndex) ?? 0 : fixedSide === "right" ? viewport.width - (rightFixedOffsets.get(cell.startIndex) ?? 0) - startMetric.width : getScrollableColumnLeft(cell.startIndex);
3547
+ const width2 = cell.leaf ? startMetric.width : getCellDisplayWidth(cell.startIndex, cell.endIndex - cell.startIndex + 1);
3548
+ const visibleActions = cell.leaf ? getVisibleHeaderActions(column, startMetric.width, columnDraggable, measureHeaderTitleWidth(cell.startIndex)) : null;
3549
+ const actionWidth = visibleActions ? (Number(visibleActions.drag) + Number(visibleActions.filter) + Number(visibleActions.sort)) * 18 : columnDraggable && !cell.leaf ? 18 : 0;
3550
+ return /* @__PURE__ */ jsxs7(
3551
+ "div",
3552
+ {
3553
+ className: `rvg-header-title rvg-header-group${column.renderHeader ? " is-custom" : ""}${cell.leaf ? " is-leaf" : ""}`,
3554
+ "data-level": cell.level,
3555
+ "data-measure-key": `group:${cell.key}:${cell.level}`,
3556
+ style: {
3557
+ left,
3558
+ top: headerTop,
3559
+ width: width2,
3560
+ height: rowHeightForLevel,
3561
+ paddingRight: cell.leaf ? actionWidth + 10 : 10,
3562
+ justifyContent: column.align === "right" ? "flex-end" : column.align === "center" ? "center" : "flex-start",
3563
+ textAlign: column.align === "right" ? "right" : column.align === "center" ? "center" : "left"
3564
+ },
3565
+ children: [
3566
+ /* @__PURE__ */ jsx7(
3567
+ "span",
3568
+ {
3569
+ ref: (node) => {
3570
+ if (!column.renderHeader) return;
3571
+ const key = `group:${cell.key}:${cell.level}`;
3572
+ if (node) {
3573
+ customHeaderContentRefs.current.set(key, node);
3574
+ requestAnimationFrame(measureCustomHeaderHeight);
3575
+ } else customHeaderContentRefs.current.delete(key);
3576
+ },
3577
+ children: column.renderHeader ? column.renderHeader(column) : column.title
3578
+ }
3579
+ ),
3580
+ !cell.leaf && columnDraggable && /* @__PURE__ */ jsx7(
3581
+ HeaderDragIcon,
3582
+ {
3583
+ className: `rvg-header-icon rvg-header-drag-icon${hoveredHeaderAction?.columnIndex === cell.startIndex && hoveredHeaderAction.action === "drag" ? " is-hovered" : ""}`,
3584
+ style: { left: width2 - 16, top: (rowHeightForLevel - 14) / 2 }
3585
+ }
3586
+ )
3587
+ ]
3588
+ },
3589
+ `group:${cell.key}:${cell.level}`
3590
+ );
3591
+ };
3026
3592
  const rangeHandlePosition = (() => {
3027
3593
  if (!rangeSelection) return null;
3028
3594
  const bounds = getRangeBounds();
@@ -3214,15 +3780,19 @@ function Table({
3214
3780
  const headerY = fixedHeader ? 0 : -scrollRef.current.top;
3215
3781
  const columnIndex = locateColumn(event.clientX);
3216
3782
  const inHeader = localY >= headerY && localY < headerY + headerHeight;
3217
- const hoveredAction = inHeader ? locateHeaderAction(event.clientX, columnIndex) : null;
3218
- const hoveredDragHandle = inHeader && locateHeaderDragHandle(event.clientX, columnIndex);
3219
- const hoveredTitle = inHeader && !hoveredAction && !hoveredDragHandle && columnIndex >= 0 && Boolean(columns[columnIndex]?.title) && (tooltipConfig.header || isHeaderTitleTruncated(columnIndex));
3220
- const nextHover = hoveredAction ? { columnIndex, action: hoveredAction } : hoveredDragHandle ? { columnIndex, action: "drag" } : hoveredTitle ? { columnIndex, action: "title" } : null;
3783
+ const inLeafHeader = inHeader && localY >= headerY + headerLeafTop;
3784
+ const hoveredHeaderCell = inHeader ? locateHeaderCell(event.clientX, event.clientY) : null;
3785
+ const hoveredAction = inLeafHeader ? locateHeaderAction(event.clientX, columnIndex) : null;
3786
+ const hoveredDragHandle = Boolean(hoveredHeaderCell && locateHeaderCellDragHandle(event.clientX, hoveredHeaderCell));
3787
+ const hoveredDragColumnIndex = hoveredHeaderCell?.startIndex ?? columnIndex;
3788
+ const hoveredTitle = inHeader && !hoveredAction && !hoveredDragHandle && hoveredHeaderCell && Boolean(hoveredHeaderCell.column.title) && isPointerOnHeaderTitle(event.clientX, event.clientY, hoveredHeaderCell) && (tooltipConfig.header || hoveredHeaderCell.leaf && isHeaderTitleTruncated(hoveredHeaderCell.startIndex));
3789
+ const nextHover = hoveredAction ? { columnIndex, action: hoveredAction } : hoveredDragHandle ? { columnIndex: hoveredDragColumnIndex, action: "drag" } : hoveredTitle ? { columnIndex: hoveredHeaderCell.startIndex, action: "title" } : null;
3790
+ const nextTooltip = hoveredAction && hoveredHeaderCell ? createHeaderTooltip(hoveredAction, columnIndex, hoveredHeaderCell) : hoveredDragHandle && hoveredHeaderCell ? createHeaderTooltip("drag", hoveredDragColumnIndex, hoveredHeaderCell) : hoveredTitle && hoveredHeaderCell ? createHeaderTooltip("title", hoveredHeaderCell.startIndex, hoveredHeaderCell) : null;
3221
3791
  const previousHover = hoveredHeaderActionRef.current;
3222
3792
  if (previousHover?.columnIndex !== nextHover?.columnIndex || previousHover?.action !== nextHover?.action) {
3223
3793
  hoveredHeaderActionRef.current = nextHover;
3224
3794
  setHoveredHeaderAction(nextHover);
3225
- showHeaderTooltipAfterDelay(nextHover);
3795
+ showHeaderTooltipAfterDelay(nextTooltip);
3226
3796
  scheduleDraw();
3227
3797
  }
3228
3798
  if (inHeader) {
@@ -3246,8 +3816,8 @@ function Table({
3246
3816
  if (annotation?.content.trim()) {
3247
3817
  const cellLeft = getDisplayedColumnLeft(cell.columnIndex);
3248
3818
  const cellTop = bodyTop + cell.rowIndex * rowHeight - scrollRef.current.top;
3249
- 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 };
3250
- showCellTooltip(nextTooltip);
3819
+ 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 };
3820
+ showCellTooltip(nextTooltip2);
3251
3821
  } else {
3252
3822
  const label = getCellLabel(cell.rowIndex, cell.columnIndex);
3253
3823
  const context = event.currentTarget.getContext("2d");
@@ -3274,7 +3844,7 @@ function Table({
3274
3844
  showCellTooltipAfterDelay(null);
3275
3845
  } else {
3276
3846
  const placement = textRight + 330 > viewport.width ? "left" : "right";
3277
- const nextTooltip = {
3847
+ const nextTooltip2 = {
3278
3848
  rowIndex: cell.rowIndex,
3279
3849
  columnIndex: cell.columnIndex,
3280
3850
  left: placement === "right" ? textRight + 16 : Math.max(8, textLeft - 16),
@@ -3282,13 +3852,13 @@ function Table({
3282
3852
  label,
3283
3853
  placement
3284
3854
  };
3285
- showCellTooltipAfterDelay(nextTooltip);
3855
+ showCellTooltipAfterDelay(nextTooltip2);
3286
3856
  }
3287
3857
  }
3288
3858
  }
3289
3859
  }
3290
3860
  }
3291
- 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";
3861
+ 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";
3292
3862
  if (event.currentTarget.style.cursor !== cursor) event.currentTarget.style.cursor = cursor;
3293
3863
  },
3294
3864
  onMouseLeave: (event) => {
@@ -3331,6 +3901,7 @@ function Table({
3331
3901
  const headerY = fixedHeader ? 0 : -scrollRef.current.top;
3332
3902
  if (localY >= headerY && localY < headerY + headerHeight) {
3333
3903
  clickedCellRef.current = null;
3904
+ if (localY < headerY + headerLeafTop) return;
3334
3905
  const columnIndex = locateColumn(event.clientX);
3335
3906
  const action = locateHeaderAction(event.clientX, columnIndex);
3336
3907
  if (action === "sort") {
@@ -3392,21 +3963,30 @@ function Table({
3392
3963
  className: "rvg-text-scroll-content",
3393
3964
  style: { left: -fixedWidth, width: contentWidth, transform: `translateX(${-currentScroll.left}px)` },
3394
3965
  children: [
3395
- columns.map((column, columnIndex) => column.fixed ? null : columnIndex >= domRange.columnStart && columnIndex < domRange.columnEnd ? /* @__PURE__ */ jsx7("span", { children: renderHeaderTitle(columnIndex, metrics[columnIndex].left) }, column.key) : null),
3966
+ columns.map((column, columnIndex) => column.fixed || columnIndex < utilityColumnCount || headerDepth > 1 ? null : columnIndex >= domRange.columnStart && columnIndex < domRange.columnEnd ? /* @__PURE__ */ jsx7("span", { children: renderHeaderTitle(columnIndex, getScrollableColumnLeft(columnIndex)) }, column.key) : null),
3967
+ headerCells.map((cell) => {
3968
+ if (columns[cell.startIndex]?.fixed) return null;
3969
+ if (cell.endIndex < domRange.columnStart || cell.startIndex >= domRange.columnEnd) return null;
3970
+ return renderGroupedHeaderCell(cell);
3971
+ }),
3396
3972
  /* @__PURE__ */ jsx7("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) => {
3397
3973
  if (column.fixed || columnIndex < domRange.columnStart || columnIndex >= domRange.columnEnd) return null;
3398
- return renderCellText(rowIndex, columnIndex, metrics[columnIndex].left);
3974
+ return renderCellText(rowIndex, columnIndex, getScrollableColumnLeft(columnIndex));
3399
3975
  })) })
3400
3976
  ]
3401
3977
  }
3402
3978
  ) }),
3403
3979
  /* @__PURE__ */ jsx7("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) => {
3404
3980
  if (!column.fixed) return null;
3405
- const left = column.fixed === "left" ? metrics[columnIndex].left : viewport.width - (rightFixedOffsets.get(columnIndex) ?? 0) - metrics[columnIndex].width;
3981
+ const left = column.fixed === "left" ? leftFixedOffsets.get(columnIndex) ?? 0 : viewport.width - (rightFixedOffsets.get(columnIndex) ?? 0) - metrics[columnIndex].width;
3406
3982
  return renderCellText(rowIndex, columnIndex, left);
3407
3983
  })) }),
3408
- columns.map((column, columnIndex) => column.fixed === "left" ? renderHeaderTitle(columnIndex, metrics[columnIndex].left) : null),
3409
- columns.map((column, columnIndex) => column.fixed === "right" ? renderHeaderTitle(columnIndex, viewport.width - (rightFixedOffsets.get(columnIndex) ?? 0) - metrics[columnIndex].width) : null)
3984
+ 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),
3985
+ headerCells.map((cell) => columns[cell.startIndex]?.fixed ? renderGroupedHeaderCell(cell) : null),
3986
+ fixedWidth > 0 && currentScroll.left > 0 && /* @__PURE__ */ jsx7("div", { className: "rvg-text-fixed-shadow is-left", style: { left: fixedWidth } }),
3987
+ rightFixedWidth > 0 && currentScroll.left < contentWidth - viewport.width && /* @__PURE__ */ jsx7("div", { className: "rvg-text-fixed-shadow is-right", style: { right: rightFixedWidth } }),
3988
+ hasVerticalBorders && fixedWidth > 0 && /* @__PURE__ */ jsx7("div", { className: "rvg-text-fixed-border is-left", style: { left: fixedWidth - 1 } }),
3989
+ hasVerticalBorders && rightFixedWidth > 0 && /* @__PURE__ */ jsx7("div", { className: "rvg-text-fixed-border is-right", style: { right: rightFixedWidth } })
3410
3990
  ] }),
3411
3991
  /* @__PURE__ */ jsx7("div", { className: "rvg-spacer", style: { width: contentWidth, height: contentHeight + bodyTop, marginTop: -renderHeight } })
3412
3992
  ] }),
@@ -3414,18 +3994,18 @@ function Table({
3414
3994
  "div",
3415
3995
  {
3416
3996
  className: `rvg-summary-row is-${summaryPosition}${hasVerticalBorders ? "" : " is-vertical-borderless"}`,
3417
- style: { height: rowHeight, top: summaryPosition === "top" ? headerHeight : void 0 },
3997
+ style: { height: rowHeight, top: summaryPosition === "top" ? fixedHeader ? headerHeight : headerHeight - currentScroll.top : void 0 },
3418
3998
  children: [
3419
3999
  /* @__PURE__ */ jsx7("div", { className: "rvg-summary-scroll-clip", style: { left: fixedWidth, right: rightFixedWidth }, children: /* @__PURE__ */ jsx7(
3420
4000
  "div",
3421
4001
  {
3422
4002
  className: "rvg-summary-scroll-content",
3423
4003
  style: { left: -fixedWidth, width: contentWidth, transform: `translateX(${-currentScroll.left}px)` },
3424
- children: columns.map((column, columnIndex) => column.fixed ? null : renderSummaryCell(columnIndex, metrics[columnIndex].left))
4004
+ children: columns.map((column, columnIndex) => column.fixed ? null : renderSummaryCell(columnIndex, getScrollableColumnLeft(columnIndex)))
3425
4005
  }
3426
4006
  ) }),
3427
4007
  /* @__PURE__ */ jsxs7("div", { className: "rvg-summary-fixed-layer", children: [
3428
- columns.map((column, columnIndex) => column.fixed === "left" ? renderSummaryCell(columnIndex, metrics[columnIndex].left) : null),
4008
+ columns.map((column, columnIndex) => column.fixed === "left" ? renderSummaryCell(columnIndex, leftFixedOffsets.get(columnIndex) ?? 0) : null),
3429
4009
  columns.map((column, columnIndex) => column.fixed === "right" ? renderSummaryCell(columnIndex, viewport.width - (rightFixedOffsets.get(columnIndex) ?? 0) - metrics[columnIndex].width) : null)
3430
4010
  ] }),
3431
4011
  fixedWidth > 0 && currentScroll.left > 0 && /* @__PURE__ */ jsx7("div", { className: "rvg-summary-fixed-shadow is-left", style: { left: fixedWidth } }),
@@ -3442,11 +4022,11 @@ function Table({
3442
4022
  ref: scrollingHeaderIconsRef,
3443
4023
  className: "rvg-header-icon-layer rvg-header-icon-layer-scroll",
3444
4024
  style: { left: -fixedWidth, width: contentWidth, transform: `translateX(${-scrollRef.current.left}px)` },
3445
- children: columns.map((column, index) => column.fixed ? null : /* @__PURE__ */ jsx7("span", { children: renderHeaderIcons(index, metrics[index].left) }, column.key))
4025
+ children: columns.map((column, index) => column.fixed ? null : /* @__PURE__ */ jsx7("span", { children: renderHeaderIcons(index, getScrollableColumnLeft(index)) }, column.key))
3446
4026
  }
3447
4027
  ) }),
3448
4028
  /* @__PURE__ */ jsxs7("div", { className: "rvg-header-icon-layer", children: [
3449
- columns.map((column, index) => column.fixed === "left" ? /* @__PURE__ */ jsx7("span", { children: renderHeaderIcons(index, metrics[index].left) }, column.key) : null),
4029
+ columns.map((column, index) => column.fixed === "left" ? /* @__PURE__ */ jsx7("span", { children: renderHeaderIcons(index, leftFixedOffsets.get(index) ?? 0) }, column.key) : null),
3450
4030
  columns.map((column, index) => column.fixed === "right" ? /* @__PURE__ */ jsx7("span", { children: renderHeaderIcons(index, viewport.width - (rightFixedOffsets.get(index) ?? 0) - metrics[index].width) }, column.key) : null)
3451
4031
  ] })
3452
4032
  ] }),
@@ -3723,12 +4303,40 @@ function Table({
3723
4303
  ] })
3724
4304
  ] }) }),
3725
4305
  toast && /* @__PURE__ */ jsx7("div", { className: `rvg-toast is-${toast.type}`, role: "status", children: toast.message }),
4306
+ /* @__PURE__ */ jsx7(
4307
+ "div",
4308
+ {
4309
+ ref: verticalScrollbarRef,
4310
+ className: "rvg-vertical-scrollbar",
4311
+ onPointerEnter: () => keepCustomScrollbarVisible("vertical"),
4312
+ onPointerLeave: () => releaseCustomScrollbarVisible("vertical"),
4313
+ onPointerDown: handleVerticalScrollbarPointerDown,
4314
+ onPointerMove: handleVerticalScrollbarPointerMove,
4315
+ onPointerUp: handleVerticalScrollbarPointerUp,
4316
+ onPointerCancel: handleVerticalScrollbarPointerUp,
4317
+ children: /* @__PURE__ */ jsx7("div", { ref: verticalScrollbarThumbRef, className: "rvg-vertical-scrollbar-thumb" })
4318
+ }
4319
+ ),
4320
+ /* @__PURE__ */ jsx7(
4321
+ "div",
4322
+ {
4323
+ ref: horizontalScrollbarRef,
4324
+ className: "rvg-horizontal-scrollbar",
4325
+ onPointerEnter: () => keepCustomScrollbarVisible("horizontal"),
4326
+ onPointerLeave: () => releaseCustomScrollbarVisible("horizontal"),
4327
+ onPointerDown: handleHorizontalScrollbarPointerDown,
4328
+ onPointerMove: handleHorizontalScrollbarPointerMove,
4329
+ onPointerUp: handleHorizontalScrollbarPointerUp,
4330
+ onPointerCancel: handleHorizontalScrollbarPointerUp,
4331
+ children: /* @__PURE__ */ jsx7("div", { ref: horizontalScrollbarThumbRef, className: "rvg-horizontal-scrollbar-thumb" })
4332
+ }
4333
+ ),
3726
4334
  /* @__PURE__ */ jsx7("div", { ref: dragGuideRef, className: "rvg-column-guide" }),
3727
4335
  resizeGuideX !== null && /* @__PURE__ */ jsx7(
3728
4336
  "div",
3729
4337
  {
3730
4338
  className: "rvg-column-guide is-resizing",
3731
- style: { display: "block", transform: `translateX(${Math.round(resizeGuideX)}px)` }
4339
+ style: { display: "block", top: resizeGuideTop, transform: `translateX(${Math.round(resizeGuideX)}px)` }
3732
4340
  }
3733
4341
  ),
3734
4342
  /* @__PURE__ */ jsx7("div", { ref: rowDragGuideRef, className: "rvg-row-guide" }),