Open new workouts directly and improve accessibility

Starting a workout — from the split picker or a split's exercise list —
now drops straight into its log screen once the cache catches up, via a
shared StartedWorkoutNavigator. Adds VoiceOver labels/values to the log
checkboxes and the settings button, a color-independent numbered legend
and spoken summary to the heart-rate-zone bar, and Dynamic Type scaling
to the run-flow badges and timer.

Claude-Session: https://claude.ai/code/session_01HJDQQDA9QdP8zByg43H5v3
This commit is contained in:
2026-07-08 07:59:30 -04:00
parent 1b399ee7ba
commit e04eb83e70
7 changed files with 158 additions and 43 deletions
@@ -621,6 +621,10 @@ private struct PhaseTimerLayout<Content: View>: View {
let tint: Color
@ViewBuilder var timer: Content
/// Scales the big timer digits with Dynamic Type (falls back to `.minimumScaleFactor`
/// so the largest sizes never overflow the top half).
@ScaledMetric(relativeTo: .largeTitle) private var timerFontSize: CGFloat = 108
var body: some View {
VStack(spacing: 10) {
Text(header)
@@ -628,7 +632,7 @@ private struct PhaseTimerLayout<Content: View>: View {
.foregroundStyle(.secondary)
timer
.font(.system(size: 108, weight: .bold, design: .rounded))
.font(.system(size: timerFontSize, weight: .bold, design: .rounded))
.monospacedDigit()
.foregroundStyle(tint)
.lineLimit(1)
@@ -715,6 +719,9 @@ private extension View {
private struct CompletedPhaseView: View {
let log: WorkoutLogDocument?
/// Scales the completion badge with Dynamic Type.
@ScaledMetric(relativeTo: .largeTitle) private var badgeSize: CGFloat = 96
/// "4 sets × 12 reps" / "3 sets × 45 sec".
private var planSummary: String {
guard let log else { return "" }
@@ -736,7 +743,7 @@ private struct CompletedPhaseView: View {
var body: some View {
VStack(spacing: 14) {
Image(systemName: "checkmark.circle.fill")
.font(.system(size: 96, weight: .bold))
.font(.system(size: badgeSize, weight: .bold))
.foregroundStyle(Color.accentColor)
Text("Completed")
.font(.system(.title, design: .rounded, weight: .heavy))
@@ -766,10 +773,13 @@ private struct CompletedPhaseView: View {
/// ended early with this one unfinished). Same badge treatment as Completed,
/// in gray, matching the list row's skipped icon.
private struct SkippedPhaseView: View {
/// Scales the skipped badge with Dynamic Type.
@ScaledMetric(relativeTo: .largeTitle) private var badgeSize: CGFloat = 96
var body: some View {
VStack(spacing: 14) {
Image(systemName: "wrongwaysign")
.font(.system(size: 96, weight: .bold))
.font(.system(size: badgeSize, weight: .bold))
.foregroundStyle(.gray)
Text("Skipped")
.font(.system(.title, design: .rounded, weight: .heavy))
@@ -785,10 +795,15 @@ private struct ReadyPhaseView: View {
let summary: String
let onStart: () -> Void
/// Scales the "Ready?" headline with Dynamic Type.
@ScaledMetric(relativeTo: .largeTitle) private var readyFontSize: CGFloat = 44
var body: some View {
VStack(spacing: 14) {
Text("Ready?")
.font(.system(size: 44, weight: .bold, design: .rounded))
.font(.system(size: readyFontSize, weight: .bold, design: .rounded))
.lineLimit(1)
.minimumScaleFactor(0.5)
if !summary.isEmpty {
Text(summary)