learning_ai_clock/ios/ChronoMind/Views/Routines/RoutineListView.swift
saravanakumardb1 11e50295ea feat: fix web build, add repo infra, port iOS engine modules, add routine screens
- fix(web): cast window through unknown in platform-sync.ts (TS2352)
- docs: add AGENTS.md, README.md, CLAUDE.md, .windsurfrules, .cursorrules, env.example
- feat(ios): port Recurrence.swift from web/src/lib/recurrence.ts
- feat(ios): port NLParser.swift from web/src/lib/nl-parser.ts
- feat(ios): port ContextMessages.swift from web/src/lib/context-messages.ts
- feat(ios): add CMRoutine model + Routines.swift engine with state machine + templates
- feat(ios): add RoutineListView, RoutineRunnerView, RoutineEditorView
- feat(android): add RoutineScreen.kt with list, runner, templates, step controls

Web: 373 tests passing, build succeeds with --webpack flag
2026-02-28 01:50:35 -08:00

211 lines
7.2 KiB
Swift

// Routine List View
// Displays built-in templates and user routines with start/edit actions
import SwiftUI
struct RoutineListView: View {
@State private var routines: [CMRoutine] = RoutineTemplates.all
@State private var activeRoutine: CMRoutine?
@State private var showEditor = false
@State private var editingRoutine: CMRoutine?
var body: some View {
ScrollView {
VStack(spacing: 20) {
// Active routine banner
if let active = activeRoutine {
activeRoutineBanner(active)
}
// Templates section
sectionHeader("Templates")
ForEach(routines.filter { $0.isTemplate }) { routine in
routineCard(routine)
}
// User routines section
let userRoutines = routines.filter { !$0.isTemplate }
if !userRoutines.isEmpty {
sectionHeader("My Routines")
ForEach(userRoutines) { routine in
routineCard(routine)
}
}
}
.padding()
}
.background(CMColors.bg.ignoresSafeArea())
.navigationTitle("Routines")
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button {
editingRoutine = nil
showEditor = true
} label: {
Image(systemName: "plus")
.foregroundColor(CMColors.accent)
}
}
}
.sheet(isPresented: $showEditor) {
RoutineEditorView(
routine: editingRoutine,
onSave: { routine in
if let idx = routines.firstIndex(where: { $0.id == routine.id }) {
routines[idx] = routine
} else {
routines.append(routine)
}
showEditor = false
},
onCancel: { showEditor = false }
)
}
.fullScreenCover(item: $activeRoutine) { routine in
RoutineRunnerView(
routine: routine,
onComplete: { completed in
activeRoutine = nil
if let idx = routines.firstIndex(where: { $0.id == completed.id }) {
routines[idx] = completed
}
},
onCancel: {
activeRoutine = nil
}
)
}
}
// MARK: - Subviews
@ViewBuilder
private func sectionHeader(_ title: String) -> some View {
HStack {
Text(title)
.font(.headline)
.foregroundColor(CMColors.textSecondary)
Spacer()
}
}
@ViewBuilder
private func routineCard(_ routine: CMRoutine) -> some View {
VStack(alignment: .leading, spacing: 8) {
HStack {
VStack(alignment: .leading, spacing: 4) {
Text(routine.name)
.font(.title3.bold())
.foregroundColor(CMColors.text)
if let desc = routine.routineDescription {
Text(desc)
.font(.subheadline)
.foregroundColor(CMColors.textSecondary)
}
}
Spacer()
VStack(alignment: .trailing, spacing: 2) {
Text("\(routine.totalDurationMinutes) min")
.font(.caption.bold())
.foregroundColor(CMColors.accent)
Text("\(routine.steps.count) steps")
.font(.caption2)
.foregroundColor(CMColors.textMuted)
}
}
// Step preview
HStack(spacing: 4) {
ForEach(routine.steps.prefix(5)) { step in
Text(step.label)
.font(.caption2)
.padding(.horizontal, 6)
.padding(.vertical, 2)
.background(CMColors.surfaceHover)
.cornerRadius(4)
.foregroundColor(CMColors.textSecondary)
}
if routine.steps.count > 5 {
Text("+\(routine.steps.count - 5)")
.font(.caption2)
.foregroundColor(CMColors.textMuted)
}
}
// Actions
HStack(spacing: 12) {
Button {
var instance = routine.isTemplate ? routine.instantiate() : routine
instance.start()
activeRoutine = instance
} label: {
Label("Start", systemImage: "play.fill")
.font(.subheadline.bold())
.foregroundColor(.white)
.padding(.horizontal, 16)
.padding(.vertical, 8)
.background(CMColors.accent)
.cornerRadius(8)
}
if !routine.isTemplate {
Button {
editingRoutine = routine
showEditor = true
} label: {
Label("Edit", systemImage: "pencil")
.font(.subheadline)
.foregroundColor(CMColors.textSecondary)
}
}
}
.padding(.top, 4)
}
.padding()
.background(CMColors.surface)
.cornerRadius(12)
}
@ViewBuilder
private func activeRoutineBanner(_ routine: CMRoutine) -> some View {
HStack {
VStack(alignment: .leading, spacing: 2) {
Text("Active Routine")
.font(.caption.bold())
.foregroundColor(CMColors.accent)
Text(routine.name)
.font(.headline)
.foregroundColor(CMColors.text)
if let step = routine.currentStep {
Text("Step: \(step.label)")
.font(.subheadline)
.foregroundColor(CMColors.textSecondary)
}
}
Spacer()
Button("Resume") {
activeRoutine = routine
}
.font(.subheadline.bold())
.padding(.horizontal, 16)
.padding(.vertical, 8)
.background(CMColors.accent)
.foregroundColor(.white)
.cornerRadius(8)
}
.padding()
.background(
RoundedRectangle(cornerRadius: 12)
.fill(CMColors.surface)
.overlay(
RoundedRectangle(cornerRadius: 12)
.stroke(CMColors.accent.opacity(0.3), lineWidth: 1)
)
)
}
}