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 a8f3a5b4..6cba35f8 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
@@ -13,6 +13,60 @@ This guide answers the **"how do I…"** questions.
---
+## The scaffold is persona-neutral
+
+Nothing in the v2 API assumes a software-developer audience. The
+sample data shipped with the package happens to be a developer CV
+(Jordan Rivera, "Platform Engineer", GitHub, Projects, Tech Skills),
+but that's only because we needed *some* fixture for visual
+regression. The same builders fit any persona — skip the link / section
+types you don't need.
+
+```java
+// A primary school teacher's CV — no GitHub, no Projects, no Tech Skills
+CvDocument.builder()
+ .identity(CvIdentity.builder()
+ .name("Maria", "Lopez")
+ .contact("+34 600 000 000", "maria@example.com", "Madrid, Spain")
+ // no .link(...) calls — she has no public profiles, and that is fine
+ .build())
+ .section(new ParagraphSection("About Me",
+ "Primary school teacher with 12 years' experience in literacy " +
+ "and inclusive education."))
+ .section(EntriesSection.builder("Teaching Experience")
+ .entry("Lead Teacher Y3", "Colegio Santa Ana", "2018-Present",
+ "Year-3 lead teacher; designed the school's reading-rota " +
+ "and mentored two newly-qualified teachers.")
+ .entry("Year Teacher", "Escuela Primaria Goya", "2013-2018",
+ "Y1-Y2 generalist; led the SEN reading-club after hours.")
+ .build())
+ .section(RowsSection.builder("Languages", RowStyle.PLAIN)
+ .row("Spanish", "Native")
+ .row("English", "Fluent (CEFR C1)")
+ .row("Catalan", "Conversational")
+ .build())
+ .section(RowsSection.builder("Certifications", RowStyle.PLAIN)
+ .row("Inclusive Education", "Universidad Complutense, 2020")
+ .row("Children's First Aid", "Cruz Roja, 2022")
+ .build())
+ .build();
+```
+
+Notice what's absent:
+
+- **No `link(...)` calls.** Optional, simply omitted.
+- **No Projects, no Skills.** The three section types
+ (`ParagraphSection`, `RowsSection`, `EntriesSection`) work for any
+ content shape — you choose what to put in them and what to call
+ them.
+- **No required IT vocabulary anywhere.** Section titles are free
+ strings (`"About Me"`, `"Teaching Experience"`, `"Certifications"`).
+
+The rest of this guide leans on dev-style examples for continuity,
+but every recipe below works the same way for any persona.
+
+---
+
## Recipe 1 — change a bullet glyph
You want `▶` instead of `•`, or numbered bullets, or em-dashes.
@@ -211,7 +265,52 @@ guard.)
---
-## Recipe 6 — conditional sections (data-driven)
+## Recipe 6 — place sections in slots (sidebar / footer)
+
+A `CvDocument` is not just a flat list of sections — every section is
+**placed** into a `Slot` (one of `MAIN`, `SIDEBAR`, `FOOTER`).
+Single-column presets like `BoxedSections` read only `Slot.MAIN`
+sections; multi-column presets read whichever slots they support.
+
+**Placing a section into a slot** (builder API):
+
+```java
+CvDocument doc = CvDocument.builder()
+ .identity(identity)
+ .section(summary) // defaults to MAIN
+ .section(Slot.MAIN, technicalSkills) // explicit, same as above
+ .section(Slot.SIDEBAR, languagesSpoken) // sidebar column
+ .section(Slot.SIDEBAR, certifications) // sidebar column
+ .sections(Slot.MAIN, experience, projects) // varargs in a slot
+ .build();
+```
+
+**Reading by slot** (in a preset's `compose()`):
+
+```java
+// Single-column preset — render only MAIN, drop the rest
+List mainSections = doc.sectionsIn(Slot.MAIN);
+
+// Two-column preset
+List mainSections = doc.sectionsIn(Slot.MAIN);
+List sidebarSections = doc.sectionsIn(Slot.SIDEBAR);
+```
+
+**Why this matters:** when the user feeds your document to a
+single-column preset, sidebar sections are silently dropped (they
+have no place to render). When the same document goes through a
+two-column preset, sidebar sections show up in the sidebar column.
+The data model is the same — only the preset's interpretation
+differs.
+
+**Tip:** `doc.sections()` returns *every* section in source order
+regardless of slot. Use it for debug printing or unfiltered
+iteration — not in a preset's render loop unless you really want
+sidebar content to flow inline with main.
+
+---
+
+## Recipe 7 — conditional sections (data-driven)
Real CVs often hide a section when there's nothing to put in it:
diff --git a/src/main/java/com/demcha/compose/document/templates/cv/v2/data/CvDocument.java b/src/main/java/com/demcha/compose/document/templates/cv/v2/data/CvDocument.java
index de294886..bd214778 100644
--- a/src/main/java/com/demcha/compose/document/templates/cv/v2/data/CvDocument.java
+++ b/src/main/java/com/demcha/compose/document/templates/cv/v2/data/CvDocument.java
@@ -6,27 +6,127 @@
/**
* Root of the v2 CV data model — required {@link CvIdentity} block
- * plus an ordered list of {@link CvSection} entries that render in
- * source order beneath the header.
+ * plus an ordered list of section {@link Placement}s.
*
- * This record carries no styling or rendering decision. Pair it
- * with a
- * {@link com.demcha.compose.document.templates.cv.v2.theme.CvTheme}
- * and a preset from
- * {@code com.demcha.compose.document.templates.cv.v2.presets} to
- * produce a PDF.
+ * Each {@link Placement} pairs a {@link CvSection} with a
+ * {@link Slot} so multi-column presets can decide which sections go
+ * into the sidebar vs. the main column without having to guess from
+ * section titles. Sections built without an explicit slot default to
+ * {@link Slot#MAIN}.
*
- * @param identity required identity / contact block
- * @param sections ordered sections; rendered in source order
+ * Single-column presets read {@link #sectionsIn(Slot)
+ * sectionsIn(Slot.MAIN)} and ignore the rest; multi-column presets
+ * call {@code sectionsIn} once per slot they support.
+ *
+ * @param identity required identity / contact block
+ * @param placements ordered placements; render order matches source
+ * order within each slot
*/
-public record CvDocument(CvIdentity identity, List sections) {
+public record CvDocument(CvIdentity identity, List placements) {
public CvDocument {
Objects.requireNonNull(identity, "identity");
- Objects.requireNonNull(sections, "sections");
- sections = List.copyOf(sections);
+ Objects.requireNonNull(placements, "placements");
+ placements = List.copyOf(placements);
+ }
+
+ /**
+ * Backward-compatible factory that wraps every supplied section
+ * in a {@link Slot#MAIN} placement. Provided so callers
+ * migrating from the pre-slot {@code new CvDocument(identity,
+ * sections)} pattern have a 1:1 replacement without rewriting
+ * their construction code.
+ *
+ * Equivalent to building each section through
+ * {@link Builder#section(CvSection)} (which also defaults to
+ * {@link Slot#MAIN}). Prefer the {@link Builder} in new code —
+ * multi-column presets need explicit slot placement that the
+ * builder makes obvious at the call site.
+ *
+ * @deprecated since the slot model — prefer the {@link Builder}
+ * so non-MAIN slots are visible at the call site.
+ */
+ @Deprecated
+ public static CvDocument ofMainSections(CvIdentity identity,
+ List sections) {
+ return new CvDocument(identity, toMainPlacements(sections));
+ }
+
+ /**
+ * One section together with the slot it should render in.
+ *
+ * @param slot placement region (never null)
+ * @param section section body (never null)
+ */
+ public record Placement(Slot slot, CvSection section) {
+
+ public Placement {
+ Objects.requireNonNull(slot, "slot");
+ Objects.requireNonNull(section, "section");
+ }
+ }
+
+ // -- accessors -------------------------------------------------------
+
+ /**
+ * Returns every section in this document in source order,
+ * regardless of slot. Useful for debugging, indexing, or
+ * iterating all sections without caring where they render.
+ *
+ * Presets should usually call {@link #sectionsIn(Slot)}
+ * instead — a single-column preset that iterates this flat list
+ * will accidentally render sidebar content inline with the main
+ * flow.
+ *
+ * @return flat list of all sections, source order
+ */
+ public List sections() {
+ List out = new ArrayList<>(placements.size());
+ for (Placement p : placements) {
+ out.add(p.section());
+ }
+ return List.copyOf(out);
+ }
+
+ /**
+ * Returns the sections placed in {@code slot}, in source order.
+ * The standard call from a preset's {@code compose()} method.
+ *
+ * @param slot non-null slot
+ * @return sections placed in that slot; empty list if none
+ */
+ public List sectionsIn(Slot slot) {
+ Objects.requireNonNull(slot, "slot");
+ List out = new ArrayList<>();
+ for (Placement p : placements) {
+ if (p.slot() == slot) {
+ out.add(p.section());
+ }
+ }
+ return List.copyOf(out);
+ }
+
+ /**
+ * Returns the slot a given section was placed in. Uses identity
+ * comparison ({@code ==}) to avoid surprises when two sections
+ * happen to be record-equal but were added separately.
+ *
+ * @param section section instance to look up
+ * @return its slot, or {@link Slot#MAIN} if the section is not
+ * in this document
+ */
+ public Slot slotOf(CvSection section) {
+ Objects.requireNonNull(section, "section");
+ for (Placement p : placements) {
+ if (p.section() == section) {
+ return p.slot();
+ }
+ }
+ return Slot.MAIN;
}
+ // -- builder ---------------------------------------------------------
+
/**
* @return new fluent builder
*/
@@ -35,12 +135,16 @@ public static Builder builder() {
}
/**
- * Mutable builder. Section order in the builder is preserved 1:1
- * in the built document.
+ * Mutable builder for {@link CvDocument}.
+ *
+ * The {@code section}-family methods append to the placement
+ * list in call order. Pass a {@link Slot} as the first argument
+ * to place into a non-main slot; omit it to default to
+ * {@link Slot#MAIN}.
*/
public static final class Builder {
private CvIdentity identity;
- private final List sections = new ArrayList<>();
+ private final List placements = new ArrayList<>();
private Builder() {
}
@@ -50,37 +154,76 @@ public Builder identity(CvIdentity value) {
return this;
}
+ /**
+ * Appends one section into {@link Slot#MAIN}.
+ */
public Builder section(CvSection section) {
- this.sections.add(Objects.requireNonNull(section, "section"));
+ return section(Slot.MAIN, section);
+ }
+
+ /**
+ * Appends one section into a chosen slot.
+ */
+ public Builder section(Slot slot, CvSection section) {
+ this.placements.add(new Placement(slot, section));
return this;
}
/**
- * Varargs convenience for the common case where every section
- * is constructed up-front. Equivalent to chained
- * {@link #section(CvSection)} calls.
- *
- * @param values one or more sections in render order
- * @return this builder
+ * Varargs convenience — all sections placed in
+ * {@link Slot#MAIN}.
*/
public Builder sections(CvSection... values) {
Objects.requireNonNull(values, "values");
for (CvSection s : values) {
- section(s);
+ section(Slot.MAIN, s);
+ }
+ return this;
+ }
+
+ /**
+ * Varargs convenience — all supplied sections placed in
+ * {@code slot}.
+ */
+ public Builder sections(Slot slot, CvSection... values) {
+ Objects.requireNonNull(slot, "slot");
+ Objects.requireNonNull(values, "values");
+ for (CvSection s : values) {
+ section(slot, s);
}
return this;
}
+ /**
+ * List variant — all sections placed in {@link Slot#MAIN}.
+ */
public Builder sections(List values) {
Objects.requireNonNull(values, "values");
for (CvSection s : values) {
- section(s);
+ section(Slot.MAIN, s);
}
return this;
}
+ /**
+ * Appends a pre-built {@link Placement}.
+ */
+ public Builder placement(Placement placement) {
+ this.placements.add(Objects.requireNonNull(placement, "placement"));
+ return this;
+ }
+
public CvDocument build() {
- return new CvDocument(identity, sections);
+ return new CvDocument(identity, placements);
+ }
+ }
+
+ private static List toMainPlacements(List sections) {
+ Objects.requireNonNull(sections, "sections");
+ List out = new ArrayList<>(sections.size());
+ for (CvSection s : sections) {
+ out.add(new Placement(Slot.MAIN, s));
}
+ return out;
}
}
diff --git a/src/main/java/com/demcha/compose/document/templates/cv/v2/data/Slot.java b/src/main/java/com/demcha/compose/document/templates/cv/v2/data/Slot.java
new file mode 100644
index 00000000..da501d65
--- /dev/null
+++ b/src/main/java/com/demcha/compose/document/templates/cv/v2/data/Slot.java
@@ -0,0 +1,34 @@
+package com.demcha.compose.document.templates.cv.v2.data;
+
+/**
+ * Logical placement region inside a {@link CvDocument}. Presets read
+ * the slot of each section to decide where on the page it should
+ * appear — main column, sidebar, or footer.
+ *
+ * Single-column presets like
+ * {@code com.demcha.compose.document.templates.cv.v2.presets.BoxedSections}
+ * iterate only {@link #MAIN} sections; sections placed in
+ * {@link #SIDEBAR} or {@link #FOOTER} are silently dropped by such
+ * presets. Multi-column presets (two-column sidebar, magazine
+ * layouts) iterate {@link #MAIN} and {@link #SIDEBAR} separately to
+ * fill their columns.
+ *
+ * Sections that don't specify a slot at build time default to
+ * {@link #MAIN} — so single-column callers keep working without
+ * changes after the slot model was introduced.
+ */
+public enum Slot {
+
+ /** Primary content column. Default when no slot is specified. */
+ MAIN,
+
+ /** Sidebar / secondary column — typically narrower than MAIN. */
+ SIDEBAR,
+
+ /**
+ * Footer area below the main flow — useful for references,
+ * disclaimers, or page-bottom notes. Currently only rendered by
+ * presets that explicitly opt in.
+ */
+ FOOTER
+}
diff --git a/src/main/java/com/demcha/compose/document/templates/cv/v2/data/package-info.java b/src/main/java/com/demcha/compose/document/templates/cv/v2/data/package-info.java
index 7d3fe544..8ee6da21 100644
--- a/src/main/java/com/demcha/compose/document/templates/cv/v2/data/package-info.java
+++ b/src/main/java/com/demcha/compose/document/templates/cv/v2/data/package-info.java
@@ -39,6 +39,17 @@
* and Professional Experience interchangeably.
*
*
+ * Placement
+ *
+ * Sections live inside a {@link com.demcha.compose.document.templates.cv.v2.data.CvDocument}
+ * as ordered {@link com.demcha.compose.document.templates.cv.v2.data.CvDocument.Placement}
+ * entries. Each placement pairs a section with a
+ * {@link com.demcha.compose.document.templates.cv.v2.data.Slot}
+ * ({@code MAIN}, {@code SIDEBAR}, {@code FOOTER}). Single-column
+ * presets read only {@code MAIN}; multi-column presets read multiple
+ * slots. Sections built without an explicit slot default to
+ * {@code MAIN}, so existing call sites stay valid.
+ *
* Adding a new section type
*
*
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 ce983f0a..1fc7d5a7 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
@@ -5,6 +5,14 @@
* documents on top of GraphCompose. Below is the conceptual model
* every author should hold in their head before writing a template.
*
+ * Persona-neutral. Nothing here assumes a
+ * software-developer audience. The shipped sample data is a
+ * developer CV because we needed a fixture for visual regression,
+ * but the same builders fit a teacher, a chef, a nurse, an
+ * artist — anyone. Skip the link / section types you don't need;
+ * section titles are free strings and links are optional. See
+ * {@code AUTHORS.md} for a non-IT example.
+ *
* Four layers, one responsibility each
*
*
@@ -119,6 +127,31 @@
* }
* }
*
+ * Slots — placing sections in columns
+ *
+ * Every section is placed into a
+ * {@link com.demcha.compose.document.templates.cv.v2.data.Slot} —
+ * {@code MAIN}, {@code SIDEBAR}, or {@code FOOTER}. Single-column
+ * presets read only {@code MAIN} sections; multi-column presets
+ * read whichever slots they support, leaving the data model
+ * unchanged.
+ *
+ * {@code
+ * CvDocument doc = CvDocument.builder()
+ * .identity(identity)
+ * .section(summary) // implicit MAIN
+ * .section(Slot.SIDEBAR, languages) // sidebar column
+ * .build();
+ * }
+ *
+ * Inside a preset:
+ *
+ * {@code
+ * for (CvSection s : doc.sectionsIn(Slot.MAIN)) {
+ * // render s into the main flow
+ * }
+ * }
+ *
* Going further — extension recipes
*
* See {@code AUTHORS.md} alongside this package for longer
@@ -128,6 +161,7 @@
*
Build a new theme variant (navy / compact / serif)
* Write a brand-new preset that reuses existing renderers
* Add a brand-new section subtype (compile-checked dispatch)
+ * Place sections in slots (sidebar / footer)
*
*
* What this package is not
diff --git a/src/main/java/com/demcha/compose/document/templates/cv/v2/presets/BoxedSections.java b/src/main/java/com/demcha/compose/document/templates/cv/v2/presets/BoxedSections.java
index 57d51364..33c1705f 100644
--- a/src/main/java/com/demcha/compose/document/templates/cv/v2/presets/BoxedSections.java
+++ b/src/main/java/com/demcha/compose/document/templates/cv/v2/presets/BoxedSections.java
@@ -9,6 +9,7 @@
import com.demcha.compose.document.templates.cv.v2.components.SectionDispatcher;
import com.demcha.compose.document.templates.cv.v2.data.CvDocument;
import com.demcha.compose.document.templates.cv.v2.data.CvSection;
+import com.demcha.compose.document.templates.cv.v2.data.Slot;
import com.demcha.compose.document.templates.cv.v2.theme.CvTheme;
import java.util.List;
@@ -114,7 +115,10 @@ public void compose(DocumentSession document, CvDocument doc) {
ContactRenderer.render(section, doc.identity(), theme);
});
- List sections = doc.sections();
+ // Single-column preset — only renders MAIN-slot sections.
+ // Sidebar / footer placements are intentionally dropped here;
+ // switch to a multi-column preset to render them.
+ List sections = doc.sectionsIn(Slot.MAIN);
for (int i = 0; i < sections.size(); i++) {
final CvSection sec = sections.get(i);
final int idx = i;
diff --git a/src/main/java/com/demcha/compose/document/templates/cv/v2/presets/MinimalUnderlined.java b/src/main/java/com/demcha/compose/document/templates/cv/v2/presets/MinimalUnderlined.java
index 77dda81e..20c52adc 100644
--- a/src/main/java/com/demcha/compose/document/templates/cv/v2/presets/MinimalUnderlined.java
+++ b/src/main/java/com/demcha/compose/document/templates/cv/v2/presets/MinimalUnderlined.java
@@ -12,6 +12,7 @@
import com.demcha.compose.document.templates.cv.v2.components.TextOrnaments;
import com.demcha.compose.document.templates.cv.v2.data.CvDocument;
import com.demcha.compose.document.templates.cv.v2.data.CvSection;
+import com.demcha.compose.document.templates.cv.v2.data.Slot;
import com.demcha.compose.document.templates.cv.v2.theme.CvTheme;
import java.util.List;
@@ -108,7 +109,9 @@ public void compose(DocumentSession document, CvDocument doc) {
ContactRenderer.render(section, doc.identity(), theme);
});
- List sections = doc.sections();
+ // Single-column preset — only renders MAIN-slot sections.
+ // Sidebar / footer placements are intentionally dropped here.
+ List sections = doc.sectionsIn(Slot.MAIN);
for (int i = 0; i < sections.size(); i++) {
final CvSection sec = sections.get(i);
final int idx = i;
diff --git a/src/test/java/com/demcha/compose/document/templates/cv/v2/data/CvDocumentSlotTest.java b/src/test/java/com/demcha/compose/document/templates/cv/v2/data/CvDocumentSlotTest.java
new file mode 100644
index 00000000..272e852f
--- /dev/null
+++ b/src/test/java/com/demcha/compose/document/templates/cv/v2/data/CvDocumentSlotTest.java
@@ -0,0 +1,149 @@
+package com.demcha.compose.document.templates.cv.v2.data;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+class CvDocumentSlotTest {
+
+ private static CvIdentity identity() {
+ return CvIdentity.builder()
+ .name("Jane", "Doe")
+ .contact("+44 0", "j@d.com", "London")
+ .build();
+ }
+
+ private static ParagraphSection summary() {
+ return new ParagraphSection("Summary", "body");
+ }
+
+ private static RowsSection skills() {
+ return RowsSection.builder("Skills", RowStyle.BULLETED)
+ .row("Languages", "Java").build();
+ }
+
+ @Test
+ void builder_section_defaults_to_main() {
+ ParagraphSection s = summary();
+ CvDocument doc = CvDocument.builder()
+ .identity(identity())
+ .section(s)
+ .build();
+
+ assertThat(doc.placements()).hasSize(1);
+ assertThat(doc.placements().get(0).slot()).isEqualTo(Slot.MAIN);
+ assertThat(doc.slotOf(s)).isEqualTo(Slot.MAIN);
+ }
+
+ @Test
+ void builder_section_with_explicit_slot_uses_it() {
+ ParagraphSection s = summary();
+ CvDocument doc = CvDocument.builder()
+ .identity(identity())
+ .section(Slot.SIDEBAR, s)
+ .build();
+
+ assertThat(doc.placements().get(0).slot()).isEqualTo(Slot.SIDEBAR);
+ assertThat(doc.slotOf(s)).isEqualTo(Slot.SIDEBAR);
+ }
+
+ @Test
+ void sections_returns_all_regardless_of_slot() {
+ ParagraphSection mainSec = summary();
+ RowsSection sidebarSec = skills();
+ CvDocument doc = CvDocument.builder()
+ .identity(identity())
+ .section(mainSec)
+ .section(Slot.SIDEBAR, sidebarSec)
+ .build();
+
+ assertThat(doc.sections()).containsExactly(mainSec, sidebarSec);
+ }
+
+ @Test
+ void sectionsIn_filters_by_slot() {
+ ParagraphSection mainSec = summary();
+ RowsSection sidebarSec = skills();
+ CvDocument doc = CvDocument.builder()
+ .identity(identity())
+ .section(mainSec)
+ .section(Slot.SIDEBAR, sidebarSec)
+ .build();
+
+ assertThat(doc.sectionsIn(Slot.MAIN)).containsExactly(mainSec);
+ assertThat(doc.sectionsIn(Slot.SIDEBAR)).containsExactly(sidebarSec);
+ assertThat(doc.sectionsIn(Slot.FOOTER)).isEmpty();
+ }
+
+ @Test
+ void slotOf_returns_main_for_unknown_section() {
+ ParagraphSection orphan = new ParagraphSection("Orphan", "x");
+ CvDocument doc = CvDocument.builder()
+ .identity(identity())
+ .section(summary())
+ .build();
+
+ assertThat(doc.slotOf(orphan)).isEqualTo(Slot.MAIN);
+ }
+
+ @Test
+ void sections_varargs_with_slot_places_all_in_that_slot() {
+ ParagraphSection a = new ParagraphSection("A", "x");
+ ParagraphSection b = new ParagraphSection("B", "y");
+ CvDocument doc = CvDocument.builder()
+ .identity(identity())
+ .sections(Slot.SIDEBAR, a, b)
+ .build();
+
+ assertThat(doc.sectionsIn(Slot.SIDEBAR)).containsExactly(a, b);
+ assertThat(doc.sectionsIn(Slot.MAIN)).isEmpty();
+ }
+
+ @Test
+ @SuppressWarnings("deprecation")
+ void deprecated_ofMainSections_wraps_everything_in_main() {
+ ParagraphSection a = new ParagraphSection("A", "x");
+ ParagraphSection b = new ParagraphSection("B", "y");
+ CvDocument doc = CvDocument.ofMainSections(identity(), List.of(a, b));
+
+ assertThat(doc.sectionsIn(Slot.MAIN)).containsExactly(a, b);
+ assertThat(doc.sectionsIn(Slot.SIDEBAR)).isEmpty();
+ }
+
+ @Test
+ void placements_preserves_source_order_across_slots() {
+ ParagraphSection a = new ParagraphSection("A", "x");
+ RowsSection b = skills();
+ ParagraphSection c = new ParagraphSection("C", "z");
+ CvDocument doc = CvDocument.builder()
+ .identity(identity())
+ .section(a)
+ .section(Slot.SIDEBAR, b)
+ .section(c)
+ .build();
+
+ // sections() preserves the global add order
+ assertThat(doc.sections()).containsExactly(a, b, c);
+ // sectionsIn filters but keeps relative order
+ assertThat(doc.sectionsIn(Slot.MAIN)).containsExactly(a, c);
+ }
+
+ @Test
+ void rejects_null_section() {
+ CvDocument.Builder b = CvDocument.builder().identity(identity());
+ assertThatThrownBy(() -> b.section(null))
+ .isInstanceOf(NullPointerException.class);
+ assertThatThrownBy(() -> b.section(Slot.SIDEBAR, null))
+ .isInstanceOf(NullPointerException.class);
+ }
+
+ @Test
+ void rejects_null_slot() {
+ CvDocument.Builder b = CvDocument.builder().identity(identity());
+ assertThatThrownBy(() -> b.section(null, summary()))
+ .isInstanceOf(NullPointerException.class);
+ }
+}