
3D Extruded Buildings & Urban Footprints
Real-world 3D building extrusions with dynamic sunlight shadows, terrain DEM elevations, and pitch/bearing camera controls.
YNAB and Copilot-style zero-based monthly budget planner: envelope category budgeting, monthly pacing metrics, real-time spending progress by envelope, color-coded health indicators, and savings goal trackers.
Also available for React ->$pnpm dlx shadcn-vue@latest add https://uipkge.dev/r/vue/personal-finance-budget.json$npx shadcn-vue@latest add https://uipkge.dev/r/vue/personal-finance-budget.json$yarn dlx shadcn-vue@latest add https://uipkge.dev/r/vue/personal-finance-budget.json$bunx shadcn-vue@latest add https://uipkge.dev/r/vue/personal-finance-budget.jsonnpx shadcn-vue@latest add @uipkge/personal-finance-budgetInstalls to:app/components/blocks/| Name | Type / Values | Default | Required |
|---|---|---|---|
class | HTMLAttributes['class'] | — | optional |
Type aliases exported from this item's source. Use these to shape the data you pass in.
EnvelopeCategoryinterface EnvelopeCategory {
id: string
name: string
icon: any
budgeted: number
spent: number
note: string
status: 'healthy' | 'warning' | 'completed' | 'on-track'
statusLabel: string
isSavings?: boolean
}EnvelopeSectioninterface EnvelopeSection {
id: string
title: string
description: string
icon: any
totalBudgeted: number
totalSpent: number
categories: EnvelopeCategory[]
}<script setup lang="ts">
import { type HTMLAttributes } from 'vue'
import {
Calendar,
CheckCircle2,
Clock,
CreditCard,
Film,
FolderPlus,
HeartPulse,
Home,
Laptop,
PiggyBank,
Plane,
Plus,
ShieldCheck,
ShoppingCart,
TrendingUp,
UtensilsCrossed,
Wallet,
Zap,
} from 'lucide-vue-next'
import { cn } from '@/lib/utils'
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 { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
interface EnvelopeCategory {
id: string
name: string
icon: any
budgeted: number
spent: number
note: string
status: 'healthy' | 'warning' | 'completed' | 'on-track'
statusLabel: string
isSavings?: boolean
}
interface EnvelopeSection {
id: string
title: string
description: string
icon: any
totalBudgeted: number
totalSpent: number
categories: EnvelopeCategory[]
}
const sections: EnvelopeSection[] = [
{
id: 'immediate-obligations',
title: 'Immediate Obligations',
description: 'Non-negotiable fixed living expenses and essential monthly bills.',
icon: Home,
totalBudgeted: 3750,
totalSpent: 3550,
categories: [
{
id: 'cat-housing',
name: 'Housing & Rent',
icon: Home,
budgeted: 2400,
spent: 2400,
note: 'Mortgage, HOA fee & insurance · Auto-paid Aug 1',
status: 'completed',
statusLabel: 'Paid in Full',
},
{
id: 'cat-utilities',
name: 'Utilities & Internet',
icon: Zap,
budgeted: 350,
spent: 320,
note: 'Electric ($185), Water ($75), Fiber ($60)',
status: 'healthy',
statusLabel: 'Healthy',
},
{
id: 'cat-groceries',
name: 'Groceries & Household',
icon: ShoppingCart,
budgeted: 800,
spent: 650,
note: 'Trader Joe’s & weekly produce · $15.00/day left',
status: 'healthy',
statusLabel: 'Healthy',
},
{
id: 'cat-health',
name: 'Health & Medical',
icon: HeartPulse,
budgeted: 200,
spent: 180,
note: 'Prescription copays & health membership',
status: 'healthy',
statusLabel: 'Healthy',
},
],
},
{
id: 'lifestyle-discretionary',
title: 'Lifestyle & Discretionary',
description: 'Variable lifestyle choices, dining, entertainment, and digital subscriptions.',
icon: UtensilsCrossed,
totalBudgeted: 700,
totalSpent: 625,
categories: [
{
id: 'cat-dining',
name: 'Dining Out & Cafes',
icon: UtensilsCrossed,
budgeted: 400,
spent: 380,
note: 'Restaurants, coffee & takeout · $20.00 left for 10 days',
status: 'warning',
statusLabel: 'Near Limit',
},
{
id: 'cat-entertainment',
name: 'Entertainment & Leisure',
icon: Film,
budgeted: 200,
spent: 150,
note: 'Cinema tickets, gaming pass & weekend outings',
status: 'healthy',
statusLabel: 'Healthy',
},
{
id: 'cat-tech',
name: 'Tech & Subscriptions',
icon: Laptop,
budgeted: 100,
spent: 95,
note: 'iCloud, Spotify, GitHub Pro, ChatGPT Plus',
status: 'warning',
statusLabel: 'Near Limit',
},
],
},
{
id: 'savings-investments',
title: 'Savings Goals & Investments',
description: 'Dedicated wealth accumulation, emergency reserve, and planned sinking funds.',
icon: PiggyBank,
totalBudgeted: 1583,
totalSpent: 1483,
categories: [
{
id: 'cat-emergency',
name: 'Emergency Fund',
icon: ShieldCheck,
budgeted: 500,
spent: 500,
note: 'High-yield cash reserve · Total balance: $18,500.00',
status: 'completed',
statusLabel: 'Goal Met',
isSavings: true,
},
{
id: 'cat-roth',
name: 'Roth IRA Contribution',
icon: TrendingUp,
budgeted: 583,
spent: 583,
note: 'Broad-market index ETF · 2026 cap pacing ($7,000/yr)',
status: 'completed',
statusLabel: 'Maxed Monthly',
isSavings: true,
},
{
id: 'cat-vacation',
name: 'Vacation & Travel Fund',
icon: Plane,
budgeted: 500,
spent: 400,
note: 'Tokyo autumn trip 2027 · $3,400.00 of $5,000.00 goal',
status: 'on-track',
statusLabel: '80% Funded',
isSavings: true,
},
],
},
]
function formatCurrency(amount: number): string {
return `$${amount.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
}
function getProgressColor(status: EnvelopeCategory['status']): string {
if (status === 'warning') {
return '[&_[data-slot=progress-indicator]]:bg-warning'
}
if (status === 'completed' || status === 'healthy' || status === 'on-track') {
return '[&_[data-slot=progress-indicator]]:bg-success'
}
return '[&_[data-slot=progress-indicator]]:bg-primary'
}
function getBadgeVariant(status: EnvelopeCategory['status']) {
if (status === 'warning') return 'warning'
if (status === 'completed' || status === 'healthy' || status === 'on-track') return 'success'
return 'outline'
}
</script>
<template>
<div data-slot="personal-finance-budget" :class="cn('mx-auto w-full max-w-6xl space-y-6', props.class)">
<!-- Header -->
<div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
<div class="space-y-1.5">
<div class="flex flex-wrap items-center gap-2.5">
<h1 class="text-foreground text-2xl font-bold tracking-tight sm:text-3xl">Monthly Zero-Based Budget</h1>
<div
class="bg-muted text-foreground border-border flex items-center gap-1 rounded-md border px-2 py-0.5 text-xs font-semibold"
>
<Calendar class="text-muted-foreground size-3.5" />
<span>August 2026</span>
</div>
</div>
<div class="flex flex-wrap items-center gap-2 pt-0.5">
<div
class="border-success/20 bg-success/10 text-success inline-flex items-center gap-1.5 rounded-full border px-2.5 py-0.5 text-xs font-medium"
>
<CheckCircle2 class="size-3.5 shrink-0" />
<span
>To be Budgeted: <strong class="font-semibold tabular-nums">$0.00</strong> · Every dollar assigned!</span
>
</div>
<span class="text-muted-foreground hidden text-xs sm:inline">Zero-Based Envelope Method</span>
</div>
</div>
<div class="flex items-center gap-2">
<Button variant="outline" size="sm" class="gap-1.5 shadow-xs">
<FolderPlus class="text-muted-foreground size-4" />
<span>Add Category</span>
</Button>
<Button size="sm" class="gap-1.5 shadow-xs">
<Plus class="size-4" />
<span>Add Expense</span>
</Button>
</div>
</div>
<!-- 4 Primary Overview Cards -->
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
<!-- Card 1: Total Monthly Income -->
<Card class="flex flex-col justify-between shadow-xs">
<CardHeader class="pb-2">
<div class="flex items-center justify-between gap-2">
<div class="flex items-center gap-2">
<div
class="border-success/20 bg-success/10 text-success text-success flex size-8 shrink-0 items-center justify-center rounded-lg border shadow-xs"
aria-hidden="true"
>
<Wallet class="size-4" />
</div>
<CardTitle class="text-muted-foreground text-xs font-medium">Total Monthly Income</CardTitle>
</div>
<Badge variant="success" class="text-xs">100% Assigned</Badge>
</div>
</CardHeader>
<CardContent class="space-y-2">
<div>
<div class="text-foreground text-2xl font-bold tracking-tight tabular-nums">$8,500.00</div>
<p class="text-muted-foreground mt-0.5 text-xs">2 Inflow streams (Salary & Freelance)</p>
</div>
<div class="border-border/60 text-muted-foreground flex items-center justify-between border-t pt-2 text-xs">
<span>Unassigned Pool</span>
<span class="text-success text-success font-semibold tabular-nums">$0.00</span>
</div>
</CardContent>
</Card>
<!-- Card 2: Total Budgeted & Spent -->
<Card class="flex flex-col justify-between shadow-xs">
<CardHeader class="pb-2">
<div class="flex items-center justify-between gap-2">
<div class="flex items-center gap-2">
<div
class="bg-muted text-muted-foreground border-border flex size-8 shrink-0 items-center justify-center rounded-lg border shadow-xs"
aria-hidden="true"
>
<CreditCard class="size-4" />
</div>
<CardTitle class="text-muted-foreground text-xs font-medium">Budgeted & Spent</CardTitle>
</div>
<Badge variant="warning" class="text-xs tabular-nums">80.5% Spent</Badge>
</div>
</CardHeader>
<CardContent class="space-y-2">
<div>
<div class="flex items-baseline gap-1 text-2xl font-bold tracking-tight">
<span class="text-foreground tabular-nums">$6,840.00</span>
<span class="text-muted-foreground text-xs font-normal tabular-nums">/ $8,500.00</span>
</div>
<div class="mt-2 space-y-1">
<Progress :model-value="80.5" class="[&_[data-slot=progress-indicator]]:bg-warning h-2" />
</div>
</div>
<div class="border-border/60 text-muted-foreground flex items-center justify-between border-t pt-2 text-xs">
<span>Remaining in Envelopes</span>
<span class="text-foreground font-medium tabular-nums">$1,660.00</span>
</div>
</CardContent>
</Card>
<!-- Card 3: Total Savings Allocated -->
<Card class="flex flex-col justify-between shadow-xs">
<CardHeader class="pb-2">
<div class="flex items-center justify-between gap-2">
<div class="flex items-center gap-2">
<div
class="border-success/20 bg-success/10 text-success text-success flex size-8 shrink-0 items-center justify-center rounded-lg border shadow-xs"
aria-hidden="true"
>
<PiggyBank class="size-4" />
</div>
<CardTitle class="text-muted-foreground text-xs font-medium">Savings Allocated</CardTitle>
</div>
<Badge variant="success" class="text-xs tabular-nums">19.5% Rate</Badge>
</div>
</CardHeader>
<CardContent class="space-y-2">
<div>
<div class="text-foreground text-2xl font-bold tracking-tight tabular-nums">$1,660.00</div>
<div class="mt-2 space-y-1">
<Progress :model-value="89.3" class="[&_[data-slot=progress-indicator]]:bg-success h-2" />
</div>
</div>
<div class="border-border/60 text-muted-foreground flex items-center justify-between border-t pt-2 text-xs">
<span>Funded to Date</span>
<span class="text-success text-success font-semibold tabular-nums">$1,483.00 (89.3%)</span>
</div>
</CardContent>
</Card>
<!-- Card 4: Days Remaining in Month -->
<Card class="flex flex-col justify-between shadow-xs">
<CardHeader class="pb-2">
<div class="flex items-center justify-between gap-2">
<div class="flex items-center gap-2">
<div
class="bg-muted text-muted-foreground border-border flex size-8 shrink-0 items-center justify-center rounded-lg border shadow-xs"
aria-hidden="true"
>
<Clock class="size-4" />
</div>
<CardTitle class="text-muted-foreground text-xs font-medium">Month Pace</CardTitle>
</div>
<Badge variant="outline" class="text-xs font-normal tabular-nums">Day 21 of 31</Badge>
</div>
</CardHeader>
<CardContent class="space-y-2">
<div>
<div class="text-foreground text-2xl font-bold tracking-tight tabular-nums">10 Days left</div>
<div class="mt-2 space-y-1">
<Progress :model-value="67.7" class="h-2" />
</div>
</div>
<div class="border-border/60 text-muted-foreground flex items-center justify-between border-t pt-2 text-xs">
<span>Cycle Elapsed</span>
<span class="text-foreground font-medium tabular-nums">67.7% of month</span>
</div>
</CardContent>
</Card>
</div>
<!-- Envelope Categories List Grouped by Section -->
<div class="space-y-6">
<Card v-for="section in sections" :key="section.id" class="overflow-hidden shadow-xs">
<CardHeader class="bg-muted/20 border-border border-b py-4">
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<div class="space-y-0.5">
<div class="flex items-center gap-2">
<component :is="section.icon" class="text-muted-foreground size-4 shrink-0" />
<CardTitle class="text-base font-semibold">{{ section.title }}</CardTitle>
</div>
<CardDescription class="text-xs">
{{ section.description }}
</CardDescription>
</div>
<div class="flex items-center gap-3 text-xs">
<div class="bg-background border-border flex items-center gap-2 rounded-md border px-3 py-1.5 shadow-2xs">
<span class="text-muted-foreground">Subtotal:</span>
<span class="text-foreground font-semibold tabular-nums">{{ formatCurrency(section.totalSpent) }}</span>
<span class="text-muted-foreground">/</span>
<span class="text-muted-foreground font-medium tabular-nums">{{
formatCurrency(section.totalBudgeted)
}}</span>
</div>
</div>
</div>
</CardHeader>
<CardContent class="p-0">
<div class="overflow-x-auto">
<Table>
<TableHeader>
<TableRow class="hover:bg-transparent">
<TableHead class="min-w-[240px] text-xs font-semibold">Envelope Category</TableHead>
<TableHead class="text-right text-xs font-semibold">{{
section.id === 'savings-investments' ? 'Monthly Target' : 'Budgeted'
}}</TableHead>
<TableHead class="text-right text-xs font-semibold">{{
section.id === 'savings-investments' ? 'Funded to Date' : 'Spent to Date'
}}</TableHead>
<TableHead class="text-right text-xs font-semibold">{{
section.id === 'savings-investments' ? 'Remaining to Goal' : 'Available Balance'
}}</TableHead>
<TableHead class="min-w-[180px] text-xs font-semibold">Progress & Pace</TableHead>
<TableHead class="w-[110px] text-right text-xs font-semibold">Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-for="cat in section.categories" :key="cat.id" class="hover:bg-muted/40 transition-colors">
<!-- Envelope Category & Note -->
<TableCell class="py-3.5">
<div class="flex items-start gap-3">
<div
class="bg-muted text-muted-foreground border-border mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-lg border shadow-xs"
aria-hidden="true"
>
<component :is="cat.icon" class="size-4" />
</div>
<div class="space-y-0.5">
<p class="text-foreground text-xs font-semibold">{{ cat.name }}</p>
<p class="text-muted-foreground text-xs">{{ cat.note }}</p>
</div>
</div>
</TableCell>
<!-- Budgeted Amount -->
<TableCell class="text-foreground py-3.5 text-right text-xs font-medium tabular-nums">
{{ formatCurrency(cat.budgeted) }}
</TableCell>
<!-- Spent to Date -->
<TableCell class="text-foreground py-3.5 text-right text-xs font-medium tabular-nums">
{{ formatCurrency(cat.spent) }}
</TableCell>
<!-- Remaining Balance -->
<TableCell class="py-3.5 text-right text-xs">
<span
:class="[
'font-bold tabular-nums',
cat.budgeted - cat.spent === 0
? 'text-muted-foreground'
: cat.status === 'warning'
? 'text-warning'
: 'text-success',
]"
>
{{ formatCurrency(cat.budgeted - cat.spent) }}
</span>
</TableCell>
<!-- Progress Bar & Percentage -->
<TableCell class="py-3.5">
<div class="space-y-1.5">
<div class="flex items-center justify-between text-xs">
<span class="text-muted-foreground tabular-nums">
{{ Math.round((cat.spent / cat.budgeted) * 100) }}% {{ cat.isSavings ? 'funded' : 'used' }}
</span>
<span class="text-muted-foreground text-xs tabular-nums">
{{ formatCurrency(cat.spent) }}
</span>
</div>
<Progress
:model-value="Math.min(100, Math.round((cat.spent / cat.budgeted) * 100))"
:class="cn('h-1.5', getProgressColor(cat.status))"
/>
</div>
</TableCell>
<!-- Status Badge -->
<TableCell class="py-3.5 text-right">
<Badge :variant="getBadgeVariant(cat.status)" class="text-xs">
{{ cat.statusLabel }}
</Badge>
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
</CardContent>
</Card>
</div>
<!-- Zero-Based Budgeting Philosophy & Rules Card -->
<Card class="bg-muted/20 border-border p-4 shadow-xs">
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div class="flex items-start gap-3">
<div
class="border-success/20 bg-success/10 text-success text-success mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-lg border shadow-xs"
aria-hidden="true"
>
<CheckCircle2 class="size-4" />
</div>
<div class="space-y-0.5">
<h4 class="text-foreground text-xs font-semibold">Zero-Based Allocation Completed</h4>
<p class="text-muted-foreground text-xs">
Every dollar of your $8,500.00 income is accounted for: $4,450.00 living expenses + $1,583.00 savings
goals + $2,467.00 scheduled buffer = $0.00 unassigned.
</p>
</div>
</div>
<div class="flex shrink-0 items-center gap-2">
<Badge variant="outline" class="text-xs font-normal"> Next Payday: Sep 1, 2026 </Badge>
</div>
</div>
</Card>
</div>
</template>
Raw manifest:https://uipkge.dev/r/vue/personal-finance-budget.json