@rowakit/table 0.2.2 → 0.3.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
@@ -167,7 +167,11 @@ function renderCell(column, row, isLoading, setConfirmState) {
167
167
  return numValue.toLocaleString();
168
168
  }
169
169
  case "actions": {
170
- return /* @__PURE__ */ jsx("div", { className: "rowakit-table-actions", children: column.actions.map((action) => {
170
+ const columnWithActions = column;
171
+ if (!Array.isArray(columnWithActions.actions)) {
172
+ return null;
173
+ }
174
+ return /* @__PURE__ */ jsx("div", { className: "rowakit-table-actions", children: columnWithActions.actions.map((action) => {
171
175
  const isDisabled = isLoading || action.disabled === true || typeof action.disabled === "function" && action.disabled(row);
172
176
  const handleClick = () => {
173
177
  if (isDisabled || action.loading) {
@@ -211,7 +215,10 @@ function RowaKitTable({
211
215
  pageSizeOptions = [10, 20, 50],
212
216
  rowKey,
213
217
  className = "",
214
- enableFilters = false
218
+ enableFilters = false,
219
+ enableColumnResizing = false,
220
+ syncToUrl = false,
221
+ enableSavedViews = false
215
222
  }) {
216
223
  const [dataState, setDataState] = useState({
217
224
  state: "idle",
@@ -223,8 +230,64 @@ function RowaKitTable({
223
230
  pageSize: defaultPageSize
224
231
  });
225
232
  const [filters, setFilters] = useState({});
233
+ const [columnWidths, setColumnWidths] = useState({});
234
+ const resizeRafRef = useRef(null);
235
+ const resizePendingRef = useRef(null);
236
+ const tableRef = useRef(null);
237
+ const [savedViews, setSavedViews] = useState([]);
226
238
  const [confirmState, setConfirmState] = useState(null);
227
239
  const requestIdRef = useRef(0);
240
+ useEffect(() => {
241
+ if (!syncToUrl) return;
242
+ const params = new URLSearchParams();
243
+ params.set("page", String(query.page));
244
+ params.set("pageSize", String(query.pageSize));
245
+ if (query.sort) {
246
+ params.set("sortField", query.sort.field);
247
+ params.set("sortDirection", query.sort.direction);
248
+ }
249
+ if (query.filters && Object.keys(query.filters).length > 0) {
250
+ params.set("filters", JSON.stringify(query.filters));
251
+ }
252
+ if (enableColumnResizing && Object.keys(columnWidths).length > 0) {
253
+ params.set("columnWidths", JSON.stringify(columnWidths));
254
+ }
255
+ window.history.replaceState(null, "", `?${params.toString()}`);
256
+ }, [query, columnWidths, syncToUrl, enableColumnResizing]);
257
+ useEffect(() => {
258
+ if (!syncToUrl) return;
259
+ const params = new URLSearchParams(window.location.search);
260
+ const page = parseInt(params.get("page") ?? "1", 10);
261
+ const pageSize = parseInt(params.get("pageSize") ?? String(defaultPageSize), 10);
262
+ const sortField = params.get("sortField");
263
+ const sortDirection = params.get("sortDirection");
264
+ const filtersStr = params.get("filters");
265
+ const columnWidthsStr = params.get("columnWidths");
266
+ const newQuery = {
267
+ page: Math.max(1, page),
268
+ pageSize: Math.max(1, pageSize)
269
+ };
270
+ if (sortField && sortDirection) {
271
+ newQuery.sort = { field: sortField, direction: sortDirection };
272
+ }
273
+ if (filtersStr) {
274
+ try {
275
+ const parsedFilters = JSON.parse(filtersStr);
276
+ if (parsedFilters && typeof parsedFilters === "object") {
277
+ setFilters(parsedFilters);
278
+ newQuery.filters = parsedFilters;
279
+ }
280
+ } catch {
281
+ }
282
+ }
283
+ if (enableColumnResizing && columnWidthsStr) {
284
+ try {
285
+ setColumnWidths(JSON.parse(columnWidthsStr));
286
+ } catch {
287
+ }
288
+ }
289
+ setQuery(newQuery);
290
+ }, [syncToUrl, defaultPageSize, enableColumnResizing]);
228
291
  useEffect(() => {
229
292
  if (!enableFilters) return;
230
293
  const activeFilters = {};
@@ -310,10 +373,154 @@ function RowaKitTable({
310
373
  }
311
374
  return query.sort.direction === "asc" ? " \u2191" : " \u2193";
312
375
  };
376
+ const scheduleColumnWidthUpdate = (colId, width) => {
377
+ resizePendingRef.current = { colId, width };
378
+ if (resizeRafRef.current != null) return;
379
+ resizeRafRef.current = requestAnimationFrame(() => {
380
+ resizeRafRef.current = null;
381
+ const pending = resizePendingRef.current;
382
+ if (!pending) return;
383
+ handleColumnResize(pending.colId, pending.width);
384
+ });
385
+ };
386
+ const handleColumnResize = (columnId, newWidth) => {
387
+ const minWidth = columns.find((c) => c.id === columnId)?.minWidth ?? 80;
388
+ const maxWidth = columns.find((c) => c.id === columnId)?.maxWidth;
389
+ let finalWidth = Math.max(minWidth, newWidth);
390
+ if (maxWidth) {
391
+ finalWidth = Math.min(finalWidth, maxWidth);
392
+ }
393
+ setColumnWidths((prev) => ({
394
+ ...prev,
395
+ [columnId]: finalWidth
396
+ }));
397
+ };
398
+ const startColumnResize = (e, columnId) => {
399
+ e.preventDefault();
400
+ const startX = e.clientX;
401
+ const th = e.currentTarget.parentElement;
402
+ let startWidth = columnWidths[columnId] ?? th.offsetWidth;
403
+ const MIN_DRAG_WIDTH = 80;
404
+ if (startWidth < MIN_DRAG_WIDTH) {
405
+ const nextTh = th.nextElementSibling;
406
+ if (nextTh && nextTh.offsetWidth >= 50) {
407
+ startWidth = nextTh.offsetWidth;
408
+ } else {
409
+ startWidth = 100;
410
+ }
411
+ }
412
+ document.body.classList.add("rowakit-resizing");
413
+ const handleMouseMove = (moveEvent) => {
414
+ const delta = moveEvent.clientX - startX;
415
+ const newWidth = startWidth + delta;
416
+ scheduleColumnWidthUpdate(columnId, newWidth);
417
+ };
418
+ const handleMouseUp = () => {
419
+ document.removeEventListener("mousemove", handleMouseMove);
420
+ document.removeEventListener("mouseup", handleMouseUp);
421
+ document.body.classList.remove("rowakit-resizing");
422
+ };
423
+ document.addEventListener("mousemove", handleMouseMove);
424
+ document.addEventListener("mouseup", handleMouseUp);
425
+ };
426
+ const handleColumnResizeDoubleClick = (columnId) => {
427
+ const tableEl = tableRef.current;
428
+ if (!tableEl) return;
429
+ const th = tableEl.querySelector(`th[data-col-id="${columnId}"]`);
430
+ if (!th) return;
431
+ const tds = Array.from(tableEl.querySelectorAll(`td[data-col-id="${columnId}"]`));
432
+ const headerW = th.scrollWidth;
433
+ const cellsMaxW = tds.reduce((max, td) => Math.max(max, td.scrollWidth), 0);
434
+ const padding = 24;
435
+ const raw = Math.max(headerW, cellsMaxW) + padding;
436
+ const minW = columns.find((c) => c.id === columnId)?.minWidth ?? 80;
437
+ const maxW = columns.find((c) => c.id === columnId)?.maxWidth ?? 600;
438
+ const finalW = Math.max(minW, Math.min(raw, maxW));
439
+ setColumnWidths((prev) => ({ ...prev, [columnId]: finalW }));
440
+ };
441
+ const saveCurrentView = (name) => {
442
+ const viewState = {
443
+ page: query.page,
444
+ pageSize: query.pageSize,
445
+ sort: query.sort,
446
+ filters: query.filters,
447
+ columnWidths: enableColumnResizing ? columnWidths : void 0
448
+ };
449
+ setSavedViews((prev) => {
450
+ const filtered = prev.filter((v) => v.name !== name);
451
+ return [...filtered, { name, state: viewState }];
452
+ });
453
+ if (typeof window !== "undefined" && window.localStorage) {
454
+ try {
455
+ localStorage.setItem(`rowakit-view-${name}`, JSON.stringify(viewState));
456
+ } catch {
457
+ }
458
+ }
459
+ };
460
+ const loadSavedView = (name) => {
461
+ const view = savedViews.find((v) => v.name === name);
462
+ if (!view) return;
463
+ const { state } = view;
464
+ setQuery({
465
+ page: state.page,
466
+ pageSize: state.pageSize,
467
+ sort: state.sort,
468
+ filters: state.filters
469
+ });
470
+ setFilters(state.filters ?? {});
471
+ if (state.columnWidths && enableColumnResizing) {
472
+ setColumnWidths(state.columnWidths);
473
+ }
474
+ };
475
+ const deleteSavedView = (name) => {
476
+ setSavedViews((prev) => prev.filter((v) => v.name !== name));
477
+ if (typeof window !== "undefined" && window.localStorage) {
478
+ try {
479
+ localStorage.removeItem(`rowakit-view-${name}`);
480
+ } catch {
481
+ }
482
+ }
483
+ };
484
+ const resetTableState = () => {
485
+ setQuery({
486
+ page: 1,
487
+ pageSize: defaultPageSize
488
+ });
489
+ setFilters({});
490
+ setColumnWidths({});
491
+ };
492
+ const transformFilterValueForColumn = (column, value) => {
493
+ if (!value || column?.kind !== "number") {
494
+ return value;
495
+ }
496
+ const numberColumn = column;
497
+ if (!numberColumn.filterTransform) {
498
+ return value;
499
+ }
500
+ if (value.op === "equals" && typeof value.value === "number") {
501
+ return {
502
+ ...value,
503
+ value: numberColumn.filterTransform(value.value)
504
+ };
505
+ }
506
+ if (value.op === "range" && typeof value.value === "object") {
507
+ const { from, to } = value.value;
508
+ return {
509
+ op: "range",
510
+ value: {
511
+ from: from !== void 0 && typeof from === "number" ? numberColumn.filterTransform(from) : from,
512
+ to: to !== void 0 && typeof to === "number" ? numberColumn.filterTransform(to) : to
513
+ }
514
+ };
515
+ }
516
+ return value;
517
+ };
313
518
  const handleFilterChange = (field, value) => {
519
+ const column = columns.find((c) => c.id === field);
520
+ const transformedValue = transformFilterValueForColumn(column, value);
314
521
  setFilters((prev) => ({
315
522
  ...prev,
316
- [field]: value
523
+ [field]: transformedValue
317
524
  }));
318
525
  };
319
526
  const handleClearFilter = (field) => {
@@ -334,6 +541,52 @@ function RowaKitTable({
334
541
  const canGoNext = query.page < totalPages && !isLoading;
335
542
  const hasActiveFilters = enableFilters && Object.values(filters).some((v) => v !== void 0);
336
543
  return /* @__PURE__ */ jsxs("div", { className: `rowakit-table${className ? ` ${className}` : ""}`, children: [
544
+ enableSavedViews && /* @__PURE__ */ jsxs("div", { className: "rowakit-saved-views-group", children: [
545
+ /* @__PURE__ */ jsx(
546
+ "button",
547
+ {
548
+ onClick: () => {
549
+ const name = typeof window !== "undefined" ? window.prompt("Enter view name:") : null;
550
+ if (name) {
551
+ saveCurrentView(name);
552
+ }
553
+ },
554
+ className: "rowakit-saved-view-button",
555
+ type: "button",
556
+ children: "Save View"
557
+ }
558
+ ),
559
+ savedViews.map((view) => /* @__PURE__ */ jsxs("div", { className: "rowakit-saved-view-item", children: [
560
+ /* @__PURE__ */ jsx(
561
+ "button",
562
+ {
563
+ onClick: () => loadSavedView(view.name),
564
+ className: "rowakit-saved-view-button",
565
+ type: "button",
566
+ children: view.name
567
+ }
568
+ ),
569
+ /* @__PURE__ */ jsx(
570
+ "button",
571
+ {
572
+ onClick: () => deleteSavedView(view.name),
573
+ className: "rowakit-saved-view-button rowakit-saved-view-button-delete",
574
+ type: "button",
575
+ title: "Delete this view",
576
+ children: "\xD7"
577
+ }
578
+ )
579
+ ] }, view.name)),
580
+ (hasActiveFilters || query.page > 1 || query.sort) && /* @__PURE__ */ jsx(
581
+ "button",
582
+ {
583
+ onClick: resetTableState,
584
+ className: "rowakit-saved-view-button",
585
+ type: "button",
586
+ children: "Reset"
587
+ }
588
+ )
589
+ ] }),
337
590
  hasActiveFilters && /* @__PURE__ */ jsx("div", { className: "rowakit-table-filter-controls", children: /* @__PURE__ */ jsx(
338
591
  "button",
339
592
  {
@@ -343,14 +596,17 @@ function RowaKitTable({
343
596
  children: "Clear all filters"
344
597
  }
345
598
  ) }),
346
- /* @__PURE__ */ jsxs("table", { children: [
599
+ /* @__PURE__ */ jsxs("table", { ref: tableRef, children: [
347
600
  /* @__PURE__ */ jsxs("thead", { children: [
348
601
  /* @__PURE__ */ jsx("tr", { children: columns.map((column) => {
349
602
  const isSortable = column.kind !== "actions" && (column.kind === "custom" ? false : column.sortable === true);
350
603
  const field = column.kind === "actions" ? "" : column.kind === "custom" ? column.field : column.field;
604
+ const isResizable = enableColumnResizing && column.kind !== "actions";
605
+ const actualWidth = columnWidths[column.id] ?? column.width;
351
606
  return /* @__PURE__ */ jsxs(
352
607
  "th",
353
608
  {
609
+ "data-col-id": column.id,
354
610
  onClick: isSortable ? () => handleSort(String(field)) : void 0,
355
611
  role: isSortable ? "button" : void 0,
356
612
  tabIndex: isSortable ? 0 : void 0,
@@ -362,13 +618,23 @@ function RowaKitTable({
362
618
  } : void 0,
363
619
  "aria-sort": isSortable && query.sort?.field === String(field) ? query.sort.direction === "asc" ? "ascending" : "descending" : void 0,
364
620
  style: {
365
- width: column.width ? `${column.width}px` : void 0,
366
- textAlign: column.align
621
+ width: actualWidth ? `${actualWidth}px` : void 0,
622
+ textAlign: column.align,
623
+ position: isResizable ? "relative" : void 0
367
624
  },
368
- className: column.truncate ? "rowakit-cell-truncate" : void 0,
625
+ className: column.truncate && !isResizable ? "rowakit-cell-truncate" : void 0,
369
626
  children: [
370
627
  getHeaderLabel(column),
371
- isSortable && getSortIndicator(String(field))
628
+ isSortable && getSortIndicator(String(field)),
629
+ isResizable && /* @__PURE__ */ jsx(
630
+ "div",
631
+ {
632
+ className: "rowakit-column-resize-handle",
633
+ onMouseDown: (e) => startColumnResize(e, column.id),
634
+ onDoubleClick: () => handleColumnResizeDoubleClick(column.id),
635
+ title: "Drag to resize | Double-click to auto-fit content"
636
+ }
637
+ )
372
638
  ]
373
639
  },
374
640
  column.id
@@ -468,6 +734,51 @@ function RowaKitTable({
468
734
  ] }) }, column.id);
469
735
  }
470
736
  const isNumberColumn = column.kind === "number";
737
+ if (isNumberColumn) {
738
+ const fromValue = filterValue?.op === "range" ? String(filterValue.value.from ?? "") : filterValue?.op === "equals" && typeof filterValue.value === "number" ? String(filterValue.value) : "";
739
+ const toValue = filterValue?.op === "range" ? String(filterValue.value.to ?? "") : "";
740
+ const showRangeUI = !filterValue || filterValue.op === "range";
741
+ if (showRangeUI) {
742
+ return /* @__PURE__ */ jsx("th", { children: /* @__PURE__ */ jsxs("div", { className: "rowakit-filter-number-range", children: [
743
+ /* @__PURE__ */ jsx(
744
+ "input",
745
+ {
746
+ type: "number",
747
+ className: "rowakit-filter-input",
748
+ placeholder: "Min",
749
+ value: fromValue,
750
+ onChange: (e) => {
751
+ const from = e.target.value ? Number(e.target.value) : void 0;
752
+ const to = toValue ? Number(toValue) : void 0;
753
+ if (from === void 0 && to === void 0) {
754
+ handleClearFilter(field);
755
+ } else {
756
+ handleFilterChange(field, { op: "range", value: { from, to } });
757
+ }
758
+ }
759
+ }
760
+ ),
761
+ /* @__PURE__ */ jsx(
762
+ "input",
763
+ {
764
+ type: "number",
765
+ className: "rowakit-filter-input",
766
+ placeholder: "Max",
767
+ value: toValue,
768
+ onChange: (e) => {
769
+ const to = e.target.value ? Number(e.target.value) : void 0;
770
+ const from = fromValue ? Number(fromValue) : void 0;
771
+ if (from === void 0 && to === void 0) {
772
+ handleClearFilter(field);
773
+ } else {
774
+ handleFilterChange(field, { op: "range", value: { from, to } });
775
+ }
776
+ }
777
+ }
778
+ )
779
+ ] }) }, column.id);
780
+ }
781
+ }
471
782
  return /* @__PURE__ */ jsx("th", { children: /* @__PURE__ */ jsx(
472
783
  "input",
473
784
  {
@@ -519,12 +830,14 @@ function RowaKitTable({
519
830
  column.kind === "number" ? "rowakit-cell-number" : "",
520
831
  column.truncate ? "rowakit-cell-truncate" : ""
521
832
  ].filter(Boolean).join(" ") || void 0;
833
+ const actualWidth = columnWidths[column.id];
522
834
  return /* @__PURE__ */ jsx(
523
835
  "td",
524
836
  {
837
+ "data-col-id": column.id,
525
838
  className: cellClass,
526
839
  style: {
527
- width: column.width ? `${column.width}px` : void 0,
840
+ width: actualWidth ? `${actualWidth}px` : void 0,
528
841
  textAlign: column.align || (column.kind === "number" ? "right" : void 0)
529
842
  },
530
843
  children: renderCell(column, row, isLoading, setConfirmState)