Attachment preview

Free · MIT

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.

Markdown

Preview

Install

With the Candor CLI

First time? Get started sets up the registry and theme. Or with shadcn directly: npx shadcn@latest add @candor/attachment-preview

Brings in: chat, locale, utils · npm: lucide-react@^1.67.0

Usage

TSX · The example above
"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>

PropTypeDefaultDescription
attachmentrequiredAttachment—The file, with progress (0–1) while uploading or an error if it failed.
onRemove((id: string) => void) | undefined—Shows a remove button.
classNamestring | undefined—Extra classes for the outer element.

<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).

PropTypeDefaultDescription
attachmentrequiredAttachment—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.
classNamestring | undefined—Extra classes for the outer element.

<AttachmentList>

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

PropTypeDefaultDescription
attachmentsrequiredAttachment[]—The files.
onOpen((attachment: Attachment) => void) | undefined—Makes each tile a button that opens its file.
classNamestring | undefined—Extra classes for the outer element.

Functions

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

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

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

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

Strings

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

KeyEnglish
removeRemove {name}
openOpen {name}
attachments{ one: "{count} attachment", other: "{count} attachments" }
bytes{size} B
kilobytes{size} KB
megabytes{size} MB