Reworked the pricing tiers to include a segmented controller and tooltips
This commit is contained in:
@@ -3,6 +3,9 @@ import { Paragraph } from "../primitives/Paragraph";
|
||||
import * as Slider from "@radix-ui/react-slider";
|
||||
import { Button } from "../primitives/Buttons";
|
||||
import { CheckIcon, XMarkIcon } from "@heroicons/react/24/solid";
|
||||
import SegmentedControl from "../primitives/SegmentedControl";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../primitives/Tooltip";
|
||||
import { Header3 } from "../primitives/Headers";
|
||||
|
||||
export function PricingTiers({
|
||||
children,
|
||||
@@ -27,15 +30,21 @@ export function TierFree() {
|
||||
return (
|
||||
<TierContainer>
|
||||
<Header title="Free" flatCost={0} />
|
||||
<TierLimit description="1 concurrent Run / month" />
|
||||
<TierLimit>
|
||||
Up to 5{" "}
|
||||
<DefinitionTip title="Concurrent Runs" content="Description">
|
||||
Concurrent Runs
|
||||
</DefinitionTip>
|
||||
</TierLimit>
|
||||
<Button variant="secondary/large" fullWidth className="text-md my-6 font-medium">
|
||||
Current Plan
|
||||
</Button>
|
||||
<ul className="flex flex-col gap-2.5">
|
||||
<FeatureItem checked title="Up to 2 team members" />
|
||||
<FeatureItem checked title="Up to 10 Jobs" />
|
||||
<FeatureItem checked title="Unlimited Job Runs" />
|
||||
<FeatureItem checked title="Unlimited Run duration" />
|
||||
<FeatureItem checked title="Up to 10k Job Runs" />
|
||||
<FeatureItem checked title="Unlimited Jobs" />
|
||||
<FeatureItem checked title="Unlimited Tasks" />
|
||||
<FeatureItem checked title="Unlimited Events" />
|
||||
<FeatureItem checked title="Unlimited team members" />
|
||||
<FeatureItem checked title="24 hour log retention" />
|
||||
<FeatureItem checked title="Community support" />
|
||||
<FeatureItem title="Custom integrations" />
|
||||
@@ -51,15 +60,20 @@ export function TierPro() {
|
||||
return (
|
||||
<TierContainer isHighlighted>
|
||||
<Header title="Pro" isHighlighted flatCost={25} />
|
||||
<TierLimit pricedMetric description="Up to 5 concurrent Runs / month" />
|
||||
<TierLimit pricedMetric>
|
||||
<DefinitionTip title="Concurrent Runs" content="Description">
|
||||
Concurrent Runs
|
||||
</DefinitionTip>
|
||||
</TierLimit>
|
||||
<Button variant="primary/large" fullWidth className="text-md my-6 font-medium">
|
||||
Upgrade
|
||||
</Button>
|
||||
<ul className="flex flex-col gap-2.5">
|
||||
<FeatureItem checked title="Unlimited team members" />
|
||||
<FeatureItem checked title="Includes 10k Job Runs, then < $1.30/1,000 Runs" />
|
||||
<FeatureItem checked title="Unlimited Jobs" />
|
||||
<FeatureItem checked title="Unlimited Job Runs" />
|
||||
<FeatureItem checked title="Unlimited Run duration" />
|
||||
<FeatureItem checked title="Unlimited Tasks" />
|
||||
<FeatureItem checked title="Unlimited Events" />
|
||||
<FeatureItem checked title="Unlimited team members" />
|
||||
<FeatureItem checked title="7 day log retention" />
|
||||
<FeatureItem checked title="Dedicated Slack support" />
|
||||
<FeatureItem title="Custom integrations" />
|
||||
@@ -75,15 +89,21 @@ export function TierEnterprise() {
|
||||
return (
|
||||
<TierContainer>
|
||||
<Header title="Enterprise" />
|
||||
<TierLimit description="Flexible concurrent Runs / month" />
|
||||
<TierLimit>
|
||||
Flexible{" "}
|
||||
<DefinitionTip title="Concurrent Runs" content="Description">
|
||||
concurrent Runs
|
||||
</DefinitionTip>
|
||||
</TierLimit>
|
||||
<Button variant="secondary/large" fullWidth className="text-md my-6 font-medium">
|
||||
Contact us
|
||||
</Button>
|
||||
<ul className="flex flex-col gap-2.5">
|
||||
<FeatureItem checked title="Unlimited team members" />
|
||||
<FeatureItem checked title="Flexible Job Runs" />
|
||||
<FeatureItem checked title="Unlimited Jobs" />
|
||||
<FeatureItem checked title="Unlimited Job Runs" />
|
||||
<FeatureItem checked title="Unlimited Run duration" />
|
||||
<FeatureItem checked title="Unlimited Tasks" />
|
||||
<FeatureItem checked title="Unlimited Events" />
|
||||
<FeatureItem checked title="Unlimited team members" />
|
||||
<FeatureItem checked title="30 day log retention" />
|
||||
<FeatureItem checked title="Priority support" />
|
||||
<FeatureItem checked title="Custom integrations" />
|
||||
@@ -114,6 +134,36 @@ function TierContainer({
|
||||
);
|
||||
}
|
||||
|
||||
function DefinitionTip({
|
||||
content,
|
||||
children,
|
||||
title,
|
||||
}: {
|
||||
content: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
title: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip disableHoverableContent>
|
||||
<TooltipTrigger>
|
||||
<span className="underline decoration-slate-600 decoration-dashed underline-offset-4 transition hover:decoration-slate-500">
|
||||
{children}
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent align="end" side="right" variant="dark" className="">
|
||||
<Header3 className="mb-1">{title}</Header3>
|
||||
{typeof content === "string" ? (
|
||||
<Paragraph variant="small">{content}</Paragraph>
|
||||
) : (
|
||||
<div>{content}</div>
|
||||
)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function Header({
|
||||
title,
|
||||
flatCost,
|
||||
@@ -140,18 +190,41 @@ function Header({
|
||||
);
|
||||
}
|
||||
|
||||
function TierLimit({ description, pricedMetric }: { description: string; pricedMetric?: boolean }) {
|
||||
function TierLimit({
|
||||
children,
|
||||
pricedMetric,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
pricedMetric?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
{pricedMetric ? <PricingSlider /> : <hr className="my-[1.6rem]" />}
|
||||
<Paragraph variant="small/bright" className="">
|
||||
{description}
|
||||
</Paragraph>
|
||||
{pricedMetric ? (
|
||||
<>
|
||||
<Paragraph variant="small/bright" className="mb-2 mt-6">
|
||||
{children}
|
||||
</Paragraph>
|
||||
<SegmentedControl name={"Concurrent Runs"} options={options} fullWidth />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<hr className="my-[1.9rem]" />
|
||||
<Paragraph variant="small/bright" className="mb-[0.6rem]">
|
||||
{children}
|
||||
</Paragraph>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PricingSlider() {
|
||||
const options = [
|
||||
{ label: "Up to 20", value: "20" },
|
||||
{ label: "Up to 50", value: "50" },
|
||||
{ label: "Up to 100", value: "100" },
|
||||
];
|
||||
|
||||
function UsageSlider() {
|
||||
return (
|
||||
<form>
|
||||
<Slider.Root
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
import { RadioGroup } from "@headlessui/react";
|
||||
import { cn } from "~/utils/cn";
|
||||
|
||||
type Options = {
|
||||
label: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
type FormSegmentedControlProps = {
|
||||
name: string;
|
||||
defaultValue?: string;
|
||||
options: Options[];
|
||||
onChange?: (value: string) => void;
|
||||
};
|
||||
|
||||
export default function FormSegmentedControl({
|
||||
name,
|
||||
defaultValue,
|
||||
options,
|
||||
onChange,
|
||||
}: FormSegmentedControlProps) {
|
||||
return (
|
||||
<div className="flex h-8 w-fit rounded bg-slate-800 p-1">
|
||||
<RadioGroup
|
||||
defaultValue={defaultValue ?? options[0].value}
|
||||
name={name}
|
||||
onChange={(c: string) => {
|
||||
if (onChange) {
|
||||
onChange(c);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex gap-x-1.5">
|
||||
{options.map((option) => (
|
||||
<RadioGroup.Option
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
className={({ active, checked }) =>
|
||||
cn(
|
||||
"relative flex cursor-pointer rounded-[2px] px-3 py-[0.13rem] shadow-md focus:outline-none",
|
||||
active
|
||||
? "focus-visible:ring focus-visible:ring-indigo-500 focus-visible:ring-opacity-60"
|
||||
: "",
|
||||
checked
|
||||
? "bg-slate-700 text-bright"
|
||||
: "bg-transparent transition hover:bg-slate-750"
|
||||
)
|
||||
}
|
||||
>
|
||||
{({ checked }) => (
|
||||
<>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<div className="text-sm">
|
||||
<RadioGroup.Label
|
||||
as="p"
|
||||
className={cn("font-normal", checked ? "text-bright" : "text-dimmed")}
|
||||
>
|
||||
{option.label}
|
||||
</RadioGroup.Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</RadioGroup.Option>
|
||||
))}
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { RadioGroup } from "@headlessui/react";
|
||||
import { motion } from "framer-motion";
|
||||
import { cn } from "~/utils/cn";
|
||||
|
||||
type Options = {
|
||||
label: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
type SegmentedControlProps = {
|
||||
name: string;
|
||||
defaultValue?: string;
|
||||
options: Options[];
|
||||
fullWidth?: boolean;
|
||||
onChange?: (value: string) => void;
|
||||
};
|
||||
|
||||
export default function SegmentedControl({
|
||||
name,
|
||||
defaultValue,
|
||||
options,
|
||||
fullWidth,
|
||||
onChange,
|
||||
}: SegmentedControlProps) {
|
||||
return (
|
||||
<div className={cn("flex h-10 rounded bg-slate-850", fullWidth ? "w-full" : "w-fit")}>
|
||||
<RadioGroup
|
||||
defaultValue={defaultValue ?? options[0].value}
|
||||
name={name}
|
||||
onChange={(c: string) => {
|
||||
if (onChange) {
|
||||
onChange(c);
|
||||
}
|
||||
}}
|
||||
className="w-full"
|
||||
>
|
||||
<div className="flex h-full w-full items-center justify-between">
|
||||
{options.map((option) => (
|
||||
<RadioGroup.Option
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
className={({ active, checked }) =>
|
||||
cn(
|
||||
"relative flex h-full grow cursor-pointer rounded-[2px] font-normal focus:outline-none",
|
||||
active
|
||||
? "ring-offset-2 focus-visible:ring focus-visible:ring-indigo-500 focus-visible:ring-opacity-60"
|
||||
: "",
|
||||
checked ? "text-bright" : "text-dimmed transition hover:text-bright"
|
||||
)
|
||||
}
|
||||
>
|
||||
{({ checked }) => (
|
||||
<>
|
||||
<div className="relative flex h-full w-full items-center justify-between px-3 py-[0.13rem]">
|
||||
<div className="flex h-full w-full items-center justify-center text-sm">
|
||||
<RadioGroup.Label as="p">{option.label}</RadioGroup.Label>
|
||||
</div>
|
||||
{checked && (
|
||||
<motion.div
|
||||
layoutId={`segmented-control-${name}`}
|
||||
transition={{ duration: 0.4, type: "spring" }}
|
||||
className="absolute left-0 top-0 h-full w-full rounded-md border-4 border-indigo-600 shadow"
|
||||
></motion.div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</RadioGroup.Option>
|
||||
))}
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
import { cn } from "~/utils/cn";
|
||||
|
||||
const variantClasses = {
|
||||
basic:
|
||||
"bg-popover border border-slate-800 rounded-md px-3 py-1.5 text-sm text-bright shadow-md fade-in-50",
|
||||
dark: "bg-background border border-border rounded px-3 py-2 text-sm text-bright shadow-md fade-in-50",
|
||||
};
|
||||
|
||||
type Variant = keyof typeof variantClasses;
|
||||
|
||||
const TooltipProvider = TooltipPrimitive.Provider;
|
||||
|
||||
const TooltipArrow = React.forwardRef<
|
||||
@@ -22,15 +28,20 @@ Tooltip.displayName = TooltipPrimitive.Root.displayName;
|
||||
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger;
|
||||
|
||||
type TooltipContentProps = {
|
||||
variant?: Variant;
|
||||
} & React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>;
|
||||
|
||||
const TooltipContent = React.forwardRef<
|
||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
TooltipContentProps
|
||||
>(({ className, sideOffset = 4, variant = "basic", ...props }, ref) => (
|
||||
<TooltipPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 overflow-hidden rounded-md border border-slate-800 bg-popover px-3 py-1.5 text-sm text-bright shadow-md animate-in fade-in-50 data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1",
|
||||
"z-50 overflow-hidden animate-in data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1",
|
||||
variantClasses[variant],
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -43,17 +54,19 @@ function SimpleTooltip({
|
||||
content,
|
||||
side,
|
||||
hidden,
|
||||
variant,
|
||||
}: {
|
||||
button: React.ReactNode;
|
||||
content: React.ReactNode;
|
||||
side?: React.ComponentProps<typeof TooltipContent>["side"];
|
||||
hidden?: boolean;
|
||||
variant?: Variant;
|
||||
}) {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>{button}</TooltipTrigger>
|
||||
<TooltipContent side={side} hidden={hidden} className="text-xs">
|
||||
<TooltipContent side={side} hidden={hidden} className="text-xs" variant={variant}>
|
||||
{content}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { PricingTiers, TierEnterprise, TierFree, TierPro } from "../billing/PricingTiers";
|
||||
|
||||
const meta: Meta<typeof AllPricingTiers> = {
|
||||
title: "Components/PricingTiers",
|
||||
title: "Billing/PricingTiers",
|
||||
component: AllPricingTiers,
|
||||
};
|
||||
|
||||
@@ -16,8 +16,8 @@ export const AllTiers: Story = {
|
||||
|
||||
function AllPricingTiers() {
|
||||
return (
|
||||
<div className="mx-4 flex h-screen items-center justify-center">
|
||||
<PricingTiers className="">
|
||||
<div className="mx-4 flex h-screen flex-col items-center justify-center gap-4">
|
||||
<PricingTiers className="w-[80rem]">
|
||||
<TierFree />
|
||||
<TierPro />
|
||||
<TierEnterprise />
|
||||
|
||||
+8
-7
@@ -1,19 +1,20 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { withDesign } from "storybook-addon-designs";
|
||||
import FormSegmentedControl from "../primitives/FormSegmentedControl";
|
||||
import { MainCenteredContainer } from "../layout/AppLayout";
|
||||
import SegmentedControl from "../primitives/SegmentedControl";
|
||||
|
||||
const meta: Meta = {
|
||||
title: "Primitives/FormSegmentedControl",
|
||||
const meta: Meta<typeof StyledSegmentedControl> = {
|
||||
title: "Primitives/SegmentedControl",
|
||||
decorators: [withDesign],
|
||||
component: StyledSegmentedControl,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof FormSegmentedControl>;
|
||||
type Story = StoryObj<typeof StyledSegmentedControl>;
|
||||
|
||||
export const Basic: Story = {
|
||||
render: () => <SegmentedControl />,
|
||||
render: () => <StyledSegmentedControl />,
|
||||
};
|
||||
|
||||
Basic.parameters = {
|
||||
@@ -28,10 +29,10 @@ const options = [
|
||||
{ label: "Label 2", value: "Users" },
|
||||
];
|
||||
|
||||
function SegmentedControl() {
|
||||
function StyledSegmentedControl() {
|
||||
return (
|
||||
<MainCenteredContainer>
|
||||
<FormSegmentedControl name="name" options={options} />
|
||||
<SegmentedControl name="name" options={options} />
|
||||
</MainCenteredContainer>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user