app

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

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:
Mapp/shared/src/commonMain/kotlin/org/harvestcircle/application/ShellAvailability.kt | 2+-
Aapp/shared/src/commonMain/kotlin/org/harvestcircle/ui/shell/WorkspaceSidebar.kt | 69+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/shared/src/desktopTest/kotlin/org/harvestcircle/ui/shell/WorkspaceSidebarTest.kt | 30++++++++++++++++++++++++++++++
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) + } +}