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

# `lr-include`

- **Import** `import '@aceshooting/lyra-ui/components/lr-include.js';` (stable tag alias; registers the tag)
- **Class** `LyraInclude`, also available unregistered from `@aceshooting/lyra-ui/components/viewers/include/include.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** `dompurify` — see `llms/peers.md`
- **Themeable via** 2 parts, 0 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-include`

Loads an HTML fragment from `src` and transcludes it as sanitized **light-DOM** content, so the
fragment participates in the surrounding page's CSS cascade like a native server-side include —
unlike `<lr-html-viewer>`, which renders a foreign document inside an isolated preview card. The
markup always passes through the shared DOMPurify-backed sanitizer before it reaches `innerHTML`;
there is deliberately no `allow-scripts`-style escape hatch (the Web Awesome/Shoelace equivalents'
raw injection option is omitted, not shipped as a no-op).
The post-sanitization transclusion profile is network-silent and non-interactive: anchors are
retained, but only resolvable same-document `#fragment` links survive and those ids are rebased per
include instance. Other navigation and resource attributes such as `href`, `src`, `srcset`,
`action`, `ping`, and `poster` are removed, so images never load. Form controls and custom elements
cannot remain interactive; their wrappers are unwrapped when their ordinary children are safe,
while elements such as inputs that have no passive content are removed.

The same passive restrictions apply through nested template contents before a remote document is
retained in the cache or a selected fragment becomes live. Template text and fragment selection
remain supported at nested depths, including ordinary elements inside templates; permitted local
anchors still resolve against per-instance rebased IDs.

A bare primitive: no label/hint/error chrome, no implicit role, no computed accessible name, and no
`aria-live` wrapper (the fragment can carry its own landmarks; wrapping the host would re-announce
all of it on every load). The host always carries explicit `aria-busy="true"|"false"`: true while
the source is loading and being sanitized. Build error UI from `lr-include-error`.

**Properties:**

- `src: string = ''` (reflected) — source of the fragment. `#id` clones the matching same-page
  template content or element children without fetching or moving the source nodes.
  `/partial.html#id` fetches `/partial.html` without its hash, sanitizes the complete response, then
  clones the target's children; a URL without a hash transcludes the complete sanitized document.
  Remote URLs pass the shared `safeFetchUrl()` allowlist (`http:`, `https:`, `blob:`, `data:`).
  Empty/falsy is a no-op: no fetch, no events, existing content untouched.
- `mode: 'cors' | 'no-cors' | 'same-origin' = 'same-origin'` (reflected) — forwarded to
  `fetch(url, { mode })`. Defaults to `same-origin` (not the upstream components' `cors`) so
  cross-origin fetching is opt-in; an invalid value is normalized back to `same-origin` rather than
  letting `fetch()` throw. `no-cors` is accepted for enum completeness but always yields an opaque
  response (`status` `0`, unreadable body) — a Fetch API limitation, not a bug here.
- `cache: boolean = true` (attribute is not reflected) — shares matching in-flight work and retains
  successful sanitized remote documents in a bounded cache. `cache="false"` (including that exact
  HTML attribute syntax) opts this instance out of both deduplication and retention. Fragment ids
  are deliberately not part of the key: `/partial.html#one` and `/partial.html#two` share only the
  fragmentless fetch/sanitize work, then select and clone independently. Request mode, byte cap,
  and sanitizer profile are part of the key.

**Methods:** `reload(): Promise<void>` invalidates the retained remote document for this URL and
mode, then loads it again. A same-page source is simply re-cloned from its current DOM.

The shared text-viewer contract is also available for the sanitized light-DOM fragment:
`highlights`, `activeHighlightId`, `anchor`, and `anchorKinds` (`['text-quote', 'fragment']`).
`search(query)`, `searchNext()`, `searchPrevious()`, `clearSearch()`, and `scrollToAnchor()`
operate on the included text. A successful replacement explicitly recomputes any active search
against the new fragment rather than leaving results from the previous content.

**Events:**

- `lr-load` — `detail: { src }` — the fragment was sanitized and written into the light DOM.
- `lr-include-error` — `detail: { status, reason, error? }`. `reason` is a `LyraIncludeErrorReason`:
  `'blocked-url'` (`src` failed the allowlist; `fetch()` never ran), `'network'` (`fetch()` rejected),
  `'http'` (response not `ok`; `status` carries the code), `'missing-sanitizer'` (the optional
  `dompurify` peer failed to load), `'resource-too-large'` (the body exceeded the 2 MiB Include
  cap), or `'missing-fragment'` (the requested id was absent after sanitization). Non-HTTP reasons
  use status `0`; `'http'` normally carries the response code, but an opaque `mode="no-cors"`
  response is also classified as `'http'` with status `0`.
- `lr-error` — the same failure under a second name, carrying the **identical detail object** and
  always fired alongside `lr-include-error`, never instead of it. The two upstreams disagree on the
  spelling — Web Awesome's is `wa-include-error`, Shoelace's is `sl-error` — so both are supported
  and **neither is deprecated**; listen to whichever one your migration produced. `lr-error` is
  also the name every other Lyra component uses for a load failure, so a generic page-level
  listener catches this one too. Listening to both names on the same element runs your handler
  twice for one failure.
- `lr-search-change` — `detail: { query: string; matchCount: number; matchCountExact: boolean; activeIndex: number }` — fired
  whenever included-content search state changes.
- `lr-anchor-result` — `detail: { found: boolean }` — fired after an `anchor` assignment or
  `scrollToAnchor()` call is applied.
- `lr-text-select` — `detail: TextSelectDetail` (`{ text: string; anchor: LyraAnchor | null; rects:
DOMRect[] }`) — fired after a selection ends inside the included content.

`lr-highlight-activate` is not part of this component's event contract: included-text highlights
are passive and cannot be activated.

The three shared text-viewer events bubble and compose and are non-cancelable.

**Slots:** default — fallback content shown until (or unless) a fetch succeeds. It is overwritten by
the sanitized fragment on success, and left untouched on failure (as is any previously successful
include).

**CSS parts:** `base` (the `display: contents` wrapper around the default slot), 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).

An absent `dompurify` fails closed: it fires `lr-include-error` with
`reason: 'missing-sanitizer'` and leaves the existing content in place — unsanitized markup is
never transcluded.

Every inserted subtree is a clone. Its ids are rebased per Include instance, including references
from labels, ARIA idrefs, fragment links, and `url(#id)` attributes, so repeating one source does
not add duplicate document ids. Concurrent consumers lease shared work: disconnecting one aborts
the request only when no other subscriber still needs it. Rejected work is evicted and can be
retried; a stale response never paints over a newer `src`.

```html
<lr-include id="navigation" src="/partials/navigation.html#primary">
  Loading navigation…
</lr-include>
<script type="module">
  import "@aceshooting/lyra-ui/components/viewers/include/include.js";

  const include = document.querySelector("#navigation");
  include.addEventListener("lr-include-error", (event) => {
    console.error(event.detail.reason, event.detail.status);
  });
  await include.reload();
</script>
```
