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 ──
|
// ── Upcoming ──
|
||||||
if !taskVM.upcomingTasks.isEmpty {
|
if !taskVM.upcomingTasks.isEmpty {
|
||||||
HStack {
|
UpcomingSection(
|
||||||
SectionLabel(text: "Upcoming")
|
tasks: taskVM.upcomingTasks,
|
||||||
Text("\(taskVM.upcomingTasks.count)")
|
onToggle: { taskVM.toggle($0) },
|
||||||
.font(AppFonts.mono(9))
|
onPostpone: { taskVM.postpone($0) },
|
||||||
.foregroundColor(AppColors.text3(cs).opacity(0.55))
|
onDelete: { taskVM.delete($0) }
|
||||||
}
|
)
|
||||||
.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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let hasAnything = !taskVM.overdueTasks.isEmpty || !taskVM.todayTasks.isEmpty
|
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
|
// MARK: - Completed Today Section
|
||||||
struct CompletedTodaySection: View {
|
struct CompletedTodaySection: View {
|
||||||
@Environment(\.colorScheme) private var cs
|
@Environment(\.colorScheme) private var cs
|
||||||
|
|||||||
Reference in New Issue
Block a user