Menu

Change language

GitHub repo

Pricing Tier Comparison Matrix

blockmarketing

Comprehensive pricing tier comparison matrix with billing cycle discount toggles, prominent recommended plan, and granular capability matrix table.

Installation

$npx uipkge-ng@latest add pricing-tier-comparison-matrix
Manifest:https://uipkge.dev/r/angular/pricing-tier-comparison-matrix.jsonInstalls to:src/app/components/blocks/pricing-tier-comparison-matrix/

Variants

Loading interactive previews…

Schema

Type aliases exported from this item's source. Use these to shape the data you pass in.

PricingTierPlan
interface PricingTierPlan {
  id: string
  name: string
  description: string
  monthlyPrice: number
  annualPrice: number
  highlight: boolean
  badge?: string
  ctaText: string
  ctaVariant: ButtonVariant
}
TierFeatureCell
interface TierFeatureCell {
  name: string
  tooltip: string
  community: boolean | string
  pro: boolean | string
  enterprise: boolean | string
}
TierComparisonSection
interface TierComparisonSection {
  category: string
  features: TierFeatureCell[]
}

npm dependencies

Used by

Files installed (2)

  • src/app/components/blocks/pricing-tier-comparison-matrix/pricing-tier-comparison-matrix.component.ts13.1 kB
    // Comprehensive pricing tier comparison matrix with billing cycle discount toggles,
    // prominent recommended plan, and granular capability matrix table.
    // Port of the React block 1:1.
    import { ChangeDetectionStrategy, Component, Input, signal } from '@angular/core'
    import { ArrowRight, Check, LucideAngularModule, Minus, ShieldCheck } from 'lucide-angular'
    import { cn } from '@/lib/utils'
    import { UiBadgeComponent } from '@/ui/badge/badge.component'
    import { UiButtonComponent } from '@/ui/button/button.component'
    import { UiCardComponent } from '@/ui/card/card.component'
    import type { ButtonVariant } from '@/ui/button/button.component'
    
    export type BillingCycle = 'monthly' | 'annual'
    
    export interface PricingTierPlan {
      id: string
      name: string
      description: string
      monthlyPrice: number
      annualPrice: number
      highlight: boolean
      badge?: string
      ctaText: string
      ctaVariant: ButtonVariant
    }
    
    const plans: PricingTierPlan[] = [
      {
        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',
      },
    ]
    
    export interface TierFeatureCell {
      name: string
      tooltip: string
      community: boolean | string
      pro: boolean | string
      enterprise: boolean | string
    }
    
    export interface TierComparisonSection {
      category: string
      features: TierFeatureCell[]
    }
    
    const comparisonData: TierComparisonSection[] = [
      {
        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',
          },
        ],
      },
    ]
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-pricing-tier-comparison-matrix, [ui-pricing-tier-comparison-matrix]',
      standalone: true,
      // React renders the root <section> itself: the host stays out of layout and `class` goes to the root.
      host: { '[attr.class]': '"contents"' },
      imports: [LucideAngularModule, UiBadgeComponent, UiButtonComponent, UiCardComponent],
      template: `
        <section data-slot="pricing-tier-comparison-matrix" [class]="rootClass">
          <div class="mx-auto max-w-7xl space-y-16">
            <!-- Header -->
            <div class="mx-auto max-w-3xl space-y-4 text-center">
              <span ui-badge variant="secondary" class="gap-1.5 px-3 py-1 font-mono text-xs shadow-xs">
                <lucide-icon [img]="ShieldCheck" class="text-primary size-3.5" />
                Predictable Pricing
              </span>
              <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]="cycleLabelClass('monthly')">Monthly</span>
                <div class="border-border bg-card relative flex items-center rounded-full border p-1">
                  <button type="button" [class]="toggleButtonClass('monthly')" (click)="setBilling('monthly')">
                    Monthly
                  </button>
                  <button type="button" [class]="toggleButtonClass('annual')" (click)="setBilling('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">
              @for (plan of plans; track plan.id) {
                <div ui-card [class]="planCardClass(plan)">
                  <!-- Top Badge -->
                  @if (plan.badge) {
                    <div class="absolute -top-3 left-1/2 -translate-x-1/2">
                      <span ui-badge class="px-3 py-0.5 font-mono text-xs tracking-wider uppercase shadow-md">
                        {{ plan.badge }}
                      </span>
                    </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">{{ priceLabel(plan) }}</span>
                      <span class="text-muted-foreground text-xs">/ month</span>
                    </div>
                    <p class="text-muted-foreground font-mono text-xs">{{ billedNote(plan) }}</p>
                  </div>
    
                  <button ui-button [variant]="plan.ctaVariant" class="h-10 w-full gap-1.5 font-mono text-xs shadow-xs">
                    <span>{{ plan.ctaText }}</span>
                    <lucide-icon [img]="ArrowRight" class="size-3.5" />
                  </button>
                </div>
              }
            </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>
    
              <div ui-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>
                      @for (section of comparisonData; track section.category) {
                        <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>
                        @for (row of section.features; track row.name) {
                          <tr 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">
                              @if (row.community === true) {
                                <lucide-icon [img]="Check" class="text-success mx-auto size-4" />
                              } @else if (row.community === false) {
                                <lucide-icon [img]="Minus" class="text-muted-foreground/40 mx-auto size-4" />
                              } @else {
                                <span class="text-muted-foreground">{{ row.community }}</span>
                              }
                            </td>
                            <td class="p-4 text-center font-mono font-semibold">
                              @if (row.pro === true) {
                                <lucide-icon [img]="Check" class="text-success mx-auto size-4" />
                              } @else if (row.pro === false) {
                                <lucide-icon [img]="Minus" class="text-muted-foreground/40 mx-auto size-4" />
                              } @else {
                                <span class="text-primary">{{ row.pro }}</span>
                              }
                            </td>
                            <td class="p-4 text-center font-mono">
                              @if (row.enterprise === true) {
                                <lucide-icon [img]="Check" class="text-success mx-auto size-4" />
                              } @else if (row.enterprise === false) {
                                <lucide-icon [img]="Minus" class="text-muted-foreground/40 mx-auto size-4" />
                              } @else {
                                <span class="text-foreground font-semibold">{{ row.enterprise }}</span>
                              }
                            </td>
                          </tr>
                        }
                      }
                    </tbody>
                  </table>
                </div>
              </div>
            </div>
          </div>
        </section>
      `,
    })
    export class UiPricingTierComparisonMatrixComponent {
      protected readonly ArrowRight = ArrowRight
      protected readonly Check = Check
      protected readonly Minus = Minus
      protected readonly ShieldCheck = ShieldCheck
    
      readonly plans = plans
      readonly comparisonData = comparisonData
    
      @Input() set defaultBilling(v: BillingCycle | undefined) {
        if (v) this.billing.set(v)
      }
      @Input('class') className?: string
    
      readonly billing = signal<BillingCycle>('annual')
    
      get rootClass(): string {
        return cn('bg-background relative overflow-hidden px-4 py-16 sm:px-6 sm:py-24 lg:px-8', this.className)
      }
    
      cycleLabelClass(cycle: BillingCycle): string {
        return cn(this.billing() === cycle ? 'text-foreground font-bold' : 'text-muted-foreground')
      }
    
      toggleButtonClass(cycle: BillingCycle): string {
        return cn(
          'relative z-10 rounded-full px-3 py-1 font-mono text-xs transition-colors',
          cycle === 'annual' && 'flex items-center gap-1.5',
          this.billing() === cycle ? 'bg-primary text-primary-foreground font-semibold shadow-xs' : 'text-muted-foreground',
        )
      }
    
      planCardClass(plan: PricingTierPlan): string {
        return cn(
          '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',
          plan.highlight ? 'border-primary/80 ring-primary/20 scale-[1.02] shadow-sm ring-2' : 'hover:border-border/80',
        )
      }
    
      billedNote(plan: PricingTierPlan): string {
        if (this.billing() === 'annual' && plan.annualPrice > 0) return `Billed annually ($${plan.annualPrice * 12}/yr)`
        return 'Billed monthly'
      }
    
      priceLabel(plan: PricingTierPlan): string {
        return `$${this.billing() === 'annual' ? plan.annualPrice : plan.monthlyPrice}`
      }
    
      setBilling(cycle: BillingCycle): void {
        this.billing.set(cycle)
      }
    }
    
  • src/app/components/blocks/pricing-tier-comparison-matrix/index.ts0.2 kB

Raw manifest:https://uipkge.dev/r/angular/pricing-tier-comparison-matrix.json