Charts iPhone workable: chip bar, ventana+brush, header vivo, drill-down, overlay #29 #30

Fix look&feel (#29): chip bar selector visible (sustituye page dots + title menu
como navegación primaria), sin bubbles permanentes en compact (solo endpoint),
Performance a barras horizontales con >6 categorías, línea con gradiente y
estética de widget, pinch anclado al centro + doble-tap reset + haptics.

Workable (#30): modelo de ventana (inicio,fin) único para pinch/pan/brush;
minimapa bajo demanda con asas snap-a-mes y ventana arrastrable; label de rango
con pickers mes/año precisos; header vivo (valor+delta del rango, scrub con
haptic); drill-down por tap en Allocation/Performance → Evolution filtrada
(premium, chip Filtrado ✕ para volver); overlay aportaciones acumuladas
(premium); insight contextual del rango visible (premium). L10n ×7.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L38583J7AYWCVPevkivscj
This commit is contained in:
alexandrev-tibco
2026-08-06 18:03:50 +02:00
parent 525911f67a
commit d38c6943e1
14 changed files with 1031 additions and 222 deletions
@@ -5,6 +5,9 @@ struct AllocationPieChart: View {
let data: [(category: String, value: Decimal, color: String)]
var title: String = "Asset Allocation"
var showsTargetsComparison: Bool = true
/// Drill-down: tapping the chevron on a legend row jumps to Evolution
/// filtered to that category/source.
var onDrillDown: ((String) -> Void)? = nil
@State private var selectedSlice: String?
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
@@ -38,34 +41,47 @@ struct AllocationPieChart: View {
// Legend
VStack(alignment: .leading, spacing: 10) {
ForEach(data, id: \.category) { item in
Button {
if selectedSlice == item.category {
selectedSlice = nil
} else {
selectedSlice = item.category
}
} label: {
HStack(spacing: 8) {
Circle()
.fill(Color(hex: item.color) ?? .gray)
.frame(width: 11, height: 11)
VStack(alignment: .leading, spacing: 0) {
Text(item.category)
.font(.subheadline)
.foregroundColor(.primary)
let percentage = total > 0
? NSDecimalNumber(decimal: item.value / total).doubleValue * 100
: 0
Text(String(format: "%.1f%%", percentage))
.font(.caption)
.foregroundColor(.secondary)
HStack(spacing: 6) {
Button {
if selectedSlice == item.category {
selectedSlice = nil
} else {
selectedSlice = item.category
}
} label: {
HStack(spacing: 8) {
Circle()
.fill(Color(hex: item.color) ?? .gray)
.frame(width: 11, height: 11)
VStack(alignment: .leading, spacing: 0) {
Text(item.category)
.font(.subheadline)
.foregroundColor(.primary)
let percentage = total > 0
? NSDecimalNumber(decimal: item.value / total).doubleValue * 100
: 0
Text(String(format: "%.1f%%", percentage))
.font(.caption)
.foregroundColor(.secondary)
}
}
.opacity(selectedSlice == nil || selectedSlice == item.category ? 1 : 0.5)
}
.buttonStyle(.plain)
if let onDrillDown {
Button {
onDrillDown(item.category)
} label: {
Image(systemName: "chevron.right.circle.fill")
.font(.footnote)
.foregroundColor(.secondary.opacity(0.6))
}
.buttonStyle(.plain)
}
.opacity(selectedSlice == nil || selectedSlice == item.category ? 1 : 0.5)
}
.buttonStyle(.plain)
}
}
.frame(maxWidth: .infinity, alignment: .leading)