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

# `lr-ebook-viewer`

- **Import** `import '@aceshooting/lyra-ui/components/lr-ebook-viewer.js';` (stable tag alias; registers the tag)
- **Class** `LyraEbookViewer`, also available unregistered from `@aceshooting/lyra-ui/components/viewers/ebook-viewer/ebook-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** `epubjs` — see `llms/peers.md`
- **Themeable via** 9 parts, 1 custom property — see this component's own `@csspart`/`@cssprop` list below
- **Library-wide behavior** (events, form association, `locale`/`strings`, tokens, TS types): `llms/shared.md`

---

## `lr-ebook-viewer`

Renders EPUB ebooks through the optional `epubjs` peer. `src` is fetched as an `ArrayBuffer`, and
epub.js renders the reading area into its stable `mount` element, using an internal iframe for
chapter content. A usable book exposes `ready`, `renderTo()`, and `destroy()`, and its rendition
exposes `display()`, `prev()`, `next()`, `on()`, and `annotations.highlight()`/`remove()`; an absent
required capability is terminal. Navigation and spine data are optional.

Genuine native `Selection`/`Range` objects from the current chapter iframe are accepted through
platform accessors, including across document realms. Arbitrary peer-owned accessors remain
uninvoked, and callbacks from replaced or disconnected books cannot emit stale selections.

**Properties:** `src: string = ''` and `name: string = ''`. A plain `aria-label` attribute on the
host overrides the reading region's accessible name — by attribute presence, so an explicitly empty
`aria-label=""` still wins over `name`. (There is no matching JS property: the `accessibleLabel`
property was removed in 9.0.0, where it had never been readable or writable to any effect — set the
attribute.) `maxHeight: string = ''`
(attribute `max-height`) caps the `mount` area epub.js renders into; invalid CSS `max-height`
values, declaration breaks, and `url()` are ignored. `location: string = ''`
(not reflected — CFIs are long) is
a CFI or spine href identifying the current reading position: set before the book finishes
loading it's recorded and applied once ready, set after it applies immediately, and epub.js's own
`relocated` event keeps it in sync with user navigation without re-triggering its own `display()`
call. A controlled `location` assignment made synchronously inside `lr-location-change` wins over
the peer-reported CFI and is displayed. `anchorKinds: readonly LyraAnchorKind[] = ['cfi',
'text-quote']` (this
viewer's supported `LyraAnchor.kind` values for the shared anchor-target contract).

**Methods:** `getToc()` resolves the EPUB's own navigation document (`book.navigation.toc`,
populated once `book.ready` resolves) flattened into document-ordered `EbookTocItem[]` (`{ id,
label, href, level }`, `level` starting at 1 for a top-level entry, `id` falling back to `href`
when a navigation entry has none), `[]` before a book has loaded or when navigation is absent. TOC
projection examines at most 10,000 positions, nodes, and depth 100; malformed entries are skipped
without preventing later valid entries. `search(query)` resolves the
match count across every spine section, in document order, via epub.js's own `item.load()`/
`item.find()`/`item.unload()` (empty/whitespace query behaves like `clearSearch()`; a newer
`search()` call or a `src` change aborts an in-flight scan; peer output is capped at 10,000
matches after at most 1,000 spine items and 4,000,000 result code units; queries are capped at 4,096
code units); `searchNext()`/`searchPrevious()`
advance/step back through matches (wrapping, resolving `false` when there are none); `clearSearch()`
clears the query, matches, and painted search annotation. `matchCountExact: false` also reports
malformed or holey spine/find data and failed section load/unload work, not only a search ceiling.

**Events:** `lr-render-error` with `detail.error` when fetching, opening, or rendering fails;
`lr-location-change` (`detail: { cfi, href }`) fired from epub.js's own `relocated` event;
`lr-search-change` (`detail: { query, matchCount, matchCountExact, activeIndex }`) from search,
navigation, clear, canonical source reset, and effective-locale re-evaluation; `lr-anchor-result`
(`detail: { found }`) after an anchor is
applied; `lr-highlight-activate` (`detail: { highlightId }`) when a painted CFI highlight is clicked; and
`lr-text-select` (`detail: { text, anchor, rects }`) after selection inside a chapter iframe.
Selection text is capped at 4,096 code units and selection rectangles at 1,000.

**CSS parts:** `base` (explicit `aria-busy="true"|"false"`; visible loading text is ordinary
non-live shadow content and later loading transitions use the shared document-level polite sink),
`toolbar`, `previous-button`, `next-button`, `previous-icon`, `next-icon`,
`mount`, and `error` (ordinary visible text; later error transitions use the shared document-level
assertive sink), plus `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). Search results
are appended to the shared document-level polite sink, which lives in the host's light DOM; the
empty `announcer` shadow mirror that used to carry a part of that name was removed in 9.0.0 (it had
no styling of its own and never held any text).

**Themeable custom properties:** `--lr-ebook-viewer-max-height` (default `none`) — maximum block
size of `[part="mount"]` before it scrolls internally; also settable via the `max-height` property,
which writes this token inline.

The toolbar buttons use the component-specific localized labels `ebookViewerPreviousChapter` and
`ebookViewerNextChapter` (English: “Previous chapter” / “Next chapter”), so they remain
unambiguous beside other previous/next controls and are overridable through `.strings`.

**Optional peer dependency:** install `epubjs` with `pnpm add epubjs`. The document-viewer registry
matches `application/epub+zip` and `.epub` filenames, declaring `{ anchors: ['cfi', 'text-quote'],
search: true, textSelect: true }` capabilities and forwarding `anchor`/`highlights` to the mounted
viewer. The peer loader requires the callable EPUB factory; malformed module shapes fail closed.
A granular consumer (not importing the `all.js` compatibility bundle) must also import
`@aceshooting/lyra-ui/components/viewers/ebook-viewer/ebook-viewer-register.js` once to install
this registration; without it `<lr-document-viewer>` never recognizes `.epub` files. That entry also
exports `EBOOK_VIEWER_TAG` (`'lr-ebook-viewer'`) as a stable reference to the tag it eventually
registers.

Remote resources are capped at 25 MB; exceeding it surfaces the localized
`documentPreviewResourceTooLarge` message instead of the ebook.

Adopts the shared anchor-target contract (`highlights`, `activeHighlightId`, `scrollToAnchor()`,
events `lr-highlight-activate`/`lr-text-select`/`lr-anchor-result`): a `cfi` anchor displays
directly via `rendition.display()`; a `text-quote` anchor resolves by scanning the spine with
epub.js's own `item.find()`, since chapter content lives inside epub.js-owned iframes rather than
this component's own shadow DOM — `lr-text-select` mirrors epub.js's own `selected` event for the
same reason. `highlights` (kind `cfi`) paint via `rendition.annotations.highlight()` and are
re-applied whenever the rendition is recreated (a `src` change, or a reconnect remount), since
epub.js doesn't persist annotations across a fresh `renderTo()`.

Rejected or synchronous failures from display, previous/next navigation, search annotation, or
anchor application enter the localized error state and emit `lr-render-error`. Anchor failures
emit one `{ found: false }`, and superseded async anchor/search work cannot mutate the current book.
