From c5285a2e30b662572bae22971272b807184919b5 Mon Sep 17 00:00:00 2001 From: kutesir Date: Tue, 7 Jul 2026 00:29:43 +0300 Subject: [PATCH] Calendar week view: 7-day timeline (TickTick-style) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the mini-month + agenda-list split with a proper weekly timeline — hours down the left axis, one column per weekday, events as time-positioned blocks — reusing the same cvTimeGrid/cvDayColumnHeader the day and 3-day views already use. Horizontal swipe navigates by week (navigateWeek). Removes the now-unused cvAgendaItems helper. (Not build-verified — sim runtime removed to reclaim disk; reuses existing timeline components.) Co-Authored-By: Claude Opus 4.8 --- KisaniCal/Views/CalendarView.swift | 95 ++++++------------------------ 1 file changed, 17 insertions(+), 78 deletions(-) diff --git a/KisaniCal/Views/CalendarView.swift b/KisaniCal/Views/CalendarView.swift index c8eaa92..1ff4523 100644 --- a/KisaniCal/Views/CalendarView.swift +++ b/KisaniCal/Views/CalendarView.swift @@ -840,84 +840,28 @@ struct CalendarView: View { // MARK: - Week + // Week: a 7-day timeline (TickTick-style) — hours down the left, one column + // per weekday, events as positioned blocks. Reuses the day/3-day time grid. private var weekView: some View { - HStack(alignment: .top, spacing: 0) { - VStack(spacing: 0) { - HStack(spacing: 0) { - ForEach(Array(weekdays.enumerated()), id: \.offset) { _, d in - Text(d).font(AppFonts.mono(7, weight: .semibold)) - .foregroundColor(AppColors.text3(cs)).frame(maxWidth: .infinity) - } - } - .padding(.horizontal, 6).padding(.top, 10) - LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 0), count: 7), spacing: 2) { - ForEach(Array(gridDays().enumerated()), id: \.offset) { _, date in - let isTod = cal.isDateInToday(date) - let isSel = cal.isDate(date, inSameDayAs: selectedDate) - let inMon = cal.isDate(date, equalTo: displayMonth, toGranularity: .month) - ZStack { - if isTod { Circle().fill(AppColors.accent).frame(width: 20, height: 20) } - else if isSel { Circle().fill(AppColors.accentSoft).frame(width: 20, height: 20) } - Text("\(cal.component(.day, from: date))") - .font(AppFonts.sans(9, weight: isTod ? .bold : .regular)) - .foregroundColor(isTod ? .white : inMon ? AppColors.text(cs) : AppColors.text3(cs)) - } - .frame(height: 22) - .onTapGesture { withAnimation(KisaniSpring.micro) { selectedDate = date } } - } - } - .padding(.horizontal, 4) + let days = cvWeekDays() + return VStack(spacing: 0) { + cvDayColumnHeader(days) + .contentShape(Rectangle()) .gesture(DragGesture(minimumDistance: 40).onEnded { v in - if v.translation.width < -40 { navigateMonth(1) } - else if v.translation.width > 40 { navigateMonth(-1) } + if v.translation.width < -40 { navigateWeek(1) } + else if v.translation.width > 40 { navigateWeek(-1) } }) - Spacer() - } - .frame(maxWidth: .infinity) - - Rectangle().fill(AppColors.border(cs)).frame(width: 1) - + Divider().background(AppColors.border(cs)) ScrollView(showsIndicators: false) { - VStack(spacing: 0) { - ForEach(cvWeekDays(), id: \.self) { date in - let isTod = cal.isDateInToday(date) - let isSel = cal.isDate(date, inSameDayAs: selectedDate) - let items = cvAgendaItems(date) - VStack(alignment: .leading, spacing: 5) { - HStack(spacing: 4) { - Text(cvDayFmt.string(from: date)) - .font(AppFonts.mono(9, weight: .semibold)) - .foregroundColor(isTod ? AppColors.accent : AppColors.text2(cs)) - Text("\(cal.component(.day, from: date))") - .font(AppFonts.sans(12, weight: isTod ? .bold : .medium)) - .foregroundColor(isTod ? AppColors.accent : AppColors.text(cs)) - } - if items.isEmpty { - Text("Free").font(AppFonts.sans(9)).foregroundColor(AppColors.text3(cs)) - } else { - ForEach(Array(items.prefix(4).enumerated()), id: \.offset) { _, item in - HStack(spacing: 5) { - RoundedRectangle(cornerRadius: 1.5).fill(item.color) - .frame(width: 2.5, height: 11) - Text(item.title).font(AppFonts.sans(9)) - .foregroundColor(AppColors.text2(cs)).lineLimit(1) - } - } - if items.count > 4 { - Text("+\(items.count - 4) more").font(AppFonts.sans(8)) - .foregroundColor(AppColors.text3(cs)) - } - } - } - .padding(10) - .background(isSel ? AppColors.accentSoft : Color.clear) - .contentShape(Rectangle()) - .onTapGesture { withAnimation(KisaniSpring.micro) { selectedDate = date } } - Divider().background(AppColors.border(cs)) - } - } + cvTimeGrid(days: days).padding(.bottom, 40) } - .frame(maxWidth: .infinity) + } + } + + private func navigateWeek(_ offset: Int) { + withAnimation(KisaniSpring.snappy) { + selectedDate = cal.date(byAdding: .day, value: offset * 7, to: selectedDate) ?? selectedDate + displayMonth = cal.date(from: cal.dateComponents([.year, .month], from: selectedDate)) ?? displayMonth } } @@ -997,11 +941,6 @@ struct CalendarView: View { return items } - /// Everything happening on a date — all-day items first, then timed — for compact - /// agenda listings (e.g. the Week column). Recurrence-aware. - private func cvAgendaItems(_ date: Date) -> [CVTimeItem] { - (cvAllDayItems(date) + cvTimeItems(date)).sorted { $0.startMin < $1.startMin } - } @ViewBuilder private func cvAllDayBand(days: [Date]) -> some View {