Panel

import { Panel } from "@o/pipeline/experimental/layouts/panel";

Panel builds the inside of a column: a header, the scrolling region under it, and the content cap. Inside an App column each part matches that column’s geometry. Outside App the parts still render, without column geometry.

Parts

ExportPurpose
Panel.RootThe flex column the header and body stack in. It takes the height its column leaves.
Panel.Header48px header that matches its column’s geometry.
Panel.BodyScrolls the content under Panel.Header: a fade in the body, a top fade in a flat context panel, a divider on a card or sheet.
Panel.ContentCaps body content at var(--app-content-max-width) plus gutters: 1rem on mobile, 3rem on desktop.
Panel.ContentBreakoutStretches a block inside Panel.Content past the cap to the column’s edges, still inset by the gutters.

The package also exports PanelRootProps, PanelHeaderProps, PanelBodyProps, PanelContentProps, and PanelContentBreakoutProps.

<App.Body>
  <Panel.Root>
    <Panel.Header>Deals</Panel.Header>
    <Panel.Body>
      <Panel.Content>…</Panel.Content>
    </Panel.Body>
  </Panel.Root>
</App.Body>

Panel.Root replaces a hand-written min-h-0 flex-1 column. It takes a className and a render prop.

Scrolling

Panel.Body takes the height the header leaves and marks its edges once scrolled, so rows don’t stop dead at the header. In the body column the content fades out at the header’s edge and at the bottom. In the context panel on desktop, unless it’s a drawer, it fades only under the header. On a card or sheet (a drawer layer, a drawer context panel, or any context panel below desktop) it draws a divider once scrolled instead. Outside App it fades at both edges.

The header stays outside it, because a mask on a scroll container also masks a sticky header inside it. Render it after the header, in a flex column with a bounded height. A view that scrolls itself, such as a chat transcript, skips Panel.Body.

Content breakout

Prose reads best at the content cap, but tables and charts need more room. Wrap them in Panel.ContentBreakout to stretch them to the edges of the nearest size container, still inset by the gutters. In the body column that’s the column itself, or a closer @container such as a scroll viewport.

<Panel.Content>
  <p>Prose stays at reading width.</p>
  <Panel.ContentBreakout>
    <Table.Root>…</Table.Root>
  </Panel.ContentBreakout>
</Panel.Content>

This page is itself an App body. In the column, the table wraps every cell:

CandidateWhat checked outWhy it is not verifiedSource
Northwind LogisticsHeadcount and funding match the brief.No public evidence of a revenue operations team.Company site, funding announcement
Brightline HealthUses the same CRM and recently hired a sales lead.The hiring post may be for a different region.Job board, press release
Copperleaf StudioExpansion into two new markets this year.Budget owner is not named anywhere public.Interview, LinkedIn

Broken out, it uses the column’s width:

CandidateWhat checked outWhy it is not verifiedSource
Northwind LogisticsHeadcount and funding match the brief.No public evidence of a revenue operations team.Company site, funding announcement
Brightline HealthUses the same CRM and recently hired a sales lead.The hiring post may be for a different region.Job board, press release
Copperleaf StudioExpansion into two new markets this year.Budget owner is not named anywhere public.Interview, LinkedIn

The breakout’s negative margins assume its parent is centered on the column. Keep it a direct child of Panel.Content, or inside a descendant that spans its full width, and in block flow. When the column is no wider than the content, the margins are zero. Outside the body column it’s a plain wrapper.