fix(theme): complete color and component contrast contracts (#1417)
This commit is contained in:
22 files changed
+1013
-318
No files matched your search
@@ -10,8 +10,13 @@ import androidx.compose.ui.graphics.luminance
|
||||
* Material You changes the *primary* / *surface* palette based on the user's
|
||||
* wallpaper, but success / warning / error / info colours must stay
|
||||
* semantically correct (green = good, red = bad) regardless of what the
|
||||
* wallpaper-derived palette would choose. This struct is provided via
|
||||
* [LocalHermesStatusColors] and should be used for all status indicators,
|
||||
* wallpaper-derived palette would choose.
|
||||
*
|
||||
* `onSuccess`/`onWarning`/`onError`/`onInfo` are foregrounds on the matching
|
||||
* status fills, not on their container colors. Reusable status badges use
|
||||
* these fill/on pairs; error-container content uses `onErrorContainer`.
|
||||
*
|
||||
* Use [LocalHermesStatusColors] for all status indicators,
|
||||
* badges, and semantic feedback colours.
|
||||
*/
|
||||
data class HermesStatusColors(
|
||||
|
||||
@@ -28,6 +28,9 @@ enum class ThemeMode { FULL, DARK_ONLY, LIGHT_ONLY }
|
||||
*
|
||||
* The Material error slots are derived from [status] — the theme author
|
||||
* defines semantic colors once and the scheme inherits them.
|
||||
* [primary] and the status error are also used directly for text on surfaces.
|
||||
* [outline] must contrast with [surface] and [surfaceContainerHighest], which
|
||||
* Material uses as the unchecked switch track.
|
||||
*/
|
||||
data class PaletteColors(
|
||||
val primary: Color,
|
||||
@@ -48,6 +51,20 @@ data class PaletteColors(
|
||||
val onSurface: Color,
|
||||
val surfaceVariant: Color,
|
||||
val onSurfaceVariant: Color,
|
||||
val surfaceDim: Color,
|
||||
val surfaceBright: Color,
|
||||
val primaryFixed: Color,
|
||||
val primaryFixedDim: Color,
|
||||
val onPrimaryFixed: Color,
|
||||
val onPrimaryFixedVariant: Color,
|
||||
val secondaryFixed: Color,
|
||||
val secondaryFixedDim: Color,
|
||||
val onSecondaryFixed: Color,
|
||||
val onSecondaryFixedVariant: Color,
|
||||
val tertiaryFixed: Color,
|
||||
val tertiaryFixedDim: Color,
|
||||
val onTertiaryFixed: Color,
|
||||
val onTertiaryFixedVariant: Color,
|
||||
val surfaceContainerLowest: Color,
|
||||
val surfaceContainerLow: Color,
|
||||
val surfaceContainer: Color,
|
||||
@@ -123,6 +140,20 @@ private fun darkSchemeOf(c: PaletteColors): ColorScheme =
|
||||
surfaceVariant = c.surfaceVariant,
|
||||
onSurfaceVariant = c.onSurfaceVariant,
|
||||
surfaceTint = c.primary,
|
||||
surfaceDim = c.surfaceDim,
|
||||
surfaceBright = c.surfaceBright,
|
||||
primaryFixed = c.primaryFixed,
|
||||
primaryFixedDim = c.primaryFixedDim,
|
||||
onPrimaryFixed = c.onPrimaryFixed,
|
||||
onPrimaryFixedVariant = c.onPrimaryFixedVariant,
|
||||
secondaryFixed = c.secondaryFixed,
|
||||
secondaryFixedDim = c.secondaryFixedDim,
|
||||
onSecondaryFixed = c.onSecondaryFixed,
|
||||
onSecondaryFixedVariant = c.onSecondaryFixedVariant,
|
||||
tertiaryFixed = c.tertiaryFixed,
|
||||
tertiaryFixedDim = c.tertiaryFixedDim,
|
||||
onTertiaryFixed = c.onTertiaryFixed,
|
||||
onTertiaryFixedVariant = c.onTertiaryFixedVariant,
|
||||
surfaceContainerLowest = c.surfaceContainerLowest,
|
||||
surfaceContainerLow = c.surfaceContainerLow,
|
||||
surfaceContainer = c.surfaceContainer,
|
||||
@@ -161,6 +192,20 @@ private fun lightSchemeOf(c: PaletteColors): ColorScheme =
|
||||
surfaceVariant = c.surfaceVariant,
|
||||
onSurfaceVariant = c.onSurfaceVariant,
|
||||
surfaceTint = c.primary,
|
||||
surfaceDim = c.surfaceDim,
|
||||
surfaceBright = c.surfaceBright,
|
||||
primaryFixed = c.primaryFixed,
|
||||
primaryFixedDim = c.primaryFixedDim,
|
||||
onPrimaryFixed = c.onPrimaryFixed,
|
||||
onPrimaryFixedVariant = c.onPrimaryFixedVariant,
|
||||
secondaryFixed = c.secondaryFixed,
|
||||
secondaryFixedDim = c.secondaryFixedDim,
|
||||
onSecondaryFixed = c.onSecondaryFixed,
|
||||
onSecondaryFixedVariant = c.onSecondaryFixedVariant,
|
||||
tertiaryFixed = c.tertiaryFixed,
|
||||
tertiaryFixedDim = c.tertiaryFixedDim,
|
||||
onTertiaryFixed = c.onTertiaryFixed,
|
||||
onTertiaryFixedVariant = c.onTertiaryFixedVariant,
|
||||
surfaceContainerLowest = c.surfaceContainerLowest,
|
||||
surfaceContainerLow = c.surfaceContainerLow,
|
||||
surfaceContainer = c.surfaceContainer,
|
||||
|
||||
@@ -12,7 +12,8 @@ All theme code is under:
|
||||
|
||||
```
|
||||
app/src/main/java/com/m57/hermescontrol/theme/
|
||||
├── Theme.kt # dispatcher — preset lookup + mode fallback
|
||||
├── Theme.kt # dispatcher — mode fallback + dynamic colors
|
||||
├── ThemeRegistry.kt # single preset/palette registry
|
||||
├── PaletteTemplate.kt # the template — the ONE shape every theme follows
|
||||
├── Color.kt # shared tokens for non-theme code (status fallbacks, code blocks, …)
|
||||
├── HermesStatusColors.kt # semantic status color model (success/warning/error/info + on*)
|
||||
@@ -55,7 +56,14 @@ val MyTheme =
|
||||
The template maps `PaletteColors` into the Material 3 `ColorScheme` slots. The
|
||||
Material `error` slots (`error`, `onError`, `errorContainer`, `onErrorContainer`)
|
||||
are **derived** from the theme's status set — the theme author defines semantic
|
||||
colors once.
|
||||
colors once. `surfaceTint` comes from `primary`. Every other Material color role
|
||||
is required explicitly, including bright/dim surfaces and all fixed accent roles.
|
||||
No role may inherit Material baseline colors. Fixed accent and foreground roles
|
||||
stay identical across a preset’s shipped modes; Fixed/Dim may share an official
|
||||
swatch when the source palette has no accessible tonal ladder.
|
||||
|
||||
Prefer upstream swatches. Document every derived color beside its declaration
|
||||
with the official pairing’s measured contrast and why a derived role is needed.
|
||||
|
||||
## Preset conventions
|
||||
|
||||
@@ -69,8 +77,33 @@ colors once.
|
||||
light text would fail contrast.
|
||||
- **Grayscale status colors** (Monochrome, AMOLED) separate success/warning/
|
||||
error/info by lightness only — consumers must pair with icons or labels.
|
||||
- Every shipped mode's error slot pairs are enforced at **>= 3:1 contrast** by
|
||||
`ThemePaletteTest` — a preset that breaks this fails the unit-test gate.
|
||||
- Normal-text foreground/background pairs, including accent containers, fixed
|
||||
accents, semantic status fills, errors, and the surface ladder, are enforced at **>= 4.5:1 contrast**
|
||||
by `ThemePaletteTest`. The separate full-bleed header guard remains >= 3:1.
|
||||
|
||||
Reusable `StatusBadge` and `SessionLiveStatusIndicator` use a shared
|
||||
`statusBadgeColors` mapping: status fills (`success`, `warning`, `error`, `info`)
|
||||
with their matching `on*` text/icons. `onSuccess` is not a foreground token for
|
||||
`successContainer`. Palette tests exercise this exact renderer mapping in every
|
||||
resolved mode, including fallback modes; neutral badges use Material tokens.
|
||||
|
||||
Enabled Material components also have a component-state contract:
|
||||
- Graphics/boundaries target >= 3:1: switch thumbs/tracks and borders, radio and
|
||||
checkbox states, slider active/inactive tracks, text-field borders, and segmented borders.
|
||||
- Primary/error text and field labels on surface/background target >= 4.5:1.
|
||||
- Segmented content uses the corresponding container foreground at >= 4.5:1.
|
||||
|
||||
`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
|
||||
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),
|
||||
not the reversed `primaryContainer` track / `primary` thumb combination.
|
||||
|
||||
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.
|
||||
|
||||
## Theme modes
|
||||
|
||||
@@ -87,37 +120,33 @@ doesn't ship — never to a sibling preset.
|
||||
|
||||
## The dispatcher
|
||||
|
||||
`Theme.kt` is a **pure lookup** — no special-casing. Given a `ThemePreset` and
|
||||
a dark flag, `themeFor()` returns the preset's `ThemePalette`, then the scheme /
|
||||
status are read off it (with the mode fallback above):
|
||||
`ThemeRegistry.kt` maps every `ThemePreset` to one `ThemePalette`. Resolution,
|
||||
UI selection, and palette tests use this registry. An exact enum/registry coverage
|
||||
test rejects missing, duplicate, or reordered entries. Android string resources
|
||||
stay in the UI’s exhaustive label mapping, outside the palette model.
|
||||
|
||||
```kotlin
|
||||
private fun themeFor(preset: ThemePreset): ThemePalette = when (preset) {
|
||||
ThemePreset.DEFAULT -> DefaultTheme
|
||||
ThemePreset.MONOCHROME -> MonochromeTheme
|
||||
// …one line per preset…
|
||||
}
|
||||
```
|
||||
`Theme.kt` reads `preset.palette()` and applies the mode fallback above.
|
||||
|
||||
Dynamic (Material You) color on API 31+ can optionally override the preset scheme when
|
||||
`useDynamicColors = true` (defaults to `false`). Semantic status colors are always resolved from the
|
||||
active preset via `LocalHermesStatusColors`.
|
||||
active preset via `LocalHermesStatusColors`. The preset selector stays enabled
|
||||
and explains that it controls status colors while Material You supplies app colors.
|
||||
|
||||
## Adding a new theme
|
||||
|
||||
1. **Create** `presets/<Name>Scheme.kt` as a template fill (see above) —
|
||||
`buildTheme` for a full theme, `buildThemeDarkOnly` / `buildThemeLightOnly`
|
||||
for single-mode themes. Every status color must be bespoke — no aliasing.
|
||||
2. **Add 1 line** to `themeFor()` in `Theme.kt` + import the new val.
|
||||
2. **Register** the new palette in `ThemeRegistry.kt`.
|
||||
3. **Add the enum entry** `MY_THEME` to `ThemePreset` in `Theme.kt`.
|
||||
4. **Wire the UI**: add the label + selection in
|
||||
4. **Wire the UI**: add the exhaustive label mapping in
|
||||
`ui/settings/components/AppearanceSection.kt` (and any string resource).
|
||||
5. **Verify**:
|
||||
```bash
|
||||
./gradlew ktlintCheck testDebugUnitTest
|
||||
```
|
||||
`ThemePaletteTest` asserts >= 3:1 contrast on every shipped mode's error
|
||||
slot pairs and the ThemeMode invariants — it must stay green.
|
||||
`ThemePaletteTest` checks text contrast, complete Material slot mapping,
|
||||
mode-independent fixed roles, registry coverage, and mode/fallback invariants.
|
||||
|
||||
## Conventions
|
||||
|
||||
|
||||
@@ -10,12 +10,7 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
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.GruvboxTheme
|
||||
import com.m57.hermescontrol.theme.presets.MonochromeTheme
|
||||
import com.m57.hermescontrol.theme.presets.NordTheme
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
@@ -28,20 +23,6 @@ val LocalThemePreference = compositionLocalOf { ThemePreference.SYSTEM }
|
||||
val LocalThemePreset = compositionLocalOf { ThemePreset.DEFAULT }
|
||||
val LocalChatFontScale = compositionLocalOf { 1.0f }
|
||||
|
||||
/**
|
||||
* The 6 preset themes — one file each, all built from the same
|
||||
* [PaletteTemplate] shape (see `PaletteTemplate.kt`).
|
||||
*/
|
||||
private fun themeFor(preset: ThemePreset): ThemePalette =
|
||||
when (preset) {
|
||||
ThemePreset.DEFAULT -> DefaultTheme
|
||||
ThemePreset.MONOCHROME -> MonochromeTheme
|
||||
ThemePreset.GRUVBOX -> GruvboxTheme
|
||||
ThemePreset.CATPPUCCIN -> CatppuccinTheme
|
||||
ThemePreset.AMOLED -> AmoledTheme
|
||||
ThemePreset.NORD -> NordTheme
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the Material 3 [ColorScheme] for a preset + dark flag.
|
||||
*
|
||||
@@ -53,7 +34,7 @@ internal fun resolveColorScheme(
|
||||
preset: ThemePreset,
|
||||
darkTheme: Boolean,
|
||||
): ColorScheme {
|
||||
val theme = themeFor(preset)
|
||||
val theme = preset.palette()
|
||||
// DefaultTheme is FULL — its scheme is never null for either mode.
|
||||
return theme.schemeFor(darkTheme) ?: requireNotNull(DefaultTheme.schemeFor(darkTheme))
|
||||
}
|
||||
@@ -66,7 +47,7 @@ internal fun resolveStatusColors(
|
||||
preset: ThemePreset,
|
||||
darkTheme: Boolean,
|
||||
): HermesStatusColors {
|
||||
val theme = themeFor(preset)
|
||||
val theme = preset.palette()
|
||||
// DefaultTheme is FULL — its status colors are never null for either mode.
|
||||
return theme.statusFor(darkTheme) ?: requireNotNull(DefaultTheme.statusFor(darkTheme))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
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.GruvboxTheme
|
||||
import com.m57.hermescontrol.theme.presets.MonochromeTheme
|
||||
import com.m57.hermescontrol.theme.presets.NordTheme
|
||||
|
||||
internal data class ThemeDefinition(
|
||||
val preset: ThemePreset,
|
||||
val palette: ThemePalette,
|
||||
)
|
||||
|
||||
// Regression theme-contract: selection, resolution, and tests share this registry.
|
||||
internal val ThemeRegistry =
|
||||
listOf(
|
||||
ThemeDefinition(ThemePreset.DEFAULT, DefaultTheme),
|
||||
ThemeDefinition(ThemePreset.MONOCHROME, MonochromeTheme),
|
||||
ThemeDefinition(ThemePreset.GRUVBOX, GruvboxTheme),
|
||||
ThemeDefinition(ThemePreset.CATPPUCCIN, CatppuccinTheme),
|
||||
ThemeDefinition(ThemePreset.AMOLED, AmoledTheme),
|
||||
ThemeDefinition(ThemePreset.NORD, NordTheme),
|
||||
)
|
||||
|
||||
internal fun ThemePreset.palette(): ThemePalette = ThemeRegistry.single { it.preset == this }.palette
|
||||
@@ -59,6 +59,20 @@ val AmoledTheme =
|
||||
onSurface = AmoledTextBright,
|
||||
surfaceVariant = AmoledContainerHigh,
|
||||
onSurfaceVariant = AmoledTextDim,
|
||||
surfaceDim = AmoledFloor,
|
||||
surfaceBright = AmoledContainerHighest,
|
||||
primaryFixed = AmoledTextBright,
|
||||
primaryFixedDim = AmoledTextBright,
|
||||
onPrimaryFixed = AmoledFloor,
|
||||
onPrimaryFixedVariant = AmoledFloor,
|
||||
secondaryFixed = AmoledSecondary,
|
||||
secondaryFixedDim = AmoledSecondary,
|
||||
onSecondaryFixed = AmoledFloor,
|
||||
onSecondaryFixedVariant = AmoledFloor,
|
||||
tertiaryFixed = AmoledTertiary,
|
||||
tertiaryFixedDim = AmoledTertiary,
|
||||
onTertiaryFixed = AmoledFloor,
|
||||
onTertiaryFixedVariant = AmoledFloor,
|
||||
surfaceContainerLowest = AmoledFloor,
|
||||
surfaceContainerLow = AmoledFloor,
|
||||
surfaceContainer = AmoledContainer,
|
||||
|
||||
@@ -19,7 +19,6 @@ private val MochaRed = Color(0xFFF38BA8)
|
||||
private val MochaText = Color(0xFFCDD6F4)
|
||||
private val MochaSubtext1 = Color(0xFFBAC2DE)
|
||||
private val MochaSubtext0 = Color(0xFFA6ADC8)
|
||||
private val MochaOverlay0 = Color(0xFF6C7086)
|
||||
private val MochaSurface1 = Color(0xFF45475A)
|
||||
private val MochaSurface0 = Color(0xFF313244)
|
||||
private val MochaBase = Color(0xFF1E1E2E)
|
||||
@@ -31,24 +30,33 @@ private val MochaCrust = Color(0xFF11111B)
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
private val LatteMauve = Color(0xFF8839EF)
|
||||
|
||||
// Derived role, not an upstream Catppuccin swatch: Latte Text/Pink is 3.02:1
|
||||
// and Text/Green is 2.39:1. Deeper ink reaches 5.88:1 and 4.65:1 respectively.
|
||||
private val LatteDeepInk = Color(0xFF202334)
|
||||
|
||||
// Derived role, not an upstream Catppuccin swatch: Latte Base/Blue is only 4.34:1.
|
||||
// White/Blue reaches 4.94:1 for normal text.
|
||||
private val LatteOnBlue = Color(0xFFFFFFFF)
|
||||
private val LatteBlue = Color(0xFF1E66F5)
|
||||
private val LattePink = Color(0xFFEA76CB)
|
||||
private val LatteGreen = Color(0xFF40A02B)
|
||||
private val LatteYellow = Color(0xFFDF8E1D)
|
||||
private val LatteRed = Color(0xFFD20F39)
|
||||
private val LatteText = Color(0xFF4C4F69)
|
||||
|
||||
// Official Latte Subtext0 keeps borders visible against Surface0 (3.20:1).
|
||||
private val LatteSubtext0 = Color(0xFF6C6F85)
|
||||
private val LatteOverlay0 = Color(0xFF9CA0B0)
|
||||
private val LatteText = Color(0xFF4C4F69)
|
||||
private val LatteSurface1 = Color(0xFFBCC0CC)
|
||||
private val LatteSurface0 = Color(0xFFCCD0DA)
|
||||
private val LatteBase = Color(0xFFEFF1F5)
|
||||
private val LatteMantle = Color(0xFFE6E9EF)
|
||||
private val LatteCrust = Color(0xFFDCE0E8)
|
||||
|
||||
// Bespoke tint used only for the light primary container — not part of
|
||||
// the official Catppuccin swatch set, kept local to this file.
|
||||
private val LattePrimaryContainer = Color(0xFFE6CAFA)
|
||||
private val LatteOnPrimaryContainer = Color(0xFF380075)
|
||||
// Regression theme-contract: fixed accents use Mocha in both modes. Fixed/Dim
|
||||
// share official accents rather than inventing tonal steps; on-roles use Crust.
|
||||
// Latte surface containers stop at Surface0 so normal text stays >= 4.5:1.
|
||||
// Upstream: https://github.com/catppuccin/palette/blob/main/palette.json
|
||||
|
||||
/** Catppuccin theme — Mocha (dark) / Latte (light). */
|
||||
val CatppuccinTheme =
|
||||
@@ -73,6 +81,20 @@ val CatppuccinTheme =
|
||||
onSurface = MochaText,
|
||||
surfaceVariant = MochaSurface0,
|
||||
onSurfaceVariant = MochaSubtext0,
|
||||
surfaceDim = MochaBase,
|
||||
surfaceBright = MochaSurface1,
|
||||
primaryFixed = MochaMauve,
|
||||
primaryFixedDim = MochaMauve,
|
||||
onPrimaryFixed = MochaCrust,
|
||||
onPrimaryFixedVariant = MochaCrust,
|
||||
secondaryFixed = MochaBlue,
|
||||
secondaryFixedDim = MochaBlue,
|
||||
onSecondaryFixed = MochaCrust,
|
||||
onSecondaryFixedVariant = MochaCrust,
|
||||
tertiaryFixed = MochaPink,
|
||||
tertiaryFixedDim = MochaPink,
|
||||
onTertiaryFixed = MochaCrust,
|
||||
onTertiaryFixedVariant = MochaCrust,
|
||||
surfaceContainerLowest = MochaCrust,
|
||||
surfaceContainerLow = MochaMantle,
|
||||
surfaceContainer = MochaBase,
|
||||
@@ -81,7 +103,7 @@ val CatppuccinTheme =
|
||||
inverseSurface = MochaText,
|
||||
inverseOnSurface = MochaBase,
|
||||
inversePrimary = LatteMauve,
|
||||
outline = MochaOverlay0,
|
||||
outline = MochaSubtext0,
|
||||
outlineVariant = MochaSurface1,
|
||||
scrim = MochaCrust,
|
||||
status =
|
||||
@@ -105,48 +127,62 @@ val CatppuccinTheme =
|
||||
PaletteColors(
|
||||
primary = LatteMauve,
|
||||
onPrimary = LatteBase,
|
||||
primaryContainer = LattePrimaryContainer,
|
||||
onPrimaryContainer = LatteOnPrimaryContainer,
|
||||
primaryContainer = LatteBase,
|
||||
onPrimaryContainer = LatteMauve,
|
||||
secondary = LatteBlue,
|
||||
onSecondary = LatteBase,
|
||||
onSecondary = LatteOnBlue,
|
||||
secondaryContainer = LatteMantle,
|
||||
onSecondaryContainer = LatteText,
|
||||
tertiary = LattePink,
|
||||
onTertiary = LatteBase,
|
||||
tertiaryContainer = LatteSurface1,
|
||||
onTertiary = LatteDeepInk,
|
||||
tertiaryContainer = LatteSurface0,
|
||||
onTertiaryContainer = LatteText,
|
||||
background = LatteBase,
|
||||
onBackground = LatteText,
|
||||
surface = LatteBase,
|
||||
onSurface = LatteText,
|
||||
surfaceVariant = LatteSurface0,
|
||||
onSurfaceVariant = LatteSubtext0,
|
||||
onSurfaceVariant = LatteText,
|
||||
surfaceDim = LatteSurface0,
|
||||
surfaceBright = LatteBase,
|
||||
primaryFixed = MochaMauve,
|
||||
primaryFixedDim = MochaMauve,
|
||||
onPrimaryFixed = MochaCrust,
|
||||
onPrimaryFixedVariant = MochaCrust,
|
||||
secondaryFixed = MochaBlue,
|
||||
secondaryFixedDim = MochaBlue,
|
||||
onSecondaryFixed = MochaCrust,
|
||||
onSecondaryFixedVariant = MochaCrust,
|
||||
tertiaryFixed = MochaPink,
|
||||
tertiaryFixedDim = MochaPink,
|
||||
onTertiaryFixed = MochaCrust,
|
||||
onTertiaryFixedVariant = MochaCrust,
|
||||
surfaceContainerLowest = LatteCrust,
|
||||
surfaceContainerLow = LatteMantle,
|
||||
surfaceContainer = LatteBase,
|
||||
surfaceContainerHigh = LatteSurface0,
|
||||
surfaceContainerHighest = LatteSurface1,
|
||||
surfaceContainerHighest = LatteSurface0,
|
||||
inverseSurface = LatteText,
|
||||
inverseOnSurface = LatteBase,
|
||||
inversePrimary = MochaMauve,
|
||||
outline = LatteOverlay0,
|
||||
outline = LatteSubtext0,
|
||||
outlineVariant = LatteSurface1,
|
||||
scrim = LatteCrust,
|
||||
status =
|
||||
HermesStatusColors(
|
||||
success = LatteGreen,
|
||||
successContainer = LatteSurface0,
|
||||
onSuccess = LatteBase,
|
||||
onSuccess = LatteDeepInk,
|
||||
warning = LatteYellow,
|
||||
warningContainer = LatteSurface0,
|
||||
onWarning = LatteBase,
|
||||
onWarning = LatteDeepInk,
|
||||
error = LatteRed,
|
||||
errorContainer = LatteSurface0,
|
||||
onError = LatteBase,
|
||||
onErrorContainer = LatteRed,
|
||||
onErrorContainer = LatteText,
|
||||
info = LatteBlue,
|
||||
infoContainer = LatteSurface0,
|
||||
onInfo = LatteBase,
|
||||
onInfo = LatteOnBlue,
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -73,6 +73,20 @@ val DefaultTheme =
|
||||
onSurface = SlateDarkFg1,
|
||||
surfaceVariant = SlateDarkBg1,
|
||||
onSurfaceVariant = SlateDarkFg3,
|
||||
surfaceDim = SlateDarkBg0,
|
||||
surfaceBright = SlateDarkBg4,
|
||||
primaryFixed = SlateBrightTeal,
|
||||
primaryFixedDim = SlateBrightTeal,
|
||||
onPrimaryFixed = SlateDarkBg0,
|
||||
onPrimaryFixedVariant = SlateDarkBg0,
|
||||
secondaryFixed = SlateBrightCyan,
|
||||
secondaryFixedDim = SlateBrightCyan,
|
||||
onSecondaryFixed = SlateDarkBg0,
|
||||
onSecondaryFixedVariant = SlateDarkBg0,
|
||||
tertiaryFixed = SlateBrightBlue,
|
||||
tertiaryFixedDim = SlateBrightBlue,
|
||||
onTertiaryFixed = SlateDarkBg0,
|
||||
onTertiaryFixedVariant = SlateDarkBg0,
|
||||
surfaceContainerLowest = SlateDarkBg0,
|
||||
surfaceContainerLow = SlateDarkBg1,
|
||||
surfaceContainer = SlateDarkBg2,
|
||||
@@ -121,6 +135,20 @@ val DefaultTheme =
|
||||
onSurface = SlateLightFg1,
|
||||
surfaceVariant = SlateLightBg1,
|
||||
onSurfaceVariant = SlateLightFg3,
|
||||
surfaceDim = SlateLightBg4,
|
||||
surfaceBright = SlateLightBg0,
|
||||
primaryFixed = SlateBrightTeal,
|
||||
primaryFixedDim = SlateBrightTeal,
|
||||
onPrimaryFixed = SlateDarkBg0,
|
||||
onPrimaryFixedVariant = SlateDarkBg0,
|
||||
secondaryFixed = SlateBrightCyan,
|
||||
secondaryFixedDim = SlateBrightCyan,
|
||||
onSecondaryFixed = SlateDarkBg0,
|
||||
onSecondaryFixedVariant = SlateDarkBg0,
|
||||
tertiaryFixed = SlateBrightBlue,
|
||||
tertiaryFixedDim = SlateBrightBlue,
|
||||
onTertiaryFixed = SlateDarkBg0,
|
||||
onTertiaryFixedVariant = SlateDarkBg0,
|
||||
surfaceContainerLowest = SlateLightBg0,
|
||||
surfaceContainerLow = SlateLightBg1,
|
||||
surfaceContainer = SlateLightBg2,
|
||||
@@ -129,7 +157,7 @@ val DefaultTheme =
|
||||
inverseSurface = SlateLightFg1,
|
||||
inverseOnSurface = SlateLightBg0,
|
||||
inversePrimary = SlateBrightTeal,
|
||||
outline = SlateGray,
|
||||
outline = SlateLightFg3,
|
||||
outlineVariant = SlateLightBg3,
|
||||
scrim = SlateLightBg0,
|
||||
status =
|
||||
|
||||
@@ -9,18 +9,20 @@ import com.m57.hermescontrol.theme.buildTheme
|
||||
// Gruvbox Dark (medium contrast) — named swatches
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
// Official hard-contrast bg0; medium bg0/red only reaches 4.29:1.
|
||||
private val GruvboxDarkInk = Color(0xFF1D2021)
|
||||
private val GruvboxDarkBg0 = Color(0xFF282828)
|
||||
private val GruvboxDarkBg1 = Color(0xFF3C3836)
|
||||
private val GruvboxDarkBg2 = Color(0xFF504945)
|
||||
private val GruvboxDarkBg3 = Color(0xFF665C54)
|
||||
private val GruvboxDarkBg4 = Color(0xFF7C6F64)
|
||||
private val GruvboxDarkFg1 = Color(0xFFEBDBB2)
|
||||
private val GruvboxDarkFg2 = Color(0xFFD5C4A1)
|
||||
private val GruvboxDarkFg3 = Color(0xFFBDAE93)
|
||||
private val GruvboxGray = Color(0xFF928374) // same neutral gray in both modes
|
||||
|
||||
// "Bright" accents — the variants gruvbox uses against a dark background.
|
||||
private val GruvboxBrightRed = Color(0xFFFB4934)
|
||||
// Derived role, not an upstream swatch: bright red/bg0 is only 4.29:1.
|
||||
// Lighter red reaches 4.92:1 for error text and field labels on bg0.
|
||||
private val GruvboxErrorText = Color(0xFFFF5F4F)
|
||||
private val GruvboxBrightGreen = Color(0xFFB8BB26)
|
||||
private val GruvboxBrightYellow = Color(0xFFFABD2F)
|
||||
private val GruvboxBrightBlue = Color(0xFF83A598)
|
||||
@@ -35,7 +37,6 @@ private val GruvboxLightBg0 = Color(0xFFFBF1C7)
|
||||
private val GruvboxLightBg1 = Color(0xFFEBDBB2)
|
||||
private val GruvboxLightBg2 = Color(0xFFD5C4A1)
|
||||
private val GruvboxLightBg3 = Color(0xFFBDAE93)
|
||||
private val GruvboxLightBg4 = Color(0xFFA89984)
|
||||
private val GruvboxLightFg1 = Color(0xFF3C3836)
|
||||
private val GruvboxLightFg2 = Color(0xFF504945)
|
||||
private val GruvboxLightFg3 = Color(0xFF665C54)
|
||||
@@ -43,28 +44,38 @@ private val GruvboxLightFg3 = Color(0xFF665C54)
|
||||
// "Faded" accents — the muted variants gruvbox uses against a light
|
||||
// background, so contrast holds up without the neon dark-mode saturation.
|
||||
private val GruvboxFadedRed = Color(0xFF9D0006)
|
||||
|
||||
// Derived roles, not upstream Gruvbox swatches: light bg0/faded green is 4.29:1;
|
||||
// white/green reaches 4.86:1. Hard dark bg0/faded yellow is below 4.5:1;
|
||||
// deeper neutral ink/yellow reaches 4.97:1.
|
||||
private val GruvboxOnFadedGreen = Color(0xFFFFFFFF)
|
||||
private val GruvboxOnFadedYellow = Color(0xFF121212)
|
||||
private val GruvboxFadedGreen = Color(0xFF79740E)
|
||||
private val GruvboxFadedYellow = Color(0xFFB57614)
|
||||
private val GruvboxFadedBlue = Color(0xFF076678)
|
||||
private val GruvboxFadedPurple = Color(0xFF8F3F71)
|
||||
private val GruvboxFadedOrange = Color(0xFFAF3A03)
|
||||
|
||||
/** Gruvbox theme — bright accents on dark bg0–bg4, faded accents on light bg0–bg4. */
|
||||
// Regression theme-contract: fixed roles reuse bright accents in both modes;
|
||||
// Fixed/Dim share a swatch. Containers stop at bg3 for >= 4.5:1 body text.
|
||||
// Upstream: https://github.com/morhetz/gruvbox/blob/master/colors/gruvbox.vim
|
||||
|
||||
/** Gruvbox theme — bright accents on dark bg0–bg3, faded accents on light bg0–bg3. */
|
||||
val GruvboxTheme =
|
||||
buildTheme(
|
||||
dark =
|
||||
PaletteColors(
|
||||
primary = GruvboxBrightOrange,
|
||||
onPrimary = GruvboxDarkBg0,
|
||||
primaryContainer = GruvboxDarkBg4,
|
||||
primaryContainer = GruvboxDarkBg2,
|
||||
onPrimaryContainer = GruvboxDarkFg1,
|
||||
secondary = GruvboxBrightBlue,
|
||||
onSecondary = GruvboxDarkBg0,
|
||||
secondaryContainer = GruvboxDarkBg3,
|
||||
secondaryContainer = GruvboxDarkBg2,
|
||||
onSecondaryContainer = GruvboxDarkFg2,
|
||||
tertiary = GruvboxBrightPurple,
|
||||
onTertiary = GruvboxDarkBg0,
|
||||
tertiaryContainer = GruvboxDarkBg4,
|
||||
tertiaryContainer = GruvboxDarkBg2,
|
||||
onTertiaryContainer = GruvboxDarkFg1,
|
||||
background = GruvboxDarkBg0,
|
||||
onBackground = GruvboxDarkFg1,
|
||||
@@ -72,15 +83,29 @@ val GruvboxTheme =
|
||||
onSurface = GruvboxDarkFg1,
|
||||
surfaceVariant = GruvboxDarkBg1,
|
||||
onSurfaceVariant = GruvboxDarkFg3,
|
||||
surfaceDim = GruvboxDarkBg0,
|
||||
surfaceBright = GruvboxDarkBg3,
|
||||
primaryFixed = GruvboxBrightOrange,
|
||||
primaryFixedDim = GruvboxBrightOrange,
|
||||
onPrimaryFixed = GruvboxDarkBg0,
|
||||
onPrimaryFixedVariant = GruvboxDarkBg0,
|
||||
secondaryFixed = GruvboxBrightBlue,
|
||||
secondaryFixedDim = GruvboxBrightBlue,
|
||||
onSecondaryFixed = GruvboxDarkBg0,
|
||||
onSecondaryFixedVariant = GruvboxDarkBg0,
|
||||
tertiaryFixed = GruvboxBrightPurple,
|
||||
tertiaryFixedDim = GruvboxBrightPurple,
|
||||
onTertiaryFixed = GruvboxDarkBg0,
|
||||
onTertiaryFixedVariant = GruvboxDarkBg0,
|
||||
surfaceContainerLowest = GruvboxDarkBg0,
|
||||
surfaceContainerLow = GruvboxDarkBg1,
|
||||
surfaceContainer = GruvboxDarkBg2,
|
||||
surfaceContainerHigh = GruvboxDarkBg3,
|
||||
surfaceContainerHighest = GruvboxDarkBg4,
|
||||
surfaceContainerHighest = GruvboxDarkBg3,
|
||||
inverseSurface = GruvboxDarkFg1,
|
||||
inverseOnSurface = GruvboxDarkBg0,
|
||||
inversePrimary = GruvboxFadedOrange,
|
||||
outline = GruvboxGray,
|
||||
outline = GruvboxDarkFg2,
|
||||
outlineVariant = GruvboxDarkBg3,
|
||||
scrim = GruvboxDarkBg0,
|
||||
status =
|
||||
@@ -91,9 +116,9 @@ val GruvboxTheme =
|
||||
warning = GruvboxBrightYellow,
|
||||
warningContainer = GruvboxDarkBg3,
|
||||
onWarning = GruvboxDarkBg0,
|
||||
error = GruvboxBrightRed,
|
||||
error = GruvboxErrorText,
|
||||
errorContainer = GruvboxDarkBg3,
|
||||
onError = GruvboxDarkBg0,
|
||||
onError = GruvboxDarkInk,
|
||||
onErrorContainer = GruvboxDarkFg1,
|
||||
info = GruvboxBrightBlue,
|
||||
infoContainer = GruvboxDarkBg3,
|
||||
@@ -104,15 +129,15 @@ val GruvboxTheme =
|
||||
PaletteColors(
|
||||
primary = GruvboxFadedOrange,
|
||||
onPrimary = GruvboxLightBg0,
|
||||
primaryContainer = GruvboxLightBg4,
|
||||
primaryContainer = GruvboxLightBg2,
|
||||
onPrimaryContainer = GruvboxLightFg1,
|
||||
secondary = GruvboxFadedBlue,
|
||||
onSecondary = GruvboxLightBg0,
|
||||
secondaryContainer = GruvboxLightBg3,
|
||||
secondaryContainer = GruvboxLightBg2,
|
||||
onSecondaryContainer = GruvboxLightFg2,
|
||||
tertiary = GruvboxFadedPurple,
|
||||
onTertiary = GruvboxLightBg0,
|
||||
tertiaryContainer = GruvboxLightBg4,
|
||||
tertiaryContainer = GruvboxLightBg2,
|
||||
onTertiaryContainer = GruvboxLightFg1,
|
||||
background = GruvboxLightBg0,
|
||||
onBackground = GruvboxLightFg1,
|
||||
@@ -120,29 +145,43 @@ val GruvboxTheme =
|
||||
onSurface = GruvboxLightFg1,
|
||||
surfaceVariant = GruvboxLightBg1,
|
||||
onSurfaceVariant = GruvboxLightFg3,
|
||||
surfaceDim = GruvboxLightBg3,
|
||||
surfaceBright = GruvboxLightBg0,
|
||||
primaryFixed = GruvboxBrightOrange,
|
||||
primaryFixedDim = GruvboxBrightOrange,
|
||||
onPrimaryFixed = GruvboxDarkBg0,
|
||||
onPrimaryFixedVariant = GruvboxDarkBg0,
|
||||
secondaryFixed = GruvboxBrightBlue,
|
||||
secondaryFixedDim = GruvboxBrightBlue,
|
||||
onSecondaryFixed = GruvboxDarkBg0,
|
||||
onSecondaryFixedVariant = GruvboxDarkBg0,
|
||||
tertiaryFixed = GruvboxBrightPurple,
|
||||
tertiaryFixedDim = GruvboxBrightPurple,
|
||||
onTertiaryFixed = GruvboxDarkBg0,
|
||||
onTertiaryFixedVariant = GruvboxDarkBg0,
|
||||
surfaceContainerLowest = GruvboxLightBg0,
|
||||
surfaceContainerLow = GruvboxLightBg1,
|
||||
surfaceContainer = GruvboxLightBg2,
|
||||
surfaceContainerHigh = GruvboxLightBg3,
|
||||
surfaceContainerHighest = GruvboxLightBg4,
|
||||
surfaceContainerHighest = GruvboxLightBg3,
|
||||
inverseSurface = GruvboxLightFg1,
|
||||
inverseOnSurface = GruvboxLightBg0,
|
||||
inversePrimary = GruvboxBrightOrange,
|
||||
outline = GruvboxGray,
|
||||
outline = GruvboxLightFg2,
|
||||
outlineVariant = GruvboxLightBg3,
|
||||
scrim = GruvboxLightBg0,
|
||||
status =
|
||||
HermesStatusColors(
|
||||
success = GruvboxFadedGreen,
|
||||
successContainer = GruvboxLightBg3,
|
||||
onSuccess = GruvboxLightBg0,
|
||||
onSuccess = GruvboxOnFadedGreen,
|
||||
warning = GruvboxFadedYellow,
|
||||
warningContainer = GruvboxLightBg3,
|
||||
onWarning = GruvboxLightBg0,
|
||||
onWarning = GruvboxOnFadedYellow,
|
||||
error = GruvboxFadedRed,
|
||||
errorContainer = GruvboxLightBg3,
|
||||
onError = GruvboxLightBg0,
|
||||
onErrorContainer = GruvboxFadedRed,
|
||||
onErrorContainer = GruvboxLightFg1,
|
||||
info = GruvboxFadedBlue,
|
||||
infoContainer = GruvboxLightBg3,
|
||||
onInfo = GruvboxLightBg0,
|
||||
|
||||
@@ -32,7 +32,6 @@ private val MonoLightBg = Color(0xFFFAFAFA) // background, surface, containerLow
|
||||
private val MonoLightContainer = Color(0xFFF0F0F0) // surfaceContainer
|
||||
private val MonoLightContainerHigh = Color(0xFFE0E0E0) // surfaceContainerHigh, surfaceVariant, status containers
|
||||
private val MonoLightContainerHighest = Color(0xFFD1D1D1) // surfaceContainerHighest, primaryContainer
|
||||
private val MonoLightOutline = Color(0xFF808080)
|
||||
private val MonoLightOutlineVariant = Color(0xFFCCCCCC)
|
||||
private val MonoLightTextDim = Color(0xFF4D4D4D) // onSurfaceVariant
|
||||
private val MonoLightTextBright = Color(0xFF121212) // onBackground/onSurface/inverseSurface/primary/error
|
||||
@@ -71,6 +70,20 @@ val MonochromeTheme =
|
||||
onSurface = MonoDarkTextBright,
|
||||
surfaceVariant = MonoDarkContainerHigh,
|
||||
onSurfaceVariant = MonoDarkTextDim,
|
||||
surfaceDim = MonoDarkBg,
|
||||
surfaceBright = MonoDarkContainerHighest,
|
||||
primaryFixed = MonoDarkTextBright,
|
||||
primaryFixedDim = MonoDarkTextBright,
|
||||
onPrimaryFixed = MonoDarkBg,
|
||||
onPrimaryFixedVariant = MonoDarkBg,
|
||||
secondaryFixed = MonoDarkSecondary,
|
||||
secondaryFixedDim = MonoDarkSecondary,
|
||||
onSecondaryFixed = MonoDarkBg,
|
||||
onSecondaryFixedVariant = MonoDarkBg,
|
||||
tertiaryFixed = MonoDarkTertiary,
|
||||
tertiaryFixedDim = MonoDarkTertiary,
|
||||
onTertiaryFixed = MonoDarkBg,
|
||||
onTertiaryFixedVariant = MonoDarkBg,
|
||||
surfaceContainerLowest = MonoDarkFloor,
|
||||
surfaceContainerLow = MonoDarkBg,
|
||||
surfaceContainer = MonoDarkContainer,
|
||||
@@ -112,13 +125,27 @@ val MonochromeTheme =
|
||||
tertiary = MonoLightTertiary,
|
||||
onTertiary = MonoLightBg,
|
||||
tertiaryContainer = MonoLightContainerHigh,
|
||||
onTertiaryContainer = MonoLightTertiary,
|
||||
onTertiaryContainer = MonoLightSecondary,
|
||||
background = MonoLightBg,
|
||||
onBackground = MonoLightTextBright,
|
||||
surface = MonoLightBg,
|
||||
onSurface = MonoLightTextBright,
|
||||
surfaceVariant = MonoLightContainerHigh,
|
||||
onSurfaceVariant = MonoLightTextDim,
|
||||
surfaceDim = MonoLightContainerHighest,
|
||||
surfaceBright = MonoLightBg,
|
||||
primaryFixed = MonoDarkTextBright,
|
||||
primaryFixedDim = MonoDarkTextBright,
|
||||
onPrimaryFixed = MonoDarkBg,
|
||||
onPrimaryFixedVariant = MonoDarkBg,
|
||||
secondaryFixed = MonoDarkSecondary,
|
||||
secondaryFixedDim = MonoDarkSecondary,
|
||||
onSecondaryFixed = MonoDarkBg,
|
||||
onSecondaryFixedVariant = MonoDarkBg,
|
||||
tertiaryFixed = MonoDarkTertiary,
|
||||
tertiaryFixedDim = MonoDarkTertiary,
|
||||
onTertiaryFixed = MonoDarkBg,
|
||||
onTertiaryFixedVariant = MonoDarkBg,
|
||||
surfaceContainerLowest = MonoLightCeiling,
|
||||
surfaceContainerLow = MonoLightBg,
|
||||
surfaceContainer = MonoLightContainer,
|
||||
@@ -127,7 +154,7 @@ val MonochromeTheme =
|
||||
inverseSurface = MonoLightTextBright,
|
||||
inverseOnSurface = MonoLightBg,
|
||||
inversePrimary = MonoDarkTextBright,
|
||||
outline = MonoLightOutline,
|
||||
outline = MonoLightTertiary,
|
||||
outlineVariant = MonoLightOutlineVariant,
|
||||
scrim = Color.Black,
|
||||
status =
|
||||
@@ -144,7 +171,7 @@ val MonochromeTheme =
|
||||
onErrorContainer = MonoLightTextBright,
|
||||
info = MonoLightInfo,
|
||||
infoContainer = MonoLightContainerHigh,
|
||||
onInfo = MonoLightBg,
|
||||
onInfo = MonoLightTextBright,
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -11,9 +11,12 @@ import com.m57.hermescontrol.theme.buildTheme
|
||||
// supply the surfaces; Frost (7–10) and Aurora (11–15) supply the
|
||||
// accents. Unlike Gruvbox, Nord defines one accent set — not a
|
||||
// separate bright/faded pair per mode — so the same accent swatches
|
||||
// are reused in both palettes below.
|
||||
// are reused for fixed roles; primary/error need documented accessible tones.
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
// Derived role, not an upstream Nord swatch: nord0/red is 3.05:1 and
|
||||
// nord0/purple is 4.41:1. Darker Polar Night ink meets 4.5:1 on both.
|
||||
private val NordPolarInk = Color(0xFF0F1218)
|
||||
private val Nord0 = Color(0xFF2E3440) // Polar Night — darkest
|
||||
private val Nord1 = Color(0xFF3B4252)
|
||||
private val Nord2 = Color(0xFF434C5E)
|
||||
@@ -23,11 +26,23 @@ private val Nord5 = Color(0xFFE5E9F0)
|
||||
private val Nord6 = Color(0xFFECEFF4) // Snow Storm — lightest
|
||||
private val Nord8 = Color(0xFF88C0D0) // Frost — primary accent
|
||||
private val Nord9 = Color(0xFF81A1C1) // Frost — secondary / info
|
||||
private val Nord11 = Color(0xFFBF616A) // Aurora red — error
|
||||
|
||||
// Derived roles, not upstream Nord swatches. Nord10/Snow Storm is only 3.50:1;
|
||||
// deeper Frost ink reaches 4.92:1 on nord6 for primary text/focus labels.
|
||||
private val NordFrostInk = Color(0xFF486991)
|
||||
|
||||
// Official Aurora red/nord0 is 3.05:1; a brighter red reaches 4.67:1.
|
||||
private val NordErrorBright = Color(0xFFDE858F)
|
||||
|
||||
// Official Aurora red/nord6 is 3.55:1; deeper red reaches 5.69:1.
|
||||
private val NordErrorInk = Color(0xFFA53547)
|
||||
private val Nord13 = Color(0xFFEBCB8B) // Aurora yellow — warning
|
||||
private val Nord14 = Color(0xFFA3BE8C) // Aurora green — success
|
||||
private val Nord15 = Color(0xFFB48EAD) // Aurora purple — tertiary
|
||||
|
||||
// Fixed/Dim reuse each official accent in both modes (no invented tonal ladder).
|
||||
// Upstream: https://www.nordtheme.com/docs/colors-and-palettes/
|
||||
|
||||
/**
|
||||
* Nord theme.
|
||||
*
|
||||
@@ -36,12 +51,11 @@ private val Nord15 = Color(0xFFB48EAD) // Aurora purple — tertiary
|
||||
* tiers on each side share a value (surfaceContainerHigh ==
|
||||
* surfaceContainerHighest) rather than inventing an off-palette hex.
|
||||
*
|
||||
* Frost/Aurora accents are pastel by design, so onPrimary/onSecondary/
|
||||
* onTertiary/status "on" colors use Polar Night (dark text) in *both*
|
||||
* modes — reusing Snow Storm for light-mode "on" text (the convention in
|
||||
* Pastel Frost/Aurora fills use dark ink in both modes — reusing
|
||||
* Snow Storm for light-mode "on" text (the convention in
|
||||
* the Catppuccin/Gruvbox presets) would fail contrast against colors like
|
||||
* Aurora yellow, which stays light even without a separate light-mode
|
||||
* variant.
|
||||
* variant. Light primary/error use Snow Storm ink on their darker fills.
|
||||
*/
|
||||
val NordTheme =
|
||||
buildTheme(
|
||||
@@ -56,7 +70,7 @@ val NordTheme =
|
||||
secondaryContainer = Nord2,
|
||||
onSecondaryContainer = Nord4,
|
||||
tertiary = Nord15,
|
||||
onTertiary = Nord0,
|
||||
onTertiary = NordPolarInk,
|
||||
tertiaryContainer = Nord3,
|
||||
onTertiaryContainer = Nord6,
|
||||
background = Nord0,
|
||||
@@ -65,6 +79,20 @@ val NordTheme =
|
||||
onSurface = Nord6,
|
||||
surfaceVariant = Nord1,
|
||||
onSurfaceVariant = Nord4,
|
||||
surfaceDim = Nord0,
|
||||
surfaceBright = Nord3,
|
||||
primaryFixed = Nord8,
|
||||
primaryFixedDim = Nord8,
|
||||
onPrimaryFixed = Nord0,
|
||||
onPrimaryFixedVariant = Nord0,
|
||||
secondaryFixed = Nord9,
|
||||
secondaryFixedDim = Nord9,
|
||||
onSecondaryFixed = Nord0,
|
||||
onSecondaryFixedVariant = Nord0,
|
||||
tertiaryFixed = Nord15,
|
||||
tertiaryFixedDim = Nord15,
|
||||
onTertiaryFixed = NordPolarInk,
|
||||
onTertiaryFixedVariant = NordPolarInk,
|
||||
surfaceContainerLowest = Nord0,
|
||||
surfaceContainerLow = Nord1,
|
||||
surfaceContainer = Nord2,
|
||||
@@ -72,8 +100,8 @@ val NordTheme =
|
||||
surfaceContainerHighest = Nord3,
|
||||
inverseSurface = Nord6,
|
||||
inverseOnSurface = Nord0,
|
||||
inversePrimary = Nord8,
|
||||
outline = Nord3,
|
||||
inversePrimary = NordFrostInk,
|
||||
outline = Nord4,
|
||||
outlineVariant = Nord2,
|
||||
scrim = Nord0,
|
||||
status =
|
||||
@@ -84,9 +112,9 @@ val NordTheme =
|
||||
warning = Nord13,
|
||||
warningContainer = Nord2,
|
||||
onWarning = Nord0,
|
||||
error = Nord11,
|
||||
error = NordErrorBright,
|
||||
errorContainer = Nord2,
|
||||
onError = Nord0,
|
||||
onError = NordPolarInk,
|
||||
onErrorContainer = Nord6,
|
||||
info = Nord9,
|
||||
infoContainer = Nord2,
|
||||
@@ -95,8 +123,8 @@ val NordTheme =
|
||||
),
|
||||
light =
|
||||
PaletteColors(
|
||||
primary = Nord8,
|
||||
onPrimary = Nord0,
|
||||
primary = NordFrostInk,
|
||||
onPrimary = Nord6,
|
||||
primaryContainer = Nord4,
|
||||
onPrimaryContainer = Nord0,
|
||||
secondary = Nord9,
|
||||
@@ -104,7 +132,7 @@ val NordTheme =
|
||||
secondaryContainer = Nord5,
|
||||
onSecondaryContainer = Nord1,
|
||||
tertiary = Nord15,
|
||||
onTertiary = Nord0,
|
||||
onTertiary = NordPolarInk,
|
||||
tertiaryContainer = Nord4,
|
||||
onTertiaryContainer = Nord0,
|
||||
background = Nord6,
|
||||
@@ -113,6 +141,20 @@ val NordTheme =
|
||||
onSurface = Nord0,
|
||||
surfaceVariant = Nord5,
|
||||
onSurfaceVariant = Nord2,
|
||||
surfaceDim = Nord4,
|
||||
surfaceBright = Nord6,
|
||||
primaryFixed = Nord8,
|
||||
primaryFixedDim = Nord8,
|
||||
onPrimaryFixed = Nord0,
|
||||
onPrimaryFixedVariant = Nord0,
|
||||
secondaryFixed = Nord9,
|
||||
secondaryFixedDim = Nord9,
|
||||
onSecondaryFixed = Nord0,
|
||||
onSecondaryFixedVariant = Nord0,
|
||||
tertiaryFixed = Nord15,
|
||||
tertiaryFixedDim = Nord15,
|
||||
onTertiaryFixed = NordPolarInk,
|
||||
onTertiaryFixedVariant = NordPolarInk,
|
||||
surfaceContainerLowest = Nord6,
|
||||
surfaceContainerLow = Nord5,
|
||||
surfaceContainer = Nord5,
|
||||
@@ -132,10 +174,10 @@ val NordTheme =
|
||||
warning = Nord13,
|
||||
warningContainer = Nord5,
|
||||
onWarning = Nord0,
|
||||
error = Nord11,
|
||||
error = NordErrorInk,
|
||||
errorContainer = Nord5,
|
||||
onError = Nord0,
|
||||
onErrorContainer = Nord11,
|
||||
onError = Nord6,
|
||||
onErrorContainer = Nord0,
|
||||
info = Nord9,
|
||||
infoContainer = Nord5,
|
||||
onInfo = Nord0,
|
||||
|
||||
@@ -40,6 +40,7 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.m57.hermescontrol.R
|
||||
import com.m57.hermescontrol.theme.HermesStatusColors
|
||||
import com.m57.hermescontrol.theme.LocalHermesStatusColors
|
||||
import com.m57.hermescontrol.theme.LocalSpacing
|
||||
|
||||
@@ -81,28 +82,8 @@ fun StatusBadge(
|
||||
val spacing = LocalSpacing.current
|
||||
val statusColors = LocalHermesStatusColors.current
|
||||
val (bgColor, fgColor) =
|
||||
when (status) {
|
||||
StatusBadgeType.SUCCESS -> {
|
||||
statusColors.successContainer to statusColors.success
|
||||
}
|
||||
|
||||
StatusBadgeType.WARNING -> {
|
||||
statusColors.warningContainer to statusColors.warning
|
||||
}
|
||||
|
||||
StatusBadgeType.ERROR -> {
|
||||
statusColors.errorContainer to statusColors.error
|
||||
}
|
||||
|
||||
StatusBadgeType.INFO -> {
|
||||
statusColors.infoContainer to statusColors.info
|
||||
}
|
||||
|
||||
StatusBadgeType.NEUTRAL -> {
|
||||
MaterialTheme.colorScheme.surfaceContainerHigh to
|
||||
MaterialTheme.colorScheme.onSurfaceVariant
|
||||
}
|
||||
}
|
||||
statusBadgeColors(status, statusColors)
|
||||
?: (MaterialTheme.colorScheme.surfaceContainerHigh to MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
Surface(
|
||||
modifier = modifier,
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
@@ -135,6 +116,20 @@ fun StatusBadge(
|
||||
|
||||
enum class StatusBadgeType { SUCCESS, WARNING, ERROR, INFO, NEUTRAL }
|
||||
|
||||
/** Exact fill/text pair shared by status badges and live-session indicators. */
|
||||
internal fun statusBadgeColors(
|
||||
status: StatusBadgeType,
|
||||
colors: HermesStatusColors,
|
||||
): Pair<Color, Color>? =
|
||||
// PR #1417: on-status tokens belong on the status fill, not its container.
|
||||
when (status) {
|
||||
StatusBadgeType.SUCCESS -> colors.success to colors.onSuccess
|
||||
StatusBadgeType.WARNING -> colors.warning to colors.onWarning
|
||||
StatusBadgeType.ERROR -> colors.error to colors.onError
|
||||
StatusBadgeType.INFO -> colors.info to colors.onInfo
|
||||
StatusBadgeType.NEUTRAL -> null // Neutral badges use the active Material scheme.
|
||||
}
|
||||
|
||||
// ── ToggleRow — switch row with label + description ────────────────────
|
||||
|
||||
@Composable
|
||||
|
||||
+8
-14
@@ -25,6 +25,8 @@ import com.m57.hermescontrol.R
|
||||
import com.m57.hermescontrol.data.model.SessionLiveStatus
|
||||
import com.m57.hermescontrol.theme.LocalHermesStatusColors
|
||||
import com.m57.hermescontrol.theme.LocalSpacing
|
||||
import com.m57.hermescontrol.ui.common.StatusBadgeType
|
||||
import com.m57.hermescontrol.ui.common.statusBadgeColors
|
||||
|
||||
@Composable
|
||||
fun SessionLiveStatusIndicator(
|
||||
@@ -35,27 +37,26 @@ fun SessionLiveStatusIndicator(
|
||||
val spacing = LocalSpacing.current
|
||||
val statusColors = LocalHermesStatusColors.current
|
||||
|
||||
val (bgColor, fgColor, textRes, descRes) =
|
||||
val (badgeType, textRes, descRes) =
|
||||
when (liveStatus) {
|
||||
SessionLiveStatus.WORKING -> {
|
||||
Tuple4(
|
||||
statusColors.successContainer,
|
||||
statusColors.success,
|
||||
Triple(
|
||||
StatusBadgeType.SUCCESS,
|
||||
R.string.sessions_live_status_running,
|
||||
R.string.sessions_live_status_running_desc,
|
||||
)
|
||||
}
|
||||
|
||||
SessionLiveStatus.WAITING -> {
|
||||
Tuple4(
|
||||
statusColors.warningContainer,
|
||||
statusColors.warning,
|
||||
Triple(
|
||||
StatusBadgeType.WARNING,
|
||||
R.string.sessions_live_status_waiting,
|
||||
R.string.sessions_live_status_waiting_desc,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val (bgColor, fgColor) = requireNotNull(statusBadgeColors(badgeType, statusColors))
|
||||
val text = stringResource(textRes)
|
||||
val description = stringResource(descRes)
|
||||
|
||||
@@ -103,10 +104,3 @@ fun SessionLiveStatusIndicator(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private data class Tuple4<A, B, C, D>(
|
||||
val first: A,
|
||||
val second: B,
|
||||
val third: C,
|
||||
val fourth: D,
|
||||
)
|
||||
@@ -14,7 +14,6 @@ import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.SwitchDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -270,11 +269,6 @@ private fun ReleaseCandidateRow(
|
||||
checked = checked,
|
||||
onCheckedChange = onCheckedChange,
|
||||
enabled = enabled,
|
||||
colors =
|
||||
SwitchDefaults.colors(
|
||||
checkedThumbColor = MaterialTheme.colorScheme.primary,
|
||||
checkedTrackColor = MaterialTheme.colorScheme.primaryContainer,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
+27
-69
@@ -1,5 +1,6 @@
|
||||
package com.m57.hermescontrol.ui.settings.components
|
||||
|
||||
import android.os.Build
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -21,7 +22,6 @@ import androidx.compose.material3.SingleChoiceSegmentedButtonRow
|
||||
import androidx.compose.material3.Slider
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.SwitchDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
@@ -41,6 +41,7 @@ import com.m57.hermescontrol.R
|
||||
import com.m57.hermescontrol.data.model.BusySendMode
|
||||
import com.m57.hermescontrol.theme.ThemePreference
|
||||
import com.m57.hermescontrol.theme.ThemePreset
|
||||
import com.m57.hermescontrol.theme.ThemeRegistry
|
||||
import com.m57.hermescontrol.ui.chat.label
|
||||
import com.m57.hermescontrol.ui.settings.SectionCard
|
||||
import kotlin.math.abs
|
||||
@@ -55,6 +56,7 @@ internal fun AppearanceSection(
|
||||
themePreset: ThemePreset,
|
||||
onThemePresetChange: (ThemePreset) -> Unit,
|
||||
) {
|
||||
val dynamicAvailable = useDynamicColors && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
||||
SectionCard {
|
||||
Text(
|
||||
text = stringResource(R.string.settings_item_theme),
|
||||
@@ -106,11 +108,6 @@ internal fun AppearanceSection(
|
||||
Switch(
|
||||
checked = useDynamicColors,
|
||||
onCheckedChange = onUseDynamicColorsChange,
|
||||
colors =
|
||||
SwitchDefaults.colors(
|
||||
checkedThumbColor = MaterialTheme.colorScheme.primary,
|
||||
checkedTrackColor = MaterialTheme.colorScheme.primaryContainer,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -122,72 +119,30 @@ internal fun AppearanceSection(
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
if (dynamicAvailable) {
|
||||
Text(
|
||||
text = stringResource(R.string.settings_desc_theme_preset_dynamic),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
|
||||
var presetsExpanded by remember { mutableStateOf(false) }
|
||||
Box(modifier = Modifier.fillMaxWidth()) {
|
||||
OutlinedButton(
|
||||
onClick = { presetsExpanded = true },
|
||||
enabled = !useDynamicColors,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text(
|
||||
when (themePreset) {
|
||||
ThemePreset.DEFAULT -> stringResource(R.string.theme_preset_default)
|
||||
ThemePreset.MONOCHROME -> stringResource(R.string.theme_preset_monochrome)
|
||||
ThemePreset.GRUVBOX -> stringResource(R.string.theme_preset_gruvbox)
|
||||
ThemePreset.CATPPUCCIN -> stringResource(R.string.theme_preset_catppuccin)
|
||||
ThemePreset.AMOLED -> stringResource(R.string.theme_preset_amoled)
|
||||
ThemePreset.NORD -> stringResource(R.string.theme_preset_nord)
|
||||
},
|
||||
)
|
||||
Text(themePreset.label())
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = presetsExpanded,
|
||||
onDismissRequest = { presetsExpanded = false },
|
||||
modifier = Modifier.fillMaxWidth(0.85f),
|
||||
) {
|
||||
ThemePreset.entries.forEach { preset ->
|
||||
ThemeRegistry.forEach { (preset, _) ->
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(
|
||||
when (preset) {
|
||||
ThemePreset.DEFAULT -> {
|
||||
stringResource(
|
||||
R.string.theme_preset_default,
|
||||
)
|
||||
}
|
||||
|
||||
ThemePreset.MONOCHROME -> {
|
||||
stringResource(
|
||||
R.string.theme_preset_monochrome,
|
||||
)
|
||||
}
|
||||
|
||||
ThemePreset.GRUVBOX -> {
|
||||
stringResource(
|
||||
R.string.theme_preset_gruvbox,
|
||||
)
|
||||
}
|
||||
|
||||
ThemePreset.CATPPUCCIN -> {
|
||||
stringResource(
|
||||
R.string.theme_preset_catppuccin,
|
||||
)
|
||||
}
|
||||
|
||||
ThemePreset.AMOLED -> {
|
||||
stringResource(
|
||||
R.string.theme_preset_amoled,
|
||||
)
|
||||
}
|
||||
|
||||
ThemePreset.NORD -> {
|
||||
stringResource(
|
||||
R.string.theme_preset_nord,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
text = { Text(preset.label()) },
|
||||
onClick = {
|
||||
onThemePresetChange(preset)
|
||||
presetsExpanded = false
|
||||
@@ -199,6 +154,19 @@ internal fun AppearanceSection(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ThemePreset.label(): String =
|
||||
stringResource(
|
||||
when (this) {
|
||||
ThemePreset.DEFAULT -> R.string.theme_preset_default
|
||||
ThemePreset.MONOCHROME -> R.string.theme_preset_monochrome
|
||||
ThemePreset.GRUVBOX -> R.string.theme_preset_gruvbox
|
||||
ThemePreset.CATPPUCCIN -> R.string.theme_preset_catppuccin
|
||||
ThemePreset.AMOLED -> R.string.theme_preset_amoled
|
||||
ThemePreset.NORD -> R.string.theme_preset_nord
|
||||
},
|
||||
)
|
||||
|
||||
@Composable
|
||||
internal fun ChatSection(
|
||||
typingEffectEnabled: Boolean,
|
||||
@@ -485,11 +453,6 @@ internal fun ChatSection(
|
||||
checked = showModelProvider,
|
||||
onCheckedChange = onShowModelProviderChange,
|
||||
modifier = Modifier.testTag("settings_show_model_provider"),
|
||||
colors =
|
||||
SwitchDefaults.colors(
|
||||
checkedThumbColor = MaterialTheme.colorScheme.primary,
|
||||
checkedTrackColor = MaterialTheme.colorScheme.primaryContainer,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -516,11 +479,6 @@ internal fun ChatSection(
|
||||
Switch(
|
||||
checked = typingEffectEnabled,
|
||||
onCheckedChange = onTypingEffectEnabledChange,
|
||||
colors =
|
||||
SwitchDefaults.colors(
|
||||
checkedThumbColor = MaterialTheme.colorScheme.primary,
|
||||
checkedTrackColor = MaterialTheme.colorScheme.primaryContainer,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.SwitchDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -50,11 +49,6 @@ internal fun BehaviorSection(
|
||||
Switch(
|
||||
checked = autoReconnect,
|
||||
onCheckedChange = onAutoReconnectChange,
|
||||
colors =
|
||||
SwitchDefaults.colors(
|
||||
checkedThumbColor = MaterialTheme.colorScheme.primary,
|
||||
checkedTrackColor = MaterialTheme.colorScheme.primaryContainer,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -83,11 +77,6 @@ internal fun BehaviorSection(
|
||||
Switch(
|
||||
checked = keepConnectedInBackground,
|
||||
onCheckedChange = onKeepConnectedInBackgroundChange,
|
||||
colors =
|
||||
SwitchDefaults.colors(
|
||||
checkedThumbColor = MaterialTheme.colorScheme.primary,
|
||||
checkedTrackColor = MaterialTheme.colorScheme.primaryContainer,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -116,11 +105,6 @@ internal fun BehaviorSection(
|
||||
Switch(
|
||||
checked = restoreLastSession,
|
||||
onCheckedChange = onRestoreLastSessionChange,
|
||||
colors =
|
||||
SwitchDefaults.colors(
|
||||
checkedThumbColor = MaterialTheme.colorScheme.primary,
|
||||
checkedTrackColor = MaterialTheme.colorScheme.primaryContainer,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user