Compare commits

...

4 Commits

Author SHA1 Message Date
2124227a7f feat(components): add CustomAmountEditor, bump to 0.2.0
Extract the shared presets-plus-custom editor card — tonal surface with the
connected shape, an optional single-choice unit toggle, and an amount field
with a live preview + confirm — used by the reminder, agenda-range and snooze
pickers into one reusable, presentational component.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 18:01:42 +02:00
78c5fd1fd4 release: floret-kit 0.1.0
Finalize the 0.1.0 CHANGELOG entry (first tagged release). Consumed from
source via git submodule; tagging gives apps a stable, traceable pin.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 15:58:54 +02:00
e1919cab83 components: FullScreenPicker takes an optional actions slot
Forwards a trailing app-bar `actions` lambda through to CollapsingScaffold,
so a full-screen picker can carry a commit/extra action — a custom-value
Add/OK, a Reset — instead of being select-and-dismiss only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 14:23:07 +02:00
55ad536513 identity: add Modifier.animateContentSizeMotion()
Eases a composable's own height/size changes on the family's M3 Expressive
spatial motion scheme (fastSpatialSpec) instead of jumping — e.g. a text
field that wraps to a new line, or a value swapped for a longer one. Snaps
under reduced motion, like the other identity motion helpers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 10:55:57 +02:00
5 changed files with 117 additions and 4 deletions

View File

@@ -10,6 +10,8 @@ are no binary releases; "version" tracks the shared `version` in the root build
## [Unreleased] ## [Unreleased]
## [0.1.0] - 2026-07-07
### Added ### Added
- **`core-time`** (JVM library) — pure-Kotlin date/time helpers, no Android and - **`core-time`** (JVM library) — pure-Kotlin date/time helpers, no Android and
@@ -30,8 +32,11 @@ are no binary releases; "version" tracks the shared `version` in the root build
own thin themed `CrashReportActivity`. own thin themed `CrashReportActivity`.
- **`identity`** (Android library) — the family's Material 3 Expressive theme - **`identity`** (Android library) — the family's Material 3 Expressive theme
factory `FloretExpressiveTheme(lightScheme, darkScheme, …)` (dynamic colour on factory `FloretExpressiveTheme(lightScheme, darkScheme, …)` (dynamic colour on
API 31+, system light/dark fallback, the standard motion scheme) plus API 31+, system light/dark fallback, the standard motion scheme) plus the
`rememberNavSlideSpec()`. Each app passes its own seed-derived colour schemes shared motion helpers — `rememberNavSlideSpec()`, the content transitions,
`Modifier.predictiveBack()` and `Modifier.animateContentSizeMotion()` (eases a
block's own height changes, e.g. a text field wrapping to a new line), each
reduced-motion-aware. Each app passes its own seed-derived colour schemes
and typography — the mechanics are shared, the look is not. and typography — the mechanics are shared, the look is not.
- **`components`** (Android library) — the shared Compose component vocabulary: - **`components`** (Android library) — the shared Compose component vocabulary:
- `GroupedSurface` / `GroupedRow` + `Position` / `positionOf` — the canonical - `GroupedSurface` / `GroupedRow` + `Position` / `positionOf` — the canonical
@@ -43,7 +48,9 @@ are no binary releases; "version" tracks the shared `version` in the root build
`largeTopBar = false` for a pinned single-line bar (the picker variant). `largeTopBar = false` for a pinned single-line bar (the picker variant).
- `FullScreenPicker` / `OptionPicker` — the full-screen single-select picker, - `FullScreenPicker` / `OptionPicker` — the full-screen single-select picker,
now over a pinned single-line bar (a picker is a short list, so no tall now over a pinned single-line bar (a picker is a short list, so no tall
collapsing header to scroll past). collapsing header to scroll past). `FullScreenPicker` takes an optional
`actions` slot (app-bar trailing items) for pickers that carry a commit or
extra action — a custom-value **Add**/**OK**, a **Reset**.
- `ReorderableColumn` — dependency-free drag-to-reorder for the short, fixed - `ReorderableColumn` — dependency-free drag-to-reorder for the short, fixed
grouped-card Settings lists; pairs with `GroupedRow(gapBelow = false)`, which grouped-card Settings lists; pairs with `GroupedRow(gapBelow = false)`, which
hands the column uniform control of row spacing. hands the column uniform control of row spacing.

View File

@@ -10,5 +10,5 @@ plugins {
subprojects { subprojects {
group = "de.jeanlucmakiola.floret" group = "de.jeanlucmakiola.floret"
version = "0.1.0" version = "0.2.0"
} }

View File

@@ -1,8 +1,11 @@
package de.jeanlucmakiola.floret.components package de.jeanlucmakiola.floret.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
@@ -10,8 +13,12 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowDropDown import androidx.compose.material.icons.filled.ArrowDropDown
import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SegmentedButton
import androidx.compose.material3.SegmentedButtonDefaults
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -57,6 +64,83 @@ fun DialogAmountField(
} }
} }
/**
* The expanded "Custom" editor card shared by the family's presets-plus-custom
* pickers (reminder lead time, agenda range, snooze delay). A tonal surface with
* the connected-shape that meets the "Custom" row above it (tight top corners,
* full bottom), holding — top to bottom — an optional single-choice unit toggle,
* then a row of [DialogAmountField] + a live [preview] of what the amount
* resolves to + a tonal confirm.
*
* Purely presentational: the caller owns the amount/unit state and computes
* [preview] and [confirmEnabled] from its own domain, so this component carries
* no units, ranges, or strings of its own. Pass [unitLabels] empty for an
* amount-only editor (no toggle row). [onConfirm] fires on the confirm button.
*/
@Composable
fun CustomAmountEditor(
amountText: String,
onAmountChange: (String) -> Unit,
preview: String,
setLabel: String,
confirmEnabled: Boolean,
onConfirm: () -> Unit,
modifier: Modifier = Modifier,
placeholder: String = "10",
unitLabels: List<String> = emptyList(),
selectedUnit: Int = 0,
onUnitChange: (Int) -> Unit = {},
) {
Surface(
color = MaterialTheme.colorScheme.surfaceContainerHigh,
// A Position.Bottom shape: tight top corners meeting the row, full bottom.
shape = RoundedCornerShape(topStart = 6.dp, topEnd = 6.dp, bottomStart = 22.dp, bottomEnd = 22.dp),
modifier = modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
// Unit toggle first (when present) so it stays visible above the
// keyboard once the amount field below it is focused and scrolled in.
if (unitLabels.isNotEmpty()) {
SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) {
unitLabels.forEachIndexed { index, label ->
SegmentedButton(
selected = index == selectedUnit,
onClick = { onUnitChange(index) },
shape = SegmentedButtonDefaults.itemShape(index, unitLabels.size),
label = { Text(label) },
)
}
}
}
// Amount, a live preview of what it resolves to, and confirm — all on
// one row, sitting just above the keyboard.
Row(verticalAlignment = Alignment.CenterVertically) {
DialogAmountField(
value = amountText,
onValueChange = onAmountChange,
placeholder = placeholder,
)
Spacer(Modifier.width(16.dp))
Text(
text = preview,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.weight(1f),
)
Spacer(Modifier.width(16.dp))
FilledTonalButton(onClick = onConfirm, enabled = confirmEnabled) {
Text(setLabel)
}
}
}
}
}
/** Tonal dropdown trigger + menu shared by the custom reminder/recurrence steps and pickers. */ /** Tonal dropdown trigger + menu shared by the custom reminder/recurrence steps and pickers. */
@Composable @Composable
fun DialogUnitDropdown( fun DialogUnitDropdown(

View File

@@ -2,6 +2,7 @@ package de.jeanlucmakiola.floret.components
import android.view.WindowManager import android.view.WindowManager
import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.RowScope
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
@@ -25,6 +26,7 @@ fun FullScreenPicker(
title: String, title: String,
onDismiss: () -> Unit, onDismiss: () -> Unit,
predictiveBack: Boolean = false, predictiveBack: Boolean = false,
actions: @Composable RowScope.() -> Unit = {},
content: @Composable ColumnScope.() -> Unit, content: @Composable ColumnScope.() -> Unit,
) { ) {
Dialog( Dialog(
@@ -48,6 +50,7 @@ fun FullScreenPicker(
onBack = onDismiss, onBack = onDismiss,
largeTopBar = false, largeTopBar = false,
predictiveBack = predictiveBack, predictiveBack = predictiveBack,
actions = actions,
content = content, content = content,
) )
} }

View File

@@ -3,9 +3,12 @@ package de.jeanlucmakiola.floret.identity
import android.provider.Settings import android.provider.Settings
import androidx.activity.BackEventCompat import androidx.activity.BackEventCompat
import androidx.activity.compose.PredictiveBackHandler import androidx.activity.compose.PredictiveBackHandler
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@@ -14,6 +17,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import kotlin.coroutines.cancellation.CancellationException import kotlin.coroutines.cancellation.CancellationException
@@ -36,6 +40,21 @@ fun rememberReduceMotion(): Boolean {
} }
} }
/**
* Animates a composable's own size changes with the family's spatial motion, so
* a block that grows or shrinks — a text field wrapping to a new line, a value
* swapped for a longer one — eases into its new height instead of jumping. Wraps
* Compose's [androidx.compose.animation.animateContentSize] onto the active M3
* Expressive motion scheme; under reduced motion the new size snaps in.
*
* Apply it high in the modifier chain of the element whose height changes.
*/
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun Modifier.animateContentSizeMotion(reduceMotion: Boolean = rememberReduceMotion()): Modifier =
if (reduceMotion) this
else animateContentSize(animationSpec = MaterialTheme.motionScheme.fastSpatialSpec<IntSize>())
/** /**
* The standard Android predictive-back transform for a full-screen surface: as * The standard Android predictive-back transform for a full-screen surface: as
* the back gesture is dragged, the surface scales toward ~90%, shifts toward the * the back gesture is dragged, the surface scales toward ~90%, shifts toward the