From 10e0ca0b06fc73a36189c042f4dc0d6d3b669cfa Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Sun, 28 Jun 2026 14:16:48 +0200 Subject: [PATCH] components: add the shared onboarding scaffold recipe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OnboardingScaffold (scrollable hero + body, CTAs pinned to the bottom), BenefitRow and the OnboardingSpace scale — identical across Agendula and Calendula. The hero is a slot, so each app keeps its own mark. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../floret/components/Onboarding.kt | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 components/src/main/kotlin/de/jeanlucmakiola/floret/components/Onboarding.kt diff --git a/components/src/main/kotlin/de/jeanlucmakiola/floret/components/Onboarding.kt b/components/src/main/kotlin/de/jeanlucmakiola/floret/components/Onboarding.kt new file mode 100644 index 0000000..e56348f --- /dev/null +++ b/components/src/main/kotlin/de/jeanlucmakiola/floret/components/Onboarding.kt @@ -0,0 +1,117 @@ +package de.jeanlucmakiola.floret.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.unit.dp + +/** MD3 8dp spacing scale shared by the onboarding screens. */ +object OnboardingSpace { + val xs = 8.dp + val sm = 16.dp + val md = 24.dp + val lg = 32.dp + val xl = 48.dp +} + +/** + * Shared onboarding shell: a scrollable, centred [hero] + [body] with the + * call(s) to action ([actions]) pinned to the bottom (clear of the navigation + * bar). The content slot is centred horizontally; benefit rows fill the width so + * their own content left-aligns. Each app supplies its own hero so the apps read + * as one family while keeping their own mark. + */ +@Composable +fun OnboardingScaffold( + hero: @Composable () -> Unit, + actions: @Composable ColumnScope.() -> Unit, + modifier: Modifier = Modifier, + body: @Composable ColumnScope.() -> Unit, +) { + Scaffold( + modifier = modifier, + containerColor = MaterialTheme.colorScheme.surface, + bottomBar = { + Column( + modifier = Modifier + .fillMaxWidth() + .navigationBarsPadding() + .padding(horizontal = OnboardingSpace.md, vertical = OnboardingSpace.sm), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(4.dp), + content = actions, + ) + }, + ) { innerPadding -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(innerPadding) + .verticalScroll(rememberScrollState()) + .padding(horizontal = OnboardingSpace.md), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Spacer(Modifier.height(OnboardingSpace.xl)) + hero() + Spacer(Modifier.height(OnboardingSpace.lg)) + body() + Spacer(Modifier.height(OnboardingSpace.md)) + } + } +} + +/** One trust point: a tonal icon chip on the left, title + supporting text right. */ +@Composable +fun BenefitRow(icon: ImageVector, title: String, body: String) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Box( + modifier = Modifier + .size(44.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.secondaryContainer), + contentAlignment = Alignment.Center, + ) { + Icon( + imageVector = icon, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSecondaryContainer, + modifier = Modifier.size(22.dp), + ) + } + Spacer(Modifier.width(OnboardingSpace.sm)) + Column(modifier = Modifier.weight(1f)) { + Text(text = title, style = MaterialTheme.typography.titleMedium) + Text( + text = body, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } +}