mirror of
https://github.com/block/buzz.git
synced 2026-10-29 21:41:33 +00:00
Complete onboarding V3 workshop preview
Signed-off-by: kenny lopez <klopez4212@gmail.com>
This commit is contained in:
@@ -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")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
705
desktop/src/features/onboarding/ui/OnboardingPreviewJourney.tsx
Normal file
705
desktop/src/features/onboarding/ui/OnboardingPreviewJourney.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user