feat: replace workout progress bar with dot-grid indicator

7 rows of dots fill left-to-right, top-to-bottom as sets complete.
Filled dots: AppColors.green solid. Empty: green at 12% opacity.
Also: strip emoji from completion banner, tighten copy.
This commit is contained in:
kutesir
2026-05-28 18:55:09 +03:00
parent e06f15c585
commit 05a579e2f7

View File

@@ -445,8 +445,8 @@ struct WorkoutSessionSheet: View {
} }
.padding(.horizontal, 20).padding(.top, 20).padding(.bottom, 14) .padding(.horizontal, 20).padding(.top, 20).padding(.bottom, 14)
// Progress Bar // Dot Grid Progress
VStack(spacing: 6) { VStack(spacing: 8) {
HStack { HStack {
Text("PROGRESS") Text("PROGRESS")
.font(AppFonts.mono(8.5, weight: .bold)) .font(AppFonts.mono(8.5, weight: .bold))
@@ -454,37 +454,24 @@ struct WorkoutSessionSheet: View {
Spacer() Spacer()
Text(vm.totalSets > 0 ? "\(Int(vm.progress * 100))%" : "0%") Text(vm.totalSets > 0 ? "\(Int(vm.progress * 100))%" : "0%")
.font(AppFonts.mono(10, weight: .bold)) .font(AppFonts.mono(10, weight: .bold))
.foregroundColor(vm.progress >= 1 ? AppColors.green : AppColors.accent) .foregroundColor(vm.progress >= 1 ? AppColors.green : AppColors.text2(cs))
} }
GeometryReader { geo in DotGridProgress(progress: vm.totalSets > 0 ? vm.progress : 0, cs: cs)
ZStack(alignment: .leading) {
RoundedRectangle(cornerRadius: 5)
.fill(AppColors.borderHi(cs))
.frame(height: 8)
if vm.totalSets > 0 {
RoundedRectangle(cornerRadius: 5)
.fill(vm.progress >= 1 ? AppColors.green : AppColors.accent)
.frame(width: geo.size.width * CGFloat(min(vm.progress, 1)), height: 8)
.animation(KisaniSpring.entrance, value: vm.progress)
}
}
}
.frame(height: 8)
} }
.padding(.horizontal, 20).padding(.bottom, 14) .padding(.horizontal, 20).padding(.bottom, 14)
// Completion Banner // Completion Banner
if vm.progress >= 1 && vm.totalSets > 0 { if vm.progress >= 1 && vm.totalSets > 0 {
HStack(spacing: 8) { HStack(spacing: 8) {
Image(systemName: "checkmark.seal.fill") Image(systemName: "checkmark.circle.fill")
.font(.system(size: 15)).foregroundColor(AppColors.green) .font(.system(size: 14)).foregroundColor(AppColors.green)
Text("Workout complete! Great work 🎉") Text("All sets done")
.font(AppFonts.sans(12.5, weight: .semibold)) .font(AppFonts.mono(11, weight: .bold))
.foregroundColor(AppColors.green) .foregroundColor(AppColors.green)
Spacer() Spacer()
} }
.padding(.horizontal, 20).padding(.vertical, 11) .padding(.horizontal, 20).padding(.vertical, 10)
.background(AppColors.green.opacity(0.09)) .background(AppColors.green.opacity(0.08))
} }
Divider().background(AppColors.border(cs)) Divider().background(AppColors.border(cs))
@@ -1088,3 +1075,51 @@ struct AddTaskSheet: View {
dismiss() dismiss()
} }
} }
// MARK: - Dot Grid Progress
struct DotGridProgress: View {
let progress: Double
let cs: ColorScheme
private let dotSize: CGFloat = 7.5
private let gap: CGFloat = 2.5
private let rows = 7
var body: some View {
GeometryReader { geo in
let cols = max(1, Int((geo.size.width + gap) / (dotSize + gap)))
let total = cols * rows
let filled = Int(Double(total) * min(max(progress, 0), 1))
VStack(alignment: .leading, spacing: gap) {
ForEach(0..<rows, id: \.self) { row in
DotRow(cols: cols, rowStart: row * cols, filled: filled,
dotSize: dotSize, gap: gap, cs: cs)
}
}
.animation(KisaniSpring.entrance, value: filled)
}
.frame(height: CGFloat(rows) * dotSize + CGFloat(rows - 1) * gap)
}
}
private struct DotRow: View {
let cols: Int
let rowStart: Int
let filled: Int
let dotSize: CGFloat
let gap: CGFloat
let cs: ColorScheme
var body: some View {
HStack(spacing: gap) {
ForEach(0..<cols, id: \.self) { col in
let isFilled = (rowStart + col) < filled
Circle()
.fill(isFilled ? AppColors.green : AppColors.green.opacity(0.12))
.frame(width: dotSize, height: dotSize)
}
}
}
}