@rowakit/table 0.3.0 → 0.5.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,28 +1,56 @@
1
1
  # @rowakit/table
2
2
 
3
- **Opinionated, server-side-first table component for internal/business apps**
3
+ **Server-side-first React table for internal & business applications**
4
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.
5
+ RowaKit Table is an **opinionated React table component** designed for real-world internal apps and admin dashboards. It assumes **all data operations live on the server** and provides a clean, predictable API optimized for CRUD-style business screens.
6
+
7
+ > If you are looking for a spreadsheet-like data grid with virtualization, grouping, or pivoting, this is intentionally **not** that library.
8
+
9
+ ---
10
+
11
+ ## Why RowaKit Table?
12
+
13
+ Most React table libraries are **client-first** and optimized for maximum flexibility. RowaKit takes the opposite approach:
14
+
15
+ * ✅ Server-side pagination, sorting, filtering by default
16
+ * ✅ Minimal, convention-driven API (less boilerplate)
17
+ * ✅ Strong TypeScript contracts between UI and backend
18
+ * ✅ Built for long-lived internal tools, not demo-heavy grids
19
+
20
+ This makes RowaKit especially suitable for:
21
+
22
+ * Admin dashboards
23
+ * Back-office / internal tools
24
+ * B2B SaaS management screens
25
+ * Enterprise CRUD applications
26
+
27
+ ---
6
28
 
7
29
  ## Features
8
30
 
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.3.0+)
18
- ✅ **Saved views** - Save/load table state with localStorage persistence (v0.3.0+)
19
- **URL state sync** - Share URLs with exact table configuration (v0.3.0+)
20
- **Number range filters** - Min/max filtering for numeric columns (v0.3.0+)
21
- **State management** - Automatic loading, error, and empty states
22
- **Smart fetching** - Retry on error, stale request handling
31
+ * 🚀 **Server-side first** pagination, sorting, filtering handled by your backend
32
+ * 🎯 **Type-safe** full TypeScript support with generics
33
+ * 🧠 **Minimal API** convention over configuration
34
+ * 🪝 **Escape hatch** `col.custom()` for full rendering control
35
+ * 🎛️ **7 column types** text, number, date, boolean, badge, actions, custom
36
+ * 🖱️ **Column resizing** drag handles, min/max width, double-click auto-fit (v0.4.0+)
37
+ * 📌 **Saved views** persist table state to localStorage (v0.4.0+)
38
+ * 🔗 **URL sync** share exact table state via query string (v0.4.0+)
39
+ * 🧮 **Number range filters** min/max with optional value transforms
40
+ * ✅ **Row selection** select/deselect rows with bulk header checkbox (v0.5.0+)
41
+ * 🎬 **Bulk actions** execute operations on multiple selected rows (v0.5.0+)
42
+ * 💾 **CSV export** – server-triggered export with customizable formatter (v0.5.0+)
43
+ * 🔄 **Multi-column sorting** Ctrl+Click to sort by multiple columns with priority (v0.5.0+)
44
+ * **Accessibility** ARIA labels, keyboard navigation, focus management (v0.5.0+)
45
+ * 🔄 **Smart fetching** – retry on error, stale request protection
46
+ * ✅ **Built-in states** – loading, error, empty handled automatically
47
+
48
+ ---
23
49
 
24
50
  ## Installation
25
51
 
52
+ RowaKit Table is published on npm and works with **npm**, **pnpm**, or **yarn**.
53
+
26
54
  ```bash
27
55
  npm install @rowakit/table
28
56
  # or
@@ -31,1506 +59,201 @@ pnpm add @rowakit/table
31
59
  yarn add @rowakit/table
32
60
  ```
33
61
 
62
+ ---
63
+
34
64
  ## Quick Start (5 minutes)
35
65
 
36
- ### 1. Import the component and styles
66
+ ### 1. Import
37
67
 
38
68
  ```tsx
39
69
  import { RowaKitTable, col } from '@rowakit/table';
40
70
  import type { Fetcher } from '@rowakit/table';
41
- import '@rowakit/table/styles'; // Import default styles
71
+ import '@rowakit/table/styles';
72
+ ```
73
+
74
+ ### 2. Define a fetcher (server contract)
42
75
 
76
+ ```tsx
43
77
  interface User {
44
78
  id: string;
45
79
  name: string;
46
80
  email: string;
47
- createdAt: Date;
48
81
  active: boolean;
49
82
  }
50
- ```
51
-
52
- ### 2. Create a fetcher function
53
83
 
54
- Your fetcher connects the table to your backend API:
55
-
56
- ```tsx
57
- const fetchUsers: Fetcher<User> = async (query) => {
84
+ const fetchUsers: Fetcher<User> = async ({ page, pageSize, sort, filters }) => {
58
85
  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
- }),
86
+ page: String(page),
87
+ pageSize: String(pageSize),
65
88
  });
66
89
 
67
- const response = await fetch(`/api/users?${params}`);
68
- if (!response.ok) {
69
- throw new Error('Failed to fetch users');
90
+ if (sort) {
91
+ params.set('sortBy', sort.field);
92
+ params.set('sortDir', sort.direction);
70
93
  }
71
- return response.json(); // Must return: { items: User[], total: number }
94
+
95
+ const res = await fetch(`/api/users?${params}`);
96
+ if (!res.ok) throw new Error('Failed to fetch users');
97
+
98
+ return res.json(); // { items: User[], total: number }
72
99
  };
73
100
  ```
74
101
 
75
- ### 3. Define your columns and render the table
102
+ ### 3. Render the table
76
103
 
77
104
  ```tsx
78
105
  function UsersTable() {
79
106
  return (
80
107
  <RowaKitTable
81
108
  fetcher={fetchUsers}
109
+ rowKey="id"
82
110
  columns={[
83
111
  col.text('name', { header: 'Name', sortable: true }),
84
112
  col.text('email', { header: 'Email' }),
85
- col.date('createdAt', { header: 'Created' }),
86
113
  col.boolean('active', { header: 'Active' }),
87
114
  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
- ])
115
+ { id: 'edit', label: 'Edit', onClick: (row) => console.log(row) },
116
+ { id: 'delete', label: 'Delete', confirm: true },
117
+ ]),
91
118
  ]}
92
- defaultPageSize={20}
93
- rowKey="id"
94
119
  />
95
120
  );
96
121
  }
97
122
  ```
98
123
 
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
124
+ Thats it — loading, error, pagination, sorting, and retry are handled automatically.
112
125
 
113
126
  ---
114
127
 
115
- ## API Reference
116
-
117
- ### `<RowaKitTable>`
118
-
119
- Main table component.
120
-
121
- #### Props
122
-
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 |
128
+ ## Core Concepts
131
129
 
132
130
  ### Fetcher Contract
133
131
 
134
- The `Fetcher` is the primary contract for loading data. It receives query parameters and returns paginated results.
135
-
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
140
-
141
- ```typescript
142
- type Fetcher<T> = (query: FetcherQuery) => Promise<FetcherResult<T>>;
132
+ The **Fetcher** defines the contract between the table and your backend.
143
133
 
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
- };
134
+ ```ts
135
+ type Fetcher<T> = (query: {
136
+ page: number;
137
+ pageSize: number;
138
+ sort?: { field: string; direction: 'asc' | 'desc' };
151
139
  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
- }
140
+ }) => Promise<{ items: T[]; total: number }>;
158
141
  ```
159
142
 
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
- ```
143
+ * Fetcher is called on mount and whenever table state changes
144
+ * Throw an error to trigger the built-in error + retry UI
145
+ * Stale requests are ignored automatically
175
146
 
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
147
+ ---
190
148
 
191
- Use `col.*` helpers to define columns with minimal boilerplate.
149
+ ## Column API
192
150
 
193
- #### `col.text(field, options?)`
151
+ RowaKit provides a **column factory API** via `col.*` helpers.
194
152
 
195
- Text column with optional formatting.
153
+ ### Basic columns
196
154
 
197
- ```typescript
155
+ ```ts
198
156
  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
157
+ col.number('price', { format: { style: 'currency', currency: 'USD' } })
158
+ col.date('createdAt', { sortable: true })
232
159
  col.boolean('active')
233
- col.boolean('isPublished', { header: 'Published' })
234
- col.boolean('enabled', { format: (val) => val ? '✓' : '✗' })
235
160
  ```
236
161
 
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'`
162
+ ### Badge column (enum/status)
243
163
 
244
- #### `col.badge(field, options?)` (v0.2.0+)
245
-
246
- Badge column with visual tone mapping for status/enum values.
247
-
248
- ```typescript
164
+ ```ts
249
165
  col.badge('status', {
250
166
  header: 'Status',
167
+ sortable: true,
251
168
  map: {
252
169
  active: { label: 'Active', tone: 'success' },
253
170
  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
- })
171
+ error: { label: 'Error', tone: 'danger' },
172
+ },
173
+ });
310
174
  ```
311
175
 
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)`
176
+ ### Actions column
325
177
 
326
- Actions column with buttons for row operations.
327
-
328
- ```typescript
178
+ ```ts
329
179
  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
- ])
180
+ { id: 'edit', label: 'Edit', onClick: (row) => edit(row) },
181
+ { id: 'delete', label: 'Delete', confirm: true, onClick: (row) => remove(row) },
182
+ ]);
339
183
  ```
340
184
 
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)`
185
+ ### Custom column (escape hatch)
351
186
 
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
187
+ ```tsx
363
188
  col.custom('user', (row) => (
364
- <div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
365
- <img src={row.avatar} alt="" width={32} height={32} />
189
+ <div style={{ display: 'flex', gap: 8 }}>
190
+ <img src={row.avatar} width={24} />
366
191
  <span>{row.name}</span>
367
192
  </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
- />
193
+ ));
497
194
  ```
498
195
 
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
- ])
747
- ```
748
-
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 |
843
-
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
858
-
859
196
  ---
860
197
 
861
- ## Advanced Features (v0.3.0+)
862
-
863
- ### Column Resizing (C-01)
864
-
865
- Enable users to resize columns by dragging the right edge of column headers.
198
+ ## Advanced Features (v0.4.0+)
866
199
 
867
- **Basic Usage:**
200
+ ### Column Resizing
868
201
 
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
- ```
202
+ * Drag handle on header edge
203
+ * Min/max width constraints
204
+ * Double-click to auto-fit content
205
+ * Pointer Events (mouse / touch / pen)
206
+ * No accidental sort while resizing
950
207
 
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
1035
-
1036
- ```tsx
1037
- // Simple sortable columns
1038
- <RowaKitTable
1039
- 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"
1089
- />
1090
- ```
1091
-
1092
- ### With Actions
1093
-
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
- ```
1125
-
1126
- **Note:** Action buttons are automatically disabled during table loading state.
1127
-
1128
- ### Error Handling and Retry
1129
-
1130
- ```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
- }
1145
- };
1146
-
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
1151
- ```
208
+ ### Saved Views + URL Sync
1152
209
 
1153
- ### With Pagination
210
+ * Persist page, sort, filters, and column widths
211
+ * Share URLs that restore exact table state
212
+ * Named views saved to localStorage
213
+ * Safe parsing & corruption tolerance
1154
214
 
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
- ```
1191
-
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`
1197
-
1198
- ### With Custom Columns
1199
-
1200
- ```tsx
1201
- <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')
1221
- ]}
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
- fetcher={fetchUsers}
1239
- columns={[...]}
1240
- rowKey={(user) => `user-${user.id}`}
1241
- />
1242
- ```
215
+ ---
1243
216
 
1244
217
  ## Styling
1245
218
 
1246
- RowaKit Table includes minimal, customizable styling through CSS variables and a className prop.
1247
-
1248
- ### Import Styles
219
+ RowaKit ships with minimal default styles via CSS variables.
1249
220
 
1250
- Import the default styles in your app's entry point:
1251
-
1252
- ```tsx
1253
- // In your main.tsx or App.tsx
221
+ ```ts
1254
222
  import '@rowakit/table/styles';
1255
223
  ```
1256
224
 
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
225
+ You can:
1392
226
 
1393
- ### Style Imports
1394
-
1395
- You can also import tokens and table styles separately:
1396
-
1397
- ```tsx
1398
- // Just tokens (CSS variables only)
1399
- import '@rowakit/table/styles/tokens.css';
227
+ * Override CSS variables for theming
228
+ * Use `className` to scope custom styles
229
+ * Skip default styles and fully style from scratch
1400
230
 
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
- }
1421
-
1422
- // Fetcher is typed
1423
- const fetchProducts: Fetcher<Product> = async (query) => {
1424
- // query is FetcherQuery
1425
- // must return FetcherResult<Product>
1426
- };
1427
-
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
- />
1436
- ```
1437
-
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
1453
-
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.3.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 - Future** (Demand-Driven)
1485
- - Multi-column sorting
1486
- - Row selection + bulk actions
1487
- - Export CSV (server-triggered)
1488
- - Column visibility toggle
1489
-
1490
- ## Changelog
1491
-
1492
- See the detailed changelog for release history and migration notes:
231
+ ---
1493
232
 
1494
- - [CHANGELOG.md](./CHANGELOG.md) highlights and details for v0.2.2 and future releases.
233
+ ## Philosophy & Scope
1495
234
 
1496
- ### v0.2.2 - Hardening Release (2026-01-02)
1497
- - **Fixed**: Number filter type coercion for accurate field matching
1498
- - **Production Ready**: All 193 tests passing, dependencies hardened
1499
- - **Backwards Compatible**: No breaking changes from v0.2.0
235
+ * **Server-side first** client stays thin
236
+ * **Small core** no grid bloat
237
+ * **Clear escape hatch** `col.custom()`
238
+ * **Business tables spreadsheets**
1500
239
 
1501
- ### v0.2.0 - Stage B Features (2026-01-02)
1502
- - Added `col.badge` and `col.number` column types
1503
- - Column modifiers: `width`, `align`, `truncate`
1504
- - Server-side header filter UI with type-specific inputs
1505
- - Fixed numeric-filter value coercion bug (filter inputs now send numbers)
240
+ See the scope lock and rationale in the root repository docs.
1506
241
 
1507
- ## Release Notes — v0.2.0 (2026-01-02)
242
+ ---
1508
243
 
1509
- This release introduces Stage B features and several hardening fixes to make `@rowakit/table` production-ready for internal apps.
244
+ ## Versioning & Roadmap
1510
245
 
1511
- - Release: `v0.2.0`
1512
- - Date: 2026-01-02
1513
- - Key additions:
1514
- - `col.badge` visual badge mapping for enum/status values with tone support
1515
- - `col.number` — number column with Intl formatting and percentage/currency helpers
1516
- - Column modifiers (`width`, `align`, `truncate`) supported across column types
1517
- - Server-side filters: auto-generated, type-specific inputs in the header row
1518
- - Fixes & hardening:
1519
- - Removed direct React dependencies (now peerDependencies only)
1520
- - Resolved numeric filter coercion and clarified backend contract in README
1521
- - Deduplicated release checklist and improved demo documentation
246
+ * Current: **v0.5.x** (Stage E – row selection, bulk actions, export, multi-sort, a11y)
247
+ * API is stable; patches are backward compatible
248
+ * Completed: Stages A-E with full feature set for internal business applications
249
+ * See [CHANGELOG.md](./CHANGELOG.md) for detailed v0.5.0 features and [docs/ROADMAP.md](../../docs/ROADMAP.md)
1522
250
 
1523
- See the full changelog for migration notes and detailed descriptions: [CHANGELOG.md](./CHANGELOG.md)
251
+ ---
1524
252
 
1525
253
  ## License
1526
254
 
1527
255
  MIT
1528
256
 
1529
- ## Contributing
1530
-
1531
- Contributions welcome! Please read our [contributing guidelines](CONTRIBUTING.md) first.
1532
-
1533
257
  ---
1534
258
 
1535
- Made with ❤️ by the RowaKit team
1536
-
259
+ Built for teams shipping serious internal tools, not toy demos.