feat(templates): keep single-column CV preset section titles with their bodies#427
Merged
Conversation
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
force-pushed
the
feat/keep-with-next-templates
branch
from
July 22, 2026 08:27
ec08fc7 to
ad54687
Compare
…t-templates # Conflicts: # CHANGELOG.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 presetsto 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
addSectioncall: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.
ClassicSerifrenders header + body into one section; its header now sits in anested
keepWithNextsubsection (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-argconstructor; chart call sites and the convenience chain unchanged) and
LineBuilder.keepWithNext()— the line counterpart of the section flag, so aheader rule joins its title run. Default
false, byte-identical for lines that donot opt in.
Verification
Full reactor
clean verify→ BUILD SUCCESS. +4 tests inPresetHeaderKeepWithNextTest: 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
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.
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.
feat/keep-with-next); retarget todevelopafter 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()).