feat(components): step progress for the onboarding scaffold
A multi-step flow passes a progress slot, pinned above the scrolling content, and OnboardingProgress draws it as one capsule per step with the caller's own localised label. topSpacing lets a step whose hero is a large picture rather than a mark keep the content on one screen.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
package de.jeanlucmakiola.floret.components
|
||||
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -13,6 +14,7 @@ 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.statusBarsPadding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
@@ -22,10 +24,13 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
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.semantics.clearAndSetSemantics
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
/** MD3 8dp spacing scale shared by the onboarding screens. */
|
||||
@@ -43,17 +48,35 @@ object OnboardingSpace {
|
||||
* 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.
|
||||
*
|
||||
* A multi-step flow passes [progress] (see [OnboardingProgress]), which is
|
||||
* pinned above the scrolling content. [topSpacing] gives a step whose hero is a
|
||||
* large picture rather than a mark room to breathe without overflowing.
|
||||
*/
|
||||
@Composable
|
||||
fun OnboardingScaffold(
|
||||
hero: @Composable () -> Unit,
|
||||
actions: @Composable ColumnScope.() -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
progress: (@Composable () -> Unit)? = null,
|
||||
topSpacing: Dp = OnboardingSpace.xl,
|
||||
body: @Composable ColumnScope.() -> Unit,
|
||||
) {
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
topBar = {
|
||||
if (progress != null) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.statusBarsPadding()
|
||||
.padding(horizontal = OnboardingSpace.md, vertical = OnboardingSpace.sm),
|
||||
) {
|
||||
progress()
|
||||
}
|
||||
}
|
||||
},
|
||||
bottomBar = {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
@@ -74,7 +97,7 @@ fun OnboardingScaffold(
|
||||
.padding(horizontal = OnboardingSpace.md),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Spacer(Modifier.height(OnboardingSpace.xl))
|
||||
Spacer(Modifier.height(topSpacing))
|
||||
hero()
|
||||
Spacer(Modifier.height(OnboardingSpace.lg))
|
||||
body()
|
||||
@@ -83,6 +106,60 @@ fun OnboardingScaffold(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Where a multi-step onboarding flow has got to: one capsule per step, filled up
|
||||
* to and including [step] (1-based), with [label] — the caller's own localised
|
||||
* "Step 2 of 4" — underneath.
|
||||
*
|
||||
* The capsules carry no semantics of their own; [label] is the accessible
|
||||
* description, so a screen reader hears the position once rather than counting
|
||||
* decorations.
|
||||
*/
|
||||
@Composable
|
||||
fun OnboardingProgress(
|
||||
step: Int,
|
||||
total: Int,
|
||||
label: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clearAndSetSemantics { },
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
repeat(total) { index ->
|
||||
val done = index < step
|
||||
val color by animateColorAsState(
|
||||
targetValue = if (done) {
|
||||
MaterialTheme.colorScheme.primary
|
||||
} else {
|
||||
MaterialTheme.colorScheme.surfaceContainerHighest
|
||||
},
|
||||
label = "onboarding-progress-segment",
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(4.dp)
|
||||
.clip(CircleShape)
|
||||
.background(color),
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(OnboardingSpace.xs))
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** One trust point: a tonal icon chip on the left, title + supporting text right. */
|
||||
@Composable
|
||||
fun BenefitRow(icon: ImageVector, title: String, body: String) {
|
||||
|
||||
Reference in New Issue
Block a user