diff --git a/CHANGELOG.md b/CHANGELOG.md index d220c5d..bb95f76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,20 @@ are no binary releases; "version" tracks the shared `version` in the root build ### Added +- **`components`** — picker-row helpers lifted from Calendula's pickers and + settings, all additive. `GroupedRow` takes `checked` (default `false`): it + implies `selected` and fills an empty trailing slot with `SelectedCheck`, + replacing the hand-written `if (selected) { { SelectedCheck() } } else null`. + `OptionPicker` takes `dismissOnSelect` (default `true`, today's behaviour) for + preview pickers that stay open, and its rows are public as `OptionRows` for a + picker that composes its own `FullScreenPicker`. `CustomExpandRow` is the + "Custom…" row of a presets-plus-custom picker, revealing its editor + (typically `CustomAmountEditor`) as one connected container. + `GroupedSwitchRow` is the settings toggle row where the whole row flips the + switch. `GroupedHint` is the muted text beside the cards (settings hints, + picker descriptions), and `ExpandIcon` the turning chevron, now also used by + `InstructionSteps`. + - **`identity`** — a pure-black (AMOLED) dark theme. `ColorScheme.toPureBlack()` sets `background`, `surface`, `surfaceDim` and `surfaceContainerLowest` to #000 and pulls the other containers down towards black, keeping their hue 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 ee96595..06e1dec 100644 --- a/components/src/main/kotlin/de/jeanlucmakiola/floret/components/GroupedList.kt +++ b/components/src/main/kotlin/de/jeanlucmakiola/floret/components/GroupedList.kt @@ -159,6 +159,10 @@ fun GroupedSurface( * the inter-row gap for a caller that owns its own uniform spacing, e.g. * [ReorderableColumn]. [onLongClick] (see [GroupedSurface]) adds a long-press to * the row. + * + * [checked] marks a picker row as the chosen one: it implies [selected] and puts + * [SelectedCheck] in the trailing slot when [trailing] is null — the single-select + * picker row without the `if (selected) { { SelectedCheck() } } else null` dance. */ @Composable fun GroupedRow( @@ -176,6 +180,7 @@ fun GroupedRow( trailing: @Composable (() -> Unit)? = null, onClick: (() -> Unit)? = null, onLongClick: (() -> Unit)? = null, + checked: Boolean = false, ) = GroupedRow( title = AnnotatedString(title), position = position, @@ -191,6 +196,7 @@ fun GroupedRow( trailing = trailing, onClick = onClick, onLongClick = onLongClick, + checked = checked, ) /** @@ -215,8 +221,15 @@ fun GroupedRow( trailing: @Composable (() -> Unit)? = null, onClick: (() -> Unit)? = null, onLongClick: (() -> Unit)? = null, + checked: Boolean = false, ) { - val itemColors = if (selected) { + val highlighted = selected || checked + val trailingContent = trailing ?: if (checked) { + { SelectedCheck() } + } else { + null + } + val itemColors = if (highlighted) { ListItemDefaults.colors( containerColor = Color.Transparent, headlineColor = MaterialTheme.colorScheme.onSecondaryContainer, @@ -246,7 +259,7 @@ fun GroupedRow( ListItemDefaults.colors(containerColor = Color.Transparent) } val containerColor = when { - selected -> MaterialTheme.colorScheme.secondaryContainer + highlighted -> MaterialTheme.colorScheme.secondaryContainer container != null -> container else -> MaterialTheme.colorScheme.surfaceContainerHigh } @@ -262,7 +275,7 @@ fun GroupedRow( headlineContent = { Text(title) }, supportingContent = summary?.let { text -> { Text(text) } }, leadingContent = leading, - trailingContent = trailing, + trailingContent = trailingContent, colors = itemColors, modifier = Modifier.heightIn(min = minHeight), ) diff --git a/components/src/main/kotlin/de/jeanlucmakiola/floret/components/GroupedRowRecipes.kt b/components/src/main/kotlin/de/jeanlucmakiola/floret/components/GroupedRowRecipes.kt new file mode 100644 index 0000000..5070150 --- /dev/null +++ b/components/src/main/kotlin/de/jeanlucmakiola/floret/components/GroupedRowRecipes.kt @@ -0,0 +1,130 @@ +package de.jeanlucmakiola.floret.components + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.KeyboardArrowDown +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import de.jeanlucmakiola.floret.identity.collapseExit +import de.jeanlucmakiola.floret.identity.expandEnter + +/** + * A [GroupedRow] with a trailing [Switch], where a tap anywhere on the row flips + * it — the family's settings toggle. [enabled] false dims the row and disables + * both the row and the switch. + */ +@Composable +fun GroupedSwitchRow( + title: String, + checked: Boolean, + onCheckedChange: (Boolean) -> Unit, + position: Position, + modifier: Modifier = Modifier, + summary: String? = null, + enabled: Boolean = true, + leading: @Composable (() -> Unit)? = null, +) { + GroupedRow( + title = title, + position = position, + modifier = modifier, + summary = summary, + dimmed = !enabled, + leading = leading, + trailing = { + Switch(checked = checked, onCheckedChange = onCheckedChange, enabled = enabled) + }, + onClick = if (enabled) ({ onCheckedChange(!checked) }) else null, + ) +} + +/** + * The "Custom…" row that closes a presets-plus-custom picker: a [GroupedRow] that + * toggles [expanded] on tap and reveals [editor] beneath it (typically a + * [CustomAmountEditor]). Expanded, the row takes [Position.Top] so it and the + * editor read as one container; collapsed it sits at [position]. + * + * [checked] marks the custom value as the current choice, as on the preset rows; + * the caller names it through [title]/[summary]. + */ +@Composable +fun CustomExpandRow( + title: String, + position: Position, + expanded: Boolean, + onExpandedChange: (Boolean) -> Unit, + modifier: Modifier = Modifier, + summary: String? = null, + checked: Boolean = false, + editor: @Composable () -> Unit, +) { + Column(modifier) { + GroupedRow( + title = title, + summary = summary, + position = if (expanded) Position.Top else position, + onClick = { onExpandedChange(!expanded) }, + checked = checked, + ) + AnimatedVisibility( + visible = expanded, + enter = expandEnter(), + exit = collapseExit(), + ) { + editor() + } + } +} + +/** + * Muted supporting text outside the cards — under a [GroupedSectionHeader], or + * above a picker's rows — inset to [GroupedListInset] so it shares their edge. + * [style] and [verticalPadding] cover the denser variants (`bodySmall`, 8dp under + * a picker title). + */ +@Composable +fun GroupedHint( + text: String, + modifier: Modifier = Modifier, + style: TextStyle = MaterialTheme.typography.bodyMedium, + verticalPadding: Dp = 4.dp, +) { + Text( + text = text, + style = style, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = modifier.padding(horizontal = GroupedListInset, vertical = verticalPadding), + ) +} + +/** + * The open/shut chevron for a row that folds content beneath it; turns over as + * [expanded] changes. Decorative — the row carries the semantics. + */ +@Composable +fun ExpandIcon( + expanded: Boolean, + modifier: Modifier = Modifier, + tint: Color = MaterialTheme.colorScheme.onSurfaceVariant, +) { + val turn by animateFloatAsState(if (expanded) 180f else 0f, label = "expand-icon") + Icon( + imageVector = Icons.Rounded.KeyboardArrowDown, + contentDescription = null, + tint = tint, + modifier = modifier.rotate(turn), + ) +} diff --git a/components/src/main/kotlin/de/jeanlucmakiola/floret/components/InstructionSteps.kt b/components/src/main/kotlin/de/jeanlucmakiola/floret/components/InstructionSteps.kt index 2979ee9..e8dfa21 100644 --- a/components/src/main/kotlin/de/jeanlucmakiola/floret/components/InstructionSteps.kt +++ b/components/src/main/kotlin/de/jeanlucmakiola/floret/components/InstructionSteps.kt @@ -1,15 +1,11 @@ package de.jeanlucmakiola.floret.components -import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.KeyboardArrowDown -import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -20,7 +16,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.rotate import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp @@ -74,7 +69,7 @@ fun InstructionSteps( minHeight = 56.dp, // No disc: the empty leading slot is what tells the header // apart from the numbered rows it opens. - trailing = { Chevron(open) }, + trailing = { ExpandIcon(open) }, onClick = { open = !open }, ) } @@ -141,22 +136,5 @@ private fun StepNumber(number: Int) { } } -/** - * The open/shut marker. - * - * Decorative: the row it sits on carries the name and the click, so a screen - * reader announcing a chevron as well would be describing the furniture. - */ -@Composable -private fun Chevron(open: Boolean) { - val turn by animateFloatAsState(if (open) 180f else 0f, label = "instruction-chevron") - Icon( - imageVector = Icons.Rounded.KeyboardArrowDown, - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.rotate(turn), - ) -} - /** The leading slot's own size, matched to the icons that usually fill it. */ private val StepNumberSize = 28.dp 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 dd5e550..eca0fba 100644 --- a/components/src/main/kotlin/de/jeanlucmakiola/floret/components/OptionPicker.kt +++ b/components/src/main/kotlin/de/jeanlucmakiola/floret/components/OptionPicker.kt @@ -105,6 +105,9 @@ fun FullScreenPicker( * 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. + * + * [dismissOnSelect] closes the picker on a pick (the default). Pass false for a + * preview picker, which applies at once and stays open (see [FullScreenPicker]). */ @Composable fun OptionPicker( @@ -118,28 +121,49 @@ fun OptionPicker( leading: (@Composable (T) -> Unit)? = null, header: (@Composable ColumnScope.() -> Unit)? = null, predictiveBack: Boolean = false, + dismissOnSelect: Boolean = true, ) { FullScreenPicker(title = title, onDismiss = onDismiss, predictiveBack = predictiveBack) { header?.invoke(this) - options.forEachIndexed { index, option -> - val isSelected = option == selected - GroupedRow( - title = label(option), - summary = summary?.invoke(option), - position = positionOf(index, options.size), - selected = isSelected, - leading = leading?.let { { it(option) } }, - trailing = if (isSelected) { - { SelectedCheck() } - } else { - null - }, - onClick = { - onSelect(option) - onDismiss() - }, - ) - } + OptionRows( + options = options, + selected = selected, + label = label, + onSelect = { + onSelect(it) + if (dismissOnSelect) onDismiss() + }, + summary = summary, + leading = leading, + ) + } +} + +/** + * [OptionPicker]'s option list on its own: one connected run of [GroupedRow]s, + * the current option highlighted and checked. For a picker that composes its own + * [FullScreenPicker] — a preview above the rows, a description between — but + * wants the standard rows underneath. [onSelect] only reports the pick; closing + * the picker is the caller's call. + */ +@Composable +fun OptionRows( + options: List, + selected: T, + label: @Composable (T) -> String, + onSelect: (T) -> Unit, + summary: (@Composable (T) -> String?)? = null, + leading: (@Composable (T) -> Unit)? = null, +) { + options.forEachIndexed { index, option -> + GroupedRow( + title = label(option), + summary = summary?.invoke(option), + position = positionOf(index, options.size), + leading = leading?.let { { it(option) } }, + onClick = { onSelect(option) }, + checked = option == selected, + ) } }