Skip to content

Commit fc4bb18

Browse files
authored
feat(cv-v2): migrate Panel preset (#62)
Ports the legacy Panel CV preset (PanelCvTemplateComposer stacked layout) onto the v2 layered architecture. The preset reads as four equal-width panels of the same shell: * Header card — pale-teal fill, centred Poppins UPPERCASE name, optional job title, centred meta + link line. Name paragraph is inlined instead of going through Headline.uppercaseCentered because that widget rewrites the card's padding via host.padding(...) which otherwise leaks into a visibly wider header outline. * Profile card — full-width white card with UPPERCASE teal title, bronze-like teal accent strip, and the summary paragraph. * Two-column row — left card stacks Skills + Education + Additional, right card stacks Experience + Projects. Each card is its own panel with the shared shell. * All cards (header, profile, side modules) share a single CardWidget.Style: stroke 0.45pt, padding(bannerInnerPadding=8), cornerRadius(7). Only fillColor differs (header tinted teal vs panels white). Section widths are anchored explicitly. v2 engine sections default to fit-content widths inside a vertical column, so a card with long content (Skills) would otherwise render visibly wider than a card with short content (Education). The new widthAnchor(card, width) helper drops a zero-height spacer of the pre-computed target width as the card's first element, pinning every panel to identical outer widths regardless of paragraph length. Body rendering uses a preset-local dispatcher because the engine bans nested horizontal rows and the side cards sit inside flow.addRow. EntriesSection headers therefore use EntryCompactRenderer.titleDateBody (single 'title - date' paragraph) instead of the standard EntryRenderer's 2-column Row header. Theme additions are additive: * CvPalette.panel() — body slate ink, pale teal stroke + banner fill. * CvTypography.panel() — Poppins/Lato 22/8.9/10.4/9.4/9.4/9.0/9.4 1.2. * CvSpacing.panel() — 6pt page-flow gap, 8pt card padding, 2.2pt accent strip, 7pt corner radius. * CvTheme.panel() factory wires the three above with the classic decoration. CardWidget gains a PageFlowBuilder overload so top-level cards (Profile) can be rendered directly under PageFlowBuilder without an intermediate wrapper section. The applyStyle path is shared between both render() overloads. Tests: * CvV2VisualParityTest now exercises 11 presets (added Panel); 11/11 pass at the existing 50k pixel-diff budget against the new visual-baselines/cv-v2-layered/panel-page-0.png baseline (single page). * PanelSmokeTest covers stable identity + default-factory and custom-theme render paths.
1 parent 6819ace commit fc4bb18

10 files changed

Lines changed: 765 additions & 12 deletions

File tree

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package com.demcha.examples.templates.cv.v2;
2+
3+
import com.demcha.compose.GraphCompose;
4+
import com.demcha.compose.document.api.DocumentPageSize;
5+
import com.demcha.compose.document.api.DocumentSession;
6+
import com.demcha.compose.document.templates.api.DocumentTemplate;
7+
import com.demcha.compose.document.templates.cv.v2.data.CvDocument;
8+
import com.demcha.compose.document.templates.cv.v2.presets.Panel;
9+
import com.demcha.examples.support.ExampleDataFactory;
10+
import com.demcha.examples.support.ExampleOutputPaths;
11+
12+
import java.nio.file.Path;
13+
14+
/**
15+
* Renders the v2 Panel CV preset against the shared grouped skills
16+
* sample data — pale-teal header card with centred Poppins masthead,
17+
* full-width Profile panel, two-column row pairing Skills + Education
18+
* on the left with Experience + Projects on the right, and a closing
19+
* Additional panel.
20+
*
21+
* <p>Output:
22+
* {@code examples/target/generated-pdfs/templates/cv/cv-panel-v2.pdf}.</p>
23+
*/
24+
public final class CvPanelExample {
25+
26+
private CvPanelExample() {
27+
}
28+
29+
public static Path generate() throws Exception {
30+
Path outputFile = ExampleOutputPaths.prepare(
31+
"templates/cv", "cv-panel-v2.pdf");
32+
CvDocument doc = ExampleDataFactory.sampleCvDocumentV2();
33+
DocumentTemplate<CvDocument> template = Panel.create();
34+
35+
float m = (float) Panel.RECOMMENDED_MARGIN;
36+
try (DocumentSession document = GraphCompose.document(outputFile)
37+
.pageSize(DocumentPageSize.A4)
38+
.margin(m, m, m, m)
39+
.create()) {
40+
template.compose(document, doc);
41+
document.buildPdf();
42+
}
43+
return outputFile;
44+
}
45+
46+
public static void main(String[] args) throws Exception {
47+
System.out.println("Generated: " + generate());
48+
}
49+
}

src/main/java/com/demcha/compose/document/templates/cv/v2/presets/Panel.java

Lines changed: 508 additions & 0 deletions
Large diffs are not rendered by default.

src/main/java/com/demcha/compose/document/templates/cv/v2/theme/CvPalette.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,23 @@ public static CvPalette editorialBlue() {
123123
DocumentColor.rgb(193, 201, 211));
124124
}
125125

126+
/**
127+
* Panel palette ported from the v1 {@code PanelCvTemplateComposer}
128+
* (ProductLeader tokens): body slate ink, slightly lighter slate
129+
* for italic subtitles, the pale teal stroke used by every panel
130+
* border, and the pale teal header card fill. The deeper header
131+
* navy (rgb(20,44,66)), teal accent (rgb(0,128,128)), and white
132+
* panel fill are preset-local because they are the fifth/sixth/
133+
* seventh tokens — other v2 presets do not share them today.
134+
*/
135+
public static CvPalette panel() {
136+
return new CvPalette(
137+
DocumentColor.rgb(54, 68, 84), // ink — V1 BODY_TEXT/HEADER_META slate
138+
DocumentColor.rgb(105, 117, 132), // muted — slightly lighter slate
139+
DocumentColor.rgb(179, 214, 211), // rule — V1 PANEL_STROKE pale teal
140+
DocumentColor.rgb(231, 246, 244)); // banner — V1 HEADER_FILL pale teal
141+
}
142+
126143
/**
127144
* Executive palette ported from the v1 {@code ExecutiveSlateCvTemplate}:
128145
* mid-slate body ink, soft muted slate for italic subtitles, the

src/main/java/com/demcha/compose/document/templates/cv/v2/theme/CvSpacing.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,30 @@ public static CvSpacing editorialBlue() {
279279
3.0); // entrySeparation
280280
}
281281

282+
/**
283+
* Spacing for the Panel preset: card-led layout that has to fit
284+
* Header / Profile / two-column row / Additional on one A4 page,
285+
* so paddings and inter-card gaps are tight by design. Corner
286+
* radius and accent rule width match the V1 ProductLeader tokens.
287+
*/
288+
public static CvSpacing panel() {
289+
return new CvSpacing(
290+
6, // pageFlowSpacing (tight inter-card gap)
291+
3, // sectionBodySpacing (inside a card)
292+
DocumentInsets.zero(), // sectionBodyPadding (the card supplies its own padding)
293+
DocumentInsets.zero(), // headlinePadding
294+
DocumentInsets.zero(), // contactPadding
295+
7.0, // bannerCornerRadius (V1 CORNER_RADIUS)
296+
8.0, // bannerInnerPadding (compact card padding)
297+
DocumentInsets.zero(), // bannerMargin
298+
2.2, // accentRuleWidth (V1 ACCENT_HEIGHT)
299+
1.0, // paragraphMarginTop
300+
8.0, // entryHeaderRowSpacing
301+
1.0, // entryTitleWeight
302+
0.45, // entryDateWeight
303+
2.0); // entrySeparation
304+
}
305+
282306
/**
283307
* Spacing for the Executive preset: generous executive feel with
284308
* an 8pt page-flow rhythm, compact module bodies, and a 1.1pt

src/main/java/com/demcha/compose/document/templates/cv/v2/theme/CvTheme.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,21 @@ public static CvTheme editorialBlue() {
167167
CvDecoration.classic());
168168
}
169169

170+
/**
171+
* The "Panel" look — Poppins headlines + Lato body, pale teal
172+
* header card and module panels with thin teal stroke, deep navy
173+
* masthead text, and teal section headings with a small accent
174+
* strip beneath each title. Visual signature ported from the v1
175+
* {@code PanelCvTemplateComposer} (ProductLeader tokens).
176+
*/
177+
public static CvTheme panel() {
178+
return new CvTheme(
179+
CvPalette.panel(),
180+
CvTypography.panel(),
181+
CvSpacing.panel(),
182+
CvDecoration.classic());
183+
}
184+
170185
/**
171186
* The "Executive" look — Poppins masthead + Lato body, deep slate
172187
* primary, warm bronze accent on module headings and contact

src/main/java/com/demcha/compose/document/templates/cv/v2/theme/CvTypography.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,26 @@ public static CvTypography editorialBlue() {
191191
1.45); // line spacing
192192
}
193193

194+
/**
195+
* Poppins headline + Lato body scale ported from the v1
196+
* {@code PanelCvTemplateComposer} (ProductLeader tokens): a 22pt
197+
* uppercase name in the tinted header card, a 10.4pt section-title
198+
* slot for the teal module headings, and a 9.4pt body with 1.2
199+
* line spacing tuned for the dense card layout.
200+
*/
201+
public static CvTypography panel() {
202+
return new CvTypography(
203+
FontName.POPPINS, FontName.LATO,
204+
22.0, // headline (centered uppercase name)
205+
8.9, // contact (V1 META_SIZE = body - 0.5)
206+
10.4, // banner / module title (V1 SECTION_SIZE)
207+
9.4, // entry title
208+
9.4, // entry date
209+
9.0, // entry subtitle (italic)
210+
9.4, // body (V1 BODY_SIZE)
211+
1.2); // line spacing
212+
}
213+
194214
/**
195215
* Poppins headline + Lato body scale ported from the v1
196216
* {@code ExecutiveSlateCvTemplate}: a 24pt uppercase masthead, a

src/main/java/com/demcha/compose/document/templates/widgets/CardWidget.java

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.demcha.compose.document.templates.widgets;
22

3+
import com.demcha.compose.document.dsl.PageFlowBuilder;
34
import com.demcha.compose.document.dsl.SectionBuilder;
45
import com.demcha.compose.document.style.DocumentColor;
56
import com.demcha.compose.document.style.DocumentCornerRadius;
@@ -31,21 +32,47 @@ public static void render(SectionBuilder parent,
3132
Style safeStyle = style == null ? Style.builder().build() : style;
3233

3334
parent.addSection(name, card -> {
34-
card.spacing(safeStyle.spacing())
35-
.padding(safeStyle.padding());
36-
if (safeStyle.fillColor() != null) {
37-
card.fillColor(safeStyle.fillColor());
38-
}
39-
if (safeStyle.stroke() != null) {
40-
card.stroke(safeStyle.stroke());
41-
}
42-
if (safeStyle.cornerRadius() != null) {
43-
card.cornerRadius(safeStyle.cornerRadius());
44-
}
35+
applyStyle(card, safeStyle);
36+
content.accept(card);
37+
});
38+
}
39+
40+
/**
41+
* Top-level overload — renders the card as a page-flow section so
42+
* presets can place full-width cards directly under
43+
* {@link PageFlowBuilder} without wrapping them in a parent
44+
* section. Visual shell behaves identically to the
45+
* {@link #render(SectionBuilder, String, Style, Consumer)}
46+
* variant.
47+
*/
48+
public static void render(PageFlowBuilder flow,
49+
String name,
50+
Style style,
51+
Consumer<SectionBuilder> content) {
52+
Objects.requireNonNull(flow, "flow");
53+
Objects.requireNonNull(content, "content");
54+
Style safeStyle = style == null ? Style.builder().build() : style;
55+
56+
flow.addSection(name, card -> {
57+
applyStyle(card, safeStyle);
4558
content.accept(card);
4659
});
4760
}
4861

62+
private static void applyStyle(SectionBuilder card, Style style) {
63+
card.spacing(style.spacing())
64+
.padding(style.padding());
65+
if (style.fillColor() != null) {
66+
card.fillColor(style.fillColor());
67+
}
68+
if (style.stroke() != null) {
69+
card.stroke(style.stroke());
70+
}
71+
if (style.cornerRadius() != null) {
72+
card.cornerRadius(style.cornerRadius());
73+
}
74+
}
75+
4976
/**
5077
* Visual shell options for {@link CardWidget}.
5178
*/

src/test/java/com/demcha/compose/document/templates/cv/v2/presets/CvV2VisualParityTest.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,10 @@ private static Stream<Arguments> presets() {
114114
(Supplier<DocumentTemplate<CvDocument>>) CompactMono::create),
115115
Arguments.of("executive",
116116
Executive.RECOMMENDED_MARGIN,
117-
(Supplier<DocumentTemplate<CvDocument>>) Executive::create));
117+
(Supplier<DocumentTemplate<CvDocument>>) Executive::create),
118+
Arguments.of("panel",
119+
Panel.RECOMMENDED_MARGIN,
120+
(Supplier<DocumentTemplate<CvDocument>>) Panel::create));
118121
}
119122

120123
/**
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package com.demcha.compose.document.templates.cv.v2.presets;
2+
3+
import com.demcha.compose.GraphCompose;
4+
import com.demcha.compose.document.api.DocumentSession;
5+
import com.demcha.compose.document.style.DocumentInsets;
6+
import com.demcha.compose.document.templates.api.DocumentTemplate;
7+
import com.demcha.compose.document.templates.cv.v2.data.CvDocument;
8+
import com.demcha.compose.document.templates.cv.v2.data.CvIdentity;
9+
import com.demcha.compose.document.templates.cv.v2.data.EntriesSection;
10+
import com.demcha.compose.document.templates.cv.v2.data.ParagraphSection;
11+
import com.demcha.compose.document.templates.cv.v2.data.RowStyle;
12+
import com.demcha.compose.document.templates.cv.v2.data.RowsSection;
13+
import com.demcha.compose.document.templates.cv.v2.data.SkillsSection;
14+
import com.demcha.compose.document.templates.cv.v2.theme.CvTheme;
15+
import org.junit.jupiter.api.Test;
16+
17+
import static org.assertj.core.api.Assertions.assertThat;
18+
19+
/**
20+
* Smoke test for the v2 Panel preset. Covers the header card with
21+
* optional job title + link row, plus the two-column row composition
22+
* fed through {@link com.demcha.compose.document.templates.cv.v2.components.SectionLookup}
23+
* and {@link com.demcha.compose.document.templates.cv.v2.components.SectionDispatcher}.
24+
*/
25+
class PanelSmokeTest {
26+
27+
@Test
28+
void exposes_stable_identity() {
29+
DocumentTemplate<CvDocument> template = Panel.create();
30+
assertThat(template.id()).isEqualTo("panel");
31+
assertThat(template.displayName()).isEqualTo("Panel");
32+
}
33+
34+
@Test
35+
void default_factory_renders_full_document() throws Exception {
36+
renderAndAssertNonEmpty(Panel.create(), fullDocument());
37+
}
38+
39+
@Test
40+
void custom_theme_factory_renders() throws Exception {
41+
renderAndAssertNonEmpty(Panel.create(CvTheme.panel()), fullDocument());
42+
}
43+
44+
private static void renderAndAssertNonEmpty(
45+
DocumentTemplate<CvDocument> template,
46+
CvDocument doc) throws Exception {
47+
try (DocumentSession session = GraphCompose.document()
48+
.pageSize(420, 595)
49+
.margin(DocumentInsets.of(18))
50+
.create()) {
51+
template.compose(session, doc);
52+
assertThat(session.roots()).isNotEmpty();
53+
}
54+
}
55+
56+
private static CvDocument fullDocument() {
57+
return CvDocument.builder()
58+
.identity(CvIdentity.builder()
59+
.name("Jane", "Doe")
60+
.jobTitle("Product Lead")
61+
.contact("+44 0", "j@d.com", "London")
62+
.link("LinkedIn", "https://linkedin.com/in/jane-doe")
63+
.build())
64+
.sections(
65+
new ParagraphSection("Professional Summary",
66+
"Builds **reliable** product platforms."),
67+
SkillsSection.builder("Technical Skills")
68+
.group("Languages", "Java 21", "Kotlin")
69+
.group("Testing", "JUnit 5", "AssertJ")
70+
.build(),
71+
EntriesSection.builder("Education & Certifications")
72+
.entry("MSc Computer Science",
73+
"University of Manchester",
74+
"2019-2021",
75+
"Distinction.")
76+
.build(),
77+
RowsSection.builder("Projects", RowStyle.BULLETED_STACKED)
78+
.row("GraphCompose (Java, PDFBox)",
79+
"Declarative PDF layout engine.")
80+
.build(),
81+
EntriesSection.builder("Professional Experience")
82+
.entry("Lead", "Acme", "2021-2024",
83+
"Built rendering services.")
84+
.build(),
85+
RowsSection.builder("Additional Information", RowStyle.PLAIN)
86+
.row("Languages", "English, German")
87+
.build())
88+
.build();
89+
}
90+
}
150 KB
Loading

0 commit comments

Comments
 (0)