@rowakit/table 0.1.0 → 0.2.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
@@ -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,120 @@ 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
+ **Clear Filters:**
648
+
649
+ A "Clear all filters" button appears automatically when filters are active:
650
+
651
+ ```typescript
652
+ // User applies filters
653
+ // → "Clear all filters" button appears above table
654
+ // → Click button → all filters cleared → page resets to 1
655
+ ```
656
+
424
657
  ### Actions
425
658
 
426
659
  The table provides built-in support for row actions with confirmation dialogs, loading states, and conditional disabling.
package/dist/index.cjs CHANGED
@@ -11,7 +11,10 @@ function text(field, options) {
11
11
  field,
12
12
  header: options?.header,
13
13
  sortable: options?.sortable ?? false,
14
- format: options?.format
14
+ format: options?.format,
15
+ width: options?.width,
16
+ align: options?.align,
17
+ truncate: options?.truncate
15
18
  };
16
19
  }
17
20
  function date(field, options) {
@@ -21,7 +24,10 @@ function date(field, options) {
21
24
  field,
22
25
  header: options?.header,
23
26
  sortable: options?.sortable ?? false,
24
- format: options?.format
27
+ format: options?.format,
28
+ width: options?.width,
29
+ align: options?.align,
30
+ truncate: options?.truncate
25
31
  };
26
32
  }
27
33
  function boolean(field, options) {
@@ -31,7 +37,36 @@ function boolean(field, options) {
31
37
  field,
32
38
  header: options?.header,
33
39
  sortable: options?.sortable ?? false,
34
- format: options?.format
40
+ format: options?.format,
41
+ width: options?.width,
42
+ align: options?.align,
43
+ truncate: options?.truncate
44
+ };
45
+ }
46
+ function badge(field, options) {
47
+ return {
48
+ id: field,
49
+ kind: "badge",
50
+ field,
51
+ header: options?.header,
52
+ sortable: options?.sortable ?? false,
53
+ map: options?.map,
54
+ width: options?.width,
55
+ align: options?.align,
56
+ truncate: options?.truncate
57
+ };
58
+ }
59
+ function number(field, options) {
60
+ return {
61
+ id: field,
62
+ kind: "number",
63
+ field,
64
+ header: options?.header,
65
+ sortable: options?.sortable ?? false,
66
+ format: options?.format,
67
+ width: options?.width,
68
+ align: options?.align,
69
+ truncate: options?.truncate
35
70
  };
36
71
  }
37
72
  function actions(actions2) {
@@ -41,19 +76,32 @@ function actions(actions2) {
41
76
  actions: actions2
42
77
  };
43
78
  }
44
- function custom(options) {
79
+ function custom(arg1, arg2) {
80
+ if (typeof arg1 === "string") {
81
+ if (typeof arg2 !== "function") {
82
+ throw new Error("col.custom(field, render): render must be a function");
83
+ }
84
+ return {
85
+ id: arg1,
86
+ kind: "custom",
87
+ field: arg1,
88
+ render: arg2
89
+ };
90
+ }
45
91
  return {
46
- id: options.id,
92
+ id: arg1.id,
47
93
  kind: "custom",
48
- header: options.header,
49
- field: options.field,
50
- render: options.render
94
+ header: arg1.header,
95
+ field: arg1.field,
96
+ render: arg1.render
51
97
  };
52
98
  }
53
99
  var col = {
54
100
  text,
55
101
  date,
56
102
  boolean,
103
+ badge,
104
+ number,
57
105
  actions,
58
106
  custom
59
107
  };
@@ -101,6 +149,25 @@ function renderCell(column, row, isLoading, setConfirmState) {
101
149
  }
102
150
  return value ? "Yes" : "No";
103
151
  }
152
+ case "badge": {
153
+ const value = row[column.field];
154
+ const valueStr = String(value ?? "");
155
+ const mapped = column.map?.[valueStr];
156
+ const label = mapped?.label ?? valueStr;
157
+ const tone = mapped?.tone ?? "neutral";
158
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { className: `rowakit-badge rowakit-badge-${tone}`, children: label });
159
+ }
160
+ case "number": {
161
+ const value = row[column.field];
162
+ const numValue = Number(value ?? 0);
163
+ if (column.format) {
164
+ if (typeof column.format === "function") {
165
+ return column.format(numValue, row);
166
+ }
167
+ return new Intl.NumberFormat(void 0, column.format).format(numValue);
168
+ }
169
+ return numValue.toLocaleString();
170
+ }
104
171
  case "actions": {
105
172
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rowakit-table-actions", children: column.actions.map((action) => {
106
173
  const isDisabled = isLoading || action.disabled === true || typeof action.disabled === "function" && action.disabled(row);
@@ -145,7 +212,8 @@ function RowaKitTable({
145
212
  defaultPageSize = 20,
146
213
  pageSizeOptions = [10, 20, 50],
147
214
  rowKey,
148
- className = ""
215
+ className = "",
216
+ enableFilters = false
149
217
  }) {
150
218
  const [dataState, setDataState] = react.useState({
151
219
  state: "idle",
@@ -156,8 +224,27 @@ function RowaKitTable({
156
224
  page: 1,
157
225
  pageSize: defaultPageSize
158
226
  });
227
+ const [filters, setFilters] = react.useState({});
159
228
  const [confirmState, setConfirmState] = react.useState(null);
160
229
  const requestIdRef = react.useRef(0);
230
+ react.useEffect(() => {
231
+ if (!enableFilters) return;
232
+ const activeFilters = {};
233
+ let hasFilters = false;
234
+ for (const [field, value] of Object.entries(filters)) {
235
+ if (value !== void 0) {
236
+ activeFilters[field] = value;
237
+ hasFilters = true;
238
+ }
239
+ }
240
+ const filtersToSend = hasFilters ? activeFilters : void 0;
241
+ setQuery((prev) => ({
242
+ ...prev,
243
+ filters: filtersToSend,
244
+ page: 1
245
+ // Reset page to 1 when filters change
246
+ }));
247
+ }, [filters, enableFilters]);
161
248
  react.useEffect(() => {
162
249
  const currentRequestId = ++requestIdRef.current;
163
250
  setDataState((prev) => ({ ...prev, state: "loading" }));
@@ -225,38 +312,190 @@ function RowaKitTable({
225
312
  }
226
313
  return query.sort.direction === "asc" ? " \u2191" : " \u2193";
227
314
  };
315
+ const handleFilterChange = (field, value) => {
316
+ setFilters((prev) => ({
317
+ ...prev,
318
+ [field]: value
319
+ }));
320
+ };
321
+ const handleClearFilter = (field) => {
322
+ setFilters((prev) => {
323
+ const newFilters = { ...prev };
324
+ delete newFilters[field];
325
+ return newFilters;
326
+ });
327
+ };
328
+ const handleClearAllFilters = () => {
329
+ setFilters({});
330
+ };
228
331
  const isLoading = dataState.state === "loading";
229
332
  const isError = dataState.state === "error";
230
333
  const isEmpty = dataState.state === "empty";
231
334
  const totalPages = Math.ceil(dataState.total / query.pageSize);
232
335
  const canGoPrevious = query.page > 1 && !isLoading;
233
336
  const canGoNext = query.page < totalPages && !isLoading;
337
+ const hasActiveFilters = enableFilters && Object.values(filters).some((v) => v !== void 0);
234
338
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `rowakit-table${className ? ` ${className}` : ""}`, children: [
339
+ hasActiveFilters && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rowakit-table-filter-controls", children: /* @__PURE__ */ jsxRuntime.jsx(
340
+ "button",
341
+ {
342
+ onClick: handleClearAllFilters,
343
+ className: "rowakit-button rowakit-button-secondary",
344
+ type: "button",
345
+ children: "Clear all filters"
346
+ }
347
+ ) }),
235
348
  /* @__PURE__ */ jsxRuntime.jsxs("table", { children: [
236
- /* @__PURE__ */ jsxRuntime.jsx("thead", { children: /* @__PURE__ */ jsxRuntime.jsx("tr", { children: columns.map((column) => {
237
- const isSortable = column.kind !== "actions" && (column.kind === "custom" ? false : column.sortable === true);
238
- const field = column.kind === "actions" ? "" : column.kind === "custom" ? column.field : column.field;
239
- return /* @__PURE__ */ jsxRuntime.jsxs(
240
- "th",
241
- {
242
- onClick: isSortable ? () => handleSort(String(field)) : void 0,
243
- role: isSortable ? "button" : void 0,
244
- tabIndex: isSortable ? 0 : void 0,
245
- onKeyDown: isSortable ? (e) => {
246
- if (e.key === "Enter" || e.key === " ") {
247
- e.preventDefault();
248
- handleSort(String(field));
349
+ /* @__PURE__ */ jsxRuntime.jsxs("thead", { children: [
350
+ /* @__PURE__ */ jsxRuntime.jsx("tr", { children: columns.map((column) => {
351
+ const isSortable = column.kind !== "actions" && (column.kind === "custom" ? false : column.sortable === true);
352
+ const field = column.kind === "actions" ? "" : column.kind === "custom" ? column.field : column.field;
353
+ return /* @__PURE__ */ jsxRuntime.jsxs(
354
+ "th",
355
+ {
356
+ onClick: isSortable ? () => handleSort(String(field)) : void 0,
357
+ role: isSortable ? "button" : void 0,
358
+ tabIndex: isSortable ? 0 : void 0,
359
+ onKeyDown: isSortable ? (e) => {
360
+ if (e.key === "Enter" || e.key === " ") {
361
+ e.preventDefault();
362
+ handleSort(String(field));
363
+ }
364
+ } : void 0,
365
+ "aria-sort": isSortable && query.sort?.field === String(field) ? query.sort.direction === "asc" ? "ascending" : "descending" : void 0,
366
+ style: {
367
+ width: column.width ? `${column.width}px` : void 0,
368
+ textAlign: column.align
369
+ },
370
+ className: column.truncate ? "rowakit-cell-truncate" : void 0,
371
+ children: [
372
+ getHeaderLabel(column),
373
+ isSortable && getSortIndicator(String(field))
374
+ ]
375
+ },
376
+ column.id
377
+ );
378
+ }) }),
379
+ enableFilters && /* @__PURE__ */ jsxRuntime.jsx("tr", { className: "rowakit-table-filter-row", children: columns.map((column) => {
380
+ const field = column.kind === "actions" || column.kind === "custom" ? "" : String(column.field);
381
+ const canFilter = field && column.kind !== "actions";
382
+ if (!canFilter) {
383
+ return /* @__PURE__ */ jsxRuntime.jsx("th", {}, column.id);
384
+ }
385
+ const filterValue = filters[field];
386
+ if (column.kind === "badge") {
387
+ const options = column.map ? Object.keys(column.map) : [];
388
+ return /* @__PURE__ */ jsxRuntime.jsx("th", { children: /* @__PURE__ */ jsxRuntime.jsxs(
389
+ "select",
390
+ {
391
+ className: "rowakit-filter-select",
392
+ value: filterValue?.op === "equals" ? String(filterValue.value ?? "") : "",
393
+ onChange: (e) => {
394
+ const value = e.target.value;
395
+ if (value === "") {
396
+ handleClearFilter(field);
397
+ } else {
398
+ handleFilterChange(field, { op: "equals", value });
399
+ }
400
+ },
401
+ children: [
402
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "", children: "All" }),
403
+ options.map((opt) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: opt, children: opt }, opt))
404
+ ]
249
405
  }
250
- } : void 0,
251
- "aria-sort": isSortable && query.sort?.field === String(field) ? query.sort.direction === "asc" ? "ascending" : "descending" : void 0,
252
- children: [
253
- getHeaderLabel(column),
254
- isSortable && getSortIndicator(String(field))
255
- ]
256
- },
257
- column.id
258
- );
259
- }) }) }),
406
+ ) }, column.id);
407
+ }
408
+ if (column.kind === "boolean") {
409
+ return /* @__PURE__ */ jsxRuntime.jsx("th", { children: /* @__PURE__ */ jsxRuntime.jsxs(
410
+ "select",
411
+ {
412
+ className: "rowakit-filter-select",
413
+ value: filterValue?.op === "equals" && typeof filterValue.value === "boolean" ? String(filterValue.value) : "",
414
+ onChange: (e) => {
415
+ const value = e.target.value;
416
+ if (value === "") {
417
+ handleClearFilter(field);
418
+ } else {
419
+ handleFilterChange(field, { op: "equals", value: value === "true" });
420
+ }
421
+ },
422
+ children: [
423
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "", children: "All" }),
424
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "true", children: "True" }),
425
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "false", children: "False" })
426
+ ]
427
+ }
428
+ ) }, column.id);
429
+ }
430
+ if (column.kind === "date") {
431
+ const fromValue = filterValue?.op === "range" ? filterValue.value.from ?? "" : "";
432
+ const toValue = filterValue?.op === "range" ? filterValue.value.to ?? "" : "";
433
+ return /* @__PURE__ */ jsxRuntime.jsx("th", { children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rowakit-filter-date-range", children: [
434
+ /* @__PURE__ */ jsxRuntime.jsx(
435
+ "input",
436
+ {
437
+ type: "date",
438
+ className: "rowakit-filter-input",
439
+ placeholder: "From",
440
+ value: fromValue,
441
+ onChange: (e) => {
442
+ const from = e.target.value || void 0;
443
+ const to = toValue || void 0;
444
+ if (!from && !to) {
445
+ handleClearFilter(field);
446
+ } else {
447
+ handleFilterChange(field, { op: "range", value: { from, to } });
448
+ }
449
+ }
450
+ }
451
+ ),
452
+ /* @__PURE__ */ jsxRuntime.jsx(
453
+ "input",
454
+ {
455
+ type: "date",
456
+ className: "rowakit-filter-input",
457
+ placeholder: "To",
458
+ value: toValue,
459
+ onChange: (e) => {
460
+ const to = e.target.value || void 0;
461
+ const from = fromValue || void 0;
462
+ if (!from && !to) {
463
+ handleClearFilter(field);
464
+ } else {
465
+ handleFilterChange(field, { op: "range", value: { from, to } });
466
+ }
467
+ }
468
+ }
469
+ )
470
+ ] }) }, column.id);
471
+ }
472
+ const isNumberColumn = column.kind === "number";
473
+ return /* @__PURE__ */ jsxRuntime.jsx("th", { children: /* @__PURE__ */ jsxRuntime.jsx(
474
+ "input",
475
+ {
476
+ type: isNumberColumn ? "number" : "text",
477
+ className: "rowakit-filter-input",
478
+ placeholder: `Filter ${getHeaderLabel(column)}...`,
479
+ value: filterValue?.op === "contains" ? filterValue.value : filterValue?.op === "equals" && typeof filterValue.value === "string" ? filterValue.value : filterValue?.op === "equals" && typeof filterValue.value === "number" ? String(filterValue.value) : "",
480
+ onChange: (e) => {
481
+ const rawValue = e.target.value;
482
+ if (rawValue === "") {
483
+ handleClearFilter(field);
484
+ } else if (isNumberColumn) {
485
+ const numValue = Number(rawValue);
486
+ if (!isNaN(numValue)) {
487
+ handleFilterChange(field, { op: "equals", value: numValue });
488
+ } else {
489
+ handleClearFilter(field);
490
+ }
491
+ } else {
492
+ handleFilterChange(field, { op: "contains", value: rawValue });
493
+ }
494
+ }
495
+ }
496
+ ) }, column.id);
497
+ }) })
498
+ ] }),
260
499
  /* @__PURE__ */ jsxRuntime.jsxs("tbody", { children: [
261
500
  isLoading && /* @__PURE__ */ jsxRuntime.jsx("tr", { children: /* @__PURE__ */ jsxRuntime.jsxs("td", { colSpan: columns.length, className: "rowakit-table-loading", children: [
262
501
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rowakit-table-loading-spinner" }),
@@ -277,7 +516,24 @@ function RowaKitTable({
277
516
  isEmpty && /* @__PURE__ */ jsxRuntime.jsx("tr", { children: /* @__PURE__ */ jsxRuntime.jsx("td", { colSpan: columns.length, className: "rowakit-table-empty", children: "No data" }) }),
278
517
  dataState.state === "success" && dataState.items.map((row) => {
279
518
  const key = getRowKey(row, rowKey);
280
- return /* @__PURE__ */ jsxRuntime.jsx("tr", { children: columns.map((column) => /* @__PURE__ */ jsxRuntime.jsx("td", { children: renderCell(column, row, isLoading, setConfirmState) }, column.id)) }, key);
519
+ return /* @__PURE__ */ jsxRuntime.jsx("tr", { children: columns.map((column) => {
520
+ const cellClass = [
521
+ column.kind === "number" ? "rowakit-cell-number" : "",
522
+ column.truncate ? "rowakit-cell-truncate" : ""
523
+ ].filter(Boolean).join(" ") || void 0;
524
+ return /* @__PURE__ */ jsxRuntime.jsx(
525
+ "td",
526
+ {
527
+ className: cellClass,
528
+ style: {
529
+ width: column.width ? `${column.width}px` : void 0,
530
+ textAlign: column.align || (column.kind === "number" ? "right" : void 0)
531
+ },
532
+ children: renderCell(column, row, isLoading, setConfirmState)
533
+ },
534
+ column.id
535
+ );
536
+ }) }, key);
281
537
  })
282
538
  ] })
283
539
  ] }),