From d3f28d584ec7d9ff2111fe654bacab9570b7cc8b Mon Sep 17 00:00:00 2001 From: DemchaAV Date: Sun, 24 May 2026 15:11:29 +0200 Subject: [PATCH] docs(templates-layered): add comprehensive guide for the v2 layered architecture Closes the documentation gap surfaced after Phase 3: until now, information about the cv/v2 layered pattern was scattered between package-info JavaDocs and an in-package AUTHORS.md. New contributors had nowhere to land for a coherent walkthrough. Adds docs/templates-layered/ with five focused docs, one per reader persona: - README.md landing page + "you are here" persona map - quickstart.md 5-min orientation + 5-layer diagram + working render-a-CV example - using-templates.md builder API, section types, slots, theme variants, common patterns (incl. persona-neutral teacher example) - authoring-presets.md widget cookbook, anatomy of a preset, full worked CardStyle example, when-to-go-inline guidance - contributor-guide.md 5-layer convention, package map, naming rules, walkthrough for adding invoice-v2 / cover-letter-v2 family, test + doc + PR review checklist Total: 1320 lines across 5 docs. Each doc has a TOC and cross-references the others. README.md updated with a new "Documentation" entry pointing to the landing page and four persona-specific links. Existing v1 surface docs (template-authoring.md, templates-v2.md) flagged as covering the legacy surface; both still valid. No engine, source, or test changes. Pure documentation. --- README.md | 5 +- docs/templates-layered/README.md | 118 +++++++ docs/templates-layered/authoring-presets.md | 350 +++++++++++++++++++ docs/templates-layered/contributor-guide.md | 360 ++++++++++++++++++++ docs/templates-layered/quickstart.md | 142 ++++++++ docs/templates-layered/using-templates.md | 350 +++++++++++++++++++ 6 files changed, 1323 insertions(+), 2 deletions(-) create mode 100644 docs/templates-layered/README.md create mode 100644 docs/templates-layered/authoring-presets.md create mode 100644 docs/templates-layered/contributor-guide.md create mode 100644 docs/templates-layered/quickstart.md create mode 100644 docs/templates-layered/using-templates.md diff --git a/README.md b/README.md index 6fb8ff44..120c9eb0 100644 --- a/README.md +++ b/README.md @@ -189,8 +189,9 @@ document.pageFlow().addCanvas(523, 360, canvas -> canvas ## Documentation -- [Template authoring cheatsheet](./docs/template-authoring.md) — read this once before writing your own template -- [Templates v2 landing](./docs/templates-v2.md) — CV / cover-letter / invoice / proposal preset library +- πŸ†• [**Templates β€” layered architecture**](./docs/templates-layered/README.md) — the canonical going-forward pattern for new template families (CV v2 is the reference implementation). Personas: [quickstart](./docs/templates-layered/quickstart.md) Β· [using templates](./docs/templates-layered/using-templates.md) Β· [authoring presets](./docs/templates-layered/authoring-presets.md) Β· [contributing a new family](./docs/templates-layered/contributor-guide.md). +- [Template authoring cheatsheet](./docs/template-authoring.md) — read this once before writing your own template (covers v1 template surface) +- [Templates v2 landing](./docs/templates-v2.md) — CV / cover-letter / invoice / proposal preset library (v1 surface, still shipped) - [Examples gallery](./examples/README.md) — every runnable example with PDF preview - [Architecture](./docs/architecture.md) Β· [Lifecycle](./docs/lifecycle.md) Β· [Production rendering](./docs/production-rendering.md) - Recipes: [shape-as-container](./docs/recipes/shape-as-container.md) Β· [transforms](./docs/recipes/transforms.md) Β· [tables](./docs/recipes/tables.md) Β· [themes](./docs/recipes/themes.md) Β· [streaming](./docs/recipes/streaming.md) Β· [extending](./docs/recipes/extending.md) diff --git a/docs/templates-layered/README.md b/docs/templates-layered/README.md new file mode 100644 index 00000000..e6894904 --- /dev/null +++ b/docs/templates-layered/README.md @@ -0,0 +1,118 @@ +# Templates β€” Layered Architecture + +The **canonical going-forward pattern** for building business documents +on GraphCompose. CV is the reference implementation today +(`com.demcha.compose.document.templates.cv.v2`); invoice, cover-letter, +proposal, and any new template family will follow the same shape as +they're migrated. + +This is the entry point. Pick the doc that matches your goal. + +--- + +## Pick your path + +### πŸ†• You're new to GraphCompose and templates +Start with **[quickstart.md](quickstart.md)** β€” 5 minutes of orientation: +what GraphCompose templates are, why they're layered, and a copy-paste +example that renders a CV to a PDF. + +### πŸ‘€ You want to render a document with your own data +You like an existing preset (Boxed Sections, Minimal Underlined, +Modern Professional). Now you just want to feed it your name, your +experience, your skills. + +β†’ **[using-templates.md](using-templates.md)** + +You'll learn the `CvDocument` builder API, the three section types +(paragraph / rows / entries), how slots place sections into columns, +and how to swap a theme (colours / fonts / glyphs) without forking a +preset. + +### 🎨 You want a custom visual style on top of v2 +Existing presets aren't quite your design. You want a new look β€” +different section title style, different alignment, different colour +palette β€” but still using the v2 building blocks. + +β†’ **[authoring-presets.md](authoring-presets.md)** + +You'll learn the **widget cookbook** (`Headline`, `ContactLine`, +`SectionHeader`), the 12-line `compose()` pattern, when to drop down to +inline DSL, and how to ship a new preset as ~150 lines that anyone can +read end-to-end. + +### πŸ›  You're adding a new template family to the library +You're a GraphCompose maintainer or contributor. You want to bring +invoice-v2, cover-letter-v2, or a new document type onto the same +5-layer pattern that CV uses. + +β†’ **[contributor-guide.md](contributor-guide.md)** + +You'll get the package convention (`/v2/data` / +`theme` / `components` / `widgets` / `presets`), naming rules, test +expectations, doc expectations, and a worked checklist for a new +template family from empty folder to merged PR. + +--- + +## The 5-layer pattern at a glance + +``` +presets/ composition: data + theme + widgets β†’ DocumentTemplate + β”‚ compose from + β–Ό +widgets/ LEGO bricks: Headline, ContactLine, SectionHeader, … + β”‚ delegate to read tokens from + β–Ό β–Ό +components/ internal renderers + primitives theme/ palette + typography + spacing + decoration + β”‚ render + β–Ό +data/ records describing what to render (no styling) +``` + +Every layer has **one job**. Layers below don't know about layers +above. Adding a new theme variant, a new widget, a new section +subtype, or a new preset each touches one layer and leaves the +others alone. + +The detailed contract for each layer is in +[contributor-guide.md](contributor-guide.md). + +--- + +## What this pattern is *not* + +- ❌ **Not a migration mandate.** Existing v1 templates + (`cv/spec`, `cv/builder`, `cv/presets`) continue to work and + ship. The layered pattern is for **new** templates and major + rewrites. +- ❌ **Not a framework with magic.** Every file is plain + Java records + static helpers. No reflection, no annotations, + no codegen. +- ❌ **Not coupled to CV.** The pattern is domain-agnostic; CV is + just the first family migrated. Invoice or cover-letter would + use the same five folders with their own data shapes inside. +- ❌ **Not a UI framework.** No state, no events, no lifecycle. + Templates render static PDFs from immutable data. + +--- + +## See also + +- **Per-package JavaDocs**: + [`cv/v2/package-info.java`](../../src/main/java/com/demcha/compose/document/templates/cv/v2/package-info.java) + has the ASCII diagram and 4-step author walkthrough. +- **AUTHORS.md**: + [`cv/v2/AUTHORS.md`](../../src/main/java/com/demcha/compose/document/templates/cv/v2/AUTHORS.md) + is the recipe cookbook β€” 7 hands-on recipes from "change a bullet + glyph" to "add a new section subtype". +- **Examples**: + [`examples/cv/v2/`](../../examples/src/main/java/com/demcha/examples/templates/cv/v2) + has three runnable rendering examples β€” one per shipped preset. +- **Legacy v1 surface**: + [`docs/templates-v2.md`](../templates-v2.md) describes the older + spec / preset / theme split used by the v1 templates. Still valid + for the v1 packages; superseded by this guide for v2 work. diff --git a/docs/templates-layered/authoring-presets.md b/docs/templates-layered/authoring-presets.md new file mode 100644 index 00000000..a5b013f2 --- /dev/null +++ b/docs/templates-layered/authoring-presets.md @@ -0,0 +1,350 @@ +# Authoring Presets β€” write your own visual style + +You like the layered architecture, but the three shipped presets +(`BoxedSections`, `MinimalUnderlined`, `ModernProfessional`) don't +match the design you want. This doc walks you through writing a +new preset from scratch β€” **without subclassing, without duplicating +rendering code**. + +If you haven't read [quickstart.md](quickstart.md) and +[using-templates.md](using-templates.md), do those first. + +--- + +## Table of contents + +1. [The core idea β€” compose, don't subclass](#the-core-idea) +2. [The widget catalog](#the-widget-catalog) +3. [Anatomy of a preset](#anatomy-of-a-preset) +4. [Full worked example β€” `CardStyle` preset](#full-worked-example) +5. [When the widget doesn't fit β€” go inline](#when-the-widget-doesnt-fit) +6. [Three layers of widget customisation](#three-layers-of-widget-customisation) +7. [Adding a new widget β€” the test of when](#adding-a-new-widget) +8. [Tests + render parity](#tests--render-parity) + +--- + + +## The core idea β€” compose, don't subclass + +A preset is one `public final class` (no inheritance) with a +`create()` factory that returns a `DocumentTemplate`. +Inside, `compose()` is the orchestration method: it sequences +**widgets** in a page flow. + +```java +@Override +public void compose(DocumentSession document, CvDocument doc) { + document.dsl().pageFlow() + .name("MyPresetRoot") + .spacing(theme.spacing().pageFlowSpacing()) + .addSection("Headline", s -> Headline.spacedCentered(s, doc.identity().name(), theme)) + .addSection("Contact", s -> ContactLine.centered(s, doc.identity(), theme)); + + for (CvSection sec : doc.sectionsIn(Slot.MAIN)) { + pageFlow + .addSection("Title", s -> SectionHeader.banner(s, sec.title(), theme)) + .addSection("Body", s -> SectionDispatcher.renderBody(s, sec, theme)); + } + pageFlow.build(); +} +``` + +That's the **entire** rendering decision tree. ~12 lines. No DSL +plumbing. No private `renderXxx` methods. Each line is a single +visual decision you can read like a recipe. + +--- + + +## The widget catalog + +Today, three widget classes live in +`com.demcha.compose.document.templates.cv.v2.widgets`. Each has 2-3 +named variants. + +### `Headline` β€” top-of-document name + +| Variant | Visual | +|---|---| +| `Headline.spacedCentered(host, name, theme)` | Centred letter-spaced uppercase (`J A N E D O E`) | +| `Headline.rightAligned(host, name, theme)` | Right-aligned plain bold (`Jane Doe`) | +| `Headline.render(host, name, theme, align, spacedCaps)` | Low-level β€” any (alignment, transform) combo | + +### `ContactLine` β€” phone / email / address / links row + +| Variant | Visual | +|---|---| +| `ContactLine.centered(host, identity, theme)` | Centred, phone β†’ email β†’ address β†’ links | +| `ContactLine.rightAligned(host, identity, theme)` | Right-aligned, address β†’ phone β†’ email β†’ links | +| `ContactLine.render(host, identity, theme, align, order)` | Low-level β€” any alignment + field-order combo | + +### `SectionHeader` β€” title above each section body + +| Variant | Visual | +|---|---| +| `SectionHeader.banner(host, title, theme)` | Pale-grey panel + centred spaced-caps inside | +| `SectionHeader.underlined(host, title, theme)` | Small left spaced-caps + thin rule below | +| `SectionHeader.flat(host, title, color, theme)` | Large bold title in a given colour, no panel | + +The separator glyph used by `ContactLine`, the bullet glyph used by +`RowRenderer`, and other character-level choices come from +`theme.decoration()` β€” swap a `CvDecoration` to change them +globally. + +--- + + +## Anatomy of a preset + +Every preset is the same skeleton: + +```java +public final class MyPreset { + + public static final String ID = "my-preset"; + public static final String DISPLAY_NAME = "My Preset"; + public static final double RECOMMENDED_MARGIN = 28.0; + + private MyPreset() { } + + public static DocumentTemplate create() { + return create(CvTheme.boxedClassic()); + } + + 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, CvDocument doc) { + // ← the only place that varies between presets + } + } +} +``` + +Two factories (`create()` and `create(CvTheme)`), three constants +(`ID`, `DISPLAY_NAME`, `RECOMMENDED_MARGIN`), one inner `Template` +class implementing `DocumentTemplate`. Stable. + +--- + + +## Full worked example β€” `CardStyle` preset + +Suppose you want a preset where each section is wrapped in a soft +card with a coloured left accent stripe. Here's the full preset. + +```java +public final class CardStyle { + + public static final String ID = "card-style"; + public static final String DISPLAY_NAME = "Card Style"; + public static final double RECOMMENDED_MARGIN = 24.0; + + private static final DocumentColor ACCENT = DocumentColor.rgb(33, 150, 243); + + private CardStyle() { } + + public static DocumentTemplate create() { + return create(CvTheme.boxedClassic()); + } + + 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, CvDocument doc) { + PageFlowBuilder pageFlow = document.dsl().pageFlow() + .name("CardStyleRoot") + .spacing(8) + .addSection("Headline", s -> + Headline.rightAligned(s, doc.identity().name(), theme)) + .addSection("Contact", s -> + ContactLine.rightAligned(s, doc.identity(), theme)); + + for (CvSection sec : doc.sectionsIn(Slot.MAIN)) { + pageFlow.addSection("Card", host -> { + host.accentLeft(ACCENT, 3.0) // ← the "card" stripe + .padding(new DocumentInsets(8, 12, 8, 12)); + SectionHeader.flat(host, sec.title(), ACCENT, theme); + SectionDispatcher.renderBody(host, sec, theme); + }); + } + pageFlow.build(); + } + } +} +``` + +**Forty-five lines** including the boilerplate. Everything that +makes it visually distinct is in `compose()`: + +- right-aligned headline (existing widget) +- right-aligned contact (existing widget) +- a custom "card" wrapper around each section (inline β€” uses DSL + `accentLeft` + `padding` directly) +- flat coloured section title (existing widget, given `ACCENT`) +- body rendered via the dispatcher (no custom rendering) + +You used **three widgets** (`Headline`, `ContactLine`, +`SectionHeader`) plus **two inline DSL calls** (`accentLeft` and +`padding`) to build the card shape. No private `renderXxx`. No +duplicated rendering. + +--- + + +## When the widget doesn't fit β€” go inline + +Widgets are **optional helpers**, not required wrappers. If your +preset needs something the catalog doesn't cover, **inline it**. + +Example: `ModernProfessional` uses preset-specific colours +(slate-blue name, royal-blue link underlines) that no widget +default knows about. Its `renderHeader` and `renderContact` stay +inline β€” only `renderSectionTitle` uses a widget +(`SectionHeader.flat(..., SECTION_TITLE_COLOR, theme)` because that +widget takes a colour parameter). + +```java +private void renderHeader(SectionBuilder section, CvIdentity identity) { + DocumentTextStyle nameStyle = DocumentTextStyle.builder() + .fontName(FontName.HELVETICA_BOLD) + .size(theme.typography().sizeHeadline()) + .color(NAME_COLOR) + .build(); + + section.addParagraph(p -> p + .text(identity.name().full()) + .textStyle(nameStyle) + .align(TextAlign.RIGHT) + .margin(DocumentInsets.zero())); +} +``` + +This is fine. **Widgets coexist with inline DSL** in the same +`compose()`. If you see the same inline rendering repeating across +**2+ presets**, *then* extract a widget β€” not before. + +--- + + +## Three layers of widget customisation + +Every widget exposes three layers, escalating from convenience to +control: + +### Layer 1 β€” convenience factory (covers ~80% of cases) + +```java +Headline.spacedCentered(host, name, theme); +``` + +One line. No params beyond `(host, content, theme)`. + +### Layer 2 β€” `.render(...)` with parameters (covers ~15%) + +```java +Headline.render(host, name, theme, TextAlign.LEFT, /* spacedCaps */ false); +``` + +Same widget, fully parameterised. Use when the convenience method +doesn't match your need but the widget shape is right. + +### Layer 3 β€” inline DSL (covers ~5%) + +```java +section.addParagraph(p -> p + .text(name.full()) + .textStyle(myCustomStyle) + .align(TextAlign.RIGHT)); +``` + +Bypass the widget entirely. Use when no widget shape fits. + +**Don't fight the widget API.** If Layer 1 fits, use Layer 1. If +not, try Layer 2. If still not, go inline. That's the design. + +--- + + +## Adding a new widget β€” the test of when + +| Pattern repetition across presets | Action | +|---|---| +| 1 preset only | Inline. Don't extract. | +| 2 presets | Add a new factory method to an existing widget, OR add a parameter to `.render(...)`. | +| 3+ presets | It's its own widget β€” new class in `cv/v2/widgets/`. | + +Don't predict β€” extract. Premature widgets are noise; they add API +surface that nobody calls. + +When you do add a new widget: + +1. **One file per widget** in `cv/v2/widgets/`. +2. **`public final class`** with a private constructor. +3. **2-3 named factories** + a lower-level `.render(...)`. +4. **First parameter is always `SectionBuilder host`**. +5. **Last parameter is always `CvTheme theme`**. +6. **No instance state** β€” all static, all stateless. +7. **JavaDoc the visual** β€” what does this look like? Who uses it? +8. **Add to `WidgetSmokeTest`** with a basic "renders without + throwing" check. + +--- + + +## Tests + render parity + +A new preset needs at least: + +1. **Smoke test** in `src/test/.../cv/v2/presets/MyPresetSmokeTest.java`: + - `exposes_stable_identity` β€” checks `id()` and `displayName()` + - `default_factory_renders` β€” calls `create().compose(...)` with + a full sample document, asserts `session.roots()` is non-empty + - `custom_theme_renders` β€” same but with `create(theme)` + - `renders_with_classic_theme_too` β€” proves the preset doesn't + depend on theme-specific tokens + +2. **Example runner** in + `examples/src/main/java/com/demcha/examples/templates/cv/v2/CvMyPresetExample.java`: + - Renders to `examples/target/generated-pdfs/templates/cv/cv-my-preset.pdf` + - Uses `ExampleDataFactory.sampleCvDocumentV2()` for content + +3. **Eyeball the rendered PDF** β€” does it match your design + intent? Are sections in the right slots? Is page break sensible? + +A future Phase will add PDF/PNG snapshot diffing so visual +regressions break the build. Until then, render parity is by-hand. + +--- + +## Next step + +β†’ Want to add a brand-new template family (invoice-v2, +cover-letter-v2) following the same layered shape? +[**contributor-guide.md**](contributor-guide.md) + +β†’ The full recipe cookbook (with code for every customisation +combo): +[`cv/v2/AUTHORS.md`](../../src/main/java/com/demcha/compose/document/templates/cv/v2/AUTHORS.md) diff --git a/docs/templates-layered/contributor-guide.md b/docs/templates-layered/contributor-guide.md new file mode 100644 index 00000000..b5565eaf --- /dev/null +++ b/docs/templates-layered/contributor-guide.md @@ -0,0 +1,360 @@ +# Contributor Guide β€” add a new template family + +You're a GraphCompose contributor and you want to bring a brand-new +document type onto the layered architecture β€” invoice-v2, +cover-letter-v2, report-v2, anything that isn't CV. This doc is +your checklist + convention reference. + +It assumes you've read [quickstart.md](quickstart.md) and +[authoring-presets.md](authoring-presets.md) β€” those explain *why* +the layered pattern looks the way it does. + +--- + +## Table of contents + +1. [The 5-layer convention](#the-5-layer-convention) +2. [Package map for a new family](#package-map-for-a-new-family) +3. [Naming rules](#naming-rules) +4. [Worked walkthrough β€” `invoice/v2`](#worked-walkthrough) +5. [Required deliverables](#required-deliverables) +6. [Test checklist](#test-checklist) +7. [Doc checklist](#doc-checklist) +8. [What you must NOT do](#what-you-must-not-do) +9. [PR review expectations](#pr-review-expectations) + +--- + + +## The 5-layer convention + +Every new template family lives under +`com.demcha.compose.document.templates..v2` with **exactly +five sub-packages**: + +``` +/v2/ +β”œβ”€β”€ data/ records describing what's on the page +β”œβ”€β”€ theme/ cosmetic tokens (palette, typography, spacing, decoration) +β”œβ”€β”€ components/ internal renderers + low-level primitives +β”œβ”€β”€ widgets/ reusable visual LEGO bricks +└── presets/ composition: data + theme + widgets β†’ DocumentTemplate +``` + +Each layer's contract: + +| Layer | Contract | +|---|---| +| `data/` | Pure records. Zero dependencies on rendering, theming, or DSL. Sealed hierarchy for section / block subtypes. | +| `theme/` | Records + factories. No rendering logic. Aggregate root is `Theme(palette, typography, spacing, decoration)`. | +| `components/` | Static helpers. Take `(host, data, theme)`. No statics holding state. No magic numbers β€” read tokens from theme. | +| `widgets/` | Static helpers. Named factory methods per visual variant. Compose internally from `components/`. | +| `presets/` | One `public final class` per visual style. Two factories: `create()` and `create(Theme)`. Inner `Template` implements `DocumentTemplate<Document>`. | + +**The convention is the same regardless of domain.** A cv has +identity + sections; an invoice has parties + line items + totals; a +proposal has scope + pricing + acceptance terms. The *shape* of the +data differs; the *layering* is identical. + +--- + + +## Package map for a new family + +Mirror the CV v2 layout. Concrete example for invoice: + +``` +src/main/java/com/demcha/compose/document/templates/invoice/v2/ +β”œβ”€β”€ package-info.java ← ASCII diagram + 4-step walkthrough +β”œβ”€β”€ AUTHORS.md ← recipe cookbook +β”œβ”€β”€ data/ +β”‚ β”œβ”€β”€ package-info.java +β”‚ β”œβ”€β”€ InvoiceDocument.java ← root record +β”‚ β”œβ”€β”€ InvoiceParty.java ← from / bill-to +β”‚ β”œβ”€β”€ InvoiceLine.java ← line item record +β”‚ β”œβ”€β”€ InvoiceTotals.java ← subtotal / tax / total +β”‚ β”œβ”€β”€ InvoiceSection.java ← sealed interface +β”‚ β”œβ”€β”€ HeaderSection.java ← concrete subtype +β”‚ β”œβ”€β”€ LineItemsSection.java ← concrete subtype +β”‚ β”œβ”€β”€ TotalsSection.java ← concrete subtype +β”‚ β”œβ”€β”€ NotesSection.java ← concrete subtype +β”‚ └── Slot.java ← if multi-column variants needed +β”œβ”€β”€ theme/ +β”‚ β”œβ”€β”€ package-info.java +β”‚ β”œβ”€β”€ InvoicePalette.java ← ink / muted / rule / accent +β”‚ β”œβ”€β”€ InvoiceTypography.java ← scale +β”‚ β”œβ”€β”€ InvoiceSpacing.java ← margins, gaps +β”‚ β”œβ”€β”€ InvoiceDecoration.java ← row separators, totals divider +β”‚ └── InvoiceTheme.java ← aggregate + factories +β”œβ”€β”€ components/ +β”‚ β”œβ”€β”€ package-info.java +β”‚ β”œβ”€β”€ ParagraphPrimitive.java ← internal β€” package-private +β”‚ β”œβ”€β”€ LineRowRenderer.java ← one line item row +β”‚ β”œβ”€β”€ TotalsBlockRenderer.java ← right-aligned totals stack +β”‚ β”œβ”€β”€ PartyBlockRenderer.java ← from / bill-to address block +β”‚ β”œβ”€β”€ SectionDispatcher.java ← branches on sealed subtype +β”‚ └── … (other internal renderers) +β”œβ”€β”€ widgets/ +β”‚ β”œβ”€β”€ package-info.java +β”‚ β”œβ”€β”€ Letterhead.java ← top-of-doc invoice header +β”‚ β”œβ”€β”€ PartyPair.java ← from + bill-to side-by-side +β”‚ β”œβ”€β”€ LineTable.java ← line items table +β”‚ └── TotalsCard.java ← totals summary +└── presets/ + β”œβ”€β”€ package-info.java + β”œβ”€β”€ ClassicInvoice.java ← reference preset + └── (more as added) +``` + +The structure is **identical** to cv/v2 β€” only the records inside +differ. + +--- + + +## Naming rules + +- **Family prefix** on top-level records to avoid name collisions. + CV uses `CvName`, `CvContact`, `CvTheme`. Invoice should use + `InvoiceParty`, `InvoiceLine`, `InvoiceTheme`. Cover letter: + `CoverLetterRecipient`, `CoverLetterTheme`. Etc. +- **`Document`** for the root record. (`CvDocument`, + `InvoiceDocument`, `CoverLetterDocument`.) +- **`Section` (sealed)** for the body content hierarchy. +- **`Theme(palette, typography, spacing, decoration)`** for + the aggregate theme record. +- **Widgets** are domain-specific verbs / nouns describing the + visual: `Headline`, `ContactLine`, `SectionHeader` (CV); + `Letterhead`, `LineTable`, `TotalsCard` (invoice). Don't try to + share widget names across families β€” they capture different + visual ideas. +- **Presets** are descriptive proper-noun names: `BoxedSections`, + `ModernProfessional`, `ClassicInvoice`, `MinimalLetter`. Avoid + generic names like `Default` or `Standard`. + +--- + + +## Worked walkthrough β€” `invoice/v2` + +Order to write things in: + +### 1. `data/` first (no rendering deps) + +```java +public record InvoiceDocument( + InvoiceParty issuer, + InvoiceParty recipient, + String invoiceNumber, + String issueDate, + String dueDate, + List sections) { + // builder, validation, accessors +} + +public sealed interface InvoiceSection + permits HeaderSection, LineItemsSection, TotalsSection, NotesSection { + String title(); +} +``` + +Sealed `InvoiceSection` lists every body shape an invoice can have. +Concrete subtypes (`LineItemsSection`, `TotalsSection`, …) are +records carrying the section data. + +### 2. `theme/` second (no rendering, just tokens) + +```java +public record InvoiceTheme( + InvoicePalette palette, + InvoiceTypography typography, + InvoiceSpacing spacing, + InvoiceDecoration decoration) { + + public static InvoiceTheme classic() { + return new InvoiceTheme( + InvoicePalette.classic(), + InvoiceTypography.classic(), + InvoiceSpacing.classic(), + InvoiceDecoration.classic()); + } +} +``` + +### 3. `components/` third (low-level renderers consume data + theme) + +```java +public final class LineRowRenderer { + private LineRowRenderer() {} + + public static void render(SectionBuilder host, InvoiceLine line, + InvoiceTheme theme) { + // … DSL calls reading theme tokens … + } +} +``` + +Also: extract the shared paragraph DSL into a package-private +`ParagraphPrimitive` (same idea as CV) so renderers don't duplicate +configuration. + +### 4. `widgets/` fourth (named visual building blocks) + +```java +public final class LineTable { + private LineTable() {} + + public static void render(SectionBuilder host, + List lines, InvoiceTheme theme) { + for (InvoiceLine line : lines) { + LineRowRenderer.render(host, line, theme); + } + } +} +``` + +Widgets wrap components into composable units that a preset can drop +into its page flow. + +### 5. `presets/` last (orchestration) + +```java +public final class ClassicInvoice { + public static final String ID = "classic-invoice"; + public static final String DISPLAY_NAME = "Classic Invoice"; + public static final double RECOMMENDED_MARGIN = 28.0; + + private ClassicInvoice() {} + + public static DocumentTemplate create() { + return create(InvoiceTheme.classic()); + } + + public static DocumentTemplate create(InvoiceTheme theme) { + return new Template(theme); + } + + private static final class Template + implements DocumentTemplate { + // … compose() picks widgets in order … + } +} +``` + +Mirror the CV preset shape exactly. + +--- + + +## Required deliverables + +A new template family PR ships with: + +- [ ] **5 packages** under `/v2/` populated per convention +- [ ] **At least 1 reference preset** that renders a sample document +- [ ] **`AUTHORS.md`** in the family root (recipe cookbook β€” copy + the cv/v2 one as starting structure) +- [ ] **`package-info.java`** at family root + each sub-package +- [ ] **Sample fixture** in `ExampleDataFactory.sampleDocumentV2()` +- [ ] **Example runner** in `examples/.../templates//v2/` +- [ ] **Smoke tests** per the checklist below +- [ ] **No edits** to `engine/`, `dsl/`, or v1 `/` surface + +--- + + +## Test checklist + +Minimum test coverage matching CV v2: + +| Test class | What it asserts | +|---|---| +| `DocumentTest` | Builder rejects null / blank required fields; valid build succeeds | +| `ThemeTest` | All factories produce valid themes; deprecated constructors (if any) wrap correctly | +| `SmokeTest` | `id()`, `displayName()`, default-factory render, custom-theme render | +| `SectionDispatcherTest` *(optional)* | Each sealed subtype routes correctly | +| `WidgetSmokeTest` | Each public widget variant renders without throwing | + +CI must be green before merge. Visual regression (PNG diff against +a reference render) is encouraged but optional today. + +--- + + +## Doc checklist + +- [ ] `/v2/package-info.java` β€” ASCII diagram of the 5 + layers, plus a 4-step "how to author a document" walkthrough + (copy the cv/v2 one's structure). +- [ ] `/v2/AUTHORS.md` β€” recipe cookbook. At least: + change a glyph, change colours, add a new section subtype, + conditional sections. +- [ ] `/v2//package-info.java` β€” each + sub-package gets a paragraph explaining its role. +- [ ] **Update [`docs/templates-layered/README.md`](README.md)** to + list the new family in the "implementations" section. + +--- + + +## What you must NOT do + +- ❌ **Don't edit the engine** (`document/api`, `document/dsl`, + `document/engine`, `document/node`, `document/style`). If your + family needs an engine feature that doesn't exist, that's a + separate prerequisite PR. +- ❌ **Don't edit v1 surface** for the same family. They coexist. + Mark v1 `@Deprecated` only after the v2 surface is feature-complete + and shipped β€” that's a follow-up PR. +- ❌ **Don't fork widgets across families.** If invoice needs a + `Headline`, write `templates/invoice/v2/widgets/Letterhead.java` + β€” an invoice letterhead has different needs from a CV name + headline. Don't try to share the same widget class across + domains. +- ❌ **Don't use inheritance.** All records are sealed, all + components / widgets / presets are `final` with private + constructors. The intentional API shape. +- ❌ **Don't add `instanceof` on the sealed section type outside + `SectionDispatcher`.** The dispatcher is the single dispatch + point. +- ❌ **Don't hard-code colours / fonts / sizes in components or + widgets.** Every value reads from the theme. Preset-specific + accents (a single colour used only by one preset) may live as + `private static final` in that preset β€” but only when no other + preset needs them. + +--- + + +## PR review expectations + +A new template family PR is reviewed against: + +1. **Layer discipline** β€” do data / theme / components / widgets / + presets each obey their contract? +2. **Test coverage** β€” smoke tests for every public surface; + builder validation tests; theme factory tests. +3. **Doc completeness** β€” `package-info.java` everywhere, + `AUTHORS.md` with at least 4 recipes, root README updated. +4. **Visual signature** β€” render the reference preset, attach the + PDF to the PR description, eyeball-validate it matches the + intent. +5. **No engine / v1 edits** β€” additive only. + +Expected size: ~1500-2500 lines of new code for a fresh family. +Compare to cv/v2 baseline (PR #45) which was 2082 lines including +35 files. + +--- + +## See also + +- The **CV v2** package + (`com.demcha.compose.document.templates.cv.v2`) is the reference + implementation. Read it end-to-end before starting a new family + β€” every convention listed here is visible there. +- [authoring-presets.md](authoring-presets.md) β€” how preset authors + use widgets. Same conventions apply when designing widgets for a + new family. +- [using-templates.md](using-templates.md) β€” what end users see. + Your new family's API should feel consistent with this. diff --git a/docs/templates-layered/quickstart.md b/docs/templates-layered/quickstart.md new file mode 100644 index 00000000..f7e4e0bb --- /dev/null +++ b/docs/templates-layered/quickstart.md @@ -0,0 +1,142 @@ +# Quickstart β€” Templates Layered Architecture + +**5 minutes.** What it is, why it's structured this way, and a working +example that renders a CV PDF. + +--- + +## What you get + +GraphCompose's templates v2 (layered) gives you: + +- **Records describing content** β€” `CvDocument`, `CvIdentity`, + `CvSection`. No styling, no rendering, just structured data. +- **Themes describing visuals** β€” `CvTheme` (palette + typography + + spacing + decoration). Swap a theme to change colours, fonts, + bullet glyphs without touching renderers. +- **Widgets as visual LEGO bricks** β€” `Headline`, `ContactLine`, + `SectionHeader`. Each one is a named visual decision you can drop + into a preset. +- **Presets as compositions** β€” a preset orchestrates widgets in a + page flow. `BoxedSections`, `MinimalUnderlined`, `ModernProfessional` + ship today; writing your own is ~150 lines. + +You hand a `CvDocument` to a preset, you get a PDF. The preset +internally composes widgets that read theme tokens that ultimately +emit DSL calls to the engine. + +--- + +## Render your first CV + +```java +import com.demcha.compose.GraphCompose; +import com.demcha.compose.document.api.DocumentPageSize; +import com.demcha.compose.document.api.DocumentSession; +import com.demcha.compose.document.templates.api.DocumentTemplate; +import com.demcha.compose.document.templates.cv.v2.data.*; +import com.demcha.compose.document.templates.cv.v2.presets.BoxedSections; + +import java.nio.file.Path; + +CvDocument doc = CvDocument.builder() + .identity(CvIdentity.builder() + .name("Jane", "Doe") + .contact("+44 20 7946 0958", "jane@example.com", "London, UK") + .link("LinkedIn", "https://linkedin.com/in/jane-doe") + .build()) + .section(new ParagraphSection("Professional Summary", + "Backend engineer with **5 years** of experience building " + + "high-throughput payment systems.")) + .section(RowsSection.builder("Technical Skills", RowStyle.BULLETED) + .row("Languages", "Java 21, Kotlin, SQL") + .row("Frameworks", "Spring Boot, Quarkus") + .build()) + .section(EntriesSection.builder("Experience") + .entry("Senior Engineer", "Acme Payments", "2022-Present", + "Owned the settlement service handling **2M+ tx/day**.") + .entry("Engineer", "Bright Bank", "2019-2022", + "Built the fraud-detection rule engine.") + .build()) + .build(); + +DocumentTemplate template = BoxedSections.create(); + +try (DocumentSession session = GraphCompose.document(Path.of("cv.pdf")) + .pageSize(DocumentPageSize.A4) + .margin(28, 28, 28, 28) + .create()) { + template.compose(session, doc); + session.buildPdf(); +} +``` + +Run it. You get `cv.pdf` rendered in the Boxed Sections visual style. +Want it in the Modern Professional style instead? Change one line: + +```java +DocumentTemplate template = ModernProfessional.create(); +``` + +Same data, different visual. That's the layering. + +--- + +## The 5 layers + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ presets/ BoxedSections, MinimalUnderlined, β”‚ +β”‚ ModernProfessional β”‚ +β”‚ β€” composition of widgets in a page flow β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ compose from widgets + β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ widgets/ Headline, ContactLine, SectionHeader β”‚ +β”‚ β€” named visual LEGO bricks β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ delegate to ↓ β”‚ read tokens from ↓ + β–Ό β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ components/ β”‚ β”‚ theme/ β”‚ +β”‚ SectionDispatcher β”‚ β”‚ CvPalette (colours) β”‚ +β”‚ EntryRenderer β”‚ β”‚ CvTypography (fonts + sizes) β”‚ +β”‚ RowRenderer β”‚ β”‚ CvSpacing (margins + gaps) β”‚ +β”‚ ParagraphRenderer β”‚ β”‚ CvDecoration (bullet, sep) β”‚ +β”‚ + primitives β”‚ β”‚ CvTheme (bundle + factories) β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ renders into DSL + β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ data/ CvDocument, CvIdentity, CvSection (sealed), β”‚ +β”‚ ParagraphSection / RowsSection / EntriesSection,β”‚ +β”‚ CvRow, CvEntry, Slot β”‚ +β”‚ β€” pure records, zero rendering deps β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +**What each layer is for** (in plain English): + +| Layer | "Answers the question…" | +|---|---| +| `data/` | "What goes on the page?" β€” content, no styling. | +| `theme/` | "How does it look?" β€” colours, fonts, glyphs. | +| `components/` | "How is one element drawn?" β€” paragraph, row, entry primitives. | +| `widgets/` | "Which visual building block do I want here?" β€” named LEGO bricks. | +| `presets/` | "In what order, with which widgets, on which page flow?" β€” composition. | + +Lower layers don't know about higher ones. A theme change doesn't +touch a renderer. A new widget doesn't touch the data model. + +--- + +## Where to go next + +| You want to… | Read | +|---|---| +| Render your own CV with your data | [using-templates.md](using-templates.md) | +| Make a new visual style | [authoring-presets.md](authoring-presets.md) | +| Add a new template family to the library (invoice, cover-letter) | [contributor-guide.md](contributor-guide.md) | +| See hands-on recipes (change a bullet, swap colours, …) | [`cv/v2/AUTHORS.md`](../../src/main/java/com/demcha/compose/document/templates/cv/v2/AUTHORS.md) | +| Run the shipped examples | [`examples/cv/v2/`](../../examples/src/main/java/com/demcha/examples/templates/cv/v2) | diff --git a/docs/templates-layered/using-templates.md b/docs/templates-layered/using-templates.md new file mode 100644 index 00000000..49c40aec --- /dev/null +++ b/docs/templates-layered/using-templates.md @@ -0,0 +1,350 @@ +# Using Templates β€” author your own document + +You have a preset you like. You want to render **your** content +(your name, your experience, your skills, your design tweaks). This +doc walks the `CvDocument` builder, the section types, and the +theme variants. + +If you haven't read [quickstart.md](quickstart.md), do that first β€” +it sets up the conceptual model in 5 minutes. + +--- + +## Table of contents + +1. [The three pieces you assemble](#the-three-pieces-you-assemble) +2. [Identity β€” name, contact, optional links](#identity) +3. [Section types β€” three shapes cover every case](#section-types) +4. [Slots β€” main vs sidebar](#slots) +5. [Picking a preset](#picking-a-preset) +6. [Customising a theme](#customising-a-theme) +7. [Rendering β€” pageSize, margins, output](#rendering) +8. [Common patterns](#common-patterns) + +--- + +## The three pieces you assemble + +```java +CvDocument doc = …; // your content +CvTheme theme = CvTheme.boxedClassic(); // optional override +DocumentTemplate tpl = BoxedSections.create(theme); + +try (DocumentSession s = GraphCompose.document(path).create()) { + tpl.compose(s, doc); + s.buildPdf(); +} +``` + +Three lines of "what": +- **`CvDocument`** β€” your content. Built via builder. +- **`CvTheme`** β€” visual style. Use a shipped factory or build your own. +- **A preset** β€” orchestrates them into a page flow. + +--- + + +## Identity β€” name, contact, optional links + +Every CV starts with a `CvIdentity`. Two required pieces, links are +optional. + +```java +CvIdentity identity = CvIdentity.builder() + // Required: first + last (middle is optional) + .name("Jane", "Doe") + // .name("Jane", "Q.", "Doe") // ← with middle name + + // Required: phone + email + address (none can be blank) + .contact("+44 20 7946 0958", + "jane.doe@example.com", + "London, UK") + + // Optional: any number of labelled links + .link("LinkedIn", "https://linkedin.com/in/jane-doe") + .link("GitHub", "https://github.com/jane-doe") + .link("Portfolio","https://jane.dev") + .build(); +``` + +**No `.link(...)` calls?** Header just renders the phone / email / +address. Optional means truly optional. + +**Label is a free string.** `"Behance"`, `"Substack"`, `"Etsy"`, anything. +The widget renders the label; the URL is the click target. + +--- + + +## Section types β€” three shapes cover every case + +The `CvSection` sealed hierarchy has exactly **three** concrete +shapes. Each captures a structurally different content pattern, not +a visual flavour. + +### 1. `ParagraphSection` β€” one block of prose + +For Professional Summary, Profile, Objective, About Me. + +```java +new ParagraphSection("Professional Summary", + "Backend engineer with **5 years** of experience..."); +``` + +Inline markdown (`**bold**`, `*italic*`, `_italic_`) is honoured. + +### 2. `RowsSection` β€” list of label/body rows with a decoration style + +For Technical Skills, Languages, Awards, Additional Information, +Projects, anything with "label: value" entries. + +```java +RowsSection.builder("Technical Skills", RowStyle.BULLETED) + .row("Languages", "Java 21, Kotlin, SQL") + .row("Tools", "Maven, Docker, GitHub Actions") + .build(); +``` + +**Three decoration styles** β€” pick by what you want visually: + +```java +RowStyle.PLAIN // Label: body (no bullet, single line) +RowStyle.BULLETED // β€’ Label: body +RowStyle.BULLETED_STACKED // β€’ Label + // body (on second line, indented) +``` + +The same `RowsSection` type covers Technical Skills, Additional +Information, Projects β€” pick the style that matches the visual +density you want. + +### 3. `EntriesSection` β€” timeline entries (title / subtitle / date / body) + +For Education, Professional Experience β€” anything where you have a +list of items each with a title, subtitle, date, and description. + +```java +EntriesSection.builder("Experience") + .entry("Senior Engineer", // title (bold) + "Acme Payments", // subtitle (italic, muted) + "2022-Present", // date (right-aligned) + "Owned the settlement service...") // body (paragraph) + .entry("Engineer", + "Bright Bank", + "2019-2022", + "Built the fraud-detection rule engine.") + .build(); +``` + +Blank fields collapse β€” a blank `date` removes the right column, a +blank `subtitle` drops the italic line, a blank `body` drops the +paragraph beneath. + +--- + + +## Slots β€” main vs sidebar + +By default, every section is placed in `Slot.MAIN` β€” the main +column. Multi-column presets read `Slot.SIDEBAR` separately. + +```java +CvDocument doc = CvDocument.builder() + .identity(identity) + .section(summary) // β†’ MAIN (default) + .section(Slot.MAIN, skills) // β†’ MAIN (explicit) + .section(Slot.SIDEBAR, languagesSpoken) // β†’ SIDEBAR + .sections(Slot.MAIN, experience, education) // varargs β†’ MAIN + .build(); +``` + +**Single-column presets** (`BoxedSections`, `MinimalUnderlined`, +`ModernProfessional`) render only `Slot.MAIN`. Sidebar content is +silently dropped β€” switch to a multi-column preset to render it. + +If you don't use slots at all, your sections go to `MAIN` and every +preset renders them. The slot model is opt-in. + +--- + + +## Picking a preset + +Three shipped today: + +| Preset | Visual signature | +|---|---| +| `BoxedSections.create()` | Centred letter-spaced name, pale-grey panel section banners, two-page friendly | +| `MinimalUnderlined.create()` | Centred name with thin rule, small spaced-caps section titles with accent rule, single page | +| `ModernProfessional.create()` | Right-aligned big slate-blue name, flat bright-blue bold section titles, dense single page | + +Each factory has a no-arg form (uses a sensible default theme) and +a `create(CvTheme)` form (custom theme). + +```java +BoxedSections.create() // default theme +BoxedSections.create(CvTheme.boxedClassic()) // explicit +BoxedSections.create(myCustomTheme) // your own +``` + +--- + + +## Customising a theme + +Themes are records made of four sub-records: + +| Sub-record | What it controls | +|---|---| +| `CvPalette` | Colours (`ink`, `muted`, `rule`, `banner`) | +| `CvTypography` | Fonts + size scale (8 sizes + line spacing) | +| `CvSpacing` | Margins, padding, weights, gaps | +| `CvDecoration` | Bullet glyph, stacked indent, contact separator | + +**Swap one piece, keep the rest:** + +```java +// Navy palette, classic everything else +CvPalette navy = new CvPalette( + DocumentColor.rgb(15, 34, 80), // ink β€” primary text + DocumentColor.rgb(90, 110, 150), // muted β€” italic subtitles + DocumentColor.rgb(120, 140, 180), // rule β€” separator lines + DocumentColor.rgb(220, 230, 240)); // banner β€” pale fill + +CvTheme navyTheme = new CvTheme( + navy, + CvTypography.classic(), + CvSpacing.classic(), + CvDecoration.classic()); + +BoxedSections.create(navyTheme); +``` + +**Change a glyph** (bullet, separator): + +```java +CvDecoration arrowDecoration = new CvDecoration( + "β–Ά ", // bullet glyph + " ", // stacked-row second-line indent + " Β· "); // contact-line separator + +CvTheme theme = new CvTheme( + CvPalette.classic(), + CvTypography.classic(), + CvSpacing.classic(), + arrowDecoration); +``` + +**Change a font** (`Helvetica` instead of `PT Serif`): + +```java +CvTypography sans = new CvTypography( + FontName.HELVETICA_BOLD, FontName.HELVETICA, + 21.5, 8.5, 9.6, 9.2, 8.8, 8.4, 8.6, 1.4); // sizes per role + +CvTheme theme = new CvTheme( + CvPalette.classic(), sans, CvSpacing.classic(), CvDecoration.classic()); +``` + +For more recipes (compact spacing, alternative typography scales, +etc.) see [`cv/v2/AUTHORS.md`](../../src/main/java/com/demcha/compose/document/templates/cv/v2/AUTHORS.md). + +--- + + +## Rendering β€” pageSize, margins, output + +Standard session-first API. The preset has a `RECOMMENDED_MARGIN` +constant that pairs visually with its design. + +```java +float m = (float) BoxedSections.RECOMMENDED_MARGIN; // 28pt for Boxed + +try (DocumentSession session = GraphCompose.document(Path.of("cv.pdf")) + .pageSize(DocumentPageSize.A4) + .margin(m, m, m, m) + .create()) { + + template.compose(session, doc); + session.buildPdf(); // writes the file +} +``` + +Other output forms: + +```java +session.toPdfBytes(); // byte[] +session.buildPdf(output); // OutputStream +``` + +--- + + +## Common patterns + +### Conditional section (omit if data is empty) + +```java +CvDocument.Builder b = CvDocument.builder().identity(identity); +b.section(summary); +if (!certificates.isEmpty()) { + b.section(buildCertificationsSection(certificates)); +} +CvDocument doc = b.build(); +``` + +### Sidebar content + +```java +CvDocument.builder() + .identity(identity) + .section(summary) // main + .sections(Slot.SIDEBAR, skills, languages) // sidebar + .build(); +``` + +Then render with a multi-column preset (when one ships) β€” sidebar +content is dropped by single-column presets today. + +### Skip section title + +The section's `title` is rendered by the preset. To suppress a +section title visually you'd need a preset that doesn't render it +(or write your own β€” see [authoring-presets.md](authoring-presets.md)). + +### Persona-neutral content + +Nothing in the API assumes a developer audience. A teacher's CV +looks the same β€” different strings, same builders: + +```java +CvDocument.builder() + .identity(CvIdentity.builder() + .name("Maria", "Lopez") + .contact("+34 600 000 000", "maria@example.com", "Madrid, Spain") + // no .link() β€” Maria has no public profiles + .build()) + .section(new ParagraphSection("About Me", + "Primary school teacher with 12 years' experience.")) + .section(EntriesSection.builder("Teaching Experience") + .entry("Lead Teacher Y3", "Colegio Santa Ana", "2018-Present", + "Year-3 lead, mentored two NQTs.") + .build()) + .section(RowsSection.builder("Languages", RowStyle.PLAIN) + .row("Spanish", "Native") + .row("English", "Fluent (CEFR C1)") + .build()) + .build(); +``` + +No GitHub, no Projects, no Tech Skills β€” and the API doesn't notice. + +--- + +## Next step + +β†’ Want a custom visual style on top of the v2 building blocks? +[**authoring-presets.md**](authoring-presets.md) + +β†’ Reference for every recipe (change bullet, swap colours, …) +[`cv/v2/AUTHORS.md`](../../src/main/java/com/demcha/compose/document/templates/cv/v2/AUTHORS.md)