analytica-frontend-lib 1.1.49 → 1.1.51

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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/VideoPlayer/VideoPlayer.tsx","../../src/utils/utils.ts","../../src/components/IconButton/IconButton.tsx","../../src/components/Text/Text.tsx"],"sourcesContent":["import {\n useRef,\n useState,\n useEffect,\n useCallback,\n MouseEvent,\n KeyboardEvent,\n} from 'react';\nimport {\n Play,\n Pause,\n SpeakerHigh,\n SpeakerSlash,\n ArrowsOutSimple,\n ArrowsInSimple,\n ClosedCaptioning,\n DotsThreeVertical,\n} from 'phosphor-react';\nimport { cn } from '../../utils/utils';\nimport IconButton from '../IconButton/IconButton';\nimport Text from '../Text/Text';\n\n// Constants for timeout durations\nconst CONTROLS_HIDE_TIMEOUT = 3000; // 3 seconds for normal control hiding\nconst LEAVE_HIDE_TIMEOUT = 1000; // 1 second when mouse leaves the video area\nconst INIT_DELAY = 100; // ms delay to initialize controls on mount\n\n/**\n * VideoPlayer component props interface\n */\ninterface VideoPlayerProps {\n /** Video source URL */\n src: string;\n /** Video poster/thumbnail URL */\n poster?: string;\n /** Subtitles URL */\n subtitles?: string;\n /** Video title */\n title?: string;\n /** Video subtitle/description */\n subtitle?: string;\n /** Initial playback time in seconds */\n initialTime?: number;\n /** Callback fired when video time updates (seconds) */\n onTimeUpdate?: (seconds: number) => void;\n /** Callback fired with progress percentage (0-100) */\n onProgress?: (progress: number) => void;\n /** Callback fired when video completes (>95% watched) */\n onVideoComplete?: () => void;\n /** Additional CSS classes */\n className?: string;\n /** Auto-save progress to localStorage */\n autoSave?: boolean;\n /** localStorage key for saving progress */\n storageKey?: string;\n}\n\n/**\n * Format seconds to MM:SS display format\n * @param seconds - Time in seconds\n * @returns Formatted time string\n */\nconst formatTime = (seconds: number): string => {\n if (!seconds || isNaN(seconds)) return '0:00';\n const mins = Math.floor(seconds / 60);\n const secs = Math.floor(seconds % 60);\n return `${mins}:${secs.toString().padStart(2, '0')}`;\n};\n\n/**\n * Progress bar component props\n */\ninterface ProgressBarProps {\n currentTime: number;\n duration: number;\n progressPercentage: number;\n onSeek: (time: number) => void;\n}\n\n/**\n * Progress bar subcomponent\n */\nconst ProgressBar = ({\n currentTime,\n duration,\n progressPercentage,\n onSeek,\n}: ProgressBarProps) => (\n <div className=\"px-4 pb-2\">\n <input\n type=\"range\"\n min={0}\n max={duration || 100}\n value={currentTime}\n onChange={(e) => onSeek(parseFloat(e.target.value))}\n className=\"w-full h-1 bg-neutral-600 rounded-full appearance-none cursor-pointer slider:bg-primary-600 focus:outline-none focus:ring-2 focus:ring-primary-500\"\n aria-label=\"Video progress\"\n style={{\n background: `linear-gradient(to right, var(--color-primary-700) ${progressPercentage}%, var(--color-secondary-300) ${progressPercentage}%)`,\n }}\n />\n </div>\n);\n\n/**\n * Volume controls component props\n */\ninterface VolumeControlsProps {\n volume: number;\n isMuted: boolean;\n onVolumeChange: (volume: number) => void;\n onToggleMute: () => void;\n}\n\n/**\n * Volume controls subcomponent\n */\nconst VolumeControls = ({\n volume,\n isMuted,\n onVolumeChange,\n onToggleMute,\n}: VolumeControlsProps) => (\n <div className=\"flex items-center gap-2\">\n <IconButton\n icon={isMuted ? <SpeakerSlash size={24} /> : <SpeakerHigh size={24} />}\n onClick={onToggleMute}\n aria-label={isMuted ? 'Unmute' : 'Mute'}\n className=\"!bg-transparent !text-white hover:!bg-white/20\"\n />\n\n <input\n type=\"range\"\n min={0}\n max={100}\n value={Math.round(volume * 100)}\n onChange={(e) => onVolumeChange(parseInt(e.target.value))}\n className=\"w-20 h-1 bg-neutral-600 rounded-full appearance-none cursor-pointer focus:outline-none focus:ring-2 focus:ring-primary-500\"\n aria-label=\"Volume control\"\n style={{\n background: `linear-gradient(to right, var(--color-primary-700) ${volume * 100}%, var(--color-secondary-300) ${volume * 100}%)`,\n }}\n />\n </div>\n);\n\n/**\n * Speed menu component props\n */\ninterface SpeedMenuProps {\n showSpeedMenu: boolean;\n playbackRate: number;\n onToggleMenu: () => void;\n onSpeedChange: (speed: number) => void;\n}\n\n/**\n * Speed menu subcomponent\n */\nconst SpeedMenu = ({\n showSpeedMenu,\n playbackRate,\n onToggleMenu,\n onSpeedChange,\n}: SpeedMenuProps) => (\n <div className=\"relative\">\n <IconButton\n icon={<DotsThreeVertical size={24} />}\n onClick={onToggleMenu}\n aria-label=\"Playback speed\"\n className=\"!bg-transparent !text-white hover:!bg-white/20\"\n />\n {showSpeedMenu && (\n <div className=\"absolute bottom-12 right-0 bg-black/90 rounded-lg p-2 min-w-20\">\n {[0.5, 0.75, 1, 1.25, 1.5, 2].map((speed) => (\n <button\n key={speed}\n onClick={() => onSpeedChange(speed)}\n className={`block w-full text-left px-3 py-1 text-sm rounded hover:bg-white/20 transition-colors ${\n playbackRate === speed ? 'text-primary-400' : 'text-white'\n }`}\n >\n {speed}x\n </button>\n ))}\n </div>\n )}\n </div>\n);\n\n/**\n * Video player component with controls and progress tracking\n * Integrates with backend lesson progress system\n *\n * @param props - VideoPlayer component props\n * @returns Video player element with controls\n */\nconst VideoPlayer = ({\n src,\n poster,\n subtitles,\n title,\n subtitle: subtitleText,\n initialTime = 0,\n onTimeUpdate,\n onProgress,\n onVideoComplete,\n className,\n autoSave = true,\n storageKey = 'video-progress',\n}: VideoPlayerProps) => {\n const videoRef = useRef<HTMLVideoElement>(null);\n const [isPlaying, setIsPlaying] = useState(false);\n const [currentTime, setCurrentTime] = useState(0);\n const [duration, setDuration] = useState(0);\n const [isMuted, setIsMuted] = useState(false);\n const [volume, setVolume] = useState(1);\n const [isFullscreen, setIsFullscreen] = useState(false);\n const [showControls, setShowControls] = useState(true);\n const [hasCompleted, setHasCompleted] = useState(false);\n const [showCaptions, setShowCaptions] = useState(false);\n\n // Reset completion flag when changing videos\n useEffect(() => {\n setHasCompleted(false);\n }, [src]);\n const [playbackRate, setPlaybackRate] = useState(1);\n const [showSpeedMenu, setShowSpeedMenu] = useState(false);\n const lastSaveTimeRef = useRef(0);\n const trackRef = useRef<HTMLTrackElement>(null);\n const controlsTimeoutRef = useRef<number | null>(null);\n const lastMousePositionRef = useRef({ x: 0, y: 0 });\n\n /**\n * Check if user is currently interacting with controls\n */\n const isUserInteracting = useCallback(() => {\n // Check if speed menu is open\n if (showSpeedMenu) {\n return true;\n }\n\n // Check if any control element has focus\n const activeElement = document.activeElement;\n const videoContainer = videoRef.current?.parentElement;\n\n if (activeElement && videoContainer?.contains(activeElement)) {\n // Ignore the video element itself - it should not prevent control hiding\n if (activeElement === videoRef.current) {\n return false;\n }\n\n // Check if focused element is a control (button, input, etc.)\n const isControl = activeElement.matches('button, input, [tabindex]');\n if (isControl) {\n return true;\n }\n }\n\n return false;\n }, [showSpeedMenu]);\n\n /**\n * Clear controls timeout\n */\n const clearControlsTimeout = useCallback(() => {\n if (controlsTimeoutRef.current) {\n clearTimeout(controlsTimeoutRef.current);\n controlsTimeoutRef.current = null;\n }\n }, []);\n\n /**\n * Show controls and set auto-hide timer\n */\n const showControlsWithTimer = useCallback(() => {\n setShowControls(true);\n clearControlsTimeout();\n\n // In fullscreen mode, only hide if video is playing\n if (isFullscreen) {\n if (isPlaying) {\n controlsTimeoutRef.current = window.setTimeout(() => {\n setShowControls(false);\n }, CONTROLS_HIDE_TIMEOUT);\n }\n } else {\n // In normal mode, always set a timer to hide controls\n controlsTimeoutRef.current = window.setTimeout(() => {\n setShowControls(false);\n }, CONTROLS_HIDE_TIMEOUT);\n }\n }, [isFullscreen, isPlaying, clearControlsTimeout]);\n\n /**\n * Handle mouse move with position detection\n */\n const handleMouseMove = useCallback(\n (event: MouseEvent) => {\n const currentX = event.clientX;\n const currentY = event.clientY;\n const lastPos = lastMousePositionRef.current;\n\n // Check if mouse actually moved (minimum 5px threshold)\n const hasMoved =\n Math.abs(currentX - lastPos.x) > 5 ||\n Math.abs(currentY - lastPos.y) > 5;\n\n if (hasMoved) {\n lastMousePositionRef.current = { x: currentX, y: currentY };\n showControlsWithTimer();\n }\n },\n [showControlsWithTimer]\n );\n\n /**\n * Handle mouse enter to show controls with appropriate timer logic\n */\n const handleMouseEnter = useCallback(() => {\n showControlsWithTimer();\n }, [showControlsWithTimer]);\n\n /**\n * Handle mouse leave to hide controls faster\n */\n const handleMouseLeave = useCallback(() => {\n const userInteracting = isUserInteracting();\n clearControlsTimeout();\n\n // Hide controls when mouse leaves, except when in fullscreen or user is interacting\n if (!isFullscreen && !userInteracting) {\n // Use shorter timeout when mouse leaves\n controlsTimeoutRef.current = window.setTimeout(() => {\n setShowControls(false);\n }, LEAVE_HIDE_TIMEOUT);\n }\n }, [isFullscreen, clearControlsTimeout, isUserInteracting]);\n\n /**\n * Initialize video element properties\n */\n useEffect(() => {\n // Set initial volume\n if (videoRef.current) {\n videoRef.current.volume = volume;\n videoRef.current.muted = isMuted;\n }\n }, [volume, isMuted]);\n\n /**\n * Synchronize isPlaying state with media events\n */\n useEffect(() => {\n const video = videoRef.current;\n if (!video) return;\n\n const onPlay = () => setIsPlaying(true);\n const onPause = () => setIsPlaying(false);\n const onEnded = () => setIsPlaying(false);\n\n video.addEventListener('play', onPlay);\n video.addEventListener('pause', onPause);\n video.addEventListener('ended', onEnded);\n\n return () => {\n video.removeEventListener('play', onPlay);\n video.removeEventListener('pause', onPause);\n video.removeEventListener('ended', onEnded);\n };\n }, []);\n\n /**\n * Handle controls auto-hide when play state changes\n */\n useEffect(() => {\n if (isPlaying) {\n // Start timer when video starts playing\n showControlsWithTimer();\n } else {\n // Keep controls visible when paused only in fullscreen\n clearControlsTimeout();\n if (isFullscreen) {\n setShowControls(true);\n } else {\n // In normal mode (not fullscreen), initialize timer even when paused\n // This ensures controls will hide properly from the start\n showControlsWithTimer();\n }\n }\n }, [isPlaying, isFullscreen, showControlsWithTimer, clearControlsTimeout]);\n\n /**\n * Handle fullscreen state changes from browser events\n */\n useEffect(() => {\n const handleFullscreenChange = () => {\n const isCurrentlyFullscreen = !!document.fullscreenElement;\n setIsFullscreen(isCurrentlyFullscreen);\n\n // Show controls when entering fullscreen, hide after timeout if playing\n if (isCurrentlyFullscreen) {\n showControlsWithTimer();\n }\n };\n\n document.addEventListener('fullscreenchange', handleFullscreenChange);\n\n return () => {\n document.removeEventListener('fullscreenchange', handleFullscreenChange);\n };\n }, [showControlsWithTimer]);\n\n /**\n * Initialize controls behavior on component mount\n * This ensures controls work correctly from the first load\n */\n useEffect(() => {\n const init = () => {\n if (!isFullscreen) {\n showControlsWithTimer();\n }\n };\n // Prefer rAF to avoid arbitrary timing if available; fall back to INIT_DELAY.\n let raf1 = 0,\n raf2 = 0,\n tid: number | undefined;\n if (typeof window.requestAnimationFrame === 'function') {\n raf1 = requestAnimationFrame(() => {\n raf2 = requestAnimationFrame(init);\n });\n return () => {\n cancelAnimationFrame(raf1);\n cancelAnimationFrame(raf2);\n };\n } else {\n tid = window.setTimeout(init, INIT_DELAY);\n return () => {\n if (tid) clearTimeout(tid);\n };\n }\n }, []); // Run only once on mount\n\n /**\n * Get initial time from props or localStorage\n */\n const getInitialTime = useCallback((): number | undefined => {\n if (!autoSave || !storageKey) {\n return Number.isFinite(initialTime) && initialTime >= 0\n ? initialTime\n : undefined;\n }\n\n const saved = Number(localStorage.getItem(`${storageKey}-${src}`) || NaN);\n const hasValidInitial = Number.isFinite(initialTime) && initialTime >= 0;\n const hasValidSaved = Number.isFinite(saved) && saved >= 0;\n\n if (hasValidInitial) return initialTime;\n if (hasValidSaved) return saved;\n return undefined;\n }, [autoSave, storageKey, src, initialTime]);\n\n /**\n * Load saved progress from localStorage\n */\n useEffect(() => {\n const start = getInitialTime();\n if (start !== undefined && videoRef.current) {\n videoRef.current.currentTime = start;\n setCurrentTime(start);\n }\n }, [getInitialTime]);\n\n /**\n * Save progress to localStorage periodically\n */\n const saveProgress = useCallback(\n (time: number) => {\n if (!autoSave || !storageKey) return;\n\n const now = Date.now();\n if (now - lastSaveTimeRef.current > 5000) {\n localStorage.setItem(`${storageKey}-${src}`, time.toString());\n lastSaveTimeRef.current = now;\n }\n },\n [autoSave, storageKey, src]\n );\n\n /**\n * Handle play/pause toggle\n */\n const togglePlayPause = useCallback(async () => {\n const video = videoRef.current;\n if (!video) return;\n\n if (!video.paused) {\n video.pause();\n return;\n }\n\n try {\n await video.play();\n } catch {\n // Playback prevented (e.g., autoplay policy); keep state unchanged.\n }\n }, []);\n\n /**\n * Handle volume change\n */\n const handleVolumeChange = useCallback(\n (newVolume: number) => {\n const video = videoRef.current;\n if (!video) return;\n\n const volumeValue = newVolume / 100; // Convert 0-100 to 0-1\n video.volume = volumeValue;\n setVolume(volumeValue);\n\n // Auto mute/unmute based on volume\n const shouldMute = volumeValue === 0;\n const shouldUnmute = volumeValue > 0 && isMuted;\n\n if (shouldMute) {\n video.muted = true;\n setIsMuted(true);\n } else if (shouldUnmute) {\n video.muted = false;\n setIsMuted(false);\n }\n },\n [isMuted]\n );\n\n /**\n * Handle mute toggle\n */\n const toggleMute = useCallback(() => {\n const video = videoRef.current;\n if (!video) return;\n\n if (isMuted) {\n // Unmute: restore volume or set to 50% if it was 0\n const restoreVolume = volume > 0 ? volume : 0.5;\n video.volume = restoreVolume;\n video.muted = false;\n setVolume(restoreVolume);\n setIsMuted(false);\n } else {\n // Mute: set volume to 0 and mute\n video.muted = true;\n setIsMuted(true);\n }\n }, [isMuted, volume]);\n\n /**\n * Handle video seek\n */\n const handleSeek = useCallback((newTime: number) => {\n const video = videoRef.current;\n if (video) {\n video.currentTime = newTime;\n }\n }, []);\n\n /**\n * Handle fullscreen toggle\n */\n const toggleFullscreen = useCallback(() => {\n const container = videoRef.current?.parentElement;\n if (!container) return;\n\n if (!isFullscreen && container.requestFullscreen) {\n container.requestFullscreen();\n } else if (isFullscreen && document.exitFullscreen) {\n document.exitFullscreen();\n }\n }, [isFullscreen]);\n\n /**\n * Handle playback speed change\n */\n const handleSpeedChange = useCallback((speed: number) => {\n if (videoRef.current) {\n videoRef.current.playbackRate = speed;\n setPlaybackRate(speed);\n setShowSpeedMenu(false);\n }\n }, []);\n\n /**\n * Toggle speed menu visibility\n */\n const toggleSpeedMenu = useCallback(() => {\n setShowSpeedMenu(!showSpeedMenu);\n }, [showSpeedMenu]);\n\n /**\n * Toggle captions visibility\n */\n const toggleCaptions = useCallback(() => {\n if (!trackRef.current?.track || !subtitles) return;\n\n const newShowCaptions = !showCaptions;\n setShowCaptions(newShowCaptions);\n\n // Control track mode programmatically - only show if subtitles are available\n trackRef.current.track.mode =\n newShowCaptions && subtitles ? 'showing' : 'hidden';\n }, [showCaptions, subtitles]);\n\n /**\n * Check video completion and fire callback\n */\n const checkVideoCompletion = useCallback(\n (progressPercent: number) => {\n if (progressPercent >= 95 && !hasCompleted) {\n setHasCompleted(true);\n onVideoComplete?.();\n }\n },\n [hasCompleted, onVideoComplete]\n );\n\n /**\n * Handle time update\n */\n const handleTimeUpdate = useCallback(() => {\n const video = videoRef.current;\n if (!video) return;\n\n const current = video.currentTime;\n setCurrentTime(current);\n\n // Save progress periodically\n saveProgress(current);\n\n // Fire callbacks\n onTimeUpdate?.(current);\n\n if (duration > 0) {\n const progressPercent = (current / duration) * 100;\n onProgress?.(progressPercent);\n checkVideoCompletion(progressPercent);\n }\n }, [duration, saveProgress, onTimeUpdate, onProgress, checkVideoCompletion]);\n\n /**\n * Handle loaded metadata\n */\n const handleLoadedMetadata = useCallback(() => {\n if (videoRef.current) {\n setDuration(videoRef.current.duration);\n }\n }, []);\n\n /**\n * Initialize track mode when track is available\n */\n useEffect(() => {\n if (trackRef.current?.track) {\n // Set initial mode based on showCaptions state and subtitle availability\n trackRef.current.track.mode =\n showCaptions && subtitles ? 'showing' : 'hidden';\n }\n }, [subtitles, showCaptions]);\n\n /**\n * Handle visibility change and blur to pause video when losing focus\n */\n useEffect(() => {\n const handleVisibilityChange = () => {\n if (document.hidden && isPlaying && videoRef.current) {\n videoRef.current.pause();\n setIsPlaying(false);\n }\n };\n\n const handleBlur = () => {\n if (isPlaying && videoRef.current) {\n videoRef.current.pause();\n setIsPlaying(false);\n }\n };\n\n document.addEventListener('visibilitychange', handleVisibilityChange);\n window.addEventListener('blur', handleBlur);\n\n return () => {\n document.removeEventListener('visibilitychange', handleVisibilityChange);\n window.removeEventListener('blur', handleBlur);\n // Clean up timers on unmount\n clearControlsTimeout();\n };\n }, [isPlaying, clearControlsTimeout]);\n\n const progressPercentage = duration > 0 ? (currentTime / duration) * 100 : 0;\n\n /**\n * Calculate top controls opacity based on state\n */\n const getTopControlsOpacity = useCallback(() => {\n return showControls ? 'opacity-100' : 'opacity-0';\n }, [showControls]);\n\n /**\n * Calculate bottom controls opacity based on state\n */\n const getBottomControlsOpacity = useCallback(() => {\n return showControls ? 'opacity-100' : 'opacity-0';\n }, [showControls]);\n\n /**\n * Handle video element keyboard events\n */\n const handleVideoKeyDown = useCallback(\n (e: KeyboardEvent) => {\n if (e.key) {\n // Prevent bubbling to parent handlers to avoid double toggles\n e.stopPropagation();\n showControlsWithTimer();\n }\n\n switch (e.key) {\n case ' ':\n case 'Enter':\n e.preventDefault();\n togglePlayPause();\n break;\n case 'ArrowLeft':\n e.preventDefault();\n if (videoRef.current) {\n videoRef.current.currentTime -= 10;\n }\n break;\n case 'ArrowRight':\n e.preventDefault();\n if (videoRef.current) {\n videoRef.current.currentTime += 10;\n }\n break;\n case 'ArrowUp':\n e.preventDefault();\n handleVolumeChange(Math.min(100, volume * 100 + 10));\n break;\n case 'ArrowDown':\n e.preventDefault();\n handleVolumeChange(Math.max(0, volume * 100 - 10));\n break;\n case 'm':\n case 'M':\n e.preventDefault();\n toggleMute();\n break;\n case 'f':\n case 'F':\n e.preventDefault();\n toggleFullscreen();\n break;\n default:\n break;\n }\n },\n [\n showControlsWithTimer,\n togglePlayPause,\n handleVolumeChange,\n volume,\n toggleMute,\n toggleFullscreen,\n ]\n );\n\n return (\n <div className={cn('flex flex-col', className)}>\n {/* Integrated Header */}\n {(title || subtitleText) && (\n <div className=\"bg-subject-1 px-8 py-4 flex items-end justify-between min-h-20\">\n <div className=\"flex flex-col gap-1\">\n {title && (\n <Text\n as=\"h2\"\n size=\"lg\"\n weight=\"bold\"\n color=\"text-text-900\"\n className=\"leading-5 tracking-wide\"\n >\n {title}\n </Text>\n )}\n {subtitleText && (\n <Text\n as=\"p\"\n size=\"sm\"\n weight=\"normal\"\n color=\"text-text-600\"\n className=\"leading-5\"\n >\n {subtitleText}\n </Text>\n )}\n </div>\n </div>\n )}\n\n {/* Video Container */}\n <section\n className={cn(\n 'relative w-full bg-background overflow-hidden group',\n 'rounded-b-xl',\n // Hide cursor when controls are hidden and video is playing\n isPlaying && !showControls\n ? 'cursor-none group-hover:cursor-default'\n : 'cursor-default'\n )}\n aria-label={title ? `Video player: ${title}` : 'Video player'}\n onMouseMove={handleMouseMove}\n onMouseEnter={handleMouseEnter}\n onTouchStart={handleMouseEnter}\n onMouseLeave={handleMouseLeave}\n >\n {/* Video Element */}\n <video\n ref={videoRef}\n src={src}\n poster={poster}\n className=\"w-full h-full object-contain\"\n controlsList=\"nodownload\"\n onTimeUpdate={handleTimeUpdate}\n onLoadedMetadata={handleLoadedMetadata}\n onClick={togglePlayPause}\n onKeyDown={handleVideoKeyDown}\n tabIndex={0}\n aria-label={title ? `Video: ${title}` : 'Video player'}\n >\n <track\n ref={trackRef}\n kind=\"captions\"\n src={subtitles || 'data:text/vtt;charset=utf-8,WEBVTT'}\n srcLang=\"pt-br\"\n label={\n subtitles ? 'Legendas em Português' : 'Sem legendas disponíveis'\n }\n default={false}\n />\n </video>\n\n {/* Center Play Button */}\n {!isPlaying && (\n <div className=\"absolute inset-0 flex items-center justify-center bg-black/30 transition-opacity\">\n <IconButton\n icon={<Play size={32} weight=\"regular\" className=\"ml-1\" />}\n onClick={togglePlayPause}\n aria-label=\"Play video\"\n className=\"!bg-transparent !text-white !w-auto !h-auto hover:!bg-transparent hover:!text-gray-200\"\n />\n </div>\n )}\n\n {/* Top Controls */}\n <div\n className={cn(\n 'absolute top-0 left-0 right-0 p-4 bg-gradient-to-b from-black/70 to-transparent transition-opacity',\n getTopControlsOpacity()\n )}\n >\n <div className=\"flex justify-start\">\n <IconButton\n icon={\n isFullscreen ? (\n <ArrowsInSimple size={24} />\n ) : (\n <ArrowsOutSimple size={24} />\n )\n }\n onClick={toggleFullscreen}\n aria-label={isFullscreen ? 'Exit fullscreen' : 'Enter fullscreen'}\n className=\"!bg-transparent !text-white hover:!bg-white/20\"\n />\n </div>\n </div>\n\n {/* Bottom Controls */}\n <div\n className={cn(\n 'absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/90 to-transparent transition-opacity',\n getBottomControlsOpacity()\n )}\n >\n {/* Progress Bar */}\n <ProgressBar\n currentTime={currentTime}\n duration={duration}\n progressPercentage={progressPercentage}\n onSeek={handleSeek}\n />\n\n {/* Control Buttons */}\n <div className=\"flex items-center justify-between px-4 pb-4\">\n {/* Left Controls */}\n <div className=\"flex items-center gap-4\">\n {/* Play/Pause */}\n <IconButton\n icon={isPlaying ? <Pause size={24} /> : <Play size={24} />}\n onClick={togglePlayPause}\n aria-label={isPlaying ? 'Pause' : 'Play'}\n className=\"!bg-transparent !text-white hover:!bg-white/20\"\n />\n\n {/* Volume */}\n <VolumeControls\n volume={volume}\n isMuted={isMuted}\n onVolumeChange={handleVolumeChange}\n onToggleMute={toggleMute}\n />\n\n {/* Captions */}\n {subtitles && (\n <IconButton\n icon={<ClosedCaptioning size={24} />}\n onClick={toggleCaptions}\n aria-label={showCaptions ? 'Hide captions' : 'Show captions'}\n className={cn(\n '!bg-transparent hover:!bg-white/20',\n showCaptions ? '!text-primary-400' : '!text-white'\n )}\n />\n )}\n\n {/* Time Display */}\n <Text size=\"sm\" weight=\"medium\" color=\"text-white\">\n {formatTime(currentTime)} / {formatTime(duration)}\n </Text>\n </div>\n\n {/* Right Controls */}\n <div className=\"flex items-center gap-4\">\n {/* Speed Control */}\n <SpeedMenu\n showSpeedMenu={showSpeedMenu}\n playbackRate={playbackRate}\n onToggleMenu={toggleSpeedMenu}\n onSpeedChange={handleSpeedChange}\n />\n </div>\n </div>\n </div>\n </section>\n </div>\n );\n};\n\nexport default VideoPlayer;\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, 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 { ComponentPropsWithoutRef, ElementType, ReactNode } from 'react';\nimport { cn } from '../../utils/utils';\n\n/**\n * Base text component props\n */\ntype BaseTextProps = {\n /** Content to be displayed */\n children?: ReactNode;\n /** Text size variant */\n size?:\n | '2xs'\n | 'xs'\n | 'sm'\n | 'md'\n | 'lg'\n | 'xl'\n | '2xl'\n | '3xl'\n | '4xl'\n | '5xl'\n | '6xl';\n /** Font weight variant */\n weight?:\n | 'hairline'\n | 'light'\n | 'normal'\n | 'medium'\n | 'semibold'\n | 'bold'\n | 'extrabold'\n | 'black';\n /** Color variant - white for light backgrounds, black for dark backgrounds */\n color?: string;\n /** Additional CSS classes to apply */\n className?: string;\n};\n\n/**\n * Polymorphic text component props that ensures type safety based on the 'as' prop\n */\ntype TextProps<T extends ElementType = 'p'> = BaseTextProps & {\n /** HTML tag to render */\n as?: T;\n} & Omit<ComponentPropsWithoutRef<T>, keyof BaseTextProps>;\n\n/**\n * Text component for Analytica Ensino platforms\n *\n * A flexible polymorphic text component with multiple sizes, weights, and colors.\n * Automatically adapts to dark and light themes with full type safety.\n *\n * @param children - The content to display\n * @param size - The text size variant (2xs, xs, sm, md, lg, xl, 2xl, 3xl, 4xl, 5xl, 6xl)\n * @param weight - The font weight variant (hairline, light, normal, medium, semibold, bold, extrabold, black)\n * @param color - The color variant - adapts to theme\n * @param as - The HTML tag to render - determines allowed attributes via TypeScript\n * @param className - Additional CSS classes\n * @param props - HTML attributes valid for the chosen tag only\n * @returns A styled text element with type-safe attributes\n *\n * @example\n * ```tsx\n * <Text size=\"lg\" weight=\"bold\" color=\"text-info-800\">\n * This is a large, bold text\n * </Text>\n *\n * <Text as=\"a\" href=\"/link\" target=\"_blank\">\n * Link with type-safe anchor attributes\n * </Text>\n *\n * <Text as=\"button\" onClick={handleClick} disabled>\n * Button with type-safe button attributes\n * </Text>\n * ```\n */\nconst Text = <T extends ElementType = 'p'>({\n children,\n size = 'md',\n weight = 'normal',\n color = 'text-text-950',\n as,\n className = '',\n ...props\n}: TextProps<T>) => {\n let sizeClasses = '';\n let weightClasses = '';\n\n // Text size classes mapping\n const sizeClassMap = {\n '2xs': 'text-2xs',\n xs: 'text-xs',\n sm: 'text-sm',\n md: 'text-md',\n lg: 'text-lg',\n xl: 'text-xl',\n '2xl': 'text-2xl',\n '3xl': 'text-3xl',\n '4xl': 'text-4xl',\n '5xl': 'text-5xl',\n '6xl': 'text-6xl',\n } as const;\n\n sizeClasses = sizeClassMap[size] ?? sizeClassMap.md;\n\n // Font weight classes mapping\n const weightClassMap = {\n hairline: 'font-hairline',\n light: 'font-light',\n normal: 'font-normal',\n medium: 'font-medium',\n semibold: 'font-semibold',\n bold: 'font-bold',\n extrabold: 'font-extrabold',\n black: 'font-black',\n } as const;\n\n weightClasses = weightClassMap[weight] ?? weightClassMap.normal;\n\n const baseClasses = 'font-primary';\n const Component = as ?? ('p' as ElementType);\n\n return (\n <Component\n className={cn(baseClasses, sizeClasses, weightClasses, color, className)}\n {...props}\n >\n {children}\n </Component>\n );\n};\n\nexport default Text;\n"],"mappings":";AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAGK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;;;ACjBP,SAAS,YAA6B;AACtC,SAAS,eAAe;AAEjB,SAAS,MAAM,QAAsB;AAC1C,SAAO,QAAQ,KAAK,MAAM,CAAC;AAC7B;;;ACLA,SAA0C,kBAAkB;AAyHpD;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;;;ACNX,gBAAAA,YAAA;AA/CJ,IAAM,OAAO,CAA8B;AAAA,EACzC;AAAA,EACA,OAAO;AAAA,EACP,SAAS;AAAA,EACT,QAAQ;AAAA,EACR;AAAA,EACA,YAAY;AAAA,EACZ,GAAG;AACL,MAAoB;AAClB,MAAI,cAAc;AAClB,MAAI,gBAAgB;AAGpB,QAAM,eAAe;AAAA,IACnB,OAAO;AAAA,IACP,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,OAAO;AAAA,IACP,OAAO;AAAA,IACP,OAAO;AAAA,IACP,OAAO;AAAA,EACT;AAEA,gBAAc,aAAa,IAAI,KAAK,aAAa;AAGjD,QAAM,iBAAiB;AAAA,IACrB,UAAU;AAAA,IACV,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,UAAU;AAAA,IACV,MAAM;AAAA,IACN,WAAW;AAAA,IACX,OAAO;AAAA,EACT;AAEA,kBAAgB,eAAe,MAAM,KAAK,eAAe;AAEzD,QAAM,cAAc;AACpB,QAAM,YAAY,MAAO;AAEzB,SACE,gBAAAA;AAAA,IAAC;AAAA;AAAA,MACC,WAAW,GAAG,aAAa,aAAa,eAAe,OAAO,SAAS;AAAA,MACtE,GAAG;AAAA,MAEH;AAAA;AAAA,EACH;AAEJ;AAEA,IAAO,eAAQ;;;AH3CX,gBAAAC,MAkCF,YAlCE;AAlEJ,IAAM,wBAAwB;AAC9B,IAAM,qBAAqB;AAC3B,IAAM,aAAa;AAqCnB,IAAM,aAAa,CAAC,YAA4B;AAC9C,MAAI,CAAC,WAAW,MAAM,OAAO,EAAG,QAAO;AACvC,QAAM,OAAO,KAAK,MAAM,UAAU,EAAE;AACpC,QAAM,OAAO,KAAK,MAAM,UAAU,EAAE;AACpC,SAAO,GAAG,IAAI,IAAI,KAAK,SAAS,EAAE,SAAS,GAAG,GAAG,CAAC;AACpD;AAeA,IAAM,cAAc,CAAC;AAAA,EACnB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,MACE,gBAAAA,KAAC,SAAI,WAAU,aACb,0BAAAA;AAAA,EAAC;AAAA;AAAA,IACC,MAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK,YAAY;AAAA,IACjB,OAAO;AAAA,IACP,UAAU,CAAC,MAAM,OAAO,WAAW,EAAE,OAAO,KAAK,CAAC;AAAA,IAClD,WAAU;AAAA,IACV,cAAW;AAAA,IACX,OAAO;AAAA,MACL,YAAY,sDAAsD,kBAAkB,iCAAiC,kBAAkB;AAAA,IACzI;AAAA;AACF,GACF;AAgBF,IAAM,iBAAiB,CAAC;AAAA,EACtB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,MACE,qBAAC,SAAI,WAAU,2BACb;AAAA,kBAAAA;AAAA,IAAC;AAAA;AAAA,MACC,MAAM,UAAU,gBAAAA,KAAC,gBAAa,MAAM,IAAI,IAAK,gBAAAA,KAAC,eAAY,MAAM,IAAI;AAAA,MACpE,SAAS;AAAA,MACT,cAAY,UAAU,WAAW;AAAA,MACjC,WAAU;AAAA;AAAA,EACZ;AAAA,EAEA,gBAAAA;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,KAAK;AAAA,MACL,KAAK;AAAA,MACL,OAAO,KAAK,MAAM,SAAS,GAAG;AAAA,MAC9B,UAAU,CAAC,MAAM,eAAe,SAAS,EAAE,OAAO,KAAK,CAAC;AAAA,MACxD,WAAU;AAAA,MACV,cAAW;AAAA,MACX,OAAO;AAAA,QACL,YAAY,sDAAsD,SAAS,GAAG,iCAAiC,SAAS,GAAG;AAAA,MAC7H;AAAA;AAAA,EACF;AAAA,GACF;AAgBF,IAAM,YAAY,CAAC;AAAA,EACjB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,MACE,qBAAC,SAAI,WAAU,YACb;AAAA,kBAAAA;AAAA,IAAC;AAAA;AAAA,MACC,MAAM,gBAAAA,KAAC,qBAAkB,MAAM,IAAI;AAAA,MACnC,SAAS;AAAA,MACT,cAAW;AAAA,MACX,WAAU;AAAA;AAAA,EACZ;AAAA,EACC,iBACC,gBAAAA,KAAC,SAAI,WAAU,kEACZ,WAAC,KAAK,MAAM,GAAG,MAAM,KAAK,CAAC,EAAE,IAAI,CAAC,UACjC;AAAA,IAAC;AAAA;AAAA,MAEC,SAAS,MAAM,cAAc,KAAK;AAAA,MAClC,WAAW,wFACT,iBAAiB,QAAQ,qBAAqB,YAChD;AAAA,MAEC;AAAA;AAAA,QAAM;AAAA;AAAA;AAAA,IANF;AAAA,EAOP,CACD,GACH;AAAA,GAEJ;AAUF,IAAM,cAAc,CAAC;AAAA,EACnB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,UAAU;AAAA,EACV,cAAc;AAAA,EACd;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX,aAAa;AACf,MAAwB;AACtB,QAAM,WAAW,OAAyB,IAAI;AAC9C,QAAM,CAAC,WAAW,YAAY,IAAI,SAAS,KAAK;AAChD,QAAM,CAAC,aAAa,cAAc,IAAI,SAAS,CAAC;AAChD,QAAM,CAAC,UAAU,WAAW,IAAI,SAAS,CAAC;AAC1C,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,KAAK;AAC5C,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,CAAC;AACtC,QAAM,CAAC,cAAc,eAAe,IAAI,SAAS,KAAK;AACtD,QAAM,CAAC,cAAc,eAAe,IAAI,SAAS,IAAI;AACrD,QAAM,CAAC,cAAc,eAAe,IAAI,SAAS,KAAK;AACtD,QAAM,CAAC,cAAc,eAAe,IAAI,SAAS,KAAK;AAGtD,YAAU,MAAM;AACd,oBAAgB,KAAK;AAAA,EACvB,GAAG,CAAC,GAAG,CAAC;AACR,QAAM,CAAC,cAAc,eAAe,IAAI,SAAS,CAAC;AAClD,QAAM,CAAC,eAAe,gBAAgB,IAAI,SAAS,KAAK;AACxD,QAAM,kBAAkB,OAAO,CAAC;AAChC,QAAM,WAAW,OAAyB,IAAI;AAC9C,QAAM,qBAAqB,OAAsB,IAAI;AACrD,QAAM,uBAAuB,OAAO,EAAE,GAAG,GAAG,GAAG,EAAE,CAAC;AAKlD,QAAM,oBAAoB,YAAY,MAAM;AAE1C,QAAI,eAAe;AACjB,aAAO;AAAA,IACT;AAGA,UAAM,gBAAgB,SAAS;AAC/B,UAAM,iBAAiB,SAAS,SAAS;AAEzC,QAAI,iBAAiB,gBAAgB,SAAS,aAAa,GAAG;AAE5D,UAAI,kBAAkB,SAAS,SAAS;AACtC,eAAO;AAAA,MACT;AAGA,YAAM,YAAY,cAAc,QAAQ,2BAA2B;AACnE,UAAI,WAAW;AACb,eAAO;AAAA,MACT;AAAA,IACF;AAEA,WAAO;AAAA,EACT,GAAG,CAAC,aAAa,CAAC;AAKlB,QAAM,uBAAuB,YAAY,MAAM;AAC7C,QAAI,mBAAmB,SAAS;AAC9B,mBAAa,mBAAmB,OAAO;AACvC,yBAAmB,UAAU;AAAA,IAC/B;AAAA,EACF,GAAG,CAAC,CAAC;AAKL,QAAM,wBAAwB,YAAY,MAAM;AAC9C,oBAAgB,IAAI;AACpB,yBAAqB;AAGrB,QAAI,cAAc;AAChB,UAAI,WAAW;AACb,2BAAmB,UAAU,OAAO,WAAW,MAAM;AACnD,0BAAgB,KAAK;AAAA,QACvB,GAAG,qBAAqB;AAAA,MAC1B;AAAA,IACF,OAAO;AAEL,yBAAmB,UAAU,OAAO,WAAW,MAAM;AACnD,wBAAgB,KAAK;AAAA,MACvB,GAAG,qBAAqB;AAAA,IAC1B;AAAA,EACF,GAAG,CAAC,cAAc,WAAW,oBAAoB,CAAC;AAKlD,QAAM,kBAAkB;AAAA,IACtB,CAAC,UAAsB;AACrB,YAAM,WAAW,MAAM;AACvB,YAAM,WAAW,MAAM;AACvB,YAAM,UAAU,qBAAqB;AAGrC,YAAM,WACJ,KAAK,IAAI,WAAW,QAAQ,CAAC,IAAI,KACjC,KAAK,IAAI,WAAW,QAAQ,CAAC,IAAI;AAEnC,UAAI,UAAU;AACZ,6BAAqB,UAAU,EAAE,GAAG,UAAU,GAAG,SAAS;AAC1D,8BAAsB;AAAA,MACxB;AAAA,IACF;AAAA,IACA,CAAC,qBAAqB;AAAA,EACxB;AAKA,QAAM,mBAAmB,YAAY,MAAM;AACzC,0BAAsB;AAAA,EACxB,GAAG,CAAC,qBAAqB,CAAC;AAK1B,QAAM,mBAAmB,YAAY,MAAM;AACzC,UAAM,kBAAkB,kBAAkB;AAC1C,yBAAqB;AAGrB,QAAI,CAAC,gBAAgB,CAAC,iBAAiB;AAErC,yBAAmB,UAAU,OAAO,WAAW,MAAM;AACnD,wBAAgB,KAAK;AAAA,MACvB,GAAG,kBAAkB;AAAA,IACvB;AAAA,EACF,GAAG,CAAC,cAAc,sBAAsB,iBAAiB,CAAC;AAK1D,YAAU,MAAM;AAEd,QAAI,SAAS,SAAS;AACpB,eAAS,QAAQ,SAAS;AAC1B,eAAS,QAAQ,QAAQ;AAAA,IAC3B;AAAA,EACF,GAAG,CAAC,QAAQ,OAAO,CAAC;AAKpB,YAAU,MAAM;AACd,UAAM,QAAQ,SAAS;AACvB,QAAI,CAAC,MAAO;AAEZ,UAAM,SAAS,MAAM,aAAa,IAAI;AACtC,UAAM,UAAU,MAAM,aAAa,KAAK;AACxC,UAAM,UAAU,MAAM,aAAa,KAAK;AAExC,UAAM,iBAAiB,QAAQ,MAAM;AACrC,UAAM,iBAAiB,SAAS,OAAO;AACvC,UAAM,iBAAiB,SAAS,OAAO;AAEvC,WAAO,MAAM;AACX,YAAM,oBAAoB,QAAQ,MAAM;AACxC,YAAM,oBAAoB,SAAS,OAAO;AAC1C,YAAM,oBAAoB,SAAS,OAAO;AAAA,IAC5C;AAAA,EACF,GAAG,CAAC,CAAC;AAKL,YAAU,MAAM;AACd,QAAI,WAAW;AAEb,4BAAsB;AAAA,IACxB,OAAO;AAEL,2BAAqB;AACrB,UAAI,cAAc;AAChB,wBAAgB,IAAI;AAAA,MACtB,OAAO;AAGL,8BAAsB;AAAA,MACxB;AAAA,IACF;AAAA,EACF,GAAG,CAAC,WAAW,cAAc,uBAAuB,oBAAoB,CAAC;AAKzE,YAAU,MAAM;AACd,UAAM,yBAAyB,MAAM;AACnC,YAAM,wBAAwB,CAAC,CAAC,SAAS;AACzC,sBAAgB,qBAAqB;AAGrC,UAAI,uBAAuB;AACzB,8BAAsB;AAAA,MACxB;AAAA,IACF;AAEA,aAAS,iBAAiB,oBAAoB,sBAAsB;AAEpE,WAAO,MAAM;AACX,eAAS,oBAAoB,oBAAoB,sBAAsB;AAAA,IACzE;AAAA,EACF,GAAG,CAAC,qBAAqB,CAAC;AAM1B,YAAU,MAAM;AACd,UAAM,OAAO,MAAM;AACjB,UAAI,CAAC,cAAc;AACjB,8BAAsB;AAAA,MACxB;AAAA,IACF;AAEA,QAAI,OAAO,GACT,OAAO,GACP;AACF,QAAI,OAAO,OAAO,0BAA0B,YAAY;AACtD,aAAO,sBAAsB,MAAM;AACjC,eAAO,sBAAsB,IAAI;AAAA,MACnC,CAAC;AACD,aAAO,MAAM;AACX,6BAAqB,IAAI;AACzB,6BAAqB,IAAI;AAAA,MAC3B;AAAA,IACF,OAAO;AACL,YAAM,OAAO,WAAW,MAAM,UAAU;AACxC,aAAO,MAAM;AACX,YAAI,IAAK,cAAa,GAAG;AAAA,MAC3B;AAAA,IACF;AAAA,EACF,GAAG,CAAC,CAAC;AAKL,QAAM,iBAAiB,YAAY,MAA0B;AAC3D,QAAI,CAAC,YAAY,CAAC,YAAY;AAC5B,aAAO,OAAO,SAAS,WAAW,KAAK,eAAe,IAClD,cACA;AAAA,IACN;AAEA,UAAM,QAAQ,OAAO,aAAa,QAAQ,GAAG,UAAU,IAAI,GAAG,EAAE,KAAK,GAAG;AACxE,UAAM,kBAAkB,OAAO,SAAS,WAAW,KAAK,eAAe;AACvE,UAAM,gBAAgB,OAAO,SAAS,KAAK,KAAK,SAAS;AAEzD,QAAI,gBAAiB,QAAO;AAC5B,QAAI,cAAe,QAAO;AAC1B,WAAO;AAAA,EACT,GAAG,CAAC,UAAU,YAAY,KAAK,WAAW,CAAC;AAK3C,YAAU,MAAM;AACd,UAAM,QAAQ,eAAe;AAC7B,QAAI,UAAU,UAAa,SAAS,SAAS;AAC3C,eAAS,QAAQ,cAAc;AAC/B,qBAAe,KAAK;AAAA,IACtB;AAAA,EACF,GAAG,CAAC,cAAc,CAAC;AAKnB,QAAM,eAAe;AAAA,IACnB,CAAC,SAAiB;AAChB,UAAI,CAAC,YAAY,CAAC,WAAY;AAE9B,YAAM,MAAM,KAAK,IAAI;AACrB,UAAI,MAAM,gBAAgB,UAAU,KAAM;AACxC,qBAAa,QAAQ,GAAG,UAAU,IAAI,GAAG,IAAI,KAAK,SAAS,CAAC;AAC5D,wBAAgB,UAAU;AAAA,MAC5B;AAAA,IACF;AAAA,IACA,CAAC,UAAU,YAAY,GAAG;AAAA,EAC5B;AAKA,QAAM,kBAAkB,YAAY,YAAY;AAC9C,UAAM,QAAQ,SAAS;AACvB,QAAI,CAAC,MAAO;AAEZ,QAAI,CAAC,MAAM,QAAQ;AACjB,YAAM,MAAM;AACZ;AAAA,IACF;AAEA,QAAI;AACF,YAAM,MAAM,KAAK;AAAA,IACnB,QAAQ;AAAA,IAER;AAAA,EACF,GAAG,CAAC,CAAC;AAKL,QAAM,qBAAqB;AAAA,IACzB,CAAC,cAAsB;AACrB,YAAM,QAAQ,SAAS;AACvB,UAAI,CAAC,MAAO;AAEZ,YAAM,cAAc,YAAY;AAChC,YAAM,SAAS;AACf,gBAAU,WAAW;AAGrB,YAAM,aAAa,gBAAgB;AACnC,YAAM,eAAe,cAAc,KAAK;AAExC,UAAI,YAAY;AACd,cAAM,QAAQ;AACd,mBAAW,IAAI;AAAA,MACjB,WAAW,cAAc;AACvB,cAAM,QAAQ;AACd,mBAAW,KAAK;AAAA,MAClB;AAAA,IACF;AAAA,IACA,CAAC,OAAO;AAAA,EACV;AAKA,QAAM,aAAa,YAAY,MAAM;AACnC,UAAM,QAAQ,SAAS;AACvB,QAAI,CAAC,MAAO;AAEZ,QAAI,SAAS;AAEX,YAAM,gBAAgB,SAAS,IAAI,SAAS;AAC5C,YAAM,SAAS;AACf,YAAM,QAAQ;AACd,gBAAU,aAAa;AACvB,iBAAW,KAAK;AAAA,IAClB,OAAO;AAEL,YAAM,QAAQ;AACd,iBAAW,IAAI;AAAA,IACjB;AAAA,EACF,GAAG,CAAC,SAAS,MAAM,CAAC;AAKpB,QAAM,aAAa,YAAY,CAAC,YAAoB;AAClD,UAAM,QAAQ,SAAS;AACvB,QAAI,OAAO;AACT,YAAM,cAAc;AAAA,IACtB;AAAA,EACF,GAAG,CAAC,CAAC;AAKL,QAAM,mBAAmB,YAAY,MAAM;AACzC,UAAM,YAAY,SAAS,SAAS;AACpC,QAAI,CAAC,UAAW;AAEhB,QAAI,CAAC,gBAAgB,UAAU,mBAAmB;AAChD,gBAAU,kBAAkB;AAAA,IAC9B,WAAW,gBAAgB,SAAS,gBAAgB;AAClD,eAAS,eAAe;AAAA,IAC1B;AAAA,EACF,GAAG,CAAC,YAAY,CAAC;AAKjB,QAAM,oBAAoB,YAAY,CAAC,UAAkB;AACvD,QAAI,SAAS,SAAS;AACpB,eAAS,QAAQ,eAAe;AAChC,sBAAgB,KAAK;AACrB,uBAAiB,KAAK;AAAA,IACxB;AAAA,EACF,GAAG,CAAC,CAAC;AAKL,QAAM,kBAAkB,YAAY,MAAM;AACxC,qBAAiB,CAAC,aAAa;AAAA,EACjC,GAAG,CAAC,aAAa,CAAC;AAKlB,QAAM,iBAAiB,YAAY,MAAM;AACvC,QAAI,CAAC,SAAS,SAAS,SAAS,CAAC,UAAW;AAE5C,UAAM,kBAAkB,CAAC;AACzB,oBAAgB,eAAe;AAG/B,aAAS,QAAQ,MAAM,OACrB,mBAAmB,YAAY,YAAY;AAAA,EAC/C,GAAG,CAAC,cAAc,SAAS,CAAC;AAK5B,QAAM,uBAAuB;AAAA,IAC3B,CAAC,oBAA4B;AAC3B,UAAI,mBAAmB,MAAM,CAAC,cAAc;AAC1C,wBAAgB,IAAI;AACpB,0BAAkB;AAAA,MACpB;AAAA,IACF;AAAA,IACA,CAAC,cAAc,eAAe;AAAA,EAChC;AAKA,QAAM,mBAAmB,YAAY,MAAM;AACzC,UAAM,QAAQ,SAAS;AACvB,QAAI,CAAC,MAAO;AAEZ,UAAM,UAAU,MAAM;AACtB,mBAAe,OAAO;AAGtB,iBAAa,OAAO;AAGpB,mBAAe,OAAO;AAEtB,QAAI,WAAW,GAAG;AAChB,YAAM,kBAAmB,UAAU,WAAY;AAC/C,mBAAa,eAAe;AAC5B,2BAAqB,eAAe;AAAA,IACtC;AAAA,EACF,GAAG,CAAC,UAAU,cAAc,cAAc,YAAY,oBAAoB,CAAC;AAK3E,QAAM,uBAAuB,YAAY,MAAM;AAC7C,QAAI,SAAS,SAAS;AACpB,kBAAY,SAAS,QAAQ,QAAQ;AAAA,IACvC;AAAA,EACF,GAAG,CAAC,CAAC;AAKL,YAAU,MAAM;AACd,QAAI,SAAS,SAAS,OAAO;AAE3B,eAAS,QAAQ,MAAM,OACrB,gBAAgB,YAAY,YAAY;AAAA,IAC5C;AAAA,EACF,GAAG,CAAC,WAAW,YAAY,CAAC;AAK5B,YAAU,MAAM;AACd,UAAM,yBAAyB,MAAM;AACnC,UAAI,SAAS,UAAU,aAAa,SAAS,SAAS;AACpD,iBAAS,QAAQ,MAAM;AACvB,qBAAa,KAAK;AAAA,MACpB;AAAA,IACF;AAEA,UAAM,aAAa,MAAM;AACvB,UAAI,aAAa,SAAS,SAAS;AACjC,iBAAS,QAAQ,MAAM;AACvB,qBAAa,KAAK;AAAA,MACpB;AAAA,IACF;AAEA,aAAS,iBAAiB,oBAAoB,sBAAsB;AACpE,WAAO,iBAAiB,QAAQ,UAAU;AAE1C,WAAO,MAAM;AACX,eAAS,oBAAoB,oBAAoB,sBAAsB;AACvE,aAAO,oBAAoB,QAAQ,UAAU;AAE7C,2BAAqB;AAAA,IACvB;AAAA,EACF,GAAG,CAAC,WAAW,oBAAoB,CAAC;AAEpC,QAAM,qBAAqB,WAAW,IAAK,cAAc,WAAY,MAAM;AAK3E,QAAM,wBAAwB,YAAY,MAAM;AAC9C,WAAO,eAAe,gBAAgB;AAAA,EACxC,GAAG,CAAC,YAAY,CAAC;AAKjB,QAAM,2BAA2B,YAAY,MAAM;AACjD,WAAO,eAAe,gBAAgB;AAAA,EACxC,GAAG,CAAC,YAAY,CAAC;AAKjB,QAAM,qBAAqB;AAAA,IACzB,CAAC,MAAqB;AACpB,UAAI,EAAE,KAAK;AAET,UAAE,gBAAgB;AAClB,8BAAsB;AAAA,MACxB;AAEA,cAAQ,EAAE,KAAK;AAAA,QACb,KAAK;AAAA,QACL,KAAK;AACH,YAAE,eAAe;AACjB,0BAAgB;AAChB;AAAA,QACF,KAAK;AACH,YAAE,eAAe;AACjB,cAAI,SAAS,SAAS;AACpB,qBAAS,QAAQ,eAAe;AAAA,UAClC;AACA;AAAA,QACF,KAAK;AACH,YAAE,eAAe;AACjB,cAAI,SAAS,SAAS;AACpB,qBAAS,QAAQ,eAAe;AAAA,UAClC;AACA;AAAA,QACF,KAAK;AACH,YAAE,eAAe;AACjB,6BAAmB,KAAK,IAAI,KAAK,SAAS,MAAM,EAAE,CAAC;AACnD;AAAA,QACF,KAAK;AACH,YAAE,eAAe;AACjB,6BAAmB,KAAK,IAAI,GAAG,SAAS,MAAM,EAAE,CAAC;AACjD;AAAA,QACF,KAAK;AAAA,QACL,KAAK;AACH,YAAE,eAAe;AACjB,qBAAW;AACX;AAAA,QACF,KAAK;AAAA,QACL,KAAK;AACH,YAAE,eAAe;AACjB,2BAAiB;AACjB;AAAA,QACF;AACE;AAAA,MACJ;AAAA,IACF;AAAA,IACA;AAAA,MACE;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAEA,SACE,qBAAC,SAAI,WAAW,GAAG,iBAAiB,SAAS,GAEzC;AAAA,cAAS,iBACT,gBAAAA,KAAC,SAAI,WAAU,kEACb,+BAAC,SAAI,WAAU,uBACZ;AAAA,eACC,gBAAAA;AAAA,QAAC;AAAA;AAAA,UACC,IAAG;AAAA,UACH,MAAK;AAAA,UACL,QAAO;AAAA,UACP,OAAM;AAAA,UACN,WAAU;AAAA,UAET;AAAA;AAAA,MACH;AAAA,MAED,gBACC,gBAAAA;AAAA,QAAC;AAAA;AAAA,UACC,IAAG;AAAA,UACH,MAAK;AAAA,UACL,QAAO;AAAA,UACP,OAAM;AAAA,UACN,WAAU;AAAA,UAET;AAAA;AAAA,MACH;AAAA,OAEJ,GACF;AAAA,IAIF;AAAA,MAAC;AAAA;AAAA,QACC,WAAW;AAAA,UACT;AAAA,UACA;AAAA;AAAA,UAEA,aAAa,CAAC,eACV,2CACA;AAAA,QACN;AAAA,QACA,cAAY,QAAQ,iBAAiB,KAAK,KAAK;AAAA,QAC/C,aAAa;AAAA,QACb,cAAc;AAAA,QACd,cAAc;AAAA,QACd,cAAc;AAAA,QAGd;AAAA,0BAAAA;AAAA,YAAC;AAAA;AAAA,cACC,KAAK;AAAA,cACL;AAAA,cACA;AAAA,cACA,WAAU;AAAA,cACV,cAAa;AAAA,cACb,cAAc;AAAA,cACd,kBAAkB;AAAA,cAClB,SAAS;AAAA,cACT,WAAW;AAAA,cACX,UAAU;AAAA,cACV,cAAY,QAAQ,UAAU,KAAK,KAAK;AAAA,cAExC,0BAAAA;AAAA,gBAAC;AAAA;AAAA,kBACC,KAAK;AAAA,kBACL,MAAK;AAAA,kBACL,KAAK,aAAa;AAAA,kBAClB,SAAQ;AAAA,kBACR,OACE,YAAY,6BAA0B;AAAA,kBAExC,SAAS;AAAA;AAAA,cACX;AAAA;AAAA,UACF;AAAA,UAGC,CAAC,aACA,gBAAAA,KAAC,SAAI,WAAU,oFACb,0BAAAA;AAAA,YAAC;AAAA;AAAA,cACC,MAAM,gBAAAA,KAAC,QAAK,MAAM,IAAI,QAAO,WAAU,WAAU,QAAO;AAAA,cACxD,SAAS;AAAA,cACT,cAAW;AAAA,cACX,WAAU;AAAA;AAAA,UACZ,GACF;AAAA,UAIF,gBAAAA;AAAA,YAAC;AAAA;AAAA,cACC,WAAW;AAAA,gBACT;AAAA,gBACA,sBAAsB;AAAA,cACxB;AAAA,cAEA,0BAAAA,KAAC,SAAI,WAAU,sBACb,0BAAAA;AAAA,gBAAC;AAAA;AAAA,kBACC,MACE,eACE,gBAAAA,KAAC,kBAAe,MAAM,IAAI,IAE1B,gBAAAA,KAAC,mBAAgB,MAAM,IAAI;AAAA,kBAG/B,SAAS;AAAA,kBACT,cAAY,eAAe,oBAAoB;AAAA,kBAC/C,WAAU;AAAA;AAAA,cACZ,GACF;AAAA;AAAA,UACF;AAAA,UAGA;AAAA,YAAC;AAAA;AAAA,cACC,WAAW;AAAA,gBACT;AAAA,gBACA,yBAAyB;AAAA,cAC3B;AAAA,cAGA;AAAA,gCAAAA;AAAA,kBAAC;AAAA;AAAA,oBACC;AAAA,oBACA;AAAA,oBACA;AAAA,oBACA,QAAQ;AAAA;AAAA,gBACV;AAAA,gBAGA,qBAAC,SAAI,WAAU,+CAEb;AAAA,uCAAC,SAAI,WAAU,2BAEb;AAAA,oCAAAA;AAAA,sBAAC;AAAA;AAAA,wBACC,MAAM,YAAY,gBAAAA,KAAC,SAAM,MAAM,IAAI,IAAK,gBAAAA,KAAC,QAAK,MAAM,IAAI;AAAA,wBACxD,SAAS;AAAA,wBACT,cAAY,YAAY,UAAU;AAAA,wBAClC,WAAU;AAAA;AAAA,oBACZ;AAAA,oBAGA,gBAAAA;AAAA,sBAAC;AAAA;AAAA,wBACC;AAAA,wBACA;AAAA,wBACA,gBAAgB;AAAA,wBAChB,cAAc;AAAA;AAAA,oBAChB;AAAA,oBAGC,aACC,gBAAAA;AAAA,sBAAC;AAAA;AAAA,wBACC,MAAM,gBAAAA,KAAC,oBAAiB,MAAM,IAAI;AAAA,wBAClC,SAAS;AAAA,wBACT,cAAY,eAAe,kBAAkB;AAAA,wBAC7C,WAAW;AAAA,0BACT;AAAA,0BACA,eAAe,sBAAsB;AAAA,wBACvC;AAAA;AAAA,oBACF;AAAA,oBAIF,qBAAC,gBAAK,MAAK,MAAK,QAAO,UAAS,OAAM,cACnC;AAAA,iCAAW,WAAW;AAAA,sBAAE;AAAA,sBAAI,WAAW,QAAQ;AAAA,uBAClD;AAAA,qBACF;AAAA,kBAGA,gBAAAA,KAAC,SAAI,WAAU,2BAEb,0BAAAA;AAAA,oBAAC;AAAA;AAAA,sBACC;AAAA,sBACA;AAAA,sBACA,cAAc;AAAA,sBACd,eAAe;AAAA;AAAA,kBACjB,GACF;AAAA,mBACF;AAAA;AAAA;AAAA,UACF;AAAA;AAAA;AAAA,IACF;AAAA,KACF;AAEJ;AAEA,IAAO,sBAAQ;","names":["jsx","jsx"]}
1
+ {"version":3,"sources":["../../src/components/VideoPlayer/VideoPlayer.tsx","../../src/utils/utils.ts","../../src/components/IconButton/IconButton.tsx","../../src/components/Text/Text.tsx"],"sourcesContent":["import {\n useRef,\n useState,\n useEffect,\n useCallback,\n MouseEvent,\n KeyboardEvent,\n} from 'react';\nimport { createPortal } from 'react-dom';\nimport {\n Play,\n Pause,\n SpeakerHigh,\n SpeakerSlash,\n ArrowsOutSimple,\n ArrowsInSimple,\n ClosedCaptioning,\n DotsThreeVertical,\n} from 'phosphor-react';\nimport { cn } from '../../utils/utils';\nimport IconButton from '../IconButton/IconButton';\nimport Text from '../Text/Text';\n\n// Constants for timeout durations\nconst CONTROLS_HIDE_TIMEOUT = 3000; // 3 seconds for normal control hiding\nconst LEAVE_HIDE_TIMEOUT = 1000; // 1 second when mouse leaves the video area\nconst INIT_DELAY = 100; // ms delay to initialize controls on mount\n\n/**\n * VideoPlayer component props interface\n */\ninterface VideoPlayerProps {\n /** Video source URL */\n src: string;\n /** Video poster/thumbnail URL */\n poster?: string;\n /** Subtitles URL */\n subtitles?: string;\n /** Video title */\n title?: string;\n /** Video subtitle/description */\n subtitle?: string;\n /** Initial playback time in seconds */\n initialTime?: number;\n /** Callback fired when video time updates (seconds) */\n onTimeUpdate?: (seconds: number) => void;\n /** Callback fired with progress percentage (0-100) */\n onProgress?: (progress: number) => void;\n /** Callback fired when video completes (>95% watched) */\n onVideoComplete?: () => void;\n /** Additional CSS classes */\n className?: string;\n /** Auto-save progress to localStorage */\n autoSave?: boolean;\n /** localStorage key for saving progress */\n storageKey?: string;\n}\n\n/**\n * Format seconds to MM:SS display format\n * @param seconds - Time in seconds\n * @returns Formatted time string\n */\nconst formatTime = (seconds: number): string => {\n if (!seconds || isNaN(seconds)) return '0:00';\n const mins = Math.floor(seconds / 60);\n const secs = Math.floor(seconds % 60);\n return `${mins}:${secs.toString().padStart(2, '0')}`;\n};\n\n/**\n * Progress bar component props\n */\ninterface ProgressBarProps {\n currentTime: number;\n duration: number;\n progressPercentage: number;\n onSeek: (time: number) => void;\n}\n\n/**\n * Progress bar subcomponent\n */\nconst ProgressBar = ({\n currentTime,\n duration,\n progressPercentage,\n onSeek,\n}: ProgressBarProps) => (\n <div className=\"px-4 pb-2\">\n <input\n type=\"range\"\n min={0}\n max={duration || 100}\n value={currentTime}\n onChange={(e) => onSeek(parseFloat(e.target.value))}\n className=\"w-full h-1 bg-neutral-600 rounded-full appearance-none cursor-pointer slider:bg-primary-600 focus:outline-none focus:ring-2 focus:ring-primary-500\"\n aria-label=\"Video progress\"\n style={{\n background: `linear-gradient(to right, var(--color-primary-700) ${progressPercentage}%, var(--color-secondary-300) ${progressPercentage}%)`,\n }}\n />\n </div>\n);\n\n/**\n * Volume controls component props\n */\ninterface VolumeControlsProps {\n volume: number;\n isMuted: boolean;\n onVolumeChange: (volume: number) => void;\n onToggleMute: () => void;\n}\n\n/**\n * Volume controls subcomponent\n */\nconst VolumeControls = ({\n volume,\n isMuted,\n onVolumeChange,\n onToggleMute,\n}: VolumeControlsProps) => (\n <div className=\"flex items-center gap-2\">\n <IconButton\n icon={isMuted ? <SpeakerSlash size={24} /> : <SpeakerHigh size={24} />}\n onClick={onToggleMute}\n aria-label={isMuted ? 'Unmute' : 'Mute'}\n className=\"!bg-transparent !text-white hover:!bg-white/20\"\n />\n\n <input\n type=\"range\"\n min={0}\n max={100}\n value={Math.round(volume * 100)}\n onChange={(e) => onVolumeChange(parseInt(e.target.value))}\n className=\"w-20 h-1 bg-neutral-600 rounded-full appearance-none cursor-pointer focus:outline-none focus:ring-2 focus:ring-primary-500\"\n aria-label=\"Volume control\"\n style={{\n background: `linear-gradient(to right, var(--color-primary-700) ${volume * 100}%, var(--color-secondary-300) ${volume * 100}%)`,\n }}\n />\n </div>\n);\n\n/**\n * Speed menu component props\n */\ninterface SpeedMenuProps {\n showSpeedMenu: boolean;\n playbackRate: number;\n onToggleMenu: () => void;\n onSpeedChange: (speed: number) => void;\n isFullscreen: boolean;\n}\n\n/**\n * Speed menu subcomponent\n */\nconst SpeedMenu = ({\n showSpeedMenu,\n playbackRate,\n onToggleMenu,\n onSpeedChange,\n isFullscreen,\n}: SpeedMenuProps) => {\n const buttonRef = useRef<HTMLButtonElement>(null);\n\n const getMenuPosition = () => {\n if (!buttonRef.current) return { top: 0, left: 0 };\n const rect = buttonRef.current.getBoundingClientRect();\n return {\n // Fixed coords are viewport-based — no scroll offsets.\n top: Math.max(8, rect.top - 180),\n left: Math.max(8, rect.right - 80),\n };\n };\n\n const position = getMenuPosition();\n\n const menuContent = (\n <div\n role=\"menu\"\n aria-label=\"Playback speed\"\n className={\n isFullscreen\n ? 'absolute bottom-12 right-0 bg-black/90 rounded-lg p-2 min-w-20 z-[9999]'\n : 'fixed bg-black/90 rounded-lg p-2 min-w-20 z-[9999]'\n }\n style={\n !isFullscreen\n ? {\n top: `${position.top}px`,\n left: `${position.left}px`,\n }\n : undefined\n }\n >\n {[0.5, 0.75, 1, 1.25, 1.5, 2].map((speed) => (\n <button\n key={speed}\n role=\"menuitemradio\"\n aria-checked={playbackRate === speed}\n onClick={() => onSpeedChange(speed)}\n className={`block w-full text-left px-3 py-1 text-sm rounded hover:bg-white/20 transition-colors ${\n playbackRate === speed ? 'text-primary-400' : 'text-white'\n }`}\n >\n {speed}x\n </button>\n ))}\n </div>\n );\n\n // SSR-safe portal content\n const portalContent =\n typeof window !== 'undefined' && typeof document !== 'undefined'\n ? createPortal(menuContent, document.body)\n : null;\n\n return (\n <div className=\"relative\">\n <IconButton\n ref={buttonRef}\n icon={<DotsThreeVertical size={24} />}\n onClick={onToggleMenu}\n aria-label=\"Playback speed\"\n aria-haspopup=\"menu\"\n aria-expanded={showSpeedMenu}\n className=\"!bg-transparent !text-white hover:!bg-white/20\"\n />\n {showSpeedMenu && (isFullscreen ? menuContent : portalContent)}\n </div>\n );\n};\n\n/**\n * Video player component with controls and progress tracking\n * Integrates with backend lesson progress system\n *\n * @param props - VideoPlayer component props\n * @returns Video player element with controls\n */\nconst VideoPlayer = ({\n src,\n poster,\n subtitles,\n title,\n subtitle: subtitleText,\n initialTime = 0,\n onTimeUpdate,\n onProgress,\n onVideoComplete,\n className,\n autoSave = true,\n storageKey = 'video-progress',\n}: VideoPlayerProps) => {\n const videoRef = useRef<HTMLVideoElement>(null);\n const [isPlaying, setIsPlaying] = useState(false);\n const [currentTime, setCurrentTime] = useState(0);\n const [duration, setDuration] = useState(0);\n const [isMuted, setIsMuted] = useState(false);\n const [volume, setVolume] = useState(1);\n const [isFullscreen, setIsFullscreen] = useState(false);\n const [showControls, setShowControls] = useState(true);\n const [hasCompleted, setHasCompleted] = useState(false);\n const [showCaptions, setShowCaptions] = useState(false);\n\n // Reset completion flag when changing videos\n useEffect(() => {\n setHasCompleted(false);\n }, [src]);\n const [playbackRate, setPlaybackRate] = useState(1);\n const [showSpeedMenu, setShowSpeedMenu] = useState(false);\n const lastSaveTimeRef = useRef(0);\n const trackRef = useRef<HTMLTrackElement>(null);\n const controlsTimeoutRef = useRef<number | null>(null);\n const lastMousePositionRef = useRef({ x: 0, y: 0 });\n\n /**\n * Check if user is currently interacting with controls\n */\n const isUserInteracting = useCallback(() => {\n // Check if speed menu is open\n if (showSpeedMenu) {\n return true;\n }\n\n // Check if any control element has focus\n const activeElement = document.activeElement;\n const videoContainer = videoRef.current?.parentElement;\n\n if (activeElement && videoContainer?.contains(activeElement)) {\n // Ignore the video element itself - it should not prevent control hiding\n if (activeElement === videoRef.current) {\n return false;\n }\n\n // Check if focused element is a control (button, input, etc.)\n const isControl = activeElement.matches('button, input, [tabindex]');\n if (isControl) {\n return true;\n }\n }\n\n return false;\n }, [showSpeedMenu]);\n\n /**\n * Clear controls timeout\n */\n const clearControlsTimeout = useCallback(() => {\n if (controlsTimeoutRef.current) {\n clearTimeout(controlsTimeoutRef.current);\n controlsTimeoutRef.current = null;\n }\n }, []);\n\n /**\n * Show controls and set auto-hide timer\n */\n const showControlsWithTimer = useCallback(() => {\n setShowControls(true);\n clearControlsTimeout();\n\n // In fullscreen mode, only hide if video is playing\n if (isFullscreen) {\n if (isPlaying) {\n controlsTimeoutRef.current = window.setTimeout(() => {\n setShowControls(false);\n }, CONTROLS_HIDE_TIMEOUT);\n }\n } else {\n // In normal mode, always set a timer to hide controls\n controlsTimeoutRef.current = window.setTimeout(() => {\n setShowControls(false);\n }, CONTROLS_HIDE_TIMEOUT);\n }\n }, [isFullscreen, isPlaying, clearControlsTimeout]);\n\n /**\n * Handle mouse move with position detection\n */\n const handleMouseMove = useCallback(\n (event: MouseEvent) => {\n const currentX = event.clientX;\n const currentY = event.clientY;\n const lastPos = lastMousePositionRef.current;\n\n // Check if mouse actually moved (minimum 5px threshold)\n const hasMoved =\n Math.abs(currentX - lastPos.x) > 5 ||\n Math.abs(currentY - lastPos.y) > 5;\n\n if (hasMoved) {\n lastMousePositionRef.current = { x: currentX, y: currentY };\n showControlsWithTimer();\n }\n },\n [showControlsWithTimer]\n );\n\n /**\n * Handle mouse enter to show controls with appropriate timer logic\n */\n const handleMouseEnter = useCallback(() => {\n showControlsWithTimer();\n }, [showControlsWithTimer]);\n\n /**\n * Handle mouse leave to hide controls faster\n */\n const handleMouseLeave = useCallback(() => {\n const userInteracting = isUserInteracting();\n clearControlsTimeout();\n\n // Hide controls when mouse leaves, except when in fullscreen or user is interacting\n if (!isFullscreen && !userInteracting) {\n // Use shorter timeout when mouse leaves\n controlsTimeoutRef.current = window.setTimeout(() => {\n setShowControls(false);\n }, LEAVE_HIDE_TIMEOUT);\n }\n }, [isFullscreen, clearControlsTimeout, isUserInteracting]);\n\n /**\n * Initialize video element properties\n */\n useEffect(() => {\n // Set initial volume\n if (videoRef.current) {\n videoRef.current.volume = volume;\n videoRef.current.muted = isMuted;\n }\n }, [volume, isMuted]);\n\n /**\n * Synchronize isPlaying state with media events\n */\n useEffect(() => {\n const video = videoRef.current;\n if (!video) return;\n\n const onPlay = () => setIsPlaying(true);\n const onPause = () => setIsPlaying(false);\n const onEnded = () => setIsPlaying(false);\n\n video.addEventListener('play', onPlay);\n video.addEventListener('pause', onPause);\n video.addEventListener('ended', onEnded);\n\n return () => {\n video.removeEventListener('play', onPlay);\n video.removeEventListener('pause', onPause);\n video.removeEventListener('ended', onEnded);\n };\n }, []);\n\n /**\n * Handle controls auto-hide when play state changes\n */\n useEffect(() => {\n if (isPlaying) {\n // Start timer when video starts playing\n showControlsWithTimer();\n } else {\n // Keep controls visible when paused only in fullscreen\n clearControlsTimeout();\n if (isFullscreen) {\n setShowControls(true);\n } else {\n // In normal mode (not fullscreen), initialize timer even when paused\n // This ensures controls will hide properly from the start\n showControlsWithTimer();\n }\n }\n }, [isPlaying, isFullscreen, showControlsWithTimer, clearControlsTimeout]);\n\n /**\n * Handle fullscreen state changes from browser events\n */\n useEffect(() => {\n const handleFullscreenChange = () => {\n const isCurrentlyFullscreen = !!document.fullscreenElement;\n setIsFullscreen(isCurrentlyFullscreen);\n\n // Show controls when entering fullscreen, hide after timeout if playing\n if (isCurrentlyFullscreen) {\n showControlsWithTimer();\n }\n };\n\n document.addEventListener('fullscreenchange', handleFullscreenChange);\n\n return () => {\n document.removeEventListener('fullscreenchange', handleFullscreenChange);\n };\n }, [showControlsWithTimer]);\n\n /**\n * Initialize controls behavior on component mount\n * This ensures controls work correctly from the first load\n */\n useEffect(() => {\n const init = () => {\n if (!isFullscreen) {\n showControlsWithTimer();\n }\n };\n // Prefer rAF to avoid arbitrary timing if available; fall back to INIT_DELAY.\n let raf1 = 0,\n raf2 = 0,\n tid: number | undefined;\n if (typeof window.requestAnimationFrame === 'function') {\n raf1 = requestAnimationFrame(() => {\n raf2 = requestAnimationFrame(init);\n });\n return () => {\n cancelAnimationFrame(raf1);\n cancelAnimationFrame(raf2);\n };\n } else {\n tid = window.setTimeout(init, INIT_DELAY);\n return () => {\n if (tid) clearTimeout(tid);\n };\n }\n }, []); // Run only once on mount\n\n /**\n * Get initial time from props or localStorage\n */\n const getInitialTime = useCallback((): number | undefined => {\n if (!autoSave || !storageKey) {\n return Number.isFinite(initialTime) && initialTime >= 0\n ? initialTime\n : undefined;\n }\n\n const saved = Number(localStorage.getItem(`${storageKey}-${src}`) || NaN);\n const hasValidInitial = Number.isFinite(initialTime) && initialTime >= 0;\n const hasValidSaved = Number.isFinite(saved) && saved >= 0;\n\n if (hasValidInitial) return initialTime;\n if (hasValidSaved) return saved;\n return undefined;\n }, [autoSave, storageKey, src, initialTime]);\n\n /**\n * Load saved progress from localStorage\n */\n useEffect(() => {\n const start = getInitialTime();\n if (start !== undefined && videoRef.current) {\n videoRef.current.currentTime = start;\n setCurrentTime(start);\n }\n }, [getInitialTime]);\n\n /**\n * Save progress to localStorage periodically\n */\n const saveProgress = useCallback(\n (time: number) => {\n if (!autoSave || !storageKey) return;\n\n const now = Date.now();\n if (now - lastSaveTimeRef.current > 5000) {\n localStorage.setItem(`${storageKey}-${src}`, time.toString());\n lastSaveTimeRef.current = now;\n }\n },\n [autoSave, storageKey, src]\n );\n\n /**\n * Handle play/pause toggle\n */\n const togglePlayPause = useCallback(async () => {\n const video = videoRef.current;\n if (!video) return;\n\n if (!video.paused) {\n video.pause();\n return;\n }\n\n try {\n await video.play();\n } catch {\n // Playback prevented (e.g., autoplay policy); keep state unchanged.\n }\n }, []);\n\n /**\n * Handle volume change\n */\n const handleVolumeChange = useCallback(\n (newVolume: number) => {\n const video = videoRef.current;\n if (!video) return;\n\n const volumeValue = newVolume / 100; // Convert 0-100 to 0-1\n video.volume = volumeValue;\n setVolume(volumeValue);\n\n // Auto mute/unmute based on volume\n const shouldMute = volumeValue === 0;\n const shouldUnmute = volumeValue > 0 && isMuted;\n\n if (shouldMute) {\n video.muted = true;\n setIsMuted(true);\n } else if (shouldUnmute) {\n video.muted = false;\n setIsMuted(false);\n }\n },\n [isMuted]\n );\n\n /**\n * Handle mute toggle\n */\n const toggleMute = useCallback(() => {\n const video = videoRef.current;\n if (!video) return;\n\n if (isMuted) {\n // Unmute: restore volume or set to 50% if it was 0\n const restoreVolume = volume > 0 ? volume : 0.5;\n video.volume = restoreVolume;\n video.muted = false;\n setVolume(restoreVolume);\n setIsMuted(false);\n } else {\n // Mute: set volume to 0 and mute\n video.muted = true;\n setIsMuted(true);\n }\n }, [isMuted, volume]);\n\n /**\n * Handle video seek\n */\n const handleSeek = useCallback((newTime: number) => {\n const video = videoRef.current;\n if (video) {\n video.currentTime = newTime;\n }\n }, []);\n\n /**\n * Handle fullscreen toggle\n */\n const toggleFullscreen = useCallback(() => {\n const container = videoRef.current?.parentElement;\n if (!container) return;\n\n if (!isFullscreen && container.requestFullscreen) {\n container.requestFullscreen();\n } else if (isFullscreen && document.exitFullscreen) {\n document.exitFullscreen();\n }\n }, [isFullscreen]);\n\n /**\n * Handle playback speed change\n */\n const handleSpeedChange = useCallback((speed: number) => {\n if (videoRef.current) {\n videoRef.current.playbackRate = speed;\n setPlaybackRate(speed);\n setShowSpeedMenu(false);\n }\n }, []);\n\n /**\n * Toggle speed menu visibility\n */\n const toggleSpeedMenu = useCallback(() => {\n setShowSpeedMenu(!showSpeedMenu);\n }, [showSpeedMenu]);\n\n /**\n * Toggle captions visibility\n */\n const toggleCaptions = useCallback(() => {\n if (!trackRef.current?.track || !subtitles) return;\n\n const newShowCaptions = !showCaptions;\n setShowCaptions(newShowCaptions);\n\n // Control track mode programmatically - only show if subtitles are available\n trackRef.current.track.mode =\n newShowCaptions && subtitles ? 'showing' : 'hidden';\n }, [showCaptions, subtitles]);\n\n /**\n * Check video completion and fire callback\n */\n const checkVideoCompletion = useCallback(\n (progressPercent: number) => {\n if (progressPercent >= 95 && !hasCompleted) {\n setHasCompleted(true);\n onVideoComplete?.();\n }\n },\n [hasCompleted, onVideoComplete]\n );\n\n /**\n * Handle time update\n */\n const handleTimeUpdate = useCallback(() => {\n const video = videoRef.current;\n if (!video) return;\n\n const current = video.currentTime;\n setCurrentTime(current);\n\n // Save progress periodically\n saveProgress(current);\n\n // Fire callbacks\n onTimeUpdate?.(current);\n\n if (duration > 0) {\n const progressPercent = (current / duration) * 100;\n onProgress?.(progressPercent);\n checkVideoCompletion(progressPercent);\n }\n }, [duration, saveProgress, onTimeUpdate, onProgress, checkVideoCompletion]);\n\n /**\n * Handle loaded metadata\n */\n const handleLoadedMetadata = useCallback(() => {\n if (videoRef.current) {\n setDuration(videoRef.current.duration);\n }\n }, []);\n\n /**\n * Initialize track mode when track is available\n */\n useEffect(() => {\n if (trackRef.current?.track) {\n // Set initial mode based on showCaptions state and subtitle availability\n trackRef.current.track.mode =\n showCaptions && subtitles ? 'showing' : 'hidden';\n }\n }, [subtitles, showCaptions]);\n\n /**\n * Handle visibility change and blur to pause video when losing focus\n */\n useEffect(() => {\n const handleVisibilityChange = () => {\n if (document.hidden && isPlaying && videoRef.current) {\n videoRef.current.pause();\n setIsPlaying(false);\n }\n };\n\n const handleBlur = () => {\n if (isPlaying && videoRef.current) {\n videoRef.current.pause();\n setIsPlaying(false);\n }\n };\n\n document.addEventListener('visibilitychange', handleVisibilityChange);\n window.addEventListener('blur', handleBlur);\n\n return () => {\n document.removeEventListener('visibilitychange', handleVisibilityChange);\n window.removeEventListener('blur', handleBlur);\n // Clean up timers on unmount\n clearControlsTimeout();\n };\n }, [isPlaying, clearControlsTimeout]);\n\n const progressPercentage = duration > 0 ? (currentTime / duration) * 100 : 0;\n\n /**\n * Calculate top controls opacity based on state\n */\n const getTopControlsOpacity = useCallback(() => {\n return showControls ? 'opacity-100' : 'opacity-0';\n }, [showControls]);\n\n /**\n * Calculate bottom controls opacity based on state\n */\n const getBottomControlsOpacity = useCallback(() => {\n return showControls ? 'opacity-100' : 'opacity-0';\n }, [showControls]);\n\n /**\n * Handle video element keyboard events\n */\n const handleVideoKeyDown = useCallback(\n (e: KeyboardEvent) => {\n if (e.key) {\n // Prevent bubbling to parent handlers to avoid double toggles\n e.stopPropagation();\n showControlsWithTimer();\n }\n\n switch (e.key) {\n case ' ':\n case 'Enter':\n e.preventDefault();\n togglePlayPause();\n break;\n case 'ArrowLeft':\n e.preventDefault();\n if (videoRef.current) {\n videoRef.current.currentTime -= 10;\n }\n break;\n case 'ArrowRight':\n e.preventDefault();\n if (videoRef.current) {\n videoRef.current.currentTime += 10;\n }\n break;\n case 'ArrowUp':\n e.preventDefault();\n handleVolumeChange(Math.min(100, volume * 100 + 10));\n break;\n case 'ArrowDown':\n e.preventDefault();\n handleVolumeChange(Math.max(0, volume * 100 - 10));\n break;\n case 'm':\n case 'M':\n e.preventDefault();\n toggleMute();\n break;\n case 'f':\n case 'F':\n e.preventDefault();\n toggleFullscreen();\n break;\n default:\n break;\n }\n },\n [\n showControlsWithTimer,\n togglePlayPause,\n handleVolumeChange,\n volume,\n toggleMute,\n toggleFullscreen,\n ]\n );\n\n return (\n <div className={cn('flex flex-col', className)}>\n {/* Integrated Header */}\n {(title || subtitleText) && (\n <div className=\"bg-subject-1 px-8 py-4 flex items-end justify-between min-h-20\">\n <div className=\"flex flex-col gap-1\">\n {title && (\n <Text\n as=\"h2\"\n size=\"lg\"\n weight=\"bold\"\n color=\"text-text-900\"\n className=\"leading-5 tracking-wide\"\n >\n {title}\n </Text>\n )}\n {subtitleText && (\n <Text\n as=\"p\"\n size=\"sm\"\n weight=\"normal\"\n color=\"text-text-600\"\n className=\"leading-5\"\n >\n {subtitleText}\n </Text>\n )}\n </div>\n </div>\n )}\n\n {/* Video Container */}\n <section\n className={cn(\n 'relative w-full bg-background overflow-hidden group',\n 'rounded-b-xl',\n // Hide cursor when controls are hidden and video is playing\n isPlaying && !showControls\n ? 'cursor-none group-hover:cursor-default'\n : 'cursor-default'\n )}\n aria-label={title ? `Video player: ${title}` : 'Video player'}\n onMouseMove={handleMouseMove}\n onMouseEnter={handleMouseEnter}\n onTouchStart={handleMouseEnter}\n onMouseLeave={handleMouseLeave}\n >\n {/* Video Element */}\n <video\n ref={videoRef}\n src={src}\n poster={poster}\n className=\"w-full h-full object-contain\"\n controlsList=\"nodownload\"\n onTimeUpdate={handleTimeUpdate}\n onLoadedMetadata={handleLoadedMetadata}\n onClick={togglePlayPause}\n onKeyDown={handleVideoKeyDown}\n tabIndex={0}\n aria-label={title ? `Video: ${title}` : 'Video player'}\n >\n <track\n ref={trackRef}\n kind=\"captions\"\n src={subtitles || 'data:text/vtt;charset=utf-8,WEBVTT'}\n srcLang=\"pt-br\"\n label={\n subtitles ? 'Legendas em Português' : 'Sem legendas disponíveis'\n }\n default={false}\n />\n </video>\n\n {/* Center Play Button */}\n {!isPlaying && (\n <div className=\"absolute inset-0 flex items-center justify-center bg-black/30 transition-opacity\">\n <IconButton\n icon={<Play size={32} weight=\"regular\" className=\"ml-1\" />}\n onClick={togglePlayPause}\n aria-label=\"Play video\"\n className=\"!bg-transparent !text-white !w-auto !h-auto hover:!bg-transparent hover:!text-gray-200\"\n />\n </div>\n )}\n\n {/* Top Controls */}\n <div\n className={cn(\n 'absolute top-0 left-0 right-0 p-4 bg-gradient-to-b from-black/70 to-transparent transition-opacity',\n getTopControlsOpacity()\n )}\n >\n <div className=\"flex justify-start\">\n <IconButton\n icon={\n isFullscreen ? (\n <ArrowsInSimple size={24} />\n ) : (\n <ArrowsOutSimple size={24} />\n )\n }\n onClick={toggleFullscreen}\n aria-label={isFullscreen ? 'Exit fullscreen' : 'Enter fullscreen'}\n className=\"!bg-transparent !text-white hover:!bg-white/20\"\n />\n </div>\n </div>\n\n {/* Bottom Controls */}\n <div\n className={cn(\n 'absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/90 to-transparent transition-opacity',\n getBottomControlsOpacity()\n )}\n >\n {/* Progress Bar */}\n <ProgressBar\n currentTime={currentTime}\n duration={duration}\n progressPercentage={progressPercentage}\n onSeek={handleSeek}\n />\n\n {/* Control Buttons */}\n <div className=\"flex items-center justify-between px-4 pb-4\">\n {/* Left Controls */}\n <div className=\"flex items-center gap-4\">\n {/* Play/Pause */}\n <IconButton\n icon={isPlaying ? <Pause size={24} /> : <Play size={24} />}\n onClick={togglePlayPause}\n aria-label={isPlaying ? 'Pause' : 'Play'}\n className=\"!bg-transparent !text-white hover:!bg-white/20\"\n />\n\n {/* Volume */}\n <VolumeControls\n volume={volume}\n isMuted={isMuted}\n onVolumeChange={handleVolumeChange}\n onToggleMute={toggleMute}\n />\n\n {/* Captions */}\n {subtitles && (\n <IconButton\n icon={<ClosedCaptioning size={24} />}\n onClick={toggleCaptions}\n aria-label={showCaptions ? 'Hide captions' : 'Show captions'}\n className={cn(\n '!bg-transparent hover:!bg-white/20',\n showCaptions ? '!text-primary-400' : '!text-white'\n )}\n />\n )}\n\n {/* Time Display */}\n <Text size=\"sm\" weight=\"medium\" color=\"text-white\">\n {formatTime(currentTime)} / {formatTime(duration)}\n </Text>\n </div>\n\n {/* Right Controls */}\n <div className=\"flex items-center gap-4\">\n {/* Speed Control */}\n <SpeedMenu\n showSpeedMenu={showSpeedMenu}\n playbackRate={playbackRate}\n onToggleMenu={toggleSpeedMenu}\n onSpeedChange={handleSpeedChange}\n isFullscreen={isFullscreen}\n />\n </div>\n </div>\n </div>\n </section>\n </div>\n );\n};\n\nexport default VideoPlayer;\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, 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 { ComponentPropsWithoutRef, ElementType, ReactNode } from 'react';\nimport { cn } from '../../utils/utils';\n\n/**\n * Base text component props\n */\ntype BaseTextProps = {\n /** Content to be displayed */\n children?: ReactNode;\n /** Text size variant */\n size?:\n | '2xs'\n | 'xs'\n | 'sm'\n | 'md'\n | 'lg'\n | 'xl'\n | '2xl'\n | '3xl'\n | '4xl'\n | '5xl'\n | '6xl';\n /** Font weight variant */\n weight?:\n | 'hairline'\n | 'light'\n | 'normal'\n | 'medium'\n | 'semibold'\n | 'bold'\n | 'extrabold'\n | 'black';\n /** Color variant - white for light backgrounds, black for dark backgrounds */\n color?: string;\n /** Additional CSS classes to apply */\n className?: string;\n};\n\n/**\n * Polymorphic text component props that ensures type safety based on the 'as' prop\n */\ntype TextProps<T extends ElementType = 'p'> = BaseTextProps & {\n /** HTML tag to render */\n as?: T;\n} & Omit<ComponentPropsWithoutRef<T>, keyof BaseTextProps>;\n\n/**\n * Text component for Analytica Ensino platforms\n *\n * A flexible polymorphic text component with multiple sizes, weights, and colors.\n * Automatically adapts to dark and light themes with full type safety.\n *\n * @param children - The content to display\n * @param size - The text size variant (2xs, xs, sm, md, lg, xl, 2xl, 3xl, 4xl, 5xl, 6xl)\n * @param weight - The font weight variant (hairline, light, normal, medium, semibold, bold, extrabold, black)\n * @param color - The color variant - adapts to theme\n * @param as - The HTML tag to render - determines allowed attributes via TypeScript\n * @param className - Additional CSS classes\n * @param props - HTML attributes valid for the chosen tag only\n * @returns A styled text element with type-safe attributes\n *\n * @example\n * ```tsx\n * <Text size=\"lg\" weight=\"bold\" color=\"text-info-800\">\n * This is a large, bold text\n * </Text>\n *\n * <Text as=\"a\" href=\"/link\" target=\"_blank\">\n * Link with type-safe anchor attributes\n * </Text>\n *\n * <Text as=\"button\" onClick={handleClick} disabled>\n * Button with type-safe button attributes\n * </Text>\n * ```\n */\nconst Text = <T extends ElementType = 'p'>({\n children,\n size = 'md',\n weight = 'normal',\n color = 'text-text-950',\n as,\n className = '',\n ...props\n}: TextProps<T>) => {\n let sizeClasses = '';\n let weightClasses = '';\n\n // Text size classes mapping\n const sizeClassMap = {\n '2xs': 'text-2xs',\n xs: 'text-xs',\n sm: 'text-sm',\n md: 'text-md',\n lg: 'text-lg',\n xl: 'text-xl',\n '2xl': 'text-2xl',\n '3xl': 'text-3xl',\n '4xl': 'text-4xl',\n '5xl': 'text-5xl',\n '6xl': 'text-6xl',\n } as const;\n\n sizeClasses = sizeClassMap[size] ?? sizeClassMap.md;\n\n // Font weight classes mapping\n const weightClassMap = {\n hairline: 'font-hairline',\n light: 'font-light',\n normal: 'font-normal',\n medium: 'font-medium',\n semibold: 'font-semibold',\n bold: 'font-bold',\n extrabold: 'font-extrabold',\n black: 'font-black',\n } as const;\n\n weightClasses = weightClassMap[weight] ?? weightClassMap.normal;\n\n const baseClasses = 'font-primary';\n const Component = as ?? ('p' as ElementType);\n\n return (\n <Component\n className={cn(baseClasses, sizeClasses, weightClasses, color, className)}\n {...props}\n >\n {children}\n </Component>\n );\n};\n\nexport default Text;\n"],"mappings":"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","names":["jsx","jsx"]}
@@ -1,8 +1,13 @@
1
+ type ThemeMode = 'light' | 'dark' | 'system';
1
2
  /**
2
- * Hook para detectar preferências do sistema e aplicar dark mode automaticamente
3
- * Este hook aplica o theme baseado nas preferências do sistema (prefers-color-scheme)
4
- * na inicialização do componente.
3
+ * Hook para gerenciar temas com suporte a alternância manual e detecção automática do sistema
4
+ * Este hook permite alternar entre temas light, dark e automático baseado nas preferências do sistema
5
5
  */
6
- declare const useTheme: () => void;
6
+ declare const useTheme: () => {
7
+ themeMode: ThemeMode;
8
+ isDark: boolean;
9
+ toggleTheme: () => void;
10
+ setTheme: (mode: ThemeMode) => void;
11
+ };
7
12
 
8
13
  export { useTheme };
@@ -1,8 +1,13 @@
1
+ type ThemeMode = 'light' | 'dark' | 'system';
1
2
  /**
2
- * Hook para detectar preferências do sistema e aplicar dark mode automaticamente
3
- * Este hook aplica o theme baseado nas preferências do sistema (prefers-color-scheme)
4
- * na inicialização do componente.
3
+ * Hook para gerenciar temas com suporte a alternância manual e detecção automática do sistema
4
+ * Este hook permite alternar entre temas light, dark e automático baseado nas preferências do sistema
5
5
  */
6
- declare const useTheme: () => void;
6
+ declare const useTheme: () => {
7
+ themeMode: ThemeMode;
8
+ isDark: boolean;
9
+ toggleTheme: () => void;
10
+ setTheme: (mode: ThemeMode) => void;
11
+ };
7
12
 
8
13
  export { useTheme };
@@ -25,25 +25,87 @@ __export(useTheme_exports, {
25
25
  module.exports = __toCommonJS(useTheme_exports);
26
26
  var import_react = require("react");
27
27
  var useTheme = () => {
28
- (0, import_react.useEffect)(() => {
28
+ const [themeMode, setThemeMode] = (0, import_react.useState)("system");
29
+ const [isDark, setIsDark] = (0, import_react.useState)(false);
30
+ const themeModeRef = (0, import_react.useRef)("system");
31
+ const applyTheme = (0, import_react.useCallback)((mode) => {
29
32
  const htmlElement = document.documentElement;
30
- const currentTheme = htmlElement.getAttribute("data-theme");
31
- if (currentTheme && !htmlElement.getAttribute("data-original-theme")) {
32
- htmlElement.setAttribute("data-original-theme", currentTheme);
33
- }
34
- const applyTheme = () => {
35
- const isDarkMode = window.matchMedia(
33
+ const originalTheme = htmlElement.getAttribute("data-original-theme");
34
+ if (mode === "dark") {
35
+ htmlElement.setAttribute("data-theme", "dark");
36
+ setIsDark(true);
37
+ } else if (mode === "light") {
38
+ if (originalTheme) {
39
+ htmlElement.setAttribute("data-theme", originalTheme);
40
+ }
41
+ setIsDark(false);
42
+ } else if (mode === "system") {
43
+ const isSystemDark = window.matchMedia(
36
44
  "(prefers-color-scheme: dark)"
37
45
  ).matches;
38
- const originalTheme = htmlElement.getAttribute("data-original-theme");
39
- if (isDarkMode) {
46
+ if (isSystemDark) {
40
47
  htmlElement.setAttribute("data-theme", "dark");
48
+ setIsDark(true);
41
49
  } else if (originalTheme) {
42
50
  htmlElement.setAttribute("data-theme", originalTheme);
51
+ setIsDark(false);
43
52
  }
44
- };
45
- applyTheme();
53
+ }
46
54
  }, []);
55
+ const toggleTheme = (0, import_react.useCallback)(() => {
56
+ let newMode;
57
+ if (themeMode === "light") {
58
+ newMode = "dark";
59
+ } else if (themeMode === "dark") {
60
+ newMode = "light";
61
+ } else {
62
+ newMode = "dark";
63
+ }
64
+ setThemeMode(newMode);
65
+ themeModeRef.current = newMode;
66
+ applyTheme(newMode);
67
+ localStorage.setItem("theme-mode", newMode);
68
+ }, [themeMode, applyTheme]);
69
+ const setTheme = (0, import_react.useCallback)(
70
+ (mode) => {
71
+ setThemeMode(mode);
72
+ themeModeRef.current = mode;
73
+ applyTheme(mode);
74
+ localStorage.setItem("theme-mode", mode);
75
+ },
76
+ [applyTheme]
77
+ );
78
+ (0, import_react.useEffect)(() => {
79
+ const htmlElement = document.documentElement;
80
+ const currentTheme = htmlElement.getAttribute("data-theme");
81
+ if (currentTheme && !htmlElement.getAttribute("data-original-theme")) {
82
+ htmlElement.setAttribute("data-original-theme", currentTheme);
83
+ }
84
+ const savedThemeMode = localStorage.getItem("theme-mode");
85
+ const initialMode = savedThemeMode || "system";
86
+ if (!savedThemeMode) {
87
+ localStorage.setItem("theme-mode", "system");
88
+ }
89
+ setThemeMode(initialMode);
90
+ themeModeRef.current = initialMode;
91
+ applyTheme(initialMode);
92
+ const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
93
+ const handleSystemThemeChange = () => {
94
+ if (themeModeRef.current === "system") {
95
+ applyTheme("system");
96
+ }
97
+ };
98
+ mediaQuery.addEventListener("change", handleSystemThemeChange);
99
+ return () => {
100
+ mediaQuery.removeEventListener("change", handleSystemThemeChange);
101
+ };
102
+ }, [applyTheme]);
103
+ return {
104
+ themeMode,
105
+ isDark,
106
+ toggleTheme,
107
+ setTheme
108
+ };
47
109
  };
48
110
  // Annotate the CommonJS export names for ESM import in node:
49
111
  0 && (module.exports = {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/hooks/useTheme.ts"],"sourcesContent":["import { useEffect } from 'react';\n\n/**\n * Hook para detectar preferências do sistema e aplicar dark mode automaticamente\n * Este hook aplica o theme baseado nas preferências do sistema (prefers-color-scheme)\n * na inicialização do componente.\n */\nexport const useTheme = () => {\n useEffect(() => {\n const htmlElement = document.documentElement;\n\n // Salva o theme original do white label na primeira execução\n const currentTheme = htmlElement.getAttribute('data-theme');\n if (currentTheme && !htmlElement.getAttribute('data-original-theme')) {\n htmlElement.setAttribute('data-original-theme', currentTheme);\n }\n\n // Função para aplicar o theme baseado nas preferências do sistema\n const applyTheme = () => {\n const isDarkMode = window.matchMedia(\n '(prefers-color-scheme: dark)'\n ).matches;\n const originalTheme = htmlElement.getAttribute('data-original-theme');\n\n if (isDarkMode) {\n // Aplica o theme dark\n htmlElement.setAttribute('data-theme', 'dark');\n } else if (originalTheme) {\n // Restaura o theme light do white label\n htmlElement.setAttribute('data-theme', originalTheme);\n }\n };\n\n // Aplica o theme inicial\n applyTheme();\n }, []);\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAA0B;AAOnB,IAAM,WAAW,MAAM;AAC5B,8BAAU,MAAM;AACd,UAAM,cAAc,SAAS;AAG7B,UAAM,eAAe,YAAY,aAAa,YAAY;AAC1D,QAAI,gBAAgB,CAAC,YAAY,aAAa,qBAAqB,GAAG;AACpE,kBAAY,aAAa,uBAAuB,YAAY;AAAA,IAC9D;AAGA,UAAM,aAAa,MAAM;AACvB,YAAM,aAAa,OAAO;AAAA,QACxB;AAAA,MACF,EAAE;AACF,YAAM,gBAAgB,YAAY,aAAa,qBAAqB;AAEpE,UAAI,YAAY;AAEd,oBAAY,aAAa,cAAc,MAAM;AAAA,MAC/C,WAAW,eAAe;AAExB,oBAAY,aAAa,cAAc,aAAa;AAAA,MACtD;AAAA,IACF;AAGA,eAAW;AAAA,EACb,GAAG,CAAC,CAAC;AACP;","names":[]}
1
+ {"version":3,"sources":["../../../src/hooks/useTheme.ts"],"sourcesContent":["import { useEffect, useState, useCallback, useRef } from 'react';\n\ntype ThemeMode = 'light' | 'dark' | 'system';\n\n/**\n * Hook para gerenciar temas com suporte a alternância manual e detecção automática do sistema\n * Este hook permite alternar entre temas light, dark e automático baseado nas preferências do sistema\n */\nexport const useTheme = () => {\n const [themeMode, setThemeMode] = useState<ThemeMode>('system');\n const [isDark, setIsDark] = useState(false);\n\n // Ref para manter o estado atual do tema de forma síncrona\n const themeModeRef = useRef<ThemeMode>('system');\n\n // Função para aplicar o tema baseado no modo selecionado\n const applyTheme = useCallback((mode: ThemeMode) => {\n const htmlElement = document.documentElement;\n const originalTheme = htmlElement.getAttribute('data-original-theme');\n\n if (mode === 'dark') {\n htmlElement.setAttribute('data-theme', 'dark');\n setIsDark(true);\n } else if (mode === 'light') {\n if (originalTheme) {\n htmlElement.setAttribute('data-theme', originalTheme);\n }\n setIsDark(false);\n } else if (mode === 'system') {\n const isSystemDark = window.matchMedia(\n '(prefers-color-scheme: dark)'\n ).matches;\n if (isSystemDark) {\n htmlElement.setAttribute('data-theme', 'dark');\n setIsDark(true);\n } else if (originalTheme) {\n htmlElement.setAttribute('data-theme', originalTheme);\n setIsDark(false);\n }\n }\n }, []);\n\n // Função para alternar entre os temas\n const toggleTheme = useCallback(() => {\n let newMode: ThemeMode;\n if (themeMode === 'light') {\n newMode = 'dark';\n } else if (themeMode === 'dark') {\n newMode = 'light';\n } else {\n // Se estiver em 'system', vai para 'dark'\n newMode = 'dark';\n }\n setThemeMode(newMode);\n themeModeRef.current = newMode;\n applyTheme(newMode);\n localStorage.setItem('theme-mode', newMode);\n }, [themeMode, applyTheme]);\n\n // Função para definir um tema específico\n const setTheme = useCallback(\n (mode: ThemeMode) => {\n setThemeMode(mode);\n themeModeRef.current = mode;\n applyTheme(mode);\n localStorage.setItem('theme-mode', mode);\n },\n [applyTheme]\n );\n\n useEffect(() => {\n const htmlElement = document.documentElement;\n\n // Salva o theme original do white label na primeira execução\n const currentTheme = htmlElement.getAttribute('data-theme');\n if (currentTheme && !htmlElement.getAttribute('data-original-theme')) {\n htmlElement.setAttribute('data-original-theme', currentTheme);\n }\n\n // Carrega o tema salvo no localStorage ou usa 'system' como padrão\n const savedThemeMode = localStorage.getItem('theme-mode') as ThemeMode;\n const initialMode = savedThemeMode || 'system';\n\n // Se não há tema salvo, persiste 'system' como padrão\n if (!savedThemeMode) {\n localStorage.setItem('theme-mode', 'system');\n }\n\n setThemeMode(initialMode);\n themeModeRef.current = initialMode;\n applyTheme(initialMode);\n\n // Listener para mudanças nas preferências do sistema (apenas quando mode é 'system')\n const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');\n const handleSystemThemeChange = () => {\n // Usa o ref para ter acesso ao estado atual de forma síncrona\n if (themeModeRef.current === 'system') {\n applyTheme('system');\n }\n };\n\n mediaQuery.addEventListener('change', handleSystemThemeChange);\n\n return () => {\n mediaQuery.removeEventListener('change', handleSystemThemeChange);\n };\n }, [applyTheme]);\n\n return {\n themeMode,\n isDark,\n toggleTheme,\n setTheme,\n };\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAAyD;AAQlD,IAAM,WAAW,MAAM;AAC5B,QAAM,CAAC,WAAW,YAAY,QAAI,uBAAoB,QAAQ;AAC9D,QAAM,CAAC,QAAQ,SAAS,QAAI,uBAAS,KAAK;AAG1C,QAAM,mBAAe,qBAAkB,QAAQ;AAG/C,QAAM,iBAAa,0BAAY,CAAC,SAAoB;AAClD,UAAM,cAAc,SAAS;AAC7B,UAAM,gBAAgB,YAAY,aAAa,qBAAqB;AAEpE,QAAI,SAAS,QAAQ;AACnB,kBAAY,aAAa,cAAc,MAAM;AAC7C,gBAAU,IAAI;AAAA,IAChB,WAAW,SAAS,SAAS;AAC3B,UAAI,eAAe;AACjB,oBAAY,aAAa,cAAc,aAAa;AAAA,MACtD;AACA,gBAAU,KAAK;AAAA,IACjB,WAAW,SAAS,UAAU;AAC5B,YAAM,eAAe,OAAO;AAAA,QAC1B;AAAA,MACF,EAAE;AACF,UAAI,cAAc;AAChB,oBAAY,aAAa,cAAc,MAAM;AAC7C,kBAAU,IAAI;AAAA,MAChB,WAAW,eAAe;AACxB,oBAAY,aAAa,cAAc,aAAa;AACpD,kBAAU,KAAK;AAAA,MACjB;AAAA,IACF;AAAA,EACF,GAAG,CAAC,CAAC;AAGL,QAAM,kBAAc,0BAAY,MAAM;AACpC,QAAI;AACJ,QAAI,cAAc,SAAS;AACzB,gBAAU;AAAA,IACZ,WAAW,cAAc,QAAQ;AAC/B,gBAAU;AAAA,IACZ,OAAO;AAEL,gBAAU;AAAA,IACZ;AACA,iBAAa,OAAO;AACpB,iBAAa,UAAU;AACvB,eAAW,OAAO;AAClB,iBAAa,QAAQ,cAAc,OAAO;AAAA,EAC5C,GAAG,CAAC,WAAW,UAAU,CAAC;AAG1B,QAAM,eAAW;AAAA,IACf,CAAC,SAAoB;AACnB,mBAAa,IAAI;AACjB,mBAAa,UAAU;AACvB,iBAAW,IAAI;AACf,mBAAa,QAAQ,cAAc,IAAI;AAAA,IACzC;AAAA,IACA,CAAC,UAAU;AAAA,EACb;AAEA,8BAAU,MAAM;AACd,UAAM,cAAc,SAAS;AAG7B,UAAM,eAAe,YAAY,aAAa,YAAY;AAC1D,QAAI,gBAAgB,CAAC,YAAY,aAAa,qBAAqB,GAAG;AACpE,kBAAY,aAAa,uBAAuB,YAAY;AAAA,IAC9D;AAGA,UAAM,iBAAiB,aAAa,QAAQ,YAAY;AACxD,UAAM,cAAc,kBAAkB;AAGtC,QAAI,CAAC,gBAAgB;AACnB,mBAAa,QAAQ,cAAc,QAAQ;AAAA,IAC7C;AAEA,iBAAa,WAAW;AACxB,iBAAa,UAAU;AACvB,eAAW,WAAW;AAGtB,UAAM,aAAa,OAAO,WAAW,8BAA8B;AACnE,UAAM,0BAA0B,MAAM;AAEpC,UAAI,aAAa,YAAY,UAAU;AACrC,mBAAW,QAAQ;AAAA,MACrB;AAAA,IACF;AAEA,eAAW,iBAAiB,UAAU,uBAAuB;AAE7D,WAAO,MAAM;AACX,iBAAW,oBAAoB,UAAU,uBAAuB;AAAA,IAClE;AAAA,EACF,GAAG,CAAC,UAAU,CAAC;AAEf,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;","names":[]}
@@ -1,25 +1,87 @@
1
1
  // src/hooks/useTheme.ts
2
- import { useEffect } from "react";
2
+ import { useEffect, useState, useCallback, useRef } from "react";
3
3
  var useTheme = () => {
4
- useEffect(() => {
4
+ const [themeMode, setThemeMode] = useState("system");
5
+ const [isDark, setIsDark] = useState(false);
6
+ const themeModeRef = useRef("system");
7
+ const applyTheme = useCallback((mode) => {
5
8
  const htmlElement = document.documentElement;
6
- const currentTheme = htmlElement.getAttribute("data-theme");
7
- if (currentTheme && !htmlElement.getAttribute("data-original-theme")) {
8
- htmlElement.setAttribute("data-original-theme", currentTheme);
9
- }
10
- const applyTheme = () => {
11
- const isDarkMode = window.matchMedia(
9
+ const originalTheme = htmlElement.getAttribute("data-original-theme");
10
+ if (mode === "dark") {
11
+ htmlElement.setAttribute("data-theme", "dark");
12
+ setIsDark(true);
13
+ } else if (mode === "light") {
14
+ if (originalTheme) {
15
+ htmlElement.setAttribute("data-theme", originalTheme);
16
+ }
17
+ setIsDark(false);
18
+ } else if (mode === "system") {
19
+ const isSystemDark = window.matchMedia(
12
20
  "(prefers-color-scheme: dark)"
13
21
  ).matches;
14
- const originalTheme = htmlElement.getAttribute("data-original-theme");
15
- if (isDarkMode) {
22
+ if (isSystemDark) {
16
23
  htmlElement.setAttribute("data-theme", "dark");
24
+ setIsDark(true);
17
25
  } else if (originalTheme) {
18
26
  htmlElement.setAttribute("data-theme", originalTheme);
27
+ setIsDark(false);
19
28
  }
20
- };
21
- applyTheme();
29
+ }
22
30
  }, []);
31
+ const toggleTheme = useCallback(() => {
32
+ let newMode;
33
+ if (themeMode === "light") {
34
+ newMode = "dark";
35
+ } else if (themeMode === "dark") {
36
+ newMode = "light";
37
+ } else {
38
+ newMode = "dark";
39
+ }
40
+ setThemeMode(newMode);
41
+ themeModeRef.current = newMode;
42
+ applyTheme(newMode);
43
+ localStorage.setItem("theme-mode", newMode);
44
+ }, [themeMode, applyTheme]);
45
+ const setTheme = useCallback(
46
+ (mode) => {
47
+ setThemeMode(mode);
48
+ themeModeRef.current = mode;
49
+ applyTheme(mode);
50
+ localStorage.setItem("theme-mode", mode);
51
+ },
52
+ [applyTheme]
53
+ );
54
+ useEffect(() => {
55
+ const htmlElement = document.documentElement;
56
+ const currentTheme = htmlElement.getAttribute("data-theme");
57
+ if (currentTheme && !htmlElement.getAttribute("data-original-theme")) {
58
+ htmlElement.setAttribute("data-original-theme", currentTheme);
59
+ }
60
+ const savedThemeMode = localStorage.getItem("theme-mode");
61
+ const initialMode = savedThemeMode || "system";
62
+ if (!savedThemeMode) {
63
+ localStorage.setItem("theme-mode", "system");
64
+ }
65
+ setThemeMode(initialMode);
66
+ themeModeRef.current = initialMode;
67
+ applyTheme(initialMode);
68
+ const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
69
+ const handleSystemThemeChange = () => {
70
+ if (themeModeRef.current === "system") {
71
+ applyTheme("system");
72
+ }
73
+ };
74
+ mediaQuery.addEventListener("change", handleSystemThemeChange);
75
+ return () => {
76
+ mediaQuery.removeEventListener("change", handleSystemThemeChange);
77
+ };
78
+ }, [applyTheme]);
79
+ return {
80
+ themeMode,
81
+ isDark,
82
+ toggleTheme,
83
+ setTheme
84
+ };
23
85
  };
24
86
  export {
25
87
  useTheme
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/hooks/useTheme.ts"],"sourcesContent":["import { useEffect } from 'react';\n\n/**\n * Hook para detectar preferências do sistema e aplicar dark mode automaticamente\n * Este hook aplica o theme baseado nas preferências do sistema (prefers-color-scheme)\n * na inicialização do componente.\n */\nexport const useTheme = () => {\n useEffect(() => {\n const htmlElement = document.documentElement;\n\n // Salva o theme original do white label na primeira execução\n const currentTheme = htmlElement.getAttribute('data-theme');\n if (currentTheme && !htmlElement.getAttribute('data-original-theme')) {\n htmlElement.setAttribute('data-original-theme', currentTheme);\n }\n\n // Função para aplicar o theme baseado nas preferências do sistema\n const applyTheme = () => {\n const isDarkMode = window.matchMedia(\n '(prefers-color-scheme: dark)'\n ).matches;\n const originalTheme = htmlElement.getAttribute('data-original-theme');\n\n if (isDarkMode) {\n // Aplica o theme dark\n htmlElement.setAttribute('data-theme', 'dark');\n } else if (originalTheme) {\n // Restaura o theme light do white label\n htmlElement.setAttribute('data-theme', originalTheme);\n }\n };\n\n // Aplica o theme inicial\n applyTheme();\n }, []);\n};\n"],"mappings":";AAAA,SAAS,iBAAiB;AAOnB,IAAM,WAAW,MAAM;AAC5B,YAAU,MAAM;AACd,UAAM,cAAc,SAAS;AAG7B,UAAM,eAAe,YAAY,aAAa,YAAY;AAC1D,QAAI,gBAAgB,CAAC,YAAY,aAAa,qBAAqB,GAAG;AACpE,kBAAY,aAAa,uBAAuB,YAAY;AAAA,IAC9D;AAGA,UAAM,aAAa,MAAM;AACvB,YAAM,aAAa,OAAO;AAAA,QACxB;AAAA,MACF,EAAE;AACF,YAAM,gBAAgB,YAAY,aAAa,qBAAqB;AAEpE,UAAI,YAAY;AAEd,oBAAY,aAAa,cAAc,MAAM;AAAA,MAC/C,WAAW,eAAe;AAExB,oBAAY,aAAa,cAAc,aAAa;AAAA,MACtD;AAAA,IACF;AAGA,eAAW;AAAA,EACb,GAAG,CAAC,CAAC;AACP;","names":[]}
1
+ {"version":3,"sources":["../../../src/hooks/useTheme.ts"],"sourcesContent":["import { useEffect, useState, useCallback, useRef } from 'react';\n\ntype ThemeMode = 'light' | 'dark' | 'system';\n\n/**\n * Hook para gerenciar temas com suporte a alternância manual e detecção automática do sistema\n * Este hook permite alternar entre temas light, dark e automático baseado nas preferências do sistema\n */\nexport const useTheme = () => {\n const [themeMode, setThemeMode] = useState<ThemeMode>('system');\n const [isDark, setIsDark] = useState(false);\n\n // Ref para manter o estado atual do tema de forma síncrona\n const themeModeRef = useRef<ThemeMode>('system');\n\n // Função para aplicar o tema baseado no modo selecionado\n const applyTheme = useCallback((mode: ThemeMode) => {\n const htmlElement = document.documentElement;\n const originalTheme = htmlElement.getAttribute('data-original-theme');\n\n if (mode === 'dark') {\n htmlElement.setAttribute('data-theme', 'dark');\n setIsDark(true);\n } else if (mode === 'light') {\n if (originalTheme) {\n htmlElement.setAttribute('data-theme', originalTheme);\n }\n setIsDark(false);\n } else if (mode === 'system') {\n const isSystemDark = window.matchMedia(\n '(prefers-color-scheme: dark)'\n ).matches;\n if (isSystemDark) {\n htmlElement.setAttribute('data-theme', 'dark');\n setIsDark(true);\n } else if (originalTheme) {\n htmlElement.setAttribute('data-theme', originalTheme);\n setIsDark(false);\n }\n }\n }, []);\n\n // Função para alternar entre os temas\n const toggleTheme = useCallback(() => {\n let newMode: ThemeMode;\n if (themeMode === 'light') {\n newMode = 'dark';\n } else if (themeMode === 'dark') {\n newMode = 'light';\n } else {\n // Se estiver em 'system', vai para 'dark'\n newMode = 'dark';\n }\n setThemeMode(newMode);\n themeModeRef.current = newMode;\n applyTheme(newMode);\n localStorage.setItem('theme-mode', newMode);\n }, [themeMode, applyTheme]);\n\n // Função para definir um tema específico\n const setTheme = useCallback(\n (mode: ThemeMode) => {\n setThemeMode(mode);\n themeModeRef.current = mode;\n applyTheme(mode);\n localStorage.setItem('theme-mode', mode);\n },\n [applyTheme]\n );\n\n useEffect(() => {\n const htmlElement = document.documentElement;\n\n // Salva o theme original do white label na primeira execução\n const currentTheme = htmlElement.getAttribute('data-theme');\n if (currentTheme && !htmlElement.getAttribute('data-original-theme')) {\n htmlElement.setAttribute('data-original-theme', currentTheme);\n }\n\n // Carrega o tema salvo no localStorage ou usa 'system' como padrão\n const savedThemeMode = localStorage.getItem('theme-mode') as ThemeMode;\n const initialMode = savedThemeMode || 'system';\n\n // Se não há tema salvo, persiste 'system' como padrão\n if (!savedThemeMode) {\n localStorage.setItem('theme-mode', 'system');\n }\n\n setThemeMode(initialMode);\n themeModeRef.current = initialMode;\n applyTheme(initialMode);\n\n // Listener para mudanças nas preferências do sistema (apenas quando mode é 'system')\n const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');\n const handleSystemThemeChange = () => {\n // Usa o ref para ter acesso ao estado atual de forma síncrona\n if (themeModeRef.current === 'system') {\n applyTheme('system');\n }\n };\n\n mediaQuery.addEventListener('change', handleSystemThemeChange);\n\n return () => {\n mediaQuery.removeEventListener('change', handleSystemThemeChange);\n };\n }, [applyTheme]);\n\n return {\n themeMode,\n isDark,\n toggleTheme,\n setTheme,\n };\n};\n"],"mappings":";AAAA,SAAS,WAAW,UAAU,aAAa,cAAc;AAQlD,IAAM,WAAW,MAAM;AAC5B,QAAM,CAAC,WAAW,YAAY,IAAI,SAAoB,QAAQ;AAC9D,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,KAAK;AAG1C,QAAM,eAAe,OAAkB,QAAQ;AAG/C,QAAM,aAAa,YAAY,CAAC,SAAoB;AAClD,UAAM,cAAc,SAAS;AAC7B,UAAM,gBAAgB,YAAY,aAAa,qBAAqB;AAEpE,QAAI,SAAS,QAAQ;AACnB,kBAAY,aAAa,cAAc,MAAM;AAC7C,gBAAU,IAAI;AAAA,IAChB,WAAW,SAAS,SAAS;AAC3B,UAAI,eAAe;AACjB,oBAAY,aAAa,cAAc,aAAa;AAAA,MACtD;AACA,gBAAU,KAAK;AAAA,IACjB,WAAW,SAAS,UAAU;AAC5B,YAAM,eAAe,OAAO;AAAA,QAC1B;AAAA,MACF,EAAE;AACF,UAAI,cAAc;AAChB,oBAAY,aAAa,cAAc,MAAM;AAC7C,kBAAU,IAAI;AAAA,MAChB,WAAW,eAAe;AACxB,oBAAY,aAAa,cAAc,aAAa;AACpD,kBAAU,KAAK;AAAA,MACjB;AAAA,IACF;AAAA,EACF,GAAG,CAAC,CAAC;AAGL,QAAM,cAAc,YAAY,MAAM;AACpC,QAAI;AACJ,QAAI,cAAc,SAAS;AACzB,gBAAU;AAAA,IACZ,WAAW,cAAc,QAAQ;AAC/B,gBAAU;AAAA,IACZ,OAAO;AAEL,gBAAU;AAAA,IACZ;AACA,iBAAa,OAAO;AACpB,iBAAa,UAAU;AACvB,eAAW,OAAO;AAClB,iBAAa,QAAQ,cAAc,OAAO;AAAA,EAC5C,GAAG,CAAC,WAAW,UAAU,CAAC;AAG1B,QAAM,WAAW;AAAA,IACf,CAAC,SAAoB;AACnB,mBAAa,IAAI;AACjB,mBAAa,UAAU;AACvB,iBAAW,IAAI;AACf,mBAAa,QAAQ,cAAc,IAAI;AAAA,IACzC;AAAA,IACA,CAAC,UAAU;AAAA,EACb;AAEA,YAAU,MAAM;AACd,UAAM,cAAc,SAAS;AAG7B,UAAM,eAAe,YAAY,aAAa,YAAY;AAC1D,QAAI,gBAAgB,CAAC,YAAY,aAAa,qBAAqB,GAAG;AACpE,kBAAY,aAAa,uBAAuB,YAAY;AAAA,IAC9D;AAGA,UAAM,iBAAiB,aAAa,QAAQ,YAAY;AACxD,UAAM,cAAc,kBAAkB;AAGtC,QAAI,CAAC,gBAAgB;AACnB,mBAAa,QAAQ,cAAc,QAAQ;AAAA,IAC7C;AAEA,iBAAa,WAAW;AACxB,iBAAa,UAAU;AACvB,eAAW,WAAW;AAGtB,UAAM,aAAa,OAAO,WAAW,8BAA8B;AACnE,UAAM,0BAA0B,MAAM;AAEpC,UAAI,aAAa,YAAY,UAAU;AACrC,mBAAW,QAAQ;AAAA,MACrB;AAAA,IACF;AAEA,eAAW,iBAAiB,UAAU,uBAAuB;AAE7D,WAAO,MAAM;AACX,iBAAW,oBAAoB,UAAU,uBAAuB;AAAA,IAClE;AAAA,EACF,GAAG,CAAC,UAAU,CAAC;AAEf,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;","names":[]}
package/dist/index.css CHANGED
@@ -584,6 +584,9 @@
584
584
  .z-50 {
585
585
  z-index: 50;
586
586
  }
587
+ .z-\[9999\] {
588
+ z-index: 9999;
589
+ }
587
590
  .container {
588
591
  width: 100%;
589
592
  @media (width >= 40rem) {
@@ -2767,6 +2770,9 @@
2767
2770
  .py-1 {
2768
2771
  padding-block: calc(var(--spacing) * 1);
2769
2772
  }
2773
+ .py-1\.5 {
2774
+ padding-block: calc(var(--spacing) * 1.5);
2775
+ }
2770
2776
  .py-2 {
2771
2777
  padding-block: calc(var(--spacing) * 2);
2772
2778
  }
@@ -4916,6 +4922,13 @@
4916
4922
  }
4917
4923
  }
4918
4924
  }
4925
+ .hover\:bg-gray-300 {
4926
+ &:hover {
4927
+ @media (hover: hover) {
4928
+ background-color: var(--color-gray-300);
4929
+ }
4930
+ }
4931
+ }
4919
4932
  .hover\:bg-green-600 {
4920
4933
  &:hover {
4921
4934
  @media (hover: hover) {
@@ -8117,6 +8130,11 @@
8117
8130
  --tw-ring-color: var(--color-indicator-info);
8118
8131
  }
8119
8132
  }
8133
+ .focus-visible\:ring-primary-500 {
8134
+ &:focus-visible {
8135
+ --tw-ring-color: var(--color-primary-500);
8136
+ }
8137
+ }
8120
8138
  .focus-visible\:ring-primary-600 {
8121
8139
  &:focus-visible {
8122
8140
  --tw-ring-color: var(--color-primary-600);
@@ -18253,6 +18271,30 @@
18253
18271
  grid-template-columns: repeat(2, minmax(0, 1fr));
18254
18272
  }
18255
18273
  }
18274
+ .dark\:border-gray-600 {
18275
+ @media (prefers-color-scheme: dark) {
18276
+ border-color: var(--color-gray-600);
18277
+ }
18278
+ }
18279
+ .dark\:bg-gray-700 {
18280
+ @media (prefers-color-scheme: dark) {
18281
+ background-color: var(--color-gray-700);
18282
+ }
18283
+ }
18284
+ .dark\:text-gray-300 {
18285
+ @media (prefers-color-scheme: dark) {
18286
+ color: var(--color-gray-300);
18287
+ }
18288
+ }
18289
+ .dark\:hover\:bg-gray-600 {
18290
+ @media (prefers-color-scheme: dark) {
18291
+ &:hover {
18292
+ @media (hover: hover) {
18293
+ background-color: var(--color-gray-600);
18294
+ }
18295
+ }
18296
+ }
18297
+ }
18256
18298
  .\[\&_tr\:first-child\]\:border-0 {
18257
18299
  & tr:first-child {
18258
18300
  border-style: var(--tw-border-style);