@rowakit/table 0.3.0 → 0.5.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,6 +105,834 @@ var col = {
105
105
  actions,
106
106
  custom
107
107
  };
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
+ };
233
+ }
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
+ };
280
+ }
281
+ function validateViewName(name) {
282
+ const trimmed = name.trim();
283
+ if (trimmed.length === 0) {
284
+ return { valid: false, error: "Name cannot be empty" };
285
+ }
286
+ if (trimmed.length > 40) {
287
+ return { valid: false, error: "Name cannot exceed 40 characters" };
288
+ }
289
+ const invalidChars = /[/\\?%*:|"<>\x00-\x1f\x7f]/;
290
+ if (invalidChars.test(trimmed)) {
291
+ return { valid: false, error: "Name contains invalid characters" };
292
+ }
293
+ return { valid: true };
294
+ }
295
+ function getSavedViewsIndex() {
296
+ if (typeof window === "undefined" || !window.localStorage) {
297
+ return [];
298
+ }
299
+ try {
300
+ const indexStr = localStorage.getItem("rowakit-views-index");
301
+ if (indexStr) {
302
+ const index = JSON.parse(indexStr);
303
+ if (Array.isArray(index)) {
304
+ return index;
305
+ }
306
+ }
307
+ } catch {
308
+ }
309
+ const rebuilt = [];
310
+ try {
311
+ for (let i = 0; i < localStorage.length; i++) {
312
+ const key = localStorage.key(i);
313
+ if (key?.startsWith("rowakit-view-")) {
314
+ const name = key.substring("rowakit-view-".length);
315
+ rebuilt.push({ name, updatedAt: Date.now() });
316
+ }
317
+ }
318
+ } catch {
319
+ }
320
+ return rebuilt;
321
+ }
322
+ function setSavedViewsIndex(index) {
323
+ if (typeof window === "undefined" || !window.localStorage) {
324
+ return;
325
+ }
326
+ try {
327
+ localStorage.setItem("rowakit-views-index", JSON.stringify(index));
328
+ } catch {
329
+ }
330
+ }
331
+ function loadSavedViewsFromStorage() {
332
+ if (typeof window === "undefined" || !window.localStorage) {
333
+ return [];
334
+ }
335
+ const index = getSavedViewsIndex();
336
+ const views = [];
337
+ for (const entry of index) {
338
+ try {
339
+ const viewStr = localStorage.getItem(`rowakit-view-${entry.name}`);
340
+ if (viewStr) {
341
+ const state = JSON.parse(viewStr);
342
+ views.push({ name: entry.name, state });
343
+ }
344
+ } catch {
345
+ }
346
+ }
347
+ return views;
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
+ }
542
+ function parseUrlState(params, defaultPageSize, pageSizeOptions) {
543
+ const pageStr = params.get("page");
544
+ let page = 1;
545
+ if (pageStr) {
546
+ const parsed = parseInt(pageStr, 10);
547
+ page = !isNaN(parsed) && parsed >= 1 ? parsed : 1;
548
+ }
549
+ const pageSizeStr = params.get("pageSize");
550
+ let pageSize = defaultPageSize;
551
+ if (pageSizeStr) {
552
+ const parsed = parseInt(pageSizeStr, 10);
553
+ if (!isNaN(parsed) && parsed >= 1) {
554
+ if (pageSizeOptions && pageSizeOptions.length > 0) {
555
+ pageSize = pageSizeOptions.includes(parsed) ? parsed : defaultPageSize;
556
+ } else {
557
+ pageSize = parsed;
558
+ }
559
+ }
560
+ }
561
+ const result = { page, pageSize };
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 sortField = params.get("sortField");
574
+ const sortDir = params.get("sortDirection");
575
+ if (sortField && (sortDir === "asc" || sortDir === "desc")) {
576
+ result.sort = { field: sortField, direction: sortDir };
577
+ }
578
+ }
579
+ const filtersStr = params.get("filters");
580
+ if (filtersStr) {
581
+ try {
582
+ const parsed = JSON.parse(filtersStr);
583
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
584
+ result.filters = parsed;
585
+ }
586
+ } catch {
587
+ }
588
+ }
589
+ const widthsStr = params.get("columnWidths");
590
+ if (widthsStr) {
591
+ try {
592
+ const parsed = JSON.parse(widthsStr);
593
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
594
+ const widths = {};
595
+ for (const [key, value] of Object.entries(parsed)) {
596
+ if (typeof value === "number" && value > 0) {
597
+ widths[key] = value;
598
+ }
599
+ }
600
+ if (Object.keys(widths).length > 0) {
601
+ result.columnWidths = widths;
602
+ }
603
+ }
604
+ } catch {
605
+ }
606
+ }
607
+ return result;
608
+ }
609
+ function serializeUrlState(query, filters, columnWidths, defaultPageSize, enableColumnResizing) {
610
+ const params = new URLSearchParams();
611
+ params.set("page", String(query.page));
612
+ if (query.pageSize !== defaultPageSize) {
613
+ params.set("pageSize", String(query.pageSize));
614
+ }
615
+ if (query.sorts && query.sorts.length > 0) {
616
+ params.set("sorts", JSON.stringify(query.sorts));
617
+ } else if (query.sort) {
618
+ params.set("sortField", query.sort.field);
619
+ params.set("sortDirection", query.sort.direction);
620
+ }
621
+ if (filters && Object.keys(filters).length > 0) {
622
+ const nonEmptyFilters = Object.fromEntries(
623
+ Object.entries(filters).filter(([, v]) => v !== void 0)
624
+ );
625
+ if (Object.keys(nonEmptyFilters).length > 0) {
626
+ params.set("filters", JSON.stringify(nonEmptyFilters));
627
+ }
628
+ }
629
+ if (enableColumnResizing && Object.keys(columnWidths).length > 0) {
630
+ params.set("columnWidths", JSON.stringify(columnWidths));
631
+ }
632
+ return params.toString();
633
+ }
634
+ function useUrlSync({
635
+ syncToUrl,
636
+ enableColumnResizing,
637
+ defaultPageSize,
638
+ pageSizeOptions,
639
+ columns,
640
+ query,
641
+ setQuery,
642
+ filters,
643
+ setFilters,
644
+ columnWidths,
645
+ setColumnWidths
646
+ }) {
647
+ const didHydrateUrlRef = react.useRef(false);
648
+ const didSkipInitialUrlSyncRef = react.useRef(false);
649
+ const urlSyncDebounceRef = react.useRef(null);
650
+ react.useEffect(() => {
651
+ if (!syncToUrl) {
652
+ didSkipInitialUrlSyncRef.current = false;
653
+ return;
654
+ }
655
+ if (!didSkipInitialUrlSyncRef.current) {
656
+ didSkipInitialUrlSyncRef.current = true;
657
+ return;
658
+ }
659
+ if (urlSyncDebounceRef.current) {
660
+ clearTimeout(urlSyncDebounceRef.current);
661
+ urlSyncDebounceRef.current = null;
662
+ }
663
+ const urlStr = serializeUrlState(query, filters, columnWidths, defaultPageSize, enableColumnResizing);
664
+ const qs = urlStr ? `?${urlStr}` : "";
665
+ window.history.replaceState(null, "", `${window.location.pathname}${qs}${window.location.hash}`);
666
+ }, [
667
+ query,
668
+ filters,
669
+ syncToUrl,
670
+ enableColumnResizing,
671
+ defaultPageSize,
672
+ columnWidths
673
+ ]);
674
+ react.useEffect(() => {
675
+ if (!syncToUrl || !enableColumnResizing) return;
676
+ if (!didSkipInitialUrlSyncRef.current) return;
677
+ if (urlSyncDebounceRef.current) {
678
+ clearTimeout(urlSyncDebounceRef.current);
679
+ }
680
+ urlSyncDebounceRef.current = setTimeout(() => {
681
+ const urlStr = serializeUrlState(query, filters, columnWidths, defaultPageSize, enableColumnResizing);
682
+ const qs = urlStr ? `?${urlStr}` : "";
683
+ window.history.replaceState(null, "", `${window.location.pathname}${qs}${window.location.hash}`);
684
+ urlSyncDebounceRef.current = null;
685
+ }, 150);
686
+ return () => {
687
+ if (urlSyncDebounceRef.current) {
688
+ clearTimeout(urlSyncDebounceRef.current);
689
+ urlSyncDebounceRef.current = null;
690
+ }
691
+ };
692
+ }, [
693
+ columnWidths,
694
+ syncToUrl,
695
+ enableColumnResizing,
696
+ query,
697
+ filters,
698
+ defaultPageSize
699
+ ]);
700
+ react.useEffect(() => {
701
+ if (!syncToUrl) {
702
+ didHydrateUrlRef.current = false;
703
+ return;
704
+ }
705
+ if (didHydrateUrlRef.current) return;
706
+ didHydrateUrlRef.current = true;
707
+ const params = new URLSearchParams(window.location.search);
708
+ const parsed = parseUrlState(params, defaultPageSize, pageSizeOptions);
709
+ setQuery({
710
+ page: parsed.page,
711
+ pageSize: parsed.pageSize,
712
+ sort: parsed.sort,
713
+ sorts: parsed.sorts,
714
+ filters: parsed.filters
715
+ });
716
+ if (parsed.filters) {
717
+ setFilters(parsed.filters);
718
+ }
719
+ if (parsed.columnWidths && enableColumnResizing) {
720
+ const clamped = {};
721
+ for (const [colId, rawWidth] of Object.entries(parsed.columnWidths)) {
722
+ const widthNum = typeof rawWidth === "number" ? rawWidth : Number(rawWidth);
723
+ if (!Number.isFinite(widthNum)) continue;
724
+ const colDef = columns.find((c) => c.id === colId);
725
+ if (!colDef) continue;
726
+ const minW = colDef.minWidth ?? 80;
727
+ const maxW = colDef.maxWidth;
728
+ let finalW = Math.max(minW, widthNum);
729
+ if (maxW != null) {
730
+ finalW = Math.min(finalW, maxW);
731
+ }
732
+ clamped[colId] = finalW;
733
+ }
734
+ setColumnWidths(clamped);
735
+ }
736
+ }, [
737
+ syncToUrl,
738
+ defaultPageSize,
739
+ enableColumnResizing,
740
+ pageSizeOptions,
741
+ columns,
742
+ setQuery,
743
+ setFilters,
744
+ setColumnWidths
745
+ ]);
746
+ }
747
+ var FOCUSABLE_SELECTORS = [
748
+ "button",
749
+ "[href]",
750
+ "input",
751
+ "select",
752
+ "textarea",
753
+ '[tabindex]:not([tabindex="-1"])'
754
+ ].join(",");
755
+ function useFocusTrap(ref, options = {}) {
756
+ const { onEscape, autoFocus = true } = options;
757
+ const firstFocusableRef = react.useRef(null);
758
+ const lastFocusableRef = react.useRef(null);
759
+ react.useEffect(() => {
760
+ const modalEl = ref.current;
761
+ if (!modalEl) return;
762
+ const getFocusableElements = () => {
763
+ const elements = Array.from(modalEl.querySelectorAll(FOCUSABLE_SELECTORS));
764
+ return elements.filter((el) => !el.hasAttribute("disabled") && el.offsetParent !== null);
765
+ };
766
+ let focusableElements = getFocusableElements();
767
+ if (focusableElements.length === 0) return;
768
+ firstFocusableRef.current = focusableElements[0] || null;
769
+ lastFocusableRef.current = focusableElements[focusableElements.length - 1] || null;
770
+ if (autoFocus && firstFocusableRef.current) {
771
+ firstFocusableRef.current.focus();
772
+ }
773
+ const handleKeyDown = (e) => {
774
+ focusableElements = getFocusableElements();
775
+ if (focusableElements.length === 0) return;
776
+ const activeEl = document.activeElement;
777
+ const firstEl = focusableElements[0] || null;
778
+ const lastEl = focusableElements[focusableElements.length - 1] || null;
779
+ if (e.key === "Escape") {
780
+ e.preventDefault();
781
+ onEscape?.();
782
+ return;
783
+ }
784
+ if (e.key === "Tab") {
785
+ if (e.shiftKey) {
786
+ if (activeEl === firstEl && lastEl) {
787
+ e.preventDefault();
788
+ lastEl.focus();
789
+ }
790
+ } else {
791
+ if (activeEl === lastEl && firstEl) {
792
+ e.preventDefault();
793
+ firstEl.focus();
794
+ }
795
+ }
796
+ }
797
+ };
798
+ modalEl.addEventListener("keydown", handleKeyDown);
799
+ return () => {
800
+ modalEl.removeEventListener("keydown", handleKeyDown);
801
+ };
802
+ }, [ref, onEscape, autoFocus]);
803
+ }
804
+ function RowSelectionHeaderCell(props) {
805
+ const checkboxRef = react.useRef(null);
806
+ react.useEffect(() => {
807
+ if (!checkboxRef.current) return;
808
+ checkboxRef.current.indeterminate = props.indeterminate;
809
+ }, [props.indeterminate]);
810
+ return /* @__PURE__ */ jsxRuntime.jsx("th", { children: /* @__PURE__ */ jsxRuntime.jsx(
811
+ "input",
812
+ {
813
+ ref: checkboxRef,
814
+ type: "checkbox",
815
+ "aria-label": "Select all rows",
816
+ disabled: props.disabled,
817
+ checked: props.checked,
818
+ onChange: (e) => props.onChange(e.target.checked)
819
+ }
820
+ ) });
821
+ }
822
+ function RowSelectionCell(props) {
823
+ return /* @__PURE__ */ jsxRuntime.jsx("td", { children: /* @__PURE__ */ jsxRuntime.jsx(
824
+ "input",
825
+ {
826
+ type: "checkbox",
827
+ "aria-label": `Select row ${props.rowKey}`,
828
+ disabled: props.disabled,
829
+ checked: props.checked,
830
+ onChange: (e) => props.onChange(e.target.checked)
831
+ }
832
+ ) });
833
+ }
834
+ function BulkActionBar(props) {
835
+ if (props.selectedCount <= 0) return null;
836
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rowakit-bulk-action-bar", children: [
837
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
838
+ props.selectedCount,
839
+ " selected"
840
+ ] }),
841
+ props.actions.map((action) => /* @__PURE__ */ jsxRuntime.jsx(
842
+ "button",
843
+ {
844
+ type: "button",
845
+ className: "rowakit-button rowakit-button-secondary",
846
+ onClick: () => props.onActionClick(action.id),
847
+ children: action.label
848
+ },
849
+ action.id
850
+ ))
851
+ ] });
852
+ }
853
+ function downloadBlob(blob, filename) {
854
+ if (typeof window === "undefined") return;
855
+ if (typeof URL === "undefined" || typeof URL.createObjectURL !== "function") return;
856
+ const url = URL.createObjectURL(blob);
857
+ try {
858
+ const a = document.createElement("a");
859
+ a.href = url;
860
+ a.download = filename;
861
+ a.rel = "noopener noreferrer";
862
+ a.click();
863
+ } finally {
864
+ URL.revokeObjectURL(url);
865
+ }
866
+ }
867
+ function openUrl(url) {
868
+ if (typeof window === "undefined") return;
869
+ if (typeof window.open === "function") {
870
+ window.open(url, "_blank", "noopener,noreferrer");
871
+ return;
872
+ }
873
+ try {
874
+ window.location.assign(url);
875
+ } catch {
876
+ }
877
+ }
878
+ function ExportButton(props) {
879
+ const [isExporting, setIsExporting] = react.useState(false);
880
+ const [error, setError] = react.useState(null);
881
+ const onClick = async () => {
882
+ if (isExporting) return;
883
+ setIsExporting(true);
884
+ setError(null);
885
+ try {
886
+ const snapshot = { ...props.query };
887
+ const result = await props.exporter(snapshot);
888
+ if (result instanceof Blob) {
889
+ downloadBlob(result, "rowakit-export.csv");
890
+ return;
891
+ }
892
+ openUrl(result.url);
893
+ } catch (e) {
894
+ setError(e instanceof Error ? e.message : "Export failed");
895
+ } finally {
896
+ setIsExporting(false);
897
+ }
898
+ };
899
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rowakit-export", children: [
900
+ /* @__PURE__ */ jsxRuntime.jsx(
901
+ "button",
902
+ {
903
+ type: "button",
904
+ className: "rowakit-button rowakit-button-secondary",
905
+ onClick,
906
+ disabled: isExporting,
907
+ children: isExporting ? "Exporting\u2026" : "Export CSV"
908
+ }
909
+ ),
910
+ error && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rowakit-export-error", children: error })
911
+ ] });
912
+ }
913
+
914
+ // src/state/selection.ts
915
+ function toggleSelectionKey(selected, key) {
916
+ if (selected.includes(key)) {
917
+ return selected.filter((k) => k !== key);
918
+ }
919
+ return [...selected, key];
920
+ }
921
+ function isAllSelected(selected, pageKeys) {
922
+ if (pageKeys.length === 0) return false;
923
+ return pageKeys.every((k) => selected.includes(k));
924
+ }
925
+ function isIndeterminate(selected, pageKeys) {
926
+ if (pageKeys.length === 0) return false;
927
+ const selectedCount = pageKeys.filter((k) => selected.includes(k)).length;
928
+ return selectedCount > 0 && selectedCount < pageKeys.length;
929
+ }
930
+ function selectAll(pageKeys) {
931
+ return [...pageKeys];
932
+ }
933
+ function clearSelection() {
934
+ return [];
935
+ }
108
936
  function getRowKey(row, rowKey) {
109
937
  if (typeof rowKey === "function") {
110
938
  return rowKey(row);
@@ -220,76 +1048,92 @@ function RowaKitTable({
220
1048
  enableFilters = false,
221
1049
  enableColumnResizing = false,
222
1050
  syncToUrl = false,
223
- enableSavedViews = false
1051
+ enableSavedViews = false,
1052
+ enableRowSelection = false,
1053
+ onSelectionChange,
1054
+ bulkActions,
1055
+ exporter
224
1056
  }) {
225
- const [dataState, setDataState] = react.useState({
226
- state: "idle",
227
- items: [],
228
- total: 0
229
- });
230
1057
  const [query, setQuery] = react.useState({
231
1058
  page: 1,
232
1059
  pageSize: defaultPageSize
233
1060
  });
234
1061
  const [filters, setFilters] = react.useState({});
235
- const [columnWidths, setColumnWidths] = react.useState({});
236
- const resizeRafRef = react.useRef(null);
237
- const resizePendingRef = react.useRef(null);
238
- const tableRef = react.useRef(null);
239
- const [savedViews, setSavedViews] = react.useState([]);
240
1062
  const [confirmState, setConfirmState] = react.useState(null);
241
- const requestIdRef = react.useRef(0);
1063
+ const [selectedKeys, setSelectedKeys] = react.useState([]);
1064
+ const [bulkConfirmState, setBulkConfirmState] = react.useState(null);
1065
+ const confirmModalRef = react.useRef(null);
1066
+ const bulkConfirmModalRef = react.useRef(null);
1067
+ const {
1068
+ tableRef,
1069
+ columnWidths,
1070
+ setColumnWidths,
1071
+ startColumnResize,
1072
+ handleColumnResizeDoubleClick,
1073
+ isResizingRef,
1074
+ lastResizeEndTsRef,
1075
+ resizingColIdRef
1076
+ } = useColumnResizing(columns);
1077
+ useUrlSync({
1078
+ syncToUrl,
1079
+ enableColumnResizing,
1080
+ defaultPageSize,
1081
+ pageSizeOptions,
1082
+ columns,
1083
+ query,
1084
+ setQuery,
1085
+ filters,
1086
+ setFilters,
1087
+ columnWidths,
1088
+ setColumnWidths
1089
+ });
1090
+ const { dataState, handleRetry, isLoading, isError, isEmpty } = useFetcherState(fetcher, query, setQuery);
1091
+ const { handleSort, getSortIndicator } = useSortingState(query, setQuery);
1092
+ const pageRowKeys = dataState.items.map((row) => getRowKey(row, rowKey));
1093
+ const headerChecked = isAllSelected(selectedKeys, pageRowKeys);
1094
+ const headerIndeterminate = isIndeterminate(selectedKeys, pageRowKeys);
242
1095
  react.useEffect(() => {
243
- if (!syncToUrl) return;
244
- const params = new URLSearchParams();
245
- params.set("page", String(query.page));
246
- params.set("pageSize", String(query.pageSize));
247
- if (query.sort) {
248
- params.set("sortField", query.sort.field);
249
- params.set("sortDirection", query.sort.direction);
250
- }
251
- if (query.filters && Object.keys(query.filters).length > 0) {
252
- params.set("filters", JSON.stringify(query.filters));
253
- }
254
- if (enableColumnResizing && Object.keys(columnWidths).length > 0) {
255
- params.set("columnWidths", JSON.stringify(columnWidths));
256
- }
257
- window.history.replaceState(null, "", `?${params.toString()}`);
258
- }, [query, columnWidths, syncToUrl, enableColumnResizing]);
1096
+ if (!enableRowSelection) return;
1097
+ setSelectedKeys(clearSelection());
1098
+ }, [enableRowSelection, query.page, dataState.items]);
259
1099
  react.useEffect(() => {
260
- if (!syncToUrl) return;
261
- const params = new URLSearchParams(window.location.search);
262
- const page = parseInt(params.get("page") ?? "1", 10);
263
- const pageSize = parseInt(params.get("pageSize") ?? String(defaultPageSize), 10);
264
- const sortField = params.get("sortField");
265
- const sortDirection = params.get("sortDirection");
266
- const filtersStr = params.get("filters");
267
- const columnWidthsStr = params.get("columnWidths");
268
- const newQuery = {
269
- page: Math.max(1, page),
270
- pageSize: Math.max(1, pageSize)
271
- };
272
- if (sortField && sortDirection) {
273
- newQuery.sort = { field: sortField, direction: sortDirection };
274
- }
275
- if (filtersStr) {
276
- try {
277
- const parsedFilters = JSON.parse(filtersStr);
278
- if (parsedFilters && typeof parsedFilters === "object") {
279
- setFilters(parsedFilters);
280
- newQuery.filters = parsedFilters;
281
- }
282
- } catch {
283
- }
284
- }
285
- if (enableColumnResizing && columnWidthsStr) {
286
- try {
287
- setColumnWidths(JSON.parse(columnWidthsStr));
288
- } catch {
289
- }
290
- }
291
- setQuery(newQuery);
292
- }, [syncToUrl, defaultPageSize, enableColumnResizing]);
1100
+ if (!enableRowSelection || !onSelectionChange) return;
1101
+ onSelectionChange(selectedKeys);
1102
+ }, [enableRowSelection, onSelectionChange, selectedKeys]);
1103
+ const {
1104
+ savedViews,
1105
+ showSaveViewForm,
1106
+ saveViewInput,
1107
+ saveViewError,
1108
+ overwriteConfirmName,
1109
+ openSaveViewForm,
1110
+ cancelSaveViewForm,
1111
+ onSaveViewInputChange,
1112
+ onSaveViewInputKeyDown,
1113
+ attemptSave,
1114
+ confirmOverwrite,
1115
+ cancelOverwrite,
1116
+ loadSavedView,
1117
+ deleteSavedView,
1118
+ resetTableState
1119
+ } = useSavedViews({
1120
+ enableSavedViews,
1121
+ enableColumnResizing,
1122
+ defaultPageSize,
1123
+ query,
1124
+ setQuery,
1125
+ setFilters,
1126
+ columnWidths,
1127
+ setColumnWidths
1128
+ });
1129
+ useFocusTrap(confirmModalRef, {
1130
+ onEscape: () => setConfirmState(null),
1131
+ autoFocus: true
1132
+ });
1133
+ useFocusTrap(bulkConfirmModalRef, {
1134
+ onEscape: () => setBulkConfirmState(null),
1135
+ autoFocus: true
1136
+ });
293
1137
  react.useEffect(() => {
294
1138
  if (!enableFilters) return;
295
1139
  const activeFilters = {};
@@ -297,52 +1141,17 @@ function RowaKitTable({
297
1141
  for (const [field, value] of Object.entries(filters)) {
298
1142
  if (value !== void 0) {
299
1143
  activeFilters[field] = value;
300
- hasFilters = true;
301
- }
302
- }
303
- const filtersToSend = hasFilters ? activeFilters : void 0;
304
- setQuery((prev) => ({
305
- ...prev,
306
- filters: filtersToSend,
307
- page: 1
308
- // Reset page to 1 when filters change
309
- }));
310
- }, [filters, enableFilters]);
311
- react.useEffect(() => {
312
- const currentRequestId = ++requestIdRef.current;
313
- setDataState((prev) => ({ ...prev, state: "loading" }));
314
- fetcher(query).then((result) => {
315
- if (currentRequestId !== requestIdRef.current) {
316
- return;
317
- }
318
- if (result.items.length === 0) {
319
- setDataState({
320
- state: "empty",
321
- items: [],
322
- total: result.total
323
- });
324
- } else {
325
- setDataState({
326
- state: "success",
327
- items: result.items,
328
- total: result.total
329
- });
330
- }
331
- }).catch((error) => {
332
- if (currentRequestId !== requestIdRef.current) {
333
- return;
1144
+ hasFilters = true;
334
1145
  }
335
- setDataState({
336
- state: "error",
337
- items: [],
338
- total: 0,
339
- error: error instanceof Error ? error.message : "Failed to load data"
340
- });
341
- });
342
- }, [fetcher, query]);
343
- const handleRetry = () => {
344
- setQuery({ ...query });
345
- };
1146
+ }
1147
+ const filtersToSend = hasFilters ? activeFilters : void 0;
1148
+ setQuery((prev) => ({
1149
+ ...prev,
1150
+ filters: filtersToSend,
1151
+ page: 1
1152
+ // Reset page to 1 when filters change
1153
+ }));
1154
+ }, [filters, enableFilters]);
346
1155
  const handlePreviousPage = () => {
347
1156
  if (query.page > 1) {
348
1157
  setQuery((prev) => ({ ...prev, page: prev.page - 1 }));
@@ -357,140 +1166,6 @@ function RowaKitTable({
357
1166
  const handlePageSizeChange = (newPageSize) => {
358
1167
  setQuery({ ...query, pageSize: newPageSize, page: 1 });
359
1168
  };
360
- const handleSort = (field) => {
361
- setQuery((prev) => {
362
- if (prev.sort?.field !== field) {
363
- return { ...prev, sort: { field, direction: "asc" }, page: 1 };
364
- }
365
- if (prev.sort.direction === "asc") {
366
- return { ...prev, sort: { field, direction: "desc" }, page: 1 };
367
- }
368
- const { sort: _sort, ...rest } = prev;
369
- return { ...rest, page: 1 };
370
- });
371
- };
372
- const getSortIndicator = (field) => {
373
- if (!query.sort || query.sort.field !== field) {
374
- return "";
375
- }
376
- return query.sort.direction === "asc" ? " \u2191" : " \u2193";
377
- };
378
- const scheduleColumnWidthUpdate = (colId, width) => {
379
- resizePendingRef.current = { colId, width };
380
- if (resizeRafRef.current != null) return;
381
- resizeRafRef.current = requestAnimationFrame(() => {
382
- resizeRafRef.current = null;
383
- const pending = resizePendingRef.current;
384
- if (!pending) return;
385
- handleColumnResize(pending.colId, pending.width);
386
- });
387
- };
388
- const handleColumnResize = (columnId, newWidth) => {
389
- const minWidth = columns.find((c) => c.id === columnId)?.minWidth ?? 80;
390
- const maxWidth = columns.find((c) => c.id === columnId)?.maxWidth;
391
- let finalWidth = Math.max(minWidth, newWidth);
392
- if (maxWidth) {
393
- finalWidth = Math.min(finalWidth, maxWidth);
394
- }
395
- setColumnWidths((prev) => ({
396
- ...prev,
397
- [columnId]: finalWidth
398
- }));
399
- };
400
- const startColumnResize = (e, columnId) => {
401
- e.preventDefault();
402
- const startX = e.clientX;
403
- const th = e.currentTarget.parentElement;
404
- let startWidth = columnWidths[columnId] ?? th.offsetWidth;
405
- const MIN_DRAG_WIDTH = 80;
406
- if (startWidth < MIN_DRAG_WIDTH) {
407
- const nextTh = th.nextElementSibling;
408
- if (nextTh && nextTh.offsetWidth >= 50) {
409
- startWidth = nextTh.offsetWidth;
410
- } else {
411
- startWidth = 100;
412
- }
413
- }
414
- document.body.classList.add("rowakit-resizing");
415
- const handleMouseMove = (moveEvent) => {
416
- const delta = moveEvent.clientX - startX;
417
- const newWidth = startWidth + delta;
418
- scheduleColumnWidthUpdate(columnId, newWidth);
419
- };
420
- const handleMouseUp = () => {
421
- document.removeEventListener("mousemove", handleMouseMove);
422
- document.removeEventListener("mouseup", handleMouseUp);
423
- document.body.classList.remove("rowakit-resizing");
424
- };
425
- document.addEventListener("mousemove", handleMouseMove);
426
- document.addEventListener("mouseup", handleMouseUp);
427
- };
428
- const handleColumnResizeDoubleClick = (columnId) => {
429
- const tableEl = tableRef.current;
430
- if (!tableEl) return;
431
- const th = tableEl.querySelector(`th[data-col-id="${columnId}"]`);
432
- if (!th) return;
433
- const tds = Array.from(tableEl.querySelectorAll(`td[data-col-id="${columnId}"]`));
434
- const headerW = th.scrollWidth;
435
- const cellsMaxW = tds.reduce((max, td) => Math.max(max, td.scrollWidth), 0);
436
- const padding = 24;
437
- const raw = Math.max(headerW, cellsMaxW) + padding;
438
- const minW = columns.find((c) => c.id === columnId)?.minWidth ?? 80;
439
- const maxW = columns.find((c) => c.id === columnId)?.maxWidth ?? 600;
440
- const finalW = Math.max(minW, Math.min(raw, maxW));
441
- setColumnWidths((prev) => ({ ...prev, [columnId]: finalW }));
442
- };
443
- const saveCurrentView = (name) => {
444
- const viewState = {
445
- page: query.page,
446
- pageSize: query.pageSize,
447
- sort: query.sort,
448
- filters: query.filters,
449
- columnWidths: enableColumnResizing ? columnWidths : void 0
450
- };
451
- setSavedViews((prev) => {
452
- const filtered = prev.filter((v) => v.name !== name);
453
- return [...filtered, { name, state: viewState }];
454
- });
455
- if (typeof window !== "undefined" && window.localStorage) {
456
- try {
457
- localStorage.setItem(`rowakit-view-${name}`, JSON.stringify(viewState));
458
- } catch {
459
- }
460
- }
461
- };
462
- const loadSavedView = (name) => {
463
- const view = savedViews.find((v) => v.name === name);
464
- if (!view) return;
465
- const { state } = view;
466
- setQuery({
467
- page: state.page,
468
- pageSize: state.pageSize,
469
- sort: state.sort,
470
- filters: state.filters
471
- });
472
- setFilters(state.filters ?? {});
473
- if (state.columnWidths && enableColumnResizing) {
474
- setColumnWidths(state.columnWidths);
475
- }
476
- };
477
- const deleteSavedView = (name) => {
478
- setSavedViews((prev) => prev.filter((v) => v.name !== name));
479
- if (typeof window !== "undefined" && window.localStorage) {
480
- try {
481
- localStorage.removeItem(`rowakit-view-${name}`);
482
- } catch {
483
- }
484
- }
485
- };
486
- const resetTableState = () => {
487
- setQuery({
488
- page: 1,
489
- pageSize: defaultPageSize
490
- });
491
- setFilters({});
492
- setColumnWidths({});
493
- };
494
1169
  const transformFilterValueForColumn = (column, value) => {
495
1170
  if (!value || column?.kind !== "number") {
496
1171
  return value;
@@ -535,29 +1210,100 @@ function RowaKitTable({
535
1210
  const handleClearAllFilters = () => {
536
1211
  setFilters({});
537
1212
  };
538
- const isLoading = dataState.state === "loading";
539
- const isError = dataState.state === "error";
540
- const isEmpty = dataState.state === "empty";
541
1213
  const totalPages = Math.ceil(dataState.total / query.pageSize);
542
1214
  const canGoPrevious = query.page > 1 && !isLoading;
543
1215
  const canGoNext = query.page < totalPages && !isLoading;
1216
+ const tableColumnCount = columns.length + (enableRowSelection ? 1 : 0);
544
1217
  const hasActiveFilters = enableFilters && Object.values(filters).some((v) => v !== void 0);
545
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `rowakit-table${className ? ` ${className}` : ""}`, children: [
1218
+ const containerClass = [
1219
+ "rowakit-table",
1220
+ enableColumnResizing ? "rowakit-layout-fixed" : "",
1221
+ className
1222
+ ].filter(Boolean).join(" ");
1223
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: containerClass, children: [
1224
+ enableRowSelection && bulkActions && bulkActions.length > 0 && selectedKeys.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
1225
+ BulkActionBar,
1226
+ {
1227
+ selectedCount: selectedKeys.length,
1228
+ actions: bulkActions,
1229
+ onActionClick: (actionId) => {
1230
+ const action = bulkActions.find((a) => a.id === actionId);
1231
+ if (!action) return;
1232
+ const snapshot = [...selectedKeys];
1233
+ if (action.confirm) {
1234
+ setBulkConfirmState({ action, selectedKeys: snapshot });
1235
+ return;
1236
+ }
1237
+ action.onClick(snapshot);
1238
+ }
1239
+ }
1240
+ ),
1241
+ exporter && /* @__PURE__ */ jsxRuntime.jsx(ExportButton, { exporter, query }),
546
1242
  enableSavedViews && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rowakit-saved-views-group", children: [
547
- /* @__PURE__ */ jsxRuntime.jsx(
1243
+ !showSaveViewForm ? /* @__PURE__ */ jsxRuntime.jsx(
548
1244
  "button",
549
1245
  {
550
- onClick: () => {
551
- const name = typeof window !== "undefined" ? window.prompt("Enter view name:") : null;
552
- if (name) {
553
- saveCurrentView(name);
554
- }
555
- },
1246
+ onClick: openSaveViewForm,
556
1247
  className: "rowakit-saved-view-button",
557
1248
  type: "button",
558
1249
  children: "Save View"
559
1250
  }
560
- ),
1251
+ ) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rowakit-save-view-form", children: overwriteConfirmName ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rowakit-save-view-confirm", children: [
1252
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { children: [
1253
+ 'View "',
1254
+ overwriteConfirmName,
1255
+ '" already exists. Overwrite?'
1256
+ ] }),
1257
+ /* @__PURE__ */ jsxRuntime.jsx(
1258
+ "button",
1259
+ {
1260
+ onClick: confirmOverwrite,
1261
+ className: "rowakit-saved-view-button",
1262
+ type: "button",
1263
+ children: "Overwrite"
1264
+ }
1265
+ ),
1266
+ /* @__PURE__ */ jsxRuntime.jsx(
1267
+ "button",
1268
+ {
1269
+ onClick: cancelOverwrite,
1270
+ className: "rowakit-saved-view-button",
1271
+ type: "button",
1272
+ children: "Cancel"
1273
+ }
1274
+ )
1275
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1276
+ /* @__PURE__ */ jsxRuntime.jsx(
1277
+ "input",
1278
+ {
1279
+ type: "text",
1280
+ value: saveViewInput,
1281
+ onChange: onSaveViewInputChange,
1282
+ onKeyDown: onSaveViewInputKeyDown,
1283
+ placeholder: "Enter view name...",
1284
+ className: "rowakit-save-view-input"
1285
+ }
1286
+ ),
1287
+ saveViewError && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rowakit-save-view-error", children: saveViewError }),
1288
+ /* @__PURE__ */ jsxRuntime.jsx(
1289
+ "button",
1290
+ {
1291
+ onClick: attemptSave,
1292
+ className: "rowakit-saved-view-button",
1293
+ type: "button",
1294
+ children: "Save"
1295
+ }
1296
+ ),
1297
+ /* @__PURE__ */ jsxRuntime.jsx(
1298
+ "button",
1299
+ {
1300
+ onClick: cancelSaveViewForm,
1301
+ className: "rowakit-saved-view-button",
1302
+ type: "button",
1303
+ children: "Cancel"
1304
+ }
1305
+ )
1306
+ ] }) }),
561
1307
  savedViews.map((view) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rowakit-saved-view-item", children: [
562
1308
  /* @__PURE__ */ jsxRuntime.jsx(
563
1309
  "button",
@@ -600,159 +1346,143 @@ function RowaKitTable({
600
1346
  ) }),
601
1347
  /* @__PURE__ */ jsxRuntime.jsxs("table", { ref: tableRef, children: [
602
1348
  /* @__PURE__ */ jsxRuntime.jsxs("thead", { children: [
603
- /* @__PURE__ */ jsxRuntime.jsx("tr", { children: columns.map((column) => {
604
- const isSortable = column.kind !== "actions" && (column.kind === "custom" ? false : column.sortable === true);
605
- const field = column.kind === "actions" ? "" : column.kind === "custom" ? column.field : column.field;
606
- const isResizable = enableColumnResizing && column.kind !== "actions";
607
- const actualWidth = columnWidths[column.id] ?? column.width;
608
- return /* @__PURE__ */ jsxRuntime.jsxs(
609
- "th",
1349
+ /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
1350
+ enableRowSelection && /* @__PURE__ */ jsxRuntime.jsx(
1351
+ RowSelectionHeaderCell,
610
1352
  {
611
- "data-col-id": column.id,
612
- onClick: isSortable ? () => handleSort(String(field)) : void 0,
613
- role: isSortable ? "button" : void 0,
614
- tabIndex: isSortable ? 0 : void 0,
615
- onKeyDown: isSortable ? (e) => {
616
- if (e.key === "Enter" || e.key === " ") {
617
- e.preventDefault();
618
- handleSort(String(field));
1353
+ checked: headerChecked,
1354
+ indeterminate: headerIndeterminate,
1355
+ disabled: isLoading || pageRowKeys.length === 0,
1356
+ onChange: (checked) => {
1357
+ if (checked) {
1358
+ setSelectedKeys(selectAll(pageRowKeys));
1359
+ } else {
1360
+ setSelectedKeys(clearSelection());
619
1361
  }
620
- } : void 0,
621
- "aria-sort": isSortable && query.sort?.field === String(field) ? query.sort.direction === "asc" ? "ascending" : "descending" : void 0,
622
- style: {
623
- width: actualWidth ? `${actualWidth}px` : void 0,
624
- textAlign: column.align,
625
- position: isResizable ? "relative" : void 0
626
- },
627
- className: column.truncate && !isResizable ? "rowakit-cell-truncate" : void 0,
628
- children: [
629
- getHeaderLabel(column),
630
- isSortable && getSortIndicator(String(field)),
631
- isResizable && /* @__PURE__ */ jsxRuntime.jsx(
632
- "div",
633
- {
634
- className: "rowakit-column-resize-handle",
635
- onMouseDown: (e) => startColumnResize(e, column.id),
636
- onDoubleClick: () => handleColumnResizeDoubleClick(column.id),
637
- title: "Drag to resize | Double-click to auto-fit content"
638
- }
639
- )
640
- ]
641
- },
642
- column.id
643
- );
644
- }) }),
645
- enableFilters && /* @__PURE__ */ jsxRuntime.jsx("tr", { className: "rowakit-table-filter-row", children: columns.map((column) => {
646
- const field = column.kind === "actions" || column.kind === "custom" ? "" : String(column.field);
647
- const canFilter = field && column.kind !== "actions";
648
- if (!canFilter) {
649
- return /* @__PURE__ */ jsxRuntime.jsx("th", {}, column.id);
650
- }
651
- const filterValue = filters[field];
652
- if (column.kind === "badge") {
653
- const options = column.map ? Object.keys(column.map) : [];
654
- return /* @__PURE__ */ jsxRuntime.jsx("th", { children: /* @__PURE__ */ jsxRuntime.jsxs(
655
- "select",
656
- {
657
- className: "rowakit-filter-select",
658
- value: filterValue?.op === "equals" ? String(filterValue.value ?? "") : "",
659
- onChange: (e) => {
660
- const value = e.target.value;
661
- if (value === "") {
662
- handleClearFilter(field);
663
- } else {
664
- handleFilterChange(field, { op: "equals", value });
665
- }
666
- },
667
- children: [
668
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "", children: "All" }),
669
- options.map((opt) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: opt, children: opt }, opt))
670
- ]
671
1362
  }
672
- ) }, column.id);
673
- }
674
- if (column.kind === "boolean") {
675
- return /* @__PURE__ */ jsxRuntime.jsx("th", { children: /* @__PURE__ */ jsxRuntime.jsxs(
676
- "select",
1363
+ }
1364
+ ),
1365
+ columns.map((column) => {
1366
+ const isSortable = column.kind !== "actions" && (column.kind === "custom" ? false : column.sortable === true);
1367
+ const field = column.kind === "actions" ? "" : column.kind === "custom" ? column.field : column.field;
1368
+ const isResizable = enableColumnResizing && column.kind !== "actions";
1369
+ const actualWidth = columnWidths[column.id] ?? column.width;
1370
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1371
+ "th",
677
1372
  {
678
- className: "rowakit-filter-select",
679
- value: filterValue?.op === "equals" && typeof filterValue.value === "boolean" ? String(filterValue.value) : "",
680
- onChange: (e) => {
681
- const value = e.target.value;
682
- if (value === "") {
683
- handleClearFilter(field);
684
- } else {
685
- handleFilterChange(field, { op: "equals", value: value === "true" });
1373
+ "data-col-id": column.id,
1374
+ onClick: isSortable ? (e) => {
1375
+ if (isResizingRef.current) return;
1376
+ if (Date.now() - lastResizeEndTsRef.current < 150) return;
1377
+ const isMultiSort = e.ctrlKey || e.metaKey;
1378
+ handleSort(String(field), isMultiSort);
1379
+ } : void 0,
1380
+ role: isSortable ? "button" : void 0,
1381
+ tabIndex: isSortable ? 0 : void 0,
1382
+ onKeyDown: isSortable ? (e) => {
1383
+ if (e.key === "Enter" || e.key === " ") {
1384
+ e.preventDefault();
1385
+ const isMultiSort = e.shiftKey;
1386
+ handleSort(String(field), isMultiSort);
686
1387
  }
1388
+ } : void 0,
1389
+ "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,
1390
+ style: {
1391
+ width: actualWidth != null ? `${actualWidth}px` : void 0,
1392
+ textAlign: column.align,
1393
+ position: isResizable ? "relative" : void 0
687
1394
  },
1395
+ className: [
1396
+ column.truncate ? "rowakit-cell-truncate" : "",
1397
+ resizingColIdRef.current === column.id ? "resizing" : ""
1398
+ // PRD-01
1399
+ ].filter(Boolean).join(" ") || void 0,
688
1400
  children: [
689
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "", children: "All" }),
690
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "true", children: "True" }),
691
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "false", children: "False" })
1401
+ getHeaderLabel(column),
1402
+ isSortable && getSortIndicator(String(field)),
1403
+ isResizable && /* @__PURE__ */ jsxRuntime.jsx(
1404
+ "div",
1405
+ {
1406
+ className: "rowakit-column-resize-handle",
1407
+ onPointerDown: (e) => startColumnResize(e, column.id),
1408
+ onDoubleClick: (e) => handleColumnResizeDoubleClick(e, column.id),
1409
+ title: "Drag to resize | Double-click to auto-fit content"
1410
+ }
1411
+ )
692
1412
  ]
693
- }
694
- ) }, column.id);
695
- }
696
- if (column.kind === "date") {
697
- const fromValue = filterValue?.op === "range" ? filterValue.value.from ?? "" : "";
698
- const toValue = filterValue?.op === "range" ? filterValue.value.to ?? "" : "";
699
- return /* @__PURE__ */ jsxRuntime.jsx("th", { children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rowakit-filter-date-range", children: [
700
- /* @__PURE__ */ jsxRuntime.jsx(
701
- "input",
1413
+ },
1414
+ column.id
1415
+ );
1416
+ })
1417
+ ] }),
1418
+ enableFilters && /* @__PURE__ */ jsxRuntime.jsxs("tr", { className: "rowakit-table-filter-row", children: [
1419
+ enableRowSelection && /* @__PURE__ */ jsxRuntime.jsx("th", {}),
1420
+ columns.map((column) => {
1421
+ const field = column.kind === "actions" || column.kind === "custom" ? "" : String(column.field);
1422
+ const canFilter = field && column.kind !== "actions";
1423
+ if (!canFilter) {
1424
+ return /* @__PURE__ */ jsxRuntime.jsx("th", {}, column.id);
1425
+ }
1426
+ const filterValue = filters[field];
1427
+ if (column.kind === "badge") {
1428
+ const options = column.map ? Object.keys(column.map) : [];
1429
+ return /* @__PURE__ */ jsxRuntime.jsx("th", { children: /* @__PURE__ */ jsxRuntime.jsxs(
1430
+ "select",
702
1431
  {
703
- type: "date",
704
- className: "rowakit-filter-input",
705
- placeholder: "From",
706
- value: fromValue,
1432
+ className: "rowakit-filter-select",
1433
+ value: filterValue?.op === "equals" ? String(filterValue.value ?? "") : "",
707
1434
  onChange: (e) => {
708
- const from = e.target.value || void 0;
709
- const to = toValue || void 0;
710
- if (!from && !to) {
1435
+ const value = e.target.value;
1436
+ if (value === "") {
711
1437
  handleClearFilter(field);
712
1438
  } else {
713
- handleFilterChange(field, { op: "range", value: { from, to } });
1439
+ handleFilterChange(field, { op: "equals", value });
714
1440
  }
715
- }
1441
+ },
1442
+ children: [
1443
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "", children: "All" }),
1444
+ options.map((opt) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: opt, children: opt }, opt))
1445
+ ]
716
1446
  }
717
- ),
718
- /* @__PURE__ */ jsxRuntime.jsx(
719
- "input",
1447
+ ) }, column.id);
1448
+ }
1449
+ if (column.kind === "boolean") {
1450
+ return /* @__PURE__ */ jsxRuntime.jsx("th", { children: /* @__PURE__ */ jsxRuntime.jsxs(
1451
+ "select",
720
1452
  {
721
- type: "date",
722
- className: "rowakit-filter-input",
723
- placeholder: "To",
724
- value: toValue,
1453
+ className: "rowakit-filter-select",
1454
+ value: filterValue?.op === "equals" && typeof filterValue.value === "boolean" ? String(filterValue.value) : "",
725
1455
  onChange: (e) => {
726
- const to = e.target.value || void 0;
727
- const from = fromValue || void 0;
728
- if (!from && !to) {
1456
+ const value = e.target.value;
1457
+ if (value === "") {
729
1458
  handleClearFilter(field);
730
1459
  } else {
731
- handleFilterChange(field, { op: "range", value: { from, to } });
1460
+ handleFilterChange(field, { op: "equals", value: value === "true" });
732
1461
  }
733
- }
1462
+ },
1463
+ children: [
1464
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "", children: "All" }),
1465
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "true", children: "True" }),
1466
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "false", children: "False" })
1467
+ ]
734
1468
  }
735
- )
736
- ] }) }, column.id);
737
- }
738
- const isNumberColumn = column.kind === "number";
739
- if (isNumberColumn) {
740
- const fromValue = filterValue?.op === "range" ? String(filterValue.value.from ?? "") : filterValue?.op === "equals" && typeof filterValue.value === "number" ? String(filterValue.value) : "";
741
- const toValue = filterValue?.op === "range" ? String(filterValue.value.to ?? "") : "";
742
- const showRangeUI = !filterValue || filterValue.op === "range";
743
- if (showRangeUI) {
744
- return /* @__PURE__ */ jsxRuntime.jsx("th", { children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rowakit-filter-number-range", children: [
1469
+ ) }, column.id);
1470
+ }
1471
+ if (column.kind === "date") {
1472
+ const fromValue = filterValue?.op === "range" ? filterValue.value.from ?? "" : "";
1473
+ const toValue = filterValue?.op === "range" ? filterValue.value.to ?? "" : "";
1474
+ return /* @__PURE__ */ jsxRuntime.jsx("th", { children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rowakit-filter-date-range", children: [
745
1475
  /* @__PURE__ */ jsxRuntime.jsx(
746
1476
  "input",
747
1477
  {
748
- type: "number",
1478
+ type: "date",
749
1479
  className: "rowakit-filter-input",
750
- placeholder: "Min",
1480
+ placeholder: "From",
751
1481
  value: fromValue,
752
1482
  onChange: (e) => {
753
- const from = e.target.value ? Number(e.target.value) : void 0;
754
- const to = toValue ? Number(toValue) : void 0;
755
- if (from === void 0 && to === void 0) {
1483
+ const from = e.target.value || void 0;
1484
+ const to = toValue || void 0;
1485
+ if (!from && !to) {
756
1486
  handleClearFilter(field);
757
1487
  } else {
758
1488
  handleFilterChange(field, { op: "range", value: { from, to } });
@@ -763,14 +1493,14 @@ function RowaKitTable({
763
1493
  /* @__PURE__ */ jsxRuntime.jsx(
764
1494
  "input",
765
1495
  {
766
- type: "number",
1496
+ type: "date",
767
1497
  className: "rowakit-filter-input",
768
- placeholder: "Max",
1498
+ placeholder: "To",
769
1499
  value: toValue,
770
1500
  onChange: (e) => {
771
- const to = e.target.value ? Number(e.target.value) : void 0;
772
- const from = fromValue ? Number(fromValue) : void 0;
773
- if (from === void 0 && to === void 0) {
1501
+ const to = e.target.value || void 0;
1502
+ const from = fromValue || void 0;
1503
+ if (!from && !to) {
774
1504
  handleClearFilter(field);
775
1505
  } else {
776
1506
  handleFilterChange(field, { op: "range", value: { from, to } });
@@ -780,39 +1510,85 @@ function RowaKitTable({
780
1510
  )
781
1511
  ] }) }, column.id);
782
1512
  }
783
- }
784
- return /* @__PURE__ */ jsxRuntime.jsx("th", { children: /* @__PURE__ */ jsxRuntime.jsx(
785
- "input",
786
- {
787
- type: isNumberColumn ? "number" : "text",
788
- className: "rowakit-filter-input",
789
- placeholder: `Filter ${getHeaderLabel(column)}...`,
790
- 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) : "",
791
- onChange: (e) => {
792
- const rawValue = e.target.value;
793
- if (rawValue === "") {
794
- handleClearFilter(field);
795
- } else if (isNumberColumn) {
796
- const numValue = Number(rawValue);
797
- if (!isNaN(numValue)) {
798
- handleFilterChange(field, { op: "equals", value: numValue });
799
- } else {
1513
+ const isNumberColumn = column.kind === "number";
1514
+ if (isNumberColumn) {
1515
+ const fromValue = filterValue?.op === "range" ? String(filterValue.value.from ?? "") : filterValue?.op === "equals" && typeof filterValue.value === "number" ? String(filterValue.value) : "";
1516
+ const toValue = filterValue?.op === "range" ? String(filterValue.value.to ?? "") : "";
1517
+ const showRangeUI = !filterValue || filterValue.op === "range";
1518
+ if (showRangeUI) {
1519
+ return /* @__PURE__ */ jsxRuntime.jsx("th", { children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rowakit-filter-number-range", children: [
1520
+ /* @__PURE__ */ jsxRuntime.jsx(
1521
+ "input",
1522
+ {
1523
+ type: "number",
1524
+ className: "rowakit-filter-input",
1525
+ placeholder: "Min",
1526
+ value: fromValue,
1527
+ onChange: (e) => {
1528
+ const from = e.target.value ? Number(e.target.value) : void 0;
1529
+ const to = toValue ? Number(toValue) : void 0;
1530
+ if (from === void 0 && to === void 0) {
1531
+ handleClearFilter(field);
1532
+ } else {
1533
+ handleFilterChange(field, { op: "range", value: { from, to } });
1534
+ }
1535
+ }
1536
+ }
1537
+ ),
1538
+ /* @__PURE__ */ jsxRuntime.jsx(
1539
+ "input",
1540
+ {
1541
+ type: "number",
1542
+ className: "rowakit-filter-input",
1543
+ placeholder: "Max",
1544
+ value: toValue,
1545
+ onChange: (e) => {
1546
+ const to = e.target.value ? Number(e.target.value) : void 0;
1547
+ const from = fromValue ? Number(fromValue) : void 0;
1548
+ if (from === void 0 && to === void 0) {
1549
+ handleClearFilter(field);
1550
+ } else {
1551
+ handleFilterChange(field, { op: "range", value: { from, to } });
1552
+ }
1553
+ }
1554
+ }
1555
+ )
1556
+ ] }) }, column.id);
1557
+ }
1558
+ }
1559
+ return /* @__PURE__ */ jsxRuntime.jsx("th", { children: /* @__PURE__ */ jsxRuntime.jsx(
1560
+ "input",
1561
+ {
1562
+ type: isNumberColumn ? "number" : "text",
1563
+ className: "rowakit-filter-input",
1564
+ placeholder: `Filter ${getHeaderLabel(column)}...`,
1565
+ 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) : "",
1566
+ onChange: (e) => {
1567
+ const rawValue = e.target.value;
1568
+ if (rawValue === "") {
800
1569
  handleClearFilter(field);
1570
+ } else if (isNumberColumn) {
1571
+ const numValue = Number(rawValue);
1572
+ if (!isNaN(numValue)) {
1573
+ handleFilterChange(field, { op: "equals", value: numValue });
1574
+ } else {
1575
+ handleClearFilter(field);
1576
+ }
1577
+ } else {
1578
+ handleFilterChange(field, { op: "contains", value: rawValue });
801
1579
  }
802
- } else {
803
- handleFilterChange(field, { op: "contains", value: rawValue });
804
1580
  }
805
1581
  }
806
- }
807
- ) }, column.id);
808
- }) })
1582
+ ) }, column.id);
1583
+ })
1584
+ ] })
809
1585
  ] }),
810
1586
  /* @__PURE__ */ jsxRuntime.jsxs("tbody", { children: [
811
- isLoading && /* @__PURE__ */ jsxRuntime.jsx("tr", { children: /* @__PURE__ */ jsxRuntime.jsxs("td", { colSpan: columns.length, className: "rowakit-table-loading", children: [
1587
+ isLoading && /* @__PURE__ */ jsxRuntime.jsx("tr", { children: /* @__PURE__ */ jsxRuntime.jsxs("td", { colSpan: tableColumnCount, className: "rowakit-table-loading", children: [
812
1588
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rowakit-table-loading-spinner" }),
813
1589
  /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Loading..." })
814
1590
  ] }) }),
815
- isError && /* @__PURE__ */ jsxRuntime.jsx("tr", { children: /* @__PURE__ */ jsxRuntime.jsxs("td", { colSpan: columns.length, className: "rowakit-table-error", children: [
1591
+ isError && /* @__PURE__ */ jsxRuntime.jsx("tr", { children: /* @__PURE__ */ jsxRuntime.jsxs("td", { colSpan: tableColumnCount, className: "rowakit-table-error", children: [
816
1592
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rowakit-table-error-message", children: dataState.error ?? "An error occurred" }),
817
1593
  /* @__PURE__ */ jsxRuntime.jsx(
818
1594
  "button",
@@ -824,29 +1600,42 @@ function RowaKitTable({
824
1600
  }
825
1601
  )
826
1602
  ] }) }),
827
- isEmpty && /* @__PURE__ */ jsxRuntime.jsx("tr", { children: /* @__PURE__ */ jsxRuntime.jsx("td", { colSpan: columns.length, className: "rowakit-table-empty", children: "No data" }) }),
1603
+ isEmpty && /* @__PURE__ */ jsxRuntime.jsx("tr", { children: /* @__PURE__ */ jsxRuntime.jsx("td", { colSpan: tableColumnCount, className: "rowakit-table-empty", children: "No data" }) }),
828
1604
  dataState.state === "success" && dataState.items.map((row) => {
829
1605
  const key = getRowKey(row, rowKey);
830
- return /* @__PURE__ */ jsxRuntime.jsx("tr", { children: columns.map((column) => {
831
- const cellClass = [
832
- column.kind === "number" ? "rowakit-cell-number" : "",
833
- column.truncate ? "rowakit-cell-truncate" : ""
834
- ].filter(Boolean).join(" ") || void 0;
835
- const actualWidth = columnWidths[column.id];
836
- return /* @__PURE__ */ jsxRuntime.jsx(
837
- "td",
1606
+ return /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
1607
+ enableRowSelection && /* @__PURE__ */ jsxRuntime.jsx(
1608
+ RowSelectionCell,
838
1609
  {
839
- "data-col-id": column.id,
840
- className: cellClass,
841
- style: {
842
- width: actualWidth ? `${actualWidth}px` : void 0,
843
- textAlign: column.align || (column.kind === "number" ? "right" : void 0)
1610
+ rowKey: key,
1611
+ disabled: isLoading,
1612
+ checked: selectedKeys.includes(key),
1613
+ onChange: () => {
1614
+ setSelectedKeys((prev) => toggleSelectionKey(prev, key));
1615
+ }
1616
+ }
1617
+ ),
1618
+ columns.map((column) => {
1619
+ const cellClass = [
1620
+ column.kind === "number" ? "rowakit-cell-number" : "",
1621
+ column.truncate ? "rowakit-cell-truncate" : ""
1622
+ ].filter(Boolean).join(" ") || void 0;
1623
+ const actualWidth = columnWidths[column.id] ?? column.width;
1624
+ return /* @__PURE__ */ jsxRuntime.jsx(
1625
+ "td",
1626
+ {
1627
+ "data-col-id": column.id,
1628
+ className: cellClass,
1629
+ style: {
1630
+ width: actualWidth != null ? `${actualWidth}px` : void 0,
1631
+ textAlign: column.align || (column.kind === "number" ? "right" : void 0)
1632
+ },
1633
+ children: renderCell(column, row, isLoading, setConfirmState)
844
1634
  },
845
- children: renderCell(column, row, isLoading, setConfirmState)
846
- },
847
- column.id
848
- );
849
- }) }, key);
1635
+ column.id
1636
+ );
1637
+ })
1638
+ ] }, key);
850
1639
  })
851
1640
  ] })
852
1641
  ] }),
@@ -901,6 +1690,7 @@ function RowaKitTable({
901
1690
  confirmState && /* @__PURE__ */ jsxRuntime.jsx(
902
1691
  "div",
903
1692
  {
1693
+ ref: confirmModalRef,
904
1694
  className: "rowakit-modal-backdrop",
905
1695
  onClick: () => setConfirmState(null),
906
1696
  role: "dialog",
@@ -938,13 +1728,51 @@ function RowaKitTable({
938
1728
  ] })
939
1729
  ] })
940
1730
  }
1731
+ ),
1732
+ bulkConfirmState && /* @__PURE__ */ jsxRuntime.jsx(
1733
+ "div",
1734
+ {
1735
+ ref: bulkConfirmModalRef,
1736
+ className: "rowakit-modal-backdrop",
1737
+ onClick: () => setBulkConfirmState(null),
1738
+ role: "dialog",
1739
+ "aria-modal": "true",
1740
+ "aria-labelledby": "bulk-confirm-dialog-title",
1741
+ children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rowakit-modal", onClick: (e) => e.stopPropagation(), children: [
1742
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { id: "bulk-confirm-dialog-title", className: "rowakit-modal-title", children: bulkConfirmState.action.confirm?.title ?? "Confirm Action" }),
1743
+ /* @__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." }),
1744
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rowakit-modal-actions", children: [
1745
+ /* @__PURE__ */ jsxRuntime.jsx(
1746
+ "button",
1747
+ {
1748
+ onClick: () => setBulkConfirmState(null),
1749
+ className: "rowakit-button rowakit-button-secondary",
1750
+ type: "button",
1751
+ children: "Cancel"
1752
+ }
1753
+ ),
1754
+ /* @__PURE__ */ jsxRuntime.jsx(
1755
+ "button",
1756
+ {
1757
+ onClick: () => {
1758
+ bulkConfirmState.action.onClick(bulkConfirmState.selectedKeys);
1759
+ setBulkConfirmState(null);
1760
+ },
1761
+ className: "rowakit-button rowakit-button-danger",
1762
+ type: "button",
1763
+ children: "Confirm"
1764
+ }
1765
+ )
1766
+ ] })
1767
+ ] })
1768
+ }
941
1769
  )
942
1770
  ] });
943
1771
  }
944
1772
  var SmartTable = RowaKitTable;
945
1773
 
946
1774
  // src/index.ts
947
- var VERSION = "0.1.0";
1775
+ var VERSION = "0.5.0" ;
948
1776
 
949
1777
  exports.RowaKitTable = RowaKitTable;
950
1778
  exports.SmartTable = SmartTable;