app

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

commit 6eb5b7b534360955432904a24cba0904ce648166
parent 643337eb6af242892777fb9f4b00da642f811b18
Author: triesap <tyson@radroots.org>
Date:   Tue, 11 Aug 2026 16:25:16 +0000

ui: implement the canvas scaffold

- fix the canvas header and action regions
- center the bounded nine-hundred-sixty-dp body
- expose navigation and workflow-step slots
- enable body scrolling only for very large text

Diffstat:
Aapp/shared/src/commonMain/kotlin/org/harvestcircle/ui/shell/CanvasScaffold.kt | 68++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/shared/src/desktopTest/kotlin/org/harvestcircle/ui/shell/CanvasScaffoldTest.kt | 47+++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 115 insertions(+), 0 deletions(-)

diff --git a/app/shared/src/commonMain/kotlin/org/harvestcircle/ui/shell/CanvasScaffold.kt b/app/shared/src/commonMain/kotlin/org/harvestcircle/ui/shell/CanvasScaffold.kt @@ -0,0 +1,68 @@ +package org.harvestcircle.ui.shell + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.dp +import org.harvestcircle.design.TextSizePreference + +@Composable +fun CanvasScaffold( + textSize: TextSizePreference, + navigation: @Composable () -> Unit = {}, + header: @Composable () -> Unit, + step: @Composable () -> Unit = {}, + body: @Composable () -> Unit, + actionBar: @Composable () -> Unit, +) { + Column(Modifier.fillMaxSize().testTag("canvas-scaffold")) { + Row( + Modifier + .fillMaxWidth() + .height(64.dp) + .semantics { contentDescription = "Canvas header" } + .testTag("canvas-header"), + verticalAlignment = Alignment.CenterVertically, + ) { + Box(Modifier.testTag("canvas-navigation")) { navigation() } + Box(Modifier.weight(1f)) { header() } + Box(Modifier.testTag("canvas-step")) { step() } + } + val bodyModifier = + Modifier + .weight(1f) + .fillMaxWidth() + .let { modifier -> + if (canvasBodyScroll(textSize) == ScrollOwnership.CanvasBodyAccessibility) { + modifier.verticalScroll(rememberScrollState()) + } else { + modifier + } + }.semantics { contentDescription = "Canvas body" } + .testTag("canvas-body") + Box(bodyModifier, contentAlignment = Alignment.TopCenter) { + Box(Modifier.fillMaxWidth().widthIn(max = 960.dp)) { body() } + } + Box( + Modifier + .fillMaxWidth() + .height(72.dp) + .semantics { contentDescription = "Canvas action bar" } + .testTag("canvas-action-bar"), + ) { + actionBar() + } + } +} diff --git a/app/shared/src/desktopTest/kotlin/org/harvestcircle/ui/shell/CanvasScaffoldTest.kt b/app/shared/src/desktopTest/kotlin/org/harvestcircle/ui/shell/CanvasScaffoldTest.kt @@ -0,0 +1,47 @@ +package org.harvestcircle.ui.shell + +import androidx.compose.ui.semantics.SemanticsActions +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.assertCountEquals +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.onAllNodesWithTag +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.v2.runComposeUiTest +import org.harvestcircle.design.TextSizePreference +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +@OptIn(ExperimentalTestApi::class) +class CanvasScaffoldTest { + @Test + fun baselineCanvasKeepsHeaderBodyAndActionRegionsFixed() = + runComposeUiTest { + setContent { canvas(TextSizePreference.Default) } + onNodeWithTag("canvas-header").assertIsDisplayed() + onNodeWithTag("canvas-body").assertIsDisplayed() + onNodeWithTag("canvas-action-bar").assertIsDisplayed() + onAllNodesWithTag("canvas-navigation").assertCountEquals(1) + assertFalse(onNodeWithTag("canvas-body").fetchSemanticsNode().config.contains(SemanticsActions.ScrollBy)) + } + + @Test + fun veryLargeTextEnablesOnlyBoundedBodyScrolling() = + runComposeUiTest { + setContent { canvas(TextSizePreference.VeryLarge) } + assertTrue(onNodeWithTag("canvas-body").fetchSemanticsNode().config.contains(SemanticsActions.ScrollBy)) + onNodeWithTag("canvas-action-bar").assertIsDisplayed() + } +} + +@androidx.compose.runtime.Composable +private fun canvas(textSize: TextSizePreference) { + CanvasScaffold( + textSize = textSize, + navigation = {}, + header = {}, + step = {}, + body = {}, + actionBar = {}, + ) +}