@lensui/lens-table 0.1.2 → 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
@@ -784,8 +784,9 @@ function buildColumnMetrics(columns, columnDraggableOrOptions = false) {
784
784
  });
785
785
  const viewportWidth = options.viewportWidth ?? 0;
786
786
  if (viewportWidth <= left || columns.length === 0) return metrics;
787
- const stretchable = columns.map((column, index) => ({ column, index })).filter(({ column }) => column.fixed !== "right" && !column.rowSelection && !column.rowDragHandle && !column.rowNumber);
788
- const targets = stretchable.length > 0 ? stretchable : columns.map((column, index) => ({ column, index })).filter(({ column }) => column.fixed !== "right");
787
+ const unsetWidthStretchable = columns.map((column, index) => ({ column, index })).filter(({ column }) => column.width === void 0 && column.fixed === void 0 && !column.rowSelection && !column.rowDragHandle && !column.rowNumber);
788
+ const dataStretchable = columns.map((column, index) => ({ column, index })).filter(({ column }) => column.fixed === void 0 && !column.rowSelection && !column.rowDragHandle && !column.rowNumber);
789
+ const targets = unsetWidthStretchable.length > 0 ? unsetWidthStretchable : dataStretchable.length > 0 ? dataStretchable : [];
789
790
  if (targets.length === 0) return metrics;
790
791
  const extra = viewportWidth - left;
791
792
  const baseTotal = targets.reduce((sum, { index }) => sum + metrics[index].width, 0);
@@ -862,6 +863,8 @@ function ellipsizeText(context, text, maxWidth) {
862
863
  }
863
864
  function paintGrid(options) {
864
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;
865
868
  const colors = { ...COLORS, ...options.colors };
866
869
  const bodyTop = options.bodyTop ?? headerHeight;
867
870
  const getCellSpan = (rowIndex, columnIndex) => options.cellSpans.get(getCellCoordKey(rowIndex, columnIndex));
@@ -878,9 +881,16 @@ function paintGrid(options) {
878
881
  ctx.fillRect(0, 0, width, height);
879
882
  ctx.font = "13px Inter, ui-sans-serif, system-ui, sans-serif";
880
883
  ctx.textBaseline = "middle";
881
- 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);
882
885
  const rightFixedWidth = columns.reduce((value, column, index) => column.fixed === "right" ? value + metrics[index].width : value, 0);
883
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
+ }
884
894
  const rightOffsets = /* @__PURE__ */ new Map();
885
895
  let rightOffset = 0;
886
896
  for (let index = columns.length - 1; index >= 0; index -= 1) {
@@ -888,8 +898,14 @@ function paintGrid(options) {
888
898
  rightOffsets.set(index, rightOffset);
889
899
  rightOffset += metrics[index].width;
890
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
+ }
891
907
  const getColumnX = (index) => {
892
- 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;
893
909
  };
894
910
  const paintBodyColumns = (layer) => {
895
911
  const rowStart = Math.max(0, range.rowStart - Math.max(0, options.maxRowSpan - 1));
@@ -1039,7 +1055,7 @@ function paintGrid(options) {
1039
1055
  if (!verticalBorderless) ctx.fillRect(x + metric.width - 1, headerY, 1, headerHeight);
1040
1056
  ctx.fillRect(x, headerY + headerHeight - 1, metric.width, 1);
1041
1057
  const column = columns[columnIndex];
1042
- if (!column.rowSelection && !column.rowDragHandle && !column.rowNumber && column.title) {
1058
+ if (headerLeafTop === 0 && !column.rowSelection && !column.rowDragHandle && !column.rowNumber && !column.renderHeader && column.title) {
1043
1059
  ctx.font = "600 13px Inter, ui-sans-serif, system-ui, sans-serif";
1044
1060
  const titleWidth = ctx.measureText(column.title).width;
1045
1061
  const visibleActions = getVisibleHeaderActions(column, metric.width, columnDraggable, titleWidth);
@@ -1056,11 +1072,11 @@ function paintGrid(options) {
1056
1072
  const textX = titleAlign === "right" ? x + contentWidth - padding : titleAlign === "center" ? x + centeredTextX : x + padding;
1057
1073
  ctx.save();
1058
1074
  ctx.beginPath();
1059
- 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);
1060
1076
  ctx.clip();
1061
1077
  ctx.fillStyle = colors.muted;
1062
1078
  ctx.textAlign = titleAlign === "right" ? "right" : titleAlign === "center" ? "center" : "left";
1063
- ctx.fillText(title, textX, headerY + headerHeight / 2);
1079
+ ctx.fillText(title, textX, headerY + headerLeafTop + headerLeafHeight / 2);
1064
1080
  ctx.restore();
1065
1081
  }
1066
1082
  }
@@ -1068,22 +1084,6 @@ function paintGrid(options) {
1068
1084
  paintHeaderColumns("scroll");
1069
1085
  paintHeaderColumns("left");
1070
1086
  paintHeaderColumns("right");
1071
- if (leftFixedWidth > 0 && scrollLeft > 0) {
1072
- const shadowWidth = 10;
1073
- const shadow = ctx.createLinearGradient(leftFixedWidth, 0, leftFixedWidth + shadowWidth, 0);
1074
- shadow.addColorStop(0, "rgba(5, 5, 5, 0.06)");
1075
- shadow.addColorStop(1, "rgba(5, 5, 5, 0)");
1076
- ctx.fillStyle = shadow;
1077
- ctx.fillRect(leftFixedWidth, 0, shadowWidth, height);
1078
- }
1079
- if (rightFixedWidth > 0 && scrollLeft < (metrics[metrics.length - 1]?.right ?? 0) - width) {
1080
- const shadowWidth = 10;
1081
- const shadow = ctx.createLinearGradient(rightFixedLeft - shadowWidth, 0, rightFixedLeft, 0);
1082
- shadow.addColorStop(0, "rgba(5, 5, 5, 0)");
1083
- shadow.addColorStop(1, "rgba(5, 5, 5, 0.06)");
1084
- ctx.fillStyle = shadow;
1085
- ctx.fillRect(rightFixedLeft - shadowWidth, 0, shadowWidth, height);
1086
- }
1087
1087
  if (rightFixedWidth > 0 && !verticalBorderless) {
1088
1088
  ctx.fillStyle = colors.grid;
1089
1089
  ctx.fillRect(rightFixedLeft - 1, 0, 1, height);
@@ -1235,6 +1235,45 @@ var getRowNumberColumnWidth = (rowCount) => {
1235
1235
  const digits = String(Math.max(1, rowCount)).length;
1236
1236
  return digits <= 4 ? 44 : 44 + (digits - 4) * 8;
1237
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
+ }
1238
1277
  var createPendingInsertId = () => `table-inserted-${globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(36).slice(2)}`}`;
1239
1278
  var waitForPaint = () => new Promise((resolve) => {
1240
1279
  requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
@@ -1298,12 +1337,14 @@ function Table({
1298
1337
  rowKey,
1299
1338
  width = "100%",
1300
1339
  height = "100%",
1301
- autoHeight = false,
1302
- rowHeight = 36,
1303
- headerHeight = 40,
1340
+ autoHeight = true,
1341
+ layout,
1342
+ rowHeight: rowHeightProp,
1343
+ headerHeight: headerHeightProp,
1304
1344
  fixedHeader = true,
1305
1345
  locale = "zh-CN",
1306
- verticalBorderless = false,
1346
+ borderless,
1347
+ verticalBorderless,
1307
1348
  striped = false,
1308
1349
  highlight,
1309
1350
  cellSpans = [],
@@ -1345,8 +1386,27 @@ function Table({
1345
1386
  style,
1346
1387
  ariaLabel = "Data grid"
1347
1388
  }) {
1389
+ const rowHeight = layout?.rowHeight ?? rowHeightProp ?? 36;
1390
+ const baseHeaderHeight = layout?.headerHeight ?? headerHeightProp ?? 40;
1391
+ const headerDepth = useMemo(() => Math.max(1, ...sourceColumns.map(getColumnDepth)), [sourceColumns]);
1348
1392
  const { language, labels } = useMemo(() => resolveGridLocale(locale), [locale]);
1349
- const hasVerticalBorders = !verticalBorderless;
1393
+ const customHeaderContentRefs = useRef2(/* @__PURE__ */ new Map());
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;
1409
+ const hasVerticalBorders = !(borderless ?? verticalBorderless ?? false);
1350
1410
  const hasStripedRows = Boolean(striped);
1351
1411
  const stripedColor = typeof striped === "string" ? striped : void 0;
1352
1412
  const editedCellsHighlight = highlight?.editedCells ?? false;
@@ -1365,6 +1425,13 @@ function Table({
1365
1425
  const summaryEmptyValue = typeof summary === "object" ? summary.emptyValue ?? "" : "";
1366
1426
  const contextMenuEnabled = contextMenuConfig !== false;
1367
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]);
1368
1435
  const columns = useMemo(() => {
1369
1436
  const utilityColumns = [];
1370
1437
  if (rowDraggable) {
@@ -1376,9 +1443,24 @@ function Table({
1376
1443
  if (rowNumber) {
1377
1444
  utilityColumns.push({ key: "__rvg_row_number__", title: "#", width: getRowNumberColumnWidth(sourceRows.length), align: "center", fixed: "left", rowNumber: true });
1378
1445
  }
1379
- return [...utilityColumns, ...sourceColumns];
1380
- }, [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]);
1381
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]);
1382
1464
  const canvasRef = useRef2(null);
1383
1465
  const scrollerRef = useRef2(null);
1384
1466
  const frameRef = useRef2(null);
@@ -1396,6 +1478,14 @@ function Table({
1396
1478
  const dragGuideRef = useRef2(null);
1397
1479
  const rowDragGuideRef = useRef2(null);
1398
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);
1399
1489
  const rowByKeyRef = useRef2(/* @__PURE__ */ new Map());
1400
1490
  const rowIndexByKeyRef = useRef2(/* @__PURE__ */ new Map());
1401
1491
  const headerIconsRef = useRef2(null);
@@ -1448,10 +1538,65 @@ function Table({
1448
1538
  rangeDragRef.current = null;
1449
1539
  setSelectionRange(null);
1450
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]);
1451
1561
  useEffect2(() => () => {
1452
1562
  if (headerTooltipTimerRef.current !== null) clearTimeout(headerTooltipTimerRef.current);
1453
1563
  if (cellTooltipTimerRef.current !== null) clearTimeout(cellTooltipTimerRef.current);
1454
1564
  }, []);
1565
+ const measureCustomHeaderHeight = useCallback2(() => {
1566
+ const nextHeights = Array.from({ length: headerDepth }, () => baseHeaderHeight);
1567
+ customHeaderContentRefs.current.forEach((node) => {
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 });
1573
+ });
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]);
1583
+ useLayoutEffect2(() => {
1584
+ customHeaderContentRefs.current.forEach((_, 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);
1589
+ });
1590
+ if (activeCustomHeaderLevels.size === 0) {
1591
+ setMeasuredHeaderRowHeights([]);
1592
+ return;
1593
+ }
1594
+ measureCustomHeaderHeight();
1595
+ if (typeof ResizeObserver === "undefined") return;
1596
+ const observer = new ResizeObserver(measureCustomHeaderHeight);
1597
+ customHeaderContentRefs.current.forEach((node) => observer.observe(node));
1598
+ return () => observer.disconnect();
1599
+ }, [activeCustomHeaderLevels, measureCustomHeaderHeight, viewport.width]);
1455
1600
  const [filterEditor, setFilterEditor] = useState4(null);
1456
1601
  const [contextMenu, setContextMenu] = useState4(null);
1457
1602
  const [insertCounts, setInsertCounts] = useState4({ before: 1, after: 1 });
@@ -1460,6 +1605,8 @@ function Table({
1460
1605
  const [insertBusy, setInsertBusy] = useState4(false);
1461
1606
  const [deleteBusy, setDeleteBusy] = useState4(false);
1462
1607
  const [toast, setToast] = useState4(null);
1608
+ const [resizeGuideX, setResizeGuideX] = useState4(null);
1609
+ const [resizeGuideTop, setResizeGuideTop] = useState4(0);
1463
1610
  const [hoveredHeaderAction, setHoveredHeaderAction] = useState4(null);
1464
1611
  const [visibleHeaderTooltip, setVisibleHeaderTooltip] = useState4(null);
1465
1612
  const [hoveredCellTooltip, setHoveredCellTooltip] = useState4(null);
@@ -1544,7 +1691,17 @@ function Table({
1544
1691
  const metrics = useMemo(() => buildColumnMetrics(columns, { columnDraggable, viewportWidth: viewport.width }), [columnDraggable, columns, viewport.width]);
1545
1692
  const contentWidth = metrics.length > 0 ? metrics[metrics.length - 1].right : 0;
1546
1693
  const contentHeight = rows.length * rowHeight;
1547
- 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]);
1548
1705
  const rightFixedWidth = useMemo(() => columns.reduce((width2, column, index) => column.fixed === "right" ? width2 + metrics[index].width : width2, 0), [columns, metrics]);
1549
1706
  const rightFixedOffsets = useMemo(() => {
1550
1707
  const offsets = /* @__PURE__ */ new Map();
@@ -1556,6 +1713,16 @@ function Table({
1556
1713
  }
1557
1714
  return offsets;
1558
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]);
1559
1726
  const hasRowSelectionColumn = useMemo(() => columns.some((column) => column.rowSelection), [columns]);
1560
1727
  const rowSelectionMode = typeof rowSelection === "object" ? rowSelection.mode ?? "multiple" : "multiple";
1561
1728
  const hasSummaryRow = useMemo(() => rows.length > 0 && summaryEnabled && columns.some((column) => Boolean(column.summary)), [columns, rows.length, summaryEnabled]);
@@ -1566,7 +1733,7 @@ function Table({
1566
1733
  const realContentHeight = bodyTop + bodyContentHeight + bottomSummaryHeight + (contentWidth > viewport.width ? horizontalScrollbarHeight : 0);
1567
1734
  const effectiveHeight = autoHeight ? Math.max(bodyTop + bottomSummaryHeight, Math.min(resolvedHeight, realContentHeight)) : resolvedHeight;
1568
1735
  const renderHeight = Math.max(0, effectiveHeight - bottomSummaryHeight);
1569
- const bodyViewportHeight = Math.max(0, renderHeight - (fixedHeader ? headerHeight : 0) - topSummaryHeight);
1736
+ const bodyViewportHeight = Math.max(0, renderHeight - (fixedHeader ? headerHeight + topSummaryHeight : 0));
1570
1737
  const [summaryState, setSummaryState] = useState4(() => ({ columns: null, rows: null, values: /* @__PURE__ */ new Map(), pending: false }));
1571
1738
  const summaryPending = hasSummaryRow && (loading || summaryState.pending || summaryState.columns !== columns || summaryState.rows !== rows);
1572
1739
  const summaryValues = hasSummaryRow && summaryState.columns === columns && summaryState.rows === rows ? summaryState.values : /* @__PURE__ */ new Map();
@@ -1613,14 +1780,30 @@ function Table({
1613
1780
  rowIndexByKeyRef.current.set(key, index);
1614
1781
  return key;
1615
1782
  }, [getDataRowKey]);
1783
+ const resolvedCellSpans = useMemo(() => {
1784
+ if (Array.isArray(cellSpans)) return cellSpans;
1785
+ const spans = [];
1786
+ rows.forEach((row, rowIndex) => {
1787
+ columns.forEach((column, columnIndex) => {
1788
+ const span = cellSpans({ row, rowIndex, column, columnIndex, rows, columns });
1789
+ if (!span) return;
1790
+ spans.push({
1791
+ rowIndex,
1792
+ columnKey: column.key,
1793
+ ...span
1794
+ });
1795
+ });
1796
+ });
1797
+ return spans;
1798
+ }, [cellSpans, columns, rows]);
1616
1799
  const cellSpanLookup = useMemo(() => {
1617
- if (cellSpans.length === 0) return createEmptyCellSpanLookup();
1800
+ if (resolvedCellSpans.length === 0) return createEmptyCellSpanLookup();
1618
1801
  const rowIndexByKey = /* @__PURE__ */ new Map();
1619
1802
  rows.forEach((row, index) => rowIndexByKey.set(getRowKey(row, index), index));
1620
1803
  const columnIndexByKey = new Map(columns.map((column, index) => [column.key, index]));
1621
1804
  const lookup = createEmptyCellSpanLookup();
1622
1805
  const occupied = /* @__PURE__ */ new Set();
1623
- for (const span of cellSpans) {
1806
+ for (const span of resolvedCellSpans) {
1624
1807
  const rowIndex = span.rowKey !== void 0 ? rowIndexByKey.get(span.rowKey) : span.rowIndex;
1625
1808
  const columnIndex = columnIndexByKey.get(span.columnKey);
1626
1809
  if (rowIndex === void 0 || columnIndex === void 0) continue;
@@ -1656,7 +1839,7 @@ function Table({
1656
1839
  }
1657
1840
  }
1658
1841
  return lookup;
1659
- }, [cellSpans, columns, getRowKey, rows]);
1842
+ }, [columns, getRowKey, resolvedCellSpans, rows]);
1660
1843
  const selectedRowKeySet = useMemo(() => new Set(rowKeys), [rowKeys]);
1661
1844
  const selectedColumnKeySet = useMemo(() => new Set(columnKeys), [columnKeys]);
1662
1845
  const insertedRowKeySet = useMemo(() => insertedRowKeys, [insertedRowKeys]);
@@ -1726,8 +1909,8 @@ function Table({
1726
1909
  const scroll = scrollRef.current;
1727
1910
  const rowScrollTop = fixedHeader ? scroll.top : Math.max(0, scroll.top - bodyTop);
1728
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 };
1729
- 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 });
1730
- }, [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]);
1731
1914
  const scheduleDraw = useCallback2(() => {
1732
1915
  if (frameRef.current === null) frameRef.current = requestAnimationFrame(draw);
1733
1916
  }, [draw]);
@@ -1764,6 +1947,8 @@ function Table({
1764
1947
  if (sortDebounceRef.current !== null) clearTimeout(sortDebounceRef.current);
1765
1948
  if (textFrameRef.current !== null) cancelAnimationFrame(textFrameRef.current);
1766
1949
  if (toastTimerRef.current !== null) clearTimeout(toastTimerRef.current);
1950
+ if (verticalScrollbarVisibilityTimerRef.current !== null) clearTimeout(verticalScrollbarVisibilityTimerRef.current);
1951
+ if (horizontalScrollbarVisibilityTimerRef.current !== null) clearTimeout(horizontalScrollbarVisibilityTimerRef.current);
1767
1952
  }, []);
1768
1953
  useEffect2(() => {
1769
1954
  const scroller = scrollerRef.current;
@@ -1815,9 +2000,204 @@ function Table({
1815
2000
  scroller.addEventListener("scroll", lockScroll);
1816
2001
  return () => scroller.removeEventListener("scroll", lockScroll);
1817
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]);
1818
2195
  const handleScroll = useCallback2(() => {
1819
2196
  const element = scrollerRef.current;
1820
2197
  if (!element) return;
2198
+ const previousScroll = scrollRef.current;
2199
+ const scrollDeltaX = element.scrollLeft - previousScroll.left;
2200
+ const scrollDeltaY = element.scrollTop - previousScroll.top;
1821
2201
  scrollRef.current = { left: element.scrollLeft, top: element.scrollTop };
1822
2202
  const focus = selectionFocusRef.current;
1823
2203
  if (focus && selection && !selectionRange) {
@@ -1828,7 +2208,7 @@ function Table({
1828
2208
  if (!validSelection) {
1829
2209
  focus.style.display = "none";
1830
2210
  } else {
1831
- 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;
1832
2212
  const rawTop = bodyTop + selection.rowIndex * rowHeight - element.scrollTop;
1833
2213
  const span = getCellSpan(selection.rowIndex, selection.columnIndex);
1834
2214
  const rawRight = rawLeft + getCellDisplayWidth(selection.columnIndex, span?.colSpan ?? 1);
@@ -1861,6 +2241,10 @@ function Table({
1861
2241
  if (scrollingTextRef.current) scrollingTextRef.current.style.transform = `translateX(${-element.scrollLeft}px)`;
1862
2242
  if (scrollingHeaderIconsRef.current) scrollingHeaderIconsRef.current.style.transform = `translateX(${-element.scrollLeft}px)`;
1863
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
+ }
1864
2248
  if (hoveredHeaderActionRef.current) {
1865
2249
  hoveredHeaderActionRef.current = null;
1866
2250
  setHoveredHeaderAction(null);
@@ -1868,7 +2252,7 @@ function Table({
1868
2252
  setHoveredCellTooltip(null);
1869
2253
  scheduleDraw();
1870
2254
  setEditing(null);
1871
- }, [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]);
1872
2256
  const locateColumn = useCallback2((clientX) => {
1873
2257
  const canvas = canvasRef.current;
1874
2258
  if (!canvas) return -1;
@@ -1876,7 +2260,11 @@ function Table({
1876
2260
  const localX = clientX - rect.left;
1877
2261
  let columnIndex;
1878
2262
  if (localX < fixedWidth) {
1879
- 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
+ });
1880
2268
  } else if (rightFixedWidth > 0 && localX >= viewport.width - rightFixedWidth) {
1881
2269
  columnIndex = columns.findIndex((column, index) => {
1882
2270
  if (column.fixed !== "right") return false;
@@ -1884,31 +2272,33 @@ function Table({
1884
2272
  return localX >= left && localX < left + metrics[index].width;
1885
2273
  });
1886
2274
  } else {
1887
- 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
+ });
1888
2281
  }
1889
2282
  return columnIndex;
1890
- }, [columns, fixedWidth, metrics, rightFixedOffsets, rightFixedWidth, viewport.width]);
2283
+ }, [columns, fixedWidth, getScrollableColumnLeft, leftFixedOffsets, metrics, rightFixedOffsets, rightFixedWidth, viewport.width]);
1891
2284
  const getDisplayedColumnLeft = useCallback2((columnIndex) => {
1892
2285
  const metric = metrics[columnIndex];
1893
- if (columns[columnIndex].fixed === "left") return metric.left;
2286
+ if (columns[columnIndex].fixed === "left") return leftFixedOffsets.get(columnIndex) ?? 0;
1894
2287
  if (columns[columnIndex].fixed === "right") return viewport.width - (rightFixedOffsets.get(columnIndex) ?? 0) - metric.width;
1895
- return metric.left - scrollRef.current.left;
1896
- }, [columns, metrics, rightFixedOffsets, viewport.width]);
1897
- const locateResizeHandle = useCallback2((clientX) => {
1898
- if (!columnResizable || !canvasRef.current) return -1;
1899
- const localX = clientX - canvasRef.current.getBoundingClientRect().left;
1900
- let closest = -1;
1901
- let distance = 6;
1902
- for (let index = 0; index < columns.length; index += 1) {
1903
- if (columns[index].rowSelection || columns[index].rowDragHandle || columns[index].rowNumber) continue;
1904
- const nextDistance = Math.abs(localX - (getDisplayedColumnLeft(index) + metrics[index].width));
1905
- if (nextDistance < distance) {
1906
- distance = nextDistance;
1907
- closest = index;
1908
- }
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;
1909
2295
  }
1910
- return closest;
1911
- }, [columnResizable, columns, getDisplayedColumnLeft, metrics]);
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]);
1912
2302
  const locateHeaderAction = useCallback2((clientX, columnIndex) => {
1913
2303
  if (columnIndex < 0 || !canvasRef.current) return null;
1914
2304
  const column = columns[columnIndex];
@@ -1943,11 +2333,70 @@ function Table({
1943
2333
  const columnRight = getDisplayedColumnLeft(columnIndex) + metrics[columnIndex].width;
1944
2334
  return localX >= columnRight - 16 && localX < columnRight - 5;
1945
2335
  }, [columnDraggable, columns, getDisplayedColumnLeft, measureHeaderTitleWidth, metrics]);
1946
- 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) => {
1947
2393
  const guide = dragGuideRef.current;
1948
2394
  if (!guide) return;
1949
2395
  guide.style.display = x === null ? "none" : "block";
1950
- 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
+ }
1951
2400
  }, []);
1952
2401
  const setRowDragGuide = useCallback2((y) => {
1953
2402
  const guide = rowDragGuideRef.current;
@@ -1965,6 +2414,15 @@ function Table({
1965
2414
  useEffect2(() => {
1966
2415
  const canvas = canvasRef.current;
1967
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
+ };
1968
2426
  const getHeaderColumn = (clientX, clientY) => {
1969
2427
  const rect = canvas.getBoundingClientRect();
1970
2428
  const headerY = fixedHeader ? 0 : -scrollRef.current.top;
@@ -1972,22 +2430,35 @@ function Table({
1972
2430
  if (localY < headerY || localY >= headerY + headerHeight) return -1;
1973
2431
  return locateColumn(clientX);
1974
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
+ };
1975
2438
  const cleanupDrag = draggable({
1976
2439
  element: canvas,
1977
2440
  canDrag: ({ input }) => {
1978
- const index = getHeaderColumn(input.clientX, input.clientY);
1979
- 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;
1980
2443
  const columnIndex = locateColumn(input.clientX);
1981
2444
  return rowDraggable && columnIndex >= 0 && Boolean(columns[columnIndex].rowDragHandle) && locateRow(input.clientY) >= 0;
1982
2445
  },
1983
2446
  getInitialData: ({ input }) => {
1984
- const headerColumn = getHeaderColumn(input.clientX, input.clientY);
1985
- if (headerColumn < 0) {
2447
+ const headerDragCell = columnDraggable ? getHeaderDragCell(input.clientX, input.clientY) : null;
2448
+ if (!headerDragCell) {
1986
2449
  return { type: "table-row", sourceIndex: locateRow(input.clientY) };
1987
2450
  }
1988
- const sourceIndex = headerColumn;
2451
+ const sourceIndex = headerDragCell.startIndex;
1989
2452
  columnDragPreviewRef.current = { sourceIndex, targetIndex: sourceIndex };
1990
- 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
+ };
1991
2462
  },
1992
2463
  onGenerateDragPreview: ({ nativeSetDragImage, source }) => {
1993
2464
  const sourceIndex = Number(source.data.sourceIndex);
@@ -2090,31 +2561,31 @@ function Table({
2090
2561
  return;
2091
2562
  }
2092
2563
  const targetIndex = Number(target?.targetIndex);
2093
- 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) {
2094
2565
  setDragGuide(null);
2095
2566
  return;
2096
2567
  }
2097
2568
  const edge = target?.columnEdge;
2098
- let destinationIndex = targetIndex + (edge === "right" ? 1 : 0);
2099
- if (sourceIndex < destinationIndex) destinationIndex -= 1;
2100
- destinationIndex = Math.max(0, Math.min(columns.length - 1, destinationIndex));
2569
+ const destinationIndex = getColumnDropIndex(sourceIndex, targetIndex, edge);
2101
2570
  columnDragPreviewRef.current = { sourceIndex, targetIndex: destinationIndex };
2102
- const targetLeft = getDisplayedColumnLeft(targetIndex);
2103
- 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);
2104
2575
  },
2105
2576
  onDrop: ({ source, location }) => {
2106
2577
  const target = location.current.dropTargets[0]?.data;
2107
2578
  const sourceIndex = Number(source.data.sourceIndex);
2108
2579
  if (source.data.type === "table-row") {
2109
- const rawTargetIndex = Number(target?.targetIndex);
2110
- let targetIndex2 = rawTargetIndex + (target?.rowEdge === "bottom" ? 1 : 0);
2580
+ const rawTargetIndex2 = Number(target?.targetIndex);
2581
+ let targetIndex2 = rawTargetIndex2 + (target?.rowEdge === "bottom" ? 1 : 0);
2111
2582
  if (sourceIndex < targetIndex2) targetIndex2 -= 1;
2112
2583
  targetIndex2 = Math.max(0, Math.min(rows.length - 1, targetIndex2));
2113
2584
  scrollerRef.current?.classList.add("rvg-row-drop-settling");
2114
2585
  setRowDragPreview(null);
2115
2586
  setRowDragGuide(null);
2116
2587
  canvas.style.cursor = "default";
2117
- if (Number.isInteger(rawTargetIndex) && sourceIndex !== targetIndex2) onRowOrderChange?.(sourceIndex, targetIndex2);
2588
+ if (Number.isInteger(rawTargetIndex2) && sourceIndex !== targetIndex2) onRowOrderChange?.(sourceIndex, targetIndex2);
2118
2589
  requestAnimationFrame(() => {
2119
2590
  requestAnimationFrame(() => scrollerRef.current?.classList.remove("rvg-row-drop-settling"));
2120
2591
  });
@@ -2123,12 +2594,23 @@ function Table({
2123
2594
  }, 0);
2124
2595
  return;
2125
2596
  }
2126
- 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);
2127
2599
  canvas.style.cursor = "default";
2128
2600
  setDragGuide(null);
2129
2601
  columnDragPreviewRef.current = null;
2130
- if (Number.isInteger(targetIndex) && sourceIndex !== targetIndex && columns[targetIndex]?.fixed === columns[sourceIndex]?.fixed) {
2131
- 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
+ );
2132
2614
  }
2133
2615
  window.setTimeout(() => {
2134
2616
  suppressClickRef.current = false;
@@ -2146,24 +2628,38 @@ function Table({
2146
2628
  const rowEdge = input.clientY - canvas.getBoundingClientRect().top < rowTop + rowHeight / 2 ? "top" : "bottom";
2147
2629
  return attachClosestEdge({ targetIndex: targetIndex2, rowEdge }, { element, input, allowedEdges: ["top", "bottom"] });
2148
2630
  }
2149
- let targetIndex = getHeaderColumn(input.clientX, input.clientY);
2150
- 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 };
2151
2637
  if (columns[targetIndex].rowSelection || columns[targetIndex].rowDragHandle || columns[targetIndex].rowNumber) {
2152
2638
  const fixedSide = columns[Number(source.data.sourceIndex)]?.fixed;
2153
2639
  targetIndex = columns.findIndex((column, index) => index > targetIndex && column.fixed === fixedSide && !column.rowSelection && !column.rowDragHandle && !column.rowNumber);
2154
2640
  if (targetIndex < 0) return { targetIndex: -1 };
2155
2641
  return { targetIndex, columnEdge: "left" };
2156
2642
  }
2157
- const left = getDisplayedColumnLeft(targetIndex);
2158
- const columnEdge = input.clientX - canvas.getBoundingClientRect().left < left + metrics[targetIndex].width / 2 ? "left" : "right";
2159
- 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"] });
2160
2656
  }
2161
2657
  });
2162
2658
  return () => {
2163
2659
  cleanupDrag();
2164
2660
  cleanupDrop();
2165
2661
  };
2166
- }, [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]);
2167
2663
  const locateCell = useCallback2((clientX, clientY) => {
2168
2664
  const canvas = canvasRef.current;
2169
2665
  if (!canvas) return null;
@@ -2270,8 +2766,9 @@ function Table({
2270
2766
  const horizontalFocusGap = 0;
2271
2767
  const verticalFocusGap = 0;
2272
2768
  if (column.fixed === void 0) {
2273
- if (metric.left - nextLeft < fixedWidth) nextLeft = Math.max(0, metric.left - fixedWidth - horizontalFocusGap);
2274
- 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;
2275
2772
  }
2276
2773
  const rowTop = (fixedHeader ? 0 : bodyTop) + cell.rowIndex * rowHeight;
2277
2774
  const rowBottom = rowTop + rowHeight * rowSpan;
@@ -2284,7 +2781,7 @@ function Table({
2284
2781
  scroller.scrollTop = nextTop;
2285
2782
  scrollRef.current = { left: scroller.scrollLeft, top: scroller.scrollTop };
2286
2783
  scheduleDraw();
2287
- }, [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]);
2288
2785
  const beginEdit = useCallback2((cell) => {
2289
2786
  const column = columns[cell.columnIndex];
2290
2787
  if (!column?.editable || column.dataIndex === void 0) return;
@@ -2305,8 +2802,9 @@ function Table({
2305
2802
  const horizontalFocusGap = 0;
2306
2803
  const verticalFocusGap = 0;
2307
2804
  if (column.fixed === void 0) {
2308
- if (metric.left - nextLeft < fixedWidth) nextLeft = Math.max(0, metric.left - fixedWidth - horizontalFocusGap);
2309
- 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;
2310
2808
  }
2311
2809
  const rowTop = (fixedHeader ? 0 : bodyTop) + cell.rowIndex * rowHeight;
2312
2810
  const rowBottom = rowTop + rowHeight * rowSpan;
@@ -2328,7 +2826,7 @@ function Table({
2328
2826
  return;
2329
2827
  }
2330
2828
  setEditing(cell);
2331
- }, [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]);
2332
2830
  const getCellEditKey = (cell) => `${typeof cell.rowKey}:${String(cell.rowKey)}\0${cell.columnKey}`;
2333
2831
  const markCellEdited = (cell, previousValue, nextValue) => {
2334
2832
  const key = getCellEditKey(cell);
@@ -2459,10 +2957,13 @@ function Table({
2459
2957
  const visibleRowHeight = fixedHeader ? renderHeight - bodyTop : renderHeight;
2460
2958
  if (rowTop < scroller.scrollTop) scroller.scrollTop = rowTop;
2461
2959
  if (rowBottom > scroller.scrollTop + visibleRowHeight) scroller.scrollTop = rowBottom - visibleRowHeight;
2462
- if (columns[columnIndex].fixed === void 0 && metric.left < scroller.scrollLeft + fixedWidth) scroller.scrollLeft = Math.max(0, metric.left - fixedWidth);
2463
- 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
+ }
2464
2965
  }
2465
- }, [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]);
2466
2967
  const editorStyle = useMemo(() => {
2467
2968
  if (!editing) return void 0;
2468
2969
  const metric = metrics[editing.columnIndex];
@@ -2470,43 +2971,83 @@ function Table({
2470
2971
  const span = getCellSpan(editing.rowIndex, editing.columnIndex);
2471
2972
  const cellWidth = getCellDisplayWidth(editing.columnIndex, span?.colSpan ?? 1);
2472
2973
  const cellHeight = rowHeight * (span?.rowSpan ?? 1);
2473
- 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;
2474
2975
  return {
2475
2976
  left: (fixedSide ? cellLeft : Math.max(cellLeft, fixedWidth)) + 1,
2476
2977
  top: Math.max(fixedHeader ? bodyTop : 0, bodyTop + editing.rowIndex * rowHeight - scrollRef.current.top) + 1,
2477
2978
  width: cellWidth - 2,
2478
2979
  height: cellHeight - 2
2479
2980
  };
2480
- }, [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]);
2481
2982
  const handleColumnPointerDown = useCallback2((event) => {
2482
2983
  const rect = event.currentTarget.getBoundingClientRect();
2483
2984
  const localY = event.clientY - rect.top;
2484
2985
  const headerY = fixedHeader ? 0 : -scrollRef.current.top;
2485
2986
  if (localY < headerY || localY >= headerY + headerHeight) return;
2486
- const resizeIndex = locateResizeHandle(event.clientX);
2487
- if (resizeIndex >= 0) {
2987
+ const resizeHit = locateHeaderResizeHit(event.clientX, event.clientY);
2988
+ if (resizeHit) {
2989
+ const { cell: resizeCell, edge: resizeEdge } = resizeHit;
2488
2990
  event.preventDefault();
2489
2991
  event.currentTarget.setPointerCapture(event.pointerId);
2490
- columnDragRef.current = { type: "resize", columnIndex: resizeIndex, startX: event.clientX, startWidth: 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);
2996
+ columnDragRef.current = {
2997
+ type: "resize",
2998
+ columnIndex: resizeIndex,
2999
+ startX: event.clientX,
3000
+ startWidth: startWidths.reduce((total, width2) => total + width2, 0),
3001
+ startGuideX,
3002
+ guideTop: headerRowOffsets[resizeCell.level] ?? 0,
3003
+ resizeEdge,
3004
+ resizeIndices,
3005
+ startWidths
3006
+ };
2491
3007
  suppressClickRef.current = true;
2492
- setDragGuide(getDisplayedColumnLeft(resizeIndex) + metrics[resizeIndex].width);
3008
+ setResizeGuideTop(headerRowOffsets[resizeCell.level] ?? 0);
3009
+ setResizeGuideX(startGuideX);
2493
3010
  return;
2494
3011
  }
2495
- }, [fixedHeader, getDisplayedColumnLeft, headerHeight, locateResizeHandle, metrics, setDragGuide]);
3012
+ }, [fixedHeader, getHeaderCellLeft, getHeaderCellWidth, headerHeight, headerRowOffsets, locateHeaderResizeHit, metrics]);
2496
3013
  const handleColumnPointerMove = useCallback2((event) => {
2497
3014
  const drag = columnDragRef.current;
2498
3015
  if (!drag) return;
2499
- const width2 = Math.max(getMinimumColumnWidth(columns[drag.columnIndex], columnDraggable), Math.round(drag.startWidth + event.clientX - drag.startX));
2500
- onColumnResize?.(columns[drag.columnIndex].key, width2);
2501
- setDragGuide(getDisplayedColumnLeft(drag.columnIndex) + width2);
2502
- }, [columnDraggable, columns, getDisplayedColumnLeft, onColumnResize, setDragGuide]);
3016
+ const column = columns[drag.columnIndex];
3017
+ const minWidth = getMinimumColumnWidth(column, columnDraggable);
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));
3037
+ return;
3038
+ }
3039
+ const width2 = Math.max(minWidth, Math.round(drag.startWidth + delta));
3040
+ resizeColumn(column.key, width2);
3041
+ setResizeGuideX(drag.startGuideX + (drag.resizeEdge === "left" ? drag.startWidth - width2 : width2 - drag.startWidth));
3042
+ }, [columnDraggable, columns, resizeColumn]);
2503
3043
  const handleColumnPointerUp = useCallback2((event) => {
2504
3044
  const drag = columnDragRef.current;
2505
3045
  if (!drag) return;
2506
3046
  if (event.currentTarget.hasPointerCapture(event.pointerId)) event.currentTarget.releasePointerCapture(event.pointerId);
2507
3047
  columnDragRef.current = null;
2508
- setDragGuide(null);
2509
- }, [setDragGuide]);
3048
+ setResizeGuideX(null);
3049
+ setResizeGuideTop(0);
3050
+ }, []);
2510
3051
  const queueSortStateChange = useCallback2((next) => {
2511
3052
  setDisplaySortState(next);
2512
3053
  if (sortDebounceRef.current !== null) clearTimeout(sortDebounceRef.current);
@@ -2518,7 +3059,7 @@ function Table({
2518
3059
  const renderHeaderIcons = (columnIndex, left) => {
2519
3060
  const column = columns[columnIndex];
2520
3061
  if (column.rowSelection || column.rowDragHandle || column.rowNumber) return null;
2521
- const top = (headerHeight - 14) / 2;
3062
+ const top = headerLeafTop + (headerLeafHeight - 14) / 2;
2522
3063
  const visibleActions = getVisibleHeaderActions(column, metrics[columnIndex].width, columnDraggable, measureHeaderTitleWidth(columnIndex));
2523
3064
  let right = left + metrics[columnIndex].width - (visibleActions.drag ? 18 : 4);
2524
3065
  const icons = [];
@@ -2544,44 +3085,87 @@ function Table({
2544
3085
  }
2545
3086
  return icons;
2546
3087
  };
2547
- const headerTooltip = (() => {
2548
- if (!visibleHeaderTooltip) return null;
2549
- const { columnIndex, action } = visibleHeaderTooltip;
2550
- const column = columns[columnIndex];
3088
+ const createHeaderTooltip = useCallback2((action, columnIndex, cell) => {
3089
+ const column = cell?.column ?? columns[columnIndex];
2551
3090
  if (!column) return null;
2552
- const columnRight = getDisplayedColumnLeft(columnIndex) + metrics[columnIndex].width;
2553
- const visibleActions = getVisibleHeaderActions(column, metrics[columnIndex].width, columnDraggable, measureHeaderTitleWidth(columnIndex));
2554
- let anchorX = columnRight - (visibleActions.drag ? 18 : 4);
2555
- let anchorLeft = anchorX;
2556
- 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;
2557
3099
  if (action === "title") {
2558
- const titleWidth = measureHeaderTitleWidth(columnIndex);
2559
- const actionWidth = (Number(visibleActions.drag) + Number(visibleActions.filter) + Number(visibleActions.sort)) * 18;
2560
- const contentWidth2 = Math.max(0, metrics[columnIndex].width - actionWidth);
2561
- const textWidth = Math.min(titleWidth, Math.max(0, contentWidth2 - 20));
2562
- const columnLeft = getDisplayedColumnLeft(columnIndex);
2563
- anchorX = column.align === "right" ? columnLeft + contentWidth2 - 10 : column.align === "center" ? columnLeft + metrics[columnIndex].width / 2 + textWidth / 2 : columnLeft + 10 + textWidth;
2564
- anchorLeft = column.align === "right" ? anchorX - textWidth : column.align === "center" ? columnLeft + metrics[columnIndex].width / 2 - textWidth / 2 : columnLeft + 10;
2565
- 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
+ }
2566
3119
  } else if (action === "drag") {
2567
- anchorX = columnRight;
3120
+ anchorX = cellLeft + cellWidth;
2568
3121
  anchorLeft = anchorX;
2569
3122
  label = labels.dragColumn;
2570
3123
  } else if (action === "sort") {
2571
3124
  const direction = displaySortState?.columnKey === column.key ? displaySortState.direction : null;
2572
3125
  label = direction === "asc" ? labels.sortAsc : direction === "desc" ? labels.sortDesc : labels.sortBoth;
2573
- } else {
2574
- if (visibleActions.sort) anchorX -= 18;
3126
+ anchorX = cellLeft + cellWidth - 22;
3127
+ anchorLeft = anchorX;
3128
+ } else if (action === "filter") {
2575
3129
  const value = filterValues[column.key];
2576
3130
  label = value ? labels.filterWithValue(value) : labels.filter;
3131
+ anchorX = cellLeft + cellWidth - 40;
3132
+ anchorLeft = anchorX;
2577
3133
  }
2578
3134
  const placement = anchorX + 328 > viewport.width ? "left" : "right";
2579
3135
  return {
3136
+ key: `${action}:${cell ? `cell:${cell.key}:${cell.level}` : `column:${column.key}`}:${Math.round(anchorX)}:${Math.round(rowTop)}`,
3137
+ columnIndex,
3138
+ action,
2580
3139
  left: placement === "right" ? anchorX + 8 : Math.max(8, anchorLeft - 8),
2581
- top: (fixedHeader ? 0 : -scrollPosition.top) + headerHeight / 2,
3140
+ top: rowTop + rowHeight2 / 2,
2582
3141
  label,
2583
3142
  placement
2584
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;
2585
3169
  })();
2586
3170
  const getRangeBounds = () => selectionRange ? {
2587
3171
  rowStart: Math.min(selectionRange.anchor.rowIndex, selectionRange.focus.rowIndex),
@@ -2640,7 +3224,7 @@ function Table({
2640
3224
  }, 1800);
2641
3225
  };
2642
3226
  const showHeaderTooltipAfterDelay = (next) => {
2643
- const nextKey = next ? `${next.columnIndex}:${next.action}` : "";
3227
+ const nextKey = next ? next.key : "";
2644
3228
  if (nextKey && pendingHeaderTooltipKeyRef.current === nextKey) return;
2645
3229
  pendingHeaderTooltipKeyRef.current = nextKey;
2646
3230
  if (headerTooltipTimerRef.current !== null) clearTimeout(headerTooltipTimerRef.current);
@@ -2888,12 +3472,14 @@ function Table({
2888
3472
  const renderHeaderTitle = (columnIndex, left) => {
2889
3473
  const column = columns[columnIndex];
2890
3474
  const currentScrollTop = scrollRef.current.top;
3475
+ const headerTop = fixedHeader ? 0 : -currentScrollTop;
3476
+ const leafTop = headerTop + headerLeafTop;
2891
3477
  if (column.rowNumber) {
2892
3478
  return /* @__PURE__ */ jsx7(
2893
3479
  "div",
2894
3480
  {
2895
3481
  className: "rvg-header-title",
2896
- 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" },
2897
3483
  children: /* @__PURE__ */ jsx7("span", { children: column.title })
2898
3484
  },
2899
3485
  column.key
@@ -2907,14 +3493,102 @@ function Table({
2907
3493
  "div",
2908
3494
  {
2909
3495
  className: "rvg-header-selection-icon",
2910
- style: { left, top: fixedHeader ? 0 : -currentScrollTop, width: metrics[columnIndex].width, height: headerHeight },
3496
+ style: { left, top: headerTop, width: metrics[columnIndex].width, height: headerHeight },
2911
3497
  children: /* @__PURE__ */ jsx7(SelectionIcon, { checked: allSelected, indeterminate })
2912
3498
  },
2913
3499
  column.key
2914
3500
  );
2915
3501
  }
3502
+ if (column.renderHeader) {
3503
+ const visibleActions = getVisibleHeaderActions(column, metrics[columnIndex].width, columnDraggable, measureHeaderTitleWidth(columnIndex));
3504
+ const actionWidth = (Number(visibleActions.drag) + Number(visibleActions.filter) + Number(visibleActions.sort)) * 18;
3505
+ return /* @__PURE__ */ jsx7(
3506
+ "div",
3507
+ {
3508
+ className: "rvg-header-title is-custom",
3509
+ "data-level": headerDepth - 1,
3510
+ "data-measure-key": column.key,
3511
+ style: {
3512
+ left,
3513
+ top: leafTop,
3514
+ width: Math.max(0, metrics[columnIndex].width - actionWidth),
3515
+ height: headerLeafHeight,
3516
+ justifyContent: column.align === "right" ? "flex-end" : column.align === "center" ? "center" : "flex-start",
3517
+ textAlign: column.align === "right" ? "right" : column.align === "center" ? "center" : "left"
3518
+ },
3519
+ children: /* @__PURE__ */ jsx7(
3520
+ "span",
3521
+ {
3522
+ ref: (node) => {
3523
+ if (node) {
3524
+ customHeaderContentRefs.current.set(column.key, node);
3525
+ requestAnimationFrame(measureCustomHeaderHeight);
3526
+ } else customHeaderContentRefs.current.delete(column.key);
3527
+ },
3528
+ children: column.renderHeader(column)
3529
+ }
3530
+ )
3531
+ },
3532
+ column.key
3533
+ );
3534
+ }
2916
3535
  return null;
2917
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
+ };
2918
3592
  const rangeHandlePosition = (() => {
2919
3593
  if (!rangeSelection) return null;
2920
3594
  const bounds = getRangeBounds();
@@ -3106,15 +3780,19 @@ function Table({
3106
3780
  const headerY = fixedHeader ? 0 : -scrollRef.current.top;
3107
3781
  const columnIndex = locateColumn(event.clientX);
3108
3782
  const inHeader = localY >= headerY && localY < headerY + headerHeight;
3109
- const hoveredAction = inHeader ? locateHeaderAction(event.clientX, columnIndex) : null;
3110
- const hoveredDragHandle = inHeader && locateHeaderDragHandle(event.clientX, columnIndex);
3111
- const hoveredTitle = inHeader && !hoveredAction && !hoveredDragHandle && columnIndex >= 0 && Boolean(columns[columnIndex]?.title) && (tooltipConfig.header || isHeaderTitleTruncated(columnIndex));
3112
- 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;
3113
3791
  const previousHover = hoveredHeaderActionRef.current;
3114
3792
  if (previousHover?.columnIndex !== nextHover?.columnIndex || previousHover?.action !== nextHover?.action) {
3115
3793
  hoveredHeaderActionRef.current = nextHover;
3116
3794
  setHoveredHeaderAction(nextHover);
3117
- showHeaderTooltipAfterDelay(nextHover);
3795
+ showHeaderTooltipAfterDelay(nextTooltip);
3118
3796
  scheduleDraw();
3119
3797
  }
3120
3798
  if (inHeader) {
@@ -3138,8 +3816,8 @@ function Table({
3138
3816
  if (annotation?.content.trim()) {
3139
3817
  const cellLeft = getDisplayedColumnLeft(cell.columnIndex);
3140
3818
  const cellTop = bodyTop + cell.rowIndex * rowHeight - scrollRef.current.top;
3141
- 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 };
3142
- 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);
3143
3821
  } else {
3144
3822
  const label = getCellLabel(cell.rowIndex, cell.columnIndex);
3145
3823
  const context = event.currentTarget.getContext("2d");
@@ -3166,7 +3844,7 @@ function Table({
3166
3844
  showCellTooltipAfterDelay(null);
3167
3845
  } else {
3168
3846
  const placement = textRight + 330 > viewport.width ? "left" : "right";
3169
- const nextTooltip = {
3847
+ const nextTooltip2 = {
3170
3848
  rowIndex: cell.rowIndex,
3171
3849
  columnIndex: cell.columnIndex,
3172
3850
  left: placement === "right" ? textRight + 16 : Math.max(8, textLeft - 16),
@@ -3174,13 +3852,13 @@ function Table({
3174
3852
  label,
3175
3853
  placement
3176
3854
  };
3177
- showCellTooltipAfterDelay(nextTooltip);
3855
+ showCellTooltipAfterDelay(nextTooltip2);
3178
3856
  }
3179
3857
  }
3180
3858
  }
3181
3859
  }
3182
3860
  }
3183
- 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";
3184
3862
  if (event.currentTarget.style.cursor !== cursor) event.currentTarget.style.cursor = cursor;
3185
3863
  },
3186
3864
  onMouseLeave: (event) => {
@@ -3223,6 +3901,7 @@ function Table({
3223
3901
  const headerY = fixedHeader ? 0 : -scrollRef.current.top;
3224
3902
  if (localY >= headerY && localY < headerY + headerHeight) {
3225
3903
  clickedCellRef.current = null;
3904
+ if (localY < headerY + headerLeafTop) return;
3226
3905
  const columnIndex = locateColumn(event.clientX);
3227
3906
  const action = locateHeaderAction(event.clientX, columnIndex);
3228
3907
  if (action === "sort") {
@@ -3284,21 +3963,30 @@ function Table({
3284
3963
  className: "rvg-text-scroll-content",
3285
3964
  style: { left: -fixedWidth, width: contentWidth, transform: `translateX(${-currentScroll.left}px)` },
3286
3965
  children: [
3287
- 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
+ }),
3288
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) => {
3289
3973
  if (column.fixed || columnIndex < domRange.columnStart || columnIndex >= domRange.columnEnd) return null;
3290
- return renderCellText(rowIndex, columnIndex, metrics[columnIndex].left);
3974
+ return renderCellText(rowIndex, columnIndex, getScrollableColumnLeft(columnIndex));
3291
3975
  })) })
3292
3976
  ]
3293
3977
  }
3294
3978
  ) }),
3295
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) => {
3296
3980
  if (!column.fixed) return null;
3297
- 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;
3298
3982
  return renderCellText(rowIndex, columnIndex, left);
3299
3983
  })) }),
3300
- columns.map((column, columnIndex) => column.fixed === "left" ? renderHeaderTitle(columnIndex, metrics[columnIndex].left) : null),
3301
- 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 } })
3302
3990
  ] }),
3303
3991
  /* @__PURE__ */ jsx7("div", { className: "rvg-spacer", style: { width: contentWidth, height: contentHeight + bodyTop, marginTop: -renderHeight } })
3304
3992
  ] }),
@@ -3306,18 +3994,18 @@ function Table({
3306
3994
  "div",
3307
3995
  {
3308
3996
  className: `rvg-summary-row is-${summaryPosition}${hasVerticalBorders ? "" : " is-vertical-borderless"}`,
3309
- style: { height: rowHeight, top: summaryPosition === "top" ? headerHeight : void 0 },
3997
+ style: { height: rowHeight, top: summaryPosition === "top" ? fixedHeader ? headerHeight : headerHeight - currentScroll.top : void 0 },
3310
3998
  children: [
3311
3999
  /* @__PURE__ */ jsx7("div", { className: "rvg-summary-scroll-clip", style: { left: fixedWidth, right: rightFixedWidth }, children: /* @__PURE__ */ jsx7(
3312
4000
  "div",
3313
4001
  {
3314
4002
  className: "rvg-summary-scroll-content",
3315
4003
  style: { left: -fixedWidth, width: contentWidth, transform: `translateX(${-currentScroll.left}px)` },
3316
- 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)))
3317
4005
  }
3318
4006
  ) }),
3319
4007
  /* @__PURE__ */ jsxs7("div", { className: "rvg-summary-fixed-layer", children: [
3320
- 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),
3321
4009
  columns.map((column, columnIndex) => column.fixed === "right" ? renderSummaryCell(columnIndex, viewport.width - (rightFixedOffsets.get(columnIndex) ?? 0) - metrics[columnIndex].width) : null)
3322
4010
  ] }),
3323
4011
  fixedWidth > 0 && currentScroll.left > 0 && /* @__PURE__ */ jsx7("div", { className: "rvg-summary-fixed-shadow is-left", style: { left: fixedWidth } }),
@@ -3334,11 +4022,11 @@ function Table({
3334
4022
  ref: scrollingHeaderIconsRef,
3335
4023
  className: "rvg-header-icon-layer rvg-header-icon-layer-scroll",
3336
4024
  style: { left: -fixedWidth, width: contentWidth, transform: `translateX(${-scrollRef.current.left}px)` },
3337
- 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))
3338
4026
  }
3339
4027
  ) }),
3340
4028
  /* @__PURE__ */ jsxs7("div", { className: "rvg-header-icon-layer", children: [
3341
- 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),
3342
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)
3343
4031
  ] })
3344
4032
  ] }),
@@ -3615,7 +4303,42 @@ function Table({
3615
4303
  ] })
3616
4304
  ] }) }),
3617
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
+ ),
3618
4334
  /* @__PURE__ */ jsx7("div", { ref: dragGuideRef, className: "rvg-column-guide" }),
4335
+ resizeGuideX !== null && /* @__PURE__ */ jsx7(
4336
+ "div",
4337
+ {
4338
+ className: "rvg-column-guide is-resizing",
4339
+ style: { display: "block", top: resizeGuideTop, transform: `translateX(${Math.round(resizeGuideX)}px)` }
4340
+ }
4341
+ ),
3619
4342
  /* @__PURE__ */ jsx7("div", { ref: rowDragGuideRef, className: "rvg-row-guide" }),
3620
4343
  filterEditor && /* @__PURE__ */ jsxs7("div", { ref: filterRef, className: "rvg-filter", style: { left: filterEditor.left, top: filterEditor.top }, children: [
3621
4344
  /* @__PURE__ */ jsx7(