Android overhaul: the §9.6 text and date limits in :core:design's Limits.kt

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
This commit is contained in:
2026-10-18 23:09:39 +02:00
parent e08aaf099b
commit 0714242954
6 changed files with 34 additions and 21 deletions
@@ -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<Category>, 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,
@@ -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
@@ -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<FieldError>, 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<FieldError>, 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
@@ -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
@@ -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.
@@ -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 {