From 07142429545f3fb515919e97e65aa00a71f32fc4 Mon Sep 17 00:00:00 2001 From: Jacob Hagstedt Date: Sun, 18 Oct 2026 23:09:39 +0200 Subject: [PATCH] =?UTF-8?q?Android=20overhaul:=20the=20=C2=A79.6=20text=20?= =?UTF-8?q?and=20date=20limits=20in=20:core:design's=20Limits.kt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DESCRIPTION_MAX (S-7 and S-19 each had 100), NOTES_MAX (S-7's 1000 and NotesField's private copy), FIRST_DATE and DAYS_AHEAD_MAX (the S-7 and S-9 date pickers each spelled 2000-01-01 and 365). Values unchanged. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B --- .../hagstedt/kvitt/design/component/Editor.kt | 8 ++++---- .../se/hagstedt/kvitt/design/format/Limits.kt | 14 ++++++++++++++ .../feature/expense/ExpenseEditorScreen.kt | 18 +++++++++--------- .../recurring/RecurringEditorScreen.kt | 1 + .../settings/recurring/RecurringForm.kt | 4 +--- .../kvitt/feature/settle/SettleScreen.kt | 10 +++++----- 6 files changed, 34 insertions(+), 21 deletions(-) diff --git a/android/core/design/src/main/kotlin/se/hagstedt/kvitt/design/component/Editor.kt b/android/core/design/src/main/kotlin/se/hagstedt/kvitt/design/component/Editor.kt index deea8e2..7c27637 100644 --- a/android/core/design/src/main/kotlin/se/hagstedt/kvitt/design/component/Editor.kt +++ b/android/core/design/src/main/kotlin/se/hagstedt/kvitt/design/component/Editor.kt @@ -22,9 +22,9 @@ import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.selection.selectableGroup import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Icon +import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text -import androidx.compose.material3.LocalContentColor import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue @@ -52,10 +52,10 @@ import androidx.compose.ui.semantics.testTag import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.rememberTextMeasurer import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -65,6 +65,7 @@ import se.hagstedt.kvitt.design.format.AmountKey import se.hagstedt.kvitt.design.format.DateFormatter import se.hagstedt.kvitt.design.format.KvittLanguage import se.hagstedt.kvitt.design.format.MoneyFormatter +import se.hagstedt.kvitt.design.format.NOTES_MAX import se.hagstedt.kvitt.design.icon.CheckBadgeIcon import se.hagstedt.kvitt.design.icon.KvittIcon import se.hagstedt.kvitt.design.theme.KvittDimens @@ -255,7 +256,7 @@ fun CategoryQuickRow(categories: List, selected: CategoryId?, onPick: /** * One category item of the quick row or the grid sheet. The check badge of a selected item overhangs the tile's top-right - * corner; it is drawn in a layer above the clipped, clickable item so the item's ripple clip never cuts it (M8b). + * corner; it is drawn in a layer above the clipped, clickable item so the item's ripple clip never cuts it. */ @Composable fun CategoryItem(category: Category, selected: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) { @@ -636,7 +637,6 @@ fun NotesField(value: String, onValueChange: (String) -> Unit, expanded: Boolean ) } -private const val NOTES_MAX = 1000 /** * The compact row pinned under the app bar when the amount has scrolled away (design-spec §3.7): tile 36, diff --git a/android/core/design/src/main/kotlin/se/hagstedt/kvitt/design/format/Limits.kt b/android/core/design/src/main/kotlin/se/hagstedt/kvitt/design/format/Limits.kt index 3d03d20..bb8a8d9 100644 --- a/android/core/design/src/main/kotlin/se/hagstedt/kvitt/design/format/Limits.kt +++ b/android/core/design/src/main/kotlin/se/hagstedt/kvitt/design/format/Limits.kt @@ -1,7 +1,21 @@ package se.hagstedt.kvitt.design.format +import kotlinx.datetime.LocalDate + /** Basis points of a whole: a ratio's per-user shares sum to 10000 bp, 100 % (SPEC §1, §9.3). */ const val BP_TOTAL = 10_000 /** A user's display name is 1 to 40 code points after trimming (SPEC §9.6). */ const val DISPLAY_NAME_MAX = 40 + +/** An expense's or recurring template's description is 1 to 100 code points (SPEC §9.6). */ +const val DESCRIPTION_MAX = 100 + +/** An expense's notes are 0 to 1000 code points (SPEC §9.6). */ +const val NOTES_MAX = 1000 + +/** The earliest expense or settlement date (SPEC §9.6). */ +val FIRST_DATE: LocalDate = LocalDate(2000, 1, 1) + +/** The latest expense or settlement date is today plus this many days (SPEC §9.6). */ +const val DAYS_AHEAD_MAX = 365 diff --git a/android/feature/expense/src/main/kotlin/se/hagstedt/kvitt/feature/expense/ExpenseEditorScreen.kt b/android/feature/expense/src/main/kotlin/se/hagstedt/kvitt/feature/expense/ExpenseEditorScreen.kt index 2c5030b..478cdda 100644 --- a/android/feature/expense/src/main/kotlin/se/hagstedt/kvitt/feature/expense/ExpenseEditorScreen.kt +++ b/android/feature/expense/src/main/kotlin/se/hagstedt/kvitt/feature/expense/ExpenseEditorScreen.kt @@ -55,7 +55,6 @@ import kotlinx.datetime.DateTimeUnit import kotlinx.datetime.LocalDate import kotlinx.datetime.plus import se.hagstedt.kvitt.design.KvittTheme -import se.hagstedt.kvitt.core.design.R as DesignR import se.hagstedt.kvitt.design.component.AccountPicker import se.hagstedt.kvitt.design.component.AmountDisplay import se.hagstedt.kvitt.design.component.AmountKeypad @@ -90,6 +89,10 @@ import se.hagstedt.kvitt.design.component.splitOption import se.hagstedt.kvitt.design.component.viewerPercentOf import se.hagstedt.kvitt.design.format.AmountInput import se.hagstedt.kvitt.design.format.AmountKey +import se.hagstedt.kvitt.design.format.DAYS_AHEAD_MAX +import se.hagstedt.kvitt.design.format.DESCRIPTION_MAX +import se.hagstedt.kvitt.design.format.FIRST_DATE +import se.hagstedt.kvitt.design.format.NOTES_MAX import se.hagstedt.kvitt.design.icon.KvittIcon import se.hagstedt.kvitt.model.AccountId import se.hagstedt.kvitt.model.CategoryId @@ -97,6 +100,7 @@ import se.hagstedt.kvitt.model.ExpenseId import se.hagstedt.kvitt.model.FieldError import se.hagstedt.kvitt.model.SplitMode import se.hagstedt.kvitt.model.UserId +import se.hagstedt.kvitt.core.design.R as DesignR /** What S-7 can ask its state holder to do; [ExpenseEditorViewModel] implements it. */ interface ExpenseEditorActions { @@ -400,8 +404,8 @@ private fun EditorDatePicker(date: String, today: String, actions: ExpenseEditor @OptIn(ExperimentalMaterial3Api::class) @Composable private fun LocalizedDatePicker(date: String, today: String, actions: ExpenseEditorActions, inline: Boolean) { - val first = remember { LocalDate(FIRST_YEAR, 1, 1).toEpochDays() * DAY_MILLIS } - val last = remember(today) { LocalDate.parse(today).plus(MAX_DAYS_AHEAD, DateTimeUnit.DAY).toEpochDays() * DAY_MILLIS } + val first = remember { FIRST_DATE.toEpochDays() * DAY_MILLIS } + val last = remember(today) { LocalDate.parse(today).plus(DAYS_AHEAD_MAX, DateTimeUnit.DAY).toEpochDays() * DAY_MILLIS } val picker = rememberDatePickerState( initialSelectedDateMillis = LocalDate.parse(date).toEpochDays() * DAY_MILLIS, selectableDates = object : SelectableDates { @@ -479,9 +483,9 @@ internal fun errorText(errors: List, field: String): String? { } ExpenseEditorViewModel.FIELD_DESCRIPTION -> when (error.error) { ExpenseEditorViewModel.REQUIRED -> stringResource(DesignR.string.error_description_required) - else -> pluralStringResource(DesignR.plurals.error_too_long, MAX_DESCRIPTION, MAX_DESCRIPTION) + else -> pluralStringResource(DesignR.plurals.error_too_long, DESCRIPTION_MAX, DESCRIPTION_MAX) } - ExpenseEditorViewModel.FIELD_NOTES -> pluralStringResource(DesignR.plurals.error_too_long, MAX_NOTES, MAX_NOTES) + ExpenseEditorViewModel.FIELD_NOTES -> pluralStringResource(DesignR.plurals.error_too_long, NOTES_MAX, NOTES_MAX) ExpenseEditorViewModel.FIELD_DATE -> stringResource(DesignR.string.error_date_out_of_range) ExpenseEditorViewModel.FIELD_SPLIT -> when (error.error) { "exact_sum_mismatch", "exact_negative" -> stringResource(DesignR.string.error_exact_share_too_large) @@ -495,8 +499,4 @@ internal fun errorText(errors: List, field: String): String? { private const val PERCENT_HUNDREDTHS = 100L private const val HALF = 50 private const val MIN_AMOUNT_ORE = 1L -private const val MAX_DESCRIPTION = 100 -private const val MAX_NOTES = 1000 -private const val FIRST_YEAR = 2000 -private const val MAX_DAYS_AHEAD = 365 private const val DAY_MILLIS = 86_400_000L diff --git a/android/feature/settings/src/main/kotlin/se/hagstedt/kvitt/feature/settings/recurring/RecurringEditorScreen.kt b/android/feature/settings/src/main/kotlin/se/hagstedt/kvitt/feature/settings/recurring/RecurringEditorScreen.kt index a4ed370..680d679 100644 --- a/android/feature/settings/src/main/kotlin/se/hagstedt/kvitt/feature/settings/recurring/RecurringEditorScreen.kt +++ b/android/feature/settings/src/main/kotlin/se/hagstedt/kvitt/feature/settings/recurring/RecurringEditorScreen.kt @@ -77,6 +77,7 @@ import se.hagstedt.kvitt.design.component.splitOption import se.hagstedt.kvitt.design.format.AmountInput import se.hagstedt.kvitt.design.format.AmountKey import se.hagstedt.kvitt.design.format.BP_TOTAL +import se.hagstedt.kvitt.design.format.DESCRIPTION_MAX import se.hagstedt.kvitt.design.format.DateFormatter import se.hagstedt.kvitt.design.format.KvittLanguage import se.hagstedt.kvitt.design.icon.KvittIcon diff --git a/android/feature/settings/src/main/kotlin/se/hagstedt/kvitt/feature/settings/recurring/RecurringForm.kt b/android/feature/settings/src/main/kotlin/se/hagstedt/kvitt/feature/settings/recurring/RecurringForm.kt index 5a7b379..8c85d63 100644 --- a/android/feature/settings/src/main/kotlin/se/hagstedt/kvitt/feature/settings/recurring/RecurringForm.kt +++ b/android/feature/settings/src/main/kotlin/se/hagstedt/kvitt/feature/settings/recurring/RecurringForm.kt @@ -4,6 +4,7 @@ import se.hagstedt.kvitt.design.component.RatioField import se.hagstedt.kvitt.design.format.AmountInput import se.hagstedt.kvitt.design.format.AmountKey import se.hagstedt.kvitt.design.format.BP_TOTAL +import se.hagstedt.kvitt.design.format.DESCRIPTION_MAX import se.hagstedt.kvitt.model.AccountId import se.hagstedt.kvitt.model.CategoryId import se.hagstedt.kvitt.model.ExpenseDraft @@ -16,9 +17,6 @@ import se.hagstedt.kvitt.model.UserId /** Which keypad field has focus (design-spec §3.7: the keypad drives the amount, the exact share and the percents). */ enum class EditorFocus { None, Amount, RatioViewer, RatioPartner, Exact } -/** §9.6: description 1 to 100, notes 0 to 1000 code points. */ -const val DESCRIPTION_MAX = 100 - /** * The S-19 editor's input, independent of the repositories: the expense fields of S-7 without the date (D-019) plus * the schedule of FR-9. Pure, so the editor's rules are unit-tested without Android. diff --git a/android/feature/settle/src/main/kotlin/se/hagstedt/kvitt/feature/settle/SettleScreen.kt b/android/feature/settle/src/main/kotlin/se/hagstedt/kvitt/feature/settle/SettleScreen.kt index 7769d04..28c53f2 100644 --- a/android/feature/settle/src/main/kotlin/se/hagstedt/kvitt/feature/settle/SettleScreen.kt +++ b/android/feature/settle/src/main/kotlin/se/hagstedt/kvitt/feature/settle/SettleScreen.kt @@ -40,7 +40,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import kotlinx.datetime.DateTimeUnit import kotlinx.datetime.LocalDate import kotlinx.datetime.plus -import se.hagstedt.kvitt.core.design.R as DesignR import se.hagstedt.kvitt.design.KvittTheme import se.hagstedt.kvitt.design.component.AccountTile import se.hagstedt.kvitt.design.component.AllocationRow @@ -69,20 +68,21 @@ import se.hagstedt.kvitt.design.component.TopBarNavigation import se.hagstedt.kvitt.design.component.detailDate import se.hagstedt.kvitt.design.component.kvittLanguage import se.hagstedt.kvitt.design.component.money +import se.hagstedt.kvitt.design.format.DAYS_AHEAD_MAX import se.hagstedt.kvitt.design.format.DateFormatter +import se.hagstedt.kvitt.design.format.FIRST_DATE import se.hagstedt.kvitt.design.format.MoneyFormatter import se.hagstedt.kvitt.design.icon.KvittIcon import se.hagstedt.kvitt.design.theme.KvittDimens import se.hagstedt.kvitt.model.Account import se.hagstedt.kvitt.model.FieldError import se.hagstedt.kvitt.model.User +import se.hagstedt.kvitt.core.design.R as DesignR /** §9.6: the longest settlement note, in code points. */ private const val NOTE_MAX = 200 -/** §9.6: dates from 2000-01-01 to today + 365 days. */ -private val FIRST_DATE = LocalDate(2000, 1, 1) -private const val DAYS_AHEAD = 365 +/** The date picker counts in UTC milliseconds; dates run from [FIRST_DATE] to today + [DAYS_AHEAD_MAX] days (§9.6). */ private const val MILLIS_PER_DAY = 86_400_000L /** @@ -440,7 +440,7 @@ private fun AddAccountRow(account: Account, colorIndex: Int, onClick: () -> Unit @Composable private fun SettleDatePicker(isoDate: String, actions: SettleActions) { val today = LocalKvittClock.current.today - val last = today.plus(DAYS_AHEAD, DateTimeUnit.DAY) + val last = today.plus(DAYS_AHEAD_MAX, DateTimeUnit.DAY) val selectable = remember(today) { object : SelectableDates { override fun isSelectableDate(utcTimeMillis: Long): Boolean {