@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 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
+