feat(FN-2172): add embedded dev server preview panel

- Add usePreviewEmbed hook to derive preview embed URLs and availability state
- Add PreviewIframe component with iframe load/error wiring and deduplicated error callbacks
- Integrate preview panel actions, fallback messaging, and embedded iframe rendering in DevServerView
- Expand test coverage for usePreviewEmbed, PreviewIframe, and DevServerView preview integration scenarios
This commit is contained in:
Fusion
2026-04-20 19:48:32 -07:00
committed by gsxdsm
parent 64f5c0862b
commit b0defea550
8 changed files with 918 additions and 152 deletions

View File

@@ -0,0 +1,73 @@
import { useCallback, useEffect, useMemo, useRef, useState, type RefObject } from "react";
export type EmbedStatus = "unknown" | "loading" | "embedded" | "blocked" | "error";
interface UsePreviewEmbedResult {
embedStatus: EmbedStatus;
iframeRef: RefObject<HTMLIFrameElement | null>;
handleIframeLoad: () => void;
handleIframeError: () => void;
resetEmbed: () => void;
isEmbedded: boolean;
isBlocked: boolean;
}
export function usePreviewEmbed(url: string | null): UsePreviewEmbedResult {
const iframeRef = useRef<HTMLIFrameElement | null>(null);
const [embedStatus, setEmbedStatus] = useState<EmbedStatus>("unknown");
useEffect(() => {
if (!url) {
setEmbedStatus("unknown");
return;
}
queueMicrotask(() => {
setEmbedStatus("loading");
});
}, [url]);
const handleIframeLoad = useCallback(() => {
const iframeEl = iframeRef.current;
if (!iframeEl) {
setEmbedStatus("embedded");
return;
}
try {
const frameHref = iframeEl.contentWindow?.location?.href;
if (frameHref === "about:blank" && iframeEl.src !== "about:blank") {
setEmbedStatus("blocked");
return;
}
} catch {
// Cross-origin access can throw; do not treat it as blocked.
}
setEmbedStatus("embedded");
}, []);
const handleIframeError = useCallback(() => {
setEmbedStatus("error");
}, []);
const resetEmbed = useCallback(() => {
setEmbedStatus("unknown");
}, []);
const isEmbedded = useMemo(() => embedStatus === "embedded", [embedStatus]);
const isBlocked = useMemo(
() => embedStatus === "blocked" || embedStatus === "error",
[embedStatus],
);
return {
embedStatus,
iframeRef,
handleIframeLoad,
handleIframeError,
resetEmbed,
isEmbedded,
isBlocked,
};
}