@rowakit/table 0.4.0 → 1.0.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/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { useState, useRef, useEffect } from 'react';
1
+ import { useState, useRef, useEffect, useMemo } from 'react';
2
2
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
3
3
 
4
4
  // src/column-helpers.ts
@@ -103,20 +103,178 @@ var col = {
103
103
  actions,
104
104
  custom
105
105
  };
106
- function getRowKey(row, rowKey) {
107
- if (typeof rowKey === "function") {
108
- return rowKey(row);
109
- }
110
- if (rowKey) {
111
- return String(row[rowKey]);
112
- }
113
- if (row && typeof row === "object" && "id" in row) {
114
- return String(row.id);
115
- }
116
- return String(row);
106
+ function useColumnResizing(columns) {
107
+ const [columnWidths, setColumnWidths] = useState({});
108
+ const resizeRafRef = useRef(null);
109
+ const resizePendingRef = useRef(null);
110
+ const tableRef = useRef(null);
111
+ const isResizingRef = useRef(false);
112
+ const lastResizeEndTsRef = useRef(0);
113
+ const resizingColIdRef = useRef(null);
114
+ const scheduleColumnWidthUpdate = (colId, width) => {
115
+ resizePendingRef.current = { colId, width };
116
+ if (resizeRafRef.current != null) return;
117
+ resizeRafRef.current = requestAnimationFrame(() => {
118
+ resizeRafRef.current = null;
119
+ const pending = resizePendingRef.current;
120
+ if (!pending) return;
121
+ handleColumnResize(pending.colId, pending.width);
122
+ });
123
+ };
124
+ const handleColumnResize = (columnId, newWidth) => {
125
+ const minWidth = columns.find((c) => c.id === columnId)?.minWidth ?? 80;
126
+ const maxWidth = columns.find((c) => c.id === columnId)?.maxWidth;
127
+ let finalWidth = Math.max(minWidth, newWidth);
128
+ if (maxWidth) {
129
+ finalWidth = Math.min(finalWidth, maxWidth);
130
+ }
131
+ if (columnWidths[columnId] === finalWidth) {
132
+ return;
133
+ }
134
+ setColumnWidths((prev) => ({
135
+ ...prev,
136
+ [columnId]: finalWidth
137
+ }));
138
+ };
139
+ const autoFitColumnWidth = (columnId) => {
140
+ const tableEl = tableRef.current;
141
+ if (!tableEl) return;
142
+ const th = tableEl.querySelector(`th[data-col-id="${columnId}"]`);
143
+ if (!th) return;
144
+ const tds = Array.from(
145
+ tableEl.querySelectorAll(`td[data-col-id="${columnId}"]`)
146
+ );
147
+ const headerW = th.scrollWidth;
148
+ const cellsMaxW = tds.reduce((max, td) => Math.max(max, td.scrollWidth), 0);
149
+ const padding = 24;
150
+ const raw = Math.max(headerW, cellsMaxW) + padding;
151
+ const colDef = columns.find((c) => c.id === columnId);
152
+ const minW = colDef?.minWidth ?? 80;
153
+ const maxW = colDef?.maxWidth ?? 600;
154
+ const finalW = Math.max(minW, Math.min(raw, maxW));
155
+ setColumnWidths((prev) => ({ ...prev, [columnId]: finalW }));
156
+ };
157
+ const startColumnResize = (e, columnId) => {
158
+ e.preventDefault();
159
+ e.stopPropagation();
160
+ if (e.detail === 2) {
161
+ autoFitColumnWidth(columnId);
162
+ return;
163
+ }
164
+ if (e.pointerType === "mouse" && e.buttons !== 1) {
165
+ return;
166
+ }
167
+ const target = e.currentTarget;
168
+ const pointerId = e.pointerId;
169
+ try {
170
+ target.setPointerCapture(pointerId);
171
+ } catch {
172
+ }
173
+ isResizingRef.current = true;
174
+ resizingColIdRef.current = columnId;
175
+ const startX = e.clientX;
176
+ const th = target.parentElement;
177
+ let startWidth = columnWidths[columnId] ?? th.offsetWidth;
178
+ const MIN_DRAG_WIDTH = 80;
179
+ if (startWidth < MIN_DRAG_WIDTH) {
180
+ const nextTh = th.nextElementSibling;
181
+ if (nextTh && nextTh.offsetWidth >= 50) {
182
+ startWidth = nextTh.offsetWidth;
183
+ } else {
184
+ startWidth = 100;
185
+ }
186
+ }
187
+ document.body.classList.add("rowakit-resizing");
188
+ const handlePointerMove = (moveEvent) => {
189
+ const delta = moveEvent.clientX - startX;
190
+ const newWidth = startWidth + delta;
191
+ scheduleColumnWidthUpdate(columnId, newWidth);
192
+ };
193
+ const cleanupResize = () => {
194
+ target.removeEventListener("pointermove", handlePointerMove);
195
+ target.removeEventListener("pointerup", handlePointerUp);
196
+ target.removeEventListener("pointercancel", handlePointerCancel);
197
+ document.body.classList.remove("rowakit-resizing");
198
+ isResizingRef.current = false;
199
+ resizingColIdRef.current = null;
200
+ lastResizeEndTsRef.current = Date.now();
201
+ try {
202
+ target.releasePointerCapture(pointerId);
203
+ } catch {
204
+ }
205
+ };
206
+ const handlePointerUp = () => {
207
+ cleanupResize();
208
+ };
209
+ const handlePointerCancel = () => {
210
+ cleanupResize();
211
+ };
212
+ target.addEventListener("pointermove", handlePointerMove);
213
+ target.addEventListener("pointerup", handlePointerUp);
214
+ target.addEventListener("pointercancel", handlePointerCancel);
215
+ };
216
+ const handleColumnResizeDoubleClick = (e, columnId) => {
217
+ e.preventDefault();
218
+ e.stopPropagation();
219
+ autoFitColumnWidth(columnId);
220
+ };
221
+ return {
222
+ tableRef,
223
+ columnWidths,
224
+ setColumnWidths,
225
+ startColumnResize,
226
+ handleColumnResizeDoubleClick,
227
+ isResizingRef,
228
+ lastResizeEndTsRef,
229
+ resizingColIdRef
230
+ };
117
231
  }
118
- function getHeaderLabel(column) {
119
- return column.header ?? column.id;
232
+ function useFetcherState(fetcher, query, setQuery) {
233
+ const [dataState, setDataState] = useState({
234
+ state: "idle",
235
+ items: [],
236
+ total: 0
237
+ });
238
+ const requestIdRef = useRef(0);
239
+ useEffect(() => {
240
+ const currentRequestId = ++requestIdRef.current;
241
+ setDataState((prev) => ({ ...prev, state: "loading" }));
242
+ fetcher(query).then((result) => {
243
+ if (currentRequestId !== requestIdRef.current) return;
244
+ if (result.items.length === 0) {
245
+ setDataState({
246
+ state: "empty",
247
+ items: [],
248
+ total: result.total
249
+ });
250
+ return;
251
+ }
252
+ setDataState({
253
+ state: "success",
254
+ items: result.items,
255
+ total: result.total
256
+ });
257
+ }).catch((error) => {
258
+ if (currentRequestId !== requestIdRef.current) return;
259
+ setDataState({
260
+ state: "error",
261
+ items: [],
262
+ total: 0,
263
+ error: error instanceof Error ? error.message : "Failed to load data"
264
+ });
265
+ });
266
+ }, [fetcher, query]);
267
+ const handleRetry = () => {
268
+ setQuery({ ...query });
269
+ };
270
+ return {
271
+ dataState,
272
+ setDataState,
273
+ handleRetry,
274
+ isLoading: dataState.state === "loading",
275
+ isError: dataState.state === "error",
276
+ isEmpty: dataState.state === "empty"
277
+ };
120
278
  }
121
279
  function validateViewName(name) {
122
280
  const trimmed = name.trim();
@@ -152,10 +310,7 @@ function getSavedViewsIndex() {
152
310
  const key = localStorage.key(i);
153
311
  if (key?.startsWith("rowakit-view-")) {
154
312
  const name = key.substring("rowakit-view-".length);
155
- rebuilt.push({
156
- name,
157
- updatedAt: Date.now()
158
- });
313
+ rebuilt.push({ name, updatedAt: Date.now() });
159
314
  }
160
315
  }
161
316
  } catch {
@@ -182,16 +337,206 @@ function loadSavedViewsFromStorage() {
182
337
  const viewStr = localStorage.getItem(`rowakit-view-${entry.name}`);
183
338
  if (viewStr) {
184
339
  const state = JSON.parse(viewStr);
185
- views.push({
186
- name: entry.name,
187
- state
188
- });
340
+ views.push({ name: entry.name, state });
189
341
  }
190
342
  } catch {
191
343
  }
192
344
  }
193
345
  return views;
194
346
  }
347
+ function useSavedViews(options) {
348
+ const [savedViews, setSavedViews] = useState([]);
349
+ const [showSaveViewForm, setShowSaveViewForm] = useState(false);
350
+ const [saveViewInput, setSaveViewInput] = useState("");
351
+ const [saveViewError, setSaveViewError] = useState("");
352
+ const [overwriteConfirmName, setOverwriteConfirmName] = useState(null);
353
+ useEffect(() => {
354
+ if (!options.enableSavedViews) return;
355
+ setSavedViews(loadSavedViewsFromStorage());
356
+ }, [options.enableSavedViews]);
357
+ const saveCurrentView = (name) => {
358
+ const viewState = {
359
+ page: options.query.page,
360
+ pageSize: options.query.pageSize,
361
+ sort: options.query.sort,
362
+ filters: options.query.filters,
363
+ columnWidths: options.enableColumnResizing ? options.columnWidths : void 0
364
+ };
365
+ setSavedViews((prev) => {
366
+ const filtered = prev.filter((v) => v.name !== name);
367
+ return [...filtered, { name, state: viewState }];
368
+ });
369
+ if (typeof window !== "undefined" && window.localStorage) {
370
+ try {
371
+ localStorage.setItem(`rowakit-view-${name}`, JSON.stringify(viewState));
372
+ const index = getSavedViewsIndex();
373
+ const filtered = index.filter((v) => v.name !== name);
374
+ filtered.push({ name, updatedAt: Date.now() });
375
+ setSavedViewsIndex(filtered);
376
+ } catch {
377
+ }
378
+ }
379
+ };
380
+ const loadSavedView = (name) => {
381
+ const view = savedViews.find((v) => v.name === name);
382
+ if (!view) return;
383
+ const { state } = view;
384
+ options.setQuery({
385
+ page: state.page,
386
+ pageSize: state.pageSize,
387
+ sort: state.sort,
388
+ filters: state.filters
389
+ });
390
+ options.setFilters(state.filters ?? {});
391
+ if (state.columnWidths && options.enableColumnResizing) {
392
+ options.setColumnWidths(state.columnWidths);
393
+ }
394
+ };
395
+ const deleteSavedView = (name) => {
396
+ setSavedViews((prev) => prev.filter((v) => v.name !== name));
397
+ if (typeof window !== "undefined" && window.localStorage) {
398
+ try {
399
+ localStorage.removeItem(`rowakit-view-${name}`);
400
+ const index = getSavedViewsIndex();
401
+ const filtered = index.filter((v) => v.name !== name);
402
+ setSavedViewsIndex(filtered);
403
+ } catch {
404
+ }
405
+ }
406
+ };
407
+ const resetTableState = () => {
408
+ options.setQuery({
409
+ page: 1,
410
+ pageSize: options.defaultPageSize
411
+ });
412
+ options.setFilters({});
413
+ options.setColumnWidths({});
414
+ };
415
+ const shouldShowReset = useMemo(() => {
416
+ if (!options.enableSavedViews) return false;
417
+ return Boolean(options.query.page > 1 || options.query.sort || options.query.filters && Object.keys(options.query.filters).length > 0);
418
+ }, [options.enableSavedViews, options.query.page, options.query.sort, options.query.filters]);
419
+ const openSaveViewForm = () => {
420
+ setShowSaveViewForm(true);
421
+ setSaveViewInput("");
422
+ setSaveViewError("");
423
+ setOverwriteConfirmName(null);
424
+ };
425
+ const cancelSaveViewForm = () => {
426
+ setShowSaveViewForm(false);
427
+ setSaveViewInput("");
428
+ setSaveViewError("");
429
+ setOverwriteConfirmName(null);
430
+ };
431
+ const onSaveViewInputChange = (e) => {
432
+ setSaveViewInput(e.target.value);
433
+ setSaveViewError("");
434
+ };
435
+ const attemptSave = () => {
436
+ const validation = validateViewName(saveViewInput);
437
+ if (!validation.valid) {
438
+ setSaveViewError(validation.error || "Invalid name");
439
+ return;
440
+ }
441
+ const trimmed = saveViewInput.trim();
442
+ if (savedViews.some((v) => v.name === trimmed)) {
443
+ setOverwriteConfirmName(trimmed);
444
+ return;
445
+ }
446
+ saveCurrentView(trimmed);
447
+ cancelSaveViewForm();
448
+ };
449
+ const onSaveViewInputKeyDown = (e) => {
450
+ if (e.key !== "Enter") return;
451
+ attemptSave();
452
+ };
453
+ const confirmOverwrite = () => {
454
+ if (!overwriteConfirmName) return;
455
+ saveCurrentView(overwriteConfirmName);
456
+ cancelSaveViewForm();
457
+ };
458
+ const cancelOverwrite = () => {
459
+ setOverwriteConfirmName(null);
460
+ };
461
+ return {
462
+ savedViews,
463
+ showSaveViewForm,
464
+ saveViewInput,
465
+ saveViewError,
466
+ overwriteConfirmName,
467
+ openSaveViewForm,
468
+ cancelSaveViewForm,
469
+ onSaveViewInputChange,
470
+ onSaveViewInputKeyDown,
471
+ attemptSave,
472
+ confirmOverwrite,
473
+ cancelOverwrite,
474
+ loadSavedView,
475
+ deleteSavedView,
476
+ resetTableState,
477
+ shouldShowReset
478
+ };
479
+ }
480
+
481
+ // src/hooks/useSortingState.ts
482
+ function useSortingState(query, setQuery) {
483
+ const handleSort = (field, isMultiSort = false) => {
484
+ setQuery((prev) => {
485
+ const currentSorts = prev.sorts || [];
486
+ const existingSort = currentSorts.find((s) => s.field === field);
487
+ if (!isMultiSort) {
488
+ if (existingSort?.priority === 0) {
489
+ if (existingSort.direction === "asc") {
490
+ return {
491
+ ...prev,
492
+ sorts: [{ field, direction: "desc", priority: 0 }],
493
+ page: 1
494
+ };
495
+ }
496
+ const { sorts: _removed, ...rest } = prev;
497
+ return {
498
+ ...rest,
499
+ page: 1
500
+ };
501
+ }
502
+ return {
503
+ ...prev,
504
+ sorts: [{ field, direction: "asc", priority: 0 }],
505
+ page: 1
506
+ };
507
+ }
508
+ if (existingSort) {
509
+ const newSorts2 = currentSorts.map((s) => {
510
+ if (s.field === field) {
511
+ const newDirection = s.direction === "asc" ? "desc" : "asc";
512
+ return { ...s, direction: newDirection };
513
+ }
514
+ return s;
515
+ });
516
+ return { ...prev, sorts: newSorts2, page: 1 };
517
+ }
518
+ const nextPriority = currentSorts.length;
519
+ const newSorts = [...currentSorts, { field, direction: "asc", priority: nextPriority }];
520
+ return { ...prev, sorts: newSorts, page: 1 };
521
+ });
522
+ };
523
+ const getSortIndicator = (field) => {
524
+ const sorts = query.sorts || [];
525
+ const sort = sorts.find((s) => s.field === field);
526
+ if (!sort) {
527
+ return "";
528
+ }
529
+ const directionIcon = sort.direction === "asc" ? " \u2191" : " \u2193";
530
+ const priorityLabel = sort.priority === 0 ? "" : ` [${sort.priority + 1}]`;
531
+ return directionIcon + priorityLabel;
532
+ };
533
+ const getSortPriority = (field) => {
534
+ const sorts = query.sorts || [];
535
+ const sort = sorts.find((s) => s.field === field);
536
+ return sort ? sort.priority : null;
537
+ };
538
+ return { handleSort, getSortIndicator, getSortPriority };
539
+ }
195
540
  function parseUrlState(params, defaultPageSize, pageSizeOptions) {
196
541
  const pageStr = params.get("page");
197
542
  let page = 1;
@@ -212,10 +557,33 @@ function parseUrlState(params, defaultPageSize, pageSizeOptions) {
212
557
  }
213
558
  }
214
559
  const result = { page, pageSize };
215
- const sortField = params.get("sortField");
216
- const sortDir = params.get("sortDirection");
217
- if (sortField && (sortDir === "asc" || sortDir === "desc")) {
218
- result.sort = { field: sortField, direction: sortDir };
560
+ const sortsStr = params.get("sorts");
561
+ if (sortsStr) {
562
+ try {
563
+ const parsed = JSON.parse(sortsStr);
564
+ if (Array.isArray(parsed) && parsed.every((s) => typeof s.field === "string" && (s.direction === "asc" || s.direction === "desc") && typeof s.priority === "number")) {
565
+ result.sorts = parsed;
566
+ }
567
+ } catch {
568
+ }
569
+ }
570
+ if (!result.sorts) {
571
+ const sortStr = params.get("sort");
572
+ if (sortStr) {
573
+ const [field, dir] = sortStr.split(":");
574
+ if (field && (dir === "asc" || dir === "desc")) {
575
+ result.sort = { field, direction: dir };
576
+ result.sorts = [{ field, direction: dir, priority: 0 }];
577
+ }
578
+ }
579
+ }
580
+ if (!result.sorts) {
581
+ const sortField = params.get("sortField");
582
+ const sortDir = params.get("sortDirection");
583
+ if (sortField && (sortDir === "asc" || sortDir === "desc")) {
584
+ result.sort = { field: sortField, direction: sortDir };
585
+ result.sorts = [{ field: sortField, direction: sortDir, priority: 0 }];
586
+ }
219
587
  }
220
588
  const filtersStr = params.get("filters");
221
589
  if (filtersStr) {
@@ -253,7 +621,9 @@ function serializeUrlState(query, filters, columnWidths, defaultPageSize, enable
253
621
  if (query.pageSize !== defaultPageSize) {
254
622
  params.set("pageSize", String(query.pageSize));
255
623
  }
256
- if (query.sort) {
624
+ if (query.sorts && query.sorts.length > 0) {
625
+ params.set("sorts", JSON.stringify(query.sorts));
626
+ } else if (query.sort) {
257
627
  params.set("sortField", query.sort.field);
258
628
  params.set("sortDirection", query.sort.direction);
259
629
  }
@@ -270,6 +640,385 @@ function serializeUrlState(query, filters, columnWidths, defaultPageSize, enable
270
640
  }
271
641
  return params.toString();
272
642
  }
643
+ function useUrlSync({
644
+ syncToUrl,
645
+ enableColumnResizing,
646
+ defaultPageSize,
647
+ pageSizeOptions,
648
+ columns,
649
+ query,
650
+ setQuery,
651
+ filters,
652
+ setFilters,
653
+ columnWidths,
654
+ setColumnWidths
655
+ }) {
656
+ const didHydrateUrlRef = useRef(false);
657
+ const urlSyncDebounceRef = useRef(null);
658
+ const isApplyingUrlStateRef = useRef(false);
659
+ const clearApplyingTimerRef = useRef(null);
660
+ const hasWrittenUrlRef = useRef(false);
661
+ const lastQueryForUrlRef = useRef(null);
662
+ const defaultPageSizeRef = useRef(defaultPageSize);
663
+ const pageSizeOptionsRef = useRef(pageSizeOptions);
664
+ const enableColumnResizingRef = useRef(enableColumnResizing);
665
+ const columnsRef = useRef(columns);
666
+ defaultPageSizeRef.current = defaultPageSize;
667
+ pageSizeOptionsRef.current = pageSizeOptions;
668
+ enableColumnResizingRef.current = enableColumnResizing;
669
+ columnsRef.current = columns;
670
+ useEffect(() => {
671
+ if (!syncToUrl) {
672
+ hasWrittenUrlRef.current = false;
673
+ lastQueryForUrlRef.current = null;
674
+ return;
675
+ }
676
+ if (!didHydrateUrlRef.current) return;
677
+ if (isApplyingUrlStateRef.current) return;
678
+ if (urlSyncDebounceRef.current) {
679
+ clearTimeout(urlSyncDebounceRef.current);
680
+ urlSyncDebounceRef.current = null;
681
+ }
682
+ const urlStr = serializeUrlState(query, filters, columnWidths, defaultPageSizeRef.current, enableColumnResizingRef.current);
683
+ const qs = urlStr ? `?${urlStr}` : "";
684
+ const nextUrl = `${window.location.pathname}${qs}${window.location.hash}`;
685
+ const prevQuery = lastQueryForUrlRef.current;
686
+ const shouldPush = hasWrittenUrlRef.current && prevQuery != null && prevQuery.page !== query.page;
687
+ if (shouldPush) {
688
+ window.history.pushState(null, "", nextUrl);
689
+ } else {
690
+ window.history.replaceState(null, "", nextUrl);
691
+ }
692
+ hasWrittenUrlRef.current = true;
693
+ lastQueryForUrlRef.current = query;
694
+ }, [
695
+ query,
696
+ filters,
697
+ syncToUrl,
698
+ columnWidths
699
+ ]);
700
+ useEffect(() => {
701
+ if (!syncToUrl || !enableColumnResizingRef.current) return;
702
+ if (!didHydrateUrlRef.current) return;
703
+ if (!hasWrittenUrlRef.current) return;
704
+ if (isApplyingUrlStateRef.current) return;
705
+ if (urlSyncDebounceRef.current) {
706
+ clearTimeout(urlSyncDebounceRef.current);
707
+ }
708
+ urlSyncDebounceRef.current = setTimeout(() => {
709
+ const urlStr = serializeUrlState(query, filters, columnWidths, defaultPageSizeRef.current, enableColumnResizingRef.current);
710
+ const qs = urlStr ? `?${urlStr}` : "";
711
+ window.history.replaceState(null, "", `${window.location.pathname}${qs}${window.location.hash}`);
712
+ urlSyncDebounceRef.current = null;
713
+ }, 150);
714
+ return () => {
715
+ if (urlSyncDebounceRef.current) {
716
+ clearTimeout(urlSyncDebounceRef.current);
717
+ urlSyncDebounceRef.current = null;
718
+ }
719
+ };
720
+ }, [
721
+ columnWidths,
722
+ syncToUrl,
723
+ query,
724
+ filters
725
+ ]);
726
+ function scheduleClearApplyingFlag() {
727
+ if (clearApplyingTimerRef.current) {
728
+ clearTimeout(clearApplyingTimerRef.current);
729
+ clearApplyingTimerRef.current = null;
730
+ }
731
+ clearApplyingTimerRef.current = setTimeout(() => {
732
+ isApplyingUrlStateRef.current = false;
733
+ clearApplyingTimerRef.current = null;
734
+ }, 0);
735
+ }
736
+ function applyUrlToState() {
737
+ const params = new URLSearchParams(window.location.search);
738
+ const parsed = parseUrlState(params, defaultPageSizeRef.current, pageSizeOptionsRef.current);
739
+ const nextQuery = {
740
+ page: parsed.page,
741
+ pageSize: parsed.pageSize,
742
+ sort: parsed.sort,
743
+ sorts: parsed.sorts,
744
+ filters: parsed.filters
745
+ };
746
+ isApplyingUrlStateRef.current = true;
747
+ scheduleClearApplyingFlag();
748
+ setQuery(nextQuery);
749
+ setFilters(parsed.filters ?? {});
750
+ if (!hasWrittenUrlRef.current) {
751
+ const urlStr = serializeUrlState(
752
+ nextQuery,
753
+ parsed.filters ?? {},
754
+ parsed.columnWidths ?? {},
755
+ defaultPageSizeRef.current,
756
+ enableColumnResizingRef.current
757
+ );
758
+ const qs = urlStr ? `?${urlStr}` : "";
759
+ window.history.replaceState(null, "", `${window.location.pathname}${qs}${window.location.hash}`);
760
+ hasWrittenUrlRef.current = true;
761
+ lastQueryForUrlRef.current = nextQuery;
762
+ }
763
+ if (enableColumnResizingRef.current && parsed.columnWidths) {
764
+ const clamped = {};
765
+ for (const [colId, rawWidth] of Object.entries(parsed.columnWidths)) {
766
+ const widthNum = typeof rawWidth === "number" ? rawWidth : Number(rawWidth);
767
+ if (!Number.isFinite(widthNum)) continue;
768
+ const colDef = columnsRef.current.find((c) => c.id === colId);
769
+ if (!colDef) continue;
770
+ const minW = colDef.minWidth ?? 80;
771
+ const maxW = colDef.maxWidth;
772
+ let finalW = Math.max(minW, widthNum);
773
+ if (maxW != null) {
774
+ finalW = Math.min(finalW, maxW);
775
+ }
776
+ clamped[colId] = finalW;
777
+ }
778
+ setColumnWidths(clamped);
779
+ } else if (enableColumnResizingRef.current) {
780
+ setColumnWidths({});
781
+ }
782
+ }
783
+ useEffect(() => {
784
+ if (!syncToUrl) {
785
+ didHydrateUrlRef.current = false;
786
+ hasWrittenUrlRef.current = false;
787
+ lastQueryForUrlRef.current = null;
788
+ return;
789
+ }
790
+ if (didHydrateUrlRef.current) return;
791
+ didHydrateUrlRef.current = true;
792
+ applyUrlToState();
793
+ }, [
794
+ syncToUrl,
795
+ setQuery,
796
+ setFilters,
797
+ setColumnWidths
798
+ ]);
799
+ useEffect(() => {
800
+ if (!syncToUrl) return;
801
+ const onPopState = () => {
802
+ applyUrlToState();
803
+ };
804
+ window.addEventListener("popstate", onPopState);
805
+ return () => {
806
+ window.removeEventListener("popstate", onPopState);
807
+ };
808
+ }, [syncToUrl, setQuery, setFilters, setColumnWidths]);
809
+ useEffect(() => {
810
+ return () => {
811
+ if (clearApplyingTimerRef.current) {
812
+ clearTimeout(clearApplyingTimerRef.current);
813
+ clearApplyingTimerRef.current = null;
814
+ }
815
+ };
816
+ }, []);
817
+ }
818
+ var FOCUSABLE_SELECTORS = [
819
+ "button",
820
+ "[href]",
821
+ "input",
822
+ "select",
823
+ "textarea",
824
+ '[tabindex]:not([tabindex="-1"])'
825
+ ].join(",");
826
+ function useFocusTrap(ref, options = {}) {
827
+ const { onEscape, autoFocus = true } = options;
828
+ const firstFocusableRef = useRef(null);
829
+ const lastFocusableRef = useRef(null);
830
+ useEffect(() => {
831
+ const modalEl = ref.current;
832
+ if (!modalEl) return;
833
+ const getFocusableElements = () => {
834
+ const elements = Array.from(modalEl.querySelectorAll(FOCUSABLE_SELECTORS));
835
+ return elements.filter((el) => !el.hasAttribute("disabled") && el.offsetParent !== null);
836
+ };
837
+ let focusableElements = getFocusableElements();
838
+ if (focusableElements.length === 0) return;
839
+ firstFocusableRef.current = focusableElements[0] || null;
840
+ lastFocusableRef.current = focusableElements[focusableElements.length - 1] || null;
841
+ if (autoFocus && firstFocusableRef.current) {
842
+ firstFocusableRef.current.focus();
843
+ }
844
+ const handleKeyDown = (e) => {
845
+ focusableElements = getFocusableElements();
846
+ if (focusableElements.length === 0) return;
847
+ const activeEl = document.activeElement;
848
+ const firstEl = focusableElements[0] || null;
849
+ const lastEl = focusableElements[focusableElements.length - 1] || null;
850
+ if (e.key === "Escape") {
851
+ e.preventDefault();
852
+ onEscape?.();
853
+ return;
854
+ }
855
+ if (e.key === "Tab") {
856
+ if (e.shiftKey) {
857
+ if (activeEl === firstEl && lastEl) {
858
+ e.preventDefault();
859
+ lastEl.focus();
860
+ }
861
+ } else {
862
+ if (activeEl === lastEl && firstEl) {
863
+ e.preventDefault();
864
+ firstEl.focus();
865
+ }
866
+ }
867
+ }
868
+ };
869
+ modalEl.addEventListener("keydown", handleKeyDown);
870
+ return () => {
871
+ modalEl.removeEventListener("keydown", handleKeyDown);
872
+ };
873
+ }, [ref, onEscape, autoFocus]);
874
+ }
875
+ function RowSelectionHeaderCell(props) {
876
+ const checkboxRef = useRef(null);
877
+ useEffect(() => {
878
+ if (!checkboxRef.current) return;
879
+ checkboxRef.current.indeterminate = props.indeterminate;
880
+ }, [props.indeterminate]);
881
+ return /* @__PURE__ */ jsx("th", { children: /* @__PURE__ */ jsx(
882
+ "input",
883
+ {
884
+ ref: checkboxRef,
885
+ type: "checkbox",
886
+ "aria-label": "Select all rows",
887
+ disabled: props.disabled,
888
+ checked: props.checked,
889
+ onChange: (e) => props.onChange(e.target.checked)
890
+ }
891
+ ) });
892
+ }
893
+ function RowSelectionCell(props) {
894
+ return /* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx(
895
+ "input",
896
+ {
897
+ type: "checkbox",
898
+ "aria-label": `Select row ${props.rowKey}`,
899
+ disabled: props.disabled,
900
+ checked: props.checked,
901
+ onChange: (e) => props.onChange(e.target.checked)
902
+ }
903
+ ) });
904
+ }
905
+ function BulkActionBar(props) {
906
+ if (props.selectedCount <= 0) return null;
907
+ return /* @__PURE__ */ jsxs("div", { className: "rowakit-bulk-action-bar", children: [
908
+ /* @__PURE__ */ jsxs("span", { children: [
909
+ props.selectedCount,
910
+ " selected"
911
+ ] }),
912
+ props.actions.map((action) => /* @__PURE__ */ jsx(
913
+ "button",
914
+ {
915
+ type: "button",
916
+ className: "rowakit-button rowakit-button-secondary",
917
+ onClick: () => props.onActionClick(action.id),
918
+ children: action.label
919
+ },
920
+ action.id
921
+ ))
922
+ ] });
923
+ }
924
+ function downloadBlob(blob, filename) {
925
+ if (typeof window === "undefined") return;
926
+ if (typeof URL === "undefined" || typeof URL.createObjectURL !== "function") return;
927
+ const url = URL.createObjectURL(blob);
928
+ try {
929
+ const a = document.createElement("a");
930
+ a.href = url;
931
+ a.download = filename;
932
+ a.rel = "noopener noreferrer";
933
+ a.click();
934
+ } finally {
935
+ URL.revokeObjectURL(url);
936
+ }
937
+ }
938
+ function openUrl(url) {
939
+ if (typeof window === "undefined") return;
940
+ if (typeof window.open === "function") {
941
+ window.open(url, "_blank", "noopener,noreferrer");
942
+ return;
943
+ }
944
+ try {
945
+ window.location.assign(url);
946
+ } catch {
947
+ }
948
+ }
949
+ function ExportButton(props) {
950
+ const [isExporting, setIsExporting] = useState(false);
951
+ const [error, setError] = useState(null);
952
+ const onClick = async () => {
953
+ if (isExporting) return;
954
+ setIsExporting(true);
955
+ setError(null);
956
+ try {
957
+ const snapshot = { ...props.query };
958
+ const result = await props.exporter(snapshot);
959
+ if (result instanceof Blob) {
960
+ downloadBlob(result, "rowakit-export.csv");
961
+ return;
962
+ }
963
+ openUrl(result.url);
964
+ } catch (e) {
965
+ setError(e instanceof Error ? e.message : "Export failed");
966
+ } finally {
967
+ setIsExporting(false);
968
+ }
969
+ };
970
+ return /* @__PURE__ */ jsxs("div", { className: "rowakit-export", children: [
971
+ /* @__PURE__ */ jsx(
972
+ "button",
973
+ {
974
+ type: "button",
975
+ className: "rowakit-button rowakit-button-secondary",
976
+ onClick,
977
+ disabled: isExporting,
978
+ children: isExporting ? "Exporting\u2026" : "Export CSV"
979
+ }
980
+ ),
981
+ error && /* @__PURE__ */ jsx("div", { className: "rowakit-export-error", children: error })
982
+ ] });
983
+ }
984
+
985
+ // src/state/selection.ts
986
+ function toggleSelectionKey(selected, key) {
987
+ if (selected.includes(key)) {
988
+ return selected.filter((k) => k !== key);
989
+ }
990
+ return [...selected, key];
991
+ }
992
+ function isAllSelected(selected, pageKeys) {
993
+ if (pageKeys.length === 0) return false;
994
+ return pageKeys.every((k) => selected.includes(k));
995
+ }
996
+ function isIndeterminate(selected, pageKeys) {
997
+ if (pageKeys.length === 0) return false;
998
+ const selectedCount = pageKeys.filter((k) => selected.includes(k)).length;
999
+ return selectedCount > 0 && selectedCount < pageKeys.length;
1000
+ }
1001
+ function selectAll(pageKeys) {
1002
+ return [...pageKeys];
1003
+ }
1004
+ function clearSelection() {
1005
+ return [];
1006
+ }
1007
+ function getRowKey(row, rowKey) {
1008
+ if (typeof rowKey === "function") {
1009
+ return rowKey(row);
1010
+ }
1011
+ if (rowKey) {
1012
+ return String(row[rowKey]);
1013
+ }
1014
+ if (row && typeof row === "object" && "id" in row) {
1015
+ return String(row.id);
1016
+ }
1017
+ return String(row);
1018
+ }
1019
+ function getHeaderLabel(column) {
1020
+ return column.header ?? column.id;
1021
+ }
273
1022
  function renderCell(column, row, isLoading, setConfirmState) {
274
1023
  switch (column.kind) {
275
1024
  case "text": {
@@ -351,380 +1100,146 @@ function renderCell(column, row, isLoading, setConfirmState) {
351
1100
  );
352
1101
  }) });
353
1102
  }
354
- case "custom": {
355
- return column.render(row);
356
- }
357
- default: {
358
- const _exhaustive = column;
359
- return _exhaustive;
360
- }
361
- }
362
- }
363
- function RowaKitTable({
364
- fetcher,
365
- columns,
366
- defaultPageSize = 20,
367
- pageSizeOptions = [10, 20, 50],
368
- rowKey,
369
- className = "",
370
- enableFilters = false,
371
- enableColumnResizing = false,
372
- syncToUrl = false,
373
- enableSavedViews = false
374
- }) {
375
- const [dataState, setDataState] = useState({
376
- state: "idle",
377
- items: [],
378
- total: 0
379
- });
380
- const [query, setQuery] = useState({
381
- page: 1,
382
- pageSize: defaultPageSize
383
- });
384
- const [filters, setFilters] = useState({});
385
- const [columnWidths, setColumnWidths] = useState({});
386
- const resizeRafRef = useRef(null);
387
- const resizePendingRef = useRef(null);
388
- const tableRef = useRef(null);
389
- const isResizingRef = useRef(false);
390
- const lastResizeEndTsRef = useRef(0);
391
- const resizingColIdRef = useRef(null);
392
- const didHydrateUrlRef = useRef(false);
393
- const didSkipInitialUrlSyncRef = useRef(false);
394
- const urlSyncDebounceRef = useRef(null);
395
- const [savedViews, setSavedViews] = useState([]);
396
- const [showSaveViewForm, setShowSaveViewForm] = useState(false);
397
- const [saveViewInput, setSaveViewInput] = useState("");
398
- const [saveViewError, setSaveViewError] = useState("");
399
- const [overwriteConfirmName, setOverwriteConfirmName] = useState(null);
400
- useEffect(() => {
401
- if (!enableSavedViews) return;
402
- const views = loadSavedViewsFromStorage();
403
- setSavedViews(views);
404
- }, [enableSavedViews]);
405
- const [confirmState, setConfirmState] = useState(null);
406
- const requestIdRef = useRef(0);
407
- useEffect(() => {
408
- if (!syncToUrl) {
409
- didSkipInitialUrlSyncRef.current = false;
410
- return;
411
- }
412
- if (!didSkipInitialUrlSyncRef.current) {
413
- didSkipInitialUrlSyncRef.current = true;
414
- return;
415
- }
416
- if (urlSyncDebounceRef.current) {
417
- clearTimeout(urlSyncDebounceRef.current);
418
- urlSyncDebounceRef.current = null;
419
- }
420
- const urlStr = serializeUrlState(query, filters, columnWidths, defaultPageSize, enableColumnResizing);
421
- const qs = urlStr ? `?${urlStr}` : "";
422
- window.history.replaceState(null, "", `${window.location.pathname}${qs}${window.location.hash}`);
423
- }, [query, filters, syncToUrl, enableColumnResizing, defaultPageSize, columnWidths]);
424
- useEffect(() => {
425
- if (!syncToUrl || !enableColumnResizing) return;
426
- if (!didSkipInitialUrlSyncRef.current) return;
427
- if (urlSyncDebounceRef.current) {
428
- clearTimeout(urlSyncDebounceRef.current);
429
- }
430
- urlSyncDebounceRef.current = setTimeout(() => {
431
- const urlStr = serializeUrlState(query, filters, columnWidths, defaultPageSize, enableColumnResizing);
432
- const qs = urlStr ? `?${urlStr}` : "";
433
- window.history.replaceState(null, "", `${window.location.pathname}${qs}${window.location.hash}`);
434
- urlSyncDebounceRef.current = null;
435
- }, 150);
436
- return () => {
437
- if (urlSyncDebounceRef.current) {
438
- clearTimeout(urlSyncDebounceRef.current);
439
- urlSyncDebounceRef.current = null;
440
- }
441
- };
442
- }, [columnWidths, syncToUrl, enableColumnResizing, query, filters, defaultPageSize]);
443
- useEffect(() => {
444
- if (!syncToUrl) {
445
- didHydrateUrlRef.current = false;
446
- return;
447
- }
448
- if (didHydrateUrlRef.current) return;
449
- didHydrateUrlRef.current = true;
450
- const params = new URLSearchParams(window.location.search);
451
- const parsed = parseUrlState(params, defaultPageSize, pageSizeOptions);
452
- setQuery({
453
- page: parsed.page,
454
- pageSize: parsed.pageSize,
455
- sort: parsed.sort,
456
- filters: parsed.filters
457
- });
458
- if (parsed.filters) {
459
- setFilters(parsed.filters);
460
- }
461
- if (parsed.columnWidths && enableColumnResizing) {
462
- const clamped = {};
463
- for (const [colId, rawWidth] of Object.entries(parsed.columnWidths)) {
464
- const widthNum = typeof rawWidth === "number" ? rawWidth : Number(rawWidth);
465
- if (!Number.isFinite(widthNum)) continue;
466
- const colDef = columns.find((c) => c.id === colId);
467
- if (!colDef) continue;
468
- const minW = colDef.minWidth ?? 80;
469
- const maxW = colDef.maxWidth;
470
- let finalW = Math.max(minW, widthNum);
471
- if (maxW != null) {
472
- finalW = Math.min(finalW, maxW);
473
- }
474
- clamped[colId] = finalW;
475
- }
476
- setColumnWidths(clamped);
477
- }
478
- }, [syncToUrl, defaultPageSize, enableColumnResizing, pageSizeOptions, columns]);
479
- useEffect(() => {
480
- if (!enableFilters) return;
481
- const activeFilters = {};
482
- let hasFilters = false;
483
- for (const [field, value] of Object.entries(filters)) {
484
- if (value !== void 0) {
485
- activeFilters[field] = value;
486
- hasFilters = true;
487
- }
488
- }
489
- const filtersToSend = hasFilters ? activeFilters : void 0;
490
- setQuery((prev) => ({
491
- ...prev,
492
- filters: filtersToSend,
493
- page: 1
494
- // Reset page to 1 when filters change
495
- }));
496
- }, [filters, enableFilters]);
497
- useEffect(() => {
498
- const currentRequestId = ++requestIdRef.current;
499
- setDataState((prev) => ({ ...prev, state: "loading" }));
500
- fetcher(query).then((result) => {
501
- if (currentRequestId !== requestIdRef.current) {
502
- return;
503
- }
504
- if (result.items.length === 0) {
505
- setDataState({
506
- state: "empty",
507
- items: [],
508
- total: result.total
509
- });
510
- } else {
511
- setDataState({
512
- state: "success",
513
- items: result.items,
514
- total: result.total
515
- });
516
- }
517
- }).catch((error) => {
518
- if (currentRequestId !== requestIdRef.current) {
519
- return;
520
- }
521
- setDataState({
522
- state: "error",
523
- items: [],
524
- total: 0,
525
- error: error instanceof Error ? error.message : "Failed to load data"
526
- });
527
- });
528
- }, [fetcher, query]);
529
- const handleRetry = () => {
530
- setQuery({ ...query });
531
- };
532
- const handlePreviousPage = () => {
533
- if (query.page > 1) {
534
- setQuery((prev) => ({ ...prev, page: prev.page - 1 }));
535
- }
536
- };
537
- const handleNextPage = () => {
538
- const totalPages2 = Math.ceil(dataState.total / query.pageSize);
539
- if (query.page < totalPages2) {
540
- setQuery((prev) => ({ ...prev, page: prev.page + 1 }));
541
- }
542
- };
543
- const handlePageSizeChange = (newPageSize) => {
544
- setQuery({ ...query, pageSize: newPageSize, page: 1 });
545
- };
546
- const handleSort = (field) => {
547
- setQuery((prev) => {
548
- if (prev.sort?.field !== field) {
549
- return { ...prev, sort: { field, direction: "asc" }, page: 1 };
550
- }
551
- if (prev.sort.direction === "asc") {
552
- return { ...prev, sort: { field, direction: "desc" }, page: 1 };
553
- }
554
- const { sort: _sort, ...rest } = prev;
555
- return { ...rest, page: 1 };
556
- });
557
- };
558
- const getSortIndicator = (field) => {
559
- if (!query.sort || query.sort.field !== field) {
560
- return "";
561
- }
562
- return query.sort.direction === "asc" ? " \u2191" : " \u2193";
563
- };
564
- const scheduleColumnWidthUpdate = (colId, width) => {
565
- resizePendingRef.current = { colId, width };
566
- if (resizeRafRef.current != null) return;
567
- resizeRafRef.current = requestAnimationFrame(() => {
568
- resizeRafRef.current = null;
569
- const pending = resizePendingRef.current;
570
- if (!pending) return;
571
- handleColumnResize(pending.colId, pending.width);
572
- });
573
- };
574
- const handleColumnResize = (columnId, newWidth) => {
575
- const minWidth = columns.find((c) => c.id === columnId)?.minWidth ?? 80;
576
- const maxWidth = columns.find((c) => c.id === columnId)?.maxWidth;
577
- let finalWidth = Math.max(minWidth, newWidth);
578
- if (maxWidth) {
579
- finalWidth = Math.min(finalWidth, maxWidth);
580
- }
581
- if (columnWidths[columnId] === finalWidth) {
582
- return;
583
- }
584
- setColumnWidths((prev) => ({
585
- ...prev,
586
- [columnId]: finalWidth
587
- }));
588
- };
589
- const autoFitColumnWidth = (columnId) => {
590
- const tableEl = tableRef.current;
591
- if (!tableEl) return;
592
- const th = tableEl.querySelector(`th[data-col-id="${columnId}"]`);
593
- if (!th) return;
594
- const tds = Array.from(
595
- tableEl.querySelectorAll(`td[data-col-id="${columnId}"]`)
596
- );
597
- const headerW = th.scrollWidth;
598
- const cellsMaxW = tds.reduce((max, td) => Math.max(max, td.scrollWidth), 0);
599
- const padding = 24;
600
- const raw = Math.max(headerW, cellsMaxW) + padding;
601
- const colDef = columns.find((c) => c.id === columnId);
602
- const minW = colDef?.minWidth ?? 80;
603
- const maxW = colDef?.maxWidth ?? 600;
604
- const finalW = Math.max(minW, Math.min(raw, maxW));
605
- setColumnWidths((prev) => ({ ...prev, [columnId]: finalW }));
606
- };
607
- const startColumnResize = (e, columnId) => {
608
- e.preventDefault();
609
- e.stopPropagation();
610
- if (e.detail === 2) {
611
- autoFitColumnWidth(columnId);
612
- return;
613
- }
614
- if (e.pointerType === "mouse" && e.buttons !== 1) {
615
- return;
1103
+ case "custom": {
1104
+ return column.render(row);
616
1105
  }
617
- const target = e.currentTarget;
618
- const pointerId = e.pointerId;
619
- try {
620
- target.setPointerCapture(pointerId);
621
- } catch {
1106
+ default: {
1107
+ const _exhaustive = column;
1108
+ return _exhaustive;
622
1109
  }
623
- isResizingRef.current = true;
624
- resizingColIdRef.current = columnId;
625
- const startX = e.clientX;
626
- const th = target.parentElement;
627
- let startWidth = columnWidths[columnId] ?? th.offsetWidth;
628
- const MIN_DRAG_WIDTH = 80;
629
- if (startWidth < MIN_DRAG_WIDTH) {
630
- const nextTh = th.nextElementSibling;
631
- if (nextTh && nextTh.offsetWidth >= 50) {
632
- startWidth = nextTh.offsetWidth;
633
- } else {
634
- startWidth = 100;
635
- }
1110
+ }
1111
+ }
1112
+ function RowaKitTable({
1113
+ fetcher,
1114
+ columns,
1115
+ defaultPageSize = 20,
1116
+ pageSizeOptions = [10, 20, 50],
1117
+ rowKey,
1118
+ className = "",
1119
+ enableFilters = false,
1120
+ enableColumnResizing = false,
1121
+ syncToUrl = false,
1122
+ enableSavedViews = false,
1123
+ enableRowSelection = false,
1124
+ onSelectionChange,
1125
+ bulkActions,
1126
+ exporter
1127
+ }) {
1128
+ const [query, setQuery] = useState({
1129
+ page: 1,
1130
+ pageSize: defaultPageSize
1131
+ });
1132
+ const [filters, setFilters] = useState({});
1133
+ const [confirmState, setConfirmState] = useState(null);
1134
+ const [selectedKeys, setSelectedKeys] = useState([]);
1135
+ const [bulkConfirmState, setBulkConfirmState] = useState(null);
1136
+ const confirmModalRef = useRef(null);
1137
+ const bulkConfirmModalRef = useRef(null);
1138
+ const {
1139
+ tableRef,
1140
+ columnWidths,
1141
+ setColumnWidths,
1142
+ startColumnResize,
1143
+ handleColumnResizeDoubleClick,
1144
+ isResizingRef,
1145
+ lastResizeEndTsRef,
1146
+ resizingColIdRef
1147
+ } = useColumnResizing(columns);
1148
+ useUrlSync({
1149
+ syncToUrl,
1150
+ enableColumnResizing,
1151
+ defaultPageSize,
1152
+ pageSizeOptions,
1153
+ columns,
1154
+ query,
1155
+ setQuery,
1156
+ filters,
1157
+ setFilters,
1158
+ columnWidths,
1159
+ setColumnWidths
1160
+ });
1161
+ const { dataState, handleRetry, isLoading, isError, isEmpty } = useFetcherState(fetcher, query, setQuery);
1162
+ const { handleSort, getSortIndicator } = useSortingState(query, setQuery);
1163
+ const pageRowKeys = dataState.items.map((row) => getRowKey(row, rowKey));
1164
+ const headerChecked = isAllSelected(selectedKeys, pageRowKeys);
1165
+ const headerIndeterminate = isIndeterminate(selectedKeys, pageRowKeys);
1166
+ useEffect(() => {
1167
+ setSelectedKeys(clearSelection());
1168
+ }, [query.page]);
1169
+ useEffect(() => {
1170
+ if (!enableRowSelection) {
1171
+ setSelectedKeys(clearSelection());
636
1172
  }
637
- document.body.classList.add("rowakit-resizing");
638
- const handlePointerMove = (moveEvent) => {
639
- const delta = moveEvent.clientX - startX;
640
- const newWidth = startWidth + delta;
641
- scheduleColumnWidthUpdate(columnId, newWidth);
642
- };
643
- const cleanupResize = () => {
644
- target.removeEventListener("pointermove", handlePointerMove);
645
- target.removeEventListener("pointerup", handlePointerUp);
646
- target.removeEventListener("pointercancel", handlePointerCancel);
647
- document.body.classList.remove("rowakit-resizing");
648
- isResizingRef.current = false;
649
- resizingColIdRef.current = null;
650
- lastResizeEndTsRef.current = Date.now();
651
- try {
652
- target.releasePointerCapture(pointerId);
653
- } catch {
654
- }
655
- };
656
- const handlePointerUp = () => {
657
- cleanupResize();
658
- };
659
- const handlePointerCancel = () => {
660
- cleanupResize();
661
- };
662
- target.addEventListener("pointermove", handlePointerMove);
663
- target.addEventListener("pointerup", handlePointerUp);
664
- target.addEventListener("pointercancel", handlePointerCancel);
665
- };
666
- const handleColumnResizeDoubleClick = (e, columnId) => {
667
- e.preventDefault();
668
- e.stopPropagation();
669
- autoFitColumnWidth(columnId);
670
- };
671
- const saveCurrentView = (name) => {
672
- const viewState = {
673
- page: query.page,
674
- pageSize: query.pageSize,
675
- sort: query.sort,
676
- filters: query.filters,
677
- columnWidths: enableColumnResizing ? columnWidths : void 0
678
- };
679
- setSavedViews((prev) => {
680
- const filtered = prev.filter((v) => v.name !== name);
681
- return [...filtered, { name, state: viewState }];
682
- });
683
- if (typeof window !== "undefined" && window.localStorage) {
684
- try {
685
- localStorage.setItem(`rowakit-view-${name}`, JSON.stringify(viewState));
686
- const index = getSavedViewsIndex();
687
- const filtered = index.filter((v) => v.name !== name);
688
- filtered.push({ name, updatedAt: Date.now() });
689
- setSavedViewsIndex(filtered);
690
- } catch {
1173
+ }, [enableRowSelection]);
1174
+ useEffect(() => {
1175
+ if (!enableRowSelection || !onSelectionChange) return;
1176
+ onSelectionChange(selectedKeys);
1177
+ }, [enableRowSelection, onSelectionChange, selectedKeys]);
1178
+ const {
1179
+ savedViews,
1180
+ showSaveViewForm,
1181
+ saveViewInput,
1182
+ saveViewError,
1183
+ overwriteConfirmName,
1184
+ openSaveViewForm,
1185
+ cancelSaveViewForm,
1186
+ onSaveViewInputChange,
1187
+ onSaveViewInputKeyDown,
1188
+ attemptSave,
1189
+ confirmOverwrite,
1190
+ cancelOverwrite,
1191
+ loadSavedView,
1192
+ deleteSavedView,
1193
+ resetTableState
1194
+ } = useSavedViews({
1195
+ enableSavedViews,
1196
+ enableColumnResizing,
1197
+ defaultPageSize,
1198
+ query,
1199
+ setQuery,
1200
+ setFilters,
1201
+ columnWidths,
1202
+ setColumnWidths
1203
+ });
1204
+ useFocusTrap(confirmModalRef, {
1205
+ onEscape: () => setConfirmState(null),
1206
+ autoFocus: true
1207
+ });
1208
+ useFocusTrap(bulkConfirmModalRef, {
1209
+ onEscape: () => setBulkConfirmState(null),
1210
+ autoFocus: true
1211
+ });
1212
+ useEffect(() => {
1213
+ if (!enableFilters) return;
1214
+ const activeFilters = {};
1215
+ let hasFilters = false;
1216
+ for (const [field, value] of Object.entries(filters)) {
1217
+ if (value !== void 0) {
1218
+ activeFilters[field] = value;
1219
+ hasFilters = true;
691
1220
  }
692
1221
  }
693
- };
694
- const loadSavedView = (name) => {
695
- const view = savedViews.find((v) => v.name === name);
696
- if (!view) return;
697
- const { state } = view;
698
- setQuery({
699
- page: state.page,
700
- pageSize: state.pageSize,
701
- sort: state.sort,
702
- filters: state.filters
703
- });
704
- setFilters(state.filters ?? {});
705
- if (state.columnWidths && enableColumnResizing) {
706
- setColumnWidths(state.columnWidths);
1222
+ const filtersToSend = hasFilters ? activeFilters : void 0;
1223
+ setQuery((prev) => ({
1224
+ ...prev,
1225
+ filters: filtersToSend,
1226
+ page: 1
1227
+ // Reset page to 1 when filters change
1228
+ }));
1229
+ }, [filters, enableFilters]);
1230
+ const handlePreviousPage = () => {
1231
+ if (query.page > 1) {
1232
+ setQuery((prev) => ({ ...prev, page: prev.page - 1 }));
707
1233
  }
708
1234
  };
709
- const deleteSavedView = (name) => {
710
- setSavedViews((prev) => prev.filter((v) => v.name !== name));
711
- if (typeof window !== "undefined" && window.localStorage) {
712
- try {
713
- localStorage.removeItem(`rowakit-view-${name}`);
714
- const index = getSavedViewsIndex();
715
- const filtered = index.filter((v) => v.name !== name);
716
- setSavedViewsIndex(filtered);
717
- } catch {
718
- }
1235
+ const handleNextPage = () => {
1236
+ const totalPages2 = Math.ceil(dataState.total / query.pageSize);
1237
+ if (query.page < totalPages2) {
1238
+ setQuery((prev) => ({ ...prev, page: prev.page + 1 }));
719
1239
  }
720
1240
  };
721
- const resetTableState = () => {
722
- setQuery({
723
- page: 1,
724
- pageSize: defaultPageSize
725
- });
726
- setFilters({});
727
- setColumnWidths({});
1241
+ const handlePageSizeChange = (newPageSize) => {
1242
+ setQuery({ ...query, pageSize: newPageSize, page: 1 });
728
1243
  };
729
1244
  const transformFilterValueForColumn = (column, value) => {
730
1245
  if (!value || column?.kind !== "number") {
@@ -770,12 +1285,10 @@ function RowaKitTable({
770
1285
  const handleClearAllFilters = () => {
771
1286
  setFilters({});
772
1287
  };
773
- const isLoading = dataState.state === "loading";
774
- const isError = dataState.state === "error";
775
- const isEmpty = dataState.state === "empty";
776
1288
  const totalPages = Math.ceil(dataState.total / query.pageSize);
777
1289
  const canGoPrevious = query.page > 1 && !isLoading;
778
1290
  const canGoNext = query.page < totalPages && !isLoading;
1291
+ const tableColumnCount = columns.length + (enableRowSelection ? 1 : 0);
779
1292
  const hasActiveFilters = enableFilters && Object.values(filters).some((v) => v !== void 0);
780
1293
  const containerClass = [
781
1294
  "rowakit-table",
@@ -783,16 +1296,29 @@ function RowaKitTable({
783
1296
  className
784
1297
  ].filter(Boolean).join(" ");
785
1298
  return /* @__PURE__ */ jsxs("div", { className: containerClass, children: [
1299
+ enableRowSelection && bulkActions && bulkActions.length > 0 && selectedKeys.length > 0 && /* @__PURE__ */ jsx(
1300
+ BulkActionBar,
1301
+ {
1302
+ selectedCount: selectedKeys.length,
1303
+ actions: bulkActions,
1304
+ onActionClick: (actionId) => {
1305
+ const action = bulkActions.find((a) => a.id === actionId);
1306
+ if (!action) return;
1307
+ const snapshot = [...selectedKeys];
1308
+ if (action.confirm) {
1309
+ setBulkConfirmState({ action, selectedKeys: snapshot });
1310
+ return;
1311
+ }
1312
+ action.onClick(snapshot);
1313
+ }
1314
+ }
1315
+ ),
1316
+ exporter && /* @__PURE__ */ jsx(ExportButton, { exporter, query }),
786
1317
  enableSavedViews && /* @__PURE__ */ jsxs("div", { className: "rowakit-saved-views-group", children: [
787
1318
  !showSaveViewForm ? /* @__PURE__ */ jsx(
788
1319
  "button",
789
1320
  {
790
- onClick: () => {
791
- setShowSaveViewForm(true);
792
- setSaveViewInput("");
793
- setSaveViewError("");
794
- setOverwriteConfirmName(null);
795
- },
1321
+ onClick: openSaveViewForm,
796
1322
  className: "rowakit-saved-view-button",
797
1323
  type: "button",
798
1324
  children: "Save View"
@@ -806,13 +1332,7 @@ function RowaKitTable({
806
1332
  /* @__PURE__ */ jsx(
807
1333
  "button",
808
1334
  {
809
- onClick: () => {
810
- saveCurrentView(overwriteConfirmName);
811
- setShowSaveViewForm(false);
812
- setSaveViewInput("");
813
- setSaveViewError("");
814
- setOverwriteConfirmName(null);
815
- },
1335
+ onClick: confirmOverwrite,
816
1336
  className: "rowakit-saved-view-button",
817
1337
  type: "button",
818
1338
  children: "Overwrite"
@@ -821,9 +1341,7 @@ function RowaKitTable({
821
1341
  /* @__PURE__ */ jsx(
822
1342
  "button",
823
1343
  {
824
- onClick: () => {
825
- setOverwriteConfirmName(null);
826
- },
1344
+ onClick: cancelOverwrite,
827
1345
  className: "rowakit-saved-view-button",
828
1346
  type: "button",
829
1347
  children: "Cancel"
@@ -835,27 +1353,8 @@ function RowaKitTable({
835
1353
  {
836
1354
  type: "text",
837
1355
  value: saveViewInput,
838
- onChange: (e) => {
839
- setSaveViewInput(e.target.value);
840
- setSaveViewError("");
841
- },
842
- onKeyDown: (e) => {
843
- if (e.key === "Enter") {
844
- const validation = validateViewName(saveViewInput);
845
- if (!validation.valid) {
846
- setSaveViewError(validation.error || "Invalid name");
847
- return;
848
- }
849
- if (savedViews.some((v) => v.name === saveViewInput.trim())) {
850
- setOverwriteConfirmName(saveViewInput.trim());
851
- } else {
852
- saveCurrentView(saveViewInput.trim());
853
- setShowSaveViewForm(false);
854
- setSaveViewInput("");
855
- setSaveViewError("");
856
- }
857
- }
858
- },
1356
+ onChange: onSaveViewInputChange,
1357
+ onKeyDown: onSaveViewInputKeyDown,
859
1358
  placeholder: "Enter view name...",
860
1359
  className: "rowakit-save-view-input"
861
1360
  }
@@ -864,21 +1363,7 @@ function RowaKitTable({
864
1363
  /* @__PURE__ */ jsx(
865
1364
  "button",
866
1365
  {
867
- onClick: () => {
868
- const validation = validateViewName(saveViewInput);
869
- if (!validation.valid) {
870
- setSaveViewError(validation.error || "Invalid name");
871
- return;
872
- }
873
- if (savedViews.some((v) => v.name === saveViewInput.trim())) {
874
- setOverwriteConfirmName(saveViewInput.trim());
875
- } else {
876
- saveCurrentView(saveViewInput.trim());
877
- setShowSaveViewForm(false);
878
- setSaveViewInput("");
879
- setSaveViewError("");
880
- }
881
- },
1366
+ onClick: attemptSave,
882
1367
  className: "rowakit-saved-view-button",
883
1368
  type: "button",
884
1369
  children: "Save"
@@ -887,11 +1372,7 @@ function RowaKitTable({
887
1372
  /* @__PURE__ */ jsx(
888
1373
  "button",
889
1374
  {
890
- onClick: () => {
891
- setShowSaveViewForm(false);
892
- setSaveViewInput("");
893
- setSaveViewError("");
894
- },
1375
+ onClick: cancelSaveViewForm,
895
1376
  className: "rowakit-saved-view-button",
896
1377
  type: "button",
897
1378
  children: "Cancel"
@@ -940,167 +1421,143 @@ function RowaKitTable({
940
1421
  ) }),
941
1422
  /* @__PURE__ */ jsxs("table", { ref: tableRef, children: [
942
1423
  /* @__PURE__ */ jsxs("thead", { children: [
943
- /* @__PURE__ */ jsx("tr", { children: columns.map((column) => {
944
- const isSortable = column.kind !== "actions" && (column.kind === "custom" ? false : column.sortable === true);
945
- const field = column.kind === "actions" ? "" : column.kind === "custom" ? column.field : column.field;
946
- const isResizable = enableColumnResizing && column.kind !== "actions";
947
- const actualWidth = columnWidths[column.id] ?? column.width;
948
- return /* @__PURE__ */ jsxs(
949
- "th",
1424
+ /* @__PURE__ */ jsxs("tr", { children: [
1425
+ enableRowSelection && /* @__PURE__ */ jsx(
1426
+ RowSelectionHeaderCell,
950
1427
  {
951
- "data-col-id": column.id,
952
- onClick: isSortable ? () => {
953
- if (isResizingRef.current) return;
954
- if (Date.now() - lastResizeEndTsRef.current < 150) return;
955
- handleSort(String(field));
956
- } : void 0,
957
- role: isSortable ? "button" : void 0,
958
- tabIndex: isSortable ? 0 : void 0,
959
- onKeyDown: isSortable ? (e) => {
960
- if (e.key === "Enter" || e.key === " ") {
961
- e.preventDefault();
962
- handleSort(String(field));
1428
+ checked: headerChecked,
1429
+ indeterminate: headerIndeterminate,
1430
+ disabled: isLoading || pageRowKeys.length === 0,
1431
+ onChange: (checked) => {
1432
+ if (checked) {
1433
+ setSelectedKeys(selectAll(pageRowKeys));
1434
+ } else {
1435
+ setSelectedKeys(clearSelection());
963
1436
  }
964
- } : void 0,
965
- "aria-sort": isSortable && query.sort?.field === String(field) ? query.sort.direction === "asc" ? "ascending" : "descending" : void 0,
966
- style: {
967
- width: actualWidth != null ? `${actualWidth}px` : void 0,
968
- textAlign: column.align,
969
- position: isResizable ? "relative" : void 0
970
- },
971
- className: [
972
- column.truncate ? "rowakit-cell-truncate" : "",
973
- resizingColIdRef.current === column.id ? "resizing" : ""
974
- // PRD-01
975
- ].filter(Boolean).join(" ") || void 0,
976
- children: [
977
- getHeaderLabel(column),
978
- isSortable && getSortIndicator(String(field)),
979
- isResizable && /* @__PURE__ */ jsx(
980
- "div",
981
- {
982
- className: "rowakit-column-resize-handle",
983
- onPointerDown: (e) => startColumnResize(e, column.id),
984
- onDoubleClick: (e) => handleColumnResizeDoubleClick(e, column.id),
985
- title: "Drag to resize | Double-click to auto-fit content"
986
- }
987
- )
988
- ]
989
- },
990
- column.id
991
- );
992
- }) }),
993
- enableFilters && /* @__PURE__ */ jsx("tr", { className: "rowakit-table-filter-row", children: columns.map((column) => {
994
- const field = column.kind === "actions" || column.kind === "custom" ? "" : String(column.field);
995
- const canFilter = field && column.kind !== "actions";
996
- if (!canFilter) {
997
- return /* @__PURE__ */ jsx("th", {}, column.id);
998
- }
999
- const filterValue = filters[field];
1000
- if (column.kind === "badge") {
1001
- const options = column.map ? Object.keys(column.map) : [];
1002
- return /* @__PURE__ */ jsx("th", { children: /* @__PURE__ */ jsxs(
1003
- "select",
1004
- {
1005
- className: "rowakit-filter-select",
1006
- value: filterValue?.op === "equals" ? String(filterValue.value ?? "") : "",
1007
- onChange: (e) => {
1008
- const value = e.target.value;
1009
- if (value === "") {
1010
- handleClearFilter(field);
1011
- } else {
1012
- handleFilterChange(field, { op: "equals", value });
1013
- }
1014
- },
1015
- children: [
1016
- /* @__PURE__ */ jsx("option", { value: "", children: "All" }),
1017
- options.map((opt) => /* @__PURE__ */ jsx("option", { value: opt, children: opt }, opt))
1018
- ]
1019
1437
  }
1020
- ) }, column.id);
1021
- }
1022
- if (column.kind === "boolean") {
1023
- return /* @__PURE__ */ jsx("th", { children: /* @__PURE__ */ jsxs(
1024
- "select",
1438
+ }
1439
+ ),
1440
+ columns.map((column) => {
1441
+ const isSortable = column.kind !== "actions" && (column.kind === "custom" ? false : column.sortable === true);
1442
+ const field = column.kind === "actions" ? "" : column.kind === "custom" ? column.field : column.field;
1443
+ const isResizable = enableColumnResizing && column.kind !== "actions";
1444
+ const actualWidth = columnWidths[column.id] ?? column.width;
1445
+ return /* @__PURE__ */ jsxs(
1446
+ "th",
1025
1447
  {
1026
- className: "rowakit-filter-select",
1027
- value: filterValue?.op === "equals" && typeof filterValue.value === "boolean" ? String(filterValue.value) : "",
1028
- onChange: (e) => {
1029
- const value = e.target.value;
1030
- if (value === "") {
1031
- handleClearFilter(field);
1032
- } else {
1033
- handleFilterChange(field, { op: "equals", value: value === "true" });
1448
+ "data-col-id": column.id,
1449
+ onClick: isSortable ? (e) => {
1450
+ if (isResizingRef.current) return;
1451
+ if (Date.now() - lastResizeEndTsRef.current < 150) return;
1452
+ const isMultiSort = e.ctrlKey || e.metaKey;
1453
+ handleSort(String(field), isMultiSort);
1454
+ } : void 0,
1455
+ role: isSortable ? "button" : void 0,
1456
+ tabIndex: isSortable ? 0 : void 0,
1457
+ onKeyDown: isSortable ? (e) => {
1458
+ if (e.key === "Enter" || e.key === " ") {
1459
+ e.preventDefault();
1460
+ const isMultiSort = e.shiftKey;
1461
+ handleSort(String(field), isMultiSort);
1034
1462
  }
1463
+ } : void 0,
1464
+ "aria-sort": isSortable && (query.sorts?.find((s) => s.field === String(field))?.priority === 0 || query.sort?.field === String(field)) ? (query.sorts?.find((s) => s.field === String(field))?.direction ?? query.sort?.direction) === "asc" ? "ascending" : "descending" : void 0,
1465
+ style: {
1466
+ width: actualWidth != null ? `${actualWidth}px` : void 0,
1467
+ textAlign: column.align,
1468
+ position: isResizable ? "relative" : void 0
1035
1469
  },
1470
+ className: [
1471
+ column.truncate ? "rowakit-cell-truncate" : "",
1472
+ resizingColIdRef.current === column.id ? "resizing" : ""
1473
+ // PRD-01
1474
+ ].filter(Boolean).join(" ") || void 0,
1036
1475
  children: [
1037
- /* @__PURE__ */ jsx("option", { value: "", children: "All" }),
1038
- /* @__PURE__ */ jsx("option", { value: "true", children: "True" }),
1039
- /* @__PURE__ */ jsx("option", { value: "false", children: "False" })
1476
+ getHeaderLabel(column),
1477
+ isSortable && getSortIndicator(String(field)),
1478
+ isResizable && /* @__PURE__ */ jsx(
1479
+ "div",
1480
+ {
1481
+ className: "rowakit-column-resize-handle",
1482
+ onPointerDown: (e) => startColumnResize(e, column.id),
1483
+ onDoubleClick: (e) => handleColumnResizeDoubleClick(e, column.id),
1484
+ title: "Drag to resize | Double-click to auto-fit content"
1485
+ }
1486
+ )
1040
1487
  ]
1041
- }
1042
- ) }, column.id);
1043
- }
1044
- if (column.kind === "date") {
1045
- const fromValue = filterValue?.op === "range" ? filterValue.value.from ?? "" : "";
1046
- const toValue = filterValue?.op === "range" ? filterValue.value.to ?? "" : "";
1047
- return /* @__PURE__ */ jsx("th", { children: /* @__PURE__ */ jsxs("div", { className: "rowakit-filter-date-range", children: [
1048
- /* @__PURE__ */ jsx(
1049
- "input",
1488
+ },
1489
+ column.id
1490
+ );
1491
+ })
1492
+ ] }),
1493
+ enableFilters && /* @__PURE__ */ jsxs("tr", { className: "rowakit-table-filter-row", children: [
1494
+ enableRowSelection && /* @__PURE__ */ jsx("th", {}),
1495
+ columns.map((column) => {
1496
+ const field = column.kind === "actions" || column.kind === "custom" ? "" : String(column.field);
1497
+ const canFilter = field && column.kind !== "actions";
1498
+ if (!canFilter) {
1499
+ return /* @__PURE__ */ jsx("th", {}, column.id);
1500
+ }
1501
+ const filterValue = filters[field];
1502
+ if (column.kind === "badge") {
1503
+ const options = column.map ? Object.keys(column.map) : [];
1504
+ return /* @__PURE__ */ jsx("th", { children: /* @__PURE__ */ jsxs(
1505
+ "select",
1050
1506
  {
1051
- type: "date",
1052
- className: "rowakit-filter-input",
1053
- placeholder: "From",
1054
- value: fromValue,
1507
+ className: "rowakit-filter-select",
1508
+ value: filterValue?.op === "equals" ? String(filterValue.value ?? "") : "",
1055
1509
  onChange: (e) => {
1056
- const from = e.target.value || void 0;
1057
- const to = toValue || void 0;
1058
- if (!from && !to) {
1510
+ const value = e.target.value;
1511
+ if (value === "") {
1059
1512
  handleClearFilter(field);
1060
1513
  } else {
1061
- handleFilterChange(field, { op: "range", value: { from, to } });
1514
+ handleFilterChange(field, { op: "equals", value });
1062
1515
  }
1063
- }
1516
+ },
1517
+ children: [
1518
+ /* @__PURE__ */ jsx("option", { value: "", children: "All" }),
1519
+ options.map((opt) => /* @__PURE__ */ jsx("option", { value: opt, children: opt }, opt))
1520
+ ]
1064
1521
  }
1065
- ),
1066
- /* @__PURE__ */ jsx(
1067
- "input",
1522
+ ) }, column.id);
1523
+ }
1524
+ if (column.kind === "boolean") {
1525
+ return /* @__PURE__ */ jsx("th", { children: /* @__PURE__ */ jsxs(
1526
+ "select",
1068
1527
  {
1069
- type: "date",
1070
- className: "rowakit-filter-input",
1071
- placeholder: "To",
1072
- value: toValue,
1528
+ className: "rowakit-filter-select",
1529
+ value: filterValue?.op === "equals" && typeof filterValue.value === "boolean" ? String(filterValue.value) : "",
1073
1530
  onChange: (e) => {
1074
- const to = e.target.value || void 0;
1075
- const from = fromValue || void 0;
1076
- if (!from && !to) {
1531
+ const value = e.target.value;
1532
+ if (value === "") {
1077
1533
  handleClearFilter(field);
1078
1534
  } else {
1079
- handleFilterChange(field, { op: "range", value: { from, to } });
1535
+ handleFilterChange(field, { op: "equals", value: value === "true" });
1080
1536
  }
1081
- }
1537
+ },
1538
+ children: [
1539
+ /* @__PURE__ */ jsx("option", { value: "", children: "All" }),
1540
+ /* @__PURE__ */ jsx("option", { value: "true", children: "True" }),
1541
+ /* @__PURE__ */ jsx("option", { value: "false", children: "False" })
1542
+ ]
1082
1543
  }
1083
- )
1084
- ] }) }, column.id);
1085
- }
1086
- const isNumberColumn = column.kind === "number";
1087
- if (isNumberColumn) {
1088
- const fromValue = filterValue?.op === "range" ? String(filterValue.value.from ?? "") : filterValue?.op === "equals" && typeof filterValue.value === "number" ? String(filterValue.value) : "";
1089
- const toValue = filterValue?.op === "range" ? String(filterValue.value.to ?? "") : "";
1090
- const showRangeUI = !filterValue || filterValue.op === "range";
1091
- if (showRangeUI) {
1092
- return /* @__PURE__ */ jsx("th", { children: /* @__PURE__ */ jsxs("div", { className: "rowakit-filter-number-range", children: [
1544
+ ) }, column.id);
1545
+ }
1546
+ if (column.kind === "date") {
1547
+ const fromValue = filterValue?.op === "range" ? filterValue.value.from ?? "" : "";
1548
+ const toValue = filterValue?.op === "range" ? filterValue.value.to ?? "" : "";
1549
+ return /* @__PURE__ */ jsx("th", { children: /* @__PURE__ */ jsxs("div", { className: "rowakit-filter-date-range", children: [
1093
1550
  /* @__PURE__ */ jsx(
1094
1551
  "input",
1095
1552
  {
1096
- type: "number",
1553
+ type: "date",
1097
1554
  className: "rowakit-filter-input",
1098
- placeholder: "Min",
1555
+ placeholder: "From",
1099
1556
  value: fromValue,
1100
1557
  onChange: (e) => {
1101
- const from = e.target.value ? Number(e.target.value) : void 0;
1102
- const to = toValue ? Number(toValue) : void 0;
1103
- if (from === void 0 && to === void 0) {
1558
+ const from = e.target.value || void 0;
1559
+ const to = toValue || void 0;
1560
+ if (!from && !to) {
1104
1561
  handleClearFilter(field);
1105
1562
  } else {
1106
1563
  handleFilterChange(field, { op: "range", value: { from, to } });
@@ -1111,14 +1568,14 @@ function RowaKitTable({
1111
1568
  /* @__PURE__ */ jsx(
1112
1569
  "input",
1113
1570
  {
1114
- type: "number",
1571
+ type: "date",
1115
1572
  className: "rowakit-filter-input",
1116
- placeholder: "Max",
1573
+ placeholder: "To",
1117
1574
  value: toValue,
1118
1575
  onChange: (e) => {
1119
- const to = e.target.value ? Number(e.target.value) : void 0;
1120
- const from = fromValue ? Number(fromValue) : void 0;
1121
- if (from === void 0 && to === void 0) {
1576
+ const to = e.target.value || void 0;
1577
+ const from = fromValue || void 0;
1578
+ if (!from && !to) {
1122
1579
  handleClearFilter(field);
1123
1580
  } else {
1124
1581
  handleFilterChange(field, { op: "range", value: { from, to } });
@@ -1128,39 +1585,85 @@ function RowaKitTable({
1128
1585
  )
1129
1586
  ] }) }, column.id);
1130
1587
  }
1131
- }
1132
- return /* @__PURE__ */ jsx("th", { children: /* @__PURE__ */ jsx(
1133
- "input",
1134
- {
1135
- type: isNumberColumn ? "number" : "text",
1136
- className: "rowakit-filter-input",
1137
- placeholder: `Filter ${getHeaderLabel(column)}...`,
1138
- 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) : "",
1139
- onChange: (e) => {
1140
- const rawValue = e.target.value;
1141
- if (rawValue === "") {
1142
- handleClearFilter(field);
1143
- } else if (isNumberColumn) {
1144
- const numValue = Number(rawValue);
1145
- if (!isNaN(numValue)) {
1146
- handleFilterChange(field, { op: "equals", value: numValue });
1147
- } else {
1588
+ const isNumberColumn = column.kind === "number";
1589
+ if (isNumberColumn) {
1590
+ const fromValue = filterValue?.op === "range" ? String(filterValue.value.from ?? "") : filterValue?.op === "equals" && typeof filterValue.value === "number" ? String(filterValue.value) : "";
1591
+ const toValue = filterValue?.op === "range" ? String(filterValue.value.to ?? "") : "";
1592
+ const showRangeUI = !filterValue || filterValue.op === "range";
1593
+ if (showRangeUI) {
1594
+ return /* @__PURE__ */ jsx("th", { children: /* @__PURE__ */ jsxs("div", { className: "rowakit-filter-number-range", children: [
1595
+ /* @__PURE__ */ jsx(
1596
+ "input",
1597
+ {
1598
+ type: "number",
1599
+ className: "rowakit-filter-input",
1600
+ placeholder: "Min",
1601
+ value: fromValue,
1602
+ onChange: (e) => {
1603
+ const from = e.target.value ? Number(e.target.value) : void 0;
1604
+ const to = toValue ? Number(toValue) : void 0;
1605
+ if (from === void 0 && to === void 0) {
1606
+ handleClearFilter(field);
1607
+ } else {
1608
+ handleFilterChange(field, { op: "range", value: { from, to } });
1609
+ }
1610
+ }
1611
+ }
1612
+ ),
1613
+ /* @__PURE__ */ jsx(
1614
+ "input",
1615
+ {
1616
+ type: "number",
1617
+ className: "rowakit-filter-input",
1618
+ placeholder: "Max",
1619
+ value: toValue,
1620
+ onChange: (e) => {
1621
+ const to = e.target.value ? Number(e.target.value) : void 0;
1622
+ const from = fromValue ? Number(fromValue) : void 0;
1623
+ if (from === void 0 && to === void 0) {
1624
+ handleClearFilter(field);
1625
+ } else {
1626
+ handleFilterChange(field, { op: "range", value: { from, to } });
1627
+ }
1628
+ }
1629
+ }
1630
+ )
1631
+ ] }) }, column.id);
1632
+ }
1633
+ }
1634
+ return /* @__PURE__ */ jsx("th", { children: /* @__PURE__ */ jsx(
1635
+ "input",
1636
+ {
1637
+ type: isNumberColumn ? "number" : "text",
1638
+ className: "rowakit-filter-input",
1639
+ placeholder: `Filter ${getHeaderLabel(column)}...`,
1640
+ 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) : "",
1641
+ onChange: (e) => {
1642
+ const rawValue = e.target.value;
1643
+ if (rawValue === "") {
1148
1644
  handleClearFilter(field);
1645
+ } else if (isNumberColumn) {
1646
+ const numValue = Number(rawValue);
1647
+ if (!isNaN(numValue)) {
1648
+ handleFilterChange(field, { op: "equals", value: numValue });
1649
+ } else {
1650
+ handleClearFilter(field);
1651
+ }
1652
+ } else {
1653
+ handleFilterChange(field, { op: "contains", value: rawValue });
1149
1654
  }
1150
- } else {
1151
- handleFilterChange(field, { op: "contains", value: rawValue });
1152
1655
  }
1153
1656
  }
1154
- }
1155
- ) }, column.id);
1156
- }) })
1657
+ ) }, column.id);
1658
+ })
1659
+ ] })
1157
1660
  ] }),
1158
1661
  /* @__PURE__ */ jsxs("tbody", { children: [
1159
- isLoading && /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsxs("td", { colSpan: columns.length, className: "rowakit-table-loading", children: [
1662
+ isLoading && /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsxs("td", { colSpan: tableColumnCount, className: "rowakit-table-loading", children: [
1160
1663
  /* @__PURE__ */ jsx("div", { className: "rowakit-table-loading-spinner" }),
1161
1664
  /* @__PURE__ */ jsx("span", { children: "Loading..." })
1162
1665
  ] }) }),
1163
- isError && /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsxs("td", { colSpan: columns.length, className: "rowakit-table-error", children: [
1666
+ isError && /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsxs("td", { colSpan: tableColumnCount, className: "rowakit-table-error", children: [
1164
1667
  /* @__PURE__ */ jsx("div", { className: "rowakit-table-error-message", children: dataState.error ?? "An error occurred" }),
1165
1668
  /* @__PURE__ */ jsx(
1166
1669
  "button",
@@ -1172,29 +1675,42 @@ function RowaKitTable({
1172
1675
  }
1173
1676
  )
1174
1677
  ] }) }),
1175
- isEmpty && /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx("td", { colSpan: columns.length, className: "rowakit-table-empty", children: "No data" }) }),
1678
+ isEmpty && /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx("td", { colSpan: tableColumnCount, className: "rowakit-table-empty", children: "No data" }) }),
1176
1679
  dataState.state === "success" && dataState.items.map((row) => {
1177
1680
  const key = getRowKey(row, rowKey);
1178
- return /* @__PURE__ */ jsx("tr", { children: columns.map((column) => {
1179
- const cellClass = [
1180
- column.kind === "number" ? "rowakit-cell-number" : "",
1181
- column.truncate ? "rowakit-cell-truncate" : ""
1182
- ].filter(Boolean).join(" ") || void 0;
1183
- const actualWidth = columnWidths[column.id] ?? column.width;
1184
- return /* @__PURE__ */ jsx(
1185
- "td",
1681
+ return /* @__PURE__ */ jsxs("tr", { children: [
1682
+ enableRowSelection && /* @__PURE__ */ jsx(
1683
+ RowSelectionCell,
1186
1684
  {
1187
- "data-col-id": column.id,
1188
- className: cellClass,
1189
- style: {
1190
- width: actualWidth != null ? `${actualWidth}px` : void 0,
1191
- textAlign: column.align || (column.kind === "number" ? "right" : void 0)
1685
+ rowKey: key,
1686
+ disabled: isLoading,
1687
+ checked: selectedKeys.includes(key),
1688
+ onChange: () => {
1689
+ setSelectedKeys((prev) => toggleSelectionKey(prev, key));
1690
+ }
1691
+ }
1692
+ ),
1693
+ columns.map((column) => {
1694
+ const cellClass = [
1695
+ column.kind === "number" ? "rowakit-cell-number" : "",
1696
+ column.truncate ? "rowakit-cell-truncate" : ""
1697
+ ].filter(Boolean).join(" ") || void 0;
1698
+ const actualWidth = columnWidths[column.id] ?? column.width;
1699
+ return /* @__PURE__ */ jsx(
1700
+ "td",
1701
+ {
1702
+ "data-col-id": column.id,
1703
+ className: cellClass,
1704
+ style: {
1705
+ width: actualWidth != null ? `${actualWidth}px` : void 0,
1706
+ textAlign: column.align || (column.kind === "number" ? "right" : void 0)
1707
+ },
1708
+ children: renderCell(column, row, isLoading, setConfirmState)
1192
1709
  },
1193
- children: renderCell(column, row, isLoading, setConfirmState)
1194
- },
1195
- column.id
1196
- );
1197
- }) }, key);
1710
+ column.id
1711
+ );
1712
+ })
1713
+ ] }, key);
1198
1714
  })
1199
1715
  ] })
1200
1716
  ] }),
@@ -1249,6 +1765,7 @@ function RowaKitTable({
1249
1765
  confirmState && /* @__PURE__ */ jsx(
1250
1766
  "div",
1251
1767
  {
1768
+ ref: confirmModalRef,
1252
1769
  className: "rowakit-modal-backdrop",
1253
1770
  onClick: () => setConfirmState(null),
1254
1771
  role: "dialog",
@@ -1286,13 +1803,51 @@ function RowaKitTable({
1286
1803
  ] })
1287
1804
  ] })
1288
1805
  }
1806
+ ),
1807
+ bulkConfirmState && /* @__PURE__ */ jsx(
1808
+ "div",
1809
+ {
1810
+ ref: bulkConfirmModalRef,
1811
+ className: "rowakit-modal-backdrop",
1812
+ onClick: () => setBulkConfirmState(null),
1813
+ role: "dialog",
1814
+ "aria-modal": "true",
1815
+ "aria-labelledby": "bulk-confirm-dialog-title",
1816
+ children: /* @__PURE__ */ jsxs("div", { className: "rowakit-modal", onClick: (e) => e.stopPropagation(), children: [
1817
+ /* @__PURE__ */ jsx("h2", { id: "bulk-confirm-dialog-title", className: "rowakit-modal-title", children: bulkConfirmState.action.confirm?.title ?? "Confirm Action" }),
1818
+ /* @__PURE__ */ jsx("p", { className: "rowakit-modal-content", children: bulkConfirmState.action.confirm?.description ?? "Are you sure you want to perform this action? This action cannot be undone." }),
1819
+ /* @__PURE__ */ jsxs("div", { className: "rowakit-modal-actions", children: [
1820
+ /* @__PURE__ */ jsx(
1821
+ "button",
1822
+ {
1823
+ onClick: () => setBulkConfirmState(null),
1824
+ className: "rowakit-button rowakit-button-secondary",
1825
+ type: "button",
1826
+ children: "Cancel"
1827
+ }
1828
+ ),
1829
+ /* @__PURE__ */ jsx(
1830
+ "button",
1831
+ {
1832
+ onClick: () => {
1833
+ bulkConfirmState.action.onClick(bulkConfirmState.selectedKeys);
1834
+ setBulkConfirmState(null);
1835
+ },
1836
+ className: "rowakit-button rowakit-button-danger",
1837
+ type: "button",
1838
+ children: "Confirm"
1839
+ }
1840
+ )
1841
+ ] })
1842
+ ] })
1843
+ }
1289
1844
  )
1290
1845
  ] });
1291
1846
  }
1292
1847
  var SmartTable = RowaKitTable;
1293
1848
 
1294
1849
  // src/index.ts
1295
- var VERSION = "0.4.0";
1850
+ var VERSION = "1.0.0" ;
1296
1851
 
1297
1852
  export { RowaKitTable, SmartTable, VERSION, col };
1298
1853
  //# sourceMappingURL=index.js.map