Restyle onboarding: branded landing screen, yellow/gradient backgrounds, new starter avatars (#1982)

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Pinky <44b8e82baa6e0e254e0208d68f335c283c94e7b78dd1fa10d5a49d3f13dd0435@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
Wes
2026-07-16 11:48:52 -07:00
committed by GitHub
co-authored by Pinky Brain
parent 44359f635a
commit 831c80c1a5
6 changed files with 213 additions and 21 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 325 KiB

File diff suppressed because one or more lines are too long
@@ -0,0 +1,168 @@
import * as React from "react";
import { BuzzMark } from "@/shared/ui/buzz-logo/BuzzMark";
import { FlappingBee } from "@/shared/ui/buzz-logo/FlappingBee";
type Bee = {
top: string;
left: string;
size: number;
rotate: number;
color: string;
};
const WHITE = "#FFFFFF";
const YELLOW = "#E9E94F";
// Fixed scatter so the field doesn't shimmer between renders.
const BEES: Bee[] = [
{ top: "4%", left: "27%", size: 34, rotate: -12, color: WHITE },
{ top: "7%", left: "58%", size: 28, rotate: 18, color: YELLOW },
{ top: "5%", left: "88%", size: 32, rotate: -20, color: WHITE },
{ top: "13%", left: "12%", size: 36, rotate: 18, color: YELLOW },
{ top: "12%", left: "73%", size: 26, rotate: -8, color: WHITE },
{ top: "18%", left: "44%", size: 24, rotate: 25, color: YELLOW },
{ top: "22%", left: "90%", size: 34, rotate: 10, color: WHITE },
{ top: "28%", left: "5%", size: 28, rotate: -18, color: YELLOW },
{ top: "31%", left: "21%", size: 24, rotate: 8, color: YELLOW },
{ top: "35%", left: "84%", size: 32, rotate: -14, color: WHITE },
{ top: "45%", left: "13%", size: 32, rotate: 20, color: YELLOW },
{ top: "47%", left: "93%", size: 26, rotate: -6, color: YELLOW },
{ top: "55%", left: "30%", size: 26, rotate: -24, color: WHITE },
{ top: "57%", left: "70%", size: 34, rotate: 12, color: YELLOW },
{ top: "63%", left: "8%", size: 34, rotate: 16, color: WHITE },
{ top: "66%", left: "88%", size: 28, rotate: -10, color: YELLOW },
{ top: "72%", left: "48%", size: 26, rotate: 22, color: YELLOW },
{ top: "76%", left: "18%", size: 32, rotate: -16, color: WHITE },
{ top: "80%", left: "64%", size: 28, rotate: 8, color: YELLOW },
{ top: "86%", left: "34%", size: 34, rotate: -20, color: WHITE },
{ top: "88%", left: "80%", size: 32, rotate: 14, color: YELLOW },
{ top: "92%", left: "10%", size: 26, rotate: -8, color: YELLOW },
{ top: "3%", left: "42%", size: 22, rotate: 14, color: WHITE },
{ top: "9%", left: "5%", size: 24, rotate: -22, color: YELLOW },
{ top: "16%", left: "62%", size: 30, rotate: -4, color: YELLOW },
{ top: "20%", left: "30%", size: 22, rotate: 12, color: WHITE },
{ top: "26%", left: "52%", size: 26, rotate: -14, color: YELLOW },
{ top: "33%", left: "68%", size: 22, rotate: 24, color: WHITE },
{ top: "40%", left: "40%", size: 24, rotate: -10, color: YELLOW },
{ top: "42%", left: "78%", size: 28, rotate: 6, color: YELLOW },
{ top: "52%", left: "55%", size: 22, rotate: -18, color: WHITE },
{ top: "60%", left: "42%", size: 28, rotate: 10, color: YELLOW },
{ top: "68%", left: "26%", size: 24, rotate: -6, color: WHITE },
{ top: "70%", left: "76%", size: 30, rotate: 18, color: YELLOW },
{ top: "82%", left: "6%", size: 28, rotate: 22, color: WHITE },
{ top: "84%", left: "50%", size: 24, rotate: -12, color: YELLOW },
{ top: "94%", left: "60%", size: 28, rotate: 16, color: YELLOW },
{ top: "95%", left: "90%", size: 22, rotate: -24, color: WHITE },
];
const REPEL_RADIUS = 180;
const REPEL_STRENGTH = 110;
// Autonomous wander: each bee drifts on its own smooth loop.
const WANDER_X = 26;
const WANDER_Y = 20;
export function LandingBees() {
const fieldRef = React.useRef<HTMLDivElement>(null);
const beeRefs = React.useRef<(HTMLSpanElement | null)[]>([]);
const pointer = React.useRef<{ x: number; y: number } | null>(null);
const offsets = React.useRef(BEES.map(() => ({ x: 0, y: 0 })));
React.useEffect(() => {
const field = fieldRef.current;
if (!field) return;
let raf = 0;
const start = performance.now();
const tick = (now: number) => {
const t = (now - start) / 1000;
const rect = field.getBoundingClientRect();
const p = pointer.current;
beeRefs.current.forEach((el, i) => {
if (!el) return;
const bee = BEES[i];
// Per-bee wander: two incommensurate sine waves, phase-shifted by index.
const phase = i * 1.7;
const wx =
Math.sin(t * (0.7 + (i % 5) * 0.13) + phase) * WANDER_X +
Math.sin(t * 1.9 + phase * 2.1) * 6;
const wy =
Math.cos(t * (0.6 + (i % 7) * 0.11) + phase) * WANDER_Y +
Math.cos(t * 2.3 + phase * 1.3) * 5;
let rx = 0;
let ry = 0;
if (p) {
const cx = rect.left + (rect.width * parseFloat(bee.left)) / 100;
const cy = rect.top + (rect.height * parseFloat(bee.top)) / 100;
const ox = cx - p.x;
const oy = cy - p.y;
const dist = Math.hypot(ox, oy);
if (dist < REPEL_RADIUS && dist > 0.01) {
const push =
((REPEL_RADIUS - dist) / REPEL_RADIUS) * REPEL_STRENGTH;
rx = (ox / dist) * push;
ry = (oy / dist) * push;
}
}
// Ease toward the combined target so repulsion enters/exits smoothly.
const target = { x: wx + rx, y: wy + ry };
const cur = offsets.current[i];
cur.x += (target.x - cur.x) * 0.12;
cur.y += (target.y - cur.y) * 0.12;
el.style.transform = `translate(${cur.x}px, ${cur.y}px) rotate(${bee.rotate}deg)`;
});
raf = requestAnimationFrame(tick);
};
const onMove = (event: MouseEvent) => {
pointer.current = { x: event.clientX, y: event.clientY };
};
const onLeave = () => {
pointer.current = null;
};
const reduced = window.matchMedia("(prefers-reduced-motion: reduce)");
if (!reduced.matches) {
raf = requestAnimationFrame(tick);
window.addEventListener("mousemove", onMove);
window.addEventListener("mouseout", onLeave);
}
return () => {
window.removeEventListener("mousemove", onMove);
window.removeEventListener("mouseout", onLeave);
if (raf) cancelAnimationFrame(raf);
};
}, []);
return (
<div
ref={fieldRef}
aria-hidden
className="pointer-events-none absolute inset-0 overflow-hidden"
>
<span className="absolute left-6 top-12 block w-11 text-[#231E1E]">
<BuzzMark className="h-auto w-full" />
</span>
{BEES.map((bee, i) => (
<span
key={`${bee.top}-${bee.left}`}
ref={(el) => {
beeRefs.current[i] = el;
}}
className="absolute block will-change-transform"
style={{
top: bee.top,
left: bee.left,
width: bee.size,
color: bee.color,
transform: `rotate(${bee.rotate}deg)`,
opacity: 0.9,
}}
>
<FlappingBee className="w-full" />
</span>
))}
</div>
);
}
@@ -9,6 +9,7 @@ import {
import { Button } from "@/shared/ui/button";
import { StartupWindowDragRegion } from "@/shared/ui/StartupWindowDragRegion";
import { BackupStep } from "./BackupStep";
import { LandingBees } from "./LandingBees";
import { NostrKeyImportForm } from "./NostrKeyImportForm";
import { OnboardingSlideTransition } from "./OnboardingSlideTransition";
import { SetupStep } from "./SetupStep";
@@ -86,37 +87,35 @@ export function MachineOnboardingFlow({
return (
<div
className="buzz-onboarding-neutral-theme buzz-startup-shell flex items-center justify-center bg-background px-4 py-8 text-foreground"
className={`buzz-onboarding-neutral-theme buzz-startup-shell flex items-center justify-center px-4 py-8 text-foreground ${
page === "identity" ? "buzz-onboarding-welcome" : ""
}`}
data-testid="machine-onboarding-gate"
>
<StartupWindowDragRegion />
{page === "identity" ? <LandingBees /> : null}
<div className="relative flex w-full max-w-[920px] flex-col items-center text-center">
{page === "identity" ? (
<OnboardingSlideTransition
className="flex w-full max-w-[500px] flex-col items-center text-center"
className="flex w-full max-w-[720px] flex-col items-center text-center"
direction="forward"
effect="mask-reveal-up"
transitionKey="machine-identity"
>
<img
alt="Buzz"
className="h-14 w-14 rounded-xl shadow-xs"
src="/app-icon@2x.png"
srcSet="/app-icon@2x.png 1x, /app-icon@3x.png 2x"
className="w-full max-w-[560px]"
src="/landing/buzz-wordmark.png"
/>
<h1 className="mt-6 text-3xl font-semibold tracking-tight">
Welcome to Buzz
</h1>
<p className="mt-3 max-w-[440px] text-sm leading-6 text-muted-foreground">
Start with a new Nostr identity or bring the key you already use.
Your identity works across every community you join.
<p className="mt-2 max-w-[560px] text-center text-2xl font-normal leading-none text-foreground">
Your people, your agents, your projects all in one place.
</p>
{error ? (
<p className="mt-4 text-sm text-destructive">{error}</p>
) : null}
<div className="mt-8 flex w-full flex-col gap-3">
<div className="mt-10 flex flex-col items-center gap-3">
<Button
className="h-10 w-full"
className="h-10 rounded-full px-6"
disabled={isPending}
onClick={() => void loadFreshIdentity()}
type="button"
@@ -124,13 +123,13 @@ export function MachineOnboardingFlow({
{isPending ? "Saving identity…" : "Get started"}
</Button>
<Button
className="h-10 w-full"
className="h-9 rounded-full bg-foreground/10 px-5 hover:bg-foreground/15"
disabled={isPending}
onClick={() => setPage("key-import")}
type="button"
variant="ghost"
>
I already have a key
Enter a key
</Button>
</div>
</OnboardingSlideTransition>
@@ -214,6 +214,15 @@
width: 100%;
}
.buzz-onboarding-neutral-theme.buzz-startup-shell {
background-image: linear-gradient(to bottom, #d7d72e, #d7e7f6);
}
.buzz-onboarding-neutral-theme.buzz-startup-shell.buzz-onboarding-welcome {
background-color: #d7d72e;
background-image: none;
}
.buzz-onboarding-neutral-theme {
--background: 0 0% 100%;
--foreground: 0 0% 9%;
@@ -233,7 +242,7 @@
--buzz-onboarding-emoji-picker-input: 255, 255, 255;
}
.dark .buzz-onboarding-neutral-theme {
.dark .buzz-onboarding-neutral-theme:not(.buzz-startup-shell) {
--background: 0 0% 3.9%;
--foreground: 0 0% 98%;
--primary: 0 0% 98%;
@@ -271,7 +280,9 @@
--buzz-onboarding-emoji-picker-input: 255, 255, 255;
}
.buzz-onboarding-neutral-theme[data-system-color-scheme="dark"] {
.buzz-onboarding-neutral-theme[data-system-color-scheme="dark"]:not(
.buzz-startup-shell
) {
--background: 0 0% 3.9%;
--foreground: 0 0% 98%;
--primary: 0 0% 98%;
+15 -1
View File
@@ -7,18 +7,32 @@ import { installMockBridge, TEST_IDENTITIES } from "../helpers/bridge";
test("normal first launch uses the already-persisted identity", async ({
page,
}) => {
await page.emulateMedia({ colorScheme: "dark" });
await installMockBridge(page, undefined, {
skipCommunitySeed: true,
skipOnboardingSeed: true,
});
await page.goto("/");
await expect(page.getByTestId("machine-onboarding-gate")).toBeVisible();
const gate = page.getByTestId("machine-onboarding-gate");
await expect(gate).toBeVisible();
await expect(gate).toHaveCSS("background-color", "rgb(215, 215, 46)");
await expect(gate).toHaveCSS("background-image", "none");
await expect(gate).toHaveCSS("color", "rgb(23, 23, 23)");
await expect(page.getByRole("button", { name: "Get started" })).toHaveCSS(
"background-color",
"rgb(23, 23, 23)",
);
await page.getByRole("button", { name: "Get started" }).click();
await expect(
page.getByRole("heading", { name: "Save your private key" }),
).toBeVisible();
await expect(gate).toHaveCSS(
"background-image",
"linear-gradient(rgb(215, 215, 46), rgb(215, 231, 246))",
);
await expect(gate).toHaveCSS("color", "rgb(23, 23, 23)");
const commands = await page.evaluate(
() =>
(