148 lines
5.9 KiB
Swift
148 lines
5.9 KiB
Swift
// ── Alarm Overlay ──────────────────────────────────────────────
|
|
// Full-screen overlay for CRITICAL urgency firing timers
|
|
// Requires confirm-to-dismiss
|
|
|
|
import SwiftUI
|
|
|
|
struct AlarmOverlay: View {
|
|
let timer: CMTimer
|
|
|
|
@EnvironmentObject var store: TimerStore
|
|
@State private var showConfirmDismiss = false
|
|
@State private var pulseScale: CGFloat = 1.0
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
// Background
|
|
CMColors.bg.opacity(0.95)
|
|
.ignoresSafeArea()
|
|
|
|
VStack(spacing: CMSpacing.xxl) {
|
|
Spacer()
|
|
|
|
// Pulsing urgency ring
|
|
ZStack {
|
|
Circle()
|
|
.stroke(CMColors.critical.opacity(0.2), lineWidth: 4)
|
|
.frame(width: 200, height: 200)
|
|
.scaleEffect(pulseScale)
|
|
.animation(
|
|
.easeInOut(duration: 1.0).repeatForever(autoreverses: true),
|
|
value: pulseScale
|
|
)
|
|
|
|
Circle()
|
|
.stroke(CMColors.critical, lineWidth: 3)
|
|
.frame(width: 160, height: 160)
|
|
|
|
VStack(spacing: CMSpacing.sm) {
|
|
Image(systemName: "bell.fill")
|
|
.font(.system(size: 36))
|
|
.foregroundStyle(CMColors.critical)
|
|
|
|
Text("NOW")
|
|
.font(CMFonts.mono(size: 24, weight: .bold))
|
|
.foregroundStyle(CMColors.critical)
|
|
}
|
|
}
|
|
|
|
// Timer info
|
|
VStack(spacing: CMSpacing.sm) {
|
|
UrgencyBadge(urgency: .critical)
|
|
|
|
Text(timer.label)
|
|
.font(CMFonts.display(size: 28))
|
|
.foregroundStyle(CMColors.text)
|
|
.multilineTextAlignment(.center)
|
|
|
|
if let desc = timer.description, !desc.isEmpty {
|
|
Text(desc)
|
|
.font(CMFonts.body(size: 16))
|
|
.foregroundStyle(CMColors.textSecondary)
|
|
.multilineTextAlignment(.center)
|
|
}
|
|
|
|
if timer.snoozeCount > 0 {
|
|
Text("Snoozed \(timer.snoozeCount) time\(timer.snoozeCount == 1 ? "" : "s")")
|
|
.font(CMFonts.body(size: 14))
|
|
.foregroundStyle(CMColors.textMuted)
|
|
}
|
|
}
|
|
|
|
Spacer()
|
|
|
|
// Snooze buttons
|
|
HStack(spacing: CMSpacing.lg) {
|
|
Button {
|
|
HapticEngine.tap()
|
|
store.snooze(timer.id, minutes: 5)
|
|
} label: {
|
|
VStack(spacing: CMSpacing.xs) {
|
|
Image(systemName: "moon.zzz")
|
|
.font(.title3)
|
|
Text("5 min")
|
|
.font(CMFonts.body(size: 13, weight: .medium))
|
|
}
|
|
.foregroundStyle(CMColors.text)
|
|
.frame(maxWidth: .infinity)
|
|
.padding(.vertical, CMSpacing.lg)
|
|
.background(CMColors.surface)
|
|
.clipShape(RoundedRectangle(cornerRadius: CMRadius.md))
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: CMRadius.md)
|
|
.stroke(CMColors.border, lineWidth: 1)
|
|
)
|
|
}
|
|
|
|
Button {
|
|
HapticEngine.tap()
|
|
store.snooze(timer.id, minutes: 15)
|
|
} label: {
|
|
VStack(spacing: CMSpacing.xs) {
|
|
Image(systemName: "moon.zzz.fill")
|
|
.font(.title3)
|
|
Text("15 min")
|
|
.font(CMFonts.body(size: 13, weight: .medium))
|
|
}
|
|
.foregroundStyle(CMColors.text)
|
|
.frame(maxWidth: .infinity)
|
|
.padding(.vertical, CMSpacing.lg)
|
|
.background(CMColors.surface)
|
|
.clipShape(RoundedRectangle(cornerRadius: CMRadius.md))
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: CMRadius.md)
|
|
.stroke(CMColors.border, lineWidth: 1)
|
|
)
|
|
}
|
|
}
|
|
.padding(.horizontal, CMSpacing.xl)
|
|
|
|
// Dismiss button (requires confirmation for Critical)
|
|
Button {
|
|
HapticEngine.tap()
|
|
showConfirmDismiss = true
|
|
} label: {
|
|
Text("Dismiss")
|
|
.font(CMFonts.body(size: 18, weight: .bold))
|
|
.foregroundStyle(.white)
|
|
.frame(maxWidth: .infinity)
|
|
.padding(.vertical, CMSpacing.lg)
|
|
.background(CMColors.critical)
|
|
.clipShape(RoundedRectangle(cornerRadius: CMRadius.md))
|
|
}
|
|
.padding(.horizontal, CMSpacing.xl)
|
|
.padding(.bottom, CMSpacing.xxl)
|
|
}
|
|
}
|
|
.onAppear { pulseScale = 1.15 }
|
|
.alert("Dismiss Critical Timer?", isPresented: $showConfirmDismiss) {
|
|
Button("Cancel", role: .cancel) {}
|
|
Button("Dismiss", role: .destructive) {
|
|
store.dismiss(timer.id)
|
|
}
|
|
} message: {
|
|
Text("Are you sure you want to dismiss \"\(timer.label)\"? This is a critical timer.")
|
|
}
|
|
}
|
|
}
|