UNPKG

analytica-frontend-lib

Version:

Repositório público dos componentes utilizados nas plataformas da Analytica Ensino

1 lines 390 kB
{"version":3,"sources":["../../src/components/AlertManager/AlertsManager.tsx","../../src/utils/utils.ts","../../src/components/Text/Text.tsx","../../src/components/Button/Button.tsx","../../src/components/Badge/Badge.tsx","../../src/components/SelectionButton/SelectionButton.tsx","../../src/components/CheckBox/CheckBox.tsx","../../src/components/ImageUpload/ImageUpload.tsx","../../src/components/CheckBoxGroup/CheckBoxGroup.tsx","../../src/components/CheckBoxGroup/CheckBoxGroup.helpers.ts","../../src/components/AlertManager/useAlertForm.ts","../../src/components/Modal/Modal.tsx","../../src/components/Modal/utils/videoUtils.ts","../../src/components/Divider/Divider.tsx","../../src/components/TextArea/TextArea.tsx","../../src/components/Input/Input.tsx","../../src/components/DropdownMenu/DropdownMenu.tsx","../../src/components/ThemeToggle/ThemeToggle.tsx","../../src/hooks/useTheme.ts","../../src/store/themeStore.ts","../../src/components/ProgressBar/ProgressBar.tsx","../../src/components/Stepper/Stepper.tsx","../../src/components/Calendar/Calendar.tsx","../../src/components/DateTimeInput/DateTimeInput.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/AlertManager/AlertSteps/MessageStep.tsx","../../src/components/AlertManager/AlertSteps/RecipientsStep.tsx","../../src/components/AlertManager/AlertSteps/DateStep.tsx","../../src/components/AlertManager/AlertSteps/PreviewStep.tsx","../../src/components/AlertManager/validation.ts"],"sourcesContent":["import {\n useState,\n useEffect,\n ReactNode,\n useMemo,\n useCallback,\n useSyncExternalStore,\n} from 'react';\nimport { Button, Modal, Stepper } from '../..';\nimport { CaretLeft, CaretRight, PaperPlaneTilt } from 'phosphor-react';\nimport { StepData } from '../Stepper/Stepper';\nimport { useAlertFormStore } from './useAlertForm';\nimport type { AlertsConfig } from '.';\nimport {\n MessageStep,\n RecipientsStep,\n DateStep,\n PreviewStep,\n} from './AlertSteps';\nimport {\n canFinish as canFinishValidation,\n isCurrentStepValid as isCurrentStepValidValidation,\n handleNext as handleNextValidation,\n} from './validation';\n\ninterface AlertsManagerProps {\n config: AlertsConfig;\n isOpen?: boolean;\n onClose?: () => void;\n /** URL da imagem após upload (prioritária - será exibida primeiro) */\n imageLink?: string | null;\n /** Imagem padrão a ser exibida quando não há imagem selecionada (deve ser URL string - o front deve converter File para URL se necessário) */\n defaultImage?: string | null;\n}\n\nconst StepWrapper = ({ children }: { children: ReactNode }) => (\n <div>{children}</div>\n);\n\nexport const AlertsManager = ({\n config,\n isOpen = false,\n onClose,\n imageLink,\n defaultImage,\n}: AlertsManagerProps) => {\n const [isModalOpen, setIsModalOpen] = useState(isOpen);\n const [currentStep, setCurrentStep] = useState(0);\n const [completedSteps, setCompletedSteps] = useState<number[]>([]);\n const [categories, setCategories] = useState(config.categories);\n\n // Subscribe to form store changes using useSyncExternalStore\n const formData = useSyncExternalStore(\n useAlertFormStore.subscribe,\n useAlertFormStore.getState\n );\n\n // Sincroniza com a prop isOpen\n useEffect(() => {\n setIsModalOpen(isOpen);\n }, [isOpen]);\n\n const { labels, behavior, steps: customSteps } = config;\n\n // Steps padrão se não fornecidos\n const defaultSteps: StepData[] = [\n {\n id: '1',\n label: 'Mensagem',\n state: 'completed',\n },\n {\n id: '2',\n label: labels?.recipientsTitle || 'Destinatários',\n state: 'current',\n },\n {\n id: '3',\n label: labels?.dateLabel || 'Data de envio',\n state: 'pending',\n },\n {\n id: '4',\n label: labels?.previewTitle || 'Prévia',\n state: 'pending',\n },\n ];\n\n const steps = customSteps || defaultSteps;\n\n const handleCloseModal = () => {\n setIsModalOpen(false);\n onClose?.();\n };\n\n // Verifica se o step atual é válido\n const isCurrentStepValid = useCallback(() => {\n return isCurrentStepValidValidation(\n currentStep,\n formData,\n categories,\n customSteps\n );\n }, [currentStep, categories, customSteps, formData]);\n\n // Verifica se pode finalizar\n const canFinish = useCallback(() => {\n return canFinishValidation(formData, categories);\n }, [categories, formData]);\n\n const handleNext = useCallback(() => {\n const result = handleNextValidation({\n currentStep,\n steps,\n formData,\n categories,\n customSteps,\n completedSteps,\n setCompletedSteps,\n setCurrentStep,\n });\n\n if (!result.success && result.error) {\n alert(result.error);\n }\n }, [\n currentStep,\n steps,\n categories,\n customSteps,\n completedSteps,\n setCompletedSteps,\n setCurrentStep,\n formData,\n ]);\n\n const handlePrevious = useCallback(() => {\n if (currentStep > 0) {\n setCurrentStep(currentStep - 1);\n }\n }, [currentStep, setCurrentStep]);\n\n const handleFinish = async () => {\n if (!completedSteps.includes(currentStep)) {\n setCompletedSteps([...completedSteps, currentStep]);\n }\n\n const alertData = {\n title: formData.title,\n message: formData.message,\n image: formData.image,\n date: formData.date,\n time: formData.time,\n sendToday: formData.sendToday,\n sendCopyToEmail: formData.sendCopyToEmail,\n recipientCategories: Object.fromEntries(\n Object.entries(formData.recipientCategories).map(([key, c]) => [\n key,\n {\n selectedIds: c.selectedIds || [],\n allSelected: !!c.allSelected,\n },\n ])\n ),\n };\n\n try {\n if (behavior?.onSendAlert) {\n await behavior.onSendAlert(alertData);\n } else {\n console.log('Dados do formulário:', alertData);\n alert('Aviso enviado com sucesso!');\n }\n handleCloseModal();\n } catch (error) {\n console.error('Erro ao enviar aviso:', error);\n }\n };\n\n const dynamicSteps: StepData[] = (steps as StepData[]).map(\n (step, index: number) => {\n if (completedSteps.includes(index)) {\n return { ...step, state: 'completed' as const };\n }\n\n if (index === currentStep) {\n return { ...step, state: 'current' as const };\n }\n\n return { ...step, state: 'pending' as const };\n }\n );\n\n // Memoize step content to prevent re-renders and focus loss\n const currentStepContent = useMemo(() => {\n if (customSteps?.[currentStep]?.component) {\n const CustomComponent = customSteps[currentStep].component;\n return (\n <StepWrapper>\n <CustomComponent onNext={handleNext} onPrevious={handlePrevious} />\n </StepWrapper>\n );\n }\n\n switch (currentStep) {\n case 0:\n return (\n <StepWrapper>\n <MessageStep\n labels={labels}\n allowImageAttachment={behavior?.allowImageAttachment}\n />\n </StepWrapper>\n );\n case 1:\n return (\n <StepWrapper>\n <RecipientsStep\n categories={categories}\n labels={labels}\n onCategoriesChange={setCategories}\n />\n </StepWrapper>\n );\n case 2:\n return (\n <StepWrapper>\n <DateStep\n labels={labels}\n allowScheduling={behavior?.allowScheduling}\n allowEmailCopy={behavior?.allowEmailCopy}\n />\n </StepWrapper>\n );\n case 3:\n return (\n <StepWrapper>\n <PreviewStep imageLink={imageLink} defaultImage={defaultImage} />\n </StepWrapper>\n );\n default:\n return null;\n }\n }, [\n currentStep,\n customSteps,\n categories,\n labels,\n behavior,\n handleNext,\n handlePrevious,\n imageLink,\n defaultImage,\n ]); // handleNext e handlePrevious agora estão nas dependências\n\n const isFirstStep = currentStep === 0;\n const isLastStep = currentStep === steps.length - 1;\n\n return (\n <Modal\n isOpen={isModalOpen}\n onClose={handleCloseModal}\n title={labels?.modalTitle || 'Enviar aviso'}\n size={'md'}\n contentClassName=\"p-0\"\n footer={\n <div className=\"flex gap-3 justify-end w-full\">\n <div className=\"flex gap-3\">\n <Button variant=\"link\" size=\"small\" onClick={handleCloseModal}>\n {labels?.cancelButton || 'Cancelar'}\n </Button>\n {!isFirstStep && (\n <Button\n variant=\"outline\"\n size=\"small\"\n iconLeft={<CaretLeft />}\n onClick={handlePrevious}\n >\n {labels?.previousButton || 'Anterior'}\n </Button>\n )}\n {isLastStep ? (\n <Button\n variant=\"solid\"\n iconLeft={<PaperPlaneTilt />}\n size=\"small\"\n onClick={handleFinish}\n disabled={!canFinish()}\n >\n {labels?.finishButton || 'Enviar Aviso'}\n </Button>\n ) : (\n <Button\n variant=\"solid\"\n iconRight={<CaretRight />}\n size=\"small\"\n onClick={handleNext}\n disabled={!isCurrentStepValid()}\n >\n {labels?.nextButton || 'Próximo'}\n </Button>\n )}\n </div>\n </div>\n }\n >\n <div className=\"flex flex-col h-[calc(100vh-14rem)] max-h-[600px]\">\n {/* Stepper fixo no topo */}\n <div className=\"shrink-0 px-6 pt-4\">\n <Stepper\n steps={dynamicSteps}\n size=\"small\"\n showProgress\n responsive\n progressText={`Etapa ${currentStep + 1} de ${steps.length}`}\n />\n </div>\n\n {/* Área de conteúdo com scroll */}\n <div className=\"flex-1 overflow-y-auto px-6 pb-4\">\n {currentStepContent}\n </div>\n </div>\n </Modal>\n );\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 { 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} & ButtonHTMLAttributes<HTMLButtonElement>;\n\n/**\n * Button component for Analytica Ensino platforms\n *\n * A flexible button component with multiple variants, sizes and actions.\n *\n * @param children - The content to display inside the button\n * @param size - The size variant (extra-small, small, medium, large, extra-large)\n * @param variant - The visual style variant (solid, outline, link)\n * @param action - The action type (primary, positive, negative)\n * @param className - Additional CSS classes\n * @param props - All other standard button HTML attributes\n * @returns A styled button element\n *\n * @example\n * ```tsx\n * <Button variant=\"solid\" action=\"primary\" size=\"medium\" onClick={() => console.log('clicked')}>\n * Click me\n * </Button>\n * ```\n */\nconst Button = ({\n children,\n iconLeft,\n iconRight,\n size = 'medium',\n variant = 'solid',\n action = 'primary',\n className = '',\n disabled,\n type = 'button',\n ...props\n}: ButtonProps) => {\n // Get classes from lookup tables\n const sizeClasses = SIZE_CLASSES[size];\n const variantClasses = VARIANT_ACTION_CLASSES[variant][action];\n\n const baseClasses =\n 'inline-flex items-center justify-center rounded-full cursor-pointer font-medium';\n\n return (\n <button\n className={cn(baseClasses, variantClasses, sizeClasses, className)}\n disabled={disabled}\n type={type}\n {...props}\n >\n {iconLeft && <span className=\"mr-2 flex items-center\">{iconLeft}</span>}\n {children}\n {iconRight && <span className=\"ml-2 flex items-center\">{iconRight}</span>}\n </button>\n );\n};\n\nexport default Button;\n","import { HTMLAttributes, ReactNode } from 'react';\nimport { Bell } from 'phosphor-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 error: 'bg-error-background text-error-700 focus-visible:outline-none',\n warning: 'bg-warning text-warning-800 focus-visible:outline-none',\n success: 'bg-success text-success-800 focus-visible:outline-none',\n info: 'bg-info text-info-800 focus-visible:outline-none',\n muted: 'bg-background-muted text-background-800 focus-visible:outline-none',\n },\n outlined: {\n error:\n 'bg-error text-error-700 border border-error-300 focus-visible:outline-none',\n warning:\n 'bg-warning text-warning-800 border border-warning-300 focus-visible:outline-none',\n success:\n 'bg-success text-success-800 border border-success-300 focus-visible:outline-none',\n info: 'bg-info text-info-800 border border-info-300 focus-visible:outline-none',\n muted:\n 'bg-background-muted text-background-800 border border-border-300 focus-visible:outline-none',\n },\n exams: {\n exam1: 'bg-exam-1 text-info-700 focus-visible:outline-none',\n exam2: 'bg-exam-2 text-typography-1 focus-visible:outline-none',\n exam3: 'bg-exam-3 text-typography-2 focus-visible:outline-none',\n exam4: 'bg-exam-4 text-success-700 focus-visible:outline-none',\n },\n examsOutlined: {\n exam1:\n 'bg-exam-1 text-info-700 border border-info-700 focus-visible:outline-none',\n exam2:\n 'bg-exam-2 text-typography-1 border border-typography-1 focus-visible:outline-none',\n exam3:\n 'bg-exam-3 text-typography-2 border border-typography-2 focus-visible:outline-none',\n exam4:\n 'bg-exam-4 text-success-700 border border-success-700 focus-visible:outline-none',\n },\n resultStatus: {\n negative: 'bg-error text-error-800 focus-visible:outline-none',\n positive: 'bg-success text-success-800 focus-visible:outline-none',\n },\n notification: 'text-primary',\n} as const;\n\n/**\n * Lookup table for size classes\n */\nconst SIZE_CLASSES = {\n small: 'text-2xs px-2 py-1',\n medium: 'text-xs px-2 py-1',\n large: 'text-sm px-2 py-1',\n} as const;\n\nconst SIZE_CLASSES_ICON = {\n small: 'size-3',\n medium: 'size-3.5',\n large: 'size-4',\n} as const;\n\n/**\n * Badge component props interface\n */\ntype BadgeProps = {\n /** Content to be displayed inside the badge */\n children?: ReactNode;\n /** Ícone à direita do texto */\n iconRight?: ReactNode;\n /** Ícone à esquerda do texto */\n iconLeft?: ReactNode;\n /** Size of the badge */\n size?: 'small' | 'medium' | 'large';\n /** Visual variant of the badge */\n variant?:\n | 'solid'\n | 'outlined'\n | 'exams'\n | 'examsOutlined'\n | 'resultStatus'\n | 'notification';\n /** Action type of the badge */\n action?:\n | 'error'\n | 'warning'\n | 'success'\n | 'info'\n | 'muted'\n | 'exam1'\n | 'exam2'\n | 'exam3'\n | 'exam4'\n | 'positive'\n | 'negative';\n /** Additional CSS classes to apply */\n className?: string;\n notificationActive?: boolean;\n} & HTMLAttributes<HTMLDivElement>;\n\n/**\n * Badge component for Analytica Ensino platforms\n *\n * A flexible button component with multiple variants, sizes and actions.\n *\n * @param children - The content to display inside the badge\n * @param size - The size variant (extra-small, small, medium, large, extra-large)\n * @param variant - The visual style variant (solid, outline, link)\n * @param action - The action type (primary, positive, negative)\n * @param className - Additional CSS classes\n * @param props - All other standard div HTML attributes\n * @returns A styled badge element\n *\n * @example\n * ```tsx\n * <Badge variant=\"solid\" action=\"info\" size=\"medium\">\n * Information\n * </Badge>\n * ```\n */\nconst Badge = ({\n children,\n iconLeft,\n iconRight,\n size = 'medium',\n variant = 'solid',\n action = 'error',\n className = '',\n notificationActive = false,\n ...props\n}: BadgeProps) => {\n // Get classes from lookup tables\n const sizeClasses = SIZE_CLASSES[size];\n const sizeClassesIcon = SIZE_CLASSES_ICON[size];\n const variantActionMap = VARIANT_ACTION_CLASSES[variant] || {};\n const variantClasses =\n typeof variantActionMap === 'string'\n ? variantActionMap\n : ((variantActionMap as Record<string, string>)[action] ??\n (variantActionMap as Record<string, string>).muted ??\n '');\n\n const baseClasses =\n 'inline-flex items-center justify-center rounded-xs font-normal gap-1 relative';\n\n const baseClassesIcon = 'flex items-center';\n if (variant === 'notification') {\n return (\n <div\n className={cn(baseClasses, variantClasses, sizeClasses, className)}\n {...props}\n >\n <Bell size={24} className=\"text-current\" aria-hidden=\"true\" />\n\n {notificationActive && (\n <span\n data-testid=\"notification-dot\"\n className=\"absolute top-[5px] right-[10px] block h-2 w-2 rounded-full bg-indicator-error ring-2 ring-white\"\n />\n )}\n </div>\n );\n }\n return (\n <div\n className={cn(baseClasses, variantClasses, sizeClasses, className)}\n {...props}\n >\n {iconLeft && (\n <span className={cn(baseClassesIcon, sizeClassesIcon)}>{iconLeft}</span>\n )}\n {children}\n {iconRight && (\n <span className={cn(baseClassesIcon, sizeClassesIcon)}>\n {iconRight}\n </span>\n )}\n </div>\n );\n};\n\nexport default Badge;\n","import { ButtonHTMLAttributes, ReactNode, forwardRef } from 'react';\nimport { cn } from '../../utils/utils';\n\n/**\n * SelectionButton component props interface\n */\ntype SelectionButtonProps = {\n /** Ícone a ser exibido no botão */\n icon: ReactNode;\n /** Texto/label a ser exibido ao lado do ícone */\n label: string;\n /** Estado de seleção do botão */\n selected?: boolean;\n /** Additional CSS classes to apply */\n className?: string;\n} & ButtonHTMLAttributes<HTMLButtonElement>;\n\n/**\n * SelectionButton component for Analytica Ensino platforms\n *\n * Um botão com ícone e texto para ações e navegação com estado de seleção.\n * Ideal para filtros, tags, categorias, seleção de tipos, etc.\n * Suporta forwardRef para acesso programático ao elemento DOM.\n *\n * @param icon - O ícone a ser exibido no botão\n * @param label - O texto/label a ser exibido\n * @param selected - Estado de seleção do botão\n * @param className - Classes CSS adicionais\n * @param props - Todos os outros atributos HTML padrão de button\n * @returns Um elemento button estilizado\n *\n * @example\n * ```tsx\n * <SelectionButton\n * icon={<TagIcon />}\n * label=\"Categoria\"\n * selected={false}\n * onClick={() => handleSelection()}\n * />\n * ```\n *\n * @example\n * ```tsx\n * // Usando ref para foco programático\n * const buttonRef = useRef<HTMLButtonElement>(null);\n *\n * const handleFocus = () => {\n * buttonRef.current?.focus();\n * };\n *\n * <SelectionButton\n * ref={buttonRef}\n * icon={<TagIcon />}\n * label=\"Categoria\"\n * selected={isSelected}\n * onClick={() => setSelected(!isSelected)}\n * />\n * ```\n */\nconst SelectionButton = forwardRef<HTMLButtonElement, SelectionButtonProps>(\n (\n { icon, label, selected = false, className = '', disabled, ...props },\n ref\n ) => {\n // Classes base para todos os estados\n const baseClasses = [\n 'inline-flex',\n 'items-center',\n 'justify-start',\n 'gap-2',\n 'p-4',\n 'rounded-xl',\n 'cursor-pointer',\n 'border',\n 'border-border-50',\n 'bg-background',\n 'text-sm',\n 'text-text-700',\n 'font-bold',\n 'shadow-soft-shadow-1',\n 'hover:bg-background-100',\n 'focus-visible:outline-none',\n 'focus-visible:ring-2',\n 'focus-visible:ring-indicator-info',\n 'focus-visible:ring-offset-0',\n 'focus-visible:shadow-none',\n 'active:ring-2',\n 'active:ring-primary-950',\n 'active:ring-offset-0',\n 'active:shadow-none',\n 'disabled:opacity-50',\n 'disabled:cursor-not-allowed',\n ];\n\n const stateClasses = selected\n ? ['ring-primary-950', 'ring-2', 'ring-offset-0', 'shadow-none']\n : [];\n\n const allClasses = [...baseClasses, ...stateClasses].join(' ');\n\n return (\n <button\n ref={ref}\n type=\"button\"\n className={cn(allClasses, className)}\n disabled={disabled}\n aria-pressed={selected}\n {...props}\n >\n <span className=\"flex items-center justify-center w-6 h-6\">{icon}</span>\n <span>{label}</span>\n </button>\n );\n }\n);\n\nSelectionButton.displayName = 'SelectionButton';\n\nexport default SelectionButton;\n","import {\n InputHTMLAttributes,\n ReactNode,\n forwardRef,\n useState,\n useId,\n ChangeEvent,\n} from 'react';\nimport Text from '../Text/Text';\nimport { Check, Minus } from 'phosphor-react';\nimport { cn } from '../../utils/utils';\n\n/**\n * CheckBox size variants\n */\ntype CheckBoxSize = 'small' | 'medium' | 'large';\n\n/**\n * CheckBox visual state\n */\ntype CheckBoxState = 'default' | 'hovered' | 'focused' | 'invalid' | 'disabled';\n\n/**\n * Size configurations using Tailwind classes\n */\nconst SIZE_CLASSES = {\n small: {\n checkbox: 'w-4 h-4', // 16px x 16px\n textSize: 'sm' as const,\n spacing: 'gap-1.5', // 6px\n borderWidth: 'border-2',\n iconSize: 14, // pixels for Phosphor icons\n labelHeight: 'h-[21px]',\n },\n medium: {\n checkbox: 'w-5 h-5', // 20px x 20px\n textSize: 'md' as const,\n spacing: 'gap-2', // 8px\n borderWidth: 'border-2',\n iconSize: 16, // pixels for Phosphor icons\n labelHeight: 'h-6',\n },\n large: {\n checkbox: 'w-6 h-6', // 24px x 24px\n textSize: 'lg' as const,\n spacing: 'gap-2', // 8px\n borderWidth: 'border-[3px]', // 3px border\n iconSize: 20, // pixels for Phosphor icons\n labelHeight: 'h-[27px]',\n },\n} as const;\n\n/**\n * Base checkbox styling classes using design system colors\n */\nconst BASE_CHECKBOX_CLASSES =\n 'rounded border cursor-pointer transition-all duration-200 flex items-center justify-center focus:outline-none';\n\n/**\n * State-based styling classes using design system colors from styles.css\n */\nconst STATE_CLASSES = {\n default: {\n unchecked: 'border-border-400 bg-background hover:border-border-500',\n checked:\n 'border-primary-950 bg-primary-950 text-text hover:border-primary-800 hover:bg-primary-800',\n },\n hovered: {\n unchecked: 'border-border-500 bg-background',\n checked: 'border-primary-800 bg-primary-800 text-text',\n },\n focused: {\n unchecked:\n 'border-indicator-info bg-background ring-2 ring-indicator-info/20',\n checked:\n 'border-indicator-info bg-primary-950 text-text ring-2 ring-indicator-info/20',\n },\n invalid: {\n unchecked: 'border-error-700 bg-background hover:border-error-600',\n checked: 'border-error-700 bg-primary-950 text-text',\n },\n disabled: {\n unchecked: 'border-border-400 bg-background cursor-not-allowed opacity-40',\n checked:\n 'border-primary-600 bg-primary-600 text-text cursor-not-allowed opacity-40',\n },\n} as const;\n\n/**\n * CheckBox component props interface\n */\nexport type CheckBoxProps = {\n /** Label text to display next to the checkbox */\n label?: ReactNode;\n /** Size variant of the checkbox */\n size?: CheckBoxSize;\n /** Visual state of the checkbox */\n state?: CheckBoxState;\n /** Indeterminate state for partial selections */\n indeterminate?: boolean;\n /** Error message to display */\n errorMessage?: string;\n /** Helper text to display */\n helperText?: string;\n /** Additional CSS classes */\n className?: string;\n /** Label CSS classes */\n labelClassName?: string;\n} & Omit<InputHTMLAttributes<HTMLInputElement>, 'size' | 'type'>;\n\n/**\n * CheckBox component for Analytica Ensino platforms\n *\n * A checkbox component with essential states, sizes and themes.\n * Uses the Analytica Ensino Design System colors from styles.css with automatic\n * light/dark mode support. Includes Text component integration for consistent typography.\n *\n * @example\n * ```tsx\n * // Basic checkbox\n * <CheckBox label=\"Option\" />\n *\n * // Small size\n * <CheckBox size=\"small\" label=\"Small option\" />\n *\n * // Invalid state\n * <CheckBox state=\"invalid\" label=\"Required field\" />\n *\n * // Disabled state\n * <CheckBox disabled label=\"Disabled option\" />\n * ```\n */\nconst CheckBox = forwardRef<HTMLInputElement, CheckBoxProps>(\n (\n {\n label,\n size = 'medium',\n state = 'default',\n indeterminate = false,\n errorMessage,\n helperText,\n className = '',\n labelClassName = '',\n checked: checkedProp,\n disabled,\n id,\n onChange,\n ...props\n },\n ref\n ) => {\n // Generate unique ID if not provided\n const generatedId = useId();\n const inputId = id ?? `checkbox-${generatedId}`;\n\n // Handle controlled vs uncontrolled behavior\n const [internalChecked, setInternalChecked] = useState(false);\n const isControlled = checkedProp !== undefined;\n const checked = isControlled ? checkedProp : internalChecked;\n\n // Handle change events\n const handleChange = (event: ChangeEvent<HTMLInputElement>) => {\n if (!isControlled) {\n setInternalChecked(event.target.checked);\n }\n onChange?.(event);\n };\n\n // Determine current state based on props\n const currentState = disabled ? 'disabled' : state;\n\n // Get size classes\n const sizeClasses = SIZE_CLASSES[size];\n\n // Determine checkbox visual variant\n const checkVariant = checked || indeterminate ? 'checked' : 'unchecked';\n\n // Get styling classes\n const stylingClasses = STATE_CLASSES[currentState][checkVariant];\n\n // Special border width handling for focused/hovered states and large size\n const borderWidthClass =\n state === 'focused' || (state === 'hovered' && size === 'large')\n ? 'border-[3px]'\n : sizeClasses.borderWidth;\n\n // Get final checkbox classes\n const checkboxClasses = cn(\n BASE_CHECKBOX_CLASSES,\n sizeClasses.checkbox,\n borderWidthClass,\n stylingClasses,\n className\n );\n\n // Render appropriate icon based on state\n const renderIcon = () => {\n if (indeterminate) {\n return (\n <Minus\n size={sizeClasses.iconSize}\n weight=\"bold\"\n color=\"currentColor\"\n />\n );\n }\n\n if (checked) {\n return (\n <Check\n size={sizeClasses.iconSize}\n weight=\"bold\"\n color=\"currentColor\"\n />\n );\n }\n\n return null;\n };\n\n return (\n <div className=\"flex flex-col\">\n <div\n className={cn(\n 'flex flex-row items-center',\n sizeClasses.spacing,\n disabled ? 'opacity-40' : ''\n )}\n >\n {/* Hidden native input for accessibility and form submission */}\n <input\n ref={ref}\n type=\"checkbox\"\n id={inputId}\n checked={checked}\n disabled={disabled}\n onChange={handleChange}\n className=\"sr-only\"\n {...props}\n />\n\n {/* Custom styled checkbox */}\n <label htmlFor={inputId} className={checkboxClasses}>\n {/* Show appropriate icon based on state */}\n {renderIcon()}\n </label>\n\n {/* Label text */}\n {label && (\n <div\n className={cn(\n 'flex flex-row items-center',\n sizeClasses.labelHeight\n )}\n >\n <Text\n as=\"label\"\n htmlFor={inputId}\n size={sizeClasses.textSize}\n weight=\"normal\"\n className={cn(\n 'cursor-pointer select-none leading-[150%] flex items-center font-roboto',\n labelClassName\n )}\n >\n {label}\n </Text>\n </div>\n )}\n </div>\n\n {/* Error message */}\n {errorMessage && (\n <Text\n size=\"sm\"\n weight=\"normal\"\n className=\"mt-1.5\"\n color=\"text-error-600\"\n >\n {errorMessage}\n </Text>\n )}\n\n {/* Helper text */}\n {helperText && !errorMessage && (\n <Text\n size=\"sm\"\n weight=\"normal\"\n className=\"mt-1.5\"\n color=\"text-text-500\"\n >\n {helperText}\n </Text>\n )}\n </div>\n );\n }\n);\n\nCheckBox.displayName = 'CheckBox';\n\nexport default CheckBox;\n","import type React from 'react';\nimport { useRef, useState } from 'react';\nimport { Image, Paperclip, X } from 'phosphor-react';\nimport Text from '../Text/Text';\nimport Button from '../Button/Button';\nimport { cn } from '../../utils/utils';\n\nexport interface ImageUploadProps {\n /** File currently selected */\n selectedFile?: File | null;\n /** Upload progress (0-100). If not provided, progress bar won't show */\n uploadProgress?: number;\n /** Show progress bar even if uploadProgress is 100 */\n showProgress?: boolean;\n /** Callback when a file is selected */\n onFileSelect?: (file: File) => void;\n /** Callback when file is removed */\n onRemoveFile?: () => void;\n /** Custom button text when no file is selected */\n buttonText?: string;\n /** Custom button icon when no file is selected */\n buttonIcon?: React.ReactNode;\n /** Accept specific file types (default: \"image/*\") */\n accept?: string;\n /** Disabled state */\n disabled?: boolean;\n /** Custom class name */\n className?: string;\n /** Max file size in bytes */\n maxSize?: number;\n /** Callback when file size exceeds maxSize */\n onSizeError?: (file: File, maxSize: number) => void;\n /** Callback when invalid file type is selected */\n onTypeError?: (file: File) => void;\n /** Variant of the upload button */\n variant?: 'default' | 'compact';\n}\n\nexport default function ImageUpload({\n selectedFile,\n onFileSelect,\n onRemoveFile,\n buttonText = 'Inserir imagem',\n buttonIcon,\n accept = 'image/*',\n disabled = false,\n className,\n maxSize,\n onSizeError,\n onTypeError,\n variant = 'default',\n}: Readonly<ImageUploadProps>) {\n const fileInputRef = useRef<HTMLInputElement>(null);\n const [internalFile, setInternalFile] = useState<File | null>(null);\n\n // Use controlled or uncontrolled mode\n const currentFile = selectedFile ?? internalFile;\n const hasFile = currentFile !== null;\n\n const handleButtonClick = () => {\n if (!disabled) {\n fileInputRef.current?.click();\n }\n };\n\n const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {\n const file = event.target.files?.[0];\n\n if (!file) return;\n\n // Validate file type\n const acceptedTypes = accept.split(',').map((type) => type.trim());\n const isValidType = acceptedTypes.some((type) => {\n if (type.endsWith('/*')) {\n const mainType = type.split('/')[0];\n return file.type.startsWith(mainType + '/');\n }\n if (type.startsWith('.')) {\n return file.name.toLowerCase().endsWith(type.toLowerCase());\n }\n return file.type === type;\n });\n\n if (!isValidType) {\n onTypeError?.(file);\n return;\n }\n\n // Validate file size\n if (maxSize && file.size > maxSize) {\n onSizeError?.(file, maxSize);\n return;\n }\n\n // Update state\n if (selectedFile === undefined) {\n setInternalFile(file);\n }\n onFileSelect?.(file);\n\n // Reset input value to allow selecting the same file again\n event.target.value = '';\n };\n\n const handleRemoveFile = () => {\n if (!disabled) {\n if (selectedFile === undefined) {\n setInternalFile(null);\n }\n onRemoveFile?.();\n }\n };\n\n if (variant === 'compact') {\n return (\n <div className={cn('inline-flex items-center gap-2', className)}>\n {hasFile ? (\n <div className=\"inline-flex items-center gap-2 bg-muted rounded-md px-3 py-1.5\">\n <Paperclip className=\"h-4 w-4 text-text-800\" />\n <Text size=\"sm\" weight=\"medium\" className=\"text-text-800\">\n {currentFile.name}\n </Text>\n <button\n type=\"button\"\n onClick={handleRemoveFile}\n disabled={disabled}\n className=\"hover:opacity-70 disabled:cursor-not-allowed disabled:opacity-50\"\n aria-label=\"Remover imagem\"\n title=\"Remover imagem\"\n >\n <X className=\"h-3 w-3 text-primary-950\" />\n </button>\n </div>\n ) : (\n <Button\n type=\"button\"\n variant=\"link\"\n size=\"extra-small\"\n onClick={handleButtonClick}\n disabled={disabled}\n >\n {buttonIcon || <Image className=\"h-4 w-4\" />}\n <span className=\"ml-2\">{buttonText}</span>\n </Button>\n )}\n <input\n ref={fileInputRef}\n type=\"file\"\n accept={accept}\n className=\"hidden\"\n onChange={handleFileChange}\n disabled={disabled}\n />\n </div>\n );\n }\n\n return (\n <div className={cn('space-y-3', className)}>\n {hasFile ? (\n <div className=\"space-y-2\">\n <div className=\"inline-flex items-center gap-2 bg-muted rounded-md px-3 py-2\">\n <Paperclip className=\"h-4 w-4 text-text-800\" />\n <Text size=\"sm\" weight=\"medium\" className=\"text-text-800\">\n {currentFile.name}\n </Text>\n <button\n type=\"button\"\n onClick={handleRemoveFile}\n disabled={disabled}\n className=\"hover:opacity-70 disabled:cursor-not-allowed disabled:opacity-50\"\n >\n <X className=\"h-4 w-4 text-primary-950 cursor-pointer\" />\n </button>\n </div>\n </div>\n ) : (\n <Button\n type=\"button\"\n variant=\"link\"\n onClick={handleButtonClick}\n disabled={disabled}\n >\n {buttonIcon || <Image className=\"h-4 w-4 mr-2\" />}\n {buttonText}\n </Button>\n )}\n <input\n ref={fileInputRef}\n type=\"file\"\n accept={accept}\n className=\"hidden\"\n onChange={handleFileChange}\n disabled={disabled}\n />\n </div>\n );\n}\n","import { useEffect, useMemo, useRef, useState } from 'react';\nimport {\n AccordionGroup,\n Badge,\n CardAccordation,\n CheckBox,\n cn,\n Text,\n Divider,\n} from '../../';\nimport {\n areSelectedIdsEqual,\n isCategoryEnabled as isCategoryEnabledHelper,\n getBadgeText as getBadgeTextHelper,\n handleAccordionValueChange as handleAccordionValueChangeHelper,\n calculateFormattedItemsForAutoSelection,\n} from './CheckBoxGroup.helpers';\n\nexport type Item = {\n id: string;\n name: string;\n [key: string]: unknown;\n};\n\nexport type CategoryConfig = {\n key: string;\n label: string;\n selectedIds?: string[];\n dependsOn?: string[];\n itens?: Item[];\n filteredBy?: { key: string; internalField: string }[];\n};\n\nexport const CheckboxGroup = ({\n categories,\n onCategoriesChange,\n compactSingleItem = true,\n showDivider = true,\n showSingleItem = false,\n}: {\n categories: CategoryConfig[];\n onCategoriesChange: (categories: CategoryConfig[]) => void;\n compactSingleItem?: boolean;\n showDivider?: boolean;\n showSingleItem?: boolean;\n}) => {\n const [openAccordion, setOpenAccordion] = useState<string>('');\n\n // Refs to prevent infinite loops and track auto-selection state\n const autoSelectionAppliedRef = useRef<boolean>(false);\n const onCategoriesChangeRef = useRef(onCategoriesChange);\n const previousCategoriesRef = useRef<CategoryConfig[]>(categories);\n\n // Update ref when onCategoriesChange changes\n useEffect(() => {\n onCategoriesChangeRef.current = onCategoriesChange;\n }, [onCategoriesChange]);\n\n // Use helper function for comparing selectedIds arrays\n\n // Auto-seleciona categorias com apenas um item (considerando itens filtrados)\n const categoriesWithAutoSelection = useMemo(() => {\n return categories.map((category) => {\n // Get filtered/visible items for this category\n const filteredItems = calculateFormattedItemsForAutoSelection(\n category,\n categories\n );\n\n // Se tem apenas um item filtrado/visível e nenhum está selecionado, auto-seleciona\n if (\n filteredItems.length === 1 &&\n (!category.selectedIds || category.selectedIds.length === 0)\n ) {\n return {\n ...category,\n selectedIds: [filteredItems[0].id],\n };\n }\n return category;\n });\n }, [categories]);\n\n // Aplica a auto-seleção se necessário\n // Note: onCategoriesChange should be memoized by the parent component to prevent re-renders\n useEffect(() => {\n // Check if categories have actually changed by comparing with previous reference\n const categoriesChanged = categories !== previousCategoriesRef.current;\n\n if (!categoriesChanged && autoSelectionAppliedRef.current) {\n // No changes and auto-selection already applied, skip\n return;\n }\n\n // Update previous categories reference\n previousCategoriesRef.current = categories;\n\n // Check for auto-selection changes using efficient comparison\n const hasAutoSelectionChanges = categoriesWithAutoSelection.some(\n (cat, index) => {\n const originalCat = categories[index];\n return !areSelectedIdsEqual(cat.selectedIds, originalCat.selectedIds);\n }\n );\n\n if (hasAutoSelectionChanges) {\n autoSelectionAppliedRef.current = true;\n // Use ref to avoid dependency on potentially non-memoized callback\n onCategoriesChangeRef.current(categoriesWithAutoSelection);\n } else if (categoriesChanged) {\n // Reset auto-selection flag when categories change externally\n autoSelectionAppliedRef.current = false;\n }\n }, [categoriesWithAutoSelection, categories]);\n\n const isCheckBoxIsSelected = (categoryKey: string, itemId: string) => {\n const category = categories.find((c) => c.key === categoryKey);\n if (!category) return false;\n return category.selectedIds?.includes(itemId) || false;\n };\n\n const isMinimalOneCheckBoxIsSelected = (categoryKey: string) => {\n const category = categories.find((c) => c.key === categoryKey);\n if (!category) return false;\n\n // Obtém apenas os itens filtrados (visíveis)\n const formattedItems = getFormattedItems(categoryKey);\n const filteredItems = formattedItems.flatMap((group) => group.itens || []);\n const filteredItemIds = filteredItems.map((item) => item.id);\n\n // Verifica se pelo menos um item filtrado está selecionado\n return filteredItemIds.some((itemId) =>\n category.selectedIds?.includes(itemId)\n );\n };\n\n // Helper function to cr