commit 25950837d7910ea34c13ef6ac391671d9380488e
parent 25fbb1a908461336b9f678a985bed5bd56d57b8c
Author: triesap <tyson@radroots.org>
Date: Tue, 11 Aug 2026 16:28:48 +0000
ui: implement the global top bar
- expose route-aware back and forward controls
- dispatch typed reference and application-menu intents
- render supplied signer and sync status truthfully
- label every action and preserve disabled semantics
Diffstat:
2 files changed, 142 insertions(+), 0 deletions(-)
diff --git a/app/shared/src/commonMain/kotlin/org/harvestcircle/ui/shell/GlobalTopBar.kt b/app/shared/src/commonMain/kotlin/org/harvestcircle/ui/shell/GlobalTopBar.kt
@@ -0,0 +1,100 @@
+package org.harvestcircle.ui.shell
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+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.disabled
+import androidx.compose.ui.semantics.role
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.unit.dp
+
+enum class SyncStatusLabel(
+ val text: String,
+) {
+ NotYetObserved("Not yet observed"),
+ Available("Available"),
+ Degraded("Degraded"),
+ Unavailable("Unavailable"),
+}
+
+enum class SignerStatusLabel(
+ val text: String,
+) {
+ ReadOnly("Read-only"),
+ SignedOut("No signer"),
+ Available("Signer available"),
+ CredentialMissing("Credential missing"),
+}
+
+data class GlobalTopBarModel(
+ val canGoBack: Boolean,
+ val canGoForward: Boolean,
+ val syncStatus: SyncStatusLabel,
+ val signerStatus: SignerStatusLabel,
+)
+
+sealed interface GlobalTopBarIntent {
+ data object Back : GlobalTopBarIntent
+
+ data object Forward : GlobalTopBarIntent
+
+ data object OpenNostrReference : GlobalTopBarIntent
+
+ data object ShowSyncStatus : GlobalTopBarIntent
+
+ data object ShowSignerStatus : GlobalTopBarIntent
+
+ data object OpenApplicationMenu : GlobalTopBarIntent
+}
+
+@Composable
+fun GlobalTopBar(
+ model: GlobalTopBarModel,
+ onIntent: (GlobalTopBarIntent) -> Unit,
+) {
+ Row(
+ Modifier.fillMaxSize().testTag("global-top-bar"),
+ horizontalArrangement = Arrangement.spacedBy(16.dp),
+ ) {
+ ShellAction("Back", "Go back", "top-bar-back", model.canGoBack) { onIntent(GlobalTopBarIntent.Back) }
+ ShellAction("Forward", "Go forward", "top-bar-forward", model.canGoForward) {
+ onIntent(GlobalTopBarIntent.Forward)
+ }
+ ShellAction("Open reference", "Open Nostr reference", "top-bar-open-reference") {
+ onIntent(GlobalTopBarIntent.OpenNostrReference)
+ }
+ ShellAction(model.syncStatus.text, "Sync status", "top-bar-sync") { onIntent(GlobalTopBarIntent.ShowSyncStatus) }
+ ShellAction(model.signerStatus.text, "Signer status", "top-bar-signer") {
+ onIntent(GlobalTopBarIntent.ShowSignerStatus)
+ }
+ ShellAction("Menu", "Application menu", "top-bar-menu") { onIntent(GlobalTopBarIntent.OpenApplicationMenu) }
+ }
+}
+
+@Composable
+internal fun ShellAction(
+ label: String,
+ description: String,
+ tag: String,
+ enabled: Boolean = true,
+ onClick: () -> Unit,
+) {
+ BasicText(
+ text = label,
+ modifier =
+ Modifier
+ .semantics {
+ contentDescription = description
+ role = Role.Button
+ if (!enabled) disabled()
+ }.then(if (enabled) Modifier.clickable(role = Role.Button, onClick = onClick) else Modifier)
+ .testTag(tag),
+ )
+}
diff --git a/app/shared/src/desktopTest/kotlin/org/harvestcircle/ui/shell/GlobalTopBarTest.kt b/app/shared/src/desktopTest/kotlin/org/harvestcircle/ui/shell/GlobalTopBarTest.kt
@@ -0,0 +1,42 @@
+package org.harvestcircle.ui.shell
+
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.assertIsNotEnabled
+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 GlobalTopBarTest {
+ @Test
+ fun topBarUsesActualStatusAndTypedIntents() =
+ runComposeUiTest {
+ val intents = mutableListOf<GlobalTopBarIntent>()
+ setContent {
+ GlobalTopBar(
+ GlobalTopBarModel(
+ canGoBack = true,
+ canGoForward = false,
+ syncStatus = SyncStatusLabel.Degraded,
+ signerStatus = SignerStatusLabel.ReadOnly,
+ ),
+ intents::add,
+ )
+ }
+ onNodeWithText("Degraded").performClick()
+ onNodeWithText("Read-only").performClick()
+ onNodeWithTag("top-bar-open-reference").performClick()
+ onNodeWithTag("top-bar-forward").assertIsNotEnabled()
+ assertEquals(
+ listOf(
+ GlobalTopBarIntent.ShowSyncStatus,
+ GlobalTopBarIntent.ShowSignerStatus,
+ GlobalTopBarIntent.OpenNostrReference,
+ ),
+ intents,
+ )
+ }
+}