@bmsuisse/datagrid 0.23.4 → 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.d.ts CHANGED
@@ -762,6 +762,37 @@ interface DataGridProps<TRow> {
762
762
  * a page with more than one.
763
763
  */
764
764
  testId?: string;
765
+ /**
766
+ * Buckets the grid's already-filtered/sorted/paginated `rows` into groups,
767
+ * rendering one full-width `colSpan`'d group-header row before each
768
+ * bucket's own rows, in first-seen bucket order (rows are never re-sorted
769
+ * to cluster a group together — the caller's own sort already determines
770
+ * which key appears first). Single level only — no nested grouping, no
771
+ * aggregate/summary calculation; a caller wanting a subtotal computes it
772
+ * themselves in `renderGroupHeader` off that bucket's own row array.
773
+ * Operates on whatever `rows` already resolved to — i.e. the current page,
774
+ * if paginated — with no special-casing; pair with a large `pageSize`/
775
+ * `showPagination: false` if a group's full membership should never be
776
+ * split across pages. Omit entirely to disable — no grouping, no header
777
+ * rows, unchanged default rendering.
778
+ *
779
+ * Not supported together with `virtualize` yet — when both are set,
780
+ * virtualization is silently disabled and the grid renders as a plain,
781
+ * fully-rendered (but still grouped) `<table>` instead. See AGENTS.md's
782
+ * "Known limitations."
783
+ */
784
+ groupBy?: (row: TRow) => string;
785
+ /** Customizes a group-header row's content. Defaults to `` `${key} (${rows.length})` ``. */
786
+ renderGroupHeader?: (key: string, rows: TRow[], expanded: boolean) => ReactNode;
787
+ /** Whether a newly-seen group starts expanded. Defaults to true. */
788
+ defaultGroupsExpanded?: boolean;
789
+ /**
790
+ * Controlled per-group expand/collapse state, keyed by the `groupBy` key.
791
+ * Both must be supplied together to control it — omit both for
792
+ * `<DataGrid>` to own this state internally.
793
+ */
794
+ expandedGroups?: Record<string, boolean>;
795
+ onExpandedGroupsChange?: (expanded: Record<string, boolean>) => void;
765
796
  }
766
797
 
767
798
  /**
@@ -772,7 +803,7 @@ interface DataGridProps<TRow> {
772
803
  * (`"server"`). Column filter UI defaults to the type-appropriate widget
773
804
  * from `src/filter` unless a column supplies its own `cell` renderer.
774
805
  */
775
- declare function DataGrid<TRow extends RowData>({ columns, dataSource, getRowId, rowActions, headerActions, enableMultiSort, initialState, columnVisibility, renderDetail, enableColumnResizing, columnResizeMode, columnSizing: controlledColumnSizing, onColumnSizingChange, virtualize, showPagination, getRowProps, getRowTestId, gridState, selectedIds: controlledSelectedIds, onSelectedIdsChange, testId, loading: loadingProp, }: DataGridProps<TRow>): ReactElement;
806
+ declare function DataGrid<TRow extends RowData>({ columns, dataSource, getRowId, rowActions, headerActions, enableMultiSort, initialState, columnVisibility, renderDetail, enableColumnResizing, columnResizeMode, columnSizing: controlledColumnSizing, onColumnSizingChange, virtualize, showPagination, getRowProps, getRowTestId, gridState, selectedIds: controlledSelectedIds, onSelectedIdsChange, testId, loading: loadingProp, groupBy, renderGroupHeader, defaultGroupsExpanded, expandedGroups: controlledExpandedGroups, onExpandedGroupsChange, }: DataGridProps<TRow>): ReactElement;
776
807
 
777
808
  interface GridStateController {
778
809
  state: GridState;
@@ -954,6 +985,48 @@ interface TreeDataGridProps<TRow> extends TreeAccessors<TRow> {
954
985
  estimatedRowHeight?: number;
955
986
  /** Max height of the scrollable table body in pixels. Required for virtualization to have a viewport to scroll within. Defaults to 480. */
956
987
  maxBodyHeight?: number;
988
+ /**
989
+ * Controlled row selection — same convention as `<DataGrid>`'s own
990
+ * `selectedIds`/`onSelectedIdsChange`. Both must be supplied together to
991
+ * render a checkbox column at all; there is no internal/uncontrolled
992
+ * fallback here, unlike `<DataGrid>` — `<TreeDataGrid>` has no
993
+ * `headerActions` bulk-toolbar concept that would need selection state
994
+ * even without external control, so there's nothing for an uncontrolled
995
+ * default to serve.
996
+ *
997
+ * Clicking a row's checkbox always toggles exactly that row's own id in
998
+ * this set — never its children or parent. Any cascade ("select whole
999
+ * group" / "select all descendants") is the caller's job, done by
1000
+ * recomputing the desired final set inside `onSelectedIdsChange` before
1001
+ * committing it to their own state.
1002
+ */
1003
+ selectedIds?: ReadonlySet<string>;
1004
+ onSelectedIdsChange?: (ids: ReadonlySet<string>) => void;
1005
+ /**
1006
+ * Fully overrides a row's checked/indeterminate display, replacing the
1007
+ * default derivation below. Needed for selection semantics this component
1008
+ * won't build in — e.g. a "select whole group vs. split into individual
1009
+ * children once loaded" cascade, where a parent should read as checked
1010
+ * even though not every descendant id is literally in `selectedIds`.
1011
+ * Return `undefined` for a given row to fall back to the default
1012
+ * derivation for just that row.
1013
+ *
1014
+ * Omitted (the default): `checked` is exactly `selectedIds.has(getRowId(row))`
1015
+ * — `selectedIds` always stays the single source of truth for "is this id
1016
+ * selected." `indeterminate` is true only when the row itself isn't
1017
+ * checked but at least one *currently loaded* descendant (recursively, via
1018
+ * the same `getChildren`/`onLoadChildren` cache this component already
1019
+ * maintains) is checked or itself indeterminate. A selected descendant
1020
+ * hidden behind a node that's never been expanded (so its children were
1021
+ * never fetched) can't be inspected — that ancestor reads as unchecked,
1022
+ * not indeterminate, until the fetch happens.
1023
+ */
1024
+ getRowSelectionState?: (row: TRow) => {
1025
+ checked: boolean;
1026
+ indeterminate?: boolean;
1027
+ } | undefined;
1028
+ /** Disables just the checkbox for a specific row. Defaults to `() => false`. */
1029
+ isRowSelectionDisabled?: (row: TRow) => boolean;
957
1030
  }
958
1031
 
959
1032
  /**
@@ -970,7 +1043,7 @@ interface TreeDataGridProps<TRow> extends TreeAccessors<TRow> {
970
1043
  * column's cell matches `treeColumnId` (defaults to `columns[0]`); every
971
1044
  * other column renders exactly like it would in `<DataGrid>`.
972
1045
  */
973
- declare function TreeDataGrid<TRow>({ columns, data, treeColumnId, getRowId, getChildren, hasChildren, onLoadChildren, initialExpandedLevel, expanded: controlledExpanded, onExpandedChange, indentSize, getRowProps, getRowTestId, rowActions, virtualizeThreshold, estimatedRowHeight, maxBodyHeight, }: TreeDataGridProps<TRow>): ReactElement;
1046
+ declare function TreeDataGrid<TRow>({ columns, data, treeColumnId, getRowId, getChildren, hasChildren, onLoadChildren, initialExpandedLevel, expanded: controlledExpanded, onExpandedChange, indentSize, getRowProps, getRowTestId, rowActions, virtualizeThreshold, estimatedRowHeight, maxBodyHeight, selectedIds: controlledSelectedIds, onSelectedIdsChange, getRowSelectionState, isRowSelectionDisabled, }: TreeDataGridProps<TRow>): ReactElement;
974
1047
 
975
1048
  interface FlatTreeRow<TRow> {
976
1049
  row: TRow;
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: [
@@ -2108,7 +2233,16 @@ function DataGrid({
2108
2233
  }
2109
2234
  ),
2110
2235
  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 }) }) }),
2236
+ /* @__PURE__ */ jsx19(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx19(Button, { variant: "ghost", size: "icon", "aria-label": `Filter ${column.header}`, children: /* @__PURE__ */ jsx19(
2237
+ FunnelIcon7,
2238
+ {
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
2244
+ }
2245
+ ) }) }),
2112
2246
  /* @__PURE__ */ jsx19(PopoverContent, { children: renderFilterWidget(column) })
2113
2247
  ] })
2114
2248
  ] }),
@@ -2124,7 +2258,7 @@ function DataGrid({
2124
2258
  ] }, header.id);
2125
2259
  }
2126
2260
  return /* @__PURE__ */ jsxs12(Fragment2, { children: [
2127
- 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: [
2128
2262
  showDetailColumn && /* @__PURE__ */ jsx19(
2129
2263
  "th",
2130
2264
  {
@@ -2156,7 +2290,7 @@ function DataGrid({
2156
2290
  "th",
2157
2291
  {
2158
2292
  colSpan: run.columns.length,
2159
- className: "border-b p-2 text-center font-medium",
2293
+ className: "border-b border-border p-2 text-center font-medium",
2160
2294
  children: run.label
2161
2295
  },
2162
2296
  `group-${run.label}-${run.columns[0].id}`
@@ -2172,7 +2306,7 @@ function DataGrid({
2172
2306
  }
2173
2307
  )
2174
2308
  ] }, `${headerGroup.id}-groups`),
2175
- /* @__PURE__ */ jsxs12("tr", { children: [
2309
+ /* @__PURE__ */ jsxs12("tr", { className: "divide-x divide-border", children: [
2176
2310
  !hasHeaderGroups && showDetailColumn && /* @__PURE__ */ jsx19(
2177
2311
  "th",
2178
2312
  {
@@ -2213,7 +2347,7 @@ function DataGrid({
2213
2347
  ] }, headerGroup.id)
2214
2348
  ] }, headerGroup.id);
2215
2349
  }),
2216
- hasFilterRow && /* @__PURE__ */ jsxs12("tr", { "data-testid": "filter-row", children: [
2350
+ hasFilterRow && /* @__PURE__ */ jsxs12("tr", { "data-testid": "filter-row", className: "divide-x divide-border", children: [
2217
2351
  showDetailColumn && /* @__PURE__ */ jsx19("th", { className: detailHeaderCellProps.className, style: detailHeaderCellProps.style }),
2218
2352
  showSelectionColumn && /* @__PURE__ */ jsx19(
2219
2353
  "th",
@@ -2241,7 +2375,7 @@ function DataGrid({
2241
2375
  (virtualItem) => renderRow(tableRows[virtualItem.index], (el) => virtualizer.measureElement(el))
2242
2376
  ),
2243
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 } }) }) })
2244
- ] }) : tableRows.map((row) => renderRow(row))
2378
+ ] }) : groupedBuckets ? groupedBuckets.map((bucket) => renderGroupedBucket(bucket)) : tableRows.map((row) => renderRow(row))
2245
2379
  ] })
2246
2380
  }
2247
2381
  )
@@ -2306,6 +2440,31 @@ function flattenTree(roots, accessors, expanded, childrenOverride) {
2306
2440
  return out;
2307
2441
  }
2308
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
+
2309
2468
  // src/tree/useTreeState.ts
2310
2469
  import { useCallback as useCallback3, useMemo as useMemo7, useState as useState7 } from "react";
2311
2470
  function useTreeState({
@@ -2412,6 +2571,8 @@ function useTreeState({
2412
2571
 
2413
2572
  // src/tree/TreeDataGrid.tsx
2414
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 };
2415
2576
  function renderCell(column, row) {
2416
2577
  const value = getColumnValue(column, row);
2417
2578
  return column.cell ? column.cell(value, row) : defaultFormat(column, value);
@@ -2466,7 +2627,11 @@ function TreeDataGrid({
2466
2627
  rowActions,
2467
2628
  virtualizeThreshold = 100,
2468
2629
  estimatedRowHeight = 40,
2469
- maxBodyHeight = 480
2630
+ maxBodyHeight = 480,
2631
+ selectedIds: controlledSelectedIds,
2632
+ onSelectedIdsChange,
2633
+ getRowSelectionState,
2634
+ isRowSelectionDisabled
2470
2635
  }) {
2471
2636
  const accessors = useMemo8(
2472
2637
  () => ({ getRowId, getChildren, hasChildren }),
@@ -2493,7 +2658,38 @@ function TreeDataGrid({
2493
2658
  );
2494
2659
  const treeColId = treeColumnId ?? columns[0]?.id;
2495
2660
  const showRowActionsColumn = Boolean(rowActions?.length);
2496
- 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
+ }
2497
2693
  const bodyTdClassByColumn = useMemo8(() => {
2498
2694
  const map = /* @__PURE__ */ new Map();
2499
2695
  for (const column of columns) map.set(column.id, `p-2 ${alignClassName(column)}`);
@@ -2517,8 +2713,25 @@ function TreeDataGrid({
2517
2713
  ref: measureRef,
2518
2714
  ...rowProps,
2519
2715
  "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" : ""}`,
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" : ""}`,
2521
2721
  children: [
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
+ })() }),
2522
2735
  columns.map((column) => /* @__PURE__ */ jsx20("td", { className: bodyTdClassByColumn.get(column.id), children: column.id === treeColId ? /* @__PURE__ */ jsx20(
2523
2736
  TreeCell,
2524
2737
  {
@@ -2554,17 +2767,25 @@ function TreeDataGrid({
2554
2767
  className: shouldVirtualize ? "overflow-y-auto" : void 0,
2555
2768
  style: shouldVirtualize ? { maxHeight: maxBodyHeight } : void 0,
2556
2769
  children: /* @__PURE__ */ jsxs13("table", { className: "w-full border-collapse text-sm", children: [
2557
- /* @__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
+ ) }),
2558
2779
  columns.map((column) => /* @__PURE__ */ jsx20(
2559
2780
  "th",
2560
2781
  {
2561
2782
  style: column.width ? { width: column.width } : void 0,
2562
- className: `border-b p-2 font-medium ${alignClassName(column)}`,
2783
+ className: `border-b border-border p-2 font-medium ${alignClassName(column)}`,
2563
2784
  children: column.header
2564
2785
  },
2565
2786
  column.id
2566
2787
  )),
2567
- 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" })
2568
2789
  ] }) }),
2569
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: [
2570
2791
  paddingTop > 0 && /* @__PURE__ */ jsx20("tr", { "aria-hidden": "true", children: /* @__PURE__ */ jsx20("td", { colSpan: totalColumnCount, style: { padding: 0, border: "none", height: paddingTop } }) }),