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

# `lr-responsive-panel`

- **Import** `import '@aceshooting/lyra-ui/components/lr-responsive-panel.js';` (stable tag alias; registers the tag)
- **Class** `LyraResponsivePanel`, also available unregistered from `@aceshooting/lyra-ui/components/layout/responsive-panel/responsive-panel.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** 6 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-responsive-panel`

The same slotted content either docked inline in its containing layout or presented as a
full-screen/bottom-sheet/side-anchored overlay, depending on the panel's allocated inline size.
First-party invention (no `wa-*`/`sl-*` counterpart).

**Properties:**

- `open: boolean = false` (reflected) — in the inline presentation this just means visible/mounted;
  in the overlay presentation this is the actual modal open/closed state.
- `mode: LyraResponsivePanelMode = 'auto'` (reflected) — `'auto'` tracks `overlay-breakpoint`
  against the component's allocation; `'inline'`/`'overlay'` force that presentation.
- `effectiveMode: LyraResponsivePanelEffectiveMode` (readonly) — the currently resolved
  `'inline'|'overlay'` presentation.
- `shape: LyraResponsivePanelShape = 'fullscreen'` (reflected) — only affects the overlay
  presentation's visual treatment: `'fullscreen'` covers the whole viewport; `'bottom-sheet'`
  anchors to its block-end edge and does not cover the full height; `'start'`/`'end'` anchor to the
  matching *logical* inline edge instead, like a docked sidebar's slide-in-from-the-edge overlay
  counterpart — the anchored edge and the panel's rounded free edge both flip automatically under
  `dir="rtl"` (logical `inset-inline-*`/border-radius properties, no `:dir()` selector involved).
  Has no visual effect while the effective presentation resolves to `'inline'`.
- `label: string = ''` — accessible name for the overlay presentation's `role="dialog"`, used
  verbatim when set — but a plain `aria-label` attribute on the host wins outright over `label`
  when both are present, the standard ARIA convention for a consumer that wants full control over
  the announced name (matching `lr-dialog`'s `accessibleLabel` pattern). When both the host
  `aria-label` and `label` are empty, this falls back to the `header` slot's content: a heading
  element (`h1`–`h6` or `[role="heading"]`) among the slotted header content wins if present,
  otherwise the header slot's combined text content is used (mirrors `lr-dialog`'s
  `detectHeading()` fallback, via `aria-label` rather than `aria-labelledby` since the header
  content is light DOM while `[part="panel"]` is in shadow DOM). A panel opened with none of a host
  `aria-label`, `label`, or header content uses the localized `responsivePanel` fallback (`"Panel"`
  in the default locale), so its dialog is never unnamed. Unused in the inline presentation, which
  has no dialog semantics to name.
- `overlayBreakpoint: string = '768px'` (attribute `overlay-breakpoint`) — CSS length compared with
  the component's allocated inline size in `mode="auto"`; at or below it, the effective presentation
  is `'overlay'`.

**Methods:** `close(reason: LyraResponsivePanelCloseReason = 'api'): void` — requests a close by
emitting `lr-close` with `reason` before changing `open`. A listener can call `preventDefault()` to
keep the panel open; otherwise it sets `open = false` and — only in the overlay presentation —
returns focus to whichever element triggered the open. No-op if already closed. Built-in overlay
triggers call this with `'escape'`/`'backdrop'`; a consumer's own close affordance (a footer button,
a docked panel's own toggle) should call it directly with its own reason string.

**Events:** `lr-close` (`detail: LyraResponsivePanelCloseReason` = `'escape'|'backdrop'|'api'|string`;
cancelable pre-close veto, fired by the overlay presentation's built-in dismiss triggers — Escape,
backdrop click — and by any `close()` call, in either presentation; calling `preventDefault()` keeps
the panel open and leaves active overlay chrome/focus trapping intact. A plain `open = false`
property write does **not** fire it, only going through `close()` counts as a dismissal),
`lr-mode-change`
(`detail: LyraResponsivePanelModeChangeDetail` = `{ mode: LyraResponsivePanelEffectiveMode }`; fired whenever
the _effective_ mode — not the `mode` prop's possibly-`'auto'` literal value — changes between
`'inline'` and `'overlay'`; never fired on the initial render, only for a live change thereafter).

**Slots:** default (the panel body), `header` (optional header content, rendered above the body),
`footer` (optional footer content, e.g. action buttons, rendered below the body).

**CSS parts:** `base`, `backdrop`, `panel`, `header`, `body`, `footer` (`backdrop` is only rendered
in the overlay presentation).

**Themeable custom properties:** `--lr-responsive-panel-overlay-color` (default
`var(--lr-color-overlay)` — the overlay presentation's backdrop scrim color),
`--lr-responsive-panel-sheet-max-block-size` (default `85dvh`, falling back to `85vh` where `dvh`
isn't supported — the maximum height of a `shape="bottom-sheet"` overlay panel, so a long sheet
stops short of the top of the viewport instead of covering it; it has no effect on
`shape="fullscreen"` or on the inline presentation),
`--lr-responsive-panel-side-inline-size` (default `var(--lr-size-20rem)` — the width of a
`shape="start"`/`shape="end"` overlay panel along the inline axis; no effect on any other shape or
on the inline presentation),
`--lr-responsive-panel-overlay-panel-bg` (default `var(--lr-color-surface-overlay)`), and
`--lr-responsive-panel-overlay-panel-shadow` (default `var(--lr-shadow-l)`). The latter two are
inherited inline fallbacks for `[part="panel"]` only while the effective presentation is overlay;
they do not affect inline panels. Plus shared tokens (`--lr-color-border`, `--lr-color-surface`,
`--lr-space-*`, `--lr-radius`, `--lr-shadow`).

**Optional peer deps:** none.

```html
<lr-responsive-panel
  id="settings-panel"
  label="Settings"
  shape="bottom-sheet"
  overlay-breakpoint="48rem"
>
  <span slot="header"><h2>Settings</h2></span>
  <div>Panel body content — a form, a list, anything.</div>
  <span slot="footer"
    ><button onclick="document.getElementById('settings-panel').close()">
      Done
    </button></span
  >
</lr-responsive-panel>
```

Breakpoint detection uses `ResizeObserver` on the component allocation. Resizing a parent layout
across `overlayBreakpoint` while `mode="auto"` updates the effective presentation without
unmounting or re-creating the slotted content.
Inline and overlay presentations share the same shadow DOM, so slotted content and scroll position
survive the transition. Focus already inside the panel is preserved. If focus is outside when an
open inline panel becomes an overlay, focus moves to the first composed focus target (falling back
to the panel), so it cannot remain behind `aria-modal="true"`. An allowed close restores the element
captured when the panel originally opened, even when that original open happened inline. The overlay
presentation participates in the shared modal stack rather than nesting a `<lr-dialog>`.

The granular route exports the pure
`resolveResponsivePanelEffectiveMode(mode: LyraResponsivePanelMode,
belowBreakpoint: boolean): LyraResponsivePanelEffectiveMode` resolver alongside the
`LyraResponsivePanelMode`/`LyraResponsivePanelEffectiveMode`/`LyraResponsivePanelShape`/
`LyraResponsivePanelCloseReason`/`LyraResponsivePanelModeChangeDetail` types. It's the same logic
the element's allocation observer calls: `'inline'`/`'overlay'` pass straight through
unchanged; `'auto'` resolves to `'overlay'` when `belowBreakpoint` is true, `'inline'` otherwise —
exposed standalone so a consumer can compute or unit-test the same resolution without a real browser
window.

**Known gotchas:**

- assigning `open` directly still does not emit `lr-close` and therefore cannot be vetoed; use
  `close()` when the dismissal event/reason or a close guard is required. While overlay chrome is
  active, however, the `true` → `false` state transition restores opener focus regardless of
  whether it came from an allowed `close()` call, a property write, or attribute removal.
- crossing inline → overlay while already open preserves focus that is already inside and moves
  outside focus into the panel; do not expect focus to remain on page content behind the modal.
- `shape="bottom-sheet"`/`shape="start"`/`shape="end"` have no visible effect at all while the
  effective presentation is `'inline'` — they only change the overlay presentation's
  anchoring/height/width.
- `shape="start"`/`shape="end"` are logical, not physical: `'start'` anchors to the inline-start
  edge (left in `dir="ltr"`, right in `dir="rtl"`) and `'end'` to the inline-end edge, so neither
  value alone tells you which physical side a given instance renders on without also knowing its
  resolved direction.
- 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 overlay chrome was still active
  across the move — `disconnectedCallback`/`connectedCallback` fire back-to-back with no update in
  between, so `willUpdate()` alone wouldn't otherwise notice.
- `overlay-breakpoint` follows allocation, not the viewport. Use `mode="overlay"` for a deliberate
  viewport-modal policy independent of the component's containing layout.

---
