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