@seedgrid/fe-components 2026.10.3 → 2026.11.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.
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import React from "react";
4
4
  import { createPortal } from "react-dom";
5
5
  import { ArrowDown, ArrowUp, Check, ChevronFirst, ChevronLast, ChevronLeft, ChevronRight, ChevronsUpDown, GripVertical, MoreHorizontal, SlidersHorizontal } from "lucide-react";
@@ -7,6 +7,9 @@ import { SgButton } from "../buttons/SgButton";
7
7
  import { useHasSgEnvironmentProvider, useSgPersistence } from "../environment/SgEnvironmentProvider";
8
8
  import { SgGroupBox } from "../layout/SgGroupBox";
9
9
  import { t, useComponentsI18n } from "../i18n";
10
+ import { useSgContainerWidth } from "./useSgContainerWidth";
11
+ /** Largura (px) do container abaixo da qual o modo `cards` entra. Igual ao corte do Flutter. */
12
+ export const SG_DATATABLE_MOBILE_BREAKPOINT = 600;
10
13
  function cn(...parts) {
11
14
  return parts.filter(Boolean).join(" ");
12
15
  }
@@ -223,7 +226,7 @@ function SgDatatableBase(props, imperativeRef) {
223
226
  const i18n = useComponentsI18n();
224
227
  const hasEnvironmentProvider = useHasSgEnvironmentProvider();
225
228
  const persistence = useSgPersistence();
226
- const { id, title, value, columns, dataKey, lazy = false, totalRecords: controlledTotalRecords, paginator = false, rows = 10, first, rowsPerPageOptions, onPage, sortField, sortOrder, onSort, removableSort = true, selectionMode, selection, onSelectionChange, showGlobalFilter = false, globalFilter, globalFilterPlaceholder, onGlobalFilterChange, showColumnFilters = false, filters, onFilter, showClearFiltersButton = false, stripedRows = false, showGridlines = false, hoverableRows = true, loading = false, emptyMessage, className = "", style, tableClassName = "", rowClassName, groupBoxProps = {}, showColumnManager = true, columnManagerLabel, columnManagerMaxHeight = 320 } = props;
229
+ const { id, title, value, columns, dataKey, lazy = false, totalRecords: controlledTotalRecords, paginator = false, rows = 10, first, rowsPerPageOptions, onPage, infiniteScroll, onLoadMore, sortField, sortOrder, onSort, removableSort = true, selectionMode, selection, onSelectionChange, showGlobalFilter = false, globalFilter, globalFilterPlaceholder, onGlobalFilterChange, showColumnFilters = false, filters, onFilter, showClearFiltersButton = false, stripedRows = false, showGridlines = false, hoverableRows = true, loading = false, emptyMessage, className = "", style, tableClassName = "", rowClassName, groupBoxProps = {}, showColumnManager = true, columnManagerLabel, columnManagerMaxHeight = 320, mobileMode } = props;
227
230
  const isFirstControlled = first !== undefined;
228
231
  const isSortFieldControlled = sortField !== undefined;
229
232
  const isSortOrderControlled = sortOrder !== undefined;
@@ -237,6 +240,12 @@ function SgDatatableBase(props, imperativeRef) {
237
240
  const [internalSelection, setInternalSelection] = React.useState(null);
238
241
  const [internalGlobalFilter, setInternalGlobalFilter] = React.useState(globalFilter ?? "");
239
242
  const [internalFilters, setInternalFilters] = React.useState(() => normalizeFilters(filters));
243
+ const rootRef = React.useRef(null);
244
+ const containerWidth = useSgContainerWidth(rootRef, mobileMode === undefined);
245
+ const cardsActive = mobileMode === "cards" ||
246
+ (mobileMode === undefined && containerWidth !== null && containerWidth < SG_DATATABLE_MOBILE_BREAKPOINT);
247
+ const infiniteActive = paginator && (infiniteScroll ?? cardsActive);
248
+ const [isCardFiltersOpen, setIsCardFiltersOpen] = React.useState(false);
240
249
  const columnManagerRef = React.useRef(null);
241
250
  const columnManagerButtonRef = React.useRef(null);
242
251
  const columnManagerPanelRef = React.useRef(null);
@@ -480,13 +489,22 @@ function SgDatatableBase(props, imperativeRef) {
480
489
  const pageCount = Math.max(1, Math.ceil(Math.max(resolvedTotalRecords, 1) / currentRows));
481
490
  const maxFirst = Math.max(0, (pageCount - 1) * currentRows);
482
491
  const safeFirst = Math.min(currentFirst, maxFirst);
492
+ const [loadedPages, setLoadedPages] = React.useState(1);
483
493
  const pagedRows = React.useMemo(() => {
484
494
  if (!paginator)
485
495
  return sortedRows;
486
496
  if (lazy)
487
497
  return sortedRows;
498
+ if (infiniteActive)
499
+ return sortedRows.slice(0, loadedPages * currentRows);
488
500
  return sortedRows.slice(safeFirst, safeFirst + currentRows);
489
- }, [paginator, lazy, sortedRows, safeFirst, currentRows]);
501
+ }, [paginator, lazy, infiniteActive, loadedPages, sortedRows, safeFirst, currentRows]);
502
+ const infiniteHasMore = infiniteActive && (lazy ? value.length < resolvedTotalRecords : sortedRows.length > loadedPages * currentRows);
503
+ // Ordenar/filtrar/mudar o tamanho da pagina recomeca a rolagem infinita na primeira pagina.
504
+ const infiniteResetKey = JSON.stringify([currentSortField, currentSortOrder, currentGlobalFilter, currentFilters, currentRows]);
505
+ React.useEffect(() => {
506
+ setLoadedPages(1);
507
+ }, [infiniteResetKey]);
490
508
  const currentPage = Math.floor(safeFirst / currentRows) + 1;
491
509
  const paginatorRef = React.useRef(null);
492
510
  const [maxPageButtons, setMaxPageButtons] = React.useState(5);
@@ -547,6 +565,49 @@ function SgDatatableBase(props, imperativeRef) {
547
565
  lastReconciledFirstRef.current = safeFirst;
548
566
  commitPage(safeFirst, currentRows);
549
567
  }, [safeFirst, currentFirst, commitPage, currentRows]);
568
+ const sentinelRef = React.useRef(null);
569
+ const onLoadMoreRef = React.useRef(onLoadMore);
570
+ onLoadMoreRef.current = onLoadMore;
571
+ const requestedLengthRef = React.useRef(-1);
572
+ const loadMore = React.useCallback(() => {
573
+ if (!infiniteHasMore || loading)
574
+ return;
575
+ const total = resolvedTotalRecordsRef.current;
576
+ const pageCountNow = Math.max(1, Math.ceil(Math.max(total, 1) / currentRows));
577
+ let nextFirst;
578
+ if (lazy) {
579
+ if (requestedLengthRef.current === value.length)
580
+ return;
581
+ requestedLengthRef.current = value.length;
582
+ nextFirst = value.length;
583
+ }
584
+ else {
585
+ nextFirst = loadedPages * currentRows;
586
+ setLoadedPages(loadedPages + 1);
587
+ }
588
+ const event = {
589
+ first: nextFirst,
590
+ rows: currentRows,
591
+ page: Math.floor(nextFirst / currentRows) + 1,
592
+ pageCount: pageCountNow,
593
+ totalRecords: total
594
+ };
595
+ onPageRef.current?.(event);
596
+ onLoadMoreRef.current?.(event);
597
+ }, [infiniteHasMore, loading, lazy, value.length, loadedPages, currentRows]);
598
+ React.useEffect(() => {
599
+ if (!infiniteHasMore)
600
+ return;
601
+ const element = sentinelRef.current;
602
+ if (!element || typeof IntersectionObserver === "undefined")
603
+ return;
604
+ const observer = new IntersectionObserver((entries) => {
605
+ if (entries.some((entry) => entry.isIntersecting))
606
+ loadMore();
607
+ }, { rootMargin: "200px" });
608
+ observer.observe(element);
609
+ return () => observer.disconnect();
610
+ }, [infiniteHasMore, loadMore, pagedRows.length]);
550
611
  const commitSort = React.useCallback((nextSortField, nextSortOrder) => {
551
612
  if (!isSortFieldControlled)
552
613
  setInternalSortField(nextSortField);
@@ -603,6 +664,8 @@ function SgDatatableBase(props, imperativeRef) {
603
664
  }), [sortedRows, pagedRows, clearFilters, clearSelection, resetSort, commitPage, currentRows, goToPage]);
604
665
  const emptyLabel = emptyMessage ?? t(i18n, "components.datatable.empty");
605
666
  const loadingLabel = t(i18n, "components.datatable.loading");
667
+ const loadingMoreLabel = t(i18n, "components.datatable.loadingMore");
668
+ const endOfListLabel = t(i18n, "components.datatable.endOfList");
606
669
  const globalFilterLabel = globalFilterPlaceholder ?? t(i18n, "components.datatable.globalFilterPlaceholder");
607
670
  const clearFiltersLabel = t(i18n, "components.datatable.clearFilters");
608
671
  const firstLabel = t(i18n, "components.datatable.first");
@@ -718,8 +781,37 @@ function SgDatatableBase(props, imperativeRef) {
718
781
  return next;
719
782
  });
720
783
  }, [columnDescriptors]);
784
+ const renderCellContent = (column, rowData, absoluteRowIndex) => {
785
+ const cellField = column.field;
786
+ const cellValue = getFieldValue(rowData, cellField);
787
+ return column.body ? (column.body(rowData, {
788
+ rowData,
789
+ rowIndex: absoluteRowIndex,
790
+ field: cellField,
791
+ value: cellValue
792
+ })) : (_jsx("span", { children: cellValue === null || cellValue === undefined ? "-" : String(cellValue) }));
793
+ };
794
+ // Modo cartoes: o titulo e' a primeira coluna marcada com cardTitle; as demais viram "rotulo: valor",
795
+ // exceto as marcadas com cardHidden.
796
+ const cardTitleColumn = cardsActive ? visibleColumns.find((column) => column.cardTitle) : undefined;
797
+ const cardBodyColumns = cardsActive
798
+ ? visibleColumns.filter((column) => column !== cardTitleColumn && !column.cardHidden)
799
+ : [];
800
+ const cardFilterColumns = cardsActive
801
+ ? visibleColumns.filter((column) => column.filter && (column.filterField ?? column.field))
802
+ : [];
803
+ const cardSortColumns = cardsActive
804
+ ? visibleColumns.filter((column) => column.sortable && (column.sortField ?? column.field))
805
+ : [];
806
+ const cardHeaderLabel = (column) => typeof column.header === "string" || typeof column.header === "number"
807
+ ? String(column.header)
808
+ : column.field ?? "";
809
+ const sortByLabel = resolveMessage(t(i18n, "components.datatable.sortBy"), "components.datatable.sortBy", "Ordenar por");
810
+ const sortNoneLabel = resolveMessage(t(i18n, "components.datatable.sortNone"), "components.datatable.sortNone", "Sem ordem");
811
+ const sortDirectionLabel = resolveMessage(t(i18n, "components.datatable.sortDirection"), "components.datatable.sortDirection", "Inverter ordem");
812
+ const filtersPanelLabel = resolveMessage(t(i18n, "components.datatable.filters"), "components.datatable.filters", "Filtros");
721
813
  const resolvedTitle = (groupBoxProps.title ?? title ?? "").trim() || " ";
722
- return (_jsx("div", { className: className, style: style, children: _jsx(SgGroupBox, { ...groupBoxProps, title: resolvedTitle, children: _jsxs("div", { className: "space-y-3", children: [shouldShowToolbar ? (_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [showGlobalFilter ? (_jsx("input", { value: currentGlobalFilter, onChange: (event) => commitFilters(currentFilters, event.target.value), placeholder: globalFilterLabel, className: "min-w-[220px] flex-1 rounded-md border border-[rgb(var(--sg-border))] bg-[rgb(var(--sg-input-bg,var(--sg-surface)))] px-3 py-2 text-sm text-[rgb(var(--sg-input-fg,var(--sg-text)))] placeholder:text-[rgb(var(--sg-input-placeholder,var(--sg-muted)))] outline-none focus:ring-2 focus:ring-[rgb(var(--sg-ring))]" })) : null, showClearFiltersButton ? (_jsx(SgButton, { size: "sm", appearance: "outline", onClick: clearFilters, children: clearFiltersLabel })) : null] })) : null, _jsxs("div", { className: "relative", ref: columnManagerRef, children: [showColumnManager ? (_jsx("div", { className: "mb-2 flex items-center justify-start", children: _jsx("button", { ref: columnManagerButtonRef, type: "button", "aria-label": resolvedColumnManagerLabel, title: resolvedColumnManagerLabel, onClick: () => setIsColumnManagerOpen((prev) => !prev), className: "inline-flex h-9 w-9 items-center justify-center rounded-md border border-[rgb(var(--sg-border))] bg-[rgb(var(--sg-surface))] text-[rgb(var(--sg-primary-600))] shadow-sm transition hover:bg-[rgb(var(--sg-primary-50))] hover:text-[rgb(var(--sg-primary-700))]", children: _jsx(SlidersHorizontal, { size: 18 }) }) })) : null, showColumnManager && isColumnManagerOpen && columnManagerPosition && typeof document !== "undefined"
814
+ return (_jsx("div", { ref: rootRef, className: className, style: style, children: _jsx(SgGroupBox, { ...groupBoxProps, title: resolvedTitle, children: _jsxs("div", { className: "space-y-3", children: [shouldShowToolbar ? (_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [showGlobalFilter ? (_jsx("input", { value: currentGlobalFilter, onChange: (event) => commitFilters(currentFilters, event.target.value), placeholder: globalFilterLabel, className: "min-w-[220px] flex-1 rounded-md border border-[rgb(var(--sg-border))] bg-[rgb(var(--sg-input-bg,var(--sg-surface)))] px-3 py-2 text-sm text-[rgb(var(--sg-input-fg,var(--sg-text)))] placeholder:text-[rgb(var(--sg-input-placeholder,var(--sg-muted)))] outline-none focus:ring-2 focus:ring-[rgb(var(--sg-ring))]" })) : null, showClearFiltersButton ? (_jsx(SgButton, { size: "sm", appearance: "outline", onClick: clearFilters, children: clearFiltersLabel })) : null] })) : null, _jsxs("div", { className: "relative", ref: columnManagerRef, children: [showColumnManager ? (_jsx("div", { className: "mb-2 flex items-center justify-start", children: _jsx("button", { ref: columnManagerButtonRef, type: "button", "aria-label": resolvedColumnManagerLabel, title: resolvedColumnManagerLabel, onClick: () => setIsColumnManagerOpen((prev) => !prev), className: "inline-flex h-9 w-9 items-center justify-center rounded-md border border-[rgb(var(--sg-border))] bg-[rgb(var(--sg-surface))] text-[rgb(var(--sg-primary-600))] shadow-sm transition hover:bg-[rgb(var(--sg-primary-50))] hover:text-[rgb(var(--sg-primary-700))]", children: _jsx(SlidersHorizontal, { size: 18 }) }) })) : null, showColumnManager && isColumnManagerOpen && columnManagerPosition && typeof document !== "undefined"
723
815
  ? createPortal(_jsxs("div", { ref: columnManagerPanelRef, className: "fixed z-[1400] w-[300px] rounded-xl border border-[rgb(var(--sg-border))] bg-[rgb(var(--sg-surface))] shadow-xl", style: {
724
816
  top: columnManagerPosition.top,
725
817
  left: columnManagerPosition.left
@@ -749,7 +841,40 @@ function SgDatatableBase(props, imperativeRef) {
749
841
  ? "text-[rgb(var(--sg-muted))]"
750
842
  : "text-[rgb(var(--sg-text))]", !canHideThisColumn ? "cursor-not-allowed" : ""), children: columnLabel }), _jsx("div", { className: cn("inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-[rgb(var(--sg-muted))]", isReorderable ? "cursor-grab active:cursor-grabbing" : "opacity-40"), title: dragHandleLabel, children: _jsx(GripVertical, { size: 16 }) })] }, `manager-${item.key}`));
751
843
  }) }) }), _jsxs("div", { className: "flex items-center gap-2 border-t border-[rgb(var(--sg-border))] px-3 py-3", children: [_jsx(SgButton, { size: "sm", appearance: "outline", onClick: restoreDefaultColumns, children: restoreColumnsLabel }), _jsx(SgButton, { size: "sm", appearance: "outline", onClick: showAllColumns, children: showAllColumnsLabel })] })] }), document.body)
752
- : null, _jsx("div", { className: "overflow-x-auto rounded-lg border border-[rgb(var(--sg-border))] bg-[rgb(var(--sg-surface))]", children: _jsxs("table", { id: id, className: cn("min-w-full border-collapse text-sm", tableClassName), children: [_jsxs("thead", { children: [_jsx("tr", { className: "bg-[rgb(var(--sg-primary-50))]", children: visibleColumns.map((column, columnIndex) => {
844
+ : null, cardsActive ? (_jsxs("div", { "data-sg-datatable-cards": "true", className: "space-y-2", children: [cardSortColumns.length > 0 || (showColumnFilters && cardFilterColumns.length > 0) ? (_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [cardSortColumns.length > 0 ? (_jsxs(_Fragment, { children: [_jsxs("select", { "aria-label": sortByLabel, value: currentSortOrder === 0 || !currentSortField ? "" : currentSortField, onChange: (event) => {
845
+ const next = event.target.value;
846
+ if (!next)
847
+ commitSort(null, 0);
848
+ else
849
+ commitSort(next, 1);
850
+ }, className: "min-w-0 flex-1 rounded-md border border-[rgb(var(--sg-border))] bg-[rgb(var(--sg-input-bg,var(--sg-surface)))] px-2 py-2 text-sm text-[rgb(var(--sg-input-fg,var(--sg-text)))]", children: [_jsx("option", { value: "", children: sortNoneLabel }), cardSortColumns.map((column, columnIndex) => {
851
+ const key = (column.sortField ?? column.field);
852
+ return (_jsx("option", { value: key, children: cardHeaderLabel(column) }, `sort-${column.columnId ?? key}-${columnIndex}`));
853
+ })] }), _jsx(SgButton, { size: "sm", appearance: "outline", iconOnly: true, "aria-label": sortDirectionLabel, title: sortDirectionLabel, disabled: !currentSortField || currentSortOrder === 0, onClick: () => {
854
+ if (!currentSortField)
855
+ return;
856
+ commitSort(currentSortField, currentSortOrder === 1 ? -1 : 1);
857
+ }, leftIcon: currentSortOrder === -1 ? _jsx(ArrowDown, { size: 16 }) : _jsx(ArrowUp, { size: 16 }) })] })) : null, showColumnFilters && cardFilterColumns.length > 0 ? (_jsx(SgButton, { size: "sm", appearance: "outline", "aria-expanded": isCardFiltersOpen, onClick: () => setIsCardFiltersOpen((prev) => !prev), children: filtersPanelLabel })) : null] })) : null, showColumnFilters && isCardFiltersOpen && cardFilterColumns.length > 0 ? (_jsx("div", { "data-sg-datatable-filters-panel": "true", className: "space-y-2 rounded-lg border border-[rgb(var(--sg-border))] bg-[rgb(var(--sg-surface))] p-3", children: cardFilterColumns.map((column, columnIndex) => {
858
+ const filterField = (column.filterField ?? column.field);
859
+ const filterValue = currentFilters[filterField] ?? "";
860
+ return (_jsxs("label", { className: "block space-y-1", children: [_jsx("span", { className: "text-xs font-semibold text-[rgb(var(--sg-muted))]", children: column.header }), _jsx("input", { value: filterValue, onChange: (event) => {
861
+ const normalized = normalizeFilters({ ...currentFilters, [filterField]: event.target.value });
862
+ commitFilters(normalized, currentGlobalFilter);
863
+ }, placeholder: column.filterPlaceholder ?? t(i18n, "components.datatable.columnFilterPlaceholder", { column: String(column.header ?? "") }), className: "w-full rounded-md border border-[rgb(var(--sg-border))] bg-[rgb(var(--sg-input-bg,var(--sg-surface)))] px-3 py-2 text-sm text-[rgb(var(--sg-input-fg,var(--sg-text)))] placeholder:text-[rgb(var(--sg-input-placeholder,var(--sg-muted)))] outline-none focus:ring-2 focus:ring-[rgb(var(--sg-ring))]" })] }, `cardfilter-${column.columnId ?? filterField}-${columnIndex}`));
864
+ }) })) : null, loading && !(infiniteActive && pagedRows.length > 0) ? (_jsx("p", { "data-sg-card-state": "loading", className: "px-4 py-6 text-center text-sm text-[rgb(var(--sg-muted))]", children: loadingLabel })) : pagedRows.length === 0 ? (_jsx("p", { "data-sg-card-state": "empty", className: "px-4 py-6 text-center text-sm text-[rgb(var(--sg-muted))]", children: emptyLabel })) : (_jsx("ul", { role: "list", className: "space-y-2", children: pagedRows.map((rowData, rowIndex) => {
865
+ const absoluteRowIndex = safeFirst + rowIndex;
866
+ const selected = isRowSelected(rowData);
867
+ const customRowClass = typeof rowClassName === "function" ? rowClassName(rowData, absoluteRowIndex) : rowClassName;
868
+ return (_jsxs("li", { "data-sg-card": "true", "aria-selected": selected || undefined, onClick: (event) => {
869
+ const target = event.target;
870
+ if (target.closest("button,a,input,select,textarea,label,[data-sg-stop-row-select='true']")) {
871
+ return;
872
+ }
873
+ handleRowSelection(rowData);
874
+ }, className: cn("rounded-lg border border-[rgb(var(--sg-border))] bg-[rgb(var(--sg-surface))] p-3", selectionMode ? "cursor-pointer" : "", stripedRows && absoluteRowIndex % 2 === 1 ? "bg-[rgb(var(--sg-primary-50))]" : "", selected ? "border-[rgb(var(--sg-primary-600))] bg-[rgb(var(--sg-primary-100))]" : "", customRowClass), children: [cardTitleColumn ? (_jsx("div", { "data-sg-card-title": "true", className: "mb-2 text-base font-semibold text-[rgb(var(--sg-text))]", children: renderCellContent(cardTitleColumn, rowData, absoluteRowIndex) })) : null, _jsx("dl", { className: "grid grid-cols-[auto_minmax(0,1fr)] gap-x-3 gap-y-1 text-sm", children: cardBodyColumns.map((column, columnIndex) => (_jsxs(React.Fragment, { children: [_jsx("dt", { className: "font-semibold text-[rgb(var(--sg-muted))]", children: column.header }), _jsx("dd", { className: cn("min-w-0 break-words text-[rgb(var(--sg-text))]", column.className), children: renderCellContent(column, rowData, absoluteRowIndex) })] }, `card-${column.columnId ?? column.field ?? columnIndex}-${absoluteRowIndex}`))) })] }, String(getRowIdentity(rowData, dataKey) ?? absoluteRowIndex)));
875
+ }) })), hasFooter ? (_jsx("dl", { "data-sg-card-footer": "true", className: "grid grid-cols-[auto_minmax(0,1fr)] gap-x-3 gap-y-1 rounded-lg bg-[rgb(var(--sg-primary-50))] p-3 text-sm font-semibold text-[rgb(var(--sg-text))]", children: visibleColumns
876
+ .filter((column) => column.footer !== undefined && !column.cardHidden)
877
+ .map((column, columnIndex) => (_jsxs(React.Fragment, { children: [_jsx("dt", { children: column.header }), _jsx("dd", { children: typeof column.footer === "function" ? column.footer(sortedRows) : column.footer })] }, `cardfoot-${column.columnId ?? column.field ?? columnIndex}`))) })) : null] })) : (_jsx("div", { className: "overflow-x-auto rounded-lg border border-[rgb(var(--sg-border))] bg-[rgb(var(--sg-surface))]", children: _jsxs("table", { id: id, className: cn("min-w-full border-collapse text-sm", tableClassName), children: [_jsxs("thead", { children: [_jsx("tr", { className: "bg-[rgb(var(--sg-primary-50))]", children: visibleColumns.map((column, columnIndex) => {
753
878
  const sortCandidate = column.sortField ?? column.field;
754
879
  const isSorted = Boolean(sortCandidate) && sortCandidate === currentSortField;
755
880
  // O indicador de ordenacao era TEXTO literal (" ^", " v", " <>"), entao o
@@ -782,24 +907,16 @@ function SgDatatableBase(props, imperativeRef) {
782
907
  handleRowSelection(rowData);
783
908
  }, className: cn(selectionMode ? "cursor-pointer" : "", stripedRows && absoluteRowIndex % 2 === 1 ? "bg-[rgb(var(--sg-primary-50))]" : "", hoverableRows ? "hover:bg-[rgb(var(--sg-primary-100))]" : "", selected ? "bg-[rgb(var(--sg-primary-100))]" : "", customRowClass), children: visibleColumns.map((column, columnIndex) => {
784
909
  const cellField = column.field;
785
- const cellValue = getFieldValue(rowData, cellField);
786
910
  const customCellClass = typeof column.bodyClassName === "function"
787
911
  ? column.bodyClassName(rowData, absoluteRowIndex)
788
912
  : column.bodyClassName;
789
- const renderedValue = column.body
790
- ? column.body(rowData, {
791
- rowData,
792
- rowIndex: absoluteRowIndex,
793
- field: cellField,
794
- value: cellValue
795
- })
796
- : (_jsx("span", { children: cellValue === null || cellValue === undefined ? "-" : String(cellValue) }));
913
+ const renderedValue = renderCellContent(column, rowData, absoluteRowIndex);
797
914
  return (_jsx("td", { className: cn("px-3 py-2 text-[rgb(var(--sg-text))]", resolveAlignmentClass(column.align), showGridlines ? "border-b border-r border-[rgb(var(--sg-border))] last:border-r-0" : "border-b border-[rgb(var(--sg-border))]", column.className, customCellClass), style: { width: toCssSize(column.width), minWidth: toCssSize(column.minWidth) }, children: renderedValue }, `cell-${column.columnId ?? cellField ?? columnIndex}-${absoluteRowIndex}`));
798
915
  }) }, String(getRowIdentity(rowData, dataKey) ?? absoluteRowIndex)));
799
916
  })) }), hasFooter ? (_jsx("tfoot", { children: _jsx("tr", { className: "bg-[rgb(var(--sg-primary-50))]", children: visibleColumns.map((column, columnIndex) => {
800
917
  const footerValue = typeof column.footer === "function" ? column.footer(sortedRows) : column.footer;
801
918
  return (_jsx("td", { className: cn("px-3 py-2 font-semibold text-[rgb(var(--sg-text))]", resolveAlignmentClass(column.align), showGridlines ? "border-t border-r border-[rgb(var(--sg-border))] last:border-r-0" : "border-t border-[rgb(var(--sg-border))]", column.className), style: { width: toCssSize(column.width), minWidth: toCssSize(column.minWidth) }, children: footerValue ?? null }, `foot-${column.columnId ?? column.field ?? columnIndex}`));
802
- }) }) })) : null] }) })] }), paginator ? (_jsxs("div", { ref: paginatorRef, className: "flex flex-wrap items-center gap-1.5", children: [_jsxs("nav", { "aria-label": t(i18n, "components.datatable.pagination"), className: "flex flex-wrap items-center gap-1.5", children: [_jsx(SgButton, { size: "sm", appearance: "outline", shape: "default", iconOnly: true, "aria-label": firstLabel, title: firstLabel, disabled: currentPage <= 1, onClick: () => goToPage(1), leftIcon: _jsx(ChevronFirst, { size: 16 }) }), _jsx(SgButton, { size: "sm", appearance: "outline", shape: "default", iconOnly: true, "aria-label": prevLabel, title: prevLabel, disabled: currentPage <= 1, onClick: () => goToPage(currentPage - 1), leftIcon: _jsx(ChevronLeft, { size: 16 }) }), pageTokens.map((token, tokenIndex) => {
919
+ }) }) })) : null] }) }))] }), infiniteActive ? (_jsx("div", { ref: sentinelRef, "data-sg-infinite-sentinel": "true", role: "status", "aria-live": "polite", className: "px-4 py-3 text-center text-xs text-[rgb(var(--sg-muted))]", children: infiniteHasMore ? (loading ? loadingMoreLabel : "") : pagedRows.length > 0 ? endOfListLabel : "" })) : null, paginator && !infiniteActive ? (_jsxs("div", { ref: paginatorRef, className: "flex flex-wrap items-center gap-1.5", children: [_jsxs("nav", { "aria-label": t(i18n, "components.datatable.pagination"), className: "flex flex-wrap items-center gap-1.5", children: [_jsx(SgButton, { size: "sm", appearance: "outline", shape: "default", iconOnly: true, "aria-label": firstLabel, title: firstLabel, disabled: currentPage <= 1, onClick: () => goToPage(1), leftIcon: _jsx(ChevronFirst, { size: 16 }) }), _jsx(SgButton, { size: "sm", appearance: "outline", shape: "default", iconOnly: true, "aria-label": prevLabel, title: prevLabel, disabled: currentPage <= 1, onClick: () => goToPage(currentPage - 1), leftIcon: _jsx(ChevronLeft, { size: 16 }) }), pageTokens.map((token, tokenIndex) => {
803
920
  if (token.type === "ellipsis") {
804
921
  const ellipsisLabel = token.direction === "backward" ? jumpBackwardLabel : jumpForwardLabel;
805
922
  return (_jsx(SgButton, { size: "sm", appearance: "ghost", shape: "default", iconOnly: true, "aria-label": ellipsisLabel, title: ellipsisLabel, onClick: () => goToPage(token.target), leftIcon: _jsx(MoreHorizontal, { size: 16 }) }, `ellipsis-${token.direction}-${tokenIndex}`));
@@ -1 +1 @@
1
- {"version":3,"file":"SgDatatable.meta.d.ts","sourceRoot":"","sources":["../../src/inputs/SgDatatable.meta.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAE9D,eAAO,MAAM,MAAM,EAAE,QA8BpB,CAAC;AAEF,eAAO,MAAM,OAAO,EAAE,WAmBrB,CAAC"}
1
+ {"version":3,"file":"SgDatatable.meta.d.ts","sourceRoot":"","sources":["../../src/inputs/SgDatatable.meta.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAE9D,eAAO,MAAM,MAAM,EAAE,QAiCpB,CAAC;AAEF,eAAO,MAAM,OAAO,EAAE,WAoBrB,CAAC"}
@@ -7,9 +7,9 @@ export const sgMeta = {
7
7
  displayName: "SgDatatable",
8
8
  category: "data",
9
9
  subcategory: "datatable",
10
- description: "Tabela de dados interativa com colunas configuraveis, paginacao, ordenacao, filtro, selecao e persistencia de layout de colunas.",
10
+ description: "Tabela de dados interativa com colunas configuraveis, paginacao, ordenacao, filtro, selecao e persistencia de layout de colunas. Em celular vira lista de cartoes automaticamente (mobileMode omitido = automatico).",
11
11
  tags: ["data", "table", "grid", "listing", "interactive"],
12
- capabilities: ["pagination", "sorting", "filtering", "selection", "column-config", "column-layout-persistence", "row-actions"],
12
+ capabilities: ["pagination", "sorting", "filtering", "selection", "column-config", "column-layout-persistence", "row-actions", "mobile-cards"],
13
13
  fieldSemantics: ["tabularData", "dataGrid", "listing", "collectionView"],
14
14
  props: [
15
15
  { name: "id", type: "string", description: "Identificador da tabela usado para DOM e persistencia do layout de colunas.", semanticRole: "data", bindable: true },
@@ -17,6 +17,9 @@ export const sgMeta = {
17
17
  { name: "rows", type: "number", description: "Linhas por pagina quando a paginacao esta ativa (os dados vao em value).", semanticRole: "behavior", bindable: true },
18
18
  { name: "selectionMode", type: "\"single\" | \"multiple\"", description: "Modo de selecao de linhas.", semanticRole: "behavior", bindable: true },
19
19
  { name: "loading", type: "boolean", default: false, description: "Indica estado de carregamento.", semanticRole: "appearance", bindable: true },
20
+ { name: "mobileMode", type: "\"scroll\" | \"cards\"", default: "(omitido = automatico)", description: "Comportamento em tela estreita. Omitido (padrao): cartoes quando o container fica abaixo de 600 px, tabela acima; cards: forca cartoes em qualquer largura; scroll: forca a tabela com rolagem horizontal. Nos cartoes mantem ordenacao, selecao, acoes, rodape e paginacao.", semanticRole: "behavior", bindable: true },
21
+ { name: "infiniteScroll", type: "boolean", default: "(true nos cartoes, false na tabela)", description: "Rolagem infinita: sem paginador, ao chegar perto do fim da lista avanca a pagina (rows) e acrescenta os proximos registros, com indicador de carregando e de fim da lista. Padrao ligado quando a lista e exibida como cartoes (abaixo de 600 px no modo automatico) e desligado na tabela. Exige paginator. Com lazy, o pai acrescenta a proxima pagina a value (first/rows/page vem em onPage/onLoadMore).", semanticRole: "behavior", bindable: true },
22
+ { name: "onLoadMore", type: "(event: SgDatatablePageEvent) => void", description: "Chamado (alem de onPage) quando a rolagem infinita pede a proxima pagina.", semanticRole: "event", bindable: false },
20
23
  { name: "paginator", type: "boolean", default: false, description: "Ativa paginacao.", semanticRole: "behavior", bindable: true },
21
24
  { name: "onPage", type: "(event: SgDatatablePageEvent) => void", description: "Callback de paginacao.", semanticRole: "event", bindable: false },
22
25
  { name: "onSort", type: "(event: SgDatatableSortEvent) => void", description: "Callback de ordenacao.", semanticRole: "event", bindable: false }
@@ -34,14 +37,15 @@ export const aiHints = {
34
37
  preferredUseCases: [
35
38
  "Exibicao de colecoes tabulares com interacao.",
36
39
  "Listagens administrativas com filtro, ordenacao e pagina.",
37
- "Tabelas com acoes por linha e selecao."
40
+ "Tabelas com acoes por linha e selecao.",
41
+ "Listagens que precisam funcionar em celular: por padrao (mobileMode omitido) a tabela vira cartoes abaixo de 600 px; use \"scroll\" para forcar a tabela."
38
42
  ],
39
43
  avoidUseCases: [
40
44
  "Formularios de entrada simples.",
41
45
  "Colecoes muito visuais em formato de cards.",
42
46
  "Dados pequenos onde uma lista simples basta."
43
47
  ],
44
- synonyms: ["datatable", "data grid", "table", "listing grid"],
48
+ synonyms: ["datatable", "tabela no celular", "tabela responsiva", "data grid", "table", "listing grid", "tabela mobile", "lista de cartoes", "cards no celular", "responsive table", "mobile table"],
45
49
  relatedEntityFields: ["rows", "records", "items", "results", "tableData"],
46
50
  compositionHints: [
47
51
  "Combinar com filtros acima da tabela usando inputs de busca e selecao.",
@@ -0,0 +1,13 @@
1
+ import React from "react";
2
+ /**
3
+ * Mede a largura (px) do elemento observado e acompanha o redimensionamento.
4
+ *
5
+ * - Devolve `null` enquanto a largura nao e conhecida (SSR, antes da primeira medicao,
6
+ * elemento oculto/sem layout). Quem consome deve tratar `null` como "nao estreito",
7
+ * para nunca trocar o layout no servidor nem piscar na hidratacao.
8
+ * - Mede o CONTAINER (e nao a janela), como o `LayoutBuilder` do Flutter: a tabela dentro de
9
+ * um painel estreito num desktop tambem e considerada estreita.
10
+ * - `enabled=false` nao observa nada (custo zero para quem nao usa).
11
+ */
12
+ export declare function useSgContainerWidth(ref: React.RefObject<HTMLElement | null>, enabled?: boolean): number | null;
13
+ //# sourceMappingURL=useSgContainerWidth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useSgContainerWidth.d.ts","sourceRoot":"","sources":["../../src/inputs/useSgContainerWidth.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,MAAM,OAAO,CAAC;AAI1B;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,KAAK,CAAC,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,EACxC,OAAO,UAAO,GACb,MAAM,GAAG,IAAI,CAiCf"}
@@ -0,0 +1,42 @@
1
+ "use client";
2
+ import React from "react";
3
+ const useIsomorphicLayoutEffect = typeof window === "undefined" ? React.useEffect : React.useLayoutEffect;
4
+ /**
5
+ * Mede a largura (px) do elemento observado e acompanha o redimensionamento.
6
+ *
7
+ * - Devolve `null` enquanto a largura nao e conhecida (SSR, antes da primeira medicao,
8
+ * elemento oculto/sem layout). Quem consome deve tratar `null` como "nao estreito",
9
+ * para nunca trocar o layout no servidor nem piscar na hidratacao.
10
+ * - Mede o CONTAINER (e nao a janela), como o `LayoutBuilder` do Flutter: a tabela dentro de
11
+ * um painel estreito num desktop tambem e considerada estreita.
12
+ * - `enabled=false` nao observa nada (custo zero para quem nao usa).
13
+ */
14
+ export function useSgContainerWidth(ref, enabled = true) {
15
+ const [width, setWidth] = React.useState(null);
16
+ useIsomorphicLayoutEffect(() => {
17
+ if (!enabled) {
18
+ setWidth((prev) => (prev === null ? prev : null));
19
+ return;
20
+ }
21
+ const element = ref.current;
22
+ if (!element)
23
+ return;
24
+ const measure = () => {
25
+ const next = element.getBoundingClientRect().width;
26
+ const resolved = next > 0 ? Math.round(next) : null;
27
+ setWidth((prev) => (prev === resolved ? prev : resolved));
28
+ };
29
+ measure();
30
+ if (typeof ResizeObserver !== "undefined") {
31
+ const observer = new ResizeObserver(measure);
32
+ observer.observe(element);
33
+ return () => observer.disconnect();
34
+ }
35
+ if (typeof window !== "undefined") {
36
+ window.addEventListener("resize", measure);
37
+ return () => window.removeEventListener("resize", measure);
38
+ }
39
+ return;
40
+ }, [ref, enabled]);
41
+ return width;
42
+ }