diff --git a/apps/web/src/components/editor/preview-panel.tsx b/apps/web/src/components/editor/preview-panel.tsx index 2ecee391..b20b0eed 100644 --- a/apps/web/src/components/editor/preview-panel.tsx +++ b/apps/web/src/components/editor/preview-panel.tsx @@ -17,7 +17,7 @@ import { DropdownMenuSeparator, } from "@/components/ui/dropdown-menu"; import { Play, Pause, Expand } from "lucide-react"; -import { useState, useRef, useEffect } from "react"; +import { useState, useRef, useEffect, useCallback } from "react"; import { cn } from "@/lib/utils"; import { formatTimeCode } from "@/lib/time"; import { FONT_CLASS_MAP } from "@/lib/font-config"; @@ -41,6 +41,7 @@ export function PreviewPanel() { width: 0, height: 0, }); + const [isExpanded, setIsExpanded] = useState(false); const { activeProject } = useProjectStore(); // Calculate optimal preview size that fits in container while maintaining aspect ratio @@ -48,32 +49,41 @@ export function PreviewPanel() { const updatePreviewSize = () => { if (!containerRef.current) return; - const container = containerRef.current.getBoundingClientRect(); - const computedStyle = getComputedStyle(containerRef.current); + let container; + let availableWidth, availableHeight; - // Get padding values - const paddingTop = parseFloat(computedStyle.paddingTop); - const paddingBottom = parseFloat(computedStyle.paddingBottom); - const paddingLeft = parseFloat(computedStyle.paddingLeft); - const paddingRight = parseFloat(computedStyle.paddingRight); + if (isExpanded) { + // Use full viewport when expanded + availableWidth = window.innerWidth; + availableHeight = window.innerHeight; + } else { + container = containerRef.current.getBoundingClientRect(); + const computedStyle = getComputedStyle(containerRef.current); - // Get gap value (gap-4 = 1rem = 16px) - const gap = parseFloat(computedStyle.gap) || 16; + // Get padding values + const paddingTop = parseFloat(computedStyle.paddingTop); + const paddingBottom = parseFloat(computedStyle.paddingBottom); + const paddingLeft = parseFloat(computedStyle.paddingLeft); + const paddingRight = parseFloat(computedStyle.paddingRight); - // Get toolbar height if it exists - const toolbar = containerRef.current.querySelector("[data-toolbar]"); - const toolbarHeight = toolbar - ? toolbar.getBoundingClientRect().height - : 0; + // Get gap value (gap-4 = 1rem = 16px) + const gap = parseFloat(computedStyle.gap) || 16; - // Calculate available space after accounting for padding, gap, and toolbar - const availableWidth = container.width - paddingLeft - paddingRight; - const availableHeight = - container.height - - paddingTop - - paddingBottom - - toolbarHeight - - (toolbarHeight > 0 ? gap : 0); + // Get toolbar height if it exists + const toolbar = containerRef.current.querySelector("[data-toolbar]"); + const toolbarHeight = toolbar + ? toolbar.getBoundingClientRect().height + : 0; + + // Calculate available space after accounting for padding, gap, and toolbar + availableWidth = container.width - paddingLeft - paddingRight; + availableHeight = + container.height - + paddingTop - + paddingBottom - + toolbarHeight - + (toolbarHeight > 0 ? gap : 0); + } const targetRatio = canvasSize.width / canvasSize.height; const containerRatio = availableWidth / availableHeight; @@ -82,11 +92,11 @@ export function PreviewPanel() { if (containerRatio > targetRatio) { // Container is wider - constrain by height - height = availableHeight; + height = availableHeight * (isExpanded ? 0.9 : 1); // Leave some margin when expanded width = height * targetRatio; } else { // Container is taller - constrain by width - width = availableWidth; + width = availableWidth * (isExpanded ? 0.9 : 1); // Leave some margin when expanded height = width / targetRatio; } @@ -100,8 +110,44 @@ export function PreviewPanel() { resizeObserver.observe(containerRef.current); } - return () => resizeObserver.disconnect(); - }, [canvasSize.width, canvasSize.height]); + // Listen for window resize when expanded + if (isExpanded) { + window.addEventListener("resize", updatePreviewSize); + } + + return () => { + resizeObserver.disconnect(); + if (isExpanded) { + window.removeEventListener("resize", updatePreviewSize); + } + }; + }, [canvasSize.width, canvasSize.height, isExpanded]); + + // Handle escape key to exit expanded mode + useEffect(() => { + const handleEscapeKey = (event: KeyboardEvent) => { + if (event.key === "Escape" && isExpanded) { + setIsExpanded(false); + } + }; + + if (isExpanded) { + document.addEventListener("keydown", handleEscapeKey); + // Prevent body scroll when expanded + document.body.style.overflow = "hidden"; + } else { + document.body.style.overflow = ""; + } + + return () => { + document.removeEventListener("keydown", handleEscapeKey); + document.body.style.overflow = ""; + }; + }, [isExpanded]); + + const toggleExpanded = useCallback(() => { + setIsExpanded((prev) => !prev); + }, []); // Get active elements at current time const getActiveElements = (): ActiveElement[] => { @@ -146,7 +192,7 @@ export function PreviewPanel() { element.type === "media" && mediaItem && (mediaItem.type === "video" || mediaItem.type === "image") && - element.mediaId !== "test" // Exclude test elements + element.mediaId !== "test", // Exclude test elements ); }; @@ -338,55 +384,114 @@ export function PreviewPanel() { }; return ( -
{formatTimeCode( currentTime, "HH:MM:SS:FF", - activeProject?.fps || 30 + activeProject?.fps || 30, )} / @@ -432,7 +537,7 @@ function PreviewToolbar({ hasAnyElements }: { hasAnyElements: boolean }) { {formatTimeCode( getTotalDuration(), "HH:MM:SS:FF", - activeProject?.fps || 30 + activeProject?.fps || 30, )}
@@ -476,7 +581,7 @@ function PreviewToolbar({ hasAnyElements }: { hasAnyElements: boolean }) { onClick={() => handlePresetSelect(preset)} className={cn( "text-xs", - currentPreset?.name === preset.name && "font-semibold" + currentPreset?.name === preset.name && "font-semibold", )} > {preset.name} @@ -488,6 +593,8 @@ function PreviewToolbar({ hasAnyElements }: { hasAnyElements: boolean }) { variant="text" size="icon" className="!size-4 text-muted-foreground" + onClick={onToggleExpanded} + title={isExpanded ? "Exit fullscreen (Esc)" : "Enter fullscreen"} >