From 11a54cd5193679758b344433a6b8087a499967df Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Sun, 28 Jun 2026 20:29:57 +0200 Subject: [PATCH] refactor(ui): draw InlineTextField/OptionCard/DialogControls from floret-kit components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adopt the floret-kit components module for the three widgets whose kit form is identical to Calendula's, deleting the app-local copies and repointing imports. One behavioural guard: the kit's InlineTextField defaults capitalization to Sentences (the family default), where Calendula's defaulted to None. The three Calendula call sites that relied on the old default now pass KeyboardCapitalization.None explicitly — notably the guest **email** field, which must not auto-capitalize — so behaviour is unchanged. The richer app-local primitives (GroupedRow with dimmed/container, CollapsingScaffold with actions/snackbar/predictive-back) stay local for now: they're supersets of the kit's versions, so consuming the kit forms would drop features. Promoting those into the kit is a follow-up. Compile + unit tests + lintDebug green. Co-Authored-By: Claude Opus 4.8 (1M context) --- app/build.gradle.kts | 1 + .../calendula/ui/calendars/CalendarsScreen.kt | 6 +- .../calendula/ui/common/DialogControls.kt | 94 ------------------- .../calendula/ui/common/InlineTextField.kt | 83 ---------------- .../calendula/ui/common/OptionCard.kt | 94 ------------------- .../calendula/ui/common/Picker.kt | 1 + .../calendula/ui/detail/EventDetailScreen.kt | 2 +- .../calendula/ui/edit/EventEditScreen.kt | 11 ++- .../calendula/ui/imports/ImportScreen.kt | 2 +- .../calendula/ui/search/SearchScreen.kt | 4 +- 10 files changed, 17 insertions(+), 281 deletions(-) delete mode 100644 app/src/main/java/de/jeanlucmakiola/calendula/ui/common/DialogControls.kt delete mode 100644 app/src/main/java/de/jeanlucmakiola/calendula/ui/common/InlineTextField.kt delete mode 100644 app/src/main/java/de/jeanlucmakiola/calendula/ui/common/OptionCard.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6aa33f8..0c4ad3a 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -164,6 +164,7 @@ dependencies { implementation("de.jeanlucmakiola.floret:core-crash") implementation("de.jeanlucmakiola.floret:core-reminders") implementation("de.jeanlucmakiola.floret:identity") + implementation("de.jeanlucmakiola.floret:components") implementation(libs.kotlinx.coroutines.core) debugImplementation(libs.androidx.ui.tooling) diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/calendars/CalendarsScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/calendars/CalendarsScreen.kt index e1f16ad..008b999 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/calendars/CalendarsScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/calendars/CalendarsScreen.kt @@ -96,15 +96,15 @@ import de.jeanlucmakiola.calendula.data.prefs.SettingsPrefs import de.jeanlucmakiola.calendula.domain.CalendarSource import de.jeanlucmakiola.calendula.ui.common.CALENDAR_COLOR_PALETTE import de.jeanlucmakiola.calendula.ui.common.CalendarColorChip -import de.jeanlucmakiola.calendula.ui.common.DialogAmountField -import de.jeanlucmakiola.calendula.ui.common.DialogUnitDropdown +import de.jeanlucmakiola.floret.components.DialogAmountField +import de.jeanlucmakiola.floret.components.DialogUnitDropdown import de.jeanlucmakiola.floret.identity.collapseExit import de.jeanlucmakiola.floret.identity.expandEnter import de.jeanlucmakiola.floret.identity.predictiveBack import de.jeanlucmakiola.calendula.ui.common.CollapsingScaffold import de.jeanlucmakiola.calendula.ui.common.ColorSwatchRow import de.jeanlucmakiola.calendula.ui.common.GroupedRow -import de.jeanlucmakiola.calendula.ui.common.InlineTextField +import de.jeanlucmakiola.floret.components.InlineTextField import de.jeanlucmakiola.calendula.ui.common.Position import de.jeanlucmakiola.calendula.ui.common.pastelize import java.time.LocalDate diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/DialogControls.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/DialogControls.kt deleted file mode 100644 index 7f8fa08..0000000 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/DialogControls.kt +++ /dev/null @@ -1,94 +0,0 @@ -package de.jeanlucmakiola.calendula.ui.common - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowDropDown -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.unit.dp - -/** - * Tonal 3-digit number input shared by the custom reminder/recurrence steps and - * the reminder pickers — the app's [InlineTextField] over a tonal surface, so it - * matches the card/grouped-row design language (not Material's outlined field). - */ -@Composable -fun DialogAmountField( - value: String, - onValueChange: (String) -> Unit, - placeholder: String, -) { - // surfaceContainerHighest — the picker/dialog sits on surfaceContainerHigh, - // so anything lower vanishes. - Surface( - color = MaterialTheme.colorScheme.surfaceContainerHighest, - shape = RoundedCornerShape(12.dp), - ) { - InlineTextField( - value = value, - onValueChange = { text -> - if (text.length <= 3 && text.all(Char::isDigit)) onValueChange(text) - }, - placeholder = placeholder, - textStyle = MaterialTheme.typography.titleMedium, - keyboardType = KeyboardType.Number, - modifier = Modifier - .width(72.dp) - .padding(horizontal = 14.dp, vertical = 12.dp), - ) - } -} - -/** Tonal dropdown trigger + menu shared by the custom reminder/recurrence steps and pickers. */ -@Composable -fun DialogUnitDropdown( - label: String, - entries: List, - onPick: (Int) -> Unit, -) { - var open by remember { mutableStateOf(false) } - Box { - Surface( - color = MaterialTheme.colorScheme.surfaceContainerHighest, - shape = RoundedCornerShape(12.dp), - onClick = { open = true }, - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(start = 14.dp, end = 8.dp, top = 12.dp, bottom = 12.dp), - ) { - Text(text = label, style = MaterialTheme.typography.titleMedium) - Spacer(Modifier.width(4.dp)) - Icon(imageVector = Icons.Default.ArrowDropDown, contentDescription = null) - } - } - DropdownMenu(expanded = open, onDismissRequest = { open = false }) { - entries.forEachIndexed { index, entry -> - DropdownMenuItem( - text = { Text(entry) }, - onClick = { - onPick(index) - open = false - }, - ) - } - } - } -} diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/InlineTextField.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/InlineTextField.kt deleted file mode 100644 index 6a541f1..0000000 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/InlineTextField.kt +++ /dev/null @@ -1,83 +0,0 @@ -package de.jeanlucmakiola.calendula.ui.common - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.text.BasicTextField -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.SolidColor -import androidx.compose.ui.graphics.isSpecified -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardCapitalization -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.unit.dp - -/** - * The app's borderless text input: no underline, no outline, just the tonal - * card behind it. This is the standard input across the app — we deliberately - * don't use Material's outlined/filled text fields, so anything that takes text - * (the event form, the calendar manager, dialogs) uses this inside a tonal - * [androidx.compose.material3.Surface]. - */ -@Composable -fun InlineTextField( - value: String, - onValueChange: (String) -> Unit, - placeholder: String, - modifier: Modifier = Modifier - .fillMaxWidth() - .padding(vertical = 4.dp), - textStyle: TextStyle = MaterialTheme.typography.titleMedium, - singleLine: Boolean = true, - minLines: Int = 1, - keyboardType: KeyboardType = KeyboardType.Text, - capitalization: KeyboardCapitalization = KeyboardCapitalization.None, - imeAction: ImeAction = ImeAction.Default, - /** Invoked when the IME action key (e.g. Done) is pressed. */ - onImeAction: (() -> Unit)? = null, -) { - val resolvedStyle = textStyle.copy( - color = if (textStyle.color.isSpecified) { - textStyle.color - } else { - MaterialTheme.colorScheme.onSurface - }, - ) - BasicTextField( - value = value, - onValueChange = onValueChange, - textStyle = resolvedStyle, - singleLine = singleLine, - minLines = minLines, - keyboardOptions = KeyboardOptions( - keyboardType = keyboardType, - capitalization = capitalization, - imeAction = imeAction, - ), - keyboardActions = onImeAction?.let { action -> - KeyboardActions(onAny = { action() }) - } ?: KeyboardActions.Default, - cursorBrush = SolidColor(MaterialTheme.colorScheme.primary), - decorationBox = { innerTextField -> - Box { - if (value.isEmpty()) { - // Clearly fainter than typed text, so a hint never reads as - // prefilled content. - Text( - text = placeholder, - style = resolvedStyle, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f), - ) - } - innerTextField() - } - }, - modifier = modifier, - ) -} diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/OptionCard.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/OptionCard.kt deleted file mode 100644 index d6f214b..0000000 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/OptionCard.kt +++ /dev/null @@ -1,94 +0,0 @@ -package de.jeanlucmakiola.calendula.ui.common - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -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.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.isSpecified -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.unit.dp - -/** - * The app's standard pick in a selection dialog: a full-width tonal card, - * optionally with a leading icon and a supporting line; the selected option - * is highlighted. Stack with 8dp gaps inside an AlertDialog — this is the - * only sanctioned selection-modal style (no radio rows, no bare text lists). - */ -@Composable -fun OptionCard( - label: String, - onClick: () -> Unit, - modifier: Modifier = Modifier, - icon: ImageVector? = null, - /** Icon tint override, e.g. a calendar colour; unspecified follows selection. */ - iconTint: Color = Color.Unspecified, - supportingText: String? = null, - selected: Boolean = false, - /** Label colour override, e.g. primary for an emphasised "Custom" entry. */ - labelColor: Color = Color.Unspecified, -) { - val contentColor = if (selected) { - MaterialTheme.colorScheme.onSecondaryContainer - } else { - MaterialTheme.colorScheme.onSurface - } - Surface( - onClick = onClick, - color = if (selected) { - MaterialTheme.colorScheme.secondaryContainer - } else { - MaterialTheme.colorScheme.surfaceContainerHighest - }, - shape = RoundedCornerShape(12.dp), - modifier = modifier.fillMaxWidth(), - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(horizontal = 16.dp, vertical = 14.dp), - ) { - if (icon != null) { - Icon( - imageVector = icon, - contentDescription = null, - tint = when { - iconTint.isSpecified -> iconTint - selected -> MaterialTheme.colorScheme.onSecondaryContainer - else -> MaterialTheme.colorScheme.onSurfaceVariant - }, - modifier = Modifier.size(20.dp), - ) - Spacer(Modifier.width(12.dp)) - } - Column { - Text( - text = label, - style = MaterialTheme.typography.titleMedium, - color = if (labelColor.isSpecified) labelColor else contentColor, - ) - if (supportingText != null) { - Text( - text = supportingText, - style = MaterialTheme.typography.bodySmall, - color = if (selected) { - MaterialTheme.colorScheme.onSecondaryContainer.copy(alpha = 0.8f) - } else { - MaterialTheme.colorScheme.onSurfaceVariant - }, - ) - } - } - } - } -} diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/Picker.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/Picker.kt index 1a2004b..725eabd 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/Picker.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/Picker.kt @@ -39,6 +39,7 @@ import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogWindowProvider import de.jeanlucmakiola.calendula.R import de.jeanlucmakiola.calendula.ui.agenda.AgendaRange +import de.jeanlucmakiola.floret.components.DialogAmountField import de.jeanlucmakiola.floret.identity.collapseExit import de.jeanlucmakiola.floret.identity.expandEnter import de.jeanlucmakiola.floret.reminders.ReminderOverride diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/detail/EventDetailScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/detail/EventDetailScreen.kt index 8392514..ca40014 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/detail/EventDetailScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/detail/EventDetailScreen.kt @@ -93,7 +93,7 @@ import de.jeanlucmakiola.calendula.domain.RecurringWriteScope import de.jeanlucmakiola.calendula.domain.Reminder import de.jeanlucmakiola.floret.identity.predictiveBack import de.jeanlucmakiola.calendula.ui.common.CalendarFailure -import de.jeanlucmakiola.calendula.ui.common.OptionCard +import de.jeanlucmakiola.floret.components.OptionCard import de.jeanlucmakiola.calendula.ui.common.currentLocale import de.jeanlucmakiola.calendula.ui.common.LocalUse24HourFormat import de.jeanlucmakiola.calendula.ui.common.timeOfDayFormatter diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditScreen.kt index 3067217..9228d4d 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditScreen.kt @@ -93,6 +93,7 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -120,13 +121,13 @@ import de.jeanlucmakiola.floret.identity.predictiveBack import de.jeanlucmakiola.calendula.ui.common.CALENDAR_COLOR_PALETTE import de.jeanlucmakiola.calendula.ui.common.CalendarDatePickerDialog import de.jeanlucmakiola.calendula.ui.common.ColorSwatchRow -import de.jeanlucmakiola.calendula.ui.common.DialogAmountField -import de.jeanlucmakiola.calendula.ui.common.DialogUnitDropdown +import de.jeanlucmakiola.floret.components.DialogAmountField +import de.jeanlucmakiola.floret.components.DialogUnitDropdown import de.jeanlucmakiola.calendula.ui.common.eventFormFieldIcon import de.jeanlucmakiola.calendula.ui.common.eventFormFieldLabel import de.jeanlucmakiola.calendula.ui.common.MILLIS_PER_DAY -import de.jeanlucmakiola.calendula.ui.common.InlineTextField -import de.jeanlucmakiola.calendula.ui.common.OptionCard +import de.jeanlucmakiola.floret.components.InlineTextField +import de.jeanlucmakiola.floret.components.OptionCard import de.jeanlucmakiola.calendula.ui.common.Position import de.jeanlucmakiola.calendula.ui.common.positionOf import de.jeanlucmakiola.calendula.ui.common.REMINDER_PRESETS @@ -1569,6 +1570,7 @@ private fun AddGuestInlineCard( value = text, onValueChange = { text = it }, placeholder = stringResource(R.string.event_edit_add_guest_hint), + capitalization = KeyboardCapitalization.None, keyboardType = KeyboardType.Email, imeAction = ImeAction.Done, onImeAction = { commit() }, @@ -1855,6 +1857,7 @@ private fun InlineField( singleLine = singleLine, minLines = minLines, keyboardType = keyboardType, + capitalization = KeyboardCapitalization.None, ) } diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/imports/ImportScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/imports/ImportScreen.kt index 64d7bcd..fb05680 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/imports/ImportScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/imports/ImportScreen.kt @@ -39,7 +39,7 @@ import de.jeanlucmakiola.calendula.R import de.jeanlucmakiola.calendula.domain.EventForm import de.jeanlucmakiola.calendula.domain.ics.IcsParseWarning import de.jeanlucmakiola.floret.identity.predictiveBack -import de.jeanlucmakiola.calendula.ui.common.OptionCard +import de.jeanlucmakiola.floret.components.OptionCard /** * Handles an opened/received `.ics` file. A single event is handed straight to diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/search/SearchScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/search/SearchScreen.kt index 6a90766..69bc290 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/search/SearchScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/search/SearchScreen.kt @@ -42,6 +42,7 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel @@ -51,7 +52,7 @@ import de.jeanlucmakiola.calendula.domain.EventInstance import de.jeanlucmakiola.floret.identity.animateItemMotion import de.jeanlucmakiola.floret.identity.predictiveBack import de.jeanlucmakiola.calendula.ui.common.GroupedRow -import de.jeanlucmakiola.calendula.ui.common.InlineTextField +import de.jeanlucmakiola.floret.components.InlineTextField import de.jeanlucmakiola.calendula.ui.common.Position import de.jeanlucmakiola.calendula.ui.common.currentLocale import de.jeanlucmakiola.calendula.ui.common.LocalUse24HourFormat @@ -101,6 +102,7 @@ fun SearchScreen( value = query, onValueChange = viewModel::setQuery, placeholder = stringResource(R.string.search_hint), + capitalization = KeyboardCapitalization.None, imeAction = ImeAction.Search, onImeAction = { keyboard?.hide() }, modifier = Modifier