feat(components): give OptionPicker a per-option summary slot

A picker row often names something it doesn't spell out — a range's dates, a
reminder's firing time, what "follow the system" resolves to today. The
summary slot puts that under the label, so every option shows its effect at
once and stays comparable, without the cost of a live preview.

The KDoc now also states the three rungs of picker richness (plain rows /
per-option summary / live preview) and the rule that a preview picker must
not close on selection, since closing hides the thing it exists to show.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-30 21:12:37 +02:00
parent b4d3ead8f0
commit bb3ff1f316

View File

@@ -26,6 +26,20 @@ import de.jeanlucmakiola.floret.identity.LocalFloretDarkTheme
* [scrollable] forwards to [CollapsingScaffold]: leave it on unless [content] * [scrollable] forwards to [CollapsingScaffold]: leave it on unless [content]
* scrolls itself, which a picker only needs for an option list long enough to * scrolls itself, which a picker only needs for an option list long enough to
* warrant a lazy container. * 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 @Composable
fun FullScreenPicker( fun FullScreenPicker(
@@ -82,6 +96,15 @@ fun FullScreenPicker(
* General single-select picker, full-screen: each option is a connected grouped * 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 * row and the current one carries a check. The drop-in for the family's option
* dialogs (theme, default list, reminder offset, …). * 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 @Composable
fun <T> OptionPicker( fun <T> OptionPicker(
@@ -91,6 +114,7 @@ fun <T> OptionPicker(
label: @Composable (T) -> String, label: @Composable (T) -> String,
onSelect: (T) -> Unit, onSelect: (T) -> Unit,
onDismiss: () -> Unit, onDismiss: () -> Unit,
summary: (@Composable (T) -> String?)? = null,
leading: (@Composable (T) -> Unit)? = null, leading: (@Composable (T) -> Unit)? = null,
header: (@Composable ColumnScope.() -> Unit)? = null, header: (@Composable ColumnScope.() -> Unit)? = null,
predictiveBack: Boolean = false, predictiveBack: Boolean = false,
@@ -101,6 +125,7 @@ fun <T> OptionPicker(
val isSelected = option == selected val isSelected = option == selected
GroupedRow( GroupedRow(
title = label(option), title = label(option),
summary = summary?.invoke(option),
position = positionOf(index, options.size), position = positionOf(index, options.size),
selected = isSelected, selected = isSelected,
leading = leading?.let { { it(option) } }, leading = leading?.let { { it(option) } },