@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 +137 -1414
- package/dist/index.cjs +1256 -428
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +49 -4
- package/dist/index.d.ts +49 -4
- package/dist/index.js +1258 -430
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/styles/table.css +56 -1
package/README.md
CHANGED
|
@@ -1,28 +1,56 @@
|
|
|
1
1
|
# @rowakit/table
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
**Server-side-first React table for internal & business applications**
|
|
4
4
|
|
|
5
|
-
RowaKit Table is
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
✅ **
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
|
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';
|
|
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
|
-
|
|
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:
|
|
60
|
-
pageSize:
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
90
|
+
if (sort) {
|
|
91
|
+
params.set('sortBy', sort.field);
|
|
92
|
+
params.set('sortDir', sort.direction);
|
|
70
93
|
}
|
|
71
|
-
|
|
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.
|
|
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: (
|
|
89
|
-
{ id: 'delete', label: 'Delete', confirm: true
|
|
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
|
-
|
|
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
|
+
That’s it — loading, error, pagination, sorting, and retry are handled automatically.
|
|
112
125
|
|
|
113
126
|
---
|
|
114
127
|
|
|
115
|
-
##
|
|
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
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
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
|
-
|
|
149
|
+
## Column API
|
|
192
150
|
|
|
193
|
-
|
|
151
|
+
RowaKit provides a **column factory API** via `col.*` helpers.
|
|
194
152
|
|
|
195
|
-
|
|
153
|
+
### Basic columns
|
|
196
154
|
|
|
197
|
-
```
|
|
155
|
+
```ts
|
|
198
156
|
col.text('name')
|
|
199
|
-
col.
|
|
200
|
-
col.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
255
|
-
|
|
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
|
-
|
|
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
|
-
|
|
327
|
-
|
|
328
|
-
```typescript
|
|
178
|
+
```ts
|
|
329
179
|
col.actions([
|
|
330
|
-
{ id: 'edit', label: 'Edit', onClick: (row) =>
|
|
331
|
-
{
|
|
332
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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',
|
|
365
|
-
<img src={row.avatar}
|
|
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.
|
|
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
|
-
|
|
200
|
+
### Column Resizing
|
|
868
201
|
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1247
|
-
|
|
1248
|
-
### Import Styles
|
|
219
|
+
RowaKit ships with minimal default styles via CSS variables.
|
|
1249
220
|
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
```tsx
|
|
1253
|
-
// In your main.tsx or App.tsx
|
|
221
|
+
```ts
|
|
1254
222
|
import '@rowakit/table/styles';
|
|
1255
223
|
```
|
|
1256
224
|
|
|
1257
|
-
|
|
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
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
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
|
-
|
|
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
|
-
|
|
233
|
+
## Philosophy & Scope
|
|
1495
234
|
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
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
|
-
|
|
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
|
-
|
|
242
|
+
---
|
|
1508
243
|
|
|
1509
|
-
|
|
244
|
+
## Versioning & Roadmap
|
|
1510
245
|
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
-
|
|
1514
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1536
|
-
|
|
259
|
+
Built for teams shipping serious internal tools, not toy demos.
|