mirror of
https://github.com/block/buzz.git
synced 2026-10-27 12:40:30 +00:00
feat(desktop): add fuzzy onboarding cards (#2141)
Signed-off-by: npub19x6jnl6rhepymwyl2xlltz3ce7rfg2ktllle3g2vu59n3s490k8s9n40l3 <29b529ff43be424db89f51bff58a38cf86942acbffff98a14ce50b38c2a57d8f@sprout-oss.stage.blox.sqprod.co> Co-authored-by: npub19x6jnl6rhepymwyl2xlltz3ce7rfg2ktllle3g2vu59n3s490k8s9n40l3 <29b529ff43be424db89f51bff58a38cf86942acbffff98a14ce50b38c2a57d8f@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
@@ -5,7 +5,6 @@ import { useCommunityOnboarding } from "@/features/onboarding/communityOnboardin
|
||||
import { normalizeRelayUrl } from "@/features/communities/relayProbe";
|
||||
import { InviteRedeemForm } from "@/features/onboarding/ui/InviteRedeemForm";
|
||||
import {
|
||||
ONBOARDING_KEY_FRAME_CLASS,
|
||||
ONBOARDING_KEY_ROW_CLASS,
|
||||
ONBOARDING_KEY_TEXT_CLASS,
|
||||
} from "@/features/onboarding/ui/NsecMaskedDisplay";
|
||||
@@ -20,10 +19,14 @@ import {
|
||||
import { getIdentity } from "@/shared/api/tauriIdentity";
|
||||
import { pubkeyToNpub } from "@/shared/lib/nostrUtils";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { Card } from "@/shared/ui/card";
|
||||
import { Input } from "@/shared/ui/input";
|
||||
import { StartupWindowDragRegion } from "@/shared/ui/StartupWindowDragRegion";
|
||||
import { useSystemColorScheme } from "@/shared/theme/useSystemColorScheme";
|
||||
import { OnboardingChrome } from "@/features/onboarding/ui/OnboardingChrome";
|
||||
import {
|
||||
ONBOARDING_PRIMARY_CTA_CLASS,
|
||||
OnboardingChrome,
|
||||
} from "@/features/onboarding/ui/OnboardingChrome";
|
||||
import { HostedCommunityOnboarding } from "@/features/communities/ui/HostedCommunityOnboarding";
|
||||
import { writeTextToClipboard } from "@/shared/lib/clipboard";
|
||||
|
||||
@@ -37,7 +40,16 @@ type WelcomeSetupProps = {
|
||||
};
|
||||
|
||||
const COMMUNITY_OPTION_CARD_CLASS =
|
||||
"flex min-h-24 w-full max-w-[352px] items-center justify-center rounded-xl bg-white/75 px-6 py-4 text-center text-sm font-normal leading-6 text-foreground transition-colors duration-150 ease-out hover:bg-white/85 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-foreground/35";
|
||||
"w-full max-w-[320px] items-center px-6 py-4 text-center text-sm font-normal leading-6 text-foreground [--buzz-card-textured-min-height:88px] transition-[filter] duration-150 ease-out hover:brightness-[0.98] focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-foreground/35";
|
||||
const WIDE_TEXTURE_CARD_CLASS =
|
||||
"relative left-1/2 w-[min(calc(100%+10rem),calc(100vw-2rem))] max-w-[1040px] -translate-x-1/2 px-8 py-6 [--buzz-card-textured-min-height:192px]";
|
||||
const WIDE_TEXTURE_CONTENT_CLASS = "mx-auto w-full max-w-[840px]";
|
||||
const HORIZONTAL_INPUT_OVERFLOW_FADE = {
|
||||
WebkitMaskImage:
|
||||
"linear-gradient(to right, transparent, black 2rem, black calc(100% - 2rem), transparent)",
|
||||
maskImage:
|
||||
"linear-gradient(to right, transparent, black 2rem, black calc(100% - 2rem), transparent)",
|
||||
};
|
||||
|
||||
export function WelcomeSetup({
|
||||
initialPage = "welcome",
|
||||
@@ -111,16 +123,17 @@ export function WelcomeSetup({
|
||||
|
||||
return (
|
||||
<div
|
||||
className="buzz-onboarding-neutral-theme buzz-startup-shell flex min-h-dvh items-start justify-center overflow-y-auto bg-background px-4 pb-36 pt-[106px] text-foreground"
|
||||
className="buzz-onboarding-neutral-theme buzz-startup-shell flex h-dvh items-start justify-center overflow-y-auto bg-background px-4 pb-36 pt-[106px] text-foreground"
|
||||
data-system-color-scheme={systemColorScheme}
|
||||
>
|
||||
<StartupWindowDragRegion />
|
||||
<OnboardingChrome current={5} />
|
||||
<OnboardingFooterProvider>
|
||||
<div className="relative flex w-full max-w-4xl flex-col items-center text-center">
|
||||
<div className="relative flex min-h-0 w-full max-w-[920px] flex-1 flex-col items-center text-center">
|
||||
{page === "welcome" ? (
|
||||
<OnboardingSlideTransition
|
||||
className="flex w-full flex-col items-center text-center"
|
||||
className="flex h-full min-h-0 w-full flex-col items-center text-center"
|
||||
containerClassName="h-full min-h-0 [&>.buzz-onboarding-transition-line]:h-full"
|
||||
direction={transitionDirection}
|
||||
effect={welcomeEffect}
|
||||
transitionKey={`welcome-${welcomeEffect}-${transitionDirection}`}
|
||||
@@ -134,30 +147,36 @@ export function WelcomeSetup({
|
||||
link, you can open it directly to continue setup.
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-28 flex w-full flex-col items-center gap-6">
|
||||
<button
|
||||
<div className="flex w-full flex-1 translate-y-16 flex-col items-center justify-center gap-20 py-8">
|
||||
<Card
|
||||
asChild
|
||||
className={COMMUNITY_OPTION_CARD_CLASS}
|
||||
onClick={() => showPage("join")}
|
||||
type="button"
|
||||
variant="textured"
|
||||
>
|
||||
Add me to a community
|
||||
</button>
|
||||
<button
|
||||
<button onClick={() => showPage("join")} type="button">
|
||||
Add me to a community
|
||||
</button>
|
||||
</Card>
|
||||
<Card
|
||||
asChild
|
||||
className={COMMUNITY_OPTION_CARD_CLASS}
|
||||
onClick={() => showPage("invite")}
|
||||
type="button"
|
||||
variant="textured"
|
||||
>
|
||||
I have an invite link
|
||||
</button>
|
||||
<button
|
||||
<button onClick={() => showPage("invite")} type="button">
|
||||
I have an invite link
|
||||
</button>
|
||||
</Card>
|
||||
<Card
|
||||
asChild
|
||||
className={COMMUNITY_OPTION_CARD_CLASS}
|
||||
onClick={() => showPage("owned")}
|
||||
type="button"
|
||||
variant="textured"
|
||||
>
|
||||
<span className="max-w-52">
|
||||
Create or connect to my own community
|
||||
</span>
|
||||
</button>
|
||||
<button onClick={() => showPage("owned")} type="button">
|
||||
<span className="max-w-52">
|
||||
Create or connect to my own community
|
||||
</span>
|
||||
</button>
|
||||
</Card>
|
||||
</div>
|
||||
<OnboardingFooter>
|
||||
<Button
|
||||
@@ -192,58 +211,61 @@ export function WelcomeSetup({
|
||||
>
|
||||
Request access to a community
|
||||
</h1>
|
||||
<p className="mx-auto mt-3 max-w-[430px] text-sm leading-6">
|
||||
Ask the community host to send you an invite link or add you
|
||||
directly using your public key.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex w-full flex-1 items-center justify-center pb-2 pt-6">
|
||||
<div className="w-full max-w-4xl space-y-16">
|
||||
<section aria-labelledby="welcome-join-key-step">
|
||||
<div className="flex w-full flex-1 items-center justify-center">
|
||||
<div className="w-full max-w-[920px] space-y-16">
|
||||
<section
|
||||
aria-labelledby="welcome-join-key-step"
|
||||
className="translate-y-12"
|
||||
>
|
||||
<h2
|
||||
className="mb-4 text-sm font-normal"
|
||||
className="relative z-10 mb-8 text-sm font-normal"
|
||||
id="welcome-join-key-step"
|
||||
>
|
||||
Step 1: Share your public key
|
||||
Step 1: Ask your community host to send you an invite link
|
||||
or add you using your public key
|
||||
</h2>
|
||||
<div
|
||||
className={ONBOARDING_KEY_FRAME_CLASS}
|
||||
<Card
|
||||
className={WIDE_TEXTURE_CARD_CLASS}
|
||||
data-testid="welcome-join-npub-frame"
|
||||
variant="textured"
|
||||
>
|
||||
<div className={ONBOARDING_KEY_ROW_CLASS}>
|
||||
<div className="min-w-0 flex-1">
|
||||
<code
|
||||
className={`${ONBOARDING_KEY_TEXT_CLASS} block`}
|
||||
data-testid="welcome-join-npub"
|
||||
<div className={WIDE_TEXTURE_CONTENT_CLASS}>
|
||||
<div className={ONBOARDING_KEY_ROW_CLASS}>
|
||||
<div className="min-w-0 flex-1">
|
||||
<code
|
||||
className={`${ONBOARDING_KEY_TEXT_CLASS} block`}
|
||||
data-testid="welcome-join-npub"
|
||||
>
|
||||
{npub || "Loading…"}
|
||||
</code>
|
||||
</div>
|
||||
<Button
|
||||
aria-label="Copy npub"
|
||||
className="h-10 w-10 shrink-0 text-[var(--buzz-onboarding-backup-ink)] hover:bg-transparent hover:text-foreground"
|
||||
disabled={!npub}
|
||||
onClick={() => {
|
||||
void writeTextToClipboard(npub).then(() => {
|
||||
setCopied(true);
|
||||
window.setTimeout(() => setCopied(false), 1500);
|
||||
});
|
||||
}}
|
||||
size="icon"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
{npub || "Loading…"}
|
||||
</code>
|
||||
{copied ? (
|
||||
<Check
|
||||
className="h-6 w-6 text-primary"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : (
|
||||
<Copy className="h-6 w-6" aria-hidden="true" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
aria-label="Copy npub"
|
||||
className="h-10 w-10 shrink-0 text-[var(--buzz-onboarding-backup-ink)] hover:bg-transparent hover:text-foreground"
|
||||
disabled={!npub}
|
||||
onClick={() => {
|
||||
void writeTextToClipboard(npub).then(() => {
|
||||
setCopied(true);
|
||||
window.setTimeout(() => setCopied(false), 1500);
|
||||
});
|
||||
}}
|
||||
size="icon"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
{copied ? (
|
||||
<Check
|
||||
className="h-6 w-6 text-primary"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : (
|
||||
<Copy className="h-6 w-6" aria-hidden="true" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
{identityError ? (
|
||||
<p className="mt-4 text-sm text-destructive">
|
||||
{identityError}
|
||||
@@ -257,27 +279,37 @@ export function WelcomeSetup({
|
||||
onSubmit={handleJoin}
|
||||
>
|
||||
<h2
|
||||
className="mb-4 text-sm font-normal"
|
||||
className="relative z-10 mb-8 text-sm font-normal"
|
||||
id="welcome-join-url-step"
|
||||
>
|
||||
Step 2: Paste in your community URL
|
||||
</h2>
|
||||
<Input
|
||||
aria-label="Community URL"
|
||||
autoCapitalize="none"
|
||||
autoCorrect="off"
|
||||
className="h-auto rounded-xl border-0 bg-white/50 px-8 py-7 text-center font-mono !text-4xl text-[color:var(--buzz-onboarding-backup-ink)] shadow-none placeholder:text-[color:var(--buzz-onboarding-backup-ink)] placeholder:opacity-10 focus-visible:ring-1 focus-visible:ring-[rgb(113_113_6_/_0.5)]"
|
||||
data-testid="welcome-join-community-url"
|
||||
id="welcome-join-community-url"
|
||||
onChange={(event) => {
|
||||
setRelayUrl(event.target.value);
|
||||
setRelayUrlError(null);
|
||||
}}
|
||||
placeholder="Enter community URL"
|
||||
spellCheck={false}
|
||||
type="url"
|
||||
value={relayUrl}
|
||||
/>
|
||||
<Card
|
||||
className={`${WIDE_TEXTURE_CARD_CLASS} [--buzz-card-textured-min-height:128px]`}
|
||||
variant="textured"
|
||||
>
|
||||
<div
|
||||
className={WIDE_TEXTURE_CONTENT_CLASS}
|
||||
style={HORIZONTAL_INPUT_OVERFLOW_FADE}
|
||||
>
|
||||
<Input
|
||||
aria-label="Community URL"
|
||||
autoCapitalize="none"
|
||||
autoCorrect="off"
|
||||
className="h-auto rounded-none border-0 bg-transparent p-0 text-center font-mono !text-4xl text-[color:var(--buzz-onboarding-backup-ink)] shadow-none placeholder:text-[color:var(--buzz-onboarding-backup-ink)] placeholder:opacity-10 focus-visible:ring-0"
|
||||
data-testid="welcome-join-community-url"
|
||||
id="welcome-join-community-url"
|
||||
onChange={(event) => {
|
||||
setRelayUrl(event.target.value);
|
||||
setRelayUrlError(null);
|
||||
}}
|
||||
placeholder="Enter community URL"
|
||||
spellCheck={false}
|
||||
type="url"
|
||||
value={relayUrl}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
{relayUrlError ? (
|
||||
<p className="mt-3 text-sm text-destructive">
|
||||
{relayUrlError}
|
||||
@@ -288,7 +320,7 @@ export function WelcomeSetup({
|
||||
</div>
|
||||
<OnboardingFooter>
|
||||
<Button
|
||||
className="h-10 w-44 rounded-full"
|
||||
className={ONBOARDING_PRIMARY_CTA_CLASS}
|
||||
disabled={!relayUrl.trim()}
|
||||
form="welcome-join-form"
|
||||
type="submit"
|
||||
@@ -297,7 +329,7 @@ export function WelcomeSetup({
|
||||
<span className="sr-only">Join community</span>
|
||||
</Button>
|
||||
<Button
|
||||
className="h-10 w-44 rounded-full bg-foreground/10 hover:bg-foreground/15"
|
||||
className="h-9 rounded-full bg-foreground/10 px-6 hover:bg-foreground/15"
|
||||
onClick={() => showPage("welcome")}
|
||||
type="button"
|
||||
variant="ghost"
|
||||
@@ -321,7 +353,7 @@ export function WelcomeSetup({
|
||||
continue setup.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex w-full flex-1 items-center justify-center pb-4 pt-12">
|
||||
<div className="flex w-full flex-1 items-center justify-center">
|
||||
<InviteRedeemForm
|
||||
error={null}
|
||||
isRedeeming={false}
|
||||
|
||||
@@ -3,6 +3,7 @@ import * as React from "react";
|
||||
|
||||
import { getNsec } from "@/shared/api/tauriIdentity";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { Card } from "@/shared/ui/card";
|
||||
import { Spinner } from "@/shared/ui/spinner";
|
||||
import { ONBOARDING_PRIMARY_CTA_CLASS } from "./OnboardingChrome";
|
||||
import { OnboardingFooter } from "./OnboardingFooter";
|
||||
@@ -10,10 +11,7 @@ import {
|
||||
type OnboardingTransitionDirection,
|
||||
OnboardingSlideTransition,
|
||||
} from "./OnboardingSlideTransition";
|
||||
import {
|
||||
NsecMaskedDisplay,
|
||||
ONBOARDING_KEY_FRAME_CLASS,
|
||||
} from "./NsecMaskedDisplay";
|
||||
import { NsecMaskedDisplay } from "./NsecMaskedDisplay";
|
||||
|
||||
/**
|
||||
* Pure helper so the disabled logic can be unit-tested without a DOM.
|
||||
@@ -78,12 +76,12 @@ export function BackupStep({ direction, onBack, onNext }: BackupStepProps) {
|
||||
|
||||
return (
|
||||
<OnboardingSlideTransition
|
||||
className="flex w-full flex-col items-center"
|
||||
className="flex min-h-0 w-full flex-col items-center"
|
||||
data-testid="onboarding-page-backup"
|
||||
direction={direction}
|
||||
transitionKey={`backup-${direction}`}
|
||||
>
|
||||
<div className="w-full max-w-[500px] text-center">
|
||||
<div className="flex w-full max-w-[500px] shrink-0 flex-col text-center">
|
||||
<h1 className="text-title font-normal text-foreground">
|
||||
Your unique identity key has been created
|
||||
</h1>
|
||||
@@ -93,7 +91,7 @@ export function BackupStep({ direction, onBack, onNext }: BackupStepProps) {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-10 w-full max-w-4xl">
|
||||
<div className="flex w-full max-w-[1040px] flex-1 flex-col justify-center py-10">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center gap-2 py-6 text-sm text-foreground/70">
|
||||
<Spinner className="h-4 w-4 border-2" />
|
||||
@@ -125,10 +123,11 @@ export function BackupStep({ direction, onBack, onNext }: BackupStepProps) {
|
||||
</Button>
|
||||
</div>
|
||||
) : nsec ? (
|
||||
// Translucent white card frames the key with equal padding.
|
||||
<div className={ONBOARDING_KEY_FRAME_CLASS}>
|
||||
<NsecMaskedDisplay nsec={nsec} variant="bare" />
|
||||
</div>
|
||||
<Card className="w-full px-8 py-6" variant="textured">
|
||||
<div className="mx-auto w-full max-w-[832px]">
|
||||
<NsecMaskedDisplay nsec={nsec} variant="bare" />
|
||||
</div>
|
||||
</Card>
|
||||
) : (
|
||||
<p className="text-center text-sm text-foreground/70">
|
||||
No key available to back up.
|
||||
|
||||
@@ -13,11 +13,11 @@ import {
|
||||
} from "@/shared/api/invites";
|
||||
import { cn } from "@/shared/lib/cn";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { Card } from "@/shared/ui/card";
|
||||
import { Input } from "@/shared/ui/input";
|
||||
import { Spinner } from "@/shared/ui/spinner";
|
||||
import { JoinPolicyNotice } from "./JoinPolicyNotice";
|
||||
import {
|
||||
ONBOARDING_KEY_FRAME_CLASS,
|
||||
ONBOARDING_KEY_ROW_CLASS,
|
||||
ONBOARDING_KEY_TEXT_CLASS,
|
||||
} from "./NsecMaskedDisplay";
|
||||
@@ -26,6 +26,13 @@ import { OnboardingFooter } from "./OnboardingFooter";
|
||||
|
||||
const POLICY_DISCOVERY_DELAY_MS = 250;
|
||||
const POLICY_REVEAL_EASE = [0.23, 1, 0.32, 1] as const;
|
||||
const SPOTLIGHT_TEXTURE_CONTENT_CLASS = "mx-auto w-full max-w-[920px]";
|
||||
const SPOTLIGHT_OVERFLOW_FADE = {
|
||||
WebkitMaskImage:
|
||||
"linear-gradient(to right, transparent, black 2rem, black calc(100% - 2rem), transparent)",
|
||||
maskImage:
|
||||
"linear-gradient(to right, transparent, black 2rem, black calc(100% - 2rem), transparent)",
|
||||
};
|
||||
|
||||
type InviteRedeemFormProps = {
|
||||
/**
|
||||
@@ -255,38 +262,45 @@ export function InviteRedeemForm({
|
||||
<form
|
||||
className={cn(
|
||||
"flex w-full flex-col",
|
||||
isOnboardingSpotlight ? "items-center gap-4" : "gap-3",
|
||||
isOnboardingSpotlight ? "relative items-center" : "gap-3",
|
||||
)}
|
||||
id={formId}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
{isOnboardingSpotlight ? (
|
||||
<label
|
||||
className={cn("w-full max-w-4xl", ONBOARDING_KEY_FRAME_CLASS)}
|
||||
<Card
|
||||
className="w-[min(calc(100%+12rem),calc(100vw-2rem))] max-w-[1120px] translate-y-8 px-8 py-6"
|
||||
data-testid="invite-redeem-input-frame"
|
||||
htmlFor="invite-input"
|
||||
variant="textured"
|
||||
>
|
||||
<span className="sr-only">Invite link or code</span>
|
||||
<span className={ONBOARDING_KEY_ROW_CLASS}>
|
||||
<input
|
||||
autoCapitalize="none"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
className={cn(
|
||||
ONBOARDING_KEY_TEXT_CLASS,
|
||||
"block border-0 bg-transparent p-0 text-center shadow-none outline-none placeholder:text-[var(--buzz-onboarding-backup-ink)] placeholder:opacity-40 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50",
|
||||
)}
|
||||
data-testid="invite-redeem-input"
|
||||
disabled={isRedeeming}
|
||||
id="invite-input"
|
||||
onChange={handleInviteInputChange}
|
||||
placeholder="https://relay.example.com/invite/abc123"
|
||||
spellCheck={false}
|
||||
type="text"
|
||||
value={inviteInput}
|
||||
/>
|
||||
</span>
|
||||
</label>
|
||||
<div
|
||||
className={SPOTLIGHT_TEXTURE_CONTENT_CLASS}
|
||||
style={SPOTLIGHT_OVERFLOW_FADE}
|
||||
>
|
||||
<label className="block w-full" htmlFor="invite-input">
|
||||
<span className="sr-only">Invite link or code</span>
|
||||
<span className={ONBOARDING_KEY_ROW_CLASS}>
|
||||
<input
|
||||
autoCapitalize="none"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
className={cn(
|
||||
ONBOARDING_KEY_TEXT_CLASS,
|
||||
"block border-0 bg-transparent p-0 text-center shadow-none outline-none placeholder:text-[var(--buzz-onboarding-backup-ink)] placeholder:opacity-40 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50",
|
||||
)}
|
||||
data-testid="invite-redeem-input"
|
||||
disabled={isRedeeming}
|
||||
id="invite-input"
|
||||
onChange={handleInviteInputChange}
|
||||
placeholder="https://relay.example.com/invite/abc123"
|
||||
spellCheck={false}
|
||||
type="text"
|
||||
value={inviteInput}
|
||||
/>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="space-y-1.5 text-left">
|
||||
<label
|
||||
@@ -317,7 +331,7 @@ export function InviteRedeemForm({
|
||||
aria-hidden={!showInvalidInviteTip}
|
||||
aria-live="polite"
|
||||
className={cn(
|
||||
"min-h-5 w-full max-w-4xl text-center text-sm text-[#717106] transition-opacity duration-150 ease-out",
|
||||
"absolute top-[calc(100%+2rem)] mt-4 min-h-5 w-full max-w-4xl text-center text-sm text-[#717106] transition-opacity duration-150 ease-out",
|
||||
showInvalidInviteTip ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
data-testid="invalid-invite-tip"
|
||||
|
||||
@@ -179,7 +179,7 @@ export function MachineOnboardingFlow({
|
||||
) : null}
|
||||
<OnboardingFooterProvider>
|
||||
<div
|
||||
className={`relative flex w-full max-w-[920px] flex-col items-center text-center ${
|
||||
className={`relative flex w-full max-w-[1040px] flex-col items-center text-center ${
|
||||
page === "identity" ? "my-auto" : "buzz-onboarding-step-frame"
|
||||
}`}
|
||||
>
|
||||
|
||||
@@ -7,10 +7,7 @@ import { Button } from "@/shared/ui/button";
|
||||
import { Card } from "@/shared/ui/card";
|
||||
import { Input } from "@/shared/ui/input";
|
||||
import { Spinner } from "@/shared/ui/spinner";
|
||||
import {
|
||||
ONBOARDING_INK_ICON_CLASS,
|
||||
ONBOARDING_PRIMARY_CTA_CLASS,
|
||||
} from "./OnboardingChrome";
|
||||
import { ONBOARDING_PRIMARY_CTA_CLASS } from "./OnboardingChrome";
|
||||
import { OnboardingFooter } from "./OnboardingFooter";
|
||||
|
||||
const NOSTR_KEY_FILE_MAX_BYTES = 1024;
|
||||
@@ -184,8 +181,7 @@ export function NostrKeyImportForm({
|
||||
isRevealed ? "Hide private key" : "Reveal private key"
|
||||
}
|
||||
className={cn(
|
||||
ONBOARDING_INK_ICON_CLASS,
|
||||
"absolute -right-2 top-1/2 h-10 w-10 -translate-y-1/2 transition-opacity duration-300 motion-reduce:transition-none",
|
||||
"absolute right-8 top-1/2 h-10 w-10 -translate-y-1/2 text-muted-foreground transition-opacity duration-300 hover:bg-foreground/10 hover:text-foreground motion-reduce:transition-none",
|
||||
hasInput ? "opacity-100" : "pointer-events-none opacity-0",
|
||||
)}
|
||||
data-testid="nostr-import-reveal-toggle"
|
||||
|
||||
@@ -50,7 +50,7 @@ export function OnboardingChrome({
|
||||
return (
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none fixed inset-x-0 top-14 z-10 flex items-center px-6 text-foreground"
|
||||
className="pointer-events-none fixed inset-x-0 top-12 z-10 flex items-center px-6 text-foreground"
|
||||
>
|
||||
<span className="block w-11" data-testid="onboarding-logo">
|
||||
<BuzzMark className="h-auto w-full" />
|
||||
|
||||
@@ -15,6 +15,7 @@ import { getInstallErrorMessage } from "@/shared/lib/installError";
|
||||
import { cn } from "@/shared/lib/cn";
|
||||
import { Badge } from "@/shared/ui/badge";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { Card } from "@/shared/ui/card";
|
||||
import { FlappingBee } from "@/shared/ui/buzz-logo/FlappingBee";
|
||||
import { Spinner } from "@/shared/ui/spinner";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip";
|
||||
@@ -85,7 +86,7 @@ function RuntimeSelectionIndicator({
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={cn(
|
||||
"pointer-events-none absolute right-3 top-3 flex h-8 w-8 scale-90 items-center justify-center rounded-full border border-[var(--buzz-welcome-chartreuse)] bg-white/75 opacity-0 transition-[background-color,opacity,transform] duration-200 ease-out group-hover:scale-100 group-hover:opacity-100 group-focus-visible:scale-100 group-focus-visible:opacity-100",
|
||||
"pointer-events-none absolute right-8 top-8 flex h-8 w-8 scale-90 items-center justify-center rounded-full border border-[var(--buzz-welcome-chartreuse)] bg-white/75 opacity-0 transition-[background-color,opacity,transform] duration-200 ease-out group-hover:scale-100 group-hover:opacity-100 group-focus-visible:scale-100 group-focus-visible:opacity-100",
|
||||
selected &&
|
||||
"scale-100 bg-[var(--buzz-welcome-chartreuse)] opacity-100 group-hover:opacity-100",
|
||||
)}
|
||||
@@ -541,14 +542,13 @@ function RuntimeCard({
|
||||
const canSelect = runtimeCanBeSelected(runtime);
|
||||
|
||||
return (
|
||||
// biome-ignore lint/a11y/useSemanticElements: Cannot use <input> because this card contains nested setup and details buttons, which require interactive content
|
||||
<div
|
||||
<Card
|
||||
aria-checked={selected}
|
||||
aria-disabled={!canSelect}
|
||||
className={cn(
|
||||
"group relative flex aspect-[288/132] min-h-[96px] w-full max-w-[288px] select-none flex-col items-center justify-center rounded-2xl border-0 bg-white/75 px-3 py-1.5 text-center outline-none transition-colors duration-150 ease-out hover:bg-white/80 active:bg-white/90 focus-visible:ring-2 focus-visible:ring-foreground/40",
|
||||
"group h-[224px] w-full max-w-[288px] select-none items-center px-3 py-1.5 text-center outline-none transition-[filter] duration-150 ease-out hover:brightness-[0.98] active:brightness-[0.96] focus-visible:ring-2 focus-visible:ring-foreground/40",
|
||||
installError && "ring-1 ring-destructive/40",
|
||||
selected && "bg-white/90 hover:bg-white/90",
|
||||
selected && "brightness-[0.98] hover:brightness-[0.98]",
|
||||
canSelect ? "cursor-pointer" : "cursor-default",
|
||||
)}
|
||||
data-testid={`onboarding-runtime-${runtime.id}`}
|
||||
@@ -562,6 +562,7 @@ function RuntimeCard({
|
||||
}}
|
||||
role="checkbox"
|
||||
tabIndex={canSelect ? 0 : -1}
|
||||
variant="textured"
|
||||
>
|
||||
<RuntimeSelectionIndicator runtime={runtime} selected={selected} />
|
||||
|
||||
@@ -604,7 +605,7 @@ function RuntimeCard({
|
||||
) : (
|
||||
<RuntimeAuthError runtime={runtime} />
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -27,9 +27,11 @@
|
||||
* on either axis the opposing bands collapse into each other: the solid
|
||||
* white center region disappears and the bands meet on a semi-transparent
|
||||
* row of the fade, which shows the page background through as a thin seam
|
||||
* line. Enforce the floor so the texture can never degenerate.
|
||||
* line. Keep 224px as the default floor. Compact decorative cards may opt
|
||||
* into a smaller height with `--buzz-card-textured-min-height` when the
|
||||
* center seam is acceptable for that deliberately compressed surface.
|
||||
*/
|
||||
min-height: 224px;
|
||||
min-height: var(--buzz-card-textured-min-height, 224px);
|
||||
min-width: 224px;
|
||||
border-image-source: url("./assets/card-texture.png");
|
||||
border-image-slice: 416 fill;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/shared/lib/cn";
|
||||
@@ -46,16 +47,21 @@ const cardVariants = cva("text-card-foreground", {
|
||||
|
||||
export interface CardProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof cardVariants> {}
|
||||
VariantProps<typeof cardVariants> {
|
||||
asChild?: boolean;
|
||||
}
|
||||
|
||||
const Card = React.forwardRef<HTMLDivElement, CardProps>(
|
||||
({ className, variant, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(cardVariants({ variant, className }))}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
({ asChild = false, className, variant, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "div";
|
||||
return (
|
||||
<Comp
|
||||
ref={ref}
|
||||
className={cn(cardVariants({ variant, className }))}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
Card.displayName = "Card";
|
||||
|
||||
|
||||
@@ -643,6 +643,7 @@ test("first-community choices expose npub and invite input", async ({
|
||||
await expectCommunityBranchFramePosition(page, joinKeyFrame);
|
||||
const joinKeyFrameBox = await joinKeyFrame.boundingBox();
|
||||
expect(joinKeyFrameBox?.width).toBeGreaterThan(700);
|
||||
await expect(joinKeyFrame).toHaveClass(/buzz-card-textured/);
|
||||
const joinKeyFrameStyles = await joinKeyFrame.evaluate((element) => {
|
||||
const styles = window.getComputedStyle(element);
|
||||
return {
|
||||
@@ -650,8 +651,8 @@ test("first-community choices expose npub and invite input", async ({
|
||||
borderRadius: styles.borderRadius,
|
||||
};
|
||||
});
|
||||
expect(joinKeyFrameStyles.backgroundColor).toMatch(/(0\.5\)|\/ 0\.5\))/);
|
||||
expect(joinKeyFrameStyles.borderRadius).toBe("12px");
|
||||
expect(joinKeyFrameStyles.backgroundColor).toBe("rgba(0, 0, 0, 0)");
|
||||
expect(joinKeyFrameStyles.borderRadius).toBe("0px");
|
||||
await expect
|
||||
.poll(() =>
|
||||
joinNpub.evaluate((element) => {
|
||||
@@ -687,6 +688,7 @@ test("first-community choices expose npub and invite input", async ({
|
||||
await expectCommunityBranchFramePosition(page, inviteInputFrame);
|
||||
const inviteInputFrameBox = await inviteInputFrame.boundingBox();
|
||||
expect(inviteInputFrameBox?.width).toBeGreaterThan(700);
|
||||
await expect(inviteInputFrame).toHaveClass(/buzz-card-textured/);
|
||||
const inviteInputFrameStyles = await inviteInputFrame.evaluate((element) => {
|
||||
const styles = window.getComputedStyle(element);
|
||||
return {
|
||||
@@ -694,8 +696,8 @@ test("first-community choices expose npub and invite input", async ({
|
||||
borderRadius: styles.borderRadius,
|
||||
};
|
||||
});
|
||||
expect(inviteInputFrameStyles.backgroundColor).toMatch(/(0\.5\)|\/ 0\.5\))/);
|
||||
expect(inviteInputFrameStyles.borderRadius).toBe("12px");
|
||||
expect(inviteInputFrameStyles.backgroundColor).toBe("rgba(0, 0, 0, 0)");
|
||||
expect(inviteInputFrameStyles.borderRadius).toBe("0px");
|
||||
await expect
|
||||
.poll(() =>
|
||||
inviteInput.evaluate((element) => {
|
||||
|
||||
Reference in New Issue
Block a user