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
|
package de.jeanlucmakiola.floret.components
|
||||||
|
|
||||||
|
import androidx.compose.animation.animateColorAsState
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
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.navigationBarsPadding
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.statusBarsPadding
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
@@ -22,10 +24,13 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
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
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
/** MD3 8dp spacing scale shared by the onboarding screens. */
|
/** 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
|
* 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
|
* their own content left-aligns. Each app supplies its own hero so the apps read
|
||||||
* as one family while keeping their own mark.
|
* 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
|
@Composable
|
||||||
fun OnboardingScaffold(
|
fun OnboardingScaffold(
|
||||||
hero: @Composable () -> Unit,
|
hero: @Composable () -> Unit,
|
||||||
actions: @Composable ColumnScope.() -> Unit,
|
actions: @Composable ColumnScope.() -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
progress: (@Composable () -> Unit)? = null,
|
||||||
|
topSpacing: Dp = OnboardingSpace.xl,
|
||||||
body: @Composable ColumnScope.() -> Unit,
|
body: @Composable ColumnScope.() -> Unit,
|
||||||
) {
|
) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
containerColor = MaterialTheme.colorScheme.surface,
|
containerColor = MaterialTheme.colorScheme.surface,
|
||||||
|
topBar = {
|
||||||
|
if (progress != null) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.statusBarsPadding()
|
||||||
|
.padding(horizontal = OnboardingSpace.md, vertical = OnboardingSpace.sm),
|
||||||
|
) {
|
||||||
|
progress()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -74,7 +97,7 @@ fun OnboardingScaffold(
|
|||||||
.padding(horizontal = OnboardingSpace.md),
|
.padding(horizontal = OnboardingSpace.md),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
) {
|
) {
|
||||||
Spacer(Modifier.height(OnboardingSpace.xl))
|
Spacer(Modifier.height(topSpacing))
|
||||||
hero()
|
hero()
|
||||||
Spacer(Modifier.height(OnboardingSpace.lg))
|
Spacer(Modifier.height(OnboardingSpace.lg))
|
||||||
body()
|
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. */
|
/** One trust point: a tonal icon chip on the left, title + supporting text right. */
|
||||||
@Composable
|
@Composable
|
||||||
fun BenefitRow(icon: ImageVector, title: String, body: String) {
|
fun BenefitRow(icon: ImageVector, title: String, body: String) {
|
||||||
|
|||||||
Reference in New Issue
Block a user