@rowakit/table 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1073 -0
- package/dist/index.cjs +385 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +389 -0
- package/dist/index.d.ts +389 -0
- package/dist/index.js +380 -0
- package/dist/index.js.map +1 -0
- package/package.json +73 -0
- package/src/styles/index.css +18 -0
- package/src/styles/table.css +319 -0
- package/src/styles/tokens.css +177 -0
package/README.md
ADDED
|
@@ -0,0 +1,1073 @@
|
|
|
1
|
+
# @rowakit/table
|
|
2
|
+
|
|
3
|
+
**Opinionated, server-side-first table component for internal/business apps**
|
|
4
|
+
|
|
5
|
+
RowaKit Table is a React table component designed for real-world internal applications. It prioritizes server-side operations, clean APIs, and developer experience over configuration complexity.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
✅ **Server-side first** - Pagination, sorting, and filtering handled by your backend
|
|
10
|
+
✅ **Type-safe** - Full TypeScript support with generics
|
|
11
|
+
✅ **Minimal API** - Few props, convention over configuration
|
|
12
|
+
✅ **Escape hatch** - `col.custom()` for any rendering need
|
|
13
|
+
✅ **Action buttons** - Built-in support for row actions with confirmation
|
|
14
|
+
✅ **5 column types** - Text, Date, Boolean, Actions, Custom
|
|
15
|
+
✅ **State management** - Automatic loading, error, and empty states
|
|
16
|
+
✅ **Smart fetching** - Retry on error, stale request handling
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install @rowakit/table
|
|
22
|
+
# or
|
|
23
|
+
pnpm add @rowakit/table
|
|
24
|
+
# or
|
|
25
|
+
yarn add @rowakit/table
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Quick Start (5 minutes)
|
|
29
|
+
|
|
30
|
+
### 1. Import the component and styles
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
import { RowaKitTable, col } from '@rowakit/table';
|
|
34
|
+
import type { Fetcher } from '@rowakit/table';
|
|
35
|
+
import '@rowakit/table/styles'; // Import default styles
|
|
36
|
+
|
|
37
|
+
interface User {
|
|
38
|
+
id: string;
|
|
39
|
+
name: string;
|
|
40
|
+
email: string;
|
|
41
|
+
createdAt: Date;
|
|
42
|
+
active: boolean;
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### 2. Create a fetcher function
|
|
47
|
+
|
|
48
|
+
Your fetcher connects the table to your backend API:
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
const fetchUsers: Fetcher<User> = async (query) => {
|
|
52
|
+
const params = new URLSearchParams({
|
|
53
|
+
page: query.page.toString(),
|
|
54
|
+
pageSize: query.pageSize.toString(),
|
|
55
|
+
...(query.sort && {
|
|
56
|
+
sortField: query.sort.field,
|
|
57
|
+
sortDir: query.sort.direction,
|
|
58
|
+
}),
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const response = await fetch(`/api/users?${params}`);
|
|
62
|
+
if (!response.ok) {
|
|
63
|
+
throw new Error('Failed to fetch users');
|
|
64
|
+
}
|
|
65
|
+
return response.json(); // Must return: { items: User[], total: number }
|
|
66
|
+
};
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 3. Define your columns and render the table
|
|
70
|
+
|
|
71
|
+
```tsx
|
|
72
|
+
function UsersTable() {
|
|
73
|
+
return (
|
|
74
|
+
<RowaKitTable
|
|
75
|
+
fetcher={fetchUsers}
|
|
76
|
+
columns={[
|
|
77
|
+
col.text('name', { header: 'Name', sortable: true }),
|
|
78
|
+
col.text('email', { header: 'Email' }),
|
|
79
|
+
col.date('createdAt', { header: 'Created' }),
|
|
80
|
+
col.boolean('active', { header: 'Active' }),
|
|
81
|
+
col.actions([
|
|
82
|
+
{ id: 'edit', label: 'Edit', onClick: (user) => console.log('Edit:', user) },
|
|
83
|
+
{ id: 'delete', label: 'Delete', confirm: true, onClick: (user) => console.log('Delete:', user) }
|
|
84
|
+
])
|
|
85
|
+
]}
|
|
86
|
+
defaultPageSize={20}
|
|
87
|
+
rowKey="id"
|
|
88
|
+
/>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### That's it! 🎉
|
|
94
|
+
|
|
95
|
+
The table automatically handles:
|
|
96
|
+
- Fetches data on mount
|
|
97
|
+
- Shows loading state while fetching
|
|
98
|
+
- Displays error with retry button on failure
|
|
99
|
+
- Shows "No data" when empty
|
|
100
|
+
- Renders your data when successful
|
|
101
|
+
|
|
102
|
+
**Next Steps:**
|
|
103
|
+
- See [examples/](./examples/) for complete patterns (mock server, custom columns, styling)
|
|
104
|
+
- Read the [API Reference](#api-reference) below for all features
|
|
105
|
+
- Check [CONTRIBUTING.md](./CONTRIBUTING.md) to contribute
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## API Reference
|
|
110
|
+
|
|
111
|
+
### `<RowaKitTable>`
|
|
112
|
+
|
|
113
|
+
Main table component.
|
|
114
|
+
|
|
115
|
+
#### Props
|
|
116
|
+
|
|
117
|
+
| Prop | Type | Required | Default | Description |
|
|
118
|
+
|------|------|----------|---------|-------------|
|
|
119
|
+
| `fetcher` | `Fetcher<T>` | ✅ Yes | - | Function to fetch data from server |
|
|
120
|
+
| `columns` | `ColumnDef<T>[]` | ✅ Yes | - | Array of column definitions |
|
|
121
|
+
| `rowKey` | `keyof T \| (row: T) => string` | No | `'id'` | Field or function to extract unique row key |
|
|
122
|
+
| `defaultPageSize` | `number` | No | `20` | Initial page size |
|
|
123
|
+
| `pageSizeOptions` | `number[]` | No | `[10, 20, 50]` | Available page size options |
|
|
124
|
+
| `className` | `string` | No | `''` | Additional CSS classes for the table container |
|
|
125
|
+
|
|
126
|
+
### Fetcher Contract
|
|
127
|
+
|
|
128
|
+
The `Fetcher` is the primary contract for loading data. It receives query parameters and returns paginated results.
|
|
129
|
+
|
|
130
|
+
**The fetcher is called automatically:**
|
|
131
|
+
- On component mount
|
|
132
|
+
- When query parameters change (page, pageSize, sort, filters)
|
|
133
|
+
- When retry button is clicked after an error
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
type Fetcher<T> = (query: FetcherQuery) => Promise<FetcherResult<T>>;
|
|
137
|
+
|
|
138
|
+
interface FetcherQuery {
|
|
139
|
+
page: number; // Current page (1-based)
|
|
140
|
+
pageSize: number; // Items per page
|
|
141
|
+
sort?: {
|
|
142
|
+
field: string; // Field to sort by
|
|
143
|
+
direction: 'asc' | 'desc';
|
|
144
|
+
};
|
|
145
|
+
filters?: Record<string, unknown>;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
interface FetcherResult<T> {
|
|
149
|
+
items: T[]; // Array of items for current page
|
|
150
|
+
total: number; // Total number of items across all pages
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
**Error handling:**
|
|
155
|
+
|
|
156
|
+
Throw an error from your fetcher to trigger the error state with retry button:
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
const fetchUsers: Fetcher<User> = async (query) => {
|
|
160
|
+
const response = await fetch(`/api/users?page=${query.page}`);
|
|
161
|
+
|
|
162
|
+
if (!response.ok) {
|
|
163
|
+
throw new Error('Failed to load users');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return response.json();
|
|
167
|
+
};
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
**State Management:**
|
|
171
|
+
|
|
172
|
+
The table automatically manages these states:
|
|
173
|
+
|
|
174
|
+
- **Loading** - Shows "Loading..." while fetcher is executing
|
|
175
|
+
- **Success** - Renders table with data when items.length > 0
|
|
176
|
+
- **Empty** - Shows "No data" when items.length === 0
|
|
177
|
+
- **Error** - Shows error message with "Retry" button when fetcher throws
|
|
178
|
+
|
|
179
|
+
**Stale Request Handling:**
|
|
180
|
+
|
|
181
|
+
The table automatically ignores responses from outdated requests, ensuring the UI always shows data from the most recent query.
|
|
182
|
+
|
|
183
|
+
### Column Helpers
|
|
184
|
+
|
|
185
|
+
Use `col.*` helpers to define columns with minimal boilerplate.
|
|
186
|
+
|
|
187
|
+
#### `col.text(field, options?)`
|
|
188
|
+
|
|
189
|
+
Text column with optional formatting.
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
col.text('name')
|
|
193
|
+
col.text('email', { header: 'Email Address', sortable: true })
|
|
194
|
+
col.text('status', { format: (val) => val.toUpperCase() })
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
**Options:**
|
|
198
|
+
- `header?: string` - Custom header label
|
|
199
|
+
- `sortable?: boolean` - Enable sorting
|
|
200
|
+
- `format?: (value: unknown) => string` - Custom formatter
|
|
201
|
+
|
|
202
|
+
#### `col.date(field, options?)`
|
|
203
|
+
|
|
204
|
+
Date column with optional formatting.
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
col.date('createdAt')
|
|
208
|
+
col.date('updatedAt', { header: 'Last Modified', sortable: true })
|
|
209
|
+
col.date('birthday', {
|
|
210
|
+
format: (date) => new Date(date).toLocaleDateString('en-US')
|
|
211
|
+
})
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
**Options:**
|
|
215
|
+
- `header?: string` - Custom header label
|
|
216
|
+
- `sortable?: boolean` - Enable sorting
|
|
217
|
+
- `format?: (value: Date | string | number) => string` - Custom date formatter
|
|
218
|
+
|
|
219
|
+
**Default format:** `date.toLocaleDateString()`
|
|
220
|
+
|
|
221
|
+
#### `col.boolean(field, options?)`
|
|
222
|
+
|
|
223
|
+
Boolean column with optional formatting.
|
|
224
|
+
|
|
225
|
+
```typescript
|
|
226
|
+
col.boolean('active')
|
|
227
|
+
col.boolean('isPublished', { header: 'Published' })
|
|
228
|
+
col.boolean('enabled', { format: (val) => val ? '✓' : '✗' })
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
**Options:**
|
|
232
|
+
- `header?: string` - Custom header label
|
|
233
|
+
- `sortable?: boolean` - Enable sorting
|
|
234
|
+
- `format?: (value: boolean) => string` - Custom formatter
|
|
235
|
+
|
|
236
|
+
**Default format:** `'Yes'` / `'No'`
|
|
237
|
+
|
|
238
|
+
#### `col.actions(actions)`
|
|
239
|
+
|
|
240
|
+
Actions column with buttons for row operations.
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
col.actions([
|
|
244
|
+
{ id: 'edit', label: 'Edit', onClick: (row) => editItem(row) },
|
|
245
|
+
{
|
|
246
|
+
id: 'delete',
|
|
247
|
+
label: 'Delete',
|
|
248
|
+
confirm: true,
|
|
249
|
+
icon: '🗑️',
|
|
250
|
+
onClick: (row) => deleteItem(row)
|
|
251
|
+
}
|
|
252
|
+
])
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
**ActionDef properties:**
|
|
256
|
+
- `id: string` - Unique action identifier
|
|
257
|
+
- `label: string` - Button label
|
|
258
|
+
- `onClick: (row: T) => void | Promise<void>` - Action handler
|
|
259
|
+
- `icon?: string | ReactNode` - Optional icon
|
|
260
|
+
- `confirm?: boolean` - Show confirmation dialog
|
|
261
|
+
- `disabled?: boolean | ((row: T) => boolean)` - Disable button
|
|
262
|
+
- `loading?: boolean` - Show loading state
|
|
263
|
+
|
|
264
|
+
#### `col.custom(field, render)`
|
|
265
|
+
|
|
266
|
+
Custom column with full rendering control. **This is your escape hatch.**
|
|
267
|
+
|
|
268
|
+
```typescript
|
|
269
|
+
// Badge
|
|
270
|
+
col.custom('status', (row) => (
|
|
271
|
+
<Badge color={row.status === 'active' ? 'green' : 'gray'}>
|
|
272
|
+
{row.status}
|
|
273
|
+
</Badge>
|
|
274
|
+
))
|
|
275
|
+
|
|
276
|
+
// Avatar + Name
|
|
277
|
+
col.custom('user', (row) => (
|
|
278
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
|
279
|
+
<img src={row.avatar} alt="" width={32} height={32} />
|
|
280
|
+
<span>{row.name}</span>
|
|
281
|
+
</div>
|
|
282
|
+
))
|
|
283
|
+
|
|
284
|
+
// Money formatting
|
|
285
|
+
col.custom('price', (row) => (
|
|
286
|
+
<Money amount={row.price} currency={row.currency} />
|
|
287
|
+
))
|
|
288
|
+
|
|
289
|
+
// Complex logic
|
|
290
|
+
col.custom('summary', (row) => {
|
|
291
|
+
const status = row.active ? 'Active' : 'Inactive';
|
|
292
|
+
const lastSeen = formatDistanceToNow(row.lastSeenAt);
|
|
293
|
+
return `${status} - ${lastSeen}`;
|
|
294
|
+
})
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
### Pagination
|
|
298
|
+
|
|
299
|
+
The table includes built-in pagination controls that appear automatically when data is loaded.
|
|
300
|
+
|
|
301
|
+
**Features:**
|
|
302
|
+
- **Page Size Selector** - Dropdown to change rows per page
|
|
303
|
+
- **Page Navigation** - Previous/Next buttons to navigate pages
|
|
304
|
+
- **Page Info** - Shows current page, total pages, and total items
|
|
305
|
+
- **Auto-disable** - Buttons disabled during loading or at boundaries
|
|
306
|
+
|
|
307
|
+
**Configuration:**
|
|
308
|
+
|
|
309
|
+
```typescript
|
|
310
|
+
<RowaKitTable
|
|
311
|
+
fetcher={fetchUsers}
|
|
312
|
+
columns={columns}
|
|
313
|
+
defaultPageSize={20} // Initial page size (default: 20)
|
|
314
|
+
pageSizeOptions={[10, 20, 50]} // Available sizes (default: [10, 20, 50])
|
|
315
|
+
rowKey="id"
|
|
316
|
+
/>
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
**Behavior:**
|
|
320
|
+
|
|
321
|
+
1. **On Page Change**: Fetcher called with new `page` value
|
|
322
|
+
2. **On Page Size Change**: Fetcher called with new `pageSize` and resets to `page: 1`
|
|
323
|
+
3. **During Loading**: All pagination controls are disabled
|
|
324
|
+
4. **No Data/Error**: Pagination controls are hidden
|
|
325
|
+
|
|
326
|
+
**Example Fetcher with Pagination:**
|
|
327
|
+
|
|
328
|
+
```typescript
|
|
329
|
+
const fetchUsers: Fetcher<User> = async ({ page, pageSize }) => {
|
|
330
|
+
const response = await fetch(
|
|
331
|
+
`/api/users?page=${page}&limit=${pageSize}`
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
const data = await response.json();
|
|
335
|
+
|
|
336
|
+
return {
|
|
337
|
+
items: data.users,
|
|
338
|
+
total: data.totalCount // Required for page calculation
|
|
339
|
+
};
|
|
340
|
+
};
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
**Pagination Math:**
|
|
344
|
+
|
|
345
|
+
- `totalPages = Math.ceil(total / pageSize)`
|
|
346
|
+
- Previous button disabled when `page === 1`
|
|
347
|
+
- Next button disabled when `page === totalPages`
|
|
348
|
+
- Changing page size resets to page 1 to avoid invalid page numbers
|
|
349
|
+
|
|
350
|
+
### Sorting
|
|
351
|
+
|
|
352
|
+
The table supports single-column sorting with automatic state management and visual indicators.
|
|
353
|
+
|
|
354
|
+
**Features:**
|
|
355
|
+
- **Click to Sort** - Click sortable column headers to toggle sort
|
|
356
|
+
- **Sort Indicators** - Visual arrows (↑ ↓) show current sort direction
|
|
357
|
+
- **Keyboard Accessible** - Sort with Enter or Space keys
|
|
358
|
+
- **Three-State Toggle** - None → Ascending → Descending → None
|
|
359
|
+
- **Auto Reset** - Page resets to 1 when sort changes
|
|
360
|
+
|
|
361
|
+
**Making Columns Sortable:**
|
|
362
|
+
|
|
363
|
+
Add `sortable: true` to column options:
|
|
364
|
+
|
|
365
|
+
```typescript
|
|
366
|
+
<RowaKitTable
|
|
367
|
+
fetcher={fetchUsers}
|
|
368
|
+
columns={[
|
|
369
|
+
col.text('name', { sortable: true }),
|
|
370
|
+
col.text('email', { sortable: true }),
|
|
371
|
+
col.date('createdAt', { sortable: true, header: 'Created' }),
|
|
372
|
+
col.boolean('active', { sortable: true }),
|
|
373
|
+
]}
|
|
374
|
+
rowKey="id"
|
|
375
|
+
/>
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
**Sort Behavior:**
|
|
379
|
+
|
|
380
|
+
1. **First Click**: Sort ascending (↑)
|
|
381
|
+
2. **Second Click**: Sort descending (↓)
|
|
382
|
+
3. **Third Click**: Remove sort (back to default order)
|
|
383
|
+
4. **Different Column**: Switches to new column, starts with ascending
|
|
384
|
+
|
|
385
|
+
**Fetcher Integration:**
|
|
386
|
+
|
|
387
|
+
The fetcher receives sort information in the query:
|
|
388
|
+
|
|
389
|
+
```typescript
|
|
390
|
+
const fetchUsers: Fetcher<User> = async ({ page, pageSize, sort }) => {
|
|
391
|
+
// sort is undefined when no sort is active
|
|
392
|
+
// sort = { field: 'name', direction: 'asc' | 'desc' } when sorting
|
|
393
|
+
|
|
394
|
+
const params = new URLSearchParams({
|
|
395
|
+
page: String(page),
|
|
396
|
+
limit: String(pageSize),
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
if (sort) {
|
|
400
|
+
params.append('sortBy', sort.field);
|
|
401
|
+
params.append('sortOrder', sort.direction);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const response = await fetch(`/api/users?${params}`);
|
|
405
|
+
return response.json();
|
|
406
|
+
};
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
**Which Columns Can Be Sorted:**
|
|
410
|
+
|
|
411
|
+
- ✅ Text columns with `sortable: true`
|
|
412
|
+
- ✅ Date columns with `sortable: true`
|
|
413
|
+
- ✅ Boolean columns with `sortable: true`
|
|
414
|
+
- ❌ Actions columns (never sortable)
|
|
415
|
+
- ❌ Custom columns (not sortable by default)
|
|
416
|
+
|
|
417
|
+
**Sort State:**
|
|
418
|
+
|
|
419
|
+
- Maintained when navigating pages
|
|
420
|
+
- Maintained when changing page size (but resets to page 1)
|
|
421
|
+
- Cleared when clicking a sorted column three times
|
|
422
|
+
- Replaced when clicking a different sortable column
|
|
423
|
+
|
|
424
|
+
### Actions
|
|
425
|
+
|
|
426
|
+
The table provides built-in support for row actions with confirmation dialogs, loading states, and conditional disabling.
|
|
427
|
+
|
|
428
|
+
**Features:**
|
|
429
|
+
- **Action Buttons** - Render buttons for each row (edit, delete, view, etc.)
|
|
430
|
+
- **Confirmation Dialogs** - Built-in modal for destructive actions
|
|
431
|
+
- **Loading States** - Disable actions while table is loading
|
|
432
|
+
- **Conditional Disabling** - Disable actions based on row data
|
|
433
|
+
- **Icons** - Support for string or React component icons
|
|
434
|
+
- **Async Support** - Action handlers can be async/Promise-based
|
|
435
|
+
|
|
436
|
+
**Basic Actions:**
|
|
437
|
+
|
|
438
|
+
```typescript
|
|
439
|
+
<RowaKitTable
|
|
440
|
+
fetcher={fetchUsers}
|
|
441
|
+
columns={[
|
|
442
|
+
col.text('name'),
|
|
443
|
+
col.text('email'),
|
|
444
|
+
col.actions([
|
|
445
|
+
{
|
|
446
|
+
id: 'edit',
|
|
447
|
+
label: 'Edit',
|
|
448
|
+
onClick: (row) => {
|
|
449
|
+
console.log('Editing:', row);
|
|
450
|
+
// Handle edit
|
|
451
|
+
}
|
|
452
|
+
},
|
|
453
|
+
{
|
|
454
|
+
id: 'view',
|
|
455
|
+
label: 'View Details',
|
|
456
|
+
icon: '👁️',
|
|
457
|
+
onClick: (row) => {
|
|
458
|
+
window.open(`/users/${row.id}`, '_blank');
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
])
|
|
462
|
+
]}
|
|
463
|
+
rowKey="id"
|
|
464
|
+
/>
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
**Actions with Confirmation:**
|
|
468
|
+
|
|
469
|
+
Use `confirm: true` to show a confirmation dialog before executing the action:
|
|
470
|
+
|
|
471
|
+
```typescript
|
|
472
|
+
col.actions([
|
|
473
|
+
{
|
|
474
|
+
id: 'delete',
|
|
475
|
+
label: 'Delete',
|
|
476
|
+
icon: '🗑️',
|
|
477
|
+
confirm: true, // Shows confirmation modal
|
|
478
|
+
onClick: async (row) => {
|
|
479
|
+
await deleteUser(row.id);
|
|
480
|
+
// Refetch data to update table
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
])
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
When the user clicks "Delete", a modal appears asking for confirmation:
|
|
487
|
+
- **Title:** "Confirm Action"
|
|
488
|
+
- **Message:** "Are you sure you want to delete? This action cannot be undone."
|
|
489
|
+
- **Buttons:** "Cancel" (gray) and "Confirm" (red)
|
|
490
|
+
|
|
491
|
+
**Conditional Disabling:**
|
|
492
|
+
|
|
493
|
+
Disable actions based on row data:
|
|
494
|
+
|
|
495
|
+
```typescript
|
|
496
|
+
col.actions([
|
|
497
|
+
{
|
|
498
|
+
id: 'delete',
|
|
499
|
+
label: 'Delete',
|
|
500
|
+
confirm: true,
|
|
501
|
+
// Disable for admin users
|
|
502
|
+
disabled: (row) => row.role === 'admin',
|
|
503
|
+
onClick: async (row) => {
|
|
504
|
+
await deleteUser(row.id);
|
|
505
|
+
}
|
|
506
|
+
},
|
|
507
|
+
{
|
|
508
|
+
id: 'edit',
|
|
509
|
+
label: 'Edit',
|
|
510
|
+
// Always disabled
|
|
511
|
+
disabled: true,
|
|
512
|
+
onClick: (row) => editUser(row)
|
|
513
|
+
}
|
|
514
|
+
])
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
**Loading States:**
|
|
518
|
+
|
|
519
|
+
Set `loading: true` to disable an action button and show loading state:
|
|
520
|
+
|
|
521
|
+
```typescript
|
|
522
|
+
const [deletingId, setDeletingId] = useState<string | null>(null);
|
|
523
|
+
|
|
524
|
+
col.actions([
|
|
525
|
+
{
|
|
526
|
+
id: 'delete',
|
|
527
|
+
label: 'Delete',
|
|
528
|
+
loading: deletingId === row.id, // Disable while deleting
|
|
529
|
+
onClick: async (row) => {
|
|
530
|
+
setDeletingId(row.id);
|
|
531
|
+
await deleteUser(row.id);
|
|
532
|
+
setDeletingId(null);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
])
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
**Multiple Actions:**
|
|
539
|
+
|
|
540
|
+
You can combine multiple actions in one column:
|
|
541
|
+
|
|
542
|
+
```typescript
|
|
543
|
+
col.actions([
|
|
544
|
+
{
|
|
545
|
+
id: 'edit',
|
|
546
|
+
label: 'Edit',
|
|
547
|
+
icon: '✏️',
|
|
548
|
+
onClick: (row) => editUser(row)
|
|
549
|
+
},
|
|
550
|
+
{
|
|
551
|
+
id: 'view',
|
|
552
|
+
label: 'View',
|
|
553
|
+
icon: '👁️',
|
|
554
|
+
onClick: (row) => viewUser(row)
|
|
555
|
+
},
|
|
556
|
+
{
|
|
557
|
+
id: 'delete',
|
|
558
|
+
label: 'Delete',
|
|
559
|
+
icon: '🗑️',
|
|
560
|
+
confirm: true,
|
|
561
|
+
disabled: (row) => row.role === 'admin',
|
|
562
|
+
onClick: async (row) => {
|
|
563
|
+
await deleteUser(row.id);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
])
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
**Action Properties:**
|
|
570
|
+
|
|
571
|
+
| Property | Type | Required | Description |
|
|
572
|
+
|----------|------|----------|-------------|
|
|
573
|
+
| `id` | `string` | ✅ Yes | Unique identifier for the action |
|
|
574
|
+
| `label` | `string` | ✅ Yes | Button label text |
|
|
575
|
+
| `onClick` | `(row: T) => void \| Promise<void>` | ✅ Yes | Action handler (called after confirmation if required) |
|
|
576
|
+
| `icon` | `string \| ReactNode` | No | Icon displayed before label (emoji or React component) |
|
|
577
|
+
| `confirm` | `boolean` | No | Show confirmation dialog before executing (default: `false`) |
|
|
578
|
+
| `disabled` | `boolean \| (row: T) => boolean` | No | Disable button (static or per-row function) |
|
|
579
|
+
| `loading` | `boolean` | No | Show loading state and disable button |
|
|
580
|
+
|
|
581
|
+
**Best Practices:**
|
|
582
|
+
|
|
583
|
+
1. **Use Confirmation for Destructive Actions** - Always set `confirm: true` for delete, remove, or irreversible actions
|
|
584
|
+
2. **Provide Visual Feedback** - Use icons to make actions more recognizable
|
|
585
|
+
3. **Handle Async Properly** - Mark action handlers as `async` and handle errors
|
|
586
|
+
4. **Disable Appropriately** - Use `disabled` function to prevent invalid actions
|
|
587
|
+
5. **Keep Actions Minimal** - Limit to 2-3 primary actions per row; use a dropdown menu for many actions
|
|
588
|
+
|
|
589
|
+
**Automatic Behavior:**
|
|
590
|
+
|
|
591
|
+
- All actions are disabled while the table is loading data
|
|
592
|
+
- Confirmation modal prevents accidental clicks on backdrop (must click Cancel or Confirm)
|
|
593
|
+
- Keyboard accessible (Tab to focus, Enter/Space to activate)
|
|
594
|
+
- Actions column is never sortable
|
|
595
|
+
|
|
596
|
+
## Examples
|
|
597
|
+
|
|
598
|
+
### Basic Table
|
|
599
|
+
|
|
600
|
+
```tsx
|
|
601
|
+
<RowaKitTable
|
|
602
|
+
fetcher={fetchUsers}
|
|
603
|
+
columns={[
|
|
604
|
+
col.text('name'),
|
|
605
|
+
col.text('email'),
|
|
606
|
+
col.boolean('active')
|
|
607
|
+
]}
|
|
608
|
+
rowKey="id"
|
|
609
|
+
/>
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
### With Sorting
|
|
613
|
+
|
|
614
|
+
```tsx
|
|
615
|
+
// Simple sortable columns
|
|
616
|
+
<RowaKitTable
|
|
617
|
+
fetcher={fetchUsers}
|
|
618
|
+
columns={[
|
|
619
|
+
col.text('name', { sortable: true }),
|
|
620
|
+
col.text('email', { sortable: true }),
|
|
621
|
+
col.date('createdAt', { sortable: true, header: 'Created' })
|
|
622
|
+
]}
|
|
623
|
+
rowKey="id"
|
|
624
|
+
/>
|
|
625
|
+
|
|
626
|
+
// Fetcher with sort handling
|
|
627
|
+
const fetchUsers: Fetcher<User> = async ({ page, pageSize, sort }) => {
|
|
628
|
+
const params = new URLSearchParams({
|
|
629
|
+
page: String(page),
|
|
630
|
+
limit: String(pageSize),
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
if (sort) {
|
|
634
|
+
params.append('sortBy', sort.field);
|
|
635
|
+
params.append('sortOrder', sort.direction);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
const response = await fetch(`/api/users?${params}`);
|
|
639
|
+
return response.json();
|
|
640
|
+
};
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
**Sort behavior:**
|
|
644
|
+
- Click header once: sort ascending (↑)
|
|
645
|
+
- Click again: sort descending (↓)
|
|
646
|
+
- Click third time: remove sort
|
|
647
|
+
- Sortable headers have pointer cursor and are keyboard accessible
|
|
648
|
+
|
|
649
|
+
### With Custom Formatting
|
|
650
|
+
|
|
651
|
+
```tsx
|
|
652
|
+
<RowaKitTable
|
|
653
|
+
fetcher={fetchProducts}
|
|
654
|
+
columns={[
|
|
655
|
+
col.text('name'),
|
|
656
|
+
col.text('category', {
|
|
657
|
+
format: (val) => String(val).toUpperCase()
|
|
658
|
+
}),
|
|
659
|
+
col.date('createdAt', {
|
|
660
|
+
format: (date) => new Date(date).toLocaleDateString()
|
|
661
|
+
}),
|
|
662
|
+
col.boolean('inStock', {
|
|
663
|
+
format: (val) => val ? '✓ In Stock' : '✗ Out of Stock'
|
|
664
|
+
})
|
|
665
|
+
]}
|
|
666
|
+
rowKey="id"
|
|
667
|
+
/>
|
|
668
|
+
```
|
|
669
|
+
|
|
670
|
+
### With Actions
|
|
671
|
+
|
|
672
|
+
```tsx
|
|
673
|
+
<RowaKitTable
|
|
674
|
+
fetcher={fetchUsers}
|
|
675
|
+
columns={[
|
|
676
|
+
col.text('name'),
|
|
677
|
+
col.text('email'),
|
|
678
|
+
col.actions([
|
|
679
|
+
{
|
|
680
|
+
id: 'view',
|
|
681
|
+
label: 'View',
|
|
682
|
+
onClick: (user) => navigate(`/users/${user.id}`)
|
|
683
|
+
},
|
|
684
|
+
{
|
|
685
|
+
id: 'edit',
|
|
686
|
+
label: 'Edit',
|
|
687
|
+
onClick: (user) => openEditModal(user)
|
|
688
|
+
},
|
|
689
|
+
{
|
|
690
|
+
id: 'delete',
|
|
691
|
+
label: 'Delete',
|
|
692
|
+
confirm: true,
|
|
693
|
+
onClick: async (user) => {
|
|
694
|
+
await deleteUser(user.id);
|
|
695
|
+
// Trigger refetch by updating query (A-06 will add this)
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
])
|
|
699
|
+
]}
|
|
700
|
+
rowKey="id"
|
|
701
|
+
/>
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
**Note:** Action buttons are automatically disabled during table loading state.
|
|
705
|
+
|
|
706
|
+
### Error Handling and Retry
|
|
707
|
+
|
|
708
|
+
```tsx
|
|
709
|
+
// Fetcher with error handling
|
|
710
|
+
const fetchUsers: Fetcher<User> = async (query) => {
|
|
711
|
+
try {
|
|
712
|
+
const response = await fetch(`/api/users?page=${query.page}`);
|
|
713
|
+
|
|
714
|
+
if (!response.ok) {
|
|
715
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
return response.json();
|
|
719
|
+
} catch (error) {
|
|
720
|
+
// Network error or parsing error
|
|
721
|
+
throw new Error('Failed to connect to server');
|
|
722
|
+
}
|
|
723
|
+
};
|
|
724
|
+
|
|
725
|
+
// The table will:
|
|
726
|
+
// 1. Show error message in the UI
|
|
727
|
+
// 2. Display a "Retry" button
|
|
728
|
+
// 3. Call the fetcher again with the same query when retry is clicked
|
|
729
|
+
```
|
|
730
|
+
|
|
731
|
+
### With Pagination
|
|
732
|
+
|
|
733
|
+
```tsx
|
|
734
|
+
// Basic pagination with defaults
|
|
735
|
+
<RowaKitTable
|
|
736
|
+
fetcher={fetchUsers}
|
|
737
|
+
columns={[
|
|
738
|
+
col.text('name'),
|
|
739
|
+
col.text('email'),
|
|
740
|
+
col.date('createdAt')
|
|
741
|
+
]}
|
|
742
|
+
rowKey="id"
|
|
743
|
+
// Uses default: defaultPageSize={20}, pageSizeOptions={[10, 20, 50]}
|
|
744
|
+
/>
|
|
745
|
+
|
|
746
|
+
// Custom pagination settings
|
|
747
|
+
<RowaKitTable
|
|
748
|
+
fetcher={fetchUsers}
|
|
749
|
+
columns={columns}
|
|
750
|
+
defaultPageSize={25}
|
|
751
|
+
pageSizeOptions={[25, 50, 100, 200]}
|
|
752
|
+
rowKey="id"
|
|
753
|
+
/>
|
|
754
|
+
|
|
755
|
+
// Fetcher receives page and pageSize
|
|
756
|
+
const fetchUsers: Fetcher<User> = async ({ page, pageSize }) => {
|
|
757
|
+
const offset = (page - 1) * pageSize;
|
|
758
|
+
const response = await fetch(
|
|
759
|
+
`/api/users?offset=${offset}&limit=${pageSize}`
|
|
760
|
+
);
|
|
761
|
+
const data = await response.json();
|
|
762
|
+
|
|
763
|
+
return {
|
|
764
|
+
items: data.users,
|
|
765
|
+
total: data.totalCount // Required for pagination
|
|
766
|
+
};
|
|
767
|
+
};
|
|
768
|
+
```
|
|
769
|
+
|
|
770
|
+
**Pagination automatically:**
|
|
771
|
+
- Hides when `total === 0` (no data or error)
|
|
772
|
+
- Disables controls during loading
|
|
773
|
+
- Resets to page 1 when page size changes
|
|
774
|
+
- Calculates total pages from `total / pageSize`
|
|
775
|
+
|
|
776
|
+
### With Custom Columns
|
|
777
|
+
|
|
778
|
+
```tsx
|
|
779
|
+
<RowaKitTable
|
|
780
|
+
fetcher={fetchUsers}
|
|
781
|
+
columns={[
|
|
782
|
+
col.custom('user', (row) => (
|
|
783
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
|
784
|
+
<Avatar src={row.avatar} size="sm" />
|
|
785
|
+
<div>
|
|
786
|
+
<div style={{ fontWeight: 600 }}>{row.name}</div>
|
|
787
|
+
<div style={{ fontSize: '0.875rem', color: '#6b7280' }}>
|
|
788
|
+
{row.email}
|
|
789
|
+
</div>
|
|
790
|
+
</div>
|
|
791
|
+
</div>
|
|
792
|
+
)),
|
|
793
|
+
col.custom('status', (row) => (
|
|
794
|
+
<Badge color={row.active ? 'green' : 'gray'}>
|
|
795
|
+
{row.active ? 'Active' : 'Inactive'}
|
|
796
|
+
</Badge>
|
|
797
|
+
)),
|
|
798
|
+
col.text('role')
|
|
799
|
+
]}
|
|
800
|
+
rowKey="id"
|
|
801
|
+
/>
|
|
802
|
+
```
|
|
803
|
+
|
|
804
|
+
### Custom Row Key
|
|
805
|
+
|
|
806
|
+
```tsx
|
|
807
|
+
// Using field name
|
|
808
|
+
<RowaKitTable
|
|
809
|
+
fetcher={fetchUsers}
|
|
810
|
+
columns={[...]}
|
|
811
|
+
rowKey="id"
|
|
812
|
+
/>
|
|
813
|
+
|
|
814
|
+
// Using function
|
|
815
|
+
<RowaKitTable
|
|
816
|
+
fetcher={fetchUsers}
|
|
817
|
+
columns={[...]}
|
|
818
|
+
rowKey={(user) => `user-${user.id}`}
|
|
819
|
+
/>
|
|
820
|
+
```
|
|
821
|
+
|
|
822
|
+
## Styling
|
|
823
|
+
|
|
824
|
+
RowaKit Table includes minimal, customizable styling through CSS variables and a className prop.
|
|
825
|
+
|
|
826
|
+
### Import Styles
|
|
827
|
+
|
|
828
|
+
Import the default styles in your app's entry point:
|
|
829
|
+
|
|
830
|
+
```tsx
|
|
831
|
+
// In your main.tsx or App.tsx
|
|
832
|
+
import '@rowakit/table/styles';
|
|
833
|
+
```
|
|
834
|
+
|
|
835
|
+
The table will now have sensible default styling with proper spacing, borders, hover states, and responsive behavior.
|
|
836
|
+
|
|
837
|
+
### Custom Styling via className
|
|
838
|
+
|
|
839
|
+
Override or extend styles using the `className` prop:
|
|
840
|
+
|
|
841
|
+
```tsx
|
|
842
|
+
<RowaKitTable
|
|
843
|
+
fetcher={fetchUsers}
|
|
844
|
+
columns={[...]}
|
|
845
|
+
className="my-custom-table"
|
|
846
|
+
rowKey="id"
|
|
847
|
+
/>
|
|
848
|
+
```
|
|
849
|
+
|
|
850
|
+
```css
|
|
851
|
+
/* Custom styles */
|
|
852
|
+
.my-custom-table table {
|
|
853
|
+
font-size: 0.875rem;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
.my-custom-table th {
|
|
857
|
+
background: #f9fafb;
|
|
858
|
+
text-transform: uppercase;
|
|
859
|
+
}
|
|
860
|
+
```
|
|
861
|
+
|
|
862
|
+
### Theming with CSS Variables
|
|
863
|
+
|
|
864
|
+
Customize the look and feel by overriding CSS variables (design tokens):
|
|
865
|
+
|
|
866
|
+
```css
|
|
867
|
+
:root {
|
|
868
|
+
/* Colors */
|
|
869
|
+
--rowakit-color-primary-600: #2563eb; /* Primary buttons, links */
|
|
870
|
+
--rowakit-color-gray-100: #f3f4f6; /* Table header background */
|
|
871
|
+
--rowakit-color-gray-200: #e5e7eb; /* Borders */
|
|
872
|
+
|
|
873
|
+
/* Spacing */
|
|
874
|
+
--rowakit-spacing-md: 0.75rem; /* Cell padding */
|
|
875
|
+
--rowakit-spacing-lg: 1rem; /* Pagination padding */
|
|
876
|
+
|
|
877
|
+
/* Typography */
|
|
878
|
+
--rowakit-font-size-sm: 0.875rem; /* Small text */
|
|
879
|
+
--rowakit-font-size-base: 1rem; /* Default text */
|
|
880
|
+
--rowakit-font-weight-semibold: 600; /* Header weight */
|
|
881
|
+
|
|
882
|
+
/* Borders */
|
|
883
|
+
--rowakit-border-radius-md: 0.375rem; /* Button radius */
|
|
884
|
+
--rowakit-border-width: 1px; /* Border thickness */
|
|
885
|
+
}
|
|
886
|
+
```
|
|
887
|
+
|
|
888
|
+
**Available Token Categories:**
|
|
889
|
+
|
|
890
|
+
- **Colors**: Neutral grays, primary blue, danger red, success green
|
|
891
|
+
- **Spacing**: xs, sm, md, lg, xl, 2xl (0.25rem to 2rem)
|
|
892
|
+
- **Typography**: Font families, sizes (xs to xl), weights (400-700), line heights
|
|
893
|
+
- **Borders**: Widths, radius (sm to lg)
|
|
894
|
+
- **Shadows**: sm, md, lg, xl for depth
|
|
895
|
+
- **Z-index**: Modal and dropdown layering
|
|
896
|
+
|
|
897
|
+
### Responsive Design
|
|
898
|
+
|
|
899
|
+
The table automatically handles responsive behavior:
|
|
900
|
+
|
|
901
|
+
**Horizontal Scrolling:**
|
|
902
|
+
- Tables wider than the viewport scroll horizontally
|
|
903
|
+
- Webkit touch scrolling enabled for smooth mobile experience
|
|
904
|
+
- Container uses `overflow-x: auto`
|
|
905
|
+
|
|
906
|
+
**Mobile Pagination:**
|
|
907
|
+
- Pagination controls wrap on narrow screens (<640px)
|
|
908
|
+
- Buttons and selectors stack vertically for better touch targets
|
|
909
|
+
|
|
910
|
+
```css
|
|
911
|
+
/* Responsive breakpoint */
|
|
912
|
+
@media (max-width: 640px) {
|
|
913
|
+
.rowakit-table-pagination {
|
|
914
|
+
flex-direction: column;
|
|
915
|
+
gap: 1rem;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
```
|
|
919
|
+
|
|
920
|
+
### Dark Mode Support
|
|
921
|
+
|
|
922
|
+
Override tokens in a dark mode media query or class:
|
|
923
|
+
|
|
924
|
+
```css
|
|
925
|
+
@media (prefers-color-scheme: dark) {
|
|
926
|
+
:root {
|
|
927
|
+
--rowakit-color-gray-50: #18181b;
|
|
928
|
+
--rowakit-color-gray-100: #27272a;
|
|
929
|
+
--rowakit-color-gray-200: #3f3f46;
|
|
930
|
+
--rowakit-color-gray-800: #e4e4e7;
|
|
931
|
+
--rowakit-color-gray-900: #f4f4f5;
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
/* Or with a class */
|
|
936
|
+
.dark {
|
|
937
|
+
--rowakit-color-gray-50: #18181b;
|
|
938
|
+
/* ...other dark tokens */
|
|
939
|
+
}
|
|
940
|
+
```
|
|
941
|
+
|
|
942
|
+
### Advanced Customization
|
|
943
|
+
|
|
944
|
+
For complete control, you can skip the default styles and write your own:
|
|
945
|
+
|
|
946
|
+
```tsx
|
|
947
|
+
// Don't import default styles
|
|
948
|
+
// import '@rowakit/table/styles'; ❌
|
|
949
|
+
|
|
950
|
+
// Use your own styles with the base classes
|
|
951
|
+
import './my-table-styles.css';
|
|
952
|
+
|
|
953
|
+
<RowaKitTable
|
|
954
|
+
fetcher={fetchUsers}
|
|
955
|
+
columns={[...]}
|
|
956
|
+
className="my-completely-custom-table"
|
|
957
|
+
rowKey="id"
|
|
958
|
+
/>
|
|
959
|
+
```
|
|
960
|
+
|
|
961
|
+
The table uses semantic class names:
|
|
962
|
+
- `.rowakit-table` - Main container
|
|
963
|
+
- `.rowakit-table-loading` - Loading state
|
|
964
|
+
- `.rowakit-table-error` - Error state
|
|
965
|
+
- `.rowakit-table-empty` - Empty state
|
|
966
|
+
- `.rowakit-table-pagination` - Pagination container
|
|
967
|
+
- `.rowakit-button` - Action buttons
|
|
968
|
+
- `.rowakit-modal-backdrop` - Confirmation modal backdrop
|
|
969
|
+
- `.rowakit-modal` - Confirmation modal
|
|
970
|
+
|
|
971
|
+
### Style Imports
|
|
972
|
+
|
|
973
|
+
You can also import tokens and table styles separately:
|
|
974
|
+
|
|
975
|
+
```tsx
|
|
976
|
+
// Just tokens (CSS variables only)
|
|
977
|
+
import '@rowakit/table/styles/tokens.css';
|
|
978
|
+
|
|
979
|
+
// Just table styles (uses tokens)
|
|
980
|
+
import '@rowakit/table/styles/table.css';
|
|
981
|
+
|
|
982
|
+
// Both (same as '@rowakit/table/styles')
|
|
983
|
+
import '@rowakit/table/styles/tokens.css';
|
|
984
|
+
import '@rowakit/table/styles/table.css';
|
|
985
|
+
```
|
|
986
|
+
|
|
987
|
+
## TypeScript
|
|
988
|
+
|
|
989
|
+
All components and helpers are fully typed with generics.
|
|
990
|
+
|
|
991
|
+
```typescript
|
|
992
|
+
interface Product {
|
|
993
|
+
id: number;
|
|
994
|
+
name: string;
|
|
995
|
+
price: number;
|
|
996
|
+
category: string;
|
|
997
|
+
inStock: boolean;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
// Fetcher is typed
|
|
1001
|
+
const fetchProducts: Fetcher<Product> = async (query) => {
|
|
1002
|
+
// query is FetcherQuery
|
|
1003
|
+
// must return FetcherResult<Product>
|
|
1004
|
+
};
|
|
1005
|
+
|
|
1006
|
+
// Columns are type-checked
|
|
1007
|
+
<RowaKitTable<Product>
|
|
1008
|
+
fetcher={fetchProducts}
|
|
1009
|
+
columns={[
|
|
1010
|
+
col.text<Product>('name'), // ✅ 'name' exists on Product
|
|
1011
|
+
col.text<Product>('invalid'), // ❌ TypeScript error
|
|
1012
|
+
]}
|
|
1013
|
+
/>
|
|
1014
|
+
```
|
|
1015
|
+
|
|
1016
|
+
## Principles
|
|
1017
|
+
|
|
1018
|
+
### Server-Side First
|
|
1019
|
+
|
|
1020
|
+
All data operations (pagination, sorting, filtering) go through your backend. The table doesn't manage data locally.
|
|
1021
|
+
|
|
1022
|
+
### Minimal API
|
|
1023
|
+
|
|
1024
|
+
Few props, strong conventions. Most configuration happens through column definitions.
|
|
1025
|
+
|
|
1026
|
+
### Escape Hatch
|
|
1027
|
+
|
|
1028
|
+
`col.custom()` gives you full rendering control. We don't bloat the API with every possible column type.
|
|
1029
|
+
|
|
1030
|
+
### Type Safety
|
|
1031
|
+
|
|
1032
|
+
Full TypeScript support. Your data model drives type checking throughout.
|
|
1033
|
+
|
|
1034
|
+
## Roadmap
|
|
1035
|
+
|
|
1036
|
+
**Stage A - MVP 0.1** ✅ Complete (2024-12-31)
|
|
1037
|
+
- ✅ A-01: Repo scaffold
|
|
1038
|
+
- ✅ A-02: Core types (Fetcher, ColumnDef, ActionDef)
|
|
1039
|
+
- ✅ A-03: Column helpers (col.*)
|
|
1040
|
+
- ✅ A-04: SmartTable component core rendering
|
|
1041
|
+
- ✅ A-05: Data fetching state machine (loading/error/empty/retry)
|
|
1042
|
+
- ✅ A-06: Pagination UI (page controls, page size selector)
|
|
1043
|
+
- ✅ A-07: Single-column sorting (click headers, sort indicators)
|
|
1044
|
+
- ✅ A-08: Actions with confirmation modal (confirm dialogs, disable, loading)
|
|
1045
|
+
- ✅ A-09: Minimal styling tokens (CSS variables, responsive, className)
|
|
1046
|
+
- ✅ A-10: Documentation & examples (4 complete examples, CHANGELOG, CONTRIBUTING)
|
|
1047
|
+
|
|
1048
|
+
**Stage B - Production Ready** (Next)
|
|
1049
|
+
- Column visibility toggle
|
|
1050
|
+
- Bulk actions
|
|
1051
|
+
- Search/text filter
|
|
1052
|
+
- Export (CSV, Excel)
|
|
1053
|
+
- Dense/comfortable view modes
|
|
1054
|
+
- Additional column types (badge, number)
|
|
1055
|
+
|
|
1056
|
+
**Stage C - Advanced (Demand-Driven)** (Future)
|
|
1057
|
+
- Multi-column sorting
|
|
1058
|
+
- Advanced filters
|
|
1059
|
+
- Column resizing
|
|
1060
|
+
- Saved views
|
|
1061
|
+
|
|
1062
|
+
## License
|
|
1063
|
+
|
|
1064
|
+
MIT
|
|
1065
|
+
|
|
1066
|
+
## Contributing
|
|
1067
|
+
|
|
1068
|
+
Contributions welcome! Please read our [contributing guidelines](CONTRIBUTING.md) first.
|
|
1069
|
+
|
|
1070
|
+
---
|
|
1071
|
+
|
|
1072
|
+
Made with ❤️ by the RowaKit team
|
|
1073
|
+
|