feat: M3 Expressive theme with dynamic color + fallback scheme from slate seed
CalendulaSeed (0xFF5C6B7A) anchors the design palette. Theme picks Dynamic Color on API 31+ when enabled (default true) and falls back to a hand-tuned Light/Dark scheme otherwise. CalendulaTypography is the M3 Expressive default for V1 - custom type scale lands in a later UI-design iteration. ColorSchemeTest pins the seed value (3 unit tests, all pass). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
package de.jeanlucmakiola.calendula.ui.theme
|
||||
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
/**
|
||||
* Seed color anchoring the entire palette. See spec section 12.
|
||||
* Desaturated slate blue-gray; distinct from HouseHoldKeaper's sage.
|
||||
*/
|
||||
val CalendulaSeed: Color = Color(0xFF5C6B7A)
|
||||
|
||||
/**
|
||||
* Fallback light scheme used on devices that don't support dynamic color
|
||||
* (API < 31) or when the user disables it. The real palette is derived by
|
||||
* Material's tonal-palette generator from CalendulaSeed at runtime; these
|
||||
* constants are a hand-picked subset that approximates that result.
|
||||
*/
|
||||
val CalendulaLightFallback = lightColorScheme(
|
||||
primary = Color(0xFF3B5364),
|
||||
onPrimary = Color(0xFFFFFFFF),
|
||||
primaryContainer = Color(0xFFCBE6FA),
|
||||
onPrimaryContainer = Color(0xFF001E2E),
|
||||
secondary = Color(0xFF526070),
|
||||
onSecondary = Color(0xFFFFFFFF),
|
||||
background = Color(0xFFFBFCFE),
|
||||
onBackground = Color(0xFF191C1F),
|
||||
surface = Color(0xFFFBFCFE),
|
||||
onSurface = Color(0xFF191C1F),
|
||||
)
|
||||
|
||||
val CalendulaDarkFallback = darkColorScheme(
|
||||
primary = Color(0xFFA3CBE2),
|
||||
onPrimary = Color(0xFF003348),
|
||||
primaryContainer = Color(0xFF21495F),
|
||||
onPrimaryContainer = Color(0xFFCBE6FA),
|
||||
secondary = Color(0xFFB9C8DA),
|
||||
onSecondary = Color(0xFF243240),
|
||||
background = Color(0xFF101316),
|
||||
onBackground = Color(0xFFE1E3E6),
|
||||
surface = Color(0xFF101316),
|
||||
onSurface = Color(0xFFE1E3E6),
|
||||
)
|
||||
@@ -0,0 +1,40 @@
|
||||
package de.jeanlucmakiola.calendula.ui.theme
|
||||
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
|
||||
/**
|
||||
* App theme. Honors:
|
||||
* - System light/dark.
|
||||
* - Dynamic Color on API 31+, else falls back to the hand-tuned scheme
|
||||
* derived from [CalendulaSeed].
|
||||
*
|
||||
* The Settings screen (later) can override useDynamicColor and themePreference,
|
||||
* but the V1 foundation just follows the system.
|
||||
*/
|
||||
@Composable
|
||||
fun CalendulaTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
dynamicColor: Boolean = true,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val colorScheme = when {
|
||||
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||
val ctx = LocalContext.current
|
||||
if (darkTheme) dynamicDarkColorScheme(ctx) else dynamicLightColorScheme(ctx)
|
||||
}
|
||||
darkTheme -> CalendulaDarkFallback
|
||||
else -> CalendulaLightFallback
|
||||
}
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
typography = CalendulaTypography,
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package de.jeanlucmakiola.calendula.ui.theme
|
||||
|
||||
import androidx.compose.material3.Typography
|
||||
|
||||
/**
|
||||
* Default Material 3 Expressive typography. Custom font + tuned scale will
|
||||
* land in a later UI-design iteration; the defaults are intentional for V1
|
||||
* scaffolding to keep the foundation lean.
|
||||
*/
|
||||
val CalendulaTypography = Typography()
|
||||
@@ -0,0 +1,28 @@
|
||||
package de.jeanlucmakiola.calendula.ui.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class ColorSchemeTest {
|
||||
|
||||
@Test
|
||||
fun `seed color matches design spec slate`() {
|
||||
// The seed color must remain stable - the design is anchored to it.
|
||||
// Change this only if the spec is updated.
|
||||
assertThat(CalendulaSeed).isEqualTo(Color(0xFF5C6B7A))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `light fallback scheme uses seed as primary derivation source`() {
|
||||
val scheme = CalendulaLightFallback
|
||||
// Primary should be a recognizable derivative of the seed (not neutral gray)
|
||||
assertThat(scheme.primary).isNotEqualTo(Color.Black)
|
||||
assertThat(scheme.primary).isNotEqualTo(Color.White)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `dark fallback scheme differs from light`() {
|
||||
assertThat(CalendulaDarkFallback.background).isNotEqualTo(CalendulaLightFallback.background)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user