Files
2026-07-12 10:17:17 -04:00

107 lines
3.5 KiB
Markdown

# 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