Virtual Card Manager
blockfinanceMercury/Ramp-style corporate virtual card manager: interactive dark credit card visual hero with EMV chip, revealable credentials, live spending limit progress bar, instant freeze toggles, limit adjusters, and an active virtual cards table.
Installation
$pnpm dlx uipkge-ng@latest add virtual-card-manager$npx uipkge-ng@latest add virtual-card-manager$yarn dlx uipkge-ng@latest add virtual-card-manager$bunx uipkge-ng@latest add virtual-card-managerManifest:
https://uipkge.dev/r/angular/virtual-card-manager.jsonInstalls to:src/app/components/blocks/virtual-card-manager/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.
VirtualCardinterface VirtualCard {
id: string
name: string
brand: 'mastercard' | 'visa'
last4: string
fullNumber: string
holder: string
avatar: string
initials: string
expiry: string
cvv: string
spent: number
limit: number
type: 'Monthly Recurring' | 'Single-use'
status: 'Active' | 'Frozen'
billingCycle: string
purpose: string
}Files installed (2)
src/app/components/blocks/virtual-card-manager/virtual-card-manager.component.ts28.6 kB
// Mercury / Ramp style corporate virtual card manager: interactive dark credit card // visual hero with EMV chip, revealable credentials, live spending limit progress, // instant freeze toggles, limit adjusters, and an active virtual cards table. // Port of the React block 1:1. import { ChangeDetectionStrategy, Component, Input, computed, signal } from '@angular/core' import { Check, CheckCircle2, Copy, CreditCard, Eye, EyeOff, LucideAngularModule, Plus, Radio, ShieldCheck, SlidersHorizontal, Snowflake, Trash2, Zap, } from 'lucide-angular' import { cn } from '@/lib/utils' import { UiAvatarComponent, UiAvatarFallbackComponent, UiAvatarImageComponent } from '@/ui/avatar/avatar.component' import { UiBadgeComponent } from '@/ui/badge/badge.component' import { UiButtonComponent } from '@/ui/button/button.component' import { UiCardComponent, UiCardContentComponent, UiCardDescriptionComponent, UiCardHeaderComponent, UiCardTitleComponent, } from '@/ui/card/card.component' import { UiProgressComponent } from '@/ui/progress/progress.component' import { UiSwitchComponent } from '@/ui/switch/switch.component' export interface VirtualCard { id: string name: string brand: 'mastercard' | 'visa' last4: string fullNumber: string holder: string avatar: string initials: string expiry: string cvv: string spent: number limit: number type: 'Monthly Recurring' | 'Single-use' status: 'Active' | 'Frozen' billingCycle: string purpose: string } const initialCards: VirtualCard[] = [ { id: 'vc-1', name: 'AWS & Cloud Infrastructure', brand: 'mastercard', last4: '8492', fullNumber: '5532 8920 4108 8492', holder: 'Elena Rostova', avatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=100&auto=format&fit=crop&q=80', initials: 'ER', expiry: '08/29', cvv: '482', spent: 4200, limit: 5000, type: 'Monthly Recurring', status: 'Active', billingCycle: 'Resets on 1st of month', purpose: 'Cloud compute, S3 buckets, and RDS instances', }, { id: 'vc-2', name: 'Marketing & Ad Spend', brand: 'visa', last4: '3190', fullNumber: '4012 8831 9204 3190', holder: 'Marcus Chen', avatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=100&auto=format&fit=crop&q=80', initials: 'MC', expiry: '11/28', cvv: '915', spent: 8750, limit: 10000, type: 'Monthly Recurring', status: 'Active', billingCycle: 'Resets on 1st of month', purpose: 'Google Ads, LinkedIn campaigns, and Meta Ads', }, { id: 'vc-3', name: 'Travel & Conferences', brand: 'visa', last4: '6021', fullNumber: '4111 5900 1284 6021', holder: 'Sarah Jenkins', avatar: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=100&auto=format&fit=crop&q=80', initials: 'SJ', expiry: '03/27', cvv: '304', spent: 1850, limit: 2500, type: 'Single-use', status: 'Frozen', billingCycle: 'Single event allowance', purpose: 'Q3 Design Systems Summit flights and lodging', }, { id: 'vc-4', name: 'SaaS Subscriptions', brand: 'mastercard', last4: '9455', fullNumber: '5241 7719 3302 9455', holder: 'Elena Rostova', avatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=100&auto=format&fit=crop&q=80', initials: 'ER', expiry: '05/29', cvv: '729', spent: 620, limit: 1200, type: 'Monthly Recurring', status: 'Active', billingCycle: 'Resets on 1st of month', purpose: 'GitHub Enterprise, Figma, and Slack licenses', }, ] @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-virtual-card-manager, [ui-virtual-card-manager]', 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, UiAvatarComponent, UiAvatarFallbackComponent, UiAvatarImageComponent, UiBadgeComponent, UiButtonComponent, UiCardComponent, UiCardContentComponent, UiCardDescriptionComponent, UiCardHeaderComponent, UiCardTitleComponent, UiProgressComponent, UiSwitchComponent, ], template: ` <div data-slot="virtual-card-manager" [class]="rootClass"> <!-- Header --> <div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between"> <div class="space-y-1"> <div class="flex items-center gap-2.5"> <h2 class="text-foreground text-xl font-semibold tracking-tight sm:text-2xl">Virtual Cards</h2> <span ui-badge wrap variant="secondary" class="font-mono text-xs tabular-nums"> {{ cards().length }} Cards </span> </div> <p class="text-muted-foreground text-sm"> Generate instant corporate cards for subscriptions, vendors, and team expense limits. </p> </div> <div class="flex items-center gap-2"> <button ui-button size="sm" class="gap-1.5 shadow-xs" (click)="issueNewCard()"> <lucide-icon [img]="Plus" class="size-4" /> Issue New Card </button> </div> </div> <!-- Notification / Action Toast feedback --> @if (actionFeedback()) { <div class="bg-primary/10 text-primary border-primary/20 flex items-center justify-between rounded-lg border px-4 py-2.5 text-xs font-medium transition-colors" > <div class="flex items-center gap-2"> <lucide-icon [img]="CheckCircle2" class="size-4 shrink-0" /> <span>{{ actionFeedback() }}</span> </div> <button ui-button aria-label="Dismiss notification" variant="ghost" size="xs" class="h-6 px-2 text-xs" (click)="actionFeedback.set(null)" > Dismiss </button> </div> } <!-- Virtual Card Visual Hero --> <div ui-card class="border-border bg-card shadow-xs"> <div ui-card-header class="border-b pb-4"> <div class="flex flex-wrap items-center justify-between gap-2"> <div> <div class="flex items-center gap-2"> <h3 ui-card-title class="text-base font-semibold">{{ selectedCard().name }}</h3> <span ui-badge wrap [variant]="selectedCard().status === 'Active' ? 'success' : 'warning'" class="text-xs whitespace-normal" > {{ selectedCard().status }} </span> </div> <p ui-card-description class="mt-0.5 text-xs"> {{ selectedCard().purpose }} · {{ selectedCard().billingCycle }} </p> </div> <span ui-badge wrap variant="outline" class="font-mono text-xs"> Ending in {{ selectedCard().last4 }} </span> </div> </div> <div ui-card-content class="pt-6"> <div class="grid gap-6 lg:grid-cols-12 lg:items-center"> <!-- Stylized Dark Credit Card Container --> <div class="lg:col-span-6 xl:col-span-5"> <div [class]="cardVisualClass"> <!-- Ambient light accents --> <div class="bg-chart-1/15 pointer-events-none absolute -top-16 -right-16 size-48 rounded-full blur-2xl" aria-hidden="true" ></div> <div class="bg-success/10 pointer-events-none absolute -bottom-16 -left-16 size-48 rounded-full blur-2xl" aria-hidden="true" ></div> <!-- Frozen watermark overlay --> @if (isSelectedFrozen()) { <div class="absolute inset-0 z-20 flex flex-col items-center justify-center gap-2 rounded-2xl bg-zinc-950/70 backdrop-blur-xs" > <div class="border-warning/40 bg-warning/20 text-warning inline-flex items-center gap-1.5 rounded-full border px-3 py-1 text-xs font-semibold tracking-wider uppercase" > <lucide-icon [img]="Snowflake" class="size-3.5" /> Card Frozen </div> <p class="text-muted-foreground text-xs">Transactions are currently blocked</p> </div> } <!-- Top Row: Chip & Contactless + Brand Logo --> <div class="relative z-10 flex items-center justify-between"> <div class="flex items-center gap-3"> <!-- Metallic EMV Chip --> <div class="border-warning/20/40 relative flex h-8 w-11 items-center justify-center rounded-md border bg-gradient-to-br from-amber-200/30 via-amber-400/20 to-amber-600/30 shadow-inner" aria-hidden="true" > <div class="border-warning/20/30 absolute inset-0 grid grid-cols-3 grid-rows-2"> <div class="border-warning/30 border-r border-b"></div> <div class="border-warning/30 border-r border-b"></div> <div class="border-warning/30 border-b"></div> <div class="border-warning/30 border-r"></div> <div class="border-warning/30 border-r"></div> <div></div> </div> </div> <!-- NFC Wave icon --> <div class="text-muted-foreground/80 rotate-90" aria-hidden="true"> <lucide-icon [img]="Radio" class="size-4" /> </div> </div> <!-- Brand Indicator --> <div class="flex items-center gap-2"> <span class="text-muted-foreground font-mono text-xs font-medium tracking-wider uppercase" >Virtual</span > @if (selectedCard().brand === 'mastercard') { <div class="flex -space-x-2" aria-label="Mastercard"> <div class="bg-destructive/90 size-6 rounded-full"></div> <div class="bg-warning/90 size-6 rounded-full mix-blend-screen"></div> </div> } @else { <div class="text-base font-bold tracking-widest text-zinc-100 italic" aria-label="Visa">VISA</div> } </div> </div> <!-- Middle Row: Card Number & Quick Actions --> <div class="relative z-10 my-auto pt-4"> <div class="flex items-center justify-between gap-2"> <p class="font-mono text-lg font-medium tracking-[0.16em] text-zinc-100 tabular-nums drop-shadow-sm sm:text-xl" > {{ showDetails() ? selectedCard().fullNumber : '•••• •••• •••• ' + selectedCard().last4 }} </p> <div class="flex items-center gap-1"> <button ui-button variant="ghost" size="icon-sm" class="size-7 text-zinc-300 hover:bg-white/10 hover:text-white" [attr.aria-label]="showDetails() ? 'Hide card number' : 'Reveal card number'" (click)="showDetails.set(!showDetails())" > @if (showDetails()) { <lucide-icon [img]="EyeOff" class="size-3.5" /> } @else { <lucide-icon [img]="Eye" class="size-3.5" /> } </button> <button ui-button variant="ghost" size="icon-sm" class="size-7 text-zinc-300 hover:bg-white/10 hover:text-white" aria-label="Copy card number" (click)="handleCopyNumber()" > @if (copied()) { <lucide-icon [img]="Check" class="text-success size-3.5" /> } @else { <lucide-icon [img]="Copy" class="size-3.5" /> } </button> </div> </div> </div> <!-- Bottom Row: Cardholder, Expiry, CVV --> <div class="relative z-10 flex items-end justify-between pt-2"> <div class="space-y-0.5"> <p class="text-muted-foreground font-mono text-xs tracking-wider uppercase">Cardholder</p> <p class="max-w-[140px] truncate text-xs font-semibold tracking-wide text-zinc-200 uppercase"> {{ selectedCard().holder }} </p> </div> <div class="space-y-0.5"> <p class="text-muted-foreground font-mono text-xs tracking-wider uppercase">Expires</p> <p class="font-mono text-xs font-medium text-zinc-200 tabular-nums"> {{ selectedCard().expiry }} </p> </div> <div class="space-y-0.5 text-right"> <p class="text-muted-foreground font-mono text-xs tracking-wider uppercase">CVV</p> <p class="font-mono text-xs font-medium text-zinc-200 tabular-nums"> {{ showDetails() ? selectedCard().cvv : '•••' }} </p> </div> </div> </div> </div> <!-- Card Controls & Spending Breakdown --> <div class="space-y-6 lg:col-span-6 xl:col-span-7"> <!-- Spending Limit Meter --> <div class="border-border bg-muted/30 space-y-3 rounded-lg border p-4"> <div class="flex items-center justify-between"> <span class="text-sm font-medium">Monthly Spending Limit</span> <span class="text-muted-foreground font-mono text-xs tabular-nums"> \${{ selectedCard().spent.toLocaleString() }} / \${{ selectedCard().limit.toLocaleString() }} </span> </div> <ui-progress [value]="selectedPercent()" class="h-2" /> <div class="text-muted-foreground flex items-center justify-between text-xs"> <span class="text-foreground font-medium tabular-nums"> \${{ availableFunds().toLocaleString() }} available </span> <span class="font-medium tabular-nums"> {{ selectedPercent() }}% utilized </span> </div> </div> <!-- Card Controls Row --> <div class="grid gap-3 sm:grid-cols-3"> <!-- Freeze Card Toggle --> <div class="border-border bg-card flex items-center justify-between rounded-lg border p-3 sm:flex-col sm:items-start sm:justify-between sm:gap-2" > <div class="space-y-0.5"> <p class="text-foreground text-xs font-medium">Freeze Card</p> <p class="text-muted-foreground text-xs">Block charges</p> </div> <ui-switch size="sm" aria-label="Toggle card freeze status" [checked]="isSelectedFrozen()" (checkedChange)="toggleFreeze(selectedCard().id)" /> </div> <!-- Change Limit Button --> <button ui-button variant="outline" size="sm" class="h-auto flex-col items-start gap-1 p-3 text-left" (click)="adjustLimit(selectedCard().id)" > <span class="flex w-full items-center justify-between"> <span class="text-foreground text-xs font-medium">Adjust Limit</span> <lucide-icon [img]="SlidersHorizontal" class="text-muted-foreground size-3.5" /> </span> <span class="text-muted-foreground text-xs font-normal">Edit monthly cap</span> </button> <!-- Terminate Card Button --> <button ui-button variant="outline" size="sm" class="text-destructive hover:text-destructive hover:bg-destructive/10 border-border h-auto flex-col items-start gap-1 p-3 text-left" (click)="terminateCard(selectedCard().id)" > <span class="flex w-full items-center justify-between"> <span class="text-xs font-medium">Terminate</span> <lucide-icon [img]="Trash2" class="size-3.5" /> </span> <span class="text-muted-foreground text-xs font-normal">Close permanently</span> </button> </div> <!-- Card Meta Specs --> <div class="grid grid-cols-2 gap-3 pt-1 sm:grid-cols-3"> <div class="space-y-0.5"> <span class="text-muted-foreground text-xs">Cardholder</span> <div class="flex items-center gap-1.5 pt-0.5"> <ui-avatar size="xs" class="size-4.5"> <ui-avatar-image [src]="selectedCard().avatar" [alt]="selectedCard().holder" /> <ui-avatar-fallback class="text-xs">{{ selectedCard().initials }}</ui-avatar-fallback> </ui-avatar> <span class="truncate text-xs font-medium">{{ selectedCard().holder }}</span> </div> </div> <div class="space-y-0.5"> <span class="text-muted-foreground text-xs">Card Type</span> <p class="text-foreground text-xs font-medium">{{ selectedCard().type }}</p> </div> <div class="space-y-0.5"> <span class="text-muted-foreground text-xs">Fraud Protection</span> <div class="text-success flex items-center gap-1 text-xs font-medium"> <lucide-icon [img]="ShieldCheck" class="size-3.5" /> <span>3D Secure Active</span> </div> </div> </div> </div> </div> </div> </div> <!-- Active Cards Grid / Table --> <div ui-card class="border-border bg-card shadow-xs"> <div ui-card-header class="border-b pb-4"> <div class="flex items-center justify-between"> <div> <h3 ui-card-title class="text-base font-semibold">Active Cards List</h3> <p ui-card-description class="mt-0.5 text-xs"> All company virtual cards currently assigned to team members and services. </p> </div> <span ui-badge wrap variant="outline" class="font-mono text-xs tabular-nums"> {{ cards().length }} Issued </span> </div> </div> <div ui-card-content class="p-0"> <div class="divide-border divide-y"> @for (card of cards(); track card.id) { <div [class]="rowClass(card)"> <!-- Card Identity & Number --> <div class="flex min-w-0 items-center gap-3.5 sm:w-1/3"> <div [class]="cardIconClass(card)"> <lucide-icon [img]="CreditCard" class="size-5" /> </div> <div class="min-w-0 flex-1 space-y-0.5"> <div class="flex items-center gap-2"> <p class="text-foreground truncate text-sm font-medium">{{ card.name }}</p> </div> <div class="text-muted-foreground flex items-center gap-2 text-xs"> <span class="font-mono tabular-nums">•••• {{ card.last4 }}</span> <span>·</span> <span class="uppercase">{{ card.brand }}</span> </div> </div> </div> <!-- Cardholder Avatar & Name --> <div class="flex items-center gap-2 sm:w-1/5"> <ui-avatar size="sm"> <ui-avatar-image [src]="card.avatar" [alt]="card.holder" /> <ui-avatar-fallback>{{ card.initials }}</ui-avatar-fallback> </ui-avatar> <div class="min-w-0"> <p class="text-foreground truncate text-xs font-medium">{{ card.holder }}</p> <p class="text-muted-foreground truncate text-xs">{{ card.type }}</p> </div> </div> <!-- Spending Limit & Progress --> <div class="space-y-1.5 sm:w-1/4"> <div class="flex items-center justify-between text-xs"> <span class="text-foreground font-mono font-medium tabular-nums"> \${{ card.spent.toLocaleString() }} / \${{ card.limit.toLocaleString() }} </span> <span class="text-muted-foreground font-mono tabular-nums"> {{ cardPercent(card) }}% </span> </div> <ui-progress [value]="cardPercent(card)" class="h-1.5" /> </div> <!-- Status Badge & Actions --> <div class="flex items-center justify-between gap-3 sm:justify-end"> <span ui-badge wrap [variant]="card.status === 'Active' ? 'success' : 'warning'" class="text-xs whitespace-normal" > {{ card.status }} </span> <div class="flex items-center gap-1.5"> <button ui-button [variant]="selectedId() === card.id ? 'secondary' : 'ghost'" size="sm" class="text-xs" (click)="selectedId.set(card.id)" > {{ selectedId() === card.id ? 'Viewing' : 'View Card' }} </button> <button ui-button variant="ghost" size="icon-sm" [attr.aria-label]="card.status === 'Active' ? 'Freeze card' : 'Unfreeze card'" (click)="toggleFreeze(card.id)" > @if (card.status === 'Active') { <lucide-icon [img]="Snowflake" class="text-muted-foreground size-4" /> } @else { <lucide-icon [img]="Zap" class="text-warning size-4" /> } </button> </div> </div> </div> } </div> </div> </div> </div> `, }) export class UiVirtualCardManagerComponent { protected readonly Check = Check protected readonly CheckCircle2 = CheckCircle2 protected readonly Copy = Copy protected readonly CreditCard = CreditCard protected readonly Eye = Eye protected readonly EyeOff = EyeOff protected readonly Plus = Plus protected readonly Radio = Radio protected readonly ShieldCheck = ShieldCheck protected readonly SlidersHorizontal = SlidersHorizontal protected readonly Snowflake = Snowflake protected readonly Trash2 = Trash2 protected readonly Zap = Zap @Input('class') className?: string @Input() set initialCards(v: VirtualCard[] | undefined) { if (v) this.cards.set(v.map((c) => ({ ...c }))) } readonly cards = signal<VirtualCard[]>(initialCards.map((c) => ({ ...c }))) readonly selectedId = signal<string>('vc-1') readonly showDetails = signal<boolean>(false) readonly copied = signal<boolean>(false) readonly actionFeedback = signal<string | null>(null) private copyTimer: ReturnType<typeof setTimeout> | undefined private feedbackTimer: ReturnType<typeof setTimeout> | undefined readonly selectedCard = computed(() => this.cards().find((c) => c.id === this.selectedId()) ?? this.cards()[0]) readonly selectedPercent = computed(() => { const card = this.selectedCard() if (!card || card.limit === 0) return 0 return Math.min(100, Math.round((card.spent / card.limit) * 100)) }) readonly availableFunds = computed(() => { const card = this.selectedCard() return Math.max(0, card.limit - card.spent) }) readonly isSelectedFrozen = computed(() => this.selectedCard()?.status === 'Frozen') get rootClass(): string { return cn('mx-auto w-full max-w-6xl space-y-6', this.className) } get cardVisualClass(): string { return cn( 'relative aspect-[1.586/1] w-full max-w-[420px] rounded-2xl p-6 text-white transition-[filter] duration-300 lg:max-w-full', 'border border-zinc-700/60 bg-gradient-to-br from-zinc-900 via-neutral-900 to-zinc-950 shadow-xl', this.isSelectedFrozen() && 'contrast-95 grayscale filter', ) } rowClass(card: VirtualCard): string { return cn( 'group flex flex-col gap-4 p-4 transition-colors sm:flex-row sm:items-center sm:justify-between', this.selectedId() === card.id ? 'bg-muted/40' : 'hover:bg-muted/20', ) } cardIconClass(card: VirtualCard): string { return cn( 'border-border flex size-10 shrink-0 items-center justify-center rounded-lg border shadow-xs transition-transform', card.status === 'Frozen' ? 'bg-muted/60 text-muted-foreground' : 'bg-primary/10 text-primary', ) } cardPercent(card: VirtualCard): number { if (card.limit === 0) return 0 return Math.min(100, Math.round((card.spent / card.limit) * 100)) } toggleFreeze(id: string): void { this.cards.update((prev) => prev.map((c) => { if (c.id === id) { const nextStatus = c.status === 'Active' ? 'Frozen' : 'Active' return { ...c, status: nextStatus } } return c }), ) } handleCopyNumber(): void { const card = this.selectedCard() if (!card) return if (typeof navigator !== 'undefined' && navigator.clipboard) { void navigator.clipboard.writeText(card.fullNumber.replace(/\s+/g, '')) } this.copied.set(true) clearTimeout(this.copyTimer) this.copyTimer = setTimeout(() => this.copied.set(false), 2000) } adjustLimit(cardId: string): void { const current = this.cards().find((c) => c.id === cardId) const newLimit = !current ? 0 : current.limit === 5000 ? 7500 : current.limit === 10000 ? 12000 : current.limit + 1000 this.cards.update((prev) => prev.map((c) => { if (c.id === cardId) { const next = c.limit === 5000 ? 7500 : c.limit === 10000 ? 12000 : c.limit + 1000 return { ...c, limit: next } } return c }), ) this.showFeedback(`Limit updated to $${newLimit.toLocaleString()}`) } terminateCard(cardId: string): void { if (this.cards().length <= 1) { this.showFeedback('Cannot terminate the last remaining card') return } const remaining = this.cards().filter((c) => c.id !== cardId) this.cards.set(remaining) if (this.selectedId() === cardId) { this.selectedId.set(remaining[0].id) } this.showFeedback('Card terminated permanently') } issueNewCard(): void { const newId = `vc-${Date.now().toString().slice(-4)}` const newLast4 = Math.floor(1000 + Math.random() * 9000).toString() const newCard: VirtualCard = { id: newId, name: 'AI & API Workloads', brand: 'visa', last4: newLast4, fullNumber: `4400 1284 9912 ${newLast4}`, holder: 'Elena Rostova', avatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=100&auto=format&fit=crop&q=80', initials: 'ER', expiry: '09/30', cvv: '814', spent: 0, limit: 3000, type: 'Monthly Recurring', status: 'Active', billingCycle: 'Resets on 1st of month', purpose: 'OpenAI, Anthropic, and Vector DB infrastructure', } this.cards.update((prev) => [newCard, ...prev]) this.selectedId.set(newId) this.showFeedback('New virtual card issued successfully') } showFeedback(msg: string): void { this.actionFeedback.set(msg) clearTimeout(this.feedbackTimer) this.feedbackTimer = setTimeout(() => { if (this.actionFeedback() === msg) this.actionFeedback.set(null) }, 3000) } }src/app/components/blocks/virtual-card-manager/index.ts0.1 kB
export { UiVirtualCardManagerComponent, type VirtualCard } from './virtual-card-manager.component'
Raw manifest:https://uipkge.dev/r/angular/virtual-card-manager.json