@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 CHANGED
@@ -1,25 +1,29 @@
1
1
  # @rowakit/table
2
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
- ✅ **7 column types** - Text, Date, Boolean, Badge, Number, Actions, Custom
15
- ✅ **Column modifiers** - Width, align, truncate, minWidth, maxWidth (v0.2.0+)
16
- **Server-side filters** - Type-specific filter UI with auto-generated inputs (v0.2.0+)
17
- ✅ **Column resizing** - Drag-to-resize handles with constraints (v0.4.0+)
18
- **Saved views** - Save/load table state with localStorage persistence (v0.4.0+)
19
- **URL state sync** - Share URLs with exact table configuration (v0.4.0+)
20
- ✅ **Number range filters** - Min/max filtering for numeric columns (v0.4.0+)
21
- **State management** - Automatic loading, error, and empty states
22
- **Smart fetching** - Retry on error, stale request handling
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
- ## Quick Start (5 minutes)
38
+ Import base styles:
35
39
 
36
- ### 1. Import the component and styles
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'; // Import default 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
- Your fetcher connects the table to your backend API:
53
+ type User = { id: string; name: string; email: string; active: boolean };
55
54
 
56
- ```tsx
57
- const fetchUsers: Fetcher<User> = async (query) => {
55
+ const fetchUsers: Fetcher<User> = async ({ page, pageSize, sort }) => {
58
56
  const params = new URLSearchParams({
59
- page: query.page.toString(),
60
- pageSize: query.pageSize.toString(),
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
- const response = await fetch(`/api/users?${params}`);
68
- if (!response.ok) {
69
- throw new Error('Failed to fetch users');
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
- ### 3. Define your columns and render the table
66
+ const res = await fetch(`/api/users?${params}`);
67
+ if (!res.ok) throw new Error('Failed to fetch users');
76
68
 
77
- ```tsx
78
- function UsersTable() {
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', onClick: (user) => console.log('Edit:', user) },
89
- { id: 'delete', label: 'Delete', confirm: true, onClick: (user) => console.log('Delete:', user) }
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
- ## API Reference
93
+ ## Features (v1.0.0)
116
94
 
117
- ### `<RowaKitTable>`
95
+ ### Core table
118
96
 
119
- Main table component.
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
- #### Props
102
+ ### Columns
122
103
 
123
- | Prop | Type | Required | Default | Description |
124
- |------|------|----------|---------|-------------|
125
- | `fetcher` | `Fetcher<T>` | ✅ Yes | - | Function to fetch data from server |
126
- | `columns` | `ColumnDef<T>[]` | ✅ Yes | - | Array of column definitions |
127
- | `rowKey` | `keyof T \| (row: T) => string` | No | `'id'` | Field or function to extract unique row key |
128
- | `defaultPageSize` | `number` | No | `20` | Initial page size |
129
- | `pageSizeOptions` | `number[]` | No | `[10, 20, 50]` | Available page size options |
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
- ### Fetcher Contract
112
+ ### UX & workflows
133
113
 
134
- The `Fetcher` is the primary contract for loading data. It receives query parameters and returns paginated results.
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
- **The fetcher is called automatically:**
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
- ```typescript
142
- type Fetcher<T> = (query: FetcherQuery) => Promise<FetcherResult<T>>;
124
+ ## Fetcher Contract
143
125
 
144
- interface FetcherQuery {
145
- page: number; // Current page (1-based)
146
- pageSize: number; // Items per page
147
- sort?: {
148
- field: string; // Field to sort by
149
- direction: 'asc' | 'desc';
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
- When the user clicks "Delete", a modal appears asking for confirmation:
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
- **Best Practices:**
845
-
846
- 1. **Use Confirmation for Destructive Actions** - Always set `confirm: true` for delete, remove, or irreversible actions
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
- ## Advanced Features (v0.4.0+)
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
- ### With Actions
157
+ * Selection is page-scoped
158
+ * Resets on page change
1093
159
 
1094
- ```tsx
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
- **Note:** Action buttons are automatically disabled during table loading state.
162
+ ## Multi-Column Sorting
1127
163
 
1128
- ### Error Handling and Retry
164
+ Sort by multiple columns simultaneously using **Ctrl+Click** (Windows/Linux) or **Cmd+Click** (Mac) on column headers:
1129
165
 
1130
166
  ```tsx
1131
- // Fetcher with error handling
1132
- const fetchUsers: Fetcher<User> = async (query) => {
1133
- try {
1134
- const response = await fetch(`/api/users?page=${query.page}`);
1135
-
1136
- if (!response.ok) {
1137
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
1138
- }
1139
-
1140
- return response.json();
1141
- } catch (error) {
1142
- // Network error or parsing error
1143
- throw new Error('Failed to connect to server');
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
- // The table will:
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
- ### With Pagination
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
- ```tsx
1156
- // Basic pagination with defaults
1157
- <RowaKitTable
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
- **Pagination automatically:**
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
- ### With Custom Columns
198
+ ## Bulk Actions
1199
199
 
1200
200
  ```tsx
1201
201
  <RowaKitTable
1202
- fetcher={fetchUsers}
1203
- columns={[
1204
- col.custom('user', (row) => (
1205
- <div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
1206
- <Avatar src={row.avatar} size="sm" />
1207
- <div>
1208
- <div style={{ fontWeight: 600 }}>{row.name}</div>
1209
- <div style={{ fontSize: '0.875rem', color: '#6b7280' }}>
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
- ## Styling
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
- You can also import tokens and table styles separately:
218
+ ## Export (CSV)
1396
219
 
1397
220
  ```tsx
1398
- // Just tokens (CSS variables only)
1399
- import '@rowakit/table/styles/tokens.css';
1400
-
1401
- // Just table styles (uses tokens)
1402
- import '@rowakit/table/styles/table.css';
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
- // Fetcher is typed
1423
- const fetchProducts: Fetcher<Product> = async (query) => {
1424
- // query is FetcherQuery
1425
- // must return FetcherResult<Product>
227
+ const { url } = await res.json();
228
+ return { url };
1426
229
  };
1427
230
 
1428
- // Columns are type-checked
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
- ## Principles
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
- Full TypeScript support. Your data model drives type checking throughout.
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
- ## Changelog
238
+ ## Roadmap & Versioning
1494
239
 
1495
- See the detailed changelog for release history and migration notes:
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
- - [CHANGELOG.md](./CHANGELOG.md) — highlights and details for v0.2.2 and future releases.
244
+ See roadmap: [docs/ROADMAP.md](docs/ROADMAP.md)
1498
245
 
1499
- ### v0.2.2 - Hardening Release (2026-01-02)
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
- ### v0.2.0 - Stage B Features (2026-01-02)
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
- ## Release Notes v0.2.0 (2026-01-02)
250
+ If RowaKit helps your team:
1511
251
 
1512
- This release introduces Stage B features and several hardening fixes to make `@rowakit/table` production-ready for internal apps.
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
- - Release: `v0.2.0`
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
- See the full changelog for migration notes and detailed descriptions: [CHANGELOG.md](./CHANGELOG.md)
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
- Made with ❤️ by the RowaKit team
1539
-
266
+ **Built for teams shipping internal tools, not demos.**