# 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
