Skip to content

feat(templates): keep single-column CV preset section titles with their bodies#427

Merged
DemchaAV merged 3 commits into
developfrom
feat/keep-with-next-templates
Jul 22, 2026
Merged

feat(templates): keep single-column CV preset section titles with their bodies#427
DemchaAV merged 3 commits into
developfrom
feat/keep-with-next-templates

Conversation

@DemchaAV

@DemchaAV DemchaAV commented Jul 22, 2026

Copy link
Copy Markdown
Owner

Why

CV presets emit a section header — a boxed banner, an underlined title, or a
rule + banner + rule — that flows down the page ahead of its body. When the first
line of the body does not fit in the remaining space, the header strands at a page
bottom apart from its content (a boxed title torn from its background). #426 added
the keepWithNext() pagination primitive; this wires the single-column CV presets
to it.

Which node to mark is a composition decision, not a rendering one: a header that is
its own page-flow section must mark that section (so it binds to the body
section that follows); a header that shares one section with its body must mark
a nested group around the header (so it binds to the body's first line, not to the
next module). A shared header widget can't tell those two shapes apart, so the
policy lives in the presets, and the widgets stay pure rendering.

What changed

  • Standalone-header presets mark the header section at the addSection call:
    BoxedSections, MinimalUnderlined, ModernProfessional, Executive. CenteredHeadline
    marks its title section and its trailing rule line so the two-node group binds
    as one run.
  • FlowSectionHeader (BlueBanner / EditorialBlue) marks its whole title run —
    top rule + banner/title section + bottom rule — so the group relocates as a unit.
  • ClassicSerif renders header + body into one section; its header now sits in a
    nested keepWithNext subsection (keptHeader) whose spacing matches the module,
    so the title stays with the body's first line while placement is unchanged.
  • LineNode.keepWithNext() (new record component + back-compat 17-arg
    constructor; chart call sites and the convenience chain unchanged) and
    LineBuilder.keepWithNext() — the line counterpart of the section flag, so a
    header rule joins its title run. Default false, byte-identical for lines that do
    not opt in.

Verification

Full reactor clean verifyBUILD SUCCESS. +4 tests in
PresetHeaderKeepWithNextTest: BoxedSections marks each banner and no body section;
ClassicSerif marks the nested header subsection but not the combined module
section; BlueBanner marks the whole rule + banner + rule run; and a nested
header-subsection relocates with the body's first line across a page break (with a
control that strands without the opt-in). All 31 CV + cover-letter visual-parity
baselines are unchanged — the wiring only changes pagination at a boundary, not
placement. Behaviour is covered by #426's SectionKeepWithNextTest.

Notes

  • Multi-column presets need nothing (CompactMono, NordicClean, Panel side cards,
    TimelineMinimal, EngineeringResume, MonogramSidebar, SidebarPortrait, MintEditorial):
    their sections sit in fixed Row columns that never paginate, so a heading cannot
    strand there. Cover letters have no repeating section headers.
  • Follow-up: Panel's full-width Profile card is the one single-column card whose
    header could strand if a long summary splits the card; its header widget is shared
    with the inert multi-column cards, so it is handled separately to keep Panel's
    parity byte-identical.
  • Stacked on feat(engine): keep a heading with the first line of its following block #426 (feat/keep-with-next); retarget to develop after feat(engine): keep a heading with the first line of its following block #426 merges.

Lane: templates (CV preset composition) + a canonical node addition
(LineNode / LineBuilder.keepWithNext()).

DemchaAV added 2 commits July 22, 2026 01:49
A section header emitted as its own block strands at a page bottom when the
first line of the block below it does not fit in the remaining space: the
header flows on page N while its body starts on page N+1 — a boxed title torn
from its background. keepTogether() cannot fix this; it relocates the whole
block, so a page-spanning body makes it inert and the orphan returns.

Add an opt-in keepWithNext() (CSS break-after:avoid): a section may not be the
last placed block on its page when a sibling with content follows. When the
section plus the first line of that block overflow the remaining space but fit
on a fresh page, the compiler relocates the section so the heading stays with
its body.

- LayoutCompiler runs a run-aware lookahead in the vertical child loop: at the
  start of a run of consecutive keep-with-next siblings it sums the run plus the
  next block's leading line and hoists the break before the run's first member,
  so a multi-part heading (rule + banner + rule) moves as a unit.
- leadingUnitHeight descends the following block's first-child chain to its first
  paragraph line; an indivisible or non-paragraph-splittable first unit is kept
  whole. Best-effort: a heading plus one line that cannot share a page flows in
  place; inert when nothing follows.
- keepWithNext() on DocumentNode (default false), SectionNode (new component plus
  back-compat constructor), and SectionBuilder. Default off, so layouts that do
  not opt in are byte-identical.
…ir bodies

CV presets emit a section header that flows down the page ahead of its body; when
the first line of the body does not fit in the remaining space the header strands at
a page bottom apart from its content. Wire the single-column presets to the
keep-with-next primitive.

Which node to mark is a composition decision: a header that is its own page-flow
section marks that section (binding it to the following body section); a header that
shares one section with its body marks a nested group around the header (binding it
to the body's first line, not the next module). A shared widget cannot tell those
apart, so the policy lives in the presets and the widgets stay pure rendering.

- Standalone-header presets mark the header section at addSection: BoxedSections,
  MinimalUnderlined, ModernProfessional, Executive. CenteredHeadline also marks its
  trailing rule so the title + rule group binds as one run.
- FlowSectionHeader (BlueBanner, EditorialBlue) marks its whole rule + banner + rule
  run so the title block relocates as a unit.
- ClassicSerif renders header + body into one section; its header now sits in a
  nested keep-with-next subsection whose spacing matches the module, so the title
  stays with the body's first line while placement is unchanged.
- LineNode.keepWithNext() (new component plus back-compat constructor) and
  LineBuilder.keepWithNext() — the line counterpart of the section flag, so a header
  rule joins its title run. Default off, byte-identical for lines that do not opt in.

Multi-column presets place their sections in fixed Row columns that never paginate,
so no heading can strand there.
@DemchaAV
DemchaAV force-pushed the feat/keep-with-next-templates branch from ec08fc7 to ad54687 Compare July 22, 2026 08:27
@DemchaAV DemchaAV changed the title feat(templates): keep preset section headers with their bodies feat(templates): keep single-column CV preset section titles with their bodies Jul 22, 2026
@DemchaAV
DemchaAV changed the base branch from feat/keep-with-next to develop July 22, 2026 09:42
@DemchaAV
DemchaAV merged commit a5616b7 into develop Jul 22, 2026
13 checks passed
@DemchaAV
DemchaAV deleted the feat/keep-with-next-templates branch July 22, 2026 10:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant