"use client" import * as React from "react" import { CMD_K_FORWARD_TYPE } from "@/app/(app)/(create)/components/action-menu" import { CreateDevtools } from "@/app/(app)/(create)/components/create-devtools" import { REDO_FORWARD_TYPE, UNDO_FORWARD_TYPE, } from "@/app/(app)/(create)/components/history-buttons" import { DARK_MODE_FORWARD_TYPE } from "@/app/(app)/(create)/components/mode-switcher" import { usePreviewOverrideValue } from "@/app/(app)/(create)/components/preview-override" import { PreviewSwitcher } from "@/app/(app)/(create)/components/preview-switcher" import { RANDOMIZE_FORWARD_TYPE } from "@/app/(app)/(create)/components/random-button" import { sendToIframe } from "@/app/(app)/(create)/hooks/use-iframe-sync" import { OPEN_PRESET_FORWARD_TYPE } from "@/app/(app)/(create)/hooks/use-open-preset" import { RESET_FORWARD_TYPE } from "@/app/(app)/(create)/hooks/use-reset" import { serializeDesignSystemSearchParams, useDesignSystemSearchParams, } from "@/app/(app)/(create)/lib/search-params" // Hoisted — avoids recreating on every message event. (js-hoist-regexp) const MAC_REGEX = /Mac|iPhone|iPad|iPod/ function isSameParams( a: ReturnType[0] | null, b: ReturnType[0] ) { if (!a) { return false } return Object.keys(b).every( (key) => a[key as keyof typeof b] === b[key as keyof typeof b] ) } export function Preview() { const [params] = useDesignSystemSearchParams() const override = usePreviewOverrideValue() const iframeRef = React.useRef(null) const lastSentParamsRef = React.useRef(null) // Hover previews overlay the committed params without touching the URL — // clearing the override re-sends the committed params, reverting the preview. const mergedParams = React.useMemo( () => (override ? { ...params, ...override } : params), [params, override] ) React.useEffect(() => { const iframe = iframeRef.current if (!iframe) { return } const sendParams = () => { sendToIframe(iframe, "design-system-params", mergedParams) lastSentParamsRef.current = mergedParams } // Skip content-identical re-sends (e.g. an override matching the committed // params) — each message triggers a full param sync in the iframe. if ( iframe.contentWindow && !isSameParams(lastSentParamsRef.current, mergedParams) ) { sendParams() } iframe.addEventListener("load", sendParams) return () => { iframe.removeEventListener("load", sendParams) } }, [mergedParams]) React.useEffect(() => { const handleMessage = (event: MessageEvent) => { const iframeWindow = iframeRef.current?.contentWindow if ( !iframeWindow || event.origin !== window.location.origin || event.source !== iframeWindow || !event.data || typeof event.data !== "object" ) { return } const type = event.data.type if (type === CMD_K_FORWARD_TYPE) { const isMac = MAC_REGEX.test(navigator.userAgent) document.dispatchEvent( new KeyboardEvent("keydown", { key: event.data.key || "k", metaKey: isMac, ctrlKey: !isMac, bubbles: true, cancelable: true, }) ) } else if (type === RANDOMIZE_FORWARD_TYPE) { document.dispatchEvent( new KeyboardEvent("keydown", { key: event.data.key || "r", bubbles: true, cancelable: true, }) ) } else if (type === OPEN_PRESET_FORWARD_TYPE) { document.dispatchEvent( new KeyboardEvent("keydown", { key: event.data.key || "o", bubbles: true, cancelable: true, }) ) } else if (type === UNDO_FORWARD_TYPE) { const isMac = MAC_REGEX.test(navigator.userAgent) document.dispatchEvent( new KeyboardEvent("keydown", { key: "z", metaKey: isMac, ctrlKey: !isMac, bubbles: true, cancelable: true, }) ) } else if (type === REDO_FORWARD_TYPE) { const isMac = MAC_REGEX.test(navigator.userAgent) document.dispatchEvent( new KeyboardEvent("keydown", { key: "z", shiftKey: true, metaKey: isMac, ctrlKey: !isMac, bubbles: true, cancelable: true, }) ) } else if (type === RESET_FORWARD_TYPE) { document.dispatchEvent( new KeyboardEvent("keydown", { key: "R", shiftKey: true, bubbles: true, cancelable: true, }) ) } else if (type === DARK_MODE_FORWARD_TYPE) { document.dispatchEvent( new KeyboardEvent("keydown", { key: event.data.key || "d", bubbles: true, cancelable: true, }) ) } } window.addEventListener("message", handleMessage) return () => { window.removeEventListener("message", handleMessage) } }, []) const iframeSrc = React.useMemo(() => { // The iframe src needs to include the serialized design system params // for the initial load, but not be reactive to them as it would cause // full-iframe reloads on every param change (flashes & loss of state). // Further updates of the search params will be sent to the iframe // via a postMessage channel, for it to sync its own history onto the host's. return serializeDesignSystemSearchParams( `/preview/${params.base}/${params.item}`, params ) // eslint-disable-next-line react-hooks/exhaustive-deps }, [params.base, params.item]) return (