analytica-frontend-lib 1.1.91 → 1.1.92

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -224,7 +224,6 @@ var DownloadButton = ({
224
224
  await new Promise((resolve) => setTimeout(resolve, 200));
225
225
  }
226
226
  } catch (error) {
227
- console.error(`Erro ao baixar ${item.label}:`, error);
228
227
  onDownloadError?.(
229
228
  item.type,
230
229
  error instanceof Error ? error : new Error(`Falha ao baixar ${item.label}`)
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/DownloadButton/DownloadButton.tsx","../../src/components/IconButton/IconButton.tsx","../../src/utils/utils.ts"],"sourcesContent":["import { useCallback, useState } from 'react';\nimport { DownloadSimple } from 'phosphor-react';\nimport IconButton from '../IconButton/IconButton';\nimport { cn } from '../../utils/utils';\n\n/**\n * Download content interface for lesson materials\n */\nexport interface DownloadContent {\n /** Document URL (PDF) */\n urlDoc?: string;\n /** Initial frame image URL */\n urlInitialFrame?: string;\n /** Final frame image URL */\n urlFinalFrame?: string;\n /** Podcast audio URL */\n urlPodcast?: string;\n /** Video URL */\n urlVideo?: string;\n}\n\n/**\n * Props for DownloadButton component\n */\nexport interface DownloadButtonProps {\n /** Content URLs to download */\n content: DownloadContent;\n /** Additional CSS classes */\n className?: string;\n /** Callback fired when download starts */\n onDownloadStart?: (contentType: string) => void;\n /** Callback fired when download completes */\n onDownloadComplete?: (contentType: string) => void;\n /** Callback fired when download fails */\n onDownloadError?: (contentType: string, error: Error) => void;\n /** Lesson title for download file naming */\n lessonTitle?: string;\n /** Whether the button is disabled */\n disabled?: boolean;\n}\n\n/**\n * Get MIME type based on file extension\n * @param url - URL to extract extension from\n * @returns MIME type string\n */\nconst getMimeType = (url: string): string => {\n const extension = getFileExtension(url);\n const mimeTypes: Record<string, string> = {\n pdf: 'application/pdf',\n png: 'image/png',\n jpg: 'image/jpeg',\n jpeg: 'image/jpeg',\n mp3: 'audio/mpeg',\n mp4: 'video/mp4',\n vtt: 'text/vtt',\n };\n return mimeTypes[extension] || 'application/octet-stream';\n};\n\n/**\n * Download file via fetch and blob to ensure proper download behavior\n * @param url - URL to download\n * @param filename - Filename for the download\n * @returns Promise<void>\n */\nconst triggerDownload = async (\n url: string,\n filename: string\n): Promise<void> => {\n try {\n // Fetch the file as blob\n const response = await fetch(url, {\n mode: 'cors',\n credentials: 'same-origin',\n });\n\n if (!response.ok) {\n throw new Error(\n `Failed to fetch file: ${response.status} ${response.statusText}`\n );\n }\n\n const blob = await response.blob();\n const mimeType = getMimeType(url);\n\n // Create a blob with the correct MIME type\n const typedBlob = new Blob([blob], { type: mimeType });\n\n // Create object URL\n const blobUrl = URL.createObjectURL(typedBlob);\n\n // Create download link\n const link = document.createElement('a');\n link.href = blobUrl;\n link.download = filename;\n link.rel = 'noopener noreferrer';\n\n // Add to DOM, click, and remove\n document.body.appendChild(link);\n link.click();\n link.remove();\n\n // Clean up object URL after a short delay\n setTimeout(() => {\n URL.revokeObjectURL(blobUrl);\n }, 1000);\n } catch (error) {\n // Fallback to direct link if fetch fails\n console.warn('Fetch download failed, falling back to direct link:', error);\n\n const link = document.createElement('a');\n link.href = url;\n link.download = filename;\n link.rel = 'noopener noreferrer';\n link.target = '_blank'; // Open in new tab as fallback\n\n document.body.appendChild(link);\n link.click();\n link.remove();\n }\n};\n\n/**\n * Get file extension from URL\n * @param url - URL to extract extension from\n * @returns File extension or default\n */\nconst getFileExtension = (url: string): string => {\n try {\n const u = new URL(url, globalThis.location?.origin || 'http://localhost');\n url = u.pathname;\n } catch {\n // keep original url (likely relative)\n }\n const path = url.split(/[?#]/)[0];\n const dot = path.lastIndexOf('.');\n return dot > -1 ? path.slice(dot + 1).toLowerCase() : 'file';\n};\n\n/**\n * Generate filename for download\n * @param contentType - Type of content being downloaded\n * @param lessonTitle - Title of the lesson\n * @param url - URL to get extension from\n * @returns Generated filename\n */\nconst generateFilename = (\n contentType: string,\n url: string,\n lessonTitle: string = 'aula'\n): string => {\n const sanitizedTitle = lessonTitle\n .toLowerCase()\n .replaceAll(/[^a-z0-9\\s]/g, '')\n .replaceAll(/\\s+/g, '-')\n .substring(0, 50);\n\n const extension = getFileExtension(url);\n return `${sanitizedTitle}-${contentType}.${extension}`;\n};\n\n/**\n * DownloadButton component for downloading lesson content\n * Provides a single button that downloads all available content for a lesson\n *\n * @param props - DownloadButton component props\n * @returns Download button element\n */\nconst DownloadButton = ({\n content,\n className,\n onDownloadStart,\n onDownloadComplete,\n onDownloadError,\n lessonTitle = 'aula',\n disabled = false,\n}: DownloadButtonProps) => {\n const [isDownloading, setIsDownloading] = useState(false);\n\n /**\n * Check if URL is valid and not empty\n * @param url - URL to validate\n * @returns Whether URL is valid\n */\n const isValidUrl = useCallback((url?: string): boolean => {\n return Boolean(\n url && url.trim() !== '' && url !== 'undefined' && url !== 'null'\n );\n }, []);\n\n /**\n * Get available download content\n * @returns Array of available download items\n */\n const getAvailableContent = useCallback(() => {\n const downloads: Array<{ type: string; url: string; label: string }> = [];\n\n if (isValidUrl(content.urlDoc)) {\n downloads.push({\n type: 'documento',\n url: content.urlDoc!,\n label: 'Documento',\n });\n }\n\n if (isValidUrl(content.urlInitialFrame)) {\n downloads.push({\n type: 'quadro-inicial',\n url: content.urlInitialFrame!,\n label: 'Quadro Inicial',\n });\n }\n\n if (isValidUrl(content.urlFinalFrame)) {\n downloads.push({\n type: 'quadro-final',\n url: content.urlFinalFrame!,\n label: 'Quadro Final',\n });\n }\n\n if (isValidUrl(content.urlPodcast)) {\n downloads.push({\n type: 'podcast',\n url: content.urlPodcast!,\n label: 'Podcast',\n });\n }\n\n if (isValidUrl(content.urlVideo)) {\n downloads.push({ type: 'video', url: content.urlVideo!, label: 'Vídeo' });\n }\n\n return downloads;\n }, [content, isValidUrl]);\n\n /**\n * Handle download of all available content\n */\n const handleDownload = useCallback(async () => {\n if (disabled || isDownloading) return;\n\n const availableContent = getAvailableContent();\n\n if (availableContent.length === 0) {\n return;\n }\n\n setIsDownloading(true);\n\n try {\n // Download each available content sequentially with small delay\n for (let i = 0; i < availableContent.length; i++) {\n const item = availableContent[i];\n\n try {\n onDownloadStart?.(item.type);\n\n const filename = generateFilename(item.type, item.url, lessonTitle);\n await triggerDownload(item.url, filename);\n\n onDownloadComplete?.(item.type);\n\n // Add small delay between downloads to prevent browser blocking\n if (i < availableContent.length - 1) {\n await new Promise((resolve) => setTimeout(resolve, 200));\n }\n } catch (error) {\n console.error(`Erro ao baixar ${item.label}:`, error);\n onDownloadError?.(\n item.type,\n error instanceof Error\n ? error\n : new Error(`Falha ao baixar ${item.label}`)\n );\n }\n }\n } finally {\n setIsDownloading(false);\n }\n }, [\n disabled,\n isDownloading,\n getAvailableContent,\n lessonTitle,\n onDownloadStart,\n onDownloadComplete,\n onDownloadError,\n ]);\n\n // Don't render if no content is available\n const hasContent = getAvailableContent().length > 0;\n\n if (!hasContent) {\n return null;\n }\n\n return (\n <div className={cn('flex items-center', className)}>\n <IconButton\n icon={<DownloadSimple size={24} />}\n onClick={handleDownload}\n disabled={disabled || isDownloading}\n aria-label={(() => {\n if (isDownloading) {\n return 'Baixando conteúdo...';\n }\n const contentCount = getAvailableContent().length;\n const suffix = contentCount > 1 ? 's' : '';\n return `Baixar conteúdo da aula (${contentCount} arquivo${suffix})`;\n })()}\n className={cn(\n '!bg-transparent hover:!bg-black/10 transition-colors',\n isDownloading && 'opacity-60 cursor-not-allowed'\n )}\n />\n </div>\n );\n};\n\nexport default DownloadButton;\n","import { ButtonHTMLAttributes, ReactNode, forwardRef } from 'react';\nimport { cn } from '../../utils/utils';\n\n/**\n * IconButton component props interface\n */\nexport type IconButtonProps = {\n /** Ícone a ser exibido no botão */\n icon: ReactNode;\n /** Tamanho do botão */\n size?: 'sm' | 'md';\n /** Estado de seleção/ativo do botão - permanece ativo até ser clicado novamente ou outro botão ser ativado */\n active?: boolean;\n /** Additional CSS classes to apply */\n className?: string;\n} & ButtonHTMLAttributes<HTMLButtonElement>;\n\n/**\n * IconButton component for Analytica Ensino platforms\n *\n * Um botão compacto apenas com ícone, ideal para menus dropdown,\n * barras de ferramentas e ações secundárias.\n * Oferece dois tamanhos com estilo consistente.\n * Estado ativo permanece até ser clicado novamente ou outro botão ser ativado.\n * Suporta forwardRef para acesso programático ao elemento DOM.\n *\n * @param icon - O ícone a ser exibido no botão\n * @param size - Tamanho do botão (sm, md)\n * @param active - Estado ativo/selecionado 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 compacto estilizado apenas com ícone\n *\n * @example\n * ```tsx\n * <IconButton\n * icon={<MoreVerticalIcon />}\n * size=\"sm\"\n * onClick={() => openMenu()}\n * />\n * ```\n *\n * @example\n * ```tsx\n * // Botão ativo em uma barra de ferramentas - permanece ativo até outro clique\n * <IconButton\n * icon={<BoldIcon />}\n * active={isBold}\n * onClick={toggleBold}\n * />\n * ```\n *\n * @example\n * ```tsx\n * // Usando ref para controle programático\n * const buttonRef = useRef<HTMLButtonElement>(null);\n *\n * <IconButton\n * ref={buttonRef}\n * icon={<EditIcon />}\n * size=\"md\"\n * onClick={() => startEditing()}\n * />\n * ```\n */\nconst IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(\n (\n { icon, size = 'md', active = 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-center',\n 'rounded-lg',\n 'font-medium',\n 'bg-transparent',\n 'text-text-950',\n 'cursor-pointer',\n 'hover:bg-primary-600',\n 'hover:text-text',\n 'focus-visible:outline-none',\n 'focus-visible:ring-2',\n 'focus-visible:ring-offset-0',\n 'focus-visible:ring-indicator-info',\n 'disabled:opacity-50',\n 'disabled:cursor-not-allowed',\n 'disabled:pointer-events-none',\n ];\n\n // Classes de tamanho\n const sizeClasses = {\n sm: ['w-6', 'h-6', 'text-sm'],\n md: ['w-10', 'h-10', 'text-base'],\n };\n\n // Classes de estado ativo\n const activeClasses = active\n ? ['!bg-primary-50', '!text-primary-950', 'hover:!bg-primary-100']\n : [];\n\n const allClasses = [\n ...baseClasses,\n ...sizeClasses[size],\n ...activeClasses,\n ].join(' ');\n\n // Garantir acessibilidade com aria-label padrão\n const ariaLabel = props['aria-label'] ?? 'Botão de ação';\n\n return (\n <button\n ref={ref}\n type=\"button\"\n className={cn(allClasses, className)}\n disabled={disabled}\n aria-pressed={active}\n aria-label={ariaLabel}\n {...props}\n >\n <span className=\"flex items-center justify-center\">{icon}</span>\n </button>\n );\n }\n);\n\nIconButton.displayName = 'IconButton';\n\nexport default IconButton;\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';\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"],"mappings":"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","names":["import_react","import_jsx_runtime"]}
1
+ {"version":3,"sources":["../../src/components/DownloadButton/DownloadButton.tsx","../../src/components/IconButton/IconButton.tsx","../../src/utils/utils.ts"],"sourcesContent":["import { useCallback, useState } from 'react';\nimport { DownloadSimple } from 'phosphor-react';\nimport IconButton from '../IconButton/IconButton';\nimport { cn } from '../../utils/utils';\n\n/**\n * Download content interface for lesson materials\n */\nexport interface DownloadContent {\n /** Document URL (PDF) */\n urlDoc?: string;\n /** Initial frame image URL */\n urlInitialFrame?: string;\n /** Final frame image URL */\n urlFinalFrame?: string;\n /** Podcast audio URL */\n urlPodcast?: string;\n /** Video URL */\n urlVideo?: string;\n}\n\n/**\n * Props for DownloadButton component\n */\nexport interface DownloadButtonProps {\n /** Content URLs to download */\n content: DownloadContent;\n /** Additional CSS classes */\n className?: string;\n /** Callback fired when download starts */\n onDownloadStart?: (contentType: string) => void;\n /** Callback fired when download completes */\n onDownloadComplete?: (contentType: string) => void;\n /** Callback fired when download fails */\n onDownloadError?: (contentType: string, error: Error) => void;\n /** Lesson title for download file naming */\n lessonTitle?: string;\n /** Whether the button is disabled */\n disabled?: boolean;\n}\n\n/**\n * Get MIME type based on file extension\n * @param url - URL to extract extension from\n * @returns MIME type string\n */\nconst getMimeType = (url: string): string => {\n const extension = getFileExtension(url);\n const mimeTypes: Record<string, string> = {\n pdf: 'application/pdf',\n png: 'image/png',\n jpg: 'image/jpeg',\n jpeg: 'image/jpeg',\n mp3: 'audio/mpeg',\n mp4: 'video/mp4',\n vtt: 'text/vtt',\n };\n return mimeTypes[extension] || 'application/octet-stream';\n};\n\n/**\n * Download file via fetch and blob to ensure proper download behavior\n * @param url - URL to download\n * @param filename - Filename for the download\n * @returns Promise<void>\n */\nconst triggerDownload = async (\n url: string,\n filename: string\n): Promise<void> => {\n try {\n // Fetch the file as blob\n const response = await fetch(url, {\n mode: 'cors',\n credentials: 'same-origin',\n });\n\n if (!response.ok) {\n throw new Error(\n `Failed to fetch file: ${response.status} ${response.statusText}`\n );\n }\n\n const blob = await response.blob();\n const mimeType = getMimeType(url);\n\n // Create a blob with the correct MIME type\n const typedBlob = new Blob([blob], { type: mimeType });\n\n // Create object URL\n const blobUrl = URL.createObjectURL(typedBlob);\n\n // Create download link\n const link = document.createElement('a');\n link.href = blobUrl;\n link.download = filename;\n link.rel = 'noopener noreferrer';\n\n // Add to DOM, click, and remove\n document.body.appendChild(link);\n link.click();\n link.remove();\n\n // Clean up object URL after a short delay\n setTimeout(() => {\n URL.revokeObjectURL(blobUrl);\n }, 1000);\n } catch (error) {\n // Fallback to direct link if fetch fails\n console.warn('Fetch download failed, falling back to direct link:', error);\n\n const link = document.createElement('a');\n link.href = url;\n link.download = filename;\n link.rel = 'noopener noreferrer';\n link.target = '_blank'; // Open in new tab as fallback\n\n document.body.appendChild(link);\n link.click();\n link.remove();\n }\n};\n\n/**\n * Get file extension from URL\n * @param url - URL to extract extension from\n * @returns File extension or default\n */\nconst getFileExtension = (url: string): string => {\n try {\n const u = new URL(url, globalThis.location?.origin || 'http://localhost');\n url = u.pathname;\n } catch {\n // keep original url (likely relative)\n }\n const path = url.split(/[?#]/)[0];\n const dot = path.lastIndexOf('.');\n return dot > -1 ? path.slice(dot + 1).toLowerCase() : 'file';\n};\n\n/**\n * Generate filename for download\n * @param contentType - Type of content being downloaded\n * @param lessonTitle - Title of the lesson\n * @param url - URL to get extension from\n * @returns Generated filename\n */\nconst generateFilename = (\n contentType: string,\n url: string,\n lessonTitle: string = 'aula'\n): string => {\n const sanitizedTitle = lessonTitle\n .toLowerCase()\n .replaceAll(/[^a-z0-9\\s]/g, '')\n .replaceAll(/\\s+/g, '-')\n .substring(0, 50);\n\n const extension = getFileExtension(url);\n return `${sanitizedTitle}-${contentType}.${extension}`;\n};\n\n/**\n * DownloadButton component for downloading lesson content\n * Provides a single button that downloads all available content for a lesson\n *\n * @param props - DownloadButton component props\n * @returns Download button element\n */\nconst DownloadButton = ({\n content,\n className,\n onDownloadStart,\n onDownloadComplete,\n onDownloadError,\n lessonTitle = 'aula',\n disabled = false,\n}: DownloadButtonProps) => {\n const [isDownloading, setIsDownloading] = useState(false);\n\n /**\n * Check if URL is valid and not empty\n * @param url - URL to validate\n * @returns Whether URL is valid\n */\n const isValidUrl = useCallback((url?: string): boolean => {\n return Boolean(\n url && url.trim() !== '' && url !== 'undefined' && url !== 'null'\n );\n }, []);\n\n /**\n * Get available download content\n * @returns Array of available download items\n */\n const getAvailableContent = useCallback(() => {\n const downloads: Array<{ type: string; url: string; label: string }> = [];\n\n if (isValidUrl(content.urlDoc)) {\n downloads.push({\n type: 'documento',\n url: content.urlDoc!,\n label: 'Documento',\n });\n }\n\n if (isValidUrl(content.urlInitialFrame)) {\n downloads.push({\n type: 'quadro-inicial',\n url: content.urlInitialFrame!,\n label: 'Quadro Inicial',\n });\n }\n\n if (isValidUrl(content.urlFinalFrame)) {\n downloads.push({\n type: 'quadro-final',\n url: content.urlFinalFrame!,\n label: 'Quadro Final',\n });\n }\n\n if (isValidUrl(content.urlPodcast)) {\n downloads.push({\n type: 'podcast',\n url: content.urlPodcast!,\n label: 'Podcast',\n });\n }\n\n if (isValidUrl(content.urlVideo)) {\n downloads.push({ type: 'video', url: content.urlVideo!, label: 'Vídeo' });\n }\n\n return downloads;\n }, [content, isValidUrl]);\n\n /**\n * Handle download of all available content\n */\n const handleDownload = useCallback(async () => {\n if (disabled || isDownloading) return;\n\n const availableContent = getAvailableContent();\n\n if (availableContent.length === 0) {\n return;\n }\n\n setIsDownloading(true);\n\n try {\n // Download each available content sequentially with small delay\n for (let i = 0; i < availableContent.length; i++) {\n const item = availableContent[i];\n\n try {\n onDownloadStart?.(item.type);\n\n const filename = generateFilename(item.type, item.url, lessonTitle);\n await triggerDownload(item.url, filename);\n\n onDownloadComplete?.(item.type);\n\n // Add small delay between downloads to prevent browser blocking\n if (i < availableContent.length - 1) {\n await new Promise((resolve) => setTimeout(resolve, 200));\n }\n } catch (error) {\n // Silent error handling - delegate to callback\n onDownloadError?.(\n item.type,\n error instanceof Error\n ? error\n : new Error(`Falha ao baixar ${item.label}`)\n );\n }\n }\n } finally {\n setIsDownloading(false);\n }\n }, [\n disabled,\n isDownloading,\n getAvailableContent,\n lessonTitle,\n onDownloadStart,\n onDownloadComplete,\n onDownloadError,\n ]);\n\n // Don't render if no content is available\n const hasContent = getAvailableContent().length > 0;\n\n if (!hasContent) {\n return null;\n }\n\n return (\n <div className={cn('flex items-center', className)}>\n <IconButton\n icon={<DownloadSimple size={24} />}\n onClick={handleDownload}\n disabled={disabled || isDownloading}\n aria-label={(() => {\n if (isDownloading) {\n return 'Baixando conteúdo...';\n }\n const contentCount = getAvailableContent().length;\n const suffix = contentCount > 1 ? 's' : '';\n return `Baixar conteúdo da aula (${contentCount} arquivo${suffix})`;\n })()}\n className={cn(\n '!bg-transparent hover:!bg-black/10 transition-colors',\n isDownloading && 'opacity-60 cursor-not-allowed'\n )}\n />\n </div>\n );\n};\n\nexport default DownloadButton;\n","import { ButtonHTMLAttributes, ReactNode, forwardRef } from 'react';\nimport { cn } from '../../utils/utils';\n\n/**\n * IconButton component props interface\n */\nexport type IconButtonProps = {\n /** Ícone a ser exibido no botão */\n icon: ReactNode;\n /** Tamanho do botão */\n size?: 'sm' | 'md';\n /** Estado de seleção/ativo do botão - permanece ativo até ser clicado novamente ou outro botão ser ativado */\n active?: boolean;\n /** Additional CSS classes to apply */\n className?: string;\n} & ButtonHTMLAttributes<HTMLButtonElement>;\n\n/**\n * IconButton component for Analytica Ensino platforms\n *\n * Um botão compacto apenas com ícone, ideal para menus dropdown,\n * barras de ferramentas e ações secundárias.\n * Oferece dois tamanhos com estilo consistente.\n * Estado ativo permanece até ser clicado novamente ou outro botão ser ativado.\n * Suporta forwardRef para acesso programático ao elemento DOM.\n *\n * @param icon - O ícone a ser exibido no botão\n * @param size - Tamanho do botão (sm, md)\n * @param active - Estado ativo/selecionado 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 compacto estilizado apenas com ícone\n *\n * @example\n * ```tsx\n * <IconButton\n * icon={<MoreVerticalIcon />}\n * size=\"sm\"\n * onClick={() => openMenu()}\n * />\n * ```\n *\n * @example\n * ```tsx\n * // Botão ativo em uma barra de ferramentas - permanece ativo até outro clique\n * <IconButton\n * icon={<BoldIcon />}\n * active={isBold}\n * onClick={toggleBold}\n * />\n * ```\n *\n * @example\n * ```tsx\n * // Usando ref para controle programático\n * const buttonRef = useRef<HTMLButtonElement>(null);\n *\n * <IconButton\n * ref={buttonRef}\n * icon={<EditIcon />}\n * size=\"md\"\n * onClick={() => startEditing()}\n * />\n * ```\n */\nconst IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(\n (\n { icon, size = 'md', active = 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-center',\n 'rounded-lg',\n 'font-medium',\n 'bg-transparent',\n 'text-text-950',\n 'cursor-pointer',\n 'hover:bg-primary-600',\n 'hover:text-text',\n 'focus-visible:outline-none',\n 'focus-visible:ring-2',\n 'focus-visible:ring-offset-0',\n 'focus-visible:ring-indicator-info',\n 'disabled:opacity-50',\n 'disabled:cursor-not-allowed',\n 'disabled:pointer-events-none',\n ];\n\n // Classes de tamanho\n const sizeClasses = {\n sm: ['w-6', 'h-6', 'text-sm'],\n md: ['w-10', 'h-10', 'text-base'],\n };\n\n // Classes de estado ativo\n const activeClasses = active\n ? ['!bg-primary-50', '!text-primary-950', 'hover:!bg-primary-100']\n : [];\n\n const allClasses = [\n ...baseClasses,\n ...sizeClasses[size],\n ...activeClasses,\n ].join(' ');\n\n // Garantir acessibilidade com aria-label padrão\n const ariaLabel = props['aria-label'] ?? 'Botão de ação';\n\n return (\n <button\n ref={ref}\n type=\"button\"\n className={cn(allClasses, className)}\n disabled={disabled}\n aria-pressed={active}\n aria-label={ariaLabel}\n {...props}\n >\n <span className=\"flex items-center justify-center\">{icon}</span>\n </button>\n );\n }\n);\n\nIconButton.displayName = 'IconButton';\n\nexport default IconButton;\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';\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"],"mappings":"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","names":["import_react","import_jsx_runtime"]}
@@ -200,7 +200,6 @@ var DownloadButton = ({
200
200
  await new Promise((resolve) => setTimeout(resolve, 200));
201
201
  }
202
202
  } catch (error) {
203
- console.error(`Erro ao baixar ${item.label}:`, error);
204
203
  onDownloadError?.(
205
204
  item.type,
206
205
  error instanceof Error ? error : new Error(`Falha ao baixar ${item.label}`)
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/DownloadButton/DownloadButton.tsx","../../src/components/IconButton/IconButton.tsx","../../src/utils/utils.ts"],"sourcesContent":["import { useCallback, useState } from 'react';\nimport { DownloadSimple } from 'phosphor-react';\nimport IconButton from '../IconButton/IconButton';\nimport { cn } from '../../utils/utils';\n\n/**\n * Download content interface for lesson materials\n */\nexport interface DownloadContent {\n /** Document URL (PDF) */\n urlDoc?: string;\n /** Initial frame image URL */\n urlInitialFrame?: string;\n /** Final frame image URL */\n urlFinalFrame?: string;\n /** Podcast audio URL */\n urlPodcast?: string;\n /** Video URL */\n urlVideo?: string;\n}\n\n/**\n * Props for DownloadButton component\n */\nexport interface DownloadButtonProps {\n /** Content URLs to download */\n content: DownloadContent;\n /** Additional CSS classes */\n className?: string;\n /** Callback fired when download starts */\n onDownloadStart?: (contentType: string) => void;\n /** Callback fired when download completes */\n onDownloadComplete?: (contentType: string) => void;\n /** Callback fired when download fails */\n onDownloadError?: (contentType: string, error: Error) => void;\n /** Lesson title for download file naming */\n lessonTitle?: string;\n /** Whether the button is disabled */\n disabled?: boolean;\n}\n\n/**\n * Get MIME type based on file extension\n * @param url - URL to extract extension from\n * @returns MIME type string\n */\nconst getMimeType = (url: string): string => {\n const extension = getFileExtension(url);\n const mimeTypes: Record<string, string> = {\n pdf: 'application/pdf',\n png: 'image/png',\n jpg: 'image/jpeg',\n jpeg: 'image/jpeg',\n mp3: 'audio/mpeg',\n mp4: 'video/mp4',\n vtt: 'text/vtt',\n };\n return mimeTypes[extension] || 'application/octet-stream';\n};\n\n/**\n * Download file via fetch and blob to ensure proper download behavior\n * @param url - URL to download\n * @param filename - Filename for the download\n * @returns Promise<void>\n */\nconst triggerDownload = async (\n url: string,\n filename: string\n): Promise<void> => {\n try {\n // Fetch the file as blob\n const response = await fetch(url, {\n mode: 'cors',\n credentials: 'same-origin',\n });\n\n if (!response.ok) {\n throw new Error(\n `Failed to fetch file: ${response.status} ${response.statusText}`\n );\n }\n\n const blob = await response.blob();\n const mimeType = getMimeType(url);\n\n // Create a blob with the correct MIME type\n const typedBlob = new Blob([blob], { type: mimeType });\n\n // Create object URL\n const blobUrl = URL.createObjectURL(typedBlob);\n\n // Create download link\n const link = document.createElement('a');\n link.href = blobUrl;\n link.download = filename;\n link.rel = 'noopener noreferrer';\n\n // Add to DOM, click, and remove\n document.body.appendChild(link);\n link.click();\n link.remove();\n\n // Clean up object URL after a short delay\n setTimeout(() => {\n URL.revokeObjectURL(blobUrl);\n }, 1000);\n } catch (error) {\n // Fallback to direct link if fetch fails\n console.warn('Fetch download failed, falling back to direct link:', error);\n\n const link = document.createElement('a');\n link.href = url;\n link.download = filename;\n link.rel = 'noopener noreferrer';\n link.target = '_blank'; // Open in new tab as fallback\n\n document.body.appendChild(link);\n link.click();\n link.remove();\n }\n};\n\n/**\n * Get file extension from URL\n * @param url - URL to extract extension from\n * @returns File extension or default\n */\nconst getFileExtension = (url: string): string => {\n try {\n const u = new URL(url, globalThis.location?.origin || 'http://localhost');\n url = u.pathname;\n } catch {\n // keep original url (likely relative)\n }\n const path = url.split(/[?#]/)[0];\n const dot = path.lastIndexOf('.');\n return dot > -1 ? path.slice(dot + 1).toLowerCase() : 'file';\n};\n\n/**\n * Generate filename for download\n * @param contentType - Type of content being downloaded\n * @param lessonTitle - Title of the lesson\n * @param url - URL to get extension from\n * @returns Generated filename\n */\nconst generateFilename = (\n contentType: string,\n url: string,\n lessonTitle: string = 'aula'\n): string => {\n const sanitizedTitle = lessonTitle\n .toLowerCase()\n .replaceAll(/[^a-z0-9\\s]/g, '')\n .replaceAll(/\\s+/g, '-')\n .substring(0, 50);\n\n const extension = getFileExtension(url);\n return `${sanitizedTitle}-${contentType}.${extension}`;\n};\n\n/**\n * DownloadButton component for downloading lesson content\n * Provides a single button that downloads all available content for a lesson\n *\n * @param props - DownloadButton component props\n * @returns Download button element\n */\nconst DownloadButton = ({\n content,\n className,\n onDownloadStart,\n onDownloadComplete,\n onDownloadError,\n lessonTitle = 'aula',\n disabled = false,\n}: DownloadButtonProps) => {\n const [isDownloading, setIsDownloading] = useState(false);\n\n /**\n * Check if URL is valid and not empty\n * @param url - URL to validate\n * @returns Whether URL is valid\n */\n const isValidUrl = useCallback((url?: string): boolean => {\n return Boolean(\n url && url.trim() !== '' && url !== 'undefined' && url !== 'null'\n );\n }, []);\n\n /**\n * Get available download content\n * @returns Array of available download items\n */\n const getAvailableContent = useCallback(() => {\n const downloads: Array<{ type: string; url: string; label: string }> = [];\n\n if (isValidUrl(content.urlDoc)) {\n downloads.push({\n type: 'documento',\n url: content.urlDoc!,\n label: 'Documento',\n });\n }\n\n if (isValidUrl(content.urlInitialFrame)) {\n downloads.push({\n type: 'quadro-inicial',\n url: content.urlInitialFrame!,\n label: 'Quadro Inicial',\n });\n }\n\n if (isValidUrl(content.urlFinalFrame)) {\n downloads.push({\n type: 'quadro-final',\n url: content.urlFinalFrame!,\n label: 'Quadro Final',\n });\n }\n\n if (isValidUrl(content.urlPodcast)) {\n downloads.push({\n type: 'podcast',\n url: content.urlPodcast!,\n label: 'Podcast',\n });\n }\n\n if (isValidUrl(content.urlVideo)) {\n downloads.push({ type: 'video', url: content.urlVideo!, label: 'Vídeo' });\n }\n\n return downloads;\n }, [content, isValidUrl]);\n\n /**\n * Handle download of all available content\n */\n const handleDownload = useCallback(async () => {\n if (disabled || isDownloading) return;\n\n const availableContent = getAvailableContent();\n\n if (availableContent.length === 0) {\n return;\n }\n\n setIsDownloading(true);\n\n try {\n // Download each available content sequentially with small delay\n for (let i = 0; i < availableContent.length; i++) {\n const item = availableContent[i];\n\n try {\n onDownloadStart?.(item.type);\n\n const filename = generateFilename(item.type, item.url, lessonTitle);\n await triggerDownload(item.url, filename);\n\n onDownloadComplete?.(item.type);\n\n // Add small delay between downloads to prevent browser blocking\n if (i < availableContent.length - 1) {\n await new Promise((resolve) => setTimeout(resolve, 200));\n }\n } catch (error) {\n console.error(`Erro ao baixar ${item.label}:`, error);\n onDownloadError?.(\n item.type,\n error instanceof Error\n ? error\n : new Error(`Falha ao baixar ${item.label}`)\n );\n }\n }\n } finally {\n setIsDownloading(false);\n }\n }, [\n disabled,\n isDownloading,\n getAvailableContent,\n lessonTitle,\n onDownloadStart,\n onDownloadComplete,\n onDownloadError,\n ]);\n\n // Don't render if no content is available\n const hasContent = getAvailableContent().length > 0;\n\n if (!hasContent) {\n return null;\n }\n\n return (\n <div className={cn('flex items-center', className)}>\n <IconButton\n icon={<DownloadSimple size={24} />}\n onClick={handleDownload}\n disabled={disabled || isDownloading}\n aria-label={(() => {\n if (isDownloading) {\n return 'Baixando conteúdo...';\n }\n const contentCount = getAvailableContent().length;\n const suffix = contentCount > 1 ? 's' : '';\n return `Baixar conteúdo da aula (${contentCount} arquivo${suffix})`;\n })()}\n className={cn(\n '!bg-transparent hover:!bg-black/10 transition-colors',\n isDownloading && 'opacity-60 cursor-not-allowed'\n )}\n />\n </div>\n );\n};\n\nexport default DownloadButton;\n","import { ButtonHTMLAttributes, ReactNode, forwardRef } from 'react';\nimport { cn } from '../../utils/utils';\n\n/**\n * IconButton component props interface\n */\nexport type IconButtonProps = {\n /** Ícone a ser exibido no botão */\n icon: ReactNode;\n /** Tamanho do botão */\n size?: 'sm' | 'md';\n /** Estado de seleção/ativo do botão - permanece ativo até ser clicado novamente ou outro botão ser ativado */\n active?: boolean;\n /** Additional CSS classes to apply */\n className?: string;\n} & ButtonHTMLAttributes<HTMLButtonElement>;\n\n/**\n * IconButton component for Analytica Ensino platforms\n *\n * Um botão compacto apenas com ícone, ideal para menus dropdown,\n * barras de ferramentas e ações secundárias.\n * Oferece dois tamanhos com estilo consistente.\n * Estado ativo permanece até ser clicado novamente ou outro botão ser ativado.\n * Suporta forwardRef para acesso programático ao elemento DOM.\n *\n * @param icon - O ícone a ser exibido no botão\n * @param size - Tamanho do botão (sm, md)\n * @param active - Estado ativo/selecionado 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 compacto estilizado apenas com ícone\n *\n * @example\n * ```tsx\n * <IconButton\n * icon={<MoreVerticalIcon />}\n * size=\"sm\"\n * onClick={() => openMenu()}\n * />\n * ```\n *\n * @example\n * ```tsx\n * // Botão ativo em uma barra de ferramentas - permanece ativo até outro clique\n * <IconButton\n * icon={<BoldIcon />}\n * active={isBold}\n * onClick={toggleBold}\n * />\n * ```\n *\n * @example\n * ```tsx\n * // Usando ref para controle programático\n * const buttonRef = useRef<HTMLButtonElement>(null);\n *\n * <IconButton\n * ref={buttonRef}\n * icon={<EditIcon />}\n * size=\"md\"\n * onClick={() => startEditing()}\n * />\n * ```\n */\nconst IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(\n (\n { icon, size = 'md', active = 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-center',\n 'rounded-lg',\n 'font-medium',\n 'bg-transparent',\n 'text-text-950',\n 'cursor-pointer',\n 'hover:bg-primary-600',\n 'hover:text-text',\n 'focus-visible:outline-none',\n 'focus-visible:ring-2',\n 'focus-visible:ring-offset-0',\n 'focus-visible:ring-indicator-info',\n 'disabled:opacity-50',\n 'disabled:cursor-not-allowed',\n 'disabled:pointer-events-none',\n ];\n\n // Classes de tamanho\n const sizeClasses = {\n sm: ['w-6', 'h-6', 'text-sm'],\n md: ['w-10', 'h-10', 'text-base'],\n };\n\n // Classes de estado ativo\n const activeClasses = active\n ? ['!bg-primary-50', '!text-primary-950', 'hover:!bg-primary-100']\n : [];\n\n const allClasses = [\n ...baseClasses,\n ...sizeClasses[size],\n ...activeClasses,\n ].join(' ');\n\n // Garantir acessibilidade com aria-label padrão\n const ariaLabel = props['aria-label'] ?? 'Botão de ação';\n\n return (\n <button\n ref={ref}\n type=\"button\"\n className={cn(allClasses, className)}\n disabled={disabled}\n aria-pressed={active}\n aria-label={ariaLabel}\n {...props}\n >\n <span className=\"flex items-center justify-center\">{icon}</span>\n </button>\n );\n }\n);\n\nIconButton.displayName = 'IconButton';\n\nexport default IconButton;\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';\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"],"mappings":";AAAA,SAAS,aAAa,gBAAgB;AACtC,SAAS,sBAAsB;;;ACD/B,SAA0C,kBAAkB;;;ACA5D,SAAS,YAA6B;AACtC,SAAS,eAAe;AAEjB,SAAS,MAAM,QAAsB;AAC1C,SAAO,QAAQ,KAAK,MAAM,CAAC;AAC7B;;;ADoHQ;AAxDR,IAAM,aAAa;AAAA,EACjB,CACE,EAAE,MAAM,OAAO,MAAM,SAAS,OAAO,YAAY,IAAI,UAAU,GAAG,MAAM,GACxE,QACG;AAEH,UAAM,cAAc;AAAA,MAClB;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAGA,UAAM,cAAc;AAAA,MAClB,IAAI,CAAC,OAAO,OAAO,SAAS;AAAA,MAC5B,IAAI,CAAC,QAAQ,QAAQ,WAAW;AAAA,IAClC;AAGA,UAAM,gBAAgB,SAClB,CAAC,kBAAkB,qBAAqB,uBAAuB,IAC/D,CAAC;AAEL,UAAM,aAAa;AAAA,MACjB,GAAG;AAAA,MACH,GAAG,YAAY,IAAI;AAAA,MACnB,GAAG;AAAA,IACL,EAAE,KAAK,GAAG;AAGV,UAAM,YAAY,MAAM,YAAY,KAAK;AAEzC,WACE;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA,MAAK;AAAA,QACL,WAAW,GAAG,YAAY,SAAS;AAAA,QACnC;AAAA,QACA,gBAAc;AAAA,QACd,cAAY;AAAA,QACX,GAAG;AAAA,QAEJ,8BAAC,UAAK,WAAU,oCAAoC,gBAAK;AAAA;AAAA,IAC3D;AAAA,EAEJ;AACF;AAEA,WAAW,cAAc;AAEzB,IAAO,qBAAQ;;;AD4KD,gBAAAA,YAAA;AA/Pd,IAAM,cAAc,CAAC,QAAwB;AAC3C,QAAM,YAAY,iBAAiB,GAAG;AACtC,QAAM,YAAoC;AAAA,IACxC,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,MAAM;AAAA,IACN,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,EACP;AACA,SAAO,UAAU,SAAS,KAAK;AACjC;AAQA,IAAM,kBAAkB,OACtB,KACA,aACkB;AAClB,MAAI;AAEF,UAAM,WAAW,MAAM,MAAM,KAAK;AAAA,MAChC,MAAM;AAAA,MACN,aAAa;AAAA,IACf,CAAC;AAED,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,IAAI;AAAA,QACR,yBAAyB,SAAS,MAAM,IAAI,SAAS,UAAU;AAAA,MACjE;AAAA,IACF;AAEA,UAAM,OAAO,MAAM,SAAS,KAAK;AACjC,UAAM,WAAW,YAAY,GAAG;AAGhC,UAAM,YAAY,IAAI,KAAK,CAAC,IAAI,GAAG,EAAE,MAAM,SAAS,CAAC;AAGrD,UAAM,UAAU,IAAI,gBAAgB,SAAS;AAG7C,UAAM,OAAO,SAAS,cAAc,GAAG;AACvC,SAAK,OAAO;AACZ,SAAK,WAAW;AAChB,SAAK,MAAM;AAGX,aAAS,KAAK,YAAY,IAAI;AAC9B,SAAK,MAAM;AACX,SAAK,OAAO;AAGZ,eAAW,MAAM;AACf,UAAI,gBAAgB,OAAO;AAAA,IAC7B,GAAG,GAAI;AAAA,EACT,SAAS,OAAO;AAEd,YAAQ,KAAK,uDAAuD,KAAK;AAEzE,UAAM,OAAO,SAAS,cAAc,GAAG;AACvC,SAAK,OAAO;AACZ,SAAK,WAAW;AAChB,SAAK,MAAM;AACX,SAAK,SAAS;AAEd,aAAS,KAAK,YAAY,IAAI;AAC9B,SAAK,MAAM;AACX,SAAK,OAAO;AAAA,EACd;AACF;AAOA,IAAM,mBAAmB,CAAC,QAAwB;AAChD,MAAI;AACF,UAAM,IAAI,IAAI,IAAI,KAAK,WAAW,UAAU,UAAU,kBAAkB;AACxE,UAAM,EAAE;AAAA,EACV,QAAQ;AAAA,EAER;AACA,QAAM,OAAO,IAAI,MAAM,MAAM,EAAE,CAAC;AAChC,QAAM,MAAM,KAAK,YAAY,GAAG;AAChC,SAAO,MAAM,KAAK,KAAK,MAAM,MAAM,CAAC,EAAE,YAAY,IAAI;AACxD;AASA,IAAM,mBAAmB,CACvB,aACA,KACA,cAAsB,WACX;AACX,QAAM,iBAAiB,YACpB,YAAY,EACZ,WAAW,gBAAgB,EAAE,EAC7B,WAAW,QAAQ,GAAG,EACtB,UAAU,GAAG,EAAE;AAElB,QAAM,YAAY,iBAAiB,GAAG;AACtC,SAAO,GAAG,cAAc,IAAI,WAAW,IAAI,SAAS;AACtD;AASA,IAAM,iBAAiB,CAAC;AAAA,EACtB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd,WAAW;AACb,MAA2B;AACzB,QAAM,CAAC,eAAe,gBAAgB,IAAI,SAAS,KAAK;AAOxD,QAAM,aAAa,YAAY,CAAC,QAA0B;AACxD,WAAO;AAAA,MACL,OAAO,IAAI,KAAK,MAAM,MAAM,QAAQ,eAAe,QAAQ;AAAA,IAC7D;AAAA,EACF,GAAG,CAAC,CAAC;AAML,QAAM,sBAAsB,YAAY,MAAM;AAC5C,UAAM,YAAiE,CAAC;AAExE,QAAI,WAAW,QAAQ,MAAM,GAAG;AAC9B,gBAAU,KAAK;AAAA,QACb,MAAM;AAAA,QACN,KAAK,QAAQ;AAAA,QACb,OAAO;AAAA,MACT,CAAC;AAAA,IACH;AAEA,QAAI,WAAW,QAAQ,eAAe,GAAG;AACvC,gBAAU,KAAK;AAAA,QACb,MAAM;AAAA,QACN,KAAK,QAAQ;AAAA,QACb,OAAO;AAAA,MACT,CAAC;AAAA,IACH;AAEA,QAAI,WAAW,QAAQ,aAAa,GAAG;AACrC,gBAAU,KAAK;AAAA,QACb,MAAM;AAAA,QACN,KAAK,QAAQ;AAAA,QACb,OAAO;AAAA,MACT,CAAC;AAAA,IACH;AAEA,QAAI,WAAW,QAAQ,UAAU,GAAG;AAClC,gBAAU,KAAK;AAAA,QACb,MAAM;AAAA,QACN,KAAK,QAAQ;AAAA,QACb,OAAO;AAAA,MACT,CAAC;AAAA,IACH;AAEA,QAAI,WAAW,QAAQ,QAAQ,GAAG;AAChC,gBAAU,KAAK,EAAE,MAAM,SAAS,KAAK,QAAQ,UAAW,OAAO,WAAQ,CAAC;AAAA,IAC1E;AAEA,WAAO;AAAA,EACT,GAAG,CAAC,SAAS,UAAU,CAAC;AAKxB,QAAM,iBAAiB,YAAY,YAAY;AAC7C,QAAI,YAAY,cAAe;AAE/B,UAAM,mBAAmB,oBAAoB;AAE7C,QAAI,iBAAiB,WAAW,GAAG;AACjC;AAAA,IACF;AAEA,qBAAiB,IAAI;AAErB,QAAI;AAEF,eAAS,IAAI,GAAG,IAAI,iBAAiB,QAAQ,KAAK;AAChD,cAAM,OAAO,iBAAiB,CAAC;AAE/B,YAAI;AACF,4BAAkB,KAAK,IAAI;AAE3B,gBAAM,WAAW,iBAAiB,KAAK,MAAM,KAAK,KAAK,WAAW;AAClE,gBAAM,gBAAgB,KAAK,KAAK,QAAQ;AAExC,+BAAqB,KAAK,IAAI;AAG9B,cAAI,IAAI,iBAAiB,SAAS,GAAG;AACnC,kBAAM,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,GAAG,CAAC;AAAA,UACzD;AAAA,QACF,SAAS,OAAO;AACd,kBAAQ,MAAM,kBAAkB,KAAK,KAAK,KAAK,KAAK;AACpD;AAAA,YACE,KAAK;AAAA,YACL,iBAAiB,QACb,QACA,IAAI,MAAM,mBAAmB,KAAK,KAAK,EAAE;AAAA,UAC/C;AAAA,QACF;AAAA,MACF;AAAA,IACF,UAAE;AACA,uBAAiB,KAAK;AAAA,IACxB;AAAA,EACF,GAAG;AAAA,IACD;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC;AAGD,QAAM,aAAa,oBAAoB,EAAE,SAAS;AAElD,MAAI,CAAC,YAAY;AACf,WAAO;AAAA,EACT;AAEA,SACE,gBAAAA,KAAC,SAAI,WAAW,GAAG,qBAAqB,SAAS,GAC/C,0BAAAA;AAAA,IAAC;AAAA;AAAA,MACC,MAAM,gBAAAA,KAAC,kBAAe,MAAM,IAAI;AAAA,MAChC,SAAS;AAAA,MACT,UAAU,YAAY;AAAA,MACtB,eAAa,MAAM;AACjB,YAAI,eAAe;AACjB,iBAAO;AAAA,QACT;AACA,cAAM,eAAe,oBAAoB,EAAE;AAC3C,cAAM,SAAS,eAAe,IAAI,MAAM;AACxC,eAAO,+BAA4B,YAAY,WAAW,MAAM;AAAA,MAClE,GAAG;AAAA,MACH,WAAW;AAAA,QACT;AAAA,QACA,iBAAiB;AAAA,MACnB;AAAA;AAAA,EACF,GACF;AAEJ;AAEA,IAAO,yBAAQ;","names":["jsx"]}
1
+ {"version":3,"sources":["../../src/components/DownloadButton/DownloadButton.tsx","../../src/components/IconButton/IconButton.tsx","../../src/utils/utils.ts"],"sourcesContent":["import { useCallback, useState } from 'react';\nimport { DownloadSimple } from 'phosphor-react';\nimport IconButton from '../IconButton/IconButton';\nimport { cn } from '../../utils/utils';\n\n/**\n * Download content interface for lesson materials\n */\nexport interface DownloadContent {\n /** Document URL (PDF) */\n urlDoc?: string;\n /** Initial frame image URL */\n urlInitialFrame?: string;\n /** Final frame image URL */\n urlFinalFrame?: string;\n /** Podcast audio URL */\n urlPodcast?: string;\n /** Video URL */\n urlVideo?: string;\n}\n\n/**\n * Props for DownloadButton component\n */\nexport interface DownloadButtonProps {\n /** Content URLs to download */\n content: DownloadContent;\n /** Additional CSS classes */\n className?: string;\n /** Callback fired when download starts */\n onDownloadStart?: (contentType: string) => void;\n /** Callback fired when download completes */\n onDownloadComplete?: (contentType: string) => void;\n /** Callback fired when download fails */\n onDownloadError?: (contentType: string, error: Error) => void;\n /** Lesson title for download file naming */\n lessonTitle?: string;\n /** Whether the button is disabled */\n disabled?: boolean;\n}\n\n/**\n * Get MIME type based on file extension\n * @param url - URL to extract extension from\n * @returns MIME type string\n */\nconst getMimeType = (url: string): string => {\n const extension = getFileExtension(url);\n const mimeTypes: Record<string, string> = {\n pdf: 'application/pdf',\n png: 'image/png',\n jpg: 'image/jpeg',\n jpeg: 'image/jpeg',\n mp3: 'audio/mpeg',\n mp4: 'video/mp4',\n vtt: 'text/vtt',\n };\n return mimeTypes[extension] || 'application/octet-stream';\n};\n\n/**\n * Download file via fetch and blob to ensure proper download behavior\n * @param url - URL to download\n * @param filename - Filename for the download\n * @returns Promise<void>\n */\nconst triggerDownload = async (\n url: string,\n filename: string\n): Promise<void> => {\n try {\n // Fetch the file as blob\n const response = await fetch(url, {\n mode: 'cors',\n credentials: 'same-origin',\n });\n\n if (!response.ok) {\n throw new Error(\n `Failed to fetch file: ${response.status} ${response.statusText}`\n );\n }\n\n const blob = await response.blob();\n const mimeType = getMimeType(url);\n\n // Create a blob with the correct MIME type\n const typedBlob = new Blob([blob], { type: mimeType });\n\n // Create object URL\n const blobUrl = URL.createObjectURL(typedBlob);\n\n // Create download link\n const link = document.createElement('a');\n link.href = blobUrl;\n link.download = filename;\n link.rel = 'noopener noreferrer';\n\n // Add to DOM, click, and remove\n document.body.appendChild(link);\n link.click();\n link.remove();\n\n // Clean up object URL after a short delay\n setTimeout(() => {\n URL.revokeObjectURL(blobUrl);\n }, 1000);\n } catch (error) {\n // Fallback to direct link if fetch fails\n console.warn('Fetch download failed, falling back to direct link:', error);\n\n const link = document.createElement('a');\n link.href = url;\n link.download = filename;\n link.rel = 'noopener noreferrer';\n link.target = '_blank'; // Open in new tab as fallback\n\n document.body.appendChild(link);\n link.click();\n link.remove();\n }\n};\n\n/**\n * Get file extension from URL\n * @param url - URL to extract extension from\n * @returns File extension or default\n */\nconst getFileExtension = (url: string): string => {\n try {\n const u = new URL(url, globalThis.location?.origin || 'http://localhost');\n url = u.pathname;\n } catch {\n // keep original url (likely relative)\n }\n const path = url.split(/[?#]/)[0];\n const dot = path.lastIndexOf('.');\n return dot > -1 ? path.slice(dot + 1).toLowerCase() : 'file';\n};\n\n/**\n * Generate filename for download\n * @param contentType - Type of content being downloaded\n * @param lessonTitle - Title of the lesson\n * @param url - URL to get extension from\n * @returns Generated filename\n */\nconst generateFilename = (\n contentType: string,\n url: string,\n lessonTitle: string = 'aula'\n): string => {\n const sanitizedTitle = lessonTitle\n .toLowerCase()\n .replaceAll(/[^a-z0-9\\s]/g, '')\n .replaceAll(/\\s+/g, '-')\n .substring(0, 50);\n\n const extension = getFileExtension(url);\n return `${sanitizedTitle}-${contentType}.${extension}`;\n};\n\n/**\n * DownloadButton component for downloading lesson content\n * Provides a single button that downloads all available content for a lesson\n *\n * @param props - DownloadButton component props\n * @returns Download button element\n */\nconst DownloadButton = ({\n content,\n className,\n onDownloadStart,\n onDownloadComplete,\n onDownloadError,\n lessonTitle = 'aula',\n disabled = false,\n}: DownloadButtonProps) => {\n const [isDownloading, setIsDownloading] = useState(false);\n\n /**\n * Check if URL is valid and not empty\n * @param url - URL to validate\n * @returns Whether URL is valid\n */\n const isValidUrl = useCallback((url?: string): boolean => {\n return Boolean(\n url && url.trim() !== '' && url !== 'undefined' && url !== 'null'\n );\n }, []);\n\n /**\n * Get available download content\n * @returns Array of available download items\n */\n const getAvailableContent = useCallback(() => {\n const downloads: Array<{ type: string; url: string; label: string }> = [];\n\n if (isValidUrl(content.urlDoc)) {\n downloads.push({\n type: 'documento',\n url: content.urlDoc!,\n label: 'Documento',\n });\n }\n\n if (isValidUrl(content.urlInitialFrame)) {\n downloads.push({\n type: 'quadro-inicial',\n url: content.urlInitialFrame!,\n label: 'Quadro Inicial',\n });\n }\n\n if (isValidUrl(content.urlFinalFrame)) {\n downloads.push({\n type: 'quadro-final',\n url: content.urlFinalFrame!,\n label: 'Quadro Final',\n });\n }\n\n if (isValidUrl(content.urlPodcast)) {\n downloads.push({\n type: 'podcast',\n url: content.urlPodcast!,\n label: 'Podcast',\n });\n }\n\n if (isValidUrl(content.urlVideo)) {\n downloads.push({ type: 'video', url: content.urlVideo!, label: 'Vídeo' });\n }\n\n return downloads;\n }, [content, isValidUrl]);\n\n /**\n * Handle download of all available content\n */\n const handleDownload = useCallback(async () => {\n if (disabled || isDownloading) return;\n\n const availableContent = getAvailableContent();\n\n if (availableContent.length === 0) {\n return;\n }\n\n setIsDownloading(true);\n\n try {\n // Download each available content sequentially with small delay\n for (let i = 0; i < availableContent.length; i++) {\n const item = availableContent[i];\n\n try {\n onDownloadStart?.(item.type);\n\n const filename = generateFilename(item.type, item.url, lessonTitle);\n await triggerDownload(item.url, filename);\n\n onDownloadComplete?.(item.type);\n\n // Add small delay between downloads to prevent browser blocking\n if (i < availableContent.length - 1) {\n await new Promise((resolve) => setTimeout(resolve, 200));\n }\n } catch (error) {\n // Silent error handling - delegate to callback\n onDownloadError?.(\n item.type,\n error instanceof Error\n ? error\n : new Error(`Falha ao baixar ${item.label}`)\n );\n }\n }\n } finally {\n setIsDownloading(false);\n }\n }, [\n disabled,\n isDownloading,\n getAvailableContent,\n lessonTitle,\n onDownloadStart,\n onDownloadComplete,\n onDownloadError,\n ]);\n\n // Don't render if no content is available\n const hasContent = getAvailableContent().length > 0;\n\n if (!hasContent) {\n return null;\n }\n\n return (\n <div className={cn('flex items-center', className)}>\n <IconButton\n icon={<DownloadSimple size={24} />}\n onClick={handleDownload}\n disabled={disabled || isDownloading}\n aria-label={(() => {\n if (isDownloading) {\n return 'Baixando conteúdo...';\n }\n const contentCount = getAvailableContent().length;\n const suffix = contentCount > 1 ? 's' : '';\n return `Baixar conteúdo da aula (${contentCount} arquivo${suffix})`;\n })()}\n className={cn(\n '!bg-transparent hover:!bg-black/10 transition-colors',\n isDownloading && 'opacity-60 cursor-not-allowed'\n )}\n />\n </div>\n );\n};\n\nexport default DownloadButton;\n","import { ButtonHTMLAttributes, ReactNode, forwardRef } from 'react';\nimport { cn } from '../../utils/utils';\n\n/**\n * IconButton component props interface\n */\nexport type IconButtonProps = {\n /** Ícone a ser exibido no botão */\n icon: ReactNode;\n /** Tamanho do botão */\n size?: 'sm' | 'md';\n /** Estado de seleção/ativo do botão - permanece ativo até ser clicado novamente ou outro botão ser ativado */\n active?: boolean;\n /** Additional CSS classes to apply */\n className?: string;\n} & ButtonHTMLAttributes<HTMLButtonElement>;\n\n/**\n * IconButton component for Analytica Ensino platforms\n *\n * Um botão compacto apenas com ícone, ideal para menus dropdown,\n * barras de ferramentas e ações secundárias.\n * Oferece dois tamanhos com estilo consistente.\n * Estado ativo permanece até ser clicado novamente ou outro botão ser ativado.\n * Suporta forwardRef para acesso programático ao elemento DOM.\n *\n * @param icon - O ícone a ser exibido no botão\n * @param size - Tamanho do botão (sm, md)\n * @param active - Estado ativo/selecionado 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 compacto estilizado apenas com ícone\n *\n * @example\n * ```tsx\n * <IconButton\n * icon={<MoreVerticalIcon />}\n * size=\"sm\"\n * onClick={() => openMenu()}\n * />\n * ```\n *\n * @example\n * ```tsx\n * // Botão ativo em uma barra de ferramentas - permanece ativo até outro clique\n * <IconButton\n * icon={<BoldIcon />}\n * active={isBold}\n * onClick={toggleBold}\n * />\n * ```\n *\n * @example\n * ```tsx\n * // Usando ref para controle programático\n * const buttonRef = useRef<HTMLButtonElement>(null);\n *\n * <IconButton\n * ref={buttonRef}\n * icon={<EditIcon />}\n * size=\"md\"\n * onClick={() => startEditing()}\n * />\n * ```\n */\nconst IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(\n (\n { icon, size = 'md', active = 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-center',\n 'rounded-lg',\n 'font-medium',\n 'bg-transparent',\n 'text-text-950',\n 'cursor-pointer',\n 'hover:bg-primary-600',\n 'hover:text-text',\n 'focus-visible:outline-none',\n 'focus-visible:ring-2',\n 'focus-visible:ring-offset-0',\n 'focus-visible:ring-indicator-info',\n 'disabled:opacity-50',\n 'disabled:cursor-not-allowed',\n 'disabled:pointer-events-none',\n ];\n\n // Classes de tamanho\n const sizeClasses = {\n sm: ['w-6', 'h-6', 'text-sm'],\n md: ['w-10', 'h-10', 'text-base'],\n };\n\n // Classes de estado ativo\n const activeClasses = active\n ? ['!bg-primary-50', '!text-primary-950', 'hover:!bg-primary-100']\n : [];\n\n const allClasses = [\n ...baseClasses,\n ...sizeClasses[size],\n ...activeClasses,\n ].join(' ');\n\n // Garantir acessibilidade com aria-label padrão\n const ariaLabel = props['aria-label'] ?? 'Botão de ação';\n\n return (\n <button\n ref={ref}\n type=\"button\"\n className={cn(allClasses, className)}\n disabled={disabled}\n aria-pressed={active}\n aria-label={ariaLabel}\n {...props}\n >\n <span className=\"flex items-center justify-center\">{icon}</span>\n </button>\n );\n }\n);\n\nIconButton.displayName = 'IconButton';\n\nexport default IconButton;\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';\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"],"mappings":"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","names":["jsx"]}
@@ -1067,7 +1067,6 @@ var useQuizStore = (0, import_zustand2.create)()(
1067
1067
  const activityId = quiz.id;
1068
1068
  const userId = get().getUserId();
1069
1069
  if (!userId || userId === "") {
1070
- console.warn("selectAnswer called before userId is set");
1071
1070
  return;
1072
1071
  }
1073
1072
  const question = quiz.questions.find((q) => q.id === questionId);
@@ -1101,7 +1100,6 @@ var useQuizStore = (0, import_zustand2.create)()(
1101
1100
  const activityId = quiz.id;
1102
1101
  const userId = get().getUserId();
1103
1102
  if (!userId || userId === "") {
1104
- console.warn("selectMultipleAnswer called before userId is set");
1105
1103
  return;
1106
1104
  }
1107
1105
  const question = quiz.questions.find((q) => q.id === questionId);
@@ -1136,16 +1134,10 @@ var useQuizStore = (0, import_zustand2.create)()(
1136
1134
  const activityId = quiz.id;
1137
1135
  const userId = get().getUserId();
1138
1136
  if (!userId || userId === "") {
1139
- console.warn(
1140
- "selectDissertativeAnswer called before userId is set"
1141
- );
1142
1137
  return;
1143
1138
  }
1144
1139
  const question = quiz.questions.find((q) => q.id === questionId);
1145
1140
  if (!question || question.questionType !== "DISSERTATIVA" /* DISSERTATIVA */) {
1146
- console.warn(
1147
- "selectDissertativeAnswer called for non-dissertative question"
1148
- );
1149
1141
  return;
1150
1142
  }
1151
1143
  const existingAnswerIndex = userAnswers.findIndex(
@@ -1179,7 +1171,6 @@ var useQuizStore = (0, import_zustand2.create)()(
1179
1171
  const activityId = quiz.id;
1180
1172
  const userId = get().getUserId();
1181
1173
  if (!userId || userId === "") {
1182
- console.warn("skipQuestion called before userId is set");
1183
1174
  return;
1184
1175
  }
1185
1176
  const existingAnswerIndex = userAnswers.findIndex(
@@ -1212,7 +1203,6 @@ var useQuizStore = (0, import_zustand2.create)()(
1212
1203
  const activityId = quiz.id;
1213
1204
  const userId = get().getUserId();
1214
1205
  if (!userId || userId === "") {
1215
- console.warn("addUserAnswer called before userId is set");
1216
1206
  return;
1217
1207
  }
1218
1208
  const question = quiz.questions.find((q) => q.id === questionId);
@@ -1440,9 +1430,6 @@ var useQuizStore = (0, import_zustand2.create)()(
1440
1430
  );
1441
1431
  }
1442
1432
  if (questionIndex === -1) {
1443
- console.warn(
1444
- `Question with id "${question.id}" not found in active quiz`
1445
- );
1446
1433
  return;
1447
1434
  }
1448
1435
  set({ currentQuestionIndex: questionIndex });
@@ -5901,34 +5888,16 @@ var QuizFooter = (0, import_react14.forwardRef)(
5901
5888
  children: "Avan\xE7ar"
5902
5889
  }
5903
5890
  )
5904
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "flex flex-row items-center justify-between w-full", children: [
5905
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { children: quiz?.canRetry && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
5906
- Button_default,
5907
- {
5908
- variant: "link",
5909
- action: "primary",
5910
- size: "medium",
5911
- onClick: () => openModal("modalResolution"),
5912
- children: "Ver Resolu\xE7\xE3o"
5913
- }
5914
- ) }),
5915
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
5916
- Button_default,
5917
- {
5918
- variant: "solid",
5919
- action: "primary",
5920
- size: "medium",
5921
- onClick: () => {
5922
- if (quiz?.canRetry) {
5923
- onRepeat?.();
5924
- } else {
5925
- openModal("modalResolution");
5926
- }
5927
- },
5928
- children: quiz?.canRetry ? `Repetir ${getTypeLabel(quiz.type)}` : "Ver Resolu\xE7\xE3o"
5929
- }
5930
- )
5931
- ] })
5891
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { className: "flex flex-row items-center justify-center w-full", children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
5892
+ Button_default,
5893
+ {
5894
+ variant: "link",
5895
+ action: "primary",
5896
+ size: "medium",
5897
+ onClick: () => openModal("modalResolution"),
5898
+ children: "Ver resolu\xE7\xE3o"
5899
+ }
5900
+ ) })
5932
5901
  }
5933
5902
  ),
5934
5903
  /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(