From dccf934172df2bb6fbfd0969a93ff9ae2f188d6a Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Sun, 28 Jun 2026 12:57:27 +0200 Subject: [PATCH] identity: shared M3 Expressive theme factory + nav slide spec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FloretExpressiveTheme(lightScheme, darkScheme, darkTheme, dynamicColor, typography) holds the family's theme mechanics — dynamic-colour resolution (API 31+), system light/dark fallback, and the STANDARD motion scheme — while each app supplies its own seed-derived colour schemes, so the look stays per-app and the machinery is shared. rememberNavSlideSpec() shares the fast spatial nav slide. android-library, Compose-only. Co-Authored-By: Claude Opus 4.8 (1M context) --- identity/build.gradle.kts | 39 ++++++++++++++ .../floret/identity/NavTransitions.kt | 19 +++++++ .../jeanlucmakiola/floret/identity/Theme.kt | 52 +++++++++++++++++++ settings.gradle.kts | 1 + 4 files changed, 111 insertions(+) create mode 100644 identity/build.gradle.kts create mode 100644 identity/src/main/kotlin/de/jeanlucmakiola/floret/identity/NavTransitions.kt create mode 100644 identity/src/main/kotlin/de/jeanlucmakiola/floret/identity/Theme.kt diff --git a/identity/build.gradle.kts b/identity/build.gradle.kts new file mode 100644 index 0000000..c21297b --- /dev/null +++ b/identity/build.gradle.kts @@ -0,0 +1,39 @@ +// identity — the family's Material 3 Expressive theme factory + shared motion. +// Holds the theme MECHANICS (dynamic-colour resolution, light/dark, the STANDARD +// motion scheme) and the nav slide spec; each app supplies its own seed-derived +// colour schemes, so the look stays per-app while the machinery is shared. +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.compose) +} + +android { + namespace = "de.jeanlucmakiola.floret.identity" + compileSdk = 37 + + defaultConfig { + minSdk = 29 + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + buildFeatures { + compose = true + } +} + +kotlin { + compilerOptions { + jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 + } +} + +dependencies { + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.ui) + implementation(libs.androidx.foundation) + implementation(libs.androidx.material3) +} diff --git a/identity/src/main/kotlin/de/jeanlucmakiola/floret/identity/NavTransitions.kt b/identity/src/main/kotlin/de/jeanlucmakiola/floret/identity/NavTransitions.kt new file mode 100644 index 0000000..ff5ce86 --- /dev/null +++ b/identity/src/main/kotlin/de/jeanlucmakiola/floret/identity/NavTransitions.kt @@ -0,0 +1,19 @@ +package de.jeanlucmakiola.floret.identity + +import androidx.compose.animation.core.FiniteAnimationSpec +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.IntOffset + +/** + * The horizontal slide spec for screen-to-screen navigation: the *fast* + * spring-physics spec from the active motion scheme — snappy with a subtle + * springy settle, rather than a fixed easing curve. Shared so the family's apps + * move the same way. Read in composable scope so the non-composable `NavHost` + * transition lambdas can capture it. + */ +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@Composable +fun rememberNavSlideSpec(): FiniteAnimationSpec = + MaterialTheme.motionScheme.fastSpatialSpec() diff --git a/identity/src/main/kotlin/de/jeanlucmakiola/floret/identity/Theme.kt b/identity/src/main/kotlin/de/jeanlucmakiola/floret/identity/Theme.kt new file mode 100644 index 0000000..c8178d9 --- /dev/null +++ b/identity/src/main/kotlin/de/jeanlucmakiola/floret/identity/Theme.kt @@ -0,0 +1,52 @@ +package de.jeanlucmakiola.floret.identity + +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.ColorScheme +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.MaterialExpressiveTheme +import androidx.compose.material3.MotionScheme +import androidx.compose.material3.Typography +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext + +/** + * The family's Material 3 Expressive theme factory. Honours system light/dark + * and Material You dynamic colour (API 31+), falling back to the app-supplied + * [lightScheme] / [darkScheme] otherwise. Routes all component + custom motion + * through `MaterialTheme.motionScheme` with the **standard** scheme — spring + * choreography without the overshoot, the family's deliberate motion choice. + * + * The mechanics are shared; the look is not. Each app keeps its identity by + * passing its own hand-tuned, seed-derived [lightScheme] / [darkScheme] and an + * optional [typography]. A thin per-app wrapper (e.g. `AgendulaTheme`) supplies + * those and forwards [darkTheme] / [dynamicColor] from settings. + */ +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@Composable +fun FloretExpressiveTheme( + lightScheme: ColorScheme, + darkScheme: ColorScheme, + darkTheme: Boolean = isSystemInDarkTheme(), + dynamicColor: Boolean = true, + typography: Typography = Typography(), + 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 -> darkScheme + else -> lightScheme + } + + MaterialExpressiveTheme( + colorScheme = colorScheme, + typography = typography, + motionScheme = MotionScheme.standard(), + content = content, + ) +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 20ee9b7..f3a32aa 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -33,3 +33,4 @@ rootProject.name = "floret-kit" include(":core-time") include(":core-crash") +include(":identity")