UIPackage
Menu

Framework

Change language

Boilerplate repo

Virtual Card Manager

blockfinance

Mercury/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.

Also available for React ->

Installation

$npx shadcn-vue@latest add https://uipkge.dev/r/vue/virtual-card-manager.json
Named registry:npx shadcn-vue@latest add @uipkge/virtual-card-managerInstalls to:app/components/blocks/

Variants

Loading interactive previews…

Props

NameType / ValuesDefaultRequired
classHTMLAttributes['class']optional

Schema

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

VirtualCard
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
}

Files installed (1)

  • app/components/blocks/VirtualCardManager.vue23.9 kB
    <script setup lang="ts">
    import { computed, ref } from 'vue'
    import type { HTMLAttributes } from 'vue'
    import {
      Check,
      CheckCircle2,
      Copy,
      CreditCard,
      Eye,
      EyeOff,
      Plus,
      Radio,
      ShieldCheck,
      SlidersHorizontal,
      Snowflake,
      Trash2,
      Zap,
    } from 'lucide-vue-next'
    import { cn } from '@/lib/utils'
    import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
    import { Badge } from '@/components/ui/badge'
    import { Button } from '@/components/ui/button'
    import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
    import { Progress } from '@/components/ui/progress'
    import { Switch } from '@/components/ui/switch'
    
    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 props = defineProps<{
      class?: HTMLAttributes['class']
    }>()
    
    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',
      },
    ]
    
    const cards = ref<VirtualCard[]>(initialCards)
    const selectedId = ref<string>('vc-1')
    const showDetails = ref<boolean>(false)
    const copied = ref<boolean>(false)
    const actionFeedback = ref<string | null>(null)
    
    const selectedCard = computed(() => cards.value.find((c) => c.id === selectedId.value) ?? cards.value[0])
    
    const selectedPercent = computed(() => {
      if (!selectedCard.value || selectedCard.value.limit === 0) return 0
      return Math.min(100, Math.round((selectedCard.value.spent / selectedCard.value.limit) * 100))
    })
    
    const isSelectedFrozen = computed(() => selectedCard.value?.status === 'Frozen')
    
    function toggleFreeze(id: string) {
      cards.value = cards.value.map((c) => {
        if (c.id === id) {
          const nextStatus = c.status === 'Active' ? 'Frozen' : 'Active'
          return { ...c, status: nextStatus }
        }
        return c
      })
    }
    
    function handleCopyNumber() {
      if (!selectedCard.value) return
      if (navigator?.clipboard) {
        navigator.clipboard.writeText(selectedCard.value.fullNumber.replace(/\s+/g, ''))
      }
      copied.value = true
      setTimeout(() => {
        copied.value = false
      }, 2000)
    }
    
    function adjustLimit(cardId: string) {
      cards.value = cards.value.map((c) => {
        if (c.id === cardId) {
          const newLimit = c.limit === 5000 ? 7500 : c.limit === 10000 ? 12000 : c.limit + 1000
          return { ...c, limit: newLimit }
        }
        return c
      })
      showFeedback(`Limit updated to $${selectedCard.value.limit.toLocaleString()}`)
    }
    
    function terminateCard(cardId: string) {
      if (cards.value.length <= 1) {
        showFeedback('Cannot terminate the last remaining card')
        return
      }
      const remaining = cards.value.filter((c) => c.id !== cardId)
      cards.value = remaining
      if (selectedId.value === cardId) {
        selectedId.value = remaining[0].id
      }
      showFeedback('Card terminated permanently')
    }
    
    function issueNewCard() {
      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',
      }
      cards.value = [newCard, ...cards.value]
      selectedId.value = newId
      showFeedback('New virtual card issued successfully')
    }
    
    function showFeedback(msg: string) {
      actionFeedback.value = msg
      setTimeout(() => {
        if (actionFeedback.value === msg) {
          actionFeedback.value = null
        }
      }, 3000)
    }
    </script>
    
    <template>
      <div data-slot="virtual-card-manager" :class="cn('mx-auto w-full max-w-6xl space-y-6', props.class)">
        <!-- 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>
              <Badge wrap variant="secondary" class="font-mono text-xs tabular-nums">{{ cards.length }} Cards</Badge>
            </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 size="sm" class="gap-1.5 shadow-xs" @click="issueNewCard">
              <Plus class="size-4" />
              Issue New Card
            </Button>
          </div>
        </div>
    
        <!-- Notification / Action Toast feedback -->
        <div
          v-if="actionFeedback"
          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">
            <CheckCircle2 class="size-4 shrink-0" />
            <span>{{ actionFeedback }}</span>
          </div>
          <Button
            aria-label="Dismiss notification"
            variant="ghost"
            size="xs"
            class="h-6 px-2 text-xs"
            @click="actionFeedback = null"
            >Dismiss</Button
          >
        </div>
    
        <!-- Virtual Card Visual Hero -->
        <Card class="border-border bg-card shadow-xs">
          <CardHeader class="border-b pb-4">
            <div class="flex flex-wrap items-center justify-between gap-2">
              <div>
                <div class="flex items-center gap-2">
                  <CardTitle class="text-base font-semibold">{{ selectedCard.name }}</CardTitle>
                  <Badge
                    :variant="selectedCard.status === 'Active' ? 'success' : 'warning'"
                    class="text-xs whitespace-normal"
                  >
                    {{ selectedCard.status }}
                  </Badge>
                </div>
                <CardDescription class="mt-0.5 text-xs">
                  {{ selectedCard.purpose }} · {{ selectedCard.billingCycle }}
                </CardDescription>
              </div>
              <Badge wrap variant="outline" class="font-mono text-xs"> Ending in {{ selectedCard.last4 }} </Badge>
            </div>
          </CardHeader>
    
          <CardContent 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="
                    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',
                      isSelectedFrozen && 'contrast-95 grayscale filter',
                    )
                  "
                >
                  <!-- 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
                    class="bg-success/10 pointer-events-none absolute -bottom-16 -left-16 size-48 rounded-full blur-2xl"
                    aria-hidden="true"
                  />
    
                  <!-- Frozen watermark overlay -->
                  <div
                    v-if="isSelectedFrozen"
                    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"
                    >
                      <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 class="border-warning/30 border-r border-b" />
                          <div class="border-warning/30 border-b" />
                          <div class="border-warning/30 border-r" />
                          <div class="border-warning/30 border-r" />
                          <div />
                        </div>
                      </div>
    
                      <!-- NFC Wave icon -->
                      <div class="text-muted-foreground/80 rotate-90" aria-hidden="true">
                        <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
                      >
                      <div v-if="selectedCard.brand === 'mastercard'" class="flex -space-x-2" aria-label="Mastercard">
                        <div class="bg-destructive/90 size-6 rounded-full" />
                        <div class="bg-warning/90 size-6 rounded-full mix-blend-screen" />
                      </div>
                      <div v-else 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
                          variant="ghost"
                          size="icon-sm"
                          class="size-7 text-zinc-300 hover:bg-white/10 hover:text-white"
                          :aria-label="showDetails ? 'Hide card number' : 'Reveal card number'"
                          @click="showDetails = !showDetails"
                        >
                          <EyeOff v-if="showDetails" class="size-3.5" />
                          <Eye v-else class="size-3.5" />
                        </Button>
                        <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"
                        >
                          <Check v-if="copied" class="text-success size-3.5" />
                          <Copy v-else 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>
    
                  <Progress :model-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">
                      ${{ Math.max(0, selectedCard.limit - selectedCard.spent).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>
                    <Switch
                      :model-value="isSelectedFrozen"
                      size="sm"
                      aria-label="Toggle card freeze status"
                      @update:model-value="toggleFreeze(selectedCard.id)"
                    />
                  </div>
    
                  <!-- Change Limit Button -->
                  <Button
                    variant="outline"
                    size="sm"
                    class="h-auto flex-col items-start gap-1 p-3 text-left"
                    @click="adjustLimit(selectedCard.id)"
                  >
                    <div class="flex w-full items-center justify-between">
                      <span class="text-foreground text-xs font-medium">Adjust Limit</span>
                      <SlidersHorizontal class="text-muted-foreground size-3.5" />
                    </div>
                    <span class="text-muted-foreground text-xs font-normal">Edit monthly cap</span>
                  </Button>
    
                  <!-- Terminate Card Button -->
                  <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)"
                  >
                    <div class="flex w-full items-center justify-between">
                      <span class="text-xs font-medium">Terminate</span>
                      <Trash2 class="size-3.5" />
                    </div>
                    <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">
                      <Avatar size="xs" class="size-4.5">
                        <AvatarImage :src="selectedCard.avatar" :alt="selectedCard.holder" />
                        <AvatarFallback class="text-xs">{{ selectedCard.initials }}</AvatarFallback>
                      </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">
                      <ShieldCheck class="size-3.5" />
                      <span>3D Secure Active</span>
                    </div>
                  </div>
                </div>
              </div>
            </div>
          </CardContent>
        </Card>
    
        <!-- Active Cards Grid / Table -->
        <Card class="border-border bg-card shadow-xs">
          <CardHeader class="border-b pb-4">
            <div class="flex items-center justify-between">
              <div>
                <CardTitle class="text-base font-semibold">Active Cards List</CardTitle>
                <CardDescription class="mt-0.5 text-xs">
                  All company virtual cards currently assigned to team members and services.
                </CardDescription>
              </div>
              <Badge wrap variant="outline" class="font-mono text-xs tabular-nums"> {{ cards.length }} Issued </Badge>
            </div>
          </CardHeader>
    
          <CardContent class="p-0">
            <div class="divide-border divide-y">
              <div
                v-for="card in cards"
                :key="card.id"
                :class="
                  cn(
                    'group flex flex-col gap-4 p-4 transition-colors sm:flex-row sm:items-center sm:justify-between',
                    selectedId === card.id ? 'bg-muted/40' : 'hover:bg-muted/20',
                  )
                "
              >
                <!-- Card Identity & Number -->
                <div class="flex min-w-0 items-center gap-3.5 sm:w-1/3">
                  <div
                    :class="
                      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',
                      )
                    "
                  >
                    <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">
                  <Avatar size="sm">
                    <AvatarImage :src="card.avatar" :alt="card.holder" />
                    <AvatarFallback>{{ card.initials }}</AvatarFallback>
                  </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">
                      {{ Math.round((card.spent / card.limit) * 100) }}%
                    </span>
                  </div>
                  <Progress :model-value="Math.min(100, Math.round((card.spent / card.limit) * 100))" class="h-1.5" />
                </div>
    
                <!-- Status Badge & Actions -->
                <div class="flex items-center justify-between gap-3 sm:justify-end">
                  <Badge :variant="card.status === 'Active' ? 'success' : 'warning'" class="text-xs whitespace-normal">
                    {{ card.status }}
                  </Badge>
    
                  <div class="flex items-center gap-1.5">
                    <Button
                      :variant="selectedId === card.id ? 'secondary' : 'ghost'"
                      size="sm"
                      class="text-xs"
                      @click="selectedId = card.id"
                    >
                      {{ selectedId === card.id ? 'Viewing' : 'View Card' }}
                    </Button>
    
                    <Button
                      variant="ghost"
                      size="icon-sm"
                      :aria-label="card.status === 'Active' ? 'Freeze card' : 'Unfreeze card'"
                      @click="toggleFreeze(card.id)"
                    >
                      <Snowflake v-if="card.status === 'Active'" class="text-muted-foreground size-4" />
                      <Zap v-else class="text-warning size-4" />
                    </Button>
                  </div>
                </div>
              </div>
            </div>
          </CardContent>
        </Card>
      </div>
    </template>
    

Raw manifest:https://uipkge.dev/r/vue/virtual-card-manager.json