
3D Extruded Buildings & Urban Footprints
Real-world 3D building extrusions with dynamic sunlight shadows, terrain DEM elevations, and pitch/bearing camera controls.
Carta-style company equity capitalization table, share class breakdown, fully diluted ownership percentages, shareholder vesting progress, and interactive round dilution modeling simulator.
Also available for React ->$pnpm dlx shadcn-vue@latest add https://uipkge.dev/r/vue/cap-table-ownership-summary.json$npx shadcn-vue@latest add https://uipkge.dev/r/vue/cap-table-ownership-summary.json$yarn dlx shadcn-vue@latest add https://uipkge.dev/r/vue/cap-table-ownership-summary.json$bunx shadcn-vue@latest add https://uipkge.dev/r/vue/cap-table-ownership-summary.jsonnpx shadcn-vue@latest add @uipkge/cap-table-ownership-summaryInstalls to:app/components/blocks/| Name | Type / Values | Default | Required |
|---|---|---|---|
initialInvestment | number | 10000000 | optional |
initialPreMoney | number | 50000000 | optional |
class | HTMLAttributes['class'] | — | optional |
Type aliases exported from this item's source. Use these to shape the data you pass in.
Shareholderinterface Shareholder {
id: string
name: string
role: string
avatar: string
shareClass: 'Common Stock' | 'Series Seed Preferred' | 'ISO Option Pool'
shares: number
ownershipPct: number
equityValue: number
vestingDetail: string
vestedMonths: number
totalMonths: number
isVestedComplete: boolean
grantDate: string
liquidationPref?: string
}<script setup lang="ts">
import { ref, computed } from 'vue'
import type { HTMLAttributes } from 'vue'
import {
Award,
Building2,
Calculator,
Check,
FileSpreadsheet,
Info,
Layers,
Plus,
RefreshCw,
Search,
ShieldCheck,
TrendingUp,
Users,
X,
Coins,
} from 'lucide-vue-next'
import { cn } from '@/lib/utils'
import { Avatar, AvatarFallback } 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 { Input } from '@/components/ui/input'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
interface Props {
initialInvestment?: number
initialPreMoney?: number
class?: HTMLAttributes['class']
}
const props = withDefaults(defineProps<Props>(), {
initialInvestment: 10000000,
initialPreMoney: 50000000,
})
// Current Cap Table Static Baseline
const currentSharePrice = 4.5
const currentTotalShares = 10000000
const currentValuation = 45000000
interface Shareholder {
id: string
name: string
role: string
avatar: string
shareClass: 'Common Stock' | 'Series Seed Preferred' | 'ISO Option Pool'
shares: number
ownershipPct: number
equityValue: number
vestingDetail: string
vestedMonths: number
totalMonths: number
isVestedComplete: boolean
grantDate: string
liquidationPref?: string
}
const shareholders = ref<Shareholder[]>([
{
id: 'sh-1',
name: 'Elena Rostova',
role: 'Co-Founder & CEO',
avatar: 'ER',
shareClass: 'Common Stock',
shares: 3500000,
ownershipPct: 35.0,
equityValue: 15750000,
vestingDetail: '48-Month Vesting · 24/48 Months Vested',
vestedMonths: 24,
totalMonths: 48,
isVestedComplete: false,
grantDate: 'Feb 15, 2024 · 1-yr Cliff (Satisfied)',
},
{
id: 'sh-2',
name: 'Marcus Vance',
role: 'Co-Founder & CTO',
avatar: 'MV',
shareClass: 'Common Stock',
shares: 2500000,
ownershipPct: 25.0,
equityValue: 11250000,
vestingDetail: '48-Month Vesting · 24/48 Months Vested',
vestedMonths: 24,
totalMonths: 48,
isVestedComplete: false,
grantDate: 'Feb 15, 2024 · 1-yr Cliff (Satisfied)',
},
{
id: 'sh-3',
name: 'Founders Fund / Seed Syndicate',
role: 'Lead Seed Investor',
avatar: 'FF',
shareClass: 'Series Seed Preferred',
shares: 1500000,
ownershipPct: 15.0,
equityValue: 6750000,
vestingDetail: 'Fully Vested (100%) · 1.0x Non-Participating',
vestedMonths: 48,
totalMonths: 48,
isVestedComplete: true,
grantDate: 'Aug 10, 2024 · Board Seat (1)',
liquidationPref: '1.0x Pref ($6.75M)',
},
{
id: 'sh-4',
name: 'Sequoia Scout Seed SPV',
role: 'Seed Co-Investor',
avatar: 'SQ',
shareClass: 'Series Seed Preferred',
shares: 1000000,
ownershipPct: 10.0,
equityValue: 4500000,
vestingDetail: 'Fully Vested (100%) · 1.0x Non-Participating',
vestedMonths: 48,
totalMonths: 48,
isVestedComplete: true,
grantDate: 'Aug 10, 2024 · Pro-Rata Rights',
liquidationPref: '1.0x Pref ($4.50M)',
},
{
id: 'sh-5',
name: 'Unallocated Employee Option Pool',
role: '2024 Equity Incentive Plan (EIP)',
avatar: 'EP',
shareClass: 'ISO Option Pool',
shares: 1500000,
ownershipPct: 15.0,
equityValue: 6750000,
vestingDetail: 'Authorized Pool · Available for Future Key Hires',
vestedMonths: 0,
totalMonths: 48,
isVestedComplete: false,
grantDate: 'Board Authorized · Dec 01, 2024',
},
])
// Filter and Search State
const searchQuery = ref('')
const selectedClassFilter = ref<'All' | 'Common Stock' | 'Series Seed Preferred' | 'ISO Option Pool'>('All')
const filteredShareholders = computed(() => {
return shareholders.value.filter((sh) => {
if (selectedClassFilter.value !== 'All' && sh.shareClass !== selectedClassFilter.value) {
return false
}
if (searchQuery.value.trim()) {
const q = searchQuery.value.toLowerCase()
const matchName = sh.name.toLowerCase().includes(q)
const matchRole = sh.role.toLowerCase().includes(q)
const matchClass = sh.shareClass.toLowerCase().includes(q)
return matchName || matchRole || matchClass
}
return true
})
})
// Export feedback state
const isExporting = ref(false)
const exportSuccess = ref(false)
function handleExportCapTable() {
isExporting.value = true
setTimeout(() => {
isExporting.value = false
exportSuccess.value = true
setTimeout(() => {
exportSuccess.value = false
}, 2500)
}, 1000)
}
// Issue Grant Modal State
const isGrantDialogOpen = ref(false)
const grantSuccessMessage = ref(false)
const grantName = ref('')
const grantRole = ref('')
const grantShares = ref(50000)
const grantClass = ref<'ISO Options' | 'NSO Options' | 'Restricted Stock'>('ISO Options')
const grantVesting = ref('4-Year Vesting · 1-Year Cliff (25%), then Monthly')
function handleCreateGrant() {
if (!grantName.value.trim() || grantShares.value <= 0) return
grantSuccessMessage.value = true
setTimeout(() => {
grantSuccessMessage.value = false
isGrantDialogOpen.value = false
grantName.value = ''
grantRole.value = ''
grantShares.value = 50000
}, 1500)
}
// Round Modeling Simulator State
const simPreMoney = ref(props.initialPreMoney)
const simInvestment = ref(props.initialInvestment)
function setPresetScenario(preMoney: number, investment: number) {
simPreMoney.value = preMoney
simInvestment.value = investment
}
// Dynamic Round Modeling Calculations
const simPostMoney = computed(() => simPreMoney.value + simInvestment.value)
const simNewSharePrice = computed(() => {
if (currentTotalShares <= 0) return 0
return simPreMoney.value / currentTotalShares
})
const simNewSharesIssued = computed(() => {
if (simNewSharePrice.value <= 0) return 0
return Math.round(simInvestment.value / simNewSharePrice.value)
})
const simTotalPostShares = computed(() => currentTotalShares + simNewSharesIssued.value)
const simFounderShares = 6000000
const simSeedShares = 2500000
const simOptionShares = 1500000
const simFounderPct = computed(() => (simFounderShares / simTotalPostShares.value) * 100)
const simSeedPct = computed(() => (simSeedShares / simTotalPostShares.value) * 100)
const simOptionPct = computed(() => (simOptionShares / simTotalPostShares.value) * 100)
const simNewInvestorPct = computed(() => (simNewSharesIssued.value / simTotalPostShares.value) * 100)
const simFounderValue = computed(() => (simFounderPct.value / 100) * simPostMoney.value)
const simSeedValue = computed(() => (simSeedPct.value / 100) * simPostMoney.value)
const simOptionValue = computed(() => (simOptionPct.value / 100) * simPostMoney.value)
const simNewInvestorValue = computed(() => (simNewInvestorPct.value / 100) * simPostMoney.value)
function formatCurrency(val: number): string {
return val.toLocaleString('en-US', {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})
}
function formatNumber(val: number): string {
return val.toLocaleString('en-US')
}
function formatPercent(val: number, decimals = 1): string {
return `${val.toFixed(decimals)}%`
}
</script>
<template>
<div data-slot="cap-table-ownership-summary" :class="cn('mx-auto w-full max-w-6xl space-y-6', props.class)">
<!-- Top Header -->
<header class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div class="space-y-1.5">
<div class="flex flex-wrap items-center gap-2.5">
<div
class="bg-primary/10 text-primary border-primary/20 flex size-9 shrink-0 items-center justify-center rounded-lg border shadow-xs"
aria-hidden="true"
>
<Building2 class="size-5" />
</div>
<div>
<h1 class="text-foreground text-2xl font-bold tracking-tight">
Capitalization Table & Equity Ownership
</h1>
</div>
<Badge variant="outline" class="border-success/30 bg-success/10 text-success gap-1.5 font-normal">
<span class="bg-success size-1.5 animate-pulse rounded-full" aria-hidden="true" />
Audited · Series Seed
</Badge>
</div>
<div class="text-muted-foreground flex flex-wrap items-center gap-2 text-xs">
<span class="bg-muted/70 text-foreground border-border/80 rounded border px-2 py-0.5 font-medium">
UIPKGE Technologies Inc. · Delaware C-Corp
</span>
<span>EIN: 93-8472910</span>
<span class="inline-flex items-center gap-1 font-mono">
<ShieldCheck class="text-primary size-3.5" aria-hidden="true" />
409A FMV: ${{ currentSharePrice.toFixed(2) }}/share
</span>
</div>
</div>
<!-- Action Buttons -->
<div class="flex flex-wrap items-center gap-2">
<Button
variant="outline"
size="sm"
class="gap-1.5 shadow-xs"
:disabled="isExporting"
@click="handleExportCapTable"
>
<RefreshCw v-if="isExporting" class="size-4 animate-spin" aria-hidden="true" />
<Check v-else-if="exportSuccess" class="text-success size-4" aria-hidden="true" />
<FileSpreadsheet v-else class="size-4" aria-hidden="true" />
<span>{{
exportSuccess ? 'Cap Table Exported!' : isExporting ? 'Exporting...' : 'Export Cap Table (Excel)'
}}</span>
</Button>
<Button size="sm" class="gap-1.5 shadow-xs" @click="isGrantDialogOpen = true">
<Plus class="size-4" aria-hidden="true" />
Issue Equity Grant
</Button>
</div>
</header>
<!-- Issue Equity Grant Dialog Modal Overlay -->
<div
v-if="isGrantDialogOpen"
class="bg-background/80 fixed inset-0 z-50 flex items-center justify-center p-4 backdrop-blur-xs"
role="dialog"
aria-modal="true"
aria-labelledby="grant-dialog-title"
>
<Card class="border-border w-full max-w-lg shadow-lg">
<CardHeader class="pb-3">
<div class="flex flex-wrap items-center justify-between gap-x-2">
<CardTitle id="grant-dialog-title" class="flex items-center gap-2 text-base font-semibold">
<Award class="text-primary size-5" />
Issue New Equity Option Grant
</CardTitle>
<Button
variant="ghost"
size="icon-sm"
class="text-muted-foreground hover:text-foreground size-7"
aria-label="Close dialog"
@click="isGrantDialogOpen = false"
>
<X class="size-4" />
</Button>
</div>
<CardDescription class="text-xs">
Authorize and draft an option grant under the 2024 Equity Incentive Plan (EIP).
</CardDescription>
</CardHeader>
<CardContent class="space-y-4">
<div v-if="grantSuccessMessage" class="border-success/20 bg-success/10 rounded-lg border p-4 text-center">
<Check class="text-success mx-auto size-6" />
<p class="text-foreground mt-2 text-sm font-semibold">Option Grant Issued & Drafted!</p>
<p class="text-muted-foreground mt-0.5 text-xs">
Sent to Board of Directors for electronic consent signature.
</p>
</div>
<form v-else class="space-y-3.5" @submit.prevent="handleCreateGrant">
<div class="space-y-1.5">
<label for="grantee-name" class="text-foreground text-xs font-medium">Grantee Legal Name</label>
<Input
id="grantee-name"
v-model="grantName"
placeholder="e.g. Dr. Sarah Chen"
required
class="h-9 text-xs"
/>
</div>
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
<div class="space-y-1.5">
<label for="grantee-role" class="text-foreground text-xs font-medium">Role / Title</label>
<Input
id="grantee-role"
v-model="grantRole"
placeholder="e.g. Principal AI Scientist"
required
class="h-9 text-xs"
/>
</div>
<div class="space-y-1.5">
<label for="grant-quantity" class="text-foreground text-xs font-medium"
>Number of Options / Shares</label
>
<Input
id="grant-quantity"
v-model.number="grantShares"
type="number"
min="1000"
step="1000"
required
class="h-9 font-mono text-xs tabular-nums"
/>
</div>
</div>
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
<div class="space-y-1.5">
<label class="text-foreground text-xs font-medium">Equity Class</label>
<div
class="border-border bg-muted/30 flex h-9 items-center justify-between rounded-md border px-3 text-xs"
>
<span class="font-medium">{{ grantClass }}</span>
<Badge variant="outline" class="text-xs">EIP-2024</Badge>
</div>
</div>
<div class="space-y-1.5">
<label class="text-foreground text-xs font-medium">Exercise Price (409A FMV)</label>
<div
class="border-border bg-muted/30 flex h-9 items-center justify-between rounded-md border px-3 font-mono text-xs tabular-nums"
>
<span>${{ currentSharePrice.toFixed(2) }} / share</span>
<span class="text-muted-foreground text-xs">Fair Market</span>
</div>
</div>
</div>
<div class="space-y-1.5">
<label class="text-foreground text-xs font-medium">Standard Vesting Schedule</label>
<Input v-model="grantVesting" readonly class="bg-muted/40 h-9 text-xs" />
<p class="text-muted-foreground text-xs">
25% vests at 12-month cliff; remaining 75% vests monthly in 36 equal installments.
</p>
</div>
<div class="flex items-center justify-end gap-2 pt-3">
<Button type="button" variant="outline" size="sm" @click="isGrantDialogOpen = false"> Cancel </Button>
<Button type="submit" size="sm" class="gap-1.5">
<Check class="size-4" />
Draft & Issue Grant
</Button>
</div>
</form>
</CardContent>
</Card>
</div>
<!-- 4 Capital Structure Overview Cards -->
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
<!-- Total Fully Diluted Shares Card -->
<Card class="border-border relative overflow-hidden shadow-xs">
<CardHeader class="flex flex-row items-center justify-between pb-2">
<CardTitle class="text-muted-foreground text-xs font-medium">Total Fully Diluted Shares</CardTitle>
<div class="bg-primary/10 text-primary rounded-md p-1.5">
<Layers class="size-4" aria-hidden="true" />
</div>
</CardHeader>
<CardContent class="space-y-1">
<div class="text-foreground text-2xl font-bold tracking-tight tabular-nums">10,000,000</div>
<div class="text-muted-foreground flex flex-wrap items-center justify-between gap-x-2 text-xs">
<span>Authorized: 15,000,000</span>
<span class="text-success font-mono">66.7% Issued</span>
</div>
</CardContent>
</Card>
<!-- Founder Common Stock Card -->
<Card class="border-border relative overflow-hidden shadow-xs">
<CardHeader class="flex flex-row items-center justify-between pb-2">
<CardTitle class="text-muted-foreground text-xs font-medium">Founder Common Stock</CardTitle>
<div class="bg-success/10 text-success rounded-md p-1.5">
<Users class="size-4" aria-hidden="true" />
</div>
</CardHeader>
<CardContent class="space-y-1">
<div class="flex items-baseline gap-2">
<span class="text-foreground text-2xl font-bold tracking-tight tabular-nums">6,000,000</span>
<span class="text-success font-mono text-xs font-semibold">60.0%</span>
</div>
<div class="text-muted-foreground flex flex-wrap items-center justify-between gap-x-2 text-xs">
<span>2 Co-Founders</span>
<span class="font-mono tabular-nums">$27,000,000.00</span>
</div>
</CardContent>
</Card>
<!-- Series Seed Preferred Card -->
<Card class="border-border relative overflow-hidden shadow-xs">
<CardHeader class="flex flex-row items-center justify-between pb-2">
<CardTitle class="text-muted-foreground text-xs font-medium">Series Seed Preferred</CardTitle>
<div class="bg-chart-2/10 text-chart-2 rounded-md p-1.5">
<TrendingUp class="size-4" aria-hidden="true" />
</div>
</CardHeader>
<CardContent class="space-y-1">
<div class="flex items-baseline gap-2">
<span class="text-foreground text-2xl font-bold tracking-tight tabular-nums">2,500,000</span>
<span class="text-chart-2 font-mono text-xs font-semibold">25.0%</span>
</div>
<div class="text-muted-foreground flex flex-wrap items-center justify-between gap-x-2 text-xs">
<span>2 Seed Investors · 1.0x Pref</span>
<span class="font-mono tabular-nums">$11,250,000.00</span>
</div>
</CardContent>
</Card>
<!-- Unallocated Option Pool Card -->
<Card class="border-border relative overflow-hidden shadow-xs">
<CardHeader class="flex flex-row items-center justify-between pb-2">
<CardTitle class="text-muted-foreground text-xs font-medium">Unallocated Option Pool</CardTitle>
<div class="bg-info/10 text-info rounded-md p-1.5">
<Coins class="size-4" aria-hidden="true" />
</div>
</CardHeader>
<CardContent class="space-y-1">
<div class="flex items-baseline gap-2">
<span class="text-foreground text-2xl font-bold tracking-tight tabular-nums">1,500,000</span>
<span class="text-info font-mono text-xs font-semibold">15.0%</span>
</div>
<div class="text-muted-foreground flex flex-wrap items-center justify-between gap-x-2 text-xs">
<span>2024 EIP Pool</span>
<span class="font-mono tabular-nums">$6,750,000.00</span>
</div>
</CardContent>
</Card>
</div>
<!-- Company Valuation Hero & Visual Stacked Ownership Bar Card -->
<Card class="border-border overflow-hidden shadow-xs">
<CardHeader class="pb-3">
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<div>
<CardDescription class="text-muted-foreground text-xs font-semibold tracking-wider uppercase">
Current Post-Money Valuation
</CardDescription>
<div class="mt-1 flex flex-wrap items-baseline gap-3">
<span class="text-foreground text-3xl font-bold tracking-tight tabular-nums sm:text-4xl">
$45,000,000.00
</span>
<div
class="border-success/20 bg-success/10 text-success text-success inline-flex items-center gap-1 rounded-full border px-2.5 py-0.5 text-xs font-semibold tabular-nums"
>
<TrendingUp class="size-3.5" aria-hidden="true" />
Seed Round Closed
<span class="text-muted-foreground ml-0.5 font-normal">($4.50/share)</span>
</div>
</div>
</div>
<div class="text-muted-foreground flex items-center gap-3 text-xs">
<div class="flex items-center gap-1.5">
<span class="bg-success size-2 rounded-full" aria-hidden="true" />
<span>Cap Table Reconciled</span>
</div>
<span class="font-mono">10,000,000 Total Units</span>
</div>
</div>
</CardHeader>
<CardContent class="space-y-4 pt-1">
<!-- Multi-segment Stacked Ownership Bar -->
<div class="space-y-2">
<div class="text-muted-foreground flex flex-wrap items-center justify-between gap-x-2 text-xs">
<span class="text-foreground font-medium">Fully Diluted Equity Distribution</span>
<span class="tabular-nums">100.0% Allocation</span>
</div>
<div class="bg-muted/60 border-border/50 flex h-4 w-full gap-0.5 overflow-hidden rounded-full border p-0.5">
<!-- Founders 60% Emerald -->
<div
style="width: 60%"
class="bg-success h-full rounded-l-full transition-opacity duration-300 hover:opacity-90"
title="Founders (Common Stock): 60.0% · 6,000,000 Shares"
/>
<!-- Seed Investors 25% -->
<div
style="width: 25%"
class="bg-chart-2 h-full transition-opacity duration-300 hover:opacity-90"
title="Seed Investors (Preferred): 25.0% · 2,500,000 Shares"
/>
<!-- Option Pool 15% Blue -->
<div
style="width: 15%"
class="bg-info h-full rounded-r-full transition-opacity duration-300 hover:opacity-90"
title="Unallocated Option Pool: 15.0% · 1,500,000 Shares"
/>
</div>
<!-- Stacked Bar Legend -->
<div class="grid grid-cols-1 gap-3 pt-2 sm:grid-cols-3">
<div
class="border-border/60 bg-muted/20 flex flex-wrap items-center justify-between gap-x-2 rounded-md border p-2.5 text-xs"
>
<div class="flex items-center gap-2">
<span class="bg-success size-2.5 shrink-0 rounded-full shadow-xs" aria-hidden="true" />
<div>
<div class="text-foreground font-medium">Founders (Common)</div>
<div class="text-muted-foreground font-mono tabular-nums">6,000,000 shares</div>
</div>
</div>
<div class="text-right">
<div class="text-foreground font-bold tabular-nums">60.0%</div>
<div class="text-muted-foreground font-mono tabular-nums">$27,000,000.00</div>
</div>
</div>
<div
class="border-border/60 bg-muted/20 flex flex-wrap items-center justify-between gap-x-2 rounded-md border p-2.5 text-xs"
>
<div class="flex items-center gap-2">
<span class="bg-chart-2 size-2.5 shrink-0 rounded-full shadow-xs" aria-hidden="true" />
<div>
<div class="text-foreground font-medium">Seed Investors (Preferred)</div>
<div class="text-muted-foreground font-mono tabular-nums">2,500,000 shares</div>
</div>
</div>
<div class="text-right">
<div class="text-foreground font-bold tabular-nums">25.0%</div>
<div class="text-muted-foreground font-mono tabular-nums">$11,250,000.00</div>
</div>
</div>
<div
class="border-border/60 bg-muted/20 flex flex-wrap items-center justify-between gap-x-2 rounded-md border p-2.5 text-xs"
>
<div class="flex items-center gap-2">
<span class="bg-info size-2.5 shrink-0 rounded-full shadow-xs" aria-hidden="true" />
<div>
<div class="text-foreground font-medium">Employee Option Pool</div>
<div class="text-muted-foreground font-mono tabular-nums">1,500,000 shares</div>
</div>
</div>
<div class="text-right">
<div class="text-foreground font-bold tabular-nums">15.0%</div>
<div class="text-muted-foreground font-mono tabular-nums">$6,750,000.00</div>
</div>
</div>
</div>
</div>
</CardContent>
</Card>
<!-- Shareholder Breakdown Table Card -->
<Card class="border-border shadow-xs">
<CardHeader class="border-border/60 border-b pb-4">
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<CardTitle class="text-foreground text-base font-semibold">Shareholder & Equity Register</CardTitle>
<CardDescription class="text-muted-foreground mt-0.5 text-xs">
Detailed registry of individual grants, share classes, fully diluted ownership, and vesting timelines.
</CardDescription>
</div>
<div class="flex flex-col gap-2 sm:flex-row sm:items-center">
<!-- Search -->
<div class="relative w-full sm:w-56">
<Search class="text-muted-foreground absolute top-1/2 left-2.5 size-3.5 -translate-y-1/2" />
<Input v-model="searchQuery" placeholder="Search shareholder or role..." class="h-8 pl-8 text-xs" />
</div>
<!-- Share Class Filter Buttons -->
<div class="border-border bg-muted/40 flex items-center rounded-lg border p-0.5">
<button
type="button"
:class="
cn(
'rounded-md px-2.5 py-1 text-xs font-medium transition-colors',
selectedClassFilter === 'All'
? 'bg-background text-foreground shadow-xs'
: 'text-muted-foreground hover:text-foreground',
)
"
@click="selectedClassFilter = 'All'"
>
All (5)
</button>
<button
type="button"
:class="
cn(
'rounded-md px-2.5 py-1 text-xs font-medium transition-colors',
selectedClassFilter === 'Common Stock'
? 'bg-background text-foreground shadow-xs'
: 'text-muted-foreground hover:text-foreground',
)
"
@click="selectedClassFilter = 'Common Stock'"
>
Common (2)
</button>
<button
type="button"
:class="
cn(
'rounded-md px-2.5 py-1 text-xs font-medium transition-colors',
selectedClassFilter === 'Series Seed Preferred'
? 'bg-background text-foreground shadow-xs'
: 'text-muted-foreground hover:text-foreground',
)
"
@click="selectedClassFilter = 'Series Seed Preferred'"
>
Preferred (2)
</button>
<button
type="button"
:class="
cn(
'rounded-md px-2.5 py-1 text-xs font-medium transition-colors',
selectedClassFilter === 'ISO Option Pool'
? 'bg-background text-foreground shadow-xs'
: 'text-muted-foreground hover:text-foreground',
)
"
@click="selectedClassFilter = 'ISO Option Pool'"
>
Pool (1)
</button>
</div>
</div>
</div>
</CardHeader>
<CardContent class="p-0">
<div class="overflow-x-auto">
<Table>
<TableHeader>
<TableRow class="hover:bg-transparent">
<TableHead class="w-[260px] pl-6 text-xs font-medium">Shareholder / Entity</TableHead>
<TableHead class="w-[180px] text-xs font-medium">Share Class</TableHead>
<TableHead class="text-right text-xs font-medium">Shares Owned</TableHead>
<TableHead class="text-right text-xs font-medium">Fully Diluted %</TableHead>
<TableHead class="text-right text-xs font-medium">Total Equity Value</TableHead>
<TableHead class="min-w-[240px] pr-6 text-xs font-medium">Vesting & Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-for="sh in filteredShareholders" :key="sh.id" class="hover:bg-muted/30 transition-colors">
<!-- Shareholder Name & Role -->
<TableCell class="py-3.5 pl-6">
<div class="flex items-center gap-3">
<Avatar class="border-border/80 size-9 border shadow-xs">
<AvatarFallback
:class="
cn(
'text-xs font-bold',
sh.shareClass === 'Common Stock'
? 'bg-success/10 text-success'
: sh.shareClass === 'Series Seed Preferred'
? 'bg-chart-2/10 text-chart-2'
: 'bg-info/10 text-info',
)
"
>
{{ sh.avatar }}
</AvatarFallback>
</Avatar>
<div class="min-w-0">
<div class="text-foreground truncate text-xs font-semibold">{{ sh.name }}</div>
<div class="text-muted-foreground truncate text-xs">{{ sh.role }}</div>
<div class="text-muted-foreground/70 font-mono text-xs">{{ sh.grantDate }}</div>
</div>
</div>
</TableCell>
<!-- Share Class Badge -->
<TableCell class="py-3.5">
<Badge
v-if="sh.shareClass === 'Common Stock'"
variant="outline"
class="border-success/30 bg-success/10 text-success text-xs font-medium"
>
Common Stock
</Badge>
<Badge
v-else-if="sh.shareClass === 'Series Seed Preferred'"
variant="outline"
class="border-chart-2/30 bg-chart-2/10 text-chart-2 text-xs font-medium"
>
Series Seed Preferred
</Badge>
<Badge v-else variant="outline" class="border-info/30 bg-info/10 text-info text-xs font-medium">
ISO Option Pool
</Badge>
</TableCell>
<!-- Shares Owned -->
<TableCell class="py-3.5 text-right">
<div class="text-foreground font-mono text-sm font-bold tabular-nums">
{{ formatNumber(sh.shares) }}
</div>
<div class="text-muted-foreground text-xs">Shares</div>
</TableCell>
<!-- Fully Diluted Ownership Percentage -->
<TableCell class="py-3.5 text-right">
<div class="text-foreground font-mono text-sm font-semibold tabular-nums">
{{ formatPercent(sh.ownershipPct) }}
</div>
<div class="text-muted-foreground text-xs">of 10,000,000</div>
</TableCell>
<!-- Total Equity Value -->
<TableCell class="py-3.5 text-right">
<div class="text-foreground font-mono text-sm font-semibold tabular-nums">
{{ formatCurrency(sh.equityValue) }}
</div>
<div class="text-muted-foreground text-xs">@ $4.50/share</div>
</TableCell>
<!-- Vesting Status Progress -->
<TableCell class="py-3.5 pr-6">
<div class="space-y-1.5">
<div class="flex flex-wrap items-center justify-between gap-x-2 text-xs">
<span class="text-foreground font-medium">
{{
sh.isVestedComplete
? '100% Vested'
: sh.vestedMonths > 0
? `${Math.round((sh.vestedMonths / sh.totalMonths) * 100)}% Vested`
: 'Unallocated'
}}
</span>
<span class="text-muted-foreground font-mono tabular-nums">
{{ sh.vestedMonths }}/{{ sh.totalMonths }} mo
</span>
</div>
<div class="bg-muted/80 relative h-2 w-full overflow-hidden rounded-full">
<div
:class="
cn(
'h-full transition-[width,background-color] duration-300',
sh.isVestedComplete ? 'bg-primary' : sh.vestedMonths > 0 ? 'bg-success' : 'bg-info/40',
)
"
:style="{ width: `${sh.isVestedComplete ? 100 : (sh.vestedMonths / sh.totalMonths) * 100}%` }"
/>
</div>
<div class="text-muted-foreground text-xs">
{{ sh.vestingDetail }}
</div>
</div>
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
<!-- Table Summary Bar -->
<div
class="border-border/60 bg-muted/20 flex flex-wrap items-center justify-between gap-4 border-t px-6 py-3 text-xs"
>
<div class="text-muted-foreground">
Showing <span class="text-foreground font-semibold">{{ filteredShareholders.length }}</span> of
<span class="text-foreground font-semibold">5</span> cap table stakeholders
</div>
<div class="flex flex-wrap items-center gap-6 font-mono">
<div>
<span class="text-muted-foreground">Total Shares: </span>
<span class="text-foreground font-bold tabular-nums">10,000,000</span>
</div>
<div>
<span class="text-muted-foreground">Total Ownership: </span>
<span class="text-foreground font-bold tabular-nums">100.0%</span>
</div>
<div>
<span class="text-muted-foreground">Total Valuation: </span>
<span class="text-foreground font-bold tabular-nums">$45,000,000.00</span>
</div>
</div>
</div>
</CardContent>
</Card>
<!-- Round Modeling Simulator Card (Series A Dilution Simulator) -->
<Card class="border-border shadow-xs">
<CardHeader class="pb-3">
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<div class="space-y-1">
<div class="flex items-center gap-2">
<div class="bg-primary/10 text-primary rounded-md p-1.5">
<Calculator class="size-4" aria-hidden="true" />
</div>
<CardTitle class="text-foreground text-base font-semibold">
Round Modeling Simulator (Series A Pro-Forma)
</CardTitle>
</div>
<CardDescription class="text-muted-foreground text-xs">
Simulate Series A pre-money valuation and capital raised to preview diluted ownership percentages, new
share price, and stakeholder equity value accretion.
</CardDescription>
</div>
<!-- Preset Scenario Buttons -->
<div class="flex flex-wrap items-center gap-1.5">
<Button
variant="outline"
size="sm"
class="h-7 text-xs font-medium"
@click="setPresetScenario(30000000, 5000000)"
>
$5M @ $30M Pre
</Button>
<Button
variant="outline"
size="sm"
class="h-7 text-xs font-medium"
@click="setPresetScenario(50000000, 10000000)"
>
$10M @ $50M Pre
</Button>
<Button
variant="outline"
size="sm"
class="h-7 text-xs font-medium"
@click="setPresetScenario(60000000, 15000000)"
>
$15M @ $60M Pre
</Button>
</div>
</div>
</CardHeader>
<CardContent class="space-y-6">
<!-- Simulator Inputs -->
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
<!-- Pre-Money Valuation Input -->
<div class="bg-muted/30 border-border/80 space-y-2 rounded-lg border p-4">
<div class="flex flex-wrap items-center justify-between gap-x-2 text-xs">
<label for="sim-pre-money" class="text-foreground font-semibold">Series A Pre-Money Valuation</label>
<span class="text-muted-foreground font-mono tabular-nums"
>${{ (simPreMoney / 1000000).toFixed(1) }}M</span
>
</div>
<div class="flex items-center gap-3">
<Input
id="sim-pre-money"
v-model.number="simPreMoney"
type="number"
step="1000000"
min="10000000"
class="font-mono text-base font-bold tabular-nums"
/>
</div>
<p class="text-muted-foreground text-xs">
Implies a share price of
<strong class="text-foreground font-mono tabular-nums">${{ simNewSharePrice.toFixed(2) }} / share</strong>
(+{{ (((simNewSharePrice - currentSharePrice) / currentSharePrice) * 100).toFixed(1) }}% step-up).
</p>
</div>
<!-- New Investment Raised Input -->
<div class="bg-muted/30 border-border/80 space-y-2 rounded-lg border p-4">
<div class="flex flex-wrap items-center justify-between gap-x-2 text-xs">
<label for="sim-investment" class="text-foreground font-semibold">Series A Capital Raised</label>
<span class="text-muted-foreground font-mono tabular-nums"
>${{ (simInvestment / 1000000).toFixed(1) }}M</span
>
</div>
<div class="flex items-center gap-3">
<Input
id="sim-investment"
v-model.number="simInvestment"
type="number"
step="1000000"
min="1000000"
class="font-mono text-base font-bold tabular-nums"
/>
</div>
<p class="text-muted-foreground text-xs">
Issues
<strong class="text-foreground font-mono tabular-nums"
>{{ formatNumber(simNewSharesIssued) }} new shares</strong
>
to Series A lead syndicate.
</p>
</div>
</div>
<!-- Calculated Pro-Forma Summary Metrics -->
<div class="grid grid-cols-2 gap-3 sm:grid-cols-4">
<div class="border-border/60 bg-muted/20 space-y-1 rounded-md border p-3">
<div class="text-muted-foreground text-xs">Post-Money Valuation</div>
<div class="text-foreground font-mono text-lg font-bold tabular-nums">
{{ formatCurrency(simPostMoney) }}
</div>
<div class="text-muted-foreground text-xs">Pre-Money + Investment</div>
</div>
<div class="border-border/60 bg-muted/20 space-y-1 rounded-md border p-3">
<div class="text-muted-foreground text-xs">Series A Share Price</div>
<div class="text-foreground font-mono text-lg font-bold tabular-nums">
${{ simNewSharePrice.toFixed(2) }}
</div>
<div class="text-success text-xs">
vs ${{ currentSharePrice.toFixed(2) }} Seed (+{{
(((simNewSharePrice - currentSharePrice) / currentSharePrice) * 100).toFixed(1)
}}%)
</div>
</div>
<div class="border-border/60 bg-muted/20 space-y-1 rounded-md border p-3">
<div class="text-muted-foreground text-xs">Total Diluted Shares</div>
<div class="text-foreground font-mono text-lg font-bold tabular-nums">
{{ formatNumber(simTotalPostShares) }}
</div>
<div class="text-muted-foreground text-xs">+{{ formatNumber(simNewSharesIssued) }} New Shares</div>
</div>
<div class="border-border/60 bg-muted/20 space-y-1 rounded-md border p-3">
<div class="text-muted-foreground text-xs">Investor Dilution Rate</div>
<div class="text-foreground text-warning text-warning font-mono text-lg font-bold tabular-nums">
{{ formatPercent(simNewInvestorPct) }}
</div>
<div class="text-muted-foreground text-xs">Effective Round Dilution</div>
</div>
</div>
<!-- Post-Round Dilution & Valuation Comparison Table -->
<div class="space-y-3">
<div class="flex flex-wrap items-center justify-between gap-x-2 text-xs">
<span class="text-foreground font-semibold">Post-Series A Ownership Comparison</span>
<span class="text-muted-foreground">Pro-Forma Stakeholder Impact</span>
</div>
<!-- Visual Comparison Bar -->
<div class="space-y-1.5">
<div class="bg-muted/60 border-border/50 flex h-4 w-full gap-0.5 overflow-hidden rounded-full border p-0.5">
<div
:style="{ width: `${simFounderPct}%` }"
class="bg-success h-full rounded-l-full transition-[width] duration-300"
:title="`Founders: ${simFounderPct.toFixed(1)}%`"
/>
<div
:style="{ width: `${simSeedPct}%` }"
class="bg-chart-2 h-full transition-[width] duration-300"
:title="`Seed Investors: ${simSeedPct.toFixed(1)}%`"
/>
<div
:style="{ width: `${simOptionPct}%` }"
class="bg-info h-full transition-[width] duration-300"
:title="`Option Pool: ${simOptionPct.toFixed(1)}%`"
/>
<div
:style="{ width: `${simNewInvestorPct}%` }"
class="bg-warning h-full rounded-r-full transition-[width] duration-300"
:title="`Series A Investors: ${simNewInvestorPct.toFixed(1)}%`"
/>
</div>
</div>
<!-- Stakeholder Comparative Grid -->
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
<!-- Founders -->
<div class="border-border/80 bg-card space-y-2 rounded-lg border p-3 text-xs shadow-xs">
<div class="flex flex-wrap items-center justify-between gap-x-2">
<div class="flex items-center gap-1.5">
<span class="bg-success size-2 rounded-full" aria-hidden="true" />
<span class="text-foreground font-semibold">Founders</span>
</div>
<Badge variant="outline" class="border-success/30 text-success font-mono text-xs">
{{ formatPercent(simFounderPct) }}
</Badge>
</div>
<div class="text-muted-foreground flex flex-wrap items-center justify-between gap-x-2">
<span>Dilution:</span>
<span class="text-destructive text-destructive font-mono tabular-nums">
60.0% → {{ formatPercent(simFounderPct) }} (-{{ (60 - simFounderPct).toFixed(1) }}%)
</span>
</div>
<div class="text-muted-foreground flex flex-wrap items-center justify-between gap-x-2">
<span>Equity Value:</span>
<span class="text-foreground font-mono font-semibold tabular-nums">{{
formatCurrency(simFounderValue)
}}</span>
</div>
<div class="text-success flex flex-wrap items-center justify-between gap-x-2 text-xs">
<span>Value Accretion:</span>
<span class="font-mono font-semibold tabular-nums"
>+{{ formatCurrency(simFounderValue - 27000000) }}</span
>
</div>
</div>
<!-- Seed Investors -->
<div class="border-border/80 bg-card space-y-2 rounded-lg border p-3 text-xs shadow-xs">
<div class="flex flex-wrap items-center justify-between gap-x-2">
<div class="flex items-center gap-1.5">
<span class="bg-chart-2 size-2 rounded-full" aria-hidden="true" />
<span class="text-foreground font-semibold">Seed Investors</span>
</div>
<Badge variant="outline" class="border-chart-2/30 text-chart-2 font-mono text-xs">
{{ formatPercent(simSeedPct) }}
</Badge>
</div>
<div class="text-muted-foreground flex flex-wrap items-center justify-between gap-x-2">
<span>Dilution:</span>
<span class="text-destructive text-destructive font-mono tabular-nums">
25.0% → {{ formatPercent(simSeedPct) }} (-{{ (25 - simSeedPct).toFixed(1) }}%)
</span>
</div>
<div class="text-muted-foreground flex flex-wrap items-center justify-between gap-x-2">
<span>Equity Value:</span>
<span class="text-foreground font-mono font-semibold tabular-nums">{{
formatCurrency(simSeedValue)
}}</span>
</div>
<div class="text-success flex flex-wrap items-center justify-between gap-x-2 text-xs">
<span>Value Accretion:</span>
<span class="font-mono font-semibold tabular-nums">+{{ formatCurrency(simSeedValue - 11250000) }}</span>
</div>
</div>
<!-- Option Pool -->
<div class="border-border/80 bg-card space-y-2 rounded-lg border p-3 text-xs shadow-xs">
<div class="flex flex-wrap items-center justify-between gap-x-2">
<div class="flex items-center gap-1.5">
<span class="bg-info size-2 rounded-full" aria-hidden="true" />
<span class="text-foreground font-semibold">Option Pool</span>
</div>
<Badge variant="outline" class="border-info/30 text-info font-mono text-xs">
{{ formatPercent(simOptionPct) }}
</Badge>
</div>
<div class="text-muted-foreground flex flex-wrap items-center justify-between gap-x-2">
<span>Dilution:</span>
<span class="text-destructive text-destructive font-mono tabular-nums">
15.0% → {{ formatPercent(simOptionPct) }} (-{{ (15 - simOptionPct).toFixed(1) }}%)
</span>
</div>
<div class="text-muted-foreground flex flex-wrap items-center justify-between gap-x-2">
<span>Equity Value:</span>
<span class="text-foreground font-mono font-semibold tabular-nums">{{
formatCurrency(simOptionValue)
}}</span>
</div>
<div class="text-success flex flex-wrap items-center justify-between gap-x-2 text-xs">
<span>Value Accretion:</span>
<span class="font-mono font-semibold tabular-nums"
>+{{ formatCurrency(simOptionValue - 6750000) }}</span
>
</div>
</div>
<!-- New Series A Investors -->
<div class="border-border/80 bg-card space-y-2 rounded-lg border p-3 text-xs shadow-xs">
<div class="flex flex-wrap items-center justify-between gap-x-2">
<div class="flex items-center gap-1.5">
<span class="bg-warning size-2 rounded-full" aria-hidden="true" />
<span class="text-foreground font-semibold">Series A Syndicate</span>
</div>
<Badge variant="outline" class="border-warning/30 text-warning font-mono text-xs"> New Investor </Badge>
</div>
<div class="text-muted-foreground flex flex-wrap items-center justify-between gap-x-2">
<span>New Ownership:</span>
<span class="text-warning text-warning font-mono font-bold tabular-nums">
{{ formatPercent(simNewInvestorPct) }}
</span>
</div>
<div class="text-muted-foreground flex flex-wrap items-center justify-between gap-x-2">
<span>Shares Purchased:</span>
<span class="text-foreground font-mono font-semibold tabular-nums">{{
formatNumber(simNewSharesIssued)
}}</span>
</div>
<div class="text-muted-foreground flex flex-wrap items-center justify-between gap-x-2 text-xs">
<span>Capital Invested:</span>
<span class="text-foreground font-mono font-semibold tabular-nums">{{
formatCurrency(simInvestment)
}}</span>
</div>
</div>
</div>
</div>
<!-- Insight Banner Callout -->
<div class="border-border/60 bg-muted/40 flex items-start gap-3 rounded-lg border p-3.5 text-xs">
<Info class="text-primary mt-0.5 size-4 shrink-0" aria-hidden="true" />
<div class="text-muted-foreground space-y-1">
<span class="text-foreground font-semibold">Round Dynamics Takeaway: </span>
Raising <span class="text-foreground font-mono font-medium">{{ formatCurrency(simInvestment) }}</span> at a
<span class="text-foreground font-mono font-medium">{{ formatCurrency(simPreMoney) }}</span> pre-money
valuation generates a
<span class="text-foreground font-mono font-medium">${{ simNewSharePrice.toFixed(2) }}</span> share price.
Although existing founders experience
<span class="text-foreground font-mono font-medium">{{ (60 - simFounderPct).toFixed(1) }}%</span> dilution,
their net equity value increases by
<span class="text-success font-mono font-bold">+{{ formatCurrency(simFounderValue - 27000000) }}</span>
(+{{ (((simFounderValue - 27000000) / 27000000) * 100).toFixed(1) }}%) due to the valuation step-up.
</div>
</div>
</CardContent>
</Card>
</div>
</template>
Raw manifest:https://uipkge.dev/r/vue/cap-table-ownership-summary.json