snapshot of https://github.com/shadcn-ui/ui.git (main, 2026-09-15; blobs > 131072B omitted)
This commit is contained in:
197
apps/v4/app/(app)/(create)/components/preview.tsx
Normal file
197
apps/v4/app/(app)/(create)/components/preview.tsx
Normal file
@@ -0,0 +1,197 @@
|
||||
"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<typeof useDesignSystemSearchParams>[0] | null,
|
||||
b: ReturnType<typeof useDesignSystemSearchParams>[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<HTMLIFrameElement>(null)
|
||||
const lastSentParamsRef = React.useRef<typeof params | null>(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 (
|
||||
<div className="relative flex flex-1 flex-col justify-center overflow-hidden rounded-2xl ring ring-foreground/10 md:ring-muted dark:ring-foreground/10">
|
||||
<div className="relative z-0 mx-auto flex w-full flex-1 flex-col overflow-hidden">
|
||||
<div className="absolute inset-0 bg-muted dark:bg-muted/30" />
|
||||
<iframe
|
||||
key={params.base + params.item}
|
||||
ref={iframeRef}
|
||||
src={iframeSrc}
|
||||
className="z-10 size-full flex-1"
|
||||
title="Preview"
|
||||
/>
|
||||
</div>
|
||||
<CreateDevtools />
|
||||
<PreviewSwitcher />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user