feat(theme): add Garnet preset with deep red accents (#1418)
This commit is contained in:
@@ -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.
|
- **Core Principle**: Zero fluff. Fast render cycles, explicit states, flat surfaces, and zero visual ambiguity.
|
||||||
|
|
||||||
## Colors
|
## 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**:
|
1. **Surfaces**:
|
||||||
- `MaterialTheme.colorScheme.background`: Main canvas and full-bleed chat background.
|
- `MaterialTheme.colorScheme.background`: Main canvas and full-bleed chat background.
|
||||||
|
|||||||
@@ -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.
|
- **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.
|
- **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.
|
- **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.
|
- **Modern UX:** Native Material 3 design with pull-to-refresh, scroll-aware TopBar, and customizable bottom navigation.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ app/src/main/java/com/m57/hermescontrol/theme/
|
|||||||
├── GruvboxScheme.kt
|
├── GruvboxScheme.kt
|
||||||
├── CatppuccinScheme.kt
|
├── CatppuccinScheme.kt
|
||||||
├── AmoledScheme.kt # dark-only (ThemeMode.DARK_ONLY)
|
├── AmoledScheme.kt # dark-only (ThemeMode.DARK_ONLY)
|
||||||
└── NordScheme.kt
|
├── NordScheme.kt
|
||||||
|
└── GarnetScheme.kt
|
||||||
```
|
```
|
||||||
|
|
||||||
## The template — one shape for every theme
|
## 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
|
`ThemeComponentContrastTest` checks the pinned Material role combinations in every
|
||||||
resolved preset/mode. `ThemeComponentGalleryTest` reads the installed Compose
|
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
|
combinations, including focused/error fields. Disabled controls are deliberately
|
||||||
excluded from the enabled contrast contract; Material uses opacity to mute them.
|
excluded from the enabled contrast contract; Material uses opacity to mute them.
|
||||||
Settings switches use Material defaults (`primary` track / `onPrimary` thumb),
|
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
|
(`surfaceContainerHighest`). Named palettes may need documented derived primary
|
||||||
or error tones for direct text; fixed roles still preserve their upstream accents.
|
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
|
## Theme modes
|
||||||
|
|
||||||
A theme declares which modes it ships via the factory you build it with:
|
A theme declares which modes it ships via the factory you build it with:
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import kotlinx.serialization.Serializable
|
|||||||
enum class ThemePreference { SYSTEM, LIGHT, DARK }
|
enum class ThemePreference { SYSTEM, LIGHT, DARK }
|
||||||
|
|
||||||
@Serializable
|
@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 LocalThemePreference = compositionLocalOf { ThemePreference.SYSTEM }
|
||||||
val LocalThemePreset = compositionLocalOf { ThemePreset.DEFAULT }
|
val LocalThemePreset = compositionLocalOf { ThemePreset.DEFAULT }
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.m57.hermescontrol.theme
|
|||||||
import com.m57.hermescontrol.theme.presets.AmoledTheme
|
import com.m57.hermescontrol.theme.presets.AmoledTheme
|
||||||
import com.m57.hermescontrol.theme.presets.CatppuccinTheme
|
import com.m57.hermescontrol.theme.presets.CatppuccinTheme
|
||||||
import com.m57.hermescontrol.theme.presets.DefaultTheme
|
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.GruvboxTheme
|
||||||
import com.m57.hermescontrol.theme.presets.MonochromeTheme
|
import com.m57.hermescontrol.theme.presets.MonochromeTheme
|
||||||
import com.m57.hermescontrol.theme.presets.NordTheme
|
import com.m57.hermescontrol.theme.presets.NordTheme
|
||||||
@@ -21,6 +22,7 @@ internal val ThemeRegistry =
|
|||||||
ThemeDefinition(ThemePreset.CATPPUCCIN, CatppuccinTheme),
|
ThemeDefinition(ThemePreset.CATPPUCCIN, CatppuccinTheme),
|
||||||
ThemeDefinition(ThemePreset.AMOLED, AmoledTheme),
|
ThemeDefinition(ThemePreset.AMOLED, AmoledTheme),
|
||||||
ThemeDefinition(ThemePreset.NORD, NordTheme),
|
ThemeDefinition(ThemePreset.NORD, NordTheme),
|
||||||
|
ThemeDefinition(ThemePreset.GARNET, GarnetTheme),
|
||||||
)
|
)
|
||||||
|
|
||||||
internal fun ThemePreset.palette(): ThemePalette = ThemeRegistry.single { it.preset == this }.palette
|
internal fun ThemePreset.palette(): ThemePalette = ThemeRegistry.single { it.preset == this }.palette
|
||||||
|
|||||||
@@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
@@ -164,6 +164,7 @@ private fun ThemePreset.label(): String =
|
|||||||
ThemePreset.CATPPUCCIN -> R.string.theme_preset_catppuccin
|
ThemePreset.CATPPUCCIN -> R.string.theme_preset_catppuccin
|
||||||
ThemePreset.AMOLED -> R.string.theme_preset_amoled
|
ThemePreset.AMOLED -> R.string.theme_preset_amoled
|
||||||
ThemePreset.NORD -> R.string.theme_preset_nord
|
ThemePreset.NORD -> R.string.theme_preset_nord
|
||||||
|
ThemePreset.GARNET -> R.string.theme_preset_garnet
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -365,6 +365,7 @@
|
|||||||
<string name="theme_preset_catppuccin">Catppuccin</string>
|
<string name="theme_preset_catppuccin">Catppuccin</string>
|
||||||
<string name="theme_preset_amoled">AMOLED Black (Dark only)</string>
|
<string name="theme_preset_amoled">AMOLED Black (Dark only)</string>
|
||||||
<string name="theme_preset_nord">Nord</string>
|
<string name="theme_preset_nord">Nord</string>
|
||||||
|
<string name="theme_preset_garnet">Garnet</string>
|
||||||
|
|
||||||
<!-- Language -->
|
<!-- Language -->
|
||||||
<string name="settings_item_language">Language</string>
|
<string name="settings_item_language">Language</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user