// ── Routine Editor View ─────────────────────────────────────── // Create or edit a routine with steps, durations, and transitions import SwiftUI struct RoutineEditorView: View { let routine: CMRoutine? let onSave: (CMRoutine) -> Void let onCancel: () -> Void @State private var name: String = "" @State private var description: String = "" @State private var steps: [CMRoutineStep] = [] var body: some View { NavigationStack { ScrollView { VStack(spacing: 20) { // Name & description VStack(alignment: .leading, spacing: 8) { Text("Name") .font(.subheadline.bold()) .foregroundColor(CMColors.textSecondary) TextField("Morning Routine", text: $name) .textFieldStyle(.plain) .padding(10) .background(CMColors.surfaceHover) .cornerRadius(8) .foregroundColor(CMColors.text) Text("Description") .font(.subheadline.bold()) .foregroundColor(CMColors.textSecondary) TextField("Start your day with intention", text: $description) .textFieldStyle(.plain) .padding(10) .background(CMColors.surfaceHover) .cornerRadius(8) .foregroundColor(CMColors.text) } // Steps VStack(alignment: .leading, spacing: 8) { HStack { Text("Steps") .font(.subheadline.bold()) .foregroundColor(CMColors.textSecondary) Spacer() Text("\(totalDuration) min total") .font(.caption) .foregroundColor(CMColors.accent) } ForEach(Array(steps.enumerated()), id: \.element.id) { idx, step in stepRow(step, index: idx) } Button { steps.append(CMRoutineStep( label: "", durationMinutes: 10 )) } label: { Label("Add Step", systemImage: "plus.circle.fill") .font(.subheadline) .foregroundColor(CMColors.accent) } .padding(.top, 4) } } .padding() } .background(CMColors.bg.ignoresSafeArea()) .navigationTitle(routine == nil ? "New Routine" : "Edit Routine") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { onCancel() } .foregroundColor(CMColors.textSecondary) } ToolbarItem(placement: .confirmationAction) { Button("Save") { save() } .foregroundColor(CMColors.accent) .disabled(name.trimmingCharacters(in: .whitespaces).isEmpty || steps.isEmpty) } } .onAppear { if let r = routine { name = r.name description = r.routineDescription ?? "" steps = r.steps } } } } // MARK: - Step Row @ViewBuilder private func stepRow(_ step: CMRoutineStep, index: Int) -> some View { VStack(spacing: 8) { HStack(spacing: 8) { // Drag handle / index Text("\(index + 1)") .font(.caption.bold()) .foregroundColor(CMColors.textMuted) .frame(width: 20) // Label TextField("Step name", text: Binding( get: { steps[safe: index]?.label ?? "" }, set: { if steps.indices.contains(index) { steps[index].label = $0 } } )) .textFieldStyle(.plain) .padding(8) .background(CMColors.surfaceHover) .cornerRadius(6) .foregroundColor(CMColors.text) .font(.subheadline) // Duration stepper HStack(spacing: 4) { Button { if steps.indices.contains(index) && steps[index].durationMinutes > 1 { steps[index].durationMinutes -= 1 } } label: { Image(systemName: "minus") .font(.caption2) .foregroundColor(CMColors.textSecondary) } Text("\(steps[safe: index]?.durationMinutes ?? 0)m") .font(.caption.monospacedDigit()) .foregroundColor(CMColors.accent) .frame(width: 30) Button { if steps.indices.contains(index) { steps[index].durationMinutes += 1 } } label: { Image(systemName: "plus") .font(.caption2) .foregroundColor(CMColors.textSecondary) } } .padding(.horizontal, 8) .padding(.vertical, 4) .background(CMColors.surface) .cornerRadius(6) // Delete Button { steps.remove(at: index) } label: { Image(systemName: "trash") .font(.caption) .foregroundColor(CMColors.critical.opacity(0.7)) } } // Transition picker (not on last step) if index < steps.count - 1 { HStack(spacing: 8) { Spacer().frame(width: 20) Image(systemName: "arrow.down") .font(.caption2) .foregroundColor(CMColors.textMuted) Picker("Transition", selection: Binding( get: { steps[safe: index]?.transition ?? .immediate }, set: { if steps.indices.contains(index) { steps[index].transition = $0 } } )) { ForEach(TransitionType.allCases) { t in Text(t.label).tag(t) } } .pickerStyle(.menu) .tint(CMColors.textSecondary) .font(.caption) Spacer() } } // Notes TextField("Notes (optional)", text: Binding( get: { steps[safe: index]?.notes ?? "" }, set: { if steps.indices.contains(index) { steps[index].notes = $0.isEmpty ? nil : $0 } } )) .textFieldStyle(.plain) .padding(6) .background(CMColors.surfaceHover.opacity(0.5)) .cornerRadius(4) .foregroundColor(CMColors.textMuted) .font(.caption) .padding(.leading, 28) } .padding(10) .background(CMColors.surface) .cornerRadius(8) } // MARK: - Computed private var totalDuration: Int { CMRoutine.calculateTotalDuration(steps: steps) } // MARK: - Actions private func save() { let trimmedName = name.trimmingCharacters(in: .whitespaces) guard !trimmedName.isEmpty, !steps.isEmpty else { return } let validSteps = steps.filter { !$0.label.trimmingCharacters(in: .whitespaces).isEmpty } guard !validSteps.isEmpty else { return } if var existing = routine { existing.name = trimmedName existing.routineDescription = description.isEmpty ? nil : description existing.steps = validSteps existing.totalDurationMinutes = CMRoutine.calculateTotalDuration(steps: validSteps) onSave(existing) } else { let newRoutine = CMRoutine( name: trimmedName, routineDescription: description.isEmpty ? nil : description, steps: validSteps ) onSave(newRoutine) } } } // MARK: - Safe Array Subscript private extension Array { subscript(safe index: Int) -> Element? { indices.contains(index) ? self[index] : nil } }