# Invoice Templates Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** Replace the single hardcoded invoice layout with 15 visually distinct templates, a template picker with live preview, and business identity support. **Architecture:** Template Config Objects approach — 15 config objects define colors, layout, typography, and decorative elements. A shared jsPDF renderer reads the config to generate PDFs. A Vue component reads the same config to render HTML previews. The template picker is a split-pane UI with template list on the left and live preview on the right. **Tech Stack:** Vue 3 Composition API, jsPDF (already installed), Tailwind CSS v4, Pinia stores, Tauri v2 IPC --- ### Task 1: Create template config types and registry **Files:** - Create: `src/utils/invoiceTemplates.ts` **Context:** This is the foundation file. It defines the TypeScript interface for template configs and exports all 15 template config objects plus a registry. The existing `InvoiceItem` interface in `src/utils/invoicePdf.ts:9-15` will be reused. **Step 1: Create the template config types and all 15 templates** Create `src/utils/invoiceTemplates.ts` with: ```ts export interface InvoiceTemplateColors { primary: string secondary: string background: string headerBg: string headerText: string bodyText: string tableHeaderBg: string tableHeaderText: string tableRowAlt: string tableBorder: string totalHighlight: string } export interface InvoiceTemplateLayout { logoPosition: 'top-left' | 'top-center' | 'top-right' headerStyle: 'full-width' | 'split' | 'minimal' | 'sidebar' | 'gradient' | 'geometric' | 'centered' tableStyle: 'bordered' | 'striped' | 'borderless' | 'minimal-lines' | 'colored-sections' totalsPosition: 'right' | 'center' showDividers: boolean dividerStyle: 'thin' | 'double' | 'thick' | 'none' } export interface InvoiceTemplateTypography { titleSize: number titleWeight: 'bold' | 'normal' headerSize: number bodySize: number numberStyle: 'normal' | 'monospace-feel' } export interface InvoiceTemplateDecorative { cornerShape: 'none' | 'colored-block' | 'triangle' | 'diagonal' sidebarWidth: number sidebarColor: string useGradientHeader: boolean gradientFrom?: string gradientTo?: string backgroundTint: boolean backgroundTintColor?: string } export interface InvoiceTemplateConfig { id: string name: string category: 'essential' | 'creative' | 'warm' | 'premium' description: string colors: InvoiceTemplateColors layout: InvoiceTemplateLayout typography: InvoiceTemplateTypography decorative: InvoiceTemplateDecorative } // --- Template definitions --- // Define all 15 templates. Each has unique colors, layout, typography, decorative. const cleanMinimal: InvoiceTemplateConfig = { id: 'clean-minimal', name: 'Clean Minimal', category: 'essential', description: 'Swiss-inspired simplicity with a single accent line', colors: { primary: '#3B82F6', secondary: '#93C5FD', background: '#FFFFFF', headerBg: '#FFFFFF', headerText: '#111827', bodyText: '#374151', tableHeaderBg: '#F9FAFB', tableHeaderText: '#374151', tableRowAlt: '#F9FAFB', tableBorder: '#E5E7EB', totalHighlight: '#3B82F6', }, layout: { logoPosition: 'top-left', headerStyle: 'minimal', tableStyle: 'bordered', totalsPosition: 'right', showDividers: true, dividerStyle: 'thin', }, typography: { titleSize: 24, titleWeight: 'bold', headerSize: 11, bodySize: 9, numberStyle: 'normal' }, decorative: { cornerShape: 'none', sidebarWidth: 0, sidebarColor: '', useGradientHeader: false, backgroundTint: false }, } const corporateClassic: InvoiceTemplateConfig = { id: 'corporate-classic', name: 'Corporate Classic', category: 'essential', description: 'Navy header band with professional table layout', colors: { primary: '#1E3A5F', secondary: '#2563EB', background: '#FFFFFF', headerBg: '#1E3A5F', headerText: '#FFFFFF', bodyText: '#374151', tableHeaderBg: '#1E3A5F', tableHeaderText: '#FFFFFF', tableRowAlt: '#F3F4F6', tableBorder: '#D1D5DB', totalHighlight: '#1E3A5F', }, layout: { logoPosition: 'top-left', headerStyle: 'full-width', tableStyle: 'striped', totalsPosition: 'right', showDividers: false, dividerStyle: 'none', }, typography: { titleSize: 28, titleWeight: 'bold', headerSize: 11, bodySize: 9, numberStyle: 'normal' }, decorative: { cornerShape: 'none', sidebarWidth: 0, sidebarColor: '', useGradientHeader: false, backgroundTint: false }, } const modernBold: InvoiceTemplateConfig = { id: 'modern-bold', name: 'Modern Bold', category: 'essential', description: 'Large accent block with oversized typography', colors: { primary: '#6366F1', secondary: '#A5B4FC', background: '#FFFFFF', headerBg: '#6366F1', headerText: '#FFFFFF', bodyText: '#1F2937', tableHeaderBg: '#6366F1', tableHeaderText: '#FFFFFF', tableRowAlt: '#F5F3FF', tableBorder: '#E0E7FF', totalHighlight: '#6366F1', }, layout: { logoPosition: 'top-left', headerStyle: 'full-width', tableStyle: 'borderless', totalsPosition: 'right', showDividers: false, dividerStyle: 'none', }, typography: { titleSize: 32, titleWeight: 'bold', headerSize: 12, bodySize: 10, numberStyle: 'normal' }, decorative: { cornerShape: 'colored-block', sidebarWidth: 0, sidebarColor: '', useGradientHeader: false, backgroundTint: false }, } const elegantSerif: InvoiceTemplateConfig = { id: 'elegant-serif', name: 'Elegant Serif', category: 'essential', description: 'Refined charcoal and gold with centered layout', colors: { primary: '#374151', secondary: '#B8860B', background: '#FFFFFF', headerBg: '#FFFFFF', headerText: '#374151', bodyText: '#4B5563', tableHeaderBg: '#374151', tableHeaderText: '#FFFFFF', tableRowAlt: '#FAFAFA', tableBorder: '#D1D5DB', totalHighlight: '#B8860B', }, layout: { logoPosition: 'top-center', headerStyle: 'centered', tableStyle: 'minimal-lines', totalsPosition: 'right', showDividers: true, dividerStyle: 'double', }, typography: { titleSize: 26, titleWeight: 'normal', headerSize: 11, bodySize: 9, numberStyle: 'normal' }, decorative: { cornerShape: 'none', sidebarWidth: 0, sidebarColor: '', useGradientHeader: false, backgroundTint: false }, } const simpleTwoTone: InvoiceTemplateConfig = { id: 'simple-two-tone', name: 'Simple Two-Tone', category: 'essential', description: 'Dark/light split header with emerald accents', colors: { primary: '#1F2937', secondary: '#10B981', background: '#FFFFFF', headerBg: '#1F2937', headerText: '#FFFFFF', bodyText: '#374151', tableHeaderBg: '#F3F4F6', tableHeaderText: '#1F2937', tableRowAlt: '#FFFFFF', tableBorder: '#E5E7EB', totalHighlight: '#10B981', }, layout: { logoPosition: 'top-left', headerStyle: 'split', tableStyle: 'minimal-lines', totalsPosition: 'right', showDividers: true, dividerStyle: 'thin', }, typography: { titleSize: 24, titleWeight: 'bold', headerSize: 11, bodySize: 9, numberStyle: 'normal' }, decorative: { cornerShape: 'none', sidebarWidth: 0, sidebarColor: '', useGradientHeader: false, backgroundTint: false }, } const gradientHeader: InvoiceTemplateConfig = { id: 'gradient-header', name: 'Gradient Header', category: 'creative', description: 'Full-width gradient header with modern feel', colors: { primary: '#667EEA', secondary: '#764BA2', background: '#FFFFFF', headerBg: '#667EEA', headerText: '#FFFFFF', bodyText: '#374151', tableHeaderBg: '#667EEA', tableHeaderText: '#FFFFFF', tableRowAlt: '#F5F3FF', tableBorder: '#E0E7FF', totalHighlight: '#667EEA', }, layout: { logoPosition: 'top-left', headerStyle: 'gradient', tableStyle: 'striped', totalsPosition: 'right', showDividers: false, dividerStyle: 'none', }, typography: { titleSize: 28, titleWeight: 'bold', headerSize: 11, bodySize: 9, numberStyle: 'normal' }, decorative: { cornerShape: 'none', sidebarWidth: 0, sidebarColor: '', useGradientHeader: true, gradientFrom: '#667EEA', gradientTo: '#764BA2', backgroundTint: false }, } const sidebarAccent: InvoiceTemplateConfig = { id: 'sidebar-accent', name: 'Sidebar Accent', category: 'creative', description: 'Rose vertical bar with asymmetric layout', colors: { primary: '#E11D48', secondary: '#FB7185', background: '#FFFFFF', headerBg: '#FFFFFF', headerText: '#111827', bodyText: '#374151', tableHeaderBg: '#FFF1F2', tableHeaderText: '#E11D48', tableRowAlt: '#FFF1F2', tableBorder: '#FECDD3', totalHighlight: '#E11D48', }, layout: { logoPosition: 'top-left', headerStyle: 'sidebar', tableStyle: 'borderless', totalsPosition: 'right', showDividers: true, dividerStyle: 'thin', }, typography: { titleSize: 24, titleWeight: 'bold', headerSize: 11, bodySize: 9, numberStyle: 'normal' }, decorative: { cornerShape: 'none', sidebarWidth: 4, sidebarColor: '#E11D48', useGradientHeader: false, backgroundTint: false }, } const geometricModern: InvoiceTemplateConfig = { id: 'geometric-modern', name: 'Geometric Modern', category: 'creative', description: 'Violet triangle accent with diagonal elements', colors: { primary: '#8B5CF6', secondary: '#C4B5FD', background: '#FFFFFF', headerBg: '#FFFFFF', headerText: '#1F2937', bodyText: '#374151', tableHeaderBg: '#8B5CF6', tableHeaderText: '#FFFFFF', tableRowAlt: '#F5F3FF', tableBorder: '#DDD6FE', totalHighlight: '#8B5CF6', }, layout: { logoPosition: 'top-left', headerStyle: 'geometric', tableStyle: 'striped', totalsPosition: 'right', showDividers: false, dividerStyle: 'none', }, typography: { titleSize: 26, titleWeight: 'bold', headerSize: 11, bodySize: 9, numberStyle: 'normal' }, decorative: { cornerShape: 'triangle', sidebarWidth: 0, sidebarColor: '', useGradientHeader: false, backgroundTint: false }, } const darkMode: InvoiceTemplateConfig = { id: 'dark-mode', name: 'Dark Mode', category: 'creative', description: 'Near-black background with cyan highlights', colors: { primary: '#06B6D4', secondary: '#22D3EE', background: '#1A1A2E', headerBg: '#1A1A2E', headerText: '#E2E8F0', bodyText: '#CBD5E1', tableHeaderBg: '#0F172A', tableHeaderText: '#06B6D4', tableRowAlt: '#16213E', tableBorder: '#334155', totalHighlight: '#06B6D4', }, layout: { logoPosition: 'top-left', headerStyle: 'minimal', tableStyle: 'striped', totalsPosition: 'right', showDividers: true, dividerStyle: 'thin', }, typography: { titleSize: 26, titleWeight: 'bold', headerSize: 11, bodySize: 9, numberStyle: 'monospace-feel' }, decorative: { cornerShape: 'none', sidebarWidth: 0, sidebarColor: '', useGradientHeader: false, backgroundTint: false }, } const warmNatural: InvoiceTemplateConfig = { id: 'warm-natural', name: 'Warm Natural', category: 'warm', description: 'Earth tones on warm beige background', colors: { primary: '#C2703E', secondary: '#6B7A3D', background: '#FDF6EC', headerBg: '#FDF6EC', headerText: '#78350F', bodyText: '#57534E', tableHeaderBg: '#C2703E', tableHeaderText: '#FFFFFF', tableRowAlt: '#FEF3C7', tableBorder: '#D6D3D1', totalHighlight: '#C2703E', }, layout: { logoPosition: 'top-left', headerStyle: 'minimal', tableStyle: 'striped', totalsPosition: 'right', showDividers: true, dividerStyle: 'thin', }, typography: { titleSize: 26, titleWeight: 'bold', headerSize: 11, bodySize: 9, numberStyle: 'normal' }, decorative: { cornerShape: 'none', sidebarWidth: 0, sidebarColor: '', useGradientHeader: false, backgroundTint: true, backgroundTintColor: '#FDF6EC' }, } const playfulColorBlock: InvoiceTemplateConfig = { id: 'playful-color-block', name: 'Playful Color Block', category: 'warm', description: 'Teal and coral blocks for a fun professional feel', colors: { primary: '#0D9488', secondary: '#F97316', background: '#FFFFFF', headerBg: '#0D9488', headerText: '#FFFFFF', bodyText: '#1F2937', tableHeaderBg: '#F97316', tableHeaderText: '#FFFFFF', tableRowAlt: '#FFF7ED', tableBorder: '#FDBA74', totalHighlight: '#0D9488', }, layout: { logoPosition: 'top-left', headerStyle: 'full-width', tableStyle: 'colored-sections', totalsPosition: 'right', showDividers: false, dividerStyle: 'none', }, typography: { titleSize: 28, titleWeight: 'bold', headerSize: 11, bodySize: 9, numberStyle: 'normal' }, decorative: { cornerShape: 'colored-block', sidebarWidth: 0, sidebarColor: '', useGradientHeader: false, backgroundTint: false }, } const retroProfessional: InvoiceTemplateConfig = { id: 'retro-professional', name: 'Retro Professional', category: 'warm', description: 'Vintage double-rule lines with warm brown palette', colors: { primary: '#78350F', secondary: '#92400E', background: '#FFFBEB', headerBg: '#FFFBEB', headerText: '#78350F', bodyText: '#57534E', tableHeaderBg: '#78350F', tableHeaderText: '#FFFFFF', tableRowAlt: '#FEF3C7', tableBorder: '#92400E', totalHighlight: '#78350F', }, layout: { logoPosition: 'top-left', headerStyle: 'minimal', tableStyle: 'bordered', totalsPosition: 'right', showDividers: true, dividerStyle: 'double', }, typography: { titleSize: 24, titleWeight: 'bold', headerSize: 11, bodySize: 9, numberStyle: 'normal' }, decorative: { cornerShape: 'none', sidebarWidth: 0, sidebarColor: '', useGradientHeader: false, backgroundTint: true, backgroundTintColor: '#FFFBEB' }, } const techMinimal: InvoiceTemplateConfig = { id: 'tech-minimal', name: 'Tech Minimal', category: 'premium', description: 'Slate and electric green with code-editor aesthetic', colors: { primary: '#475569', secondary: '#22C55E', background: '#FFFFFF', headerBg: '#FFFFFF', headerText: '#0F172A', bodyText: '#475569', tableHeaderBg: '#0F172A', tableHeaderText: '#22C55E', tableRowAlt: '#F8FAFC', tableBorder: '#E2E8F0', totalHighlight: '#22C55E', }, layout: { logoPosition: 'top-left', headerStyle: 'minimal', tableStyle: 'minimal-lines', totalsPosition: 'right', showDividers: true, dividerStyle: 'thin', }, typography: { titleSize: 22, titleWeight: 'bold', headerSize: 10, bodySize: 9, numberStyle: 'monospace-feel' }, decorative: { cornerShape: 'none', sidebarWidth: 0, sidebarColor: '', useGradientHeader: false, backgroundTint: false }, } const executivePremium: InvoiceTemplateConfig = { id: 'executive-premium', name: 'Executive Premium', category: 'premium', description: 'Black and gold luxury with generous spacing', colors: { primary: '#111827', secondary: '#D4AF37', background: '#FFFFFF', headerBg: '#111827', headerText: '#D4AF37', bodyText: '#374151', tableHeaderBg: '#111827', tableHeaderText: '#D4AF37', tableRowAlt: '#F9FAFB', tableBorder: '#D1D5DB', totalHighlight: '#D4AF37', }, layout: { logoPosition: 'top-left', headerStyle: 'full-width', tableStyle: 'minimal-lines', totalsPosition: 'right', showDividers: true, dividerStyle: 'thin', }, typography: { titleSize: 28, titleWeight: 'normal', headerSize: 11, bodySize: 10, numberStyle: 'normal' }, decorative: { cornerShape: 'none', sidebarWidth: 0, sidebarColor: '', useGradientHeader: false, backgroundTint: false }, } const dataDrivenClean: InvoiceTemplateConfig = { id: 'data-driven-clean', name: 'Data-Driven Clean', category: 'premium', description: 'Deep blue with emphasis on data hierarchy', colors: { primary: '#1E40AF', secondary: '#3B82F6', background: '#FFFFFF', headerBg: '#1E40AF', headerText: '#FFFFFF', bodyText: '#1F2937', tableHeaderBg: '#1E40AF', tableHeaderText: '#FFFFFF', tableRowAlt: '#EFF6FF', tableBorder: '#BFDBFE', totalHighlight: '#1E40AF', }, layout: { logoPosition: 'top-right', headerStyle: 'full-width', tableStyle: 'striped', totalsPosition: 'center', showDividers: false, dividerStyle: 'none', }, typography: { titleSize: 24, titleWeight: 'bold', headerSize: 12, bodySize: 10, numberStyle: 'monospace-feel' }, decorative: { cornerShape: 'none', sidebarWidth: 0, sidebarColor: '', useGradientHeader: false, backgroundTint: false }, } // --- Registry --- export const INVOICE_TEMPLATES: InvoiceTemplateConfig[] = [ cleanMinimal, corporateClassic, modernBold, elegantSerif, simpleTwoTone, gradientHeader, sidebarAccent, geometricModern, darkMode, warmNatural, playfulColorBlock, retroProfessional, techMinimal, executivePremium, dataDrivenClean, ] export const TEMPLATE_CATEGORIES = [ { id: 'essential', label: 'Professional Essentials' }, { id: 'creative', label: 'Creative & Modern' }, { id: 'warm', label: 'Warm & Distinctive' }, { id: 'premium', label: 'Premium & Specialized' }, ] as const export function getTemplateById(id: string): InvoiceTemplateConfig { return INVOICE_TEMPLATES.find(t => t.id === id) || INVOICE_TEMPLATES[0] } export function getTemplatesByCategory(category: string): InvoiceTemplateConfig[] { return INVOICE_TEMPLATES.filter(t => t.category === category) } ``` **Step 2: Verify the file has no TypeScript errors** Run: `npx tsc --noEmit --pretty 2>&1 | head -20` If there are import errors, fix them. The file should have zero imports (it's self-contained types + data). **Step 3: Commit** ```bash git add src/utils/invoiceTemplates.ts git commit -m "feat: add 15 invoice template configs and registry" ``` --- ### Task 2: Create the jsPDF template renderer **Files:** - Create: `src/utils/invoicePdfRenderer.ts` - Modify: `src/utils/invoicePdf.ts` (will become a thin wrapper) **Context:** The current `src/utils/invoicePdf.ts` has a single `generateInvoicePdf()` function with hardcoded amber theme. We need a new renderer that reads template config. The old file's `InvoiceItem` interface and `generateInvoicePdf` signature stay the same (they're used by `Invoices.vue:545,755`), but internally it delegates to the new renderer. **Step 1: Create the config-driven renderer** Create `src/utils/invoicePdfRenderer.ts`. This file exports `renderInvoicePdf(doc, config, invoice, client, items, businessInfo)` which draws onto an existing jsPDF document using the template config. The renderer must handle: - **Page background**: Fill entire page with `config.colors.background` (skip if white) - **Decorative elements**: sidebar bar, corner shapes, gradients, background tints - **Header section**: 7 header styles — each is a separate function: - `minimal`: Title left, invoice details below, client info right. Simple accent line. - `full-width`: Full-width colored header band with white text. Business name + invoice number. - `split`: Left half dark background (business info), right half light (invoice details). - `sidebar`: Narrow colored bar on left edge, content offset. Business info near sidebar. - `gradient`: Simulated gradient via multiple thin colored rect strips across header width. - `geometric`: Triangle in top-right corner drawn via path, invoice details below. - `centered`: Centered title, centered business name, dividers above and below. - **Bill To section**: Client name, email, address. Position depends on header style. - **Line items table**: 5 table styles: - `bordered`: Full cell borders - `striped`: Alternating row backgrounds, no vertical borders - `borderless`: No borders at all, just spacing - `minimal-lines`: Horizontal lines only between rows - `colored-sections`: Colored background for header, alternating colored rows - **Totals section**: Subtotal, tax, discount, total. Right-aligned or centered per config. - **Notes section**: If invoice has notes, render below totals. - **Logo rendering**: If `businessInfo.logo` is set, render via `doc.addImage()` at the position specified by `config.layout.logoPosition`. - **Page breaks**: Check `yPosition > 270` before each row, add new page if needed. Key technical notes: - jsPDF gradient: Simulate by drawing 20+ thin horizontal `rect()` strips with interpolated colors from `gradientFrom` to `gradientTo`. - jsPDF triangle: Use `doc.triangle(x1,y1, x2,y2, x3,y3, 'F')`. - jsPDF sidebar: `doc.rect(0, 0, sidebarWidth, pageHeight, 'F')`. - Font: jsPDF only has `helvetica`, `courier`, `times`. Use `helvetica` for all. For `monospace-feel` numbers, use `courier` for amount columns only. - Color parsing: The config uses hex strings. jsPDF `setFillColor`/`setTextColor` accept hex strings directly. - Business info comes from settings store: `{ name, address, email, phone, logo }`. ```ts import { jsPDF } from 'jspdf' import type { InvoiceTemplateConfig } from './invoiceTemplates' import type { Invoice } from '../stores/invoices' import type { Client } from '../stores/clients' import type { InvoiceItem } from './invoicePdf' import { formatCurrency, formatDate } from './locale' export interface BusinessInfo { name: string address: string email: string phone: string logo: string // base64 data URL or empty } export function renderInvoicePdf( config: InvoiceTemplateConfig, invoice: Invoice, client: Client, items: InvoiceItem[], businessInfo: BusinessInfo ): jsPDF { const doc = new jsPDF() const pageWidth = doc.internal.pageSize.getWidth() // 210 const pageHeight = doc.internal.pageSize.getHeight() // 297 const margin = 20 const contentWidth = pageWidth - margin * 2 // Draw page background if (config.colors.background !== '#FFFFFF') { doc.setFillColor(config.colors.background) doc.rect(0, 0, pageWidth, pageHeight, 'F') } // Draw decorative elements renderDecorative(doc, config, pageWidth, pageHeight, margin) let y = margin // Draw header y = renderHeader(doc, config, invoice, client, businessInfo, y, margin, pageWidth, contentWidth) // Draw line items table y = renderTable(doc, config, items, y, margin, contentWidth) // Draw totals y = renderTotals(doc, config, invoice, y, margin, pageWidth, contentWidth) // Draw notes if (invoice.notes) { y = renderNotes(doc, config, invoice.notes, y, margin, contentWidth, pageHeight) } return doc } // ... implement each render function following the patterns above. // Full implementation provided in code — each function uses doc.setFillColor, // doc.setTextColor, doc.setFont, doc.text, doc.rect, doc.line, doc.triangle etc. ``` **Important implementation details for each render function:** `renderDecorative(doc, config, pageWidth, pageHeight, margin)`: - If `config.decorative.sidebarWidth > 0`: draw `doc.rect(0, 0, config.decorative.sidebarWidth, pageHeight, 'F')` with `sidebarColor`. - If `config.decorative.cornerShape === 'colored-block'`: draw `doc.rect(0, 0, 60, 60, 'F')` with `config.colors.primary`. - If `config.decorative.cornerShape === 'triangle'`: draw `doc.triangle(pageWidth, 0, pageWidth, 80, pageWidth - 80, 0, 'F')` with `config.colors.primary`. - If `config.decorative.cornerShape === 'diagonal'`: draw a filled triangle across top. - If `config.decorative.backgroundTint && config.decorative.backgroundTintColor`: fill full page. `renderHeader(doc, config, invoice, client, businessInfo, y, margin, pageWidth, contentWidth)`: - Switch on `config.layout.headerStyle`. - For `'gradient'`: draw 30 thin rect strips from y=0 to y=45, interpolating color from `gradientFrom` to `gradientTo`. Then overlay white text. - For `'split'`: draw left half rect with headerBg color, right half stays white. - For `'full-width'`: draw full-width rect with headerBg color. - For `'sidebar'`: offset all content by `sidebarWidth + 6`. - After header style-specific rendering, always draw: - Invoice title "INVOICE" in `config.colors.primary` at `config.typography.titleSize` - Invoice number, date, due date, status - "Bill To:" section with client info - Logo if available - Return new y position after header. `renderTable(doc, config, items, y, margin, contentWidth)`: - Column widths: Description 50%, Qty 15%, Rate 17.5%, Amount 17.5% - Table header row: background `config.colors.tableHeaderBg`, text `config.colors.tableHeaderText` - For each item row: apply table style - `'bordered'`: draw rect border around each cell - `'striped'`: draw alternating `config.colors.tableRowAlt` backgrounds - `'borderless'`: no borders, just text - `'minimal-lines'`: draw horizontal line after each row - `'colored-sections'`: draw colored background sections - Font for amounts: if `config.typography.numberStyle === 'monospace-feel'`, use `courier` font for Qty/Rate/Amount columns. - Page break check: if y > 260, `doc.addPage()`, redraw background if needed, reset y. - Return new y position. `renderTotals(doc, config, invoice, y, margin, pageWidth, contentWidth)`: - If `config.layout.totalsPosition === 'center'`: center the totals block - Else: right-align at `pageWidth - margin - 60` - Draw Subtotal, Tax (if > 0), Discount (if > 0) - Draw separator line - Draw Total in `config.colors.totalHighlight` at larger font size - Return new y position. `renderNotes(doc, config, notes, y, margin, contentWidth, pageHeight)`: - If y > 240, add new page - Draw "Notes:" label and wrapped text - Return new y position. **Step 2: Update the old invoicePdf.ts to delegate** Modify `src/utils/invoicePdf.ts` to keep the same `generateInvoicePdf` signature but delegate to the new renderer with a default template: ```ts import type { Invoice } from '../stores/invoices' import type { Client } from '../stores/clients' import { renderInvoicePdf, type BusinessInfo } from './invoicePdfRenderer' import { getTemplateById } from './invoiceTemplates' export interface InvoiceItem { id?: number description: string quantity: number rate: number amount: number } /** * Generate a PDF invoice using a template */ export function generateInvoicePdf( invoice: Invoice, client: Client, items: InvoiceItem[], templateId: string = 'clean-minimal', businessInfo?: BusinessInfo ): jsPDF { const config = getTemplateById(templateId) const info: BusinessInfo = businessInfo || { name: '', address: '', email: '', phone: '', logo: '' } return renderInvoicePdf(config, invoice, client, items, info) } ``` **Step 3: Update the Invoices.vue call site** In `src/views/Invoices.vue:755`, the current call is: ```ts const doc = generateInvoicePdf(invoice, client, items) ``` This still works because `templateId` has a default value. No changes needed yet — template selection will be added in Task 5. **Step 4: Verify build** Run: `npm run build 2>&1 | tail -20` Expected: Build succeeds. **Step 5: Commit** ```bash git add src/utils/invoicePdfRenderer.ts src/utils/invoicePdf.ts git commit -m "feat: add config-driven jsPDF invoice renderer with 7 header styles and 5 table styles" ``` --- ### Task 3: Create InvoicePreview.vue HTML component **Files:** - Create: `src/components/InvoicePreview.vue` **Context:** This component renders an HTML preview of an invoice using the template config. It replicates the jsPDF renderer's visual output in HTML/CSS so the user sees exactly what the PDF will look like. It replaces the hardcoded amber preview in `Invoices.vue:417-497`. The component receives props: `template` (InvoiceTemplateConfig), `invoice` (Invoice), `client` (Client | null), `items` (InvoiceItem[]), `businessInfo` (BusinessInfo). It renders a scaled-down A4-ratio div (8.5in x 11in) with: - Page background color from `config.colors.background` - All decorative elements (sidebar, corners, gradient header) via CSS - Header section matching the template's header style - Client "Bill To" section - Line items table with the template's table style - Totals section - Notes section **Step 1: Create the component** ```vue ``` **Step 2: Verify build** Run: `npm run build 2>&1 | tail -20` **Step 3: Commit** ```bash git add src/components/InvoicePreview.vue git commit -m "feat: add InvoicePreview.vue HTML template preview component" ``` --- ### Task 4: Create InvoiceTemplatePicker.vue **Files:** - Create: `src/components/InvoiceTemplatePicker.vue` **Context:** Split-pane template picker. Left panel (30%) has a scrollable list of template names grouped by category with small color dots. Right panel (70%) shows a live preview of the selected template. The component emits `update:modelValue` with the selected template ID. ```vue ``` **Step 2: Verify build** Run: `npm run build 2>&1 | tail -20` **Step 3: Commit** ```bash git add src/components/InvoiceTemplatePicker.vue git commit -m "feat: add InvoiceTemplatePicker split-pane component" ``` --- ### Task 5: Integrate template picker into Invoices.vue **Files:** - Modify: `src/views/Invoices.vue` **Context:** Three integration points: 1. Add template picker to the **Create** view (between the top form section and line items) 2. Add template selector dropdown to the **Preview dialog** toolbar 3. Pass selected template ID to `generateInvoicePdf()` and to `InvoicePreview` in the preview dialog 4. Replace the hardcoded amber preview in the dialog with `InvoicePreview` component **Step 1: Add imports and state** At the top of `