Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 120 additions & 0 deletions src/main/java/com/demcha/compose/document/templates/cv/v2/AUTHORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,126 @@ sidebar content to flow inline with main.

---

## Widget cookbook — the LEGO bricks

When you build a preset, you compose your `compose()` method from
**widgets** that live in
`com.demcha.compose.document.templates.cv.v2.widgets`. Each widget
captures one visual idea, with named variants per visual style.

This means your preset reads as a sequence of visual decisions, not
as DSL plumbing. Below is the current catalog.

### `Headline` — top-of-document name

| Variant | Visual | Used in |
|---|---|---|
| `Headline.spacedCentered(host, name, theme)` | centred letter-spaced uppercase (`J A N E D O E`) | BoxedSections, MinimalUnderlined |
| `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 | — |

### `ContactLine` — phone / email / address / links row

| Variant | Visual | Used in |
|---|---|---|
| `ContactLine.centered(host, identity, theme)` | centred, phone → email → address → links | BoxedSections, MinimalUnderlined |
| `ContactLine.rightAligned(host, identity, theme)` | right-aligned, address → phone → email → links | ModernProfessional |
| `ContactLine.render(host, identity, theme, align, order)` | low-level: pick alignment + field order | — |

The separator glyph comes from
`theme.decoration().contactSeparator()` — swap `CvDecoration` to
change ` | ` to ` · ` or anything else.

### `SectionHeader` — title above a section body

| Variant | Visual | Used in |
|---|---|---|
| `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 |

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.

### Composing a preset from widgets

A typical preset's `compose()` becomes a sequence of widget calls:

```java
@Override
public void compose(DocumentSession document, CvDocument doc) {
PageFlowBuilder pageFlow = 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 whole preset.** Twelve lines. No paragraph DSL, no
custom rendering helpers.

### When the widget doesn't fit

Widgets are optional, not mandatory. When a preset needs something
no widget covers (e.g. unusual link colours, special separator,
custom alignment combo), **inline it**:

```java
.addSection("Contact", section -> {
DocumentTextStyle myStyle = DocumentTextStyle.builder()
.fontName(FontName.HELVETICA)
.color(DocumentColor.rgb(65, 105, 225))
.build();

section.addParagraph(p -> p
.text(doc.identity().contact().email())
.textStyle(myStyle)
.align(TextAlign.RIGHT));
})
```

Inline is fine for one-off needs. If the same inline pattern shows
up in **2+ presets**, that's the signal to extract a new widget
variant or add a parameter to an existing one. Don't pre-extract.

### Adding a new widget — the test of when

| Pattern repetition | Action |
|---|---|
| 1 preset only | Inline. Leave it alone. |
| 2 presets | Add a new factory method to an existing widget, OR add a parameter. |
| 3+ presets | It's its own widget. New class in `cv/v2/widgets/`. |

### Examples of widgets we could add (not done yet)

These don't exist today but illustrate where the catalog could
grow. **Don't write them until a real preset needs them** —
premature widgets are noise.

- `Badge.pill(host, label, fillColor, textColor)` — labelled pill
for tags, awards, certifications.
- `IconLabel.render(host, icon, text, theme)` — small icon next
to a text label, useful for contact rows that show icons.
- `Divider.thin(host, theme)` / `.thick(...)` — horizontal rule
with configurable weight + colour.
- `TwoColumnRow.render(host, leftBuilder, rightBuilder, weights, theme)` —
generic split with weights, useful for entry headers across
presets.

---

## Recipe 7 — conditional sections (data-driven)

Real CVs often hide a section when there's nothing to put in it:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,32 +1,29 @@
package com.demcha.compose.document.templates.cv.v2.components;

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.templates.cv.v2.theme.CvTheme;
import com.demcha.compose.document.templates.cv.v2.widgets.SectionHeader;

/**
* Draws the pale-grey banner row holding a centred, letter-spaced
* uppercase section title (e.g. {@code P R O J E C T S}).
*
* <p>The host {@link SectionBuilder} becomes the banner panel —
* theme tokens drive the fill colour, corner radius, inner padding,
* and surrounding margin.</p>
* @deprecated Use
* {@link com.demcha.compose.document.templates.cv.v2.widgets.SectionHeader#banner}
* instead — the widget groups the banner alongside its sibling
* variants ({@code underlined}, {@code flat}) so picking a section-
* title style becomes one choice in one place. Kept as a thin
* delegating shim so v2 code written before the widgets layer keeps
* compiling unchanged.
*/
@Deprecated
public final class BannerRenderer {

private BannerRenderer() {
}

/**
* @deprecated delegates to {@link SectionHeader#banner}.
*/
@Deprecated
public static void render(SectionBuilder section, String title, CvTheme theme) {
section.softPanel(theme.palette().banner(),
theme.spacing().bannerCornerRadius(),
theme.spacing().bannerInnerPadding())
.margin(theme.spacing().bannerMargin())
.addParagraph(p -> p
.text(TextOrnaments.spacedUpper(title))
.textStyle(theme.bannerStyle())
.align(TextAlign.CENTER)
.margin(DocumentInsets.zero()));
SectionHeader.banner(section, title, theme);
}
}
Original file line number Diff line number Diff line change
@@ -1,74 +1,29 @@
package com.demcha.compose.document.templates.cv.v2.components;

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.DocumentInsets;
import com.demcha.compose.document.style.DocumentTextStyle;
import com.demcha.compose.document.templates.cv.v2.data.CvContact;
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 com.demcha.compose.document.templates.cv.v2.widgets.ContactLine;

/**
* Draws the centred pipe-separated contact row under the headline:
* phone → email (clickable mailto) → address → optional labelled
* links in source order, each rendered as an active hyperlink.
*
* <p>Required contact fields are guaranteed non-blank by
* {@link CvContact} so the helper never needs to skip-on-empty for
* them. Optional {@link CvLink} entries are rendered if present and
* silently omitted if not — that's the whole point of the
* {@code .link(...)} builder method.</p>
* @deprecated Use
* {@link com.demcha.compose.document.templates.cv.v2.widgets.ContactLine#centered}
* instead — the widget gives you named centred/right-aligned
* variants plus a configurable field order. Kept as a thin
* delegating shim so v2 code written before the widgets layer keeps
* compiling unchanged.
*/
@Deprecated
public final class ContactRenderer {

private ContactRenderer() {
}

/**
* @deprecated delegates to {@link ContactLine#centered}.
*/
@Deprecated
public static void render(SectionBuilder section, CvIdentity identity, CvTheme theme) {
List<Part> parts = parts(identity);
DocumentTextStyle textStyle = theme.contactStyle();
DocumentTextStyle separatorStyle = theme.contactSeparatorStyle();

section.spacing(0)
.padding(theme.spacing().contactPadding())
.addParagraph(p -> p
.textStyle(textStyle)
.align(TextAlign.CENTER)
.margin(DocumentInsets.zero())
.rich(rich -> {
for (int i = 0; i < parts.size(); i++) {
Part part = parts.get(i);
if (part.link() != null) {
rich.link(part.text(), part.link());
} else {
rich.style(part.text(), textStyle);
}
if (i < parts.size() - 1) {
rich.style(theme.decoration().contactSeparator(),
separatorStyle);
}
}
}));
}

private static List<Part> parts(CvIdentity identity) {
CvContact contact = identity.contact();
List<Part> parts = new ArrayList<>(4 + identity.links().size());
parts.add(new Part(contact.phone(), null));
parts.add(new Part(contact.email(),
new DocumentLinkOptions("mailto:" + contact.email())));
parts.add(new Part(contact.address(), null));
for (CvLink link : identity.links()) {
parts.add(new Part(link.label(), new DocumentLinkOptions(link.url())));
}
return parts;
}

private record Part(String text, DocumentLinkOptions link) {
ContactLine.centered(section, identity, theme);
}
}
Original file line number Diff line number Diff line change
@@ -1,32 +1,29 @@
package com.demcha.compose.document.templates.cv.v2.components;

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.templates.cv.v2.data.CvName;
import com.demcha.compose.document.templates.cv.v2.theme.CvTheme;
import com.demcha.compose.document.templates.cv.v2.widgets.Headline;

/**
* Draws the centred letter-spaced uppercase name headline at the very
* top of a CV — the first thing a reader sees.
* @deprecated Use
* {@link com.demcha.compose.document.templates.cv.v2.widgets.Headline#spacedCentered}
* instead — the widget gives you a named API plus alignment +
* spaced-caps variants, while this class only ever did the
* centred-spaced-caps form. Kept as a thin delegating shim so v2
* code written before the widgets layer keeps compiling unchanged.
*/
@Deprecated
public final class HeadlineRenderer {

private HeadlineRenderer() {
}

/**
* @param section host section that will host the headline paragraph
* @param name full name to render
* @param theme active theme
* @deprecated delegates to {@link Headline#spacedCentered}.
*/
@Deprecated
public static void render(SectionBuilder section, CvName name, CvTheme theme) {
section.spacing(2)
.padding(theme.spacing().headlinePadding())
.addParagraph(p -> p
.text(TextOrnaments.spacedUpper(name.full()))
.textStyle(theme.headlineStyle())
.align(TextAlign.CENTER)
.margin(DocumentInsets.zero()));
Headline.spacedCentered(section, name, theme);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,25 +20,36 @@
* │ presets/ │
* │ BoxedSections ← composition: data + theme + render │
* │ MinimalUnderlined ← another composition, same pieces │
* │ ModernProfessional ← third preset, partial widget use │
* └─────────────────────────────────────────────────────────────┘
* │ uses │ uses
* │ compose from
* ▼
* ┌─────────────────────────────────────────────────────────────┐
* │ widgets/ ← named visual building blocks (LEGO bricks) │
* │ Headline .spacedCentered | .rightAligned │
* │ ContactLine .centered | .rightAligned │
* │ SectionHeader .banner | .underlined | .flat │
* └─────────────────────────────────────────────────────────────┘
* │ delegate to │ read tokens from
* ▼ ▼
* ┌──────────────────────┐ ┌──────────────────────────┐
* │ components/ │ │ theme/ │
* │ HeadlineRenderer │ reads │ CvPalette (colours) │
* │ ContactRenderer │◀────────│ CvTypography (fonts) │
* │ BannerRenderer │ │ CvSpacing (margins) │
* │ RowRenderer │ │ CvDecoration (glyphs) │
* │ EntryRenderer │ │ CvTheme (bundle) │
* │ SectionDispatcher │ └──────────────────────────┘
* │ RowRenderer │ reads │ CvPalette (colours) │
* │ EntryRenderer │◀────────│ CvTypography (fonts) │
* │ ParagraphRenderer │ │ CvSpacing (margins) │
* │ SectionDispatcher │ │ CvDecoration (glyphs) │
* │ ParagraphPrimitive│ │ CvTheme (bundle) │
* │ MarkdownInline │ └──────────────────────────┘
* │ TextOrnaments │
* └──────────────────────┘
* │ renders
* ▼
* ┌─────────────────────────────────────────────────────────────┐
* │ data/ │
* │ CvIdentity ← name, contact, optional links │
* │ CvSection ← sealed: Paragraph | Rows | Entries │
* │ CvDocument ← identity + ordered sections │
* │ CvDocument ← identity + Placement(slot, section) │
* │ Slot ← MAIN | SIDEBAR | FOOTER │
* └─────────────────────────────────────────────────────────────┘
* </pre>
*
Expand All @@ -56,6 +67,14 @@
* touching renderers. If you want a navy theme or a different
* bullet character, this is where you live.</dd>
*
* <dt><b>{@code widgets/}</b></dt>
* <dd>The LEGO bricks. <em>"Which visual building block do I
* want here — a banner, an underlined title, a right-aligned
* headline?"</em> Each widget has 2-3 named variants and a
* lower-level entry for ad-hoc parameter combinations. This
* is where most preset code lives — picking widgets and
* composing them.</dd>
*
* <dt><b>{@code components/}</b></dt>
* <dd>The reusable drawing primitives. <em>"How is a section
* row laid out?"</em> Each renderer takes a host
Expand Down
Loading