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:
@@ -445,8 +445,8 @@ struct WorkoutSessionSheet: View {
|
||||
}
|
||||
.padding(.horizontal, 20).padding(.top, 20).padding(.bottom, 14)
|
||||
|
||||
// ── Progress Bar ──
|
||||
VStack(spacing: 6) {
|
||||
// ── Dot Grid Progress ──
|
||||
VStack(spacing: 8) {
|
||||
HStack {
|
||||
Text("PROGRESS")
|
||||
.font(AppFonts.mono(8.5, weight: .bold))
|
||||
@@ -454,37 +454,24 @@ struct WorkoutSessionSheet: View {
|
||||
Spacer()
|
||||
Text(vm.totalSets > 0 ? "\(Int(vm.progress * 100))%" : "0%")
|
||||
.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
|
||||
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)
|
||||
DotGridProgress(progress: vm.totalSets > 0 ? vm.progress : 0, cs: cs)
|
||||
}
|
||||
.padding(.horizontal, 20).padding(.bottom, 14)
|
||||
|
||||
// ── Completion Banner ──
|
||||
if vm.progress >= 1 && vm.totalSets > 0 {
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: "checkmark.seal.fill")
|
||||
.font(.system(size: 15)).foregroundColor(AppColors.green)
|
||||
Text("Workout complete! Great work 🎉")
|
||||
.font(AppFonts.sans(12.5, weight: .semibold))
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.font(.system(size: 14)).foregroundColor(AppColors.green)
|
||||
Text("All sets done")
|
||||
.font(AppFonts.mono(11, weight: .bold))
|
||||
.foregroundColor(AppColors.green)
|
||||
Spacer()
|
||||
}
|
||||
.padding(.horizontal, 20).padding(.vertical, 11)
|
||||
.background(AppColors.green.opacity(0.09))
|
||||
.padding(.horizontal, 20).padding(.vertical, 10)
|
||||
.background(AppColors.green.opacity(0.08))
|
||||
}
|
||||
|
||||
Divider().background(AppColors.border(cs))
|
||||
@@ -1088,3 +1075,51 @@ struct AddTaskSheet: View {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user