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

# `lr-document-preview`

- **Import** `import '@aceshooting/lyra-ui/components/lr-document-preview.js';` (stable tag alias; registers the tag)
- **Class** `LyraDocumentPreview`, also available unregistered from `@aceshooting/lyra-ui/components/viewers/document-preview/document-preview.class.js`
- **Family** `components/viewers/` — 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)
- **Deprecations** none
- **Optional peers** none
- **Themeable via** 18 parts, 12 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-document-preview`

A format-dispatching viewer for one document/attachment, plus the visual state machine for an async
server-side conversion a host app runs in front of it. First-party invention (no Web Awesome
equivalent).

Fitting images remain centered; overflowing nonzoomable images begin inside the reachable body
scroll range, so their top and bottom are available.

Format dispatch is intentionally minimal: only `text/*`/`application/json` (a plain, scrollable
`<pre>` — no syntax highlighting; compose `<lr-code-block>` yourself via the `unsupported` slot for
that) and `image/*` (a contained `<img>`) render inline. Everything else — PDF, office documents,
video, audio, or any unrecognized MIME type — falls back to a generic "can't preview this" state: a
file glyph, a short message, and (when `src` is set) a native `<a download>` link. This is a
deliberate ceiling, not a gap: the component ships a dispatch _shell_, not a format registry. The
`unsupported` slot is the escape hatch for every format left out of the built-in three.

`status="converting"` is a second, independent axis from format dispatch. This component doesn't know
your backend's conversion API shape and owns none of the actual polling/fetch — a host converting a
non-natively-previewable format server-side (e.g. `.docx` → `.pdf`) polls its own backend and updates
`status`/`progress`/`src` here as that proceeds; this component only _visualizes_ that state (an
indeterminate spinner, or a determinate one once `progress` is supplied). The one piece of async work
this component _does_ own is fetching a `text/*`/`application/json` `src` itself — there's no other
way to get a `<pre>`'s text content from a URL — gated behind a generation-counter guard
(`lr-tool-result-view`'s `resolve()` uses the identical pattern) so a `src` reassigned mid-fetch
can't have a stale response clobber a newer one.

Every `src` is validated (via `internal/safe-url.ts`) against a scheme allowlist specific to the DOM/
API sink it's about to reach: `fetch(src)` (text preview) and an `<img src>` (image preview) both
allow relative URLs plus `http:`, `https:`, `blob:`, and `data:`; the download `<a href>` deliberately
excludes `data:` (following a `data:text/html` URL can create an active document, unlike using it as
inert media/fetch data). A `src` that fails its sink's check never reaches `fetch()`/`<img>`/the
anchor: the text preview renders `[part="error"]` with `"Document URL is not allowed."`, the image
preview silently falls back to the download fallback **directly** — not the generic
download-or-`unsupported`-slot one, so `<slot name="unsupported">` content is _not_ consulted on the
image path — and the generic fallback simply omits `[part="download-link"]` entirely.

**Properties:**

- `src: string = ''` — URL to fetch (for `text`/`application/json`) or display (`image`, or as the
  generic fallback's download `href`). Optional — gracefully absent while, e.g., a conversion is
  still in progress. Validated per-sink before use — see the URL-safety note above; an unsafe/
  malformed value is treated as if `src` were unusable for that sink, never passed to `fetch()`/
  `<img>`/the anchor.
- `mimeType: string = ''` (attribute `mime-type`) — drives format dispatch using the case-insensitive
  MIME essence before parameters. For example, `application/json; charset=utf-8` renders as text.
  Attribute removal retains `null` readback and is interpreted as an absent format by the renderer;
  an explicitly empty attribute remains empty, and later valid values restore dispatch.
- `filename: string = ''` — shown in the header and used as the download link's suggested filename.
- `alt?: string` — image description. When omitted, the filename/localized image-preview fallback
  is used; an explicit empty string keeps a decorative preview's `alt=""` intact.
- `status: 'idle' | 'converting' | 'ready' | 'error' = 'idle'` (reflected) — host-owned lifecycle
  state. `"converting"` shows the spinner regardless of `mimeType`/`src`; `"error"` shows
  `errorText` regardless of either. `"idle"`/`"ready"` both resume normal format dispatch — a host
  with no conversion step never has to explicitly set `"ready"`.
- `progress?: number` (type `Number`) — 0-100. Only consulted while `status="converting"`. Unset (the
  default) renders an indeterminate spinner instead of a determinate progress bar.
- `errorText: string = ''` (attribute `error-text`; spelled `errorMessage`/`error-message` before
  9.0.0) — shown via `[part="error"]` while `status="error"`. Caller-supplied text, not localized;
  left empty, the localized `documentPreviewGenericError` fallback shows instead.
- `maxHeight: string = ''` (attribute `max-height`) — a CSS length (e.g. `"24rem"`); once set,
  `[part="body"]` scrolls internally past this height instead of growing the page — same contract as
  `lr-json-viewer`'s identically-named prop. Invalid CSS `max-height` values, declaration breaks,
  and `url()` are ignored, leaving the stylesheet token in control.
- `zoomable: boolean = false` (reflected) — wraps the rendered image (image format only) in an
  internal `<lr-pan-zoom>`. `false` (the default) preserves the exact pre-`zoomable` DOM — an
  inline thumbnail (e.g. in a chat stream) must not unexpectedly grow a focusable zoom-chrome
  viewport; an inspection surface opts in.
- `suppressDownload: boolean = false` (attribute: false) — omits the generic fallback's download
  action when a composing shell already owns that action. This is property-only composition state;
  it does not suppress inline preview rendering.
- `highlights: readonly LyraHighlight[] = []` (attribute: false) — display-only `region` highlights painted
  over the image-format preview; ignored for the `text`/`generic` formats. IDs are trimmed and
  required to be nonempty, with the first record retained when IDs repeat. A rectangle renders only
  when `x`/`y`/`width`/`height` are finite numbers and both dimensions are nonnegative. Image-region
  geometry is captured when `highlights` is assigned: an invalid or accessor-backed `region` anchor
  is ignored, and mutating an accepted anchor later does not move its painted region. Reassign
  `highlights` to update it; the original public anchor identity remains available for
  reference-based `scrollToAnchor()`.
- `activeHighlightId: string | null = null` (attribute `active-highlight-id`) — the `highlights`
  entry, if any, currently treated as active (`data-active` on its `region-highlight`).
- `anchorKinds: readonly LyraAnchor['kind'][] = ['region']` (this viewer's supported
  `LyraAnchor.kind` values for the
  shared anchor-target contract).

**Methods:** `scrollToAnchor(target)` — scrolls a `region` highlight (by id, or a `LyraAnchor`
matched back to its owning `LyraHighlight` by reference) into view; resolves `false` when nothing
matches, the anchor isn't `region`, or the format isn't currently `image`.

**Events:**

- `lr-download` — `detail: { src, filename }` — fired when the generic-download fallback's link is
  activated. The browser download itself needs no JS (a plain `<a download>` handles it); this is
  purely for a host that wants to observe/log the download.
- `lr-render-error` — `detail: { error }` — fired when this component rejects an unsafe text/JSON
  URL or when its own `text/*`/`application/json` `fetch(src)` fails (network error or non-2xx
  response). Distinct from `status="error"`, which is entirely host-driven.
- `lr-highlight-activate` — `detail: { highlightId }` — a region highlight was clicked or activated via
  Enter/Space (image format only).

**Slots:** `unsupported` — escape hatch: when populated, its content renders _instead of_ the generic
download fallback for any `mime-type` this component doesn't natively support. Ignored while
`mime-type` resolves to `text`/`image` dispatch, or while `status` is `"converting"`/`"error"`.

**CSS parts:** `base`, `header` (hidden entirely when `filename` is unset), `filename`, `body`,
`spinner` (ordinary non-live shadow content while indeterminate, or `role="progressbar"` once
numeric progress is known — used both for `status="converting"` and this component's own in-flight
text fetch), `error` (ordinary visible shadow text used both for `status="error"` and a failed text
fetch; error transitions use the shared document-level assertive sink), `download-link` (only
rendered when `src` is set _and_ passes the link-safe scheme allowlist — see the URL-safety note
above; excludes `data:` even though the other two sinks allow it), `highlight-layer` (wrapper around
every rendered region highlight, image format only), `region-highlight` (one region highlight,
`data-tone`, `data-active`; image format only), `region-highlight-target` (transparent activation
geometry with a minimum hit area independent of the visual rectangle), `highlight-actions`
(non-overlapping actions used when multiple minimum hit areas would overlap),
`region-highlight-action` (one action in that list), `frame-viewport`/`frame-content`/`frame-controls`/
`frame-zoom-in`/`frame-zoom-out`/`frame-reset` (forwarded from the internal `<lr-pan-zoom>`
while `zoomable`; image format only)

**Themeable custom properties:** `--lr-document-preview-max-height` (default `none`) — the
consumer-tunable scroll cap on `[part="body"]`, set from `max-height`; `none` means the preview grows
with its content until a caller opts in. `--lr-document-preview-font` (default
`var(--lr-font-mono)`, so a themed monospace stack reaches plain-text previews with no
per-component override) and `--lr-document-preview-spin-duration` (default
`var(--lr-transition-ambient)`, stopped under reduced motion). `--lr-document-preview-progress`
(default `0`) — a unitless
0–100 number the determinate spinner's `conic-gradient` fill reads; written inline on the ring by
the component itself from the clamped `progress` property, so overriding it only makes sense to
repaint the fraction. `--lr-document-preview-active-border` (default
`var(--lr-color-warning, var(--lr-color-brand))`) — the border color of the `[part='region-highlight']`
matching `activeHighlightId` (image format only), deliberately distinct from the resting highlight
border so the active region can be recolored without touching the rest. Like the library's other
state hooks it is an inline `var()` fallback at the point of use rather than a `:host` declaration,
so it can be set on the element or on any ancestor — `::part(region-highlight)[data-active]` is
invalid CSS (Shadow Parts forbids an attribute selector after `::part()`), which previously left
re-pointing the shared `--lr-color-warning`/`--lr-color-brand` tokens as the only lever, repainting
every other element that read them. The tone-specific resting border and hover tint use
`--lr-document-preview-highlight-accent-color`, `--lr-document-preview-highlight-success-color`,
`--lr-document-preview-highlight-warning-color`, `--lr-document-preview-highlight-danger-color`,
and `--lr-document-preview-highlight-neutral-color` (defaulting respectively to the matching
brand/success/warning/danger/neutral color tokens). `--lr-document-preview-download-link-hover-bg`
defaults to `color-mix(in oklab, var(--lr-color-brand), var(--lr-color-mix-partner)
var(--lr-color-mix-hover))`; `--lr-document-preview-download-link-active-bg` uses the same mix with
`var(--lr-color-mix-active)`, styling the native download action's hover and pressed backgrounds.
Plus shared tokens
`--lr-color-border`, `--lr-radius`, `--lr-color-surface`, `--lr-space-s/-m/-l/-xs`,
`--lr-color-text`, `--lr-color-text-quiet`, `--lr-color-danger`, `--lr-color-brand`,
`--lr-color-on-brand`, `--lr-focus-ring-width/-color/-offset`, `--lr-transition-fast`.

**Optional peer deps:** none directly — the `unsupported` slot is commonly used to compose
`<lr-code-block>` (which has its own optional `shiki` peer dependency; see that component's own
entry) or a third-party PDF/office-doc viewer, but neither is a dependency of this component itself.

```html
<lr-document-preview
  id="text-preview"
  filename="board-notes.txt"
  mime-type="text/plain"
  src="/files/board-notes.txt"
  max-height="24rem"
></lr-document-preview>

<!-- A host driving its own server-side conversion -->
<lr-document-preview filename="deck.pptx" status="converting" progress="42"></lr-document-preview>

<!-- Escape hatch for an unsupported format -->
<lr-document-preview filename="deck.pptx" mime-type="application/vnd.ms-powerpoint" src="/files/deck.pptx">
  <lr-code-block slot="unsupported" language="text">Open in PowerPoint to preview.</lr-code-block>
</lr-document-preview>
<script type="module">
  document
    .getElementById("text-preview")
    .addEventListener("lr-render-error", (e) => console.error(e.detail.error));
</script>
```

Accessibility: after the initial silent baseline, entering `"converting"` without numeric
`progress` appends the localized "Converting document…" transition to the pre-mounted shared
document-level polite sink; the visible spinner and its visually-hidden label remain ordinary,
non-live shadow content. Once finite `progress` is available, the spinner becomes a standard
`role="progressbar"`, self-describing via `aria-valuenow`, and does not duplicate that information
through the live sink. A later finite-to-indeterminate transition is announced. `status="error"`
keeps `[part="error"]` as ordinary visible text and appends later error transitions to the shared
document-level assertive sink.

**Known gotchas:**

- `status="converting"`/`status="error"` always win over format dispatch, regardless of
  `mimeType`/`src` — a `text`/`image` source is not shown until `status` returns to `"idle"`/`"ready"`.
- The component's own text/JSON `fetch(src)` is a _different_ async operation from the host-driven
  `status="converting"` conversion. A failed fetch fires `lr-render-error` and renders
  `[part="error"]` on its own; it never sets `status="error"` itself.
- changing `src` aborts the superseded text fetch as well as ignoring any stale completion; removing
  the component aborts its active request.
- `progress` is only ever consulted for the host-driven `"converting"` state — this component's own
  in-flight text fetch always renders the indeterminate spinner, with no numeric-progress path.
- the text/JSON fetch is capped at 25 MB, enforced while streaming so it holds even when the server
  omits `Content-Length`; going over renders `[part="error"]` with the localized
  `documentPreviewResourceTooLarge` message. The cap is not overridable per component. The `image`
  preview is unaffected — it hands `src` to an `<img>` rather than reading it.
- The `unsupported` slot's initial presence is detected once, before the first render, by scanning
  light-DOM children directly (not the slot's `assignedElements()`); every later slot-content change
  is instead picked up via the slot's own `slotchange` listener. Both paths keep detection correct,
  just via two different mechanisms depending on timing.
- `download-link` (and thus `lr-download`) only renders/fires when `src` is set — a generic-fallback
  state with no `src` shows only the file glyph and message, with no download affordance at all.
- `download-link` also never renders for a `data:` or `mailto:` URL, even though `data:` is accepted
  for text/image sinks and `mailto:` is accepted for navigation anchors elsewhere in the library — a
  `src="data:..."` document renders/fetches fine but falls back to no download affordance in the
  generic state, and a `mailto:` names no retrievable bytes so it cannot be a download target at all.
- A `src` that fails its sink's URL-safety check raises `lr-render-error` for the text/JSON path,
  with the localized URL-rejection error; the event is also raised when an allowed `fetch()` fails
  at the network layer. An unsafe/malformed image or download `src` is instead treated as unusable
  without an event (a silent image fallback or omitted download link — and on the image path that
  fallback is the download fallback directly, so an `unsupported` slot the host supplied is
  bypassed).

---
