theme(849): unify presets behind one palette template + bespoke AMOLED status colors (#852)

* refactor(theme): unify all presets behind one palette template

* docs(theme): update THEMES.md and AGENTS.md for the palette template

* fix(theme): restore per-preset error slot contrast, merge StatusColors into HermesStatusColors

- onErrorContainer becomes an explicit per-preset field (was derived from
  onError — same-color pairs in Gruvbox/Catppuccin light made error text
  invisible; 6 screens render this slot)
- Default dark + AMOLED onError -> White (were <3:1 on their error color)
- drop StatusColors + statusColorsOf mapper; PaletteColors.status is typed
  HermesStatusColors directly (identical field lists, pure copy ceremony)
- ThemePalette getters now by lazy; schemeFor/statusFor accessors;
  requireNotNull on the default-theme fallback (DefaultTheme is FULL)
- resolvers internal for testability

* test(theme): contrast, mode-validation, and fallback contract tests

- every shipped mode's onError/error + onErrorContainer/errorContainer
  pair meets >= 3:1 WCAG contrast (kills the error-slot regression class)
- ThemeMode init validation rejects illegal palette combinations
- AMOLED darkOnly contract + light-mode fallback to Default light

* refactor(theme): Catppuccin — named swatches from the official palette spec

Hex values verified identical (zero color drift): repeated colors (Crust,
Surface0/1, Base, Text...) now live once as named swatches matching
catppuccin.com; bespoke light primary container tints stay local.
Mocha/Latte pair unchanged.

* refactor(theme): Gruvbox — authentic bright/faded accent scheme with named swatches

Redesign per gruvbox spec: bright accents on dark bg0-bg4, faded accents
on light bg0-bg4 (correct contrast play for light mode). Named swatches,
single Gray shared across modes. Dark error pair fixed for >= 3:1
(onErrorContainer -> DarkFg1 cream, was bright red on Bg3 = 1.87:1).

* feat(theme): replace default preset with Slate — modern slate blues + teal accents

The brand default (voltage purple/amber) becomes Slate: bright teal/blue
accents on dark bg0-bg4, faded variants on light bg0-bg4. Named swatches,
single neutral gray shared across modes. Slate is also the fallback for
darkOnly/lightOnly presets (AMOLED light mode).

* feat(theme): add Nord preset — Polar Night/Snow Storm surfaces, Frost/Aurora accents

Nord's single accent set is reused across both modes (pastel accents stay
light, so on* colors use Polar Night dark text in both modes — documented
in-file). Surface ladder shares outer tiers (Nord ships 4 dark + 3 light
tones for 5 tiers). Dark error pair fixed for >= 3:1 (onErrorContainer ->
Nord6, was Nord11 on Nord2 = 2.0:1). Wired into ThemePreset, the settings
picker, and docs; default label now reads Default (Slate).

* refactor(theme): Monochrome + AMOLED — pure grayscale ramp, true-black AMOLED variant

Monochrome: every swatch R=G=B, roles by lightness only (status colors
included — documented trade-off: pair with icons/labels). AMOLED: dark-only
true-black variant (pure #000000, OLED pixels off; #121212 baseline of
Monochrome doesn't do that), containers collapse where nothing darker
exists. Both ship maximum-contrast grayscale pairs (12.8:1+ on error
slots).

* chore(theme): drop Neon Noir, finalize docs + picker label

- remove NeonNoirScheme.kt + ThemePreset.NEON_NOIR everywhere (dispatcher,
  picker, strings EN/KO, tests, AGENTS.md, THEMES.md) — 6 presets remain:
  Default, Monochrome, Gruvbox, Catppuccin, AMOLED, Nord
- picker label for the default preset is now plain 'Default'
- THEMES.md: preset conventions section (named swatches, bright/faded
  split, grayscale-status caveat, contrast test gate) + verify step runs
  testDebugUnitTest

* fix(theme): make drawer icon colors visibly follow the active preset

Selected icon now tints with the theme's primary accent (teal for Slate,
orange for Gruvbox, mauve for Catppuccin, frost-blue for Nord) instead of
onPrimaryContainer — the new presets' containers are all dark gray with
near-white text, so the selected row looked identical across themes.
Unselected icon/text colors pinned explicitly to onSurfaceVariant.

* chore(ui): remove lightning emoji from the app

- drawer title + About name: '⚡ Hermes' -> 'Hermes' (EN + KO)
- skill markers (chat self-improvement badge, memory graph): ⚡ -> 🛠
- in-progress indicators (subagent inspection): ⚡ -> 🔄

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
This commit is contained in:
M57andM57 authored and GitHub committed 2026-08-08 15:58:25 +03:00
1 parent 52e33a96b7
commit ab42e19b7f
21 files changed
+1312 -862

No files matched your search

@@ -260,7 +260,9 @@ fun MainNavigation(sessionId: String? = null) {
NavigationDrawerItemDefaults.colors(
selectedContainerColor = MaterialTheme.colorScheme.primaryContainer,
selectedTextColor = MaterialTheme.colorScheme.onPrimaryContainer,
selectedIconColor = MaterialTheme.colorScheme.onPrimaryContainer,
selectedIconColor = MaterialTheme.colorScheme.primary,
unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant,
unselectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant,
),
)
}
@@ -2,67 +2,11 @@ package com.m57.hermescontrol.theme
import androidx.compose.ui.graphics.Color
// Hermes Mobile brand palette — v3 (Material You rewrite).
//
// On Android 12+ (API 31+) the app uses dynamic colour derived from the user's
// wallpaper — this palette is NOT used for primary/surface roles on those
// devices (see HermesControlTheme). On API 26–30 it serves as the full colour
// scheme. Semantic status colours and chat-bubble colours are ALWAYS
// brand-defined. All pairs verified for WCAG AA contrast (4.5:1 text, 3:1
// large text / icons).
// ── Brand: Voltage Purple (primary) ──────────────────────────────────────
val HermesPurple = Color(0xFF7C5CFF)
val HermesPurpleLight = Color(0xFFAC93FF)
val HermesPurpleDark = Color(0xFF5A3FE0)
val HermesPurpleContainer = Color(0xFF2B2159)
val HermesPurpleOnContainer = Color(0xFFD9CCFF)
// ── Brand: Plasma Amber (secondary) ─────────────────────────────────────
val HermesAmber = Color(0xFFFFB627)
val HermesAmberLight = Color(0xFFFFE082)
val HermesAmberDark = Color(0xFFC68400)
// ── Dark surfaces (5-step elevation ladder) ─────────────────────────────
val DarkBackground = Color(0xFF0B0B11)
val DarkSurface = Color(0xFF121218)
val DarkSurfaceVariant = Color(0xFF1C1C26)
val DarkSurfaceContainerLowest = Color(0xFF07070C)
val DarkSurfaceContainerLow = Color(0xFF0F0F14)
val DarkSurfaceContainer = Color(0xFF1C1C26)
val DarkSurfaceContainerHigh = Color(0xFF262633)
val DarkSurfaceContainerHighest = Color(0xFF30303D)
val DarkOnSurface = Color(0xFFE8E6EE)
val DarkOnSurfaceVariant = Color(0xFFB6B2C4)
val DarkOutline = Color(0xFF4A4A5A)
val DarkOutlineVariant = Color(0xFF2F2F3D)
val DarkInverseSurface = Color(0xFFE8E6EE)
val DarkInverseOnSurface = Color(0xFF1A1A24)
val DarkScrim = Color(0xFF000000)
// ── Light surfaces (5-step elevation ladder) ────────────────────────────
val LightBackground = Color(0xFFF7F6FB)
val LightSurface = Color(0xFFFFFFFF)
val LightSurfaceVariant = Color(0xFFEFEDF4)
val LightSurfaceContainerLowest = Color(0xFFFFFFFF)
val LightSurfaceContainerLow = Color(0xFFF3F1F8)
val LightSurfaceContainer = Color(0xFFEFEDF4)
val LightSurfaceContainerHigh = Color(0xFFE6E3EE)
val LightSurfaceContainerHighest = Color(0xFFDDD9EA)
val LightOnSurface = Color(0xFF1A1A24)
val LightOnSurfaceVariant = Color(0xFF56536A)
val LightOutline = Color(0xFF79748E)
val LightOutlineVariant = Color(0xFFC9C5D6)
val LightInverseSurface = Color(0xFF1A1A24)
val LightInverseOnSurface = Color(0xFFE8E6EE)
val LightScrim = Color(0xFF000000)
// ── Semantic status colours (always brand-defined) ──────────────────────
// Shared color tokens — only what code OUTSIDE the theme presets consumes.
// The preset palettes themselves are self-contained template fills in
// presets/ (see PaletteTemplate.kt); they don't import from here.
// Semantic status colors (always brand-defined).
val StatusGreen = Color(0xFF3DDC84)
val StatusGreenContainer = Color(0xFF143A23)
val StatusRed = Color(0xFFFF5C5C)
@@ -75,31 +19,25 @@ val StatusGrey = Color(0xFF9E9E9E)
val StatusGreyDark = Color(0xFF1A1A24)
val StatusGreyLight = Color(0xFFF5F5F5)
// ── Chat-specific colours (default theme fallback tokens) ─────────────────
val AssistantBubble = Color(0xFF1C1C26)
val AssistantBubbleLight = Color(0xFFEDEAF4)
val SystemMessageColor = Color(0xFF8B879A)
val ToolChipColor = Color(0xFF262633)
val ToolChipColorLight = Color(0xFFE6E3EE)
// ── Syntax highlighting tokens (code blocks) ─────────────────────────────
// VS Code Dark+ palette — good general readability across all presets.
// These are consumed exclusively by CodeBlockCard (MessageCards.kt).
// Chat-specific fallback tokens (default theme surface text colors).
val DarkOnSurface = Color(0xFFE8E6EE)
val LightOnSurface = Color(0xFF1A1A24)
// Syntax highlighting tokens (code blocks) — VS Code Dark+ palette — good
// general readability across all presets. Consumed by CodeBlockCard.
val CodeKeyword = Color(0xFF569CD6) // blue — for val, fun, class, etc.
val CodeString = Color(0xFFCE9178) // orange — for "string literals"
val CodeComment = Color(0xFF6A9955) // green — for // comments
val CodeNumber = Color(0xFFB5CEA8) // light green — for 42, 0xFF
val CodePunctuation = Color(0xFFD4D4D4) // gray — for {, (, ;, etc.
// ── Code block / terminal surface colours (issue #659) ───────────────
// Code block / terminal surface colors (issue #659).
val CodeTerminalBg = Color(0xFF1E1E1E)
val CodeTerminalBorder = Color(0xFF333333)
val CodeTerminalText = Color(0xFFD4D4D4)
val CodeTerminalMuted = Color(0xFF808080)
// ── Diff viewer tokens (issue #738) ──────────────────────────────────
// Diff viewer tokens (issue #738).
val CodeDiffAddBg = Color(0x263DDC84)
val CodeDiffAddText = Color(0xFF3DDC84)
val CodeDiffDeleteBg = Color(0x26FF5C5C)
@@ -107,9 +45,8 @@ val CodeDiffDeleteText = Color(0xFFFF5C5C)
val CodeDiffHunkBg = Color(0x264DA8FF)
val CodeDiffHunkText = Color(0xFF4DA8FF)
// ── Custom vector icons ─────────────────────────────────────────────
// Default fill for hand-declared ImageVector icons (e.g. NeurologyIcon).
// Mirrors Material's own icon set: vectors are drawn opaque black and
// Icon()'s tint (LocalContentColor) replaces the RGB at draw time — only
// the alpha channel survives, so this must stay opaque.
// Custom vector icons — default fill for hand-declared ImageVector icons
// (e.g. NeurologyIcon). Mirrors Material's own icon set: vectors are drawn
// opaque black and Icon()'s tint (LocalContentColor) replaces the RGB at
// draw time — only the alpha channel survives, so this must stay opaque.
val IconDefaultFill = Color.Black
@@ -24,6 +24,7 @@ data class HermesStatusColors(
val error: Color,
val errorContainer: Color,
val onError: Color,
val onErrorContainer: Color = StatusGreyLight,
val info: Color,
val infoContainer: Color,
val onInfo: Color,
@@ -0,0 +1,179 @@
package com.m57.hermescontrol.theme
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.ui.graphics.Color
/**
* Theme template — the ONE shape every preset follows.
*
* A theme preset is a single [ThemePalette] built from raw [PaletteColors]
* specs. Every preset file has the same skeleton: fill in the colors, done.
* The mapping from raw colors to the Material 3 [ColorScheme] slots and the
* [HermesStatusColors] model lives here — no per-preset behavior.
*
* A theme can declare which modes it ships via [ThemeMode]:
* - [ThemeMode.FULL] — both dark and light palettes.
* - [ThemeMode.DARK_ONLY] — dark palette only; the dispatcher falls back to
* the default light palette in light mode (e.g. AMOLED — a light AMOLED
* makes no sense).
* - [ThemeMode.LIGHT_ONLY] — light palette only; symmetric fallback.
*/
enum class ThemeMode { FULL, DARK_ONLY, LIGHT_ONLY }
/**
* Raw color spec for ONE mode (dark or light) of a theme.
*
* The Material error slots are derived from [status] — the theme author
* defines semantic colors once and the scheme inherits them.
*/
data class PaletteColors(
val primary: Color,
val onPrimary: Color,
val primaryContainer: Color,
val onPrimaryContainer: Color,
val secondary: Color,
val onSecondary: Color,
val secondaryContainer: Color,
val onSecondaryContainer: Color,
val tertiary: Color,
val onTertiary: Color,
val tertiaryContainer: Color,
val onTertiaryContainer: Color,
val background: Color,
val onBackground: Color,
val surface: Color,
val onSurface: Color,
val surfaceVariant: Color,
val onSurfaceVariant: Color,
val surfaceContainerLowest: Color,
val surfaceContainerLow: Color,
val surfaceContainer: Color,
val surfaceContainerHigh: Color,
val surfaceContainerHighest: Color,
val inverseSurface: Color,
val inverseOnSurface: Color,
val inversePrimary: Color,
val outline: Color,
val outlineVariant: Color,
val scrim: Color,
val status: HermesStatusColors,
)
/** A complete theme — one per preset file, same shape everywhere. */
data class ThemePalette(
val mode: ThemeMode,
val dark: PaletteColors?,
val light: PaletteColors?,
) {
init {
require(mode != ThemeMode.FULL || (dark != null && light != null)) {
"FULL theme must provide both dark and light palettes"
}
require(mode != ThemeMode.DARK_ONLY || (dark != null && light == null)) {
"DARK_ONLY theme must provide a dark palette and no light palette"
}
require(mode != ThemeMode.LIGHT_ONLY || (dark == null && light != null)) {
"LIGHT_ONLY theme must provide a light palette and no dark palette"
}
}
val darkScheme: ColorScheme? by lazy { dark?.let(::darkSchemeOf) }
val lightScheme: ColorScheme? by lazy { light?.let(::lightSchemeOf) }
val darkStatus: HermesStatusColors? by lazy { dark?.status }
val lightStatus: HermesStatusColors? by lazy { light?.status }
fun schemeFor(dark: Boolean): ColorScheme? = if (dark) darkScheme else lightScheme
fun statusFor(dark: Boolean): HermesStatusColors? = if (dark) darkStatus else lightStatus
}
/** Full theme: bespoke dark + light palettes. */
fun buildTheme(
dark: PaletteColors,
light: PaletteColors,
): ThemePalette = ThemePalette(ThemeMode.FULL, dark, light)
/** Dark-only theme (e.g. AMOLED). Light mode falls back to the default theme. */
fun buildThemeDarkOnly(dark: PaletteColors): ThemePalette = ThemePalette(ThemeMode.DARK_ONLY, dark, null)
/** Light-only theme. Dark mode falls back to the default theme. */
fun buildThemeLightOnly(light: PaletteColors): ThemePalette = ThemePalette(ThemeMode.LIGHT_ONLY, null, light)
private fun darkSchemeOf(c: PaletteColors): ColorScheme =
darkColorScheme(
primary = c.primary,
onPrimary = c.onPrimary,
primaryContainer = c.primaryContainer,
onPrimaryContainer = c.onPrimaryContainer,
secondary = c.secondary,
onSecondary = c.onSecondary,
secondaryContainer = c.secondaryContainer,
onSecondaryContainer = c.onSecondaryContainer,
tertiary = c.tertiary,
onTertiary = c.onTertiary,
tertiaryContainer = c.tertiaryContainer,
onTertiaryContainer = c.onTertiaryContainer,
background = c.background,
onBackground = c.onBackground,
surface = c.surface,
onSurface = c.onSurface,
surfaceVariant = c.surfaceVariant,
onSurfaceVariant = c.onSurfaceVariant,
surfaceTint = c.primary,
surfaceContainerLowest = c.surfaceContainerLowest,
surfaceContainerLow = c.surfaceContainerLow,
surfaceContainer = c.surfaceContainer,
surfaceContainerHigh = c.surfaceContainerHigh,
surfaceContainerHighest = c.surfaceContainerHighest,
inverseSurface = c.inverseSurface,
inverseOnSurface = c.inverseOnSurface,
inversePrimary = c.inversePrimary,
error = c.status.error,
onError = c.status.onError,
errorContainer = c.status.errorContainer,
onErrorContainer = c.status.onErrorContainer,
outline = c.outline,
outlineVariant = c.outlineVariant,
scrim = c.scrim,
)
private fun lightSchemeOf(c: PaletteColors): ColorScheme =
lightColorScheme(
primary = c.primary,
onPrimary = c.onPrimary,
primaryContainer = c.primaryContainer,
onPrimaryContainer = c.onPrimaryContainer,
secondary = c.secondary,
onSecondary = c.onSecondary,
secondaryContainer = c.secondaryContainer,
onSecondaryContainer = c.onSecondaryContainer,
tertiary = c.tertiary,
onTertiary = c.onTertiary,
tertiaryContainer = c.tertiaryContainer,
onTertiaryContainer = c.onTertiaryContainer,
background = c.background,
onBackground = c.onBackground,
surface = c.surface,
onSurface = c.onSurface,
surfaceVariant = c.surfaceVariant,
onSurfaceVariant = c.onSurfaceVariant,
surfaceTint = c.primary,
surfaceContainerLowest = c.surfaceContainerLowest,
surfaceContainerLow = c.surfaceContainerLow,
surfaceContainer = c.surfaceContainer,
surfaceContainerHigh = c.surfaceContainerHigh,
surfaceContainerHighest = c.surfaceContainerHighest,
inverseSurface = c.inverseSurface,
inverseOnSurface = c.inverseOnSurface,
inversePrimary = c.inversePrimary,
error = c.status.error,
onError = c.status.onError,
errorContainer = c.status.errorContainer,
onErrorContainer = c.status.onErrorContainer,
outline = c.outline,
outlineVariant = c.outlineVariant,
scrim = c.scrim,
)
@@ -8,106 +8,116 @@ All theme code is under:
```
app/src/main/java/com/m57/hermescontrol/theme/
├── Theme.kt # dispatcher — resolves scheme + status colors per preset
├── Color.kt # brand design tokens (DarkSurface, HermesPurple, StatusGreen, …)
├── Theme.kt # dispatcher — preset lookup + mode fallback
├── 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*)
├── Type.kt / Shapes.kt # typography + shape tokens
├── Spacing.kt / Motion.kt # spacing + motion tokens
└── presets/ # one file per ThemePreset
└── presets/ # one file per ThemePreset — same skeleton everywhere
├── DefaultScheme.kt
├── MonochromeScheme.kt
├── GruvboxScheme.kt
├── CatppuccinScheme.kt
├── AmoledScheme.kt
└── NeonNoirScheme.kt
├── AmoledScheme.kt # dark-only (ThemeMode.DARK_ONLY)
└── NordScheme.kt
```
## The 4-token shape
## The template — one shape for every theme
Every file in `presets/` is uniform — it exports the same four tokens:
| Token | Type | Purpose |
|-------|------|---------|
| `XxxDarkColorScheme` | `ColorScheme` | dark-mode Material 3 scheme |
| `XxxLightColorScheme` | `ColorScheme` | light-mode Material 3 scheme |
| `XxxDarkStatusColors` | `HermesStatusColors` | dark semantic status colors |
| `XxxLightStatusColors` | `HermesStatusColors` | light semantic status colors |
A preset with no bespoke status set (AMOLED) aliases the defaults:
Every preset file is a **single `ThemePalette`** built from raw color specs via
the template in `PaletteTemplate.kt`. A theme is a pure color spec: fill in
`PaletteColors` (the Material slots) + the semantic status set
(`HermesStatusColors`) and the app does the rest. No per-preset behavior, no
aliases, no shared-token imports.
```kotlin
val AmoledDarkStatusColors = DefaultDarkStatusColors
val AmoledLightStatusColors = DefaultLightStatusColors
val MyTheme =
buildTheme(
dark =
PaletteColors(
primary = Color(0xFF…),
// …every slot…
status =
HermesStatusColors(
success = Color(0xFF…),
// …
),
),
light = PaletteColors(/* … */),
)
```
AMOLED has no bespoke light scheme, so it aliases the default light scheme too:
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.
```kotlin
val AmoledLightColorScheme = DefaultLightColorScheme
```
## Preset conventions
- **Named swatches** for any hex reused across slots (Gruvbox bg ladder,
Catppuccin's official names, Nord's nord0–nord15, Monochrome's mono ramp).
A theme with repeated hexes should define them once at the top of the file.
- **Bright/faded accent split** (Gruvbox, Slate): bright variants against dark
surfaces, muted "faded" variants against light surfaces so contrast holds
without neon saturation. Single-accent-set themes (Nord) instead keep dark
"on" text in both modes — pastel accents stay light, so Snow-Storm-style
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.
## Theme modes
A theme declares which modes it ships via the factory you build it with:
| Factory | Mode | Meaning |
|---------|------|---------|
| `buildTheme(...)` | `FULL` | bespoke dark + light palettes |
| `buildThemeDarkOnly(...)` | `DARK_ONLY` | dark only; light mode falls back to the default theme (AMOLED) |
| `buildThemeLightOnly(...)` | `LIGHT_ONLY` | light only; dark mode falls back to the default theme |
The dispatcher falls back to the brand **default** theme for any mode a preset
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, it returns the matching scheme/status from the preset file:
`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):
```kotlin
private fun resolveColorScheme(preset: ThemePreset, darkTheme: Boolean) = when (preset) {
ThemePreset.DEFAULT -> if (darkTheme) DefaultDarkColorScheme else DefaultLightColorScheme
ThemePreset.MONOCHROME -> if (darkTheme) MonochromeDarkColorScheme else MonochromeLightColorScheme
// …one line per preset…
}
private fun resolveStatusColors(preset: ThemePreset, darkTheme: Boolean) = when (preset) {
ThemePreset.DEFAULT -> if (darkTheme) DefaultDarkStatusColors else DefaultLightStatusColors
private fun themeFor(preset: ThemePreset): ThemePalette = when (preset) {
ThemePreset.DEFAULT -> DefaultTheme
ThemePreset.MONOCHROME -> MonochromeTheme
// …one line per preset…
}
```
Dynamic (Material You) color on API 31+ overrides the preset scheme when
`useDynamicColors = true`.
## DEFAULT uses design tokens — that's intentional
`DefaultScheme.kt` imports ~47 named tokens from `Color.kt`
(`DarkBackground`, `HermesPurple`, `StatusGreenContainer`, …) and builds its
schemes from them. This is **by design**: DEFAULT is the brand reference theme,
so a palette change in `Color.kt` flows straight through.
The other presets are hand-authored community palettes and use **raw hex**
(`Color(0xFFFE8019)`), so they only import `Color` (+ `HermesStatusColors`).
Do **not** "fix" DEFAULT to raw hex to match them — that would break the token
linkage. The asymmetry is semantic, not sloppy.
`useDynamicColors = true`. Semantic status colors are always resolved from the
active preset via `LocalHermesStatusColors`.
## Adding a new theme
1. **Create** `presets/<Name>Scheme.kt` exporting the 4 tokens:
```kotlin
package com.m57.hermescontrol.theme.presets
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.ui.graphics.Color
import com.m57.hermescontrol.theme.HermesStatusColors
val MyThemeDarkColorScheme = darkColorScheme(/* … */)
val MyThemeLightColorScheme = lightColorScheme(/* … */)
val MyThemeDarkStatusColors = HermesStatusColors(/* … */)
val MyThemeLightStatusColors = HermesStatusColors(/* … */)
```
2. **Add 2 lines** to each `when` in `Theme.kt` (`resolveColorScheme` +
`resolveStatusColors`), importing the new tokens at the top of the file.
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.
3. **Add the enum entry** `MY_THEME` to `ThemePreset` in `Theme.kt`.
4. **Wire the UI**: add the label + selection in
`ui/settings/components/AppearanceSection.kt` (and any string resource).
5. **Verify**:
```bash
./ktlint --format app/src/main/java/com/m57/hermescontrol/theme/presets/<Name>Scheme.kt
./gradlew compileDebugKotlin
./gradlew ktlintCheck testDebugUnitTest
```
`ThemePaletteTest` asserts >= 3:1 contrast on every shipped mode's error
slot pairs and the ThemeMode invariants — it must stay green.
## Conventions
- ktlint 1.2.1 is enforced in CI. Run `./ktlint --format` before committing.
- ktlint 1.2.1 is enforced in CI. Run `./gradlew ktlintCheck` before committing
(the gradle task is the gate, not the standalone `ktlint` binary).
- Import order is ASCII-lexicographic (uppercase before lowercase).
- Every change goes through a PR — never push directly to `main`.
@@ -2,6 +2,7 @@ package com.m57.hermescontrol.theme
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
@@ -9,75 +10,64 @@ 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.AmoledDarkColorScheme
import com.m57.hermescontrol.theme.presets.AmoledDarkStatusColors
import com.m57.hermescontrol.theme.presets.AmoledLightColorScheme
import com.m57.hermescontrol.theme.presets.AmoledLightStatusColors
import com.m57.hermescontrol.theme.presets.CatppuccinDarkColorScheme
import com.m57.hermescontrol.theme.presets.CatppuccinDarkStatusColors
import com.m57.hermescontrol.theme.presets.CatppuccinLightColorScheme
import com.m57.hermescontrol.theme.presets.CatppuccinLightStatusColors
import com.m57.hermescontrol.theme.presets.DefaultDarkColorScheme
import com.m57.hermescontrol.theme.presets.DefaultDarkStatusColors
import com.m57.hermescontrol.theme.presets.DefaultLightColorScheme
import com.m57.hermescontrol.theme.presets.DefaultLightStatusColors
import com.m57.hermescontrol.theme.presets.GruvboxDarkColorScheme
import com.m57.hermescontrol.theme.presets.GruvboxDarkStatusColors
import com.m57.hermescontrol.theme.presets.GruvboxLightColorScheme
import com.m57.hermescontrol.theme.presets.GruvboxLightStatusColors
import com.m57.hermescontrol.theme.presets.MonochromeDarkColorScheme
import com.m57.hermescontrol.theme.presets.MonochromeDarkStatusColors
import com.m57.hermescontrol.theme.presets.MonochromeLightColorScheme
import com.m57.hermescontrol.theme.presets.MonochromeLightStatusColors
import com.m57.hermescontrol.theme.presets.NeonNoirDarkColorScheme
import com.m57.hermescontrol.theme.presets.NeonNoirDarkStatusColors
import com.m57.hermescontrol.theme.presets.NeonNoirLightColorScheme
import com.m57.hermescontrol.theme.presets.NeonNoirLightStatusColors
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
enum class ThemePreference { SYSTEM, LIGHT, DARK }
@Serializable
enum class ThemePreset { DEFAULT, MONOCHROME, GRUVBOX, CATPPUCCIN, AMOLED, NEON_NOIR }
enum class ThemePreset { DEFAULT, MONOCHROME, GRUVBOX, CATPPUCCIN, AMOLED, NORD }
val LocalThemePreference = compositionLocalOf { ThemePreference.SYSTEM }
val LocalThemePreset = compositionLocalOf { ThemePreset.DEFAULT }
/**
* 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.
*
* Every preset exports both a dark and light scheme. AMOLED's light scheme is
* an explicit alias of the brand default light scheme.
* A preset that doesn't ship the requested mode (DARK_ONLY / LIGHT_ONLY —
* e.g. AMOLED has no light palette) falls back to the default theme's
* palette for that mode.
*/
private fun resolveColorScheme(
internal fun resolveColorScheme(
preset: ThemePreset,
darkTheme: Boolean,
) = when (preset) {
ThemePreset.DEFAULT -> if (darkTheme) DefaultDarkColorScheme else DefaultLightColorScheme
ThemePreset.MONOCHROME -> if (darkTheme) MonochromeDarkColorScheme else MonochromeLightColorScheme
ThemePreset.GRUVBOX -> if (darkTheme) GruvboxDarkColorScheme else GruvboxLightColorScheme
ThemePreset.CATPPUCCIN -> if (darkTheme) CatppuccinDarkColorScheme else CatppuccinLightColorScheme
ThemePreset.AMOLED -> if (darkTheme) AmoledDarkColorScheme else AmoledLightColorScheme
ThemePreset.NEON_NOIR -> if (darkTheme) NeonNoirDarkColorScheme else NeonNoirLightColorScheme
): ColorScheme {
val theme = themeFor(preset)
// DefaultTheme is FULL — its scheme is never null for either mode.
return theme.schemeFor(darkTheme) ?: requireNotNull(DefaultTheme.schemeFor(darkTheme))
}
/**
* Resolve the semantic status colors for a preset + dark flag.
*
* Every preset exports both a dark and light status set. MONOCHROME and AMOLED
* alias the brand default status colors (they have no bespoke semantic set).
* Same fallback rule as the scheme.
*/
private fun resolveStatusColors(
internal fun resolveStatusColors(
preset: ThemePreset,
darkTheme: Boolean,
) = when (preset) {
ThemePreset.DEFAULT -> if (darkTheme) DefaultDarkStatusColors else DefaultLightStatusColors
ThemePreset.MONOCHROME -> if (darkTheme) MonochromeDarkStatusColors else MonochromeLightStatusColors
ThemePreset.GRUVBOX -> if (darkTheme) GruvboxDarkStatusColors else GruvboxLightStatusColors
ThemePreset.CATPPUCCIN -> if (darkTheme) CatppuccinDarkStatusColors else CatppuccinLightStatusColors
ThemePreset.AMOLED -> if (darkTheme) AmoledDarkStatusColors else AmoledLightStatusColors
ThemePreset.NEON_NOIR -> if (darkTheme) NeonNoirDarkStatusColors else NeonNoirLightStatusColors
): HermesStatusColors {
val theme = themeFor(preset)
// DefaultTheme is FULL — its status colors are never null for either mode.
return theme.statusFor(darkTheme) ?: requireNotNull(DefaultTheme.statusFor(darkTheme))
}
@Composable
@@ -1,63 +1,90 @@
package com.m57.hermescontrol.theme.presets
import androidx.compose.material3.darkColorScheme
import androidx.compose.ui.graphics.Color
import com.m57.hermescontrol.theme.HermesAmber
import com.m57.hermescontrol.theme.HermesAmberLight
import com.m57.hermescontrol.theme.HermesPurple
import com.m57.hermescontrol.theme.HermesPurpleOnContainer
import com.m57.hermescontrol.theme.HermesStatusColors
import com.m57.hermescontrol.theme.PaletteColors
import com.m57.hermescontrol.theme.buildThemeDarkOnly
// ---------------------------------------------------------------------
// AMOLED — true-black variant of MonochromeTheme. Dark only:
// background is pure #000000 so OLED panels switch those pixels off,
// which MonochromeTheme's #121212 baseline doesn't give you.
// ---------------------------------------------------------------------
private val AmoledFloor = Color(0xFF000000) // background/surface/lowest — nothing darker exists
private val AmoledContainer = Color(0xFF0D0D0D) // surfaceContainer
private val AmoledContainerHigh = Color(0xFF1A1A1A) // surfaceContainerHigh, surfaceVariant, status containers
private val AmoledContainerHighest = Color(0xFF262626) // surfaceContainerHighest, primaryContainer
private val AmoledOutline = Color(0xFF808080)
private val AmoledOutlineVariant = Color(0xFF404040)
private val AmoledTextDim = Color(0xFFB3B3B3) // onSurfaceVariant
private val AmoledTextBright = Color(0xFFFFFFFF) // pure white — max contrast on black; primary, error, text
private val AmoledSecondary = Color(0xFFCCCCCC) // secondary accent + warning
private val AmoledTertiary = Color(0xFF999999) // tertiary accent + success
private val AmoledInfo = Color(0xFF808080) // info — same value as outline, distinct role
private val AmoledInversePrimary = Color(0xFF121212) // dark tone on the (light, fallback) inverseSurface
/**
* AMOLED color scheme (dark only).
* AMOLED theme — dark-only variant of MonochromeTheme for
* true-black displays. Background is pure #000000 rather than the
* #121212 baseline the standard Monochrome preset uses, so surfaceContainerLowest
* and surfaceContainerLow collapse to the same value — there's nothing
* darker than black to separate them into. Light mode isn't shipped
* (a light AMOLED theme is a contradiction); the dispatcher falls back
* to the default theme.
*
* AMOLED has no light variant — callers fall back to the brand default light
* scheme for light mode.
* Same accessibility note as MonochromeTheme: status colors are pure
* grayscale, separated only by lightness, so don't rely on them alone
* to convey success/warning/error/info — pair with an icon or label.
*/
val AmoledDarkColorScheme =
darkColorScheme(
primary = HermesPurple,
onPrimary = Color.White,
primaryContainer = Color(0xFF1A1040),
onPrimaryContainer = HermesPurpleOnContainer,
inversePrimary = Color(0xFF6750A4),
secondary = HermesAmber,
onSecondary = Color.Black,
secondaryContainer = Color(0xFF2A2000),
onSecondaryContainer = HermesAmberLight,
tertiary = HermesAmberLight,
onTertiary = Color.Black,
tertiaryContainer = Color(0xFF2A2000),
onTertiaryContainer = HermesAmberLight,
background = Color.Black,
onBackground = Color(0xFFE8E6EE),
surface = Color.Black,
onSurface = Color(0xFFE8E6EE),
surfaceVariant = Color(0xFF121218),
onSurfaceVariant = Color(0xFFB6B2C4),
surfaceTint = HermesPurple,
surfaceContainerLowest = Color.Black,
surfaceContainerLow = Color(0xFF08080A),
surfaceContainer = Color(0xFF0E0E12),
surfaceContainerHigh = Color(0xFF16161C),
surfaceContainerHighest = Color(0xFF1F1F28),
inverseSurface = Color(0xFFE8E6EE),
inverseOnSurface = Color.Black,
error = Color(0xFFCF6679),
onError = Color.Black,
errorContainer = Color(0xFF3700B3),
onErrorContainer = Color(0xFFF2B8B5),
outline = Color(0xFF3A3A4A),
outlineVariant = Color(0xFF252532),
scrim = Color.Black,
val AmoledTheme =
buildThemeDarkOnly(
dark =
PaletteColors(
primary = AmoledTextBright,
onPrimary = AmoledFloor,
primaryContainer = AmoledContainerHighest,
onPrimaryContainer = AmoledTextBright,
secondary = AmoledSecondary,
onSecondary = AmoledFloor,
secondaryContainer = AmoledContainerHigh,
onSecondaryContainer = AmoledSecondary,
tertiary = AmoledTertiary,
onTertiary = AmoledFloor,
tertiaryContainer = AmoledContainerHigh,
onTertiaryContainer = AmoledTertiary,
background = AmoledFloor,
onBackground = AmoledTextBright,
surface = AmoledFloor,
onSurface = AmoledTextBright,
surfaceVariant = AmoledContainerHigh,
onSurfaceVariant = AmoledTextDim,
surfaceContainerLowest = AmoledFloor,
surfaceContainerLow = AmoledFloor,
surfaceContainer = AmoledContainer,
surfaceContainerHigh = AmoledContainerHigh,
surfaceContainerHighest = AmoledContainerHighest,
inverseSurface = AmoledTextBright,
inverseOnSurface = AmoledFloor,
inversePrimary = AmoledInversePrimary,
outline = AmoledOutline,
outlineVariant = AmoledOutlineVariant,
scrim = Color.Black,
status =
HermesStatusColors(
success = AmoledTertiary,
successContainer = AmoledContainerHigh,
onSuccess = AmoledFloor,
warning = AmoledSecondary,
warningContainer = AmoledContainerHigh,
onWarning = AmoledFloor,
error = AmoledTextBright,
errorContainer = AmoledContainerHigh,
onError = AmoledFloor,
onErrorContainer = AmoledTextBright,
info = AmoledInfo,
infoContainer = AmoledContainerHigh,
onInfo = AmoledFloor,
),
),
)
/**
* AMOLED has no bespoke light scheme — reuse the brand default light scheme.
*/
val AmoledLightColorScheme = DefaultLightColorScheme
/** AMOLED status colors (dark) — reuses the brand default set. */
val AmoledDarkStatusColors = DefaultDarkStatusColors
/** AMOLED status colors (light) — reuses the brand default set. */
val AmoledLightStatusColors = DefaultLightStatusColors
@@ -1,118 +1,152 @@
package com.m57.hermescontrol.theme.presets
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.ui.graphics.Color
import com.m57.hermescontrol.theme.HermesStatusColors
import com.m57.hermescontrol.theme.PaletteColors
import com.m57.hermescontrol.theme.buildTheme
/** Catppuccin color scheme (dark). */
val CatppuccinDarkColorScheme =
darkColorScheme(
primary = Color(0xFFCBA6F7), // Mauve
onPrimary = Color(0xFF11111B), // Crust
primaryContainer = Color(0xFF45475A), // Surface 1
onPrimaryContainer = Color(0xFFCDD6F4), // Text
inversePrimary = Color(0xFF8839EF), // Latte Mauve
secondary = Color(0xFF89B4FA), // Blue
onSecondary = Color(0xFF11111B), // Crust
secondaryContainer = Color(0xFF313244), // Surface 0
onSecondaryContainer = Color(0xFFBAC2DE), // Subtext 1
tertiary = Color(0xFFF5C2E7), // Pink
onTertiary = Color(0xFF11111B), // Crust
tertiaryContainer = Color(0xFF45475A), // Surface 1
onTertiaryContainer = Color(0xFFCDD6F4), // Text
background = Color(0xFF1E1E2E), // Base
onBackground = Color(0xFFCDD6F4), // Text
surface = Color(0xFF1E1E2E), // Base
onSurface = Color(0xFFCDD6F4), // Text
surfaceVariant = Color(0xFF313244), // Surface 0
onSurfaceVariant = Color(0xFFA6ADC8), // Subtext 0
surfaceTint = Color(0xFFCBA6F7), // Mauve
surfaceContainerLowest = Color(0xFF11111B), // Crust
surfaceContainerLow = Color(0xFF181825), // Mantle
surfaceContainer = Color(0xFF1E1E2E), // Base
surfaceContainerHigh = Color(0xFF313244), // Surface 0
surfaceContainerHighest = Color(0xFF45475A), // Surface 1
inverseSurface = Color(0xFFCDD6F4), // Text
inverseOnSurface = Color(0xFF1E1E2E), // Base
error = Color(0xFFF38BA8), // Red
onError = Color(0xFF11111B), // Crust
errorContainer = Color(0xFF313244), // Surface 0
onErrorContainer = Color(0xFFF38BA8), // Red
outline = Color(0xFF6C7086), // Overlay 0
outlineVariant = Color(0xFF45475A), // Surface 1
scrim = Color(0xFF11111B), // Crust
)
// ---------------------------------------------------------------------
// Mocha (dark) — named swatches, so repeated hexes (Crust, Surface0/1,
// Base, Text, Blue...) live in exactly one place.
// ---------------------------------------------------------------------
/** Catppuccin color scheme (light). */
val CatppuccinLightColorScheme =
lightColorScheme(
primary = Color(0xFF8839EF), // Mauve
onPrimary = Color(0xFFEFF1F5), // Base
primaryContainer = Color(0xFFE6CAFA), // Light Mauve Container
onPrimaryContainer = Color(0xFF380075),
inversePrimary = Color(0xFFCBA6F7), // Mocha Mauve
secondary = Color(0xFF1E66F5), // Blue
onSecondary = Color(0xFFEFF1F5), // Base
secondaryContainer = Color(0xFFE6E9EF), // Mantle
onSecondaryContainer = Color(0xFF4C4F69), // Text
tertiary = Color(0xFFEA76CB), // Pink
onTertiary = Color(0xFFEFF1F5), // Base
tertiaryContainer = Color(0xFFBCC0CC), // Surface 1
onTertiaryContainer = Color(0xFF4C4F69), // Text
background = Color(0xFFEFF1F5), // Base
onBackground = Color(0xFF4C4F69), // Text
surface = Color(0xFFEFF1F5), // Base
onSurface = Color(0xFF4C4F69), // Text
surfaceVariant = Color(0xFFCCD0DA), // Surface 0
onSurfaceVariant = Color(0xFF6C6F85), // Subtext 0
surfaceTint = Color(0xFF8839EF), // Mauve
surfaceContainerLowest = Color(0xFFDCE0E8), // Crust
surfaceContainerLow = Color(0xFFE6E9EF), // Mantle
surfaceContainer = Color(0xFFEFF1F5), // Base
surfaceContainerHigh = Color(0xFFCCD0DA), // Surface 0
surfaceContainerHighest = Color(0xFFBCC0CC), // Surface 1
inverseSurface = Color(0xFF4C4F69), // Text
inverseOnSurface = Color(0xFFEFF1F5), // Base
error = Color(0xFFD20F39), // Red
onError = Color(0xFFEFF1F5), // Base
errorContainer = Color(0xFFCCD0DA), // Surface 0
onErrorContainer = Color(0xFFD20F39), // Red
outline = Color(0xFF9CA0B0), // Overlay 0
outlineVariant = Color(0xFFBCC0CC), // Surface 1
scrim = Color(0xFFDCE0E8), // Crust
)
private val MochaMauve = Color(0xFFCBA6F7)
private val MochaBlue = Color(0xFF89B4FA)
private val MochaPink = Color(0xFFF5C2E7)
private val MochaGreen = Color(0xFFA6E3A1)
private val MochaYellow = Color(0xFFF9E2AF)
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)
private val MochaMantle = Color(0xFF181825)
private val MochaCrust = Color(0xFF11111B)
/** Catppuccin status colors (dark). */
val CatppuccinDarkStatusColors =
HermesStatusColors(
success = Color(0xFFA6E3A1), // Green
successContainer = Color(0xFF313244), // Surface 0
onSuccess = Color(0xFF11111B), // Crust
warning = Color(0xFFF9E2AF), // Yellow
warningContainer = Color(0xFF313244), // Surface 0
onWarning = Color(0xFF11111B), // Crust
error = Color(0xFFF38BA8), // Red
errorContainer = Color(0xFF313244), // Surface 0
onError = Color(0xFF11111B), // Crust
info = Color(0xFF89B4FA), // Blue
infoContainer = Color(0xFF313244), // Surface 0
onInfo = Color(0xFF11111B), // Crust
)
// ---------------------------------------------------------------------
// Latte (light) — named swatches
// ---------------------------------------------------------------------
/** Catppuccin status colors (light). */
val CatppuccinLightStatusColors =
HermesStatusColors(
success = Color(0xFF40A02B), // Green
successContainer = Color(0xFFCCD0DA), // Surface 0
onSuccess = Color(0xFFEFF1F5), // Base
warning = Color(0xFFDF8E1D), // Yellow
warningContainer = Color(0xFFCCD0DA), // Surface 0
onWarning = Color(0xFFEFF1F5), // Base
error = Color(0xFFD20F39), // Red
errorContainer = Color(0xFFCCD0DA), // Surface 0
onError = Color(0xFFEFF1F5), // Base
info = Color(0xFF1E66F5), // Blue
infoContainer = Color(0xFFCCD0DA), // Surface 0
onInfo = Color(0xFFEFF1F5), // Base
private val LatteMauve = Color(0xFF8839EF)
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)
private val LatteSubtext0 = Color(0xFF6C6F85)
private val LatteOverlay0 = Color(0xFF9CA0B0)
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)
/** Catppuccin theme — Mocha (dark) / Latte (light). */
val CatppuccinTheme =
buildTheme(
dark =
PaletteColors(
primary = MochaMauve,
onPrimary = MochaCrust,
primaryContainer = MochaSurface1,
onPrimaryContainer = MochaText,
secondary = MochaBlue,
onSecondary = MochaCrust,
secondaryContainer = MochaSurface0,
onSecondaryContainer = MochaSubtext1,
tertiary = MochaPink,
onTertiary = MochaCrust,
tertiaryContainer = MochaSurface1,
onTertiaryContainer = MochaText,
background = MochaBase,
onBackground = MochaText,
surface = MochaBase,
onSurface = MochaText,
surfaceVariant = MochaSurface0,
onSurfaceVariant = MochaSubtext0,
surfaceContainerLowest = MochaCrust,
surfaceContainerLow = MochaMantle,
surfaceContainer = MochaBase,
surfaceContainerHigh = MochaSurface0,
surfaceContainerHighest = MochaSurface1,
inverseSurface = MochaText,
inverseOnSurface = MochaBase,
inversePrimary = LatteMauve,
outline = MochaOverlay0,
outlineVariant = MochaSurface1,
scrim = MochaCrust,
status =
HermesStatusColors(
success = MochaGreen,
successContainer = MochaSurface0,
onSuccess = MochaCrust,
warning = MochaYellow,
warningContainer = MochaSurface0,
onWarning = MochaCrust,
error = MochaRed,
errorContainer = MochaSurface0,
onError = MochaCrust,
onErrorContainer = MochaRed,
info = MochaBlue,
infoContainer = MochaSurface0,
onInfo = MochaCrust,
),
),
light =
PaletteColors(
primary = LatteMauve,
onPrimary = LatteBase,
primaryContainer = LattePrimaryContainer,
onPrimaryContainer = LatteOnPrimaryContainer,
secondary = LatteBlue,
onSecondary = LatteBase,
secondaryContainer = LatteMantle,
onSecondaryContainer = LatteText,
tertiary = LattePink,
onTertiary = LatteBase,
tertiaryContainer = LatteSurface1,
onTertiaryContainer = LatteText,
background = LatteBase,
onBackground = LatteText,
surface = LatteBase,
onSurface = LatteText,
surfaceVariant = LatteSurface0,
onSurfaceVariant = LatteSubtext0,
surfaceContainerLowest = LatteCrust,
surfaceContainerLow = LatteMantle,
surfaceContainer = LatteBase,
surfaceContainerHigh = LatteSurface0,
surfaceContainerHighest = LatteSurface1,
inverseSurface = LatteText,
inverseOnSurface = LatteBase,
inversePrimary = MochaMauve,
outline = LatteOverlay0,
outlineVariant = LatteSurface1,
scrim = LatteCrust,
status =
HermesStatusColors(
success = LatteGreen,
successContainer = LatteSurface0,
onSuccess = LatteBase,
warning = LatteYellow,
warningContainer = LatteSurface0,
onWarning = LatteBase,
error = LatteRed,
errorContainer = LatteSurface0,
onError = LatteBase,
onErrorContainer = LatteRed,
info = LatteBlue,
infoContainer = LatteSurface0,
onInfo = LatteBase,
),
),
)
@@ -1,165 +1,152 @@
package com.m57.hermescontrol.theme.presets
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.ui.graphics.Color
import com.m57.hermescontrol.theme.DarkBackground
import com.m57.hermescontrol.theme.DarkInverseOnSurface
import com.m57.hermescontrol.theme.DarkInverseSurface
import com.m57.hermescontrol.theme.DarkOnSurface
import com.m57.hermescontrol.theme.DarkOnSurfaceVariant
import com.m57.hermescontrol.theme.DarkOutline
import com.m57.hermescontrol.theme.DarkOutlineVariant
import com.m57.hermescontrol.theme.DarkScrim
import com.m57.hermescontrol.theme.DarkSurface
import com.m57.hermescontrol.theme.DarkSurfaceContainer
import com.m57.hermescontrol.theme.DarkSurfaceContainerHigh
import com.m57.hermescontrol.theme.DarkSurfaceContainerHighest
import com.m57.hermescontrol.theme.DarkSurfaceContainerLow
import com.m57.hermescontrol.theme.DarkSurfaceContainerLowest
import com.m57.hermescontrol.theme.DarkSurfaceVariant
import com.m57.hermescontrol.theme.HermesAmber
import com.m57.hermescontrol.theme.HermesAmberDark
import com.m57.hermescontrol.theme.HermesAmberLight
import com.m57.hermescontrol.theme.HermesPurple
import com.m57.hermescontrol.theme.HermesPurpleContainer
import com.m57.hermescontrol.theme.HermesPurpleDark
import com.m57.hermescontrol.theme.HermesPurpleLight
import com.m57.hermescontrol.theme.HermesPurpleOnContainer
import com.m57.hermescontrol.theme.HermesStatusColors
import com.m57.hermescontrol.theme.LightBackground
import com.m57.hermescontrol.theme.LightInverseOnSurface
import com.m57.hermescontrol.theme.LightInverseSurface
import com.m57.hermescontrol.theme.LightOnSurface
import com.m57.hermescontrol.theme.LightOnSurfaceVariant
import com.m57.hermescontrol.theme.LightOutline
import com.m57.hermescontrol.theme.LightOutlineVariant
import com.m57.hermescontrol.theme.LightScrim
import com.m57.hermescontrol.theme.LightSurface
import com.m57.hermescontrol.theme.LightSurfaceContainer
import com.m57.hermescontrol.theme.LightSurfaceContainerHigh
import com.m57.hermescontrol.theme.LightSurfaceContainerHighest
import com.m57.hermescontrol.theme.LightSurfaceContainerLow
import com.m57.hermescontrol.theme.LightSurfaceContainerLowest
import com.m57.hermescontrol.theme.LightSurfaceVariant
import com.m57.hermescontrol.theme.StatusBlue
import com.m57.hermescontrol.theme.StatusBlueContainer
import com.m57.hermescontrol.theme.StatusGreen
import com.m57.hermescontrol.theme.StatusGreenContainer
import com.m57.hermescontrol.theme.StatusGreyLight
import com.m57.hermescontrol.theme.StatusRed
import com.m57.hermescontrol.theme.StatusRedContainer
import com.m57.hermescontrol.theme.StatusYellow
import com.m57.hermescontrol.theme.StatusYellowContainer
import com.m57.hermescontrol.theme.PaletteColors
import com.m57.hermescontrol.theme.buildTheme
/** Brand default color scheme (dark) — Voltage Purple primary. */
val DefaultDarkColorScheme =
darkColorScheme(
primary = HermesPurple,
onPrimary = Color.White,
primaryContainer = HermesPurpleContainer,
onPrimaryContainer = HermesPurpleOnContainer,
inversePrimary = HermesPurpleDark,
secondary = HermesAmber,
onSecondary = Color.Black,
secondaryContainer = Color(0xFF3D2F0F),
onSecondaryContainer = HermesAmberLight,
tertiary = HermesAmberLight,
onTertiary = Color.Black,
tertiaryContainer = Color(0xFF3D2F0F),
onTertiaryContainer = HermesAmberLight,
background = DarkBackground,
onBackground = DarkOnSurface,
surface = DarkSurface,
onSurface = DarkOnSurface,
surfaceVariant = DarkSurfaceVariant,
onSurfaceVariant = DarkOnSurfaceVariant,
surfaceTint = HermesPurple,
surfaceContainerLowest = DarkSurfaceContainerLowest,
surfaceContainerLow = DarkSurfaceContainerLow,
surfaceContainer = DarkSurfaceContainer,
surfaceContainerHigh = DarkSurfaceContainerHigh,
surfaceContainerHighest = DarkSurfaceContainerHighest,
inverseSurface = DarkInverseSurface,
inverseOnSurface = DarkInverseOnSurface,
error = StatusRed,
onError = Color.White,
errorContainer = StatusRedContainer,
onErrorContainer = Color(0xFFFFB4B4),
outline = DarkOutline,
outlineVariant = DarkOutlineVariant,
scrim = DarkScrim,
)
// ---------------------------------------------------------------------
// Slate Dark — named swatches
// ---------------------------------------------------------------------
/** Brand default color scheme (light) — Voltage Purple primary. */
val DefaultLightColorScheme =
lightColorScheme(
primary = HermesPurpleDark,
onPrimary = Color.White,
primaryContainer = HermesPurpleLight,
onPrimaryContainer = Color(0xFF1E0F66),
inversePrimary = HermesPurpleLight,
secondary = HermesAmberDark,
onSecondary = Color.White,
secondaryContainer = HermesAmberLight,
onSecondaryContainer = Color(0xFF3D2F0F),
tertiary = HermesAmberDark,
onTertiary = Color.White,
tertiaryContainer = HermesAmberLight,
onTertiaryContainer = Color(0xFF3D2F0F),
background = LightBackground,
onBackground = LightOnSurface,
surface = LightSurface,
onSurface = LightOnSurface,
surfaceVariant = LightSurfaceVariant,
onSurfaceVariant = LightOnSurfaceVariant,
surfaceTint = HermesPurple,
surfaceContainerLowest = LightSurfaceContainerLowest,
surfaceContainerLow = LightSurfaceContainerLow,
surfaceContainer = LightSurfaceContainer,
surfaceContainerHigh = LightSurfaceContainerHigh,
surfaceContainerHighest = LightSurfaceContainerHighest,
inverseSurface = LightInverseSurface,
inverseOnSurface = LightInverseOnSurface,
error = Color(0xFFB3261E),
onError = Color.White,
errorContainer = Color(0xFFF9DEDC),
onErrorContainer = Color(0xFF410E0B),
outline = LightOutline,
outlineVariant = LightOutlineVariant,
scrim = LightScrim,
)
private val SlateDarkBg0 = Color(0xFF0F1416)
private val SlateDarkBg1 = Color(0xFF171C1E)
private val SlateDarkBg2 = Color(0xFF1B2022)
private val SlateDarkBg3 = Color(0xFF252B2D)
private val SlateDarkBg4 = Color(0xFF303638)
private val SlateDarkFg1 = Color(0xFFDEE3E5)
private val SlateDarkFg2 = Color(0xFFCEE7EC)
private val SlateDarkFg3 = Color(0xFFBEC8CA)
private val SlateGray = Color(0xFF889294) // same neutral gray in both modes
/** Default status colors (dark). */
val DefaultDarkStatusColors =
HermesStatusColors(
success = StatusGreen,
successContainer = StatusGreenContainer,
onSuccess = StatusGreyLight,
warning = StatusYellow,
warningContainer = StatusYellowContainer,
onWarning = StatusGreyLight,
error = StatusRed,
errorContainer = StatusRedContainer,
onError = StatusGreyLight,
info = StatusBlue,
infoContainer = StatusBlueContainer,
onInfo = StatusGreyLight,
)
// "Bright" accents — high-visibility variants against dark surfaces.
private val SlateBrightTeal = Color(0xFF80D5D2)
private val SlateBrightBlue = Color(0xFFA3C9EC)
private val SlateBrightCyan = Color(0xFFB2CBD0)
private val SlateBrightGreen = Color(0xFF81D692)
private val SlateBrightYellow = Color(0xFFECC248)
private val SlateBrightRed = Color(0xFFFFB4AB)
private val SlateBrightInfo = Color(0xFF8ECEFF)
/** Default status colors (light). */
val DefaultLightStatusColors =
HermesStatusColors(
success = Color(0xFF1B873A),
successContainer = Color(0xFFD7F5E0),
onSuccess = Color(0xFFFDF8FF),
warning = HermesAmberDark,
warningContainer = Color(0xFFFFEAB3),
onWarning = Color(0xFFFDF8FF),
error = Color(0xFFB3261E),
errorContainer = Color(0xFFF9DEDC),
onError = Color(0xFFFDF8FF),
info = Color(0xFF2E6FBD),
infoContainer = Color(0xFFD4E7FF),
onInfo = Color(0xFFFDF8FF),
// ---------------------------------------------------------------------
// Slate Light — named swatches
// ---------------------------------------------------------------------
private val SlateLightBg0 = Color(0xFFF5FAFC)
private val SlateLightBg1 = Color(0xFFEFF4F6)
private val SlateLightBg2 = Color(0xFFE9EEF0)
private val SlateLightBg3 = Color(0xFFE3E8EA)
private val SlateLightBg4 = Color(0xFFDEE3E5)
private val SlateLightFg1 = Color(0xFF171C1E)
private val SlateLightFg2 = Color(0xFF051F23)
private val SlateLightFg3 = Color(0xFF3F484A)
// "Faded" accents — muted variants for light background contrast.
private val SlateFadedTeal = Color(0xFF006A68)
private val SlateFadedBlue = Color(0xFF3B6184)
private val SlateFadedCyan = Color(0xFF4A6367)
private val SlateFadedGreen = Color(0xFF126D2B)
private val SlateFadedYellow = Color(0xFF725C00)
private val SlateFadedRed = Color(0xFFBA1A1A)
private val SlateFadedInfo = Color(0xFF006393)
/** Default theme (Slate) — modern slate blues with teal accent notes. */
val DefaultTheme =
buildTheme(
dark =
PaletteColors(
primary = SlateBrightTeal,
onPrimary = SlateDarkBg0,
primaryContainer = SlateDarkBg4,
onPrimaryContainer = SlateDarkFg1,
secondary = SlateBrightCyan,
onSecondary = SlateDarkBg0,
secondaryContainer = SlateDarkBg3,
onSecondaryContainer = SlateDarkFg2,
tertiary = SlateBrightBlue,
onTertiary = SlateDarkBg0,
tertiaryContainer = SlateDarkBg4,
onTertiaryContainer = SlateDarkFg1,
background = SlateDarkBg0,
onBackground = SlateDarkFg1,
surface = SlateDarkBg0,
onSurface = SlateDarkFg1,
surfaceVariant = SlateDarkBg1,
onSurfaceVariant = SlateDarkFg3,
surfaceContainerLowest = SlateDarkBg0,
surfaceContainerLow = SlateDarkBg1,
surfaceContainer = SlateDarkBg2,
surfaceContainerHigh = SlateDarkBg3,
surfaceContainerHighest = SlateDarkBg4,
inverseSurface = SlateDarkFg1,
inverseOnSurface = SlateDarkBg0,
inversePrimary = SlateFadedTeal,
outline = SlateGray,
outlineVariant = SlateDarkBg3,
scrim = SlateDarkBg0,
status =
HermesStatusColors(
success = SlateBrightGreen,
successContainer = SlateDarkBg3,
onSuccess = SlateDarkBg0,
warning = SlateBrightYellow,
warningContainer = SlateDarkBg3,
onWarning = SlateDarkBg0,
error = SlateBrightRed,
errorContainer = SlateDarkBg3,
onError = SlateDarkBg0,
onErrorContainer = SlateBrightRed,
info = SlateBrightInfo,
infoContainer = SlateDarkBg3,
onInfo = SlateDarkBg0,
),
),
light =
PaletteColors(
primary = SlateFadedTeal,
onPrimary = SlateLightBg0,
primaryContainer = SlateLightBg4,
onPrimaryContainer = SlateLightFg1,
secondary = SlateFadedCyan,
onSecondary = SlateLightBg0,
secondaryContainer = SlateLightBg3,
onSecondaryContainer = SlateLightFg2,
tertiary = SlateFadedBlue,
onTertiary = SlateLightBg0,
tertiaryContainer = SlateLightBg4,
onTertiaryContainer = SlateLightFg1,
background = SlateLightBg0,
onBackground = SlateLightFg1,
surface = SlateLightBg0,
onSurface = SlateLightFg1,
surfaceVariant = SlateLightBg1,
onSurfaceVariant = SlateLightFg3,
surfaceContainerLowest = SlateLightBg0,
surfaceContainerLow = SlateLightBg1,
surfaceContainer = SlateLightBg2,
surfaceContainerHigh = SlateLightBg3,
surfaceContainerHighest = SlateLightBg4,
inverseSurface = SlateLightFg1,
inverseOnSurface = SlateLightBg0,
inversePrimary = SlateBrightTeal,
outline = SlateGray,
outlineVariant = SlateLightBg3,
scrim = SlateLightBg0,
status =
HermesStatusColors(
success = SlateFadedGreen,
successContainer = SlateLightBg3,
onSuccess = SlateLightBg0,
warning = SlateFadedYellow,
warningContainer = SlateLightBg3,
onWarning = SlateLightBg0,
error = SlateFadedRed,
errorContainer = SlateLightBg3,
onError = SlateLightBg0,
onErrorContainer = SlateFadedRed,
info = SlateFadedInfo,
infoContainer = SlateLightBg3,
onInfo = SlateLightBg0,
),
),
)
@@ -1,118 +1,151 @@
package com.m57.hermescontrol.theme.presets
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.ui.graphics.Color
import com.m57.hermescontrol.theme.HermesStatusColors
import com.m57.hermescontrol.theme.PaletteColors
import com.m57.hermescontrol.theme.buildTheme
/** Gruvbox color scheme (dark). */
val GruvboxDarkColorScheme =
darkColorScheme(
primary = Color(0xFFFE8019),
onPrimary = Color(0xFF282828),
primaryContainer = Color(0xFFD65D0E),
onPrimaryContainer = Color(0xFFFBF1C7),
inversePrimary = Color(0xFFD65D0E),
secondary = Color(0xFFFABD2F),
onSecondary = Color(0xFF282828),
secondaryContainer = Color(0xFF3C3836),
onSecondaryContainer = Color(0xFFBDAE93),
tertiary = Color(0xFF8EC07C),
onTertiary = Color(0xFF282828),
tertiaryContainer = Color(0xFF458588),
onTertiaryContainer = Color(0xFFFBF1C7),
background = Color(0xFF282828),
onBackground = Color(0xFFFBF1C7),
surface = Color(0xFF32302F),
onSurface = Color(0xFFFBF1C7),
surfaceVariant = Color(0xFF3C3836),
onSurfaceVariant = Color(0xFFBDAE93),
surfaceTint = Color(0xFFFE8019),
surfaceContainerLowest = Color(0xFF1D2021),
surfaceContainerLow = Color(0xFF282828),
surfaceContainer = Color(0xFF32302F),
surfaceContainerHigh = Color(0xFF3C3836),
surfaceContainerHighest = Color(0xFF504945),
inverseSurface = Color(0xFFFBF1C7),
inverseOnSurface = Color(0xFF282828),
error = Color(0xFFFB4934),
onError = Color(0xFF282828),
errorContainer = Color(0xFFCC241D),
onErrorContainer = Color(0xFFFBF1C7),
outline = Color(0xFF665C54),
outlineVariant = Color(0xFF504945),
scrim = Color(0xFF1D2021),
)
// ---------------------------------------------------------------------
// Gruvbox Dark (medium contrast) — named swatches
// ---------------------------------------------------------------------
/** Gruvbox color scheme (light). */
val GruvboxLightColorScheme =
lightColorScheme(
primary = Color(0xFFD65D0E),
onPrimary = Color(0xFFFBF1C7),
primaryContainer = Color(0xFFFE8019),
onPrimaryContainer = Color(0xFF282828),
inversePrimary = Color(0xFFFE8019),
secondary = Color(0xFF458588),
onSecondary = Color(0xFFFBF1C7),
secondaryContainer = Color(0xFFEBDBB2),
onSecondaryContainer = Color(0xFF3C3836),
tertiary = Color(0xFF689D6A),
onTertiary = Color(0xFFFBF1C7),
tertiaryContainer = Color(0xFFD5C4A1),
onTertiaryContainer = Color(0xFF282828),
background = Color(0xFFFBF1C7),
onBackground = Color(0xFF282828),
surface = Color(0xFFF2E5BC),
onSurface = Color(0xFF282828),
surfaceVariant = Color(0xFFEBDBB2),
onSurfaceVariant = Color(0xFF7C6F64),
surfaceTint = Color(0xFFD65D0E),
surfaceContainerLowest = Color(0xFFFFFFFF),
surfaceContainerLow = Color(0xFFF9F5D7),
surfaceContainer = Color(0xFFFBF1C7),
surfaceContainerHigh = Color(0xFFF2E5BC),
surfaceContainerHighest = Color(0xFFEBDBB2),
inverseSurface = Color(0xFF282828),
inverseOnSurface = Color(0xFFFBF1C7),
error = Color(0xFFCC241D),
onError = Color(0xFFFBF1C7),
errorContainer = Color(0xFFF9F5D7),
onErrorContainer = Color(0xFF9D0006),
outline = Color(0xFFA89984),
outlineVariant = Color(0xFFD5C4A1),
scrim = Color(0xFF3C3836),
)
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
/** Gruvbox status colors (dark). */
val GruvboxDarkStatusColors =
HermesStatusColors(
success = Color(0xFFB8BB26),
successContainer = Color(0xFF32302F),
onSuccess = Color(0xFF282828),
warning = Color(0xFFFABD2F),
warningContainer = Color(0xFF3C3836),
onWarning = Color(0xFF282828),
error = Color(0xFFFB4934),
errorContainer = Color(0xFF282828),
onError = Color(0xFF282828),
info = Color(0xFF83A598),
infoContainer = Color(0xFF3C3836),
onInfo = Color(0xFF282828),
)
// "Bright" accents — the variants gruvbox uses against a dark background.
private val GruvboxBrightRed = Color(0xFFFB4934)
private val GruvboxBrightGreen = Color(0xFFB8BB26)
private val GruvboxBrightYellow = Color(0xFFFABD2F)
private val GruvboxBrightBlue = Color(0xFF83A598)
private val GruvboxBrightPurple = Color(0xFFD3869B)
private val GruvboxBrightOrange = Color(0xFFFE8019)
/** Gruvbox status colors (light). */
val GruvboxLightStatusColors =
HermesStatusColors(
success = Color(0xFF98971A),
successContainer = Color(0xFFEBDBB2),
onSuccess = Color(0xFFEBDBB2),
warning = Color(0xFFD79921),
warningContainer = Color(0xFFEBDBB2),
onWarning = Color(0xFFEBDBB2),
error = Color(0xFFCC241D),
errorContainer = Color(0xFFEBDBB2),
onError = Color(0xFFEBDBB2),
info = Color(0xFF458588),
infoContainer = Color(0xFFEBDBB2),
onInfo = Color(0xFFEBDBB2),
// ---------------------------------------------------------------------
// Gruvbox Light (medium contrast) — named swatches
// ---------------------------------------------------------------------
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)
// "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)
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. */
val GruvboxTheme =
buildTheme(
dark =
PaletteColors(
primary = GruvboxBrightOrange,
onPrimary = GruvboxDarkBg0,
primaryContainer = GruvboxDarkBg4,
onPrimaryContainer = GruvboxDarkFg1,
secondary = GruvboxBrightBlue,
onSecondary = GruvboxDarkBg0,
secondaryContainer = GruvboxDarkBg3,
onSecondaryContainer = GruvboxDarkFg2,
tertiary = GruvboxBrightPurple,
onTertiary = GruvboxDarkBg0,
tertiaryContainer = GruvboxDarkBg4,
onTertiaryContainer = GruvboxDarkFg1,
background = GruvboxDarkBg0,
onBackground = GruvboxDarkFg1,
surface = GruvboxDarkBg0,
onSurface = GruvboxDarkFg1,
surfaceVariant = GruvboxDarkBg1,
onSurfaceVariant = GruvboxDarkFg3,
surfaceContainerLowest = GruvboxDarkBg0,
surfaceContainerLow = GruvboxDarkBg1,
surfaceContainer = GruvboxDarkBg2,
surfaceContainerHigh = GruvboxDarkBg3,
surfaceContainerHighest = GruvboxDarkBg4,
inverseSurface = GruvboxDarkFg1,
inverseOnSurface = GruvboxDarkBg0,
inversePrimary = GruvboxFadedOrange,
outline = GruvboxGray,
outlineVariant = GruvboxDarkBg3,
scrim = GruvboxDarkBg0,
status =
HermesStatusColors(
success = GruvboxBrightGreen,
successContainer = GruvboxDarkBg3,
onSuccess = GruvboxDarkBg0,
warning = GruvboxBrightYellow,
warningContainer = GruvboxDarkBg3,
onWarning = GruvboxDarkBg0,
error = GruvboxBrightRed,
errorContainer = GruvboxDarkBg3,
onError = GruvboxDarkBg0,
onErrorContainer = GruvboxDarkFg1,
info = GruvboxBrightBlue,
infoContainer = GruvboxDarkBg3,
onInfo = GruvboxDarkBg0,
),
),
light =
PaletteColors(
primary = GruvboxFadedOrange,
onPrimary = GruvboxLightBg0,
primaryContainer = GruvboxLightBg4,
onPrimaryContainer = GruvboxLightFg1,
secondary = GruvboxFadedBlue,
onSecondary = GruvboxLightBg0,
secondaryContainer = GruvboxLightBg3,
onSecondaryContainer = GruvboxLightFg2,
tertiary = GruvboxFadedPurple,
onTertiary = GruvboxLightBg0,
tertiaryContainer = GruvboxLightBg4,
onTertiaryContainer = GruvboxLightFg1,
background = GruvboxLightBg0,
onBackground = GruvboxLightFg1,
surface = GruvboxLightBg0,
onSurface = GruvboxLightFg1,
surfaceVariant = GruvboxLightBg1,
onSurfaceVariant = GruvboxLightFg3,
surfaceContainerLowest = GruvboxLightBg0,
surfaceContainerLow = GruvboxLightBg1,
surfaceContainer = GruvboxLightBg2,
surfaceContainerHigh = GruvboxLightBg3,
surfaceContainerHighest = GruvboxLightBg4,
inverseSurface = GruvboxLightFg1,
inverseOnSurface = GruvboxLightBg0,
inversePrimary = GruvboxBrightOrange,
outline = GruvboxGray,
outlineVariant = GruvboxLightBg3,
scrim = GruvboxLightBg0,
status =
HermesStatusColors(
success = GruvboxFadedGreen,
successContainer = GruvboxLightBg3,
onSuccess = GruvboxLightBg0,
warning = GruvboxFadedYellow,
warningContainer = GruvboxLightBg3,
onWarning = GruvboxLightBg0,
error = GruvboxFadedRed,
errorContainer = GruvboxLightBg3,
onError = GruvboxLightBg0,
onErrorContainer = GruvboxFadedRed,
info = GruvboxFadedBlue,
infoContainer = GruvboxLightBg3,
onInfo = GruvboxLightBg0,
),
),
)
@@ -1,118 +1,150 @@
package com.m57.hermescontrol.theme.presets
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.ui.graphics.Color
import com.m57.hermescontrol.theme.HermesStatusColors
import com.m57.hermescontrol.theme.PaletteColors
import com.m57.hermescontrol.theme.buildTheme
/** Monochrome color scheme (dark). */
val MonochromeDarkColorScheme =
darkColorScheme(
primary = Color.White,
onPrimary = Color.Black,
primaryContainer = Color(0xFF404040),
onPrimaryContainer = Color.White,
inversePrimary = Color(0xFF121212),
secondary = Color(0xFFB0B0B0),
onSecondary = Color.Black,
secondaryContainer = Color(0xFF222222),
onSecondaryContainer = Color(0xFFE0E0E0),
tertiary = Color(0xFFD0D0D0),
onTertiary = Color.Black,
tertiaryContainer = Color(0xFF333333),
onTertiaryContainer = Color(0xFFE0E0E0),
background = Color(0xFF121212),
onBackground = Color(0xFFE0E0E0),
surface = Color(0xFF1E1E1E),
onSurface = Color(0xFFE0E0E0),
surfaceVariant = Color(0xFF2B2B2B),
onSurfaceVariant = Color(0xFFB0B0B0),
surfaceTint = Color.White,
surfaceContainerLowest = Color(0xFF0F0F0F),
surfaceContainerLow = Color(0xFF1A1A1A),
surfaceContainer = Color(0xFF1E1E1E),
surfaceContainerHigh = Color(0xFF252525),
surfaceContainerHighest = Color(0xFF2C2C2C),
inverseSurface = Color(0xFFE0E0E0),
inverseOnSurface = Color(0xFF121212),
error = Color(0xFFE0E0E0),
onError = Color.Black,
errorContainer = Color(0xFF404040),
onErrorContainer = Color(0xFFE0E0E0),
outline = Color(0xFF666666),
outlineVariant = Color(0xFF444444),
scrim = Color.Black,
)
// ---------------------------------------------------------------------
// Monochrome Dark — every swatch is R=G=B; roles are distinguished
// purely by lightness, not hue.
// ---------------------------------------------------------------------
/** Monochrome color scheme (light). */
val MonochromeLightColorScheme =
lightColorScheme(
primary = Color.Black,
onPrimary = Color.White,
primaryContainer = Color(0xFFC8C8C8),
onPrimaryContainer = Color.Black,
inversePrimary = Color(0xFFE0E0E0),
secondary = Color(0xFF4A4A4A),
onSecondary = Color.White,
secondaryContainer = Color(0xFFF0F0F0),
onSecondaryContainer = Color(0xFF111111),
tertiary = Color(0xFF333333),
onTertiary = Color.White,
tertiaryContainer = Color(0xFFE8E8E8),
onTertiaryContainer = Color(0xFF111111),
background = Color(0xFFFFFFFF),
onBackground = Color(0xFF111111),
surface = Color(0xFFF5F5F5),
onSurface = Color(0xFF111111),
surfaceVariant = Color(0xFFE0E0E0),
onSurfaceVariant = Color(0xFF4A4A4A),
surfaceTint = Color.Black,
surfaceContainerLowest = Color(0xFFFFFFFF),
surfaceContainerLow = Color(0xFFF8F8F8),
surfaceContainer = Color(0xFFF5F5F5),
surfaceContainerHigh = Color(0xFFEEEEEE),
surfaceContainerHighest = Color(0xFFE3E3E3),
inverseSurface = Color(0xFF111111),
inverseOnSurface = Color(0xFFE0E0E0),
error = Color(0xFF111111),
onError = Color.White,
errorContainer = Color(0xFFD6D6D6),
onErrorContainer = Color(0xFF111111),
outline = Color(0xFF888888),
outlineVariant = Color(0xFFCCCCCC),
scrim = Color.Black,
)
private val MonoDarkFloor = Color(0xFF0A0A0A) // surfaceContainerLowest
private val MonoDarkBg = Color(0xFF121212) // background, surface, containerLow, all "on*" ink
private val MonoDarkContainer = Color(0xFF1E1E1E) // surfaceContainer
private val MonoDarkContainerHigh = Color(0xFF2A2A2A) // surfaceContainerHigh, surfaceVariant, status containers
private val MonoDarkContainerHighest = Color(0xFF363636) // surfaceContainerHighest, primaryContainer
private val MonoDarkOutline = Color(0xFF808080)
private val MonoDarkOutlineVariant = Color(0xFF404040)
private val MonoDarkTextDim = Color(0xFFB3B3B3) // onSurfaceVariant
private val MonoDarkTextBright = Color(0xFFF2F2F2) // onBackground/onSurface/inverseSurface/primary/error
private val MonoDarkSecondary = Color(0xFFCCCCCC) // secondary accent + warning
private val MonoDarkTertiary = Color(0xFF999999) // tertiary accent + success
private val MonoDarkInfo = Color(0xFF808080) // info — same value as outline, distinct role
/** Monochrome status colors (dark). */
val MonochromeDarkStatusColors =
HermesStatusColors(
success = Color(0xFFE0E0E0),
successContainer = Color(0xFF2A2A2A),
onSuccess = Color(0xFF121212),
warning = Color(0xFFCCCCCC),
warningContainer = Color(0xFF333333),
onWarning = Color(0xFF121212),
error = Color(0xFFFFFFFF),
errorContainer = Color(0xFF404040),
onError = Color(0xFF121212),
info = Color(0xFFB0B0B0),
infoContainer = Color(0xFF242424),
onInfo = Color(0xFF121212),
)
// ---------------------------------------------------------------------
// Monochrome Light — the same ramp, inverted
// ---------------------------------------------------------------------
/** Monochrome status colors (light). */
val MonochromeLightStatusColors =
HermesStatusColors(
success = Color(0xFF222222),
successContainer = Color(0xFFE5E5E5),
onSuccess = Color(0xFFFFFFFF),
warning = Color(0xFF444444),
warningContainer = Color(0xFFE0E0E0),
onWarning = Color(0xFFFFFFFF),
error = Color(0xFF000000),
errorContainer = Color(0xFFD6D6D6),
onError = Color(0xFFFFFFFF),
info = Color(0xFF555555),
infoContainer = Color(0xFFEAEAEA),
onInfo = Color(0xFFFFFFFF),
private val MonoLightCeiling = Color(0xFFFFFFFF) // surfaceContainerLowest
private val MonoLightBg = Color(0xFFFAFAFA) // background, surface, containerLow, all "on*" ink
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
private val MonoLightSecondary = Color(0xFF333333) // secondary accent + warning
private val MonoLightTertiary = Color(0xFF666666) // tertiary accent + success
private val MonoLightInfo = Color(0xFF808080) // info — same value as outline, distinct role
/**
* Monochrome theme — pure grayscale, no hue in any slot including status
* colors. `success`/`warning`/`error`/`info` are separated only by
* lightness (darkest/most emphasized = error, lightest/most muted = info),
* so anything that leans on this theme for status meaning should not rely
* on color alone — pair it with an icon, label, or weight change. This is
* the one real accessibility trade-off of going fully monochrome; every
* other preset keeps status colors hued specifically to avoid it.
*/
val MonochromeTheme =
buildTheme(
dark =
PaletteColors(
primary = MonoDarkTextBright,
onPrimary = MonoDarkBg,
primaryContainer = MonoDarkContainerHighest,
onPrimaryContainer = MonoDarkTextBright,
secondary = MonoDarkSecondary,
onSecondary = MonoDarkBg,
secondaryContainer = MonoDarkContainerHigh,
onSecondaryContainer = MonoDarkSecondary,
tertiary = MonoDarkTertiary,
onTertiary = MonoDarkBg,
tertiaryContainer = MonoDarkContainerHigh,
onTertiaryContainer = MonoDarkTertiary,
background = MonoDarkBg,
onBackground = MonoDarkTextBright,
surface = MonoDarkBg,
onSurface = MonoDarkTextBright,
surfaceVariant = MonoDarkContainerHigh,
onSurfaceVariant = MonoDarkTextDim,
surfaceContainerLowest = MonoDarkFloor,
surfaceContainerLow = MonoDarkBg,
surfaceContainer = MonoDarkContainer,
surfaceContainerHigh = MonoDarkContainerHigh,
surfaceContainerHighest = MonoDarkContainerHighest,
inverseSurface = MonoDarkTextBright,
inverseOnSurface = MonoDarkBg,
inversePrimary = MonoLightTextBright,
outline = MonoDarkOutline,
outlineVariant = MonoDarkOutlineVariant,
scrim = Color.Black,
status =
HermesStatusColors(
success = MonoDarkTertiary,
successContainer = MonoDarkContainerHigh,
onSuccess = MonoDarkBg,
warning = MonoDarkSecondary,
warningContainer = MonoDarkContainerHigh,
onWarning = MonoDarkBg,
error = MonoDarkTextBright,
errorContainer = MonoDarkContainerHigh,
onError = MonoDarkBg,
onErrorContainer = MonoDarkTextBright,
info = MonoDarkInfo,
infoContainer = MonoDarkContainerHigh,
onInfo = MonoDarkBg,
),
),
light =
PaletteColors(
primary = MonoLightTextBright,
onPrimary = MonoLightBg,
primaryContainer = MonoLightContainerHighest,
onPrimaryContainer = MonoLightTextBright,
secondary = MonoLightSecondary,
onSecondary = MonoLightBg,
secondaryContainer = MonoLightContainerHigh,
onSecondaryContainer = MonoLightSecondary,
tertiary = MonoLightTertiary,
onTertiary = MonoLightBg,
tertiaryContainer = MonoLightContainerHigh,
onTertiaryContainer = MonoLightTertiary,
background = MonoLightBg,
onBackground = MonoLightTextBright,
surface = MonoLightBg,
onSurface = MonoLightTextBright,
surfaceVariant = MonoLightContainerHigh,
onSurfaceVariant = MonoLightTextDim,
surfaceContainerLowest = MonoLightCeiling,
surfaceContainerLow = MonoLightBg,
surfaceContainer = MonoLightContainer,
surfaceContainerHigh = MonoLightContainerHigh,
surfaceContainerHighest = MonoLightContainerHighest,
inverseSurface = MonoLightTextBright,
inverseOnSurface = MonoLightBg,
inversePrimary = MonoDarkTextBright,
outline = MonoLightOutline,
outlineVariant = MonoLightOutlineVariant,
scrim = Color.Black,
status =
HermesStatusColors(
success = MonoLightTertiary,
successContainer = MonoLightContainerHigh,
onSuccess = MonoLightBg,
warning = MonoLightSecondary,
warningContainer = MonoLightContainerHigh,
onWarning = MonoLightBg,
error = MonoLightTextBright,
errorContainer = MonoLightContainerHigh,
onError = MonoLightBg,
onErrorContainer = MonoLightTextBright,
info = MonoLightInfo,
infoContainer = MonoLightContainerHigh,
onInfo = MonoLightBg,
),
),
)
@@ -1,118 +0,0 @@
package com.m57.hermescontrol.theme.presets
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.ui.graphics.Color
import com.m57.hermescontrol.theme.HermesStatusColors
/** Neon Noir color scheme (dark). */
val NeonNoirDarkColorScheme =
darkColorScheme(
primary = Color(0xFFFF6BF4),
onPrimary = Color(0xFF1A001A),
primaryContainer = Color(0xFF3A003A),
onPrimaryContainer = Color(0xFFFFCCF2),
inversePrimary = Color(0xFFC440A8),
secondary = Color(0xFFA78BFA),
onSecondary = Color(0xFF0F0B1A),
secondaryContainer = Color(0xFF2B1F59),
onSecondaryContainer = Color(0xFFD9CCFF),
tertiary = Color(0xFF60F0FF),
onTertiary = Color(0xFF003B40),
tertiaryContainer = Color(0xFF004F59),
onTertiaryContainer = Color(0xFFBFFAFF),
background = Color(0xFF0F0B1A),
onBackground = Color(0xFFE8E0F0),
surface = Color(0xFF1A1530),
onSurface = Color(0xFFE8E0F0),
surfaceVariant = Color(0xFF2B2540),
onSurfaceVariant = Color(0xFF9A90B0),
surfaceTint = Color(0xFFFF6BF4),
surfaceContainerLowest = Color(0xFF0A0712),
surfaceContainerLow = Color(0xFF140F22),
surfaceContainer = Color(0xFF1A1530),
surfaceContainerHigh = Color(0xFF252040),
surfaceContainerHighest = Color(0xFF2F2A50),
inverseSurface = Color(0xFFE8E0F0),
inverseOnSurface = Color(0xFF0F0B1A),
error = Color(0xFFFF3D68),
onError = Color.White,
errorContainer = Color(0xFF3A0A14),
onErrorContainer = Color(0xFFFFB3C0),
outline = Color(0xFF3A3060),
outlineVariant = Color(0xFF2E2545),
scrim = Color(0xFF000000),
)
/** Neon Noir color scheme (light). */
val NeonNoirLightColorScheme =
lightColorScheme(
primary = Color(0xFFC440A8),
onPrimary = Color.White,
primaryContainer = Color(0xFFFFD9F2),
onPrimaryContainer = Color(0xFF3A003A),
inversePrimary = Color(0xFFFF6BF4),
secondary = Color(0xFF7C4DFF),
onSecondary = Color.White,
secondaryContainer = Color(0xFFEDE0FF),
onSecondaryContainer = Color(0xFF1E0066),
tertiary = Color(0xFF0098A6),
onTertiary = Color.White,
tertiaryContainer = Color(0xFFBFFAFF),
onTertiaryContainer = Color(0xFF003B40),
background = Color(0xFFF5F0FA),
onBackground = Color(0xFF0F0B1A),
surface = Color.White,
onSurface = Color(0xFF0F0B1A),
surfaceVariant = Color(0xFFEDE6F5),
onSurfaceVariant = Color(0xFF7A7090),
surfaceTint = Color(0xFFC440A8),
surfaceContainerLowest = Color(0xFFFFFFFF),
surfaceContainerLow = Color(0xFFFAF5FF),
surfaceContainer = Color(0xFFFDF8FF),
surfaceContainerHigh = Color(0xFFF0EDF5),
surfaceContainerHighest = Color(0xFFE6E0ED),
inverseSurface = Color(0xFF0F0B1A),
inverseOnSurface = Color(0xFFE8E0F0),
error = Color(0xFFB3261E),
onError = Color.White,
errorContainer = Color(0xFFF9DEDC),
onErrorContainer = Color(0xFF410E0B),
outline = Color(0xFFBFB8CC),
outlineVariant = Color(0xFFD9D2E3),
scrim = Color(0xFF000000),
)
/** Neon Noir status colors (dark). */
val NeonNoirDarkStatusColors =
HermesStatusColors(
success = Color(0xFF6FEA8B),
successContainer = Color(0xFF0A3A14),
onSuccess = Color(0xFF0F0B1A),
warning = Color(0xFFFFB347),
warningContainer = Color(0xFF3A2000),
onWarning = Color(0xFF0F0B1A),
error = Color(0xFFFF3D68),
errorContainer = Color(0xFF3A0A14),
onError = Color(0xFF0F0B1A),
info = Color(0xFF60F0FF),
infoContainer = Color(0xFF003B40),
onInfo = Color(0xFF0F0B1A),
)
/** Neon Noir status colors (light). */
val NeonNoirLightStatusColors =
HermesStatusColors(
success = Color(0xFF1B873A),
successContainer = Color(0xFFD7F5E0),
onSuccess = Color(0xFFFDF8FF),
warning = Color(0xFFB87800),
warningContainer = Color(0xFFFFEAB3),
onWarning = Color(0xFFFDF8FF),
error = Color(0xFFB3261E),
errorContainer = Color(0xFFF9DEDC),
onError = Color(0xFFFDF8FF),
info = Color(0xFF2E6FBD),
infoContainer = Color(0xFFD4E7FF),
onInfo = Color(0xFFFDF8FF),
)
@@ -0,0 +1,144 @@
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
// ---------------------------------------------------------------------
// Nord — named swatches, official numbering (nord0–nord15).
// Polar Night (0–3, dark neutrals) and Snow Storm (4–6, light neutrals)
// 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.
// ---------------------------------------------------------------------
private val Nord0 = Color(0xFF2E3440) // Polar Night — darkest
private val Nord1 = Color(0xFF3B4252)
private val Nord2 = Color(0xFF434C5E)
private val Nord3 = Color(0xFF4C566A)
private val Nord4 = Color(0xFFD8DEE9) // Snow Storm
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
private val Nord13 = Color(0xFFEBCB8B) // Aurora yellow — warning
private val Nord14 = Color(0xFFA3BE8C) // Aurora green — success
private val Nord15 = Color(0xFFB48EAD) // Aurora purple — tertiary
/**
* Nord theme.
*
* Nord only ships 4 Polar Night tones and 3 Snow Storm tones, one tone
* short of the 5-tier surface-container ladder, so the two outermost
* 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
* the Catppuccin/Gruvbox presets) would fail contrast against colors like
* Aurora yellow, which stays light even without a separate light-mode
* variant.
*/
val NordTheme =
buildTheme(
dark =
PaletteColors(
primary = Nord8,
onPrimary = Nord0,
primaryContainer = Nord3,
onPrimaryContainer = Nord6,
secondary = Nord9,
onSecondary = Nord0,
secondaryContainer = Nord2,
onSecondaryContainer = Nord4,
tertiary = Nord15,
onTertiary = Nord0,
tertiaryContainer = Nord3,
onTertiaryContainer = Nord6,
background = Nord0,
onBackground = Nord6,
surface = Nord0,
onSurface = Nord6,
surfaceVariant = Nord1,
onSurfaceVariant = Nord4,
surfaceContainerLowest = Nord0,
surfaceContainerLow = Nord1,
surfaceContainer = Nord2,
surfaceContainerHigh = Nord3,
surfaceContainerHighest = Nord3,
inverseSurface = Nord6,
inverseOnSurface = Nord0,
inversePrimary = Nord8,
outline = Nord3,
outlineVariant = Nord2,
scrim = Nord0,
status =
HermesStatusColors(
success = Nord14,
successContainer = Nord2,
onSuccess = Nord0,
warning = Nord13,
warningContainer = Nord2,
onWarning = Nord0,
error = Nord11,
errorContainer = Nord2,
onError = Nord0,
onErrorContainer = Nord6,
info = Nord9,
infoContainer = Nord2,
onInfo = Nord0,
),
),
light =
PaletteColors(
primary = Nord8,
onPrimary = Nord0,
primaryContainer = Nord4,
onPrimaryContainer = Nord0,
secondary = Nord9,
onSecondary = Nord0,
secondaryContainer = Nord5,
onSecondaryContainer = Nord1,
tertiary = Nord15,
onTertiary = Nord0,
tertiaryContainer = Nord4,
onTertiaryContainer = Nord0,
background = Nord6,
onBackground = Nord0,
surface = Nord6,
onSurface = Nord0,
surfaceVariant = Nord5,
onSurfaceVariant = Nord2,
surfaceContainerLowest = Nord6,
surfaceContainerLow = Nord5,
surfaceContainer = Nord5,
surfaceContainerHigh = Nord4,
surfaceContainerHighest = Nord4,
inverseSurface = Nord0,
inverseOnSurface = Nord6,
inversePrimary = Nord8,
outline = Nord2,
outlineVariant = Nord4,
scrim = Nord6,
status =
HermesStatusColors(
success = Nord14,
successContainer = Nord5,
onSuccess = Nord0,
warning = Nord13,
warningContainer = Nord5,
onWarning = Nord0,
error = Nord11,
errorContainer = Nord5,
onError = Nord0,
onErrorContainer = Nord11,
info = Nord9,
infoContainer = Nord5,
onInfo = Nord0,
),
),
)
@@ -445,7 +445,7 @@ private fun SelfImprovementReviewCard(
val isSkill =
cleanText.contains("skill", ignoreCase = true) ||
cleanText.contains("SKILL.md", ignoreCase = true)
val icon = if (isSkill) "⚡" else "🧠"
val icon = if (isSkill) "🛠" else "🧠"
val title =
if (isSkill) {
"Self-Improvement Review • Skill Patched"
@@ -187,7 +187,7 @@ private fun TodoInspectionCard(todo: TodoItem) {
val statusSymbol =
when {
todo.isCompleted -> "✅"
todo.isInProgress -> "⚡"
todo.isInProgress -> "🔄"
todo.isCancelled -> "❌"
else -> "⭕"
}
@@ -225,7 +225,7 @@ private fun InspectionItemCard(indicator: SubagentIndicator) {
when {
indicator.isComplete -> "✅"
indicator.isFailed -> "❌"
else -> "⚡"
else -> "🔄"
}
Text(text = statusSymbol, fontSize = 16.sp)
Spacer(modifier = Modifier.width(8.dp))
@@ -344,7 +344,7 @@ private fun SelfImprovementSection(graph: LearningGraphResponse?) {
modifier = Modifier.padding(horizontal = 10.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
val icon = if (node.kind == "skill") "⚡" else "🧠"
val icon = if (node.kind == "skill") "🛠" else "🧠"
Text(text = icon, fontSize = 16.sp)
Spacer(modifier = Modifier.width(4.dp))
Column(modifier = Modifier.weight(1f)) {
@@ -129,7 +129,7 @@ internal fun AppearanceSection(
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.NEON_NOIR -> stringResource(R.string.theme_preset_neon_noir)
ThemePreset.NORD -> stringResource(R.string.theme_preset_nord)
},
)
}
@@ -173,9 +173,9 @@ internal fun AppearanceSection(
)
}
ThemePreset.NEON_NOIR -> {
ThemePreset.NORD -> {
stringResource(
R.string.theme_preset_neon_noir,
R.string.theme_preset_nord,
)
}
},