@ethanhann/mantine-dataview 0.1.2 → 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 +549 -252
- package/dist/components/DataCards/DataCards.d.ts +3 -1
- package/dist/components/DataTable/DataTable.d.ts +5 -1
- package/dist/components/DataToolbar/DataToolbar.d.ts +8 -1
- package/dist/components/DataToolbar/ViewSwitcher.d.ts +8 -2
- package/dist/components/DataToolbar/index.d.ts +1 -0
- package/dist/components/DataView/DataView.d.ts +3 -1
- package/dist/components/DataView/context.d.ts +1 -0
- package/dist/core/colBuilder.d.ts +37 -0
- package/dist/core/useDataViewFetcher.d.ts +3 -1
- package/dist/core/useRowTransition.d.ts +8 -0
- package/dist/index.cjs +3 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +655 -484
- package/dist/index.js.map +1 -1
- package/dist/mantine-dataview.css +2 -0
- package/dist/types/options.d.ts +7 -1
- package/dist/types/request.d.ts +4 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,26 +7,27 @@
|
|
|
7
7
|
[](./LICENSE)
|
|
8
8
|
|
|
9
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.
|
|
10
|
+
**table** or a **card grid**, switchable at runtime, with full feature parity between the two.
|
|
12
11
|
|
|
13
|
-
|
|
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.
|
|
12
|
+
Built on [Mantine](https://mantine.dev) v9 and [TanStack Table](https://tanstack.com/table) v8.
|
|
16
13
|
|
|
17
14
|
## Features
|
|
18
15
|
|
|
19
16
|
- One hook drives both a Mantine `Table` and a Mantine `Card` grid; switch at runtime.
|
|
20
17
|
- Server-side pagination, sorting (including multi-sort), column filters, and global search.
|
|
21
18
|
- 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,
|
|
23
|
-
|
|
19
|
+
- Seven filter variants with smart controls: `SegmentedControl` for booleans, `RangeSlider` for bounded numbers,
|
|
20
|
+
`DatePickerInput` for dates.
|
|
21
|
+
- Custom filter components. Bring your own UI per column.
|
|
24
22
|
- Column pinning (left/right) with sticky positioning.
|
|
25
23
|
- CSV export with optional formatted output.
|
|
26
24
|
- Router-agnostic URL state sync with a default History-API adapter.
|
|
27
25
|
- Cross-page row selection + a shared bulk-action bar.
|
|
28
26
|
- Column-meta card composition (`title`/`subtitle`/`media`/`badge`/`meta`) + a `renderCard` escape hatch.
|
|
29
27
|
- Responsive: force-to-cards below a breakpoint, filters collapse to a bottom drawer on mobile.
|
|
28
|
+
- Faceted search with server-provided counts on filter options and range buckets with dynamic totals.
|
|
29
|
+
- External parameters (`params`) for scope selectors, toggles, and other non-column filters.
|
|
30
|
+
- Controls automatically disabled while data is loading (opt-out with `disableWhileLoading`).
|
|
30
31
|
- Loading / empty / filtered-empty / error states, consistent across both views.
|
|
31
32
|
- Dark mode support via Mantine's color scheme system.
|
|
32
33
|
- Strongly typed end to end; ships its own `.d.ts`. No icon dependency.
|
|
@@ -49,7 +50,7 @@ tree in a provider:
|
|
|
49
50
|
```tsx
|
|
50
51
|
import "@mantine/core/styles.css";
|
|
51
52
|
import "@mantine/dates/styles.css";
|
|
52
|
-
import {
|
|
53
|
+
import {MantineProvider} from "@mantine/core";
|
|
53
54
|
|
|
54
55
|
<MantineProvider>{/* ... */}</MantineProvider>;
|
|
55
56
|
```
|
|
@@ -60,166 +61,307 @@ The easiest path is `useDataViewFetcher`, which owns the fetch lifecycle for you
|
|
|
60
61
|
|
|
61
62
|
```tsx
|
|
62
63
|
import {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
DataView,
|
|
65
|
+
useDataViewFetcher,
|
|
66
|
+
createColumnHelper,
|
|
67
|
+
type DataColumnDef,
|
|
67
68
|
} from "@ethanhann/mantine-dataview";
|
|
68
69
|
|
|
69
70
|
interface User {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
id: string;
|
|
72
|
+
name: string;
|
|
73
|
+
email: string;
|
|
74
|
+
status: "active" | "invited";
|
|
74
75
|
}
|
|
75
76
|
|
|
76
77
|
const col = createColumnHelper<User>();
|
|
77
78
|
const columns = [
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
79
|
+
col.accessor("name", {header: "Name", meta: {card: {role: "title"}}}),
|
|
80
|
+
col.accessor("email", {header: "Email", meta: {card: {role: "subtitle"}}}),
|
|
81
|
+
col.accessor("status", {
|
|
82
|
+
header: "Status",
|
|
83
|
+
meta: {
|
|
84
|
+
card: {role: "badge"},
|
|
85
|
+
filter: {
|
|
86
|
+
variant: "select",
|
|
87
|
+
options: [
|
|
88
|
+
{value: "active", label: "Active"},
|
|
89
|
+
{value: "invited", label: "Invited"},
|
|
90
|
+
],
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
}),
|
|
93
94
|
] satisfies DataColumnDef<User>[];
|
|
94
95
|
|
|
95
96
|
function Users() {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
97
|
+
const view = useDataViewFetcher<User>({
|
|
98
|
+
columns,
|
|
99
|
+
getRowId: (u) => u.id,
|
|
100
|
+
fetcher: async (request) => {
|
|
101
|
+
const res = await fetch(`/api/users?${toParams(request)}`);
|
|
102
|
+
const json = await res.json();
|
|
103
|
+
return {rows: json.items, rowCount: json.total};
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
return <DataView view={view}/>;
|
|
107
108
|
}
|
|
108
109
|
```
|
|
109
110
|
|
|
110
111
|
`<DataView view={view} />` renders the toolbar, the active presentation, and pagination.
|
|
111
112
|
|
|
113
|
+
## Column builder
|
|
114
|
+
|
|
115
|
+
The fluent `col<T>()` builder reduces column definition verbosity. Each method sets
|
|
116
|
+
`dataType`, `filter`, `align`, and `meta.label` with sensible defaults for the type:
|
|
117
|
+
|
|
118
|
+
```tsx
|
|
119
|
+
import {col} from "@ethanhann/mantine-dataview";
|
|
120
|
+
|
|
121
|
+
const columns = col<User>()
|
|
122
|
+
.text("name", {card: "title"})
|
|
123
|
+
.text("email", {card: "subtitle", filter: false})
|
|
124
|
+
.currency("salary", {card: "meta"})
|
|
125
|
+
.number("age", {card: "meta", filter: {min: 18, max: 100}})
|
|
126
|
+
.boolean("active", {card: "badge"})
|
|
127
|
+
.date("createdAt", {card: "meta"})
|
|
128
|
+
.select("role", {
|
|
129
|
+
options: [{value: "admin", label: "Admin"}, {value: "user", label: "User"}],
|
|
130
|
+
card: "badge",
|
|
131
|
+
})
|
|
132
|
+
.build();
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Available presets
|
|
136
|
+
|
|
137
|
+
| Method | `dataType` | `filter` | `align` |
|
|
138
|
+
|------------------------------------|------------|---------------|---------|
|
|
139
|
+
| `.text(field)` | `text` | `text` | left |
|
|
140
|
+
| `.number(field)` | `number` | `numberRange` | right |
|
|
141
|
+
| `.currency(field)` | `currency` | `numberRange` | right |
|
|
142
|
+
| `.date(field)` | `date` | `dateRange` | left |
|
|
143
|
+
| `.boolean(field)` | `boolean` | `boolean` | left |
|
|
144
|
+
| `.select(field, { options })` | — | `select` | left |
|
|
145
|
+
| `.multiselect(field, { options })` | — | `multiselect` | left |
|
|
146
|
+
| `.custom(colDef)` | — | — | — |
|
|
147
|
+
|
|
148
|
+
Headers are auto-humanized from field names (`createdAt` → `"Created At"`,
|
|
149
|
+
`first_name` → `"First Name"`). Override with `{ header: "Custom Label" }`.
|
|
150
|
+
|
|
151
|
+
Options: `header`, `card` (role shorthand), `cardOrder`, `filter` (`false` to disable,
|
|
152
|
+
or object to merge), `format`, `align`, `cell`, `enableSorting`.
|
|
153
|
+
|
|
112
154
|
## Custom layout
|
|
113
155
|
|
|
114
156
|
Compose your own layout by passing children:
|
|
115
157
|
|
|
116
158
|
```tsx
|
|
117
159
|
<DataView view={view}>
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
160
|
+
<DataView.Toolbar/>
|
|
161
|
+
<DataView.BulkActions/>
|
|
162
|
+
<DataView.Body/>
|
|
163
|
+
<DataView.Pagination/>
|
|
122
164
|
</DataView>
|
|
123
165
|
```
|
|
124
166
|
|
|
125
167
|
Or use the standalone components directly for full control:
|
|
126
168
|
|
|
127
169
|
```tsx
|
|
128
|
-
<DataToolbar view={view} showSearch showFilters
|
|
129
|
-
<DataTable view={view} striped highlightOnHover
|
|
130
|
-
<DataPagination view={view}
|
|
170
|
+
<DataToolbar view={view} showSearch showFilters/>
|
|
171
|
+
<DataTable view={view} striped highlightOnHover/>
|
|
172
|
+
<DataPagination view={view}/>
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Toolbar sections
|
|
176
|
+
|
|
177
|
+
Inject controls into the toolbar without rebuilding it from scratch using `leftSection`
|
|
178
|
+
and `rightSection`:
|
|
179
|
+
|
|
180
|
+
```tsx
|
|
181
|
+
<DataView.Toolbar
|
|
182
|
+
leftSection={<Text fw={600}>Users</Text>}
|
|
183
|
+
rightSection={
|
|
184
|
+
<Group gap="xs">
|
|
185
|
+
<Button size="xs" onClick={() => view.exportCsv()}>Export</Button>
|
|
186
|
+
<Button size="xs" onClick={() => view.refetch()}>Refresh</Button>
|
|
187
|
+
</Group>
|
|
188
|
+
}
|
|
189
|
+
/>
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
- `leftSection` renders before the search input (start of the left group).
|
|
193
|
+
- `rightSection` renders after the view switcher (end of the right group).
|
|
194
|
+
|
|
195
|
+
Both sections are disabled during loading along with the other toolbar controls.
|
|
196
|
+
|
|
197
|
+
### View switcher
|
|
198
|
+
|
|
199
|
+
The `ViewSwitcher` is exported for standalone use with customizable labels:
|
|
200
|
+
|
|
201
|
+
```tsx
|
|
202
|
+
import { ViewSwitcher } from "@ethanhann/mantine-dataview";
|
|
203
|
+
|
|
204
|
+
// Default
|
|
205
|
+
<ViewSwitcher view={view} />
|
|
206
|
+
|
|
207
|
+
// Custom labels (text or icons)
|
|
208
|
+
<ViewSwitcher view={view} tableLabel="List" cardsLabel="Grid" />
|
|
209
|
+
<ViewSwitcher view={view} tableLabel={<IconList />} cardsLabel={<IconGrid />} />
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Or drive the view programmatically:
|
|
213
|
+
|
|
214
|
+
```tsx
|
|
215
|
+
view.setView("cards"); // switch to cards
|
|
216
|
+
view.view; // current view: "table" | "cards"
|
|
131
217
|
```
|
|
132
218
|
|
|
133
219
|
## Controlled (bring your own data layer)
|
|
134
220
|
|
|
135
|
-
`useDataViewFetcher` is a thin convenience wrapper. The core, `useDataView`, is fully controlled
|
|
136
|
-
|
|
221
|
+
`useDataViewFetcher` is a thin convenience wrapper. The core, `useDataView`, is fully controlled.
|
|
222
|
+
You supply `rows`/`rowCount`/`status` and respond to `onRequestChange`:
|
|
137
223
|
|
|
138
224
|
```tsx
|
|
139
|
-
const [resp, setResp] = useState({
|
|
225
|
+
const [resp, setResp] = useState({rows: [], rowCount: 0});
|
|
140
226
|
const [status, setStatus] = useState<Status>("idle");
|
|
141
227
|
|
|
142
228
|
const view = useDataView<User>({
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
229
|
+
columns,
|
|
230
|
+
getRowId: (u) => u.id,
|
|
231
|
+
rows: resp.rows,
|
|
232
|
+
rowCount: resp.rowCount,
|
|
233
|
+
status,
|
|
234
|
+
onRequestChange: async (request) => {
|
|
235
|
+
setStatus("loading");
|
|
236
|
+
try {
|
|
237
|
+
setResp(await myApi.list(request));
|
|
238
|
+
setStatus("success");
|
|
239
|
+
} catch {
|
|
240
|
+
setStatus("error");
|
|
241
|
+
}
|
|
242
|
+
},
|
|
157
243
|
});
|
|
158
244
|
```
|
|
159
245
|
|
|
160
246
|
The request is emitted immediately for pagination/sorting and debounced for search/filters.
|
|
161
247
|
|
|
248
|
+
### External parameters
|
|
249
|
+
|
|
250
|
+
Pass arbitrary parameters that aren't tied to a column. They're included in every
|
|
251
|
+
`DataViewRequest`, trigger a refetch when they change, and reset pagination to page 1:
|
|
252
|
+
|
|
253
|
+
```tsx
|
|
254
|
+
const [tenantId, setTenantId] = useState("acme");
|
|
255
|
+
const [showArchived, setShowArchived] = useState(false);
|
|
256
|
+
|
|
257
|
+
const view = useDataViewFetcher<User>({
|
|
258
|
+
columns,
|
|
259
|
+
getRowId,
|
|
260
|
+
fetcher: async (request) => {
|
|
261
|
+
// request.params = { tenantId: "acme", showArchived: false }
|
|
262
|
+
const res = await api.list(request);
|
|
263
|
+
return {rows: res.items, rowCount: res.total};
|
|
264
|
+
},
|
|
265
|
+
params: {tenantId, showArchived},
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// Render your own controls...
|
|
269
|
+
<Select data={tenants} value={tenantId} onChange={setTenantId}/>
|
|
270
|
+
<Switch checked={showArchived} onChange={(e) => setShowArchived(e.currentTarget.checked)}/>
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Values are typed as `FilterParam` (`string | number | boolean | null | string[] | number[]`).
|
|
274
|
+
|
|
275
|
+
### Refetching on external changes
|
|
276
|
+
|
|
277
|
+
For cases where external state affects the fetcher but isn't a named parameter (e.g. it's
|
|
278
|
+
baked into the closure), use `deps` to trigger a refetch:
|
|
279
|
+
|
|
280
|
+
```tsx
|
|
281
|
+
const view = useDataViewFetcher<User>({
|
|
282
|
+
columns,
|
|
283
|
+
getRowId,
|
|
284
|
+
fetcher,
|
|
285
|
+
deps: [selectedTenantId],
|
|
286
|
+
});
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
When any value in `deps` changes, the current request is re-emitted to the fetcher.
|
|
290
|
+
Prefer `params` when the server needs to see the values; use `deps` when they're already
|
|
291
|
+
in the fetcher closure.
|
|
292
|
+
|
|
293
|
+
### Manual refresh
|
|
294
|
+
|
|
295
|
+
Re-fetch the current data without changing any state:
|
|
296
|
+
|
|
297
|
+
```tsx
|
|
298
|
+
<Button onClick={() => view.refetch()}>Refresh</Button>
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
This re-emits the current request to the fetcher. It's the same mechanism the built-in
|
|
302
|
+
error retry button uses.
|
|
303
|
+
|
|
162
304
|
## Column data types and formatting
|
|
163
305
|
|
|
164
306
|
Set `dataType` on a column's meta to enable automatic value formatting. When no explicit
|
|
165
307
|
`cell` renderer is provided, the library formats values using `Intl.NumberFormat` or
|
|
166
308
|
`Intl.DateTimeFormat` based on the data type. Raw values are preserved for server requests,
|
|
167
|
-
sorting, and filtering
|
|
309
|
+
sorting, and filtering. Formatting is display-only.
|
|
168
310
|
|
|
169
311
|
```tsx
|
|
170
312
|
col.accessor("price", {
|
|
171
|
-
|
|
172
|
-
|
|
313
|
+
header: "Price",
|
|
314
|
+
meta: {dataType: "currency", align: "right"},
|
|
173
315
|
});
|
|
174
316
|
|
|
175
317
|
col.accessor("createdAt", {
|
|
176
|
-
|
|
177
|
-
|
|
318
|
+
header: "Created",
|
|
319
|
+
meta: {dataType: "date"},
|
|
178
320
|
});
|
|
179
321
|
```
|
|
180
322
|
|
|
181
|
-
| Data type | Default format
|
|
182
|
-
|
|
183
|
-
| `text` | `String(value)`
|
|
184
|
-
| `number` | `Intl.NumberFormat`
|
|
185
|
-
| `currency` | `Intl.NumberFormat` with currency | `$1,234.56`
|
|
186
|
-
| `date` | `Intl.DateTimeFormat`
|
|
187
|
-
| `boolean` | `"Yes"` / `"No"`
|
|
323
|
+
| Data type | Default format | Example |
|
|
324
|
+
|------------|-----------------------------------|---------------|
|
|
325
|
+
| `text` | `String(value)` | `"hello"` |
|
|
326
|
+
| `number` | `Intl.NumberFormat` | `1,234` |
|
|
327
|
+
| `currency` | `Intl.NumberFormat` with currency | `$1,234.56` |
|
|
328
|
+
| `date` | `Intl.DateTimeFormat` | `Jun 2, 2026` |
|
|
329
|
+
| `boolean` | `"Yes"` / `"No"` | `Yes` |
|
|
188
330
|
|
|
189
331
|
### Format overrides (three levels)
|
|
190
332
|
|
|
191
|
-
1. **Library defaults
|
|
192
|
-
2. **Table-scoped
|
|
193
|
-
3. **Column-scoped
|
|
333
|
+
1. **Library defaults**, the built-in formatters per data type (above).
|
|
334
|
+
2. **Table-scoped**, `formatDefaults` on the hook options, keyed by data type.
|
|
335
|
+
3. **Column-scoped**, `format` on `ColumnMeta`, overrides everything for that column.
|
|
194
336
|
|
|
195
337
|
```tsx
|
|
196
338
|
const view = useDataViewFetcher({
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
339
|
+
columns,
|
|
340
|
+
getRowId,
|
|
341
|
+
fetcher,
|
|
342
|
+
// All dates in this table use short format, currency is EUR
|
|
343
|
+
formatDefaults: {
|
|
344
|
+
date: {dateStyle: "short"},
|
|
345
|
+
currency: {currency: "EUR"},
|
|
346
|
+
},
|
|
205
347
|
});
|
|
206
348
|
|
|
207
349
|
// This column overrides the table default
|
|
208
350
|
col.accessor("createdAt", {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
351
|
+
header: "Created",
|
|
352
|
+
meta: {
|
|
353
|
+
dataType: "date",
|
|
354
|
+
format: {dateStyle: "long"},
|
|
355
|
+
},
|
|
214
356
|
});
|
|
215
357
|
|
|
216
358
|
// Or use a function for full control
|
|
217
359
|
col.accessor("revenue", {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
360
|
+
header: "Revenue",
|
|
361
|
+
meta: {
|
|
362
|
+
dataType: "currency",
|
|
363
|
+
format: (v) => `€${(v as number).toFixed(0)}`,
|
|
364
|
+
},
|
|
223
365
|
});
|
|
224
366
|
```
|
|
225
367
|
|
|
@@ -244,23 +386,23 @@ headers only.
|
|
|
244
386
|
Disable sorting on a specific column:
|
|
245
387
|
|
|
246
388
|
```tsx
|
|
247
|
-
col.accessor("avatar", {
|
|
389
|
+
col.accessor("avatar", {header: "Avatar", enableSorting: false});
|
|
248
390
|
```
|
|
249
391
|
|
|
250
392
|
## Custom headers
|
|
251
393
|
|
|
252
|
-
Column headers support the same render function pattern as cells
|
|
394
|
+
Column headers support the same render function pattern as cells. Pass a component or
|
|
253
395
|
function to the `header` property:
|
|
254
396
|
|
|
255
397
|
```tsx
|
|
256
398
|
col.accessor("revenue", {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
399
|
+
header: () => (
|
|
400
|
+
<Group gap={4}>
|
|
401
|
+
<IconCurrencyDollar size={14}/>
|
|
402
|
+
<span>Revenue</span>
|
|
403
|
+
</Group>
|
|
404
|
+
),
|
|
405
|
+
meta: {align: "right"},
|
|
264
406
|
});
|
|
265
407
|
```
|
|
266
408
|
|
|
@@ -272,18 +414,18 @@ Export the current page's visible columns as a CSV file:
|
|
|
272
414
|
<Button onClick={() => view.exportCsv()}>Export CSV</Button>
|
|
273
415
|
|
|
274
416
|
// With options
|
|
275
|
-
view.exportCsv({
|
|
417
|
+
view.exportCsv({filename: "users.csv", separator: ";"});
|
|
276
418
|
|
|
277
419
|
// Export formatted values instead of raw data
|
|
278
|
-
view.exportCsv({
|
|
420
|
+
view.exportCsv({formatted: true});
|
|
279
421
|
```
|
|
280
422
|
|
|
281
423
|
The `exportCsv` function is also available as a standalone utility:
|
|
282
424
|
|
|
283
425
|
```tsx
|
|
284
|
-
import {
|
|
426
|
+
import {exportCsv} from "@ethanhann/mantine-dataview";
|
|
285
427
|
|
|
286
|
-
exportCsv(view.table, {
|
|
428
|
+
exportCsv(view.table, {filename: "report.csv"});
|
|
287
429
|
```
|
|
288
430
|
|
|
289
431
|
## Column pinning
|
|
@@ -294,12 +436,12 @@ Pin columns to the left or right edge so they stay visible while scrolling horiz
|
|
|
294
436
|
|
|
295
437
|
```tsx
|
|
296
438
|
const view = useDataViewFetcher<User>({
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
439
|
+
columns,
|
|
440
|
+
getRowId,
|
|
441
|
+
fetcher,
|
|
442
|
+
initialState: {
|
|
443
|
+
columnPinning: {left: ["name"], right: ["actions"]},
|
|
444
|
+
},
|
|
303
445
|
});
|
|
304
446
|
```
|
|
305
447
|
|
|
@@ -322,28 +464,50 @@ view.table.getColumn("name")?.pin(false); // unpin
|
|
|
322
464
|
|
|
323
465
|
Define filters declaratively on column meta. Seven variants are built in:
|
|
324
466
|
|
|
325
|
-
| Variant
|
|
326
|
-
|
|
327
|
-
| `text`
|
|
328
|
-
| `select`
|
|
329
|
-
| `multiselect` | `MultiSelect`
|
|
330
|
-
| `boolean`
|
|
467
|
+
| Variant | Control | Notes |
|
|
468
|
+
|---------------|-------------------------------------|---------------------------------|
|
|
469
|
+
| `text` | `TextInput` | Free-text search |
|
|
470
|
+
| `select` | `Select` (dropdown) | Single choice, clearable |
|
|
471
|
+
| `multiselect` | `MultiSelect` | Multiple choices |
|
|
472
|
+
| `boolean` | `SegmentedControl` (All/Yes/No) | One-click toggle |
|
|
331
473
|
| `numberRange` | `RangeSlider` or two `NumberInput`s | Slider when `min`/`max` are set |
|
|
332
|
-
| `date`
|
|
333
|
-
| `dateRange`
|
|
474
|
+
| `date` | `DatePickerInput` | Calendar picker |
|
|
475
|
+
| `dateRange` | `DatePickerInput` (range) | Two-date calendar picker |
|
|
334
476
|
|
|
335
477
|
```tsx
|
|
336
|
-
// Boolean
|
|
337
|
-
meta: {
|
|
478
|
+
// Boolean, renders as a segmented control
|
|
479
|
+
meta: {
|
|
480
|
+
filter: {
|
|
481
|
+
variant: "boolean"
|
|
482
|
+
}
|
|
483
|
+
}
|
|
338
484
|
|
|
339
485
|
// Number range with slider
|
|
340
|
-
meta: {
|
|
486
|
+
meta: {
|
|
487
|
+
filter: {
|
|
488
|
+
variant: "numberRange", min
|
|
489
|
+
:
|
|
490
|
+
0, max
|
|
491
|
+
:
|
|
492
|
+
1000, step
|
|
493
|
+
:
|
|
494
|
+
10
|
|
495
|
+
}
|
|
496
|
+
}
|
|
341
497
|
|
|
342
498
|
// Number range without bounds (falls back to two number inputs)
|
|
343
|
-
meta: {
|
|
499
|
+
meta: {
|
|
500
|
+
filter: {
|
|
501
|
+
variant: "numberRange"
|
|
502
|
+
}
|
|
503
|
+
}
|
|
344
504
|
|
|
345
505
|
// Date range
|
|
346
|
-
meta: {
|
|
506
|
+
meta: {
|
|
507
|
+
filter: {
|
|
508
|
+
variant: "dateRange"
|
|
509
|
+
}
|
|
510
|
+
}
|
|
347
511
|
```
|
|
348
512
|
|
|
349
513
|
### Custom filter component
|
|
@@ -351,22 +515,22 @@ meta: { filter: { variant: "dateRange" } }
|
|
|
351
515
|
For filters that don't fit the built-in variants, provide a `component` instead:
|
|
352
516
|
|
|
353
517
|
```tsx
|
|
354
|
-
import type {
|
|
355
|
-
|
|
356
|
-
function LocationFilter({
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
518
|
+
import type {CustomFilterComponentProps} from "@ethanhann/mantine-dataview";
|
|
519
|
+
|
|
520
|
+
function LocationFilter({value, onChange}: CustomFilterComponentProps) {
|
|
521
|
+
return (
|
|
522
|
+
<Chip.Group value={(value as string) ?? ""} onChange={(v) => onChange(v || undefined)}>
|
|
523
|
+
<Group gap={4}>
|
|
524
|
+
<Chip value="london" size="xs">London</Chip>
|
|
525
|
+
<Chip value="berlin" size="xs">Berlin</Chip>
|
|
526
|
+
</Group>
|
|
527
|
+
</Chip.Group>
|
|
528
|
+
);
|
|
365
529
|
}
|
|
366
530
|
|
|
367
531
|
col.accessor("location", {
|
|
368
|
-
|
|
369
|
-
|
|
532
|
+
header: "Location",
|
|
533
|
+
meta: {filter: {component: LocationFilter}},
|
|
370
534
|
});
|
|
371
535
|
```
|
|
372
536
|
|
|
@@ -375,18 +539,30 @@ col.accessor("location", {
|
|
|
375
539
|
`FilterControl` is exported so you can place individual filters anywhere in your layout:
|
|
376
540
|
|
|
377
541
|
```tsx
|
|
378
|
-
import {
|
|
542
|
+
import {FilterControl} from "@ethanhann/mantine-dataview";
|
|
379
543
|
|
|
380
544
|
<DataView view={view}>
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
545
|
+
{view.table.getColumn("inStock") && (
|
|
546
|
+
<FilterControl column={view.table.getColumn("inStock")!}/>
|
|
547
|
+
)}
|
|
548
|
+
<DataView.Toolbar/>
|
|
549
|
+
<DataView.Body/>
|
|
550
|
+
<DataView.Pagination/>
|
|
387
551
|
</DataView>
|
|
388
552
|
```
|
|
389
553
|
|
|
554
|
+
### Programmatic filter control
|
|
555
|
+
|
|
556
|
+
Reset all filters or clear a specific column from anywhere, no need to be inside the toolbar:
|
|
557
|
+
|
|
558
|
+
```tsx
|
|
559
|
+
// Reset all filters
|
|
560
|
+
<Button onClick={() => view.resetAllFilters()}>Reset all filters</Button>
|
|
561
|
+
|
|
562
|
+
// Clear a single column's filter
|
|
563
|
+
<Button onClick={() => view.resetFilter("status")}>Clear status filter</Button>
|
|
564
|
+
```
|
|
565
|
+
|
|
390
566
|
### Filter display behavior
|
|
391
567
|
|
|
392
568
|
- **Desktop, few filters** (at or below `filterInlineThreshold`, default 3): rendered inline in the toolbar.
|
|
@@ -394,12 +570,79 @@ import { FilterControl } from "@ethanhann/mantine-dataview";
|
|
|
394
570
|
- **Mobile** (below `sm` breakpoint): always collapsed into a bottom drawer.
|
|
395
571
|
- A "Reset filters" button appears automatically when any filter is active.
|
|
396
572
|
|
|
573
|
+
## Faceted search
|
|
574
|
+
|
|
575
|
+
When the server returns `facets` in the response, filter controls automatically adapt to show
|
|
576
|
+
dynamic counts, disable zero-result options, and render clickable range buckets.
|
|
577
|
+
|
|
578
|
+
### Server response with facets
|
|
579
|
+
|
|
580
|
+
```tsx
|
|
581
|
+
fetcher: async (request) => {
|
|
582
|
+
const res = await api.list(request);
|
|
583
|
+
return {
|
|
584
|
+
rows: res.items,
|
|
585
|
+
rowCount: res.total,
|
|
586
|
+
facets: {
|
|
587
|
+
size: {
|
|
588
|
+
type: "values",
|
|
589
|
+
values: [
|
|
590
|
+
{ value: "S", label: "Small", count: 12 },
|
|
591
|
+
{ value: "M", label: "Medium", count: 34 },
|
|
592
|
+
{ value: "L", label: "Large", count: 0 },
|
|
593
|
+
],
|
|
594
|
+
},
|
|
595
|
+
price: {
|
|
596
|
+
type: "ranges",
|
|
597
|
+
ranges: [
|
|
598
|
+
{ label: "Under $25", from: 0, to: 25, count: 15 },
|
|
599
|
+
{ label: "$25-$50", from: 25, to: 50, count: 28 },
|
|
600
|
+
{ label: "$50+", from: 50, to: 999, count: 7 },
|
|
601
|
+
],
|
|
602
|
+
min: 5,
|
|
603
|
+
max: 249,
|
|
604
|
+
},
|
|
605
|
+
},
|
|
606
|
+
};
|
|
607
|
+
};
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
### How controls adapt
|
|
611
|
+
|
|
612
|
+
| Filter type | Without facets | With value facets | With range facets |
|
|
613
|
+
|------------|---------------|-------------------|-------------------|
|
|
614
|
+
| Select | Static options | Options with counts, zero-count dimmed | - |
|
|
615
|
+
| Boolean | All / Yes / No | All / Yes (12) / No (3) | - |
|
|
616
|
+
| Number range | Slider or inputs | Slider (bounds from facet) | Clickable range buckets + slider |
|
|
617
|
+
| Date range | Date picker | Date picker | Clickable range buckets + picker |
|
|
618
|
+
|
|
619
|
+
Facets are optional and backward compatible. Facet data updates on every fetch, creating the
|
|
620
|
+
classic faceted search loop where filtering one dimension updates counts on all others.
|
|
621
|
+
|
|
622
|
+
### Facet types
|
|
623
|
+
|
|
624
|
+
```ts
|
|
625
|
+
// Discrete values - for select, multiselect, boolean filters
|
|
626
|
+
type ValueFacet = {
|
|
627
|
+
type: "values";
|
|
628
|
+
values: { value: string; label?: string; count: number }[];
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
// Bucketed ranges - for numberRange, dateRange filters
|
|
632
|
+
type RangeFacet = {
|
|
633
|
+
type: "ranges";
|
|
634
|
+
ranges: { label: string; from: number | string; to: number | string; count: number }[];
|
|
635
|
+
min?: number | string;
|
|
636
|
+
max?: number | string;
|
|
637
|
+
};
|
|
638
|
+
```
|
|
639
|
+
|
|
397
640
|
## Card composition
|
|
398
641
|
|
|
399
642
|
In card view, each visible column is placed by its `meta.card.role`:
|
|
400
643
|
|
|
401
644
|
| role | rendered as |
|
|
402
|
-
|
|
645
|
+
|------------|-----------------------------|
|
|
403
646
|
| `title` | card heading |
|
|
404
647
|
| `subtitle` | dimmed line under the title |
|
|
405
648
|
| `media` | full-bleed top section |
|
|
@@ -416,14 +659,14 @@ For full control over card content, use `renderCard`:
|
|
|
416
659
|
|
|
417
660
|
```tsx
|
|
418
661
|
<DataView
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
662
|
+
view={view}
|
|
663
|
+
renderCard={({data, selected, toggleSelected}) => (
|
|
664
|
+
<Card withBorder padding="md" onClick={toggleSelected}>
|
|
665
|
+
<Text fw={700}>{data.name}</Text>
|
|
666
|
+
<Text size="sm" c="dimmed">{data.email}</Text>
|
|
667
|
+
{selected && <Badge>Selected</Badge>}
|
|
668
|
+
</Card>
|
|
669
|
+
)}
|
|
427
670
|
/>
|
|
428
671
|
```
|
|
429
672
|
|
|
@@ -431,18 +674,18 @@ To keep the default composition but wrap it in a custom card shell, use the `Car
|
|
|
431
674
|
|
|
432
675
|
```tsx
|
|
433
676
|
<DataView
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
677
|
+
view={view}
|
|
678
|
+
slots={{
|
|
679
|
+
Card: ({data, selected, children}) => (
|
|
680
|
+
<Card
|
|
681
|
+
withBorder
|
|
682
|
+
padding="lg"
|
|
683
|
+
style={{background: selected ? "var(--mantine-color-blue-light)" : undefined}}
|
|
684
|
+
>
|
|
685
|
+
{children}
|
|
686
|
+
</Card>
|
|
687
|
+
),
|
|
688
|
+
}}
|
|
446
689
|
/>
|
|
447
690
|
```
|
|
448
691
|
|
|
@@ -452,21 +695,21 @@ Provide a `BulkActions` slot to add actions when rows are selected:
|
|
|
452
695
|
|
|
453
696
|
```tsx
|
|
454
697
|
<DataView
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
698
|
+
view={view}
|
|
699
|
+
slots={{
|
|
700
|
+
BulkActions: (selection) => (
|
|
701
|
+
<Button
|
|
702
|
+
color="red"
|
|
703
|
+
variant="light"
|
|
704
|
+
onClick={() => {
|
|
705
|
+
deleteUsers(selection.ids);
|
|
706
|
+
selection.clear();
|
|
707
|
+
}}
|
|
708
|
+
>
|
|
709
|
+
Delete {selection.count}
|
|
710
|
+
</Button>
|
|
711
|
+
),
|
|
712
|
+
}}
|
|
470
713
|
/>
|
|
471
714
|
```
|
|
472
715
|
|
|
@@ -479,22 +722,22 @@ Override loading, empty, and error states:
|
|
|
479
722
|
|
|
480
723
|
```tsx
|
|
481
724
|
<DataView
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
725
|
+
view={view}
|
|
726
|
+
slots={{
|
|
727
|
+
Empty: () => <Text>No users found.</Text>,
|
|
728
|
+
ErrorState: ({retry}) => (
|
|
729
|
+
<Stack align="center">
|
|
730
|
+
<Text c="red">Something went wrong.</Text>
|
|
731
|
+
<Button onClick={retry}>Retry</Button>
|
|
732
|
+
</Stack>
|
|
733
|
+
),
|
|
734
|
+
LoadingTable: () => <MySkeleton/>,
|
|
735
|
+
LoadingCards: () => <MyCardSkeleton/>,
|
|
736
|
+
}}
|
|
494
737
|
/>
|
|
495
738
|
```
|
|
496
739
|
|
|
497
|
-
A filtered-empty state is handled automatically
|
|
740
|
+
A filtered-empty state is handled automatically. It shows a "clear filters" action so
|
|
498
741
|
users can reset without manually removing each filter.
|
|
499
742
|
|
|
500
743
|
## URL state sync
|
|
@@ -502,14 +745,14 @@ users can reset without manually removing each filter.
|
|
|
502
745
|
Router-agnostic. The default adapter uses the History API; memoize it once:
|
|
503
746
|
|
|
504
747
|
```tsx
|
|
505
|
-
import {
|
|
748
|
+
import {windowHistoryAdapter} from "@ethanhann/mantine-dataview/url";
|
|
506
749
|
|
|
507
750
|
const adapter = useMemo(() => windowHistoryAdapter(), []);
|
|
508
751
|
const view = useDataViewFetcher<User>({
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
752
|
+
columns,
|
|
753
|
+
getRowId,
|
|
754
|
+
fetcher,
|
|
755
|
+
urlSync: {adapter},
|
|
513
756
|
});
|
|
514
757
|
```
|
|
515
758
|
|
|
@@ -523,12 +766,14 @@ To integrate with a router, implement these three methods:
|
|
|
523
766
|
|
|
524
767
|
```ts
|
|
525
768
|
interface UrlStateAdapter {
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
769
|
+
/** Current query params as a flat record. */
|
|
770
|
+
read(): Record<string, string>;
|
|
771
|
+
|
|
772
|
+
/** Write the next params; `replace` controls history entry vs push. */
|
|
773
|
+
write(next: Record<string, string>, opts?: { replace?: boolean }): void;
|
|
774
|
+
|
|
775
|
+
/** Optional: notify on external nav (back/forward). Returns an unsubscribe fn. */
|
|
776
|
+
subscribe?(onChange: () => void): () => void;
|
|
532
777
|
}
|
|
533
778
|
```
|
|
534
779
|
|
|
@@ -537,36 +782,36 @@ interface UrlStateAdapter {
|
|
|
537
782
|
### React Router
|
|
538
783
|
|
|
539
784
|
```tsx
|
|
540
|
-
import {
|
|
541
|
-
import type {
|
|
785
|
+
import {useSearchParams} from "react-router-dom";
|
|
786
|
+
import type {UrlStateAdapter} from "@ethanhann/mantine-dataview/url";
|
|
542
787
|
|
|
543
788
|
function useReactRouterAdapter(): UrlStateAdapter {
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
789
|
+
const [searchParams, setSearchParams] = useSearchParams();
|
|
790
|
+
return useMemo<UrlStateAdapter>(
|
|
791
|
+
() => ({
|
|
792
|
+
read: () => Object.fromEntries(new URLSearchParams(window.location.search)),
|
|
793
|
+
write: (next, opts) => setSearchParams(next, {replace: opts?.replace}),
|
|
794
|
+
}),
|
|
795
|
+
[searchParams, setSearchParams],
|
|
796
|
+
);
|
|
552
797
|
}
|
|
553
798
|
```
|
|
554
799
|
|
|
555
800
|
### TanStack Router
|
|
556
801
|
|
|
557
802
|
```tsx
|
|
558
|
-
import {
|
|
559
|
-
import type {
|
|
803
|
+
import {useNavigate} from "@tanstack/react-router";
|
|
804
|
+
import type {UrlStateAdapter} from "@ethanhann/mantine-dataview/url";
|
|
560
805
|
|
|
561
806
|
function useTanStackRouterAdapter(): UrlStateAdapter {
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
807
|
+
const navigate = useNavigate();
|
|
808
|
+
return useMemo<UrlStateAdapter>(
|
|
809
|
+
() => ({
|
|
810
|
+
read: () => Object.fromEntries(new URLSearchParams(window.location.search)),
|
|
811
|
+
write: (next, opts) => navigate({search: () => next, replace: opts?.replace}),
|
|
812
|
+
}),
|
|
813
|
+
[navigate],
|
|
814
|
+
);
|
|
570
815
|
}
|
|
571
816
|
```
|
|
572
817
|
|
|
@@ -582,32 +827,66 @@ Force the card view below a breakpoint:
|
|
|
582
827
|
|
|
583
828
|
```tsx
|
|
584
829
|
const view = useDataViewFetcher<User>({
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
830
|
+
columns,
|
|
831
|
+
getRowId,
|
|
832
|
+
fetcher,
|
|
833
|
+
responsive: {forceCardsBelow: "sm", lockSwitcherOnMobile: true},
|
|
589
834
|
});
|
|
590
835
|
|
|
591
|
-
<DataView view={view} lockSwitcherOnMobile
|
|
836
|
+
<DataView view={view} lockSwitcherOnMobile/>;
|
|
592
837
|
```
|
|
593
838
|
|
|
594
839
|
When `forceCardsBelow` is set and the viewport is below that breakpoint:
|
|
840
|
+
|
|
595
841
|
- The view is forced to cards regardless of the user's choice.
|
|
596
842
|
- The user's explicit choice is preserved and restored above the breakpoint.
|
|
597
843
|
- The view switcher is disabled (or hidden entirely with `lockSwitcherOnMobile`).
|
|
598
844
|
- Filters always open in a bottom drawer on mobile.
|
|
599
845
|
|
|
846
|
+
## Loading behavior
|
|
847
|
+
|
|
848
|
+
By default, filter controls, sort controls, and column visibility/pinning menus are disabled
|
|
849
|
+
while data is loading. Sort headers in the table also become non-interactive, with a dimmed
|
|
850
|
+
appearance showing the current sort state. The search input stays enabled so users can keep
|
|
851
|
+
typing during debounced search.
|
|
852
|
+
|
|
853
|
+
Opt out per component:
|
|
854
|
+
|
|
855
|
+
```tsx
|
|
856
|
+
<DataTable view={view} disableWhileLoading={false} />
|
|
857
|
+
<DataToolbar view={view} disableWhileLoading={false} />
|
|
858
|
+
```
|
|
859
|
+
|
|
860
|
+
### Animated row transitions
|
|
861
|
+
|
|
862
|
+
Instead of skeleton loading, rows can animate in and out with CSS transitions. New rows
|
|
863
|
+
fade and slide in, removed rows fade out, and unchanged rows stay in place:
|
|
864
|
+
|
|
865
|
+
```tsx
|
|
866
|
+
<DataView view={view} animateRows />
|
|
867
|
+
```
|
|
868
|
+
|
|
869
|
+
When `animateRows` is enabled:
|
|
870
|
+
- Previous rows stay visible while new data loads (no skeleton flash).
|
|
871
|
+
- New rows enter with a slide-down fade-in animation (200ms).
|
|
872
|
+
- Removed rows fade out (150ms) before being removed from the DOM.
|
|
873
|
+
- Works in both table and card views.
|
|
874
|
+
|
|
875
|
+
This is opt-in. The default behavior (skeleton loading) is unchanged.
|
|
876
|
+
|
|
600
877
|
## API overview
|
|
601
878
|
|
|
602
|
-
| Export
|
|
603
|
-
|
|
604
|
-
| `useDataView`
|
|
605
|
-
| `useDataViewFetcher`
|
|
879
|
+
| Export | Purpose |
|
|
880
|
+
|----------------------------------------------------------------------|-----------------------------------------------|
|
|
881
|
+
| `useDataView` | Headless core, owns all feature state |
|
|
882
|
+
| `useDataViewFetcher` | Convenience wrapper that manages the fetch |
|
|
606
883
|
| `DataView` (+ `.Toolbar` / `.BulkActions` / `.Body` / `.Pagination`) | Orchestrator + compound parts |
|
|
607
|
-
| `DataTable`, `DataCards`
|
|
884
|
+
| `DataTable`, `DataCards` | The two presentations (usable standalone) |
|
|
608
885
|
| `DataToolbar`, `DataPagination`, `DataBulkActions` | Standalone affordances |
|
|
609
|
-
| `FilterControl`
|
|
610
|
-
| `
|
|
886
|
+
| `FilterControl` | Individual filter control (place anywhere) |
|
|
887
|
+
| `ViewSwitcher` | Table/Cards toggle (customizable labels) |
|
|
888
|
+
| `exportCsv` | Standalone CSV export utility |
|
|
889
|
+
| `col` | Fluent column builder factory |
|
|
611
890
|
| `createColumnHelper`, `composeCardLayout`, `resolveColumnLabel` | Column helpers |
|
|
612
891
|
| `@ethanhann/mantine-dataview/url` | `windowHistoryAdapter` + serializer utilities |
|
|
613
892
|
|
|
@@ -615,15 +894,33 @@ When `forceCardsBelow` is set and the viewport is below that breakpoint:
|
|
|
615
894
|
|
|
616
895
|
Passed via the `slots` prop on `DataView` or the presentation components:
|
|
617
896
|
|
|
618
|
-
| Slot | Receives
|
|
619
|
-
|
|
620
|
-
| `Empty` | `{ view }`
|
|
621
|
-
| `ErrorState` | `{ retry }`
|
|
622
|
-
| `LoadingTable` | —
|
|
623
|
-
| `LoadingCards`
|
|
624
|
-
| `Row` | `{ row, children }`
|
|
625
|
-
| `Card` | `{ row, data, selected, children }`
|
|
626
|
-
| `BulkActions` | `{ count, ids, rows, clear }`
|
|
897
|
+
| Slot | Receives | Purpose |
|
|
898
|
+
|----------------|-------------------------------------|----------------------------|
|
|
899
|
+
| `Empty` | `{ view }` | No data state |
|
|
900
|
+
| `ErrorState` | `{ retry }` | Error with retry action |
|
|
901
|
+
| `LoadingTable` | — | Table skeleton replacement |
|
|
902
|
+
| `LoadingCards` | — | Card skeleton replacement |
|
|
903
|
+
| `Row` | `{ row, children }` | Wrap each table row |
|
|
904
|
+
| `Card` | `{ row, data, selected, children }` | Wrap each card |
|
|
905
|
+
| `BulkActions` | `{ count, ids, rows, clear }` | Bulk action bar content |
|
|
906
|
+
|
|
907
|
+
## Known issues
|
|
908
|
+
|
|
909
|
+
### `DataView` name shadows the JS global
|
|
910
|
+
|
|
911
|
+
The `DataView` component shares its name with the JavaScript `DataView` global (typed arrays).
|
|
912
|
+
Linters like Biome's `noShadowRestrictedNames` will flag the import. Suppress it with:
|
|
913
|
+
|
|
914
|
+
```tsx
|
|
915
|
+
// biome-ignore lint/suspicious/noShadowRestrictedNames: component name
|
|
916
|
+
import {DataView} from "@ethanhann/mantine-dataview";
|
|
917
|
+
```
|
|
918
|
+
|
|
919
|
+
Or import with an alias:
|
|
920
|
+
|
|
921
|
+
```tsx
|
|
922
|
+
import {DataView as MantineDataView} from "@ethanhann/mantine-dataview";
|
|
923
|
+
```
|
|
627
924
|
|
|
628
925
|
## Development
|
|
629
926
|
|