diff --git a/CHANGELOG.md b/CHANGELOG.md index ccc60184..a34e704d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,15 @@ All notable changes to GraphCompose are documented here. Versions follow semantic versioning; release dates are ISO 8601. +## v1.6.5 — Planned + +### Templates v2 + +- Added the `CenteredHeadline` CV preset to the `cv/v2` layered + template surface, including its isolated theme tokens, visual + regression baselines, and reusable `Subheadline` / + `SectionHeader.flatSpacedCaps` widget support. + ## v1.6.4 — 2026-05-22 Bug fix + structured-block patch. Adds two new public Block types — diff --git a/docs/templates/v2-layered/README.md b/docs/templates/v2-layered/README.md index d9f484f4..765e91cc 100644 --- a/docs/templates/v2-layered/README.md +++ b/docs/templates/v2-layered/README.md @@ -71,7 +71,7 @@ template family from empty folder to merged PR. presets/ composition: data + theme + widgets → DocumentTemplate │ compose from ▼ -widgets/ LEGO bricks: Headline, ContactLine, SectionHeader, … +widgets/ LEGO bricks: Headline, Subheadline, ContactLine, SectionHeader, … │ delegate to read tokens from ▼ ▼ components/ internal renderers + primitives theme/ palette diff --git a/docs/templates/v2-layered/authoring-presets.md b/docs/templates/v2-layered/authoring-presets.md index 385e7328..387c44a0 100644 --- a/docs/templates/v2-layered/authoring-presets.md +++ b/docs/templates/v2-layered/authoring-presets.md @@ -1,10 +1,10 @@ # 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**. +You like the layered architecture, but the shipped presets +(`BoxedSections`, `MinimalUnderlined`, `ModernProfessional`, +`CenteredHeadline`) 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. @@ -59,9 +59,9 @@ 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. +Today, four widget classes live in +`com.demcha.compose.document.templates.cv.v2.widgets`. Each has a +small set of named variants. ### `Headline` — top-of-document name @@ -71,12 +71,19 @@ named variants. | `Headline.rightAligned(host, name, theme)` | Right-aligned plain bold (`Jane Doe`) | | `Headline.render(host, name, theme, align, spacedCaps)` | Low-level — any (alignment, transform) combo | +### `Subheadline` — secondary tagline under the name + +| Variant | Visual | +|---|---| +| `Subheadline.centeredSpacedCaps(host, text, style)` | Centred letter-spaced uppercase tagline (`P R O F E S S I O N A L T I T L E`) | + ### `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.twoRowRightAligned(host, identity, theme, bodyStyle, linkStyle, separatorStyle)` | Right-aligned address/phone row plus email/link row | | `ContactLine.render(host, identity, theme, align, order)` | Low-level — any alignment + field-order combo | ### `SectionHeader` — title above each section body @@ -86,6 +93,7 @@ named variants. | `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 | +| `SectionHeader.flatSpacedCaps(host, title, color, theme, titleStyle)` | Small left spaced-caps title in a soft colour, no panel | The separator glyph used by `ContactLine`, the bullet glyph used by `RowRenderer`, and other character-level choices come from @@ -303,9 +311,10 @@ 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(...)`. +3. **1-3 named factories** + a lower-level `.render(...)` when useful. 4. **First parameter is always `SectionBuilder host`**. -5. **Last parameter is always `CvTheme theme`**. +5. **Pass `CvTheme theme` when the widget reads shared tokens**; + pass an explicit style only when the preset owns that unique style. 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 diff --git a/docs/templates/v2-layered/quickstart.md b/docs/templates/v2-layered/quickstart.md index 8d7de664..2838b3f4 100644 --- a/docs/templates/v2-layered/quickstart.md +++ b/docs/templates/v2-layered/quickstart.md @@ -14,12 +14,13 @@ GraphCompose's templates v2 (layered) gives you: - **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. +- **Widgets as visual LEGO bricks** — `Headline`, `Subheadline`, + `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. + page flow. `BoxedSections`, `MinimalUnderlined`, + `ModernProfessional`, `CenteredHeadline` 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 @@ -87,13 +88,14 @@ Same data, different visual. That's the layering. ``` ┌─────────────────────────────────────────────────────────────┐ │ presets/ BoxedSections, MinimalUnderlined, │ -│ ModernProfessional │ +│ ModernProfessional, CenteredHeadline │ │ — composition of widgets in a page flow │ └─────────────────────────────────────────────────────────────┘ │ compose from widgets ▼ ┌─────────────────────────────────────────────────────────────┐ -│ widgets/ Headline, ContactLine, SectionHeader │ +│ widgets/ Headline, Subheadline, ContactLine, │ +│ SectionHeader │ │ — named visual LEGO bricks │ └─────────────────────────────────────────────────────────────┘ │ delegate to ↓ │ read tokens from ↓ diff --git a/docs/templates/v2-layered/using-templates.md b/docs/templates/v2-layered/using-templates.md index 94b499a9..e09eafb9 100644 --- a/docs/templates/v2-layered/using-templates.md +++ b/docs/templates/v2-layered/using-templates.md @@ -159,8 +159,9 @@ CvDocument doc = CvDocument.builder() ``` **Single-column presets** (`BoxedSections`, `MinimalUnderlined`, -`ModernProfessional`) render only `Slot.MAIN`. Sidebar content is -silently dropped — switch to a multi-column preset to render it. +`ModernProfessional`, `CenteredHeadline`) 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. @@ -170,13 +171,14 @@ preset renders them. The slot model is opt-in. ## Picking a preset -Three shipped today: +Four 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 | +| `CenteredHeadline.create()` | Centred spaced-caps name, small subheadline, full-width rules around contact and modules | Each factory has a no-arg form (uses a sensible default theme) and a `create(CvTheme)` form (custom theme). 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 9a0bac7f..a762b8c0 100644 --- a/examples/src/main/java/com/demcha/examples/support/ExampleDataFactory.java +++ b/examples/src/main/java/com/demcha/examples/support/ExampleDataFactory.java @@ -504,17 +504,17 @@ public static CvDocument sampleCvDocumentV2() { .builder("Education & Certifications") .entry("MSc Computer Science", "University of Manchester", - "2021", + "2020-2021", "Distinction. Thesis: *Composable layout primitives " + "for deterministic document rendering*.") .entry("BSc Software Engineering", "Imperial College London", - "2019", + "2016-2019", "First-class honours. Specialisation in compilers and " + "static analysis.") .entry("Oracle Java Certification", "Professional track", - "2023", + "2023-2024", "Java 17 platform deep-dive: records, sealed types, " + "pattern matching, virtual threads.") .build(); diff --git a/src/main/java/com/demcha/compose/document/templates/cv/v2/AUTHORS.md b/src/main/java/com/demcha/compose/document/templates/cv/v2/AUTHORS.md index 979151bc..1ee92c73 100644 --- a/src/main/java/com/demcha/compose/document/templates/cv/v2/AUTHORS.md +++ b/src/main/java/com/demcha/compose/document/templates/cv/v2/AUTHORS.md @@ -324,16 +324,23 @@ as DSL plumbing. Below is the current catalog. | Variant | Visual | Used in | |---|---|---| -| `Headline.spacedCentered(host, name, theme)` | centred letter-spaced uppercase (`J A N E D O E`) | BoxedSections, MinimalUnderlined | +| `Headline.spacedCentered(host, name, theme)` | centred letter-spaced uppercase (`J A N E D O E`) | BoxedSections, MinimalUnderlined, CenteredHeadline | | `Headline.rightAligned(host, name, theme)` | right-aligned plain bold (`Jane Doe`) | ModernProfessional | | `Headline.render(host, name, theme, align, spacedCaps)` | low-level: pick any alignment + transform | — | +### `Subheadline` — secondary tagline under the name + +| Variant | Visual | Used in | +|---|---|---| +| `Subheadline.centeredSpacedCaps(host, text, style)` | centred letter-spaced uppercase tagline | CenteredHeadline | + ### `ContactLine` — phone / email / address / links row | Variant | Visual | Used in | |---|---|---| -| `ContactLine.centered(host, identity, theme)` | centred, phone → email → address → links | BoxedSections, MinimalUnderlined | +| `ContactLine.centered(host, identity, theme)` | centred, phone → email → address → links | BoxedSections, MinimalUnderlined, CenteredHeadline | | `ContactLine.rightAligned(host, identity, theme)` | right-aligned, address → phone → email → links | ModernProfessional | +| `ContactLine.twoRowRightAligned(host, identity, theme, bodyStyle, linkStyle, separatorStyle)` | right-aligned address/phone row plus email/link row | ModernProfessional | | `ContactLine.render(host, identity, theme, align, order)` | low-level: pick alignment + field order | — | The separator glyph comes from @@ -347,11 +354,12 @@ change ` | ` to ` · ` or anything else. | `SectionHeader.banner(host, title, theme)` | pale-grey panel with centred spaced-caps inside | BoxedSections | | `SectionHeader.underlined(host, title, theme)` | small spaced-caps left-aligned, thin rule below | MinimalUnderlined | | `SectionHeader.flat(host, title, color, theme)` | large bold title in a given colour, no panel | ModernProfessional | +| `SectionHeader.flatSpacedCaps(host, title, color, theme, titleStyle)` | small spaced-caps title in a soft colour, no panel | CenteredHeadline | -Note that `flat` takes a `DocumentColor` argument — the section -title colour is the preset's signature accent, and the widget -deliberately surfaces it as a parameter rather than burying it in -the theme. +Note that `flat` and `flatSpacedCaps` take a `DocumentColor` +argument — the section title colour is the preset's signature +accent, and the widget deliberately surfaces it as a parameter +rather than burying it in the theme. ### Composing a preset from widgets 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 0fa4f9e3..45d7007b 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 @@ -20,15 +20,19 @@ * │ presets/ │ * │ BoxedSections ← composition: data + theme + render │ * │ MinimalUnderlined ← another composition, same pieces │ - * │ ModernProfessional ← third preset, partial widget use │ + * │ ModernProfessional ← corporate composition variant │ + * │ CenteredHeadline ← classic centred headline variant │ * └─────────────────────────────────────────────────────────────┘ * │ compose from * ▼ * ┌─────────────────────────────────────────────────────────────┐ * │ widgets/ ← named visual building blocks (LEGO bricks) │ * │ Headline .spacedCentered | .rightAligned │ - * │ ContactLine .centered | .rightAligned │ + * │ Subheadline .centeredSpacedCaps │ + * │ ContactLine .centered | .rightAligned │ + * │ .twoRowRightAligned │ * │ SectionHeader .banner | .underlined | .flat │ + * │ .flatSpacedCaps │ * └─────────────────────────────────────────────────────────────┘ * │ delegate to │ read tokens from * ▼ ▼ @@ -70,8 +74,9 @@ *
Visual signature ported from the legacy v1 preset:
+ *The preset reuses three existing widgets ({@link Headline}, + * {@link ContactLine}, {@link SectionHeader#flatSpacedCaps}) and one + * newly introduced widget ({@link Subheadline}). The thin rules + * between sections are emitted as inline {@code pageFlow.addLine(...)} + * calls because they are part of the page-flow composition, not a + * section-scoped widget — turning them into a widget would force every + * caller to round-trip through a {@link com.demcha.compose.document.dsl.SectionBuilder}, + * which is the wrong granularity for a single-line ornament.
+ * + *Why the subline says "PROFESSIONAL TITLE" verbatim: + * the v2 {@link com.demcha.compose.document.templates.cv.v2.data.CvIdentity} + * model does not carry a {@code jobTitle} field today — the v1 preset + * hard-coded the same string for visual signature purposes and we + * preserve that exactly to keep the baseline matching. When a + * {@code jobTitle} field is introduced, swap the hard-coded string + * for {@code doc.identity().jobTitle()} (with a sensible fallback).
+ */ +public final class CenteredHeadline { + + /** Stable template identifier. */ + public static final String ID = "centered-headline"; + + /** Human-readable display name. */ + public static final String DISPLAY_NAME = "Centered Headline"; + + /** Recommended page margin (in points) — matches the legacy v1 preset. */ + public static final double RECOMMENDED_MARGIN = 28.0; + + /** + * Fixed subheadline text. Matches the v1 preset's hard-coded + * caption verbatim — see class Javadoc for rationale. + */ + private static final String SUBHEADLINE_CAPTION = "Professional Title"; + + private CenteredHeadline() { + // utility class — not instantiable + } + + /** + * Builds the preset with the classic Centered Headline theme + * ({@link CvTheme#centeredHeadline()}). + */ + public static DocumentTemplateThe {@code banner} slot is required by the record but unused + * by the Centered Headline visual signature (no banner panels); + * we reuse the classic banner colour so themes can swap the body + * style without leaving an obvious gap if a future preset reuses + * this palette with a banner-style section header.
+ */ + public static CvPalette centeredHeadline() { + return new CvPalette( + DocumentColor.rgb(54, 54, 54), // ink (#363636) + DocumentColor.rgb(105, 105, 105), // muted / soft (#696969) + DocumentColor.rgb(188, 188, 188), // rule (#BCBCBC) + DocumentColor.rgb(220, 226, 230)); // banner (inherits classic) + } } diff --git a/src/main/java/com/demcha/compose/document/templates/cv/v2/theme/CvSpacing.java b/src/main/java/com/demcha/compose/document/templates/cv/v2/theme/CvSpacing.java index 97288def..c900d5bb 100644 --- a/src/main/java/com/demcha/compose/document/templates/cv/v2/theme/CvSpacing.java +++ b/src/main/java/com/demcha/compose/document/templates/cv/v2/theme/CvSpacing.java @@ -117,6 +117,32 @@ public static CvSpacing classic() { 3.0); // entrySeparation } + /** + * Spacing for the {@code CenteredHeadline} preset — designed for a + * classic single-column resume with thin full-width rules above + * and below the contact line, and small inter-module rules between + * sections. Banner-panel fields are left at sensible defaults but + * unused (the preset uses {@code flatSpacedCaps} section headers, + * not banners). + */ + public static CvSpacing centeredHeadline() { + return new CvSpacing( + 0, // pageFlowSpacing (zero — rules supply visual gaps) + 1.5, // sectionBodySpacing + DocumentInsets.zero(), // sectionBodyPadding + new DocumentInsets(8, 0, 0, 0), // headlinePadding (small top breathing room) + new DocumentInsets(7, 0, 7, 0), // contactPadding + 0.0, // bannerCornerRadius (unused) + 5.0, // bannerInnerPadding (unused) + DocumentInsets.top(0), // bannerMargin (unused) + 0.55, // accentRuleWidth (thin v1 rule) + 1.2, // paragraphMarginTop + 8.0, // entryHeaderRowSpacing + 1.0, // entryTitleWeight + 0.45, // entryDateWeight + 3.0); // entrySeparation + } + /** * Tighter spacing for the Modern Professional preset — no banner * panels, denser body, single-page-friendly proportions. diff --git a/src/main/java/com/demcha/compose/document/templates/cv/v2/theme/CvTheme.java b/src/main/java/com/demcha/compose/document/templates/cv/v2/theme/CvTheme.java index a7cb88e3..4652c83e 100644 --- a/src/main/java/com/demcha/compose/document/templates/cv/v2/theme/CvTheme.java +++ b/src/main/java/com/demcha/compose/document/templates/cv/v2/theme/CvTheme.java @@ -87,6 +87,21 @@ public static CvTheme modernProfessional() { CvDecoration.classic()); } + /** + * The "Centered Headline" classic look ported from the v1 preset + * of the same name — Poppins headline, Lato body, soft greyscale + * palette, thin full-width rules separating headline / contact / + * each module. Pipe contact separator matches the classic + * decoration. + */ + public static CvTheme centeredHeadline() { + return new CvTheme( + CvPalette.centeredHeadline(), + CvTypography.centeredHeadline(), + CvSpacing.centeredHeadline(), + CvDecoration.classic()); + } + // -- pre-built text-style helpers ------------------------------------ // Renderers ask the theme for an already-composed DocumentTextStyle // instead of re-assembling font + size + decoration + colour every diff --git a/src/main/java/com/demcha/compose/document/templates/cv/v2/theme/CvTypography.java b/src/main/java/com/demcha/compose/document/templates/cv/v2/theme/CvTypography.java index 1e4dea62..7de1427b 100644 --- a/src/main/java/com/demcha/compose/document/templates/cv/v2/theme/CvTypography.java +++ b/src/main/java/com/demcha/compose/document/templates/cv/v2/theme/CvTypography.java @@ -78,4 +78,28 @@ public static CvTypography modernProfessional() { 10.0, // body 1.35); // line spacing } + + /** + * Poppins headline + Lato body scale ported from the original + * {@code CenteredHeadline} v1 preset. The headline is the page's + * loudest element (24pt spaced caps); everything else is at + * 8-9pt for a classic-resume density. + * + *{@code sizeBanner} feeds the + * {@link com.demcha.compose.document.templates.cv.v2.widgets.SectionHeader#flatSpacedCaps} + * variant — small bold spaced-caps title in the soft palette + * tone.
+ */ + public static CvTypography centeredHeadline() { + return new CvTypography( + FontName.POPPINS, FontName.LATO, + 24.0, // headline (spaced-caps name) + 8.3, // contact + 9.5, // banner (used as small spaced-caps section title) + 8.8, // entry title + 8.6, // entry date + 8.4, // entry subtitle + 8.7, // body + 1.45); // line spacing + } } diff --git a/src/main/java/com/demcha/compose/document/templates/cv/v2/widgets/SectionHeader.java b/src/main/java/com/demcha/compose/document/templates/cv/v2/widgets/SectionHeader.java index 260ce84f..f5239886 100644 --- a/src/main/java/com/demcha/compose/document/templates/cv/v2/widgets/SectionHeader.java +++ b/src/main/java/com/demcha/compose/document/templates/cv/v2/widgets/SectionHeader.java @@ -23,6 +23,11 @@ *Unlike {@link Headline} (one rendering shape, two text @@ -96,4 +101,45 @@ public static void flat(SectionBuilder host, String title, .align(TextAlign.LEFT) .margin(DocumentInsets.zero())); } + + /** + * Small left-aligned spaced-caps bold title in a given colour. No + * panel, no rule — flat like {@link #flat} but typographically + * quieter: body font, body-sized, transformed to letter-spaced + * uppercase via {@link TextOrnaments#spacedUpper(String)}. Visual + * signature of {@code CenteredHeadline}. + * + *
If the {@code titleStyle} parameter is {@code null} the widget + * derives a style from the theme's body typography + * ({@code bodyFont} at {@code sizeBanner} weight, bold, the given + * colour). Pass an explicit style when the preset needs a specific + * font / size combination that doesn't map to a theme slot.
+ * + * @param host host section + * @param title verbatim title text (transformed to spaced caps + * by the widget) + * @param color title colour — typically a soft / muted accent + * @param theme active theme (used as the style default and + * for the padding token) + * @param titleStyle explicit style override; pass {@code null} to + * fall back to the theme-derived default + */ + public static void flatSpacedCaps(SectionBuilder host, String title, + DocumentColor color, CvTheme theme, + DocumentTextStyle titleStyle) { + DocumentTextStyle resolved = titleStyle != null + ? titleStyle + : DocumentTextStyle.builder() + .fontName(theme.typography().bodyFont()) + .size(theme.typography().sizeBanner()) + .decoration(DocumentTextDecoration.BOLD) + .color(color) + .build(); + host.padding(new DocumentInsets(0, 0, 0, 0)) + .addParagraph(p -> p + .text(TextOrnaments.spacedUpper(title)) + .textStyle(resolved) + .align(TextAlign.LEFT) + .margin(DocumentInsets.zero())); + } } diff --git a/src/main/java/com/demcha/compose/document/templates/cv/v2/widgets/Subheadline.java b/src/main/java/com/demcha/compose/document/templates/cv/v2/widgets/Subheadline.java new file mode 100644 index 00000000..60b6e59d --- /dev/null +++ b/src/main/java/com/demcha/compose/document/templates/cv/v2/widgets/Subheadline.java @@ -0,0 +1,66 @@ +package com.demcha.compose.document.templates.cv.v2.widgets; + +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.DocumentTextStyle; +import com.demcha.compose.document.templates.cv.v2.components.TextOrnaments; + +/** + * Secondary headline widget — the small spaced-caps tagline rendered + * directly beneath the main {@link Headline} (e.g. + * {@code P R O F E S S I O N A L T I T L E}). + * + *Reach for {@code Subheadline} when a preset stacks a quieter + * descriptor under the subject's name — typical of classic / + * editorial layouts where the headline block reads as two centred + * lines: a loud name and a soft caption. Visual signature of + * {@code CenteredHeadline}; will likely fit + * {@code EditorialBlue} when ported.
+ * + *If a future preset wants a right-aligned or verbatim (non-spaced) + * subheadline, add a sibling factory here — keep the + * {@code (host, text, style)} signature shape so call sites stay + * uniform. Padding is supplied by the caller via {@link SectionBuilder} + * because subheadline insets are part of the headline block's vertical + * rhythm, not a per-widget concern.
+ */ +public final class Subheadline { + + private Subheadline() { + } + + /** + * Centred letter-spaced uppercase subheadline. Text is transformed + * through {@link TextOrnaments#spacedUpper(String)} — pass the raw + * caption ({@code "Professional Title"}) and the widget handles the + * spacing. + * + * @param host host section (typically the same section that + * hosts the main {@link Headline}) + * @param text caption text to render, before the spaced-caps + * transform + * @param style explicit text style — the subheadline has no + * dedicated theme slot, so the caller composes + * {@code font + size + decoration + colour} and hands + * it in. Centralise the style in the preset, not at + * each call site. + */ + public static void centeredSpacedCaps(SectionBuilder host, String text, + DocumentTextStyle style) { + host.addParagraph(p -> p + .text(TextOrnaments.spacedUpper(text)) + .textStyle(style) + .align(TextAlign.CENTER) + .margin(DocumentInsets.top(1))); + } +} diff --git a/src/main/java/com/demcha/compose/document/templates/cv/v2/widgets/package-info.java b/src/main/java/com/demcha/compose/document/templates/cv/v2/widgets/package-info.java index 90ce6ac5..308012f6 100644 --- a/src/main/java/com/demcha/compose/document/templates/cv/v2/widgets/package-info.java +++ b/src/main/java/com/demcha/compose/document/templates/cv/v2/widgets/package-info.java @@ -5,7 +5,7 @@ * (addParagraph / softPanel / accentBottom) and CV-specific * renderers in {@code components/}. Each widget captures one * visual idea (a headline, a contact line, a section - * header) with two or three named variants, so a preset author + * header) with a small set of named variants, so a preset author * composes a page by picking widgets instead of * writing rendering DSL by hand. * @@ -44,12 +44,16 @@ *Each widget delegates internally to the lower-level renderers
diff --git a/src/test/java/com/demcha/compose/document/templates/cv/v2/presets/CvV2VisualParityTest.java b/src/test/java/com/demcha/compose/document/templates/cv/v2/presets/CvV2VisualParityTest.java
index c2c469d8..b4fe7583 100644
--- a/src/test/java/com/demcha/compose/document/templates/cv/v2/presets/CvV2VisualParityTest.java
+++ b/src/test/java/com/demcha/compose/document/templates/cv/v2/presets/CvV2VisualParityTest.java
@@ -92,7 +92,10 @@ private static Stream