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:
cynfria
2026-07-19 14:18:32 -07:00
committed by GitHub
parent 1342fc564a
commit e84f3fe8ed
10 changed files with 204 additions and 152 deletions

View File

@@ -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}

View File

@@ -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.

View File

@@ -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"

View File

@@ -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"
}`}
>

View File

@@ -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"

View File

@@ -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" />

View File

@@ -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>
);
}

View File

@@ -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;

View File

@@ -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";

View File

@@ -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) => {