Merge develop -> main: full session (KC-51 through KC-71) #28

Merged
kutesir merged 64 commits from develop into main 2026-07-12 22:57:14 +00:00
Showing only changes of commit 0a87769561 - Show all commits

View File

@@ -296,9 +296,6 @@ struct CalendarView: View {
@State private var calTaskListMode: Bool = false @State private var calTaskListMode: Bool = false
/// Year currently at the top of the continuous year scroll (drives the header in .year mode). /// Year currently at the top of the continuous year scroll (drives the header in .year mode).
@State private var visibleYear: Int = Calendar.current.component(.year, from: Date()) @State private var visibleYear: Int = Calendar.current.component(.year, from: Date())
/// 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. /// Week view: timeline when false, grid of day-cards when true.
@State private var weekGrid: Bool = false @State private var weekGrid: Bool = false
@State private var showAddTask = false @State private var showAddTask = false
@@ -467,25 +464,10 @@ struct CalendarView: View {
CalendarGrid.monthGrid(for: displayMonth, calendar: cal) CalendarGrid.monthGrid(for: displayMonth, calendar: cal)
} }
/// The month grid split into week rows of 7.
private func weekRows() -> [[Date]] {
let days = gridDays()
return stride(from: 0, to: days.count, by: 7).map { Array(days[$0..<min($0 + 7, days.count)]) }
}
/// The single week row that contains the currently selected day.
private func selectedWeekRow() -> [Date] {
let rows = weekRows()
return rows.first { row in row.contains { cal.isDate($0, inSameDayAs: selectedDate) } } ?? (rows.first ?? [])
}
/// Colors of the schedule bars shown under a day in the month grid
/// (workout · calendar events · tasks), capped so cells stay compact.
private func eventDots(for date: Date) -> [Color] { private func eventDots(for date: Date) -> [Color] {
let maxBars = 4
var dots: [Color] = [] var dots: [Color] = []
if workoutVM.program(for: date) != nil { dots.append(AppColors.blue) } if workoutVM.program(for: date) != nil { dots.append(AppColors.blue) }
if calStore.isAuthorized && dots.count < maxBars { if calStore.isAuthorized && dots.count < 3 {
let calDay = calStore.events(for: date) let calDay = calStore.events(for: date)
if !calDay.isEmpty { if !calDay.isEmpty {
let calColor = calDay.first.flatMap { Color(cgColor: $0.calendar.cgColor) } ?? AppColors.green let calColor = calDay.first.flatMap { Color(cgColor: $0.calendar.cgColor) } ?? AppColors.green
@@ -495,7 +477,7 @@ struct CalendarView: View {
// Includes recurring occurrences that land on this date. // Includes recurring occurrences that land on this date.
let taskDots = taskVM.occurrences(on: date) let taskDots = taskVM.occurrences(on: date)
.filter { !$0.isComplete } .filter { !$0.isComplete }
.prefix(max(0, maxBars - dots.count)) .prefix(max(0, 3 - dots.count))
.map { $0.quadrant.color } .map { $0.quadrant.color }
dots.append(contentsOf: taskDots) dots.append(contentsOf: taskDots)
return dots return dots
@@ -584,27 +566,16 @@ struct CalendarView: View {
} }
.padding(.horizontal, 16).padding(.top, 14).padding(.bottom, 3) .padding(.horizontal, 16).padding(.top, 14).padding(.bottom, 3)
// Full month (expanded) or just the selected day's week (collapsed). LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 1), count: 7), spacing: 1) {
let rows = monthExpanded ? weekRows() : [selectedWeekRow()] ForEach(gridDays(), id: \.self) { date in
VStack(spacing: 1) { DayCell(
ForEach(rows.indices, id: \.self) { ri in date: date,
HStack(spacing: 1) { isCurrentMonth: cal.isDate(date, equalTo: displayMonth, toGranularity: .month),
ForEach(rows[ri], id: \.self) { date in isToday: cal.isDateInToday(date),
DayCell( isSelected: cal.isDate(date, inSameDayAs: selectedDate),
date: date, dots: eventDots(for: date)
isCurrentMonth: cal.isDate(date, equalTo: displayMonth, toGranularity: .month), )
isToday: cal.isDateInToday(date), .onTapGesture { withAnimation(KisaniSpring.micro) { selectedDate = date } }
isSelected: cal.isDate(date, inSameDayAs: selectedDate),
bars: eventDots(for: date)
)
.onTapGesture {
withAnimation(KisaniSpring.snappy) {
selectedDate = date
monthExpanded = false // collapse to this week, reveal the agenda
}
}
}
}
} }
} }
.padding(.horizontal, 16) .padding(.horizontal, 16)
@@ -613,19 +584,7 @@ struct CalendarView: View {
else if v.translation.width > 40 { navigateMonth(-1) } else if v.translation.width > 40 { navigateMonth(-1) }
}) })
// Grabber expand back to the full month, or collapse to the week. Divider().background(AppColors.border(cs)).padding(.horizontal, 16).padding(.top, 6)
Button {
withAnimation(KisaniSpring.snappy) { monthExpanded.toggle() }
} label: {
Image(systemName: monthExpanded ? "chevron.up" : "chevron.down")
.font(.system(size: 11, weight: .semibold))
.foregroundColor(AppColors.text3(cs))
.frame(maxWidth: .infinity).frame(height: 22)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
Divider().background(AppColors.border(cs)).padding(.horizontal, 16)
ScrollView(showsIndicators: false) { ScrollView(showsIndicators: false) {
DayTimelineView( DayTimelineView(
@@ -1731,7 +1690,7 @@ struct AddCalendarHolidaysView: View {
// MARK: - Day Cell // MARK: - Day Cell
struct DayCell: View { struct DayCell: View {
@Environment(\.colorScheme) private var cs @Environment(\.colorScheme) private var cs
let date: Date; let isCurrentMonth: Bool; let isToday: Bool; let isSelected: Bool; let bars: [Color] let date: Date; let isCurrentMonth: Bool; let isToday: Bool; let isSelected: Bool; let dots: [Color]
private let cal = Calendar.current private let cal = Calendar.current
var dayNum: String { "\(cal.component(.day, from: date))" } var dayNum: String { "\(cal.component(.day, from: date))" }
/// Locale week-of-year (NOT ISO), intentionally matching the locale-driven grid /// Locale week-of-year (NOT ISO), intentionally matching the locale-driven grid
@@ -1756,19 +1715,14 @@ struct DayCell: View {
Text("W\(weekNum)").font(AppFonts.mono(6.5)).foregroundColor(AppColors.text3(cs)).offset(x: -2, y: -10) Text("W\(weekNum)").font(AppFonts.mono(6.5)).foregroundColor(AppColors.text3(cs)).offset(x: -2, y: -10)
} }
} }
// Schedule bars one per event/task, stacked (TickTick-style). HStack(spacing: 2) {
VStack(spacing: 1.5) { ForEach(dots.indices, id: \.self) { i in
ForEach(bars.indices, id: \.self) { i in Circle().fill(dots[i]).frame(width: 3.5, height: 3.5)
RoundedRectangle(cornerRadius: 1)
.fill(bars[i].opacity(0.85))
.frame(height: 3)
} }
} }
.frame(maxWidth: .infinity, alignment: .top) .frame(height: 4)
.padding(.horizontal, 3)
Spacer(minLength: 0)
} }
.frame(maxWidth: .infinity).frame(height: 54).contentShape(Rectangle()) .frame(maxWidth: .infinity).frame(height: 42).contentShape(Rectangle())
} }
} }