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

### Templates

- **A clinical CV preset in five bands: `TealPulse`.** A one-page sheet whose mark is a
heart crossed by a flat pulse, beside a letter-spaced name over a contact strip
divided by short rules; then a two-column body carrying the competencies as dotted
lines beside the summary and the roles under badged headings; then a three-column
closing band for the degree, the certifications and the facts; and a tracked line
under a rule that ends in a small heart. Ships as `cv.presets.TealPulse` on the
existing `CvDocument` model with **no model change at all**, porting the rendered
layout of a published standalone template. **It sets its own page**, unlike every CV
preset before it: the design was drawn on a raster whose proportion is not A4's and
every length is a share of that grid, so a caller's page size and margin are
overwritten rather than followed. Six berths reach their sections by title, including
a tagline berth whose body is drawn and whose title is not — the title is how a
document names the berth. Both vertical rules are the left border of the column to
their right rather than lines placed beside them, so they are the grid: change a
weight and the rules follow. The main headings are laid over their own rule with the
paper knocked out behind every letter, because a row cannot nest in a row cell and the
rule's visible length still has to follow the words. **What happens past one page is
not what the other ported CV presets do:** the bands are stacked in the page flow, so
a CV with more roles than the design holds carries the closing band onto a second page
rather than losing anything, while the body row itself is atomic and a body taller
than a page raises `AtomicNodeTooLargeException`. As on `TerracottaRail`, a link is
drawn as its own label with the address behind it, which is the one deliberate
departure from the ported sheet — writing the URL out makes the strip's gaps depend on
how long a profile is called — measured at six of 121 nodes shifting sideways and
7 724 of 1 987 720 pixels, with nothing moving vertically. Guarded by a smoke test
(including the empty document, the page override, the tagline's title staying off the
sheet, the link targets, a linked role and degree, the run onto a second page and the
refusal of a body taller than one), an exact layout snapshot and a pixel-parity gate;
the examples showcase gains `cv-teal-pulse-v2`.

- **An architect's two-column CV preset: `TerracottaRail`.** A one-page A4 sheet whose
narrow column carries a serif monogram over a terracotta rule, the contact channels
behind their marks, two bulleted lists and a block of closing facts, beside a wide
Expand Down
Binary file added assets/readme/examples/cv-teal-pulse-v2.pdf
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
import com.demcha.examples.templates.coverletter.CvSidebarPortraitLetterV2Example;
import com.demcha.examples.templates.coverletter.CvTimelineMinimalLetterV2Example;
import com.demcha.examples.templates.cv.v2.CharcoalGoldExample;
import com.demcha.examples.templates.cv.v2.TealPulseExample;
import com.demcha.examples.templates.cv.v2.TerracottaRailExample;
import com.demcha.examples.templates.cv.v2.CvBlueBannerExample;
import com.demcha.examples.templates.cv.v2.CvBoxedV2Example;
Expand Down Expand Up @@ -156,6 +157,7 @@ public static void main(String[] args) throws Exception {
System.out.println("Generated: " + SerifHeadlineExample.generate());
System.out.println("Generated: " + CharcoalGoldExample.generate());
System.out.println("Generated: " + TerracottaRailExample.generate());
System.out.println("Generated: " + TealPulseExample.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
Expand Up @@ -65,6 +65,7 @@ record Entry(String title, String description, List<String> tags, String codeUrl
cv("cv-serif-headline-v2", "SerifHeadlineExample", "Serif Headline", "Volkhov masthead over a two-column body: roles on a timeline rail and marked project cards beside degrees and skill meters, closing with full-width certification and achievement bands.", "serif", "two-column");
cv("cv-charcoal-gold-v2", "CharcoalGoldExample", "Charcoal Gold", "Charcoal sidebar with a ringed photograph, rated skills and languages, beside a paper column with a two-tone name, a dated experience rail and paired credential columns.", "sidebar", "photo", "gold");
cv("cv-terracotta-rail-v2", "TerracottaRailExample", "Terracotta Rail", "Serif monogram over a terracotta rule, contact channels behind their marks and two bulleted lists, beside a letter-spaced masthead, roles on a ringed rail, a projects grid and the degrees.", "sidebar", "monogram", "terracotta");
cv("cv-teal-pulse-v2", "TealPulseExample", "Teal Pulse", "Clinical sheet in five bands: a heart crossed by a pulse beside the name, a contact strip on rules, competencies beside the summary and roles, and a three-column closing band over a tracked tagline.", "clinical", "badges", "teal");

// ===== 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,167 @@
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.CvEntry;
import com.demcha.compose.document.templates.cv.data.CvIdentity;
import com.demcha.compose.document.templates.cv.data.CvName;
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.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.OptionalDouble;

/**
* Shared sample data for the Teal Pulse CV example.
*
* <p>The sample is the preset's reference content: a nurse's one-page CV with
* four contact channels including a link, twelve competencies, a summary,
* three roles with their highlights, a degree, four certifications, three
* facts and the closing tagline.</p>
*
* <p>Kept in lockstep with the qa module's {@code TealPulseFixtures} — the
* two modules cannot share a source file, so a content change here belongs
* there too.</p>
*/
public final class TealPulseSampleData {

/** The break an entry stacks its highlights on. */
private static final String NEWLINE = String.valueOf((char) 10);

/** The dash this sheet writes a date range with. */
private static final String EN_DASH = String.valueOf((char) 0x2013);

private TealPulseSampleData() {
}

/** The single-page reference CV. */
public static CvDocument sample() {
List<CvDocument.Placement> placements = new ArrayList<>();
placements.add(new CvDocument.Placement(Slot.SIDEBAR, competencies()));
placements.add(new CvDocument.Placement(Slot.MAIN, summary()));
placements.add(new CvDocument.Placement(Slot.MAIN, experience()));
placements.add(new CvDocument.Placement(Slot.FOOTER, education()));
placements.add(new CvDocument.Placement(Slot.FOOTER, certifications()));
placements.add(new CvDocument.Placement(Slot.FOOTER, facts()));
placements.add(new CvDocument.Placement(Slot.FOOTER, tagline()));
return new CvDocument(identity(), placements);
}

private static CvIdentity identity() {
return new CvIdentity(
CvName.of("ISABELLA", "MOORE"),
"REGISTERED NURSE",
new Contact("+44 7700 900123", "isabella.moore@email.com",
"Manchester, United Kingdom"),
List.of(new Link("LinkedIn", "https://www.linkedin.com/in/isabellamoore-rn")),
Optional.empty());
}

private static SkillsSection competencies() {
List<CvSkill> entries = new ArrayList<>();
for (String name : new String[] {
"Patient Assessment", "Care Planning", "Medication Administration",
"Clinical Documentation", "Infection Prevention", "IV Therapy",
"Wound Care", "Patient Education", "Safeguarding", "Team Collaboration",
"Discharge Coordination", "Electronic Health Records"}) {
entries.add(new CvSkill(name, OptionalDouble.empty()));
}
return new SkillsSection("CORE COMPETENCIES",
List.of(new SkillGroup("CORE COMPETENCIES", entries)));
}

private static ParagraphSection summary() {
return new ParagraphSection("PROFESSIONAL SUMMARY",
"Compassionate Registered Nurse with 7+ years of experience delivering "
+ "high-quality patient care across acute medical, surgical, and "
+ "community settings. Skilled in patient assessment, medication "
+ "administration, multidisciplinary collaboration, discharge "
+ "planning, and maintaining accurate clinical documentation. Known "
+ "for calm decision-making, strong communication, and patient-centred "
+ "care.");
}

private static EntriesSection experience() {
return new EntriesSection("PROFESSIONAL EXPERIENCE", List.of(
CvEntry.builder("Senior Staff Nurse")
.subtitle("Manchester Royal Infirmary, Manchester, UK")
.date("2021" + EN_DASH + "Present")
.body(String.join(NEWLINE,
"Deliver direct nursing care for adult patients on a busy "
+ "acute medical ward.",
"Coordinate care plans with doctors, therapists, and support "
+ "staff to improve outcomes.",
"Supervise junior nurses and support student placements during "
+ "clinical rotations.",
"Improved discharge coordination and patient communication "
+ "across the ward.",
"Maintain precise documentation and ensure compliance with "
+ "clinical standards."))
.build(),
CvEntry.builder("Staff Nurse")
.subtitle("Salford General Hospital, Salford, UK")
.date("2018" + EN_DASH + "2021")
.body(String.join(NEWLINE,
"Provided nursing care across medical and surgical units.",
"Administered medications, monitored vital signs, and escalated "
+ "deteriorating patients promptly.",
"Supported infection control procedures and safe discharge "
+ "planning.",
"Built strong rapport with patients and families during "
+ "treatment and recovery."))
.build(),
CvEntry.builder("Community Nurse")
.subtitle("NorthCare Community Health, Greater Manchester, UK")
.date("2016" + EN_DASH + "2018")
.body(String.join(NEWLINE,
"Delivered home-based nursing care and patient education.",
"Managed wound care, medication support, and follow-up visits "
+ "for vulnerable patients.",
"Worked closely with GPs, social services, and families to "
+ "coordinate holistic care."))
.build()));
}

private static EntriesSection education() {
return new EntriesSection("EDUCATION", List.of(
CvEntry.builder("BSc (Hons) Adult Nursing")
.subtitle("University of Manchester")
.date("2013" + EN_DASH + "2016")
.build()));
}

private static EntriesSection certifications() {
List<CvEntry> entries = new ArrayList<>();
for (String name : new String[] {
"NMC Registered Nurse", "Immediate Life Support (ILS)",
"Venepuncture and Cannulation Certification", "Safeguarding Adults Level 3"}) {
entries.add(CvEntry.builder(name).build());
}
return new EntriesSection("CERTIFICATIONS", entries);
}

private static EntriesSection facts() {
return new EntriesSection("ADDITIONAL INFORMATION", List.of(
CvEntry.builder("Languages:")
.body("English (Native), Polish (Conversational)")
.build(),
CvEntry.builder("Right to Work:").body("United Kingdom").build(),
CvEntry.builder("Availability:").body("1 month notice").build()));
}

/**
* The closing line. Its title names the berth and is not drawn — only the
* body reaches the sheet.
*/
private static ParagraphSection tagline() {
return new ParagraphSection("TAGLINE",
"COMPASSIONATE CARE. CLINICAL EXCELLENCE. BETTER OUTCOMES.");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
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.TealPulse;
import com.demcha.examples.support.ExampleOutputPaths;
import com.demcha.examples.support.TealPulseSampleData;

import java.nio.file.Path;

/**
* Renders the layered {@code cv.v2} Teal Pulse preset against the nursing
* sample.
*
* <p>Output:
* {@code examples/target/generated-pdfs/templates/cv/cv-teal-pulse-v2.pdf}.</p>
*
* <p>The preset owns its page geometry — the design is drawn on a raster
* whose proportion is not A4's — so the session starts unconfigured.</p>
*/
public final class TealPulseExample {

private TealPulseExample() {
}

/**
* @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-teal-pulse-v2.pdf");
CvDocument doc = TealPulseSampleData.sample();
DocumentTemplate<CvDocument> template = TealPulse.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