diff --git a/README.md b/README.md index b52bd7dd..466b6a28 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ GraphCompose uses PDFBox under the hood as the rendering backend — the com | You want to… | Surface | Entry point | |---|---|---| | Generate a one-off PDF programmatically | DSL | `GraphCompose.document(...).pageFlow(...)` — see [Hello world](#hello-world) below | -| Generate a CV / cover letter / invoice / proposal from data | Templates v2 | `ModernProfessional.create(BusinessTheme.modern()).compose(session, spec)` — see [templates v2](./docs/templates/v1-classic/README.md) | +| Generate a CV / cover letter from data | Layered templates | `ModernProfessional.create().compose(session, cvDocument)` — see [layered templates](./docs/templates/v2-layered/README.md) | | Add a custom visual primitive | Engine extension | `NodeDefinition` + `PdfFragmentRenderHandler` — see [extension guide](./docs/contributing/extension-guide.md) | | Regression-test generated layouts | Layout snapshots | `DocumentSession#layoutSnapshot()` — see [snapshot testing](./docs/operations/layout-snapshot-testing.md) | @@ -146,7 +146,7 @@ For a Spring Boot `@RestController` streaming the PDF straight to the response, ## What's in v1.6 — "expressive" -- **Templates v2** — 14 CV and 14 paired cover-letter presets, theme-driven via `BusinessTheme`, one-liner `create(theme)` factories. Inline markdown, slot-based multi-column layouts. See [`docs/templates/v1-classic/README.md`](./docs/templates/v1-classic/README.md). +- **Layered templates** — 14 CV and 14 paired cover-letter presets on the layered `cv.v2` / `coverletter.v2` architecture (data β†’ theme β†’ components β†’ widgets β†’ presets), one-liner `create()` factories over a typed `CvDocument` / `CoverLetterDocument`. Inline markdown, multi-column layouts. The going-forward standard for new template families. See [`docs/templates/v2-layered/README.md`](./docs/templates/v2-layered/README.md). (The earlier `BusinessTheme`-based preset surface is now deprecated.) - **Composed primitives** — `ListBuilder.addItem(label, Consumer)` (nested lists), `DocumentTableCell.node(...)` (any node inside a cell), `CanvasLayerNode` (pixel-precise free-canvas placement). - **Architecture hardening** — `@Internal` API stability marker, public `PdfFragmentRenderHandler` SPI, `DocumentRenderingException` on the convenience render path, documented thread-safety contract. @@ -193,7 +193,7 @@ document.pageFlow().addCanvas(523, 360, canvas -> canvas ### Templates - πŸ†• [**Templates β€” v2 layered architecture**](./docs/templates/v2-layered/README.md) — the canonical going-forward pattern for new template families (CV v2 is the reference implementation). Personas: [quickstart](./docs/templates/v2-layered/quickstart.md) Β· [using templates](./docs/templates/v2-layered/using-templates.md) Β· [authoring presets](./docs/templates/v2-layered/authoring-presets.md) Β· [contributing a new family](./docs/templates/v2-layered/contributor-guide.md). -- [Templates v1-classic landing](./docs/templates/v1-classic/README.md) — CV / cover-letter / invoice / proposal preset library (v1.6 surface, still shipped). Cheat sheet: [authoring](./docs/templates/v1-classic/authoring.md). +- [Templates v1-classic landing](./docs/templates/v1-classic/README.md) — the older `BusinessTheme` / `CvSpec` CV / cover-letter / invoice / proposal preset library (**deprecated** β€” CV + cover letter are superseded by [v2-layered](./docs/templates/v2-layered/README.md); invoice / proposal / schedule are not yet ported). Cheat sheet: [authoring](./docs/templates/v1-classic/authoring.md). ### Architecture & operations - [Architecture overview](./docs/architecture/overview.md) Β· [Lifecycle](./docs/architecture/lifecycle.md) Β· [Production rendering](./docs/operations/production-rendering.md) Β· [Layout snapshot testing](./docs/operations/layout-snapshot-testing.md) diff --git a/assets/readme/examples/cv-classic-serif.pdf b/assets/readme/examples/cv-classic-serif.pdf index 8801d73c..c0ec3d9f 100644 Binary files a/assets/readme/examples/cv-classic-serif.pdf and b/assets/readme/examples/cv-classic-serif.pdf differ diff --git a/assets/readme/examples/cv-compact-mono.pdf b/assets/readme/examples/cv-compact-mono.pdf index 01058c54..1ecfdc02 100644 Binary files a/assets/readme/examples/cv-compact-mono.pdf and b/assets/readme/examples/cv-compact-mono.pdf differ diff --git a/assets/readme/examples/cv-engineering-resume.pdf b/assets/readme/examples/cv-engineering-resume.pdf new file mode 100644 index 00000000..1b2312a6 Binary files /dev/null and b/assets/readme/examples/cv-engineering-resume.pdf differ diff --git a/assets/readme/examples/cv-modern-professional.pdf b/assets/readme/examples/cv-modern-professional.pdf index b8854461..f75d5989 100644 Binary files a/assets/readme/examples/cv-modern-professional.pdf and b/assets/readme/examples/cv-modern-professional.pdf differ diff --git a/assets/readme/examples/cv-nordic-clean.pdf b/assets/readme/examples/cv-nordic-clean.pdf index 88dfbeb9..c99d9503 100644 Binary files a/assets/readme/examples/cv-nordic-clean.pdf and b/assets/readme/examples/cv-nordic-clean.pdf differ diff --git a/assets/readme/examples/cv-panel.pdf b/assets/readme/examples/cv-panel.pdf new file mode 100644 index 00000000..97dea47d Binary files /dev/null and b/assets/readme/examples/cv-panel.pdf differ diff --git a/assets/readme/examples/cv-product-leader.pdf b/assets/readme/examples/cv-product-leader.pdf deleted file mode 100644 index 23901d0e..00000000 Binary files a/assets/readme/examples/cv-product-leader.pdf and /dev/null differ diff --git a/assets/readme/examples/cv-tech-lead.pdf b/assets/readme/examples/cv-tech-lead.pdf deleted file mode 100644 index 95ca00da..00000000 Binary files a/assets/readme/examples/cv-tech-lead.pdf and /dev/null differ diff --git a/assets/readme/examples/cv-timeline-minimal.pdf b/assets/readme/examples/cv-timeline-minimal.pdf index 010981ae..ce15da94 100644 Binary files a/assets/readme/examples/cv-timeline-minimal.pdf and b/assets/readme/examples/cv-timeline-minimal.pdf differ diff --git a/docs/README.md b/docs/README.md index 539c3e0e..304c3984 100644 --- a/docs/README.md +++ b/docs/README.md @@ -74,10 +74,11 @@ it does. - **[adr/0002-theme-unification.md](adr/0002-theme-unification.md)** β€” single canonical theme model. - **[adr/0003-api-stability-and-internal-marker.md](adr/0003-api-stability-and-internal-marker.md)** β€” public-API guarantees + `@Internal` marker. - **[adr/0004-pdf-handler-spi-extension.md](adr/0004-pdf-handler-spi-extension.md)** β€” PDF render handler SPI. -- **[adr/0011-templates-v2-architecture.md](adr/0011-templates-v2-architecture.md)** β€” the v1.6 templates restructure (spec/builder/presets/themes). +- **[adr/0011-templates-v2-architecture.md](adr/0011-templates-v2-architecture.md)** β€” the v1.6 templates restructure (spec/builder/presets/themes); **superseded** for CV + cover letter by [0015](adr/0015-layered-template-architecture.md). - **[adr/0012-nested-list-evolution.md](adr/0012-nested-list-evolution.md)** β€” nested list rendering evolution. - **[adr/0013-composed-table-cell.md](adr/0013-composed-table-cell.md)** β€” composed table cell model. - **[adr/0014-controlled-absolute-placement.md](adr/0014-controlled-absolute-placement.md)** β€” controlled absolute placement strategy. +- **[adr/0015-layered-template-architecture.md](adr/0015-layered-template-architecture.md)** β€” the layered `cv.v2` / `coverletter.v2` authoring model (current standard); supersedes the preset/builder portion of 0011. > **ADR numbering gap (0005–0010)** is intentional β€” those numbers > were reserved during a v1.5 restructure that landed under ADR 0011 diff --git a/docs/adr/0015-layered-template-architecture.md b/docs/adr/0015-layered-template-architecture.md new file mode 100644 index 00000000..49bb92ef --- /dev/null +++ b/docs/adr/0015-layered-template-architecture.md @@ -0,0 +1,80 @@ +# ADR 0015 β€” Layered template architecture (cv.v2 / coverletter.v2) + +- **Status:** Accepted +- **Date:** 2026-05-28 +- **Authors:** Artem Demchyshyn + +## Context + +ADR 0011 reorganised templates into a per-domain "Templates v2" surface +(`templates/cv/{presets,builder,spec,layouts}`, `templates/coverletter/…`) +built on `CvSpec` / `CoverLetterSpec` data records, `BusinessTheme`, and +flat copy-and-tweak preset classes. That removed the v1.5 mess (one +600–700-line composer per template), but two limits remained: + +- **Preset classes still re-implemented header / contact / section + rendering.** Each preset carried its own `addHeader`, contact row, and + body dispatch β€” the duplication ADR 0011 set out to kill kept creeping + back at the preset layer. +- **Style, layout, and data were not cleanly separated.** A visual + re-skin still meant editing preset code; there was no single token + source a preset read from. + +A refined **layered** architecture was prototyped under `cv/v2`, proved +out across all 14 CV presets (with a pixel-parity migration from the +Gen-2 presets), then extended to all 14 cover letters under +`coverletter/v2` (which reuses the CV theme + components so a CV and its +paired letter render as a matched set). + +## Decision + +Adopt the **layered** template architecture as the canonical +template-authoring pattern. A template family is built in five layers: + +1. **data** β€” typed input records (`CvDocument`, `CoverLetterDocument`, + reusing `CvIdentity`); no rendering logic. +2. **theme** β€” `CvTheme` = palette + typography + spacing + decoration + tokens; the only place colour / font / spacing literals live. +3. **components** β€” shared stateless renderers (`SectionDispatcher`, + `RichParagraphRenderer`, `MarkdownInline`, `CvTextStyles`, + `LetterBody`, …) reused across presets and families. +4. **widgets** β€” composable visual blocks (`Headline`, `ContactLine`, + `Masthead`, `CardWidget`, `SectionHeader`, …). +5. **presets** β€” thin orchestrators: a `create()` / `create(CvTheme)` + factory plus a `compose()` that lays out the page-flow and delegates + to components / widgets. No re-implemented parsing, no duplicated + headers. + +Presets are exposed through the generic `DocumentTemplate` contract. +`cv.v2` is the reference implementation; `coverletter.v2` is the paired +family. + +The earlier Gen-2 surface +(`templates/cv/{presets,builder,spec,layouts}` and the equivalent +`templates/coverletter/{presets,builder,spec,layouts}`) is **deprecated** +(`@Deprecated(since = "1.7.0", forRemoval = true)`) and scheduled for +removal in a future major. It keeps compiling and working until then β€” +existing callers are not broken. + +## Consequences + +- **One documented authoring path.** New template families follow + `docs/templates/v2-layered/` (quickstart Β· using-templates Β· + authoring-presets Β· contributor-guide). A new preset is a thin + orchestrator, not a forked composer. +- **Re-skin without code edits.** A new visual flavour is a new + `CvTheme.()` factory; the preset is unchanged. +- **Naming overlap resolved.** Gen-2 package-info prose previously also + called itself "Templates v2", colliding with the `cv.v2` folder name. + Deprecating Gen-2 and correcting its package-info removes the + ambiguity; "the v2 / layered surface" now unambiguously means + `cv.v2` / `coverletter.v2`. +- **Showcase + examples render the layered surface.** The CV and + cover-letter gallery examples and the committed README previews are + generated from `cv.v2` / `coverletter.v2`. +- **Not yet ported:** invoice, proposal, and schedule remain on the + Gen-2 / builtins surface (`BusinessTheme`-based) and are **not** + deprecated. Porting them to the layered architecture is future work. +- **Supersedes the preset / builder / spec portion of ADR 0011** while + keeping its domain-folder split (cv / coverletter / invoice / + proposal / schedule). diff --git a/docs/roadmaps/migration-v1-5-to-v1-6.md b/docs/roadmaps/migration-v1-5-to-v1-6.md index f2974e52..e1948aed 100644 --- a/docs/roadmaps/migration-v1-5-to-v1-6.md +++ b/docs/roadmaps/migration-v1-5-to-v1-6.md @@ -332,13 +332,13 @@ try (DocumentSession session = GraphCompose.document(out).create()) { ``` The full builder fluent surface and module / block taxonomy are -documented in [`docs/templates/v1-classic/authoring.md`](template-authoring.md). -The full gallery of v2 CV / cover-letter renders lives under -[`assets/readme/examples/`](../assets/readme/examples/) and is +documented in [`docs/templates/v1-classic/authoring.md`](../templates/v1-classic/authoring.md). +The full gallery of CV / cover-letter renders lives under +[`assets/readme/examples/`](../../assets/readme/examples/) and is regenerable via -[`examples/CvTemplateGalleryFileExample`](../examples/src/main/java/com/demcha/examples/CvTemplateGalleryFileExample.java) +[`CvTemplateGalleryFileExample`](../../examples/src/main/java/com/demcha/examples/templates/cv/CvTemplateGalleryFileExample.java) and -[`CoverLetterTemplateGalleryFileExample`](../examples/src/main/java/com/demcha/examples/CoverLetterTemplateGalleryFileExample.java). +[`CoverLetterTemplateGalleryFileExample`](../../examples/src/main/java/com/demcha/examples/templates/coverletter/CoverLetterTemplateGalleryFileExample.java). ### What v2 gives you for free diff --git a/examples/README.md b/examples/README.md index 86d8e536..97cd3d4b 100644 --- a/examples/README.md +++ b/examples/README.md @@ -214,8 +214,8 @@ Generates every v2 CV preset in one orchestrated run β€” 14 presets covering single-column, two-column-sidebar, and three-column-magazine layouts. Use this as the side-by-side catalogue when picking a base preset for your own CV product. Each preset is a one-liner factory -(`ModernProfessional.create(theme)`, `NordicClean.create(theme)`, -…); see `templates/cv/presets/` for the full list. +(`ModernProfessional.create()`, `NordicClean.create()`, +…); see `cv/v2/presets/` for the full list. | Variant | PDF | |---|---| @@ -224,8 +224,8 @@ preset for your own CV product. Each preset is a one-liner factory | Classic serif | [PDF](../assets/readme/examples/cv-classic-serif.pdf) | | Compact mono | [PDF](../assets/readme/examples/cv-compact-mono.pdf) | | Timeline minimal | [PDF](../assets/readme/examples/cv-timeline-minimal.pdf) | -| Engineering resume (was "Tech lead") | [PDF](../assets/readme/examples/cv-tech-lead.pdf) | -| Panel (was "Product leader") | [PDF](../assets/readme/examples/cv-product-leader.pdf) | +| Engineering resume | [PDF](../assets/readme/examples/cv-engineering-resume.pdf) | +| Panel | [PDF](../assets/readme/examples/cv-panel.pdf) | | Executive Β· BoxedSections Β· CenteredHeadline Β· BlueBanner Β· EditorialBlue Β· SidebarPortrait Β· MonogramSidebar | run the gallery to render | [πŸ“œ Full source](src/main/java/com/demcha/examples/templates/cv/CvTemplateGalleryFileExample.java) @@ -235,9 +235,9 @@ preset for your own CV product. Each preset is a one-liner factory Generates all 14 paired v2 cover-letter presets in one run β€” one letter style per CV preset so a candidate's CV and cover letter share the same visual language end-to-end. Each preset is a -one-liner factory (`ModernProfessionalLetter.create(theme)`, -`NordicCleanLetter.create(theme)`, …) under -`templates/coverletter/presets/`. +one-liner factory (`ModernProfessionalLetter.create()`, +`NordicCleanLetter.create()`, …) under +`coverletter/v2/presets/`. [πŸ“œ Full source](src/main/java/com/demcha/examples/templates/coverletter/CoverLetterTemplateGalleryFileExample.java) diff --git a/examples/src/main/java/com/demcha/examples/support/ExampleDataFactory.java b/examples/src/main/java/com/demcha/examples/support/ExampleDataFactory.java index c96d9b20..f1893ae6 100644 --- a/examples/src/main/java/com/demcha/examples/support/ExampleDataFactory.java +++ b/examples/src/main/java/com/demcha/examples/support/ExampleDataFactory.java @@ -9,6 +9,7 @@ import com.demcha.compose.document.templates.blocks.WorkHistoryBlock; import com.demcha.compose.document.templates.coverletter.spec.CoverLetterHeader; import com.demcha.compose.document.templates.coverletter.spec.CoverLetterSpec; +import com.demcha.compose.document.templates.coverletter.v2.data.CoverLetterDocument; import com.demcha.compose.document.templates.cv.spec.CvHeader; import com.demcha.compose.document.templates.cv.spec.CvModule; import com.demcha.compose.document.templates.cv.spec.CvSpec; @@ -451,6 +452,53 @@ public static CoverLetterHeader sampleCoverLetterHeaderV2() { // -- Templates v2 (cv/v2) sample data ------------------------------ + /** + * Returns the canonical Jordan Rivera identity shared by the v2 CV + * sample and the v2 cover-letter sample, so a CV and its paired + * letter render an identical masthead. + * + * @return sample v2 identity block + */ + public static CvIdentity sampleCvIdentityV2() { + return CvIdentity.builder() + .name("Jordan", "Rivera") + .jobTitle("Platform Engineer") + .contact("+44 20 5555 1000", + "jordan.rivera@example.com", + "London, UK") + .link("LinkedIn", "https://linkedin.com/in/jordan-rivera-demo") + .link("GitHub", "https://github.com/jrivera-demo") + .build(); + } + + /** + * Returns a sample {@code CoverLetterDocument} for the v2 + * cover-letter pipeline. Reuses {@link #sampleCvIdentityV2()} so the + * letter masthead matches the paired CV exactly; the greeting / body + * / closing reuse the canonical v2 cover-letter sample content. + * + * @return sample v2 cover letter document + */ + public static CoverLetterDocument sampleCoverLetterDocumentV2() { + return CoverLetterDocument.builder() + .identity(sampleCvIdentityV2()) + .greeting("Dear Hiring Team at **Northwind Systems**,") + .paragraph("I am excited to share my interest in the Senior " + + "Platform Engineer role. My recent work has focused " + + "on building **reusable document-generation systems** " + + "that balance public API design, render quality, and " + + "maintainability.") + .paragraph("I enjoy translating fuzzy workflow requirements into " + + "clear template abstractions, reliable test coverage, " + + "and examples that make adoption easier for the rest " + + "of the team.") + .paragraph("I would welcome the opportunity to bring that same " + + "mix of engineering rigor and product thinking to your " + + "platform group.") + .closing("Sincerely,") + .build(); + } + /** * Returns a sample {@code CvDocument} for the v2 CV pipeline β€” * the canonical Jordan Rivera content expressed in the v2 @@ -470,15 +518,7 @@ public static CoverLetterHeader sampleCoverLetterHeaderV2() { * @return sample v2 CV document */ public static CvDocument sampleCvDocumentV2() { - CvIdentity identity = CvIdentity.builder() - .name("Jordan", "Rivera") - .jobTitle("Platform Engineer") - .contact("+44 20 5555 1000", - "jordan.rivera@example.com", - "London, UK") - .link("LinkedIn", "https://linkedin.com/in/jordan-rivera-demo") - .link("GitHub", "https://github.com/jrivera-demo") - .build(); + CvIdentity identity = sampleCvIdentityV2(); ParagraphSection summary = new ParagraphSection( "Professional Summary", diff --git a/examples/src/main/java/com/demcha/examples/support/PdfRasterizer.java b/examples/src/main/java/com/demcha/examples/support/PdfRasterizer.java new file mode 100644 index 00000000..6a400acf --- /dev/null +++ b/examples/src/main/java/com/demcha/examples/support/PdfRasterizer.java @@ -0,0 +1,50 @@ +package com.demcha.examples.support; + +import org.apache.pdfbox.Loader; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.rendering.PDFRenderer; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.File; +import java.nio.file.Path; + +/** + * Tiny dev/review utility: rasterizes each page of a PDF to a PNG via + * PDFBox so generated documents can be eyeballed without a system + * Ghostscript/poppler install. + * + *
{@code
+ * exec:java -Dexec.mainClass=com.demcha.examples.support.PdfRasterizer \
+ *   -Dexec.args="path/to/doc.pdf out/prefix 140"
+ * }
+ * + *

Writes {@code prefix-p0.png}, {@code prefix-p1.png}, … one per + * page.

+ */ +public final class PdfRasterizer { + + private PdfRasterizer() { + } + + public static void main(String[] args) throws Exception { + if (args.length < 2) { + System.err.println("usage: PdfRasterizer [dpi]"); + System.exit(2); + } + Path pdf = Path.of(args[0]); + String prefix = args[1]; + float dpi = args.length > 2 ? Float.parseFloat(args[2]) : 140f; + + try (PDDocument doc = Loader.loadPDF(pdf.toFile())) { + PDFRenderer renderer = new PDFRenderer(doc); + int pages = doc.getNumberOfPages(); + for (int i = 0; i < pages; i++) { + BufferedImage image = renderer.renderImageWithDPI(i, dpi); + File out = new File(prefix + "-p" + i + ".png"); + ImageIO.write(image, "png", out); + System.out.println("Wrote: " + out.getAbsolutePath()); + } + } + } +} diff --git a/examples/src/main/java/com/demcha/examples/support/ShowcaseMetadata.java b/examples/src/main/java/com/demcha/examples/support/ShowcaseMetadata.java index af4c1c69..f67a619e 100644 --- a/examples/src/main/java/com/demcha/examples/support/ShowcaseMetadata.java +++ b/examples/src/main/java/com/demcha/examples/support/ShowcaseMetadata.java @@ -57,7 +57,6 @@ record Entry(String title, String description, List tags, String codeUrl cv("cv-panel", "Panel", "Soft-tinted panels per section, Product-Leader feel β€” was ProductLeader in v1.5.", "panel"); cv("cv-sidebar-portrait", "Sidebar Portrait", "Edge-to-edge grey sidebar with portrait photo, contact stack, and skills.", "sidebar", "portrait"); cv("cv-monogram-sidebar", "Monogram Sidebar", "Sidebar with monogram badge, accent rule, and structured contact + skills column.", "sidebar", "monogram"); - cv("cv-modern-professional", "Modern Professional CV", "Same data, ModernProfessional preset β€” single-file CvFileExample for canonical authoring.", "minimal"); // ===== Templates / Cover Letter ===== letter("cover-letter", "Cover Letter (canonical)", "Single-file canonical cover letter authored via CoverLetterFileExample.", "letter"); diff --git a/examples/src/main/java/com/demcha/examples/templates/coverletter/CoverLetterTemplateGalleryFileExample.java b/examples/src/main/java/com/demcha/examples/templates/coverletter/CoverLetterTemplateGalleryFileExample.java index 98b06075..1f110bb2 100644 --- a/examples/src/main/java/com/demcha/examples/templates/coverletter/CoverLetterTemplateGalleryFileExample.java +++ b/examples/src/main/java/com/demcha/examples/templates/coverletter/CoverLetterTemplateGalleryFileExample.java @@ -4,51 +4,49 @@ 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.coverletter.presets.BlueBannerLetter; -import com.demcha.compose.document.templates.coverletter.presets.BoxedSectionsLetter; -import com.demcha.compose.document.templates.coverletter.presets.CenteredHeadlineLetter; -import com.demcha.compose.document.templates.coverletter.presets.ClassicSerifLetter; -import com.demcha.compose.document.templates.coverletter.presets.CompactMonoLetter; -import com.demcha.compose.document.templates.coverletter.presets.EditorialBlueLetter; -import com.demcha.compose.document.templates.coverletter.presets.EngineeringResumeLetter; -import com.demcha.compose.document.templates.coverletter.presets.ExecutiveLetter; -import com.demcha.compose.document.templates.coverletter.presets.ModernProfessionalLetter; -import com.demcha.compose.document.templates.coverletter.presets.MonogramSidebarLetter; -import com.demcha.compose.document.templates.coverletter.presets.NordicCleanLetter; -import com.demcha.compose.document.templates.coverletter.presets.PanelLetter; -import com.demcha.compose.document.templates.coverletter.presets.SidebarPortraitLetter; -import com.demcha.compose.document.templates.coverletter.presets.TimelineMinimalLetter; -import com.demcha.compose.document.templates.coverletter.spec.CoverLetterSpec; -import com.demcha.compose.document.theme.BusinessTheme; +import com.demcha.compose.document.templates.coverletter.v2.data.CoverLetterDocument; +import com.demcha.compose.document.templates.coverletter.v2.presets.BlueBannerLetter; +import com.demcha.compose.document.templates.coverletter.v2.presets.BoxedSectionsLetter; +import com.demcha.compose.document.templates.coverletter.v2.presets.CenteredHeadlineLetter; +import com.demcha.compose.document.templates.coverletter.v2.presets.ClassicSerifLetter; +import com.demcha.compose.document.templates.coverletter.v2.presets.CompactMonoLetter; +import com.demcha.compose.document.templates.coverletter.v2.presets.EditorialBlueLetter; +import com.demcha.compose.document.templates.coverletter.v2.presets.EngineeringResumeLetter; +import com.demcha.compose.document.templates.coverletter.v2.presets.ExecutiveLetter; +import com.demcha.compose.document.templates.coverletter.v2.presets.ModernProfessionalLetter; +import com.demcha.compose.document.templates.coverletter.v2.presets.MonogramSidebarLetter; +import com.demcha.compose.document.templates.coverletter.v2.presets.NordicCleanLetter; +import com.demcha.compose.document.templates.coverletter.v2.presets.PanelLetter; +import com.demcha.compose.document.templates.coverletter.v2.presets.SidebarPortraitLetter; +import com.demcha.compose.document.templates.coverletter.v2.presets.TimelineMinimalLetter; import com.demcha.examples.support.ExampleDataFactory; import com.demcha.examples.support.ExampleOutputPaths; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; -import java.util.function.Function; +import java.util.function.Supplier; /** - * Renders all 14 Templates v2 cover-letter presets against the same - * shared sample data. Each PDF lands in - * {@code examples/target/generated-pdfs/cover-letter-.pdf} - * where {@code } is the paired CV preset's stable identifier - * (e.g. {@code cover-letter-modern-professional.pdf}). + * Renders all 14 layered cover-letter presets ({@code + * coverletter.v2.presets.*} β€” the polished current standard) against + * the same shared sample {@link CoverLetterDocument}. Each PDF lands in + * {@code examples/target/generated-pdfs/templates/coverletter/cover-letter-.pdf} + * where {@code } is the paired CV preset's stable identifier (e.g. + * {@code cover-letter-modern-professional.pdf}). * *

The 14 letter renders match the 14 CV renders in - * {@link CvTemplateGalleryFileExample} β€” a writer can render both - * galleries and pick a CV / cover-letter pair sharing the same - * visual signature.

+ * {@link com.demcha.examples.templates.cv.CvTemplateGalleryFileExample} + * β€” a writer can render both galleries and pick a CV / cover-letter + * pair sharing the same visual signature.

*/ public final class CoverLetterTemplateGalleryFileExample { - private static final BusinessTheme THEME = BusinessTheme.modern(); - private CoverLetterTemplateGalleryFileExample() { } /** - * Renders all 14 v2 cover-letter preset gallery PDFs. + * Renders all 14 layered cover-letter preset gallery PDFs. * * @return list of absolute paths of the rendered PDFs in source * order @@ -59,51 +57,49 @@ public static List generate() throws Exception { } /** - * Renders one preset (when {@code presetId} matches its stable id) - * or all presets when {@code presetId} is null. + * Renders one preset (when {@code presetId} matches its slug) or all + * presets when {@code presetId} is null. * - * @param presetId stable preset id to render exclusively, or null - * to render all presets + * @param presetId slug to render exclusively, or null to render all * @return list of absolute paths of the rendered PDFs * @throws Exception if any rendering fails */ public static List generate(String presetId) throws Exception { + // The slug strips the "-letter" suffix so the example file name + // matches the paired CV (cover-letter-modern-professional.pdf + // pairs with cv-modern-professional.pdf). List runs = List.of( - // Stable id stripped of the "-letter" suffix so the - // example file name matches the paired CV (e.g. - // cover-letter-modern-professional.pdf pairs with - // cv-modern-professional.pdf). - run("modern-professional", ModernProfessionalLetter::create), - run("nordic-clean", NordicCleanLetter::create), - run("classic-serif", ClassicSerifLetter::create), - run("compact-mono", CompactMonoLetter::create), - run("executive", ExecutiveLetter::create), - run("engineering-resume", EngineeringResumeLetter::create), - run("timeline-minimal", TimelineMinimalLetter::create), - run("boxed-sections", BoxedSectionsLetter::create), - run("centered-headline", CenteredHeadlineLetter::create), - run("blue-banner", BlueBannerLetter::create), - run("editorial-blue", EditorialBlueLetter::create), - run("panel", PanelLetter::create), - run("sidebar-portrait", SidebarPortraitLetter::create), - run("monogram-sidebar", MonogramSidebarLetter::create)); + run("modern-professional", ModernProfessionalLetter.RECOMMENDED_MARGIN, ModernProfessionalLetter::create), + run("nordic-clean", NordicCleanLetter.RECOMMENDED_MARGIN, NordicCleanLetter::create), + run("classic-serif", ClassicSerifLetter.RECOMMENDED_MARGIN, ClassicSerifLetter::create), + run("compact-mono", CompactMonoLetter.RECOMMENDED_MARGIN, CompactMonoLetter::create), + run("executive", ExecutiveLetter.RECOMMENDED_MARGIN, ExecutiveLetter::create), + run("engineering-resume", EngineeringResumeLetter.RECOMMENDED_MARGIN, EngineeringResumeLetter::create), + run("timeline-minimal", TimelineMinimalLetter.RECOMMENDED_MARGIN, TimelineMinimalLetter::create), + run("boxed-sections", BoxedSectionsLetter.RECOMMENDED_MARGIN, BoxedSectionsLetter::create), + run("centered-headline", CenteredHeadlineLetter.RECOMMENDED_MARGIN, CenteredHeadlineLetter::create), + run("blue-banner", BlueBannerLetter.RECOMMENDED_MARGIN, BlueBannerLetter::create), + run("editorial-blue", EditorialBlueLetter.RECOMMENDED_MARGIN, EditorialBlueLetter::create), + run("panel", PanelLetter.RECOMMENDED_MARGIN, PanelLetter::create), + run("sidebar-portrait", SidebarPortraitLetter.RECOMMENDED_MARGIN, SidebarPortraitLetter::create), + run("monogram-sidebar", MonogramSidebarLetter.RECOMMENDED_MARGIN, MonogramSidebarLetter::create)); - CoverLetterSpec spec = ExampleDataFactory.sampleCoverLetterSpecV2(); + CoverLetterDocument doc = ExampleDataFactory.sampleCoverLetterDocumentV2(); List generated = new ArrayList<>(); for (Run letter : runs) { - if (presetId != null && !letter.id.equals(presetId)) { + if (presetId != null && !letter.id().equals(presetId)) { continue; } - generated.add(renderOne(spec, letter)); + generated.add(renderOne(doc, letter)); } return List.copyOf(generated); } /** - * Renders all v2 cover-letter preset gallery PDFs and prints each - * path. + * Renders all layered cover-letter preset gallery PDFs and prints + * each path. * - * @param args optional first arg = preset id filter + * @param args optional first arg = slug filter * @throws Exception if any rendering fails */ public static void main(String[] args) throws Exception { @@ -113,24 +109,28 @@ public static void main(String[] args) throws Exception { } } - private static Path renderOne(CoverLetterSpec spec, Run letter) throws Exception { - Path outputFile = ExampleOutputPaths.prepare("templates/coverletter", "cover-letter-" + letter.id + ".pdf"); - DocumentTemplate template = letter.factory.apply(THEME); + private static Path renderOne(CoverLetterDocument doc, Run letter) throws Exception { + Path outputFile = ExampleOutputPaths.prepare( + "templates/coverletter", "cover-letter-" + letter.id() + ".pdf"); + DocumentTemplate template = letter.factory().get(); + float m = (float) letter.margin(); try (DocumentSession document = GraphCompose.document(outputFile) .pageSize(DocumentPageSize.A4) - .margin(48, 48, 48, 48) + .margin(m, m, m, m) .create()) { - template.compose(document, spec); + template.compose(document, doc); document.buildPdf(); } return outputFile; } - private static Run run(String id, Function> factory) { - return new Run(id, factory); + private static Run run(String id, double margin, + Supplier> factory) { + return new Run(id, margin, factory); } - private record Run(String id, Function> factory) { + private record Run(String id, double margin, + Supplier> factory) { } } diff --git a/examples/src/main/java/com/demcha/examples/templates/coverletter/v2/CvBlueBannerLetterV2Example.java b/examples/src/main/java/com/demcha/examples/templates/coverletter/v2/CvBlueBannerLetterV2Example.java new file mode 100644 index 00000000..60d4ea9b --- /dev/null +++ b/examples/src/main/java/com/demcha/examples/templates/coverletter/v2/CvBlueBannerLetterV2Example.java @@ -0,0 +1,47 @@ +package com.demcha.examples.templates.coverletter.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.coverletter.v2.data.CoverLetterDocument; +import com.demcha.compose.document.templates.coverletter.v2.presets.BlueBannerLetter; +import com.demcha.examples.support.ExampleDataFactory; +import com.demcha.examples.support.ExampleOutputPaths; + +import java.nio.file.Path; + +/** + * Renders the v2 Blue Banner cover-letter preset β€” centred PT-Serif + * spaced-caps name over a compact centred contact row (brand carried by + * the blue-toned theme), then a single-column letter body. + * + *

Output: + * {@code examples/target/generated-pdfs/templates/coverletter/cover-letter-blue-banner-v2.pdf}.

+ */ +public final class CvBlueBannerLetterV2Example { + + private CvBlueBannerLetterV2Example() { + } + + public static Path generate() throws Exception { + Path outputFile = ExampleOutputPaths.prepare( + "templates/coverletter", "cover-letter-blue-banner-v2.pdf"); + CoverLetterDocument doc = ExampleDataFactory.sampleCoverLetterDocumentV2(); + DocumentTemplate template = BlueBannerLetter.create(); + + float m = (float) BlueBannerLetter.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()); + } +} diff --git a/examples/src/main/java/com/demcha/examples/templates/coverletter/v2/CvBoxedSectionsLetterV2Example.java b/examples/src/main/java/com/demcha/examples/templates/coverletter/v2/CvBoxedSectionsLetterV2Example.java new file mode 100644 index 00000000..5e6ad803 --- /dev/null +++ b/examples/src/main/java/com/demcha/examples/templates/coverletter/v2/CvBoxedSectionsLetterV2Example.java @@ -0,0 +1,47 @@ +package com.demcha.examples.templates.coverletter.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.coverletter.v2.data.CoverLetterDocument; +import com.demcha.compose.document.templates.coverletter.v2.presets.BoxedSectionsLetter; +import com.demcha.examples.support.ExampleDataFactory; +import com.demcha.examples.support.ExampleOutputPaths; + +import java.nio.file.Path; + +/** + * Renders the v2 Boxed Sections cover-letter preset β€” centred + * spaced-caps PT-Serif masthead with rules, then a single-column + * letter body. Pair with {@code CvBoxedV2Example}. + * + *

Output: + * {@code examples/target/generated-pdfs/templates/coverletter/cover-letter-boxed-sections-v2.pdf}.

+ */ +public final class CvBoxedSectionsLetterV2Example { + + private CvBoxedSectionsLetterV2Example() { + } + + public static Path generate() throws Exception { + Path outputFile = ExampleOutputPaths.prepare( + "templates/coverletter", "cover-letter-boxed-sections-v2.pdf"); + CoverLetterDocument doc = ExampleDataFactory.sampleCoverLetterDocumentV2(); + DocumentTemplate template = BoxedSectionsLetter.create(); + + float m = (float) BoxedSectionsLetter.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()); + } +} diff --git a/examples/src/main/java/com/demcha/examples/templates/coverletter/v2/CvCenteredHeadlineLetterV2Example.java b/examples/src/main/java/com/demcha/examples/templates/coverletter/v2/CvCenteredHeadlineLetterV2Example.java new file mode 100644 index 00000000..7614cd97 --- /dev/null +++ b/examples/src/main/java/com/demcha/examples/templates/coverletter/v2/CvCenteredHeadlineLetterV2Example.java @@ -0,0 +1,47 @@ +package com.demcha.examples.templates.coverletter.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.coverletter.v2.data.CoverLetterDocument; +import com.demcha.compose.document.templates.coverletter.v2.presets.CenteredHeadlineLetter; +import com.demcha.examples.support.ExampleDataFactory; +import com.demcha.examples.support.ExampleOutputPaths; + +import java.nio.file.Path; + +/** + * Renders the v2 Centered Headline cover-letter preset β€” centred + * spaced-caps Poppins name, spaced-caps subheadline, framing rules, and + * centred contact, then a single-column letter body. + * + *

Output: + * {@code examples/target/generated-pdfs/templates/coverletter/cover-letter-centered-headline-v2.pdf}.

+ */ +public final class CvCenteredHeadlineLetterV2Example { + + private CvCenteredHeadlineLetterV2Example() { + } + + public static Path generate() throws Exception { + Path outputFile = ExampleOutputPaths.prepare( + "templates/coverletter", "cover-letter-centered-headline-v2.pdf"); + CoverLetterDocument doc = ExampleDataFactory.sampleCoverLetterDocumentV2(); + DocumentTemplate template = CenteredHeadlineLetter.create(); + + float m = (float) CenteredHeadlineLetter.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()); + } +} diff --git a/examples/src/main/java/com/demcha/examples/templates/coverletter/v2/CvClassicSerifLetterV2Example.java b/examples/src/main/java/com/demcha/examples/templates/coverletter/v2/CvClassicSerifLetterV2Example.java new file mode 100644 index 00000000..6a2724ac --- /dev/null +++ b/examples/src/main/java/com/demcha/examples/templates/coverletter/v2/CvClassicSerifLetterV2Example.java @@ -0,0 +1,48 @@ +package com.demcha.examples.templates.coverletter.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.coverletter.v2.data.CoverLetterDocument; +import com.demcha.compose.document.templates.coverletter.v2.presets.ClassicSerifLetter; +import com.demcha.examples.support.ExampleDataFactory; +import com.demcha.examples.support.ExampleOutputPaths; + +import java.nio.file.Path; + +/** + * Renders the v2 Classic Serif cover-letter preset β€” centred + * spaced-caps PT-Serif masthead, tan rule, centred contact with + * tan-accent links, then a single-column letter body. Pair with + * {@code CvClassicSerifExample}. + * + *

Output: + * {@code examples/target/generated-pdfs/templates/coverletter/cover-letter-classic-serif-v2.pdf}.

+ */ +public final class CvClassicSerifLetterV2Example { + + private CvClassicSerifLetterV2Example() { + } + + public static Path generate() throws Exception { + Path outputFile = ExampleOutputPaths.prepare( + "templates/coverletter", "cover-letter-classic-serif-v2.pdf"); + CoverLetterDocument doc = ExampleDataFactory.sampleCoverLetterDocumentV2(); + DocumentTemplate template = ClassicSerifLetter.create(); + + float m = (float) ClassicSerifLetter.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()); + } +} diff --git a/examples/src/main/java/com/demcha/examples/templates/coverletter/v2/CvCompactMonoLetterV2Example.java b/examples/src/main/java/com/demcha/examples/templates/coverletter/v2/CvCompactMonoLetterV2Example.java new file mode 100644 index 00000000..5161265b --- /dev/null +++ b/examples/src/main/java/com/demcha/examples/templates/coverletter/v2/CvCompactMonoLetterV2Example.java @@ -0,0 +1,48 @@ +package com.demcha.examples.templates.coverletter.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.coverletter.v2.data.CoverLetterDocument; +import com.demcha.compose.document.templates.coverletter.v2.presets.CompactMonoLetter; +import com.demcha.examples.support.ExampleDataFactory; +import com.demcha.examples.support.ExampleOutputPaths; + +import java.nio.file.Path; + +/** + * Renders the v2 Compact Mono cover-letter preset β€” near-black rounded + * command-bar header (UPPERCASE name, left-aligned contact with cyan + * links) then a single-column letter body. Pair with + * {@code CvCompactMonoExample}. + * + *

Output: + * {@code examples/target/generated-pdfs/templates/coverletter/cover-letter-compact-mono-v2.pdf}.

+ */ +public final class CvCompactMonoLetterV2Example { + + private CvCompactMonoLetterV2Example() { + } + + public static Path generate() throws Exception { + Path outputFile = ExampleOutputPaths.prepare( + "templates/coverletter", "cover-letter-compact-mono-v2.pdf"); + CoverLetterDocument doc = ExampleDataFactory.sampleCoverLetterDocumentV2(); + DocumentTemplate template = CompactMonoLetter.create(); + + float m = (float) CompactMonoLetter.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()); + } +} diff --git a/examples/src/main/java/com/demcha/examples/templates/coverletter/v2/CvEditorialBlueLetterV2Example.java b/examples/src/main/java/com/demcha/examples/templates/coverletter/v2/CvEditorialBlueLetterV2Example.java new file mode 100644 index 00000000..1b3cdbde --- /dev/null +++ b/examples/src/main/java/com/demcha/examples/templates/coverletter/v2/CvEditorialBlueLetterV2Example.java @@ -0,0 +1,48 @@ +package com.demcha.examples.templates.coverletter.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.coverletter.v2.data.CoverLetterDocument; +import com.demcha.compose.document.templates.coverletter.v2.presets.EditorialBlueLetter; +import com.demcha.examples.support.ExampleDataFactory; +import com.demcha.examples.support.ExampleOutputPaths; + +import java.nio.file.Path; + +/** + * Renders the v2 Editorial Blue cover-letter preset β€” centred navy + * Helvetica masthead (name + job title), centred contact with blue + * links, then a single-column letter body. Pair with + * {@code CvEditorialBlueExample}. + * + *

Output: + * {@code examples/target/generated-pdfs/templates/coverletter/cover-letter-editorial-blue-v2.pdf}.

+ */ +public final class CvEditorialBlueLetterV2Example { + + private CvEditorialBlueLetterV2Example() { + } + + public static Path generate() throws Exception { + Path outputFile = ExampleOutputPaths.prepare( + "templates/coverletter", "cover-letter-editorial-blue-v2.pdf"); + CoverLetterDocument doc = ExampleDataFactory.sampleCoverLetterDocumentV2(); + DocumentTemplate template = EditorialBlueLetter.create(); + + float m = (float) EditorialBlueLetter.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()); + } +} diff --git a/examples/src/main/java/com/demcha/examples/templates/coverletter/v2/CvEngineeringResumeLetterV2Example.java b/examples/src/main/java/com/demcha/examples/templates/coverletter/v2/CvEngineeringResumeLetterV2Example.java new file mode 100644 index 00000000..209bf300 --- /dev/null +++ b/examples/src/main/java/com/demcha/examples/templates/coverletter/v2/CvEngineeringResumeLetterV2Example.java @@ -0,0 +1,48 @@ +package com.demcha.examples.templates.coverletter.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.coverletter.v2.data.CoverLetterDocument; +import com.demcha.compose.document.templates.coverletter.v2.presets.EngineeringResumeLetter; +import com.demcha.examples.support.ExampleDataFactory; +import com.demcha.examples.support.ExampleOutputPaths; + +import java.nio.file.Path; + +/** + * Renders the v2 Engineering Resume cover-letter preset β€” full-width + * navy command header (UPPERCASE name + role subtitle, right-aligned + * contact with cyan-green links, green accent strip) then a + * single-column letter body. Pair with {@code CvEngineeringResumeExample}. + * + *

Output: + * {@code examples/target/generated-pdfs/templates/coverletter/cover-letter-engineering-resume-v2.pdf}.

+ */ +public final class CvEngineeringResumeLetterV2Example { + + private CvEngineeringResumeLetterV2Example() { + } + + public static Path generate() throws Exception { + Path outputFile = ExampleOutputPaths.prepare( + "templates/coverletter", "cover-letter-engineering-resume-v2.pdf"); + CoverLetterDocument doc = ExampleDataFactory.sampleCoverLetterDocumentV2(); + DocumentTemplate template = EngineeringResumeLetter.create(); + + float m = (float) EngineeringResumeLetter.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()); + } +} diff --git a/examples/src/main/java/com/demcha/examples/templates/coverletter/v2/CvExecutiveLetterV2Example.java b/examples/src/main/java/com/demcha/examples/templates/coverletter/v2/CvExecutiveLetterV2Example.java new file mode 100644 index 00000000..db9fa34c --- /dev/null +++ b/examples/src/main/java/com/demcha/examples/templates/coverletter/v2/CvExecutiveLetterV2Example.java @@ -0,0 +1,51 @@ +package com.demcha.examples.templates.coverletter.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.coverletter.v2.data.CoverLetterDocument; +import com.demcha.compose.document.templates.coverletter.v2.presets.ExecutiveLetter; +import com.demcha.examples.support.ExampleDataFactory; +import com.demcha.examples.support.ExampleOutputPaths; + +import java.nio.file.Path; + +/** + * Renders the v2 Executive cover-letter preset against the shared + * Jordan Rivera identity β€” the same masthead as the Executive CV + * (uppercase Poppins slate name, Lato meta + bronze link row, + * full-width muted rule) followed by a single-column letter body. + * + *

Pair with {@code CvExecutiveExample} to view the CV and letter as + * a matched set.

+ * + *

Output: + * {@code examples/target/generated-pdfs/templates/coverletter/cover-letter-executive-v2.pdf}.

+ */ +public final class CvExecutiveLetterV2Example { + + private CvExecutiveLetterV2Example() { + } + + public static Path generate() throws Exception { + Path outputFile = ExampleOutputPaths.prepare( + "templates/coverletter", "cover-letter-executive-v2.pdf"); + CoverLetterDocument doc = ExampleDataFactory.sampleCoverLetterDocumentV2(); + DocumentTemplate template = ExecutiveLetter.create(); + + float m = (float) ExecutiveLetter.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()); + } +} diff --git a/examples/src/main/java/com/demcha/examples/templates/coverletter/v2/CvModernProfessionalLetterV2Example.java b/examples/src/main/java/com/demcha/examples/templates/coverletter/v2/CvModernProfessionalLetterV2Example.java new file mode 100644 index 00000000..07bfe2e7 --- /dev/null +++ b/examples/src/main/java/com/demcha/examples/templates/coverletter/v2/CvModernProfessionalLetterV2Example.java @@ -0,0 +1,52 @@ +package com.demcha.examples.templates.coverletter.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.coverletter.v2.data.CoverLetterDocument; +import com.demcha.compose.document.templates.coverletter.v2.presets.ModernProfessionalLetter; +import com.demcha.examples.support.ExampleDataFactory; +import com.demcha.examples.support.ExampleOutputPaths; + +import java.nio.file.Path; + +/** + * Renders the v2 Modern Professional cover-letter preset against the + * shared Jordan Rivera identity β€” the same masthead as the Modern + * Professional CV (right-aligned slate-blue Helvetica name, two-row + * right-aligned contact stack with royal-blue links, bottom accent + * rule) followed by a single-column letter body. + * + *

Pair with {@code CvModernV2Example} to view the CV and letter as + * a matched set.

+ * + *

Output: + * {@code examples/target/generated-pdfs/templates/coverletter/cover-letter-modern-professional-v2.pdf}.

+ */ +public final class CvModernProfessionalLetterV2Example { + + private CvModernProfessionalLetterV2Example() { + } + + public static Path generate() throws Exception { + Path outputFile = ExampleOutputPaths.prepare( + "templates/coverletter", "cover-letter-modern-professional-v2.pdf"); + CoverLetterDocument doc = ExampleDataFactory.sampleCoverLetterDocumentV2(); + DocumentTemplate template = ModernProfessionalLetter.create(); + + float m = (float) ModernProfessionalLetter.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()); + } +} diff --git a/examples/src/main/java/com/demcha/examples/templates/coverletter/v2/CvMonogramSidebarLetterV2Example.java b/examples/src/main/java/com/demcha/examples/templates/coverletter/v2/CvMonogramSidebarLetterV2Example.java new file mode 100644 index 00000000..6b66fc6f --- /dev/null +++ b/examples/src/main/java/com/demcha/examples/templates/coverletter/v2/CvMonogramSidebarLetterV2Example.java @@ -0,0 +1,48 @@ +package com.demcha.examples.templates.coverletter.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.coverletter.v2.data.CoverLetterDocument; +import com.demcha.compose.document.templates.coverletter.v2.presets.MonogramSidebarLetter; +import com.demcha.examples.support.ExampleDataFactory; +import com.demcha.examples.support.ExampleOutputPaths; + +import java.nio.file.Path; + +/** + * Renders the v2 Monogram Sidebar cover-letter preset β€” centred + * monogram-ring badge over a stacked spaced-caps name, gold role line, + * and centred contact, then a single-column letter body. Pair with + * {@code CvMonogramSidebarExample}. + * + *

Output: + * {@code examples/target/generated-pdfs/templates/coverletter/cover-letter-monogram-sidebar-v2.pdf}.

+ */ +public final class CvMonogramSidebarLetterV2Example { + + private CvMonogramSidebarLetterV2Example() { + } + + public static Path generate() throws Exception { + Path outputFile = ExampleOutputPaths.prepare( + "templates/coverletter", "cover-letter-monogram-sidebar-v2.pdf"); + CoverLetterDocument doc = ExampleDataFactory.sampleCoverLetterDocumentV2(); + DocumentTemplate template = MonogramSidebarLetter.create(); + + float m = (float) MonogramSidebarLetter.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()); + } +} diff --git a/examples/src/main/java/com/demcha/examples/templates/coverletter/v2/CvNordicCleanLetterV2Example.java b/examples/src/main/java/com/demcha/examples/templates/coverletter/v2/CvNordicCleanLetterV2Example.java new file mode 100644 index 00000000..f72529d3 --- /dev/null +++ b/examples/src/main/java/com/demcha/examples/templates/coverletter/v2/CvNordicCleanLetterV2Example.java @@ -0,0 +1,48 @@ +package com.demcha.examples.templates.coverletter.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.coverletter.v2.data.CoverLetterDocument; +import com.demcha.compose.document.templates.coverletter.v2.presets.NordicCleanLetter; +import com.demcha.examples.support.ExampleDataFactory; +import com.demcha.examples.support.ExampleOutputPaths; + +import java.nio.file.Path; + +/** + * Renders the v2 Nordic Clean cover-letter preset β€” left-aligned + * UPPERCASE name with a teal accent bar + role sub-line, right-aligned + * stacked contact with teal links, then a single-column letter body. + * Pair with {@code CvNordicV2Example}. + * + *

Output: + * {@code examples/target/generated-pdfs/templates/coverletter/cover-letter-nordic-clean-v2.pdf}.

+ */ +public final class CvNordicCleanLetterV2Example { + + private CvNordicCleanLetterV2Example() { + } + + public static Path generate() throws Exception { + Path outputFile = ExampleOutputPaths.prepare( + "templates/coverletter", "cover-letter-nordic-clean-v2.pdf"); + CoverLetterDocument doc = ExampleDataFactory.sampleCoverLetterDocumentV2(); + DocumentTemplate template = NordicCleanLetter.create(); + + float m = (float) NordicCleanLetter.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()); + } +} diff --git a/examples/src/main/java/com/demcha/examples/templates/coverletter/v2/CvPanelLetterV2Example.java b/examples/src/main/java/com/demcha/examples/templates/coverletter/v2/CvPanelLetterV2Example.java new file mode 100644 index 00000000..913e11a8 --- /dev/null +++ b/examples/src/main/java/com/demcha/examples/templates/coverletter/v2/CvPanelLetterV2Example.java @@ -0,0 +1,47 @@ +package com.demcha.examples.templates.coverletter.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.coverletter.v2.data.CoverLetterDocument; +import com.demcha.compose.document.templates.coverletter.v2.presets.PanelLetter; +import com.demcha.examples.support.ExampleDataFactory; +import com.demcha.examples.support.ExampleOutputPaths; + +import java.nio.file.Path; + +/** + * Renders the v2 Panel cover-letter preset β€” full-width pale-teal + * header card (centred Poppins name, job title, meta + teal links) then + * a single-column letter body. Pair with {@code CvPanelExample}. + * + *

Output: + * {@code examples/target/generated-pdfs/templates/coverletter/cover-letter-panel-v2.pdf}.

+ */ +public final class CvPanelLetterV2Example { + + private CvPanelLetterV2Example() { + } + + public static Path generate() throws Exception { + Path outputFile = ExampleOutputPaths.prepare( + "templates/coverletter", "cover-letter-panel-v2.pdf"); + CoverLetterDocument doc = ExampleDataFactory.sampleCoverLetterDocumentV2(); + DocumentTemplate template = PanelLetter.create(); + + float m = (float) PanelLetter.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()); + } +} diff --git a/examples/src/main/java/com/demcha/examples/templates/coverletter/v2/CvSidebarPortraitLetterV2Example.java b/examples/src/main/java/com/demcha/examples/templates/coverletter/v2/CvSidebarPortraitLetterV2Example.java new file mode 100644 index 00000000..4bfb6174 --- /dev/null +++ b/examples/src/main/java/com/demcha/examples/templates/coverletter/v2/CvSidebarPortraitLetterV2Example.java @@ -0,0 +1,48 @@ +package com.demcha.examples.templates.coverletter.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.coverletter.v2.data.CoverLetterDocument; +import com.demcha.compose.document.templates.coverletter.v2.presets.SidebarPortraitLetter; +import com.demcha.examples.support.ExampleDataFactory; +import com.demcha.examples.support.ExampleOutputPaths; + +import java.nio.file.Path; + +/** + * Renders the v2 Sidebar Portrait cover-letter preset β€” full-width beige + * hero band (centred serif name + spaced-caps role) + centred contact, + * then a single-column letter body. Pair with + * {@code CvSidebarPortraitExample}. + * + *

Output: + * {@code examples/target/generated-pdfs/templates/coverletter/cover-letter-sidebar-portrait-v2.pdf}.

+ */ +public final class CvSidebarPortraitLetterV2Example { + + private CvSidebarPortraitLetterV2Example() { + } + + public static Path generate() throws Exception { + Path outputFile = ExampleOutputPaths.prepare( + "templates/coverletter", "cover-letter-sidebar-portrait-v2.pdf"); + CoverLetterDocument doc = ExampleDataFactory.sampleCoverLetterDocumentV2(); + DocumentTemplate template = SidebarPortraitLetter.create(); + + float m = (float) SidebarPortraitLetter.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()); + } +} diff --git a/examples/src/main/java/com/demcha/examples/templates/coverletter/v2/CvTimelineMinimalLetterV2Example.java b/examples/src/main/java/com/demcha/examples/templates/coverletter/v2/CvTimelineMinimalLetterV2Example.java new file mode 100644 index 00000000..384e2f67 --- /dev/null +++ b/examples/src/main/java/com/demcha/examples/templates/coverletter/v2/CvTimelineMinimalLetterV2Example.java @@ -0,0 +1,48 @@ +package com.demcha.examples.templates.coverletter.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.coverletter.v2.data.CoverLetterDocument; +import com.demcha.compose.document.templates.coverletter.v2.presets.TimelineMinimalLetter; +import com.demcha.examples.support.ExampleDataFactory; +import com.demcha.examples.support.ExampleOutputPaths; + +import java.nio.file.Path; + +/** + * Renders the v2 Timeline Minimal cover-letter preset β€” left spaced-caps + * name + role over a right-aligned PNG-icon contact stack under a thin + * rule, then a single-column letter body. Pair with + * {@code CvTimelineMinimalExample}. + * + *

Output: + * {@code examples/target/generated-pdfs/templates/coverletter/cover-letter-timeline-minimal-v2.pdf}.

+ */ +public final class CvTimelineMinimalLetterV2Example { + + private CvTimelineMinimalLetterV2Example() { + } + + public static Path generate() throws Exception { + Path outputFile = ExampleOutputPaths.prepare( + "templates/coverletter", "cover-letter-timeline-minimal-v2.pdf"); + CoverLetterDocument doc = ExampleDataFactory.sampleCoverLetterDocumentV2(); + DocumentTemplate template = TimelineMinimalLetter.create(); + + float m = (float) TimelineMinimalLetter.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()); + } +} diff --git a/examples/src/main/java/com/demcha/examples/templates/cv/CvFileExample.java b/examples/src/main/java/com/demcha/examples/templates/cv/CvFileExample.java index 632f2017..b90ec116 100644 --- a/examples/src/main/java/com/demcha/examples/templates/cv/CvFileExample.java +++ b/examples/src/main/java/com/demcha/examples/templates/cv/CvFileExample.java @@ -4,19 +4,19 @@ 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.presets.ModernProfessional; -import com.demcha.compose.document.templates.cv.spec.CvSpec; -import com.demcha.compose.document.theme.BusinessTheme; +import com.demcha.compose.document.templates.cv.v2.data.CvDocument; +import com.demcha.compose.document.templates.cv.v2.presets.ModernProfessional; import com.demcha.examples.support.ExampleDataFactory; import com.demcha.examples.support.ExampleOutputPaths; import java.nio.file.Path; /** - * Renders the canonical Modern Professional CV using the Templates v2 - * preset {@link ModernProfessional}. This replaces the legacy - * {@code CvTemplateV1} example wiring; the visual signature and - * sample data both come from the v2 surface. + * Canonical single-file CV authoring demo using the layered + * {@code cv.v2} surface β€” the simplest "author one CV" path: build a + * {@link CvDocument}, pick a preset's {@code create()} factory, compose. + * The full 14-preset gallery lives in + * {@link CvTemplateGalleryFileExample}. */ public final class CvFileExample { @@ -24,21 +24,22 @@ private CvFileExample() { } /** - * Renders the example PDF to {@code generated-pdfs/cv-modern-professional.pdf}. + * Renders the example PDF to {@code generated-pdfs/templates/cv/cv-modern-professional.pdf}. * * @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-modern-professional.pdf"); - CvSpec spec = ExampleDataFactory.sampleCvSpecV2(); - DocumentTemplate template = ModernProfessional.create(BusinessTheme.modern()); + CvDocument doc = ExampleDataFactory.sampleCvDocumentV2(); + DocumentTemplate template = ModernProfessional.create(); + float m = (float) ModernProfessional.RECOMMENDED_MARGIN; try (DocumentSession document = GraphCompose.document(outputFile) .pageSize(DocumentPageSize.A4) - .margin(28, 28, 28, 28) + .margin(m, m, m, m) .create()) { - template.compose(document, spec); + template.compose(document, doc); document.buildPdf(); } return outputFile; diff --git a/examples/src/main/java/com/demcha/examples/templates/cv/CvTemplateGalleryFileExample.java b/examples/src/main/java/com/demcha/examples/templates/cv/CvTemplateGalleryFileExample.java index 3251f6e0..5ecce10d 100644 --- a/examples/src/main/java/com/demcha/examples/templates/cv/CvTemplateGalleryFileExample.java +++ b/examples/src/main/java/com/demcha/examples/templates/cv/CvTemplateGalleryFileExample.java @@ -4,50 +4,48 @@ 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.presets.BlueBanner; -import com.demcha.compose.document.templates.cv.presets.BoxedSections; -import com.demcha.compose.document.templates.cv.presets.CenteredHeadline; -import com.demcha.compose.document.templates.cv.presets.ClassicSerif; -import com.demcha.compose.document.templates.cv.presets.CompactMono; -import com.demcha.compose.document.templates.cv.presets.EditorialBlue; -import com.demcha.compose.document.templates.cv.presets.EngineeringResume; -import com.demcha.compose.document.templates.cv.presets.Executive; -import com.demcha.compose.document.templates.cv.presets.ModernProfessional; -import com.demcha.compose.document.templates.cv.presets.MonogramSidebar; -import com.demcha.compose.document.templates.cv.presets.NordicClean; -import com.demcha.compose.document.templates.cv.presets.Panel; -import com.demcha.compose.document.templates.cv.presets.SidebarPortrait; -import com.demcha.compose.document.templates.cv.presets.TimelineMinimal; -import com.demcha.compose.document.templates.cv.spec.CvSpec; -import com.demcha.compose.document.theme.BusinessTheme; +import com.demcha.compose.document.templates.cv.v2.data.CvDocument; +import com.demcha.compose.document.templates.cv.v2.presets.BlueBanner; +import com.demcha.compose.document.templates.cv.v2.presets.BoxedSections; +import com.demcha.compose.document.templates.cv.v2.presets.CenteredHeadline; +import com.demcha.compose.document.templates.cv.v2.presets.ClassicSerif; +import com.demcha.compose.document.templates.cv.v2.presets.CompactMono; +import com.demcha.compose.document.templates.cv.v2.presets.EditorialBlue; +import com.demcha.compose.document.templates.cv.v2.presets.EngineeringResume; +import com.demcha.compose.document.templates.cv.v2.presets.Executive; +import com.demcha.compose.document.templates.cv.v2.presets.ModernProfessional; +import com.demcha.compose.document.templates.cv.v2.presets.MonogramSidebar; +import com.demcha.compose.document.templates.cv.v2.presets.NordicClean; +import com.demcha.compose.document.templates.cv.v2.presets.Panel; +import com.demcha.compose.document.templates.cv.v2.presets.SidebarPortrait; +import com.demcha.compose.document.templates.cv.v2.presets.TimelineMinimal; import com.demcha.examples.support.ExampleDataFactory; import com.demcha.examples.support.ExampleOutputPaths; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; -import java.util.function.Function; +import java.util.function.Supplier; /** - * Renders all 14 Templates v2 CV presets against the same shared - * sample data. Each PDF lands in - * {@code examples/target/generated-pdfs/cv-.pdf} where - * {@code } is the preset's stable identifier (e.g. + * Renders all 14 layered CV presets ({@code cv.v2.presets.*} β€” the + * polished current standard) against the same shared sample + * {@link CvDocument}. Each PDF lands in + * {@code examples/target/generated-pdfs/templates/cv/cv-.pdf} + * where {@code } is the preset's stable identifier (e.g. * {@code cv-modern-professional.pdf}). * - *

This is the single source of truth for the example CV gallery - * in v2. The matching cover-letter gallery lives in - * {@link CoverLetterTemplateGalleryFileExample}.

+ *

This is the single source of truth for the CV showcase gallery. + * The matching cover-letter gallery lives in + * {@link com.demcha.examples.templates.coverletter.CoverLetterTemplateGalleryFileExample}.

*/ public final class CvTemplateGalleryFileExample { - private static final BusinessTheme THEME = BusinessTheme.modern(); - private CvTemplateGalleryFileExample() { } /** - * Renders all 14 v2 CV preset gallery PDFs. + * Renders all 14 layered CV preset gallery PDFs. * * @return list of absolute paths of the rendered PDFs in source * order @@ -83,19 +81,19 @@ public static List generate(String presetId) throws Exception { run(SidebarPortrait.ID, SidebarPortrait.RECOMMENDED_MARGIN, SidebarPortrait::create), run(MonogramSidebar.ID, MonogramSidebar.RECOMMENDED_MARGIN, MonogramSidebar::create)); - CvSpec spec = ExampleDataFactory.sampleCvSpecV2(); + CvDocument doc = ExampleDataFactory.sampleCvDocumentV2(); List generated = new ArrayList<>(); for (Run cv : runs) { - if (presetId != null && !cv.id.equals(presetId)) { + if (presetId != null && !cv.id().equals(presetId)) { continue; } - generated.add(renderOne(spec, cv)); + generated.add(renderOne(doc, cv)); } return List.copyOf(generated); } /** - * Renders all v2 CV preset gallery PDFs and prints each path. + * Renders all layered CV preset gallery PDFs and prints each path. * * @param args optional first arg = preset id filter * @throws Exception if any rendering fails @@ -107,25 +105,27 @@ public static void main(String[] args) throws Exception { } } - private static Path renderOne(CvSpec spec, Run cv) throws Exception { - Path outputFile = ExampleOutputPaths.prepare("templates/cv", "cv-" + cv.id + ".pdf"); - DocumentTemplate template = cv.factory.apply(THEME); + private static Path renderOne(CvDocument doc, Run cv) throws Exception { + Path outputFile = ExampleOutputPaths.prepare("templates/cv", "cv-" + cv.id() + ".pdf"); + DocumentTemplate template = cv.factory().get(); - float m = (float) cv.margin; + float m = (float) cv.margin(); try (DocumentSession document = GraphCompose.document(outputFile) .pageSize(DocumentPageSize.A4) .margin(m, m, m, m) .create()) { - template.compose(document, spec); + template.compose(document, doc); document.buildPdf(); } return outputFile; } - private static Run run(String id, double margin, Function> factory) { + private static Run run(String id, double margin, + Supplier> factory) { return new Run(id, margin, factory); } - private record Run(String id, double margin, Function> factory) { + private record Run(String id, double margin, + Supplier> factory) { } } diff --git a/src/main/java/com/demcha/compose/document/templates/coverletter/builder/CoverLetterBuilder.java b/src/main/java/com/demcha/compose/document/templates/coverletter/builder/CoverLetterBuilder.java index e270faec..e41a8818 100644 --- a/src/main/java/com/demcha/compose/document/templates/coverletter/builder/CoverLetterBuilder.java +++ b/src/main/java/com/demcha/compose/document/templates/coverletter/builder/CoverLetterBuilder.java @@ -34,7 +34,15 @@ * {@code header}, {@code layout}, {@code bodyStyle}, {@code spacing}, * and at least implicit alignment via the layout / body style) must * be configured before calling {@link #build()}.

+ * + * @deprecated Superseded by the layered …v2… surface (the current + * standard) β€” the layered model + * {@link com.demcha.compose.document.templates.coverletter.v2.data.CoverLetterDocument} + * plus the {@code coverletter.v2} presets. Kept for backward + * compatibility; scheduled for removal in a future major. See + * {@code docs/templates/v2-layered/}. */ +@Deprecated(since = "1.7.0", forRemoval = true) public final class CoverLetterBuilder { private String id; diff --git a/src/main/java/com/demcha/compose/document/templates/coverletter/layouts/LetterFormat.java b/src/main/java/com/demcha/compose/document/templates/coverletter/layouts/LetterFormat.java index b47202e1..4d5c5227 100644 --- a/src/main/java/com/demcha/compose/document/templates/coverletter/layouts/LetterFormat.java +++ b/src/main/java/com/demcha/compose/document/templates/coverletter/layouts/LetterFormat.java @@ -17,7 +17,15 @@ * letter is structurally simpler than a CV (one continuous reading * flow), so the layout takes the rendered nodes in source order and * emits one container.

+ * + * @deprecated Superseded by the layered …v2… surface (the current + * standard) β€” the layered model + * {@link com.demcha.compose.document.templates.coverletter.v2.data.CoverLetterDocument} + * plus the {@code coverletter.v2} presets. Kept for backward + * compatibility; scheduled for removal in a future major. See + * {@code docs/templates/v2-layered/}. */ +@Deprecated(since = "1.7.0", forRemoval = true) public final class LetterFormat { private static final String LAYOUT_NAME = "layout.letterFormat"; diff --git a/src/main/java/com/demcha/compose/document/templates/coverletter/layouts/package-info.java b/src/main/java/com/demcha/compose/document/templates/coverletter/layouts/package-info.java index d0198827..106290a3 100644 --- a/src/main/java/com/demcha/compose/document/templates/coverletter/layouts/package-info.java +++ b/src/main/java/com/demcha/compose/document/templates/coverletter/layouts/package-info.java @@ -1,13 +1,28 @@ /** - * Templates v2 cover-letter layouts β€” caркасы that arrange a header + * Superseded Gen-2 cover-letter layouts β€” frames that arrange a header * plus letter blocks (greeting, body paragraphs, closing) into a * final document tree. * + *

Deprecated surface. These are the older Gen-2 + * cover-letter layouts. They are not the current standard. The + * current standard is the layered surface + * {@code com.demcha.compose.document.templates.coverletter.v2} (data / theme / + * components / widgets / presets). This package is kept only for backward + * compatibility and is scheduled for removal in a future major.

+ * *

Currently a single layout * ({@link com.demcha.compose.document.templates.coverletter.layouts.LetterFormat}) - * covers all 14 cover-letter pair presets. Additional layouts will - * land here when their preset migrations require them.

+ * covers all 14 cover-letter pair presets.

+ * + *

New code should target the layered {@code coverletter.v2} surface + * instead. See {@code docs/templates/v2-layered/}.

* * @since 1.6.0 + * @deprecated Superseded by the layered + * {@code com.demcha.compose.document.templates.coverletter.v2} + * surface (the current standard). This Gen-2 package is kept for + * backward compatibility and will be removed in a future major. + * See {@code docs/templates/v2-layered/}. */ +@Deprecated(since = "1.7.0", forRemoval = true) package com.demcha.compose.document.templates.coverletter.layouts; diff --git a/src/main/java/com/demcha/compose/document/templates/coverletter/package-info.java b/src/main/java/com/demcha/compose/document/templates/coverletter/package-info.java index 18e85a12..5e191747 100644 --- a/src/main/java/com/demcha/compose/document/templates/coverletter/package-info.java +++ b/src/main/java/com/demcha/compose/document/templates/coverletter/package-info.java @@ -1,16 +1,18 @@ /** - * Templates v2 cover-letter domain β€” layouts, presets, builder, and spec data types. + * Superseded Gen-2 cover-letter domain β€” layout, presets, builder, and spec + * data types. * - *

This package is the home of all cover-letter templates in the v2 - * architecture. The user requirement is one cover-letter preset paired - * with each CV preset (same Header / Typography / Palette), so writers - * can ship a CV and a matching cover letter with consistent visual - * identity.

+ *

Deprecated surface. This package is the older Gen-2 + * cover-letter template stack. It is not the current standard. The + * current standard is the layered surface + * {@code com.demcha.compose.document.templates.coverletter.v2} (data / theme / + * components / widgets / presets). This package is kept only for backward + * compatibility and is scheduled for removal in a future major.

* - *

Sub-packages partition the domain by concern:

+ *

Sub-packages partition the (deprecated) domain by concern:

* *
    - *
  • {@code coverletter.layouts} β€” slot каркасы (LetterFormat β€” a + *
  • {@code coverletter.layouts} β€” slot frames (LetterFormat β€” a * single-column layout with generous side margins for letter body * text).
  • *
  • {@code coverletter.presets} β€” flat copy-and-tweak preset classes, @@ -25,8 +27,8 @@ * with header, greeting, body paragraphs, closing).
  • *
* - *

Sub-packages will be populated during Phase E of the Templates v2 - * migration.

+ *

New code should target the layered {@code coverletter.v2} surface + * instead. See {@code docs/templates/v2-layered/}.

* *

Naming note: the user-facing concept is * "cover-letter" with a hyphen, but Java packages cannot contain hyphens. @@ -35,5 +37,11 @@ * (e.g. {@code cover-letter-modern-professional.pdf}).

* * @since 1.6.0 + * @deprecated Superseded by the layered + * {@code com.demcha.compose.document.templates.coverletter.v2} + * surface (the current standard). This Gen-2 package is kept for + * backward compatibility and will be removed in a future major. + * See {@code docs/templates/v2-layered/}. */ +@Deprecated(since = "1.7.0", forRemoval = true) package com.demcha.compose.document.templates.coverletter; diff --git a/src/main/java/com/demcha/compose/document/templates/coverletter/presets/BlueBannerLetter.java b/src/main/java/com/demcha/compose/document/templates/coverletter/presets/BlueBannerLetter.java index 3656110d..cf0f4b55 100644 --- a/src/main/java/com/demcha/compose/document/templates/coverletter/presets/BlueBannerLetter.java +++ b/src/main/java/com/demcha/compose/document/templates/coverletter/presets/BlueBannerLetter.java @@ -19,7 +19,13 @@ * BANNER_BG used by * {@link com.demcha.compose.document.templates.cv.presets.BlueBanner} * for accent runs.

+ * + * @deprecated Superseded by the layered …v2… surface (the current + * standard). Kept for backward compatibility; scheduled for removal + * in a future major. See {@code docs/templates/v2-layered/} and + * {@link com.demcha.compose.document.templates.coverletter.v2.presets.BlueBannerLetter}. */ +@Deprecated(since = "1.7.0", forRemoval = true) public final class BlueBannerLetter { /** Stable template identifier. */ diff --git a/src/main/java/com/demcha/compose/document/templates/coverletter/presets/BoxedSectionsLetter.java b/src/main/java/com/demcha/compose/document/templates/coverletter/presets/BoxedSectionsLetter.java index b03ee997..4d37092e 100644 --- a/src/main/java/com/demcha/compose/document/templates/coverletter/presets/BoxedSectionsLetter.java +++ b/src/main/java/com/demcha/compose/document/templates/coverletter/presets/BoxedSectionsLetter.java @@ -17,7 +17,13 @@ * *

PT Serif throughout, dark grey ink β€” matches * {@link com.demcha.compose.document.templates.cv.presets.BoxedSections}.

+ * + * @deprecated Superseded by the layered …v2… surface (the current + * standard). Kept for backward compatibility; scheduled for removal + * in a future major. See {@code docs/templates/v2-layered/} and + * {@link com.demcha.compose.document.templates.coverletter.v2.presets.BoxedSectionsLetter}. */ +@Deprecated(since = "1.7.0", forRemoval = true) public final class BoxedSectionsLetter { /** Stable template identifier. */ diff --git a/src/main/java/com/demcha/compose/document/templates/coverletter/presets/CenteredHeadlineLetter.java b/src/main/java/com/demcha/compose/document/templates/coverletter/presets/CenteredHeadlineLetter.java index ce1c6093..1afe6de7 100644 --- a/src/main/java/com/demcha/compose/document/templates/coverletter/presets/CenteredHeadlineLetter.java +++ b/src/main/java/com/demcha/compose/document/templates/coverletter/presets/CenteredHeadlineLetter.java @@ -17,7 +17,13 @@ * *

Poppins headline + Lato body, dark grey ink β€” matches * {@link com.demcha.compose.document.templates.cv.presets.CenteredHeadline}.

+ * + * @deprecated Superseded by the layered …v2… surface (the current + * standard). Kept for backward compatibility; scheduled for removal + * in a future major. See {@code docs/templates/v2-layered/} and + * {@link com.demcha.compose.document.templates.coverletter.v2.presets.CenteredHeadlineLetter}. */ +@Deprecated(since = "1.7.0", forRemoval = true) public final class CenteredHeadlineLetter { /** Stable template identifier. */ diff --git a/src/main/java/com/demcha/compose/document/templates/coverletter/presets/ClassicSerifLetter.java b/src/main/java/com/demcha/compose/document/templates/coverletter/presets/ClassicSerifLetter.java index 6207d08f..69007a25 100644 --- a/src/main/java/com/demcha/compose/document/templates/coverletter/presets/ClassicSerifLetter.java +++ b/src/main/java/com/demcha/compose/document/templates/coverletter/presets/ClassicSerifLetter.java @@ -17,7 +17,13 @@ * *

PT Serif throughout with the bronze accent and warm INK palette * of {@link com.demcha.compose.document.templates.cv.presets.ClassicSerif}.

+ * + * @deprecated Superseded by the layered …v2… surface (the current + * standard). Kept for backward compatibility; scheduled for removal + * in a future major. See {@code docs/templates/v2-layered/} and + * {@link com.demcha.compose.document.templates.coverletter.v2.presets.ClassicSerifLetter}. */ +@Deprecated(since = "1.7.0", forRemoval = true) public final class ClassicSerifLetter { /** Stable template identifier. */ diff --git a/src/main/java/com/demcha/compose/document/templates/coverletter/presets/CompactMonoLetter.java b/src/main/java/com/demcha/compose/document/templates/coverletter/presets/CompactMonoLetter.java index 53681f5e..50412499 100644 --- a/src/main/java/com/demcha/compose/document/templates/coverletter/presets/CompactMonoLetter.java +++ b/src/main/java/com/demcha/compose/document/templates/coverletter/presets/CompactMonoLetter.java @@ -18,7 +18,13 @@ *

IBM Plex Mono headline + Lato body, dark INK ink with the * teal-blue ACCENT used by * {@link com.demcha.compose.document.templates.cv.presets.CompactMono}.

+ * + * @deprecated Superseded by the layered …v2… surface (the current + * standard). Kept for backward compatibility; scheduled for removal + * in a future major. See {@code docs/templates/v2-layered/} and + * {@link com.demcha.compose.document.templates.coverletter.v2.presets.CompactMonoLetter}. */ +@Deprecated(since = "1.7.0", forRemoval = true) public final class CompactMonoLetter { /** Stable template identifier. */ diff --git a/src/main/java/com/demcha/compose/document/templates/coverletter/presets/EditorialBlueLetter.java b/src/main/java/com/demcha/compose/document/templates/coverletter/presets/EditorialBlueLetter.java index 516f6f68..513f885c 100644 --- a/src/main/java/com/demcha/compose/document/templates/coverletter/presets/EditorialBlueLetter.java +++ b/src/main/java/com/demcha/compose/document/templates/coverletter/presets/EditorialBlueLetter.java @@ -18,7 +18,13 @@ *

Helvetica throughout, deep navy ink with bright editorial blue * accent β€” matches * {@link com.demcha.compose.document.templates.cv.presets.EditorialBlue}.

+ * + * @deprecated Superseded by the layered …v2… surface (the current + * standard). Kept for backward compatibility; scheduled for removal + * in a future major. See {@code docs/templates/v2-layered/} and + * {@link com.demcha.compose.document.templates.coverletter.v2.presets.EditorialBlueLetter}. */ +@Deprecated(since = "1.7.0", forRemoval = true) public final class EditorialBlueLetter { /** Stable template identifier. */ diff --git a/src/main/java/com/demcha/compose/document/templates/coverletter/presets/EngineeringResumeLetter.java b/src/main/java/com/demcha/compose/document/templates/coverletter/presets/EngineeringResumeLetter.java index 27a45246..70eeeecd 100644 --- a/src/main/java/com/demcha/compose/document/templates/coverletter/presets/EngineeringResumeLetter.java +++ b/src/main/java/com/demcha/compose/document/templates/coverletter/presets/EngineeringResumeLetter.java @@ -17,7 +17,13 @@ * *

Navy primary, green accent, Barlow headline + Lato body β€” matches * {@link com.demcha.compose.document.templates.cv.presets.EngineeringResume}.

+ * + * @deprecated Superseded by the layered …v2… surface (the current + * standard). Kept for backward compatibility; scheduled for removal + * in a future major. See {@code docs/templates/v2-layered/} and + * {@link com.demcha.compose.document.templates.coverletter.v2.presets.EngineeringResumeLetter}. */ +@Deprecated(since = "1.7.0", forRemoval = true) public final class EngineeringResumeLetter { /** Stable template identifier. */ diff --git a/src/main/java/com/demcha/compose/document/templates/coverletter/presets/ExecutiveLetter.java b/src/main/java/com/demcha/compose/document/templates/coverletter/presets/ExecutiveLetter.java index 7b280808..12101ddd 100644 --- a/src/main/java/com/demcha/compose/document/templates/coverletter/presets/ExecutiveLetter.java +++ b/src/main/java/com/demcha/compose/document/templates/coverletter/presets/ExecutiveLetter.java @@ -17,7 +17,13 @@ * *

Slate primary, bronze accent, Poppins headline + Lato body β€” * matches {@link com.demcha.compose.document.templates.cv.presets.Executive}.

+ * + * @deprecated Superseded by the layered …v2… surface (the current + * standard). Kept for backward compatibility; scheduled for removal + * in a future major. See {@code docs/templates/v2-layered/} and + * {@link com.demcha.compose.document.templates.coverletter.v2.presets.ExecutiveLetter}. */ +@Deprecated(since = "1.7.0", forRemoval = true) public final class ExecutiveLetter { /** Stable template identifier. */ diff --git a/src/main/java/com/demcha/compose/document/templates/coverletter/presets/ModernProfessionalLetter.java b/src/main/java/com/demcha/compose/document/templates/coverletter/presets/ModernProfessionalLetter.java index 227d823a..df537871 100644 --- a/src/main/java/com/demcha/compose/document/templates/coverletter/presets/ModernProfessionalLetter.java +++ b/src/main/java/com/demcha/compose/document/templates/coverletter/presets/ModernProfessionalLetter.java @@ -21,7 +21,13 @@ * contact links), same Helvetica body type. The cover letter itself * is a single-column letter β€” header on top, greeting, body * paragraphs separated by paragraph spacing, closing.

+ * + * @deprecated Superseded by the layered …v2… surface (the current + * standard). Kept for backward compatibility; scheduled for removal + * in a future major. See {@code docs/templates/v2-layered/} and + * {@link com.demcha.compose.document.templates.coverletter.v2.presets.ModernProfessionalLetter}. */ +@Deprecated(since = "1.7.0", forRemoval = true) public final class ModernProfessionalLetter { /** Stable template identifier. */ diff --git a/src/main/java/com/demcha/compose/document/templates/coverletter/presets/MonogramSidebarLetter.java b/src/main/java/com/demcha/compose/document/templates/coverletter/presets/MonogramSidebarLetter.java index 3f1f6a8c..85ec4bb0 100644 --- a/src/main/java/com/demcha/compose/document/templates/coverletter/presets/MonogramSidebarLetter.java +++ b/src/main/java/com/demcha/compose/document/templates/coverletter/presets/MonogramSidebarLetter.java @@ -20,7 +20,13 @@ * {@link com.demcha.compose.document.templates.cv.presets.MonogramSidebar}. * The cover letter is a simple single-column letter β€” the CV's * monogram sidebar is intentionally not replicated.

+ * + * @deprecated Superseded by the layered …v2… surface (the current + * standard). Kept for backward compatibility; scheduled for removal + * in a future major. See {@code docs/templates/v2-layered/} and + * {@link com.demcha.compose.document.templates.coverletter.v2.presets.MonogramSidebarLetter}. */ +@Deprecated(since = "1.7.0", forRemoval = true) public final class MonogramSidebarLetter { /** Stable template identifier. */ diff --git a/src/main/java/com/demcha/compose/document/templates/coverletter/presets/NordicCleanLetter.java b/src/main/java/com/demcha/compose/document/templates/coverletter/presets/NordicCleanLetter.java index f1340b2c..7b53d52a 100644 --- a/src/main/java/com/demcha/compose/document/templates/coverletter/presets/NordicCleanLetter.java +++ b/src/main/java/com/demcha/compose/document/templates/coverletter/presets/NordicCleanLetter.java @@ -19,7 +19,13 @@ * {@link com.demcha.compose.document.templates.cv.presets.NordicClean} * CV. Single-column letter format β€” header on top, greeting, body * paragraphs, closing.

+ * + * @deprecated Superseded by the layered …v2… surface (the current + * standard). Kept for backward compatibility; scheduled for removal + * in a future major. See {@code docs/templates/v2-layered/} and + * {@link com.demcha.compose.document.templates.coverletter.v2.presets.NordicCleanLetter}. */ +@Deprecated(since = "1.7.0", forRemoval = true) public final class NordicCleanLetter { /** Stable template identifier. */ diff --git a/src/main/java/com/demcha/compose/document/templates/coverletter/presets/PanelLetter.java b/src/main/java/com/demcha/compose/document/templates/coverletter/presets/PanelLetter.java index 740dd5cb..c41726d3 100644 --- a/src/main/java/com/demcha/compose/document/templates/coverletter/presets/PanelLetter.java +++ b/src/main/java/com/demcha/compose/document/templates/coverletter/presets/PanelLetter.java @@ -17,7 +17,13 @@ * *

Deep-slate primary, teal accent, Poppins headline + Lato body β€” * matches {@link com.demcha.compose.document.templates.cv.presets.Panel}.

+ * + * @deprecated Superseded by the layered …v2… surface (the current + * standard). Kept for backward compatibility; scheduled for removal + * in a future major. See {@code docs/templates/v2-layered/} and + * {@link com.demcha.compose.document.templates.coverletter.v2.presets.PanelLetter}. */ +@Deprecated(since = "1.7.0", forRemoval = true) public final class PanelLetter { /** Stable template identifier. */ diff --git a/src/main/java/com/demcha/compose/document/templates/coverletter/presets/SidebarPortraitLetter.java b/src/main/java/com/demcha/compose/document/templates/coverletter/presets/SidebarPortraitLetter.java index 8faee868..8a7ce9c2 100644 --- a/src/main/java/com/demcha/compose/document/templates/coverletter/presets/SidebarPortraitLetter.java +++ b/src/main/java/com/demcha/compose/document/templates/coverletter/presets/SidebarPortraitLetter.java @@ -20,7 +20,13 @@ * {@link com.demcha.compose.document.templates.cv.presets.SidebarPortrait}. * The cover letter is a simple single-column letter β€” the CV's * portrait sidebar is intentionally not replicated.

+ * + * @deprecated Superseded by the layered …v2… surface (the current + * standard). Kept for backward compatibility; scheduled for removal + * in a future major. See {@code docs/templates/v2-layered/} and + * {@link com.demcha.compose.document.templates.coverletter.v2.presets.SidebarPortraitLetter}. */ +@Deprecated(since = "1.7.0", forRemoval = true) public final class SidebarPortraitLetter { /** Stable template identifier. */ diff --git a/src/main/java/com/demcha/compose/document/templates/coverletter/presets/TimelineMinimalLetter.java b/src/main/java/com/demcha/compose/document/templates/coverletter/presets/TimelineMinimalLetter.java index 5f4607c2..20473f35 100644 --- a/src/main/java/com/demcha/compose/document/templates/coverletter/presets/TimelineMinimalLetter.java +++ b/src/main/java/com/demcha/compose/document/templates/coverletter/presets/TimelineMinimalLetter.java @@ -18,7 +18,13 @@ *

All-grey palette with Barlow Condensed for the headline and Lato * body β€” matches * {@link com.demcha.compose.document.templates.cv.presets.TimelineMinimal}.

+ * + * @deprecated Superseded by the layered …v2… surface (the current + * standard). Kept for backward compatibility; scheduled for removal + * in a future major. See {@code docs/templates/v2-layered/} and + * {@link com.demcha.compose.document.templates.coverletter.v2.presets.TimelineMinimalLetter}. */ +@Deprecated(since = "1.7.0", forRemoval = true) public final class TimelineMinimalLetter { /** Stable template identifier. */ diff --git a/src/main/java/com/demcha/compose/document/templates/coverletter/presets/package-info.java b/src/main/java/com/demcha/compose/document/templates/coverletter/presets/package-info.java index ce7097e6..76661b47 100644 --- a/src/main/java/com/demcha/compose/document/templates/coverletter/presets/package-info.java +++ b/src/main/java/com/demcha/compose/document/templates/coverletter/presets/package-info.java @@ -1,18 +1,29 @@ /** - * Templates v2 cover-letter presets β€” flat copy-and-tweak recipe + * Superseded Gen-2 cover-letter presets β€” flat copy-and-tweak recipe * classes paired one-to-one with the * {@link com.demcha.compose.document.templates.cv.presets} CV * preset family. * + *

Deprecated surface. These are the older Gen-2 + * cover-letter presets. They are not the current standard. The + * current standard is the layered surface + * {@code com.demcha.compose.document.templates.coverletter.v2.presets}. This + * package is kept only for backward compatibility and is scheduled for + * removal in a future major.

+ * *

Each preset shares the typography palette and spacing rhythm * of its paired CV preset, so a writer's CV and cover letter ship * as a matched set with consistent visual identity.

* - *

To customise: copy the {@code create(...)} method body of any - * preset into your own class and tweak the {@code CoverLetterBuilder} - * calls (header style, body text style, spacing tokens, layout - * choice).

+ *

New code should target the layered {@code coverletter.v2} presets + * instead. See {@code docs/templates/v2-layered/}.

* * @since 1.6.0 + * @deprecated Superseded by the layered + * {@code com.demcha.compose.document.templates.coverletter.v2} + * surface (the current standard). This Gen-2 package is kept for + * backward compatibility and will be removed in a future major. + * See {@code docs/templates/v2-layered/}. */ +@Deprecated(since = "1.7.0", forRemoval = true) package com.demcha.compose.document.templates.coverletter.presets; diff --git a/src/main/java/com/demcha/compose/document/templates/coverletter/spec/CoverLetterHeader.java b/src/main/java/com/demcha/compose/document/templates/coverletter/spec/CoverLetterHeader.java index fd16cbfa..ceff37f2 100644 --- a/src/main/java/com/demcha/compose/document/templates/coverletter/spec/CoverLetterHeader.java +++ b/src/main/java/com/demcha/compose/document/templates/coverletter/spec/CoverLetterHeader.java @@ -17,7 +17,14 @@ * @param email optional email address; empty string when absent * @param links ordered list of {@link Link} entries (typically * LinkedIn, GitHub); never null after construction + * @deprecated Superseded by the layered …v2… surface (the current + * standard) β€” the layered model + * {@link com.demcha.compose.document.templates.coverletter.v2.data.CoverLetterDocument} + * plus the {@code coverletter.v2} presets. Kept for backward + * compatibility; scheduled for removal in a future major. See + * {@code docs/templates/v2-layered/}. */ +@Deprecated(since = "1.7.0", forRemoval = true) public record CoverLetterHeader( String name, String address, diff --git a/src/main/java/com/demcha/compose/document/templates/coverletter/spec/CoverLetterSpec.java b/src/main/java/com/demcha/compose/document/templates/coverletter/spec/CoverLetterSpec.java index 7c251281..ba8ae9ac 100644 --- a/src/main/java/com/demcha/compose/document/templates/coverletter/spec/CoverLetterSpec.java +++ b/src/main/java/com/demcha/compose/document/templates/coverletter/spec/CoverLetterSpec.java @@ -25,7 +25,14 @@ * {@code *italic*}) * @param closing last body line (required, may be blank); * typically {@code "Sincerely, Alex"} + * @deprecated Superseded by the layered …v2… surface (the current + * standard) β€” the layered model + * {@link com.demcha.compose.document.templates.coverletter.v2.data.CoverLetterDocument} + * plus the {@code coverletter.v2} presets. Kept for backward + * compatibility; scheduled for removal in a future major. See + * {@code docs/templates/v2-layered/}. */ +@Deprecated(since = "1.7.0", forRemoval = true) public record CoverLetterSpec( CoverLetterHeader header, String greeting, diff --git a/src/main/java/com/demcha/compose/document/templates/coverletter/spec/package-info.java b/src/main/java/com/demcha/compose/document/templates/coverletter/spec/package-info.java index 8a644fb4..3f591eb8 100644 --- a/src/main/java/com/demcha/compose/document/templates/coverletter/spec/package-info.java +++ b/src/main/java/com/demcha/compose/document/templates/coverletter/spec/package-info.java @@ -1,9 +1,17 @@ /** - * Templates v2 cover-letter specification records β€” user-facing data + * Superseded Gen-2 cover-letter specification records β€” user-facing data * types. * + *

Deprecated surface. These are the older Gen-2 + * cover-letter spec records. They are not the current standard. The + * current standard is the layered model + * {@link com.demcha.compose.document.templates.coverletter.v2.data.CoverLetterDocument} + * in the {@code com.demcha.compose.document.templates.coverletter.v2} + * surface. This package is kept only for backward compatibility and is + * scheduled for removal in a future major.

+ * *

This package holds the immutable records a user fills with their - * cover-letter content before passing the spec to a preset for + * cover-letter content before passing the spec to a Gen-2 preset for * rendering:

* *
    @@ -15,6 +23,15 @@ * {@code *italic*}) for inline emphasis. *
* + *

New code should target the layered {@code coverletter.v2} data model + * instead. See {@code docs/templates/v2-layered/}.

+ * * @since 1.6.0 + * @deprecated Superseded by the layered + * {@code com.demcha.compose.document.templates.coverletter.v2} + * surface (the current standard). This Gen-2 package is kept for + * backward compatibility and will be removed in a future major. + * See {@code docs/templates/v2-layered/}. */ +@Deprecated(since = "1.7.0", forRemoval = true) package com.demcha.compose.document.templates.coverletter.spec; diff --git a/src/main/java/com/demcha/compose/document/templates/coverletter/v2/components/LetterBody.java b/src/main/java/com/demcha/compose/document/templates/coverletter/v2/components/LetterBody.java new file mode 100644 index 00000000..ca092fec --- /dev/null +++ b/src/main/java/com/demcha/compose/document/templates/coverletter/v2/components/LetterBody.java @@ -0,0 +1,123 @@ +package com.demcha.compose.document.templates.coverletter.v2.components; + +import com.demcha.compose.document.dsl.SectionBuilder; +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.coverletter.v2.data.CoverLetterDocument; +import com.demcha.compose.document.templates.cv.v2.components.CvTextStyles; +import com.demcha.compose.document.templates.cv.v2.components.RichParagraphRenderer; +import com.demcha.compose.document.templates.cv.v2.theme.CvTheme; + +/** + * Shared cover-letter body renderer β€” the letter analog of + * {@code SectionDispatcher}. + * + *

Stacks the greeting line, the ordered body paragraphs, and the + * closing sign-off into a single host section. Each block is rendered + * with the theme's body font / size / ink and line spacing through the + * shared {@link RichParagraphRenderer}, so inline markdown + * ({@code **bold**}, {@code *italic*}) is honoured exactly as in the + * paired CV preset's body prose.

+ * + *

Every v2 letter preset reuses this, so all letters share one + * identical reading rhythm and only the masthead differs by brand β€” + * which is what makes a CV and its letter read as a matched set.

+ */ +public final class LetterBody { + + private LetterBody() { + } + + /** + * Renders greeting + body paragraphs + closing into {@code host} + * using the theme's body style and line spacing. + * + * @param host host section (typically one page-flow section) + * @param doc cover-letter content + * @param theme active brand theme β€” share the same instance with the + * paired CV preset so the body colour / font / size match + */ + public static void render(SectionBuilder host, CoverLetterDocument doc, + CvTheme theme) { + render(host, doc, theme, theme.typography().sizeBody()); + } + + /** + * Variant that renders the letter prose at an explicit point size + * instead of {@code theme.typography().sizeBody()}. Used by presets + * whose paired CV theme carries a very small body size tuned for a + * dense multi-column CV (e.g. Monogram Sidebar, Timeline Minimal) β€” + * a single-column letter needs a more readable size, so the preset + * supplies one without disturbing the CV. + * + * @param host host section + * @param doc cover-letter content + * @param theme active brand theme (font, line spacing, ink) + * @param bodySize body text size in points + */ + public static void render(SectionBuilder host, CoverLetterDocument doc, + CvTheme theme, double bodySize) { + DocumentTextStyle bodyStyle = CvTextStyles.of( + theme.typography().bodyFont(), + bodySize, + DocumentTextDecoration.DEFAULT, + theme.palette().ink()); + double lineSpacing = theme.typography().bodyLineSpacing(); + // Letter paragraph rhythm scales with the body size so a compact + // brand stays tight and a roomy serif brand breathes, without a + // separate hand-tuned token per brand. + double gap = bodySize * 1.25; + // Clear breathing room below the masthead so the letter body never + // reads as "stuck" to the header. Normalised: the page-flow gap + // between the header section and this body section already supplies + // some space, so we top it up to a consistent ~1.8x the body size + // total and subtract what the brand's pageFlowSpacing already gives. + // The result is the same comfortable separation under every brand's + // masthead, whether its CV uses a dense (0pt) or roomy (8pt) gap. + double headerGap = Math.max(2.0, + bodySize * 1.8 - theme.spacing().pageFlowSpacing()); + // The signed name sits on the line directly below the sign-off + // (standard letter convention), so it gets only a small gap. + double signatureGap = bodySize * 0.4; + DocumentTextStyle signatureStyle = CvTextStyles.of( + theme.typography().bodyFont(), + bodySize, + DocumentTextDecoration.ITALIC, + theme.palette().ink()); + + boolean[] emitted = {false}; + emit(host, doc.greeting(), bodyStyle, lineSpacing, headerGap, gap, emitted); + for (String paragraph : doc.body()) { + emit(host, paragraph, bodyStyle, lineSpacing, headerGap, gap, emitted); + } + // Closing block: the sign-off ("Sincerely,") on one line, then the + // signer's name on the line directly below it. The name is pulled + // from the shared identity so the signature always matches the + // masthead and never drifts from the paired CV. + if (!doc.closing().isBlank()) { + double top = emitted[0] ? gap : headerGap; + RichParagraphRenderer.render(host, doc.closing(), bodyStyle, + lineSpacing, DocumentInsets.top(top)); + String signature = doc.identity().name().full(); + if (!signature.isBlank()) { + RichParagraphRenderer.render(host, signature, signatureStyle, + lineSpacing, DocumentInsets.top(signatureGap)); + } + } + } + + private static void emit(SectionBuilder host, String text, + DocumentTextStyle style, double lineSpacing, + double firstTop, double gap, boolean[] emitted) { + if (text == null || text.isBlank()) { + return; + } + // First emitted block gets the larger header gap (separation from + // the masthead); every subsequent block gets the inter-paragraph gap. + double top = emitted[0] ? gap : firstTop; + RichParagraphRenderer.render(host, text, style, lineSpacing, + DocumentInsets.top(top)); + emitted[0] = true; + } +} diff --git a/src/main/java/com/demcha/compose/document/templates/coverletter/v2/components/package-info.java b/src/main/java/com/demcha/compose/document/templates/coverletter/v2/components/package-info.java new file mode 100644 index 00000000..1c4a039b --- /dev/null +++ b/src/main/java/com/demcha/compose/document/templates/coverletter/v2/components/package-info.java @@ -0,0 +1,10 @@ +/** + * Shared rendering components for Templates v2 cover letters. + * + *

{@link com.demcha.compose.document.templates.coverletter.v2.components.LetterBody} + * is the letter analog of the CV {@code SectionDispatcher}: every + * letter preset delegates its greeting / paragraphs / closing to it so + * all letters share one reading rhythm and inline-markdown handling + * (via the reused {@code cv.v2.components.RichParagraphRenderer}).

+ */ +package com.demcha.compose.document.templates.coverletter.v2.components; diff --git a/src/main/java/com/demcha/compose/document/templates/coverletter/v2/data/CoverLetterDocument.java b/src/main/java/com/demcha/compose/document/templates/coverletter/v2/data/CoverLetterDocument.java new file mode 100644 index 00000000..58090336 --- /dev/null +++ b/src/main/java/com/demcha/compose/document/templates/coverletter/v2/data/CoverLetterDocument.java @@ -0,0 +1,126 @@ +package com.demcha.compose.document.templates.coverletter.v2.data; + +import com.demcha.compose.document.templates.cv.v2.data.CvIdentity; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * User-facing data record for a Templates v2 cover letter. + * + *

Reuses {@link CvIdentity} for the top-of-document identity block + * so a writer hands the same identity object to both their CV + * preset and their paired cover-letter preset. Because the masthead + * (name, contact, links) then renders through the identical widget + * path, the CV and the letter read as one matched set β€” which is the + * whole point of pairing them.

+ * + *

The letter-specific content is deliberately tiny: an opening + * greeting, an ordered list of body paragraphs, and a closing sign-off. + * This mirrors {@code CvDocument} (identity + sections) but with the + * far simpler single-flow shape of a letter.

+ * + * @param identity top-of-document identity block (required) β€” share + * the same instance with the paired CV preset + * @param greeting opening line (e.g. {@code "Dear Hiring Team,"}); a + * blank value suppresses the line; may carry inline + * markdown ({@code **bold**}, {@code *italic*}) + * @param body ordered body paragraphs; blank paragraphs are skipped + * at render; each may carry inline markdown + * @param closing sign-off line (e.g. {@code "Sincerely, Alex"}); a + * blank value suppresses the line; may carry inline + * markdown + */ +public record CoverLetterDocument(CvIdentity identity, + String greeting, + List body, + String closing) { + + /** + * Compact constructor that normalises null strings to empty and + * defensively copies the body list. + * + * @throws NullPointerException if {@code identity} is null + */ + public CoverLetterDocument { + Objects.requireNonNull(identity, "identity"); + greeting = greeting == null ? "" : greeting; + closing = closing == null ? "" : closing; + body = body == null ? List.of() : List.copyOf(body); + } + + /** + * @return new fluent builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Mutable builder for {@link CoverLetterDocument}. + */ + public static final class Builder { + private CvIdentity identity; + private String greeting = ""; + private final List body = new ArrayList<>(); + private String closing = ""; + + private Builder() { + } + + /** + * Sets the shared identity block. + * + * @param value non-null identity (reuse the paired CV's instance) + * @return this builder + */ + public Builder identity(CvIdentity value) { + this.identity = value; + return this; + } + + /** + * Sets the opening greeting line. + * + * @param value greeting; null treated as empty + * @return this builder + */ + public Builder greeting(String value) { + this.greeting = value == null ? "" : value; + return this; + } + + /** + * Appends one body paragraph. + * + * @param value non-null paragraph text + * @return this builder + */ + public Builder paragraph(String value) { + Objects.requireNonNull(value, "paragraph"); + this.body.add(value); + return this; + } + + /** + * Sets the closing sign-off line. + * + * @param value closing; null treated as empty + * @return this builder + */ + public Builder closing(String value) { + this.closing = value == null ? "" : value; + return this; + } + + /** + * Builds an immutable {@link CoverLetterDocument}. + * + * @return new document + */ + public CoverLetterDocument build() { + return new CoverLetterDocument(identity, greeting, body, closing); + } + } +} diff --git a/src/main/java/com/demcha/compose/document/templates/coverletter/v2/data/package-info.java b/src/main/java/com/demcha/compose/document/templates/coverletter/v2/data/package-info.java new file mode 100644 index 00000000..7237adab --- /dev/null +++ b/src/main/java/com/demcha/compose/document/templates/coverletter/v2/data/package-info.java @@ -0,0 +1,10 @@ +/** + * User-facing data records for Templates v2 cover letters. + * + *

{@link com.demcha.compose.document.templates.coverletter.v2.data.CoverLetterDocument} + * is the single input type β€” it reuses + * {@link com.demcha.compose.document.templates.cv.v2.data.CvIdentity} + * for the masthead so a CV and its paired letter share one identity + * object and render identical headers.

+ */ +package com.demcha.compose.document.templates.coverletter.v2.data; diff --git a/src/main/java/com/demcha/compose/document/templates/coverletter/v2/presets/BlueBannerLetter.java b/src/main/java/com/demcha/compose/document/templates/coverletter/v2/presets/BlueBannerLetter.java new file mode 100644 index 00000000..7d4653c0 --- /dev/null +++ b/src/main/java/com/demcha/compose/document/templates/coverletter/v2/presets/BlueBannerLetter.java @@ -0,0 +1,98 @@ +package com.demcha.compose.document.templates.coverletter.v2.presets; + +import com.demcha.compose.document.api.DocumentSession; +import com.demcha.compose.document.dsl.PageFlowBuilder; +import com.demcha.compose.document.templates.api.DocumentTemplate; +import com.demcha.compose.document.templates.coverletter.v2.components.LetterBody; +import com.demcha.compose.document.templates.coverletter.v2.data.CoverLetterDocument; +import com.demcha.compose.document.templates.cv.v2.theme.CvTheme; +import com.demcha.compose.document.templates.cv.v2.widgets.ContactLine; +import com.demcha.compose.document.templates.cv.v2.widgets.Headline; + +import java.util.Objects; + +/** + * v2 cover-letter pair for the {@code BlueBanner} CV preset. + * + *

Renders the identical masthead as + * {@link com.demcha.compose.document.templates.cv.v2.presets.BlueBanner} + * β€” a centred PT-Serif spaced-caps name over a compact centred contact + * row β€” then a single-column letter body via the shared + * {@link LetterBody}. Both documents read everything from + * {@link CvTheme#blueBanner()}.

+ * + *

The CV's signature blue banners decorate section titles, + * which a letter has none of, so the brand identity here is carried by + * the theme: the compact PT-Serif headline scale and the dark-blue rule + * tone of the contact separators / links. No preset-local colour is + * needed.

+ */ +public final class BlueBannerLetter { + + /** Stable template identifier. */ + public static final String ID = "blue-banner-letter"; + + /** Human-readable display name. */ + public static final String DISPLAY_NAME = "Blue Banner Letter"; + + /** Recommended page margin (in points) β€” generous business-letter feel. */ + public static final double RECOMMENDED_MARGIN = 48.0; + + private BlueBannerLetter() { + } + + /** + * Builds the letter with its Blue Banner theme. + */ + public static DocumentTemplate create() { + return create(CvTheme.blueBanner()); + } + + /** + * Builds the letter with a caller-supplied theme (share the paired + * CV's theme instance for a guaranteed visual match). + */ + public static DocumentTemplate create(CvTheme theme) { + Objects.requireNonNull(theme, "theme"); + return new Template(theme); + } + + private static final class Template implements DocumentTemplate { + + 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, CoverLetterDocument doc) { + Objects.requireNonNull(document, "document"); + Objects.requireNonNull(doc, "doc"); + + PageFlowBuilder flow = document.dsl() + .pageFlow() + .name("CoverLetterV2BlueBannerRoot") + .spacing(theme.spacing().pageFlowSpacing()) + .addSection("CoverLetterV2BlueBannerHeader", section -> + Headline.spacedCentered(section, doc.identity().name(), theme)) + .addSection("CoverLetterV2BlueBannerContact", section -> + ContactLine.centered(section, doc.identity(), theme)); + + flow.addSection("CoverLetterV2BlueBannerBody", host -> + LetterBody.render(host, doc, theme)); + + flow.build(); + } + } +} diff --git a/src/main/java/com/demcha/compose/document/templates/coverletter/v2/presets/BoxedSectionsLetter.java b/src/main/java/com/demcha/compose/document/templates/coverletter/v2/presets/BoxedSectionsLetter.java new file mode 100644 index 00000000..0e3b213e --- /dev/null +++ b/src/main/java/com/demcha/compose/document/templates/coverletter/v2/presets/BoxedSectionsLetter.java @@ -0,0 +1,103 @@ +package com.demcha.compose.document.templates.coverletter.v2.presets; + +import com.demcha.compose.document.api.DocumentSession; +import com.demcha.compose.document.dsl.PageFlowBuilder; +import com.demcha.compose.document.templates.api.DocumentTemplate; +import com.demcha.compose.document.templates.coverletter.v2.components.LetterBody; +import com.demcha.compose.document.templates.coverletter.v2.data.CoverLetterDocument; +import com.demcha.compose.document.templates.cv.v2.theme.CvTheme; +import com.demcha.compose.document.templates.cv.v2.widgets.ContactLine; +import com.demcha.compose.document.templates.cv.v2.widgets.Headline; + +import java.util.Objects; + +/** + * v2 cover-letter pair for the {@code BoxedSections} CV preset. + * + *

Renders the identical masthead as + * {@link com.demcha.compose.document.templates.cv.v2.presets.BoxedSections} + * β€” a centred letter-spaced PT-Serif name with a thin rule beneath it, + * then a centred pipe-separated contact line with its own rule beneath + * β€” then a single-column letter body via the shared {@link LetterBody}. + * Both documents read everything from {@link CvTheme#boxedClassic()}.

+ * + *

The header is composed entirely from shared widgets + * ({@link Headline#spacedCentered} + {@link ContactLine#centered}) at + * the theme's default styles, so this preset carries no preset-local + * colour β€” the cleanest possible matched-set letter.

+ */ +public final class BoxedSectionsLetter { + + /** Stable template identifier. */ + public static final String ID = "boxed-sections-letter"; + + /** Human-readable display name. */ + public static final String DISPLAY_NAME = "Boxed Sections Letter"; + + /** Recommended page margin (in points) β€” generous business-letter feel. */ + public static final double RECOMMENDED_MARGIN = 48.0; + + private BoxedSectionsLetter() { + } + + /** + * Builds the letter with its Boxed Sections theme. + */ + public static DocumentTemplate create() { + return create(CvTheme.boxedClassic()); + } + + /** + * Builds the letter with a caller-supplied theme (share the paired + * CV's theme instance for a guaranteed visual match). + */ + public static DocumentTemplate create(CvTheme theme) { + Objects.requireNonNull(theme, "theme"); + return new Template(theme); + } + + private static final class Template implements DocumentTemplate { + + 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, CoverLetterDocument doc) { + Objects.requireNonNull(document, "document"); + Objects.requireNonNull(doc, "doc"); + + PageFlowBuilder flow = document.dsl() + .pageFlow() + .name("CoverLetterV2BoxedRoot") + .spacing(theme.spacing().pageFlowSpacing()) + .addSection("CoverLetterV2BoxedHeadline", section -> { + section.accentBottom(theme.palette().rule(), + theme.spacing().accentRuleWidth()); + Headline.spacedCentered(section, doc.identity().name(), theme); + }) + .addSection("CoverLetterV2BoxedContact", section -> { + section.accentBottom(theme.palette().rule(), + theme.spacing().accentRuleWidth()); + ContactLine.centered(section, doc.identity(), theme); + }); + + flow.addSection("CoverLetterV2BoxedBody", host -> + LetterBody.render(host, doc, theme)); + + flow.build(); + } + } +} diff --git a/src/main/java/com/demcha/compose/document/templates/coverletter/v2/presets/CenteredHeadlineLetter.java b/src/main/java/com/demcha/compose/document/templates/coverletter/v2/presets/CenteredHeadlineLetter.java new file mode 100644 index 00000000..39e10488 --- /dev/null +++ b/src/main/java/com/demcha/compose/document/templates/coverletter/v2/presets/CenteredHeadlineLetter.java @@ -0,0 +1,133 @@ +package com.demcha.compose.document.templates.coverletter.v2.presets; + +import com.demcha.compose.document.api.DocumentSession; +import com.demcha.compose.document.dsl.LineBuilder; +import com.demcha.compose.document.dsl.PageFlowBuilder; +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.coverletter.v2.components.LetterBody; +import com.demcha.compose.document.templates.coverletter.v2.data.CoverLetterDocument; +import com.demcha.compose.document.templates.cv.v2.components.CvTextStyles; +import com.demcha.compose.document.templates.cv.v2.data.CvIdentity; +import com.demcha.compose.document.templates.cv.v2.theme.CvTheme; +import com.demcha.compose.document.templates.cv.v2.widgets.ContactLine; +import com.demcha.compose.document.templates.cv.v2.widgets.Headline; +import com.demcha.compose.document.templates.cv.v2.widgets.Subheadline; + +import java.util.Objects; + +/** + * v2 cover-letter pair for the {@code CenteredHeadline} CV preset. + * + *

Renders the identical masthead as + * {@link com.demcha.compose.document.templates.cv.v2.presets.CenteredHeadline} + * β€” a centred letter-spaced Poppins name, a small spaced-caps + * subheadline, and a centred contact line framed by thin full-width + * rules β€” then a single-column letter body via the shared + * {@link LetterBody}. Both documents read everything from + * {@link CvTheme#centeredHeadline()}.

+ * + *

The subheadline uses the real {@link CvIdentity#jobTitle()} (the + * CV preset still shows a hard-coded placeholder pending its own + * jobTitle wiring); on a letter the writer's actual title reads more + * naturally and stays a faithful match to the CV's design. The + * rule that the CV places below the contact is dropped here because the + * shared {@code LetterBody} already supplies the gap to the greeting.

+ */ +public final class CenteredHeadlineLetter { + + /** Stable template identifier. */ + public static final String ID = "centered-headline-letter"; + + /** Human-readable display name. */ + public static final String DISPLAY_NAME = "Centered Headline Letter"; + + /** Recommended page margin (in points) β€” generous business-letter feel. */ + public static final double RECOMMENDED_MARGIN = 48.0; + + private CenteredHeadlineLetter() { + } + + /** + * Builds the letter with its Centered Headline theme. + */ + public static DocumentTemplate create() { + return create(CvTheme.centeredHeadline()); + } + + /** + * Builds the letter with a caller-supplied theme (share the paired + * CV's theme instance for a guaranteed visual match). + */ + public static DocumentTemplate create(CvTheme theme) { + Objects.requireNonNull(theme, "theme"); + return new Template(theme); + } + + private static final class Template implements DocumentTemplate { + + 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, CoverLetterDocument doc) { + Objects.requireNonNull(document, "document"); + Objects.requireNonNull(doc, "doc"); + + double width = document.canvas().innerWidth(); + CvIdentity identity = doc.identity(); + + PageFlowBuilder flow = document.dsl() + .pageFlow() + .name("CoverLetterV2CenteredHeadlineRoot") + .spacing(theme.spacing().pageFlowSpacing()) + .addSection("CoverLetterV2CenteredHeadlineHeadline", section -> { + Headline.spacedCentered(section, identity.name(), theme); + if (!identity.jobTitle().isBlank()) { + Subheadline.centeredSpacedCaps(section, + identity.jobTitle(), subheadlineStyle()); + } + }) + .addLine(line -> rule(line, "CoverLetterV2CenteredHeadlineRule", + width, 7, 0)) + .addSection("CoverLetterV2CenteredHeadlineContact", section -> + ContactLine.centered(section, identity, theme)) + .addLine(line -> rule(line, "CoverLetterV2CenteredContactRule", + width, 0, 0)); + + flow.addSection("CoverLetterV2CenteredHeadlineBody", host -> + LetterBody.render(host, doc, theme)); + + flow.build(); + } + + private DocumentTextStyle subheadlineStyle() { + return CvTextStyles.of(theme.typography().headlineFont(), 8.6, + DocumentTextDecoration.DEFAULT, theme.palette().muted()); + } + + private void rule(LineBuilder line, String name, double width, + double top, double bottom) { + line.name(name) + .horizontal(width) + .color(theme.palette().rule()) + .thickness(theme.spacing().accentRuleWidth()) + .margin(new DocumentInsets(top, 0, bottom, 0)); + } + } +} diff --git a/src/main/java/com/demcha/compose/document/templates/coverletter/v2/presets/ClassicSerifLetter.java b/src/main/java/com/demcha/compose/document/templates/coverletter/v2/presets/ClassicSerifLetter.java new file mode 100644 index 00000000..0368c0f9 --- /dev/null +++ b/src/main/java/com/demcha/compose/document/templates/coverletter/v2/presets/ClassicSerifLetter.java @@ -0,0 +1,144 @@ +package com.demcha.compose.document.templates.coverletter.v2.presets; + +import com.demcha.compose.document.api.DocumentSession; +import com.demcha.compose.document.dsl.PageFlowBuilder; +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.coverletter.v2.components.LetterBody; +import com.demcha.compose.document.templates.coverletter.v2.data.CoverLetterDocument; +import com.demcha.compose.document.templates.cv.v2.components.CvTextStyles; +import com.demcha.compose.document.templates.cv.v2.data.CvIdentity; +import com.demcha.compose.document.templates.cv.v2.theme.CvTheme; +import com.demcha.compose.document.templates.cv.v2.widgets.ContactLine; +import com.demcha.compose.document.templates.cv.v2.widgets.Headline; + +import java.util.Objects; + +/** + * v2 cover-letter pair for the {@code ClassicSerif} CV preset. + * + *

Renders the identical masthead as + * {@link com.demcha.compose.document.templates.cv.v2.presets.ClassicSerif} + * β€” a centred letter-spaced PT-Serif name, a thin tan rule, and a + * centred contact line with tan-accent underlined links β€” then a + * single-column letter body via the shared {@link LetterBody}. Both + * documents read their palette / typography from + * {@link CvTheme#classicSerif()}.

+ * + *

The header mirrors the CV's preset-local header DSL (spaced name + + * rule line + centred contact). The bronze {@code ACCENT} is mirrored + * from the CV, which keeps it preset-local there because no other brand + * shares that fifth colour token.

+ */ +public final class ClassicSerifLetter { + + /** Stable template identifier. */ + public static final String ID = "classic-serif-letter"; + + /** Human-readable display name. */ + public static final String DISPLAY_NAME = "Classic Serif Letter"; + + /** Recommended page margin (in points) β€” generous business-letter feel. */ + public static final double RECOMMENDED_MARGIN = 48.0; + + /** Bronze accent for contact links. Mirrors the ClassicSerif CV's preset-local token. */ + private static final DocumentColor ACCENT = DocumentColor.rgb(126, 93, 52); + + private ClassicSerifLetter() { + } + + /** + * Builds the letter with its Classic Serif theme. + */ + public static DocumentTemplate create() { + return create(CvTheme.classicSerif()); + } + + /** + * Builds the letter with a caller-supplied theme (share the paired + * CV's theme instance for a guaranteed visual match). + */ + public static DocumentTemplate create(CvTheme theme) { + Objects.requireNonNull(theme, "theme"); + return new Template(theme); + } + + private static final class Template implements DocumentTemplate { + + 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, CoverLetterDocument doc) { + Objects.requireNonNull(document, "document"); + Objects.requireNonNull(doc, "doc"); + + double width = document.canvas().innerWidth(); + PageFlowBuilder flow = document.dsl() + .pageFlow() + .name("CoverLetterV2ClassicSerifRoot") + .spacing(theme.spacing().pageFlowSpacing()); + + addHeader(flow, doc.identity(), width); + + flow.addSection("CoverLetterV2ClassicSerifBody", host -> + LetterBody.render(host, doc, theme)); + + flow.build(); + } + + private void addHeader(PageFlowBuilder flow, CvIdentity identity, + double width) { + flow.addSection("CoverLetterV2ClassicSerifHeader", section -> { + section.spacing(5); + Headline.spacedCentered(section, identity.name(), theme); + section.addLine(line -> line + .name("CoverLetterV2ClassicSerifHeaderRule") + .horizontal(width) + .color(theme.palette().rule()) + .thickness(theme.spacing().accentRuleWidth()) + .margin(new DocumentInsets(1, 0, 0, 0))); + ContactLine.centered(section, identity, theme, + contactMetaStyle(), contactLinkStyle(), + contactSeparatorStyle()); + }); + } + + private DocumentTextStyle contactMetaStyle() { + return CvTextStyles.of(theme.typography().bodyFont(), + theme.typography().sizeContact(), + DocumentTextDecoration.DEFAULT, + theme.palette().muted()); + } + + private DocumentTextStyle contactLinkStyle() { + return CvTextStyles.of(theme.typography().bodyFont(), + theme.typography().sizeContact(), + DocumentTextDecoration.UNDERLINE, + ACCENT); + } + + private DocumentTextStyle contactSeparatorStyle() { + return CvTextStyles.of(theme.typography().bodyFont(), + theme.typography().sizeContact(), + DocumentTextDecoration.DEFAULT, + theme.palette().rule()); + } + } +} diff --git a/src/main/java/com/demcha/compose/document/templates/coverletter/v2/presets/CompactMonoLetter.java b/src/main/java/com/demcha/compose/document/templates/coverletter/v2/presets/CompactMonoLetter.java new file mode 100644 index 00000000..654411df --- /dev/null +++ b/src/main/java/com/demcha/compose/document/templates/coverletter/v2/presets/CompactMonoLetter.java @@ -0,0 +1,163 @@ +package com.demcha.compose.document.templates.coverletter.v2.presets; + +import com.demcha.compose.document.api.DocumentSession; +import com.demcha.compose.document.dsl.PageFlowBuilder; +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.coverletter.v2.components.LetterBody; +import com.demcha.compose.document.templates.coverletter.v2.data.CoverLetterDocument; +import com.demcha.compose.document.templates.cv.v2.components.CvTextStyles; +import com.demcha.compose.document.templates.cv.v2.data.CvIdentity; +import com.demcha.compose.document.templates.cv.v2.theme.CvTheme; +import com.demcha.compose.document.templates.cv.v2.widgets.ContactLine; +import com.demcha.compose.document.templates.cv.v2.widgets.Headline; + +import java.util.Objects; + +/** + * v2 cover-letter pair for the {@code CompactMono} CV preset. + * + *

Carries the CV's signature dark command-bar header + * into the letter: a near-black rounded band holding the UPPERCASE + * left-aligned name over a left-aligned contact line with cyan links and + * grey separators β€” the same header as + * {@link com.demcha.compose.document.templates.cv.v2.presets.CompactMono}. + * Below it, a single-column letter body via the shared + * {@link LetterBody}. Body palette / typography come from + * {@link CvTheme#compactMono()}.

+ * + *

The four command-bar colours are mirrored from the CV, where they + * are preset-local. A near-invisible width rule (band-coloured, 0.1pt) + * pins the band to the full content width β€” the same trick the CV uses + * so the dark bar spans the page instead of shrinking to the name.

+ */ +public final class CompactMonoLetter { + + /** Stable template identifier. */ + public static final String ID = "compact-mono-letter"; + + /** Human-readable display name. */ + public static final String DISPLAY_NAME = "Compact Mono Letter"; + + /** Recommended page margin (in points) β€” generous business-letter feel. */ + public static final double RECOMMENDED_MARGIN = 48.0; + + /** Near-black command-bar fill. Mirrors the CompactMono CV token. */ + private static final DocumentColor HEADER = DocumentColor.rgb(18, 24, 32); + + /** Contact metadata over the dark band. Mirrors the CV token. */ + private static final DocumentColor HEADER_SOFT = DocumentColor.rgb(192, 207, 219); + + /** Cyan contact-link colour over the band. Mirrors the CV token. */ + private static final DocumentColor LINK_CYAN = DocumentColor.rgb(108, 213, 222); + + /** Contact separator colour over the band. Mirrors the CV token. */ + private static final DocumentColor SEPARATOR_GRAY = DocumentColor.rgb(102, 117, 132); + + private CompactMonoLetter() { + } + + /** + * Builds the letter with its Compact Mono theme. + */ + public static DocumentTemplate create() { + return create(CvTheme.compactMono()); + } + + /** + * Builds the letter with a caller-supplied theme (share the paired + * CV's theme instance for a guaranteed visual match). + */ + public static DocumentTemplate create(CvTheme theme) { + Objects.requireNonNull(theme, "theme"); + return new Template(theme); + } + + private static final class Template implements DocumentTemplate { + + 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, CoverLetterDocument doc) { + Objects.requireNonNull(document, "document"); + Objects.requireNonNull(doc, "doc"); + + double width = document.canvas().innerWidth(); + PageFlowBuilder flow = document.dsl() + .pageFlow() + .name("CoverLetterV2CompactMonoRoot") + .spacing(theme.spacing().pageFlowSpacing()); + + addHeader(flow, doc.identity(), width); + + flow.addSection("CoverLetterV2CompactMonoBody", host -> + LetterBody.render(host, doc, theme)); + + flow.build(); + } + + private void addHeader(PageFlowBuilder flow, CvIdentity identity, + double width) { + flow.addSection("CoverLetterV2CompactMonoHeader", section -> { + section.spacing(4) + .padding(new DocumentInsets(13, 16, 14, 16)) + .fillColor(HEADER) + .cornerRadius(3); + section.addSection("Name", name -> + Headline.uppercaseLeftAligned(name, identity.name(), + theme, headerNameStyle())); + section.addSection("Contact", contact -> + ContactLine.leftAligned(contact, identity, theme, + headerMetaStyle(), headerLinkStyle(), + headerSeparatorStyle())); + section.addLine(line -> line + .name("CoverLetterV2CompactMonoHeaderWidthRule") + .horizontal(Math.max(0, width - 32)) + .color(HEADER) + .thickness(0.1) + .margin(DocumentInsets.zero())); + }); + } + + private DocumentTextStyle headerNameStyle() { + return CvTextStyles.of(theme.typography().headlineFont(), + theme.typography().sizeHeadline(), + DocumentTextDecoration.BOLD, DocumentColor.WHITE); + } + + private DocumentTextStyle headerMetaStyle() { + return CvTextStyles.of(theme.typography().bodyFont(), + theme.typography().sizeContact(), + DocumentTextDecoration.DEFAULT, HEADER_SOFT); + } + + private DocumentTextStyle headerLinkStyle() { + return CvTextStyles.of(theme.typography().bodyFont(), + theme.typography().sizeContact(), + DocumentTextDecoration.UNDERLINE, LINK_CYAN); + } + + private DocumentTextStyle headerSeparatorStyle() { + return CvTextStyles.of(theme.typography().bodyFont(), + theme.typography().sizeContact(), + DocumentTextDecoration.DEFAULT, SEPARATOR_GRAY); + } + } +} diff --git a/src/main/java/com/demcha/compose/document/templates/coverletter/v2/presets/EditorialBlueLetter.java b/src/main/java/com/demcha/compose/document/templates/coverletter/v2/presets/EditorialBlueLetter.java new file mode 100644 index 00000000..24f12dac --- /dev/null +++ b/src/main/java/com/demcha/compose/document/templates/coverletter/v2/presets/EditorialBlueLetter.java @@ -0,0 +1,126 @@ +package com.demcha.compose.document.templates.coverletter.v2.presets; + +import com.demcha.compose.document.api.DocumentSession; +import com.demcha.compose.document.dsl.PageFlowBuilder; +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.coverletter.v2.components.LetterBody; +import com.demcha.compose.document.templates.coverletter.v2.data.CoverLetterDocument; +import com.demcha.compose.document.templates.cv.v2.components.CvTextStyles; +import com.demcha.compose.document.templates.cv.v2.theme.CvTheme; +import com.demcha.compose.document.templates.cv.v2.widgets.Masthead; +import com.demcha.compose.font.FontName; + +import java.util.Objects; + +/** + * v2 cover-letter pair for the {@code EditorialBlue} CV preset. + * + *

Renders the identical masthead as + * {@link com.demcha.compose.document.templates.cv.v2.presets.EditorialBlue} + * β€” a centred navy Helvetica name (with the job-title subtitle), centred + * contact metadata, and blue underlined profile links, via the shared + * {@link Masthead#centered} widget β€” then a single-column letter body + * via the shared {@link LetterBody}. Both documents read their palette / + * typography from {@link CvTheme#editorialBlue()}.

+ * + *

Only the navy {@code NAME_COLOR} is mirrored from the CV (its + * preset-local token); everything else flows through {@code Masthead} + * and the theme.

+ */ +public final class EditorialBlueLetter { + + /** Stable template identifier. */ + public static final String ID = "editorial-blue-letter"; + + /** Human-readable display name. */ + public static final String DISPLAY_NAME = "Editorial Blue Letter"; + + /** Recommended page margin (in points) β€” generous business-letter feel. */ + public static final double RECOMMENDED_MARGIN = 48.0; + + /** Navy display name. Mirrors the EditorialBlue CV's preset-local token. */ + private static final DocumentColor NAME_COLOR = DocumentColor.rgb(18, 31, 72); + + private EditorialBlueLetter() { + } + + /** + * Builds the letter with its Editorial Blue theme. + */ + public static DocumentTemplate create() { + return create(CvTheme.editorialBlue()); + } + + /** + * Builds the letter with a caller-supplied theme (share the paired + * CV's theme instance for a guaranteed visual match). + */ + public static DocumentTemplate create(CvTheme theme) { + Objects.requireNonNull(theme, "theme"); + return new Template(theme); + } + + private static final class Template implements DocumentTemplate { + + 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, CoverLetterDocument doc) { + Objects.requireNonNull(document, "document"); + Objects.requireNonNull(doc, "doc"); + + PageFlowBuilder flow = document.dsl() + .pageFlow() + .name("CoverLetterV2EditorialBlueRoot") + .spacing(theme.spacing().pageFlowSpacing()); + + flow.addSection("CoverLetterV2EditorialBlueHeader", section -> + Masthead.centered(section, doc.identity(), theme, + mastheadStyle())); + + flow.addSection("CoverLetterV2EditorialBlueBody", host -> + LetterBody.render(host, doc, theme)); + + flow.build(); + } + + private Masthead.Style mastheadStyle() { + DocumentTextStyle nameStyle = CvTextStyles.of(FontName.HELVETICA_BOLD, + theme.typography().sizeHeadline(), + DocumentTextDecoration.BOLD, NAME_COLOR); + DocumentTextStyle titleStyle = CvTextStyles.of(FontName.HELVETICA, + 10.0, DocumentTextDecoration.DEFAULT, + theme.palette().ink()); + DocumentTextStyle linkStyle = CvTextStyles.of(FontName.HELVETICA, + theme.typography().sizeContact(), + DocumentTextDecoration.UNDERLINE, + theme.palette().rule()); + return Masthead.Style.builder() + .nameStyle(nameStyle) + .titleStyle(titleStyle) + .metaStyle(theme.contactStyle()) + .linkStyle(linkStyle) + .separatorStyle(theme.contactStyle()) + .lineMargin(DocumentInsets.top(1)) + .build(); + } + } +} diff --git a/src/main/java/com/demcha/compose/document/templates/coverletter/v2/presets/EngineeringResumeLetter.java b/src/main/java/com/demcha/compose/document/templates/coverletter/v2/presets/EngineeringResumeLetter.java new file mode 100644 index 00000000..39bfa281 --- /dev/null +++ b/src/main/java/com/demcha/compose/document/templates/coverletter/v2/presets/EngineeringResumeLetter.java @@ -0,0 +1,230 @@ +package com.demcha.compose.document.templates.coverletter.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.DocumentCornerRadius; +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.coverletter.v2.components.LetterBody; +import com.demcha.compose.document.templates.coverletter.v2.data.CoverLetterDocument; +import com.demcha.compose.document.templates.cv.v2.components.CvTextStyles; +import com.demcha.compose.document.templates.cv.v2.components.MarkdownInline; +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.theme.CvTheme; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Objects; + +/** + * v2 cover-letter pair for the {@code EngineeringResume} CV preset. + * + *

Carries the CV's signature full-width navy command + * header into the letter: a deep-navy band (rounded top, green + * accent strip beneath) holding the UPPERCASE name + role subtitle on + * the left and a right-aligned contact stack with cyan-green underlined + * links β€” the same masthead as + * {@link com.demcha.compose.document.templates.cv.v2.presets.EngineeringResume}. + * Below the band, a single-column letter body via the shared + * {@link LetterBody}. Body palette / typography come from + * {@link CvTheme#engineeringResume()}.

+ * + *

The five navy-header colours are mirrored from the CV, where they + * are preset-local (the theme only covers body ink / muted / rule / + * profile-band fill β€” no other brand shares this navy command look).

+ */ +public final class EngineeringResumeLetter { + + /** Stable template identifier. */ + public static final String ID = "engineering-resume-letter"; + + /** Human-readable display name. */ + public static final String DISPLAY_NAME = "Engineering Resume Letter"; + + /** Recommended page margin (in points) β€” generous business-letter feel. */ + public static final double RECOMMENDED_MARGIN = 48.0; + + /** Deep navy command-header fill. Mirrors the EngineeringResume CV token. */ + private static final DocumentColor NAVY = DocumentColor.rgb(13, 32, 47); + + /** Green accent strip beneath the header. Mirrors the CV token. */ + private static final DocumentColor GREEN = DocumentColor.rgb(27, 145, 104); + + /** Role-subtitle colour under the name. Mirrors the CV token. */ + private static final DocumentColor SUBTITLE_COLOR = DocumentColor.rgb(190, 209, 219); + + /** Contact metadata colour over the navy header. Mirrors the CV token. */ + private static final DocumentColor CONTACT_META = DocumentColor.rgb(196, 211, 220); + + /** Cyan-green contact-link colour over the navy header. Mirrors the CV token. */ + private static final DocumentColor CONTACT_LINK = DocumentColor.rgb(78, 207, 161); + + private EngineeringResumeLetter() { + } + + /** + * Builds the letter with its Engineering Resume theme. + */ + public static DocumentTemplate create() { + return create(CvTheme.engineeringResume()); + } + + /** + * Builds the letter with a caller-supplied theme (share the paired + * CV's theme instance for a guaranteed visual match). + */ + public static DocumentTemplate create(CvTheme theme) { + Objects.requireNonNull(theme, "theme"); + return new Template(theme); + } + + private static final class Template implements DocumentTemplate { + + 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, CoverLetterDocument doc) { + Objects.requireNonNull(document, "document"); + Objects.requireNonNull(doc, "doc"); + + PageFlowBuilder flow = document.dsl() + .pageFlow() + .name("CoverLetterV2EngineeringResumeRoot") + .spacing(theme.spacing().pageFlowSpacing()); + + addHeader(flow, doc.identity()); + + flow.addSection("CoverLetterV2EngineeringResumeBody", host -> + LetterBody.render(host, doc, theme)); + + flow.build(); + } + + private void addHeader(PageFlowBuilder flow, CvIdentity identity) { + flow.addSection("CoverLetterV2EngineeringResumeHeader", section -> section + .spacing(5) + .padding(new DocumentInsets(13, 15, 13, 15)) + .fillColor(NAVY) + .cornerRadius(DocumentCornerRadius.top( + theme.spacing().bannerCornerRadius())) + .accentBottom(GREEN, theme.spacing().accentRuleWidth()) + .addRow("CoverLetterV2EngineeringResumeHeaderRow", row -> row + .spacing(12) + .weights(1.15, 0.85) + .addSection("CoverLetterV2EngineeringResumeIdentity", + block -> addIdentityBlock(block, identity)) + .addSection("CoverLetterV2EngineeringResumeContact", + contact -> addContactStack(contact, identity)))); + } + + private void addIdentityBlock(SectionBuilder block, CvIdentity identity) { + block.padding(DocumentInsets.zero()) + .spacing(3) + .addParagraph(paragraph -> paragraph + .text(identity.name().full().toUpperCase(Locale.ROOT)) + .textStyle(nameStyle()) + .autoSize(theme.typography().sizeHeadline(), 19.0) + .margin(DocumentInsets.zero())); + String subtitle = headerSubtitleText(identity); + if (!subtitle.isBlank()) { + block.addParagraph(paragraph -> paragraph + .text(subtitle) + .textStyle(subtitleStyle()) + .margin(DocumentInsets.zero())); + } + } + + private void addContactStack(SectionBuilder section, CvIdentity identity) { + section.spacing(2).padding(DocumentInsets.zero()); + DocumentTextStyle meta = contactMetaStyle(); + DocumentTextStyle link = contactLinkStyle(); + for (ContactPart part : contactParts(identity)) { + section.addParagraph(paragraph -> paragraph + .text(part.text()) + .textStyle(part.linkOptions() == null ? meta : link) + .link(part.linkOptions()) + .align(TextAlign.RIGHT) + .margin(DocumentInsets.zero())); + } + } + + private DocumentTextStyle nameStyle() { + return CvTextStyles.of(theme.typography().headlineFont(), + theme.typography().sizeHeadline(), + DocumentTextDecoration.BOLD, DocumentColor.WHITE); + } + + private DocumentTextStyle subtitleStyle() { + return CvTextStyles.of(theme.typography().bodyFont(), 7.6, + DocumentTextDecoration.BOLD, SUBTITLE_COLOR); + } + + private DocumentTextStyle contactMetaStyle() { + return CvTextStyles.of(theme.typography().bodyFont(), + theme.typography().sizeContact(), + DocumentTextDecoration.DEFAULT, CONTACT_META); + } + + private DocumentTextStyle contactLinkStyle() { + return CvTextStyles.of(theme.typography().bodyFont(), + theme.typography().sizeContact(), + DocumentTextDecoration.UNDERLINE, CONTACT_LINK); + } + + private static String headerSubtitleText(CvIdentity identity) { + String jobTitle = identity.jobTitle(); + if (jobTitle == null || jobTitle.isBlank()) { + return ""; + } + return MarkdownInline.plainText(jobTitle).toUpperCase(Locale.ROOT); + } + + private static List contactParts(CvIdentity identity) { + List parts = new ArrayList<>(); + addPart(parts, identity.contact().address(), null); + addPart(parts, identity.contact().phone(), null); + String email = identity.contact().email(); + if (!email.isBlank()) { + addPart(parts, email, new DocumentLinkOptions("mailto:" + email)); + } + for (CvLink link : identity.links()) { + addPart(parts, link.label(), link.url().isBlank() + ? null + : new DocumentLinkOptions(link.url().trim())); + } + return List.copyOf(parts); + } + + private static void addPart(List parts, String text, + DocumentLinkOptions linkOptions) { + if (text != null && !text.isBlank()) { + parts.add(new ContactPart(text.trim(), linkOptions)); + } + } + + private record ContactPart(String text, DocumentLinkOptions linkOptions) { + } + } +} diff --git a/src/main/java/com/demcha/compose/document/templates/coverletter/v2/presets/ExecutiveLetter.java b/src/main/java/com/demcha/compose/document/templates/coverletter/v2/presets/ExecutiveLetter.java new file mode 100644 index 00000000..d2f8ee9b --- /dev/null +++ b/src/main/java/com/demcha/compose/document/templates/coverletter/v2/presets/ExecutiveLetter.java @@ -0,0 +1,220 @@ +package com.demcha.compose.document.templates.coverletter.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.coverletter.v2.components.LetterBody; +import com.demcha.compose.document.templates.coverletter.v2.data.CoverLetterDocument; +import com.demcha.compose.document.templates.cv.v2.components.CvTextStyles; +import com.demcha.compose.document.templates.cv.v2.components.TextOrnaments; +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.theme.CvTheme; +import com.demcha.compose.document.templates.cv.v2.widgets.Headline; +import com.demcha.compose.font.FontName; + +import java.util.Objects; + +/** + * v2 cover-letter pair for the {@code Executive} CV preset. + * + *

Renders the identical masthead as + * {@link com.demcha.compose.document.templates.cv.v2.presets.Executive} + * β€” UPPERCASE name in deep slate, a {@code address | phone} meta line, a + * bronze-underlined link row, and a thin full-width muted rule β€” then a + * single-column letter body (greeting, paragraphs, closing) via the + * shared {@link LetterBody}. Both documents read all colour, font, and + * spacing from {@link CvTheme#executive()}, so a writer's CV and cover + * letter ship as one matched set.

+ * + *

The masthead block is preset-local inline DSL mirroring the CV's, + * because the CV's header is itself preset-local (V1 splits meta and + * links across two rows β€” no shared v2 contact widget has that exact + * shape today). When a second brand needs the same header shape, this + * block should be promoted to a shared {@code coverletter/v2/widgets} + * letterhead widget the CV preset can also adopt.

+ */ +public final class ExecutiveLetter { + + /** Stable template identifier. */ + public static final String ID = "executive-letter"; + + /** Human-readable display name. */ + public static final String DISPLAY_NAME = "Executive Letter"; + + /** Recommended page margin (in points) β€” generous business-letter feel. */ + public static final double RECOMMENDED_MARGIN = 48.0; + + /** + * Deeper slate of the Executive masthead. Mirrors the preset-local + * {@code PRIMARY_NAME} token of the Executive CV (the theme's + * {@code palette().ink()} is the lighter body slate); kept local + * here for the same reason it is local there β€” no other brand + * shares it. + */ + private static final DocumentColor PRIMARY_NAME = + DocumentColor.rgb(24, 35, 51); + + /** + * Warm bronze of the Executive contact links. Mirrors the + * preset-local {@code ACCENT} token of the Executive CV. + */ + private static final DocumentColor ACCENT = + DocumentColor.rgb(172, 112, 55); + + private ExecutiveLetter() { + } + + /** + * Builds the letter with its Executive theme. + */ + public static DocumentTemplate create() { + return create(CvTheme.executive()); + } + + /** + * Builds the letter with a caller-supplied theme (share the paired + * CV's theme instance for a guaranteed visual match). + */ + public static DocumentTemplate create(CvTheme theme) { + Objects.requireNonNull(theme, "theme"); + return new Template(theme); + } + + private static final class Template implements DocumentTemplate { + + 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, CoverLetterDocument doc) { + Objects.requireNonNull(document, "document"); + Objects.requireNonNull(doc, "doc"); + + double width = document.canvas().innerWidth(); + PageFlowBuilder flow = document.dsl() + .pageFlow() + .name("CoverLetterV2ExecutiveRoot") + .spacing(theme.spacing().pageFlowSpacing()); + + addHeader(flow, doc.identity(), width); + + flow.addSection("CoverLetterV2ExecutiveBody", host -> + LetterBody.render(host, doc, theme)); + + flow.build(); + } + + private void addHeader(PageFlowBuilder flow, CvIdentity identity, + double width) { + flow.addSection("CoverLetterV2ExecutiveHeader", section -> { + section.spacing(2) + .padding(DocumentInsets.zero()); + Headline.uppercaseLeftAligned(section, identity.name(), theme, + nameStyle()); + String meta = TextOrnaments.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("CoverLetterV2ExecutiveHeaderRule") + .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); + } + } +} diff --git a/src/main/java/com/demcha/compose/document/templates/coverletter/v2/presets/ModernProfessionalLetter.java b/src/main/java/com/demcha/compose/document/templates/coverletter/v2/presets/ModernProfessionalLetter.java new file mode 100644 index 00000000..d2017538 --- /dev/null +++ b/src/main/java/com/demcha/compose/document/templates/coverletter/v2/presets/ModernProfessionalLetter.java @@ -0,0 +1,130 @@ +package com.demcha.compose.document.templates.coverletter.v2.presets; + +import com.demcha.compose.document.api.DocumentSession; +import com.demcha.compose.document.dsl.PageFlowBuilder; +import com.demcha.compose.document.style.DocumentColor; +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.coverletter.v2.components.LetterBody; +import com.demcha.compose.document.templates.coverletter.v2.data.CoverLetterDocument; +import com.demcha.compose.document.templates.cv.v2.components.CvTextStyles; +import com.demcha.compose.document.templates.cv.v2.theme.CvTheme; +import com.demcha.compose.document.templates.cv.v2.widgets.ContactLine; +import com.demcha.compose.document.templates.cv.v2.widgets.Headline; +import com.demcha.compose.font.FontName; + +import java.util.Objects; + +/** + * v2 cover-letter pair for the {@code ModernProfessional} CV preset. + * + *

Renders the identical masthead as + * {@link com.demcha.compose.document.templates.cv.v2.presets.ModernProfessional} + * β€” a right-aligned slate-blue Helvetica display name over a two-row, + * right-aligned contact stack with royal-blue underlined links and a + * bottom accent rule β€” then a single-column letter body via the shared + * {@link LetterBody}. Both documents read their scale and palette from + * {@link CvTheme#modernProfessional()}.

+ * + *

Unlike Executive, the header is composed almost entirely from + * shared widgets ({@link Headline#rightAligned} + + * {@link ContactLine#twoRowRightAligned}); only the three preset-local + * colours (slate-blue name, royal-blue links) are mirrored from the CV, + * which keeps them preset-local there because no other brand shares + * them.

+ */ +public final class ModernProfessionalLetter { + + /** Stable template identifier. */ + public static final String ID = "modern-professional-letter"; + + /** Human-readable display name. */ + public static final String DISPLAY_NAME = "Modern Professional Letter"; + + /** Recommended page margin (in points) β€” generous business-letter feel. */ + public static final double RECOMMENDED_MARGIN = 48.0; + + /** Slate-blue display name. Mirrors the ModernProfessional CV's preset-local token. */ + private static final DocumentColor NAME_COLOR = DocumentColor.rgb(44, 62, 80); + + /** Royal-blue contact links. Mirrors the ModernProfessional CV's preset-local token. */ + private static final DocumentColor LINK_COLOR = DocumentColor.rgb(65, 105, 225); + + private ModernProfessionalLetter() { + } + + /** + * Builds the letter with its Modern Professional theme. + */ + public static DocumentTemplate create() { + return create(CvTheme.modernProfessional()); + } + + /** + * Builds the letter with a caller-supplied theme (share the paired + * CV's theme instance for a guaranteed visual match). + */ + public static DocumentTemplate create(CvTheme theme) { + Objects.requireNonNull(theme, "theme"); + return new Template(theme); + } + + private static final class Template implements DocumentTemplate { + + 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, CoverLetterDocument doc) { + Objects.requireNonNull(document, "document"); + Objects.requireNonNull(doc, "doc"); + + DocumentTextStyle nameStyle = CvTextStyles.of(FontName.HELVETICA_BOLD, + theme.typography().sizeHeadline(), + DocumentTextDecoration.BOLD, NAME_COLOR); + DocumentTextStyle contactBodyStyle = CvTextStyles.of(FontName.HELVETICA, + theme.typography().sizeContact(), + DocumentTextDecoration.DEFAULT, theme.palette().ink()); + DocumentTextStyle contactLinkStyle = CvTextStyles.of(FontName.HELVETICA, + theme.typography().sizeContact(), + DocumentTextDecoration.UNDERLINE, LINK_COLOR); + DocumentTextStyle contactSeparatorStyle = CvTextStyles.of(FontName.HELVETICA, + theme.typography().sizeContact(), + DocumentTextDecoration.DEFAULT, theme.palette().rule()); + + PageFlowBuilder flow = document.dsl() + .pageFlow() + .name("CoverLetterV2ModernRoot") + .spacing(theme.spacing().pageFlowSpacing()) + .addSection("Header", section -> + Headline.rightAligned(section, doc.identity().name(), + theme, nameStyle)) + .addSection("Contact", section -> { + section.accentBottom(theme.palette().rule(), + theme.spacing().accentRuleWidth()); + ContactLine.twoRowRightAligned(section, doc.identity(), + theme, contactBodyStyle, contactLinkStyle, + contactSeparatorStyle); + }); + + flow.addSection("CoverLetterV2ModernBody", host -> + LetterBody.render(host, doc, theme)); + + flow.build(); + } + } +} diff --git a/src/main/java/com/demcha/compose/document/templates/coverletter/v2/presets/MonogramSidebarLetter.java b/src/main/java/com/demcha/compose/document/templates/coverletter/v2/presets/MonogramSidebarLetter.java new file mode 100644 index 00000000..0c4e2bbe --- /dev/null +++ b/src/main/java/com/demcha/compose/document/templates/coverletter/v2/presets/MonogramSidebarLetter.java @@ -0,0 +1,261 @@ +package com.demcha.compose.document.templates.coverletter.v2.presets; + +import com.demcha.compose.document.api.DocumentSession; +import com.demcha.compose.document.dsl.EllipseBuilder; +import com.demcha.compose.document.dsl.LayerStackBuilder; +import com.demcha.compose.document.dsl.PageFlowBuilder; +import com.demcha.compose.document.dsl.ParagraphBuilder; +import com.demcha.compose.document.dsl.SectionBuilder; +import com.demcha.compose.document.node.LayerAlign; +import com.demcha.compose.document.node.LayerStackNode; +import com.demcha.compose.document.node.SpacerNode; +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.DocumentStroke; +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.coverletter.v2.components.LetterBody; +import com.demcha.compose.document.templates.coverletter.v2.data.CoverLetterDocument; +import com.demcha.compose.document.templates.cv.v2.components.CvTextStyles; +import com.demcha.compose.document.templates.cv.v2.components.TextOrnaments; +import com.demcha.compose.document.templates.cv.v2.data.CvIdentity; +import com.demcha.compose.document.templates.cv.v2.data.CvName; +import com.demcha.compose.document.templates.cv.v2.theme.CvTheme; +import com.demcha.compose.document.templates.cv.v2.widgets.ContactLine; +import com.demcha.compose.font.FontName; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * v2 cover-letter pair for the {@code MonogramSidebar} CV preset. + * + *

Carries the CV's signature monogram-ring badge + * into the letter: the dark-slate initials ring sits centred at the top, + * over the centred spaced-caps name (stacked first / last) and a muted + * gold spaced-caps role line, then a centred contact line and a + * single-column letter body via the shared {@link LetterBody}. The CV's + * pale-teal sidebar column (painted by {@code pageBackgrounds}) and its + * icon contact stack are sidebar-only and are dropped for the + * single-column letter; the badge + name treatment is what makes the two + * read as a set. Palette / typography come from + * {@link CvTheme#monogramSidebar()}.

+ * + *

The gold accent, dark monogram ring, and the PT-Serif monogram font + * are mirrored from the CV, where they are preset-local.

+ */ +public final class MonogramSidebarLetter { + + /** Stable template identifier. */ + public static final String ID = "monogram-sidebar-letter"; + + /** Human-readable display name. */ + public static final String DISPLAY_NAME = "Monogram Sidebar Letter"; + + /** Recommended page margin (in points) β€” generous business-letter feel. */ + public static final double RECOMMENDED_MARGIN = 48.0; + + /** Muted gold accent (name sub-line + links). Mirrors the CV token. */ + private static final DocumentColor ACCENT = DocumentColor.rgb(158, 146, 104); + + /** Dark slate monogram ring + initials. Mirrors the CV token. */ + private static final DocumentColor MONOGRAM_RING = DocumentColor.rgb(54, 62, 74); + + /** PT-Serif used only for the monogram initials. Mirrors the CV token. */ + private static final FontName MONOGRAM_FONT = FontName.PT_SERIF; + + /** Monogram ring diameter (matches the CV badge). */ + private static final double MONOGRAM_DIAMETER = 122.0; + + /** + * Letter body size. The Monogram Sidebar CV theme uses a 7.5pt body + * tuned for its dense two-column layout β€” too small for a + * single-column letter, so the prose is rendered a touch larger here. + */ + private static final double LETTER_BODY_SIZE = 9.0; + + private MonogramSidebarLetter() { + } + + /** + * Builds the letter with its Monogram Sidebar theme. + */ + public static DocumentTemplate create() { + return create(CvTheme.monogramSidebar()); + } + + /** + * Builds the letter with a caller-supplied theme (share the paired + * CV's theme instance for a guaranteed visual match). + */ + public static DocumentTemplate create(CvTheme theme) { + Objects.requireNonNull(theme, "theme"); + return new Template(theme); + } + + private static final class Template implements DocumentTemplate { + + 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, CoverLetterDocument doc) { + Objects.requireNonNull(document, "document"); + Objects.requireNonNull(doc, "doc"); + + double innerWidth = document.canvas().innerWidth(); + PageFlowBuilder flow = document.dsl() + .pageFlow() + .name("CoverLetterV2MonogramSidebarRoot") + .spacing(theme.spacing().pageFlowSpacing()); + + flow.addSection("CoverLetterV2MonogramSidebarHeader", section -> { + section.spacing(2).padding(DocumentInsets.zero()); + addMonogramBlock(section, initials(doc.identity().name()), + innerWidth); + addNameBlock(section, doc.identity()); + ContactLine.centered(section, doc.identity(), theme, + contactMetaStyle(), contactLinkStyle(), + contactSeparatorStyle()); + }); + + flow.addSection("CoverLetterV2MonogramSidebarBody", host -> + LetterBody.render(host, doc, theme, LETTER_BODY_SIZE)); + + flow.build(); + } + + private void addMonogramBlock(SectionBuilder section, String initialsText, + double innerWidth) { + LayerStackNode badge = new LayerStackBuilder() + .name("CoverLetterV2MonogramSidebarBadge") + .back(new EllipseBuilder() + .name("CoverLetterV2MonogramSidebarRing") + .size(MONOGRAM_DIAMETER, MONOGRAM_DIAMETER) + .stroke(DocumentStroke.of(MONOGRAM_RING, 1.25)) + .build()) + .layer(new ParagraphBuilder() + .name("CoverLetterV2MonogramSidebarInitials") + .text(initialsText) + .textStyle(CvTextStyles.of(MONOGRAM_FONT, 44.0, + DocumentTextDecoration.BOLD, MONOGRAM_RING)) + .align(TextAlign.LEFT) + .build(), LayerAlign.CENTER) + .build(); + + section.addLayerStack(outer -> outer + .name("CoverLetterV2MonogramSidebarFrame") + .margin(DocumentInsets.bottom(20)) + .back(new SpacerNode( + "CoverLetterV2MonogramSidebarSpace", + Math.max(MONOGRAM_DIAMETER, innerWidth), + MONOGRAM_DIAMETER, + DocumentInsets.zero(), + DocumentInsets.zero())) + .layer(badge, LayerAlign.TOP_CENTER)); + } + + private void addNameBlock(SectionBuilder section, CvIdentity identity) { + CvName name = identity.name(); + List parts = new ArrayList<>(); + if (!name.first().isBlank()) { + parts.add(name.first()); + } + if (!name.last().isBlank()) { + parts.add(name.last()); + } + if (parts.isEmpty()) { + parts.add(""); + } + DocumentTextStyle nameStyle = nameStyle(); + DocumentTextStyle titleStyle = subtitleStyle(); + + for (int index = 0; index < parts.size(); index++) { + String part = parts.get(index); + DocumentInsets margin = index == parts.size() - 1 + ? DocumentInsets.zero() + : DocumentInsets.bottom(6); + section.addParagraph(paragraph -> paragraph + .text(TextOrnaments.spacedUpper(part)) + .textStyle(nameStyle) + .align(TextAlign.CENTER) + .lineSpacing(1.0) + .margin(margin)); + } + String jobTitle = identity.jobTitle(); + if (jobTitle != null && !jobTitle.isBlank()) { + section.addParagraph(paragraph -> paragraph + .text(TextOrnaments.spacedUpper(jobTitle)) + .textStyle(titleStyle) + .align(TextAlign.CENTER) + .margin(new DocumentInsets(12, 0, 18, 0))); + } + } + + private DocumentTextStyle nameStyle() { + return CvTextStyles.of(theme.typography().headlineFont(), + theme.typography().sizeHeadline(), + DocumentTextDecoration.DEFAULT, theme.palette().ink()); + } + + private DocumentTextStyle subtitleStyle() { + return CvTextStyles.of(theme.typography().bodyFont(), + theme.typography().sizeContact(), + DocumentTextDecoration.BOLD, ACCENT); + } + + private DocumentTextStyle contactMetaStyle() { + return CvTextStyles.of(theme.typography().bodyFont(), + theme.typography().sizeContact(), + DocumentTextDecoration.DEFAULT, theme.palette().muted()); + } + + private DocumentTextStyle contactLinkStyle() { + return CvTextStyles.of(theme.typography().bodyFont(), + theme.typography().sizeContact(), + DocumentTextDecoration.UNDERLINE, ACCENT); + } + + private DocumentTextStyle contactSeparatorStyle() { + return CvTextStyles.of(theme.typography().bodyFont(), + theme.typography().sizeContact(), + DocumentTextDecoration.DEFAULT, theme.palette().rule()); + } + + private static String initials(CvName name) { + if (name == null) { + return ""; + } + StringBuilder builder = new StringBuilder(); + appendInitial(builder, name.first()); + appendInitial(builder, name.last()); + return builder.toString(); + } + + private static void appendInitial(StringBuilder builder, String value) { + if (builder.length() >= 2 || value == null) { + return; + } + String trimmed = value.trim(); + if (!trimmed.isEmpty() && Character.isLetter(trimmed.charAt(0))) { + builder.append(Character.toUpperCase(trimmed.charAt(0))); + } + } + } +} diff --git a/src/main/java/com/demcha/compose/document/templates/coverletter/v2/presets/NordicCleanLetter.java b/src/main/java/com/demcha/compose/document/templates/coverletter/v2/presets/NordicCleanLetter.java new file mode 100644 index 00000000..507bd092 --- /dev/null +++ b/src/main/java/com/demcha/compose/document/templates/coverletter/v2/presets/NordicCleanLetter.java @@ -0,0 +1,155 @@ +package com.demcha.compose.document.templates.coverletter.v2.presets; + +import com.demcha.compose.document.api.DocumentSession; +import com.demcha.compose.document.dsl.PageFlowBuilder; +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.coverletter.v2.components.LetterBody; +import com.demcha.compose.document.templates.coverletter.v2.data.CoverLetterDocument; +import com.demcha.compose.document.templates.cv.v2.components.CvTextStyles; +import com.demcha.compose.document.templates.cv.v2.components.MarkdownInline; +import com.demcha.compose.document.templates.cv.v2.data.CvIdentity; +import com.demcha.compose.document.templates.cv.v2.theme.CvTheme; +import com.demcha.compose.document.templates.cv.v2.widgets.ContactLine; +import com.demcha.compose.document.templates.cv.v2.widgets.Headline; + +import java.util.Locale; +import java.util.Objects; + +/** + * v2 cover-letter pair for the {@code NordicClean} CV preset. + * + *

Reproduces the CV's signature header: a left-aligned UPPERCASE + * Barlow name with a short teal accent bar beneath it + * and an UPPERCASE role sub-line, balanced by a right-aligned stacked + * contact list with teal links β€” the same masthead as + * {@link com.demcha.compose.document.templates.cv.v2.presets.NordicClean}. + * Below it, a single-column letter body via the shared + * {@link LetterBody}. Body palette / typography come from + * {@link CvTheme#nordicClean()}; the CV's tinted profile band is a + * CV-body element and is intentionally not part of the letter.

+ * + *

The teal {@code ACCENT} is mirrored from the CV's default accent + * (the CV exposes it via an Options knob; the letter uses the default).

+ */ +public final class NordicCleanLetter { + + /** Stable template identifier. */ + public static final String ID = "nordic-clean-letter"; + + /** Human-readable display name. */ + public static final String DISPLAY_NAME = "Nordic Clean Letter"; + + /** Recommended page margin (in points) β€” generous business-letter feel. */ + public static final double RECOMMENDED_MARGIN = 48.0; + + /** Teal accent bar + link colour. Mirrors the NordicClean CV default accent. */ + private static final DocumentColor ACCENT = DocumentColor.rgb(28, 128, 135); + + private NordicCleanLetter() { + } + + /** + * Builds the letter with its Nordic Clean theme. + */ + public static DocumentTemplate create() { + return create(CvTheme.nordicClean()); + } + + /** + * Builds the letter with a caller-supplied theme (share the paired + * CV's theme instance for a guaranteed visual match). + */ + public static DocumentTemplate create(CvTheme theme) { + Objects.requireNonNull(theme, "theme"); + return new Template(theme); + } + + private static final class Template implements DocumentTemplate { + + 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, CoverLetterDocument doc) { + Objects.requireNonNull(document, "document"); + Objects.requireNonNull(doc, "doc"); + + PageFlowBuilder flow = document.dsl() + .pageFlow() + .name("CoverLetterV2NordicCleanRoot") + .spacing(theme.spacing().pageFlowSpacing()); + + addHeader(flow, doc.identity()); + + flow.addSection("CoverLetterV2NordicCleanBody", host -> + LetterBody.render(host, doc, theme)); + + flow.build(); + } + + private void addHeader(PageFlowBuilder flow, CvIdentity identity) { + flow.addRow("CoverLetterV2NordicCleanHeader", row -> row + .spacing(14) + .weights(1.2, 0.8) + .addSection("Identity", id -> { + id.spacing(3).padding(new DocumentInsets(1, 0, 2, 0)); + Headline.uppercaseLeftAligned(id, identity.name(), theme, + headlineStyle()); + id.addShape(shape -> shape + .name("CoverLetterV2NordicCleanNameAccent") + .size(64, 2.6) + .fillColor(ACCENT) + .cornerRadius(1.3) + .margin(DocumentInsets.zero())); + if (!identity.jobTitle().isBlank()) { + id.addParagraph(paragraph -> paragraph + .text(MarkdownInline.plainText(identity.jobTitle()) + .toUpperCase(Locale.ROOT)) + .textStyle(CvTextStyles.of( + theme.typography().bodyFont(), 7.7, + DocumentTextDecoration.BOLD, + theme.palette().muted())) + .margin(DocumentInsets.zero())); + } + }) + .addSection("Contact", contact -> + ContactLine.rightAlignedStacked(contact, identity, + theme, contactMetaStyle(), contactLinkStyle()))); + } + + private DocumentTextStyle headlineStyle() { + return CvTextStyles.of(theme.typography().headlineFont(), + theme.typography().sizeHeadline(), + DocumentTextDecoration.BOLD, theme.palette().ink()); + } + + private DocumentTextStyle contactMetaStyle() { + return CvTextStyles.of(theme.typography().bodyFont(), + theme.typography().sizeContact(), + DocumentTextDecoration.DEFAULT, theme.palette().muted()); + } + + private DocumentTextStyle contactLinkStyle() { + return CvTextStyles.of(theme.typography().bodyFont(), + theme.typography().sizeContact(), + DocumentTextDecoration.UNDERLINE, ACCENT); + } + } +} diff --git a/src/main/java/com/demcha/compose/document/templates/coverletter/v2/presets/PanelLetter.java b/src/main/java/com/demcha/compose/document/templates/coverletter/v2/presets/PanelLetter.java new file mode 100644 index 00000000..2db1fb80 --- /dev/null +++ b/src/main/java/com/demcha/compose/document/templates/coverletter/v2/presets/PanelLetter.java @@ -0,0 +1,231 @@ +package com.demcha.compose.document.templates.coverletter.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.DocumentStroke; +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.coverletter.v2.components.LetterBody; +import com.demcha.compose.document.templates.coverletter.v2.data.CoverLetterDocument; +import com.demcha.compose.document.templates.cv.v2.components.CvTextStyles; +import com.demcha.compose.document.templates.cv.v2.components.TextOrnaments; +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.theme.CvTheme; +import com.demcha.compose.document.templates.widgets.CardWidget; +import com.demcha.compose.font.FontName; + +import java.util.Locale; +import java.util.Objects; + +/** + * v2 cover-letter pair for the {@code Panel} CV preset. + * + *

Carries the CV's signature pale-teal header card + * into the letter: a full-width rounded card (thin teal stroke) holding + * the centred UPPERCASE Poppins name, job title, centred meta line, and + * a centred link row with teal accent links β€” the same header card as + * {@link com.demcha.compose.document.templates.cv.v2.presets.Panel}. + * Below it, a single-column letter body via the shared + * {@link LetterBody}. Card shell + body palette come from + * {@link CvTheme#panel()}.

+ * + *

The two masthead colours (deep-navy header text, teal accent) are + * mirrored from the CV, where they are preset-local. The header card is + * pinned to the full content width with a zero-height spacer + * ({@code widthAnchor}) so it spans the page rather than shrinking to + * fit the name β€” the same trick the CV uses to keep its panels aligned.

+ */ +public final class PanelLetter { + + /** Stable template identifier. */ + public static final String ID = "panel-letter"; + + /** Human-readable display name. */ + public static final String DISPLAY_NAME = "Panel Letter"; + + /** Recommended page margin (in points) β€” generous business-letter feel. */ + public static final double RECOMMENDED_MARGIN = 48.0; + + /** Deep navy masthead text. Mirrors the Panel CV's preset-local token. */ + private static final DocumentColor HEADER_TEXT = DocumentColor.rgb(20, 44, 66); + + /** Teal accent for header links. Mirrors the Panel CV's preset-local token. */ + private static final DocumentColor ACCENT = DocumentColor.rgb(0, 128, 128); + + /** Thin card stroke β€” single value keeps the card outline crisp. Mirrors the CV. */ + private static final double PANEL_STROKE_THICKNESS = 0.45; + + private PanelLetter() { + } + + /** + * Builds the letter with its Panel theme. + */ + public static DocumentTemplate create() { + return create(CvTheme.panel()); + } + + /** + * Builds the letter with a caller-supplied theme (share the paired + * CV's theme instance for a guaranteed visual match). + */ + public static DocumentTemplate create(CvTheme theme) { + Objects.requireNonNull(theme, "theme"); + return new Template(theme); + } + + private static final class Template implements DocumentTemplate { + + 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, CoverLetterDocument doc) { + Objects.requireNonNull(document, "document"); + Objects.requireNonNull(doc, "doc"); + + double innerWidth = document.canvas().innerWidth(); + double cardPadding = theme.spacing().bannerInnerPadding(); + double cardContentWidth = innerWidth - 2 * cardPadding; + + PageFlowBuilder flow = document.dsl() + .pageFlow() + .name("CoverLetterV2PanelRoot") + .spacing(theme.spacing().pageFlowSpacing()); + + addHeader(flow, doc.identity(), cardContentWidth); + + flow.addSection("CoverLetterV2PanelBody", host -> + LetterBody.render(host, doc, theme)); + + flow.build(); + } + + private void addHeader(PageFlowBuilder flow, CvIdentity identity, + double anchorWidth) { + CardWidget.render(flow, "CoverLetterV2PanelHeader", headerStyle(), + card -> { + widthAnchor(card, anchorWidth); + card.addParagraph(paragraph -> paragraph + .text(identity.name().full().toUpperCase(Locale.ROOT)) + .textStyle(nameStyle()) + .align(TextAlign.CENTER) + .margin(DocumentInsets.zero())); + if (!identity.jobTitle().isBlank()) { + card.addParagraph(paragraph -> paragraph + .text(identity.jobTitle()) + .textStyle(headerBodyStyle()) + .align(TextAlign.CENTER) + .margin(DocumentInsets.zero())); + } + String contact = TextOrnaments.joinPipe(identity.contact().address(), + identity.contact().phone()); + if (!contact.isBlank()) { + card.addParagraph(paragraph -> paragraph + .text(contact) + .textStyle(headerMetaStyle()) + .align(TextAlign.CENTER) + .margin(DocumentInsets.zero())); + } + addLinkRow(card, identity); + }); + } + + private void addLinkRow(SectionBuilder section, CvIdentity identity) { + boolean hasEmail = !identity.contact().email().isBlank(); + boolean hasLinks = !identity.links().isEmpty(); + if (!hasEmail && !hasLinks) { + return; + } + DocumentTextStyle bodyStyle = headerMetaStyle(); + DocumentTextStyle linkStyle = headerLinkStyle(); + section.addParagraph(paragraph -> paragraph + .textStyle(bodyStyle) + .align(TextAlign.CENTER) + .margin(DocumentInsets.zero()) + .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 void widthAnchor(SectionBuilder card, double width) { + card.spacer(width, 0.0); + } + + private CardWidget.Style headerStyle() { + return CardWidget.Style.builder() + .spacing(4) + .padding(DocumentInsets.of(theme.spacing().bannerInnerPadding())) + .fillColor(theme.palette().banner()) + .stroke(DocumentStroke.of(theme.palette().rule(), + PANEL_STROKE_THICKNESS)) + .cornerRadius(theme.spacing().bannerCornerRadius()) + .build(); + } + + private DocumentTextStyle nameStyle() { + return CvTextStyles.of(FontName.POPPINS, + theme.typography().sizeHeadline(), + DocumentTextDecoration.BOLD, HEADER_TEXT); + } + + private DocumentTextStyle headerBodyStyle() { + return CvTextStyles.of(theme.typography().bodyFont(), + theme.typography().sizeBody(), + DocumentTextDecoration.DEFAULT, theme.palette().ink()); + } + + private DocumentTextStyle headerMetaStyle() { + return CvTextStyles.of(theme.typography().bodyFont(), + theme.typography().sizeContact(), + DocumentTextDecoration.DEFAULT, theme.palette().ink()); + } + + private DocumentTextStyle headerLinkStyle() { + return CvTextStyles.of(theme.typography().bodyFont(), + theme.typography().sizeContact(), + DocumentTextDecoration.UNDERLINE, ACCENT); + } + } +} diff --git a/src/main/java/com/demcha/compose/document/templates/coverletter/v2/presets/SidebarPortraitLetter.java b/src/main/java/com/demcha/compose/document/templates/coverletter/v2/presets/SidebarPortraitLetter.java new file mode 100644 index 00000000..b7c2c05f --- /dev/null +++ b/src/main/java/com/demcha/compose/document/templates/coverletter/v2/presets/SidebarPortraitLetter.java @@ -0,0 +1,160 @@ +package com.demcha.compose.document.templates.coverletter.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.TextAlign; +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.coverletter.v2.components.LetterBody; +import com.demcha.compose.document.templates.coverletter.v2.data.CoverLetterDocument; +import com.demcha.compose.document.templates.cv.v2.components.CvTextStyles; +import com.demcha.compose.document.templates.cv.v2.components.TextOrnaments; +import com.demcha.compose.document.templates.cv.v2.data.CvIdentity; +import com.demcha.compose.document.templates.cv.v2.theme.CvTheme; +import com.demcha.compose.document.templates.cv.v2.widgets.ContactLine; + +import java.util.Objects; + +/** + * v2 cover-letter pair for the {@code SidebarPortrait} CV preset. + * + *

The CV's identity treatment is a beige "hero strip" carrying the + * centred serif name + spaced-caps role sub-line. The letter keeps that + * centred name treatment but drops the beige fill β€” a + * coloured box read as out of place on a single-column letter β€” leaving a + * clean centred letterhead, followed by a centred contact line and a + * single-column letter body via the shared {@link LetterBody}. The CV's + * circular portrait, icon contact stack, and pale sidebar column (painted + * via {@code pageBackgrounds}) are sidebar-only and are intentionally + * dropped for the single-column letter. Palette / typography come from + * {@link CvTheme#sidebarPortrait()}.

+ */ +public final class SidebarPortraitLetter { + + /** Stable template identifier. */ + public static final String ID = "sidebar-portrait-letter"; + + /** Human-readable display name. */ + public static final String DISPLAY_NAME = "Sidebar Portrait Letter"; + + /** Recommended page margin (in points) β€” generous business-letter feel. */ + public static final double RECOMMENDED_MARGIN = 48.0; + + private SidebarPortraitLetter() { + } + + /** + * Builds the letter with its Sidebar Portrait theme. + */ + public static DocumentTemplate create() { + return create(CvTheme.sidebarPortrait()); + } + + /** + * Builds the letter with a caller-supplied theme (share the paired + * CV's theme instance for a guaranteed visual match). + */ + public static DocumentTemplate create(CvTheme theme) { + Objects.requireNonNull(theme, "theme"); + return new Template(theme); + } + + private static final class Template implements DocumentTemplate { + + 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, CoverLetterDocument doc) { + Objects.requireNonNull(document, "document"); + Objects.requireNonNull(doc, "doc"); + + PageFlowBuilder flow = document.dsl() + .pageFlow() + .name("CoverLetterV2SidebarPortraitRoot") + .spacing(theme.spacing().pageFlowSpacing()); + + addHeroBand(flow, doc.identity()); + addContact(flow, doc.identity()); + + flow.addSection("CoverLetterV2SidebarPortraitBody", host -> + LetterBody.render(host, doc, theme)); + + flow.build(); + } + + private void addHeroBand(PageFlowBuilder flow, CvIdentity identity) { + String displayName = identity.name().full(); + String jobTitle = identity.jobTitle(); + String subline = jobTitle == null || jobTitle.isBlank() + ? "" + : TextOrnaments.spacedUpper(jobTitle); + flow.addSection("CoverLetterV2SidebarPortraitHero", hero -> { + // No fill: the CV's beige hero band reads as a coloured box + // on a single-column letter, which clashed with the concept, + // so the name treatment is kept but the background dropped. + hero.padding(new DocumentInsets(19, 34, 17, 34)) + .spacing(3) + .addParagraph(paragraph -> paragraph + .text(displayName) + .textStyle(nameStyle()) + .align(TextAlign.CENTER) + .lineSpacing(1.0) + .margin(DocumentInsets.zero())); + if (!subline.isBlank()) { + hero.addParagraph(paragraph -> paragraph + .text(subline) + .textStyle(subtitleStyle()) + .align(TextAlign.CENTER) + .margin(DocumentInsets.zero())); + } + }); + } + + private void addContact(PageFlowBuilder flow, CvIdentity identity) { + flow.addSection("CoverLetterV2SidebarPortraitContact", section -> + ContactLine.centered(section, identity, theme, + contactStyle(), contactLinkStyle(), contactStyle())); + } + + private DocumentTextStyle nameStyle() { + return CvTextStyles.of(theme.typography().headlineFont(), + theme.typography().sizeHeadline(), + DocumentTextDecoration.BOLD, theme.palette().ink()); + } + + private DocumentTextStyle subtitleStyle() { + return CvTextStyles.of(theme.typography().bodyFont(), + theme.typography().sizeEntryDate(), + DocumentTextDecoration.DEFAULT, theme.palette().ink()); + } + + private DocumentTextStyle contactStyle() { + return CvTextStyles.of(theme.typography().bodyFont(), + theme.typography().sizeContact(), + DocumentTextDecoration.DEFAULT, theme.palette().ink()); + } + + private DocumentTextStyle contactLinkStyle() { + return CvTextStyles.of(theme.typography().bodyFont(), + theme.typography().sizeContact(), + DocumentTextDecoration.UNDERLINE, theme.palette().muted()); + } + } +} diff --git a/src/main/java/com/demcha/compose/document/templates/coverletter/v2/presets/TimelineMinimalLetter.java b/src/main/java/com/demcha/compose/document/templates/coverletter/v2/presets/TimelineMinimalLetter.java new file mode 100644 index 00000000..f4341c2f --- /dev/null +++ b/src/main/java/com/demcha/compose/document/templates/coverletter/v2/presets/TimelineMinimalLetter.java @@ -0,0 +1,294 @@ +package com.demcha.compose.document.templates.coverletter.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.image.DocumentImageData; +import com.demcha.compose.document.node.DocumentLinkOptions; +import com.demcha.compose.document.node.InlineImageAlignment; +import com.demcha.compose.document.node.TextAlign; +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.coverletter.v2.components.LetterBody; +import com.demcha.compose.document.templates.coverletter.v2.data.CoverLetterDocument; +import com.demcha.compose.document.templates.cv.v2.components.CvTextStyles; +import com.demcha.compose.document.templates.cv.v2.components.SectionLookup; +import com.demcha.compose.document.templates.cv.v2.components.TextOrnaments; +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.theme.CvTheme; + +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; + +/** + * v2 cover-letter pair for the {@code TimelineMinimal} CV preset. + * + *

Reproduces the CV's masthead: a left spaced-caps Barlow-Condensed + * name + UPPERCASE role line, balanced by a right-aligned contact stack + * where each line ends with its PNG glyph icon (LinkedIn / GitHub / + * location / phone / email), all under a thin full-width rule β€” the same + * header as + * {@link com.demcha.compose.document.templates.cv.v2.presets.TimelineMinimal}. + * Below it, a single-column letter body via the shared {@link LetterBody}. + * Palette / typography come from {@link CvTheme#timelineMinimal()}; the + * CV's three-column timeline axis is a body element and is not part of + * the letter.

+ * + *

The contact icons reuse the CV's icon set + * ({@code /templates/cv/timeline-minimal/icons/}) and its text-glyph + * fallback, so no new assets are introduced.

+ */ +public final class TimelineMinimalLetter { + + /** Stable template identifier. */ + public static final String ID = "timeline-minimal-letter"; + + /** Human-readable display name. */ + public static final String DISPLAY_NAME = "Timeline Minimal Letter"; + + /** Recommended page margin (in points) β€” generous business-letter feel. */ + public static final double RECOMMENDED_MARGIN = 48.0; + + /** + * Letter body size. The Timeline Minimal CV theme uses a 7.8pt body + * tuned for its dense three-column layout β€” too small for a + * single-column letter, so the prose is rendered a touch larger here. + */ + private static final double LETTER_BODY_SIZE = 9.0; + + private static final double CONTACT_ICON_SIZE = 10.5; + private static final double CONTACT_ICON_BASELINE_OFFSET = -1.35; + private static final String CONTACT_ICON_ROOT = + "/templates/cv/timeline-minimal/icons/"; + private static final Map CONTACT_ICON_CACHE = + new ConcurrentHashMap<>(); + + private TimelineMinimalLetter() { + } + + /** + * Builds the letter with its Timeline Minimal theme. + */ + public static DocumentTemplate create() { + return create(CvTheme.timelineMinimal()); + } + + /** + * Builds the letter with a caller-supplied theme (share the paired + * CV's theme instance for a guaranteed visual match). + */ + public static DocumentTemplate create(CvTheme theme) { + Objects.requireNonNull(theme, "theme"); + return new Template(theme); + } + + private static final class Template implements DocumentTemplate { + + 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, CoverLetterDocument doc) { + Objects.requireNonNull(document, "document"); + Objects.requireNonNull(doc, "doc"); + + double width = document.canvas().innerWidth(); + PageFlowBuilder flow = document.dsl() + .pageFlow() + .name("CoverLetterV2TimelineMinimalRoot") + .spacing(theme.spacing().pageFlowSpacing()) + .addRow("CoverLetterV2TimelineMinimalHeader", row -> row + .spacing(3) + .weights(1.00, 0.61) + .addSection("CoverLetterV2TimelineMinimalName", + section -> addNameBlock(section, doc.identity())) + .addSection("CoverLetterV2TimelineMinimalContact", + section -> addContact(section, doc.identity()))) + .addLine(line -> line + .name("CoverLetterV2TimelineMinimalHeaderRule") + .horizontal(width) + .color(theme.palette().rule()) + .thickness(theme.spacing().accentRuleWidth()) + .margin(DocumentInsets.zero())); + + flow.addSection("CoverLetterV2TimelineMinimalBody", host -> + LetterBody.render(host, doc, theme, LETTER_BODY_SIZE)); + + flow.build(); + } + + private void addNameBlock(SectionBuilder section, CvIdentity identity) { + section.spacing(4) + .addParagraph(paragraph -> paragraph + .text(TextOrnaments.spacedUpper(identity.name().full())) + .textStyle(nameStyle()) + .margin(DocumentInsets.zero())); + String jobTitle = identity.jobTitle(); + if (!jobTitle.isBlank()) { + section.addParagraph(paragraph -> paragraph + .text(jobTitle.toUpperCase(Locale.ROOT)) + .textStyle(jobTitleStyle()) + .margin(DocumentInsets.zero())); + } + } + + private void addContact(SectionBuilder section, CvIdentity identity) { + section.spacing(3); + DocumentTextStyle textStyle = contactTextStyle(); + DocumentTextStyle fallbackIconStyle = fallbackIconStyle(); + for (ContactItem item : contactItems(identity)) { + section.addParagraph(paragraph -> paragraph + .textStyle(textStyle) + .align(TextAlign.RIGHT) + .link(item.linkOptions()) + .margin(DocumentInsets.zero()) + .rich(rich -> { + rich.style(item.text(), textStyle); + rich.plain(" "); + if (item.iconFile() != null) { + rich.image(contactIcon(item.iconFile()), + CONTACT_ICON_SIZE, + CONTACT_ICON_SIZE, + InlineImageAlignment.CENTER, + CONTACT_ICON_BASELINE_OFFSET, + item.linkOptions()); + } else { + rich.style(item.fallbackIcon(), fallbackIconStyle); + } + })); + } + } + + private List contactItems(CvIdentity identity) { + if (identity == null) { + return List.of(); + } + List items = new ArrayList<>(); + addContactItem(items, "LOC", "location.png", + identity.contact().address(), null); + addContactItem(items, "TEL", "phone.png", + identity.contact().phone(), null); + String email = identity.contact().email(); + if (!email.isBlank()) { + addContactItem(items, "@", "email.png", email, + new DocumentLinkOptions("mailto:" + email)); + } + for (CvLink link : identity.links()) { + String label = link.label(); + if (label.isBlank()) { + continue; + } + String url = link.url(); + addContactItem(items, pickFallbackIcon(label), + pickIconFile(label), label, + url.isBlank() ? null : new DocumentLinkOptions(url.trim())); + } + return List.copyOf(items); + } + + private DocumentImageData contactIcon(String iconFile) { + return DocumentImageData.fromBytes( + CONTACT_ICON_CACHE.computeIfAbsent(iconFile, + TimelineMinimalLetter::readIconBytes)); + } + + private DocumentTextStyle nameStyle() { + return CvTextStyles.of(theme.typography().headlineFont(), + theme.typography().sizeHeadline(), + DocumentTextDecoration.DEFAULT, theme.palette().ink()); + } + + private DocumentTextStyle jobTitleStyle() { + return CvTextStyles.of(theme.typography().headlineFont(), 9.5, + DocumentTextDecoration.BOLD, theme.palette().ink()); + } + + private DocumentTextStyle contactTextStyle() { + return CvTextStyles.of(theme.typography().bodyFont(), + theme.typography().sizeContact(), + DocumentTextDecoration.BOLD, theme.palette().muted()); + } + + private DocumentTextStyle fallbackIconStyle() { + return CvTextStyles.of(theme.typography().headlineFont(), 8.0, + DocumentTextDecoration.BOLD, theme.palette().muted()); + } + } + + private static void addContactItem(List items, + String fallbackIcon, String iconFile, + String text, DocumentLinkOptions linkOptions) { + if (text != null && !text.isBlank()) { + items.add(new ContactItem(fallbackIcon, iconFile, text, linkOptions)); + } + } + + private static String pickIconFile(String label) { + String normalized = SectionLookup.normalize(label); + if (normalized.contains("linkedin")) { + return "linkedin.png"; + } + if (normalized.contains("github")) { + return "github.png"; + } + if (normalized.contains("dribbble")) { + return "dribbble.png"; + } + if (normalized.contains("google")) { + return "google.png"; + } + return null; + } + + private static String pickFallbackIcon(String label) { + String normalized = SectionLookup.normalize(label); + if (normalized.contains("linkedin")) { + return "in"; + } + if (normalized.contains("github")) { + return "GH"; + } + return "@"; + } + + private static byte[] readIconBytes(String iconFile) { + try (InputStream input = TimelineMinimalLetter.class.getResourceAsStream( + CONTACT_ICON_ROOT + iconFile)) { + if (input == null) { + throw new IllegalStateException( + "Missing timeline minimal contact icon: " + iconFile); + } + return input.readAllBytes(); + } catch (IOException e) { + throw new UncheckedIOException( + "Failed to read timeline minimal contact icon: " + iconFile, e); + } + } + + private record ContactItem(String fallbackIcon, String iconFile, + String text, DocumentLinkOptions linkOptions) { + } +} diff --git a/src/main/java/com/demcha/compose/document/templates/coverletter/v2/presets/package-info.java b/src/main/java/com/demcha/compose/document/templates/coverletter/v2/presets/package-info.java new file mode 100644 index 00000000..2388b451 --- /dev/null +++ b/src/main/java/com/demcha/compose/document/templates/coverletter/v2/presets/package-info.java @@ -0,0 +1,11 @@ +/** + * Templates v2 cover-letter presets β€” one per paired CV preset. + * + *

Each preset is a thin orchestrator that reads colour, font, and + * spacing from its paired {@code CvTheme.()} (the single source + * of truth shared with the CV), renders the same masthead treatment as + * the CV, and delegates the letter body to the shared + * {@code coverletter.v2.components.LetterBody}. The result is a CV and a + * cover letter that read as one matched set.

+ */ +package com.demcha.compose.document.templates.coverletter.v2.presets; diff --git a/src/main/java/com/demcha/compose/document/templates/cv/builder/CvBuilder.java b/src/main/java/com/demcha/compose/document/templates/cv/builder/CvBuilder.java index 982ec4ab..d3ac5201 100644 --- a/src/main/java/com/demcha/compose/document/templates/cv/builder/CvBuilder.java +++ b/src/main/java/com/demcha/compose/document/templates/cv/builder/CvBuilder.java @@ -36,7 +36,15 @@ * must be configured before calling {@link #build()}; missing values * are rejected at build time with an explicit {@code NullPointerException} * naming the missing field.

+ * + * @deprecated Superseded by the layered …v2… surface (the current + * standard) β€” the layered model + * {@link com.demcha.compose.document.templates.cv.v2.data.CvDocument} + * plus the {@code cv.v2} presets. Kept for backward compatibility; + * scheduled for removal in a future major. See + * {@code docs/templates/v2-layered/}. */ +@Deprecated(since = "1.7.0", forRemoval = true) public final class CvBuilder { private String id; diff --git a/src/main/java/com/demcha/compose/document/templates/cv/builder/package-info.java b/src/main/java/com/demcha/compose/document/templates/cv/builder/package-info.java index 30d7e0ee..4a9fd87c 100644 --- a/src/main/java/com/demcha/compose/document/templates/cv/builder/package-info.java +++ b/src/main/java/com/demcha/compose/document/templates/cv/builder/package-info.java @@ -1,14 +1,29 @@ /** - * Templates v2 CV preset builders β€” fluent assembly of + * Superseded Gen-2 CV preset builder β€” fluent assembly of * {@link com.demcha.compose.document.templates.api.DocumentTemplate} * instances from layouts, components, and spec data. * + *

Deprecated surface. This is the older Gen-2 CV + * builder. It is not the current standard. The current standard is + * the layered surface + * {@code com.demcha.compose.document.templates.cv.v2} (data / theme / + * components / widgets / presets). This package is kept only for backward + * compatibility and is scheduled for removal in a future major.

+ * *

The single class of interest is * {@link com.demcha.compose.document.templates.cv.builder.CvBuilder}. * Preset classes wrap one builder call inside their - * {@code create(BusinessTheme)} factory; users wanting a custom preset - * copy that factory body and tweak the chain.

+ * {@code create(BusinessTheme)} factory.

+ * + *

New code should target the layered {@code cv.v2} surface instead. See + * {@code docs/templates/v2-layered/}.

* * @since 1.6.0 + * @deprecated Superseded by the layered + * {@code com.demcha.compose.document.templates.cv.v2} surface (the + * current standard). This Gen-2 package is kept for backward + * compatibility and will be removed in a future major. See + * {@code docs/templates/v2-layered/}. */ +@Deprecated(since = "1.7.0", forRemoval = true) package com.demcha.compose.document.templates.cv.builder; diff --git a/src/main/java/com/demcha/compose/document/templates/cv/layouts/CvLayout.java b/src/main/java/com/demcha/compose/document/templates/cv/layouts/CvLayout.java index ef72b2d1..f733627c 100644 --- a/src/main/java/com/demcha/compose/document/templates/cv/layouts/CvLayout.java +++ b/src/main/java/com/demcha/compose/document/templates/cv/layouts/CvLayout.java @@ -29,7 +29,15 @@ * construction time. Theming and spacing live with the components and * theme tokens; layouts only decide where blocks of pre-rendered * content sit on the page.

+ * + * @deprecated Superseded by the layered …v2… surface (the current + * standard) β€” the layered model + * {@link com.demcha.compose.document.templates.cv.v2.data.CvDocument} + * plus the {@code cv.v2} presets. Kept for backward compatibility; + * scheduled for removal in a future major. See + * {@code docs/templates/v2-layered/}. */ +@Deprecated(since = "1.7.0", forRemoval = true) public interface CvLayout { /** diff --git a/src/main/java/com/demcha/compose/document/templates/cv/layouts/SingleColumn.java b/src/main/java/com/demcha/compose/document/templates/cv/layouts/SingleColumn.java index 8fe40e04..27d992a1 100644 --- a/src/main/java/com/demcha/compose/document/templates/cv/layouts/SingleColumn.java +++ b/src/main/java/com/demcha/compose/document/templates/cv/layouts/SingleColumn.java @@ -18,7 +18,15 @@ * *

Used by presets such as Modern Professional and Classic Serif * where a tight, focused single-page layout is the goal.

+ * + * @deprecated Superseded by the layered …v2… surface (the current + * standard) β€” the layered model + * {@link com.demcha.compose.document.templates.cv.v2.data.CvDocument} + * plus the {@code cv.v2} presets. Kept for backward compatibility; + * scheduled for removal in a future major. See + * {@code docs/templates/v2-layered/}. */ +@Deprecated(since = "1.7.0", forRemoval = true) public final class SingleColumn implements CvLayout { /** Stable slot name that holds all module content. */ diff --git a/src/main/java/com/demcha/compose/document/templates/cv/layouts/ThreeColumnMagazine.java b/src/main/java/com/demcha/compose/document/templates/cv/layouts/ThreeColumnMagazine.java index d9b683a1..4a28b69f 100644 --- a/src/main/java/com/demcha/compose/document/templates/cv/layouts/ThreeColumnMagazine.java +++ b/src/main/java/com/demcha/compose/document/templates/cv/layouts/ThreeColumnMagazine.java @@ -25,7 +25,15 @@ *

Column weights default to equal thirds and are configurable via * {@link #weights(double, double, double)}. Inter-column gap and * inter-module gap are also tunable.

+ * + * @deprecated Superseded by the layered …v2… surface (the current + * standard) β€” the layered model + * {@link com.demcha.compose.document.templates.cv.v2.data.CvDocument} + * plus the {@code cv.v2} presets. Kept for backward compatibility; + * scheduled for removal in a future major. See + * {@code docs/templates/v2-layered/}. */ +@Deprecated(since = "1.7.0", forRemoval = true) public final class ThreeColumnMagazine implements CvLayout { /** Stable slot name for the first column. */ diff --git a/src/main/java/com/demcha/compose/document/templates/cv/layouts/TwoColumnSidebar.java b/src/main/java/com/demcha/compose/document/templates/cv/layouts/TwoColumnSidebar.java index e02d59cf..caf29711 100644 --- a/src/main/java/com/demcha/compose/document/templates/cv/layouts/TwoColumnSidebar.java +++ b/src/main/java/com/demcha/compose/document/templates/cv/layouts/TwoColumnSidebar.java @@ -25,7 +25,15 @@ * and are configurable via {@link #mainWeight(double)} / * {@link #sidebarWeight(double)}. Inter-column gap and inter-module * gap are also tunable.

+ * + * @deprecated Superseded by the layered …v2… surface (the current + * standard) β€” the layered model + * {@link com.demcha.compose.document.templates.cv.v2.data.CvDocument} + * plus the {@code cv.v2} presets. Kept for backward compatibility; + * scheduled for removal in a future major. See + * {@code docs/templates/v2-layered/}. */ +@Deprecated(since = "1.7.0", forRemoval = true) public final class TwoColumnSidebar implements CvLayout { /** Stable slot name for the primary (wider) content column. */ diff --git a/src/main/java/com/demcha/compose/document/templates/cv/layouts/package-info.java b/src/main/java/com/demcha/compose/document/templates/cv/layouts/package-info.java index a19b5fbe..a546def8 100644 --- a/src/main/java/com/demcha/compose/document/templates/cv/layouts/package-info.java +++ b/src/main/java/com/demcha/compose/document/templates/cv/layouts/package-info.java @@ -1,7 +1,14 @@ /** - * Templates v2 CV layouts β€” slot caркасы that arrange a header plus + * Superseded Gen-2 CV layouts β€” slot frames that arrange a header plus * named slot content into a final document tree. * + *

Deprecated surface. These are the older Gen-2 CV + * layouts. They are not the current standard. The current standard + * is the layered surface + * {@code com.demcha.compose.document.templates.cv.v2} (data / theme / + * components / widgets / presets). This package is kept only for backward + * compatibility and is scheduled for removal in a future major.

+ * *

Layouts are pure structural composers. They expose a fixed set of * named slots ({@code "main"}, {@code "sidebar"}, {@code "col-1"} etc.) * and a single composition seam that takes a pre-rendered header node @@ -19,10 +26,15 @@ * β€” col-1 / col-2 / col-3 slots, weighted row beneath the header. * * - *

Additional layouts ({@code HeroAndTwoColumn}, etc.) will land - * alongside the presets that need them in Phase E of the Templates v2 - * migration.

+ *

New code should target the layered {@code cv.v2} surface instead. See + * {@code docs/templates/v2-layered/}.

* * @since 1.6.0 + * @deprecated Superseded by the layered + * {@code com.demcha.compose.document.templates.cv.v2} surface (the + * current standard). This Gen-2 package is kept for backward + * compatibility and will be removed in a future major. See + * {@code docs/templates/v2-layered/}. */ +@Deprecated(since = "1.7.0", forRemoval = true) package com.demcha.compose.document.templates.cv.layouts; diff --git a/src/main/java/com/demcha/compose/document/templates/cv/package-info.java b/src/main/java/com/demcha/compose/document/templates/cv/package-info.java index 9031678d..b4a00e9d 100644 --- a/src/main/java/com/demcha/compose/document/templates/cv/package-info.java +++ b/src/main/java/com/demcha/compose/document/templates/cv/package-info.java @@ -1,15 +1,22 @@ /** - * Templates v2 CV domain β€” layouts, presets, builder, and spec data types. + * Superseded Gen-2 CV domain β€” slot-based layouts, presets, builder, and + * spec data types. * - *

This package is the home of all CV (rΓ©sumΓ©) templates in the v2 - * architecture. Sub-packages partition the domain by concern:

+ *

Deprecated surface. This package is the older Gen-2 + * CV (rΓ©sumΓ©) template stack. It is not the current standard. The + * current standard is the layered surface + * {@code com.demcha.compose.document.templates.cv.v2} (data / theme / + * components / widgets / presets). This package is kept only for backward + * compatibility and is scheduled for removal in a future major.

+ * + *

Sub-packages partition the (deprecated) domain by concern:

* *
    - *
  • {@code cv.layouts} β€” slot caркасы (single-column, two-column-sidebar, - * three-column-magazine, hero-and-two-column).
  • + *
  • {@code cv.layouts} β€” slot frames (single-column, two-column-sidebar, + * three-column-magazine).
  • *
  • {@code cv.presets} β€” flat copy-and-tweak preset classes * (ModernProfessional, NordicClean, ClassicSerif, CompactMono, - * Executive, EngineeringResume, Panel, Sidebar, MonogramSidebar, + * Executive, EngineeringResume, Panel, SidebarPortrait, MonogramSidebar, * TimelineMinimal, BoxedSections, CenteredHeadline, BlueBanner, * EditorialBlue).
  • *
  • {@code cv.builder} β€” {@code CvBuilder} for users composing @@ -18,10 +25,15 @@ * {@code CvModule}) describing the user's CV content.
  • *
* - *

Sub-packages will be populated during Phases B–E of the Templates v2 - * migration. Top-level marker file lives here to register the package - * with the build.

+ *

New code should target the layered {@code cv.v2} surface instead. See + * {@code docs/templates/v2-layered/}.

* * @since 1.6.0 + * @deprecated Superseded by the layered + * {@code com.demcha.compose.document.templates.cv.v2} surface (the + * current standard). This Gen-2 package is kept for backward + * compatibility and will be removed in a future major. See + * {@code docs/templates/v2-layered/}. */ +@Deprecated(since = "1.7.0", forRemoval = true) package com.demcha.compose.document.templates.cv; diff --git a/src/main/java/com/demcha/compose/document/templates/cv/presets/BlueBanner.java b/src/main/java/com/demcha/compose/document/templates/cv/presets/BlueBanner.java index 25b4ec96..da72f90b 100644 --- a/src/main/java/com/demcha/compose/document/templates/cv/presets/BlueBanner.java +++ b/src/main/java/com/demcha/compose/document/templates/cv/presets/BlueBanner.java @@ -46,7 +46,13 @@ * *

Inline markdown ({@code **bold**}, {@code *italic*}) is parsed * through the shared {@link MarkdownText} helper.

+ * + * @deprecated Superseded by the layered …v2… surface (the current + * standard). Kept for backward compatibility; scheduled for removal + * in a future major. See {@code docs/templates/v2-layered/} and + * {@link com.demcha.compose.document.templates.cv.v2.presets.BlueBanner}. */ +@Deprecated(since = "1.7.0", forRemoval = true) public final class BlueBanner { /** Stable template identifier. */ diff --git a/src/main/java/com/demcha/compose/document/templates/cv/presets/BoxedSections.java b/src/main/java/com/demcha/compose/document/templates/cv/presets/BoxedSections.java index a54c87ac..f86bb769 100644 --- a/src/main/java/com/demcha/compose/document/templates/cv/presets/BoxedSections.java +++ b/src/main/java/com/demcha/compose/document/templates/cv/presets/BoxedSections.java @@ -45,7 +45,13 @@ * description below. Visual signature ported from the legacy * {@code BoxedSectionsCvTemplateComposer}: PT Serif throughout, dark * grey ink, soft grey banner.

+ * + * @deprecated Superseded by the layered …v2… surface (the current + * standard). Kept for backward compatibility; scheduled for removal + * in a future major. See {@code docs/templates/v2-layered/} and + * {@link com.demcha.compose.document.templates.cv.v2.presets.BoxedSections}. */ +@Deprecated(since = "1.7.0", forRemoval = true) public final class BoxedSections { /** Stable template identifier. */ diff --git a/src/main/java/com/demcha/compose/document/templates/cv/presets/CenteredHeadline.java b/src/main/java/com/demcha/compose/document/templates/cv/presets/CenteredHeadline.java index e7b7ca2d..865b10bc 100644 --- a/src/main/java/com/demcha/compose/document/templates/cv/presets/CenteredHeadline.java +++ b/src/main/java/com/demcha/compose/document/templates/cv/presets/CenteredHeadline.java @@ -46,7 +46,13 @@ *

Inline markdown ({@code **bold**}, {@code *italic*}) is parsed * through the shared {@link MarkdownText} helper so spec authors can * carry inline emphasis in body text without preprocessing.

+ * + * @deprecated Superseded by the layered …v2… surface (the current + * standard). Kept for backward compatibility; scheduled for removal + * in a future major. See {@code docs/templates/v2-layered/} and + * {@link com.demcha.compose.document.templates.cv.v2.presets.CenteredHeadline}. */ +@Deprecated(since = "1.7.0", forRemoval = true) public final class CenteredHeadline { /** Stable template identifier. */ diff --git a/src/main/java/com/demcha/compose/document/templates/cv/presets/ClassicSerif.java b/src/main/java/com/demcha/compose/document/templates/cv/presets/ClassicSerif.java index e1868dc6..4416d534 100644 --- a/src/main/java/com/demcha/compose/document/templates/cv/presets/ClassicSerif.java +++ b/src/main/java/com/demcha/compose/document/templates/cv/presets/ClassicSerif.java @@ -47,7 +47,13 @@ * {@link com.demcha.compose.document.templates.cv.builder.CvBuilder} * abstraction exposes. To customise, copy this class and rewrite the * row / section calls.

+ * + * @deprecated Superseded by the layered …v2… surface (the current + * standard). Kept for backward compatibility; scheduled for removal + * in a future major. See {@code docs/templates/v2-layered/} and + * {@link com.demcha.compose.document.templates.cv.v2.presets.ClassicSerif}. */ +@Deprecated(since = "1.7.0", forRemoval = true) public final class ClassicSerif { /** Stable template identifier. */ diff --git a/src/main/java/com/demcha/compose/document/templates/cv/presets/CompactMono.java b/src/main/java/com/demcha/compose/document/templates/cv/presets/CompactMono.java index c29d9204..7810fd4a 100644 --- a/src/main/java/com/demcha/compose/document/templates/cv/presets/CompactMono.java +++ b/src/main/java/com/demcha/compose/document/templates/cv/presets/CompactMono.java @@ -46,7 +46,13 @@ * structure is richer than what the slot-based * {@link com.demcha.compose.document.templates.cv.builder.CvBuilder} * abstraction exposes.

+ * + * @deprecated Superseded by the layered …v2… surface (the current + * standard). Kept for backward compatibility; scheduled for removal + * in a future major. See {@code docs/templates/v2-layered/} and + * {@link com.demcha.compose.document.templates.cv.v2.presets.CompactMono}. */ +@Deprecated(since = "1.7.0", forRemoval = true) public final class CompactMono { /** Stable template identifier. */ diff --git a/src/main/java/com/demcha/compose/document/templates/cv/presets/EditorialBlue.java b/src/main/java/com/demcha/compose/document/templates/cv/presets/EditorialBlue.java index 3666fc88..858f7666 100644 --- a/src/main/java/com/demcha/compose/document/templates/cv/presets/EditorialBlue.java +++ b/src/main/java/com/demcha/compose/document/templates/cv/presets/EditorialBlue.java @@ -43,7 +43,13 @@ * Skills render as a four-column table; Education / Projects / * Employment History render structured entries with bold leading * titles, bold accent dates, and italic muted subtitles.

+ * + * @deprecated Superseded by the layered …v2… surface (the current + * standard). Kept for backward compatibility; scheduled for removal + * in a future major. See {@code docs/templates/v2-layered/} and + * {@link com.demcha.compose.document.templates.cv.v2.presets.EditorialBlue}. */ +@Deprecated(since = "1.7.0", forRemoval = true) public final class EditorialBlue { /** Stable template identifier. */ diff --git a/src/main/java/com/demcha/compose/document/templates/cv/presets/EngineeringResume.java b/src/main/java/com/demcha/compose/document/templates/cv/presets/EngineeringResume.java index 85409b27..6ee658c1 100644 --- a/src/main/java/com/demcha/compose/document/templates/cv/presets/EngineeringResume.java +++ b/src/main/java/com/demcha/compose/document/templates/cv/presets/EngineeringResume.java @@ -39,7 +39,13 @@ * Leadership Experience plus Technical Evidence on the right. Visual * signature ported from the legacy {@code TechLeadCvTemplateComposer}: * Barlow headings, Lato body, navy primary, green accent.

+ * + * @deprecated Superseded by the layered …v2… surface (the current + * standard). Kept for backward compatibility; scheduled for removal + * in a future major. See {@code docs/templates/v2-layered/} and + * {@link com.demcha.compose.document.templates.cv.v2.presets.EngineeringResume}. */ +@Deprecated(since = "1.7.0", forRemoval = true) public final class EngineeringResume { /** Stable template identifier. */ diff --git a/src/main/java/com/demcha/compose/document/templates/cv/presets/Executive.java b/src/main/java/com/demcha/compose/document/templates/cv/presets/Executive.java index 9772f811..7352757e 100644 --- a/src/main/java/com/demcha/compose/document/templates/cv/presets/Executive.java +++ b/src/main/java/com/demcha/compose/document/templates/cv/presets/Executive.java @@ -37,7 +37,13 @@ * over a single-column body. Visual signature ported from the legacy * {@code ExecutiveSlateCvTemplate}: Poppins for headings, Lato for * body, slate primary, bronze accent.

+ * + * @deprecated Superseded by the layered …v2… surface (the current + * standard). Kept for backward compatibility; scheduled for removal + * in a future major. See {@code docs/templates/v2-layered/} and + * {@link com.demcha.compose.document.templates.cv.v2.presets.Executive}. */ +@Deprecated(since = "1.7.0", forRemoval = true) public final class Executive { /** Stable template identifier. */ diff --git a/src/main/java/com/demcha/compose/document/templates/cv/presets/ModernProfessional.java b/src/main/java/com/demcha/compose/document/templates/cv/presets/ModernProfessional.java index 91e01085..efed7089 100644 --- a/src/main/java/com/demcha/compose/document/templates/cv/presets/ModernProfessional.java +++ b/src/main/java/com/demcha/compose/document/templates/cv/presets/ModernProfessional.java @@ -35,7 +35,13 @@ * {@link CvSpec} must declare modules with these names; alternative * orderings are achieved by copying the preset and changing the * {@code .place(...)} calls.

+ * + * @deprecated Superseded by the layered …v2… surface (the current + * standard). Kept for backward compatibility; scheduled for removal + * in a future major. See {@code docs/templates/v2-layered/} and + * {@link com.demcha.compose.document.templates.cv.v2.presets.ModernProfessional}. */ +@Deprecated(since = "1.7.0", forRemoval = true) public final class ModernProfessional { /** Stable template identifier. */ diff --git a/src/main/java/com/demcha/compose/document/templates/cv/presets/MonogramSidebar.java b/src/main/java/com/demcha/compose/document/templates/cv/presets/MonogramSidebar.java index de330c28..e946da75 100644 --- a/src/main/java/com/demcha/compose/document/templates/cv/presets/MonogramSidebar.java +++ b/src/main/java/com/demcha/compose/document/templates/cv/presets/MonogramSidebar.java @@ -55,7 +55,13 @@ * Visual signature ported from the legacy * {@code MonogramSidebarCvTemplateComposer}: Crimson Text headline, * PT Serif monogram, muted gold accent.

+ * + * @deprecated Superseded by the layered …v2… surface (the current + * standard). Kept for backward compatibility; scheduled for removal + * in a future major. See {@code docs/templates/v2-layered/} and + * {@link com.demcha.compose.document.templates.cv.v2.presets.MonogramSidebar}. */ +@Deprecated(since = "1.7.0", forRemoval = true) public final class MonogramSidebar { /** Stable template identifier. */ diff --git a/src/main/java/com/demcha/compose/document/templates/cv/presets/NordicClean.java b/src/main/java/com/demcha/compose/document/templates/cv/presets/NordicClean.java index 7f05ec61..41384833 100644 --- a/src/main/java/com/demcha/compose/document/templates/cv/presets/NordicClean.java +++ b/src/main/java/com/demcha/compose/document/templates/cv/presets/NordicClean.java @@ -54,7 +54,13 @@ * {@code "additional"}, {@code "experience"}, {@code "projects"}) so * naming variants like "Professional Summary" / "Profile" or * "Technical Skills" / "Skills" all work without configuration.

+ * + * @deprecated Superseded by the layered …v2… surface (the current + * standard). Kept for backward compatibility; scheduled for removal + * in a future major. See {@code docs/templates/v2-layered/} and + * {@link com.demcha.compose.document.templates.cv.v2.presets.NordicClean}. */ +@Deprecated(since = "1.7.0", forRemoval = true) public final class NordicClean { /** Stable template identifier. */ diff --git a/src/main/java/com/demcha/compose/document/templates/cv/presets/Panel.java b/src/main/java/com/demcha/compose/document/templates/cv/presets/Panel.java index 8e1cfd17..f167a8cd 100644 --- a/src/main/java/com/demcha/compose/document/templates/cv/presets/Panel.java +++ b/src/main/java/com/demcha/compose/document/templates/cv/presets/Panel.java @@ -46,7 +46,13 @@ * Additional panel closes the document. Visual signature ported from * {@code PanelCvTemplateComposer.Layout.stacked}: Poppins headlines, * Lato body, deep slate ink, teal accent.

+ * + * @deprecated Superseded by the layered …v2… surface (the current + * standard). Kept for backward compatibility; scheduled for removal + * in a future major. See {@code docs/templates/v2-layered/} and + * {@link com.demcha.compose.document.templates.cv.v2.presets.Panel}. */ +@Deprecated(since = "1.7.0", forRemoval = true) public final class Panel { /** Stable template identifier. */ diff --git a/src/main/java/com/demcha/compose/document/templates/cv/presets/SidebarPortrait.java b/src/main/java/com/demcha/compose/document/templates/cv/presets/SidebarPortrait.java index 97ae0345..44509a90 100644 --- a/src/main/java/com/demcha/compose/document/templates/cv/presets/SidebarPortrait.java +++ b/src/main/java/com/demcha/compose/document/templates/cv/presets/SidebarPortrait.java @@ -49,7 +49,13 @@ * ported from the legacy {@code SidebarPortraitCvTemplateComposer}: * Crimson Text serif for the hero name, Lato body, restrained grey * palette.

+ * + * @deprecated Superseded by the layered …v2… surface (the current + * standard). Kept for backward compatibility; scheduled for removal + * in a future major. See {@code docs/templates/v2-layered/} and + * {@link com.demcha.compose.document.templates.cv.v2.presets.SidebarPortrait}. */ +@Deprecated(since = "1.7.0", forRemoval = true) public final class SidebarPortrait { /** Stable template identifier. */ diff --git a/src/main/java/com/demcha/compose/document/templates/cv/presets/TimelineMinimal.java b/src/main/java/com/demcha/compose/document/templates/cv/presets/TimelineMinimal.java index d714efcf..6c6183b8 100644 --- a/src/main/java/com/demcha/compose/document/templates/cv/presets/TimelineMinimal.java +++ b/src/main/java/com/demcha/compose/document/templates/cv/presets/TimelineMinimal.java @@ -47,7 +47,13 @@ * {@code TimelineMinimalCvTemplateComposer}: spaced caps name in * Barlow Condensed, contact stack with PNG icons, all-grey palette, * three timeline dots.

+ * + * @deprecated Superseded by the layered …v2… surface (the current + * standard). Kept for backward compatibility; scheduled for removal + * in a future major. See {@code docs/templates/v2-layered/} and + * {@link com.demcha.compose.document.templates.cv.v2.presets.TimelineMinimal}. */ +@Deprecated(since = "1.7.0", forRemoval = true) public final class TimelineMinimal { /** Stable template identifier. */ diff --git a/src/main/java/com/demcha/compose/document/templates/cv/presets/package-info.java b/src/main/java/com/demcha/compose/document/templates/cv/presets/package-info.java index 297821b1..daf4b0b9 100644 --- a/src/main/java/com/demcha/compose/document/templates/cv/presets/package-info.java +++ b/src/main/java/com/demcha/compose/document/templates/cv/presets/package-info.java @@ -1,24 +1,28 @@ /** - * Templates v2 CV presets β€” flat copy-and-tweak recipe classes. + * Superseded Gen-2 CV presets β€” flat copy-and-tweak recipe classes. + * + *

Deprecated surface. These are the older Gen-2 CV + * presets. They are not the current standard. The current standard + * is the layered surface + * {@code com.demcha.compose.document.templates.cv.v2.presets}. This package + * is kept only for backward compatibility and is scheduled for removal in a + * future major.

* *

Each preset is a small final class with one static * {@code create(BusinessTheme)} factory method whose body fully * configures a {@link com.demcha.compose.document.templates.cv.builder.CvBuilder} * to produce a ready-to-use - * {@link com.demcha.compose.document.templates.api.DocumentTemplate}. - * No inheritance, no abstract base β€” every visual choice is visible - * in the preset's source.

- * - *

To customise a preset: copy the {@code create(...)} method body - * into your own class and adjust the {@code CvBuilder} calls (slot - * placements, module style, spacing tokens, layout choice). The - * surrounding session lifecycle is unchanged.

+ * {@link com.demcha.compose.document.templates.api.DocumentTemplate}.

* - *

Phase D ships the pilot preset - * {@link com.demcha.compose.document.templates.cv.presets.ModernProfessional}; - * the remaining 13 CV presets land in Phase E of the Templates v2 - * migration.

+ *

New code should target the layered {@code cv.v2} presets instead. See + * {@code docs/templates/v2-layered/}.

* * @since 1.6.0 + * @deprecated Superseded by the layered + * {@code com.demcha.compose.document.templates.cv.v2} surface (the + * current standard). This Gen-2 package is kept for backward + * compatibility and will be removed in a future major. See + * {@code docs/templates/v2-layered/}. */ +@Deprecated(since = "1.7.0", forRemoval = true) package com.demcha.compose.document.templates.cv.presets; diff --git a/src/main/java/com/demcha/compose/document/templates/cv/spec/CvHeader.java b/src/main/java/com/demcha/compose/document/templates/cv/spec/CvHeader.java index a1eefc72..77934c2d 100644 --- a/src/main/java/com/demcha/compose/document/templates/cv/spec/CvHeader.java +++ b/src/main/java/com/demcha/compose/document/templates/cv/spec/CvHeader.java @@ -22,7 +22,14 @@ * @param email optional email address; empty string when absent * @param links ordered list of {@link Link} entries (typically * LinkedIn, GitHub); never null, may be empty + * @deprecated Superseded by the layered …v2… surface (the current + * standard) β€” the layered model + * {@link com.demcha.compose.document.templates.cv.v2.data.CvDocument} + * plus the {@code cv.v2} presets. Kept for backward compatibility; + * scheduled for removal in a future major. See + * {@code docs/templates/v2-layered/}. */ +@Deprecated(since = "1.7.0", forRemoval = true) public record CvHeader( String name, String jobTitle, diff --git a/src/main/java/com/demcha/compose/document/templates/cv/spec/CvModule.java b/src/main/java/com/demcha/compose/document/templates/cv/spec/CvModule.java index 013fdb06..913c041e 100644 --- a/src/main/java/com/demcha/compose/document/templates/cv/spec/CvModule.java +++ b/src/main/java/com/demcha/compose/document/templates/cv/spec/CvModule.java @@ -20,7 +20,14 @@ * @param title heading text rendered above the body (may be empty to * suppress the heading row) * @param body body content block (must not be null) + * @deprecated Superseded by the layered …v2… surface (the current + * standard) β€” the layered model + * {@link com.demcha.compose.document.templates.cv.v2.data.CvDocument} + * plus the {@code cv.v2} presets. Kept for backward compatibility; + * scheduled for removal in a future major. See + * {@code docs/templates/v2-layered/}. */ +@Deprecated(since = "1.7.0", forRemoval = true) public record CvModule(String name, String title, Block body) { /** diff --git a/src/main/java/com/demcha/compose/document/templates/cv/spec/CvSpec.java b/src/main/java/com/demcha/compose/document/templates/cv/spec/CvSpec.java index 68392b2d..dff8f212 100644 --- a/src/main/java/com/demcha/compose/document/templates/cv/spec/CvSpec.java +++ b/src/main/java/com/demcha/compose/document/templates/cv/spec/CvSpec.java @@ -17,7 +17,14 @@ * @param header identity block (required) * @param modules ordered list of named modules; insertion order * preserved + * @deprecated Superseded by the layered …v2… surface (the current + * standard) β€” the layered model + * {@link com.demcha.compose.document.templates.cv.v2.data.CvDocument} + * plus the {@code cv.v2} presets. Kept for backward compatibility; + * scheduled for removal in a future major. See + * {@code docs/templates/v2-layered/}. */ +@Deprecated(since = "1.7.0", forRemoval = true) public record CvSpec(CvHeader header, List modules) { /** diff --git a/src/main/java/com/demcha/compose/document/templates/cv/spec/package-info.java b/src/main/java/com/demcha/compose/document/templates/cv/spec/package-info.java index 179a52de..ecc170bb 100644 --- a/src/main/java/com/demcha/compose/document/templates/cv/spec/package-info.java +++ b/src/main/java/com/demcha/compose/document/templates/cv/spec/package-info.java @@ -1,8 +1,16 @@ /** - * Templates v2 CV specification records β€” user-facing data types. + * Superseded Gen-2 CV specification records β€” user-facing data types. + * + *

Deprecated surface. These are the older Gen-2 CV spec + * records. They are not the current standard. The current standard + * is the layered model + * {@link com.demcha.compose.document.templates.cv.v2.data.CvDocument} in the + * {@code com.demcha.compose.document.templates.cv.v2} surface. This package + * is kept only for backward compatibility and is scheduled for removal in a + * future major.

* *

This package holds the immutable records a user fills with their - * CV content before passing the spec to a preset for rendering:

+ * CV content before passing the spec to a Gen-2 preset for rendering:

* *
    *
  • {@link com.demcha.compose.document.templates.cv.spec.CvHeader} @@ -16,11 +24,15 @@ * lookups.
  • *
* - *

This is the v2 replacement for the legacy - * {@code com.demcha.compose.document.templates.data.cv.*} mutable - * Lombok beans. The legacy package remains during the migration - * window and will be removed in Phase G.

+ *

New code should target the layered {@code cv.v2} data model instead. See + * {@code docs/templates/v2-layered/}.

* * @since 1.6.0 + * @deprecated Superseded by the layered + * {@code com.demcha.compose.document.templates.cv.v2} surface (the + * current standard). This Gen-2 package is kept for backward + * compatibility and will be removed in a future major. See + * {@code docs/templates/v2-layered/}. */ +@Deprecated(since = "1.7.0", forRemoval = true) package com.demcha.compose.document.templates.cv.spec; diff --git a/src/main/java/com/demcha/compose/document/templates/cv/v2/components/TextOrnaments.java b/src/main/java/com/demcha/compose/document/templates/cv/v2/components/TextOrnaments.java index f8c01028..14af837c 100644 --- a/src/main/java/com/demcha/compose/document/templates/cv/v2/components/TextOrnaments.java +++ b/src/main/java/com/demcha/compose/document/templates/cv/v2/components/TextOrnaments.java @@ -43,4 +43,27 @@ public static String spacedUpper(String value) { } return out.toString(); } + + /** + * Joins the non-blank parts with a {@code " | "} pipe separator + * (e.g. {@code joinPipe("London", "", "+44") -> "London | +44"}). + * Null / blank parts are skipped; each kept part is trimmed. Used to + * build single-line contact/meta strings in headers. + * + * @param parts ordered parts (null / blank entries ignored) + * @return pipe-joined string, empty when no non-blank parts + */ + public 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(); + } } diff --git a/src/main/java/com/demcha/compose/document/templates/cv/v2/package-info.java b/src/main/java/com/demcha/compose/document/templates/cv/v2/package-info.java index 5d33e9cf..caa65510 100644 --- a/src/main/java/com/demcha/compose/document/templates/cv/v2/package-info.java +++ b/src/main/java/com/demcha/compose/document/templates/cv/v2/package-info.java @@ -205,9 +205,11 @@ *
  • A free-form layout engine. The engine lives in * {@code com.demcha.compose.document.engine.*}; this package * is a thin author-facing layer on top of its public DSL.
  • - *
  • A replacement for v1 yet. The legacy - * {@code com.demcha.compose.document.templates.cv.*} surface is - * untouched; both pipelines coexist while v2 stabilises.
  • * + * + *

    This layered surface is the current standard for CV documents. The + * older Gen-2 stack at + * {@code com.demcha.compose.document.templates.cv.*} is deprecated and + * kept only for backward compatibility.

    */ package com.demcha.compose.document.templates.cv.v2; diff --git a/src/main/java/com/demcha/compose/document/templates/data/coverletter/CoverLetterDocumentSpec.java b/src/main/java/com/demcha/compose/document/templates/data/coverletter/CoverLetterDocumentSpec.java index fa19f033..a7ae0149 100644 --- a/src/main/java/com/demcha/compose/document/templates/data/coverletter/CoverLetterDocumentSpec.java +++ b/src/main/java/com/demcha/compose/document/templates/data/coverletter/CoverLetterDocumentSpec.java @@ -15,7 +15,13 @@ * @param body cover-letter body text * @param jobDetails target role metadata used by templates * @author Artem Demchyshyn + * @deprecated Test-only dead code; the live CV/cover-letter model is + * {@code cv.v2} / {@code coverletter.v2}. Kept for backward + * compatibility; scheduled for removal in a future major. See + * {@code docs/templates/v2-layered/} and + * {@link com.demcha.compose.document.templates.coverletter.v2.data.CoverLetterDocument}. */ +@Deprecated(since = "1.7.0", forRemoval = true) public record CoverLetterDocumentSpec( Header header, String body, diff --git a/src/main/java/com/demcha/compose/document/templates/data/cv/CvDocumentSpec.java b/src/main/java/com/demcha/compose/document/templates/data/cv/CvDocumentSpec.java index 4b525993..08e5217f 100644 --- a/src/main/java/com/demcha/compose/document/templates/data/cv/CvDocumentSpec.java +++ b/src/main/java/com/demcha/compose/document/templates/data/cv/CvDocumentSpec.java @@ -19,7 +19,13 @@ * @param header optional header block rendered at the top of the document * @param modules ordered content modules rendered after the header * @author Artem Demchyshyn + * @deprecated Test-only dead code; the live CV/cover-letter model is + * {@code cv.v2} / {@code coverletter.v2}. Kept for backward + * compatibility; scheduled for removal in a future major. See + * {@code docs/templates/v2-layered/} and + * {@link com.demcha.compose.document.templates.cv.v2.data.CvDocument}. */ +@Deprecated(since = "1.7.0", forRemoval = true) public record CvDocumentSpec( Header header, List modules diff --git a/src/test/java/com/demcha/compose/document/templates/coverletter/v2/presets/CoverLetterV2SmokeTest.java b/src/test/java/com/demcha/compose/document/templates/coverletter/v2/presets/CoverLetterV2SmokeTest.java new file mode 100644 index 00000000..85452b24 --- /dev/null +++ b/src/test/java/com/demcha/compose/document/templates/coverletter/v2/presets/CoverLetterV2SmokeTest.java @@ -0,0 +1,100 @@ +package com.demcha.compose.document.templates.coverletter.v2.presets; + +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.coverletter.v2.data.CoverLetterDocument; +import com.demcha.compose.document.templates.cv.v2.data.CvIdentity; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.function.Supplier; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Smoke test for every v2 cover-letter preset β€” one parametrized check + * instead of a near-identical file per preset, because all letters + * share the same {@code LetterBody} shape and differ only in their + * (already pixel-gated) masthead. + * + *

    Asserts each preset's stable {@code id()} / {@code displayName()} + * and that it composes a non-empty document. Pixel fidelity of each + * masthead is covered separately by {@link CoverLetterV2VisualParityTest}.

    + */ +class CoverLetterV2SmokeTest { + + @ParameterizedTest(name = "{1}") + @MethodSource("presets") + void exposes_stable_identity_and_renders( + Supplier> factory, + String id, + String displayName) throws Exception { + DocumentTemplate template = factory.get(); + assertThat(template.id()).isEqualTo(id); + assertThat(template.displayName()).isEqualTo(displayName); + + try (DocumentSession session = GraphCompose.document() + .pageSize(DocumentPageSize.A4) + .margin(48, 48, 48, 48) + .create()) { + template.compose(session, sampleDocument()); + assertThat(session.roots()).isNotEmpty(); + } + } + + private static Stream presets() { + return Stream.of( + Arguments.of((Supplier>) ExecutiveLetter::create, + "executive-letter", "Executive Letter"), + Arguments.of((Supplier>) ModernProfessionalLetter::create, + "modern-professional-letter", "Modern Professional Letter"), + Arguments.of((Supplier>) BoxedSectionsLetter::create, + "boxed-sections-letter", "Boxed Sections Letter"), + Arguments.of((Supplier>) ClassicSerifLetter::create, + "classic-serif-letter", "Classic Serif Letter"), + Arguments.of((Supplier>) EditorialBlueLetter::create, + "editorial-blue-letter", "Editorial Blue Letter"), + Arguments.of((Supplier>) CenteredHeadlineLetter::create, + "centered-headline-letter", "Centered Headline Letter"), + Arguments.of((Supplier>) BlueBannerLetter::create, + "blue-banner-letter", "Blue Banner Letter"), + Arguments.of((Supplier>) EngineeringResumeLetter::create, + "engineering-resume-letter", "Engineering Resume Letter"), + Arguments.of((Supplier>) PanelLetter::create, + "panel-letter", "Panel Letter"), + Arguments.of((Supplier>) CompactMonoLetter::create, + "compact-mono-letter", "Compact Mono Letter"), + Arguments.of((Supplier>) NordicCleanLetter::create, + "nordic-clean-letter", "Nordic Clean Letter"), + Arguments.of((Supplier>) SidebarPortraitLetter::create, + "sidebar-portrait-letter", "Sidebar Portrait Letter"), + Arguments.of((Supplier>) MonogramSidebarLetter::create, + "monogram-sidebar-letter", "Monogram Sidebar Letter"), + Arguments.of((Supplier>) TimelineMinimalLetter::create, + "timeline-minimal-letter", "Timeline Minimal Letter")); + } + + private static CoverLetterDocument sampleDocument() { + return CoverLetterDocument.builder() + .identity(CvIdentity.builder() + .name("Jordan", "Rivera") + .jobTitle("Platform Engineer") + .contact("+44 20 5555 1000", "jordan.rivera@example.com", + "London, UK") + .link("LinkedIn", "https://linkedin.com/in/jordan-rivera-demo") + .link("GitHub", "https://github.com/jrivera-demo") + .build()) + .greeting("Dear Hiring Team at **Northwind Systems**,") + .paragraph("I am excited to share my interest in the Senior " + + "Platform Engineer role, building **reusable " + + "document-generation systems**.") + .paragraph("I enjoy turning fuzzy requirements into clear template " + + "abstractions and reliable test coverage.") + .closing("Sincerely,") + .build(); + } +} diff --git a/src/test/java/com/demcha/compose/document/templates/coverletter/v2/presets/CoverLetterV2VisualParityTest.java b/src/test/java/com/demcha/compose/document/templates/coverletter/v2/presets/CoverLetterV2VisualParityTest.java new file mode 100644 index 00000000..bb796752 --- /dev/null +++ b/src/test/java/com/demcha/compose/document/templates/coverletter/v2/presets/CoverLetterV2VisualParityTest.java @@ -0,0 +1,156 @@ +package com.demcha.compose.document.templates.coverletter.v2.presets; + +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.coverletter.v2.data.CoverLetterDocument; +import com.demcha.compose.document.templates.cv.v2.data.CvIdentity; +import com.demcha.testing.visual.PdfVisualRegression; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.nio.file.Path; +import java.util.function.Supplier; +import java.util.stream.Stream; + +/** + * Pixel-diff visual parity gate for the v2 layered cover-letter + * presets β€” the letter sibling of {@code CvV2VisualParityTest}. + * + *

    Each preset renders the same canonical {@link CoverLetterDocument} + * on full A4 with the preset's {@code RECOMMENDED_MARGIN}; the + * resulting PDF is rasterised page-by-page and compared per-pixel + * against a checked-in baseline PNG. Failures write the actual render + + * diff image next to the baseline.

    + * + *

    Re-blessing baselines β€” after a deliberate + * visual change, re-run with + * {@code -Dgraphcompose.visual.approve=true} (or environment variable + * {@code GRAPHCOMPOSE_VISUAL_APPROVE=true}) to overwrite the baselines + * with the current rendering. Commit the updated PNGs as part of the + * same change.

    + * + *

    Baselines live under + * {@code src/test/resources/visual-baselines/coverletter-v2-layered/}. + * Budget mirrors {@code CvV2VisualParityTest} (50 000 mismatched + * pixels at per-channel tolerance 8) β€” sized for the worst-case + * Helvetica cross-platform drift between Windows-recorded baselines + * and Linux CI.

    + */ +class CoverLetterV2VisualParityTest { + + private static final Path BASELINE_ROOT = Path.of( + "src", "test", "resources", "visual-baselines", "coverletter-v2-layered"); + + private static final long PIXEL_DIFF_BUDGET = 50_000L; + private static final int PER_PIXEL_TOLERANCE = 8; + + @ParameterizedTest(name = "{0}") + @MethodSource("presets") + void rendersWithinPixelDiffBudget(String slug, + double margin, + Supplier> factory) + throws Exception { + DocumentTemplate template = factory.get(); + float m = (float) margin; + byte[] pdfBytes; + try (DocumentSession document = GraphCompose.document() + .pageSize(DocumentPageSize.A4) + .margin(m, m, m, m) + .create()) { + template.compose(document, canonicalLetter()); + pdfBytes = document.toPdfBytes(); + } + + PdfVisualRegression.standard() + .baselineRoot(BASELINE_ROOT) + .perPixelTolerance(PER_PIXEL_TOLERANCE) + .mismatchedPixelBudget(PIXEL_DIFF_BUDGET) + .assertMatchesBaseline(slug, pdfBytes); + } + + private static Stream presets() { + return Stream.of( + Arguments.of("executive", + ExecutiveLetter.RECOMMENDED_MARGIN, + (Supplier>) ExecutiveLetter::create), + Arguments.of("modern_professional", + ModernProfessionalLetter.RECOMMENDED_MARGIN, + (Supplier>) ModernProfessionalLetter::create), + Arguments.of("boxed_sections", + BoxedSectionsLetter.RECOMMENDED_MARGIN, + (Supplier>) BoxedSectionsLetter::create), + Arguments.of("classic_serif", + ClassicSerifLetter.RECOMMENDED_MARGIN, + (Supplier>) ClassicSerifLetter::create), + Arguments.of("editorial_blue", + EditorialBlueLetter.RECOMMENDED_MARGIN, + (Supplier>) EditorialBlueLetter::create), + Arguments.of("centered_headline", + CenteredHeadlineLetter.RECOMMENDED_MARGIN, + (Supplier>) CenteredHeadlineLetter::create), + Arguments.of("blue_banner", + BlueBannerLetter.RECOMMENDED_MARGIN, + (Supplier>) BlueBannerLetter::create), + Arguments.of("engineering_resume", + EngineeringResumeLetter.RECOMMENDED_MARGIN, + (Supplier>) EngineeringResumeLetter::create), + Arguments.of("panel", + PanelLetter.RECOMMENDED_MARGIN, + (Supplier>) PanelLetter::create), + Arguments.of("compact_mono", + CompactMonoLetter.RECOMMENDED_MARGIN, + (Supplier>) CompactMonoLetter::create), + Arguments.of("nordic_clean", + NordicCleanLetter.RECOMMENDED_MARGIN, + (Supplier>) NordicCleanLetter::create), + Arguments.of("sidebar_portrait", + SidebarPortraitLetter.RECOMMENDED_MARGIN, + (Supplier>) SidebarPortraitLetter::create), + Arguments.of("monogram_sidebar", + MonogramSidebarLetter.RECOMMENDED_MARGIN, + (Supplier>) MonogramSidebarLetter::create), + Arguments.of("timeline_minimal", + TimelineMinimalLetter.RECOMMENDED_MARGIN, + (Supplier>) TimelineMinimalLetter::create)); + } + + /** + * Canonical sample letter β€” the same Jordan Rivera identity as + * {@code CvV2VisualParityTest} so the letter masthead is verified + * against the same content the CV gate uses, plus a greeting, three + * body paragraphs with inline markdown, and a closing. + * + *

    Kept inline (not pulled from the examples module) so the test + * depends only on main + main-test code.

    + */ + private static CoverLetterDocument canonicalLetter() { + return CoverLetterDocument.builder() + .identity(CvIdentity.builder() + .name("Jordan", "Rivera") + .jobTitle("Platform Engineer") + .contact("+44 20 5555 1000", + "jordan.rivera@example.com", + "London, UK") + .link("LinkedIn", "https://linkedin.com/in/jordan-rivera-demo") + .link("GitHub", "https://github.com/jrivera-demo") + .build()) + .greeting("Dear Hiring Team at **Northwind Systems**,") + .paragraph("I am excited to share my interest in the Senior " + + "Platform Engineer role. My recent work has focused " + + "on building **reusable document-generation systems** " + + "that balance public API design, render quality, and " + + "maintainability.") + .paragraph("I enjoy translating fuzzy workflow requirements into " + + "clear template abstractions, reliable test coverage, " + + "and examples that make adoption easier for the rest " + + "of the team.") + .paragraph("I would welcome the opportunity to bring that same " + + "mix of engineering rigor and product thinking to your " + + "platform group.") + .closing("Sincerely,") + .build(); + } +} diff --git a/src/test/resources/visual-baselines/coverletter-v2-layered/blue_banner-page-0.png b/src/test/resources/visual-baselines/coverletter-v2-layered/blue_banner-page-0.png new file mode 100644 index 00000000..61189b5b Binary files /dev/null and b/src/test/resources/visual-baselines/coverletter-v2-layered/blue_banner-page-0.png differ diff --git a/src/test/resources/visual-baselines/coverletter-v2-layered/boxed_sections-page-0.png b/src/test/resources/visual-baselines/coverletter-v2-layered/boxed_sections-page-0.png new file mode 100644 index 00000000..7c555734 Binary files /dev/null and b/src/test/resources/visual-baselines/coverletter-v2-layered/boxed_sections-page-0.png differ diff --git a/src/test/resources/visual-baselines/coverletter-v2-layered/centered_headline-page-0.png b/src/test/resources/visual-baselines/coverletter-v2-layered/centered_headline-page-0.png new file mode 100644 index 00000000..a9a7bfb6 Binary files /dev/null and b/src/test/resources/visual-baselines/coverletter-v2-layered/centered_headline-page-0.png differ diff --git a/src/test/resources/visual-baselines/coverletter-v2-layered/classic_serif-page-0.png b/src/test/resources/visual-baselines/coverletter-v2-layered/classic_serif-page-0.png new file mode 100644 index 00000000..ad38a57b Binary files /dev/null and b/src/test/resources/visual-baselines/coverletter-v2-layered/classic_serif-page-0.png differ diff --git a/src/test/resources/visual-baselines/coverletter-v2-layered/compact_mono-page-0.png b/src/test/resources/visual-baselines/coverletter-v2-layered/compact_mono-page-0.png new file mode 100644 index 00000000..6e34b928 Binary files /dev/null and b/src/test/resources/visual-baselines/coverletter-v2-layered/compact_mono-page-0.png differ diff --git a/src/test/resources/visual-baselines/coverletter-v2-layered/editorial_blue-page-0.png b/src/test/resources/visual-baselines/coverletter-v2-layered/editorial_blue-page-0.png new file mode 100644 index 00000000..b331a7da Binary files /dev/null and b/src/test/resources/visual-baselines/coverletter-v2-layered/editorial_blue-page-0.png differ diff --git a/src/test/resources/visual-baselines/coverletter-v2-layered/engineering_resume-page-0.png b/src/test/resources/visual-baselines/coverletter-v2-layered/engineering_resume-page-0.png new file mode 100644 index 00000000..808897ee Binary files /dev/null and b/src/test/resources/visual-baselines/coverletter-v2-layered/engineering_resume-page-0.png differ diff --git a/src/test/resources/visual-baselines/coverletter-v2-layered/executive-page-0.png b/src/test/resources/visual-baselines/coverletter-v2-layered/executive-page-0.png new file mode 100644 index 00000000..bd13449a Binary files /dev/null and b/src/test/resources/visual-baselines/coverletter-v2-layered/executive-page-0.png differ diff --git a/src/test/resources/visual-baselines/coverletter-v2-layered/modern_professional-page-0.png b/src/test/resources/visual-baselines/coverletter-v2-layered/modern_professional-page-0.png new file mode 100644 index 00000000..333545d9 Binary files /dev/null and b/src/test/resources/visual-baselines/coverletter-v2-layered/modern_professional-page-0.png differ diff --git a/src/test/resources/visual-baselines/coverletter-v2-layered/monogram_sidebar-page-0.png b/src/test/resources/visual-baselines/coverletter-v2-layered/monogram_sidebar-page-0.png new file mode 100644 index 00000000..ff0ec8e5 Binary files /dev/null and b/src/test/resources/visual-baselines/coverletter-v2-layered/monogram_sidebar-page-0.png differ diff --git a/src/test/resources/visual-baselines/coverletter-v2-layered/nordic_clean-page-0.png b/src/test/resources/visual-baselines/coverletter-v2-layered/nordic_clean-page-0.png new file mode 100644 index 00000000..40ca8b4d Binary files /dev/null and b/src/test/resources/visual-baselines/coverletter-v2-layered/nordic_clean-page-0.png differ diff --git a/src/test/resources/visual-baselines/coverletter-v2-layered/panel-page-0.png b/src/test/resources/visual-baselines/coverletter-v2-layered/panel-page-0.png new file mode 100644 index 00000000..5ed6a4d1 Binary files /dev/null and b/src/test/resources/visual-baselines/coverletter-v2-layered/panel-page-0.png differ diff --git a/src/test/resources/visual-baselines/coverletter-v2-layered/sidebar_portrait-page-0.png b/src/test/resources/visual-baselines/coverletter-v2-layered/sidebar_portrait-page-0.png new file mode 100644 index 00000000..12e3502d Binary files /dev/null and b/src/test/resources/visual-baselines/coverletter-v2-layered/sidebar_portrait-page-0.png differ diff --git a/src/test/resources/visual-baselines/coverletter-v2-layered/timeline_minimal-page-0.png b/src/test/resources/visual-baselines/coverletter-v2-layered/timeline_minimal-page-0.png new file mode 100644 index 00000000..97c5765c Binary files /dev/null and b/src/test/resources/visual-baselines/coverletter-v2-layered/timeline_minimal-page-0.png differ