@seedgrid/fe-components 2026.10.3 → 2026.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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, 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 = "scroll" } = props;
227
230
  const isFirstControlled = first !== undefined;
228
231
  const isSortFieldControlled = sortField !== undefined;
229
232
  const isSortOrderControlled = sortOrder !== undefined;
@@ -237,6 +240,10 @@ 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 === "cards");
245
+ const cardsActive = mobileMode === "cards" && containerWidth !== null && containerWidth < SG_DATATABLE_MOBILE_BREAKPOINT;
246
+ const [isCardFiltersOpen, setIsCardFiltersOpen] = React.useState(false);
240
247
  const columnManagerRef = React.useRef(null);
241
248
  const columnManagerButtonRef = React.useRef(null);
242
249
  const columnManagerPanelRef = React.useRef(null);
@@ -718,8 +725,37 @@ function SgDatatableBase(props, imperativeRef) {
718
725
  return next;
719
726
  });
720
727
  }, [columnDescriptors]);
728
+ const renderCellContent = (column, rowData, absoluteRowIndex) => {
729
+ const cellField = column.field;
730
+ const cellValue = getFieldValue(rowData, cellField);
731
+ return column.body ? (column.body(rowData, {
732
+ rowData,
733
+ rowIndex: absoluteRowIndex,
734
+ field: cellField,
735
+ value: cellValue
736
+ })) : (_jsx("span", { children: cellValue === null || cellValue === undefined ? "-" : String(cellValue) }));
737
+ };
738
+ // Modo cartoes: o titulo e' a primeira coluna marcada com cardTitle; as demais viram "rotulo: valor",
739
+ // exceto as marcadas com cardHidden.
740
+ const cardTitleColumn = cardsActive ? visibleColumns.find((column) => column.cardTitle) : undefined;
741
+ const cardBodyColumns = cardsActive
742
+ ? visibleColumns.filter((column) => column !== cardTitleColumn && !column.cardHidden)
743
+ : [];
744
+ const cardFilterColumns = cardsActive
745
+ ? visibleColumns.filter((column) => column.filter && (column.filterField ?? column.field))
746
+ : [];
747
+ const cardSortColumns = cardsActive
748
+ ? visibleColumns.filter((column) => column.sortable && (column.sortField ?? column.field))
749
+ : [];
750
+ const cardHeaderLabel = (column) => typeof column.header === "string" || typeof column.header === "number"
751
+ ? String(column.header)
752
+ : column.field ?? "";
753
+ const sortByLabel = resolveMessage(t(i18n, "components.datatable.sortBy"), "components.datatable.sortBy", "Ordenar por");
754
+ const sortNoneLabel = resolveMessage(t(i18n, "components.datatable.sortNone"), "components.datatable.sortNone", "Sem ordem");
755
+ const sortDirectionLabel = resolveMessage(t(i18n, "components.datatable.sortDirection"), "components.datatable.sortDirection", "Inverter ordem");
756
+ const filtersPanelLabel = resolveMessage(t(i18n, "components.datatable.filters"), "components.datatable.filters", "Filtros");
721
757
  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"
758
+ 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
759
  ? 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
760
  top: columnManagerPosition.top,
725
761
  left: columnManagerPosition.left
@@ -749,7 +785,40 @@ function SgDatatableBase(props, imperativeRef) {
749
785
  ? "text-[rgb(var(--sg-muted))]"
750
786
  : "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
787
  }) }) }), _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) => {
788
+ : 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) => {
789
+ const next = event.target.value;
790
+ if (!next)
791
+ commitSort(null, 0);
792
+ else
793
+ commitSort(next, 1);
794
+ }, 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) => {
795
+ const key = (column.sortField ?? column.field);
796
+ return (_jsx("option", { value: key, children: cardHeaderLabel(column) }, `sort-${column.columnId ?? key}-${columnIndex}`));
797
+ })] }), _jsx(SgButton, { size: "sm", appearance: "outline", iconOnly: true, "aria-label": sortDirectionLabel, title: sortDirectionLabel, disabled: !currentSortField || currentSortOrder === 0, onClick: () => {
798
+ if (!currentSortField)
799
+ return;
800
+ commitSort(currentSortField, currentSortOrder === 1 ? -1 : 1);
801
+ }, 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) => {
802
+ const filterField = (column.filterField ?? column.field);
803
+ const filterValue = currentFilters[filterField] ?? "";
804
+ 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) => {
805
+ const normalized = normalizeFilters({ ...currentFilters, [filterField]: event.target.value });
806
+ commitFilters(normalized, currentGlobalFilter);
807
+ }, 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}`));
808
+ }) })) : null, loading ? (_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) => {
809
+ const absoluteRowIndex = safeFirst + rowIndex;
810
+ const selected = isRowSelected(rowData);
811
+ const customRowClass = typeof rowClassName === "function" ? rowClassName(rowData, absoluteRowIndex) : rowClassName;
812
+ return (_jsxs("li", { "data-sg-card": "true", "aria-selected": selected || undefined, onClick: (event) => {
813
+ const target = event.target;
814
+ if (target.closest("button,a,input,select,textarea,label,[data-sg-stop-row-select='true']")) {
815
+ return;
816
+ }
817
+ handleRowSelection(rowData);
818
+ }, 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)));
819
+ }) })), 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
820
+ .filter((column) => column.footer !== undefined && !column.cardHidden)
821
+ .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
822
  const sortCandidate = column.sortField ?? column.field;
754
823
  const isSorted = Boolean(sortCandidate) && sortCandidate === currentSortField;
755
824
  // O indicador de ordenacao era TEXTO literal (" ^", " v", " <>"), entao o
@@ -782,24 +851,16 @@ function SgDatatableBase(props, imperativeRef) {
782
851
  handleRowSelection(rowData);
783
852
  }, 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
853
  const cellField = column.field;
785
- const cellValue = getFieldValue(rowData, cellField);
786
854
  const customCellClass = typeof column.bodyClassName === "function"
787
855
  ? column.bodyClassName(rowData, absoluteRowIndex)
788
856
  : 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) }));
857
+ const renderedValue = renderCellContent(column, rowData, absoluteRowIndex);
797
858
  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
859
  }) }, String(getRowIdentity(rowData, dataKey) ?? absoluteRowIndex)));
799
860
  })) }), hasFooter ? (_jsx("tfoot", { children: _jsx("tr", { className: "bg-[rgb(var(--sg-primary-50))]", children: visibleColumns.map((column, columnIndex) => {
800
861
  const footerValue = typeof column.footer === "function" ? column.footer(sortedRows) : column.footer;
801
862
  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) => {
863
+ }) }) })) : 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) => {
803
864
  if (token.type === "ellipsis") {
804
865
  const ellipsisLabel = token.direction === "backward" ? jumpBackwardLabel : jumpForwardLabel;
805
866
  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,QA+BpB,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 pode virar lista de cartoes (mobileMode=\"cards\").",
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,7 @@ 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: "scroll", description: "Comportamento em tela estreita (container abaixo de 600 px): scroll rola a tabela na horizontal; cards mostra cada linha como cartao 'coluna: valor' mantendo ordenacao, selecao, acoes, rodape e paginacao.", semanticRole: "behavior", bindable: true },
20
21
  { name: "paginator", type: "boolean", default: false, description: "Ativa paginacao.", semanticRole: "behavior", bindable: true },
21
22
  { name: "onPage", type: "(event: SgDatatablePageEvent) => void", description: "Callback de paginacao.", semanticRole: "event", bindable: false },
22
23
  { name: "onSort", type: "(event: SgDatatableSortEvent) => void", description: "Callback de ordenacao.", semanticRole: "event", bindable: false }
@@ -34,14 +35,15 @@ export const aiHints = {
34
35
  preferredUseCases: [
35
36
  "Exibicao de colecoes tabulares com interacao.",
36
37
  "Listagens administrativas com filtro, ordenacao e pagina.",
37
- "Tabelas com acoes por linha e selecao."
38
+ "Tabelas com acoes por linha e selecao.",
39
+ "Listagens que precisam funcionar em celular: mobileMode=\"cards\" mostra cada linha como cartao (tabela que se adapta ao celular)."
38
40
  ],
39
41
  avoidUseCases: [
40
42
  "Formularios de entrada simples.",
41
43
  "Colecoes muito visuais em formato de cards.",
42
44
  "Dados pequenos onde uma lista simples basta."
43
45
  ],
44
- synonyms: ["datatable", "data grid", "table", "listing grid"],
46
+ 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
47
  relatedEntityFields: ["rows", "records", "items", "results", "tableData"],
46
48
  compositionHints: [
47
49
  "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
+ }