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

# `lr-source-card`

- **Import** `import '@aceshooting/lyra-ui/components/lr-source-card.js';` (stable tag alias; registers the tag)
- **Class** `LyraSourceCard`, also available unregistered from `@aceshooting/lyra-ui/components/retrieval/source-card/source-card.class.js`
- **Family** `components/retrieval/` — 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** 5 parts, 3 custom properties — see this component's own `@csspart`/`@cssprop` list below
- **Documented with** `lr-source-list` (same section below)
- **Library-wide behavior** (events, form association, `locale`/`strings`, tokens, TS types): `llms/shared.md`

---

## `lr-source-list` / `lr-source-card`

A collapsible "Sources" panel for one chat message (`lr-source-list`) that groups a set of
`lr-source-card` entries. First-party invention (no Web Awesome equivalent). Cards are meant to be
direct light-DOM children of the list (plain composition — no `.items` array prop, the same shape
`<lr-multi-split>`'s panels take), though `lr-source-card` renders and functions fine standalone.

### `lr-source-list`

**Properties:**

- `expanded: boolean = false` (reflected) — whether the card list is currently shown. Starts
  collapsed by default so a message's sources don't eat vertical space until asked for.
- `label?: string` — header text used when `label-plural` isn't set, e.g. `"Sources"`.
- `labelPlural: string = ''` (attribute `label-plural`) — fully consumer-built, already-pluralized
  header summary, e.g. `"3 sources"` or `"1 source"`; this component never counts or pluralizes on
  its own. Takes precedence over `label` when both are set. If neither is set, the header falls back
  to the localized `sourceListDefaultLabel` string (English default: `"Sources"`).

**Getters:** `sourceCount: number` — read-only, live-updated count of the currently-slotted children,
handy for building a `label-plural` string reactively, e.g. `` list.labelPlural = `${list.sourceCount} sources` ``.

**Events:** `lr-toggle` (`detail: { expanded: boolean }`) — the header was activated, expanding or
collapsing the list.

**Slots:** default — `<lr-source-card>` elements, neutral `<div>`/`<span>` wrappers, or
author-owned `role="listitem"` entries. When every assigned child is one of those list-compatible
forms, `[part="list"]` supplies `role="list"` and neutral, unroled children temporarily receive
`role="listitem"`. Native controls and children with another explicit or native semantic role retain
their own semantics; their presence omits list/listitem roles rather than creating an invalid ARIA
list context. When every assigned child returns to this list-compatible allowlist, the
list/listitem semantics return.

**CSS parts:** `base` (outer container), `header` (the clickable `<button>` toggling `expanded`),
`toggle` (the chevron indicator inside the header), `list` (wrapper around the default slot, `hidden`
while collapsed).

**Themeable custom properties:** shared tokens only — `--lr-color-border`, `--lr-color-surface`,
`--lr-color-text`, `--lr-color-brand` / `-brand-quiet`, `--lr-radius`, `--lr-space-xs`/`-s`/
`-m`, `--lr-transition-fast`, `--lr-focus-ring-*`.

**Optional peer deps:** none.

### `lr-source-card`

**Properties:**

- `sourceId: string = ''` (attribute `source-id`) — stable identifier matching a
  `<lr-citation-badge>` elsewhere on the page.
- `title: string = ''` — the source's display title, e.g. a filename. Falls back to `"Untitled
source"` when empty.
- `page?: string | number` — optional page reference, e.g. `12` or `"iv"`, rendered as-is (never
  parsed/validated as a number), appended to the title as `" — p. {page}"`.
- `href?: string` — optional URL, echoed back (unopened) in `lr-open`'s detail.
- `compact: boolean = false` (reflected) — tighter root padding and row gap, for the dense citation
  lists these cards usually render in — the same convention as `lr-empty`'s `compact`. Purely a
  density knob: the border and background stay. `false` (the default) keeps the full card padding.
- `frame: LyraFrame = 'card'` (reflected) — container treatment, in the library-wide `frame`
  vocabulary (`'card' | 'plain'`). `'card'` (the default) keeps the bordered, filled, padded box;
  `'plain'` removes the border, background, padding and corner radius, so a card inside a
  `<lr-source-list>` (or any container already drawing its own border/dividers) doesn't double it.
  `plain` wins over `compact` when both are set — nothing left to tighten. The title and toggle keep
  their brand color and hover underline under `plain`, since neither ever depended on the card
  chrome. Use the shared `LyraFrame` type when authoring this property.
- `disabled: boolean = false` (reflected) — turns off this card's OWN controls. The `title` button
  and the "Show more"/"Show less" `toggle` both render `disabled`, so neither one can emit `lr-open`
  or `lr-expand` and neither remains in the tab order, and the card paints at
  `--lr-opacity-disabled` with a `not-allowed` cursor on both. The one `lr-expand` a disabled card
  can still emit is the automatic collapse when the `full` slot empties while expanded — that
  reports a state change the card genuinely made, exactly as it does when enabled. Both buttons
  carry the native `:disabled`, so `::part(title):disabled` / `::part(toggle):disabled` restyle the
  disabled affordance from outside. Every self-rendered sub-control is
  gated, not just the primary one: a card whose title is inert but whose disclosure toggle still
  expands reads as half-broken rather than disabled. Slotted `excerpt`/`full` content stays yours
  to disable. Not form-associated, so an ancestor `<fieldset disabled>` does not cascade here.
- `aria-pressed` and `aria-current` (attributes only) — forwarded reactively onto the `title`
  button, the control that carries this card's action, the same mechanism `<lr-button>` and
  `<lr-icon-button>` use. `aria-pressed` accepts `'true' | 'false' | 'mixed'`; `aria-current`
  accepts `'page' | 'step' | 'location' | 'date' | 'time' | 'true' | 'false'`. Anything outside
  those sets is dropped rather than passed through. The `toggle` button never receives either — it
  already owns `aria-expanded` for its own disclosure state, and two conflicting state claims on
  one control is worse than one. This is what lets a single-select citation list announce which
  source is the active one.

**Events:**

- `lr-expand` (`detail: { sourceId: string; expanded: boolean }`) — the per-card "Show
  more"/"Show less" toggle was activated. Unrelated to the parent `lr-source-list`'s own
  expand/collapse, which only ever hides/shows the _set_ of cards, never a single card's own content.
- `lr-open` (`detail: { sourceId: string; href?: string }`) — the title was activated. This
  component never navigates on its own (a controlled component, the same convention
  `<lr-tool-call-chip>`'s `lr-tool-call-chip-select` follows); a listener decides what "open"
  means.

**Slots:** `excerpt` (a short preview; when left empty, the `excerpt` part collapses away entirely —
its wrapper is `hidden`, not just visually empty), `full` (the complete source text/chunk, hidden
behind the "Show more"/"Show less" toggle — when left empty, no toggle renders at all; removing all
`full`-slotted content while expanded auto-collapses it back).

**CSS parts:** `base` (outer container), `title` (the clickable title/page heading, a `<button>`),
`excerpt` (wrapper around the `excerpt` slot, `hidden` when the slot has no assigned content), `full`
(wrapper around the `full` slot, `hidden` while collapsed), `toggle` (the "Show more"/"Show less"
button — only rendered when the `full` slot has content).

**Themeable custom properties:** `--lr-source-card-bg` (default `var(--lr-color-surface)`) —
`[part='base']`'s RESTING background, the companion to the `compact` tier's levers below;
`frame='plain'` still drops the fill entirely. `--lr-source-card-compact-padding` (default
`var(--lr-space-xs)`) —
`[part='base']`'s padding while `compact`; `--lr-source-card-compact-gap` (default
`var(--lr-space-2xs)`) — the gap between `[part='base']`'s rows while `compact`. Both apply only in
the `compact` state, so a dense citation list can be tuned without re-pointing shared spacing tokens
elsewhere. Plus shared tokens — `--lr-color-border`, `--lr-color-surface`,
`--lr-color-text` / `-text-quiet`, `--lr-color-brand`, `--lr-radius`, `--lr-space-xs`/`-s`,
`--lr-focus-ring-*`.

**Optional peer deps:** none.

```html
<lr-source-list label-plural="2 sources">
  <lr-source-card source-id="doc-1" title="annual_report.pdf" page="12">
    <span slot="excerpt">Revenue grew 12% year over year...</span>
    <span slot="full"
      >Revenue grew 12% year over year, driven primarily by...</span
    >
  </lr-source-card>
  <lr-source-card source-id="doc-2" title="q3_notes.md">
    <span slot="excerpt">No matching full-text chunk for this source.</span>
  </lr-source-card>
</lr-source-list>
<script type="module">
  // Elsewhere, a <lr-citation-badge>'s activation handler can scroll to and
  // highlight the matching card -- neither component needs extra API surface
  // for that, only source-id to be targeted by:
  document.addEventListener("lr-citation-activate", (e) => {
    const card = document.querySelector(
      `lr-source-card[source-id="${e.detail.sourceId}"]`
    );
    card?.scrollIntoView({ block: "center" });
  });
</script>
```

`lr-source-list` removes the card list from the accessibility tree (not just visually hides it)
while collapsed, via the native `hidden` attribute on `[part="list"]` — a screen reader user tabbing
past the header never lands on off-screen source cards they can't currently see. Both components
track slot presence in JS state (`slottedCount` on the list, `hasFullSlot` on the card) rather than
relying on CSS `:empty`, since a `[part]` wrapper always contains a literal `<slot>` child regardless
of assigned content; both also reconcile that state in `firstUpdated()` as a fallback for slot-
forwarding scenarios or engines that don't fire `slotchange` for content already present at parse
time.

**Known gotchas:**

- `lr-source-list` does not automatically build an “N sources” plural summary: provide
  `labelPlural` when that is wanted. Its no-label fallback is localized through
  `sourceListDefaultLabel`, and a per-instance `.strings` override reaches the rendered header.
- `lr-source-card`'s own expand/collapse (`full` slot, `lr-expand` event) is completely
  independent of the parent list's `expanded`/`lr-toggle` — collapsing the list doesn't reset an
  individual card's `fullExpanded` state, and there is no cross-talk between the two components at
  all beyond DOM nesting.
- `lr-source-card` actively strips a bare host-level `title` attribute right after Lit syncs it into
  the `title` property — otherwise the whole card would grow an unsolicited native tooltip repeating
  the title text on hover. Set `title` only as a property/attribute meant to become the rendered
  heading; don't rely on it surviving as a DOM attribute afterward.

---
