The settings shell painted the onboarding overlay (opaque stage, mask, #root inert) the moment a step was registered and not locally completed, while every step still had to load its private join before deciding whether to show — rendering null could not suppress the shell-owned chrome. Every reload on the hero therefore flashed a full-screen opaque layer (white in the light palette) for one settings/credential RPC round-trip after the session list turned ready. The chrome now belongs to the step: a new zero-cordis OnboardingSurface primitive (ui-primitives) renders the body-portaled overlay/mask/stage verbatim from the former SettingsRoot stylesheet and holds #root inert for exactly its own lifetime. WelcomeNotice and DeepSeekOnboardingDialog wrap only their visible branch in it, so their existing null branches paint and block nothing by construction. SettingsRoot keeps the coordinator unchanged but renders the elected step bare, and the settings.onboarding contract now names the surface wrap as the registrant's obligation. The onboarding e2e gains a held-join reload scenario pinning that a configured world never mounts the takeover chrome or inerts the app.
35 lines
1.4 KiB
TypeScript
35 lines
1.4 KiB
TypeScript
// OnboardingSurface: the full-viewport first-run takeover an onboarding step
|
|
// wraps its visible content in. The overlay portals to this document's body
|
|
// (the Modal precedent: ancestor stacking contexts cannot leave sticky page
|
|
// controls above the mask), and the surface holds `#root` inert for exactly
|
|
// its own lifetime — a step that renders null paints nothing and blocks
|
|
// nothing, so "should onboarding show right now" stays a plain render
|
|
// decision inside the step component.
|
|
|
|
import { useEffect } from 'react'
|
|
import type { ReactNode } from 'react'
|
|
import { createPortal } from 'react-dom'
|
|
import css from './OnboardingSurface.module.css'
|
|
|
|
/**
|
|
* Render the onboarding takeover chrome (mask + opaque stage) around one
|
|
* step's content and keep the application root inert while mounted.
|
|
* @param props.children - the step's page content, centered on the stage.
|
|
* @returns the body-portaled overlay tree.
|
|
*/
|
|
export function OnboardingSurface({ children }: { children: ReactNode }) {
|
|
useEffect(() => {
|
|
const appRoot = document.getElementById('root')
|
|
if (appRoot === null) return
|
|
appRoot.inert = true
|
|
return () => { appRoot.inert = false }
|
|
}, [])
|
|
|
|
return createPortal((
|
|
<div className={css.onboardingOverlay} role="presentation">
|
|
<div className={css.onboardingMask} aria-hidden="true" />
|
|
<div className={css.onboardingStage}>{children}</div>
|
|
</div>
|
|
), document.body)
|
|
}
|