Skip to content
On this page Covered area

Layout

PageFrame

The page container for map-first apps. A panel, split or full page over a live stage, with a non-modal bottom sheet on phones and a stageInsets store that reports what it covers.

<script>
  import { PageFrame, stageInsets } from "@codeatlasdev/ui";
  $effect(() => map.fitBounds(bounds, { padding: stageInsets.current }));
</script>

<PageFrame mode="map" side="left" width="md" sheet label="Territory" headerBorder>
  {#snippet header()}<h2>Territory</h2>{/snippet}
  {#snippet footer()}<Button variant="filled">Save</Button>{/snippet}
  <!-- scrolling content -->
</PageFrame>

Use PageFrame when a live stage (a map, a canvas) sits behind the page content. PageShell and AppList own the whole content area; PageFrame lays the page out over the stage and tells the stage what it covers. Place it as the page content of AppShellLayout: it fills the content area and lays its panel out inside it, leaving the rail and panel alone. It does not need the shell: it fills any position: relative parent that has a height, so a one-screen map app puts the stage and the PageFrame in its own <main id="main" tabindex="-1"> (PageFrame never renders <main>). See Map page without a shell.

mode Desktop Below 900px
map A floating panel (side, width). The stage around it stays interactive. Full screen, or with sheet a non-modal bottom sheet.
split A work area of about 60% (tables, kanban) plus a stage strip. Full screen; mobileStage shows the stage instead.
page An opaque full page. The stage is hidden (data-ca-stage="hidden" on <html>). The same.

header, toolbar and footer stay fixed; only the body scrolls (overscroll-behavior: contain). The body is keyboard-scrollable while it overflows.

Covered area

The frame publishes the rectangle it covers, in px from each edge of the stage, on stageInsets. It updates on resize, mode, side, width and every sheet height, and resets to zero when the frame unmounts. When two frames are mounted the newest one wins and the previous one takes over when it leaves.

import { stageInsets } from "@codeatlasdev/ui";

stageInsets.current; // { top, right, bottom, left }, reactive
map.fitBounds(bounds, { padding: stageInsets.current });

Without JavaScript, read --ca-stage-inset-top, -right, -bottom and -left on <html>. Never copy the panel widths or the 45% sheet height into the app: they are tokens of the frame.

Bottom sheet

With sheet, mode="map" below 900px is a bottom sheet, not a Sheet: it is not modal (no backdrop, no focus trap, no scroll lock, no inert), so the stage above it still takes taps and pans. Its height snaps to a detent: peek (header only), half (45%, the default) or full. Drag the handle, or focus it and press Arrow Up and Arrow Down (Home and End jump to the ends). bind:detent follows the user.

#610

Map, split and page

Switch the mode and side; the readout shows the pixels the frame reports on stageInsets.

Territory

Covers 0px left, 0px right

Reference #610 · Pick in the gallery

Code
svelte
<script lang="ts">
  import { PageFrame, Button, TextField, SegmentedControl, stageInsets } from '@codeatlasdev/ui';

  let mode = $state('map');
  let side = $state('left');
  let query = $state('');
</script>

<div class="stage">
  <!-- The stage stand-in: a real map keeps this area interactive and pads fitBounds with stageInsets. -->
  <div class="map" aria-hidden="true">
    <span class="pin" style="--x: {50 + (stageInsets.current.left - stageInsets.current.right) / 16}%"></span>
  </div>

  <PageFrame mode={mode as 'map' | 'split' | 'page'} side={side as 'left' | 'right'} width="sm" label="Territory" headerBorder>
    {#snippet header()}
      <h2 class="title">Territory</h2>
      <p class="sub">Covers {stageInsets.current.left}px left, {stageInsets.current.right}px right</p>
    {/snippet}
    {#snippet toolbar()}
      <TextField label="Search" bind:value={query} />
    {/snippet}
    <SegmentedControl aria-label="Mode" bind:value={mode} items={[{ value: 'map', label: 'Map' }, { value: 'split', label: 'Split' }, { value: 'page', label: 'Page' }]} />
    <SegmentedControl aria-label="Side" bind:value={side} items={[{ value: 'left', label: 'Left' }, { value: 'right', label: 'Right' }]} />
    {#snippet footer()}
      <Button variant="filled" size="sm">Save</Button>
    {/snippet}
  </PageFrame>
</div>

<style>
  .stage {
    --ca-vh: 380px;
    position: relative;
    width: 100%;
    height: 380px;
    contain: layout paint;
    overflow: hidden;
    border-radius: var(--ca-radius-lg);
    box-shadow: inset 0 0 0 1px var(--ca-outline-subtle);
  }
  .map {
    position: absolute;
    inset: 0;
    background: var(--ca-surface-sunken, var(--ca-outline-subtle));
  }
  .pin {
    position: absolute;
    top: 50%;
    left: var(--x);
    width: var(--ca-space-4);
    height: var(--ca-space-4);
    border-radius: var(--ca-radius-pill);
    background: var(--ca-brand-main);
    transition: left var(--ca-duration-base) var(--ca-ease-standard);
  }
  .title {
    margin: 0;
    font-size: var(--ca-font-size-h6);
  }
  .sub {
    margin: 0;
    color: var(--ca-text-secondary);
    font-size: var(--ca-font-size-body2);
  }
</style>

Props

PropTypeDefaultDescription
mode'map' | 'split' | 'page''map'`map`: floating panel. `split`: work area plus stage strip. `page`: opaque full page, stage hidden.
side'left' | 'right''left'Edge the panel (or the split work area) sits on.
width'sm' | 'md' | 'lg' | 'xl''md'Panel width on desktop: 352, 416, 544 or 704px. Ignored by `split` and `page`.
sheetbooleanfalseBelow 900px, `mode="map"` is a non-modal bottom sheet instead of a full-screen layer.
detent'peek' | 'half' | 'full''half'Sheet height step (bindable). Dragging the handle or its arrow keys change it.
mobileStagebooleanfalse`mode="split"` below 900px: show the stage instead of the work area.
paddedbooleantruePads the body. Turn off for full-bleed tables.
headerBorderbooleanfalseSeparator under the header.
publishInsetsbooleantruePublish the covered rectangle on `stageInsets`.
labelstring'Panel'Accessible name of the panel region (localized default).
headerSnippet–Title, description and actions. Fixed.
toolbarSnippet–Fixed row under the header: search, filters, tabs.
footerSnippet–Fixed row at the bottom: primary actions.
childrenSnippet–Scrolling body.
classstring''Extra class names.