feat(components): navigation slot and full-bleed content for the onboarding shell

A step that can be stepped back from passes navigationIcon. The slot's
width is reserved whether or not it is filled, and mirrored on the other
side, so the progress stays centred as steps come and go.

contentPadding lets a step built from grouped rows drop the column's own
inset, since those rows carry their own.
This commit is contained in:
2026-08-10 17:17:16 +02:00
parent 63661b9a7e
commit a366ca9188
@@ -33,6 +33,9 @@ import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
/** Standard touch target, reserved for the shell's navigation slot. */
private val NavigationSlotSize = 48.dp
/** MD3 8dp spacing scale shared by the onboarding screens. */
object OnboardingSpace {
val xs = 8.dp
@@ -50,8 +53,14 @@ object OnboardingSpace {
* 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.
* pinned above the scrolling content, and [navigationIcon] for a step that can
* be stepped back from. The navigation slot's width is reserved whether or not
* it is filled, and mirrored on the other side, so the progress stays centred
* and does not shift as steps come and go.
*
* [topSpacing] gives a step whose hero is a large picture rather than a mark
* room to breathe. [contentPadding] is the scrolling column's own horizontal
* inset — a step built from grouped rows passes 0, since those carry their own.
*/
@Composable
fun OnboardingScaffold(
@@ -59,21 +68,26 @@ fun OnboardingScaffold(
actions: @Composable ColumnScope.() -> Unit,
modifier: Modifier = Modifier,
progress: (@Composable () -> Unit)? = null,
navigationIcon: (@Composable () -> Unit)? = null,
topSpacing: Dp = OnboardingSpace.xl,
contentPadding: Dp = OnboardingSpace.md,
body: @Composable ColumnScope.() -> Unit,
) {
Scaffold(
modifier = modifier,
containerColor = MaterialTheme.colorScheme.surface,
topBar = {
if (progress != null) {
Box(
if (progress != null || navigationIcon != null) {
Row(
modifier = Modifier
.fillMaxWidth()
.statusBarsPadding()
.padding(horizontal = OnboardingSpace.md, vertical = OnboardingSpace.sm),
.padding(horizontal = OnboardingSpace.xs, vertical = OnboardingSpace.sm),
verticalAlignment = Alignment.CenterVertically,
) {
progress()
Box(Modifier.size(NavigationSlotSize)) { navigationIcon?.invoke() }
Box(Modifier.weight(1f)) { progress?.invoke() }
Spacer(Modifier.width(NavigationSlotSize))
}
}
},
@@ -94,7 +108,7 @@ fun OnboardingScaffold(
.fillMaxSize()
.padding(innerPadding)
.verticalScroll(rememberScrollState())
.padding(horizontal = OnboardingSpace.md),
.padding(horizontal = contentPadding),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Spacer(Modifier.height(topSpacing))