Compare commits
3 Commits
152b0bf9b5
...
3134c064e9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3134c064e9 | ||
|
|
0a87769561 | ||
|
|
e8d74d4f59 |
@@ -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
|
||||||
@@ -395,6 +392,9 @@ struct CalendarView: View {
|
|||||||
calDayTaskListView
|
calDayTaskListView
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Pin to the top so views whose content doesn't fill the height
|
||||||
|
// (week / 3-day timelines) don't float to the bottom of the ZStack.
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||||
|
|
||||||
FAB { showAddTask = true }
|
FAB { showAddTask = true }
|
||||||
.padding(.trailing, 20).padding(.bottom, 10)
|
.padding(.trailing, 20).padding(.bottom, 10)
|
||||||
@@ -467,25 +467,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 +480,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,59 +569,25 @@ 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),
|
|
||||||
highlightColumn: !monthExpanded && cal.isDate(date, inSameDayAs: selectedDate)
|
|
||||||
)
|
|
||||||
.onTapGesture {
|
|
||||||
withAnimation(KisaniSpring.snappy) {
|
|
||||||
selectedDate = date
|
|
||||||
monthExpanded = false // collapse to this week, reveal the agenda
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 16)
|
.padding(.horizontal, 16)
|
||||||
.gesture(DragGesture(minimumDistance: 30).onEnded { v in
|
.gesture(DragGesture(minimumDistance: 40).onEnded { v in
|
||||||
let dx = v.translation.width, dy = v.translation.height
|
if v.translation.width < -40 { navigateMonth(1) }
|
||||||
if abs(dx) > abs(dy) {
|
else if v.translation.width > 40 { navigateMonth(-1) }
|
||||||
// Horizontal swipe → previous/next month
|
|
||||||
if dx < -40 { navigateMonth(1) }
|
|
||||||
else if dx > 40 { navigateMonth(-1) }
|
|
||||||
} else {
|
|
||||||
// Vertical drag → collapse to week (up) / expand to month (down)
|
|
||||||
withAnimation(KisaniSpring.snappy) {
|
|
||||||
if dy < -30 { monthExpanded = false }
|
|
||||||
else if dy > 30 { monthExpanded = true }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// 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(
|
||||||
@@ -1742,8 +1693,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]
|
||||||
var highlightColumn: Bool = false
|
|
||||||
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
|
||||||
@@ -1768,24 +1718,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)
|
|
||||||
}
|
}
|
||||||
.padding(.top, 4)
|
.frame(maxWidth: .infinity).frame(height: 42).contentShape(Rectangle())
|
||||||
.frame(maxWidth: .infinity).frame(height: 54).contentShape(Rectangle())
|
|
||||||
.background(
|
|
||||||
RoundedRectangle(cornerRadius: 10)
|
|
||||||
.fill(highlightColumn ? AppColors.text3(cs).opacity(0.12) : .clear)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user