Epic: Composite Address Controls and Mixed-Content Text
Vision / why this matters: Today every text element is one line, and each line is either pure static text or a single bound CSV column. Real address blocks and salutation lines need to combine literal text with one or more fields on the same line (e.g., “Attn: {First Name} {Last Name}"), and operators currently have to place, align, and manage every address line by hand with no notion that they belong together. This epic adds that composition, then lets operators place a whole address block as one manageable unit.
Stories
Mix static text and CSV fields within a single text element - Status: Not Started
Card
As a print operator, I want to combine literal text and one or more CSV fields within a single text element (e.g., “Attn: {First Name} {Last Name}"), so that I can compose natural sentences and labels instead of being limited to one static line or one bound field per element.
Conversation notes
- Confirmed with the user (2026-09-29): this is a general upgrade, not scoped only to the Address Control below — every text element in the designer gains this capability, replacing today's mutually-exclusive
StaticText/ColumnName model (TextElementLayout, TemplateElement) everywhere.
- Content becomes an ordered sequence of “runs”: literal text runs and field-token runs (each bound to a CSV column), concatenated per record at render time.
- Backward compatibility is non-negotiable: every existing saved template (pure static text, or a single bound column) must continue to render byte-for-byte identically — model these as the one-run special case of the new run-sequence, not a separate parallel code path that could drift from it.
- A field token bound to a column that isn't a real CSV header remains a hard, whole-run failure before any page renders — the same blocking rule as today's single-column binding (
TEMPLATE_FORMAT.md's “Known gaps”), just evaluated per token instead of per element.
- The properties panel needs a real editing affordance for composing mixed content — not a plain text box where a field token is just a raw substring an operator could accidentally mistype or break. Exact mechanism (e.g., an “Insert field” action that drops in a protected token chip at the cursor, or a
{Column Name} bracket-typing convention that's parsed back into runs on commit) is a Development Team design decision; document whichever is chosen.
- Canvas display should visually distinguish the field-token portions from literal text within the same element, extending today's highlight-fill treatment for pure dynamic elements so it now applies per-run rather than per-element.
- Persisted XML shape (e.g., nested run/field child elements, or an inline token syntax within the element's own text content) is a Development Team decision, to be documented in
TEMPLATE_FORMAT.md — but it must remain backward-compatible with every template saved before this story.
- Out of scope: any formatting/transformation applied to a token's value (date formatting, casing, truncation) — verbatim column value substitution only, matching today's single-column binding behavior exactly.
Confirmation (Acceptance Criteria)
Estimate: TBD (dev-team to size)
Dependencies: Depends on “Create a dynamic text token from a CSV column” (Done, Sprint 3) — extends, rather than replaces, that story's persisted format for the existing single-run case.
Group lines into a single, movable Address Control - Status: Not Started
Card
As a print operator, I want to group a custom, ordered set of lines (each using the mixed static-text-and-field content from the previous story) into a single Address Control element that I can place, move, and resize on the canvas as one unit, so that I can build and manage address blocks without positioning every line by hand.
Conversation notes
- Confirmed with the user (2026-09-29): the operator builds a fully custom line list — add, remove, and reorder any number of lines — not a fixed starter template of pre-named lines.
- Confirmed with the user: the whole control moves and resizes via a single anchor point; line spacing is automatic. Exact spacing rule (derived from each line's own font size/leading, or an explicit “line spacing” value stored on the control) is a Development Team design decision, documented alongside the persisted format.
- Confirmed with the user: “Collapse if blank” (already shipped, Sprint 4) stays a per-line checkbox exactly as it works today, but defaults to checked/on for a line added inside an Address Control — an explicit change from a standalone element's default-off. Existing standalone elements are unaffected.
AddressLineCollapser's current grouping rule (lines sharing the same X position stack together) is an implicit heuristic from before this story existed. Once lines live inside one named control, the team should decide whether the collapse-stack grouping should key off “belongs to the same control” instead of (or in addition to) shared X — flagged for the team to resolve during implementation, not decided here, since it affects whether two visually-adjacent-but-unrelated elements could accidentally group.
- Persisted format: a new container element (e.g.
<addressControl>) wrapping its child lines — exact shape is a Development Team decision, documented in TEMPLATE_FORMAT.md, but must round-trip through save/reopen and remain independently renderable by the CLI without the desktop app, same as every other template element.
- Rotating the control as a whole group (interacting with Sprint 4's rotation feature) is explicitly out of scope for this story — note as a candidate follow-up, not required here.
- Selecting the control on canvas must still allow editing an individual line's content/binding (via the previous story's editing UI) without breaking the group's single-anchor movement behavior.
Confirmation (Acceptance Criteria)
Estimate: TBD (dev-team to size)
Dependencies: Depends on “Mix static text and CSV fields within a single text element” (above, same epic) and “Collapse blank optional address lines consistently” (Done, Sprint 4).