# PageShell

> The app frame: an icon rail whose rows peek, the page header, the content; tabs on phones.

- **Level**: organism · **Status**: stable · **Role**: surface
- **Import**: `import { PageShell, PageShellMobileNavBubble, PageShellMobileNavTrigger, PageShellMobileTabBar, PageShellRailPeek, PageShellSidebarTrigger } from "@rrios-forge/ui/organisms/page-shell";`
- **Source**: `packages/ui/src/organisms/page-shell`

**Purpose.** The slot-based layout organism for application pages. Its default frame is **Poesía's navigation (ADR-044)**: a permanent icon rail in a glass capsule centred on the window (`sidebarMode="rail"`, `sidebarPresentation="inset"`), every row in one untitled `NavGroup`, each row wrapped in `<PageShell.RailPeek>` so hovering it opens a glass panel over the page — composed from `PeekHeader`, `PeekFacts` and `PeekLinks`. Search and settings are rows; there is no top bar; phones get `<PageShell.MobileTabBar>` (`mobileNav="tabs"`). See the Playground story.

**Opt-ins.** The labelled panel (`sidebarMode="auto"` / `"panel"`, collapsible with ⌘\, ADR-014/040), the `island` and `column` presentations (ADR-025), a `topBar` for pages without a rail (ADR-009), and the floating bubble with its drawer on phones (`mobileNav="bubble"`, ADR-011/013). The stories below that show them say so.

**Identity moves.**
- The rail never grows; the page keeps its width. A detail (a count, a status) goes in the row's peek, never on the icon.
- One group, one gap: home first, settings last (J-36).
- Glass only where you navigate or act (ADR-038): the rail capsule and the peek; the content rests on the content material.

**Controls that stay in reach (ADR-045).** A screen whose content scrolls past its filters, search, view switch or section tabs puts them in `<PageShell.StickyBar title="…">` right under its `PageHeader`. At rest the bar is part of the page; once it pins it condenses onto glass and the page title folds into it, small (left out on phones). Its box never changes, so nothing jumps; it never hides focus. With `edge="bottom"` it holds a selection's or a form's actions. One bar per edge: put a view switch and its filter in the same bar. See the Sticky bars story.

**When to use.**
- Any application page: the rail frame, a page header and the content body.
- A settings page (`width="md"`), a data page (`width="xl"` + `mainPadding="flush"`), a dashboard (`width="xl"`), a marketing surface (`width="fluid"` + `frame="off"`).

**When NOT to use.**
- An overlay surface (Dialog, DropdownMenu, Popover) — these are not pages.
- A fixed authentication / onboarding screen with a single centered card → use a simpler `<main>` + `Card` directly.

**Composes with.**
- `<SidebarSection aria-label>` + one `<NavGroup>` of `<PageShell.RailPeek>`-wrapped `<NavItem>`s in the `sidebar` slot.
- `<CommandMenu>` opened by the search row (⌘K).
- `<PageHeader>` in the `header` slot — title, breadcrumb, page actions — or in the body, followed by a `<PageShell.StickyBar>` when the screen has controls to keep in reach (ADR-045).
- `<TopBar>` in the `topBar` slot only for pages without a rail (marketing, reading).

**Mobile.** The tab bar by default (five tabs at most). The bubble is an opt-in:
- With `mobileNav="bubble"` (or no `mobileTabBar`) it auto-renders at `<md` whenever a `sidebar` is provided. Override via `navBubble={false}` to suppress (and mount the compound manually for full positioning control), or `navBubblePosition="start"` to anchor it on the left.
- The bubble is `<PageShell.MobileNavBubble />`. Its predecessor `<PageShell.MobileNavTrigger />` is deprecated — kept exported one release for migration, removed in the next.

**Accessibility.**
- Renders `<main aria-label={mainLabel}>` (default `"Main content"`).
- The mobile drawer is a Radix Dialog — focus trap, Escape-to-close and overlay click are right by construction.
- The bubble announces `aria-expanded` and `aria-controls` linking to the drawer; its `aria-label` toggles between "Open navigation" and "Close navigation" depending on state.
- The shell uses an **app-shell scroll model**: only the main column scrolls. PageHeader stays sticky inside that scroll context.

**Related patterns.**
- `Patterns/Page Shells · DashboardSurface / SettingsPage / DataPage` — the canonical configurations.
- `Patterns/Information Architecture · SectionAnatomy` — why the shell stays visually quiet.
- `Patterns/Decision Matrix · LayoutPrimitive` — when PageShell vs a simpler `<main>` is the right answer.

## Use it for

- App frame / navigation → **The rail that peeks (ADR-044) — the default and the recommendation.** `PageShell` (defaults: `sidebarMode="rail"`, `sidebarPresentation="inset"`, `mobileNav="tabs"`) + `SidebarSection aria-label` + ONE untitled `NavGroup`: home, search, the rooms, settings — same size, same gap. Wrap every `NavItem` in `PageShell.RailPeek` and compose its panel with `PageShell.PeekHeader` / `PeekFacts` / `PeekLinks`. Search is a button row opening `CommandMenu` (⌘K); settings' peek holds the quick controls; phones get `PageShell.MobileTabBar` (≤ 5). No `TopBar` in an app frame. Opt-ins only when a product needs them: the labelled panel (`sidebarMode="auto"`, ⌘\\ collapse, ADR-040), `island`/`column` (ADR-025), the bubble (`mobileNav="bubble"`)
- Controls that must stay in reach while the screen scrolls (filters, search, a view switch, section tabs) — or a selection's / a form's actions → `PageShell.StickyBar` (ADR-045) under the `PageHeader`, `edge="bottom"` for actions: at rest part of the page; pinned, it condenses onto glass and carries the page title small. One bar per edge; never hides focus

## Axes

| Axis | Part | Values | Default | Meaning |
|---|---|---|---|---|
| `frame` | — | `auto` `on` `off` | `auto` | PageShell editorial frame. |
| `scroll` | — | `pane` `page` | `pane` | PageShell scroll owner. |
| `stage` | — | `field` `none` | `field` | PageShell stage paint. |
| `sidebarMode` | — | `auto` `panel` `rail` | `auto` | How the app frame shows its navigation: rail, the default, is a column of icons whose rows preview their page; auto and panel show the labelled panel instead. |
| `presentation` | — | `island` `column` `inset` | `island` | How the same thing is staged. |
| `material` | — | `solid` `vibrant` | `solid` | The layer's material (ADR-022, ADR-027). |
| `mainPadding` | — | `default` `flush` | `default` | PageShell main padding. |
| `position` | — | `start` `end` | `end` | Placement of a docked element. |

## Stories

- [Playground](/?path=/story/components-organisms-pageshell--playground) · frame: `/iframe.html?id=components-organisms-pageshell--playground&viewMode=story`
- [Apple-style desktop](/?path=/story/components-organisms-pageshell--apple-style-desktop) · frame: `/iframe.html?id=components-organisms-pageshell--apple-style-desktop&viewMode=story`
- [Dashboard](/?path=/story/components-organisms-pageshell--dashboard) · frame: `/iframe.html?id=components-organisms-pageshell--dashboard&viewMode=story`
- [Settings page](/?path=/story/components-organisms-pageshell--settings-page) · frame: `/iframe.html?id=components-organisms-pageshell--settings-page&viewMode=story`
- [Data page](/?path=/story/components-organisms-pageshell--data-page) · frame: `/iframe.html?id=components-organisms-pageshell--data-page&viewMode=story`
- [Sticky bars](/?path=/story/components-organisms-pageshell--sticky-bars) — interaction test · frame: `/iframe.html?id=components-organisms-pageshell--sticky-bars&viewMode=story`
- [With footer](/?path=/story/components-organisms-pageshell--with-footer) · frame: `/iframe.html?id=components-organisms-pageshell--with-footer&viewMode=story`
- [Frame · auto / on / off](/?path=/story/components-organisms-pageshell--frame-triptych) · frame: `/iframe.html?id=components-organisms-pageshell--frame-triptych&viewMode=story`
- [Sidebar · panel / rail / auto](/?path=/story/components-organisms-pageshell--sidebar-modes) · frame: `/iframe.html?id=components-organisms-pageshell--sidebar-modes&viewMode=story`
- [Sidebar · column (Sissificate register)](/?path=/story/components-organisms-pageshell--sidebar-column) · frame: `/iframe.html?id=components-organisms-pageshell--sidebar-column&viewMode=story`
- [Liquid Glass (inset sidebar)](/?path=/story/components-organisms-pageshell--liquid-glass) · frame: `/iframe.html?id=components-organisms-pageshell--liquid-glass&viewMode=story`
- [Sidebar · collapse (default)](/?path=/story/components-organisms-pageshell--sidebar-collapsible) — interaction test · frame: `/iframe.html?id=components-organisms-pageshell--sidebar-collapsible&viewMode=story`
- [Sidebar · rail peek](/?path=/story/components-organisms-pageshell--sidebar-rail-peek) — interaction test · frame: `/iframe.html?id=components-organisms-pageshell--sidebar-rail-peek&viewMode=story`
- [Mobile (drawer + bubble)](/?path=/story/components-organisms-pageshell--mobile) · frame: `/iframe.html?id=components-organisms-pageshell--mobile&viewMode=story`
- [Mobile · drawer open](/?path=/story/components-organisms-pageshell--mobile-bubble-open) · frame: `/iframe.html?id=components-organisms-pageshell--mobile-bubble-open&viewMode=story`
- [Mobile · bubble start (left)](/?path=/story/components-organisms-pageshell--mobile-bubble-start) · frame: `/iframe.html?id=components-organisms-pageshell--mobile-bubble-start&viewMode=story`
- [Mobile · bottom tabs (iOS register)](/?path=/story/components-organisms-pageshell--mobile-nav-tabs) · frame: `/iframe.html?id=components-organisms-pageshell--mobile-nav-tabs&viewMode=story`
- [Mobile · no nav (embedded)](/?path=/story/components-organisms-pageshell--mobile-nav-none) · frame: `/iframe.html?id=components-organisms-pageshell--mobile-nav-none&viewMode=story`
- [Container · flush body (own gutter)](/?path=/story/components-organisms-pageshell--container-padding-flush) · frame: `/iframe.html?id=components-organisms-pageshell--container-padding-flush&viewMode=story`
- [Container · opt-out (disableContainer)](/?path=/story/components-organisms-pageshell--disable-container) · frame: `/iframe.html?id=components-organisms-pageshell--disable-container&viewMode=story`
- [Tablet](/?path=/story/components-organisms-pageshell--tablet) · frame: `/iframe.html?id=components-organisms-pageshell--tablet&viewMode=story`
- [Widths](/?path=/story/components-organisms-pageshell--widths) · frame: `/iframe.html?id=components-organisms-pageshell--widths&viewMode=story`
- [Accessibility](/?path=/story/components-organisms-pageshell--accessibility) · frame: `/iframe.html?id=components-organisms-pageshell--accessibility&viewMode=story`
- [Vibrant chrome](/?path=/story/components-organisms-pageshell--vibrant-chrome) · frame: `/iframe.html?id=components-organisms-pageshell--vibrant-chrome&viewMode=story`

## Patterns consulted

- Patterns/Page Shells · DashboardSurface
- Patterns/Page Shells · SettingsPage
- Patterns/Page Shells · DataPage
- Patterns/Information Architecture · SectionAnatomy
- Patterns/Navigation · NavigationLayers
- Patterns/Density & Growth · ContentGrowth
- Patterns/Decision Matrix · ComponentChoice + LayoutPrimitive

## Decisions that name it

- ADR-003 — Storybook editorial convention and pattern↔component bidirectionality
- ADR-004 — Migrate boolean configuration props to semantic axes
- ADR-009 — Editorial Frame: PageShell v3 + new `TopBar` molecule
- ADR-010 — PageShell v4: bottom dock + mobile floating bubble
- ADR-011 — PageShell v5: adaptive lateral sidebar + mobile nav bubble
- ADR-013 — PageShell: mobile-nav polymorphism
- ADR-014 — PageShell: Apple Mail / macOS Big Sur visual register
- ADR-015 — Drawer molecule on top of Radix Dialog
- ADR-016 — NotificationCenter v2: Popover → Drawer + Provider/Hooks
- ADR-018 — Motion language as a Pattern Layer contract
- ADR-022 — The material family: `solid` and `vibrant`
- ADR-025 — The sidebar is an island, not a wall (amended: two stagings)
- ADR-026 — Sunset functional palette + the stage field
- ADR-027 — Vibrant is the system's material, not one of its members
- ADR-029 — The page gutter belongs outside the frame
- ADR-030 — Axis vocabulary: one name, one meaning
- ADR-032 — Themes compose by layers; brands are a checked layer
- ADR-033 — The material ships once, and the night is tokens
- ADR-037 — Continuity motion: a decelerating curve, a travelling indicator, view transitions and the SegmentedControl
- ADR-038 — Liquid Glass layers: glass for navigation and controls, a content material for everything read
- ADR-039 — The component premium pass: translucent fills, a selected register, quiet scroll, and a system-wide audit
- ADR-040 — The sidebar collapses by default and gives the page the room
- ADR-041 — The rail peeks instead of growing
- ADR-044 — Navigation is a rail that peeks
- ADR-045 — A screen's controls pin and condense

## Always

- Tokens only (R04); variants are semantic axes, never appearance (R02).
- Typography and actions are Poesía atoms, never raw HTML with utilities.
- Vibrant panes need a ground and never nest (ADR-027).
