Skip to content

Commit 6e3f5da

Browse files
committed
feat(web): pixel-perfect Bud-System onboarding (15-series) + center 15b
Reimplements the first-run onboarding to match the Paper 'Bud System' 15b-15f artboards exactly: card-less, viewport-centered, Geist 14px, #1B1B1B ink, 32px dark pills with a return-arrow, 14px-radius cards, orange #FBA62A sparkle. - New onboarding-icons.tsx: SVG paths lifted verbatim from the artboards. - onboarding-steps.tsx: intro (agent self-intro + 5 feature rows), name (1/4), tools (2/4, GitHub/Notion/Slack cards), basics (3/4, numbered example cards), plan (4/4, Pro/Premium/Ultra/Max + BYOK + dashboard link). - onboarding-flow.tsx: drop the card wrapper (page centers), sparkle-based status/retry cards, guard checkout for unconfigured tiers. - onboarding/page.tsx: pulsing-sparkle boot fallback.
1 parent 38859e9 commit 6e3f5da

4 files changed

Lines changed: 503 additions & 207 deletions

File tree

apps/web/src/app/onboarding/page.tsx

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
11
import { Suspense } from "react";
22
import { AuthRequiredGate } from "@/components/auth/auth-required-gate";
33
import { OnboardingFlow } from "@/components/onboarding/onboarding-flow";
4+
import { Sparkle } from "@/components/onboarding/onboarding-icons";
5+
6+
function BootSparkle() {
7+
return (
8+
<div className="animate-pulse">
9+
<Sparkle />
10+
</div>
11+
);
12+
}
413

514
export default function OnboardingPage() {
615
return (
716
<main className="flex min-h-screen items-center justify-center bg-white px-6 py-12 text-[#1b1b1b]">
8-
<Suspense
9-
fallback={
10-
<div className="h-96 w-full max-w-[392px] rounded-[28px] border border-[#f1f1f1] bg-white" />
11-
}
12-
>
13-
<AuthRequiredGate
14-
fallback={
15-
<div className="h-96 w-full max-w-[392px] rounded-[28px] border border-[#f1f1f1] bg-white shadow-[0_18px_70px_rgba(0,0,0,0.08)]" />
16-
}
17-
>
17+
<Suspense fallback={<BootSparkle />}>
18+
<AuthRequiredGate fallback={<BootSparkle />}>
1819
<OnboardingFlow />
1920
</AuthRequiredGate>
2021
</Suspense>

apps/web/src/components/onboarding/onboarding-flow.tsx

Lines changed: 49 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { useMutation } from "@tanstack/react-query";
1515
import { useRouter, useSearchParams } from "next/navigation";
1616
import { type ReactNode, useCallback, useEffect, useRef, useState } from "react";
1717
import { toast } from "sonner";
18+
import { Sparkle } from "@/components/onboarding/onboarding-icons";
1819
import {
1920
BasicsStep,
2021
IntroStep,
@@ -177,7 +178,13 @@ export function OnboardingFlow() {
177178
recordStep("basics", "skipped");
178179
advance();
179180
},
180-
onCheckout: (tier) => checkoutMutation.mutate(tier),
181+
onCheckout: (tier) => {
182+
if (availableTiers.has(tier)) {
183+
checkoutMutation.mutate(tier);
184+
} else {
185+
toast.info("That plan isn't available yet - bring your own keys to start building now.");
186+
}
187+
},
181188
onIntro: () => {
182189
recordStep("intro", "done");
183190
advance();
@@ -202,11 +209,7 @@ export function OnboardingFlow() {
202209
},
203210
};
204211

205-
return (
206-
<div className="w-full max-w-[392px] rounded-[28px] border border-[#f1f1f1] bg-[#f8f8f8] p-1 shadow-[0_18px_70px_rgba(0,0,0,0.08)]">
207-
<div className="rounded-[24px] bg-white p-8">{renderStep(stepName, stepProps)}</div>
208-
</div>
209-
);
212+
return renderStep(stepName, stepProps);
210213
}
211214

212215
function renderStep(stepName: OnboardingStep, props: StepProps): ReactNode {
@@ -260,51 +263,64 @@ function useCheckoutMutation(getToken: () => Promise<null | string>) {
260263

261264
function StatusCard({ label }: { label: string }) {
262265
return (
263-
<div className="flex w-full max-w-[392px] flex-col items-center gap-4 rounded-[28px] border border-[#f1f1f1] bg-white p-12 text-center">
264-
<Loader2 aria-hidden="true" className="h-6 w-6 animate-spin text-[#a0a0a0]" />
265-
<p className="text-[#707070] text-sm">{label}</p>
266+
<div className="flex flex-col items-center gap-4 text-center">
267+
<Sparkle />
268+
<p className="flex items-center gap-2 font-medium text-[#585858] text-[14px] leading-[18px]">
269+
<Loader2 aria-hidden="true" className="h-3.5 w-3.5 animate-spin" />
270+
{label}
271+
</p>
266272
</div>
267273
);
268274
}
269275

270-
function RetryCard({ isPending, onRetry }: { isPending: boolean; onRetry: () => void }) {
276+
function StatusShell({
277+
detail,
278+
isPending,
279+
onRetry,
280+
title,
281+
}: {
282+
detail: string;
283+
isPending: boolean;
284+
onRetry: () => void;
285+
title: string;
286+
}) {
271287
return (
272-
<div className="flex w-full max-w-[392px] flex-col items-center gap-5 rounded-[28px] border border-[#f1f1f1] bg-white p-12 text-center">
273-
<p className="text-[#1b1b1b] text-sm">Finishing setup...</p>
274-
<p className="max-w-sm text-[#707070] text-xs leading-relaxed">
275-
Your progress is saved. We could not refresh your session - retry to finish, or this
276-
resolves on the next sign-in.
277-
</p>
288+
<div className="flex max-w-[360px] flex-col items-center gap-4 text-center">
289+
<Sparkle />
290+
<p className="font-medium text-[#1B1B1B] text-[14px] leading-[18px]">{title}</p>
291+
<p className="font-medium text-[#585858] text-[13px] leading-[18px]">{detail}</p>
278292
<button
279-
className="inline-flex h-11 items-center justify-center rounded-full bg-[#1b1b1b] px-6 font-medium text-white transition-colors hover:bg-black disabled:cursor-not-allowed disabled:opacity-60"
293+
className="mt-1 flex h-8 items-center gap-2 rounded-full bg-[#1B1B1B] px-3.5 font-medium text-[14px] text-white leading-[18px] transition-colors hover:bg-black disabled:cursor-not-allowed disabled:opacity-60"
280294
disabled={isPending}
281295
onClick={onRetry}
282296
type="button"
283297
>
284-
{isPending ? <Loader2 aria-hidden="true" className="mr-2 h-4 w-4 animate-spin" /> : null}
298+
{isPending ? <Loader2 aria-hidden="true" className="h-3.5 w-3.5 animate-spin" /> : null}
285299
Retry
286300
</button>
287301
</div>
288302
);
289303
}
290304

305+
function RetryCard({ isPending, onRetry }: { isPending: boolean; onRetry: () => void }) {
306+
return (
307+
<StatusShell
308+
detail="Your progress is saved. We could not refresh your session - retry to finish, or this resolves on the next sign-in."
309+
isPending={isPending}
310+
onRetry={onRetry}
311+
title="Finishing setup..."
312+
/>
313+
);
314+
}
315+
291316
function LoadErrorCard({ isPending, onRetry }: { isPending: boolean; onRetry: () => void }) {
292317
return (
293-
<div className="flex w-full max-w-[392px] flex-col items-center gap-5 rounded-[28px] border border-[#f1f1f1] bg-white p-12 text-center">
294-
<p className="text-[#1b1b1b] text-sm">Setup could not load</p>
295-
<p className="max-w-sm text-[#707070] text-xs leading-relaxed">
296-
We could not reach your profile. Check the local server and try again.
297-
</p>
298-
<button
299-
className="inline-flex h-11 items-center justify-center rounded-full bg-[#1b1b1b] px-6 font-medium text-white transition-colors hover:bg-black disabled:cursor-not-allowed disabled:opacity-60"
300-
disabled={isPending}
301-
onClick={onRetry}
302-
type="button"
303-
>
304-
{isPending ? <Loader2 aria-hidden="true" className="mr-2 h-4 w-4 animate-spin" /> : null}
305-
Retry
306-
</button>
307-
</div>
318+
<StatusShell
319+
detail="We could not reach your profile. Check the connection and try again."
320+
isPending={isPending}
321+
onRetry={onRetry}
322+
title="Setup could not load"
323+
/>
308324
);
309325
}
310326

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
// Onboarding iconography — SVG paths lifted verbatim from the Paper "Bud System"
2+
// 15-series artboards (15b–15f) so the first-run flow is pixel-identical to the design.
3+
4+
export function Sparkle({ size = 48 }: { size?: number }) {
5+
return (
6+
<svg
7+
aria-hidden="true"
8+
height={size}
9+
viewBox="0 0 24 24"
10+
width={size}
11+
xmlns="http://www.w3.org/2000/svg"
12+
>
13+
<path
14+
d="M12 2C12.6 7.2 16.8 11.4 22 12C16.8 12.6 12.6 16.8 12 22C11.4 16.8 7.2 12.6 2 12C7.2 11.4 11.4 7.2 12 2Z"
15+
fill="#FBA62A"
16+
/>
17+
</svg>
18+
);
19+
}
20+
21+
export function IconComputer() {
22+
return (
23+
<svg
24+
aria-hidden="true"
25+
height="15"
26+
viewBox="0 0 16 16"
27+
width="15"
28+
xmlns="http://www.w3.org/2000/svg"
29+
>
30+
<rect
31+
fill="none"
32+
height="9"
33+
rx="1.5"
34+
stroke="#1B1B1B"
35+
strokeWidth="1.3"
36+
width="12"
37+
x="2"
38+
y="3"
39+
/>
40+
<path
41+
d="M4.5 6L6.5 7.5L4.5 9M7.5 9.5H10"
42+
fill="none"
43+
stroke="#1B1B1B"
44+
strokeLinecap="round"
45+
strokeLinejoin="round"
46+
strokeWidth="1.2"
47+
/>
48+
</svg>
49+
);
50+
}
51+
52+
export function IconBrowser() {
53+
return (
54+
<svg
55+
aria-hidden="true"
56+
height="15"
57+
viewBox="0 0 16 16"
58+
width="15"
59+
xmlns="http://www.w3.org/2000/svg"
60+
>
61+
<circle cx="8" cy="8" fill="none" r="6" stroke="#1B1B1B" strokeWidth="1.3" />
62+
<path
63+
d="M2 8H14M8 2C9.5 3.8 10.3 5.8 10.3 8C10.3 10.2 9.5 12.2 8 14C6.5 12.2 5.7 10.2 5.7 8C5.7 5.8 6.5 3.8 8 2Z"
64+
fill="none"
65+
stroke="#1B1B1B"
66+
strokeWidth="1.1"
67+
/>
68+
</svg>
69+
);
70+
}
71+
72+
export function IconSkills() {
73+
return (
74+
<svg
75+
aria-hidden="true"
76+
height="15"
77+
viewBox="0 0 24 24"
78+
width="15"
79+
xmlns="http://www.w3.org/2000/svg"
80+
>
81+
<path
82+
d="M12 2C12.6 7.2 16.8 11.4 22 12C16.8 12.6 12.6 16.8 12 22C11.4 16.8 7.2 12.6 2 12C7.2 11.4 11.4 7.2 12 2Z"
83+
fill="none"
84+
stroke="#1B1B1B"
85+
strokeWidth="1.6"
86+
/>
87+
</svg>
88+
);
89+
}
90+
91+
export function IconKeys() {
92+
return (
93+
<svg
94+
aria-hidden="true"
95+
height="15"
96+
viewBox="0 0 16 16"
97+
width="15"
98+
xmlns="http://www.w3.org/2000/svg"
99+
>
100+
<circle cx="5.5" cy="10.5" fill="none" r="3" stroke="#1B1B1B" strokeWidth="1.3" />
101+
<path
102+
d="M7.7 8.3L13.5 2.5M11 5L13 7M9.5 6.5L11 8"
103+
fill="none"
104+
stroke="#1B1B1B"
105+
strokeLinecap="round"
106+
strokeWidth="1.3"
107+
/>
108+
</svg>
109+
);
110+
}
111+
112+
export function IconPhone() {
113+
return (
114+
<svg
115+
aria-hidden="true"
116+
height="15"
117+
viewBox="0 0 16 16"
118+
width="15"
119+
xmlns="http://www.w3.org/2000/svg"
120+
>
121+
<rect
122+
fill="none"
123+
height="12"
124+
rx="1.8"
125+
stroke="#1B1B1B"
126+
strokeWidth="1.3"
127+
width="7"
128+
x="4.5"
129+
y="2"
130+
/>
131+
<path d="M7 12.2H9" fill="none" stroke="#1B1B1B" strokeLinecap="round" strokeWidth="1.2" />
132+
</svg>
133+
);
134+
}
135+
136+
export function GitHubMark() {
137+
return (
138+
<svg
139+
aria-hidden="true"
140+
height="14"
141+
viewBox="0 0 16 16"
142+
width="14"
143+
xmlns="http://www.w3.org/2000/svg"
144+
>
145+
<path
146+
d="M8 1C4.13 1 1 4.13 1 8C1 11.09 3.01 13.71 5.79 14.64C6.14 14.7 6.27 14.49 6.27 14.31C6.27 14.14 6.26 13.58 6.26 12.98C4.5 13.3 4.04 12.55 3.9 12.16C3.82 11.96 3.48 11.34 3.18 11.17C2.93 11.04 2.58 10.72 3.17 10.71C3.73 10.7 4.13 11.22 4.26 11.43C4.9 12.5 5.92 12.2 6.3 12.02C6.36 11.56 6.55 11.25 6.75 11.07C5.18 10.89 3.53 10.28 3.53 7.58C3.53 6.81 3.82 6.18 4.28 5.69C4.21 5.51 3.96 4.79 4.35 3.82C4.35 3.82 4.94 3.63 6.27 4.52C6.83 4.37 7.42 4.29 8.01 4.29C8.6 4.29 9.19 4.37 9.75 4.52C11.08 3.62 11.67 3.82 11.67 3.82C12.06 4.79 11.81 5.51 11.74 5.69C12.2 6.18 12.49 6.8 12.49 7.58C12.49 10.29 10.83 10.89 9.26 11.07C9.51 11.29 9.73 11.71 9.73 12.37C9.73 13.31 9.72 14.07 9.72 14.31C9.72 14.49 9.85 14.71 10.2 14.64C12.97 13.71 14.98 11.08 14.98 8C14.98 4.13 11.85 1 7.98 1H8Z"
147+
fill="#1B1B1B"
148+
/>
149+
</svg>
150+
);
151+
}
152+
153+
export function ReturnArrow() {
154+
return (
155+
<svg
156+
aria-hidden="true"
157+
height="11"
158+
viewBox="0 0 12 12"
159+
width="11"
160+
xmlns="http://www.w3.org/2000/svg"
161+
>
162+
<path
163+
d="M10 2V6H2.5M2.5 6L5 3.5M2.5 6L5 8.5"
164+
fill="none"
165+
stroke="currentColor"
166+
strokeLinecap="round"
167+
strokeLinejoin="round"
168+
strokeWidth="1.3"
169+
/>
170+
</svg>
171+
);
172+
}
173+
174+
export function SearchIcon() {
175+
return (
176+
<svg
177+
aria-hidden="true"
178+
height="13"
179+
viewBox="0 0 16 16"
180+
width="13"
181+
xmlns="http://www.w3.org/2000/svg"
182+
>
183+
<circle cx="7" cy="7" fill="none" r="4.5" stroke="#9B9B9B" strokeWidth="1.4" />
184+
<path
185+
d="M10.5 10.5L13.5 13.5"
186+
fill="none"
187+
stroke="#9B9B9B"
188+
strokeLinecap="round"
189+
strokeWidth="1.4"
190+
/>
191+
</svg>
192+
);
193+
}

0 commit comments

Comments
 (0)