From e0e4f709b67c6d0838cd91be4d0c4fe01144a42a Mon Sep 17 00:00:00 2001 From: kutesir Date: Fri, 29 May 2026 13:51:37 +0300 Subject: [PATCH] feat: replace day strip with vertical timeline in month view Selected-day panel below the month grid now renders as a proper vertical timeline: time labels in the left gutter, continuous track line with circle markers, and white cards for each item. All-day tasks and events get a stroke circle; timed entries get a solid colored dot; workout gets a dumbbell icon; completed items show a checkmark circle with strikethrough. Tasks can be toggled directly from the timeline card. Cal events show end time as subtitle. Empty state shows a calendar icon. --- KisaniCal/Views/CalendarView.swift | 570 ++++++++++++++++++++--------- 1 file changed, 407 insertions(+), 163 deletions(-) diff --git a/KisaniCal/Views/CalendarView.swift b/KisaniCal/Views/CalendarView.swift index 992e529..0a428f2 100644 --- a/KisaniCal/Views/CalendarView.swift +++ b/KisaniCal/Views/CalendarView.swift @@ -114,6 +114,7 @@ struct CalendarView: View { return c.date(from: c.dateComponents([.year, .month], from: Date())) ?? Date() }() @State private var viewMode: CalendarViewMode = .month + @State private var calTaskListMode: Bool = false @State private var showAddTask = false @State private var showCalendarConnect = false @State private var showWorkoutSession = false @@ -127,31 +128,49 @@ struct CalendarView: View { VStack(spacing: 0) { // ── Header ── - HStack { + HStack(spacing: 0) { + // Left circle: view mode picker + Menu { + Picker(selection: $viewMode, label: EmptyView()) { + ForEach(CalendarViewMode.allCases, id: \.self) { mode in + Label(mode.rawValue, systemImage: mode.sfSymbol).tag(mode) + } + } + } label: { + Image(systemName: viewMode.sfSymbol) + .font(.system(size: 15, weight: .medium)) + .foregroundColor(AppColors.text(cs)) + .frame(width: 46, height: 46) + .background(AppColors.surface(cs)) + .clipShape(Circle()) + .contentShape(Circle()) + .shadow(color: .black.opacity(0.08), radius: 10, x: 0, y: 3) + } + .menuStyle(.automatic) + + // Center: month name + Spacer() Text(monthShortTitle) - .font(AppFonts.sans(26, weight: .bold)) + .font(AppFonts.sans(17, weight: .semibold)) .foregroundColor(AppColors.text(cs)) Spacer() + + // Right: controls pill (view mode picker + more) HStack(spacing: 0) { - // View mode picker - Menu { - Picker(selection: $viewMode, label: EmptyView()) { - ForEach(CalendarViewMode.allCases, id: \.self) { mode in - Label(mode.rawValue, systemImage: mode.sfSymbol).tag(mode) - } - } + // Right pill left button: task list toggle + Button { + withAnimation(KisaniSpring.snappy) { calTaskListMode.toggle() } } label: { - Image(systemName: "slider.horizontal.3") - .font(.system(size: 14, weight: .medium)) - .foregroundColor(AppColors.text(cs)) + Image(systemName: calTaskListMode ? "calendar" : "slider.horizontal.3") + .font(.system(size: 14, weight: calTaskListMode ? .semibold : .medium)) + .foregroundColor(calTaskListMode ? AppColors.accent : AppColors.text(cs)) .frame(width: 46, height: 36) + .contentShape(Rectangle()) } .buttonStyle(.plain) - // Divider Rectangle() .fill(AppColors.border(cs)) .frame(width: 1, height: 18) - // More options Menu { Button { } label: { Label("Filter View Range", systemImage: "line.3.horizontal.decrease") } Button { } label: { Label("View Options", systemImage: "gearshape.2") } @@ -169,8 +188,9 @@ struct CalendarView: View { .font(.system(size: 14, weight: .medium)) .foregroundColor(AppColors.text(cs)) .frame(width: 46, height: 36) + .contentShape(Rectangle()) } - .buttonStyle(.plain) + .menuStyle(.automatic) } .background(AppColors.surface2(cs)) .clipShape(Capsule()) @@ -178,10 +198,17 @@ struct CalendarView: View { } .padding(.horizontal, 18).padding(.top, 8) - calendarContent + if calTaskListMode { + cvWeekStrip.padding(.top, 12).padding(.bottom, 6) + Divider().background(AppColors.border(cs)).padding(.horizontal, 16) + calDayTaskListView + } else { + calendarContent + } } - FAB { showAddTask = true }.padding(.trailing, 20).padding(.bottom, 10) + FAB { showAddTask = true } + .padding(.trailing, 20).padding(.bottom, 10) } .sheet(isPresented: $showAddTask) { AddTaskSheet(prefilledDate: selectedDate) @@ -257,6 +284,67 @@ struct CalendarView: View { return dots } + // MARK: - Task list mode (toggle view) + + private var calDayTaskListView: some View { + let all = taskVM.tasks.filter { t in + guard let d = t.dueDate else { return false } + return cal.isDate(d, inSameDayAs: selectedDate) + } + let incomplete = all.filter { !$0.isComplete } + let complete = all.filter { $0.isComplete } + + return ScrollView(showsIndicators: false) { + VStack(alignment: .leading, spacing: 0) { + if !incomplete.isEmpty { + Text("Today") + .font(AppFonts.sans(12, weight: .semibold)) + .foregroundColor(AppColors.text2(cs)) + .padding(.horizontal, 18).padding(.top, 14).padding(.bottom, 8) + + VStack(spacing: 0) { + ForEach(Array(incomplete.enumerated()), id: \.element.id) { i, task in + CalTaskRow(task: task) { taskVM.toggle(task) } + if i < incomplete.count - 1 { + Divider().background(AppColors.border(cs)).padding(.leading, 52) + } + } + } + .background(AppColors.surface(cs)) + .clipShape(RoundedRectangle(cornerRadius: AppRadius.large)) + .overlay(RoundedRectangle(cornerRadius: AppRadius.large).stroke(AppColors.border(cs), lineWidth: 1)) + .padding(.horizontal, 16) + } + + if !complete.isEmpty { + Text("Completed") + .font(AppFonts.sans(12, weight: .semibold)) + .foregroundColor(AppColors.text3(cs)) + .padding(.horizontal, 18).padding(.top, 16).padding(.bottom, 8) + + VStack(spacing: 0) { + ForEach(Array(complete.enumerated()), id: \.element.id) { i, task in + CalTaskRow(task: task) { taskVM.toggle(task) } + if i < complete.count - 1 { + Divider().background(AppColors.border(cs)).padding(.leading, 52) + } + } + } + .background(AppColors.surface(cs)) + .clipShape(RoundedRectangle(cornerRadius: AppRadius.large)) + .overlay(RoundedRectangle(cornerRadius: AppRadius.large).stroke(AppColors.border(cs), lineWidth: 1)) + .padding(.horizontal, 16) + } + + if incomplete.isEmpty && complete.isEmpty { + cvEmptyDay.padding(.top, 48) + } + + Spacer().frame(height: 90) + } + } + } + // MARK: - Content switcher @ViewBuilder private var calendarContent: some View { @@ -303,18 +391,24 @@ struct CalendarView: View { Divider().background(AppColors.border(cs)).padding(.horizontal, 16).padding(.top, 6) ScrollView(showsIndicators: false) { - DayStripView( + DayTimelineView( date: selectedDate, - tasks: taskVM.tasks.filter { !$0.isComplete }, + tasks: taskVM.tasks.filter { t in + guard let d = t.dueDate else { return false } + return cal.isDate(d, inSameDayAs: selectedDate) + }, calEvents: calStore.isAuthorized ? calStore.events(for: selectedDate) : [], workout: workoutVM.program(for: selectedDate), onWorkoutTap: { if let w = workoutVM.program(for: selectedDate), workoutVM.activeProgramId != w.id { workoutVM.switchProgram(to: w.id) } showWorkoutSession = true + }, + onToggle: { task in + withAnimation(KisaniSpring.snappy) { taskVM.toggle(task) } } ) - .padding(.horizontal, 16).padding(.vertical, 10) + .padding(.horizontal, 16).padding(.top, 12).padding(.bottom, 20) } } } @@ -868,169 +962,319 @@ struct DayCell: View { } } -// MARK: - Day Strip -struct DayStripView: View { +// MARK: - Day Timeline + +private struct DayTLEntry: Identifiable { + var id = UUID() + var timeLabel: String + var ampm: String? + var title: String + var subtitle: String? + var color: Color + var isComplete: Bool + var isAllDay: Bool + var sortKey: Int // < 0 = all-day/special; else minute-of-day + var taskRef: TaskItem? + var workoutRef: WorkoutProgram? +} + +struct DayTimelineView: View { @Environment(\.colorScheme) private var cs - let date: Date - let tasks: [TaskItem] - let calEvents: [EKEvent] - var workout: WorkoutProgram? = nil + + let date: Date + let tasks: [TaskItem] + let calEvents: [EKEvent] + let workout: WorkoutProgram? var onWorkoutTap: (() -> Void)? = nil + var onToggle: ((TaskItem) -> Void)? = nil private let cal = Calendar.current - private let fmt: DateFormatter = { let f = DateFormatter(); f.dateFormat = "EEE d"; return f }() - private let timeFmt: DateFormatter = { let f = DateFormatter(); f.dateFormat = "h:mm a"; return f }() - var dayTasks: [TaskItem] { - tasks.filter { guard let d = $0.dueDate else { return false }; return cal.isDate(d, inSameDayAs: date) } + private var entries: [DayTLEntry] { + var out: [DayTLEntry] = [] + + // All-day tasks (no specific time) + for t in tasks where !t.hasTime { + out.append(DayTLEntry( + timeLabel: "All Day", + title: t.title, + color: t.taskColor.color(), + isComplete: t.isComplete, + isAllDay: true, sortKey: -1, taskRef: t + )) + } + + // All-day calendar events + for ev in calEvents where ev.isAllDay { + out.append(DayTLEntry( + timeLabel: "All Day", + title: ev.title ?? "Event", + color: Color(cgColor: ev.calendar.cgColor), + isComplete: false, isAllDay: true, sortKey: -1 + )) + } + + // Workout + if let w = workout { + let done = w.doneSets == w.totalSets && w.totalSets > 0 + out.append(DayTLEntry( + timeLabel: "Workout", + title: w.name, + subtitle: "\(w.totalExercises) exercise\(w.totalExercises == 1 ? "" : "s")", + color: AppColors.blue, + isComplete: done, isAllDay: true, sortKey: -2, workoutRef: w + )) + } + + // Timed tasks + for t in tasks where t.hasTime { + guard let d = t.dueDate else { continue } + let h = cal.component(.hour, from: d), m = cal.component(.minute, from: d) + let hd = h == 0 ? 12 : (h > 12 ? h - 12 : h) + let ms = m == 0 ? "" : ":\(String(format: "%02d", m))" + out.append(DayTLEntry( + timeLabel: "\(hd)\(ms)", ampm: h < 12 ? "AM" : "PM", + title: t.title, color: t.taskColor.color(), + isComplete: t.isComplete, isAllDay: false, + sortKey: h * 60 + m, taskRef: t + )) + } + + // Timed calendar events + for ev in calEvents where !ev.isAllDay { + guard let s = ev.startDate else { continue } + let h = cal.component(.hour, from: s), m = cal.component(.minute, from: s) + let hd = h == 0 ? 12 : (h > 12 ? h - 12 : h) + let ms = m == 0 ? "" : ":\(String(format: "%02d", m))" + let endLabel: String? = ev.endDate.map { + let eh = cal.component(.hour, from: $0), em = cal.component(.minute, from: $0) + let ehd = eh == 0 ? 12 : (eh > 12 ? eh - 12 : eh) + let ems = em == 0 ? "" : ":\(String(format: "%02d", em))" + let eap = eh < 12 ? "AM" : "PM" + return "until \(ehd)\(ems) \(eap)" + } + out.append(DayTLEntry( + timeLabel: "\(hd)\(ms)", ampm: h < 12 ? "AM" : "PM", + title: ev.title ?? "Event", subtitle: endLabel, + color: Color(cgColor: ev.calendar.cgColor), + isComplete: false, isAllDay: false, sortKey: h * 60 + m + )) + } + + return out.sorted { + if $0.sortKey < 0 && $1.sortKey < 0 { return $0.sortKey > $1.sortKey } + if $0.sortKey < 0 { return true } + if $1.sortKey < 0 { return false } + return $0.sortKey < $1.sortKey + } } - var isEmpty: Bool { dayTasks.isEmpty && calEvents.isEmpty && workout == nil } - var body: some View { - VStack(alignment: .leading, spacing: 8) { - HStack(spacing: 6) { - Text(fmt.string(from: date).uppercased()) - .font(AppFonts.mono(10.5, weight: .bold)) - .foregroundColor(AppColors.accent) - let total = dayTasks.count + calEvents.count + (workout != nil ? 1 : 0) - Text(isEmpty ? "Nothing scheduled" : "\(total) event\(total == 1 ? "" : "s")") - .font(AppFonts.sans(10.5)) + if entries.isEmpty { + VStack(spacing: 6) { + Image(systemName: "calendar.badge.checkmark") + .font(.system(size: 24)) + .foregroundColor(AppColors.text3(cs)) + Text("Nothing scheduled") + .font(AppFonts.sans(13, weight: .medium)) + .foregroundColor(AppColors.text2(cs)) + Text("Tap + to add a task") + .font(AppFonts.sans(11)) .foregroundColor(AppColors.text3(cs)) } - - if isEmpty { - VStack(spacing: 2) { - Text("You have a free day").font(AppFonts.sans(12.5, weight: .medium)).foregroundColor(AppColors.text2(cs)) - Text("Take it easy").font(AppFonts.sans(10.5)).foregroundColor(AppColors.text3(cs)) - } - .frame(maxWidth: .infinity).padding(.vertical, 6) - } else { - // Workout block - if let w = workout { - CalendarWorkoutBlock(program: w, onTap: onWorkoutTap) - } - - // Calendar events - ForEach(calEvents, id: \.eventIdentifier) { event in - HStack(spacing: 8) { - Circle().fill(Color(cgColor: event.calendar.cgColor)).frame(width: 6, height: 6) - VStack(alignment: .leading, spacing: 1) { - Text(event.title ?? "Event") - .font(AppFonts.sans(12, weight: .medium)) - .foregroundColor(AppColors.text(cs)) - if let start = event.startDate { - Text(timeFmt.string(from: start)) - .font(AppFonts.mono(9.5)) - .foregroundColor(AppColors.text3(cs)) - } - } - Spacer() - TagChip(text: "Cal", color: Color(cgColor: event.calendar.cgColor), bg: Color(cgColor: event.calendar.cgColor).opacity(0.12)) - } - .padding(.vertical, 5).padding(.horizontal, 10) - .background(AppColors.surface(cs)) - .clipShape(RoundedRectangle(cornerRadius: AppRadius.small)) - .overlay(RoundedRectangle(cornerRadius: AppRadius.small).stroke(AppColors.border(cs), lineWidth: 1)) - } - - // Task items - ForEach(dayTasks) { task in - HStack(spacing: 8) { - Circle().fill(task.taskColor.color()).frame(width: 6, height: 6) - Text(task.title).font(AppFonts.sans(12)).foregroundColor(AppColors.text(cs)) - Spacer() - TagChip(text: task.category.rawValue, color: task.taskColor.color(), bg: task.taskColor.soft()) - } - .padding(.vertical, 5).padding(.horizontal, 10) - .background(AppColors.surface(cs)) - .clipShape(RoundedRectangle(cornerRadius: AppRadius.small)) - .overlay(RoundedRectangle(cornerRadius: AppRadius.small).stroke(AppColors.border(cs), lineWidth: 1)) + .frame(maxWidth: .infinity).padding(.vertical, 36) + } else { + VStack(spacing: 0) { + ForEach(Array(entries.enumerated()), id: \.element.id) { i, entry in + DayTLRow( + entry: entry, + isFirst: i == 0, + isLast: i == entries.count - 1, + cs: cs, + onWorkoutTap: entry.workoutRef != nil ? onWorkoutTap : nil, + onToggle: entry.taskRef.map { t in { onToggle?(t) } } + ) } } } } } -// MARK: - Calendar Workout Block -struct CalendarWorkoutBlock: View { - @Environment(\.colorScheme) private var cs - let program: WorkoutProgram - var onTap: (() -> Void)? = nil - @State private var isExpanded = false +private struct DayTLRow: View { + let entry: DayTLEntry + let isFirst: Bool + let isLast: Bool + let cs: ColorScheme + var onWorkoutTap: (() -> Void)? + var onToggle: (() -> Void)? - var body: some View { - VStack(alignment: .leading, spacing: 0) { - // ── Header row — taps open session ── - HStack(spacing: 8) { - WorkoutSectionBar(count: program.sections.count) - .frame(width: 30, height: 30) + private let circleD: CGFloat = 18 + private let timeW: CGFloat = 46 + private let trackW: CGFloat = 26 + private let topGap: CGFloat = 10 - VStack(alignment: .leading, spacing: 1) { - Text(program.name) - .font(AppFonts.sans(12, weight: .semibold)) - .foregroundColor(AppColors.text(cs)) - Text("\(program.totalExercises) exercise\(program.totalExercises == 1 ? "" : "s") · \(program.sections.count) section\(program.sections.count == 1 ? "" : "s")") - .font(AppFonts.mono(9)) - .foregroundColor(AppColors.text3(cs)) - } - Spacer() - HStack(spacing: 6) { - TagChip(text: "Workout", color: AppColors.blue, bg: AppColors.blueSoft) - Image(systemName: "chevron.right") - .font(.system(size: 9, weight: .semibold)) - .foregroundColor(AppColors.blue.opacity(0.45)) - } - } - .padding(.horizontal, 10).padding(.vertical, 8) - .contentShape(Rectangle()) - .onTapGesture { onTap?() } - - // ── Expand toggle ── - if !program.sections.isEmpty { - Button { - withAnimation(KisaniSpring.snappy) { isExpanded.toggle() } - } label: { - HStack(spacing: 4) { - Image(systemName: isExpanded ? "chevron.up" : "chevron.down") - .font(.system(size: 7, weight: .bold)) - Text(isExpanded ? "Less" : "Show \(program.sections.count) sections") - .font(AppFonts.mono(8.5, weight: .bold)) - } - .foregroundColor(AppColors.blue.opacity(0.55)) - .frame(maxWidth: .infinity) - .padding(.vertical, 6) - } - .buttonStyle(.plain) - - // ── Section breakdown (expanded) ── - if isExpanded { - Divider().background(AppColors.blue.opacity(0.12)) - VStack(alignment: .leading, spacing: 6) { - ForEach(program.sections) { section in - HStack(alignment: .top, spacing: 6) { - Text(section.name.uppercased()) - .font(AppFonts.mono(8, weight: .bold)) - .foregroundColor(AppColors.blue.opacity(0.75)) - .frame(width: 64, alignment: .leading) - Text(section.exercises.isEmpty - ? "No exercises" - : section.exercises.map(\.name).joined(separator: " · ")) - .font(AppFonts.sans(10.5)) - .foregroundColor(AppColors.text2(cs)) - .lineLimit(2) - Spacer() - } - } - } - .padding(.horizontal, 10).padding(.top, 6).padding(.bottom, 10) - .transition(.opacity.combined(with: .move(edge: .top))) - } + @ViewBuilder private var marker: some View { + ZStack { + if entry.isComplete { + Circle() + .fill(entry.color.opacity(0.14)) + .frame(width: circleD, height: circleD) + Image(systemName: "checkmark") + .font(.system(size: 7.5, weight: .bold)) + .foregroundColor(entry.color) + } else if entry.workoutRef != nil { + Circle() + .fill(AppColors.blue.opacity(0.14)) + .frame(width: circleD, height: circleD) + Image(systemName: "dumbbell.fill") + .font(.system(size: 8)) + .foregroundColor(AppColors.blue) + } else if entry.isAllDay { + Circle() + .strokeBorder(entry.color, lineWidth: 1.5) + .frame(width: circleD, height: circleD) + } else { + Circle() + .fill(entry.color) + .frame(width: 9, height: 9) } } - .background(AppColors.blue.opacity(0.07)) - .clipShape(RoundedRectangle(cornerRadius: AppRadius.small)) - .overlay(RoundedRectangle(cornerRadius: AppRadius.small).stroke(AppColors.blue.opacity(0.2), lineWidth: 1)) + .frame(width: circleD, height: circleD) + } + + var body: some View { + HStack(alignment: .top, spacing: 0) { + + // Time column + VStack(alignment: .trailing, spacing: 0) { + Text(entry.timeLabel) + .font(AppFonts.mono( + entry.isAllDay ? 8.5 : 11, + weight: entry.isAllDay ? .medium : .bold + )) + .foregroundColor(entry.isAllDay ? AppColors.text3(cs) : AppColors.text(cs)) + if let ap = entry.ampm { + Text(ap) + .font(AppFonts.mono(7, weight: .medium)) + .foregroundColor(AppColors.text3(cs)) + } + } + .frame(width: timeW, alignment: .trailing) + .padding(.top, topGap) + + // Vertical track: top line → circle → bottom fill line + VStack(spacing: 0) { + Rectangle() + .fill(isFirst ? Color.clear : AppColors.border(cs)) + .frame(width: 1, height: topGap) + .frame(maxWidth: trackW) + marker + Rectangle() + .fill(isLast ? Color.clear : AppColors.border(cs)) + .frame(minWidth: 1, maxWidth: 1, maxHeight: .infinity) + .frame(maxWidth: trackW) + } + .frame(width: trackW) + + // Event card + HStack(alignment: .center, spacing: 8) { + VStack(alignment: .leading, spacing: 3) { + Text(entry.title) + .font(AppFonts.sans(13, weight: .medium)) + .foregroundColor(entry.isComplete ? AppColors.text3(cs) : AppColors.text(cs)) + .strikethrough(entry.isComplete, color: AppColors.text3(cs)) + .lineLimit(2) + if let sub = entry.subtitle { + Text(sub) + .font(AppFonts.mono(9.5)) + .foregroundColor(AppColors.text3(cs)) + } + } + Spacer(minLength: 0) + if let toggle = onToggle { + Button(action: toggle) { + Image(systemName: entry.isComplete ? "checkmark.circle.fill" : "circle") + .font(.system(size: 18)) + .foregroundColor(entry.isComplete ? entry.color : AppColors.text3(cs)) + } + .buttonStyle(.plain) + } else if onWorkoutTap != nil { + Image(systemName: "chevron.right") + .font(.system(size: 10, weight: .semibold)) + .foregroundColor(AppColors.text3(cs)) + } + } + .padding(.horizontal, 13).padding(.vertical, 11) + .background(AppColors.surface(cs)) + .clipShape(RoundedRectangle(cornerRadius: 10)) + .overlay(RoundedRectangle(cornerRadius: 10).stroke(AppColors.border(cs), lineWidth: 1)) + .padding(.leading, 8) + .padding(.bottom, 8) + .frame(maxWidth: .infinity, alignment: .leading) + .contentShape(Rectangle()) + .onTapGesture { if onWorkoutTap != nil { onWorkoutTap?() } } + } + } +} + +// MARK: - Calendar Task Row (used in task list toggle mode) +private struct CalTaskRow: View { + @Environment(\.colorScheme) private var cs + let task: TaskItem + let onToggle: () -> Void + + private static let timeFmt: DateFormatter = { + let f = DateFormatter(); f.dateFormat = "H:mm"; return f + }() + + private var timeLabel: String? { + guard let d = task.dueDate, task.hasTime else { return nil } + let comps = Calendar.current.dateComponents([.hour, .minute], from: d) + guard comps.hour != 0 || comps.minute != 0 else { return nil } + return Self.timeFmt.string(from: d) + } + + var body: some View { + HStack(spacing: 12) { + Button(action: onToggle) { + ZStack { + RoundedRectangle(cornerRadius: 6) + .fill(task.isComplete ? AppColors.surface3(cs) : Color.clear) + .frame(width: 22, height: 22) + RoundedRectangle(cornerRadius: 6) + .strokeBorder( + task.isComplete ? Color.clear : task.taskColor.color(), + lineWidth: 1.5 + ) + .frame(width: 22, height: 22) + if task.isComplete { + Image(systemName: "checkmark") + .font(.system(size: 10, weight: .bold)) + .foregroundColor(AppColors.text3(cs)) + } + } + } + .buttonStyle(PressButtonStyle(scale: 0.88)) + + Text(task.title) + .font(AppFonts.sans(13, weight: task.isComplete ? .regular : .medium)) + .foregroundColor(task.isComplete ? AppColors.text3(cs) : AppColors.text(cs)) + .strikethrough(task.isComplete, color: AppColors.text3(cs)) + + Spacer() + + if let t = timeLabel { + Text(t) + .font(AppFonts.mono(11, weight: .semibold)) + .foregroundColor(AppColors.accent) + } else if task.isComplete { + Text("Today") + .font(AppFonts.mono(10)) + .foregroundColor(AppColors.text3(cs)) + } + } + .padding(.horizontal, 16).padding(.vertical, 13) + .contentShape(Rectangle()) } }