@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 +183 -14
- package/dist/index.cjs +322 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +21 -4
- package/dist/index.d.ts +21 -4
- package/dist/index.js +322 -9
- package/dist/index.js.map +1 -1
- package/package.json +5 -1
- package/src/styles/table.css +98 -0
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
|
|
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
|
|
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)**
|
|
1308
|
-
-
|
|
1309
|
-
-
|
|
1310
|
-
-
|
|
1311
|
-
-
|
|
1312
|
-
-
|
|
1313
|
-
-
|
|
1314
|
-
|
|
1315
|
-
**Stage C - Advanced (
|
|
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
|
-
-
|
|
1318
|
-
-
|
|
1319
|
-
-
|
|
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
|
-
|
|
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]:
|
|
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:
|
|
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:
|
|
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)
|