Redesign the workout exercise row layout
Weight moves up beside the exercise name (matching font), sets × reps sits below it with a dimmed ×, and recorded machine settings show as a secondary line under the name — no placeholder when unconfigured. Single-line bodyweight rows center against the checkbox. Claude-Session: https://claude.ai/code/session_01P152LxjZ4vePHsSorQa5Jf
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
**July 2026**
|
||||
|
||||
Exercise rows in a workout have a cleaner layout — the weight sits beside the exercise name, with sets × reps and any recorded machine settings beneath.
|
||||
|
||||
The Splits and Exercises entries in Settings now show how many each contains.
|
||||
|
||||
The Abdominal figure's arms no longer bend backward at the elbow, and a few other form guides lose their subtly hyperextended joints.
|
||||
|
||||
The exercise library more than doubles — deadlifts, squats, lunges, bench presses, dumbbell work, pull-ups, and the cable stations — each with its own animated form guide.
|
||||
|
||||
@@ -372,10 +372,10 @@ struct WorkoutLogListView: View {
|
||||
|
||||
// MARK: - Workout Log Row
|
||||
|
||||
/// One exercise line item: the status checkbox, the exercise name with a
|
||||
/// prominent sets × reps line under it, and a trailing column for the load.
|
||||
/// Weight only appears for weighted exercises — a bodyweight exercise
|
||||
/// (`LoadType.none`) shows no load at all.
|
||||
/// One exercise line item: the status checkbox, the exercise name with the
|
||||
/// machine settings line under it, and a trailing column with the load on top
|
||||
/// and sets × reps below. Weight only appears for weighted exercises — a
|
||||
/// bodyweight exercise (`LoadType.none`) shows no load at all.
|
||||
private struct WorkoutLogRow: View {
|
||||
let status: CheckboxStatus
|
||||
let log: WorkoutLogDocument
|
||||
@@ -385,21 +385,25 @@ private struct WorkoutLogRow: View {
|
||||
|
||||
private var loadType: LoadType { LoadType(rawValue: log.loadType) ?? .none }
|
||||
|
||||
/// Prominent "3 × 12" (or "3 × 5 min" for timed exercises).
|
||||
private var setsAndReps: String {
|
||||
/// "3 × 12" (or "3 × 5 min" for timed exercises) with the numbers in full
|
||||
/// strength and the × dimmed, so the counts read at a glance.
|
||||
private var setsAndReps: Text {
|
||||
let times = Text(" × ").foregroundStyle(.secondary)
|
||||
let count: String
|
||||
if loadType == .duration {
|
||||
let mins = log.durationSeconds / 60
|
||||
let secs = log.durationSeconds % 60
|
||||
if mins > 0 && secs > 0 {
|
||||
return "\(log.sets) × \(mins)m \(secs)s"
|
||||
count = "\(mins)m \(secs)s"
|
||||
} else if mins > 0 {
|
||||
return "\(log.sets) × \(mins) min"
|
||||
count = "\(mins) min"
|
||||
} else {
|
||||
return "\(log.sets) × \(secs) sec"
|
||||
count = "\(secs) sec"
|
||||
}
|
||||
} else {
|
||||
return "\(log.sets) × \(log.reps)"
|
||||
count = "\(log.reps)"
|
||||
}
|
||||
return Text("\(log.sets)") + times + Text(count)
|
||||
}
|
||||
|
||||
private var showsWeight: Bool { loadType == .weight }
|
||||
@@ -415,8 +419,16 @@ private struct WorkoutLogRow: View {
|
||||
return parts.isEmpty ? nil : parts.joined(separator: " · ")
|
||||
}
|
||||
|
||||
/// Top-align when either column stacks two lines (weight over sets × reps,
|
||||
/// or name over settings), so the first lines pair up. A single-line row —
|
||||
/// a bodyweight exercise with no settings — centers against the checkbox
|
||||
/// instead of hanging off its top edge.
|
||||
private var rowAlignment: VerticalAlignment {
|
||||
showsWeight || settingsSummary != nil ? .top : .center
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .top) {
|
||||
HStack(alignment: rowAlignment) {
|
||||
Button {
|
||||
onCheckboxTap()
|
||||
} label: {
|
||||
@@ -428,44 +440,44 @@ private struct WorkoutLogRow: View {
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(log.exerciseName)
|
||||
.font(.headline)
|
||||
.foregroundColor(.primary)
|
||||
Text(setsAndReps)
|
||||
.font(.title3.weight(.semibold))
|
||||
.monospacedDigit()
|
||||
.foregroundStyle(.primary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
VStack(alignment: .trailing, spacing: 4) {
|
||||
if showsWeight {
|
||||
Text(weightUnit.format(log.weight))
|
||||
.font(.title3.weight(.semibold))
|
||||
.monospacedDigit()
|
||||
.foregroundStyle(.primary)
|
||||
}
|
||||
|
||||
// Machine comfort settings — shown when the log carries settings, or
|
||||
// for any exercise the authored library identifies as machine-based
|
||||
// (same rule as ExerciseView), so older logs offer the affordance
|
||||
// before settings are first recorded. Tapping opens the editor sheet;
|
||||
// `.plain` keeps the tap from firing the row's NavigationLink (same
|
||||
// trick as the checkbox button).
|
||||
if log.machineSettings != nil
|
||||
|| ExerciseInfoLibrary.info(for: log.exerciseName)?.isMachineBased == true {
|
||||
// Machine comfort settings — the line appears only once settings are
|
||||
// actually recorded (an unconfigured machine shows nothing; recording
|
||||
// the first values happens in the exercise's edit screen or progress
|
||||
// flow). Tapping opens the editor sheet; `.plain` keeps the tap from
|
||||
// firing the row's NavigationLink (same trick as the checkbox).
|
||||
if let settingsSummary {
|
||||
Button(action: onSettingsTap) {
|
||||
Label(settingsSummary ?? "Settings", systemImage: "slider.horizontal.3")
|
||||
.font(.footnote.weight(.medium))
|
||||
.foregroundStyle(Color.accentColor)
|
||||
.multilineTextAlignment(.trailing)
|
||||
Label(settingsSummary, systemImage: "slider.horizontal.3")
|
||||
.font(.subheadline.weight(.medium))
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(2)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
// The weight matches the exercise name's font so the two sit on the
|
||||
// same visual line; sets × reps keeps its larger glanceable size below.
|
||||
VStack(alignment: .trailing, spacing: 4) {
|
||||
if showsWeight {
|
||||
Text(weightUnit.format(log.weight))
|
||||
.font(.headline)
|
||||
.monospacedDigit()
|
||||
.foregroundStyle(.primary)
|
||||
}
|
||||
|
||||
setsAndReps
|
||||
.font(.title3)
|
||||
.monospacedDigit()
|
||||
.foregroundStyle(.primary)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user