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
+1308
-858
No files matched your search
@@ -200,8 +200,12 @@ breakdown, edge cases, and timeline of previous occurrences.
|
|||||||
### Theme
|
### Theme
|
||||||
|
|
||||||
`Theme.kt` uses a preset-based theme system with 6 built-in presets: Default,
|
`Theme.kt` uses a preset-based theme system with 6 built-in presets: Default,
|
||||||
Monochrome, Gruvbox, Catppuccin, AMOLED, and Neon Noir. Each preset provides both
|
Monochrome, Gruvbox, Catppuccin, AMOLED, and Nord. Each preset provides light
|
||||||
light and dark color schemes plus matching semantic status colors.
|
and dark color schemes plus matching semantic status colors (AMOLED is
|
||||||
|
dark-only — light mode falls back to the default theme).
|
||||||
|
Every preset is a `ThemePalette` template fill (`PaletteTemplate.kt`): raw
|
||||||
|
`PaletteColors` + `HermesStatusColors` mapped into the Material 3
|
||||||
|
`ColorScheme` slots, with the error slots derived from the status set.
|
||||||
|
|
||||||
Dynamic color (Material You on API 31+) is controlled by a `useDynamicColors`
|
Dynamic color (Material You on API 31+) is controlled by a `useDynamicColors`
|
||||||
parameter on `HermesControlTheme`, not a compile-time constant. When dynamic
|
parameter on `HermesControlTheme`, not a compile-time constant. When dynamic
|
||||||
@@ -288,7 +292,7 @@ com.m57.hermescontrol/
|
|||||||
│ └── ws/ HermesWsClient, JSON-RPC models, WsEvent, BillingRepository
|
│ └── ws/ HermesWsClient, JSON-RPC models, WsEvent, BillingRepository
|
||||||
├── notification/ ChatNotificationService, NotificationReplyReceiver
|
├── notification/ ChatNotificationService, NotificationReplyReceiver
|
||||||
├── theme/ Color, Theme, Motion, Spacing, Shapes, Type, HermesStatusColors
|
├── theme/ Color, Theme, Motion, Spacing, Shapes, Type, HermesStatusColors
|
||||||
│ └── presets/ Default, Monochrome, Gruvbox, Catppuccin, AMOLED, NeonNoir
|
│ └── presets/ Default, Monochrome, Gruvbox, Catppuccin, AMOLED, Nord
|
||||||
├── ui/
|
├── ui/
|
||||||
│ ├── common/ HermesScaffold, StateViews, SharedComponents, DetailDialog, DetailRows
|
│ ├── common/ HermesScaffold, StateViews, SharedComponents, DetailDialog, DetailRows
|
||||||
│ └── 28 feature packages (achievements, analytics, authlogin, billing, channels,
|
│ └── 28 feature packages (achievements, analytics, authlogin, billing, channels,
|
||||||
|
|||||||
@@ -260,7 +260,9 @@ fun MainNavigation(sessionId: String? = null) {
|
|||||||
NavigationDrawerItemDefaults.colors(
|
NavigationDrawerItemDefaults.colors(
|
||||||
selectedContainerColor = MaterialTheme.colorScheme.primaryContainer,
|
selectedContainerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||||
selectedTextColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
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
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
// Hermes Mobile brand palette — v3 (Material You rewrite).
|
// Shared color tokens — only what code OUTSIDE the theme presets consumes.
|
||||||
//
|
// The preset palettes themselves are self-contained template fills in
|
||||||
// On Android 12+ (API 31+) the app uses dynamic colour derived from the user's
|
// presets/ (see PaletteTemplate.kt); they don't import from here.
|
||||||
// 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) ──────────────────────
|
|
||||||
|
|
||||||
|
// Semantic status colors (always brand-defined).
|
||||||
val StatusGreen = Color(0xFF3DDC84)
|
val StatusGreen = Color(0xFF3DDC84)
|
||||||
val StatusGreenContainer = Color(0xFF143A23)
|
val StatusGreenContainer = Color(0xFF143A23)
|
||||||
val StatusRed = Color(0xFFFF5C5C)
|
val StatusRed = Color(0xFFFF5C5C)
|
||||||
@@ -75,31 +19,25 @@ val StatusGrey = Color(0xFF9E9E9E)
|
|||||||
val StatusGreyDark = Color(0xFF1A1A24)
|
val StatusGreyDark = Color(0xFF1A1A24)
|
||||||
val StatusGreyLight = Color(0xFFF5F5F5)
|
val StatusGreyLight = Color(0xFFF5F5F5)
|
||||||
|
|
||||||
// ── Chat-specific colours (default theme fallback tokens) ─────────────────
|
// Chat-specific fallback tokens (default theme surface text colors).
|
||||||
|
val DarkOnSurface = Color(0xFFE8E6EE)
|
||||||
val AssistantBubble = Color(0xFF1C1C26)
|
val LightOnSurface = Color(0xFF1A1A24)
|
||||||
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).
|
|
||||||
|
|
||||||
|
// 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 CodeKeyword = Color(0xFF569CD6) // blue — for val, fun, class, etc.
|
||||||
val CodeString = Color(0xFFCE9178) // orange — for "string literals"
|
val CodeString = Color(0xFFCE9178) // orange — for "string literals"
|
||||||
val CodeComment = Color(0xFF6A9955) // green — for // comments
|
val CodeComment = Color(0xFF6A9955) // green — for // comments
|
||||||
val CodeNumber = Color(0xFFB5CEA8) // light green — for 42, 0xFF
|
val CodeNumber = Color(0xFFB5CEA8) // light green — for 42, 0xFF
|
||||||
val CodePunctuation = Color(0xFFD4D4D4) // gray — for {, (, ;, etc.
|
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 CodeTerminalBg = Color(0xFF1E1E1E)
|
||||||
val CodeTerminalBorder = Color(0xFF333333)
|
val CodeTerminalBorder = Color(0xFF333333)
|
||||||
val CodeTerminalText = Color(0xFFD4D4D4)
|
val CodeTerminalText = Color(0xFFD4D4D4)
|
||||||
val CodeTerminalMuted = Color(0xFF808080)
|
val CodeTerminalMuted = Color(0xFF808080)
|
||||||
|
|
||||||
// ── Diff viewer tokens (issue #738) ──────────────────────────────────
|
// Diff viewer tokens (issue #738).
|
||||||
val CodeDiffAddBg = Color(0x263DDC84)
|
val CodeDiffAddBg = Color(0x263DDC84)
|
||||||
val CodeDiffAddText = Color(0xFF3DDC84)
|
val CodeDiffAddText = Color(0xFF3DDC84)
|
||||||
val CodeDiffDeleteBg = Color(0x26FF5C5C)
|
val CodeDiffDeleteBg = Color(0x26FF5C5C)
|
||||||
@@ -107,9 +45,8 @@ val CodeDiffDeleteText = Color(0xFFFF5C5C)
|
|||||||
val CodeDiffHunkBg = Color(0x264DA8FF)
|
val CodeDiffHunkBg = Color(0x264DA8FF)
|
||||||
val CodeDiffHunkText = Color(0xFF4DA8FF)
|
val CodeDiffHunkText = Color(0xFF4DA8FF)
|
||||||
|
|
||||||
// ── Custom vector icons ─────────────────────────────────────────────
|
// Custom vector icons — default fill for hand-declared ImageVector icons
|
||||||
// Default fill for hand-declared ImageVector icons (e.g. NeurologyIcon).
|
// (e.g. NeurologyIcon). Mirrors Material's own icon set: vectors are drawn
|
||||||
// Mirrors Material's own icon set: vectors are drawn opaque black and
|
// opaque black and Icon()'s tint (LocalContentColor) replaces the RGB at
|
||||||
// Icon()'s tint (LocalContentColor) replaces the RGB at draw time — only
|
// draw time — only the alpha channel survives, so this must stay opaque.
|
||||||
// the alpha channel survives, so this must stay opaque.
|
|
||||||
val IconDefaultFill = Color.Black
|
val IconDefaultFill = Color.Black
|
||||||
@@ -24,6 +24,7 @@ data class HermesStatusColors(
|
|||||||
val error: Color,
|
val error: Color,
|
||||||
val errorContainer: Color,
|
val errorContainer: Color,
|
||||||
val onError: Color,
|
val onError: Color,
|
||||||
|
val onErrorContainer: Color = StatusGreyLight,
|
||||||
val info: Color,
|
val info: Color,
|
||||||
val infoContainer: Color,
|
val infoContainer: Color,
|
||||||
val onInfo: 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/
|
app/src/main/java/com/m57/hermescontrol/theme/
|
||||||
├── Theme.kt # dispatcher — resolves scheme + status colors per preset
|
├── Theme.kt # dispatcher — preset lookup + mode fallback
|
||||||
├── Color.kt # brand design tokens (DarkSurface, HermesPurple, StatusGreen, …)
|
├── 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*)
|
├── HermesStatusColors.kt # semantic status color model (success/warning/error/info + on*)
|
||||||
├── Type.kt / Shapes.kt # typography + shape tokens
|
├── Type.kt / Shapes.kt # typography + shape tokens
|
||||||
├── Spacing.kt / Motion.kt # spacing + motion tokens
|
├── Spacing.kt / Motion.kt # spacing + motion tokens
|
||||||
└── presets/ # one file per ThemePreset
|
└── presets/ # one file per ThemePreset — same skeleton everywhere
|
||||||
├── DefaultScheme.kt
|
├── DefaultScheme.kt
|
||||||
├── MonochromeScheme.kt
|
├── MonochromeScheme.kt
|
||||||
├── GruvboxScheme.kt
|
├── GruvboxScheme.kt
|
||||||
├── CatppuccinScheme.kt
|
├── CatppuccinScheme.kt
|
||||||
├── AmoledScheme.kt
|
├── AmoledScheme.kt # dark-only (ThemeMode.DARK_ONLY)
|
||||||
└── NeonNoirScheme.kt
|
└── 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:
|
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
|
||||||
| Token | Type | Purpose |
|
`PaletteColors` (the Material slots) + the semantic status set
|
||||||
|-------|------|---------|
|
(`HermesStatusColors`) and the app does the rest. No per-preset behavior, no
|
||||||
| `XxxDarkColorScheme` | `ColorScheme` | dark-mode Material 3 scheme |
|
aliases, no shared-token imports.
|
||||||
| `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:
|
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
val AmoledDarkStatusColors = DefaultDarkStatusColors
|
val MyTheme =
|
||||||
val AmoledLightStatusColors = DefaultLightStatusColors
|
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
|
## Preset conventions
|
||||||
val AmoledLightColorScheme = DefaultLightColorScheme
|
|
||||||
```
|
- **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
|
## The dispatcher
|
||||||
|
|
||||||
`Theme.kt` is a **pure lookup** — no special-casing. Given a `ThemePreset`
|
`Theme.kt` is a **pure lookup** — no special-casing. Given a `ThemePreset` and
|
||||||
and a dark flag, it returns the matching scheme/status from the preset file:
|
a dark flag, `themeFor()` returns the preset's `ThemePalette`, then the scheme /
|
||||||
|
status are read off it (with the mode fallback above):
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
private fun resolveColorScheme(preset: ThemePreset, darkTheme: Boolean) = when (preset) {
|
private fun themeFor(preset: ThemePreset): ThemePalette = when (preset) {
|
||||||
ThemePreset.DEFAULT -> if (darkTheme) DefaultDarkColorScheme else DefaultLightColorScheme
|
ThemePreset.DEFAULT -> DefaultTheme
|
||||||
ThemePreset.MONOCHROME -> if (darkTheme) MonochromeDarkColorScheme else MonochromeLightColorScheme
|
ThemePreset.MONOCHROME -> MonochromeTheme
|
||||||
// …one line per preset…
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun resolveStatusColors(preset: ThemePreset, darkTheme: Boolean) = when (preset) {
|
|
||||||
ThemePreset.DEFAULT -> if (darkTheme) DefaultDarkStatusColors else DefaultLightStatusColors
|
|
||||||
// …one line per preset…
|
// …one line per preset…
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Dynamic (Material You) color on API 31+ overrides the preset scheme when
|
Dynamic (Material You) color on API 31+ overrides the preset scheme when
|
||||||
`useDynamicColors = true`.
|
`useDynamicColors = true`. Semantic status colors are always resolved from the
|
||||||
|
active preset via `LocalHermesStatusColors`.
|
||||||
## 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.
|
|
||||||
|
|
||||||
## Adding a new theme
|
## Adding a new theme
|
||||||
|
|
||||||
1. **Create** `presets/<Name>Scheme.kt` exporting the 4 tokens:
|
1. **Create** `presets/<Name>Scheme.kt` as a template fill (see above) —
|
||||||
```kotlin
|
`buildTheme` for a full theme, `buildThemeDarkOnly` / `buildThemeLightOnly`
|
||||||
package com.m57.hermescontrol.theme.presets
|
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.
|
||||||
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.
|
|
||||||
3. **Add the enum entry** `MY_THEME` to `ThemePreset` in `Theme.kt`.
|
3. **Add the enum entry** `MY_THEME` to `ThemePreset` in `Theme.kt`.
|
||||||
4. **Wire the UI**: add the label + selection in
|
4. **Wire the UI**: add the label + selection in
|
||||||
`ui/settings/components/AppearanceSection.kt` (and any string resource).
|
`ui/settings/components/AppearanceSection.kt` (and any string resource).
|
||||||
5. **Verify**:
|
5. **Verify**:
|
||||||
```bash
|
```bash
|
||||||
./ktlint --format app/src/main/java/com/m57/hermescontrol/theme/presets/<Name>Scheme.kt
|
./gradlew ktlintCheck testDebugUnitTest
|
||||||
./gradlew compileDebugKotlin
|
|
||||||
```
|
```
|
||||||
|
`ThemePaletteTest` asserts >= 3:1 contrast on every shipped mode's error
|
||||||
|
slot pairs and the ThemeMode invariants — it must stay green.
|
||||||
|
|
||||||
## Conventions
|
## 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).
|
- Import order is ASCII-lexicographic (uppercase before lowercase).
|
||||||
- Every change goes through a PR — never push directly to `main`.
|
- 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 android.os.Build
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
|
import androidx.compose.material3.ColorScheme
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.dynamicDarkColorScheme
|
import androidx.compose.material3.dynamicDarkColorScheme
|
||||||
import androidx.compose.material3.dynamicLightColorScheme
|
import androidx.compose.material3.dynamicLightColorScheme
|
||||||
@@ -9,75 +10,64 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.compositionLocalOf
|
import androidx.compose.runtime.compositionLocalOf
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import com.m57.hermescontrol.theme.presets.AmoledDarkColorScheme
|
import com.m57.hermescontrol.theme.presets.AmoledTheme
|
||||||
import com.m57.hermescontrol.theme.presets.AmoledDarkStatusColors
|
import com.m57.hermescontrol.theme.presets.CatppuccinTheme
|
||||||
import com.m57.hermescontrol.theme.presets.AmoledLightColorScheme
|
import com.m57.hermescontrol.theme.presets.DefaultTheme
|
||||||
import com.m57.hermescontrol.theme.presets.AmoledLightStatusColors
|
import com.m57.hermescontrol.theme.presets.GruvboxTheme
|
||||||
import com.m57.hermescontrol.theme.presets.CatppuccinDarkColorScheme
|
import com.m57.hermescontrol.theme.presets.MonochromeTheme
|
||||||
import com.m57.hermescontrol.theme.presets.CatppuccinDarkStatusColors
|
import com.m57.hermescontrol.theme.presets.NordTheme
|
||||||
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 kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
enum class ThemePreference { SYSTEM, LIGHT, DARK }
|
enum class ThemePreference { SYSTEM, LIGHT, DARK }
|
||||||
|
|
||||||
@Serializable
|
@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 LocalThemePreference = compositionLocalOf { ThemePreference.SYSTEM }
|
||||||
val LocalThemePreset = compositionLocalOf { ThemePreset.DEFAULT }
|
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.
|
* Resolve the Material 3 [ColorScheme] for a preset + dark flag.
|
||||||
*
|
*
|
||||||
* Every preset exports both a dark and light scheme. AMOLED's light scheme is
|
* A preset that doesn't ship the requested mode (DARK_ONLY / LIGHT_ONLY —
|
||||||
* an explicit alias of the brand default light scheme.
|
* 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,
|
preset: ThemePreset,
|
||||||
darkTheme: Boolean,
|
darkTheme: Boolean,
|
||||||
) = when (preset) {
|
): ColorScheme {
|
||||||
ThemePreset.DEFAULT -> if (darkTheme) DefaultDarkColorScheme else DefaultLightColorScheme
|
val theme = themeFor(preset)
|
||||||
ThemePreset.MONOCHROME -> if (darkTheme) MonochromeDarkColorScheme else MonochromeLightColorScheme
|
// DefaultTheme is FULL — its scheme is never null for either mode.
|
||||||
ThemePreset.GRUVBOX -> if (darkTheme) GruvboxDarkColorScheme else GruvboxLightColorScheme
|
return theme.schemeFor(darkTheme) ?: requireNotNull(DefaultTheme.schemeFor(darkTheme))
|
||||||
ThemePreset.CATPPUCCIN -> if (darkTheme) CatppuccinDarkColorScheme else CatppuccinLightColorScheme
|
|
||||||
ThemePreset.AMOLED -> if (darkTheme) AmoledDarkColorScheme else AmoledLightColorScheme
|
|
||||||
ThemePreset.NEON_NOIR -> if (darkTheme) NeonNoirDarkColorScheme else NeonNoirLightColorScheme
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve the semantic status colors for a preset + dark flag.
|
* Resolve the semantic status colors for a preset + dark flag.
|
||||||
*
|
* Same fallback rule as the scheme.
|
||||||
* 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).
|
|
||||||
*/
|
*/
|
||||||
private fun resolveStatusColors(
|
internal fun resolveStatusColors(
|
||||||
preset: ThemePreset,
|
preset: ThemePreset,
|
||||||
darkTheme: Boolean,
|
darkTheme: Boolean,
|
||||||
) = when (preset) {
|
): HermesStatusColors {
|
||||||
ThemePreset.DEFAULT -> if (darkTheme) DefaultDarkStatusColors else DefaultLightStatusColors
|
val theme = themeFor(preset)
|
||||||
ThemePreset.MONOCHROME -> if (darkTheme) MonochromeDarkStatusColors else MonochromeLightStatusColors
|
// DefaultTheme is FULL — its status colors are never null for either mode.
|
||||||
ThemePreset.GRUVBOX -> if (darkTheme) GruvboxDarkStatusColors else GruvboxLightStatusColors
|
return theme.statusFor(darkTheme) ?: requireNotNull(DefaultTheme.statusFor(darkTheme))
|
||||||
ThemePreset.CATPPUCCIN -> if (darkTheme) CatppuccinDarkStatusColors else CatppuccinLightStatusColors
|
|
||||||
ThemePreset.AMOLED -> if (darkTheme) AmoledDarkStatusColors else AmoledLightStatusColors
|
|
||||||
ThemePreset.NEON_NOIR -> if (darkTheme) NeonNoirDarkStatusColors else NeonNoirLightStatusColors
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@@ -1,63 +1,90 @@
|
|||||||
package com.m57.hermescontrol.theme.presets
|
package com.m57.hermescontrol.theme.presets
|
||||||
|
|
||||||
import androidx.compose.material3.darkColorScheme
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import com.m57.hermescontrol.theme.HermesAmber
|
import com.m57.hermescontrol.theme.HermesStatusColors
|
||||||
import com.m57.hermescontrol.theme.HermesAmberLight
|
import com.m57.hermescontrol.theme.PaletteColors
|
||||||
import com.m57.hermescontrol.theme.HermesPurple
|
import com.m57.hermescontrol.theme.buildThemeDarkOnly
|
||||||
import com.m57.hermescontrol.theme.HermesPurpleOnContainer
|
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
// 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
|
* Same accessibility note as MonochromeTheme: status colors are pure
|
||||||
* scheme for light mode.
|
* 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 =
|
val AmoledTheme =
|
||||||
darkColorScheme(
|
buildThemeDarkOnly(
|
||||||
primary = HermesPurple,
|
dark =
|
||||||
onPrimary = Color.White,
|
PaletteColors(
|
||||||
primaryContainer = Color(0xFF1A1040),
|
primary = AmoledTextBright,
|
||||||
onPrimaryContainer = HermesPurpleOnContainer,
|
onPrimary = AmoledFloor,
|
||||||
inversePrimary = Color(0xFF6750A4),
|
primaryContainer = AmoledContainerHighest,
|
||||||
secondary = HermesAmber,
|
onPrimaryContainer = AmoledTextBright,
|
||||||
onSecondary = Color.Black,
|
secondary = AmoledSecondary,
|
||||||
secondaryContainer = Color(0xFF2A2000),
|
onSecondary = AmoledFloor,
|
||||||
onSecondaryContainer = HermesAmberLight,
|
secondaryContainer = AmoledContainerHigh,
|
||||||
tertiary = HermesAmberLight,
|
onSecondaryContainer = AmoledSecondary,
|
||||||
onTertiary = Color.Black,
|
tertiary = AmoledTertiary,
|
||||||
tertiaryContainer = Color(0xFF2A2000),
|
onTertiary = AmoledFloor,
|
||||||
onTertiaryContainer = HermesAmberLight,
|
tertiaryContainer = AmoledContainerHigh,
|
||||||
background = Color.Black,
|
onTertiaryContainer = AmoledTertiary,
|
||||||
onBackground = Color(0xFFE8E6EE),
|
background = AmoledFloor,
|
||||||
surface = Color.Black,
|
onBackground = AmoledTextBright,
|
||||||
onSurface = Color(0xFFE8E6EE),
|
surface = AmoledFloor,
|
||||||
surfaceVariant = Color(0xFF121218),
|
onSurface = AmoledTextBright,
|
||||||
onSurfaceVariant = Color(0xFFB6B2C4),
|
surfaceVariant = AmoledContainerHigh,
|
||||||
surfaceTint = HermesPurple,
|
onSurfaceVariant = AmoledTextDim,
|
||||||
surfaceContainerLowest = Color.Black,
|
surfaceContainerLowest = AmoledFloor,
|
||||||
surfaceContainerLow = Color(0xFF08080A),
|
surfaceContainerLow = AmoledFloor,
|
||||||
surfaceContainer = Color(0xFF0E0E12),
|
surfaceContainer = AmoledContainer,
|
||||||
surfaceContainerHigh = Color(0xFF16161C),
|
surfaceContainerHigh = AmoledContainerHigh,
|
||||||
surfaceContainerHighest = Color(0xFF1F1F28),
|
surfaceContainerHighest = AmoledContainerHighest,
|
||||||
inverseSurface = Color(0xFFE8E6EE),
|
inverseSurface = AmoledTextBright,
|
||||||
inverseOnSurface = Color.Black,
|
inverseOnSurface = AmoledFloor,
|
||||||
error = Color(0xFFCF6679),
|
inversePrimary = AmoledInversePrimary,
|
||||||
onError = Color.Black,
|
outline = AmoledOutline,
|
||||||
errorContainer = Color(0xFF3700B3),
|
outlineVariant = AmoledOutlineVariant,
|
||||||
onErrorContainer = Color(0xFFF2B8B5),
|
|
||||||
outline = Color(0xFF3A3A4A),
|
|
||||||
outlineVariant = Color(0xFF252532),
|
|
||||||
scrim = Color.Black,
|
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
|
package com.m57.hermescontrol.theme.presets
|
||||||
|
|
||||||
import androidx.compose.material3.darkColorScheme
|
|
||||||
import androidx.compose.material3.lightColorScheme
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import com.m57.hermescontrol.theme.HermesStatusColors
|
import com.m57.hermescontrol.theme.HermesStatusColors
|
||||||
|
import com.m57.hermescontrol.theme.PaletteColors
|
||||||
|
import com.m57.hermescontrol.theme.buildTheme
|
||||||
|
|
||||||
/** Catppuccin color scheme (dark). */
|
// ---------------------------------------------------------------------
|
||||||
val CatppuccinDarkColorScheme =
|
// Mocha (dark) — named swatches, so repeated hexes (Crust, Surface0/1,
|
||||||
darkColorScheme(
|
// Base, Text, Blue...) live in exactly one place.
|
||||||
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
|
|
||||||
)
|
|
||||||
|
|
||||||
/** Catppuccin color scheme (light). */
|
private val MochaMauve = Color(0xFFCBA6F7)
|
||||||
val CatppuccinLightColorScheme =
|
private val MochaBlue = Color(0xFF89B4FA)
|
||||||
lightColorScheme(
|
private val MochaPink = Color(0xFFF5C2E7)
|
||||||
primary = Color(0xFF8839EF), // Mauve
|
private val MochaGreen = Color(0xFFA6E3A1)
|
||||||
onPrimary = Color(0xFFEFF1F5), // Base
|
private val MochaYellow = Color(0xFFF9E2AF)
|
||||||
primaryContainer = Color(0xFFE6CAFA), // Light Mauve Container
|
private val MochaRed = Color(0xFFF38BA8)
|
||||||
onPrimaryContainer = Color(0xFF380075),
|
private val MochaText = Color(0xFFCDD6F4)
|
||||||
inversePrimary = Color(0xFFCBA6F7), // Mocha Mauve
|
private val MochaSubtext1 = Color(0xFFBAC2DE)
|
||||||
secondary = Color(0xFF1E66F5), // Blue
|
private val MochaSubtext0 = Color(0xFFA6ADC8)
|
||||||
onSecondary = Color(0xFFEFF1F5), // Base
|
private val MochaOverlay0 = Color(0xFF6C7086)
|
||||||
secondaryContainer = Color(0xFFE6E9EF), // Mantle
|
private val MochaSurface1 = Color(0xFF45475A)
|
||||||
onSecondaryContainer = Color(0xFF4C4F69), // Text
|
private val MochaSurface0 = Color(0xFF313244)
|
||||||
tertiary = Color(0xFFEA76CB), // Pink
|
private val MochaBase = Color(0xFF1E1E2E)
|
||||||
onTertiary = Color(0xFFEFF1F5), // Base
|
private val MochaMantle = Color(0xFF181825)
|
||||||
tertiaryContainer = Color(0xFFBCC0CC), // Surface 1
|
private val MochaCrust = Color(0xFF11111B)
|
||||||
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
|
|
||||||
)
|
|
||||||
|
|
||||||
/** Catppuccin status colors (dark). */
|
// ---------------------------------------------------------------------
|
||||||
val CatppuccinDarkStatusColors =
|
// Latte (light) — named swatches
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
|
||||||
|
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(
|
HermesStatusColors(
|
||||||
success = Color(0xFFA6E3A1), // Green
|
success = MochaGreen,
|
||||||
successContainer = Color(0xFF313244), // Surface 0
|
successContainer = MochaSurface0,
|
||||||
onSuccess = Color(0xFF11111B), // Crust
|
onSuccess = MochaCrust,
|
||||||
warning = Color(0xFFF9E2AF), // Yellow
|
warning = MochaYellow,
|
||||||
warningContainer = Color(0xFF313244), // Surface 0
|
warningContainer = MochaSurface0,
|
||||||
onWarning = Color(0xFF11111B), // Crust
|
onWarning = MochaCrust,
|
||||||
error = Color(0xFFF38BA8), // Red
|
error = MochaRed,
|
||||||
errorContainer = Color(0xFF313244), // Surface 0
|
errorContainer = MochaSurface0,
|
||||||
onError = Color(0xFF11111B), // Crust
|
onError = MochaCrust,
|
||||||
info = Color(0xFF89B4FA), // Blue
|
onErrorContainer = MochaRed,
|
||||||
infoContainer = Color(0xFF313244), // Surface 0
|
info = MochaBlue,
|
||||||
onInfo = Color(0xFF11111B), // Crust
|
infoContainer = MochaSurface0,
|
||||||
)
|
onInfo = MochaCrust,
|
||||||
|
),
|
||||||
/** Catppuccin status colors (light). */
|
),
|
||||||
val CatppuccinLightStatusColors =
|
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(
|
HermesStatusColors(
|
||||||
success = Color(0xFF40A02B), // Green
|
success = LatteGreen,
|
||||||
successContainer = Color(0xFFCCD0DA), // Surface 0
|
successContainer = LatteSurface0,
|
||||||
onSuccess = Color(0xFFEFF1F5), // Base
|
onSuccess = LatteBase,
|
||||||
warning = Color(0xFFDF8E1D), // Yellow
|
warning = LatteYellow,
|
||||||
warningContainer = Color(0xFFCCD0DA), // Surface 0
|
warningContainer = LatteSurface0,
|
||||||
onWarning = Color(0xFFEFF1F5), // Base
|
onWarning = LatteBase,
|
||||||
error = Color(0xFFD20F39), // Red
|
error = LatteRed,
|
||||||
errorContainer = Color(0xFFCCD0DA), // Surface 0
|
errorContainer = LatteSurface0,
|
||||||
onError = Color(0xFFEFF1F5), // Base
|
onError = LatteBase,
|
||||||
info = Color(0xFF1E66F5), // Blue
|
onErrorContainer = LatteRed,
|
||||||
infoContainer = Color(0xFFCCD0DA), // Surface 0
|
info = LatteBlue,
|
||||||
onInfo = Color(0xFFEFF1F5), // Base
|
infoContainer = LatteSurface0,
|
||||||
|
onInfo = LatteBase,
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
@@ -1,165 +1,152 @@
|
|||||||
package com.m57.hermescontrol.theme.presets
|
package com.m57.hermescontrol.theme.presets
|
||||||
|
|
||||||
import androidx.compose.material3.darkColorScheme
|
|
||||||
import androidx.compose.material3.lightColorScheme
|
|
||||||
import androidx.compose.ui.graphics.Color
|
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.HermesStatusColors
|
||||||
import com.m57.hermescontrol.theme.LightBackground
|
import com.m57.hermescontrol.theme.PaletteColors
|
||||||
import com.m57.hermescontrol.theme.LightInverseOnSurface
|
import com.m57.hermescontrol.theme.buildTheme
|
||||||
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
|
|
||||||
|
|
||||||
/** Brand default color scheme (dark) — Voltage Purple primary. */
|
// ---------------------------------------------------------------------
|
||||||
val DefaultDarkColorScheme =
|
// Slate Dark — named swatches
|
||||||
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,
|
|
||||||
)
|
|
||||||
|
|
||||||
/** Brand default color scheme (light) — Voltage Purple primary. */
|
private val SlateDarkBg0 = Color(0xFF0F1416)
|
||||||
val DefaultLightColorScheme =
|
private val SlateDarkBg1 = Color(0xFF171C1E)
|
||||||
lightColorScheme(
|
private val SlateDarkBg2 = Color(0xFF1B2022)
|
||||||
primary = HermesPurpleDark,
|
private val SlateDarkBg3 = Color(0xFF252B2D)
|
||||||
onPrimary = Color.White,
|
private val SlateDarkBg4 = Color(0xFF303638)
|
||||||
primaryContainer = HermesPurpleLight,
|
private val SlateDarkFg1 = Color(0xFFDEE3E5)
|
||||||
onPrimaryContainer = Color(0xFF1E0F66),
|
private val SlateDarkFg2 = Color(0xFFCEE7EC)
|
||||||
inversePrimary = HermesPurpleLight,
|
private val SlateDarkFg3 = Color(0xFFBEC8CA)
|
||||||
secondary = HermesAmberDark,
|
private val SlateGray = Color(0xFF889294) // same neutral gray in both modes
|
||||||
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,
|
|
||||||
)
|
|
||||||
|
|
||||||
/** Default status colors (dark). */
|
// "Bright" accents — high-visibility variants against dark surfaces.
|
||||||
val DefaultDarkStatusColors =
|
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)
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
// 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(
|
HermesStatusColors(
|
||||||
success = StatusGreen,
|
success = SlateBrightGreen,
|
||||||
successContainer = StatusGreenContainer,
|
successContainer = SlateDarkBg3,
|
||||||
onSuccess = StatusGreyLight,
|
onSuccess = SlateDarkBg0,
|
||||||
warning = StatusYellow,
|
warning = SlateBrightYellow,
|
||||||
warningContainer = StatusYellowContainer,
|
warningContainer = SlateDarkBg3,
|
||||||
onWarning = StatusGreyLight,
|
onWarning = SlateDarkBg0,
|
||||||
error = StatusRed,
|
error = SlateBrightRed,
|
||||||
errorContainer = StatusRedContainer,
|
errorContainer = SlateDarkBg3,
|
||||||
onError = StatusGreyLight,
|
onError = SlateDarkBg0,
|
||||||
info = StatusBlue,
|
onErrorContainer = SlateBrightRed,
|
||||||
infoContainer = StatusBlueContainer,
|
info = SlateBrightInfo,
|
||||||
onInfo = StatusGreyLight,
|
infoContainer = SlateDarkBg3,
|
||||||
)
|
onInfo = SlateDarkBg0,
|
||||||
|
),
|
||||||
/** Default status colors (light). */
|
),
|
||||||
val DefaultLightStatusColors =
|
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(
|
HermesStatusColors(
|
||||||
success = Color(0xFF1B873A),
|
success = SlateFadedGreen,
|
||||||
successContainer = Color(0xFFD7F5E0),
|
successContainer = SlateLightBg3,
|
||||||
onSuccess = Color(0xFFFDF8FF),
|
onSuccess = SlateLightBg0,
|
||||||
warning = HermesAmberDark,
|
warning = SlateFadedYellow,
|
||||||
warningContainer = Color(0xFFFFEAB3),
|
warningContainer = SlateLightBg3,
|
||||||
onWarning = Color(0xFFFDF8FF),
|
onWarning = SlateLightBg0,
|
||||||
error = Color(0xFFB3261E),
|
error = SlateFadedRed,
|
||||||
errorContainer = Color(0xFFF9DEDC),
|
errorContainer = SlateLightBg3,
|
||||||
onError = Color(0xFFFDF8FF),
|
onError = SlateLightBg0,
|
||||||
info = Color(0xFF2E6FBD),
|
onErrorContainer = SlateFadedRed,
|
||||||
infoContainer = Color(0xFFD4E7FF),
|
info = SlateFadedInfo,
|
||||||
onInfo = Color(0xFFFDF8FF),
|
infoContainer = SlateLightBg3,
|
||||||
|
onInfo = SlateLightBg0,
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
@@ -1,118 +1,151 @@
|
|||||||
package com.m57.hermescontrol.theme.presets
|
package com.m57.hermescontrol.theme.presets
|
||||||
|
|
||||||
import androidx.compose.material3.darkColorScheme
|
|
||||||
import androidx.compose.material3.lightColorScheme
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import com.m57.hermescontrol.theme.HermesStatusColors
|
import com.m57.hermescontrol.theme.HermesStatusColors
|
||||||
|
import com.m57.hermescontrol.theme.PaletteColors
|
||||||
|
import com.m57.hermescontrol.theme.buildTheme
|
||||||
|
|
||||||
/** Gruvbox color scheme (dark). */
|
// ---------------------------------------------------------------------
|
||||||
val GruvboxDarkColorScheme =
|
// Gruvbox Dark (medium contrast) — named swatches
|
||||||
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 color scheme (light). */
|
private val GruvboxDarkBg0 = Color(0xFF282828)
|
||||||
val GruvboxLightColorScheme =
|
private val GruvboxDarkBg1 = Color(0xFF3C3836)
|
||||||
lightColorScheme(
|
private val GruvboxDarkBg2 = Color(0xFF504945)
|
||||||
primary = Color(0xFFD65D0E),
|
private val GruvboxDarkBg3 = Color(0xFF665C54)
|
||||||
onPrimary = Color(0xFFFBF1C7),
|
private val GruvboxDarkBg4 = Color(0xFF7C6F64)
|
||||||
primaryContainer = Color(0xFFFE8019),
|
private val GruvboxDarkFg1 = Color(0xFFEBDBB2)
|
||||||
onPrimaryContainer = Color(0xFF282828),
|
private val GruvboxDarkFg2 = Color(0xFFD5C4A1)
|
||||||
inversePrimary = Color(0xFFFE8019),
|
private val GruvboxDarkFg3 = Color(0xFFBDAE93)
|
||||||
secondary = Color(0xFF458588),
|
private val GruvboxGray = Color(0xFF928374) // same neutral gray in both modes
|
||||||
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),
|
|
||||||
)
|
|
||||||
|
|
||||||
/** Gruvbox status colors (dark). */
|
// "Bright" accents — the variants gruvbox uses against a dark background.
|
||||||
val GruvboxDarkStatusColors =
|
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 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(
|
HermesStatusColors(
|
||||||
success = Color(0xFFB8BB26),
|
success = GruvboxBrightGreen,
|
||||||
successContainer = Color(0xFF32302F),
|
successContainer = GruvboxDarkBg3,
|
||||||
onSuccess = Color(0xFF282828),
|
onSuccess = GruvboxDarkBg0,
|
||||||
warning = Color(0xFFFABD2F),
|
warning = GruvboxBrightYellow,
|
||||||
warningContainer = Color(0xFF3C3836),
|
warningContainer = GruvboxDarkBg3,
|
||||||
onWarning = Color(0xFF282828),
|
onWarning = GruvboxDarkBg0,
|
||||||
error = Color(0xFFFB4934),
|
error = GruvboxBrightRed,
|
||||||
errorContainer = Color(0xFF282828),
|
errorContainer = GruvboxDarkBg3,
|
||||||
onError = Color(0xFF282828),
|
onError = GruvboxDarkBg0,
|
||||||
info = Color(0xFF83A598),
|
onErrorContainer = GruvboxDarkFg1,
|
||||||
infoContainer = Color(0xFF3C3836),
|
info = GruvboxBrightBlue,
|
||||||
onInfo = Color(0xFF282828),
|
infoContainer = GruvboxDarkBg3,
|
||||||
)
|
onInfo = GruvboxDarkBg0,
|
||||||
|
),
|
||||||
/** Gruvbox status colors (light). */
|
),
|
||||||
val GruvboxLightStatusColors =
|
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(
|
HermesStatusColors(
|
||||||
success = Color(0xFF98971A),
|
success = GruvboxFadedGreen,
|
||||||
successContainer = Color(0xFFEBDBB2),
|
successContainer = GruvboxLightBg3,
|
||||||
onSuccess = Color(0xFFEBDBB2),
|
onSuccess = GruvboxLightBg0,
|
||||||
warning = Color(0xFFD79921),
|
warning = GruvboxFadedYellow,
|
||||||
warningContainer = Color(0xFFEBDBB2),
|
warningContainer = GruvboxLightBg3,
|
||||||
onWarning = Color(0xFFEBDBB2),
|
onWarning = GruvboxLightBg0,
|
||||||
error = Color(0xFFCC241D),
|
error = GruvboxFadedRed,
|
||||||
errorContainer = Color(0xFFEBDBB2),
|
errorContainer = GruvboxLightBg3,
|
||||||
onError = Color(0xFFEBDBB2),
|
onError = GruvboxLightBg0,
|
||||||
info = Color(0xFF458588),
|
onErrorContainer = GruvboxFadedRed,
|
||||||
infoContainer = Color(0xFFEBDBB2),
|
info = GruvboxFadedBlue,
|
||||||
onInfo = Color(0xFFEBDBB2),
|
infoContainer = GruvboxLightBg3,
|
||||||
|
onInfo = GruvboxLightBg0,
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
@@ -1,118 +1,150 @@
|
|||||||
package com.m57.hermescontrol.theme.presets
|
package com.m57.hermescontrol.theme.presets
|
||||||
|
|
||||||
import androidx.compose.material3.darkColorScheme
|
|
||||||
import androidx.compose.material3.lightColorScheme
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import com.m57.hermescontrol.theme.HermesStatusColors
|
import com.m57.hermescontrol.theme.HermesStatusColors
|
||||||
|
import com.m57.hermescontrol.theme.PaletteColors
|
||||||
|
import com.m57.hermescontrol.theme.buildTheme
|
||||||
|
|
||||||
/** Monochrome color scheme (dark). */
|
// ---------------------------------------------------------------------
|
||||||
val MonochromeDarkColorScheme =
|
// Monochrome Dark — every swatch is R=G=B; roles are distinguished
|
||||||
darkColorScheme(
|
// purely by lightness, not hue.
|
||||||
primary = Color.White,
|
// ---------------------------------------------------------------------
|
||||||
onPrimary = Color.Black,
|
|
||||||
primaryContainer = Color(0xFF404040),
|
private val MonoDarkFloor = Color(0xFF0A0A0A) // surfaceContainerLowest
|
||||||
onPrimaryContainer = Color.White,
|
private val MonoDarkBg = Color(0xFF121212) // background, surface, containerLow, all "on*" ink
|
||||||
inversePrimary = Color(0xFF121212),
|
private val MonoDarkContainer = Color(0xFF1E1E1E) // surfaceContainer
|
||||||
secondary = Color(0xFFB0B0B0),
|
private val MonoDarkContainerHigh = Color(0xFF2A2A2A) // surfaceContainerHigh, surfaceVariant, status containers
|
||||||
onSecondary = Color.Black,
|
private val MonoDarkContainerHighest = Color(0xFF363636) // surfaceContainerHighest, primaryContainer
|
||||||
secondaryContainer = Color(0xFF222222),
|
private val MonoDarkOutline = Color(0xFF808080)
|
||||||
onSecondaryContainer = Color(0xFFE0E0E0),
|
private val MonoDarkOutlineVariant = Color(0xFF404040)
|
||||||
tertiary = Color(0xFFD0D0D0),
|
private val MonoDarkTextDim = Color(0xFFB3B3B3) // onSurfaceVariant
|
||||||
onTertiary = Color.Black,
|
private val MonoDarkTextBright = Color(0xFFF2F2F2) // onBackground/onSurface/inverseSurface/primary/error
|
||||||
tertiaryContainer = Color(0xFF333333),
|
private val MonoDarkSecondary = Color(0xFFCCCCCC) // secondary accent + warning
|
||||||
onTertiaryContainer = Color(0xFFE0E0E0),
|
private val MonoDarkTertiary = Color(0xFF999999) // tertiary accent + success
|
||||||
background = Color(0xFF121212),
|
private val MonoDarkInfo = Color(0xFF808080) // info — same value as outline, distinct role
|
||||||
onBackground = Color(0xFFE0E0E0),
|
|
||||||
surface = Color(0xFF1E1E1E),
|
// ---------------------------------------------------------------------
|
||||||
onSurface = Color(0xFFE0E0E0),
|
// Monochrome Light — the same ramp, inverted
|
||||||
surfaceVariant = Color(0xFF2B2B2B),
|
// ---------------------------------------------------------------------
|
||||||
onSurfaceVariant = Color(0xFFB0B0B0),
|
|
||||||
surfaceTint = Color.White,
|
private val MonoLightCeiling = Color(0xFFFFFFFF) // surfaceContainerLowest
|
||||||
surfaceContainerLowest = Color(0xFF0F0F0F),
|
private val MonoLightBg = Color(0xFFFAFAFA) // background, surface, containerLow, all "on*" ink
|
||||||
surfaceContainerLow = Color(0xFF1A1A1A),
|
private val MonoLightContainer = Color(0xFFF0F0F0) // surfaceContainer
|
||||||
surfaceContainer = Color(0xFF1E1E1E),
|
private val MonoLightContainerHigh = Color(0xFFE0E0E0) // surfaceContainerHigh, surfaceVariant, status containers
|
||||||
surfaceContainerHigh = Color(0xFF252525),
|
private val MonoLightContainerHighest = Color(0xFFD1D1D1) // surfaceContainerHighest, primaryContainer
|
||||||
surfaceContainerHighest = Color(0xFF2C2C2C),
|
private val MonoLightOutline = Color(0xFF808080)
|
||||||
inverseSurface = Color(0xFFE0E0E0),
|
private val MonoLightOutlineVariant = Color(0xFFCCCCCC)
|
||||||
inverseOnSurface = Color(0xFF121212),
|
private val MonoLightTextDim = Color(0xFF4D4D4D) // onSurfaceVariant
|
||||||
error = Color(0xFFE0E0E0),
|
private val MonoLightTextBright = Color(0xFF121212) // onBackground/onSurface/inverseSurface/primary/error
|
||||||
onError = Color.Black,
|
private val MonoLightSecondary = Color(0xFF333333) // secondary accent + warning
|
||||||
errorContainer = Color(0xFF404040),
|
private val MonoLightTertiary = Color(0xFF666666) // tertiary accent + success
|
||||||
onErrorContainer = Color(0xFFE0E0E0),
|
private val MonoLightInfo = Color(0xFF808080) // info — same value as outline, distinct role
|
||||||
outline = Color(0xFF666666),
|
|
||||||
outlineVariant = Color(0xFF444444),
|
/**
|
||||||
|
* 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,
|
scrim = Color.Black,
|
||||||
)
|
status =
|
||||||
|
HermesStatusColors(
|
||||||
/** Monochrome color scheme (light). */
|
success = MonoDarkTertiary,
|
||||||
val MonochromeLightColorScheme =
|
successContainer = MonoDarkContainerHigh,
|
||||||
lightColorScheme(
|
onSuccess = MonoDarkBg,
|
||||||
primary = Color.Black,
|
warning = MonoDarkSecondary,
|
||||||
onPrimary = Color.White,
|
warningContainer = MonoDarkContainerHigh,
|
||||||
primaryContainer = Color(0xFFC8C8C8),
|
onWarning = MonoDarkBg,
|
||||||
onPrimaryContainer = Color.Black,
|
error = MonoDarkTextBright,
|
||||||
inversePrimary = Color(0xFFE0E0E0),
|
errorContainer = MonoDarkContainerHigh,
|
||||||
secondary = Color(0xFF4A4A4A),
|
onError = MonoDarkBg,
|
||||||
onSecondary = Color.White,
|
onErrorContainer = MonoDarkTextBright,
|
||||||
secondaryContainer = Color(0xFFF0F0F0),
|
info = MonoDarkInfo,
|
||||||
onSecondaryContainer = Color(0xFF111111),
|
infoContainer = MonoDarkContainerHigh,
|
||||||
tertiary = Color(0xFF333333),
|
onInfo = MonoDarkBg,
|
||||||
onTertiary = Color.White,
|
),
|
||||||
tertiaryContainer = Color(0xFFE8E8E8),
|
),
|
||||||
onTertiaryContainer = Color(0xFF111111),
|
light =
|
||||||
background = Color(0xFFFFFFFF),
|
PaletteColors(
|
||||||
onBackground = Color(0xFF111111),
|
primary = MonoLightTextBright,
|
||||||
surface = Color(0xFFF5F5F5),
|
onPrimary = MonoLightBg,
|
||||||
onSurface = Color(0xFF111111),
|
primaryContainer = MonoLightContainerHighest,
|
||||||
surfaceVariant = Color(0xFFE0E0E0),
|
onPrimaryContainer = MonoLightTextBright,
|
||||||
onSurfaceVariant = Color(0xFF4A4A4A),
|
secondary = MonoLightSecondary,
|
||||||
surfaceTint = Color.Black,
|
onSecondary = MonoLightBg,
|
||||||
surfaceContainerLowest = Color(0xFFFFFFFF),
|
secondaryContainer = MonoLightContainerHigh,
|
||||||
surfaceContainerLow = Color(0xFFF8F8F8),
|
onSecondaryContainer = MonoLightSecondary,
|
||||||
surfaceContainer = Color(0xFFF5F5F5),
|
tertiary = MonoLightTertiary,
|
||||||
surfaceContainerHigh = Color(0xFFEEEEEE),
|
onTertiary = MonoLightBg,
|
||||||
surfaceContainerHighest = Color(0xFFE3E3E3),
|
tertiaryContainer = MonoLightContainerHigh,
|
||||||
inverseSurface = Color(0xFF111111),
|
onTertiaryContainer = MonoLightTertiary,
|
||||||
inverseOnSurface = Color(0xFFE0E0E0),
|
background = MonoLightBg,
|
||||||
error = Color(0xFF111111),
|
onBackground = MonoLightTextBright,
|
||||||
onError = Color.White,
|
surface = MonoLightBg,
|
||||||
errorContainer = Color(0xFFD6D6D6),
|
onSurface = MonoLightTextBright,
|
||||||
onErrorContainer = Color(0xFF111111),
|
surfaceVariant = MonoLightContainerHigh,
|
||||||
outline = Color(0xFF888888),
|
onSurfaceVariant = MonoLightTextDim,
|
||||||
outlineVariant = Color(0xFFCCCCCC),
|
surfaceContainerLowest = MonoLightCeiling,
|
||||||
|
surfaceContainerLow = MonoLightBg,
|
||||||
|
surfaceContainer = MonoLightContainer,
|
||||||
|
surfaceContainerHigh = MonoLightContainerHigh,
|
||||||
|
surfaceContainerHighest = MonoLightContainerHighest,
|
||||||
|
inverseSurface = MonoLightTextBright,
|
||||||
|
inverseOnSurface = MonoLightBg,
|
||||||
|
inversePrimary = MonoDarkTextBright,
|
||||||
|
outline = MonoLightOutline,
|
||||||
|
outlineVariant = MonoLightOutlineVariant,
|
||||||
scrim = Color.Black,
|
scrim = Color.Black,
|
||||||
)
|
status =
|
||||||
|
|
||||||
/** Monochrome status colors (dark). */
|
|
||||||
val MonochromeDarkStatusColors =
|
|
||||||
HermesStatusColors(
|
HermesStatusColors(
|
||||||
success = Color(0xFFE0E0E0),
|
success = MonoLightTertiary,
|
||||||
successContainer = Color(0xFF2A2A2A),
|
successContainer = MonoLightContainerHigh,
|
||||||
onSuccess = Color(0xFF121212),
|
onSuccess = MonoLightBg,
|
||||||
warning = Color(0xFFCCCCCC),
|
warning = MonoLightSecondary,
|
||||||
warningContainer = Color(0xFF333333),
|
warningContainer = MonoLightContainerHigh,
|
||||||
onWarning = Color(0xFF121212),
|
onWarning = MonoLightBg,
|
||||||
error = Color(0xFFFFFFFF),
|
error = MonoLightTextBright,
|
||||||
errorContainer = Color(0xFF404040),
|
errorContainer = MonoLightContainerHigh,
|
||||||
onError = Color(0xFF121212),
|
onError = MonoLightBg,
|
||||||
info = Color(0xFFB0B0B0),
|
onErrorContainer = MonoLightTextBright,
|
||||||
infoContainer = Color(0xFF242424),
|
info = MonoLightInfo,
|
||||||
onInfo = Color(0xFF121212),
|
infoContainer = MonoLightContainerHigh,
|
||||||
)
|
onInfo = MonoLightBg,
|
||||||
|
),
|
||||||
/** 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),
|
|
||||||
)
|
)
|
||||||
@@ -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 =
|
val isSkill =
|
||||||
cleanText.contains("skill", ignoreCase = true) ||
|
cleanText.contains("skill", ignoreCase = true) ||
|
||||||
cleanText.contains("SKILL.md", ignoreCase = true)
|
cleanText.contains("SKILL.md", ignoreCase = true)
|
||||||
val icon = if (isSkill) "⚡" else "🧠"
|
val icon = if (isSkill) "🛠" else "🧠"
|
||||||
val title =
|
val title =
|
||||||
if (isSkill) {
|
if (isSkill) {
|
||||||
"Self-Improvement Review • Skill Patched"
|
"Self-Improvement Review • Skill Patched"
|
||||||
|
|||||||
+2
-2
@@ -187,7 +187,7 @@ private fun TodoInspectionCard(todo: TodoItem) {
|
|||||||
val statusSymbol =
|
val statusSymbol =
|
||||||
when {
|
when {
|
||||||
todo.isCompleted -> "✅"
|
todo.isCompleted -> "✅"
|
||||||
todo.isInProgress -> "⚡"
|
todo.isInProgress -> "🔄"
|
||||||
todo.isCancelled -> "❌"
|
todo.isCancelled -> "❌"
|
||||||
else -> "⭕"
|
else -> "⭕"
|
||||||
}
|
}
|
||||||
@@ -225,7 +225,7 @@ private fun InspectionItemCard(indicator: SubagentIndicator) {
|
|||||||
when {
|
when {
|
||||||
indicator.isComplete -> "✅"
|
indicator.isComplete -> "✅"
|
||||||
indicator.isFailed -> "❌"
|
indicator.isFailed -> "❌"
|
||||||
else -> "⚡"
|
else -> "🔄"
|
||||||
}
|
}
|
||||||
Text(text = statusSymbol, fontSize = 16.sp)
|
Text(text = statusSymbol, fontSize = 16.sp)
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
|||||||
@@ -344,7 +344,7 @@ private fun SelfImprovementSection(graph: LearningGraphResponse?) {
|
|||||||
modifier = Modifier.padding(horizontal = 10.dp, vertical = 8.dp),
|
modifier = Modifier.padding(horizontal = 10.dp, vertical = 8.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
val icon = if (node.kind == "skill") "⚡" else "🧠"
|
val icon = if (node.kind == "skill") "🛠" else "🧠"
|
||||||
Text(text = icon, fontSize = 16.sp)
|
Text(text = icon, fontSize = 16.sp)
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ internal fun AppearanceSection(
|
|||||||
ThemePreset.GRUVBOX -> stringResource(R.string.theme_preset_gruvbox)
|
ThemePreset.GRUVBOX -> stringResource(R.string.theme_preset_gruvbox)
|
||||||
ThemePreset.CATPPUCCIN -> stringResource(R.string.theme_preset_catppuccin)
|
ThemePreset.CATPPUCCIN -> stringResource(R.string.theme_preset_catppuccin)
|
||||||
ThemePreset.AMOLED -> stringResource(R.string.theme_preset_amoled)
|
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(
|
stringResource(
|
||||||
R.string.theme_preset_neon_noir,
|
R.string.theme_preset_nord,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -49,7 +49,7 @@
|
|||||||
<string name="billing_topup">충전</string>
|
<string name="billing_topup">충전</string>
|
||||||
|
|
||||||
<!-- Navigation & Drawer -->
|
<!-- Navigation & Drawer -->
|
||||||
<string name="nav_drawer_title">⚡ 헤르메스</string>
|
<string name="nav_drawer_title">헤르메스</string>
|
||||||
<string name="nav_drawer_subtitle">AI 에이전트 제어</string>
|
<string name="nav_drawer_subtitle">AI 에이전트 제어</string>
|
||||||
<string name="nav_drawer_section_converse">대화</string>
|
<string name="nav_drawer_section_converse">대화</string>
|
||||||
<string name="nav_drawer_section_automate">자동화</string>
|
<string name="nav_drawer_section_automate">자동화</string>
|
||||||
@@ -148,7 +148,7 @@
|
|||||||
<string name="settings_delay_100ms">100 ms</string>
|
<string name="settings_delay_100ms">100 ms</string>
|
||||||
<string name="settings_action_test_connection">연결 테스트</string>
|
<string name="settings_action_test_connection">연결 테스트</string>
|
||||||
<string name="settings_sec_about">정보</string>
|
<string name="settings_sec_about">정보</string>
|
||||||
<string name="settings_about_app_name">⚡ 헤르메스 모바일</string>
|
<string name="settings_about_app_name">헤르메스 모바일</string>
|
||||||
<string name="settings_about_version">버전</string>
|
<string name="settings_about_version">버전</string>
|
||||||
<string name="settings_about_build">빌드</string>
|
<string name="settings_about_build">빌드</string>
|
||||||
<string name="settings_about_debug">디버그</string>
|
<string name="settings_about_debug">디버그</string>
|
||||||
@@ -176,7 +176,6 @@
|
|||||||
<string name="theme_preset_gruvbox">Gruvbox</string>
|
<string name="theme_preset_gruvbox">Gruvbox</string>
|
||||||
<string name="theme_preset_catppuccin">Catppuccin</string>
|
<string name="theme_preset_catppuccin">Catppuccin</string>
|
||||||
<string name="theme_preset_amoled">AMOLED 블랙 (다크 전용)</string>
|
<string name="theme_preset_amoled">AMOLED 블랙 (다크 전용)</string>
|
||||||
<string name="theme_preset_neon_noir">네온 느와르</string>
|
|
||||||
|
|
||||||
<!-- Connect Screen -->
|
<!-- Connect Screen -->
|
||||||
<string name="connect_selected_profile">프로필: %1$s</string>
|
<string name="connect_selected_profile">프로필: %1$s</string>
|
||||||
|
|||||||
@@ -61,7 +61,7 @@
|
|||||||
<string name="billing_topup">Top-up</string>
|
<string name="billing_topup">Top-up</string>
|
||||||
|
|
||||||
<!-- Navigation & Drawer -->
|
<!-- Navigation & Drawer -->
|
||||||
<string name="nav_drawer_title">⚡ Hermes</string>
|
<string name="nav_drawer_title">Hermes</string>
|
||||||
<string name="nav_drawer_subtitle">AI Agent Control</string>
|
<string name="nav_drawer_subtitle">AI Agent Control</string>
|
||||||
<string name="nav_drawer_section_converse">Converse</string>
|
<string name="nav_drawer_section_converse">Converse</string>
|
||||||
<string name="nav_drawer_section_automate">Automate</string>
|
<string name="nav_drawer_section_automate">Automate</string>
|
||||||
@@ -163,7 +163,7 @@
|
|||||||
<string name="settings_delay_100ms">100 ms</string>
|
<string name="settings_delay_100ms">100 ms</string>
|
||||||
<string name="settings_action_test_connection">Test Connection</string>
|
<string name="settings_action_test_connection">Test Connection</string>
|
||||||
<string name="settings_sec_about">About</string>
|
<string name="settings_sec_about">About</string>
|
||||||
<string name="settings_about_app_name">⚡ Hermes Mobile</string>
|
<string name="settings_about_app_name">Hermes Mobile</string>
|
||||||
<string name="settings_about_version">Version</string>
|
<string name="settings_about_version">Version</string>
|
||||||
<string name="settings_about_build">Build</string>
|
<string name="settings_about_build">Build</string>
|
||||||
<string name="settings_about_debug">Debug</string>
|
<string name="settings_about_debug">Debug</string>
|
||||||
@@ -196,12 +196,12 @@
|
|||||||
<string name="settings_item_use_dynamic_colors">Dynamic Colors</string>
|
<string name="settings_item_use_dynamic_colors">Dynamic Colors</string>
|
||||||
<string name="settings_desc_use_dynamic_colors">Derive scheme from wallpaper (Android 12+)</string>
|
<string name="settings_desc_use_dynamic_colors">Derive scheme from wallpaper (Android 12+)</string>
|
||||||
<string name="settings_item_theme_preset">Preset Theme</string>
|
<string name="settings_item_theme_preset">Preset Theme</string>
|
||||||
<string name="theme_preset_default">Default (Voltage Purple)</string>
|
<string name="theme_preset_default">Default</string>
|
||||||
<string name="theme_preset_monochrome">Monochrome</string>
|
<string name="theme_preset_monochrome">Monochrome</string>
|
||||||
<string name="theme_preset_gruvbox">Gruvbox</string>
|
<string name="theme_preset_gruvbox">Gruvbox</string>
|
||||||
<string name="theme_preset_catppuccin">Catppuccin</string>
|
<string name="theme_preset_catppuccin">Catppuccin</string>
|
||||||
<string name="theme_preset_amoled">AMOLED Black (Dark only)</string>
|
<string name="theme_preset_amoled">AMOLED Black (Dark only)</string>
|
||||||
<string name="theme_preset_neon_noir">Neon Noir</string>
|
<string name="theme_preset_nord">Nord</string>
|
||||||
|
|
||||||
<!-- Language -->
|
<!-- Language -->
|
||||||
<string name="settings_item_language">Language</string>
|
<string name="settings_item_language">Language</string>
|
||||||
|
|||||||
@@ -0,0 +1,189 @@
|
|||||||
|
package com.m57.hermescontrol.theme
|
||||||
|
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.luminance
|
||||||
|
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 org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertThrows
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Guards the palette template's color invariants:
|
||||||
|
* 1. Every shipped error slot pair meets >= 3:1 WCAG contrast.
|
||||||
|
* 2. ThemeMode combinations are validated at construction time.
|
||||||
|
* 3. One-mode themes resolve and fall back to the default theme correctly.
|
||||||
|
*/
|
||||||
|
class ThemePaletteTest {
|
||||||
|
private val themes =
|
||||||
|
listOf(
|
||||||
|
ThemePreset.DEFAULT to DefaultTheme,
|
||||||
|
ThemePreset.MONOCHROME to MonochromeTheme,
|
||||||
|
ThemePreset.GRUVBOX to GruvboxTheme,
|
||||||
|
ThemePreset.CATPPUCCIN to CatppuccinTheme,
|
||||||
|
ThemePreset.AMOLED to AmoledTheme,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun errorSlotPairsMeetContrastInEveryShippedMode() {
|
||||||
|
themes.forEach { (preset, theme) ->
|
||||||
|
listOf(true, false).forEach { dark ->
|
||||||
|
val scheme = theme.schemeFor(dark) ?: return@forEach
|
||||||
|
assertTrue(
|
||||||
|
"$preset dark=$dark onError/error contrast must be >= 3:1",
|
||||||
|
contrast(scheme.onError, scheme.error) >= 3f,
|
||||||
|
)
|
||||||
|
assertTrue(
|
||||||
|
"$preset dark=$dark onErrorContainer/errorContainer contrast must be >= 3:1",
|
||||||
|
contrast(scheme.onErrorContainer, scheme.errorContainer) >= 3f,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun amoledShipsDarkOnly() {
|
||||||
|
assertTrue("AMOLED must ship a dark scheme", AmoledTheme.darkScheme != null)
|
||||||
|
assertTrue("AMOLED must not ship a light scheme", AmoledTheme.lightScheme == null)
|
||||||
|
assertTrue("AMOLED must ship dark status colors", AmoledTheme.darkStatus != null)
|
||||||
|
assertTrue("AMOLED must not ship light status colors", AmoledTheme.lightStatus == null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun everyPresetResolvesItsDeclaredModes() {
|
||||||
|
themes.forEach { (preset, theme) ->
|
||||||
|
listOf(true, false).forEach { dark ->
|
||||||
|
if (theme.schemeFor(dark) != null) {
|
||||||
|
assertTrue(
|
||||||
|
"$preset dark=$dark scheme must resolve",
|
||||||
|
resolveColorScheme(preset, darkTheme = dark) != null,
|
||||||
|
)
|
||||||
|
assertTrue(
|
||||||
|
"$preset dark=$dark status must resolve",
|
||||||
|
resolveStatusColors(preset, darkTheme = dark) != null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun amoledLightModeFallsBackToDefaultLight() {
|
||||||
|
val fallbackScheme = resolveColorScheme(ThemePreset.AMOLED, darkTheme = false)
|
||||||
|
val defaultLight = DefaultTheme.lightScheme!!
|
||||||
|
assertEquals(
|
||||||
|
"AMOLED light scheme must fall back to Default light background",
|
||||||
|
defaultLight.background,
|
||||||
|
fallbackScheme.background,
|
||||||
|
)
|
||||||
|
|
||||||
|
val fallbackStatus = resolveStatusColors(ThemePreset.AMOLED, darkTheme = false)
|
||||||
|
val defaultLightStatus = DefaultTheme.lightStatus!!
|
||||||
|
assertEquals(
|
||||||
|
"AMOLED light status must fall back to Default light success",
|
||||||
|
defaultLightStatus.success,
|
||||||
|
fallbackStatus.success,
|
||||||
|
)
|
||||||
|
|
||||||
|
// AMOLED dark resolves to its own palette, not the default.
|
||||||
|
val ownDark = AmoledTheme.darkScheme!!
|
||||||
|
assertEquals(
|
||||||
|
"AMOLED dark scheme must be its own",
|
||||||
|
ownDark.background,
|
||||||
|
resolveColorScheme(ThemePreset.AMOLED, darkTheme = true).background,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun themePaletteRejectsIllegalModeCombinations() {
|
||||||
|
val colors = dummyColors()
|
||||||
|
assertThrows(IllegalArgumentException::class.java) {
|
||||||
|
ThemePalette(ThemeMode.FULL, null, null)
|
||||||
|
}
|
||||||
|
assertThrows(IllegalArgumentException::class.java) {
|
||||||
|
ThemePalette(ThemeMode.FULL, colors, null)
|
||||||
|
}
|
||||||
|
assertThrows(IllegalArgumentException::class.java) {
|
||||||
|
ThemePalette(ThemeMode.FULL, null, colors)
|
||||||
|
}
|
||||||
|
assertThrows(IllegalArgumentException::class.java) {
|
||||||
|
ThemePalette(ThemeMode.DARK_ONLY, null, null)
|
||||||
|
}
|
||||||
|
assertThrows(IllegalArgumentException::class.java) {
|
||||||
|
ThemePalette(ThemeMode.DARK_ONLY, null, colors)
|
||||||
|
}
|
||||||
|
assertThrows(IllegalArgumentException::class.java) {
|
||||||
|
ThemePalette(ThemeMode.LIGHT_ONLY, null, null)
|
||||||
|
}
|
||||||
|
assertThrows(IllegalArgumentException::class.java) {
|
||||||
|
ThemePalette(ThemeMode.LIGHT_ONLY, colors, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Valid combinations must construct without throwing.
|
||||||
|
ThemePalette(ThemeMode.FULL, colors, colors)
|
||||||
|
ThemePalette(ThemeMode.DARK_ONLY, colors, null)
|
||||||
|
ThemePalette(ThemeMode.LIGHT_ONLY, null, colors)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun contrast(
|
||||||
|
a: Color,
|
||||||
|
b: Color,
|
||||||
|
): Float {
|
||||||
|
val lighter = maxOf(a.luminance(), b.luminance())
|
||||||
|
val darker = minOf(a.luminance(), b.luminance())
|
||||||
|
return (lighter + 0.05f) / (darker + 0.05f)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun dummyColors(): PaletteColors =
|
||||||
|
PaletteColors(
|
||||||
|
primary = Color.White,
|
||||||
|
onPrimary = Color.Black,
|
||||||
|
primaryContainer = Color.White,
|
||||||
|
onPrimaryContainer = Color.Black,
|
||||||
|
secondary = Color.White,
|
||||||
|
onSecondary = Color.Black,
|
||||||
|
secondaryContainer = Color.White,
|
||||||
|
onSecondaryContainer = Color.Black,
|
||||||
|
tertiary = Color.White,
|
||||||
|
onTertiary = Color.Black,
|
||||||
|
tertiaryContainer = Color.White,
|
||||||
|
onTertiaryContainer = Color.Black,
|
||||||
|
background = Color.White,
|
||||||
|
onBackground = Color.Black,
|
||||||
|
surface = Color.White,
|
||||||
|
onSurface = Color.Black,
|
||||||
|
surfaceVariant = Color.White,
|
||||||
|
onSurfaceVariant = Color.Black,
|
||||||
|
surfaceContainerLowest = Color.White,
|
||||||
|
surfaceContainerLow = Color.White,
|
||||||
|
surfaceContainer = Color.White,
|
||||||
|
surfaceContainerHigh = Color.White,
|
||||||
|
surfaceContainerHighest = Color.White,
|
||||||
|
inverseSurface = Color.Black,
|
||||||
|
inverseOnSurface = Color.White,
|
||||||
|
inversePrimary = Color.Black,
|
||||||
|
outline = Color.Gray,
|
||||||
|
outlineVariant = Color.Gray,
|
||||||
|
scrim = Color.Black,
|
||||||
|
status =
|
||||||
|
HermesStatusColors(
|
||||||
|
success = Color.Green,
|
||||||
|
successContainer = Color.White,
|
||||||
|
onSuccess = Color.Black,
|
||||||
|
warning = Color.Yellow,
|
||||||
|
warningContainer = Color.White,
|
||||||
|
onWarning = Color.Black,
|
||||||
|
error = Color.Red,
|
||||||
|
errorContainer = Color.White,
|
||||||
|
onError = Color.Black,
|
||||||
|
onErrorContainer = Color.Black,
|
||||||
|
info = Color.Blue,
|
||||||
|
infoContainer = Color.White,
|
||||||
|
onInfo = Color.Black,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in new issue
Block a user