# Attachment preview

A file waiting to be sent, as a chip in the composer: its name, then upload progress, an error, or its size, and a remove button. Images show their preview when the app gives one.

- Registry name: `attachment-preview` (`@candor/attachment-preview`)
- Tier: Free (MIT). No licence key needed.
- Docs: https://candor.themethodic.com/docs/attachment-preview

## Install

```bash
npx candor-ui add attachment-preview
```

Run `npx candor-ui@latest init` once first (https://candor.themethodic.com/get-started.md). Without the CLI: `npx shadcn@latest add @candor/attachment-preview`.
It brings in: chat, locale, utils.
npm packages: lucide-react@^1.67.0.

## Example

```tsx
"use client"

import { AttachmentChip, AttachmentList } from "@/components/attachment-preview"
import { Message } from "@/components/message"

// A tiny inline image stands in for a thumbnail your app would serve.
const thumb =
  "data:image/svg+xml;utf8," +
  encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="160" height="96"><rect width="160" height="96" fill="#dbe4ff"/><rect x="16" y="20" width="80" height="10" rx="3" fill="#6b7cff"/><rect x="16" y="40" width="120" height="8" rx="3" fill="#a5b0ff"/><rect x="16" y="56" width="100" height="8" rx="3" fill="#a5b0ff"/></svg>')

export default function Example() {
  return (
    <div className="flex max-w-2xl flex-col gap-5">
      <Message
        onOpenAttachment={() => {}}
        message={{
          id: "u1",
          author: { role: "user" },
          status: "complete",
          blocks: [
            { kind: "attachments", files: [{ id: "a", name: "checkout-error.png", mediaType: "image/png", size: 48_200, previewUrl: thumb }, { id: "b", name: "orders-export.csv", mediaType: "text/csv", size: 2_400_000 }] },
            { kind: "plain", text: "Checkout fails for these orders. Screenshot attached." },
          ],
        }}
      />
      <AttachmentList attachments={[{ id: "c", name: "runbook.md", size: 6_100 }, { id: "d", name: "config.json", mediaType: "application/json", size: 812 }]} />
      <div className="flex flex-wrap gap-1.5">
        <AttachmentChip attachment={{ id: "e", name: "trace.zip", size: 9_800_000, progress: 0.4 }} onRemove={() => {}} />
        <AttachmentChip attachment={{ id: "f", name: "huge-video.mov", error: "Too large (max 25 MB)" }} onRemove={() => {}} />
      </div>
    </div>
  )
}
```

## <AttachmentChip>

Props:

- `attachment: Attachment`: The file, with progress (0–1) while uploading or an error if it failed.
- `onRemove?: ((id: string) => void) | undefined`: Shows a remove button.
- `className?: string | undefined`

## <AttachmentPreview>

A sent file, as a tile in a message: an image preview when the app supplied one, else an icon for its type, with its name and size. The name is never a link: with `onOpen` the tile is a button and your app decides what opening means (a viewer, a signed download).

Props:

- `attachment: Attachment`: The file. `previewUrl` must be one your app serves (a thumbnail, a blob: URL); it's shown only for images.
- `onOpen?: ((attachment: Attachment) => void) | undefined`: Makes the tile a button that opens the file.
- `className?: string | undefined`

## <AttachmentList>

Several sent files, as a wrapping row of tiles, announced as a list.

Props:

- `attachments: Attachment[]`: The files.
- `onOpen?: ((attachment: Attachment) => void) | undefined`: Makes each tile a button that opens its file.
- `className?: string | undefined`

## Functions

### formatFileSize

```ts
formatFileSize(bytes: number | undefined, number: Formatter["number"], t: Translate<typeof attachmentPreviewStrings>)
```

"820 B", "14 KB", "2.4 MB", in the locale.

### fileIcon

```ts
fileIcon(a: Pick<Attachment, "name" | "mediaType">)
```

An icon for a file, from its media type or, failing that, its extension.

## Strings (`attachmentPreview`)

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

- `remove`: Remove {name}
- `open`: Open {name}
- `attachments`: { one: "{count} attachment", other: "{count} attachments" }
- `bytes`: {size} B
- `kilobytes`: {size} KB
- `megabytes`: {size} MB

## 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`.
