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

### Public API

- **`CvSkill` carries the level as the document words it.** A rated skill could say how
much — a number in `[0, 1]` a preset draws as dots or a meter — or it could say it in
words by not being a skill at all and living in a `RowsSection` instead. It could not
say both, which is why `CharcoalGold` sets its languages as rows: that design writes
"Native" and "B2 – Upper Intermediate" out, and a number could not carry them back. A
design that shows a rating *and* names it had nowhere to go. `CvSkill` now carries
`note` — a plain string, blank when absent, like the fields beside it — reachable
through `CvSkill.of(name, level, note)`. The two channels stay separate because a
number cannot carry a wording and a wording cannot be measured into a meter; a preset
draws whichever it has room for, or both. The two-argument constructor and both
existing factories are kept explicitly, so existing calls compile and link unchanged
and every skill built through them still carries no note.

- **The structured invoice model carries what a second sheet needs.** It landed with
one consumer, `ConsultingInvoice`, and a model shaped around one document is a model
nobody has tested. Fitting a second published invoice to it found six things it could
Expand Down Expand Up @@ -84,6 +97,32 @@ follow semantic versioning; release dates are ISO 8601.

### Templates

- **A masthead-and-rail CV preset: `SlateOrange`.** A one-page sheet built as a
full-bleed slate band over a two-column body: an orange monogram tile beside the name,
the role line and a tracked specialism strip, with the contact lines across an orange
hairline; then a narrow column of marked competencies, trophied achievements, rated
languages and closing facts, beside a wide column carrying the profile, the roles held
on a rail, and a credentials footer of education and certifications. Ships as
`cv.presets.SlateOrange` on the existing `CvDocument` model, porting the rendered
layout of a published standalone template; it is the first preset to use
`CvSkill.note`, which its language rows need to show a rating and name it on the same
line. The four fills that reach a paper edge — the slate band, the orange tile and the
two column dividers — are page backgrounds rather than section fills, because a fill
on a section is bounded by its content. Every horizontal pair inside a body column is
a table with fixed widths: a row cannot nest in a row cell and both columns are cells,
so mark and label, language and rating, role and dates, and the two credential columns
are all tables, and where a cell needs several stacked lines it holds a single-column
table of its own. Seven berths reach their sections by title, including a specialism
berth whose body is drawn and whose title is not. As on the presets before it, a link
is drawn as its own label with the address behind it — the one deliberate departure
from the ported sheet, measured at 2 100 of 2 173 720 pixels — and a role or a degree
becomes a link when its entry carries one. Guarded by a smoke test (including the
unknown-mark data error, an achievement with no mark falling back to the trophy, a
language with and without its wording, the monogram and the strip, the link targets,
an identity with no links, a document with nothing but an identity, and the refusal of
a CV taller than the sheet), an exact layout snapshot and a pixel-parity gate; the
examples showcase gains `cv-slate-orange-v2`.

- **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
Expand Down
Binary file added assets/readme/examples/cv-slate-orange-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.SlateOrangeExample;
import com.demcha.examples.templates.cv.v2.TealPulseExample;
import com.demcha.examples.templates.cv.v2.TerracottaRailExample;
import com.demcha.examples.templates.cv.v2.CvBlueBannerExample;
Expand Down Expand Up @@ -158,6 +159,7 @@ public static void main(String[] args) throws Exception {
System.out.println("Generated: " + CharcoalGoldExample.generate());
System.out.println("Generated: " + TerracottaRailExample.generate());
System.out.println("Generated: " + TealPulseExample.generate());
System.out.println("Generated: " + SlateOrangeExample.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 @@ -66,6 +66,7 @@ record Entry(String title, String description, List<String> tags, String codeUrl
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");
cv("cv-slate-orange-v2", "SlateOrangeExample", "Slate Orange", "Full-bleed slate masthead with an orange monogram tile, over a sidebar of marked competencies, trophied achievements and rated languages beside a profile, a dated experience rail and a credentials footer.", "sidebar", "masthead", "orange");

// ===== 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,207 @@
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;

/**
* Shared sample data for the Slate Orange CV example.
*
* <p>The sample is the preset's reference content: a one-page CV with four
* contact lines including a link, ten marked competencies, three
* achievements, three rated languages, three closing facts, a profile, four
* roles, a degree and four certifications.</p>
*
* <p>Kept in lockstep with the qa module's {@code SlateOrangeFixtures} —
* the two modules cannot share a source file, so a content change here
* belongs there too.</p>
*/
public final class SlateOrangeSampleData {

/** The break a body stacks its lines on. */
private static final String NEWLINE = String.valueOf((char) 10);

private SlateOrangeSampleData() {
}

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

private static CvIdentity identity() {
return new CvIdentity(
CvName.of("DANIEL", "HARPER"),
"VERSATILE PROFESSIONAL",
new Contact("(555) 123-4567",
"daniel.harper@email.com",
"Austin, Texas \u2022 Open to Relocate"),
List.of(new Link("LinkedIn", "https://www.linkedin.com/in/danielharper")),
Optional.empty());
}

/** The strip under the role. Only the body is drawn; the title names the berth. */
private static ParagraphSection specialisms() {
return new ParagraphSection("SPECIALISMS", String.join(NEWLINE,
"OPERATIONS", "CUSTOMER SUCCESS", "COORDINATION"));
}

/** The competencies, each behind the mark its entry names. */
private static EntriesSection competencies() {
return new EntriesSection("CORE COMPETENCIES", List.of(
CvEntry.builder("Customer Service Excellence").icon("customer-service").build(),
CvEntry.builder("Operations & Process Support").icon("operations").build(),
CvEntry.builder("Project & Calendar Management").icon("calendar").build(),
CvEntry.builder("Data Management & Reporting").icon("reporting").build(),
CvEntry.builder("Communication & Stakeholders").icon("communication").build(),
CvEntry.builder("Problem Solving & Adaptability").icon("problem-solving").build(),
CvEntry.builder("Sales & Relationship Building").icon("sales").build(),
CvEntry.builder("Microsoft Office & Google Suite").icon("office-suite").build(),
CvEntry.builder("CRM Systems (Salesforce, HubSpot)").icon("crm").build(),
CvEntry.builder("Time Management & Prioritization").icon("time-management").build()));
}

/** The achievements: a bold title over its body, beside one shared trophy. */
private static EntriesSection achievements() {
return new EntriesSection("SELECTED ACHIEVEMENTS", List.of(
CvEntry.builder("Process Improvement")
.icon("achievement")
.body("Designed a new tracking system that reduced reporting time "
+ "by 30% and improved accuracy.")
.build(),
CvEntry.builder("Customer Satisfaction")
.icon("achievement")
.body("Maintained a 96% customer satisfaction rating through "
+ "responsive support and issue resolution.")
.build(),
CvEntry.builder("Revenue Contribution")
.icon("achievement")
.body("Consistently exceeded monthly sales goals by 15\u201320% "
+ "through relationship building and consultative service.")
.build()));
}

/** The languages, each carrying both the rating and the word for it. */
private static SkillsSection languages() {
return new SkillsSection("LANGUAGES", List.of(new SkillGroup("LANGUAGES", List.of(
CvSkill.of("English", 1.0, "Native"),
CvSkill.of("Spanish", 0.8, "Professional Working"),
CvSkill.of("French", 0.4, "Basic")))));
}

/** The closing facts, on the same mark column at a wider pitch. */
private static EntriesSection facts() {
return new EntriesSection("ADDITIONAL INFORMATION", List.of(
CvEntry.builder("Availability: Full-time").icon("availability").build(),
CvEntry.builder("Willing to Relocate: Yes").icon("relocation").build(),
CvEntry.builder("Remote Work: Open to Hybrid/Remote").icon("remote-work").build()));
}

private static ParagraphSection profile() {
return new ParagraphSection("PROFESSIONAL PROFILE",
"Adaptable and resourceful professional with 6+ years of "
+ "experience across operations, customer service, sales, and "
+ "project support. Known for strong communication, "
+ "organizational skills, and the ability to thrive in "
+ "fast-paced environments. Adept at managing priorities, "
+ "building relationships, and delivering results that drive "
+ "efficiency, customer satisfaction, and business growth.");
}

/** The roles held, each a headline over its bullets. */
private static EntriesSection experience() {
return new EntriesSection("PROFESSIONAL EXPERIENCE", List.of(
CvEntry.builder("Operations Coordinator")
.subtitle("BrightStart Logistics, Austin, TX")
.date("Jan 2022 \u2013 Present")
.body(String.join(NEWLINE,
"Coordinate daily operations and scheduling for a team of "
+ "12 across multiple sites.",
"Develop and maintain dashboards and reports to track KPIs "
+ "and operational metrics.",
"Streamline communication between departments, reducing "
+ "response time by 25%.",
"Manage vendor relationships and assist with contract and "
+ "invoice reconciliation."))
.build(),
CvEntry.builder("Customer Support Specialist")
.subtitle("TechWave Solutions, Remote")
.date("Jun 2020 \u2013 Dec 2021")
.body(String.join(NEWLINE,
"Provided technical support to customers via phone, email, "
+ "and live chat.",
"Resolved an average of 40+ inquiries daily with a "
+ "first-contact resolution rate of 92%.",
"Created knowledge base articles and improved internal "
+ "documentation.",
"Collaborated with product and engineering teams to "
+ "escalate and resolve issues."))
.build(),
CvEntry.builder("Sales Associate")
.subtitle("Urban Outfitters, Austin, TX")
.date("Mar 2019 \u2013 May 2020")
.body(String.join(NEWLINE,
"Consistently exceeded sales targets by 15\u201320% through "
+ "product knowledge and personalized customer service.",
"Built and maintained strong customer relationships, "
+ "driving repeat business.",
"Merchandised floor displays and contributed to visual "
+ "standards and promotions.",
"Recognized as \u201cTop Performer\u201d for Q4 2019."))
.build(),
CvEntry.builder("Administrative Assistant / Project Assistant")
.subtitle("Greenfield Nonprofit Initiative, Austin, TX")
.date("May 2018 \u2013 Feb 2019")
.body(String.join(NEWLINE,
"Supported project managers with scheduling, documentation, "
+ "and meeting logistics.",
"Managed calendars, travel arrangements, and expense "
+ "reports.",
"Maintained donor records and prepared reports for "
+ "leadership and stakeholders.",
"Assisted with event planning and community outreach "
+ "initiatives."))
.build()));
}

/** The degree; the design's third line is where and when, as one line. */
private static EntriesSection education() {
return new EntriesSection("EDUCATION", List.of(
CvEntry.builder("Bachelor of Arts in Communications")
.subtitle("University of Texas at Austin")
.place("Austin, TX \u2022 Graduated May 2018")
.build()));
}

/** The certifications, one bulleted line each. */
private static EntriesSection certifications() {
return new EntriesSection("CERTIFICATIONS & TRAINING", List.of(
CvEntry.builder("Google Project Management Certificate (Coursera) \u2013 2023").build(),
CvEntry.builder("HubSpot Customer Service Certification \u2013 2022").build(),
CvEntry.builder("Microsoft Excel: Advanced Formulas & Functions \u2013 2021").build(),
CvEntry.builder("ServSafe Food Handler Certification \u2013 2019").build()));
}
}
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.SlateOrange;
import com.demcha.examples.support.ExampleOutputPaths;
import com.demcha.examples.support.SlateOrangeSampleData;

import java.nio.file.Path;

/**
* Renders the layered {@code cv.v2} Slate Orange preset against the
* operations sample.
*
* <p>Output:
* {@code examples/target/generated-pdfs/templates/cv/cv-slate-orange-v2.pdf}.</p>
*
* <p>The preset owns its page geometry — the sheet is drawn to the paper edge
* and every column pads itself — so the session starts unconfigured.</p>
*/
public final class SlateOrangeExample {

private SlateOrangeExample() {
}

/**
* @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-slate-orange-v2.pdf");
CvDocument doc = SlateOrangeSampleData.sample();
DocumentTemplate<CvDocument> template = SlateOrange.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