fix(ui): move the onboarding takeover chrome into the step

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.
This commit is contained in:
imccyu
2026-08-06 20:43:22 +08:00
parent 6a32047e77
commit 63b80956f2
20 changed files with 317 additions and 106 deletions
@@ -219,33 +219,3 @@
clip: rect(0 0 0 0);
white-space: nowrap;
}
/* First-run stage: keep the product top bar visible, then let onboarding own
the complete workspace instead of presenting another settings modal. */
.onboardingOverlay {
position: fixed;
inset: 0;
z-index: 1100;
}
/* Mask */
.onboardingMask {
position: absolute;
left: 0px;
right: 0px;
top: 80px;
bottom: 0px;
background: rgba(0, 0, 0, 0.24);
/* Mask-blur */
backdrop-filter: blur(2px);
}
.onboardingStage {
position: absolute;
z-index: 1;
inset: 0;
display: flex;
justify-content: center;
overflow: hidden;
background: var(--dsw-alias-bg-layer-1);
}
@@ -7,10 +7,11 @@
* aria-labelledby the title node; close: visually-hidden slot text). Modal
* open state and the active section id are component-local viewing state;
* the onboarding coordinator mounts exactly one ordered registrant while the
* sessions-derived empty-Hero fact is active.
* sessions-derived empty-Hero fact is active — the takeover chrome
* (OnboardingSurface) belongs to the step, so a mounted-but-deciding step
* paints nothing here.
*/
import { useCallback, useEffect, useId, useRef, useState } from 'react'
import { createPortal } from 'react-dom'
import clsx from 'clsx'
import { IconCloseOutline16, IconDataOutline16, IconSettingsOutline16 } from '@deepseek-ai/dsh-client-ui-primitives'
import type { SettingsRootComponentProps, SettingsSectionRow } from './contract/slots.ts'
@@ -134,14 +135,6 @@ export function SettingsRoot(props: SettingsRootComponentProps) {
})
}, [])
useEffect(() => {
if (onboardingStep === undefined) return
const appRoot = document.getElementById('root')
if (appRoot === null) return
appRoot.inert = true
return () => { appRoot.inert = false }
}, [onboardingStep])
return (
<>
<button
@@ -162,18 +155,15 @@ export function SettingsRoot(props: SettingsRootComponentProps) {
onClose={close}
/>
)}
{onboardingStep !== undefined && createPortal((
<div className={css.onboardingOverlay} role="presentation">
<div className={css.onboardingMask} aria-hidden="true" />
<div className={css.onboardingStage}>
{renderSlot('settings.onboarding', {
stepId: onboardingStep.id,
complete: () => { completeOnboardingStep(onboardingStep.id) },
openSection,
}, { only: onboardingStep.id })}
</div>
</div>
), document.body)}
{/* The takeover chrome (OnboardingSurface: mask, opaque stage, `#root`
inert) lives inside the step component, wrapped around its visible
content — a step still deciding (private facts loading) renders
null, so nothing paints or blocks while it decides. */}
{onboardingStep !== undefined && renderSlot('settings.onboarding', {
stepId: onboardingStep.id,
complete: () => { completeOnboardingStep(onboardingStep.id) },
openSection,
}, { only: onboardingStep.id })}
</>
)
}
@@ -57,7 +57,13 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
* Root-scoped onboarding steps contributed by settings features. The
* shell mounts one ordered step at a time; the active registrant either
* completes itself or keeps ownership until the user completes its sole
* path. Registrants own readiness, copy, and dialog behavior.
* path. Registrants own readiness, copy, dialog behavior, AND the
* takeover chrome: a step wraps its visible content in the
* OnboardingSurface primitive (mask, opaque stage, `#root` inert) and
* renders null while its private facts are still loading — the shell
* paints no chrome of its own, so a mounted-but-deciding step shows and
* blocks nothing (the reload white-flash fix; a bare unwrapped step
* would render without mask or stage).
*/
'settings.onboarding': { kind: 'list'; scope: 'root'; owner: SettingsOnboardingOwnerProps }
}