@rowakit/table 0.3.0 → 0.4.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 +14 -11
- package/dist/index.cjs +427 -77
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +428 -78
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/styles/table.css +56 -1
package/README.md
CHANGED
|
@@ -14,10 +14,10 @@ RowaKit Table is a React table component designed for real-world internal applic
|
|
|
14
14
|
✅ **7 column types** - Text, Date, Boolean, Badge, Number, Actions, Custom
|
|
15
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.
|
|
18
|
-
✅ **Saved views** - Save/load table state with localStorage persistence (v0.
|
|
19
|
-
✅ **URL state sync** - Share URLs with exact table configuration (v0.
|
|
20
|
-
✅ **Number range filters** - Min/max filtering for numeric columns (v0.
|
|
17
|
+
✅ **Column resizing** - Drag-to-resize handles with constraints (v0.4.0+)
|
|
18
|
+
✅ **Saved views** - Save/load table state with localStorage persistence (v0.4.0+)
|
|
19
|
+
✅ **URL state sync** - Share URLs with exact table configuration (v0.4.0+)
|
|
20
|
+
✅ **Number range filters** - Min/max filtering for numeric columns (v0.4.0+)
|
|
21
21
|
✅ **State management** - Automatic loading, error, and empty states
|
|
22
22
|
✅ **Smart fetching** - Retry on error, stale request handling
|
|
23
23
|
|
|
@@ -858,7 +858,7 @@ col.actions([
|
|
|
858
858
|
|
|
859
859
|
---
|
|
860
860
|
|
|
861
|
-
## Advanced Features (v0.
|
|
861
|
+
## Advanced Features (v0.4.0+)
|
|
862
862
|
|
|
863
863
|
### Column Resizing (C-01)
|
|
864
864
|
|
|
@@ -1475,17 +1475,20 @@ Full TypeScript support. Your data model drives type checking throughout.
|
|
|
1475
1475
|
- ✅ Fixed numeric filter value coercion
|
|
1476
1476
|
- ✅ Production hardening and comprehensive documentation
|
|
1477
1477
|
|
|
1478
|
-
**Stage C - Advanced Features (v0.
|
|
1478
|
+
**Stage C - Advanced Features (v0.4.0)** ✅ Complete (2026-01-03)
|
|
1479
1479
|
- ✅ C-01: Column resizing with drag handles (minWidth/maxWidth constraints)
|
|
1480
1480
|
- ✅ C-02: Saved views with localStorage persistence
|
|
1481
1481
|
- ✅ C-02: URL state sync (page, pageSize, sort, filters, columnWidths)
|
|
1482
1482
|
- ✅ C-03: Number range filters with optional filterTransform
|
|
1483
1483
|
|
|
1484
|
-
**Stage D -
|
|
1485
|
-
-
|
|
1486
|
-
-
|
|
1487
|
-
-
|
|
1488
|
-
-
|
|
1484
|
+
**Stage D - Polish + Correctness (v0.4.0)** ✅ Complete (2026-01-05)
|
|
1485
|
+
- ✅ D-01: Prevent accidental sort while resizing (stopPropagation, suppression window)
|
|
1486
|
+
- ✅ D-02: Pointer Events resizing (mouse, touch, pen) with pointer capture and cleanup
|
|
1487
|
+
- ✅ D-03: Column width model hardening (apply widths to th+td, fixed layout, truncation)
|
|
1488
|
+
- ✅ D-04: Saved views persistence (index, hydration, corruption-safe)
|
|
1489
|
+
- ✅ D-05: URL sync hardening (validation, debounce, backward compatible)
|
|
1490
|
+
|
|
1491
|
+
See [ROADMAP.md](./docs/ROADMAP.md) and `docs/ROWAKIT_STAGE_D_ISSUES_v3.md` for implementation details and rationale.
|
|
1489
1492
|
|
|
1490
1493
|
## Changelog
|
|
1491
1494
|
|
package/dist/index.cjs
CHANGED
|
@@ -120,6 +120,158 @@ function getRowKey(row, rowKey) {
|
|
|
120
120
|
function getHeaderLabel(column) {
|
|
121
121
|
return column.header ?? column.id;
|
|
122
122
|
}
|
|
123
|
+
function validateViewName(name) {
|
|
124
|
+
const trimmed = name.trim();
|
|
125
|
+
if (trimmed.length === 0) {
|
|
126
|
+
return { valid: false, error: "Name cannot be empty" };
|
|
127
|
+
}
|
|
128
|
+
if (trimmed.length > 40) {
|
|
129
|
+
return { valid: false, error: "Name cannot exceed 40 characters" };
|
|
130
|
+
}
|
|
131
|
+
const invalidChars = /[/\\?%*:|"<>\x00-\x1f\x7f]/;
|
|
132
|
+
if (invalidChars.test(trimmed)) {
|
|
133
|
+
return { valid: false, error: "Name contains invalid characters" };
|
|
134
|
+
}
|
|
135
|
+
return { valid: true };
|
|
136
|
+
}
|
|
137
|
+
function getSavedViewsIndex() {
|
|
138
|
+
if (typeof window === "undefined" || !window.localStorage) {
|
|
139
|
+
return [];
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
const indexStr = localStorage.getItem("rowakit-views-index");
|
|
143
|
+
if (indexStr) {
|
|
144
|
+
const index = JSON.parse(indexStr);
|
|
145
|
+
if (Array.isArray(index)) {
|
|
146
|
+
return index;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
} catch {
|
|
150
|
+
}
|
|
151
|
+
const rebuilt = [];
|
|
152
|
+
try {
|
|
153
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
154
|
+
const key = localStorage.key(i);
|
|
155
|
+
if (key?.startsWith("rowakit-view-")) {
|
|
156
|
+
const name = key.substring("rowakit-view-".length);
|
|
157
|
+
rebuilt.push({
|
|
158
|
+
name,
|
|
159
|
+
updatedAt: Date.now()
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
} catch {
|
|
164
|
+
}
|
|
165
|
+
return rebuilt;
|
|
166
|
+
}
|
|
167
|
+
function setSavedViewsIndex(index) {
|
|
168
|
+
if (typeof window === "undefined" || !window.localStorage) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
localStorage.setItem("rowakit-views-index", JSON.stringify(index));
|
|
173
|
+
} catch {
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
function loadSavedViewsFromStorage() {
|
|
177
|
+
if (typeof window === "undefined" || !window.localStorage) {
|
|
178
|
+
return [];
|
|
179
|
+
}
|
|
180
|
+
const index = getSavedViewsIndex();
|
|
181
|
+
const views = [];
|
|
182
|
+
for (const entry of index) {
|
|
183
|
+
try {
|
|
184
|
+
const viewStr = localStorage.getItem(`rowakit-view-${entry.name}`);
|
|
185
|
+
if (viewStr) {
|
|
186
|
+
const state = JSON.parse(viewStr);
|
|
187
|
+
views.push({
|
|
188
|
+
name: entry.name,
|
|
189
|
+
state
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
} catch {
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return views;
|
|
196
|
+
}
|
|
197
|
+
function parseUrlState(params, defaultPageSize, pageSizeOptions) {
|
|
198
|
+
const pageStr = params.get("page");
|
|
199
|
+
let page = 1;
|
|
200
|
+
if (pageStr) {
|
|
201
|
+
const parsed = parseInt(pageStr, 10);
|
|
202
|
+
page = !isNaN(parsed) && parsed >= 1 ? parsed : 1;
|
|
203
|
+
}
|
|
204
|
+
const pageSizeStr = params.get("pageSize");
|
|
205
|
+
let pageSize = defaultPageSize;
|
|
206
|
+
if (pageSizeStr) {
|
|
207
|
+
const parsed = parseInt(pageSizeStr, 10);
|
|
208
|
+
if (!isNaN(parsed) && parsed >= 1) {
|
|
209
|
+
if (pageSizeOptions && pageSizeOptions.length > 0) {
|
|
210
|
+
pageSize = pageSizeOptions.includes(parsed) ? parsed : defaultPageSize;
|
|
211
|
+
} else {
|
|
212
|
+
pageSize = parsed;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const result = { page, pageSize };
|
|
217
|
+
const sortField = params.get("sortField");
|
|
218
|
+
const sortDir = params.get("sortDirection");
|
|
219
|
+
if (sortField && (sortDir === "asc" || sortDir === "desc")) {
|
|
220
|
+
result.sort = { field: sortField, direction: sortDir };
|
|
221
|
+
}
|
|
222
|
+
const filtersStr = params.get("filters");
|
|
223
|
+
if (filtersStr) {
|
|
224
|
+
try {
|
|
225
|
+
const parsed = JSON.parse(filtersStr);
|
|
226
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
227
|
+
result.filters = parsed;
|
|
228
|
+
}
|
|
229
|
+
} catch {
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
const widthsStr = params.get("columnWidths");
|
|
233
|
+
if (widthsStr) {
|
|
234
|
+
try {
|
|
235
|
+
const parsed = JSON.parse(widthsStr);
|
|
236
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
237
|
+
const widths = {};
|
|
238
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
239
|
+
if (typeof value === "number" && value > 0) {
|
|
240
|
+
widths[key] = value;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (Object.keys(widths).length > 0) {
|
|
244
|
+
result.columnWidths = widths;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
} catch {
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return result;
|
|
251
|
+
}
|
|
252
|
+
function serializeUrlState(query, filters, columnWidths, defaultPageSize, enableColumnResizing) {
|
|
253
|
+
const params = new URLSearchParams();
|
|
254
|
+
params.set("page", String(query.page));
|
|
255
|
+
if (query.pageSize !== defaultPageSize) {
|
|
256
|
+
params.set("pageSize", String(query.pageSize));
|
|
257
|
+
}
|
|
258
|
+
if (query.sort) {
|
|
259
|
+
params.set("sortField", query.sort.field);
|
|
260
|
+
params.set("sortDirection", query.sort.direction);
|
|
261
|
+
}
|
|
262
|
+
if (filters && Object.keys(filters).length > 0) {
|
|
263
|
+
const nonEmptyFilters = Object.fromEntries(
|
|
264
|
+
Object.entries(filters).filter(([, v]) => v !== void 0)
|
|
265
|
+
);
|
|
266
|
+
if (Object.keys(nonEmptyFilters).length > 0) {
|
|
267
|
+
params.set("filters", JSON.stringify(nonEmptyFilters));
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
if (enableColumnResizing && Object.keys(columnWidths).length > 0) {
|
|
271
|
+
params.set("columnWidths", JSON.stringify(columnWidths));
|
|
272
|
+
}
|
|
273
|
+
return params.toString();
|
|
274
|
+
}
|
|
123
275
|
function renderCell(column, row, isLoading, setConfirmState) {
|
|
124
276
|
switch (column.kind) {
|
|
125
277
|
case "text": {
|
|
@@ -236,60 +388,96 @@ function RowaKitTable({
|
|
|
236
388
|
const resizeRafRef = react.useRef(null);
|
|
237
389
|
const resizePendingRef = react.useRef(null);
|
|
238
390
|
const tableRef = react.useRef(null);
|
|
391
|
+
const isResizingRef = react.useRef(false);
|
|
392
|
+
const lastResizeEndTsRef = react.useRef(0);
|
|
393
|
+
const resizingColIdRef = react.useRef(null);
|
|
394
|
+
const didHydrateUrlRef = react.useRef(false);
|
|
395
|
+
const didSkipInitialUrlSyncRef = react.useRef(false);
|
|
396
|
+
const urlSyncDebounceRef = react.useRef(null);
|
|
239
397
|
const [savedViews, setSavedViews] = react.useState([]);
|
|
398
|
+
const [showSaveViewForm, setShowSaveViewForm] = react.useState(false);
|
|
399
|
+
const [saveViewInput, setSaveViewInput] = react.useState("");
|
|
400
|
+
const [saveViewError, setSaveViewError] = react.useState("");
|
|
401
|
+
const [overwriteConfirmName, setOverwriteConfirmName] = react.useState(null);
|
|
402
|
+
react.useEffect(() => {
|
|
403
|
+
if (!enableSavedViews) return;
|
|
404
|
+
const views = loadSavedViewsFromStorage();
|
|
405
|
+
setSavedViews(views);
|
|
406
|
+
}, [enableSavedViews]);
|
|
240
407
|
const [confirmState, setConfirmState] = react.useState(null);
|
|
241
408
|
const requestIdRef = react.useRef(0);
|
|
242
409
|
react.useEffect(() => {
|
|
243
|
-
if (!syncToUrl)
|
|
244
|
-
|
|
245
|
-
|
|
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);
|
|
410
|
+
if (!syncToUrl) {
|
|
411
|
+
didSkipInitialUrlSyncRef.current = false;
|
|
412
|
+
return;
|
|
250
413
|
}
|
|
251
|
-
if (
|
|
252
|
-
|
|
414
|
+
if (!didSkipInitialUrlSyncRef.current) {
|
|
415
|
+
didSkipInitialUrlSyncRef.current = true;
|
|
416
|
+
return;
|
|
253
417
|
}
|
|
254
|
-
if (
|
|
255
|
-
|
|
418
|
+
if (urlSyncDebounceRef.current) {
|
|
419
|
+
clearTimeout(urlSyncDebounceRef.current);
|
|
420
|
+
urlSyncDebounceRef.current = null;
|
|
256
421
|
}
|
|
257
|
-
|
|
258
|
-
|
|
422
|
+
const urlStr = serializeUrlState(query, filters, columnWidths, defaultPageSize, enableColumnResizing);
|
|
423
|
+
const qs = urlStr ? `?${urlStr}` : "";
|
|
424
|
+
window.history.replaceState(null, "", `${window.location.pathname}${qs}${window.location.hash}`);
|
|
425
|
+
}, [query, filters, syncToUrl, enableColumnResizing, defaultPageSize, columnWidths]);
|
|
259
426
|
react.useEffect(() => {
|
|
260
|
-
if (!syncToUrl) return;
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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 };
|
|
427
|
+
if (!syncToUrl || !enableColumnResizing) return;
|
|
428
|
+
if (!didSkipInitialUrlSyncRef.current) return;
|
|
429
|
+
if (urlSyncDebounceRef.current) {
|
|
430
|
+
clearTimeout(urlSyncDebounceRef.current);
|
|
274
431
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
432
|
+
urlSyncDebounceRef.current = setTimeout(() => {
|
|
433
|
+
const urlStr = serializeUrlState(query, filters, columnWidths, defaultPageSize, enableColumnResizing);
|
|
434
|
+
const qs = urlStr ? `?${urlStr}` : "";
|
|
435
|
+
window.history.replaceState(null, "", `${window.location.pathname}${qs}${window.location.hash}`);
|
|
436
|
+
urlSyncDebounceRef.current = null;
|
|
437
|
+
}, 150);
|
|
438
|
+
return () => {
|
|
439
|
+
if (urlSyncDebounceRef.current) {
|
|
440
|
+
clearTimeout(urlSyncDebounceRef.current);
|
|
441
|
+
urlSyncDebounceRef.current = null;
|
|
283
442
|
}
|
|
443
|
+
};
|
|
444
|
+
}, [columnWidths, syncToUrl, enableColumnResizing, query, filters, defaultPageSize]);
|
|
445
|
+
react.useEffect(() => {
|
|
446
|
+
if (!syncToUrl) {
|
|
447
|
+
didHydrateUrlRef.current = false;
|
|
448
|
+
return;
|
|
284
449
|
}
|
|
285
|
-
if (
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
450
|
+
if (didHydrateUrlRef.current) return;
|
|
451
|
+
didHydrateUrlRef.current = true;
|
|
452
|
+
const params = new URLSearchParams(window.location.search);
|
|
453
|
+
const parsed = parseUrlState(params, defaultPageSize, pageSizeOptions);
|
|
454
|
+
setQuery({
|
|
455
|
+
page: parsed.page,
|
|
456
|
+
pageSize: parsed.pageSize,
|
|
457
|
+
sort: parsed.sort,
|
|
458
|
+
filters: parsed.filters
|
|
459
|
+
});
|
|
460
|
+
if (parsed.filters) {
|
|
461
|
+
setFilters(parsed.filters);
|
|
462
|
+
}
|
|
463
|
+
if (parsed.columnWidths && enableColumnResizing) {
|
|
464
|
+
const clamped = {};
|
|
465
|
+
for (const [colId, rawWidth] of Object.entries(parsed.columnWidths)) {
|
|
466
|
+
const widthNum = typeof rawWidth === "number" ? rawWidth : Number(rawWidth);
|
|
467
|
+
if (!Number.isFinite(widthNum)) continue;
|
|
468
|
+
const colDef = columns.find((c) => c.id === colId);
|
|
469
|
+
if (!colDef) continue;
|
|
470
|
+
const minW = colDef.minWidth ?? 80;
|
|
471
|
+
const maxW = colDef.maxWidth;
|
|
472
|
+
let finalW = Math.max(minW, widthNum);
|
|
473
|
+
if (maxW != null) {
|
|
474
|
+
finalW = Math.min(finalW, maxW);
|
|
475
|
+
}
|
|
476
|
+
clamped[colId] = finalW;
|
|
289
477
|
}
|
|
478
|
+
setColumnWidths(clamped);
|
|
290
479
|
}
|
|
291
|
-
|
|
292
|
-
}, [syncToUrl, defaultPageSize, enableColumnResizing]);
|
|
480
|
+
}, [syncToUrl, defaultPageSize, enableColumnResizing, pageSizeOptions, columns]);
|
|
293
481
|
react.useEffect(() => {
|
|
294
482
|
if (!enableFilters) return;
|
|
295
483
|
const activeFilters = {};
|
|
@@ -392,15 +580,52 @@ function RowaKitTable({
|
|
|
392
580
|
if (maxWidth) {
|
|
393
581
|
finalWidth = Math.min(finalWidth, maxWidth);
|
|
394
582
|
}
|
|
583
|
+
if (columnWidths[columnId] === finalWidth) {
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
395
586
|
setColumnWidths((prev) => ({
|
|
396
587
|
...prev,
|
|
397
588
|
[columnId]: finalWidth
|
|
398
589
|
}));
|
|
399
590
|
};
|
|
591
|
+
const autoFitColumnWidth = (columnId) => {
|
|
592
|
+
const tableEl = tableRef.current;
|
|
593
|
+
if (!tableEl) return;
|
|
594
|
+
const th = tableEl.querySelector(`th[data-col-id="${columnId}"]`);
|
|
595
|
+
if (!th) return;
|
|
596
|
+
const tds = Array.from(
|
|
597
|
+
tableEl.querySelectorAll(`td[data-col-id="${columnId}"]`)
|
|
598
|
+
);
|
|
599
|
+
const headerW = th.scrollWidth;
|
|
600
|
+
const cellsMaxW = tds.reduce((max, td) => Math.max(max, td.scrollWidth), 0);
|
|
601
|
+
const padding = 24;
|
|
602
|
+
const raw = Math.max(headerW, cellsMaxW) + padding;
|
|
603
|
+
const colDef = columns.find((c) => c.id === columnId);
|
|
604
|
+
const minW = colDef?.minWidth ?? 80;
|
|
605
|
+
const maxW = colDef?.maxWidth ?? 600;
|
|
606
|
+
const finalW = Math.max(minW, Math.min(raw, maxW));
|
|
607
|
+
setColumnWidths((prev) => ({ ...prev, [columnId]: finalW }));
|
|
608
|
+
};
|
|
400
609
|
const startColumnResize = (e, columnId) => {
|
|
401
610
|
e.preventDefault();
|
|
611
|
+
e.stopPropagation();
|
|
612
|
+
if (e.detail === 2) {
|
|
613
|
+
autoFitColumnWidth(columnId);
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
if (e.pointerType === "mouse" && e.buttons !== 1) {
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
const target = e.currentTarget;
|
|
620
|
+
const pointerId = e.pointerId;
|
|
621
|
+
try {
|
|
622
|
+
target.setPointerCapture(pointerId);
|
|
623
|
+
} catch {
|
|
624
|
+
}
|
|
625
|
+
isResizingRef.current = true;
|
|
626
|
+
resizingColIdRef.current = columnId;
|
|
402
627
|
const startX = e.clientX;
|
|
403
|
-
const th =
|
|
628
|
+
const th = target.parentElement;
|
|
404
629
|
let startWidth = columnWidths[columnId] ?? th.offsetWidth;
|
|
405
630
|
const MIN_DRAG_WIDTH = 80;
|
|
406
631
|
if (startWidth < MIN_DRAG_WIDTH) {
|
|
@@ -412,33 +637,38 @@ function RowaKitTable({
|
|
|
412
637
|
}
|
|
413
638
|
}
|
|
414
639
|
document.body.classList.add("rowakit-resizing");
|
|
415
|
-
const
|
|
640
|
+
const handlePointerMove = (moveEvent) => {
|
|
416
641
|
const delta = moveEvent.clientX - startX;
|
|
417
642
|
const newWidth = startWidth + delta;
|
|
418
643
|
scheduleColumnWidthUpdate(columnId, newWidth);
|
|
419
644
|
};
|
|
420
|
-
const
|
|
421
|
-
|
|
422
|
-
|
|
645
|
+
const cleanupResize = () => {
|
|
646
|
+
target.removeEventListener("pointermove", handlePointerMove);
|
|
647
|
+
target.removeEventListener("pointerup", handlePointerUp);
|
|
648
|
+
target.removeEventListener("pointercancel", handlePointerCancel);
|
|
423
649
|
document.body.classList.remove("rowakit-resizing");
|
|
650
|
+
isResizingRef.current = false;
|
|
651
|
+
resizingColIdRef.current = null;
|
|
652
|
+
lastResizeEndTsRef.current = Date.now();
|
|
653
|
+
try {
|
|
654
|
+
target.releasePointerCapture(pointerId);
|
|
655
|
+
} catch {
|
|
656
|
+
}
|
|
657
|
+
};
|
|
658
|
+
const handlePointerUp = () => {
|
|
659
|
+
cleanupResize();
|
|
660
|
+
};
|
|
661
|
+
const handlePointerCancel = () => {
|
|
662
|
+
cleanupResize();
|
|
424
663
|
};
|
|
425
|
-
|
|
426
|
-
|
|
664
|
+
target.addEventListener("pointermove", handlePointerMove);
|
|
665
|
+
target.addEventListener("pointerup", handlePointerUp);
|
|
666
|
+
target.addEventListener("pointercancel", handlePointerCancel);
|
|
427
667
|
};
|
|
428
|
-
const handleColumnResizeDoubleClick = (columnId) => {
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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 }));
|
|
668
|
+
const handleColumnResizeDoubleClick = (e, columnId) => {
|
|
669
|
+
e.preventDefault();
|
|
670
|
+
e.stopPropagation();
|
|
671
|
+
autoFitColumnWidth(columnId);
|
|
442
672
|
};
|
|
443
673
|
const saveCurrentView = (name) => {
|
|
444
674
|
const viewState = {
|
|
@@ -455,6 +685,10 @@ function RowaKitTable({
|
|
|
455
685
|
if (typeof window !== "undefined" && window.localStorage) {
|
|
456
686
|
try {
|
|
457
687
|
localStorage.setItem(`rowakit-view-${name}`, JSON.stringify(viewState));
|
|
688
|
+
const index = getSavedViewsIndex();
|
|
689
|
+
const filtered = index.filter((v) => v.name !== name);
|
|
690
|
+
filtered.push({ name, updatedAt: Date.now() });
|
|
691
|
+
setSavedViewsIndex(filtered);
|
|
458
692
|
} catch {
|
|
459
693
|
}
|
|
460
694
|
}
|
|
@@ -479,6 +713,9 @@ function RowaKitTable({
|
|
|
479
713
|
if (typeof window !== "undefined" && window.localStorage) {
|
|
480
714
|
try {
|
|
481
715
|
localStorage.removeItem(`rowakit-view-${name}`);
|
|
716
|
+
const index = getSavedViewsIndex();
|
|
717
|
+
const filtered = index.filter((v) => v.name !== name);
|
|
718
|
+
setSavedViewsIndex(filtered);
|
|
482
719
|
} catch {
|
|
483
720
|
}
|
|
484
721
|
}
|
|
@@ -542,22 +779,127 @@ function RowaKitTable({
|
|
|
542
779
|
const canGoPrevious = query.page > 1 && !isLoading;
|
|
543
780
|
const canGoNext = query.page < totalPages && !isLoading;
|
|
544
781
|
const hasActiveFilters = enableFilters && Object.values(filters).some((v) => v !== void 0);
|
|
545
|
-
|
|
782
|
+
const containerClass = [
|
|
783
|
+
"rowakit-table",
|
|
784
|
+
enableColumnResizing ? "rowakit-layout-fixed" : "",
|
|
785
|
+
className
|
|
786
|
+
].filter(Boolean).join(" ");
|
|
787
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: containerClass, children: [
|
|
546
788
|
enableSavedViews && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rowakit-saved-views-group", children: [
|
|
547
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
789
|
+
!showSaveViewForm ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
548
790
|
"button",
|
|
549
791
|
{
|
|
550
792
|
onClick: () => {
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
793
|
+
setShowSaveViewForm(true);
|
|
794
|
+
setSaveViewInput("");
|
|
795
|
+
setSaveViewError("");
|
|
796
|
+
setOverwriteConfirmName(null);
|
|
555
797
|
},
|
|
556
798
|
className: "rowakit-saved-view-button",
|
|
557
799
|
type: "button",
|
|
558
800
|
children: "Save View"
|
|
559
801
|
}
|
|
560
|
-
),
|
|
802
|
+
) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rowakit-save-view-form", children: overwriteConfirmName ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rowakit-save-view-confirm", children: [
|
|
803
|
+
/* @__PURE__ */ jsxRuntime.jsxs("p", { children: [
|
|
804
|
+
'View "',
|
|
805
|
+
overwriteConfirmName,
|
|
806
|
+
'" already exists. Overwrite?'
|
|
807
|
+
] }),
|
|
808
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
809
|
+
"button",
|
|
810
|
+
{
|
|
811
|
+
onClick: () => {
|
|
812
|
+
saveCurrentView(overwriteConfirmName);
|
|
813
|
+
setShowSaveViewForm(false);
|
|
814
|
+
setSaveViewInput("");
|
|
815
|
+
setSaveViewError("");
|
|
816
|
+
setOverwriteConfirmName(null);
|
|
817
|
+
},
|
|
818
|
+
className: "rowakit-saved-view-button",
|
|
819
|
+
type: "button",
|
|
820
|
+
children: "Overwrite"
|
|
821
|
+
}
|
|
822
|
+
),
|
|
823
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
824
|
+
"button",
|
|
825
|
+
{
|
|
826
|
+
onClick: () => {
|
|
827
|
+
setOverwriteConfirmName(null);
|
|
828
|
+
},
|
|
829
|
+
className: "rowakit-saved-view-button",
|
|
830
|
+
type: "button",
|
|
831
|
+
children: "Cancel"
|
|
832
|
+
}
|
|
833
|
+
)
|
|
834
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
835
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
836
|
+
"input",
|
|
837
|
+
{
|
|
838
|
+
type: "text",
|
|
839
|
+
value: saveViewInput,
|
|
840
|
+
onChange: (e) => {
|
|
841
|
+
setSaveViewInput(e.target.value);
|
|
842
|
+
setSaveViewError("");
|
|
843
|
+
},
|
|
844
|
+
onKeyDown: (e) => {
|
|
845
|
+
if (e.key === "Enter") {
|
|
846
|
+
const validation = validateViewName(saveViewInput);
|
|
847
|
+
if (!validation.valid) {
|
|
848
|
+
setSaveViewError(validation.error || "Invalid name");
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
if (savedViews.some((v) => v.name === saveViewInput.trim())) {
|
|
852
|
+
setOverwriteConfirmName(saveViewInput.trim());
|
|
853
|
+
} else {
|
|
854
|
+
saveCurrentView(saveViewInput.trim());
|
|
855
|
+
setShowSaveViewForm(false);
|
|
856
|
+
setSaveViewInput("");
|
|
857
|
+
setSaveViewError("");
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
},
|
|
861
|
+
placeholder: "Enter view name...",
|
|
862
|
+
className: "rowakit-save-view-input"
|
|
863
|
+
}
|
|
864
|
+
),
|
|
865
|
+
saveViewError && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rowakit-save-view-error", children: saveViewError }),
|
|
866
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
867
|
+
"button",
|
|
868
|
+
{
|
|
869
|
+
onClick: () => {
|
|
870
|
+
const validation = validateViewName(saveViewInput);
|
|
871
|
+
if (!validation.valid) {
|
|
872
|
+
setSaveViewError(validation.error || "Invalid name");
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
if (savedViews.some((v) => v.name === saveViewInput.trim())) {
|
|
876
|
+
setOverwriteConfirmName(saveViewInput.trim());
|
|
877
|
+
} else {
|
|
878
|
+
saveCurrentView(saveViewInput.trim());
|
|
879
|
+
setShowSaveViewForm(false);
|
|
880
|
+
setSaveViewInput("");
|
|
881
|
+
setSaveViewError("");
|
|
882
|
+
}
|
|
883
|
+
},
|
|
884
|
+
className: "rowakit-saved-view-button",
|
|
885
|
+
type: "button",
|
|
886
|
+
children: "Save"
|
|
887
|
+
}
|
|
888
|
+
),
|
|
889
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
890
|
+
"button",
|
|
891
|
+
{
|
|
892
|
+
onClick: () => {
|
|
893
|
+
setShowSaveViewForm(false);
|
|
894
|
+
setSaveViewInput("");
|
|
895
|
+
setSaveViewError("");
|
|
896
|
+
},
|
|
897
|
+
className: "rowakit-saved-view-button",
|
|
898
|
+
type: "button",
|
|
899
|
+
children: "Cancel"
|
|
900
|
+
}
|
|
901
|
+
)
|
|
902
|
+
] }) }),
|
|
561
903
|
savedViews.map((view) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rowakit-saved-view-item", children: [
|
|
562
904
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
563
905
|
"button",
|
|
@@ -609,7 +951,11 @@ function RowaKitTable({
|
|
|
609
951
|
"th",
|
|
610
952
|
{
|
|
611
953
|
"data-col-id": column.id,
|
|
612
|
-
onClick: isSortable ? () =>
|
|
954
|
+
onClick: isSortable ? () => {
|
|
955
|
+
if (isResizingRef.current) return;
|
|
956
|
+
if (Date.now() - lastResizeEndTsRef.current < 150) return;
|
|
957
|
+
handleSort(String(field));
|
|
958
|
+
} : void 0,
|
|
613
959
|
role: isSortable ? "button" : void 0,
|
|
614
960
|
tabIndex: isSortable ? 0 : void 0,
|
|
615
961
|
onKeyDown: isSortable ? (e) => {
|
|
@@ -620,11 +966,15 @@ function RowaKitTable({
|
|
|
620
966
|
} : void 0,
|
|
621
967
|
"aria-sort": isSortable && query.sort?.field === String(field) ? query.sort.direction === "asc" ? "ascending" : "descending" : void 0,
|
|
622
968
|
style: {
|
|
623
|
-
width: actualWidth ? `${actualWidth}px` : void 0,
|
|
969
|
+
width: actualWidth != null ? `${actualWidth}px` : void 0,
|
|
624
970
|
textAlign: column.align,
|
|
625
971
|
position: isResizable ? "relative" : void 0
|
|
626
972
|
},
|
|
627
|
-
className:
|
|
973
|
+
className: [
|
|
974
|
+
column.truncate ? "rowakit-cell-truncate" : "",
|
|
975
|
+
resizingColIdRef.current === column.id ? "resizing" : ""
|
|
976
|
+
// PRD-01
|
|
977
|
+
].filter(Boolean).join(" ") || void 0,
|
|
628
978
|
children: [
|
|
629
979
|
getHeaderLabel(column),
|
|
630
980
|
isSortable && getSortIndicator(String(field)),
|
|
@@ -632,8 +982,8 @@ function RowaKitTable({
|
|
|
632
982
|
"div",
|
|
633
983
|
{
|
|
634
984
|
className: "rowakit-column-resize-handle",
|
|
635
|
-
|
|
636
|
-
onDoubleClick: () => handleColumnResizeDoubleClick(column.id),
|
|
985
|
+
onPointerDown: (e) => startColumnResize(e, column.id),
|
|
986
|
+
onDoubleClick: (e) => handleColumnResizeDoubleClick(e, column.id),
|
|
637
987
|
title: "Drag to resize | Double-click to auto-fit content"
|
|
638
988
|
}
|
|
639
989
|
)
|
|
@@ -832,14 +1182,14 @@ function RowaKitTable({
|
|
|
832
1182
|
column.kind === "number" ? "rowakit-cell-number" : "",
|
|
833
1183
|
column.truncate ? "rowakit-cell-truncate" : ""
|
|
834
1184
|
].filter(Boolean).join(" ") || void 0;
|
|
835
|
-
const actualWidth = columnWidths[column.id];
|
|
1185
|
+
const actualWidth = columnWidths[column.id] ?? column.width;
|
|
836
1186
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
837
1187
|
"td",
|
|
838
1188
|
{
|
|
839
1189
|
"data-col-id": column.id,
|
|
840
1190
|
className: cellClass,
|
|
841
1191
|
style: {
|
|
842
|
-
width: actualWidth ? `${actualWidth}px` : void 0,
|
|
1192
|
+
width: actualWidth != null ? `${actualWidth}px` : void 0,
|
|
843
1193
|
textAlign: column.align || (column.kind === "number" ? "right" : void 0)
|
|
844
1194
|
},
|
|
845
1195
|
children: renderCell(column, row, isLoading, setConfirmState)
|
|
@@ -944,7 +1294,7 @@ function RowaKitTable({
|
|
|
944
1294
|
var SmartTable = RowaKitTable;
|
|
945
1295
|
|
|
946
1296
|
// src/index.ts
|
|
947
|
-
var VERSION = "0.
|
|
1297
|
+
var VERSION = "0.4.0";
|
|
948
1298
|
|
|
949
1299
|
exports.RowaKitTable = RowaKitTable;
|
|
950
1300
|
exports.SmartTable = SmartTable;
|