@ethanhann/mantine-dataview 0.1.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.
Files changed (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +640 -0
  3. package/dist/components/DataBulkActions/DataBulkActions.d.ts +9 -0
  4. package/dist/components/DataBulkActions/index.d.ts +1 -0
  5. package/dist/components/DataCards/DataCards.d.ts +24 -0
  6. package/dist/components/DataCards/index.d.ts +1 -0
  7. package/dist/components/DataPagination/DataPagination.d.ts +13 -0
  8. package/dist/components/DataPagination/index.d.ts +1 -0
  9. package/dist/components/DataTable/DataTable.d.ts +13 -0
  10. package/dist/components/DataTable/index.d.ts +1 -0
  11. package/dist/components/DataToolbar/DataToolbar.d.ts +16 -0
  12. package/dist/components/DataToolbar/FacetBuckets.d.ts +6 -0
  13. package/dist/components/DataToolbar/FilterControl.d.ts +6 -0
  14. package/dist/components/DataToolbar/FilterControls.d.ts +5 -0
  15. package/dist/components/DataToolbar/SortControl.d.ts +4 -0
  16. package/dist/components/DataToolbar/ViewSwitcher.d.ts +5 -0
  17. package/dist/components/DataToolbar/VisibilityMenu.d.ts +4 -0
  18. package/dist/components/DataToolbar/index.d.ts +2 -0
  19. package/dist/components/DataView/DataView.d.ts +36 -0
  20. package/dist/components/DataView/context.d.ts +13 -0
  21. package/dist/components/DataView/index.d.ts +2 -0
  22. package/dist/components/StateMessage.d.ts +12 -0
  23. package/dist/components/icons.d.ts +14 -0
  24. package/dist/components/types.d.ts +43 -0
  25. package/dist/core/cardComposition.d.ts +34 -0
  26. package/dist/core/exportCsv.d.ts +10 -0
  27. package/dist/core/formatValue.d.ts +4 -0
  28. package/dist/core/resolveStatus.d.ts +11 -0
  29. package/dist/core/useDataView.d.ts +2 -0
  30. package/dist/core/useDataViewFetcher.d.ts +7 -0
  31. package/dist/core/useForceCards.d.ts +8 -0
  32. package/dist/index.cjs +4 -0
  33. package/dist/index.cjs.map +1 -0
  34. package/dist/index.d.ts +19 -0
  35. package/dist/index.js +1395 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/serializer-CGmBq-Jz.cjs +2 -0
  38. package/dist/serializer-CGmBq-Jz.cjs.map +1 -0
  39. package/dist/serializer-u2zq_sU7.js +117 -0
  40. package/dist/serializer-u2zq_sU7.js.map +1 -0
  41. package/dist/stories/data.d.ts +14 -0
  42. package/dist/types/column.d.ts +74 -0
  43. package/dist/types/facets.d.ts +27 -0
  44. package/dist/types/options.d.ts +95 -0
  45. package/dist/types/request.d.ts +18 -0
  46. package/dist/types/state.d.ts +57 -0
  47. package/dist/url/index.cjs +2 -0
  48. package/dist/url/index.cjs.map +1 -0
  49. package/dist/url/index.d.ts +5 -0
  50. package/dist/url/index.js +20 -0
  51. package/dist/url/index.js.map +1 -0
  52. package/dist/url/serializer.d.ts +33 -0
  53. package/dist/url/types.d.ts +23 -0
  54. package/dist/url/useUrlSync.d.ts +23 -0
  55. package/package.json +90 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ethan Hann
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,640 @@
1
+ # @ethanhann/mantine-dataview
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@ethanhann/mantine-dataview.svg)](https://www.npmjs.com/package/@ethanhann/mantine-dataview)
4
+ [![CI](https://github.com/ethanhann/mantine-grid/actions/workflows/ci.yml/badge.svg)](https://github.com/ethanhann/mantine-grid/actions/workflows/ci.yml)
5
+ [![Coverage](https://img.shields.io/endpoint?url=https%3A%2F%2Fethanhann.github.io%2Fmantine-grid%2Fcoverage-badge.json)](https://ethanhann.github.io/mantine-grid)
6
+ [![Storybook](https://img.shields.io/badge/Storybook-deployed-ff4785?logo=storybook&logoColor=white)](https://ethanhann.github.io/mantine-grid)
7
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
8
+
9
+ A reusable React library that renders **server-driven, paginated datasets** as either a
10
+ **table** or a **card grid**, switchable at runtime, with full feature parity between the two —
11
+ built on [Mantine](https://mantine.dev) v9 and [TanStack Table](https://tanstack.com/table) v8.
12
+
13
+ > A table and a card grid are not two components — they are two _projections_ of one shared,
14
+ > headless state. Sort, filter, search, selection, visibility and pagination all live in a single
15
+ > core, so parity between the views is guaranteed by construction, not maintained by hand.
16
+
17
+ ## Features
18
+
19
+ - One hook drives both a Mantine `Table` and a Mantine `Card` grid; switch at runtime.
20
+ - Server-side pagination, sorting (including multi-sort), column filters, and global search.
21
+ - Column data types (`text`, `number`, `currency`, `date`, `boolean`) with automatic Intl-based formatting.
22
+ - Seven filter variants with smart controls: `SegmentedControl` for booleans, `RangeSlider` for bounded numbers, `DatePickerInput` for dates.
23
+ - Custom filter components — bring your own UI per column.
24
+ - Column pinning (left/right) with sticky positioning.
25
+ - CSV export with optional formatted output.
26
+ - Router-agnostic URL state sync with a default History-API adapter.
27
+ - Cross-page row selection + a shared bulk-action bar.
28
+ - Column-meta card composition (`title`/`subtitle`/`media`/`badge`/`meta`) + a `renderCard` escape hatch.
29
+ - Responsive: force-to-cards below a breakpoint, filters collapse to a bottom drawer on mobile.
30
+ - Loading / empty / filtered-empty / error states, consistent across both views.
31
+ - Dark mode support via Mantine's color scheme system.
32
+ - Strongly typed end to end; ships its own `.d.ts`. No icon dependency.
33
+
34
+ ## Install
35
+
36
+ ```sh
37
+ npm install @ethanhann/mantine-dataview
38
+ ```
39
+
40
+ ### Peer dependencies
41
+
42
+ ```sh
43
+ npm install react react-dom @mantine/core @mantine/dates @mantine/hooks @tanstack/react-table
44
+ ```
45
+
46
+ The library renders Mantine components, so your app must import Mantine's styles and wrap the
47
+ tree in a provider:
48
+
49
+ ```tsx
50
+ import "@mantine/core/styles.css";
51
+ import "@mantine/dates/styles.css";
52
+ import { MantineProvider } from "@mantine/core";
53
+
54
+ <MantineProvider>{/* ... */}</MantineProvider>;
55
+ ```
56
+
57
+ ## Quickstart
58
+
59
+ The easiest path is `useDataViewFetcher`, which owns the fetch lifecycle for you:
60
+
61
+ ```tsx
62
+ import {
63
+ DataView,
64
+ useDataViewFetcher,
65
+ createColumnHelper,
66
+ type DataColumnDef,
67
+ } from "@ethanhann/mantine-dataview";
68
+
69
+ interface User {
70
+ id: string;
71
+ name: string;
72
+ email: string;
73
+ status: "active" | "invited";
74
+ }
75
+
76
+ const col = createColumnHelper<User>();
77
+ const columns = [
78
+ col.accessor("name", { header: "Name", meta: { card: { role: "title" } } }),
79
+ col.accessor("email", { header: "Email", meta: { card: { role: "subtitle" } } }),
80
+ col.accessor("status", {
81
+ header: "Status",
82
+ meta: {
83
+ card: { role: "badge" },
84
+ filter: {
85
+ variant: "select",
86
+ options: [
87
+ { value: "active", label: "Active" },
88
+ { value: "invited", label: "Invited" },
89
+ ],
90
+ },
91
+ },
92
+ }),
93
+ ] satisfies DataColumnDef<User>[];
94
+
95
+ function Users() {
96
+ const view = useDataViewFetcher<User>({
97
+ columns,
98
+ getRowId: (u) => u.id,
99
+ fetcher: async (request) => {
100
+ const res = await fetch(`/api/users?${toParams(request)}`);
101
+ const json = await res.json();
102
+ return { rows: json.items, rowCount: json.total };
103
+ },
104
+ });
105
+
106
+ return <DataView view={view} />;
107
+ }
108
+ ```
109
+
110
+ `<DataView view={view} />` renders the toolbar, the active presentation, and pagination.
111
+
112
+ ## Custom layout
113
+
114
+ Compose your own layout by passing children:
115
+
116
+ ```tsx
117
+ <DataView view={view}>
118
+ <DataView.Toolbar />
119
+ <DataView.BulkActions />
120
+ <DataView.Body />
121
+ <DataView.Pagination />
122
+ </DataView>
123
+ ```
124
+
125
+ Or use the standalone components directly for full control:
126
+
127
+ ```tsx
128
+ <DataToolbar view={view} showSearch showFilters />
129
+ <DataTable view={view} striped highlightOnHover />
130
+ <DataPagination view={view} />
131
+ ```
132
+
133
+ ## Controlled (bring your own data layer)
134
+
135
+ `useDataViewFetcher` is a thin convenience wrapper. The core, `useDataView`, is fully controlled —
136
+ you supply `rows`/`rowCount`/`status` and respond to `onRequestChange`:
137
+
138
+ ```tsx
139
+ const [resp, setResp] = useState({ rows: [], rowCount: 0 });
140
+ const [status, setStatus] = useState<Status>("idle");
141
+
142
+ const view = useDataView<User>({
143
+ columns,
144
+ getRowId: (u) => u.id,
145
+ rows: resp.rows,
146
+ rowCount: resp.rowCount,
147
+ status,
148
+ onRequestChange: async (request) => {
149
+ setStatus("loading");
150
+ try {
151
+ setResp(await myApi.list(request));
152
+ setStatus("success");
153
+ } catch {
154
+ setStatus("error");
155
+ }
156
+ },
157
+ });
158
+ ```
159
+
160
+ The request is emitted immediately for pagination/sorting and debounced for search/filters.
161
+
162
+ ## Column data types and formatting
163
+
164
+ Set `dataType` on a column's meta to enable automatic value formatting. When no explicit
165
+ `cell` renderer is provided, the library formats values using `Intl.NumberFormat` or
166
+ `Intl.DateTimeFormat` based on the data type. Raw values are preserved for server requests,
167
+ sorting, and filtering — formatting is display-only.
168
+
169
+ ```tsx
170
+ col.accessor("price", {
171
+ header: "Price",
172
+ meta: { dataType: "currency", align: "right" },
173
+ });
174
+
175
+ col.accessor("createdAt", {
176
+ header: "Created",
177
+ meta: { dataType: "date" },
178
+ });
179
+ ```
180
+
181
+ | Data type | Default format | Example |
182
+ |------------|---------------|---------|
183
+ | `text` | `String(value)` | `"hello"` |
184
+ | `number` | `Intl.NumberFormat` | `1,234` |
185
+ | `currency` | `Intl.NumberFormat` with currency | `$1,234.56` |
186
+ | `date` | `Intl.DateTimeFormat` | `Jun 2, 2026` |
187
+ | `boolean` | `"Yes"` / `"No"` | `Yes` |
188
+
189
+ ### Format overrides (three levels)
190
+
191
+ 1. **Library defaults** — built-in formatters per data type (above).
192
+ 2. **Table-scoped** — `formatDefaults` on the hook options, keyed by data type.
193
+ 3. **Column-scoped** — `format` on `ColumnMeta`, overrides everything for that column.
194
+
195
+ ```tsx
196
+ const view = useDataViewFetcher({
197
+ columns,
198
+ getRowId,
199
+ fetcher,
200
+ // All dates in this table use short format, currency is EUR
201
+ formatDefaults: {
202
+ date: { dateStyle: "short" },
203
+ currency: { currency: "EUR" },
204
+ },
205
+ });
206
+
207
+ // This column overrides the table default
208
+ col.accessor("createdAt", {
209
+ header: "Created",
210
+ meta: {
211
+ dataType: "date",
212
+ format: { dateStyle: "long" },
213
+ },
214
+ });
215
+
216
+ // Or use a function for full control
217
+ col.accessor("revenue", {
218
+ header: "Revenue",
219
+ meta: {
220
+ dataType: "currency",
221
+ format: (v) => `€${(v as number).toFixed(0)}`,
222
+ },
223
+ });
224
+ ```
225
+
226
+ If you provide your own `cell` renderer on a column, it takes full precedence over `dataType`
227
+ formatting.
228
+
229
+ ## Sorting
230
+
231
+ Columns are sortable by default via table header clicks. The `request.sorting` array is sent
232
+ to the server so it can apply the sort.
233
+
234
+ ### Multi-column sorting
235
+
236
+ Hold **Shift** and click additional column headers to add secondary, tertiary, etc. sort keys.
237
+ The header shows a small index number (2, 3, ...) next to the sort icon for secondary sorts.
238
+
239
+ The toolbar's sort control drives the primary sort. Multi-sort is available through table
240
+ headers only.
241
+
242
+ ### Disabling sorting
243
+
244
+ Disable sorting on a specific column:
245
+
246
+ ```tsx
247
+ col.accessor("avatar", { header: "Avatar", enableSorting: false });
248
+ ```
249
+
250
+ ## Custom headers
251
+
252
+ Column headers support the same render function pattern as cells — pass a component or
253
+ function to the `header` property:
254
+
255
+ ```tsx
256
+ col.accessor("revenue", {
257
+ header: () => (
258
+ <Group gap={4}>
259
+ <IconCurrencyDollar size={14} />
260
+ <span>Revenue</span>
261
+ </Group>
262
+ ),
263
+ meta: { align: "right" },
264
+ });
265
+ ```
266
+
267
+ ## CSV export
268
+
269
+ Export the current page's visible columns as a CSV file:
270
+
271
+ ```tsx
272
+ <Button onClick={() => view.exportCsv()}>Export CSV</Button>
273
+
274
+ // With options
275
+ view.exportCsv({ filename: "users.csv", separator: ";" });
276
+
277
+ // Export formatted values instead of raw data
278
+ view.exportCsv({ formatted: true });
279
+ ```
280
+
281
+ The `exportCsv` function is also available as a standalone utility:
282
+
283
+ ```tsx
284
+ import { exportCsv } from "@ethanhann/mantine-dataview";
285
+
286
+ exportCsv(view.table, { filename: "report.csv" });
287
+ ```
288
+
289
+ ## Column pinning
290
+
291
+ Pin columns to the left or right edge so they stay visible while scrolling horizontally.
292
+
293
+ ### Via initial state
294
+
295
+ ```tsx
296
+ const view = useDataViewFetcher<User>({
297
+ columns,
298
+ getRowId,
299
+ fetcher,
300
+ initialState: {
301
+ columnPinning: { left: ["name"], right: ["actions"] },
302
+ },
303
+ });
304
+ ```
305
+
306
+ ### Via the UI
307
+
308
+ The **Columns** dropdown in the toolbar includes pin toggle buttons (left/right) next to each
309
+ column's visibility checkbox. Clicking a pin button freezes that column to the corresponding
310
+ edge; clicking it again unpins.
311
+
312
+ ### Programmatic
313
+
314
+ ```tsx
315
+ view.table.getColumn("name")?.pin("left");
316
+ view.table.getColumn("name")?.pin(false); // unpin
317
+ ```
318
+
319
+ ## Filters
320
+
321
+ ### Built-in filter variants
322
+
323
+ Define filters declaratively on column meta. Seven variants are built in:
324
+
325
+ | Variant | Control | Notes |
326
+ |---------|---------|-------|
327
+ | `text` | `TextInput` | Free-text search |
328
+ | `select` | `Select` (dropdown) | Single choice, clearable |
329
+ | `multiselect` | `MultiSelect` | Multiple choices |
330
+ | `boolean` | `SegmentedControl` (All/Yes/No) | One-click toggle |
331
+ | `numberRange` | `RangeSlider` or two `NumberInput`s | Slider when `min`/`max` are set |
332
+ | `date` | `DatePickerInput` | Calendar picker |
333
+ | `dateRange` | `DatePickerInput` (range) | Two-date calendar picker |
334
+
335
+ ```tsx
336
+ // Boolean — renders as a segmented control
337
+ meta: { filter: { variant: "boolean" } }
338
+
339
+ // Number range with slider
340
+ meta: { filter: { variant: "numberRange", min: 0, max: 1000, step: 10 } }
341
+
342
+ // Number range without bounds (falls back to two number inputs)
343
+ meta: { filter: { variant: "numberRange" } }
344
+
345
+ // Date range
346
+ meta: { filter: { variant: "dateRange" } }
347
+ ```
348
+
349
+ ### Custom filter component
350
+
351
+ For filters that don't fit the built-in variants, provide a `component` instead:
352
+
353
+ ```tsx
354
+ import type { CustomFilterComponentProps } from "@ethanhann/mantine-dataview";
355
+
356
+ function LocationFilter({ value, onChange }: CustomFilterComponentProps) {
357
+ return (
358
+ <Chip.Group value={(value as string) ?? ""} onChange={(v) => onChange(v || undefined)}>
359
+ <Group gap={4}>
360
+ <Chip value="london" size="xs">London</Chip>
361
+ <Chip value="berlin" size="xs">Berlin</Chip>
362
+ </Group>
363
+ </Chip.Group>
364
+ );
365
+ }
366
+
367
+ col.accessor("location", {
368
+ header: "Location",
369
+ meta: { filter: { component: LocationFilter } },
370
+ });
371
+ ```
372
+
373
+ ### Inline filter placement
374
+
375
+ `FilterControl` is exported so you can place individual filters anywhere in your layout:
376
+
377
+ ```tsx
378
+ import { FilterControl } from "@ethanhann/mantine-dataview";
379
+
380
+ <DataView view={view}>
381
+ {view.table.getColumn("inStock") && (
382
+ <FilterControl column={view.table.getColumn("inStock")!} />
383
+ )}
384
+ <DataView.Toolbar />
385
+ <DataView.Body />
386
+ <DataView.Pagination />
387
+ </DataView>
388
+ ```
389
+
390
+ ### Filter display behavior
391
+
392
+ - **Desktop, few filters** (at or below `filterInlineThreshold`, default 3): rendered inline in the toolbar.
393
+ - **Desktop, many filters**: collapsed into a "Filters" popover button with active count badge.
394
+ - **Mobile** (below `sm` breakpoint): always collapsed into a bottom drawer.
395
+ - A "Reset filters" button appears automatically when any filter is active.
396
+
397
+ ## Card composition
398
+
399
+ In card view, each visible column is placed by its `meta.card.role`:
400
+
401
+ | role | rendered as |
402
+ | ---------- | --------------------------- |
403
+ | `title` | card heading |
404
+ | `subtitle` | dimmed line under the title |
405
+ | `media` | full-bleed top section |
406
+ | `badge` | inline badge |
407
+ | `meta` | label / value pair |
408
+ | `hidden` | omitted |
409
+
410
+ Hiding a column via the toolbar hides both its table cell **and** its card field. Within each
411
+ role group, columns are ordered by `meta.card.order`.
412
+
413
+ ### Custom card rendering
414
+
415
+ For full control over card content, use `renderCard`:
416
+
417
+ ```tsx
418
+ <DataView
419
+ view={view}
420
+ renderCard={({ data, selected, toggleSelected }) => (
421
+ <Card withBorder padding="md" onClick={toggleSelected}>
422
+ <Text fw={700}>{data.name}</Text>
423
+ <Text size="sm" c="dimmed">{data.email}</Text>
424
+ {selected && <Badge>Selected</Badge>}
425
+ </Card>
426
+ )}
427
+ />
428
+ ```
429
+
430
+ To keep the default composition but wrap it in a custom card shell, use the `Card` slot:
431
+
432
+ ```tsx
433
+ <DataView
434
+ view={view}
435
+ slots={{
436
+ Card: ({ data, selected, children }) => (
437
+ <Card
438
+ withBorder
439
+ padding="lg"
440
+ style={{ background: selected ? "var(--mantine-color-blue-light)" : undefined }}
441
+ >
442
+ {children}
443
+ </Card>
444
+ ),
445
+ }}
446
+ />
447
+ ```
448
+
449
+ ## Bulk actions
450
+
451
+ Provide a `BulkActions` slot to add actions when rows are selected:
452
+
453
+ ```tsx
454
+ <DataView
455
+ view={view}
456
+ slots={{
457
+ BulkActions: (selection) => (
458
+ <Button
459
+ color="red"
460
+ variant="light"
461
+ onClick={() => {
462
+ deleteUsers(selection.ids);
463
+ selection.clear();
464
+ }}
465
+ >
466
+ Delete {selection.count}
467
+ </Button>
468
+ ),
469
+ }}
470
+ />
471
+ ```
472
+
473
+ The `selection` object provides `count`, `ids` (all selected row IDs across pages),
474
+ `rows` (selected row data on the current page), and `clear()`.
475
+
476
+ ## Custom state slots
477
+
478
+ Override loading, empty, and error states:
479
+
480
+ ```tsx
481
+ <DataView
482
+ view={view}
483
+ slots={{
484
+ Empty: () => <Text>No users found.</Text>,
485
+ ErrorState: ({ retry }) => (
486
+ <Stack align="center">
487
+ <Text c="red">Something went wrong.</Text>
488
+ <Button onClick={retry}>Retry</Button>
489
+ </Stack>
490
+ ),
491
+ LoadingTable: () => <MySkeleton />,
492
+ LoadingCards: () => <MyCardSkeleton />,
493
+ }}
494
+ />
495
+ ```
496
+
497
+ A filtered-empty state is handled automatically — it shows a "clear filters" action so
498
+ users can reset without manually removing each filter.
499
+
500
+ ## URL state sync
501
+
502
+ Router-agnostic. The default adapter uses the History API; memoize it once:
503
+
504
+ ```tsx
505
+ import { windowHistoryAdapter } from "@ethanhann/mantine-dataview/url";
506
+
507
+ const adapter = useMemo(() => windowHistoryAdapter(), []);
508
+ const view = useDataViewFetcher<User>({
509
+ columns,
510
+ getRowId,
511
+ fetcher,
512
+ urlSync: { adapter },
513
+ });
514
+ ```
515
+
516
+ State round-trips through the query string
517
+ (`?page=2&size=25&sort=name:asc&q=ada&view=cards&f.status=active`) and stays in sync with
518
+ browser back/forward.
519
+
520
+ ### `UrlStateAdapter` interface
521
+
522
+ To integrate with a router, implement these three methods:
523
+
524
+ ```ts
525
+ interface UrlStateAdapter {
526
+ /** Current query params as a flat record. */
527
+ read(): Record<string, string>;
528
+ /** Write the next params; `replace` controls history entry vs push. */
529
+ write(next: Record<string, string>, opts?: { replace?: boolean }): void;
530
+ /** Optional: notify on external nav (back/forward). Returns an unsubscribe fn. */
531
+ subscribe?(onChange: () => void): () => void;
532
+ }
533
+ ```
534
+
535
+ > Always memoize the adapter so the sync effects don't re-bind every render.
536
+
537
+ ### React Router
538
+
539
+ ```tsx
540
+ import { useSearchParams } from "react-router-dom";
541
+ import type { UrlStateAdapter } from "@ethanhann/mantine-dataview/url";
542
+
543
+ function useReactRouterAdapter(): UrlStateAdapter {
544
+ const [searchParams, setSearchParams] = useSearchParams();
545
+ return useMemo<UrlStateAdapter>(
546
+ () => ({
547
+ read: () => Object.fromEntries(new URLSearchParams(window.location.search)),
548
+ write: (next, opts) => setSearchParams(next, { replace: opts?.replace }),
549
+ }),
550
+ [searchParams, setSearchParams],
551
+ );
552
+ }
553
+ ```
554
+
555
+ ### TanStack Router
556
+
557
+ ```tsx
558
+ import { useNavigate } from "@tanstack/react-router";
559
+ import type { UrlStateAdapter } from "@ethanhann/mantine-dataview/url";
560
+
561
+ function useTanStackRouterAdapter(): UrlStateAdapter {
562
+ const navigate = useNavigate();
563
+ return useMemo<UrlStateAdapter>(
564
+ () => ({
565
+ read: () => Object.fromEntries(new URLSearchParams(window.location.search)),
566
+ write: (next, opts) => navigate({ search: () => next, replace: opts?.replace }),
567
+ }),
568
+ [navigate],
569
+ );
570
+ }
571
+ ```
572
+
573
+ ### URL sync options
574
+
575
+ - Restrict which slices sync with `urlSync.include` (e.g. only pagination and sorting).
576
+ - Override param names or codecs with `urlSync.serialize`.
577
+ - Selection, column visibility, and column pinning are not URL-synced by design.
578
+
579
+ ## Responsive behavior
580
+
581
+ Force the card view below a breakpoint:
582
+
583
+ ```tsx
584
+ const view = useDataViewFetcher<User>({
585
+ columns,
586
+ getRowId,
587
+ fetcher,
588
+ responsive: { forceCardsBelow: "sm", lockSwitcherOnMobile: true },
589
+ });
590
+
591
+ <DataView view={view} lockSwitcherOnMobile />;
592
+ ```
593
+
594
+ When `forceCardsBelow` is set and the viewport is below that breakpoint:
595
+ - The view is forced to cards regardless of the user's choice.
596
+ - The user's explicit choice is preserved and restored above the breakpoint.
597
+ - The view switcher is disabled (or hidden entirely with `lockSwitcherOnMobile`).
598
+ - Filters always open in a bottom drawer on mobile.
599
+
600
+ ## API overview
601
+
602
+ | Export | Purpose |
603
+ | --------------------------------------------------------------------- | --------------------------------------------- |
604
+ | `useDataView` | Headless core — owns all feature state |
605
+ | `useDataViewFetcher` | Convenience wrapper that manages the fetch |
606
+ | `DataView` (+ `.Toolbar` / `.BulkActions` / `.Body` / `.Pagination`) | Orchestrator + compound parts |
607
+ | `DataTable`, `DataCards` | The two presentations (usable standalone) |
608
+ | `DataToolbar`, `DataPagination`, `DataBulkActions` | Standalone affordances |
609
+ | `FilterControl` | Individual filter control (place anywhere) |
610
+ | `exportCsv` | Standalone CSV export utility |
611
+ | `createColumnHelper`, `composeCardLayout`, `resolveColumnLabel` | Column helpers |
612
+ | `@ethanhann/mantine-dataview/url` | `windowHistoryAdapter` + serializer utilities |
613
+
614
+ ### Customization slots
615
+
616
+ Passed via the `slots` prop on `DataView` or the presentation components:
617
+
618
+ | Slot | Receives | Purpose |
619
+ | -------------- | ------------------------------------------ | --------------------------------- |
620
+ | `Empty` | `{ view }` | No data state |
621
+ | `ErrorState` | `{ retry }` | Error with retry action |
622
+ | `LoadingTable` | — | Table skeleton replacement |
623
+ | `LoadingCards` | — | Card skeleton replacement |
624
+ | `Row` | `{ row, children }` | Wrap each table row |
625
+ | `Card` | `{ row, data, selected, children }` | Wrap each card |
626
+ | `BulkActions` | `{ count, ids, rows, clear }` | Bulk action bar content |
627
+
628
+ ## Development
629
+
630
+ ```sh
631
+ npm run dev # Storybook
632
+ npm test # Vitest (watch)
633
+ npm run test:coverage
634
+ npm run typecheck
635
+ npm run build
636
+ ```
637
+
638
+ ## License
639
+
640
+ MIT
@@ -0,0 +1,9 @@
1
+ import { PaperProps } from '@mantine/core';
2
+ import { UseDataViewReturn } from '../../types/options';
3
+ import { DataViewSlots } from '../types';
4
+ export interface DataBulkActionsProps<TData> extends Omit<PaperProps, "children"> {
5
+ /** The `useDataView` instance to project. */
6
+ view: UseDataViewReturn<TData>;
7
+ slots?: DataViewSlots<TData>;
8
+ }
9
+ export declare function DataBulkActions<TData>({ view, slots, ...paperProps }: DataBulkActionsProps<TData>): import("react").JSX.Element | null;
@@ -0,0 +1 @@
1
+ export { DataBulkActions, type DataBulkActionsProps, } from './DataBulkActions';
@@ -0,0 +1,24 @@
1
+ import { SimpleGridProps } from '@mantine/core';
2
+ import { Row } from '@tanstack/react-table';
3
+ import { ReactNode } from 'react';
4
+ import { ComposeCardOptions } from '../../core/cardComposition';
5
+ import { UseDataViewReturn } from '../../types/options';
6
+ import { DataViewSlots } from '../types';
7
+ export interface DataCardsProps<TData> extends Omit<SimpleGridProps, "children"> {
8
+ /** The `useDataView` instance to project. */
9
+ view: UseDataViewReturn<TData>;
10
+ slots?: DataViewSlots<TData>;
11
+ /** Full per card escape hatch. It replaces the default composition. */
12
+ renderCard?: (ctx: {
13
+ row: Row<TData>;
14
+ data: TData;
15
+ selected: boolean;
16
+ toggleSelected: () => void;
17
+ }) => ReactNode;
18
+ /** Role for accessor columns that declare none. It is forwarded to the composition. */
19
+ fallbackRole?: ComposeCardOptions["fallbackRole"];
20
+ enableSelection?: boolean;
21
+ /** Skeleton cards shown while loading. It defaults to the current page size, capped at 6. */
22
+ loadingCardCount?: number;
23
+ }
24
+ export declare function DataCards<TData>({ view, slots, renderCard, fallbackRole, enableSelection, loadingCardCount, cols, ...gridProps }: DataCardsProps<TData>): string | number | bigint | boolean | Iterable<ReactNode> | Promise<string | number | bigint | boolean | import('react').ReactPortal | import('react').ReactElement<unknown, string | import('react').JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | import("react").JSX.Element | null | undefined;
@@ -0,0 +1 @@
1
+ export { DataCards, type DataCardsProps } from './DataCards';