app

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

commit c7af77f8e33e47e8c8ebc26c752ba515ba8e6b75
parent 15bdb7b5469f37ab46ff158d695d673467ab2e16
Author: triesap <tyson@radroots.org>
Date:   Tue, 11 Aug 2026 16:21:27 +0000

ui: add shared shell layout primitives

- lock preferred and minimum shell dimensions
- define fixed regions and bounded scroll ownership
- order dashboard and canvas focus groups
- adapt the inspector and large-text body safely

Diffstat:
Aapp/shared/src/commonMain/kotlin/org/harvestcircle/ui/shell/LayoutPrimitives.kt | 87+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/shared/src/commonTest/kotlin/org/harvestcircle/ui/shell/LayoutPrimitivesTest.kt | 32++++++++++++++++++++++++++++++++
2 files changed, 119 insertions(+), 0 deletions(-)

diff --git a/app/shared/src/commonMain/kotlin/org/harvestcircle/ui/shell/LayoutPrimitives.kt b/app/shared/src/commonMain/kotlin/org/harvestcircle/ui/shell/LayoutPrimitives.kt @@ -0,0 +1,87 @@ +package org.harvestcircle.ui.shell + +import org.harvestcircle.design.TextSizePreference + +object ShellDimensions { + const val PREFERRED_WINDOW_WIDTH_DP = 1280 + const val PREFERRED_WINDOW_HEIGHT_DP = 800 + const val MINIMUM_WINDOW_WIDTH_DP = 1100 + const val MINIMUM_WINDOW_HEIGHT_DP = 720 + const val SIDEBAR_WIDTH_DP = 232 + const val GLOBAL_TOP_BAR_HEIGHT_DP = 56 + const val MAIN_HEADER_HEIGHT_DP = 56 + const val INSPECTOR_WIDTH_DP = 400 + const val MINIMUM_INSPECTOR_WIDTH_DP = 360 + const val MINIMUM_CENTER_WIDTH_DP = 560 + const val CANVAS_HEADER_HEIGHT_DP = 64 + const val CANVAS_ACTION_HEIGHT_DP = 72 + const val CANVAS_BODY_MAX_WIDTH_DP = 960 + const val DIVIDER_WIDTH_DP = 1 +} + +enum class ShellRegion { + GlobalTopBar, + Sidebar, + MainHeader, + MainBody, + Inspector, + CanvasHeader, + CanvasBody, + CanvasActionBar, +} + +enum class ScrollOwnership { None, InternalPane, CanvasBodyAccessibility } + +enum class InspectorPlacement { Hidden, Beside, Overlay } + +data class ShellLayoutRegion( + val region: ShellRegion, + val fixed: Boolean, + val focusOrder: Int, + val scrollOwnership: ScrollOwnership = ScrollOwnership.None, +) + +val dashboardRegions = + listOf( + ShellLayoutRegion(ShellRegion.GlobalTopBar, fixed = true, focusOrder = 0), + ShellLayoutRegion(ShellRegion.Sidebar, fixed = true, focusOrder = 1), + ShellLayoutRegion(ShellRegion.MainHeader, fixed = true, focusOrder = 2), + ShellLayoutRegion( + ShellRegion.MainBody, + fixed = false, + focusOrder = 3, + scrollOwnership = ScrollOwnership.InternalPane, + ), + ShellLayoutRegion( + ShellRegion.Inspector, + fixed = false, + focusOrder = 4, + scrollOwnership = ScrollOwnership.InternalPane, + ), + ) + +val canvasRegions = + listOf( + ShellLayoutRegion(ShellRegion.CanvasHeader, fixed = true, focusOrder = 0), + ShellLayoutRegion(ShellRegion.CanvasBody, fixed = false, focusOrder = 1), + ShellLayoutRegion(ShellRegion.CanvasActionBar, fixed = true, focusOrder = 2), + ) + +fun inspectorPlacement( + windowWidthDp: Int, + inspectorRequested: Boolean, +): InspectorPlacement { + if (!inspectorRequested) return InspectorPlacement.Hidden + val besideThreshold = + ShellDimensions.SIDEBAR_WIDTH_DP + + ShellDimensions.MINIMUM_CENTER_WIDTH_DP + + ShellDimensions.MINIMUM_INSPECTOR_WIDTH_DP + return if (windowWidthDp >= besideThreshold) InspectorPlacement.Beside else InspectorPlacement.Overlay +} + +fun canvasBodyScroll(textSize: TextSizePreference): ScrollOwnership = + if (textSize == TextSizePreference.VeryLarge) { + ScrollOwnership.CanvasBodyAccessibility + } else { + ScrollOwnership.None + } diff --git a/app/shared/src/commonTest/kotlin/org/harvestcircle/ui/shell/LayoutPrimitivesTest.kt b/app/shared/src/commonTest/kotlin/org/harvestcircle/ui/shell/LayoutPrimitivesTest.kt @@ -0,0 +1,32 @@ +package org.harvestcircle.ui.shell + +import org.harvestcircle.design.TextSizePreference +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class LayoutPrimitivesTest { + @Test + fun dimensionsAndFocusGroupsMatchTheLockedLayout() { + assertEquals(1280, ShellDimensions.PREFERRED_WINDOW_WIDTH_DP) + assertEquals(1100, ShellDimensions.MINIMUM_WINDOW_WIDTH_DP) + assertEquals(232, ShellDimensions.SIDEBAR_WIDTH_DP) + assertEquals(960, ShellDimensions.CANVAS_BODY_MAX_WIDTH_DP) + assertEquals(dashboardRegions.indices.toList(), dashboardRegions.map(ShellLayoutRegion::focusOrder)) + assertTrue(dashboardRegions.filter(ShellLayoutRegion::fixed).map(ShellLayoutRegion::region).contains(ShellRegion.Sidebar)) + } + + @Test + fun scrollingIsOwnedByBoundedInternalRegions() { + assertEquals(ScrollOwnership.InternalPane, dashboardRegions.single { it.region == ShellRegion.MainBody }.scrollOwnership) + assertEquals(ScrollOwnership.None, canvasBodyScroll(TextSizePreference.Default)) + assertEquals(ScrollOwnership.CanvasBodyAccessibility, canvasBodyScroll(TextSizePreference.VeryLarge)) + } + + @Test + fun inspectorMovesBesideOrOverlaysAtTheUsefulCenterBreakpoint() { + assertEquals(InspectorPlacement.Beside, inspectorPlacement(1280, inspectorRequested = true)) + assertEquals(InspectorPlacement.Overlay, inspectorPlacement(1100, inspectorRequested = true)) + assertEquals(InspectorPlacement.Hidden, inspectorPlacement(1280, inspectorRequested = false)) + } +}