From b78047a3411e474e35dbe565f17361f0773b4415 Mon Sep 17 00:00:00 2001 From: DemchaAV Date: Mon, 1 Jun 2026 17:16:26 +0100 Subject: [PATCH] feat(theme): add nordic / editorial / cinematic / monochrome BusinessTheme presets (@since 1.6.8) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- CHANGELOG.md | 19 +++ .../compose/document/theme/BusinessTheme.java | 114 +++++++++++++++++- .../document/theme/BusinessThemeTest.java | 83 +++++++++++++ 3 files changed, 214 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 21a92492..67bee444 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,25 @@ follow-ups carried over from the v1.6.7 senior review (see [ROADMAP.md](ROADMAP.md) and the private taskboard). No breaking changes are planned. +### Public API + +- Four new `BusinessTheme` factory presets `@since 1.6.8`: + `BusinessTheme.nordic()` (Scandinavian minimal — cool whites + + slate-blue accent + generous whitespace, for design-studio + reports and product launch decks), + `BusinessTheme.editorial()` (warm cream surface + deep ink + + brick-red accent on a serif body, for long-form proposals and + annual reports), + `BusinessTheme.cinematic()` (inverted dark navy surface with + light text + bright copper accent, for investor pitch decks and + product launch one-pagers), and + `BusinessTheme.monochrome()` (pure black-on-white with a single + bold yellow accent, for brutalist editorial layouts where + typographic contrast carries the identity). Pure additions — + no change to the existing `classic()` / `modern()` / + `executive()` presets. japicmp gate against v1.6.7 reports + `semver PATCH` (compatible additions only). + ### Build - Bumped `jackson-bom` 2.21.3 → 2.21.4 (broken 2.22.0 skipped via diff --git a/src/main/java/com/demcha/compose/document/theme/BusinessTheme.java b/src/main/java/com/demcha/compose/document/theme/BusinessTheme.java index feb32f1e..4527a9af 100644 --- a/src/main/java/com/demcha/compose/document/theme/BusinessTheme.java +++ b/src/main/java/com/demcha/compose/document/theme/BusinessTheme.java @@ -19,8 +19,14 @@ * an invoice, a proposal, and a status report rendered through the same theme * look like a single product instead of three independently styled documents.

* - *

Use one of the built-in presets for an immediate style: - * {@link #classic()}, {@link #modern()}, {@link #executive()}.

+ *

Use one of the built-in presets for an immediate style. The + * original three are tuned for formal documents: + * {@link #classic()}, {@link #modern()}, {@link #executive()}. + * Four contemporary additions cover the modern document-design + * spectrum: {@link #nordic()} (Scandinavian minimal), + * {@link #editorial()} (warm magazine), {@link #cinematic()} + * (dark moody surface), and {@link #monochrome()} (brutalist + * black-and-white with one accent).

* * @param name human-readable theme identifier (used for diagnostics) * @param palette color tokens @@ -119,6 +125,110 @@ public static BusinessTheme executive() { return new BusinessTheme("executive", palette, spacing, text, table, null); } + /** + * "Nordic" theme — cool near-white surface, deep slate-blue + * primary, dusty slate accent, generous spacing. Tuned for + * design studios, product reports, and clean startup decks + * where whitespace is the dominant visual element. + * + * @return nordic theme + * @since 1.6.8 + */ + public static BusinessTheme nordic() { + DocumentPalette palette = DocumentPalette.builder() + .primary(new Color(36, 50, 64)) // deep slate-blue + .accent(new Color(96, 118, 142)) // dusty slate + .surface(new Color(252, 253, 254)) // cool near-white + .surfaceMuted(new Color(240, 243, 246)) + .textPrimary(new Color(36, 50, 64)) + .textMuted(new Color(108, 120, 134)) + .rule(new Color(220, 226, 232)) // very subtle cool line + .build(); + SpacingScale spacing = new SpacingScale(6.0, 12.0, 18.0, 28.0, 44.0); + TextScale text = textScale(palette, FontName.HELVETICA, FontName.HELVETICA_BOLD, + 26, 16, 12, 10, 9); + TablePreset table = tablePreset(palette, spacing); + return new BusinessTheme("nordic", palette, spacing, text, table, null); + } + + /** + * "Editorial" theme — warm cream surface, deep ink primary, + * brick-red accent on a serif body. Tuned for long-form + * proposals, annual reports, and brand decks that want a + * magazine feel. + * + * @return editorial theme + * @since 1.6.8 + */ + public static BusinessTheme editorial() { + DocumentPalette palette = DocumentPalette.builder() + .primary(new Color(22, 22, 22)) // deep ink + .accent(new Color(160, 60, 50)) // brick red + .surface(new Color(250, 245, 235)) // warm cream + .surfaceMuted(new Color(240, 232, 218)) + .textPrimary(new Color(22, 22, 22)) + .textMuted(new Color(95, 90, 85)) // warm grey + .rule(new Color(200, 190, 175)) + .build(); + SpacingScale spacing = SpacingScale.defaultScale(); + TextScale text = textScale(palette, FontName.TIMES_ROMAN, FontName.TIMES_ROMAN, + 30, 18, 14, 11, 9); + TablePreset table = tablePreset(palette, spacing); + return new BusinessTheme("editorial", palette, spacing, text, table, palette.surface()); + } + + /** + * "Cinematic" theme — deep navy surface with light text and a + * bright copper accent. Inverts the usual dark-on-light document + * convention; tuned for investor pitch decks, product launch + * one-pagers, and presentations that need a moody premium feel. + * + * @return cinematic theme + * @since 1.6.8 + */ + public static BusinessTheme cinematic() { + DocumentPalette palette = DocumentPalette.builder() + .primary(new Color(245, 248, 252)) // near-white (text on dark) + .accent(new Color(220, 130, 50)) // bright copper + .surface(new Color(16, 24, 36)) // deep navy SURFACE + .surfaceMuted(new Color(28, 36, 48)) // slightly lighter navy panel + .textPrimary(new Color(245, 248, 252)) + .textMuted(new Color(160, 170, 188)) // muted light blue-grey + .rule(new Color(54, 64, 78)) // subtle on-dark rule + .build(); + SpacingScale spacing = new SpacingScale(4.0, 8.0, 14.0, 24.0, 40.0); + TextScale text = textScale(palette, FontName.HELVETICA, FontName.HELVETICA_BOLD, + 30, 18, 14, 11, 9); + TablePreset table = tablePreset(palette, spacing); + return new BusinessTheme("cinematic", palette, spacing, text, table, palette.surface()); + } + + /** + * "Monochrome" theme — pure black on white with a single bold + * yellow accent. Tuned for design-studio one-pagers, fashion- + * magazine-style covers, and brutalist editorial layouts where + * typographic contrast is the entire identity. + * + * @return monochrome theme + * @since 1.6.8 + */ + public static BusinessTheme monochrome() { + DocumentPalette palette = DocumentPalette.builder() + .primary(new Color(0, 0, 0)) // pure black + .accent(new Color(240, 196, 25)) // bold yellow + .surface(new Color(255, 255, 255)) // pure white + .surfaceMuted(new Color(244, 244, 244)) + .textPrimary(new Color(0, 0, 0)) + .textMuted(new Color(115, 115, 115)) // medium grey + .rule(new Color(0, 0, 0)) // bold rules + .build(); + SpacingScale spacing = new SpacingScale(4.0, 8.0, 12.0, 20.0, 36.0); + TextScale text = textScale(palette, FontName.HELVETICA, FontName.HELVETICA_BOLD, + 32, 20, 14, 11, 9); + TablePreset table = tablePreset(palette, spacing); + return new BusinessTheme("monochrome", palette, spacing, text, table, null); + } + /** * Returns a copy of this theme with the page background overridden. * diff --git a/src/test/java/com/demcha/compose/document/theme/BusinessThemeTest.java b/src/test/java/com/demcha/compose/document/theme/BusinessThemeTest.java index a1027029..42890767 100644 --- a/src/test/java/com/demcha/compose/document/theme/BusinessThemeTest.java +++ b/src/test/java/com/demcha/compose/document/theme/BusinessThemeTest.java @@ -149,4 +149,87 @@ void allThreeBuiltInThemesAreDistinct() { assertThat(modern.palette()).isNotEqualTo(executive.palette()); assertThat(classic.text().h1()).isNotEqualTo(modern.text().h1()); } + + // --- Contemporary themes added in v1.6.8 ------------------------------- + + @Test + void nordicThemeHasAllTokensNonNullAndUsesGenerousSpacing() { + BusinessTheme theme = BusinessTheme.nordic(); + assertThat(theme.name()).isEqualTo("nordic"); + assertThat(theme.palette()).isNotNull(); + assertThat(theme.spacing()).isNotNull(); + assertThat(theme.text()).isNotNull(); + assertThat(theme.table()).isNotNull(); + assertThat(theme.pageBackground()).isNull(); + // Nordic is tuned for whitespace — every step is at least as wide + // as the default scale, and md is strictly wider. + SpacingScale standard = SpacingScale.defaultScale(); + assertThat(theme.spacing().md()).isGreaterThan(standard.md()); + assertThat(theme.spacing().xl()).isGreaterThan(standard.xl()); + } + + @Test + void editorialThemeUsesTimesRomanBodyAndCreamPageBackground() { + BusinessTheme theme = BusinessTheme.editorial(); + assertThat(theme.name()).isEqualTo("editorial"); + assertThat(theme.text().body().fontName().name()).isEqualTo("Times-Roman"); + assertThat(theme.text().h1().fontName().name()).isEqualTo("Times-Roman"); + // Cream page background distinguishes editorial from the + // strictly-white classic theme. + assertThat(theme.pageBackground()).isNotNull(); + assertThat(theme.pageBackground()).isEqualTo(theme.palette().surface()); + } + + @Test + void cinematicThemeInvertsPaletteToLightTextOnDarkSurface() { + BusinessTheme theme = BusinessTheme.cinematic(); + assertThat(theme.name()).isEqualTo("cinematic"); + // The defining trait: surface is dark, primary/text is light. + Color surface = theme.palette().surface().color(); + Color textPrimary = theme.palette().textPrimary().color(); + int surfaceLuminance = (surface.getRed() + surface.getGreen() + surface.getBlue()) / 3; + int textLuminance = (textPrimary.getRed() + textPrimary.getGreen() + textPrimary.getBlue()) / 3; + assertThat(surfaceLuminance).isLessThan(64); // genuinely dark surface + assertThat(textLuminance).isGreaterThan(192); // genuinely light text + // And the surface doubles as the page background so the moody + // look fills the page edges too. + assertThat(theme.pageBackground()).isEqualTo(theme.palette().surface()); + } + + @Test + void monochromeThemeIsPureBlackOnWhiteWithBoldYellowAccent() { + BusinessTheme theme = BusinessTheme.monochrome(); + assertThat(theme.name()).isEqualTo("monochrome"); + assertThat(theme.palette().primary().color()).isEqualTo(Color.BLACK); + assertThat(theme.palette().surface().color()).isEqualTo(Color.WHITE); + // The single accent is the entire identity of the theme — assert + // it leans yellow (R and G high, B low) rather than pinning the + // exact RGB, so a future shade tweak does not break the test. + Color accent = theme.palette().accent().color(); + assertThat(accent.getRed()).isGreaterThan(200); + assertThat(accent.getGreen()).isGreaterThan(150); + assertThat(accent.getBlue()).isLessThan(80); + } + + @Test + void allSevenBuiltInThemesArePairwiseDistinctByPalette() { + BusinessTheme[] all = { + BusinessTheme.classic(), + BusinessTheme.modern(), + BusinessTheme.executive(), + BusinessTheme.nordic(), + BusinessTheme.editorial(), + BusinessTheme.cinematic(), + BusinessTheme.monochrome() + }; + for (int i = 0; i < all.length; i++) { + for (int j = i + 1; j < all.length; j++) { + assertThat(all[i].palette()) + .as("Themes '%s' and '%s' must have distinct palettes", + all[i].name(), all[j].name()) + .isNotEqualTo(all[j].palette()); + assertThat(all[i].name()).isNotEqualTo(all[j].name()); + } + } + } }