"use client"; import { ChevronLeft, ChevronRight } from "lucide-react"; import { AnimatePresence, motion, useReducedMotion } from "motion/react"; import type { ReactNode } from "react"; import { EASE_OUT } from "@/lib/ease"; import { cn } from "@/lib/utils"; export interface ArtifactPanelProps { className?: string; children?: ReactNode; } /** * Panel shell for a live artifact/canvas surface next to a conversation — the * hairline-ringed card that hosts a header, a scrollable content region and * (optionally) a version-nav footer. Uses the same CSS-variable box-shadow * hairline technique as `ThreadCard` / `WorkbenchSummaryCard`, tuned to a * full opaque surface rather than a tinted one since this panel is the * primary content, not an inline card in a message stream. */ export function ArtifactPanel({ className, children }: ArtifactPanelProps) { return (
{children}
); } export interface ArtifactHeaderProps { /** 16px leading icon, e.g. ``. */ icon?: ReactNode; title: ReactNode; /** Rendered beside the title on the same line, truncating independently. */ subtitle?: ReactNode; /** Trailing slot for the caller's own controls — copy/download buttons, an `ArtifactViewToggle`. */ actions?: ReactNode; className?: string; } /** Fixed 48px header row: icon, truncating title/subtitle, and a trailing action cluster. */ export function ArtifactHeader({ icon, title, subtitle, actions, className }: ArtifactHeaderProps) { return (
{icon ? ( {icon} ) : null}
{title} {subtitle ? {subtitle} : null}
{actions ?
{actions}
: null}
); } export interface ArtifactActionProps { "aria-label": string; onClick?: () => void; children?: ReactNode; className?: string; } /** One icon button inside an `ArtifactHeader`'s actions slot (copy, download, ...) — same visual as `ThreadActionButton`. */ export function ArtifactAction({ "aria-label": ariaLabel, onClick, children, className, }: ArtifactActionProps) { return ( ); } export type ArtifactView = "preview" | "code"; export interface ArtifactViewToggleProps { value: ArtifactView; onChange: (next: ArtifactView) => void; previewLabel?: ReactNode; codeLabel?: ReactNode; className?: string; } /** Two-segment pill for switching between a rendered "Preview" and raw "Code" view — same visual language as the preview's scene-switcher pill. */ export function ArtifactViewToggle({ value, onChange, previewLabel = "Preview", codeLabel = "Code", className, }: ArtifactViewToggleProps) { return (
{(["preview", "code"] as const).map((view) => ( ))}
); } export interface ArtifactContentProps { /** When passed, `children` cross-fades keyed on this value (e.g. the active `ArtifactView`, or a composite key that also folds in a version so version switches transition too). Omit for static content that never swaps. */ view?: string | number; className?: string; children?: ReactNode; } /** * Scrollable body region of an `ArtifactPanel`. When `view` is passed, the * previous and next `children` cross-fade (opacity + a 4px rise, 0.15s * `EASE_OUT`, `AnimatePresence mode="wait"`) instead of swapping instantly — * reduced to a plain opacity fade under `useReducedMotion()`. Without `view`, * `children` render directly with no transition wrapper. */ export function ArtifactContent({ view, className, children }: ArtifactContentProps) { const reduce = useReducedMotion() ?? false; return (
{view !== undefined ? ( {children} ) : ( children )}
); } export interface ArtifactVersionNavProps { /** 1-based position of the version currently shown. */ index: number; count: number; onPrev?: () => void; onNext?: () => void; /** Renders a trailing ghost "Restore" button when passed — the caller decides when restoring makes sense (e.g. only on non-latest versions). */ onRestore?: () => void; restoreLabel?: ReactNode; className?: string; } /** * Prev/next control for stepping through an artifact's version history — the * same shape as `ThreadBranchSwitcher`: chevron buttons flanking a * tabular-nums "v2 / 3" readout that crossfades with a short y-shift on * change (`AnimatePresence mode="popLayout"`), reduced to an instant swap * under `useReducedMotion()`. An optional trailing ghost "Restore" button * appears whenever `onRestore` is passed. */ export function ArtifactVersionNav({ index, count, onPrev, onNext, onRestore, restoreLabel = "Restore", className, }: ArtifactVersionNavProps) { const reduce = useReducedMotion() ?? false; return (
v{index} / {count} {onRestore ? ( ) : null}
); }