From a366ca9188270f0c621d6c78f081113e412efbdf Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Sat, 8 Aug 2026 21:43:57 +0200 Subject: [PATCH] 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. --- .../floret/components/Onboarding.kt | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/components/src/main/kotlin/de/jeanlucmakiola/floret/components/Onboarding.kt b/components/src/main/kotlin/de/jeanlucmakiola/floret/components/Onboarding.kt index f8eb277..7e72158 100644 --- a/components/src/main/kotlin/de/jeanlucmakiola/floret/components/Onboarding.kt +++ b/components/src/main/kotlin/de/jeanlucmakiola/floret/components/Onboarding.kt @@ -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))