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

# `lr-source-picker`

- **Import** `import '@aceshooting/lyra-ui/components/lr-source-picker.js';` (stable tag alias; registers the tag)
- **Class** `LyraSourcePicker`, also available unregistered from `@aceshooting/lyra-ui/components/retrieval/source-picker/source-picker.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** 14 parts, 5 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-source-picker`

A checkbox tree/list scoping which sources ground the next answer: tri-state folders, select-all,
type icons, search. **Not `FormAssociated`, deliberately**: this is a scoping panel, not a form
control — the selection is immediate app state consumed by the next retrieval call, exactly the
stance `lr-tool-select-dialog` already takes.

Toggling search retains the query and selected source IDs. Re-enabling a retained filter repairs the
active tree row so one visible usable Tab stop remains whenever rows are visible; focus already in
the tree follows that repair, while focus outside is preserved.

**Properties:**

- `sources: LyraSourceEntry[] = []` (attribute: false) — `LyraSourceEntry { id: string; label:
string; mimeType?: string; name?: string; children?: LyraSourceEntry[] }`; flat (no `children`) or
  a tree — presence of `children` makes a row a group/folder with tri-state select. Input is
  normalized once into a deterministic nonblank first-id-wins model with identity/cycle detection,
  a depth ceiling of 64 and a 2,000-node ceiling; blank/whitespace ids, duplicate ids, and other
  rejected/truncated input fail closed with localized visible status rather than recursing or
  exposing ambiguous controls. A nonempty raw payload with no valid root entries renders a localized
  invalid-value error rather than the ordinary no-data state
- `selectedSourceIds: string[] = []` (attribute: false) — controlled; blank ids, duplicates, and ids
  that are not leaves in the current `sources` tree are pruned, and the host assigns updates back from
  `lr-sources-change`
- `showSelectAll: boolean = true` (attribute `show-select-all`)
- `searchable: boolean = true`
- `label?: string` — fallback name for the source tree; omission uses the localized picker label,
  while an explicit empty string stays empty
- `accessibleLabel: string | null = null` (attribute `aria-label`) — as a JS-only property while
  the host attribute is absent, overrides the tree name. Authored host `aria-label` instead names
  the picker as a whole (including explicit-empty/dynamic values) and is not cloned onto the tree,
  which retains the distinct `label`/localized name
- `size?: LyraSize` (reflected) — opt-in density tier forwarded to the composed filter `lr-input`,
  on the library's one six-step ladder (`2xs`/`xs`/`s`/`m`/`l`/`xl`, or `small`/`medium`/`large`).
  Forwarding is the only way to reach that field, which resolves its tier inside its own shadow
  root. With no `size` the field keeps its own `m` default; an unsupported value normalizes to the
  omitted state and removes the attribute

**Events:** `lr-sources-change` (`detail: { selectedSourceIds }`, the complete updated leaf-id array,
fired after every toggle including select-all).

**Slots:** none.

**CSS parts:** `base`, `search` (the built-in filter `lr-input`, only when `searchable`),
`select-all` (only when `showSelectAll`), `select-all-control` (the shared `lr-checkbox` semantic
owner), `summary` ("{selected} of {total} selected"), `tree`
(`role="tree"`), `item` (`role="treeitem"`; selection appears only through tri-state
`aria-checked` — `"true"`, `"false"`, or `"mixed"` — and intentionally has no duplicate
`aria-selected` state), `disclosure` (a folder row's pointer-only expand/collapse indicator; the
surrounding treeitem owns keyboard expansion),
`checkbox` (tri-state glyph), `icon` (the `lr-file-icon` type badge), `label`, `empty` (`noData`
when `sources` is empty, `noMatches` when a filter empties the tree), `error` (a nonempty raw
payload containing no valid roots), `limit` (bounded-normalizer
failure/truncation). Post-mount no-match and recovery transitions announce through the shared
light-DOM polite sink; the shadow messages are visible mirrors, never live regions.

**Themeable custom properties:** `--lr-source-picker-checked-bg` — the background of a fully-checked
selection control: the `select-all` pill (whose resting default is `var(--lr-color-brand-quiet)`) and
a fully-selected entry's `[part='checkbox']` (whose resting default is `var(--lr-color-brand)`). The
two keep their distinct defaults while it is unset; setting it unifies both.
`--lr-source-picker-checked-border` (default `var(--lr-color-brand)`) — the border color of every
checked _or_ mixed selection control. `--lr-source-picker-mixed-bg` (default
`color-mix(in srgb, var(--lr-color-brand) 50%, var(--lr-color-surface))`) — the background of a
partially-selected entry's `[part='checkbox']`, so a tri-state folder reads as distinct from a fully
selected one. All three are inline `var()` fallbacks at the point of use rather than `:host`
declarations, so each can be set on the element _or on any ancestor_:
`::part(checkbox)[aria-checked='true']` 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. `--lr-source-picker-indent-size`
(default `var(--lr-size-1-25rem)`) — the indent step added to `[part='item']`'s
`padding-inline-start` per nesting level; the total indent is capped at `--lr-size-8rem` so a deeply
nested tree cannot push its labels out of view. The component writes each row's own depth inline as
the plain number `--lr-source-picker-depth`, which is indent plumbing rather than a retheming knob —
set the step, not the depth. Plus shared tokens otherwise.

**Optional peer deps:** none.

```html
<lr-source-picker></lr-source-picker>
<script>
  const picker = document.querySelector("lr-source-picker");
  picker.sources = [
    {
      id: "folder-1",
      label: "Reports",
      children: [{ id: "doc-1", label: "Q3.pdf", mimeType: "application/pdf" }],
    },
  ];
  picker.addEventListener(
    "lr-sources-change",
    (e) => (retrievalScope = e.detail.selectedSourceIds)
  );
</script>
```

**Known gotchas:**

- Deliberately not form-associated — `lr-sources-change` is the only wiring; there's no
  `name`/`value`/`FormData` participation the way a genuine form control would have.
- Selection, filtering and projection all consume the same bounded normalized tree. Repeated ids
  use the first depth-first occurrence; cyclic/repeated object identities are skipped.

---
