Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 39 additions & 2 deletions apps/HeartCoach/iOS/Views/DashboardView+BuddyCards.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ extension DashboardView {
}
)
}
.buttonStyle(.plain)
.buttonStyle(CardButtonStyle())
.accessibilityHint("Double tap to view details")
}
}
Expand Down Expand Up @@ -183,6 +183,16 @@ extension DashboardView {
.font(.caption)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)

// Metric impact tag
HStack(spacing: 4) {
Image(systemName: metricImpactIcon(rec.category))
.font(.system(size: 8))
Text(metricImpactLabel(rec.category))
.font(.system(size: 9, weight: .medium))
}
.foregroundStyle(buddyRecColor(rec))
.padding(.top, 2)
}

Spacer()
Expand All @@ -201,7 +211,7 @@ extension DashboardView {
.strokeBorder(buddyRecColor(rec).opacity(0.12), lineWidth: 1)
)
}
.buttonStyle(.plain)
.buttonStyle(CardButtonStyle())
.accessibilityLabel("\(rec.title): \(rec.message)")
.accessibilityHint("Double tap for details")
}
Expand Down Expand Up @@ -240,4 +250,31 @@ extension DashboardView {
case .sunlight: return Color(hex: 0xF59E0B)
}
}

/// Maps a recommendation category to the metric it improves.
func metricImpactLabel(_ category: NudgeCategory) -> String {
switch category {
case .walk: return "Improves VO2 max & recovery"
case .rest: return "Lowers resting heart rate"
case .hydrate: return "Supports HRV & recovery"
case .breathe: return "Reduces stress score"
case .moderate: return "Boosts cardio fitness"
case .celebrate: return "Keep it up!"
case .seekGuidance: return "Protect your heart health"
case .sunlight: return "Improves sleep & circadian rhythm"
}
}

func metricImpactIcon(_ category: NudgeCategory) -> String {
switch category {
case .walk: return "arrow.up.heart.fill"
case .rest: return "heart.fill"
case .hydrate: return "waveform.path.ecg"
case .breathe: return "brain.head.profile"
case .moderate: return "lungs.fill"
case .celebrate: return "star.fill"
case .seekGuidance: return "shield.fill"
case .sunlight: return "moon.zzz.fill"
}
}
}
4 changes: 4 additions & 0 deletions apps/HeartCoach/iOS/Views/DashboardView+Recovery.swift
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,10 @@ extension DashboardView {
.accessibilityElement(children: .combine)
.accessibilityLabel("How you recovered: \(recoveryNarrative(wow: wow))")
.accessibilityIdentifier("dashboard_recovery_card")
.onTapGesture {
InteractionLog.log(.cardTap, element: "recovery_card", page: "Dashboard")
withAnimation { selectedTab = 3 }
}
}
}

Expand Down
87 changes: 87 additions & 0 deletions apps/HeartCoach/iOS/Views/DashboardView+ThumpCheck.swift
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ extension DashboardView {
)
}

// Week-over-week trend indicators
if let trend = viewModel.assessment?.weekOverWeekTrend {
weekOverWeekBanner(trend)
}

// Recovery context banner — shown when readiness is low.
if let ctx = viewModel.assessment?.recoveryContext {
recoveryContextBanner(ctx)
Expand Down Expand Up @@ -331,6 +336,88 @@ extension DashboardView {
.accessibilityLabel("Recovery note: \(ctx.reason). Tonight: \(ctx.tonightAction)")
}

/// Shows week-over-week RHR change and recovery trend as a compact banner.
func weekOverWeekBanner(_ trend: WeekOverWeekTrend) -> some View {
let rhrChange = trend.currentWeekMean - trend.baselineMean
let rhrArrow = rhrChange <= -1 ? "↓" : rhrChange >= 1 ? "↑" : "→"
let rhrColor: Color = rhrChange <= -1
? Color(hex: 0x22C55E)
: rhrChange >= 1 ? Color(hex: 0xEF4444) : .secondary

return VStack(spacing: 6) {
// RHR trend line
HStack(spacing: 6) {
Image(systemName: trend.direction.icon)
.font(.caption2)
.foregroundStyle(rhrColor)
Text("RHR \(Int(trend.baselineMean)) \(rhrArrow) \(Int(trend.currentWeekMean)) bpm")
.font(.caption2)
.fontWeight(.medium)
.foregroundStyle(.primary)
Spacer()
Text(trendLabel(trend.direction))
.font(.system(size: 9))
.foregroundStyle(rhrColor)
}

// Recovery trend line (if available)
if let recovery = viewModel.assessment?.recoveryTrend,
recovery.direction != .insufficientData,
let current = recovery.currentWeekMean,
let baseline = recovery.baselineMean {
let recChange = current - baseline
let recArrow = recChange >= 1 ? "↑" : recChange <= -1 ? "↓" : "→"
let recColor: Color = recChange >= 1
? Color(hex: 0x22C55E)
: recChange <= -1 ? Color(hex: 0xEF4444) : .secondary

HStack(spacing: 6) {
Image(systemName: "arrow.uturn.up")
.font(.caption2)
.foregroundStyle(recColor)
Text("Recovery \(Int(baseline)) \(recArrow) \(Int(current)) bpm drop")
.font(.caption2)
.fontWeight(.medium)
.foregroundStyle(.primary)
Spacer()
Text(recoveryDirectionLabel(recovery.direction))
.font(.system(size: 9))
.foregroundStyle(recColor)
}
}
}
.padding(10)
.background(
RoundedRectangle(cornerRadius: 10)
.fill(Color(.tertiarySystemGroupedBackground))
)
.accessibilityElement(children: .combine)
.accessibilityLabel("RHR trend: \(Int(trend.baselineMean)) to \(Int(trend.currentWeekMean)) bpm, \(trendLabel(trend.direction))")
.onTapGesture {
InteractionLog.log(.cardTap, element: "wow_trend_banner", page: "Dashboard")
withAnimation { selectedTab = 3 }
}
}

func trendLabel(_ direction: WeeklyTrendDirection) -> String {
switch direction {
case .significantImprovement: return "Improving fast"
case .improving: return "Trending down"
case .stable: return "Steady"
case .elevated: return "Creeping up"
case .significantElevation: return "Elevated"
}
}

func recoveryDirectionLabel(_ direction: RecoveryTrendDirection) -> String {
switch direction {
case .improving: return "Getting faster"
case .stable: return "Steady"
case .declining: return "Slowing down"
case .insufficientData: return "Not enough data"
}
}

func readinessColor(for level: ReadinessLevel) -> Color {
switch level {
case .primed: return Color(hex: 0x22C55E)
Expand Down
12 changes: 11 additions & 1 deletion apps/HeartCoach/iOS/Views/DashboardView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -612,11 +612,21 @@ struct DashboardView: View {
isLocked: false
)
}
.buttonStyle(.plain)
.buttonStyle(CardButtonStyle())
.accessibilityHint("Double tap to view trends")
}
}

/// Button style that adds a subtle press effect for card-like buttons.
struct CardButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.opacity(configuration.isPressed ? 0.7 : 1.0)
.scaleEffect(configuration.isPressed ? 0.98 : 1.0)
.animation(.easeInOut(duration: 0.15), value: configuration.isPressed)
}
}

// MARK: - Preview

#Preview("Dashboard - Loaded") {
Expand Down
Loading