<!-- GENERATED by scripts/build-llms.mjs from llms/media.md — do not edit this file. -->

# `lr-attachment-chip`

- **Import** `import '@aceshooting/lyra-ui/components/lr-attachment-chip.js';` (stable tag alias; registers the tag)
- **Class** `LyraAttachmentChip`, also available unregistered from `@aceshooting/lyra-ui/components/media/attachment-chip/attachment-chip.class.js`
- **Family** `components/media/` — see `llms/index.md` for its siblings
- **Status** `stable` since `4.0.0` — see the maturity and deprecation policy in `llms/shared.md`
- **Release history** [CHANGELOG.md](../../CHANGELOG.md); family-wide breaking-change summaries: [llms-full.txt](../../llms-full.txt)
- **Deprecations** none
- **Optional peers** none
- **Themeable via** 12 parts, 10 custom properties — see this component's own `@csspart`/`@cssprop` list below
- **Library-wide behavior** (events, form association, `locale`/`strings`, tokens, TS types): `llms/shared.md`

---

## `lr-attachment-chip`

A compact chip representing one file queued for (or already part of) a chat message — used in a
composer's pre-send attachment tray or a sent message's attachments display. Two independent ways
to populate it: set `file` to a real `File` (fresh from a picker/drop), from which `name`/`bytes`/
`mime-type` and the image thumbnail are all auto-derived; or set the plain `name`/`bytes`/
`mime-type`/`thumbnail-src` props instead, for reconstructing a chip from server-persisted
attachment metadata after a page reload, when no real `File` object exists any more. `file` always
wins when both are present. When a real `File` or `preview-src` is available, the chip offers a
localized action that emits a plain, non-cancelable `lr-preview-request`; it never registers or owns a viewer
or overlay, so the host composes the desired preview surface.

An error already present at first mount or reconnection renders as existing state without an
announcement, including a failure assigned while detached and reconnected before the next render. A
new connected transition to error still appends one localized failure announcement; repeated
connected failures remain separately announced, including a retry whose uploading and error writes
coalesce into one update.

**Properties:**

- `file?: File` (attribute `false`, i.e. property-only) — when set, `name`/`bytes`/`mimeType`/the
  image thumbnail are all derived from it, taking precedence over the independent props below
- `name: string = ''` — filename, used only while `file` is unset
- `bytes?: number` — file size in bytes, used only while `file` is unset. `0` is a known empty file
  and renders `0 B`; omission means unknown. Negative/non-finite writes normalize to omission.
- `attachmentId: string = ''` (attribute `attachment-id`) — stable domain identity carried by
  attachment action events. Empty or whitespace-only values use the fallback identity below. The
  platform `id` remains available for DOM identity/idrefs only.
- `mimeType: string = ''` (attribute `mime-type`) — used only while `file` is unset
- `thumbnailSrc: string = ''` (attribute `thumbnail-src`) — thumbnail image URL, used only while
  `file` is unset; rendered whenever present regardless of `mimeType` (no `file`-derived equivalent
  exists for a non-image file)
- `previewSrc: string = ''` (attribute `preview-src`) — source URL used for preview and download when
  `file` is unset; a real `File` takes precedence and uses a temporary blob URL
- `previewable: boolean = true` (reflected) — shows the preview action whenever a `file` or
  `preview-src` is available
- `status: LyraAttachmentUploadStatus = 'pending'` (reflected) — `'pending' | 'uploading' |
'error' | 'success'`; invalid values normalize to `pending`. Drives the accent tint and which of
  `progress`/`spinner`/`retry-button` renders.
- `progress: number = 0` — upload completion, 0-100; only meaningful while `status="uploading"`, a
  value of `0` or `NaN` falls back to the indeterminate spinner
- `removable: boolean = true` (reflected) — shows the remove (×) button
- `compact: boolean = false` (reflected) — renders a smaller, borderless pill presentation instead of
  the default bordered/chrome-heavy chip, e.g. for a composer's pending-attachment tray. `false` (the
  default) is visually identical to the standard chip.
- `thumbnailOnly: boolean = false` (reflected, attribute `thumbnail-only`) — when both this and
  `compact` are set, hides `[part='meta']` (the filename/size text) entirely for an image-mime
  attachment, leaving only the thumbnail. Has no effect for a non-image chip, or when `compact` is
  unset. `false` (the default) reproduces the chip's exact existing output. While it actually hides
  `[part='meta']`, `[part='base']` also switches to a reduced, symmetric padding sized for a lone
  thumbnail instead of the compact text row's padding — see
  `--lr-attachment-chip-compact-thumbnail-only-padding` below.
- `removeLabel?: string` (attribute `remove-label`) — verb used in the remove button's accessible
  name; omitting it reads back `undefined` and routes through the complete localized
  `removeWithContext` template
- `retryLabel?: string` (attribute `retry-label`) — verb used in the retry button's accessible
  name; omitting it reads back `undefined` and routes through the complete localized
  `attachmentRetryWithContext` template
- `uploadingLabel?: string` (attribute `uploading-label`) — verb used in the visible uploading
  status; omitting it reads back `undefined` and uses complete localized messages for progress,
  indeterminate state, and filename context so translators can reorder every value
- `uploadFailedLabel?: string` (attribute `upload-failed-label`) — visible status text shown for
  `status="error"`; override for i18n/locale. Omitting it reads back `undefined` and uses the
  localized default (`'Upload failed'` in English)
- `untitledLabel?: string` (attribute `untitled-label`) — fallback filename and tooltip when
  neither `file` nor `name` supplies a name. Omitting it reads back `undefined` and uses the
  localized default (`'Untitled file'` in English)

**Renamed in 8.0.0 — breaking:** the byte count is `bytes`, not `size` (same rename as
`lr-file-icon`'s). Everywhere else in this library `size` names a tier on the shared size ladder,
and a numeric byte count answering to the same property name is a collision a consumer only
discovers at runtime. A leftover `size="245000"` is an unknown attribute now: `bytes` stays omitted
and the `size` part renders nothing.

The component identifies _which_ attachment an action event is about through `attachmentId`. Set
`attachment-id="..."` when you have a stable server-side identity; when unset or whitespace-only
and `file` is set, a stable attachment id is
derived from `` `${file.name}:${file.size}:${file.lastModified}` ``; when neither is available, a
generated internal id is used as a last resort.

**Events:** `lr-remove` (`detail: { attachmentId }`, only rendered while `removable`), `lr-retry`
(`detail: { attachmentId }`, only rendered while `status="error"`), and
`lr-preview-request` (`detail: { attachmentId, name, mimeType, src }`) — a plain, non-cancelable
notification that the preview action was activated. **Breaking in 10.0.0:** this event was
advertised as cancelable, but the chip never read `defaultPrevented` and owns no preview default
action to veto (it never registers or owns a viewer/overlay), so `preventDefault()` was a no-op.
The flag is gone rather than left as a promise the component cannot keep.

**Slots:** none.

**CSS parts:** `base`, `thumbnail`, `meta`, `name`, `size` (the formatted `bytes` count; the part
keeps its pre-rename name — it is the rendered size _text_, and renaming a part would break shipped
`::part()` rules for no gain), `status-text` (the visible text twin of
the status accent color, so the state is carried in words and not only in color; empty and hidden
for `pending`/`success`), `progress`, `progress-fill`, `spinner` (decorative/`aria-hidden` while the
adjacent `status-text` supplies the wording), `retry-button`, `preview-button`,
`remove-button`

**`status-text` carries no live-region role (public surface change).** It is plain visible text
that stays in the accessibility tree and reads normally once a user reaches the chip. The
interrupting announcement a transition _into_ `status="error"` makes — so a screen-reader user not
already focused on the chip still hears an upload failure — goes to the library's shared
**light-DOM** assertive region instead, appended to the consumer's `<body>` and marked
`data-lr-live-region="assertive"`: a live region inside a shadow root is not reliably announced
(JAWS with Firefox ignores one outright). Two consequences worth knowing:

- Only a _transition_ into `error` announces. A chip that mounts already failed is history the user
  can read at their own pace, and a retry that fails the same way twice is announced twice rather
  than being a silent no-op. The ticking `uploading` readout announces nothing at all — a live
  region re-announcing every progress tick is noise, not information.
- Read the announcement from `[data-lr-live-region="assertive"]` in the document;
  `::part(status-text)` remains the styling hook and visible-status text.

**Themeable custom properties:** `--lr-attachment-chip-accent` (default
`var(--lr-color-text-quiet)`), `--lr-attachment-chip-bg` (default `var(--lr-color-surface)`),
`--lr-attachment-chip-border` (default `var(--lr-color-border)`) — the trio's private defaults
change per `status` (`uploading` → brand/brand-quiet/transparent, `error` →
danger/danger-quiet/transparent, `success` → success/success-quiet/transparent), while an inherited
or direct public value remains authoritative; `--lr-attachment-chip-compact-thumbnail-size` (default
`1.75rem`), `--lr-attachment-chip-compact-font-size` (default `var(--lr-font-size-xs)`),
`--lr-attachment-chip-compact-gap` (default `0.25rem`) — govern the chip's thumbnail size, text
size, and internal gap while `compact` is set; `--lr-attachment-chip-spinner-duration` (default
`var(--lr-transition-ambient)`) controls the indeterminate rotation's duration and easing and stops
under reduced motion.
`--lr-attachment-chip-padding` (default `var(--lr-space-xs) var(--lr-space-s)`) and
`--lr-attachment-chip-compact-padding` (default `var(--lr-size-0-125rem) var(--lr-space-xs)`) make
`[part='base']`'s padding themeable in the resting and `compact` states respectively — both were
previously hardcoded. `--lr-attachment-chip-compact-thumbnail-only-padding` (default
`var(--lr-size-0-125rem)`, applied symmetrically to every side) governs `[part='base']`'s padding
specifically while `compact` and `thumbnail-only` together actually hide `[part='meta']` for an
image-mime attachment — reduced from the compact padding above, since the lone thumbnail no longer
needs inline padding sized for a text row that isn't rendering. It has no effect for a non-image
chip, where `[part='meta']` stays visible and the ordinary compact padding still applies.
Plus shared tokens `--lr-space-xs`, `--lr-space-s`,
`--lr-radius`, `--lr-color-text`, `--lr-color-danger`, `--lr-icon-button-size`,
`--lr-transition-fast`, `--lr-transition-base`, `--lr-focus-ring-width`,
`--lr-focus-ring-color`, `--lr-focus-ring-offset`.

**Optional peer deps:** none.

Also exported from the package root:
`formatFileSize(bytes: number, unitLabel?: (unit: 'B' | 'KB' | 'MB' | 'GB' | 'TB') => string,
numberLabel?: (value: number, fractionDigits: number) => string): string`. `unitLabel` localizes each
selected unit abbreviation; `numberLabel` formats the scaled value and receives `fractionDigits`
as `0` for bytes or `1` for larger units. Their defaults preserve the built-in output: `512` →
`"512 B"` (whole bytes never get a decimal), `2415919` → `"2.3 MB"` (every unit past bytes gets
exactly one decimal place), and a negative or non-finite input (`NaN`, `Infinity`) returns `""` so
an unknown size renders nothing instead of `"NaN B"`.

```html
<lr-attachment-chip
  name="report.pdf"
  bytes="245000"
  mime-type="application/pdf"
  status="success"
></lr-attachment-chip>
<lr-attachment-chip
  attachment-id="att-2"
  status="uploading"
  progress="42"
></lr-attachment-chip>
<script type="module">
  import { formatFileSize } from "@aceshooting/lyra-ui/components/media/attachment-chip/file-size.js";

  const chip = document.createElement("lr-attachment-chip");
  chip.file = pickedFile; // name/bytes/mime-type/thumbnail all derived from the File
  chip.addEventListener("lr-remove", (e) =>
    removeAttachment(e.detail.attachmentId)
  );
  chip.addEventListener("lr-retry", (e) => retryUpload(e.detail.attachmentId));
  chip.addEventListener("lr-preview-request", (e) => openPreview(e.detail));
  console.log(formatFileSize(pickedFile.size));
</script>
```

The image thumbnail for a real `File` is a cached `URL.createObjectURL()` blob URL, allocated in
`willUpdate()` — the update lifecycle, deliberately **never** from `render()`, so rendering stays a
pure projection of already-prepared state and URL allocation never happens as a render side effect.
It is created only when `file` is an image (or `previewable` is set), reused for as long as the same
`File` object stays assigned, and revoked when `file` changes to a different `File`, to a non-image,
or to `undefined`, and again on disconnect. Because the same pass that allocates also revokes the
previous entry, reassigning `file` several times before the next paint leaks nothing.

**Known gotchas:**

- `file` always wins over `name`/`bytes`/`mimeType` when both are set — assigning those props while
  `file` is also set has no visible effect on the rendered chip.
- A `0` `bytes` value is a known empty file and renders `"0 B"`; only omission means unknown and
  hides the `size` part. Negative and non-finite writes normalize to omission.
- `progress` only renders as a numeric bar when `status="uploading"` **and** `progress` is finite
  and `> 0`; otherwise it's either nothing (non-`uploading` status) or the indeterminate spinner
  (`uploading` with no known progress).
- `thumbnailSrc` is used whenever present regardless of `mimeType` — pass a URL that's already known
  to be an image; there's no non-image fallback check for it the way there is for `file`.
- `previewSrc` is used only when `file` is unset. A preview action is omitted when neither a real
  `File` nor `preview-src` supplies a source. The viewer's footer omits its download link for unsafe
  source schemes.

---
