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