app

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

commit 26b8653fd6ef466ae0233f2a2dfd8eb4d7b0e3a5
parent 8d0b76b1553c205a37ba50be5014f5b036ae4c5d
Author: triesap <tyson@radroots.org>
Date:   Thu, 13 Aug 2026 15:56:59 +0000

ui: migrate the HarvestCircle dashboard chrome

- compose dashboard regions with the owned adaptive application frame
- replace template toolbar and sidebar controls with design components
- expose real settings, build, source, and licence menu actions
- verify dashboard geometry, navigation, status, and menu behavior

Diffstat:
Mapp/design_system/src/commonMain/kotlin/org/harvestcircle/designsystem/component/menu/HarvestCirclePopupButton.kt | 6++++--
Mapp/shared/src/commonMain/kotlin/org/harvestcircle/ui/shell/DashboardScaffold.kt | 73++++++++++++++++++++++++++++++++++++++++++++++---------------------------
Mapp/shared/src/commonMain/kotlin/org/harvestcircle/ui/shell/GlobalTopBar.kt | 126+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
Mapp/shared/src/commonMain/kotlin/org/harvestcircle/ui/shell/HarvestCircleShell.kt | 51++++++++++++++++++++++++++++++++++++---------------
Mapp/shared/src/commonMain/kotlin/org/harvestcircle/ui/shell/MainPanelHeader.kt | 60+++++++++++++++++++++++++++++++++++++++++++-----------------
Mapp/shared/src/commonMain/kotlin/org/harvestcircle/ui/shell/WorkspaceSidebar.kt | 77++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
Mapp/shared/src/desktopTest/kotlin/org/harvestcircle/ui/shell/GlobalTopBarTest.kt | 3+++
Mapp/shared/src/desktopTest/kotlin/org/harvestcircle/ui/shell/WorkspaceSidebarTest.kt | 5-----
8 files changed, 283 insertions(+), 118 deletions(-)

diff --git a/app/design_system/src/commonMain/kotlin/org/harvestcircle/designsystem/component/menu/HarvestCirclePopupButton.kt b/app/design_system/src/commonMain/kotlin/org/harvestcircle/designsystem/component/menu/HarvestCirclePopupButton.kt @@ -90,6 +90,8 @@ public fun <T> HarvestCirclePopupButton( options: List<HarvestCircleMenuOption<T>>, onValueChange: (T) -> Unit, modifier: Modifier = Modifier, + buttonLabel: String? = null, + showSelection: Boolean = true, enabled: Boolean = true, size: HarvestCircleControlSize = HarvestCircleControlSize.Medium, focusRing: HarvestCircleFocusRing = HarvestCircleFocusRing.WhenFocused, @@ -159,7 +161,7 @@ public fun <T> HarvestCirclePopupButton( focusRing = focusRing, ) { HarvestCircleText( - text = selected.label, + text = buttonLabel ?: selected.label, role = HarvestCircleTextRole.Label, tone = HarvestCircleContentTone.Inherit, maxLines = 1, @@ -291,7 +293,7 @@ public fun <T> HarvestCirclePopupButton( modifier = Modifier.width(HarvestCircleTheme.shell.dimensions.iconSmall), contentAlignment = Alignment.Center, ) { - if (selectedOption) { + if (selectedOption && showSelection) { HarvestCircleIcon( resource = HarvestCircleIcons.Check, contentDescription = null, diff --git a/app/shared/src/commonMain/kotlin/org/harvestcircle/ui/shell/DashboardScaffold.kt b/app/shared/src/commonMain/kotlin/org/harvestcircle/ui/shell/DashboardScaffold.kt @@ -1,21 +1,22 @@ package org.harvestcircle.ui.shell -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.width import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp +import org.harvestcircle.designsystem.layout.HarvestCircleAppFrame +import org.harvestcircle.designsystem.layout.HarvestCirclePaneSlot +import org.harvestcircle.designsystem.layout.HarvestCirclePaneWidth +import org.harvestcircle.designsystem.primitive.HarvestCircleSurface +import org.harvestcircle.designsystem.primitive.HarvestCircleSurfaceRole import kotlin.math.roundToInt @Composable @@ -29,30 +30,53 @@ fun DashboardScaffold( ) { BoxWithConstraints(Modifier.fillMaxSize().testTag("dashboard-scaffold")) { val placement = inspectorPlacement(maxWidth.value.roundToInt(), inspectorVisible) - val palette = LocalHarvestCirclePalette.current - Column(Modifier.fillMaxSize().background(palette.background.toComposeColor())) { - Region("dashboard-top-bar", "Global top bar", Modifier.fillMaxWidth().height(56.dp), topBar) - Row(Modifier.fillMaxSize()) { - Region("dashboard-sidebar", "Workspace sidebar", Modifier.width(232.dp).fillMaxHeight(), sidebar) - Column(Modifier.weight(1f).fillMaxHeight()) { - Region("dashboard-main-header", "Main panel header", Modifier.fillMaxWidth().height(56.dp), mainHeader) - Region("dashboard-main-body", "Main panel body", Modifier.fillMaxSize(), mainBody) - } - if (placement == InspectorPlacement.Beside) { - Region("dashboard-inspector-beside", "Inspector", Modifier.width(400.dp).fillMaxHeight(), inspector) - } + val inspectorPane = + if (placement == InspectorPlacement.Beside) { + HarvestCirclePaneSlot( + width = HarvestCirclePaneWidth.Inspector, + header = {}, + content = { + DashboardRegion("dashboard-inspector-beside", "Inspector", Modifier.fillMaxSize(), inspector) + }, + ) + } else { + null } - } + + HarvestCircleAppFrame( + sidebarCollapsed = false, + sidebar = { + DashboardRegion("dashboard-sidebar", "Workspace sidebar", Modifier.fillMaxSize(), sidebar) + }, + topBar = { + DashboardRegion("dashboard-top-bar", "Global top bar", Modifier.fillMaxSize(), topBar) + }, + mainHeader = { + DashboardRegion("dashboard-main-header", "Main panel header", Modifier.fillMaxSize(), mainHeader) + }, + utilityPane = inspectorPane, + mainContent = { + DashboardRegion("dashboard-main-body", "Main panel body", Modifier.fillMaxSize(), mainBody) + }, + ) + if (placement == InspectorPlacement.Overlay) { - Box(Modifier.fillMaxSize(), contentAlignment = androidx.compose.ui.Alignment.CenterEnd) { - Region("dashboard-inspector-overlay", "Inspector", Modifier.width(360.dp).fillMaxHeight(), inspector) + HarvestCircleSurface( + modifier = + Modifier + .width(ShellDimensions.MINIMUM_INSPECTOR_WIDTH_DP.dp) + .fillMaxHeight() + .align(Alignment.CenterEnd), + role = HarvestCircleSurfaceRole.Overlay, + ) { + DashboardRegion("dashboard-inspector-overlay", "Inspector", Modifier.fillMaxSize(), inspector) } } } } @Composable -private fun Region( +private fun DashboardRegion( tag: String, label: String, modifier: Modifier, @@ -61,12 +85,7 @@ private fun Region( Box( modifier = modifier - .background( - when (tag) { - "dashboard-main-body" -> LocalHarvestCirclePalette.current.background.toComposeColor() - else -> LocalHarvestCirclePalette.current.surface.toComposeColor() - }, - ).semantics { contentDescription = label } + .semantics { contentDescription = label } .testTag(tag), ) { content() 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 @@ -1,15 +1,25 @@ package org.harvestcircle.ui.shell -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag -import androidx.compose.ui.unit.dp +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics import org.harvestcircle.application.ShellFocusTarget import org.harvestcircle.application.SignerStatusLabel import org.harvestcircle.application.SyncStatusLabel +import org.harvestcircle.designsystem.component.HarvestCircleButtonVariant +import org.harvestcircle.designsystem.component.HarvestCircleControlSize +import org.harvestcircle.designsystem.component.HarvestCircleTextRole +import org.harvestcircle.designsystem.component.action.HarvestCircleButton +import org.harvestcircle.designsystem.component.action.HarvestCircleIconButton +import org.harvestcircle.designsystem.component.menu.HarvestCircleMenuOption +import org.harvestcircle.designsystem.component.menu.HarvestCirclePopupButton +import org.harvestcircle.designsystem.icon.HarvestCircleIcons +import org.harvestcircle.designsystem.layout.HarvestCircleToolbar +import org.harvestcircle.designsystem.primitive.HarvestCircleText data class GlobalTopBarModel( val canGoBack: Boolean, @@ -18,6 +28,15 @@ data class GlobalTopBarModel( val signerStatus: SignerStatusLabel, ) +enum class ApplicationMenuAction( + val label: String, +) { + Settings("Settings"), + AboutBuild("About this build"), + Source("Source"), + Licence("Licence"), +} + sealed interface GlobalTopBarIntent { data object Back : GlobalTopBarIntent @@ -29,7 +48,9 @@ sealed interface GlobalTopBarIntent { data object ShowSignerStatus : GlobalTopBarIntent - data object OpenApplicationMenu : GlobalTopBarIntent + data class SelectApplicationMenu( + val action: ApplicationMenuAction, + ) : GlobalTopBarIntent } @Composable @@ -37,37 +58,80 @@ 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 a Nostr reference", - "Open a Nostr reference", - "top-bar-open-reference", - modifier = Modifier.shellFocusTarget(ShellFocusTarget.TopBarReference), + HarvestCircleToolbar(Modifier.fillMaxSize().testTag("global-top-bar")) { + HarvestCircleIconButton( + onClick = { onIntent(GlobalTopBarIntent.Back) }, + icon = HarvestCircleIcons.ChevronLeft, + label = "Go back", + modifier = Modifier.testTag("top-bar-back"), + size = HarvestCircleControlSize.Small, + enabled = model.canGoBack, + ) + HarvestCircleIconButton( + onClick = { onIntent(GlobalTopBarIntent.Forward) }, + icon = HarvestCircleIcons.ChevronRight, + label = "Go forward", + modifier = Modifier.testTag("top-bar-forward"), + size = HarvestCircleControlSize.Small, + enabled = model.canGoForward, + ) + HarvestCircleButton( + onClick = { onIntent(GlobalTopBarIntent.OpenNostrReference) }, + modifier = + Modifier + .shellFocusTarget(ShellFocusTarget.TopBarReference) + .semantics { contentDescription = "Open a Nostr reference" } + .testTag("top-bar-open-reference"), + variant = HarvestCircleButtonVariant.Ghost, + size = HarvestCircleControlSize.Small, + leadingIcon = { + org.harvestcircle.designsystem.primitive.HarvestCircleIcon( + resource = HarvestCircleIcons.Search, + contentDescription = null, + ) + }, ) { - onIntent(GlobalTopBarIntent.OpenNostrReference) + HarvestCircleText("Open a Nostr reference", role = HarvestCircleTextRole.Label) } - ShellAction( - model.syncStatus.text, - "Sync status", - "top-bar-sync", - modifier = Modifier.shellFocusTarget(ShellFocusTarget.TopBarSync), - ) { onIntent(GlobalTopBarIntent.ShowSyncStatus) } - ShellAction( - model.signerStatus.text, - "Signer status", - "top-bar-signer", - modifier = Modifier.shellFocusTarget(ShellFocusTarget.TopBarSigner), - ) { + + Spacer(Modifier.weight(1f)) + StatusAction(model.syncStatus.text, "Sync status", "top-bar-sync") { + onIntent(GlobalTopBarIntent.ShowSyncStatus) + } + StatusAction(model.signerStatus.text, "Signer status", "top-bar-signer") { onIntent(GlobalTopBarIntent.ShowSignerStatus) } - ShellAction("Menu", "Application menu", "top-bar-menu") { onIntent(GlobalTopBarIntent.OpenApplicationMenu) } + HarvestCirclePopupButton( + selectedValue = ApplicationMenuAction.Settings, + options = ApplicationMenuAction.entries.map { HarvestCircleMenuOption(it, it.label) }, + onValueChange = { onIntent(GlobalTopBarIntent.SelectApplicationMenu(it)) }, + modifier = Modifier.testTag("top-bar-menu"), + buttonLabel = "Menu", + showSelection = false, + size = HarvestCircleControlSize.Small, + ) + } +} + +@Composable +private fun StatusAction( + label: String, + description: String, + tag: String, + onClick: () -> Unit, +) { + HarvestCircleButton( + onClick = onClick, + modifier = + Modifier + .shellFocusTarget( + if (tag == "top-bar-sync") ShellFocusTarget.TopBarSync else ShellFocusTarget.TopBarSigner, + ).semantics { contentDescription = description } + .testTag(tag), + variant = HarvestCircleButtonVariant.Ghost, + size = HarvestCircleControlSize.Small, + ) { + HarvestCircleText(label, role = HarvestCircleTextRole.Label) } } 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 @@ -28,6 +28,7 @@ import org.harvestcircle.identities.ui.toUiModel import org.harvestcircle.navigation.AppRoute import org.harvestcircle.navigation.BootstrapStep import org.harvestcircle.navigation.NavigationIntent +import org.harvestcircle.navigation.SettingsSection import org.harvestcircle.product.ScreenKey @Composable @@ -179,7 +180,7 @@ private fun DashboardRoot( syncStatus = status.sync, signerStatus = status.signer, ), - onIntent = { intent -> dispatchTopBar(intent, dispatch) }, + onIntent = { intent -> dispatchTopBar(intent, platformActions, dispatch) }, ) }, sidebar = { WorkspaceSidebar(route.screenKey) { dispatch(HarvestCircleShellIntent.Navigate(it)) } }, @@ -265,26 +266,46 @@ private fun todayContext(state: HarvestCircleShellState): String = private fun dispatchTopBar( intent: GlobalTopBarIntent, + platformActions: HarvestCirclePlatformActions, dispatch: (HarvestCircleShellIntent) -> Unit, ) { - val shellIntent = - when (intent) { - GlobalTopBarIntent.Back -> HarvestCircleShellIntent.Navigation(NavigationIntent.Back) - GlobalTopBarIntent.Forward -> HarvestCircleShellIntent.Navigation(NavigationIntent.Forward) - GlobalTopBarIntent.OpenNostrReference -> - HarvestCircleShellIntent.Overlay(OverlayIntent.OpenReference(ShellFocusTarget.TopBarReference)) - GlobalTopBarIntent.ShowSyncStatus -> + when (intent) { + GlobalTopBarIntent.Back -> dispatch(HarvestCircleShellIntent.Navigation(NavigationIntent.Back)) + GlobalTopBarIntent.Forward -> dispatch(HarvestCircleShellIntent.Navigation(NavigationIntent.Forward)) + GlobalTopBarIntent.OpenNostrReference -> + dispatch(HarvestCircleShellIntent.Overlay(OverlayIntent.OpenReference(ShellFocusTarget.TopBarReference))) + GlobalTopBarIntent.ShowSyncStatus -> + dispatch( HarvestCircleShellIntent.Overlay( OverlayIntent.Open(FoundationOverlay.Status(StatusOverlayKey.Sync), ShellFocusTarget.TopBarSync), - ) - GlobalTopBarIntent.ShowSignerStatus -> + ), + ) + GlobalTopBarIntent.ShowSignerStatus -> + dispatch( HarvestCircleShellIntent.Overlay( OverlayIntent.Open(FoundationOverlay.Status(StatusOverlayKey.Signer), ShellFocusTarget.TopBarSigner), - ) - GlobalTopBarIntent.OpenApplicationMenu -> - HarvestCircleShellIntent.Navigate(ScreenKey.Settings) - } - dispatch(shellIntent) + ), + ) + is GlobalTopBarIntent.SelectApplicationMenu -> + when (intent.action) { + ApplicationMenuAction.Settings -> openSettingsSection(SettingsSection.Appearance, dispatch) + ApplicationMenuAction.AboutBuild -> openSettingsSection(SettingsSection.Project, dispatch) + ApplicationMenuAction.Source -> platformActions.openSource() + ApplicationMenuAction.Licence -> platformActions.openLicence() + } + } +} + +private fun openSettingsSection( + section: SettingsSection, + dispatch: (HarvestCircleShellIntent) -> Unit, +) { + dispatch(HarvestCircleShellIntent.Navigate(ScreenKey.Settings)) + dispatch( + HarvestCircleShellIntent.Navigation( + NavigationIntent.SelectSettingsSection(section), + ), + ) } private fun AppRoute.title(): String = diff --git a/app/shared/src/commonMain/kotlin/org/harvestcircle/ui/shell/MainPanelHeader.kt b/app/shared/src/commonMain/kotlin/org/harvestcircle/ui/shell/MainPanelHeader.kt @@ -4,12 +4,19 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.unit.dp +import org.harvestcircle.designsystem.component.HarvestCircleContentTone +import org.harvestcircle.designsystem.component.HarvestCircleTextRole +import org.harvestcircle.designsystem.component.navigation.HarvestCircleTab +import org.harvestcircle.designsystem.component.navigation.HarvestCircleTabRow +import org.harvestcircle.designsystem.primitive.HarvestCircleText +import org.harvestcircle.designsystem.theme.HarvestCircleTheme data class MainPanelHeaderModel( val title: String, @@ -34,28 +41,47 @@ fun MainPanelHeader( primaryAction: @Composable () -> Unit = {}, ) { Row( - Modifier.fillMaxSize().testTag("main-panel-header"), - horizontalArrangement = Arrangement.spacedBy(16.dp), + modifier = + Modifier + .fillMaxSize() + .padding(horizontal = HarvestCircleTheme.shell.layout.paneInset) + .testTag("main-panel-header"), + horizontalArrangement = Arrangement.spacedBy(HarvestCircleTheme.foundation.spacing.lg), + verticalAlignment = Alignment.CenterVertically, ) { Column(Modifier.weight(1f)) { - ShellText( - model.title, - Modifier.semantics { contentDescription = "Screen title: ${model.title}" }.testTag("main-title"), - ShellTextRole.ScreenTitle, + HarvestCircleText( + text = model.title, + modifier = + Modifier + .semantics { contentDescription = "Screen title: ${model.title}" } + .testTag("main-title"), + role = HarvestCircleTextRole.PageTitle, ) if (model.breadcrumb.isNotEmpty()) { - ShellText(model.breadcrumb.joinToString(" / "), Modifier.testTag("main-breadcrumb"), ShellTextRole.Secondary) + HarvestCircleText( + text = model.breadcrumb.joinToString(" / "), + modifier = Modifier.testTag("main-breadcrumb"), + role = HarvestCircleTextRole.LabelSmall, + tone = HarvestCircleContentTone.Secondary, + ) } - model.localStatus?.let { ShellBadge(it, Modifier.testTag("main-local-status")) } } - model.tabs.forEach { tab -> - ShellTab( - label = tab.label, - description = "Show ${tab.label}", - selected = tab.key == model.selectedTab, - onClick = { onTabSelected(tab.key) }, - modifier = Modifier.testTag("main-tab-${tab.key.value}"), - ) + model.localStatus?.let { ShellBadge(it, Modifier.testTag("main-local-status")) } + if (model.tabs.isNotEmpty()) { + HarvestCircleTabRow { + model.tabs.forEach { tab -> + HarvestCircleTab( + selected = tab.key == model.selectedTab, + onClick = { if (tab.key != model.selectedTab) onTabSelected(tab.key) }, + label = tab.label, + modifier = + Modifier + .semantics { contentDescription = "Show ${tab.label}" } + .testTag("main-tab-${tab.key.value}"), + ) + } + } } Row(Modifier.testTag("main-secondary-action")) { secondaryAction() } Row(Modifier.testTag("main-primary-action")) { primaryAction() } 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 @@ -4,13 +4,24 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics import org.harvestcircle.application.ShellNavigationItem import org.harvestcircle.application.addFarmWorkspaceAction import org.harvestcircle.application.shellNavigationItems import org.harvestcircle.application.shellSettingsItem +import org.harvestcircle.designsystem.component.HarvestCircleContentTone +import org.harvestcircle.designsystem.component.HarvestCircleTextRole +import org.harvestcircle.designsystem.component.navigation.HarvestCircleNavigationItem +import org.harvestcircle.designsystem.component.utility.HarvestCircleHorizontalDivider +import org.harvestcircle.designsystem.layout.HarvestCircleSidebar +import org.harvestcircle.designsystem.layout.HarvestCircleSidebarSectionHeader +import org.harvestcircle.designsystem.primitive.HarvestCircleText +import org.harvestcircle.designsystem.theme.HarvestCircleTheme import org.harvestcircle.product.ScreenKey @Composable @@ -18,16 +29,16 @@ fun WorkspaceSidebar( selected: ScreenKey, onScreen: (ScreenKey) -> Unit, ) { - Column(Modifier.fillMaxHeight().testTag("workspace-sidebar")) { - ShellText("Workspace", Modifier.testTag("workspace-label"), ShellTextRole.SectionTitle) - ShellText("Personal", Modifier.testTag("workspace-personal"), ShellTextRole.Secondary) - ShellDivider() + HarvestCircleSidebar(Modifier.fillMaxHeight().testTag("workspace-sidebar")) { + HarvestCircleSidebarSectionHeader("Navigation", Modifier.testTag("workspace-label")) shellNavigationItems.forEach { item -> SidebarItem(item, selected == item.screenKey, onScreen) } + + HarvestCircleSidebarSectionHeader("Workspaces") DisabledWorkspaceAction() Spacer(Modifier.weight(1f)) - ShellDivider() + HarvestCircleHorizontalDivider() SidebarItem(shellSettingsItem, selected == shellSettingsItem.screenKey, onScreen) } } @@ -44,25 +55,49 @@ private fun SidebarItem( } else { "${item.label}. ${item.unavailableExplanation}" } - ShellTab( - label = item.label, - description = description, - selected = selected, - onClick = { onScreen(item.screenKey) }, - modifier = Modifier.fillMaxWidth().testTag("sidebar-${item.screenKey.name}"), - enabled = item.enabled, - ) + Column { + HarvestCircleNavigationItem( + selected = selected, + onClick = { if (item.enabled && !selected) onScreen(item.screenKey) }, + label = item.label, + modifier = + Modifier + .fillMaxWidth() + .semantics { contentDescription = description } + .testTag("sidebar-${item.screenKey.name}"), + enabled = item.enabled, + ) + if (!item.enabled) { + HarvestCircleText( + text = requireNotNull(item.unavailableExplanation), + modifier = Modifier.padding(horizontal = HarvestCircleTheme.foundation.spacing.md), + role = HarvestCircleTextRole.LabelSmall, + tone = HarvestCircleContentTone.Muted, + ) + } + } } @Composable private fun DisabledWorkspaceAction() { val action = addFarmWorkspaceAction - ShellButton( - label = action.label, - description = "${action.label}. ${action.unavailableExplanation}", - onClick = {}, - modifier = Modifier.fillMaxWidth().testTag("sidebar-add-farm"), - enabled = false, - kind = ShellButtonKind.Quiet, - ) + Column { + HarvestCircleNavigationItem( + selected = false, + onClick = {}, + label = action.label, + modifier = + Modifier + .fillMaxWidth() + .semantics { contentDescription = "${action.label}. ${action.unavailableExplanation}" } + .testTag("sidebar-add-farm"), + enabled = false, + ) + HarvestCircleText( + text = requireNotNull(action.unavailableExplanation), + modifier = Modifier.padding(horizontal = HarvestCircleTheme.foundation.spacing.md), + role = HarvestCircleTextRole.LabelSmall, + tone = HarvestCircleContentTone.Muted, + ) + } } 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 @@ -33,11 +33,14 @@ class GlobalTopBarTest { onNodeWithText("Open a Nostr reference").assertExists() onNodeWithTag("top-bar-open-reference").performClick() onNodeWithTag("top-bar-forward").assertIsNotEnabled() + onNodeWithTag("top-bar-menu").performClick() + onNodeWithText("About this build").performClick() assertEquals( listOf( GlobalTopBarIntent.ShowSyncStatus, GlobalTopBarIntent.ShowSignerStatus, GlobalTopBarIntent.OpenNostrReference, + GlobalTopBarIntent.SelectApplicationMenu(ApplicationMenuAction.AboutBuild), ), intents, ) 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 @@ -1,6 +1,5 @@ package org.harvestcircle.ui.shell -import androidx.compose.ui.semantics.SemanticsProperties import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.assertIsEnabled import androidx.compose.ui.test.assertIsNotEnabled @@ -14,7 +13,6 @@ import androidx.compose.ui.test.v2.runComposeUiTest import org.harvestcircle.product.ScreenKey import kotlin.test.Test import kotlin.test.assertEquals -import kotlin.test.assertFalse import kotlin.test.assertTrue @OptIn(ExperimentalTestApi::class) @@ -31,9 +29,6 @@ class WorkspaceSidebarTest { onNodeWithContentDescription("Explore. Not available in this build.").assertExists() onNodeWithContentDescription("Activity. Not available in this build.").assertExists() onNodeWithContentDescription("Add a farm workspace. Not available in this build.").assertExists() - assertFalse( - onNodeWithTag("sidebar-add-farm").fetchSemanticsNode().config.contains(SemanticsProperties.Selected), - ) onNodeWithText("Explore").performClick() onNodeWithText("Network").performClick() onNodeWithText("Settings").performClick()