analytica-frontend-lib
Version:
Repositório público dos componentes utilizados nas plataformas da Analytica Ensino
484 lines (482 loc) • 15.1 kB
JavaScript
import {
EmptyState_default
} from "./chunk-6WVHF7N3.mjs";
import {
SkeletonTable
} from "./chunk-NKVUUWCA.mjs";
import {
NoSearchResult_default
} from "./chunk-ZBEW4SPB.mjs";
import {
cn
} from "./chunk-53ICLDGS.mjs";
// src/components/Table/Table.tsx
import {
forwardRef,
useCallback,
useState,
useMemo,
useEffect,
useRef,
Children,
isValidElement
} from "react";
import { ArrowUpIcon } from "@phosphor-icons/react/dist/csr/ArrowUp";
import { ArrowDownIcon } from "@phosphor-icons/react/dist/csr/ArrowDown";
import { jsx, jsxs } from "react/jsx-runtime";
var getUrlKeys = (prefix) => ({
sortBy: prefix ? `${prefix}_sortBy` : "sortBy",
sortOrder: prefix ? `${prefix}_sortOrder` : "sortOrder",
/** Contrato antigo (`sort=ASC`), ainda lido para não quebrar links salvos. */
legacySort: prefix ? `${prefix}_sort` : "sort"
});
function useTableSort(data, options = {}) {
const {
syncWithUrl = false,
mode = "client",
urlKeyPrefix,
allowedColumns,
defaultSort,
onSortChange
} = options;
const urlKeys = useMemo(() => getUrlKeys(urlKeyPrefix), [urlKeyPrefix]);
const allowedColumnsRef = useRef(allowedColumns);
allowedColumnsRef.current = allowedColumns;
const onSortChangeRef = useRef(onSortChange);
onSortChangeRef.current = onSortChange;
const defaultSortRef = useRef(defaultSort);
defaultSortRef.current = defaultSort;
const readFromUrl = useCallback(() => {
const fallback = defaultSortRef.current ? {
column: defaultSortRef.current.sortBy,
direction: defaultSortRef.current.sortOrder
} : { column: null, direction: null };
if (!syncWithUrl || globalThis.window === void 0) {
return fallback;
}
const params = new URLSearchParams(globalThis.location.search);
const column = params.get(urlKeys.sortBy);
if (!column) return fallback;
const rawOrder = params.get(urlKeys.sortOrder) ?? params.get(urlKeys.legacySort);
const direction = rawOrder?.toLowerCase();
if (direction !== "asc" && direction !== "desc") return fallback;
const allowed = allowedColumnsRef.current;
if (allowed && !allowed.includes(column)) return fallback;
return { column, direction };
}, [syncWithUrl, urlKeys]);
const [initialState] = useState(readFromUrl);
const [sortColumn, setSortColumn] = useState(
initialState.column
);
const [sortDirection, setSortDirection] = useState(
initialState.direction
);
const hasWrittenUrlRef = useRef(false);
useEffect(() => {
if (!syncWithUrl || globalThis.window === void 0) return;
const url = new URL(globalThis.location.href);
const params = url.searchParams;
if (sortColumn && sortDirection) {
params.set(urlKeys.sortBy, sortColumn);
params.set(urlKeys.sortOrder, sortDirection);
params.delete(urlKeys.legacySort);
hasWrittenUrlRef.current = true;
} else if (hasWrittenUrlRef.current) {
params.delete(urlKeys.sortBy);
params.delete(urlKeys.sortOrder);
params.delete(urlKeys.legacySort);
} else {
return;
}
globalThis.history.replaceState({}, "", url.toString());
}, [sortColumn, sortDirection, syncWithUrl, urlKeys]);
useEffect(() => {
if (!syncWithUrl || globalThis.window === void 0) return;
const handlePopState = () => {
const next = readFromUrl();
setSortColumn(next.column);
setSortDirection(next.direction);
};
globalThis.addEventListener("popstate", handlePopState);
return () => globalThis.removeEventListener("popstate", handlePopState);
}, [syncWithUrl, readFromUrl]);
const handleSort = (column) => {
if (sortColumn === column && sortDirection === "asc") {
setSortDirection("desc");
} else if (sortColumn === column && sortDirection === "desc") {
setSortColumn(null);
setSortDirection(null);
} else {
setSortColumn(column);
setSortDirection("asc");
}
onSortChangeRef.current?.();
};
const sortedData = useMemo(() => {
if (mode === "server" || !sortColumn || !sortDirection) {
return data;
}
return [...data].sort((a, b) => {
const aValue = a[sortColumn];
const bValue = b[sortColumn];
if (typeof aValue === "string" && typeof bValue === "string") {
const comparison = aValue.localeCompare(bValue);
return sortDirection === "asc" ? comparison : -comparison;
}
if (typeof aValue === "number" && typeof bValue === "number") {
return sortDirection === "asc" ? aValue - bValue : bValue - aValue;
}
return 0;
});
}, [data, sortColumn, sortDirection, mode]);
return { sortedData, sortColumn, sortDirection, handleSort };
}
var renderHeaderElements = (children) => {
return Children.map(children, (child) => {
if (isValidElement(child) && (child.type === TableCaption || child.type === TableHeader)) {
return child;
}
return null;
});
};
var getNoSearchResultContent = (config, defaultTitle, defaultDescription) => {
if (config.component) {
return config.component;
}
if (config.image) {
return /* @__PURE__ */ jsx(
NoSearchResult_default,
{
image: config.image,
title: config.title || defaultTitle,
description: config.description || defaultDescription
}
);
}
return /* @__PURE__ */ jsxs("div", { className: "text-center", children: [
/* @__PURE__ */ jsx("p", { className: "text-text-600 text-lg font-semibold mb-2", children: config.title || defaultTitle }),
/* @__PURE__ */ jsx("p", { className: "text-text-500 text-sm", children: config.description || defaultDescription })
] });
};
var getEmptyStateContent = (config, defaultTitle, defaultDescription) => {
if (config?.component) {
return config.component;
}
return /* @__PURE__ */ jsx(
EmptyState_default,
{
image: config?.image,
title: config?.title || defaultTitle,
description: config?.description || defaultDescription,
buttonText: config?.buttonText,
buttonIcon: config?.buttonIcon,
onButtonClick: config?.onButtonClick,
buttonVariant: config?.buttonVariant,
buttonAction: config?.buttonAction
}
);
};
var renderTableWrapper = (variant, tableRef, className, children, stateContent, tableProps) => {
return /* @__PURE__ */ jsxs(
"div",
{
className: cn(
"relative w-full overflow-x-auto",
variant === "default" && "border border-border-200 rounded-xl"
),
children: [
/* @__PURE__ */ jsx(
"table",
{
ref: tableRef,
className: cn(
"analytica-table w-full caption-bottom text-sm border-separate border-spacing-0",
className
),
...tableProps,
children: renderHeaderElements(children)
}
),
/* @__PURE__ */ jsx("div", { className: "py-8 flex justify-center", children: stateContent })
]
}
);
};
var Table = forwardRef(
({
variant = "default",
className,
children,
showLoading = false,
loadingState,
showNoSearchResult = false,
noSearchResultState,
showEmpty = false,
emptyState,
...props
}, ref) => {
const defaultNoSearchResultState = {
title: "Nenhum resultado encontrado",
description: "N\xE3o encontramos nenhum resultado com esse nome. Tente revisar a busca ou usar outra palavra-chave."
};
const defaultEmptyState = {
title: "Nenhum dado dispon\xEDvel",
description: "N\xE3o h\xE1 dados para exibir no momento."
};
const finalNoSearchResultState = noSearchResultState || defaultNoSearchResultState;
const finalEmptyState = emptyState || defaultEmptyState;
if (showLoading) {
const loadingContent = loadingState?.component || /* @__PURE__ */ jsx(SkeletonTable, { rows: 5, columns: 4, showHeader: false });
return renderTableWrapper(
variant,
ref,
className,
children,
loadingContent,
props
);
}
if (showNoSearchResult) {
const noSearchContent = getNoSearchResultContent(
finalNoSearchResultState,
defaultNoSearchResultState.title || "",
defaultNoSearchResultState.description || ""
);
return renderTableWrapper(
variant,
ref,
className,
children,
noSearchContent,
props
);
}
if (showEmpty) {
const emptyContent = getEmptyStateContent(
finalEmptyState,
defaultEmptyState.title || "Nenhum dado dispon\xEDvel",
defaultEmptyState.description || "N\xE3o h\xE1 dados para exibir no momento."
);
return renderTableWrapper(
variant,
ref,
className,
children,
emptyContent,
props
);
}
return /* @__PURE__ */ jsx(
"div",
{
className: cn(
"relative w-full overflow-x-auto",
variant === "default" && "border border-border-200 rounded-xl"
),
children: /* @__PURE__ */ jsxs(
"table",
{
ref,
className: cn(
variant === "default" && "analytica-table",
variant === "default" && "border-separate border-spacing-0",
"w-full caption-bottom text-sm",
className
),
...props,
children: [
!Children.toArray(children).some(
(child) => isValidElement(child) && child.type === TableCaption
) && /* @__PURE__ */ jsx("caption", { className: "sr-only", children: "My Table" }),
children
]
}
)
}
);
}
);
Table.displayName = "Table";
var TableHeader = forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
"thead",
{
ref,
className: cn("[&_tr:first-child]:border-0", className),
...props
}
));
TableHeader.displayName = "TableHeader";
var TableBody = forwardRef(
({ className, variant = "default", ...props }, ref) => /* @__PURE__ */ jsx(
"tbody",
{
ref,
className: cn(
"[&_tr:last-child]:border-border-200",
variant === "default" && "border-t border-border-200",
className
),
...props
}
)
);
TableBody.displayName = "TableBody";
var TableFooter = forwardRef(
({ variant = "default", className, ...props }, ref) => /* @__PURE__ */ jsx(
"tfoot",
{
ref,
className: cn(
"bg-background-50 font-medium [&>tr]:last:border-b-0 px-6 py-3.5",
variant === "default" && "border-t border-border-200",
className
),
...props
}
)
);
TableFooter.displayName = "TableFooter";
var VARIANT_STATES_ROW = {
default: {
default: "border border-border-200",
defaultBorderless: "border-b border-border-200",
borderless: ""
},
selected: {
default: "border-b-2 border-indicator-primary",
defaultBorderless: "border-b border-indicator-primary",
borderless: "bg-indicator-primary/10"
},
invalid: {
default: "border-b-2 border-indicator-error",
defaultBorderless: "border-b border-indicator-error",
borderless: "bg-indicator-error/10"
},
disabled: {
default: "border-b border-border-100 bg-background-50 opacity-50 cursor-not-allowed",
defaultBorderless: "border-b border-border-100 bg-background-50 opacity-50 cursor-not-allowed",
borderless: "bg-background-50 opacity-50 cursor-not-allowed"
}
};
var TableRow = forwardRef(
({
variant = "default",
state = "default",
clickable = false,
className,
...props
}, ref) => {
return /* @__PURE__ */ jsx(
"tr",
{
ref,
className: cn(
"transition-colors",
state === "disabled" ? "" : "hover:bg-muted/50",
state === "disabled" || !clickable ? "" : "cursor-pointer",
VARIANT_STATES_ROW[state][variant],
className
),
"aria-disabled": state === "disabled",
...props
}
);
}
);
TableRow.displayName = "TableRow";
var TableHead = forwardRef(
({
className,
sortable = false,
sortDirection = null,
onSort,
filterSlot,
children,
...props
}, ref) => {
const handleClick = () => {
if (sortable && onSort) {
onSort();
}
};
const ariaSort = (() => {
if (!sortable) return void 0;
if (sortDirection === "asc") return "ascending";
if (sortDirection === "desc") return "descending";
return "none";
})();
const sortArrow = sortable && sortDirection && /* @__PURE__ */ jsx("span", { className: "flex shrink-0", children: sortDirection === "asc" ? /* @__PURE__ */ jsx(ArrowUpIcon, { size: 16, className: "text-text-800" }) : /* @__PURE__ */ jsx(ArrowDownIcon, { size: 16, className: "text-text-800" }) });
return /* @__PURE__ */ jsx(
"th",
{
ref,
className: cn(
"h-10 px-6 py-3.5 text-left align-middle font-bold text-base text-text-800 tracking-[0.2px] leading-none [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px] whitespace-nowrap",
sortable && "cursor-pointer select-none hover:bg-muted/30",
className
),
"aria-sort": ariaSort,
...props,
children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
sortable ? /* @__PURE__ */ jsxs(
"button",
{
type: "button",
onClick: handleClick,
className: cn(
"flex items-center gap-2 text-left cursor-pointer select-none hover:opacity-80",
// Span the cell so pointer users keep a full-width target — but
// not when a filter caret follows, or it would be shoved to the
// far edge of the column instead of sitting next to the label.
!filterSlot && "flex-1"
),
children: [
children,
sortArrow
]
}
) : children,
filterSlot
] })
}
);
}
);
TableHead.displayName = "TableHead";
var TableCell = forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
"td",
{
ref,
className: cn(
"p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px] text-base font-normal text-text-800 leading-[150%] tracking-normal px-6 py-3.5 whitespace-nowrap",
className
),
...props
}
));
TableCell.displayName = "TableCell";
var TableCaption = forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
"caption",
{
ref,
className: cn(
"border-t border-border-200 text-sm text-text-800 px-6 py-3.5",
className
),
...props
}
));
TableCaption.displayName = "TableCaption";
var Table_default = Table;
export {
useTableSort,
TableHeader,
TableBody,
TableFooter,
TableRow,
TableHead,
TableCell,
TableCaption,
Table_default
};
//# sourceMappingURL=chunk-66F2QDAX.mjs.map