tablero 1.0.1 → 1.0.2
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 +736 -57
- package/dist/core.d.mts +1 -447
- package/dist/core.d.ts +1 -447
- package/dist/core.js +37 -10
- package/dist/core.js.map +1 -1
- package/dist/core.mjs +37 -10
- package/dist/core.mjs.map +1 -1
- package/dist/index-CBkEItSc.d.mts +503 -0
- package/dist/index-CBkEItSc.d.ts +503 -0
- package/dist/react.d.mts +266 -4
- package/dist/react.d.ts +266 -4
- package/dist/react.js +1026 -101
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +1023 -103
- package/dist/react.mjs.map +1 -1
- package/dist/ui.d.mts +23 -3
- package/dist/ui.d.ts +23 -3
- package/dist/ui.js +155 -64
- package/dist/ui.js.map +1 -1
- package/dist/ui.mjs +155 -64
- package/dist/ui.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,12 +9,18 @@ A type-safe, framework-agnostic data table library with React bindings. Built wi
|
|
|
9
9
|
- ⚛️ **React hooks** - `useDataTable` hook for easy React integration
|
|
10
10
|
- 🎨 **Customizable UI** - Flexible, CSS-variable based styling
|
|
11
11
|
- 📊 **Sorting** - Single-column sorting (extensible to multi-column)
|
|
12
|
-
- 📄 **Pagination** - Client-side
|
|
12
|
+
- 📄 **Pagination** - Client-side and server-side pagination support
|
|
13
13
|
- 🔍 **Filtering** - Global and column-specific text filtering
|
|
14
|
+
- ✅ **Row Selection** - Single and multi-select with select all support
|
|
15
|
+
- 🌐 **URL Sync** - Synchronize table state with URL search params
|
|
16
|
+
- 🖥️ **Server-side Mode** - Delegate sorting, filtering, and pagination to server
|
|
14
17
|
- 📌 **Sticky headers & columns** - Keep headers and first column visible while scrolling
|
|
15
18
|
- 🔧 **Column resizing** - Resize columns with pointer-based interaction
|
|
19
|
+
- 👁️ **Column visibility** - Show/hide columns dynamically
|
|
20
|
+
- 🔀 **Column reordering** - Reorder columns programmatically
|
|
16
21
|
- 🎭 **Custom renderers** - Customize cell, header, and row rendering
|
|
17
22
|
- ♿ **Accessible** - ARIA attributes and keyboard support
|
|
23
|
+
- 🎛️ **Controlled/Uncontrolled** - Flexible state management patterns
|
|
18
24
|
|
|
19
25
|
## Installation
|
|
20
26
|
|
|
@@ -28,13 +34,12 @@ yarn add tablero
|
|
|
28
34
|
|
|
29
35
|
## Quick Start
|
|
30
36
|
|
|
31
|
-
### React Example
|
|
37
|
+
### Basic React Example
|
|
32
38
|
|
|
33
39
|
```tsx
|
|
34
40
|
import { useDataTable } from "tablero/react";
|
|
35
41
|
import { DataTable } from "tablero/ui";
|
|
36
42
|
import { defineColumns, col } from "tablero/core";
|
|
37
|
-
import "tablero/dist/ui.css"; // Optional: import default styles
|
|
38
43
|
|
|
39
44
|
interface User {
|
|
40
45
|
id: number;
|
|
@@ -56,99 +61,723 @@ function MyTable() {
|
|
|
56
61
|
pageSize: 10,
|
|
57
62
|
});
|
|
58
63
|
|
|
59
|
-
return
|
|
64
|
+
return (
|
|
65
|
+
<DataTable
|
|
66
|
+
table={table}
|
|
67
|
+
stickyHeader
|
|
68
|
+
bordered
|
|
69
|
+
maxHeight={400}
|
|
70
|
+
getRowKey={(row) => row.id}
|
|
71
|
+
/>
|
|
72
|
+
);
|
|
60
73
|
}
|
|
61
74
|
```
|
|
62
75
|
|
|
63
|
-
|
|
76
|
+
## Packages
|
|
64
77
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
78
|
+
This library is organized into three packages:
|
|
79
|
+
|
|
80
|
+
- **`tablero/core`** - Framework-agnostic core logic (state management, sorting, filtering, pagination)
|
|
81
|
+
- **`tablero/react`** - React hooks (`useDataTable`) and URL sync utilities
|
|
82
|
+
- **`tablero/ui`** - React UI components (`DataTable`, `TableHeader`, `TableBody`, `TableCell`)
|
|
83
|
+
|
|
84
|
+
## Column Definitions
|
|
85
|
+
|
|
86
|
+
### Basic Column Definition
|
|
87
|
+
|
|
88
|
+
```tsx
|
|
89
|
+
import { defineColumns, col } from "tablero/core";
|
|
75
90
|
|
|
76
91
|
const columns = defineColumns<User>()([
|
|
77
|
-
col("name", {
|
|
78
|
-
|
|
92
|
+
col("name", {
|
|
93
|
+
header: "Name",
|
|
94
|
+
sortable: true,
|
|
95
|
+
filter: "text",
|
|
96
|
+
width: 200,
|
|
97
|
+
align: "left", // "left" | "center" | "right"
|
|
98
|
+
}),
|
|
79
99
|
]);
|
|
100
|
+
```
|
|
80
101
|
|
|
81
|
-
|
|
82
|
-
const state = createInitialTableState(columns.map((c) => c.id));
|
|
102
|
+
### Column Options
|
|
83
103
|
|
|
84
|
-
|
|
85
|
-
|
|
104
|
+
- `header` - Column header text
|
|
105
|
+
- `sortable` - Enable sorting for this column
|
|
106
|
+
- `filter` - Filter type: `"text"` | `"none"` (default: `"none"`)
|
|
107
|
+
- `width` - Column width in pixels
|
|
108
|
+
- `minWidth` - Minimum column width
|
|
109
|
+
- `maxWidth` - Maximum column width
|
|
110
|
+
- `align` - Text alignment: `"left"` | `"center"` | `"right"`
|
|
86
111
|
|
|
87
|
-
|
|
88
|
-
const sorted = applySort(filtered, state.sorting, getValue);
|
|
112
|
+
### Custom Accessor
|
|
89
113
|
|
|
90
|
-
|
|
91
|
-
|
|
114
|
+
```tsx
|
|
115
|
+
import { colWithAccessor } from "tablero/core";
|
|
116
|
+
|
|
117
|
+
colWithAccessor("fullName", (user) => `${user.firstName} ${user.lastName}`, {
|
|
118
|
+
header: "Full Name",
|
|
119
|
+
sortable: true,
|
|
120
|
+
});
|
|
92
121
|
```
|
|
93
122
|
|
|
94
|
-
##
|
|
123
|
+
## Sorting
|
|
95
124
|
|
|
96
|
-
|
|
125
|
+
### Basic Sorting
|
|
97
126
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
127
|
+
```tsx
|
|
128
|
+
const table = useDataTable({
|
|
129
|
+
data: users,
|
|
130
|
+
columns,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// Access sort state
|
|
134
|
+
table.sorting.state; // { columnId: string | null, direction: "asc" | "desc" | null }
|
|
135
|
+
|
|
136
|
+
// Sort handlers
|
|
137
|
+
table.sorting.toggle("name"); // Toggle sort for column
|
|
138
|
+
table.sorting.set("name", "asc"); // Set explicit sort
|
|
139
|
+
table.sorting.clear(); // Clear sorting
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Initial Sort State
|
|
143
|
+
|
|
144
|
+
```tsx
|
|
145
|
+
const table = useDataTable({
|
|
146
|
+
data: users,
|
|
147
|
+
columns,
|
|
148
|
+
state: {
|
|
149
|
+
sorting: { columnId: "name", direction: "asc" },
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Filtering
|
|
155
|
+
|
|
156
|
+
### Global Filter
|
|
157
|
+
|
|
158
|
+
```tsx
|
|
159
|
+
const table = useDataTable({
|
|
160
|
+
data: users,
|
|
161
|
+
columns,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Set global filter
|
|
165
|
+
table.filtering.setGlobalFilter("search term");
|
|
166
|
+
|
|
167
|
+
// Access filter state
|
|
168
|
+
table.filtering.state.globalFilter; // string
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Column Filters
|
|
172
|
+
|
|
173
|
+
```tsx
|
|
174
|
+
// Set column-specific filter
|
|
175
|
+
table.filtering.setColumnFilter("email", "example.com");
|
|
176
|
+
|
|
177
|
+
// Clear column filter
|
|
178
|
+
table.filtering.clearColumnFilter("email");
|
|
179
|
+
|
|
180
|
+
// Clear all filters
|
|
181
|
+
table.filtering.clearAllFilters();
|
|
182
|
+
|
|
183
|
+
// Access filter state
|
|
184
|
+
table.filtering.state.columnFilters; // Record<string, string>
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Filtering Example
|
|
188
|
+
|
|
189
|
+
```tsx
|
|
190
|
+
function FilteredTable() {
|
|
191
|
+
const table = useDataTable({
|
|
192
|
+
data: users,
|
|
193
|
+
columns,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
return (
|
|
197
|
+
<div>
|
|
198
|
+
<input
|
|
199
|
+
type="text"
|
|
200
|
+
value={table.filtering.state.globalFilter}
|
|
201
|
+
onChange={(e) => table.filtering.setGlobalFilter(e.target.value)}
|
|
202
|
+
placeholder="Search all columns..."
|
|
203
|
+
/>
|
|
204
|
+
<DataTable table={table} />
|
|
205
|
+
</div>
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Pagination
|
|
211
|
+
|
|
212
|
+
### Basic Pagination
|
|
213
|
+
|
|
214
|
+
```tsx
|
|
215
|
+
const table = useDataTable({
|
|
216
|
+
data: users,
|
|
217
|
+
columns,
|
|
218
|
+
pageSize: 10, // Default: 10
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// Access pagination state
|
|
222
|
+
table.pageIndex; // Current page (0-based)
|
|
223
|
+
table.pageSize; // Items per page
|
|
224
|
+
table.pageCount; // Total pages
|
|
225
|
+
table.hasNextPage; // boolean
|
|
226
|
+
table.hasPreviousPage; // boolean
|
|
227
|
+
|
|
228
|
+
// Pagination handlers
|
|
229
|
+
table.pagination.nextPage();
|
|
230
|
+
table.pagination.previousPage();
|
|
231
|
+
table.pagination.goToPage(2);
|
|
232
|
+
table.pagination.setPageSize(20);
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Pagination Controls Example
|
|
236
|
+
|
|
237
|
+
```tsx
|
|
238
|
+
<div>
|
|
239
|
+
<button
|
|
240
|
+
onClick={() => table.pagination.previousPage()}
|
|
241
|
+
disabled={!table.hasPreviousPage}
|
|
242
|
+
>
|
|
243
|
+
Previous
|
|
244
|
+
</button>
|
|
245
|
+
<span>
|
|
246
|
+
Page {table.pageIndex + 1} of {table.pageCount}
|
|
247
|
+
</span>
|
|
248
|
+
<button
|
|
249
|
+
onClick={() => table.pagination.nextPage()}
|
|
250
|
+
disabled={!table.hasNextPage}
|
|
251
|
+
>
|
|
252
|
+
Next
|
|
253
|
+
</button>
|
|
254
|
+
<select
|
|
255
|
+
value={table.pageSize}
|
|
256
|
+
onChange={(e) => table.pagination.setPageSize(Number(e.target.value))}
|
|
257
|
+
>
|
|
258
|
+
<option value={10}>10 per page</option>
|
|
259
|
+
<option value={20}>20 per page</option>
|
|
260
|
+
<option value={50}>50 per page</option>
|
|
261
|
+
</select>
|
|
262
|
+
</div>
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
## Row Selection
|
|
266
|
+
|
|
267
|
+
### Basic Selection
|
|
101
268
|
|
|
102
|
-
|
|
269
|
+
```tsx
|
|
270
|
+
const table = useDataTable({
|
|
271
|
+
data: users,
|
|
272
|
+
columns,
|
|
273
|
+
getRowKey: (row) => row.id, // Required for selection
|
|
274
|
+
selection: {
|
|
275
|
+
enabled: true,
|
|
276
|
+
mode: "multi", // or "single"
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
// Access selection state
|
|
281
|
+
table.selection.selectedRowIds; // Set<string | number>
|
|
282
|
+
table.selection.selectedCount; // number
|
|
283
|
+
table.selection.isAllSelected; // boolean (all rows on current page)
|
|
284
|
+
table.selection.isIndeterminate; // boolean (some rows selected)
|
|
285
|
+
|
|
286
|
+
// Selection handlers
|
|
287
|
+
table.selection.select(rowId);
|
|
288
|
+
table.selection.deselect(rowId);
|
|
289
|
+
table.selection.toggle(rowId);
|
|
290
|
+
table.selection.selectAll(); // Select all rows on current page
|
|
291
|
+
table.selection.deselectAll(); // Deselect all rows on current page
|
|
292
|
+
table.selection.clear(); // Clear all selections
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
### Selection Example
|
|
296
|
+
|
|
297
|
+
```tsx
|
|
298
|
+
function SelectableTable() {
|
|
299
|
+
const table = useDataTable({
|
|
300
|
+
data: users,
|
|
301
|
+
columns,
|
|
302
|
+
getRowKey: (row) => row.id,
|
|
303
|
+
selection: {
|
|
304
|
+
enabled: true,
|
|
305
|
+
mode: "multi",
|
|
306
|
+
},
|
|
307
|
+
});
|
|
103
308
|
|
|
104
|
-
|
|
309
|
+
return (
|
|
310
|
+
<div>
|
|
311
|
+
<p>Selected: {table.selection.selectedCount} rows</p>
|
|
312
|
+
<DataTable table={table} />
|
|
313
|
+
{table.selection.selectedCount > 0 && (
|
|
314
|
+
<button onClick={() => table.selection.clear()}>Clear Selection</button>
|
|
315
|
+
)}
|
|
316
|
+
</div>
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
```
|
|
105
320
|
|
|
106
|
-
|
|
321
|
+
## Server-Side Mode
|
|
107
322
|
|
|
108
|
-
|
|
323
|
+
When using server-side data fetching, disable client-side transformations:
|
|
109
324
|
|
|
110
325
|
```tsx
|
|
111
326
|
const table = useDataTable({
|
|
112
|
-
data:
|
|
113
|
-
columns
|
|
114
|
-
|
|
115
|
-
|
|
327
|
+
data: apiResponse, // Data already filtered/sorted/paginated by server
|
|
328
|
+
columns,
|
|
329
|
+
serverMode: {
|
|
330
|
+
pagination: true, // Server handles pagination
|
|
331
|
+
sorting: true, // Server handles sorting
|
|
332
|
+
filtering: true, // Server handles filtering
|
|
333
|
+
},
|
|
116
334
|
});
|
|
117
335
|
```
|
|
118
336
|
|
|
119
|
-
###
|
|
337
|
+
### Server-Side with Controlled State
|
|
338
|
+
|
|
339
|
+
```tsx
|
|
340
|
+
function ServerSideTable() {
|
|
341
|
+
const [tableState, setTableState] = useState({
|
|
342
|
+
pagination: { pageIndex: 0, pageSize: 10 },
|
|
343
|
+
sorting: { columnId: null, direction: null },
|
|
344
|
+
filtering: { globalFilter: "", columnFilters: {} },
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
const table = useDataTable({
|
|
348
|
+
data: apiData,
|
|
349
|
+
columns,
|
|
350
|
+
serverMode: {
|
|
351
|
+
pagination: true,
|
|
352
|
+
sorting: true,
|
|
353
|
+
filtering: true,
|
|
354
|
+
},
|
|
355
|
+
state: {
|
|
356
|
+
pagination: tableState.pagination,
|
|
357
|
+
sorting: tableState.sorting,
|
|
358
|
+
filtering: tableState.filtering,
|
|
359
|
+
onStateChange: (updates) => {
|
|
360
|
+
setTableState((prev) => ({ ...prev, ...updates }));
|
|
361
|
+
// Fetch new data from API with updated state
|
|
362
|
+
fetchData(updates);
|
|
363
|
+
},
|
|
364
|
+
},
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
return <DataTable table={table} />;
|
|
368
|
+
}
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
## URL Synchronization
|
|
372
|
+
|
|
373
|
+
### Basic URL Sync (Browser History API)
|
|
374
|
+
|
|
375
|
+
```tsx
|
|
376
|
+
const table = useDataTable({
|
|
377
|
+
data: users,
|
|
378
|
+
columns,
|
|
379
|
+
urlSync: {
|
|
380
|
+
enabled: true,
|
|
381
|
+
features: {
|
|
382
|
+
pagination: true,
|
|
383
|
+
sorting: true,
|
|
384
|
+
filtering: true,
|
|
385
|
+
},
|
|
386
|
+
debounceMs: 300, // Optional: debounce URL updates
|
|
387
|
+
},
|
|
388
|
+
});
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
### Next.js App Router
|
|
392
|
+
|
|
393
|
+
```tsx
|
|
394
|
+
import { useSearchParams, useRouter, usePathname } from "next/navigation";
|
|
395
|
+
import { useDataTable, createNextAppRouterAdapter } from "tablero/react";
|
|
396
|
+
|
|
397
|
+
function NextJsTable() {
|
|
398
|
+
const searchParams = useSearchParams();
|
|
399
|
+
const router = useRouter();
|
|
400
|
+
const pathname = usePathname();
|
|
401
|
+
|
|
402
|
+
const table = useDataTable({
|
|
403
|
+
data: users,
|
|
404
|
+
columns,
|
|
405
|
+
urlSync: {
|
|
406
|
+
enabled: true,
|
|
407
|
+
routerAdapter: createNextAppRouterAdapter(searchParams, router, pathname),
|
|
408
|
+
paramNames: {
|
|
409
|
+
page: "p",
|
|
410
|
+
sortColumn: "sort",
|
|
411
|
+
sortDir: "dir",
|
|
412
|
+
globalFilter: "q",
|
|
413
|
+
},
|
|
414
|
+
},
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
return <DataTable table={table} />;
|
|
418
|
+
}
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
### Custom Parameter Names
|
|
422
|
+
|
|
423
|
+
```tsx
|
|
424
|
+
urlSync: {
|
|
425
|
+
enabled: true,
|
|
426
|
+
paramNames: {
|
|
427
|
+
page: "page",
|
|
428
|
+
pageSize: "size",
|
|
429
|
+
sortColumn: "sort",
|
|
430
|
+
sortDir: "direction",
|
|
431
|
+
globalFilter: "search",
|
|
432
|
+
columnFilterPrefix: "filter_",
|
|
433
|
+
},
|
|
434
|
+
}
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
### URL Format
|
|
438
|
+
|
|
439
|
+
The URL will look like:
|
|
440
|
+
|
|
441
|
+
```
|
|
442
|
+
?page=1&pageSize=10&sort=name&sortDir=asc&q=search&filter_email=example.com
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
## Column Management
|
|
446
|
+
|
|
447
|
+
### Column Visibility
|
|
448
|
+
|
|
449
|
+
```tsx
|
|
450
|
+
// Toggle column visibility
|
|
451
|
+
table.columnManagement.toggleVisibility("email");
|
|
452
|
+
|
|
453
|
+
// Set column visibility
|
|
454
|
+
table.columnManagement.setVisibility("email", false);
|
|
455
|
+
|
|
456
|
+
// Access visibility state
|
|
457
|
+
table.state.columnVisibility; // Record<string, boolean>
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
### Column Reordering
|
|
461
|
+
|
|
462
|
+
```tsx
|
|
463
|
+
// Reorder columns
|
|
464
|
+
table.columnManagement.reorder(["name", "email", "age"]);
|
|
465
|
+
|
|
466
|
+
// Access column order
|
|
467
|
+
table.state.columnOrder; // string[]
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
## Custom Renderers
|
|
471
|
+
|
|
472
|
+
### Custom Cell Renderer
|
|
473
|
+
|
|
474
|
+
```tsx
|
|
475
|
+
<DataTable
|
|
476
|
+
table={table}
|
|
477
|
+
renderCell={(value, row, column) => {
|
|
478
|
+
if (column.id === "active") {
|
|
479
|
+
return <span>{value ? "✓" : "✗"}</span>;
|
|
480
|
+
}
|
|
481
|
+
return <span>{value}</span>;
|
|
482
|
+
}}
|
|
483
|
+
/>
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
### Custom Header Renderer
|
|
487
|
+
|
|
488
|
+
```tsx
|
|
489
|
+
<DataTable
|
|
490
|
+
table={table}
|
|
491
|
+
renderHeader={(column, sortState) => {
|
|
492
|
+
return (
|
|
493
|
+
<div>
|
|
494
|
+
{column.header}
|
|
495
|
+
{sortState.columnId === column.id && (
|
|
496
|
+
<span>{sortState.direction === "asc" ? "↑" : "↓"}</span>
|
|
497
|
+
)}
|
|
498
|
+
</div>
|
|
499
|
+
);
|
|
500
|
+
}}
|
|
501
|
+
/>
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
### Custom Row Renderer
|
|
120
505
|
|
|
121
506
|
```tsx
|
|
122
507
|
<DataTable
|
|
123
508
|
table={table}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
509
|
+
renderRow={(row, index, cells) => {
|
|
510
|
+
return <tr className={row.active ? "active-row" : ""}>{cells}</tr>;
|
|
511
|
+
}}
|
|
512
|
+
/>
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
## UI Features
|
|
516
|
+
|
|
517
|
+
### Sticky Header
|
|
518
|
+
|
|
519
|
+
```tsx
|
|
520
|
+
<DataTable table={table} stickyHeader maxHeight={400} />
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
### Sticky First Column
|
|
524
|
+
|
|
525
|
+
```tsx
|
|
526
|
+
<DataTable table={table} stickyFirstColumn />
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
### Column Resizing
|
|
530
|
+
|
|
531
|
+
```tsx
|
|
532
|
+
<DataTable table={table} enableResizing />
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
### Borders
|
|
536
|
+
|
|
537
|
+
```tsx
|
|
538
|
+
<DataTable
|
|
539
|
+
table={table}
|
|
540
|
+
bordered // Default: true
|
|
541
|
+
/>
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
### Loading and Error States
|
|
545
|
+
|
|
546
|
+
```tsx
|
|
547
|
+
<DataTable
|
|
548
|
+
table={table}
|
|
549
|
+
isLoading={loading}
|
|
550
|
+
error={error}
|
|
129
551
|
slots={{
|
|
130
|
-
loader
|
|
131
|
-
empty
|
|
132
|
-
error
|
|
552
|
+
loader: () => <div>Loading...</div>,
|
|
553
|
+
empty: ({ columns }) => <div>No data available</div>,
|
|
554
|
+
error: ({ error }) => <div>Error: {error}</div>,
|
|
133
555
|
}}
|
|
134
|
-
renderCell?: (value, row, column) => React.ReactNode
|
|
135
|
-
renderHeader?: (column, sortState) => React.ReactNode
|
|
136
|
-
renderRow?: (row, index, cells) => React.ReactNode
|
|
137
556
|
/>
|
|
138
557
|
```
|
|
139
558
|
|
|
559
|
+
## State Management
|
|
560
|
+
|
|
561
|
+
### Uncontrolled (Default)
|
|
562
|
+
|
|
563
|
+
```tsx
|
|
564
|
+
const table = useDataTable({
|
|
565
|
+
data: users,
|
|
566
|
+
columns,
|
|
567
|
+
});
|
|
568
|
+
// All state managed internally
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
### Fully Controlled
|
|
572
|
+
|
|
573
|
+
```tsx
|
|
574
|
+
const [tableState, setTableState] = useState(
|
|
575
|
+
createInitialTableState(columnIds)
|
|
576
|
+
);
|
|
577
|
+
|
|
578
|
+
const table = useDataTable({
|
|
579
|
+
data: users,
|
|
580
|
+
columns,
|
|
581
|
+
state: {
|
|
582
|
+
state: tableState,
|
|
583
|
+
setState: setTableState,
|
|
584
|
+
},
|
|
585
|
+
});
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
### Per-State Control
|
|
589
|
+
|
|
590
|
+
```tsx
|
|
591
|
+
const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 10 });
|
|
592
|
+
const [sorting, setSorting] = useState({ columnId: null, direction: null });
|
|
593
|
+
|
|
594
|
+
const table = useDataTable({
|
|
595
|
+
data: users,
|
|
596
|
+
columns,
|
|
597
|
+
state: {
|
|
598
|
+
pagination,
|
|
599
|
+
sorting,
|
|
600
|
+
onPaginationChange: setPagination,
|
|
601
|
+
onSortingChange: setSorting,
|
|
602
|
+
},
|
|
603
|
+
});
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
## API Reference
|
|
607
|
+
|
|
608
|
+
### `useDataTable` Hook
|
|
609
|
+
|
|
610
|
+
```tsx
|
|
611
|
+
interface UseDataTableOptions<TData> {
|
|
612
|
+
data: TData[];
|
|
613
|
+
columns: readonly ColumnDef<TData>[];
|
|
614
|
+
pageSize?: number;
|
|
615
|
+
state?: TableStateHandler | UncontrolledTableState | PerStateControl;
|
|
616
|
+
serverMode?: {
|
|
617
|
+
pagination?: boolean;
|
|
618
|
+
sorting?: boolean;
|
|
619
|
+
filtering?: boolean;
|
|
620
|
+
};
|
|
621
|
+
selection?: {
|
|
622
|
+
enabled?: boolean;
|
|
623
|
+
mode?: "single" | "multi";
|
|
624
|
+
initialSelectedRowIds?: (string | number)[];
|
|
625
|
+
};
|
|
626
|
+
getRowKey?: (row: TData, index: number) => string | number;
|
|
627
|
+
urlSync?: {
|
|
628
|
+
enabled?: boolean;
|
|
629
|
+
paramNames?: UrlParamNames;
|
|
630
|
+
debounceMs?: number;
|
|
631
|
+
features?: {
|
|
632
|
+
pagination?: boolean;
|
|
633
|
+
sorting?: boolean;
|
|
634
|
+
filtering?: boolean;
|
|
635
|
+
};
|
|
636
|
+
routerAdapter?: RouterAdapter;
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
### `DataTable` Component
|
|
642
|
+
|
|
643
|
+
```tsx
|
|
644
|
+
interface DataTableProps<TData> {
|
|
645
|
+
table: TableInstance<TData>;
|
|
646
|
+
slots?: {
|
|
647
|
+
loader?: React.ComponentType;
|
|
648
|
+
empty?: React.ComponentType<{ columns: Column<TData>[] }>;
|
|
649
|
+
error?: React.ComponentType<{ error: Error | string }>;
|
|
650
|
+
};
|
|
651
|
+
renderCell?: (
|
|
652
|
+
value: unknown,
|
|
653
|
+
row: TData,
|
|
654
|
+
column: Column<TData>
|
|
655
|
+
) => React.ReactNode;
|
|
656
|
+
renderHeader?: (
|
|
657
|
+
column: Column<TData>,
|
|
658
|
+
sortState: SortState
|
|
659
|
+
) => React.ReactNode;
|
|
660
|
+
renderRow?: (
|
|
661
|
+
row: TData,
|
|
662
|
+
index: number,
|
|
663
|
+
cells: React.ReactNode[]
|
|
664
|
+
) => React.ReactNode;
|
|
665
|
+
getRowKey?: (row: TData, index: number) => string | number;
|
|
666
|
+
stickyHeader?: boolean;
|
|
667
|
+
stickyFirstColumn?: boolean;
|
|
668
|
+
enableResizing?: boolean;
|
|
669
|
+
maxHeight?: number | string;
|
|
670
|
+
bordered?: boolean;
|
|
671
|
+
className?: string;
|
|
672
|
+
isLoading?: boolean;
|
|
673
|
+
error?: Error | string | null;
|
|
674
|
+
}
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
### `TableInstance` API
|
|
678
|
+
|
|
679
|
+
```tsx
|
|
680
|
+
interface TableInstance<TData> {
|
|
681
|
+
// State
|
|
682
|
+
state: TableState;
|
|
683
|
+
columns: Column<TData>[];
|
|
684
|
+
visibleColumns: Column<TData>[];
|
|
685
|
+
|
|
686
|
+
// Data
|
|
687
|
+
data: TData[];
|
|
688
|
+
filteredData: TData[];
|
|
689
|
+
sortedData: TData[];
|
|
690
|
+
paginatedData: TData[];
|
|
691
|
+
|
|
692
|
+
// Pagination
|
|
693
|
+
pageIndex: number;
|
|
694
|
+
pageSize: number;
|
|
695
|
+
pageCount: number;
|
|
696
|
+
hasNextPage: boolean;
|
|
697
|
+
hasPreviousPage: boolean;
|
|
698
|
+
|
|
699
|
+
// Handlers
|
|
700
|
+
sorting: {
|
|
701
|
+
state: SortState;
|
|
702
|
+
toggle: (columnId: string) => void;
|
|
703
|
+
set: (columnId: string | null, direction: "asc" | "desc" | null) => void;
|
|
704
|
+
clear: () => void;
|
|
705
|
+
};
|
|
706
|
+
|
|
707
|
+
pagination: {
|
|
708
|
+
state: PaginationState;
|
|
709
|
+
nextPage: () => void;
|
|
710
|
+
previousPage: () => void;
|
|
711
|
+
goToPage: (pageIndex: number) => void;
|
|
712
|
+
setPageSize: (pageSize: number) => void;
|
|
713
|
+
};
|
|
714
|
+
|
|
715
|
+
filtering: {
|
|
716
|
+
state: FilterState;
|
|
717
|
+
setGlobalFilter: (filter: string) => void;
|
|
718
|
+
setColumnFilter: (columnId: string, filter: string) => void;
|
|
719
|
+
clearColumnFilter: (columnId: string) => void;
|
|
720
|
+
clearAllFilters: () => void;
|
|
721
|
+
};
|
|
722
|
+
|
|
723
|
+
columnManagement: {
|
|
724
|
+
toggleVisibility: (columnId: string) => void;
|
|
725
|
+
setVisibility: (columnId: string, visible: boolean) => void;
|
|
726
|
+
reorder: (columnOrder: string[]) => void;
|
|
727
|
+
};
|
|
728
|
+
|
|
729
|
+
selection: {
|
|
730
|
+
state: SelectionState;
|
|
731
|
+
enabled: boolean;
|
|
732
|
+
mode: "single" | "multi";
|
|
733
|
+
selectedRowIds: Set<string | number>;
|
|
734
|
+
selectedCount: number;
|
|
735
|
+
isSelected: (rowId: string | number) => boolean;
|
|
736
|
+
select: (rowId: string | number) => void;
|
|
737
|
+
deselect: (rowId: string | number) => void;
|
|
738
|
+
toggle: (rowId: string | number) => void;
|
|
739
|
+
selectAll: () => void;
|
|
740
|
+
deselectAll: () => void;
|
|
741
|
+
clear: () => void;
|
|
742
|
+
isAllSelected: boolean;
|
|
743
|
+
isIndeterminate: boolean;
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
```
|
|
747
|
+
|
|
140
748
|
## Styling
|
|
141
749
|
|
|
142
|
-
The library uses CSS variables for easy theming. Import
|
|
750
|
+
The library uses CSS variables for easy theming. Import default styles or create your own:
|
|
143
751
|
|
|
144
752
|
```css
|
|
145
753
|
:root {
|
|
146
|
-
--
|
|
147
|
-
--
|
|
148
|
-
--
|
|
149
|
-
--
|
|
150
|
-
--
|
|
151
|
-
--
|
|
754
|
+
--table-x-bg: #ffffff;
|
|
755
|
+
--table-x-header-bg: #f9fafb;
|
|
756
|
+
--table-x-sticky-bg: #ffffff;
|
|
757
|
+
--table-x-border-color: #e5e7eb;
|
|
758
|
+
--table-x-border-width: 1px;
|
|
759
|
+
--table-x-hover-bg: #f3f4f6;
|
|
760
|
+
--table-x-text-color: #111827;
|
|
761
|
+
}
|
|
762
|
+
```
|
|
763
|
+
|
|
764
|
+
### Custom Styles
|
|
765
|
+
|
|
766
|
+
```css
|
|
767
|
+
.table-x-header-cell {
|
|
768
|
+
background-color: var(--table-x-header-bg, #f9fafb);
|
|
769
|
+
border: var(--table-x-border-width, 1px) solid var(
|
|
770
|
+
--table-x-border-color,
|
|
771
|
+
#e5e7eb
|
|
772
|
+
);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
.table-x-cell {
|
|
776
|
+
padding: 12px;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
.table-x-checkbox {
|
|
780
|
+
cursor: pointer;
|
|
152
781
|
}
|
|
153
782
|
```
|
|
154
783
|
|
|
@@ -162,8 +791,58 @@ const columns = defineColumns<User>()([
|
|
|
162
791
|
col("name", { ... }), // ✅ Type-safe
|
|
163
792
|
col("invalid", { ... }), // ❌ Type error
|
|
164
793
|
]);
|
|
794
|
+
|
|
795
|
+
// Row data is typed
|
|
796
|
+
const table = useDataTable({
|
|
797
|
+
data: users, // TData inferred from columns
|
|
798
|
+
columns,
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
// Access typed data
|
|
802
|
+
table.paginatedData.forEach((user) => {
|
|
803
|
+
user.name; // ✅ Type-safe
|
|
804
|
+
user.invalid; // ❌ Type error
|
|
805
|
+
});
|
|
165
806
|
```
|
|
166
807
|
|
|
808
|
+
## Core Usage (Framework-agnostic)
|
|
809
|
+
|
|
810
|
+
```tsx
|
|
811
|
+
import {
|
|
812
|
+
defineColumns,
|
|
813
|
+
col,
|
|
814
|
+
createColumns,
|
|
815
|
+
createInitialTableState,
|
|
816
|
+
applyFilters,
|
|
817
|
+
applySort,
|
|
818
|
+
getPaginatedData,
|
|
819
|
+
} from "tablero/core";
|
|
820
|
+
|
|
821
|
+
const columns = defineColumns<User>()([
|
|
822
|
+
col("name", { header: "Name", sortable: true }),
|
|
823
|
+
col("email", { header: "Email" }),
|
|
824
|
+
]);
|
|
825
|
+
|
|
826
|
+
const runtimeColumns = createColumns(columns);
|
|
827
|
+
const state = createInitialTableState(columns.map((c) => c.id));
|
|
828
|
+
|
|
829
|
+
// Apply filters
|
|
830
|
+
const filtered = applyFilters(data, state.filtering, getValue);
|
|
831
|
+
|
|
832
|
+
// Apply sorting
|
|
833
|
+
const sorted = applySort(filtered, state.sorting, getValue);
|
|
834
|
+
|
|
835
|
+
// Paginate
|
|
836
|
+
const paginated = getPaginatedData(sorted, state.pagination);
|
|
837
|
+
```
|
|
838
|
+
|
|
839
|
+
## Examples
|
|
840
|
+
|
|
841
|
+
See the [examples](./examples/) directory for complete working examples:
|
|
842
|
+
|
|
843
|
+
- `react-example.tsx` - Full-featured React example with all features
|
|
844
|
+
- `basic-example.ts` - Core usage example
|
|
845
|
+
|
|
167
846
|
## License
|
|
168
847
|
|
|
169
848
|
MIT
|