Component · Chatbot

Attachments

Composable system for displaying file attachments — inline badges in a prompt input, grid cards in a message bubble, or full-detail rows in an upload UI. Works standalone or inside PromptInput.

Preview — grid

design-spec.pdf application/pdf · 240.0 KB voice-note.mp3 audio/mpeg · 2.0 MB screen-recording.mp4 video/mp4 · 14.0 MB

Preview — inline

design-spec.pdfvoice-note.mp3screen-recording.mp4

Preview — list

design-spec.pdf240.0 KB
voice-note.mp32.0 MB
screen-recording.mp414.0 MB

Empty state

Drop files here or paste an image.

Anatomy

text
<attachment-list variant="grid | inline | list">
├── <attachment-item [data]="..." (removed)="...">
│   ├── <attachment-preview [fallback]="..." />        Image / video / icon
│   ├── <attachment-info  [showMediaType]="..." />     Name + secondary line
│   │   — or finer-grained:
│   ├── <attachment-name />                            Filename only
│   ├── <attachment-size />                            Formatted bytes
│   └── <attachment-remove [label]="..." />            Close button
└── <attachment-empty>                                 Empty-state slot
      Custom message via <ng-content>
    </attachment-empty>

Installation

bash
npm install @shadng/attachments @shadng/core

Or via the CLI:

bash
npx shadng add attachments

Basic usage

typescript
import { Component, signal } from '@angular/core';
import {
  AttachmentInfo,
  AttachmentItem,
  AttachmentList,
  AttachmentPreview,
  AttachmentRemove,
  toAttachmentData,
  type AttachmentData,
} from '@shadng/attachments';

@Component({
  selector: 'app-uploader',
  imports: [
    AttachmentList,
    AttachmentItem,
    AttachmentPreview,
    AttachmentInfo,
    AttachmentRemove,
  ],
  template: `
    <attachment-list variant="grid">
      @for (item of items(); track item.id) {
        <attachment-item [data]="item" (removed)="onRemove($event)">
          <attachment-preview />
          <attachment-info [showMediaType]="true" />
          <attachment-remove label="Remove this file" />
        </attachment-item>
      }
    </attachment-list>
  `,
})
export class Uploader {
  items = signal<AttachmentData[]>([]);

  onFileSelected(file: File) {
    this.items.update((prev) => [...prev, toAttachmentData(file)]);
  }

  onRemove(item: AttachmentData) {
    this.items.update((prev) => prev.filter((it) => it.id !== item.id));
  }
}

Variants

  • grid (default) — wraps cards with a thumbnail header. Best for message bubbles and rich previews.
  • inline — compact badge row. Best for the row above a prompt input's textarea.
  • list — vertical stack with full metadata. Best for upload UIs.

Composition with PromptInput

When nested inside a PromptInput, the attachment state is owned by the container. Bind the attachments model both ways and wire the list:

typescript
<prompt-input
  [(value)]="text"
  [(attachments)]="attachments"
  [acceptAttachments]="['image/*', 'application/pdf']"
  (submitted)="send($event)"
>
  @if (attachments().length > 0) {
    <attachment-list variant="inline">
      @for (item of attachments(); track item.id) {
        <attachment-item [data]="item" (removed)="removeAttachment($event)">
          <attachment-preview />
          <attachment-name />
          <attachment-remove />
        </attachment-item>
      }
    </attachment-list>
  }
  <prompt-input-textarea />
  <prompt-input-toolbar>
    <prompt-input-submit />
  </prompt-input-toolbar>
</prompt-input>

API — AttachmentData

The shape every item accepts. Compatible with FileUIPart from the Vercel AI SDK.

NameTypeDescription
idstringStable identifier — used for tracking in @for loops.
filenamestring?Display name. Falls back to a category label ("Image", "Document") when omitted.
mediaTypestring?MIME type. Drives the preview surface and label fallback.
urlstring?Already-uploaded URL — used as <img>/<video> source.
fileFile?Native File — used to derive a blob URL for image previews.
sizenumber?Bytes. When omitted but `file` is set, derived from file.size.

API — AttachmentList

NameTypeDefaultDescription
variant'grid' | 'inline' | 'list''grid'Layout shape. Items read this from the list via DI.
ariaLabelstring'Attachments'aria-label applied to the role="list" host.

API — AttachmentItem

NameTypeDefaultDescription
dataAttachmentData(required)The attachment to render.
loadingbooleanfalseShow animated skeleton overlay on the preview.
erroredbooleanfalseDestructive border + "Error" badge.
variant'grid' | 'inline' | 'list' | undefinedundefinedOverride the parent list variant for a single item.

API — AttachmentItem outputs

NameTypeDescription
removedAttachmentDataEmitted when AttachmentRemove inside the item is clicked. Carries the data object.

API — AttachmentPreview

NameTypeDefaultDescription
fallbackTemplateRef<unknown> | nullnullCustom non-image surface (your own icon system, custom layout, etc).

API — AttachmentInfo

NameTypeDefaultDescription
showMediaTypebooleanfalseAdd the MIME type to the secondary line.
showSizebooleantrueAdd the formatted file size to the secondary line.

API — AttachmentRemove

NameTypeDefaultDescription
labelstring'Remove attachment'Screen-reader label and tooltip.

Utility functions

Re-exported from @shadng/attachments:

NameTypeDescription
toAttachmentData(file, overrides?)(File, Partial<AttachmentData>?) => AttachmentDataConvert a native File into the shape components consume. Generates an id if you do not provide one.
getMediaCategory(data)(data) => "image" | "video" | "audio" | "document" | "source" | "unknown"Coarse classification driving the preview surface.
getAttachmentLabel(data)(data) => stringBest human-readable label — filename, then category fallback, then "Attachment".
formatFileSize(bytes)(number | undefined) => stringFormat bytes as a short string like "240 KB" or "12.4 MB".

Accessibility

  • role="list" on the AttachmentList container, configurable ariaLabel
  • role="listitem" on each AttachmentItem, with data-media-category for CSS targeting
  • Remove button has an explicit aria-label (default "Remove attachment")
  • Preview images get an alt derived from the filename — pass a more descriptive value via your own fallback template when needed
  • Errored state surfaces a visible "Error" badge in addition to the destructive border

Theming

Semantic tokens consumed:

  • --card / --card-foreground — item background
  • --border — item border
  • --muted / --muted-foreground — preview background, secondary text
  • --accent / --accent-foreground — remove button hover
  • --destructive / --destructive-foreground — errored item border + error badge
  • --ring — focus rings

The component does not ship its own tokens — see Theme tokens for the full list.

Known issues

  • Hover cards deferred to v0.2. AI Elements ships an AttachmentHoverCard for showing a larger preview on hover in inline variant. ShadNG will adopt it once the popover primitive lands (target: v0.2 alongside Conversation).
  • id generation.toAttachmentData() uses an internal counter for ids. If you persist attachments across reloads, generate stable ids yourself.
MIT © Kalvnerv0.1.0 · pre-release