@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.
- package/LICENSE +21 -0
- package/README.md +640 -0
- package/dist/components/DataBulkActions/DataBulkActions.d.ts +9 -0
- package/dist/components/DataBulkActions/index.d.ts +1 -0
- package/dist/components/DataCards/DataCards.d.ts +24 -0
- package/dist/components/DataCards/index.d.ts +1 -0
- package/dist/components/DataPagination/DataPagination.d.ts +13 -0
- package/dist/components/DataPagination/index.d.ts +1 -0
- package/dist/components/DataTable/DataTable.d.ts +13 -0
- package/dist/components/DataTable/index.d.ts +1 -0
- package/dist/components/DataToolbar/DataToolbar.d.ts +16 -0
- package/dist/components/DataToolbar/FacetBuckets.d.ts +6 -0
- package/dist/components/DataToolbar/FilterControl.d.ts +6 -0
- package/dist/components/DataToolbar/FilterControls.d.ts +5 -0
- package/dist/components/DataToolbar/SortControl.d.ts +4 -0
- package/dist/components/DataToolbar/ViewSwitcher.d.ts +5 -0
- package/dist/components/DataToolbar/VisibilityMenu.d.ts +4 -0
- package/dist/components/DataToolbar/index.d.ts +2 -0
- package/dist/components/DataView/DataView.d.ts +36 -0
- package/dist/components/DataView/context.d.ts +13 -0
- package/dist/components/DataView/index.d.ts +2 -0
- package/dist/components/StateMessage.d.ts +12 -0
- package/dist/components/icons.d.ts +14 -0
- package/dist/components/types.d.ts +43 -0
- package/dist/core/cardComposition.d.ts +34 -0
- package/dist/core/exportCsv.d.ts +10 -0
- package/dist/core/formatValue.d.ts +4 -0
- package/dist/core/resolveStatus.d.ts +11 -0
- package/dist/core/useDataView.d.ts +2 -0
- package/dist/core/useDataViewFetcher.d.ts +7 -0
- package/dist/core/useForceCards.d.ts +8 -0
- package/dist/index.cjs +4 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +1395 -0
- package/dist/index.js.map +1 -0
- package/dist/serializer-CGmBq-Jz.cjs +2 -0
- package/dist/serializer-CGmBq-Jz.cjs.map +1 -0
- package/dist/serializer-u2zq_sU7.js +117 -0
- package/dist/serializer-u2zq_sU7.js.map +1 -0
- package/dist/stories/data.d.ts +14 -0
- package/dist/types/column.d.ts +74 -0
- package/dist/types/facets.d.ts +27 -0
- package/dist/types/options.d.ts +95 -0
- package/dist/types/request.d.ts +18 -0
- package/dist/types/state.d.ts +57 -0
- package/dist/url/index.cjs +2 -0
- package/dist/url/index.cjs.map +1 -0
- package/dist/url/index.d.ts +5 -0
- package/dist/url/index.js +20 -0
- package/dist/url/index.js.map +1 -0
- package/dist/url/serializer.d.ts +33 -0
- package/dist/url/types.d.ts +23 -0
- package/dist/url/useUrlSync.d.ts +23 -0
- 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
|
+
[](https://www.npmjs.com/package/@ethanhann/mantine-dataview)
|
|
4
|
+
[](https://github.com/ethanhann/mantine-grid/actions/workflows/ci.yml)
|
|
5
|
+
[](https://ethanhann.github.io/mantine-grid)
|
|
6
|
+
[](https://ethanhann.github.io/mantine-grid)
|
|
7
|
+
[](./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';
|