app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

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:
Mapp/desktop/src/test/kotlin/org/harvestcircle/architecture/ProductNamespaceGuardTest.kt | 15+++++++++++++--
Mapp/shared/src/commonMain/kotlin/org/harvestcircle/ui/shell/HarvestCircleTheme.kt | 62++++++++++++++++++++++++++++++++++++++++++--------------------
Aapp/shared/src/commonTest/kotlin/org/harvestcircle/ui/shell/HarvestCircleThemeAdapterTest.kt | 33+++++++++++++++++++++++++++++++++
Mbuild-logic/plugins/src/functionalTest/kotlin/org/harvestcircle/buildlogic/plugins/ConventionPluginSmokeTest.kt | 22+++++++++++++++++++---
Mbuild-logic/plugins/src/main/kotlin/org/harvestcircle/buildlogic/plugins/HarvestCircleKmpSharedPlugin.kt | 1+
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())