From d9e4e877cc05450bcf121e2cb4140d0efda96f8e Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Sun, 28 Jun 2026 20:46:51 +0200 Subject: [PATCH] components: grow GroupedRow + CollapsingScaffold + pickers to the family superset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Promote the richer variants Calendula had kept app-local into the shared kit, as backward-compatible additions (every new parameter defaults to the prior behaviour, so Agendula's call sites are unchanged): - GroupedRow: add `dimmed` (fade headline/summary to M3 disabled emphasis while the trailing control stays live — for present-but-off rows) and `container` (tonal-colour override, e.g. a category tint). - CollapsingScaffold: add `actions` (trailing app-bar items), `snackbarHost`, and opt-in `predictiveBack` (wires the back-gesture preview to onBack for full-screen surfaces that want it). - FullScreenPicker: thread opt-in `predictiveBack` to the scaffold, and apply SOFT_INPUT_ADJUST_NOTHING so a picker's own imePadding lifts the focused field instead of leaving a black gap above the keyboard. - OptionPicker: forward `predictiveBack` and add an optional `header` slot above the option rows. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../floret/components/CollapsingScaffold.kt | 13 ++++++++++ .../floret/components/GroupedList.kt | 26 ++++++++++++++++--- .../floret/components/OptionPicker.kt | 26 +++++++++++++++++-- 3 files changed, 59 insertions(+), 6 deletions(-) diff --git a/components/src/main/kotlin/de/jeanlucmakiola/floret/components/CollapsingScaffold.kt b/components/src/main/kotlin/de/jeanlucmakiola/floret/components/CollapsingScaffold.kt index 72a7526..d8c662d 100644 --- a/components/src/main/kotlin/de/jeanlucmakiola/floret/components/CollapsingScaffold.kt +++ b/components/src/main/kotlin/de/jeanlucmakiola/floret/components/CollapsingScaffold.kt @@ -3,6 +3,7 @@ package de.jeanlucmakiola.floret.components import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.imePadding @@ -25,6 +26,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import de.jeanlucmakiola.floret.identity.predictiveBack /** * The family's collapsing scaffold for settings hubs, sub-screens and the @@ -32,6 +34,11 @@ import androidx.compose.ui.unit.dp * button, and a vertically scrolling content column. [content] lays out the * connected grouped rows. The back content description uses the kit's `back` * string, which an app can override with its own (localized) `back`. + * + * Optional extras, all defaulting to the bare scaffold so existing callers are + * unchanged: [actions] adds trailing app-bar items; [snackbarHost] hosts + * transient messages; [predictiveBack] (off by default) wires the gesture + * preview to [onBack] for full-screen surfaces that want it. */ @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -39,12 +46,16 @@ fun CollapsingScaffold( title: String, onBack: () -> Unit, modifier: Modifier = Modifier, + predictiveBack: Boolean = false, + actions: @Composable RowScope.() -> Unit = {}, + snackbarHost: @Composable () -> Unit = {}, content: @Composable ColumnScope.() -> Unit, ) { val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState()) Scaffold( modifier = modifier + .then(if (predictiveBack) Modifier.predictiveBack(onBack = onBack) else Modifier) .fillMaxSize() .background(MaterialTheme.colorScheme.surface) .nestedScroll(scrollBehavior.nestedScrollConnection), @@ -59,12 +70,14 @@ fun CollapsingScaffold( ) } }, + actions = actions, scrollBehavior = scrollBehavior, colors = TopAppBarDefaults.largeTopAppBarColors( scrolledContainerColor = MaterialTheme.colorScheme.surface, ), ) }, + snackbarHost = snackbarHost, ) { innerPadding -> Column( modifier = Modifier diff --git a/components/src/main/kotlin/de/jeanlucmakiola/floret/components/GroupedList.kt b/components/src/main/kotlin/de/jeanlucmakiola/floret/components/GroupedList.kt index 817bb35..70e0fab 100644 --- a/components/src/main/kotlin/de/jeanlucmakiola/floret/components/GroupedList.kt +++ b/components/src/main/kotlin/de/jeanlucmakiola/floret/components/GroupedList.kt @@ -82,6 +82,12 @@ fun GroupedSurface( * One row in a grouped list: an M3 [ListItem] over a [GroupedSurface] (the * tonal, position-shaped, press-morphing family container). Insets 16dp like * the lists overview. + * + * [selected] highlights the row in the secondary container. [dimmed] fades the + * headline and summary to the M3 disabled emphasis while leaving the [trailing] + * control at full opacity — for rows that are present but switched off. + * [container] overrides the row's tonal colour (e.g. a category tint); ignored + * when [selected]. */ @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -91,6 +97,8 @@ fun GroupedRow( modifier: Modifier = Modifier, summary: String? = null, selected: Boolean = false, + dimmed: Boolean = false, + container: Color? = null, minHeight: Dp = 72.dp, leading: @Composable (() -> Unit)? = null, trailing: @Composable (() -> Unit)? = null, @@ -104,13 +112,23 @@ fun GroupedRow( supportingColor = MaterialTheme.colorScheme.onSecondaryContainer, trailingIconColor = MaterialTheme.colorScheme.onSecondaryContainer, ) + } else if (dimmed) { + // M3 disabled emphasis (0.38α) on the text/leading; the trailing control + // stays full-opacity so a toggle reads as live even on a faded row. + val muted = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f) + ListItemDefaults.colors( + containerColor = Color.Transparent, + headlineColor = muted, + leadingIconColor = muted, + supportingColor = muted, + ) } else { ListItemDefaults.colors(containerColor = Color.Transparent) } - val containerColor = if (selected) { - MaterialTheme.colorScheme.secondaryContainer - } else { - MaterialTheme.colorScheme.surfaceContainerHigh + val containerColor = when { + selected -> MaterialTheme.colorScheme.secondaryContainer + container != null -> container + else -> MaterialTheme.colorScheme.surfaceContainerHigh } GroupedSurface( position = position, diff --git a/components/src/main/kotlin/de/jeanlucmakiola/floret/components/OptionPicker.kt b/components/src/main/kotlin/de/jeanlucmakiola/floret/components/OptionPicker.kt index d297437..e4c9c56 100644 --- a/components/src/main/kotlin/de/jeanlucmakiola/floret/components/OptionPicker.kt +++ b/components/src/main/kotlin/de/jeanlucmakiola/floret/components/OptionPicker.kt @@ -1,13 +1,17 @@ package de.jeanlucmakiola.floret.components +import android.view.WindowManager import androidx.compose.foundation.layout.ColumnScope import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Check import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.ui.platform.LocalView import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties +import androidx.compose.ui.window.DialogWindowProvider /** * Full-screen scaffold for selection pickers: a full-bleed [Dialog] that reuses @@ -19,6 +23,7 @@ import androidx.compose.ui.window.DialogProperties fun FullScreenPicker( title: String, onDismiss: () -> Unit, + predictiveBack: Boolean = false, content: @Composable ColumnScope.() -> Unit, ) { Dialog( @@ -28,7 +33,21 @@ fun FullScreenPicker( decorFitsSystemWindows = false, ), ) { - CollapsingScaffold(title = title, onBack = onDismiss, content = content) + // The dialog window pans by default when the keyboard opens, which — + // combined with the content's own imePadding — leaves a fixed black gap + // above the keyboard. Switch it to ADJUST_NOTHING so the window stays + // full-screen and imePadding alone lifts the focused field. + val view = LocalView.current + SideEffect { + (view.parent as? DialogWindowProvider)?.window + ?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING) + } + CollapsingScaffold( + title = title, + onBack = onDismiss, + predictiveBack = predictiveBack, + content = content, + ) } } @@ -46,8 +65,11 @@ fun OptionPicker( onSelect: (T) -> Unit, onDismiss: () -> Unit, leading: (@Composable (T) -> Unit)? = null, + header: (@Composable ColumnScope.() -> Unit)? = null, + predictiveBack: Boolean = false, ) { - FullScreenPicker(title = title, onDismiss = onDismiss) { + FullScreenPicker(title = title, onDismiss = onDismiss, predictiveBack = predictiveBack) { + header?.invoke(this) options.forEachIndexed { index, option -> val isSelected = option == selected GroupedRow(