Calendar week view: timeline ⇄ grid layout toggle
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user