feat: My Tasks widget, event countdown redesign, completed section

- My Tasks widget: today's tasks with interactive check-off (ToggleTaskIntent)
  and a "+" that opens the app's add-task sheet; writes preserve all task
  fields and sync via the App Group.
- Event Countdown: AZURE/AWS-style header (accent name + countdown label +
  percent); tap the label to cycle days → weeks → time remaining.
- Today: collapsible "Completed" archive section below Upcoming.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Robin Kutesa
2026-06-03 13:31:04 +03:00
parent 8b094691f5
commit 646cdf6a9b
10 changed files with 386 additions and 44 deletions

View File

@@ -123,6 +123,16 @@ struct TodayView: View {
)
}
// Completed (all, collapsible)
if !taskVM.completedTasks.isEmpty {
CompletedSection(
tasks: taskVM.completedTasks,
onUndo: { taskVM.toggle($0) }
)
.padding(.horizontal, 18)
.padding(.top, 10)
}
let hasAnything = !taskVM.overdueTasks.isEmpty || !taskVM.todayTasks.isEmpty
|| !taskVM.upcomingTasks.isEmpty || !taskVM.completedTodayTasks.isEmpty
|| !taskVM.next3DaysTasks.isEmpty || !todayCalEvents.isEmpty
@@ -1009,6 +1019,74 @@ struct CompletedTodaySection: View {
}
}
// MARK: - Completed Section (all completed, collapsible archive)
struct CompletedSection: View {
@Environment(\.colorScheme) private var cs
let tasks: [TaskItem]
let onUndo: (TaskItem) -> Void
@State private var expanded = false
private let dateFmt: DateFormatter = {
let f = DateFormatter(); f.dateFormat = "MMM d"; return f
}()
var body: some View {
VStack(spacing: 0) {
// Header
HStack {
Text("Completed")
.font(AppFonts.sans(15, weight: .bold))
.foregroundColor(AppColors.text2(cs))
Spacer()
Text("\(tasks.count)")
.font(AppFonts.mono(11))
.foregroundColor(AppColors.text3(cs))
Image(systemName: "chevron.down")
.font(.system(size: 11, weight: .semibold))
.foregroundColor(AppColors.text3(cs))
.rotationEffect(.degrees(expanded ? 0 : -90))
.animation(KisaniSpring.micro, value: expanded)
}
.padding(.horizontal, 14).padding(.vertical, 12)
.contentShape(Rectangle())
.onTapGesture { withAnimation(KisaniSpring.snappy) { expanded.toggle() } }
if expanded {
ForEach(tasks) { task in
HStack(spacing: 12) {
Button {
withAnimation(KisaniSpring.snappy) { onUndo(task) }
} label: {
Image(systemName: "checkmark.square.fill")
.font(.system(size: 22))
.foregroundColor(AppColors.text3(cs).opacity(0.6))
}
.buttonStyle(.plain)
Text(task.title)
.font(AppFonts.sans(14))
.foregroundColor(AppColors.text3(cs))
.strikethrough(true, color: AppColors.text3(cs).opacity(0.7))
.lineLimit(1)
Spacer(minLength: 8)
if let d = task.completedAt {
Text(dateFmt.string(from: d))
.font(AppFonts.sans(13))
.foregroundColor(AppColors.text3(cs))
}
}
.padding(.horizontal, 14).padding(.vertical, 10)
}
.padding(.bottom, 4)
}
}
.background(AppColors.surface(cs))
.clipShape(RoundedRectangle(cornerRadius: AppRadius.large))
.overlay(RoundedRectangle(cornerRadius: AppRadius.large).stroke(AppColors.border(cs), lineWidth: 1))
}
}
// MARK: - Task Row
struct TaskRowView: View {
@Environment(\.colorScheme) private var cs