Site Layout

How a site sits on the universal surface (ADR-0028). Every site has a job — Promotion, Reference, or Application — but they all land on just two layouts, because layout is about arrangement, not content. application gives you a left rail and a content well (Application and Reference both use it — only the rail's content changes); promotion moves the nav into the app bar, adds a footer, and goes full-bleed.

SiteLayout takes care of the chrome for you: it mounts the AppBar at the top, hosts the rail (or footer), wraps your page content in ContentWell, and drops in the two always-there corners (PreferencesCorner bottom-left, CommsCorner bottom-right). It also paints the page splash onto <body>. You bring the bar contents and the rail (or footer) — SiteLayout handles the rest.

You're looking at it

This design site is a real, running <SiteLayout variant="application"> — gold app bar up top, the doc TOC in the left rail, this content well in the middle, and the two silver corners holding down bottom-left and bottom-right. The map below labels the four-corner setup from ADR-0027 (register × audience):

BrandusAccountyouPreferencesyouCommsusgold · groundedsilver · ambient

Variants

Just two. Audience, permissions, and data don't earn you new layouts — internal support, customer admin, tenant admin, and platform admin all run the Application layout, and only authorization and configuration differ. Reference uses it too, swapping app nav for a doc TOC.

application

Left nav rail + content well. The rail's content differs by site purpose (app nav for Application, doc TOC for Reference, filter rail for analytical variants) — that's content, not layout.

AppBar
rail
ContentWell

promotion

Full-bleed content + footer sitemap; nav moves into the AppBar middle slot. No rail.

AppBar
ContentWell
footer

Page-level ErrorBoundary — baked in

SiteLayout wraps the route children in an <ErrorBoundary> with <ErrorState scope="page"> as the fallback. Consumers don't add the page-level boundary themselves — it ships with the Layout. AppBar, PreferencesCorner, and CommsCorner all sit OUTSIDE this boundary and self-isolate independently, so a route crash leaves the brand, the sign-in path, the preferences, and the human-comms door all reachable (per the placement doctrine in /error-handling).

tsx
<ContentWell>
  <ErrorBoundary fallback={<ErrorState scope="page" />}>
    {children}
  </ErrorBoundary>
</ContentWell>
What SiteLayout effectively renders inside the content well.

Usage

src/components/AppShell.tsxtsx
import {
  AccountSlot,
  AppBar,
  BrandSlot,
  Scroll,
  SiteLayout,
} from '@halo-compliance/ui'

export function AppShell({ children }) {
  return (
    <SiteLayout
      variant="application"
      appBar={
        <AppBar>
          <AppBar.Brand>
            <BrandSlot variant="lockup" />
          </AppBar.Brand>
          <AppBar.Middle />
          <AppBar.End>
            <AccountSlot state="signed-out" signInHref="/sign-in" />
          </AppBar.End>
        </AppBar>
      }
      rail={<DocTOC />}
      comms={{ mailto: 'hello@halocompliance.com', version: 'v1.0.0' }}
    >
      <Scroll>{children}</Scroll>
    </SiteLayout>
  )
}

Compositional parts

SiteLayout's slots are passed as props. The AppBar is a required slot; rail and footer are variant-conditional via the discriminated-union types.

variant
application | promotion

Which layout. application is the default; covers Application and Reference sites.

appBar
ReactNode

The configured <AppBar> with its three slots filled. Required.

rail
ReactNode

Left nav rail content. Required for application; rejected for promotion.

footer
ReactNode

Footer sitemap. Allowed for promotion; rejected for application.

comms
{ mailto, subjectPrefix?, version? }

CommsCorner config — pass-through for the mailto fallback.

cookieDomain
string

Pass-through to PreferencesCorner. Set to .halocompliance.com in production for cross-subdomain prefs.

children
ReactNode

Page content — typically a <Scroll> template inside.

When to use which

application
Your default. Use it for any work surface — Application sites, Reference sites (docs, runbook), and BI or analytical variants. What changes is what goes in the rail, not the arrangement.
promotion
Use it for marketing — the homepage, product pages, pricing. Full-bleed pages are happier without a rail; the top nav rides in AppBar.Middle and the sitemap lives in the footer.

Anti-patterns

  • Don't invent a third layout to support an audience. Internal support, customer admin, tenant admin, and platform admin are all the Application layout. Audience, permissions, and data don't multiply layouts (ADR-0028).
  • Don't hand-roll the corners. Preferences and Comms are part of the universal surface — SiteLayout includes them automatically. Adding them again, or replacing them with bespoke chrome, breaks the invariant per ADR-0027.
  • Don't make the shell scroll. SiteLayout is viewport-locked; only the ContentWell scrolls internally. Page-level scroll defeats the pinned AppBar and rail. If a page needs more height, use <Scroll> inside the well.
  • Don't use SiteLayout for the sign-in door. Portals get their own Layout — no AppBar, the brand sits above the centered sign-in task, and the two bottom corners are still there.
  • /templates — Scroll template; what mounts inside the content well.
  • /app-bar — the universal-surface spine: brand slot, middle (shell-conditional), language, account & auth.
  • /preferences, /comms — the two bottom-corner chrome patterns SiteLayout composes.
  • /error-handling — the placement doctrine for the baked-in boundary.