"use client"
import type { ComponentProps, ReactNode } from "react"
import { createContext, useContext, useEffect, useState } from "react"
import { ChevronDownIcon } from "lucide-react"
import { cn } from "@/lib/utils"
import { Button } from "@/registry/new-york/ui/button"
import {
Disclosure,
DisclosureHeader,
DisclosurePanel,
} from "@/registry/new-york/ui/disclosure"
import { Input } from "@/registry/new-york/ui/input"
import { Tooltip, TooltipTrigger } from "@/registry/new-york/ui/tooltip"
export type WebPreviewContextValue = {
url: string
setUrl: (url: string) => void
consoleOpen: boolean
setConsoleOpen: (open: boolean) => void
}
const WebPreviewContext = createContext<WebPreviewContextValue | null>(null)
const useWebPreview = () => {
const context = useContext(WebPreviewContext)
if (!context) {
throw new Error("WebPreview components must be used within a WebPreview")
}
return context
}
export type WebPreviewProps = ComponentProps<"div"> & {
defaultUrl?: string
onUrlChange?: (url: string) => void
}
export const WebPreview = ({
className,
children,
defaultUrl = "",
onUrlChange,
...props
}: WebPreviewProps) => {
const [url, setUrl] = useState(defaultUrl)
const [consoleOpen, setConsoleOpen] = useState(false)
const handleUrlChange = (newUrl: string) => {
setUrl(newUrl)
onUrlChange?.(newUrl)
}
const contextValue: WebPreviewContextValue = {
url,
setUrl: handleUrlChange,
consoleOpen,
setConsoleOpen,
}
return (
<WebPreviewContext.Provider value={contextValue}>
<div
className={cn(
"bg-card flex size-full flex-col rounded-lg border",
className
)}
{...props}
>
{children}
</div>
</WebPreviewContext.Provider>
)
}
export type WebPreviewNavigationProps = ComponentProps<"div">
export const WebPreviewNavigation = ({
className,
children,
...props
}: WebPreviewNavigationProps) => (
<div
className={cn("flex items-center gap-1 border-b p-2", className)}
{...props}
>
{children}
</div>
)
export type WebPreviewNavigationButtonProps = ComponentProps<typeof Button> & {
tooltip?: string
}
export const WebPreviewNavigationButton = ({
onClick: onPress,
disabled,
tooltip,
children,
...props
}: WebPreviewNavigationButtonProps) => (
<TooltipTrigger>
<Button
className="hover:text-foreground h-8 w-8 p-0"
disabled={disabled}
onPress={onPress}
size="sm"
variant="ghost"
{...props}
>
{children}
</Button>
<Tooltip>{tooltip}</Tooltip>
</TooltipTrigger>
)
export type WebPreviewUrlProps = ComponentProps<typeof Input>
export const WebPreviewUrl = ({
value,
onChange,
onKeyDown,
...props
}: WebPreviewUrlProps) => {
const { url, setUrl } = useWebPreview()
const [inputValue, setInputValue] = useState(url)
// Sync input value with context URL when it changes externally
useEffect(() => {
setInputValue(url)
}, [url])
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setInputValue(event.target.value)
onChange?.(event)
}
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === "Enter") {
const target = event.target as HTMLInputElement
setUrl(target.value)
}
onKeyDown?.(event)
}
return (
<Input
className="h-8 flex-1 text-sm"
onChange={onChange ?? handleChange}
onKeyDown={handleKeyDown}
placeholder="Enter URL..."
value={value ?? inputValue}
{...props}
/>
)
}
export type WebPreviewBodyProps = ComponentProps<"iframe"> & {
loading?: ReactNode
}
export const WebPreviewBody = ({
className,
loading,
src,
...props
}: WebPreviewBodyProps) => {
const { url } = useWebPreview()
return (
<div className="flex-1">
<iframe
className={cn("size-full", className)}
sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-presentation"
src={(src ?? url) || undefined}
title="Preview"
{...props}
/>
{loading}
</div>
)
}
export type WebPreviewConsoleProps = ComponentProps<"div"> & {
logs?: Array<{
level: "log" | "warn" | "error"
message: string
timestamp: Date
}>
}
export const WebPreviewConsole = ({
className,
logs = [],
children,
...props
}: WebPreviewConsoleProps) => {
const { consoleOpen, setConsoleOpen } = useWebPreview()
return (
<Disclosure
className={cn("bg-muted/50 border-t font-mono text-sm", className)}
onOpenChange={setConsoleOpen}
open={consoleOpen}
{...props}
>
<DisclosureHeader asChild>
<Button
className="hover:bg-muted/50 flex w-full items-center justify-between p-4 text-left font-medium"
variant="ghost"
>
Console
<ChevronDownIcon
className={cn(
"h-4 w-4 transition-transform duration-200",
consoleOpen && "rotate-180"
)}
/>
</Button>
</DisclosureHeader>
<DisclosurePanel
className={cn(
"px-4 pb-4",
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=open]:animate-in outline-none"
)}
>
<div className="max-h-48 space-y-1 overflow-y-auto">
{logs.length === 0 ? (
<p className="text-muted-foreground">No console output</p>
) : (
logs.map((log, index) => (
<div
className={cn(
"text-xs",
log.level === "error" && "text-destructive",
log.level === "warn" && "text-yellow-600",
log.level === "log" && "text-foreground"
)}
key={`${log.timestamp.getTime()}-${index}`}
>
<span className="text-muted-foreground">
{log.timestamp.toLocaleTimeString()}
</span>{" "}
{log.message}
</div>
))
)}
{children}
</div>
</DisclosurePanel>
</Disclosure>
)
}