Compare commits

...

2 Commits

Author SHA1 Message Date
566caf4305 Merge origin/main: component superset alongside core-time date util
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 22:07:10 +02:00
d9e4e877cc components: grow GroupedRow + CollapsingScaffold + pickers to the family superset
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) <noreply@anthropic.com>
2026-06-28 20:49:34 +02:00
3 changed files with 59 additions and 6 deletions

View File

@@ -3,6 +3,7 @@ package de.jeanlucmakiola.floret.components
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.imePadding 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.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import de.jeanlucmakiola.floret.identity.predictiveBack
/** /**
* The family's collapsing scaffold for settings hubs, sub-screens and the * 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 * button, and a vertically scrolling content column. [content] lays out the
* connected grouped rows. The back content description uses the kit's `back` * connected grouped rows. The back content description uses the kit's `back`
* string, which an app can override with its own (localized) `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) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@@ -39,12 +46,16 @@ fun CollapsingScaffold(
title: String, title: String,
onBack: () -> Unit, onBack: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
predictiveBack: Boolean = false,
actions: @Composable RowScope.() -> Unit = {},
snackbarHost: @Composable () -> Unit = {},
content: @Composable ColumnScope.() -> Unit, content: @Composable ColumnScope.() -> Unit,
) { ) {
val scrollBehavior = val scrollBehavior =
TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState()) TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState())
Scaffold( Scaffold(
modifier = modifier modifier = modifier
.then(if (predictiveBack) Modifier.predictiveBack(onBack = onBack) else Modifier)
.fillMaxSize() .fillMaxSize()
.background(MaterialTheme.colorScheme.surface) .background(MaterialTheme.colorScheme.surface)
.nestedScroll(scrollBehavior.nestedScrollConnection), .nestedScroll(scrollBehavior.nestedScrollConnection),
@@ -59,12 +70,14 @@ fun CollapsingScaffold(
) )
} }
}, },
actions = actions,
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
colors = TopAppBarDefaults.largeTopAppBarColors( colors = TopAppBarDefaults.largeTopAppBarColors(
scrolledContainerColor = MaterialTheme.colorScheme.surface, scrolledContainerColor = MaterialTheme.colorScheme.surface,
), ),
) )
}, },
snackbarHost = snackbarHost,
) { innerPadding -> ) { innerPadding ->
Column( Column(
modifier = Modifier modifier = Modifier

View File

@@ -82,6 +82,12 @@ fun GroupedSurface(
* One row in a grouped list: an M3 [ListItem] over a [GroupedSurface] (the * One row in a grouped list: an M3 [ListItem] over a [GroupedSurface] (the
* tonal, position-shaped, press-morphing family container). Insets 16dp like * tonal, position-shaped, press-morphing family container). Insets 16dp like
* the lists overview. * 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) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@@ -91,6 +97,8 @@ fun GroupedRow(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
summary: String? = null, summary: String? = null,
selected: Boolean = false, selected: Boolean = false,
dimmed: Boolean = false,
container: Color? = null,
minHeight: Dp = 72.dp, minHeight: Dp = 72.dp,
leading: @Composable (() -> Unit)? = null, leading: @Composable (() -> Unit)? = null,
trailing: @Composable (() -> Unit)? = null, trailing: @Composable (() -> Unit)? = null,
@@ -104,13 +112,23 @@ fun GroupedRow(
supportingColor = MaterialTheme.colorScheme.onSecondaryContainer, supportingColor = MaterialTheme.colorScheme.onSecondaryContainer,
trailingIconColor = 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 { } else {
ListItemDefaults.colors(containerColor = Color.Transparent) ListItemDefaults.colors(containerColor = Color.Transparent)
} }
val containerColor = if (selected) { val containerColor = when {
MaterialTheme.colorScheme.secondaryContainer selected -> MaterialTheme.colorScheme.secondaryContainer
} else { container != null -> container
MaterialTheme.colorScheme.surfaceContainerHigh else -> MaterialTheme.colorScheme.surfaceContainerHigh
} }
GroupedSurface( GroupedSurface(
position = position, position = position,

View File

@@ -1,13 +1,17 @@
package de.jeanlucmakiola.floret.components package de.jeanlucmakiola.floret.components
import android.view.WindowManager
import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Check import androidx.compose.material.icons.rounded.Check
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable 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.Dialog
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import androidx.compose.ui.window.DialogWindowProvider
/** /**
* Full-screen scaffold for selection pickers: a full-bleed [Dialog] that reuses * Full-screen scaffold for selection pickers: a full-bleed [Dialog] that reuses
@@ -19,6 +23,7 @@ import androidx.compose.ui.window.DialogProperties
fun FullScreenPicker( fun FullScreenPicker(
title: String, title: String,
onDismiss: () -> Unit, onDismiss: () -> Unit,
predictiveBack: Boolean = false,
content: @Composable ColumnScope.() -> Unit, content: @Composable ColumnScope.() -> Unit,
) { ) {
Dialog( Dialog(
@@ -28,7 +33,21 @@ fun FullScreenPicker(
decorFitsSystemWindows = false, 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 <T> OptionPicker(
onSelect: (T) -> Unit, onSelect: (T) -> Unit,
onDismiss: () -> Unit, onDismiss: () -> Unit,
leading: (@Composable (T) -> Unit)? = null, 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 -> options.forEachIndexed { index, option ->
val isSelected = option == selected val isSelected = option == selected
GroupedRow( GroupedRow(