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

# `lr-retrieval-results`

- **Import** `import '@aceshooting/lyra-ui/components/lr-retrieval-results.js';` (stable tag alias; registers the tag)
- **Class** `LyraRetrievalResults`, also available unregistered from `@aceshooting/lyra-ui/components/retrieval/retrieval-results/retrieval-results.class.js`
- **Family** `components/retrieval/` — see `llms/index.md` for its siblings
- **Status** `stable` since `4.1.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** 29 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-retrieval-results`

Orchestration-level ranked-chunk surface: takes raw `RetrievalChunk[]` and adds identity
canonicalization, optional source grouping, multi-selection, pagination/infinite loading, and a
compact/expanded switch. Each row wraps exactly one chunk in an internal `lr-chunk-inspector` (fed
a single-element array), reusing its score bar, tier coloring, title+page rendering, and expandable
text verbatim. Large sets window through an internal `lr-virtual-list`.

**Properties:**

- `chunks: RetrievalChunk[] = []` (attribute: false) — **`RetrievalChunk`, imported from
  `@aceshooting/lyra-ui/ai`**: `{ id: string; text: string; score: number;
source: DocumentRef; metadata?: Record<string, unknown>; rank?: number; locator?: DocumentLocator;
queryId?: string; stage?: string; traceId?: string; scores?: RetrievalScoreBreakdown }`. The raw,
  unsorted/ungrouped result set; host-owned. Blank ids and later duplicate ids are omitted
  first-wins before sorting, grouping, selection, rendering, or events. Internally mapped to
  `lr-chunk-inspector`'s flatter `LyraChunk` via `source.id → sourceId`, `source.name → title`, and
  `locator → anchor`; a `page`-kind `locator` additionally supplies the inspector's visible `page`.
  Nothing is ever guessed from `metadata` — a chunk with no `locator` simply leaves `anchor`/`page`
  unset
- `selectedChunkIds: string[] = []` (attribute: false) — controlled selection by chunk `id`. Blank
  ids, duplicates, and ids absent from the canonical chunk model are pruned. The component updates
  its own copy on toggle _then_ emits `lr-select`; reassign to control
- `selectable: boolean = true` (reflected) — shows a per-row `lr-checkbox`
- `dedupe: boolean = true` (reflected) — retained for compatibility; malformed, blank, and later
  duplicate chunk ids are always omitted first-wins so identity never becomes ambiguous
- `sort: 'score' | 'none' = 'score'` — `'score'` sorts descending; `'none'` preserves given order
- `grouping: 'source' | 'custom' | 'none' = 'none'` — `'source'` buckets rows under a header per
  `source.id` (the header text is that source's `name`, or a localized "untitled source" when it has
  none); `'custom'` buckets them under whatever key `groupBy` returns; `'none'` is a flat ranked
  list. Buckets appear in order of first appearance in the already-`sort`ed list, so with the default
  `sort="score"` that is best-scoring-chunk order, and with `sort="none"` it is the order the chunks
  arrived in. Grouping **always** virtualizes, regardless of `virtualizeAt`
- `groupBy?: (chunk: RetrievalChunk) => string` (attribute: false) — `grouping="custom"` only: the
  group id for each chunk (a date bucket, a relevance tier, a domain-specific bucket). Left unset,
  `'custom'` degrades to the same flat list `'none'` renders rather than inventing a key, so the
  built-in identity/sort/virtualization pipeline stays usable either way. The same escape hatch
  `lr-thread-list` already exposes
- `groupLabel?: (id: string, chunks: RetrievalChunk[]) => string` (attribute: false) —
  `grouping="custom"` only: the group's header text. Left unset, the group id is shown verbatim
- `groupOrder?: string[] | ((a: string, b: string) => number)` (attribute: false) —
  `grouping="custom"` only: explicit group-id order, or a comparator over the first-seen ids. Ids an
  array omits follow after the listed ones in their own first-seen order, never dropped
- `presentation: 'compact' | 'expanded' = 'expanded'` — `'expanded'` shows each chunk's full row
  (score bar, text preview with its own toggle) plus any `metadata`; `'compact'` shows title + score
  bar only and omits `metadata` entirely
- `thresholds: { high: number; medium: number } = { high: 0.75, medium: 0.5 }` (attribute: false) —
  forwarded to every per-row `lr-chunk-inspector`
- `virtualizeAt: number = 50` (attribute `virtualize-at`) — row count (after identity
  canonicalization, before grouping) above which rendering switches to the internal
  `lr-virtual-list`
- `activeChunkId: string = ''` (attribute `active-chunk-id`) — the chunk currently open in a viewer; forwarded
  to each row and to the virtual list (which scrolls the matching row into view)
- `loading: boolean = false` (reflected)
- `hasMore: boolean = false` (attribute `has-more`, reflected) — while virtualized, forwarded to the
  virtual list so scroll-near-bottom fires `lr-load-more`; otherwise shows the built-in footer button
- `errorText: string = ''` (attribute `error-text`; spelled plain `error` before 9.0.0) — non-empty
  replaces the whole result view with a neutral visible message. Caller-supplied text is not
  localized (app/network data, not library copy). A new non-empty value is announced through a
  shared assertive light-DOM region; initial and reconnect content is not replayed
- `announce: boolean = false` (reflected) — opt-in: announce the state the panel is **already
  presenting** the first time it mounts, rather than only announcing later transitions into it.
  Urgency follows the state, exactly as the live path does and in the same branch order the panel
  renders: a non-empty `errorText` wins and is announced verbatim through the shared assertive
  light-DOM region, otherwise a panel that is neither `loading` nor holding any chunk announces the
  localized empty-result message through the shared polite region. A panel still `loading`, or one
  already showing chunks, announces nothing — rendered results are ordinary content read in
  document order. The announcement is deferred one frame past the first update so the shared region
  exists before its text lands, and any live transition arriving first retires the pending
  mount-time announcement so nothing is read twice. Set it where the panel is rendered in response
  to a retrieval the user just ran and nothing else reports the outcome; leave it unset for a panel
  that is part of the page a user is arriving on. Read once, when the panel first mounts: a later
  reconnection or adoption stages the existing state again rather than replaying it, and later
  transitions are announced either way. Remove any host `role="status"`/`role="alert"` hand-added
  before this property existed once it is set — otherwise the initial state is announced twice,
  through the native role and again through the shared sink
- `label?: string` — fallback name for the populated result group; omission uses localized
  `chunkInspectorLabel`. A non-empty host `aria-label` makes the host the sole overall owner; an
  explicitly empty host label stays empty

When a later update enters the settled empty state — including a completed loading cycle that
found no results — the localized empty message is announced through the shared polite light-DOM
region. Initial empty content, loading intermediates, and reconnects are not replayed.

**Events:**

- `lr-select` (`detail: RetrievalResultsSelectDetail` = `{ chunkIds: string[]; chunks: RetrievalChunk[] }`)
  — the _complete_ updated selection, both as ids and as exactly one canonical record per id, so a
  host needn't re-look-up ids against its own copy on every toggle. This derived detail is always
  canonicalized nonblank/first-wins regardless of the legacy `dedupe` switch.
- `lr-load-more` (`detail: null`) — from the virtual list's scroll-near-bottom detection while
  virtualized, or the `[part="load-more"]` button otherwise. Only fires while `hasMore` is true and
  `loading` is false.
- `lr-chunk-open` (`detail: { chunkId, sourceId, anchor? }`) — forwarded verbatim from a row's
  `lr-chunk-inspector`; the event a host routes into `lr-document-viewer`.

**Slots:** none.

**CSS parts:** `base`, `error` (neutral visible message while `errorText` is non-empty), `spinner`
(initial-load `lr-spinner`, while `loading` and `chunks` is still empty), `empty` (when `chunks` is
empty and neither `errorText` nor `loading` is set), `row` (a plain element in this shadow root below the
virtualization threshold; exported from the internal `lr-virtual-list`'s own `row` part while
virtualized — `::part(row)` reaches it either way), `group-header` (exported from the virtual list's
`group` part; grouped/virtualized mode only), `select` (per-row `lr-checkbox`, omitted when
`selectable` is false), `row-body` (carries `data-selected`), `row-body-selected` (additional part
on a selected `row-body`), `metadata` (a `<dl>`; omitted when the chunk has none or while
`presentation="compact"`), `metadata-entry`, `metadata-term` (the `<dt>` carrying a metadata key),
`metadata-value` (the `<dd>` carrying its value), `load-more-row`, `load-more`.

The per-row `lr-chunk-inspector`'s own parts are forwarded onward under a `chunk-` prefix —
`chunk`, `chunk-current`, `chunk-score`, `chunk-score-current`, `chunk-score-bar`,
`chunk-score-fill`, `chunk-score-fill-success`, `chunk-score-fill-warning`,
`chunk-score-fill-danger`, `chunk-open-button`,
`chunk-title`, `chunk-text`, `chunk-text-clamped`, `chunk-toggle`. Those elements sit two shadow
hops deep while virtualized, so this forwarding is the only way to reach them.

Selection state is exposed as the additional `row-body-selected` part name rather than through the
`data-selected` attribute, because Shadow Parts forbids an attribute selector after `::part()`:
`::part(row-body)[data-selected]` is invalid CSS, and while virtualized `::part()` is the only way
in. `data-selected` is unchanged. A state part is a second token in the same `part` attribute, so
a `[part~="…"]` (not `[part="…"]`) selector is the one that matches inside a tree. The `<dt>`/`<dd>`
carry their own part names for the same class of reason — `::part()` matches a single element and
cannot be followed by a descendant combinator, so `::part(metadata-entry) dt` reaches nothing.

**Themeable custom properties:** `--lr-retrieval-results-selected-border` (default
`var(--lr-color-brand)`) — the inline-start border color marking a selected row body
(`::part(row-body-selected)`). A
border rather than a fill by design: the row's own text (the nested chunk inspector's quiet-toned
score line in particular) is sized and colored for the page's default surface, and a tinted
background can drop it below the required contrast ratio, while a border-only indicator carries no
such risk — so recoloring this hook is contrast-safe. 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(row-body)[data-selected]` is invalid CSS — Shadow Parts forbids an attribute selector after
`::part()` — which previously left re-pointing the library-wide `--lr-color-brand` token as the only
lever, repainting every other brand surface with it. Plus shared tokens otherwise.

**Optional peer deps:** none.

**Known gotchas:**

- While virtualized, each row's content lives inside `lr-virtual-list`'s shadow root, not this
  component's.
- Below the virtualization threshold, scroll-near-bottom isn't a meaningful gesture, so a
  `[part="load-more"]` button takes its place (replaced by a spinner while `loading`).
- Source-group fallback labels are re-evaluated when the effective locale or a `.strings`
  override changes; a cached grouping never leaves the previous localized “untitled source” text.

---
