- Status: Accepted (with phased migration)
- Date: 2026-04-30
- Authors: Artem Demchyshyn
GraphCompose ships two theme types in v1.5:
| Type | Package | Used by |
|---|---|---|
BusinessTheme |
com.demcha.compose.document.theme |
InvoiceTemplateV2, ProposalTemplateV2, getting-started.md quick-start, addSection recipe code, runnable examples |
CvTheme |
com.demcha.compose.document.templates.theme |
CvTemplateV1, the CV template gallery (PanelCvTemplateComposer, ten Cv* template variants), CvFileExample |
The two types describe overlapping but not identical concerns:
BusinessThemeis a canonical record exposingDocumentPalette,SpacingScale,TextScale(withh1/h2/h3/body/caption/label/accentslots),TablePreset, and an optionalpageBackgroundcolour. It uses canonical types only so authors who never touch the engine API can reach for it.CvThemeis a CV-specific record carrying eight numeric and colour fields (primaryColor,secondaryColor,bodyColor,accentColor, header / body fonts, name / header / body font sizes, spacing tokens). Its accessor methods return enginecom.demcha.compose.engine.components.content.text.TextStylevalues, which is the type the legacy CV composers consume.
Rendering the same data through both layers is awkward: the business-themed templates and the CV templates can drift visually even when consumers expect "one product look" because the two theme types are independent.
We adopt a two-step unification rather than a single big-bang refactor:
-
CvThemebecomes a thin adapter that can be derived from aBusinessTheme. A new static factoryCvTheme.fromBusinessTheme(BusinessTheme)materialises aCvThemewhose colours, fonts, and sizes are derived from the business theme'spalette()+text()slots. ExistingCvTheme.defaultTheme()/timesRoman()/courier()factories remain so every legacy caller compiles unchanged. -
CV templates and composers migrate to the bridge incrementally. New CV-style templates that need theme parity with the business templates can take a
BusinessThemeparameter and internally build theirCvThemevia the new factory. Existing composers that hand-thread aCvThemekeep working until they're touched for unrelated reasons.
The alternative we considered ("Option B" — extract a common
Theme interface that both records implement) was rejected because:
- Both are records with different field counts and accessor names; a common interface would either be a thin marker (no real polymorphism benefit) or require a mid-level "ThemeView" with duplicated accessors — extra surface area without a corresponding consumer.
- The CV composers and the BusinessTheme composers have different semantic anchors (CV speaks in "name / section header / body / small body" while Business speaks in "h1 / h2 / h3 / body / caption / label / accent"). Forcing them under one interface loses the CV-specific vocabulary that 10 templates already rely on.
Positive:
- Authors who already use
BusinessTheme.modern()for their invoice and proposal templates can instantly derive a matchingCvThemefor their CV templates:CvTheme.fromBusinessTheme(myTheme). The output looks visually consistent across all three document types. - Migration is incremental — no template needs to change all at once.
The bridge factory works whether the consumer eventually moves to a
BusinessTheme-only path or sticks withCvTheme. - The
BusinessThemeAPI surface stays narrow. We did not need a new interface or wrapper.
Negative / accepted trade-offs:
- The two types still coexist. Code that wants to know "the project has a single theme" has to know about both. The bridge keeps them in sync but doesn't collapse them.
- The bridge's mapping decisions (which
palette()slot drives which CV colour, whichtext()slot drives which CV font size) are judgement calls. They are documented inCvTheme.fromBusinessThemeand surface as values that callers can override after construction viaCvTheme's legacy factory pattern (build manually from primitives).
Out of scope for v1.5:
- Mass migration of the 10 CV templates to a
BusinessTheme-driven composer. Existing templates keep working; new templates can use the bridge. - A unified
theme.text().h1()style for both BusinessTheme-driven and CvTheme-driven flows. The CV composers continue to call into CvTheme's CV-specific accessors (nameTextStyle,sectionHeaderTextStyle, etc.).
- Add
CvTheme.fromBusinessTheme(BusinessTheme)factory. - Pin behaviour with
CvThemeBusinessThemeAdapterTest— the derived theme uses the business palette / text fonts. - Document the bridge in
docs/recipes/themes.mdso readers know how to keep CV and business themes in sync. - Defer further migration to a follow-up release (per-template refactor when each template is otherwise touched).