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; /** Imperative handle exposed via `controllerRef` — surfaces the artifact's latent * toolbar capabilities (history back/forward/reload/home, programmatic navigate, * file selection, open-in-new-tab, maximize/restore) so the `` * facade can forward them as instance methods. Each delegates to the SAME internal * handler the toolbar buttons use, so every existing event still fires. */ export interface ArtifactController { /** Go back in the artifact's own history stack (no-op when there's no prior entry). */ back(): void; /** Go forward in the history stack (no-op when there's no forward entry). */ forward(): void; /** Force-reload the current preview url (also re-renders an inline PDF). */ reload(): void; /** Navigate to the `src` home url (no-op when there's no `src`). */ home(): void; /** Push + load a url in the preview (the path-field submit path). */ navigate(url: string): void; /** Select a file by path: highlights the tree, shows its source, navigates the preview. */ selectFile(path: string): void; /** Open the current url in a new browser tab (no-op when there's no concrete url). */ openExternal(): void; /** Enter the maximized view-state (fires onMaximizeChange/kai-maximize-change). */ maximize(): void; /** Exit the maximized view-state. */ restore(): void; } 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[]; /** Controlled active tab — when set, the artifact follows it (re-asserted on * every change). When undefined the tab is uncontrolled (see `defaultTab`). */ tab?: ArtifactTab; /** Uncontrolled INITIAL tab (used only when `tab` is undefined). The user can * then freely switch tabs; defaults to `preview`. */ defaultTab?: 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; /** Friendly address shown in the path field INSTEAD of the real current url * (read-only, non-navigable). Use when the framed url is not consumer-facing * (e.g. a `data:` blob) so a clean address is shown instead of leaking it. * Unset = show the real url (editable per `readonlyPath`). */ displayUrl?: string; /** Receive the imperative controller once mounted. The `` facade * forwards these as element methods (back/forward/reload/home/navigate/ * selectFile/openExternal/maximize/restore). */ controllerRef?: (controller: ArtifactController) => void; } 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 `