feat: upcoming section — collapse, 5-item cap, show more

Default view shows the next 5 upcoming tasks. If more exist, a
"N more ↓" button expands the full list; tapping again collapses
back to 5. The section header is tappable to fully hide the list
(only title + count badge remain visible). Collapsed state is
persisted via @AppStorage so it survives app restarts.
This commit is contained in:
kutesir
2026-05-30 02:42:50 +03:00
parent cab5c4ef05
commit a11258dea5

View File

@@ -132,32 +132,12 @@ struct TodayView: View {
// Upcoming
if !taskVM.upcomingTasks.isEmpty {
HStack {
SectionLabel(text: "Upcoming")
Text("\(taskVM.upcomingTasks.count)")
.font(AppFonts.mono(9))
.foregroundColor(AppColors.text3(cs).opacity(0.55))
}
.padding(.horizontal, 18)
.padding(.top, 14)
.padding(.bottom, 6)
ForEach(taskVM.upcomingTasks.prefix(20)) { task in
TaskRowView(task: task) {
withAnimation(KisaniSpring.snappy) { taskVM.toggle(task) }
}
.contextMenu {
Button { withAnimation { taskVM.postpone(task) } } label: {
Label("Postpone 1 Day", systemImage: "clock.arrow.circlepath")
}
Divider()
Button(role: .destructive) { withAnimation { taskVM.delete(task) } } label: {
Label("Delete", systemImage: "trash")
}
}
.padding(.horizontal, 18)
.padding(.bottom, 5)
}
UpcomingSection(
tasks: taskVM.upcomingTasks,
onToggle: { taskVM.toggle($0) },
onPostpone: { taskVM.postpone($0) },
onDelete: { taskVM.delete($0) }
)
}
let hasAnything = !taskVM.overdueTasks.isEmpty || !taskVM.todayTasks.isEmpty
@@ -580,6 +560,104 @@ private struct SessionSectionHeader: View {
}
}
// MARK: - Upcoming Section
struct UpcomingSection: View {
@Environment(\.colorScheme) private var cs
let tasks: [TaskItem]
let onToggle: (TaskItem) -> Void
let onPostpone: (TaskItem) -> Void
let onDelete: (TaskItem) -> Void
@AppStorage("upcomingCollapsed") private var collapsed = false
@State private var showAll = false
private let pageSize = 5
private var visible: [TaskItem] {
showAll ? tasks : Array(tasks.prefix(pageSize))
}
var body: some View {
VStack(alignment: .leading, spacing: 0) {
// Header (tappable to collapse)
HStack(spacing: 6) {
Text("Upcoming")
.font(AppFonts.sans(10.5, weight: .semibold))
.foregroundColor(AppColors.text3(cs))
Text("\(tasks.count)")
.font(AppFonts.mono(9, weight: .semibold))
.foregroundColor(AppColors.text3(cs).opacity(0.5))
.padding(.horizontal, 6).padding(.vertical, 2)
.background(AppColors.surface2(cs))
.clipShape(Capsule())
Spacer()
Image(systemName: "chevron.down")
.font(.system(size: 10, weight: .semibold))
.foregroundColor(AppColors.text3(cs).opacity(0.6))
.rotationEffect(.degrees(collapsed ? -90 : 0))
.animation(KisaniSpring.micro, value: collapsed)
}
.padding(.horizontal, 18)
.padding(.top, 14)
.padding(.bottom, collapsed ? 2 : 8)
.contentShape(Rectangle())
.onTapGesture {
withAnimation(KisaniSpring.snappy) {
collapsed.toggle()
if !collapsed { showAll = false }
}
}
if !collapsed {
// Task rows
ForEach(visible) { task in
TaskRowView(task: task) {
withAnimation(KisaniSpring.snappy) { onToggle(task) }
}
.contextMenu {
Button { withAnimation { onPostpone(task) } } label: {
Label("Postpone 1 Day", systemImage: "clock.arrow.circlepath")
}
Divider()
Button(role: .destructive) { withAnimation { onDelete(task) } } label: {
Label("Delete", systemImage: "trash")
}
}
.padding(.horizontal, 18)
.padding(.bottom, 5)
}
// Show more / collapse overflow
if tasks.count > pageSize {
Button {
withAnimation(KisaniSpring.snappy) { showAll.toggle() }
} label: {
HStack(spacing: 5) {
Text(showAll
? "Show less"
: "\(tasks.count - pageSize) more")
.font(AppFonts.mono(9.5, weight: .semibold))
.foregroundColor(AppColors.accent)
Image(systemName: showAll ? "chevron.up" : "chevron.down")
.font(.system(size: 8, weight: .bold))
.foregroundColor(AppColors.accent)
}
.padding(.horizontal, 18)
.padding(.top, 2)
.padding(.bottom, 4)
}
.buttonStyle(.plain)
}
}
}
}
}
// MARK: - Completed Today Section
struct CompletedTodaySection: View {
@Environment(\.colorScheme) private var cs