app

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

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:
Aapp/shared/src/commonMain/kotlin/org/harvestcircle/ui/shell/GlobalTopBar.kt | 100+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/shared/src/desktopTest/kotlin/org/harvestcircle/ui/shell/GlobalTopBarTest.kt | 42++++++++++++++++++++++++++++++++++++++++++
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, + ) + } +}