commit 6bf42ac2a5962dccb2f0f64dc74e0af6e02759f1
parent 25950837d7910ea34c13ef6ac391671d9380488e
Author: triesap <tyson@radroots.org>
Date: Tue, 11 Aug 2026 16:30:34 +0000
ui: implement the workspace sidebar
- render the locked workspace and personal labels
- select enabled Today Network and Settings entries
- expose deferred entries with disabled semantics
- prevent Explore Activity and farm activation
Diffstat:
3 files changed, 100 insertions(+), 1 deletion(-)
diff --git a/app/shared/src/commonMain/kotlin/org/harvestcircle/application/ShellAvailability.kt b/app/shared/src/commonMain/kotlin/org/harvestcircle/application/ShellAvailability.kt
@@ -40,7 +40,7 @@ val shellNavigationItems: List<ShellNavigationItem> =
true,
route = AppRoute.Settings(SettingsSection.Appearance),
),
- unavailable(ShellDestination.AddFarm, "Add farm"),
+ unavailable(ShellDestination.AddFarm, "Add a farm workspace"),
)
sealed interface ShellRoot {
diff --git a/app/shared/src/commonMain/kotlin/org/harvestcircle/ui/shell/WorkspaceSidebar.kt b/app/shared/src/commonMain/kotlin/org/harvestcircle/ui/shell/WorkspaceSidebar.kt
@@ -0,0 +1,69 @@
+package org.harvestcircle.ui.shell
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.layout.padding
+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.selected
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.unit.dp
+import org.harvestcircle.application.ShellDestination
+import org.harvestcircle.application.ShellNavigationItem
+import org.harvestcircle.application.shellNavigationItems
+
+@Composable
+fun WorkspaceSidebar(
+ selected: ShellDestination,
+ onDestination: (ShellDestination) -> Unit,
+) {
+ Column(Modifier.testTag("workspace-sidebar")) {
+ BasicText("Workspace", Modifier.testTag("workspace-label"))
+ BasicText("Personal", Modifier.testTag("workspace-personal"))
+ shellNavigationItems.forEach { item ->
+ SidebarItem(item, selected == item.destination, onDestination)
+ }
+ }
+}
+
+@Composable
+private fun SidebarItem(
+ item: ShellNavigationItem,
+ selected: Boolean,
+ onDestination: (ShellDestination) -> Unit,
+) {
+ val description =
+ if (item.enabled) {
+ item.label
+ } else {
+ "${item.label}. ${item.unavailableExplanation}"
+ }
+ BasicText(
+ text = item.label,
+ modifier =
+ Modifier
+ .fillMaxWidth()
+ .heightIn(min = 44.dp)
+ .semantics {
+ contentDescription = description
+ role = Role.Tab
+ this.selected = selected
+ if (!item.enabled) disabled()
+ }.then(
+ if (item.enabled) {
+ Modifier.clickable(role = Role.Tab) { onDestination(item.destination) }
+ } else {
+ Modifier
+ },
+ ).padding(horizontal = 16.dp, vertical = 12.dp)
+ .testTag("sidebar-${item.destination.name}"),
+ )
+}
diff --git a/app/shared/src/desktopTest/kotlin/org/harvestcircle/ui/shell/WorkspaceSidebarTest.kt b/app/shared/src/desktopTest/kotlin/org/harvestcircle/ui/shell/WorkspaceSidebarTest.kt
@@ -0,0 +1,30 @@
+package org.harvestcircle.ui.shell
+
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.assertIsNotEnabled
+import androidx.compose.ui.test.assertIsSelected
+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 org.harvestcircle.application.ShellDestination
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+@OptIn(ExperimentalTestApi::class)
+class WorkspaceSidebarTest {
+ @Test
+ fun sidebarSelectsFoundationRoutesAndDisablesDeferredEntries() =
+ runComposeUiTest {
+ val selected = mutableListOf<ShellDestination>()
+ setContent { WorkspaceSidebar(ShellDestination.Today, selected::add) }
+ onNodeWithTag("sidebar-Today").assertIsSelected()
+ onNodeWithText("Explore").assertIsNotEnabled()
+ onNodeWithText("Activity").assertIsNotEnabled()
+ onNodeWithText("Add a farm workspace").assertIsNotEnabled()
+ onNodeWithText("Explore").performClick()
+ onNodeWithText("Network").performClick()
+ onNodeWithText("Settings").performClick()
+ assertEquals(listOf(ShellDestination.Network, ShellDestination.Settings), selected)
+ }
+}