From 8e5d4cf6491e02eeb305066d6fcc75f9475d390c Mon Sep 17 00:00:00 2001 From: DemchaAV Date: Wed, 22 Jul 2026 22:46:45 +0100 Subject: [PATCH] fix(templates): keep Panel's full-width Profile card header with its body MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Panel renders its full-width Profile card in the single-column page flow via a splittable CardWidget; its header (module title + accent strip, from the shared renderModuleBody) could strand at a page bottom if a long summary split the card. The other single-column presets were wired for this already, but renderModuleBody is shared with Panel's side-column cards, where marking the whole card would be wrong. Wrap the header (title + accent strip) in a nested keep-with-next subsection whose spacing matches the card, so the header stays with the body's first line. In the full-width Profile card (page flow) the header relocates with its body; in the side cards (fixed Row columns) keep-with-next is inert, so those cards are byte-identical. Reuses SectionNode.keepWithNext — no new API. --- CHANGELOG.md | 6 ++++-- .../presets/PresetHeaderKeepWithNextTest.java | 17 +++++++++++++++++ .../document/templates/cv/presets/Panel.java | 14 ++++++++++++-- 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 52244a09..d2834085 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -302,8 +302,10 @@ for this cycle. section heading with the first line of its body across a page break: a standalone header section binds to the following body section (a multi-node rule + banner + rule group binds as one run), and a combined header+body module keeps the heading in a - nested keep-with-next group. Multi-column presets place their sections in fixed - columns that do not paginate, so no heading can strand there. + nested keep-with-next group — including Panel's full-width Profile card, whose header + now stays with the summary if a long profile splits the card. Multi-column presets + place their sections in fixed columns that do not paginate, so no heading can strand + there. - **Reproducible PDF output** (`@Beta`). `PdfFixedLayoutBackend.builder().deterministic(true)` (or `.deterministic(Instant)` for an explicit timestamp) pins the document CreationDate / ModDate and derives the PDF `/ID` from the document metadata instead diff --git a/qa/src/test/java/com/demcha/compose/document/templates/cv/presets/PresetHeaderKeepWithNextTest.java b/qa/src/test/java/com/demcha/compose/document/templates/cv/presets/PresetHeaderKeepWithNextTest.java index 84f77e67..9e46ed1c 100644 --- a/qa/src/test/java/com/demcha/compose/document/templates/cv/presets/PresetHeaderKeepWithNextTest.java +++ b/qa/src/test/java/com/demcha/compose/document/templates/cv/presets/PresetHeaderKeepWithNextTest.java @@ -107,6 +107,23 @@ void blueBannerKeepsWholeTitleRun() { assertThat(firstRun).hasSizeGreaterThanOrEqualTo(3).allMatch(DocumentNode::keepWithNext); } + /** + * Panel renders each module's header (title + accent strip) in a nested + * keep-with-next subsection so its full-width single-column Profile card keeps + * the title with the body if a long summary splits the card. Every header + * subsection is kept; the card sections that wrap header and body are not (marking + * them would bind a card to the next card). + */ + @Test + void panelKeepsCardHeaderSubsectionNotCard() { + List nodes = nodesOf(Panel.create()); + assertThat(nodes.stream().filter(n -> "Header".equals(n.name())).toList()) + .isNotEmpty().allMatch(DocumentNode::keepWithNext); + assertThat(nodes.stream() + .filter(n -> n.name().startsWith("CvV2Panel") && n.name().endsWith("Card")).toList()) + .isNotEmpty().noneMatch(DocumentNode::keepWithNext); + } + /** * End-to-end for the combined-preset shape: a module section whose header is a * nested keep-with-next subsection relocates the header with the first line of the diff --git a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/Panel.java b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/Panel.java index 9bdc9a5f..3f5e4453 100644 --- a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/Panel.java +++ b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/Panel.java @@ -352,7 +352,17 @@ private static boolean hasContent(CvSection section) { private void renderModuleBody(SectionBuilder card, String title, CvSection section) { - card.addParagraph(paragraph -> paragraph + // Header (title + accent strip) in a keep-with-next subsection so, when + // this is the full-width single-column Profile card and a long summary + // splits it across a page break, the title stays with the first line of + // the body rather than stranding at the page bottom. The subsection's + // spacing matches the card, so placement is unchanged; in the side + // columns (fixed Row slots) keep-with-next is inert, so those cards are + // byte-identical. + card.addSection("Header", header -> header + .keepWithNext() + .spacing(theme.spacing().sectionBodySpacing()) + .addParagraph(paragraph -> paragraph .text(title.toUpperCase(Locale.ROOT)) .textStyle(moduleTitleStyle()) .align(TextAlign.LEFT) @@ -365,7 +375,7 @@ private void renderModuleBody(SectionBuilder card, String title, .fillColor(ACCENT) .cornerRadius( theme.spacing().accentRuleWidth() / 2.0) - .margin(DocumentInsets.zero())); + .margin(DocumentInsets.zero()))); renderCardBody(card, section); }