99 lines
3.2 KiB
Swift
99 lines
3.2 KiB
Swift
// ── Countdown Ring ─────────────────────────────────────────────
|
|
// Visual countdown ring (like Time Timer) — neurodivergent-friendly
|
|
// Color transitions: green → yellow → orange → red as time decreases
|
|
|
|
import SwiftUI
|
|
|
|
struct CountdownRing: View {
|
|
let progress: Double // 0.0 (full) → 1.0 (empty)
|
|
let urgency: UrgencyLevel
|
|
let remainingSeconds: TimeInterval
|
|
let totalSeconds: TimeInterval
|
|
var size: CGFloat = 220
|
|
var lineWidth: CGFloat = 12
|
|
|
|
private var ringColor: Color {
|
|
// Color transition based on remaining time ratio
|
|
let ratio = 1.0 - progress // 1.0 = full time, 0.0 = no time
|
|
if ratio > 0.5 {
|
|
return CMColors.gentle // green
|
|
} else if ratio > 0.25 {
|
|
return CMColors.standard // yellow
|
|
} else if ratio > 0.1 {
|
|
return CMColors.important // orange
|
|
} else {
|
|
return CMColors.critical // red
|
|
}
|
|
}
|
|
|
|
private var glowColor: Color {
|
|
ringColor.opacity(0.3)
|
|
}
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
// Background track
|
|
Circle()
|
|
.stroke(CMColors.border, lineWidth: lineWidth)
|
|
.frame(width: size, height: size)
|
|
|
|
// Progress arc (fills clockwise from 12 o'clock)
|
|
Circle()
|
|
.trim(from: 0, to: 1.0 - progress)
|
|
.stroke(
|
|
ringColor,
|
|
style: StrokeStyle(lineWidth: lineWidth, lineCap: .round)
|
|
)
|
|
.frame(width: size, height: size)
|
|
.rotationEffect(.degrees(-90))
|
|
.shadow(color: glowColor, radius: 8)
|
|
.animation(.easeInOut(duration: 0.3), value: progress)
|
|
|
|
// Center content
|
|
VStack(spacing: CMSpacing.xs) {
|
|
Text(formatDuration(remainingSeconds))
|
|
.font(CMFonts.mono(size: size * 0.18, weight: .bold))
|
|
.foregroundStyle(CMColors.text)
|
|
.contentTransition(.numericText())
|
|
|
|
if totalSeconds > 0 {
|
|
Text(formatDurationCompact(totalSeconds))
|
|
.font(CMFonts.body(size: size * 0.06))
|
|
.foregroundStyle(CMColors.textMuted)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Mini Countdown Ring (for cards)
|
|
|
|
struct MiniCountdownRing: View {
|
|
let progress: Double
|
|
let urgency: UrgencyLevel
|
|
var size: CGFloat = 32
|
|
var lineWidth: CGFloat = 3
|
|
|
|
private var ringColor: Color {
|
|
CMColors.urgencyColor(urgency)
|
|
}
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
Circle()
|
|
.stroke(CMColors.border, lineWidth: lineWidth)
|
|
.frame(width: size, height: size)
|
|
|
|
Circle()
|
|
.trim(from: 0, to: 1.0 - progress)
|
|
.stroke(
|
|
ringColor,
|
|
style: StrokeStyle(lineWidth: lineWidth, lineCap: .round)
|
|
)
|
|
.frame(width: size, height: size)
|
|
.rotationEffect(.degrees(-90))
|
|
.animation(.easeInOut(duration: 0.3), value: progress)
|
|
}
|
|
}
|
|
}
|