| import { Alert02Icon, Globe02Icon } from "@hugeicons/core-free-icons"; |
| import { HugeiconsIcon } from "@hugeicons/react"; |
| import { forwardRef, useImperativeHandle, useRef, useState } from "react"; |
| import { |
| PreviewAddressBar, |
| type PreviewAddressBarHandle, |
| } from "./PreviewAddressBar"; |
|
|
| export type PreviewPaneHandle = { |
| reload: () => void; |
| focusAddressBar: () => void; |
| getUrl: () => string; |
| }; |
|
|
| type Props = { |
| url: string; |
| visible: boolean; |
| onUrlChange: (url: string) => void; |
| }; |
|
|
| export const PreviewPane = forwardRef<PreviewPaneHandle, Props>( |
| function PreviewPane({ url, visible, onUrlChange }, ref) { |
| |
| |
| |
| const [nonce, setNonce] = useState(0); |
| const addressRef = useRef<PreviewAddressBarHandle>(null); |
|
|
| useImperativeHandle( |
| ref, |
| () => ({ |
| reload: () => setNonce((n) => n + 1), |
| focusAddressBar: () => addressRef.current?.focus(), |
| getUrl: () => url, |
| }), |
| [url], |
| ); |
|
|
| const showXfoHint = url ? !isLocalUrl(url) : false; |
|
|
| return ( |
| <div |
| className="flex h-full w-full flex-col overflow-hidden rounded-md border border-border/60 bg-background" |
| style={{ |
| visibility: visible ? "visible" : "hidden", |
| pointerEvents: visible ? "auto" : "none", |
| }} |
| > |
| <PreviewAddressBar |
| ref={addressRef} |
| url={url} |
| onSubmit={onUrlChange} |
| onReload={() => setNonce((n) => n + 1)} |
| /> |
| {showXfoHint ? ( |
| <div className="flex h-7 shrink-0 items-center gap-1.5 border-b border-border/60 bg-amber-500/8 px-3 text-[11px] text-amber-600 dark:text-amber-400"> |
| <HugeiconsIcon |
| icon={Alert02Icon} |
| size={12} |
| strokeWidth={1.75} |
| className="shrink-0" |
| /> |
| <span className="truncate"> |
| Many public sites refuse to embed (X-Frame-Options). If the page |
| is blank, open it externally. |
| </span> |
| </div> |
| ) : null} |
| <div |
| className={ |
| url |
| ? "relative min-h-0 flex-1 bg-white" |
| : "relative min-h-0 flex-1 bg-background" |
| } |
| > |
| {url ? ( |
| <iframe |
| key={`${url}#${nonce}`} |
| src={url} |
| title="Preview" |
| className="h-full w-full border-0" |
| allow="clipboard-read; clipboard-write; fullscreen" |
| /> |
| ) : ( |
| <EmptyState /> |
| )} |
| </div> |
| </div> |
| ); |
| }, |
| ); |
|
|
| function EmptyState() { |
| return ( |
| <div className="flex h-full w-full flex-col items-center justify-center gap-4 px-6 text-center"> |
| <div className="flex size-12 items-center justify-center rounded-2xl border border-border/60 bg-card text-muted-foreground"> |
| <HugeiconsIcon icon={Globe02Icon} size={20} strokeWidth={1.5} /> |
| </div> |
| <div className="space-y-1.5"> |
| <p className="text-sm font-medium text-foreground"> |
| Nothing to preview yet |
| </p> |
| <p className="max-w-sm text-xs leading-relaxed text-muted-foreground"> |
| Type a URL above, or open the{" "} |
| <span className="rounded bg-muted px-1 py-0.5 font-mono text-[10.5px]"> |
| Ports |
| </span>{" "} |
| dropdown to jump straight to your running dev server. Public sites |
| often block embedding — open them in your browser via the link icon |
| if you see a blank page. |
| </p> |
| </div> |
| </div> |
| ); |
| } |
|
|
| function isLocalUrl(url: string): boolean { |
| try { |
| const u = new URL(url); |
| const h = u.hostname; |
| return ( |
| h === "localhost" || |
| h === "127.0.0.1" || |
| h === "0.0.0.0" || |
| h === "[::1]" || |
| h.endsWith(".localhost") |
| ); |
| } catch { |
| return false; |
| } |
| } |
|
|