Components / Canvas Sheet

Canvas Sheet

The stacking panel system inside the canvas column: every area page is a base sheet, and detail views stack on top of it. Overlays in the app are stacked CanvasSheets, not modals.

import { CanvasSheet, CanvasSheetBody, CanvasSheetChrome, CanvasSheetFooter, CanvasSheetHeader, CanvasSheetToolbar } from '@layers/ui/canvas-sheet'

Rendered stock

House rules

  • Sheets are siblings inside the canvas main column, never nested; a detail view renders after its base sheet, not inside it.
  • stackedDepth on the base sheet drives the recession (0 normal, up to 3); the base sheet stays mounted while details stack above.
  • onClose on the top sheet renders the X and handles Escape; the state that gates a sheet lives in URL search params or layout state, not the page component.
  • CanvasSheetChrome is the sticky header for tabbed sheets (NavTabs plus an optional CanvasSheetToolbar, every toolbar control size="sm"); CanvasSheetHeader is the standardized title row for chrome-less sheets, never a hand-rolled h2. Every sheet carries at least one of the two (founder ruling 2026-08-06): no anonymous sheets. Titles are Title Case; a description under the title is optional and defaults to absent.
  • Scroll-away chrome (founder direction 2026-08-15, the vertical-real-estate pattern): pass the NavTabs to CanvasSheet's `chrome` slot (always visible, above the scroller) and render CanvasSheetToolbar with `reveal` as a direct child of the sheet, before the body. The toolbar then scrolls away with the content and slides back in as an overlay on any upward scroll (the marketing SiteHeader mechanics: scroll-coupled hide, transitioned reveal, overscroll clamped, focus-within reveals, always shown near the top). Body content gains plain `sticky top-0` headers (StickySectionHeader) that pin under the tabs. On `fill` pages the scroller never moves, so the same markup keeps the toolbar pinned — use one composition for the whole area.
  • CanvasSheetFooter is the standard action row for the footer slot, never a hand-rolled div. It aligns LEFT with children in visual order — primary first, the dismiss second (the inverse of a Dialog's order): actions align with the column they act on. The dismiss reads CLOSE, never Cancel (founder ruling 2026-08-06): a sheet is a place you leave, and Cancel beside a disabled pristine Save implies aborting edits that were never made. Save stays disabled until dirty — that is the affordance. Dialogs keep Cancel.
  • A subpage's toolbar mounts in CanvasSheetToolbar inside the route layout's chrome — never as page content in CanvasSheetBody. A body-mounted toolbar double-stacks chrome + body spacing and scrolls away instead of sticking; toolbar ↔ page state goes through URL search params or a small zustand store, never props across the Outlet.
  • Toolbars fill from the left; negative space lives on the right, matching the NavTabs above. A lone filter is left-aligned — anchoring a control right (justify-between) is the deliberate option for balance or separation: the accounts scope filter, a primary action.
  • Vertical rhythm is automatic: Body carries pt-6, dropping to pt-0 when the chrome holds a toolbar, and the scroll region carries the bottom — 40px, mirroring the 40px sides (112px when a footer is passed, because the footer is out of flow and content scrolls under its blur). Never add manual pt/pb/gap; a pb-* on a page inside a sheet is always wrong (founder ruling 2026-07-31).
  • CanvasSheetBody takes `fill` when the page inside is ONE region that fills the sheet and scrolls internally (a DataTable, the events two-pane, the schedule calendar) rather than a document that scrolls the sheet. `fill` adds min-h-0, giving the body a definite height so an h-full/flex-1 child lands exactly 40px off the bottom edge at any chrome height and any viewport. It is opt-in because the default must stay min-height: auto — that is what lets a tall document grow and scroll the sheet. A viewport constant cannot do this job: it has to assume a chrome height, which left 88px of dead space under the events pane and 56px under a toolbar page's table.

See also