107 lines
3.5 KiB
Markdown
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
|