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

# `lr-document-viewer`

- **Import** `import '@aceshooting/lyra-ui/components/lr-document-viewer.js';` (stable tag alias; registers the tag)
- **Class** `LyraDocumentViewer`, also available unregistered from `@aceshooting/lyra-ui/components/viewers/document-viewer/document-viewer.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** 2 parts, 5 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-viewer`

A dialog-hosted, format-dispatching full viewer for one document or attachment. It uses a pluggable
renderer registry and falls back to `<lr-document-preview>` when no renderer matches the file's MIME
type. First-party invention.

MIME matching uses the case-insensitive essence before parameters and ignores outer whitespace. An
extensionless `application/*+xml` file selects the XML renderer when it is registered, including a
value such as ` APPLICATION/ATOM+XML ; charset=utf-8 `. Exact registry keys retain precedence,
filename extensions remain a fallback, and the renderer receives the original MIME value.

A host `aria-label` names the nested dialog by attribute presence, including an explicitly empty
value, without suppressing the visible `name` heading.

**Properties:**

- `open: boolean = false` (reflected) — opens or closes the viewer dialog.
- `name: string = ''` — display name passed to the renderer and used as the dialog heading.
- `mimeType: string = ''` (attribute `mime-type`) — MIME type used for exact renderer dispatch.
- `src: string = ''` — source URL passed to the selected renderer or the fallback preview.
- `payload?: LyraDocumentRendererPayload` (attribute: false) — opt-in renderer-specific input.
  Assignment immediately clones, validates, bounds, and freezes the payload. While set,
  `payload.file` is authoritative for MIME dispatch, the dialog heading, renderer/fallback input,
  anchors/highlights, and download; the scalar `name`, `mimeType`, `src`, `anchor`, `highlights`,
  and `alt` properties resume their legacy behavior when `payload` is reset to `undefined`.
- `registry?: DocumentRendererRegistry` (attribute: false) — optional per-instance registry
  override. Assign either a native map or the result of `createDocumentRendererRegistry()`; both
  use the same bounded snapshot behind a frozen readonly facade. Definition records are cloned and
  frozen while callback identities are retained. Later source-map or definition mutation is not
  observed. Reassigning the same source or retained snapshot leaves the snapshot unchanged. When unset, the
  instance owns an immutable snapshot of the built-ins registered when it was constructed. A later
  module import/registration cannot mutate an existing viewer. A throwing consumer matcher or
  renderer is contained as the localized error state rather than escaping the update.
- `alt?: string` — media alt text forwarded to the resolved renderer, for image-like renderers.
  Unset lets the renderer derive its fallback; an explicit `''` preserves decorative media.
- `anchor: LyraAnchor | string | null = null` (attribute: false) — declarative scroll-to-anchor
  target forwarded to the resolved renderer; a string is a highlight id in `highlights`.
  `hasChanged: () => true`, so re-assigning the same value (e.g. re-clicking the same citation
  badge) still re-fires.
- `highlights: readonly LyraHighlight[] = []` (attribute: false) — highlights forwarded to the resolved
  renderer after the shared trimmed, nonempty, first-wins identity normalization.

**Events:**

- `lr-close` — `detail: DocumentViewerCloseReason`, the viewer shell dialog's dismissal reason.
  The event is emitted after the viewer sets `open` to `false`. A registered renderer may compose
  its own descendant dialog; closing that inner dialog keeps its own `lr-close` path — the shell
  guards on `event.target !== event.currentTarget` — and does not close the document viewer.
  (`<lr-dialog>`'s close event was spelled `lr-dialog-close` before 10.0.0.)
- `lr-download` — `detail: { src, filename }`, emitted when the native safe download action is
  activated. The browser download itself is handled by the link.
- `lr-anchor-result` — `detail: { found }`. Emitted by this shell as `{ found: false }` once per
  applied `anchor` when a resolved renderer can't honor it (it declares no `capabilities.anchors`,
  or none matching the anchor's `kind`). When the file uses `<lr-document-preview>`—including after
  a lazy renderer fails to load—the shell delegates to that preview's `scrollToAnchor()` and emits
  its actual `found` result. A capable renderer instead emits its own `lr-anchor-result` from its
  embedded `DocumentAnchorTarget` mixin, which composes up through this element unchanged — the
  shell stays silent in that case, so the event fires exactly once either way. A string `anchor`
  (a highlight id) counts as supported by any renderer declaring at least one anchor kind.
- `lr-render-error` — `detail: { error }`. The fallback preview or an embedded renderer emits this
  when fetching, parsing, sanitizing, or rendering fails; the composed event reaches the document
  viewer unchanged.

**CSS parts:** `body` — wrapper around the active renderer, loading/error state, or fallback preview;
it renders explicit `aria-busy="true"|"false"`. Visible loading/error text is ordinary non-live
shadow content; later loading and error transitions use the pre-mounted shared document-level
polite and assertive sinks, respectively;
`download-link` — the native download action, rendered when `src` passes Lyra's safe-link policy.

**Themeable custom properties:** `--lr-document-viewer-max-height` (default `70vh`) — maximum block
size of `[part="body"]` before the dialog body scrolls internally.
`--lr-document-viewer-min-height` (default `var(--lr-size-12rem)`) — minimum block size of
`[part="body"]` before it grows to fit content.
`--lr-document-viewer-width` (default `auto`) — assertive inline size for the nested dialog panel,
forwarded straight to `<lr-dialog>`'s own `--lr-dialog-width`; set this on `<lr-document-viewer>`
itself rather than reaching through to `--lr-dialog-width` directly, which stays an internal detail
of the nested dialog. Left at `auto` the panel keeps shrink-wrapping to content, capped by the
dialog's own `--lr-dialog-max-width` tier.
`--lr-document-viewer-download-link-hover-bg` defaults to `color-mix(in oklab,
var(--lr-color-brand), var(--lr-color-mix-partner) var(--lr-color-mix-hover))`, and
`--lr-document-viewer-download-link-active-bg` uses `var(--lr-color-mix-active)` in that same mix
for the native download action's hover and pressed backgrounds.

**Renderer registry exports:**

- `DocumentFile` — the compatible mutable lookup input passed to `matches(file)` callbacks:
  `{ name, mimeType, src, anchor?, highlights?, alt? }`. Adapter and legacy-render boundaries
  receive an immutable file snapshot whose highlights use the shared trimmed, nonempty,
  first-wins identity projection.
- `LyraDocumentFile` — the readonly file snapshot wrapped by every discriminated payload.
- `LyraDocumentRendererPayload` — readonly `kind: 'document' | 'av'` discriminated input wrapping an
  immutable `file`. The AV branch adds readonly `cues` and `tracks`; snapshots retain at most 10,000
  cues and 64 tracks, clone and freeze every retained record, and bound every retained string.
- `LyraGenericDocumentRendererPayload` / `LyraAvDocumentRendererPayload` — the concrete
  `kind: 'document'` and `kind: 'av'` branches. `LyraDocumentRendererPayloadKind` names their kind
  union, and `LyraDocumentRendererPayloadFor<K>` extracts one branch for adapter authoring.
- `LyraDocumentRendererAdapterDefinition<K>` / `LyraDocumentRendererAdapter` — strongly typed
  authoring input and its factory-created, type-erased registry form. The callbacks adapt the legacy
  file to one payload kind, derive capabilities from that retained payload, and render it.
- `DocumentRendererDefinition` (also `LyraDocumentRendererDefinition`) — a validated direct
  `{ render, matches?, capabilities? }`, adapted `{ adapter, matches? }`, or lazy
  `{ load, matches?, capabilities? }` definition; exactly one of `render`, `adapter`, and `load` is
  required. Static capabilities are rejected on adapted definitions so their adapter remains the
  single source of truth. Adapted definitions register eagerly; a lazy definition resolves a
  legacy direct renderer, keeping the static and payload-derived capability branches distinct.
- `LyraAdaptedDocumentRendererDefinition` / `LyraResolvedDocumentRendererDefinition` — the adapted
  registry branch and the union of both immediately renderable branches. `LyraAdaptedDocumentRenderer`
  is the frozen payload/capabilities/render invocation returned by `adaptDocumentRenderer()`.
- `DocumentRendererRegistry` — `ReadonlyMap<string, DocumentRendererDefinition>`.
- `createDocumentRendererAdapter(definition)` — preserves discriminator-specific callback types
  while producing the validated adapter accepted by a registry definition.
- `adaptDocumentRenderer(definition, file, payload?)` — binds one resolved definition to an
  immutable payload and derives its frozen capabilities. It snapshots the file once and passes
  that canonical object to either the adapter or legacy renderer; caller object identity is not
  retained across this boundary.
- `snapshotLyraDocumentRendererPayload(payload)` — returns the same validated, bounded, frozen
  assignment snapshot used by `<lr-document-viewer>`.
- `createDocumentRendererRegistry(overrides?)` — returns a truly immutable built-in snapshot plus
  optional per-instance overrides. MIME keys are trimmed, lowercased, and reduced to their essence.
- `registerDocumentRenderer(key, definition)` — adds or replaces a built-in-builder entry for
  registry snapshots created later; it never mutates existing instances.
- `findDocumentRenderer(file, registry?)` — checks normalized MIME essence (case-insensitive and
  parameter-independent), then the first matching `matches()` entry in registration order.
- `loadDocumentRenderer(definition)` — resolves and identity-caches a lazy direct definition;
  rejected loads are retried on the next call.

Every built-in kind ships a lazy, register-only entry named `<kind>-viewer-register.js`
(`archive-viewer-register.js`, `ebook-viewer-register.js`, `pdf-viewer-register.js`,
`docx-viewer-register.js`, `pptx-viewer-register.js`, `spreadsheet-viewer-register.js`,
`csv-viewer-register.js`, `xml-viewer-register.js`), which installs that kind's registration
without pulling its element class module into the importing graph until a matching file is
actually opened, and exports a `<KIND>_VIEWER_TAG` string constant naming the tag it eventually
registers. `document-viewer/document-viewer-kinds.js` imports and re-exports all eight at once, for
a consumer who wants every built-in kind available lazily without importing each entry
individually. `<lr-document-viewer>` itself (`document-viewer.js`) is always a separate import.

```html
<lr-document-viewer
  open
  name="report.pdf"
  mime-type="application/pdf"
  src="/files/report.pdf"
></lr-document-viewer>
```

Construct and inject a renderer registry per application/viewer. The definition may load a heavy
optional viewer only when a matching document is opened:

```ts
import { createDocumentRendererRegistry } from "@aceshooting/lyra-ui/components/viewers/document-viewer/registry.js";

const registry = createDocumentRendererRegistry([
  ["application/x-example", { render: (file) => `Preview: ${file.name}` }],
]);

html`<lr-document-viewer .registry=${registry}></lr-document-viewer>`;
```

Supply AV metadata without widening every legacy renderer callback. The built-in AV adapter always
declares `time-range` anchors and declares search only when the retained cue snapshot contains
non-whitespace transcript or speaker text:

```ts
import type { LyraDocumentRendererPayload } from "@aceshooting/lyra-ui/components/viewers/document-viewer/registry.js";

const payload = {
  kind: "av",
  file: { name: "episode.mp4", mimeType: "video/mp4", src: "/episode.mp4" },
  cues: [{ cueId: "intro", start: 0, text: "Welcome", speaker: "Host" }],
  tracks: [
    {
      src: "/episode-en.vtt",
      kind: "captions",
      srclang: "en",
      label: "English",
    },
  ],
} satisfies LyraDocumentRendererPayload;

html`<lr-document-viewer open .payload=${payload}></lr-document-viewer>`;
```

When no renderer matches, the viewer renders `<lr-document-preview>`, which handles text and images
inline and provides a safe generic fallback for other formats.

If a consumer matcher/renderer throws while an anchor is pending, the viewer renders localized
ordinary error text, appends the transition to the shared document-level assertive sink, and emits
exactly one `lr-anchor-result` with `{ found: false }`.
