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

# `lr-page`

- **Import** `import '@aceshooting/lyra-ui/components/lr-page.js';` (stable tag alias; registers the tag)
- **Class** `LyraPage`, also available unregistered from `@aceshooting/lyra-ui/components/layout/page/page.class.js`
- **Family** `components/layout/` — see `llms/index.md` for its siblings
- **Status** `stable` since `8.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** 22 parts, 23 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-page`

Semantic application/page shell with page-wide banner/header/subheader/footer regions, a compact
menu, primary navigation, main header/content/footer, and an aside. It derives mobile versus
desktop presentation from **its own allocated inline size**, not the viewport: a Page inside a
narrow split pane becomes mobile even on a wide monitor. The first/server-safe state is desktop;
the first live measurement corrects it before normal interaction.

Navigation has one static shadow subtree and one `navigation` slot in both presentations. Desktop
places it in the grid; mobile promotes that exact subtree into a logical-edge modal drawer. Assigned
nodes are never cloned or recreated, so focus, custom-element instances, form state, scroll state,
and event listeners survive every breakpoint crossing.

**Properties:**

- `view: 'mobile' | 'desktop' = 'desktop'` (reflected) — current allocation-derived presentation.
- `navOpen: boolean = false` (attribute `nav-open`, reflected) — mobile drawer state. Navigation is
  visible on desktop independently. The state is retained through a desktop crossing, so returning
  to mobile restores the same open drawer rather than replacing its content.
- `mobileBreakpoint: string = '768px'` (attribute `mobile-breakpoint`, not reflected) — accepts a
  bare number/px length, `rem` resolved against the live root font size, or `em` resolved against
  the Page's live font size. It is re-resolved on every allocation measurement. Invalid values,
  including `%`, viewport units, `calc()`, and `var()`, fall back to `768px`.
- `navigationPlacement: 'start' | 'end' = 'start'` (attribute `navigation-placement`, reflected) —
  a logical placement: `start` is left in LTR and right in RTL; `end` is the reverse.
- `tabindex: string = '-1'` (reflected) — the host fragment target's native focusability. The Page
  preserves an authored value and otherwise keeps the host target focusable programmatically so a
  skip link or URL fragment can transfer focus into the main content.
- `disableNavigationToggle: boolean = false` (attribute `disable-navigation-toggle`, reflected) —
  hides the built-in mobile toggle. One or more custom controls assigned to `navigation-toggle`, or
  a slotted control carrying `data-toggle-nav`, can still own the action.
- `strings`/`locale` and host `aria-label` follow the shared localization contract. `aria-label`
  overrides the localized name of the internal navigation landmark.

**Methods:** `showNavigation(): void`, `hideNavigation(): void`, and
`toggleNavigation(): void` update `navOpen`. `visiblePixelsInViewport(element: HTMLElement | null):
number` returns the element's finite, viewport-clamped vertical intersection in CSS pixels (`0` for
`null`, invalid geometry, no intersection, or an element in a detached document with no viewport).
This is a deliberate owner-realm safety divergence from Web Awesome 3.11, whose method returns
`null` for a null input and measures detached-document geometry against the ambient page viewport;
code migrating from `wa-page` should treat Lyra's always-finite `number` result as canonical.

**Events:** `lr-nav-toggle` (cancelable; `detail: { open }` is the `navOpen` state proposed by
`showNavigation()`/`hideNavigation()`/`toggleNavigation()` or a built-in dismissal — backdrop
click, Escape, or the default/custom navigation-toggle control, all of which route through those
same methods. Call `preventDefault()` to leave `navOpen` unchanged.)

The default mobile toggle is a native button with localized open/close names and explicit
`aria-haspopup="dialog"`, `aria-expanded="true|false"`, plus `aria-controls` pointing to this
Page's unique drawer. Opening
uses Lyra's shared modal overlay stack for inerting, scroll lock, Escape/backdrop dismissal, focus
trapping, stacking, reconnect suspension, and focus return. Modal inerting is scoped to the live
drawer root, so header/main/footer siblings inside the Page become inert without inerting the
drawer itself. Every custom `navigation-toggle` and the composed descendant that actually receives
focus are wired to the same state with `aria-haspopup="dialog"`, synchronized `aria-expanded`, and
a localized label when unnamed; any available assigned control opens the same Page-owned drawer,
while disabled, `aria-disabled`, hidden, and inert controls remain non-actions. The component
supplies the real drawer to the shared controls owner; current browsers normalize that inward
private relationship to the public Page host. Generated whole-value state remains authoritative
while assigned, authored relationship tokens compose, and exact initial or late-authored baselines
return when a toggle is replaced, removed, or the Page disconnects. If the opening toggle is
replaced while the drawer is open, both the ARIA owner and eventual focus-return target retarget to
the next available assigned toggle's real composed control.

`navigation-toggle-icon` is decorative visual content: its assigned subtree is inert and hidden
from assistive technology, while the native toggle retains the sole action and localized name.
Likewise, `skip-to-content` replaces only visible skip-link text; its assigned subtree is inert and
hidden from assistive technology, but its text names the Page's sole skip link. Supply text or a
glyph as appropriate to the slot, not a separate interactive control: `skip-to-content` needs
descriptive text, while `navigation-toggle-icon` needs a glyph.

The focus-visible skip link has a localized `Skip to content` fallback and focuses the unique
internal `<main>`. Native URL fragments cannot address an id inside a shadow root, so the Page host
is the unique, programmatically focusable fragment target; activation then focuses and scrolls its
own main landmark. Multiple Page instances therefore never share a global `#main-content` target.

**Slots (15):** default (main content), `aside`, `banner`, `footer`, `header`, `main-footer`,
`main-header`, `menu`, `navigation`, `navigation-footer`, `navigation-header`,
`navigation-toggle`, `navigation-toggle-icon`, `skip-to-content`, and `subheader`.

**CSS parts (22):** `aside`, `banner`, `base` and `page` (same root node), `body`,
`dialog-wrapper`, `drawer`, `footer`, `header`, `main`, `main-content`, `main-footer`,
`main-header`, `menu`, `navigation` and `navigation-desktop` (same navigation landmark),
`navigation-footer`, `navigation-header`, `navigation-toggle`, `navigation-toggle-icon`,
`skip-to-content`, and `subheader`.

**Themeable custom properties:** `--lr-page-aside-width` (default `auto`),
`--lr-page-banner-height` (`0px`), `--lr-page-header-height` (`0px`),
`--lr-page-main-width` (`1fr`), `--lr-page-menu-width` (`auto`), and
`--lr-page-subheader-height` (`0px`). The six Web Awesome spellings remain accepted as aliases:
`--aside-width`, `--banner-height`, `--header-height`, `--main-width`, `--menu-width`, and
`--subheader-height`. Set either spelling on the Page itself; the prefixed name is Lyra's canonical
form. The following interaction and overlay paints are inherited inline fallbacks, so an element or
ancestor may retheme only the named state: `--lr-page-skip-to-content-hover-bg` (default
`var(--lr-color-brand-quiet)`), `--lr-page-skip-to-content-hover-color` (default
`var(--lr-color-brand)`), `--lr-page-skip-to-content-active-bg` (default `color-mix(in oklab,
var(--lr-color-brand-quiet), var(--lr-color-mix-partner) var(--lr-color-mix-active))`),
`--lr-page-skip-to-content-active-color` (default `var(--lr-color-brand)`),
`--lr-page-navigation-toggle-hover-bg` (default `var(--lr-color-brand-quiet)`),
`--lr-page-navigation-toggle-hover-color` (default `var(--lr-color-brand)`),
`--lr-page-navigation-toggle-active-bg` (default `color-mix(in oklab, var(--lr-color-brand-quiet),
var(--lr-color-mix-partner) var(--lr-color-mix-active))`),
`--lr-page-navigation-toggle-active-color` (default `var(--lr-color-brand)`),
`--lr-page-navigation-backdrop-bg` (default `var(--lr-color-overlay)`),
`--lr-page-navigation-drawer-bg` (default `var(--lr-color-surface-overlay)`), and
`--lr-page-navigation-drawer-shadow` (default `var(--lr-shadow-l)`).

`disable-sticky` is a whitespace-token attribute, not a comma-separated value. Accepted tokens are
`banner`, `header`, `subheader`, `menu`, and `aside`; each only disables that region. Sticky offsets
use the three configured height properties, so set them to the real minimum heights when those rows
carry content. Motion uses Lyra transition tokens and is removed under `prefers-reduced-motion`.
Every region has a zero-minimum inline size and anywhere wrapping; the drawer clamps inside a 320px
allocation, and long localized or consumer-provided text cannot widen the Page.

Import only the Page registration when it is the only layout component this bundle needs:

```js
import "@aceshooting/lyra-ui/components/layout/page/page.js";
```

```html
<lr-page
  mobile-breakpoint="48rem"
  navigation-placement="start"
  disable-sticky="aside"
  style="--lr-page-main-width: 1fr; --lr-page-aside-width: 14rem"
>
  <strong slot="header">Workspace</strong>
  <button slot="header" data-toggle-nav>Sections</button>
  <h2 slot="navigation-header">Sections</h2>
  <a slot="navigation" href="/overview">Overview</a>
  <a slot="navigation" href="/reports">Reports</a>
  <h1 slot="main-header">Overview</h1>
  <p>Main content</p>
  <aside slot="aside">Related reports</aside>
  <small slot="footer">Workspace footer</small>
</lr-page>
```
