learning_ai_clock/ios/ChronoMind/Views/Components/TimerCard.swift

257 lines
9.3 KiB
Swift

// 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)
}
}
}
}
}