Merge develop -> main: full session (KC-51 through KC-71) #28
@@ -10,6 +10,25 @@ struct PressButtonStyle: ButtonStyle {
|
||||
}
|
||||
}
|
||||
|
||||
/// A quiet accent button: outlined/tinted at rest, fills solid only while
|
||||
/// pressed. Color is a response to touch, not a permanent decoration —
|
||||
/// matches "nothing shouts, color earns its place."
|
||||
struct QuietAccentButtonStyle: ButtonStyle {
|
||||
@Environment(\.colorScheme) private var cs
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
configuration.label
|
||||
.foregroundColor(configuration.isPressed ? .white : AppColors.accent)
|
||||
.background(configuration.isPressed ? AppColors.accent : AppColors.accentSoft)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 11))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 11)
|
||||
.stroke(AppColors.accent.opacity(configuration.isPressed ? 0 : 0.35), lineWidth: 1)
|
||||
)
|
||||
.scaleEffect(configuration.isPressed ? 0.97 : 1.0)
|
||||
.animation(KisaniSpring.micro, value: configuration.isPressed)
|
||||
}
|
||||
}
|
||||
|
||||
struct FabButtonStyle: ButtonStyle {
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
configuration.label
|
||||
|
||||
@@ -1705,3 +1705,20 @@ the main page) into a 3-state completion card:
|
||||
action); kept "Clear All Sets" there as a distinct reset utility.
|
||||
|
||||
Files: `WorkoutView.swift`, `ExerciseModels.swift`.
|
||||
|
||||
### KC-52 — follow-up: restraint, confirmation, clear (user feedback)
|
||||
User pushed back on the first pass: the primary button was a permanently-filled
|
||||
accent pill ("giving AI," violates PRODUCT.md "nothing shouts / color earns its
|
||||
place"), "Finish Anyway" skipped confirming the % logged before overriding
|
||||
progress, and there was no quick way to undo an accidental "Check all sets."
|
||||
|
||||
- New `QuietAccentButtonStyle` (SharedComponents.swift): outlined/tinted at
|
||||
rest, fills solid accent only while pressed — color as a response to touch,
|
||||
not a static decoration. Applied to the Finish button.
|
||||
- `confirmationDialog` before finishing with partial sets: "Finish with X of Y
|
||||
sets logged?" + the logged % in the message. Skipped when already 100% (no
|
||||
need to ask about something already true).
|
||||
- "Check all sets" now flips to "Clear all sets" once every set is checked —
|
||||
the undo is the button itself, always one tap away, no separate menu needed.
|
||||
|
||||
Files: `WorkoutView.swift`, `Components/SharedComponents.swift`.
|
||||
|
||||
@@ -144,6 +144,7 @@ struct WorkoutView: View {
|
||||
isMissedToday: vm.isWorkoutMissed(on: Date()),
|
||||
onFinish: { withAnimation(KisaniSpring.snappy) { vm.markWorkoutDone(on: Date()) } },
|
||||
onMarkAllSets: { withAnimation(KisaniSpring.snappy) { vm.setAllSetsDone(true) } },
|
||||
onClearAllSets: { withAnimation(KisaniSpring.snappy) { vm.setAllSetsDone(false) } },
|
||||
onNotCompleted: { withAnimation(KisaniSpring.snappy) { vm.markWorkoutMissed(on: Date()) } },
|
||||
onUndo: {
|
||||
withAnimation(KisaniSpring.snappy) {
|
||||
@@ -588,9 +589,12 @@ struct DotProgressCard: View {
|
||||
/// a workout happened (Health/Watch already knows that independently).
|
||||
let onFinish: () -> Void
|
||||
let onMarkAllSets: () -> Void
|
||||
let onClearAllSets: () -> Void
|
||||
let onNotCompleted: () -> Void
|
||||
let onUndo: () -> Void
|
||||
|
||||
@State private var showFinishConfirm = false
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 12) {
|
||||
HStack {
|
||||
@@ -620,6 +624,18 @@ struct DotProgressCard: View {
|
||||
}
|
||||
.padding(14)
|
||||
.cardStyle(borderColor: isDoneToday ? AppColors.green.opacity(0.3) : nil)
|
||||
.confirmationDialog(
|
||||
"Finish with \(done) of \(total) sets logged?",
|
||||
isPresented: $showFinishConfirm,
|
||||
titleVisibility: .visible
|
||||
) {
|
||||
Button("Finish Anyway") { onFinish() }
|
||||
Button("Cancel", role: .cancel) {}
|
||||
} message: {
|
||||
Text(total > 0
|
||||
? "You've logged \(Int(progress * 100))% of sets. This still counts as today's workout."
|
||||
: "No sets logged yet. This still counts as today's workout.")
|
||||
}
|
||||
}
|
||||
|
||||
private var statusTitle: String {
|
||||
@@ -651,22 +667,34 @@ struct DotProgressCard: View {
|
||||
}
|
||||
|
||||
/// Primary: finish the workout as-is (honest capture, not gated on 100% sets).
|
||||
/// Secondary: the mechanical "tick every box" action, and the explicit miss.
|
||||
/// Secondary: the mechanical "tick every box" action (toggles to a clear once
|
||||
/// all are checked, so a mis-tap is always one tap to undo), and the miss.
|
||||
private var actionRow: some View {
|
||||
VStack(spacing: 8) {
|
||||
Button(action: onFinish) {
|
||||
Button {
|
||||
if done == total && total > 0 { onFinish() } else { showFinishConfirm = true }
|
||||
} label: {
|
||||
Label(done == total && total > 0 ? "Finish Workout" : "Finish Anyway",
|
||||
systemImage: "checkmark.seal.fill")
|
||||
.font(AppFonts.sans(13, weight: .semibold))
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 11)
|
||||
}
|
||||
.foregroundColor(.white)
|
||||
.background(AppColors.accent)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 11))
|
||||
.buttonStyle(PressButtonStyle(scale: 0.97))
|
||||
.buttonStyle(QuietAccentButtonStyle())
|
||||
|
||||
HStack(spacing: 8) {
|
||||
if done == total && total > 0 {
|
||||
Button(action: onClearAllSets) {
|
||||
Label("Clear all sets", systemImage: "circle")
|
||||
.font(AppFonts.sans(11, weight: .medium))
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
.foregroundColor(AppColors.text2(cs))
|
||||
.background(AppColors.surface2(cs))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 9))
|
||||
.buttonStyle(PressButtonStyle(scale: 0.97))
|
||||
} else {
|
||||
Button(action: onMarkAllSets) {
|
||||
Label("Check all sets", systemImage: "checkmark.circle")
|
||||
.font(AppFonts.sans(11, weight: .medium))
|
||||
@@ -677,6 +705,7 @@ struct DotProgressCard: View {
|
||||
.background(AppColors.surface2(cs))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 9))
|
||||
.buttonStyle(PressButtonStyle(scale: 0.97))
|
||||
}
|
||||
|
||||
Button(action: onNotCompleted) {
|
||||
Label("Didn't train", systemImage: "moon.zzz")
|
||||
|
||||
Reference in New Issue
Block a user