@rowakit/table 0.1.0 → 0.2.1
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 +296 -1
- package/dist/index.cjs +289 -33
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +144 -6
- package/dist/index.d.ts +144 -6
- package/dist/index.js +289 -33
- package/dist/index.js.map +1 -1
- package/package.json +5 -10
- package/src/styles/table.css +104 -0
package/README.md
CHANGED
|
@@ -11,7 +11,9 @@ RowaKit Table is a React table component designed for real-world internal applic
|
|
|
11
11
|
✅ **Minimal API** - Few props, convention over configuration
|
|
12
12
|
✅ **Escape hatch** - `col.custom()` for any rendering need
|
|
13
13
|
✅ **Action buttons** - Built-in support for row actions with confirmation
|
|
14
|
-
✅ **
|
|
14
|
+
✅ **7 column types** - Text, Date, Boolean, Badge, Number, Actions, Custom
|
|
15
|
+
✅ **Column modifiers** - Width, align, truncate support (v0.2.0+)
|
|
16
|
+
✅ **Server-side filters** - Type-specific filter UI with auto-generated inputs (v0.2.0+)
|
|
15
17
|
✅ **State management** - Automatic loading, error, and empty states
|
|
16
18
|
✅ **Smart fetching** - Retry on error, stale request handling
|
|
17
19
|
|
|
@@ -235,6 +237,86 @@ col.boolean('enabled', { format: (val) => val ? '✓' : '✗' })
|
|
|
235
237
|
|
|
236
238
|
**Default format:** `'Yes'` / `'No'`
|
|
237
239
|
|
|
240
|
+
#### `col.badge(field, options?)` (v0.2.0+)
|
|
241
|
+
|
|
242
|
+
Badge column with visual tone mapping for status/enum values.
|
|
243
|
+
|
|
244
|
+
```typescript
|
|
245
|
+
col.badge('status', {
|
|
246
|
+
header: 'Status',
|
|
247
|
+
map: {
|
|
248
|
+
active: { label: 'Active', tone: 'success' },
|
|
249
|
+
pending: { label: 'Pending', tone: 'warning' },
|
|
250
|
+
inactive: { label: 'Inactive', tone: 'neutral' },
|
|
251
|
+
error: { label: 'Error', tone: 'danger' }
|
|
252
|
+
}
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
col.badge('priority', {
|
|
256
|
+
header: 'Priority',
|
|
257
|
+
sortable: true,
|
|
258
|
+
map: {
|
|
259
|
+
high: { label: 'High', tone: 'danger' },
|
|
260
|
+
medium: { label: 'Medium', tone: 'warning' },
|
|
261
|
+
low: { label: 'Low', tone: 'success' }
|
|
262
|
+
}
|
|
263
|
+
})
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
**Options:**
|
|
267
|
+
- `header?: string` - Custom header label
|
|
268
|
+
- `sortable?: boolean` - Enable sorting
|
|
269
|
+
- `map?: Record<string, { label: string; tone: BadgeTone }>` - Map values to badge labels and visual tones
|
|
270
|
+
- `width?: number` - Column width in pixels
|
|
271
|
+
- `align?: 'left' | 'center' | 'right'` - Text alignment
|
|
272
|
+
- `truncate?: boolean` - Truncate with ellipsis
|
|
273
|
+
|
|
274
|
+
**Badge Tones:**
|
|
275
|
+
- `'neutral'` - Gray (default)
|
|
276
|
+
- `'success'` - Green
|
|
277
|
+
- `'warning'` - Yellow/Orange
|
|
278
|
+
- `'danger'` - Red
|
|
279
|
+
|
|
280
|
+
#### `col.number(field, options?)` (v0.2.0+)
|
|
281
|
+
|
|
282
|
+
Number column with formatting support (currency, percentages, decimals).
|
|
283
|
+
|
|
284
|
+
```typescript
|
|
285
|
+
// Basic number
|
|
286
|
+
col.number('quantity')
|
|
287
|
+
|
|
288
|
+
// Currency formatting with Intl.NumberFormat
|
|
289
|
+
col.number('price', {
|
|
290
|
+
header: 'Price',
|
|
291
|
+
sortable: true,
|
|
292
|
+
format: { style: 'currency', currency: 'USD' }
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
// Percentage
|
|
296
|
+
col.number('discount', {
|
|
297
|
+
header: 'Discount',
|
|
298
|
+
format: { style: 'percent', minimumFractionDigits: 1 }
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
// Custom formatter
|
|
302
|
+
col.number('score', {
|
|
303
|
+
header: 'Score',
|
|
304
|
+
format: (value) => `${value.toFixed(1)} pts`
|
|
305
|
+
})
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
**Options:**
|
|
309
|
+
- `header?: string` - Custom header label
|
|
310
|
+
- `sortable?: boolean` - Enable sorting
|
|
311
|
+
- `format?: Intl.NumberFormatOptions | ((value: number) => string)` - Formatting
|
|
312
|
+
- `Intl.NumberFormatOptions`: e.g., `{ style: 'currency', currency: 'USD' }`
|
|
313
|
+
- Function: Custom formatter like `(value) => value.toFixed(2)`
|
|
314
|
+
- `width?: number` - Column width in pixels
|
|
315
|
+
- `align?: 'left' | 'center' | 'right'` - Text alignment (defaults to 'right')
|
|
316
|
+
- `truncate?: boolean` - Truncate with ellipsis
|
|
317
|
+
|
|
318
|
+
**Default format:** Plain number with right alignment
|
|
319
|
+
|
|
238
320
|
#### `col.actions(actions)`
|
|
239
321
|
|
|
240
322
|
Actions column with buttons for row operations.
|
|
@@ -294,6 +376,41 @@ col.custom('summary', (row) => {
|
|
|
294
376
|
})
|
|
295
377
|
```
|
|
296
378
|
|
|
379
|
+
### Column Modifiers (v0.2.0+)
|
|
380
|
+
|
|
381
|
+
All column types (text, date, boolean, badge, number) support these optional modifiers:
|
|
382
|
+
|
|
383
|
+
```typescript
|
|
384
|
+
// Width: Set fixed column width in pixels
|
|
385
|
+
col.text('name', { width: 200 })
|
|
386
|
+
|
|
387
|
+
// Align: Control text alignment
|
|
388
|
+
col.text('status', { align: 'center' })
|
|
389
|
+
col.number('price', { align: 'right' }) // numbers default to 'right'
|
|
390
|
+
|
|
391
|
+
// Truncate: Enable text truncation with ellipsis
|
|
392
|
+
col.text('description', { truncate: true, width: 300 })
|
|
393
|
+
|
|
394
|
+
// Combine multiple modifiers
|
|
395
|
+
col.badge('status', {
|
|
396
|
+
header: 'Status',
|
|
397
|
+
width: 120,
|
|
398
|
+
align: 'center',
|
|
399
|
+
truncate: true,
|
|
400
|
+
map: { active: 'success', inactive: 'neutral' }
|
|
401
|
+
})
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
**Modifiers:**
|
|
405
|
+
- `width?: number` - Column width in pixels
|
|
406
|
+
- `align?: 'left' | 'center' | 'right'` - Text alignment
|
|
407
|
+
- `truncate?: boolean` - Truncate long text with ellipsis (requires `width`)
|
|
408
|
+
|
|
409
|
+
**Notes:**
|
|
410
|
+
- Number columns default to `align: 'right'`
|
|
411
|
+
- Other columns default to `align: 'left'`
|
|
412
|
+
- Truncate works best with a fixed `width`
|
|
413
|
+
|
|
297
414
|
### Pagination
|
|
298
415
|
|
|
299
416
|
The table includes built-in pagination controls that appear automatically when data is loaded.
|
|
@@ -411,6 +528,8 @@ const fetchUsers: Fetcher<User> = async ({ page, pageSize, sort }) => {
|
|
|
411
528
|
- ✅ Text columns with `sortable: true`
|
|
412
529
|
- ✅ Date columns with `sortable: true`
|
|
413
530
|
- ✅ Boolean columns with `sortable: true`
|
|
531
|
+
- ✅ Badge columns with `sortable: true` (v0.2.0+)
|
|
532
|
+
- ✅ Number columns with `sortable: true` (v0.2.0+)
|
|
414
533
|
- ❌ Actions columns (never sortable)
|
|
415
534
|
- ❌ Custom columns (not sortable by default)
|
|
416
535
|
|
|
@@ -421,6 +540,147 @@ const fetchUsers: Fetcher<User> = async ({ page, pageSize, sort }) => {
|
|
|
421
540
|
- Cleared when clicking a sorted column three times
|
|
422
541
|
- Replaced when clicking a different sortable column
|
|
423
542
|
|
|
543
|
+
### Filters (v0.2.0+)
|
|
544
|
+
|
|
545
|
+
Server-side filtering with type-specific filter inputs rendered in a header row.
|
|
546
|
+
|
|
547
|
+
**Features:**
|
|
548
|
+
- **Auto-Generated UI** - Filter inputs based on column type
|
|
549
|
+
- **Server-Side Only** - All filtering happens in your backend
|
|
550
|
+
- **Multiple Operators** - contains, equals, in, range
|
|
551
|
+
- **Clear Filters** - Individual and bulk filter clearing
|
|
552
|
+
- **Page Reset** - Resets to page 1 when filters change
|
|
553
|
+
|
|
554
|
+
**Enable Filters:**
|
|
555
|
+
|
|
556
|
+
```typescript
|
|
557
|
+
<RowaKitTable
|
|
558
|
+
fetcher={fetchUsers}
|
|
559
|
+
columns={columns}
|
|
560
|
+
rowKey="id"
|
|
561
|
+
enableFilters={true} // Add this prop
|
|
562
|
+
/>
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
**Filter Types by Column:**
|
|
566
|
+
|
|
567
|
+
```typescript
|
|
568
|
+
// Text column: Text input with "contains" operator
|
|
569
|
+
col.text('name', { header: 'Name' })
|
|
570
|
+
// → User types "john" → filters: { name: { op: 'contains', value: 'john' } }
|
|
571
|
+
|
|
572
|
+
// Number column: Text input with "equals" operator
|
|
573
|
+
col.number('age', { header: 'Age' })
|
|
574
|
+
// → User types "25" → filters: { age: { op: 'equals', value: '25' } }
|
|
575
|
+
|
|
576
|
+
// Badge column: Select dropdown with "equals" operator
|
|
577
|
+
col.badge('status', {
|
|
578
|
+
header: 'Status',
|
|
579
|
+
map: { active: 'success', inactive: 'neutral', pending: 'warning' }
|
|
580
|
+
})
|
|
581
|
+
// → User selects "active" → filters: { status: { op: 'equals', value: 'active' } }
|
|
582
|
+
|
|
583
|
+
// Boolean column: Select dropdown (All/True/False) with "equals" operator
|
|
584
|
+
col.boolean('isVerified', { header: 'Verified' })
|
|
585
|
+
// → User selects "True" → filters: { isVerified: { op: 'equals', value: true } }
|
|
586
|
+
|
|
587
|
+
// Date column: Two date inputs with "range" operator
|
|
588
|
+
col.date('createdAt', { header: 'Created' })
|
|
589
|
+
// → User enters from/to dates → filters: { createdAt: { op: 'range', value: { from: '2024-01-01', to: '2024-12-31' } } }
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
**Fetcher Integration:**
|
|
593
|
+
|
|
594
|
+
```typescript
|
|
595
|
+
const fetchUsers: Fetcher<User> = async ({ page, pageSize, sort, filters }) => {
|
|
596
|
+
// filters is undefined when no filters are active
|
|
597
|
+
// filters = { fieldName: FilterValue, ... } when filtering
|
|
598
|
+
|
|
599
|
+
const params = new URLSearchParams({
|
|
600
|
+
page: String(page),
|
|
601
|
+
limit: String(pageSize),
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
if (sort) {
|
|
605
|
+
params.append('sortBy', sort.field);
|
|
606
|
+
params.append('sortOrder', sort.direction);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
if (filters) {
|
|
610
|
+
// Example: Convert filters to query params
|
|
611
|
+
for (const [field, filter] of Object.entries(filters)) {
|
|
612
|
+
if (filter.op === 'contains') {
|
|
613
|
+
params.append(`${field}_contains`, filter.value);
|
|
614
|
+
} else if (filter.op === 'equals') {
|
|
615
|
+
params.append(field, String(filter.value));
|
|
616
|
+
} else if (filter.op === 'range') {
|
|
617
|
+
if (filter.value.from) params.append(`${field}_from`, filter.value.from);
|
|
618
|
+
if (filter.value.to) params.append(`${field}_to`, filter.value.to);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const response = await fetch(`/api/users?${params}`);
|
|
624
|
+
return response.json();
|
|
625
|
+
};
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
**Filter Value Types:**
|
|
629
|
+
|
|
630
|
+
```typescript
|
|
631
|
+
type FilterValue =
|
|
632
|
+
| { op: 'contains'; value: string } // Text search
|
|
633
|
+
| { op: 'equals'; value: string | number | boolean | null } // Exact match
|
|
634
|
+
| { op: 'in'; value: Array<string | number> } // Multiple values (future)
|
|
635
|
+
| { op: 'range'; value: { from?: string; to?: string } }; // Date range
|
|
636
|
+
|
|
637
|
+
type Filters = Record<string, FilterValue>;
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
**Important Rules:**
|
|
641
|
+
|
|
642
|
+
1. **Undefined when empty**: `query.filters` is `undefined` when no filters are active (not `{}`)
|
|
643
|
+
2. **Page resets**: Changing any filter resets page to 1
|
|
644
|
+
3. **No client filtering**: All filtering must be handled by your backend
|
|
645
|
+
4. **Actions/Custom columns**: Not filterable (no filter input rendered)
|
|
646
|
+
|
|
647
|
+
**⚠️ Number Filter Behavior:**
|
|
648
|
+
|
|
649
|
+
Number filters use **exact match** semantics (`op: 'equals'`). The filter value is sent as a string, and your backend must handle the comparison:
|
|
650
|
+
|
|
651
|
+
```typescript
|
|
652
|
+
// If user types "15" in a number filter input
|
|
653
|
+
filters: { discount: { op: 'equals', value: '15' } }
|
|
654
|
+
|
|
655
|
+
// Your backend must convert and compare appropriately:
|
|
656
|
+
// If your data has discount as a fraction (0.15), you must convert:
|
|
657
|
+
// parseInt('15') / 100 === 0.15 OR parseFloat('15') / 100 === 0.15
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
**Common Pattern:**
|
|
661
|
+
|
|
662
|
+
If your data uses decimals or percentages, ensure your backend coerces the filter value correctly:
|
|
663
|
+
|
|
664
|
+
```typescript
|
|
665
|
+
// Example: Number filter for percentage discount
|
|
666
|
+
if (filters?.discount) {
|
|
667
|
+
const filterValue = parseFloat(filters.discount.value);
|
|
668
|
+
// If data is stored as fraction (0.15):
|
|
669
|
+
const compareValue = filterValue / 100; // Convert "15" → 0.15
|
|
670
|
+
// Filter records where discount === compareValue
|
|
671
|
+
}
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
**Clear Filters:**
|
|
675
|
+
|
|
676
|
+
A "Clear all filters" button appears automatically when filters are active:
|
|
677
|
+
|
|
678
|
+
```typescript
|
|
679
|
+
// User applies filters
|
|
680
|
+
// → "Clear all filters" button appears above table
|
|
681
|
+
// → Click button → all filters cleared → page resets to 1
|
|
682
|
+
```
|
|
683
|
+
|
|
424
684
|
### Actions
|
|
425
685
|
|
|
426
686
|
The table provides built-in support for row actions with confirmation dialogs, loading states, and conditional disabling.
|
|
@@ -1059,6 +1319,41 @@ Full TypeScript support. Your data model drives type checking throughout.
|
|
|
1059
1319
|
- Column resizing
|
|
1060
1320
|
- Saved views
|
|
1061
1321
|
|
|
1322
|
+
## Changelog
|
|
1323
|
+
|
|
1324
|
+
See the detailed changelog for release history and migration notes:
|
|
1325
|
+
|
|
1326
|
+
- [CHANGELOG.md](./CHANGELOG.md) — highlights and details for v0.2.1 and future releases.
|
|
1327
|
+
|
|
1328
|
+
### v0.2.1 - Production Release (2026-01-02)
|
|
1329
|
+
- ✅ **Fixed**: Number filter type coercion for accurate field matching
|
|
1330
|
+
- ✅ **Production Ready**: All 193 tests passing, dependencies hardened
|
|
1331
|
+
- ✅ **Backwards Compatible**: No breaking changes from v0.2.0
|
|
1332
|
+
|
|
1333
|
+
### v0.2.0 - Stage B Features (2026-01-02)
|
|
1334
|
+
- Added `col.badge` and `col.number` column types
|
|
1335
|
+
- Column modifiers: `width`, `align`, `truncate`
|
|
1336
|
+
- Server-side header filter UI with type-specific inputs
|
|
1337
|
+
- Fixed numeric-filter value coercion bug (filter inputs now send numbers)
|
|
1338
|
+
|
|
1339
|
+
## Release Notes — v0.2.0 (2026-01-02)
|
|
1340
|
+
|
|
1341
|
+
This release introduces Stage B features and several hardening fixes to make `@rowakit/table` production-ready for internal apps.
|
|
1342
|
+
|
|
1343
|
+
- Release: `v0.2.0`
|
|
1344
|
+
- Date: 2026-01-02
|
|
1345
|
+
- Key additions:
|
|
1346
|
+
- `col.badge` — visual badge mapping for enum/status values with tone support
|
|
1347
|
+
- `col.number` — number column with Intl formatting and percentage/currency helpers
|
|
1348
|
+
- Column modifiers (`width`, `align`, `truncate`) supported across column types
|
|
1349
|
+
- Server-side filters: auto-generated, type-specific inputs in the header row
|
|
1350
|
+
- Fixes & hardening:
|
|
1351
|
+
- Removed direct React dependencies (now peerDependencies only)
|
|
1352
|
+
- Resolved numeric filter coercion and clarified backend contract in README
|
|
1353
|
+
- Deduplicated release checklist and improved demo documentation
|
|
1354
|
+
|
|
1355
|
+
See the full changelog for migration notes and detailed descriptions: [CHANGELOG.md](./CHANGELOG.md)
|
|
1356
|
+
|
|
1062
1357
|
## License
|
|
1063
1358
|
|
|
1064
1359
|
MIT
|