commit 1b17496d1b4e802183bd2f639c0fa087137e8ade
parent a4fab8f9067c2e46a77386ac0cd82014404f5543
Author: triesap <tyson@radroots.org>
Date: Tue, 11 Aug 2026 16:54:06 +0000
ui: implement the foundation Today screen
- Render the locked empty Today state from actual shell session context.
- Keep circle exploration visibly disabled with its deferred explanation.
- Route Nostr reference entry through the existing typed overlay contract.
- Verify component, application, formatting, Rust, and desktop test suites.
Diffstat:
4 files changed, 112 insertions(+), 4 deletions(-)
diff --git a/app/desktop/src/test/kotlin/org/harvestcircle/application/HarvestCircleApplicationTest.kt b/app/desktop/src/test/kotlin/org/harvestcircle/application/HarvestCircleApplicationTest.kt
@@ -10,7 +10,6 @@ import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.assertCountEquals
import androidx.compose.ui.test.assertIsDisplayed
-import androidx.compose.ui.test.assertTextEquals
import androidx.compose.ui.test.onAllNodesWithTag
import androidx.compose.ui.test.onAllNodesWithText
import androidx.compose.ui.test.onNodeWithTag
@@ -81,8 +80,9 @@ class HarvestCircleApplicationTest {
onNodeWithText("Coordinate local food with clear, signed terms.").assertIsDisplayed()
onAllNodesWithText("Connect a remote signer").assertCountEquals(0)
onNodeWithTag("bootstrap-read-only").performClick()
- waitUntil { onAllNodesWithTag("foundation-route-body").fetchSemanticsNodes().isNotEmpty() }
- onNodeWithTag("foundation-route-body").assertIsDisplayed().assertTextEquals("Today")
+ waitUntil { onAllNodesWithTag("foundation-today").fetchSemanticsNodes().isNotEmpty() }
+ onNodeWithTag("foundation-today").assertIsDisplayed()
+ onNodeWithText("Read-only session").assertIsDisplayed()
}
@OptIn(ExperimentalTestApi::class)
diff --git a/app/shared/src/commonMain/kotlin/org/harvestcircle/ui/shell/FoundationTodayScreen.kt b/app/shared/src/commonMain/kotlin/org/harvestcircle/ui/shell/FoundationTodayScreen.kt
@@ -0,0 +1,47 @@
+package org.harvestcircle.ui.shell
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+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.unit.dp
+
+data class FoundationTodayModel(
+ val context: String,
+)
+
+@Composable
+fun FoundationTodayScreen(
+ model: FoundationTodayModel,
+ openNostrReference: () -> Unit,
+) {
+ SingleFocusTemplate {
+ Column(
+ Modifier.testTag("foundation-today"),
+ verticalArrangement = Arrangement.spacedBy(16.dp),
+ ) {
+ BasicText(model.context, Modifier.testTag("today-context"))
+ BasicText("No active commitments")
+ BasicText("Explore nearby buying circles or open a shared Nostr reference.")
+ Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
+ ShellAction(
+ "Explore circles",
+ "Explore circles",
+ "today-explore-circles",
+ enabled = false,
+ onClick = {},
+ )
+ ShellAction(
+ "Open a Nostr reference",
+ "Open a Nostr reference",
+ "today-open-reference",
+ onClick = openNostrReference,
+ )
+ }
+ BasicText("Not available in this build.", Modifier.testTag("today-deferred-helper"))
+ }
+ }
+}
diff --git a/app/shared/src/commonMain/kotlin/org/harvestcircle/ui/shell/HarvestCircleShell.kt b/app/shared/src/commonMain/kotlin/org/harvestcircle/ui/shell/HarvestCircleShell.kt
@@ -148,10 +148,33 @@ private fun DashboardRoot(
},
sidebar = { WorkspaceSidebar(destination) { dispatch(HarvestCircleShellIntent.Navigate(it)) } },
mainHeader = { MainPanelHeader(MainPanelHeaderModel(title = route.title())) },
- mainBody = { BasicText(route.title(), Modifier.testTag("foundation-route-body")) },
+ mainBody = {
+ when (route) {
+ AppRoute.PersonalToday ->
+ FoundationTodayScreen(
+ model = FoundationTodayModel(todayContext(state)),
+ openNostrReference = {
+ dispatch(
+ HarvestCircleShellIntent.Overlay(
+ OverlayIntent.Open(FoundationOverlay.OpenNostrReference()),
+ ),
+ )
+ },
+ )
+ else -> BasicText(route.title(), Modifier.testTag("foundation-route-body"))
+ }
+ },
)
}
+private fun todayContext(state: HarvestCircleShellState): String =
+ when {
+ state.session.readOnly -> "Read-only session"
+ state.identity.snapshot.activeIdentity != null ->
+ state.identity.snapshot.activeIdentity.identity.displayLabel
+ else -> "Signed out"
+ }
+
private fun dispatchTopBar(
intent: GlobalTopBarIntent,
dispatch: (HarvestCircleShellIntent) -> Unit,
diff --git a/app/shared/src/desktopTest/kotlin/org/harvestcircle/ui/shell/FoundationTodayScreenTest.kt b/app/shared/src/desktopTest/kotlin/org/harvestcircle/ui/shell/FoundationTodayScreenTest.kt
@@ -0,0 +1,38 @@
+package org.harvestcircle.ui.shell
+
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.assertIsNotEnabled
+import androidx.compose.ui.test.assertTextEquals
+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 FoundationTodayScreenTest {
+ @Test
+ fun readOnlyEmptyStateIsTruthfulAndReferenceActionIsTyped() =
+ runComposeUiTest {
+ var opened = 0
+ setContent {
+ FoundationTodayScreen(FoundationTodayModel("Read-only session")) { opened += 1 }
+ }
+
+ onNodeWithText("Read-only session").assertExists()
+ onNodeWithText("No active commitments").assertExists()
+ onNodeWithText("Explore nearby buying circles or open a shared Nostr reference.").assertExists()
+ onNodeWithTag("today-explore-circles").assertIsNotEnabled()
+ onNodeWithText("Not available in this build.").assertExists()
+ onNodeWithTag("today-open-reference").performClick()
+ assertEquals(1, opened)
+ }
+
+ @Test
+ fun activeContextUsesTheActualIdentityLabel() =
+ runComposeUiTest {
+ setContent { FoundationTodayScreen(FoundationTodayModel("Local grower"), openNostrReference = {}) }
+ onNodeWithTag("today-context").assertTextEquals("Local grower")
+ }
+}