From 82d7ab1bdc9a5e02ae719e04a364d7601fcfd7b9 Mon Sep 17 00:00:00 2001 From: Amp Date: Wed, 16 Sep 2026 07:34:25 +0000 Subject: [PATCH] Pricing: dashboard plan badges and tabular spec rows Port the dashboard's PlanBadge to the /cloud pricing cards and comparison table. Replace the check-list features with hairline label | value rows, drop the Enterprise Edition ink header, and remove the MFA row. Amp-Thread-ID: https://ampcode.com/threads/T-01a0a912-5ef1-771b-a37a-182c62c1eb56 Co-authored-by: Nicholas Kissel --- .../marketing/pricing/PlanBadge.tsx | 33 ++++ .../marketing/pricing/PricingPageClient.tsx | 167 ++++++++++-------- 2 files changed, 123 insertions(+), 77 deletions(-) create mode 100644 src/components/marketing/pricing/PlanBadge.tsx diff --git a/src/components/marketing/pricing/PlanBadge.tsx b/src/components/marketing/pricing/PlanBadge.tsx new file mode 100644 index 0000000..a43368a --- /dev/null +++ b/src/components/marketing/pricing/PlanBadge.tsx @@ -0,0 +1,33 @@ +// Plan tag, ported from the dashboard's `PlanBadge` +// (frontend/src/app/billing/billing-plan-badge.tsx in rivet-dev/actors) so the +// website's pricing page and the billing UI label plans identically. + +export type PlanKey = 'free' | 'hobby' | 'team' | 'enterprise'; + +// One accent color per plan. Tailwind's JIT needs the full class strings to +// exist literally, so each plan spells out its classes. These are the +// dashboard's light-mode variants; the pricing page has no ink surface. +const PLAN_COLORS: Record = { + free: 'border-emerald-500/40 bg-emerald-500/10 text-emerald-700', + hobby: 'border-orange-500/40 bg-orange-500/10 text-orange-700', + team: 'border-blue-500/40 bg-blue-500/10 text-blue-700', + enterprise: 'border-purple-500/40 bg-purple-500/10 text-purple-700', +}; + +export function PlanBadge({ + plan, + children, + className = '', +}: { + plan: PlanKey; + children: string; + className?: string; +}) { + return ( + + {children} + + ); +} diff --git a/src/components/marketing/pricing/PricingPageClient.tsx b/src/components/marketing/pricing/PricingPageClient.tsx index 20372c3..b01f70b 100644 --- a/src/components/marketing/pricing/PricingPageClient.tsx +++ b/src/components/marketing/pricing/PricingPageClient.tsx @@ -3,7 +3,6 @@ import { useId, useState } from 'react'; import { ArrowRight, - Check, ShieldCheck, Cpu, MemoryStick, @@ -20,6 +19,7 @@ import imgA16z from '@/images/logos/a16z.svg'; import { canonicalizeInternalHref } from '@/lib/internalHref'; import { CARD_TITLE_CLASS, + EYEBROW_CLASS, EYEBROW_ON_INK_CLASS, PRODUCT_HERO_PRIMARY_BUTTON_CLASS, PRODUCT_HERO_SECONDARY_BUTTON_CLASS, @@ -40,6 +40,7 @@ import { import { InkPanel } from '@/components/marketing/editorial/InkPanel'; import { SectionRule } from '@/components/marketing/SectionRule'; import { DeploymentDiagram, type DeploymentDiagramVariant } from '@/components/marketing/diagrams/deploymentDiagrams'; +import { PlanBadge, type PlanKey } from '@/components/marketing/pricing/PlanBadge'; // --- Page Sections --- @@ -178,17 +179,9 @@ const ComparisonTable = () => { { name: "Writes / mo", free: "5 Million max", hobby: "50 Million included", team: "50 Million included" }, { name: "Egress", free: "100GB max", hobby: "1TB included", team: "1TB included" }, { name: "Support", free: "Community", hobby: "Email", team: "Slack & Email" }, - { name: "MFA", free: false, hobby: false, team: true }, ]; - const renderCell = (value) => { - if (typeof value === 'boolean') { - return value ? -
: -
; - } - return {value}; - }; + const renderCell = (value: string) => {value}; return (
@@ -198,9 +191,9 @@ const ComparisonTable = () => { Feature - Free - Hobby - Team + Free + Hobby + Team @@ -380,71 +373,85 @@ const ComputeCalculator = () => { ); }; +// One row of a plan card's spec table. A `value` renders a two-column +// label | value row; without one the label spans the row (used for the +// Enterprise Edition capability list, which has no per-item quantity). +interface PlanRow { + label: string; + value?: string; +} + interface Plan { + key: PlanKey; name: string; prefix?: string; price: string; period: string; desc: string; - features: string[]; + rows: PlanRow[]; cta: string; highlight: boolean; - inkHeader?: boolean; + /** Quiet label shown opposite the badge (e.g. "On-Prem"). */ + tag?: string; } const Pricing = () => { const cloudPlans: Plan[] = [ { + key: "free", name: "Free", price: "$0", period: "/mo", desc: "For prototyping and small projects.", - features: [ - "100,000 Awake Actor Hours /mo limit", - "$5 /mo Compute limit", - "1 vCPU Max", - "5GB Limit", - "5 Million Writes /mo Limit", - "200 Million Reads /mo Limit", - "100GB Egress Limit", - "Community Support" + rows: [ + { label: "Awake Actor Hours", value: "100,000 /mo max" }, + { label: "Compute", value: "$5 /mo max" }, + { label: "Max vCPU", value: "1" }, + { label: "Storage", value: "5GB max" }, + { label: "Reads", value: "200M /mo max" }, + { label: "Writes", value: "5M /mo max" }, + { label: "Egress", value: "100GB max" }, + { label: "Support", value: "Community" }, ], cta: "Get Started", highlight: false }, { + key: "hobby", name: "Hobby", prefix: "From", price: "$20", period: "/mo + Usage", desc: "For scaling applications.", - features: [ - "400,000 Awake Actor Hours Included", - "Up to 8 vCPU", - "25 Billion Reads /mo included", - "50 Million Writes /mo included", - "5GB Storage included", - "1TB Egress included", - "Email Support" + rows: [ + { label: "Awake Actor Hours", value: "400,000 /mo" }, + { label: "Compute", value: "Usage-based" }, + { label: "Max vCPU", value: "8" }, + { label: "Storage", value: "5GB" }, + { label: "Reads", value: "25B /mo" }, + { label: "Writes", value: "50M /mo" }, + { label: "Egress", value: "1TB" }, + { label: "Support", value: "Email" }, ], cta: "Get Started", highlight: true }, { + key: "team", name: "Team", prefix: "From", price: "$200", period: "/mo + Usage", desc: "For growing teams and businesses.", - features: [ - "400,000 Awake Actor Hours Included", - "Up to 8 vCPU", - "25 Billion Reads /mo included", - "50 Million Writes /mo included", - "5GB Storage included", - "1TB Egress included", - "MFA", - "Slack Support" + rows: [ + { label: "Awake Actor Hours", value: "400,000 /mo" }, + { label: "Compute", value: "Usage-based" }, + { label: "Max vCPU", value: "8" }, + { label: "Storage", value: "5GB" }, + { label: "Reads", value: "25B /mo" }, + { label: "Writes", value: "50M /mo" }, + { label: "Egress", value: "1TB" }, + { label: "Support", value: "Slack & Email" }, ], cta: "Get Started", highlight: false @@ -454,32 +461,33 @@ const Pricing = () => { // Enterprise Edition is the self-hosted, on-prem offering. It is the only // enterprise tier, so it is shown on both toggle states: alongside the // self-hosted plans, and appended to the cloud plans so it stays visible by - // default (it keeps its "Self-Hosted" header tag in either view). + // default (it keeps its "On-Prem" tag in either view). const enterpriseEditionPlan: Plan = { + key: "enterprise", name: "Enterprise Edition", price: "Custom", period: "", desc: "Additional operational features for running Rivet in your own VPC, customer environments, or regulated networks.", - features: [ - "Actor control plane", - "FoundationDB persistence layer", - "Cloud layer for multi-tenant", - "SQLite backup", - "SQLite PITR", - "Forking", - "ACL system", - "ACL for agents", - "Advanced ClickHouse analytics", - "OpenTelemetry integration", - "Alert manager rules, Prometheus rules, Grafana configs", - "Kubernetes manifests", - "Air-gapped & sovereign-cloud deployments", - "Priority support & SLA", - "Hardening guidance for FedRAMP, HIPAA, regulated industries" + rows: [ + { label: "Actor control plane" }, + { label: "FoundationDB persistence layer" }, + { label: "Cloud layer for multi-tenant" }, + { label: "SQLite backup" }, + { label: "SQLite PITR" }, + { label: "Forking" }, + { label: "ACL system" }, + { label: "ACL for agents" }, + { label: "Advanced ClickHouse analytics" }, + { label: "OpenTelemetry integration" }, + { label: "Alert manager rules, Prometheus rules, Grafana configs" }, + { label: "Kubernetes manifests" }, + { label: "Air-gapped & sovereign-cloud deployments" }, + { label: "Priority support & SLA" }, + { label: "Hardening guidance for FedRAMP, HIPAA, regulated industries" }, ], cta: "Contact Sales", highlight: false, - inkHeader: true + tag: "On-Prem" }; const plans = [...cloudPlans, enterpriseEditionPlan]; @@ -507,19 +515,18 @@ const Pricing = () => { plan.highlight ? 'border-pine/60' : 'border-ink/10' }`} > - {plan.inkHeader ? ( -
- {plan.name} - On-Prem -
- ) : null}
- {!plan.inkHeader ? ( -

{plan.name}

- ) : null} +

+ {plan.name} + {plan.tag ? {plan.tag} : null} +

- {plan.prefix && {plan.prefix}} + {/* Always reserve the prefix line so prices and the + spec rows below stay level across cards. */} + + {plan.prefix ?? 'From'} +
{plan.price} {plan.period && {plan.period}} @@ -528,16 +535,22 @@ const Pricing = () => {
- {plan.desc &&

{plan.desc}

} - -
- {plan.features.map((feat, i) => ( -
- - {feat} + {/* min-height is exactly two lines at 15px/1.625 so one-line descriptions + don't pull the spec rows out of line with neighboring cards. */} + {plan.desc &&

{plan.desc}

} + + {/* Spec table: hairline-divided label | value rows, so + the four cards scan like columns of one table. */} +
+ {plan.rows.map((row) => ( +
+
{row.label}
+ {row.value ? ( +
{row.value}
+ ) : null}
))} -
+