54 / Code
Web Preview
A sandboxed frame for whatever the assistant just built, with the widths to check it at.
"use client"
import { WebPreview } from "mischief-ui/web-preview"
export function WebPreviewDemo() {
return (
<div className="w-full max-w-2xl">
<WebPreview
src="/docs/components/kbd"
title="The Kbd documentation page"
defaultSize="tablet"
height={360}
/>
</div>
)
}Installation
Copy the source into your project, or keep it behind a package.
npx shadcn@latest add Tinkerers-Labs/mischief-ui/web-previewimport { WebPreview } from "mischief-ui/web-preview"Or paste it in yourself. The source imports the shared cn helper from @/lib/utils, so point that at your own copy.
"use client" import * as React from "react"import { ExternalLink, RotateCw } from "lucide-react" import { cn } from "@/lib/utils" export type PreviewSize = { id: string label: string width: number} export type WebPreviewProps = Omit<Usage
export function Result({ url }) {
return <WebPreview src={url} title="The page the agent built" />
}The sandbox is most of the component
Generated code goes in a frame because it cannot be trusted, and the default here is scripts, forms, popups and modals. What it deliberately leaves out is allow-same-origin.
Granting allow-scripts and allow-same-origin together undoes the sandbox completely: the framed page shares your origin, so its script can reach the frame element in the parent document and rewrite the sandbox attribute it is meant to be held by. Widen this only for content you wrote.
The cost of leaving it out is that the frame has no origin at all, so a request from inside it arrives without one: fonts served from your own domain fail CORS, cookies do not travel, and storage is empty. A preview looking slightly wrong in the typeface is the sandbox working.
<WebPreview
src={url}
title="The page the agent built"
sandbox="allow-scripts allow-forms"
/>The address is what you asked for
It is not where the frame ended up. A page on another origin will not tell you its own location, and reading it throws, so the bar shows the address that was requested and stops making promises after that.
Reload works by mounting the frame again rather than by calling into it, for the same reason. The link out is a real anchor to the same address, so it can be middle-clicked and copied like any other.
Widths, scaled rather than cut off
A phone width inside a panel narrower than a phone is scaled down, not clipped. The page inside still believes it has 390 pixels, which is the only way the media queries it was built with actually run.
API
srcstringWhat to show.titlestringNames the frame. Required: an unnamed frame is announced as nothing else.sizesreadonly PreviewSize[]Widths to offer. Phone, tablet, full.defaultSizestringWhich width to open at.editablebooleanLet someone type a different address.onNavigate(src: string) => voidA new address was entered.sandboxstringReplaces the default. Read the note before widening it.heightnumberPixels. Defaults to 420....rootPropsHTMLAttributes<HTMLDivElement>Native root attributes.PreviewSize
idstringUnique within the set.labelstringShown on the button.widthnumberPixels. Zero fills the panel.Accessibility
The frame carries a required title, because one without a name is announced as "frame" and nothing more. The address is a labelled input rather than styled text, so it can be focused, read and copied, and it is read-only until editing is turned on. Width choices are a labelled group of toggles carrying aria-pressed. The link out says where it goes and that it opens in a new tab. Every control is at least 32px with a visible focus ring.