{
  "name": "PageShell",
  "summary": "The frame every screen sits in: header, navigation, a named content region.",
  "useWhen": [
    "building any full application screen rather than an embedded widget"
  ],
  "composes": [
    "SkipLink",
    "Header",
    "NavItem",
    "Sidebar",
    "SidebarSection",
    "SidebarItem",
    "Drawer",
    "Logo",
    "LogoMark",
    "Avatar",
    "PageHeader",
    "Button",
    "Divider",
    "ToastProvider",
    "CommandPalette",
    "Kbd",
    "Banner",
    "Popover",
    "Tooltip",
    "NotificationsPanel",
    "Stack",
    "Grid",
    "ThemeZone",
    "I18nProvider",
    "Tour",
    "AppSwitcher"
  ],
  "structure": [
    "A SkipLink first in the body, before any Banner and the Header, with `target` the id of <main>: the first Tab on any page reaches it, and Enter lands in the content. The shell renders it once; <main> carries the `id` on every route.",
    "Header at the top with the brand in `brand`, primary navigation in `center` and account actions in `end`.",
    "A Header that is dark whatever the page's theme is wrapped in `<ThemeZone theme=\"dark\" contents>`: `contents` keeps the Header the shell's sticky row, and what it opens — Notifications, an account menu — opens dark with it. What the shell mounts outside the Header, the navigation Drawer and the CommandPalette, follows the page. The page's own theme is `data-theme` on the root, never a zone around <main>.",
    "A Banner, for a notice about the whole product — maintenance, read-only mode, a scheduled deletion — goes above the Header, outside <main>, mounted by the shell so it persists across routes and is announced once. Several stack, the most severe first; one that must stay has no `dismissKey`.",
    "One <main> content region, named by the page's PageHeader: pass PageHeader a `titleId` and point <main aria-labelledby> at it. The PageHeader's title is the page's h1.",
    "ToastProvider mounted once around the whole shell, not per route.",
    "Breadcrumbs, when present, go in PageHeader's `breadcrumb` slot, inside <main> above the h1 — they describe where the content is, not where the chrome is.",
    "When navigation is hierarchical or longer than a header row holds, a Sidebar replaces the NavItems in Header's `center`: Header keeps the brand and account actions, and the Sidebar sits beside <main> as the primary navigation. Below the tablet breakpoint the same Sidebar moves into a Drawer opened by Header's own toggle with `menuType=\"dialog\"` — one copy, not two, and one menu button.",
    "A CommandPalette, when the product has one, is mounted once inside the shell beside ToastProvider, not per route. Header's `end` carries a Search button showing <Kbd shortcut=\"mod+k\" /> — the visible door for anyone who does not know the shortcut, and the only one on a phone. Everything in the palette is also reachable from the navigation.",
    "Notifications, when the product has them, are a bell in Header's `end`: a Tooltip inside a Popover titled Notifications, size `lg`, whose content is a NotificationsPanel. The bell's name carries the count — \"Notifications, 2 unread\" — because a dot on it is not read. The shell loads them once, so the count is right on every page before the panel is opened, and opening a notification closes the panel.",
    "A product that is one of a suite has an AppSwitcher in Header's `end`, before the account: `apps` the suite and `currentApp` this product's `id`. It opens with the Header's theme, as Notifications do. The suite's products are links to their homes; the product's own sections stay in its Sidebar.",
    "Inside <main>, the page is a Stack: its sections one after another — PageHeader, a filter row, the content — with a gap from the scale (20 to 32 between sections). A row of filters or suggestions inside it is a `direction=\"row\" wrap` Stack with gap 8, never hand-written flex CSS.",
    "Content that lines up in columns is a Grid, not a Stack: a main column beside its aside is `columns={[2, 1]} collapse=\"tablet\"`, falling under it at 1023px; a set of cards is `minColumnWidth`, which follows the space with no breakpoint. Never a hand-written `display: grid` with its own media query.",
    "One I18nProvider around the whole shell, with the product's locale. Right-to-left is that locale and `dir=\"rtl\"` on <html>, set together: the stylesheets are logical and mirror on `dir` alone, but react-aria's arrow keys follow the locale. Nothing in the shell needs an RTL stylesheet of its own.",
    "A page that offers a first look round has a Tour, started from a \"Take the tour\" Button in its PageHeader actions — never on arrival. Its steps name the page's own ids; `onComplete` records that it was seen through, and the button stays for anyone who ended it early or wants it again."
  ],
  "states": {
    "loading": {
      "must": "Render the shell and the PageHeader immediately and load only what is beneath it. Never block the header on the page's data; when the title itself is data (a record's name), show the route's generic name until it arrives.",
      "why": "the navigation is what a user reaches for when a page is slow; hiding it makes a slow page feel like a broken one"
    },
    "empty": {
      "must": "A route with nothing in it still renders the shell, the heading, and a statement of what would be here.",
      "why": "a blank content region with working chrome reads as a failed load"
    },
    "error": {
      "must": "Keep the shell and the PageHeader. Put the error beneath the header inside <main>, with a retry and a way back to somewhere that works. When the whole page cannot be shown — not found, no access, failed, offline — follow the FullPageError pattern, which does this.",
      "why": "a full-page error strands the user with no navigation, so their only option is the browser's back button"
    }
  },
  "propsUsed": {
    "Banner": [
      "intent",
      "title",
      "actions",
      "dismissKey"
    ],
    "Popover": [
      "title",
      "size",
      "content",
      "isOpen",
      "onOpenChange"
    ],
    "Tooltip": [
      "label"
    ],
    "NotificationsPanel": [
      "notifications",
      "isLoading",
      "onReadChange",
      "onMarkAllRead",
      "onOpen"
    ],
    "Header": [
      "brand",
      "center",
      "end",
      "open",
      "onOpenChange",
      "menuLabel",
      "menuType"
    ],
    "NavItem": [
      "icon",
      "showChevron",
      "href"
    ],
    "Sidebar": [
      "label",
      "header",
      "footer"
    ],
    "SidebarSection": [
      "title",
      "isCollapsible",
      "actions"
    ],
    "SidebarItem": [
      "label",
      "icon",
      "href",
      "isCurrent",
      "badge",
      "actions"
    ],
    "Drawer": [
      "isOpen",
      "onOpenChange",
      "title",
      "placement"
    ],
    "ToastProvider": [
      "placement",
      "limit"
    ],
    "Logo": [
      "size",
      "wordmark"
    ],
    "LogoMark": [
      "size",
      "label"
    ],
    "CommandPalette": [
      "commands",
      "onAction",
      "isOpen",
      "onOpenChange",
      "label"
    ],
    "Kbd": [
      "shortcut"
    ],
    "PageHeader": [
      "title",
      "titleId",
      "description",
      "breadcrumb",
      "status",
      "actions"
    ],
    "Stack": [
      "direction",
      "gap",
      "align",
      "justify",
      "wrap",
      "as"
    ],
    "Grid": [
      "columns",
      "minColumnWidth",
      "collapse",
      "gap",
      "rowGap",
      "align",
      "as"
    ],
    "ThemeZone": [
      "theme",
      "contents"
    ],
    "SkipLink": [
      "target"
    ],
    "I18nProvider": [
      "locale"
    ],
    "Tour": [
      "steps",
      "isOpen",
      "onOpenChange",
      "onComplete"
    ],
    "AppSwitcher": [
      "apps",
      "currentApp",
      "footer"
    ]
  },
  "variantsUsed": {
    "ToastProvider": {
      "placement": [
        "top-right"
      ]
    },
    "Header": {
      "menuType": [
        "dialog"
      ]
    }
  },
  "a11y": {
    "requires": [
      "`menuLabel` on Header — the mobile toggle is icon-only",
      "a <nav> with its own accessible name wrapping the NavItems; Header supplies the banner landmark, not the navigation one",
      "exactly one <main>, and exactly one h1 inside it — the PageHeader's title",
      "`aria-current=\"page\"` on the NavItem for the current route — colour alone does not tell a screen-reader user where they are",
      "with a Sidebar, `isCurrent` on the SidebarItem for the current route, and a Sidebar `label` distinct from any other nav on the page"
    ],
    "notes": [
      "The desktop/tablet/mobile switch is a media query inside Header. Do not duplicate that decision in React state."
    ]
  },
  "antiPatterns": [
    {
      "dont": "a Tour that opens itself on a first visit",
      "do": "a \"Take the tour\" Button in the PageHeader",
      "why": "every step takes focus; unasked, the tour takes the page from someone who came to do something"
    },
    {
      "dont": "a ToastProvider per route",
      "why": "toasts queued before a navigation are unmounted mid-announcement, and two live regions announce over each other"
    },
    {
      "dont": "the page title as an h2 because the logo is an h1",
      "why": "the brand is not a heading; the page's subject is"
    },
    {
      "dont": "a Sidebar AND the same destinations as NavItems in Header",
      "why": "two navigation landmarks listing the same pages double every link a screen-reader user tabs through"
    },
    {
      "dont": "rendering a second nav for mobile",
      "why": "Header moves the one copy, and two copies duplicate every link in the accessibility tree"
    },
    {
      "dont": "a hand-built title block instead of PageHeader",
      "why": "the demo had five, in three shapes, with actions aligned three ways; PageHeader is the one shape, and the one place its gaps and alignment are decided"
    },
    {
      "dont": "a page laid out with hand-written flex or grid CSS, pixel gaps and its own breakpoints",
      "do": "Stack for things one after another, Grid for columns, each with a gap from the spacing scale",
      "why": "every screen then has its own spacing, and nothing checks it against the tokens"
    },
    {
      "dont": "`data-theme=\"dark\"` by hand on the Header or a wrapper around it",
      "do": "ThemeZone with `contents`",
      "why": "the attribute alone leaves the Header's menus and popovers in the page's theme, since they are portalled out of it, and a wrapper with a box of its own unsticks the Header"
    },
    {
      "dont": "a skip link anywhere but first, or none",
      "do": "SkipLink as the first element of the shell",
      "why": "a keyboard user otherwise tabs through every banner button and Header control on every page before reaching the content — WCAG 2.4.1"
    },
    {
      "dont": "the product's own sections in the AppSwitcher",
      "do": "Sidebar for sections, AppSwitcher for the other products in the suite",
      "why": "sections are moved between constantly and belong in view; hidden behind nine dots, every move costs two presses"
    }
  ]
}
