@dynostack/react-grid 0.4.0 → 0.5.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
@@ -1,1096 +1,1089 @@
1
- <div align="center">
2
-
3
- # @dynostack/react-grid
4
-
5
- **Enterprise-grade React data grid. Drop-in.**
6
-
7
- Built on [TanStack Table v8](https://tanstack.com/table) · [Radix UI](https://www.radix-ui.com/) · [Tailwind CSS](https://tailwindcss.com/) · ships shadcn/ui look-and-feel out of the box.
8
-
9
- [![npm version](https://img.shields.io/npm/v/@dynostack/react-grid.svg?style=flat-square)](https://www.npmjs.com/package/@dynostack/react-grid)
10
- [![bundle size](https://img.shields.io/bundlephobia/minzip/@dynostack/react-grid?style=flat-square)](https://bundlephobia.com/package/@dynostack/react-grid)
11
- [![license](https://img.shields.io/npm/l/@dynostack/react-grid.svg?style=flat-square)](./LICENSE)
12
- [![types](https://img.shields.io/npm/types/@dynostack/react-grid?style=flat-square)](./dist/index.d.ts)
13
-
14
- </div>
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
-
20
- ---
21
-
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.
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
-
50
- ## Highlights
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
-
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
56
- - **Inline editing** — double-click cell to edit, or enter row-edit mode with `Save` / `Cancel`
57
- - **Add row** — local optimistic insert, edit, then commit on save
58
- - **Per-column sort, hide, pin, resize, drag-reorder** — pinned columns are fully opaque while you scroll horizontally
59
- - **Selection + bulk actions** — pinned `__select` column with select-all, clear, bulk delete
60
- - **Expandable rows** — provide a `renderSubRow` panel or use TanStack's nested `getSubRows`
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
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
65
- - **Density** — `compact` · `default` · `comfortable`
66
- - **i18n / labels** — every visible string is overridable
67
- - **Feature flags** — turn off any toolbar control or table capability with a single boolean
68
- - **Tiny API, full TypeScript** — one component, fully typed generics, no provider context to wire up
69
-
70
- ---
71
-
72
- ## Table of contents
73
-
74
- - [Install](#install)
75
- - [Showcase](#showcase)
76
- - [Tailwind setup](#tailwind-setup)
77
- - [Theme tokens](#theme-tokens)
78
- - [Quick start](#quick-start)
79
- - [Data fetching](#data-fetching)
80
- - [Theming](#theming)
81
- - [Density](#density)
82
- - [Feature flags](#feature-flags)
83
- - [Labels (i18n)](#labels-i18n)
84
- - [Column meta](#column-meta)
85
- - [Editing](#editing)
86
- - [Filters](#filters)
87
- - [Selection & bulk actions](#selection--bulk-actions)
88
- - [Expandable rows](#expandable-rows)
89
- - [Export](#export)
90
- - [View sheet](#view-sheet)
91
- - [Delete confirmation](#delete-confirmation)
92
- - [Custom row actions](#custom-row-actions)
93
- - [Server-side data](#server-side-data)
94
- - [API reference](#api-reference)
95
- - [Compatibility](#compatibility)
96
- - [Roadmap](#roadmap)
97
- - [Contributing](#contributing)
98
- - [License](#license)
99
-
100
- ---
101
-
102
- ## Install
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
-
132
- ```sh
133
- npm i @dynostack/react-grid
134
- # or
135
- pnpm add @dynostack/react-grid
136
- # or
137
- yarn add @dynostack/react-grid
138
- ```
139
-
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.
141
-
142
- ## Tailwind setup
143
-
144
- The component ships Tailwind class names verbatim, so your Tailwind build needs to know two things:
145
-
146
- 1. **Where to scan** for the class strings inside the bundle.
147
- 2. **Which semantic color tokens** (`bg-popover`, `bg-card`, `text-foreground`, …) exist.
148
-
149
- The package's `styles.css` registers the tokens for you via Tailwind v4's `@theme inline`. You only need to wire scanning.
150
-
151
- ### Tailwind v4 — zero config
152
-
153
- ```css
154
- /* your global stylesheet (e.g. src/index.css) */
155
- @import "tailwindcss";
156
- @source "../node_modules/@dynostack/react-grid/dist";
157
- @import "@dynostack/react-grid/styles.css";
158
- @import "@dynostack/react-grid/page.css"; /* optional: extend tokens to <body> */
159
- ```
160
-
161
- That's the whole setup. No `tailwind.config.js`, no `@theme` block to copy-paste, no shadcn install required. Overlay surfaces (popovers, dropdowns, sheets, the row-actions menu) all render correctly out of the box.
162
-
163
- ### Tailwind v3
164
-
165
- v3 doesn't read CSS `@theme` directives, so the semantic-color mapping has to live in your `tailwind.config.js`. The shadcn install guide for v3 covers the exact `theme.extend.colors` block you need — copy that, plus add the package's `dist` to your `content` array:
166
-
167
- ```js
168
- // tailwind.config.{js,ts}
169
- export default {
170
- content: [
171
- "./src/**/*.{ts,tsx}",
172
- "./node_modules/@dynostack/react-grid/dist/**/*.{js,mjs,cjs}",
173
- ],
174
- theme: {
175
- extend: {
176
- colors: {
177
- // copy the shadcn v3 color mapping here
178
- // (background, foreground, card, popover, primary, secondary,
179
- // muted, accent, destructive, border, input, ring)
180
- background: "hsl(var(--background))",
181
- foreground: "hsl(var(--foreground))",
182
- // … etc
183
- },
184
- },
185
- },
186
- }
187
- ```
188
-
189
- Then import `styles.css` as usual:
190
-
191
- ```ts
192
- import "@dynostack/react-grid/styles.css"
193
- ```
194
-
195
- > Starting a new project? **Use Tailwind v4.** The v4 path above is meaningfully simpler — the package handles token registration for you.
196
-
197
- ## Theme tokens
198
-
199
- The grid is built on **shadcn/ui CSS variables**. It auto-adjusts to whatever theme your app already has:
200
-
201
- | Your app has… | What you do | What you get |
202
- |---|---|---|
203
- | Nothing (bare React) | `import "@dynostack/react-grid/styles.css"` | Clean light theme, auto-switches to dark on OS preference. |
204
- | shadcn/ui (default theme) | Nothing | Grid inherits your `:root` tokens automatically. |
205
- | shadcn/ui with a custom theme (Stone / Zinc / your own hue) | Nothing | Grid picks up your custom tokens automatically. |
206
- | Custom theme using shadcn token names | Nothing | Same as above. |
207
- | Custom theme with non-shadcn names | Pass [`theme` prop](#theming) | Per-instance override mapped to shadcn vars. |
208
- | Want one grid to ignore the app theme | Pass `isolate` | Grid uses bundled defaults regardless of `:root`. |
209
-
210
- **Why this just works.** The bundled `styles.css`:
211
-
212
- 1. **Registers Tailwind v4 utility tokens** via a top-level `@theme inline` block — so `bg-popover`, `text-foreground`, `border-border`, etc. resolve to your tokens without any consumer-side `@theme` block.
213
- 2. **Declares variable values inside the `dynostack-grid-defaults` cascade layer** — any unlayered consumer rule (which is where shadcn and most app CSS lives) automatically wins, regardless of import order. You can't accidentally overwrite your app's theme by importing the grid's stylesheet.
214
-
215
- ### Minimal install (Tailwind v4)
216
-
217
- ```css
218
- /* your global stylesheet */
219
- @import "tailwindcss";
220
- @source "../node_modules/@dynostack/react-grid/dist";
221
- @import "@dynostack/react-grid/styles.css";
222
- ```
223
-
224
- See the [Tailwind setup](#tailwind-setup) section for v3.
225
-
226
- ### Optional: extend the theme to the page
227
-
228
- By default the grid only styles itself, not the surrounding page. If you want `<body>` to use the same background/foreground as the grid:
229
-
230
- ```ts
231
- import "@dynostack/react-grid/styles.css"
232
- import "@dynostack/react-grid/page.css" // optional
233
- ```
234
-
235
- ### Dark mode
236
-
237
- | Mode | How to enable | Behavior |
238
- |---|---|---|
239
- | Follow OS | Default — no action required | Light by day, dark by night via `prefers-color-scheme`. |
240
- | Force light | Add `class="light"` to `<html>` | Stays light regardless of OS. |
241
- | Force dark | Add `class="dark"` to `<html>` | Stays dark regardless of OS. |
242
- | Per-instance | `<DataTable theme={themePresets.violet}>` | Grid auto-flips light/dark inside the moded preset. |
243
-
244
- Either way you can still override any token per-instance via the [`theme`](#theming) prop.
245
-
246
- ---
247
-
248
- ## Quick start
249
-
250
- ```tsx
251
- import { DataTable } from "@dynostack/react-grid"
252
- import "@dynostack/react-grid/styles.css" // optional — only if you don't have shadcn tokens
253
-
254
- type User = {
255
- id: number
256
- name: string
257
- email: string
258
- role: "admin" | "viewer"
259
- joinedAt: string
260
- }
261
-
262
- const columns = [
263
- { accessorKey: "id", header: "ID", size: 70 },
264
- {
265
- accessorKey: "name",
266
- header: "Name",
267
- meta: { label: "Name", editor: "text", filterType: "text" },
268
- },
269
- {
270
- accessorKey: "email",
271
- header: "Email",
272
- meta: { label: "Email", editor: "text", filterType: "text" },
273
- },
274
- {
275
- accessorKey: "role",
276
- header: "Role",
277
- meta: {
278
- label: "Role",
279
- editor: "select",
280
- filterType: "multi-select",
281
- selectOptions: [
282
- { value: "admin", label: "Admin" },
283
- { value: "viewer", label: "Viewer" },
284
- ],
285
- },
286
- },
287
- {
288
- accessorKey: "joinedAt",
289
- header: "Joined",
290
- meta: { label: "Joined", editor: "date", filterType: "date" },
291
- },
292
- ]
293
-
294
- export function Users({ data }: { data: User[] }) {
295
- return (
296
- <DataTable<User>
297
- data={data}
298
- columns={columns}
299
- onCellEdit={(row, columnId, value) => save({ ...row, [columnId]: value })}
300
- onRowSave={(row, draft) => save({ ...row, ...draft })}
301
- onAddRow={() => ({ name: "", email: "", role: "viewer", joinedAt: today() })}
302
- onBulkDelete={(rows) => removeMany(rows.map((r) => r.id))}
303
- initialColumnPinning={{ left: ["__select", "id", "name"], right: ["__actions"] }}
304
- />
305
- )
306
- }
307
- ```
308
-
309
- That's it. You now have sort + filter + edit + add + delete + export + pin + resize + reorder + select.
310
-
311
- ---
312
-
313
- ## Data fetching
314
-
315
- `DataTable` supports two data ownership models.
316
-
317
- ### 1. Controlled data from your page
318
-
319
- Use this when your app already owns fetching with TanStack Query, SWR, Redux,
320
- loader functions, or custom hooks. The table receives rows and loading flags as
321
- props, and your app owns error/toast behavior.
322
-
323
- ```tsx
324
- const usersQuery = useQuery({
325
- queryKey: ["users"],
326
- queryFn: fetchUsers,
327
- })
328
-
329
- <DataTable<User>
330
- data={usersQuery.data ?? []}
331
- columns={columns}
332
- isLoading={usersQuery.isLoading}
333
- isFetching={usersQuery.isFetching}
334
- onRefresh={() => usersQuery.refetch()}
335
- totalRecords={usersQuery.data?.length ?? 0}
336
- />
337
- ```
338
-
339
- For blocking load errors, render your own page-level error state or pass an empty
340
- array. For background errors, show a toast from your query/mutation callbacks.
341
-
342
- ### 2. Internal fetching with `dataSource`
343
-
344
- Use this when you want the table to own fetch/loading/error/refresh state.
345
- `fetchRows` receives the current table state and can return either an array or
346
- `{ rows, totalRecords }`.
347
-
348
- ```tsx
349
- <DataTable<User>
350
- columns={columns}
351
- dataSource={{
352
- fetchRows: async ({ pageIndex, pageSize, sorting, columnFilters, globalFilter }) => {
353
- const res = await fetch("/api/users", {
354
- method: "POST",
355
- headers: { "content-type": "application/json" },
356
- body: JSON.stringify({
357
- pageIndex,
358
- pageSize,
359
- sorting,
360
- columnFilters,
361
- q: globalFilter,
362
- }),
363
- })
364
-
365
- if (!res.ok) throw new Error("Failed to load users")
366
- return res.json() as Promise<{ rows: User[]; totalRecords: number }>
367
- },
368
- mode: "server",
369
- onError: (error, context) => {
370
- toast.error(context.message)
371
- console.error(error)
372
- },
373
- }}
374
- />
375
- ```
376
-
377
- Internal mode behavior:
378
-
379
- - Initial load shows the table skeleton.
380
- - Initial load failure shows an inline `Could not load rows` state with `Retry`.
381
- - Refresh failure keeps the last successful rows visible and calls `onError`.
382
- - `mode: "client"` expects the full row array and lets the table sort/filter/page in memory.
383
- - `mode: "server"` expects the current page and uses `totalRecords` for pagination.
384
-
385
- Keep using `onCellEdit`, `onRowSave`, `onAddRow`, and `onBulkDelete` for mutations.
386
- The table does not assume your write API; this lets you choose optimistic updates,
387
- rollback, toast notifications, and validation.
388
-
389
- ---
390
-
391
- ## Theming
392
-
393
- The `theme` prop accepts **two shapes**. Pick whichever fits your use case.
394
-
395
- ### Shape 1 — Flat tokens
396
-
397
- ```tsx
398
- <DataTable
399
- theme={{
400
- primary: "oklch(0.6 0.2 200)",
401
- primaryForeground: "oklch(1 0 0)",
402
- accent: "oklch(0.94 0.05 200)",
403
- radius: "0.25rem",
404
- fontFamily: "Inter, system-ui, sans-serif",
405
- }}
406
- />
407
- ```
408
-
409
- All shadcn tokens are supported plus `radius` and `fontFamily`. Anything you omit falls through to whatever your app's `:root` provides.
410
-
411
- ### Shape 2 — Moded `{ light, dark }`
412
-
413
- A moded theme repaints the whole table **and** auto-flips on dark mode (OS preference *or* a `.dark` ancestor):
414
-
415
- ```tsx
416
- <DataTable
417
- theme={{
418
- light: { background: "oklch(0.99 0.005 285)", primary: "oklch(0.55 0.22 285)", /* … */ },
419
- dark: { background: "oklch(0.16 0.012 285)", primary: "oklch(0.7 0.18 285)", /* … */ },
420
- }}
421
- />
422
- ```
423
-
424
- The grid emits a tiny scoped `<style>` block that targets only this instance — multiple grids on the same page can wear different moded themes without interfering.
425
-
426
- ### Use a preset
427
-
428
- Presets ship in **moded shape** — passing one repaints the entire table and follows dark mode automatically:
429
-
430
- ```tsx
431
- import { DataTable, themePresets } from "@dynostack/react-grid"
432
-
433
- <DataTable theme={themePresets.violet} />
434
- ```
435
-
436
- Available presets:
437
-
438
- | Preset | Hue |
439
- |---|---|
440
- | `neutral` | Grayscale (default appearance) |
441
- | `light` | Force light, no dark variant |
442
- | `dark` | Force dark, no light variant |
443
- | `violet` | 285° |
444
- | `emerald` | 162° |
445
- | `amber` | 65° |
446
- | `rose` | 15° |
447
- | `sky` | 235° |
448
- | `slate` | 240° (low chroma) |
449
-
450
- ### Build a custom preset from a single hue
451
-
452
- ```tsx
453
- import { DataTable, buildPreset } from "@dynostack/react-grid"
454
-
455
- const teal = buildPreset(180) // hue only
456
- const subtleTeal = buildPreset(180, 0.015) // hue + custom chroma
457
-
458
- <DataTable theme={teal} />
459
- ```
460
-
461
- `buildPreset(hue, chroma?)` returns a full `{ light, dark }` token set tinted around the given OKLCH hue.
462
-
463
- ### Compose with a preset
464
-
465
- ```tsx
466
- <DataTable
467
- theme={{
468
- ...themePresets.violet,
469
- light: { ...themePresets.violet.light, primary: "oklch(0.7 0.18 250)" },
470
- }}
471
- />
472
- ```
473
-
474
- ### Isolate a grid from the app theme
475
-
476
- When embedding inside a heavily-themed shell where you want the table to keep its own look:
477
-
478
- ```tsx
479
- <DataTable isolate /* uses bundled neutral tokens, ignores app :root */ />
480
- <DataTable isolate theme={themePresets.violet} /* isolated AND violet */ />
481
- ```
482
-
483
- ### Multiple grids, different themes
484
-
485
- CSS variables are emitted on each table root, so this works:
486
-
487
- ```tsx
488
- <DataTable theme={themePresets.violet} />
489
- <DataTable theme={themePresets.emerald} />
490
- <DataTable theme={{ primary: "oklch(0.6 0.2 200)" }} />
491
- ```
492
-
493
- ### Precedence summary
494
-
495
- ```
496
- ┌──────────────────────────────────────────────────────────┐
497
- │ Inline style on the grid root (per-instance `theme`) │ ← highest
498
- ├──────────────────────────────────────────────────────────┤
499
- │ Consumer's :root rules (shadcn, custom app CSS) │
500
- ├──────────────────────────────────────────────────────────┤
501
- │ @layer dynostack-grid-defaults (bundled styles.css) │ ← lowest
502
- └──────────────────────────────────────────────────────────┘
503
- ```
504
-
505
- ---
506
-
507
- ## Density
508
-
509
- ```tsx
510
- <DataTable density="compact" /* tighter rows */ />
511
- <DataTable density="default" /* shadcn defaults */ />
512
- <DataTable density="comfortable" /* extra padding */ />
513
- ```
514
-
515
- ---
516
-
517
- ## Feature flags
518
-
519
- Every toolbar control and table capability is a switch. Defaults are sensible — only set what you want to disable.
520
-
521
- ```tsx
522
- <DataTable
523
- features={{
524
- search: true, // global search input
525
- refresh: true, // refresh button (when onRefresh is provided)
526
- columnVisibility: true, // columns popover
527
- export: true, // CSV / Excel menu
528
- addRow: true, // "Add row" button (when onAddRow is provided)
529
- pagination: true, // bottom pagination bar
530
- sorting: true, // sort headers
531
- filtering: true, // per-column filter popovers
532
- resizing: true, // resize handles
533
- reordering: true, // drag-to-reorder columns
534
- pinning: true, // pin / unpin column controls
535
- }}
536
- />
537
- ```
538
-
539
- ---
540
-
541
- ## Labels (i18n)
542
-
543
- Every user-facing string is overridable.
544
-
545
- ```tsx
546
- <DataTable
547
- labels={{
548
- search: "Rechercher...",
549
- addRow: "Ajouter",
550
- delete: "Supprimer",
551
- clear: "Effacer",
552
- selected: "sélectionné(s)",
553
- refresh: "Actualiser",
554
- columns: "Colonnes",
555
- export: "Exporter",
556
- csv: "CSV",
557
- excel: "Excel",
558
- total: "Total",
559
- noData: "Aucune donnée.",
560
- noResults: "Aucun résultat.",
561
- refreshing: "Actualisation",
562
- rowsPerPage: "Lignes par page",
563
- page: "Page",
564
- of: "sur",
565
- }}
566
- />
567
- ```
568
-
569
- ---
570
-
571
- ## Column meta
572
-
573
- Each column can declare:
574
-
575
- ```ts
576
- type ColumnMeta = {
577
- label?: string // header label & filter title
578
- editor?:
579
- | "text" | "number" | "currency" | "date"
580
- | "select" | "switch" | "checkbox"
581
- filterType?:
582
- | "text" | "number" | "date"
583
- | "select" | "multi-select" | "boolean"
584
- selectOptions?: { value: string; label: string }[]
585
- align?: "left" | "right" | "center"
586
- cellClassName?: string
587
- headerClassName?: string
588
- exportable?: boolean
589
- isEditable?: boolean | ((row) => boolean)
590
- badgeMap?: Partial<
591
- Record<string,
592
- "default" | "secondary" | "destructive" |
593
- "success" | "warning" | "outline"
594
- >
595
- >
596
- }
597
- ```
598
-
599
- The default `filterFn` for a column is wired automatically from `meta.filterType`. You can still set a custom `filterFn` on the column to override it.
600
-
601
- ---
602
-
603
- ## Editing
604
-
605
- Two modes, both prop-driven, both work simultaneously:
606
-
607
- ### Single cell — double-click
608
-
609
- ```tsx
610
- <DataTable
611
- onCellEdit={(row, columnId, value) =>
612
- saveMutation.mutate({ ...row, [columnId]: value })
613
- }
614
- />
615
- ```
616
-
617
- ### Whole row — Edit action → Save / Cancel
618
-
619
- ```tsx
620
- <DataTable
621
- onRowSave={(row, draft) =>
622
- saveMutation.mutate({ ...row, ...draft })
623
- }
624
- />
625
- ```
626
-
627
- `isEditable` on `meta` can disable editing for individual rows or columns:
628
-
629
- ```tsx
630
- {
631
- accessorKey: "email",
632
- meta: {
633
- editor: "text",
634
- isEditable: (row) => row.role !== "billing",
635
- },
636
- }
637
- ```
638
-
639
- ---
640
-
641
- ## Filters
642
-
643
- The package exports the filter primitives so you can build custom panels too:
644
-
645
- ```ts
646
- import {
647
- textFilterFn,
648
- numberFilterFn,
649
- dateFilterFn,
650
- setFilterFn,
651
- booleanFilterFn,
652
- type AdvFilter,
653
- type SetFilter,
654
- type TextOp,
655
- type NumberOp,
656
- type DateOp,
657
- } from "@dynostack/react-grid"
658
- ```
659
-
660
- | `filterType` | Operators | Value shape |
661
- | -------------- | -------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- |
662
- | `text` | `contains`, `notContains`, `equals`, `notEqual`, `startsWith`, `endsWith`, `blank`, `notBlank` | `AdvFilter<TextOp, string>` |
663
- | `number` | `equals`, `notEqual`, `lessThan`, `lessThanOrEqual`, `greaterThan`, `greaterThanOrEqual`, `inRange`, `blank`, `notBlank` | `AdvFilter<NumberOp, number>` |
664
- | `date` | `equals`, `notEqual`, `before`, `after`, `inRange`, `blank`, `notBlank` | `AdvFilter<DateOp, string>` |
665
- | `select` | set filter | `SetFilter` (`{ selected: string[] }`) |
666
- | `multi-select` | set filter | `SetFilter` (`{ selected: string[] }`) |
667
- | `boolean` | `All` / `True` / `False` | `boolean \| undefined` |
668
-
669
- Text/number/date panels also expose **AND/OR combine** of a second condition, ag-grid style.
670
-
671
- The set filter automatically derives unique values from the visible rows when `selectOptions` is not declared — search box, "Select all (filtered)" with indeterminate state, individual checkboxes.
672
-
673
- ---
674
-
675
- ## Selection & bulk actions
676
-
677
- Selection is on by default (`enableSelection: true`). When any row is selected the toolbar swaps in:
678
-
679
- - A `<count> selected` badge
680
- - `Delete` button → opens the confirmation dialog first, then calls `onBulkDelete?(rows)` after confirm
681
- - `Clear` button → resets selection
682
-
683
- ```tsx
684
- <DataTable
685
- onBulkDelete={(rows) => removeMany(rows.map((r) => r.id))}
686
- />
687
- ```
688
-
689
- ---
690
-
691
- ## Expandable rows
692
-
693
- ### Sub-row panel (custom JSX)
694
-
695
- ```tsx
696
- <DataTable
697
- renderSubRow={(row) => <UserAuditPanel user={row} />}
698
- />
699
- ```
700
-
701
- ### Nested rows (TanStack `getSubRows`)
702
-
703
- ```tsx
704
- <DataTable
705
- getSubRows={(row) => row.children}
706
- />
707
- ```
708
-
709
- When either is set, an `__expand` chevron column is added and pinned right next to `__select`.
710
-
711
- ---
712
-
713
- ## Export
714
-
715
- ```tsx
716
- <DataTable exportFileName="users" />
717
- ```
718
-
719
- Toolbar `Export` menu offers **CSV** and **Excel**. If any rows are selected, the menu becomes "Export N selected"; otherwise it exports all visible (filtered) rows.
720
-
721
- Mark a column non-exportable via `meta.exportable: false`.
722
-
723
- ---
724
-
725
- ## View sheet
726
-
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.
736
-
737
- Works out of the box with no props. Customize via `viewSheet`:
738
-
739
- ```tsx
740
- <DataTable
741
- viewSheet={{
742
- side: "right", // or "left"
743
- defaultDensity: "relaxed", // initial layout density
744
- hideDensityTabs: true, // hide the layout picker
745
- fields: ["name", "email", "role"], // limit / reorder shown columns
746
- renderField: ({ column, value, row }) => // override how a value renders
747
- column.id === "phone" ? <a href={`tel:${value}`}>{String(value)}</a> : null,
748
- renderHeader: (row) => <YourCustomHeader row={row} />,
749
- labels: {
750
- title: (row) => `${row.name} (${row.role})`,
751
- description: (row) => `Joined ${row.joinedAt}`,
752
- emptyValue: "—",
753
- density: { compact: "3 cols", relaxed: "2 cols", comfy: "1 col" },
754
- },
755
- }}
756
- onView={(row) => track("user.view", row)} // optional side-effect
757
- />
758
- ```
759
-
760
- Disable the built-in sheet entirely:
761
-
762
- ```tsx
763
- <DataTable viewSheet={false} onView={(row) => router.push(`/users/${row.id}`)} />
764
- ```
765
-
766
- `onView` fires before the sheet opens, so you can navigate / log / fetch alongside it.
767
-
768
- ---
769
-
770
- ## Delete confirmation
771
-
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.
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
-
776
- ```tsx
777
- <DataTable
778
- onDelete={(row) => api.deleteUser(row.id)}
779
- onBulkDelete={(rows) => api.bulkDelete(rows.map(r => r.id))}
780
- confirmDelete={{
781
- title: ({ rows, source }) =>
782
- source === "bulk"
783
- ? `Delete ${rows.length} users?`
784
- : `Delete ${rows[0].name}?`,
785
- description: ({ rows }) =>
786
- `${rows.length === 1 ? "This user" : "These users"} will be permanently removed. This cannot be undone.`,
787
- confirmLabel: "Yes, delete",
788
- cancelLabel: "Keep",
789
- }}
790
- />
791
- ```
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
-
795
- Skip the dialog (fire immediately):
796
-
797
- ```tsx
798
- <DataTable confirmDelete={false} onDelete={(row) => softDelete(row)} />
799
- ```
800
-
801
- ---
802
-
803
- ## Custom row actions
804
-
805
- ```tsx
806
- <DataTable
807
- rowActions={["view", "edit", "duplicate", "delete"]}
808
- customRowActions={[
809
- {
810
- id: "suspend",
811
- label: "Suspend",
812
- icon: <BanIcon />,
813
- danger: true,
814
- show: (r) => r.status !== "suspended",
815
- },
816
- { id: "archive", label: "Archive", icon: <ArchiveIcon /> },
817
- ]}
818
- onRowAction={(action, row) => {
819
- if (action === "suspend") saveMutation.mutate({ ...row, status: "suspended" })
820
- // ...
821
- }}
822
- onDelete={(row) => deleteMutation.mutate([row.id])}
823
- />
824
- ```
825
-
826
- ---
827
-
828
- ## Server-side data
829
-
830
- You can do server-side data in either mode.
831
-
832
- ### Controlled server-side data
833
-
834
- Own the API call in your page and pass the result into the table:
835
-
836
- ```tsx
837
- const [q, setQ] = useState("")
838
- const usersQuery = useQuery({
839
- queryKey: ["users", q],
840
- queryFn: () => fetchUsers({ q }),
841
- })
842
-
843
- <DataTable
844
- data={usersQuery.data ?? []}
845
- isLoading={usersQuery.isLoading}
846
- isFetching={usersQuery.isFetching}
847
- onRefresh={() => usersQuery.refetch()}
848
- globalFilter={q}
849
- onGlobalFilterChange={setQ}
850
- totalRecords={usersQuery.data?.length ?? 0}
851
- />
852
- ```
853
-
854
- Pair this with TanStack Query's pagination/cursor utilities when you want query
855
- caching and mutation orchestration outside the grid.
856
-
857
- ### Built-in server-side data
858
-
859
- Let the grid call your API by setting `dataSource.mode` to `"server"`:
860
-
861
- ```tsx
862
- <DataTable<User>
863
- columns={columns}
864
- dataSource={{
865
- mode: "server",
866
- fetchRows: async (state) => {
867
- const res = await fetch("/api/users/grid", {
868
- method: "POST",
869
- headers: { "content-type": "application/json" },
870
- body: JSON.stringify(state),
871
- })
872
-
873
- if (!res.ok) throw new Error("Users request failed")
874
- return res.json() as Promise<{ rows: User[]; totalRecords: number }>
875
- },
876
- onError: (_error, context) => {
877
- toast.error(context.message)
878
- },
879
- }}
880
- />
881
- ```
882
-
883
- `state` contains `pageIndex`, `pageSize`, `sorting`, `columnFilters`, and
884
- `globalFilter`. In server mode the table assumes the API already applied those
885
- operations and only renders the returned page.
886
-
887
- ---
888
-
889
- ## API reference
890
-
891
- | Prop | Type | Default | Description |
892
- | ------------------------- | ---------------------------------------------------------- | ---------------------- | ------------------------------------------------------ |
893
- | `data` | `TData[]` | `[]` | Controlled row data. Use this when fetching outside the table. |
894
- | `columns` | `ColumnDef<TData>[]` | — | TanStack column definitions. |
895
- | `dataSource` | `DataTableDataSource<TData>` | — | Optional internal fetcher for client/server data loading. |
896
- | `isLoading` | `boolean` | `false` | Initial skeleton state. |
897
- | `isFetching` | `boolean` | `false` | Background-refresh indicator. |
898
- | `onRefresh` | `() => void` | — | Refresh button handler. |
899
- | `totalRecords` | `number` | `data.length` | Total count badge in toolbar. |
900
- | `exportFileName` | `string` | `"export"` | Base filename for CSV / Excel export. |
901
- | `enableSelection` | `boolean` | `true` | Show the `__select` column. |
902
- | `renderSubRow` | `(row: TData) => ReactNode` | — | Custom expandable panel. |
903
- | `getSubRows` | `(row: TData) => TData[] \| undefined` | — | Nested rows accessor. |
904
- | `rowActions` | `("view" \| "edit" \| "duplicate" \| "delete")[]` | all four | Built-in row actions. |
905
- | `customRowActions` | `CustomRowAction<TData>[]` | `[]` | Extra row actions. |
906
- | `onRowAction` | `(action, row) => void` | — | Row action handler. |
907
- | `onCellEdit` | `(row, columnId, value) => void` | — | Single-cell save handler. |
908
- | `onRowSave` | `(row, draft) => void` | — | Row-edit save handler. |
909
- | `onAddRow` | `() => Partial<TData>` | — | Returns the empty draft for "Add row". |
910
- | `onBulkDelete` | `(rows: TData[]) => void` | — | Bulk delete handler. |
911
- | `initialPageSize` | `number` | `10` | Initial pagination size. |
912
- | `pageSizeOptions` | `number[]` | shadcn defaults | Page-size dropdown options. |
913
- | `initialColumnPinning` | `ColumnPinningState` | `{ left: [], right: [] }` | Initial pinned columns. |
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. |
921
- | `globalFilter` | `string` | uncontrolled | Controlled global filter value. |
922
- | `onGlobalFilterChange` | `(value: string) => void` | — | Controlled global filter setter. |
923
- | `className` | `string` | — | Extra classes on the table root. |
924
- | `toolbarSlot` | `ReactNode` | — | Custom JSX prepended into the toolbar. |
925
- | `features` | `DataTableFeatures` | all on | Feature flags. |
926
- | `labels` | `DataTableLabels` | English defaults | i18n labels. |
927
- | `density` | `"compact" \| "default" \| "comfortable"` | `"default"` | Row density. |
928
- | `theme` | `DataTableTheme` | inherits `:root` | Per-instance CSS-variable overrides. Accepts flat tokens **or** `{ light, dark }`. |
929
- | `isolate` | `boolean` | `false` | Ignore the app's `:root` and render with bundled defaults. |
930
- | `onView` | `(row: TData) => void` | — | Side-effect when "View" is clicked. Fires *before* the sheet opens. |
931
- | `onDelete` | `(row: TData) => void` | — | Single-row delete handler. Fires *after* the confirm modal (or immediately if `confirmDelete={false}`). |
932
- | `viewSheet` | `ViewSheetConfig<TData> \| false` | enabled | Configure or disable the built-in View sheet. |
933
- | `confirmDelete` | `ConfirmDeleteConfig<TData> \| boolean` | `true` | Configure or disable the delete confirmation modal (applies to single + bulk). |
934
-
935
- `TData` must extend `{ id: string \| number }`.
936
-
937
- ```ts
938
- type DataTableDataSource<TData> = {
939
- fetchRows: (params: {
940
- pageIndex: number
941
- pageSize: number
942
- sorting: SortingState
943
- columnFilters: ColumnFiltersState
944
- globalFilter: string
945
- }) => Promise<TData[] | { rows: TData[]; totalRecords?: number }>
946
- mode?: "client" | "server"
947
- enabled?: boolean
948
- initialData?: TData[]
949
- deps?: readonly unknown[]
950
- onError?: (
951
- error: unknown,
952
- context: { type: "load" | "refresh"; message: string }
953
- ) => void
954
- }
955
- ```
956
-
957
- ```ts
958
- // Theme types
959
- type DataTableTokens = {
960
- background?: string
961
- foreground?: string
962
- card?: string
963
- cardForeground?: string
964
- popover?: string
965
- popoverForeground?: string
966
- primary?: string
967
- primaryForeground?: string
968
- secondary?: string
969
- secondaryForeground?: string
970
- muted?: string
971
- mutedForeground?: string
972
- accent?: string
973
- accentForeground?: string
974
- destructive?: string
975
- destructiveForeground?: string
976
- border?: string
977
- input?: string
978
- ring?: string
979
- radius?: string
980
- fontFamily?: string
981
- }
982
-
983
- type DataTableModedTheme = {
984
- light?: DataTableTokens
985
- dark?: DataTableTokens
986
- }
987
-
988
- type DataTableTheme = DataTableTokens | DataTableModedTheme
989
- ```
990
-
991
- ```ts
992
- // Theme exports
993
- import {
994
- themePresets, // ready-made moded presets
995
- buildPreset, // (hue, chroma?) => DataTableModedTheme
996
- splitTheme, // (theme) => { light, dark }
997
- tokensToStyle, // (tokens) => React.CSSProperties
998
- tokensToCssBlock, // (tokens) => "var:val;var:val" string
999
- ISOLATE_LIGHT_TOKENS,
1000
- ISOLATE_DARK_TOKENS,
1001
- } from "@dynostack/react-grid"
1002
- ```
1003
-
1004
- ```ts
1005
- // View sheet types
1006
- type ViewSheetDensity = "compact" | "relaxed" | "comfy"
1007
-
1008
- type ViewSheetConfig<TData> = {
1009
- side?: "right" | "left" | "top" | "bottom"
1010
- defaultDensity?: ViewSheetDensity
1011
- hideDensityTabs?: boolean
1012
- fields?: string[]
1013
- renderField?: (args: {
1014
- column: Column<TData, unknown>
1015
- value: unknown
1016
- row: TData
1017
- }) => React.ReactNode
1018
- renderHeader?: (row: TData) => React.ReactNode
1019
- labels?: {
1020
- title?: (row: TData) => React.ReactNode
1021
- description?: (row: TData) => React.ReactNode
1022
- emptyValue?: string
1023
- density?: { compact?: string; relaxed?: string; comfy?: string }
1024
- }
1025
- }
1026
-
1027
- // Confirm-delete types
1028
- type ConfirmDeleteContext<TData> = {
1029
- rows: TData[]
1030
- source: "single" | "bulk"
1031
- }
1032
-
1033
- type ConfirmDeleteConfig<TData> = {
1034
- title?: (ctx: ConfirmDeleteContext<TData>) => React.ReactNode
1035
- description?: (ctx: ConfirmDeleteContext<TData>) => React.ReactNode
1036
- confirmLabel?: string
1037
- cancelLabel?: string
1038
- }
1039
- ```
1040
-
1041
- ---
1042
-
1043
- ## Compatibility
1044
-
1045
- | Stack | Tested on |
1046
- | ----------------- | -------------------------- |
1047
- | React | 18.x · 19.x |
1048
- | TanStack Table | 8.21+ |
1049
- | Tailwind CSS | 3.x · 4.x |
1050
- | Bundler | Vite · Next.js · Webpack 5 |
1051
-
1052
- ESM and CJS bundles ship side-by-side. Tree-shakeable. Marked `"use client"` for Next.js App Router compatibility.
1053
-
1054
- ---
1055
-
1056
- ## Roadmap
1057
-
1058
- - [ ] Server-side pagination/sorting helpers (controlled-state recipes)
1059
- - [ ] Column groups (header rowSpan/colSpan)
1060
- - [ ] Pivot mode
1061
- - [ ] Aggregation row (sum, avg, min, max, count)
1062
- - [ ] Saved view profiles (filter + visibility + pinning snapshots)
1063
- - [ ] Virtualized rows (TanStack Virtual integration)
1064
- - [ ] Storybook + visual regression tests
1065
- - [ ] CodeSandbox / StackBlitz starter
1066
-
1067
- Have a use case that isn't covered? [Open an issue](https://github.com/wanted-coder-vijay/wcv-data-grid/issues/new) — happy to consider it.
1068
-
1069
- ---
1070
-
1071
- ## Contributing
1072
-
1073
- ```sh
1074
- git clone https://github.com/wanted-coder-vijay/wcv-data-grid.git
1075
- cd wcv-data-grid
1076
- npm install
1077
- npm run dev # tsup --watch
1078
- npm run typecheck # tsc --noEmit
1079
- npm run build # produce dist/
1080
- ```
1081
-
1082
- PRs welcome. Please keep the prop API additive — feature toggles over breaking changes.
1083
-
1084
- ---
1085
-
1086
- ## License
1087
-
1088
- [Apache-2.0](./LICENSE) · Copyright © 2026 vijay kumar anchupogu (wanted-coder-vijay)
1089
-
1090
- See [NOTICE](./NOTICE) for attribution requirements.
1091
-
1092
- > **Package history.** This package was briefly published as
1093
- > `@dynostack/gridstack@0.1.0` under the MIT license before being
1094
- > renamed to `@dynostack/react-grid` and relicensed under Apache-2.0.
1095
- > The old name is deprecated; new code should depend on
1096
- > `@dynostack/react-grid` only.
1
+ <div align="center">
2
+
3
+ # @dynostack/react-grid
4
+
5
+ **Enterprise-grade React data grid. Drop-in.**
6
+
7
+ Built on [TanStack Table v8](https://tanstack.com/table) · [Radix UI](https://www.radix-ui.com/) · [Tailwind CSS](https://tailwindcss.com/) · ships shadcn/ui look-and-feel out of the box.
8
+
9
+ [![npm version](https://img.shields.io/npm/v/@dynostack/react-grid.svg?style=flat-square)](https://www.npmjs.com/package/@dynostack/react-grid)
10
+ [![bundle size](https://img.shields.io/bundlephobia/minzip/@dynostack/react-grid?style=flat-square)](https://bundlephobia.com/package/@dynostack/react-grid)
11
+ [![license](https://img.shields.io/npm/l/@dynostack/react-grid.svg?style=flat-square)](./LICENSE)
12
+ [![types](https://img.shields.io/npm/types/@dynostack/react-grid?style=flat-square)](./dist/index.d.ts)
13
+
14
+ </div>
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
+
20
+ ---
21
+
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.
23
+
24
+ ## Showcase
25
+
26
+ A graphite studio for your data. Switch between light and dark, configure every feature, and copy the React configuration in the [live playground](https://dynostack-react-grid.vercel.app/playground).
27
+
28
+ | Graphite dark | Precision light |
29
+ | --- | --- |
30
+ | ![Dark table playground](https://dynostack-react-grid.vercel.app/table-images/grid-dark.png) | ![Light table playground](https://dynostack-react-grid.vercel.app/table-images/grid-light.png) |
31
+
32
+ | Filter builder | Selection and export |
33
+ | --- | --- |
34
+ | ![Set filter with conditions](https://dynostack-react-grid.vercel.app/table-images/grid-filter.png) | ![Selected row with export menu](https://dynostack-react-grid.vercel.app/table-images/grid-selection.png) |
35
+
36
+ ```tsx
37
+ import { DataTable, themePresets } from '@dynostack/react-grid'
38
+
39
+ // Add .dark to an ancestor for dark mode, or .light for light mode.
40
+ <DataTable data={rows} columns={columns} theme={themePresets.graphite} />
41
+ ```
42
+
43
+ ## Highlights
44
+
45
+ - **Layout and state controls** — `initialSorting`, `onSelectionChange`, `ariaLabel`, `striped`, `stickyHeader`, and `maxHeight`. Disabling pagination renders all matching loaded rows.
46
+ - **Safer exports and editing** — spreadsheet formulas in untrusted text are neutralized; row editing respects `meta.isEditable`; theme values cannot escape scoped CSS declarations.
47
+
48
+ - **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
49
+ - **Inline editing** — double-click cell to edit, or enter row-edit mode with `Save` / `Cancel`
50
+ - **Add row** — local optimistic insert, edit, then commit on save
51
+ - **Per-column sort, hide, pin, resize, drag-reorder** — pinned columns are fully opaque while you scroll horizontally
52
+ - **Selection + bulk actions** — pinned `__select` column with select-all, clear, bulk delete
53
+ - **Expandable rows** — provide a `renderSubRow` panel or use TanStack's nested `getSubRows`
54
+ - **CSV / Excel export** — selection-aware (export selected vs. all)
55
+ - **Built-in row Details panel** — `View` opens a scoped sheet with compact, relaxed, and comfy field layouts
56
+ - **Scoped delete confirmation** — row and bulk delete confirmations stay inside the table instead of covering the entire app
57
+ - **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 (`graphite`, `violet`, `emerald`, `amber`, `rose`, `sky`, `slate`, …), `buildPreset(hue)` for custom hues, and `isolate` to opt out of inheriting the app theme
58
+ - **Density** — `compact` · `default` · `comfortable`
59
+ - **i18n / labels** — every visible string is overridable
60
+ - **Feature flags** — turn off any toolbar control or table capability with a single boolean
61
+ - **Tiny API, full TypeScript** — one component, fully typed generics, no provider context to wire up
62
+
63
+ ---
64
+
65
+ ## Table of contents
66
+
67
+ - [Install](#install)
68
+ - [Showcase](#showcase)
69
+ - [Tailwind setup](#tailwind-setup)
70
+ - [Theme tokens](#theme-tokens)
71
+ - [Quick start](#quick-start)
72
+ - [Data fetching](#data-fetching)
73
+ - [Theming](#theming)
74
+ - [Density](#density)
75
+ - [Feature flags](#feature-flags)
76
+ - [Labels (i18n)](#labels-i18n)
77
+ - [Column meta](#column-meta)
78
+ - [Editing](#editing)
79
+ - [Filters](#filters)
80
+ - [Selection & bulk actions](#selection--bulk-actions)
81
+ - [Expandable rows](#expandable-rows)
82
+ - [Export](#export)
83
+ - [View sheet](#view-sheet)
84
+ - [Delete confirmation](#delete-confirmation)
85
+ - [Custom row actions](#custom-row-actions)
86
+ - [Server-side data](#server-side-data)
87
+ - [API reference](#api-reference)
88
+ - [Compatibility](#compatibility)
89
+ - [Roadmap](#roadmap)
90
+ - [Contributing](#contributing)
91
+ - [License](#license)
92
+
93
+ ---
94
+
95
+ ## Install
96
+
97
+ ### Run the showcase locally
98
+
99
+ ```sh
100
+ git clone https://github.com/wanted-coder-vijay/wcv-data-grid.git
101
+ cd wcv-data-grid
102
+ npm install
103
+ npm run build
104
+ npm install --prefix showcase
105
+ npm run showcase:dev
106
+ ```
107
+
108
+ ### New layout controls
109
+
110
+ ```tsx
111
+ <DataTable
112
+ data={rows}
113
+ columns={columns}
114
+ initialSorting={[{ id: "name", desc: false }]}
115
+ onSelectionChange={(selectedRows) => setSelectedRows(selectedRows)}
116
+ ariaLabel="Project workspace"
117
+ striped
118
+ stickyHeader
119
+ maxHeight="480px"
120
+ />
121
+ ```
122
+
123
+ `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.
124
+
125
+ ```sh
126
+ npm i @dynostack/react-grid
127
+ # or
128
+ pnpm add @dynostack/react-grid
129
+ # or
130
+ yarn add @dynostack/react-grid
131
+ ```
132
+
133
+ **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.
134
+
135
+ ## Tailwind setup
136
+
137
+ The component ships Tailwind class names verbatim, so your Tailwind build needs to know two things:
138
+
139
+ 1. **Where to scan** for the class strings inside the bundle.
140
+ 2. **Which semantic color tokens** (`bg-popover`, `bg-card`, `text-foreground`, …) exist.
141
+
142
+ The package's `styles.css` registers the tokens for you via Tailwind v4's `@theme inline`. You only need to wire scanning.
143
+
144
+ ### Tailwind v4 — zero config
145
+
146
+ ```css
147
+ /* your global stylesheet (e.g. src/index.css) */
148
+ @import "tailwindcss";
149
+ @source "../node_modules/@dynostack/react-grid/dist";
150
+ @import "@dynostack/react-grid/styles.css";
151
+ @import "@dynostack/react-grid/page.css"; /* optional: extend tokens to <body> */
152
+ ```
153
+
154
+ That's the whole setup. No `tailwind.config.js`, no `@theme` block to copy-paste, no shadcn install required. Overlay surfaces (popovers, dropdowns, sheets, the row-actions menu) all render correctly out of the box.
155
+
156
+ ### Tailwind v3
157
+
158
+ v3 doesn't read CSS `@theme` directives, so the semantic-color mapping has to live in your `tailwind.config.js`. The shadcn install guide for v3 covers the exact `theme.extend.colors` block you need — copy that, plus add the package's `dist` to your `content` array:
159
+
160
+ ```js
161
+ // tailwind.config.{js,ts}
162
+ export default {
163
+ content: [
164
+ "./src/**/*.{ts,tsx}",
165
+ "./node_modules/@dynostack/react-grid/dist/**/*.{js,mjs,cjs}",
166
+ ],
167
+ theme: {
168
+ extend: {
169
+ colors: {
170
+ // copy the shadcn v3 color mapping here
171
+ // (background, foreground, card, popover, primary, secondary,
172
+ // muted, accent, destructive, border, input, ring)
173
+ background: "hsl(var(--background))",
174
+ foreground: "hsl(var(--foreground))",
175
+ // … etc
176
+ },
177
+ },
178
+ },
179
+ }
180
+ ```
181
+
182
+ Then import `styles.css` as usual:
183
+
184
+ ```ts
185
+ import "@dynostack/react-grid/styles.css"
186
+ ```
187
+
188
+ > Starting a new project? **Use Tailwind v4.** The v4 path above is meaningfully simpler — the package handles token registration for you.
189
+
190
+ ## Theme tokens
191
+
192
+ The grid is built on **shadcn/ui CSS variables**. It auto-adjusts to whatever theme your app already has:
193
+
194
+ | Your app has… | What you do | What you get |
195
+ |---|---|---|
196
+ | Nothing (bare React) | `import "@dynostack/react-grid/styles.css"` | Clean light theme, auto-switches to dark on OS preference. |
197
+ | shadcn/ui (default theme) | Nothing | Grid inherits your `:root` tokens automatically. |
198
+ | shadcn/ui with a custom theme (Stone / Zinc / your own hue) | Nothing | Grid picks up your custom tokens automatically. |
199
+ | Custom theme using shadcn token names | Nothing | Same as above. |
200
+ | Custom theme with non-shadcn names | Pass [`theme` prop](#theming) | Per-instance override mapped to shadcn vars. |
201
+ | Want one grid to ignore the app theme | Pass `isolate` | Grid uses bundled defaults regardless of `:root`. |
202
+
203
+ **Why this just works.** The bundled `styles.css`:
204
+
205
+ 1. **Registers Tailwind v4 utility tokens** via a top-level `@theme inline` block — so `bg-popover`, `text-foreground`, `border-border`, etc. resolve to your tokens without any consumer-side `@theme` block.
206
+ 2. **Declares variable values inside the `dynostack-grid-defaults` cascade layer** — any unlayered consumer rule (which is where shadcn and most app CSS lives) automatically wins, regardless of import order. You can't accidentally overwrite your app's theme by importing the grid's stylesheet.
207
+
208
+ ### Minimal install (Tailwind v4)
209
+
210
+ ```css
211
+ /* your global stylesheet */
212
+ @import "tailwindcss";
213
+ @source "../node_modules/@dynostack/react-grid/dist";
214
+ @import "@dynostack/react-grid/styles.css";
215
+ ```
216
+
217
+ See the [Tailwind setup](#tailwind-setup) section for v3.
218
+
219
+ ### Optional: extend the theme to the page
220
+
221
+ By default the grid only styles itself, not the surrounding page. If you want `<body>` to use the same background/foreground as the grid:
222
+
223
+ ```ts
224
+ import "@dynostack/react-grid/styles.css"
225
+ import "@dynostack/react-grid/page.css" // optional
226
+ ```
227
+
228
+ ### Dark mode
229
+
230
+ | Mode | How to enable | Behavior |
231
+ |---|---|---|
232
+ | Follow OS | Default — no action required | Light by day, dark by night via `prefers-color-scheme`. |
233
+ | Force light | Add `class="light"` to `<html>` | Stays light regardless of OS. |
234
+ | Force dark | Add `class="dark"` to `<html>` | Stays dark regardless of OS. |
235
+ | Per-instance | `<DataTable theme={themePresets.violet}>` | Grid auto-flips light/dark inside the moded preset. |
236
+
237
+ Either way you can still override any token per-instance via the [`theme`](#theming) prop.
238
+
239
+ ---
240
+
241
+ ## Quick start
242
+
243
+ ```tsx
244
+ import { DataTable } from "@dynostack/react-grid"
245
+ import "@dynostack/react-grid/styles.css" // optional — only if you don't have shadcn tokens
246
+
247
+ type User = {
248
+ id: number
249
+ name: string
250
+ email: string
251
+ role: "admin" | "viewer"
252
+ joinedAt: string
253
+ }
254
+
255
+ const columns = [
256
+ { accessorKey: "id", header: "ID", size: 70 },
257
+ {
258
+ accessorKey: "name",
259
+ header: "Name",
260
+ meta: { label: "Name", editor: "text", filterType: "text" },
261
+ },
262
+ {
263
+ accessorKey: "email",
264
+ header: "Email",
265
+ meta: { label: "Email", editor: "text", filterType: "text" },
266
+ },
267
+ {
268
+ accessorKey: "role",
269
+ header: "Role",
270
+ meta: {
271
+ label: "Role",
272
+ editor: "select",
273
+ filterType: "multi-select",
274
+ selectOptions: [
275
+ { value: "admin", label: "Admin" },
276
+ { value: "viewer", label: "Viewer" },
277
+ ],
278
+ },
279
+ },
280
+ {
281
+ accessorKey: "joinedAt",
282
+ header: "Joined",
283
+ meta: { label: "Joined", editor: "date", filterType: "date" },
284
+ },
285
+ ]
286
+
287
+ export function Users({ data }: { data: User[] }) {
288
+ return (
289
+ <DataTable<User>
290
+ data={data}
291
+ columns={columns}
292
+ onCellEdit={(row, columnId, value) => save({ ...row, [columnId]: value })}
293
+ onRowSave={(row, draft) => save({ ...row, ...draft })}
294
+ onAddRow={() => ({ name: "", email: "", role: "viewer", joinedAt: today() })}
295
+ onBulkDelete={(rows) => removeMany(rows.map((r) => r.id))}
296
+ initialColumnPinning={{ left: ["__select", "id", "name"], right: ["__actions"] }}
297
+ />
298
+ )
299
+ }
300
+ ```
301
+
302
+ That's it. You now have sort + filter + edit + add + delete + export + pin + resize + reorder + select.
303
+
304
+ ---
305
+
306
+ ## Data fetching
307
+
308
+ `DataTable` supports two data ownership models.
309
+
310
+ ### 1. Controlled data from your page
311
+
312
+ Use this when your app already owns fetching with TanStack Query, SWR, Redux,
313
+ loader functions, or custom hooks. The table receives rows and loading flags as
314
+ props, and your app owns error/toast behavior.
315
+
316
+ ```tsx
317
+ const usersQuery = useQuery({
318
+ queryKey: ["users"],
319
+ queryFn: fetchUsers,
320
+ })
321
+
322
+ <DataTable<User>
323
+ data={usersQuery.data ?? []}
324
+ columns={columns}
325
+ isLoading={usersQuery.isLoading}
326
+ isFetching={usersQuery.isFetching}
327
+ onRefresh={() => usersQuery.refetch()}
328
+ totalRecords={usersQuery.data?.length ?? 0}
329
+ />
330
+ ```
331
+
332
+ For blocking load errors, render your own page-level error state or pass an empty
333
+ array. For background errors, show a toast from your query/mutation callbacks.
334
+
335
+ ### 2. Internal fetching with `dataSource`
336
+
337
+ Use this when you want the table to own fetch/loading/error/refresh state.
338
+ `fetchRows` receives the current table state and can return either an array or
339
+ `{ rows, totalRecords }`.
340
+
341
+ ```tsx
342
+ <DataTable<User>
343
+ columns={columns}
344
+ dataSource={{
345
+ fetchRows: async ({ pageIndex, pageSize, sorting, columnFilters, globalFilter }) => {
346
+ const res = await fetch("/api/users", {
347
+ method: "POST",
348
+ headers: { "content-type": "application/json" },
349
+ body: JSON.stringify({
350
+ pageIndex,
351
+ pageSize,
352
+ sorting,
353
+ columnFilters,
354
+ q: globalFilter,
355
+ }),
356
+ })
357
+
358
+ if (!res.ok) throw new Error("Failed to load users")
359
+ return res.json() as Promise<{ rows: User[]; totalRecords: number }>
360
+ },
361
+ mode: "server",
362
+ onError: (error, context) => {
363
+ toast.error(context.message)
364
+ console.error(error)
365
+ },
366
+ }}
367
+ />
368
+ ```
369
+
370
+ Internal mode behavior:
371
+
372
+ - Initial load shows the table skeleton.
373
+ - Initial load failure shows an inline `Could not load rows` state with `Retry`.
374
+ - Refresh failure keeps the last successful rows visible and calls `onError`.
375
+ - `mode: "client"` expects the full row array and lets the table sort/filter/page in memory.
376
+ - `mode: "server"` expects the current page and uses `totalRecords` for pagination.
377
+
378
+ Keep using `onCellEdit`, `onRowSave`, `onAddRow`, and `onBulkDelete` for mutations.
379
+ The table does not assume your write API; this lets you choose optimistic updates,
380
+ rollback, toast notifications, and validation.
381
+
382
+ ---
383
+
384
+ ## Theming
385
+
386
+ The `theme` prop accepts **two shapes**. Pick whichever fits your use case.
387
+
388
+ ### Shape 1 — Flat tokens
389
+
390
+ ```tsx
391
+ <DataTable
392
+ theme={{
393
+ primary: "oklch(0.6 0.2 200)",
394
+ primaryForeground: "oklch(1 0 0)",
395
+ accent: "oklch(0.94 0.05 200)",
396
+ radius: "0.25rem",
397
+ fontFamily: "Inter, system-ui, sans-serif",
398
+ }}
399
+ />
400
+ ```
401
+
402
+ All shadcn tokens are supported plus `radius` and `fontFamily`. Anything you omit falls through to whatever your app's `:root` provides.
403
+
404
+ ### Shape 2 — Moded `{ light, dark }`
405
+
406
+ A moded theme repaints the whole table **and** auto-flips on dark mode (OS preference *or* a `.dark` ancestor):
407
+
408
+ ```tsx
409
+ <DataTable
410
+ theme={{
411
+ light: { background: "oklch(0.99 0.005 285)", primary: "oklch(0.55 0.22 285)", /* … */ },
412
+ dark: { background: "oklch(0.16 0.012 285)", primary: "oklch(0.7 0.18 285)", /* … */ },
413
+ }}
414
+ />
415
+ ```
416
+
417
+ The grid emits a scoped `<style>` block for each instance, so grids can use different moded themes. Set `className="dark"` or `className="light"` on a table to force its mode independently of the page. Moded light tokens live in the scoped stylesheet so they cannot override the dark declarations.
418
+
419
+ ### Use a preset
420
+
421
+ Presets ship in **moded shape** — passing one repaints the entire table and follows dark mode automatically:
422
+
423
+ ```tsx
424
+ import { DataTable, themePresets } from "@dynostack/react-grid"
425
+
426
+ <DataTable theme={themePresets.violet} />
427
+ ```
428
+
429
+ Available presets:
430
+
431
+ | Preset | Hue |
432
+ |---|---|
433
+ | `neutral` | Grayscale (default appearance) |
434
+ | `light` | Force light, no dark variant |
435
+ | `dark` | Force dark, no light variant |
436
+ | `violet` | 285° |
437
+ | `emerald` | 162° |
438
+ | `amber` | 65° |
439
+ | `rose` | 15° |
440
+ | `sky` | 235° |
441
+ | `slate` | 240° (low chroma) |
442
+
443
+ ### Build a custom preset from a single hue
444
+
445
+ ```tsx
446
+ import { DataTable, buildPreset } from "@dynostack/react-grid"
447
+
448
+ const teal = buildPreset(180) // hue only
449
+ const subtleTeal = buildPreset(180, 0.015) // hue + custom chroma
450
+
451
+ <DataTable theme={teal} />
452
+ ```
453
+
454
+ `buildPreset(hue, chroma?)` returns a full `{ light, dark }` token set tinted around the given OKLCH hue.
455
+
456
+ ### Compose with a preset
457
+
458
+ ```tsx
459
+ <DataTable
460
+ theme={{
461
+ ...themePresets.violet,
462
+ light: { ...themePresets.violet.light, primary: "oklch(0.7 0.18 250)" },
463
+ }}
464
+ />
465
+ ```
466
+
467
+ ### Isolate a grid from the app theme
468
+
469
+ When embedding inside a heavily-themed shell where you want the table to keep its own look:
470
+
471
+ ```tsx
472
+ <DataTable isolate /* uses bundled neutral tokens, ignores app :root */ />
473
+ <DataTable isolate theme={themePresets.violet} /* isolated AND violet */ />
474
+ ```
475
+
476
+ ### Multiple grids, different themes
477
+
478
+ CSS variables are emitted on each table root, so this works:
479
+
480
+ ```tsx
481
+ <DataTable theme={themePresets.violet} />
482
+ <DataTable theme={themePresets.emerald} />
483
+ <DataTable theme={{ primary: "oklch(0.6 0.2 200)" }} />
484
+ ```
485
+
486
+ ### Precedence summary
487
+
488
+ ```
489
+ ┌──────────────────────────────────────────────────────────┐
490
+ │ Scoped grid tokens / flat inline theme overrides │ ← highest
491
+ ├──────────────────────────────────────────────────────────┤
492
+ │ Consumer's :root rules (shadcn, custom app CSS) │
493
+ ├──────────────────────────────────────────────────────────┤
494
+ │ @layer dynostack-grid-defaults (bundled styles.css) │ ← lowest
495
+ └──────────────────────────────────────────────────────────┘
496
+ ```
497
+
498
+ ---
499
+
500
+ ## Density
501
+
502
+ ```tsx
503
+ <DataTable density="compact" /* tighter rows */ />
504
+ <DataTable density="default" /* shadcn defaults */ />
505
+ <DataTable density="comfortable" /* extra padding */ />
506
+ ```
507
+
508
+ ---
509
+
510
+ ## Feature flags
511
+
512
+ Every toolbar control and table capability is a switch. Defaults are sensible — only set what you want to disable.
513
+
514
+ ```tsx
515
+ <DataTable
516
+ features={{
517
+ search: true, // global search input
518
+ refresh: true, // refresh button (when onRefresh is provided)
519
+ columnVisibility: true, // columns popover
520
+ export: true, // CSV / Excel menu
521
+ addRow: true, // "Add row" button (when onAddRow is provided)
522
+ pagination: true, // bottom pagination bar
523
+ sorting: true, // sort headers
524
+ filtering: true, // per-column filter popovers
525
+ resizing: true, // resize handles
526
+ reordering: true, // drag-to-reorder columns
527
+ pinning: true, // pin / unpin column controls
528
+ }}
529
+ />
530
+ ```
531
+
532
+ ---
533
+
534
+ ## Labels (i18n)
535
+
536
+ Every user-facing string is overridable.
537
+
538
+ ```tsx
539
+ <DataTable
540
+ labels={{
541
+ search: "Rechercher...",
542
+ addRow: "Ajouter",
543
+ delete: "Supprimer",
544
+ clear: "Effacer",
545
+ selected: "sélectionné(s)",
546
+ refresh: "Actualiser",
547
+ columns: "Colonnes",
548
+ export: "Exporter",
549
+ csv: "CSV",
550
+ excel: "Excel",
551
+ total: "Total",
552
+ noData: "Aucune donnée.",
553
+ noResults: "Aucun résultat.",
554
+ refreshing: "Actualisation",
555
+ rowsPerPage: "Lignes par page",
556
+ page: "Page",
557
+ of: "sur",
558
+ }}
559
+ />
560
+ ```
561
+
562
+ ---
563
+
564
+ ## Column meta
565
+
566
+ Each column can declare:
567
+
568
+ ```ts
569
+ type ColumnMeta = {
570
+ label?: string // header label & filter title
571
+ editor?:
572
+ | "text" | "number" | "currency" | "date"
573
+ | "select" | "switch" | "checkbox"
574
+ filterType?:
575
+ | "text" | "number" | "date"
576
+ | "select" | "multi-select" | "boolean"
577
+ selectOptions?: { value: string; label: string }[]
578
+ align?: "left" | "right" | "center"
579
+ cellClassName?: string
580
+ headerClassName?: string
581
+ exportable?: boolean
582
+ isEditable?: boolean | ((row) => boolean)
583
+ badgeMap?: Partial<
584
+ Record<string,
585
+ "default" | "secondary" | "destructive" |
586
+ "success" | "warning" | "outline"
587
+ >
588
+ >
589
+ }
590
+ ```
591
+
592
+ The default `filterFn` for a column is wired automatically from `meta.filterType`. You can still set a custom `filterFn` on the column to override it.
593
+
594
+ ---
595
+
596
+ ## Editing
597
+
598
+ Two modes, both prop-driven, both work simultaneously:
599
+
600
+ ### Single cell — double-click
601
+
602
+ ```tsx
603
+ <DataTable
604
+ onCellEdit={(row, columnId, value) =>
605
+ saveMutation.mutate({ ...row, [columnId]: value })
606
+ }
607
+ />
608
+ ```
609
+
610
+ ### Whole row — Edit action → Save / Cancel
611
+
612
+ ```tsx
613
+ <DataTable
614
+ onRowSave={(row, draft) =>
615
+ saveMutation.mutate({ ...row, ...draft })
616
+ }
617
+ />
618
+ ```
619
+
620
+ `isEditable` on `meta` can disable editing for individual rows or columns:
621
+
622
+ ```tsx
623
+ {
624
+ accessorKey: "email",
625
+ meta: {
626
+ editor: "text",
627
+ isEditable: (row) => row.role !== "billing",
628
+ },
629
+ }
630
+ ```
631
+
632
+ ---
633
+
634
+ ## Filters
635
+
636
+ The package exports the filter primitives so you can build custom panels too:
637
+
638
+ ```ts
639
+ import {
640
+ textFilterFn,
641
+ numberFilterFn,
642
+ dateFilterFn,
643
+ setFilterFn,
644
+ booleanFilterFn,
645
+ type AdvFilter,
646
+ type SetFilter,
647
+ type TextOp,
648
+ type NumberOp,
649
+ type DateOp,
650
+ } from "@dynostack/react-grid"
651
+ ```
652
+
653
+ | `filterType` | Operators | Value shape |
654
+ | -------------- | -------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- |
655
+ | `text` | `contains`, `notContains`, `equals`, `notEqual`, `startsWith`, `endsWith`, `blank`, `notBlank` | `AdvFilter<TextOp, string>` |
656
+ | `number` | `equals`, `notEqual`, `lessThan`, `lessThanOrEqual`, `greaterThan`, `greaterThanOrEqual`, `inRange`, `blank`, `notBlank` | `AdvFilter<NumberOp, number>` |
657
+ | `date` | `equals`, `notEqual`, `before`, `after`, `inRange`, `blank`, `notBlank` | `AdvFilter<DateOp, string>` |
658
+ | `select` | set filter | `SetFilter` (`{ selected: string[] }`) |
659
+ | `multi-select` | set filter | `SetFilter` (`{ selected: string[] }`) |
660
+ | `boolean` | `All` / `True` / `False` | `boolean \| undefined` |
661
+
662
+ Text/number/date panels also expose **AND/OR combine** of a second condition, ag-grid style.
663
+
664
+ The set filter automatically derives unique values from the visible rows when `selectOptions` is not declared — search box, "Select all (filtered)" with indeterminate state, individual checkboxes.
665
+
666
+ ---
667
+
668
+ ## Selection & bulk actions
669
+
670
+ Selection is on by default (`enableSelection: true`). When any row is selected the toolbar swaps in:
671
+
672
+ - A `<count> selected` badge
673
+ - `Delete` button → opens the confirmation dialog first, then calls `onBulkDelete?(rows)` after confirm
674
+ - `Clear` button → resets selection
675
+
676
+ ```tsx
677
+ <DataTable
678
+ onBulkDelete={(rows) => removeMany(rows.map((r) => r.id))}
679
+ />
680
+ ```
681
+
682
+ ---
683
+
684
+ ## Expandable rows
685
+
686
+ ### Sub-row panel (custom JSX)
687
+
688
+ ```tsx
689
+ <DataTable
690
+ renderSubRow={(row) => <UserAuditPanel user={row} />}
691
+ />
692
+ ```
693
+
694
+ ### Nested rows (TanStack `getSubRows`)
695
+
696
+ ```tsx
697
+ <DataTable
698
+ getSubRows={(row) => row.children}
699
+ />
700
+ ```
701
+
702
+ When either is set, an `__expand` chevron column is added and pinned right next to `__select`.
703
+
704
+ ---
705
+
706
+ ## Export
707
+
708
+ ```tsx
709
+ <DataTable exportFileName="users" />
710
+ ```
711
+
712
+ Toolbar `Export` menu offers **CSV** and **Excel**. If any rows are selected, the menu becomes "Export N selected"; otherwise it exports all visible (filtered) rows.
713
+
714
+ Mark a column non-exportable via `meta.exportable: false`.
715
+
716
+ ---
717
+
718
+ ## View sheet
719
+
720
+ 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:
721
+
722
+ | Option | Layout | Intended use |
723
+ | --- | --- | --- |
724
+ | `Compact` | 3 columns, tighter cards | Scan more fields at once. |
725
+ | `Relaxed` | 2 columns, medium spacing | Balanced default for mixed values. |
726
+ | `Comfy` | 1 column, roomier cards | Read long values without cramped wrapping. |
727
+
728
+ The sheet is responsive and wider on desktop so the multi-column modes have enough room for real row data.
729
+
730
+ Works out of the box with no props. Customize via `viewSheet`:
731
+
732
+ ```tsx
733
+ <DataTable
734
+ viewSheet={{
735
+ side: "right", // or "left"
736
+ defaultDensity: "relaxed", // initial layout density
737
+ hideDensityTabs: true, // hide the layout picker
738
+ fields: ["name", "email", "role"], // limit / reorder shown columns
739
+ renderField: ({ column, value, row }) => // override how a value renders
740
+ column.id === "phone" ? <a href={`tel:${value}`}>{String(value)}</a> : null,
741
+ renderHeader: (row) => <YourCustomHeader row={row} />,
742
+ labels: {
743
+ title: (row) => `${row.name} (${row.role})`,
744
+ description: (row) => `Joined ${row.joinedAt}`,
745
+ emptyValue: "—",
746
+ density: { compact: "3 cols", relaxed: "2 cols", comfy: "1 col" },
747
+ },
748
+ }}
749
+ onView={(row) => track("user.view", row)} // optional side-effect
750
+ />
751
+ ```
752
+
753
+ Disable the built-in sheet entirely:
754
+
755
+ ```tsx
756
+ <DataTable viewSheet={false} onView={(row) => router.push(`/users/${row.id}`)} />
757
+ ```
758
+
759
+ `onView` fires before the sheet opens, so you can navigate / log / fetch alongside it.
760
+
761
+ ---
762
+
763
+ ## Delete confirmation
764
+
765
+ 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.
766
+
767
+ 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.
768
+
769
+ ```tsx
770
+ <DataTable
771
+ onDelete={(row) => api.deleteUser(row.id)}
772
+ onBulkDelete={(rows) => api.bulkDelete(rows.map(r => r.id))}
773
+ confirmDelete={{
774
+ title: ({ rows, source }) =>
775
+ source === "bulk"
776
+ ? `Delete ${rows.length} users?`
777
+ : `Delete ${rows[0].name}?`,
778
+ description: ({ rows }) =>
779
+ `${rows.length === 1 ? "This user" : "These users"} will be permanently removed. This cannot be undone.`,
780
+ confirmLabel: "Yes, delete",
781
+ cancelLabel: "Keep",
782
+ }}
783
+ />
784
+ ```
785
+
786
+ 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.
787
+
788
+ Skip the dialog (fire immediately):
789
+
790
+ ```tsx
791
+ <DataTable confirmDelete={false} onDelete={(row) => softDelete(row)} />
792
+ ```
793
+
794
+ ---
795
+
796
+ ## Custom row actions
797
+
798
+ ```tsx
799
+ <DataTable
800
+ rowActions={["view", "edit", "duplicate", "delete"]}
801
+ customRowActions={[
802
+ {
803
+ id: "suspend",
804
+ label: "Suspend",
805
+ icon: <BanIcon />,
806
+ danger: true,
807
+ show: (r) => r.status !== "suspended",
808
+ },
809
+ { id: "archive", label: "Archive", icon: <ArchiveIcon /> },
810
+ ]}
811
+ onRowAction={(action, row) => {
812
+ if (action === "suspend") saveMutation.mutate({ ...row, status: "suspended" })
813
+ // ...
814
+ }}
815
+ onDelete={(row) => deleteMutation.mutate([row.id])}
816
+ />
817
+ ```
818
+
819
+ ---
820
+
821
+ ## Server-side data
822
+
823
+ You can do server-side data in either mode.
824
+
825
+ ### Controlled server-side data
826
+
827
+ Own the API call in your page and pass the result into the table:
828
+
829
+ ```tsx
830
+ const [q, setQ] = useState("")
831
+ const usersQuery = useQuery({
832
+ queryKey: ["users", q],
833
+ queryFn: () => fetchUsers({ q }),
834
+ })
835
+
836
+ <DataTable
837
+ data={usersQuery.data ?? []}
838
+ isLoading={usersQuery.isLoading}
839
+ isFetching={usersQuery.isFetching}
840
+ onRefresh={() => usersQuery.refetch()}
841
+ globalFilter={q}
842
+ onGlobalFilterChange={setQ}
843
+ totalRecords={usersQuery.data?.length ?? 0}
844
+ />
845
+ ```
846
+
847
+ Pair this with TanStack Query's pagination/cursor utilities when you want query
848
+ caching and mutation orchestration outside the grid.
849
+
850
+ ### Built-in server-side data
851
+
852
+ Let the grid call your API by setting `dataSource.mode` to `"server"`:
853
+
854
+ ```tsx
855
+ <DataTable<User>
856
+ columns={columns}
857
+ dataSource={{
858
+ mode: "server",
859
+ fetchRows: async (state) => {
860
+ const res = await fetch("/api/users/grid", {
861
+ method: "POST",
862
+ headers: { "content-type": "application/json" },
863
+ body: JSON.stringify(state),
864
+ })
865
+
866
+ if (!res.ok) throw new Error("Users request failed")
867
+ return res.json() as Promise<{ rows: User[]; totalRecords: number }>
868
+ },
869
+ onError: (_error, context) => {
870
+ toast.error(context.message)
871
+ },
872
+ }}
873
+ />
874
+ ```
875
+
876
+ `state` contains `pageIndex`, `pageSize`, `sorting`, `columnFilters`, and
877
+ `globalFilter`. In server mode the table assumes the API already applied those
878
+ operations and only renders the returned page.
879
+
880
+ ---
881
+
882
+ ## API reference
883
+
884
+ | Prop | Type | Default | Description |
885
+ | ------------------------- | ---------------------------------------------------------- | ---------------------- | ------------------------------------------------------ |
886
+ | `data` | `TData[]` | `[]` | Controlled row data. Use this when fetching outside the table. |
887
+ | `columns` | `ColumnDef<TData>[]` | — | TanStack column definitions. |
888
+ | `dataSource` | `DataTableDataSource<TData>` | — | Optional internal fetcher for client/server data loading. |
889
+ | `isLoading` | `boolean` | `false` | Initial skeleton state. |
890
+ | `isFetching` | `boolean` | `false` | Background-refresh indicator. |
891
+ | `onRefresh` | `() => void` | — | Refresh button handler. |
892
+ | `totalRecords` | `number` | `data.length` | Total count badge in toolbar. |
893
+ | `exportFileName` | `string` | `"export"` | Base filename for CSV / Excel export. |
894
+ | `enableSelection` | `boolean` | `true` | Show the `__select` column. |
895
+ | `renderSubRow` | `(row: TData) => ReactNode` | — | Custom expandable panel. |
896
+ | `getSubRows` | `(row: TData) => TData[] \| undefined` | — | Nested rows accessor. |
897
+ | `rowActions` | `("view" \| "edit" \| "duplicate" \| "delete")[]` | all four | Built-in row actions. |
898
+ | `customRowActions` | `CustomRowAction<TData>[]` | `[]` | Extra row actions. |
899
+ | `onRowAction` | `(action, row) => void` | — | Row action handler. |
900
+ | `onCellEdit` | `(row, columnId, value) => void` | — | Single-cell save handler. |
901
+ | `onRowSave` | `(row, draft) => void` | — | Row-edit save handler. |
902
+ | `onAddRow` | `() => Partial<TData>` | — | Returns the empty draft for "Add row". |
903
+ | `onBulkDelete` | `(rows: TData[]) => void` | — | Bulk delete handler. |
904
+ | `initialPageSize` | `number` | `10` | Initial pagination size. |
905
+ | `pageSizeOptions` | `number[]` | shadcn defaults | Page-size dropdown options. |
906
+ | `initialColumnPinning` | `ColumnPinningState` | `{ left: [], right: [] }` | Initial pinned columns. |
907
+ | `initialColumnVisibility` | `VisibilityState` | `{}` | Initial hidden columns. |
908
+ | `initialSorting` | `SortingState` | `[]` | Initial column sort order. |
909
+ | `onSelectionChange` | `(rows: TData[]) => void` | — | Observe selected loaded rows. |
910
+ | `ariaLabel` | `string` | `"Data table"` | Accessible name for the table. |
911
+ | `striped` | `boolean` | `false` | Alternate row backgrounds. |
912
+ | `stickyHeader` | `boolean` | `false` | Keep headers visible in the scroll viewport. |
913
+ | `maxHeight` | `CSSProperties["maxHeight"]` | — | Limit the vertical scroll viewport. |
914
+ | `globalFilter` | `string` | uncontrolled | Controlled global filter value. |
915
+ | `onGlobalFilterChange` | `(value: string) => void` | — | Controlled global filter setter. |
916
+ | `className` | `string` | — | Extra classes on the table root. |
917
+ | `toolbarSlot` | `ReactNode` | — | Custom JSX prepended into the toolbar. |
918
+ | `features` | `DataTableFeatures` | all on | Feature flags. |
919
+ | `labels` | `DataTableLabels` | English defaults | i18n labels. |
920
+ | `density` | `"compact" \| "default" \| "comfortable"` | `"default"` | Row density. |
921
+ | `theme` | `DataTableTheme` | inherits `:root` | Per-instance CSS-variable overrides. Accepts flat tokens **or** `{ light, dark }`. |
922
+ | `isolate` | `boolean` | `false` | Ignore the app's `:root` and render with bundled defaults. |
923
+ | `onView` | `(row: TData) => void` | — | Side-effect when "View" is clicked. Fires *before* the sheet opens. |
924
+ | `onDelete` | `(row: TData) => void` | — | Single-row delete handler. Fires *after* the confirm modal (or immediately if `confirmDelete={false}`). |
925
+ | `viewSheet` | `ViewSheetConfig<TData> \| false` | enabled | Configure or disable the built-in View sheet. |
926
+ | `confirmDelete` | `ConfirmDeleteConfig<TData> \| boolean` | `true` | Configure or disable the delete confirmation modal (applies to single + bulk). |
927
+
928
+ `TData` must extend `{ id: string \| number }`.
929
+
930
+ ```ts
931
+ type DataTableDataSource<TData> = {
932
+ fetchRows: (params: {
933
+ pageIndex: number
934
+ pageSize: number
935
+ sorting: SortingState
936
+ columnFilters: ColumnFiltersState
937
+ globalFilter: string
938
+ }) => Promise<TData[] | { rows: TData[]; totalRecords?: number }>
939
+ mode?: "client" | "server"
940
+ enabled?: boolean
941
+ initialData?: TData[]
942
+ deps?: readonly unknown[]
943
+ onError?: (
944
+ error: unknown,
945
+ context: { type: "load" | "refresh"; message: string }
946
+ ) => void
947
+ }
948
+ ```
949
+
950
+ ```ts
951
+ // Theme types
952
+ type DataTableTokens = {
953
+ background?: string
954
+ foreground?: string
955
+ card?: string
956
+ cardForeground?: string
957
+ popover?: string
958
+ popoverForeground?: string
959
+ primary?: string
960
+ primaryForeground?: string
961
+ secondary?: string
962
+ secondaryForeground?: string
963
+ muted?: string
964
+ mutedForeground?: string
965
+ accent?: string
966
+ accentForeground?: string
967
+ destructive?: string
968
+ destructiveForeground?: string
969
+ border?: string
970
+ input?: string
971
+ ring?: string
972
+ radius?: string
973
+ fontFamily?: string
974
+ }
975
+
976
+ type DataTableModedTheme = {
977
+ light?: DataTableTokens
978
+ dark?: DataTableTokens
979
+ }
980
+
981
+ type DataTableTheme = DataTableTokens | DataTableModedTheme
982
+ ```
983
+
984
+ ```ts
985
+ // Theme exports
986
+ import {
987
+ themePresets, // ready-made moded presets
988
+ buildPreset, // (hue, chroma?) => DataTableModedTheme
989
+ splitTheme, // (theme) => { light, dark }
990
+ tokensToStyle, // (tokens) => React.CSSProperties
991
+ tokensToCssBlock, // (tokens) => "var:val;var:val" string
992
+ ISOLATE_LIGHT_TOKENS,
993
+ ISOLATE_DARK_TOKENS,
994
+ } from "@dynostack/react-grid"
995
+ ```
996
+
997
+ ```ts
998
+ // View sheet types
999
+ type ViewSheetDensity = "compact" | "relaxed" | "comfy"
1000
+
1001
+ type ViewSheetConfig<TData> = {
1002
+ side?: "right" | "left" | "top" | "bottom"
1003
+ defaultDensity?: ViewSheetDensity
1004
+ hideDensityTabs?: boolean
1005
+ fields?: string[]
1006
+ renderField?: (args: {
1007
+ column: Column<TData, unknown>
1008
+ value: unknown
1009
+ row: TData
1010
+ }) => React.ReactNode
1011
+ renderHeader?: (row: TData) => React.ReactNode
1012
+ labels?: {
1013
+ title?: (row: TData) => React.ReactNode
1014
+ description?: (row: TData) => React.ReactNode
1015
+ emptyValue?: string
1016
+ density?: { compact?: string; relaxed?: string; comfy?: string }
1017
+ }
1018
+ }
1019
+
1020
+ // Confirm-delete types
1021
+ type ConfirmDeleteContext<TData> = {
1022
+ rows: TData[]
1023
+ source: "single" | "bulk"
1024
+ }
1025
+
1026
+ type ConfirmDeleteConfig<TData> = {
1027
+ title?: (ctx: ConfirmDeleteContext<TData>) => React.ReactNode
1028
+ description?: (ctx: ConfirmDeleteContext<TData>) => React.ReactNode
1029
+ confirmLabel?: string
1030
+ cancelLabel?: string
1031
+ }
1032
+ ```
1033
+
1034
+ ---
1035
+
1036
+ ## Compatibility
1037
+
1038
+ | Stack | Tested on |
1039
+ | ----------------- | -------------------------- |
1040
+ | React | 18.x · 19.x |
1041
+ | TanStack Table | 8.21+ |
1042
+ | Tailwind CSS | 3.x · 4.x |
1043
+ | Bundler | Vite · Next.js · Webpack 5 |
1044
+
1045
+ ESM and CJS bundles ship side-by-side. Tree-shakeable. Marked `"use client"` for Next.js App Router compatibility.
1046
+
1047
+ ---
1048
+
1049
+ ## Roadmap
1050
+
1051
+ - [ ] Server-side pagination/sorting helpers (controlled-state recipes)
1052
+ - [ ] Column groups (header rowSpan/colSpan)
1053
+ - [ ] Pivot mode
1054
+ - [ ] Aggregation row (sum, avg, min, max, count)
1055
+ - [ ] Saved view profiles (filter + visibility + pinning snapshots)
1056
+ - [ ] Virtualized rows (TanStack Virtual integration)
1057
+ - [ ] Storybook + visual regression tests
1058
+ - [ ] CodeSandbox / StackBlitz starter
1059
+
1060
+ Have a use case that isn't covered? [Open an issue](https://github.com/wanted-coder-vijay/wcv-data-grid/issues/new) — happy to consider it.
1061
+
1062
+ ---
1063
+
1064
+ ## Contributing
1065
+
1066
+ ```sh
1067
+ git clone https://github.com/wanted-coder-vijay/wcv-data-grid.git
1068
+ cd wcv-data-grid
1069
+ npm install
1070
+ npm run dev # tsup --watch
1071
+ npm run typecheck # tsc --noEmit
1072
+ npm run build # produce dist/
1073
+ ```
1074
+
1075
+ PRs welcome. Please keep the prop API additive — feature toggles over breaking changes.
1076
+
1077
+ ---
1078
+
1079
+ ## License
1080
+
1081
+ [Apache-2.0](./LICENSE) · Copyright © 2026 vijay kumar anchupogu (wanted-coder-vijay)
1082
+
1083
+ See [NOTICE](./NOTICE) for attribution requirements.
1084
+
1085
+ > **Package history.** This package was briefly published as
1086
+ > `@dynostack/gridstack@0.1.0` under the MIT license before being
1087
+ > renamed to `@dynostack/react-grid` and relicensed under Apache-2.0.
1088
+ > The old name is deprecated; new code should depend on
1089
+ > `@dynostack/react-grid` only.