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.
This commit is contained in:
kutesir
2026-05-29 13:51:37 +03:00
parent 98eaa9fb27
commit e0e4f709b6

View File

@@ -114,6 +114,7 @@ struct CalendarView: View {
return c.date(from: c.dateComponents([.year, .month], from: Date())) ?? Date() return c.date(from: c.dateComponents([.year, .month], from: Date())) ?? Date()
}() }()
@State private var viewMode: CalendarViewMode = .month @State private var viewMode: CalendarViewMode = .month
@State private var calTaskListMode: Bool = false
@State private var showAddTask = false @State private var showAddTask = false
@State private var showCalendarConnect = false @State private var showCalendarConnect = false
@State private var showWorkoutSession = false @State private var showWorkoutSession = false
@@ -127,31 +128,49 @@ struct CalendarView: View {
VStack(spacing: 0) { VStack(spacing: 0) {
// Header // 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) Text(monthShortTitle)
.font(AppFonts.sans(26, weight: .bold)) .font(AppFonts.sans(17, weight: .semibold))
.foregroundColor(AppColors.text(cs)) .foregroundColor(AppColors.text(cs))
Spacer() Spacer()
// Right: controls pill (view mode picker + more)
HStack(spacing: 0) { HStack(spacing: 0) {
// View mode picker // Right pill left button: task list toggle
Menu { Button {
Picker(selection: $viewMode, label: EmptyView()) { withAnimation(KisaniSpring.snappy) { calTaskListMode.toggle() }
ForEach(CalendarViewMode.allCases, id: \.self) { mode in
Label(mode.rawValue, systemImage: mode.sfSymbol).tag(mode)
}
}
} label: { } label: {
Image(systemName: "slider.horizontal.3") Image(systemName: calTaskListMode ? "calendar" : "slider.horizontal.3")
.font(.system(size: 14, weight: .medium)) .font(.system(size: 14, weight: calTaskListMode ? .semibold : .medium))
.foregroundColor(AppColors.text(cs)) .foregroundColor(calTaskListMode ? AppColors.accent : AppColors.text(cs))
.frame(width: 46, height: 36) .frame(width: 46, height: 36)
.contentShape(Rectangle())
} }
.buttonStyle(.plain) .buttonStyle(.plain)
// Divider
Rectangle() Rectangle()
.fill(AppColors.border(cs)) .fill(AppColors.border(cs))
.frame(width: 1, height: 18) .frame(width: 1, height: 18)
// More options
Menu { Menu {
Button { } label: { Label("Filter View Range", systemImage: "line.3.horizontal.decrease") } Button { } label: { Label("Filter View Range", systemImage: "line.3.horizontal.decrease") }
Button { } label: { Label("View Options", systemImage: "gearshape.2") } Button { } label: { Label("View Options", systemImage: "gearshape.2") }
@@ -169,8 +188,9 @@ struct CalendarView: View {
.font(.system(size: 14, weight: .medium)) .font(.system(size: 14, weight: .medium))
.foregroundColor(AppColors.text(cs)) .foregroundColor(AppColors.text(cs))
.frame(width: 46, height: 36) .frame(width: 46, height: 36)
.contentShape(Rectangle())
} }
.buttonStyle(.plain) .menuStyle(.automatic)
} }
.background(AppColors.surface2(cs)) .background(AppColors.surface2(cs))
.clipShape(Capsule()) .clipShape(Capsule())
@@ -178,10 +198,17 @@ struct CalendarView: View {
} }
.padding(.horizontal, 18).padding(.top, 8) .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) { .sheet(isPresented: $showAddTask) {
AddTaskSheet(prefilledDate: selectedDate) AddTaskSheet(prefilledDate: selectedDate)
@@ -257,6 +284,67 @@ struct CalendarView: View {
return dots 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 // MARK: - Content switcher
@ViewBuilder private var calendarContent: some View { @ViewBuilder private var calendarContent: some View {
@@ -303,18 +391,24 @@ struct CalendarView: View {
Divider().background(AppColors.border(cs)).padding(.horizontal, 16).padding(.top, 6) Divider().background(AppColors.border(cs)).padding(.horizontal, 16).padding(.top, 6)
ScrollView(showsIndicators: false) { ScrollView(showsIndicators: false) {
DayStripView( DayTimelineView(
date: selectedDate, 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) : [], calEvents: calStore.isAuthorized ? calStore.events(for: selectedDate) : [],
workout: workoutVM.program(for: selectedDate), workout: workoutVM.program(for: selectedDate),
onWorkoutTap: { onWorkoutTap: {
if let w = workoutVM.program(for: selectedDate), if let w = workoutVM.program(for: selectedDate),
workoutVM.activeProgramId != w.id { workoutVM.switchProgram(to: w.id) } workoutVM.activeProgramId != w.id { workoutVM.switchProgram(to: w.id) }
showWorkoutSession = true 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 // MARK: - Day Timeline
struct DayStripView: View {
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 @Environment(\.colorScheme) private var cs
let date: Date
let tasks: [TaskItem] let date: Date
let calEvents: [EKEvent] let tasks: [TaskItem]
var workout: WorkoutProgram? = nil let calEvents: [EKEvent]
let workout: WorkoutProgram?
var onWorkoutTap: (() -> Void)? = nil var onWorkoutTap: (() -> Void)? = nil
var onToggle: ((TaskItem) -> Void)? = nil
private let cal = Calendar.current 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] { private var entries: [DayTLEntry] {
tasks.filter { guard let d = $0.dueDate else { return false }; return cal.isDate(d, inSameDayAs: date) } 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 { var body: some View {
VStack(alignment: .leading, spacing: 8) { if entries.isEmpty {
HStack(spacing: 6) { VStack(spacing: 6) {
Text(fmt.string(from: date).uppercased()) Image(systemName: "calendar.badge.checkmark")
.font(AppFonts.mono(10.5, weight: .bold)) .font(.system(size: 24))
.foregroundColor(AppColors.accent) .foregroundColor(AppColors.text3(cs))
let total = dayTasks.count + calEvents.count + (workout != nil ? 1 : 0) Text("Nothing scheduled")
Text(isEmpty ? "Nothing scheduled" : "\(total) event\(total == 1 ? "" : "s")") .font(AppFonts.sans(13, weight: .medium))
.font(AppFonts.sans(10.5)) .foregroundColor(AppColors.text2(cs))
Text("Tap + to add a task")
.font(AppFonts.sans(11))
.foregroundColor(AppColors.text3(cs)) .foregroundColor(AppColors.text3(cs))
} }
.frame(maxWidth: .infinity).padding(.vertical, 36)
if isEmpty { } else {
VStack(spacing: 2) { VStack(spacing: 0) {
Text("You have a free day").font(AppFonts.sans(12.5, weight: .medium)).foregroundColor(AppColors.text2(cs)) ForEach(Array(entries.enumerated()), id: \.element.id) { i, entry in
Text("Take it easy").font(AppFonts.sans(10.5)).foregroundColor(AppColors.text3(cs)) DayTLRow(
} entry: entry,
.frame(maxWidth: .infinity).padding(.vertical, 6) isFirst: i == 0,
} else { isLast: i == entries.count - 1,
// Workout block cs: cs,
if let w = workout { onWorkoutTap: entry.workoutRef != nil ? onWorkoutTap : nil,
CalendarWorkoutBlock(program: w, onTap: onWorkoutTap) onToggle: entry.taskRef.map { t in { onToggle?(t) } }
} )
// 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))
} }
} }
} }
} }
} }
// MARK: - Calendar Workout Block private struct DayTLRow: View {
struct CalendarWorkoutBlock: View { let entry: DayTLEntry
@Environment(\.colorScheme) private var cs let isFirst: Bool
let program: WorkoutProgram let isLast: Bool
var onTap: (() -> Void)? = nil let cs: ColorScheme
@State private var isExpanded = false var onWorkoutTap: (() -> Void)?
var onToggle: (() -> Void)?
var body: some View { private let circleD: CGFloat = 18
VStack(alignment: .leading, spacing: 0) { private let timeW: CGFloat = 46
// Header row taps open session private let trackW: CGFloat = 26
HStack(spacing: 8) { private let topGap: CGFloat = 10
WorkoutSectionBar(count: program.sections.count)
.frame(width: 30, height: 30)
VStack(alignment: .leading, spacing: 1) { @ViewBuilder private var marker: some View {
Text(program.name) ZStack {
.font(AppFonts.sans(12, weight: .semibold)) if entry.isComplete {
.foregroundColor(AppColors.text(cs)) Circle()
Text("\(program.totalExercises) exercise\(program.totalExercises == 1 ? "" : "s") · \(program.sections.count) section\(program.sections.count == 1 ? "" : "s")") .fill(entry.color.opacity(0.14))
.font(AppFonts.mono(9)) .frame(width: circleD, height: circleD)
.foregroundColor(AppColors.text3(cs)) Image(systemName: "checkmark")
} .font(.system(size: 7.5, weight: .bold))
Spacer() .foregroundColor(entry.color)
HStack(spacing: 6) { } else if entry.workoutRef != nil {
TagChip(text: "Workout", color: AppColors.blue, bg: AppColors.blueSoft) Circle()
Image(systemName: "chevron.right") .fill(AppColors.blue.opacity(0.14))
.font(.system(size: 9, weight: .semibold)) .frame(width: circleD, height: circleD)
.foregroundColor(AppColors.blue.opacity(0.45)) Image(systemName: "dumbbell.fill")
} .font(.system(size: 8))
} .foregroundColor(AppColors.blue)
.padding(.horizontal, 10).padding(.vertical, 8) } else if entry.isAllDay {
.contentShape(Rectangle()) Circle()
.onTapGesture { onTap?() } .strokeBorder(entry.color, lineWidth: 1.5)
.frame(width: circleD, height: circleD)
// Expand toggle } else {
if !program.sections.isEmpty { Circle()
Button { .fill(entry.color)
withAnimation(KisaniSpring.snappy) { isExpanded.toggle() } .frame(width: 9, height: 9)
} 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)))
}
} }
} }
.background(AppColors.blue.opacity(0.07)) .frame(width: circleD, height: circleD)
.clipShape(RoundedRectangle(cornerRadius: AppRadius.small)) }
.overlay(RoundedRectangle(cornerRadius: AppRadius.small).stroke(AppColors.blue.opacity(0.2), lineWidth: 1))
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())
} }
} }