mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
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:
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%;
|
||||
|
||||
@@ -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(
|
||||
() =>
|
||||
(
|
||||
|
||||
Reference in New Issue
Block a user