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

# `lr-span-waterfall`

- **Import** `import '@aceshooting/lyra-ui/components/lr-span-waterfall.js';` (stable tag alias; registers the tag)
- **Class** `LyraSpanWaterfall`, also available unregistered from `@aceshooting/lyra-ui/components/agent-tools/span-waterfall/span-waterfall.class.js`
- **Family** `components/agent-tools/` — 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, 9 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-span-waterfall`

The horizontal-timeline projection of the same `LyraSpan[]` `<lr-trace-tree>` consumes: a time
axis, one row per span in start order, status-toned bars (Langfuse timeline / Temporal
event-history style).

Span views omit whitespace-only IDs and preserve every nonblank business ID exactly, including
surrounding whitespace. The first valid admitted duplicate continues to win.

**Properties:** `spans: LyraSpan[] = []` (attribute: false) — `LyraSpan { id: string; parentId?:
string; name: string; kind: 'agent' | 'llm' | 'tool' | 'retriever' | 'embedding' | 'other';
startMs: number; endMs?: number; status: 'pending' | 'running' | 'success' | 'error' | 'denied';
tokensIn?: number; tokensOut?: number; costText?: string; detail?: string }`, exported from
`trace-tree/span.ts`. `startMs`/`endMs` are milliseconds **relative to the trace start**, not
wall-clock timestamps; `endMs` is absent while the span is still running. `costText` is preformatted
by the host (e.g. `"$0.0012"`) and rendered verbatim, never parsed or summed. One flat array powers
both this component (timeline projection via `startMs`/`endMs`) and `lr-trace-tree` (hierarchy
projection via `parentId`) — never two shapes. Foreign runtime `kind` and `status` values render
as `'other'` and `'pending'` rather than throwing, although hosts should continue to use the
documented literal sets. At most 500 unique valid spans mount; when `activeSpanId` resolves beyond
the ordinary input-order budget, that span and its ancestor path reserve positions so the
controlled active state remains visible. A localized `[part="limit"]` note exposes truncation. The
time axis always scales to the whole trace, measured before that 500-span cap is applied, so a
truncated tail can never shrink the axis and stretch the surviving bars across the track.
`activeSpanId: string | null = null`
(attribute `active-span-id`), `viewStartMs: number | null = null` (attribute `view-start-ms`) and
`viewEndMs: number | null = null` (attribute `view-end-ms`) — override the auto-computed time
window, `hideAxis: boolean = false` (attribute `hide-axis`), and `label: string = ''`.

The granular `@aceshooting/lyra-ui/components/agent-tools/trace-tree/trace-tree.js` entry also
type-exports `LyraSpanKind` and `LyraSpanStatus`, and exports
`normalizeLyraSpanKind(value)` / `normalizeLyraSpanStatus(value)` for normalizing provider data
before assigning `spans`. These helpers are intentionally granular-only rather than root-barrel
exports.

**Events:** `lr-span-select` — `detail: { spanId: string }`, a bar/row was activated (click, Enter,
Space).

**CSS parts:** `base`, `axis` (the time-ruler row, hidden when `hideAxis`), `tick`, `tick-label`,
`row`, `name` (the row's name gutter), `bar-track`, `bar` (the interactive, focusable status-toned
bar), `meta` (secondary row info, shown inline under 480px), `status-text`, `duration`, `empty` (shown
when `spans` is empty), `limit` (the 500-span projection notice), and `live-region`.
The interactive `bar` keeps a 24px minimum target in both axes even when its duration-derived
paint width would otherwise be only a few pixels.

The terminal axis tick is end-aligned so its label remains inside the allocated chart width. Roving
keyboard focus is computed from the currently rendered/filtered span ids, so a hidden active span
cannot leave the component with no `tabindex="0"` stop.

**Themeable custom properties:** `--lr-span-waterfall-name-width` (default `8rem`),
`--lr-span-waterfall-stripe-speed` (a `running` span's striped-bar animation duration; defaults to
`--lr-duration-ambient` — the bare-duration token, not the `--lr-transition-ambient`
duration+easing shorthand, which is invalid in an `animation-duration` slot), and
`--lr-span-waterfall-row-active-bg` (default
`var(--lr-color-brand-quiet)`) — the background of the active (`activeSpanId`) row.
Status-scoped bar hooks are `--lr-span-waterfall-success-color` (default
`var(--lr-color-success)`), `--lr-span-waterfall-error-color` (default
`var(--lr-color-danger)`), `--lr-span-waterfall-denied-color` (default
`var(--lr-color-warning)`), `--lr-span-waterfall-running-color` (default
`var(--lr-color-brand)`) for the running stripe foreground,
`--lr-span-waterfall-running-stripe-color` (default `var(--lr-color-brand-quiet)`) for its
contrasting background, and `--lr-span-waterfall-pending-border-color` (default
`var(--lr-color-border-strong)`) for pending bars.

That last one follows the convention every **state-scoped** custom property in this family uses, and
it is worth reading once: it is an inline `var()` fallback at its point of use and is deliberately
**not** declared on `:host`, so it can be set on the element _or on any ancestor_ and still reach the
rule that consumes it. It exists because Shadow Parts forbids an attribute selector after `::part()`
— `::part(row)[data-active]` and every selector like it is invalid CSS — so before it, the only way
to restyle a state-dependent surface was to override a library-wide `--lr-color-*` token, which repaints
every other surface reading that token. Every `*-active-*`, `*-selected-*` and per-state color
property below works the same way.
