Cap Table Ownership Summary
blockfinanceCarta-style company equity capitalization table, share class breakdown, fully diluted ownership percentages, shareholder vesting progress, and interactive round dilution modeling simulator.
Installation
$pnpm dlx uipkge-ng@latest add cap-table-ownership-summary$npx uipkge-ng@latest add cap-table-ownership-summary$yarn dlx uipkge-ng@latest add cap-table-ownership-summary$bunx uipkge-ng@latest add cap-table-ownership-summaryManifest:
https://uipkge.dev/r/angular/cap-table-ownership-summary.jsonInstalls to:src/app/components/blocks/cap-table-ownership-summary/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.
Shareholderinterface Shareholder {
id: string
name: string
role: string
avatar: string
shareClass: CapTableShareClass
shares: number
ownershipPct: number
equityValue: number
vestingDetail: string
vestedMonths: number
totalMonths: number
isVestedComplete: boolean
grantDate: string
liquidationPref?: string
}Files installed (2)
src/app/components/blocks/cap-table-ownership-summary/cap-table-ownership-summary.component.ts55.7 kB
// Carta-style company equity capitalization table, share class breakdown, fully diluted // ownership percentages, shareholder vesting progress, and an interactive round dilution // modeling simulator. Port of the React block 1:1. import { ChangeDetectionStrategy, Component, Input, computed, signal } from '@angular/core' import { Award, Building2, Calculator, Check, Coins, FileSpreadsheet, Info, Layers, LucideAngularModule, Plus, RefreshCw, Search, ShieldCheck, TrendingUp, Users, X, } from 'lucide-angular' import { cn } from '@/lib/utils' import { UiAvatarComponent, UiAvatarFallbackComponent } 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 { UiInputComponent } from '@/ui/input/input.component' import { UiTableBodyComponent, UiTableCellComponent, UiTableComponent, UiTableHeadComponent, UiTableHeaderComponent, UiTableRowComponent, } from '@/ui/table/table.component' export type CapTableShareClass = 'Common Stock' | 'Series Seed Preferred' | 'ISO Option Pool' export type CapTableClassFilter = 'All' | CapTableShareClass export type GrantEquityClass = 'ISO Options' | 'NSO Options' | 'Restricted Stock' export interface Shareholder { id: string name: string role: string avatar: string shareClass: CapTableShareClass shares: number ownershipPct: number equityValue: number vestingDetail: string vestedMonths: number totalMonths: number isVestedComplete: boolean grantDate: string liquidationPref?: string } const INITIAL_SHAREHOLDERS: 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', }, ] @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-cap-table-ownership-summary, [ui-cap-table-ownership-summary]', 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, UiBadgeComponent, UiButtonComponent, UiCardComponent, UiCardContentComponent, UiCardDescriptionComponent, UiCardHeaderComponent, UiCardTitleComponent, UiInputComponent, UiTableBodyComponent, UiTableCellComponent, UiTableComponent, UiTableHeadComponent, UiTableHeaderComponent, UiTableRowComponent, ], template: ` <div data-slot="cap-table-ownership-summary" [class]="rootClass"> <!-- 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" > <lucide-icon [img]="Building2" class="size-5" /> </div> <div> <h1 class="text-foreground text-2xl font-bold tracking-tight"> Capitalization Table & Equity Ownership </h1> </div> <span ui-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"></span> Audited · Series Seed </span> </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"> <lucide-icon [img]="ShieldCheck" class="text-primary size-3.5" /> 409A FMV: ${{ currentSharePrice.toFixed(2) }}/share </span> </div> </div> <!-- Action Buttons --> <div class="flex flex-wrap items-center gap-2"> <button ui-button variant="outline" size="sm" class="gap-1.5 shadow-xs" [disabled]="isExporting()" (click)="handleExportCapTable()" > @if (isExporting()) { <lucide-icon [img]="RefreshCw" class="size-4 animate-spin" /> } @else if (exportSuccess()) { <lucide-icon [img]="Check" class="text-success size-4" /> } @else { <lucide-icon [img]="FileSpreadsheet" class="size-4" /> } <span>{{ exportButtonLabel() }}</span> </button> <button ui-button size="sm" class="gap-1.5 shadow-xs" (click)="isGrantDialogOpen.set(true)"> <lucide-icon [img]="Plus" class="size-4" /> Issue Equity Grant </button> </div> </header> <!-- Issue Equity Grant Dialog Modal Overlay --> @if (isGrantDialogOpen()) { <div 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" > <div ui-card class="border-border w-full max-w-lg shadow-lg"> <div ui-card-header class="pb-3"> <div class="flex flex-wrap items-center justify-between gap-x-2"> <h2 ui-card-title id="grant-dialog-title" class="flex items-center gap-2 text-base font-semibold"> <lucide-icon [img]="Award" class="text-primary size-5" /> Issue New Equity Option Grant </h2> <button ui-button variant="ghost" size="icon-sm" class="text-muted-foreground hover:text-foreground size-7" aria-label="Close dialog" (click)="isGrantDialogOpen.set(false)" > <lucide-icon [img]="X" class="size-4" /> </button> </div> <p ui-card-description class="text-xs"> Authorize and draft an option grant under the 2024 Equity Incentive Plan (EIP). </p> </div> <div ui-card-content class="space-y-4"> @if (grantSuccessMessage()) { <div class="border-success/20 bg-success/10 rounded-lg border p-4 text-center"> <lucide-icon [img]="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> } @else { <form class="space-y-3.5" (submit)="handleCreateGrant($event)"> <div class="space-y-1.5"> <label for="angular-grantee-name" class="text-foreground text-xs font-medium"> Grantee Legal Name </label> <ui-input id="angular-grantee-name" [value]="grantName()" (valueChange)="grantName.set($event)" placeholder="e.g. Dr. Sarah Chen" 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="angular-grantee-role" class="text-foreground text-xs font-medium">Role / Title</label> <ui-input id="angular-grantee-role" [value]="grantRole()" (valueChange)="grantRole.set($event)" placeholder="e.g. Principal AI Scientist" class="h-9 text-xs" /> </div> <div class="space-y-1.5"> <label for="angular-grant-quantity" class="text-foreground text-xs font-medium"> Number of Options / Shares </label> <ui-input id="angular-grant-quantity" [value]="grantShares()" (valueChange)="grantShares.set(numberOr($event, 0))" type="number" min="1000" step="1000" 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> <span ui-badge variant="outline" class="text-xs">EIP-2024</span> </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> <ui-input [value]="grantVesting()" (valueChange)="grantVesting.set($event)" 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 ui-button type="button" variant="outline" size="sm" (click)="isGrantDialogOpen.set(false)"> Cancel </button> <button ui-button type="submit" size="sm" class="gap-1.5"> <lucide-icon [img]="Check" class="size-4" /> Draft & Issue Grant </button> </div> </form> } </div> </div> </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 --> <div ui-card class="border-border relative overflow-hidden shadow-xs"> <div ui-card-header class="flex flex-row items-center justify-between pb-2"> <h3 ui-card-title class="text-muted-foreground text-xs font-medium">Total Fully Diluted Shares</h3> <div class="bg-primary/10 text-primary rounded-md p-1.5"> <lucide-icon [img]="Layers" class="size-4" /> </div> </div> <div ui-card-content 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> </div> </div> <!-- Founder Common Stock Card --> <div ui-card class="border-border relative overflow-hidden shadow-xs"> <div ui-card-header class="flex flex-row items-center justify-between pb-2"> <h3 ui-card-title class="text-muted-foreground text-xs font-medium">Founder Common Stock</h3> <div class="bg-success/10 text-success rounded-md p-1.5"> <lucide-icon [img]="Users" class="size-4" /> </div> </div> <div ui-card-content 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> </div> </div> <!-- Series Seed Preferred Card --> <div ui-card class="border-border relative overflow-hidden shadow-xs"> <div ui-card-header class="flex flex-row items-center justify-between pb-2"> <h3 ui-card-title class="text-muted-foreground text-xs font-medium">Series Seed Preferred</h3> <div class="bg-chart-2/10 text-chart-2 rounded-md p-1.5"> <lucide-icon [img]="TrendingUp" class="size-4" /> </div> </div> <div ui-card-content 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> </div> </div> <!-- Unallocated Option Pool Card --> <div ui-card class="border-border relative overflow-hidden shadow-xs"> <div ui-card-header class="flex flex-row items-center justify-between pb-2"> <h3 ui-card-title class="text-muted-foreground text-xs font-medium">Unallocated Option Pool</h3> <div class="bg-info/10 text-info rounded-md p-1.5"> <lucide-icon [img]="Coins" class="size-4" /> </div> </div> <div ui-card-content 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> </div> </div> </div> <!-- Company Valuation Hero & Visual Stacked Ownership Bar Card --> <div ui-card class="border-border overflow-hidden shadow-xs"> <div ui-card-header class="pb-3"> <div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between"> <div> <p ui-card-description class="text-muted-foreground text-xs font-semibold tracking-wider uppercase"> Current Post-Money Valuation </p> <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" > <lucide-icon [img]="TrendingUp" class="size-3.5" /> 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"></span> <span>Cap Table Reconciled</span> </div> <span class="font-mono">10,000,000 Total Units</span> </div> </div> </div> <div ui-card-content 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" ></div> <!-- 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" ></div> <!-- 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> </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"></span> <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"></span> <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"></span> <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> </div> </div> <!-- Shareholder Breakdown Table Card --> <div ui-card class="border-border shadow-xs"> <div ui-card-header 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> <h2 ui-card-title class="text-foreground text-base font-semibold">Shareholder & Equity Register</h2> <p ui-card-description class="text-muted-foreground mt-0.5 text-xs"> Detailed registry of individual grants, share classes, fully diluted ownership, and vesting timelines. </p> </div> <div class="flex flex-col gap-2 sm:flex-row sm:items-center"> <!-- Search --> <div class="relative w-full sm:w-56"> <lucide-icon [img]="Search" class="text-muted-foreground absolute top-1/2 left-2.5 size-3.5 -translate-y-1/2" /> <ui-input [value]="searchQuery()" (valueChange)="searchQuery.set($event)" 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]="filterButtonClass('All')" (click)="selectedClassFilter.set('All')"> All (5) </button> <button type="button" [class]="filterButtonClass('Common Stock')" (click)="selectedClassFilter.set('Common Stock')" > Common (2) </button> <button type="button" [class]="filterButtonClass('Series Seed Preferred')" (click)="selectedClassFilter.set('Series Seed Preferred')" > Preferred (2) </button> <button type="button" [class]="filterButtonClass('ISO Option Pool')" (click)="selectedClassFilter.set('ISO Option Pool')" > Pool (1) </button> </div> </div> </div> </div> <div ui-card-content class="p-0"> <div class="overflow-x-auto"> <table ui-table> <thead ui-table-header> <tr ui-table-row class="hover:bg-transparent"> <th ui-table-head class="w-[260px] pl-6 text-xs font-medium">Shareholder / Entity</th> <th ui-table-head class="w-[180px] text-xs font-medium">Share Class</th> <th ui-table-head class="text-right text-xs font-medium">Shares Owned</th> <th ui-table-head class="text-right text-xs font-medium">Fully Diluted %</th> <th ui-table-head class="text-right text-xs font-medium">Total Equity Value</th> <th ui-table-head class="min-w-[240px] pr-6 text-xs font-medium">Vesting & Status</th> </tr> </thead> <tbody ui-table-body> @for (sh of filteredShareholders(); track sh.id) { <tr ui-table-row class="hover:bg-muted/30 transition-colors"> <!-- Shareholder Name & Role --> <td ui-table-cell class="py-3.5 pl-6"> <div class="flex items-center gap-3"> <ui-avatar class="border-border/80 size-9 border shadow-xs"> <ui-avatar-fallback [class]="avatarFallbackClass(sh.shareClass)"> {{ sh.avatar }} </ui-avatar-fallback> </ui-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> </td> <!-- Share Class Badge --> <td ui-table-cell class="py-3.5"> @if (sh.shareClass === 'Common Stock') { <span ui-badge variant="outline" class="border-success/30 bg-success/10 text-success text-xs font-medium" > Common Stock </span> } @else if (sh.shareClass === 'Series Seed Preferred') { <span ui-badge variant="outline" class="border-chart-2/30 bg-chart-2/10 text-chart-2 text-xs font-medium" > Series Seed Preferred </span> } @else { <span ui-badge variant="outline" class="border-info/30 bg-info/10 text-info text-xs font-medium" > ISO Option Pool </span> } </td> <!-- Shares Owned --> <td ui-table-cell 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> </td> <!-- Fully Diluted Ownership Percentage --> <td ui-table-cell 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> </td> <!-- Total Equity Value --> <td ui-table-cell 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> </td> <!-- Vesting Status Progress --> <td ui-table-cell 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">{{ vestingLabel(sh) }}</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]="vestingBarClass(sh)" [style.width.%]="vestingPct(sh)"></div> </div> <div class="text-muted-foreground text-xs">{{ sh.vestingDetail }}</div> </div> </td> </tr> } </tbody> </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> </div> </div> <!-- Round Modeling Simulator Card (Series A Dilution Simulator) --> <div ui-card class="border-border shadow-xs"> <div ui-card-header 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"> <lucide-icon [img]="Calculator" class="size-4" /> </div> <h2 ui-card-title class="text-foreground text-base font-semibold"> Round Modeling Simulator (Series A Pro-Forma) </h2> </div> <p ui-card-description 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. </p> </div> <!-- Preset Scenario Buttons --> <div class="flex flex-wrap items-center gap-1.5"> <button ui-button variant="outline" size="sm" class="h-7 text-xs font-medium" (click)="setPresetScenario(30000000, 5000000)" > $5M @ $30M Pre </button> <button ui-button variant="outline" size="sm" class="h-7 text-xs font-medium" (click)="setPresetScenario(50000000, 10000000)" > $10M @ $50M Pre </button> <button ui-button variant="outline" size="sm" class="h-7 text-xs font-medium" (click)="setPresetScenario(60000000, 15000000)" > $15M @ $60M Pre </button> </div> </div> </div> <div ui-card-content 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="angular-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"> <ui-input id="angular-sim-pre-money" [value]="simPreMoney()" (valueChange)="simPreMoney.set(numberOr($event, 0))" 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 > (+{{ stepUpPct().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="angular-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"> <ui-input id="angular-sim-investment" [value]="simInvestment()" (valueChange)="simInvestment.set(numberOr($event, 0))" 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 (+{{ stepUpPct().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> <div [style.width.%]="simSeedPct()" class="bg-chart-2 h-full transition-[width] duration-300" [title]="'Seed Investors: ' + simSeedPct().toFixed(1) + '%'" ></div> <div [style.width.%]="simOptionPct()" class="bg-info h-full transition-[width] duration-300" [title]="'Option Pool: ' + simOptionPct().toFixed(1) + '%'" ></div> <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> </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"></span> <span class="text-foreground font-semibold">Founders</span> </div> <span ui-badge variant="outline" class="border-success/30 text-success font-mono text-xs"> {{ formatPercent(simFounderPct()) }} </span> </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"></span> <span class="text-foreground font-semibold">Seed Investors</span> </div> <span ui-badge variant="outline" class="border-chart-2/30 text-chart-2 font-mono text-xs"> {{ formatPercent(simSeedPct()) }} </span> </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"></span> <span class="text-foreground font-semibold">Option Pool</span> </div> <span ui-badge variant="outline" class="border-info/30 text-info font-mono text-xs"> {{ formatPercent(simOptionPct()) }} </span> </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"></span> <span class="text-foreground font-semibold">Series A Syndicate</span> </div> <span ui-badge variant="outline" class="border-warning/30 text-warning font-mono text-xs"> New Investor </span> </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"> <lucide-icon [img]="Info" class="text-primary mt-0.5 size-4 shrink-0" /> <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> (+{{ founderAccretionPct().toFixed(1) }}%) due to the valuation step-up. </div> </div> </div> </div> </div> `, }) export class UiCapTableOwnershipSummaryComponent { protected readonly Award = Award protected readonly Building2 = Building2 protected readonly Calculator = Calculator protected readonly Check = Check protected readonly Coins = Coins protected readonly FileSpreadsheet = FileSpreadsheet protected readonly Info = Info protected readonly Layers = Layers protected readonly Plus = Plus protected readonly RefreshCw = RefreshCw protected readonly Search = Search protected readonly ShieldCheck = ShieldCheck protected readonly TrendingUp = TrendingUp protected readonly Users = Users protected readonly X = X // Current Cap Table Static Baseline protected readonly currentSharePrice = 4.5 protected readonly currentTotalShares = 10000000 protected readonly currentValuation = 45000000 @Input() set initialInvestment(v: number | undefined) { if (v !== undefined) this.simInvestment.set(v) } @Input() set initialPreMoney(v: number | undefined) { if (v !== undefined) this.simPreMoney.set(v) } @Input('class') className?: string readonly shareholders = signal<Shareholder[]>(INITIAL_SHAREHOLDERS.map((s) => ({ ...s }))) // Filter and Search State readonly searchQuery = signal('') readonly selectedClassFilter = signal<CapTableClassFilter>('All') readonly filteredShareholders = computed(() => { const filter = this.selectedClassFilter() const q = this.searchQuery().trim().toLowerCase() return this.shareholders().filter((sh) => { if (filter !== 'All' && sh.shareClass !== filter) return false if (q) { return ( sh.name.toLowerCase().includes(q) || sh.role.toLowerCase().includes(q) || sh.shareClass.toLowerCase().includes(q) ) } return true }) }) // Export feedback state readonly isExporting = signal(false) readonly exportSuccess = signal(false) private exportTimer: ReturnType<typeof setTimeout> | undefined private exportResetTimer: ReturnType<typeof setTimeout> | undefined // Issue Grant Modal State readonly isGrantDialogOpen = signal(false) readonly grantSuccessMessage = signal(false) readonly grantName = signal('') readonly grantRole = signal('') readonly grantShares = signal(50000) readonly grantClass = signal<GrantEquityClass>('ISO Options') readonly grantVesting = signal('4-Year Vesting · 1-Year Cliff (25%), then Monthly') private grantTimer: ReturnType<typeof setTimeout> | undefined // Round Modeling Simulator State readonly simPreMoney = signal(50000000) readonly simInvestment = signal(10000000) // Dynamic Round Modeling Calculations readonly simPostMoney = computed(() => this.simPreMoney() + this.simInvestment()) readonly simNewSharePrice = computed(() => { if (this.currentTotalShares <= 0) return 0 return this.simPreMoney() / this.currentTotalShares }) readonly simNewSharesIssued = computed(() => { if (this.simNewSharePrice() <= 0) return 0 return Math.round(this.simInvestment() / this.simNewSharePrice()) }) readonly simTotalPostShares = computed(() => this.currentTotalShares + this.simNewSharesIssued()) private readonly simFounderShares = 6000000 private readonly simSeedShares = 2500000 private readonly simOptionShares = 1500000 readonly simFounderPct = computed(() => (this.simFounderShares / this.simTotalPostShares()) * 100) readonly simSeedPct = computed(() => (this.simSeedShares / this.simTotalPostShares()) * 100) readonly simOptionPct = computed(() => (this.simOptionShares / this.simTotalPostShares()) * 100) readonly simNewInvestorPct = computed(() => (this.simNewSharesIssued() / this.simTotalPostShares()) * 100) readonly simFounderValue = computed(() => (this.simFounderPct() / 100) * this.simPostMoney()) readonly simSeedValue = computed(() => (this.simSeedPct() / 100) * this.simPostMoney()) readonly simOptionValue = computed(() => (this.simOptionPct() / 100) * this.simPostMoney()) readonly stepUpPct = computed( () => ((this.simNewSharePrice() - this.currentSharePrice) / this.currentSharePrice) * 100, ) readonly founderAccretionPct = computed(() => ((this.simFounderValue() - 27000000) / 27000000) * 100) readonly exportButtonLabel = computed(() => this.exportSuccess() ? 'Cap Table Exported!' : this.isExporting() ? 'Exporting...' : 'Export Cap Table (Excel)', ) get rootClass(): string { return cn('mx-auto w-full max-w-6xl space-y-6', this.className) } filterButtonClass(value: CapTableClassFilter): string { return cn( 'rounded-md px-2.5 py-1 text-xs font-medium transition-colors', this.selectedClassFilter() === value ? 'bg-background text-foreground shadow-xs' : 'text-muted-foreground hover:text-foreground', ) } avatarFallbackClass(shareClass: CapTableShareClass): string { return cn( 'text-xs font-bold', shareClass === 'Common Stock' ? 'bg-success/10 text-success' : shareClass === 'Series Seed Preferred' ? 'bg-chart-2/10 text-chart-2' : 'bg-info/10 text-info', ) } vestingPct(sh: Shareholder): number { if (sh.isVestedComplete) return 100 return (sh.vestedMonths / sh.totalMonths) * 100 } vestingLabel(sh: Shareholder): string { if (sh.isVestedComplete) return '100% Vested' if (sh.vestedMonths > 0) return `${Math.round((sh.vestedMonths / sh.totalMonths) * 100)}% Vested` return 'Unallocated' } vestingBarClass(sh: Shareholder): string { return cn( 'h-full transition-[width,background-color] duration-300', sh.isVestedComplete ? 'bg-primary' : sh.vestedMonths > 0 ? 'bg-success' : 'bg-info/40', ) } numberOr(raw: string, fallback: number): number { const n = Number(raw) return Number.isFinite(n) ? n : fallback } formatCurrency(val: number): string { return val.toLocaleString('en-US', { style: 'currency', currency: 'USD' }) } formatNumber(val: number): string { return val.toLocaleString('en-US') } formatPercent(val: number, decimals = 1): string { return `${val.toFixed(decimals)}%` } handleExportCapTable(): void { this.isExporting.set(true) clearTimeout(this.exportTimer) clearTimeout(this.exportResetTimer) this.exportTimer = setTimeout(() => { this.isExporting.set(false) this.exportSuccess.set(true) this.exportResetTimer = setTimeout(() => this.exportSuccess.set(false), 2500) }, 1000) } handleCreateGrant(event?: Event): void { event?.preventDefault() if (!this.grantName().trim() || this.grantShares() <= 0) return this.grantSuccessMessage.set(true) clearTimeout(this.grantTimer) this.grantTimer = setTimeout(() => { this.grantSuccessMessage.set(false) this.isGrantDialogOpen.set(false) this.grantName.set('') this.grantRole.set('') this.grantShares.set(50000) }, 1500) } setPresetScenario(preMoney: number, investment: number): void { this.simPreMoney.set(preMoney) this.simInvestment.set(investment) } }src/app/components/blocks/cap-table-ownership-summary/index.ts0.2 kB
export { UiCapTableOwnershipSummaryComponent, type CapTableClassFilter, type CapTableShareClass, type GrantEquityClass, type Shareholder, } from './cap-table-ownership-summary.component'
Raw manifest:https://uipkge.dev/r/angular/cap-table-ownership-summary.json