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

# `lr-widget`

- **Import** `import '@aceshooting/lyra-ui/components/lr-widget.js';` (stable tag alias; registers the tag)
- **Class** `LyraWidget`, also available unregistered from `@aceshooting/lyra-ui/components/layout/widget/widget.class.js`
- **Family** `components/layout/` — 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** 16 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-widget`

A titled panel shell with an optional collapse toggle and an optional fullscreen-expand toggle.
First-party invention (no `wa-*`/`sl-*` counterpart). Fullscreen promotes the same host element in
place (a CSS state, not a clone/portal), so slotted content (a chart, a running simulation, scroll
position) survives the transition.

**Properties:**

- `label: string = ''` — header title; a removed attribute renders as absent while preserving
  `null` readback. Supplied empty strings remain empty and later values recover normally.
- `sublabel: string = ''` — secondary header copy with the same removal, empty-string and recovery
  behavior.
- `collapsible: boolean = false` (reflected — shows the collapse/expand chevron button)
- `collapsed: boolean = false` (reflected)
- `expandable: boolean = false` (reflected — shows the fullscreen toggle button)
- `fullscreen: boolean = false` (reflected)
- `fullscreenInset: string = ''` (attribute `fullscreen-inset`) — CSS `inset` shorthand applied to
  `[part="base"]` while fullscreen instead of the default per-side
  `max(var(--lr-space-l), <safe-area inset>)`, e.g. `"0 0 0 240px"` to leave a 240px persistent
  sidebar/toolbar visible during fullscreen. Invalid values, declaration-breaking input, and
  `url()` are ignored.
- `compact: boolean = false` (reflected) — tighter header/body padding, same convention as
  `lr-empty`'s `compact`
- `backdropInset: string = ''` (attribute `backdrop-inset`) — overrides the fullscreen backdrop's
  CSS `inset`; when empty or invalid, the backdrop remains viewport-filling (`0`) independently of
  `fullscreenInset`
- `views: readonly LyraWidgetView[] = []` (attribute: false) — named alternate views for the panel body, e.g. a
  chart/table toggle inside the same card chrome; `LyraWidgetView { viewId: string; label?: string; icon?:
TemplateResult; ariaLabel?: string }`. Each entry gets a header toggle button
  (`[part='view-toggle']`) and a `<slot name="view-${viewId}">`. An icon-only view should set
  `ariaLabel`; if both labels are omitted, the button uses `viewId` as a last-resort accessible name.
  Empty (the default) renders today's single unnamed default slot as the sole view, unchanged.
  Up to 256 valid records are snapshotted; IDs must be unique, nonempty, and whitespace-stable.
  Malformed/hostile entries are ignored without rejecting the component update.
- `activeViewId: string = ''` (attribute: false) — the currently active view's `viewId`; defaults to the
  first entry of `views` (or `''` when `views` is empty). Settable directly to control the active
  view externally; also updated internally when a view toggle is clicked. `activeView`, a
  deprecated alias that seeded this property, was removed in 16.0.0 (available since 11.2.0;
  eligible for removal from 13.0.0) — use `activeViewId`.
- `accessibleLabel: string | null = null` (attribute `aria-label`) — overrides the label-derived
  fullscreen dialog name. An explicitly empty value is retained; property, slotted-label, and
  localized fallbacks apply only when it is absent.
- `storageKey?: string` (attribute `storage-key`) — when set, persists `collapsed` to `localStorage`
  under `lr-widget:${storageKey}` and restores it on the next mount, without overwriting a
  `collapsed`/`.collapsed=${…}` binding already assigned on that same mount — including one that
  assigns `false`, the default. The restore runs once, before the first paint, and is skipped for
  any property the consumer assigned; `lr-app-rail` and `lr-table` share the same mechanism. Without
  a `storageKey` there is no persistence and storage is never touched — listen for
  `lr-collapse-change` and persist the state yourself.

**Events:** `lr-collapse-request` (cancelable; `detail: { collapsed }` is the state proposed by the
built-in collapse toggle. Call `preventDefault()` to leave `collapsed` and any persisted state
unchanged. It is not emitted when a consumer assigns `collapsed` directly), `lr-collapse-change`
(non-cancelable; `detail: { collapsed }` is the accepted built-in-toggle state. It is not emitted
when a consumer assigns `collapsed` directly), `lr-fullscreen-request` (cancelable; `detail: {
fullscreen }` is the state proposed by the fullscreen toggle, Escape, or a backdrop click. Call
`preventDefault()` to leave `fullscreen` unchanged. Not emitted when a consumer assigns
`fullscreen` directly), `lr-fullscreen-change` (non-cancelable; `detail: { fullscreen }` is the
accepted state — also fired when fullscreen is exited via Escape or a backdrop click, not just the
toggle button. Not emitted when a consumer assigns `fullscreen` directly), `lr-view-request`
(cancelable; `detail: { viewId }` is the view proposed by a header view-toggle click. Call
`preventDefault()` to leave `activeViewId` unchanged. Not emitted when a consumer assigns
`activeViewId` directly), `lr-view-change` (non-cancelable; `detail: { viewId }`, the accepted
active view's `viewId`. Not emitted when a consumer sets `activeViewId` directly), and
`lr-activate` (non-cancelable; `detail: { value }` — note the key is `value`, not `viewId` — is the
activated view's `viewId`, fired on **every** accepted header view-toggle activation whether or not
`activeViewId` actually moved. `lr-view-request` stays the veto point, and a vetoed activation emits
no activation at all. Use it for the repeat pick `lr-view-change` deliberately stays silent for —
"rebuild that view" is a real intent — which is otherwise unobservable, because the toggles live in
this shadow root, so a retargeted `click` names no view. When an activation _does_ move the view,
`lr-view-request` and `lr-view-change` are emitted first. Not emitted when a consumer sets
`activeViewId` directly)

**Slots:** default (the panel body, rendered only while `views` is empty), `icon` (optional leading
icon in the title row; its flattened subtree is inert and aria-hidden), `label` (rich label content,
overrides the `label` attribute), `sublabel` (rich sublabel content, overrides the `sublabel`
attribute), `actions` (header action controls,
rendered before the collapse/expand buttons), `collapse-icon` (replaces the built-in chevron in the
collapse toggle via native slot fallback; its assigned content is decorative, inert, and aria-hidden
so the outer toggle remains the only action. The whole button rotates while expanded, so use a
collapsed/right-facing baseline for a directional override; only meaningful while `collapsible`),
`fullscreen-icon` (replaces the built-in glyph in the fullscreen toggle — the override replaces
_both_ the "expand" and "exit fullscreen" defaults, so the consumer owns that distinction, e.g. by
reading the `fullscreen` attribute; its assigned content is decorative, inert, and aria-hidden so
the outer toggle remains the only action; only meaningful while `expandable`), and one `view-{viewId}`
slot per `views` entry, used instead of the default slot

**CSS parts:** `base`, `header`, `title`, `icon` (wrapper around the `icon` slot, hidden entirely when
empty), `label-group` (wrapper around the label and sublabel), `label`, `sublabel`, `actions`,
`view-toggles` (the header toggle-button group, only rendered when `views` is non-empty),
`view-toggle` (a single view toggle button), `view-icon` (a decorative view glyph whose rendered
subtree is inert and aria-hidden, leaving the toggle as the sole action), `view-label`
(a view's visible label), `collapse-button`, `fullscreen-button`, `body`, `backdrop`

Both header rows (`actions` and `view-toggles`) scroll horizontally on their own when the header is
too narrow for them, and each independently paints a `--lr-scroll-fade-size` edge fade while — and
only while — it actually overflows, so a clipped row reads as scrollable rather than truncated. The
overflow is measured, not assumed: a row that fits is left unmasked.
Forced-colors mode disables those decorative masks while retaining the native scroll owners. The
body is the block-axis scroll owner whenever the widget receives a constrained height, so the
header remains fixed while deep body content scrolls.

**Themeable custom properties:** `--lr-widget-overlay-color` (default `var(--lr-color-overlay)` —
the fullscreen backdrop scrim color), `--lr-widget-fullscreen-inset` (default per side
`max(var(--lr-space-l), <safe-area inset>)` — the fullscreen `[part="base"]` inset; the
`fullscreen-inset` attribute overrides it), and `--lr-widget-backdrop-inset` (defaults to `0` so
the modal backdrop covers the viewport; the `backdrop-inset` attribute overrides it), plus shared
tokens (`--lr-space-*`, `--lr-color-border/-surface/-text-quiet`,
`--lr-radius`, `--lr-shadow`, `--lr-icon-button-size`, `--lr-focus-ring-*`).

Three properties style the pressed view toggle: `--lr-widget-view-toggle-active-bg` (default
`var(--lr-color-brand-quiet)`) and `--lr-widget-view-toggle-active-color` (default
`var(--lr-color-brand)`), plus `--lr-widget-view-toggle-active-border-color` (default
`transparent`) — the background, text, and border color of the `aria-pressed="true"` toggle. All three
are **state hooks**: declared as inline `var()` fallbacks at the point of use and never on `:host`,
so setting any of them on the element _or on any ancestor_ reaches the toggle. That shape exists because
`::part(view-toggle)[aria-pressed='true']` is not valid CSS — Shadow Parts forbids an attribute
selector after `::part()` — so before these hooks the only way to recolor an active toggle was to
override the library-wide `--lr-color-brand-quiet`/`--lr-color-brand` tokens, repainting every other
element that reads them. Left unset, each falls back to exactly the token the rule used before, so
rendering is unchanged.

`--lr-widget-view-toggle-hover-bg` (default `var(--lr-color-brand-quiet)`) and
`--lr-widget-view-toggle-hover-color` (default `var(--lr-color-text)`) are the same shape for the
_hover_ state, and the `:hover` rule wraps its selector in `:where()` so a consumer's own
`::part(view-toggle):hover` override wins without `!important`.

**Optional peer deps:** none.

```html
<lr-widget label="Load profile" sublabel="Last 7 days" collapsible expandable>
  <span slot="actions"><button>Refresh</button></span>
  <div>Panel body content — a chart, a table, anything.</div>
</lr-widget>
```

While `fullscreen`, `[part="base"]` (not the host itself) takes `role="dialog"` + `aria-modal="true"`
(with `aria-label`, including an explicitly empty value, taking precedence; otherwise the `label`
property, slotted label, then `"Fullscreen panel"` supply the name), document scroll is locked
(ref-counted, safe with multiple simultaneously-fullscreen widgets), and Tab/Shift+Tab are bounded
to the panel's own focusable content (`actions` slot → collapse/fullscreen buttons → body slot,
matching visual tab order — resolved shadow-piercingly, so a slotted custom element's real
focusable target inside its own shadow root is found too) so keyboard focus can't escape to page
content hidden behind the backdrop. Escape or clicking the backdrop exits fullscreen and returns
focus to whichever button triggered it. Set `fullscreen-inset` (e.g. `"0 0 0 240px"`) to reserve
panel space for a persistent sidebar/toolbar while the default backdrop still covers the complete
viewport. Set `backdrop-inset` explicitly only when the scrim should leave the same frame open. Set
`compact` for tighter header/body padding.

The collapse-button `aria-label` is localized via its own `widgetCollapse` (default `'Collapse
panel'`) and `widgetExpand` (default `'Expand panel'`) keys.

**Known gotchas:**

- a reconnect that preserves the same element instance (e.g. a drag-and-drop reparent) resumes its
  shared overlay registration and re-acquires the scroll lock if `fullscreen` was still `true`
  across the move — `disconnectedCallback`/`connectedCallback` fire back-to-back with no update in
  between, so `willUpdate()` alone wouldn't otherwise notice.
- `collapsed` hides the body via `hidden` rather than an animated height transition — collapsing is
  instant, not a slide.
- an explicit `collapsed` assignment before the first update — an attribute, a property, or a
  framework binding, including one that pins the property to its own default `false` — always wins
  over a `storageKey`-restored value for that mount, and skips the restore entirely. For
  uncontrolled persistence, don't bind `collapsed`: read the restored value back after
  `updateComplete` and track further changes from `lr-collapse-change`, pushing the property down
  imperatively only for later external changes.
- prior to this release the collapse-button `aria-label` was localized through `lr-dock-panel`'s
  own `dockPanelCollapse`/`dockPanelExpand` keys (a borrowed, differently-named pair). A locale
  registered against those keys specifically to target `lr-widget`'s collapse button should move
  to `widgetCollapse`/`widgetExpand`; the default English strings are unchanged, and `lr-dock-panel`
  itself is unaffected.

**Additional API surface:**

- `part="view-icon"` — Decorative icon content inside a view toggle.
- `part="view-label"` — Visible label text inside a view toggle.

---
