"use client"; import cx from "classnames"; import React, { FunctionComponent, ReactNode, useEffect, useId, useRef, useState, } from "react"; import reactElementToJSXString from "react-element-to-jsx-string"; import breakpoints from "@/styles/export/breakpoint"; import { Button } from "../Button"; import { Card } from "../Card"; import { Dropdown, DropdownItem } from "../Dropdown"; import { Icon } from "../Icon"; import CodeExample from "./CodeExample"; import { getElementDisplayName } from "./getElementDisplayName"; import PreviewTitleBar from "./PreviewTitleBar"; import "./styles/style.scss"; const CLASS_ROOT = "preview"; // Helper function to generate JSX string from React elements (client-side fallback) const generateJSXString = ( children: ReactNode, options?: Record, ): string => { const defaultOptions = { showDefaultProps: false, showFunctions: true, functionValue: (fn: any) => fn.name || "Function", displayName: getElementDisplayName, filterProps: ["mdxType", "originalType"], useBooleanShorthandSyntax: false, sortProps: false, ...options, }; try { if (!children) { return "// No children provided"; } if (Array.isArray(children)) { return children .filter(Boolean) // Filter out null/undefined children .map((child) => { try { // Additional validation for React elements if (!React.isValidElement(child)) { return "// Invalid React element"; } return reactElementToJSXString(child as any, defaultOptions); } catch (error) { console.warn("Failed to generate JSX string for child:", error); return "// Unable to generate JSX for this element"; } }) .join("\n"); } // Additional validation for React elements if (!React.isValidElement(children)) { return "// Invalid React element"; } return reactElementToJSXString(children as any, defaultOptions); } catch (error) { console.warn("Failed to generate JSX string:", error); return "// Unable to generate JSX code"; } }; interface PreviewBackground { value: string; label: string; } interface PreviewRenderContext { bgTheme: string; bgThemeValue: string; themeClass: string; isDarkMode: boolean; } interface PreviewBreakpoint { deviceLabel?: string; frameClassName?: string; key: string; label: string; height: number | null; width: number | null; } const PREVIEW_BREAKPOINTS: PreviewBreakpoint[] = [ { key: "auto", label: "Auto", width: null, height: null, deviceLabel: undefined, frameClassName: undefined, }, ...Object.entries(breakpoints).map(([key, value]) => { const tokenWidth = Number.parseInt(value, 10); // `xs` is 0 in tokens (mobile-first min-width), but preview needs a real viewport size. const width = key === "xs" ? 320 : tokenWidth; const height = key === "xs" ? 640 : key === "sm" ? 812 : key === "md" ? 1024 : key === "lg" ? 900 : 860; const frameClassName = key === "xs" || key === "sm" ? "isPhone" : key === "md" ? "isTablet" : "isDesktop"; const deviceLabel = key === "xs" ? "Phone S" : key === "sm" ? "Phone L" : key === "md" ? "Tablet" : key === "lg" ? "Laptop" : "Desktop"; return { deviceLabel, frameClassName, key, height, label: `${deviceLabel} (${width} x ${height})`, width, }; }), ]; interface PreviewViewportFrameProps { className?: string; frameClassName?: string; html: string; height?: number; } const PreviewViewportFrame: React.FC = ({ className, frameClassName, height = 1, html, }) => { const iframeRef = useRef(null); const [isIframeScriptsReady, setIsIframeScriptsReady] = useState(false); useEffect(() => { const iframe = iframeRef.current; if (!iframe) return; const doc = iframe.contentDocument; if (!doc) return; doc.open(); doc.write( `
`, ); doc.close(); // Mirror parent styles so components inside iframe have same look. const stylesToClone = document.querySelectorAll( 'link[rel="stylesheet"], style', ); stylesToClone.forEach((styleNode) => { const clonedNode = styleNode.cloneNode(true); doc.head.appendChild(clonedNode); }); doc.body.style.margin = "0"; doc.body.style.padding = "0"; doc.body.style.background = "transparent"; doc.body.style.overflow = "auto"; doc.documentElement.style.margin = "0"; doc.documentElement.style.padding = "0"; doc.documentElement.style.height = "100%"; doc.documentElement.style.overflow = "auto"; doc.body.style.height = "100%"; iframe.setAttribute("scrolling", "auto"); const root = doc.getElementById("preview-iframe-root"); if (!root) return; const script = doc.createElement("script"); script.src = "/ods-preview-scripts.js"; script.addEventListener("load", () => { setIsIframeScriptsReady(true); }); doc.head.appendChild(script); root.style.width = "100%"; root.style.minHeight = "100%"; root.style.boxSizing = "border-box"; root.style.overflow = "visible"; }, []); useEffect(() => { const root = iframeRef.current?.contentDocument?.getElementById( "preview-iframe-root", ); if (!root) return; root.innerHTML = html; }, [html]); useEffect(() => { const iframe = iframeRef.current; const doc = iframe?.contentDocument; if (!doc || !isIframeScriptsReady) return; const frameWindow = doc.defaultView as | (Window & { ODS?: { initModulesWithin?: (container: ParentNode) => void; }; }) | null; const init = () => { frameWindow?.ODS?.initModulesWithin?.(doc); }; const rafId = frameWindow?.requestAnimationFrame(init); const timeoutId = frameWindow?.setTimeout(init, 0); return () => { if (typeof rafId === "number" && frameWindow) { frameWindow.cancelAnimationFrame(rafId); } if (typeof timeoutId === "number" && frameWindow) { frameWindow.clearTimeout(timeoutId); } }; }, [html, isIframeScriptsReady]); return (