@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 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
- ✅ **5 column types** - Text, Date, Boolean, Actions, Custom
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