
3D Extruded Buildings & Urban Footprints
Real-world 3D building extrusions with dynamic sunlight shadows, terrain DEM elevations, and pitch/bearing camera controls.
Pricing collection with eleven interchangeable variants: seat matrix, interactive calculator, enterprise SLA card, feature add-on builder, feature tier slider, grouped matrix, per-plan cards, sticky-header matrix, single plan, tier comparison matrix, and usage calculator slider.
Also available for React ->$pnpm dlx shadcn-vue@latest add https://uipkge.dev/r/vue/pricing.json$npx shadcn-vue@latest add https://uipkge.dev/r/vue/pricing.json$yarn dlx shadcn-vue@latest add https://uipkge.dev/r/vue/pricing.json$bunx shadcn-vue@latest add https://uipkge.dev/r/vue/pricing.jsonnpx shadcn-vue@latest add @uipkge/pricingInstalls to:app/components/blocks/pricing/| Name | Type / Values | Default | Required |
|---|---|---|---|
class | HTMLAttributes['class'] | — | optional |
Type aliases exported from this item's source. Use these to shape the data you pass in.
RequestTierinterface RequestTier {
label: string
requests: number
cost: number
}StorageTierinterface StorageTier {
label: string
gb: number
cost: number
}SlaGuaranteeinterface SlaGuarantee {
title: string
metric: string
description: string
icon: string
}ComplianceCertinterface ComplianceCert {
id: string
name: string
status: string
badgeVariant?: 'default' | 'outline' | 'secondary'
}PricingPlaninterface PricingPlan {
id: string
name: string
monthlyPrice: number
description: string
includedSeats: number
}PricingAddoninterface PricingAddon {
id: string
name: string
monthlyPrice: number
description: string
category: string
}Planinterface Plan {
id: string
name: string
description: string
monthlyPrice: number
annualPrice: number
highlight: boolean
badge?: string
ctaText: string
ctaVariant: 'default' | 'outline' | 'secondary'
}FeatureComparisonRowinterface FeatureComparisonRow {
category: string
features: {
name: string
tooltip: string
community: boolean | string
pro: boolean | string
enterprise: boolean | string
}[]
}<script setup lang="ts">
import { computed, ref } from 'vue'
import type { HTMLAttributes } from 'vue'
import { ArrowRight, Check, ChevronDown, ChevronUp, ShieldCheck, Users, Zap } from 'lucide-vue-next'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
import { Separator } from '@/components/ui/separator'
import { Slider } from '@/components/ui/slider'
import { Switch } from '@/components/ui/switch'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'
import { cn } from '@/lib/utils'
interface Props {
class?: HTMLAttributes['class']
}
defineProps<Props>()
type BillingCycle = 'monthly' | 'yearly'
type Currency = 'USD' | 'EUR' | 'GBP'
const billingCycle = ref<BillingCycle>('yearly')
const currency = ref<Currency>('USD')
const teamSeats = ref<number[]>([12])
const addDedicatedSla = ref(false)
const addAuditVault = ref(false)
const showComparisonTable = ref(false)
const currencySymbols: Record<Currency, string> = {
USD: '$',
EUR: '€',
GBP: '£',
}
const currencyRates: Record<Currency, number> = {
USD: 1.0,
EUR: 0.92,
GBP: 0.79,
}
const rates = computed(() => currencyRates[currency.value])
const sym = computed(() => currencySymbols[currency.value])
// Base tier pricing (per seat / month in USD)
const starterBase = computed(() => (billingCycle.value === 'yearly' ? 12 : 15))
const teamBase = computed(() => (billingCycle.value === 'yearly' ? 28 : 35))
const enterpriseBase = computed(() => (billingCycle.value === 'yearly' ? 68 : 85))
// Addons calculation
const slaCost = computed(() => (addDedicatedSla.value ? 99 : 0))
const vaultCost = computed(() => (addAuditVault.value ? 49 : 0))
// Computed calculated totals
const currentSeats = computed(() => teamSeats.value[0] || 1)
const starterMonthlyTotal = computed(() => {
const base = starterBase.value * currentSeats.value
return Math.round((base + (addAuditVault.value ? 29 : 0)) * rates.value)
})
const teamMonthlyTotal = computed(() => {
const base = teamBase.value * currentSeats.value
return Math.round((base + slaCost.value + vaultCost.value) * rates.value)
})
const enterpriseMonthlyTotal = computed(() => {
const base = enterpriseBase.value * currentSeats.value
return Math.round((base + slaCost.value + vaultCost.value) * rates.value)
})
const annualSavingsTeam = computed(() => {
const monthlyCost = 35 * currentSeats.value * 12
const yearlyCost = 28 * currentSeats.value * 12
return Math.round((monthlyCost - yearlyCost) * rates.value)
})
const comparisonMatrix = [
{
category: 'Platform & Compute Core',
features: [
{ name: 'Active Team Members', starter: 'Up to 10', team: 'Unlimited', enterprise: 'Unlimited + Org Units' },
{ name: 'Monthly API Requests', starter: '100,000 / mo', team: '2,500,000 / mo', enterprise: 'Custom Unlimited' },
{ name: 'Workflow Automations', starter: '10 active', team: '100 active', enterprise: 'Unlimited real-time' },
{ name: 'Data Retention History', starter: '30 days', team: '365 days', enterprise: '7 years immutable' },
],
},
{
category: 'Security & Enterprise Governance',
features: [
{
name: 'SOC 2 Type II & ISO 27001',
starter: 'Standard',
team: 'Included',
enterprise: 'Included + Auditor Portal',
},
{ name: 'SAML SSO & SCIM Provisioning', starter: '—', team: 'Google / Okta', enterprise: 'Custom IdP + SCIM v2' },
{
name: 'Role-Based Access (RBAC)',
starter: '3 predefined roles',
team: 'Granular permissions',
enterprise: 'Custom policy engine',
},
{
name: 'Immutable Audit Trail Logs',
starter: '—',
team: '90 days exportable',
enterprise: 'Real-time SIEM streaming',
},
],
},
{
category: 'Support & Success SLAs',
features: [
{
name: 'Support Channel',
starter: 'Community & Email',
team: 'Priority Email + Chat',
enterprise: 'Dedicated Private Slack',
},
{
name: 'First-Response SLA',
starter: '24 business hours',
team: '4 business hours',
enterprise: '< 15 mins (24/7/365)',
},
{ name: 'Uptime SLA Guarantee', starter: '99.9%', team: '99.95%', enterprise: '99.99% financially backed' },
{ name: 'Dedicated Solutions Architect', starter: '—', team: '—', enterprise: 'Assigned Principal Engineer' },
],
},
]
</script>
<template>
<section data-slot="pricing-01" :class="cn('bg-background w-full py-16 sm:py-24', $props.class)">
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<!-- Section Header -->
<div class="mx-auto max-w-3xl space-y-4 text-center">
<div
class="border-primary/20 bg-primary/5 text-primary inline-flex items-center gap-2 rounded-full border px-3 py-1 text-xs font-medium shadow-2xs"
>
<ShieldCheck class="size-3.5" />
<span>Predictable Enterprise Pricing</span>
</div>
<h2 class="text-foreground text-3xl font-bold tracking-tight sm:text-4xl lg:text-5xl">
Scale effortlessly without seat tax surprises
</h2>
<p class="text-muted-foreground text-base sm:text-lg">
Zero setup fees, transparent volume discounts, and instant self-serve provisioning. Switch or cancel plans
anytime.
</p>
<!-- Billing Cycle & Currency Switcher Toolbar -->
<div class="mt-8 flex flex-wrap items-center justify-center gap-4 pt-2">
<!-- Monthly vs Yearly -->
<div class="border-border bg-card inline-flex rounded-lg border p-1 shadow-2xs">
<ToggleGroup
type="single"
:model-value="billingCycle"
@update:model-value="(v) => v && (billingCycle = v as BillingCycle)"
>
<ToggleGroupItem value="monthly" class="px-3.5 py-1.5 text-xs font-medium"> Monthly </ToggleGroupItem>
<ToggleGroupItem value="yearly" class="flex items-center gap-1.5 px-3.5 py-1.5 text-xs font-medium">
<span>Annual</span>
<Badge variant="secondary" class="bg-primary/10 text-primary border-primary/20 px-1.5 py-0 text-xs">
Save 20%
</Badge>
</ToggleGroupItem>
</ToggleGroup>
</div>
<!-- Currency Selector -->
<div class="border-border bg-card inline-flex rounded-lg border p-1 shadow-2xs">
<ToggleGroup
type="single"
:model-value="currency"
@update:model-value="(v) => v && (currency = v as Currency)"
>
<ToggleGroupItem value="USD" class="px-2.5 py-1 font-mono text-xs font-medium">USD ($)</ToggleGroupItem>
<ToggleGroupItem value="EUR" class="px-2.5 py-1 font-mono text-xs font-medium">EUR (€)</ToggleGroupItem>
<ToggleGroupItem value="GBP" class="px-2.5 py-1 font-mono text-xs font-medium">GBP (£)</ToggleGroupItem>
</ToggleGroup>
</div>
</div>
<!-- Interactive Team Seat Simulator Bar -->
<div class="border-border bg-card/60 mx-auto mt-6 max-w-xl rounded-xl border p-4 shadow-2xs backdrop-blur-xs">
<div class="flex flex-wrap items-center justify-between gap-2 text-xs">
<div class="text-foreground flex items-center gap-2 font-medium">
<Users class="text-primary size-4" />
<span>Simulate Team Size:</span>
</div>
<div class="flex items-center gap-2">
<span class="bg-muted text-foreground rounded px-2 py-0.5 font-mono text-xs font-semibold">
{{ currentSeats }} {{ currentSeats === 1 ? 'seat' : 'seats' }}
</span>
<span v-if="billingCycle === 'yearly' && annualSavingsTeam > 0" class="text-success text-xs font-medium">
(Saves ~{{ sym }}{{ annualSavingsTeam.toLocaleString() }}/yr on Team plan)
</span>
</div>
</div>
<div class="mt-3">
<Slider v-model="teamSeats" :min="1" :max="50" :step="1" class="cursor-pointer" />
</div>
<div class="text-muted-foreground mt-2 flex justify-between font-mono text-xs">
<span>1 seat</span>
<span>25 seats</span>
<span>50+ seats</span>
</div>
</div>
</div>
<!-- Plan Cards Grid -->
<div class="mt-12 grid grid-cols-1 gap-8 lg:grid-cols-3 lg:items-stretch">
<!-- Tier 1: Starter -->
<Card
class="border-border bg-card hover:border-primary/40 relative flex flex-col justify-between shadow-xs transition-colors duration-200 hover:shadow-md"
>
<div>
<CardHeader class="pb-4">
<div class="flex items-center justify-between">
<Badge variant="outline" class="font-mono text-xs">Bootstrap & Indie</Badge>
</div>
<CardTitle class="text-foreground text-xl font-bold tracking-tight">Starter Studio</CardTitle>
<CardDescription class="text-muted-foreground text-xs">
For agile squads and early-stage product teams validating core workflows.
</CardDescription>
<div class="mt-6 space-y-1">
<div class="flex items-baseline gap-1.5">
<span class="text-foreground text-4xl font-bold tracking-tight">
{{ sym }}{{ Math.round(starterBase * rates) }}
</span>
<span class="text-muted-foreground text-xs font-medium">/ seat / month</span>
</div>
<div class="text-muted-foreground font-mono text-xs">
Est. {{ sym }}{{ starterMonthlyTotal.toLocaleString() }}/mo for {{ currentSeats }}
{{ currentSeats === 1 ? 'seat' : 'seats' }}
</div>
</div>
</CardHeader>
<CardContent class="space-y-4 pt-2">
<Separator />
<div class="space-y-2.5">
<p class="text-muted-foreground text-xs font-semibold tracking-wider uppercase">Included features</p>
<ul class="text-foreground space-y-2.5 text-xs">
<li class="flex items-start gap-2">
<Check class="text-success mt-0.5 size-4 shrink-0" />
<span>Up to 10 team seats & unlimited guests</span>
</li>
<li class="flex items-start gap-2">
<Check class="text-success mt-0.5 size-4 shrink-0" />
<span>100,000 monthly API event calls</span>
</li>
<li class="flex items-start gap-2">
<Check class="text-success mt-0.5 size-4 shrink-0" />
<span>30-day continuous audit & revision history</span>
</li>
<li class="flex items-start gap-2">
<Check class="text-success mt-0.5 size-4 shrink-0" />
<span>Standard Community & Email support (24h)</span>
</li>
<li class="text-muted-foreground flex items-start gap-2">
<Check class="text-muted-foreground/50 mt-0.5 size-4 shrink-0" />
<span>Community Discord access & quickstarts</span>
</li>
</ul>
</div>
</CardContent>
</div>
<CardFooter class="border-border mt-auto border-t pt-4">
<Button variant="outline" class="h-10 w-full gap-1.5 text-xs font-semibold">
<span>Start Free 14-Day Trial</span>
<ArrowRight class="size-3.5" />
</Button>
</CardFooter>
</Card>
<!-- Tier 2: Team Pro (Highlighted Flagship) -->
<Card
class="border-primary bg-card ring-primary/20 relative flex flex-col justify-between shadow-lg ring-2 transition-colors duration-200 hover:shadow-xl lg:-translate-y-2"
>
<div class="absolute -top-3.5 left-1/2 -translate-x-1/2">
<Badge class="bg-primary text-primary-foreground gap-1.5 px-3 py-0.5 text-xs font-semibold shadow-sm">
<Zap class="size-3 fill-current" />
<span>Most Popular Choice</span>
</Badge>
</div>
<div>
<CardHeader class="pt-7 pb-4">
<div class="flex items-center justify-between">
<Badge variant="secondary" class="bg-primary/10 text-primary border-primary/20 font-mono text-xs">
Scale & High-Growth
</Badge>
</div>
<CardTitle class="text-foreground text-2xl font-bold tracking-tight">Team Scale</CardTitle>
<CardDescription class="text-muted-foreground text-xs">
For fast-scaling engineering and product organizations requiring deep automation.
</CardDescription>
<div class="mt-6 space-y-1">
<div class="flex items-baseline gap-1.5">
<span class="text-foreground text-4xl font-bold tracking-tight">
{{ sym }}{{ Math.round(teamBase * rates) }}
</span>
<span class="text-muted-foreground text-xs font-medium">/ seat / month</span>
</div>
<div class="text-primary font-mono text-xs font-medium">
Est. {{ sym }}{{ teamMonthlyTotal.toLocaleString() }}/mo for {{ currentSeats }}
{{ currentSeats === 1 ? 'seat' : 'seats' }}
</div>
</div>
</CardHeader>
<CardContent class="space-y-4 pt-2">
<Separator />
<div class="space-y-2.5">
<p class="text-muted-foreground text-xs font-semibold tracking-wider uppercase">
Everything in Starter, plus
</p>
<ul class="text-foreground space-y-2.5 text-xs">
<li class="flex items-start gap-2">
<Check class="text-success mt-0.5 size-4 shrink-0" />
<span class="font-medium">Unlimited team seats & custom workspace roles</span>
</li>
<li class="flex items-start gap-2">
<Check class="text-success mt-0.5 size-4 shrink-0" />
<span>2.5M monthly API calls & priority queue</span>
</li>
<li class="flex items-start gap-2">
<Check class="text-success mt-0.5 size-4 shrink-0" />
<span>Google & Okta SAML SSO integration</span>
</li>
<li class="flex items-start gap-2">
<Check class="text-success mt-0.5 size-4 shrink-0" />
<span>365-day immutable compliance logs</span>
</li>
<li class="flex items-start gap-2">
<Check class="text-success mt-0.5 size-4 shrink-0" />
<span>Priority ticketing with 4h guaranteed SLA</span>
</li>
</ul>
</div>
<!-- Addons Selector for Team -->
<div class="border-border bg-muted/30 space-y-2.5 rounded-lg border p-3">
<p class="text-foreground text-xs font-semibold">Optional Power Add-ons</p>
<div class="flex items-center justify-between text-xs">
<label for="vue-sla-addon" class="text-muted-foreground cursor-pointer">
24/7 Dedicated SLA (+{{ sym }}{{ Math.round(99 * rates) }}/mo)
</label>
<Switch id="vue-sla-addon" v-model="addDedicatedSla" />
</div>
<div class="flex items-center justify-between text-xs">
<label for="vue-vault-addon" class="text-muted-foreground cursor-pointer">
SOC 2 Vault Streaming (+{{ sym }}{{ Math.round(49 * rates) }}/mo)
</label>
<Switch id="vue-vault-addon" v-model="addAuditVault" />
</div>
</div>
</CardContent>
</div>
<CardFooter class="border-border mt-auto border-t pt-4">
<Button class="h-10 w-full gap-1.5 text-xs font-semibold shadow-sm">
<span>Deploy Team Workspace</span>
<ArrowRight class="size-3.5" />
</Button>
</CardFooter>
</Card>
<!-- Tier 3: Enterprise Platform -->
<Card
class="border-border bg-card hover:border-primary/40 relative flex flex-col justify-between shadow-xs transition-colors duration-200 hover:shadow-md"
>
<div>
<CardHeader class="pb-4">
<div class="flex items-center justify-between">
<Badge variant="outline" class="font-mono text-xs">Enterprise & Security</Badge>
</div>
<CardTitle class="text-foreground text-xl font-bold tracking-tight">Enterprise Suite</CardTitle>
<CardDescription class="text-muted-foreground text-xs">
Dedicated infrastructure, custom security controls, and bespoke compliance SLAs.
</CardDescription>
<div class="mt-6 space-y-1">
<div class="flex items-baseline gap-1.5">
<span class="text-foreground text-4xl font-bold tracking-tight">
{{ sym }}{{ Math.round(enterpriseBase * rates) }}
</span>
<span class="text-muted-foreground text-xs font-medium">/ seat / month</span>
</div>
<div class="text-muted-foreground font-mono text-xs">
Custom volume licensing available for 100+ seats
</div>
</div>
</CardHeader>
<CardContent class="space-y-4 pt-2">
<Separator />
<div class="space-y-2.5">
<p class="text-muted-foreground text-xs font-semibold tracking-wider uppercase">
Everything in Team, plus
</p>
<ul class="text-foreground space-y-2.5 text-xs">
<li class="flex items-start gap-2">
<Check class="text-success mt-0.5 size-4 shrink-0" />
<span>Custom SCIM v2 user provisioning & directory sync</span>
</li>
<li class="flex items-start gap-2">
<Check class="text-success mt-0.5 size-4 shrink-0" />
<span>Dedicated VPC peering & custom data residency</span>
</li>
<li class="flex items-start gap-2">
<Check class="text-success mt-0.5 size-4 shrink-0" />
<span>99.99% uptime SLA with financial penalty backing</span>
</li>
<li class="flex items-start gap-2">
<Check class="text-success mt-0.5 size-4 shrink-0" />
<span>Assigned Principal Solutions Engineer & private Slack</span>
</li>
<li class="flex items-start gap-2">
<Check class="text-success mt-0.5 size-4 shrink-0" />
<span>Custom Master Service Agreement (MSA) & DPA</span>
</li>
</ul>
</div>
</CardContent>
</div>
<CardFooter class="border-border mt-auto border-t pt-4">
<Button variant="outline" class="h-10 w-full gap-1.5 text-xs font-semibold">
<ShieldCheck class="text-primary size-3.5" />
<span>Contact Solutions Team</span>
</Button>
</CardFooter>
</Card>
</div>
<!-- Collapsible Feature Matrix Trigger -->
<div class="mt-12 text-center">
<Button
variant="ghost"
size="sm"
class="text-foreground hover:bg-muted gap-2 text-xs font-semibold"
@click="showComparisonTable = !showComparisonTable"
>
<span>{{
showComparisonTable ? 'Hide Detailed Feature Comparison' : 'Compare All Features & Enterprise Limits'
}}</span>
<ChevronUp v-if="showComparisonTable" class="size-4" />
<ChevronDown v-else class="size-4" />
</Button>
</div>
<!-- Feature Comparison Matrix Table -->
<div v-if="showComparisonTable" class="border-border bg-card mt-8 overflow-hidden rounded-xl border shadow-xs">
<div class="border-border bg-muted/20 flex flex-wrap items-center justify-between gap-4 border-b p-4 sm:p-6">
<div>
<h3 class="text-foreground text-base font-bold">Detailed Specification & Limits</h3>
<p class="text-muted-foreground text-xs">Comprehensive side-by-side breakdown across every tier.</p>
</div>
<Badge variant="outline" class="font-mono text-xs">All plans include SSL & automated backups</Badge>
</div>
<div class="overflow-x-auto">
<Table>
<TableHeader>
<TableRow class="bg-muted/40">
<TableHead class="text-foreground w-[34%] text-xs font-semibold">Capabilities</TableHead>
<TableHead class="text-foreground w-[22%] text-xs font-semibold">Starter Studio</TableHead>
<TableHead class="text-primary w-[22%] text-xs font-bold font-semibold">Team Scale</TableHead>
<TableHead class="text-foreground w-[22%] text-xs font-semibold">Enterprise Suite</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<template v-for="(group, idx) in comparisonMatrix" :key="idx">
<TableRow class="bg-muted/60 text-muted-foreground text-xs font-semibold">
<TableCell colspan="4" class="py-2.5 font-mono text-xs tracking-wider uppercase">
{{ group.category }}
</TableCell>
</TableRow>
<TableRow v-for="(feat, fIdx) in group.features" :key="fIdx" class="text-xs">
<TableCell class="text-foreground py-3 font-medium">
{{ feat.name }}
</TableCell>
<TableCell class="text-muted-foreground py-3">{{ feat.starter }}</TableCell>
<TableCell class="text-foreground bg-primary/5 py-3 font-medium">{{ feat.team }}</TableCell>
<TableCell class="text-foreground py-3">{{ feat.enterprise }}</TableCell>
</TableRow>
</template>
</TableBody>
</Table>
</div>
</div>
</div>
</section>
</template>
<script setup lang="ts">
import { computed, ref, type HTMLAttributes } from 'vue'
import { ArrowRight, Check, HardDrive, Headphones, KeyRound, ShieldCheck, Users, Zap } from 'lucide-vue-next'
import { cn } from '@/lib/utils'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
import { Separator } from '@/components/ui/separator'
import { Slider } from '@/components/ui/slider'
import { Switch } from '@/components/ui/switch'
interface Props {
class?: HTMLAttributes['class']
}
const props = defineProps<Props>()
interface RequestTier {
label: string
requests: number
cost: number
}
interface StorageTier {
label: string
gb: number
cost: number
}
const REQUEST_TIERS: RequestTier[] = [
{ label: '10K requests', requests: 10_000, cost: 0 },
{ label: '50K requests', requests: 50_000, cost: 25 },
{ label: '100K requests', requests: 100_000, cost: 60 },
{ label: '250K requests', requests: 250_000, cost: 120 },
{ label: '500K requests', requests: 500_000, cost: 220 },
{ label: '1M requests', requests: 1_000_000, cost: 400 },
{ label: '2.5M requests', requests: 2_500_000, cost: 750 },
{ label: '5M requests', requests: 5_000_000, cost: 1200 },
]
const STORAGE_TIERS: StorageTier[] = [
{ label: '100 GB', gb: 100, cost: 10 },
{ label: '250 GB', gb: 250, cost: 25 },
{ label: '500 GB', gb: 500, cost: 45 },
{ label: '1 TB', gb: 1_000, cost: 80 },
{ label: '2 TB', gb: 2_000, cost: 150 },
{ label: '5 TB', gb: 5_000, cost: 320 },
{ label: '10 TB', gb: 10_000, cost: 580 },
]
const SEAT_PRICE = 15
const SUPPORT_MANAGER_PRICE = 200
const CUSTOM_SLA_PRICE = 500
const SSO_PRICE = 100
const SEAT_MARKS = {
1: '1',
25: '25',
50: '50',
75: '75',
100: '100',
}
const REQUEST_MARKS = {
0: '10K',
2: '100K',
4: '500K',
7: '5M',
}
const STORAGE_MARKS = {
0: '100 GB',
2: '500 GB',
4: '2 TB',
6: '10 TB',
}
const isAnnual = ref(true)
const seats = ref(12)
const requestIndex = ref(2)
const storageIndex = ref(2)
const addonSupport = ref(false)
const addonSla = ref(false)
const addonSso = ref(true)
const currentRequestTier = computed(() => REQUEST_TIERS[requestIndex.value] ?? REQUEST_TIERS[0])
const currentStorageTier = computed(() => STORAGE_TIERS[storageIndex.value] ?? STORAGE_TIERS[0])
const seatTier = computed(() => {
if (seats.value <= 5) return { label: 'Starter Team', variant: 'secondary' as const }
if (seats.value <= 25) return { label: 'Growth Team', variant: 'outline' as const }
return { label: 'Scale Team', variant: 'default' as const }
})
const recommendedPlan = computed(() => {
if (addonSla.value || requestIndex.value >= 6 || storageIndex.value >= 5 || seats.value >= 30) {
return { name: 'Enterprise', badge: 'Enterprise Plan', baseFee: 199, variant: 'default' as const }
}
if (seats.value >= 8 || requestIndex.value >= 3 || storageIndex.value >= 3 || addonSupport.value) {
return { name: 'Growth', badge: 'Growth Plan', baseFee: 79, variant: 'default' as const }
}
return { name: 'Starter', badge: 'Starter Plan', baseFee: 29, variant: 'secondary' as const }
})
const baseCost = computed(() => recommendedPlan.value.baseFee)
const seatsCost = computed(() => seats.value * SEAT_PRICE)
const requestsCost = computed(() => currentRequestTier.value.cost)
const storageCost = computed(() => currentStorageTier.value.cost)
const addonsCost = computed(() => {
let cost = 0
if (addonSupport.value) cost += SUPPORT_MANAGER_PRICE
if (addonSla.value) cost += CUSTOM_SLA_PRICE
if (addonSso.value) cost += SSO_PRICE
return cost
})
const activeAddonsCount = computed(() => {
let count = 0
if (addonSupport.value) count++
if (addonSla.value) count++
if (addonSso.value) count++
return count
})
const subtotalMonthly = computed(
() => baseCost.value + seatsCost.value + requestsCost.value + storageCost.value + addonsCost.value,
)
const discountMultiplier = computed(() => (isAnnual.value ? 0.8 : 1.0))
const totalMonthly = computed(() => Math.round(subtotalMonthly.value * discountMultiplier.value))
const totalAnnual = computed(() => totalMonthly.value * 12)
const annualSavings = computed(() => Math.round(subtotalMonthly.value * 0.2 * 12))
</script>
<template>
<div data-slot="pricing-calculator" :class="cn('mx-auto w-full max-w-6xl space-y-10 p-4 sm:p-6 lg:p-8', props.class)">
<div class="flex flex-col items-center gap-4 text-center">
<Badge variant="outline" class="gap-1.5 px-3 py-1 text-xs font-medium tracking-wider uppercase">
<ShieldCheck class="text-primary size-3.5" />
Pricing Calculator
</Badge>
<div class="space-y-2">
<h2 class="text-foreground text-2xl font-bold tracking-tight sm:text-3xl lg:text-4xl">
Interactive Pricing Calculator
</h2>
<p class="text-muted-foreground mx-auto max-w-2xl text-sm sm:text-base">
Calculate your exact monthly or annual investment based on usage.
</p>
</div>
<div class="border-border bg-card mt-2 inline-flex items-center gap-3 rounded-full border px-4 py-2 shadow-xs">
<span
:class="
cn(
'text-xs font-medium transition-colors',
!isAnnual ? 'text-foreground font-semibold' : 'text-muted-foreground',
)
"
>
Monthly
</span>
<Switch :model-value="isAnnual" @update:model-value="(val) => (isAnnual = val)" />
<span
:class="
cn(
'text-xs font-medium transition-colors',
isAnnual ? 'text-foreground font-semibold' : 'text-muted-foreground',
)
"
>
Pay Annually
</span>
<Badge variant="secondary" class="bg-primary/10 text-primary border-primary/20 text-xs font-semibold">
Save 20%
</Badge>
</div>
</div>
<div class="grid grid-cols-1 gap-8 lg:grid-cols-12 lg:items-start">
<div class="space-y-6 lg:col-span-7">
<Card>
<CardHeader>
<CardTitle>Capacity & Scale</CardTitle>
<CardDescription>Configure team seats, API request throughput, and dedicated storage.</CardDescription>
</CardHeader>
<CardContent class="space-y-8">
<div class="space-y-4">
<div class="flex items-center justify-between">
<div class="flex items-center gap-2.5">
<div
class="bg-primary/10 text-primary border-primary/20 flex size-8 shrink-0 items-center justify-center rounded-md border"
>
<Users class="size-4" />
</div>
<div>
<span class="text-foreground text-sm font-medium">Team Seats</span>
<Badge :variant="seatTier.variant" class="ml-2 text-xs font-normal">
{{ seatTier.label }}
</Badge>
</div>
</div>
<div class="text-right">
<span class="text-foreground text-base font-semibold tabular-nums">{{ seats }}</span>
<span class="text-muted-foreground text-xs"> seats (${{ seats * SEAT_PRICE }}/mo)</span>
</div>
</div>
<div class="pb-6">
<Slider
:model-value="seats"
:min="1"
:max="100"
:step="1"
:marks="SEAT_MARKS"
:tooltip="(val) => `${val} seats`"
@update:model-value="(val) => (seats = typeof val === 'number' ? val : val[0])"
/>
</div>
<p class="text-muted-foreground text-xs">
Full workspace access, fine-grained permission controls, and audit log tracking for each member.
</p>
</div>
<Separator />
<div class="space-y-4">
<div class="flex items-center justify-between">
<div class="flex items-center gap-2.5">
<div
class="bg-primary/10 text-primary border-primary/20 flex size-8 shrink-0 items-center justify-center rounded-md border"
>
<Zap class="size-4" />
</div>
<div>
<span class="text-foreground text-sm font-medium">Monthly API Requests</span>
</div>
</div>
<div class="text-right">
<span class="text-foreground text-base font-semibold tabular-nums">
{{ currentRequestTier.label }}
</span>
<span class="text-muted-foreground text-xs">
({{ currentRequestTier.cost === 0 ? 'Included' : `+$${currentRequestTier.cost}/mo` }})
</span>
</div>
</div>
<div class="pb-6">
<Slider
:model-value="requestIndex"
:min="0"
:max="REQUEST_TIERS.length - 1"
:step="1"
:marks="REQUEST_MARKS"
:tooltip="(idx) => REQUEST_TIERS[idx]?.label ?? ''"
@update:model-value="(val) => (requestIndex = typeof val === 'number' ? val : val[0])"
/>
</div>
<p class="text-muted-foreground text-xs">
Globally distributed edge endpoints, automatic rate limiting, and sub-50ms p99 latency SLA.
</p>
</div>
<Separator />
<div class="space-y-4">
<div class="flex items-center justify-between">
<div class="flex items-center gap-2.5">
<div
class="bg-primary/10 text-primary border-primary/20 flex size-8 shrink-0 items-center justify-center rounded-md border"
>
<HardDrive class="size-4" />
</div>
<div>
<span class="text-foreground text-sm font-medium">Dedicated Cloud Storage</span>
</div>
</div>
<div class="text-right">
<span class="text-foreground text-base font-semibold tabular-nums">
{{ currentStorageTier.label }}
</span>
<span class="text-muted-foreground text-xs"> (+${{ currentStorageTier.cost }}/mo)</span>
</div>
</div>
<div class="pb-6">
<Slider
:model-value="storageIndex"
:min="0"
:max="STORAGE_TIERS.length - 1"
:step="1"
:marks="STORAGE_MARKS"
:tooltip="(idx) => STORAGE_TIERS[idx]?.label ?? ''"
@update:model-value="(val) => (storageIndex = typeof val === 'number' ? val : val[0])"
/>
</div>
<p class="text-muted-foreground text-xs">
Encrypted at rest (AES-256) with multi-region automated replication and daily disaster backups.
</p>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Enterprise Add-ons</CardTitle>
<CardDescription
>Enhance your infrastructure with enterprise compliance, reliability, and support.</CardDescription
>
</CardHeader>
<CardContent class="space-y-3">
<div
class="border-border bg-card hover:border-primary/30 flex flex-col justify-between gap-4 rounded-lg border p-4 transition-colors sm:flex-row sm:items-center"
>
<div class="flex items-start gap-3">
<div
class="bg-primary/10 text-primary border-primary/20 mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-md border"
>
<Headphones class="size-4" />
</div>
<div class="space-y-0.5">
<div class="flex items-center gap-2">
<span class="text-foreground text-sm font-medium">Dedicated Support Manager</span>
<Badge variant="outline" class="text-xs font-normal">+$200/mo</Badge>
</div>
<p class="text-muted-foreground text-xs">
Direct Slack channel, named technical account manager & 1-hour response SLA.
</p>
</div>
</div>
<div class="flex sm:justify-end">
<Switch :model-value="addonSupport" @update:model-value="(val) => (addonSupport = val)" />
</div>
</div>
<div
class="border-border bg-card hover:border-primary/30 flex flex-col justify-between gap-4 rounded-lg border p-4 transition-colors sm:flex-row sm:items-center"
>
<div class="flex items-start gap-3">
<div
class="bg-primary/10 text-primary border-primary/20 mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-md border"
>
<ShieldCheck class="size-4" />
</div>
<div class="space-y-0.5">
<div class="flex items-center gap-2">
<span class="text-foreground text-sm font-medium">Custom SLA Guarantee</span>
<Badge variant="outline" class="text-xs font-normal">+$500/mo</Badge>
</div>
<p class="text-muted-foreground text-xs">
99.99% uptime guarantee with financial commitments and priority disaster recovery.
</p>
</div>
</div>
<div class="flex sm:justify-end">
<Switch :model-value="addonSla" @update:model-value="(val) => (addonSla = val)" />
</div>
</div>
<div
class="border-border bg-card hover:border-primary/30 flex flex-col justify-between gap-4 rounded-lg border p-4 transition-colors sm:flex-row sm:items-center"
>
<div class="flex items-start gap-3">
<div
class="bg-primary/10 text-primary border-primary/20 mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-md border"
>
<KeyRound class="size-4" />
</div>
<div class="space-y-0.5">
<div class="flex items-center gap-2">
<span class="text-foreground text-sm font-medium">Single Sign-On (SSO / SAML)</span>
<Badge variant="outline" class="text-xs font-normal">+$100/mo</Badge>
</div>
<p class="text-muted-foreground text-xs">
Okta, Azure AD, Google Workspace, and SAML 2.0 enterprise identity integration.
</p>
</div>
</div>
<div class="flex sm:justify-end">
<Switch :model-value="addonSso" @update:model-value="(val) => (addonSso = val)" />
</div>
</div>
</CardContent>
</Card>
</div>
<div class="lg:sticky lg:top-8 lg:col-span-5">
<Card class="border-border bg-card shadow-xs">
<CardHeader class="pb-4">
<div class="flex items-center justify-between">
<span class="text-muted-foreground text-xs font-semibold tracking-wider uppercase">
Estimated Investment
</span>
<Badge :variant="recommendedPlan.variant" class="gap-1 shadow-xs">
<ShieldCheck v-if="recommendedPlan.name === 'Enterprise'" class="size-3" />
{{ recommendedPlan.badge }}
</Badge>
</div>
<div class="mt-4">
<div class="flex items-baseline gap-1.5">
<span class="text-foreground text-4xl font-bold tracking-tight tabular-nums sm:text-5xl">
${{ totalMonthly }}
</span>
<span class="text-muted-foreground text-sm font-normal"> / month</span>
</div>
<p v-if="isAnnual" class="text-muted-foreground mt-2 text-xs">
Billed annually (${{ totalAnnual.toLocaleString('en-US') }}/yr) ·
<span class="text-success font-semibold">Save ${{ annualSavings.toLocaleString('en-US') }}/yr</span>
</p>
<p v-else class="text-muted-foreground mt-2 text-xs">
Billed monthly · Switch to annual to save 20% (${{ annualSavings.toLocaleString('en-US') }}/yr)
</p>
</div>
</CardHeader>
<CardContent class="space-y-4">
<Separator />
<div class="space-y-2.5">
<div class="flex items-center justify-between text-sm">
<span class="text-muted-foreground">Base platform ({{ recommendedPlan.name }})</span>
<span class="font-medium tabular-nums">${{ isAnnual ? Math.round(baseCost * 0.8) : baseCost }}/mo</span>
</div>
<div class="flex items-center justify-between text-sm">
<span class="text-muted-foreground">Team seats ({{ seats }} × ${{ SEAT_PRICE }})</span>
<span class="font-medium tabular-nums"
>${{ isAnnual ? Math.round(seatsCost * 0.8) : seatsCost }}/mo</span
>
</div>
<div class="flex items-center justify-between text-sm">
<span class="text-muted-foreground">API throughput ({{ currentRequestTier.label }})</span>
<span class="font-medium tabular-nums">
{{
requestsCost === 0 ? 'Included' : `$${isAnnual ? Math.round(requestsCost * 0.8) : requestsCost}/mo`
}}
</span>
</div>
<div class="flex items-center justify-between text-sm">
<span class="text-muted-foreground">Cloud storage ({{ currentStorageTier.label }})</span>
<span class="font-medium tabular-nums">
${{ isAnnual ? Math.round(storageCost * 0.8) : storageCost }}/mo
</span>
</div>
<div class="flex items-center justify-between text-sm">
<span class="text-muted-foreground">Add-ons ({{ activeAddonsCount }} active)</span>
<span class="font-medium tabular-nums">
{{ addonsCost === 0 ? '$0/mo' : `$${isAnnual ? Math.round(addonsCost * 0.8) : addonsCost}/mo` }}
</span>
</div>
<div
v-if="isAnnual"
class="bg-success/10 text-success flex items-center justify-between rounded-md px-2.5 py-1.5 text-xs font-medium"
>
<span>Annual discount applied</span>
<span class="font-semibold tabular-nums">20% off</span>
</div>
</div>
<Separator />
<ul class="text-muted-foreground space-y-2 text-xs">
<li class="flex items-center gap-2">
<Check class="text-primary size-3.5 shrink-0" />
<span>14-day fully featured free trial</span>
</li>
<li class="flex items-center gap-2">
<Check class="text-primary size-3.5 shrink-0" />
<span>No credit card required upfront</span>
</li>
<li class="flex items-center gap-2">
<Check class="text-primary size-3.5 shrink-0" />
<span>Zero-downtime migration assistance</span>
</li>
</ul>
</CardContent>
<CardFooter class="flex flex-col gap-2.5 pt-2">
<Button class="w-full gap-2 font-semibold shadow-xs" size="lg">
Start 14-Day Free Trial
<ArrowRight class="size-4" />
</Button>
<Button variant="outline" class="w-full" size="default"> Request Custom Quote </Button>
</CardFooter>
</Card>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { ArrowRight, Building2, Calendar, CheckCircle2, Lock, Server } from 'lucide-vue-next'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { cn } from '@/lib/utils'
export interface SlaGuarantee {
title: string
metric: string
description: string
icon: string
}
export interface ComplianceCert {
id: string
name: string
status: string
badgeVariant?: 'default' | 'outline' | 'secondary'
}
export interface PricingEnterpriseSlaCardProps {
title?: string
description?: string
guarantees?: SlaGuarantee[]
certifications?: ComplianceCert[]
class?: string
}
const DEFAULT_GUARANTEES: SlaGuarantee[] = [
{
title: 'High-Availability SLA',
metric: '99.999%',
description: 'Financial penalty-backed monthly uptime commitment across multi-region clusters.',
icon: 'ShieldCheck',
},
{
title: 'P1 Incident Response',
metric: '< 15 mins',
description: 'Direct paging to designated Staff Infrastructure Commanders 24/7/365.',
icon: 'Headphones',
},
{
title: 'Global Edge TTFB',
metric: '< 12ms',
description: 'Sub-15ms Time-To-First-Byte guaranteed via Anycast network mesh.',
icon: 'Zap',
},
{
title: 'Custom Legal & DPA',
metric: 'Bespoke',
description: 'Dedicated legal counsel review, redline allowances, and customized BAAs.',
icon: 'Scale',
},
]
const DEFAULT_CERTS: ComplianceCert[] = [
{ id: 'soc2', name: 'SOC 2 Type II Certified', status: 'Continuous Audit' },
{ id: 'hipaa', name: 'HIPAA Compliant BAA', status: 'Available' },
{ id: 'iso27001', name: 'ISO/IEC 27001:2022', status: 'Certified' },
{ id: 'gdpr', name: 'GDPR & CCPA Verified', status: 'Compliant' },
{ id: 'fedramp', name: 'FedRAMP In-Process', status: 'High Baseline' },
]
const props = withDefaults(defineProps<PricingEnterpriseSlaCardProps>(), {
title: 'Mission-critical infrastructure with contractual legal guarantees.',
description:
'Tailored enterprise licensing, custom security reviews, isolated VPC deployments, and white-glove migration engineering.',
})
const activeGuarantees = computed(() => props.guarantees ?? DEFAULT_GUARANTEES)
const activeCertifications = computed(() => props.certifications ?? DEFAULT_CERTS)
const isMeetingRequested = ref(false)
function requestMeeting() {
isMeetingRequested.value = true
setTimeout(() => {
isMeetingRequested.value = false
}, 3000)
}
</script>
<template>
<section
data-slot="pricing-enterprise-sla-card"
:class="cn('bg-background relative overflow-hidden py-16 sm:py-24', props.class)"
>
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<!-- Section Header -->
<div class="mx-auto max-w-3xl space-y-4 text-center">
<a
href="#enterprise-contract"
class="group border-border/80 bg-secondary/60 hover:bg-secondary text-foreground inline-flex items-center gap-2 rounded-full border px-3.5 py-1 text-xs font-medium shadow-2xs transition-colors"
>
<Building2 class="text-primary size-3.5" />
<span>Enterprise Custom Contracting</span>
<ArrowRight class="text-muted-foreground size-3 transition-transform group-hover:translate-x-0.5" />
</a>
<h2 class="text-foreground text-3xl font-bold tracking-tight sm:text-4xl">
{{ title }}
</h2>
<p class="text-muted-foreground text-base sm:text-lg">
{{ description }}
</p>
</div>
<!-- Main Enterprise Showcase Container -->
<div class="border-border bg-card mt-12 overflow-hidden rounded-2xl border shadow-sm">
<div class="divide-border grid grid-cols-1 divide-y lg:grid-cols-12 lg:divide-x lg:divide-y-0">
<!-- Left: SLA Guarantees & Contractual Commitments (7 Cols) -->
<div class="space-y-8 p-8 lg:col-span-7">
<div class="space-y-1">
<div class="text-primary text-xs font-bold tracking-wider uppercase">Service Level Agreement</div>
<h3 class="text-foreground text-xl font-bold">Penalty-Backed Contractual Metrics</h3>
<p class="text-muted-foreground text-xs">
Every commitment is codified into your master service agreement with direct financial remedies.
</p>
</div>
<!-- Guarantees 2x2 Grid -->
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div
v-for="(item, idx) in activeGuarantees"
:key="idx"
class="border-border bg-muted/20 space-y-2 rounded-xl border p-4"
>
<div class="flex items-center justify-between">
<span class="text-foreground text-xs font-bold">{{ item.title }}</span>
<span class="text-primary font-mono text-xs font-bold">{{ item.metric }}</span>
</div>
<p class="text-muted-foreground text-xs leading-relaxed">
{{ item.description }}
</p>
</div>
</div>
<!-- Compliance & Governance Strip -->
<div class="space-y-3 pt-2">
<div class="text-muted-foreground flex items-center gap-2 text-xs font-bold tracking-wider uppercase">
<Lock class="text-primary size-3.5" />
<span>Security & Regulatory Attestations</span>
</div>
<div class="flex flex-wrap gap-2">
<Badge
v-for="cert in activeCertifications"
:key="cert.id"
variant="outline"
class="border-border bg-background text-foreground gap-1.5 px-3 py-1 text-xs font-medium"
>
<CheckCircle2 class="text-success size-3" />
<span>{{ cert.name }}</span>
<span class="text-muted-foreground font-mono text-xs">({{ cert.status }})</span>
</Badge>
</div>
</div>
</div>
<!-- Right: Direct Enterprise Solution Consultation Card (5 Cols) -->
<div class="bg-muted/30 flex flex-col justify-between space-y-6 p-8 lg:col-span-5">
<div class="space-y-4">
<div class="border-border flex items-center justify-between border-b pb-3">
<div class="flex items-center gap-2">
<Server class="text-primary size-4" />
<span class="text-foreground text-sm font-semibold">Custom Private Deployment</span>
</div>
<Badge variant="outline" class="border-success/30 bg-success/10 text-success font-mono text-xs">
Tailored
</Badge>
</div>
<div class="space-y-2.5">
<div class="text-foreground text-xs font-bold">Included with Custom Tier:</div>
<ul class="text-muted-foreground space-y-2 text-xs">
<li class="flex items-center gap-2">
<CheckCircle2 class="text-success size-3.5 shrink-0" />
<span>Dedicated AWS / GCP VPC peering or self-hosted air-gap</span>
</li>
<li class="flex items-center gap-2">
<CheckCircle2 class="text-success size-3.5 shrink-0" />
<span>Custom SAML 2.0 / Okta / Azure AD SCIM provisioning</span>
</li>
<li class="flex items-center gap-2">
<CheckCircle2 class="text-success size-3.5 shrink-0" />
<span>Dedicated Solution Architect & design system migration team</span>
</li>
<li class="flex items-center gap-2">
<CheckCircle2 class="text-success size-3.5 shrink-0" />
<span>Invoiced payment via ACH, Wire Transfer, or AWS Marketplace</span>
</li>
</ul>
</div>
</div>
<!-- Action Block -->
<div class="border-border space-y-3 border-t pt-4">
<Button class="w-full gap-2 shadow-xs" size="lg" @click="requestMeeting">
<Calendar class="size-4" />
<span>{{
isMeetingRequested ? 'Direct Routing to Architect...' : 'Book Enterprise Technical Review'
}}</span>
</Button>
<div class="text-muted-foreground text-center text-xs">
Average executive response time: <strong>under 20 minutes</strong>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { ArrowRight, Calculator, Check, CreditCard } from 'lucide-vue-next'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Card, CardContent } from '@/components/ui/card'
import { cn } from '@/lib/utils'
export interface PricingPlan {
id: string
name: string
monthlyPrice: number
description: string
includedSeats: number
}
export interface PricingAddon {
id: string
name: string
monthlyPrice: number
description: string
category: string
}
export interface PricingFeatureAddonBuilderProps {
title?: string
description?: string
plans?: PricingPlan[]
addons?: PricingAddon[]
class?: string
}
const DEFAULT_PLANS: PricingPlan[] = [
{
id: 'starter',
name: 'Starter Tier',
monthlyPrice: 29,
description: 'For indie developers and early-stage prototypes needing unbundled speed.',
includedSeats: 2,
},
{
id: 'growth',
name: 'Growth Core',
monthlyPrice: 99,
description: 'For scaling product engineering teams building high-conversion platforms.',
includedSeats: 5,
},
{
id: 'scale',
name: 'Scale Enterprise',
monthlyPrice: 299,
description: 'For mission-critical production clusters requiring sub-10ms global edge delivery.',
includedSeats: 15,
},
]
const DEFAULT_ADDONS: PricingAddon[] = [
{
id: 'dedicated-ip',
name: 'Dedicated Static Edge IP',
monthlyPrice: 49,
description: 'Static IPv4/IPv6 address allocations with zero-reputation penalty.',
category: 'Network',
},
{
id: 'audit-logs',
name: 'Immutable SOC2 Audit Logs',
monthlyPrice: 79,
description: 'Cryptographically signed telemetry logs with 365-day cold storage retention.',
category: 'Security',
},
{
id: 'multi-region',
name: 'Multi-Region Active-Active Mesh',
monthlyPrice: 129,
description: 'Synchronized cross-continental database replicas and automatic DNS failover.',
category: 'Reliability',
},
{
id: 'priority-sla',
name: '1-Hour Enterprise Response SLA',
monthlyPrice: 199,
description: 'Direct Slack / Discord hotline with senior design engineering staff.',
category: 'Support',
},
]
const props = withDefaults(defineProps<PricingFeatureAddonBuilderProps>(), {
title: 'Build your custom plan with transparent, zero-surprise pricing.',
description:
'Select your base tier, adjust seat allocations, and toggle modular enterprise add-ons with real-time invoice calculations.',
})
const activePlans = computed(() => props.plans ?? DEFAULT_PLANS)
const activeAddons = computed(() => props.addons ?? DEFAULT_ADDONS)
const selectedPlanId = ref('growth')
const selectedAddonIds = ref<string[]>(['dedicated-ip', 'audit-logs'])
const seatCount = ref(8)
const isAnnual = ref(true)
const selectedPlan = computed(() => {
return activePlans.value.find((p) => p.id === selectedPlanId.value) || activePlans.value[0]
})
const extraSeats = computed(() => {
return Math.max(0, seatCount.value - selectedPlan.value.includedSeats)
})
const extraSeatCost = computed(() => extraSeats.value * 15)
const totalAddonsCost = computed(() => {
return activeAddons.value
.filter((a) => selectedAddonIds.value.includes(a.id))
.reduce((sum, a) => sum + a.monthlyPrice, 0)
})
const monthlySubtotal = computed(() => {
return selectedPlan.value.monthlyPrice + extraSeatCost.value + totalAddonsCost.value
})
const finalMonthlyRate = computed(() => {
if (isAnnual.value) {
return Math.round(monthlySubtotal.value * 0.8)
}
return monthlySubtotal.value
})
const annualSavings = computed(() => {
return (monthlySubtotal.value - Math.round(monthlySubtotal.value * 0.8)) * 12
})
function toggleAddon(addonId: string) {
if (selectedAddonIds.value.includes(addonId)) {
selectedAddonIds.value = selectedAddonIds.value.filter((id) => id !== addonId)
} else {
selectedAddonIds.value.push(addonId)
}
}
</script>
<template>
<section
data-slot="pricing-feature-addon-builder"
:class="cn('bg-background relative overflow-hidden py-16 sm:py-24', props.class)"
>
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<!-- Section Header -->
<div class="mx-auto max-w-3xl space-y-4 text-center">
<a
href="#pricing-calculator"
class="group border-border/80 bg-secondary/60 hover:bg-secondary text-foreground inline-flex items-center gap-2 rounded-full border px-3.5 py-1 text-xs font-medium shadow-2xs transition-colors"
>
<Calculator class="text-primary size-3.5" />
<span>Real-time Add-on Cost Synthesizer</span>
<ArrowRight class="text-muted-foreground size-3 transition-transform group-hover:translate-x-0.5" />
</a>
<h2 class="text-foreground text-3xl font-bold tracking-tight sm:text-4xl">
{{ title }}
</h2>
<p class="text-muted-foreground text-base sm:text-lg">
{{ description }}
</p>
<!-- Billing Cadence Toggle -->
<div class="flex items-center justify-center gap-3 pt-2">
<span
class="text-xs font-medium"
:class="!isAnnual ? 'text-foreground font-semibold' : 'text-muted-foreground'"
>
Monthly Billing
</span>
<button
type="button"
class="bg-muted focus-visible:ring-ring relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus-visible:ring-2 focus-visible:outline-none"
:class="isAnnual ? 'bg-primary' : 'bg-muted'"
@click="isAnnual = !isAnnual"
>
<span
class="bg-background pointer-events-none inline-block size-5 transform rounded-full shadow-lg ring-0 transition duration-200 ease-in-out"
:class="isAnnual ? 'translate-x-5' : 'translate-x-0'"
/>
</button>
<span
class="flex items-center gap-1.5 text-xs font-medium"
:class="isAnnual ? 'text-foreground font-semibold' : 'text-muted-foreground'"
>
<span>Annual Billing</span>
<Badge variant="outline" class="border-success/30 bg-success/10 text-success text-xs"> Save 20% </Badge>
</span>
</div>
</div>
<!-- Add-on Builder Workbench -->
<div class="mt-12 grid grid-cols-1 gap-6 lg:grid-cols-12">
<!-- Left: Plan Selection & Addon Toggles (7 Cols) -->
<div class="space-y-6 lg:col-span-7">
<!-- Step 1: Base Tier Cards -->
<div class="space-y-3">
<div class="text-muted-foreground text-xs font-bold tracking-wider uppercase">
Step 1: Choose Base Core Tier
</div>
<div class="grid grid-cols-1 gap-3 sm:grid-cols-3">
<button
v-for="plan in activePlans"
:key="plan.id"
type="button"
:class="
cn(
'flex flex-col justify-between rounded-xl border p-4 text-left transition-colors',
selectedPlanId === plan.id
? 'border-primary bg-primary/5 ring-primary/20 shadow-xs ring-1'
: 'border-border bg-card hover:bg-muted/40 text-muted-foreground hover:text-foreground',
)
"
@click="selectedPlanId = plan.id"
>
<div>
<div class="text-foreground text-xs font-bold">{{ plan.name }}</div>
<div class="text-foreground mt-1 font-mono text-lg font-bold">
${{ plan.monthlyPrice }}<span class="text-muted-foreground text-xs font-normal">/mo</span>
</div>
</div>
<div class="text-muted-foreground mt-2 text-xs">Includes {{ plan.includedSeats }} engineer seats</div>
</button>
</div>
</div>
<!-- Step 2: Seat Allocation Slider -->
<Card class="border-border bg-card/60 shadow-2xs">
<CardContent class="space-y-2.5 p-4">
<div class="flex items-center justify-between text-xs">
<span class="text-foreground font-bold">Engineer Team Seats</span>
<span class="text-foreground font-mono text-sm font-bold"
>{{ seatCount }} seats (${{ extraSeatCost }}/mo extra)</span
>
</div>
<input
v-model.number="seatCount"
type="range"
min="2"
max="50"
step="1"
class="accent-primary w-full cursor-pointer"
/>
<div class="text-muted-foreground flex justify-between font-mono text-xs">
<span>2 seats</span>
<span>50 seats</span>
</div>
</CardContent>
</Card>
<!-- Step 3: Enterprise Add-ons Checklist -->
<div class="space-y-3">
<div class="text-muted-foreground text-xs font-bold tracking-wider uppercase">
Step 3: Select Modular Capabilities
</div>
<div class="grid grid-cols-1 gap-2.5">
<button
v-for="addon in activeAddons"
:key="addon.id"
type="button"
:class="
cn(
'flex items-center justify-between rounded-xl border p-3.5 text-left transition-colors',
selectedAddonIds.includes(addon.id)
? 'border-primary/60 bg-primary/5 shadow-2xs'
: 'border-border bg-card hover:bg-muted/30 text-muted-foreground hover:text-foreground',
)
"
@click="toggleAddon(addon.id)"
>
<div class="flex min-w-0 items-center gap-3">
<div
:class="
cn(
'flex size-5 shrink-0 items-center justify-center rounded border transition-colors',
selectedAddonIds.includes(addon.id)
? 'border-primary bg-primary text-primary-foreground'
: 'border-border bg-background',
)
"
>
<Check v-if="selectedAddonIds.includes(addon.id)" class="size-3.5" />
</div>
<div class="min-w-0 space-y-0.5">
<div class="flex items-center gap-2">
<span class="text-foreground text-xs font-bold">{{ addon.name }}</span>
<Badge variant="outline" class="border-border text-muted-foreground text-xs">
{{ addon.category }}
</Badge>
</div>
<div class="text-muted-foreground truncate text-xs">{{ addon.description }}</div>
</div>
</div>
<div class="text-foreground shrink-0 pl-2 font-mono text-xs font-bold">
+${{ addon.monthlyPrice }}<span class="text-muted-foreground text-xs font-normal">/mo</span>
</div>
</button>
</div>
</div>
</div>
<!-- Right: Real-time Invoice Estimate Card (5 Cols) -->
<div class="lg:col-span-5">
<Card class="border-border bg-card/90 sticky top-8 shadow-md backdrop-blur-xs">
<CardContent class="space-y-6 p-6">
<div class="border-border flex items-center justify-between border-b pb-3">
<div class="flex items-center gap-2">
<CreditCard class="text-primary size-4" />
<span class="text-foreground text-sm font-semibold">Estimated Monthly Invoice</span>
</div>
<Badge variant="outline" class="border-border text-primary font-mono text-xs">
{{ isAnnual ? 'Annualized' : 'Monthly' }}
</Badge>
</div>
<!-- Price Breakdown List -->
<div class="space-y-3 text-xs">
<div class="text-muted-foreground flex justify-between">
<span>{{ selectedPlan.name }}</span>
<span class="text-foreground font-mono">${{ selectedPlan.monthlyPrice }}.00</span>
</div>
<div v-if="extraSeats > 0" class="text-muted-foreground flex justify-between">
<span>Extra Seats ({{ extraSeats }} × $15)</span>
<span class="text-foreground font-mono">${{ extraSeatCost }}.00</span>
</div>
<div
v-for="addon in activeAddons.filter((a) => selectedAddonIds.includes(a.id))"
:key="addon.id"
class="text-muted-foreground flex justify-between"
>
<span class="truncate pr-2">{{ addon.name }}</span>
<span class="text-foreground font-mono">${{ addon.monthlyPrice }}.00</span>
</div>
<div
v-if="isAnnual"
class="border-border/60 text-success flex justify-between border-t pt-2 font-medium"
>
<span>Annual Billing Discount (20%)</span>
<span class="font-mono">−${{ monthlySubtotal - finalMonthlyRate }}.00</span>
</div>
</div>
<!-- Total Sum Band -->
<div class="border-border bg-muted/40 space-y-1 rounded-lg border p-4">
<div class="text-muted-foreground text-xs font-medium">Net Monthly Investment</div>
<div class="flex items-baseline gap-1.5">
<span class="text-foreground font-mono text-3xl font-bold tracking-tight">
${{ finalMonthlyRate }}
</span>
<span class="text-muted-foreground font-mono text-xs">/ month</span>
</div>
<div v-if="isAnnual" class="text-success text-xs font-medium">
Billed annually (${{ finalMonthlyRate * 12 }}/yr • Save ${{ annualSavings }}/yr)
</div>
</div>
<Button class="w-full gap-2 shadow-xs" size="lg">
<span>Start 14-Day Free Evaluation</span>
<ArrowRight class="size-4" />
</Button>
</CardContent>
</Card>
</div>
</div>
</div>
</section>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { ArrowRight, CheckCircle2, Sliders } from 'lucide-vue-next'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Card, CardContent } from '@/components/ui/card'
import { cn } from '@/lib/utils'
export interface PricingFeatureTierSliderProps {
title?: string
description?: string
class?: string
}
const props = withDefaults(defineProps<PricingFeatureTierSliderProps>(), {
title: 'Predictable usage pricing with dynamic linear scale.',
description:
'Slide to your estimated monthly active users or edge invocations to calculate your exact monthly investment.',
})
// Slider value from 0 to 100
const sliderIndex = ref(2)
const isAnnual = ref(true)
const TIERS = [
{
label: '10K MAU',
mauCount: '10,000',
basePriceMonthly: 19,
tierName: 'Starter Hobbyist',
features: [
'10,000 Monthly Users',
'2 Dedicated Edge Locations',
'Community Discord SLA',
'Standard 1-Day Log Retention',
],
},
{
label: '50K MAU',
mauCount: '50,000',
basePriceMonthly: 49,
tierName: 'Pro Creator',
features: [
'50,000 Monthly Users',
'12 Global Anycast Edges',
'Next-Business-Day Support SLA',
'7-Day Immutable Log Retention',
],
},
{
label: '250K MAU',
mauCount: '250,000',
basePriceMonthly: 149,
tierName: 'Growth Scale',
features: [
'250,000 Monthly Users',
'All 36 Global Edge Regions',
'4-Hour Priority Engineering SLA',
'30-Day SOC2 Audit Logs',
],
},
{
label: '1M MAU',
mauCount: '1,000,000',
basePriceMonthly: 399,
tierName: 'Enterprise Core',
features: [
'1,000,000 Monthly Users',
'Dedicated VPC & Multi-Region Mesh',
'15-Minute Critical Incident SLA',
'365-Day Cold Storage Logs',
],
},
{
label: '5M+ MAU',
mauCount: '5,000,000+',
basePriceMonthly: 899,
tierName: 'Hyperscale Cluster',
features: [
'5,000,000+ Monthly Users',
'Custom Bare-Metal Cloud Partitions',
'Dedicated Solutions Architect',
'Custom Security Review & BAA',
],
},
]
const currentTier = computed(() => TIERS[sliderIndex.value])
const effectivePrice = computed(() => {
const base = currentTier.value.basePriceMonthly
return isAnnual.value ? Math.round(base * 0.8) : base
})
const annualSavings = computed(() => {
return (currentTier.value.basePriceMonthly - effectivePrice.value) * 12
})
</script>
<template>
<section
data-slot="pricing-feature-tier-slider"
:class="cn('bg-background relative overflow-hidden py-16 sm:py-24', props.class)"
>
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<!-- Section Header -->
<div class="mx-auto max-w-3xl space-y-4 text-center">
<a
href="#scale-pricing"
class="group border-border/80 bg-secondary/60 hover:bg-secondary text-foreground inline-flex items-center gap-2 rounded-full border px-3.5 py-1 text-xs font-medium shadow-2xs transition-colors"
>
<Sliders class="text-primary size-3.5" />
<span>Continuous Scale Synthesizer</span>
<ArrowRight class="text-muted-foreground size-3 transition-transform group-hover:translate-x-0.5" />
</a>
<h2 class="text-foreground text-3xl font-bold tracking-tight sm:text-4xl">
{{ title }}
</h2>
<p class="text-muted-foreground text-base sm:text-lg">
{{ description }}
</p>
<!-- Cadence Switcher -->
<div class="flex items-center justify-center gap-3 pt-2">
<span
:class="cn('text-xs font-medium', !isAnnual ? 'text-foreground font-semibold' : 'text-muted-foreground')"
>
Monthly
</span>
<button
type="button"
:class="
cn(
'relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out',
isAnnual ? 'bg-primary' : 'bg-muted',
)
"
@click="isAnnual = !isAnnual"
>
<span
:class="
cn(
'bg-background pointer-events-none inline-block size-5 transform rounded-full shadow-lg ring-0 transition duration-200 ease-in-out',
isAnnual ? 'translate-x-5' : 'translate-x-0',
)
"
/>
</button>
<span
:class="
cn(
'flex items-center gap-1.5 text-xs font-medium',
isAnnual ? 'text-foreground font-semibold' : 'text-muted-foreground',
)
"
>
<span>Annual Billing</span>
<Badge variant="outline" class="border-success/30 bg-success/10 text-success text-xs"> Save 20% </Badge>
</span>
</div>
</div>
<!-- Pricing Slider Main Card -->
<div class="mx-auto mt-12 max-w-4xl">
<Card class="border-border bg-card overflow-hidden shadow-sm">
<CardContent class="space-y-8 p-8">
<!-- Scale Indicator Bar -->
<div class="border-border flex flex-wrap items-center justify-between gap-4 border-b pb-6">
<div class="space-y-1">
<span class="text-muted-foreground text-xs font-bold tracking-wider uppercase"
>Calculated Tier Plan</span
>
<h3 class="text-foreground text-2xl font-bold">{{ currentTier.tierName }}</h3>
<div class="text-muted-foreground text-xs">
Engineered for {{ currentTier.mauCount }} active user sessions
</div>
</div>
<!-- Price Box -->
<div class="text-right">
<div class="flex items-baseline justify-end gap-1">
<span class="text-foreground font-mono text-4xl font-bold">${{ effectivePrice }}</span>
<span class="text-muted-foreground font-mono text-xs">/ month</span>
</div>
<div v-if="isAnnual" class="text-success font-mono text-xs font-medium">
Save ${{ annualSavings }}/yr on annual
</div>
</div>
</div>
<!-- Interactive Stepped Range Slider -->
<div class="space-y-3">
<div class="text-foreground flex items-center justify-between text-xs font-bold">
<span>Monthly Traffic Volume</span>
<span class="text-primary font-mono font-bold">{{ currentTier.mauCount }} Users</span>
</div>
<input
v-model.number="sliderIndex"
type="range"
min="0"
max="4"
step="1"
class="accent-primary h-2 w-full cursor-pointer"
/>
<!-- Slider Step Labels -->
<div class="text-muted-foreground flex justify-between font-mono text-xs">
<span
v-for="(t, idx) in TIERS"
:key="idx"
:class="
cn(
'hover:text-foreground cursor-pointer transition-colors',
sliderIndex === idx ? 'text-primary font-bold' : '',
)
"
@click="sliderIndex = idx"
>
{{ t.label }}
</span>
</div>
</div>
<!-- Features Checklist Grid for Current Tier -->
<div class="border-border space-y-3 border-t pt-4">
<span class="text-muted-foreground text-xs font-bold tracking-wider uppercase"
>Guaranteed Tier Deliverables</span
>
<div class="grid grid-cols-1 gap-2.5 sm:grid-cols-2">
<div
v-for="(feat, fIdx) in currentTier.features"
:key="fIdx"
class="text-foreground flex items-center gap-2 text-xs"
>
<CheckCircle2 class="text-success size-4 shrink-0" />
<span>{{ feat }}</span>
</div>
</div>
</div>
<!-- Action Button -->
<div class="pt-2">
<Button size="lg" class="w-full gap-2 shadow-xs">
<span>Deploy with {{ currentTier.tierName }}</span>
<ArrowRight class="size-4" />
</Button>
</div>
</CardContent>
</Card>
</div>
</div>
</section>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { Check, ChevronDown, Minus } from 'lucide-vue-next'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Card, CardContent } from '@/components/ui/card'
import { Separator } from '@/components/ui/separator'
import { Table, TableBody, TableCell, TableRow } from '@/components/ui/table'
const plans = ['Team', 'Business', 'Enterprise']
const groups = [
{
id: 'modelling',
label: 'Modelling',
summary: 'Definitions, versioning, and review',
rows: [
{ feature: 'Certified definitions', values: ['50', '500', 'Unlimited'] },
{ feature: 'Version history', values: ['30 days', 'Unlimited', 'Unlimited'] },
{ feature: 'Required review', values: [false, true, true] },
{ feature: 'Period locking', values: [false, true, true] },
],
},
{
id: 'access',
label: 'Access & identity',
summary: 'Who sees which rows',
rows: [
{ feature: 'SSO (SAML)', values: [true, true, true] },
{ feature: 'SCIM provisioning', values: [false, true, true] },
{ feature: 'Row-level scope', values: [false, true, true] },
{ feature: 'Customer-managed keys', values: [false, false, true] },
],
},
{
id: 'ops',
label: 'Operations',
summary: 'Cost, alerting, and support',
rows: [
{ feature: 'Query budgets', values: [false, true, true] },
{ feature: 'Drift alerting', values: [false, true, true] },
{ feature: 'Audit log export', values: [false, '90 days', 'Unlimited'] },
{ feature: 'Named support engineer', values: [false, false, true] },
],
},
]
// Opens on the first group only: a fully expanded matrix is the thing readers
// bounce off, and every group is one click from open.
const open = ref<string[]>([groups[0].id])
function toggle(id: string) {
open.value = open.value.includes(id) ? open.value.filter((entry) => entry !== id) : [...open.value, id]
}
</script>
<template>
<section data-slot="pricing-matrix-grouped" class="bg-background">
<div class="mx-auto max-w-4xl px-6 py-20 lg:py-28">
<Badge variant="secondary">Compare plans</Badge>
<h2 class="mt-4 text-3xl font-semibold tracking-tight sm:text-4xl">Grouped, so it stays readable</h2>
<Card class="mt-8">
<CardContent class="p-0">
<div
class="text-muted-foreground grid grid-cols-[1fr_repeat(3,5rem)] gap-2 px-4 py-3 text-xs font-medium sm:grid-cols-[1fr_repeat(3,7rem)]"
>
<span>Capability</span>
<span v-for="plan in plans" :key="plan" class="text-center">{{ plan }}</span>
</div>
<Separator />
<div v-for="group in groups" :key="group.id">
<button
type="button"
class="hover:bg-muted focus-visible:ring-ring flex w-full items-center gap-3 px-4 py-3 text-left transition-colors focus-visible:ring-2 focus-visible:outline-none"
:aria-expanded="open.includes(group.id)"
:aria-controls="`group-${group.id}`"
@click="toggle(group.id)"
>
<ChevronDown
class="text-muted-foreground size-4 shrink-0 transition-transform"
:class="open.includes(group.id) ? '' : '-rotate-90'"
aria-hidden="true"
/>
<span class="min-w-0">
<span class="block text-sm font-medium">{{ group.label }}</span>
<span class="text-muted-foreground block text-xs">{{ group.summary }}</span>
</span>
<span class="text-muted-foreground ml-auto shrink-0 font-mono text-xs">{{ group.rows.length }}</span>
</button>
<div v-show="open.includes(group.id)" :id="`group-${group.id}`">
<Table>
<TableBody>
<TableRow v-for="row in group.rows" :key="row.feature">
<TableCell class="pl-11 text-sm">{{ row.feature }}</TableCell>
<TableCell v-for="(value, index) in row.values" :key="index" class="w-20 text-center sm:w-28">
<template v-if="value === true">
<Check class="text-success mx-auto size-4" aria-hidden="true" />
<span class="sr-only">Included</span>
</template>
<template v-else-if="value === false">
<Minus class="text-muted-foreground/50 mx-auto size-4" aria-hidden="true" />
<span class="sr-only">Not included</span>
</template>
<span v-else class="font-mono text-xs">{{ value }}</span>
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
<Separator />
</div>
</CardContent>
</Card>
<Button class="mt-6">Start on Business</Button>
</div>
</section>
</template>
<script setup lang="ts">
import { Check, Minus } from 'lucide-vue-next'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Card, CardContent } from '@/components/ui/card'
import { Separator } from '@/components/ui/separator'
// One capability list, three plans referencing it by index — a capability can
// never appear on one card and silently vanish from another.
const capabilities = [
'Certified metric definitions',
'Connected warehouses',
'Query budget per team',
'Row-level access control',
'SCIM provisioning',
'Audit log export',
'Embedded dashboards',
'Data residency choice',
'Customer-managed keys',
'Named support engineer',
]
const plans = [
{ name: 'Team', price: '$0', cadence: 'while in beta', included: [0, 1], note: 'For one team proving it out.' },
{
name: 'Business',
price: '$1,400',
cadence: 'per month',
included: [0, 1, 2, 3, 4, 5, 6],
note: 'For finance and analytics running together.',
featured: true,
},
{
name: 'Enterprise',
price: 'Custom',
cadence: 'annual',
included: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
note: 'For regulated estates and multi-region.',
},
]
</script>
<template>
<section data-slot="pricing-matrix-plan-cards" class="bg-background">
<div class="mx-auto max-w-6xl px-6 py-20 lg:py-28">
<div class="max-w-2xl">
<Badge variant="secondary">Plans</Badge>
<h2 class="mt-4 text-3xl font-semibold tracking-tight sm:text-4xl">What each plan actually includes</h2>
<p class="text-muted-foreground mt-3 text-lg">
The same list on every card, with what is missing shown rather than omitted.
</p>
</div>
<div class="mt-10 grid gap-4 lg:grid-cols-3">
<Card v-for="plan in plans" :key="plan.name" :class="plan.featured ? 'border-primary' : ''">
<CardContent class="flex h-full flex-col p-6">
<div class="flex items-center justify-between gap-3">
<p class="text-sm font-semibold">{{ plan.name }}</p>
<Badge v-if="plan.featured" variant="secondary">Most chosen</Badge>
</div>
<p class="font-display mt-4 text-3xl font-bold tracking-tight">{{ plan.price }}</p>
<p class="text-muted-foreground mt-1 text-xs">{{ plan.cadence }}</p>
<p class="text-muted-foreground mt-3 text-sm leading-relaxed">{{ plan.note }}</p>
<Separator class="my-5" />
<ul class="space-y-2.5">
<li
v-for="(capability, index) in capabilities"
:key="capability"
class="flex items-start gap-2.5 text-sm"
:class="plan.included.includes(index) ? '' : 'text-muted-foreground/60'"
>
<Check
v-if="plan.included.includes(index)"
class="text-success mt-0.5 size-4 shrink-0"
aria-hidden="true"
/>
<Minus v-else class="text-muted-foreground/40 mt-0.5 size-4 shrink-0" aria-hidden="true" />
<span>{{ capability }}</span>
</li>
</ul>
<Button :variant="plan.featured ? 'default' : 'outline'" class="mt-auto pt-0 [&]:mt-8">
{{ plan.name === 'Enterprise' ? 'Talk to us' : `Start on ${plan.name}` }}
</Button>
</CardContent>
</Card>
</div>
</div>
</section>
</template>
<script setup lang="ts">
import { Check, Minus } from 'lucide-vue-next'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Card, CardContent } from '@/components/ui/card'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
const plans = ['Team', 'Business', 'Enterprise']
// Cells are true / false / a string, so a plan can state a limit rather than
// only claiming a feature exists.
const rows = [
{ feature: 'Certified metric definitions', values: ['50', '500', 'Unlimited'] },
{ feature: 'Connected warehouses', values: ['1', '3', 'Unlimited'] },
{ feature: 'Query budget per team', values: [false, true, true] },
{ feature: 'Row-level access control', values: [false, true, true] },
{ feature: 'SCIM provisioning', values: [false, true, true] },
{ feature: 'Audit log export', values: [false, '90 days', 'Unlimited'] },
{ feature: 'Period locking', values: [false, true, true] },
{ feature: 'Embedded dashboards', values: [false, true, true] },
{ feature: 'Data residency choice', values: [false, false, true] },
{ feature: 'Customer-managed keys', values: [false, false, true] },
{ feature: 'Private link', values: [false, false, true] },
{ feature: 'Named support engineer', values: [false, false, true] },
]
</script>
<template>
<section data-slot="pricing-matrix-sticky-header" class="bg-background">
<div class="mx-auto max-w-4xl px-6 py-20 lg:py-28">
<Badge variant="secondary">Compare plans</Badge>
<h2 class="mt-4 text-3xl font-semibold tracking-tight sm:text-4xl">Every line, no asterisks</h2>
<p class="text-muted-foreground mt-3 text-lg">
Limits differ between plans. Where a plan caps something, the cap is written in the cell.
</p>
<Card class="mt-8">
<CardContent class="p-0">
<!-- The header pins with CSS alone. Thirty rows down, a checkmark is
still attributable to a column without scrolling back up. -->
<div class="max-h-[28rem] overflow-auto">
<Table>
<TableHeader class="bg-card sticky top-0 z-10">
<TableRow>
<TableHead class="bg-card">Capability</TableHead>
<TableHead v-for="plan in plans" :key="plan" class="bg-card text-center">{{ plan }}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-for="row in rows" :key="row.feature">
<TableCell class="font-medium">{{ row.feature }}</TableCell>
<TableCell v-for="(value, index) in row.values" :key="index" class="text-center">
<template v-if="value === true">
<Check class="text-success mx-auto size-4" aria-hidden="true" />
<span class="sr-only">Included</span>
</template>
<template v-else-if="value === false">
<Minus class="text-muted-foreground/50 mx-auto size-4" aria-hidden="true" />
<span class="sr-only">Not included</span>
</template>
<span v-else class="font-mono text-xs">{{ value }}</span>
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
</CardContent>
</Card>
<div class="mt-6 flex flex-wrap gap-3">
<Button>Start on Team</Button>
<Button variant="outline">Talk about Enterprise</Button>
</div>
</div>
</section>
</template>
<script setup lang="ts">
import { ArrowRight, Check, X } from 'lucide-vue-next'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Card, CardContent } from '@/components/ui/card'
import { Separator } from '@/components/ui/separator'
const included = [
'Unlimited certified metric definitions',
'Unlimited viewers and editors',
'Row-level access from your identity provider',
'Query budgets enforced per team',
'Audit log export, 7-year retention',
'SOC 2 report under NDA',
]
// Naming exclusions is what makes a single plan credible. A list of only
// inclusions invites the reader to assume the gap is hidden somewhere.
const excluded = [
'On-premise deployment behind your firewall',
'Custom SLAs below 99.9%',
'Professional services beyond the five-week rollout',
]
</script>
<template>
<section data-slot="pricing-single-plan" class="bg-background">
<div class="mx-auto max-w-3xl px-6 py-20 lg:py-28">
<div class="text-center">
<Badge variant="secondary">Pricing</Badge>
<h2 class="mt-4 text-3xl font-semibold tracking-tight text-balance sm:text-4xl">One plan, one number</h2>
<p class="text-muted-foreground mt-3 text-lg">
No seat maths and no feature gates. The only thing that scales is query volume.
</p>
</div>
<Card class="mt-10">
<CardContent class="p-8">
<div class="flex flex-wrap items-baseline gap-3">
<span class="font-display text-4xl font-bold tracking-tight">£1,400</span>
<span class="text-muted-foreground">per month, billed annually</span>
</div>
<p class="text-muted-foreground mt-2 text-sm">
Includes 2 million queries a month. Beyond that it is £0.0004 per query, capped by your own budgets.
</p>
<Button size="lg" class="mt-6 w-full sm:w-auto">
Start the 30-day trial
<ArrowRight class="ml-2 size-4" aria-hidden="true" />
</Button>
<Separator class="my-8" />
<p class="text-muted-foreground font-mono text-xs tracking-[0.14em] uppercase">Included</p>
<ul class="mt-3 grid gap-2.5 sm:grid-cols-2">
<li v-for="item in included" :key="item" class="flex items-start gap-2.5 text-sm">
<Check class="text-success mt-0.5 size-4 shrink-0" aria-hidden="true" />
<span>{{ item }}</span>
</li>
</ul>
<Separator class="my-6" />
<p class="text-muted-foreground font-mono text-xs tracking-[0.14em] uppercase">Not included</p>
<ul class="mt-3 space-y-2.5">
<li v-for="item in excluded" :key="item" class="flex items-start gap-2.5 text-sm">
<X class="text-muted-foreground/50 mt-0.5 size-4 shrink-0" aria-hidden="true" />
<span class="text-muted-foreground">{{ item }}</span>
</li>
</ul>
<Separator class="my-6" />
<p class="text-muted-foreground text-sm leading-relaxed">
Past roughly 20 million queries a month, or if any of the exclusions above are hard requirements, a
conversation will get you a better answer than this page can.
</p>
<Button variant="outline" class="mt-4">Talk to us instead</Button>
</CardContent>
</Card>
</div>
</section>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { ArrowRight, Check, Minus, ShieldCheck } from 'lucide-vue-next'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Card } from '@/components/ui/card'
type BillingCycle = 'monthly' | 'annual'
const billing = ref<BillingCycle>('annual')
interface Plan {
id: string
name: string
description: string
monthlyPrice: number
annualPrice: number
highlight: boolean
badge?: string
ctaText: string
ctaVariant: 'default' | 'outline' | 'secondary'
}
const plans: Plan[] = [
{
id: 'community',
name: 'Community OSS',
description: 'For indie hackers and developers building open source products.',
monthlyPrice: 0,
annualPrice: 0,
highlight: false,
ctaText: 'Start Building Free',
ctaVariant: 'outline',
},
{
id: 'pro',
name: 'Pro Team',
description: 'For growing startup engineering teams that require high velocity.',
monthlyPrice: 29,
annualPrice: 24,
highlight: true,
badge: 'Most Popular',
ctaText: 'Claim Pro License',
ctaVariant: 'default',
},
{
id: 'enterprise',
name: 'Enterprise Scale',
description: 'Dedicated registry syncing, SSO, SOC2 compliance audits, and SLA.',
monthlyPrice: 119,
annualPrice: 99,
highlight: false,
ctaText: 'Talk to Sales',
ctaVariant: 'outline',
},
]
interface FeatureComparisonRow {
category: string
features: {
name: string
tooltip: string
community: boolean | string
pro: boolean | string
enterprise: boolean | string
}[]
}
const comparisonData: FeatureComparisonRow[] = [
{
category: 'Registry & Core Primitives',
features: [
{
name: 'Full AST Component Source Access',
tooltip: 'Raw SFC and TSX source files copied directly into your repository.',
community: true,
pro: true,
enterprise: true,
},
{
name: 'Dual-Framework Parity (Vue + React)',
tooltip: 'Identical DOM semantics and CVA tokens across both ecosystems.',
community: true,
pro: true,
enterprise: true,
},
{
name: 'Curated Marketing & SaaS Blocks',
tooltip: 'Access to 450+ production-grade unbundled layout blocks.',
community: '100+ Blocks',
pro: 'All 450+ Blocks',
enterprise: 'All Blocks + Custom',
},
{
name: 'Tailwind CSS v4 OKLCH Token System',
tooltip: 'Hardware-calibrated color spaces and spring curves.',
community: true,
pro: true,
enterprise: true,
},
],
},
{
category: 'Enterprise & Security Compliance',
features: [
{
name: 'Private Registry Mirroring',
tooltip: 'Host your organization’s customized internal registry behind a firewall.',
community: false,
pro: '1 Private Repo',
enterprise: 'Unlimited Private Hubs',
},
{
name: 'SOC2 & ISO 27001 Audit Packs',
tooltip: 'Pre-certified security documentation and architecture proofs.',
community: false,
pro: false,
enterprise: true,
},
{
name: 'Guaranteed 99.99% Registry CDN SLA',
tooltip: 'Global multi-region edge distribution uptime guarantee.',
community: false,
pro: '99.9% SLA',
enterprise: '99.99% High Availability',
},
{
name: 'Dedicated Design Engineering Support',
tooltip: 'Direct Slack / Discord channel with core design system maintainers.',
community: false,
pro: 'Priority Email',
enterprise: 'Dedicated Slack Channel',
},
],
},
]
</script>
<template>
<section
data-slot="pricing-tier-comparison-matrix"
class="bg-background relative overflow-hidden px-4 py-16 sm:px-6 sm:py-24 lg:px-8"
>
<div class="mx-auto max-w-7xl space-y-16">
<!-- Header -->
<div class="mx-auto max-w-3xl space-y-4 text-center">
<Badge variant="secondary" class="gap-1.5 px-3 py-1 font-mono text-xs shadow-xs">
<ShieldCheck class="text-primary size-3.5" />
Predictable Pricing
</Badge>
<h2 class="text-foreground text-3xl font-bold tracking-tight sm:text-4xl">
Zero seat taxes. Own your source code forever.
</h2>
<p class="text-muted-foreground text-base">
Choose the tier that fits your engineering team's delivery scale and compliance needs.
</p>
<!-- Billing Toggle -->
<div class="flex items-center justify-center gap-3 pt-4">
<span
class="font-mono text-xs"
:class="billing === 'monthly' ? 'text-foreground font-bold' : 'text-muted-foreground'"
>
Monthly
</span>
<div class="border-border bg-card relative flex items-center rounded-full border p-1">
<button
type="button"
class="relative z-10 rounded-full px-3 py-1 font-mono text-xs transition-colors"
:class="
billing === 'monthly'
? 'bg-primary text-primary-foreground font-semibold shadow-xs'
: 'text-muted-foreground'
"
@click="billing = 'monthly'"
>
Monthly
</button>
<button
type="button"
class="relative z-10 flex items-center gap-1.5 rounded-full px-3 py-1 font-mono text-xs transition-colors"
:class="
billing === 'annual'
? 'bg-primary text-primary-foreground font-semibold shadow-xs'
: 'text-muted-foreground'
"
@click="billing = 'annual'"
>
<span>Annual</span>
<span class="bg-success rounded-full px-1.5 py-0.5 text-xs font-bold text-white">Save 20%</span>
</button>
</div>
</div>
</div>
<!-- Pricing Plan Cards Grid (3 Columns) -->
<div class="grid grid-cols-1 items-stretch gap-6 md:grid-cols-3">
<Card
v-for="plan in plans"
:key="plan.id"
class="border-border bg-card relative flex flex-col justify-between space-y-6 rounded-2xl p-6 text-left shadow-xl transition-colors sm:p-8"
:class="
plan.highlight
? 'border-primary/80 ring-primary/20 scale-[1.02] shadow-sm ring-2'
: 'hover:border-border/80'
"
>
<!-- Top Badge -->
<div v-if="plan.badge" class="absolute -top-3 left-1/2 -translate-x-1/2">
<Badge class="px-3 py-0.5 font-mono text-xs tracking-wider uppercase shadow-md">
{{ plan.badge }}
</Badge>
</div>
<div class="space-y-4">
<div>
<h3 class="text-foreground font-mono text-xl font-bold">{{ plan.name }}</h3>
<p class="text-muted-foreground mt-1 min-h-[36px] text-xs">{{ plan.description }}</p>
</div>
<!-- Price display -->
<div class="flex items-baseline gap-1.5 font-mono">
<span class="text-foreground text-4xl font-bold">
${{ billing === 'annual' ? plan.annualPrice : plan.monthlyPrice }}
</span>
<span class="text-muted-foreground text-xs">/ month</span>
</div>
<p class="text-muted-foreground font-mono text-xs">
{{
billing === 'annual' && plan.annualPrice > 0
? 'Billed annually ($' + plan.annualPrice * 12 + '/yr)'
: 'Billed monthly'
}}
</p>
</div>
<Button :variant="plan.ctaVariant" class="h-10 w-full gap-1.5 font-mono text-xs shadow-xs">
<span>{{ plan.ctaText }}</span>
<ArrowRight class="size-3.5" />
</Button>
</Card>
</div>
<!-- Deep Feature Comparison Matrix Table -->
<div class="space-y-6">
<div class="text-center">
<h3 class="text-foreground font-mono text-xl font-bold">Detailed Feature Comparison</h3>
<p class="text-muted-foreground mt-1 text-xs">Full granular matrix of capabilities and entitlements.</p>
</div>
<Card class="border-border bg-card overflow-hidden rounded-2xl text-left shadow-sm">
<div class="overflow-x-auto">
<table class="w-full border-collapse text-left text-xs">
<thead>
<tr class="border-border bg-muted/40 text-muted-foreground border-b font-mono">
<th class="w-1/2 p-4 font-semibold">Capability</th>
<th class="p-4 text-center font-semibold">Community</th>
<th class="text-primary p-4 text-center font-bold font-semibold">Pro Team</th>
<th class="p-4 text-center font-semibold">Enterprise</th>
</tr>
</thead>
<tbody>
<template v-for="(section, sIdx) in comparisonData" :key="sIdx">
<tr class="bg-muted/20 border-border/80 border-b">
<td
colspan="4"
class="text-muted-foreground p-3 px-4 font-mono text-xs font-bold tracking-wider uppercase"
>
{{ section.category }}
</td>
</tr>
<tr
v-for="(row, rIdx) in section.features"
:key="rIdx"
class="border-border/60 hover:bg-muted/10 border-b transition-colors"
>
<td class="p-4">
<div class="text-foreground font-medium">{{ row.name }}</div>
<div class="text-muted-foreground mt-0.5 text-xs">{{ row.tooltip }}</div>
</td>
<td class="p-4 text-center font-mono">
<template v-if="typeof row.community === 'boolean'">
<Check v-if="row.community" class="text-success mx-auto size-4" />
<Minus v-else class="text-muted-foreground/40 mx-auto size-4" />
</template>
<span v-else class="text-muted-foreground">{{ row.community }}</span>
</td>
<td class="p-4 text-center font-mono font-semibold">
<template v-if="typeof row.pro === 'boolean'">
<Check v-if="row.pro" class="text-success mx-auto size-4" />
<Minus v-else class="text-muted-foreground/40 mx-auto size-4" />
</template>
<span v-else class="text-primary">{{ row.pro }}</span>
</td>
<td class="p-4 text-center font-mono">
<template v-if="typeof row.enterprise === 'boolean'">
<Check v-if="row.enterprise" class="text-success mx-auto size-4" />
<Minus v-else class="text-muted-foreground/40 mx-auto size-4" />
</template>
<span v-else class="text-foreground font-semibold">{{ row.enterprise }}</span>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</Card>
</div>
</div>
</section>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { ArrowRight, Calculator, Globe, Server, Users } from 'lucide-vue-next'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Card } from '@/components/ui/card'
// Interactive Sliders
const mau = ref(100000) // 10k to 5M
const qps = ref(500) // 100 to 20k
const edgeReplicas = ref(3) // 1 to 12
function formatNumber(num: number): string {
if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M'
if (num >= 1000) return (num / 1000).toFixed(0) + 'k'
return num.toString()
}
// Preset configurations
function applyPreset(preset: 'seed' | 'growth' | 'scale') {
if (preset === 'seed') {
mau.value = 25000
qps.value = 150
edgeReplicas.value = 2
} else if (preset === 'growth') {
mau.value = 350000
qps.value = 2500
edgeReplicas.value = 5
} else {
mau.value = 2500000
qps.value = 12000
edgeReplicas.value = 10
}
}
// Calculations
const traditionalCost = computed(() => {
const baseSeatFee = 350
const perUserFee = (mau.value / 1000) * 1.8
const qpsSurcharge = qps.value * 0.25
return Math.round(baseSeatFee + perUserFee + qpsSurcharge)
})
const uipkgeCost = computed(() => {
// Flat zero licensing + modest raw edge static bandwidth
const rawBandwidth = (mau.value / 100000) * 4
const flatProLicense = 24
return Math.round(flatProLicense + rawBandwidth)
})
const monthlySavings = computed(() => {
return Math.max(0, traditionalCost.value - uipkgeCost.value)
})
const annualSavings = computed(() => {
return monthlySavings.value * 12
})
</script>
<template>
<section
data-slot="pricing-usage-calculator-slider"
class="bg-background relative overflow-hidden px-4 py-16 sm:px-6 sm:py-24 lg:px-8"
>
<div class="mx-auto max-w-6xl space-y-12">
<!-- Section Header -->
<div class="mx-auto max-w-3xl space-y-4 text-center">
<Badge variant="secondary" class="gap-1.5 px-3 py-1 font-mono text-xs shadow-xs">
<Calculator class="text-primary size-3.5" />
Interactive Infrastructure ROI Calculator
</Badge>
<h2 class="text-foreground text-3xl font-bold tracking-tight sm:text-4xl">
Calculate your annual savings with unbundled architecture.
</h2>
<p class="text-muted-foreground text-base">
See how eliminating proprietary seat licenses and runtime SaaS wrappers cuts your front-end TCO.
</p>
<!-- Presets Bar -->
<div class="flex flex-wrap items-center justify-center gap-2 pt-2">
<Button size="sm" variant="outline" class="font-mono text-xs" @click="applyPreset('seed')">
Seed Startup (25k MAU)
</Button>
<Button size="sm" variant="outline" class="font-mono text-xs" @click="applyPreset('growth')">
Growth Scale (350k MAU)
</Button>
<Button size="sm" variant="outline" class="font-mono text-xs" @click="applyPreset('scale')">
Hypergrowth (2.5M MAU)
</Button>
</div>
</div>
<!-- 2-Column Split: Sliders Workbench Left (7 Cols), Savings Scorecard Right (5 Cols) -->
<div class="grid grid-cols-1 items-stretch gap-8 lg:grid-cols-12">
<!-- Sliders Controls (7 Cols) -->
<Card
class="border-border bg-card/95 flex flex-col justify-between space-y-6 rounded-2xl p-6 text-left shadow-xl sm:p-8 lg:col-span-7"
>
<div class="space-y-6">
<div class="border-border flex items-center justify-between border-b pb-4">
<h3 class="text-foreground font-mono text-sm font-bold">Traffic & Telemetry Inputs</h3>
<span class="text-muted-foreground font-mono text-xs">Dynamic Projection</span>
</div>
<!-- Slider 1: MAU -->
<div class="space-y-2">
<div class="flex items-center justify-between font-mono text-xs">
<span class="text-muted-foreground flex items-center gap-1.5">
<Users class="text-primary size-3.5" /> Monthly Active Users (MAU)
</span>
<span class="text-foreground text-sm font-bold">{{ formatNumber(mau) }} users</span>
</div>
<input
v-model="mau"
type="range"
min="10000"
max="5000000"
step="25000"
class="accent-primary bg-border h-2 w-full cursor-pointer rounded-lg"
/>
</div>
<!-- Slider 2: QPS -->
<div class="space-y-2">
<div class="flex items-center justify-between font-mono text-xs">
<span class="text-muted-foreground flex items-center gap-1.5">
<Server class="text-primary size-3.5" /> Peak Query Throughput
</span>
<span class="text-foreground text-sm font-bold">{{ formatNumber(qps) }} QPS</span>
</div>
<input
v-model="qps"
type="range"
min="100"
max="20000"
step="100"
class="accent-primary bg-border h-2 w-full cursor-pointer rounded-lg"
/>
</div>
<!-- Slider 3: Global Replicas -->
<div class="space-y-2">
<div class="flex items-center justify-between font-mono text-xs">
<span class="text-muted-foreground flex items-center gap-1.5">
<Globe class="text-primary size-3.5" /> Global Edge POP Replicas
</span>
<span class="text-foreground text-sm font-bold">{{ edgeReplicas }} Edge Regions</span>
</div>
<input
v-model="edgeReplicas"
type="range"
min="1"
max="12"
step="1"
class="accent-primary bg-border h-2 w-full cursor-pointer rounded-lg"
/>
</div>
</div>
<div
class="border-border text-muted-foreground flex items-center justify-between border-t pt-4 font-mono text-xs"
>
<span>Formula: Direct AST + Raw Static Hosting</span>
<span class="text-success font-semibold">✓ Zero Seat Surcharges</span>
</div>
</Card>
<!-- ROI Cost & Savings Projection Card (5 Cols) -->
<Card
class="border-border bg-card/95 flex flex-col justify-between space-y-6 rounded-2xl p-6 text-left shadow-sm sm:p-8 lg:col-span-5"
>
<div class="space-y-6">
<Badge variant="outline" class="border-success/20 bg-success/10 text-success font-mono text-xs">
Projected Annual Net Savings
</Badge>
<!-- Big Stat Display -->
<div class="space-y-1">
<div class="text-success font-mono text-4xl font-bold tracking-tight sm:text-5xl">
${{ annualSavings.toLocaleString() }}
</div>
<p class="text-muted-foreground font-mono text-xs">
Saved every year (${{ monthlySavings.toLocaleString() }}/month)
</p>
</div>
<!-- Comparative Cost Bars -->
<div class="space-y-3 pt-2">
<div class="space-y-1">
<div class="flex items-center justify-between font-mono text-xs">
<span class="text-destructive font-medium">Traditional Monolith Stack:</span>
<span class="text-foreground font-bold">${{ traditionalCost.toLocaleString() }}/mo</span>
</div>
<div class="bg-destructive/20 h-2 overflow-hidden rounded-full">
<div class="bg-destructive h-full w-full" />
</div>
</div>
<div class="space-y-1">
<div class="flex items-center justify-between font-mono text-xs">
<span class="text-success font-semibold">UIPKGE Unbundled Registry:</span>
<span class="text-foreground font-bold">${{ uipkgeCost.toLocaleString() }}/mo</span>
</div>
<div class="bg-success/20 h-2 overflow-hidden rounded-full">
<div
class="bg-success h-full transition-[width] duration-300"
:style="{ width: `${Math.max(5, (uipkgeCost / traditionalCost) * 100)}%` }"
/>
</div>
</div>
</div>
</div>
<Button class="mt-4 h-10 w-full gap-1.5 font-mono text-xs shadow-md">
<span>Lock In Pro Savings</span>
<ArrowRight class="size-3.5" />
</Button>
</Card>
</div>
</div>
</section>
</template>
Raw manifest:https://uipkge.dev/r/vue/pricing.json