import { type JSX, splitProps, mergeProps, createSignal, createEffect, createMemo, on, onMount, onCleanup, Show, } from 'solid-js'; import { cn } from '../utils/cn'; import { Button } from '../ui/button'; import { CodeBlock, CodeBlockCode } from './code-block'; import { FileTree, type FileTreeFile } from './file-tree'; import { Loader } from './loader'; import { isPdfPreviewEnabled, renderPdfInto } from '../primitives/pdf-preview'; import { ArrowLeft, ArrowRight, RotateCw, House, Eye, Code as CodeIcon, FileText, ExternalLink, Download, Maximize2, Minimize2, } from 'lucide-solid'; export type ArtifactTab = 'preview' | 'code'; /** A file the artifact can preview + show source for. */ export type ArtifactFile = FileTreeFile; export interface ArtifactProps extends Omit, 'onSelect'> { /** URL the preview iframe frames. */ src?: string; /** Files for the Code tab's tree (+ each file's preview `url`). */ files?: ArtifactFile[]; /** Active tab. Default `preview`. */ tab?: ArtifactTab; /** Selected file path (syncs tree highlight + Code source + preview). */ activeFile?: string; /** iframe `sandbox` override. Default `allow-scripts allow-forms`. */ sandbox?: string; /** Accessible iframe title. */ iframeTitle?: string; /** Fired when the preview navigates (back/forward/reload/path-edit/file-click). */ onNavigate?: (url: string) => void; /** Fired when the Preview|Code tab changes. */ onTabChange?: (tab: ArtifactTab) => void; /** Fired when a file is selected in the tree. */ onFileSelect?: (path: string) => void; // view-state /** Controlled maximize view-state (drives the expand/restore button). */ maximized?: boolean; /** Fired when the expand/restore button toggles the maximize view-state. */ onMaximizeChange?: (maximized: boolean) => void; // toolbar composition — existing five default SHOWN (no-* flags invert in the facade) /** Show the back/forward nav buttons. Default `true`. */ showNav?: boolean; /** Show the reload button. Default `true`. */ showReload?: boolean; /** Show the home button. Default `true`. */ showHome?: boolean; /** Show the editable path/address field. Default `true`. */ showPathField?: boolean; /** Show the Preview|Code tab toggle. Default `true`. */ showTabs?: boolean; // new affordances — OPT-IN (default hidden; see resolved decision #2) /** Show the expand-to-fill button. Default `false` (opt-in). */ expandable?: boolean; /** Show the open-in-new-tab button. Default `false` (opt-in). */ openInTab?: boolean; // chrome /** Standalone chrome: rounded + bordered (default in-panel = square, borderless). */ standalone?: boolean; /** Make the path field read-only (visible, nav-tracking, non-editable). */ readonlyPath?: boolean; } const DEFAULT_SANDBOX = 'allow-scripts allow-forms'; /** Resolve a file's preview URL: explicit `url`, else ` + /path`. */ function resolveFileUrl(file: ArtifactFile, src: string | undefined): string { if (file.url) return file.url; if (!src) return file.path; try { return new URL(file.path, src).href; } catch { return file.path; } } /** True when `url` should render as a PDF: a matching `files` entry is typed * `'pdf'`, or the URL path (query/hash stripped) ends in `.pdf`. */ export function isPdfUrl(url: string, files: ArtifactFile[]): boolean { if (!url) return false; const match = files.find((f) => f.url === url || f.path === url); if (match?.type === 'pdf') return true; const path = url.split(/[?#]/)[0]; return /\.pdf$/i.test(path); } /** * `Artifact` — a framed, switchable generated-artifact viewer. A functional nav * toolbar (back · forward · reload · home + editable path field + Preview|Code * toggle) over a sandboxed `