@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 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** — built on shadcn/ui CSS variables; per-instance overrides with a `theme` prop and ready-made presets
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
- If your app already has [shadcn/ui](https://ui.shadcn.com/docs/theming) tokens defined on `:root`, you're done — the table inherits them automatically.
94
+ The grid is built on **shadcn/ui CSS variables**. It auto-adjusts to whatever theme your app already has:
94
95
 
95
- If not, import the default-tokens stylesheet once:
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
- ## Theming
201
+ ## Data fetching
171
202
 
172
- All shadcn tokens are supported plus `radius` and `fontFamily`. Anything you omit falls through to the consumer's `:root`.
203
+ `DataTable` supports two data ownership models.
173
204
 
174
- ### Use a preset
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
- import { DataTable, themePresets } from "@dynostack/react-grid"
212
+ const usersQuery = useQuery({
213
+ queryKey: ["users"],
214
+ queryFn: fetchUsers,
215
+ })
178
216
 
179
- <DataTable data={data} columns={columns} theme={themePresets.violet} />
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
- Available presets: `light` · `dark` · `emerald` · `violet` · `amber`.
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
- ### Custom tokens
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
- CSS variables are emitted on the table root, so multiple instances on the same page can wear different themes.
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={{ ...themePresets.dark, primary: "oklch(0.7 0.18 250)" }}
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
- Provide a controlled global filter and refetch on change:
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 for cursor-based grids.
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[]` | — | Row data. |
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