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

# `lr-xml-viewer`

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

Collapsible, copyable, `DOMParser`-based tree view for XML documents, mirroring `lr-json-viewer`'s
UX (`collapsed-depth`, `copyable`, structural-path-keyed expand state that survives a same-shape
`xml` reassignment) adapted for XML's own node kinds: elements with attributes, text, comments, CDATA
sections, and processing instructions, preserved in their original mixed-child source order.
Namespace-literal: qualified names render exactly as authored, with no namespace-URI-aware
matching. Every document type declaration is rejected before `DOMParser`, preventing external
entity access and browser-specific internal-entity expansion. Not `lr-json-viewer` (JS values); not `lr-html-viewer`
(sanitized _rendered_ HTML). No XPath/XSLT evaluation, no editing, no schema validation.

The registration entry also matches extensionless `application/*+xml` MIME essences, ignoring
parameters, case and outer whitespace. Exact registered MIME keys retain precedence over this
fallback matcher.

Importing `xml-viewer.js` (this component's own registration entry) loads `<lr-xml-viewer>`'s class
module immediately. A granular consumer (not importing the `all.js` compatibility bundle) who wants
that deferred too can instead import
`@aceshooting/lyra-ui/components/viewers/xml-viewer/xml-viewer-register.js`, which installs the
same registration lazily and exports `XML_VIEWER_TAG` (`'lr-xml-viewer'`) as a stable reference to
the tag it eventually registers.

**Properties:** `src: string = ''` — URL to fetch and parse; ignored once `xml` is set. `xml?:
string` (property only) — raw XML text to parse and render; wins over `src`, and setting it parses
synchronously. Assigning `undefined` relinquishes inline authority and immediately resumes an
already configured `src`, or exposes idle when none exists. `source: LyraXmlViewerSource` is the
readonly discriminated effective authority (`{ kind: 'inline', value }`, `{ kind: 'url', url }`, or
`null`). `name: string = ''` — accessible label. `collapsedDepth?: number` (attribute
`collapsed-depth`) — elements at or beyond this nesting depth (root = 0) start collapsed. `copyable:
boolean = false` (reflected) — shows copy-to-clipboard affordances, one for the whole document plus
one per element. `maxHeight: string = ''` (attribute `max-height`). `anchorKinds: readonly
LyraAnchorKind[] = ['node-path']` (this viewer's supported `LyraAnchor.kind` values for the shared anchor-target
contract) — each numeric path segment is the 0-based index within the parent's _element_ children,
and an optional trailing string segment `'@attrName'` addresses one existing, nonempty-named
attribute. Invalid CSS `max-height` values, declaration breaks, and `url()` are ignored.

**Methods:** `search(query)` resolves the match count via a case-insensitive substring search over
every element's tag name, attribute names/values, and own text (empty/whitespace query behaves like
`clearSearch()`), accepting at most 4,096 query code units and scanning at most 4,000,000 code
units while retaining 10,000 matches; `searchNext()`/`searchPrevious()` advance/step back through matches (wrapping);
`clearSearch()` clears the query and matches. Imperative `search()`, `searchNext()` and
`searchPrevious()` reopen the selected match and its ancestors, including manually collapsed
nodes, before scrolling. Other explicit collapse choices remain in effect, and later manual
collapse still takes precedence over default expansion. All three resolve only after the newly
active match's row has been scrolled into view (`block: 'center'`, `behavior: 'auto'` under
`prefers-reduced-motion`) — before 9.0.0 they moved `data-active-match` without ever scrolling, so
on a document taller than the viewport the reader never saw the match they had stepped to. Replacing
the XML source clears document-relative matches and emits the canonical empty `lr-search-change`;
changing the effective locale re-evaluates and emits the retained query.

**Highlights:** host-supplied `highlights` are first-class here, not carried and ignored. Every entry
whose anchor is a `node-path` this document resolves tints its element row — `[part='node']` gains
`data-highlight` carrying the entry's `tone` (`accent` when omitted) — and adds a focusable
`[part='highlight-action']` button that emits `lr-highlight-activate`. The button's accessible name
is the entry's own `label` when supplied, otherwise a localized "Highlight n of m". `activeHighlightId`
adds `data-active-highlight` to the matching row. Entries are deduplicated by `id`; an entry whose
anchor kind or path this document cannot resolve is dropped whole rather than painted at some
coarser granularity; missing and non-array `path` values are included in that invalid case. An entry
inside a collapsed subtree paints once that subtree is expanded.
Painting retains at most 100 resolved entries from a 1,000-entry candidate window; an active entry
anywhere in the bounded 10,000-record host snapshot is placed first inside both ceilings.

**Events:** `lr-copy` — emitted only after the owning realm's clipboard write fulfills, with
`detail: { ok: true, text }`. Clipboard absence, synchronous throws, and rejected writes instead
show the localized `copyFailed` label and emit generic `lr-error` plus `lr-copy-error` with
`detail: { ok: false, text, reason, error }`; `reason` is `'unsupported'`, `'denied'`, or `'failed'`.
`lr-search-change` — `detail: { query, matchCount, matchCountExact, activeIndex }`.
`lr-render-error` — `detail: { error }`, fetching or parsing failed, including a
parse error or exceeding the node cap. `lr-anchor-result` — non-cancelable; `detail: { found:
boolean }`, fired after an `anchor` assignment or a `scrollToAnchor()` call is applied.
`lr-highlight-activate` — non-cancelable; `detail: { highlightId }`, fired when a highlight's
`[part='highlight-action']` button is activated by click or Enter/Space. `lr-text-select` is not
part of this structural tree viewer's event contract because it installs no selection binding.

**CSS parts:** `base`, `toolbar` (the whole-document copy button row, only when `copyable`),
`copy-button` (the whole-document one, or a per-node one), `tree`, `node` (`data-active` while it's
the resolved anchor target, `data-match`, `data-active-match`, `data-highlight` carrying a resolved
highlight's tone, `data-active-highlight`), `tag` (`data-match`), `attribute` (`data-active` while a
`node-path` anchor's trailing `'@attrName'` segment addresses that specific attribute — so a citation
pointing at one attribute value of a multi-attribute element stays identifiable in the rendered
tree, rather than resolving indistinguishably from the bare element path),
`attribute-name`, `attribute-value` (`data-match`), `text` (`data-match`), `comment`, `cdata`, `pi`,
`toggle` (an element's expand/collapse button on nodes with renderable children; its collapsed
chevron mirrors under effective RTL direction), `highlight-action` (the focusable button a resolved
`highlights` entry adds to its element row), `toggle-placeholder` (the empty toggle-column spacer on
leaf rows), `error`, `spinner`, and `anchor-live-region` (an aria-hidden, non-live shadow mirror of
the latest anchor-jump message; the spoken copy is appended to the shared document-level polite sink
only while the viewer and its composed ancestors are exposed to the accessibility tree).
The spinner always includes visible localized loading text alongside its decorative ring; the text
remains understandable without CSS or animation and the ring stops under reduced motion.

**Themeable custom properties:** `--lr-xml-viewer-max-height` (default `none`) — maximum block size
of the scrollable body; also settable via the `max-height` property.
`--lr-xml-viewer-active-match-color` (default `var(--lr-color-warning)`) — the solid outline on the
`[part='node']` holding the _current_ search match, leaving every other match on its dashed
`--lr-xml-viewer-match-color` outline. 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(node)[data-active-match]` is invalid CSS — Shadow Parts forbids an attribute selector after
`::part()` — so distinguishing the active match previously meant re-pointing the shared
`--lr-color-warning` token, which recolored every other match (and every other warning surface)
along with it. Unset, it falls back to that token, so rendering is unchanged.

`--lr-xml-viewer-match-color` (default `var(--lr-color-warning)`) — outline color of a non-active
`[part='node']` search match, and (via `color-mix`) the tint source for a matching `[part='text']`'s
background — kept distinct from `--lr-xml-viewer-active-match-color` so the non-active matches can
be recolored without touching the active one. `--lr-xml-viewer-match-bg` (default
`var(--lr-color-warning-quiet)`) — background of a matching `[part='tag']`/`[part='attribute-value']`.
Both are inline `var()` fallbacks at the point of use, so either can be set on the element or any
ancestor; unset, they fall back to the same shared tokens the rules used before.

`--lr-xml-viewer-highlight-accent-background` (default `var(--lr-color-brand-quiet)`),
`--lr-xml-viewer-highlight-success-background` (default `var(--lr-color-success-quiet)`),
`--lr-xml-viewer-highlight-warning-background` (default `var(--lr-color-warning-quiet)`),
`--lr-xml-viewer-highlight-danger-background` (default `var(--lr-color-danger-quiet)`) and
`--lr-xml-viewer-highlight-neutral-background` (default `var(--lr-color-surface-raised)`) are the row
backgrounds of a resolved `highlights` entry per tone. The neutral default is deliberately
`--lr-color-surface-raised` and not `--lr-color-surface`: the viewer paints its own surface with the
latter, so a neutral highlight tinted with it would render as unhighlighted.
`--lr-xml-viewer-highlight-active-outline` (default `var(--lr-color-brand)`) outlines the entry named
by `activeHighlightId`, and `--lr-xml-viewer-active-attribute-color` (default `var(--lr-color-brand)`)
outlines the `[part='attribute']` an attribute-addressing `node-path` anchor resolved to.
`--lr-xml-viewer-tag-color` (default `var(--lr-color-brand)`) retints every rendered `[part='tag']`
element name, independent of that outline.

`[part='toggle']`'s glyph box stays compact (`1.25rem`) while its _interactive_ box takes the shared
minimum target size as a floor via `--lr-icon-button-size`. That token is a floor, not a fixed size,
so lowering it never squashes the chevron below its own box — the visible glyph keeps its size while
the hit target follows the token, and it can never fall under the accessible minimum from this
component's own rules. A collapsed chevron mirrors when effective direction is RTL (including an
inherited `dir` change); an expanded chevron points down in either direction.

```ts
const viewer = document.querySelector("lr-xml-viewer");
viewer.xml = payload;
viewer.collapsedDepth = 2;
viewer.copyable = true;
await viewer.search(query);
```

Node cap: 50,000 — exceeding it renders the localized `xmlViewerTooManyNodes` error instead of the
tree. A collapsed element's child count includes element, text, comment, CDATA, and processing-
instruction children rather than only element descendants.
