feat: home/lock widgets, Health-aware streaks, workout editor, and entitlement fixes

Widgets (new KisaniCalWidgets extension)
- Event Countdown widget: configurable via AppIntent, pick from your events
  or set a custom name/date; dot-grid progress toward the event.
- Day Progress and Tasks-Done-Today widgets; lock screen widgets.
- Shared dot grid sizes circles to fill the widget evenly at any count.
- Tasks/calendar prefs now persist to the App Group so widgets read live data.

Health & streaks
- Persist HealthKit authorization and re-establish on launch (fixes the
  Today dashboard and streak sync disappearing after relaunch).
- Add a Health permission toggle to onboarding; unify Settings/Profile on the
  manager's state.
- Day streak counts from a logged Health workout OR marking all exercises
  complete; nudge to mark exercises complete even when Health logged the workout.

Workout editor
- Drag to reorder exercises and move them across sections (drag-and-drop);
  swipe to delete.
- Add-exercise sheet: global search and keep-adding-multiple with a running count.

Fixes
- Restore app entitlements link (Sign in with Apple, HealthKit, App Group, iCloud).
- Add HealthKit usage strings; widget Info.plist NSExtension + nested bundle id.
- Calendar "Connect" routes to Settings when access was denied.
- Bake DEVELOPMENT_TEAM and shared versions into project.yml.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Robin Kutesa
2026-06-03 12:27:45 +03:00
parent e5bd739499
commit 8b094691f5
23 changed files with 1272 additions and 115 deletions

View File

@@ -0,0 +1,153 @@
import WidgetKit
import SwiftUI
import AppIntents
// MARK: - Configuration Intent
//
// Each widget instance stores its own event. The user sets a name, the day the
// countdown starts ("Counting from" defaults to the day they add the widget,
// which acts as the creation anchor) and the event date. Because these values are
// persisted per-instance by WidgetKit, the widget is fully self-contained and does
// not depend on App Group data sharing.
struct EventConfigIntent: WidgetConfigurationIntent {
static var title: LocalizedStringResource = "Event Countdown"
static var description = IntentDescription("Count down to any upcoming event with a dot grid.")
// Pick one of your existing events (tasks with a due date). When set, it
// overrides the manual name/date fields below.
@Parameter(title: "Track event")
var event: EventEntity?
@Parameter(title: "Or event name", default: "My Event")
var eventName: String
@Parameter(title: "Event date", default: Date.now)
var targetDate: Date
@Parameter(title: "Counting from", default: Date.now)
var startDate: Date
}
// MARK: - Event picker (reads your tasks from the App Group)
struct EventEntity: AppEntity {
let id: String // task UUID string
let title: String
let dueDate: Date?
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Event"
static var defaultQuery = EventQuery()
var displayRepresentation: DisplayRepresentation {
if let d = dueDate {
let f = DateFormatter(); f.dateStyle = .medium
return DisplayRepresentation(title: "\(title)", subtitle: "\(f.string(from: d))")
}
return DisplayRepresentation(title: "\(title)")
}
}
struct EventQuery: EntityQuery {
func entities(for identifiers: [String]) async throws -> [EventEntity] {
allEvents().filter { identifiers.contains($0.id) }
}
func suggestedEntities() async throws -> [EventEntity] {
allEvents()
}
private func allEvents() -> [EventEntity] {
loadWidgetTasks()
.filter { !$0.isComplete && $0.dueDate != nil }
.sorted { ($0.dueDate ?? .distantFuture) < ($1.dueDate ?? .distantFuture) }
.map { EventEntity(id: $0.id.uuidString, title: $0.title, dueDate: $0.dueDate) }
}
}
// MARK: - Entry
struct EventEntry: TimelineEntry {
let date: Date
let eventName: String
let start: Date
let target: Date
/// Fraction of the countdown window that has elapsed (01).
var progress: Double {
let span = target.timeIntervalSince(start)
guard span > 0 else { return target <= date ? 1 : 0 }
return min(1, max(0, date.timeIntervalSince(start) / span))
}
/// Whole days from start to target (the number of dots in the grid).
var totalDays: Int {
let cal = Calendar.current
let d = cal.dateComponents([.day],
from: cal.startOfDay(for: start),
to: cal.startOfDay(for: target)).day ?? 0
return max(1, d)
}
/// Days remaining until the event (never negative).
var daysLeft: Int {
let cal = Calendar.current
let d = cal.dateComponents([.day],
from: cal.startOfDay(for: date),
to: cal.startOfDay(for: target)).day ?? 0
return max(0, d)
}
}
// MARK: - Provider
struct EventProvider: AppIntentTimelineProvider {
func placeholder(in context: Context) -> EventEntry {
EventEntry(date: .now,
eventName: "Mum's Birthday",
start: Date().addingTimeInterval(-86400 * 320),
target: Date().addingTimeInterval(86400 * 60))
}
func snapshot(for configuration: EventConfigIntent, in context: Context) async -> EventEntry {
entry(for: configuration)
}
func timeline(for configuration: EventConfigIntent, in context: Context) async -> Timeline<EventEntry> {
let entry = entry(for: configuration)
// Recompute at the next midnight so the grid advances one dot per day.
let nextMidnight = Calendar.current.startOfDay(for: Date().addingTimeInterval(86400))
return Timeline(entries: [entry], policy: .after(nextMidnight))
}
private func entry(for config: EventConfigIntent) -> EventEntry {
let name: String
let target: Date
if let ev = config.event, let due = ev.dueDate {
// A picked event supplies its own name + date.
name = ev.title
target = due
} else {
name = config.eventName.isEmpty ? "My Event" : config.eventName
target = config.targetDate
}
// Never let the start sit after the target.
let start = min(config.startDate, target)
return EventEntry(date: .now, eventName: name, start: start, target: target)
}
}
// MARK: - Widget
struct EventCountdownWidget: Widget {
let kind = "KisaniEventCountdown"
var body: some WidgetConfiguration {
AppIntentConfiguration(kind: kind, intent: EventConfigIntent.self, provider: EventProvider()) { entry in
EventCountdownView(entry: entry)
}
.configurationDisplayName("Event Countdown")
.description("Track the days until any upcoming event with a dot grid.")
.supportedFamilies([.systemMedium, .systemSmall])
}
}

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Kisani Cal Widgets</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.widgetkit-extension</string>
</dict>
</dict>
</plist>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.kutesir.KisaniCal</string>
</array>
</dict>
</plist>

View File

@@ -0,0 +1,114 @@
import WidgetKit
import SwiftUI
import AppIntents
// MARK: - Shared Entry
struct KisaniEntry: TimelineEntry {
let date: Date
let task: WidgetTask?
let allTasks: [WidgetTask]
let streak: Int
}
// MARK: - Shared Provider
struct KisaniProvider: TimelineProvider {
func placeholder(in context: Context) -> KisaniEntry {
let sample = WidgetTask(id: UUID(), title: "Mums Birthday", dueDate: Date().addingTimeInterval(86400 * 60),
isComplete: false, quadrant: "Urgent + Important", category: "birthday")
return KisaniEntry(date: .now, task: sample, allTasks: [sample], streak: 7)
}
func getSnapshot(in context: Context, completion: @escaping (KisaniEntry) -> Void) {
let tasks = loadWidgetTasks()
completion(KisaniEntry(date: .now, task: nextDueTask(from: tasks), allTasks: tasks, streak: loadWorkoutStreak()))
}
func getTimeline(in context: Context, completion: @escaping (Timeline<KisaniEntry>) -> Void) {
let tasks = loadWidgetTasks()
let entry = KisaniEntry(date: .now, task: nextDueTask(from: tasks), allTasks: tasks, streak: loadWorkoutStreak())
// Refresh every 30 minutes or when app triggers WidgetCenter.reloadAllTimelines()
let next = Calendar.current.date(byAdding: .minute, value: 30, to: .now)!
completion(Timeline(entries: [entry], policy: .after(next)))
}
}
// MARK: - Day Progress Widget ("Day done %")
struct DayProgressWidget: Widget {
let kind = "KisaniDayProgress"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: KisaniProvider()) { _ in
ProgressDotView(period: .day, accent: Color(red: 0.48, green: 0.87, blue: 0.80))
}
.configurationDisplayName("Day Progress")
.description("How much of today has elapsed, as a dot grid.")
.supportedFamilies([.systemSmall, .systemMedium])
}
}
// MARK: - Tasks Done Today Widget ("Tasks complete %")
struct TasksCompleteWidget: Widget {
let kind = "KisaniTasksComplete"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: KisaniProvider()) { _ in
TasksCompleteView()
}
.configurationDisplayName("Tasks Done Today")
.description("Percentage of today's tasks you've completed.")
.supportedFamilies([.systemSmall, .systemMedium])
}
}
// MARK: - Lock Screen Widgets
struct LockScreenWidget: Widget {
let kind = "KisaniLockScreen"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: KisaniProvider()) { entry in
if let task = entry.task {
LockScreenRectView(task: task)
} else {
LockScreenRectView(task: WidgetTask(id: UUID(), title: "No tasks",
dueDate: nil, isComplete: false, quadrant: "Urgent + Important", category: "reminder"))
}
}
.configurationDisplayName("Task (Lock Screen)")
.description("Glass countdown on your lock screen.")
.supportedFamilies([.accessoryRectangular])
}
}
struct LockScreenCircularWidget: Widget {
let kind = "KisaniLockCircle"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: KisaniProvider()) { entry in
if let task = entry.task {
LockScreenCircularView(task: task)
} else {
LockScreenCircularView(task: WidgetTask(id: UUID(), title: "",
dueDate: nil, isComplete: false, quadrant: "Urgent + Important", category: "reminder"))
}
}
.configurationDisplayName("Days Left (Lock Screen)")
.description("Circular countdown on your lock screen.")
.supportedFamilies([.accessoryCircular])
}
}
// MARK: - Widget Bundle
@main
struct KisaniWidgetBundle: WidgetBundle {
var body: some Widget {
EventCountdownWidget()
DayProgressWidget()
TasksCompleteWidget()
LockScreenWidget()
LockScreenCircularWidget()
}
}

View File

@@ -0,0 +1,120 @@
import Foundation
import SwiftUI
// Minimal task model shared between widget and main app via App Group
struct WidgetTask: Codable, Identifiable {
let id: UUID
let title: String
let dueDate: Date?
let isComplete: Bool
let quadrant: String // raw value of Quadrant enum
let category: String
// Days remaining until due date
var daysLeft: Int? {
guard let due = dueDate else { return nil }
let d = Calendar.current.dateComponents([.day], from: Calendar.current.startOfDay(for: .now), to: due).day ?? 0
return max(0, d)
}
// Hours remaining (for sub-day precision)
var hoursLeft: Int? {
guard let due = dueDate else { return nil }
let h = Calendar.current.dateComponents([.hour], from: .now, to: due).hour ?? 0
return max(0, h)
}
var timeLeftLabel: String {
guard dueDate != nil else { return "" }
if isComplete { return "Done" }
let days = daysLeft ?? 0
if days == 0 {
let hrs = hoursLeft ?? 0
return hrs <= 0 ? "Due now" : "\(hrs)hr left"
}
if days < 30 { return "\(days)d left" }
let months = days / 30
let remDays = days % 30
if remDays == 0 { return "\(months)mth left" }
return "\(months)mth \(remDays)d..."
}
var accentColor: Color {
switch quadrant {
case "Urgent + Important": return Color(red: 0.96, green: 0.42, blue: 0.20)
case "Schedule It": return Color(red: 0.95, green: 0.78, blue: 0.10)
case "Delegate": return Color(red: 0.24, green: 0.58, blue: 0.95)
case "Eliminate": return Color(red: 0.20, green: 0.78, blue: 0.50)
default: return Color(red: 0.96, green: 0.42, blue: 0.20)
}
}
}
// Load tasks from the shared App Group UserDefaults
func loadWidgetTasks() -> [WidgetTask] {
let uid = UserDefaults.kisani.string(forKey: "kisani.activeUserId") ?? "shared"
let key = "kisani.tasks.v2.\(uid)"
guard let data = UserDefaults.kisani.data(forKey: key),
let raw = try? JSONDecoder().decode([RawTask].self, from: data)
else { return [] }
return raw.map {
WidgetTask(id: $0.id, title: $0.title, dueDate: $0.dueDate,
isComplete: $0.isComplete, quadrant: $0.quadrant, category: $0.category)
}
}
// Partial decode of TaskItem only the fields widgets need
private struct RawTask: Codable {
let id: UUID
let title: String
let dueDate: Date?
var isComplete: Bool = false
var quadrant: String = "Urgent + Important"
var category: String = "reminder"
enum CodingKeys: String, CodingKey {
case id, title, dueDate, isComplete, quadrant, category
}
}
// Next upcoming task with a due date (for default widget display)
func nextDueTask(from tasks: [WidgetTask]) -> WidgetTask? {
tasks
.filter { !$0.isComplete && $0.dueDate != nil }
.sorted { ($0.dueDate ?? .distantFuture) < ($1.dueDate ?? .distantFuture) }
.first
}
// Today's task completion: (done, total) for tasks due today.
func todayTaskCompletion() -> (done: Int, total: Int) {
let cal = Calendar.current
let start = cal.startOfDay(for: Date())
let end = cal.date(byAdding: .day, value: 1, to: start)!
let tasks = loadWidgetTasks()
func dueToday(_ t: WidgetTask) -> Bool {
guard let due = t.dueDate else { return false }
return due >= start && due < end
}
let active = tasks.filter { !$0.isComplete && dueToday($0) }
let completed = tasks.filter { $0.isComplete && dueToday($0) }
return (completed.count, active.count + completed.count)
}
// Workout streak count
func loadWorkoutStreak() -> Int {
let uid = UserDefaults.kisani.string(forKey: "kisani.activeUserId") ?? "shared"
let key = "kisani.workout.completedDates.\(uid)"
let dates = UserDefaults.kisani.stringArray(forKey: key) ?? []
let fmt = DateFormatter(); fmt.dateFormat = "yyyy-MM-dd"
let cal = Calendar.current
let dateSet = Set(dates)
var streak = 0
var check = cal.startOfDay(for: Date())
if !dateSet.contains(fmt.string(from: check)) {
check = cal.date(byAdding: .day, value: -1, to: check)!
}
while dateSet.contains(fmt.string(from: check)) {
streak += 1
check = cal.date(byAdding: .day, value: -1, to: check)!
}
return streak
}

View File

@@ -0,0 +1,289 @@
import SwiftUI
import WidgetKit
// MARK: - Dot-grid progress (day / week / month / year)
enum TimePeriod { case day, week, month, year }
struct ProgressDotView: View {
let period: TimePeriod
let accent: Color
@Environment(\.widgetFamily) private var family
private var label: String {
switch period {
case .day: let f = DateFormatter(); f.dateFormat = "EEEE d"; return f.string(from: Date())
case .week: return "This week"
case .month: let f = DateFormatter(); f.dateFormat = "MMMM"; return f.string(from: Date())
case .year: let f = DateFormatter(); f.dateFormat = "yyyy"; return f.string(from: Date())
}
}
private var progress: Double {
let cal = Calendar.current; let now = Date()
switch period {
case .day:
let sod = cal.startOfDay(for: now)
return (now.timeIntervalSince(sod)) / 86400
case .week:
let comps = cal.dateComponents([.weekday], from: now)
let wd = (comps.weekday ?? 1) - 1
let secIntoDay = now.timeIntervalSince(cal.startOfDay(for: now))
return (Double(wd) * 86400 + secIntoDay) / (7 * 86400)
case .month:
let range = cal.range(of: .day, in: .month, for: now)!
let day = cal.component(.day, from: now) - 1
let secIntoDay = now.timeIntervalSince(cal.startOfDay(for: now))
return (Double(day) * 86400 + secIntoDay) / (Double(range.count) * 86400)
case .year:
let start = cal.date(from: cal.dateComponents([.year], from: now))!
let end = cal.date(byAdding: .year, value: 1, to: start)!
return now.timeIntervalSince(start) / end.timeIntervalSince(start)
}
}
private var subLabel: String {
let pct = Int(progress * 100)
return "\(pct)%"
}
var body: some View {
let isMedium = family == .systemMedium
VStack(alignment: .leading, spacing: isMedium ? 10 : 6) {
HStack(alignment: .firstTextBaseline) {
Text(label)
.font(.system(size: isMedium ? 22 : 14, weight: .bold))
.foregroundColor(.white)
.lineLimit(1)
Spacer()
Text(subLabel)
.font(.system(size: isMedium ? 22 : 13, weight: .bold))
.foregroundColor(Color(white: 0.5))
}
DotGrid(total: 100, filled: Int(progress * 100), color: accent)
}
.padding(.horizontal, isMedium ? 4 : 0)
.containerBackground(.black, for: .widget)
}
}
// MARK: - Tasks completed today
struct TasksCompleteView: View {
@Environment(\.widgetFamily) private var family
private let teal = Color(red: 0.48, green: 0.87, blue: 0.80)
private var stats: (done: Int, total: Int) { todayTaskCompletion() }
private var percent: Int {
let s = stats
guard s.total > 0 else { return 0 }
return Int((Double(s.done) / Double(s.total) * 100).rounded())
}
var body: some View {
let s = stats
let isMedium = family == .systemMedium
VStack(alignment: .leading, spacing: isMedium ? 10 : 6) {
HStack(alignment: .firstTextBaseline) {
Text("Today's Tasks")
.font(.system(size: isMedium ? 22 : 14, weight: .bold))
.foregroundColor(.white)
.lineLimit(1)
Spacer()
Text("\(percent)%")
.font(.system(size: isMedium ? 22 : 13, weight: .bold))
.foregroundColor(Color(white: 0.5))
}
if s.total == 0 {
Spacer(minLength: 0)
Text("No tasks due today")
.font(.system(size: 12))
.foregroundColor(Color(white: 0.5))
Spacer(minLength: 0)
} else {
Text("\(s.done) of \(s.total) done")
.font(.system(size: 12))
.foregroundColor(teal)
Spacer(minLength: 2)
DotGrid(total: s.total, filled: s.done, color: teal)
}
}
.padding(.horizontal, isMedium ? 4 : 0)
.containerBackground(.black, for: .widget)
}
}
// MARK: - Circular dot grid (event countdown motif)
struct DotGrid: View {
let total: Int
let filled: Int
let color: Color
var gap: CGFloat = 3
var body: some View {
GeometryReader { geo in
let l = layout(for: total, in: geo.size)
VStack(spacing: gap) {
ForEach(0..<l.rows, id: \.self) { row in
HStack(spacing: gap) {
ForEach(0..<l.cols, id: \.self) { col in
let idx = row * l.cols + col
if idx < total {
dot(filled: idx < filled, size: l.size)
} else {
Color.clear.frame(width: l.size, height: l.size)
}
}
Spacer(minLength: 0)
}
}
Spacer(minLength: 0)
}
}
}
@ViewBuilder
private func dot(filled: Bool, size: CGFloat) -> some View {
if filled {
Circle().fill(color).frame(width: size, height: size)
} else {
Circle()
.strokeBorder(color.opacity(0.55), lineWidth: max(0.8, size * 0.13))
.frame(width: size, height: size)
}
}
/// Pick the column count that yields the largest uniform circle that still fits
/// every dot inside the available area an even grid that fills the widget.
private func layout(for count: Int, in size: CGSize) -> (cols: Int, rows: Int, size: CGFloat) {
guard count > 0, size.width > 1, size.height > 1 else { return (1, 1, 6) }
var best = (cols: 1, rows: count, size: CGFloat(0))
for cols in 1...count {
let rows = Int(ceil(Double(count) / Double(cols)))
let w = (size.width - gap * CGFloat(cols - 1)) / CGFloat(cols)
let h = (size.height - gap * CGFloat(rows - 1)) / CGFloat(rows)
let s = min(w, h)
if s > best.size { best = (cols, rows, s) }
}
return best
}
}
// MARK: - Event Countdown Widget (small + medium)
struct EventCountdownView: View {
let entry: EventEntry
@Environment(\.widgetFamily) var family
private let teal = Color(red: 0.48, green: 0.87, blue: 0.80)
private var percent: Int { Int((entry.progress * 100).rounded()) }
var body: some View {
Group {
if family == .systemSmall {
small
} else {
medium
}
}
.containerBackground(.black, for: .widget)
}
// Wide layout that mirrors the reference: title + percent, then a dot grid.
private var medium: some View {
VStack(alignment: .leading, spacing: 10) {
HStack(alignment: .firstTextBaseline) {
Text(LocalizedStringKey(entry.eventName))
.font(.system(size: 22, weight: .bold))
.foregroundColor(.white)
.lineLimit(1)
Spacer()
Text("\(percent)%")
.font(.system(size: 22, weight: .bold))
.foregroundColor(Color(white: 0.5))
}
DotGrid(total: entry.totalDays,
filled: Int(entry.progress * Double(entry.totalDays)),
color: teal)
}
.padding(.horizontal, 4)
}
private var small: some View {
VStack(alignment: .leading, spacing: 6) {
Text(LocalizedStringKey(entry.eventName))
.font(.system(size: 14, weight: .bold))
.foregroundColor(.white)
.lineLimit(2)
HStack(alignment: .firstTextBaseline, spacing: 4) {
Text("\(entry.daysLeft)")
.font(.system(size: 28, weight: .bold, design: .rounded))
.foregroundColor(teal)
Text(entry.daysLeft == 1 ? "day" : "days")
.font(.system(size: 12))
.foregroundColor(Color(white: 0.5))
}
Spacer(minLength: 2)
DotGrid(total: entry.totalDays,
filled: Int(entry.progress * Double(entry.totalDays)),
color: teal)
}
}
}
// MARK: - Lock Screen Widget (accessoryRectangular)
struct LockScreenRectView: View {
let task: WidgetTask
var body: some View {
HStack(spacing: 10) {
VStack(alignment: .leading, spacing: 2) {
Text(task.title)
.font(.system(size: 13, weight: .semibold))
.lineLimit(1)
Text(task.timeLeftLabel)
.font(.system(size: 11))
.foregroundColor(.secondary)
}
Spacer()
if !task.isComplete, let days = task.daysLeft, days <= 365 {
ProgressView(value: Double(365 - days), total: 365)
.progressViewStyle(.circular)
.frame(width: 22, height: 22)
.tint(.white)
} else if task.isComplete {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(.green)
}
}
.containerBackground(.thinMaterial, for: .widget)
}
}
// MARK: - Lock Screen Circular
struct LockScreenCircularView: View {
let task: WidgetTask
var body: some View {
ZStack {
if !task.isComplete, let days = task.daysLeft, days <= 365 {
ProgressView(value: Double(365 - days), total: 365)
.progressViewStyle(.circular)
.tint(.white)
}
VStack(spacing: 0) {
if let days = task.daysLeft {
Text("\(days)").font(.system(size: 14, weight: .bold))
Text("days").font(.system(size: 7))
} else {
Image(systemName: "checkmark").font(.system(size: 14, weight: .bold))
}
}
}
.containerBackground(.thinMaterial, for: .widget)
}
}