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:
1 parent
52e33a96b7
commit
ab42e19b7f
21 files changed
+1312
-862
No files matched your search
@@ -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
|
||||
|
||||
Reference in new issue
Block a user