// ── Timer Card ───────────────────────────────────────────────── // Individual timer display for timeline list import SwiftUI struct TimerCard: View { let timer: CMTimer let now: Date @EnvironmentObject var store: TimerStore var body: some View { VStack(alignment: .leading, spacing: CMSpacing.sm) { // Header: urgency + label + time HStack(alignment: .top) { // Urgency indicator dot Circle() .fill(CMColors.urgencyColor(timer.urgency)) .frame(width: 8, height: 8) .padding(.top, 6) VStack(alignment: .leading, spacing: CMSpacing.xxs) { Text(timer.label) .font(CMFonts.body(size: 16, weight: .semibold)) .foregroundStyle(CMColors.text) .strikethrough(timer.state == .dismissed || timer.state == .completed) if let desc = timer.description, !desc.isEmpty { Text(desc) .font(CMFonts.body(size: 13)) .foregroundStyle(CMColors.textSecondary) .lineLimit(1) } } Spacer() // State / countdown VStack(alignment: .trailing, spacing: CMSpacing.xxs) { timerStateView Text(formatTime(timer.targetTime)) .font(CMFonts.body(size: 12)) .foregroundStyle(CMColors.textMuted) } } // Cascade progress (if applicable) if !timer.warnings.isEmpty && isTimerActive(timer) { let firedCount = timer.warnings.filter(\.fired).count CascadeProgressBar( fired: firedCount, total: timer.warnings.count, urgency: timer.urgency ) } // Pomodoro round indicator if timer.type == .pomodoro, let state = timer.pomodoroState, let config = timer.pomodoroConfig { PomodoroRoundIndicator( currentRound: state.currentRound, totalRounds: config.rounds, completedRounds: state.completedRounds, isBreak: state.isBreak || state.isLongBreak ) } // Action buttons for firing timers if timer.state == .firing { HStack(spacing: CMSpacing.sm) { Button { HapticEngine.tap() store.snooze(timer.id, minutes: 5) } label: { Label("5m", systemImage: "moon.zzz") .font(CMFonts.body(size: 13, weight: .medium)) .foregroundStyle(CMColors.text) .padding(.horizontal, CMSpacing.md) .padding(.vertical, CMSpacing.sm) .background(CMColors.surfaceHover) .clipShape(Capsule()) } Button { HapticEngine.tap() store.snooze(timer.id, minutes: 15) } label: { Label("15m", systemImage: "moon.zzz") .font(CMFonts.body(size: 13, weight: .medium)) .foregroundStyle(CMColors.text) .padding(.horizontal, CMSpacing.md) .padding(.vertical, CMSpacing.sm) .background(CMColors.surfaceHover) .clipShape(Capsule()) } Spacer() Button { HapticEngine.tap() store.dismiss(timer.id) } label: { Text("Dismiss") .font(CMFonts.body(size: 13, weight: .semibold)) .foregroundStyle(.white) .padding(.horizontal, CMSpacing.lg) .padding(.vertical, CMSpacing.sm) .background(CMColors.error) .clipShape(Capsule()) } } } // Action buttons for active/paused timers (countdown/pomodoro) if [.active, .warning, .paused].contains(timer.state) && timer.type != .alarm { HStack(spacing: CMSpacing.sm) { if timer.state == .paused { Button { HapticEngine.tap() store.resume(timer.id) } label: { Label("Resume", systemImage: "play.fill") .font(CMFonts.body(size: 13, weight: .medium)) .foregroundStyle(CMColors.accent) } } else { Button { HapticEngine.tap() store.pause(timer.id) } label: { Label("Pause", systemImage: "pause.fill") .font(CMFonts.body(size: 13, weight: .medium)) .foregroundStyle(CMColors.textSecondary) } } Spacer() Button { HapticEngine.tap() store.dismiss(timer.id) } label: { Image(systemName: "xmark") .font(.caption.weight(.bold)) .foregroundStyle(CMColors.textMuted) } } } } .padding(CMSpacing.lg) .background(cardBackground) .overlay( RoundedRectangle(cornerRadius: CMRadius.md) .stroke(cardBorder, lineWidth: 1) ) .clipShape(RoundedRectangle(cornerRadius: CMRadius.md)) .timerAccessible(timer, now: now) } // MARK: - Timer State View @ViewBuilder private var timerStateView: some View { switch timer.state { case .active, .warning: let remaining = getRemainingSeconds(timer, now: now) Text(formatDuration(remaining)) .font(CMFonts.mono(size: 16, weight: .semibold)) .foregroundStyle(CMColors.urgencyColor(timer.urgency)) case .paused: let remaining = getRemainingSeconds(timer, now: now) HStack(spacing: 4) { Image(systemName: "pause.fill") .font(.caption2) Text(formatDuration(remaining)) .font(CMFonts.mono(size: 16, weight: .semibold)) } .foregroundStyle(CMColors.textMuted) case .firing: Text("FIRING") .font(CMFonts.body(size: 12, weight: .bold)) .foregroundStyle(CMColors.urgencyColor(timer.urgency)) .padding(.horizontal, CMSpacing.sm) .padding(.vertical, CMSpacing.xxs) .background(CMColors.urgencyBg(timer.urgency)) .clipShape(Capsule()) case .snoozed: if let until = timer.snoozedUntil { Text("Snoozed → \(formatTime(until))") .font(CMFonts.body(size: 12)) .foregroundStyle(CMColors.textMuted) } case .completed: Label("Done", systemImage: "checkmark.circle.fill") .font(CMFonts.body(size: 12, weight: .medium)) .foregroundStyle(CMColors.success) case .dismissed: Text("Dismissed") .font(CMFonts.body(size: 12)) .foregroundStyle(CMColors.textMuted) case .idle: EmptyView() } } // MARK: - Card Styling private var cardBackground: Color { switch timer.state { case .firing: return CMColors.urgencyBg(timer.urgency) default: return CMColors.surface } } private var cardBorder: Color { switch timer.state { case .firing: return CMColors.urgencyBorder(timer.urgency) default: return CMColors.border } } } // MARK: - Pomodoro Round Indicator struct PomodoroRoundIndicator: View { let currentRound: Int let totalRounds: Int let completedRounds: Int let isBreak: Bool var body: some View { HStack(spacing: CMSpacing.sm) { Text(isBreak ? "Break" : "Round \(currentRound) of \(totalRounds)") .font(CMFonts.body(size: 12, weight: .medium)) .foregroundStyle(CMColors.textSecondary) HStack(spacing: CMSpacing.xs) { ForEach(1...totalRounds, id: \.self) { round in Circle() .fill(round <= completedRounds ? CMColors.accent : CMColors.border) .frame(width: 6, height: 6) } } } } }