From e45635aab68197bc675d7d752d27c716d90b9b39 Mon Sep 17 00:00:00 2001 From: Jake Howell Date: Fri, 30 Jan 2026 23:44:07 +1100 Subject: [PATCH] fix: refactor `` component to be universal (#21740) During development of #21659 I approved some `` code that had an extensive props system, however, I wasn't a huge fan of this. This approach attempts to take it further like something `shadcn` would, where-in we define the `` (and its subset of components) and we wrap around those when needed for `` and ``. Theoretically there is no real CSS/Design changes here. However screenshot for prosperity. | Previously | Now | | --- | --- | | CleanShot 2026-01-29 at 10 56
05@2x | CleanShot 2026-01-29 at 10 55
05@2x | --------- Co-authored-by: Ben Potter Co-authored-by: Claude Opus 4.5 --- site/src/components/Badges/Badges.tsx | 19 +- .../Paywall/AIBridgePaywallConfig.ts | 27 -- .../components/Paywall/Paywall.stories.tsx | 52 ++- site/src/components/Paywall/Paywall.tsx | 300 +++++++++++------- .../Paywall/PaywallAIGovernance.stories.tsx | 12 + .../Paywall/PaywallAIGovernance.tsx | 63 ++++ .../Paywall/PaywallPremium.stories.tsx | 19 ++ .../src/components/Paywall/PaywallPremium.tsx | 87 +++++ .../Paywall/PopoverPaywall.stories.tsx | 1 + .../src/components/Paywall/PopoverPaywall.tsx | 133 +------- site/src/index.css | 2 + .../RequestLogsPage/RequestLogsPageView.tsx | 5 +- site/src/pages/AuditPage/AuditPageView.tsx | 4 +- .../ConnectionLogPageView.tsx | 4 +- .../AIGovernanceSettingsPageView.tsx | 5 +- .../IdpOrgSyncPage/IdpOrgSyncPage.tsx | 4 +- site/src/pages/GroupsPage/GroupsPageView.tsx | 4 +- .../CreateOrganizationPageView.tsx | 4 +- .../CustomRolesPage/CustomRolesPageView.tsx | 4 +- .../IdpSyncPage/IdpSyncPage.tsx | 4 +- .../OrganizationProvisionerKeysPageView.tsx | 4 +- .../OrganizationProvisionersPageView.tsx | 4 +- .../TemplatePermissionsPage.tsx | 4 +- site/tailwind.config.js | 1 + 24 files changed, 451 insertions(+), 315 deletions(-) delete mode 100644 site/src/components/Paywall/AIBridgePaywallConfig.ts create mode 100644 site/src/components/Paywall/PaywallAIGovernance.stories.tsx create mode 100644 site/src/components/Paywall/PaywallAIGovernance.tsx create mode 100644 site/src/components/Paywall/PaywallPremium.stories.tsx create mode 100644 site/src/components/Paywall/PaywallPremium.tsx diff --git a/site/src/components/Badges/Badges.tsx b/site/src/components/Badges/Badges.tsx index 07b653e7f4..90ae6dc506 100644 --- a/site/src/components/Badges/Badges.tsx +++ b/site/src/components/Badges/Badges.tsx @@ -1,4 +1,5 @@ import type { Interpolation, Theme } from "@emotion/react"; +import { Badge } from "components/Badge/Badge"; import { Stack } from "components/Stack/Stack"; import { Tooltip, @@ -11,6 +12,7 @@ import { type HTMLAttributes, type PropsWithChildren, } from "react"; +import { cn } from "utils/cn"; const styles = { badge: { @@ -145,18 +147,15 @@ export const PremiumBadge: FC = ({ children = "Premium", }) => { return ( - ({ - backgroundColor: theme.branding.premium.background, - border: `1px solid ${theme.branding.premium.border}`, - color: theme.branding.premium.text, - }), - ]} + {children} - + ); }; diff --git a/site/src/components/Paywall/AIBridgePaywallConfig.ts b/site/src/components/Paywall/AIBridgePaywallConfig.ts deleted file mode 100644 index f7641453f2..0000000000 --- a/site/src/components/Paywall/AIBridgePaywallConfig.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { ComponentProps } from "react"; -import type { Paywall } from "./Paywall"; - -// Centralized AI Bridge paywall configuration to avoid duplication. -export const aiBridgePaywallConfig: Omit, ""> = { - message: "AI Bridge", - description: - "AI Bridge provides auditable visibility into user prompts and LLM tool calls from developer tools within Coder Workspaces. AI Bridge requires a Premium license with AI Governance add-on.", - documentationLink: "https://coder.com/docs/ai-coder/ai-governance", - documentationLinkText: "Learn about AI Governance", - badgeText: "AI Governance", - ctaText: "Contact Sales", - ctaLink: "https://coder.com/contact", - features: [ - { text: "Auditable history of user prompts" }, - { text: "Logged LLM tool invocations" }, - { text: "Token usage and consumption visibility" }, - { text: "Centrally-managed MCP servers" }, - { - text: "Visit", - link: { - href: "https://coder.com/docs/ai-coder/ai-bridge", - text: "AI Bridge Docs", - }, - }, - ], -}; diff --git a/site/src/components/Paywall/Paywall.stories.tsx b/site/src/components/Paywall/Paywall.stories.tsx index d5ff40d854..ba2e4dcce5 100644 --- a/site/src/components/Paywall/Paywall.stories.tsx +++ b/site/src/components/Paywall/Paywall.stories.tsx @@ -1,5 +1,17 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; -import { Paywall } from "./Paywall"; +import { PremiumBadge } from "components/Badges/Badges"; +import { + Paywall, + PaywallContent, + PaywallCTA, + PaywallDescription, + PaywallFeature, + PaywallFeatures, + PaywallHeading, + PaywallSeparator, + PaywallStack, + PaywallTitle, +} from "./Paywall"; const meta: Meta = { title: "components/Paywall", @@ -9,10 +21,40 @@ const meta: Meta = { export default meta; type Story = StoryObj; -export const Premium: Story = { +export const Default: Story = { args: { - message: "Black Lotus", - description: - "Adds 3 mana of any single color of your choice to your mana pool, then is discarded. Tapping this artifact can be played as an interrupt.", + children: ( + <> + + + Black Lotus + + + + Adds 3 mana of any single color of your choice to your mana pool, + then is discarded. Tapping this artifact can be played as an + interrupt. + + + + + + + High availability & workspace proxies + + + Multi-org & role-based access control + + 24x7 global support with SLA + + Unlimited Git & external auth integrations + + + + Learn about Premium + + + + ), }, }; diff --git a/site/src/components/Paywall/Paywall.tsx b/site/src/components/Paywall/Paywall.tsx index 5236697240..81842c7942 100644 --- a/site/src/components/Paywall/Paywall.tsx +++ b/site/src/components/Paywall/Paywall.tsx @@ -1,134 +1,192 @@ -import type { Interpolation, Theme } from "@emotion/react"; -import Link from "@mui/material/Link"; -import { PremiumBadge } from "components/Badges/Badges"; import { Button } from "components/Button/Button"; -import { Stack } from "components/Stack/Stack"; import { CircleCheckBigIcon } from "lucide-react"; -import type { FC, ReactNode } from "react"; +import type React from "react"; +import type { FC } from "react"; +import { cn } from "utils/cn"; -interface PaywallProps { - message: string; - description?: ReactNode; - documentationLink?: string; - documentationLinkText?: string; - features?: Array<{ - text: string; - link?: { href: string; text: string }; - }>; - badgeText?: string; - ctaText?: string; - ctaLink?: string; -} - -export const Paywall: FC = ({ - message, - description, - documentationLink, - documentationLinkText = "Read the documentation", - features, - badgeText = "Premium", - ctaText = "Learn about Premium", - ctaLink = "https://coder.com/pricing#compare-plans", -}) => { - const defaultFeatures: Array<{ - text: string; - link?: { href: string; text: string }; - }> = [ - { text: "High availability & workspace proxies" }, - { text: "Multi-org & role-based access control" }, - { text: "24x7 global support with SLA" }, - { text: "Unlimited Git & external auth integrations" }, - ]; - - const displayFeatures = features ?? defaultFeatures; +export const Paywall = ({ + className, + children, + ...props +}: React.ComponentProps<"div">) => { return ( -
-
- -
{message}
- {badgeText} -
- - {description && ( -

{description}

- )} - {documentationLink && ( - - {documentationLinkText} - - )} -
-
- -
    - {displayFeatures.map((feature, index) => ( -
  • - - {feature.text} - {feature.link && ( - <> - {" "} - - {feature.link.text} - - - )} -
  • - ))} -
- -
+
+ {children}
); }; -const FeatureIcon: FC = () => { +export const PaywallContent: FC> = ({ + children, + ...props +}) => { + return
{children}
; +}; + +export const PaywallHeading: FC> = ({ + children, + className, + ...props +}) => { return ( -