From cdf5fc742629467ca7c962f818eb10908587ae8b Mon Sep 17 00:00:00 2001 From: farzanni <100939450+farzanni@users.noreply.github.com> Date: Wed, 22 Jul 2026 19:50:05 +0330 Subject: [PATCH] feat: use Material You colors throughout Thunderbird Use Android 12+ dynamic colors for Thunderbird Compose and legacy UI while preserving static palettes on older Android versions and other platforms. Keep Bolt-only semantic colors from the Thunderbird palette. Fixes #8340 --- app-thunderbird/build.gradle.kts | 1 + .../src/main/res/values-v31/colors.xml | 118 ++++++++++++++++++ .../bolt/theme/PlatformColorScheme.android.kt | 25 ++++ .../components/ui/bolt/theme/BoltTheme.kt | 8 +- .../ui/bolt/theme/PlatformColorScheme.kt | 11 ++ .../ui/bolt/theme/ThemeColorScheme.kt | 47 +++++++ .../theme/thunderbird/ThunderbirdBoltTheme.kt | 1 + .../ui/bolt/theme/ThemeColorSchemeTest.kt | 54 ++++++++ .../ui/bolt/theme/PlatformColorScheme.jvm.kt | 10 ++ 9 files changed, 274 insertions(+), 1 deletion(-) create mode 100644 app-thunderbird/src/main/res/values-v31/colors.xml create mode 100644 components/ui/bolt/src/androidMain/kotlin/net/thunderbird/components/ui/bolt/theme/PlatformColorScheme.android.kt create mode 100644 components/ui/bolt/src/commonMain/kotlin/net/thunderbird/components/ui/bolt/theme/PlatformColorScheme.kt create mode 100644 components/ui/bolt/src/commonTest/kotlin/net/thunderbird/components/ui/bolt/theme/ThemeColorSchemeTest.kt create mode 100644 components/ui/bolt/src/jvmMain/kotlin/net/thunderbird/components/ui/bolt/theme/PlatformColorScheme.jvm.kt diff --git a/app-thunderbird/build.gradle.kts b/app-thunderbird/build.gradle.kts index c42a2b0986d..f2226ca91ea 100644 --- a/app-thunderbird/build.gradle.kts +++ b/app-thunderbird/build.gradle.kts @@ -222,6 +222,7 @@ val fullBetaImplementation = configurations.create("fullBetaImplementation") val fullReleaseImplementation = configurations.create("fullReleaseImplementation") dependencies { + implementation(libs.android.material) implementation(projects.appCommon) implementation(projects.core.ui.compose.common) implementation(projects.core.ui.legacy.theme2.thunderbird) diff --git a/app-thunderbird/src/main/res/values-v31/colors.xml b/app-thunderbird/src/main/res/values-v31/colors.xml new file mode 100644 index 00000000000..85f6dc80e96 --- /dev/null +++ b/app-thunderbird/src/main/res/values-v31/colors.xml @@ -0,0 +1,118 @@ + + + + + + @color/m3_sys_color_dynamic_light_primary + @color/m3_sys_color_dynamic_light_on_primary + @color/m3_sys_color_dynamic_light_primary_container + @color/m3_sys_color_dynamic_light_on_primary_container + + @color/m3_sys_color_dynamic_light_secondary + @color/m3_sys_color_dynamic_light_on_secondary + @color/m3_sys_color_dynamic_light_secondary_container + @color/m3_sys_color_dynamic_light_on_secondary_container + + @color/m3_sys_color_dynamic_light_tertiary + @color/m3_sys_color_dynamic_light_on_tertiary + @color/m3_sys_color_dynamic_light_tertiary_container + @color/m3_sys_color_dynamic_light_on_tertiary_container + + @color/m3_sys_color_light_error + @color/m3_sys_color_light_on_error + @color/m3_sys_color_light_error_container + @color/m3_sys_color_light_on_error_container + + @color/m3_sys_color_dynamic_light_surface_dim + @color/m3_sys_color_dynamic_light_surface + @color/m3_sys_color_dynamic_light_surface_bright + @color/m3_sys_color_dynamic_light_on_surface + @color/m3_sys_color_dynamic_light_on_surface_variant + + @color/m3_sys_color_dynamic_light_surface_container_lowest + @color/m3_sys_color_dynamic_light_surface_container_low + @color/m3_sys_color_dynamic_light_surface_container + @color/m3_sys_color_dynamic_light_surface_container_high + @color/m3_sys_color_dynamic_light_surface_container_highest + + @color/m3_sys_color_dynamic_light_inverse_surface + @color/m3_sys_color_dynamic_light_inverse_on_surface + @color/m3_sys_color_dynamic_light_inverse_primary + + @color/m3_sys_color_dynamic_light_outline + @color/m3_sys_color_dynamic_light_outline_variant + + @color/m3_sys_color_dynamic_primary_fixed + @color/m3_sys_color_dynamic_on_primary_fixed + @color/m3_sys_color_dynamic_primary_fixed_dim + @color/m3_sys_color_dynamic_on_primary_fixed_variant + + @color/m3_sys_color_dynamic_secondary_fixed + @color/m3_sys_color_dynamic_on_secondary_fixed + @color/m3_sys_color_dynamic_secondary_fixed_dim + @color/m3_sys_color_dynamic_on_secondary_fixed_variant + + @color/m3_sys_color_dynamic_tertiary_fixed + @color/m3_sys_color_dynamic_on_tertiary_fixed + @color/m3_sys_color_dynamic_tertiary_fixed_dim + @color/m3_sys_color_dynamic_on_tertiary_fixed_variant + + + @color/m3_sys_color_dynamic_dark_primary + @color/m3_sys_color_dynamic_dark_on_primary + @color/m3_sys_color_dynamic_dark_primary_container + @color/m3_sys_color_dynamic_dark_on_primary_container + + @color/m3_sys_color_dynamic_dark_secondary + @color/m3_sys_color_dynamic_dark_on_secondary + @color/m3_sys_color_dynamic_dark_secondary_container + @color/m3_sys_color_dynamic_dark_on_secondary_container + + @color/m3_sys_color_dynamic_dark_tertiary + @color/m3_sys_color_dynamic_dark_on_tertiary + @color/m3_sys_color_dynamic_dark_tertiary_container + @color/m3_sys_color_dynamic_dark_on_tertiary_container + + @color/m3_sys_color_dark_error + @color/m3_sys_color_dark_on_error + @color/m3_sys_color_dark_error_container + @color/m3_sys_color_dark_on_error_container + + @color/m3_sys_color_dynamic_dark_surface_dim + @color/m3_sys_color_dynamic_dark_surface + @color/m3_sys_color_dynamic_dark_surface_bright + @color/m3_sys_color_dynamic_dark_on_surface + @color/m3_sys_color_dynamic_dark_on_surface_variant + + @color/m3_sys_color_dynamic_dark_surface_container_lowest + @color/m3_sys_color_dynamic_dark_surface_container_low + @color/m3_sys_color_dynamic_dark_surface_container + @color/m3_sys_color_dynamic_dark_surface_container_high + @color/m3_sys_color_dynamic_dark_surface_container_highest + + @color/m3_sys_color_dynamic_dark_inverse_surface + @color/m3_sys_color_dynamic_dark_inverse_on_surface + @color/m3_sys_color_dynamic_dark_inverse_primary + + @color/m3_sys_color_dynamic_dark_outline + @color/m3_sys_color_dynamic_dark_outline_variant + + @color/m3_sys_color_dynamic_primary_fixed + @color/m3_sys_color_dynamic_on_primary_fixed + @color/m3_sys_color_dynamic_primary_fixed_dim + @color/m3_sys_color_dynamic_on_primary_fixed_variant + + @color/m3_sys_color_dynamic_secondary_fixed + @color/m3_sys_color_dynamic_on_secondary_fixed + @color/m3_sys_color_dynamic_secondary_fixed_dim + @color/m3_sys_color_dynamic_on_secondary_fixed_variant + + @color/m3_sys_color_dynamic_tertiary_fixed + @color/m3_sys_color_dynamic_on_tertiary_fixed + @color/m3_sys_color_dynamic_tertiary_fixed_dim + @color/m3_sys_color_dynamic_on_tertiary_fixed_variant + diff --git a/components/ui/bolt/src/androidMain/kotlin/net/thunderbird/components/ui/bolt/theme/PlatformColorScheme.android.kt b/components/ui/bolt/src/androidMain/kotlin/net/thunderbird/components/ui/bolt/theme/PlatformColorScheme.android.kt new file mode 100644 index 00000000000..9f9639c2d3b --- /dev/null +++ b/components/ui/bolt/src/androidMain/kotlin/net/thunderbird/components/ui/bolt/theme/PlatformColorScheme.android.kt @@ -0,0 +1,25 @@ +package net.thunderbird.components.ui.bolt.theme + +import android.os.Build +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext + +@Composable +internal actual fun platformColorScheme( + defaultColorScheme: ThemeColorScheme, + darkTheme: Boolean, + dynamicColor: Boolean, +): ThemeColorScheme { + if (!dynamicColor || Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return defaultColorScheme + + val context = LocalContext.current + val dynamicColorScheme = if (darkTheme) { + dynamicDarkColorScheme(context) + } else { + dynamicLightColorScheme(context) + } + + return dynamicColorScheme.toBoltColorScheme(defaultColorScheme) +} diff --git a/components/ui/bolt/src/commonMain/kotlin/net/thunderbird/components/ui/bolt/theme/BoltTheme.kt b/components/ui/bolt/src/commonMain/kotlin/net/thunderbird/components/ui/bolt/theme/BoltTheme.kt index 91407423e8d..dd3cdb2a42e 100644 --- a/components/ui/bolt/src/commonMain/kotlin/net/thunderbird/components/ui/bolt/theme/BoltTheme.kt +++ b/components/ui/bolt/src/commonMain/kotlin/net/thunderbird/components/ui/bolt/theme/BoltTheme.kt @@ -11,12 +11,18 @@ import androidx.compose.runtime.ReadOnlyComposable fun BoltTheme( themeConfig: ThemeConfig, darkTheme: Boolean = isSystemInDarkTheme(), + dynamicColor: Boolean = false, content: @Composable () -> Unit, ) { - val themeColorScheme = selectThemeColorScheme( + val defaultColorScheme = selectThemeColorScheme( themeConfig = themeConfig, darkTheme = darkTheme, ) + val themeColorScheme = platformColorScheme( + defaultColorScheme = defaultColorScheme, + darkTheme = darkTheme, + dynamicColor = dynamicColor, + ) val themeImages = selectThemeImages( themeConfig = themeConfig, darkTheme = darkTheme, diff --git a/components/ui/bolt/src/commonMain/kotlin/net/thunderbird/components/ui/bolt/theme/PlatformColorScheme.kt b/components/ui/bolt/src/commonMain/kotlin/net/thunderbird/components/ui/bolt/theme/PlatformColorScheme.kt new file mode 100644 index 00000000000..21d41c857b0 --- /dev/null +++ b/components/ui/bolt/src/commonMain/kotlin/net/thunderbird/components/ui/bolt/theme/PlatformColorScheme.kt @@ -0,0 +1,11 @@ +package net.thunderbird.components.ui.bolt.theme + +import androidx.compose.runtime.Composable + +/** Uses platform-provided colors when available, otherwise returns [defaultColorScheme]. */ +@Composable +internal expect fun platformColorScheme( + defaultColorScheme: ThemeColorScheme, + darkTheme: Boolean, + dynamicColor: Boolean, +): ThemeColorScheme diff --git a/components/ui/bolt/src/commonMain/kotlin/net/thunderbird/components/ui/bolt/theme/ThemeColorScheme.kt b/components/ui/bolt/src/commonMain/kotlin/net/thunderbird/components/ui/bolt/theme/ThemeColorScheme.kt index 669275f11c7..2eccad26942 100644 --- a/components/ui/bolt/src/commonMain/kotlin/net/thunderbird/components/ui/bolt/theme/ThemeColorScheme.kt +++ b/components/ui/bolt/src/commonMain/kotlin/net/thunderbird/components/ui/bolt/theme/ThemeColorScheme.kt @@ -77,6 +77,53 @@ data class ThemeColorScheme( val onWarningContainer: Color, ) +/** + * Convert a Material 3 [ColorScheme] to a [ThemeColorScheme]. + * + * Bolt's additional semantic colors don't have Material equivalents and are retained from [fallback]. + */ +internal fun ColorScheme.toBoltColorScheme(fallback: ThemeColorScheme): ThemeColorScheme = fallback.copy( + primary = primary, + onPrimary = onPrimary, + primaryContainer = primaryContainer, + onPrimaryContainer = onPrimaryContainer, + + secondary = secondary, + onSecondary = onSecondary, + secondaryContainer = secondaryContainer, + onSecondaryContainer = onSecondaryContainer, + + tertiary = tertiary, + onTertiary = onTertiary, + tertiaryContainer = tertiaryContainer, + onTertiaryContainer = onTertiaryContainer, + + error = error, + onError = onError, + errorContainer = errorContainer, + onErrorContainer = onErrorContainer, + + surfaceDim = surfaceDim, + surface = surface, + surfaceBright = surfaceBright, + onSurface = onSurface, + onSurfaceVariant = onSurfaceVariant, + + surfaceContainerLowest = surfaceContainerLowest, + surfaceContainerLow = surfaceContainerLow, + surfaceContainer = surfaceContainer, + surfaceContainerHigh = surfaceContainerHigh, + surfaceContainerHighest = surfaceContainerHighest, + + inverseSurface = inverseSurface, + inverseOnSurface = inverseOnSurface, + inversePrimary = inversePrimary, + + outline = outline, + outlineVariant = outlineVariant, + scrim = scrim, +) + /** * Convert a [ThemeColorScheme] to a Material 3 [ColorScheme]. * diff --git a/components/ui/bolt/src/commonMain/kotlin/net/thunderbird/components/ui/bolt/theme/thunderbird/ThunderbirdBoltTheme.kt b/components/ui/bolt/src/commonMain/kotlin/net/thunderbird/components/ui/bolt/theme/thunderbird/ThunderbirdBoltTheme.kt index 8d38307054a..4891b94ce5b 100644 --- a/components/ui/bolt/src/commonMain/kotlin/net/thunderbird/components/ui/bolt/theme/thunderbird/ThunderbirdBoltTheme.kt +++ b/components/ui/bolt/src/commonMain/kotlin/net/thunderbird/components/ui/bolt/theme/thunderbird/ThunderbirdBoltTheme.kt @@ -43,6 +43,7 @@ fun ThunderbirdBoltTheme( BoltTheme( themeConfig = themeConfig, darkTheme = darkTheme, + dynamicColor = true, content = content, ) } diff --git a/components/ui/bolt/src/commonTest/kotlin/net/thunderbird/components/ui/bolt/theme/ThemeColorSchemeTest.kt b/components/ui/bolt/src/commonTest/kotlin/net/thunderbird/components/ui/bolt/theme/ThemeColorSchemeTest.kt new file mode 100644 index 00000000000..d8b360b2e9e --- /dev/null +++ b/components/ui/bolt/src/commonTest/kotlin/net/thunderbird/components/ui/bolt/theme/ThemeColorSchemeTest.kt @@ -0,0 +1,54 @@ +package net.thunderbird.components.ui.bolt.theme + +import androidx.compose.material3.darkColorScheme +import assertk.assertThat +import assertk.assertions.isEqualTo +import kotlin.test.Test +import net.thunderbird.components.ui.bolt.theme.thunderbird.lightThemeColorScheme + +class ThemeColorSchemeTest { + + @Test + fun `toBoltColorScheme should map Material color roles`() { + val fallback = lightThemeColorScheme + val materialColorScheme = darkColorScheme() + val expected = fallback.copy( + primary = materialColorScheme.primary, + onPrimary = materialColorScheme.onPrimary, + primaryContainer = materialColorScheme.primaryContainer, + onPrimaryContainer = materialColorScheme.onPrimaryContainer, + secondary = materialColorScheme.secondary, + onSecondary = materialColorScheme.onSecondary, + secondaryContainer = materialColorScheme.secondaryContainer, + onSecondaryContainer = materialColorScheme.onSecondaryContainer, + tertiary = materialColorScheme.tertiary, + onTertiary = materialColorScheme.onTertiary, + tertiaryContainer = materialColorScheme.tertiaryContainer, + onTertiaryContainer = materialColorScheme.onTertiaryContainer, + error = materialColorScheme.error, + onError = materialColorScheme.onError, + errorContainer = materialColorScheme.errorContainer, + onErrorContainer = materialColorScheme.onErrorContainer, + surfaceDim = materialColorScheme.surfaceDim, + surface = materialColorScheme.surface, + surfaceBright = materialColorScheme.surfaceBright, + onSurface = materialColorScheme.onSurface, + onSurfaceVariant = materialColorScheme.onSurfaceVariant, + surfaceContainerLowest = materialColorScheme.surfaceContainerLowest, + surfaceContainerLow = materialColorScheme.surfaceContainerLow, + surfaceContainer = materialColorScheme.surfaceContainer, + surfaceContainerHigh = materialColorScheme.surfaceContainerHigh, + surfaceContainerHighest = materialColorScheme.surfaceContainerHighest, + inverseSurface = materialColorScheme.inverseSurface, + inverseOnSurface = materialColorScheme.inverseOnSurface, + inversePrimary = materialColorScheme.inversePrimary, + outline = materialColorScheme.outline, + outlineVariant = materialColorScheme.outlineVariant, + scrim = materialColorScheme.scrim, + ) + + val result = materialColorScheme.toBoltColorScheme(fallback) + + assertThat(result).isEqualTo(expected) + } +} diff --git a/components/ui/bolt/src/jvmMain/kotlin/net/thunderbird/components/ui/bolt/theme/PlatformColorScheme.jvm.kt b/components/ui/bolt/src/jvmMain/kotlin/net/thunderbird/components/ui/bolt/theme/PlatformColorScheme.jvm.kt new file mode 100644 index 00000000000..a8b78bc6b00 --- /dev/null +++ b/components/ui/bolt/src/jvmMain/kotlin/net/thunderbird/components/ui/bolt/theme/PlatformColorScheme.jvm.kt @@ -0,0 +1,10 @@ +package net.thunderbird.components.ui.bolt.theme + +import androidx.compose.runtime.Composable + +@Composable +internal actual fun platformColorScheme( + defaultColorScheme: ThemeColorScheme, + darkTheme: Boolean, + dynamicColor: Boolean, +): ThemeColorScheme = defaultColorScheme