@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.
- package/dist/ai/seedgrid-components.manifest.json +39 -4
- package/dist/generated/compiledStyles.d.ts +1 -1
- package/dist/generated/compiledStyles.d.ts.map +1 -1
- package/dist/generated/compiledStyles.js +1 -1
- package/dist/i18n/en-US.d.ts.map +1 -1
- package/dist/i18n/en-US.js +6 -0
- package/dist/i18n/es.d.ts.map +1 -1
- package/dist/i18n/es.js +6 -0
- package/dist/i18n/fr.d.ts.map +1 -1
- package/dist/i18n/fr.js +6 -0
- package/dist/i18n/pt-BR.d.ts.map +1 -1
- package/dist/i18n/pt-BR.js +6 -0
- package/dist/i18n/pt-PT.d.ts.map +1 -1
- package/dist/i18n/pt-PT.js +6 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/inputs/SgDatatable.d.ts +18 -0
- package/dist/inputs/SgDatatable.d.ts.map +1 -1
- package/dist/inputs/SgDatatable.js +132 -15
- package/dist/inputs/SgDatatable.meta.d.ts.map +1 -1
- package/dist/inputs/SgDatatable.meta.js +8 -4
- package/dist/inputs/useSgContainerWidth.d.ts +13 -0
- package/dist/inputs/useSgContainerWidth.d.ts.map +1 -0
- package/dist/inputs/useSgContainerWidth.js +42 -0
- package/dist/sandbox.cjs +43 -43
- package/dist/style.css +1 -1
- package/package.json +3 -3
|
@@ -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,
|
|
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
|
|
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,
|
|
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
|
+
}
|