@nexgrid/react 0.2.0 → 0.2.1

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
@@ -307,9 +307,11 @@ function TableX(props) {
307
307
  const {
308
308
  columns,
309
309
  data,
310
- total,
311
- query,
312
- onQueryChange,
310
+ total: totalProp,
311
+ query: queryProp,
312
+ onQueryChange: onQueryChangeProp,
313
+ clientSidePagination,
314
+ paginationMode,
313
315
  caption,
314
316
  density: initialDensity = "default",
315
317
  isLoading = false,
@@ -344,6 +346,8 @@ function TableX(props) {
344
346
  enableColumnReorder = false,
345
347
  onColumnOrderChange,
346
348
  onCellEdit,
349
+ storageKey,
350
+ showFilterPills = true,
347
351
  toolbarActions,
348
352
  onRowClick,
349
353
  getRowId = defaultRowId,
@@ -357,6 +361,22 @@ function TableX(props) {
357
361
  onNotify,
358
362
  theme = "light"
359
363
  } = props;
364
+ const isClientSide = clientSidePagination === true || paginationMode === "client" || onQueryChangeProp === void 0 && queryProp === void 0;
365
+ const [internalQuery, setInternalQuery] = React4.useState(() => queryProp ?? (0, import_core2.defaultQuery)());
366
+ const query = queryProp ?? internalQuery;
367
+ const onQueryChange = React4.useCallback(
368
+ (next) => {
369
+ setInternalQuery(next);
370
+ onQueryChangeProp?.(next);
371
+ },
372
+ [onQueryChangeProp]
373
+ );
374
+ const clientPaged = React4.useMemo(() => {
375
+ if (!isClientSide) return null;
376
+ return (0, import_core2.queryClientData)(data, query);
377
+ }, [isClientSide, data, query]);
378
+ const rows = isClientSide ? clientPaged?.items ?? data : data;
379
+ const total = isClientSide ? clientPaged?.total ?? data.length : totalProp ?? data.length;
360
380
  const locale = (0, import_core2.resolveLocale)(localeOverrides);
361
381
  const boolLabels = { yes: locale.booleanYes, no: locale.booleanNo };
362
382
  const isColumnsVisible = enableColumns && showColumnsButton !== false;
@@ -371,7 +391,7 @@ function TableX(props) {
371
391
  }, [columns]);
372
392
  const [density, setDensity] = React4.useState(initialDensity);
373
393
  const [hiddenCols, setHiddenCols] = React4.useState(
374
- () => (0, import_core2.initialHiddenColumns)(columns)
394
+ () => (0, import_core2.initialHiddenColumns)((0, import_core2.flattenColumns)(columns))
375
395
  );
376
396
  const [selectedIds, setSelectedIds] = React4.useState(
377
397
  () => /* @__PURE__ */ new Set()
@@ -384,9 +404,82 @@ function TableX(props) {
384
404
  const [isExporting, setIsExporting] = React4.useState(false);
385
405
  const [openFilterCol, setOpenFilterCol] = React4.useState(null);
386
406
  const [colWidths, setColWidths] = React4.useState({});
407
+ React4.useEffect(() => {
408
+ if (!storageKey) return;
409
+ const persisted = (0, import_core2.loadGridState)(storageKey);
410
+ if (persisted) {
411
+ if (persisted.density) setDensity(persisted.density);
412
+ if (persisted.columnWidths) setColWidths(persisted.columnWidths);
413
+ if (persisted.hiddenColumns && Array.isArray(persisted.hiddenColumns)) {
414
+ const hiddenMap = {};
415
+ for (const col of columns) {
416
+ const id = (0, import_core2.getColumnId)(col);
417
+ if (id) hiddenMap[id] = persisted.hiddenColumns.includes(id);
418
+ }
419
+ setHiddenCols(hiddenMap);
420
+ }
421
+ if (persisted.columnOrder && Array.isArray(persisted.columnOrder)) {
422
+ const colMap = new Map(columns.map((c) => [(0, import_core2.getColumnId)(c), c]));
423
+ const reordered = [];
424
+ for (const id of persisted.columnOrder) {
425
+ const col = colMap.get(id);
426
+ if (col) {
427
+ reordered.push(col);
428
+ colMap.delete(id);
429
+ }
430
+ }
431
+ for (const remaining of colMap.values()) {
432
+ reordered.push(remaining);
433
+ }
434
+ if (reordered.length === columns.length) {
435
+ setColList(reordered);
436
+ }
437
+ }
438
+ }
439
+ }, [storageKey, columns]);
440
+ React4.useEffect(() => {
441
+ if (!storageKey) return;
442
+ const hiddenList = Object.entries(hiddenCols).filter(([_, isHidden]) => isHidden).map(([id]) => id);
443
+ (0, import_core2.saveGridState)(storageKey, {
444
+ density,
445
+ columnWidths: colWidths,
446
+ columnOrder: colList.map(import_core2.getColumnId).filter(Boolean),
447
+ hiddenColumns: hiddenList
448
+ });
449
+ }, [storageKey, density, hiddenCols, colList, colWidths]);
387
450
  const columnsMenu = useDropdown();
388
451
  const densityMenu = useDropdown();
389
452
  const exportMenu = useDropdown();
453
+ const handleResetView = React4.useCallback(() => {
454
+ if (storageKey) {
455
+ (0, import_core2.clearGridState)(storageKey);
456
+ }
457
+ setDensity(initialDensity);
458
+ setColWidths({});
459
+ setColList(columns);
460
+ setHiddenCols((0, import_core2.initialHiddenColumns)((0, import_core2.flattenColumns)(columns)));
461
+ columnsMenu.close();
462
+ }, [storageKey, initialDensity, columns, columnsMenu]);
463
+ const leafCols = React4.useMemo(() => (0, import_core2.flattenColumns)(colList), [colList]);
464
+ const autoFitColumn = React4.useCallback((id) => {
465
+ const col = leafCols.find((c) => (0, import_core2.getColumnId)(c) === id);
466
+ if (!col) return;
467
+ const meta = col.meta ?? {};
468
+ let maxContentWidth = 0;
469
+ const headerTitle = (0, import_core2.getColumnTitle)(col) || id;
470
+ maxContentWidth = Math.max(maxContentWidth, headerTitle.length * 8.5 + 50);
471
+ for (const row of rows) {
472
+ const rawVal = (0, import_core2.getCellValue)(col, row);
473
+ const val = (0, import_core2.getCellText)(rawVal);
474
+ if (val) {
475
+ maxContentWidth = Math.max(maxContentWidth, String(val).length * 8 + 26);
476
+ }
477
+ }
478
+ const minW = meta.minWidth ?? 60;
479
+ const maxW = 550;
480
+ const finalWidth = Math.min(maxW, Math.max(minW, Math.round(maxContentWidth)));
481
+ setColWidths((prev) => ({ ...prev, [id]: finalWidth }));
482
+ }, [leafCols, rows]);
390
483
  const instanceId = React4.useId();
391
484
  const columnsButtonId = `${instanceId}-columns`;
392
485
  const densityButtonId = `${instanceId}-density`;
@@ -401,16 +494,20 @@ function TableX(props) {
401
494
  const totalPages = (0, import_core2.totalPagesFor)(total, pageSize);
402
495
  const range = (0, import_core2.getRecordRange)(currentPage, pageSize, total);
403
496
  const sort = (0, import_core2.primarySort)(query);
404
- const visible = React4.useMemo(
405
- () => (0, import_core2.visibleColumns)(colList, hiddenCols),
497
+ const headerRows = React4.useMemo(
498
+ () => (0, import_core2.buildHeaderRows)(colList, hiddenCols),
406
499
  [colList, hiddenCols]
407
500
  );
408
- const hideable = React4.useMemo(() => colList.filter(import_core2.isHideable), [colList]);
501
+ const visible = React4.useMemo(
502
+ () => (0, import_core2.visibleColumns)(leafCols, hiddenCols),
503
+ [leafCols, hiddenCols]
504
+ );
505
+ const hideable = React4.useMemo(() => leafCols.filter(import_core2.isHideable), [leafCols]);
409
506
  const pageItems = React4.useMemo(
410
507
  () => (0, import_core2.getPageNumbers)(currentPage, totalPages),
411
508
  [currentPage, totalPages]
412
509
  );
413
- const pageRowIds = React4.useMemo(() => data.map((row) => getRowId(row)), [data, getRowId]);
510
+ const pageRowIds = React4.useMemo(() => rows.map((row) => getRowId(row)), [rows, getRowId]);
414
511
  const columnCount = visible.length + (renderExpandedRow ? 1 : 0) + (showSerialNumber ? 1 : 0) + (enableSelection ? 1 : 0);
415
512
  const getPinnedOffsets = (visibleCols) => {
416
513
  const leftOffsets2 = /* @__PURE__ */ new Map();
@@ -529,7 +626,7 @@ function TableX(props) {
529
626
  setIsExporting(true);
530
627
  notify("info", "Exporting...");
531
628
  try {
532
- let exportRows = data;
629
+ let exportRows = rows;
533
630
  if (fetchEndpoint) {
534
631
  const full = await (0, import_core2.fetchAllPages)(async (page, size) => {
535
632
  const url = (0, import_core2.buildQueryUrl)(fetchEndpoint, {
@@ -542,6 +639,9 @@ function TableX(props) {
542
639
  return await response.json();
543
640
  });
544
641
  exportRows = full.items;
642
+ } else if (isClientSide) {
643
+ const full = (0, import_core2.queryClientData)(data, query, { paginate: false });
644
+ exportRows = full.items;
545
645
  }
546
646
  const exportColumns = (0, import_core2.toExportColumns)(visible, boolLabels);
547
647
  const prefix = exportFileName ?? (0, import_core2.filePrefixFromCaption)(caption);
@@ -593,6 +693,205 @@ function TableX(props) {
593
693
  onRowClick?.(row);
594
694
  };
595
695
  const hasSummary = enableSummaryRow === true || visible.some((col) => col.meta?.aggregation !== void 0);
696
+ const renderLeafTh = (col, rowSpan = 1, isGroupChild = false) => {
697
+ const id = (0, import_core2.getColumnId)(col);
698
+ const sortable = (0, import_core2.isSortable)(col) && enableSorting !== false;
699
+ const sortIndex = query.sort.findIndex((s) => s.field === id);
700
+ const sortItem = sortIndex >= 0 ? query.sort[sortIndex] : void 0;
701
+ const sorted = sortable && sortItem !== void 0;
702
+ const title = (0, import_core2.getColumnTitle)(col) || id;
703
+ const meta = col.meta;
704
+ const activeFilter = query.filter?.[id];
705
+ const isFilterActive = activeFilter !== void 0 && activeFilter !== "";
706
+ const customWidth = colWidths[id];
707
+ const baseStyle = headerCellStyle(col);
708
+ const thStyle = {
709
+ ...baseStyle,
710
+ ...customWidth !== void 0 ? { width: `${customWidth}px` } : {},
711
+ ...leftOffsets.has(id) ? { left: leftOffsets.get(id) } : {},
712
+ ...rightOffsets.has(id) ? { right: rightOffsets.get(id) } : {}
713
+ };
714
+ const startResize = (e) => {
715
+ e.preventDefault();
716
+ e.stopPropagation();
717
+ const startX = e.clientX;
718
+ const targetTh = e.currentTarget.parentElement || null;
719
+ const startWidth = targetTh ? targetTh.getBoundingClientRect().width : customWidth ?? meta?.width ?? 120;
720
+ const onMove = (moveEvent) => {
721
+ const nextW = Math.max(meta?.minWidth ?? 60, Math.round(startWidth + (moveEvent.clientX - startX)));
722
+ setColWidths((prev) => ({ ...prev, [id]: nextW }));
723
+ };
724
+ const onUp = () => {
725
+ document.removeEventListener("pointermove", onMove);
726
+ document.removeEventListener("pointerup", onUp);
727
+ document.body?.classList.remove("tbx-resizing");
728
+ };
729
+ document.body?.classList.add("tbx-resizing");
730
+ document.addEventListener("pointermove", onMove);
731
+ document.addEventListener("pointerup", onUp);
732
+ };
733
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
734
+ "th",
735
+ {
736
+ scope: "col",
737
+ rowSpan: rowSpan > 1 ? rowSpan : void 0,
738
+ "aria-sort": !sortable ? void 0 : sorted ? sortItem?.dir === "asc" ? "ascending" : "descending" : "none",
739
+ tabIndex: sortable ? 0 : void 0,
740
+ "data-column-id": id,
741
+ "data-tbx-focus": sortable ? `sort:${id}` : void 0,
742
+ draggable: enableColumnReorder && !isGroupChild,
743
+ onDragStart: enableColumnReorder && !isGroupChild ? (e) => {
744
+ setDraggedColId(id);
745
+ e.dataTransfer.effectAllowed = "move";
746
+ e.dataTransfer.setData("text/plain", id);
747
+ } : void 0,
748
+ onDragOver: enableColumnReorder && !isGroupChild ? (e) => {
749
+ e.preventDefault();
750
+ } : void 0,
751
+ onDrop: enableColumnReorder && !isGroupChild ? (e) => {
752
+ e.preventDefault();
753
+ if (!draggedColId || draggedColId === id) return;
754
+ const fromIdx = colList.findIndex((c) => (0, import_core2.getColumnId)(c) === draggedColId);
755
+ const toIdx = colList.findIndex((c) => (0, import_core2.getColumnId)(c) === id);
756
+ if (fromIdx >= 0 && toIdx >= 0) {
757
+ const nextCols = [...colList];
758
+ const moved = nextCols[fromIdx];
759
+ if (!moved) return;
760
+ nextCols.splice(fromIdx, 1);
761
+ nextCols.splice(toIdx, 0, moved);
762
+ setColList(nextCols);
763
+ onColumnOrderChange?.(nextCols.map(import_core2.getColumnId));
764
+ }
765
+ } : void 0,
766
+ onDragEnd: enableColumnReorder && !isGroupChild ? () => {
767
+ setDraggedColId(null);
768
+ } : void 0,
769
+ className: [
770
+ "tbx-th",
771
+ sortable ? "tbx-th--sortable" : void 0,
772
+ isGroupChild ? "tbx-th--grouped-child" : void 0,
773
+ enableColumnReorder && !isGroupChild ? "tbx-th--draggable" : void 0,
774
+ draggedColId === id ? "tbx-th--dragging" : void 0,
775
+ (0, import_core2.isPinned)(col) === "left" ? "tbx-th--pinned-left" : void 0,
776
+ (0, import_core2.isPinned)(col) === "right" ? "tbx-th--pinned-right" : void 0,
777
+ lastLeftPinnedId === id ? "tbx-pinned-border-left" : void 0,
778
+ firstRightPinnedId === id ? "tbx-pinned-border-right" : void 0
779
+ ].filter(Boolean).join(" "),
780
+ style: thStyle,
781
+ onClick: sortable ? (event) => {
782
+ const t = event.target;
783
+ if (t?.closest(".tbx-col-filter-wrap") || t?.closest(".tbx-resize-handle")) return;
784
+ toggleSort(id, event.shiftKey);
785
+ } : void 0,
786
+ onKeyDown: sortable ? (event) => {
787
+ if (event.key !== "Enter" && event.key !== " ") return;
788
+ const t = event.target;
789
+ if (t?.closest(".tbx-col-filter-wrap")) return;
790
+ event.preventDefault();
791
+ toggleSort(id, event.shiftKey);
792
+ } : void 0,
793
+ children: [
794
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: headerInnerClass(col), children: [
795
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: renderColumnHeader(col) }),
796
+ sortable ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "tbx-sort-icon-wrap", children: [
797
+ sorted ? sortItem?.dir === "asc" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ArrowUpIcon, { className: "tbx-sort-icon" }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ArrowDownIcon, { className: "tbx-sort-icon" }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ArrowUpDownIcon, { className: "tbx-sort-icon tbx-sort-icon--idle" }),
798
+ query.sort.length > 1 && sortIndex >= 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "tbx-sort-order", children: sortIndex + 1 }) : null
799
+ ] }) : null,
800
+ (0, import_core2.isFilterable)(col, enableColumnFilters) ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "tbx-col-filter-wrap", children: [
801
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
802
+ "button",
803
+ {
804
+ type: "button",
805
+ className: isFilterActive ? "tbx-col-filter-btn tbx-col-filter-btn--active" : "tbx-col-filter-btn",
806
+ "aria-label": `Filter ${title}`,
807
+ onClick: (e) => {
808
+ e.stopPropagation();
809
+ setOpenFilterCol(openFilterCol === id ? null : id);
810
+ },
811
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(DotsVerticalIcon, { className: "tbx-icon" })
812
+ }
813
+ ),
814
+ openFilterCol === id ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
815
+ "div",
816
+ {
817
+ className: "tbx-filter-popover",
818
+ onClick: (e) => e.stopPropagation(),
819
+ children: [
820
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
821
+ "input",
822
+ {
823
+ autoFocus: true,
824
+ type: "text",
825
+ className: "tbx-filter-popover-input",
826
+ defaultValue: activeFilter ?? "",
827
+ placeholder: `Filter by ${title}...`,
828
+ onKeyDown: (e) => {
829
+ if (e.key === "Enter") {
830
+ e.preventDefault();
831
+ const val = e.currentTarget.value.trim();
832
+ setOpenFilterCol(null);
833
+ onQueryChange((0, import_core2.withFilter)(query, id, val || void 0));
834
+ } else if (e.key === "Escape") {
835
+ setOpenFilterCol(null);
836
+ }
837
+ }
838
+ }
839
+ ),
840
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "tbx-filter-popover-actions", children: [
841
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
842
+ "button",
843
+ {
844
+ type: "button",
845
+ className: "tbx-filter-popover-btn",
846
+ onClick: (e) => {
847
+ e.stopPropagation();
848
+ setOpenFilterCol(null);
849
+ onQueryChange((0, import_core2.withFilter)(query, id, void 0));
850
+ },
851
+ children: [
852
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(RotateCcwIcon, { className: "tbx-icon" }),
853
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: locale.clearFilter })
854
+ ]
855
+ }
856
+ ),
857
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
858
+ "button",
859
+ {
860
+ type: "button",
861
+ className: "tbx-filter-popover-btn tbx-filter-popover-btn--primary",
862
+ onClick: (e) => {
863
+ e.stopPropagation();
864
+ const parent = e.currentTarget.closest(".tbx-filter-popover");
865
+ const inputEl = parent?.querySelector("input");
866
+ const val = inputEl?.value.trim();
867
+ setOpenFilterCol(null);
868
+ onQueryChange((0, import_core2.withFilter)(query, id, val || void 0));
869
+ },
870
+ children: [
871
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CheckIcon, { className: "tbx-icon" }),
872
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: locale.applyFilter })
873
+ ]
874
+ }
875
+ )
876
+ ] })
877
+ ]
878
+ }
879
+ ) : null
880
+ ] }) : null
881
+ ] }),
882
+ enableColumnResize ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
883
+ "div",
884
+ {
885
+ className: "tbx-resize-handle",
886
+ onPointerDown: startResize,
887
+ onDoubleClick: () => autoFitColumn(id)
888
+ }
889
+ ) : null
890
+ ]
891
+ },
892
+ id
893
+ );
894
+ };
596
895
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: rootClasses.join(" "), "data-density": density, ref: rootRef, children: [
597
896
  showToolbar ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "tbx-toolbar", children: [
598
897
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "tbx-toolbar-group", children: enableSearch ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "tbx-search", children: [
@@ -661,7 +960,20 @@ function TableX(props) {
661
960
  },
662
961
  id
663
962
  );
664
- })
963
+ }),
964
+ storageKey ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
965
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "tbx-menu-separator" }),
966
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
967
+ "button",
968
+ {
969
+ type: "button",
970
+ className: "tbx-menu-item tbx-menu-item--reset",
971
+ role: "menuitem",
972
+ onClick: handleResetView,
973
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: "Reset to default view" })
974
+ }
975
+ )
976
+ ] }) : null
665
977
  ] }) : null
666
978
  ] }) : null,
667
979
  isDensityVisible ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "tbx-menu-wrap", ref: densityMenu.containerRef, children: [
@@ -781,291 +1093,152 @@ function TableX(props) {
781
1093
  toolbarActions
782
1094
  ] })
783
1095
  ] }) : null,
784
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "tbx-table-wrap", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("table", { className: "tbx-table", "aria-label": caption, children: [
785
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("tr", { children: [
786
- renderExpandedRow ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
787
- "th",
788
- {
789
- className: [
790
- "tbx-th tbx-th--expand",
791
- leftOffsets.has("__expand") ? "tbx-th--pinned-left" : void 0,
792
- lastLeftPinnedId === "__expand" ? "tbx-pinned-border-left" : void 0
793
- ].filter(Boolean).join(" "),
794
- style: {
795
- width: "40px",
796
- left: leftOffsets.get("__expand")
797
- },
798
- scope: "col"
799
- }
800
- ) : null,
801
- enableSelection ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
802
- "th",
803
- {
804
- className: [
805
- "tbx-th tbx-th--select",
806
- leftOffsets.has("__select") ? "tbx-th--pinned-left" : void 0,
807
- lastLeftPinnedId === "__select" ? "tbx-pinned-border-left" : void 0
808
- ].filter(Boolean).join(" "),
809
- style: {
810
- left: leftOffsets.get("__select")
811
- },
812
- scope: "col",
813
- children: !isSingleSelect ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
814
- "input",
815
- {
816
- type: "checkbox",
817
- className: "tbx-checkbox",
818
- checked: allPageSelected,
819
- ref: (el) => {
820
- if (el) el.indeterminate = somePageSelected;
821
- },
822
- onChange: toggleSelectAll,
823
- "aria-label": locale.selectAllLabel
824
- }
825
- ) : null
826
- }
827
- ) : null,
828
- showSerialNumber ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
829
- "th",
1096
+ showFilterPills !== false && (Boolean(query.q?.trim()) || Object.keys(query.filter ?? {}).some((k) => query.filter?.[k])) ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "tbx-filter-pills-bar", children: [
1097
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "tbx-filter-pills-title", children: "Active filters:" }),
1098
+ query.q?.trim() ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "tbx-filter-pill", children: [
1099
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "tbx-filter-pill-label", children: [
1100
+ searchPlaceholder || locale.searchPlaceholder || "Search",
1101
+ ":"
1102
+ ] }),
1103
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "tbx-filter-pill-val", children: [
1104
+ '"',
1105
+ query.q,
1106
+ '"'
1107
+ ] }),
1108
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1109
+ "button",
830
1110
  {
831
- className: [
832
- "tbx-th tbx-th--serial",
833
- leftOffsets.has("__serial") ? "tbx-th--pinned-left" : void 0,
834
- lastLeftPinnedId === "__serial" ? "tbx-pinned-border-left" : void 0
835
- ].filter(Boolean).join(" "),
836
- style: {
837
- left: leftOffsets.get("__serial")
838
- },
839
- scope: "col",
840
- children: locale.serialHeader
1111
+ type: "button",
1112
+ className: "tbx-filter-pill-remove",
1113
+ title: "Clear search",
1114
+ "aria-label": "Clear search",
1115
+ onClick: () => onQueryChange((0, import_core2.withSearch)(query, "")),
1116
+ children: "\u2715"
841
1117
  }
842
- ) : null,
843
- visible.map((col, index) => {
844
- const id = (0, import_core2.getColumnId)(col);
845
- const sortable = (0, import_core2.isSortable)(col) && enableSorting !== false;
846
- const sortIndex = query.sort.findIndex((s) => s.field === id);
847
- const sortItem = sortIndex >= 0 ? query.sort[sortIndex] : void 0;
848
- const sorted = sortable && sortItem !== void 0;
849
- const title = (0, import_core2.getColumnTitle)(col) || id;
850
- const meta = col.meta;
851
- const activeFilter = query.filter?.[id];
852
- const isFilterActive = activeFilter !== void 0 && activeFilter !== "";
853
- const customWidth = colWidths[id];
854
- const baseStyle = headerCellStyle(col);
855
- const thStyle = {
856
- ...baseStyle,
857
- ...customWidth !== void 0 ? { width: `${customWidth}px` } : {},
858
- ...leftOffsets.has(id) ? { left: leftOffsets.get(id) } : {},
859
- ...rightOffsets.has(id) ? { right: rightOffsets.get(id) } : {}
860
- };
861
- const startResize = (e) => {
862
- e.preventDefault();
863
- e.stopPropagation();
864
- const startX = e.clientX;
865
- const targetTh = e.currentTarget.parentElement || null;
866
- const startWidth = targetTh ? targetTh.getBoundingClientRect().width : customWidth ?? meta?.width ?? 120;
867
- const onMove = (moveEvent) => {
868
- const nextW = Math.max(meta?.minWidth ?? 60, Math.round(startWidth + (moveEvent.clientX - startX)));
869
- setColWidths((prev) => ({ ...prev, [id]: nextW }));
870
- };
871
- const onUp = () => {
872
- document.removeEventListener("pointermove", onMove);
873
- document.removeEventListener("pointerup", onUp);
874
- document.body?.classList.remove("tbx-resizing");
875
- };
876
- document.body?.classList.add("tbx-resizing");
877
- document.addEventListener("pointermove", onMove);
878
- document.addEventListener("pointerup", onUp);
879
- };
880
- const isPinnedLeft = leftOffsets.has(id);
881
- const isPinnedRight = rightOffsets.has(id);
882
- const isLastLeft = lastLeftPinnedId === id;
883
- const isFirstRight = firstRightPinnedId === id;
884
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1118
+ )
1119
+ ] }) : null,
1120
+ Object.entries(query.filter ?? {}).map(([key, val]) => {
1121
+ if (val === void 0 || val === "") return null;
1122
+ const col = leafCols.find((c) => (0, import_core2.getColumnId)(c) === key);
1123
+ const title = col ? (0, import_core2.getColumnTitle)(col) || key : key;
1124
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "tbx-filter-pill", children: [
1125
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "tbx-filter-pill-label", children: [
1126
+ title,
1127
+ ":"
1128
+ ] }),
1129
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "tbx-filter-pill-val", children: String(val) }),
1130
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1131
+ "button",
1132
+ {
1133
+ type: "button",
1134
+ className: "tbx-filter-pill-remove",
1135
+ title: `Remove ${title} filter`,
1136
+ "aria-label": `Remove filter for ${title}`,
1137
+ onClick: () => onQueryChange((0, import_core2.withFilter)(query, key, void 0)),
1138
+ children: "\u2715"
1139
+ }
1140
+ )
1141
+ ] }, key);
1142
+ }),
1143
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1144
+ "button",
1145
+ {
1146
+ type: "button",
1147
+ className: "tbx-filter-pill-clear-all",
1148
+ onClick: () => onQueryChange({ ...query, page: 1, q: void 0, filter: {} }),
1149
+ children: "Clear all"
1150
+ }
1151
+ )
1152
+ ] }) : null,
1153
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "tbx-table-wrap", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("table", { className: "tbx-table", "aria-label": caption, children: [
1154
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("thead", { children: [
1155
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("tr", { children: [
1156
+ renderExpandedRow ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
885
1157
  "th",
886
1158
  {
887
- scope: "col",
1159
+ rowSpan: headerRows.hasGroups ? 2 : void 0,
1160
+ className: [
1161
+ "tbx-th tbx-th--expand",
1162
+ leftOffsets.has("__expand") ? "tbx-th--pinned-left" : void 0,
1163
+ lastLeftPinnedId === "__expand" ? "tbx-pinned-border-left" : void 0
1164
+ ].filter(Boolean).join(" "),
1165
+ style: {
1166
+ width: "40px",
1167
+ left: leftOffsets.get("__expand")
1168
+ },
1169
+ scope: "col"
1170
+ }
1171
+ ) : null,
1172
+ enableSelection ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1173
+ "th",
1174
+ {
1175
+ rowSpan: headerRows.hasGroups ? 2 : void 0,
888
1176
  className: [
889
- "tbx-th",
890
- sortable ? "tbx-th--sortable" : void 0,
891
- enableColumnReorder ? "tbx-th--draggable" : void 0,
892
- isPinnedLeft ? "tbx-th--pinned-left" : void 0,
893
- isPinnedRight ? "tbx-th--pinned-right" : void 0,
894
- isLastLeft ? "tbx-pinned-border-left" : void 0,
895
- isFirstRight ? "tbx-pinned-border-right" : void 0
1177
+ "tbx-th tbx-th--select",
1178
+ leftOffsets.has("__select") ? "tbx-th--pinned-left" : void 0,
1179
+ lastLeftPinnedId === "__select" ? "tbx-pinned-border-left" : void 0
896
1180
  ].filter(Boolean).join(" "),
897
- style: thStyle,
898
- "aria-sort": sorted ? sortItem?.dir === "asc" ? "ascending" : "descending" : sortable ? "none" : void 0,
899
- "aria-label": title || void 0,
900
- tabIndex: sortable ? 0 : void 0,
901
- draggable: enableColumnReorder,
902
- onDragStart: enableColumnReorder ? (e) => {
903
- setDraggedColId(id);
904
- e.dataTransfer.setData("text/plain", id);
905
- } : void 0,
906
- onDragOver: enableColumnReorder ? (e) => {
907
- e.preventDefault();
908
- } : void 0,
909
- onDrop: enableColumnReorder ? (e) => {
910
- e.preventDefault();
911
- if (!draggedColId || draggedColId === id) return;
912
- const fromIdx = colList.findIndex((c) => (0, import_core2.getColumnId)(c) === draggedColId);
913
- const toIdx = colList.findIndex((c) => (0, import_core2.getColumnId)(c) === id);
914
- if (fromIdx >= 0 && toIdx >= 0) {
915
- const nextCols = [...colList];
916
- const moved = nextCols[fromIdx];
917
- if (!moved) return;
918
- nextCols.splice(fromIdx, 1);
919
- nextCols.splice(toIdx, 0, moved);
920
- setColList(nextCols);
921
- onColumnOrderChange?.(nextCols.map(import_core2.getColumnId));
1181
+ style: {
1182
+ left: leftOffsets.get("__select")
1183
+ },
1184
+ scope: "col",
1185
+ children: !isSingleSelect ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1186
+ "input",
1187
+ {
1188
+ type: "checkbox",
1189
+ className: "tbx-checkbox",
1190
+ checked: allPageSelected,
1191
+ ref: (el) => {
1192
+ if (el) el.indeterminate = somePageSelected;
1193
+ },
1194
+ onChange: toggleSelectAll,
1195
+ "aria-label": locale.selectAllLabel
922
1196
  }
923
- setDraggedColId(null);
924
- } : void 0,
925
- onClick: sortable ? (event) => {
926
- const t = event.target;
927
- if (t?.closest(".tbx-col-filter-wrap") || t?.closest(".tbx-resize-handle")) return;
928
- toggleSort(id, event.shiftKey);
929
- } : void 0,
930
- onKeyDown: sortable ? (event) => {
931
- if (event.key !== "Enter" && event.key !== " ") return;
932
- const t = event.target;
933
- if (t?.closest(".tbx-col-filter-wrap")) return;
934
- event.preventDefault();
935
- toggleSort(id, event.shiftKey);
936
- } : void 0,
937
- children: [
938
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: headerInnerClass(col), children: [
939
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: renderColumnHeader(col) }),
940
- sortable ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "tbx-sort-icon-wrap", children: [
941
- sorted ? sortItem?.dir === "asc" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ArrowUpIcon, { className: "tbx-sort-icon" }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ArrowDownIcon, { className: "tbx-sort-icon" }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ArrowUpDownIcon, { className: "tbx-sort-icon tbx-sort-icon--idle" }),
942
- query.sort.length > 1 && sortIndex >= 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "tbx-sort-order", children: sortIndex + 1 }) : null
943
- ] }) : null,
944
- (0, import_core2.isFilterable)(col, enableColumnFilters) ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "tbx-col-filter-wrap", children: [
945
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
946
- "button",
947
- {
948
- type: "button",
949
- className: isFilterActive ? "tbx-col-filter-btn tbx-col-filter-btn--active" : "tbx-col-filter-btn",
950
- "aria-label": `Filter ${title}`,
951
- onClick: (e) => {
952
- e.stopPropagation();
953
- setOpenFilterCol(openFilterCol === id ? null : id);
954
- },
955
- children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(DotsVerticalIcon, { className: "tbx-icon" })
956
- }
957
- ),
958
- openFilterCol === id ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
959
- "div",
960
- {
961
- className: "tbx-filter-popover",
962
- onClick: (e) => e.stopPropagation(),
963
- children: [
964
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
965
- "input",
966
- {
967
- autoFocus: true,
968
- type: "text",
969
- className: "tbx-filter-popover-input",
970
- defaultValue: activeFilter ?? "",
971
- placeholder: meta?.filterPlaceholder || (0, import_core2.formatMessage)(locale.filterColumnPlaceholder, { column: title }),
972
- "aria-label": `Filter ${title}`,
973
- onChange: (e) => {
974
- const val = e.currentTarget.value.toLowerCase().trim();
975
- const pop = e.currentTarget.closest(".tbx-filter-popover");
976
- const opts = pop?.querySelectorAll(".tbx-filter-option:not(:first-child)");
977
- opts?.forEach((opt) => {
978
- const text = opt.textContent?.toLowerCase() ?? "";
979
- opt.style.display = !val || text.includes(val) ? "" : "none";
980
- });
981
- },
982
- onKeyDown: (e) => {
983
- if (e.key === "Enter") {
984
- e.preventDefault();
985
- const val = e.currentTarget.value.trim();
986
- setOpenFilterCol(null);
987
- onQueryChange((0, import_core2.withFilter)(query, id, val || void 0));
988
- } else if (e.key === "Escape") {
989
- e.preventDefault();
990
- setOpenFilterCol(null);
991
- }
992
- }
993
- }
994
- ),
995
- meta?.filterOptions && meta.filterOptions.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "tbx-filter-popover-options", children: [
996
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
997
- "div",
998
- {
999
- className: !activeFilter ? "tbx-filter-option tbx-filter-option--selected" : "tbx-filter-option",
1000
- onClick: () => {
1001
- setOpenFilterCol(null);
1002
- onQueryChange((0, import_core2.withFilter)(query, id, void 0));
1003
- },
1004
- children: locale.filterAll
1005
- }
1006
- ),
1007
- meta.filterOptions.map((opt) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1008
- "div",
1009
- {
1010
- className: activeFilter === opt ? "tbx-filter-option tbx-filter-option--selected" : "tbx-filter-option",
1011
- onClick: () => {
1012
- setOpenFilterCol(null);
1013
- onQueryChange((0, import_core2.withFilter)(query, id, opt));
1014
- },
1015
- children: opt
1016
- },
1017
- opt
1018
- ))
1019
- ] }) : null,
1020
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "tbx-filter-popover-actions", children: [
1021
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1022
- "button",
1023
- {
1024
- type: "button",
1025
- className: "tbx-filter-popover-btn",
1026
- onClick: (e) => {
1027
- e.stopPropagation();
1028
- setOpenFilterCol(null);
1029
- onQueryChange((0, import_core2.withFilter)(query, id, void 0));
1030
- },
1031
- children: [
1032
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(RotateCcwIcon, { className: "tbx-icon" }),
1033
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: locale.clearFilter })
1034
- ]
1035
- }
1036
- ),
1037
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1038
- "button",
1039
- {
1040
- type: "button",
1041
- className: "tbx-filter-popover-btn tbx-filter-popover-btn--primary",
1042
- onClick: (e) => {
1043
- e.stopPropagation();
1044
- const parent = e.currentTarget.closest(".tbx-filter-popover");
1045
- const inputEl = parent?.querySelector("input");
1046
- const val = inputEl?.value.trim();
1047
- setOpenFilterCol(null);
1048
- onQueryChange((0, import_core2.withFilter)(query, id, val || void 0));
1049
- },
1050
- children: [
1051
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CheckIcon, { className: "tbx-icon" }),
1052
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: locale.applyFilter })
1053
- ]
1054
- }
1055
- )
1056
- ] })
1057
- ]
1058
- }
1059
- ) : null
1060
- ] }) : null
1061
- ] }),
1062
- enableColumnResize ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "tbx-resize-handle", onPointerDown: startResize }) : null
1063
- ]
1064
- },
1065
- id || `col-${index}`
1066
- );
1067
- })
1068
- ] }) }),
1197
+ ) : null
1198
+ }
1199
+ ) : null,
1200
+ showSerialNumber ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1201
+ "th",
1202
+ {
1203
+ rowSpan: headerRows.hasGroups ? 2 : void 0,
1204
+ className: [
1205
+ "tbx-th tbx-th--serial",
1206
+ leftOffsets.has("__serial") ? "tbx-th--pinned-left" : void 0,
1207
+ lastLeftPinnedId === "__serial" ? "tbx-pinned-border-left" : void 0
1208
+ ].filter(Boolean).join(" "),
1209
+ style: {
1210
+ left: leftOffsets.get("__serial")
1211
+ },
1212
+ scope: "col",
1213
+ children: locale.serialHeader
1214
+ }
1215
+ ) : null,
1216
+ !headerRows.hasGroups ? visible.map((col) => renderLeafTh(col, 1, false)) : headerRows.topRow.map((cell, idx) => {
1217
+ if (cell.isGroup) {
1218
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1219
+ "th",
1220
+ {
1221
+ colSpan: cell.colSpan,
1222
+ className: "tbx-th tbx-th--group",
1223
+ scope: "colgroup",
1224
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "tbx-th-group-title", children: cell.title })
1225
+ },
1226
+ `grp-${cell.id}-${idx}`
1227
+ );
1228
+ }
1229
+ if (cell.leafColumn) {
1230
+ return renderLeafTh(cell.leafColumn, cell.rowSpan, false);
1231
+ }
1232
+ return null;
1233
+ })
1234
+ ] }),
1235
+ headerRows.hasGroups ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("tr", { children: headerRows.bottomRow.map((cell) => {
1236
+ if (cell.leafColumn) {
1237
+ return renderLeafTh(cell.leafColumn, 1, true);
1238
+ }
1239
+ return null;
1240
+ }) }) : null
1241
+ ] }),
1069
1242
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("tbody", { children: isLoading ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("tr", { children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("td", { className: "tbx-state", colSpan: Math.max(1, columnCount), children: [
1070
1243
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "tbx-dotted-loader", "aria-hidden": "true", children: [
1071
1244
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "tbx-dot" }),
@@ -1074,7 +1247,7 @@ function TableX(props) {
1074
1247
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "tbx-dot" })
1075
1248
  ] }),
1076
1249
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "tbx-loading-text", "aria-live": "polite", children: locale.loadingText })
1077
- ] }) }) : data.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("tr", { children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("td", { className: "tbx-state", colSpan: Math.max(1, columnCount), children: locale.emptyText }) }) : data.map((row, index) => {
1250
+ ] }) }) : rows.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("tr", { children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("td", { className: "tbx-state", colSpan: Math.max(1, columnCount), children: locale.emptyText }) }) : rows.map((row, index) => {
1078
1251
  const id = pageRowIds[index] ?? String(index);
1079
1252
  const isSelected = selectedIds.has(id);
1080
1253
  const isExpanded = expandedRows.has(id);
@@ -1283,7 +1456,7 @@ function TableX(props) {
1283
1456
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "tbx-dot" })
1284
1457
  ] }),
1285
1458
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "tbx-loading-text", "aria-live": "polite", children: locale.loadingText })
1286
- ] }) }) : data.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "tbx-card", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "tbx-state", children: locale.emptyText }) }) : data.map((row, index) => {
1459
+ ] }) }) : rows.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "tbx-card", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "tbx-state", children: locale.emptyText }) }) : rows.map((row, index) => {
1287
1460
  const id = pageRowIds[index] ?? String(index);
1288
1461
  const isSelected = selectedIds.has(id);
1289
1462
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(