app

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

commit d34469d6d67efe0d69f33f8ac5337c2f228087aa
parent 6bf42ac2a5962dccb2f0f64dc74e0af6e02759f1
Author: triesap <tyson@radroots.org>
Date:   Tue, 11 Aug 2026 16:32:01 +0000

ui: implement the main-panel header

- render the local title breadcrumb and status
- expose typed nested tab selection
- keep primary and secondary action slots local
- omit duplicated global navigation and status controls

Diffstat:
Aapp/shared/src/commonMain/kotlin/org/harvestcircle/ui/shell/MainPanelHeader.kt | 66++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/shared/src/desktopTest/kotlin/org/harvestcircle/ui/shell/MainPanelHeaderTest.kt | 42++++++++++++++++++++++++++++++++++++++++++
2 files changed, 108 insertions(+), 0 deletions(-)

diff --git a/app/shared/src/commonMain/kotlin/org/harvestcircle/ui/shell/MainPanelHeader.kt b/app/shared/src/commonMain/kotlin/org/harvestcircle/ui/shell/MainPanelHeader.kt @@ -0,0 +1,66 @@ +package org.harvestcircle.ui.shell + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.text.BasicText +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.role +import androidx.compose.ui.semantics.selected +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.dp + +data class MainPanelHeaderModel( + val title: String, + val breadcrumb: List<String> = emptyList(), + val localStatus: String? = null, + val tabs: List<TemplateTab> = emptyList(), + val selectedTab: TemplateSelectionKey? = null, +) { + init { + require(title.isNotBlank()) + require(breadcrumb.all(String::isNotBlank)) + require(tabs.isEmpty() == (selectedTab == null)) + require(selectedTab == null || tabs.any { it.key == selectedTab }) + } +} + +@Composable +fun MainPanelHeader( + model: MainPanelHeaderModel, + onTabSelected: (TemplateSelectionKey) -> Unit = {}, + secondaryAction: @Composable () -> Unit = {}, + primaryAction: @Composable () -> Unit = {}, +) { + Row( + Modifier.fillMaxSize().testTag("main-panel-header"), + horizontalArrangement = Arrangement.spacedBy(16.dp), + ) { + Column(Modifier.weight(1f)) { + BasicText(model.title, Modifier.semantics { contentDescription = "Screen title: ${model.title}" }.testTag("main-title")) + if (model.breadcrumb.isNotEmpty()) { + BasicText(model.breadcrumb.joinToString(" / "), Modifier.testTag("main-breadcrumb")) + } + model.localStatus?.let { BasicText(it, Modifier.testTag("main-local-status")) } + } + model.tabs.forEach { tab -> + BasicText( + tab.label, + Modifier + .semantics { + role = Role.Tab + selected = tab.key == model.selectedTab + }.clickable(role = Role.Tab) { onTabSelected(tab.key) } + .testTag("main-tab-${tab.key.value}"), + ) + } + Row(Modifier.testTag("main-secondary-action")) { secondaryAction() } + Row(Modifier.testTag("main-primary-action")) { primaryAction() } + } +} diff --git a/app/shared/src/desktopTest/kotlin/org/harvestcircle/ui/shell/MainPanelHeaderTest.kt b/app/shared/src/desktopTest/kotlin/org/harvestcircle/ui/shell/MainPanelHeaderTest.kt @@ -0,0 +1,42 @@ +package org.harvestcircle.ui.shell + +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.assertCountEquals +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsSelected +import androidx.compose.ui.test.onAllNodesWithTag +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.v2.runComposeUiTest +import kotlin.test.Test +import kotlin.test.assertEquals + +@OptIn(ExperimentalTestApi::class) +class MainPanelHeaderTest { + @Test + fun headerExposesTitleBreadcrumbStatusTabsAndActionSlots() = + runComposeUiTest { + val overview = TemplateTab(TemplateSelectionKey("overview"), "Overview") + val identity = TemplateTab(TemplateSelectionKey("identity"), "Identity") + val selections = mutableListOf<TemplateSelectionKey>() + setContent { + MainPanelHeader( + MainPanelHeaderModel( + title = "Network", + breadcrumb = listOf("Personal", "Network"), + localStatus = "Limited connection", + tabs = listOf(overview, identity), + selectedTab = overview.key, + ), + onTabSelected = selections::add, + ) + } + onNodeWithText("Network").assertIsDisplayed() + onNodeWithTag("main-breadcrumb").assertIsDisplayed() + onNodeWithTag("main-tab-overview").assertIsSelected() + onNodeWithTag("main-tab-identity").performClick() + onAllNodesWithTag("main-primary-action").assertCountEquals(1) + assertEquals(listOf(identity.key), selections) + } +}