// ── Create Timer View ────────────────────────────────────────── // Modal for creating alarm, countdown, or pomodoro timers import SwiftUI struct CreateTimerView: View { @EnvironmentObject var store: TimerStore @Environment(\.dismiss) private var dismiss @State private var timerType: CMTimerType = .countdown @State private var label = "" @State private var urgency: UrgencyLevel = .standard @State private var cascadePreset: CascadePreset = .standard @State private var category = "" // Countdown fields @State private var hours = 0 @State private var minutes = 25 @State private var seconds = 0 // Alarm fields @State private var alarmDate = Date().addingTimeInterval(3600) // 1h from now // Pomodoro fields @State private var workMinutes = 25 @State private var breakMinutes = 5 @State private var longBreakMinutes = 15 @State private var rounds = 4 var body: some View { NavigationStack { ZStack { CMColors.bg.ignoresSafeArea() ScrollView { VStack(spacing: CMSpacing.xl) { // Timer type selector timerTypePicker // Label CMTextField( title: "Label", placeholder: placeholderForType, text: $label ) // Type-specific fields switch timerType { case .countdown: countdownPicker case .alarm: alarmPicker case .pomodoro: pomodoroPicker case .event: alarmPicker // same UI for now } // Urgency selector urgencySelector // Cascade preset cascadeSelector // Time blindness reference if let ref = timeReference { HStack { Image(systemName: "lightbulb.fill") .foregroundStyle(CMColors.standard) .font(.caption) Text(ref) .font(CMFonts.body(size: 13)) .foregroundStyle(CMColors.textMuted) .italic() } .padding(CMSpacing.md) .frame(maxWidth: .infinity, alignment: .leading) .background(CMColors.surface) .clipShape(RoundedRectangle(cornerRadius: CMRadius.sm)) } } .padding(CMSpacing.lg) } } .navigationTitle("New Timer") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { dismiss() } .foregroundStyle(CMColors.textSecondary) } ToolbarItem(placement: .confirmationAction) { Button("Create") { createTimer() dismiss() } .font(.body.weight(.semibold)) .foregroundStyle(CMColors.accent) .disabled(!isValid) } } .toolbarBackground(CMColors.surface, for: .navigationBar) .toolbarColorScheme(.dark, for: .navigationBar) } } // MARK: - Timer Type Picker private var timerTypePicker: some View { HStack(spacing: 0) { ForEach([CMTimerType.countdown, .alarm, .pomodoro], id: \.self) { type in Button { HapticEngine.selection() withAnimation(.easeInOut(duration: 0.2)) { timerType = type } } label: { Text(type.label) .font(CMFonts.body(size: 14, weight: timerType == type ? .semibold : .regular)) .foregroundStyle(timerType == type ? CMColors.text : CMColors.textMuted) .frame(maxWidth: .infinity) .padding(.vertical, CMSpacing.md) .background(timerType == type ? CMColors.accent.opacity(0.15) : Color.clear) } } } .background(CMColors.surface) .clipShape(RoundedRectangle(cornerRadius: CMRadius.sm)) .overlay( RoundedRectangle(cornerRadius: CMRadius.sm) .stroke(CMColors.border, lineWidth: 1) ) } // MARK: - Countdown Picker private var countdownPicker: some View { VStack(alignment: .leading, spacing: CMSpacing.sm) { Text("Duration") .font(CMFonts.body(size: 13, weight: .medium)) .foregroundStyle(CMColors.textSecondary) HStack(spacing: CMSpacing.md) { durationWheel(title: "Hours", value: $hours, range: 0...23) durationWheel(title: "Min", value: $minutes, range: 0...59) durationWheel(title: "Sec", value: $seconds, range: 0...59) } .frame(height: 150) } } private func durationWheel(title: String, value: Binding, range: ClosedRange) -> some View { VStack(spacing: CMSpacing.xs) { Text(title) .font(CMFonts.body(size: 11, weight: .medium)) .foregroundStyle(CMColors.textMuted) Picker(title, selection: value) { ForEach(range, id: \.self) { n in Text("\(n)") .font(CMFonts.mono(size: 20)) .foregroundStyle(CMColors.text) .tag(n) } } .pickerStyle(.wheel) } .frame(maxWidth: .infinity) } // MARK: - Alarm Picker private var alarmPicker: some View { VStack(alignment: .leading, spacing: CMSpacing.sm) { Text("Date & Time") .font(CMFonts.body(size: 13, weight: .medium)) .foregroundStyle(CMColors.textSecondary) DatePicker( "Target Time", selection: $alarmDate, in: Date()..., displayedComponents: [.date, .hourAndMinute] ) .datePickerStyle(.graphical) .tint(CMColors.accent) .colorScheme(.dark) } } // MARK: - Pomodoro Picker private var pomodoroPicker: some View { VStack(alignment: .leading, spacing: CMSpacing.md) { Text("Pomodoro Settings") .font(CMFonts.body(size: 13, weight: .medium)) .foregroundStyle(CMColors.textSecondary) CMStepper(title: "Work", value: $workMinutes, range: 5...90, suffix: "min") CMStepper(title: "Break", value: $breakMinutes, range: 1...30, suffix: "min") CMStepper(title: "Long Break", value: $longBreakMinutes, range: 5...60, suffix: "min") CMStepper(title: "Rounds", value: $rounds, range: 1...12, suffix: "") // Total time let totalMinutes = (workMinutes * rounds) + (breakMinutes * (rounds - 1)) + longBreakMinutes Text("Total: \(formatDurationCompact(TimeInterval(totalMinutes * 60)))") .font(CMFonts.body(size: 13)) .foregroundStyle(CMColors.textMuted) } } // MARK: - Urgency Selector private var urgencySelector: some View { VStack(alignment: .leading, spacing: CMSpacing.sm) { Text("Urgency") .font(CMFonts.body(size: 13, weight: .medium)) .foregroundStyle(CMColors.textSecondary) HStack(spacing: CMSpacing.sm) { ForEach(UrgencyLevel.allCases) { level in Button { HapticEngine.selection() urgency = level } label: { VStack(spacing: CMSpacing.xxs) { Circle() .fill(CMColors.urgencyColor(level)) .frame(width: urgency == level ? 24 : 16, height: urgency == level ? 24 : 16) .overlay( Circle() .stroke(Color.white, lineWidth: urgency == level ? 2 : 0) ) Text(getUrgencyConfig(level).label) .font(CMFonts.body(size: 10, weight: urgency == level ? .bold : .regular)) .foregroundStyle(urgency == level ? CMColors.text : CMColors.textMuted) } .frame(maxWidth: .infinity) } .animation(.easeInOut(duration: 0.15), value: urgency) } } .padding(CMSpacing.md) .background(CMColors.surface) .clipShape(RoundedRectangle(cornerRadius: CMRadius.sm)) } } // MARK: - Cascade Selector private var cascadeSelector: some View { VStack(alignment: .leading, spacing: CMSpacing.sm) { Text("Pre-Warning Cascade") .font(CMFonts.body(size: 13, weight: .medium)) .foregroundStyle(CMColors.textSecondary) ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: CMSpacing.sm) { ForEach(CascadePreset.allCases.filter { $0 != .custom }) { preset in Button { HapticEngine.selection() cascadePreset = preset } label: { VStack(spacing: CMSpacing.xxs) { Text(preset.label) .font(CMFonts.body(size: 13, weight: cascadePreset == preset ? .semibold : .regular)) .foregroundStyle(cascadePreset == preset ? CMColors.text : CMColors.textMuted) let intervals = preset.defaultIntervals if intervals.isEmpty { Text("—") .font(CMFonts.body(size: 10)) .foregroundStyle(CMColors.textMuted) } else { Text(intervals.map { formatMinutesBefore($0) }.joined(separator: ", ")) .font(CMFonts.body(size: 9)) .foregroundStyle(CMColors.textMuted) .lineLimit(1) } } .padding(.horizontal, CMSpacing.md) .padding(.vertical, CMSpacing.sm) .background(cascadePreset == preset ? CMColors.accent.opacity(0.15) : CMColors.surface) .clipShape(RoundedRectangle(cornerRadius: CMRadius.sm)) .overlay( RoundedRectangle(cornerRadius: CMRadius.sm) .stroke(cascadePreset == preset ? CMColors.accent.opacity(0.4) : CMColors.border, lineWidth: 1) ) } } } } } } // MARK: - Helpers private var placeholderForType: String { switch timerType { case .countdown: return "e.g., Pasta timer" case .alarm: return "e.g., Team standup" case .pomodoro: return "e.g., Deep work session" case .event: return "e.g., Vacation countdown" } } private var timeReference: String? { switch timerType { case .countdown: let totalSeconds = (hours * 3600) + (minutes * 60) + seconds return getTimeReference(seconds: TimeInterval(totalSeconds)) case .pomodoro: return getTimeReference(minutes: workMinutes) default: return nil } } private var isValid: Bool { switch timerType { case .countdown: return (hours + minutes + seconds) > 0 case .alarm, .event: return alarmDate > Date() case .pomodoro: return workMinutes > 0 } } private func createTimer() { let timerLabel = label.isEmpty ? placeholderForType.replacingOccurrences(of: "e.g., ", with: "") : label let cascadeConfig = CascadeConfig(preset: cascadePreset, intervals: []) switch timerType { case .countdown: let totalSeconds = TimeInterval((hours * 3600) + (minutes * 60) + seconds) let _ = store.addCountdown(CreateCountdownParams( label: timerLabel, durationSeconds: totalSeconds, urgency: urgency, cascade: cascadeConfig, category: category.isEmpty ? nil : category )) case .alarm, .event: let _ = store.addAlarm(CreateAlarmParams( label: timerLabel, targetTime: alarmDate, urgency: urgency, cascade: cascadeConfig, category: category.isEmpty ? nil : category )) case .pomodoro: let config = PomodoroConfig( workMinutes: workMinutes, breakMinutes: breakMinutes, longBreakMinutes: longBreakMinutes, rounds: rounds ) let _ = store.addPomodoro(CreatePomodoroParams( label: timerLabel, config: config, urgency: urgency )) } HapticEngine.tap() } } // MARK: - CM Text Field struct CMTextField: View { let title: String let placeholder: String @Binding var text: String var body: some View { VStack(alignment: .leading, spacing: CMSpacing.sm) { Text(title) .font(CMFonts.body(size: 13, weight: .medium)) .foregroundStyle(CMColors.textSecondary) TextField(placeholder, text: $text) .font(CMFonts.body(size: 16)) .foregroundStyle(CMColors.text) .padding(CMSpacing.md) .background(CMColors.surface) .clipShape(RoundedRectangle(cornerRadius: CMRadius.sm)) .overlay( RoundedRectangle(cornerRadius: CMRadius.sm) .stroke(CMColors.border, lineWidth: 1) ) } } } // MARK: - CM Stepper struct CMStepper: View { let title: String @Binding var value: Int let range: ClosedRange let suffix: String var body: some View { HStack { Text(title) .font(CMFonts.body(size: 14)) .foregroundStyle(CMColors.textSecondary) Spacer() HStack(spacing: CMSpacing.md) { Button { if value > range.lowerBound { value -= 1 HapticEngine.selection() } } label: { Image(systemName: "minus.circle.fill") .font(.title3) .foregroundStyle(value > range.lowerBound ? CMColors.textSecondary : CMColors.textMuted) } .disabled(value <= range.lowerBound) Text("\(value)\(suffix.isEmpty ? "" : " \(suffix)")") .font(CMFonts.mono(size: 16, weight: .semibold)) .foregroundStyle(CMColors.text) .frame(minWidth: 60) Button { if value < range.upperBound { value += 1 HapticEngine.selection() } } label: { Image(systemName: "plus.circle.fill") .font(.title3) .foregroundStyle(value < range.upperBound ? CMColors.accent : CMColors.textMuted) } .disabled(value >= range.upperBound) } } .padding(CMSpacing.md) .background(CMColors.surface) .clipShape(RoundedRectangle(cornerRadius: CMRadius.sm)) } }