Restructure into a three-tab app with Progress, goals, and Meditation

The UX redesign's first landing (spec in UX-REDESIGN.md): ContentView
becomes a Today / Progress / Settings TabView, "Routine" replaces
"Split" in every user-facing string and view name (code-level types
keep their names), and workout starting moves to shared
WorkoutStarter / StartedWorkoutNavigator plumbing.

- New Progress tab: weekly goal streaks, workout trends, per-exercise
  weight progression, achievements, and the full history list
  (WorkoutLogsView -> WorkoutHistoryView).
- Goals: stable categories workouts roll up to, managed from Settings.
- New Meditation exercise + starter routine; timed sits record to
  Apple Health as Mind & Body sessions.

Claude-Session: https://claude.ai/code/session_012qw2itfzKyEJ1HpsFt8Ex4
This commit is contained in:
2026-07-11 07:53:01 -04:00
parent 5c201289fb
commit 6e440317c4
97 changed files with 5354 additions and 1291 deletions
@@ -7,10 +7,10 @@
import SwiftUI
/// Developer-facing tool: scans iCloud Drive for splits/workouts that are exact
/// Developer-facing tool: scans iCloud Drive for routines/workouts that are exact
/// content duplicates (see `DuplicateCleanupPlanner`) typically the residue of
/// testing, a restore, or a sync hiccup and offers to delete the extras. Never
/// touches a split referenced by a workout, a bundled starter split, or an
/// touches a routine referenced by a workout, a bundled starter routine, or an
/// in-progress workout.
struct DuplicateCleanupView: View {
@Environment(SyncEngine.self) private var sync
@@ -21,7 +21,7 @@ struct DuplicateCleanupView: View {
}
private struct CleanupSummary {
var splitsDeleted: Int
var routinesDeleted: Int
var workoutsDeleted: Int
var skipped: Int
}
@@ -58,7 +58,7 @@ struct DuplicateCleanupView: View {
pendingPlan = nil
}
} message: { _ in
Text("This permanently deletes the duplicate copies shown below. Referenced splits, starter splits, and in-progress workouts are always kept.")
Text("This permanently deletes the duplicate copies shown below. Referenced routines, starter routines, and in-progress workouts are always kept.")
}
}
@@ -93,7 +93,7 @@ struct DuplicateCleanupView: View {
Label("Scan for Duplicates", systemImage: "magnifyingglass")
}
} footer: {
Text("Looks for splits and workouts that are exact content duplicates — for example, left over from testing or a sync hiccup — and offers to delete the extra copies. Splits referenced by a workout, starter splits, and in-progress workouts are never deleted.")
Text("Looks for routines and workouts that are exact content duplicates — for example, left over from testing or a sync hiccup — and offers to delete the extra copies. Routines referenced by a workout, starter routines, and in-progress workouts are never deleted.")
}
}
}
@@ -110,10 +110,10 @@ struct DuplicateCleanupView: View {
)
} else {
List {
if !plan.splitGroups.isEmpty {
Section("Duplicate Splits") {
ForEach(plan.splitGroups) { group in
splitGroupRow(group)
if !plan.routineGroups.isEmpty {
Section("Duplicate Routines") {
ForEach(plan.routineGroups) { group in
routineGroupRow(group)
}
}
}
@@ -139,7 +139,7 @@ struct DuplicateCleanupView: View {
}
}
private func splitGroupRow(_ group: DuplicateCleanupPlan.SplitGroup) -> some View {
private func routineGroupRow(_ group: DuplicateCleanupPlan.RoutineGroup) -> some View {
VStack(alignment: .leading, spacing: 6) {
ForEach(group.keep) { doc in
memberRow(name: doc.name, detail: "\(doc.exercises.count) exercises", id: doc.id, isKeep: true)
@@ -154,14 +154,14 @@ struct DuplicateCleanupView: View {
private func workoutGroupRow(_ group: DuplicateCleanupPlan.WorkoutGroup) -> some View {
VStack(alignment: .leading, spacing: 6) {
memberRow(
name: group.keep.splitName ?? "(no split)",
name: group.keep.routineName ?? "(no routine)",
detail: group.keep.start.formattedDate(),
id: group.keep.id,
isKeep: true
)
ForEach(group.delete) { doc in
memberRow(
name: doc.splitName ?? "(no split)",
name: doc.routineName ?? "(no routine)",
detail: doc.start.formattedDate(),
id: doc.id,
isKeep: false
@@ -237,7 +237,7 @@ struct DuplicateCleanupView: View {
List {
Section {
Label(
"Deleted \(summary.splitsDeleted) split\(summary.splitsDeleted == 1 ? "" : "s"), \(summary.workoutsDeleted) workout\(summary.workoutsDeleted == 1 ? "" : "s"). \(summary.skipped) skipped (referenced or active).",
"Deleted \(summary.routinesDeleted) routine\(summary.routinesDeleted == 1 ? "" : "s"), \(summary.workoutsDeleted) workout\(summary.workoutsDeleted == 1 ? "" : "s"). \(summary.skipped) skipped (referenced or active).",
systemImage: "checkmark.circle.fill"
)
.foregroundStyle(.green)
@@ -273,7 +273,7 @@ struct DuplicateCleanupView: View {
phase = .deleting
let result = await sync.performCleanup(plan)
phase = .done(CleanupSummary(
splitsDeleted: result.splitsDeleted,
routinesDeleted: result.routinesDeleted,
workoutsDeleted: result.workoutsDeleted,
skipped: result.skipped
))