commit 6f44b3d3419fb7218dfa8107c68e7c0ee49e1b94 parent 901f1c9c738eeb27c0f254f37ca1313b64c3c0e7 Author: triesap <tyson@radroots.org> Date: Thu, 13 Aug 2026 17:43:43 +0000 ui: align bootstrap and lifecycle canvases - restyle canvas chrome with the approved Studio shell palette - migrate identity recovery chooser and failure views to owned controls - preserve secret custody copy focus and typed lifecycle actions - constrain readable content and keep accessibility scroll ownership Diffstat:
7 files changed, 246 insertions(+), 227 deletions(-)
diff --git a/app/design_system/src/commonMain/kotlin/org/harvestcircle/designsystem/layout/HarvestCircleCanvasFrame.kt b/app/design_system/src/commonMain/kotlin/org/harvestcircle/designsystem/layout/HarvestCircleCanvasFrame.kt @@ -1,5 +1,6 @@ package org.harvestcircle.designsystem.layout +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -13,12 +14,10 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import org.harvestcircle.designsystem.component.utility.HarvestCircleHorizontalDivider -import org.harvestcircle.designsystem.primitive.HarvestCircleSurface -import org.harvestcircle.designsystem.primitive.HarvestCircleSurfaceRole -import org.harvestcircle.designsystem.theme.HarvestCircleTheme +import org.harvestcircle.designsystem.shell.HarvestCircleShellMetrics +import org.harvestcircle.designsystem.shell.HarvestCircleShellPalette -/** Centered bootstrap and lifecycle frame with fixed chrome and one explicit scroll owner. */ +/** Centered bootstrap and lifecycle frame using the approved Studio-derived shell chrome. */ @Composable public fun HarvestCircleCanvasFrame( header: @Composable () -> Unit, @@ -30,53 +29,53 @@ public fun HarvestCircleCanvasFrame( step: @Composable () -> Unit = {}, bodyScrollable: Boolean = false, ) { - val frame = HarvestCircleTheme.shell.frame - HarvestCircleSurface(modifier = modifier.fillMaxSize(), role = HarvestCircleSurfaceRole.Canvas) { - Column(Modifier.fillMaxSize()) { - HarvestCircleSurface( - modifier = Modifier.fillMaxWidth().height(frame.canvasHeaderHeight), - role = HarvestCircleSurfaceRole.Raised, + val colors = HarvestCircleShellPalette + Box(modifier.fillMaxSize().background(colors.viewportCanvas), contentAlignment = Alignment.Center) { + Column(Modifier.fillMaxSize().background(colors.pane)) { + Row( + modifier = + Modifier + .fillMaxWidth() + .height(HarvestCircleShellMetrics.topBarHeight) + .background(colors.applicationFrame) + .padding(horizontal = HarvestCircleShellMetrics.contentPageHorizontalInset), + verticalAlignment = Alignment.CenterVertically, ) { - Row( - modifier = Modifier.fillMaxSize().padding(horizontal = HarvestCircleTheme.shell.layout.pageInset), - verticalAlignment = Alignment.CenterVertically, - ) { - Box { navigation() } - Box(Modifier.weight(1f)) { header() } - Box { step() } - } + Box { navigation() } + Box(Modifier.weight(1f)) { header() } + Box { step() } } - HarvestCircleHorizontalDivider() - val scrollModifier = - if (bodyScrollable) { - Modifier.verticalScroll(rememberScrollState()) - } else { - Modifier - } + HarvestCircleStructuralDivider(vertical = false) + val scrollModifier = if (bodyScrollable) Modifier.verticalScroll(rememberScrollState()) else Modifier Box( - modifier = bodyModifier.weight(1f).fillMaxWidth().then(scrollModifier), + modifier = + bodyModifier + .weight(1f) + .fillMaxWidth() + .background(colors.pane) + .then(scrollModifier), contentAlignment = Alignment.TopCenter, ) { Box( Modifier + .widthIn(max = HarvestCircleShellMetrics.canvasContentMaxWidth) .fillMaxWidth() - .widthIn(max = frame.canvasContentMaxWidth) - .padding(HarvestCircleTheme.shell.layout.pageInset), + .padding(HarvestCircleShellMetrics.contentPageHorizontalInset), ) { body() } } - HarvestCircleHorizontalDivider() - HarvestCircleSurface( - modifier = Modifier.fillMaxWidth().height(frame.canvasActionBarHeight), - role = HarvestCircleSurfaceRole.Raised, + HarvestCircleStructuralDivider(vertical = false) + Box( + modifier = + Modifier + .fillMaxWidth() + .height(HarvestCircleShellMetrics.canvasActionBarHeight) + .background(colors.applicationFrame) + .padding(horizontal = HarvestCircleShellMetrics.contentPageHorizontalInset), + contentAlignment = Alignment.CenterEnd, ) { - Box( - modifier = Modifier.fillMaxSize().padding(horizontal = HarvestCircleTheme.shell.layout.pageInset), - contentAlignment = Alignment.CenterEnd, - ) { - actionBar() - } + actionBar() } } } diff --git a/app/design_system/src/commonMain/kotlin/org/harvestcircle/designsystem/shell/HarvestCircleShellControls.kt b/app/design_system/src/commonMain/kotlin/org/harvestcircle/designsystem/shell/HarvestCircleShellControls.kt @@ -319,3 +319,54 @@ public fun HarvestCircleShellSearchField( ) } } + +@Composable +public fun HarvestCircleShellTextField( + value: String, + onValueChange: (String) -> Unit, + label: String, + placeholder: String, + modifier: Modifier = Modifier, + inputModifier: Modifier = Modifier, + visualTransformation: VisualTransformation = VisualTransformation.None, +) { + val colors = HarvestCircleShellPalette + val sources = rememberHarvestCircleShellInteractionSources("shell-field:$label") + val interaction = sources.collectHarvestCircleShellInteractions() + val shape = + androidx.compose.foundation.shape + .RoundedCornerShape(HarvestCircleShellMetrics.controlRadius) + androidx.compose.foundation.layout.Column(modifier, verticalArrangement = Arrangement.spacedBy(6.dp)) { + HarvestCircleShellText(label, role = HarvestCircleShellTextRole.Label) + Row( + modifier = + Modifier + .fillMaxWidth() + .height(40.dp) + .clip(shape) + .background(colors.input) + .border(BorderStroke(1.dp, if (interaction.focused) colors.accent else colors.border), shape) + .padding(horizontal = 12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + BasicTextField( + value = value, + onValueChange = onValueChange, + modifier = inputModifier.weight(1f), + textStyle = + HarvestCircleTheme.foundation.typography.body + .copy(color = colors.contentPrimary), + interactionSource = sources.activation, + cursorBrush = SolidColor(colors.accent), + visualTransformation = visualTransformation, + singleLine = true, + decorationBox = { inner -> + Box(contentAlignment = Alignment.CenterStart) { + if (value.isEmpty()) HarvestCircleShellText(placeholder, color = colors.contentMuted, maxLines = 1) + inner() + } + }, + ) + } + } +} diff --git a/app/design_system/src/commonMain/kotlin/org/harvestcircle/designsystem/shell/HarvestCircleShellVisuals.kt b/app/design_system/src/commonMain/kotlin/org/harvestcircle/designsystem/shell/HarvestCircleShellVisuals.kt @@ -155,6 +155,8 @@ public object HarvestCircleShellMetrics { public val emptyStateVerticalOffset: Dp = 8.dp public val contentPanelRadius: Dp = 12.dp public val contentPanelInset: Dp = 16.dp + public val canvasActionBarHeight: Dp = 64.dp + public val canvasContentMaxWidth: Dp = 760.dp public val compactBreakpoint: Dp = 976.dp public val expandedBreakpoint: Dp = 1272.dp } diff --git a/app/shared/src/commonMain/kotlin/org/harvestcircle/ui/shell/BootstrapIdentityEntry.kt b/app/shared/src/commonMain/kotlin/org/harvestcircle/ui/shell/BootstrapIdentityEntry.kt @@ -14,12 +14,13 @@ import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.password import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.unit.dp import org.harvestcircle.appearance.TextSizePreference -import org.harvestcircle.designsystem.component.HarvestCircleTextRole -import org.harvestcircle.designsystem.component.action.HarvestCircleLabeledButton -import org.harvestcircle.designsystem.component.input.HarvestCircleTextField -import org.harvestcircle.designsystem.primitive.HarvestCircleText -import org.harvestcircle.designsystem.theme.HarvestCircleTheme +import org.harvestcircle.designsystem.shell.HarvestCircleShellButton +import org.harvestcircle.designsystem.shell.HarvestCircleShellPanel +import org.harvestcircle.designsystem.shell.HarvestCircleShellText +import org.harvestcircle.designsystem.shell.HarvestCircleShellTextField +import org.harvestcircle.designsystem.shell.HarvestCircleShellTextRole import org.harvestcircle.identities.ui.HarvestCircleUiActions import org.harvestcircle.identities.ui.HarvestCircleUiModel import org.harvestcircle.navigation.BootstrapStep @@ -32,48 +33,36 @@ fun BootstrapIdentityEntry( onBack: () -> Unit, ) { require(step == BootstrapStep.CreateIdentity || step == BootstrapStep.ImportIdentity) + val title = if (step == BootstrapStep.CreateIdentity) "Create a local Nostr identity" else "Import an existing identity" CanvasScaffold( textSize = TextSizePreference.Default, navigation = { - HarvestCircleLabeledButton("Back", "Back", onBack, Modifier.testTag("identity-entry-back")) - }, - header = { - HarvestCircleText( - if (step == BootstrapStep.CreateIdentity) { - "Create a local Nostr identity" - } else { - "Import an existing identity" - }, - role = HarvestCircleTextRole.PageTitle, - ) + HarvestCircleShellButton("Back", onBack, Modifier.testTag("identity-entry-back")) }, + header = { HarvestCircleShellText(title, role = HarvestCircleShellTextRole.PaneTitle) }, body = { - if (step == BootstrapStep.CreateIdentity) { - CreateIdentityBody(model) - } else { - ImportIdentityBody(model, actions) - } + if (step == BootstrapStep.CreateIdentity) CreateIdentityBody(model) else ImportIdentityBody(model, actions) }, actionBar = { - Row(horizontalArrangement = Arrangement.spacedBy(HarvestCircleTheme.shell.layout.inlineGap)) { + Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { if (step == BootstrapStep.CreateIdentity) { - HarvestCircleLabeledButton( + HarvestCircleShellButton( "Generate identity", - "Generate identity", - { actions.generateIdentity() }, + actions.generateIdentity, Modifier.testTag("generate-key"), + primary = true, enabled = !model.busy, ) } else { - HarvestCircleLabeledButton( - "Import identity", + HarvestCircleShellButton( "Import identity", - { actions.importSecretKey() }, + actions.importSecretKey, Modifier.testTag("import-key"), + primary = true, enabled = !model.busy, ) } - HarvestCircleLabeledButton("Back", "Back", onBack, Modifier.testTag("identity-entry-cancel")) + HarvestCircleShellButton("Back", onBack, Modifier.testTag("identity-entry-cancel")) } }, ) @@ -81,13 +70,10 @@ fun BootstrapIdentityEntry( @Composable private fun CreateIdentityBody(model: HarvestCircleUiModel) { - Column( - Modifier.testTag("create-identity-entry"), - verticalArrangement = Arrangement.spacedBy(HarvestCircleTheme.shell.layout.contentGap), - ) { - HarvestCircleText("HarvestCircle will generate a new Nostr identity.") - HarvestCircleText("Save the recovery key before the identity is stored in the operating-system keyring.") - model.problem?.let { HarvestCircleText(it, Modifier.testTag("identity-entry-problem")) } + HarvestCircleShellPanel(Modifier.testTag("create-identity-entry")) { + HarvestCircleShellText("HarvestCircle will generate a new Nostr identity.", role = HarvestCircleShellTextRole.SectionTitle) + HarvestCircleShellText("Save the recovery key before the identity is stored in the operating-system keyring.") + model.problem?.let { HarvestCircleShellText(it, Modifier.testTag("identity-entry-problem")) } } } @@ -97,30 +83,29 @@ private fun ImportIdentityBody( actions: HarvestCircleUiActions, ) { val requester = remember { FocusRequester() } - Column( - Modifier.testTag("import-identity-entry"), - verticalArrangement = Arrangement.spacedBy(HarvestCircleTheme.shell.layout.contentGap), - ) { - HarvestCircleTextField( - value = model.importDraft.revealForDisplay(), - onValueChange = actions.editImportDraft, - label = "Nostr secret key", - placeholder = "nsec1…", - inputModifier = - Modifier - .focusRequester(requester) - .semantics { - password() - contentDescription = "Nostr secret key" - }.testTag("import-nsec-input"), - visualTransformation = PasswordVisualTransformation(), - ) - HarvestCircleText( - "The secret is held only for this import.\n\n" + - "It is cleared after it is sent to the local native runtime.", - ) - model.importGuidance?.let { HarvestCircleText(it, Modifier.testTag("identity-entry-guidance")) } - model.problem?.let { HarvestCircleText(it, Modifier.testTag("identity-entry-problem")) } + Column(Modifier.testTag("import-identity-entry"), verticalArrangement = Arrangement.spacedBy(16.dp)) { + HarvestCircleShellPanel { + HarvestCircleShellTextField( + value = model.importDraft.revealForDisplay(), + onValueChange = actions.editImportDraft, + label = "Nostr secret key", + placeholder = "nsec1…", + inputModifier = + Modifier + .focusRequester(requester) + .semantics { + password() + contentDescription = "Nostr secret key" + }.testTag("import-nsec-input"), + visualTransformation = PasswordVisualTransformation(), + ) + HarvestCircleShellText( + "The secret is held only for this import.\n\n" + + "It is cleared after it is sent to the local native runtime.", + ) + } + model.importGuidance?.let { HarvestCircleShellText(it, Modifier.testTag("identity-entry-guidance")) } + model.problem?.let { HarvestCircleShellText(it, Modifier.testTag("identity-entry-problem")) } } LaunchedEffect(Unit) { requester.requestFocus() } } diff --git a/app/shared/src/commonMain/kotlin/org/harvestcircle/ui/shell/BootstrapRecoveryAndChooser.kt b/app/shared/src/commonMain/kotlin/org/harvestcircle/ui/shell/BootstrapRecoveryAndChooser.kt @@ -1,7 +1,6 @@ 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.layout.fillMaxWidth import androidx.compose.foundation.lazy.LazyColumn @@ -11,13 +10,14 @@ 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.harvestcircle.appearance.TextSizePreference import org.harvestcircle.application.ShellFocusTarget -import org.harvestcircle.designsystem.component.HarvestCircleTextRole -import org.harvestcircle.designsystem.component.action.HarvestCircleLabeledButton -import org.harvestcircle.designsystem.component.feedback.HarvestCircleBadge -import org.harvestcircle.designsystem.primitive.HarvestCircleText -import org.harvestcircle.designsystem.theme.HarvestCircleTheme +import org.harvestcircle.designsystem.shell.HarvestCircleShellButton +import org.harvestcircle.designsystem.shell.HarvestCircleShellPalette +import org.harvestcircle.designsystem.shell.HarvestCircleShellPanel +import org.harvestcircle.designsystem.shell.HarvestCircleShellText +import org.harvestcircle.designsystem.shell.HarvestCircleShellTextRole import org.harvestcircle.identities.ui.HarvestCirclePlatformActions import org.harvestcircle.identities.ui.HarvestCircleUiActions import org.harvestcircle.identities.ui.HarvestCircleUiModel @@ -32,37 +32,33 @@ fun GeneratedRecoveryCanvas( val backup = model.generatedKeyBackup ?: return CanvasScaffold( textSize = TextSizePreference.Default, - header = { HarvestCircleText("Save your recovery key", role = HarvestCircleTextRole.PageTitle) }, + header = { HarvestCircleShellText("Save your recovery key", role = HarvestCircleShellTextRole.PaneTitle) }, body = { - Column( - Modifier.testTag("generated-key-backup"), - verticalArrangement = Arrangement.spacedBy(HarvestCircleTheme.shell.layout.contentGap), - ) { - HarvestCircleText("This key is shown once.") - HarvestCircleText("Store it somewhere private before continuing.") - HarvestCircleText("Recovery key", role = HarvestCircleTextRole.SubsectionTitle) - HarvestCircleText(backup.nsec, Modifier.testTag("generated-nsec"), HarvestCircleTextRole.Code) + HarvestCircleShellPanel(Modifier.testTag("generated-key-backup")) { + HarvestCircleShellText("This key is shown once.", role = HarvestCircleShellTextRole.SectionTitle) + HarvestCircleShellText("Store it somewhere private before continuing.") + HarvestCircleShellText("Recovery key", role = HarvestCircleShellTextRole.Label) + HarvestCircleShellText(backup.nsec, Modifier.testTag("generated-nsec"), HarvestCircleShellTextRole.Code) } }, actionBar = { - Row(horizontalArrangement = Arrangement.spacedBy(HarvestCircleTheme.shell.layout.inlineGap)) { - HarvestCircleLabeledButton( - "Copy recovery key", + Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { + HarvestCircleShellButton( "Copy recovery key", { platformActions.copySecret(backup.nsec) }, Modifier.testTag("copy-generated-key"), ) - HarvestCircleLabeledButton( - "I have saved the recovery key", + HarvestCircleShellButton( "I have saved the recovery key", - { actions.acknowledgeGeneratedKeyBackup() }, + actions.acknowledgeGeneratedKeyBackup, Modifier.testTag("acknowledge-key-backup"), + primary = true, ) - HarvestCircleLabeledButton( + HarvestCircleShellButton( "Cancel identity creation", - "Cancel identity creation", - { actions.cancelGeneratedKeyBackup() }, + actions.cancelGeneratedKeyBackup, Modifier.testTag("cancel-generated-key"), + destructive = true, ) } }, @@ -77,29 +73,24 @@ fun IdentityChooserCanvas( ) { CanvasScaffold( textSize = TextSizePreference.Default, - header = { HarvestCircleText("Choose a Nostr identity", role = HarvestCircleTextRole.PageTitle) }, + header = { HarvestCircleShellText("Choose a Nostr identity", role = HarvestCircleShellTextRole.PaneTitle) }, body = { - LazyColumn(Modifier.fillMaxWidth().testTag("saved-identity-list")) { - items(model.identities, key = IdentityUiModel::publicKeyHex) { identity -> - IdentityRow(identity, model, actions) - } + LazyColumn( + Modifier.fillMaxWidth().testTag("saved-identity-list"), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + items(model.identities, key = IdentityUiModel::publicKeyHex) { identity -> IdentityRow(identity, model, actions) } } }, actionBar = { - Row(horizontalArrangement = Arrangement.spacedBy(HarvestCircleTheme.shell.layout.inlineGap)) { - HarvestCircleLabeledButton( + Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { + HarvestCircleShellButton( "Create another identity", - "Create another identity", - { actions.chooseCreateIdentity() }, + actions.chooseCreateIdentity, Modifier.testTag("choose-create-identity"), ) - HarvestCircleLabeledButton( - "Import identity", - "Import identity", - { actions.chooseImportIdentity() }, - Modifier.testTag("choose-import-identity"), - ) - HarvestCircleLabeledButton("Explore read-only", "Explore read-only", onReadOnly, Modifier.testTag("chooser-read-only")) + HarvestCircleShellButton("Import identity", actions.chooseImportIdentity, Modifier.testTag("choose-import-identity")) + HarvestCircleShellButton("Explore read-only", onReadOnly, Modifier.testTag("chooser-read-only"), primary = true) } }, ) @@ -111,46 +102,45 @@ private fun IdentityRow( model: HarvestCircleUiModel, actions: HarvestCircleUiActions, ) { - Column( + HarvestCircleShellPanel( Modifier - .fillMaxWidth() .semantics { selected = identity.selected } .testTag("identity-row:${identity.publicKeyHex}"), - verticalArrangement = Arrangement.spacedBy(HarvestCircleTheme.shell.layout.inlineGap), ) { - HarvestCircleText(identity.label, role = HarvestCircleTextRole.SubsectionTitle) - HarvestCircleText(identity.shortNpub, role = HarvestCircleTextRole.Code) - HarvestCircleText( + HarvestCircleShellText(identity.label, role = HarvestCircleShellTextRole.SectionTitle) + HarvestCircleShellText(identity.shortNpub, role = HarvestCircleShellTextRole.Code) + HarvestCircleShellText( if (identity.signerAvailability == org.harvestcircle.application.SignerAvailability.Available) { "Local credential available" } else { "Local credential unavailable" }, + color = HarvestCircleShellPalette.contentSecondary, ) - if (identity.selected) HarvestCircleBadge("Selected") - HarvestCircleLabeledButton( - label = if (identity.selected) "Selected identity" else "Select identity", - accessibilityLabel = "Select ${identity.label}", - modifier = Modifier.testTag("select-identity:${identity.publicKeyHex}"), - enabled = !model.busy && !identity.selected, - onClick = { actions.selectIdentity(identity.publicKeyHex) }, - ) - HarvestCircleLabeledButton( - label = if (identity.active) "Active identity" else "Activate identity", - accessibilityLabel = "Activate ${identity.label}", - modifier = Modifier.testTag("activate-identity:${identity.publicKeyHex}"), - enabled = !model.busy && !identity.active, - onClick = { actions.activateIdentity(identity.publicKeyHex) }, - ) - HarvestCircleLabeledButton( - label = "Remove local identity", - accessibilityLabel = "Remove ${identity.label}", - enabled = !model.busy, - modifier = + if (identity.selected) HarvestCircleShellText("Selected", role = HarvestCircleShellTextRole.Small) + Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { + HarvestCircleShellButton( + if (identity.selected) "Selected identity" else "Select identity", + { actions.selectIdentity(identity.publicKeyHex) }, + Modifier.testTag("select-identity:${identity.publicKeyHex}"), + primary = !identity.selected, + enabled = !model.busy && !identity.selected, + ) + HarvestCircleShellButton( + if (identity.active) "Active identity" else "Activate identity", + { actions.activateIdentity(identity.publicKeyHex) }, + Modifier.testTag("activate-identity:${identity.publicKeyHex}"), + enabled = !model.busy && !identity.active, + ) + HarvestCircleShellButton( + "Remove local identity", + { actions.requestIdentityRemoval(identity.publicKeyHex) }, Modifier .shellFocusTarget(ShellFocusTarget.IdentityRow(identity.publicKeyHex)) .testTag("remove-identity:${identity.publicKeyHex}"), - onClick = { actions.requestIdentityRemoval(identity.publicKeyHex) }, - ) + destructive = true, + enabled = !model.busy, + ) + } } } 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 @@ -26,11 +26,12 @@ import org.harvestcircle.application.ShellFocusTarget import org.harvestcircle.application.ShellRoot import org.harvestcircle.application.StatusOverlayKey import org.harvestcircle.application.deriveShellStatus -import org.harvestcircle.designsystem.component.HarvestCircleContentTone -import org.harvestcircle.designsystem.component.HarvestCircleTextRole -import org.harvestcircle.designsystem.component.action.HarvestCircleLabeledButton import org.harvestcircle.designsystem.layout.HarvestCircleFrameLayoutClass -import org.harvestcircle.designsystem.primitive.HarvestCircleText +import org.harvestcircle.designsystem.shell.HarvestCircleShellButton +import org.harvestcircle.designsystem.shell.HarvestCircleShellPalette +import org.harvestcircle.designsystem.shell.HarvestCircleShellPanel +import org.harvestcircle.designsystem.shell.HarvestCircleShellText +import org.harvestcircle.designsystem.shell.HarvestCircleShellTextRole import org.harvestcircle.identities.ui.HarvestCirclePlatformActions import org.harvestcircle.identities.ui.HarvestCircleUiActions import org.harvestcircle.identities.ui.toUiModel @@ -125,38 +126,44 @@ private fun HarvestCircleShellContent( private fun BootstrapWelcome(dispatch: (HarvestCircleShellIntent) -> Unit) { CanvasScaffold( textSize = TextSizePreference.Default, - header = { HarvestCircleText("HarvestCircle", role = HarvestCircleTextRole.PageTitle) }, + header = { HarvestCircleShellText("HarvestCircle", role = HarvestCircleShellTextRole.PaneTitle) }, body = { Column( Modifier.testTag("bootstrap-welcome"), verticalArrangement = Arrangement.spacedBy(HarvestCircleDesignTokens.shell.layout.contentGap), ) { - HarvestCircleText("Coordinate local food with clear, signed terms.", role = HarvestCircleTextRole.SectionTitle) - HarvestCircleText("HarvestCircle helps farms and nearby buyers form one shared order.") - HarvestCircleText("You do not need a HarvestCircle account.") - HarvestCircleText("A farm opens a round", role = HarvestCircleTextRole.SubsectionTitle) - HarvestCircleText("The farm publishes the available boxes, pickup terms, and price levels.") - HarvestCircleText("Buyers make private commitments", role = HarvestCircleTextRole.SubsectionTitle) - HarvestCircleText("Each buyer signs a maximum amount without publishing their identity.") - HarvestCircleText("The authority clears the round", role = HarvestCircleTextRole.SubsectionTitle) - HarvestCircleText("The selected authority applies the farm’s signed terms and issues allocations.") - HarvestCircleText( + HarvestCircleShellPanel { + HarvestCircleShellText( + "Coordinate local food with clear, signed terms.", + role = HarvestCircleShellTextRole.SectionTitle, + ) + HarvestCircleShellText("HarvestCircle helps farms and nearby buyers form one shared order.") + HarvestCircleShellText("You do not need a HarvestCircle account.") + } + HarvestCircleShellPanel { + HarvestCircleShellText("A farm opens a round", role = HarvestCircleShellTextRole.BodyStrong) + HarvestCircleShellText("The farm publishes the available boxes, pickup terms, and price levels.") + HarvestCircleShellText("Buyers make private commitments", role = HarvestCircleShellTextRole.BodyStrong) + HarvestCircleShellText("Each buyer signs a maximum amount without publishing their identity.") + HarvestCircleShellText("The authority clears the round", role = HarvestCircleShellTextRole.BodyStrong) + HarvestCircleShellText("The selected authority applies the farm’s signed terms and issues allocations.") + } + HarvestCircleShellText( "Open source · Nostr-based · No managed service required", - role = HarvestCircleTextRole.BodySmall, - tone = HarvestCircleContentTone.Secondary, + role = HarvestCircleShellTextRole.Small, + color = HarvestCircleShellPalette.contentSecondary, ) } }, actionBar = { Row(horizontalArrangement = Arrangement.spacedBy(HarvestCircleDesignTokens.shell.layout.inlineGap)) { - HarvestCircleLabeledButton( - "Explore read-only", + HarvestCircleShellButton( "Explore read-only", { dispatch(HarvestCircleShellIntent.EnterReadOnly) }, Modifier.testTag("bootstrap-read-only"), + primary = true, ) - HarvestCircleLabeledButton( - "Create a local Nostr identity", + HarvestCircleShellButton( "Create a local Nostr identity", { dispatch(HarvestCircleShellIntent.Identity(HarvestCircleIntent.ChooseCreateIdentity)) @@ -168,8 +175,7 @@ private fun BootstrapWelcome(dispatch: (HarvestCircleShellIntent) -> Unit) { }, Modifier.testTag("bootstrap-create"), ) - HarvestCircleLabeledButton( - "Import an existing identity", + HarvestCircleShellButton( "Import an existing identity", { dispatch(HarvestCircleShellIntent.Identity(HarvestCircleIntent.ChooseImportIdentity)) @@ -311,7 +317,7 @@ private fun DashboardRoot( platformActions = platformActions, showSectionTabs = false, ) - else -> HarvestCircleText(route.title(), Modifier.testTag("foundation-route-body")) + else -> HarvestCircleShellText(route.title(), Modifier.testTag("foundation-route-body")) } } }, diff --git a/app/shared/src/commonMain/kotlin/org/harvestcircle/ui/shell/LifecycleCanvases.kt b/app/shared/src/commonMain/kotlin/org/harvestcircle/ui/shell/LifecycleCanvases.kt @@ -2,21 +2,20 @@ package org.harvestcircle.ui.shell import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row 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 org.harvestcircle.appearance.AppearanceState import org.harvestcircle.appearance.TextSizePreference import org.harvestcircle.application.HarvestCirclePresenterState import org.harvestcircle.application.HarvestCircleRoute -import org.harvestcircle.designsystem.component.HarvestCircleButtonVariant -import org.harvestcircle.designsystem.component.HarvestCircleTextRole -import org.harvestcircle.designsystem.component.action.HarvestCircleLabeledButton -import org.harvestcircle.designsystem.primitive.HarvestCircleText -import org.harvestcircle.designsystem.theme.HarvestCircleTheme +import org.harvestcircle.designsystem.shell.HarvestCircleShellButton +import org.harvestcircle.designsystem.shell.HarvestCircleShellPanel +import org.harvestcircle.designsystem.shell.HarvestCircleShellText +import org.harvestcircle.designsystem.shell.HarvestCircleShellTextRole import org.harvestcircle.identities.ui.HarvestCircleUiActions @Composable @@ -27,29 +26,19 @@ fun ShellLifecycleCanvas( val presentation = state.route.lifecyclePresentation() CanvasScaffold( textSize = TextSizePreference.Default, - header = { HarvestCircleText(presentation.title, role = HarvestCircleTextRole.PageTitle) }, + header = { HarvestCircleShellText(presentation.title, role = HarvestCircleShellTextRole.PaneTitle) }, body = { - Column( - Modifier.testTag("lifecycle-${state.route.name.lowercase()}"), - verticalArrangement = Arrangement.spacedBy(HarvestCircleTheme.shell.layout.contentGap), - ) { - HarvestCircleText(presentation.detail) - state.problem?.let { HarvestCircleText(it, Modifier.testTag("lifecycle-problem")) } + HarvestCircleShellPanel(Modifier.testTag("lifecycle-${state.route.name.lowercase()}")) { + HarvestCircleShellText(presentation.detail, role = HarvestCircleShellTextRole.SectionTitle) + state.problem?.let { HarvestCircleShellText(it, Modifier.testTag("lifecycle-problem")) } } }, actionBar = { - Row(horizontalArrangement = Arrangement.spacedBy(HarvestCircleTheme.shell.layout.inlineGap)) { + Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { if (state.lastProblem?.retryable == true) { - HarvestCircleLabeledButton("Retry", "Retry the last local operation", actions.retryLastCommand) - } - if (state.problem != null) { - HarvestCircleLabeledButton( - "Dismiss", - "Dismiss this problem", - actions.dismissProblem, - variant = HarvestCircleButtonVariant.Ghost, - ) + HarvestCircleShellButton("Retry", actions.retryLastCommand, primary = true) } + if (state.problem != null) HarvestCircleShellButton("Dismiss", actions.dismissProblem) } }, ) @@ -58,11 +47,7 @@ fun ShellLifecycleCanvas( @Composable fun StartupFailureScreen(problem: String) { HarvestCircleTheme(AppearanceState()) { - FailureCanvas( - title = "HarvestCircle could not start", - problem = problem, - tag = "startup-failure", - ) + FailureCanvas("HarvestCircle could not start", problem, "startup-failure") } } @@ -75,15 +60,16 @@ fun ShutdownFailureScreen( Box(Modifier.fillMaxSize().testTag("shutdown-failure")) { CanvasScaffold( textSize = TextSizePreference.Default, - header = { HarvestCircleText("HarvestCircle could not close safely", role = HarvestCircleTextRole.PageTitle) }, - body = { HarvestCircleText(problem, Modifier.testTag("shutdown-problem")) }, + header = { HarvestCircleShellText("HarvestCircle could not close safely", role = HarvestCircleShellTextRole.PaneTitle) }, + body = { + HarvestCircleShellPanel { HarvestCircleShellText(problem, Modifier.testTag("shutdown-problem")) } + }, actionBar = { - HarvestCircleLabeledButton( + HarvestCircleShellButton( "Force exit", - "Force HarvestCircle to exit", forceExit, Modifier.testTag("force-exit"), - variant = HarvestCircleButtonVariant.Destructive, + destructive = true, ) }, ) @@ -99,10 +85,10 @@ private fun FailureCanvas( ) { CanvasScaffold( textSize = TextSizePreference.Default, - header = { HarvestCircleText(title, role = HarvestCircleTextRole.PageTitle) }, + header = { HarvestCircleShellText(title, role = HarvestCircleShellTextRole.PaneTitle) }, body = { - Column(Modifier.testTag(tag)) { - HarvestCircleText(problem, Modifier.testTag("startup-problem")) + HarvestCircleShellPanel(Modifier.testTag(tag)) { + HarvestCircleShellText(problem, Modifier.testTag("startup-problem")) } }, actionBar = {},