Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<DocumentNode> 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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);
}

Expand Down
Loading