import SwiftUI import WidgetKit // MARK: - Shared widget theme (slate background + brand orange) enum WidgetTheme { // Exactly the brand palette: app accent RGB(232,98,42) on the logo's // dark RGB(26,29,34) — widgets read as siblings of the app icon. static let background = Color(red: 26/255, green: 29/255, blue: 34/255) static let accent = Color(red: 232/255, green: 98/255, blue: 42/255) static let text = Color.white static let textDim = Color.white.opacity(0.55) static let textFaint = Color.white.opacity(0.4) } private extension String { func widgetTitleCased() -> String { split(separator: " ").map { rawWord in let word = String(rawWord) guard let first = word.first else { return word } if word == word.uppercased(), word.count <= 5 { return word } return first.uppercased() + word.dropFirst().lowercased() } .joined(separator: " ") } } // MARK: - Vertical-bar progress (comb / equalizer style) struct BarGrid: View { let progress: Double // 0…1 let color: Color var count: Int = 64 var gap: CGFloat = 3 var body: some View { GeometryReader { geo in let totalGap = gap * CGFloat(count - 1) let barW = max(1.5, (geo.size.width - totalGap) / CGFloat(count)) let filled = Int((progress * Double(count)).rounded()) HStack(spacing: gap) { ForEach(0.. 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) } } private func layout(for count: Int, in size: CGSize) -> (cols: Int, rows: Int, size: CGFloat, width: CGFloat, height: CGFloat) { guard count > 0, size.width > 1, size.height > 1 else { return (1, 1, 6, 6, 6) } var best = (cols: 1, rows: count, size: CGFloat(0), width: CGFloat(0), height: 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) let gridWidth = CGFloat(cols) * s + gap * CGFloat(cols - 1) let gridHeight = CGFloat(rows) * s + gap * CGFloat(rows - 1) if s > best.size && gridWidth <= size.width && gridHeight <= size.height { best = (cols, rows, s, gridWidth, gridHeight) } } return best } } // MARK: - Event Countdown — Bars style (medium) struct EventBarsView: View { let entry: EventEntry var body: some View { VStack(alignment: .leading, spacing: 0) { HStack(alignment: .firstTextBaseline, spacing: 10) { Text(entry.eventName.widgetTitleCased()) .font(.system(.body, design: .monospaced)) .foregroundColor(WidgetTheme.accent) .lineLimit(1) .minimumScaleFactor(0.78) .layoutPriority(1) Button(intent: CycleCountdownUnitIntent()) { Text(entry.fineTimeLeft) .font(.system(.body, design: .monospaced)) .foregroundColor(WidgetTheme.textDim) .lineLimit(1) .minimumScaleFactor(0.9) } .buttonStyle(.plain) .fixedSize(horizontal: true, vertical: false) .layoutPriority(2) Spacer(minLength: 0) } Spacer(minLength: 8) BarGrid(progress: entry.progress, color: WidgetTheme.accent) .frame(maxHeight: .infinity) } .padding(.horizontal, 4).padding(.vertical, 2) .containerBackground(WidgetTheme.background, for: .widget) } } // 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(.caption, design: .monospaced)) .lineLimit(1) Text(task.timeLeftLabel) .font(.system(.caption2, design: .monospaced)) .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(WidgetTheme.accent) } 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(WidgetTheme.accent) } VStack(spacing: 0) { if let days = task.daysLeft { Text("\(days)").font(.system(.caption, design: .monospaced)) Text("days").font(.system(.caption2, design: .monospaced)) } else { Image(systemName: "checkmark").font(.system(size: 14)) } } } .containerBackground(.thinMaterial, for: .widget) } }