Redesign PDF header and fix layout issues
Some checks failed
/ mirror (push) Has been cancelled

- Header: remove initials box, match timesheet style (org name left,
  REIMBURSEMENT right, same size, muted subtitles below each)
- Replace thin hairline after header with thick accent-colour divider (2.5pt)
- Info strip (Staff/Period/Currency): fix unequal vertical padding — labels
  were crowded to top border; now symmetric using cap-height maths
- Item section header: replace bare left stripe with full-width surface-2 strip,
  equal visual padding above and below text, accent bar full height of strip
- Reduce excessive whitespace after receipt refs and before grand total divider
- Apply same equal-padding fix to continuation header (page 2+)

https://claude.ai/code/session_01JyuActqTJG5tuRQNLmT7fZ
This commit is contained in:
Claude 2026-06-08 17:26:47 +00:00
parent 1b6ea1b875
commit df8816dfb4
No known key found for this signature in database

View file

@ -1381,12 +1381,13 @@ async function generatePDF() {
// Continuation header: light strip with staff + period
function drawContHeader() {
const stripH = szXs + lh + 14;
const cPad = 7;
const stripH = 2 * cPad + szXs + 5 + sz;
pg.drawRectangle({ x: 0, y: y - stripH, width: pageW, height: stripH, color: clrSurface2 });
pg.drawLine({ start:{x:0, y}, end:{x:pageW, y}, thickness:0.5, color:clrBorder });
pg.drawLine({ start:{x:0, y:y-stripH}, end:{x:pageW, y:y-stripH}, thickness:0.5, color:clrBorder });
const labY = y - 7;
const labY = y - Math.round(cPad + szXs * 0.72);
const valY = labY - szXs - 5;
lbl('Staff', M.left, labY);
lbl('Period', M.left + W * 0.45, labY);
@ -1399,47 +1400,38 @@ async function generatePDF() {
// ── Page 1 ────────────────────────────────────────────────────────────────
addPage(true);
// Header: brand block left | doc title right
// Header: org name left | REIMBURSEMENT right (timesheet style)
const hdrTopY = y;
const boxSize = 44;
const nowD = new Date();
const MONTHS = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
const hdrNameSize = szLg; // org name and title same size
const hdrNameY = hdrTopY - Math.round(hdrNameSize * 0.28); // cap top near hdrTopY
if (logoImage) {
const maxWLogo = (CFG['logo-maxwidth'] || 4) * 28.3465;
const scale = Math.min(maxWLogo / logoImage.width, boxSize / logoImage.height, 1);
const scale = Math.min(maxWLogo / logoImage.width, hdrNameSize * 1.6 / logoImage.height, 1);
const lw = logoImage.width * scale, lhImg = logoImage.height * scale;
pg.drawImage(logoImage, { x: M.left, y: hdrTopY - lhImg, width: lw, height: lhImg });
} else {
// Initials box (surface-2 fill, border, accent initials)
const raw = (CFG.organization || 'ORG').trim();
const initials = raw.split(/\s+/).map(w => w[0] || '').join('').toUpperCase().slice(0, 2) || '??';
pg.drawRectangle({ x: M.left, y: hdrTopY - boxSize, width: boxSize, height: boxSize,
color: clrSurface2, borderColor: clrBorder, borderWidth: 0.5 });
const initW = fontBold.widthOfTextAtSize(initials, szLg);
pg.drawText(initials, { x: M.left + (boxSize - initW) / 2, y: hdrTopY - boxSize / 2 - szLg * 0.35,
size: szLg, font: fontBold, color: clrAccent });
pg.drawText(CFG.organization || '', { x: M.left, y: hdrNameY, size: hdrNameSize, font: fontBold, color: clrText });
}
const hdrSubtY = hdrNameY - hdrNameSize - 5;
pg.drawText('Expense reimbursement', { x: M.left, y: hdrSubtY, size: szSm, font: fontBody, color: clrMuted });
// Org name + subtitle
const orgX = M.left + boxSize + 11;
pg.drawText(CFG.organization || '', { x: orgX, y: hdrTopY - 15, size: sz + 2, font: fontBold, color: clrText });
pg.drawText('Expense reimbursement', { x: orgX, y: hdrTopY - 15 - (sz + 2) - 4, size: szSm, font: fontBody, color: clrMuted });
const titleStr = 'REIMBURSEMENT';
const titleW = fontBold.widthOfTextAtSize(titleStr, hdrNameSize);
pg.drawText(titleStr, { x: M.left + W - titleW, y: hdrNameY, size: hdrNameSize, font: fontBold, color: clrText });
// Title + claim date (right-aligned)
const titleStr = 'Reimbursement';
const titleW = fontBold.widthOfTextAtSize(titleStr, szLg);
pg.drawText(titleStr, { x: M.left + W - titleW, y: hdrTopY - 15, size: szLg, font: fontBold, color: clrText });
const nowD = new Date();
const MONTHS = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
const claimDate = `Claim · ${nowD.getDate()} ${MONTHS[nowD.getMonth()]} ${nowD.getFullYear()}`;
const claimDateW = fontBody.widthOfTextAtSize(claimDate, szSm);
pg.drawText(claimDate, { x: M.left + W - claimDateW, y: hdrTopY - 15 - szLg - 4, size: szSm, font: fontBody, color: clrMuted });
pg.drawText(claimDate, { x: M.left + W - claimDateW, y: hdrSubtY, size: szSm, font: fontBody, color: clrMuted });
y = hdrTopY - boxSize - 10;
y = hdrSubtY - szSm - 12;
// Hairline below header
pg.drawLine({ start:{x:M.left, y}, end:{x:M.left+W, y}, thickness:0.5, color:clrBorder });
y -= 12;
// Thick accent divider below header
pg.drawLine({ start:{x:M.left, y}, end:{x:M.left+W, y}, thickness:2.5, color:clrAccent });
y -= 16;
// Intro text
if (CFG.intro) {
@ -1448,13 +1440,14 @@ async function generatePDF() {
y -= 6;
}
// Staff / Period / Currency info block
const infoH = szXs + lh + 14;
// Staff / Period / Currency info block — equal visual padding top and bottom
const infoPad = 7;
const infoH = 2 * infoPad + szXs + 5 + sz;
pg.drawRectangle({ x: M.left - 8, y: y - infoH, width: W + 16, height: infoH, color: clrSurface2 });
pg.drawLine({ start:{x:M.left-8, y}, end:{x:M.left+W+8, y}, thickness:0.5, color:clrBorder });
pg.drawLine({ start:{x:M.left-8, y:y-infoH}, end:{x:M.left+W+8, y:y-infoH}, thickness:0.5, color:clrBorder });
const iLabY = y - 7;
const iLabY = y - Math.round(infoPad + szXs * 0.72);
const iValY = iLabY - szXs - 5;
const iC2 = W * 0.45, iC3 = W * 0.78;
@ -1472,17 +1465,18 @@ async function generatePDF() {
state.items.forEach(item => {
needSpace(lh * 7);
// Section header: accent stripe + item name left, subtotal right
accentStripe(y);
pg.drawText(item.name || '(no name)', { x: M.left + 8, y, size: sz, font: fontBold, color: clrTextSoft });
// Section header strip — equal visual padding, accent left bar
const secH = sz + 16;
const secTextY = Math.round(y - secH / 2 + sz * 0.22);
pg.drawRectangle({ x: M.left, y: y - secH, width: W, height: secH, color: clrSurface2 });
pg.drawLine({ start:{x:M.left, y}, end:{x:M.left+W, y}, thickness:0.5, color:clrBorder });
pg.drawLine({ start:{x:M.left, y:y-secH}, end:{x:M.left+W, y:y-secH}, thickness:0.5, color:clrBorder });
pg.drawRectangle({ x: M.left, y: y - secH, width: 3, height: secH, color: clrAccent });
pg.drawText(item.name || '(no name)', { x: M.left + 10, y: secTextY, size: sz, font: fontBold, color: clrTextSoft });
const subStr = `${baseCur} ${fmtAmt(item._subtotal)}`;
const subW = fontBold.widthOfTextAtSize(subStr, sz);
pg.drawText(subStr, { x: M.left + W - subW, y, size: sz, font: fontBold, color: clrAccent });
y -= lh + 2;
// Hairline below section header
pg.drawLine({ start:{x:M.left, y}, end:{x:M.left+W, y}, thickness:0.5, color:clrBorder });
y -= 14;
pg.drawText(subStr, { x: M.left + W - subW, y: secTextY, size: sz, font: fontBold, color: clrAccent });
y -= secH + 14;
// Lines
item.lines.forEach((ln, li) => {
@ -1571,15 +1565,15 @@ async function generatePDF() {
explLines.forEach(line => { needSpace(lh); val(line, M.left, y); y -= lh; });
}
y -= 6;
y -= 2;
});
y -= 16;
y -= 10;
});
// Grand total
needSpace(lh * 3 + 6);
y -= 6;
y -= 4;
pg.drawLine({ start:{x:M.left, y}, end:{x:M.left+W, y}, thickness:1.5, color:clrBorderStrong });
y -= lh;