refactor(theme): split per-preset color schemes into theme/presets/ (#594)
* refactor(theme): split per-preset color schemes into theme/presets/ Move each ThemePreset's ColorScheme + status colors into its own file under theme/presets/ (Default, Monochrome, Gruvbox, Catppuccin, Amoled, NeonNoir). Theme.kt becomes a thin dispatcher via resolveColorScheme/resolveStatusColors, making individual themes far easier to create and edit. - AMOLED keeps a dark-only scheme (falls back to default light) - MONOCHROME/AMOLED reuse default status colors (no bespoke semantic set) - aligned to current Voltage Purple palette + on* status-color params Verified: ktlint clean, compileDebugKotlin passes. * refactor(theme): make all preset files uniform 4-token shape Every preset file now exports XxxDarkColorScheme / XxxLightColorScheme / XxxDarkStatusColors / XxxLightStatusColors so they share one shape and import order. MONOCHROME + AMOLED alias the default status colors; AMOLED also gets an explicit AmoledLightColorScheme = DefaultLightColorScheme alias. Theme.kt dispatcher is now a pure lookup (no special-casing) for both color scheme and status colors. No runtime behavior change — pure structural cleanup. Verified: ktlint clean, compileDebugKotlin passes. * docs(theme): add THEMES.md documenting the per-preset convention Co-located in theme/ — covers layout, the uniform 4-token shape, the pure lookup dispatcher, why DEFAULT uses design tokens (intentional, not a bug), and a step-by-step 'add a new theme' recipe. Verified: ktlint clean. --------- Co-authored-by: M57 <m57@hermes-control.app>
This commit is contained in:
1 parent
29b630a2ec
commit
47258bd8b0
8 files changed
+695
-424
No files matched your search
@@ -0,0 +1,113 @@
|
||||
# Themes
|
||||
|
||||
How theming works in Hermes Control, and how to add or edit a theme.
|
||||
|
||||
## Where themes live
|
||||
|
||||
All theme code is under:
|
||||
|
||||
```
|
||||
app/src/main/java/com/m57/hermescontrol/theme/
|
||||
├── Theme.kt # dispatcher — resolves scheme + status colors per preset
|
||||
├── Color.kt # brand design tokens (DarkSurface, HermesPurple, StatusGreen, …)
|
||||
├── HermesStatusColors.kt # semantic status color model (success/warning/error/info + on*)
|
||||
├── Type.kt / Shapes.kt # typography + shape tokens
|
||||
├── Spacing.kt / Motion.kt # spacing + motion tokens
|
||||
└── presets/ # one file per ThemePreset
|
||||
├── DefaultScheme.kt
|
||||
├── MonochromeScheme.kt
|
||||
├── GruvboxScheme.kt
|
||||
├── CatppuccinScheme.kt
|
||||
├── AmoledScheme.kt
|
||||
└── NeonNoirScheme.kt
|
||||
```
|
||||
|
||||
## The 4-token shape
|
||||
|
||||
Every file in `presets/` is uniform — it exports the same four tokens:
|
||||
|
||||
| Token | Type | Purpose |
|
||||
|-------|------|---------|
|
||||
| `XxxDarkColorScheme` | `ColorScheme` | dark-mode Material 3 scheme |
|
||||
| `XxxLightColorScheme` | `ColorScheme` | light-mode Material 3 scheme |
|
||||
| `XxxDarkStatusColors` | `HermesStatusColors` | dark semantic status colors |
|
||||
| `XxxLightStatusColors` | `HermesStatusColors` | light semantic status colors |
|
||||
|
||||
A preset with no bespoke status set (MONOCHROME, AMOLED) aliases the defaults:
|
||||
|
||||
```kotlin
|
||||
val MonochromeDarkStatusColors = DefaultDarkStatusColors
|
||||
val MonochromeLightStatusColors = DefaultLightStatusColors
|
||||
```
|
||||
|
||||
AMOLED has no bespoke light scheme, so it aliases the default light scheme too:
|
||||
|
||||
```kotlin
|
||||
val AmoledLightColorScheme = DefaultLightColorScheme
|
||||
```
|
||||
|
||||
## The dispatcher
|
||||
|
||||
`Theme.kt` is a **pure lookup** — no special-casing. Given a `ThemePreset`
|
||||
and a dark flag, it returns the matching scheme/status from the preset file:
|
||||
|
||||
```kotlin
|
||||
private fun resolveColorScheme(preset: ThemePreset, darkTheme: Boolean) = when (preset) {
|
||||
ThemePreset.DEFAULT -> if (darkTheme) DefaultDarkColorScheme else DefaultLightColorScheme
|
||||
ThemePreset.MONOCHROME -> if (darkTheme) MonochromeDarkColorScheme else MonochromeLightColorScheme
|
||||
// …one line per preset…
|
||||
}
|
||||
|
||||
private fun resolveStatusColors(preset: ThemePreset, darkTheme: Boolean) = when (preset) {
|
||||
ThemePreset.DEFAULT -> if (darkTheme) DefaultDarkStatusColors else DefaultLightStatusColors
|
||||
// …one line per preset…
|
||||
}
|
||||
```
|
||||
|
||||
Dynamic (Material You) color on API 31+ overrides the resolved scheme when
|
||||
`useDynamicColors = true`.
|
||||
|
||||
## DEFAULT uses design tokens — that's intentional
|
||||
|
||||
`DefaultScheme.kt` imports ~47 named tokens from `Color.kt`
|
||||
(`DarkBackground`, `HermesPurple`, `StatusGreenContainer`, …) and builds its
|
||||
schemes from them. This is **by design**: DEFAULT is the brand reference theme,
|
||||
so a palette change in `Color.kt` flows straight through.
|
||||
|
||||
The other presets are hand-authored community palettes and use **raw hex**
|
||||
(`Color(0xFFFE8019)`), so they only import `Color` (+ `HermesStatusColors`).
|
||||
Do **not** "fix" DEFAULT to raw hex to match them — that would break the token
|
||||
linkage. The asymmetry is semantic, not sloppy.
|
||||
|
||||
## Adding a new theme
|
||||
|
||||
1. **Create** `presets/<Name>Scheme.kt` exporting the 4 tokens:
|
||||
```kotlin
|
||||
package com.m57.hermescontrol.theme.presets
|
||||
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.m57.hermescontrol.theme.HermesStatusColors
|
||||
|
||||
val MyThemeDarkColorScheme = darkColorScheme(/* … */)
|
||||
val MyThemeLightColorScheme = lightColorScheme(/* … */)
|
||||
val MyThemeDarkStatusColors = HermesStatusColors(/* … */)
|
||||
val MyThemeLightStatusColors = HermesStatusColors(/* … */)
|
||||
```
|
||||
2. **Add 2 lines** to each `when` in `Theme.kt` (`resolveColorScheme` +
|
||||
`resolveStatusColors`), importing the new tokens at the top of the file.
|
||||
3. **Add the enum entry** `MY_THEME` to `ThemePreset` in `Theme.kt`.
|
||||
4. **Wire the UI**: add the label + selection in
|
||||
`ui/settings/components/AppearanceSection.kt` (and any string resource).
|
||||
5. **Verify**:
|
||||
```bash
|
||||
./ktlint --format app/src/main/java/com/m57/hermescontrol/theme/presets/<Name>Scheme.kt
|
||||
./gradlew compileDebugKotlin
|
||||
```
|
||||
|
||||
## Conventions
|
||||
|
||||
- ktlint 1.2.1 is enforced in CI. Run `./ktlint --format` before committing.
|
||||
- Import order is ASCII-lexicographic (uppercase before lowercase).
|
||||
- Every change goes through a PR — never push directly to `main`.
|
||||
Reference in new issue
Block a user