💄 Update billing page and refine ui components (#830)

This commit is contained in:
Luke Vella 2023-08-29 17:29:26 +01:00 committed by GitHub
parent 1b100481a0
commit d261ef0d59
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 321 additions and 238 deletions

View file

@ -156,14 +156,11 @@
"goToInvite": "Go to Invite Page",
"planPro": "Pro",
"Billing": "Billing",
"annualBilling": "Annual billing (Save {discount}%)",
"planUpgrade": "Upgrade",
"subscriptionUpdatePayment": "Update Payment Details",
"subscriptionCancel": "Cancel Subscription",
"billingStatus": "Billing Status",
"billingStatusDescription": "Manage your subscription and billing details",
"subscriptionPlans": "Plans",
"subscriptionDescription": "Get access to more features by upgrading to a paid plan.",
"freeForever": "free forever",
"annualBillingDescription": "per month, billed annually",
"billingStatusState": "Status",
@ -176,12 +173,8 @@
"billingPeriod": "Period",
"billingPeriodMonthly": "Monthly",
"billingPeriodYearly": "Yearly",
"plan_unlimitedPolls": "Unlimited polls",
"plan_unlimitedParticipants": "Unlimited participants",
"monthlyBillingDescription": "per month",
"plan_finalizePolls": "Finalize polls",
"plan_extendedPollLife": "Keep polls indefinitely",
"plan_prioritySupport": "Priority support",
"becomeATranslator": "Help translate",
"noPolls": "No polls",
"noPollsDescription": "Get started by creating a new poll.",
@ -200,7 +193,6 @@
"hideScoresDescription": "Only show scores until after a participant has voted",
"disableComments": "Disable comments",
"disableCommentsDescription": "Remove the option to leave a comment on the poll",
"planCustomizablePollSettings": "Customizable poll settings",
"clockPreferences": "Clock Preferences",
"clockPreferencesDescription": "Set your preferred time zone and time format.",
"featureRequest": "Request a Feature",
@ -230,5 +222,15 @@
"billingPortal": "Billing Portal",
"supportDescription": "Need help with anything?",
"supportBilling": "Please reach out if you need any assistance.",
"contactSupport": "Contact Support"
"contactSupport": "Contact Support",
"planFreeDescription": "For casual users",
"currentPlan": "Current Plan",
"limitedAccess": "Access to core features",
"pollsDeleted": "Polls are automatically deleted once they become inactive",
"planProDescription": "For power users and professionals",
"accessAllFeatures": "Access all features",
"earlyAccess": "Get early access to new features",
"earlyAdopterDescription": "As an early adopter, you'll lock in your subscription rate and won't be affected by future price increases.",
"nextPriceIncrease": "Next price increase is scheduled for <b>{date}</b>.",
"upgradeNowSaveLater": "Upgrade now, save later"
}

View file

@ -1,159 +1,184 @@
import {
BillingPlan,
BillingPlanFooter,
BillingPlanHeader,
BillingPlanPeriod,
BillingPlanPerk,
BillingPlanPerks,
BillingPlanPrice,
BillingPlanTitle,
} from "@rallly/ui/billing-plan";
import { Label } from "@rallly/ui/label";
import { Switch } from "@rallly/ui/switch";
import { CheckCircle2Icon, TrendingUpIcon } from "@rallly/icons";
import { cn } from "@rallly/ui";
import { BillingPlanPeriod, BillingPlanPrice } from "@rallly/ui/billing-plan";
import { Button } from "@rallly/ui/button";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@rallly/ui/tabs";
import dayjs from "dayjs";
import React from "react";
import { Trans } from "@/components/trans";
import { UpgradeButton } from "@/components/upgrade-button";
import { usePlan } from "@/contexts/plan";
const monthlyPriceUsd = 5;
const annualPriceUsd = 30;
const Perks = ({ children }: React.PropsWithChildren) => {
return <ul className="grid gap-1">{children}</ul>;
};
const Perk = ({
children,
pro,
}: React.PropsWithChildren<{ pro?: boolean }>) => {
return (
<li className="flex items-start gap-x-2.5">
<CheckCircle2Icon
className={cn(
"mt-0.5 h-4 w-4 shrink-0",
!pro ? "text-gray-500" : "text-primary",
)}
/>
<div className="text-sm">{children}</div>
</li>
);
};
export const BillingPlans = () => {
const [isBilledAnnually, setBilledAnnually] = React.useState(true);
const plan = usePlan();
const isPlus = plan === "paid";
const [tab, setTab] = React.useState("yearly");
return (
<div className="space-y-4">
<div>
<Label className="mb-4">
<Trans i18nKey="subscriptionPlans" defaults="Plans" />
</Label>
<p className="text-muted-foreground mb-4 text-sm">
<Tabs value={tab} onValueChange={setTab}>
<TabsList className="mb-4">
<TabsTrigger value="monthly">
<Trans i18nKey="billingPeriodMonthly" />
</TabsTrigger>
<TabsTrigger value="yearly">
<Trans i18nKey="billingPeriodYearly" />
</TabsTrigger>
</TabsList>
<div className="grid gap-4 rounded-md md:grid-cols-2">
<div className="space-y-4 rounded-md border p-4">
<div>
<h3>
<Trans i18nKey="planFree" />
</h3>
<p className="text-muted-foreground text-sm">
<Trans
i18nKey="planFreeDescription"
defaults="For casual users"
/>
</p>
</div>
<div>
<BillingPlanPrice>$0</BillingPlanPrice>
<BillingPlanPeriod>
<Trans i18nKey="freeForever" />
</BillingPlanPeriod>
</div>
<hr />
<Button disabled className="w-full">
<Trans i18nKey="currentPlan" defaults="Current Plan" />
</Button>
<Perks>
<Perk>
<Trans
i18nKey="limitedAccess"
defaults="Access to core features"
/>
</Perk>
<Perk>
<Trans
i18nKey="pollsDeleted"
defaults="Polls are automatically deleted once they become inactive"
/>
</Perk>
</Perks>
</div>
<div className="space-y-4 rounded-md border p-4">
<div>
<h3>
<Trans i18nKey="planPro" />
</h3>
<p className="text-muted-foreground text-sm">
<Trans
i18nKey="planProDescription"
defaults="For power users and professionals"
/>
</p>
</div>
<div className="flex">
<TabsContent value="yearly">
<BillingPlanPrice
discount={`$${(annualPriceUsd / 12).toFixed(2)}`}
>
${monthlyPriceUsd}
</BillingPlanPrice>
<BillingPlanPeriod>
<Trans
i18nKey="annualBillingDescription"
defaults="per month, billed annually"
/>
</BillingPlanPeriod>
</TabsContent>
<TabsContent value="monthly">
<BillingPlanPrice>${monthlyPriceUsd}</BillingPlanPrice>
<BillingPlanPeriod>
<Trans
i18nKey="monthlyBillingDescription"
defaults="per month"
/>
</BillingPlanPeriod>
</TabsContent>
</div>
<hr />
<UpgradeButton annual={tab === "yearly"} />
<Perks>
<Perk pro={true}>
<Trans
i18nKey="accessAllFeatures"
defaults="Access all features"
/>
</Perk>
<Perk pro={true}>
<Trans i18nKey="plan_extendedPollLife" />
</Perk>
<Perk pro={true}>
<Trans
i18nKey="earlyAccess"
defaults="Get early access to new features"
/>
</Perk>
</Perks>
</div>
</div>
</Tabs>
<div className="rounded-md border border-cyan-200 bg-cyan-50 px-4 py-3 text-cyan-800">
<div className="flex items-start justify-between">
<TrendingUpIcon className="text-indigo mb-4 mr-2 mt-0.5 h-6 w-6 shrink-0 text-cyan-500" />
</div>
<div className="mb-2 flex items-center gap-x-2">
<h3 className="text-sm">
<Trans
i18nKey="upgradeNowSaveLater"
defaults="Upgrade now, save later"
/>
</h3>
</div>
<p className="-200 mb-4 text-sm">
<Trans
i18nKey="subscriptionDescription"
defaults="By subscribing, you not only gain access to all features but you are also directly supporting further development of Rallly."
i18nKey="earlyAdopterDescription"
defaults="As an early adopter, you'll lock in your subscription rate and won't be affected by future price increases."
/>
</p>
</div>
<div className="flex items-center gap-2.5">
<Switch
id="annual-switch"
checked={isBilledAnnually}
onCheckedChange={(checked) => {
setBilledAnnually(checked);
}}
/>
<Label htmlFor="annual-switch">
<p className="text-sm">
<Trans
i18nKey="annualBilling"
defaults="Annual billing (Save {discount}%)"
i18nKey="nextPriceIncrease"
defaults="Next price increase is scheduled for <b>{date}</b>."
components={{
b: (
<a
href="https://rallly.co/blog/july-recap"
className="font-bold hover:underline"
/>
),
}}
values={{
discount: Math.round(100 - (annualPriceUsd / 12 / 5) * 100),
date: dayjs("2023-09-01").format("LL"),
}}
/>
</Label>
</div>
<div className="grid gap-4 sm:grid-cols-2">
<BillingPlan>
<BillingPlanHeader>
<BillingPlanTitle>
<Trans i18nKey="planFree" defaults="Free" />
</BillingPlanTitle>
<BillingPlanPrice>$0</BillingPlanPrice>
<BillingPlanPeriod>
<Trans i18nKey="freeForever" defaults="free forever" />
</BillingPlanPeriod>
</BillingPlanHeader>
<BillingPlanPerks>
<BillingPlanPerk>
<Trans i18nKey="plan_unlimitedPolls" defaults="Unlimited polls" />
</BillingPlanPerk>
<BillingPlanPerk>
<Trans
i18nKey="plan_unlimitedParticipants"
defaults="Unlimited participants"
/>
</BillingPlanPerk>
</BillingPlanPerks>
</BillingPlan>
<ProPlan annual={isBilledAnnually}>
{!isPlus ? (
<UpgradeButton annual={isBilledAnnually}>
<Trans i18nKey="planUpgrade" defaults="Upgrade" />
</UpgradeButton>
) : null}
</ProPlan>
</p>
</div>
</div>
);
};
export const ProPlan = ({
annual,
children,
}: React.PropsWithChildren<{
annual?: boolean;
}>) => {
return (
<BillingPlan variant="primary">
<BillingPlanHeader>
<BillingPlanTitle className="text-primary">
<Trans i18nKey="planPro" defaults="Pro" />
</BillingPlanTitle>
{annual ? (
<>
<BillingPlanPrice discount={`$${(annualPriceUsd / 12).toFixed(2)}`}>
${monthlyPriceUsd}
</BillingPlanPrice>
<BillingPlanPeriod>
<Trans
i18nKey="annualBillingDescription"
defaults="per month, billed annually"
/>
</BillingPlanPeriod>
</>
) : (
<>
<BillingPlanPrice>${monthlyPriceUsd}</BillingPlanPrice>
<BillingPlanPeriod>
<Trans i18nKey="monthlyBillingDescription" defaults="per month" />
</BillingPlanPeriod>
</>
)}
</BillingPlanHeader>
<BillingPlanPerks>
<BillingPlanPerk>
<Trans i18nKey="plan_unlimitedPolls" defaults="Unlimited polls" />
</BillingPlanPerk>
<BillingPlanPerk>
<Trans
i18nKey="plan_unlimitedParticipants"
defaults="Unlimited participants"
/>
</BillingPlanPerk>
<BillingPlanPerk>
<Trans i18nKey="plan_finalizePolls" defaults="Finalize polls" />
</BillingPlanPerk>
<BillingPlanPerk>
<Trans
i18nKey="planCustomizablePollSettings"
defaults="Customizable poll settings"
/>
</BillingPlanPerk>
<BillingPlanPerk>
<Trans
i18nKey="plan_extendedPollLife"
defaults="Extended poll life"
/>
</BillingPlanPerk>
<BillingPlanPerk>
<Trans i18nKey="plan_prioritySupport" defaults="Priority support" />
</BillingPlanPerk>
</BillingPlanPerks>
<BillingPlanFooter>{children}</BillingPlanFooter>
</BillingPlan>
);
};

View file

@ -2,9 +2,11 @@ import { trpc } from "@rallly/backend";
import { HelpCircleIcon } from "@rallly/icons";
import { cn } from "@rallly/ui";
import { Button } from "@rallly/ui/button";
import { TooltipContent, TooltipTrigger } from "@rallly/ui/tooltip";
import Script from "next/script";
import React from "react";
import Tooltip from "@/components/tooltip";
import { Trans } from "@/components/trans";
import { isFeedbackEnabled } from "@/utils/constants";
@ -35,24 +37,28 @@ export const Changelog = ({ className }: { className?: string }) => {
return (
<>
<FeaturebaseScript />
<Button
className={cn(
"hidden sm:inline-flex [&>*]:pointer-events-none",
className,
)}
size="sm"
variant="ghost"
data-featurebase-changelog
>
<HelpCircleIcon className="h-4 w-4" />
<span className="hidden sm:block">
<Tooltip>
<TooltipTrigger>
<Button
className={cn(
"hidden sm:inline-flex [&>*]:pointer-events-none",
className,
)}
size="sm"
variant="ghost"
data-featurebase-changelog
>
<HelpCircleIcon className="h-4 w-4" />
<span
id="fb-update-badge"
className="bg-primary rounded-full px-2 py-px text-xs text-gray-100 empty:hidden"
/>
</Button>
</TooltipTrigger>
<TooltipContent>
<Trans key="whatsNew" defaults="What's new?" />
</span>
<span
id="fb-update-badge"
className="bg-primary rounded-full px-2 py-px text-xs text-gray-100 empty:hidden"
/>
</Button>
</TooltipContent>
</Tooltip>
</>
);
};

View file

@ -1,4 +1,4 @@
import { ListIcon, LogInIcon } from "@rallly/icons";
import { ListIcon, LogInIcon, SparklesIcon } from "@rallly/icons";
import { cn } from "@rallly/ui";
import { Button } from "@rallly/ui/button";
import clsx from "clsx";
@ -16,6 +16,7 @@ import FeedbackButton from "@/components/feedback";
import { Spinner } from "@/components/spinner";
import { Trans } from "@/components/trans";
import { UserDropdown } from "@/components/user-dropdown";
import { IfFreeUser } from "@/contexts/plan";
import { isFeedbackEnabled } from "@/utils/constants";
import { DayjsProvider } from "@/utils/dayjs";
@ -56,6 +57,17 @@ const NavMenuItem = ({
);
};
const Upgrade = () => {
return (
<Button variant="primary" size="sm" asChild>
<Link href="/settings/billing">
<SparklesIcon className="-ml-0.5 h-4 w-4" />
<Trans i18nKey="upgrade" defaults="Upgrade" />
</Link>
</Button>
);
};
const Logo = () => {
const router = useRouter();
const [isBusy, setIsBusy] = React.useState(false);
@ -106,7 +118,7 @@ const MainNav = () => {
}}
initial="hidden"
animate="visible"
exit={"hidden"}
exit="hidden"
className="border-b bg-gray-50/50"
>
<Container className="flex h-14 items-center justify-between gap-4">
@ -121,7 +133,10 @@ const MainNav = () => {
</nav>
</div>
<div className="flex items-center gap-x-4">
<nav className="flex items-center gap-x-1 sm:gap-x-2">
<nav className="flex items-center gap-x-1 sm:gap-x-1.5">
<IfFreeUser>
<Upgrade />
</IfFreeUser>
<IfGuest>
<Button
size="sm"

View file

@ -7,9 +7,7 @@ export const SettingsSection = (props: {
<div className="grid max-w-7xl grid-cols-1 gap-x-8 gap-y-4 p-3 sm:p-6 md:grid-cols-3">
<div>
<h2 className="text-base font-semibold">{props.title}</h2>
<p className="mt-1 text-sm leading-6 text-gray-500">
{props.description}
</p>
<p className="mt-1 text-sm text-gray-500">{props.description}</p>
</div>
<div className="md:col-span-2">{props.children}</div>
</div>

View file

@ -1,5 +1,6 @@
import { Button } from "@rallly/ui/button";
import Link from "next/link";
import { Trans } from "next-i18next";
import React from "react";
import { usePostHog } from "@/utils/posthog";
@ -11,23 +12,21 @@ export const UpgradeButton = ({
const posthog = usePostHog();
return (
<>
<Button
className="w-full"
variant="primary"
asChild
onClick={() => {
posthog?.capture("click upgrade button");
}}
<Button
className="w-full"
variant="primary"
asChild
onClick={() => {
posthog?.capture("click upgrade button");
}}
>
<Link
href={`/api/stripe/checkout?period=${
annual ? "yearly" : "monthly"
}&return_path=${encodeURIComponent(window.location.pathname)}`}
>
<Link
href={`/api/stripe/checkout?period=${
annual ? "yearly" : "monthly"
}&return_path=${encodeURIComponent(window.location.pathname)}`}
>
{children}
</Link>
</Button>
</>
{children || <Trans i18nKey="upgrade" defaults="Upgrade" />}
</Link>
</Button>
);
};

View file

@ -11,7 +11,6 @@ import {
UserIcon,
UserPlusIcon,
} from "@rallly/icons";
import { Badge } from "@rallly/ui/badge";
import { Button } from "@rallly/ui/button";
import {
DropdownMenu,
@ -25,27 +24,11 @@ import Link from "next/link";
import { Trans } from "@/components/trans";
import { CurrentUserAvatar } from "@/components/user";
import { usePlan } from "@/contexts/plan";
import { Plan } from "@/contexts/plan";
import { isFeedbackEnabled } from "@/utils/constants";
import { IfAuthenticated, IfGuest, useUser } from "./user-provider";
const Plan = () => {
const plan = usePlan();
if (plan === "paid") {
return (
<Badge>
<Trans i18nKey="planPro" defaults="Pro" />
</Badge>
);
}
return (
<Badge variant="secondary">
<Trans i18nKey="planFree" defaults="Free" />
</Badge>
);
};
export const UserDropdown = () => {
const { user } = useUser();
return (

View file

@ -1,4 +1,8 @@
import { trpc } from "@rallly/backend";
import { Badge } from "@rallly/ui/badge";
import React from "react";
import { Trans } from "@/components/trans";
export const usePlan = () => {
const { data } = trpc.user.subscription.useQuery();
@ -7,3 +11,32 @@ export const usePlan = () => {
return isPaid ? "paid" : "free";
};
export const IfSubscribed = ({ children }: React.PropsWithChildren) => {
const plan = usePlan();
return plan === "paid" ? <>{children}</> : null;
};
export const IfFreeUser = ({ children }: React.PropsWithChildren) => {
const plan = usePlan();
return plan === "free" ? <>{children}</> : null;
};
export const Plan = () => {
const plan = usePlan();
if (plan === "paid") {
return (
<Badge>
<Trans i18nKey="planPro" defaults="Pro" />
</Badge>
);
}
return (
<Badge variant="secondary">
<Trans i18nKey="planFree" defaults="Free" />
</Badge>
);
};

View file

@ -1,6 +1,5 @@
import { trpc } from "@rallly/backend";
import { ArrowUpRight, CreditCardIcon } from "@rallly/icons";
import { Badge } from "@rallly/ui/badge";
import { Button } from "@rallly/ui/button";
import { Card } from "@rallly/ui/card";
import { Label } from "@rallly/ui/label";
@ -14,6 +13,7 @@ import { getProfileLayout } from "@/components/layouts/profile-layout";
import { SettingsSection } from "@/components/settings/settings-section";
import { Trans } from "@/components/trans";
import { useUser } from "@/components/user-provider";
import { Plan } from "@/contexts/plan";
import { NextPageWithLayout } from "../../types";
import { getStaticTranslations } from "../../utils/with-page-translations";
@ -28,12 +28,10 @@ declare global {
const BillingPortal = () => {
return (
<div>
<div className="mb-4 flex items-center justify-between">
<Badge>
<Trans i18nKey="planPro" />
</Badge>
</div>
<p>
<Label className="mb-2">
<Trans i18nKey="billingPortal" />
</Label>
<p className="text-sm">
<Trans
i18nKey="activeSubscription"
defaults="Thank you for subscribing to Rallly Pro. You can manage your subscription and billing details from the billing portal."
@ -76,14 +74,30 @@ const SubscriptionStatus = () => {
return <>You need to be logged in.</>;
}
if (data.legacy) {
// User is on the old billing system
return <LegacyBilling />;
} else if (data.active) {
return <BillingPortal />;
} else {
return <BillingPlans />;
}
return (
<div className="space-y-6">
<div>
<Label className="mb-2">
<Trans i18nKey="currentPlan" />
</Label>
<div>
<Plan />
</div>
</div>
{!data.active ? (
<div>
<Label className="mb-4">
<Trans i18nKey="upgrade" />
</Label>
<BillingPlans />
</div>
) : data.legacy ? (
<LegacyBilling />
) : (
<BillingPortal />
)}
</div>
);
};
const LegacyBilling = () => {
@ -229,7 +243,7 @@ const Page: NextPageWithLayout = () => {
}
>
<div className="space-y-6">
<p>
<p className="text-sm">
<Trans
i18nKey="supportBilling"
defaults="Please reach out if you need any assistance."

View file

@ -20,16 +20,18 @@ export const user = router({
},
});
}),
subscription: possiblyPublicProcedure.query(async ({ ctx }) => {
if (ctx.user.isGuest) {
// guest user can't have an active subscription
return {
active: false,
};
}
subscription: possiblyPublicProcedure.query(
async ({ ctx }): Promise<{ legacy?: boolean; active: boolean }> => {
if (ctx.user.isGuest) {
// guest user can't have an active subscription
return {
active: false,
};
}
return await getSubscriptionStatus(ctx.user.id);
}),
return await getSubscriptionStatus(ctx.user.id);
},
),
changeName: privateProcedure
.input(
z.object({

View file

@ -9,6 +9,7 @@ export const getSubscriptionStatus = async (userId: string) => {
subscription: {
select: {
active: true,
periodEnd: true,
},
},
},
@ -17,6 +18,7 @@ export const getSubscriptionStatus = async (userId: string) => {
if (user?.subscription?.active === true) {
return {
active: true,
expiresAt: user.subscription.periodEnd,
};
}
@ -27,6 +29,9 @@ export const getSubscriptionStatus = async (userId: string) => {
gt: new Date(),
},
},
select: {
endDate: true,
},
});
if (
@ -36,6 +41,7 @@ export const getSubscriptionStatus = async (userId: string) => {
return {
active: true,
legacy: true,
expiresAt: userPaymentData.endDate,
};
}

View file

@ -6,7 +6,7 @@ import * as React from "react";
import { cn } from "./lib/utils";
const buttonVariants = cva(
"inline-flex border font-medium disabled:text-muted-foreground focus:ring-1 focus:ring-gray-200 disabled:bg-muted disabled:pointer-events-none select-none items-center justify-center gap-x-2 whitespace-nowrap rounded-md border",
"inline-flex border font-medium disabled:text-muted-foreground focus:ring-1 focus:ring-gray-200 disabled:bg-muted disabled:pointer-events-none select-none items-center justify-center whitespace-nowrap rounded-md border",
{
variants: {
variant: {
@ -22,8 +22,8 @@ const buttonVariants = cva(
link: "underline-offset-4 hover:underline text-primary",
},
size: {
default: "h-9 px-2.5 text-sm",
sm: "h-7 text-xs px-1.5 rounded-md",
default: "h-9 px-2.5 gap-x-2.5 text-sm",
sm: "h-7 text-xs px-2 gap-x-1.5 rounded-md",
lg: "h-11 text-base px-4 rounded-md",
},
},
@ -78,7 +78,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
{loading ? (
<SpinnerIcon className="inline-block h-4 w-4 animate-spin" />
) : Icon ? (
<Icon className="h-4 w-4" />
<Icon className="-ml-0.5 h-4 w-4" />
) : null}
{children}
</>

View file

@ -51,7 +51,7 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content
ref={ref}
className={cn(
"animate-in data-[state=open]:fade-in-90 sm:zoom-in-90 shadow-huge fixed z-50 grid w-full gap-4 overflow-hidden bg-white p-5 sm:rounded-md",
"sm:zoom-in-90 shadow-huge fixed z-50 grid w-full gap-4 overflow-hidden bg-white p-5 sm:rounded-md",
{
"sm:max-w-sm": size === "sm",
"sm:max-w-md": size === "md",
@ -62,14 +62,14 @@ const DialogContent = React.forwardRef<
{...props}
>
{children}
<DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none">
{!hideCloseButton ? (
{!hideCloseButton ? (
<DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none">
<>
<XIcon className="h-4 w-4" />
<span className="sr-only">Close</span>
</>
) : null}
</DialogPrimitive.Close>
</DialogPrimitive.Close>
) : null}
</DialogPrimitive.Content>
</DialogPortal>
));

View file

@ -44,7 +44,7 @@ const TabsContent = React.forwardRef<
<TabsPrimitive.Content
ref={ref}
className={cn(
"ring-offset-background focus-visible:ring-ring mt-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2",
"ring-offset-background focus-visible:ring-ring focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2",
className,
)}
{...props}