Merge origin/main: component superset alongside core-time date util

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-28 22:07:10 +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(