UNPKG

analytica-frontend-lib

Version:

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

1 lines 107 kB
{"version":3,"sources":["../../src/components/Card/Card.tsx","../../src/utils/utils.ts","../../src/components/Button/Button.tsx","../../src/components/Badge/Badge.tsx","../../src/components/Text/Text.tsx","../../src/components/ProgressBar/ProgressBar.tsx"],"sourcesContent":["import {\n forwardRef,\n Fragment,\n HTMLAttributes,\n ReactNode,\n useState,\n useRef,\n MouseEvent,\n ChangeEvent,\n KeyboardEvent,\n Ref,\n} from 'react';\nimport Button from '../Button/Button';\nimport Badge from '../Badge/Badge';\nimport ProgressBar from '../ProgressBar/ProgressBar';\nimport {\n CaretRight,\n ChatCircleText,\n CheckCircle,\n Clock,\n DotsThreeVertical,\n Play,\n SpeakerHigh,\n SpeakerLow,\n SpeakerSimpleX,\n XCircle,\n} from 'phosphor-react';\nimport Text from '../Text/Text';\nimport { cn } from '../../utils/utils';\n\n// Componente base reutilizável para todos os cards\ninterface CardBaseProps extends HTMLAttributes<HTMLDivElement> {\n children: ReactNode;\n variant?: 'default' | 'compact' | 'minimal';\n layout?: 'horizontal' | 'vertical';\n padding?: 'none' | 'small' | 'medium' | 'large';\n minHeight?: 'none' | 'small' | 'medium' | 'large';\n cursor?: 'default' | 'pointer';\n}\n\nconst CARD_BASE_CLASSES = {\n default: 'w-full bg-background border border-border-50 rounded-xl',\n compact: 'w-full bg-background border border-border-50 rounded-lg',\n minimal: 'w-full bg-background border border-border-100 rounded-md',\n};\n\nconst CARD_PADDING_CLASSES = {\n none: '',\n small: 'p-2',\n medium: 'p-4',\n large: 'p-6',\n};\n\nconst CARD_MIN_HEIGHT_CLASSES = {\n none: '',\n small: 'min-h-16',\n medium: 'min-h-20',\n large: 'min-h-24',\n};\n\nconst CARD_LAYOUT_CLASSES = {\n horizontal: 'flex flex-row',\n vertical: 'flex flex-col',\n};\n\nconst CARD_CURSOR_CLASSES = {\n default: '',\n pointer: 'cursor-pointer',\n};\n\nconst CardBase = forwardRef<HTMLDivElement, CardBaseProps>(\n (\n {\n children,\n variant = 'default',\n layout = 'horizontal',\n padding = 'medium',\n minHeight = 'medium',\n cursor = 'default',\n className = '',\n ...props\n },\n ref\n ) => {\n const baseClasses = CARD_BASE_CLASSES[variant];\n const paddingClasses = CARD_PADDING_CLASSES[padding];\n const minHeightClasses = CARD_MIN_HEIGHT_CLASSES[minHeight];\n const layoutClasses = CARD_LAYOUT_CLASSES[layout];\n const cursorClasses = CARD_CURSOR_CLASSES[cursor];\n\n const combinedClasses = [\n baseClasses,\n paddingClasses,\n minHeightClasses,\n layoutClasses,\n cursorClasses,\n className,\n ]\n .filter(Boolean)\n .join(' ');\n\n return (\n <div ref={ref} className={combinedClasses} {...props}>\n {children}\n </div>\n );\n }\n);\n\ninterface CardActivitiesResultsProps extends HTMLAttributes<HTMLDivElement> {\n icon: ReactNode;\n title: string;\n subTitle: string;\n header: string;\n description?: string;\n extended?: boolean;\n action?: 'warning' | 'success' | 'error' | 'info';\n}\n\nconst ACTION_CARD_CLASSES = {\n warning: 'bg-warning-background',\n success: 'bg-success-300',\n error: 'bg-error-100',\n info: 'bg-info-background',\n};\n\nconst ACTION_ICON_CLASSES = {\n warning: 'bg-warning-300 text-text',\n success: 'bg-yellow-300 text-text-950',\n error: 'bg-error-500 text-text',\n info: 'bg-info-500 text-text',\n};\n\nconst ACTION_SUBTITLE_CLASSES = {\n warning: 'text-warning-600',\n success: 'text-success-700',\n error: 'text-error-700',\n info: 'text-info-700',\n};\n\nconst ACTION_HEADER_CLASSES = {\n warning: 'text-warning-300',\n success: 'text-success-300',\n error: 'text-error-300',\n info: 'text-info-300',\n};\n\nconst CardActivitiesResults = forwardRef<\n HTMLDivElement,\n CardActivitiesResultsProps\n>(\n (\n {\n icon,\n title,\n subTitle,\n header,\n extended = false,\n action = 'success',\n description,\n className,\n ...props\n },\n ref\n ) => {\n const actionCardClasses = ACTION_CARD_CLASSES[action];\n const actionIconClasses = ACTION_ICON_CLASSES[action];\n const actionSubTitleClasses = ACTION_SUBTITLE_CLASSES[action];\n const actionHeaderClasses = ACTION_HEADER_CLASSES[action];\n\n return (\n <div\n ref={ref}\n className={cn(\n 'w-full flex flex-col border border-border-50 bg-background rounded-xl',\n className\n )}\n {...props}\n >\n <div\n className={cn(\n 'flex flex-col gap-1 items-center justify-center p-4',\n actionCardClasses,\n extended ? 'rounded-t-xl' : 'rounded-xl'\n )}\n >\n <span\n className={cn(\n 'size-7.5 rounded-full flex items-center justify-center',\n actionIconClasses\n )}\n >\n {icon}\n </span>\n\n <Text\n size=\"2xs\"\n weight=\"medium\"\n className=\"text-text-800 uppercase truncate\"\n >\n {title}\n </Text>\n\n <p\n className={cn('text-lg font-bold truncate', actionSubTitleClasses)}\n >\n {subTitle}\n </p>\n </div>\n\n {extended && (\n <div className=\"flex flex-col items-center gap-2.5 pb-9.5 pt-2.5\">\n <p\n className={cn(\n 'text-2xs font-medium uppercase truncate',\n actionHeaderClasses\n )}\n >\n {header}\n </p>\n <Badge size=\"large\" action=\"info\">\n {description}\n </Badge>\n </div>\n )}\n </div>\n );\n }\n);\n\ninterface CardQuestionProps extends HTMLAttributes<HTMLDivElement> {\n header: string;\n state?: 'done' | 'undone';\n onClickButton?: (valueButton?: unknown) => void;\n valueButton?: unknown;\n}\n\nconst CardQuestions = forwardRef<HTMLDivElement, CardQuestionProps>(\n (\n {\n header,\n state = 'undone',\n className,\n onClickButton,\n valueButton,\n ...props\n },\n ref\n ) => {\n const isDone = state === 'done';\n const stateLabel = isDone ? 'Realizado' : 'Não Realizado';\n const buttonLabel = isDone ? 'Ver Questão' : 'Responder';\n\n return (\n <CardBase\n ref={ref}\n layout=\"horizontal\"\n padding=\"medium\"\n minHeight=\"medium\"\n className={cn('justify-between gap-4', className)}\n {...props}\n >\n <section className=\"flex flex-col gap-1 flex-1 min-w-0\">\n <p className=\"font-bold text-xs text-text-950 truncate\">{header}</p>\n\n <div className=\"flex flex-row gap-6 items-center\">\n <Badge\n size=\"medium\"\n variant=\"solid\"\n action={isDone ? 'success' : 'error'}\n >\n {stateLabel}\n </Badge>\n\n <span className=\"flex flex-row items-center gap-1 text-text-700 text-xs\">\n {isDone ? 'Nota' : 'Sem nota'}\n {isDone && (\n <Badge size=\"medium\" action=\"success\">\n 00\n </Badge>\n )}\n </span>\n </div>\n </section>\n\n <span className=\"flex-shrink-0\">\n <Button\n size=\"extra-small\"\n onClick={() => onClickButton?.(valueButton)}\n className=\"min-w-fit\"\n >\n {buttonLabel}\n </Button>\n </span>\n </CardBase>\n );\n }\n);\n\ninterface CardProgressProps extends HTMLAttributes<HTMLDivElement> {\n header: string;\n subhead?: string;\n initialDate?: string;\n endDate?: string;\n progress?: number;\n direction?: 'horizontal' | 'vertical';\n icon: ReactNode;\n color?: string;\n progressVariant?: 'blue' | 'green';\n showDates?: boolean;\n}\n\nconst CardProgress = forwardRef<HTMLDivElement, CardProgressProps>(\n (\n {\n header,\n subhead,\n initialDate,\n endDate,\n progress = 0,\n direction = 'horizontal',\n icon,\n color = '#B7DFFF',\n progressVariant = 'blue',\n showDates = true,\n className,\n ...props\n },\n ref\n ) => {\n const isHorizontal = direction === 'horizontal';\n const contentComponent = {\n horizontal: (\n <>\n {showDates && (\n <div className=\"flex flex-row gap-6 items-center\">\n {initialDate && (\n <span className=\"flex flex-row gap-1 items-center text-2xs\">\n <p className=\"text-text-800 font-semibold\">Início</p>\n <p className=\"text-text-600\">{initialDate}</p>\n </span>\n )}\n {endDate && (\n <span className=\"flex flex-row gap-1 items-center text-2xs\">\n <p className=\"text-text-800 font-semibold\">Fim</p>\n <p className=\"text-text-600\">{endDate}</p>\n </span>\n )}\n </div>\n )}\n <span className=\"grid grid-cols-[1fr_auto] items-center gap-2\">\n <ProgressBar\n size=\"small\"\n value={progress}\n variant={progressVariant}\n data-testid=\"progress-bar\"\n />\n\n <Text\n size=\"xs\"\n weight=\"medium\"\n className={cn(\n 'text-text-950 leading-none tracking-normal text-center flex-none'\n )}\n >\n {Math.round(progress)}%\n </Text>\n </span>\n </>\n ),\n vertical: <p className=\"text-sm text-text-800\">{subhead}</p>,\n };\n\n return (\n <CardBase\n ref={ref}\n layout={isHorizontal ? 'horizontal' : 'vertical'}\n padding=\"none\"\n minHeight=\"medium\"\n cursor=\"pointer\"\n className={cn(isHorizontal ? 'h-20' : '', className)}\n {...props}\n >\n <div\n className={cn(\n 'flex justify-center items-center [&>svg]:size-6 text-text-950',\n isHorizontal\n ? 'min-w-[80px] min-h-[80px] rounded-l-xl'\n : 'min-h-[50px] w-full rounded-t-xl',\n !color.startsWith('#') ? `bg-${color}` : ''\n )}\n style={color.startsWith('#') ? { backgroundColor: color } : undefined}\n data-testid=\"icon-container\"\n >\n {icon}\n </div>\n\n <div\n className={cn(\n 'p-4 flex flex-col justify-between w-full h-full',\n !isHorizontal && 'gap-4'\n )}\n >\n <Text size=\"sm\" weight=\"bold\" className=\"text-text-950 truncate\">\n {header}\n </Text>\n {contentComponent[direction]}\n </div>\n </CardBase>\n );\n }\n);\n\ninterface CardTopicProps extends HTMLAttributes<HTMLDivElement> {\n header: string;\n subHead?: string[];\n progress: number;\n showPercentage?: boolean;\n progressVariant?: 'blue' | 'green';\n}\n\nconst CardTopic = forwardRef<HTMLDivElement, CardTopicProps>(\n (\n {\n header,\n subHead,\n progress,\n showPercentage = false,\n progressVariant = 'blue',\n className = '',\n ...props\n },\n ref\n ) => {\n return (\n <CardBase\n ref={ref}\n layout=\"vertical\"\n padding=\"small\"\n minHeight=\"medium\"\n cursor=\"pointer\"\n className={cn('justify-center gap-2 py-2 px-4', className)}\n {...props}\n >\n {subHead && (\n <span className=\"text-text-600 text-2xs flex flex-row gap-1\">\n {subHead.map((text, index) => (\n <Fragment key={`${text} - ${index}`}>\n <p>{text}</p>\n {index < subHead.length - 1 && <p>•</p>}\n </Fragment>\n ))}\n </span>\n )}\n\n <p className=\"text-sm text-text-950 font-bold truncate\">{header}</p>\n\n <span className=\"grid grid-cols-[1fr_auto] items-center gap-2\">\n <ProgressBar\n size=\"small\"\n value={progress}\n variant={progressVariant}\n data-testid=\"progress-bar\"\n />\n {showPercentage && (\n <Text\n size=\"xs\"\n weight=\"medium\"\n className={cn(\n 'text-text-950 leading-none tracking-normal text-center flex-none'\n )}\n >\n {Math.round(progress)}%\n </Text>\n )}\n </span>\n </CardBase>\n );\n }\n);\n\ninterface CardPerformanceProps extends HTMLAttributes<HTMLDivElement> {\n header: string;\n description?: string;\n progress?: number;\n labelProgress?: string;\n actionVariant?: 'button' | 'caret';\n progressVariant?: 'blue' | 'green';\n onClickButton?: (valueButton?: unknown) => void;\n valueButton?: unknown;\n}\n\nconst CardPerformance = forwardRef<HTMLDivElement, CardPerformanceProps>(\n (\n {\n header,\n progress,\n description = 'Sem dados ainda! Você ainda não fez um questionário neste assunto.',\n actionVariant = 'button',\n progressVariant = 'blue',\n labelProgress = '',\n className = '',\n onClickButton,\n valueButton,\n ...props\n },\n ref\n ) => {\n const hasProgress = progress !== undefined;\n\n return (\n <CardBase\n ref={ref}\n layout=\"horizontal\"\n padding=\"medium\"\n minHeight=\"none\"\n className={cn(\n actionVariant == 'caret' ? 'cursor-pointer' : '',\n className\n )}\n onClick={() => actionVariant == 'caret' && onClickButton?.(valueButton)}\n {...props}\n >\n <div className=\"w-full flex flex-col justify-between gap-2\">\n <div className=\"flex flex-row justify-between items-center gap-2\">\n <p className=\"text-lg font-bold text-text-950 truncate flex-1 min-w-0\">\n {header}\n </p>\n {actionVariant === 'button' && (\n <Button\n variant=\"outline\"\n size=\"extra-small\"\n onClick={() => onClickButton?.(valueButton)}\n className=\"min-w-fit flex-shrink-0\"\n >\n Ver Aula\n </Button>\n )}\n </div>\n\n <div className=\"w-full\">\n {hasProgress ? (\n <ProgressBar\n value={progress}\n label={`${progress}% ${labelProgress}`}\n variant={progressVariant}\n />\n ) : (\n <p className=\"text-xs text-text-600 truncate\">{description}</p>\n )}\n </div>\n </div>\n\n {actionVariant == 'caret' && (\n <CaretRight\n className=\"size-4.5 text-text-800 cursor-pointer\"\n data-testid=\"caret-icon\"\n />\n )}\n </CardBase>\n );\n }\n);\n\ninterface CardResultsProps extends HTMLAttributes<HTMLDivElement> {\n header: string;\n icon: ReactNode;\n correct_answers: number;\n incorrect_answers: number;\n direction?: 'row' | 'col';\n color?: string;\n}\n\nconst CardResults = forwardRef<HTMLDivElement, CardResultsProps>(\n (\n {\n header,\n correct_answers,\n incorrect_answers,\n icon,\n direction = 'col',\n color = '#B7DFFF',\n className,\n ...props\n },\n ref\n ) => {\n const isRow = direction == 'row';\n\n return (\n <CardBase\n ref={ref}\n layout=\"horizontal\"\n padding=\"none\"\n minHeight=\"medium\"\n className={cn('items-center cursor-pointer pr-4', className)}\n {...props}\n >\n <div\n className={cn(\n 'flex justify-center items-center [&>svg]:size-8 text-text-950 min-w-20 max-w-20 min-h-20 h-full rounded-l-xl'\n )}\n style={{\n backgroundColor: color,\n }}\n >\n {icon}\n </div>\n\n <div\n className={cn(\n 'p-4 flex justify-between w-full h-full',\n isRow ? 'flex-row items-center gap-2' : 'flex-col'\n )}\n >\n <p className=\"text-sm font-bold text-text-950 truncate flex-1 min-w-0\">\n {header}\n </p>\n <span className=\"flex flex-row gap-1 items-center\">\n <Badge\n action=\"success\"\n variant=\"solid\"\n size=\"large\"\n iconLeft={<CheckCircle />}\n >\n {correct_answers} Corretas\n </Badge>\n\n <Badge\n action=\"error\"\n variant=\"solid\"\n size=\"large\"\n iconLeft={<XCircle />}\n >\n {incorrect_answers} Incorretas\n </Badge>\n </span>\n </div>\n\n <CaretRight className=\"min-w-6 min-h-6 text-text-800\" />\n </CardBase>\n );\n }\n);\n\ninterface CardStatusProps extends HTMLAttributes<HTMLDivElement> {\n header: string;\n status?: 'correct' | 'incorrect';\n label?: string;\n}\n\nconst CardStatus = forwardRef<HTMLDivElement, CardStatusProps>(\n ({ header, className, status, label, ...props }, ref) => {\n return (\n <CardBase\n ref={ref}\n layout=\"horizontal\"\n padding=\"medium\"\n minHeight=\"medium\"\n className={cn('items-center cursor-pointer', className)}\n {...props}\n >\n <div className=\"flex justify-between w-full h-full flex-row items-center gap-2\">\n <p className=\"text-sm font-bold text-text-950 truncate flex-1 min-w-0\">\n {header}\n </p>\n <span className=\"flex flex-row gap-1 items-center flex-shrink-0\">\n {status && (\n <Badge\n action={status == 'correct' ? 'success' : 'error'}\n variant=\"solid\"\n size=\"medium\"\n iconLeft={<CheckCircle />}\n >\n {status == 'correct' ? 'Correta' : 'Incorreta'}\n </Badge>\n )}\n {label && <p className=\"text-sm text-text-800\">{label}</p>}\n </span>\n <CaretRight className=\"min-w-6 min-h-6 text-text-800 cursor-pointer flex-shrink-0 ml-2\" />\n </div>\n </CardBase>\n );\n }\n);\n\ninterface CardSettingsProps extends HTMLAttributes<HTMLDivElement> {\n icon: ReactNode;\n header: string;\n}\n\nconst CardSettings = forwardRef<HTMLDivElement, CardSettingsProps>(\n ({ header, className, icon, ...props }, ref) => {\n return (\n <CardBase\n ref={ref}\n layout=\"horizontal\"\n padding=\"small\"\n minHeight=\"none\"\n className={cn(\n 'border-none items-center gap-2 text-text-700',\n className\n )}\n {...props}\n >\n <span className=\"[&>svg]:size-6\">{icon}</span>\n\n <p className=\"w-full text-sm truncate\">{header}</p>\n\n <CaretRight size={24} className=\"cursor-pointer\" />\n </CardBase>\n );\n }\n);\n\ninterface CardSupportProps extends HTMLAttributes<HTMLDivElement> {\n header: string;\n direction?: 'row' | 'col';\n children: ReactNode;\n}\n\nconst CardSupport = forwardRef<HTMLDivElement, CardSupportProps>(\n ({ header, className, direction = 'col', children, ...props }, ref) => {\n return (\n <CardBase\n ref={ref}\n layout=\"horizontal\"\n padding=\"medium\"\n minHeight=\"none\"\n className={cn(\n 'border-none items-center gap-2 text-text-700',\n className\n )}\n {...props}\n >\n <div\n className={cn(\n 'w-full flex',\n direction == 'col' ? 'flex-col' : 'flex-row items-center'\n )}\n >\n <span className=\"w-full min-w-0\">\n <p className=\"text-sm text-text-950 font-bold truncate\">{header}</p>\n </span>\n <span className=\"flex flex-row gap-1\">{children}</span>\n </div>\n\n <CaretRight className=\"text-text-800 cursor-pointer\" size={24} />\n </CardBase>\n );\n }\n);\n\ninterface CardForumProps<T = unknown> extends HTMLAttributes<HTMLDivElement> {\n title: string;\n content: string;\n comments: number;\n date: string;\n hour: string;\n onClickComments?: (value?: T) => void;\n valueComments?: T;\n onClickProfile?: (profile?: T) => void;\n valueProfile?: T;\n}\n\nconst CardForum = forwardRef<HTMLDivElement, CardForumProps>(\n (\n {\n title,\n content,\n comments,\n onClickComments,\n valueComments,\n onClickProfile,\n valueProfile,\n className = '',\n date,\n hour,\n ...props\n },\n ref\n ) => {\n return (\n <CardBase\n ref={ref}\n layout=\"horizontal\"\n padding=\"medium\"\n minHeight=\"none\"\n variant=\"minimal\"\n className={cn('w-auto h-auto gap-3', className)}\n {...props}\n >\n <button\n type=\"button\"\n aria-label=\"Ver perfil\"\n onClick={() => onClickProfile?.(valueProfile)}\n className=\"min-w-8 h-8 rounded-full bg-background-950\"\n />\n\n <div className=\"flex flex-col gap-2 flex-1 min-w-0\">\n <div className=\"flex flex-row gap-1 items-center flex-wrap\">\n <p className=\"text-xs font-semibold text-primary-700 truncate\">\n {title}\n </p>\n <p className=\"text-xs text-text-600\">\n • {date} • {hour}\n </p>\n </div>\n\n <p className=\"text-text-950 text-sm line-clamp-2 truncate\">\n {content}\n </p>\n\n <button\n type=\"button\"\n aria-label=\"Ver comentários\"\n onClick={() => onClickComments?.(valueComments)}\n className=\"text-text-600 flex flex-row gap-2 items-center\"\n >\n <ChatCircleText aria-hidden=\"true\" size={16} />\n <p className=\"text-xs\">{comments} respostas</p>\n </button>\n </div>\n </CardBase>\n );\n }\n);\n\ninterface CardAudioProps extends HTMLAttributes<HTMLDivElement> {\n src?: string;\n title?: string;\n onPlay?: () => void;\n onPause?: () => void;\n onEnded?: () => void;\n onAudioTimeUpdate?: (currentTime: number, duration: number) => void;\n loop?: boolean;\n preload?: 'none' | 'metadata' | 'auto';\n tracks?: Array<{\n kind: 'subtitles' | 'captions' | 'descriptions' | 'chapters' | 'metadata';\n src: string;\n srcLang: string;\n label: string;\n default?: boolean;\n }>;\n}\n\nconst CardAudio = forwardRef<HTMLDivElement, CardAudioProps>(\n (\n {\n src,\n title,\n onPlay,\n onPause,\n onEnded,\n onAudioTimeUpdate,\n loop = false,\n preload = 'metadata',\n tracks,\n className,\n ...props\n },\n ref\n ) => {\n const [isPlaying, setIsPlaying] = useState(false);\n const [currentTime, setCurrentTime] = useState(0);\n const [duration, setDuration] = useState(0);\n const [volume, setVolume] = useState(1);\n const [showVolumeControl, setShowVolumeControl] = useState(false);\n const audioRef = useRef<HTMLAudioElement>(null);\n\n const formatTime = (time: number) => {\n const minutes = Math.floor(time / 60);\n const seconds = Math.floor(time % 60);\n return `${minutes}:${seconds.toString().padStart(2, '0')}`;\n };\n\n const handlePlayPause = () => {\n if (isPlaying) {\n audioRef.current?.pause();\n setIsPlaying(false);\n onPause?.();\n } else {\n audioRef.current?.play();\n setIsPlaying(true);\n onPlay?.();\n }\n };\n\n const handleTimeUpdate = () => {\n const current = audioRef.current?.currentTime ?? 0;\n const total = audioRef.current?.duration ?? 0;\n\n setCurrentTime(current);\n setDuration(total);\n onAudioTimeUpdate?.(current, total);\n };\n\n const handleLoadedMetadata = () => {\n setDuration(audioRef.current?.duration ?? 0);\n };\n\n const handleEnded = () => {\n setIsPlaying(false);\n setCurrentTime(0);\n onEnded?.();\n };\n\n const handleProgressClick = (e: MouseEvent<HTMLButtonElement>) => {\n const rect = e.currentTarget.getBoundingClientRect();\n const clickX = e.clientX - rect.left;\n const width = rect.width;\n const percentage = clickX / width;\n const newTime = percentage * duration;\n\n if (audioRef.current) {\n audioRef.current.currentTime = newTime;\n }\n setCurrentTime(newTime);\n };\n\n const handleVolumeChange = (e: ChangeEvent<HTMLInputElement>) => {\n const newVolume = parseFloat(e.target.value);\n setVolume(newVolume);\n if (audioRef.current) {\n audioRef.current.volume = newVolume;\n }\n };\n\n const toggleVolumeControl = () => {\n setShowVolumeControl(!showVolumeControl);\n };\n\n const getVolumeIcon = () => {\n if (volume === 0) {\n return <SpeakerSimpleX />;\n }\n if (volume < 0.5) {\n return <SpeakerLow />;\n }\n return <SpeakerHigh />;\n };\n\n return (\n <CardBase\n ref={ref}\n layout=\"horizontal\"\n padding=\"medium\"\n minHeight=\"none\"\n className={cn('w-auto h-14 items-center gap-2', className)}\n {...props}\n >\n {/* Audio element */}\n <audio\n ref={audioRef}\n src={src}\n loop={loop}\n preload={preload}\n onTimeUpdate={handleTimeUpdate}\n onLoadedMetadata={handleLoadedMetadata}\n onEnded={handleEnded}\n data-testid=\"audio-element\"\n aria-label={title}\n >\n {tracks ? (\n tracks.map((track) => (\n <track\n key={track.src}\n kind={track.kind}\n src={track.src}\n srcLang={track.srcLang}\n label={track.label}\n default={track.default}\n />\n ))\n ) : (\n <track\n kind=\"captions\"\n src=\"data:text/vtt;base64,\"\n srcLang=\"pt\"\n label=\"Sem legendas disponíveis\"\n />\n )}\n </audio>\n\n {/* Play/Pause Button */}\n <button\n type=\"button\"\n onClick={handlePlayPause}\n disabled={!src}\n className=\"cursor-pointer text-text-950 hover:text-primary-600 disabled:text-text-400 disabled:cursor-not-allowed\"\n aria-label={isPlaying ? 'Pausar' : 'Reproduzir'}\n >\n {isPlaying ? (\n <div className=\"w-6 h-6 flex items-center justify-center\">\n <div className=\"flex gap-0.5\">\n <div className=\"w-1 h-4 bg-current rounded-sm\"></div>\n <div className=\"w-1 h-4 bg-current rounded-sm\"></div>\n </div>\n </div>\n ) : (\n <Play size={24} />\n )}\n </button>\n\n {/* Current Time */}\n <p className=\"text-text-800 text-sm font-medium min-w-[2.5rem]\">\n {formatTime(currentTime)}\n </p>\n\n {/* Progress Bar */}\n <div className=\"flex-1 relative\" data-testid=\"progress-bar\">\n <button\n type=\"button\"\n className=\"w-full h-2 bg-border-100 rounded-full cursor-pointer\"\n onClick={handleProgressClick}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n handleProgressClick(\n e as unknown as MouseEvent<HTMLButtonElement>\n );\n }\n }}\n aria-label=\"Barra de progresso do áudio\"\n >\n <div\n className=\"h-full bg-primary-600 rounded-full transition-all duration-100\"\n style={{\n width:\n duration > 0 ? `${(currentTime / duration) * 100}%` : '0%',\n }}\n />\n </button>\n </div>\n\n {/* Duration */}\n <p className=\"text-text-800 text-sm font-medium min-w-[2.5rem]\">\n {formatTime(duration)}\n </p>\n\n {/* Volume Control */}\n <div className=\"relative\">\n <button\n type=\"button\"\n onClick={toggleVolumeControl}\n className=\"cursor-pointer text-text-950 hover:text-primary-600\"\n aria-label=\"Controle de volume\"\n >\n <div className=\"w-6 h-6 flex items-center justify-center\">\n {getVolumeIcon()}\n </div>\n </button>\n\n {showVolumeControl && (\n <button\n type=\"button\"\n className=\"absolute bottom-full right-0 mb-2 p-2 bg-background border border-border-100 rounded-lg shadow-lg focus:outline-none focus:ring-2 focus:ring-primary-500\"\n onKeyDown={(e) => {\n if (e.key === 'Escape') {\n setShowVolumeControl(false);\n }\n }}\n >\n <input\n type=\"range\"\n min=\"0\"\n max=\"1\"\n step=\"0.1\"\n value={volume}\n onChange={handleVolumeChange}\n onKeyDown={(e) => {\n if (e.key === 'ArrowUp' || e.key === 'ArrowRight') {\n e.preventDefault();\n const newVolume = Math.min(\n 1,\n Math.round((volume + 0.1) * 10) / 10\n );\n setVolume(newVolume);\n if (audioRef.current) audioRef.current.volume = newVolume;\n } else if (e.key === 'ArrowDown' || e.key === 'ArrowLeft') {\n e.preventDefault();\n const newVolume = Math.max(\n 0,\n Math.round((volume - 0.1) * 10) / 10\n );\n setVolume(newVolume);\n if (audioRef.current) audioRef.current.volume = newVolume;\n }\n }}\n className=\"w-20 h-2 bg-border-100 rounded-lg appearance-none cursor-pointer\"\n style={{\n background: `linear-gradient(to right, #3b82f6 0%, #3b82f6 ${volume * 100}%, #e5e7eb ${volume * 100}%, #e5e7eb 100%)`,\n }}\n aria-label=\"Volume\"\n aria-valuenow={Math.round(volume * 100)}\n aria-valuemin={0}\n aria-valuemax={100}\n />\n </button>\n )}\n </div>\n\n {/* Menu Button */}\n <DotsThreeVertical\n size={24}\n className=\"text-text-950 cursor-pointer hover:text-primary-600\"\n />\n </CardBase>\n );\n }\n);\n\ninterface CardSimuladoProps extends HTMLAttributes<HTMLDivElement> {\n title: string;\n duration?: string;\n info: string;\n backgroundColor: 'enem' | 'prova' | 'simuladao' | 'vestibular';\n}\n\nconst SIMULADO_BACKGROUND_CLASSES = {\n enem: 'bg-exam-1',\n prova: 'bg-exam-2',\n simuladao: 'bg-exam-3',\n vestibular: 'bg-exam-4',\n};\n\nconst CardSimulado = forwardRef<HTMLDivElement, CardSimuladoProps>(\n ({ title, duration, info, backgroundColor, className, ...props }, ref) => {\n const backgroundClass = SIMULADO_BACKGROUND_CLASSES[backgroundColor];\n\n return (\n <CardBase\n ref={ref}\n layout=\"horizontal\"\n padding=\"medium\"\n minHeight=\"none\"\n cursor=\"pointer\"\n className={cn(\n `${backgroundClass} hover:shadow-soft-shadow-2 transition-shadow duration-200`,\n className\n )}\n {...props}\n >\n <div className=\"flex justify-between items-center w-full gap-4\">\n <div className=\"flex flex-col gap-1 flex-1 min-w-0\">\n <Text size=\"lg\" weight=\"bold\" className=\"text-text-950 truncate\">\n {title}\n </Text>\n\n <div className=\"flex items-center gap-4 text-text-700\">\n {duration && (\n <div className=\"flex items-center gap-1\">\n <Clock size={16} className=\"flex-shrink-0\" />\n <Text size=\"sm\">{duration}</Text>\n </div>\n )}\n\n <Text size=\"sm\" className=\"truncate\">\n {info}\n </Text>\n </div>\n </div>\n\n <CaretRight\n size={24}\n className=\"text-text-800 flex-shrink-0\"\n data-testid=\"caret-icon\"\n />\n </div>\n </CardBase>\n );\n }\n);\n\ninterface CardTestProps extends Omit<HTMLAttributes<HTMLElement>, 'onSelect'> {\n title: string;\n duration?: string;\n questionsCount?: number;\n additionalInfo?: string;\n selected?: boolean;\n onSelect?: (selected: boolean) => void;\n}\n\nconst CardTest = forwardRef<HTMLElement, CardTestProps>(\n (\n {\n title,\n duration,\n questionsCount,\n additionalInfo,\n selected = false,\n onSelect,\n className = '',\n ...props\n },\n ref\n ) => {\n const handleClick = () => {\n if (onSelect) {\n onSelect(!selected);\n }\n };\n\n const handleKeyDown = (event: KeyboardEvent<HTMLElement>) => {\n if ((event.key === 'Enter' || event.key === ' ') && onSelect) {\n event.preventDefault();\n onSelect(!selected);\n }\n };\n\n const isSelectable = !!onSelect;\n const getQuestionsText = (count: number) => {\n const singular = count === 1 ? 'questão' : 'questões';\n return `${count} ${singular}`;\n };\n\n const displayInfo = questionsCount\n ? getQuestionsText(questionsCount)\n : additionalInfo || '';\n const baseClasses =\n 'flex flex-row items-center p-4 gap-2 w-full max-w-full bg-background shadow-soft-shadow-1 rounded-xl isolate border-0 text-left';\n const interactiveClasses = isSelectable\n ? 'cursor-pointer focus:outline-none focus:ring-2 focus:ring-primary-950 focus:ring-offset-2'\n : '';\n const selectedClasses = selected\n ? 'ring-2 ring-primary-950 ring-offset-2'\n : '';\n\n if (isSelectable) {\n return (\n <button\n ref={ref as Ref<HTMLButtonElement>}\n type=\"button\"\n className={cn(\n `${baseClasses} ${interactiveClasses} ${selectedClasses} ${className}`.trim()\n )}\n onClick={handleClick}\n onKeyDown={handleKeyDown}\n aria-pressed={selected}\n {...(props as HTMLAttributes<HTMLButtonElement>)}\n >\n <div className=\"flex flex-col justify-between gap-[27px] flex-grow min-h-[67px] w-full min-w-0\">\n <Text\n size=\"md\"\n weight=\"bold\"\n className=\"text-text-950 tracking-[0.2px] leading-[19px] truncate\"\n >\n {title}\n </Text>\n\n <div className=\"flex flex-row justify-start items-end gap-4 w-full\">\n {duration && (\n <div className=\"flex flex-row items-center gap-1 flex-shrink-0\">\n <Clock size={16} className=\"text-text-700\" />\n <Text\n size=\"sm\"\n className=\"text-text-700 leading-[21px] whitespace-nowrap\"\n >\n {duration}\n </Text>\n </div>\n )}\n\n <Text\n size=\"sm\"\n className=\"text-text-700 leading-[21px] flex-grow truncate\"\n >\n {displayInfo}\n </Text>\n </div>\n </div>\n </button>\n );\n }\n\n return (\n <div\n ref={ref as Ref<HTMLDivElement>}\n className={cn(`${baseClasses} ${className}`.trim())}\n {...(props as HTMLAttributes<HTMLDivElement>)}\n >\n <div className=\"flex flex-col justify-between gap-[27px] flex-grow min-h-[67px] w-full min-w-0\">\n <Text\n size=\"md\"\n weight=\"bold\"\n className=\"text-text-950 tracking-[0.2px] leading-[19px] truncate\"\n >\n {title}\n </Text>\n\n <div className=\"flex flex-row justify-start items-end gap-4 w-full\">\n {duration && (\n <div className=\"flex flex-row items-center gap-1 flex-shrink-0\">\n <Clock size={16} className=\"text-text-700\" />\n <Text\n size=\"sm\"\n className=\"text-text-700 leading-[21px] whitespace-nowrap\"\n >\n {duration}\n </Text>\n </div>\n )}\n\n <Text\n size=\"sm\"\n className=\"text-text-700 leading-[21px] flex-grow truncate min-w-0\"\n >\n {displayInfo}\n </Text>\n </div>\n </div>\n </div>\n );\n }\n);\n\ninterface SimulationItem {\n id: string;\n title: string;\n type: 'enem' | 'prova' | 'simulado' | 'vestibular';\n info: string;\n}\n\ninterface SimulationHistoryData {\n date: string;\n simulations: SimulationItem[];\n}\n\ninterface CardSimulationHistoryProps extends HTMLAttributes<HTMLDivElement> {\n data: SimulationHistoryData[];\n onSimulationClick?: (simulation: SimulationItem) => void;\n}\n\nconst SIMULATION_TYPE_STYLES = {\n enem: {\n background: 'bg-exam-1',\n badge: 'exam1' as const,\n text: 'Enem',\n },\n prova: {\n background: 'bg-exam-2',\n badge: 'exam2' as const,\n text: 'Prova',\n },\n simulado: {\n background: 'bg-exam-3',\n badge: 'exam3' as const,\n text: 'Simulado',\n },\n vestibular: {\n background: 'bg-exam-4',\n badge: 'exam4' as const,\n text: 'Vestibular',\n },\n};\n\nconst CardSimulationHistory = forwardRef<\n HTMLDivElement,\n CardSimulationHistoryProps\n>(({ data, onSimulationClick, className, ...props }, ref) => {\n return (\n <div\n ref={ref}\n className={cn('w-full max-w-[992px] h-auto', className)}\n {...props}\n >\n {/* Content */}\n <div className=\"flex flex-col gap-0\">\n {data.map((section, sectionIndex) => (\n <div key={section.date} className=\"flex flex-col\">\n {/* Seção com data */}\n <div\n className={cn(\n 'flex flex-row justify-center items-start px-4 py-6 gap-2 w-full bg-white',\n sectionIndex === 0 ? 'rounded-t-3xl' : ''\n )}\n >\n <Text\n size=\"xs\"\n weight=\"bold\"\n className=\"text-text-800 w-11 flex-shrink-0\"\n >\n {section.date}\n </Text>\n\n <div className=\"flex flex-col gap-2 flex-1\">\n {section.simulations.map((simulation) => {\n const typeStyles = SIMULATION_TYPE_STYLES[simulation.type];\n\n return (\n <CardBase\n key={simulation.id}\n layout=\"horizontal\"\n padding=\"medium\"\n minHeight=\"none\"\n cursor=\"pointer\"\n className={cn(\n `${typeStyles.background} rounded-xl hover:shadow-soft-shadow-2 \n transition-shadow duration-200 h-auto min-h-[61px]`\n )}\n onClick={() => onSimulationClick?.(simulation)}\n >\n <div className=\"flex justify-between items-center w-full gap-2\">\n <div className=\"flex flex-col gap-2 flex-1 min-w-0\">\n <Text\n size=\"lg\"\n weight=\"bold\"\n className=\"text-text-950 truncate\"\n >\n {simulation.title}\n </Text>\n\n <div className=\"flex items-center gap-2\">\n <Badge\n variant=\"examsOutlined\"\n action={typeStyles.badge}\n size=\"medium\"\n >\n {typeStyles.text}\n </Badge>\n\n <Text size=\"sm\" className=\"text-text-800 truncate\">\n {simulation.info}\n </Text>\n </div>\n </div>\n\n <CaretRight\n size={24}\n className=\"text-text-800 flex-shrink-0\"\n data-testid=\"caret-icon\"\n />\n </div>\n </CardBase>\n );\n })}\n </div>\n </div>\n </div>\n ))}\n\n {/* Footer rounded */}\n {data.length > 0 && (\n <div className=\"w-full h-6 bg-white rounded-b-3xl\" />\n )}\n </div>\n </div>\n );\n});\n\nexport {\n CardBase,\n CardActivitiesResults,\n CardQuestions,\n CardProgress,\n CardTopic,\n CardPerformance,\n CardResults,\n CardStatus,\n CardSettings,\n CardSupport,\n CardForum,\n CardAudio,\n CardSimulado,\n CardTest,\n CardSimulationHistory,\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","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