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:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user