components: add CollapsingScaffold + OptionPicker/FullScreenPicker (batch-2)

The family's collapsing settings/sub-screen scaffold and the full-screen
single-select picker built on it (used for theme/list/reminder option dialogs).
Adds material-icons-core (ArrowBack/Check) and a generic 'back' string apps can
override. ReminderFormatting (labels over app-specific R.plurals + per-app
presets) deliberately NOT included — it needs a label-callback API, a separate
pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-28 13:24:08 +02:00
parent 1bd3e03ad5
commit 0db0e3883f
5 changed files with 171 additions and 0 deletions

View File

@@ -37,4 +37,5 @@ dependencies {
implementation(libs.androidx.ui) implementation(libs.androidx.ui)
implementation(libs.androidx.foundation) implementation(libs.androidx.foundation)
implementation(libs.androidx.material3) implementation(libs.androidx.material3)
implementation(libs.androidx.material.icons.core)
} }

View File

@@ -0,0 +1,83 @@
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.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LargeTopAppBar
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
/**
* The family's collapsing scaffold for settings hubs, sub-screens and the
* full-screen pickers: a [LargeTopAppBar] that collapses on scroll, a back
* 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`.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CollapsingScaffold(
title: String,
onBack: () -> Unit,
modifier: Modifier = Modifier,
content: @Composable ColumnScope.() -> Unit,
) {
val scrollBehavior =
TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState())
Scaffold(
modifier = modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.surface)
.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
LargeTopAppBar(
title = { Text(title) },
navigationIcon = {
IconButton(onClick = onBack) {
Icon(
Icons.AutoMirrored.Rounded.ArrowBack,
contentDescription = stringResource(R.string.back),
)
}
},
scrollBehavior = scrollBehavior,
colors = TopAppBarDefaults.largeTopAppBarColors(
scrolledContainerColor = MaterialTheme.colorScheme.surface,
),
)
},
) { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
// Consume the scaffold's bar insets so imePadding adds only the
// keyboard height beyond them (max, not sum).
.consumeWindowInsets(innerPadding)
.fillMaxSize()
.background(MaterialTheme.colorScheme.surface)
.imePadding()
.verticalScroll(rememberScrollState())
.padding(top = 8.dp, bottom = 24.dp),
content = content,
)
}
}

View File

@@ -0,0 +1,79 @@
package de.jeanlucmakiola.floret.components
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.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
/**
* Full-screen scaffold for selection pickers: a full-bleed [Dialog] that reuses
* [CollapsingScaffold] (collapsing title + back), so a picker is visually
* identical to a settings sub-page and uses the full width. [content] places the
* connected grouped rows; selecting one calls [onDismiss].
*/
@Composable
fun FullScreenPicker(
title: String,
onDismiss: () -> Unit,
content: @Composable ColumnScope.() -> Unit,
) {
Dialog(
onDismissRequest = onDismiss,
properties = DialogProperties(
usePlatformDefaultWidth = false,
decorFitsSystemWindows = false,
),
) {
CollapsingScaffold(title = title, onBack = onDismiss, content = content)
}
}
/**
* General single-select picker, full-screen: each option is a connected grouped
* row and the current one carries a check. The drop-in for the family's option
* dialogs (theme, default list, reminder offset, …).
*/
@Composable
fun <T> OptionPicker(
title: String,
options: List<T>,
selected: T,
label: @Composable (T) -> String,
onSelect: (T) -> Unit,
onDismiss: () -> Unit,
leading: (@Composable (T) -> Unit)? = null,
) {
FullScreenPicker(title = title, onDismiss = onDismiss) {
options.forEachIndexed { index, option ->
val isSelected = option == selected
GroupedRow(
title = label(option),
position = positionOf(index, options.size),
selected = isSelected,
leading = leading?.let { { it(option) } },
trailing = if (isSelected) {
{ SelectedCheck() }
} else {
null
},
onClick = {
onSelect(option)
onDismiss()
},
)
}
}
}
@Composable
private fun SelectedCheck() {
Icon(
imageVector = Icons.Rounded.Check,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
)
}

View File

@@ -0,0 +1,7 @@
<resources>
<!--
Generic component strings. App-agnostic fallbacks; an app may override any
of these with its own (localized) value via resource merge.
-->
<string name="back">Back</string>
</resources>

View File

@@ -17,6 +17,7 @@ androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", versi
androidx-ui = { group = "androidx.compose.ui", name = "ui" } androidx-ui = { group = "androidx.compose.ui", name = "ui" }
androidx-foundation = { group = "androidx.compose.foundation", name = "foundation" } androidx-foundation = { group = "androidx.compose.foundation", name = "foundation" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3" } androidx-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3" }
androidx-material-icons-core = { group = "androidx.compose.material", name = "material-icons-core" }
junit-jupiter-api = { group = "org.junit.jupiter", name = "junit-jupiter-api", version.ref = "junit" } junit-jupiter-api = { group = "org.junit.jupiter", name = "junit-jupiter-api", version.ref = "junit" }
junit-jupiter-engine = { group = "org.junit.jupiter", name = "junit-jupiter-engine", version.ref = "junit" } junit-jupiter-engine = { group = "org.junit.jupiter", name = "junit-jupiter-engine", version.ref = "junit" }