From c4d54d2e08fc49368c2f1022ea2c3948d9130197 Mon Sep 17 00:00:00 2001 From: James Ritchie Date: Wed, 22 Nov 2023 16:53:49 +0000 Subject: [PATCH] Reworked the pricing tiers to include a segmented controller and tooltips --- .../app/components/billing/PricingTiers.tsx | 111 +++++++++++++++--- .../primitives/FormSegmentedControl.tsx | 72 ------------ .../primitives/SegmentedControl.tsx | 74 ++++++++++++ .../app/components/primitives/Tooltip.tsx | 25 +++- .../stories/PricingTiers.stories.tsx | 6 +- ...ories.tsx => SegmentedControl.stories.tsx} | 15 +-- 6 files changed, 196 insertions(+), 107 deletions(-) delete mode 100644 apps/webapp/app/components/primitives/FormSegmentedControl.tsx create mode 100644 apps/webapp/app/components/primitives/SegmentedControl.tsx rename apps/webapp/app/components/stories/{FormSegmentedControl.stories.tsx => SegmentedControl.stories.tsx} (62%) diff --git a/apps/webapp/app/components/billing/PricingTiers.tsx b/apps/webapp/app/components/billing/PricingTiers.tsx index cdb2a0c6d..4f41d7272 100644 --- a/apps/webapp/app/components/billing/PricingTiers.tsx +++ b/apps/webapp/app/components/billing/PricingTiers.tsx @@ -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 (
- + + Up to 5{" "} + + Concurrent Runs + +
    - - - - + + + + + @@ -51,15 +60,20 @@ export function TierPro() { return (
    - + + + Concurrent Runs + +
      - + - - + + + @@ -75,15 +89,21 @@ export function TierEnterprise() { return (
      - + + Flexible{" "} + + concurrent Runs + +
        - + - - + + + @@ -114,6 +134,36 @@ function TierContainer({ ); } +function DefinitionTip({ + content, + children, + title, +}: { + content: React.ReactNode; + children: React.ReactNode; + title: React.ReactNode; +}) { + return ( + + + + + {children} + + + + {title} + {typeof content === "string" ? ( + {content} + ) : ( +
        {content}
        + )} +
        +
        +
        + ); +} + 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 (
        - {pricedMetric ? :
        } - - {description} - + {pricedMetric ? ( + <> + + {children} + + + + ) : ( + <> +
        + + {children} + + + )}
        ); } -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 (
        void; -}; - -export default function FormSegmentedControl({ - name, - defaultValue, - options, - onChange, -}: FormSegmentedControlProps) { - return ( -
        - { - if (onChange) { - onChange(c); - } - }} - > -
        - {options.map((option) => ( - - 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 }) => ( - <> -
        -
        -
        - - {option.label} - -
        -
        -
        - - )} -
        - ))} -
        -
        -
        - ); -} diff --git a/apps/webapp/app/components/primitives/SegmentedControl.tsx b/apps/webapp/app/components/primitives/SegmentedControl.tsx new file mode 100644 index 000000000..da12092fa --- /dev/null +++ b/apps/webapp/app/components/primitives/SegmentedControl.tsx @@ -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 ( +
        + { + if (onChange) { + onChange(c); + } + }} + className="w-full" + > +
        + {options.map((option) => ( + + 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 }) => ( + <> +
        +
        + {option.label} +
        + {checked && ( + + )} +
        + + )} +
        + ))} +
        +
        +
        + ); +} diff --git a/apps/webapp/app/components/primitives/Tooltip.tsx b/apps/webapp/app/components/primitives/Tooltip.tsx index 6031caa63..7b9d4625f 100644 --- a/apps/webapp/app/components/primitives/Tooltip.tsx +++ b/apps/webapp/app/components/primitives/Tooltip.tsx @@ -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; + const TooltipContent = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, sideOffset = 4, ...props }, ref) => ( + TooltipContentProps +>(({ className, sideOffset = 4, variant = "basic", ...props }, ref) => ( ["side"]; hidden?: boolean; + variant?: Variant; }) { return ( {button} - diff --git a/apps/webapp/app/components/stories/PricingTiers.stories.tsx b/apps/webapp/app/components/stories/PricingTiers.stories.tsx index f5bd8b939..eaff255f0 100644 --- a/apps/webapp/app/components/stories/PricingTiers.stories.tsx +++ b/apps/webapp/app/components/stories/PricingTiers.stories.tsx @@ -2,7 +2,7 @@ import type { Meta, StoryObj } from "@storybook/react"; import { PricingTiers, TierEnterprise, TierFree, TierPro } from "../billing/PricingTiers"; const meta: Meta = { - title: "Components/PricingTiers", + title: "Billing/PricingTiers", component: AllPricingTiers, }; @@ -16,8 +16,8 @@ export const AllTiers: Story = { function AllPricingTiers() { return ( -
        - +
        + diff --git a/apps/webapp/app/components/stories/FormSegmentedControl.stories.tsx b/apps/webapp/app/components/stories/SegmentedControl.stories.tsx similarity index 62% rename from apps/webapp/app/components/stories/FormSegmentedControl.stories.tsx rename to apps/webapp/app/components/stories/SegmentedControl.stories.tsx index 1357a7352..e4acc2577 100644 --- a/apps/webapp/app/components/stories/FormSegmentedControl.stories.tsx +++ b/apps/webapp/app/components/stories/SegmentedControl.stories.tsx @@ -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 = { + title: "Primitives/SegmentedControl", decorators: [withDesign], + component: StyledSegmentedControl, }; export default meta; -type Story = StoryObj; +type Story = StoryObj; export const Basic: Story = { - render: () => , + render: () => , }; Basic.parameters = { @@ -28,10 +29,10 @@ const options = [ { label: "Label 2", value: "Users" }, ]; -function SegmentedControl() { +function StyledSegmentedControl() { return ( - + ); }