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
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,29 @@ follow semantic versioning; release dates are ISO 8601.

### Templates

- **The first CV preset that owns its page: `ProfessionalSidebar`.** A one-page CV
in two columns on the Barlow Condensed / Lato pair — a navy monogram plate over a
pale sidebar carrying the contact channels, meter-bar skills, an education rail with
dot markers and five-dot language ratings, beside a white column of the tracked name,
the profile, the roles held with bulleted highlights, the projects and the references
note. Ships as `cv.presets.ProfessionalSidebar` on the existing `CvDocument` model —
no model change was needed — porting the rendered layout of the published standalone
`professional-sidebar-cv` template. The preset owns its page: a 491.6 x 737.28pt
sheet with no margin, the page fill and the pale sidebar painted as page backgrounds. The
sheet holds one page — its two columns are a single atomic row, so a CV longer than the
sheet raises `AtomicNodeTooLargeException` rather than flowing onto a second page or
silently dropping entries the way the capped sidebar presets do. The class documentation
says so, points at `TimelineMinimal` for a preset that splits its own columns, and
`docs/templates/v2-layered/using-templates.md` carries it beside the capped presets.
Sections reach their berth by title rather than by `Slot`,
because the columns are fixed; skills and languages are both `SkillsSection`s drawn
differently, and a level the document omits draws the name alone. The contact channels
come off `CvIdentity`, with `tel:` and `mailto:` targets built from the values and the
packaged marks chosen per channel. Guarded by a smoke test (including the identity-only
document, the unlevelled skill, the PDF link targets, the one-page limit and the fields
this design has no place for), an exact layout snapshot and a pixel-parity gate; the
examples showcase gains `cv-professional-sidebar-v2`.

- **A second structured proposal preset: `EditorialProposal`.** The same document the
`NorthlineProposal` preset renders, in a different hand: an orange accent, section
headings set in the display serif over short accent rules instead of in the body sans
Expand Down
Binary file not shown.
7 changes: 7 additions & 0 deletions docs/templates/v2-layered/using-templates.md
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,13 @@ whole or the paginator raises `AtomicNodeTooLargeException`. Lifting a
cap without teaching the preset to pick its own page boundaries turns a
CV that silently lost an entry into one that fails to render.

`ProfessionalSidebar.create()` is the fourth composition for a fixed
amount of content and the one that caps nothing: it reproduces a
specific one-page sheet, so a CV longer than that sheet raises
`AtomicNodeTooLargeException` instead of losing an entry to a cap. Size
the document to it — roughly five or six roles with three or four
highlights each, alongside the sidebar blocks.

If the document's length is the author's rather than the template's,
pick a preset that paginates — `TimelineMinimal` splits its own columns
and carries every entry it is given onto as many pages as it needs.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@
import com.demcha.examples.templates.cv.v2.CvPanelExample;
import com.demcha.examples.templates.cv.v2.CvSidebarPortraitExample;
import com.demcha.examples.templates.cv.v2.CvTimelineMinimalExample;
import com.demcha.examples.templates.cv.v2.ProfessionalSidebarExample;
import com.demcha.examples.templates.invoice.ClassicInvoiceV2Example;
import com.demcha.examples.templates.invoice.ConsultingInvoiceV2Example;
import com.demcha.examples.templates.invoice.InvoiceCinematicFileExample;
Expand Down Expand Up @@ -145,6 +146,7 @@ public static void main(String[] args) throws Exception {
System.out.println("Generated: " + CvPanelExample.generate());
System.out.println("Generated: " + CvSidebarPortraitExample.generate());
System.out.println("Generated: " + CvTimelineMinimalExample.generate());
System.out.println("Generated: " + ProfessionalSidebarExample.generate());

// Cover letters (v2 layered — 15 paired letters, one per CV preset)
System.out.println("Generated: " + CvBlueBannerLetterV2Example.generate());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package com.demcha.examples.support;

import com.demcha.compose.document.templates.core.identity.Contact;
import com.demcha.compose.document.templates.core.identity.Link;
import com.demcha.compose.document.templates.cv.data.CvDocument;
import com.demcha.compose.document.templates.cv.data.CvIdentity;
import com.demcha.compose.document.templates.cv.data.CvSkill;
import com.demcha.compose.document.templates.cv.data.EntriesSection;
import com.demcha.compose.document.templates.cv.data.ParagraphSection;
import com.demcha.compose.document.templates.cv.data.SkillGroup;
import com.demcha.compose.document.templates.cv.data.SkillsSection;
import com.demcha.compose.document.templates.cv.data.Slot;

import java.util.List;

/**
* Sample data for the Professional Sidebar CV example.
*
* <p>Sized to the design: the preset draws a fixed one-page sheet, and a CV
* much longer than this one does not compose at all — the two columns are a
* single atomic row, so it raises {@code AtomicNodeTooLargeException}. The
* sidebar's one-line rows are sized to it too: a degree, skill or language
* longer than the narrow column measures overflows the row it is anchored in
* rather than wrapping inside it.</p>
*/
public final class ProfessionalSidebarSampleData {

/** The en dash the design sets between dates. */
private static final String DASH = "–";

private ProfessionalSidebarSampleData() {
}

/**
* A backend engineer's one-page CV.
*
* @return the sample document
*/
public static CvDocument sample() {
return CvDocument.builder()
.identity(CvIdentity.builder()
.name("MARTA", "LINDQVIST")
.jobTitle("SENIOR BACKEND ENGINEER")
.contact(new Contact("+46 8 123 456 78",
"marta.lindqvist@example.com",
"Stockholm, Sweden"))
.link(new Link("linkedin.com/in/mlindqvist",
"https://linkedin.com/in/mlindqvist"))
.link(new Link("martalindqvist.dev",
"https://martalindqvist.dev"))
.build())
.section(Slot.MAIN, new ParagraphSection("PROFILE",
"Backend engineer with nine years on payment and identity systems,"
+ " most of it owning services end to end: API design, the data"
+ " model underneath, and the on-call rota that follows."
+ " Comfortable in a team that ships small and often, and the"
+ " one who writes the runbook nobody asked for."))
.section(Slot.SIDEBAR, SkillsSection.of("SKILLS",
new SkillGroup("Core", List.of(
CvSkill.of("Java", 0.9),
CvSkill.of("Kotlin", 0.72),
CvSkill.of("Spring Boot", 0.86),
CvSkill.of("PostgreSQL", 0.8),
CvSkill.of("Kafka", 0.68),
CvSkill.of("Kubernetes", 0.62),
CvSkill.of("Terraform", 0.55),
CvSkill.of("Observability", 0.74),
CvSkill.of("Threat Modelling", 0.6),
CvSkill.of("Mentoring", 0.78)))))
.section(Slot.SIDEBAR, EntriesSection.builder("EDUCATION")
.entry("MSc COMPUTER SCIENCE", "KTH Royal Institute",
"2014 " + DASH + " 2016", "")
.entry("BSc SOFTWARE DESIGN", "Uppsala University",
"2011 " + DASH + " 2014", "")
.build())
.section(Slot.SIDEBAR, SkillsSection.of("LANGUAGES",
new SkillGroup("Spoken", List.of(
CvSkill.of("Swedish", 1.0),
CvSkill.of("English", 1.0),
CvSkill.of("German", 0.6)))))
.section(Slot.MAIN, EntriesSection.builder("EXPERIENCE")
.entry("SENIOR BACKEND ENGINEER",
"Nordkassa | Stockholm, SE",
"2021 " + DASH + " Present",
String.join("\n",
"Led the split of the settlement monolith into four"
+ " services with no customer-visible downtime.",
"Cut p99 authorisation latency from 780ms to 210ms by"
+ " reworking the ledger write path.",
"Introduced contract tests between payments and risk,"
+ " ending a class of release-day rollbacks.",
"Mentors three engineers; runs the internal design"
+ " review."))
.entry("BACKEND ENGINEER",
"Vinge Identity | Gothenburg, SE",
"2018 " + DASH + " 2021",
String.join("\n",
"Built the OAuth2 and OIDC provider now serving eleven"
+ " internal products.",
"Moved session storage to Redis with a migration that"
+ " ran live for six weeks.",
"Wrote the audit pipeline the compliance team still"
+ " reports from."))
.entry("SOFTWARE ENGINEER",
"Almgren Data | Uppsala, SE",
"2016 " + DASH + " 2018",
String.join("\n",
"Delivered ingest and reporting services for retail"
+ " forecasting customers.",
"Replaced nightly batch reconciliation with a streaming"
+ " job on Kafka."))
.build())
.section(Slot.MAIN, EntriesSection.builder("PROJECTS")
.entry("LEDGERKIT", "", "2024",
"Open-source double-entry ledger for JVM services, with a"
+ " property-based test suite over the posting rules.")
.entry("ONCALL DIGEST", "", "2022",
"Weekly incident summariser that reads alert history and drafts"
+ " the retrospective agenda.")
.build())
.section(Slot.MAIN, new ParagraphSection("REFERENCES",
"Available on request."))
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ record Entry(String title, String description, List<String> tags, String codeUrl
cv("cv-minimal-underlined-v2", "CvMinimalUnderlinedExample", "Minimal Underlined", "Single-column layout with underlined section titles and tight whitespace — minimalist reference shape.", "minimal");
cv("cv-mint-editorial-v2", "CvMintEditorialExample", "Mint Editorial", "Magazine-style editorial CV with mint accent palette and two-column body.", "editorial", "mint");
cv("cv-mint-editorial-v2-custom", "CvMintEditorialCustomExample", "Mint Editorial (custom band)", "The same preset with one colour changed through its Options — a kraft-paper masthead band, everything else left at the preset's defaults.", "editorial", "mint");
cv("cv-professional-sidebar-v2", "ProfessionalSidebarExample", "Professional Sidebar", "Navy monogram plate over a pale sidebar of contact marks, skill meters, an education rail and language ratings, beside a white column of profile, roles and projects.", "sidebar", "navy");

// ===== Templates / Cover Letter (v2 layered, paired 1:1 with CV) =====
// Registered directly: letter() points at the layered preset examples under
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.demcha.examples.templates.cv.v2;

import com.demcha.compose.GraphCompose;
import com.demcha.compose.document.api.DocumentSession;
import com.demcha.compose.document.templates.api.DocumentTemplate;
import com.demcha.compose.document.templates.cv.data.CvDocument;
import com.demcha.compose.document.templates.cv.presets.ProfessionalSidebar;
import com.demcha.examples.support.ExampleOutputPaths;
import com.demcha.examples.support.ProfessionalSidebarSampleData;

import java.nio.file.Path;

/**
* Renders the layered {@code cv.v2} Professional Sidebar preset against the
* backend-engineer sample.
*
* <p>Output:
* {@code examples/target/generated-pdfs/templates/cv/cv-professional-sidebar-v2.pdf}.</p>
*
* <p>The preset owns its page geometry — a 491.6 x 737.28pt sheet with no
* margin — so the session starts unconfigured.</p>
*/
public final class ProfessionalSidebarExample {

private ProfessionalSidebarExample() {
}

/**
* @return absolute path of the rendered PDF
* @throws Exception if rendering fails
*/
public static Path generate() throws Exception {
Path outputFile = ExampleOutputPaths.prepare(
"templates/cv", "cv-professional-sidebar-v2.pdf");
CvDocument doc = ProfessionalSidebarSampleData.sample();
DocumentTemplate<CvDocument> template = ProfessionalSidebar.create();

try (DocumentSession document = GraphCompose.document(outputFile).create()) {
template.compose(document, doc);
document.buildPdf();
}
return outputFile;
}

/**
* @param args ignored
* @throws Exception if rendering fails
*/
public static void main(String[] args) throws Exception {
System.out.println("Generated: " + generate());
}
}
Loading
Loading