'use client'; import { useState } from 'react'; import { Archive, Download, FileDown, Film, Loader2, Monitor, Moon, NotebookText, Package, Settings, Sun, } from 'lucide-react'; import { Switch } from '@/components/ui/switch'; import { useI18n } from '@/lib/hooks/use-i18n'; import { useTheme } from '@/lib/hooks/use-theme'; import { useStageStore } from '@/lib/store'; import { useMediaGenerationStore } from '@/lib/store/media-generation'; import { useExportPPTX } from '@/lib/export/use-export-pptx'; import { useExportClassroom } from '@/lib/export/use-export-classroom'; import { isScriptExportReady, useExportScript } from '@/lib/export/use-export-script'; import { isVideoExportEnabled } from '@/lib/config/feature-flags'; import { useVideoRenderStore } from '@/lib/store/video-render'; import { CircularProgress } from '@/components/ui/circular-progress'; import { VideoExportDialog } from './video-export-dialog'; import { LanguageSwitcher } from '../language-switcher'; import { SettingsDialog } from '../settings'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { cn } from '@/lib/utils'; import type { StageMode } from '@/lib/types/stage'; interface HeaderControlsProps { readonly mode?: StageMode; readonly proModeActive?: boolean; readonly canEdit?: boolean; readonly onToggleEditMode?: () => void; readonly showGlobalControls?: boolean; readonly showCourseActions?: boolean; /** * `default` — the chunky h-9 pill used in the playback Stage Header. * `compact` — slightly tighter padding for embedding in CommandBar's * right slot (Pro mode chrome already eats height, so the pill backs * off ring weight / blur to keep the CommandBar quiet). */ readonly variant?: 'default' | 'compact'; } /** * Stage-level global controls: language picker, theme picker, settings * modal trigger, and the Pro Switch. Extracted out of `Header` so the * Pro mode CommandBar can absorb the same affordances and the playback * Header doesn't need to stay mounted just to host them — Pro mode * therefore lands on a single top-chrome bar instead of stacking the * Stage Header above the EditShell CommandBar. * * Only one instance is ever mounted at a time (Stage renders Header * for playback and EditShell.CommandBar's trailing slot for edit, but * never both), so dropdown / dialog state and refs stay co-located * here without cross-instance leakage. */ export function HeaderControls({ mode, proModeActive, canEdit, onToggleEditMode, showGlobalControls = true, showCourseActions = true, variant = 'default', }: HeaderControlsProps) { const { t } = useI18n(); const { theme, setTheme } = useTheme(); const [settingsOpen, setSettingsOpen] = useState(false); const [videoDialogOpen, setVideoDialogOpen] = useState(false); // Export plumbing — uses the stage / media task stores to check // readiness, then hands off to the export hooks. Available in both // playback and edit chrome so the icon's screen position is stable // across mode swaps (was previously in `Header` only, missing from // CommandBar's right cluster). const scenes = useStageStore((s) => s.scenes); const generatingOutlines = useStageStore((s) => s.generatingOutlines); const failedOutlines = useStageStore((s) => s.failedOutlines); const mediaTasks = useMediaGenerationStore((s) => s.tasks); const { exporting: isExporting, exportPPTX, exportResourcePack } = useExportPPTX(); const { exporting: isExportingZip, exportClassroomZip } = useExportClassroom(); const { exporting: isExportingScript, exportScriptDocx, exportScriptMd } = useExportScript(); const videoExportEnabled = isVideoExportEnabled(); // Video render lives in a global store so its progress ring stays on the // export button even after the menu closes / scenes switch mid-render. const videoRendering = useVideoRenderStore( (s) => s.status === 'compiling' || s.status === 'rendering', ); const videoRenderPercent = useVideoRenderStore((s) => s.percent); const [exportMenuOpen, setExportMenuOpen] = useState(false); // Keep the original full-generation gate for the export menu. Script files // are text-only, but the latest review confirmed that this menu intentionally // stays unavailable until all media tasks have completed or failed. const canExport = isScriptExportReady({ scenes, generatingOutlines, failedOutlines }, mediaTasks); const exportLabel = canExport ? t('export.pptx') : t('share.notReady'); const compact = variant === 'compact'; const proChecked = proModeActive ?? mode === 'edit'; if (!showGlobalControls && !showCourseActions) { return onToggleEditMode ? (
{t('stage.proMode')}
) : null; } // Self-contained spacing so the control cluster is identical regardless of // host. The playback Header (`gap-4`) and the edit CommandBar's trailing // slot (`gap-2`) would otherwise impose different inter-control spacing on // these fragment children, making the pill/switch/export cluster visibly // shift width and position across the mode swap. A fixed internal gap keeps // the cluster pixel-stable; both hosts pad to `px-8`, so the right edge // anchors identically too. return (
{/* Language — Radix DropdownMenu so its menu portals to body and never gets clipped by an ancestor's overflow-hidden. */} {/* Theme — same Portal-backed DropdownMenu pattern. Non-modal keeps Radix from body scroll-locking a fixed-height classroom layout. */} setTheme('light')} className={cn( 'cursor-pointer gap-2', theme === 'light' && 'bg-purple-50 dark:bg-purple-900/20 text-purple-600 dark:text-purple-400', )} > {t('settings.themeOptions.light')} setTheme('dark')} className={cn( 'cursor-pointer gap-2', theme === 'dark' && 'bg-purple-50 dark:bg-purple-900/20 text-purple-600 dark:text-purple-400', )} > {t('settings.themeOptions.dark')} setTheme('system')} className={cn( 'cursor-pointer gap-2', theme === 'system' && 'bg-purple-50 dark:bg-purple-900/20 text-purple-600 dark:text-purple-400', )} > {t('settings.themeOptions.system')} {/* Settings */}
{/* Pro Switch — toggle property: on/off both clickable, not a one-way "Done" button. Disabled only when the current scene can't be entered (pending/generating/etc.). Fades in with its host bar on the mode swap (no cross-bar layoutId morph: the playback Header and edit CommandBar have different left-side widths, so morphing made the pill visibly drift). */} {onToggleEditMode && ( )} {/* Export / Download — lives to the right of the Pro Switch. Not a settings function so it does not belong inside the settings pill; kept as a separate sibling sitting between the Pro Switch and the right edge of the chrome. */} {t('export.pptx')}
{t('export.resourcePack')}
{t('export.resourcePackDesc')}
{t('export.classroomZip')}
{t('export.classroomZipDesc')}
{videoExportEnabled && ( setVideoDialogOpen(true)} className="cursor-pointer gap-2.5 border-t border-gray-200 dark:border-gray-700" title={canExport ? undefined : t('export.mediaPending')} >
{t('export.video')}
{t('export.videoDesc')}
)}
{videoExportEnabled && ( )}
); }