// ── Timeline View ────────────────────────────────────────────── // Main screen — vertical timeline showing all active/upcoming timers import SwiftUI struct TimelineView: View { @EnvironmentObject var store: TimerStore @State private var showCreateTimer = false @State private var showQuickTimer = false var body: some View { NavigationStack { ZStack { CMColors.bg.ignoresSafeArea() ScrollView { VStack(spacing: CMSpacing.xl) { // Clock header ClockHeader(now: store.now) // Next up card if let next = store.nextFiringTimer { NextUpCard(timer: next, now: store.now) } // Quick timer bar QuickTimerBar { showQuickTimer = true } // Active timers if store.activeTimers.isEmpty { EmptyTimelineView() } else { TimerListSection( title: "Active", timers: store.activeTimers.sorted { $0.targetTime < $1.targetTime }, now: store.now ) } // Firing timers overlay check let firingTimers = store.timers.filter { $0.state == .firing } if !firingTimers.isEmpty { // Show inline firing card for non-critical ForEach(firingTimers.filter { $0.urgency != .critical }) { timer in FiringCard(timer: timer) } } // Completed/dismissed (recent) let recentDone = store.timers .filter { [.completed, .dismissed].contains($0.state) } .sorted { ($0.completedAt ?? $0.dismissedAt ?? .distantPast) > ($1.completedAt ?? $1.dismissedAt ?? .distantPast) } .prefix(5) if !recentDone.isEmpty { TimerListSection( title: "Recent", timers: Array(recentDone), now: store.now ) } } .padding(.horizontal, CMSpacing.lg) .padding(.bottom, 100) } // FAB VStack { Spacer() HStack { Spacer() Button { HapticEngine.tap() showCreateTimer = true } label: { Image(systemName: "plus") .font(.title2.weight(.semibold)) .foregroundStyle(.white) .frame(width: 56, height: 56) .background(CMColors.accent) .clipShape(Circle()) .shadow(color: CMShadow.glow, radius: 12) } .padding(.trailing, CMSpacing.xl) .padding(.bottom, CMSpacing.lg) } } } .navigationBarHidden(true) .sheet(isPresented: $showCreateTimer) { CreateTimerView() } .sheet(isPresented: $showQuickTimer) { QuickTimerSheet() } .overlay { // Full-screen overlay for CRITICAL firing timers let criticalFiring = store.timers.first { $0.state == .firing && $0.urgency == .critical } if let timer = criticalFiring { AlarmOverlay(timer: timer) } } } } } // MARK: - Clock Header struct ClockHeader: View { let now: Date var body: some View { VStack(spacing: CMSpacing.xs) { Text(formatTime(now)) .font(CMFonts.mono(size: 48, weight: .bold)) .foregroundStyle(CMColors.text) .shadow(color: CMColors.accentGlow, radius: 20) Text(formatDate(now)) .font(CMFonts.body(size: 16, weight: .medium)) .foregroundStyle(CMColors.textSecondary) } .frame(maxWidth: .infinity) .padding(.top, CMSpacing.xl) .padding(.bottom, CMSpacing.md) } } // MARK: - Next Up Card struct NextUpCard: View { let timer: CMTimer let now: Date var body: some View { VStack(alignment: .leading, spacing: CMSpacing.sm) { HStack { Text("NEXT UP") .font(CMFonts.body(size: 11, weight: .bold)) .foregroundStyle(CMColors.textMuted) .tracking(1.5) Spacer() UrgencyBadge(urgency: timer.urgency) } Text(timer.label) .font(CMFonts.body(size: 18, weight: .semibold)) .foregroundStyle(CMColors.text) HStack { Image(systemName: "clock") .font(.caption) .foregroundStyle(CMColors.textMuted) Text(formatRelativeTime(timer.targetTime, now: now)) .font(CMFonts.mono(size: 14, weight: .medium)) .foregroundStyle(CMColors.urgencyColor(timer.urgency)) Spacer() Text(formatTime(timer.targetTime)) .font(CMFonts.body(size: 14)) .foregroundStyle(CMColors.textSecondary) } // Cascade progress if !timer.warnings.isEmpty { let firedCount = timer.warnings.filter(\.fired).count let total = timer.warnings.count CascadeProgressBar( fired: firedCount, total: total, urgency: timer.urgency ) } // Time blindness aid let remaining = getRemainingSeconds(timer, now: now) if let ref = getTimeReference(seconds: remaining) { Text(ref) .font(CMFonts.body(size: 12)) .foregroundStyle(CMColors.textMuted) .italic() } } .padding(CMSpacing.lg) .background(CMColors.surface) .overlay( RoundedRectangle(cornerRadius: CMRadius.lg) .stroke(CMColors.urgencyBorder(timer.urgency), lineWidth: 1) ) .clipShape(RoundedRectangle(cornerRadius: CMRadius.lg)) } } // MARK: - Quick Timer Bar struct QuickTimerBar: View { let onCustom: () -> Void @EnvironmentObject var store: TimerStore private let presets: [(String, Int)] = [ ("5m", 5), ("15m", 15), ("25m", 25), ("45m", 45), ("1h", 60), ] var body: some View { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: CMSpacing.sm) { ForEach(presets, id: \.0) { label, minutes in Button { HapticEngine.tap() let _ = store.addCountdown(CreateCountdownParams( label: "\(label) Timer", durationSeconds: TimeInterval(minutes * 60), cascade: CascadeConfig(preset: .light, intervals: []) )) } label: { Text(label) .font(CMFonts.body(size: 14, weight: .semibold)) .foregroundStyle(CMColors.text) .padding(.horizontal, CMSpacing.lg) .padding(.vertical, CMSpacing.sm) .background(CMColors.surface) .clipShape(Capsule()) .overlay( Capsule().stroke(CMColors.border, lineWidth: 1) ) } } Button { HapticEngine.tap() onCustom() } label: { Image(systemName: "slider.horizontal.3") .font(.body.weight(.semibold)) .foregroundStyle(CMColors.accent) .padding(.horizontal, CMSpacing.lg) .padding(.vertical, CMSpacing.sm) .background(CMColors.surface) .clipShape(Capsule()) .overlay( Capsule().stroke(CMColors.accent.opacity(0.4), lineWidth: 1) ) } } } } } // MARK: - Empty State struct EmptyTimelineView: View { var body: some View { VStack(spacing: CMSpacing.lg) { Image(systemName: "timer") .font(.system(size: 48)) .foregroundStyle(CMColors.textMuted) Text("No active timers") .font(CMFonts.body(size: 18, weight: .semibold)) .foregroundStyle(CMColors.textSecondary) Text("Create your first timer with the + button\nor tap a quick preset above") .font(CMFonts.body(size: 14)) .foregroundStyle(CMColors.textMuted) .multilineTextAlignment(.center) } .padding(.vertical, CMSpacing.xxxl) } }