From 129f952db4258d47a2c85fa1747082b1fe1c68d6 Mon Sep 17 00:00:00 2001 From: Maurice Date: Wed, 29 Jul 2026 20:30:20 +0200 Subject: [PATCH] fix(pdf): stop a flowing day stranding its header at the foot of a page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Keeping a stay whole moved its cards to the next sheet, and the header bar stayed behind on the old one — empty, above a stretch of white, with the same day printed again overhead. The repeat itself is the continuation marker from #1471; what was wrong is that there was nothing left to continue from. break-after on the thead and break-before on the tbody are both ignored by Chromium here, measured rather than assumed. Holding the whole day together is what works: a day that fits moves as one, and a day too long for a page still breaks and still repeats its header. Scoped to the flowing layout, since a day that starts its own page can never strand one. --- client/src/components/PDF/TripPDF.test.ts | 16 ++++++++++++++++ client/src/components/PDF/TripPDF.tsx | 9 +++++++++ 2 files changed, 25 insertions(+) diff --git a/client/src/components/PDF/TripPDF.test.ts b/client/src/components/PDF/TripPDF.test.ts index e790275ec..4f79d4ea5 100644 --- a/client/src/components/PDF/TripPDF.test.ts +++ b/client/src/components/PDF/TripPDF.test.ts @@ -1014,4 +1014,20 @@ describe('page breaks between days (#1292)', () => { const rule = html.slice(html.indexOf('.day-accommodation {')) expect(rule.slice(0, rule.indexOf('}'))).toContain('break-inside: avoid') }) + + // Measured against Chromium: with the days flowing, a header bar could be left + // at the foot of a sheet while its content moved on, and the repeat from #1471 + // then read as the same day printed twice. break-after on the thead and + // break-before on the tbody are both ignored; holding the day together is what + // works, and a day too long for one page still breaks and still repeats. + it('FE-PDF-BREAK-008: a flowing day is held together so its header is never stranded', async () => { + localStorage.setItem('trek_pdf_page_break_per_day', '0') + await downloadTripPDF(twoDays) + const html = getIframe()!.srcdoc + + expect(html).toContain('.pdf-flow .day-section { break-inside: avoid; page-break-inside: avoid; }') + // Scoped: a day that starts its own page cannot strand a header. + const at = html.indexOf('.day-section { break-inside: avoid') + expect(html.slice(at - 10, at)).toContain('.pdf-flow ') + }) }) diff --git a/client/src/components/PDF/TripPDF.tsx b/client/src/components/PDF/TripPDF.tsx index 174b20dc5..1f2856651 100644 --- a/client/src/components/PDF/TripPDF.tsx +++ b/client/src/components/PDF/TripPDF.tsx @@ -542,6 +542,15 @@ export async function downloadTripPDF({ trip, days, places, assignments = {}, ca .day-break { page-break-before: always; } .pdf-flow .day-break { page-break-before: auto; } .pdf-flow .day-section + .day-section { margin-top: 18px; } + /* Hold a flowing day together. Without this the header bar can be placed at the + foot of a sheet while its content moves to the next one, which then repeats + the header (#1471) and reads as the same day printed twice. A day too long + for one page still breaks and still repeats its header — the engine only + honours this where it can. Days that start a page of their own cannot strand + a header, so it is scoped to the flowing layout. + Measured, not assumed: break-after on the thead and break-before on the tbody + are both ignored by Chromium here. */ + .pdf-flow .day-section { break-inside: avoid; page-break-inside: avoid; } .day-section { width: 100%; border-collapse: collapse; table-layout: fixed; } .day-header-bar { background: #0f172a; padding: 11px 28px;