# Artifact panel

A document or file to read beside the work, for a SidePanel tab or a full-width view. A document's text is read into safe blocks (headings, lists, emphasis, code) and never rendered as HTML, so a document a model wrote can't inject markup or links. A file with no text shows its type and size, and an image the preview your app serves. Download goes through `onDownload`, one menu item per format; `onFullWidthChange` adds Open full width.

- Registry name: `artifact-panel` (`@candor/artifact-panel`)
- Tier: Pro. Installing it needs a Pro, Premium or Team licence key: the person runs `npx candor-ui key <key>` themselves; never ask for the key in chat or write it to a file.
- Docs: https://candor.themethodic.com/docs/artifact-panel

## Install

```bash
npx candor-ui add artifact-panel
```

Run `npx candor-ui@latest init` once first (https://candor.themethodic.com/get-started.md). Without the CLI: `npx shadcn@latest add @candor/artifact-panel`.
It brings in: attachment-preview, button, chat, dropdown-menu, locale, message-content, relative-time, safe-markdown, work, utils.
npm packages: lucide-react@^1.67.0.

## Example

```tsx
"use client"

import * as React from "react"
import { FileText, Sheet } from "lucide-react"
import { ArtifactPanel } from "@/components/artifact-panel"
import { SidePanel } from "@/components/side-panel"
import type { Artifact } from "@/lib/work"

const report: Artifact = {
  id: "report",
  title: "Pricing review",
  state: "Checked",
  formats: [
    { id: "docx", label: "Word" },
    { id: "pdf", label: "PDF" },
  ],
  // Markdown text is read into safe blocks; <b>tags</b> stay text and links aren't made.
  text: `# Pricing review

Three plans cover **92%** of last quarter's revenue. The other four can merge into them.

## What changes

- Starter and Basic become one plan at the Starter price.
- Team keeps its price; seats above 20 move to Business.

## What stays

Annual discounts and every grandfathered contract.`,
}

const data: Artifact = {
  id: "data",
  title: "Revenue by plan",
  state: "Not yet checked",
  file: { id: "csv", name: "revenue-by-plan.csv", mediaType: "text/csv", size: 184_000 },
}

export default function Example() {
  const [wide, setWide] = React.useState(false)
  const [downloaded, setDownloaded] = React.useState("")
  const download = (id: string, format?: string) => setDownloaded(`${id}${format ? ` as ${format}` : ""}`)
  const panel = (a: Artifact) => <ArtifactPanel artifact={a} onDownload={download} fullWidth={wide} onFullWidthChange={setWide} className="-m-4 h-[calc(100%+2rem)]" />
  return (
    <div className="flex max-w-2xl flex-col gap-2">
      <div className={wide ? "h-[26rem] overflow-hidden rounded-lg border bg-background" : "h-[26rem] max-w-md overflow-hidden rounded-lg border bg-chrome"}>
        <SidePanel
          tabs={[
            { id: "report", label: "Pricing review", Icon: FileText, content: panel(report) },
            { id: "data", label: "Revenue by plan", Icon: Sheet, content: panel(data) },
          ]}
        />
      </div>
      <p className="text-xs text-muted-foreground" aria-live="polite">
        {downloaded && `Your app would download ${downloaded}.`}
      </p>
    </div>
  )
}
```

## <ArtifactPanel>

Props:

- `artifact: Artifact`: What to show: a document (`text` or `blocks`) or a file.
- `onDownload?: ((id: string, format?: string) => void) | undefined`: Called with the artifact's id and the chosen format's id (undefined with no formats).
- `fullWidth?: boolean | undefined` (default `false`): Whether it's showing full width; sets a reading width and the toggle's words. Defaults to false.
- `onFullWidthChange?: ((fullWidth: boolean) => void) | undefined`: Adds the Open full width / Back to the panel button; called with the new state.
- `sources?: MessageSource[] | undefined`: Sources the document cites by marker.
- `onOpenEntity?: ((entity: EntityRef) => void) | undefined`: Makes entity chips in the document open their thing.
- `headingLevel?: 1 | 2 | 3 | 4 | undefined` (default `2`): The level of the title's heading, to fit the page's outline. Defaults to 2.
- `className?: string | undefined`

## Strings (`artifactPanel`)

The fixed words, English by default. Override any of them under `artifactPanel` in `<LocaleProvider strings={{ artifactPanel: { … } }}>`; the table is exported as `artifactPanelStrings`.

- `download`: Download
- `downloadAs`: Download {format}
- `openFullWidth`: Open full width
- `backToPanel`: Back to the panel
- `updated`: Updated {time}
- `noPreview`: There’s no preview for this file. Download it to open it.
- `empty`: Nothing to show yet.

## Rules for using Candor UI

- Components take plain typed data. Never pass model output as HTML or render it as a link; messages are typed blocks.
- A control that can't be used says why: wrap it in `<Locked reason="…">`, don't just disable it.
- Don't show a value you don't have: leave it out (it renders as —) rather than guess. An estimate is labelled as one.
- Nothing is pre-selected in a decision a person makes, and the agent is named from your app's data, never from model output.
- Your app's words go in `LocaleProvider` (fixed strings) and `TermsProvider` (nouns); entity kinds and agent roles in `EntityKindsProvider` and `AgentRolesProvider`.
