Design preview · not yet built

Overlay choreography — sheets, slide-overs, dialogs, popovers

Which overlay: sheet, split, or dialog.

SizePortrait scrolls 375 · sideways is a slide deckNotes
Design brief — overview · scope · journey (closed)

ONE rule for every overlay: what the tap intends decides which surface opens — the real BillingDrillOver.tsx / ModalWrapper.tsx choreography as a catalog.

do something → bottom sheetgo somewhere → left slide-overanswer a question → centered dialogpopover = a footnote, not a task

The widths drawn here are samples of a fluid continuum, not the only states that exist — content (spacing, type, gaps, card/column widths) scales continuously between them, each clamp reading a named container. Structure (rail, row↔stack, grid columns) steps only at sm 640 · md 768 · lg 1024; radius, hairlines, and pills never scale. Kit: design-canon.md U8.

Reading order in this document: § Desktop journey first — every desktop frame, full flow, journey order — then § Mobile journey — 375 below it — every mobile frame, same order, laid out side by side as a wrapping grid (never a tall single-file column of phone screens). Two sections, never interleaved — a review-document convention only (this repo's design-canon.md § Mockup review reading order). Mobile 375px is still designed and scored FIRST in the /design ritual (kit canon U5/U8) — the two rules never conflate.

Part I · Active — build now
Sheet
Do something
Bottom sheet
A task you complete and dismiss. Rises from the bottom over the dimmed page; the reveal gap up top keeps the page peeking through so you never lose context. The header stays fully visible above it the whole time — the dim and the sheet only occupy the content region below the masthead, never the header itself (C1).
Examples Add funds · add a card · pick a variation
Go somewhere
Left slide-over
A destination with its own URL and a back arrow. Slides in as the real page — masthead stripped. At ≥1024px it is the 40/60 split — origin LEFT 40%, live; below that, a full-screen push with a dimmed peek edge that stays tap-to-return.
Examples Wallet activity · invoicing details · Edit in Brain
Answer a question
Centered dialog
One short message, one or two buttons. Small, centered, quiet — you read it and answer, you don't work in it. On mobile it collapses into the same compact bottom sheet as everything else.
Examples Remove payment method? · turn off auto-reload? · confirm settings change
Decision test — ModalWrapper.tsx
  • Would the founder read/scroll/work in it, or just answer it? Read/work → sheet · answer → dialog · unsure → dialog.
  • Slide-over = the one case a sheet can't cover: only surface with its own URL — survives refresh and a share link.
MystFlo Platform Preview
· Peyton
Wallet balance
$41.20
Add funds to your wallet
$25
$50
$100
$200
Custom  $ ___
Card · Link · Apple Paypayment form — always rendered, no “Continue” hop
Never expires · refundable anytime — it’s your money.
Add $50
C1 — Desktop sheet
  • Dim + sheet scoped to the region below the header — masthead never covered or dimmed.
  • ~20px reveal gap under the header — the dimmed balance peeks through ("you're still here" cue).
  • ONE centered 560px work column; primary CTA in a pinned footer band outside the scroll — never below the fold.
  • Same sheet, both viewports.
768 not yet drawn for this flow. Desktop and 375 are the contract.
Wallet balance
$41.20
Wallet running low — add funds
+ Add funds
Add funds to your wallet
$25
$50
$100
$200
Custom  $ ___
Card · Link · Apple Paypayment form — always rendered, no “Continue” hop
Add $50
Never expires · refundable anytime — it’s your money.
C1 — Mobile sheet · TopUpModal.tsx
  • One surface, no hop: amount rows + Stripe payment form together from the first tap — no "amount → Continue → payment" screen.
  • Header sits OUTSIDE the dim/sheet layer; drag handle, 20px reveal gap under the masthead, home-bar safe padding.
3Connection modals are sheets — correct: sheet
Connects
AI Model MystFlo AIConnected
Google SheetsConnect
CalendlyConnect
Connect Google Sheets
MystFlo reads and writes booking rows in a sheet you choose. Nothing is shared until you pick one.
Accountpeyton@mystflo.com
Continue to Google →
T1086 · C1 — Correct: sheet
  • Every provider/connection dialog (Google Sheets, Stripe, Calendly, WhatsApp) rises as the same bottom sheet, header visible above it.
  • ConnectionModal.tsx renders presentation="sheet" — never a raw centered dialog.
3Connection modals are sheets — anti-pattern: centered popup
Connects
AI Model MystFlo AIConnected
Google SheetsConnect
CalendlyConnect
Connect Google Sheets
Sign in to continue.
retiring Anti-pattern — centered popup
  • A handful of connection dialogs still open as a small centered card — a different shape from every other connect flow.
  • Being folded into the sheet standard (left column) — one choreography for "connect an app", not two.
4Go somewhere — wallet activity (full-width push)
Split
← Billing · Wallet activity
Top-up Jul 18+$50.00
On-Brand Support run Retreat DMs · #4K2Jul 18−$0.08
AI usage Jul 18−$0.02
Retreat Booking run Uglywoo Retreat · #7J1Jul 17−$1.75
Read My Site Jul 16−$2.50
Refund to card Jul 10−$15.00
Top-up Jul 02+$10.00
Full statement for the period — every credit and debit.
Billing
Wallet balance$41.20
Auto top-up at $10.00On
Payment method Visa · 4242Edit
Stays live while you read — top up here without losing your place.
Desktop split — the go-somewhere end state
  • Masthead stripped — panel sits ABOVE the dimmed header (z-[10300] > header 10000).
  • 40/60 split at ≥1024px (modalTokens.slide.split.engageBreakpoint): the page she came from docks LEFT at 40%, live; the destination she opened takes RIGHT 60%. BOTH panes are live — neither is inert. Return is the backline, and Esc.
  • Below 1024px the 64px peek strip is still correct — no room for two columns, so it stays a full-screen push with a dimmed sliver + rotated "Tap to return"; pointer-events pass through, a click anywhere on the strip closes. The peek was never wrong, only wrong as the DESKTOP end state.
  • One task in focus still holds — the 40% pane is the origin’s context, not a second task. It is where you came from, kept live so you don’t lose your place; it is not a place to start new work.
Step 1
Sheet is open
Assistant detail
Edit in Brain →
Assistant detail sheet, header visible above it (C1). The founder taps a cross-reference link inside — “Edit in Brain,” “View team,” “See connected app.”
Step 2
Slide-over enters, sheet parks
sheet · 9500 (parked)
slide-over · 10300
Edit in Brain slides in from the LEFT as the real page, rendering ABOVE the sheet — no extra z-index math needed, the slide-over's fixed 10300 already clears a default sheet's 9500. The sheet doesn't close, it recedes underneath and waits.
Step 3
Return restores the sheet
Peek edge or backline (← Assistant detail · Edit in Brain) returns to the sheet exactly where it was parked — nothing re-opens or re-fetches, it was waiting, not closed.
Combined choreography
  • One task in focus at a time. At ≥1024px going somewhere opens the 40/60 split (origin LEFT 40% live, destination RIGHT 60%) — still one task, with its origin kept in view rather than a second task alongside. Below the breakpoint it layers with a peek edge. (Updated 2026-07-26 — this file drew the peek as the desktop end state while R35 and PageSlideOver had shipped the split.)
  • Reverse stack of Frame 6: slide-over above a sheet needs NO offset — its fixed z-[10300] already clears a default sheet's 9500.
  • Return restores the parked sheet — nothing re-opens or re-fetches.
slide-over · z 10300 (fixed)
← Billing · Wallet activity
Top-up Jul 18+$50.00
On-Brand Support run Retreat DMs · #4K2Jul 18−$0.08
AI usage Jul 18−$0.02
Tap to return
receipt backdrop · z 10500 (8000 + 2500 offset)
receipt sheet · z 12000 (9500 + 2500 offset)
Receipt · Jul 18 top-up
Amount$50.00
CardVisa ···· 4242
DateJul 18, 2026
StatusPaid
Download PDF
Stacked case — BillingDrillOver.tsx + ModalWrapper
  • Receipt opens FROM inside the slide-over — renders above it, never replaces it.
  • suppressEsc: while the receipt is open the drill-over stops handling Esc — one key press closes the receipt only.
  • Receipt = plain ModalWrapper with zIndexOffset=2500: backdrop 8000→10500, content 9500→12000 — clears the slide-over's fixed z-[10300].
  • Slide-over recedes (dim + slight blur) but stays mounted — same parentRecede treatment as nested modals.
768 not yet drawn for this flow. Desktop and 375 are the contract.
← Billing · Wallet activity
Top-up Jul 18+$50.00
On-Brand Support run Retreat DMs · #4K2Jul 18−$0.08
AI usage Jul 18−$0.02
Retreat Booking run Uglywoo Retreat · #7J1Jul 17−$1.75
Read My Site Jul 16−$2.50
Refund to card Jul 10−$15.00
Top-up Jul 02+$10.00
Full statement for the period — every credit and debit.
Mobile <lg — BillingDrillOver.tsx
  • Panel runs full-width — peek strip is hidden lg:block, no peek edge on phone.
  • Backline is the only exit besides Esc/back-gesture (page-nav feel at small sizes).
7Confirm — same compact bottom sheet
Dialog
MystFlo Platform
Billing setup
Visa ···· 4242 defaultRemove
Remove payment method?
Visa ···· 4242 will no longer be used for top-ups. This can't be undone.
Desktop confirm dialog
  • ~380px card centered below the header, quiet backdrop dim.
  • Closes a real gap: BillingOverview.tsx Remove still calls native window.confirm() — this is the canon surface for that same decision.
  • Matches the locked ConfigChangeConfirmModal.tsx pattern: presentation="dialog", Cancel + destructive primary — never destroy on one click.
MystFlo Platform Preview Building in public Assistants aren’t live yet — we’re building MystFlo in the open. Help build this →
· Peyton
Wallet balance
$41.20
C3 — Popover · PlatformStatusBadge, PlatformLayout.tsx
  • Not a task, not a destination — a footnote.
  • Opens on pointer hover or keyboard focus, never click; cursor stays default, tabindex="0" keeps it keyboard-reachable.
  • Radix Popover anchored to the badge: exact copy ("Building in public" / "Assistants aren't live yet — we're building MystFlo in the open." / "Help build this →" to WhatsApp), 250px, below-left with arrow.
  • No backdrop, no focus trap, no scroll-lock — explains, never blocks; closes on pointer-leave or blur.
Receipt sheet contentModalWrapper, zIndexOffset=2500 (9500+2500)
12000
TooltipsdesignSystem.zIndex.tooltips
10500
Receipt sheet backdropModalWrapper, zIndexOffset=2500 (8000+2500)
10500
Left slide-overBillingDrillOver.tsx, fixed z-[10300]
10300
Global headerdesignSystem.zIndex.header — above every default modal
10000
Sheet / dialog content (default)ModalWrapper.tsx, modalTokens.zIndex.content
9500
Sheet / dialog backdrop (default)ModalWrapper.tsx, modalTokens.zIndex.backdrop
8000
Mobile sidebar drawerdesignSystem.zIndex.sidebar
1200
Sidebar backdropdesignSystem.zIndex.sidebarBackdrop
1100
Page contentbase layer
0
Z-index map — reading notes
  • Popover isn't on the map — Radix portals it to document.body, no backdrop, never competes for a slot.
  • Slide-over's fixed 10300 sits ABOVE the header's 10000 on purpose (masthead stripped); default sheets/dialogs sit BELOW at 9500 so the header stays reachable.
  • Sheets don't need a z trick for header visibility (C1) — dim + sheet are DOM-scoped below the header, no overlap to resolve.
  • System toast (f10) portals at 9999 — under the global header (10000), above default sheet/dialog content (9500): visible over any open modal, never over the masthead.
Mid-task page · dialog or typing
New version available
Refresh to get the latest.
Desktop — bottom-right, 356px
  • Quiet corner placement; the founder finishes their thought, then chooses when.
Updating to the latest version...
Self-heal beat — full screen, phone and desktop identical (centered on bg-background)
  • Shown for the ~1 reload beat when a stale chunk is caught mid-navigation — calm, not an error screen.
System toast + self-heal · VersionUpdateWatcher.tsx + ui/sonner.tsx + ErrorBoundary.tsx
  • Story: As a founder mid-task, I want a deploy that lands under my open tab to ask before it refreshes, so that nothing I typed is thrown away and I never meet a crash screen.
  • Acceptance (cited, prd-pl-platform-core.md → Deploy Continuity): AC-CORE-18 ↗ typing or a dialog open ⇒ no reload; the update waits behind a dismissible toast. AC-CORE-20 ↗ a replaced lazy chunk reloads once behind "Updating to the latest version...", not the error screen, with no Sentry/Slack noise. AC-CORE-17 ↗ / AC-CORE-19 ↗ the silent paths this frame deliberately draws nothing for — section change, and return to a tab idle > 5 min. AC-CORE-21 ↗ a second failure within 30s falls through to the real crash screen — the beat must never loop.
  • When it shows: ONLY on AC-CORE-18's mid-task condition. Every other case is a silent reload — no toast, no beat, no evidence. Dismissal is swipe (mobile) or drag/Refresh (desktop); there is deliberately no ✕ — sonner's stock close is a 20px target hung off the top-left corner, below our 44px floor and off-brand.
  • Copy locked (sentence case, no jargon): "New version available" / "Refresh to get the latest." / action Refresh. The beat: "Updating to the latest version..." — spinner is the platform Loader2 idiom, 16px, text-sm muted, centered on bg-background.
  • Chrome (requires an explicit override — sonner's stock action is NOT compliant): bg-background + border-border hairline + shadow; white-on-black action at min-h-[44px] mobile / h-9 (36px) desktop and rounded-md (10px, matching the platform Button primitive — not rounded-lg). Sonner ships [data-button] at 24px tall / 4px radius / 12px type / 8px padding-x — under the 44px mobile touch floor, under the h-9 platform default, and off the radius ladder. Every overriding utility needs a `!` prefix (e.g. !rounded-md, sm:!h-9): sonner injects its own <style> into <head> at runtime, AFTER the app stylesheet, at equal specificity — an un-`!`'d utility silently loses the cascade tie and renders sonner's default while looking correct in the source. Confirmed live on a preview deploy: `min-height` and `focus-visible` won without `!` only because sonner never sets those two properties itself — everything else (radius, type size, padding, desktop height) needs it. See ui/sonner.tsx → toastOptions.classNames.actionButton for the full string; lesson recorded at lessons-learned/frontend.md#sonner-runtime-style-tie. Focus ring must also be re-specified: sonner's default is rgba(0,0,0,.4), invisible on our black surface — its rule is `:where()`-wrapped (lower specificity), so ours wins without `!`. No richColors tinting on system toasts — B&W only. One tier boundary: sonner switches mobile→desktop placement at 600px, so ~768px tablet takes the desktop bottom-right treatment drawn above.
  • Never: auto-dismiss timer, stacking, colored fill, a second toast for the same build, blocking the page, appearing over the masthead (z 9999 < header 10000).
768 not yet drawn for this flow. Desktop and 375 are the contract.
Billing setup
Visa ···· 4242 defaultRemove
Remove payment method?
Visa ···· 4242 will no longer be used for top-ups. This can't be undone.
C1 — Mobile confirm · ModalWrapper.tsx
  • Mobile branch never branches on presentation — dialog AND sheet render as the same compact bottom sheet, drag handle, header visible.
  • "Dialog" only means "centered card" on desktop.
10System toast — update toast
Mid-task page · dialog or typing
New version available
Refresh to get the latest.
Mobile 375 — update toast
  • Full-width minus 16px gutters, bottom-anchored, no backdrop — never blocks the task.
  • Persistent until acted on (duration: Infinity), swipe to dismiss, one instance ever (deduped by id).
  • Action is 44px tall here — the mobile touch floor, not the h-9 desktop default.
Part II · Future — build later
No deferred items — everything above is the current build.