feat(web): present onboarding as a continuous page
This commit is contained in:
@@ -1,14 +1,136 @@
|
||||
.dialog {
|
||||
width: min(420px, 100%);
|
||||
.page {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: min(640px, calc(100vw - 64px));
|
||||
max-height: 100vh;
|
||||
padding: clamp(64px, 9vh, 108px) 0 40px;
|
||||
box-sizing: border-box;
|
||||
overflow-y: auto;
|
||||
color: var(--dsw-alias-label-primary);
|
||||
}
|
||||
|
||||
.diagnostic {
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 42px;
|
||||
color: var(--dsw-alias-label-primary);
|
||||
}
|
||||
|
||||
.title {
|
||||
max-width: 620px;
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
font-size: clamp(30px, 4vw, 42px);
|
||||
line-height: 1.15;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.035em;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.description,
|
||||
.diagnostic {
|
||||
max-width: 600px;
|
||||
margin: 22px 0 0;
|
||||
font-size: 17px;
|
||||
line-height: 29px;
|
||||
color: var(--dsw-alias-label-secondary);
|
||||
}
|
||||
|
||||
.primary {
|
||||
width: 100%;
|
||||
.provider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
max-width: 600px;
|
||||
margin-top: 36px;
|
||||
padding: 18px 20px;
|
||||
border: 1px solid var(--dsw-alias-border-l2);
|
||||
border-radius: 16px;
|
||||
background: var(--dsw-alias-bg-module-platform);
|
||||
}
|
||||
|
||||
.providerName {
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.providerRoute {
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
color: var(--dsw-alias-label-tertiary);
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.primary {
|
||||
min-width: 132px;
|
||||
}
|
||||
|
||||
.brand,
|
||||
.title,
|
||||
.description,
|
||||
.diagnostic,
|
||||
.provider,
|
||||
.actions {
|
||||
animation: credential-enter 280ms cubic-bezier(0.23, 1, 0.32, 1) both;
|
||||
}
|
||||
|
||||
.title { animation-delay: 40ms; }
|
||||
.description,
|
||||
.diagnostic { animation-delay: 80ms; }
|
||||
.provider { animation-delay: 120ms; }
|
||||
.actions { animation-delay: 160ms; }
|
||||
|
||||
@keyframes credential-enter {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(8px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.brand,
|
||||
.title,
|
||||
.description,
|
||||
.diagnostic,
|
||||
.provider,
|
||||
.actions {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 560px) {
|
||||
.page {
|
||||
width: calc(100vw - 40px);
|
||||
padding-top: 48px;
|
||||
}
|
||||
|
||||
.brand {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.description,
|
||||
.diagnostic {
|
||||
font-size: 16px;
|
||||
line-height: 27px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
align-items: stretch;
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
.primary,
|
||||
.later {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
/**
|
||||
* Official-DeepSeek first-run dialog. Readiness comes from the same
|
||||
* Official-DeepSeek first-run step. Readiness comes from the same
|
||||
* provider/settings/credential join as the Models page; the prompt only
|
||||
* routes the user to that page's single credential editor.
|
||||
*/
|
||||
|
||||
import { useEffect } from 'react'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import type { ReactNode } from 'react'
|
||||
import type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import { Button, Modal } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import { BrandWordmark, Button } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import type { SnapshotSelectorHook } from '@deepseek-ai/dsh-client-web-react'
|
||||
import type { DeepSeekReadiness, ModelsSettingsState, ModelsSettingsStore } from './store.ts'
|
||||
import { deepSeekReadiness } from './store.ts'
|
||||
@@ -61,12 +61,13 @@ function unavailableDiagnostic(
|
||||
* Prompt a first-run user to open Models while the official adapter exists
|
||||
* and its effective credential is not configured.
|
||||
* @param props - settings-shell owner state and Models feature dependencies.
|
||||
* @returns the controlled modal or null when onboarding needs no intervention.
|
||||
* @returns the onboarding page or null when onboarding needs no intervention.
|
||||
*/
|
||||
export function DeepSeekOnboardingDialog(props: DeepSeekOnboardingDialogProps): ReactNode {
|
||||
const { complete, openSection, controller, useSnapshot, t } = props
|
||||
const state = useSnapshot(snapshot => snapshot)
|
||||
const readiness = deepSeekReadiness(state)
|
||||
const titleRef = useRef<HTMLHeadingElement | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (state.status === 'idle') void controller.load()
|
||||
@@ -81,6 +82,12 @@ export function DeepSeekOnboardingDialog(props: DeepSeekOnboardingDialogProps):
|
||||
openSection('models')
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (readiness.kind === 'credential-missing' || readiness.kind === 'unavailable') {
|
||||
titleRef.current?.focus()
|
||||
}
|
||||
}, [readiness.kind])
|
||||
|
||||
let unavailableReason: UnavailableReason | undefined
|
||||
switch (readiness.kind) {
|
||||
case 'loading':
|
||||
@@ -102,26 +109,38 @@ export function DeepSeekOnboardingDialog(props: DeepSeekOnboardingDialogProps):
|
||||
? undefined
|
||||
: unavailableDiagnostic(unavailableReason, t)
|
||||
|
||||
const title = unavailable ? t('onboardingUnavailableTitle') : t('onboardingTitle')
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open
|
||||
onClose={complete}
|
||||
title={unavailable ? t('onboardingUnavailableTitle') : t('onboardingTitle')}
|
||||
closeLabel={t('onboardingLater')}
|
||||
{...(unavailable ? {} : { description: t('onboardingDescription') })}
|
||||
className={styles['dialog'] as string}
|
||||
footer={(
|
||||
<section className={styles['page']} role="region" aria-labelledby="deepseek-onboarding-title">
|
||||
<div className={styles['brand']} aria-hidden="true"><BrandWordmark size={24} /></div>
|
||||
<h2
|
||||
ref={titleRef}
|
||||
id="deepseek-onboarding-title"
|
||||
className={styles['title']}
|
||||
tabIndex={-1}
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
{unavailable
|
||||
? <p className={styles['diagnostic']}>{diagnostic}</p>
|
||||
: <p className={styles['description']}>{t('onboardingDescription')}</p>}
|
||||
<div className={styles['provider']}>
|
||||
<span className={styles['providerName']}>DeepSeek</span>
|
||||
<span className={styles['providerRoute']}>deepseek-official</span>
|
||||
</div>
|
||||
<div className={styles['actions']}>
|
||||
<Button variant="ghost" className={styles['later']} onClick={complete}>
|
||||
{t('onboardingLater')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
className={styles['primary']}
|
||||
autoFocus
|
||||
onClick={openModels}
|
||||
>
|
||||
{t('onboardingGoToSettings')}
|
||||
</Button>
|
||||
)}
|
||||
>
|
||||
{diagnostic === undefined ? undefined : <p className={styles['diagnostic']}>{diagnostic}</p>}
|
||||
</Modal>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user