From 28c31142e2ba85136ebad7f6d610b679a34145fa Mon Sep 17 00:00:00 2001 From: Bradley Axen Date: Sun, 19 Jul 2026 11:34:37 -0700 Subject: [PATCH 1/9] feat(desktop): modal sign-in + brand the communities onboarding page Turn the "create or connect your own community" onboarding into a modal experience and align the post-login page with the rest of onboarding, per feedback on #2123. - Sign-in and identity-linking (and the identity-mismatch recovery) now open in a Radix Dialog over the "Your communities" page, which blurs behind the overlay. The modal closes automatically once you're signed in with a linked, matching identity, revealing the communities list/create view. Closing the modal clears Builderlab auth and returns to the community picker. - Brand the post-login page: replace the flat opaque white cards with the translucent, borderless frames and pill CTAs used by the invite/key screens so it reads as part of onboarding rather than a plain form. - Simplify copy: CTA is now "Continue" and the "Block-hosted" Builderlab line is dropped. - The modal re-establishes the always-light onboarding theme inside the portal so it matches the shell in both light and dark system modes. - Update the onboarding e2e assertions for the new modal DOM and copy. Co-authored-by: Claude Opus 4.8 (1M context) Co-authored-by: Bradley Axen Signed-off-by: Bradley Axen --- .../ui/HostedCommunityOnboarding.tsx | 296 +++++++++++------- desktop/tests/e2e/onboarding.spec.ts | 27 +- 2 files changed, 199 insertions(+), 124 deletions(-) diff --git a/desktop/src/features/communities/ui/HostedCommunityOnboarding.tsx b/desktop/src/features/communities/ui/HostedCommunityOnboarding.tsx index 46c1286d3f..420c860df3 100644 --- a/desktop/src/features/communities/ui/HostedCommunityOnboarding.tsx +++ b/desktop/src/features/communities/ui/HostedCommunityOnboarding.tsx @@ -26,6 +26,31 @@ import { safeNpub } from "@/shared/lib/nostrUtils"; import { Button } from "@/shared/ui/button"; import { Input } from "@/shared/ui/input"; import { OnboardingFooter } from "@/features/onboarding/ui/OnboardingFooter"; +import { ONBOARDING_PRIMARY_CTA_CLASS } from "@/features/onboarding/ui/OnboardingChrome"; +import { BuzzMark } from "@/shared/ui/buzz-logo/BuzzMark"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogTitle, +} from "@/shared/ui/dialog"; + +/** + * Branded translucent frame reused across the post-login page — mirrors the + * fuzzy-white key/invite frames (`bg-white/…` on the onboarding shell) so the + * "Your communities" page reads as part of onboarding rather than a plain + * black-on-white form. + */ +const BRAND_SECTION_CLASS = "rounded-xl bg-white/55 p-6 text-left"; + +/** + * The sign-in and identity-linking steps live in a modal that opens over the + * "Your communities" page (which blurs behind the Radix overlay). It composes + * its own panel (`surface="none"`) so it can re-establish the always-light + * onboarding theme inside the portal, matching the shell behind it. + */ +const MODAL_PANEL_CLASS = + "buzz-onboarding-neutral-theme buzz-startup-shell relative w-full max-w-md rounded-2xl !bg-none bg-white p-8 text-center text-foreground shadow-2xl"; export function HostedCommunityOnboarding({ onBack }: { onBack: () => void }) { const onboarding = useCommunityOnboarding(); @@ -268,123 +293,52 @@ export function HostedCommunityOnboarding({ onBack }: { onBack: () => void }) { }; const busy = action !== null; + // The account is set up once we're signed in with a linked, matching + // identity. Until then the sign-in / link-identity modal drives the flow and + // the page behind it shows a blurred preview of where communities will land. + const ready = Boolean(auth && identity && !identityMismatch); + const modalOpen = !loading && !ready; + + const errorBox = error ? ( +
+ + {error} +
+ ) : null; return (

Your communities

- Sign in to connect a community you already own on this machine, or - create a new one. + Connect a community you already own on this machine, or create a new + one.

- {error ? ( -
- - {error} -
- ) : null} - {loading ? (
- Checking Builderlab sign-in + Checking sign-in
- ) : !auth ? ( -
-

Sign in or create an account

-

- Builderlab provides Block-hosted Buzz communities. Authentication - opens in your browser and returns here. -

- {action === "Signing in…" ? ( -
-
- - Waiting for your browser… -
- -
- ) : ( - - )} -
- ) : !identity ? ( -
-

Connect this Buzz identity

-

- Link this device’s Buzz key to{" "} - {auth.email ?? auth.name ?? "your Builderlab account"}. Buzz signs - a one-time challenge locally; your private key never leaves - Desktop. -

- -
- ) : identityMismatch ? ( -
-
- -
-

- This account uses a different Buzz identity -

-

- This Builderlab account is connected to another Buzz identity. - You can disconnect that identity and reconnect this device, or - sign out to use a different email. -

-

- Account: {identity.npub ?? boundPubkey} -
- This device: {localNpub ?? localPubkey} -

-
- - -
-
-
-
- ) : ( + ) : ready ? ( <> -
+ {errorBox} +
- Signed in{auth.email ? ` as ${auth.email}` : ""} with this Buzz + Signed in{auth?.email ? ` as ${auth.email}` : ""} with this Buzz identity
{activeCommunities.length > 0 ? ( -
+

Connect to one you own

    {activeCommunities.map((community, index) => (
  • @@ -416,7 +370,7 @@ export function HostedCommunityOnboarding({ onBack }: { onBack: () => void }) {
) : null} -
+

Create a new community

Choose the address your team will use to connect. @@ -462,7 +416,7 @@ export function HostedCommunityOnboarding({ onBack }: { onBack: () => void }) {

) : null}
+ ) : ( + // Blurred behind the sign-in modal: a soft preview so the backdrop + // reads as the destination "Your communities" page. +
+
+
+
)}
- - + + ) : null} + + { + if (!open && !busy) goBack(); + }} + > + event.preventDefault()} + showCloseButton={false} + surface="none" > - Back - - +
+ + + {!auth ? ( + <> + + Sign in to Buzz + + + Sign in to connect a community you already own, or create a + new one. Sign-in opens in your browser and returns here. + + {errorBox ?
{errorBox}
: null} + {action === "Signing in…" ? ( +
+
+ + Waiting for your browser… +
+ +
+ ) : ( + + )} + + ) : !identity ? ( + <> + + Connect this Buzz identity + + + Link this device’s Buzz key to{" "} + {auth.email ?? auth.name ?? "your account"}. Buzz signs a + one-time challenge locally — your private key never leaves + Desktop. + + {errorBox ?
{errorBox}
: null} + + + ) : ( + <> + + This account uses a different Buzz identity + + + This account is connected to another Buzz identity. You can + disconnect that identity and reconnect this device, or sign + out to use a different email. + +

+ Account: {identity.npub ?? boundPubkey} +
+ This device: {localNpub ?? localPubkey} +

+ {errorBox ?
{errorBox}
: null} +
+ + +
+ + )} + +
+ +
+
+
+
); } diff --git a/desktop/tests/e2e/onboarding.spec.ts b/desktop/tests/e2e/onboarding.spec.ts index 6a057c8ecc..b0b230c9d9 100644 --- a/desktop/tests/e2e/onboarding.spec.ts +++ b/desktop/tests/e2e/onboarding.spec.ts @@ -624,12 +624,11 @@ test("first-community choices expose npub and invite input", async ({ await page .getByRole("button", { name: "Create or connect to my own community" }) .click(); + // Sign-in opens as a modal over the (blurred) "Your communities" page. await expect( - page.getByRole("heading", { name: "Your communities" }), - ).toBeVisible(); - await expect( - page.getByRole("button", { name: "Continue with Builderlab" }), + page.getByRole("heading", { name: "Sign in to Buzz" }), ).toBeVisible(); + await expect(page.getByRole("button", { name: "Continue" })).toBeVisible(); await page.getByRole("button", { name: "Back" }).click(); await page.getByRole("button", { name: "Add me to a community" }).click(); @@ -829,13 +828,11 @@ test("first-community owner can create and connect a hosted community", async ({ await page .getByRole("button", { name: "Create or connect to my own community" }) .click(); - await page.getByRole("button", { name: "Continue with Builderlab" }).click(); + await page.getByRole("button", { name: "Continue" }).click(); await expect( page.getByRole("heading", { name: "Connect this Buzz identity" }), ).toBeVisible(); - await page - .getByRole("button", { name: "Connect this Buzz identity" }) - .click(); + await page.getByRole("button", { name: "Continue" }).click(); await expect(page.getByText("Signed in as owner@example.com")).toBeVisible(); await page .getByRole("textbox", { name: "Community address" }) @@ -925,12 +922,10 @@ test("first-community can cancel a closed-browser sign-in and retry", async ({ await page .getByRole("button", { name: "Create or connect to my own community" }) .click(); - await page.getByRole("button", { name: "Continue with Builderlab" }).click(); + await page.getByRole("button", { name: "Continue" }).click(); await expect(page.getByText("Waiting for your browser…")).toBeVisible(); await page.getByRole("button", { name: "Cancel sign-in" }).click(); - await expect( - page.getByRole("button", { name: "Continue with Builderlab" }), - ).toBeVisible(); + await expect(page.getByRole("button", { name: "Continue" })).toBeVisible(); await expect .poll(() => page.evaluate(() => window.__BUZZ_E2E_COMMANDS__ ?? [])) .toEqual(expect.arrayContaining(["cancel_builderlab_login"])); @@ -939,7 +934,7 @@ test("first-community can cancel a closed-browser sign-in and retry", async ({ if (window.__BUZZ_E2E_CONFIG__?.mock) window.__BUZZ_E2E_CONFIG__.mock.builderlabLoginDelayMs = 0; }); - await page.getByRole("button", { name: "Continue with Builderlab" }).click(); + await page.getByRole("button", { name: "Continue" }).click(); await expect( page.getByRole("heading", { name: "Connect this Buzz identity" }), ).toBeVisible(); @@ -1036,7 +1031,7 @@ test("first-community explains when the local identity belongs to another accoun ), ).toBeVisible(); await expect( - page.getByRole("button", { name: "Connect this Buzz identity" }), + page.getByRole("heading", { name: "Connect this Buzz identity" }), ).toBeVisible(); }); @@ -1074,9 +1069,7 @@ test("back clears Builderlab auth before returning to first-community choices", await page .getByRole("button", { name: "Create or connect to my own community" }) .click(); - await expect( - page.getByRole("button", { name: "Continue with Builderlab" }), - ).toBeVisible(); + await expect(page.getByRole("button", { name: "Continue" })).toBeVisible(); }); test("first-community shows the scenario cards for localhost", async ({ From 6ae7a4e8d78a28e1bc78c905659d547536f417e6 Mon Sep 17 00:00:00 2001 From: Bradley Axen Date: Sun, 19 Jul 2026 12:53:54 -0700 Subject: [PATCH 2/9] fix(desktop): compact, branded onboarding sign-in modal Design pass on the sign-in/link-identity modal per feedback that it was "weirdly long". The panel was inheriting min-height:100dvh from the buzz-startup-shell class (added only to keep the light onboarding palette), so it stretched the full viewport with empty space below the content. - Switch the modal to the established branded onboarding-dialog pattern used by IdentityKeyHelpDialog: surface="textured" (the powder card) + buzz-onboarding-neutral-theme, which sizes to content and drops in the olive-ink treatment. No more full-height white box. - Tighten content: smaller title, olive-ink descriptions, single-column actions, the mismatch npub in a translucent bg-white/50 frame (invite-key aesthetic). Dismiss via the themed X (returns to the community picker), dropping the redundant in-panel Back link. - Update the one e2e assertion that clicked the modal Back to use Close. Co-authored-by: Claude Opus 4.8 (1M context) Co-authored-by: Bradley Axen Signed-off-by: Bradley Axen --- .../ui/HostedCommunityOnboarding.tsx | 72 ++++++++----------- desktop/tests/e2e/onboarding.spec.ts | 3 +- 2 files changed, 32 insertions(+), 43 deletions(-) diff --git a/desktop/src/features/communities/ui/HostedCommunityOnboarding.tsx b/desktop/src/features/communities/ui/HostedCommunityOnboarding.tsx index 420c860df3..a47981d820 100644 --- a/desktop/src/features/communities/ui/HostedCommunityOnboarding.tsx +++ b/desktop/src/features/communities/ui/HostedCommunityOnboarding.tsx @@ -26,7 +26,10 @@ import { safeNpub } from "@/shared/lib/nostrUtils"; import { Button } from "@/shared/ui/button"; import { Input } from "@/shared/ui/input"; import { OnboardingFooter } from "@/features/onboarding/ui/OnboardingFooter"; -import { ONBOARDING_PRIMARY_CTA_CLASS } from "@/features/onboarding/ui/OnboardingChrome"; +import { + ONBOARDING_INK_ICON_CLASS, + ONBOARDING_PRIMARY_CTA_CLASS, +} from "@/features/onboarding/ui/OnboardingChrome"; import { BuzzMark } from "@/shared/ui/buzz-logo/BuzzMark"; import { Dialog, @@ -43,15 +46,6 @@ import { */ const BRAND_SECTION_CLASS = "rounded-xl bg-white/55 p-6 text-left"; -/** - * The sign-in and identity-linking steps live in a modal that opens over the - * "Your communities" page (which blurs behind the Radix overlay). It composes - * its own panel (`surface="none"`) so it can re-establish the always-light - * onboarding theme inside the portal, matching the shell behind it. - */ -const MODAL_PANEL_CLASS = - "buzz-onboarding-neutral-theme buzz-startup-shell relative w-full max-w-md rounded-2xl !bg-none bg-white p-8 text-center text-foreground shadow-2xl"; - export function HostedCommunityOnboarding({ onBack }: { onBack: () => void }) { const onboarding = useCommunityOnboarding(); const localPubkey = useIdentityQuery().data?.pubkey ?? null; @@ -466,24 +460,26 @@ export function HostedCommunityOnboarding({ onBack }: { onBack: () => void }) { }} > event.preventDefault()} - showCloseButton={false} - surface="none" + surface="textured" > -
- +
+ {!auth ? ( <> - + Sign in to Buzz - - Sign in to connect a community you already own, or create a - new one. Sign-in opens in your browser and returns here. + + Connect a community you already own, or create a new one. + Sign-in opens in your browser and returns here. - {errorBox ?
{errorBox}
: null} + {errorBox ? ( +
{errorBox}
+ ) : null} {action === "Signing in…" ? (
@@ -505,16 +501,18 @@ export function HostedCommunityOnboarding({ onBack }: { onBack: () => void }) { ) : !identity ? ( <> - + Connect this Buzz identity - + Link this device’s Buzz key to{" "} {auth.email ?? auth.name ?? "your account"}. Buzz signs a one-time challenge locally — your private key never leaves Desktop. - {errorBox ?
{errorBox}
: null} + {errorBox ? ( +
{errorBox}
+ ) : null}
)} - -
- -
diff --git a/desktop/tests/e2e/onboarding.spec.ts b/desktop/tests/e2e/onboarding.spec.ts index b0b230c9d9..02e6029139 100644 --- a/desktop/tests/e2e/onboarding.spec.ts +++ b/desktop/tests/e2e/onboarding.spec.ts @@ -629,7 +629,8 @@ test("first-community choices expose npub and invite input", async ({ page.getByRole("heading", { name: "Sign in to Buzz" }), ).toBeVisible(); await expect(page.getByRole("button", { name: "Continue" })).toBeVisible(); - await page.getByRole("button", { name: "Back" }).click(); + // The modal's close (X) dismisses back to the community picker. + await page.getByRole("button", { name: "Close" }).click(); await page.getByRole("button", { name: "Add me to a community" }).click(); await expect(page.getByTestId("welcome-join-npub")).toHaveText( From b6e987180b160bae3ee26b66fc64d2f66fbd0a1c Mon Sep 17 00:00:00 2001 From: Bradley Axen Date: Sun, 19 Jul 2026 16:36:41 -0700 Subject: [PATCH 3/9] fix(desktop): stronger contrast, Builderlab breadcrumb, error state in onboarding modal - Bee mark, titles, and CTAs now paint full-strength foreground instead of the washed pale-blue label / thin weight that read as out of place. - Add a quiet FOSS-vs-hosted-account breadcrumb to the sign-in modal. - Give the error box a stronger treatment (filled chip + medium ink). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../ui/HostedCommunityOnboarding.tsx | 45 ++++++++++++------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/desktop/src/features/communities/ui/HostedCommunityOnboarding.tsx b/desktop/src/features/communities/ui/HostedCommunityOnboarding.tsx index a47981d820..ff6a3e9e4a 100644 --- a/desktop/src/features/communities/ui/HostedCommunityOnboarding.tsx +++ b/desktop/src/features/communities/ui/HostedCommunityOnboarding.tsx @@ -26,10 +26,7 @@ import { safeNpub } from "@/shared/lib/nostrUtils"; import { Button } from "@/shared/ui/button"; import { Input } from "@/shared/ui/input"; import { OnboardingFooter } from "@/features/onboarding/ui/OnboardingFooter"; -import { - ONBOARDING_INK_ICON_CLASS, - ONBOARDING_PRIMARY_CTA_CLASS, -} from "@/features/onboarding/ui/OnboardingChrome"; +import { ONBOARDING_INK_ICON_CLASS } from "@/features/onboarding/ui/OnboardingChrome"; import { BuzzMark } from "@/shared/ui/buzz-logo/BuzzMark"; import { Dialog, @@ -46,6 +43,14 @@ import { */ const BRAND_SECTION_CLASS = "rounded-xl bg-white/55 p-6 text-left"; +/** + * Primary-CTA styling for this onboarding surface: the shared onboarding pill + * shape, but with the pill's crisp near-white `primary-foreground` label (the + * Button default) instead of the shared pale-blue onboarding CTA label + * (`--buzz-onboarding-cta-label`), which reads washed-out on the textured card. + */ +const MODAL_CTA_CLASS = "h-[2.375rem] rounded-full px-6"; + export function HostedCommunityOnboarding({ onBack }: { onBack: () => void }) { const onboarding = useCommunityOnboarding(); const localPubkey = useIdentityQuery().data?.pubkey ?? null; @@ -295,11 +300,15 @@ export function HostedCommunityOnboarding({ onBack }: { onBack: () => void }) { const errorBox = error ? (
- - {error} + + + + + {error} +
) : null; @@ -410,7 +419,7 @@ export function HostedCommunityOnboarding({ onBack }: { onBack: () => void }) {

) : null} )} + {/* Quiet breadcrumb: Buzz itself is open source; this hosted + relay is the one account-backed piece of the flow. */} +

+ Buzz is open source. Builderlab hosts the relay for this + account. +

) : !identity ? ( <> - + Connect this Buzz identity @@ -514,7 +529,7 @@ export function HostedCommunityOnboarding({ onBack }: { onBack: () => void }) {
{errorBox}
) : null} - - ))} - -
- ) : null} - -
-

Create a new community

-

- Choose the address your team will use to connect. -

-
- { - setName(event.target.value.toLowerCase()); - setAvailability(null); - }} - placeholder="north-star" - spellCheck={false} - value={name} - /> - - .{HOSTED_COMMUNITY_SUFFIX} - -
- {atCommunityLimit ? ( -

- You’ve reached the limit of {HOSTED_COMMUNITY_LIMIT} hosted - communities. -

- ) : name && !validName ? ( -

- Use lowercase letters, numbers, and single hyphens. -

- ) : checkingName ? ( -

- Checking availability… -

- ) : availability === false ? ( -

- That address is already taken. + {hasCommunities ? ( + <> + +

+

+ Your communities +

+
    + {activeCommunities.map((community, index) => ( +
  • +
    +

    + {community.name ?? + community.slug ?? + "Hosted community"} +

    +

    + {community.normalized_host} +

    +
    + +
  • + ))} +
+ + {!showCreate ? ( + +

+ Want to create a new community? +

+ +
+ ) : ( + + {renderCreationForm(true)} + + )} +
+
+ +

+ {creationFeedback ?? "Community address status"}

- ) : availability === true ? ( -

- That address is available. + + ) : ( + <> + {renderCreationForm(false)} +

+ {creationFeedback ?? "Community address status"}

- ) : null} - -
+ + )} ) : ( - // Blurred behind the sign-in modal: a soft preview so the backdrop - // reads as the destination "Your communities" page. -
-
-
-
+ )}
- {/* The modal carries its own Back control; only show the docked footer - Back when the page is the interactive surface (no modal on top). */} {!modalOpen ? ( + -
+ ) : ( )} {/* Quiet breadcrumb: Buzz itself is open source; this hosted relay is the one account-backed piece of the flow. */} -

+

Buzz is open source. Builderlab hosts the relay for this account.

@@ -517,26 +658,26 @@ export function HostedCommunityOnboarding({ onBack }: { onBack: () => void }) { ) : !identity ? ( <> - Connect this Buzz identity + Finish connecting Buzz - - Link this device’s Buzz key to{" "} - {auth.email ?? auth.name ?? "your account"}. Buzz signs a - one-time challenge locally — your private key never leaves - Desktop. + + Your Builderlab account + {auth.email ? ` (${auth.email})` : ""} is ready. Connect this + device’s Buzz identity to finish setup. Your private key stays + on this device. {errorBox ? (
{errorBox}
) : null} ) : ( @@ -544,11 +685,11 @@ export function HostedCommunityOnboarding({ onBack }: { onBack: () => void }) { This account uses a different Buzz identity - + This account is connected to another Buzz identity. Reconnect this device, or sign out to use a different email. -

+

Account: {identity.npub ?? boundPubkey}
This device: {localNpub ?? localPubkey} @@ -558,17 +699,17 @@ export function HostedCommunityOnboarding({ onBack }: { onBack: () => void }) { ) : null}

diff --git a/desktop/src/shared/styles/globals/theme.css b/desktop/src/shared/styles/globals/theme.css index 9add81c03f..634dc78ffa 100644 --- a/desktop/src/shared/styles/globals/theme.css +++ b/desktop/src/shared/styles/globals/theme.css @@ -53,6 +53,16 @@ --buzz-gradient-dark-top: #4a4616; --buzz-gradient-dark-bottom: #0a1423; --buzz-content-dark: 0 0% 10.1960784314%; + /* Hosted-community onboarding intentionally keeps this light palette in + both app themes; semantic tokens keep component code color-agnostic. */ + --buzz-hosted-community-surface-fg: 23 23 23; + --buzz-hosted-community-divider-border: 150 150 10; + --buzz-hosted-community-action-bg: 240 240 205; + --buzz-hosted-community-action-bg-hover: 232 232 188; + --buzz-hosted-community-input-bg: 255 255 255; + --buzz-hosted-community-modal-action-fg: 255 255 255; + --buzz-hosted-community-modal-overlay-bg: 0 0 0; + --buzz-hosted-community-identity-bg: 255 255 255; } .dark { diff --git a/desktop/tests/e2e/onboarding.spec.ts b/desktop/tests/e2e/onboarding.spec.ts index 02e6029139..13cc48cf5c 100644 --- a/desktop/tests/e2e/onboarding.spec.ts +++ b/desktop/tests/e2e/onboarding.spec.ts @@ -624,11 +624,13 @@ test("first-community choices expose npub and invite input", async ({ await page .getByRole("button", { name: "Create or connect to my own community" }) .click(); - // Sign-in opens as a modal over the (blurred) "Your communities" page. + // Sign-in opens as a modal over the blurred hosted-community preview. await expect( - page.getByRole("heading", { name: "Sign in to Buzz" }), + page.getByRole("heading", { name: "Set up your community" }), + ).toBeVisible(); + await expect( + page.getByRole("button", { name: "Sign in to continue" }), ).toBeVisible(); - await expect(page.getByRole("button", { name: "Continue" })).toBeVisible(); // The modal's close (X) dismisses back to the community picker. await page.getByRole("button", { name: "Close" }).click(); @@ -803,6 +805,21 @@ test("first-community owner can connect an existing hosted community", async ({ ), ) .toContain("wss://north-star.communities.buzz.xyz"); + await page.getByTestId("community-profile-back").click(); + await expect( + page.getByRole("heading", { name: "Choose a community" }), + ).toBeVisible(); + await expect(page.getByText("North Star")).toBeVisible(); + await expect( + page.getByRole("heading", { name: "Request access to community" }), + ).toHaveCount(0); + await expect + .poll(() => + page.evaluate(() => + window.localStorage.getItem("buzz-community-onboarding-transaction.v1"), + ), + ) + .toBeNull(); }); test("first-community owner can create and connect a hosted community", async ({ @@ -829,17 +846,44 @@ test("first-community owner can create and connect a hosted community", async ({ await page .getByRole("button", { name: "Create or connect to my own community" }) .click(); - await page.getByRole("button", { name: "Continue" }).click(); + await page.getByRole("button", { name: "Sign in to continue" }).click(); await expect( - page.getByRole("heading", { name: "Connect this Buzz identity" }), + page.getByRole("heading", { name: "Finish connecting Buzz" }), ).toBeVisible(); - await page.getByRole("button", { name: "Continue" }).click(); - await expect(page.getByText("Signed in as owner@example.com")).toBeVisible(); - await page - .getByRole("textbox", { name: "Community address" }) - .fill("bee-lab"); - await expect(page.getByText("That address is available.")).toBeVisible(); - await page.getByRole("button", { name: "Create and connect" }).click(); + await page.getByRole("button", { name: "Connect and continue" }).click(); + const createSurface = page.getByTestId("hosted-community-create-surface"); + const surfaceBoxBeforeFeedback = await createSurface.boundingBox(); + await page.getByRole("textbox", { name: "Community name" }).fill("bee-lab"); + const availabilityFeedback = page.getByText("That address is available."); + await expect(availabilityFeedback).toBeVisible(); + const [feedbackBox, surfaceBox, inputBox, suffixBox] = await Promise.all([ + availabilityFeedback.boundingBox(), + createSurface.boundingBox(), + page.getByTestId("hosted-community-address-input").boundingBox(), + page.locator("#hosted-community-suffix").boundingBox(), + ]); + if ( + !surfaceBoxBeforeFeedback || + !feedbackBox || + !surfaceBox || + !inputBox || + !suffixBox + ) { + throw new Error("Could not measure hosted community creation layout"); + } + expect(surfaceBox.y).toBe(surfaceBoxBeforeFeedback.y); + expect(surfaceBox.height).toBe(surfaceBoxBeforeFeedback.height); + const addressLeft = inputBox.x; + const addressRight = suffixBox.x + suffixBox.width; + expect( + Math.abs( + (addressLeft + addressRight) / 2 - (surfaceBox.x + surfaceBox.width / 2), + ), + ).toBeLessThanOrEqual(1); + expect(feedbackBox.y).toBeGreaterThanOrEqual( + surfaceBox.y + surfaceBox.height, + ); + await page.getByRole("button", { name: "Next" }).click(); await expect( page.getByRole("heading", { name: "Build your profile" }), ).toBeVisible(); @@ -886,11 +930,9 @@ test("first-community reports a created community without a relay address", asyn await page .getByRole("button", { name: "Create or connect to my own community" }) .click(); - await page - .getByRole("textbox", { name: "Community address" }) - .fill("bee-lab"); + await page.getByRole("textbox", { name: "Community name" }).fill("bee-lab"); await expect(page.getByText("That address is available.")).toBeVisible(); - await page.getByRole("button", { name: "Create and connect" }).click(); + await page.getByRole("button", { name: "Next" }).click(); await expect(page.getByRole("alert")).toContainText( "The community was created, but Builderlab did not return its relay address.", ); @@ -899,9 +941,7 @@ test("first-community reports a created community without a relay address", asyn ).toHaveCount(0); }); -test("first-community can cancel a closed-browser sign-in and retry", async ({ - page, -}) => { +test("first-community X cancels a pending sign-in", async ({ page }) => { await seedActiveIdentity(page, BLANK_TYLER_IDENTITY); await page.addInitScript((pubkey) => { window.localStorage.setItem( @@ -923,22 +963,18 @@ test("first-community can cancel a closed-browser sign-in and retry", async ({ await page .getByRole("button", { name: "Create or connect to my own community" }) .click(); - await page.getByRole("button", { name: "Continue" }).click(); + await page.getByRole("button", { name: "Sign in to continue" }).click(); await expect(page.getByText("Waiting for your browser…")).toBeVisible(); - await page.getByRole("button", { name: "Cancel sign-in" }).click(); - await expect(page.getByRole("button", { name: "Continue" })).toBeVisible(); + await expect( + page.getByRole("button", { name: "Cancel sign-in" }), + ).toHaveCount(0); + await page.getByRole("button", { name: "Close" }).click(); + await expect( + page.getByRole("button", { name: "Create or connect to my own community" }), + ).toBeVisible(); await expect .poll(() => page.evaluate(() => window.__BUZZ_E2E_COMMANDS__ ?? [])) .toEqual(expect.arrayContaining(["cancel_builderlab_login"])); - - await page.evaluate(() => { - if (window.__BUZZ_E2E_CONFIG__?.mock) - window.__BUZZ_E2E_CONFIG__.mock.builderlabLoginDelayMs = 0; - }); - await page.getByRole("button", { name: "Continue" }).click(); - await expect( - page.getByRole("heading", { name: "Connect this Buzz identity" }), - ).toBeVisible(); }); test("first-community owner can replace a mismatched account identity", async ({ From 088951337adffe7e6a006048f80589cf985191eb Mon Sep 17 00:00:00 2001 From: npub19x6jnl6rhepymwyl2xlltz3ce7rfg2ktllle3g2vu59n3s490k8s9n40l3 <29b529ff43be424db89f51bff58a38cf86942acbffff98a14ce50b38c2a57d8f@sprout-oss.stage.blox.sqprod.co> Date: Sun, 19 Jul 2026 21:08:56 -0700 Subject: [PATCH 7/9] fix(desktop): center community URL while typing Co-authored-by: npub19x6jnl6rhepymwyl2xlltz3ce7rfg2ktllle3g2vu59n3s490k8s9n40l3 <29b529ff43be424db89f51bff58a38cf86942acbffff98a14ce50b38c2a57d8f@sprout-oss.stage.blox.sqprod.co> Signed-off-by: npub19x6jnl6rhepymwyl2xlltz3ce7rfg2ktllle3g2vu59n3s490k8s9n40l3 <29b529ff43be424db89f51bff58a38cf86942acbffff98a14ce50b38c2a57d8f@sprout-oss.stage.blox.sqprod.co> --- .../src/features/communities/ui/HostedCommunityOnboarding.tsx | 2 +- desktop/tests/e2e/onboarding.spec.ts | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/desktop/src/features/communities/ui/HostedCommunityOnboarding.tsx b/desktop/src/features/communities/ui/HostedCommunityOnboarding.tsx index 4b5675a995..a9d047411b 100644 --- a/desktop/src/features/communities/ui/HostedCommunityOnboarding.tsx +++ b/desktop/src/features/communities/ui/HostedCommunityOnboarding.tsx @@ -361,7 +361,7 @@ export function HostedCommunityOnboarding({ onBack }: { onBack: () => void }) { style={ inline ? undefined - : { width: `${Math.max(name.length, "your-community".length)}ch` } + : { width: `${name ? name.length : "your-community".length}ch` } } value={name} /> diff --git a/desktop/tests/e2e/onboarding.spec.ts b/desktop/tests/e2e/onboarding.spec.ts index 13cc48cf5c..bbb53b695e 100644 --- a/desktop/tests/e2e/onboarding.spec.ts +++ b/desktop/tests/e2e/onboarding.spec.ts @@ -853,7 +853,9 @@ test("first-community owner can create and connect a hosted community", async ({ await page.getByRole("button", { name: "Connect and continue" }).click(); const createSurface = page.getByTestId("hosted-community-create-surface"); const surfaceBoxBeforeFeedback = await createSurface.boundingBox(); - await page.getByRole("textbox", { name: "Community name" }).fill("bee-lab"); + const communityNameInput = page.getByTestId("hosted-community-address-input"); + await communityNameInput.fill("bee-lab"); + await expect(communityNameInput).toHaveAttribute("style", "width: 7ch;"); const availabilityFeedback = page.getByText("That address is available."); await expect(availabilityFeedback).toBeVisible(); const [feedbackBox, surfaceBox, inputBox, suffixBox] = await Promise.all([ From 56bda7931a7198be71b507d0fb5843d6fd5a29fc Mon Sep 17 00:00:00 2001 From: npub19x6jnl6rhepymwyl2xlltz3ce7rfg2ktllle3g2vu59n3s490k8s9n40l3 <29b529ff43be424db89f51bff58a38cf86942acbffff98a14ce50b38c2a57d8f@sprout-oss.stage.blox.sqprod.co> Date: Sun, 19 Jul 2026 21:12:37 -0700 Subject: [PATCH 8/9] test(desktop): update hosted identity assertions Co-authored-by: npub19x6jnl6rhepymwyl2xlltz3ce7rfg2ktllle3g2vu59n3s490k8s9n40l3 <29b529ff43be424db89f51bff58a38cf86942acbffff98a14ce50b38c2a57d8f@sprout-oss.stage.blox.sqprod.co> Signed-off-by: npub19x6jnl6rhepymwyl2xlltz3ce7rfg2ktllle3g2vu59n3s490k8s9n40l3 <29b529ff43be424db89f51bff58a38cf86942acbffff98a14ce50b38c2a57d8f@sprout-oss.stage.blox.sqprod.co> --- desktop/tests/e2e/onboarding.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/desktop/tests/e2e/onboarding.spec.ts b/desktop/tests/e2e/onboarding.spec.ts index bbb53b695e..9cb4229309 100644 --- a/desktop/tests/e2e/onboarding.spec.ts +++ b/desktop/tests/e2e/onboarding.spec.ts @@ -1018,7 +1018,7 @@ test("first-community owner can replace a mismatched account identity", async ({ .getByRole("button", { name: "Use this device's identity" }) .click(); await expect( - page.getByText("Signed in as old-owner@example.com"), + page.getByRole("textbox", { name: "Community name" }), ).toBeVisible(); await expect .poll(() => page.evaluate(() => window.__BUZZ_E2E_COMMANDS__ ?? [])) @@ -1070,7 +1070,7 @@ test("first-community explains when the local identity belongs to another accoun ), ).toBeVisible(); await expect( - page.getByRole("heading", { name: "Connect this Buzz identity" }), + page.getByRole("heading", { name: "Finish connecting Buzz" }), ).toBeVisible(); }); From 9d41f5c5b0eb47434c705880f68b93c08d2e2e8e Mon Sep 17 00:00:00 2001 From: Bradley Axen Date: Mon, 20 Jul 2026 09:54:23 -0700 Subject: [PATCH 9/9] fix(desktop): scale hosted community address line to fit the card MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wes flagged a responsive regression: the composed . line renders at a fixed text-4xl with an unbounded name.length-ch input inside a whitespace-nowrap row, so a valid long name overflows the card and window. A name can be up to 63 chars and the suffix (.communities.buzz.xyz) adds another 21 — well past the 800px app minimum (63 monospace 4xl glyphs alone are >1,300px). - Scale the address font-size to the container via a container-query width (min(2.25rem, calc(90cqw / (chars * 0.62)))) on both the input and the suffix, capped at text-4xl so short names keep the full-size treatment and long names shrink to fit while staying centered. Uses an inline rem-capped font-size (zoom-safe, and outside the px-text guard's class/CSS scanning). - Add an e2e assertion at the 800px minimum width with a max-length (63 char) valid name asserting the line stays within the card; the prior 7-char check could not catch this. Verified it fails on the pre-fix build. Co-authored-by: Claude Opus 4.8 (1M context) Co-authored-by: Bradley Axen Signed-off-by: Bradley Axen --- .../ui/HostedCommunityOnboarding.tsx | 27 +++++++- desktop/tests/e2e/onboarding.spec.ts | 65 ++++++++++++++++++- 2 files changed, 88 insertions(+), 4 deletions(-) diff --git a/desktop/src/features/communities/ui/HostedCommunityOnboarding.tsx b/desktop/src/features/communities/ui/HostedCommunityOnboarding.tsx index a9d047411b..cc4c263859 100644 --- a/desktop/src/features/communities/ui/HostedCommunityOnboarding.tsx +++ b/desktop/src/features/communities/ui/HostedCommunityOnboarding.tsx @@ -336,6 +336,22 @@ export function HostedCommunityOnboarding({ onBack }: { onBack: () => void }) { ? "That address is available." : null; + // The composed `.` line renders at text-4xl, but a valid name + // can be up to 63 chars and the suffix adds another 21 — far wider than the + // card at the 800px app minimum. Scale the font down to fit the container + // (container-query width) while capping at text-4xl (2.25rem) so short names + // keep the full-size treatment and long names stay fully visible instead of + // overflowing the surface. + const composedAddressLength = + (name ? name.length : "your-community".length) + + HOSTED_COMMUNITY_SUFFIX.length + + 1; // leading dot before the suffix + // ~0.62em is the monospace glyph advance; 90cqw leaves a safety margin so the + // glyphs never touch the card edge. + const addressFontSize = `min(2.25rem, calc(90cqw / ${( + composedAddressLength * 0.62 + ).toFixed(2)}))`; + const creationInput = (inline: boolean) => ( void }) { className={ inline ? "h-[2.375rem] w-[16.5rem] rounded-full border border-[color:var(--buzz-onboarding-backup-ink)]/25 bg-[rgb(var(--buzz-hosted-community-input-bg)/0.6)] px-6 text-center text-sm shadow-none placeholder:text-foreground/30 focus-visible:ring-1 focus-visible:ring-[color:var(--buzz-onboarding-backup-ink)]/40" - : "h-auto min-w-0 flex-none rounded-none border-0 bg-transparent p-0 text-right font-mono !text-4xl !text-[rgb(var(--buzz-hosted-community-surface-fg))] shadow-none placeholder:!text-[rgb(var(--buzz-hosted-community-surface-fg))] placeholder:opacity-20 focus-visible:ring-0" + : "h-auto min-w-0 flex-none rounded-none border-0 bg-transparent p-0 text-right font-mono !text-[rgb(var(--buzz-hosted-community-surface-fg))] shadow-none placeholder:!text-[rgb(var(--buzz-hosted-community-surface-fg))] placeholder:opacity-20 focus-visible:ring-0" } disabled={busy || atCommunityLimit} id="hosted-community-address" @@ -361,7 +377,10 @@ export function HostedCommunityOnboarding({ onBack }: { onBack: () => void }) { style={ inline ? undefined - : { width: `${name ? name.length : "your-community".length}ch` } + : { + width: `${name ? name.length : "your-community".length}ch`, + fontSize: addressFontSize, + } } value={name} /> @@ -392,11 +411,13 @@ export function HostedCommunityOnboarding({ onBack }: { onBack: () => void }) {
{creationInput(false)} .{HOSTED_COMMUNITY_SUFFIX} diff --git a/desktop/tests/e2e/onboarding.spec.ts b/desktop/tests/e2e/onboarding.spec.ts index 9cb4229309..34440fd507 100644 --- a/desktop/tests/e2e/onboarding.spec.ts +++ b/desktop/tests/e2e/onboarding.spec.ts @@ -855,7 +855,7 @@ test("first-community owner can create and connect a hosted community", async ({ const surfaceBoxBeforeFeedback = await createSurface.boundingBox(); const communityNameInput = page.getByTestId("hosted-community-address-input"); await communityNameInput.fill("bee-lab"); - await expect(communityNameInput).toHaveAttribute("style", "width: 7ch;"); + await expect(communityNameInput).toHaveAttribute("style", /width: 7ch;/); const availabilityFeedback = page.getByText("That address is available."); await expect(availabilityFeedback).toBeVisible(); const [feedbackBox, surfaceBox, inputBox, suffixBox] = await Promise.all([ @@ -898,6 +898,69 @@ test("first-community owner can create and connect a hosted community", async ({ .toContain("wss://bee-lab.communities.buzz.xyz"); }); +test("hosted community address line stays within the card for a long name", async ({ + page, +}) => { + await seedActiveIdentity(page, BLANK_TYLER_IDENTITY); + await page.addInitScript((pubkey) => { + window.localStorage.setItem( + `buzz-machine-onboarding-complete.v2:${pubkey}`, + "true", + ); + }, BLANK_TYLER_IDENTITY.pubkey); + await installMockBridge( + page, + {}, + { + relayWsUrl: "wss://default.example.com", + skipOnboardingSeed: true, + skipCommunitySeed: true, + }, + ); + // The 800px app minimum is the worst case for the full-width address line. + await page.setViewportSize({ width: 800, height: 720 }); + await page.goto("/"); + + await page + .getByRole("button", { name: "Create or connect to my own community" }) + .click(); + await page.getByRole("button", { name: "Sign in to continue" }).click(); + await expect( + page.getByRole("heading", { name: "Finish connecting Buzz" }), + ).toBeVisible(); + await page.getByRole("button", { name: "Connect and continue" }).click(); + + const createSurface = page.getByTestId("hosted-community-create-surface"); + const communityNameInput = page.getByTestId("hosted-community-address-input"); + // A maximum-length (63 char) valid name — the overflow case Wes flagged; the + // 7-char check above cannot catch it. + const longName = "a".repeat(63); + await communityNameInput.fill(longName); + await expect(communityNameInput).toHaveValue(longName); + + const [surfaceBox, inputBox, suffixBox] = await Promise.all([ + createSurface.boundingBox(), + communityNameInput.boundingBox(), + page.locator("#hosted-community-suffix").boundingBox(), + ]); + if (!surfaceBox || !inputBox || !suffixBox) { + throw new Error("Could not measure hosted community creation layout"); + } + const addressLeft = inputBox.x; + const addressRight = suffixBox.x + suffixBox.width; + // The composed `.` line must stay within the card — no + // horizontal overflow past the surface or the 800px window. + expect(addressLeft).toBeGreaterThanOrEqual(surfaceBox.x); + expect(addressRight).toBeLessThanOrEqual(surfaceBox.x + surfaceBox.width); + expect(addressRight).toBeLessThanOrEqual(800); + // …and it stays centered within the card. + expect( + Math.abs( + (addressLeft + addressRight) / 2 - (surfaceBox.x + surfaceBox.width / 2), + ), + ).toBeLessThanOrEqual(2); +}); + test("first-community reports a created community without a relay address", async ({ page, }) => {