@dynostack/react-grid 0.1.0 → 0.2.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 +305 -18
- package/dist/index.cjs +375 -97
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +142 -43
- package/dist/index.d.ts +142 -43
- package/dist/index.js +371 -99
- package/dist/index.js.map +1 -1
- package/dist/page.css +24 -0
- package/dist/styles.css +145 -46
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -26,7 +26,7 @@ A single `<DataTable />` component that gives you ag-grid–level functionality
|
|
|
26
26
|
- **Selection + bulk actions** — pinned `__select` column with select-all, clear, bulk delete
|
|
27
27
|
- **Expandable rows** — provide a `renderSubRow` panel or use TanStack's nested `getSubRows`
|
|
28
28
|
- **CSV / Excel export** — selection-aware (export selected vs. all)
|
|
29
|
-
- **Theming** —
|
|
29
|
+
- **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
30
|
- **Density** — `compact` · `default` · `comfortable`
|
|
31
31
|
- **i18n / labels** — every visible string is overridable
|
|
32
32
|
- **Feature flags** — turn off any toolbar control or table capability with a single boolean
|
|
@@ -40,6 +40,7 @@ A single `<DataTable />` component that gives you ag-grid–level functionality
|
|
|
40
40
|
- [Tailwind setup](#tailwind-setup)
|
|
41
41
|
- [Theme tokens](#theme-tokens)
|
|
42
42
|
- [Quick start](#quick-start)
|
|
43
|
+
- [Data fetching](#data-fetching)
|
|
43
44
|
- [Theming](#theming)
|
|
44
45
|
- [Density](#density)
|
|
45
46
|
- [Feature flags](#feature-flags)
|
|
@@ -90,14 +91,44 @@ export default {
|
|
|
90
91
|
|
|
91
92
|
## Theme tokens
|
|
92
93
|
|
|
93
|
-
|
|
94
|
+
The grid is built on **shadcn/ui CSS variables**. It auto-adjusts to whatever theme your app already has:
|
|
94
95
|
|
|
95
|
-
|
|
96
|
+
| Your app has… | What you do | What you get |
|
|
97
|
+
|---|---|---|
|
|
98
|
+
| Nothing (bare React) | `import "@dynostack/react-grid/styles.css"` | Clean light theme, auto-switches to dark on OS preference. |
|
|
99
|
+
| shadcn/ui (default theme) | Nothing | Grid inherits your `:root` tokens automatically. |
|
|
100
|
+
| shadcn/ui with a custom theme (Stone / Zinc / your own hue) | Nothing | Grid picks up your custom tokens automatically. |
|
|
101
|
+
| Custom theme using shadcn token names | Nothing | Same as above. |
|
|
102
|
+
| Custom theme with non-shadcn names | Pass [`theme` prop](#theming) | Per-instance override mapped to shadcn vars. |
|
|
103
|
+
| Want one grid to ignore the app theme | Pass `isolate` | Grid uses bundled defaults regardless of `:root`. |
|
|
104
|
+
|
|
105
|
+
**Why this just works.** The bundled `styles.css` declares its defaults inside the `dynostack-grid-defaults` cascade layer. Any unlayered consumer rule (which is where shadcn and most app CSS lives) automatically wins — import order doesn't matter, and you can't accidentally overwrite your app's theme by importing the grid's stylesheet.
|
|
106
|
+
|
|
107
|
+
### Minimal install
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
// main.tsx — once per app
|
|
111
|
+
import "@dynostack/react-grid/styles.css"
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Optional: extend the theme to the page
|
|
115
|
+
|
|
116
|
+
By default the grid only styles itself, not the surrounding page. If you want `<body>` to use the same background/foreground as the grid:
|
|
96
117
|
|
|
97
118
|
```ts
|
|
98
119
|
import "@dynostack/react-grid/styles.css"
|
|
120
|
+
import "@dynostack/react-grid/page.css" // optional
|
|
99
121
|
```
|
|
100
122
|
|
|
123
|
+
### Dark mode
|
|
124
|
+
|
|
125
|
+
| Mode | How to enable | Behavior |
|
|
126
|
+
|---|---|---|
|
|
127
|
+
| Follow OS | Default — no action required | Light by day, dark by night via `prefers-color-scheme`. |
|
|
128
|
+
| Force light | Add `class="light"` to `<html>` | Stays light regardless of OS. |
|
|
129
|
+
| Force dark | Add `class="dark"` to `<html>` | Stays dark regardless of OS. |
|
|
130
|
+
| Per-instance | `<DataTable theme={themePresets.violet}>` | Grid auto-flips light/dark inside the moded preset. |
|
|
131
|
+
|
|
101
132
|
Either way you can still override any token per-instance via the [`theme`](#theming) prop.
|
|
102
133
|
|
|
103
134
|
---
|
|
@@ -167,26 +198,92 @@ That's it. You now have sort + filter + edit + add + delete + export + pin + res
|
|
|
167
198
|
|
|
168
199
|
---
|
|
169
200
|
|
|
170
|
-
##
|
|
201
|
+
## Data fetching
|
|
171
202
|
|
|
172
|
-
|
|
203
|
+
`DataTable` supports two data ownership models.
|
|
173
204
|
|
|
174
|
-
###
|
|
205
|
+
### 1. Controlled data from your page
|
|
206
|
+
|
|
207
|
+
Use this when your app already owns fetching with TanStack Query, SWR, Redux,
|
|
208
|
+
loader functions, or custom hooks. The table receives rows and loading flags as
|
|
209
|
+
props, and your app owns error/toast behavior.
|
|
175
210
|
|
|
176
211
|
```tsx
|
|
177
|
-
|
|
212
|
+
const usersQuery = useQuery({
|
|
213
|
+
queryKey: ["users"],
|
|
214
|
+
queryFn: fetchUsers,
|
|
215
|
+
})
|
|
178
216
|
|
|
179
|
-
<DataTable
|
|
217
|
+
<DataTable<User>
|
|
218
|
+
data={usersQuery.data ?? []}
|
|
219
|
+
columns={columns}
|
|
220
|
+
isLoading={usersQuery.isLoading}
|
|
221
|
+
isFetching={usersQuery.isFetching}
|
|
222
|
+
onRefresh={() => usersQuery.refetch()}
|
|
223
|
+
totalRecords={usersQuery.data?.length ?? 0}
|
|
224
|
+
/>
|
|
180
225
|
```
|
|
181
226
|
|
|
182
|
-
|
|
227
|
+
For blocking load errors, render your own page-level error state or pass an empty
|
|
228
|
+
array. For background errors, show a toast from your query/mutation callbacks.
|
|
229
|
+
|
|
230
|
+
### 2. Internal fetching with `dataSource`
|
|
183
231
|
|
|
184
|
-
|
|
232
|
+
Use this when you want the table to own fetch/loading/error/refresh state.
|
|
233
|
+
`fetchRows` receives the current table state and can return either an array or
|
|
234
|
+
`{ rows, totalRecords }`.
|
|
185
235
|
|
|
186
236
|
```tsx
|
|
187
|
-
<DataTable
|
|
188
|
-
data={data}
|
|
237
|
+
<DataTable<User>
|
|
189
238
|
columns={columns}
|
|
239
|
+
dataSource={{
|
|
240
|
+
fetchRows: async ({ pageIndex, pageSize, sorting, columnFilters, globalFilter }) => {
|
|
241
|
+
const res = await fetch("/api/users", {
|
|
242
|
+
method: "POST",
|
|
243
|
+
headers: { "content-type": "application/json" },
|
|
244
|
+
body: JSON.stringify({
|
|
245
|
+
pageIndex,
|
|
246
|
+
pageSize,
|
|
247
|
+
sorting,
|
|
248
|
+
columnFilters,
|
|
249
|
+
q: globalFilter,
|
|
250
|
+
}),
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
if (!res.ok) throw new Error("Failed to load users")
|
|
254
|
+
return res.json() as Promise<{ rows: User[]; totalRecords: number }>
|
|
255
|
+
},
|
|
256
|
+
mode: "server",
|
|
257
|
+
onError: (error, context) => {
|
|
258
|
+
toast.error(context.message)
|
|
259
|
+
console.error(error)
|
|
260
|
+
},
|
|
261
|
+
}}
|
|
262
|
+
/>
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Internal mode behavior:
|
|
266
|
+
|
|
267
|
+
- Initial load shows the table skeleton.
|
|
268
|
+
- Initial load failure shows an inline `Could not load rows` state with `Retry`.
|
|
269
|
+
- Refresh failure keeps the last successful rows visible and calls `onError`.
|
|
270
|
+
- `mode: "client"` expects the full row array and lets the table sort/filter/page in memory.
|
|
271
|
+
- `mode: "server"` expects the current page and uses `totalRecords` for pagination.
|
|
272
|
+
|
|
273
|
+
Keep using `onCellEdit`, `onRowSave`, `onAddRow`, and `onBulkDelete` for mutations.
|
|
274
|
+
The table does not assume your write API; this lets you choose optimistic updates,
|
|
275
|
+
rollback, toast notifications, and validation.
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
## Theming
|
|
280
|
+
|
|
281
|
+
The `theme` prop accepts **two shapes**. Pick whichever fits your use case.
|
|
282
|
+
|
|
283
|
+
### Shape 1 — Flat tokens
|
|
284
|
+
|
|
285
|
+
```tsx
|
|
286
|
+
<DataTable
|
|
190
287
|
theme={{
|
|
191
288
|
primary: "oklch(0.6 0.2 200)",
|
|
192
289
|
primaryForeground: "oklch(1 0 0)",
|
|
@@ -197,16 +294,102 @@ Available presets: `light` · `dark` · `emerald` · `violet` · `amber`.
|
|
|
197
294
|
/>
|
|
198
295
|
```
|
|
199
296
|
|
|
200
|
-
|
|
297
|
+
All shadcn tokens are supported plus `radius` and `fontFamily`. Anything you omit falls through to whatever your app's `:root` provides.
|
|
298
|
+
|
|
299
|
+
### Shape 2 — Moded `{ light, dark }`
|
|
300
|
+
|
|
301
|
+
A moded theme repaints the whole table **and** auto-flips on dark mode (OS preference *or* a `.dark` ancestor):
|
|
302
|
+
|
|
303
|
+
```tsx
|
|
304
|
+
<DataTable
|
|
305
|
+
theme={{
|
|
306
|
+
light: { background: "oklch(0.99 0.005 285)", primary: "oklch(0.55 0.22 285)", /* … */ },
|
|
307
|
+
dark: { background: "oklch(0.16 0.012 285)", primary: "oklch(0.7 0.18 285)", /* … */ },
|
|
308
|
+
}}
|
|
309
|
+
/>
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
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.
|
|
313
|
+
|
|
314
|
+
### Use a preset
|
|
315
|
+
|
|
316
|
+
Presets ship in **moded shape** — passing one repaints the entire table and follows dark mode automatically:
|
|
317
|
+
|
|
318
|
+
```tsx
|
|
319
|
+
import { DataTable, themePresets } from "@dynostack/react-grid"
|
|
320
|
+
|
|
321
|
+
<DataTable theme={themePresets.violet} />
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
Available presets:
|
|
325
|
+
|
|
326
|
+
| Preset | Hue |
|
|
327
|
+
|---|---|
|
|
328
|
+
| `neutral` | Grayscale (default appearance) |
|
|
329
|
+
| `light` | Force light, no dark variant |
|
|
330
|
+
| `dark` | Force dark, no light variant |
|
|
331
|
+
| `violet` | 285° |
|
|
332
|
+
| `emerald` | 162° |
|
|
333
|
+
| `amber` | 65° |
|
|
334
|
+
| `rose` | 15° |
|
|
335
|
+
| `sky` | 235° |
|
|
336
|
+
| `slate` | 240° (low chroma) |
|
|
337
|
+
|
|
338
|
+
### Build a custom preset from a single hue
|
|
339
|
+
|
|
340
|
+
```tsx
|
|
341
|
+
import { DataTable, buildPreset } from "@dynostack/react-grid"
|
|
342
|
+
|
|
343
|
+
const teal = buildPreset(180) // hue only
|
|
344
|
+
const subtleTeal = buildPreset(180, 0.015) // hue + custom chroma
|
|
345
|
+
|
|
346
|
+
<DataTable theme={teal} />
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
`buildPreset(hue, chroma?)` returns a full `{ light, dark }` token set tinted around the given OKLCH hue.
|
|
201
350
|
|
|
202
351
|
### Compose with a preset
|
|
203
352
|
|
|
204
353
|
```tsx
|
|
205
354
|
<DataTable
|
|
206
|
-
theme={{
|
|
355
|
+
theme={{
|
|
356
|
+
...themePresets.violet,
|
|
357
|
+
light: { ...themePresets.violet.light, primary: "oklch(0.7 0.18 250)" },
|
|
358
|
+
}}
|
|
207
359
|
/>
|
|
208
360
|
```
|
|
209
361
|
|
|
362
|
+
### Isolate a grid from the app theme
|
|
363
|
+
|
|
364
|
+
When embedding inside a heavily-themed shell where you want the table to keep its own look:
|
|
365
|
+
|
|
366
|
+
```tsx
|
|
367
|
+
<DataTable isolate /* uses bundled neutral tokens, ignores app :root */ />
|
|
368
|
+
<DataTable isolate theme={themePresets.violet} /* isolated AND violet */ />
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
### Multiple grids, different themes
|
|
372
|
+
|
|
373
|
+
CSS variables are emitted on each table root, so this works:
|
|
374
|
+
|
|
375
|
+
```tsx
|
|
376
|
+
<DataTable theme={themePresets.violet} />
|
|
377
|
+
<DataTable theme={themePresets.emerald} />
|
|
378
|
+
<DataTable theme={{ primary: "oklch(0.6 0.2 200)" }} />
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
### Precedence summary
|
|
382
|
+
|
|
383
|
+
```
|
|
384
|
+
┌──────────────────────────────────────────────────────────┐
|
|
385
|
+
│ Inline style on the grid root (per-instance `theme`) │ ← highest
|
|
386
|
+
├──────────────────────────────────────────────────────────┤
|
|
387
|
+
│ Consumer's :root rules (shadcn, custom app CSS) │
|
|
388
|
+
├──────────────────────────────────────────────────────────┤
|
|
389
|
+
│ @layer dynostack-grid-defaults (bundled styles.css) │ ← lowest
|
|
390
|
+
└──────────────────────────────────────────────────────────┘
|
|
391
|
+
```
|
|
392
|
+
|
|
210
393
|
---
|
|
211
394
|
|
|
212
395
|
## Density
|
|
@@ -454,7 +637,11 @@ Mark a column non-exportable via `meta.exportable: false`.
|
|
|
454
637
|
|
|
455
638
|
## Server-side data
|
|
456
639
|
|
|
457
|
-
|
|
640
|
+
You can do server-side data in either mode.
|
|
641
|
+
|
|
642
|
+
### Controlled server-side data
|
|
643
|
+
|
|
644
|
+
Own the API call in your page and pass the result into the table:
|
|
458
645
|
|
|
459
646
|
```tsx
|
|
460
647
|
const [q, setQ] = useState("")
|
|
@@ -474,7 +661,38 @@ const usersQuery = useQuery({
|
|
|
474
661
|
/>
|
|
475
662
|
```
|
|
476
663
|
|
|
477
|
-
Pair with TanStack Query's pagination/cursor utilities
|
|
664
|
+
Pair this with TanStack Query's pagination/cursor utilities when you want query
|
|
665
|
+
caching and mutation orchestration outside the grid.
|
|
666
|
+
|
|
667
|
+
### Built-in server-side data
|
|
668
|
+
|
|
669
|
+
Let the grid call your API by setting `dataSource.mode` to `"server"`:
|
|
670
|
+
|
|
671
|
+
```tsx
|
|
672
|
+
<DataTable<User>
|
|
673
|
+
columns={columns}
|
|
674
|
+
dataSource={{
|
|
675
|
+
mode: "server",
|
|
676
|
+
fetchRows: async (state) => {
|
|
677
|
+
const res = await fetch("/api/users/grid", {
|
|
678
|
+
method: "POST",
|
|
679
|
+
headers: { "content-type": "application/json" },
|
|
680
|
+
body: JSON.stringify(state),
|
|
681
|
+
})
|
|
682
|
+
|
|
683
|
+
if (!res.ok) throw new Error("Users request failed")
|
|
684
|
+
return res.json() as Promise<{ rows: User[]; totalRecords: number }>
|
|
685
|
+
},
|
|
686
|
+
onError: (_error, context) => {
|
|
687
|
+
toast.error(context.message)
|
|
688
|
+
},
|
|
689
|
+
}}
|
|
690
|
+
/>
|
|
691
|
+
```
|
|
692
|
+
|
|
693
|
+
`state` contains `pageIndex`, `pageSize`, `sorting`, `columnFilters`, and
|
|
694
|
+
`globalFilter`. In server mode the table assumes the API already applied those
|
|
695
|
+
operations and only renders the returned page.
|
|
478
696
|
|
|
479
697
|
---
|
|
480
698
|
|
|
@@ -482,8 +700,9 @@ Pair with TanStack Query's pagination/cursor utilities for cursor-based grids.
|
|
|
482
700
|
|
|
483
701
|
| Prop | Type | Default | Description |
|
|
484
702
|
| ------------------------- | ---------------------------------------------------------- | ---------------------- | ------------------------------------------------------ |
|
|
485
|
-
| `data` | `TData[]` |
|
|
703
|
+
| `data` | `TData[]` | `[]` | Controlled row data. Use this when fetching outside the table. |
|
|
486
704
|
| `columns` | `ColumnDef<TData>[]` | — | TanStack column definitions. |
|
|
705
|
+
| `dataSource` | `DataTableDataSource<TData>` | — | Optional internal fetcher for client/server data loading. |
|
|
487
706
|
| `isLoading` | `boolean` | `false` | Initial skeleton state. |
|
|
488
707
|
| `isFetching` | `boolean` | `false` | Background-refresh indicator. |
|
|
489
708
|
| `onRefresh` | `() => void` | — | Refresh button handler. |
|
|
@@ -510,10 +729,78 @@ Pair with TanStack Query's pagination/cursor utilities for cursor-based grids.
|
|
|
510
729
|
| `features` | `DataTableFeatures` | all on | Feature flags. |
|
|
511
730
|
| `labels` | `DataTableLabels` | English defaults | i18n labels. |
|
|
512
731
|
| `density` | `"compact" \| "default" \| "comfortable"` | `"default"` | Row density. |
|
|
513
|
-
| `theme` | `DataTableTheme` | inherits `:root` | Per-instance CSS-variable overrides.
|
|
732
|
+
| `theme` | `DataTableTheme` | inherits `:root` | Per-instance CSS-variable overrides. Accepts flat tokens **or** `{ light, dark }`. |
|
|
733
|
+
| `isolate` | `boolean` | `false` | Ignore the app's `:root` and render with bundled defaults. |
|
|
514
734
|
|
|
515
735
|
`TData` must extend `{ id: string \| number }`.
|
|
516
736
|
|
|
737
|
+
```ts
|
|
738
|
+
type DataTableDataSource<TData> = {
|
|
739
|
+
fetchRows: (params: {
|
|
740
|
+
pageIndex: number
|
|
741
|
+
pageSize: number
|
|
742
|
+
sorting: SortingState
|
|
743
|
+
columnFilters: ColumnFiltersState
|
|
744
|
+
globalFilter: string
|
|
745
|
+
}) => Promise<TData[] | { rows: TData[]; totalRecords?: number }>
|
|
746
|
+
mode?: "client" | "server"
|
|
747
|
+
enabled?: boolean
|
|
748
|
+
initialData?: TData[]
|
|
749
|
+
deps?: readonly unknown[]
|
|
750
|
+
onError?: (
|
|
751
|
+
error: unknown,
|
|
752
|
+
context: { type: "load" | "refresh"; message: string }
|
|
753
|
+
) => void
|
|
754
|
+
}
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
```ts
|
|
758
|
+
// Theme types
|
|
759
|
+
type DataTableTokens = {
|
|
760
|
+
background?: string
|
|
761
|
+
foreground?: string
|
|
762
|
+
card?: string
|
|
763
|
+
cardForeground?: string
|
|
764
|
+
popover?: string
|
|
765
|
+
popoverForeground?: string
|
|
766
|
+
primary?: string
|
|
767
|
+
primaryForeground?: string
|
|
768
|
+
secondary?: string
|
|
769
|
+
secondaryForeground?: string
|
|
770
|
+
muted?: string
|
|
771
|
+
mutedForeground?: string
|
|
772
|
+
accent?: string
|
|
773
|
+
accentForeground?: string
|
|
774
|
+
destructive?: string
|
|
775
|
+
destructiveForeground?: string
|
|
776
|
+
border?: string
|
|
777
|
+
input?: string
|
|
778
|
+
ring?: string
|
|
779
|
+
radius?: string
|
|
780
|
+
fontFamily?: string
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
type DataTableModedTheme = {
|
|
784
|
+
light?: DataTableTokens
|
|
785
|
+
dark?: DataTableTokens
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
type DataTableTheme = DataTableTokens | DataTableModedTheme
|
|
789
|
+
```
|
|
790
|
+
|
|
791
|
+
```ts
|
|
792
|
+
// Theme exports
|
|
793
|
+
import {
|
|
794
|
+
themePresets, // ready-made moded presets
|
|
795
|
+
buildPreset, // (hue, chroma?) => DataTableModedTheme
|
|
796
|
+
splitTheme, // (theme) => { light, dark }
|
|
797
|
+
tokensToStyle, // (tokens) => React.CSSProperties
|
|
798
|
+
tokensToCssBlock, // (tokens) => "var:val;var:val" string
|
|
799
|
+
ISOLATE_LIGHT_TOKENS,
|
|
800
|
+
ISOLATE_DARK_TOKENS,
|
|
801
|
+
} from "@dynostack/react-grid"
|
|
802
|
+
```
|
|
803
|
+
|
|
517
804
|
---
|
|
518
805
|
|
|
519
806
|
## Compatibility
|