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
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.demcha.examples.templates.cv.v2;

import com.demcha.compose.GraphCompose;
import com.demcha.compose.document.api.DocumentPageSize;
import com.demcha.compose.document.api.DocumentSession;
import com.demcha.compose.document.templates.api.DocumentTemplate;
import com.demcha.compose.document.templates.cv.v2.data.CvDocument;
import com.demcha.compose.document.templates.cv.v2.presets.Executive;
import com.demcha.examples.support.ExampleDataFactory;
import com.demcha.examples.support.ExampleOutputPaths;

import java.nio.file.Path;

/**
* Renders the v2 Executive CV preset against the shared grouped
* skills sample data — uppercase Poppins slate masthead, Lato meta +
* link row, full-width muted rule, and bronze Poppins module
* headings over a single-column body.
*
* <p>Output:
* {@code examples/target/generated-pdfs/templates/cv/cv-executive-v2.pdf}.</p>
*/
public final class CvExecutiveExample {

private CvExecutiveExample() {
}

public static Path generate() throws Exception {
Path outputFile = ExampleOutputPaths.prepare(
"templates/cv", "cv-executive-v2.pdf");
CvDocument doc = ExampleDataFactory.sampleCvDocumentV2();
DocumentTemplate<CvDocument> template = Executive.create();

float m = (float) Executive.RECOMMENDED_MARGIN;
try (DocumentSession document = GraphCompose.document(outputFile)
.pageSize(DocumentPageSize.A4)
.margin(m, m, m, m)
.create()) {
template.compose(document, doc);
document.buildPdf();
}
return outputFile;
}

public static void main(String[] args) throws Exception {
System.out.println("Generated: " + generate());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
package com.demcha.compose.document.templates.cv.v2.presets;

import com.demcha.compose.document.api.DocumentSession;
import com.demcha.compose.document.dsl.PageFlowBuilder;
import com.demcha.compose.document.dsl.SectionBuilder;
import com.demcha.compose.document.node.DocumentLinkOptions;
import com.demcha.compose.document.node.TextAlign;
import com.demcha.compose.document.style.DocumentColor;
import com.demcha.compose.document.style.DocumentInsets;
import com.demcha.compose.document.style.DocumentTextDecoration;
import com.demcha.compose.document.style.DocumentTextStyle;
import com.demcha.compose.document.templates.api.DocumentTemplate;
import com.demcha.compose.document.templates.cv.v2.components.CvTextStyles;
import com.demcha.compose.document.templates.cv.v2.components.SectionDispatcher;
import com.demcha.compose.document.templates.cv.v2.data.CvDocument;
import com.demcha.compose.document.templates.cv.v2.data.CvIdentity;
import com.demcha.compose.document.templates.cv.v2.data.CvLink;
import com.demcha.compose.document.templates.cv.v2.data.CvSection;
import com.demcha.compose.document.templates.cv.v2.data.Slot;
import com.demcha.compose.document.templates.cv.v2.theme.CvTheme;
import com.demcha.compose.document.templates.cv.v2.widgets.Headline;
import com.demcha.compose.document.templates.cv.v2.widgets.SectionHeader;
import com.demcha.compose.font.FontName;

import java.util.List;
import java.util.Locale;
import java.util.Objects;

/**
* v2 port of the legacy "Executive" CV preset.
*
* <p>Polished business CV with restrained slate typography, a compact
* left-aligned header (UPPERCASE name in deep slate, meta line, link
* row, full-width muted rule below), and warm bronze module headings
* over a single-column body. Visual signature ported from the legacy
* {@code ExecutiveSlateCvTemplate}: Poppins for headings, Lato for
* body, slate primary, bronze accent.</p>
*
* <p>The preset stays a thin orchestrator — the header block is
* preset-local inline DSL because V1 splits meta and links across two
* rows (no v2 contact widget has that exact shape today), while
* everything below the header reuses {@link SectionHeader#flat} for
* the bronze module titles and {@link SectionDispatcher#renderBody}
* for the body of every {@code CvSection} subtype.</p>
*/
public final class Executive {

/** Stable template identifier. */
public static final String ID = "executive";

/** Human-readable display name. */
public static final String DISPLAY_NAME = "Executive";

/** Recommended page margin (in points) — generous for an executive feel. */
public static final double RECOMMENDED_MARGIN = 28.0;

/**
* Deeper slate used by the V1 Executive masthead. The theme's
* {@code palette().ink()} is the body-text slate; this is a
* preset-local fifth token because no other v2 preset shares it.
*/
private static final DocumentColor PRIMARY_NAME =
DocumentColor.rgb(24, 35, 51);

/**
* Warm bronze used by the V1 Executive module headings and the
* underlined contact links. Preset-local sixth token.
*/
private static final DocumentColor ACCENT =
DocumentColor.rgb(172, 112, 55);

private Executive() {
}

/**
* Builds the preset with its Executive theme.
*/
public static DocumentTemplate<CvDocument> create() {
return create(CvTheme.executive());
}

/**
* Builds the preset with a caller-supplied theme.
*/
public static DocumentTemplate<CvDocument> create(CvTheme theme) {
Objects.requireNonNull(theme, "theme");
return new Template(theme);
}

private static final class Template implements DocumentTemplate<CvDocument> {

private final CvTheme theme;

Template(CvTheme theme) {
this.theme = theme;
}

@Override
public String id() {
return ID;
}

@Override
public String displayName() {
return DISPLAY_NAME;
}

@Override
public void compose(DocumentSession document, CvDocument doc) {
Objects.requireNonNull(document, "document");
Objects.requireNonNull(doc, "doc");

double width = document.canvas().innerWidth();
PageFlowBuilder flow = document.dsl()
.pageFlow()
.name("CvV2ExecutiveRoot")
.spacing(theme.spacing().pageFlowSpacing());

addHeader(flow, doc.identity(), width);

List<CvSection> sections = doc.sectionsIn(Slot.MAIN);
for (int i = 0; i < sections.size(); i++) {
CvSection sec = sections.get(i);
int idx = i;
flow.addSection("CvV2ExecutiveTitle_" + idx, host ->
SectionHeader.flat(host,
sec.title().toUpperCase(Locale.ROOT),
ACCENT, theme));
flow.addSection("CvV2ExecutiveBody_" + idx, host ->
SectionDispatcher.renderBody(host, sec, theme));
}

flow.build();
}

private void addHeader(PageFlowBuilder flow, CvIdentity identity,
double width) {
flow.addSection("CvV2ExecutiveHeader", section -> {
section.spacing(2)
.padding(DocumentInsets.zero());
Headline.uppercaseLeftAligned(section, identity.name(), theme,
nameStyle());
String meta = joinPipe(identity.contact().address(),
identity.contact().phone());
if (!meta.isBlank()) {
section.addParagraph(paragraph -> paragraph
.text(meta)
.textStyle(metaStyle())
.align(TextAlign.LEFT)
.margin(DocumentInsets.top(2)));
}
addLinkRow(section, identity);
section.addLine(line -> line
.name("CvV2ExecutiveHeaderRule")
.horizontal(width)
.color(theme.palette().rule())
.thickness(theme.spacing().accentRuleWidth())
.margin(DocumentInsets.top(5)));
});
}

private void addLinkRow(SectionBuilder section, CvIdentity identity) {
boolean hasEmail = !identity.contact().email().isBlank();
boolean hasLinks = !identity.links().isEmpty();
if (!hasEmail && !hasLinks) {
return;
}
DocumentTextStyle bodyStyle = linkRowBodyStyle();
DocumentTextStyle linkStyle = linkRowLinkStyle();
section.addParagraph(paragraph -> paragraph
.textStyle(bodyStyle)
.align(TextAlign.LEFT)
.margin(DocumentInsets.top(1))
.rich(rich -> {
boolean first = true;
String email = identity.contact().email();
if (!email.isBlank()) {
rich.with(email, linkStyle,
new DocumentLinkOptions("mailto:" + email));
first = false;
}
for (CvLink link : identity.links()) {
if (link.label().isBlank()) {
continue;
}
if (!first) {
rich.style(" | ", bodyStyle);
}
first = false;
if (link.url().isBlank()) {
rich.style(link.label(), bodyStyle);
} else {
rich.with(link.label(), linkStyle,
new DocumentLinkOptions(link.url()));
}
}
}));
}

private DocumentTextStyle nameStyle() {
return CvTextStyles.of(FontName.POPPINS,
theme.typography().sizeHeadline(),
DocumentTextDecoration.BOLD,
PRIMARY_NAME);
}

private DocumentTextStyle metaStyle() {
return CvTextStyles.of(theme.typography().bodyFont(),
theme.typography().sizeContact(),
DocumentTextDecoration.DEFAULT,
theme.palette().ink());
}

private DocumentTextStyle linkRowBodyStyle() {
return CvTextStyles.of(theme.typography().bodyFont(),
theme.typography().sizeBody(),
DocumentTextDecoration.DEFAULT,
theme.palette().ink());
}

private DocumentTextStyle linkRowLinkStyle() {
return CvTextStyles.of(theme.typography().bodyFont(),
theme.typography().sizeBody(),
DocumentTextDecoration.UNDERLINE,
ACCENT);
}

private static String joinPipe(String... parts) {
StringBuilder sb = new StringBuilder();
for (String part : parts) {
if (part == null || part.isBlank()) {
continue;
}
if (sb.length() > 0) {
sb.append(" | ");
}
sb.append(part.trim());
}
return sb.toString();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -122,4 +122,22 @@ public static CvPalette editorialBlue() {
DocumentColor.rgb(86, 136, 255),
DocumentColor.rgb(193, 201, 211));
}

/**
* Executive palette ported from the v1 {@code ExecutiveSlateCvTemplate}:
* mid-slate body ink, soft muted slate for italic subtitles, the
* V1 muted-rule grey for thin separators, and a fallback banner
* tone inherited from the classic palette (the preset does not
* draw banner panels). The display name colour (deeper slate
* rgb(24,35,51)) and bronze accent (rgb(172,112,55)) are
* preset-local because they are the fifth and sixth tokens —
* other v2 presets do not share them today.
*/
public static CvPalette executive() {
return new CvPalette(
DocumentColor.rgb(49, 58, 72), // ink — V1 BODY slate
DocumentColor.rgb(105, 115, 130), // muted — slightly lighter slate
DocumentColor.rgb(193, 201, 211), // rule — V1 MUTED_RULE
DocumentColor.rgb(220, 226, 230)); // banner — unused, inherits classic
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -278,4 +278,27 @@ public static CvSpacing editorialBlue() {
0.45, // entryDateWeight
3.0); // entrySeparation
}

/**
* Spacing for the Executive preset: generous executive feel with
* an 8pt page-flow rhythm, compact module bodies, and a 1.1pt
* full-width rule under the masthead.
*/
public static CvSpacing executive() {
return new CvSpacing(
8, // pageFlowSpacing
3, // sectionBodySpacing
DocumentInsets.zero(), // sectionBodyPadding
DocumentInsets.zero(), // headlinePadding
DocumentInsets.top(2), // contactPadding (unused — preset composes header inline)
0.0, // bannerCornerRadius (unused)
5.0, // bannerInnerPadding (unused)
DocumentInsets.zero(), // bannerMargin (unused)
1.1, // accentRuleWidth (V1 header rule)
2.0, // paragraphMarginTop
8.0, // entryHeaderRowSpacing
1.0, // entryTitleWeight
0.45, // entryDateWeight
3.0); // entrySeparation
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,21 @@ public static CvTheme editorialBlue() {
CvSpacing.editorialBlue(),
CvDecoration.classic());
}

/**
* The "Executive" look — Poppins masthead + Lato body, deep slate
* primary, warm bronze accent on module headings and contact
* links, and a thin full-width muted rule under the header.
* Visual signature ported from the legacy
* {@code ExecutiveSlateCvTemplate}.
*/
public static CvTheme executive() {
return new CvTheme(
CvPalette.executive(),
CvTypography.executive(),
CvSpacing.executive(),
CvDecoration.classic());
}
// -- pre-built text-style helpers ------------------------------------
// Renderers ask the theme for an already-composed DocumentTextStyle
// instead of re-assembling font + size + decoration + colour every
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,4 +190,24 @@ public static CvTypography editorialBlue() {
9.4, // body
1.45); // line spacing
}

/**
* Poppins headline + Lato body scale ported from the v1
* {@code ExecutiveSlateCvTemplate}: a 24pt uppercase masthead, a
* 10.8pt section-title slot driving the bronze module headings,
* and a 9.5pt body with 1.25 line-spacing tuned for an executive
* single-column resume density.
*/
public static CvTypography executive() {
return new CvTypography(
FontName.POPPINS, FontName.LATO,
24.0, // headline (uppercase masthead)
9.1, // contact meta (V1 META_SIZE = body - 0.4)
10.8, // banner / section title (V1 SECTION_SIZE)
9.5, // entry title
9.5, // entry date
9.0, // entry subtitle (italic)
9.5, // body (V1 BODY_SIZE)
1.25); // line spacing
}
}
Loading