
3D Extruded Buildings & Urban Footprints
Real-world 3D building extrusions with dynamic sunlight shadows, terrain DEM elevations, and pitch/bearing camera controls.
Square/Stripe-style interactive invoice builder with itemized tax, discount calculation, dynamic line items, client presets, and a real-time paper document preview.
Also available for Vue ->$pnpm dlx shadcn@latest add https://uipkge.dev/r/react/invoice-creator-wizard.json$npx shadcn@latest add https://uipkge.dev/r/react/invoice-creator-wizard.json$yarn dlx shadcn@latest add https://uipkge.dev/r/react/invoice-creator-wizard.json$bunx shadcn@latest add https://uipkge.dev/r/react/invoice-creator-wizard.jsonnpx shadcn@latest add @uipkge-react/invoice-creator-wizardInstalls to:components/blocks/| Name | Type / Values | Default | Required |
|---|---|---|---|
className | string | — | optional |
initialInvoiceNumber | string | — | optional |
initialIssuerName | string | — | optional |
initialIssuerEmail | string | — | optional |
initialIssuerAddress | string | — | optional |
initialIssuerTaxId | string | — | optional |
initialClientName | string | — | optional |
initialClientCompany | string | — | optional |
initialClientEmail | string | — | optional |
initialClientAddress | string | — | optional |
initialInvoiceDate | string | — | optional |
initialDueDate | string | — | optional |
initialPaymentTerms | string | — | optional |
initialCurrency | string | — | optional |
initialItems | InvoiceLineItem[] | — | optional |
initialTaxRate | number | — | optional |
initialDiscountType | 'fixed' | 'percent' | — | optional |
initialDiscountValue | number | — | optional |
initialNotes | string | — | optional |
onSaveDraft | (payload: Record<string, any>) => void | — | optional |
onSendInvoice | (payload: Record<string, any>) => void | — | optional |
onPreviewPdf | () => void | — | optional |
Type aliases exported from this item's source. Use these to shape the data you pass in.
InvoiceLineIteminterface InvoiceLineItem {
id: string
description: string
quantity: number
unitPrice: number
}ClientPresetinterface ClientPreset {
id: string
name: string
company: string
email: string
address: string
}'use client'
import * as React from 'react'
import {
Building2,
CheckCircle2,
CreditCard,
Eye,
FileText,
Percent,
Plus,
Receipt,
Save,
Send,
Trash2,
} from 'lucide-react'
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 { Input } from '@/components/ui/input'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Separator } from '@/components/ui/separator'
import { Textarea } from '@/components/ui/textarea'
export interface InvoiceLineItem {
id: string
description: string
quantity: number
unitPrice: number
}
export interface ClientPreset {
id: string
name: string
company: string
email: string
address: string
}
export interface InvoiceCreatorWizardProps {
className?: string
initialInvoiceNumber?: string
initialIssuerName?: string
initialIssuerEmail?: string
initialIssuerAddress?: string
initialIssuerTaxId?: string
initialClientName?: string
initialClientCompany?: string
initialClientEmail?: string
initialClientAddress?: string
initialInvoiceDate?: string
initialDueDate?: string
initialPaymentTerms?: string
initialCurrency?: string
initialItems?: InvoiceLineItem[]
initialTaxRate?: number
initialDiscountType?: 'fixed' | 'percent'
initialDiscountValue?: number
initialNotes?: string
onSaveDraft?: (payload: Record<string, any>) => void
onSendInvoice?: (payload: Record<string, any>) => void
onPreviewPdf?: () => void
}
const CLIENT_PRESETS: ClientPreset[] = [
{
id: 'c1',
name: 'Sarah Jenkins',
company: 'Apex Digital Inc.',
email: '[email protected]',
address: '742 Evergreen Terrace, Springfield, OR 97477',
},
{
id: 'c2',
name: 'Marcus Vance',
company: 'Northwind Global Corp.',
email: '[email protected]',
address: '452 Broadway, 8th Floor, New York, NY 10013',
},
{
id: 'c3',
name: 'Elena Rostova',
company: 'Hyperion Robotics',
email: '[email protected]',
address: '10 Tech Parkway, Suite 300, Boston, MA 02115',
},
]
const DEFAULT_LINE_ITEMS: InvoiceLineItem[] = [
{
id: 'item-1',
description: 'Design System Architecture & Component Registry',
quantity: 1,
unitPrice: 3200,
},
{
id: 'item-2',
description: 'Frontend Implementation (Vue 3 + React Mirror)',
quantity: 35,
unitPrice: 120,
},
{
id: 'item-3',
description: 'Accessibility & WCAG AA Compliance Audit',
quantity: 1,
unitPrice: 850,
},
]
const PAYMENT_TERMS_MAP: Record<string, string> = {
receipt: 'Due on Receipt',
net15: 'Net 15 (Due in 15 days)',
net30: 'Net 30 (Due in 30 days)',
net60: 'Net 60 (Due in 60 days)',
}
export function InvoiceCreatorWizard({
className,
initialInvoiceNumber = 'INV-2026-0042',
initialIssuerName = 'Acme Design & Engineering Studio',
initialIssuerEmail = '[email protected]',
initialIssuerAddress = '100 Montgomery St, Suite 1400, San Francisco, CA 94104',
initialIssuerTaxId = 'US-EIN-94-2819034',
initialClientName = 'Sarah Jenkins',
initialClientCompany = 'Apex Digital Inc.',
initialClientEmail = '[email protected]',
initialClientAddress = '742 Evergreen Terrace, Springfield, OR 97477',
initialInvoiceDate = '2026-08-21',
initialDueDate = '2026-09-20',
initialPaymentTerms = 'net30',
initialCurrency = 'USD',
initialItems,
initialTaxRate = 8.5,
initialDiscountType = 'fixed',
initialDiscountValue = 150,
initialNotes = 'Thank you for your partnership! Please remit payment via wire transfer or online payment link within 30 days. Contact [email protected] for any questions.',
onSaveDraft,
onSendInvoice,
onPreviewPdf,
}: InvoiceCreatorWizardProps) {
// Form State
const [invoiceNumber] = React.useState(initialInvoiceNumber)
const [issuerName, setIssuerName] = React.useState(initialIssuerName)
const [issuerEmail, setIssuerEmail] = React.useState(initialIssuerEmail)
const [issuerAddress, setIssuerAddress] = React.useState(initialIssuerAddress)
const [issuerTaxId, setIssuerTaxId] = React.useState(initialIssuerTaxId)
const [selectedPreset, setSelectedPreset] = React.useState('c1')
const [clientName, setClientName] = React.useState(initialClientName)
const [clientCompany, setClientCompany] = React.useState(initialClientCompany)
const [clientEmail, setClientEmail] = React.useState(initialClientEmail)
const [clientAddress, setClientAddress] = React.useState(initialClientAddress)
const [invoiceDate, setInvoiceDate] = React.useState(initialInvoiceDate)
const [dueDate, setDueDate] = React.useState(initialDueDate)
const [paymentTerms, setPaymentTerms] = React.useState(initialPaymentTerms)
const [currency, setCurrency] = React.useState(initialCurrency)
const [items, setItems] = React.useState<InvoiceLineItem[]>(
initialItems && initialItems.length > 0
? initialItems.map((i) => ({ ...i }))
: DEFAULT_LINE_ITEMS.map((i) => ({ ...i })),
)
const [taxRate, setTaxRate] = React.useState(initialTaxRate)
const [discountType, setDiscountType] = React.useState<'fixed' | 'percent'>(initialDiscountType)
const [discountValue, setDiscountValue] = React.useState(initialDiscountValue)
const [notes, setNotes] = React.useState(initialNotes)
// Notification toast state
const [notification, setNotification] = React.useState<{ type: 'success' | 'info'; message: string } | null>(null)
const timeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null)
const showNotification = React.useCallback((message: string, type: 'success' | 'info' = 'success') => {
if (timeoutRef.current) clearTimeout(timeoutRef.current)
setNotification({ message, type })
timeoutRef.current = setTimeout(() => {
setNotification(null)
}, 4000)
}, [])
const handlePresetChange = (presetId: string) => {
setSelectedPreset(presetId)
if (presetId === 'custom') return
const preset = CLIENT_PRESETS.find((c) => c.id === presetId)
if (preset) {
setClientName(preset.name)
setClientCompany(preset.company)
setClientEmail(preset.email)
setClientAddress(preset.address)
}
}
const addLineItem = () => {
const newId = `item-${Date.now()}`
setItems((prev) => [
...prev,
{
id: newId,
description: '',
quantity: 1,
unitPrice: 0,
},
])
}
const removeLineItem = (id: string) => {
if (items.length > 1) {
setItems((prev) => prev.filter((i) => i.id !== id))
}
}
const updateLineItem = (id: string, field: keyof InvoiceLineItem, value: any) => {
setItems((prev) => prev.map((item) => (item.id === id ? { ...item, [field]: value } : item)))
}
// Calculations
const subtotal = React.useMemo(() => {
return items.reduce((acc, item) => {
const qty = Number(item.quantity) || 0
const price = Number(item.unitPrice) || 0
return acc + qty * price
}, 0)
}, [items])
const discountAmount = React.useMemo(() => {
const val = Number(discountValue) || 0
if (discountType === 'percent') {
return subtotal * (val / 100)
}
return val
}, [discountValue, discountType, subtotal])
const effectiveDiscount = React.useMemo(() => {
return Math.min(subtotal, Math.max(0, discountAmount))
}, [subtotal, discountAmount])
const taxableAmount = React.useMemo(() => {
return Math.max(0, subtotal - effectiveDiscount)
}, [subtotal, effectiveDiscount])
const taxAmount = React.useMemo(() => {
const rate = Number(taxRate) || 0
return taxableAmount * (rate / 100)
}, [taxableAmount, taxRate])
const totalDue = React.useMemo(() => {
return taxableAmount + taxAmount
}, [taxableAmount, taxAmount])
const formatCurrency = React.useCallback(
(amount: number, curr: string = currency) => {
try {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: curr,
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(amount)
} catch {
const symbols: Record<string, string> = { USD: '$', EUR: '€', GBP: '£', CAD: 'CA$', AUD: 'AU$' }
const s = symbols[curr] || '$'
return `${s}${amount.toFixed(2)}`
}
},
[currency],
)
const formatDate = React.useCallback((dateStr: string) => {
if (!dateStr) return '—'
try {
const [y, m, d] = dateStr.split('-').map(Number)
if (!y || !m || !d) return dateStr
const date = new Date(y, m - 1, d)
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
} catch {
return dateStr
}
}, [])
const getPayload = React.useCallback(() => {
return {
invoiceNumber,
issuer: {
name: issuerName,
email: issuerEmail,
address: issuerAddress,
taxId: issuerTaxId,
},
client: {
name: clientName,
company: clientCompany,
email: clientEmail,
address: clientAddress,
},
dates: {
issued: invoiceDate,
due: dueDate,
terms: paymentTerms,
},
currency,
items,
calculations: {
subtotal,
discount: effectiveDiscount,
tax: taxAmount,
total: totalDue,
},
notes,
}
}, [
invoiceNumber,
issuerName,
issuerEmail,
issuerAddress,
issuerTaxId,
clientName,
clientCompany,
clientEmail,
clientAddress,
invoiceDate,
dueDate,
paymentTerms,
currency,
items,
subtotal,
effectiveDiscount,
taxAmount,
totalDue,
notes,
])
const handleSaveDraft = () => {
const payload = getPayload()
onSaveDraft?.(payload)
showNotification(`Draft invoice ${invoiceNumber} saved successfully.`, 'info')
}
const handleSendInvoice = () => {
const payload = getPayload()
onSendInvoice?.(payload)
showNotification(`Invoice ${invoiceNumber} sent to ${clientEmail || 'client'}.`, 'success')
}
const handlePreviewPdf = () => {
onPreviewPdf?.()
showNotification(`Generating printable PDF preview for ${invoiceNumber}...`, 'info')
}
return (
<div data-slot="invoice-creator-wizard" className={cn('mx-auto w-full max-w-7xl space-y-6', className)}>
{/* Top Action Header */}
<div className="border-border flex flex-col gap-4 border-b pb-5 sm:flex-row sm:items-center sm:justify-between">
<div className="space-y-1">
<div className="flex items-center gap-3">
<h1 className="text-foreground text-xl font-bold tracking-tight sm:text-2xl">Create New Invoice</h1>
<Badge wrap variant="outline" className="px-2 py-0.5 font-mono text-xs font-semibold">
{invoiceNumber}
</Badge>
<Badge wrap variant="secondary" className="text-xs">
Draft
</Badge>
</div>
<p className="text-muted-foreground text-xs sm:text-sm">
Build itemized invoices with live calculation, instant document preview, and client presets.
</p>
</div>
{/* Action Buttons */}
<div className="flex flex-wrap items-center gap-2">
<Button variant="outline" size="sm" className="gap-1.5 text-xs" onClick={handlePreviewPdf}>
<FileText className="size-3.5" aria-hidden="true" />
Preview PDF
</Button>
<Button variant="outline" size="sm" className="gap-1.5 text-xs" onClick={handleSaveDraft}>
<Save className="size-3.5" aria-hidden="true" />
Save Draft
</Button>
<Button size="sm" className="gap-1.5 text-xs font-medium" onClick={handleSendInvoice}>
<Send className="size-3.5" aria-hidden="true" />
Send Invoice
</Button>
</div>
</div>
{/* Notification Toast / Alert Banner */}
{notification && (
<div
className={cn(
'flex flex-wrap items-center justify-between rounded-lg border px-4 py-2.5 text-xs transition-colors sm:text-sm',
notification.type === 'success'
? 'border-success/30 bg-success/10 text-success'
: 'border-primary/30 bg-primary/10 text-primary',
)}
role="status"
>
<div className="flex flex-wrap items-center gap-2">
<CheckCircle2 className="size-4 shrink-0" aria-hidden="true" />
<span>{notification.message}</span>
</div>
<button
type="button"
className="min-h-6 text-xs font-semibold underline-offset-2 opacity-80 hover:underline hover:opacity-100"
onClick={() => setNotification(null)}
>
Dismiss
</button>
</div>
)}
{/* 2-Column Invoice Builder Layout */}
<div className="grid grid-cols-1 items-start gap-8 lg:grid-cols-12">
{/* Left Column: Form Builder */}
<div className="space-y-6 lg:col-span-7">
{/* Section 1: Business & Client Details */}
<Card>
<CardHeader className="pb-4">
<div className="flex flex-wrap items-center gap-2">
<Building2 className="text-primary size-4" aria-hidden="true" />
<CardTitle className="text-base font-semibold">Business & Client Details</CardTitle>
</div>
<CardDescription className="text-xs">
Configure your issuing entity information and bill-to client contact.
</CardDescription>
</CardHeader>
<CardContent className="space-y-5">
{/* Issuer Details */}
<div className="space-y-3">
<div className="flex flex-wrap items-center justify-between">
<span className="text-muted-foreground text-xs font-semibold tracking-wider uppercase">
From (Issuer)
</span>
<span className="text-muted-foreground font-mono text-xs">ID: {invoiceNumber}</span>
</div>
<div className="grid gap-3 sm:grid-cols-2">
<div className="space-y-1.5 sm:col-span-2">
<label htmlFor="issuer-name" className="text-foreground text-xs font-medium">
Business / Organization Name
</label>
<Input
id="issuer-name"
value={issuerName}
onChange={(e) => setIssuerName(e.target.value)}
placeholder="Business name"
size="small"
/>
</div>
<div className="space-y-1.5">
<label htmlFor="issuer-email" className="text-foreground text-xs font-medium">
Billing Email
</label>
<Input
id="issuer-email"
type="email"
value={issuerEmail}
onChange={(e) => setIssuerEmail(e.target.value)}
placeholder="[email protected]"
size="small"
/>
</div>
<div className="space-y-1.5">
<label htmlFor="issuer-tax" className="text-foreground text-xs font-medium">
Tax ID / VAT Registration
</label>
<Input
id="issuer-tax"
value={issuerTaxId}
onChange={(e) => setIssuerTaxId(e.target.value)}
placeholder="Tax ID / EIN"
size="small"
/>
</div>
<div className="space-y-1.5 sm:col-span-2">
<label htmlFor="issuer-address" className="text-foreground text-xs font-medium">
Business Address
</label>
<Input
id="issuer-address"
value={issuerAddress}
onChange={(e) => setIssuerAddress(e.target.value)}
placeholder="Street, City, State, ZIP"
size="small"
/>
</div>
</div>
</div>
<Separator />
{/* Client Details */}
<div className="space-y-3">
<div className="flex flex-wrap items-center justify-between">
<span className="text-muted-foreground text-xs font-semibold tracking-wider uppercase">
Bill To (Client)
</span>
<div className="flex flex-wrap items-center gap-2">
<span className="text-muted-foreground text-xs">Quick Preset:</span>
<div className="w-48">
<Select value={selectedPreset} onValueChange={handlePresetChange}>
<SelectTrigger size="sm" className="h-7 text-xs">
<SelectValue placeholder="Select client" />
</SelectTrigger>
<SelectContent>
{CLIENT_PRESETS.map((preset) => (
<SelectItem key={preset.id} value={preset.id}>
{preset.company}
</SelectItem>
))}
<SelectItem value="custom">Custom Client</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
<div className="grid gap-3 sm:grid-cols-2">
<div className="space-y-1.5">
<label htmlFor="client-name" className="text-foreground text-xs font-medium">
Contact Name
</label>
<Input
id="client-name"
value={clientName}
onChange={(e) => setClientName(e.target.value)}
placeholder="Client contact name"
size="small"
/>
</div>
<div className="space-y-1.5">
<label htmlFor="client-company" className="text-foreground text-xs font-medium">
Company Name
</label>
<Input
id="client-company"
value={clientCompany}
onChange={(e) => setClientCompany(e.target.value)}
placeholder="Client company name"
size="small"
/>
</div>
<div className="space-y-1.5">
<label htmlFor="client-email" className="text-foreground text-xs font-medium">
Client Email
</label>
<Input
id="client-email"
type="email"
value={clientEmail}
onChange={(e) => setClientEmail(e.target.value)}
placeholder="[email protected]"
size="small"
/>
</div>
<div className="space-y-1.5">
<label htmlFor="client-address" className="text-foreground text-xs font-medium">
Client Address
</label>
<Input
id="client-address"
value={clientAddress}
onChange={(e) => setClientAddress(e.target.value)}
placeholder="Street, City, Postal Code"
size="small"
/>
</div>
</div>
</div>
<Separator />
{/* Invoice Timing & Currency */}
<div className="grid gap-3 sm:grid-cols-4">
<div className="space-y-1.5">
<label htmlFor="invoice-date" className="text-foreground text-xs font-medium">
Invoice Date
</label>
<Input
id="invoice-date"
type="date"
value={invoiceDate}
onChange={(e) => setInvoiceDate(e.target.value)}
size="small"
/>
</div>
<div className="space-y-1.5">
<label htmlFor="due-date" className="text-foreground text-xs font-medium">
Due Date
</label>
<Input
id="due-date"
type="date"
value={dueDate}
onChange={(e) => setDueDate(e.target.value)}
size="small"
/>
</div>
<div className="space-y-1.5">
<label htmlFor="payment-terms" className="text-foreground text-xs font-medium">
Payment Terms
</label>
<Select value={paymentTerms} onValueChange={setPaymentTerms}>
<SelectTrigger id="payment-terms" size="sm" className="h-8 text-xs">
<SelectValue placeholder="Terms" />
</SelectTrigger>
<SelectContent>
<SelectItem value="receipt">Due on Receipt</SelectItem>
<SelectItem value="net15">Net 15</SelectItem>
<SelectItem value="net30">Net 30</SelectItem>
<SelectItem value="net60">Net 60</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-1.5">
<label htmlFor="currency-select" className="text-foreground text-xs font-medium">
Currency
</label>
<Select value={currency} onValueChange={setCurrency}>
<SelectTrigger id="currency-select" size="sm" className="h-8 text-xs">
<SelectValue placeholder="Currency" />
</SelectTrigger>
<SelectContent>
<SelectItem value="USD">USD ($)</SelectItem>
<SelectItem value="EUR">EUR (€)</SelectItem>
<SelectItem value="GBP">GBP (£)</SelectItem>
<SelectItem value="CAD">CAD ($)</SelectItem>
<SelectItem value="AUD">AUD ($)</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</CardContent>
</Card>
{/* Section 2: Interactive Line Items */}
<Card>
<CardHeader className="pb-3">
<div className="flex flex-wrap items-center justify-between">
<div className="flex flex-wrap items-center gap-2">
<Receipt className="text-primary size-4" aria-hidden="true" />
<CardTitle className="text-base font-semibold">Line Items</CardTitle>
</div>
<Badge wrap variant="secondary" className="font-mono text-xs tabular-nums">
{items.length} {items.length === 1 ? 'item' : 'items'}
</Badge>
</div>
<CardDescription className="text-xs">
Itemize deliverables, billable hours, software licenses, or custom services.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="overflow-x-auto">
<table className="w-full text-left text-xs">
<thead>
<tr className="border-border text-muted-foreground border-b">
<th className="pb-2 font-medium">Description</th>
<th className="w-20 pb-2 text-right font-medium">Qty</th>
<th className="w-28 pb-2 text-right font-medium">Unit Price</th>
<th className="w-24 pb-2 text-right font-medium">Line Total</th>
<th className="w-10 pb-2 text-center font-medium"></th>
</tr>
</thead>
<tbody className="divide-border/60 divide-y">
{items.map((item) => (
<tr key={item.id} className="group">
<td className="py-2.5 pr-2">
<Input
value={item.description}
onChange={(e) => updateLineItem(item.id, 'description', e.target.value)}
placeholder="Service or product description..."
size="small"
className="w-full"
/>
</td>
<td className="px-2 py-2.5">
<Input
type="number"
min="1"
step="1"
value={item.quantity}
onChange={(e) => updateLineItem(item.id, 'quantity', Number(e.target.value))}
size="small"
className="w-20 text-right tabular-nums"
/>
</td>
<td className="px-2 py-2.5">
<Input
type="number"
min="0"
step="0.01"
value={item.unitPrice}
onChange={(e) => updateLineItem(item.id, 'unitPrice', Number(e.target.value))}
size="small"
className="w-28 text-right tabular-nums"
/>
</td>
<td className="text-foreground py-2.5 pl-2 text-right font-medium whitespace-nowrap tabular-nums">
{formatCurrency((Number(item.quantity) || 0) * (Number(item.unitPrice) || 0))}
</td>
<td className="py-2.5 pl-1 text-center">
<Button
type="button"
variant="ghost"
size="icon"
className="text-muted-foreground hover:text-destructive size-7 disabled:opacity-30"
disabled={items.length <= 1}
aria-label="Remove item"
onClick={() => removeLineItem(item.id)}
>
<Trash2 className="size-3.5" aria-hidden="true" />
</Button>
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="flex flex-wrap items-center justify-between pt-2">
<Button type="button" variant="outline" size="sm" className="gap-1 text-xs" onClick={addLineItem}>
<Plus className="size-3.5" aria-hidden="true" />
Add Line Item
</Button>
<div className="text-muted-foreground text-xs">
Subtotal:{' '}
<span className="text-foreground font-semibold tabular-nums">{formatCurrency(subtotal)}</span>
</div>
</div>
</CardContent>
</Card>
{/* Section 3: Adjustments & Payment Terms */}
<Card>
<CardHeader className="pb-3">
<div className="flex flex-wrap items-center gap-2">
<Percent className="text-primary size-4" aria-hidden="true" />
<CardTitle className="text-base font-semibold">Adjustments & Notes</CardTitle>
</div>
<CardDescription className="text-xs">
Itemize tax rates, discount subtractions, and client payment instructions.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid gap-4 sm:grid-cols-2">
{/* Tax percentage */}
<div className="space-y-1.5">
<div className="flex flex-wrap items-center justify-between">
<label htmlFor="tax-rate" className="text-foreground text-xs font-medium">
Tax Rate (%)
</label>
<span className="text-muted-foreground text-xs tabular-nums">+{formatCurrency(taxAmount)}</span>
</div>
<Input
id="tax-rate"
type="number"
min="0"
step="0.1"
value={taxRate}
onChange={(e) => setTaxRate(Number(e.target.value))}
placeholder="8.5"
size="small"
/>
</div>
{/* Discount */}
<div className="space-y-1.5">
<div className="flex flex-wrap items-center justify-between">
<label htmlFor="discount-val" className="text-foreground text-xs font-medium">
Discount
</label>
<div className="flex items-center gap-1">
<button
type="button"
className={cn(
'min-h-6 rounded px-1.5 py-0.5 text-xs font-medium transition-colors',
discountType === 'fixed'
? 'bg-primary text-primary-foreground'
: 'bg-muted text-muted-foreground hover:text-foreground',
)}
onClick={() => setDiscountType('fixed')}
>
Fixed ($)
</button>
<button
type="button"
className={cn(
'min-h-6 rounded px-1.5 py-0.5 text-xs font-medium transition-colors',
discountType === 'percent'
? 'bg-primary text-primary-foreground'
: 'bg-muted text-muted-foreground hover:text-foreground',
)}
onClick={() => setDiscountType('percent')}
>
Percent (%)
</button>
</div>
</div>
<div className="flex flex-wrap items-center gap-2">
<Input
id="discount-val"
type="number"
min="0"
step="1"
value={discountValue}
onChange={(e) => setDiscountValue(Number(e.target.value))}
placeholder="0"
size="small"
className="w-full"
/>
<span className="text-success text-success text-xs font-medium whitespace-nowrap tabular-nums">
-{formatCurrency(effectiveDiscount)}
</span>
</div>
</div>
</div>
{/* Notes / Terms */}
<div className="space-y-1.5">
<label htmlFor="invoice-notes" className="text-foreground text-xs font-medium">
Notes & Terms for Client
</label>
<Textarea
id="invoice-notes"
value={notes}
onValueChange={setNotes}
rows={3}
placeholder="Payment instructions, bank wire info, or a personal thank you note..."
className="text-xs"
/>
</div>
</CardContent>
</Card>
</div>
{/* Right Column: Live Paper Document Preview */}
<div className="space-y-4 lg:sticky lg:top-6 lg:col-span-5">
<div className="flex flex-wrap items-center justify-between px-1">
<div className="flex flex-wrap items-center gap-2">
<span className="relative flex size-2">
<span className="bg-success absolute inline-flex h-full w-full rounded-full opacity-75"></span>
<span className="bg-success relative inline-flex size-2 rounded-full"></span>
</span>
<span className="text-muted-foreground flex items-center gap-1 text-xs font-medium">
<Eye className="size-3.5" aria-hidden="true" />
Live Document Preview
</span>
</div>
<span className="text-muted-foreground font-mono text-xs">Printable A4</span>
</div>
{/* Paper Invoice Card */}
<div className="bg-card text-card-foreground border-border/80 relative space-y-6 overflow-hidden rounded-xl border p-6 shadow-md transition-colors sm:p-7">
{/* Document Header */}
<div className="flex items-start justify-between gap-4">
<div className="space-y-1">
<div className="flex flex-wrap items-center gap-2">
<div className="bg-primary/10 text-primary border-primary/20 flex size-8 items-center justify-center rounded border text-xs font-bold">
AS
</div>
<div className="text-foreground text-sm leading-tight font-bold sm:text-base">
{issuerName || 'Business Name'}
</div>
</div>
<p className="text-muted-foreground max-w-[200px] text-xs leading-relaxed">
{issuerAddress || 'Address not set'}
</p>
<p className="text-muted-foreground font-mono text-xs">{issuerEmail || '[email protected]'}</p>
{issuerTaxId && <p className="text-muted-foreground/80 font-mono text-xs">Tax: {issuerTaxId}</p>}
</div>
<div className="shrink-0 space-y-1 text-right">
<span className="text-muted-foreground text-xs font-bold tracking-wider uppercase">Invoice</span>
<div className="text-foreground font-mono text-xs font-bold sm:text-sm">{invoiceNumber}</div>
<Badge
wrap
variant="outline"
className="border-success/30 bg-success/10 text-success text-xs font-medium"
>
Ready to Send
</Badge>
</div>
</div>
<Separator className="opacity-60" />
{/* Invoice Details: Bill To & Dates */}
<div className="grid grid-cols-2 gap-4 text-xs">
<div className="space-y-1">
<span className="text-muted-foreground text-xs font-semibold tracking-wider uppercase">Billed To</span>
<p className="text-foreground font-semibold">{clientName || 'Client Name'}</p>
{clientCompany && <p className="text-muted-foreground font-medium">{clientCompany}</p>}
<p className="text-muted-foreground leading-relaxed">{clientAddress || 'No address provided'}</p>
<p className="text-muted-foreground font-mono">{clientEmail || '[email protected]'}</p>
</div>
<div className="space-y-1 text-right">
<span className="text-muted-foreground text-xs font-semibold tracking-wider uppercase">
Invoice Details
</span>
<div className="space-y-0.5">
<div className="text-muted-foreground flex justify-between gap-2 sm:justify-end">
<span>Issued:</span>
<span className="text-foreground font-medium tabular-nums">{formatDate(invoiceDate)}</span>
</div>
<div className="text-muted-foreground flex justify-between gap-2 sm:justify-end">
<span>Due:</span>
<span className="text-foreground font-medium tabular-nums">{formatDate(dueDate)}</span>
</div>
<div className="text-muted-foreground flex justify-between gap-2 sm:justify-end">
<span>Terms:</span>
<span className="text-foreground font-medium">
{PAYMENT_TERMS_MAP[paymentTerms] || paymentTerms}
</span>
</div>
</div>
</div>
</div>
<Separator className="opacity-60" />
{/* Line Items Table */}
<div className="space-y-2">
<div className="overflow-x-auto">
<table className="w-full text-xs">
<thead>
<tr className="border-border/80 text-muted-foreground border-b font-medium">
<th className="pb-2 text-left">Item & Description</th>
<th className="w-12 pb-2 text-center">Qty</th>
<th className="w-16 pb-2 text-right">Rate</th>
<th className="w-20 pb-2 text-right">Amount</th>
</tr>
</thead>
<tbody className="divide-border/40 divide-y">
{items.map((item) => (
<tr key={item.id} className="text-foreground">
<td className="py-2 pr-2">
<p className="leading-snug font-medium">{item.description || 'Custom Deliverable'}</p>
</td>
<td className="text-muted-foreground px-1 py-2 text-center tabular-nums">{item.quantity}</td>
<td className="text-muted-foreground px-1 py-2 text-right tabular-nums">
{formatCurrency(Number(item.unitPrice) || 0)}
</td>
<td className="py-2 pl-2 text-right font-medium tabular-nums">
{formatCurrency((Number(item.quantity) || 0) * (Number(item.unitPrice) || 0))}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
<Separator className="opacity-60" />
{/* Calculations Summary */}
<div className="space-y-2 text-xs">
<div className="text-muted-foreground flex justify-between">
<span>Subtotal</span>
<span className="text-foreground font-medium tabular-nums">{formatCurrency(subtotal)}</span>
</div>
{effectiveDiscount > 0 && (
<div className="text-success flex justify-between font-medium">
<span>Discount {discountType === 'percent' ? `(${discountValue}%)` : ''}</span>
<span className="tabular-nums">-{formatCurrency(effectiveDiscount)}</span>
</div>
)}
{taxRate > 0 && (
<div className="text-muted-foreground flex justify-between">
<span>Tax ({taxRate}%)</span>
<span className="text-foreground font-medium tabular-nums">+{formatCurrency(taxAmount)}</span>
</div>
)}
<Separator className="my-1.5 opacity-80" />
<div className="flex flex-wrap items-baseline justify-between gap-x-2 gap-y-0.5 pt-1">
<span className="text-foreground text-sm font-bold">Total Due</span>
<span className="text-foreground text-lg font-bold tabular-nums sm:text-xl">
{formatCurrency(totalDue)}
</span>
</div>
</div>
{/* Notes / Payment Info Callout */}
<div className="bg-muted/50 border-border/70 space-y-2 rounded-lg border p-3.5 text-xs">
<div className="text-foreground flex items-center gap-1.5 font-medium">
<CreditCard className="text-primary size-3.5" aria-hidden="true" />
<span>Payment Instructions</span>
</div>
{notes && <p className="text-muted-foreground leading-relaxed">{notes}</p>}
<div className="border-border/50 text-muted-foreground flex flex-wrap items-center justify-between gap-2 border-t pt-1.5 font-mono text-xs">
<span>ACH / Wire: **** 9104</span>
<span>Routing: **** 4892</span>
</div>
</div>
</div>
</div>
</div>
</div>
)
}
Raw manifest:https://uipkge.dev/r/react/invoice-creator-wizard.json