Which overlay: sheet, split, or dialog.
← Design HQ · HTML on GitHub · Architecture — modal-slide choreography
ONE rule for every overlay: what the tap intends decides which surface opens — the real
BillingDrillOver.tsx / ModalWrapper.tsx choreography as a catalog.
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.
ModalWrapper.tsx
TopUpModal.tsx
ConnectionModal.tsx renders presentation="sheet" — never a raw centered dialog.z-[10300] > header 10000).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.PageSlideOver had shipped the split.)z-[10300] already clears a default sheet's 9500.BillingDrillOver.tsx + ModalWrapper
suppressEsc: while the receipt is open the drill-over stops handling Esc — one key press closes the receipt only.zIndexOffset=2500: backdrop 8000→10500, content 9500→12000 — clears the slide-over's fixed z-[10300].parentRecede treatment as nested modals.BillingDrillOver.tsx
hidden lg:block, no peek edge on phone.BillingOverview.tsx Remove still calls native window.confirm() — this is the canon surface for that same decision.ConfigChangeConfirmModal.tsx pattern: presentation="dialog", Cancel + destructive primary — never destroy on one click.PlatformStatusBadge, PlatformLayout.tsx
tabindex="0" keeps it keyboard-reachable.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.document.body, no backdrop, never competes for a slot.bg-background)
VersionUpdateWatcher.tsx + ui/sonner.tsx + ErrorBoundary.tsx
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.Loader2 idiom, 16px, text-sm muted, centered on bg-background.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.ModalWrapper.tsx
presentation — dialog AND sheet render as the same compact bottom sheet, drag handle, header visible.duration: Infinity), swipe to dismiss, one instance ever (deduped by id).