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