@dynostack/react-grid 0.1.3 → 0.3.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
@@ -51,6 +51,8 @@ A single `<DataTable />` component that gives you ag-grid–level functionality
51
51
  - [Selection & bulk actions](#selection--bulk-actions)
52
52
  - [Expandable rows](#expandable-rows)
53
53
  - [Export](#export)
54
+ - [View sheet](#view-sheet)
55
+ - [Delete confirmation](#delete-confirmation)
54
56
  - [Custom row actions](#custom-row-actions)
55
57
  - [Server-side data](#server-side-data)
56
58
  - [API reference](#api-reference)
@@ -91,14 +93,44 @@ export default {
91
93
 
92
94
  ## Theme tokens
93
95
 
94
- 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.
96
+ The grid is built on **shadcn/ui CSS variables**. It auto-adjusts to whatever theme your app already has:
95
97
 
96
- If not, import the default-tokens stylesheet once:
98
+ | Your app has… | What you do | What you get |
99
+ |---|---|---|
100
+ | Nothing (bare React) | `import "@dynostack/react-grid/styles.css"` | Clean light theme, auto-switches to dark on OS preference. |
101
+ | shadcn/ui (default theme) | Nothing | Grid inherits your `:root` tokens automatically. |
102
+ | shadcn/ui with a custom theme (Stone / Zinc / your own hue) | Nothing | Grid picks up your custom tokens automatically. |
103
+ | Custom theme using shadcn token names | Nothing | Same as above. |
104
+ | Custom theme with non-shadcn names | Pass [`theme` prop](#theming) | Per-instance override mapped to shadcn vars. |
105
+ | Want one grid to ignore the app theme | Pass `isolate` | Grid uses bundled defaults regardless of `:root`. |
106
+
107
+ **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.
108
+
109
+ ### Minimal install
110
+
111
+ ```ts
112
+ // main.tsx — once per app
113
+ import "@dynostack/react-grid/styles.css"
114
+ ```
115
+
116
+ ### Optional: extend the theme to the page
117
+
118
+ By default the grid only styles itself, not the surrounding page. If you want `<body>` to use the same background/foreground as the grid:
97
119
 
98
120
  ```ts
99
121
  import "@dynostack/react-grid/styles.css"
122
+ import "@dynostack/react-grid/page.css" // optional
100
123
  ```
101
124
 
125
+ ### Dark mode
126
+
127
+ | Mode | How to enable | Behavior |
128
+ |---|---|---|
129
+ | Follow OS | Default — no action required | Light by day, dark by night via `prefers-color-scheme`. |
130
+ | Force light | Add `class="light"` to `<html>` | Stays light regardless of OS. |
131
+ | Force dark | Add `class="dark"` to `<html>` | Stays dark regardless of OS. |
132
+ | Per-instance | `<DataTable theme={themePresets.violet}>` | Grid auto-flips light/dark inside the moded preset. |
133
+
102
134
  Either way you can still override any token per-instance via the [`theme`](#theming) prop.
103
135
 
104
136
  ---
@@ -248,24 +280,12 @@ rollback, toast notifications, and validation.
248
280
 
249
281
  ## Theming
250
282
 
251
- All shadcn tokens are supported plus `radius` and `fontFamily`. Anything you omit falls through to the consumer's `:root`.
283
+ The `theme` prop accepts **two shapes**. Pick whichever fits your use case.
252
284
 
253
- ### Use a preset
254
-
255
- ```tsx
256
- import { DataTable, themePresets } from "@dynostack/react-grid"
257
-
258
- <DataTable data={data} columns={columns} theme={themePresets.violet} />
259
- ```
260
-
261
- Available presets: `light` · `dark` · `emerald` · `violet` · `amber`.
262
-
263
- ### Custom tokens
285
+ ### Shape 1 — Flat tokens
264
286
 
265
287
  ```tsx
266
288
  <DataTable
267
- data={data}
268
- columns={columns}
269
289
  theme={{
270
290
  primary: "oklch(0.6 0.2 200)",
271
291
  primaryForeground: "oklch(1 0 0)",
@@ -276,16 +296,102 @@ Available presets: `light` · `dark` · `emerald` · `violet` · `amber`.
276
296
  />
277
297
  ```
278
298
 
279
- CSS variables are emitted on the table root, so multiple instances on the same page can wear different themes.
299
+ All shadcn tokens are supported plus `radius` and `fontFamily`. Anything you omit falls through to whatever your app's `:root` provides.
300
+
301
+ ### Shape 2 — Moded `{ light, dark }`
302
+
303
+ A moded theme repaints the whole table **and** auto-flips on dark mode (OS preference *or* a `.dark` ancestor):
304
+
305
+ ```tsx
306
+ <DataTable
307
+ theme={{
308
+ light: { background: "oklch(0.99 0.005 285)", primary: "oklch(0.55 0.22 285)", /* … */ },
309
+ dark: { background: "oklch(0.16 0.012 285)", primary: "oklch(0.7 0.18 285)", /* … */ },
310
+ }}
311
+ />
312
+ ```
313
+
314
+ 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.
315
+
316
+ ### Use a preset
317
+
318
+ Presets ship in **moded shape** — passing one repaints the entire table and follows dark mode automatically:
319
+
320
+ ```tsx
321
+ import { DataTable, themePresets } from "@dynostack/react-grid"
322
+
323
+ <DataTable theme={themePresets.violet} />
324
+ ```
325
+
326
+ Available presets:
327
+
328
+ | Preset | Hue |
329
+ |---|---|
330
+ | `neutral` | Grayscale (default appearance) |
331
+ | `light` | Force light, no dark variant |
332
+ | `dark` | Force dark, no light variant |
333
+ | `violet` | 285° |
334
+ | `emerald` | 162° |
335
+ | `amber` | 65° |
336
+ | `rose` | 15° |
337
+ | `sky` | 235° |
338
+ | `slate` | 240° (low chroma) |
339
+
340
+ ### Build a custom preset from a single hue
341
+
342
+ ```tsx
343
+ import { DataTable, buildPreset } from "@dynostack/react-grid"
344
+
345
+ const teal = buildPreset(180) // hue only
346
+ const subtleTeal = buildPreset(180, 0.015) // hue + custom chroma
347
+
348
+ <DataTable theme={teal} />
349
+ ```
350
+
351
+ `buildPreset(hue, chroma?)` returns a full `{ light, dark }` token set tinted around the given OKLCH hue.
280
352
 
281
353
  ### Compose with a preset
282
354
 
283
355
  ```tsx
284
356
  <DataTable
285
- theme={{ ...themePresets.dark, primary: "oklch(0.7 0.18 250)" }}
357
+ theme={{
358
+ ...themePresets.violet,
359
+ light: { ...themePresets.violet.light, primary: "oklch(0.7 0.18 250)" },
360
+ }}
286
361
  />
287
362
  ```
288
363
 
364
+ ### Isolate a grid from the app theme
365
+
366
+ When embedding inside a heavily-themed shell where you want the table to keep its own look:
367
+
368
+ ```tsx
369
+ <DataTable isolate /* uses bundled neutral tokens, ignores app :root */ />
370
+ <DataTable isolate theme={themePresets.violet} /* isolated AND violet */ />
371
+ ```
372
+
373
+ ### Multiple grids, different themes
374
+
375
+ CSS variables are emitted on each table root, so this works:
376
+
377
+ ```tsx
378
+ <DataTable theme={themePresets.violet} />
379
+ <DataTable theme={themePresets.emerald} />
380
+ <DataTable theme={{ primary: "oklch(0.6 0.2 200)" }} />
381
+ ```
382
+
383
+ ### Precedence summary
384
+
385
+ ```
386
+ ┌──────────────────────────────────────────────────────────┐
387
+ │ Inline style on the grid root (per-instance `theme`) │ ← highest
388
+ ├──────────────────────────────────────────────────────────┤
389
+ │ Consumer's :root rules (shadcn, custom app CSS) │
390
+ ├──────────────────────────────────────────────────────────┤
391
+ │ @layer dynostack-grid-defaults (bundled styles.css) │ ← lowest
392
+ └──────────────────────────────────────────────────────────┘
393
+ ```
394
+
289
395
  ---
290
396
 
291
397
  ## Density
@@ -506,6 +612,72 @@ Mark a column non-exportable via `meta.exportable: false`.
506
612
 
507
613
  ---
508
614
 
615
+ ## View sheet
616
+
617
+ Click the row action "View" → a right-side `Sheet` slides in showing every visible column as a `{Label}: {value}` card. The user can switch layout density inline (Compact 1 col / Relaxed 2 col / Comfy 3 col).
618
+
619
+ Works out of the box with no props. Customize via `viewSheet`:
620
+
621
+ ```tsx
622
+ <DataTable
623
+ viewSheet={{
624
+ side: "right", // or "left"
625
+ defaultDensity: "relaxed", // initial column count
626
+ hideDensityTabs: true, // hide the layout picker
627
+ fields: ["name", "email", "role"], // limit / reorder shown columns
628
+ renderField: ({ column, value, row }) => // override how a value renders
629
+ column.id === "phone" ? <a href={`tel:${value}`}>{String(value)}</a> : null,
630
+ renderHeader: (row) => <YourCustomHeader row={row} />,
631
+ labels: {
632
+ title: (row) => `${row.name} (${row.role})`,
633
+ description: (row) => `Joined ${row.joinedAt}`,
634
+ emptyValue: "—",
635
+ density: { compact: "1 col", relaxed: "2 cols", comfy: "3 cols" },
636
+ },
637
+ }}
638
+ onView={(row) => track("user.view", row)} // optional side-effect
639
+ />
640
+ ```
641
+
642
+ Disable the built-in sheet entirely:
643
+
644
+ ```tsx
645
+ <DataTable viewSheet={false} onView={(row) => router.push(`/users/${row.id}`)} />
646
+ ```
647
+
648
+ `onView` fires before the sheet opens, so you can navigate / log / fetch alongside it.
649
+
650
+ ---
651
+
652
+ ## Delete confirmation
653
+
654
+ 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.
655
+
656
+ ```tsx
657
+ <DataTable
658
+ onDelete={(row) => api.deleteUser(row.id)}
659
+ onBulkDelete={(rows) => api.bulkDelete(rows.map(r => r.id))}
660
+ confirmDelete={{
661
+ title: ({ rows, source }) =>
662
+ source === "bulk"
663
+ ? `Delete ${rows.length} users?`
664
+ : `Delete ${rows[0].name}?`,
665
+ description: ({ rows }) =>
666
+ `${rows.length === 1 ? "This user" : "These users"} will be permanently removed. This cannot be undone.`,
667
+ confirmLabel: "Yes, delete",
668
+ cancelLabel: "Keep",
669
+ }}
670
+ />
671
+ ```
672
+
673
+ Skip the dialog (fire immediately):
674
+
675
+ ```tsx
676
+ <DataTable confirmDelete={false} onDelete={(row) => softDelete(row)} />
677
+ ```
678
+
679
+ ---
680
+
509
681
  ## Custom row actions
510
682
 
511
683
  ```tsx
@@ -625,7 +797,12 @@ operations and only renders the returned page.
625
797
  | `features` | `DataTableFeatures` | all on | Feature flags. |
626
798
  | `labels` | `DataTableLabels` | English defaults | i18n labels. |
627
799
  | `density` | `"compact" \| "default" \| "comfortable"` | `"default"` | Row density. |
628
- | `theme` | `DataTableTheme` | inherits `:root` | Per-instance CSS-variable overrides. |
800
+ | `theme` | `DataTableTheme` | inherits `:root` | Per-instance CSS-variable overrides. Accepts flat tokens **or** `{ light, dark }`. |
801
+ | `isolate` | `boolean` | `false` | Ignore the app's `:root` and render with bundled defaults. |
802
+ | `onView` | `(row: TData) => void` | — | Side-effect when "View" is clicked. Fires *before* the sheet opens. |
803
+ | `onDelete` | `(row: TData) => void` | — | Single-row delete handler. Fires *after* the confirm modal (or immediately if `confirmDelete={false}`). |
804
+ | `viewSheet` | `ViewSheetConfig<TData> \| false` | enabled | Configure or disable the built-in View sheet. |
805
+ | `confirmDelete` | `ConfirmDeleteConfig<TData> \| boolean` | `true` | Configure or disable the delete confirmation modal (applies to single + bulk). |
629
806
 
630
807
  `TData` must extend `{ id: string \| number }`.
631
808
 
@@ -649,6 +826,90 @@ type DataTableDataSource<TData> = {
649
826
  }
650
827
  ```
651
828
 
829
+ ```ts
830
+ // Theme types
831
+ type DataTableTokens = {
832
+ background?: string
833
+ foreground?: string
834
+ card?: string
835
+ cardForeground?: string
836
+ popover?: string
837
+ popoverForeground?: string
838
+ primary?: string
839
+ primaryForeground?: string
840
+ secondary?: string
841
+ secondaryForeground?: string
842
+ muted?: string
843
+ mutedForeground?: string
844
+ accent?: string
845
+ accentForeground?: string
846
+ destructive?: string
847
+ destructiveForeground?: string
848
+ border?: string
849
+ input?: string
850
+ ring?: string
851
+ radius?: string
852
+ fontFamily?: string
853
+ }
854
+
855
+ type DataTableModedTheme = {
856
+ light?: DataTableTokens
857
+ dark?: DataTableTokens
858
+ }
859
+
860
+ type DataTableTheme = DataTableTokens | DataTableModedTheme
861
+ ```
862
+
863
+ ```ts
864
+ // Theme exports
865
+ import {
866
+ themePresets, // ready-made moded presets
867
+ buildPreset, // (hue, chroma?) => DataTableModedTheme
868
+ splitTheme, // (theme) => { light, dark }
869
+ tokensToStyle, // (tokens) => React.CSSProperties
870
+ tokensToCssBlock, // (tokens) => "var:val;var:val" string
871
+ ISOLATE_LIGHT_TOKENS,
872
+ ISOLATE_DARK_TOKENS,
873
+ } from "@dynostack/react-grid"
874
+ ```
875
+
876
+ ```ts
877
+ // View sheet types
878
+ type ViewSheetDensity = "compact" | "relaxed" | "comfy"
879
+
880
+ type ViewSheetConfig<TData> = {
881
+ side?: "right" | "left" | "top" | "bottom"
882
+ defaultDensity?: ViewSheetDensity
883
+ hideDensityTabs?: boolean
884
+ fields?: string[]
885
+ renderField?: (args: {
886
+ column: Column<TData, unknown>
887
+ value: unknown
888
+ row: TData
889
+ }) => React.ReactNode
890
+ renderHeader?: (row: TData) => React.ReactNode
891
+ labels?: {
892
+ title?: (row: TData) => React.ReactNode
893
+ description?: (row: TData) => React.ReactNode
894
+ emptyValue?: string
895
+ density?: { compact?: string; relaxed?: string; comfy?: string }
896
+ }
897
+ }
898
+
899
+ // Confirm-delete types
900
+ type ConfirmDeleteContext<TData> = {
901
+ rows: TData[]
902
+ source: "single" | "bulk"
903
+ }
904
+
905
+ type ConfirmDeleteConfig<TData> = {
906
+ title?: (ctx: ConfirmDeleteContext<TData>) => React.ReactNode
907
+ description?: (ctx: ConfirmDeleteContext<TData>) => React.ReactNode
908
+ confirmLabel?: string
909
+ cancelLabel?: string
910
+ }
911
+ ```
912
+
652
913
  ---
653
914
 
654
915
  ## Compatibility