commit 5ad8ff2d3c25165ea2ba2f9fd2872021945615f1
parent 0ac555a6d9935a1097d9f0c010284276aeff6446
Author: triesap <tyson@radroots.org>
Date: Sat, 1 Aug 2026 18:33:57 +0000
ui: present account list and selection
- replace the placeholder with the Accounts screen boundary
- render empty and lazy account collection states
- expose selected account details and semantics
- emit explicit selection actions from account rows
Diffstat:
3 files changed, 182 insertions(+), 14 deletions(-)
diff --git a/app/desktop/src/main/kotlin/org/radroots/studio/accounts/ui/AccountsScreen.kt b/app/desktop/src/main/kotlin/org/radroots/studio/accounts/ui/AccountsScreen.kt
@@ -0,0 +1,120 @@
+package org.radroots.studio.accounts.ui
+
+import androidx.compose.foundation.clickable
+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.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+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.selected
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.unit.dp
+import org.radroots.studio.accounts.model.Account
+import org.radroots.studio.accounts.model.AccountsAction
+import org.radroots.studio.accounts.model.AccountsState
+
+@Composable
+fun AccountsScreen(
+ state: AccountsState,
+ onAction: (AccountsAction) -> Unit,
+) {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(24.dp)
+ .testTag("accounts-screen"),
+ verticalArrangement = Arrangement.spacedBy(16.dp),
+ ) {
+ BasicText("radroots")
+ BasicText("Accounts")
+
+ if (state.accounts.isEmpty()) {
+ BasicText(
+ text = "No accounts yet.",
+ modifier = Modifier.testTag("accounts-empty"),
+ )
+ } else {
+ Row(
+ modifier = Modifier.fillMaxSize(),
+ horizontalArrangement = Arrangement.spacedBy(24.dp),
+ ) {
+ LazyColumn(
+ modifier = Modifier
+ .weight(1f)
+ .testTag("accounts-list"),
+ verticalArrangement = Arrangement.spacedBy(8.dp),
+ ) {
+ items(
+ items = state.accounts,
+ key = { account -> account.id.value },
+ ) { account ->
+ AccountRow(
+ account = account,
+ isSelected = account.id == state.selectedAccountId,
+ onSelect = {
+ onAction(AccountsAction.SelectAccount(account.id))
+ },
+ )
+ }
+ }
+
+ state.accounts
+ .firstOrNull { account -> account.id == state.selectedAccountId }
+ ?.let { account ->
+ AccountDetails(
+ account = account,
+ modifier = Modifier.weight(1f),
+ )
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun AccountRow(
+ account: Account,
+ isSelected: Boolean,
+ onSelect: () -> Unit,
+) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .semantics { selected = isSelected }
+ .testTag("account-row:${account.id.value}")
+ .clickable(onClick = onSelect)
+ .padding(8.dp),
+ ) {
+ BasicText(account.displayName)
+ BasicText(account.serverUrl)
+ if (isSelected) {
+ BasicText(
+ text = "Selected",
+ modifier = Modifier.testTag("account-selected:${account.id.value}"),
+ )
+ }
+ }
+}
+
+@Composable
+private fun AccountDetails(
+ account: Account,
+ modifier: Modifier = Modifier,
+) {
+ Column(
+ modifier = modifier,
+ verticalArrangement = Arrangement.spacedBy(8.dp),
+ ) {
+ BasicText("Selected account")
+ BasicText(account.displayName)
+ BasicText(account.serverUrl)
+ BasicText("Status: ${account.loginStatus.name}")
+ }
+}
diff --git a/app/desktop/src/main/kotlin/org/radroots/studio/application/RadrootsApplication.kt b/app/desktop/src/main/kotlin/org/radroots/studio/application/RadrootsApplication.kt
@@ -1,32 +1,23 @@
package org.radroots.studio.application
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.key
import androidx.compose.runtime.remember
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
import java.util.UUID
import org.radroots.studio.accounts.model.AccountId
import org.radroots.studio.accounts.model.AccountsState
import org.radroots.studio.accounts.state.AccountIdFactory
import org.radroots.studio.accounts.state.AccountsReducer
import org.radroots.studio.accounts.state.AccountsStore
+import org.radroots.studio.accounts.ui.AccountsScreen
@Composable
fun RadrootsApplication(
store: AccountsStore = remember { createAccountsStore() },
) {
- key(store.state.value) {
- Box(
- modifier = Modifier.fillMaxSize(),
- contentAlignment = Alignment.Center,
- ) {
- BasicText("radroots")
- }
- }
+ AccountsScreen(
+ state = store.state.value,
+ onAction = store::dispatch,
+ )
}
internal fun createAccountsStore(
diff --git a/app/desktop/src/test/kotlin/org/radroots/studio/accounts/ui/AccountsScreenTest.kt b/app/desktop/src/test/kotlin/org/radroots/studio/accounts/ui/AccountsScreenTest.kt
@@ -0,0 +1,57 @@
+package org.radroots.studio.accounts.ui
+
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.assertIsDisplayed
+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 kotlin.test.Test
+import kotlin.test.assertEquals
+import org.radroots.studio.accounts.model.AccountsAction
+import org.radroots.studio.accounts.testAccount
+import org.radroots.studio.accounts.testAccountsState
+
+@OptIn(ExperimentalTestApi::class)
+class AccountsScreenTest {
+ @Test
+ fun emptyStateIsVisible() = runComposeUiTest {
+ setContent {
+ AccountsScreen(
+ state = testAccountsState(),
+ onAction = {},
+ )
+ }
+
+ onNodeWithTag("accounts-screen").assertIsDisplayed()
+ onNodeWithTag("accounts-empty").assertIsDisplayed()
+ onNodeWithText("No accounts yet.").assertIsDisplayed()
+ }
+
+ @Test
+ fun accountListShowsSelectionAndEmitsExplicitSelection() = runComposeUiTest {
+ val first = testAccount()
+ val second = testAccount(id = "account-2", displayName = "Second Account")
+ val actions = mutableListOf<AccountsAction>()
+ setContent {
+ AccountsScreen(
+ state = testAccountsState(first, second),
+ onAction = actions::add,
+ )
+ }
+
+ onNodeWithTag("accounts-list").assertIsDisplayed()
+ onNodeWithTag("account-row:${first.id.value}").assertIsSelected()
+ onNodeWithTag(
+ testTag = "account-selected:${first.id.value}",
+ useUnmergedTree = true,
+ ).assertIsDisplayed()
+ onNodeWithTag("account-row:${second.id.value}").performClick()
+
+ assertEquals(
+ listOf<AccountsAction>(AccountsAction.SelectAccount(second.id)),
+ actions,
+ )
+ }
+}