'use client' import { ExternalLink } from 'lucide-react' import { Button } from '../ui/button/button' import { ClaudeIcon } from '../icons/claude-icon' import { EmbedViewerFrame } from './embed-viewer-frame' import { useClaudeMirrorSrc } from '../../hooks/use-claude-mirror-src' import { toClaudeEmbedUrl } from '../../utils/embed-url-converters' export type ClaudeEmbedKind = 'artifact' | 'design' export interface ClaudeEmbedProps { /** Any claude.ai / claude.site url. */ url: string /** Which surface the link points at — only the heading differs. */ kind?: ClaudeEmbedKind /** The author's name for this link, when they gave one. */ title?: string /** iframe height (CSS value), matching `FigmaEmbed`'s prop. */ height?: string /** iframe loading strategy. Defaults to `lazy`, as `FigmaEmbed` does. */ loading?: 'eager' | 'lazy' } const KIND_HEADING: Record = { artifact: 'Claude Artifact', design: 'Claude Design', } /** * A Claude artifact, in the SAME chrome as every other embed viewer * (`EmbedViewerFrame` — what `FigmaEmbed` renders): icon, heading, an * "Open in Claude" action, and the frame below. Reached through the * `{{claude-artifact:URL}}` / `{{claude-design:URL}}` shortcodes, so it looks * identical wherever markdown renders — a spec body, a comment, a links rail. * * FRAME SOURCE — fully under the hood, in priority order: * 1. The host's SELF-HOSTED MIRROR: the url's artifact id derives the * storage-view proxy path (`toClaudeMirrorPath`), probed with a * 1-byte ranged fetch. When the host has ingested a copy, the frame * shows it — claude.ai frame-locks CODE artifacts entirely, so this * is the only way those ever render inline. * 2. claude.ai's own `/embed` route for a PUBLISHED artifact * (`toClaudeEmbedUrl`). * 3. Neither → the shared frame's empty state. * The consumer passes only the claude URL; "Open in Claude" always * targets it, so commenting stays on claude.ai. * * What defeats the claude.ai leg: a Claude CODE url (no Embed settings * exist for it, and its `/embed` path answers `frame-ancestors 'self'`), * a Claude Design url (no embed route), an artifact whose author has not * allow-listed this host, or an http host (mixed content). */ export function ClaudeEmbed({ url, kind = 'artifact', title, height, loading = 'lazy' }: ClaudeEmbedProps) { // Mirror detection lives in `useClaudeMirrorSrc` (derive from // EndpointsRuntime + 1-byte ranged probe + background revalidation) — this // component stays presentational. const { src: mirrorSrc, status } = useClaudeMirrorSrc(url) // While the probe is in flight (on a cold view that IS the ~1-2s // self-heal), show the shell's loading state rather than the claude.ai // fallback — a code artifact's fallback is the empty "no embeddable view" // box, and flashing it before the mirror resolves reads as broken. const isProbing = status === 'probing' // Found → the mirror; otherwise the claude.ai embed (or its empty state). // Suppressed while probing so the loading skeleton owns the body. const embedUrl = isProbing ? null : (mirrorSrc ?? toClaudeEmbedUrl(url)) return ( } title={title?.trim() || KIND_HEADING[kind]} titleVariant="h6" isLoading={isProbing} actions={ } src={embedUrl} // The framed artifact has no internal fullscreen button of its own // (unlike Figma's player) — the shell provides the toggle for both // the claude.ai `/embed` route and a mirror. fullscreenControl loading={loading} height={height} // Same frame contract Figma gets — an artifact is a real app: it copies // to the clipboard and can go fullscreen. allow="clipboard-write; clipboard-read; fullscreen" // …but UNLIKE Figma, an artifact is USER-AUTHORED HTML and JS, so it is // sandboxed. Omitting `allow-top-navigation` is the point: an artifact // cannot navigate the page it is embedded in. The `allow-same-origin` // token is CONDITIONAL on where the frame points: // - claude.ai `/embed` (no mirror): Anthropic's own embed snippet // uses it, and on a CROSS-ORIGIN frame it grants the frame // claude.ai's origin — never ours. The artifact runtime needs // it (storage, postMessage handshake). // - the derived MIRROR is served from the HOST's OWN origin — // `allow-same-origin` + `allow-scripts` there would be a no-op // sandbox handing the artifact first-party cookies and // `window.parent`. A static mirror renders fine from an opaque // origin, so the token is dropped. sandbox={ mirrorSrc ? 'allow-scripts allow-popups allow-forms' : 'allow-scripts allow-same-origin allow-popups allow-forms' } allowFullScreen emptyIcon={} emptyMessage="Open this one in Claude · it has no embeddable view" /> ) }