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.