Calendar week view: 7-day timeline (TickTick-style)

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 <noreply@anthropic.com>
This commit is contained in:
kutesir
2026-07-07 00:29:43 +03:00
committed by kutesir
parent 2441efe304
commit 7fdf50c898

View File

@@ -840,84 +840,28 @@ struct CalendarView: View {
// MARK: - Week // 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 { private var weekView: some View {
HStack(alignment: .top, spacing: 0) { let days = cvWeekDays()
VStack(spacing: 0) { return VStack(spacing: 0) {
HStack(spacing: 0) { cvDayColumnHeader(days)
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)
.gesture(DragGesture(minimumDistance: 40).onEnded { v in
if v.translation.width < -40 { navigateMonth(1) }
else if v.translation.width > 40 { navigateMonth(-1) }
})
Spacer()
}
.frame(maxWidth: .infinity)
Rectangle().fill(AppColors.border(cs)).frame(width: 1)
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()) .contentShape(Rectangle())
.onTapGesture { withAnimation(KisaniSpring.micro) { selectedDate = date } } .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)) Divider().background(AppColors.border(cs))
ScrollView(showsIndicators: false) {
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 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 @ViewBuilder
private func cvAllDayBand(days: [Date]) -> some View { private func cvAllDayBand(days: [Date]) -> some View {