@dynostack/react-grid 0.3.1 → 0.4.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/README.md CHANGED
@@ -13,12 +13,45 @@ Built on [TanStack Table v8](https://tanstack.com/table) · [Radix UI](https://w
13
13
 
14
14
  </div>
15
15
 
16
+ **[Live showcase](https://dynostack-react-grid.vercel.app)** · **[Interactive playground](https://dynostack-react-grid.vercel.app/playground)** · **[Setup and configuration docs](https://dynostack-react-grid.vercel.app/docs)**
17
+
18
+ Try every feature on real sample data, customize themes and density, share a configuration link, and copy the matching React props. The demo includes spring-based interactions and works on mobile.
19
+
16
20
  ---
17
21
 
18
22
  A single `<DataTable />` component that gives you ag-grid–level functionality with a fraction of the API surface and a shadcn/ui aesthetic. Every behavior is opt-in via props — drop it in and it works; configure it and it scales.
19
23
 
24
+ ## Showcase
25
+
26
+ <p align="center">
27
+ <img src="public/table-images/Screenshot%202026-05-25%20233937.png" alt="React data grid with toolbar, sorting, filters, pagination, export, and add-row controls" width="100%" />
28
+ </p>
29
+
30
+ | Selection + bulk actions | Column menu |
31
+ | --- | --- |
32
+ | <img src="public/table-images/Screenshot%202026-05-25%20234013.png" alt="Rows selected with bulk delete and clear actions visible in the toolbar" width="100%" /> | <img src="public/table-images/Screenshot%202026-05-25%20234035.png" alt="Column menu with ascending sort, descending sort, pin, unpin, and hide column actions" width="100%" /> |
33
+
34
+ | Filter builder | Column visibility |
35
+ | --- | --- |
36
+ | <img src="public/table-images/Screenshot%202026-05-25%20234221.png" alt="Column filter builder with operator select, value input, add condition, clear, close, and apply controls" width="100%" /> | <img src="public/table-images/Screenshot%202026-05-25%20234246.png" alt="Column visibility popover with checked columns and reset control" width="100%" /> |
37
+
38
+ | Export selected rows | Inline add and edit |
39
+ | --- | --- |
40
+ | <img src="public/table-images/Screenshot%202026-05-25%20234301.png" alt="Export menu for selected rows with CSV and Excel options" width="100%" /> | <img src="public/table-images/Screenshot%202026-05-25%20234313.png" alt="Inline add-row editing with text fields, select input, date input, save, and cancel controls" width="100%" /> |
41
+
42
+ | Row actions | Details panel |
43
+ | --- | --- |
44
+ | <img src="public/table-images/Screenshot%202026-05-25%20234410.png" alt="Row actions menu with view, edit, duplicate, and delete commands" width="100%" /> | <img src="public/table-images/Screenshot%202026-05-25%20234428.png" alt="Right-side details panel with compact, relaxed, and comfy density controls" width="100%" /> |
45
+
46
+ | Search |
47
+ | --- |
48
+ | <img src="public/table-images/Screenshot%202026-05-25%20234453.png" alt="Search input expanded in the table toolbar" width="100%" /> |
49
+
20
50
  ## Highlights
21
51
 
52
+ - **Layout and state controls** — `initialSorting`, `onSelectionChange`, `ariaLabel`, `striped`, `stickyHeader`, and `maxHeight`. Disabling pagination renders all matching loaded rows.
53
+ - **Safer exports and editing** — spreadsheet formulas in untrusted text are neutralized; row editing respects `meta.isEditable`; theme values cannot escape scoped CSS declarations.
54
+
22
55
  - **Filters that actually filter** — text, number, date with operators (`contains`, `not contains`, `equals`, `before`, `after`, `in range`, `blank`, `not blank`, …), AND/OR combine of two conditions, and a set filter with search + select-all
23
56
  - **Inline editing** — double-click cell to edit, or enter row-edit mode with `Save` / `Cancel`
24
57
  - **Add row** — local optimistic insert, edit, then commit on save
@@ -26,6 +59,8 @@ A single `<DataTable />` component that gives you ag-grid–level functionality
26
59
  - **Selection + bulk actions** — pinned `__select` column with select-all, clear, bulk delete
27
60
  - **Expandable rows** — provide a `renderSubRow` panel or use TanStack's nested `getSubRows`
28
61
  - **CSV / Excel export** — selection-aware (export selected vs. all)
62
+ - **Built-in row Details panel** — `View` opens a scoped sheet with compact, relaxed, and comfy field layouts
63
+ - **Scoped delete confirmation** — row and bulk delete confirmations stay inside the table instead of covering the entire app
29
64
  - **Theming that just works** — shadcn-compatible CSS variables, automatic OS dark-mode follow, cascade-layered defaults that never overwrite your app theme, full-repaint moded presets (`violet`, `emerald`, `amber`, `rose`, `sky`, `slate`, …), `buildPreset(hue)` for custom hues, and `isolate` to opt out of inheriting the app theme
30
65
  - **Density** — `compact` · `default` · `comfortable`
31
66
  - **i18n / labels** — every visible string is overridable
@@ -37,6 +72,7 @@ A single `<DataTable />` component that gives you ag-grid–level functionality
37
72
  ## Table of contents
38
73
 
39
74
  - [Install](#install)
75
+ - [Showcase](#showcase)
40
76
  - [Tailwind setup](#tailwind-setup)
41
77
  - [Theme tokens](#theme-tokens)
42
78
  - [Quick start](#quick-start)
@@ -65,6 +101,34 @@ A single `<DataTable />` component that gives you ag-grid–level functionality
65
101
 
66
102
  ## Install
67
103
 
104
+ ### Run the showcase locally
105
+
106
+ ```sh
107
+ git clone https://github.com/wanted-coder-vijay/wcv-data-grid.git
108
+ cd wcv-data-grid
109
+ npm install
110
+ npm run build
111
+ npm install --prefix showcase
112
+ npm run showcase:dev
113
+ ```
114
+
115
+ ### New layout controls
116
+
117
+ ```tsx
118
+ <DataTable
119
+ data={rows}
120
+ columns={columns}
121
+ initialSorting={[{ id: "name", desc: false }]}
122
+ onSelectionChange={(selectedRows) => setSelectedRows(selectedRows)}
123
+ ariaLabel="Project workspace"
124
+ striped
125
+ stickyHeader
126
+ maxHeight="480px"
127
+ />
128
+ ```
129
+
130
+ `onSelectionChange` returns selected **loaded** row objects; server-side exports and selection do not fetch unseen pages. Editing flags are UI controls: your server must also validate fields, values, and permissions. CSV/Excel exports neutralize formula prefixes in strings while preserving actual numeric values. Excel output remains an HTML-based `.xls`, not native XLSX. Theme tokens reject declaration delimiters, CSS comments, backslash escapes, and URL expressions.
131
+
68
132
  ```sh
69
133
  npm i @dynostack/react-grid
70
134
  # or
@@ -73,7 +137,7 @@ pnpm add @dynostack/react-grid
73
137
  yarn add @dynostack/react-grid
74
138
  ```
75
139
 
76
- **Peer deps:** `react >= 18`, `react-dom >= 18`. All other deps (`@tanstack/react-table`, `radix-ui`, `lucide-react`, `class-variance-authority`, `clsx`, `tailwind-merge`) are bundled.
140
+ **Peer deps:** `react >= 18.2`, `react-dom >= 18.2`. All other runtime dependencies (`@tanstack/react-table`, `radix-ui`, `lucide-react`, `class-variance-authority`, `clsx`, `tailwind-merge`) are installed automatically and remain external to the package bundle.
77
141
 
78
142
  ## Tailwind setup
79
143
 
@@ -613,7 +677,7 @@ The set filter automatically derives unique values from the visible rows when `s
613
677
  Selection is on by default (`enableSelection: true`). When any row is selected the toolbar swaps in:
614
678
 
615
679
  - A `<count> selected` badge
616
- - `Delete` button → wires to `onBulkDelete?(rows)`
680
+ - `Delete` button → opens the confirmation dialog first, then calls `onBulkDelete?(rows)` after confirm
617
681
  - `Clear` button → resets selection
618
682
 
619
683
  ```tsx
@@ -660,7 +724,15 @@ Mark a column non-exportable via `meta.exportable: false`.
660
724
 
661
725
  ## View sheet
662
726
 
663
- Click the row action "View" → a right-side `Sheet` slides in showing every visible column as a `{Label}: {value}` card. The user can switch layout density inline (Compact 1 col / Relaxed 2 col / Comfy 3 col).
727
+ Click the row action "View" → a right-side `Sheet` slides in showing every visible column as a `{Label}: {value}` card. The user can switch layout density inline:
728
+
729
+ | Option | Layout | Intended use |
730
+ | --- | --- | --- |
731
+ | `Compact` | 3 columns, tighter cards | Scan more fields at once. |
732
+ | `Relaxed` | 2 columns, medium spacing | Balanced default for mixed values. |
733
+ | `Comfy` | 1 column, roomier cards | Read long values without cramped wrapping. |
734
+
735
+ The sheet is responsive and wider on desktop so the multi-column modes have enough room for real row data.
664
736
 
665
737
  Works out of the box with no props. Customize via `viewSheet`:
666
738
 
@@ -668,7 +740,7 @@ Works out of the box with no props. Customize via `viewSheet`:
668
740
  <DataTable
669
741
  viewSheet={{
670
742
  side: "right", // or "left"
671
- defaultDensity: "relaxed", // initial column count
743
+ defaultDensity: "relaxed", // initial layout density
672
744
  hideDensityTabs: true, // hide the layout picker
673
745
  fields: ["name", "email", "role"], // limit / reorder shown columns
674
746
  renderField: ({ column, value, row }) => // override how a value renders
@@ -678,7 +750,7 @@ Works out of the box with no props. Customize via `viewSheet`:
678
750
  title: (row) => `${row.name} (${row.role})`,
679
751
  description: (row) => `Joined ${row.joinedAt}`,
680
752
  emptyValue: "—",
681
- density: { compact: "1 col", relaxed: "2 cols", comfy: "3 cols" },
753
+ density: { compact: "3 cols", relaxed: "2 cols", comfy: "1 col" },
682
754
  },
683
755
  }}
684
756
  onView={(row) => track("user.view", row)} // optional side-effect
@@ -699,6 +771,8 @@ Disable the built-in sheet entirely:
699
771
 
700
772
  Both the row-action "Delete" and the toolbar "Bulk delete" open a confirmation `AlertDialog` by default. The user must confirm before `onDelete` or `onBulkDelete` fires.
701
773
 
774
+ The dialog is mounted inside the DataTable portal container, so its blur / dim overlay covers only that table instance. It does not block or blur the rest of the page.
775
+
702
776
  ```tsx
703
777
  <DataTable
704
778
  onDelete={(row) => api.deleteUser(row.id)}
@@ -716,6 +790,8 @@ Both the row-action "Delete" and the toolbar "Bulk delete" open a confirmation `
716
790
  />
717
791
  ```
718
792
 
793
+ Use `onDelete` for the built-in row delete action. Do not put the actual delete mutation in `onRowAction("delete")`, because the built-in delete action is handled by the confirmation flow.
794
+
719
795
  Skip the dialog (fire immediately):
720
796
 
721
797
  ```tsx
@@ -740,10 +816,10 @@ Skip the dialog (fire immediately):
740
816
  { id: "archive", label: "Archive", icon: <ArchiveIcon /> },
741
817
  ]}
742
818
  onRowAction={(action, row) => {
743
- if (action === "delete") deleteMutation.mutate([row.id])
744
819
  if (action === "suspend") saveMutation.mutate({ ...row, status: "suspended" })
745
820
  // ...
746
821
  }}
822
+ onDelete={(row) => deleteMutation.mutate([row.id])}
747
823
  />
748
824
  ```
749
825
 
@@ -836,6 +912,12 @@ operations and only renders the returned page.
836
912
  | `pageSizeOptions` | `number[]` | shadcn defaults | Page-size dropdown options. |
837
913
  | `initialColumnPinning` | `ColumnPinningState` | `{ left: [], right: [] }` | Initial pinned columns. |
838
914
  | `initialColumnVisibility` | `VisibilityState` | `{}` | Initial hidden columns. |
915
+ | `initialSorting` | `SortingState` | `[]` | Initial column sort order. |
916
+ | `onSelectionChange` | `(rows: TData[]) => void` | — | Observe selected loaded rows. |
917
+ | `ariaLabel` | `string` | `"Data table"` | Accessible name for the table. |
918
+ | `striped` | `boolean` | `false` | Alternate row backgrounds. |
919
+ | `stickyHeader` | `boolean` | `false` | Keep headers visible in the scroll viewport. |
920
+ | `maxHeight` | `CSSProperties["maxHeight"]` | — | Limit the vertical scroll viewport. |
839
921
  | `globalFilter` | `string` | uncontrolled | Controlled global filter value. |
840
922
  | `onGlobalFilterChange` | `(value: string) => void` | — | Controlled global filter setter. |
841
923
  | `className` | `string` | — | Extra classes on the table root. |
package/dist/index.cjs CHANGED
@@ -283,15 +283,21 @@ function PopoverContent({
283
283
  }
284
284
 
285
285
  // src/export.ts
286
+ function sanitizeSpreadsheetValue(value) {
287
+ if (value == null) return "";
288
+ const text = String(value);
289
+ if (typeof value === "string" && (/^[\s\u0000-\u001f]*[=+\-@=+-@]/u.test(text) || /^[\t\r\n]/.test(text))) return `'${text}`;
290
+ return text;
291
+ }
286
292
  function escapeCsv(value) {
287
293
  if (value === null || value === void 0) return "";
288
- const s = String(value);
294
+ const s = sanitizeSpreadsheetValue(value);
289
295
  if (/[",\n\r]/.test(s)) return `"${s.replace(/"/g, '""')}"`;
290
296
  return s;
291
297
  }
292
298
  function escapeHtml(value) {
293
299
  if (value === null || value === void 0) return "";
294
- return String(value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
300
+ return sanitizeSpreadsheetValue(value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
295
301
  }
296
302
  function getExportRows(table) {
297
303
  const visible = table.getVisibleLeafColumns().filter((c) => {
@@ -1901,7 +1907,7 @@ function EditableCell({
1901
1907
  if (typeof flag === "function") return flag(row.original);
1902
1908
  return flag;
1903
1909
  })();
1904
- if (!isEditing) {
1910
+ if (!isEditing || !isEditable) {
1905
1911
  const display = renderDisplay(initial, editor, meta);
1906
1912
  return /* @__PURE__ */ jsxRuntime.jsx(
1907
1913
  "div",
@@ -1918,7 +1924,9 @@ function EditableCell({
1918
1924
  }
1919
1925
  );
1920
1926
  }
1921
- const commit = (value) => onCommit(value);
1927
+ const commit = (value) => {
1928
+ if (isEditable) onCommit(value);
1929
+ };
1922
1930
  const handleKey = (e) => {
1923
1931
  if (e.key === "Enter") commit(local);
1924
1932
  else if (e.key === "Escape") onCancel();
@@ -2048,6 +2056,9 @@ var TOKEN_TO_VAR = {
2048
2056
  radius: "--radius",
2049
2057
  fontFamily: "--font-family"
2050
2058
  };
2059
+ function isSafeToken(value) {
2060
+ return typeof value === "string" && !/[;{}<>@\\\u0000-\u001f]/.test(value) && !/\/\*|\*\/|url\s*\(|expression\s*\(/i.test(value);
2061
+ }
2051
2062
  function isModed(theme) {
2052
2063
  if (!theme) return false;
2053
2064
  return "light" in theme || "dark" in theme;
@@ -2056,8 +2067,8 @@ function tokensToStyle(tokens) {
2056
2067
  if (!tokens) return {};
2057
2068
  const out = {};
2058
2069
  for (const [k, v] of Object.entries(tokens)) {
2059
- if (v == null) continue;
2060
- const cssVar = TOKEN_TO_VAR[k];
2070
+ if (!isSafeToken(v)) continue;
2071
+ const cssVar = Object.prototype.hasOwnProperty.call(TOKEN_TO_VAR, k) ? TOKEN_TO_VAR[k] : void 0;
2061
2072
  if (cssVar) out[cssVar] = v;
2062
2073
  }
2063
2074
  return out;
@@ -2131,8 +2142,8 @@ function tokensToCssBlock(tokens) {
2131
2142
  if (!tokens) return "";
2132
2143
  const parts = [];
2133
2144
  for (const [k, v] of Object.entries(tokens)) {
2134
- if (v == null) continue;
2135
- const cssVar = TOKEN_TO_VAR[k];
2145
+ if (!isSafeToken(v)) continue;
2146
+ const cssVar = Object.prototype.hasOwnProperty.call(TOKEN_TO_VAR, k) ? TOKEN_TO_VAR[k] : void 0;
2136
2147
  if (cssVar) parts.push(`${cssVar}:${v}`);
2137
2148
  }
2138
2149
  return parts.join(";");
@@ -2216,8 +2227,8 @@ function SheetOverlay({
2216
2227
  );
2217
2228
  }
2218
2229
  var SIDE_CLASS = {
2219
- right: "right-0 top-0 h-full w-3/4 max-w-md border-l data-closed:slide-out-to-right data-open:slide-in-from-right",
2220
- left: "left-0 top-0 h-full w-3/4 max-w-md border-r data-closed:slide-out-to-left data-open:slide-in-from-left",
2230
+ right: "right-0 top-0 h-full w-full border-l sm:w-[min(48rem,calc(100vw-2rem))] data-closed:slide-out-to-right data-open:slide-in-from-right",
2231
+ left: "left-0 top-0 h-full w-full border-r sm:w-[min(48rem,calc(100vw-2rem))] data-closed:slide-out-to-left data-open:slide-in-from-left",
2221
2232
  top: "left-0 top-0 w-full max-h-[80vh] border-b data-closed:slide-out-to-top data-open:slide-in-from-top",
2222
2233
  bottom: "left-0 bottom-0 w-full max-h-[80vh] border-t data-closed:slide-out-to-bottom data-open:slide-in-from-bottom"
2223
2234
  };
@@ -2292,14 +2303,19 @@ function SheetDescription({
2292
2303
  );
2293
2304
  }
2294
2305
  var COLS_CLASS = {
2295
- compact: "grid-cols-1",
2306
+ compact: "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3",
2296
2307
  relaxed: "grid-cols-1 sm:grid-cols-2",
2297
- comfy: "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3"
2308
+ comfy: "grid-cols-1"
2309
+ };
2310
+ var CARD_CLASS = {
2311
+ compact: "gap-0.5 p-2.5",
2312
+ relaxed: "gap-1 p-3",
2313
+ comfy: "gap-1.5 p-4"
2298
2314
  };
2299
2315
  var DENSITY_ICON = {
2300
- compact: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ListIcon, { className: "size-3.5" }),
2316
+ compact: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.LayoutGridIcon, { className: "size-3.5" }),
2301
2317
  relaxed: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Rows3Icon, { className: "size-3.5" }),
2302
- comfy: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.LayoutGridIcon, { className: "size-3.5" })
2318
+ comfy: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ListIcon, { className: "size-3.5" })
2303
2319
  };
2304
2320
  function DataTableViewSheet({
2305
2321
  table,
@@ -2372,10 +2388,13 @@ function DataTableViewSheet({
2372
2388
  return /* @__PURE__ */ jsxRuntime.jsxs(
2373
2389
  "div",
2374
2390
  {
2375
- className: "flex flex-col gap-0.5 rounded-md border bg-card p-2.5",
2391
+ className: cn(
2392
+ "flex min-w-0 flex-col rounded-md border bg-card",
2393
+ CARD_CLASS[density]
2394
+ ),
2376
2395
  children: [
2377
2396
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[11px] uppercase tracking-wide text-muted-foreground", children: headerLabel }),
2378
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm text-foreground break-words", children: rendered })
2397
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "break-words text-sm text-foreground", children: rendered })
2379
2398
  ]
2380
2399
  },
2381
2400
  colId
@@ -2400,11 +2419,15 @@ function AlertDialogContent({
2400
2419
  ...props
2401
2420
  }) {
2402
2421
  const container = usePortalContainer();
2422
+ const positionClass = container ? "absolute" : "fixed";
2403
2423
  return /* @__PURE__ */ jsxRuntime.jsxs(radixUi.AlertDialog.Portal, { container: container ?? void 0, children: [
2404
2424
  /* @__PURE__ */ jsxRuntime.jsx(
2405
2425
  radixUi.AlertDialog.Overlay,
2406
2426
  {
2407
- className: "fixed inset-0 z-50 bg-foreground/30 backdrop-blur-sm data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0"
2427
+ className: cn(
2428
+ "inset-0 z-50 bg-foreground/30 backdrop-blur-sm data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
2429
+ positionClass
2430
+ )
2408
2431
  }
2409
2432
  ),
2410
2433
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -2412,7 +2435,8 @@ function AlertDialogContent({
2412
2435
  {
2413
2436
  "data-slot": "alert-dialog-content",
2414
2437
  className: cn(
2415
- "fixed top-1/2 left-1/2 z-50 grid w-full max-w-md -translate-x-1/2 -translate-y-1/2 gap-4 rounded-lg border bg-background p-6 shadow-lg outline-none data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
2438
+ "top-1/2 left-1/2 z-50 grid w-[calc(100%-2rem)] max-w-md -translate-x-1/2 -translate-y-1/2 gap-4 rounded-lg border bg-background p-6 shadow-lg outline-none data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
2439
+ positionClass,
2416
2440
  className
2417
2441
  ),
2418
2442
  ...props,
@@ -2543,6 +2567,10 @@ function useEditingContext() {
2543
2567
  if (!ctx) throw new Error("DataTable editing context missing");
2544
2568
  return ctx;
2545
2569
  }
2570
+ function DefaultCellHost(ctx) {
2571
+ const value = ctx.getValue();
2572
+ return value == null ? null : String(value);
2573
+ }
2546
2574
  function EditableCellHost(ctx) {
2547
2575
  const {
2548
2576
  editingCell,
@@ -2631,6 +2659,7 @@ function ActionsCellHost({ row }) {
2631
2659
  onView?.(row.original);
2632
2660
  } else if (a === "delete") {
2633
2661
  onRequestDelete?.(row.original);
2662
+ return;
2634
2663
  }
2635
2664
  onRowAction?.(a, row.original);
2636
2665
  }
@@ -2663,6 +2692,7 @@ var DENSITY_CLASS = {
2663
2692
  default: "",
2664
2693
  comfortable: "[&_tbody_td]:py-2"
2665
2694
  };
2695
+ var EMPTY_DATA = [];
2666
2696
  function DataTable({
2667
2697
  data,
2668
2698
  columns,
@@ -2686,6 +2716,12 @@ function DataTable({
2686
2716
  pageSizeOptions,
2687
2717
  initialColumnPinning,
2688
2718
  initialColumnVisibility,
2719
+ initialSorting,
2720
+ onSelectionChange,
2721
+ ariaLabel = "Data table",
2722
+ striped = false,
2723
+ stickyHeader = false,
2724
+ maxHeight,
2689
2725
  globalFilter: globalFilterProp,
2690
2726
  onGlobalFilterChange,
2691
2727
  className,
@@ -2762,7 +2798,7 @@ function DataTable({
2762
2798
  }
2763
2799
  setConfirmCtx(null);
2764
2800
  };
2765
- const [sorting, setSorting] = react.useState([]);
2801
+ const [sorting, setSorting] = react.useState(initialSorting ?? []);
2766
2802
  const [columnFilters, setColumnFilters] = react.useState([]);
2767
2803
  const [internalGlobalFilter, setInternalGlobalFilter] = react.useState("");
2768
2804
  const isControlledFilter = globalFilterProp !== void 0;
@@ -2858,7 +2894,7 @@ function DataTable({
2858
2894
  // eslint-disable-next-line react-hooks/exhaustive-deps
2859
2895
  ...dataSourceDeps
2860
2896
  ]);
2861
- const baseData = dataSourceEnabled ? sourceRows : data ?? [];
2897
+ const baseData = dataSourceEnabled ? sourceRows : data ?? EMPTY_DATA;
2862
2898
  const effectiveIsLoading = dataSourceEnabled ? sourceLoading : isLoading;
2863
2899
  const effectiveIsFetching = dataSourceEnabled ? sourceFetching : isFetching;
2864
2900
  const effectiveOnRefresh = dataSourceEnabled ? () => setSourceRefreshKey((key) => key + 1) : onRefresh;
@@ -2902,7 +2938,7 @@ function DataTable({
2902
2938
  title: headerLabel
2903
2939
  }
2904
2940
  ),
2905
- cell: hasEditor ? EditableCellHost : original
2941
+ cell: hasEditor ? EditableCellHost : original ?? DefaultCellHost
2906
2942
  };
2907
2943
  };
2908
2944
  const built = columns.map(wrap);
@@ -3014,10 +3050,10 @@ function DataTable({
3014
3050
  enableColumnFilters: f.filtering,
3015
3051
  enableColumnResizing: f.resizing,
3016
3052
  columnResizeMode: "onChange",
3017
- enableRowSelection: true,
3053
+ enableRowSelection: enableSelection,
3018
3054
  enableExpanding: !!(renderSubRow || getSubRows),
3019
3055
  enablePinning: f.pinning,
3020
- manualPagination: isServerSideDataSource,
3056
+ manualPagination: isServerSideDataSource || !f.pagination,
3021
3057
  manualSorting: isServerSideDataSource,
3022
3058
  manualFiltering: isServerSideDataSource,
3023
3059
  pageCount: isServerSideDataSource ? Math.max(1, Math.ceil(effectiveTotalRecords / pagination.pageSize)) : void 0,
@@ -3107,6 +3143,11 @@ function DataTable({
3107
3143
  setEditingRowId(tempId);
3108
3144
  } : void 0;
3109
3145
  const rows = table.getRowModel().rows;
3146
+ const selectionCallback = react.useRef(onSelectionChange);
3147
+ selectionCallback.current = onSelectionChange;
3148
+ react.useEffect(() => {
3149
+ selectionCallback.current?.(table.getSelectedRowModel().flatRows.map((row) => row.original));
3150
+ }, [rowSelection, mergedData, table]);
3110
3151
  const visibleLeafCount = table.getVisibleLeafColumns().length;
3111
3152
  const editingContextValue = react.useMemo(
3112
3153
  () => ({
@@ -3184,17 +3225,18 @@ function DataTable({
3184
3225
  labels
3185
3226
  }
3186
3227
  ),
3187
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative w-full overflow-auto", children: [
3228
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative w-full overflow-auto", style: { maxHeight }, children: [
3188
3229
  /* @__PURE__ */ jsxRuntime.jsxs(
3189
3230
  "table",
3190
3231
  {
3191
- className: "w-full caption-bottom text-sm",
3232
+ "aria-label": ariaLabel,
3233
+ className: cn("w-full caption-bottom text-sm", striped && "[&_tbody>tr:nth-child(even)]:bg-muted/25"),
3192
3234
  style: {
3193
3235
  width: table.getCenterTotalSize() ? void 0 : "100%",
3194
3236
  minWidth: "100%"
3195
3237
  },
3196
3238
  children: [
3197
- /* @__PURE__ */ jsxRuntime.jsx("thead", { className: "sticky top-0 z-10 bg-card/95 backdrop-blur supports-[backdrop-filter]:bg-card/70", children: table.getHeaderGroups().map((hg) => /* @__PURE__ */ jsxRuntime.jsx("tr", { className: "border-b", children: hg.headers.map((header) => {
3239
+ /* @__PURE__ */ jsxRuntime.jsx("thead", { className: cn("bg-card", stickyHeader && "sticky top-0 z-10"), children: table.getHeaderGroups().map((hg) => /* @__PURE__ */ jsxRuntime.jsx("tr", { className: "border-b", children: hg.headers.map((header) => {
3198
3240
  const meta = header.column.columnDef.meta;
3199
3241
  const isUtility = header.column.id === "__select" || header.column.id === "__expand" || header.column.id === "__actions";
3200
3242
  const isPinned = !!header.column.getIsPinned();
@@ -3387,6 +3429,7 @@ exports.exportToCsv = exportToCsv;
3387
3429
  exports.exportToExcel = exportToExcel;
3388
3430
  exports.isFilterActive = isFilterActive;
3389
3431
  exports.numberFilterFn = numberFilterFn;
3432
+ exports.sanitizeSpreadsheetValue = sanitizeSpreadsheetValue;
3390
3433
  exports.setFilterFn = setFilterFn;
3391
3434
  exports.splitTheme = splitTheme;
3392
3435
  exports.textFilterFn = textFilterFn;