import WidgetKit import SwiftUI // MARK: - Colors (no UIKit dependency) private enum WC { static let accent = Color(red: 232/255, green: 98/255, blue: 42/255) static let green = Color(red: 62/255, green: 207/255, blue: 142/255) static let blue = Color(red: 77/255, green: 157/255, blue: 224/255) static func bg(_ s: ColorScheme) -> Color { s == .dark ? Color(red: 9/255, green: 9/255, blue: 9/255) : Color(red: 245/255, green: 243/255, blue: 240/255) } static func surface(_ s: ColorScheme) -> Color { s == .dark ? Color(red: 19/255, green: 19/255, blue: 19/255) : Color(red: 255/255, green: 253/255, blue: 250/255) } static func text(_ s: ColorScheme) -> Color { s == .dark ? Color(red: 240/255, green: 237/255, blue: 232/255) : Color(red: 26/255, green: 23/255, blue: 20/255) } static func text2(_ s: ColorScheme) -> Color { text(s).opacity(0.48) } static func text3(_ s: ColorScheme) -> Color { text(s).opacity(0.30) } static func border(_ s: ColorScheme) -> Color { s == .dark ? Color.white.opacity(0.07) : Color.black.opacity(0.09) } } // MARK: - Timeline Entry struct KisaniEntry: TimelineEntry { let date: Date let todayTasks: [WidgetDataStore.WidgetTask] let overdueTasks: [WidgetDataStore.WidgetTask] let workout: WidgetDataStore.WidgetWorkout? } // MARK: - Provider struct KisaniProvider: TimelineProvider { func placeholder(in context: Context) -> KisaniEntry { KisaniEntry( date: Date(), todayTasks: previewTasks, overdueTasks: [], workout: WidgetDataStore.WidgetWorkout( name: "Shoulders & Arms", sectionsCount: 4, hasWorkoutToday: true) ) } func getSnapshot(in context: Context, completion: @escaping (KisaniEntry) -> Void) { completion(entry()) } func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { let next = Calendar.current.date(byAdding: .minute, value: 30, to: Date())! completion(Timeline(entries: [entry()], policy: .after(next))) } private func entry() -> KisaniEntry { let all = WidgetDataStore.loadTasks() let today = all.filter { $0.isToday } let overdue = all.filter { $0.isOverdue } let workout = WidgetDataStore.loadWorkout() return KisaniEntry(date: Date(), todayTasks: today, overdueTasks: overdue, workout: workout) } private var previewTasks: [WidgetDataStore.WidgetTask] { [ .init(id: UUID(), title: "Azure exam prep", dueDate: Date(), isToday: true, isOverdue: false), .init(id: UUID(), title: "Push AWS project", dueDate: Date(), isToday: true, isOverdue: false), .init(id: UUID(), title: "Review PR #42", dueDate: Date(), isToday: false, isOverdue: true), ] } } // MARK: - Widget Bundle @main struct KisaniCalWidgetBundle: WidgetBundle { var body: some Widget { KisaniCalWidget() } } struct KisaniCalWidget: Widget { let kind = "KisaniCalWidget" var body: some WidgetConfiguration { StaticConfiguration(kind: kind, provider: KisaniProvider()) { entry in KisaniWidgetView(entry: entry) .containerBackground(for: .widget) { Color.clear } } .configurationDisplayName("Kisani Cal") .description("Today's tasks and workout at a glance.") .supportedFamilies([.systemSmall, .systemMedium, .systemLarge]) } } // MARK: - Root Widget View struct KisaniWidgetView: View { @Environment(\.widgetFamily) private var family let entry: KisaniEntry var body: some View { switch family { case .systemSmall: SmallWidget(entry: entry) case .systemMedium: MediumWidget(entry: entry) default: LargeWidget(entry: entry) } } } // MARK: - Shared Sub-Views private struct DateBadge: View { @Environment(\.colorScheme) private var cs let size: CGFloat private static let monthFmt: DateFormatter = { let f = DateFormatter(); f.dateFormat = "MMM"; return f }() var body: some View { let day = Calendar.current.component(.day, from: Date()) VStack(spacing: 0) { Text(Self.monthFmt.string(from: Date()).uppercased()) .font(.system(size: size * 0.18, weight: .bold, design: .monospaced)) .foregroundColor(.white) .kerning(1) .frame(maxWidth: .infinity) .frame(height: size * 0.32) .background(WC.accent) Text("\(day)") .font(.system(size: size * 0.48, weight: .bold, design: .monospaced)) .foregroundColor(WC.text(cs)) .frame(maxWidth: .infinity) .frame(height: size * 0.68) .background(WC.surface(cs)) } .frame(width: size, height: size) .clipShape(RoundedRectangle(cornerRadius: size * 0.22)) .overlay( RoundedRectangle(cornerRadius: size * 0.22) .stroke(WC.border(cs), lineWidth: 1) ) } } private struct TaskRow: View { @Environment(\.colorScheme) private var cs let task: WidgetDataStore.WidgetTask private static let fmt: DateFormatter = { let f = DateFormatter(); f.dateFormat = "d MMM"; return f }() var body: some View { HStack(spacing: 5) { Circle() .stroke(task.isOverdue ? WC.accent : WC.text3(cs), lineWidth: 1.2) .frame(width: 7, height: 7) Text(task.title) .font(.system(size: 11, weight: .regular, design: .default)) .foregroundColor(WC.text(cs)) .lineLimit(1) Spacer(minLength: 0) if let d = task.dueDate { Text(task.isOverdue ? "Overdue" : Self.fmt.string(from: d)) .font(.system(size: 9, weight: .regular, design: .monospaced)) .foregroundColor(task.isOverdue ? WC.accent : WC.text3(cs)) } } } } private struct WorkoutRow: View { @Environment(\.colorScheme) private var cs let workout: WidgetDataStore.WidgetWorkout var body: some View { HStack(spacing: 6) { WorkoutSegBar(count: workout.sectionsCount) .frame(width: 22, height: 22) Text(workout.name) .font(.system(size: 11, weight: .semibold, design: .default)) .foregroundColor(WC.text(cs)) .lineLimit(1) Spacer(minLength: 0) Text("Workout") .font(.system(size: 9, weight: .regular, design: .monospaced)) .foregroundColor(WC.blue) } } } private struct WorkoutSegBar: View { @Environment(\.colorScheme) private var cs let count: Int var body: some View { let n = max(1, min(6, count)) VStack(spacing: 1.5) { ForEach(0.. 0 { Text("\(overdueCount) overdue ยท \(todayCount) today") .foregroundColor(WC.accent) } else if todayCount > 0 { Text("\(todayCount) task\(todayCount == 1 ? "" : "s") today") .foregroundColor(WC.text2(cs)) } else { Text("All clear") .foregroundColor(WC.text3(cs)) } } .font(.system(size: 9, weight: .regular, design: .monospaced)) } } .padding(14) } } } // MARK: - Medium Widget private struct MediumWidget: View { @Environment(\.colorScheme) private var cs let entry: KisaniEntry private var displayTasks: [WidgetDataStore.WidgetTask] { let all = entry.overdueTasks + entry.todayTasks return Array(all.prefix(3)) } var body: some View { ZStack { WC.bg(cs).ignoresSafeArea() HStack(alignment: .top, spacing: 14) { // Left: badge + brand VStack(spacing: 8) { DateBadge(size: 64) HStack(spacing: 0) { Text("kisani") .font(.system(size: 9, weight: .regular, design: .monospaced)) .foregroundColor(WC.text2(cs)) Text("CAL.") .font(.system(size: 9, weight: .bold, design: .monospaced)) .foregroundColor(WC.accent) } } // Right: tasks + workout VStack(alignment: .leading, spacing: 5) { if displayTasks.isEmpty && entry.workout == nil { Spacer() Text("Nothing scheduled") .font(.system(size: 11, design: .monospaced)) .foregroundColor(WC.text3(cs)) Spacer() } else { ForEach(displayTasks) { task in TaskRow(task: task) } if let workout = entry.workout, workout.hasWorkoutToday, displayTasks.count < 3 { if !displayTasks.isEmpty { Divider().opacity(0.4) } WorkoutRow(workout: workout) } Spacer(minLength: 0) } } .frame(maxWidth: .infinity, alignment: .leading) } .padding(14) } } } // MARK: - Large Widget private struct LargeWidget: View { @Environment(\.colorScheme) private var cs let entry: KisaniEntry private static let dayFmt: DateFormatter = { let f = DateFormatter(); f.dateFormat = "EEEE, MMM d"; return f }() private var allTasks: [WidgetDataStore.WidgetTask] { Array((entry.overdueTasks + entry.todayTasks).prefix(6)) } var body: some View { ZStack { WC.bg(cs).ignoresSafeArea() VStack(alignment: .leading, spacing: 0) { // Header row HStack(spacing: 12) { DateBadge(size: 56) VStack(alignment: .leading, spacing: 3) { HStack(spacing: 0) { Text("kisani") .font(.system(size: 14, weight: .regular, design: .monospaced)) .foregroundColor(WC.text2(cs)) Text("CAL") .font(.system(size: 14, weight: .bold, design: .monospaced)) .foregroundColor(WC.text(cs)) Text(".") .font(.system(size: 14, weight: .bold, design: .monospaced)) .foregroundColor(WC.accent) } Text(Self.dayFmt.string(from: Date())) .font(.system(size: 10, design: .monospaced)) .foregroundColor(WC.text2(cs)) } Spacer() let total = entry.todayTasks.count + entry.overdueTasks.count VStack(alignment: .trailing, spacing: 2) { Text("\(total)") .font(.system(size: 22, weight: .bold, design: .monospaced)) .foregroundColor(entry.overdueTasks.isEmpty ? WC.text(cs) : WC.accent) Text("tasks") .font(.system(size: 9, design: .monospaced)) .foregroundColor(WC.text3(cs)) } } .padding(.horizontal, 14) .padding(.top, 14) .padding(.bottom, 12) Divider().opacity(0.4).padding(.horizontal, 14) // Task list VStack(alignment: .leading, spacing: 7) { if allTasks.isEmpty { HStack { Spacer() Text("Nothing due today") .font(.system(size: 11, design: .monospaced)) .foregroundColor(WC.text3(cs)) Spacer() } .padding(.vertical, 8) } else { ForEach(allTasks) { task in TaskRow(task: task) } } } .padding(.horizontal, 14) .padding(.vertical, 10) Spacer(minLength: 0) // Workout row if let workout = entry.workout, workout.hasWorkoutToday { Divider().opacity(0.4).padding(.horizontal, 14) WorkoutRow(workout: workout) .padding(.horizontal, 14) .padding(.vertical, 10) } } } } }