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:
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))
+ }
+}