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
24 changes: 22 additions & 2 deletions docs/templates/v2-layered/authoring-presets.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,11 @@ visual decision you can read like a recipe.
<a id="the-widget-catalog"></a>
## The widget catalog

Today, four widget classes live in
The CV widget classes live in
`com.demcha.compose.document.templates.cv.v2.widgets`. Each has a
small set of named variants.
small set of named variants. Generic widgets that can be reused by
CVs, proposals, invoices, and cover letters live one package higher
in `com.demcha.compose.document.templates.widgets`.

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

Expand Down Expand Up @@ -102,6 +104,24 @@ small set of named variants.
| `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 |
| `SectionHeader.tickLabel(host, title, theme, color, tickWidth[, titleStyle])` | Short accent tick above compact uppercase label |
| `SectionHeader.upperRule(host, title, theme, titleStyle, ruleColor, ruleWidth)` | Uppercase label with short rule below |
| `SectionHeader.spacedCapsRule(host, title, theme, titleStyle, ruleColor, ruleWidth, ruleThickness, ruleMargin)` | Spaced-caps label with short rule below |

### Higher-order CV widgets

| Widget | Visual |
|---|---|
| `Masthead.centered(host, identity, theme, style)` | Centred editorial identity block: name, optional title, metadata, link row |
| `FlowSectionHeader.banner(...)` / `FlowSectionHeader.label(...)` | Page-flow-level headers where the surrounding rules are outside the body section |
| `ProfileBand.render(...)` | Tinted/ruled summary block with markdown-aware body text |
| `SectionModule.tick(...)` / `SectionModule.upperRule(...)` | Named rail/card module that combines a section-header variant with caller-supplied body content |

### Shared document widgets

| Widget | Visual |
|---|---|
| `TableWidget.fixed(...)` / `TableWidget.grid(...)` | Configurable tables/grids with borders, fills, zebra rows, padding, typography, and column count |
| `CardWidget.render(...)` | Reusable card/container shell with spacing, padding, fill, stroke, and corner radius |

The separator glyph used by `ContactLine`, the bullet glyph used by
`RowRenderer`, and other character-level choices come from
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,14 @@ change ` | ` to ` · ` or anything else.
| `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, ClassicSerif |
| `SectionHeader.tickLabel(host, title, theme, color, tickWidth[, titleStyle])` | short accent tick above compact uppercase label | CompactMono |
| `SectionHeader.upperRule(host, title, theme, titleStyle, ruleColor, ruleWidth)` | uppercase label with short rule below | NordicClean |
| `SectionHeader.spacedCapsRule(host, title, theme, titleStyle, ruleColor, ruleWidth, ruleThickness, ruleMargin)` | spaced-caps label with short rule below | ClassicSerif |

Use `FlowSectionHeader` when the rule/title treatment belongs to the
page flow rather than inside an existing body section. `BlueBanner`
uses its filled-banner variant; `EditorialBlue` uses its ruled-label
variant. Use `SectionModule` when a rail/card module is simply
`SectionHeader` plus caller-supplied content.

Note that `flat` and `flatSpacedCaps` take a `DocumentColor`
argument — the section title colour is the preset's signature
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.demcha.compose.document.templates.cv.v2.components;

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.font.FontName;

/**
* Small factory for preset-local text styles.
*/
public final class CvTextStyles {
private CvTextStyles() {
}

public static DocumentTextStyle of(FontName font,
double size,
DocumentTextDecoration decoration,
DocumentColor color) {
return DocumentTextStyle.builder()
.fontName(font)
.size(size)
.decoration(decoration)
.color(color)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
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.style.DocumentTextStyle;
import com.demcha.compose.document.templates.cv.v2.data.CvEntry;

import java.util.Locale;

/**
* Compact entry renderer for editorial/card/rail presets where title,
* subtitle, and date are packed tighter than the canonical
* two-column {@link EntryRenderer}.
*/
public final class EntryCompactRenderer {
private EntryCompactRenderer() {
}

public static void twoColumnTitleDateBody(SectionBuilder host,
CvEntry entry,
String rowName,
DocumentTextStyle titleStyle,
DocumentTextStyle dateStyle,
DocumentTextStyle subtitleStyle,
DocumentTextStyle bodyStyle,
double rowSpacing,
double titleWeight,
double dateWeight,
DocumentInsets subtitleMargin,
DocumentInsets bodyMargin,
double bodyLineSpacing,
boolean uppercaseTitle) {
host.addRow(rowName, row -> row
.spacing(rowSpacing)
.weights(titleWeight, dateWeight)
.addSection("Title", titleColumn -> titleColumn
.padding(DocumentInsets.zero())
.addParagraph(paragraph -> paragraph
.text(formattedTitle(entry.title(),
uppercaseTitle))
.textStyle(titleStyle)
.align(TextAlign.LEFT)
.margin(DocumentInsets.zero())))
.addSection("Date", dateColumn -> dateColumn
.padding(DocumentInsets.zero())
.addParagraph(paragraph -> paragraph
.text(MarkdownInline.plainText(entry.date()))
.textStyle(dateStyle)
.align(TextAlign.RIGHT)
.margin(DocumentInsets.zero()))));

if (!entry.subtitle().isBlank()) {
host.addParagraph(paragraph -> paragraph
.text(MarkdownInline.plainText(entry.subtitle()))
.textStyle(subtitleStyle)
.align(TextAlign.LEFT)
.margin(subtitleMargin));
}
RichParagraphRenderer.render(host, entry.body(), bodyStyle,
bodyLineSpacing, bodyMargin);
}

public static void slashMeta(SectionBuilder host,
CvEntry entry,
DocumentTextStyle titleStyle,
DocumentTextStyle metaStyle,
double lineSpacing,
DocumentInsets margin) {
host.addParagraph(paragraph -> paragraph
.textStyle(titleStyle)
.lineSpacing(lineSpacing)
.align(TextAlign.LEFT)
.margin(margin)
.rich(rich -> {
rich.style(MarkdownInline.plainText(entry.title()),
titleStyle);
MarkdownInline.appendPlainIfPresent(rich, " / ",
entry.subtitle(), metaStyle);
MarkdownInline.appendPlainIfPresent(rich, " / ",
entry.date(), metaStyle);
}));
}

public static void slashSubtitleDate(SectionBuilder host,
CvEntry entry,
DocumentTextStyle titleStyle,
DocumentTextStyle subtitleStyle,
DocumentTextStyle dateStyle,
double lineSpacing,
DocumentInsets margin) {
host.addParagraph(paragraph -> paragraph
.textStyle(titleStyle)
.lineSpacing(lineSpacing)
.align(TextAlign.LEFT)
.margin(margin)
.rich(rich -> {
rich.style(MarkdownInline.plainText(entry.title()),
titleStyle);
MarkdownInline.appendPlainIfPresent(rich, " / ",
entry.subtitle(), subtitleStyle);
MarkdownInline.appendPlainIfPresent(rich, " / ",
entry.date(), dateStyle);
}));
}

public static void titleDateBody(SectionBuilder host,
CvEntry entry,
DocumentTextStyle titleStyle,
DocumentTextStyle dateStyle,
DocumentTextStyle subtitleStyle,
DocumentTextStyle bodyStyle,
String datePrefix,
double headerLineSpacing,
DocumentInsets headerMargin,
DocumentInsets subtitleMargin,
DocumentInsets bodyMargin,
double bodyLineSpacing,
boolean uppercaseTitle) {
host.addParagraph(paragraph -> paragraph
.textStyle(titleStyle)
.lineSpacing(headerLineSpacing)
.align(TextAlign.LEFT)
.margin(headerMargin)
.rich(rich -> {
rich.style(formattedTitle(entry.title(), uppercaseTitle),
titleStyle);
if (!entry.date().isBlank()) {
rich.style(datePrefix, titleStyle);
rich.style(MarkdownInline.plainText(entry.date()),
dateStyle);
}
}));
if (!entry.subtitle().isBlank()) {
host.addParagraph(paragraph -> paragraph
.text(MarkdownInline.plainText(entry.subtitle()))
.textStyle(subtitleStyle)
.align(TextAlign.LEFT)
.margin(subtitleMargin));
}
RichParagraphRenderer.render(host, entry.body(), bodyStyle,
bodyLineSpacing, bodyMargin);
}

public static void titleSubtitleDateBody(SectionBuilder host,
CvEntry entry,
DocumentTextStyle titleStyle,
DocumentTextStyle subtitleStyle,
DocumentTextStyle dateStyle,
DocumentTextStyle bodyStyle,
String subtitlePrefix,
String datePrefix,
double headerLineSpacing,
DocumentInsets headerMargin,
DocumentInsets bodyMargin,
double bodyLineSpacing) {
host.addParagraph(paragraph -> paragraph
.textStyle(titleStyle)
.lineSpacing(headerLineSpacing)
.align(TextAlign.LEFT)
.margin(headerMargin)
.rich(rich -> {
rich.style(MarkdownInline.plainText(entry.title()),
titleStyle);
MarkdownInline.appendPlainIfPresent(rich, subtitlePrefix,
entry.subtitle(), subtitleStyle);
MarkdownInline.appendPlainIfPresent(rich, datePrefix,
entry.date(), dateStyle);
}));
RichParagraphRenderer.render(host, entry.body(), bodyStyle,
bodyLineSpacing, bodyMargin);
}

private static String formattedTitle(String title, boolean uppercase) {
String clean = MarkdownInline.plainText(title);
return uppercase ? clean.toUpperCase(Locale.ROOT) : clean;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
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.style.DocumentTextStyle;

/**
* Renders compact "Label: rich markdown value" rows used by CV detail sections.
*/
public final class LabelValueRenderer {
private LabelValueRenderer() {
}

public static void render(SectionBuilder host,
String label,
String value,
DocumentTextStyle labelStyle,
DocumentTextStyle valueStyle,
double lineSpacing,
DocumentInsets margin) {
host.addParagraph(paragraph -> paragraph
.textStyle(valueStyle)
.lineSpacing(lineSpacing)
.align(TextAlign.LEFT)
.margin(margin)
.rich(rich -> {
rich.style(normalizedLabel(label) + ":", labelStyle);
if (value != null && !value.isBlank()) {
rich.style(" ", valueStyle);
MarkdownInline.append(rich, value, valueStyle);
}
}));
}

static String normalizedLabel(String label) {
String value = MarkdownInline.plainText(label).trim();
while (value.endsWith(":")) {
value = value.substring(0, value.length() - 1).trim();
}
return value;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,30 @@ public static void append(RichText rich, String text,
rich.style(textRun.text(), runStyle);
}
}

public static void appendTrimmed(RichText rich, String text,
DocumentTextStyle baseStyle) {
append(rich, text == null ? "" : text.trim(), baseStyle);
}

public static void appendPlainIfPresent(RichText rich, String prefix,
String value,
DocumentTextStyle style) {
String clean = plainText(value);
if (!clean.isBlank()) {
rich.style(prefix + clean, style);
}
}

public static String plainText(String value) {
if (value == null) {
return "";
}
return value
.replace("**", "")
.replace("__", "")
.replace("`", "")
.replace("*", "")
.replace("_", "");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.demcha.compose.document.templates.cv.v2.components;

/**
* Splits legacy project labels like "GraphCompose (Java, PDFBox)" into display title and stack.
*/
public record ProjectLabel(String title, String stack) {
public ProjectLabel {
title = title == null ? "" : title;
stack = stack == null ? "" : stack;
}

public static ProjectLabel parse(String value) {
String clean = MarkdownInline.plainText(value).trim();
int stackOpen = clean.lastIndexOf('(');
if (stackOpen > 0 && clean.endsWith(")")) {
return new ProjectLabel(
clean.substring(0, stackOpen).trim(),
clean.substring(stackOpen + 1, clean.length() - 1).trim()
);
}
return new ProjectLabel(clean, "");
}
}
Loading