'use client'; import React, { useState, useEffect, useRef, useCallback, useMemo, forwardRef, useImperativeHandle } from 'react'; import { VirtualServer } from '@/lib/preview/virtual-server'; import { CompiledProject, PreviewMessage, FocusContextPayload, PreviewHostMessage } from '@/lib/preview/types'; import { vfs } from '@/lib/vfs'; import { Button } from '@/components/ui/button'; import { RefreshCw, Smartphone, Tablet, Monitor, ChevronLeft, ChevronRight, Home, Eye, Crosshair, Camera, Loader2, Maximize, Minimize, LayoutGrid, } from 'lucide-react'; import { PanelHeader } from '@/components/ui/panel'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { cn, logger } from '@/lib/utils'; import { captureIframeScreenshot } from '@/lib/utils/screenshot'; import type { ProjectRuntime } from '@/lib/vfs/types'; import type { PlacementResult, PlacementBlockInfo } from '@/lib/preview/types'; import { pushRuntimeError, clearRuntimeErrors } from '@/lib/preview/runtime-errors'; import { PalettePanel } from '@/components/semantic-blocks/palette-panel'; import type { SemanticBlock } from '@/lib/semantic-blocks/types'; import { useWorkspaceStore } from '@/lib/stores/workspace'; export interface MultipagePreviewHandle { captureScreenshot: (waitForContent?: boolean) => Promise; startBlockDrag: (block: PlacementBlockInfo) => void; getActivePath: () => string; removePlaceholder: (placementId: string) => void; } interface MultipagePreviewProps { projectId: string; refreshTrigger?: number; onFocusSelection?: (selection: FocusContextPayload | null) => void; hasFocusTarget?: boolean; onClose?: () => void; deploymentId?: string | null; onCaptureScreenshot?: (screenshot: string) => void; entryPoint?: string; runtime?: ProjectRuntime; onFullscreen?: () => void; isFullscreen?: boolean; placementActive?: boolean; onPlacementToggle?: () => void; onPlacementComplete?: (payload: PlacementResult) => void; } type DeviceSize = 'mobile' | 'tablet' | 'desktop' | 'responsive'; const DEVICE_SIZES: Record = { mobile: { width: '375px', height: '100%', maxHeight: '667px' }, tablet: { width: '768px', height: '100%', maxHeight: '1024px' }, desktop: { width: '100%', height: '100%', maxHeight: '900px', maxWidth: '1440px' }, responsive: { width: '100%', height: '100%' } }; function generatePlacementScript(): string { return ` `; const placementScript = generatePlacementScript(); const injectedScripts = navigationScript + placementScript; if (processedHtml.includes('')) { processedHtml = processedHtml.replace('', injectedScripts + ''); } else { processedHtml += injectedScripts; } // Expose the complete blob-URL map to the iframe so the runtime fetch/XHR // interceptor can resolve any VFS path (e.g. fetch('/components/nav.html')), // not just the partial map baked into the page during compilation. const vfsMapJson = JSON.stringify(Object.fromEntries(projectToUse.blobUrls)).replace(/window.__oswVfsBlobUrls = ${vfsMapJson};`; if (processedHtml.includes('')) { processedHtml = processedHtml.replace('', '' + vfsMapScript); } else { processedHtml = vfsMapScript + processedHtml; } // Clear stale runtime errors before loading new content — // only errors from this compilation should be in the buffer. clearRuntimeErrors(); iframeRef.current.srcdoc = processedHtml; setActivePath(normalizedPath); activePathRef.current = normalizedPath; setHistoryIndex(currentIndex => { setNavigationHistory(currentHistory => { const newHistory = [...currentHistory.slice(0, currentIndex + 1), normalizedPath]; return newHistory; }); return currentIndex + 1; }); }; const handleNavigation = useCallback((path: string) => { loadPage(path); }, [compiledProject]); const handleBack = () => { if (historyIndex > 0) { const newIndex = historyIndex - 1; setHistoryIndex(newIndex); loadPage(navigationHistory[newIndex]); } }; const handleForward = () => { if (historyIndex < navigationHistory.length - 1) { const newIndex = historyIndex + 1; setHistoryIndex(newIndex); loadPage(navigationHistory[newIndex]); } }; const handleHome = () => { loadPage('/'); }; const handleRefresh = () => { compileAndLoad(true, false); }; useEffect(() => { const handleMessage = (event: MessageEvent) => { // Only handle messages from our own iframe if (iframeRef.current && event.source !== iframeRef.current.contentWindow) { return; } const data = event.data; if (!data || typeof data !== 'object') { return; } if (data.type === 'console') { window.dispatchEvent(new CustomEvent('previewConsole', { detail: { level: data.level, args: data.args }, })); if (data.level === 'error') { pushRuntimeError(data.args.join(' ')); } return; } if (data.type === 'navigate' && data.path) { handleNavigation(data.path); return; } if (data.type === 'selector-selection' && data.payload) { setSelectorActive(false); onFocusSelection?.(data.payload); return; } if (data.type === 'selector-cancelled') { setSelectorActive(false); return; } if (data.type === 'placement-complete' && data.payload) { setPaletteVisible(true); onPlacementComplete?.(data.payload); return; } if (data.type === 'placement-cancelled') { setDraggingBlock(null); setPaletteVisible(true); return; } if (data.type === 'iframe-click') { const ps = paletteStateRef.current; if (ps.localPaletteOpen && ps.paletteVisible && !ps.draggingBlock) { setLocalPaletteOpen(false); setTimeout(() => onPlacementToggle?.(), 0); } return; } }; window.addEventListener('message', handleMessage); return () => { window.removeEventListener('message', handleMessage); }; }, [handleNavigation, onFocusSelection, onPlacementComplete]); useEffect(() => { return () => { if (serverRef.current) { serverRef.current.cleanupBlobUrls(); } }; }, []); useEffect(() => { if (placementActive && selectorActive) { setSelectorActive(false); } }, [placementActive, selectorActive]); useEffect(() => { if (!draggingBlock) return; const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape') { postMessageToIframe({ type: 'placement-cancel' }); setDraggingBlock(null); setPaletteVisible(true); } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [draggingBlock, postMessageToIframe]); if (loading) { return (

Compiling project...

); } if (error) { return (

Error

{error}

); } return (
{/* Mobile Layout - Single row with navigation and page selector */}
{onCaptureScreenshot && ( )}
{/* Page selector takes remaining space */} {compiledProject && compiledProject.routes.length > 1 && ( )}
{/* Desktop Layout - Single row */}
{onCaptureScreenshot && ( )}
{activePath}
{compiledProject && compiledProject.routes.length > 1 && ( )}
{isFullscreen ? ( ) : onFullscreen ? ( ) : null}
{/* Preview Frame */}
{ if (localPaletteOpen && paletteVisible && !draggingBlock) { setLocalPaletteOpen(false); setTimeout(() => onPlacementToggle?.(), 0); } }} > { setLocalPaletteOpen(false); setTimeout(() => onPlacementToggle?.(), 0); }} collapsed={!localPaletteOpen || !paletteVisible} />