From 26f259ddd8b01b1e4587f4b632d9a26eff654fea Mon Sep 17 00:00:00 2001 From: m57 <41645758+Hy4ri@users.noreply.github.com> Date: Wed, 30 Sep 2026 21:30:18 +0300 Subject: [PATCH] feat(theme): add Garnet preset with deep red accents (#1418) --- DESIGN.md | 2 +- README.md | 2 +- .../com/m57/hermescontrol/theme/THEMES.md | 13 +- .../java/com/m57/hermescontrol/theme/Theme.kt | 2 +- .../m57/hermescontrol/theme/ThemeRegistry.kt | 2 + .../theme/presets/GarnetScheme.kt | 185 ++++++++++++++++++ .../settings/components/AppearanceSection.kt | 1 + app/src/main/res/values/strings.xml | 1 + 8 files changed, 203 insertions(+), 5 deletions(-) create mode 100644 app/src/main/java/com/m57/hermescontrol/theme/presets/GarnetScheme.kt diff --git a/DESIGN.md b/DESIGN.md index 9908486b..376fb2ae 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -41,7 +41,7 @@ The visual language follows Material 3 guidelines strictly adapted for developer - **Core Principle**: Zero fluff. Fast render cycles, explicit states, flat surfaces, and zero visual ambiguity. ## Colors -The app supports 6 built-in presets (`Default/Slate`, `Monochrome`, `Gruvbox`, `Catppuccin`, `AMOLED`, `Nord`), plus optional Android 12+ wallpaper theming. `Theme.kt` selects the preset; `PaletteTemplate.kt` maps its colors to Material slots. AMOLED is dark-only; light mode falls back to Default. +The app supports 7 built-in presets (`Default/Slate`, `Monochrome`, `Gruvbox`, `Catppuccin`, `AMOLED`, `Nord`, `Garnet`), plus optional Android 12+ wallpaper theming. `Theme.kt` selects the preset; `PaletteTemplate.kt` maps its colors to Material slots. AMOLED is dark-only; light mode falls back to Default. 1. **Surfaces**: - `MaterialTheme.colorScheme.background`: Main canvas and full-bleed chat background. diff --git a/README.md b/README.md index 76b4aefe..10b40fc1 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ If Hermes Mobile is useful to you, consider supporting its development on [Ko-fi - **Gateway Status:** Monitor WebSocket connection, MCP servers, messaging channels, and OAuth providers. - **Productivity:** View and manage tasks via integrated Kanban boards, track agent milestones, and browse session history. - **Analytics & Billing:** Usage analytics dashboard and billing/subscription management. -- **Theming:** 6 built-in color presets (Default, Monochrome, Gruvbox, Catppuccin, AMOLED, Nord) plus Material You dynamic colors on supported devices. +- **Theming:** 7 built-in color presets (Default, Monochrome, Gruvbox, Catppuccin, AMOLED, Nord, Garnet) plus Material You dynamic colors on supported devices. - **Modern UX:** Native Material 3 design with pull-to-refresh, scroll-aware TopBar, and customizable bottom navigation. --- diff --git a/app/src/main/java/com/m57/hermescontrol/theme/THEMES.md b/app/src/main/java/com/m57/hermescontrol/theme/THEMES.md index 12f57d82..dec68b80 100644 --- a/app/src/main/java/com/m57/hermescontrol/theme/THEMES.md +++ b/app/src/main/java/com/m57/hermescontrol/theme/THEMES.md @@ -25,7 +25,8 @@ app/src/main/java/com/m57/hermescontrol/theme/ ├── GruvboxScheme.kt ├── CatppuccinScheme.kt ├── AmoledScheme.kt # dark-only (ThemeMode.DARK_ONLY) - └── NordScheme.kt + ├── NordScheme.kt + └── GarnetScheme.kt ``` ## The template — one shape for every theme @@ -95,7 +96,7 @@ Enabled Material components also have a component-state contract: `ThemeComponentContrastTest` checks the pinned Material role combinations in every resolved preset/mode. `ThemeComponentGalleryTest` reads the installed Compose -public color defaults and verifies rendered pixels for all twelve preset/mode +public color defaults and verifies rendered pixels for all fourteen preset/mode combinations, including focused/error fields. Disabled controls are deliberately excluded from the enabled contrast contract; Material uses opacity to mute them. Settings switches use Material defaults (`primary` track / `onPrimary` thumb), @@ -105,6 +106,14 @@ Outlines must contrast against both surface and the unchecked switch track (`surfaceContainerHighest`). Named palettes may need documented derived primary or error tones for direct text; fixed roles still preserve their upstream accents. +## Garnet + +Garnet preserves Default's slate surface ladder, typography, and semantic status +colors. Its seed is `#990000`, used as the light primary and dark primary +container. Dark primary/text/icons use a lighter `#FFB4AB` for contrast; rose +secondary and copper tertiary roles, their containers, fixed accents, inverse +primary, and surface tint follow the new accent family. Both modes are shipped. + ## Theme modes A theme declares which modes it ships via the factory you build it with: diff --git a/app/src/main/java/com/m57/hermescontrol/theme/Theme.kt b/app/src/main/java/com/m57/hermescontrol/theme/Theme.kt index 7931c9ad..1bef1384 100644 --- a/app/src/main/java/com/m57/hermescontrol/theme/Theme.kt +++ b/app/src/main/java/com/m57/hermescontrol/theme/Theme.kt @@ -17,7 +17,7 @@ import kotlinx.serialization.Serializable enum class ThemePreference { SYSTEM, LIGHT, DARK } @Serializable -enum class ThemePreset { DEFAULT, MONOCHROME, GRUVBOX, CATPPUCCIN, AMOLED, NORD } +enum class ThemePreset { DEFAULT, MONOCHROME, GRUVBOX, CATPPUCCIN, AMOLED, NORD, GARNET } val LocalThemePreference = compositionLocalOf { ThemePreference.SYSTEM } val LocalThemePreset = compositionLocalOf { ThemePreset.DEFAULT } diff --git a/app/src/main/java/com/m57/hermescontrol/theme/ThemeRegistry.kt b/app/src/main/java/com/m57/hermescontrol/theme/ThemeRegistry.kt index 9d475bf8..20e1f5cb 100644 --- a/app/src/main/java/com/m57/hermescontrol/theme/ThemeRegistry.kt +++ b/app/src/main/java/com/m57/hermescontrol/theme/ThemeRegistry.kt @@ -3,6 +3,7 @@ package com.m57.hermescontrol.theme import com.m57.hermescontrol.theme.presets.AmoledTheme import com.m57.hermescontrol.theme.presets.CatppuccinTheme import com.m57.hermescontrol.theme.presets.DefaultTheme +import com.m57.hermescontrol.theme.presets.GarnetTheme import com.m57.hermescontrol.theme.presets.GruvboxTheme import com.m57.hermescontrol.theme.presets.MonochromeTheme import com.m57.hermescontrol.theme.presets.NordTheme @@ -21,6 +22,7 @@ internal val ThemeRegistry = ThemeDefinition(ThemePreset.CATPPUCCIN, CatppuccinTheme), ThemeDefinition(ThemePreset.AMOLED, AmoledTheme), ThemeDefinition(ThemePreset.NORD, NordTheme), + ThemeDefinition(ThemePreset.GARNET, GarnetTheme), ) internal fun ThemePreset.palette(): ThemePalette = ThemeRegistry.single { it.preset == this }.palette diff --git a/app/src/main/java/com/m57/hermescontrol/theme/presets/GarnetScheme.kt b/app/src/main/java/com/m57/hermescontrol/theme/presets/GarnetScheme.kt new file mode 100644 index 00000000..b9b737a3 --- /dev/null +++ b/app/src/main/java/com/m57/hermescontrol/theme/presets/GarnetScheme.kt @@ -0,0 +1,185 @@ +package com.m57.hermescontrol.theme.presets + +import androidx.compose.ui.graphics.Color +import com.m57.hermescontrol.theme.HermesStatusColors +import com.m57.hermescontrol.theme.PaletteColors +import com.m57.hermescontrol.theme.buildTheme + +// --------------------------------------------------------------------- +// Default slate surfaces retained; accents use the Garnet red family. +// Garnet Dark — named swatches +// --------------------------------------------------------------------- + +private val GarnetDarkBg0 = Color(0xFF0F1416) +private val GarnetDarkBg1 = Color(0xFF171C1E) +private val GarnetDarkBg2 = Color(0xFF1B2022) +private val GarnetDarkBg3 = Color(0xFF252B2D) +private val GarnetDarkBg4 = Color(0xFF303638) +private val GarnetDarkFg1 = Color(0xFFDEE3E5) +private val GarnetDarkFg3 = Color(0xFFBEC8CA) +private val GarnetGray = Color(0xFF889294) // same neutral gray in both modes + +// "Bright" accents — high-visibility variants against dark surfaces. +private val GarnetBrightCopper = Color(0xFFEABD96) +private val GarnetBrightRose = Color(0xFFE5BDB8) +private val GarnetBrightGreen = Color(0xFF81D692) +private val GarnetBrightYellow = Color(0xFFECC248) +private val GarnetBrightRed = Color(0xFFFFB4AB) +private val GarnetBrightInfo = Color(0xFF8ECEFF) + +// --------------------------------------------------------------------- +// Garnet Light — named swatches +// --------------------------------------------------------------------- + +private val GarnetLightBg0 = Color(0xFFF5FAFC) +private val GarnetLightBg1 = Color(0xFFEFF4F6) +private val GarnetLightBg2 = Color(0xFFE9EEF0) +private val GarnetLightBg3 = Color(0xFFE3E8EA) +private val GarnetLightBg4 = Color(0xFFDEE3E5) +private val GarnetLightFg1 = Color(0xFF171C1E) +private val GarnetLightFg3 = Color(0xFF3F484A) + +// "Faded" accents — muted variants for light background contrast. +private val GarnetAccent = Color(0xFF990000) +private val GarnetFadedCopper = Color(0xFF805531) +private val GarnetFadedRose = Color(0xFF775651) +private val GarnetFadedGreen = Color(0xFF126D2B) +private val GarnetFadedYellow = Color(0xFF725C00) +private val GarnetFadedRed = Color(0xFFBA1A1A) +private val GarnetFadedInfo = Color(0xFF006393) + +// #990000 is the seed and light primary. Its contrast on the dark canvas is only +// 2.08:1; #FFB4AB supplies readable dark-mode text/icons (10.93:1). +private val GarnetLightRedContainer = Color(0xFFFFDAD5) +private val GarnetDarkRoseContainer = Color(0xFF573935) +private val GarnetDarkCopperContainer = Color(0xFF634025) +private val GarnetLightCopperContainer = Color(0xFFFFDCC1) + +/** Garnet — Default slate surfaces with a #990000 red accent and matching warm tones. */ +val GarnetTheme = + buildTheme( + dark = + PaletteColors( + primary = GarnetBrightRed, + onPrimary = GarnetDarkBg0, + primaryContainer = GarnetAccent, + onPrimaryContainer = GarnetLightBg0, + secondary = GarnetBrightRose, + onSecondary = GarnetDarkBg0, + secondaryContainer = GarnetDarkRoseContainer, + onSecondaryContainer = GarnetBrightRose, + tertiary = GarnetBrightCopper, + onTertiary = GarnetDarkBg0, + tertiaryContainer = GarnetDarkCopperContainer, + onTertiaryContainer = GarnetBrightCopper, + background = GarnetDarkBg0, + onBackground = GarnetDarkFg1, + surface = GarnetDarkBg0, + onSurface = GarnetDarkFg1, + surfaceVariant = GarnetDarkBg1, + onSurfaceVariant = GarnetDarkFg3, + surfaceDim = GarnetDarkBg0, + surfaceBright = GarnetDarkBg4, + primaryFixed = GarnetBrightRed, + primaryFixedDim = GarnetBrightRed, + onPrimaryFixed = GarnetDarkBg0, + onPrimaryFixedVariant = GarnetDarkBg0, + secondaryFixed = GarnetBrightRose, + secondaryFixedDim = GarnetBrightRose, + onSecondaryFixed = GarnetDarkBg0, + onSecondaryFixedVariant = GarnetDarkBg0, + tertiaryFixed = GarnetBrightCopper, + tertiaryFixedDim = GarnetBrightCopper, + onTertiaryFixed = GarnetDarkBg0, + onTertiaryFixedVariant = GarnetDarkBg0, + surfaceContainerLowest = GarnetDarkBg0, + surfaceContainerLow = GarnetDarkBg1, + surfaceContainer = GarnetDarkBg2, + surfaceContainerHigh = GarnetDarkBg3, + surfaceContainerHighest = GarnetDarkBg4, + inverseSurface = GarnetDarkFg1, + inverseOnSurface = GarnetDarkBg0, + inversePrimary = GarnetAccent, + outline = GarnetGray, + outlineVariant = GarnetDarkBg3, + scrim = GarnetDarkBg0, + status = + HermesStatusColors( + success = GarnetBrightGreen, + successContainer = GarnetDarkBg3, + onSuccess = GarnetDarkBg0, + warning = GarnetBrightYellow, + warningContainer = GarnetDarkBg3, + onWarning = GarnetDarkBg0, + error = GarnetBrightRed, + errorContainer = GarnetDarkBg3, + onError = GarnetDarkBg0, + onErrorContainer = GarnetBrightRed, + info = GarnetBrightInfo, + infoContainer = GarnetDarkBg3, + onInfo = GarnetDarkBg0, + ), + ), + light = + PaletteColors( + primary = GarnetAccent, + onPrimary = GarnetLightBg0, + primaryContainer = GarnetLightRedContainer, + onPrimaryContainer = GarnetAccent, + secondary = GarnetFadedRose, + onSecondary = GarnetLightBg0, + secondaryContainer = GarnetLightRedContainer, + onSecondaryContainer = GarnetFadedRose, + tertiary = GarnetFadedCopper, + onTertiary = GarnetLightBg0, + tertiaryContainer = GarnetLightCopperContainer, + onTertiaryContainer = GarnetFadedCopper, + background = GarnetLightBg0, + onBackground = GarnetLightFg1, + surface = GarnetLightBg0, + onSurface = GarnetLightFg1, + surfaceVariant = GarnetLightBg1, + onSurfaceVariant = GarnetLightFg3, + surfaceDim = GarnetLightBg4, + surfaceBright = GarnetLightBg0, + primaryFixed = GarnetBrightRed, + primaryFixedDim = GarnetBrightRed, + onPrimaryFixed = GarnetDarkBg0, + onPrimaryFixedVariant = GarnetDarkBg0, + secondaryFixed = GarnetBrightRose, + secondaryFixedDim = GarnetBrightRose, + onSecondaryFixed = GarnetDarkBg0, + onSecondaryFixedVariant = GarnetDarkBg0, + tertiaryFixed = GarnetBrightCopper, + tertiaryFixedDim = GarnetBrightCopper, + onTertiaryFixed = GarnetDarkBg0, + onTertiaryFixedVariant = GarnetDarkBg0, + surfaceContainerLowest = GarnetLightBg0, + surfaceContainerLow = GarnetLightBg1, + surfaceContainer = GarnetLightBg2, + surfaceContainerHigh = GarnetLightBg3, + surfaceContainerHighest = GarnetLightBg4, + inverseSurface = GarnetLightFg1, + inverseOnSurface = GarnetLightBg0, + inversePrimary = GarnetBrightRed, + outline = GarnetLightFg3, + outlineVariant = GarnetLightBg3, + scrim = GarnetLightBg0, + status = + HermesStatusColors( + success = GarnetFadedGreen, + successContainer = GarnetLightBg3, + onSuccess = GarnetLightBg0, + warning = GarnetFadedYellow, + warningContainer = GarnetLightBg3, + onWarning = GarnetLightBg0, + error = GarnetFadedRed, + errorContainer = GarnetLightBg3, + onError = GarnetLightBg0, + onErrorContainer = GarnetFadedRed, + info = GarnetFadedInfo, + infoContainer = GarnetLightBg3, + onInfo = GarnetLightBg0, + ), + ), + ) diff --git a/app/src/main/java/com/m57/hermescontrol/ui/settings/components/AppearanceSection.kt b/app/src/main/java/com/m57/hermescontrol/ui/settings/components/AppearanceSection.kt index a3d9c37d..5b5ae0fa 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/settings/components/AppearanceSection.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/settings/components/AppearanceSection.kt @@ -164,6 +164,7 @@ private fun ThemePreset.label(): String = ThemePreset.CATPPUCCIN -> R.string.theme_preset_catppuccin ThemePreset.AMOLED -> R.string.theme_preset_amoled ThemePreset.NORD -> R.string.theme_preset_nord + ThemePreset.GARNET -> R.string.theme_preset_garnet }, ) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f5916aa7..2bca4211 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -365,6 +365,7 @@ Catppuccin AMOLED Black (Dark only) Nord + Garnet Language