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

# `lr-context-meter`

- **Import** `import '@aceshooting/lyra-ui/components/lr-context-meter.js';` (stable tag alias; registers the tag)
- **Class** `LyraContextMeter`, also available unregistered from `@aceshooting/lyra-ui/components/data/context-meter/context-meter.class.js`
- **Family** `components/data/` — 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** 17 parts, 10 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-context-meter`

A segmented occupancy meter (bar or ring) for showing how a fixed capacity — a model's context
window, a token budget, any consumable quota — is divided across labeled categories. First-party
invention (no Web Awesome equivalent). Pure data visualization: it renders `segments`/`total` exactly
as given and never computes token counts, costs, or any other domain-specific estimate itself — the
one exception is the plain arithmetic sum of segment values used to build the accessible "X of Y
used" summary.

**Properties:**

- `segments: ContextMeterSegment[] = []` (attribute: false, JS-only) — `{ label: string; value:
number; tone?: 'brand' | 'success' | 'warning' | 'danger' | 'neutral'; color?: string; disabled?:
boolean }[]`. `value` is an _absolute_
  quantity measured against `total`, never a pre-computed percentage.
  `color`, when supplied, is a sanitized arbitrary CSS color that takes precedence over `tone`.
  `disabled`, when set, marks that band non-actionable while `interactive` is set: its control
  renders genuinely disabled (no tab stop, no hover/press affordance) and activating it emits no
  `lr-segment-activate`.
- `total: number = 0` — the full capacity segments are measured against (e.g. a model's context
  window size).
- `shape: ContextMeterShape = 'bar'` (`'bar' | 'ring'`, reflected) — the v9 geometry name;
  `variant` remains reserved for semantic tone across Lyra.
- `label: string = ''` — overall accessible caption, e.g. `"128K context window"`. Also rendered
  visually (`[part="label"]`) when set.
- `showLegend: boolean = false` (attribute `show-legend`, reflected) — renders a static
  `[part="legend"]` key below the meter, one swatch/label pair per `segments` entry, each swatch
  painted from that segment's resolved `color`/`tone`. Without it a segment's own label is exposed
  only through a hover `title` (desktop-only, undiscoverable) and the visually-hidden breakdown
  list, so a meter split across more than two or three categories reads as unlabeled colour to a
  sighted user. Non-interactive: it toggles nothing and emits nothing, mirroring
  `lr-sequence-strip`'s `showLegend` rather than the interactive `lr-graph-legend`. The whole
  subtree is `aria-hidden`, since `segment-list` already exposes the same names. Under
  `shape="ring"` the host stops being a fixed square so the key flows below the ring instead of
  being clipped.
- `legendDisplay: ContextMeterLegendDisplay = 'label'` (attribute `legend-display`) — what each
  legend row shows beside its swatch: `'label'` (the default, byte-identical to before this
  property existed), `'label-value'`, `'label-percent'` or `'label-value-percent'`, adding
  `[part="legend-value"]` and `[part="legend-percent"]` spans. The share is the same clamped ratio
  the bar or ring paints, so the key can never disagree with the band it stands for, and both
  numbers are formatted through `effectiveLocale`. Combinable rather than mutually exclusive,
  unlike `lr-chart`'s `label | value | percentage` legend vocabulary, because a part-to-whole key is
  ordinarily read as "count AND share". A foreign attribute value normalizes to `'label'`. No
  effect while `showLegend` is unset.
- `interactive: boolean = false` (reflected) — opt-in filter mode. Every band, and every legend row,
  becomes a real `<button>` emitting the cancelable `lr-segment-activate`; the ring's arcs carry
  `role="button"` with their own tab stop and Enter/Space handling, since an SVG shape cannot be a
  native button. In this mode, and only in this mode, `[part="legend"]` drops `aria-hidden` so the
  rows are reachable, and the visually-hidden `[part="segment-list"]` steps aside because the
  buttons already expose the same label/count pairs with their pressed state attached. A band's
  inline size IS its share, so a small band is a small pointer target: pair `interactive` with
  `showLegend` where that matters, since the legend row is the same action at full row height. The
  ring's arcs share one bounding box, so a focused arc reports itself by dimming as well as by the
  shared focus outline — an outline alone cannot say *which* arc.
- `selectedIndices: readonly number[] = []` (attribute: false) — indexes rendered as
  `aria-pressed="true"` plus a `segment-selected`/`legend-item-selected` part token on both the band
  and its legend row; every other control renders `aria-pressed="false"`. Meaningful only while
  `interactive` is set. Uncontrolled by default: an activation nobody vetoes toggles the index here
  itself. `preventDefault()` on `lr-segment-activate` suppresses that write, which is how a consumer
  that owns the selection takes control; assigning the property directly always wins either way. A
  non-integer or out-of-range entry selects nothing rather than throwing.

Accessible summaries, segment tooltips, and ring titles format normalized nonnegative quantities
using `effectiveLocale`. A host `aria-label` names the host without being duplicated on the nested
meter owner, which retains its generated aggregate summary.

**Events:** `lr-segment-activate` — a band or its legend row was activated while `interactive` is
set. `detail: { index: number; label: string; value: number }`, bubbling and composed like every
library event. **Cancelable, and a real veto point**: the default action is this component toggling
`index` in its own `selectedIndices`, so `preventDefault()` keeps the current selection and hands
that state entirely to the consumer. Never emitted in the default presentational mode. Because the
event dispatches synchronously *before* the write, a listener reading `selectedIndices` inside its
own handler sees the pre-activation value.

**Slots:** none.

**CSS parts:** `base` (a `<div>` for `bar`, an `<svg>` for `ring`), `semantic` (the visually hidden
meter semantics), `track` (the unfilled/empty capacity), `segment` (one occupied segment — carries
`data-tone` and, for custom colors, `--lr-context-meter-segment-color`), `segment-list` (the hidden
category list), `segment-item` (one hidden category/value entry), `label`, and — only under
`showLegend` — `legend`, `legend-item`, `legend-swatch` (carrying the same `data-tone` and custom
color hook as `segment`) and `legend-label`, plus `legend-value` and `legend-percent` under the
matching `legendDisplay` settings. While `interactive` is set, `segment` and `legend-item` are
`<button>`s (a `role="button"` arc under `shape="ring"`) and a selected one carries a second part
token — `segment-selected` / `legend-item-selected` — because nothing but a pseudo-class may follow
`::part()`, so the state has to live in the part name. Two further state tokens join them, and they
compose: `segment-empty` / `legend-item-empty` on a band whose `value` is 0, and
`segment-disabled` / `legend-item-disabled` on a band whose `segments` entry sets `disabled`. The
empty pair is DERIVED and carries no built-in treatment — it is the hook for your own "nothing in
this bucket" styling, and a zero band stays actionable. The disabled pair is DECLARED: that control
renders genuinely disabled (no tab stop, no hover or press affordance) and activating it emits no
`lr-segment-activate`. Inertness is never inferred from a zero value, because a zero band is
legitimately clickable in a budget meter

**Themeable custom properties:** `--lr-context-meter-segment-color` is set per segment when its
`color` field is supplied, and is read by both `segment` and its matching `legend-swatch` so the
two can never disagree. `--lr-context-meter-legend-swatch-size` (default `var(--lr-size-0-625rem)`)
sizes a legend chip on both axes. The `bar`-shape track is independently retunable:
`--lr-context-meter-track-size` (default `var(--lr-size-0-5rem)`) is its block size (and so the
block size of its filled segments), `--lr-context-meter-track-radius` (default
`calc(var(--lr-radius) * 0.5)`) its corner radius, `--lr-context-meter-track-bg` (default
`color-mix(in srgb, var(--lr-color-border) 30%, transparent)`) the background of its unfilled
remainder, and `--lr-context-meter-segment-seam-color` (default `var(--lr-color-surface)`) the
hairline seam painted between adjacent segments.
`--lr-context-meter-selected-ring-color` (default `var(--lr-color-text)`) and
`--lr-context-meter-selected-ring-width` (default `var(--lr-border-width-thick)`) paint the inset
ring marking a selected `bar`-shape band or legend row. It is drawn inward, because the track clips
its own overflow and an outward ring would be invisible, and it is a ring rather than an outline so
that it composes with the hover, press and focus outlines instead of being replaced by them — a
selected band stays visibly selected exactly while it is being pointed at or focused, and its focus
ring stays intact. `--lr-context-meter-selected-arc-stroke` (default `16`, in this component's
`0 0 100 100` viewBox units) is the stroke width of a selected `ring`-shape arc: every arc shares
one bounding box, so a selected arc reports itself by thickening in place rather than by an outline
that would trace the whole ring identically for every selection.
`--lr-context-meter-disabled-opacity` (default `var(--lr-opacity-disabled)`) dims a band or legend row whose entry sets
`disabled`; the band keeps its own colour, since that colour is the datum. Otherwise the component consumes shared tokens
`--lr-space-xs`, `--lr-color-text-quiet`, `--lr-font`, `--lr-radius`, `--lr-color-border`,
`--lr-color-brand`, `--lr-color-success`, `--lr-color-warning`, `--lr-color-danger`,
`--lr-transition-base`.

**Optional peer deps:** none.

```html
<lr-context-meter label="128K context window" total="128000"></lr-context-meter>

<lr-context-meter shape="ring" total="128000"></lr-context-meter>
<script type="module">
  const [meter, ringMeter] = document.querySelectorAll("lr-context-meter");
  meter.segments = [
    { label: "System prompt", value: 2200, tone: "neutral" },
    { label: "Conversation history", value: 61000, tone: "brand" },
    { label: "Retrieved context", value: 30800, tone: "warning" },
  ];
  ringMeter.segments = segments;
</script>
```

An internal visually-hidden semantic node carries `role="meter"` plus `aria-valuenow`,
`aria-valuemin`, and `aria-valuemax` whenever `total > 0`; without a valid positive total it uses
`role="group"` and omits numeric meter attributes. Its accessible name is the generated summary;
an authored host `aria-label` remains on the host as a distinct overall name. A separate
visually-hidden segment list exposes
each labeled quantity, while the visible track, segments, ring SVG, and visible label remain
`aria-hidden`. The summary's "used" figure is the sum of
`segments[].value`, clamped to `total` whenever `total > 0` so the announced text can never claim
more than 100% used (e.g. `segments` summing to `150000` against `total="128000"` still announces
`"128,000 of 128,000 used"`) — matching what the _visual_ meter shows, since each segment's ratio is
independently clamped so the running cumulative fill across all segments can never exceed 100% of the
bar/ring either: an over-`total` `segments` array renders as a fully (not over-) filled meter, with
later segments truncated or squeezed to zero width/arc-length as the budget runs out. `total <= 0`
(or non-finite) renders zero segments — an empty track/ring — and the announced summary falls back to
just `"{used} used"` with no `"of {total}"` clause, regardless of what's in `segments`. Ring geometry
(a 40-radius circle, 12px stroke, centered at 50,50) intentionally matches `lr-gauge`'s own radial
numbers, so the two circular-meter components in the library share one visual scale.

**Known gotchas:**

- The ring variant's per-segment `<title>` and the bar variant's per-segment `title=` attribute are
  native mouse-hover tooltips only — they sit inside `aria-hidden` markup. Screen readers use the
  hidden meter/group summary and segment list instead.
- `shape="ring"` fixes the host at `8em × 8em` (`:host([shape='ring'])`) — the bar shape's
  `inline-size: 100%` does not apply in ring mode; resize it via `font-size` or an explicit
  width/height override on the host instead.
- Segment order is significant for the ring's cumulative `stroke-dashoffset` — later entries in
  `segments` render further around the circle (starting at 12 o'clock, going clockwise); there's no
  independent sort/z-order control.

**Additional API surface:**

- `part="segment-item"` — One visually-hidden segment label/count pair.
- `part="segment-list"` — The visually-hidden list exposing the segment breakdown.
- `part="semantic"` — The visually-hidden meter/group carrying aggregate range semantics.

---
