Skip to content

Layout

Layout

App surfaces in Pod are composed from a small set of layout primitives from @norbital-ai/ui/layout. The primitives own geometry; the shell owns the app region and document scroll; and every surface follows the same body contract. Compile-time checks enforce this — a surface that breaks the rules fails the build, not just the review.

The primitives

Pick the primitive by the layout intent, not by what looks closest:

IntentPrimitiveNotes
Vertical rhythmStackColumn of children with gap, optional align/justify
One rowInlineSingle horizontal line of children
Wrapping groupClusterRow that wraps; tags, chips, button groups
Two adaptive regionsSplitNamed ratios (rail, sidebar, third, half, wide) and shared collapse tokens
Intrinsic gridGridAuto-fit cells from a minimum token (compact/card/panel)
Exact spansColumns + ColumnFixed counts (2/3/4/6) with explicit span
Top, main, bottomCoverCenters the body between optional top and bottom slots
Readable measureCenterConstrain width to a measure (narrow/reading/wide/full)
Media cropFrameFixed aspect ratio (square/portrait/landscape/widescreen)
Local scrollingBound + ScrollAlways a pair — never a bare overflow wrapper

All primitives accept gap and pad from the token scale (none, xs, sm, md, lg, xl), an as element (a whitelist of flow containers such as section, article, main, form), and their named props. Parents choose the layout algorithm; children do not request growth.

The app body contract

Every app is one Cover with a top heading slot and exactly one body region:

┌───────────────────────────────────────────────┐
│ Cover  top={pageHeading}                      │
│   ┌─────────────────────────────────────────┐ │
│   │ PageHeader title="Tasks" …              │ │
│   └─────────────────────────────────────────┘ │
│                                               │
│   body region (one of three, below)           │
│   ┌─────────────────────────────────────────┐ │
│   │ Scroll name="tasks" inset               │ │
│   │   ┌───────────────────────────────────┐ │ │
│   │   │ CollectionTable collection="tasks"│ │ │
│   │   └───────────────────────────────────┘ │ │
│   └─────────────────────────────────────────┘ │
└───────────────────────────────────────────────┘

There are exactly three legal body regions, and each owns both scroll and the app inset:

BodyOwns scroll + inset
<Tabs … />TabsContent, automatically
<Scroll name="…" inset> for flowing contentthe Scroll
<Bound size="full" inset> for one self-scrolling componentthe Bound
<script lang="ts">
  import { CollectionTable } from '@norbital-ai/ui/collection-table';
  import { Bound, Cover, Scroll } from '@norbital-ai/ui/layout';
  import { PageHeader } from '@norbital-ai/ui/page-header';
</script>

<svelte:head>
  <title>Tasks</title>
  <meta name="pod:icon" content="lucide:list-todo" />
</svelte:head>

{#snippet pageHeading()}
  <PageHeader title="Tasks" description="Everything the team is working on." />
{/snippet}

<Cover as="main" top={pageHeading}>
  <Scroll name="tasks-scroll" inset>
    <CollectionTable collection="tasks">
      {#snippet columns({ Column })}
        <Column name="title" />
        <Column name="status" />
      {/snippet}
    </CollectionTable>
  </Scroll>
</Cover>

Scrolling: one owner per axis

The shell owns the document scroll; every local scroll region is an explicit Bound + Scroll pair. Each ancestor chain has one scroll owner per axis and one inset owner; sibling panes may own their own. The rules that fall out of this are enforced at compile time:

  • No generic overflow wrappers, overflow-hidden clipping, or raw flex/min-size scroll chains.
  • No raw structural flex/grid containers, and no margins between siblings — spacing is the primitives' gap.
  • No literal px-4 sm:px-6-style classes — the app inset is the primitives' inset.
  • No style attribute on a layout primitive — use its named props.
  • Clipping is valid only for text truncation, Frame media, or an audited popup/sheet boundary.

Responsiveness without breakpoint recipes

  • Split takes collapse (stack, switch, none) and a shared collapseAt token instead of arbitrary widths.
  • Grid is intrinsic: it auto-fits cells to the available space from a minimum token. Use Columns only when an exact count or span matters.
  • Center keeps reading width without hand-rolled media queries. Prefer intrinsic layout and shared container-query tokens over viewport breakpoint recipes.
Enforced, not suggested
The compiler validates the layout contract on every surface: unknown layout props, raw clipping, scroll chains, and inline styles on primitives are diagnostics, and authored source outside src/apps/ is checked the same way. See UI components for how forms fit into a surface.