@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.cjs CHANGED
@@ -105,20 +105,178 @@ var col = {
105
105
  actions,
106
106
  custom
107
107
  };
108
- function getRowKey(row, rowKey) {
109
- if (typeof rowKey === "function") {
110
- return rowKey(row);
111
- }
112
- if (rowKey) {
113
- return String(row[rowKey]);
114
- }
115
- if (row && typeof row === "object" && "id" in row) {
116
- return String(row.id);
117
- }
118
- return String(row);
108
+ function useColumnResizing(columns) {
109
+ const [columnWidths, setColumnWidths] = react.useState({});
110
+ const resizeRafRef = react.useRef(null);
111
+ const resizePendingRef = react.useRef(null);
112
+ const tableRef = react.useRef(null);
113
+ const isResizingRef = react.useRef(false);
114
+ const lastResizeEndTsRef = react.useRef(0);
115
+ const resizingColIdRef = react.useRef(null);
116
+ const scheduleColumnWidthUpdate = (colId, width) => {
117
+ resizePendingRef.current = { colId, width };
118
+ if (resizeRafRef.current != null) return;
119
+ resizeRafRef.current = requestAnimationFrame(() => {
120
+ resizeRafRef.current = null;
121
+ const pending = resizePendingRef.current;
122
+ if (!pending) return;
123
+ handleColumnResize(pending.colId, pending.width);
124
+ });
125
+ };
126
+ const handleColumnResize = (columnId, newWidth) => {
127
+ const minWidth = columns.find((c) => c.id === columnId)?.minWidth ?? 80;
128
+ const maxWidth = columns.find((c) => c.id === columnId)?.maxWidth;
129
+ let finalWidth = Math.max(minWidth, newWidth);
130
+ if (maxWidth) {
131
+ finalWidth = Math.min(finalWidth, maxWidth);
132
+ }
133
+ if (columnWidths[columnId] === finalWidth) {
134
+ return;
135
+ }
136
+ setColumnWidths((prev) => ({
137
+ ...prev,
138
+ [columnId]: finalWidth
139
+ }));
140
+ };
141
+ const autoFitColumnWidth = (columnId) => {
142
+ const tableEl = tableRef.current;
143
+ if (!tableEl) return;
144
+ const th = tableEl.querySelector(`th[data-col-id="${columnId}"]`);
145
+ if (!th) return;
146
+ const tds = Array.from(
147
+ tableEl.querySelectorAll(`td[data-col-id="${columnId}"]`)
148
+ );
149
+ const headerW = th.scrollWidth;
150
+ const cellsMaxW = tds.reduce((max, td) => Math.max(max, td.scrollWidth), 0);
151
+ const padding = 24;
152
+ const raw = Math.max(headerW, cellsMaxW) + padding;
153
+ const colDef = columns.find((c) => c.id === columnId);
154
+ const minW = colDef?.minWidth ?? 80;
155
+ const maxW = colDef?.maxWidth ?? 600;
156
+ const finalW = Math.max(minW, Math.min(raw, maxW));
157
+ setColumnWidths((prev) => ({ ...prev, [columnId]: finalW }));
158
+ };
159
+ const startColumnResize = (e, columnId) => {
160
+ e.preventDefault();
161
+ e.stopPropagation();
162
+ if (e.detail === 2) {
163
+ autoFitColumnWidth(columnId);
164
+ return;
165
+ }
166
+ if (e.pointerType === "mouse" && e.buttons !== 1) {
167
+ return;
168
+ }
169
+ const target = e.currentTarget;
170
+ const pointerId = e.pointerId;
171
+ try {
172
+ target.setPointerCapture(pointerId);
173
+ } catch {
174
+ }
175
+ isResizingRef.current = true;
176
+ resizingColIdRef.current = columnId;
177
+ const startX = e.clientX;
178
+ const th = target.parentElement;
179
+ let startWidth = columnWidths[columnId] ?? th.offsetWidth;
180
+ const MIN_DRAG_WIDTH = 80;
181
+ if (startWidth < MIN_DRAG_WIDTH) {
182
+ const nextTh = th.nextElementSibling;
183
+ if (nextTh && nextTh.offsetWidth >= 50) {
184
+ startWidth = nextTh.offsetWidth;
185
+ } else {
186
+ startWidth = 100;
187
+ }
188
+ }
189
+ document.body.classList.add("rowakit-resizing");
190
+ const handlePointerMove = (moveEvent) => {
191
+ const delta = moveEvent.clientX - startX;
192
+ const newWidth = startWidth + delta;
193
+ scheduleColumnWidthUpdate(columnId, newWidth);
194
+ };
195
+ const cleanupResize = () => {
196
+ target.removeEventListener("pointermove", handlePointerMove);
197
+ target.removeEventListener("pointerup", handlePointerUp);
198
+ target.removeEventListener("pointercancel", handlePointerCancel);
199
+ document.body.classList.remove("rowakit-resizing");
200
+ isResizingRef.current = false;
201
+ resizingColIdRef.current = null;
202
+ lastResizeEndTsRef.current = Date.now();
203
+ try {
204
+ target.releasePointerCapture(pointerId);
205
+ } catch {
206
+ }
207
+ };
208
+ const handlePointerUp = () => {
209
+ cleanupResize();
210
+ };
211
+ const handlePointerCancel = () => {
212
+ cleanupResize();
213
+ };
214
+ target.addEventListener("pointermove", handlePointerMove);
215
+ target.addEventListener("pointerup", handlePointerUp);
216
+ target.addEventListener("pointercancel", handlePointerCancel);
217
+ };
218
+ const handleColumnResizeDoubleClick = (e, columnId) => {
219
+ e.preventDefault();
220
+ e.stopPropagation();
221
+ autoFitColumnWidth(columnId);
222
+ };
223
+ return {
224
+ tableRef,
225
+ columnWidths,
226
+ setColumnWidths,
227
+ startColumnResize,
228
+ handleColumnResizeDoubleClick,
229
+ isResizingRef,
230
+ lastResizeEndTsRef,
231
+ resizingColIdRef
232
+ };
119
233
  }
120
- function getHeaderLabel(column) {
121
- return column.header ?? column.id;
234
+ function useFetcherState(fetcher, query, setQuery) {
235
+ const [dataState, setDataState] = react.useState({
236
+ state: "idle",
237
+ items: [],
238
+ total: 0
239
+ });
240
+ const requestIdRef = react.useRef(0);
241
+ react.useEffect(() => {
242
+ const currentRequestId = ++requestIdRef.current;
243
+ setDataState((prev) => ({ ...prev, state: "loading" }));
244
+ fetcher(query).then((result) => {
245
+ if (currentRequestId !== requestIdRef.current) return;
246
+ if (result.items.length === 0) {
247
+ setDataState({
248
+ state: "empty",
249
+ items: [],
250
+ total: result.total
251
+ });
252
+ return;
253
+ }
254
+ setDataState({
255
+ state: "success",
256
+ items: result.items,
257
+ total: result.total
258
+ });
259
+ }).catch((error) => {
260
+ if (currentRequestId !== requestIdRef.current) return;
261
+ setDataState({
262
+ state: "error",
263
+ items: [],
264
+ total: 0,
265
+ error: error instanceof Error ? error.message : "Failed to load data"
266
+ });
267
+ });
268
+ }, [fetcher, query]);
269
+ const handleRetry = () => {
270
+ setQuery({ ...query });
271
+ };
272
+ return {
273
+ dataState,
274
+ setDataState,
275
+ handleRetry,
276
+ isLoading: dataState.state === "loading",
277
+ isError: dataState.state === "error",
278
+ isEmpty: dataState.state === "empty"
279
+ };
122
280
  }
123
281
  function validateViewName(name) {
124
282
  const trimmed = name.trim();
@@ -154,10 +312,7 @@ function getSavedViewsIndex() {
154
312
  const key = localStorage.key(i);
155
313
  if (key?.startsWith("rowakit-view-")) {
156
314
  const name = key.substring("rowakit-view-".length);
157
- rebuilt.push({
158
- name,
159
- updatedAt: Date.now()
160
- });
315
+ rebuilt.push({ name, updatedAt: Date.now() });
161
316
  }
162
317
  }
163
318
  } catch {
@@ -184,16 +339,206 @@ function loadSavedViewsFromStorage() {
184
339
  const viewStr = localStorage.getItem(`rowakit-view-${entry.name}`);
185
340
  if (viewStr) {
186
341
  const state = JSON.parse(viewStr);
187
- views.push({
188
- name: entry.name,
189
- state
190
- });
342
+ views.push({ name: entry.name, state });
191
343
  }
192
344
  } catch {
193
345
  }
194
346
  }
195
347
  return views;
196
348
  }
349
+ function useSavedViews(options) {
350
+ const [savedViews, setSavedViews] = react.useState([]);
351
+ const [showSaveViewForm, setShowSaveViewForm] = react.useState(false);
352
+ const [saveViewInput, setSaveViewInput] = react.useState("");
353
+ const [saveViewError, setSaveViewError] = react.useState("");
354
+ const [overwriteConfirmName, setOverwriteConfirmName] = react.useState(null);
355
+ react.useEffect(() => {
356
+ if (!options.enableSavedViews) return;
357
+ setSavedViews(loadSavedViewsFromStorage());
358
+ }, [options.enableSavedViews]);
359
+ const saveCurrentView = (name) => {
360
+ const viewState = {
361
+ page: options.query.page,
362
+ pageSize: options.query.pageSize,
363
+ sort: options.query.sort,
364
+ filters: options.query.filters,
365
+ columnWidths: options.enableColumnResizing ? options.columnWidths : void 0
366
+ };
367
+ setSavedViews((prev) => {
368
+ const filtered = prev.filter((v) => v.name !== name);
369
+ return [...filtered, { name, state: viewState }];
370
+ });
371
+ if (typeof window !== "undefined" && window.localStorage) {
372
+ try {
373
+ localStorage.setItem(`rowakit-view-${name}`, JSON.stringify(viewState));
374
+ const index = getSavedViewsIndex();
375
+ const filtered = index.filter((v) => v.name !== name);
376
+ filtered.push({ name, updatedAt: Date.now() });
377
+ setSavedViewsIndex(filtered);
378
+ } catch {
379
+ }
380
+ }
381
+ };
382
+ const loadSavedView = (name) => {
383
+ const view = savedViews.find((v) => v.name === name);
384
+ if (!view) return;
385
+ const { state } = view;
386
+ options.setQuery({
387
+ page: state.page,
388
+ pageSize: state.pageSize,
389
+ sort: state.sort,
390
+ filters: state.filters
391
+ });
392
+ options.setFilters(state.filters ?? {});
393
+ if (state.columnWidths && options.enableColumnResizing) {
394
+ options.setColumnWidths(state.columnWidths);
395
+ }
396
+ };
397
+ const deleteSavedView = (name) => {
398
+ setSavedViews((prev) => prev.filter((v) => v.name !== name));
399
+ if (typeof window !== "undefined" && window.localStorage) {
400
+ try {
401
+ localStorage.removeItem(`rowakit-view-${name}`);
402
+ const index = getSavedViewsIndex();
403
+ const filtered = index.filter((v) => v.name !== name);
404
+ setSavedViewsIndex(filtered);
405
+ } catch {
406
+ }
407
+ }
408
+ };
409
+ const resetTableState = () => {
410
+ options.setQuery({
411
+ page: 1,
412
+ pageSize: options.defaultPageSize
413
+ });
414
+ options.setFilters({});
415
+ options.setColumnWidths({});
416
+ };
417
+ const shouldShowReset = react.useMemo(() => {
418
+ if (!options.enableSavedViews) return false;
419
+ return Boolean(options.query.page > 1 || options.query.sort || options.query.filters && Object.keys(options.query.filters).length > 0);
420
+ }, [options.enableSavedViews, options.query.page, options.query.sort, options.query.filters]);
421
+ const openSaveViewForm = () => {
422
+ setShowSaveViewForm(true);
423
+ setSaveViewInput("");
424
+ setSaveViewError("");
425
+ setOverwriteConfirmName(null);
426
+ };
427
+ const cancelSaveViewForm = () => {
428
+ setShowSaveViewForm(false);
429
+ setSaveViewInput("");
430
+ setSaveViewError("");
431
+ setOverwriteConfirmName(null);
432
+ };
433
+ const onSaveViewInputChange = (e) => {
434
+ setSaveViewInput(e.target.value);
435
+ setSaveViewError("");
436
+ };
437
+ const attemptSave = () => {
438
+ const validation = validateViewName(saveViewInput);
439
+ if (!validation.valid) {
440
+ setSaveViewError(validation.error || "Invalid name");
441
+ return;
442
+ }
443
+ const trimmed = saveViewInput.trim();
444
+ if (savedViews.some((v) => v.name === trimmed)) {
445
+ setOverwriteConfirmName(trimmed);
446
+ return;
447
+ }
448
+ saveCurrentView(trimmed);
449
+ cancelSaveViewForm();
450
+ };
451
+ const onSaveViewInputKeyDown = (e) => {
452
+ if (e.key !== "Enter") return;
453
+ attemptSave();
454
+ };
455
+ const confirmOverwrite = () => {
456
+ if (!overwriteConfirmName) return;
457
+ saveCurrentView(overwriteConfirmName);
458
+ cancelSaveViewForm();
459
+ };
460
+ const cancelOverwrite = () => {
461
+ setOverwriteConfirmName(null);
462
+ };
463
+ return {
464
+ savedViews,
465
+ showSaveViewForm,
466
+ saveViewInput,
467
+ saveViewError,
468
+ overwriteConfirmName,
469
+ openSaveViewForm,
470
+ cancelSaveViewForm,
471
+ onSaveViewInputChange,
472
+ onSaveViewInputKeyDown,
473
+ attemptSave,
474
+ confirmOverwrite,
475
+ cancelOverwrite,
476
+ loadSavedView,
477
+ deleteSavedView,
478
+ resetTableState,
479
+ shouldShowReset
480
+ };
481
+ }
482
+
483
+ // src/hooks/useSortingState.ts
484
+ function useSortingState(query, setQuery) {
485
+ const handleSort = (field, isMultiSort = false) => {
486
+ setQuery((prev) => {
487
+ const currentSorts = prev.sorts || [];
488
+ const existingSort = currentSorts.find((s) => s.field === field);
489
+ if (!isMultiSort) {
490
+ if (existingSort?.priority === 0) {
491
+ if (existingSort.direction === "asc") {
492
+ return {
493
+ ...prev,
494
+ sorts: [{ field, direction: "desc", priority: 0 }],
495
+ page: 1
496
+ };
497
+ }
498
+ const { sorts: _removed, ...rest } = prev;
499
+ return {
500
+ ...rest,
501
+ page: 1
502
+ };
503
+ }
504
+ return {
505
+ ...prev,
506
+ sorts: [{ field, direction: "asc", priority: 0 }],
507
+ page: 1
508
+ };
509
+ }
510
+ if (existingSort) {
511
+ const newSorts2 = currentSorts.map((s) => {
512
+ if (s.field === field) {
513
+ const newDirection = s.direction === "asc" ? "desc" : "asc";
514
+ return { ...s, direction: newDirection };
515
+ }
516
+ return s;
517
+ });
518
+ return { ...prev, sorts: newSorts2, page: 1 };
519
+ }
520
+ const nextPriority = currentSorts.length;
521
+ const newSorts = [...currentSorts, { field, direction: "asc", priority: nextPriority }];
522
+ return { ...prev, sorts: newSorts, page: 1 };
523
+ });
524
+ };
525
+ const getSortIndicator = (field) => {
526
+ const sorts = query.sorts || [];
527
+ const sort = sorts.find((s) => s.field === field);
528
+ if (!sort) {
529
+ return "";
530
+ }
531
+ const directionIcon = sort.direction === "asc" ? " \u2191" : " \u2193";
532
+ const priorityLabel = sort.priority === 0 ? "" : ` [${sort.priority + 1}]`;
533
+ return directionIcon + priorityLabel;
534
+ };
535
+ const getSortPriority = (field) => {
536
+ const sorts = query.sorts || [];
537
+ const sort = sorts.find((s) => s.field === field);
538
+ return sort ? sort.priority : null;
539
+ };
540
+ return { handleSort, getSortIndicator, getSortPriority };
541
+ }
197
542
  function parseUrlState(params, defaultPageSize, pageSizeOptions) {
198
543
  const pageStr = params.get("page");
199
544
  let page = 1;
@@ -214,10 +559,22 @@ function parseUrlState(params, defaultPageSize, pageSizeOptions) {
214
559
  }
215
560
  }
216
561
  const result = { page, pageSize };
217
- const sortField = params.get("sortField");
218
- const sortDir = params.get("sortDirection");
219
- if (sortField && (sortDir === "asc" || sortDir === "desc")) {
220
- result.sort = { field: sortField, direction: sortDir };
562
+ const sortsStr = params.get("sorts");
563
+ if (sortsStr) {
564
+ try {
565
+ const parsed = JSON.parse(sortsStr);
566
+ if (Array.isArray(parsed) && parsed.every((s) => typeof s.field === "string" && (s.direction === "asc" || s.direction === "desc") && typeof s.priority === "number")) {
567
+ result.sorts = parsed;
568
+ }
569
+ } catch {
570
+ }
571
+ }
572
+ if (!result.sorts) {
573
+ const 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
+ }
221
578
  }
222
579
  const filtersStr = params.get("filters");
223
580
  if (filtersStr) {
@@ -255,7 +612,9 @@ function serializeUrlState(query, filters, columnWidths, defaultPageSize, enable
255
612
  if (query.pageSize !== defaultPageSize) {
256
613
  params.set("pageSize", String(query.pageSize));
257
614
  }
258
- if (query.sort) {
615
+ if (query.sorts && query.sorts.length > 0) {
616
+ params.set("sorts", JSON.stringify(query.sorts));
617
+ } else if (query.sort) {
259
618
  params.set("sortField", query.sort.field);
260
619
  params.set("sortDirection", query.sort.direction);
261
620
  }
@@ -272,140 +631,22 @@ function serializeUrlState(query, filters, columnWidths, defaultPageSize, enable
272
631
  }
273
632
  return params.toString();
274
633
  }
275
- function renderCell(column, row, isLoading, setConfirmState) {
276
- switch (column.kind) {
277
- case "text": {
278
- const value = row[column.field];
279
- if (column.format) {
280
- return column.format(value);
281
- }
282
- return String(value ?? "");
283
- }
284
- case "date": {
285
- const value = row[column.field];
286
- if (column.format) {
287
- return column.format(value);
288
- }
289
- if (value instanceof Date) {
290
- return value.toLocaleDateString();
291
- }
292
- if (typeof value === "string" || typeof value === "number") {
293
- return new Date(value).toLocaleDateString();
294
- }
295
- return "";
296
- }
297
- case "boolean": {
298
- const value = row[column.field];
299
- if (column.format) {
300
- return column.format(Boolean(value));
301
- }
302
- return value ? "Yes" : "No";
303
- }
304
- case "badge": {
305
- const value = row[column.field];
306
- const valueStr = String(value ?? "");
307
- const mapped = column.map?.[valueStr];
308
- const label = mapped?.label ?? valueStr;
309
- const tone = mapped?.tone ?? "neutral";
310
- return /* @__PURE__ */ jsxRuntime.jsx("span", { className: `rowakit-badge rowakit-badge-${tone}`, children: label });
311
- }
312
- case "number": {
313
- const value = row[column.field];
314
- const numValue = Number(value ?? 0);
315
- if (column.format) {
316
- if (typeof column.format === "function") {
317
- return column.format(numValue, row);
318
- }
319
- return new Intl.NumberFormat(void 0, column.format).format(numValue);
320
- }
321
- return numValue.toLocaleString();
322
- }
323
- case "actions": {
324
- const columnWithActions = column;
325
- if (!Array.isArray(columnWithActions.actions)) {
326
- return null;
327
- }
328
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rowakit-table-actions", children: columnWithActions.actions.map((action) => {
329
- const isDisabled = isLoading || action.disabled === true || typeof action.disabled === "function" && action.disabled(row);
330
- const handleClick = () => {
331
- if (isDisabled || action.loading) {
332
- return;
333
- }
334
- if (action.confirm) {
335
- setConfirmState({ action, row });
336
- } else {
337
- void action.onClick(row);
338
- }
339
- };
340
- return /* @__PURE__ */ jsxRuntime.jsxs(
341
- "button",
342
- {
343
- onClick: handleClick,
344
- disabled: isDisabled || action.loading,
345
- type: "button",
346
- className: "rowakit-button rowakit-button-secondary",
347
- children: [
348
- action.icon && typeof action.icon === "string" ? /* @__PURE__ */ jsxRuntime.jsx("span", { children: action.icon }) : action.icon,
349
- action.label
350
- ]
351
- },
352
- action.id
353
- );
354
- }) });
355
- }
356
- case "custom": {
357
- return column.render(row);
358
- }
359
- default: {
360
- const _exhaustive = column;
361
- return _exhaustive;
362
- }
363
- }
364
- }
365
- function RowaKitTable({
366
- fetcher,
634
+ function useUrlSync({
635
+ syncToUrl,
636
+ enableColumnResizing,
637
+ defaultPageSize,
638
+ pageSizeOptions,
367
639
  columns,
368
- defaultPageSize = 20,
369
- pageSizeOptions = [10, 20, 50],
370
- rowKey,
371
- className = "",
372
- enableFilters = false,
373
- enableColumnResizing = false,
374
- syncToUrl = false,
375
- enableSavedViews = false
640
+ query,
641
+ setQuery,
642
+ filters,
643
+ setFilters,
644
+ columnWidths,
645
+ setColumnWidths
376
646
  }) {
377
- const [dataState, setDataState] = react.useState({
378
- state: "idle",
379
- items: [],
380
- total: 0
381
- });
382
- const [query, setQuery] = react.useState({
383
- page: 1,
384
- pageSize: defaultPageSize
385
- });
386
- const [filters, setFilters] = react.useState({});
387
- const [columnWidths, setColumnWidths] = react.useState({});
388
- const resizeRafRef = react.useRef(null);
389
- const resizePendingRef = react.useRef(null);
390
- const tableRef = react.useRef(null);
391
- const isResizingRef = react.useRef(false);
392
- const lastResizeEndTsRef = react.useRef(0);
393
- const resizingColIdRef = react.useRef(null);
394
647
  const didHydrateUrlRef = react.useRef(false);
395
648
  const didSkipInitialUrlSyncRef = react.useRef(false);
396
649
  const urlSyncDebounceRef = react.useRef(null);
397
- const [savedViews, setSavedViews] = react.useState([]);
398
- const [showSaveViewForm, setShowSaveViewForm] = react.useState(false);
399
- const [saveViewInput, setSaveViewInput] = react.useState("");
400
- const [saveViewError, setSaveViewError] = react.useState("");
401
- const [overwriteConfirmName, setOverwriteConfirmName] = react.useState(null);
402
- react.useEffect(() => {
403
- if (!enableSavedViews) return;
404
- const views = loadSavedViewsFromStorage();
405
- setSavedViews(views);
406
- }, [enableSavedViews]);
407
- const [confirmState, setConfirmState] = react.useState(null);
408
- const requestIdRef = react.useRef(0);
409
650
  react.useEffect(() => {
410
651
  if (!syncToUrl) {
411
652
  didSkipInitialUrlSyncRef.current = false;
@@ -422,7 +663,14 @@ function RowaKitTable({
422
663
  const urlStr = serializeUrlState(query, filters, columnWidths, defaultPageSize, enableColumnResizing);
423
664
  const qs = urlStr ? `?${urlStr}` : "";
424
665
  window.history.replaceState(null, "", `${window.location.pathname}${qs}${window.location.hash}`);
425
- }, [query, filters, syncToUrl, enableColumnResizing, defaultPageSize, columnWidths]);
666
+ }, [
667
+ query,
668
+ filters,
669
+ syncToUrl,
670
+ enableColumnResizing,
671
+ defaultPageSize,
672
+ columnWidths
673
+ ]);
426
674
  react.useEffect(() => {
427
675
  if (!syncToUrl || !enableColumnResizing) return;
428
676
  if (!didSkipInitialUrlSyncRef.current) return;
@@ -441,7 +689,14 @@ function RowaKitTable({
441
689
  urlSyncDebounceRef.current = null;
442
690
  }
443
691
  };
444
- }, [columnWidths, syncToUrl, enableColumnResizing, query, filters, defaultPageSize]);
692
+ }, [
693
+ columnWidths,
694
+ syncToUrl,
695
+ enableColumnResizing,
696
+ query,
697
+ filters,
698
+ defaultPageSize
699
+ ]);
445
700
  react.useEffect(() => {
446
701
  if (!syncToUrl) {
447
702
  didHydrateUrlRef.current = false;
@@ -455,6 +710,7 @@ function RowaKitTable({
455
710
  page: parsed.page,
456
711
  pageSize: parsed.pageSize,
457
712
  sort: parsed.sort,
713
+ sorts: parsed.sorts,
458
714
  filters: parsed.filters
459
715
  });
460
716
  if (parsed.filters) {
@@ -477,256 +733,438 @@ function RowaKitTable({
477
733
  }
478
734
  setColumnWidths(clamped);
479
735
  }
480
- }, [syncToUrl, defaultPageSize, enableColumnResizing, pageSizeOptions, columns]);
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);
481
759
  react.useEffect(() => {
482
- if (!enableFilters) return;
483
- const activeFilters = {};
484
- let hasFilters = false;
485
- for (const [field, value] of Object.entries(filters)) {
486
- if (value !== void 0) {
487
- activeFilters[field] = value;
488
- hasFilters = true;
489
- }
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();
490
772
  }
491
- const filtersToSend = hasFilters ? activeFilters : void 0;
492
- setQuery((prev) => ({
493
- ...prev,
494
- filters: filtersToSend,
495
- page: 1
496
- // Reset page to 1 when filters change
497
- }));
498
- }, [filters, enableFilters]);
499
- react.useEffect(() => {
500
- const currentRequestId = ++requestIdRef.current;
501
- setDataState((prev) => ({ ...prev, state: "loading" }));
502
- fetcher(query).then((result) => {
503
- if (currentRequestId !== requestIdRef.current) {
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?.();
504
782
  return;
505
783
  }
506
- if (result.items.length === 0) {
507
- setDataState({
508
- state: "empty",
509
- items: [],
510
- total: result.total
511
- });
512
- } else {
513
- setDataState({
514
- state: "success",
515
- items: result.items,
516
- total: result.total
517
- });
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
+ }
518
796
  }
519
- }).catch((error) => {
520
- if (currentRequestId !== requestIdRef.current) {
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");
521
890
  return;
522
891
  }
523
- setDataState({
524
- state: "error",
525
- items: [],
526
- total: 0,
527
- error: error instanceof Error ? error.message : "Failed to load data"
528
- });
529
- });
530
- }, [fetcher, query]);
531
- const handleRetry = () => {
532
- setQuery({ ...query });
533
- };
534
- const handlePreviousPage = () => {
535
- if (query.page > 1) {
536
- setQuery((prev) => ({ ...prev, page: prev.page - 1 }));
892
+ openUrl(result.url);
893
+ } catch (e) {
894
+ setError(e instanceof Error ? e.message : "Export failed");
895
+ } finally {
896
+ setIsExporting(false);
537
897
  }
538
898
  };
539
- const handleNextPage = () => {
540
- const totalPages2 = Math.ceil(dataState.total / query.pageSize);
541
- if (query.page < totalPages2) {
542
- setQuery((prev) => ({ ...prev, page: prev.page + 1 }));
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
+ }
936
+ function getRowKey(row, rowKey) {
937
+ if (typeof rowKey === "function") {
938
+ return rowKey(row);
939
+ }
940
+ if (rowKey) {
941
+ return String(row[rowKey]);
942
+ }
943
+ if (row && typeof row === "object" && "id" in row) {
944
+ return String(row.id);
945
+ }
946
+ return String(row);
947
+ }
948
+ function getHeaderLabel(column) {
949
+ return column.header ?? column.id;
950
+ }
951
+ function renderCell(column, row, isLoading, setConfirmState) {
952
+ switch (column.kind) {
953
+ case "text": {
954
+ const value = row[column.field];
955
+ if (column.format) {
956
+ return column.format(value);
957
+ }
958
+ return String(value ?? "");
543
959
  }
544
- };
545
- const handlePageSizeChange = (newPageSize) => {
546
- setQuery({ ...query, pageSize: newPageSize, page: 1 });
547
- };
548
- const handleSort = (field) => {
549
- setQuery((prev) => {
550
- if (prev.sort?.field !== field) {
551
- return { ...prev, sort: { field, direction: "asc" }, page: 1 };
960
+ case "date": {
961
+ const value = row[column.field];
962
+ if (column.format) {
963
+ return column.format(value);
552
964
  }
553
- if (prev.sort.direction === "asc") {
554
- return { ...prev, sort: { field, direction: "desc" }, page: 1 };
965
+ if (value instanceof Date) {
966
+ return value.toLocaleDateString();
967
+ }
968
+ if (typeof value === "string" || typeof value === "number") {
969
+ return new Date(value).toLocaleDateString();
555
970
  }
556
- const { sort: _sort, ...rest } = prev;
557
- return { ...rest, page: 1 };
558
- });
559
- };
560
- const getSortIndicator = (field) => {
561
- if (!query.sort || query.sort.field !== field) {
562
971
  return "";
563
972
  }
564
- return query.sort.direction === "asc" ? " \u2191" : " \u2193";
565
- };
566
- const scheduleColumnWidthUpdate = (colId, width) => {
567
- resizePendingRef.current = { colId, width };
568
- if (resizeRafRef.current != null) return;
569
- resizeRafRef.current = requestAnimationFrame(() => {
570
- resizeRafRef.current = null;
571
- const pending = resizePendingRef.current;
572
- if (!pending) return;
573
- handleColumnResize(pending.colId, pending.width);
574
- });
575
- };
576
- const handleColumnResize = (columnId, newWidth) => {
577
- const minWidth = columns.find((c) => c.id === columnId)?.minWidth ?? 80;
578
- const maxWidth = columns.find((c) => c.id === columnId)?.maxWidth;
579
- let finalWidth = Math.max(minWidth, newWidth);
580
- if (maxWidth) {
581
- finalWidth = Math.min(finalWidth, maxWidth);
582
- }
583
- if (columnWidths[columnId] === finalWidth) {
584
- return;
973
+ case "boolean": {
974
+ const value = row[column.field];
975
+ if (column.format) {
976
+ return column.format(Boolean(value));
977
+ }
978
+ return value ? "Yes" : "No";
585
979
  }
586
- setColumnWidths((prev) => ({
587
- ...prev,
588
- [columnId]: finalWidth
589
- }));
590
- };
591
- const autoFitColumnWidth = (columnId) => {
592
- const tableEl = tableRef.current;
593
- if (!tableEl) return;
594
- const th = tableEl.querySelector(`th[data-col-id="${columnId}"]`);
595
- if (!th) return;
596
- const tds = Array.from(
597
- tableEl.querySelectorAll(`td[data-col-id="${columnId}"]`)
598
- );
599
- const headerW = th.scrollWidth;
600
- const cellsMaxW = tds.reduce((max, td) => Math.max(max, td.scrollWidth), 0);
601
- const padding = 24;
602
- const raw = Math.max(headerW, cellsMaxW) + padding;
603
- const colDef = columns.find((c) => c.id === columnId);
604
- const minW = colDef?.minWidth ?? 80;
605
- const maxW = colDef?.maxWidth ?? 600;
606
- const finalW = Math.max(minW, Math.min(raw, maxW));
607
- setColumnWidths((prev) => ({ ...prev, [columnId]: finalW }));
608
- };
609
- const startColumnResize = (e, columnId) => {
610
- e.preventDefault();
611
- e.stopPropagation();
612
- if (e.detail === 2) {
613
- autoFitColumnWidth(columnId);
614
- return;
980
+ case "badge": {
981
+ const value = row[column.field];
982
+ const valueStr = String(value ?? "");
983
+ const mapped = column.map?.[valueStr];
984
+ const label = mapped?.label ?? valueStr;
985
+ const tone = mapped?.tone ?? "neutral";
986
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { className: `rowakit-badge rowakit-badge-${tone}`, children: label });
615
987
  }
616
- if (e.pointerType === "mouse" && e.buttons !== 1) {
617
- return;
988
+ case "number": {
989
+ const value = row[column.field];
990
+ const numValue = Number(value ?? 0);
991
+ if (column.format) {
992
+ if (typeof column.format === "function") {
993
+ return column.format(numValue, row);
994
+ }
995
+ return new Intl.NumberFormat(void 0, column.format).format(numValue);
996
+ }
997
+ return numValue.toLocaleString();
618
998
  }
619
- const target = e.currentTarget;
620
- const pointerId = e.pointerId;
621
- try {
622
- target.setPointerCapture(pointerId);
623
- } catch {
999
+ case "actions": {
1000
+ const columnWithActions = column;
1001
+ if (!Array.isArray(columnWithActions.actions)) {
1002
+ return null;
1003
+ }
1004
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rowakit-table-actions", children: columnWithActions.actions.map((action) => {
1005
+ const isDisabled = isLoading || action.disabled === true || typeof action.disabled === "function" && action.disabled(row);
1006
+ const handleClick = () => {
1007
+ if (isDisabled || action.loading) {
1008
+ return;
1009
+ }
1010
+ if (action.confirm) {
1011
+ setConfirmState({ action, row });
1012
+ } else {
1013
+ void action.onClick(row);
1014
+ }
1015
+ };
1016
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1017
+ "button",
1018
+ {
1019
+ onClick: handleClick,
1020
+ disabled: isDisabled || action.loading,
1021
+ type: "button",
1022
+ className: "rowakit-button rowakit-button-secondary",
1023
+ children: [
1024
+ action.icon && typeof action.icon === "string" ? /* @__PURE__ */ jsxRuntime.jsx("span", { children: action.icon }) : action.icon,
1025
+ action.label
1026
+ ]
1027
+ },
1028
+ action.id
1029
+ );
1030
+ }) });
624
1031
  }
625
- isResizingRef.current = true;
626
- resizingColIdRef.current = columnId;
627
- const startX = e.clientX;
628
- const th = target.parentElement;
629
- let startWidth = columnWidths[columnId] ?? th.offsetWidth;
630
- const MIN_DRAG_WIDTH = 80;
631
- if (startWidth < MIN_DRAG_WIDTH) {
632
- const nextTh = th.nextElementSibling;
633
- if (nextTh && nextTh.offsetWidth >= 50) {
634
- startWidth = nextTh.offsetWidth;
635
- } else {
636
- startWidth = 100;
637
- }
1032
+ case "custom": {
1033
+ return column.render(row);
638
1034
  }
639
- document.body.classList.add("rowakit-resizing");
640
- const handlePointerMove = (moveEvent) => {
641
- const delta = moveEvent.clientX - startX;
642
- const newWidth = startWidth + delta;
643
- scheduleColumnWidthUpdate(columnId, newWidth);
644
- };
645
- const cleanupResize = () => {
646
- target.removeEventListener("pointermove", handlePointerMove);
647
- target.removeEventListener("pointerup", handlePointerUp);
648
- target.removeEventListener("pointercancel", handlePointerCancel);
649
- document.body.classList.remove("rowakit-resizing");
650
- isResizingRef.current = false;
651
- resizingColIdRef.current = null;
652
- lastResizeEndTsRef.current = Date.now();
653
- try {
654
- target.releasePointerCapture(pointerId);
655
- } catch {
656
- }
657
- };
658
- const handlePointerUp = () => {
659
- cleanupResize();
660
- };
661
- const handlePointerCancel = () => {
662
- cleanupResize();
663
- };
664
- target.addEventListener("pointermove", handlePointerMove);
665
- target.addEventListener("pointerup", handlePointerUp);
666
- target.addEventListener("pointercancel", handlePointerCancel);
667
- };
668
- const handleColumnResizeDoubleClick = (e, columnId) => {
669
- e.preventDefault();
670
- e.stopPropagation();
671
- autoFitColumnWidth(columnId);
672
- };
673
- const saveCurrentView = (name) => {
674
- const viewState = {
675
- page: query.page,
676
- pageSize: query.pageSize,
677
- sort: query.sort,
678
- filters: query.filters,
679
- columnWidths: enableColumnResizing ? columnWidths : void 0
680
- };
681
- setSavedViews((prev) => {
682
- const filtered = prev.filter((v) => v.name !== name);
683
- return [...filtered, { name, state: viewState }];
684
- });
685
- if (typeof window !== "undefined" && window.localStorage) {
686
- try {
687
- localStorage.setItem(`rowakit-view-${name}`, JSON.stringify(viewState));
688
- const index = getSavedViewsIndex();
689
- const filtered = index.filter((v) => v.name !== name);
690
- filtered.push({ name, updatedAt: Date.now() });
691
- setSavedViewsIndex(filtered);
692
- } catch {
1035
+ default: {
1036
+ const _exhaustive = column;
1037
+ return _exhaustive;
1038
+ }
1039
+ }
1040
+ }
1041
+ function RowaKitTable({
1042
+ fetcher,
1043
+ columns,
1044
+ defaultPageSize = 20,
1045
+ pageSizeOptions = [10, 20, 50],
1046
+ rowKey,
1047
+ className = "",
1048
+ enableFilters = false,
1049
+ enableColumnResizing = false,
1050
+ syncToUrl = false,
1051
+ enableSavedViews = false,
1052
+ enableRowSelection = false,
1053
+ onSelectionChange,
1054
+ bulkActions,
1055
+ exporter
1056
+ }) {
1057
+ const [query, setQuery] = react.useState({
1058
+ page: 1,
1059
+ pageSize: defaultPageSize
1060
+ });
1061
+ const [filters, setFilters] = react.useState({});
1062
+ const [confirmState, setConfirmState] = react.useState(null);
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);
1095
+ react.useEffect(() => {
1096
+ if (!enableRowSelection) return;
1097
+ setSelectedKeys(clearSelection());
1098
+ }, [enableRowSelection, query.page, dataState.items]);
1099
+ react.useEffect(() => {
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
+ });
1137
+ react.useEffect(() => {
1138
+ if (!enableFilters) return;
1139
+ const activeFilters = {};
1140
+ let hasFilters = false;
1141
+ for (const [field, value] of Object.entries(filters)) {
1142
+ if (value !== void 0) {
1143
+ activeFilters[field] = value;
1144
+ hasFilters = true;
693
1145
  }
694
1146
  }
695
- };
696
- const loadSavedView = (name) => {
697
- const view = savedViews.find((v) => v.name === name);
698
- if (!view) return;
699
- const { state } = view;
700
- setQuery({
701
- page: state.page,
702
- pageSize: state.pageSize,
703
- sort: state.sort,
704
- filters: state.filters
705
- });
706
- setFilters(state.filters ?? {});
707
- if (state.columnWidths && enableColumnResizing) {
708
- setColumnWidths(state.columnWidths);
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]);
1155
+ const handlePreviousPage = () => {
1156
+ if (query.page > 1) {
1157
+ setQuery((prev) => ({ ...prev, page: prev.page - 1 }));
709
1158
  }
710
1159
  };
711
- const deleteSavedView = (name) => {
712
- setSavedViews((prev) => prev.filter((v) => v.name !== name));
713
- if (typeof window !== "undefined" && window.localStorage) {
714
- try {
715
- localStorage.removeItem(`rowakit-view-${name}`);
716
- const index = getSavedViewsIndex();
717
- const filtered = index.filter((v) => v.name !== name);
718
- setSavedViewsIndex(filtered);
719
- } catch {
720
- }
1160
+ const handleNextPage = () => {
1161
+ const totalPages2 = Math.ceil(dataState.total / query.pageSize);
1162
+ if (query.page < totalPages2) {
1163
+ setQuery((prev) => ({ ...prev, page: prev.page + 1 }));
721
1164
  }
722
1165
  };
723
- const resetTableState = () => {
724
- setQuery({
725
- page: 1,
726
- pageSize: defaultPageSize
727
- });
728
- setFilters({});
729
- setColumnWidths({});
1166
+ const handlePageSizeChange = (newPageSize) => {
1167
+ setQuery({ ...query, pageSize: newPageSize, page: 1 });
730
1168
  };
731
1169
  const transformFilterValueForColumn = (column, value) => {
732
1170
  if (!value || column?.kind !== "number") {
@@ -772,12 +1210,10 @@ function RowaKitTable({
772
1210
  const handleClearAllFilters = () => {
773
1211
  setFilters({});
774
1212
  };
775
- const isLoading = dataState.state === "loading";
776
- const isError = dataState.state === "error";
777
- const isEmpty = dataState.state === "empty";
778
1213
  const totalPages = Math.ceil(dataState.total / query.pageSize);
779
1214
  const canGoPrevious = query.page > 1 && !isLoading;
780
1215
  const canGoNext = query.page < totalPages && !isLoading;
1216
+ const tableColumnCount = columns.length + (enableRowSelection ? 1 : 0);
781
1217
  const hasActiveFilters = enableFilters && Object.values(filters).some((v) => v !== void 0);
782
1218
  const containerClass = [
783
1219
  "rowakit-table",
@@ -785,16 +1221,29 @@ function RowaKitTable({
785
1221
  className
786
1222
  ].filter(Boolean).join(" ");
787
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 }),
788
1242
  enableSavedViews && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rowakit-saved-views-group", children: [
789
1243
  !showSaveViewForm ? /* @__PURE__ */ jsxRuntime.jsx(
790
1244
  "button",
791
1245
  {
792
- onClick: () => {
793
- setShowSaveViewForm(true);
794
- setSaveViewInput("");
795
- setSaveViewError("");
796
- setOverwriteConfirmName(null);
797
- },
1246
+ onClick: openSaveViewForm,
798
1247
  className: "rowakit-saved-view-button",
799
1248
  type: "button",
800
1249
  children: "Save View"
@@ -808,13 +1257,7 @@ function RowaKitTable({
808
1257
  /* @__PURE__ */ jsxRuntime.jsx(
809
1258
  "button",
810
1259
  {
811
- onClick: () => {
812
- saveCurrentView(overwriteConfirmName);
813
- setShowSaveViewForm(false);
814
- setSaveViewInput("");
815
- setSaveViewError("");
816
- setOverwriteConfirmName(null);
817
- },
1260
+ onClick: confirmOverwrite,
818
1261
  className: "rowakit-saved-view-button",
819
1262
  type: "button",
820
1263
  children: "Overwrite"
@@ -823,9 +1266,7 @@ function RowaKitTable({
823
1266
  /* @__PURE__ */ jsxRuntime.jsx(
824
1267
  "button",
825
1268
  {
826
- onClick: () => {
827
- setOverwriteConfirmName(null);
828
- },
1269
+ onClick: cancelOverwrite,
829
1270
  className: "rowakit-saved-view-button",
830
1271
  type: "button",
831
1272
  children: "Cancel"
@@ -837,27 +1278,8 @@ function RowaKitTable({
837
1278
  {
838
1279
  type: "text",
839
1280
  value: saveViewInput,
840
- onChange: (e) => {
841
- setSaveViewInput(e.target.value);
842
- setSaveViewError("");
843
- },
844
- onKeyDown: (e) => {
845
- if (e.key === "Enter") {
846
- const validation = validateViewName(saveViewInput);
847
- if (!validation.valid) {
848
- setSaveViewError(validation.error || "Invalid name");
849
- return;
850
- }
851
- if (savedViews.some((v) => v.name === saveViewInput.trim())) {
852
- setOverwriteConfirmName(saveViewInput.trim());
853
- } else {
854
- saveCurrentView(saveViewInput.trim());
855
- setShowSaveViewForm(false);
856
- setSaveViewInput("");
857
- setSaveViewError("");
858
- }
859
- }
860
- },
1281
+ onChange: onSaveViewInputChange,
1282
+ onKeyDown: onSaveViewInputKeyDown,
861
1283
  placeholder: "Enter view name...",
862
1284
  className: "rowakit-save-view-input"
863
1285
  }
@@ -866,21 +1288,7 @@ function RowaKitTable({
866
1288
  /* @__PURE__ */ jsxRuntime.jsx(
867
1289
  "button",
868
1290
  {
869
- onClick: () => {
870
- const validation = validateViewName(saveViewInput);
871
- if (!validation.valid) {
872
- setSaveViewError(validation.error || "Invalid name");
873
- return;
874
- }
875
- if (savedViews.some((v) => v.name === saveViewInput.trim())) {
876
- setOverwriteConfirmName(saveViewInput.trim());
877
- } else {
878
- saveCurrentView(saveViewInput.trim());
879
- setShowSaveViewForm(false);
880
- setSaveViewInput("");
881
- setSaveViewError("");
882
- }
883
- },
1291
+ onClick: attemptSave,
884
1292
  className: "rowakit-saved-view-button",
885
1293
  type: "button",
886
1294
  children: "Save"
@@ -889,11 +1297,7 @@ function RowaKitTable({
889
1297
  /* @__PURE__ */ jsxRuntime.jsx(
890
1298
  "button",
891
1299
  {
892
- onClick: () => {
893
- setShowSaveViewForm(false);
894
- setSaveViewInput("");
895
- setSaveViewError("");
896
- },
1300
+ onClick: cancelSaveViewForm,
897
1301
  className: "rowakit-saved-view-button",
898
1302
  type: "button",
899
1303
  children: "Cancel"
@@ -942,167 +1346,143 @@ function RowaKitTable({
942
1346
  ) }),
943
1347
  /* @__PURE__ */ jsxRuntime.jsxs("table", { ref: tableRef, children: [
944
1348
  /* @__PURE__ */ jsxRuntime.jsxs("thead", { children: [
945
- /* @__PURE__ */ jsxRuntime.jsx("tr", { children: columns.map((column) => {
946
- const isSortable = column.kind !== "actions" && (column.kind === "custom" ? false : column.sortable === true);
947
- const field = column.kind === "actions" ? "" : column.kind === "custom" ? column.field : column.field;
948
- const isResizable = enableColumnResizing && column.kind !== "actions";
949
- const actualWidth = columnWidths[column.id] ?? column.width;
950
- return /* @__PURE__ */ jsxRuntime.jsxs(
951
- "th",
1349
+ /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
1350
+ enableRowSelection && /* @__PURE__ */ jsxRuntime.jsx(
1351
+ RowSelectionHeaderCell,
952
1352
  {
953
- "data-col-id": column.id,
954
- onClick: isSortable ? () => {
955
- if (isResizingRef.current) return;
956
- if (Date.now() - lastResizeEndTsRef.current < 150) return;
957
- handleSort(String(field));
958
- } : void 0,
959
- role: isSortable ? "button" : void 0,
960
- tabIndex: isSortable ? 0 : void 0,
961
- onKeyDown: isSortable ? (e) => {
962
- if (e.key === "Enter" || e.key === " ") {
963
- e.preventDefault();
964
- handleSort(String(field));
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());
965
1361
  }
966
- } : void 0,
967
- "aria-sort": isSortable && query.sort?.field === String(field) ? query.sort.direction === "asc" ? "ascending" : "descending" : void 0,
968
- style: {
969
- width: actualWidth != null ? `${actualWidth}px` : void 0,
970
- textAlign: column.align,
971
- position: isResizable ? "relative" : void 0
972
- },
973
- className: [
974
- column.truncate ? "rowakit-cell-truncate" : "",
975
- resizingColIdRef.current === column.id ? "resizing" : ""
976
- // PRD-01
977
- ].filter(Boolean).join(" ") || void 0,
978
- children: [
979
- getHeaderLabel(column),
980
- isSortable && getSortIndicator(String(field)),
981
- isResizable && /* @__PURE__ */ jsxRuntime.jsx(
982
- "div",
983
- {
984
- className: "rowakit-column-resize-handle",
985
- onPointerDown: (e) => startColumnResize(e, column.id),
986
- onDoubleClick: (e) => handleColumnResizeDoubleClick(e, column.id),
987
- title: "Drag to resize | Double-click to auto-fit content"
988
- }
989
- )
990
- ]
991
- },
992
- column.id
993
- );
994
- }) }),
995
- enableFilters && /* @__PURE__ */ jsxRuntime.jsx("tr", { className: "rowakit-table-filter-row", children: columns.map((column) => {
996
- const field = column.kind === "actions" || column.kind === "custom" ? "" : String(column.field);
997
- const canFilter = field && column.kind !== "actions";
998
- if (!canFilter) {
999
- return /* @__PURE__ */ jsxRuntime.jsx("th", {}, column.id);
1000
- }
1001
- const filterValue = filters[field];
1002
- if (column.kind === "badge") {
1003
- const options = column.map ? Object.keys(column.map) : [];
1004
- return /* @__PURE__ */ jsxRuntime.jsx("th", { children: /* @__PURE__ */ jsxRuntime.jsxs(
1005
- "select",
1006
- {
1007
- className: "rowakit-filter-select",
1008
- value: filterValue?.op === "equals" ? String(filterValue.value ?? "") : "",
1009
- onChange: (e) => {
1010
- const value = e.target.value;
1011
- if (value === "") {
1012
- handleClearFilter(field);
1013
- } else {
1014
- handleFilterChange(field, { op: "equals", value });
1015
- }
1016
- },
1017
- children: [
1018
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "", children: "All" }),
1019
- options.map((opt) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: opt, children: opt }, opt))
1020
- ]
1021
1362
  }
1022
- ) }, column.id);
1023
- }
1024
- if (column.kind === "boolean") {
1025
- return /* @__PURE__ */ jsxRuntime.jsx("th", { children: /* @__PURE__ */ jsxRuntime.jsxs(
1026
- "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",
1027
1372
  {
1028
- className: "rowakit-filter-select",
1029
- value: filterValue?.op === "equals" && typeof filterValue.value === "boolean" ? String(filterValue.value) : "",
1030
- onChange: (e) => {
1031
- const value = e.target.value;
1032
- if (value === "") {
1033
- handleClearFilter(field);
1034
- } else {
1035
- handleFilterChange(field, { op: "equals", value: value === "true" });
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);
1036
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
1037
1394
  },
1395
+ className: [
1396
+ column.truncate ? "rowakit-cell-truncate" : "",
1397
+ resizingColIdRef.current === column.id ? "resizing" : ""
1398
+ // PRD-01
1399
+ ].filter(Boolean).join(" ") || void 0,
1038
1400
  children: [
1039
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "", children: "All" }),
1040
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "true", children: "True" }),
1041
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "false", children: "False" })
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
+ )
1042
1412
  ]
1043
- }
1044
- ) }, column.id);
1045
- }
1046
- if (column.kind === "date") {
1047
- const fromValue = filterValue?.op === "range" ? filterValue.value.from ?? "" : "";
1048
- const toValue = filterValue?.op === "range" ? filterValue.value.to ?? "" : "";
1049
- return /* @__PURE__ */ jsxRuntime.jsx("th", { children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rowakit-filter-date-range", children: [
1050
- /* @__PURE__ */ jsxRuntime.jsx(
1051
- "input",
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",
1052
1431
  {
1053
- type: "date",
1054
- className: "rowakit-filter-input",
1055
- placeholder: "From",
1056
- value: fromValue,
1432
+ className: "rowakit-filter-select",
1433
+ value: filterValue?.op === "equals" ? String(filterValue.value ?? "") : "",
1057
1434
  onChange: (e) => {
1058
- const from = e.target.value || void 0;
1059
- const to = toValue || void 0;
1060
- if (!from && !to) {
1435
+ const value = e.target.value;
1436
+ if (value === "") {
1061
1437
  handleClearFilter(field);
1062
1438
  } else {
1063
- handleFilterChange(field, { op: "range", value: { from, to } });
1439
+ handleFilterChange(field, { op: "equals", value });
1064
1440
  }
1065
- }
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
+ ]
1066
1446
  }
1067
- ),
1068
- /* @__PURE__ */ jsxRuntime.jsx(
1069
- "input",
1447
+ ) }, column.id);
1448
+ }
1449
+ if (column.kind === "boolean") {
1450
+ return /* @__PURE__ */ jsxRuntime.jsx("th", { children: /* @__PURE__ */ jsxRuntime.jsxs(
1451
+ "select",
1070
1452
  {
1071
- type: "date",
1072
- className: "rowakit-filter-input",
1073
- placeholder: "To",
1074
- value: toValue,
1453
+ className: "rowakit-filter-select",
1454
+ value: filterValue?.op === "equals" && typeof filterValue.value === "boolean" ? String(filterValue.value) : "",
1075
1455
  onChange: (e) => {
1076
- const to = e.target.value || void 0;
1077
- const from = fromValue || void 0;
1078
- if (!from && !to) {
1456
+ const value = e.target.value;
1457
+ if (value === "") {
1079
1458
  handleClearFilter(field);
1080
1459
  } else {
1081
- handleFilterChange(field, { op: "range", value: { from, to } });
1460
+ handleFilterChange(field, { op: "equals", value: value === "true" });
1082
1461
  }
1083
- }
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
+ ]
1084
1468
  }
1085
- )
1086
- ] }) }, column.id);
1087
- }
1088
- const isNumberColumn = column.kind === "number";
1089
- if (isNumberColumn) {
1090
- const fromValue = filterValue?.op === "range" ? String(filterValue.value.from ?? "") : filterValue?.op === "equals" && typeof filterValue.value === "number" ? String(filterValue.value) : "";
1091
- const toValue = filterValue?.op === "range" ? String(filterValue.value.to ?? "") : "";
1092
- const showRangeUI = !filterValue || filterValue.op === "range";
1093
- if (showRangeUI) {
1094
- return /* @__PURE__ */ jsxRuntime.jsx("th", { children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rowakit-filter-number-range", children: [
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: [
1095
1475
  /* @__PURE__ */ jsxRuntime.jsx(
1096
1476
  "input",
1097
1477
  {
1098
- type: "number",
1478
+ type: "date",
1099
1479
  className: "rowakit-filter-input",
1100
- placeholder: "Min",
1480
+ placeholder: "From",
1101
1481
  value: fromValue,
1102
1482
  onChange: (e) => {
1103
- const from = e.target.value ? Number(e.target.value) : void 0;
1104
- const to = toValue ? Number(toValue) : void 0;
1105
- if (from === void 0 && to === void 0) {
1483
+ const from = e.target.value || void 0;
1484
+ const to = toValue || void 0;
1485
+ if (!from && !to) {
1106
1486
  handleClearFilter(field);
1107
1487
  } else {
1108
1488
  handleFilterChange(field, { op: "range", value: { from, to } });
@@ -1113,14 +1493,14 @@ function RowaKitTable({
1113
1493
  /* @__PURE__ */ jsxRuntime.jsx(
1114
1494
  "input",
1115
1495
  {
1116
- type: "number",
1496
+ type: "date",
1117
1497
  className: "rowakit-filter-input",
1118
- placeholder: "Max",
1498
+ placeholder: "To",
1119
1499
  value: toValue,
1120
1500
  onChange: (e) => {
1121
- const to = e.target.value ? Number(e.target.value) : void 0;
1122
- const from = fromValue ? Number(fromValue) : void 0;
1123
- if (from === void 0 && to === void 0) {
1501
+ const to = e.target.value || void 0;
1502
+ const from = fromValue || void 0;
1503
+ if (!from && !to) {
1124
1504
  handleClearFilter(field);
1125
1505
  } else {
1126
1506
  handleFilterChange(field, { op: "range", value: { from, to } });
@@ -1130,39 +1510,85 @@ function RowaKitTable({
1130
1510
  )
1131
1511
  ] }) }, column.id);
1132
1512
  }
1133
- }
1134
- return /* @__PURE__ */ jsxRuntime.jsx("th", { children: /* @__PURE__ */ jsxRuntime.jsx(
1135
- "input",
1136
- {
1137
- type: isNumberColumn ? "number" : "text",
1138
- className: "rowakit-filter-input",
1139
- placeholder: `Filter ${getHeaderLabel(column)}...`,
1140
- value: filterValue?.op === "contains" ? filterValue.value : filterValue?.op === "equals" && typeof filterValue.value === "string" ? filterValue.value : filterValue?.op === "equals" && typeof filterValue.value === "number" ? String(filterValue.value) : "",
1141
- onChange: (e) => {
1142
- const rawValue = e.target.value;
1143
- if (rawValue === "") {
1144
- handleClearFilter(field);
1145
- } else if (isNumberColumn) {
1146
- const numValue = Number(rawValue);
1147
- if (!isNaN(numValue)) {
1148
- handleFilterChange(field, { op: "equals", value: numValue });
1149
- } else {
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 === "") {
1150
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 });
1151
1579
  }
1152
- } else {
1153
- handleFilterChange(field, { op: "contains", value: rawValue });
1154
1580
  }
1155
1581
  }
1156
- }
1157
- ) }, column.id);
1158
- }) })
1582
+ ) }, column.id);
1583
+ })
1584
+ ] })
1159
1585
  ] }),
1160
1586
  /* @__PURE__ */ jsxRuntime.jsxs("tbody", { children: [
1161
- 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: [
1162
1588
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rowakit-table-loading-spinner" }),
1163
1589
  /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Loading..." })
1164
1590
  ] }) }),
1165
- 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: [
1166
1592
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rowakit-table-error-message", children: dataState.error ?? "An error occurred" }),
1167
1593
  /* @__PURE__ */ jsxRuntime.jsx(
1168
1594
  "button",
@@ -1174,29 +1600,42 @@ function RowaKitTable({
1174
1600
  }
1175
1601
  )
1176
1602
  ] }) }),
1177
- 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" }) }),
1178
1604
  dataState.state === "success" && dataState.items.map((row) => {
1179
1605
  const key = getRowKey(row, rowKey);
1180
- return /* @__PURE__ */ jsxRuntime.jsx("tr", { children: columns.map((column) => {
1181
- const cellClass = [
1182
- column.kind === "number" ? "rowakit-cell-number" : "",
1183
- column.truncate ? "rowakit-cell-truncate" : ""
1184
- ].filter(Boolean).join(" ") || void 0;
1185
- const actualWidth = columnWidths[column.id] ?? column.width;
1186
- return /* @__PURE__ */ jsxRuntime.jsx(
1187
- "td",
1606
+ return /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
1607
+ enableRowSelection && /* @__PURE__ */ jsxRuntime.jsx(
1608
+ RowSelectionCell,
1188
1609
  {
1189
- "data-col-id": column.id,
1190
- className: cellClass,
1191
- style: {
1192
- width: actualWidth != null ? `${actualWidth}px` : void 0,
1193
- textAlign: column.align || (column.kind === "number" ? "right" : void 0)
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)
1194
1634
  },
1195
- children: renderCell(column, row, isLoading, setConfirmState)
1196
- },
1197
- column.id
1198
- );
1199
- }) }, key);
1635
+ column.id
1636
+ );
1637
+ })
1638
+ ] }, key);
1200
1639
  })
1201
1640
  ] })
1202
1641
  ] }),
@@ -1251,6 +1690,7 @@ function RowaKitTable({
1251
1690
  confirmState && /* @__PURE__ */ jsxRuntime.jsx(
1252
1691
  "div",
1253
1692
  {
1693
+ ref: confirmModalRef,
1254
1694
  className: "rowakit-modal-backdrop",
1255
1695
  onClick: () => setConfirmState(null),
1256
1696
  role: "dialog",
@@ -1288,13 +1728,51 @@ function RowaKitTable({
1288
1728
  ] })
1289
1729
  ] })
1290
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
+ }
1291
1769
  )
1292
1770
  ] });
1293
1771
  }
1294
1772
  var SmartTable = RowaKitTable;
1295
1773
 
1296
1774
  // src/index.ts
1297
- var VERSION = "0.4.0";
1775
+ var VERSION = "0.5.0" ;
1298
1776
 
1299
1777
  exports.RowaKitTable = RowaKitTable;
1300
1778
  exports.SmartTable = SmartTable;