@rowakit/table 0.4.0 → 1.0.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 +157 -1430
- package/dist/index.cjs +1190 -635
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +49 -4
- package/dist/index.d.ts +49 -4
- package/dist/index.js +1191 -636
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,25 +1,29 @@
|
|
|
1
1
|
# @rowakit/table
|
|
2
2
|
|
|
3
|
-
**
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
3
|
+
**Server-side-first React table for internal & business applications.**
|
|
4
|
+
Predictable API. Thin client. No data-grid bloat.
|
|
5
|
+
|
|
6
|
+
## Stability
|
|
7
|
+
|
|
8
|
+
`@rowakit/table` is **stable as of v1.0.0**.
|
|
9
|
+
|
|
10
|
+
See:
|
|
11
|
+
- `docs/API_STABILITY.md`
|
|
12
|
+
- `docs/API_FREEZE_SUMMARY.md`
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Why @rowakit/table?
|
|
17
|
+
|
|
18
|
+
Most React table libraries grow into complex data grids.
|
|
19
|
+
RowaKit Table is intentionally different:
|
|
20
|
+
|
|
21
|
+
* Backend owns data logic (pagination, sorting, filtering)
|
|
22
|
+
* Frontend stays thin and predictable
|
|
23
|
+
* API is opinionated and stable
|
|
24
|
+
* Workflow features are built-in, not bolted on
|
|
25
|
+
|
|
26
|
+
---
|
|
23
27
|
|
|
24
28
|
## Installation
|
|
25
29
|
|
|
@@ -31,1509 +35,232 @@ pnpm add @rowakit/table
|
|
|
31
35
|
yarn add @rowakit/table
|
|
32
36
|
```
|
|
33
37
|
|
|
34
|
-
|
|
38
|
+
Import base styles:
|
|
35
39
|
|
|
36
|
-
|
|
40
|
+
```ts
|
|
41
|
+
import '@rowakit/table/styles';
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Quick Start
|
|
37
47
|
|
|
38
48
|
```tsx
|
|
39
49
|
import { RowaKitTable, col } from '@rowakit/table';
|
|
40
50
|
import type { Fetcher } from '@rowakit/table';
|
|
41
|
-
import '@rowakit/table/styles';
|
|
42
|
-
|
|
43
|
-
interface User {
|
|
44
|
-
id: string;
|
|
45
|
-
name: string;
|
|
46
|
-
email: string;
|
|
47
|
-
createdAt: Date;
|
|
48
|
-
active: boolean;
|
|
49
|
-
}
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
### 2. Create a fetcher function
|
|
51
|
+
import '@rowakit/table/styles';
|
|
53
52
|
|
|
54
|
-
|
|
53
|
+
type User = { id: string; name: string; email: string; active: boolean };
|
|
55
54
|
|
|
56
|
-
|
|
57
|
-
const fetchUsers: Fetcher<User> = async (query) => {
|
|
55
|
+
const fetchUsers: Fetcher<User> = async ({ page, pageSize, sort }) => {
|
|
58
56
|
const params = new URLSearchParams({
|
|
59
|
-
page:
|
|
60
|
-
pageSize:
|
|
61
|
-
...(query.sort && {
|
|
62
|
-
sortField: query.sort.field,
|
|
63
|
-
sortDir: query.sort.direction,
|
|
64
|
-
}),
|
|
57
|
+
page: String(page),
|
|
58
|
+
pageSize: String(pageSize),
|
|
65
59
|
});
|
|
66
60
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
61
|
+
if (sort) {
|
|
62
|
+
params.set('sortField', sort.field);
|
|
63
|
+
params.set('sortDir', sort.direction);
|
|
70
64
|
}
|
|
71
|
-
return response.json(); // Must return: { items: User[], total: number }
|
|
72
|
-
};
|
|
73
|
-
```
|
|
74
65
|
|
|
75
|
-
|
|
66
|
+
const res = await fetch(`/api/users?${params}`);
|
|
67
|
+
if (!res.ok) throw new Error('Failed to fetch users');
|
|
76
68
|
|
|
77
|
-
|
|
78
|
-
|
|
69
|
+
return res.json();
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export function UsersTable() {
|
|
79
73
|
return (
|
|
80
74
|
<RowaKitTable
|
|
81
75
|
fetcher={fetchUsers}
|
|
76
|
+
rowKey="id"
|
|
82
77
|
columns={[
|
|
83
78
|
col.text('name', { header: 'Name', sortable: true }),
|
|
84
79
|
col.text('email', { header: 'Email' }),
|
|
85
|
-
col.date('createdAt', { header: 'Created' }),
|
|
86
80
|
col.boolean('active', { header: 'Active' }),
|
|
87
81
|
col.actions([
|
|
88
|
-
{ id: 'edit', label: 'Edit'
|
|
89
|
-
{ id: 'delete', label: 'Delete', confirm: true
|
|
90
|
-
])
|
|
82
|
+
{ id: 'edit', label: 'Edit' },
|
|
83
|
+
{ id: 'delete', label: 'Delete', confirm: true },
|
|
84
|
+
]),
|
|
91
85
|
]}
|
|
92
|
-
defaultPageSize={20}
|
|
93
|
-
rowKey="id"
|
|
94
86
|
/>
|
|
95
87
|
);
|
|
96
88
|
}
|
|
97
89
|
```
|
|
98
90
|
|
|
99
|
-
### That's it! 🎉
|
|
100
|
-
|
|
101
|
-
The table automatically handles:
|
|
102
|
-
- Fetches data on mount
|
|
103
|
-
- Shows loading state while fetching
|
|
104
|
-
- Displays error with retry button on failure
|
|
105
|
-
- Shows "No data" when empty
|
|
106
|
-
- Renders your data when successful
|
|
107
|
-
|
|
108
|
-
**Next Steps:**
|
|
109
|
-
- See [examples/](./examples/) for complete patterns (mock server, custom columns, styling)
|
|
110
|
-
- Read the [API Reference](#api-reference) below for all features
|
|
111
|
-
- Check [CONTRIBUTING.md](./CONTRIBUTING.md) to contribute
|
|
112
|
-
|
|
113
91
|
---
|
|
114
92
|
|
|
115
|
-
##
|
|
93
|
+
## Features (v1.0.0)
|
|
116
94
|
|
|
117
|
-
###
|
|
95
|
+
### Core table
|
|
118
96
|
|
|
119
|
-
|
|
97
|
+
* Server-side pagination, sorting, filtering
|
|
98
|
+
* Typed `Fetcher<T>` contract
|
|
99
|
+
* Built-in loading / error / empty states
|
|
100
|
+
* Stale request protection
|
|
120
101
|
|
|
121
|
-
|
|
102
|
+
### Columns
|
|
122
103
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
| `className` | `string` | No | `''` | Additional CSS classes for the table container |
|
|
104
|
+
* `col.text`
|
|
105
|
+
* `col.number`
|
|
106
|
+
* `col.date`
|
|
107
|
+
* `col.boolean`
|
|
108
|
+
* `col.badge`
|
|
109
|
+
* `col.actions`
|
|
110
|
+
* `col.custom`
|
|
131
111
|
|
|
132
|
-
###
|
|
112
|
+
### UX & workflows
|
|
133
113
|
|
|
134
|
-
|
|
114
|
+
* Column resizing (pointer events)
|
|
115
|
+
* Double-click auto-fit
|
|
116
|
+
* URL sync
|
|
117
|
+
* Saved views
|
|
118
|
+
* Row selection (page-scoped)
|
|
119
|
+
* Bulk actions
|
|
120
|
+
* Export via `exporter` callback
|
|
135
121
|
|
|
136
|
-
|
|
137
|
-
- On component mount
|
|
138
|
-
- When query parameters change (page, pageSize, sort, filters)
|
|
139
|
-
- When retry button is clicked after an error
|
|
122
|
+
---
|
|
140
123
|
|
|
141
|
-
|
|
142
|
-
type Fetcher<T> = (query: FetcherQuery) => Promise<FetcherResult<T>>;
|
|
124
|
+
## Fetcher Contract
|
|
143
125
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
126
|
+
```ts
|
|
127
|
+
type Fetcher<T> = (query: {
|
|
128
|
+
page: number;
|
|
129
|
+
pageSize: number;
|
|
130
|
+
/** Deprecated (kept for backward compatibility; planned removal in v2.0.0). */
|
|
131
|
+
sort?: { field: string; direction: 'asc' | 'desc' };
|
|
132
|
+
/** Multi-column sorting (preferred). */
|
|
133
|
+
sorts?: Array<{ field: string; direction: 'asc' | 'desc'; priority: number }>;
|
|
151
134
|
filters?: Record<string, unknown>;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
interface FetcherResult<T> {
|
|
155
|
-
items: T[]; // Array of items for current page
|
|
156
|
-
total: number; // Total number of items across all pages
|
|
157
|
-
}
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
**Error handling:**
|
|
161
|
-
|
|
162
|
-
Throw an error from your fetcher to trigger the error state with retry button:
|
|
163
|
-
|
|
164
|
-
```typescript
|
|
165
|
-
const fetchUsers: Fetcher<User> = async (query) => {
|
|
166
|
-
const response = await fetch(`/api/users?page=${query.page}`);
|
|
167
|
-
|
|
168
|
-
if (!response.ok) {
|
|
169
|
-
throw new Error('Failed to load users');
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
return response.json();
|
|
173
|
-
};
|
|
174
|
-
```
|
|
175
|
-
|
|
176
|
-
**State Management:**
|
|
177
|
-
|
|
178
|
-
The table automatically manages these states:
|
|
179
|
-
|
|
180
|
-
- **Loading** - Shows "Loading..." while fetcher is executing
|
|
181
|
-
- **Success** - Renders table with data when items.length > 0
|
|
182
|
-
- **Empty** - Shows "No data" when items.length === 0
|
|
183
|
-
- **Error** - Shows error message with "Retry" button when fetcher throws
|
|
184
|
-
|
|
185
|
-
**Stale Request Handling:**
|
|
186
|
-
|
|
187
|
-
The table automatically ignores responses from outdated requests, ensuring the UI always shows data from the most recent query.
|
|
188
|
-
|
|
189
|
-
### Column Helpers
|
|
190
|
-
|
|
191
|
-
Use `col.*` helpers to define columns with minimal boilerplate.
|
|
192
|
-
|
|
193
|
-
#### `col.text(field, options?)`
|
|
194
|
-
|
|
195
|
-
Text column with optional formatting.
|
|
196
|
-
|
|
197
|
-
```typescript
|
|
198
|
-
col.text('name')
|
|
199
|
-
col.text('email', { header: 'Email Address', sortable: true })
|
|
200
|
-
col.text('status', { format: (val) => val.toUpperCase() })
|
|
201
|
-
```
|
|
202
|
-
|
|
203
|
-
**Options:**
|
|
204
|
-
- `header?: string` - Custom header label
|
|
205
|
-
- `sortable?: boolean` - Enable sorting
|
|
206
|
-
- `format?: (value: unknown) => string` - Custom formatter
|
|
207
|
-
|
|
208
|
-
#### `col.date(field, options?)`
|
|
209
|
-
|
|
210
|
-
Date column with optional formatting.
|
|
211
|
-
|
|
212
|
-
```typescript
|
|
213
|
-
col.date('createdAt')
|
|
214
|
-
col.date('updatedAt', { header: 'Last Modified', sortable: true })
|
|
215
|
-
col.date('birthday', {
|
|
216
|
-
format: (date) => new Date(date).toLocaleDateString('en-US')
|
|
217
|
-
})
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
**Options:**
|
|
221
|
-
- `header?: string` - Custom header label
|
|
222
|
-
- `sortable?: boolean` - Enable sorting
|
|
223
|
-
- `format?: (value: Date | string | number) => string` - Custom date formatter
|
|
224
|
-
|
|
225
|
-
**Default format:** `date.toLocaleDateString()`
|
|
226
|
-
|
|
227
|
-
#### `col.boolean(field, options?)`
|
|
228
|
-
|
|
229
|
-
Boolean column with optional formatting.
|
|
230
|
-
|
|
231
|
-
```typescript
|
|
232
|
-
col.boolean('active')
|
|
233
|
-
col.boolean('isPublished', { header: 'Published' })
|
|
234
|
-
col.boolean('enabled', { format: (val) => val ? '✓' : '✗' })
|
|
235
|
-
```
|
|
236
|
-
|
|
237
|
-
**Options:**
|
|
238
|
-
- `header?: string` - Custom header label
|
|
239
|
-
- `sortable?: boolean` - Enable sorting
|
|
240
|
-
- `format?: (value: boolean) => string` - Custom formatter
|
|
241
|
-
|
|
242
|
-
**Default format:** `'Yes'` / `'No'`
|
|
243
|
-
|
|
244
|
-
#### `col.badge(field, options?)` (v0.2.0+)
|
|
245
|
-
|
|
246
|
-
Badge column with visual tone mapping for status/enum values.
|
|
247
|
-
|
|
248
|
-
```typescript
|
|
249
|
-
col.badge('status', {
|
|
250
|
-
header: 'Status',
|
|
251
|
-
map: {
|
|
252
|
-
active: { label: 'Active', tone: 'success' },
|
|
253
|
-
pending: { label: 'Pending', tone: 'warning' },
|
|
254
|
-
inactive: { label: 'Inactive', tone: 'neutral' },
|
|
255
|
-
error: { label: 'Error', tone: 'danger' }
|
|
256
|
-
}
|
|
257
|
-
})
|
|
258
|
-
|
|
259
|
-
col.badge('priority', {
|
|
260
|
-
header: 'Priority',
|
|
261
|
-
sortable: true,
|
|
262
|
-
map: {
|
|
263
|
-
high: { label: 'High', tone: 'danger' },
|
|
264
|
-
medium: { label: 'Medium', tone: 'warning' },
|
|
265
|
-
low: { label: 'Low', tone: 'success' }
|
|
266
|
-
}
|
|
267
|
-
})
|
|
268
|
-
```
|
|
269
|
-
|
|
270
|
-
**Options:**
|
|
271
|
-
- `header?: string` - Custom header label
|
|
272
|
-
- `sortable?: boolean` - Enable sorting
|
|
273
|
-
- `map?: Record<string, { label: string; tone: BadgeTone }>` - Map values to badge labels and visual tones
|
|
274
|
-
- `width?: number` - Column width in pixels
|
|
275
|
-
- `align?: 'left' | 'center' | 'right'` - Text alignment
|
|
276
|
-
- `truncate?: boolean` - Truncate with ellipsis
|
|
277
|
-
|
|
278
|
-
**Badge Tones:**
|
|
279
|
-
- `'neutral'` - Gray (default)
|
|
280
|
-
- `'success'` - Green
|
|
281
|
-
- `'warning'` - Yellow/Orange
|
|
282
|
-
- `'danger'` - Red
|
|
283
|
-
|
|
284
|
-
#### `col.number(field, options?)` (v0.2.0+)
|
|
285
|
-
|
|
286
|
-
Number column with formatting support (currency, percentages, decimals).
|
|
287
|
-
|
|
288
|
-
```typescript
|
|
289
|
-
// Basic number
|
|
290
|
-
col.number('quantity')
|
|
291
|
-
|
|
292
|
-
// Currency formatting with Intl.NumberFormat
|
|
293
|
-
col.number('price', {
|
|
294
|
-
header: 'Price',
|
|
295
|
-
sortable: true,
|
|
296
|
-
format: { style: 'currency', currency: 'USD' }
|
|
297
|
-
})
|
|
298
|
-
|
|
299
|
-
// Percentage
|
|
300
|
-
col.number('discount', {
|
|
301
|
-
header: 'Discount',
|
|
302
|
-
format: { style: 'percent', minimumFractionDigits: 1 }
|
|
303
|
-
})
|
|
304
|
-
|
|
305
|
-
// Custom formatter
|
|
306
|
-
col.number('score', {
|
|
307
|
-
header: 'Score',
|
|
308
|
-
format: (value) => `${value.toFixed(1)} pts`
|
|
309
|
-
})
|
|
310
|
-
```
|
|
311
|
-
|
|
312
|
-
**Options:**
|
|
313
|
-
- `header?: string` - Custom header label
|
|
314
|
-
- `sortable?: boolean` - Enable sorting
|
|
315
|
-
- `format?: Intl.NumberFormatOptions | ((value: number) => string)` - Formatting
|
|
316
|
-
- `Intl.NumberFormatOptions`: e.g., `{ style: 'currency', currency: 'USD' }`
|
|
317
|
-
- Function: Custom formatter like `(value) => value.toFixed(2)`
|
|
318
|
-
- `width?: number` - Column width in pixels
|
|
319
|
-
- `align?: 'left' | 'center' | 'right'` - Text alignment (defaults to 'right')
|
|
320
|
-
- `truncate?: boolean` - Truncate with ellipsis
|
|
321
|
-
|
|
322
|
-
**Default format:** Plain number with right alignment
|
|
323
|
-
|
|
324
|
-
#### `col.actions(actions)`
|
|
325
|
-
|
|
326
|
-
Actions column with buttons for row operations.
|
|
327
|
-
|
|
328
|
-
```typescript
|
|
329
|
-
col.actions([
|
|
330
|
-
{ id: 'edit', label: 'Edit', onClick: (row) => editItem(row) },
|
|
331
|
-
{
|
|
332
|
-
id: 'delete',
|
|
333
|
-
label: 'Delete',
|
|
334
|
-
confirm: true,
|
|
335
|
-
icon: '🗑️',
|
|
336
|
-
onClick: (row) => deleteItem(row)
|
|
337
|
-
}
|
|
338
|
-
])
|
|
339
|
-
```
|
|
340
|
-
|
|
341
|
-
**ActionDef properties:**
|
|
342
|
-
- `id: string` - Unique action identifier
|
|
343
|
-
- `label: string` - Button label
|
|
344
|
-
- `onClick: (row: T) => void | Promise<void>` - Action handler
|
|
345
|
-
- `icon?: string | ReactNode` - Optional icon
|
|
346
|
-
- `confirm?: boolean` - Show confirmation dialog
|
|
347
|
-
- `disabled?: boolean | ((row: T) => boolean)` - Disable button
|
|
348
|
-
- `loading?: boolean` - Show loading state
|
|
349
|
-
|
|
350
|
-
#### `col.custom(field, render)`
|
|
351
|
-
|
|
352
|
-
Custom column with full rendering control. **This is your escape hatch.**
|
|
353
|
-
|
|
354
|
-
```typescript
|
|
355
|
-
// Badge
|
|
356
|
-
col.custom('status', (row) => (
|
|
357
|
-
<Badge color={row.status === 'active' ? 'green' : 'gray'}>
|
|
358
|
-
{row.status}
|
|
359
|
-
</Badge>
|
|
360
|
-
))
|
|
361
|
-
|
|
362
|
-
// Avatar + Name
|
|
363
|
-
col.custom('user', (row) => (
|
|
364
|
-
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
|
365
|
-
<img src={row.avatar} alt="" width={32} height={32} />
|
|
366
|
-
<span>{row.name}</span>
|
|
367
|
-
</div>
|
|
368
|
-
))
|
|
369
|
-
|
|
370
|
-
// Money formatting
|
|
371
|
-
col.custom('price', (row) => (
|
|
372
|
-
<Money amount={row.price} currency={row.currency} />
|
|
373
|
-
))
|
|
374
|
-
|
|
375
|
-
// Complex logic
|
|
376
|
-
col.custom('summary', (row) => {
|
|
377
|
-
const status = row.active ? 'Active' : 'Inactive';
|
|
378
|
-
const lastSeen = formatDistanceToNow(row.lastSeenAt);
|
|
379
|
-
return `${status} - ${lastSeen}`;
|
|
380
|
-
})
|
|
381
|
-
```
|
|
382
|
-
|
|
383
|
-
### Column Modifiers (v0.2.0+)
|
|
384
|
-
|
|
385
|
-
All column types (text, date, boolean, badge, number) support these optional modifiers:
|
|
386
|
-
|
|
387
|
-
```typescript
|
|
388
|
-
// Width: Set fixed column width in pixels
|
|
389
|
-
col.text('name', { width: 200 })
|
|
390
|
-
|
|
391
|
-
// Align: Control text alignment
|
|
392
|
-
col.text('status', { align: 'center' })
|
|
393
|
-
col.number('price', { align: 'right' }) // numbers default to 'right'
|
|
394
|
-
|
|
395
|
-
// Truncate: Enable text truncation with ellipsis
|
|
396
|
-
col.text('description', { truncate: true, width: 300 })
|
|
397
|
-
|
|
398
|
-
// Combine multiple modifiers
|
|
399
|
-
col.badge('status', {
|
|
400
|
-
header: 'Status',
|
|
401
|
-
width: 120,
|
|
402
|
-
align: 'center',
|
|
403
|
-
truncate: true,
|
|
404
|
-
map: { active: 'success', inactive: 'neutral' }
|
|
405
|
-
})
|
|
406
|
-
```
|
|
407
|
-
|
|
408
|
-
**Modifiers:**
|
|
409
|
-
- `width?: number` - Column width in pixels
|
|
410
|
-
- `align?: 'left' | 'center' | 'right'` - Text alignment
|
|
411
|
-
- `truncate?: boolean` - Truncate long text with ellipsis (requires `width`)
|
|
412
|
-
|
|
413
|
-
**Notes:**
|
|
414
|
-
- Number columns default to `align: 'right'`
|
|
415
|
-
- Other columns default to `align: 'left'`
|
|
416
|
-
- Truncate works best with a fixed `width`
|
|
417
|
-
|
|
418
|
-
### Pagination
|
|
419
|
-
|
|
420
|
-
The table includes built-in pagination controls that appear automatically when data is loaded.
|
|
421
|
-
|
|
422
|
-
**Features:**
|
|
423
|
-
- **Page Size Selector** - Dropdown to change rows per page
|
|
424
|
-
- **Page Navigation** - Previous/Next buttons to navigate pages
|
|
425
|
-
- **Page Info** - Shows current page, total pages, and total items
|
|
426
|
-
- **Auto-disable** - Buttons disabled during loading or at boundaries
|
|
427
|
-
|
|
428
|
-
**Configuration:**
|
|
429
|
-
|
|
430
|
-
```typescript
|
|
431
|
-
<RowaKitTable
|
|
432
|
-
fetcher={fetchUsers}
|
|
433
|
-
columns={columns}
|
|
434
|
-
defaultPageSize={20} // Initial page size (default: 20)
|
|
435
|
-
pageSizeOptions={[10, 20, 50]} // Available sizes (default: [10, 20, 50])
|
|
436
|
-
rowKey="id"
|
|
437
|
-
/>
|
|
438
|
-
```
|
|
439
|
-
|
|
440
|
-
**Behavior:**
|
|
441
|
-
|
|
442
|
-
1. **On Page Change**: Fetcher called with new `page` value
|
|
443
|
-
2. **On Page Size Change**: Fetcher called with new `pageSize` and resets to `page: 1`
|
|
444
|
-
3. **During Loading**: All pagination controls are disabled
|
|
445
|
-
4. **No Data/Error**: Pagination controls are hidden
|
|
446
|
-
|
|
447
|
-
**Example Fetcher with Pagination:**
|
|
448
|
-
|
|
449
|
-
```typescript
|
|
450
|
-
const fetchUsers: Fetcher<User> = async ({ page, pageSize }) => {
|
|
451
|
-
const response = await fetch(
|
|
452
|
-
`/api/users?page=${page}&limit=${pageSize}`
|
|
453
|
-
);
|
|
454
|
-
|
|
455
|
-
const data = await response.json();
|
|
456
|
-
|
|
457
|
-
return {
|
|
458
|
-
items: data.users,
|
|
459
|
-
total: data.totalCount // Required for page calculation
|
|
460
|
-
};
|
|
461
|
-
};
|
|
462
|
-
```
|
|
463
|
-
|
|
464
|
-
**Pagination Math:**
|
|
465
|
-
|
|
466
|
-
- `totalPages = Math.ceil(total / pageSize)`
|
|
467
|
-
- Previous button disabled when `page === 1`
|
|
468
|
-
- Next button disabled when `page === totalPages`
|
|
469
|
-
- Changing page size resets to page 1 to avoid invalid page numbers
|
|
470
|
-
|
|
471
|
-
### Sorting
|
|
472
|
-
|
|
473
|
-
The table supports single-column sorting with automatic state management and visual indicators.
|
|
474
|
-
|
|
475
|
-
**Features:**
|
|
476
|
-
- **Click to Sort** - Click sortable column headers to toggle sort
|
|
477
|
-
- **Sort Indicators** - Visual arrows (↑ ↓) show current sort direction
|
|
478
|
-
- **Keyboard Accessible** - Sort with Enter or Space keys
|
|
479
|
-
- **Three-State Toggle** - None → Ascending → Descending → None
|
|
480
|
-
- **Auto Reset** - Page resets to 1 when sort changes
|
|
481
|
-
|
|
482
|
-
**Making Columns Sortable:**
|
|
483
|
-
|
|
484
|
-
Add `sortable: true` to column options:
|
|
485
|
-
|
|
486
|
-
```typescript
|
|
487
|
-
<RowaKitTable
|
|
488
|
-
fetcher={fetchUsers}
|
|
489
|
-
columns={[
|
|
490
|
-
col.text('name', { sortable: true }),
|
|
491
|
-
col.text('email', { sortable: true }),
|
|
492
|
-
col.date('createdAt', { sortable: true, header: 'Created' }),
|
|
493
|
-
col.boolean('active', { sortable: true }),
|
|
494
|
-
]}
|
|
495
|
-
rowKey="id"
|
|
496
|
-
/>
|
|
497
|
-
```
|
|
498
|
-
|
|
499
|
-
**Sort Behavior:**
|
|
500
|
-
|
|
501
|
-
1. **First Click**: Sort ascending (↑)
|
|
502
|
-
2. **Second Click**: Sort descending (↓)
|
|
503
|
-
3. **Third Click**: Remove sort (back to default order)
|
|
504
|
-
4. **Different Column**: Switches to new column, starts with ascending
|
|
505
|
-
|
|
506
|
-
**Fetcher Integration:**
|
|
507
|
-
|
|
508
|
-
The fetcher receives sort information in the query:
|
|
509
|
-
|
|
510
|
-
```typescript
|
|
511
|
-
const fetchUsers: Fetcher<User> = async ({ page, pageSize, sort }) => {
|
|
512
|
-
// sort is undefined when no sort is active
|
|
513
|
-
// sort = { field: 'name', direction: 'asc' | 'desc' } when sorting
|
|
514
|
-
|
|
515
|
-
const params = new URLSearchParams({
|
|
516
|
-
page: String(page),
|
|
517
|
-
limit: String(pageSize),
|
|
518
|
-
});
|
|
519
|
-
|
|
520
|
-
if (sort) {
|
|
521
|
-
params.append('sortBy', sort.field);
|
|
522
|
-
params.append('sortOrder', sort.direction);
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
const response = await fetch(`/api/users?${params}`);
|
|
526
|
-
return response.json();
|
|
527
|
-
};
|
|
528
|
-
```
|
|
529
|
-
|
|
530
|
-
**Which Columns Can Be Sorted:**
|
|
531
|
-
|
|
532
|
-
- ✅ Text columns with `sortable: true`
|
|
533
|
-
- ✅ Date columns with `sortable: true`
|
|
534
|
-
- ✅ Boolean columns with `sortable: true`
|
|
535
|
-
- ✅ Badge columns with `sortable: true` (v0.2.0+)
|
|
536
|
-
- ✅ Number columns with `sortable: true` (v0.2.0+)
|
|
537
|
-
- ❌ Actions columns (never sortable)
|
|
538
|
-
- ❌ Custom columns (not sortable by default)
|
|
539
|
-
|
|
540
|
-
**Sort State:**
|
|
541
|
-
|
|
542
|
-
- Maintained when navigating pages
|
|
543
|
-
- Maintained when changing page size (but resets to page 1)
|
|
544
|
-
- Cleared when clicking a sorted column three times
|
|
545
|
-
- Replaced when clicking a different sortable column
|
|
546
|
-
|
|
547
|
-
### Filters (v0.2.0+)
|
|
548
|
-
|
|
549
|
-
Server-side filtering with type-specific filter inputs rendered in a header row.
|
|
550
|
-
|
|
551
|
-
**Features:**
|
|
552
|
-
- **Auto-Generated UI** - Filter inputs based on column type
|
|
553
|
-
- **Server-Side Only** - All filtering happens in your backend
|
|
554
|
-
- **Multiple Operators** - contains, equals, in, range
|
|
555
|
-
- **Clear Filters** - Individual and bulk filter clearing
|
|
556
|
-
- **Page Reset** - Resets to page 1 when filters change
|
|
557
|
-
|
|
558
|
-
**Enable Filters:**
|
|
559
|
-
|
|
560
|
-
```typescript
|
|
561
|
-
<RowaKitTable
|
|
562
|
-
fetcher={fetchUsers}
|
|
563
|
-
columns={columns}
|
|
564
|
-
rowKey="id"
|
|
565
|
-
enableFilters={true} // Add this prop
|
|
566
|
-
/>
|
|
567
|
-
```
|
|
568
|
-
|
|
569
|
-
**Filter Types by Column:**
|
|
570
|
-
|
|
571
|
-
```typescript
|
|
572
|
-
// Text column: Text input with "contains" operator
|
|
573
|
-
col.text('name', { header: 'Name' })
|
|
574
|
-
// → User types "john" → filters: { name: { op: 'contains', value: 'john' } }
|
|
575
|
-
|
|
576
|
-
// Number column: Text input with "equals" operator
|
|
577
|
-
col.number('age', { header: 'Age' })
|
|
578
|
-
// → User types "25" → filters: { age: { op: 'equals', value: '25' } }
|
|
579
|
-
|
|
580
|
-
// Badge column: Select dropdown with "equals" operator
|
|
581
|
-
col.badge('status', {
|
|
582
|
-
header: 'Status',
|
|
583
|
-
map: { active: 'success', inactive: 'neutral', pending: 'warning' }
|
|
584
|
-
})
|
|
585
|
-
// → User selects "active" → filters: { status: { op: 'equals', value: 'active' } }
|
|
586
|
-
|
|
587
|
-
// Boolean column: Select dropdown (All/True/False) with "equals" operator
|
|
588
|
-
col.boolean('isVerified', { header: 'Verified' })
|
|
589
|
-
// → User selects "True" → filters: { isVerified: { op: 'equals', value: true } }
|
|
590
|
-
|
|
591
|
-
// Date column: Two date inputs with "range" operator
|
|
592
|
-
col.date('createdAt', { header: 'Created' })
|
|
593
|
-
// → User enters from/to dates → filters: { createdAt: { op: 'range', value: { from: '2024-01-01', to: '2024-12-31' } } }
|
|
594
|
-
```
|
|
595
|
-
|
|
596
|
-
**Fetcher Integration:**
|
|
597
|
-
|
|
598
|
-
```typescript
|
|
599
|
-
const fetchUsers: Fetcher<User> = async ({ page, pageSize, sort, filters }) => {
|
|
600
|
-
// filters is undefined when no filters are active
|
|
601
|
-
// filters = { fieldName: FilterValue, ... } when filtering
|
|
602
|
-
|
|
603
|
-
const params = new URLSearchParams({
|
|
604
|
-
page: String(page),
|
|
605
|
-
limit: String(pageSize),
|
|
606
|
-
});
|
|
607
|
-
|
|
608
|
-
if (sort) {
|
|
609
|
-
params.append('sortBy', sort.field);
|
|
610
|
-
params.append('sortOrder', sort.direction);
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
if (filters) {
|
|
614
|
-
// Example: Convert filters to query params
|
|
615
|
-
for (const [field, filter] of Object.entries(filters)) {
|
|
616
|
-
if (filter.op === 'contains') {
|
|
617
|
-
params.append(`${field}_contains`, filter.value);
|
|
618
|
-
} else if (filter.op === 'equals') {
|
|
619
|
-
params.append(field, String(filter.value));
|
|
620
|
-
} else if (filter.op === 'range') {
|
|
621
|
-
if (filter.value.from) params.append(`${field}_from`, filter.value.from);
|
|
622
|
-
if (filter.value.to) params.append(`${field}_to`, filter.value.to);
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
const response = await fetch(`/api/users?${params}`);
|
|
628
|
-
return response.json();
|
|
629
|
-
};
|
|
630
|
-
```
|
|
631
|
-
|
|
632
|
-
**Filter Value Types:**
|
|
633
|
-
|
|
634
|
-
```typescript
|
|
635
|
-
type FilterValue =
|
|
636
|
-
| { op: 'contains'; value: string } // Text search
|
|
637
|
-
| { op: 'equals'; value: string | number | boolean | null } // Exact match
|
|
638
|
-
| { op: 'in'; value: Array<string | number> } // Multiple values (future)
|
|
639
|
-
| { op: 'range'; value: { from?: string; to?: string } }; // Date range
|
|
640
|
-
|
|
641
|
-
type Filters = Record<string, FilterValue>;
|
|
642
|
-
```
|
|
643
|
-
|
|
644
|
-
**Important Rules:**
|
|
645
|
-
|
|
646
|
-
1. **Undefined when empty**: `query.filters` is `undefined` when no filters are active (not `{}`)
|
|
647
|
-
2. **Page resets**: Changing any filter resets page to 1
|
|
648
|
-
3. **No client filtering**: All filtering must be handled by your backend
|
|
649
|
-
4. **Actions/Custom columns**: Not filterable (no filter input rendered)
|
|
650
|
-
|
|
651
|
-
**⚠️ Number Filter Behavior:**
|
|
652
|
-
|
|
653
|
-
Number filters use **exact match** semantics (`op: 'equals'`). The filter value is sent as a **number** (not a string), enabling direct numeric comparison:
|
|
654
|
-
|
|
655
|
-
```typescript
|
|
656
|
-
// If user types "15" in a number filter input
|
|
657
|
-
filters: { discount: { op: 'equals', value: 15 } }
|
|
658
|
-
|
|
659
|
-
// Your backend receives a numeric value and can compare directly:
|
|
660
|
-
// if (discount === 15) { /* match */ }
|
|
661
|
-
```
|
|
662
|
-
|
|
663
|
-
**Handling Percentage/Fraction Data:**
|
|
664
|
-
|
|
665
|
-
If your data uses decimals or percentages while displaying as whole numbers, ensure your backend coerces appropriately:
|
|
666
|
-
|
|
667
|
-
```typescript
|
|
668
|
-
// Example: Number filter for percentage discount
|
|
669
|
-
if (filters?.discount) {
|
|
670
|
-
const filterValue = filters.discount.value; // Already a number
|
|
671
|
-
// If data is stored as fraction (0.15):
|
|
672
|
-
const compareValue = filterValue / 100; // Convert 15 → 0.15
|
|
673
|
-
// Filter records where discount === compareValue
|
|
674
|
-
}
|
|
675
|
-
```
|
|
676
|
-
|
|
677
|
-
**Clear Filters:**
|
|
678
|
-
|
|
679
|
-
A "Clear all filters" button appears automatically when filters are active:
|
|
680
|
-
|
|
681
|
-
```typescript
|
|
682
|
-
// User applies filters
|
|
683
|
-
// → "Clear all filters" button appears above table
|
|
684
|
-
// → Click button → all filters cleared → page resets to 1
|
|
685
|
-
```
|
|
686
|
-
|
|
687
|
-
### Actions
|
|
688
|
-
|
|
689
|
-
The table provides built-in support for row actions with confirmation dialogs, loading states, and conditional disabling.
|
|
690
|
-
|
|
691
|
-
**Features:**
|
|
692
|
-
- **Action Buttons** - Render buttons for each row (edit, delete, view, etc.)
|
|
693
|
-
- **Confirmation Dialogs** - Built-in modal for destructive actions
|
|
694
|
-
- **Loading States** - Disable actions while table is loading
|
|
695
|
-
- **Conditional Disabling** - Disable actions based on row data
|
|
696
|
-
- **Icons** - Support for string or React component icons
|
|
697
|
-
- **Async Support** - Action handlers can be async/Promise-based
|
|
698
|
-
|
|
699
|
-
**Basic Actions:**
|
|
700
|
-
|
|
701
|
-
```typescript
|
|
702
|
-
<RowaKitTable
|
|
703
|
-
fetcher={fetchUsers}
|
|
704
|
-
columns={[
|
|
705
|
-
col.text('name'),
|
|
706
|
-
col.text('email'),
|
|
707
|
-
col.actions([
|
|
708
|
-
{
|
|
709
|
-
id: 'edit',
|
|
710
|
-
label: 'Edit',
|
|
711
|
-
onClick: (row) => {
|
|
712
|
-
console.log('Editing:', row);
|
|
713
|
-
// Handle edit
|
|
714
|
-
}
|
|
715
|
-
},
|
|
716
|
-
{
|
|
717
|
-
id: 'view',
|
|
718
|
-
label: 'View Details',
|
|
719
|
-
icon: '👁️',
|
|
720
|
-
onClick: (row) => {
|
|
721
|
-
window.open(`/users/${row.id}`, '_blank');
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
])
|
|
725
|
-
]}
|
|
726
|
-
rowKey="id"
|
|
727
|
-
/>
|
|
728
|
-
```
|
|
729
|
-
|
|
730
|
-
**Actions with Confirmation:**
|
|
731
|
-
|
|
732
|
-
Use `confirm: true` to show a confirmation dialog before executing the action:
|
|
733
|
-
|
|
734
|
-
```typescript
|
|
735
|
-
col.actions([
|
|
736
|
-
{
|
|
737
|
-
id: 'delete',
|
|
738
|
-
label: 'Delete',
|
|
739
|
-
icon: '🗑️',
|
|
740
|
-
confirm: true, // Shows confirmation modal
|
|
741
|
-
onClick: async (row) => {
|
|
742
|
-
await deleteUser(row.id);
|
|
743
|
-
// Refetch data to update table
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
])
|
|
135
|
+
}) => Promise<{ items: T[]; total: number }>;
|
|
747
136
|
```
|
|
748
137
|
|
|
749
|
-
|
|
750
|
-
- **Title:** "Confirm Action"
|
|
751
|
-
- **Message:** "Are you sure you want to delete? This action cannot be undone."
|
|
752
|
-
- **Buttons:** "Cancel" (gray) and "Confirm" (red)
|
|
753
|
-
|
|
754
|
-
**Conditional Disabling:**
|
|
755
|
-
|
|
756
|
-
Disable actions based on row data:
|
|
757
|
-
|
|
758
|
-
```typescript
|
|
759
|
-
col.actions([
|
|
760
|
-
{
|
|
761
|
-
id: 'delete',
|
|
762
|
-
label: 'Delete',
|
|
763
|
-
confirm: true,
|
|
764
|
-
// Disable for admin users
|
|
765
|
-
disabled: (row) => row.role === 'admin',
|
|
766
|
-
onClick: async (row) => {
|
|
767
|
-
await deleteUser(row.id);
|
|
768
|
-
}
|
|
769
|
-
},
|
|
770
|
-
{
|
|
771
|
-
id: 'edit',
|
|
772
|
-
label: 'Edit',
|
|
773
|
-
// Always disabled
|
|
774
|
-
disabled: true,
|
|
775
|
-
onClick: (row) => editUser(row)
|
|
776
|
-
}
|
|
777
|
-
])
|
|
778
|
-
```
|
|
779
|
-
|
|
780
|
-
**Loading States:**
|
|
781
|
-
|
|
782
|
-
Set `loading: true` to disable an action button and show loading state:
|
|
783
|
-
|
|
784
|
-
```typescript
|
|
785
|
-
const [deletingId, setDeletingId] = useState<string | null>(null);
|
|
786
|
-
|
|
787
|
-
col.actions([
|
|
788
|
-
{
|
|
789
|
-
id: 'delete',
|
|
790
|
-
label: 'Delete',
|
|
791
|
-
loading: deletingId === row.id, // Disable while deleting
|
|
792
|
-
onClick: async (row) => {
|
|
793
|
-
setDeletingId(row.id);
|
|
794
|
-
await deleteUser(row.id);
|
|
795
|
-
setDeletingId(null);
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
|
-
])
|
|
799
|
-
```
|
|
800
|
-
|
|
801
|
-
**Multiple Actions:**
|
|
802
|
-
|
|
803
|
-
You can combine multiple actions in one column:
|
|
804
|
-
|
|
805
|
-
```typescript
|
|
806
|
-
col.actions([
|
|
807
|
-
{
|
|
808
|
-
id: 'edit',
|
|
809
|
-
label: 'Edit',
|
|
810
|
-
icon: '✏️',
|
|
811
|
-
onClick: (row) => editUser(row)
|
|
812
|
-
},
|
|
813
|
-
{
|
|
814
|
-
id: 'view',
|
|
815
|
-
label: 'View',
|
|
816
|
-
icon: '👁️',
|
|
817
|
-
onClick: (row) => viewUser(row)
|
|
818
|
-
},
|
|
819
|
-
{
|
|
820
|
-
id: 'delete',
|
|
821
|
-
label: 'Delete',
|
|
822
|
-
icon: '🗑️',
|
|
823
|
-
confirm: true,
|
|
824
|
-
disabled: (row) => row.role === 'admin',
|
|
825
|
-
onClick: async (row) => {
|
|
826
|
-
await deleteUser(row.id);
|
|
827
|
-
}
|
|
828
|
-
}
|
|
829
|
-
])
|
|
830
|
-
```
|
|
831
|
-
|
|
832
|
-
**Action Properties:**
|
|
833
|
-
|
|
834
|
-
| Property | Type | Required | Description |
|
|
835
|
-
|----------|------|----------|-------------|
|
|
836
|
-
| `id` | `string` | ✅ Yes | Unique identifier for the action |
|
|
837
|
-
| `label` | `string` | ✅ Yes | Button label text |
|
|
838
|
-
| `onClick` | `(row: T) => void \| Promise<void>` | ✅ Yes | Action handler (called after confirmation if required) |
|
|
839
|
-
| `icon` | `string \| ReactNode` | No | Icon displayed before label (emoji or React component) |
|
|
840
|
-
| `confirm` | `boolean` | No | Show confirmation dialog before executing (default: `false`) |
|
|
841
|
-
| `disabled` | `boolean \| (row: T) => boolean` | No | Disable button (static or per-row function) |
|
|
842
|
-
| `loading` | `boolean` | No | Show loading state and disable button |
|
|
138
|
+
Guidelines:
|
|
843
139
|
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
2. **Provide Visual Feedback** - Use icons to make actions more recognizable
|
|
848
|
-
3. **Handle Async Properly** - Mark action handlers as `async` and handle errors
|
|
849
|
-
4. **Disable Appropriately** - Use `disabled` function to prevent invalid actions
|
|
850
|
-
5. **Keep Actions Minimal** - Limit to 2-3 primary actions per row; use a dropdown menu for many actions
|
|
851
|
-
|
|
852
|
-
**Automatic Behavior:**
|
|
853
|
-
|
|
854
|
-
- All actions are disabled while the table is loading data
|
|
855
|
-
- Confirmation modal prevents accidental clicks on backdrop (must click Cancel or Confirm)
|
|
856
|
-
- Keyboard accessible (Tab to focus, Enter/Space to activate)
|
|
857
|
-
- Actions column is never sortable
|
|
140
|
+
* Backend is the source of truth
|
|
141
|
+
* Throw errors to trigger built-in error UI
|
|
142
|
+
* Ignore stale requests (handled internally)
|
|
858
143
|
|
|
859
144
|
---
|
|
860
145
|
|
|
861
|
-
##
|
|
862
|
-
|
|
863
|
-
### Column Resizing (C-01)
|
|
864
|
-
|
|
865
|
-
Enable users to resize columns by dragging the right edge of column headers.
|
|
866
|
-
|
|
867
|
-
**Basic Usage:**
|
|
868
|
-
|
|
869
|
-
```typescript
|
|
870
|
-
<RowaKitTable
|
|
871
|
-
fetcher={fetchData}
|
|
872
|
-
columns={[
|
|
873
|
-
col.text('name', {
|
|
874
|
-
minWidth: 100, // Minimum width (default: 80)
|
|
875
|
-
maxWidth: 400 // Maximum width (optional)
|
|
876
|
-
}),
|
|
877
|
-
col.number('price', {
|
|
878
|
-
minWidth: 80
|
|
879
|
-
})
|
|
880
|
-
]}
|
|
881
|
-
enableColumnResizing={true} // Enable resize feature
|
|
882
|
-
/>
|
|
883
|
-
```
|
|
884
|
-
|
|
885
|
-
**Features:**
|
|
886
|
-
- **Auto-width by default** - Columns automatically size based on header text
|
|
887
|
-
- **Drag to resize** - Drag the blue handle on the right edge of column headers
|
|
888
|
-
- **Double-click to auto-fit** - Double-click the resize handle to auto-fit content width (measures visible header + cells)
|
|
889
|
-
- **Min/max constraints** - Enforced in real-time during drag
|
|
890
|
-
- **Smooth performance** - RAF throttling prevents lag during resize
|
|
891
|
-
- **Large hitbox** - 12px wide invisible zone (1px visible line) makes dragging easy
|
|
892
|
-
- **State persistence** - Widths stored in-memory (or persisted via URL sync/saved views)
|
|
893
|
-
|
|
894
|
-
**Interaction:**
|
|
895
|
-
- **Hover** - Resize handle appears as a blue vertical line
|
|
896
|
-
- **Drag** - Click and drag to resize column width
|
|
897
|
-
- **Double-click** - Auto-fits to content (max 600px by default)
|
|
898
|
-
- Text selection is disabled during drag for smooth UX
|
|
899
|
-
|
|
900
|
-
### Saved Views + URL State Sync (C-02)
|
|
901
|
-
|
|
902
|
-
Save and restore table configurations, and share URLs with exact table state.
|
|
903
|
-
|
|
904
|
-
**Basic Usage:**
|
|
905
|
-
|
|
906
|
-
```typescript
|
|
907
|
-
<RowaKitTable
|
|
908
|
-
fetcher={fetchData}
|
|
909
|
-
columns={columns}
|
|
910
|
-
syncToUrl={true} // Sync to URL query string
|
|
911
|
-
enableSavedViews={true} // Show save/load view buttons
|
|
912
|
-
/>
|
|
913
|
-
```
|
|
914
|
-
|
|
915
|
-
**Features:**
|
|
916
|
-
|
|
917
|
-
1. **URL Sync** - Automatically saves table state to URL query parameters:
|
|
918
|
-
```
|
|
919
|
-
?page=2&pageSize=20&sortField=name&sortDirection=asc&filters=...&columnWidths=...
|
|
920
|
-
```
|
|
921
|
-
- Share URLs to preserve exact table configuration
|
|
922
|
-
- State automatically restored on page load
|
|
923
|
-
- Works with browser back/forward buttons
|
|
924
|
-
|
|
925
|
-
2. **Saved Views** - Save current table state as named presets:
|
|
926
|
-
```
|
|
927
|
-
[Save View] button → Name your view → State saved to localStorage
|
|
928
|
-
[My View] button → Click to restore saved state
|
|
929
|
-
× button → Delete saved view
|
|
930
|
-
[Reset] button → Clear all state
|
|
931
|
-
```
|
|
932
|
-
|
|
933
|
-
**Synced State:**
|
|
934
|
-
- Page number and size
|
|
935
|
-
- Sort field and direction
|
|
936
|
-
- All active filters
|
|
937
|
-
- Column widths (if resizing enabled)
|
|
938
|
-
|
|
939
|
-
**Example:**
|
|
940
|
-
|
|
941
|
-
```typescript
|
|
942
|
-
// User filters to "active users" and resizes columns
|
|
943
|
-
// They click "Save View" and name it "Active"
|
|
944
|
-
// Later, they apply different filters
|
|
945
|
-
// They click "Active" button to instantly restore previous state
|
|
946
|
-
|
|
947
|
-
// Or they copy the URL and send it to a colleague
|
|
948
|
-
// The colleague sees the exact same filters and layout
|
|
949
|
-
```
|
|
950
|
-
|
|
951
|
-
### Advanced Number Range Filters (C-03)
|
|
952
|
-
|
|
953
|
-
Number columns support min/max range filtering with optional value transformation.
|
|
954
|
-
|
|
955
|
-
**Basic Range Filter:**
|
|
956
|
-
|
|
957
|
-
```typescript
|
|
958
|
-
col.number('price', {
|
|
959
|
-
label: 'Price',
|
|
960
|
-
width: 100
|
|
961
|
-
})
|
|
962
|
-
|
|
963
|
-
// UI shows two inputs: [Min] [Max]
|
|
964
|
-
// User enters: min=100, max=500
|
|
965
|
-
// Backend receives: { op: 'range', value: { from: 100, to: 500 } }
|
|
966
|
-
```
|
|
967
|
-
|
|
968
|
-
**With Filter Transform:**
|
|
969
|
-
|
|
970
|
-
```typescript
|
|
971
|
-
col.number('discount', {
|
|
972
|
-
label: 'Discount %',
|
|
973
|
-
// Transform percentage input to fraction for backend
|
|
974
|
-
filterTransform: (percentageInput) => {
|
|
975
|
-
// User enters "15" (15%)
|
|
976
|
-
// Backend receives "0.15" (fraction)
|
|
977
|
-
if (percentageInput > 1) {
|
|
978
|
-
return percentageInput / 100;
|
|
979
|
-
}
|
|
980
|
-
return percentageInput;
|
|
981
|
-
}
|
|
982
|
-
})
|
|
983
|
-
```
|
|
984
|
-
|
|
985
|
-
**Features:**
|
|
986
|
-
- Min and max inputs can be filled independently
|
|
987
|
-
- Example: "at least 50" (min only), "up to 100" (max only), or "50-100" (both)
|
|
988
|
-
- Optional `filterTransform` to adapt filter values before sending to server
|
|
989
|
-
- Useful for unit conversion, percentage ↔ decimal, etc.
|
|
990
|
-
|
|
991
|
-
**Handling Range Filters in Fetcher:**
|
|
992
|
-
|
|
993
|
-
```typescript
|
|
994
|
-
const fetchProducts: Fetcher<Product> = async (query) => {
|
|
995
|
-
let filtered = [...allProducts];
|
|
996
|
-
|
|
997
|
-
if (query.filters) {
|
|
998
|
-
for (const [field, filter] of Object.entries(query.filters)) {
|
|
999
|
-
if (filter?.op === 'range') {
|
|
1000
|
-
const { from, to } = filter.value as { from?: number; to?: number };
|
|
1001
|
-
filtered = filtered.filter(item => {
|
|
1002
|
-
const val = item[field as keyof Product];
|
|
1003
|
-
if (from !== undefined && val < from) return false;
|
|
1004
|
-
if (to !== undefined && val > to) return false;
|
|
1005
|
-
return true;
|
|
1006
|
-
});
|
|
1007
|
-
}
|
|
1008
|
-
}
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
|
-
return {
|
|
1012
|
-
items: filtered.slice(0, query.pageSize),
|
|
1013
|
-
total: filtered.length
|
|
1014
|
-
};
|
|
1015
|
-
};
|
|
1016
|
-
```
|
|
1017
|
-
|
|
1018
|
-
## Examples
|
|
1019
|
-
|
|
1020
|
-
### Basic Table
|
|
1021
|
-
|
|
1022
|
-
```tsx
|
|
1023
|
-
<RowaKitTable
|
|
1024
|
-
fetcher={fetchUsers}
|
|
1025
|
-
columns={[
|
|
1026
|
-
col.text('name'),
|
|
1027
|
-
col.text('email'),
|
|
1028
|
-
col.boolean('active')
|
|
1029
|
-
]}
|
|
1030
|
-
rowKey="id"
|
|
1031
|
-
/>
|
|
1032
|
-
```
|
|
1033
|
-
|
|
1034
|
-
### With Sorting
|
|
146
|
+
## Row Selection
|
|
1035
147
|
|
|
1036
148
|
```tsx
|
|
1037
|
-
// Simple sortable columns
|
|
1038
149
|
<RowaKitTable
|
|
150
|
+
enableRowSelection
|
|
151
|
+
onSelectionChange={(keys) => console.log(keys)}
|
|
1039
152
|
fetcher={fetchUsers}
|
|
1040
|
-
columns={[
|
|
1041
|
-
col.text('name', { sortable: true }),
|
|
1042
|
-
col.text('email', { sortable: true }),
|
|
1043
|
-
col.date('createdAt', { sortable: true, header: 'Created' })
|
|
1044
|
-
]}
|
|
1045
|
-
rowKey="id"
|
|
1046
|
-
/>
|
|
1047
|
-
|
|
1048
|
-
// Fetcher with sort handling
|
|
1049
|
-
const fetchUsers: Fetcher<User> = async ({ page, pageSize, sort }) => {
|
|
1050
|
-
const params = new URLSearchParams({
|
|
1051
|
-
page: String(page),
|
|
1052
|
-
limit: String(pageSize),
|
|
1053
|
-
});
|
|
1054
|
-
|
|
1055
|
-
if (sort) {
|
|
1056
|
-
params.append('sortBy', sort.field);
|
|
1057
|
-
params.append('sortOrder', sort.direction);
|
|
1058
|
-
}
|
|
1059
|
-
|
|
1060
|
-
const response = await fetch(`/api/users?${params}`);
|
|
1061
|
-
return response.json();
|
|
1062
|
-
};
|
|
1063
|
-
```
|
|
1064
|
-
|
|
1065
|
-
**Sort behavior:**
|
|
1066
|
-
- Click header once: sort ascending (↑)
|
|
1067
|
-
- Click again: sort descending (↓)
|
|
1068
|
-
- Click third time: remove sort
|
|
1069
|
-
- Sortable headers have pointer cursor and are keyboard accessible
|
|
1070
|
-
|
|
1071
|
-
### With Custom Formatting
|
|
1072
|
-
|
|
1073
|
-
```tsx
|
|
1074
|
-
<RowaKitTable
|
|
1075
|
-
fetcher={fetchProducts}
|
|
1076
|
-
columns={[
|
|
1077
|
-
col.text('name'),
|
|
1078
|
-
col.text('category', {
|
|
1079
|
-
format: (val) => String(val).toUpperCase()
|
|
1080
|
-
}),
|
|
1081
|
-
col.date('createdAt', {
|
|
1082
|
-
format: (date) => new Date(date).toLocaleDateString()
|
|
1083
|
-
}),
|
|
1084
|
-
col.boolean('inStock', {
|
|
1085
|
-
format: (val) => val ? '✓ In Stock' : '✗ Out of Stock'
|
|
1086
|
-
})
|
|
1087
|
-
]}
|
|
1088
|
-
rowKey="id"
|
|
153
|
+
columns={[/* ... */]}
|
|
1089
154
|
/>
|
|
1090
155
|
```
|
|
1091
156
|
|
|
1092
|
-
|
|
157
|
+
* Selection is page-scoped
|
|
158
|
+
* Resets on page change
|
|
1093
159
|
|
|
1094
|
-
|
|
1095
|
-
<RowaKitTable
|
|
1096
|
-
fetcher={fetchUsers}
|
|
1097
|
-
columns={[
|
|
1098
|
-
col.text('name'),
|
|
1099
|
-
col.text('email'),
|
|
1100
|
-
col.actions([
|
|
1101
|
-
{
|
|
1102
|
-
id: 'view',
|
|
1103
|
-
label: 'View',
|
|
1104
|
-
onClick: (user) => navigate(`/users/${user.id}`)
|
|
1105
|
-
},
|
|
1106
|
-
{
|
|
1107
|
-
id: 'edit',
|
|
1108
|
-
label: 'Edit',
|
|
1109
|
-
onClick: (user) => openEditModal(user)
|
|
1110
|
-
},
|
|
1111
|
-
{
|
|
1112
|
-
id: 'delete',
|
|
1113
|
-
label: 'Delete',
|
|
1114
|
-
confirm: true,
|
|
1115
|
-
onClick: async (user) => {
|
|
1116
|
-
await deleteUser(user.id);
|
|
1117
|
-
// Trigger refetch by updating query (A-06 will add this)
|
|
1118
|
-
}
|
|
1119
|
-
}
|
|
1120
|
-
])
|
|
1121
|
-
]}
|
|
1122
|
-
rowKey="id"
|
|
1123
|
-
/>
|
|
1124
|
-
```
|
|
160
|
+
---
|
|
1125
161
|
|
|
1126
|
-
|
|
162
|
+
## Multi-Column Sorting
|
|
1127
163
|
|
|
1128
|
-
|
|
164
|
+
Sort by multiple columns simultaneously using **Ctrl+Click** (Windows/Linux) or **Cmd+Click** (Mac) on column headers:
|
|
1129
165
|
|
|
1130
166
|
```tsx
|
|
1131
|
-
//
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
}
|
|
167
|
+
// Hold Ctrl/Cmd and click column headers in order
|
|
168
|
+
// Priority is determined by click order (first click = priority 1)
|
|
169
|
+
|
|
170
|
+
// The fetcher receives sorts array:
|
|
171
|
+
const fetcher = async (query: FetcherQuery) => {
|
|
172
|
+
// query.sorts = [
|
|
173
|
+
// { field: 'lastName', direction: 'asc', priority: 1 },
|
|
174
|
+
// { field: 'firstName', direction: 'asc', priority: 2 },
|
|
175
|
+
// { field: 'salary', direction: 'desc', priority: 3 }
|
|
176
|
+
// ]
|
|
177
|
+
const res = await fetch('/api/users', {
|
|
178
|
+
method: 'POST',
|
|
179
|
+
body: JSON.stringify(query),
|
|
180
|
+
});
|
|
181
|
+
return res.json();
|
|
1145
182
|
};
|
|
1146
183
|
|
|
1147
|
-
|
|
1148
|
-
// 1. Show error message in the UI
|
|
1149
|
-
// 2. Display a "Retry" button
|
|
1150
|
-
// 3. Call the fetcher again with the same query when retry is clicked
|
|
184
|
+
<RowaKitTable fetcher={fetcher} columns={[/* ... */]} />
|
|
1151
185
|
```
|
|
1152
186
|
|
|
1153
|
-
|
|
187
|
+
**Migration from deprecated `sort` field:**
|
|
188
|
+
- Old format: `query.sort = { field: 'name', direction: 'asc' }`
|
|
189
|
+
- New format: `query.sorts = [{ field: 'name', direction: 'asc', priority: 1 }]`
|
|
190
|
+
- Both fields coexist during transition; `sort` will be removed in v2.0.0
|
|
1154
191
|
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
fetcher={fetchUsers}
|
|
1159
|
-
columns={[
|
|
1160
|
-
col.text('name'),
|
|
1161
|
-
col.text('email'),
|
|
1162
|
-
col.date('createdAt')
|
|
1163
|
-
]}
|
|
1164
|
-
rowKey="id"
|
|
1165
|
-
// Uses default: defaultPageSize={20}, pageSizeOptions={[10, 20, 50]}
|
|
1166
|
-
/>
|
|
1167
|
-
|
|
1168
|
-
// Custom pagination settings
|
|
1169
|
-
<RowaKitTable
|
|
1170
|
-
fetcher={fetchUsers}
|
|
1171
|
-
columns={columns}
|
|
1172
|
-
defaultPageSize={25}
|
|
1173
|
-
pageSizeOptions={[25, 50, 100, 200]}
|
|
1174
|
-
rowKey="id"
|
|
1175
|
-
/>
|
|
1176
|
-
|
|
1177
|
-
// Fetcher receives page and pageSize
|
|
1178
|
-
const fetchUsers: Fetcher<User> = async ({ page, pageSize }) => {
|
|
1179
|
-
const offset = (page - 1) * pageSize;
|
|
1180
|
-
const response = await fetch(
|
|
1181
|
-
`/api/users?offset=${offset}&limit=${pageSize}`
|
|
1182
|
-
);
|
|
1183
|
-
const data = await response.json();
|
|
1184
|
-
|
|
1185
|
-
return {
|
|
1186
|
-
items: data.users,
|
|
1187
|
-
total: data.totalCount // Required for pagination
|
|
1188
|
-
};
|
|
1189
|
-
};
|
|
1190
|
-
```
|
|
192
|
+
**UI Indicators:**
|
|
193
|
+
- Single column: Standard sort arrow indicator
|
|
194
|
+
- Multiple columns: Priority number displayed on sorted column headers
|
|
1191
195
|
|
|
1192
|
-
|
|
1193
|
-
- Hides when `total === 0` (no data or error)
|
|
1194
|
-
- Disables controls during loading
|
|
1195
|
-
- Resets to page 1 when page size changes
|
|
1196
|
-
- Calculates total pages from `total / pageSize`
|
|
196
|
+
---
|
|
1197
197
|
|
|
1198
|
-
|
|
198
|
+
## Bulk Actions
|
|
1199
199
|
|
|
1200
200
|
```tsx
|
|
1201
201
|
<RowaKitTable
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
{row.email}
|
|
1211
|
-
</div>
|
|
1212
|
-
</div>
|
|
1213
|
-
</div>
|
|
1214
|
-
)),
|
|
1215
|
-
col.custom('status', (row) => (
|
|
1216
|
-
<Badge color={row.active ? 'green' : 'gray'}>
|
|
1217
|
-
{row.active ? 'Active' : 'Inactive'}
|
|
1218
|
-
</Badge>
|
|
1219
|
-
)),
|
|
1220
|
-
col.text('role')
|
|
202
|
+
enableRowSelection
|
|
203
|
+
bulkActions={[
|
|
204
|
+
{
|
|
205
|
+
id: 'delete',
|
|
206
|
+
label: 'Delete selected',
|
|
207
|
+
confirm: { title: 'Confirm delete' },
|
|
208
|
+
onClick: (keys) => console.log(keys),
|
|
209
|
+
},
|
|
1221
210
|
]}
|
|
1222
|
-
rowKey="id"
|
|
1223
|
-
/>
|
|
1224
|
-
```
|
|
1225
|
-
|
|
1226
|
-
### Custom Row Key
|
|
1227
|
-
|
|
1228
|
-
```tsx
|
|
1229
|
-
// Using field name
|
|
1230
|
-
<RowaKitTable
|
|
1231
|
-
fetcher={fetchUsers}
|
|
1232
|
-
columns={[...]}
|
|
1233
|
-
rowKey="id"
|
|
1234
|
-
/>
|
|
1235
|
-
|
|
1236
|
-
// Using function
|
|
1237
|
-
<RowaKitTable
|
|
1238
211
|
fetcher={fetchUsers}
|
|
1239
|
-
columns={[...]}
|
|
1240
|
-
rowKey={(user) => `user-${user.id}`}
|
|
212
|
+
columns={[/* ... */]}
|
|
1241
213
|
/>
|
|
1242
214
|
```
|
|
1243
215
|
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
RowaKit Table includes minimal, customizable styling through CSS variables and a className prop.
|
|
1247
|
-
|
|
1248
|
-
### Import Styles
|
|
1249
|
-
|
|
1250
|
-
Import the default styles in your app's entry point:
|
|
1251
|
-
|
|
1252
|
-
```tsx
|
|
1253
|
-
// In your main.tsx or App.tsx
|
|
1254
|
-
import '@rowakit/table/styles';
|
|
1255
|
-
```
|
|
1256
|
-
|
|
1257
|
-
The table will now have sensible default styling with proper spacing, borders, hover states, and responsive behavior.
|
|
1258
|
-
|
|
1259
|
-
### Custom Styling via className
|
|
1260
|
-
|
|
1261
|
-
Override or extend styles using the `className` prop:
|
|
1262
|
-
|
|
1263
|
-
```tsx
|
|
1264
|
-
<RowaKitTable
|
|
1265
|
-
fetcher={fetchUsers}
|
|
1266
|
-
columns={[...]}
|
|
1267
|
-
className="my-custom-table"
|
|
1268
|
-
rowKey="id"
|
|
1269
|
-
/>
|
|
1270
|
-
```
|
|
1271
|
-
|
|
1272
|
-
```css
|
|
1273
|
-
/* Custom styles */
|
|
1274
|
-
.my-custom-table table {
|
|
1275
|
-
font-size: 0.875rem;
|
|
1276
|
-
}
|
|
1277
|
-
|
|
1278
|
-
.my-custom-table th {
|
|
1279
|
-
background: #f9fafb;
|
|
1280
|
-
text-transform: uppercase;
|
|
1281
|
-
}
|
|
1282
|
-
```
|
|
1283
|
-
|
|
1284
|
-
### Theming with CSS Variables
|
|
1285
|
-
|
|
1286
|
-
Customize the look and feel by overriding CSS variables (design tokens):
|
|
1287
|
-
|
|
1288
|
-
```css
|
|
1289
|
-
:root {
|
|
1290
|
-
/* Colors */
|
|
1291
|
-
--rowakit-color-primary-600: #2563eb; /* Primary buttons, links */
|
|
1292
|
-
--rowakit-color-gray-100: #f3f4f6; /* Table header background */
|
|
1293
|
-
--rowakit-color-gray-200: #e5e7eb; /* Borders */
|
|
1294
|
-
|
|
1295
|
-
/* Spacing */
|
|
1296
|
-
--rowakit-spacing-md: 0.75rem; /* Cell padding */
|
|
1297
|
-
--rowakit-spacing-lg: 1rem; /* Pagination padding */
|
|
1298
|
-
|
|
1299
|
-
/* Typography */
|
|
1300
|
-
--rowakit-font-size-sm: 0.875rem; /* Small text */
|
|
1301
|
-
--rowakit-font-size-base: 1rem; /* Default text */
|
|
1302
|
-
--rowakit-font-weight-semibold: 600; /* Header weight */
|
|
1303
|
-
|
|
1304
|
-
/* Borders */
|
|
1305
|
-
--rowakit-border-radius-md: 0.375rem; /* Button radius */
|
|
1306
|
-
--rowakit-border-width: 1px; /* Border thickness */
|
|
1307
|
-
}
|
|
1308
|
-
```
|
|
1309
|
-
|
|
1310
|
-
**Available Token Categories:**
|
|
1311
|
-
|
|
1312
|
-
- **Colors**: Neutral grays, primary blue, danger red, success green
|
|
1313
|
-
- **Spacing**: xs, sm, md, lg, xl, 2xl (0.25rem to 2rem)
|
|
1314
|
-
- **Typography**: Font families, sizes (xs to xl), weights (400-700), line heights
|
|
1315
|
-
- **Borders**: Widths, radius (sm to lg)
|
|
1316
|
-
- **Shadows**: sm, md, lg, xl for depth
|
|
1317
|
-
- **Z-index**: Modal and dropdown layering
|
|
1318
|
-
|
|
1319
|
-
### Responsive Design
|
|
1320
|
-
|
|
1321
|
-
The table automatically handles responsive behavior:
|
|
1322
|
-
|
|
1323
|
-
**Horizontal Scrolling:**
|
|
1324
|
-
- Tables wider than the viewport scroll horizontally
|
|
1325
|
-
- Webkit touch scrolling enabled for smooth mobile experience
|
|
1326
|
-
- Container uses `overflow-x: auto`
|
|
1327
|
-
|
|
1328
|
-
**Mobile Pagination:**
|
|
1329
|
-
- Pagination controls wrap on narrow screens (<640px)
|
|
1330
|
-
- Buttons and selectors stack vertically for better touch targets
|
|
1331
|
-
|
|
1332
|
-
```css
|
|
1333
|
-
/* Responsive breakpoint */
|
|
1334
|
-
@media (max-width: 640px) {
|
|
1335
|
-
.rowakit-table-pagination {
|
|
1336
|
-
flex-direction: column;
|
|
1337
|
-
gap: 1rem;
|
|
1338
|
-
}
|
|
1339
|
-
}
|
|
1340
|
-
```
|
|
1341
|
-
|
|
1342
|
-
### Dark Mode Support
|
|
1343
|
-
|
|
1344
|
-
Override tokens in a dark mode media query or class:
|
|
1345
|
-
|
|
1346
|
-
```css
|
|
1347
|
-
@media (prefers-color-scheme: dark) {
|
|
1348
|
-
:root {
|
|
1349
|
-
--rowakit-color-gray-50: #18181b;
|
|
1350
|
-
--rowakit-color-gray-100: #27272a;
|
|
1351
|
-
--rowakit-color-gray-200: #3f3f46;
|
|
1352
|
-
--rowakit-color-gray-800: #e4e4e7;
|
|
1353
|
-
--rowakit-color-gray-900: #f4f4f5;
|
|
1354
|
-
}
|
|
1355
|
-
}
|
|
1356
|
-
|
|
1357
|
-
/* Or with a class */
|
|
1358
|
-
.dark {
|
|
1359
|
-
--rowakit-color-gray-50: #18181b;
|
|
1360
|
-
/* ...other dark tokens */
|
|
1361
|
-
}
|
|
1362
|
-
```
|
|
1363
|
-
|
|
1364
|
-
### Advanced Customization
|
|
1365
|
-
|
|
1366
|
-
For complete control, you can skip the default styles and write your own:
|
|
1367
|
-
|
|
1368
|
-
```tsx
|
|
1369
|
-
// Don't import default styles
|
|
1370
|
-
// import '@rowakit/table/styles'; ❌
|
|
1371
|
-
|
|
1372
|
-
// Use your own styles with the base classes
|
|
1373
|
-
import './my-table-styles.css';
|
|
1374
|
-
|
|
1375
|
-
<RowaKitTable
|
|
1376
|
-
fetcher={fetchUsers}
|
|
1377
|
-
columns={[...]}
|
|
1378
|
-
className="my-completely-custom-table"
|
|
1379
|
-
rowKey="id"
|
|
1380
|
-
/>
|
|
1381
|
-
```
|
|
1382
|
-
|
|
1383
|
-
The table uses semantic class names:
|
|
1384
|
-
- `.rowakit-table` - Main container
|
|
1385
|
-
- `.rowakit-table-loading` - Loading state
|
|
1386
|
-
- `.rowakit-table-error` - Error state
|
|
1387
|
-
- `.rowakit-table-empty` - Empty state
|
|
1388
|
-
- `.rowakit-table-pagination` - Pagination container
|
|
1389
|
-
- `.rowakit-button` - Action buttons
|
|
1390
|
-
- `.rowakit-modal-backdrop` - Confirmation modal backdrop
|
|
1391
|
-
- `.rowakit-modal` - Confirmation modal
|
|
1392
|
-
|
|
1393
|
-
### Style Imports
|
|
216
|
+
---
|
|
1394
217
|
|
|
1395
|
-
|
|
218
|
+
## Export (CSV)
|
|
1396
219
|
|
|
1397
220
|
```tsx
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
// Both (same as '@rowakit/table/styles')
|
|
1405
|
-
import '@rowakit/table/styles/tokens.css';
|
|
1406
|
-
import '@rowakit/table/styles/table.css';
|
|
1407
|
-
```
|
|
1408
|
-
|
|
1409
|
-
## TypeScript
|
|
1410
|
-
|
|
1411
|
-
All components and helpers are fully typed with generics.
|
|
1412
|
-
|
|
1413
|
-
```typescript
|
|
1414
|
-
interface Product {
|
|
1415
|
-
id: number;
|
|
1416
|
-
name: string;
|
|
1417
|
-
price: number;
|
|
1418
|
-
category: string;
|
|
1419
|
-
inStock: boolean;
|
|
1420
|
-
}
|
|
221
|
+
const exporter = async (query) => {
|
|
222
|
+
const res = await fetch('/api/export', {
|
|
223
|
+
method: 'POST',
|
|
224
|
+
body: JSON.stringify(query),
|
|
225
|
+
});
|
|
1421
226
|
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
// query is FetcherQuery
|
|
1425
|
-
// must return FetcherResult<Product>
|
|
227
|
+
const { url } = await res.json();
|
|
228
|
+
return { url };
|
|
1426
229
|
};
|
|
1427
230
|
|
|
1428
|
-
|
|
1429
|
-
<RowaKitTable<Product>
|
|
1430
|
-
fetcher={fetchProducts}
|
|
1431
|
-
columns={[
|
|
1432
|
-
col.text<Product>('name'), // ✅ 'name' exists on Product
|
|
1433
|
-
col.text<Product>('invalid'), // ❌ TypeScript error
|
|
1434
|
-
]}
|
|
1435
|
-
/>
|
|
231
|
+
<RowaKitTable exporter={exporter} fetcher={fetchUsers} columns={[/* ... */]} />
|
|
1436
232
|
```
|
|
1437
233
|
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
### Server-Side First
|
|
1441
|
-
|
|
1442
|
-
All data operations (pagination, sorting, filtering) go through your backend. The table doesn't manage data locally.
|
|
1443
|
-
|
|
1444
|
-
### Minimal API
|
|
1445
|
-
|
|
1446
|
-
Few props, strong conventions. Most configuration happens through column definitions.
|
|
1447
|
-
|
|
1448
|
-
### Escape Hatch
|
|
1449
|
-
|
|
1450
|
-
`col.custom()` gives you full rendering control. We don't bloat the API with every possible column type.
|
|
1451
|
-
|
|
1452
|
-
### Type Safety
|
|
234
|
+
Export is server-triggered and scales well for large datasets.
|
|
1453
235
|
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
## Roadmap
|
|
1457
|
-
|
|
1458
|
-
**Stage A - MVP (v0.1)** ✅ Complete (2024-12-31)
|
|
1459
|
-
- ✅ A-01: Repo scaffold
|
|
1460
|
-
- ✅ A-02: Core types (Fetcher, ColumnDef, ActionDef)
|
|
1461
|
-
- ✅ A-03: Column helpers (col.*)
|
|
1462
|
-
- ✅ A-04: SmartTable component core rendering
|
|
1463
|
-
- ✅ A-05: Data fetching state machine (loading/error/empty/retry)
|
|
1464
|
-
- ✅ A-06: Pagination UI (page controls, page size selector)
|
|
1465
|
-
- ✅ A-07: Single-column sorting (click headers, sort indicators)
|
|
1466
|
-
- ✅ A-08: Actions with confirmation modal (confirm dialogs, disable, loading)
|
|
1467
|
-
- ✅ A-09: Minimal styling tokens (CSS variables, responsive, className)
|
|
1468
|
-
- ✅ A-10: Documentation & examples (4 complete examples, CHANGELOG, CONTRIBUTING)
|
|
1469
|
-
|
|
1470
|
-
**Stage B - Production Ready (v0.2.0-0.2.2)** ✅ Complete (2026-01-02)
|
|
1471
|
-
- ✅ Badge column type with visual tones (neutral, success, warning, danger)
|
|
1472
|
-
- ✅ Number column type with Intl formatting (currency, percentages, decimals)
|
|
1473
|
-
- ✅ Server-side header filter UI (type-specific inputs)
|
|
1474
|
-
- ✅ Column modifiers (width, align, truncate)
|
|
1475
|
-
- ✅ Fixed numeric filter value coercion
|
|
1476
|
-
- ✅ Production hardening and comprehensive documentation
|
|
1477
|
-
|
|
1478
|
-
**Stage C - Advanced Features (v0.4.0)** ✅ Complete (2026-01-03)
|
|
1479
|
-
- ✅ C-01: Column resizing with drag handles (minWidth/maxWidth constraints)
|
|
1480
|
-
- ✅ C-02: Saved views with localStorage persistence
|
|
1481
|
-
- ✅ C-02: URL state sync (page, pageSize, sort, filters, columnWidths)
|
|
1482
|
-
- ✅ C-03: Number range filters with optional filterTransform
|
|
1483
|
-
|
|
1484
|
-
**Stage D - Polish + Correctness (v0.4.0)** ✅ Complete (2026-01-05)
|
|
1485
|
-
- ✅ D-01: Prevent accidental sort while resizing (stopPropagation, suppression window)
|
|
1486
|
-
- ✅ D-02: Pointer Events resizing (mouse, touch, pen) with pointer capture and cleanup
|
|
1487
|
-
- ✅ D-03: Column width model hardening (apply widths to th+td, fixed layout, truncation)
|
|
1488
|
-
- ✅ D-04: Saved views persistence (index, hydration, corruption-safe)
|
|
1489
|
-
- ✅ D-05: URL sync hardening (validation, debounce, backward compatible)
|
|
1490
|
-
|
|
1491
|
-
See [ROADMAP.md](./docs/ROADMAP.md) and `docs/ROWAKIT_STAGE_D_ISSUES_v3.md` for implementation details and rationale.
|
|
236
|
+
---
|
|
1492
237
|
|
|
1493
|
-
##
|
|
238
|
+
## Roadmap & Versioning
|
|
1494
239
|
|
|
1495
|
-
|
|
240
|
+
* Current: **1.0.0** (stable)
|
|
241
|
+
* No breaking changes in 1.x (breaking changes require v2.0.0)
|
|
242
|
+
* Public API stability policy applies from v1.0.0
|
|
1496
243
|
|
|
1497
|
-
|
|
244
|
+
See roadmap: [docs/ROADMAP.md](docs/ROADMAP.md)
|
|
1498
245
|
|
|
1499
|
-
|
|
1500
|
-
- ✅ **Fixed**: Number filter type coercion for accurate field matching
|
|
1501
|
-
- ✅ **Production Ready**: All 193 tests passing, dependencies hardened
|
|
1502
|
-
- ✅ **Backwards Compatible**: No breaking changes from v0.2.0
|
|
246
|
+
---
|
|
1503
247
|
|
|
1504
|
-
|
|
1505
|
-
- Added `col.badge` and `col.number` column types
|
|
1506
|
-
- Column modifiers: `width`, `align`, `truncate`
|
|
1507
|
-
- Server-side header filter UI with type-specific inputs
|
|
1508
|
-
- Fixed numeric-filter value coercion bug (filter inputs now send numbers)
|
|
248
|
+
## Support RowaKit
|
|
1509
249
|
|
|
1510
|
-
|
|
250
|
+
If RowaKit helps your team:
|
|
1511
251
|
|
|
1512
|
-
|
|
252
|
+
* ⭐ Star the repo
|
|
253
|
+
* 💖 [Sponsor on GitHub](https://github.com/sponsors/midflow)
|
|
254
|
+
* ☕ [Buy us a coffee](https://buymeacoffee.com/midflow)
|
|
1513
255
|
|
|
1514
|
-
-
|
|
1515
|
-
- Date: 2026-01-02
|
|
1516
|
-
- Key additions:
|
|
1517
|
-
- `col.badge` — visual badge mapping for enum/status values with tone support
|
|
1518
|
-
- `col.number` — number column with Intl formatting and percentage/currency helpers
|
|
1519
|
-
- Column modifiers (`width`, `align`, `truncate`) supported across column types
|
|
1520
|
-
- Server-side filters: auto-generated, type-specific inputs in the header row
|
|
1521
|
-
- Fixes & hardening:
|
|
1522
|
-
- Removed direct React dependencies (now peerDependencies only)
|
|
1523
|
-
- Resolved numeric filter coercion and clarified backend contract in README
|
|
1524
|
-
- Deduplicated release checklist and improved demo documentation
|
|
256
|
+
Every bit of support helps sustain long-term maintenance.
|
|
1525
257
|
|
|
1526
|
-
|
|
258
|
+
---
|
|
1527
259
|
|
|
1528
260
|
## License
|
|
1529
261
|
|
|
1530
|
-
MIT
|
|
1531
|
-
|
|
1532
|
-
## Contributing
|
|
1533
|
-
|
|
1534
|
-
Contributions welcome! Please read our [contributing guidelines](CONTRIBUTING.md) first.
|
|
262
|
+
MIT © RowaKit Contributors
|
|
1535
263
|
|
|
1536
264
|
---
|
|
1537
265
|
|
|
1538
|
-
|
|
1539
|
-
|
|
266
|
+
**Built for teams shipping internal tools, not demos.**
|