@bmsuisse/datagrid 0.23.4 → 0.24.1

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
@@ -338,7 +338,13 @@ function BooleanFilter({
338
338
  "aria-label": `Filter ${column.header}`,
339
339
  "data-testid": `filter-${column.id}`,
340
340
  children: [
341
- /* @__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
+ ),
342
348
  isFiltered && /* @__PURE__ */ jsx4("span", { className: "truncate", children: summary })
343
349
  ]
344
350
  }
@@ -592,7 +598,13 @@ function EnumFilter({
592
598
  "aria-label": `Filter ${column.header}`,
593
599
  "data-testid": `filter-${column.id}`,
594
600
  children: [
595
- /* @__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
+ ),
596
608
  isFiltered && /* @__PURE__ */ jsx9("span", { className: "truncate", children: summary })
597
609
  ]
598
610
  }
@@ -877,7 +889,13 @@ function NumberComparisonFilter({
877
889
  "aria-label": `Filter ${column.header}`,
878
890
  "data-testid": `filter-${column.id}`,
879
891
  children: [
880
- /* @__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
+ ),
881
899
  isFiltered && /* @__PURE__ */ jsx10("span", { className: "truncate", children: summary })
882
900
  ]
883
901
  }
@@ -971,7 +989,13 @@ function NumberRangeFilter({
971
989
  "aria-label": `Filter ${column.header}`,
972
990
  "data-testid": `filter-${column.id}`,
973
991
  children: [
974
- /* @__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
+ ),
975
999
  isFiltered && /* @__PURE__ */ jsx11("span", { className: "truncate", children: summary })
976
1000
  ]
977
1001
  }
@@ -1184,7 +1208,13 @@ function NumberHistogramFilter({
1184
1208
  "aria-label": `Filter ${column.header}`,
1185
1209
  "data-testid": `filter-${column.id}`,
1186
1210
  children: [
1187
- /* @__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
+ ),
1188
1218
  isFiltered && /* @__PURE__ */ jsx12("span", { className: "truncate", children: summary })
1189
1219
  ]
1190
1220
  }
@@ -1300,7 +1330,13 @@ function StringFilter({
1300
1330
  "aria-label": `Filter ${column.header}`,
1301
1331
  "data-testid": `filter-${column.id}`,
1302
1332
  children: [
1303
- /* @__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
+ ),
1304
1340
  isFiltered && /* @__PURE__ */ jsx13("span", { className: "truncate", children: text })
1305
1341
  ]
1306
1342
  }
@@ -1598,6 +1634,21 @@ import { useVirtualizer } from "@tanstack/react-virtual";
1598
1634
  import { ChevronRight, Loader2 } from "lucide-react";
1599
1635
  import { Fragment as Fragment2, useEffect as useEffect7, useMemo as useMemo6, useRef as useRef5, useState as useState6 } from "react";
1600
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
+ }
1651
+
1601
1652
  // src/grid/processClientData.ts
1602
1653
  function makeComparator(sort) {
1603
1654
  return (a, b) => {
@@ -1755,8 +1806,8 @@ import { Fragment as Fragment3, jsx as jsx19, jsxs as jsxs12 } from "react/jsx-r
1755
1806
  var gridTableFeatures = tableFeatures({ columnSizingFeature, columnResizingFeature });
1756
1807
  var DEFAULT_COLUMN_SIZE = 150;
1757
1808
  var DETAIL_COLUMN_WIDTH = 44;
1758
- var SELECTION_COLUMN_WIDTH = 44;
1759
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))]";
1760
1811
  function pinnedOffsets(visibleColumns, side, getSize, leadingOffset) {
1761
1812
  const offsets = /* @__PURE__ */ new Map();
1762
1813
  let cursor = leadingOffset;
@@ -1813,7 +1864,12 @@ function DataGrid({
1813
1864
  selectedIds: controlledSelectedIds,
1814
1865
  onSelectedIdsChange,
1815
1866
  testId,
1816
- loading: loadingProp = false
1867
+ loading: loadingProp = false,
1868
+ groupBy,
1869
+ renderGroupHeader,
1870
+ defaultGroupsExpanded = true,
1871
+ expandedGroups: controlledExpandedGroups,
1872
+ onExpandedGroupsChange
1817
1873
  }) {
1818
1874
  const { state, filtersByColumn, setColumnFilter, toggleSort, setPage } = useGridState(
1819
1875
  dataSource,
@@ -1841,6 +1897,18 @@ function DataGrid({
1841
1897
  return next;
1842
1898
  });
1843
1899
  }
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
+ }
1844
1912
  const visibleColumns = useMemo6(
1845
1913
  () => columnVisibility ? columns.filter((column) => isColumnVisible(columnVisibility, column.id)) : columns,
1846
1914
  [columns, columnVisibility]
@@ -1925,8 +1993,14 @@ function DataGrid({
1925
1993
  return { style: column.width ? { width: column.width } : void 0 };
1926
1994
  }
1927
1995
  function structuralCellProps(side, offset, width, area = "body") {
1996
+ const base = "border-b border-border p-1 sticky z-10";
1928
1997
  const bg = area === "header" ? "bg-muted" : "bg-background";
1929
- return { className: `border-b p-1 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
+ };
1930
2004
  }
1931
2005
  const detailCellProps = structuralCellProps("left", 0, DETAIL_COLUMN_WIDTH);
1932
2006
  const selectionCellProps = structuralCellProps(
@@ -1943,7 +2017,7 @@ function DataGrid({
1943
2017
  "header"
1944
2018
  );
1945
2019
  const rowActionsHeaderCellProps = structuralCellProps("right", 0, ROW_ACTIONS_COLUMN_WIDTH, "header");
1946
- const BODY_TD_BASE_CLASS = "border-b p-2";
2020
+ const BODY_TD_BASE_CLASS = "border-b border-border p-2";
1947
2021
  function bodyCellClassAndStyle(column) {
1948
2022
  const pinnedProps = column.pinned || enableColumnResizing ? pinnedCellProps(column) : void 0;
1949
2023
  return {
@@ -1965,19 +2039,23 @@ function DataGrid({
1965
2039
  const leafHeaderCellPropsByColumn = useMemo6(() => {
1966
2040
  const map = /* @__PURE__ */ new Map();
1967
2041
  for (const column of visibleColumns) {
1968
- map.set(column.id, headerCellClassAndStyle(column, "relative border-b p-2 font-medium"));
2042
+ map.set(column.id, headerCellClassAndStyle(column, "relative border-b border-border p-2 font-medium"));
1969
2043
  }
1970
2044
  return map;
1971
2045
  }, [visibleColumns, enableColumnResizing, leftPinnedOffsets, rightPinnedOffsets, columnSizing]);
1972
2046
  const filterHeaderCellPropsByColumn = useMemo6(() => {
1973
2047
  const map = /* @__PURE__ */ new Map();
1974
2048
  for (const column of visibleColumns) {
1975
- map.set(column.id, headerCellClassAndStyle(column, "border-b p-2 align-top font-normal"));
2049
+ map.set(column.id, headerCellClassAndStyle(column, "border-b border-border p-2 align-top font-normal"));
1976
2050
  }
1977
2051
  return map;
1978
2052
  }, [visibleColumns, enableColumnResizing, leftPinnedOffsets, rightPinnedOffsets, columnSizing]);
1979
2053
  const tableRows = table.getRowModel().rows;
1980
- 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
+ );
1981
2059
  const scrollRef = useRef5(null);
1982
2060
  const virtualizer = useVirtualizer({
1983
2061
  count: tableRows.length,
@@ -2009,50 +2087,97 @@ function DataGrid({
2009
2087
  {
2010
2088
  ...rowProps,
2011
2089
  "data-testid": rowTestId,
2012
- className: rowProps?.className ? cn(rowProps.className, row.index % 2 === 1 && "bg-foreground/5") : row.index % 2 === 1 ? "bg-foreground/5" : void 0,
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" : ""}`,
2013
2091
  children: [
2014
- showDetailColumn && /* @__PURE__ */ jsx19("td", { className: detailCellProps.className, style: detailCellProps.style, children: /* @__PURE__ */ jsx19(
2015
- Button,
2092
+ showDetailColumn && /* @__PURE__ */ jsx19(
2093
+ "td",
2016
2094
  {
2017
- type: "button",
2018
- variant: "ghost",
2019
- size: "icon",
2020
- "aria-label": `${isExpanded ? "Collapse" : "Expand"} row ${row.id}`,
2021
- "aria-expanded": isExpanded,
2022
- onClick: (event) => {
2023
- stopRowClick(event);
2024
- toggleRowExpanded(row.id);
2025
- },
2095
+ className: row.index % 2 === 1 ? detailCellProps.classNameOdd : detailCellProps.className,
2096
+ style: detailCellProps.style,
2026
2097
  children: /* @__PURE__ */ jsx19(
2027
- ChevronRight,
2098
+ Button,
2028
2099
  {
2029
- className: `h-4 w-4 transition-transform${isExpanded ? " rotate-90" : ""}`,
2030
- "aria-hidden": true
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
+ )
2031
2116
  }
2032
2117
  )
2033
2118
  }
2034
- ) }),
2035
- showSelectionColumn && /* @__PURE__ */ jsx19("td", { className: selectionCellProps.className, style: selectionCellProps.style, children: /* @__PURE__ */ jsx19(
2036
- "input",
2119
+ ),
2120
+ showSelectionColumn && /* @__PURE__ */ jsx19(
2121
+ "td",
2037
2122
  {
2038
- type: "checkbox",
2039
- "aria-label": `Select row ${row.id}`,
2040
- checked: selectedIds.has(row.id),
2041
- onClick: stopRowClick,
2042
- 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
+ )
2043
2135
  }
2044
- ) }),
2136
+ ),
2045
2137
  row.getAllCells().map((cell) => {
2046
2138
  const cellProps = bodyCellPropsByColumn.get(cell.column.id);
2047
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);
2048
2140
  }),
2049
- showRowActionsColumn && rowActions && /* @__PURE__ */ jsx19("td", { className: 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
+ )
2050
2149
  ]
2051
2150
  }
2052
2151
  ),
2053
- 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) }) })
2054
2153
  ] }, row.id);
2055
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
+ }
2056
2181
  return /* @__PURE__ */ jsxs12("div", { className: "flex flex-col gap-2", "data-testid": "datagrid", children: [
2057
2182
  showSelectionColumn && headerActions && /* @__PURE__ */ jsxs12("div", { className: "flex items-center justify-between text-sm", children: [
2058
2183
  /* @__PURE__ */ jsxs12("span", { children: [
@@ -2088,27 +2213,40 @@ function DataGrid({
2088
2213
  const sortable = isSortable(column);
2089
2214
  const filterable = isFilterable(column) && column.filterDisplay !== "row";
2090
2215
  const cellProps = leafHeaderCellPropsByColumn.get(column.id);
2216
+ const headerContent = column.renderHeader ? column.renderHeader(column) : column.header;
2091
2217
  return /* @__PURE__ */ jsxs12("th", { rowSpan, style: cellProps?.style, className: cellProps?.className, children: [
2092
2218
  /* @__PURE__ */ jsxs12("div", { className: "flex items-center gap-1", children: [
2093
- /* @__PURE__ */ jsxs12(
2219
+ sortable ? /* @__PURE__ */ jsxs12(
2094
2220
  "button",
2095
2221
  {
2096
2222
  type: "button",
2097
- className: cn(
2098
- "flex items-center gap-1 rounded-sm px-1",
2099
- sortable && "hover:bg-accent hover:text-accent-foreground"
2100
- ),
2101
- disabled: !sortable,
2102
- onClick: (event) => sortable && toggleSort(column.id, enableMultiSort && event.shiftKey, column.sortDescFirst === true),
2223
+ className: "flex items-center gap-1 rounded-sm px-1 hover:bg-accent hover:text-accent-foreground",
2224
+ onClick: (event) => toggleSort(column.id, enableMultiSort && event.shiftKey, column.sortDescFirst === true),
2103
2225
  children: [
2104
- column.renderHeader ? column.renderHeader(column) : column.header,
2105
- sortable && sortEntry?.dir === "asc" && /* @__PURE__ */ jsx19(BarsArrowUpIcon, { className: "h-3 w-3", "aria-hidden": true }),
2106
- sortable && sortEntry?.dir === "desc" && /* @__PURE__ */ jsx19(BarsArrowDownIcon, { className: "h-3 w-3", "aria-hidden": true })
2226
+ headerContent,
2227
+ sortEntry?.dir === "asc" && /* @__PURE__ */ jsx19(BarsArrowUpIcon, { className: "h-3 w-3", "aria-hidden": true }),
2228
+ sortEntry?.dir === "desc" && /* @__PURE__ */ jsx19(BarsArrowDownIcon, { className: "h-3 w-3", "aria-hidden": true })
2107
2229
  ]
2108
2230
  }
2231
+ ) : (
2232
+ // Not wrapped in a `<button>` (unlike the sortable branch above): a
2233
+ // disabled `<button>` swallows pointer events for its entire subtree,
2234
+ // silently breaking any interactive content a column's own `renderHeader`
2235
+ // embeds directly (bare filter inputs, "select/reject all" buttons) —
2236
+ // clicks never reach them at all, even though nothing looks wrong visually.
2237
+ /* @__PURE__ */ jsx19("div", { className: "flex items-center gap-1 px-1", children: headerContent })
2109
2238
  ),
2110
2239
  filterable && /* @__PURE__ */ jsxs12(Popover, { children: [
2111
- /* @__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 }) }) }),
2240
+ /* @__PURE__ */ jsx19(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx19(Button, { variant: "ghost", size: "icon", "aria-label": `Filter ${column.header}`, children: /* @__PURE__ */ jsx19(
2241
+ FunnelIcon7,
2242
+ {
2243
+ className: cn(
2244
+ "h-3 w-3",
2245
+ filtersByColumn.get(column.id) !== void 0 ? "opacity-100 text-primary" : "opacity-40"
2246
+ ),
2247
+ "aria-hidden": true
2248
+ }
2249
+ ) }) }),
2112
2250
  /* @__PURE__ */ jsx19(PopoverContent, { children: renderFilterWidget(column) })
2113
2251
  ] })
2114
2252
  ] }),
@@ -2124,7 +2262,7 @@ function DataGrid({
2124
2262
  ] }, header.id);
2125
2263
  }
2126
2264
  return /* @__PURE__ */ jsxs12(Fragment2, { children: [
2127
- hasHeaderGroups && /* @__PURE__ */ jsxs12("tr", { "data-testid": "header-group-row", children: [
2265
+ hasHeaderGroups && /* @__PURE__ */ jsxs12("tr", { "data-testid": "header-group-row", className: "divide-x divide-border", children: [
2128
2266
  showDetailColumn && /* @__PURE__ */ jsx19(
2129
2267
  "th",
2130
2268
  {
@@ -2156,7 +2294,7 @@ function DataGrid({
2156
2294
  "th",
2157
2295
  {
2158
2296
  colSpan: run.columns.length,
2159
- className: "border-b p-2 text-center font-medium",
2297
+ className: "border-b border-border p-2 text-center font-medium",
2160
2298
  children: run.label
2161
2299
  },
2162
2300
  `group-${run.label}-${run.columns[0].id}`
@@ -2172,7 +2310,7 @@ function DataGrid({
2172
2310
  }
2173
2311
  )
2174
2312
  ] }, `${headerGroup.id}-groups`),
2175
- /* @__PURE__ */ jsxs12("tr", { children: [
2313
+ /* @__PURE__ */ jsxs12("tr", { className: "divide-x divide-border", children: [
2176
2314
  !hasHeaderGroups && showDetailColumn && /* @__PURE__ */ jsx19(
2177
2315
  "th",
2178
2316
  {
@@ -2213,7 +2351,7 @@ function DataGrid({
2213
2351
  ] }, headerGroup.id)
2214
2352
  ] }, headerGroup.id);
2215
2353
  }),
2216
- hasFilterRow && /* @__PURE__ */ jsxs12("tr", { "data-testid": "filter-row", children: [
2354
+ hasFilterRow && /* @__PURE__ */ jsxs12("tr", { "data-testid": "filter-row", className: "divide-x divide-border", children: [
2217
2355
  showDetailColumn && /* @__PURE__ */ jsx19("th", { className: detailHeaderCellProps.className, style: detailHeaderCellProps.style }),
2218
2356
  showSelectionColumn && /* @__PURE__ */ jsx19(
2219
2357
  "th",
@@ -2241,7 +2379,7 @@ function DataGrid({
2241
2379
  (virtualItem) => renderRow(tableRows[virtualItem.index], (el) => virtualizer.measureElement(el))
2242
2380
  ),
2243
2381
  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 } }) }) })
2244
- ] }) : tableRows.map((row) => renderRow(row))
2382
+ ] }) : groupedBuckets ? groupedBuckets.map((bucket) => renderGroupedBucket(bucket)) : tableRows.map((row) => renderRow(row))
2245
2383
  ] })
2246
2384
  }
2247
2385
  )
@@ -2306,6 +2444,31 @@ function flattenTree(roots, accessors, expanded, childrenOverride) {
2306
2444
  return out;
2307
2445
  }
2308
2446
 
2447
+ // src/tree/selectionState.ts
2448
+ function computeSelectionStates(roots, accessors, childrenOverride, selectedIds) {
2449
+ const { getRowId, getChildren } = accessors;
2450
+ const out = /* @__PURE__ */ new Map();
2451
+ function visit(row) {
2452
+ const id = getRowId(row);
2453
+ if (selectedIds.has(id)) {
2454
+ const state2 = { checked: true, indeterminate: false };
2455
+ out.set(id, state2);
2456
+ return state2;
2457
+ }
2458
+ const children = childrenOverride.get(id) ?? getChildren(row) ?? [];
2459
+ let anySelectedOrIndeterminate = false;
2460
+ for (const child of children) {
2461
+ const childState = visit(child);
2462
+ if (childState.checked || childState.indeterminate) anySelectedOrIndeterminate = true;
2463
+ }
2464
+ const state = { checked: false, indeterminate: anySelectedOrIndeterminate };
2465
+ out.set(id, state);
2466
+ return state;
2467
+ }
2468
+ for (const root of roots) visit(root);
2469
+ return out;
2470
+ }
2471
+
2309
2472
  // src/tree/useTreeState.ts
2310
2473
  import { useCallback as useCallback3, useMemo as useMemo7, useState as useState7 } from "react";
2311
2474
  function useTreeState({
@@ -2412,6 +2575,8 @@ function useTreeState({
2412
2575
 
2413
2576
  // src/tree/TreeDataGrid.tsx
2414
2577
  import { Fragment as Fragment4, jsx as jsx20, jsxs as jsxs13 } from "react/jsx-runtime";
2578
+ var EMPTY_SELECTED_IDS = /* @__PURE__ */ new Set();
2579
+ var UNCHECKED_STATE = { checked: false, indeterminate: false };
2415
2580
  function renderCell(column, row) {
2416
2581
  const value = getColumnValue(column, row);
2417
2582
  return column.cell ? column.cell(value, row) : defaultFormat(column, value);
@@ -2466,7 +2631,11 @@ function TreeDataGrid({
2466
2631
  rowActions,
2467
2632
  virtualizeThreshold = 100,
2468
2633
  estimatedRowHeight = 40,
2469
- maxBodyHeight = 480
2634
+ maxBodyHeight = 480,
2635
+ selectedIds: controlledSelectedIds,
2636
+ onSelectedIdsChange,
2637
+ getRowSelectionState,
2638
+ isRowSelectionDisabled
2470
2639
  }) {
2471
2640
  const accessors = useMemo8(
2472
2641
  () => ({ getRowId, getChildren, hasChildren }),
@@ -2493,7 +2662,38 @@ function TreeDataGrid({
2493
2662
  );
2494
2663
  const treeColId = treeColumnId ?? columns[0]?.id;
2495
2664
  const showRowActionsColumn = Boolean(rowActions?.length);
2496
- const totalColumnCount = columns.length + (showRowActionsColumn ? 1 : 0);
2665
+ const showSelectionColumn = controlledSelectedIds !== void 0;
2666
+ const totalColumnCount = columns.length + (showRowActionsColumn ? 1 : 0) + (showSelectionColumn ? 1 : 0);
2667
+ const selectionStates = useMemo8(
2668
+ () => showSelectionColumn ? computeSelectionStates(data, accessors, childrenMap, controlledSelectedIds ?? EMPTY_SELECTED_IDS) : void 0,
2669
+ [showSelectionColumn, data, accessors, childrenMap, controlledSelectedIds]
2670
+ );
2671
+ function resolveSelectionState(row, id) {
2672
+ const override = getRowSelectionState?.(row);
2673
+ if (override) return { checked: override.checked, indeterminate: override.indeterminate ?? false };
2674
+ return selectionStates?.get(id) ?? UNCHECKED_STATE;
2675
+ }
2676
+ function toggleRowSelected(id) {
2677
+ if (!controlledSelectedIds) return;
2678
+ const next = new Set(controlledSelectedIds);
2679
+ if (next.has(id)) next.delete(id);
2680
+ else next.add(id);
2681
+ onSelectedIdsChange?.(next);
2682
+ }
2683
+ const selectableFlatRows = useMemo8(
2684
+ () => flatRows.filter((flatRow) => !(isRowSelectionDisabled?.(flatRow.row) ?? false)),
2685
+ [flatRows, isRowSelectionDisabled]
2686
+ );
2687
+ const allVisibleSelected = showSelectionColumn && selectableFlatRows.length > 0 && selectableFlatRows.every((flatRow) => controlledSelectedIds.has(flatRow.id));
2688
+ function toggleAllVisibleSelected() {
2689
+ if (!controlledSelectedIds) return;
2690
+ const next = new Set(controlledSelectedIds);
2691
+ for (const flatRow of selectableFlatRows) {
2692
+ if (allVisibleSelected) next.delete(flatRow.id);
2693
+ else next.add(flatRow.id);
2694
+ }
2695
+ onSelectedIdsChange?.(next);
2696
+ }
2497
2697
  const bodyTdClassByColumn = useMemo8(() => {
2498
2698
  const map = /* @__PURE__ */ new Map();
2499
2699
  for (const column of columns) map.set(column.id, `p-2 ${alignClassName(column)}`);
@@ -2517,8 +2717,25 @@ function TreeDataGrid({
2517
2717
  ref: measureRef,
2518
2718
  ...rowProps,
2519
2719
  "data-testid": rowTestId,
2520
- className: rowProps?.className ? cn("border-b", rowProps.className, index % 2 === 1 && "bg-foreground/5") : `border-b${index % 2 === 1 ? " bg-foreground/5" : ""}`,
2720
+ className: rowProps?.className ? cn(
2721
+ "border-b border-border divide-x divide-border",
2722
+ rowProps.className,
2723
+ index % 2 === 1 && "bg-foreground/5"
2724
+ ) : `border-b border-border divide-x divide-border${index % 2 === 1 ? " bg-foreground/5" : ""}`,
2521
2725
  children: [
2726
+ showSelectionColumn && /* @__PURE__ */ jsx20("td", { className: "p-1", style: { width: SELECTION_COLUMN_WIDTH }, children: (() => {
2727
+ const state = resolveSelectionState(flatRow.row, flatRow.id);
2728
+ return /* @__PURE__ */ jsx20(
2729
+ Checkbox,
2730
+ {
2731
+ "aria-label": `Select row ${flatRow.id}`,
2732
+ checked: state.indeterminate ? "indeterminate" : state.checked,
2733
+ disabled: isRowSelectionDisabled?.(flatRow.row) ?? false,
2734
+ onCheckedChange: () => toggleRowSelected(flatRow.id),
2735
+ onClick: stopRowClick
2736
+ }
2737
+ );
2738
+ })() }),
2522
2739
  columns.map((column) => /* @__PURE__ */ jsx20("td", { className: bodyTdClassByColumn.get(column.id), children: column.id === treeColId ? /* @__PURE__ */ jsx20(
2523
2740
  TreeCell,
2524
2741
  {
@@ -2554,17 +2771,25 @@ function TreeDataGrid({
2554
2771
  className: shouldVirtualize ? "overflow-y-auto" : void 0,
2555
2772
  style: shouldVirtualize ? { maxHeight: maxBodyHeight } : void 0,
2556
2773
  children: /* @__PURE__ */ jsxs13("table", { className: "w-full border-collapse text-sm", children: [
2557
- /* @__PURE__ */ jsx20("thead", { children: /* @__PURE__ */ jsxs13("tr", { children: [
2774
+ /* @__PURE__ */ jsx20("thead", { children: /* @__PURE__ */ jsxs13("tr", { className: "divide-x divide-border", children: [
2775
+ showSelectionColumn && /* @__PURE__ */ jsx20("th", { className: "border-b border-border p-1", style: { width: SELECTION_COLUMN_WIDTH }, children: /* @__PURE__ */ jsx20(
2776
+ Checkbox,
2777
+ {
2778
+ "aria-label": "Select all visible rows",
2779
+ checked: allVisibleSelected,
2780
+ onCheckedChange: toggleAllVisibleSelected
2781
+ }
2782
+ ) }),
2558
2783
  columns.map((column) => /* @__PURE__ */ jsx20(
2559
2784
  "th",
2560
2785
  {
2561
2786
  style: column.width ? { width: column.width } : void 0,
2562
- className: `border-b p-2 font-medium ${alignClassName(column)}`,
2787
+ className: `border-b border-border p-2 font-medium ${alignClassName(column)}`,
2563
2788
  children: column.header
2564
2789
  },
2565
2790
  column.id
2566
2791
  )),
2567
- showRowActionsColumn && /* @__PURE__ */ jsx20("th", { className: "border-b p-2", "aria-label": "Row actions" })
2792
+ showRowActionsColumn && /* @__PURE__ */ jsx20("th", { className: "border-b border-border p-2", "aria-label": "Row actions" })
2568
2793
  ] }) }),
2569
2794
  /* @__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: [
2570
2795
  paddingTop > 0 && /* @__PURE__ */ jsx20("tr", { "aria-hidden": "true", children: /* @__PURE__ */ jsx20("td", { colSpan: totalColumnCount, style: { padding: 0, border: "none", height: paddingTop } }) }),