Skip to content

Commit b22775c

Browse files
authored
feat(theme): add nordic / editorial / cinematic / monochrome BusinessTheme presets (@SInCE 1.6.8) (#118)
Adds four contemporary BusinessTheme factory methods alongside the existing formal-skewed classic / modern / executive trio: - nordic() — cool whites + slate-blue + generous spacing (Scandinavian minimal, design-studio one-pagers) - editorial() — warm cream + deep ink + brick-red accent on a Times-Roman serif body (magazine, long-form proposals, annual reports) - cinematic() — DEEP NAVY SURFACE with light text + bright copper accent (inverted palette — first preset to ship a dark surface; tuned for investor pitch decks) - monochrome() — pure black on white with a single bold yellow accent (brutalist, fashion-cover style) Every new preset reuses the same internal `textScale()` and `tablePreset()` helpers as the existing trio, so a downstream template authored against any one preset gets the same matrix of style tokens (palette / spacing / textScale / tablePreset / optional pageBackground). The cinematic preset additionally uses the dark surface as the page background so the moody look fills the page edges. Class-level Javadoc updated to list all seven presets and group them as "formal" vs "contemporary". Test plan: - BusinessThemeTest expanded from 14 → 19 tests: * one positive-coverage test per new preset (asserts name, non-null tokens, distinctive trait — Times-Roman body for editorial, dark-on-light inversion for cinematic, etc.) * `allSevenBuiltInThemesArePairwiseDistinctByPalette` upgrades the previous three-theme distinctness check to the seven- theme pairwise matrix - `./mvnw verify -pl . -P japicmp` — **1037 tests pass**, 0 failures. japicmp vs v1.6.7 baseline: semver PATCH (compatible additions only, no breaking change).
1 parent fc8394a commit b22775c

3 files changed

Lines changed: 214 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,25 @@ follow-ups carried over from the v1.6.7 senior review (see
1515
[ROADMAP.md](ROADMAP.md) and the private taskboard). No breaking
1616
changes are planned.
1717

18+
### Public API
19+
20+
- Four new `BusinessTheme` factory presets `@since 1.6.8`:
21+
`BusinessTheme.nordic()` (Scandinavian minimal — cool whites +
22+
slate-blue accent + generous whitespace, for design-studio
23+
reports and product launch decks),
24+
`BusinessTheme.editorial()` (warm cream surface + deep ink +
25+
brick-red accent on a serif body, for long-form proposals and
26+
annual reports),
27+
`BusinessTheme.cinematic()` (inverted dark navy surface with
28+
light text + bright copper accent, for investor pitch decks and
29+
product launch one-pagers), and
30+
`BusinessTheme.monochrome()` (pure black-on-white with a single
31+
bold yellow accent, for brutalist editorial layouts where
32+
typographic contrast carries the identity). Pure additions —
33+
no change to the existing `classic()` / `modern()` /
34+
`executive()` presets. japicmp gate against v1.6.7 reports
35+
`semver PATCH` (compatible additions only).
36+
1837
### Build
1938

2039
- Bumped `jackson-bom` 2.21.3 → 2.21.4 (broken 2.22.0 skipped via

src/main/java/com/demcha/compose/document/theme/BusinessTheme.java

Lines changed: 112 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,14 @@
1919
* an invoice, a proposal, and a status report rendered through the same theme
2020
* look like a single product instead of three independently styled documents.</p>
2121
*
22-
* <p>Use one of the built-in presets for an immediate style:
23-
* {@link #classic()}, {@link #modern()}, {@link #executive()}.</p>
22+
* <p>Use one of the built-in presets for an immediate style. The
23+
* original three are tuned for formal documents:
24+
* {@link #classic()}, {@link #modern()}, {@link #executive()}.
25+
* Four contemporary additions cover the modern document-design
26+
* spectrum: {@link #nordic()} (Scandinavian minimal),
27+
* {@link #editorial()} (warm magazine), {@link #cinematic()}
28+
* (dark moody surface), and {@link #monochrome()} (brutalist
29+
* black-and-white with one accent).</p>
2430
*
2531
* @param name human-readable theme identifier (used for diagnostics)
2632
* @param palette color tokens
@@ -119,6 +125,110 @@ public static BusinessTheme executive() {
119125
return new BusinessTheme("executive", palette, spacing, text, table, null);
120126
}
121127

128+
/**
129+
* "Nordic" theme — cool near-white surface, deep slate-blue
130+
* primary, dusty slate accent, generous spacing. Tuned for
131+
* design studios, product reports, and clean startup decks
132+
* where whitespace is the dominant visual element.
133+
*
134+
* @return nordic theme
135+
* @since 1.6.8
136+
*/
137+
public static BusinessTheme nordic() {
138+
DocumentPalette palette = DocumentPalette.builder()
139+
.primary(new Color(36, 50, 64)) // deep slate-blue
140+
.accent(new Color(96, 118, 142)) // dusty slate
141+
.surface(new Color(252, 253, 254)) // cool near-white
142+
.surfaceMuted(new Color(240, 243, 246))
143+
.textPrimary(new Color(36, 50, 64))
144+
.textMuted(new Color(108, 120, 134))
145+
.rule(new Color(220, 226, 232)) // very subtle cool line
146+
.build();
147+
SpacingScale spacing = new SpacingScale(6.0, 12.0, 18.0, 28.0, 44.0);
148+
TextScale text = textScale(palette, FontName.HELVETICA, FontName.HELVETICA_BOLD,
149+
26, 16, 12, 10, 9);
150+
TablePreset table = tablePreset(palette, spacing);
151+
return new BusinessTheme("nordic", palette, spacing, text, table, null);
152+
}
153+
154+
/**
155+
* "Editorial" theme — warm cream surface, deep ink primary,
156+
* brick-red accent on a serif body. Tuned for long-form
157+
* proposals, annual reports, and brand decks that want a
158+
* magazine feel.
159+
*
160+
* @return editorial theme
161+
* @since 1.6.8
162+
*/
163+
public static BusinessTheme editorial() {
164+
DocumentPalette palette = DocumentPalette.builder()
165+
.primary(new Color(22, 22, 22)) // deep ink
166+
.accent(new Color(160, 60, 50)) // brick red
167+
.surface(new Color(250, 245, 235)) // warm cream
168+
.surfaceMuted(new Color(240, 232, 218))
169+
.textPrimary(new Color(22, 22, 22))
170+
.textMuted(new Color(95, 90, 85)) // warm grey
171+
.rule(new Color(200, 190, 175))
172+
.build();
173+
SpacingScale spacing = SpacingScale.defaultScale();
174+
TextScale text = textScale(palette, FontName.TIMES_ROMAN, FontName.TIMES_ROMAN,
175+
30, 18, 14, 11, 9);
176+
TablePreset table = tablePreset(palette, spacing);
177+
return new BusinessTheme("editorial", palette, spacing, text, table, palette.surface());
178+
}
179+
180+
/**
181+
* "Cinematic" theme — deep navy surface with light text and a
182+
* bright copper accent. Inverts the usual dark-on-light document
183+
* convention; tuned for investor pitch decks, product launch
184+
* one-pagers, and presentations that need a moody premium feel.
185+
*
186+
* @return cinematic theme
187+
* @since 1.6.8
188+
*/
189+
public static BusinessTheme cinematic() {
190+
DocumentPalette palette = DocumentPalette.builder()
191+
.primary(new Color(245, 248, 252)) // near-white (text on dark)
192+
.accent(new Color(220, 130, 50)) // bright copper
193+
.surface(new Color(16, 24, 36)) // deep navy SURFACE
194+
.surfaceMuted(new Color(28, 36, 48)) // slightly lighter navy panel
195+
.textPrimary(new Color(245, 248, 252))
196+
.textMuted(new Color(160, 170, 188)) // muted light blue-grey
197+
.rule(new Color(54, 64, 78)) // subtle on-dark rule
198+
.build();
199+
SpacingScale spacing = new SpacingScale(4.0, 8.0, 14.0, 24.0, 40.0);
200+
TextScale text = textScale(palette, FontName.HELVETICA, FontName.HELVETICA_BOLD,
201+
30, 18, 14, 11, 9);
202+
TablePreset table = tablePreset(palette, spacing);
203+
return new BusinessTheme("cinematic", palette, spacing, text, table, palette.surface());
204+
}
205+
206+
/**
207+
* "Monochrome" theme — pure black on white with a single bold
208+
* yellow accent. Tuned for design-studio one-pagers, fashion-
209+
* magazine-style covers, and brutalist editorial layouts where
210+
* typographic contrast is the entire identity.
211+
*
212+
* @return monochrome theme
213+
* @since 1.6.8
214+
*/
215+
public static BusinessTheme monochrome() {
216+
DocumentPalette palette = DocumentPalette.builder()
217+
.primary(new Color(0, 0, 0)) // pure black
218+
.accent(new Color(240, 196, 25)) // bold yellow
219+
.surface(new Color(255, 255, 255)) // pure white
220+
.surfaceMuted(new Color(244, 244, 244))
221+
.textPrimary(new Color(0, 0, 0))
222+
.textMuted(new Color(115, 115, 115)) // medium grey
223+
.rule(new Color(0, 0, 0)) // bold rules
224+
.build();
225+
SpacingScale spacing = new SpacingScale(4.0, 8.0, 12.0, 20.0, 36.0);
226+
TextScale text = textScale(palette, FontName.HELVETICA, FontName.HELVETICA_BOLD,
227+
32, 20, 14, 11, 9);
228+
TablePreset table = tablePreset(palette, spacing);
229+
return new BusinessTheme("monochrome", palette, spacing, text, table, null);
230+
}
231+
122232
/**
123233
* Returns a copy of this theme with the page background overridden.
124234
*

src/test/java/com/demcha/compose/document/theme/BusinessThemeTest.java

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,4 +149,87 @@ void allThreeBuiltInThemesAreDistinct() {
149149
assertThat(modern.palette()).isNotEqualTo(executive.palette());
150150
assertThat(classic.text().h1()).isNotEqualTo(modern.text().h1());
151151
}
152+
153+
// --- Contemporary themes added in v1.6.8 -------------------------------
154+
155+
@Test
156+
void nordicThemeHasAllTokensNonNullAndUsesGenerousSpacing() {
157+
BusinessTheme theme = BusinessTheme.nordic();
158+
assertThat(theme.name()).isEqualTo("nordic");
159+
assertThat(theme.palette()).isNotNull();
160+
assertThat(theme.spacing()).isNotNull();
161+
assertThat(theme.text()).isNotNull();
162+
assertThat(theme.table()).isNotNull();
163+
assertThat(theme.pageBackground()).isNull();
164+
// Nordic is tuned for whitespace — every step is at least as wide
165+
// as the default scale, and md is strictly wider.
166+
SpacingScale standard = SpacingScale.defaultScale();
167+
assertThat(theme.spacing().md()).isGreaterThan(standard.md());
168+
assertThat(theme.spacing().xl()).isGreaterThan(standard.xl());
169+
}
170+
171+
@Test
172+
void editorialThemeUsesTimesRomanBodyAndCreamPageBackground() {
173+
BusinessTheme theme = BusinessTheme.editorial();
174+
assertThat(theme.name()).isEqualTo("editorial");
175+
assertThat(theme.text().body().fontName().name()).isEqualTo("Times-Roman");
176+
assertThat(theme.text().h1().fontName().name()).isEqualTo("Times-Roman");
177+
// Cream page background distinguishes editorial from the
178+
// strictly-white classic theme.
179+
assertThat(theme.pageBackground()).isNotNull();
180+
assertThat(theme.pageBackground()).isEqualTo(theme.palette().surface());
181+
}
182+
183+
@Test
184+
void cinematicThemeInvertsPaletteToLightTextOnDarkSurface() {
185+
BusinessTheme theme = BusinessTheme.cinematic();
186+
assertThat(theme.name()).isEqualTo("cinematic");
187+
// The defining trait: surface is dark, primary/text is light.
188+
Color surface = theme.palette().surface().color();
189+
Color textPrimary = theme.palette().textPrimary().color();
190+
int surfaceLuminance = (surface.getRed() + surface.getGreen() + surface.getBlue()) / 3;
191+
int textLuminance = (textPrimary.getRed() + textPrimary.getGreen() + textPrimary.getBlue()) / 3;
192+
assertThat(surfaceLuminance).isLessThan(64); // genuinely dark surface
193+
assertThat(textLuminance).isGreaterThan(192); // genuinely light text
194+
// And the surface doubles as the page background so the moody
195+
// look fills the page edges too.
196+
assertThat(theme.pageBackground()).isEqualTo(theme.palette().surface());
197+
}
198+
199+
@Test
200+
void monochromeThemeIsPureBlackOnWhiteWithBoldYellowAccent() {
201+
BusinessTheme theme = BusinessTheme.monochrome();
202+
assertThat(theme.name()).isEqualTo("monochrome");
203+
assertThat(theme.palette().primary().color()).isEqualTo(Color.BLACK);
204+
assertThat(theme.palette().surface().color()).isEqualTo(Color.WHITE);
205+
// The single accent is the entire identity of the theme — assert
206+
// it leans yellow (R and G high, B low) rather than pinning the
207+
// exact RGB, so a future shade tweak does not break the test.
208+
Color accent = theme.palette().accent().color();
209+
assertThat(accent.getRed()).isGreaterThan(200);
210+
assertThat(accent.getGreen()).isGreaterThan(150);
211+
assertThat(accent.getBlue()).isLessThan(80);
212+
}
213+
214+
@Test
215+
void allSevenBuiltInThemesArePairwiseDistinctByPalette() {
216+
BusinessTheme[] all = {
217+
BusinessTheme.classic(),
218+
BusinessTheme.modern(),
219+
BusinessTheme.executive(),
220+
BusinessTheme.nordic(),
221+
BusinessTheme.editorial(),
222+
BusinessTheme.cinematic(),
223+
BusinessTheme.monochrome()
224+
};
225+
for (int i = 0; i < all.length; i++) {
226+
for (int j = i + 1; j < all.length; j++) {
227+
assertThat(all[i].palette())
228+
.as("Themes '%s' and '%s' must have distinct palettes",
229+
all[i].name(), all[j].name())
230+
.isNotEqualTo(all[j].palette());
231+
assertThat(all[i].name()).isNotEqualTo(all[j].name());
232+
}
233+
}
234+
}
152235
}

0 commit comments

Comments
 (0)