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

# `lr-image-viewer`

- **Import** `import '@aceshooting/lyra-ui/components/lr-image-viewer.js';` (stable tag alias; registers the tag)
- **Class** `LyraImageViewer`, also available unregistered from `@aceshooting/lyra-ui/components/media/image-viewer/image-viewer.class.js`
- **Family** `components/media/` — 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); family-wide breaking-change summaries: [llms-full.txt](../../llms-full.txt)
- **Deprecations** none
- **Optional peers** none
- **Themeable via** 18 parts, 19 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-image-viewer`

A full pan/zoom raster-image viewer with labeled region highlights and opt-in region annotation, the
landing surface for `region`-anchored citations. Distinct from `<lr-svg-viewer>` (rendered SVG
documents) and `<lr-image-comparer>` (before/after slotted surfaces). Adopts `DocumentAnchorTarget`
with `anchorKinds: ['region']` only — no text selection is bound.

Disabled fit, rotate and annotate controls retain their resting paint on hover and press. Enabled
controls keep their token-driven feedback.

The annotation wrapper displays the shared token-driven focus ring in keyboard modality. The ring
sits inside the wrapper so an image filling the scroll viewport cannot clip it. Annotation commands
apply when that wrapper itself owns keyboard input. Enter on a highlight button activates that
highlight without starting or committing an annotation, whether or not a draft exists; wrapper arrow
commands retain physical image-coordinate semantics in both directions.

**Properties:** `src: string = ''`, `name: string = ''`, `alt?: string`,
`fit: LyraImageFit = 'contain' | 'width' | 'actual'` (reflected; invalid writes normalize to
`contain`), `zoom: number = 1` (reflected), `minZoom: number = 0.5` (attribute
`min-zoom`), `maxZoom: number = 4` (attribute `max-zoom`), `zoomStep: number = 0.25` (attribute
`zoom-step`) — `minZoom`/`maxZoom`/`zoomStep` are pure pass-throughs to the embedded
`<lr-pan-zoom>` as its own `.minZoom`/`.maxZoom`/`.zoomStep`, which does the actual
clamping/normalizing; same names/defaults as `<lr-lightbox>`'s identical trio, both wrapping the
same pan/zoom surface — `rotation: LyraImageRotation = 0 | 90 | 180 | 270` (reflected; finite
writes round to the nearest right angle and wrap), and `annotatable: boolean = false` (reflected).
`LyraImageRegionRect` is the public `{ x, y, width, height }` percentage-coordinate shape. The
inherited anchor-target surface is
`highlights: readonly LyraHighlight[] = []` (property only; assign a new collection to update),
`activeHighlightId: string | null = null` (attribute `active-highlight-id`),
`anchor: LyraAnchor | string | null = null` (property only), and readonly
`anchorKinds: readonly LyraAnchorKind[] = ['region']`.

**Methods:** `rotate()` advances `rotation` by 90°. `zoomIn()`, `zoomOut()`, and `resetZoom()` adjust
the embedded pan-zoom surface's zoom. `scrollToAnchor(target: LyraAnchor | string):
Promise<boolean>` resolves a canonical finite, positive, in-bounds `region` anchor (or unique
highlight id) after the image loads, scrolls its rendered target into the pan/zoom viewport, and
reports true only when the target visibly intersects that viewport. Malformed/out-of-range regions
report false.

**Events:** `lr-load` (`detail: { naturalWidth, naturalHeight }`), `lr-zoom-change` (`detail: {
zoom }`), `lr-rotation-change` (`detail: { rotation }`), `lr-fit-change` (`detail: { fit }`),
`lr-highlight-activate` (`detail: { highlightId }`), `lr-annotation-create` (`detail: { anchor }`, kind
`'region'`), `lr-anchor-result` (`detail: { found }`), and `lr-render-error` (`detail: { error
}`).

`lr-text-select` is not part of this raster viewer's event contract because it binds no selectable
text.

**CSS parts:** `base`, `toolbar`, `fit-control`, `rotate-button`, `annotate-toggle`, `frame` (the
embedded `lr-pan-zoom`), forwarded aliases `frame-viewport`, `frame-content`, `frame-controls`,
`rotation-frame` (the axis-swapped 90°/270° layout footprint), `image-wrapper`, `image`,
`highlight-layer`, `highlight` (carries `data-tone`/`data-active`), `highlight-label`,
`annotation-box`, `error`, 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).

`error` is ordinary localized visible text, not a shadow live region. A fresh post-mount image
failure or transition to an unsafe source appends the localized message to the document's
pre-mounted `[data-lr-live-region="assertive"]` sink. An already-unsafe initial `src` remains
visible but does not interrupt on mount; identical later failures append distinct children.

Fit, rotate and annotate controls remain disabled until the current source reaches its own loaded
terminal. Requested annotation mode resumes after a successful replacement load, but is not
exposed as pressed/operable during idle, loading, or error state.

While effectively annotatable, `image-wrapper` is a named `role="group"` with the localized
annotation hint. Only `region` highlights whose rectangle is finite, positive and wholly within
the 0–100 image coordinate space are rendered. Malformed, empty, and blank IDs are omitted; later
duplicates use first-wins uniqueness. At most
`IMAGE_VIEWER_HIGHLIGHT_LIMIT` (200) region buttons are projected at once; one roving `tabindex=0`
is maintained, Arrow keys/Home/End move within the projection, and an active item beyond the
leading window replaces its final entry so identity stays reachable. `data-truncated` and
`data-total` on `highlight-layer` expose the bounded state. Highlight tones retain distinct border
styles as well as colors, including forced-colors mode.

At 90°/270°, `rotation-frame` swaps the untransformed wrapper's layout axes and centers the painted
transform inside that footprint, keeping every fit mode reachable in the scroll geometry in LTR
and RTL.

**RTL behavior:** the raster and annotation geometry use physical image coordinates. In annotation
mode, ArrowLeft/ArrowRight decrease/increase a draft's `x` coordinate and their Shift variants
decrease/increase its width in both text directions; the surrounding toolbar remains logical.

**Themeable custom properties:** `--lr-image-viewer-annotate-active-bg` (default
`var(--lr-color-brand-quiet)`) and `--lr-image-viewer-annotate-active-border` (default
`var(--lr-color-brand)`) — the background and border of `[part='annotate-toggle']` while annotation
mode is on. The toggle carries its own glyph in `--lr-color-text`, so keep a 4.5:1 ratio against it
when overriding the background. `--lr-image-viewer-highlight-active-color` (default
`var(--lr-color-brand)`) — the outline of the `[part='highlight']` matching `activeHighlightId`,
independent of the per-tone border colors, so the active box stays distinguishable whatever tone it
carries. `--lr-image-viewer-highlight-active-border-width` (default
`var(--lr-border-width-thick)`) controls the active highlight border width;
`--lr-image-viewer-highlight-active-outline-width` (default `var(--lr-focus-ring-width)`) and
`--lr-image-viewer-highlight-active-outline-offset` (default `var(--lr-focus-ring-offset)`) control
its outline geometry. Like the existing active color hook, each is an inline `var()` fallback and
can be set on the viewer or any ancestor. Highlight tone styling is exposed through
`--lr-image-viewer-highlight-border`,
`--lr-image-viewer-highlight-bg`, and the tone-specific
`--lr-image-viewer-highlight-success-border`, `--lr-image-viewer-highlight-success-bg`,
`--lr-image-viewer-highlight-warning-border`, `--lr-image-viewer-highlight-warning-bg`,
`--lr-image-viewer-highlight-danger-border`, `--lr-image-viewer-highlight-danger-bg`,
`--lr-image-viewer-highlight-neutral-border`, and `--lr-image-viewer-highlight-neutral-bg`
properties. `--lr-image-viewer-highlight-fill` is the resting fill a `[part='highlight']` actually
renders, resolved per tone from the `-bg` knobs above; its hover and pressed states are color mixes
taken from that value, so setting it directly retints all three states of one highlight at once —
retint a whole tone through the matching `-bg` knob instead. `--lr-image-viewer-annotation-box-border`
(default `var(--lr-color-brand)`) and `--lr-image-viewer-annotation-box-bg` (default
`color-mix(in srgb, var(--lr-color-brand) 15%, transparent)`) retint `[part='annotation-box']`, the
in-progress draft rectangle drawn while actively annotating, independent of the saved highlights'
own tone colors. These properties are declared as inline
`var()` fallbacks at the point of use rather than on `:host`, so each can be set on the element _or
on any ancestor_:
`::part(highlight)[data-active]` is invalid CSS — Shadow Parts forbids an attribute selector after
`::part()` — which previously left overriding the library-wide
`--lr-color-brand`/`--lr-color-brand-quiet` tokens as the only lever, repainting every other
element that read them. Unset, each falls back to the token its rule used before.
