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:
| Intent | Primitive | Notes |
|---|---|---|
| Vertical rhythm | Stack | Column of children with gap, optional align/justify |
| One row | Inline | Single horizontal line of children |
| Wrapping group | Cluster | Row that wraps; tags, chips, button groups |
| Two adaptive regions | Split | Named ratios (rail, sidebar, third, half, wide) and shared collapse tokens |
| Intrinsic grid | Grid | Auto-fit cells from a minimum token (compact/card/panel) |
| Exact spans | Columns + Column | Fixed counts (2/3/4/6) with explicit span |
| Top, main, bottom | Cover | Centers the body between optional top and bottom slots |
| Readable measure | Center | Constrain width to a measure (narrow/reading/wide/full) |
| Media crop | Frame | Fixed aspect ratio (square/portrait/landscape/widescreen) |
| Local scrolling | Bound + Scroll | Always 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:
| Body | Owns scroll + inset |
|---|---|
<Tabs … /> | TabsContent, automatically |
<Scroll name="…" inset> for flowing content | the Scroll |
<Bound size="full" inset> for one self-scrolling component | the 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
overflowwrappers,overflow-hiddenclipping, 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
styleattribute on a layout primitive — use its named props. - Clipping is valid only for text truncation,
Framemedia, or an audited popup/sheet boundary.
Responsiveness without breakpoint recipes
Splittakescollapse(stack,switch,none) and a sharedcollapseAttoken instead of arbitrary widths.Gridis intrinsic: it auto-fits cells to the available space from aminimumtoken. UseColumnsonly when an exact count or span matters.Centerkeeps reading width without hand-rolled media queries. Prefer intrinsic layout and shared container-query tokens over viewport breakpoint recipes.
src/apps/ is checked the same way. See UI components for how forms fit into a surface.Related guides
- Apps — app metadata and client access
- UI components — collection surfaces inside a body region
- Navigation state — record-detail stacks and the sidesheet