Seat Pricing Matrix
blockmarketingComprehensive enterprise pricing workbench with monthly/annual billing cycle toggle, currency selector (USD/EUR/GBP), team seat simulator slider, dynamic add-on calculators, and an expandable side-by-side feature comparison matrix.
Installation
$pnpm dlx shadcn@latest add https://uipkge.dev/r/lit/pricing-01.json$npx shadcn@latest add https://uipkge.dev/r/lit/pricing-01.json$yarn dlx shadcn@latest add https://uipkge.dev/r/lit/pricing-01.json$bunx shadcn@latest add https://uipkge.dev/r/lit/pricing-01.jsonManifest:
https://uipkge.dev/r/lit/pricing-01.jsonInstalls to:src/uip/blocks/pricing-01/Variants
Loading interactive previews…
Loading interactive previews…
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
HTMLElementTagNameMapinterface HTMLElementTagNameMap {
'uip-pricing-01': UipPricing01
}
}Files installed (1)
src/uip/blocks/pricing-01/pricing-01.ts30.6 kB
import { LitElement, css, html } from 'lit' import { ArrowRight, Check, ChevronDown, ChevronUp, ShieldCheck, Users, Zap } from 'lucide' import { cn } from '../../lib/utils' import { tailwind } from '../../lib/styles' import { ThemeController } from '../../lib/theme' import { icon } from '../../lib/icon' // Side-effect imports: register the composed elements for standalone use // (consumers who install only this block still get working primitives). import '../../components/badge/badge' import '../../components/button/button' import '../../components/card/card' import '../../components/separator/separator' import '../../components/slider/slider' import '../../components/switch/switch' import '../../components/toggle-group/toggle-group' import type { UipToggleGroup } from '../../components/toggle-group/toggle-group' type BillingCycle = 'monthly' | 'yearly' type Currency = 'USD' | 'EUR' | 'GBP' const currencySymbols: Record<Currency, string> = { USD: '$', EUR: '€', GBP: '£', } const currencyRates: Record<Currency, number> = { USD: 1.0, EUR: 0.92, GBP: 0.79, } 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' }, ], }, ] // Rendered class strings of the registry Table primitives verbatim // (packages/registry-react/components/table/table.tsx), composed with cn() // exactly like the React block does. const rowBase = 'hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors duration-150' const headBase = "text-foreground h-10 px-3 text-left align-middle text-sm font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]" const cellBase = "px-3 py-2 align-middle text-sm whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]" /** * <uip-pricing-01> — the registry Pricing01 block as a web component. * * Class strings are React's Pricing01 verbatim. React state (useState) becomes * @state fields; the billing/currency switchers become <uip-toggle-group> * (value + value-change, with React's `v && set` guard), the seat simulator * becomes <uip-slider> (value + value-change), the add-ons become * <uip-switch> (.checked + checked-change), and the comparison matrix is a * native table carrying the registry Table's rendered classes verbatim. */ export class UipPricing01 extends LitElement { static shadowRootOptions = { ...LitElement.shadowRootOptions, delegatesFocus: true } // :host display can't be a utility class (the host has no template of its own). static styles = [tailwind, css`:host { display: block; }`] static properties = { billingCycle: { state: true }, currency: { state: true }, teamSeats: { state: true }, addDedicatedSla: { state: true }, addAuditVault: { state: true }, showComparisonTable: { state: true }, } private billingCycle: BillingCycle = 'yearly' private currency: Currency = 'USD' private teamSeats: number[] = [12] private addDedicatedSla = false private addAuditVault = false private showComparisonTable = false constructor() { super() new ThemeController(this) } connectedCallback() { super.connectedCallback() this.setAttribute('data-uipkge', '') this.setAttribute('data-slot', 'pricing-01') } // React guards onValueChange with `v && setBillingCycle(v)`: clicking the // pressed item must keep the old value. The group clears itself internally // first, so push the kept value back to stay in sync. private onSingleToggle(id: string, value: string | string[], apply: (v: string) => void) { if (typeof value === 'string' && value) { apply(value) return } const el = this.renderRoot.querySelector(id) as UipToggleGroup | null if (el) el.value = (el.value || value) as string this.requestUpdate() } render() { const rates = currencyRates[this.currency] const sym = currencySymbols[this.currency] const starterBase = this.billingCycle === 'yearly' ? 12 : 15 const teamBase = this.billingCycle === 'yearly' ? 28 : 35 const enterpriseBase = this.billingCycle === 'yearly' ? 68 : 85 const slaCost = this.addDedicatedSla ? 99 : 0 const vaultCost = this.addAuditVault ? 49 : 0 const currentSeats = this.teamSeats[0] || 1 const starterMonthlyTotal = Math.round((starterBase * currentSeats + (this.addAuditVault ? 29 : 0)) * rates) const teamMonthlyTotal = Math.round((teamBase * currentSeats + slaCost + vaultCost) * rates) const annualSavingsTeam = Math.round((35 * currentSeats * 12 - 28 * currentSeats * 12) * rates) return html` <section data-slot="pricing-01" class="bg-background w-full py-16 sm:py-24"> <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8"> <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" > ${icon(ShieldCheck, 'shield-check', '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> <div class="mt-8 flex flex-wrap items-center justify-center gap-4 pt-2"> <div class="border-border bg-card inline-flex rounded-lg border p-1 shadow-2xs"> <uip-toggle-group id="billing-toggle" type="single" .value=${this.billingCycle} @value-change=${(e: CustomEvent<{ value: string | string[] }>) => { this.onSingleToggle('#billing-toggle', e.detail.value, (v) => { this.billingCycle = v as BillingCycle }) }} > <uip-toggle-group-item value="monthly" class="px-3.5 py-1.5 text-xs font-medium"> Monthly </uip-toggle-group-item> <uip-toggle-group-item value="yearly" class="flex items-center gap-1.5 px-3.5 py-1.5 text-xs font-medium"> <span>Annual</span> <uip-badge variant="secondary" class="bg-primary/10 text-primary border-primary/20 px-1.5 py-0 text-xs"> Save 20% </uip-badge> </uip-toggle-group-item> </uip-toggle-group> </div> <div class="border-border bg-card inline-flex rounded-lg border p-1 shadow-2xs"> <uip-toggle-group id="currency-toggle" type="single" .value=${this.currency} @value-change=${(e: CustomEvent<{ value: string | string[] }>) => { this.onSingleToggle('#currency-toggle', e.detail.value, (v) => { this.currency = v as Currency }) }} > <uip-toggle-group-item value="USD" class="px-2.5 py-1 font-mono text-xs font-medium"> USD ($) </uip-toggle-group-item> <uip-toggle-group-item value="EUR" class="px-2.5 py-1 font-mono text-xs font-medium"> EUR (€) </uip-toggle-group-item> <uip-toggle-group-item value="GBP" class="px-2.5 py-1 font-mono text-xs font-medium"> GBP (£) </uip-toggle-group-item> </uip-toggle-group> </div> </div> <div class="border-border bg-card mx-auto mt-6 max-w-xl rounded-xl border p-4 shadow-2xs"> <div class="flex flex-wrap items-center justify-between gap-2 text-xs"> <div class="text-foreground flex items-center gap-2 font-medium"> ${icon(Users, 'users', '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> ${this.billingCycle === 'yearly' && annualSavingsTeam > 0 ? html`<span class="text-success text-xs font-medium"> (Saves ~${sym}${annualSavingsTeam.toLocaleString()}/yr on Team plan) </span>` : ''} </div> </div> <div class="mt-3"> <uip-slider class="cursor-pointer" .value=${this.teamSeats} min="1" max="50" step="1" @value-change=${(e: CustomEvent<number[]>) => { this.teamSeats = e.detail }} ></uip-slider> </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> <div class="mt-12 grid grid-cols-1 gap-8 lg:grid-cols-3 lg:items-stretch"> <uip-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> <uip-card-header class="pb-4"> <div class="flex items-center justify-between"> <uip-badge variant="outline" class="font-mono text-xs">Bootstrap & Indie</uip-badge> </div> <uip-card-title class="text-foreground text-xl font-bold tracking-tight"> Starter Studio </uip-card-title> <uip-card-description class="text-muted-foreground text-xs"> For agile squads and early-stage product teams validating core workflows. </uip-card-description> <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> </uip-card-header> <uip-card-content class="space-y-4 pt-2"> <uip-separator></uip-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"> ${icon(Check, 'check', '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"> ${icon(Check, 'check', '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"> ${icon(Check, 'check', '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"> ${icon(Check, 'check', '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"> ${icon(Check, 'check', 'text-muted-foreground/50 mt-0.5 size-4 shrink-0')} <span>Community Discord access & quickstarts</span> </li> </ul> </div> </uip-card-content> </div> <uip-card-footer class="border-border mt-auto border-t pt-4"> <uip-button variant="outline" class="h-10 w-full gap-1.5 text-xs font-semibold"> <span>Start Free 14-Day Trial</span> ${icon(ArrowRight, 'arrow-right', 'size-3.5')} </uip-button> </uip-card-footer> </uip-card> <uip-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" > <uip-badge class="bg-primary text-primary-foreground gap-1.5 px-3 py-0.5 text-xs font-semibold shadow-sm"> ${icon(Zap, 'zap', 'size-3 fill-current')} <span>Most Popular Choice</span> </uip-badge> </div> <div> <uip-card-header class="pt-7 pb-4"> <div class="flex items-center justify-between"> <uip-badge variant="secondary" class="bg-primary/10 text-primary border-primary/20 font-mono text-xs"> Scale & High-Growth </uip-badge> </div> <uip-card-title class="text-foreground text-2xl font-bold tracking-tight">Team Scale</uip-card-title> <uip-card-description class="text-muted-foreground text-xs"> For fast-scaling engineering and product organizations requiring deep automation. </uip-card-description> <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> </uip-card-header> <uip-card-content class="space-y-4 pt-2"> <uip-separator></uip-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"> ${icon(Check, 'check', '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"> ${icon(Check, 'check', '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"> ${icon(Check, 'check', 'text-success mt-0.5 size-4 shrink-0')} <span>Google & Okta SAML SSO integration</span> </li> <li class="flex items-start gap-2"> ${icon(Check, 'check', 'text-success mt-0.5 size-4 shrink-0')} <span>365-day immutable compliance logs</span> </li> <li class="flex items-start gap-2"> ${icon(Check, 'check', 'text-success mt-0.5 size-4 shrink-0')} <span>Priority ticketing with 4h guaranteed SLA</span> </li> </ul> </div> <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="lit-sla-addon" class="text-muted-foreground cursor-pointer"> 24/7 Dedicated SLA (+${sym}${Math.round(99 * rates)}/mo) </label> <uip-switch id="lit-sla-addon" .checked=${this.addDedicatedSla} @checked-change=${(e: CustomEvent<boolean>) => { this.addDedicatedSla = e.detail }} ></uip-switch> </div> <div class="flex items-center justify-between text-xs"> <label for="lit-vault-addon" class="text-muted-foreground cursor-pointer"> SOC 2 Vault Streaming (+${sym}${Math.round(49 * rates)}/mo) </label> <uip-switch id="lit-vault-addon" .checked=${this.addAuditVault} @checked-change=${(e: CustomEvent<boolean>) => { this.addAuditVault = e.detail }} ></uip-switch> </div> </div> </uip-card-content> </div> <uip-card-footer class="border-border mt-auto border-t pt-4"> <uip-button class="h-10 w-full gap-1.5 text-xs font-semibold shadow-sm"> <span>Deploy Team Workspace</span> ${icon(ArrowRight, 'arrow-right', 'size-3.5')} </uip-button> </uip-card-footer> </uip-card> <uip-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> <uip-card-header class="pb-4"> <div class="flex items-center justify-between"> <uip-badge variant="outline" class="font-mono text-xs">Enterprise & Security</uip-badge> </div> <uip-card-title class="text-foreground text-xl font-bold tracking-tight"> Enterprise Suite </uip-card-title> <uip-card-description class="text-muted-foreground text-xs"> Dedicated infrastructure, custom security controls, and bespoke compliance SLAs. </uip-card-description> <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> </uip-card-header> <uip-card-content class="space-y-4 pt-2"> <uip-separator></uip-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"> ${icon(Check, 'check', '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"> ${icon(Check, 'check', '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"> ${icon(Check, 'check', '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"> ${icon(Check, 'check', '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"> ${icon(Check, 'check', 'text-success mt-0.5 size-4 shrink-0')} <span>Custom Master Service Agreement (MSA) & DPA</span> </li> </ul> </div> </uip-card-content> </div> <uip-card-footer class="border-border mt-auto border-t pt-4"> <uip-button variant="outline" class="h-10 w-full gap-1.5 text-xs font-semibold"> ${icon(ShieldCheck, 'shield-check', 'text-primary size-3.5')} <span>Contact Solutions Team</span> </uip-button> </uip-card-footer> </uip-card> </div> <div class="mt-12 text-center"> <uip-button variant="ghost" size="sm" class="text-foreground hover:bg-muted gap-2 text-xs font-semibold" @click=${() => { this.showComparisonTable = !this.showComparisonTable }} > <span> ${this.showComparisonTable ? 'Hide Detailed Feature Comparison' : 'Compare All Features & Enterprise Limits'} </span> ${this.showComparisonTable ? icon(ChevronUp, 'chevron-up', 'size-4') : icon(ChevronDown, 'chevron-down', 'size-4')} </uip-button> </div> ${this.showComparisonTable ? html` <div 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> <uip-badge variant="outline" class="font-mono text-xs"> All plans include SSL & automated backups </uip-badge> </div> <div class="overflow-x-auto"> <div data-uipkge="" data-slot="table-container" class="relative w-full overflow-auto"> <table data-uipkge="" data-slot="table" class="w-full caption-bottom text-sm"> <thead data-uipkge="" data-slot="table-header" class="bg-muted/50 [&_tr]:border-b"> <tr data-uipkge="" data-slot="table-row" class=${cn(rowBase, 'bg-muted/40')}> <th data-uipkge="" data-slot="table-head" class=${cn(headBase, 'text-foreground w-[34%] text-xs font-semibold')} > Capabilities </th> <th data-uipkge="" data-slot="table-head" class=${cn(headBase, 'text-foreground w-[22%] text-xs font-semibold')} > Starter Studio </th> <th data-uipkge="" data-slot="table-head" class=${cn(headBase, 'text-primary w-[22%] text-xs font-bold font-semibold')} > Team Scale </th> <th data-uipkge="" data-slot="table-head" class=${cn(headBase, 'text-foreground w-[22%] text-xs font-semibold')} > Enterprise Suite </th> </tr> </thead> <tbody data-uipkge="" data-slot="table-body" class="[&_tr:last-child]:border-0"> ${comparisonMatrix.map( (group) => html` <tr data-uipkge="" data-slot="table-row" class=${cn(rowBase, 'bg-muted/60 text-muted-foreground text-xs font-semibold')} > <td data-uipkge="" data-slot="table-cell" colSpan="4" class=${cn(cellBase, 'py-2.5 font-mono text-xs tracking-wider uppercase')} > ${group.category} </td> </tr> ${group.features.map( (feat) => html` <tr data-uipkge="" data-slot="table-row" class=${cn(rowBase, 'text-xs')}> <td data-uipkge="" data-slot="table-cell" class=${cn(cellBase, 'text-foreground py-3 font-medium')} > ${feat.name} </td> <td data-uipkge="" data-slot="table-cell" class=${cn(cellBase, 'text-muted-foreground py-3')} > ${feat.starter} </td> <td data-uipkge="" data-slot="table-cell" class=${cn(cellBase, 'text-foreground bg-primary/5 py-3 font-medium')} > ${feat.team} </td> <td data-uipkge="" data-slot="table-cell" class=${cn(cellBase, 'text-foreground py-3')} > ${feat.enterprise} </td> </tr> `, )} `, )} </tbody> </table> </div> </div> </div> ` : ''} </div> </section> ` } } customElements.get('uip-pricing-01') || customElements.define('uip-pricing-01', UipPricing01) declare global { interface HTMLElementTagNameMap { 'uip-pricing-01': UipPricing01 } }
Raw manifest:https://uipkge.dev/r/lit/pricing-01.json