mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
explore(desktop): anonymous app-shell home on first open
A fresh install now opens straight into a full-bleed mock of the app frame — ghost community rail and sidebar skeleton on the left, a Discord-style community-discovery home in the main pane — instead of a centered onboarding card stack. The user is presented with options (join a featured community, set up identity & agents, import a key) rather than being put in front of an agent as the first thing. Joining stays one click: identity is persisted silently and a first-community onboarding transaction connects to the chosen relay. The classic corridor and key import remain reachable below the fold. Replaces the centered DiscoveryLanding from the previous exploration with AnonymousShellLanding; the discover page escapes the onboarding shell (no chartreuse welcome chrome) and renders the app frame. Exploration for the community-first onboarding flip (Track 2), built on top of explore/discord-style-landing. Co-authored-by: Thomas Petersen <thomasp@squareup.com> Signed-off-by: Thomas Petersen <thomasp@squareup.com>
This commit is contained in:
co-authored by
Thomas Petersen
parent
3ddaa0d59d
commit
a737d62e4d
@@ -23,7 +23,7 @@ export default defineConfig({
|
||||
"**/sidebar-offcanvas-rail.spec.ts",
|
||||
"**/search-scope-screenshots.spec.ts",
|
||||
"**/onboarding-docked-cta-screenshots.spec.ts",
|
||||
"**/discovery-landing-shot.spec.ts",
|
||||
"**/anonymous-shell-shot.spec.ts",
|
||||
"**/identity-key-help.spec.ts",
|
||||
"**/key-import-reveal.spec.ts",
|
||||
"**/navigation.spec.ts",
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
import { Compass, Plus } from "lucide-react";
|
||||
|
||||
import {
|
||||
FEATURED_COMMUNITIES,
|
||||
type FeaturedCommunity,
|
||||
} from "@/features/onboarding/featuredCommunities";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { Card } from "@/shared/ui/card";
|
||||
import { Skeleton } from "@/shared/ui/skeleton";
|
||||
|
||||
import { ONBOARDING_SECONDARY_CTA_CLASS } from "./OnboardingChrome";
|
||||
|
||||
/**
|
||||
* EXPLORATION — anonymous-first app shell (Discord model).
|
||||
*
|
||||
* A fresh install opens straight into something that looks like the app:
|
||||
* a ghost sidebar on the left and a community-discovery home in the main
|
||||
* pane. No identity ceremony and no agent greeting the user — joining a
|
||||
* community creates the key silently, and agents become something you
|
||||
* discover later. The classic corridor and key import stay reachable as
|
||||
* quiet options below the fold.
|
||||
*/
|
||||
export function AnonymousShellLanding({
|
||||
error,
|
||||
isPending,
|
||||
onAdvancedSetup,
|
||||
onImportKey,
|
||||
onJoin,
|
||||
}: {
|
||||
error: string | null;
|
||||
isPending: boolean;
|
||||
/** Classic corridor: identity → backup → harness → config. */
|
||||
onAdvancedSetup: () => void;
|
||||
onImportKey: () => void;
|
||||
onJoin: (community: FeaturedCommunity) => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex h-full w-full" data-testid="discovery-landing">
|
||||
<aside
|
||||
aria-hidden
|
||||
className="flex w-14 shrink-0 flex-col items-center gap-2.5 border-r border-sidebar-border/60 bg-sidebar px-2.5 pb-5 pt-12"
|
||||
data-testid="anon-shell-rail"
|
||||
>
|
||||
<span className="flex h-9 w-9 items-center justify-center rounded-xl bg-primary text-primary-foreground">
|
||||
<Compass className="h-4 w-4" />
|
||||
</span>
|
||||
<span className="my-0.5 h-px w-7 bg-sidebar-border" />
|
||||
<Skeleton className="h-9 w-9 rounded-2xl" pulsing={false} />
|
||||
<Skeleton className="h-9 w-9 rounded-2xl" pulsing={false} />
|
||||
<span className="flex h-9 w-9 items-center justify-center rounded-2xl bg-sidebar-accent/60 text-sidebar-foreground/70">
|
||||
<Plus className="h-4 w-4" />
|
||||
</span>
|
||||
</aside>
|
||||
<aside
|
||||
aria-hidden
|
||||
className="flex w-60 shrink-0 flex-col border-r border-sidebar-border bg-sidebar px-3 pb-3 pt-12"
|
||||
data-testid="anon-shell-sidebar"
|
||||
>
|
||||
<div className="rounded-lg bg-foreground/8 px-3 py-2 text-xs text-foreground/45">
|
||||
Search everything
|
||||
</div>
|
||||
<div className="mt-4 flex flex-col gap-0.5">
|
||||
{["Inbox", "Pulse", "Projects", "Agents"].map((label) => (
|
||||
<div
|
||||
className="rounded-md px-2 py-1.5 text-sm text-foreground/40"
|
||||
key={label}
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-7 px-2 text-xs font-medium uppercase tracking-wide text-foreground/35">
|
||||
Channels
|
||||
</div>
|
||||
<div className="mt-3 flex flex-col gap-3 px-2">
|
||||
<Skeleton className="h-3.5 w-32" pulsing={false} />
|
||||
<Skeleton className="h-3.5 w-24" pulsing={false} />
|
||||
<Skeleton className="h-3.5 w-28" pulsing={false} />
|
||||
</div>
|
||||
<div className="mt-8 px-2 text-xs font-medium uppercase tracking-wide text-foreground/35">
|
||||
Direct messages
|
||||
</div>
|
||||
<div className="mt-3 flex flex-col gap-3 px-2">
|
||||
<Skeleton className="h-3.5 w-28" pulsing={false} />
|
||||
<Skeleton className="h-3.5 w-20" pulsing={false} />
|
||||
</div>
|
||||
<div className="mt-auto rounded-xl bg-foreground/5 px-3 py-2.5 text-left">
|
||||
<p className="text-sm font-medium text-foreground/80">Guest</p>
|
||||
<p className="mt-0.5 text-xs leading-4 text-foreground/50">
|
||||
Join a community to claim your identity
|
||||
</p>
|
||||
</div>
|
||||
</aside>
|
||||
<main className="flex min-w-0 flex-1 flex-col overflow-y-auto">
|
||||
<div className="mx-auto w-full max-w-[880px] px-8 pb-12 pt-14">
|
||||
<div className="rounded-2xl bg-foreground/5 px-10 py-12 text-center">
|
||||
<h1 className="text-3xl font-semibold leading-tight text-foreground">
|
||||
Find your community on Buzz
|
||||
</h1>
|
||||
<p className="mx-auto mt-3 max-w-[480px] text-sm leading-6 text-foreground/70">
|
||||
From building agents to protocol talk, there’s a place for you.
|
||||
Jump in — your identity is created as you go.
|
||||
</p>
|
||||
</div>
|
||||
{error ? (
|
||||
<p className="mt-6 text-center text-sm text-destructive">{error}</p>
|
||||
) : null}
|
||||
<h2 className="mt-10 text-left text-base font-medium text-foreground/85">
|
||||
Featured communities
|
||||
</h2>
|
||||
<div className="mt-4 grid w-full grid-cols-1 gap-x-8 gap-y-10 sm:grid-cols-2">
|
||||
{FEATURED_COMMUNITIES.map((community) => (
|
||||
<Card
|
||||
className="items-stretch px-7 py-5 text-left [--buzz-card-textured-min-height:132px]"
|
||||
key={community.id}
|
||||
variant="textured"
|
||||
>
|
||||
<div className="flex items-start gap-4">
|
||||
<span
|
||||
aria-hidden
|
||||
className="flex h-11 w-11 shrink-0 items-center justify-center rounded-xl bg-foreground/8 text-2xl"
|
||||
>
|
||||
{community.emoji}
|
||||
</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-baseline justify-between gap-3">
|
||||
<span className="truncate text-base font-medium text-foreground">
|
||||
{community.name}
|
||||
</span>
|
||||
<span className="shrink-0 text-xs text-foreground/60">
|
||||
{community.members.toLocaleString()} members
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-1 line-clamp-2 text-sm leading-5 text-foreground/75">
|
||||
{community.tagline}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 flex justify-end">
|
||||
<Button
|
||||
className="h-8 rounded-full px-5"
|
||||
data-testid={`discovery-join-${community.id}`}
|
||||
disabled={isPending}
|
||||
onClick={() => onJoin(community)}
|
||||
size="sm"
|
||||
type="button"
|
||||
>
|
||||
Join
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-12 flex flex-col items-center gap-3">
|
||||
<p className="text-sm text-foreground/70">
|
||||
Want to set things up yourself first?
|
||||
</p>
|
||||
<div className="flex flex-wrap items-center justify-center gap-3">
|
||||
<Button
|
||||
className={ONBOARDING_SECONDARY_CTA_CLASS}
|
||||
data-testid="discovery-advanced-setup"
|
||||
disabled={isPending}
|
||||
onClick={onAdvancedSetup}
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
Set up identity & agents first
|
||||
</Button>
|
||||
<Button
|
||||
className={ONBOARDING_SECONDARY_CTA_CLASS}
|
||||
data-testid="discovery-import-key"
|
||||
disabled={isPending}
|
||||
onClick={onImportKey}
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
I already have a key
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
import {
|
||||
FEATURED_COMMUNITIES,
|
||||
type FeaturedCommunity,
|
||||
} from "@/features/onboarding/featuredCommunities";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { Card } from "@/shared/ui/card";
|
||||
|
||||
import { ONBOARDING_SECONDARY_CTA_CLASS } from "./OnboardingChrome";
|
||||
|
||||
/**
|
||||
* EXPLORATION — Discord-style first-open landing.
|
||||
*
|
||||
* The very first screen a fresh install shows: a directory of communities
|
||||
* the user can join right away. No identity ceremony, no agent corridor —
|
||||
* clicking Join creates the key silently and connects. Agent setup and key
|
||||
* backup become one avenue off this screen (`onAdvancedSetup`) instead of
|
||||
* the mandatory path.
|
||||
*/
|
||||
export function DiscoveryLanding({
|
||||
error,
|
||||
isPending,
|
||||
onAdvancedSetup,
|
||||
onImportKey,
|
||||
onJoin,
|
||||
}: {
|
||||
error: string | null;
|
||||
isPending: boolean;
|
||||
/** Classic corridor: identity → backup → harness → config. */
|
||||
onAdvancedSetup: () => void;
|
||||
onImportKey: () => void;
|
||||
onJoin: (community: FeaturedCommunity) => void;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className="flex w-full max-w-[860px] flex-col items-center text-center"
|
||||
data-testid="discovery-landing"
|
||||
>
|
||||
<img
|
||||
alt="Buzz"
|
||||
className="w-full max-w-[420px]"
|
||||
src="/landing/buzz-wordmark.png"
|
||||
/>
|
||||
<h1 className="mt-4 text-2xl font-normal leading-tight text-foreground">
|
||||
Find your people
|
||||
</h1>
|
||||
<p className="mt-2 max-w-[520px] text-sm leading-6 text-foreground/80">
|
||||
Jump into a community — we’ll set up your identity as you go. Your
|
||||
agents, backups, and settings are one click away once you’re in.
|
||||
</p>
|
||||
{error ? <p className="mt-4 text-sm text-destructive">{error}</p> : null}
|
||||
<div className="mt-10 grid w-full grid-cols-1 gap-x-10 gap-y-12 sm:grid-cols-2">
|
||||
{FEATURED_COMMUNITIES.map((community) => (
|
||||
<Card
|
||||
className="items-stretch px-7 py-5 text-left [--buzz-card-textured-min-height:132px]"
|
||||
key={community.id}
|
||||
variant="textured"
|
||||
>
|
||||
<div className="flex items-start gap-4">
|
||||
<span
|
||||
aria-hidden
|
||||
className="flex h-11 w-11 shrink-0 items-center justify-center rounded-xl bg-foreground/8 text-2xl"
|
||||
>
|
||||
{community.emoji}
|
||||
</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-baseline justify-between gap-3">
|
||||
<span className="truncate text-base font-medium text-foreground">
|
||||
{community.name}
|
||||
</span>
|
||||
<span className="shrink-0 text-xs text-foreground/60">
|
||||
{community.members.toLocaleString()} members
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-1 line-clamp-2 text-sm leading-5 text-foreground/75">
|
||||
{community.tagline}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 flex justify-end">
|
||||
<Button
|
||||
className="h-8 rounded-full px-5"
|
||||
data-testid={`discovery-join-${community.id}`}
|
||||
disabled={isPending}
|
||||
onClick={() => onJoin(community)}
|
||||
size="sm"
|
||||
type="button"
|
||||
>
|
||||
Join
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-12 flex flex-col items-center gap-3 pb-10">
|
||||
<p className="text-sm text-foreground/70">
|
||||
Have an invite link, or want to run your own?
|
||||
</p>
|
||||
<div className="flex flex-wrap items-center justify-center gap-3">
|
||||
<Button
|
||||
className={ONBOARDING_SECONDARY_CTA_CLASS}
|
||||
data-testid="discovery-advanced-setup"
|
||||
disabled={isPending}
|
||||
onClick={onAdvancedSetup}
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
Set up identity & agents first
|
||||
</Button>
|
||||
<Button
|
||||
className={ONBOARDING_SECONDARY_CTA_CLASS}
|
||||
data-testid="discovery-import-key"
|
||||
disabled={isPending}
|
||||
onClick={onImportKey}
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
I already have a key
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -18,9 +18,9 @@ import {
|
||||
import { StartupWindowDragRegion } from "@/shared/ui/StartupWindowDragRegion";
|
||||
import type { FeaturedCommunity } from "@/features/onboarding/featuredCommunities";
|
||||
import { useCommunityOnboarding } from "@/features/onboarding/communityOnboarding";
|
||||
import { AnonymousShellLanding } from "./AnonymousShellLanding";
|
||||
import { BackupStep } from "./BackupStep";
|
||||
import { DefaultConfigStep } from "./DefaultConfigStep";
|
||||
import { DiscoveryLanding } from "./DiscoveryLanding";
|
||||
import { DownloadKeyStep } from "./DownloadKeyStep";
|
||||
import {
|
||||
backupSessionToPasswordEntry,
|
||||
@@ -327,12 +327,44 @@ export function MachineOnboardingFlow({
|
||||
}
|
||||
: undefined;
|
||||
|
||||
// EXPLORATION — the discover page escapes the onboarding shell entirely.
|
||||
// A fresh install opens straight into a full-bleed mock of the app frame
|
||||
// (rail + sidebar + main pane) instead of a centered onboarding corridor,
|
||||
// so the first thing a user meets is the product, not a setup ceremony.
|
||||
if (page === "discover") {
|
||||
return (
|
||||
<div
|
||||
className="flex h-dvh w-full overflow-hidden bg-background text-foreground"
|
||||
data-testid="machine-onboarding-gate"
|
||||
>
|
||||
<StartupWindowDragRegion />
|
||||
<AnonymousShellLanding
|
||||
error={error}
|
||||
isPending={isPending}
|
||||
onAdvancedSetup={() => {
|
||||
setError(null);
|
||||
setTransitionDirection("forward");
|
||||
setPage("identity");
|
||||
}}
|
||||
onImportKey={() => {
|
||||
setError(null);
|
||||
setKeyImportDialog(null);
|
||||
setKeyImportStage("key-entry");
|
||||
setTransitionDirection("forward");
|
||||
setPage("key-import");
|
||||
}}
|
||||
onJoin={(community) => void quickJoinCommunity(community)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 text-foreground ${
|
||||
isSecuritySubview ? "buzz-onboarding-security-theme" : ""
|
||||
} ${
|
||||
page === "discover" || page === "identity"
|
||||
page === "identity"
|
||||
? "buzz-onboarding-welcome py-8"
|
||||
: "pb-28 pt-[106px]"
|
||||
}`}
|
||||
@@ -340,7 +372,7 @@ export function MachineOnboardingFlow({
|
||||
>
|
||||
<StartupWindowDragRegion />
|
||||
{page === "identity" ? <LandingBees /> : null}
|
||||
{page !== "discover" && page !== "identity" && !isSecuritySubview ? (
|
||||
{page !== "identity" && !isSecuritySubview ? (
|
||||
<OnboardingChrome
|
||||
current={page === "config" ? 4 : page === "setup" ? 3 : 2}
|
||||
/>
|
||||
@@ -348,36 +380,10 @@ export function MachineOnboardingFlow({
|
||||
<OnboardingFooterProvider backAction={chromeBackAction}>
|
||||
<div
|
||||
className={`relative flex w-full max-w-[1040px] flex-col items-center text-center ${
|
||||
page === "discover" || page === "identity"
|
||||
? "my-auto"
|
||||
: "buzz-onboarding-step-frame"
|
||||
page === "identity" ? "my-auto" : "buzz-onboarding-step-frame"
|
||||
}`}
|
||||
>
|
||||
{page === "discover" ? (
|
||||
<OnboardingSlideTransition
|
||||
className="flex w-full flex-col items-center text-center"
|
||||
direction={transitionDirection}
|
||||
transitionKey={`machine-discover-${transitionDirection}`}
|
||||
>
|
||||
<DiscoveryLanding
|
||||
error={error}
|
||||
isPending={isPending}
|
||||
onAdvancedSetup={() => {
|
||||
setError(null);
|
||||
setTransitionDirection("forward");
|
||||
setPage("identity");
|
||||
}}
|
||||
onImportKey={() => {
|
||||
setError(null);
|
||||
setKeyImportDialog(null);
|
||||
setKeyImportStage("key-entry");
|
||||
setTransitionDirection("forward");
|
||||
setPage("key-import");
|
||||
}}
|
||||
onJoin={(community) => void quickJoinCommunity(community)}
|
||||
/>
|
||||
</OnboardingSlideTransition>
|
||||
) : page === "identity" ? (
|
||||
{page === "identity" ? (
|
||||
<OnboardingSlideTransition
|
||||
className="flex w-full max-w-[720px] flex-col items-center text-center"
|
||||
direction={transitionDirection}
|
||||
|
||||
+8
-4
@@ -3,22 +3,26 @@ import { expect, test } from "@playwright/test";
|
||||
import { waitForAnimations } from "../helpers/animations";
|
||||
import { installMockBridge } from "../helpers/bridge";
|
||||
|
||||
const SHOT_DIR = "test-results/discovery-landing";
|
||||
const SHOT_DIR = "test-results/anonymous-shell";
|
||||
|
||||
test.use({ viewport: { width: 1280, height: 960 } });
|
||||
|
||||
test("discord-style discovery landing on first open", async ({ page }) => {
|
||||
test("anonymous app-shell home on first open", async ({ page }) => {
|
||||
await installMockBridge(page, undefined, {
|
||||
skipCommunitySeed: true,
|
||||
skipOnboardingSeed: true,
|
||||
});
|
||||
await page.goto("/");
|
||||
|
||||
// Fresh install → straight to the discovery landing. No identity ceremony.
|
||||
// Fresh install → straight into the full-bleed app shell: ghost rail +
|
||||
// sidebar on the left, community discovery in the main pane. No identity
|
||||
// ceremony and no agent greeting.
|
||||
await expect(page.getByTestId("discovery-landing")).toBeVisible();
|
||||
await expect(page.getByTestId("anon-shell-rail")).toBeVisible();
|
||||
await expect(page.getByTestId("anon-shell-sidebar")).toBeVisible();
|
||||
await expect(page.getByTestId("discovery-join-buzz-hq")).toBeVisible();
|
||||
await waitForAnimations(page);
|
||||
await page.screenshot({ path: `${SHOT_DIR}/01-discovery-landing.png` });
|
||||
await page.screenshot({ path: `${SHOT_DIR}/01-anonymous-shell-home.png` });
|
||||
|
||||
// Advanced setup remains one click away (the classic corridor).
|
||||
await page.getByTestId("discovery-advanced-setup").click();
|
||||
Reference in New Issue
Block a user