Pricing
blockpricingPricing 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.
Installation
$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.jsonNamed registry:
npx shadcn-vue@latest add @uipkge/pricingInstalls to:app/components/blocks/pricing/Variants
Loading interactive previews…
Loading interactive previews…
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
class | HTMLAttributes['class'] | — | optional |
Schema
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
}[]
}npm dependencies
Files installed (11)
app/components/blocks/pricing/Pricing01.vue22.6 kB
<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>app/components/blocks/pricing/PricingCalculator.vue20 kB
<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>app/components/blocks/pricing/PricingEnterpriseSlaCard.vue8.6 kB
<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>app/components/blocks/pricing/PricingFeatureAddonBuilder.vue14.4 kB
<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>app/components/blocks/pricing/PricingFeatureTierSlider.vue8.6 kB
<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>app/components/blocks/pricing/PricingMatrixGrouped.vue4.9 kB
<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>app/components/blocks/pricing/PricingMatrixPlanCards.vue3.4 kB
<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>app/components/blocks/pricing/PricingMatrixStickyHeader.vue3.5 kB
<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>app/components/blocks/pricing/PricingSinglePlan.vue3.3 kB
<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>app/components/blocks/pricing/PricingTierComparisonMatrix.vue11.3 kB
<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>app/components/blocks/pricing/PricingUsageCalculatorSlider.vue8.7 kB
<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