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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user