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