commit 8b2a6fca1487f12c94e96056c728efe7d4a3a3a3
parent 4fd184d71d22f61512773f02d47eb26decfc6075
Author: triesap <tyson@radroots.org>
Date: Thu, 13 Aug 2026 15:45:41 +0000
ui: install the owned appearance adapter
- map application appearance into the bounded design theme configuration
- remove duplicate density scaling while preserving current shell behavior
- declare and fixture the one-way shared-to-design dependency
- align duplicate namespace tests with governed provenance ownership
Diffstat:
5 files changed, 108 insertions(+), 25 deletions(-)
diff --git a/app/desktop/src/test/kotlin/org/harvestcircle/architecture/ProductNamespaceGuardTest.kt b/app/desktop/src/test/kotlin/org/harvestcircle/architecture/ProductNamespaceGuardTest.kt
@@ -94,6 +94,8 @@ class ProductNamespaceGuardTest {
val temporaryPath = temporaryNamespace.replace('.', '/')
val repositoryUrlException = "https://github.com/radrootslabs/" + legacyProduct + "_app"
val provenanceException = "core/provenance/" + legacyProduct + "-import-v1.toml"
+ val designProvenanceException = "config/design/source_baseline_v1.toml"
+ val designAuditException = "tools/xtask/src/lib.rs"
val textExtensions =
setOf(
"gradle",
@@ -117,7 +119,11 @@ class ProductNamespaceGuardTest {
trackedFiles(root).flatMap { relative ->
buildList {
val normalizedRelative = relative.lowercase()
- if (relative != provenanceException && normalizedRelative.contains(legacyProduct)) {
+ if (
+ relative != provenanceException &&
+ relative != designProvenanceException &&
+ normalizedRelative.contains(legacyProduct)
+ ) {
add("$relative: legacy product name in tracked path")
}
if (normalizedRelative.contains(temporaryPath)) {
@@ -125,7 +131,12 @@ class ProductNamespaceGuardTest {
}
val path = root.resolve(relative)
- if (relative != provenanceException && (path.extension in textExtensions || path.name in textNames)) {
+ if (
+ relative != provenanceException &&
+ relative != designProvenanceException &&
+ relative != designAuditException &&
+ (path.extension in textExtensions || path.name in textNames)
+ ) {
var inspected = path.readText().replace(repositoryUrlException, "")
inspected =
inspected
diff --git a/app/shared/src/commonMain/kotlin/org/harvestcircle/ui/shell/HarvestCircleTheme.kt b/app/shared/src/commonMain/kotlin/org/harvestcircle/ui/shell/HarvestCircleTheme.kt
@@ -1,23 +1,26 @@
package org.harvestcircle.ui.shell
-import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.staticCompositionLocalOf
-import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.unit.Density
import org.harvestcircle.design.AppearanceState
import org.harvestcircle.design.ColorToken
import org.harvestcircle.design.HarvestCircleDesign
import org.harvestcircle.design.HarvestCirclePalette
import org.harvestcircle.design.MotionPreference
+import org.harvestcircle.design.TextSizePreference
import org.harvestcircle.design.ThemePreference
import org.harvestcircle.design.TypographyToken
+import org.harvestcircle.designsystem.theme.HarvestCircleContrast
+import org.harvestcircle.designsystem.theme.HarvestCircleDensity
+import org.harvestcircle.designsystem.theme.HarvestCircleDesignTheme
+import org.harvestcircle.designsystem.theme.HarvestCircleInputMode
+import org.harvestcircle.designsystem.theme.HarvestCircleMotionMode
+import org.harvestcircle.designsystem.theme.HarvestCircleTextScale
+import org.harvestcircle.designsystem.theme.HarvestCircleThemeConfig
+import org.harvestcircle.designsystem.theme.HarvestCircleThemeMode
enum class ResolvedTheme { Light, Dark }
@@ -80,6 +83,32 @@ fun harvestCircleThemeTokens(
)
}
+fun harvestCircleDesignThemeConfig(
+ appearance: AppearanceState,
+ systemDark: Boolean,
+): HarvestCircleThemeConfig =
+ HarvestCircleThemeConfig(
+ mode =
+ when (resolveTheme(appearance.theme, systemDark)) {
+ ResolvedTheme.Light -> HarvestCircleThemeMode.Light
+ ResolvedTheme.Dark -> HarvestCircleThemeMode.Dark
+ },
+ contrast = HarvestCircleContrast.Standard,
+ density = HarvestCircleDensity.Comfortable,
+ motion =
+ when (appearance.motion) {
+ MotionPreference.Standard -> HarvestCircleMotionMode.Full
+ MotionPreference.Reduced -> HarvestCircleMotionMode.Reduced
+ },
+ inputMode = HarvestCircleInputMode.Pointer,
+ textScale =
+ when (appearance.textSize) {
+ TextSizePreference.Default -> HarvestCircleTextScale.Standard
+ TextSizePreference.Large -> HarvestCircleTextScale.Large
+ TextSizePreference.VeryLarge -> HarvestCircleTextScale.ExtraLarge
+ },
+ )
+
@Composable
fun HarvestCircleTheme(
appearance: AppearanceState,
@@ -87,20 +116,13 @@ fun HarvestCircleTheme(
content: @Composable () -> Unit,
) {
val tokens = harvestCircleThemeTokens(appearance, systemDark)
- val baseDensity = LocalDensity.current
- val scaledDensity = Density(baseDensity.density, baseDensity.fontScale * tokens.textScale)
- CompositionLocalProvider(
- LocalShellAppearance provides appearance,
- LocalHarvestCirclePalette provides tokens.palette,
- LocalHarvestCircleTypography provides tokens.typography,
- LocalHarvestCircleShapes provides tokens.shapes,
- LocalHarvestCircleMotion provides tokens.motion,
- LocalDensity provides scaledDensity,
- ) {
- Box(
- Modifier
- .fillMaxSize()
- .background(tokens.palette.background.toComposeColor()),
+ HarvestCircleDesignTheme(config = harvestCircleDesignThemeConfig(appearance, systemDark)) {
+ CompositionLocalProvider(
+ LocalShellAppearance provides appearance,
+ LocalHarvestCirclePalette provides tokens.palette,
+ LocalHarvestCircleTypography provides tokens.typography,
+ LocalHarvestCircleShapes provides tokens.shapes,
+ LocalHarvestCircleMotion provides tokens.motion,
) {
content()
}
diff --git a/app/shared/src/commonTest/kotlin/org/harvestcircle/ui/shell/HarvestCircleThemeAdapterTest.kt b/app/shared/src/commonTest/kotlin/org/harvestcircle/ui/shell/HarvestCircleThemeAdapterTest.kt
@@ -0,0 +1,33 @@
+package org.harvestcircle.ui.shell
+
+import org.harvestcircle.design.AppearanceState
+import org.harvestcircle.design.MotionPreference
+import org.harvestcircle.design.TextSizePreference
+import org.harvestcircle.design.ThemePreference
+import org.harvestcircle.designsystem.theme.HarvestCircleInputMode
+import org.harvestcircle.designsystem.theme.HarvestCircleMotionMode
+import org.harvestcircle.designsystem.theme.HarvestCircleTextScale
+import org.harvestcircle.designsystem.theme.HarvestCircleThemeMode
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+class HarvestCircleThemeAdapterTest {
+ @Test
+ fun applicationAppearanceMapsToBoundedDesignConfiguration() {
+ val config =
+ harvestCircleDesignThemeConfig(
+ appearance =
+ AppearanceState(
+ theme = ThemePreference.System,
+ textSize = TextSizePreference.VeryLarge,
+ motion = MotionPreference.Reduced,
+ ),
+ systemDark = true,
+ )
+
+ assertEquals(HarvestCircleThemeMode.Dark, config.mode)
+ assertEquals(HarvestCircleTextScale.ExtraLarge, config.textScale)
+ assertEquals(HarvestCircleMotionMode.Reduced, config.motion)
+ assertEquals(HarvestCircleInputMode.Pointer, config.inputMode)
+ }
+}
diff --git a/build-logic/plugins/src/functionalTest/kotlin/org/harvestcircle/buildlogic/plugins/ConventionPluginSmokeTest.kt b/build-logic/plugins/src/functionalTest/kotlin/org/harvestcircle/buildlogic/plugins/ConventionPluginSmokeTest.kt
@@ -40,6 +40,9 @@ class ConventionPluginSmokeTest {
if (pluginId == "org.harvestcircle.build.design-system") {
append("include(\":app:design_system\")\n")
}
+ if (pluginId == "org.harvestcircle.build.kmp-shared") {
+ append("include(\":app:design_system\", \":app:shared\")\n")
+ }
if (pluginId == "org.harvestcircle.build.design-catalog") {
append("include(\":app:design_system\", \":tools:design_catalog\")\n")
}
@@ -60,6 +63,12 @@ class ConventionPluginSmokeTest {
} else if (pluginId == "org.harvestcircle.build.design-system") {
fixture.resolve("app/design_system").createDirectories()
.resolve("build.gradle.kts")
+ } else if (pluginId == "org.harvestcircle.build.kmp-shared") {
+ fixture.resolve("app/design_system").createDirectories()
+ .resolve("build.gradle.kts")
+ .writeText("plugins { `java-library` }\n")
+ fixture.resolve("app/shared").createDirectories()
+ .resolve("build.gradle.kts")
} else if (pluginId == "org.harvestcircle.build.design-catalog") {
fixture.resolve("app/design_system").createDirectories()
.resolve("build.gradle.kts")
@@ -160,11 +169,16 @@ class ConventionPluginSmokeTest {
fun sharedPluginRejectsPlatformDependenciesFromCommonSources() {
val fixture = createTempDirectory("harvestcircle-shared-plugin-")
fixture.resolve("settings.gradle.kts").writeText(
- "pluginManagement { repositories { gradlePluginPortal(); mavenCentral() } }\nrootProject.name = \"fixture\"\n",
+ "pluginManagement { repositories { gradlePluginPortal(); mavenCentral() } }\n" +
+ "rootProject.name = \"fixture\"\n" +
+ "include(\":app:design_system\", \":app:shared\")\n",
)
fixture.resolve("gradle").createDirectories().resolve("libs.versions.toml").writeText(kmpCatalog)
- fixture.resolve("build.gradle.kts").writeText("plugins { id(\"org.harvestcircle.build.kmp-shared\") }\n")
- fixture.resolve("src/commonMain/kotlin").createDirectories().resolve("Leak.kt").writeText(
+ fixture.resolve("app/design_system").createDirectories().resolve("build.gradle.kts")
+ .writeText("plugins { `java-library` }\n")
+ fixture.resolve("app/shared").createDirectories().resolve("build.gradle.kts")
+ .writeText("plugins { id(\"org.harvestcircle.build.kmp-shared\") }\n")
+ fixture.resolve("app/shared/src/commonMain/kotlin").createDirectories().resolve("Leak.kt").writeText(
"package fixture\nimport org.harvestcircle.ffi.BuildInfoDto\n",
)
@@ -516,6 +530,8 @@ class ConventionPluginSmokeTest {
coroutines = "1.9.0"
[libraries]
+ compose-animation = { module = "org.jetbrains.compose.animation:animation", version.ref = "compose" }
+ compose-components-resources = { module = "org.jetbrains.compose.components:components-resources", version.ref = "compose" }
compose-foundation = { module = "org.jetbrains.compose.foundation:foundation", version.ref = "compose" }
compose-runtime = { module = "org.jetbrains.compose.runtime:runtime", version.ref = "compose" }
compose-ui = { module = "org.jetbrains.compose.ui:ui", version.ref = "compose" }
diff --git a/build-logic/plugins/src/main/kotlin/org/harvestcircle/buildlogic/plugins/HarvestCircleKmpSharedPlugin.kt b/build-logic/plugins/src/main/kotlin/org/harvestcircle/buildlogic/plugins/HarvestCircleKmpSharedPlugin.kt
@@ -12,6 +12,7 @@ public class HarvestCircleKmpSharedPlugin : Plugin<Project> {
val kotlinVersion = catalog.findVersion("kotlin").get().requiredVersion
val composeVersion = catalog.findVersion("compose").get().requiredVersion
+ target.dependencies.add("commonMainImplementation", target.project(":app:design_system"))
target.dependencies.add("commonMainImplementation", catalog.findLibrary("compose-foundation").get())
target.dependencies.add("commonMainImplementation", catalog.findLibrary("compose-runtime").get())
target.dependencies.add("commonMainImplementation", catalog.findLibrary("compose-ui").get())