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 f11b6d6..0cb7535 100644 --- a/components/src/main/kotlin/de/jeanlucmakiola/floret/components/GroupedList.kt +++ b/components/src/main/kotlin/de/jeanlucmakiola/floret/components/GroupedList.kt @@ -37,6 +37,16 @@ fun positionOf(index: Int, count: Int): Position = when { else -> Position.Middle } +/** + * The inset a grouped list keeps from the screen edge. + * + * Exported because it is a *shared* line, not a private detail of [GroupedRow]: + * everything that sits outside the cards but belongs to them — section headers, + * hints, picker descriptions — has to start on the same edge, or the page reads + * as two ragged columns. Take this value rather than writing 16.dp again. + */ +val GroupedListInset = 16.dp + /** Corner radii for a grouped segment: full at the group's outer edges, small between. */ fun groupedShape(position: Position, full: Dp, small: Dp): Shape = when (position) { Position.Alone -> RoundedCornerShape(full) @@ -140,7 +150,7 @@ fun GroupedRow( } GroupedSurface( position = position, - modifier = modifier.padding(horizontal = 16.dp), + modifier = modifier.padding(horizontal = GroupedListInset), onClick = onClick, color = containerColor, gapBelow = gapBelow, 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 6c0d984..dd5e550 100644 --- a/components/src/main/kotlin/de/jeanlucmakiola/floret/components/OptionPicker.kt +++ b/components/src/main/kotlin/de/jeanlucmakiola/floret/components/OptionPicker.kt @@ -26,6 +26,20 @@ import de.jeanlucmakiola.floret.identity.LocalFloretDarkTheme * [scrollable] forwards to [CollapsingScaffold]: leave it on unless [content] * scrolls itself, which a picker only needs for an option list long enough to * warrant a lazy container. + * + * **Picker richness.** Three shapes, in ascending order — take the cheapest one + * that makes the choice obvious: + * 1. plain option rows, when the label *is* the meaning ("English", "Dark"); + * 2. rows plus a per-option summary of the concrete effect, when the meaning is + * a date, a time or a number the label only names ("Next 7 days" → + * "30 Jul – 5 Aug"). [OptionPicker]'s `summary` covers this; + * 3. a live preview above the rows, when the effect is *visual* and no words + * carry it (month grid style, dimmed past events, week start). + * + * A picker of the third kind **must not close when an option is tapped** — it + * applies immediately and stays open, because closing would hide the very thing + * the screen exists to show. The user leaves via back. The first two kinds close + * on tap as usual. */ @Composable fun FullScreenPicker( @@ -82,6 +96,15 @@ fun FullScreenPicker( * 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, …). + * + * [summary] adds a second line per option — reserve it for the option's + * *concrete effect*, the thing its label names but doesn't spell out: the dates + * a range resolves to, the time a format renders as, what "follow the system" + * currently means. Returning null leaves that row single-line, so one option + * (typically the automatic one) can carry a note the others don't need. It is + * the cheap middle rung of the richness ladder described on [FullScreenPicker]: + * every option shows its effect at once and stays comparable, without the cost + * of a live preview. */ @Composable fun OptionPicker( @@ -91,6 +114,7 @@ fun OptionPicker( label: @Composable (T) -> String, onSelect: (T) -> Unit, onDismiss: () -> Unit, + summary: (@Composable (T) -> String?)? = null, leading: (@Composable (T) -> Unit)? = null, header: (@Composable ColumnScope.() -> Unit)? = null, predictiveBack: Boolean = false, @@ -101,6 +125,7 @@ fun OptionPicker( val isSelected = option == selected GroupedRow( title = label(option), + summary = summary?.invoke(option), position = positionOf(index, options.size), selected = isSelected, leading = leading?.let { { it(option) } }, diff --git a/components/src/main/kotlin/de/jeanlucmakiola/floret/components/SettingsRecipes.kt b/components/src/main/kotlin/de/jeanlucmakiola/floret/components/SettingsRecipes.kt index 9f647da..aaebaa4 100644 --- a/components/src/main/kotlin/de/jeanlucmakiola/floret/components/SettingsRecipes.kt +++ b/components/src/main/kotlin/de/jeanlucmakiola/floret/components/SettingsRecipes.kt @@ -13,12 +13,10 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton -import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -41,6 +39,12 @@ data class AboutLink(val icon: ImageVector, val label: String, val url: String) * outlined buttons (e.g. source, licence) and an optional full-width * [highlightLink] tonal button (e.g. donate). Each link opens its URL in the * browser. The app supplies its own logo, strings and links. + * + * [position] lets the card join a grouped list instead of standing alone: with + * [Position.Top] it takes the group's top corners and the 2dp gap below, so a + * [GroupedRow] underneath continues the same container. Use that when the card's + * call to action reads better as a row of its own than as a button inside the + * card — the two then form one block rather than a card with a lid on it. */ @Composable fun AboutCard( @@ -49,15 +53,16 @@ fun AboutCard( author: String, primaryLinks: List, highlightLink: AboutLink? = null, + position: Position = Position.Alone, ) { val context = LocalContext.current val open = { url: String -> runCatching { context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url))) } Unit } - Surface( + GroupedSurface( + position = position, color = MaterialTheme.colorScheme.surfaceContainerHigh, - shape = RoundedCornerShape(24.dp), modifier = Modifier.fillMaxWidth(), ) { Column(Modifier.fillMaxWidth().padding(16.dp)) {