# PDF Layout Refinement Patterns (jsPDF) Patterns learned from iterative PDF layout fixes in `quote-tab-manager.js` (ShopProQuote). ## Fill-Before-Text Ordering When drawing a background box with text on top, always draw the fill FIRST, then the text, then the border last: ```javascript // 1. Draw fill-only background BEFORE any text doc.setFillColor(248, 248, 248); doc.roundedRect(x, y, w, initialHeight, r, r, 'F'); // 2. Draw all text content on top of the fill doc.text(...); // 3. Compute actual height, draw border-only at correct size const actualHeight = finalY - startY; doc.setDrawColor(220, 220, 220); doc.setLineWidth(0.5); doc.roundedRect(x, y, w, actualHeight, r, r, 'D'); ``` NEVER use `'FD'` (fill+draw) after text — it paints over the text. The two-pass approach (fill first with generous initial height, border after at computed height) ensures text is always visible and borders are tight. ## Dynamic Box Height Never hardcode box height. Compute it from content: ```javascript const headerStartY = yPos; // ... draw all content, advancing yPos ... const actualHeight = yPos - headerStartY; ``` This prevents boxes from being too short (clipping content) or too tall (wasted space with large gaps). ## Overlap Prevention When a footer note appears below a box, position it relative to the box's visual bottom, not the last text line: ```javascript // WRONG — note may overlap the box yPos += 15; doc.text(note, x, yPos); // RIGHT — note starts after box's visual boundary yPos = boxBottomY + 10; doc.text(note, x, yPos); ``` ## Color Visibility Thresholds On white paper (#ffffff), very light fills and borders are invisible: | Color | Visible? | Minimum for visibility | |-------|----------|----------------------| | Fill #fafafa (250,250,250) | No — indistinguishable from white | #f8f8f8 (248,248,248) | | Fill #f8f8f8 (248,248,248) | Yes — subtle but visible | — | | Border #e6e6e6 at 0.5px | No — too light and thin | #dcdcdc at 0.5px | | Border #dcdcdc at 0.5px | Yes — subtle but defined | — | Rule: fill at #f8f8f8, border at #dcdcdc, line width 0.5px. These match across header boxes, totals boxes, and service separators for consistent style. ## Label Alignment (Widest-First) When rendering label-value pairs in a PDF, align all values at a consistent offset after the widest label: ```javascript // 1. Measure the widest label let maxWidth = 0; labels.forEach(label => { const w = doc.getTextWidth(label); if (w > maxWidth) maxWidth = w; }); // 2. Position all values at widest label + gap const valueX = labelStartX + maxWidth + 12; labels.forEach((label, value) => { doc.text(label, labelStartX, y); doc.text(value, valueX, y); }); ``` This produces a clean table-like alignment without wasted whitespace between label and value. ## Multi-Pass Page Numbering Add page numbers AFTER all content is laid out, iterating through all pages: ```javascript const totalPages = doc.internal.getNumberOfPages(); for (let i = 1; i <= totalPages; i++) { doc.setPage(i); doc.text(`Page ${i} of ${totalPages}`, pageWidth - margin, pageHeight - 10, { align: 'right' }); } ``` This ensures page numbers reflect the final page count, not an intermediate count. ## Service-Specific Pitfalls - `technicianNotes` should be excluded from customer-facing PDFs (internal context only) - Approved/declined badges should use colored backgrounds (green #dcfce7 for approved, red #fef2f2 for declined) - Declined services should be grayed out with strikethrough - Services should be grouped: approved first, then pending/declined