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 6456d8f69a - Show all commits

View File

@@ -294,6 +294,8 @@ struct CalendarView: View {
}()
@State private var viewMode: CalendarViewMode = .month
@State private var calTaskListMode: Bool = false
/// 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 showAddTask = false
@State private var showCalendarConnect = false
@State private var showWorkoutSession = false
@@ -333,7 +335,7 @@ struct CalendarView: View {
// Center: month + year
Spacer()
Text(monthTitle)
Text(headerTitle)
.font(AppFonts.sans(17, weight: .semibold))
.foregroundColor(AppColors.text(cs))
Spacer()
@@ -434,6 +436,12 @@ struct CalendarView: View {
}
}
/// In the continuous year view the header follows the scrolled-to year;
/// otherwise it's the month + year of the displayed month.
private var headerTitle: String {
viewMode == .year ? String(visibleYear) : monthTitle
}
private var monthTitle: String {
let f = DateFormatter(); f.dateFormat = "MMMM yyyy"
return f.string(from: displayMonth)
@@ -673,6 +681,15 @@ struct CalendarView: View {
// MARK: - Year
// Reports each year block's top offset within the year scroll, so the
// header can follow whichever year is currently at the top.
private struct YearTopKey: PreferenceKey {
static var defaultValue: [Int: CGFloat] = [:]
static func reduce(value: inout [Int: CGFloat], nextValue: () -> [Int: CGFloat]) {
value.merge(nextValue()) { _, new in new }
}
}
// Continuous, TickTick-style: years flow one after another in an infinite
// scroll. Opens anchored on the current year; scroll up for past years,
// down for future ones no paging, no dead space below December.
@@ -693,7 +710,22 @@ struct CalendarView: View {
.padding(.top, 8)
.padding(.bottom, 96) // clear the floating tab bar / + button
}
.onAppear { proxy.scrollTo(currentYear, anchor: .top) }
.coordinateSpace(name: "yearScroll")
.onPreferenceChange(YearTopKey.self) { tops in
// The header year is the topmost block that has reached the top edge:
// among blocks at/above a small threshold, the one nearest the top.
let threshold: CGFloat = 60
let reached = tops.filter { $0.value <= threshold }
if let y = reached.max(by: { $0.value < $1.value })?.key
?? tops.min(by: { $0.value < $1.value })?.key,
y != visibleYear {
visibleYear = y
}
}
.onAppear {
visibleYear = currentYear
proxy.scrollTo(currentYear, anchor: .top)
}
}
}
@@ -712,6 +744,14 @@ struct CalendarView: View {
}
.padding(.horizontal, 16)
}
.background(
GeometryReader { geo in
Color.clear.preference(
key: YearTopKey.self,
value: [year: geo.frame(in: .named("yearScroll")).minY]
)
}
)
}
private func cvMiniMonth(_ month: Date) -> some View {