From a11258dea5eddd64a1b0154bab7458a956edd4a4 Mon Sep 17 00:00:00 2001 From: kutesir Date: Sat, 30 May 2026 02:42:50 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20upcoming=20section=20=E2=80=94=20collap?= =?UTF-8?q?se,=205-item=20cap,=20show=20more?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- KisaniCal/Views/TodayView.swift | 130 +++++++++++++++++++++++++------- 1 file changed, 104 insertions(+), 26 deletions(-) diff --git a/KisaniCal/Views/TodayView.swift b/KisaniCal/Views/TodayView.swift index 311d0a5..13f59bb 100644 --- a/KisaniCal/Views/TodayView.swift +++ b/KisaniCal/Views/TodayView.swift @@ -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