M2: build out task list, detail & edit screens

Replace the base scaffolds with the real Material 3 Expressive UI on the
existing M1 ViewModels.

Task list: due-date section headers (collapsible Completed), grouped tonal
rows with a list-colour accent bar, swipe-to-complete / -delete, inline add,
list-name chip and subtask "done/total" chip in mixed views (children folded
into their parent). New pure TaskSections grouping + unit test.

Detail: each fact its own tonal card with a leading icon (Calendula pattern) —
when/list/priority/progress/description; progress shows only with subtasks.
Subtasks are a connected grouped run whose first segment is the expand toggle,
with an accent-tinted inline add row; completed subtasks sink to the bottom.

Edit: title, description, list dropdown, start/due date-time + all-day,
priority segmented buttons, reminder offset, validation; IME auto-scroll.

Extract GroupedSurface from GroupedRow so the connected-row look is reusable
at any width; add ListNameChip + date-time format/picker helpers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-18 16:03:37 +02:00
parent b196d9ebe0
commit a19b1373a4
12 changed files with 1537 additions and 119 deletions

View File

@@ -0,0 +1,47 @@
package de.jeanlucmakiola.floret.domain
import com.google.common.truth.Truth.assertThat
import org.junit.jupiter.api.Test
import kotlin.time.Instant
class TaskSectionsTest {
private val dayMs = 86_400_000L
private val todayStart = Instant.fromEpochMilliseconds(1000 * dayMs)
private val todayEnd = Instant.fromEpochMilliseconds(1001 * dayMs)
private fun at(millis: Long) = Instant.fromEpochMilliseconds(millis)
private fun sectionOf(task: Task) = TaskSections.sectionOf(task, todayStart, todayEnd)
@Test
fun `buckets by due relative to today`() {
assertThat(sectionOf(testTask(due = at(1000 * dayMs - 1)))).isEqualTo(TaskSection.OVERDUE)
assertThat(sectionOf(testTask(due = at(1000 * dayMs + 1)))).isEqualTo(TaskSection.TODAY)
assertThat(sectionOf(testTask(due = at(1001 * dayMs + 1)))).isEqualTo(TaskSection.UPCOMING)
assertThat(sectionOf(testTask(due = null))).isEqualTo(TaskSection.NO_DATE)
}
@Test
fun `completed always lands in completed regardless of due`() {
val overdueButDone = testTask(status = TaskStatus.COMPLETED, due = at(1000 * dayMs - 1))
assertThat(sectionOf(overdueButDone)).isEqualTo(TaskSection.COMPLETED)
}
@Test
fun `of returns only non-empty sections in fixed order`() {
val tasks = listOf(
testTask(id = 1, due = at(1001 * dayMs + 1)), // upcoming
testTask(id = 2, due = at(1000 * dayMs - 1)), // overdue
testTask(id = 3, status = TaskStatus.COMPLETED), // completed
)
val sections = TaskSections.of(tasks, todayStart, todayEnd)
assertThat(sections.map { it.section })
.containsExactly(TaskSection.OVERDUE, TaskSection.UPCOMING, TaskSection.COMPLETED)
.inOrder()
}
@Test
fun `empty input yields no sections`() {
assertThat(TaskSections.of(emptyList(), todayStart, todayEnd)).isEmpty()
}
}