From 6e20d665fd794492ac24d1634bccb6635d7facf2 Mon Sep 17 00:00:00 2001 From: kutesir Date: Tue, 7 Jul 2026 00:39:29 +0300 Subject: [PATCH] =?UTF-8?q?Calendar=20week=20view:=20timeline=20=E2=87=84?= =?UTF-8?q?=20grid=20layout=20toggle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Completes the TickTick weekly reference — a toggle flips the week between the timeline and a grid of day-cards (2 columns), each card listing that day's schedule as colored bars. Tapping a card opens that day. Both layouts swipe by week. - weekGrid state + toggle button (timeline/grid icons) - weekGridLayout + weekDayCard (Not build-verified — sim runtime removed to reclaim disk.) Co-Authored-By: Claude Opus 4.8 --- KisaniCal/Views/CalendarView.swift | 103 ++++++++++++++++++++++++++--- 1 file changed, 93 insertions(+), 10 deletions(-) diff --git a/KisaniCal/Views/CalendarView.swift b/KisaniCal/Views/CalendarView.swift index 24ad8b0..26917dd 100644 --- a/KisaniCal/Views/CalendarView.swift +++ b/KisaniCal/Views/CalendarView.swift @@ -299,6 +299,8 @@ struct CalendarView: View { /// Month view: full grid when true, only the selected day's week when false /// (TickTick-style — collapses to give the day agenda more room). @State private var monthExpanded: Bool = true + /// Week view: timeline when false, grid of day-cards when true. + @State private var weekGrid: Bool = false @State private var showAddTask = false @State private var showCalendarConnect = false @State private var showWorkoutSession = false @@ -841,20 +843,101 @@ 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. + // per weekday, events as positioned blocks. Toggle to a grid of day-cards. private var weekView: some View { let days = cvWeekDays() return VStack(spacing: 0) { - cvDayColumnHeader(days) - .contentShape(Rectangle()) - .gesture(DragGesture(minimumDistance: 40).onEnded { v in - if v.translation.width < -40 { navigateWeek(1) } - else if v.translation.width > 40 { navigateWeek(-1) } - }) - Divider().background(AppColors.border(cs)) - ScrollView(showsIndicators: false) { - cvTimeGrid(days: days).padding(.bottom, 40) + // Layout toggle: timeline ⇄ grid + HStack { + Spacer() + Button { + withAnimation(KisaniSpring.snappy) { weekGrid.toggle() } + } label: { + Image(systemName: weekGrid ? "calendar.day.timeline.left" : "square.grid.2x2") + .font(.system(size: 13, weight: .semibold)) + .foregroundColor(AppColors.text2(cs)) + .padding(.horizontal, 10).padding(.vertical, 5) + .background(AppColors.surface2(cs)) + .clipShape(RoundedRectangle(cornerRadius: 8)) + } + .buttonStyle(.plain) } + .padding(.horizontal, 14).padding(.top, 6).padding(.bottom, 2) + + if weekGrid { + weekGridLayout(days) + } else { + cvDayColumnHeader(days) + .contentShape(Rectangle()) + .gesture(DragGesture(minimumDistance: 40).onEnded { v in + if v.translation.width < -40 { navigateWeek(1) } + else if v.translation.width > 40 { navigateWeek(-1) } + }) + Divider().background(AppColors.border(cs)) + ScrollView(showsIndicators: false) { + cvTimeGrid(days: days).padding(.bottom, 40) + } + } + } + } + + // Grid layout: the 7 days as cards (2 columns), each listing its schedule. + private func weekGridLayout(_ days: [Date]) -> some View { + ScrollView(showsIndicators: false) { + LazyVGrid(columns: [GridItem(.flexible(), spacing: 10), GridItem(.flexible(), spacing: 10)], spacing: 10) { + ForEach(days, id: \.self) { date in + weekDayCard(date) + } + } + .padding(16) + } + .gesture(DragGesture(minimumDistance: 40).onEnded { v in + if v.translation.width < -40 { navigateWeek(1) } + else if v.translation.width > 40 { navigateWeek(-1) } + }) + } + + private func weekDayCard(_ date: Date) -> some View { + let items = (cvAllDayItems(date) + cvTimeItems(date)).sorted { $0.startMin < $1.startMin } + let isTod = cal.isDateInToday(date) + let isSel = cal.isDate(date, inSameDayAs: selectedDate) + return VStack(alignment: .leading, spacing: 6) { + HStack(spacing: 5) { + Text(cvDayFmt.string(from: date)) + .font(AppFonts.mono(9, weight: .semibold)) + .foregroundColor(isTod ? AppColors.accent : AppColors.text3(cs)) + Text("\(cal.component(.day, from: date))") + .font(AppFonts.sans(14, weight: isTod ? .bold : .semibold)) + .foregroundColor(isTod ? AppColors.accent : AppColors.text(cs)) + Spacer(minLength: 0) + } + if items.isEmpty { + Text("Free").font(AppFonts.sans(9)).foregroundColor(AppColors.text3(cs)) + } else { + ForEach(Array(items.prefix(5).enumerated()), id: \.offset) { _, item in + HStack(spacing: 4) { + RoundedRectangle(cornerRadius: 2).fill(item.color).frame(width: 3, height: 12) + Text(item.title).font(AppFonts.sans(9)) + .foregroundColor(AppColors.text2(cs)).lineLimit(1) + Spacer(minLength: 0) + } + } + if items.count > 5 { + Text("+\(items.count - 5) more").font(AppFonts.sans(8)).foregroundColor(AppColors.text3(cs)) + } + } + } + .padding(10) + .frame(maxWidth: .infinity, minHeight: 92, alignment: .topLeading) + .background(AppColors.surface(cs)) + .clipShape(RoundedRectangle(cornerRadius: AppRadius.large)) + .overlay( + RoundedRectangle(cornerRadius: AppRadius.large) + .stroke(isSel ? AppColors.accent : AppColors.border(cs), lineWidth: isSel ? 1.5 : 1) + ) + .contentShape(Rectangle()) + .onTapGesture { + withAnimation(KisaniSpring.micro) { selectedDate = date; viewMode = .day } } }