Pricing Feature Addon Builder
blockpricingInteractive add-on pricing calculator with tier bundling, seat scaling, and real-time invoice generation.
Installation
$pnpm dlx uipkge-ng@latest add pricing-feature-addon-builder$npx uipkge-ng@latest add pricing-feature-addon-builder$yarn dlx uipkge-ng@latest add pricing-feature-addon-builder$bunx uipkge-ng@latest add pricing-feature-addon-builderManifest:
https://uipkge.dev/r/angular/pricing-feature-addon-builder.jsonInstalls to:src/app/components/blocks/pricing-feature-addon-builder/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.
PricingPlaninterface PricingPlan {
id: string
name: string
monthlyPrice: number
description: string
includedSeats: number
}PricingAddoninterface PricingAddon {
id: string
name: string
monthlyPrice: number
description: string
category: string
}npm dependencies
Used by
Files installed (2)
src/app/components/blocks/pricing-feature-addon-builder/pricing-feature-addon-builder.component.ts17.2 kB
// Interactive add-on pricing calculator: tier bundling, seat scaling, and real-time // invoice generation with an annual billing toggle. Port of the React block 1:1. import { ChangeDetectionStrategy, Component, Input, computed, signal } from '@angular/core' import { ArrowRight, Calculator, Check, CreditCard, LucideAngularModule } from 'lucide-angular' import { cn } from '@/lib/utils' import { UiBadgeComponent } from '@/ui/badge/badge.component' import { UiButtonComponent } from '@/ui/button/button.component' import { UiCardComponent, UiCardContentComponent } from '@/ui/card/card.component' 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 } 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', }, ] @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-pricing-feature-addon-builder, [ui-pricing-feature-addon-builder]', 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, UiCardContentComponent], template: ` <section data-slot="pricing-feature-addon-builder" [class]="rootClass"> <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" > <lucide-icon [img]="Calculator" class="text-primary size-3.5" /> <span>Real-time Add-on Cost Synthesizer</span> <lucide-icon [img]="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]="cadenceLabelClass(!isAnnual())">Monthly Billing</span> <button type="button" aria-label="Toggle annual billing" [class]="cadenceTrackClass()" (click)="isAnnual.set(!isAnnual())" > <span aria-hidden="true" [class]="cadenceThumbClass()"></span> </button> <span [class]="cadenceAnnualLabelClass()"> <span>Annual Billing</span> <span ui-badge variant="outline" class="border-success/30 bg-success/10 text-success text-xs"> Save 20% </span> </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"> @for (plan of activePlans(); track plan.id) { <button type="button" [class]="planCardClass(plan.id)" (click)="selectedPlanId.set(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 --> <div ui-card class="border-border bg-card/60 shadow-2xs"> <div ui-card-content 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 type="range" min="2" max="50" step="1" aria-label="Engineer team seats" [value]="seatCount()" (input)="onSeatInput($event)" 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> </div> </div> <!-- 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"> @for (addon of activeAddons(); track addon.id) { <button type="button" [class]="addonCardClass(addon.id)" (click)="toggleAddon(addon.id)"> <div class="flex min-w-0 items-center gap-3"> <div [class]="addonCheckClass(addon.id)"> @if (isAddonSelected(addon.id)) { <lucide-icon [img]="Check" 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> <span ui-badge variant="outline" class="border-border text-muted-foreground text-xs"> {{ addon.category }} </span> </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"> <div ui-card class="border-border bg-card sticky top-8 shadow-sm"> <div ui-card-content 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"> <lucide-icon [img]="CreditCard" class="text-primary size-4" /> <span class="text-foreground text-sm font-semibold">Estimated Monthly Invoice</span> </div> <span ui-badge variant="outline" class="border-border text-primary font-mono text-xs"> {{ isAnnual() ? 'Annualized' : 'Monthly' }} </span> </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> @if (extraSeats() > 0) { <div class="text-muted-foreground flex justify-between"> <span>Extra Seats ({{ extraSeats() }} × $15)</span> <span class="text-foreground font-mono">${{ extraSeatCost() }}.00</span> </div> } @for (addon of selectedAddons(); track addon.id) { <div 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> } @if (isAnnual()) { <div 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> @if (isAnnual()) { <div class="text-success text-xs font-medium"> Billed annually (${{ finalMonthlyRate() * 12 }}/yr • Save ${{ annualSavings() }}/yr) </div> } </div> <button ui-button class="w-full gap-2 shadow-xs" size="lg"> <span>Start 14-Day Free Evaluation</span> <lucide-icon [img]="ArrowRight" class="size-4" /> </button> </div> </div> </div> </div> </div> </section> `, }) export class UiPricingFeatureAddonBuilderComponent { protected readonly ArrowRight = ArrowRight protected readonly Calculator = Calculator protected readonly Check = Check protected readonly CreditCard = CreditCard @Input('title') set titleInput(v: string) { if (v !== undefined) this.titleSignal.set(v) } @Input('description') set descriptionInput(v: string) { if (v !== undefined) this.descriptionSignal.set(v) } @Input('plans') set plansInput(v: PricingPlan[] | undefined) { this.plansSignal.set(v) } @Input('addons') set addonsInput(v: PricingAddon[] | undefined) { this.addonsSignal.set(v) } @Input('class') className?: string private readonly titleSignal = signal('Build your custom plan with transparent, zero-surprise pricing.') private readonly descriptionSignal = signal( 'Select your base tier, adjust seat allocations, and toggle modular enterprise add-ons with real-time invoice calculations.', ) private readonly plansSignal = signal<PricingPlan[] | undefined>(undefined) private readonly addonsSignal = signal<PricingAddon[] | undefined>(undefined) readonly title = computed(() => this.titleSignal()) readonly description = computed(() => this.descriptionSignal()) readonly activePlans = computed(() => this.plansSignal() ?? DEFAULT_PLANS) readonly activeAddons = computed(() => this.addonsSignal() ?? DEFAULT_ADDONS) readonly selectedPlanId = signal('growth') readonly selectedAddonIds = signal<string[]>(['dedicated-ip', 'audit-logs']) readonly seatCount = signal(8) readonly isAnnual = signal(true) readonly selectedPlan = computed(() => { return this.activePlans().find((p) => p.id === this.selectedPlanId()) ?? this.activePlans()[0]! }) readonly extraSeats = computed(() => Math.max(0, this.seatCount() - this.selectedPlan().includedSeats)) readonly extraSeatCost = computed(() => this.extraSeats() * 15) readonly selectedAddons = computed(() => this.activeAddons().filter((a) => this.selectedAddonIds().includes(a.id))) readonly totalAddonsCost = computed(() => this.selectedAddons().reduce((sum, a) => sum + a.monthlyPrice, 0)) readonly monthlySubtotal = computed( () => this.selectedPlan().monthlyPrice + this.extraSeatCost() + this.totalAddonsCost(), ) readonly finalMonthlyRate = computed(() => this.isAnnual() ? Math.round(this.monthlySubtotal() * 0.8) : this.monthlySubtotal(), ) readonly annualSavings = computed(() => (this.monthlySubtotal() - Math.round(this.monthlySubtotal() * 0.8)) * 12) get rootClass(): string { return cn('bg-background relative overflow-hidden py-16 sm:py-24', this.className) } cadenceLabelClass(active: boolean): string { return cn('text-xs font-medium', active ? 'text-foreground font-semibold' : 'text-muted-foreground') } cadenceAnnualLabelClass(): string { return cn( 'flex items-center gap-1.5 text-xs font-medium', this.isAnnual() ? 'text-foreground font-semibold' : 'text-muted-foreground', ) } cadenceTrackClass(): string { return cn( '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', this.isAnnual() ? 'bg-primary' : 'bg-muted', ) } cadenceThumbClass(): string { return cn( 'bg-background pointer-events-none inline-block size-5 transform rounded-full shadow-lg ring-0 transition duration-200 ease-in-out', this.isAnnual() ? 'translate-x-5' : 'translate-x-0', ) } planCardClass(planId: string): string { return cn( 'flex flex-col justify-between rounded-xl border p-4 text-left transition-colors', this.selectedPlanId() === planId ? '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', ) } addonCardClass(addonId: string): string { return cn( 'flex items-center justify-between rounded-xl border p-3.5 text-left transition-colors', this.isAddonSelected(addonId) ? 'border-primary/60 bg-primary/5 shadow-2xs' : 'border-border bg-card hover:bg-muted/30 text-muted-foreground hover:text-foreground', ) } addonCheckClass(addonId: string): string { return cn( 'flex size-5 shrink-0 items-center justify-center rounded border transition-colors', this.isAddonSelected(addonId) ? 'border-primary bg-primary text-primary-foreground' : 'border-border bg-background', ) } isAddonSelected(addonId: string): boolean { return this.selectedAddonIds().includes(addonId) } toggleAddon(addonId: string): void { if (this.isAddonSelected(addonId)) { this.selectedAddonIds.set(this.selectedAddonIds().filter((id) => id !== addonId)) } else { this.selectedAddonIds.set([...this.selectedAddonIds(), addonId]) } } onSeatInput(event: Event): void { const value = Number((event.target as HTMLInputElement).value) if (Number.isFinite(value)) this.seatCount.set(value) } }src/app/components/blocks/pricing-feature-addon-builder/index.ts0.1 kB
export { UiPricingFeatureAddonBuilderComponent, type PricingPlan, type PricingAddon, } from './pricing-feature-addon-builder.component'
Raw manifest:https://uipkge.dev/r/angular/pricing-feature-addon-builder.json