Billing Usage Center
blockfinanceStacked billing center: a current-plan card with seat meter, monthly usage rows whose bars shift success to warning to destructive as they approach the limit (with overage notes), and an invoice history table with status pills, an unpaid-balance alert, and an empty state.
Installation
$pnpm dlx uipkge-ng@latest add billing-usage-center$npx uipkge-ng@latest add billing-usage-center$yarn dlx uipkge-ng@latest add billing-usage-center$bunx uipkge-ng@latest add billing-usage-centerManifest:
https://uipkge.dev/r/angular/billing-usage-center.jsonInstalls to:src/app/components/blocks/billing-usage-center/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.
PlanInfointerface PlanInfo {
name: string
price: string
/** Billing cadence shown after the price, e.g. 'month' or 'year'. */
cadence: string
renewalDate: string
seatsUsed: number
seatsTotal: number
}UsageMetricinterface UsageMetric {
id: string
label: string
usedText: string
includedText: string
/** Percent of the included allowance. Above 100 means overage. */
percent: number
/** Shown when percent exceeds 100; falls back to a generic overage note. */
overageNote?: string
}Invoiceinterface Invoice {
id: string
number: string
period: string
amount: string
status: InvoiceStatus
}npm dependencies
Files installed (2)
src/app/components/blocks/billing-usage-center/billing-usage-center.component.ts11.1 kB
// Stacked billing center: a current-plan card with seat meter, monthly usage rows whose // bars shift success → warning → destructive as they approach the limit (with overage // notes), and an invoice history table with status pills, an unpaid-balance alert, and an // empty state. Port of the React block 1:1. import { ChangeDetectionStrategy, Component, Input, computed, signal } from '@angular/core' import { CreditCard, Download, FileText, LucideAngularModule, TriangleAlert } from 'lucide-angular' import { cn } from '@/lib/utils' import { UiAlertDescriptionComponent, UiAlertTitleComponent, UiAlertComponent } from '@/ui/alert/alert.component' import { UiBadgeComponent } from '@/ui/badge/badge.component' import { UiButtonComponent } from '@/ui/button/button.component' import { UiProgressComponent } from '@/ui/progress/progress.component' import { UiSectionCardComponent } from '@/ui/section-card/section-card.component' import { UiTableBodyComponent, UiTableCellComponent, UiTableComponent, UiTableHeadComponent, UiTableHeaderComponent, UiTableRowComponent, } from '@/ui/table/table.component' export interface PlanInfo { name: string price: string /** Billing cadence shown after the price, e.g. 'month' or 'year'. */ cadence: string renewalDate: string seatsUsed: number seatsTotal: number } export interface UsageMetric { id: string label: string usedText: string includedText: string /** Percent of the included allowance. Above 100 means overage. */ percent: number /** Shown when percent exceeds 100; falls back to a generic overage note. */ overageNote?: string } export type InvoiceStatus = 'paid' | 'open' | 'past-due' export interface Invoice { id: string number: string period: string amount: string status: InvoiceStatus } const defaultPlan: PlanInfo = { name: 'Pro', price: '$49', cadence: 'month', renewalDate: 'September 12, 2026', seatsUsed: 18, seatsTotal: 20, } const defaultUsage: UsageMetric[] = [ { id: 'storage', label: 'Storage', usedText: '8.2 GB', includedText: '10 GB', percent: 82 }, { id: 'bandwidth', label: 'Bandwidth', usedText: '412 GB', includedText: '1 TB', percent: 41 }, { id: 'api-calls', label: 'API calls', usedText: '640K', includedText: '1M', percent: 64 }, ] const defaultInvoices: Invoice[] = [ { id: 'inv-003', number: 'INV-2026-003', period: 'Jul 1 – Jul 31, 2026', amount: '$49.00', status: 'paid' }, { id: 'inv-002', number: 'INV-2026-002', period: 'Jun 1 – Jun 30, 2026', amount: '$49.00', status: 'paid' }, { id: 'inv-001', number: 'INV-2026-001', period: 'May 1 – May 31, 2026', amount: '$49.00', status: 'paid' }, ] // Inlined record (not an indexed VariantProps access): keeps the compiler happy. const statusMeta: Record<InvoiceStatus, { label: string; variant: 'success' | 'default' | 'destructive' }> = { paid: { label: 'Paid', variant: 'success' }, open: { label: 'Open', variant: 'default' }, 'past-due': { label: 'Past due', variant: 'destructive' }, } @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-billing-usage-center, [ui-billing-usage-center]', standalone: true, // React renders the root <div> itself: the host stays out of layout and `class` goes to the root. host: { '[attr.class]': '"contents"' }, imports: [ LucideAngularModule, UiAlertComponent, UiAlertDescriptionComponent, UiAlertTitleComponent, UiBadgeComponent, UiButtonComponent, UiProgressComponent, UiSectionCardComponent, UiTableBodyComponent, UiTableCellComponent, UiTableComponent, UiTableHeadComponent, UiTableHeaderComponent, UiTableRowComponent, ], template: ` <div data-slot="billing-usage-center" [class]="rootClass"> <div ui-section-card title="Current plan" [description]="'Renews on ' + plan().renewalDate"> <div class="flex items-start justify-between gap-4"> <div> <p class="text-2xl font-bold tracking-tight">{{ plan().name }}</p> <p class="text-muted-foreground mt-1 text-sm"> {{ plan().price }} <span class="text-xs">/ {{ plan().cadence }}</span> </p> </div> </div> <div class="mt-6 space-y-1.5"> <div class="flex flex-wrap items-baseline justify-between gap-4 text-sm"> <span class="font-medium">Seats used</span> <span class="text-muted-foreground text-xs tabular-nums" >{{ plan().seatsUsed }} of {{ plan().seatsTotal }}</span > </div> <ui-progress [value]="seatsPercent()" /> <p class="text-muted-foreground text-xs">{{ seatsRemaining() }} seats remaining on this plan.</p> </div> <div class="mt-6 flex flex-wrap justify-end gap-2"> <button ui-button variant="outline" size="sm"> <lucide-icon [img]="CreditCard" /> Manage payment </button> <button ui-button size="sm">Change plan</button> </div> </div> <div ui-section-card title="Usage this month" description="Metered against your plan allowances."> <div class="space-y-5"> @for (metric of usage(); track metric.id) { <div class="space-y-1.5"> <div class="flex flex-wrap items-baseline justify-between gap-4 text-sm"> <span class="font-medium">{{ metric.label }}</span> <span class="text-muted-foreground text-xs tabular-nums"> {{ metric.usedText }} of {{ metric.includedText }} </span> </div> <ui-progress [value]="Math.min(metric.percent, 100)" [class]="usageBarClass(metric.percent)" /> @if (metric.percent > 100) { <p class="text-destructive text-xs"> {{ metric.overageNote ?? 'Over the ' + metric.includedText + ' included in your plan — overage charges apply.' }} </p> } </div> } </div> </div> <div ui-section-card title="Invoice history" [description]="invoices().length + ' invoice' + (invoices().length === 1 ? '' : 's')" > @if (invoices().length > 0) { <div class="space-y-4"> @if (unpaidInvoices().length > 0) { <div ui-alert class="border-warning/40 bg-warning/5"> <lucide-icon [img]="TriangleAlert" class="text-warning size-4" /> <h5 ui-alert-title>Past-due balance</h5> <p ui-alert-description>{{ unpaidNote() }}</p> </div> } <div class="overflow-x-auto"> <table ui-table> <thead ui-table-header> <tr ui-table-row> <th ui-table-head>Invoice</th> <th ui-table-head>Period</th> <th ui-table-head class="text-right">Amount</th> <th ui-table-head>Status</th> <th ui-table-head class="w-12"> <span class="sr-only">Download</span> </th> </tr> </thead> <tbody ui-table-body> @for (invoice of invoices(); track invoice.id) { <tr ui-table-row> <td ui-table-cell class="font-medium">{{ invoice.number }}</td> <td ui-table-cell class="text-muted-foreground">{{ invoice.period }}</td> <td ui-table-cell class="text-right tabular-nums">{{ invoice.amount }}</td> <td ui-table-cell> <span ui-badge [variant]="invoiceVariant(invoice.status)">{{ invoiceLabel(invoice.status) }}</span> </td> <td ui-table-cell class="text-right"> <button ui-button variant="ghost" size="icon-sm" [attr.aria-label]="'Download invoice ' + invoice.number" > <lucide-icon [img]="Download" /> </button> </td> </tr> } </tbody> </table> </div> </div> } @else { <div class="flex flex-col items-center justify-center px-6 py-10 text-center"> <div class="bg-muted mb-3 rounded-full p-3"> <lucide-icon [img]="FileText" class="text-muted-foreground size-5" /> </div> <p class="text-sm font-medium">No invoices yet</p> <p class="text-muted-foreground mt-0.5 text-xs">Invoices appear here after your first billing cycle.</p> </div> } </div> </div> `, }) export class UiBillingUsageCenterComponent { protected readonly CreditCard = CreditCard protected readonly Download = Download protected readonly FileText = FileText protected readonly Math = Math protected readonly TriangleAlert = TriangleAlert readonly plan = signal<PlanInfo>({ ...defaultPlan }) readonly usage = signal<UsageMetric[]>(defaultUsage.map((m) => ({ ...m }))) readonly invoices = signal<Invoice[]>(defaultInvoices.map((i) => ({ ...i }))) @Input('plan') set planValue(v: PlanInfo | undefined) { if (v) this.plan.set({ ...v }) } @Input('usage') set usageValue(v: UsageMetric[] | undefined) { if (v) this.usage.set(v.map((m) => ({ ...m }))) } @Input('invoices') set invoicesValue(v: Invoice[] | undefined) { if (v) this.invoices.set(v.map((i) => ({ ...i }))) } @Input('class') className?: string readonly seatsPercent = computed(() => { const p = this.plan() return p.seatsTotal > 0 ? (p.seatsUsed / p.seatsTotal) * 100 : 0 }) readonly seatsRemaining = computed(() => Math.max(this.plan().seatsTotal - this.plan().seatsUsed, 0)) readonly unpaidInvoices = computed(() => this.invoices().filter((invoice) => invoice.status !== 'paid')) readonly pastDueCount = computed(() => this.invoices().filter((invoice) => invoice.status === 'past-due').length) readonly unpaidNote = computed(() => { const count = this.unpaidInvoices().length const past = this.pastDueCount() const plural = count === 1 ? '' : 's' const pastPart = past > 0 ? `, ${past} of them past due` : '' return `You have ${count} unpaid invoice${plural}${pastPart}. Update your payment method to avoid service interruption.` }) get rootClass(): string { return cn('mx-auto w-full max-w-3xl space-y-4', this.className) } invoiceLabel(status: InvoiceStatus): string { return statusMeta[status].label } invoiceVariant(status: InvoiceStatus): 'success' | 'default' | 'destructive' { return statusMeta[status].variant } // success below 80%, warning as usage approaches the limit, destructive at/over it. usageBarClass(percent: number): string { if (percent >= 100) return '[&_[data-slot=progress-indicator]]:bg-destructive' if (percent >= 80) return '[&_[data-slot=progress-indicator]]:bg-warning' return '[&_[data-slot=progress-indicator]]:bg-success' } }src/app/components/blocks/billing-usage-center/index.ts0.2 kB
export { UiBillingUsageCenterComponent, type Invoice, type InvoiceStatus, type PlanInfo, type UsageMetric, } from './billing-usage-center.component'
Raw manifest:https://uipkge.dev/r/angular/billing-usage-center.json