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

# `lr-conversation-item`

- **Import** `import '@aceshooting/lyra-ui/components/lr-conversation-item.js';` (stable tag alias; registers the tag)
- **Class** `LyraConversationItem`, also available unregistered from `@aceshooting/lyra-ui/components/conversation/conversation-item/conversation-item.class.js`
- **Family** `components/conversation/` — 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** 12 parts, 8 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-conversation-item`

A selectable row representing one chat session in a history sidebar list. Usable standalone or as the
`renderItem()` payload of `<lr-virtual-list>`; has no dependency on that (or any) other component.
First-party invention (no Web Awesome equivalent).

Inline rename keeps IME composition keys in the editor; ordinary Enter commits and Escape cancels
after composition.

**Properties:**

- `conversationId: string = ''` (attribute `conversation-id`) — stable domain identity carried by
  both selection and rename details. Native `id` remains ordinary document/CSS/ARIA identity.
- `label: string = ''` — the session's visible label. Falls back to "Untitled conversation" when
  empty (display only — the property itself is never mutated by that fallback). Native `title`
  remains available for tooltip semantics.
- `excerpt: string = ''` — a short preview snippet of the last message. Omit for no excerpt line.
  Ignored entirely once the `excerpt` slot has assigned content.
- `timestamp?: LyraTimestamp` (attribute: false) — accepts a `Date`, ISO/date string, or epoch-ms
  number; invalid and TimeClip-out-of-range input is treated as unset (no `<time>` rendered).
- `formatTimestamp?: (date: Date) => string` (attribute: false) — overrides the default absolute-time
  rendering (clock time for same-day timestamps, otherwise a calendar date). Not a fuzzy "2 hours ago"
  relative string — bucketed relative grouping is a list-level concern, not this row's job.
- `active: boolean = false` (reflected) — whether this is the currently-selected/open session; drives
  the brand-quiet background treatment.
- `renamable: boolean = true` (reflected) — whether inline-rename is available at all. When `false`, the
  rename button never renders and the row can never enter its editing state; flipping it to `false`
  while a rename is already in progress cancels that edit (discards the draft, like Escape) rather
  than leaving it stranded and still committable.
- `compact: boolean = false` (reflected) — tighter row padding and gaps, for the dense history
  sidebars these rows usually render in (same convention as `lr-empty`'s `compact`). Tightens
  `[part='base']`'s padding to `var(--lr-space-xs) var(--lr-space-s)` and its gap to
  `var(--lr-space-2xs)`, and collapses `[part='content']`'s inter-line gap to `0`. Deliberately
  changes nothing else: it does **not** shrink `[part='rename-button']` below the shared
  `--lr-icon-button-size` target floor, hide the excerpt, or reduce the excerpt/timestamp font
  sizes — so a row carrying a rename button or slotted `actions` still floors at roughly that icon
  size plus the compact padding, while a row with `renamable=false` and no actions collapses much
  further.
- `spellcheck: boolean = true` — forwarded to the in-place rename `<input>`; `spellcheck="false"` is
  parsed as false (not Lit's default boolean-attribute behavior)
- `autocapitalize: string = ''` — forwarded to the in-place rename `<input>`; empty omits the attribute
- `autocorrect: boolean = true` — forwarded canonically as `autocorrect="on"|"off"`; JavaScript
  writes also accept legacy `'off'`/`'false'` strings and normalize reads to boolean.

**Methods:** `click()` activates the selectable row like its internal button; while an inline rename
is active, it forwards to the label input instead and does not re-select the conversation.

**Events:** `lr-select` (`detail: { conversationId }`; fires from the selectable region on click or
Enter/Space while not renaming), `lr-rename` (`detail: { conversationId, label }`; a controlled
rename request that never mutates `label`, and is omitted for an empty or unchanged trimmed draft),
plus bubbling/composed `blur` and `focus` with `null` detail relayed from the rename input.

**Slots:**

- `actions` — overflow/icon-button controls rendered at the trailing edge of the row (e.g. a
  pin/delete control); only visually shown once it actually has assigned elements. The only slot that
  may hold focusable content.
- `start` — non-interactive leading content (avatar, purpose icon, status dot), rendered inside the
  selectable region before the label/excerpt content.
- `content` — replaces the built-in label + excerpt + meta content area with host-supplied
  non-interactive row content.
- `excerpt` — full override of the excerpt presentation (e.g. a search-hit snippet with `<mark>`);
  wins over the `excerpt` property whenever it has assigned content.
- `meta` — small, non-focusable structured fields below the label/excerpt (a day label, cost, request
  count); entirely app-supplied, this component computes none of it.

`start`/`content`/`excerpt`/`meta` must all stay non-focusable — see the `role="button"` note below.

**CSS parts:** `base`, `active-indicator` (decorative, rendered only while `active`),
`select-button`, `start`, `content`, `label`, `label-input`, `rename-button`, `excerpt`, `meta`, `timestamp`,
`actions`

**Themeable custom properties:** `--lr-conversation-item-active-bg` (default
`var(--lr-color-brand-quiet)`) — the row's background while `active`. `--lr-conversation-item-active-color`
(default `var(--lr-color-text)`) — the text color of `[part='excerpt']` and `[part='timestamp']`
while `active`. Both are declared as inline `var()` fallbacks at the point of use and never on
`:host`, so either can be set on the element _or on any ancestor_ (a thread-list wrapper, a page
theme layer); `::part(base)[active]` is not valid CSS — Shadow Parts forbids an attribute selector
after `::part()` — so the only previous lever was overriding the library-wide `--lr-color-brand-quiet`
token and repainting everything else reading it. Unset, each falls back to exactly the token its rule
used before.

**These two are a contrast-sensitive pair — override them together, never one alone.** The
`-active-color` hook exists precisely because the quiet text tone only reaches about 4.25:1 against
the default active background; keep any override at 4.5:1 or better against it. And note that
`[part='label']` is _not_ restyled by the pair — it keeps `--lr-color-text` regardless — so a dark
custom active background needs its own label color set alongside them, or the label drops below
contrast while the excerpt stays legible.

`--lr-conversation-item-active-indicator-color` (default `var(--lr-color-brand)`) controls the
decorative `active-indicator` part's color. `--lr-conversation-item-active-indicator-width`
(default `var(--lr-size-2px)`) controls its inline width. `--lr-conversation-item-active-indicator-inset-inline`
(default `0 auto`) controls its logical inline insets; set `auto 0` to move the indicator to
inline-end. The indicator is `aria-hidden`, occupies the full row block-size, and is absent while
the row is inactive.

`--lr-conversation-item-compact-padding` (default `var(--lr-space-xs) var(--lr-space-s)`) —
`[part='base']`'s padding while `compact`. `--lr-conversation-item-compact-gap` (default
`var(--lr-space-2xs)`) — `[part='base']`'s gap while `compact`. Like the active-state pair, both are
inline `var()` fallbacks at the point of use and never declared on `:host`, so a surrounding list can
retune every row at once from an ancestor. `[part='content']`'s gap collapses to a flat `0` under
`compact` with no hatch of its own — there is no smaller step left to retune to. `:host([compact])
[part='base']` is ordered _before_ `:host([active]) [part='base']` (equal specificity), so a row that
is both compact and active keeps the active background and the promoted excerpt/timestamp contrast.

`--lr-conversation-item-align` (default `flex-start`) controls the cross-axis `align-items` of both
`[part='base']` and `[part='select-button']`. `flex-start` — today's only behavior — suits the
common multi-line row (a title plus an `excerpt`): centering that layout against a single-line
trailing action would misalign the title's own baseline. Set it to `center` for a row that is
reliably single-line (no excerpt, no wrapping title) alongside a taller trailing action (e.g. an
`actions` control), where centered alignment reads better. It is not the default because switching
it would misalign every existing multi-line row.

Plus shared tokens — `--lr-space-xs/-s/-m`, `--lr-radius`,
`--lr-transition-fast`, `--lr-color-text/-text-quiet/-brand/-brand-quiet/-surface`,
`--lr-focus-ring-width/-color/-offset`, `--lr-icon-button-size`.

**Optional peer deps:** none.

```html
<lr-conversation-item
  conversation-id="sess_123"
  label="Q3 roadmap planning"
  excerpt="Let's revisit the timeline for the launch…"
>
  <button slot="actions" aria-label="Delete conversation">✕</button>
</lr-conversation-item>
<script type="module">
  const item = document.querySelector("lr-conversation-item");
  item.timestamp = session.updatedAt;
  item.active = session.id === currentSessionId;
  item.addEventListener("lr-select", (e) => openSession(e.detail.conversationId));
  item.addEventListener("lr-rename", (e) => renameSession(e.detail.conversationId, e.detail.label));
</script>
```

`role="button"` lives on `[part="select-button"]`, so the row has
valid semantics both standalone and inside a larger history-list layout: it activates one current
session rather than being a listbox option, so it requires no particular owner role. Selection is
conveyed via `aria-current="true"` while `active`, not `aria-selected`. Because `role="button"`
forbids focusable descendants (axe-core's `nested-interactive` rule), the rename button and the
`actions` slot are rendered as DOM _siblings_ of `[part="select-button"]` inside `[part="base"]`, not nested
inside it — the same constraint the in-place rename `<input>` runs into one level deeper, which is
why `[part="select-button"]` sheds its `role`/`tabindex`/`aria-current`/`aria-label` entirely for the
duration of an edit rather than just visually swapping content (a row mid-edit _is_ a text field).

**Known gotchas:**

- Both `lr-select` and `lr-rename` carry the stable `conversationId`; do not overload native `id`
  as domain identity.
- Renaming is a controlled interaction: committing `lr-rename` never updates `label` locally: the
  consumer must apply the new label once it's actually persisted.
- An empty or unchanged (post-trim) rename draft is treated as an implicit cancel — no `lr-rename`
  fires, and the row silently reverts to showing `label`.
- Rename is triggered only by the dedicated pencil-icon button, never a double-click on the label —
  double-click has no keyboard/screen-reader equivalent and would also swallow the row's own
  single-click `lr-select`.
- While renaming, `[part="select-button"]` has no `role`/`tabindex`/`aria-current`/`aria-label` at all — a
  screen reader briefly stops announcing it as a button for the duration of the edit.
- Setting `renamable = false` mid-rename silently discards the in-progress draft (no `lr-rename`
  fires) — a consumer toggling `renamable` off (e.g. in response to some other row entering rename
  mode) should not expect the previous edit to be committed first.
- `compact` is a spacing knob only — it never lowers the rename button's `--lr-icon-button-size`
  floor. A compact row that still shows a rename button (or slotted `actions` at the same floor)
  therefore bottoms out at roughly that icon size plus the compact padding, not at the text height.
  Lowering `--lr-icon-button-size` at an ancestor is the explicit, informed opt-out of the
  target-size floor; a density flag deliberately won't do it silently on your behalf.

---
