@rowakit/table 0.2.2 → 0.3.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
@@ -12,8 +12,12 @@ RowaKit Table is a React table component designed for real-world internal applic
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+)
15
+ ✅ **Column modifiers** - Width, align, truncate, minWidth, maxWidth (v0.2.0+)
16
16
  ✅ **Server-side filters** - Type-specific filter UI with auto-generated inputs (v0.2.0+)
17
+ ✅ **Column resizing** - Drag-to-resize handles with constraints (v0.3.0+)
18
+ ✅ **Saved views** - Save/load table state with localStorage persistence (v0.3.0+)
19
+ ✅ **URL state sync** - Share URLs with exact table configuration (v0.3.0+)
20
+ ✅ **Number range filters** - Min/max filtering for numeric columns (v0.3.0+)
17
21
  ✅ **State management** - Automatic loading, error, and empty states
18
22
  ✅ **Smart fetching** - Retry on error, stale request handling
19
23
 
@@ -852,6 +856,165 @@ col.actions([
852
856
  - Keyboard accessible (Tab to focus, Enter/Space to activate)
853
857
  - Actions column is never sortable
854
858
 
859
+ ---
860
+
861
+ ## Advanced Features (v0.3.0+)
862
+
863
+ ### Column Resizing (C-01)
864
+
865
+ Enable users to resize columns by dragging the right edge of column headers.
866
+
867
+ **Basic Usage:**
868
+
869
+ ```typescript
870
+ <RowaKitTable
871
+ fetcher={fetchData}
872
+ columns={[
873
+ col.text('name', {
874
+ minWidth: 100, // Minimum width (default: 80)
875
+ maxWidth: 400 // Maximum width (optional)
876
+ }),
877
+ col.number('price', {
878
+ minWidth: 80
879
+ })
880
+ ]}
881
+ enableColumnResizing={true} // Enable resize feature
882
+ />
883
+ ```
884
+
885
+ **Features:**
886
+ - **Auto-width by default** - Columns automatically size based on header text
887
+ - **Drag to resize** - Drag the blue handle on the right edge of column headers
888
+ - **Double-click to auto-fit** - Double-click the resize handle to auto-fit content width (measures visible header + cells)
889
+ - **Min/max constraints** - Enforced in real-time during drag
890
+ - **Smooth performance** - RAF throttling prevents lag during resize
891
+ - **Large hitbox** - 12px wide invisible zone (1px visible line) makes dragging easy
892
+ - **State persistence** - Widths stored in-memory (or persisted via URL sync/saved views)
893
+
894
+ **Interaction:**
895
+ - **Hover** - Resize handle appears as a blue vertical line
896
+ - **Drag** - Click and drag to resize column width
897
+ - **Double-click** - Auto-fits to content (max 600px by default)
898
+ - Text selection is disabled during drag for smooth UX
899
+
900
+ ### Saved Views + URL State Sync (C-02)
901
+
902
+ Save and restore table configurations, and share URLs with exact table state.
903
+
904
+ **Basic Usage:**
905
+
906
+ ```typescript
907
+ <RowaKitTable
908
+ fetcher={fetchData}
909
+ columns={columns}
910
+ syncToUrl={true} // Sync to URL query string
911
+ enableSavedViews={true} // Show save/load view buttons
912
+ />
913
+ ```
914
+
915
+ **Features:**
916
+
917
+ 1. **URL Sync** - Automatically saves table state to URL query parameters:
918
+ ```
919
+ ?page=2&pageSize=20&sortField=name&sortDirection=asc&filters=...&columnWidths=...
920
+ ```
921
+ - Share URLs to preserve exact table configuration
922
+ - State automatically restored on page load
923
+ - Works with browser back/forward buttons
924
+
925
+ 2. **Saved Views** - Save current table state as named presets:
926
+ ```
927
+ [Save View] button → Name your view → State saved to localStorage
928
+ [My View] button → Click to restore saved state
929
+ × button → Delete saved view
930
+ [Reset] button → Clear all state
931
+ ```
932
+
933
+ **Synced State:**
934
+ - Page number and size
935
+ - Sort field and direction
936
+ - All active filters
937
+ - Column widths (if resizing enabled)
938
+
939
+ **Example:**
940
+
941
+ ```typescript
942
+ // User filters to "active users" and resizes columns
943
+ // They click "Save View" and name it "Active"
944
+ // Later, they apply different filters
945
+ // They click "Active" button to instantly restore previous state
946
+
947
+ // Or they copy the URL and send it to a colleague
948
+ // The colleague sees the exact same filters and layout
949
+ ```
950
+
951
+ ### Advanced Number Range Filters (C-03)
952
+
953
+ Number columns support min/max range filtering with optional value transformation.
954
+
955
+ **Basic Range Filter:**
956
+
957
+ ```typescript
958
+ col.number('price', {
959
+ label: 'Price',
960
+ width: 100
961
+ })
962
+
963
+ // UI shows two inputs: [Min] [Max]
964
+ // User enters: min=100, max=500
965
+ // Backend receives: { op: 'range', value: { from: 100, to: 500 } }
966
+ ```
967
+
968
+ **With Filter Transform:**
969
+
970
+ ```typescript
971
+ col.number('discount', {
972
+ label: 'Discount %',
973
+ // Transform percentage input to fraction for backend
974
+ filterTransform: (percentageInput) => {
975
+ // User enters "15" (15%)
976
+ // Backend receives "0.15" (fraction)
977
+ if (percentageInput > 1) {
978
+ return percentageInput / 100;
979
+ }
980
+ return percentageInput;
981
+ }
982
+ })
983
+ ```
984
+
985
+ **Features:**
986
+ - Min and max inputs can be filled independently
987
+ - Example: "at least 50" (min only), "up to 100" (max only), or "50-100" (both)
988
+ - Optional `filterTransform` to adapt filter values before sending to server
989
+ - Useful for unit conversion, percentage ↔ decimal, etc.
990
+
991
+ **Handling Range Filters in Fetcher:**
992
+
993
+ ```typescript
994
+ const fetchProducts: Fetcher<Product> = async (query) => {
995
+ let filtered = [...allProducts];
996
+
997
+ if (query.filters) {
998
+ for (const [field, filter] of Object.entries(query.filters)) {
999
+ if (filter?.op === 'range') {
1000
+ const { from, to } = filter.value as { from?: number; to?: number };
1001
+ filtered = filtered.filter(item => {
1002
+ const val = item[field as keyof Product];
1003
+ if (from !== undefined && val < from) return false;
1004
+ if (to !== undefined && val > to) return false;
1005
+ return true;
1006
+ });
1007
+ }
1008
+ }
1009
+ }
1010
+
1011
+ return {
1012
+ items: filtered.slice(0, query.pageSize),
1013
+ total: filtered.length
1014
+ };
1015
+ };
1016
+ ```
1017
+
855
1018
  ## Examples
856
1019
 
857
1020
  ### Basic Table
@@ -1292,7 +1455,7 @@ Full TypeScript support. Your data model drives type checking throughout.
1292
1455
 
1293
1456
  ## Roadmap
1294
1457
 
1295
- **Stage A - MVP 0.1** ✅ Complete (2024-12-31)
1458
+ **Stage A - MVP (v0.1)** ✅ Complete (2024-12-31)
1296
1459
  - ✅ A-01: Repo scaffold
1297
1460
  - ✅ A-02: Core types (Fetcher, ColumnDef, ActionDef)
1298
1461
  - ✅ A-03: Column helpers (col.*)
@@ -1304,19 +1467,25 @@ Full TypeScript support. Your data model drives type checking throughout.
1304
1467
  - ✅ A-09: Minimal styling tokens (CSS variables, responsive, className)
1305
1468
  - ✅ A-10: Documentation & examples (4 complete examples, CHANGELOG, CONTRIBUTING)
1306
1469
 
1307
- **Stage B - Production Ready (v0.2.2)** Production release 2026-01-02
1308
- - Column visibility toggle
1309
- - Bulk actions
1310
- - Search/text filter
1311
- - Export (CSV, Excel)
1312
- - Dense/comfortable view modes
1313
- - Additional column types (badge, number)
1314
-
1315
- **Stage C - Advanced (Demand-Driven)** (Future)
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)
1316
1485
  - Multi-column sorting
1317
- - Advanced filters
1318
- - Column resizing
1319
- - Saved views
1486
+ - Row selection + bulk actions
1487
+ - Export CSV (server-triggered)
1488
+ - Column visibility toggle
1320
1489
 
1321
1490
  ## Changelog
1322
1491
 
package/dist/index.cjs CHANGED
@@ -169,7 +169,11 @@ function renderCell(column, row, isLoading, setConfirmState) {
169
169
  return numValue.toLocaleString();
170
170
  }
171
171
  case "actions": {
172
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rowakit-table-actions", children: column.actions.map((action) => {
172
+ const columnWithActions = column;
173
+ if (!Array.isArray(columnWithActions.actions)) {
174
+ return null;
175
+ }
176
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rowakit-table-actions", children: columnWithActions.actions.map((action) => {
173
177
  const isDisabled = isLoading || action.disabled === true || typeof action.disabled === "function" && action.disabled(row);
174
178
  const handleClick = () => {
175
179
  if (isDisabled || action.loading) {
@@ -213,7 +217,10 @@ function RowaKitTable({
213
217
  pageSizeOptions = [10, 20, 50],
214
218
  rowKey,
215
219
  className = "",
216
- enableFilters = false
220
+ enableFilters = false,
221
+ enableColumnResizing = false,
222
+ syncToUrl = false,
223
+ enableSavedViews = false
217
224
  }) {
218
225
  const [dataState, setDataState] = react.useState({
219
226
  state: "idle",
@@ -225,8 +232,64 @@ function RowaKitTable({
225
232
  pageSize: defaultPageSize
226
233
  });
227
234
  const [filters, setFilters] = react.useState({});
235
+ const [columnWidths, setColumnWidths] = react.useState({});
236
+ const resizeRafRef = react.useRef(null);
237
+ const resizePendingRef = react.useRef(null);
238
+ const tableRef = react.useRef(null);
239
+ const [savedViews, setSavedViews] = react.useState([]);
228
240
  const [confirmState, setConfirmState] = react.useState(null);
229
241
  const requestIdRef = react.useRef(0);
242
+ react.useEffect(() => {
243
+ if (!syncToUrl) return;
244
+ const params = new URLSearchParams();
245
+ params.set("page", String(query.page));
246
+ params.set("pageSize", String(query.pageSize));
247
+ if (query.sort) {
248
+ params.set("sortField", query.sort.field);
249
+ params.set("sortDirection", query.sort.direction);
250
+ }
251
+ if (query.filters && Object.keys(query.filters).length > 0) {
252
+ params.set("filters", JSON.stringify(query.filters));
253
+ }
254
+ if (enableColumnResizing && Object.keys(columnWidths).length > 0) {
255
+ params.set("columnWidths", JSON.stringify(columnWidths));
256
+ }
257
+ window.history.replaceState(null, "", `?${params.toString()}`);
258
+ }, [query, columnWidths, syncToUrl, enableColumnResizing]);
259
+ react.useEffect(() => {
260
+ if (!syncToUrl) return;
261
+ const params = new URLSearchParams(window.location.search);
262
+ const page = parseInt(params.get("page") ?? "1", 10);
263
+ const pageSize = parseInt(params.get("pageSize") ?? String(defaultPageSize), 10);
264
+ const sortField = params.get("sortField");
265
+ const sortDirection = params.get("sortDirection");
266
+ const filtersStr = params.get("filters");
267
+ const columnWidthsStr = params.get("columnWidths");
268
+ const newQuery = {
269
+ page: Math.max(1, page),
270
+ pageSize: Math.max(1, pageSize)
271
+ };
272
+ if (sortField && sortDirection) {
273
+ newQuery.sort = { field: sortField, direction: sortDirection };
274
+ }
275
+ if (filtersStr) {
276
+ try {
277
+ const parsedFilters = JSON.parse(filtersStr);
278
+ if (parsedFilters && typeof parsedFilters === "object") {
279
+ setFilters(parsedFilters);
280
+ newQuery.filters = parsedFilters;
281
+ }
282
+ } catch {
283
+ }
284
+ }
285
+ if (enableColumnResizing && columnWidthsStr) {
286
+ try {
287
+ setColumnWidths(JSON.parse(columnWidthsStr));
288
+ } catch {
289
+ }
290
+ }
291
+ setQuery(newQuery);
292
+ }, [syncToUrl, defaultPageSize, enableColumnResizing]);
230
293
  react.useEffect(() => {
231
294
  if (!enableFilters) return;
232
295
  const activeFilters = {};
@@ -312,10 +375,154 @@ function RowaKitTable({
312
375
  }
313
376
  return query.sort.direction === "asc" ? " \u2191" : " \u2193";
314
377
  };
378
+ const scheduleColumnWidthUpdate = (colId, width) => {
379
+ resizePendingRef.current = { colId, width };
380
+ if (resizeRafRef.current != null) return;
381
+ resizeRafRef.current = requestAnimationFrame(() => {
382
+ resizeRafRef.current = null;
383
+ const pending = resizePendingRef.current;
384
+ if (!pending) return;
385
+ handleColumnResize(pending.colId, pending.width);
386
+ });
387
+ };
388
+ const handleColumnResize = (columnId, newWidth) => {
389
+ const minWidth = columns.find((c) => c.id === columnId)?.minWidth ?? 80;
390
+ const maxWidth = columns.find((c) => c.id === columnId)?.maxWidth;
391
+ let finalWidth = Math.max(minWidth, newWidth);
392
+ if (maxWidth) {
393
+ finalWidth = Math.min(finalWidth, maxWidth);
394
+ }
395
+ setColumnWidths((prev) => ({
396
+ ...prev,
397
+ [columnId]: finalWidth
398
+ }));
399
+ };
400
+ const startColumnResize = (e, columnId) => {
401
+ e.preventDefault();
402
+ const startX = e.clientX;
403
+ const th = e.currentTarget.parentElement;
404
+ let startWidth = columnWidths[columnId] ?? th.offsetWidth;
405
+ const MIN_DRAG_WIDTH = 80;
406
+ if (startWidth < MIN_DRAG_WIDTH) {
407
+ const nextTh = th.nextElementSibling;
408
+ if (nextTh && nextTh.offsetWidth >= 50) {
409
+ startWidth = nextTh.offsetWidth;
410
+ } else {
411
+ startWidth = 100;
412
+ }
413
+ }
414
+ document.body.classList.add("rowakit-resizing");
415
+ const handleMouseMove = (moveEvent) => {
416
+ const delta = moveEvent.clientX - startX;
417
+ const newWidth = startWidth + delta;
418
+ scheduleColumnWidthUpdate(columnId, newWidth);
419
+ };
420
+ const handleMouseUp = () => {
421
+ document.removeEventListener("mousemove", handleMouseMove);
422
+ document.removeEventListener("mouseup", handleMouseUp);
423
+ document.body.classList.remove("rowakit-resizing");
424
+ };
425
+ document.addEventListener("mousemove", handleMouseMove);
426
+ document.addEventListener("mouseup", handleMouseUp);
427
+ };
428
+ const handleColumnResizeDoubleClick = (columnId) => {
429
+ const tableEl = tableRef.current;
430
+ if (!tableEl) return;
431
+ const th = tableEl.querySelector(`th[data-col-id="${columnId}"]`);
432
+ if (!th) return;
433
+ const tds = Array.from(tableEl.querySelectorAll(`td[data-col-id="${columnId}"]`));
434
+ const headerW = th.scrollWidth;
435
+ const cellsMaxW = tds.reduce((max, td) => Math.max(max, td.scrollWidth), 0);
436
+ const padding = 24;
437
+ const raw = Math.max(headerW, cellsMaxW) + padding;
438
+ const minW = columns.find((c) => c.id === columnId)?.minWidth ?? 80;
439
+ const maxW = columns.find((c) => c.id === columnId)?.maxWidth ?? 600;
440
+ const finalW = Math.max(minW, Math.min(raw, maxW));
441
+ setColumnWidths((prev) => ({ ...prev, [columnId]: finalW }));
442
+ };
443
+ const saveCurrentView = (name) => {
444
+ const viewState = {
445
+ page: query.page,
446
+ pageSize: query.pageSize,
447
+ sort: query.sort,
448
+ filters: query.filters,
449
+ columnWidths: enableColumnResizing ? columnWidths : void 0
450
+ };
451
+ setSavedViews((prev) => {
452
+ const filtered = prev.filter((v) => v.name !== name);
453
+ return [...filtered, { name, state: viewState }];
454
+ });
455
+ if (typeof window !== "undefined" && window.localStorage) {
456
+ try {
457
+ localStorage.setItem(`rowakit-view-${name}`, JSON.stringify(viewState));
458
+ } catch {
459
+ }
460
+ }
461
+ };
462
+ const loadSavedView = (name) => {
463
+ const view = savedViews.find((v) => v.name === name);
464
+ if (!view) return;
465
+ const { state } = view;
466
+ setQuery({
467
+ page: state.page,
468
+ pageSize: state.pageSize,
469
+ sort: state.sort,
470
+ filters: state.filters
471
+ });
472
+ setFilters(state.filters ?? {});
473
+ if (state.columnWidths && enableColumnResizing) {
474
+ setColumnWidths(state.columnWidths);
475
+ }
476
+ };
477
+ const deleteSavedView = (name) => {
478
+ setSavedViews((prev) => prev.filter((v) => v.name !== name));
479
+ if (typeof window !== "undefined" && window.localStorage) {
480
+ try {
481
+ localStorage.removeItem(`rowakit-view-${name}`);
482
+ } catch {
483
+ }
484
+ }
485
+ };
486
+ const resetTableState = () => {
487
+ setQuery({
488
+ page: 1,
489
+ pageSize: defaultPageSize
490
+ });
491
+ setFilters({});
492
+ setColumnWidths({});
493
+ };
494
+ const transformFilterValueForColumn = (column, value) => {
495
+ if (!value || column?.kind !== "number") {
496
+ return value;
497
+ }
498
+ const numberColumn = column;
499
+ if (!numberColumn.filterTransform) {
500
+ return value;
501
+ }
502
+ if (value.op === "equals" && typeof value.value === "number") {
503
+ return {
504
+ ...value,
505
+ value: numberColumn.filterTransform(value.value)
506
+ };
507
+ }
508
+ if (value.op === "range" && typeof value.value === "object") {
509
+ const { from, to } = value.value;
510
+ return {
511
+ op: "range",
512
+ value: {
513
+ from: from !== void 0 && typeof from === "number" ? numberColumn.filterTransform(from) : from,
514
+ to: to !== void 0 && typeof to === "number" ? numberColumn.filterTransform(to) : to
515
+ }
516
+ };
517
+ }
518
+ return value;
519
+ };
315
520
  const handleFilterChange = (field, value) => {
521
+ const column = columns.find((c) => c.id === field);
522
+ const transformedValue = transformFilterValueForColumn(column, value);
316
523
  setFilters((prev) => ({
317
524
  ...prev,
318
- [field]: value
525
+ [field]: transformedValue
319
526
  }));
320
527
  };
321
528
  const handleClearFilter = (field) => {
@@ -336,6 +543,52 @@ function RowaKitTable({
336
543
  const canGoNext = query.page < totalPages && !isLoading;
337
544
  const hasActiveFilters = enableFilters && Object.values(filters).some((v) => v !== void 0);
338
545
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `rowakit-table${className ? ` ${className}` : ""}`, children: [
546
+ enableSavedViews && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rowakit-saved-views-group", children: [
547
+ /* @__PURE__ */ jsxRuntime.jsx(
548
+ "button",
549
+ {
550
+ onClick: () => {
551
+ const name = typeof window !== "undefined" ? window.prompt("Enter view name:") : null;
552
+ if (name) {
553
+ saveCurrentView(name);
554
+ }
555
+ },
556
+ className: "rowakit-saved-view-button",
557
+ type: "button",
558
+ children: "Save View"
559
+ }
560
+ ),
561
+ savedViews.map((view) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rowakit-saved-view-item", children: [
562
+ /* @__PURE__ */ jsxRuntime.jsx(
563
+ "button",
564
+ {
565
+ onClick: () => loadSavedView(view.name),
566
+ className: "rowakit-saved-view-button",
567
+ type: "button",
568
+ children: view.name
569
+ }
570
+ ),
571
+ /* @__PURE__ */ jsxRuntime.jsx(
572
+ "button",
573
+ {
574
+ onClick: () => deleteSavedView(view.name),
575
+ className: "rowakit-saved-view-button rowakit-saved-view-button-delete",
576
+ type: "button",
577
+ title: "Delete this view",
578
+ children: "\xD7"
579
+ }
580
+ )
581
+ ] }, view.name)),
582
+ (hasActiveFilters || query.page > 1 || query.sort) && /* @__PURE__ */ jsxRuntime.jsx(
583
+ "button",
584
+ {
585
+ onClick: resetTableState,
586
+ className: "rowakit-saved-view-button",
587
+ type: "button",
588
+ children: "Reset"
589
+ }
590
+ )
591
+ ] }),
339
592
  hasActiveFilters && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rowakit-table-filter-controls", children: /* @__PURE__ */ jsxRuntime.jsx(
340
593
  "button",
341
594
  {
@@ -345,14 +598,17 @@ function RowaKitTable({
345
598
  children: "Clear all filters"
346
599
  }
347
600
  ) }),
348
- /* @__PURE__ */ jsxRuntime.jsxs("table", { children: [
601
+ /* @__PURE__ */ jsxRuntime.jsxs("table", { ref: tableRef, children: [
349
602
  /* @__PURE__ */ jsxRuntime.jsxs("thead", { children: [
350
603
  /* @__PURE__ */ jsxRuntime.jsx("tr", { children: columns.map((column) => {
351
604
  const isSortable = column.kind !== "actions" && (column.kind === "custom" ? false : column.sortable === true);
352
605
  const field = column.kind === "actions" ? "" : column.kind === "custom" ? column.field : column.field;
606
+ const isResizable = enableColumnResizing && column.kind !== "actions";
607
+ const actualWidth = columnWidths[column.id] ?? column.width;
353
608
  return /* @__PURE__ */ jsxRuntime.jsxs(
354
609
  "th",
355
610
  {
611
+ "data-col-id": column.id,
356
612
  onClick: isSortable ? () => handleSort(String(field)) : void 0,
357
613
  role: isSortable ? "button" : void 0,
358
614
  tabIndex: isSortable ? 0 : void 0,
@@ -364,13 +620,23 @@ function RowaKitTable({
364
620
  } : void 0,
365
621
  "aria-sort": isSortable && query.sort?.field === String(field) ? query.sort.direction === "asc" ? "ascending" : "descending" : void 0,
366
622
  style: {
367
- width: column.width ? `${column.width}px` : void 0,
368
- textAlign: column.align
623
+ width: actualWidth ? `${actualWidth}px` : void 0,
624
+ textAlign: column.align,
625
+ position: isResizable ? "relative" : void 0
369
626
  },
370
- className: column.truncate ? "rowakit-cell-truncate" : void 0,
627
+ className: column.truncate && !isResizable ? "rowakit-cell-truncate" : void 0,
371
628
  children: [
372
629
  getHeaderLabel(column),
373
- isSortable && getSortIndicator(String(field))
630
+ isSortable && getSortIndicator(String(field)),
631
+ isResizable && /* @__PURE__ */ jsxRuntime.jsx(
632
+ "div",
633
+ {
634
+ className: "rowakit-column-resize-handle",
635
+ onMouseDown: (e) => startColumnResize(e, column.id),
636
+ onDoubleClick: () => handleColumnResizeDoubleClick(column.id),
637
+ title: "Drag to resize | Double-click to auto-fit content"
638
+ }
639
+ )
374
640
  ]
375
641
  },
376
642
  column.id
@@ -470,6 +736,51 @@ function RowaKitTable({
470
736
  ] }) }, column.id);
471
737
  }
472
738
  const isNumberColumn = column.kind === "number";
739
+ if (isNumberColumn) {
740
+ const fromValue = filterValue?.op === "range" ? String(filterValue.value.from ?? "") : filterValue?.op === "equals" && typeof filterValue.value === "number" ? String(filterValue.value) : "";
741
+ const toValue = filterValue?.op === "range" ? String(filterValue.value.to ?? "") : "";
742
+ const showRangeUI = !filterValue || filterValue.op === "range";
743
+ if (showRangeUI) {
744
+ return /* @__PURE__ */ jsxRuntime.jsx("th", { children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rowakit-filter-number-range", children: [
745
+ /* @__PURE__ */ jsxRuntime.jsx(
746
+ "input",
747
+ {
748
+ type: "number",
749
+ className: "rowakit-filter-input",
750
+ placeholder: "Min",
751
+ value: fromValue,
752
+ onChange: (e) => {
753
+ const from = e.target.value ? Number(e.target.value) : void 0;
754
+ const to = toValue ? Number(toValue) : void 0;
755
+ if (from === void 0 && to === void 0) {
756
+ handleClearFilter(field);
757
+ } else {
758
+ handleFilterChange(field, { op: "range", value: { from, to } });
759
+ }
760
+ }
761
+ }
762
+ ),
763
+ /* @__PURE__ */ jsxRuntime.jsx(
764
+ "input",
765
+ {
766
+ type: "number",
767
+ className: "rowakit-filter-input",
768
+ placeholder: "Max",
769
+ value: toValue,
770
+ onChange: (e) => {
771
+ const to = e.target.value ? Number(e.target.value) : void 0;
772
+ const from = fromValue ? Number(fromValue) : void 0;
773
+ if (from === void 0 && to === void 0) {
774
+ handleClearFilter(field);
775
+ } else {
776
+ handleFilterChange(field, { op: "range", value: { from, to } });
777
+ }
778
+ }
779
+ }
780
+ )
781
+ ] }) }, column.id);
782
+ }
783
+ }
473
784
  return /* @__PURE__ */ jsxRuntime.jsx("th", { children: /* @__PURE__ */ jsxRuntime.jsx(
474
785
  "input",
475
786
  {
@@ -521,12 +832,14 @@ function RowaKitTable({
521
832
  column.kind === "number" ? "rowakit-cell-number" : "",
522
833
  column.truncate ? "rowakit-cell-truncate" : ""
523
834
  ].filter(Boolean).join(" ") || void 0;
835
+ const actualWidth = columnWidths[column.id];
524
836
  return /* @__PURE__ */ jsxRuntime.jsx(
525
837
  "td",
526
838
  {
839
+ "data-col-id": column.id,
527
840
  className: cellClass,
528
841
  style: {
529
- width: column.width ? `${column.width}px` : void 0,
842
+ width: actualWidth ? `${actualWidth}px` : void 0,
530
843
  textAlign: column.align || (column.kind === "number" ? "right" : void 0)
531
844
  },
532
845
  children: renderCell(column, row, isLoading, setConfirmState)