# Artifact toolbar

The actions on an artifact or a dashboard tile: maximise and restore, download (one format a button, several a menu), and "Ask about this", which hands the artifact to the app's chat (Candor attaches nothing itself). Each button names the artifact, so a page of tiles doesn't read "Download, Download, Download". A control whose handler isn't given isn't drawn; one that can't be used now stays, locked, saying why. It is one tab stop: the arrow keys move between its buttons (`ArtifactToolbarButton` for your own).

- Registry name: `artifact-toolbar` (`@candor/artifact-toolbar`)
- 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-toolbar

## Install

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

Run `npx candor-ui@latest init` once first (https://candor.themethodic.com/get-started.md). Without the CLI: `npx shadcn@latest add @candor/artifact-toolbar`.
It brings in: button, dropdown-menu, locale, locked, tokens, utils.
npm packages: @base-ui/react@^1.9.0, lucide-react@^1.67.0.

## Example

```tsx
"use client"

import * as React from "react"
import { Share2 } from "lucide-react"
import { ArtifactToolbar, ArtifactToolbarButton } from "@/components/candor/artifact-toolbar"
import { TooltipProvider } from "@/components/candor/ui/tooltip"

export default function Example() {
  const [maximised, setMaximised] = React.useState(false)
  const [last, setLast] = React.useState("Nothing yet.")
  return (
    <TooltipProvider>
      <div className="flex max-w-md flex-col gap-2 rounded-xl border bg-card p-4">
        <div className="flex items-center gap-2">
          <p className="flex-1 text-sm font-semibold">Failed runs this week</p>
          <ArtifactToolbar
            title="Failed runs this week"
            maximised={maximised}
            onMaximisedChange={setMaximised}
            downloads={[{ format: "csv", label: "CSV" }, { format: "png", label: "PNG image" }]}
            onDownload={(format) => setLast(`Download as ${format}`)}
            onAsk={() => setLast("Chat opened with the table attached")}
          >
            <ArtifactToolbarButton aria-label="Share Failed runs this week" onClick={() => setLast("Share")}>
              <Share2 aria-hidden />
            </ArtifactToolbarButton>
          </ArtifactToolbar>
        </div>
        <p className="text-xs text-muted-foreground">{last}</p>
      </div>
    </TooltipProvider>
  )
}
```

## <ArtifactToolbar>

Props:

- `title: string`: The artifact's name, in every button's accessible name: "Download Failed runs".
- `maximised?: boolean | undefined` (default `false`): Whether the artifact is maximised now: the button's name is then Restore rather than Maximise.
- `onMaximisedChange?: ((maximised: boolean) => void) | undefined`: Called with the state the person asks for. Without it, there's no maximise button.
- `downloads?: ArtifactDownload[] | undefined`: The formats it downloads as: one is a button, more a menu.
- `onDownload?: ((format: string) => void) | undefined`: Called with the format the person picks; the app makes the file. Without it, there's no download.
- `onAsk?: (() => void) | undefined`: Opens the app's chat with this artifact attached ("Ask about this"). Without it, there's no ask button.
- `locked?: { [K in ArtifactToolbarControl]?: string | null | undefined } | undefined`: Why a control can't be used now, by control. It stays focusable, marked `aria-disabled`, and says why. A control whose reason is `null` or left out is usable.
- `children?: React.ReactNode | undefined`: More actions, as `ArtifactToolbarButton`s, after the built-in ones.
- `className?: string | undefined`: Classes for the root, merged with Candor's.

## <ArtifactToolbarButton>

One more action in an `ArtifactToolbar`, so the arrow keys reach it: a small ghost button. Name it, with the artifact's title, in `aria-label` when it shows only an icon.

Props:

- `className?: string | undefined`: Classes for the button, merged with Candor's.

Also takes the props of `Omit<ToolbarPrimitive.Button.Props, "className" | "render">`.

## Strings (`artifactToolbar`)

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

- `toolbar`: Actions for {title}
- `maximise`: Maximise {title}
- `restore`: Restore {title}
- `download`: Download {title}
- `downloadAs`: Download {title} as {format}
- `ask`: Ask about this: {title}
- `askShort`: Ask about this

## Types

### ArtifactDownload

One format the artifact downloads as: its id, passed to `onDownload`, and the words for it ("CSV").

```ts
{
  /** Passed to `onDownload`: "csv", "png". */
  format: string;
  /** The format's words; defaults to `format`. */
  label?: string | undefined;
}
```

### ArtifactToolbarControl

The controls a reason can lock.

```ts
"maximise" | "download" | "ask"
```

## 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`: its fixed strings in `strings`, its nouns in `nouns`; entity kinds and agent roles in `EntityKindsProvider` and `AgentRolesProvider`.
