Complete onboarding V3 workshop preview

Signed-off-by: kenny lopez <klopez4212@gmail.com>
This commit is contained in:
kenny lopez
2026-08-27 07:32:08 +01:00
parent 55af8df0da
commit 597164d089
3 changed files with 934 additions and 51 deletions

View File

@@ -15,9 +15,21 @@ import {
ONBOARDING_LANDING_CTA_CLASS,
ONBOARDING_PRIMARY_CTA_CLASS,
ONBOARDING_SECONDARY_CTA_CLASS,
OnboardingChrome,
} from "./OnboardingChrome";
import { OnboardingFooter, OnboardingFooterProvider } from "./OnboardingFooter";
import {
BackupPasswordPreview,
CommunityChoicePreview,
CommunityConnectingPreview,
CommunityEntryPreview,
CommunityHomePreview,
CommunityProfilePreview,
type CommunityPreviewRoute,
DefaultConfigPreview,
StarterTeamPreview,
WelcomeChannelPreview,
} from "./OnboardingPreviewJourney";
import { OnboardingPreviewStep } from "./OnboardingPreviewShell";
import { OnboardingSlideTransition } from "./OnboardingSlideTransition";
import { SetupStepPreview } from "./SetupStepPreview";
@@ -28,7 +40,17 @@ type PreviewPage =
| "sign-in"
| "sign-in-key"
| "forgot-password"
| "setup";
| "backup-options"
| "backup-password"
| "setup"
| "config"
| "community-choice"
| "community-entry"
| "community-connecting"
| "community-profile"
| "starter-team"
| "welcome-channel"
| "community-home";
const EMAIL_SIGNUP_POLICY: JoinPolicy = {
ageAttestationRequired: true,
@@ -123,33 +145,6 @@ function Landing({ onNavigate }: { onNavigate: (page: PreviewPage) => void }) {
);
}
function PreviewStep({
children,
current = 2,
onBack,
testId,
}: {
children: React.ReactNode;
current?: number;
onBack: () => void;
testId: string;
}) {
return (
<div
className="buzz-onboarding-neutral-theme buzz-startup-shell flex max-h-dvh items-start justify-center overflow-x-hidden overflow-y-auto px-4 pb-28 pt-[106px] text-foreground"
data-testid={testId}
>
<StartupWindowDragRegion />
<OnboardingChrome current={current} />
<OnboardingFooterProvider backAction={{ onClick: onBack }}>
<div className="buzz-onboarding-step-frame relative flex w-full max-w-[1040px] flex-col items-center text-center">
{children}
</div>
</OnboardingFooterProvider>
</div>
);
}
type PreviewPasswordStrength = "empty" | "weak" | "moderate" | "strong";
const strengthLabelPhraseVariants = {
@@ -446,12 +441,18 @@ function EmailSignup({
const [confirmPassword, setConfirmPassword] = React.useState("");
const [ageConfirmed, setAgeConfirmed] = React.useState(false);
const [agreementConfirmed, setAgreementConfirmed] = React.useState(false);
// This workshop never creates an account, so it deliberately permits the
// next preview step without collecting real credentials or consent.
const canContinue = true;
const [openPolicyDocument, setOpenPolicyDocument] = React.useState<
"terms" | "privacy" | null
>(null);
const canContinue =
email.trim().includes("@") &&
password.length > 0 &&
confirmPassword === password &&
ageConfirmed &&
agreementConfirmed;
return (
<PreviewStep onBack={onBack} testId="onboarding-preview-email">
<OnboardingPreviewStep onBack={onBack} testId="onboarding-preview-email">
<OnboardingSlideTransition
className="flex min-h-0 w-full max-w-[500px] flex-col items-center"
transitionKey="preview-email"
@@ -487,7 +488,7 @@ function EmailSignup({
agreementConfirmed={agreementConfirmed}
onAgeConfirmedChange={setAgeConfirmed}
onAgreementConfirmedChange={setAgreementConfirmed}
onOpenDocument={() => undefined}
onOpenDocument={setOpenPolicyDocument}
policy={EMAIL_SIGNUP_POLICY}
relayWsUrl="wss://preview.invalid"
textTone="foreground"
@@ -504,8 +505,35 @@ function EmailSignup({
</Button>
</OnboardingFooter>
</form>
{openPolicyDocument ? (
<div
aria-modal="true"
className="fixed inset-0 z-[110] flex items-center justify-center bg-black/30 p-6 backdrop-blur-sm"
role="dialog"
>
<div className="w-full max-w-lg rounded-2xl border border-foreground/15 bg-background p-6 text-left shadow-xl">
<h2 className="text-xl font-medium text-foreground">
{openPolicyDocument === "terms"
? "Terms of Service"
: "Privacy Policy"}
</h2>
<p className="mt-3 text-sm leading-6 text-foreground/80">
{openPolicyDocument === "terms"
? "Review the terms that apply when you use Buzz."
: "Review how information is handled when you use Buzz."}
</p>
<Button
className="mt-5"
onClick={() => setOpenPolicyDocument(null)}
type="button"
>
Close
</Button>
</div>
</div>
) : null}
</OnboardingSlideTransition>
</PreviewStep>
</OnboardingPreviewStep>
);
}
@@ -517,9 +545,13 @@ function AgentHarnessStep({
onNext: () => void;
}) {
return (
<PreviewStep current={3} onBack={onBack} testId="onboarding-preview-setup">
<OnboardingPreviewStep
current={3}
onBack={onBack}
testId="onboarding-preview-setup"
>
<SetupStepPreview onBack={onBack} onNext={onNext} />
</PreviewStep>
</OnboardingPreviewStep>
);
}
@@ -542,7 +574,7 @@ function EmailSignIn({
const canContinue = email.trim().length > 0 && password.length > 0;
return (
<PreviewStep onBack={onBack} testId="onboarding-preview-sign-in">
<OnboardingPreviewStep onBack={onBack} testId="onboarding-preview-sign-in">
<OnboardingSlideTransition
className="flex min-h-0 w-full max-w-[500px] flex-col items-center"
transitionKey="preview-sign-in"
@@ -607,7 +639,7 @@ function EmailSignIn({
</div>
</form>
</OnboardingSlideTransition>
</PreviewStep>
</OnboardingPreviewStep>
);
}
@@ -623,7 +655,7 @@ function PasswordReset({
if (submitted) {
return (
<PreviewStep
<OnboardingPreviewStep
onBack={onBack}
testId="onboarding-preview-password-reset-sent"
>
@@ -638,12 +670,15 @@ function PasswordReset({
Use the link we sent to {email.trim()} to reset your password.
</p>
</OnboardingSlideTransition>
</PreviewStep>
</OnboardingPreviewStep>
);
}
return (
<PreviewStep onBack={onBack} testId="onboarding-preview-password-reset">
<OnboardingPreviewStep
onBack={onBack}
testId="onboarding-preview-password-reset"
>
<OnboardingSlideTransition
className="flex min-h-0 w-full max-w-[500px] flex-col items-center"
transitionKey="preview-password-reset"
@@ -689,7 +724,7 @@ function PasswordReset({
</OnboardingFooter>
</form>
</OnboardingSlideTransition>
</PreviewStep>
</OnboardingPreviewStep>
);
}
@@ -701,7 +736,10 @@ function KeySignIn({
onContinue: () => void;
}) {
return (
<PreviewStep onBack={onBack} testId="onboarding-preview-sign-in-key">
<OnboardingPreviewStep
onBack={onBack}
testId="onboarding-preview-sign-in-key"
>
<OnboardingSlideTransition
className="flex min-h-[calc(100dvh-13.25rem)] w-full max-w-[560px] flex-col items-center justify-center"
transitionKey="preview-sign-in-key"
@@ -723,7 +761,7 @@ function KeySignIn({
/>
</div>
</OnboardingSlideTransition>
</PreviewStep>
</OnboardingPreviewStep>
);
}
@@ -735,6 +773,11 @@ export function OnboardingPreviewApp() {
const [passwordResetEmail, setPasswordResetEmail] = React.useState("");
const [setupBackPage, setSetupBackPage] =
React.useState<PreviewPage>("landing");
const [communityRoute, setCommunityRoute] =
React.useState<CommunityPreviewRoute>("join");
const [communityName, setCommunityName] = React.useState("Block Community");
const [displayName, setDisplayName] = React.useState("");
const [hasAvatar, setHasAvatar] = React.useState(false);
React.useEffect(() => {
if (page === "landing") {
@@ -750,6 +793,10 @@ export function OnboardingPreviewApp() {
setRun((current) => current + 1);
setSignInEmail("");
setSignupPassword("");
setCommunityRoute("join");
setCommunityName("Block Community");
setDisplayName("");
setHasAvatar(false);
}, []);
let content: React.ReactNode;
@@ -769,7 +816,7 @@ export function OnboardingPreviewApp() {
);
} else if (page === "identity-key") {
content = (
<PreviewStep
<OnboardingPreviewStep
onBack={() => setPage("landing")}
testId="onboarding-preview-identity-key"
>
@@ -779,13 +826,37 @@ export function OnboardingPreviewApp() {
setSetupBackPage("identity-key");
setPage("setup");
}}
onOpenPasswordBackup={() => undefined}
onShowOptions={() => undefined}
onOpenPasswordBackup={() => setPage("backup-password")}
onShowOptions={() => setPage("backup-options")}
optionsExpanded={false}
previewMode
returningFromSecurity={false}
/>
</PreviewStep>
</OnboardingPreviewStep>
);
} else if (page === "backup-options") {
content = (
<OnboardingPreviewStep
onBack={() => setPage("identity-key")}
testId="onboarding-preview-backup-options"
>
<BackupStep
direction="forward"
onNext={() => setPage("setup")}
onOpenPasswordBackup={() => setPage("backup-password")}
onShowOptions={() => setPage("backup-options")}
optionsExpanded
previewMode
returningFromSecurity={false}
/>
</OnboardingPreviewStep>
);
} else if (page === "backup-password") {
content = (
<BackupPasswordPreview
onBack={() => setPage("backup-options")}
onDone={() => setPage("backup-options")}
/>
);
} else if (page === "sign-in") {
content = (
@@ -821,11 +892,80 @@ export function OnboardingPreviewApp() {
}}
/>
);
} else {
} else if (page === "setup") {
content = (
<AgentHarnessStep
onBack={() => setPage(setupBackPage)}
onNext={() => undefined}
onNext={() => setPage("config")}
/>
);
} else if (page === "config") {
content = (
<DefaultConfigPreview
onBack={() => setPage("setup")}
onNext={() => setPage("community-choice")}
/>
);
} else if (page === "community-choice") {
content = (
<CommunityChoicePreview
onBack={() => setPage("config")}
onChoose={(route) => {
setCommunityRoute(route);
setPage("community-entry");
}}
/>
);
} else if (page === "community-entry") {
content = (
<CommunityEntryPreview
onBack={() => setPage("community-choice")}
onContinue={(name) => {
setCommunityName(name);
setPage("community-connecting");
}}
route={communityRoute}
/>
);
} else if (page === "community-connecting") {
content = (
<CommunityConnectingPreview
communityName={communityName}
onBack={() => setPage("community-entry")}
onContinue={() => setPage("community-profile")}
/>
);
} else if (page === "community-profile") {
content = (
<CommunityProfilePreview
displayName={displayName}
hasAvatar={hasAvatar}
onAvatarChange={() => setHasAvatar((current) => !current)}
onBack={() => setPage("community-entry")}
onDisplayNameChange={setDisplayName}
onNext={() => setPage("starter-team")}
/>
);
} else if (page === "starter-team") {
content = (
<StarterTeamPreview
onBack={() => setPage("community-profile")}
onNext={() => setPage("welcome-channel")}
/>
);
} else if (page === "welcome-channel") {
content = (
<WelcomeChannelPreview
communityName={communityName}
onBack={() => setPage("starter-team")}
onNext={() => setPage("community-home")}
/>
);
} else {
content = (
<CommunityHomePreview
communityName={communityName}
onBack={() => setPage("welcome-channel")}
/>
);
}

View File

@@ -0,0 +1,705 @@
import { Check, Plus, Users } from "lucide-react";
import * as React from "react";
import { Button } from "@/shared/ui/button";
import { Card } from "@/shared/ui/card";
import { Input } from "@/shared/ui/input";
import {
ONBOARDING_PRIMARY_CTA_CLASS,
ONBOARDING_SECONDARY_CTA_CLASS,
} from "./OnboardingChrome";
import { OnboardingFooter } from "./OnboardingFooter";
import { OnboardingPreviewStep } from "./OnboardingPreviewShell";
import { OnboardingSlideTransition } from "./OnboardingSlideTransition";
const FIELD_CLASS =
"h-12 rounded-2xl border-foreground/15 bg-white px-4 text-sm shadow-none";
const COMMUNITY_OPTION_CLASS =
"w-full max-w-[320px] items-center px-6 py-4 text-center text-sm font-normal leading-6 text-foreground [--buzz-card-textured-min-height:88px] transition-[filter] duration-150 ease-out hover:brightness-[0.98] focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-foreground/35";
function LabeledField({
id,
label,
onChange,
placeholder,
type = "text",
value,
}: {
id: string;
label: string;
onChange: (value: string) => void;
placeholder: string;
type?: "password" | "text" | "url";
value: string;
}) {
return (
<label className="block w-full text-left" htmlFor={id}>
<span className="mb-2 block pl-3 text-sm font-medium text-foreground">
{label}
</span>
<Input
autoComplete="off"
className={FIELD_CLASS}
id={id}
onChange={(event) => onChange(event.target.value)}
placeholder={placeholder}
type={type}
value={value}
/>
</label>
);
}
type BackupPhase = "password" | "saved" | "verify" | "verified";
export function BackupPasswordPreview({
onBack,
onDone,
}: {
onBack: () => void;
onDone: () => void;
}) {
const [phase, setPhase] = React.useState<BackupPhase>("password");
const [password, setPassword] = React.useState("");
const [verificationPassword, setVerificationPassword] = React.useState("");
const heading =
phase === "verified"
? "Your backup is verified"
: phase === "verify"
? "That’s your backup file"
: phase === "saved"
? "Optionally, test your backup"
: "Backup your key with a password";
const description =
phase === "verified"
? "Your file and password can restore your identity."
: phase === "verify"
? "Now enter your password to prove you can unlock it."
: phase === "saved"
? "Learn how your backup works. Choose the file you just saved and unlock it with your password."
: "Keep the downloaded file private — you need both it and your password to restore your identity. Save the backup password somewhere safe; Buzz cannot reset it if lost.";
return (
<OnboardingPreviewStep
onBack={phase === "password" ? onBack : () => setPhase("password")}
security
testId="onboarding-preview-backup-password"
>
<OnboardingSlideTransition
className="flex min-h-0 w-full flex-col items-center"
transitionKey={`preview-backup-${phase}`}
>
<div className="w-full max-w-[500px] shrink-0 text-center">
<h1 className="text-title font-normal text-foreground">{heading}</h1>
<p className="mt-5 text-sm leading-6 text-foreground/80">
{description}
</p>
</div>
<div className="flex w-full max-w-[560px] flex-1 items-center justify-center py-10">
{phase === "password" ? (
<div className="w-full space-y-5">
<LabeledField
id="preview-backup-password"
label="Backup password"
onChange={setPassword}
placeholder="Create a backup password"
type="password"
value={password}
/>
<Card className="px-5 py-4 text-left" variant="textured">
<p className="text-sm font-medium">Buzz identity backup</p>
<p className="mt-1 text-xs leading-5 text-foreground/65">
Protected with your backup password and ready to save.
</p>
</Card>
</div>
) : phase === "saved" ? (
<Card
className="flex w-full items-center gap-4 px-6 py-5 text-left"
variant="textured"
>
<span className="flex h-10 w-10 items-center justify-center rounded-full bg-green-600 text-white">
<Check className="h-5 w-5" aria-hidden />
</span>
<span>
<span className="block text-sm font-medium">
Buzz identity backup saved
</span>
<span className="mt-1 block text-xs text-foreground/65">
Keep the file and password somewhere safe.
</span>
</span>
</Card>
) : phase === "verify" ? (
<div className="w-full space-y-5">
<Card className="px-5 py-4 text-left" variant="textured">
<p className="text-sm font-medium">Buzz identity backup</p>
<p className="mt-1 text-xs text-foreground/65">
Selected and ready to unlock.
</p>
</Card>
<LabeledField
id="preview-backup-verification-password"
label="Backup password"
onChange={setVerificationPassword}
placeholder="Enter your backup password"
type="password"
value={verificationPassword}
/>
</div>
) : (
<span className="flex h-20 w-20 items-center justify-center rounded-full bg-green-600 text-white shadow-sm">
<Check className="h-10 w-10" aria-hidden />
</span>
)}
</div>
<OnboardingFooter>
{phase === "password" ? (
<Button
className={ONBOARDING_PRIMARY_CTA_CLASS}
data-testid="preview-create-locked-backup"
disabled={!password}
onClick={() => setPhase("saved")}
>
Create locked backup
</Button>
) : phase === "saved" ? (
<>
<Button
className={ONBOARDING_PRIMARY_CTA_CLASS}
data-testid="preview-test-backup"
onClick={() => setPhase("verify")}
>
Test backup
</Button>
<Button
className={ONBOARDING_SECONDARY_CTA_CLASS}
onClick={onDone}
variant="ghost"
>
Skip for now
</Button>
</>
) : phase === "verify" ? (
<Button
className={ONBOARDING_PRIMARY_CTA_CLASS}
data-testid="preview-verify-backup"
disabled={!verificationPassword}
onClick={() => setPhase("verified")}
>
Verify backup
</Button>
) : (
<Button
className={ONBOARDING_PRIMARY_CTA_CLASS}
data-testid="preview-finish-backup"
onClick={onDone}
>
Finish
</Button>
)}
</OnboardingFooter>
</OnboardingSlideTransition>
</OnboardingPreviewStep>
);
}
export function DefaultConfigPreview({
onBack,
onNext,
}: {
onBack: () => void;
onNext: () => void;
}) {
const [harness, setHarness] = React.useState("buzz-agent");
const [provider, setProvider] = React.useState("");
const [model, setModel] = React.useState("");
return (
<OnboardingPreviewStep
current={4}
onBack={onBack}
testId="onboarding-preview-config"
>
<OnboardingSlideTransition
className="flex min-h-full w-full flex-col items-center"
transitionKey="preview-default-config"
>
<div className="w-full max-w-[500px] text-center">
<h1 className="text-title font-normal text-foreground">
Configure your default model settings
</h1>
<p className="mx-auto mt-3 max-w-[440px] text-sm leading-5 text-foreground/80">
This will be set as your default model configuration across Buzz.
You can always change this in your Settings or give specific agents
a different configuration.
</p>
</div>
<div className="flex w-full flex-1 items-center justify-center py-10">
<div className="w-full max-w-[328px] space-y-5 text-left">
<label
className="block text-sm font-medium"
htmlFor="preview-default-harness"
>
<span className="mb-2 block pl-3">Default harness</span>
<select
className={`${FIELD_CLASS} w-full`}
id="preview-default-harness"
onChange={(event) => setHarness(event.target.value)}
value={harness}
>
<option value="buzz-agent">Buzz Agent</option>
<option value="claude">Claude Code</option>
<option value="codex">Codex</option>
<option value="goose">Goose</option>
</select>
</label>
<label
className="block text-sm font-medium"
htmlFor="preview-default-provider"
>
<span className="mb-2 block pl-3">Provider</span>
<select
className={`${FIELD_CLASS} w-full`}
id="preview-default-provider"
onChange={(event) => setProvider(event.target.value)}
value={provider}
>
<option value="">Select a provider</option>
<option value="anthropic">Anthropic</option>
<option value="openai">OpenAI</option>
<option value="ollama">Ollama</option>
</select>
</label>
<label
className="block text-sm font-medium"
htmlFor="preview-default-model"
>
<span className="mb-2 block pl-3">Model</span>
<select
className={`${FIELD_CLASS} w-full`}
id="preview-default-model"
onChange={(event) => setModel(event.target.value)}
value={model}
>
<option value="">Select a model</option>
<option value="default">Provider default</option>
</select>
</label>
</div>
</div>
<OnboardingFooter>
<Button
className={ONBOARDING_PRIMARY_CTA_CLASS}
data-testid="onboarding-preview-config-next"
onClick={onNext}
>
Next
</Button>
<Button
className="h-9 whitespace-nowrap rounded-full px-6 text-sm hover:bg-foreground/10"
data-testid="onboarding-preview-config-skip"
onClick={onNext}
variant="ghost"
>
Skip for now
</Button>
</OnboardingFooter>
</OnboardingSlideTransition>
</OnboardingPreviewStep>
);
}
export type CommunityPreviewRoute = "join" | "create" | "existing";
export function CommunityChoicePreview({
onBack,
onChoose,
}: {
onBack: () => void;
onChoose: (route: CommunityPreviewRoute) => void;
}) {
return (
<OnboardingPreviewStep
current={5}
onBack={onBack}
testId="onboarding-preview-community-choice"
>
<OnboardingSlideTransition
className="flex min-h-full w-full flex-col items-center"
transitionKey="preview-community-choice"
>
<div className="w-full max-w-[760px]">
<h1 className="text-title font-normal">Join or create a community</h1>
<p className="mt-3 text-sm leading-6 text-foreground/80">
Join with an invite, create your own community, or reconnect one you
already have.
</p>
</div>
<div className="flex w-full flex-1 flex-col items-center justify-center gap-8 py-10">
{(
[
["join", "Join a community"],
["create", "Create a community"],
["existing", "I already have a community"],
] as const
).map(([route, label]) => (
<Card
asChild
className={COMMUNITY_OPTION_CLASS}
key={route}
variant="textured"
>
<button
data-testid={`onboarding-preview-community-${route}`}
onClick={() => onChoose(route)}
type="button"
>
{label}
</button>
</Card>
))}
</div>
</OnboardingSlideTransition>
</OnboardingPreviewStep>
);
}
export function CommunityEntryPreview({
onBack,
onContinue,
route,
}: {
onBack: () => void;
onContinue: (communityName: string) => void;
route: CommunityPreviewRoute;
}) {
const [value, setValue] = React.useState("");
const isCreate = route === "create";
const heading = isCreate
? "Create a community"
: route === "existing"
? "Reconnect to your community"
: "Join a community";
return (
<OnboardingPreviewStep
current={5}
onBack={onBack}
testId="onboarding-preview-community-entry"
>
<OnboardingSlideTransition
className="flex min-h-full w-full max-w-[620px] flex-col items-center"
transitionKey={`preview-community-${route}`}
>
<h1 className="text-title font-normal">{heading}</h1>
<p className="mt-3 max-w-[500px] text-sm leading-6 text-foreground/80">
{isCreate
? "Choose a name for the space where your people and agents will work together."
: route === "existing"
? "Enter the community URL or an invite link. Your role will be restored when you connect."
: "Enter the invite link or community URL you received."}
</p>
<form
className="flex w-full flex-1 flex-col items-center justify-center gap-6 py-10"
onSubmit={(event) => {
event.preventDefault();
onContinue(
isCreate ? value.trim() || "Your community" : "Block Community",
);
}}
>
<LabeledField
id="onboarding-preview-community-value"
label={isCreate ? "Community name" : "Invite or community URL"}
onChange={setValue}
placeholder={
isCreate
? "Enter a community name"
: "Paste an invite, code, or URL"
}
type={isCreate ? "text" : "url"}
value={value}
/>
<Button
className={ONBOARDING_PRIMARY_CTA_CLASS}
data-testid="onboarding-preview-community-continue"
type="submit"
>
{isCreate ? "Create community" : "Continue"}
</Button>
</form>
</OnboardingSlideTransition>
</OnboardingPreviewStep>
);
}
export function CommunityConnectingPreview({
communityName,
onBack,
onContinue,
}: {
communityName: string;
onBack: () => void;
onContinue: () => void;
}) {
return (
<OnboardingPreviewStep
current={5}
onBack={onBack}
testId="onboarding-preview-community-connecting"
>
<OnboardingSlideTransition
className="flex min-h-full w-full max-w-[560px] flex-col items-center justify-center"
transitionKey="preview-community-connecting"
>
<Users className="h-10 w-10" aria-hidden />
<h1 className="mt-5 text-title font-normal">Joining {communityName}</h1>
<p className="mt-3 text-sm text-foreground/80">Connecting securely…</p>
<OnboardingFooter>
<Button
className={ONBOARDING_PRIMARY_CTA_CLASS}
data-testid="onboarding-preview-connecting-continue"
onClick={onContinue}
>
Continue
</Button>
</OnboardingFooter>
</OnboardingSlideTransition>
</OnboardingPreviewStep>
);
}
export function CommunityProfilePreview({
displayName,
hasAvatar,
onAvatarChange,
onBack,
onDisplayNameChange,
onNext,
}: {
displayName: string;
hasAvatar: boolean;
onAvatarChange: () => void;
onBack: () => void;
onDisplayNameChange: (value: string) => void;
onNext: () => void;
}) {
return (
<OnboardingPreviewStep
current={6}
onBack={onBack}
testId="onboarding-preview-community-profile"
>
<OnboardingSlideTransition
className="flex min-h-full w-full max-w-[500px] flex-col items-center"
transitionKey="preview-community-profile"
>
<div className="shrink-0">
<h1 className="text-title font-normal">Build your profile</h1>
<p className="mx-auto mt-3 max-w-[380px] text-sm leading-6 text-foreground/80">
Add a name and avatar. They’ll show up on your messages, reactions,
and agent handoffs.
</p>
</div>
<div className="flex w-full flex-1 flex-col items-center justify-center pt-8">
<button
aria-label={hasAvatar ? "Change your avatar" : "Add an avatar"}
className="flex h-36 w-36 items-center justify-center overflow-hidden rounded-full bg-white/30 text-[var(--buzz-onboarding-backup-ink)] shadow-xs transition-colors hover:bg-white/40"
data-testid="onboarding-preview-profile-avatar"
onClick={onAvatarChange}
type="button"
>
{hasAvatar ? (
<span className="text-5xl" role="img" aria-label="Bee avatar">
🐝
</span>
) : (
<Plus className="h-7 w-7" aria-hidden />
)}
</button>
<label
className="mt-7 block w-full max-w-[412px] text-left"
htmlFor="onboarding-preview-community-name"
>
<span className="mb-2 block pl-4 text-sm text-foreground">
Your username
</span>
<Input
autoComplete="username"
className="h-14 rounded-2xl border-foreground/25 bg-white/95 px-5 text-sm shadow-none"
id="onboarding-preview-community-name"
onChange={(event) => onDisplayNameChange(event.target.value)}
placeholder="Enter your username here"
value={displayName}
/>
</label>
</div>
<OnboardingFooter>
<Button
className={ONBOARDING_PRIMARY_CTA_CLASS}
data-testid="onboarding-preview-profile-next"
disabled={!displayName.trim()}
onClick={onNext}
>
Next
</Button>
</OnboardingFooter>
</OnboardingSlideTransition>
</OnboardingPreviewStep>
);
}
const STARTER_TEAM = ["Fizz", "Honey", "Pollen"] as const;
export function StarterTeamPreview({
onBack,
onNext,
}: {
onBack: () => void;
onNext: () => void;
}) {
return (
<OnboardingPreviewStep
current={7}
onBack={onBack}
testId="onboarding-preview-starter-team"
>
<OnboardingSlideTransition
className="flex min-h-full w-full max-w-[760px] flex-col items-center"
transitionKey="preview-starter-team"
>
<h1 className="text-title font-normal">Meet your starter team</h1>
<p className="mx-auto mt-3 max-w-[400px] text-sm leading-6 text-foreground/80">
Buzz lets you bring multiple agents into the same workspace. Your team
will help you get started using Buzz.
</p>
<div className="flex w-full flex-1 items-center justify-center py-10">
<div className="flex flex-wrap justify-center gap-8">
{STARTER_TEAM.map((name) => (
<div className="flex w-40 flex-col items-center gap-3" key={name}>
<img
alt={`${name} animated character`}
className="h-40 w-40 object-contain"
src={`/onboarding/starter-team/${name.toLowerCase()}.png`}
/>
<span className="font-mono text-xs font-medium uppercase tracking-[0.15em]">
{name}
</span>
</div>
))}
</div>
</div>
<OnboardingFooter>
<Button
className={ONBOARDING_PRIMARY_CTA_CLASS}
data-testid="onboarding-preview-team-next"
onClick={onNext}
>
Take me to Buzz
</Button>
</OnboardingFooter>
</OnboardingSlideTransition>
</OnboardingPreviewStep>
);
}
export function WelcomeChannelPreview({
communityName,
onBack,
onNext,
}: {
communityName: string;
onBack: () => void;
onNext: () => void;
}) {
return (
<OnboardingPreviewStep
current={7}
onBack={onBack}
testId="onboarding-preview-welcome-channel"
>
<OnboardingSlideTransition
className="flex min-h-full w-full max-w-[680px] flex-col items-center justify-center"
transitionKey="preview-welcome-channel"
>
<h1 className="text-title font-normal">
Your Welcome channel is ready
</h1>
<p className="mt-4 max-w-[480px] text-sm leading-6 text-foreground/80">
This is your starting place in {communityName}. Your starter team can
help you learn the space and decide what to do next.
</p>
<Card
className="mt-10 w-full max-w-[520px] px-6 py-6 text-left"
variant="textured"
>
<p className="text-lg font-medium"># Welcome</p>
<p className="mt-2 text-sm leading-6 text-foreground/70">
Meet your team, start a conversation, or explore your community.
</p>
<div className="mt-5 flex -space-x-2">
{STARTER_TEAM.map((name) => (
<span
aria-label={name}
className="flex h-9 w-9 items-center justify-center rounded-full border-2 border-background bg-primary/15 text-xs font-semibold"
key={name}
role="img"
>
{name[0]}
</span>
))}
</div>
</Card>
<OnboardingFooter>
<Button
className={ONBOARDING_PRIMARY_CTA_CLASS}
data-testid="onboarding-preview-welcome-open"
onClick={onNext}
>
Open Welcome
</Button>
</OnboardingFooter>
</OnboardingSlideTransition>
</OnboardingPreviewStep>
);
}
export function CommunityHomePreview({
communityName,
onBack,
}: {
communityName: string;
onBack: () => void;
}) {
return (
<OnboardingPreviewStep
onBack={onBack}
current={7}
testId="onboarding-preview-community-home"
>
<OnboardingSlideTransition
className="flex min-h-full w-full max-w-[760px] flex-col items-center justify-center"
transitionKey="preview-community-home"
>
<p className="text-sm font-medium uppercase tracking-[0.16em] text-foreground/60">
{communityName}
</p>
<h1 className="mt-3 text-title font-normal">Welcome to Buzz</h1>
<p className="mt-4 max-w-[480px] text-sm leading-6 text-foreground/80">
You’re in the Welcome channel. Start with a message, meet the people
and agents here, or explore the rest of the community.
</p>
<Card
className="mt-10 w-full max-w-[620px] px-6 py-6 text-left"
variant="textured"
>
<p className="text-sm font-medium">Fizz</p>
<p className="mt-2 text-sm leading-6 text-foreground/75">
Welcome! I can show you around and help you get started.
</p>
</Card>
</OnboardingSlideTransition>
</OnboardingPreviewStep>
);
}

View File

@@ -0,0 +1,38 @@
import type * as React from "react";
import { StartupWindowDragRegion } from "@/shared/ui/StartupWindowDragRegion";
import { OnboardingChrome } from "./OnboardingChrome";
import { OnboardingFooterProvider } from "./OnboardingFooter";
export function OnboardingPreviewStep({
children,
current = 2,
onBack,
security = false,
testId,
}: {
children: React.ReactNode;
current?: number;
onBack?: () => void;
security?: boolean;
testId: string;
}) {
return (
<div
className={`buzz-onboarding-neutral-theme buzz-startup-shell flex max-h-dvh items-start justify-center overflow-x-hidden overflow-y-auto px-4 pb-28 pt-[106px] text-foreground ${
security ? "buzz-onboarding-security-theme" : ""
}`}
data-testid={testId}
>
<StartupWindowDragRegion />
{!security ? <OnboardingChrome current={current} /> : null}
<OnboardingFooterProvider
backAction={onBack ? { onClick: onBack } : undefined}
>
<div className="buzz-onboarding-step-frame relative flex w-full max-w-[1040px] flex-col items-center text-center">
{children}
</div>
</OnboardingFooterProvider>
</div>
);
}