analytica-frontend-lib
Version:
Repositório público dos componentes utilizados nas plataformas da Analytica Ensino
1 lines • 373 kB
Source Map (JSON)
{"version":3,"sources":["../../src/components/TableProvider/TableProvider.tsx","../../src/components/Table/Table.tsx","../../src/utils/utils.ts","../../src/components/Text/Text.tsx","../../src/components/NoSearchResult/NoSearchResult.tsx","../../src/components/Button/Button.tsx","../../src/components/EmptyState/EmptyState.tsx","../../src/components/Skeleton/Skeleton.tsx","../../src/components/Table/TablePagination.tsx","../../src/components/Filter/useTableFilter.ts","../../src/components/Search/Search.tsx","../../src/components/DropdownMenu/DropdownMenu.tsx","../../src/components/Modal/Modal.tsx","../../src/components/Modal/utils/videoUtils.ts","../../src/components/ThemeToggle/ThemeToggle.tsx","../../src/components/SelectionButton/SelectionButton.tsx","../../src/hooks/useTheme.ts","../../src/store/themeStore.ts","../../src/components/CheckBoxGroup/CheckBoxGroup.tsx","../../src/components/Badge/Badge.tsx","../../src/components/CheckBox/CheckBox.tsx","../../src/components/Divider/Divider.tsx","../../src/components/ProgressBar/ProgressBar.tsx","../../src/components/Accordation/Accordation.tsx","../../src/components/Card/Card.tsx","../../src/components/IconRender/IconRender.tsx","../../src/assets/icons/subjects/ChatPT.tsx","../../src/assets/icons/subjects/ChatEN.tsx","../../src/assets/icons/subjects/ChatES.tsx","../../src/components/Accordation/AccordionGroup.tsx","../../src/components/CheckBoxGroup/CheckBoxGroup.helpers.ts","../../src/components/Filter/FilterModal.tsx"],"sourcesContent":["import { useState, useEffect, useMemo, useCallback, ReactNode } from 'react';\nimport Table, {\n TableBody,\n TableHead,\n TableRow,\n TableCell,\n useTableSort,\n TablePagination,\n} from '../Table/Table';\nimport { useTableFilter, FilterConfig } from '../Filter/useTableFilter';\nimport Search from '../Search/Search';\nimport { FilterModal } from '../Filter/FilterModal';\nimport Button from '../Button/Button';\nimport { Funnel } from 'phosphor-react';\n\n/**\n * Column configuration with flexible rendering options\n */\nexport interface ColumnConfig<T = Record<string, unknown>> {\n /** Column key (must match data object key) */\n key: string;\n /** Column label - can be string or JSX */\n label: string | ReactNode;\n /** Enable sorting for this column */\n sortable?: boolean;\n /** Custom render function for cell content */\n render?: (value: unknown, row: T, index: number) => ReactNode;\n /** Column width */\n width?: string;\n /** Additional CSS classes */\n className?: string;\n /** Text alignment */\n align?: 'left' | 'center' | 'right';\n}\n\n/**\n * Combined parameters sent via onParamsChange\n */\nexport interface TableParams {\n /** Current page number */\n page: number;\n /** Items per page */\n limit: number;\n /** Search query */\n search?: string;\n /** Active filters (dynamic keys based on filter configs) */\n [key: string]: unknown;\n /** Sort column */\n sortBy?: string;\n /** Sort direction */\n sortOrder?: 'asc' | 'desc';\n}\n\n/**\n * Pagination configuration\n */\nexport interface PaginationConfig {\n /** Label for items (e.g., \"atividades\") */\n itemLabel?: string;\n /** Items per page options */\n itemsPerPageOptions?: number[];\n /** Default items per page */\n defaultItemsPerPage?: number;\n /** Total items (for displaying pagination info) */\n totalItems?: number;\n /** Total pages (if known from backend) */\n totalPages?: number;\n}\n\n/**\n * Empty state configuration\n */\nexport interface EmptyStateConfig {\n /** Custom component to render when table is empty (no data and no search active) */\n component?: ReactNode;\n /** Image to display in empty state (path from project) */\n image?: string;\n /** Title text for empty state */\n title?: string;\n /** Description text for empty state */\n description?: string;\n /** Button text for empty state action (optional) */\n buttonText?: string;\n /** Icon to display on button (optional) */\n buttonIcon?: ReactNode;\n /** Callback when empty state button is clicked */\n onButtonClick?: () => void;\n /** Button variant (solid, outline, or link) */\n buttonVariant?: 'solid' | 'outline' | 'link';\n /** Button action color (primary, positive, or negative) */\n buttonAction?: 'primary' | 'positive' | 'negative';\n}\n\n/**\n * Loading state configuration\n */\nexport interface LoadingStateConfig {\n /** Custom component to render when table is loading */\n component?: ReactNode;\n}\n\n/**\n * No search result state configuration\n */\nexport interface NoSearchResultConfig {\n /** Custom component to render when search returns no results */\n component?: ReactNode;\n /** Title for no search result state */\n title?: string;\n /** Description for no search result state */\n description?: string;\n /** Image to display in no search result state */\n image?: string;\n}\n\n/**\n * Table components exposed via render prop\n */\nexport interface TableComponents {\n /** Search and filter controls */\n controls: ReactNode;\n /** Table with data */\n table: ReactNode;\n /** Pagination controls */\n pagination: ReactNode;\n}\n\n/**\n * TableProvider Props\n */\nexport interface TableProviderProps<T = Record<string, unknown>> {\n /** Data to display in the table */\n readonly data: T[];\n /** Column configurations */\n readonly headers: ColumnConfig<T>[];\n /** Loading state */\n readonly loading?: boolean;\n /** Table variant */\n readonly variant?: 'default' | 'borderless';\n\n /** Enable search functionality */\n readonly enableSearch?: boolean;\n /** Enable filters functionality */\n readonly enableFilters?: boolean;\n /** Enable table sorting */\n readonly enableTableSort?: boolean;\n /** Enable pagination */\n readonly enablePagination?: boolean;\n /** Enable row click functionality */\n readonly enableRowClick?: boolean;\n\n /** Initial filter configurations */\n readonly initialFilters?: FilterConfig[];\n /** Pagination configuration */\n readonly paginationConfig?: PaginationConfig;\n /** Search placeholder text */\n readonly searchPlaceholder?: string;\n /** Empty state configuration (when table is empty with no search) */\n readonly emptyState?: EmptyStateConfig;\n /** Loading state configuration (when table is loading) */\n readonly loadingState?: LoadingStateConfig;\n /** No search result state configuration (when search returns no results) */\n readonly noSearchResultState?: NoSearchResultConfig;\n /** Key field name to use for unique row identification (recommended for better performance) */\n readonly rowKey?: keyof T;\n\n /** Callback when any parameter changes */\n readonly onParamsChange?: (params: TableParams) => void;\n /** Callback when row is clicked */\n readonly onRowClick?: (row: T, index: number) => void;\n\n /**\n * Render prop for custom layout control\n * When provided, gives full control over component positioning\n * @param components - Table components (controls, table, pagination)\n * @returns Custom layout JSX\n *\n * @example\n * ```tsx\n * <TableProvider {...props}>\n * {({ controls, table, pagination }) => (\n * <>\n * <div className=\"mb-4\">{controls}</div>\n * <div className=\"bg-white p-6\">\n * {table}\n * {pagination}\n * </div>\n * </>\n * )}\n * </TableProvider>\n * ```\n */\n readonly children?: (components: TableComponents) => ReactNode;\n}\n\n/**\n * TableProvider - Self-contained table component with search, filters, sorting, and pagination\n *\n * @example\n * ```tsx\n * <TableProvider\n * data={activities}\n * headers={[\n * { key: 'title', label: 'Título', sortable: true },\n * { key: 'status', label: 'Status', render: (value) => <Badge>{value}</Badge> }\n * ]}\n * loading={loading}\n * variant=\"borderless\"\n * enableSearch\n * enableFilters\n * enableTableSort\n * enablePagination\n * enableRowClick\n * initialFilters={filterConfigs}\n * paginationConfig={{ itemLabel: 'atividades' }}\n * onParamsChange={handleParamsChange}\n * onRowClick={handleRowClick}\n * />\n * ```\n */\nexport function TableProvider<T extends Record<string, unknown>>({\n data,\n headers,\n loading = false,\n variant = 'default',\n enableSearch = false,\n enableFilters = false,\n enableTableSort = false,\n enablePagination = false,\n enableRowClick = false,\n initialFilters = [],\n paginationConfig = {},\n searchPlaceholder = 'Buscar...',\n emptyState,\n loadingState,\n noSearchResultState,\n rowKey,\n onParamsChange,\n onRowClick,\n children,\n}: TableProviderProps<T>) {\n // Search state\n const [searchQuery, setSearchQuery] = useState('');\n\n // Sorting state - always call hook (React Rules of Hooks)\n const sortResultRaw = useTableSort(data, { syncWithUrl: true });\n const sortResult = enableTableSort\n ? sortResultRaw\n : {\n sortedData: data,\n sortColumn: null,\n sortDirection: null,\n handleSort: () => {},\n };\n\n const { sortedData, sortColumn, sortDirection, handleSort } = sortResult;\n\n // Filter state - always call hook (React Rules of Hooks)\n const filterResultRaw = useTableFilter(initialFilters, { syncWithUrl: true });\n\n // Memoize disabled filter result to prevent recreating object on every render\n const disabledFilterResult = useMemo(\n () => ({\n filterConfigs: [],\n activeFilters: {},\n hasActiveFilters: false,\n updateFilters: () => {},\n applyFilters: () => {},\n clearFilters: () => {},\n }),\n []\n );\n\n const filterResult = enableFilters ? filterResultRaw : disabledFilterResult;\n\n const {\n filterConfigs,\n activeFilters,\n hasActiveFilters,\n updateFilters,\n applyFilters,\n clearFilters,\n } = filterResult;\n\n // Pagination state (only if enabled)\n const {\n defaultItemsPerPage = 10,\n itemsPerPageOptions = [10, 20, 50, 100],\n itemLabel = 'itens',\n totalItems,\n totalPages,\n } = paginationConfig;\n\n const [currentPage, setCurrentPage] = useState(1);\n const [itemsPerPage, setItemsPerPage] = useState(defaultItemsPerPage);\n\n // Filter modal state\n const [isFilterModalOpen, setIsFilterModalOpen] = useState(false);\n\n // Combine all parameters\n const combinedParams = useMemo((): TableParams => {\n const params: TableParams = {\n page: currentPage,\n limit: itemsPerPage,\n };\n\n if (enableSearch && searchQuery) {\n params.search = searchQuery;\n }\n\n if (enableFilters) {\n Object.assign(params, activeFilters);\n }\n\n if (enableTableSort && sortColumn && sortDirection) {\n params.sortBy = sortColumn;\n params.sortOrder = sortDirection;\n }\n\n return params;\n }, [\n currentPage,\n itemsPerPage,\n searchQuery,\n activeFilters,\n sortColumn,\n sortDirection,\n enableSearch,\n enableFilters,\n enableTableSort,\n ]);\n\n // Notify parent when parameters change\n // Note: onParamsChange is omitted from dependencies intentionally to prevent infinite loops\n useEffect(() => {\n onParamsChange?.(combinedParams);\n }, [combinedParams]);\n\n // Handle search changes\n const handleSearchChange = useCallback((value: string) => {\n setSearchQuery(value);\n setCurrentPage(1); // Reset to first page on search\n }, []);\n\n // Handle filter apply\n const handleFilterApply = useCallback(() => {\n applyFilters();\n setIsFilterModalOpen(false);\n setCurrentPage(1); // Reset to first page on filter\n }, [applyFilters]);\n\n // Handle pagination change\n const handlePageChange = useCallback((page: number) => {\n setCurrentPage(page);\n }, []);\n\n const handleItemsPerPageChange = useCallback((items: number) => {\n setItemsPerPage(items);\n setCurrentPage(1); // Reset to first page when changing items per page\n }, []);\n\n // Handle row click\n const handleRowClickInternal = useCallback(\n (row: T, index: number) => {\n if (enableRowClick && onRowClick) {\n onRowClick(row, index);\n }\n },\n [enableRowClick, onRowClick]\n );\n\n // Detect if pagination should be managed internally\n const useInternalPagination = useMemo(\n () =>\n enablePagination &&\n !onParamsChange &&\n totalItems === undefined &&\n totalPages === undefined,\n [enablePagination, onParamsChange, totalItems, totalPages]\n );\n\n // Calculate total pages from data if not provided\n const calculatedTotalPages =\n totalPages ??\n Math.ceil(\n (totalItems ??\n (useInternalPagination ? sortedData.length : data.length)) /\n itemsPerPage\n );\n const calculatedTotalItems =\n totalItems ?? (useInternalPagination ? sortedData.length : data.length);\n\n // Apply pagination to data when managed internally\n const displayData = useMemo(() => {\n if (!useInternalPagination) {\n return sortedData;\n }\n\n const start = (currentPage - 1) * itemsPerPage;\n return sortedData.slice(start, start + itemsPerPage);\n }, [useInternalPagination, sortedData, currentPage, itemsPerPage]);\n\n // Empty state check\n const isEmpty = data.length === 0;\n\n // Calculate state control booleans - Table is responsible for rendering, TableProvider controls WHEN\n const showLoading = loading;\n const showNoSearchResult =\n !loading && data.length === 0 && searchQuery.trim() !== '';\n const showEmpty = !loading && data.length === 0 && searchQuery.trim() === '';\n\n // Extract components for render prop pattern\n const controls = (enableSearch || enableFilters) && (\n <div className=\"flex items-center gap-4\">\n {/* Filter Button */}\n {enableFilters && (\n <Button\n variant=\"outline\"\n size=\"medium\"\n onClick={() => setIsFilterModalOpen(true)}\n >\n <Funnel size={20} />\n Filtros\n {hasActiveFilters && (\n <span className=\"ml-2 rounded-full bg-primary-500 px-2 py-0.5 text-xs text-white\">\n {Object.keys(activeFilters).length}\n </span>\n )}\n </Button>\n )}\n\n {/* Search */}\n {enableSearch && (\n <div className=\"flex-1\">\n <Search\n value={searchQuery}\n onSearch={handleSearchChange}\n onClear={() => handleSearchChange('')}\n options={[]}\n placeholder={searchPlaceholder}\n />\n </div>\n )}\n </div>\n );\n\n const table = (\n <div className=\"w-full overflow-x-auto\">\n <Table\n variant={variant}\n showLoading={showLoading}\n loadingState={loadingState}\n showNoSearchResult={showNoSearchResult}\n noSearchResultState={noSearchResultState}\n showEmpty={showEmpty}\n emptyState={emptyState}\n >\n {/* Table Header */}\n <thead>\n <TableRow\n variant={variant === 'borderless' ? 'defaultBorderless' : 'default'}\n >\n {headers.map((header, index) => (\n <TableHead\n key={`header-${header.key}-${index}`}\n sortable={enableTableSort && header.sortable}\n sortDirection={\n enableTableSort && sortColumn === header.key\n ? sortDirection\n : null\n }\n onSort={() =>\n enableTableSort && header.sortable && handleSort(header.key)\n }\n className={header.className}\n style={header.width ? { width: header.width } : undefined}\n >\n {header.label}\n </TableHead>\n ))}\n </TableRow>\n </thead>\n\n {/* Table Body */}\n <TableBody>\n {loading ? (\n <TableRow>\n <TableCell colSpan={headers.length} className=\"text-center py-8\">\n <span className=\"text-text-400 text-sm\">Carregando...</span>\n </TableCell>\n </TableRow>\n ) : (\n displayData.map((row, rowIndex) => {\n // Calculate effective index for row click and keys\n const effectiveIndex = useInternalPagination\n ? (currentPage - 1) * itemsPerPage + rowIndex\n : rowIndex;\n\n const rowKeyValue = rowKey\n ? (() => {\n const keyValue = row[rowKey];\n if (keyValue === null || keyValue === undefined) {\n return `row-${effectiveIndex}`;\n }\n if (typeof keyValue === 'object') {\n return JSON.stringify(keyValue);\n }\n return String(keyValue);\n })()\n : `row-${effectiveIndex}`;\n return (\n <TableRow\n key={rowKeyValue}\n variant={\n variant === 'borderless' ? 'defaultBorderless' : 'default'\n }\n clickable={enableRowClick}\n onClick={() => handleRowClickInternal(row, effectiveIndex)}\n >\n {headers.map((header, cellIndex) => {\n const value = row[header.key];\n\n let defaultContent = '';\n\n if (value !== null && value !== undefined) {\n if (\n typeof value === 'string' ||\n typeof value === 'number' ||\n typeof value === 'boolean' ||\n typeof value === 'bigint'\n ) {\n // Only convert primitives directly to string\n defaultContent = String(value);\n } else if (typeof value === 'object') {\n // Serialize objects and arrays with JSON\n defaultContent = JSON.stringify(value);\n } else if (typeof value === 'function') {\n // Handle functions - don't expose function code\n defaultContent = '[Function]';\n } else if (typeof value === 'symbol') {\n // Handle symbols\n defaultContent = String(value);\n }\n // All possible types covered - no else needed\n }\n\n const content = header.render\n ? header.render(value, row, effectiveIndex)\n : defaultContent;\n\n return (\n <TableCell\n key={`cell-${effectiveIndex}-${cellIndex}`}\n className={header.className}\n style={{\n textAlign: header.align,\n }}\n >\n {content}\n </TableCell>\n );\n })}\n </TableRow>\n );\n })\n )}\n </TableBody>\n </Table>\n </div>\n );\n\n const pagination = enablePagination && !isEmpty && (\n <div className=\"flex justify-end\">\n <TablePagination\n currentPage={currentPage}\n totalPages={calculatedTotalPages}\n totalItems={calculatedTotalItems}\n itemsPerPage={itemsPerPage}\n itemsPerPageOptions={itemsPerPageOptions}\n onPageChange={handlePageChange}\n onItemsPerPageChange={handleItemsPerPageChange}\n itemLabel={itemLabel}\n />\n </div>\n );\n\n // If children prop provided, use render props pattern\n if (children) {\n return (\n <>\n {children({ controls, table, pagination })}\n {/* Filter Modal */}\n {enableFilters && (\n <FilterModal\n isOpen={isFilterModalOpen}\n onClose={() => setIsFilterModalOpen(false)}\n filterConfigs={filterConfigs}\n onFiltersChange={updateFilters}\n onApply={handleFilterApply}\n onClear={clearFilters}\n />\n )}\n </>\n );\n }\n\n // Default layout (backward compatible)\n return (\n <div className=\"w-full space-y-4\">\n {controls}\n {table}\n {pagination}\n\n {/* Filter Modal */}\n {enableFilters && (\n <FilterModal\n isOpen={isFilterModalOpen}\n onClose={() => setIsFilterModalOpen(false)}\n filterConfigs={filterConfigs}\n onFiltersChange={updateFilters}\n onApply={handleFilterApply}\n onClear={clearFilters}\n />\n )}\n </div>\n );\n}\n\nexport default TableProvider;\n","import React, {\n forwardRef,\n HTMLAttributes,\n TdHTMLAttributes,\n ThHTMLAttributes,\n useState,\n useMemo,\n useEffect,\n Children,\n isValidElement,\n ReactNode,\n} from 'react';\nimport { cn } from '../../utils/utils';\nimport { CaretUp, CaretDown } from 'phosphor-react';\nimport NoSearchResult from '../NoSearchResult/NoSearchResult';\nimport EmptyState from '../EmptyState/EmptyState';\nimport { SkeletonTable } from '../Skeleton/Skeleton';\nimport type {\n EmptyStateConfig,\n LoadingStateConfig,\n NoSearchResultConfig,\n} from '../TableProvider/TableProvider';\n\ntype TableVariant = 'default' | 'borderless';\ntype TableRowState = 'default' | 'selected' | 'invalid' | 'disabled';\nexport type SortDirection = 'asc' | 'desc' | null;\n\nexport interface UseTableSortOptions {\n /** Se true, sincroniza o estado de ordenação com os parâmetros da URL */\n syncWithUrl?: boolean;\n}\n\n/**\n * Hook para gerenciar ordenação de dados da tabela\n *\n * @param data - Array de dados a serem ordenados\n * @param options - Opções de configuração do hook\n * @returns Objeto com dados ordenados, coluna/direção atual e função de sort\n *\n * @example\n * ```tsx\n * const activities = [\n * { id: 1, name: 'Task A', date: '2024-01-01' },\n * { id: 2, name: 'Task B', date: '2024-01-02' },\n * ];\n *\n * // Sem sincronização com URL\n * const { sortedData, sortColumn, sortDirection, handleSort } = useTableSort(activities);\n *\n * // Com sincronização com URL\n * const { sortedData, sortColumn, sortDirection, handleSort } = useTableSort(activities, { syncWithUrl: true });\n *\n * <TableHead\n * sortDirection={sortColumn === 'name' ? sortDirection : null}\n * onSort={() => handleSort('name')}\n * >\n * Name\n * </TableHead>\n * ```\n */\nexport function useTableSort<T extends Record<string, unknown>>(\n data: T[],\n options: UseTableSortOptions = {}\n) {\n const { syncWithUrl = false } = options;\n\n // Inicializar estado a partir da URL se syncWithUrl estiver habilitado\n const getInitialState = () => {\n if (!syncWithUrl || globalThis.window === undefined) {\n return { column: null, direction: null };\n }\n\n const params = new URLSearchParams(globalThis.location.search);\n const sortBy = params.get('sortBy');\n const sort = params.get('sort');\n\n if (sortBy && sort && (sort === 'ASC' || sort === 'DESC')) {\n return {\n column: sortBy,\n direction: sort.toLowerCase() as SortDirection,\n };\n }\n\n return { column: null, direction: null };\n };\n\n const initialState = getInitialState();\n const [sortColumn, setSortColumn] = useState<string | null>(\n initialState.column\n );\n const [sortDirection, setSortDirection] = useState<SortDirection>(\n initialState.direction\n );\n\n // Atualizar URL quando o estado de ordenação mudar\n useEffect(() => {\n if (!syncWithUrl || globalThis.window === undefined) return;\n\n const url = new URL(globalThis.location.href);\n const params = url.searchParams;\n\n if (sortColumn && sortDirection) {\n params.set('sortBy', sortColumn);\n params.set('sort', sortDirection.toUpperCase());\n } else {\n params.delete('sortBy');\n params.delete('sort');\n }\n\n // Atualizar URL sem recarregar a página\n globalThis.history.replaceState({}, '', url.toString());\n }, [sortColumn, sortDirection, syncWithUrl]);\n\n const handleSort = (column: string) => {\n if (sortColumn === column) {\n if (sortDirection === 'asc') {\n setSortDirection('desc');\n } else if (sortDirection === 'desc') {\n setSortColumn(null);\n setSortDirection(null);\n }\n } else {\n setSortColumn(column);\n setSortDirection('asc');\n }\n };\n\n const sortedData = useMemo(() => {\n if (!sortColumn || !sortDirection) {\n return data;\n }\n\n return [...data].sort((a, b) => {\n const aValue = a[sortColumn as keyof T];\n const bValue = b[sortColumn as keyof T];\n\n if (typeof aValue === 'string' && typeof bValue === 'string') {\n const comparison = aValue.localeCompare(bValue);\n return sortDirection === 'asc' ? comparison : -comparison;\n }\n\n if (typeof aValue === 'number' && typeof bValue === 'number') {\n return sortDirection === 'asc' ? aValue - bValue : bValue - aValue;\n }\n\n return 0;\n });\n }, [data, sortColumn, sortDirection]);\n\n return { sortedData, sortColumn, sortDirection, handleSort };\n}\n\ninterface TableProps extends HTMLAttributes<HTMLTableElement> {\n variant?: TableVariant;\n\n /** Show loading state (controlled by TableProvider) */\n showLoading?: boolean;\n /** Loading state configuration */\n loadingState?: LoadingStateConfig;\n\n /** Show no search result state (controlled by TableProvider) */\n showNoSearchResult?: boolean;\n /** No search result state configuration */\n noSearchResultState?: NoSearchResultConfig;\n\n /** Show empty state (controlled by TableProvider) */\n showEmpty?: boolean;\n /** Empty state configuration */\n emptyState?: EmptyStateConfig;\n}\n\ninterface TableRowProps extends HTMLAttributes<HTMLTableRowElement> {\n state?: TableRowState;\n}\n\n/**\n * Renders the table header and caption from children\n */\nconst renderHeaderElements = (children: ReactNode) => {\n return Children.map(children, (child) => {\n if (\n isValidElement(child) &&\n (child.type === TableCaption || child.type === TableHeader)\n ) {\n return child;\n }\n return null;\n });\n};\n\n/**\n * Gets no search result content based on configuration\n */\nconst getNoSearchResultContent = (\n config: NoSearchResultConfig,\n defaultTitle: string,\n defaultDescription: string\n) => {\n if (config.component) {\n return config.component;\n }\n\n if (config.image) {\n return (\n <NoSearchResult\n image={config.image}\n title={config.title || defaultTitle}\n description={config.description || defaultDescription}\n />\n );\n }\n\n return (\n <div className=\"text-center\">\n <p className=\"text-text-600 text-lg font-semibold mb-2\">\n {config.title || defaultTitle}\n </p>\n <p className=\"text-text-500 text-sm\">\n {config.description || defaultDescription}\n </p>\n </div>\n );\n};\n\n/**\n * Gets empty state content based on configuration\n */\nconst getEmptyStateContent = (\n config: EmptyStateConfig | undefined,\n defaultTitle: string,\n defaultDescription: string\n) => {\n if (config?.component) {\n return config.component;\n }\n\n return (\n <EmptyState\n image={config?.image}\n title={config?.title || defaultTitle}\n description={config?.description || defaultDescription}\n buttonText={config?.buttonText}\n buttonIcon={config?.buttonIcon}\n onButtonClick={config?.onButtonClick}\n buttonVariant={config?.buttonVariant}\n buttonAction={config?.buttonAction}\n />\n );\n};\n\n/**\n * Renders table wrapper with header and state content\n */\nconst renderTableWrapper = (\n variant: TableVariant,\n tableRef: React.Ref<HTMLTableElement>,\n className: string | undefined,\n children: ReactNode,\n stateContent: ReactNode,\n tableProps: HTMLAttributes<HTMLTableElement>\n) => {\n return (\n <div\n className={cn(\n 'relative w-full overflow-x-auto',\n variant === 'default' && 'border border-border-200 rounded-xl'\n )}\n >\n <table\n ref={tableRef}\n className={cn(\n 'analytica-table w-full caption-bottom text-sm border-separate border-spacing-0',\n className\n )}\n {...tableProps}\n >\n {renderHeaderElements(children)}\n </table>\n <div className=\"py-8 flex justify-center\">{stateContent}</div>\n </div>\n );\n};\n\nconst Table = forwardRef<HTMLTableElement, TableProps>(\n (\n {\n variant = 'default',\n className,\n children,\n showLoading = false,\n loadingState,\n showNoSearchResult = false,\n noSearchResultState,\n showEmpty = false,\n emptyState,\n ...props\n },\n ref\n ) => {\n // Default configurations\n const defaultNoSearchResultState: NoSearchResultConfig = {\n title: 'Nenhum resultado encontrado',\n description:\n 'Não encontramos nenhum resultado com esse nome. Tente revisar a busca ou usar outra palavra-chave.',\n };\n\n const defaultEmptyState: EmptyStateConfig = {\n title: 'Nenhum dado disponível',\n description: 'Não há dados para exibir no momento.',\n };\n\n const finalNoSearchResultState =\n noSearchResultState || defaultNoSearchResultState;\n const finalEmptyState = emptyState || defaultEmptyState;\n\n // Render Loading State FIRST (highest priority)\n if (showLoading) {\n const loadingContent = loadingState?.component || (\n <SkeletonTable rows={5} columns={4} showHeader={false} />\n );\n return renderTableWrapper(\n variant,\n ref,\n className,\n children,\n loadingContent,\n props\n );\n }\n\n // Render NoSearchResult outside table\n if (showNoSearchResult) {\n const noSearchContent = getNoSearchResultContent(\n finalNoSearchResultState,\n defaultNoSearchResultState.title || '',\n defaultNoSearchResultState.description || ''\n );\n return renderTableWrapper(\n variant,\n ref,\n className,\n children,\n noSearchContent,\n props\n );\n }\n\n // Render Empty State outside table (same pattern as NoSearchResult)\n if (showEmpty) {\n const emptyContent = getEmptyStateContent(\n finalEmptyState,\n defaultEmptyState.title || 'Nenhum dado disponível',\n defaultEmptyState.description || 'Não há dados para exibir no momento.'\n );\n return renderTableWrapper(\n variant,\n ref,\n className,\n children,\n emptyContent,\n props\n );\n }\n\n return (\n <div\n className={cn(\n 'relative w-full overflow-x-auto',\n variant === 'default' && 'border border-border-200 rounded-xl'\n )}\n >\n <table\n ref={ref}\n className={cn(\n variant === 'default' && 'analytica-table',\n variant === 'default' && 'border-separate border-spacing-0',\n 'w-full caption-bottom text-sm',\n className\n )}\n {...props}\n >\n {/* Render fallback caption only if no TableCaption provided */}\n {!Children.toArray(children).some(\n (child) => isValidElement(child) && child.type === TableCaption\n ) && <caption className=\"sr-only\">My Table</caption>}\n {children}\n </table>\n </div>\n );\n }\n);\n\nTable.displayName = 'Table';\n\nconst TableHeader = forwardRef<\n HTMLTableSectionElement,\n HTMLAttributes<HTMLTableSectionElement>\n>(({ className, ...props }, ref) => (\n <thead\n ref={ref}\n className={cn('[&_tr:first-child]:border-0', className)}\n {...props}\n />\n));\nTableHeader.displayName = 'TableHeader';\n\ninterface TableBodyProps extends HTMLAttributes<HTMLTableSectionElement> {\n variant?: TableVariant;\n}\n\nconst TableBody = forwardRef<HTMLTableSectionElement, TableBodyProps>(\n ({ className, variant = 'default', ...props }, ref) => (\n <tbody\n ref={ref}\n className={cn(\n '[&_tr:last-child]:border-border-200',\n variant === 'default' && 'border-t border-border-200',\n className\n )}\n {...props}\n />\n )\n);\nTableBody.displayName = 'TableBody';\n\ninterface TableFooterProps extends HTMLAttributes<HTMLTableSectionElement> {\n variant?: TableVariant;\n}\n\nconst TableFooter = forwardRef<HTMLTableSectionElement, TableFooterProps>(\n ({ variant = 'default', className, ...props }, ref) => (\n <tfoot\n ref={ref}\n className={cn(\n 'bg-background-50 font-medium [&>tr]:last:border-b-0 px-6 py-3.5',\n variant === 'default' && 'border-t border-border-200',\n className\n )}\n {...props}\n />\n )\n);\nTableFooter.displayName = 'TableFooter';\n\nconst VARIANT_STATES_ROW = {\n default: {\n default: 'border border-border-200',\n defaultBorderless: 'border-b border-border-200',\n borderless: '',\n },\n selected: {\n default: 'border-b-2 border-indicator-primary',\n defaultBorderless: 'border-b border-indicator-primary',\n borderless: 'bg-indicator-primary/10',\n },\n invalid: {\n default: 'border-b-2 border-indicator-error',\n defaultBorderless: 'border-b border-indicator-error',\n borderless: 'bg-indicator-error/10',\n },\n disabled: {\n default:\n 'border-b border-border-100 bg-background-50 opacity-50 cursor-not-allowed',\n defaultBorderless:\n 'border-b border-border-100 bg-background-50 opacity-50 cursor-not-allowed',\n borderless: 'bg-background-50 opacity-50 cursor-not-allowed',\n },\n} as const;\n\ninterface TableRowPropsExtended extends TableRowProps {\n variant?: TableVariant | 'defaultBorderless';\n clickable?: boolean;\n}\n\nconst TableRow = forwardRef<HTMLTableRowElement, TableRowPropsExtended>(\n (\n {\n variant = 'default',\n state = 'default',\n clickable = false,\n className,\n ...props\n },\n ref\n ) => {\n return (\n <tr\n ref={ref}\n className={cn(\n 'transition-colors',\n state === 'disabled' ? '' : 'hover:bg-muted/50',\n state === 'disabled' || !clickable ? '' : 'cursor-pointer',\n VARIANT_STATES_ROW[state][variant],\n className\n )}\n aria-disabled={state === 'disabled'}\n {...props}\n />\n );\n }\n);\nTableRow.displayName = 'TableRow';\n\ninterface TableHeadProps extends ThHTMLAttributes<HTMLTableCellElement> {\n /** Enable sorting on this column (default: true) */\n sortable?: boolean;\n /** Current sort direction for this column */\n sortDirection?: SortDirection;\n /** Callback when column header is clicked */\n onSort?: () => void;\n}\n\nconst TableHead = forwardRef<HTMLTableCellElement, TableHeadProps>(\n (\n {\n className,\n sortable = true,\n sortDirection = null,\n onSort,\n children,\n ...props\n },\n ref\n ) => {\n const handleClick = () => {\n if (sortable && onSort) {\n onSort();\n }\n };\n\n return (\n <th\n ref={ref}\n className={cn(\n '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',\n sortable && 'cursor-pointer select-none hover:bg-muted/30',\n className\n )}\n onClick={handleClick}\n {...props}\n >\n <div className=\"flex items-center gap-2\">\n {children}\n {sortable && (\n <div className=\"flex flex-col\">\n {sortDirection === 'asc' && (\n <CaretUp size={16} weight=\"fill\" className=\"text-text-800\" />\n )}\n {sortDirection === 'desc' && (\n <CaretDown size={16} weight=\"fill\" className=\"text-text-800\" />\n )}\n </div>\n )}\n </div>\n </th>\n );\n }\n);\nTableHead.displayName = 'TableHead';\n\nconst TableCell = forwardRef<\n HTMLTableCellElement,\n TdHTMLAttributes<HTMLTableCellElement>\n>(({ className, ...props }, ref) => (\n <td\n ref={ref}\n className={cn(\n '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',\n className\n )}\n {...props}\n />\n));\nTableCell.displayName = 'TableCell';\n\nconst TableCaption = forwardRef<\n HTMLTableCaptionElement,\n HTMLAttributes<HTMLTableCaptionElement>\n>(({ className, ...props }, ref) => (\n <caption\n ref={ref}\n className={cn(\n 'border-t border-border-200 text-sm text-text-800 px-6 py-3.5',\n className\n )}\n {...props}\n />\n));\nTableCaption.displayName = 'TableCaption';\n\nexport { default as TablePagination } from './TablePagination';\nexport type { TablePaginationProps } from './TablePagination';\n\nexport default Table;\nexport {\n TableHeader,\n TableBody,\n TableFooter,\n TableHead,\n TableRow,\n TableCell,\n TableCaption,\n};\n","import { clsx, type ClassValue } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n\nexport { syncDropdownState } from './dropdown';\nexport {\n getSelectedIdsFromCategories,\n toggleArrayItem,\n toggleSingleValue,\n areFiltersEqual,\n} from './activityFilters';\nexport {\n mapQuestionTypeToEnum,\n mapQuestionTypeToEnumRequired,\n} from './questionTypeUtils';\nexport {\n getStatusBadgeConfig,\n formatTimeSpent,\n formatQuestionNumbers,\n formatDateToBrazilian,\n} from './activityDetailsUtils';\n\n/**\n * Retorna a cor hexadecimal com opacidade 0.3 (4d) se não estiver em dark mode.\n * Se estiver em dark mode, retorna a cor original.\n *\n * @param hexColor - Cor hexadecimal (ex: \"#0066b8\" ou \"0066b8\")\n * @param isDark - booleano indicando se está em dark mode\n * @returns string - cor hexadecimal com opacidade se necessário\n */\nexport function getSubjectColorWithOpacity(\n hexColor: string | undefined,\n isDark: boolean\n): string | undefined {\n if (!hexColor) return undefined;\n // Remove o '#' se existir\n let color = hexColor.replace(/^#/, '').toLowerCase();\n\n if (isDark) {\n // Se está em dark mode, sempre remove opacidade se existir\n if (color.length === 8) {\n color = color.slice(0, 6);\n }\n return `#${color}`;\n } else {\n // Se não está em dark mode (light mode)\n let resultColor: string;\n if (color.length === 6) {\n // Adiciona opacidade 0.3 (4D) para cores de 6 dígitos\n resultColor = `#${color}4d`;\n } else if (color.length === 8) {\n // Já tem opacidade, retorna como está\n resultColor = `#${color}`;\n } else {\n // Para outros tamanhos (3, 4, 5 dígitos), retorna como está\n resultColor = `#${color}`;\n }\n return resultColor;\n }\n}\n","import { ComponentPropsWithoutRef, ElementType, ReactNode } from 'react';\nimport { cn } from '../../utils/utils';\n\n/**\n * Base text component props\n */\ntype BaseTextProps = {\n /** Content to be displayed */\n children?: ReactNode;\n /** Text size variant */\n size?:\n | '2xs'\n | 'xs'\n | 'sm'\n | 'md'\n | 'lg'\n | 'xl'\n | '2xl'\n | '3xl'\n | '4xl'\n | '5xl'\n | '6xl';\n /** Font weight variant */\n weight?:\n | 'hairline'\n | 'light'\n | 'normal'\n | 'medium'\n | 'semibold'\n | 'bold'\n | 'extrabold'\n | 'black';\n /** Color variant - white for light backgrounds, black for dark backgrounds */\n color?: string;\n /** Additional CSS classes to apply */\n className?: string;\n};\n\n/**\n * Polymorphic text component props that ensures type safety based on the 'as' prop\n */\ntype TextProps<T extends ElementType = 'p'> = BaseTextProps & {\n /** HTML tag to render */\n as?: T;\n} & Omit<ComponentPropsWithoutRef<T>, keyof BaseTextProps>;\n\n/**\n * Text component for Analytica Ensino platforms\n *\n * A flexible polymorphic text component with multiple sizes, weights, and colors.\n * Automatically adapts to dark and light themes with full type safety.\n *\n * @param children - The content to display\n * @param size - The text size variant (2xs, xs, sm, md, lg, xl, 2xl, 3xl, 4xl, 5xl, 6xl)\n * @param weight - The font weight variant (hairline, light, normal, medium, semibold, bold, extrabold, black)\n * @param color - The color variant - adapts to theme\n * @param as - The HTML tag to render - determines allowed attributes via TypeScript\n * @param className - Additional CSS classes\n * @param props - HTML attributes valid for the chosen tag only\n * @returns A styled text element with type-safe attributes\n *\n * @example\n * ```tsx\n * <Text size=\"lg\" weight=\"bold\" color=\"text-info-800\">\n * This is a large, bold text\n * </Text>\n *\n * <Text as=\"a\" href=\"/link\" target=\"_blank\">\n * Link with type-safe anchor attributes\n * </Text>\n *\n * <Text as=\"button\" onClick={handleClick} disabled>\n * Button with type-safe button attributes\n * </Text>\n * ```\n */\nconst Text = <T extends ElementType = 'p'>({\n children,\n size = 'md',\n weight = 'normal',\n color = 'text-text-950',\n as,\n className = '',\n ...props\n}: TextProps<T>) => {\n let sizeClasses = '';\n let weightClasses = '';\n\n // Text size classes mapping\n const sizeClassMap = {\n '2xs': 'text-2xs',\n xs: 'text-xs',\n sm: 'text-sm',\n md: 'text-md',\n lg: 'text-lg',\n xl: 'text-xl',\n '2xl': 'text-2xl',\n '3xl': 'text-3xl',\n '4xl': 'text-4xl',\n '5xl': 'text-5xl',\n '6xl': 'text-6xl',\n } as const;\n\n sizeClasses = sizeClassMap[size] ?? sizeClassMap.md;\n\n // Font weight classes mapping\n const weightClassMap = {\n hairline: 'font-hairline',\n light: 'font-light',\n normal: 'font-normal',\n medium: 'font-medium',\n semibold: 'font-semibold',\n bold: 'font-bold',\n extrabold: 'font-extrabold',\n black: 'font-black',\n } as const;\n\n weightClasses = weightClassMap[weight] ?? weightClassMap.normal;\n\n const baseClasses = 'font-primary';\n const Component = as ?? ('p' as ElementType);\n\n return (\n <Component\n className={cn(baseClasses, sizeClasses, weightClasses, color, className)}\n {...props}\n >\n {children}\n </Component>\n );\n};\n\nexport default Text;\n","import Text from '../Text/Text';\n\nexport interface NoSearchResultProps {\n /**\n * Image source for the illustration\n */\n image: string;\n /**\n * Title text to display\n * @default \"Nenhum resultado encontrado\"\n */\n title?: string;\n /**\n * Description text to display below the title\n * @default \"Não encontramos nenhum resultado com esse nome. Tente revisar a busca ou usar outra palavra-chave.\"\n */\n description?: string;\n}\n\n/**\n * Component displayed when no search results are found\n * Shows an illustration with customizable title and description in horizontal layout\n *\n * @example\n * ```tsx\n * import { NoSearchResult } from 'analytica-frontend-lib';\n * import noSearchImage from './assets/no-search.png';\n *\n * <NoSearchResult\n * image={noSearchImage}\n * title=\"Nenhum resultado encontrado\"\n * description=\"Tente usar outros filtros\"\n * />\n * ```\n */\nconst NoSearchResult = ({ image, title, description }: NoSearchResultProps) => {\n const displayTitle = title || 'Nenhum resultado encontrado';\n const displayDescription =\n description ||\n 'Não encontramos nenhum resultado com esse nome. Tente revisar a busca ou usar outra palavra-chave.';\n\n return (\n <div className=\"flex flex-row justify-center items-center gap-8 w-full max-w-4xl min-h-96\">\n {/* Illustration */}\n <div className=\"w-72 h-72 flex-shrink-0 relative\">\n <img\n src={image}\n alt=\"No search results\"\n className=\"w-full h-full object-contain\"\n />\n </div>\n\n {/* Text Content */}\n <div className=\"flex flex-col items-start w-full max-w-md\">\n {/* Header Container */}\n <div className=\"flex flex-row justify-between items-end px-6 pt-6 pb-4 w-full rounded-t-xl\">\n {/* Title */}\n <Text\n as=\"h2\"\n className=\"text-text-950 font-semibold text-3xl leading-tight w-full flex items-center\"\n >\n {displayTitle}\n </Text>\n </div>\n\n {/* Description Container */}\n <div className=\"flex flex-row justify-center items-center px-6 gap-2 w-full\">\n {/* Description */}\n <Text className=\"text-text-600 font-normal text-lg leading-relaxed w-full text-justify\">\n {displayDescription}\n </Text>\n </div>\n </div>\n </div>\n );\n};\n\nexport default NoSearchResult;\n","import { ButtonHTMLAttributes, ReactNode } from 'react';\nimport { cn } from '../../utils/utils';\n\n/**\n * Lookup table for variant and action class combinations\n */\nconst VARIANT_ACTION_CLASSES = {\n solid: {\n primary:\n 'bg-primary-950 text-text border border-primary-950 hover:bg-primary-800 hover:border-primary-800 focus-visible:outline-none focus-visible:bg-primary-950 focus-visible:ring-2 focus-visible:ring-offset-0 focus-visible:ring-indicator-info active:bg-primary-700 active:border-primary-700 disabled:bg-primary-500 disabled:border-primary-500 disabled:opacity-40 disabled:cursor-not-allowed',\n positive:\n 'bg-success-500 text-text border border-success-500 hover:bg-success-600 hover:border-success-600 focus-visible:outline-none focus-visible:bg-success-500 focus-visible:ring-2 focus-visible:ring-offset-0 focus-visible:ring-indicator-info active:bg-success-700 active:border-success-700 disabled:bg-success-500 disabled:border-success-500 disabled:opacity-40 disabled:cursor-not-allowed',\n negative:\n 'bg-error-500 text-text border border-error-500 hover:bg-error-600 hover:border-error-600 focus-visible:outline-none focus-visible:bg-error-500 focus-visible:ring-2 focus-visible:ring-offset-0 focus-visible:ring-indicator-info active:bg-error-700 active:border-error-700 disabled:bg-error-500 disabled:border-error-500 disabled:opacity-40 disabled:cursor-not-allowed',\n },\n outline: {\n primary:\n 'bg-transparent text-primary-950 border border-primary-950 hover:bg-background-50 hover:text-primary-400 hover:border-primary-400 focus-visible:border-0 focus-visible:outline-none focus-visible:text-primary-600 focus-visible:ring-2 focus-visible:ring-offset-0 focus-visible:ring-indicator-info active:text-primary-700 active:border-primary-700 disabled:opacity-40 disabled:cursor-not-allowed',\n positive:\n 'bg-transparent text-success-500 border border-success-300 hover:bg-background-50 hover:text-success-400 hover:border-success-400 focus-visible:border-0 focus-visible:outline-none focus-visible:text-success-600 focus-visible:ring-2 focus-visible:ring-offset-0 focus-visible:ring-indicator-info active:text-success-700 active:border-success-700 disabled:opacity-40 disabled:cursor-not-allowed',\n negative:\n 'bg-transparent text-error-500 border border-error-300 hover:bg-background-50 hover:text-error-400 hover:border-error-400 focus-visible:border-0 focus-visible:outline-none focus-visible:text-error-600 focus-visible:ring-2 focus-visible:ring-offset-0 focus-visible:ring-indicator-info active:text-error-700 active:border-error-700 disabled:opacity-40 disabled:cursor-not-allowed',\n },\n link: {\n primary:\n 'bg-transparent text-primary-950 hover:text-primary-400 focus-visible:outline-none focus-visible:text-primary-600 focus-visible:ring-2 focus-visible:ring-offset-0 focus-visible:ring-indicator-info active:text-primary-700 disabled:opacity-40 disabled:cursor-not-allowed',\n positive:\n 'bg-transparent text-success-500 hover:text-success-400 focus-visible:outline-none focus-visible:text-success-600 focus-visible:ring-2 focus-visible:ring-offset-0 focus-visible:ring-indicator-info active:text-success-700 disabled:opacity-40 disabled:cursor-not-allowed',\n negative:\n 'bg-transparent text-error-500 hover:text-error-400 focus-visible:outline-none focus-visible:text-error-600 focus-visible:ring-2 focus-visible:ring-offset-0 focus-visible:ring-indicator-info active:text-error-700 disabled:opacity-40 disabled:cursor-not-allowed',\n },\n} as const;\n\n/**\n * Lookup table for size classes\n */\nconst SIZE_CLASSES = {\n 'extra-small': 'text-xs px-3.5 py-2',\n small: 'text-sm px-4 py-2.5',\n medium: 'text-md px-5 py-2.5',\n large: 'text-lg px-6 py-3',\n 'extra-large': 'text-lg px-7 py-3.5',\n} as const;\n\n/**\n * Button component props interface\n */\ntype ButtonProps = {\n /** Content to be displayed inside the button */\n children: ReactNode;\n /** Ícone à esquerda do texto */\n iconLeft?: ReactNode;\n /** Ícone à direita do texto */\n iconRight?: ReactNode;\n /** Size of the button */\n size?: 'extra-small' | 'small' | 'medium' | 'large' | 'extra-large';\n /** Visual variant of the button */\n variant?: 'solid' | 'outline' | 'link';\n /** Action type of the button */\n action?: 'primary' | 'positive' | 'negative';\n /** Additional CSS classes to apply */\n className?: string;\n} & ButtonHTMLA