"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 (
);
}
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 (