@bmsuisse/datagrid 0.23.2 → 0.24.0

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
@@ -192,7 +192,8 @@ var buttonVariants = cva(
192
192
  var Button = forwardRef(
193
193
  ({ className, variant, size, asChild = false, ...props }, ref) => {
194
194
  const Comp = asChild ? Slot : "button";
195
- return /* @__PURE__ */ jsx(Comp, { className: cn(buttonVariants({ variant, size, className })), ref, ...props });
195
+ const variantClass = buttonVariants({ variant, size });
196
+ return /* @__PURE__ */ jsx(Comp, { className: className ? cn(variantClass, className) : variantClass, ref, ...props });
196
197
  }
197
198
  );
198
199
  Button.displayName = "Button";
@@ -337,7 +338,13 @@ function BooleanFilter({
337
338
  "aria-label": `Filter ${column.header}`,
338
339
  "data-testid": `filter-${column.id}`,
339
340
  children: [
340
- /* @__PURE__ */ jsx4(FunnelIcon, { className: "h-3.5 w-3.5 shrink-0 opacity-70", "aria-hidden": true }),
341
+ /* @__PURE__ */ jsx4(
342
+ FunnelIcon,
343
+ {
344
+ className: cn("h-3.5 w-3.5 shrink-0", isFiltered ? "opacity-100 text-primary" : "opacity-40"),
345
+ "aria-hidden": true
346
+ }
347
+ ),
341
348
  isFiltered && /* @__PURE__ */ jsx4("span", { className: "truncate", children: summary })
342
349
  ]
343
350
  }
@@ -591,7 +598,13 @@ function EnumFilter({
591
598
  "aria-label": `Filter ${column.header}`,
592
599
  "data-testid": `filter-${column.id}`,
593
600
  children: [
594
- /* @__PURE__ */ jsx9(FunnelIcon2, { className: "h-3.5 w-3.5 shrink-0 opacity-70", "aria-hidden": true }),
601
+ /* @__PURE__ */ jsx9(
602
+ FunnelIcon2,
603
+ {
604
+ className: cn("h-3.5 w-3.5 shrink-0", isFiltered ? "opacity-100 text-primary" : "opacity-40"),
605
+ "aria-hidden": true
606
+ }
607
+ ),
595
608
  isFiltered && /* @__PURE__ */ jsx9("span", { className: "truncate", children: summary })
596
609
  ]
597
610
  }
@@ -648,6 +661,15 @@ function toNumber(value) {
648
661
  }
649
662
  return null;
650
663
  }
664
+ var currencyFormatters = /* @__PURE__ */ new Map();
665
+ function currencyFormatter(currency) {
666
+ let formatter = currencyFormatters.get(currency);
667
+ if (!formatter) {
668
+ formatter = new Intl.NumberFormat(void 0, { style: "currency", currency });
669
+ currencyFormatters.set(currency, formatter);
670
+ }
671
+ return formatter;
672
+ }
651
673
  function defaultFormat(column, value) {
652
674
  if (value === null || value === void 0) return "";
653
675
  switch (column.type) {
@@ -660,10 +682,7 @@ function defaultFormat(column, value) {
660
682
  case "currency": {
661
683
  const num = toNumber(value);
662
684
  if (num === null) return String(value);
663
- return new Intl.NumberFormat(void 0, {
664
- style: "currency",
665
- currency: column.currency ?? "USD"
666
- }).format(num);
685
+ return currencyFormatter(column.currency ?? "USD").format(num);
667
686
  }
668
687
  case "date": {
669
688
  const date = toDate(value);
@@ -870,7 +889,13 @@ function NumberComparisonFilter({
870
889
  "aria-label": `Filter ${column.header}`,
871
890
  "data-testid": `filter-${column.id}`,
872
891
  children: [
873
- /* @__PURE__ */ jsx10(FunnelIcon3, { className: "h-3.5 w-3.5 shrink-0 opacity-70", "aria-hidden": true }),
892
+ /* @__PURE__ */ jsx10(
893
+ FunnelIcon3,
894
+ {
895
+ className: cn("h-3.5 w-3.5 shrink-0", isFiltered ? "opacity-100 text-primary" : "opacity-40"),
896
+ "aria-hidden": true
897
+ }
898
+ ),
874
899
  isFiltered && /* @__PURE__ */ jsx10("span", { className: "truncate", children: summary })
875
900
  ]
876
901
  }
@@ -964,7 +989,13 @@ function NumberRangeFilter({
964
989
  "aria-label": `Filter ${column.header}`,
965
990
  "data-testid": `filter-${column.id}`,
966
991
  children: [
967
- /* @__PURE__ */ jsx11(FunnelIcon4, { className: "h-3.5 w-3.5 shrink-0 opacity-70", "aria-hidden": true }),
992
+ /* @__PURE__ */ jsx11(
993
+ FunnelIcon4,
994
+ {
995
+ className: cn("h-3.5 w-3.5 shrink-0", isFiltered ? "opacity-100 text-primary" : "opacity-40"),
996
+ "aria-hidden": true
997
+ }
998
+ ),
968
999
  isFiltered && /* @__PURE__ */ jsx11("span", { className: "truncate", children: summary })
969
1000
  ]
970
1001
  }
@@ -1177,7 +1208,13 @@ function NumberHistogramFilter({
1177
1208
  "aria-label": `Filter ${column.header}`,
1178
1209
  "data-testid": `filter-${column.id}`,
1179
1210
  children: [
1180
- /* @__PURE__ */ jsx12(FunnelIcon5, { className: "h-3.5 w-3.5 shrink-0 opacity-70", "aria-hidden": true }),
1211
+ /* @__PURE__ */ jsx12(
1212
+ FunnelIcon5,
1213
+ {
1214
+ className: cn("h-3.5 w-3.5 shrink-0", isFiltered ? "opacity-100 text-primary" : "opacity-40"),
1215
+ "aria-hidden": true
1216
+ }
1217
+ ),
1181
1218
  isFiltered && /* @__PURE__ */ jsx12("span", { className: "truncate", children: summary })
1182
1219
  ]
1183
1220
  }
@@ -1293,7 +1330,13 @@ function StringFilter({
1293
1330
  "aria-label": `Filter ${column.header}`,
1294
1331
  "data-testid": `filter-${column.id}`,
1295
1332
  children: [
1296
- /* @__PURE__ */ jsx13(FunnelIcon6, { className: "h-3.5 w-3.5 shrink-0 opacity-70", "aria-hidden": true }),
1333
+ /* @__PURE__ */ jsx13(
1334
+ FunnelIcon6,
1335
+ {
1336
+ className: cn("h-3.5 w-3.5 shrink-0", isFiltered ? "opacity-100 text-primary" : "opacity-40"),
1337
+ "aria-hidden": true
1338
+ }
1339
+ ),
1297
1340
  isFiltered && /* @__PURE__ */ jsx13("span", { className: "truncate", children: text })
1298
1341
  ]
1299
1342
  }
@@ -1508,6 +1551,7 @@ function resolveMenuItems(items, ctx) {
1508
1551
 
1509
1552
  // src/menu/ActionsMenu.tsx
1510
1553
  import { MoreVertical } from "lucide-react";
1554
+ import { useMemo as useMemo3 } from "react";
1511
1555
 
1512
1556
  // src/components/ui/dropdown-menu.tsx
1513
1557
  import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
@@ -1563,7 +1607,10 @@ DropdownMenuLabel.displayName = "DropdownMenuLabel";
1563
1607
  // src/menu/ActionsMenu.tsx
1564
1608
  import { jsx as jsx18, jsxs as jsxs11 } from "react/jsx-runtime";
1565
1609
  function ActionsMenu({ items, ctx, triggerLabel }) {
1566
- const resolved = resolveMenuItems(items, ctx);
1610
+ const resolved = useMemo3(
1611
+ () => resolveMenuItems(items, ctx),
1612
+ [items, ctx.row, ctx.selectedRows]
1613
+ );
1567
1614
  if (resolved.length === 0) return null;
1568
1615
  return /* @__PURE__ */ jsxs11(DropdownMenu, { children: [
1569
1616
  /* @__PURE__ */ jsx18(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsx18(Button, { variant: "ghost", size: "icon", "aria-label": triggerLabel, onClick: stopRowClick, children: /* @__PURE__ */ jsx18(MoreVertical, { className: "h-4 w-4", "aria-hidden": true }) }) }),
@@ -1585,7 +1632,22 @@ import { BarsArrowDownIcon, BarsArrowUpIcon, FunnelIcon as FunnelIcon7 } from "@
1585
1632
  import { columnResizingFeature, columnSizingFeature, flexRender, tableFeatures, useTable } from "@tanstack/react-table";
1586
1633
  import { useVirtualizer } from "@tanstack/react-virtual";
1587
1634
  import { ChevronRight, Loader2 } from "lucide-react";
1588
- import { Fragment as Fragment2, useEffect as useEffect7, useMemo as useMemo5, useRef as useRef5, useState as useState6 } from "react";
1635
+ import { Fragment as Fragment2, useEffect as useEffect7, useMemo as useMemo6, useRef as useRef5, useState as useState6 } from "react";
1636
+
1637
+ // src/lib/structuralColumns.ts
1638
+ var SELECTION_COLUMN_WIDTH = 44;
1639
+
1640
+ // src/grid/groupRows.ts
1641
+ function groupRows(items, groupBy) {
1642
+ const buckets = /* @__PURE__ */ new Map();
1643
+ for (const item of items) {
1644
+ const key = groupBy(item);
1645
+ const bucket = buckets.get(key);
1646
+ if (bucket) bucket.push(item);
1647
+ else buckets.set(key, [item]);
1648
+ }
1649
+ return Array.from(buckets, ([key, groupItems]) => ({ key, items: groupItems }));
1650
+ }
1589
1651
 
1590
1652
  // src/grid/processClientData.ts
1591
1653
  function makeComparator(sort) {
@@ -1613,10 +1675,10 @@ function processClientData(data, state) {
1613
1675
  }
1614
1676
 
1615
1677
  // src/grid/useGridState.ts
1616
- import { useCallback as useCallback2, useEffect as useEffect6, useMemo as useMemo4, useState as useState5 } from "react";
1678
+ import { useCallback as useCallback2, useEffect as useEffect6, useMemo as useMemo5, useState as useState5 } from "react";
1617
1679
 
1618
1680
  // src/hooks/useDebouncedCallback.ts
1619
- import { useEffect as useEffect5, useMemo as useMemo3, useRef as useRef4 } from "react";
1681
+ import { useEffect as useEffect5, useMemo as useMemo4, useRef as useRef4 } from "react";
1620
1682
  function useDebouncedCallback(callback, delayMs) {
1621
1683
  const callbackRef = useRef4(callback);
1622
1684
  callbackRef.current = callback;
@@ -1626,7 +1688,7 @@ function useDebouncedCallback(callback, delayMs) {
1626
1688
  clearTimeout(timeoutRef.current);
1627
1689
  };
1628
1690
  }, []);
1629
- return useMemo3(() => {
1691
+ return useMemo4(() => {
1630
1692
  return {
1631
1693
  run: (...args) => {
1632
1694
  clearTimeout(timeoutRef.current);
@@ -1676,7 +1738,7 @@ function useGridState(dataSource, initialState, externalState) {
1676
1738
  cancelDebouncedNotify();
1677
1739
  setState(externalState);
1678
1740
  }, [externalState, cancelDebouncedNotify]);
1679
- const filtersByColumn = useMemo4(() => {
1741
+ const filtersByColumn = useMemo5(() => {
1680
1742
  const map = /* @__PURE__ */ new Map();
1681
1743
  for (const child of state.filter?.filters ?? []) {
1682
1744
  if ("operator" in child) map.set(fieldKey(child.field), child);
@@ -1744,8 +1806,8 @@ import { Fragment as Fragment3, jsx as jsx19, jsxs as jsxs12 } from "react/jsx-r
1744
1806
  var gridTableFeatures = tableFeatures({ columnSizingFeature, columnResizingFeature });
1745
1807
  var DEFAULT_COLUMN_SIZE = 150;
1746
1808
  var DETAIL_COLUMN_WIDTH = 44;
1747
- var SELECTION_COLUMN_WIDTH = 44;
1748
1809
  var ROW_ACTIONS_COLUMN_WIDTH = 44;
1810
+ var STRUCTURAL_ZEBRA_BG_CLASS = "bg-[color-mix(in_srgb,var(--color-foreground)_5%,var(--color-background))]";
1749
1811
  function pinnedOffsets(visibleColumns, side, getSize, leadingOffset) {
1750
1812
  const offsets = /* @__PURE__ */ new Map();
1751
1813
  let cursor = leadingOffset;
@@ -1802,7 +1864,12 @@ function DataGrid({
1802
1864
  selectedIds: controlledSelectedIds,
1803
1865
  onSelectedIdsChange,
1804
1866
  testId,
1805
- loading: loadingProp = false
1867
+ loading: loadingProp = false,
1868
+ groupBy,
1869
+ renderGroupHeader,
1870
+ defaultGroupsExpanded = true,
1871
+ expandedGroups: controlledExpandedGroups,
1872
+ onExpandedGroupsChange
1806
1873
  }) {
1807
1874
  const { state, filtersByColumn, setColumnFilter, toggleSort, setPage } = useGridState(
1808
1875
  dataSource,
@@ -1830,16 +1897,28 @@ function DataGrid({
1830
1897
  return next;
1831
1898
  });
1832
1899
  }
1833
- const visibleColumns = useMemo5(
1900
+ const [internalExpandedGroups, setInternalExpandedGroups] = useState6({});
1901
+ const expandedGroupsState = controlledExpandedGroups ?? internalExpandedGroups;
1902
+ function updateExpandedGroups(next) {
1903
+ setInternalExpandedGroups(next);
1904
+ onExpandedGroupsChange?.(next);
1905
+ }
1906
+ function isGroupExpanded(key) {
1907
+ return expandedGroupsState[key] ?? defaultGroupsExpanded;
1908
+ }
1909
+ function toggleGroupExpanded(key) {
1910
+ updateExpandedGroups({ ...expandedGroupsState, [key]: !isGroupExpanded(key) });
1911
+ }
1912
+ const visibleColumns = useMemo6(
1834
1913
  () => columnVisibility ? columns.filter((column) => isColumnVisible(columnVisibility, column.id)) : columns,
1835
1914
  [columns, columnVisibility]
1836
1915
  );
1837
1916
  const serverRowCount = dataSource.mode === "server" ? dataSource.rowCount : void 0;
1838
- const { rows, rowCount } = useMemo5(() => {
1917
+ const { rows, rowCount } = useMemo6(() => {
1839
1918
  if (dataSource.mode === "client") return processClientData(dataSource.data, state);
1840
1919
  return { rows: dataSource.data, rowCount: serverRowCount ?? 0 };
1841
1920
  }, [dataSource.mode, dataSource.data, serverRowCount, state]);
1842
- const selectedRows = useMemo5(
1921
+ const selectedRows = useMemo6(
1843
1922
  () => rows.filter((row) => selectedIds.has(getRowId(row))),
1844
1923
  [rows, selectedIds, getRowId]
1845
1924
  );
@@ -1858,8 +1937,8 @@ function DataGrid({
1858
1937
  }
1859
1938
  updateSelectedIds(next);
1860
1939
  }
1861
- const tanstackColumns = useMemo5(() => toTanstackColumns(visibleColumns), [visibleColumns]);
1862
- const columnById = useMemo5(() => {
1940
+ const tanstackColumns = useMemo6(() => toTanstackColumns(visibleColumns), [visibleColumns]);
1941
+ const columnById = useMemo6(() => {
1863
1942
  const map = /* @__PURE__ */ new Map();
1864
1943
  for (const column of visibleColumns) map.set(column.id, column);
1865
1944
  return map;
@@ -1884,7 +1963,7 @@ function DataGrid({
1884
1963
  const showDetailColumn = Boolean(renderDetail);
1885
1964
  const totalColumnCount = visibleColumns.length + (showDetailColumn ? 1 : 0) + (showSelectionColumn ? 1 : 0) + (showRowActionsColumn ? 1 : 0);
1886
1965
  const hasFilterRow = visibleColumns.some((column) => isFilterable(column) && column.filterDisplay === "row");
1887
- const headerRuns = useMemo5(() => computeHeaderRuns(visibleColumns), [visibleColumns]);
1966
+ const headerRuns = useMemo6(() => computeHeaderRuns(visibleColumns), [visibleColumns]);
1888
1967
  const hasHeaderGroups = visibleColumns.some((column) => column.headerGroup);
1889
1968
  function renderFilterWidget(column) {
1890
1969
  const value = filtersByColumn.get(column.id);
@@ -1894,22 +1973,34 @@ function DataGrid({
1894
1973
  const columnSize = (columnId) => table.getColumn(columnId)?.getSize() ?? DEFAULT_COLUMN_SIZE;
1895
1974
  const leadingStructuralWidth = (showDetailColumn ? DETAIL_COLUMN_WIDTH : 0) + (showSelectionColumn ? SELECTION_COLUMN_WIDTH : 0);
1896
1975
  const trailingStructuralWidth = showRowActionsColumn ? ROW_ACTIONS_COLUMN_WIDTH : 0;
1897
- const leftPinnedOffsets = pinnedOffsets(visibleColumns, "left", columnSize, leadingStructuralWidth);
1898
- const rightPinnedOffsets = pinnedOffsets(visibleColumns, "right", columnSize, trailingStructuralWidth);
1976
+ const leftPinnedOffsets = useMemo6(
1977
+ () => pinnedOffsets(visibleColumns, "left", columnSize, leadingStructuralWidth),
1978
+ [visibleColumns, columnSizing, leadingStructuralWidth]
1979
+ );
1980
+ const rightPinnedOffsets = useMemo6(
1981
+ () => pinnedOffsets(visibleColumns, "right", columnSize, trailingStructuralWidth),
1982
+ [visibleColumns, columnSizing, trailingStructuralWidth]
1983
+ );
1899
1984
  function pinnedCellProps(column, area = "body") {
1900
1985
  const bg = area === "header" ? "bg-muted" : "bg-background";
1901
1986
  if (column.pinned === "left") {
1902
- return { className: cn("sticky z-10", bg), style: { left: leftPinnedOffsets.get(column.id), width: columnSize(column.id) } };
1987
+ return { className: `sticky z-10 ${bg}`, style: { left: leftPinnedOffsets.get(column.id), width: columnSize(column.id) } };
1903
1988
  }
1904
1989
  if (column.pinned === "right") {
1905
- return { className: cn("sticky z-10", bg), style: { right: rightPinnedOffsets.get(column.id), width: columnSize(column.id) } };
1990
+ return { className: `sticky z-10 ${bg}`, style: { right: rightPinnedOffsets.get(column.id), width: columnSize(column.id) } };
1906
1991
  }
1907
1992
  if (enableColumnResizing) return { style: { width: columnSize(column.id) } };
1908
1993
  return { style: column.width ? { width: column.width } : void 0 };
1909
1994
  }
1910
1995
  function structuralCellProps(side, offset, width, area = "body") {
1996
+ const base = "border-b border-border p-1 sticky z-10";
1911
1997
  const bg = area === "header" ? "bg-muted" : "bg-background";
1912
- return { className: cn("sticky z-10", bg), style: { [side]: offset, width } };
1998
+ const bgOdd = area === "header" ? "bg-muted" : STRUCTURAL_ZEBRA_BG_CLASS;
1999
+ return {
2000
+ className: `${base} ${bg}`,
2001
+ classNameOdd: `${base} ${bgOdd}`,
2002
+ style: { [side]: offset, width }
2003
+ };
1913
2004
  }
1914
2005
  const detailCellProps = structuralCellProps("left", 0, DETAIL_COLUMN_WIDTH);
1915
2006
  const selectionCellProps = structuralCellProps(
@@ -1926,8 +2017,45 @@ function DataGrid({
1926
2017
  "header"
1927
2018
  );
1928
2019
  const rowActionsHeaderCellProps = structuralCellProps("right", 0, ROW_ACTIONS_COLUMN_WIDTH, "header");
2020
+ const BODY_TD_BASE_CLASS = "border-b border-border p-2";
2021
+ function bodyCellClassAndStyle(column) {
2022
+ const pinnedProps = column.pinned || enableColumnResizing ? pinnedCellProps(column) : void 0;
2023
+ return {
2024
+ className: pinnedProps?.className ? `${BODY_TD_BASE_CLASS} ${pinnedProps.className}` : BODY_TD_BASE_CLASS,
2025
+ style: pinnedProps?.style
2026
+ };
2027
+ }
2028
+ const bodyCellPropsByColumn = useMemo6(() => {
2029
+ const map = /* @__PURE__ */ new Map();
2030
+ for (const column of visibleColumns) map.set(column.id, bodyCellClassAndStyle(column));
2031
+ return map;
2032
+ }, [visibleColumns, enableColumnResizing, leftPinnedOffsets, rightPinnedOffsets, columnSizing]);
2033
+ function headerCellClassAndStyle(column, base) {
2034
+ const pinnedProps = pinnedCellProps(column, "header");
2035
+ const align = alignClassName(column);
2036
+ const className = pinnedProps.className ? `${base} ${align} ${pinnedProps.className}` : `${base} ${align}`;
2037
+ return { className, style: pinnedProps.style };
2038
+ }
2039
+ const leafHeaderCellPropsByColumn = useMemo6(() => {
2040
+ const map = /* @__PURE__ */ new Map();
2041
+ for (const column of visibleColumns) {
2042
+ map.set(column.id, headerCellClassAndStyle(column, "relative border-b border-border p-2 font-medium"));
2043
+ }
2044
+ return map;
2045
+ }, [visibleColumns, enableColumnResizing, leftPinnedOffsets, rightPinnedOffsets, columnSizing]);
2046
+ const filterHeaderCellPropsByColumn = useMemo6(() => {
2047
+ const map = /* @__PURE__ */ new Map();
2048
+ for (const column of visibleColumns) {
2049
+ map.set(column.id, headerCellClassAndStyle(column, "border-b border-border p-2 align-top font-normal"));
2050
+ }
2051
+ return map;
2052
+ }, [visibleColumns, enableColumnResizing, leftPinnedOffsets, rightPinnedOffsets, columnSizing]);
1929
2053
  const tableRows = table.getRowModel().rows;
1930
- const shouldVirtualize = Boolean(virtualize) && tableRows.length > (virtualize?.threshold ?? 100);
2054
+ const shouldVirtualize = Boolean(virtualize) && !groupBy && tableRows.length > (virtualize?.threshold ?? 100);
2055
+ const groupedBuckets = useMemo6(
2056
+ () => groupBy ? groupRows(tableRows, (row) => groupBy(row.original)) : void 0,
2057
+ [tableRows, groupBy]
2058
+ );
1931
2059
  const scrollRef = useRef5(null);
1932
2060
  const virtualizer = useVirtualizer({
1933
2061
  count: tableRows.length,
@@ -1959,45 +2087,97 @@ function DataGrid({
1959
2087
  {
1960
2088
  ...rowProps,
1961
2089
  "data-testid": rowTestId,
1962
- className: cn(rowProps?.className, row.index % 2 === 1 && "bg-foreground/5"),
2090
+ className: rowProps?.className ? cn("divide-x divide-border", rowProps.className, row.index % 2 === 1 && "bg-foreground/5") : `divide-x divide-border${row.index % 2 === 1 ? " bg-foreground/5" : ""}`,
1963
2091
  children: [
1964
- showDetailColumn && /* @__PURE__ */ jsx19("td", { className: cn("border-b p-1", detailCellProps.className), style: detailCellProps.style, children: /* @__PURE__ */ jsx19(
1965
- Button,
2092
+ showDetailColumn && /* @__PURE__ */ jsx19(
2093
+ "td",
1966
2094
  {
1967
- type: "button",
1968
- variant: "ghost",
1969
- size: "icon",
1970
- "aria-label": `${isExpanded ? "Collapse" : "Expand"} row ${row.id}`,
1971
- "aria-expanded": isExpanded,
1972
- onClick: (event) => {
1973
- stopRowClick(event);
1974
- toggleRowExpanded(row.id);
1975
- },
1976
- children: /* @__PURE__ */ jsx19(ChevronRight, { className: cn("h-4 w-4 transition-transform", isExpanded && "rotate-90"), "aria-hidden": true })
2095
+ className: row.index % 2 === 1 ? detailCellProps.classNameOdd : detailCellProps.className,
2096
+ style: detailCellProps.style,
2097
+ children: /* @__PURE__ */ jsx19(
2098
+ Button,
2099
+ {
2100
+ type: "button",
2101
+ variant: "ghost",
2102
+ size: "icon",
2103
+ "aria-label": `${isExpanded ? "Collapse" : "Expand"} row ${row.id}`,
2104
+ "aria-expanded": isExpanded,
2105
+ onClick: (event) => {
2106
+ stopRowClick(event);
2107
+ toggleRowExpanded(row.id);
2108
+ },
2109
+ children: /* @__PURE__ */ jsx19(
2110
+ ChevronRight,
2111
+ {
2112
+ className: `h-4 w-4 transition-transform${isExpanded ? " rotate-90" : ""}`,
2113
+ "aria-hidden": true
2114
+ }
2115
+ )
2116
+ }
2117
+ )
1977
2118
  }
1978
- ) }),
1979
- showSelectionColumn && /* @__PURE__ */ jsx19("td", { className: cn("border-b p-1", selectionCellProps.className), style: selectionCellProps.style, children: /* @__PURE__ */ jsx19(
1980
- "input",
2119
+ ),
2120
+ showSelectionColumn && /* @__PURE__ */ jsx19(
2121
+ "td",
1981
2122
  {
1982
- type: "checkbox",
1983
- "aria-label": `Select row ${row.id}`,
1984
- checked: selectedIds.has(row.id),
1985
- onClick: stopRowClick,
1986
- onChange: () => toggleRowSelected(row.id)
2123
+ className: row.index % 2 === 1 ? selectionCellProps.classNameOdd : selectionCellProps.className,
2124
+ style: selectionCellProps.style,
2125
+ children: /* @__PURE__ */ jsx19(
2126
+ "input",
2127
+ {
2128
+ type: "checkbox",
2129
+ "aria-label": `Select row ${row.id}`,
2130
+ checked: selectedIds.has(row.id),
2131
+ onClick: stopRowClick,
2132
+ onChange: () => toggleRowSelected(row.id)
2133
+ }
2134
+ )
1987
2135
  }
1988
- ) }),
2136
+ ),
1989
2137
  row.getAllCells().map((cell) => {
1990
- const cellColumn = columnById.get(cell.column.id);
1991
- const pinnedProps = cellColumn && (cellColumn.pinned || enableColumnResizing) ? pinnedCellProps(cellColumn) : void 0;
1992
- return /* @__PURE__ */ jsx19("td", { style: pinnedProps?.style, className: cn("border-b p-2", pinnedProps?.className), children: flexRender(cell.column.columnDef.cell, cell.getContext()) }, cell.id);
2138
+ const cellProps = bodyCellPropsByColumn.get(cell.column.id);
2139
+ return /* @__PURE__ */ jsx19("td", { style: cellProps?.style, className: cellProps?.className ?? BODY_TD_BASE_CLASS, children: flexRender(cell.column.columnDef.cell, cell.getContext()) }, cell.id);
1993
2140
  }),
1994
- showRowActionsColumn && rowActions && /* @__PURE__ */ jsx19("td", { className: cn("border-b p-1", rowActionsCellProps.className), style: rowActionsCellProps.style, children: /* @__PURE__ */ jsx19(ActionsMenu, { items: rowActions, ctx: { row: row.original }, triggerLabel: `Row actions for ${row.id}` }) })
2141
+ showRowActionsColumn && rowActions && /* @__PURE__ */ jsx19(
2142
+ "td",
2143
+ {
2144
+ className: row.index % 2 === 1 ? rowActionsCellProps.classNameOdd : rowActionsCellProps.className,
2145
+ style: rowActionsCellProps.style,
2146
+ children: /* @__PURE__ */ jsx19(ActionsMenu, { items: rowActions, ctx: { row: row.original }, triggerLabel: `Row actions for ${row.id}` })
2147
+ }
2148
+ )
1995
2149
  ]
1996
2150
  }
1997
2151
  ),
1998
- showDetailColumn && isExpanded && renderDetail && /* @__PURE__ */ jsx19("tr", { "data-testid": `${rowTestId}-detail`, children: /* @__PURE__ */ jsx19("td", { colSpan: totalColumnCount, className: "border-b p-2", children: renderDetail(row.original) }) })
2152
+ showDetailColumn && isExpanded && renderDetail && /* @__PURE__ */ jsx19("tr", { "data-testid": `${rowTestId}-detail`, children: /* @__PURE__ */ jsx19("td", { colSpan: totalColumnCount, className: "border-b border-border p-2", children: renderDetail(row.original) }) })
1999
2153
  ] }, row.id);
2000
2154
  }
2155
+ function renderGroupedBucket(bucket) {
2156
+ const expanded = isGroupExpanded(bucket.key);
2157
+ const originalRows = bucket.items.map((row) => row.original);
2158
+ return /* @__PURE__ */ jsxs12(Fragment2, { children: [
2159
+ /* @__PURE__ */ jsx19("tbody", { children: /* @__PURE__ */ jsx19("tr", { "data-testid": `group-header-${bucket.key}`, children: /* @__PURE__ */ jsx19("td", { colSpan: totalColumnCount, className: "border-b bg-muted/50 p-2 font-medium", children: /* @__PURE__ */ jsxs12(
2160
+ "button",
2161
+ {
2162
+ type: "button",
2163
+ className: "flex w-full items-center gap-1 text-left",
2164
+ onClick: () => toggleGroupExpanded(bucket.key),
2165
+ "aria-expanded": expanded,
2166
+ children: [
2167
+ /* @__PURE__ */ jsx19(
2168
+ ChevronRight,
2169
+ {
2170
+ className: `h-4 w-4 shrink-0 transition-transform${expanded ? " rotate-90" : ""}`,
2171
+ "aria-hidden": true
2172
+ }
2173
+ ),
2174
+ renderGroupHeader ? renderGroupHeader(bucket.key, originalRows, expanded) : `${bucket.key} (${originalRows.length})`
2175
+ ]
2176
+ }
2177
+ ) }) }) }),
2178
+ expanded && bucket.items.map((row) => renderRow(row))
2179
+ ] }, `group-${bucket.key}`);
2180
+ }
2001
2181
  return /* @__PURE__ */ jsxs12("div", { className: "flex flex-col gap-2", "data-testid": "datagrid", children: [
2002
2182
  showSelectionColumn && headerActions && /* @__PURE__ */ jsxs12("div", { className: "flex items-center justify-between text-sm", children: [
2003
2183
  /* @__PURE__ */ jsxs12("span", { children: [
@@ -2032,58 +2212,58 @@ function DataGrid({
2032
2212
  const sortEntry = state.sort.find((s) => s.field === column.id);
2033
2213
  const sortable = isSortable(column);
2034
2214
  const filterable = isFilterable(column) && column.filterDisplay !== "row";
2035
- const pinnedProps = pinnedCellProps(column, "header");
2036
- return /* @__PURE__ */ jsxs12(
2037
- "th",
2038
- {
2039
- rowSpan,
2040
- style: pinnedProps.style,
2041
- className: cn("relative border-b p-2 font-medium", alignClassName(column), pinnedProps.className),
2042
- children: [
2043
- /* @__PURE__ */ jsxs12("div", { className: "flex items-center gap-1", children: [
2044
- /* @__PURE__ */ jsxs12(
2045
- "button",
2046
- {
2047
- type: "button",
2048
- className: cn(
2049
- "flex items-center gap-1 rounded-sm px-1",
2050
- sortable && "hover:bg-accent hover:text-accent-foreground"
2051
- ),
2052
- disabled: !sortable,
2053
- onClick: (event) => sortable && toggleSort(column.id, enableMultiSort && event.shiftKey, column.sortDescFirst === true),
2054
- children: [
2055
- column.renderHeader ? column.renderHeader(column) : column.header,
2056
- sortable && sortEntry?.dir === "asc" && /* @__PURE__ */ jsx19(BarsArrowUpIcon, { className: "h-3 w-3", "aria-hidden": true }),
2057
- sortable && sortEntry?.dir === "desc" && /* @__PURE__ */ jsx19(BarsArrowDownIcon, { className: "h-3 w-3", "aria-hidden": true })
2058
- ]
2059
- }
2215
+ const cellProps = leafHeaderCellPropsByColumn.get(column.id);
2216
+ return /* @__PURE__ */ jsxs12("th", { rowSpan, style: cellProps?.style, className: cellProps?.className, children: [
2217
+ /* @__PURE__ */ jsxs12("div", { className: "flex items-center gap-1", children: [
2218
+ /* @__PURE__ */ jsxs12(
2219
+ "button",
2220
+ {
2221
+ type: "button",
2222
+ className: cn(
2223
+ "flex items-center gap-1 rounded-sm px-1",
2224
+ sortable && "hover:bg-accent hover:text-accent-foreground"
2060
2225
  ),
2061
- filterable && /* @__PURE__ */ jsxs12(Popover, { children: [
2062
- /* @__PURE__ */ jsx19(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx19(Button, { variant: "ghost", size: "icon", "aria-label": `Filter ${column.header}`, children: /* @__PURE__ */ jsx19(FunnelIcon7, { className: "h-3 w-3", "aria-hidden": true }) }) }),
2063
- /* @__PURE__ */ jsx19(PopoverContent, { children: renderFilterWidget(column) })
2064
- ] })
2065
- ] }),
2066
- enableColumnResizing && /* @__PURE__ */ jsx19(
2067
- "div",
2226
+ disabled: !sortable,
2227
+ onClick: (event) => sortable && toggleSort(column.id, enableMultiSort && event.shiftKey, column.sortDescFirst === true),
2228
+ children: [
2229
+ column.renderHeader ? column.renderHeader(column) : column.header,
2230
+ sortable && sortEntry?.dir === "asc" && /* @__PURE__ */ jsx19(BarsArrowUpIcon, { className: "h-3 w-3", "aria-hidden": true }),
2231
+ sortable && sortEntry?.dir === "desc" && /* @__PURE__ */ jsx19(BarsArrowDownIcon, { className: "h-3 w-3", "aria-hidden": true })
2232
+ ]
2233
+ }
2234
+ ),
2235
+ filterable && /* @__PURE__ */ jsxs12(Popover, { children: [
2236
+ /* @__PURE__ */ jsx19(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx19(Button, { variant: "ghost", size: "icon", "aria-label": `Filter ${column.header}`, children: /* @__PURE__ */ jsx19(
2237
+ FunnelIcon7,
2068
2238
  {
2069
- "data-testid": `resize-handle-${column.id}`,
2070
- onMouseDown: header.getResizeHandler(),
2071
- onTouchStart: header.getResizeHandler(),
2072
- className: "absolute right-0 top-0 h-full w-1 cursor-col-resize touch-none select-none hover:bg-accent"
2239
+ className: cn(
2240
+ "h-3 w-3",
2241
+ filtersByColumn.get(column.id) !== void 0 ? "opacity-100 text-primary" : "opacity-40"
2242
+ ),
2243
+ "aria-hidden": true
2073
2244
  }
2074
- )
2075
- ]
2076
- },
2077
- header.id
2078
- );
2245
+ ) }) }),
2246
+ /* @__PURE__ */ jsx19(PopoverContent, { children: renderFilterWidget(column) })
2247
+ ] })
2248
+ ] }),
2249
+ enableColumnResizing && /* @__PURE__ */ jsx19(
2250
+ "div",
2251
+ {
2252
+ "data-testid": `resize-handle-${column.id}`,
2253
+ onMouseDown: header.getResizeHandler(),
2254
+ onTouchStart: header.getResizeHandler(),
2255
+ className: "absolute right-0 top-0 h-full w-1 cursor-col-resize touch-none select-none hover:bg-accent"
2256
+ }
2257
+ )
2258
+ ] }, header.id);
2079
2259
  }
2080
2260
  return /* @__PURE__ */ jsxs12(Fragment2, { children: [
2081
- hasHeaderGroups && /* @__PURE__ */ jsxs12("tr", { "data-testid": "header-group-row", children: [
2261
+ hasHeaderGroups && /* @__PURE__ */ jsxs12("tr", { "data-testid": "header-group-row", className: "divide-x divide-border", children: [
2082
2262
  showDetailColumn && /* @__PURE__ */ jsx19(
2083
2263
  "th",
2084
2264
  {
2085
2265
  rowSpan: 2,
2086
- className: cn("border-b p-1", detailHeaderCellProps.className),
2266
+ className: detailHeaderCellProps.className,
2087
2267
  style: detailHeaderCellProps.style,
2088
2268
  "aria-hidden": true
2089
2269
  }
@@ -2092,7 +2272,7 @@ function DataGrid({
2092
2272
  "th",
2093
2273
  {
2094
2274
  rowSpan: 2,
2095
- className: cn("border-b p-1", selectionHeaderCellProps.className),
2275
+ className: selectionHeaderCellProps.className,
2096
2276
  style: selectionHeaderCellProps.style,
2097
2277
  children: /* @__PURE__ */ jsx19(
2098
2278
  "input",
@@ -2110,7 +2290,7 @@ function DataGrid({
2110
2290
  "th",
2111
2291
  {
2112
2292
  colSpan: run.columns.length,
2113
- className: "border-b p-2 text-center font-medium",
2293
+ className: "border-b border-border p-2 text-center font-medium",
2114
2294
  children: run.label
2115
2295
  },
2116
2296
  `group-${run.label}-${run.columns[0].id}`
@@ -2120,17 +2300,17 @@ function DataGrid({
2120
2300
  "th",
2121
2301
  {
2122
2302
  rowSpan: 2,
2123
- className: cn("border-b p-1", rowActionsHeaderCellProps.className),
2303
+ className: rowActionsHeaderCellProps.className,
2124
2304
  style: rowActionsHeaderCellProps.style,
2125
2305
  "aria-label": "Row actions"
2126
2306
  }
2127
2307
  )
2128
2308
  ] }, `${headerGroup.id}-groups`),
2129
- /* @__PURE__ */ jsxs12("tr", { children: [
2309
+ /* @__PURE__ */ jsxs12("tr", { className: "divide-x divide-border", children: [
2130
2310
  !hasHeaderGroups && showDetailColumn && /* @__PURE__ */ jsx19(
2131
2311
  "th",
2132
2312
  {
2133
- className: cn("border-b p-1", detailHeaderCellProps.className),
2313
+ className: detailHeaderCellProps.className,
2134
2314
  style: detailHeaderCellProps.style,
2135
2315
  "aria-hidden": true
2136
2316
  }
@@ -2138,7 +2318,7 @@ function DataGrid({
2138
2318
  !hasHeaderGroups && showSelectionColumn && /* @__PURE__ */ jsx19(
2139
2319
  "th",
2140
2320
  {
2141
- className: cn("border-b p-1", selectionHeaderCellProps.className),
2321
+ className: selectionHeaderCellProps.className,
2142
2322
  style: selectionHeaderCellProps.style,
2143
2323
  children: /* @__PURE__ */ jsx19(
2144
2324
  "input",
@@ -2159,7 +2339,7 @@ function DataGrid({
2159
2339
  !hasHeaderGroups && showRowActionsColumn && /* @__PURE__ */ jsx19(
2160
2340
  "th",
2161
2341
  {
2162
- className: cn("border-b p-1", rowActionsHeaderCellProps.className),
2342
+ className: rowActionsHeaderCellProps.className,
2163
2343
  style: rowActionsHeaderCellProps.style,
2164
2344
  "aria-label": "Row actions"
2165
2345
  }
@@ -2167,31 +2347,23 @@ function DataGrid({
2167
2347
  ] }, headerGroup.id)
2168
2348
  ] }, headerGroup.id);
2169
2349
  }),
2170
- hasFilterRow && /* @__PURE__ */ jsxs12("tr", { "data-testid": "filter-row", children: [
2171
- showDetailColumn && /* @__PURE__ */ jsx19("th", { className: cn("border-b p-1", detailHeaderCellProps.className), style: detailHeaderCellProps.style }),
2350
+ hasFilterRow && /* @__PURE__ */ jsxs12("tr", { "data-testid": "filter-row", className: "divide-x divide-border", children: [
2351
+ showDetailColumn && /* @__PURE__ */ jsx19("th", { className: detailHeaderCellProps.className, style: detailHeaderCellProps.style }),
2172
2352
  showSelectionColumn && /* @__PURE__ */ jsx19(
2173
2353
  "th",
2174
2354
  {
2175
- className: cn("border-b p-1", selectionHeaderCellProps.className),
2355
+ className: selectionHeaderCellProps.className,
2176
2356
  style: selectionHeaderCellProps.style
2177
2357
  }
2178
2358
  ),
2179
2359
  visibleColumns.map((column) => {
2180
- const pinnedProps = pinnedCellProps(column, "header");
2181
- return /* @__PURE__ */ jsx19(
2182
- "th",
2183
- {
2184
- style: pinnedProps.style,
2185
- className: cn("border-b p-2 align-top font-normal", alignClassName(column), pinnedProps.className),
2186
- children: isFilterable(column) && column.filterDisplay === "row" ? renderFilterWidget(column) : null
2187
- },
2188
- column.id
2189
- );
2360
+ const cellProps = filterHeaderCellPropsByColumn.get(column.id);
2361
+ return /* @__PURE__ */ jsx19("th", { style: cellProps?.style, className: cellProps?.className, children: isFilterable(column) && column.filterDisplay === "row" ? renderFilterWidget(column) : null }, column.id);
2190
2362
  }),
2191
2363
  showRowActionsColumn && /* @__PURE__ */ jsx19(
2192
2364
  "th",
2193
2365
  {
2194
- className: cn("border-b p-1", rowActionsHeaderCellProps.className),
2366
+ className: rowActionsHeaderCellProps.className,
2195
2367
  style: rowActionsHeaderCellProps.style
2196
2368
  }
2197
2369
  )
@@ -2203,7 +2375,7 @@ function DataGrid({
2203
2375
  (virtualItem) => renderRow(tableRows[virtualItem.index], (el) => virtualizer.measureElement(el))
2204
2376
  ),
2205
2377
  paddingBottom > 0 && /* @__PURE__ */ jsx19("tbody", { children: /* @__PURE__ */ jsx19("tr", { "aria-hidden": "true", children: /* @__PURE__ */ jsx19("td", { colSpan: totalColumnCount, style: { padding: 0, border: "none", height: paddingBottom } }) }) })
2206
- ] }) : tableRows.map((row) => renderRow(row))
2378
+ ] }) : groupedBuckets ? groupedBuckets.map((bucket) => renderGroupedBucket(bucket)) : tableRows.map((row) => renderRow(row))
2207
2379
  ] })
2208
2380
  }
2209
2381
  )
@@ -2249,7 +2421,7 @@ function DataGrid({
2249
2421
  // src/tree/TreeDataGrid.tsx
2250
2422
  import { useVirtualizer as useVirtualizer2 } from "@tanstack/react-virtual";
2251
2423
  import { AlertCircle, ChevronDown as ChevronDown2, ChevronRight as ChevronRight2, Loader2 as Loader22 } from "lucide-react";
2252
- import { useEffect as useEffect8, useMemo as useMemo7, useRef as useRef6 } from "react";
2424
+ import { useEffect as useEffect8, useMemo as useMemo8, useRef as useRef6 } from "react";
2253
2425
 
2254
2426
  // src/tree/flattenTree.ts
2255
2427
  function flattenTree(roots, accessors, expanded, childrenOverride) {
@@ -2268,8 +2440,33 @@ function flattenTree(roots, accessors, expanded, childrenOverride) {
2268
2440
  return out;
2269
2441
  }
2270
2442
 
2443
+ // src/tree/selectionState.ts
2444
+ function computeSelectionStates(roots, accessors, childrenOverride, selectedIds) {
2445
+ const { getRowId, getChildren } = accessors;
2446
+ const out = /* @__PURE__ */ new Map();
2447
+ function visit(row) {
2448
+ const id = getRowId(row);
2449
+ if (selectedIds.has(id)) {
2450
+ const state2 = { checked: true, indeterminate: false };
2451
+ out.set(id, state2);
2452
+ return state2;
2453
+ }
2454
+ const children = childrenOverride.get(id) ?? getChildren(row) ?? [];
2455
+ let anySelectedOrIndeterminate = false;
2456
+ for (const child of children) {
2457
+ const childState = visit(child);
2458
+ if (childState.checked || childState.indeterminate) anySelectedOrIndeterminate = true;
2459
+ }
2460
+ const state = { checked: false, indeterminate: anySelectedOrIndeterminate };
2461
+ out.set(id, state);
2462
+ return state;
2463
+ }
2464
+ for (const root of roots) visit(root);
2465
+ return out;
2466
+ }
2467
+
2271
2468
  // src/tree/useTreeState.ts
2272
- import { useCallback as useCallback3, useMemo as useMemo6, useState as useState7 } from "react";
2469
+ import { useCallback as useCallback3, useMemo as useMemo7, useState as useState7 } from "react";
2273
2470
  function useTreeState({
2274
2471
  data,
2275
2472
  getRowId,
@@ -2294,7 +2491,7 @@ function useTreeState({
2294
2491
  const [childrenMap, setChildrenMap] = useState7(/* @__PURE__ */ new Map());
2295
2492
  const [loadingIds, setLoadingIds] = useState7(/* @__PURE__ */ new Set());
2296
2493
  const [errorIds, setErrorIds] = useState7(/* @__PURE__ */ new Map());
2297
- const resolveHasChildren = useMemo6(
2494
+ const resolveHasChildren = useMemo7(
2298
2495
  () => hasChildren ?? ((row) => (getChildren(row)?.length ?? 0) > 0),
2299
2496
  [hasChildren, getChildren]
2300
2497
  );
@@ -2374,6 +2571,8 @@ function useTreeState({
2374
2571
 
2375
2572
  // src/tree/TreeDataGrid.tsx
2376
2573
  import { Fragment as Fragment4, jsx as jsx20, jsxs as jsxs13 } from "react/jsx-runtime";
2574
+ var EMPTY_SELECTED_IDS = /* @__PURE__ */ new Set();
2575
+ var UNCHECKED_STATE = { checked: false, indeterminate: false };
2377
2576
  function renderCell(column, row) {
2378
2577
  const value = getColumnValue(column, row);
2379
2578
  return column.cell ? column.cell(value, row) : defaultFormat(column, value);
@@ -2428,9 +2627,13 @@ function TreeDataGrid({
2428
2627
  rowActions,
2429
2628
  virtualizeThreshold = 100,
2430
2629
  estimatedRowHeight = 40,
2431
- maxBodyHeight = 480
2630
+ maxBodyHeight = 480,
2631
+ selectedIds: controlledSelectedIds,
2632
+ onSelectedIdsChange,
2633
+ getRowSelectionState,
2634
+ isRowSelectionDisabled
2432
2635
  }) {
2433
- const accessors = useMemo7(
2636
+ const accessors = useMemo8(
2434
2637
  () => ({ getRowId, getChildren, hasChildren }),
2435
2638
  [getRowId, getChildren, hasChildren]
2436
2639
  );
@@ -2449,13 +2652,49 @@ function TreeDataGrid({
2449
2652
  didAutoExpand.current = true;
2450
2653
  if (initialExpandedLevel > 0) void expandToLevel(initialExpandedLevel);
2451
2654
  }, []);
2452
- const flatRows = useMemo7(
2655
+ const flatRows = useMemo8(
2453
2656
  () => flattenTree(data, accessors, expanded, childrenMap),
2454
2657
  [data, accessors, expanded, childrenMap]
2455
2658
  );
2456
2659
  const treeColId = treeColumnId ?? columns[0]?.id;
2457
2660
  const showRowActionsColumn = Boolean(rowActions?.length);
2458
- const totalColumnCount = columns.length + (showRowActionsColumn ? 1 : 0);
2661
+ const showSelectionColumn = controlledSelectedIds !== void 0;
2662
+ const totalColumnCount = columns.length + (showRowActionsColumn ? 1 : 0) + (showSelectionColumn ? 1 : 0);
2663
+ const selectionStates = useMemo8(
2664
+ () => showSelectionColumn ? computeSelectionStates(data, accessors, childrenMap, controlledSelectedIds ?? EMPTY_SELECTED_IDS) : void 0,
2665
+ [showSelectionColumn, data, accessors, childrenMap, controlledSelectedIds]
2666
+ );
2667
+ function resolveSelectionState(row, id) {
2668
+ const override = getRowSelectionState?.(row);
2669
+ if (override) return { checked: override.checked, indeterminate: override.indeterminate ?? false };
2670
+ return selectionStates?.get(id) ?? UNCHECKED_STATE;
2671
+ }
2672
+ function toggleRowSelected(id) {
2673
+ if (!controlledSelectedIds) return;
2674
+ const next = new Set(controlledSelectedIds);
2675
+ if (next.has(id)) next.delete(id);
2676
+ else next.add(id);
2677
+ onSelectedIdsChange?.(next);
2678
+ }
2679
+ const selectableFlatRows = useMemo8(
2680
+ () => flatRows.filter((flatRow) => !(isRowSelectionDisabled?.(flatRow.row) ?? false)),
2681
+ [flatRows, isRowSelectionDisabled]
2682
+ );
2683
+ const allVisibleSelected = showSelectionColumn && selectableFlatRows.length > 0 && selectableFlatRows.every((flatRow) => controlledSelectedIds.has(flatRow.id));
2684
+ function toggleAllVisibleSelected() {
2685
+ if (!controlledSelectedIds) return;
2686
+ const next = new Set(controlledSelectedIds);
2687
+ for (const flatRow of selectableFlatRows) {
2688
+ if (allVisibleSelected) next.delete(flatRow.id);
2689
+ else next.add(flatRow.id);
2690
+ }
2691
+ onSelectedIdsChange?.(next);
2692
+ }
2693
+ const bodyTdClassByColumn = useMemo8(() => {
2694
+ const map = /* @__PURE__ */ new Map();
2695
+ for (const column of columns) map.set(column.id, `p-2 ${alignClassName(column)}`);
2696
+ return map;
2697
+ }, [columns]);
2459
2698
  const scrollRef = useRef6(null);
2460
2699
  const shouldVirtualize = flatRows.length > virtualizeThreshold;
2461
2700
  const virtualizer = useVirtualizer2({
@@ -2474,9 +2713,26 @@ function TreeDataGrid({
2474
2713
  ref: measureRef,
2475
2714
  ...rowProps,
2476
2715
  "data-testid": rowTestId,
2477
- className: cn("border-b", rowProps?.className, index % 2 === 1 && "bg-foreground/5"),
2716
+ className: rowProps?.className ? cn(
2717
+ "border-b border-border divide-x divide-border",
2718
+ rowProps.className,
2719
+ index % 2 === 1 && "bg-foreground/5"
2720
+ ) : `border-b border-border divide-x divide-border${index % 2 === 1 ? " bg-foreground/5" : ""}`,
2478
2721
  children: [
2479
- columns.map((column) => /* @__PURE__ */ jsx20("td", { className: cn("p-2", alignClassName(column)), children: column.id === treeColId ? /* @__PURE__ */ jsx20(
2722
+ showSelectionColumn && /* @__PURE__ */ jsx20("td", { className: "p-1", style: { width: SELECTION_COLUMN_WIDTH }, children: (() => {
2723
+ const state = resolveSelectionState(flatRow.row, flatRow.id);
2724
+ return /* @__PURE__ */ jsx20(
2725
+ Checkbox,
2726
+ {
2727
+ "aria-label": `Select row ${flatRow.id}`,
2728
+ checked: state.indeterminate ? "indeterminate" : state.checked,
2729
+ disabled: isRowSelectionDisabled?.(flatRow.row) ?? false,
2730
+ onCheckedChange: () => toggleRowSelected(flatRow.id),
2731
+ onClick: stopRowClick
2732
+ }
2733
+ );
2734
+ })() }),
2735
+ columns.map((column) => /* @__PURE__ */ jsx20("td", { className: bodyTdClassByColumn.get(column.id), children: column.id === treeColId ? /* @__PURE__ */ jsx20(
2480
2736
  TreeCell,
2481
2737
  {
2482
2738
  flatRow,
@@ -2511,17 +2767,25 @@ function TreeDataGrid({
2511
2767
  className: shouldVirtualize ? "overflow-y-auto" : void 0,
2512
2768
  style: shouldVirtualize ? { maxHeight: maxBodyHeight } : void 0,
2513
2769
  children: /* @__PURE__ */ jsxs13("table", { className: "w-full border-collapse text-sm", children: [
2514
- /* @__PURE__ */ jsx20("thead", { children: /* @__PURE__ */ jsxs13("tr", { children: [
2770
+ /* @__PURE__ */ jsx20("thead", { children: /* @__PURE__ */ jsxs13("tr", { className: "divide-x divide-border", children: [
2771
+ showSelectionColumn && /* @__PURE__ */ jsx20("th", { className: "border-b border-border p-1", style: { width: SELECTION_COLUMN_WIDTH }, children: /* @__PURE__ */ jsx20(
2772
+ Checkbox,
2773
+ {
2774
+ "aria-label": "Select all visible rows",
2775
+ checked: allVisibleSelected,
2776
+ onCheckedChange: toggleAllVisibleSelected
2777
+ }
2778
+ ) }),
2515
2779
  columns.map((column) => /* @__PURE__ */ jsx20(
2516
2780
  "th",
2517
2781
  {
2518
2782
  style: column.width ? { width: column.width } : void 0,
2519
- className: cn("border-b p-2 font-medium", alignClassName(column)),
2783
+ className: `border-b border-border p-2 font-medium ${alignClassName(column)}`,
2520
2784
  children: column.header
2521
2785
  },
2522
2786
  column.id
2523
2787
  )),
2524
- showRowActionsColumn && /* @__PURE__ */ jsx20("th", { className: "border-b p-2", "aria-label": "Row actions" })
2788
+ showRowActionsColumn && /* @__PURE__ */ jsx20("th", { className: "border-b border-border p-2", "aria-label": "Row actions" })
2525
2789
  ] }) }),
2526
2790
  /* @__PURE__ */ jsx20("tbody", { children: flatRows.length === 0 ? /* @__PURE__ */ jsx20("tr", { children: /* @__PURE__ */ jsx20("td", { colSpan: totalColumnCount, className: "p-4 text-center text-muted-foreground", children: "No results." }) }) : shouldVirtualize ? /* @__PURE__ */ jsxs13(Fragment4, { children: [
2527
2791
  paddingTop > 0 && /* @__PURE__ */ jsx20("tr", { "aria-hidden": "true", children: /* @__PURE__ */ jsx20("td", { colSpan: totalColumnCount, style: { padding: 0, border: "none", height: paddingTop } }) }),