Feedback iPad: allocation grande + %, Home 2 columnas, YoY KPIs, achievements fix, iCloud calmado (build 58)

Allocation:
- Semicírculo responsive que llena su mitad de la fila (grande en iPad, no
  minúsculo en una esquina) + porcentaje dentro de cada segmento >=7%.
  SemicircleArc acepta center explícito; labels en el ángulo medio del arco.

Home iPad:
- Ya no columna única estrecha a 720. Hero + check-in a ancho completo, resto
  de cards en DOS columnas masonry independientes (sin acoplar alturas → sin
  gaps del grid). Cap 1100pt.

Year vs Year KPIs:
- Retorno del año actual + diferencia (pp) vs año anterior + mejor/peor año
  histórico. Usa forecastEndValue para el año en curso.

Fixes:
- 'View all achievements' en Journal no hacía nada: NavigationLink inline
  (frágil según contexto de navegación) → sheet fiable en cualquier sitio.
- iCloud: CKErrorPartialFailure tras una sync que funciona ya NO se pinta en
  rojo alarmante — nota calmada 'Sincronizando con iCloud' + detalle técnico
  en disclosure. Rojo solo si nunca ha sincronizado y es fallo total.
  syncErrorIsCritical + strings ×7.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WoScpmHdVj1aUf4rAp6hbe
This commit is contained in:
alexandrev-tibco
2026-07-10 14:48:06 +02:00
parent 18f2609d87
commit 1abffdb7bb
13 changed files with 221 additions and 48 deletions
@@ -171,18 +171,49 @@ struct DashboardView: View {
// MARK: - iPad Layout Helpers
/// Full-width "hero" zones that lead the dashboard, in order.
private var iPadLeadSections: [DashboardSectionConfig] {
let leadIds = [DashboardSection.totalValue.id, DashboardSection.monthlyCheckIn.id]
return leadIds.compactMap { id in visibleSections.first { $0.id == id } }
}
/// Remaining context cards, split into two balanced masonry columns so the
/// dashboard fills the iPad width without the row-height coupling (and gaps)
/// a LazyVGrid produced.
private var iPadContextColumns: ([DashboardSectionConfig], [DashboardSectionConfig]) {
let leadIds = Set([DashboardSection.totalValue.id, DashboardSection.monthlyCheckIn.id])
let rest = visibleSections.filter { !leadIds.contains($0.id) }
var left: [DashboardSectionConfig] = []
var right: [DashboardSectionConfig] = []
for (i, cfg) in rest.enumerated() {
if i.isMultiple(of: 2) { left.append(cfg) } else { right.append(cfg) }
}
return (left, right)
}
@ViewBuilder
private var iPadDashboardLayout: some View {
let columns = iPadContextColumns
VStack(spacing: 16) {
ForEach(visibleSections) { config in
ForEach(iPadLeadSections) { config in
sectionView(for: config)
if config.id == DashboardSection.monthlyCheckIn.id,
!viewModel.insights.isEmpty {
if config.id == DashboardSection.monthlyCheckIn.id, !viewModel.insights.isEmpty {
InsightsRow(insights: viewModel.insights)
}
}
HStack(alignment: .top, spacing: 16) {
VStack(spacing: 16) {
ForEach(columns.0) { sectionView(for: $0) }
}
.frame(maxWidth: .infinity)
VStack(spacing: 16) {
ForEach(columns.1) { sectionView(for: $0) }
}
.frame(maxWidth: .infinity)
}
}
.frame(maxWidth: 720)
.frame(maxWidth: 1100)
.frame(maxWidth: .infinity)
}
@@ -776,6 +807,7 @@ struct MomentumStreaksCard: View {
/// and achievement detail don't fit stack tiles in a grid and drop detail.
var compact = false
@State private var stats: MonthlyCheckInStats = .empty
@State private var showingAchievements = false
var body: some View {
VStack(alignment: .leading, spacing: 12) {
@@ -865,8 +897,11 @@ struct MomentumStreaksCard: View {
}
}
NavigationLink {
AchievementsView(referenceDate: Date())
// Sheet instead of NavigationLink: this card lives in several
// navigation contexts (dashboard column, Journal sidebar) where an
// inline push was unreliable the tap did nothing on iPad.
Button {
showingAchievements = true
} label: {
HStack {
Text(String(localized: "achievements_view_all"))
@@ -877,6 +912,7 @@ struct MomentumStreaksCard: View {
.foregroundColor(.secondary)
}
.padding(.vertical, 8)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
}
@@ -888,6 +924,16 @@ struct MomentumStreaksCard: View {
.onReceive(NotificationCenter.default.publisher(for: .NSManagedObjectContextObjectsDidChange)) { _ in
refreshStats()
}
.sheet(isPresented: $showingAchievements) {
NavigationStack {
AchievementsView(referenceDate: Date())
.toolbar {
ToolbarItem(placement: .confirmationAction) {
Button(String(localized: "cancel")) { showingAchievements = false }
}
}
}
}
}
private var onTimeRateText: String {