iPhone Duo (fase 1): navegación adaptativa por size class y layouts regular×regular

- ContentView: un único TabView (sidebarAdaptable en iOS 18+) para todas las
  size classes; desaparece el cambio de jerarquía iPhone/iPad, así abrir o
  cerrar el Duo conserva el estado de cada pestaña. @SceneStorage de la tab.
- Fuentes y Diario: NavigationSplitView (lista + detalle a la vez en ancho
  regular, stack colapsable en compacto) con selección nativa de List.
- Dashboard: Quick Update como .inspector (panel lateral junto a los gráficos
  en regular, sheet en compacto).
- Gráficos: chartHeightScale (+35% de alto en regular) vía chartFrame(height:),
  sin ignoresSafeArea en contenido interactivo.
- Liquid Glass (glassEffect) en las etiquetas flotantes del Diario en iOS 26+.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F1u4K16xy7eQVtgsYNZ9Vn
This commit is contained in:
alexandrev-tibco
2026-09-16 11:44:50 +02:00
parent a8a9ad64f3
commit 680255f69d
17 changed files with 485 additions and 547 deletions
@@ -57,14 +57,14 @@ struct AllocationPieChart: View {
VStack(alignment: .leading, spacing: 0) {
Text(item.category)
.font(.subheadline)
.foregroundColor(.primary)
.foregroundStyle(.primary)
let percentage = total > 0
? NSDecimalNumber(decimal: item.value / total).doubleValue * 100
: 0
Text(String(format: "%.1f%%", percentage))
.font(.caption)
.foregroundColor(.secondary)
.foregroundStyle(.secondary)
}
}
.opacity(selectedSlice == nil || selectedSlice == item.category ? 1 : 0.5)
@@ -77,7 +77,7 @@ struct AllocationPieChart: View {
} label: {
Image(systemName: "chevron.right.circle.fill")
.font(.footnote)
.foregroundColor(.secondary.opacity(0.6))
.foregroundStyle(.secondary.opacity(0.6))
}
.buttonStyle(.plain)
}
@@ -92,14 +92,14 @@ struct AllocationPieChart: View {
}
} else {
Text("No allocation data available")
.foregroundColor(.secondary)
.frame(height: 200)
.foregroundStyle(.secondary)
.chartFrame(height: 200)
.frame(maxWidth: .infinity)
}
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(AppConstants.UI.cornerRadius)
.clipShape(RoundedRectangle(cornerRadius: AppConstants.UI.cornerRadius))
.shadow(color: .black.opacity(0.05), radius: 8, y: 2)
}
}
@@ -167,7 +167,7 @@ struct SemicircleAllocation: View {
y: center.y + radius * CoreGraphics.sin(angle))
Text("\(Int((frac * 100).rounded()))%")
.font(.system(size: lineWidth * 0.42, weight: .bold))
.foregroundColor(.white)
.foregroundStyle(.white)
.position(pos)
.opacity(selectedSlice == nil || selectedSlice == seg.category ? 1 : 0.4)
}
@@ -178,14 +178,14 @@ struct SemicircleAllocation: View {
VStack(spacing: 1) {
Text(c.label)
.font(.caption2)
.foregroundColor(.secondary)
.foregroundStyle(.secondary)
.lineLimit(1)
Text(c.value)
.font(.headline)
if let sub = c.sub {
Text(sub)
.font(.caption2)
.foregroundColor(.secondary)
.foregroundStyle(.secondary)
}
}
.position(x: center.x, y: center.y - lineWidth * 0.25)
@@ -243,7 +243,7 @@ struct AllocationTargetsComparisonChart: View {
if targetData.allSatisfy({ $0.target == 0 }) {
Text("Set allocation targets to compare your portfolio against your plan.")
.font(.subheadline)
.foregroundColor(.secondary)
.foregroundStyle(.secondary)
} else {
Chart {
ForEach(targetData, id: \.category) { item in
@@ -275,7 +275,7 @@ struct AllocationTargetsComparisonChart: View {
AxisValueLabel()
}
}
.frame(height: 220)
.chartFrame(height: 220)
ForEach(targetData, id: \.category) { item in
let drift = item.actual - item.target
@@ -289,20 +289,20 @@ struct AllocationTargetsComparisonChart: View {
Spacer()
Text("Actual \(String(format: "%.1f%%", item.actual))")
.font(.caption2)
.foregroundColor(.secondary)
.foregroundStyle(.secondary)
Text("Target \(String(format: "%.0f%%", item.target))")
.font(.caption2)
.foregroundColor(.secondary)
.foregroundStyle(.secondary)
Text("\(prefix)\(String(format: "%.1f%%", drift))")
.font(.caption2.weight(.semibold))
.foregroundColor(drift >= 0 ? .positiveGreen : .negativeRed)
.foregroundStyle(drift >= 0 ? Color.positiveGreen : Color.negativeRed)
}
}
}
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(AppConstants.UI.cornerRadius)
.clipShape(RoundedRectangle(cornerRadius: AppConstants.UI.cornerRadius))
.shadow(color: .black.opacity(0.05), radius: 8, y: 2)
}
@@ -351,7 +351,7 @@ struct AllocationListView: View {
Text(item.value.compactCurrencyString)
.font(.caption)
.foregroundColor(.secondary)
.foregroundStyle(.secondary)
.frame(width: 70, alignment: .trailing)
}
@@ -365,12 +365,12 @@ struct AllocationListView: View {
Rectangle()
.fill(Color.gray.opacity(0.1))
.frame(height: 6)
.cornerRadius(3)
.clipShape(RoundedRectangle(cornerRadius: 3))
Rectangle()
.fill(Color(hex: item.color) ?? .gray)
.frame(width: geometry.size.width * percentage, height: 6)
.cornerRadius(3)
.clipShape(RoundedRectangle(cornerRadius: 3))
}
}
.frame(height: 6)
@@ -379,7 +379,7 @@ struct AllocationListView: View {
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(AppConstants.UI.cornerRadius)
.clipShape(RoundedRectangle(cornerRadius: AppConstants.UI.cornerRadius))
.shadow(color: .black.opacity(0.05), radius: 8, y: 2)
}
}
@@ -19,7 +19,7 @@ struct AllocationSimulatorView: View {
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(AppConstants.UI.cornerRadius)
.clipShape(RoundedRectangle(cornerRadius: AppConstants.UI.cornerRadius))
.shadow(color: .black.opacity(0.05), radius: 8, y: 2)
}
@@ -29,13 +29,13 @@ struct AllocationSimulatorView: View {
VStack(spacing: 12) {
Image(systemName: "slider.horizontal.3")
.font(.system(size: 36))
.foregroundColor(.secondary)
.foregroundStyle(.secondary)
Text("No sources available")
.font(.subheadline)
.foregroundColor(.secondary)
.foregroundStyle(.secondary)
}
.frame(maxWidth: .infinity)
.frame(height: 300)
.chartFrame(height: 300)
}
// MARK: - Allocation Card
@@ -56,7 +56,7 @@ struct AllocationSimulatorView: View {
}
.padding(12)
.background(Color(.systemGray6))
.cornerRadius(10)
.clipShape(RoundedRectangle(cornerRadius: 10))
}
private var totalAllocationBadge: some View {
@@ -67,8 +67,8 @@ struct AllocationSimulatorView: View {
.padding(.horizontal, 8)
.padding(.vertical, 3)
.background(isNear100 ? Color.green.opacity(0.15) : Color.orange.opacity(0.15))
.foregroundColor(isNear100 ? .green : .orange)
.cornerRadius(8)
.foregroundStyle(isNear100 ? .green : .orange)
.clipShape(RoundedRectangle(cornerRadius: 8))
}
private var resetButton: some View {
@@ -87,7 +87,7 @@ struct AllocationSimulatorView: View {
} label: {
Image(systemName: "arrow.counterclockwise")
.font(.caption)
.foregroundColor(.secondary)
.foregroundStyle(.secondary)
}
.buttonStyle(.plain)
}
@@ -104,11 +104,11 @@ struct AllocationSimulatorView: View {
Spacer()
Text(String(format: "%.0f%%", source.simulatedPct))
.font(.caption.weight(.semibold))
.foregroundColor(.primary)
.foregroundStyle(.primary)
.frame(width: 36, alignment: .trailing)
Text(String(format: "(%.0f%%)", source.currentPct))
.font(.caption2)
.foregroundColor(.secondary)
.foregroundStyle(.secondary)
}
Slider(
@@ -142,8 +142,8 @@ struct AllocationSimulatorView: View {
if viewModel.simulatorActualData.isEmpty && viewModel.simulatorData.isEmpty {
Text("Not enough data to simulate.")
.font(.subheadline)
.foregroundColor(.secondary)
.frame(height: 220)
.foregroundStyle(.secondary)
.chartFrame(height: 220)
} else {
simulationChart
chartLegend
@@ -194,7 +194,7 @@ struct AllocationSimulatorView: View {
.foregroundStyle(Color.secondary.opacity(0.15))
}
}
.frame(height: 220)
.chartFrame(height: 220)
.chartDrawingGroup(disabledForExport: chartImageExport)
}
@@ -224,7 +224,7 @@ struct AllocationSimulatorView: View {
}
Text(label)
.font(.caption)
.foregroundColor(.secondary)
.foregroundStyle(.secondary)
}
}
@@ -24,16 +24,16 @@ struct ChartStatsRow: View {
VStack(alignment: .center, spacing: 3) {
Text(stats[i].label)
.font(.caption2)
.foregroundColor(.secondary)
.foregroundStyle(.secondary)
Text(stats[i].value)
.font(.subheadline.weight(.semibold))
.foregroundColor(stats[i].color)
.foregroundStyle(stats[i].color)
}
.frame(minWidth: 60)
.padding(.horizontal, 12)
.padding(.vertical, 8)
.background(Color(.systemGray6))
.cornerRadius(8)
.clipShape(RoundedRectangle(cornerRadius: 8))
}
}
}
@@ -84,28 +84,28 @@ struct ChartDataTable: View {
Image(systemName: isExpanded ? "chevron.up" : "chevron.down")
.font(.caption2)
}
.foregroundColor(.appPrimary)
.foregroundStyle(Color.appPrimary)
.frame(maxWidth: .infinity)
.padding(.vertical, 8)
}
}
}
.background(Color(.systemGray6))
.cornerRadius(8)
.clipShape(RoundedRectangle(cornerRadius: 8))
}
private var tableHeader: some View {
HStack(spacing: 0) {
Text("Date").font(.caption2.weight(.semibold)).foregroundColor(.secondary)
Text("Date").font(.caption2.weight(.semibold)).foregroundStyle(.secondary)
.frame(maxWidth: .infinity, alignment: .leading)
Text(valueHeader).font(.caption2.weight(.semibold)).foregroundColor(.secondary)
Text(valueHeader).font(.caption2.weight(.semibold)).foregroundStyle(.secondary)
.frame(width: 72, alignment: .trailing)
if let h = deltaPrevHeader {
Text(h).font(.caption2.weight(.semibold)).foregroundColor(.secondary)
Text(h).font(.caption2.weight(.semibold)).foregroundStyle(.secondary)
.frame(width: 62, alignment: .trailing)
}
if let h = deltaFirstHeader {
Text(h).font(.caption2.weight(.semibold)).foregroundColor(.secondary)
Text(h).font(.caption2.weight(.semibold)).foregroundStyle(.secondary)
.frame(width: 62, alignment: .trailing)
}
}
@@ -115,18 +115,18 @@ struct ChartDataTable: View {
private func tableDataRow(_ row: ChartDataTableRow) -> some View {
HStack(spacing: 0) {
Text(row.label).font(.caption2).foregroundColor(.primary)
Text(row.label).font(.caption2).foregroundStyle(.primary)
.frame(maxWidth: .infinity, alignment: .leading)
Text(row.value).font(.caption2.weight(.medium))
.frame(width: 72, alignment: .trailing)
if let dp = row.deltaPrev, deltaPrevHeader != nil {
Text(dp).font(.caption2.weight(.medium))
.foregroundColor(row.isPrevPositive ? .positiveGreen : .negativeRed)
.foregroundStyle(row.isPrevPositive ? Color.positiveGreen : Color.negativeRed)
.frame(width: 62, alignment: .trailing)
}
if let df = row.deltaFirst, deltaFirstHeader != nil {
Text(df).font(.caption2.weight(.medium))
.foregroundColor(row.isFirstPositive ? .positiveGreen : .negativeRed)
.foregroundStyle(row.isFirstPositive ? Color.positiveGreen : Color.negativeRed)
.frame(width: 62, alignment: .trailing)
}
}
@@ -57,7 +57,7 @@ struct ChartValueBubble: View {
var body: some View {
Text(text)
.font(prominent ? .caption.weight(.bold) : .caption2.weight(.semibold))
.foregroundColor(prominent ? .white : color)
.foregroundStyle(prominent ? .white : color)
.padding(.horizontal, prominent ? 8 : 5)
.padding(.vertical, prominent ? 4 : 2)
.background(
@@ -76,16 +76,16 @@ struct ChartSelectionCard: View {
VStack(alignment: .leading, spacing: 3) {
Text(title)
.font(.caption2.weight(.semibold))
.foregroundColor(.secondary)
.foregroundStyle(.secondary)
ForEach(Array(rows.enumerated()), id: \.offset) { _, row in
HStack(spacing: 5) {
Circle().fill(row.color).frame(width: 6, height: 6)
Text(row.label)
.font(.caption2)
.foregroundColor(.secondary)
.foregroundStyle(.secondary)
Text(row.value)
.font(.caption2.weight(.semibold))
.foregroundColor(.primary)
.foregroundStyle(.primary)
}
}
}
@@ -191,10 +191,10 @@ struct ChartRangeBrush: View {
HStack(spacing: 5) {
Text("\(Self.labelFormatter.string(from: dates[lo])) \(Self.labelFormatter.string(from: dates[hi]))")
.font(.caption.weight(.semibold))
.foregroundColor(.appPrimary)
.foregroundStyle(Color.appPrimary)
Image(systemName: "chevron.up.chevron.down")
.font(.system(size: 8, weight: .semibold))
.foregroundColor(.secondary)
.foregroundStyle(.secondary)
}
}
.buttonStyle(.plain)
@@ -410,7 +410,7 @@ struct ChartRangePickerSheet: View {
VStack(spacing: 2) {
Text(title)
.font(.caption.weight(.semibold))
.foregroundColor(.secondary)
.foregroundStyle(.secondary)
Picker(title, selection: selection) {
ForEach(dates.indices, id: \.self) { i in
Text(Self.optionFormatter.string(from: dates[i])).tag(i)
@@ -1,5 +1,6 @@
import SwiftUI
import Charts
import CoreData
import TipKit
struct ChartsContainerView: View {
@@ -28,10 +29,10 @@ struct ChartsContainerView: View {
// pattern) replaces the 14-chip horizontal carousel.
.toolbarTitleMenu { chartTypePicker }
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) { accountFilterMenu }
ToolbarItem(placement: .navigationBarTrailing) { filterMenu }
ToolbarItem(placement: .topBarTrailing) { accountFilterMenu }
ToolbarItem(placement: .topBarTrailing) { filterMenu }
if horizontalSizeClass != .regular {
ToolbarItem(placement: .navigationBarTrailing) { shareButton }
ToolbarItem(placement: .topBarTrailing) { shareButton }
}
}
.onAppear { syncState() }
@@ -105,11 +106,14 @@ struct ChartsContainerView: View {
}
}
.padding()
// Regular width has a wide canvas next to the sidebar: give
// every plot ~35% more height so the extra area shows data,
// not just a stretched iPhone chart.
.environment(\.chartHeightScale, 1.35)
}
}
.clipped()
}
.ignoresSafeArea(edges: .bottom)
// Attached here (not on the layout-switching Group): sheets presented from
// a view that gets replaced when the size class changes were unreliable in
// NavigationSplitView premium tiles looked like they "did nothing".
@@ -144,7 +148,7 @@ struct ChartsContainerView: View {
if chartType.isPremium && !viewModel.isPremium {
Image(systemName: "lock.fill")
.font(.caption2)
.foregroundColor(.secondary)
.foregroundStyle(.secondary)
}
}
}
@@ -465,19 +469,19 @@ struct ChartsContainerView: View {
VStack(alignment: .leading, spacing: 4) {
Text(label)
.font(.caption)
.foregroundColor(.secondary)
.foregroundStyle(.secondary)
.lineLimit(1)
.minimumScaleFactor(0.8)
Text(value)
.font(.system(.title3, design: .rounded).weight(.semibold))
.foregroundColor(color)
.foregroundStyle(color)
.lineLimit(1)
.minimumScaleFactor(0.6)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(12)
.background(Color(.systemBackground))
.cornerRadius(AppConstants.UI.smallCornerRadius)
.clipShape(RoundedRectangle(cornerRadius: AppConstants.UI.smallCornerRadius))
.shadow(color: .black.opacity(0.05), radius: 4, y: 1)
}
@@ -492,7 +496,7 @@ struct ChartsContainerView: View {
.font(.headline)
Text(viewModel.selectedChartType.description)
.font(.caption)
.foregroundColor(.secondary)
.foregroundStyle(.secondary)
.lineLimit(1)
}
Spacer()
@@ -589,10 +593,10 @@ struct ChartsContainerView: View {
.font(.caption)
}
.font(.footnote.weight(.medium))
.foregroundColor(.appPrimary)
.foregroundStyle(Color.appPrimary)
.padding(12)
.background(Color.appPrimary.opacity(0.08))
.cornerRadius(AppConstants.UI.smallCornerRadius)
.clipShape(RoundedRectangle(cornerRadius: AppConstants.UI.smallCornerRadius))
}
.buttonStyle(.plain)
}
@@ -673,7 +677,7 @@ struct ChartsContainerView: View {
}
.padding(.horizontal, 12)
.padding(.vertical, 7)
.foregroundColor(isSelected ? .white : (locked ? .secondary : .primary))
.foregroundStyle(isSelected ? .white : (locked ? .secondary : .primary))
.background(
Capsule().fill(isSelected ? Color.appPrimary : Color(.systemBackground))
)
@@ -706,7 +710,7 @@ struct ChartsContainerView: View {
Image(systemName: "xmark.circle.fill")
.font(.footnote)
}
.foregroundColor(.appPrimary)
.foregroundStyle(Color.appPrimary)
.padding(.horizontal, 12)
.padding(.vertical, 7)
.background(Capsule().fill(Color.appPrimary.opacity(0.12)))
@@ -777,7 +781,7 @@ struct ChartsContainerView: View {
} label: {
Image(systemName: "calendar.badge.clock")
.font(.system(size: 14, weight: .medium))
.foregroundColor(showRangeBrush ? .white : .appPrimary)
.foregroundStyle(showRangeBrush ? .white : Color.appPrimary)
.frame(width: 38, height: 30)
.background(
RoundedRectangle(cornerRadius: 8)
@@ -899,7 +903,7 @@ struct ChartsContainerView: View {
premiumLockedView
} else if viewModel.isLoading {
ProgressView()
.frame(height: 300)
.chartFrame(height: 300)
} else if !viewModel.hasData {
emptyStateView
} else {
@@ -975,10 +979,10 @@ struct ChartsContainerView: View {
.frame(width: 88, height: 88)
Image(systemName: viewModel.selectedChartType.icon)
.font(.system(size: 34))
.foregroundColor(.appPrimary)
.foregroundStyle(Color.appPrimary)
Image(systemName: "lock.circle.fill")
.font(.system(size: 26))
.foregroundColor(.appWarning)
.foregroundStyle(Color.appWarning)
.background(Circle().fill(Color(.systemBackground)))
.offset(x: 32, y: 30)
}
@@ -988,7 +992,7 @@ struct ChartsContainerView: View {
Text(viewModel.selectedChartType.description)
.font(.subheadline)
.foregroundColor(.secondary)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
Button {
@@ -1000,14 +1004,14 @@ struct ChartsContainerView: View {
.padding(.horizontal, 24)
.padding(.vertical, 12)
.background(Color.appPrimary)
.foregroundColor(.white)
.cornerRadius(24)
.foregroundStyle(.white)
.clipShape(RoundedRectangle(cornerRadius: 24))
}
}
.padding(32)
.frame(maxWidth: .infinity, minHeight: 320)
.background(Color(.systemBackground))
.cornerRadius(AppConstants.UI.cornerRadius)
.clipShape(RoundedRectangle(cornerRadius: AppConstants.UI.cornerRadius))
.shadow(color: .black.opacity(0.05), radius: 8, y: 2)
}
@@ -1015,17 +1019,17 @@ struct ChartsContainerView: View {
VStack(spacing: 16) {
Image(systemName: "chart.bar.xaxis")
.font(.system(size: 48))
.foregroundColor(.secondary)
.foregroundStyle(.secondary)
Text("No Data Available")
.font(.headline)
Text("Add some investment sources and snapshots to see charts.")
.font(.subheadline)
.foregroundColor(.secondary)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
}
.frame(height: 300)
.chartFrame(height: 300)
}
private var accountFilterMenu: some View {
@@ -1167,10 +1171,10 @@ struct EvolutionChartView: View {
HStack(spacing: 6) {
Image(systemName: "lightbulb.fill")
.font(.caption2)
.foregroundColor(.appWarning)
.foregroundStyle(Color.appWarning)
Text(insight)
.font(.caption)
.foregroundColor(.secondary)
.foregroundStyle(.secondary)
}
}
if !data.isEmpty && chartMode == .total {
@@ -1179,7 +1183,7 @@ struct EvolutionChartView: View {
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(AppConstants.UI.cornerRadius)
.clipShape(RoundedRectangle(cornerRadius: AppConstants.UI.cornerRadius))
.shadow(color: .black.opacity(0.05), radius: 8, y: 2)
}
@@ -1228,7 +1232,7 @@ struct EvolutionChartView: View {
}
.padding(.horizontal, 10)
.padding(.vertical, 5)
.foregroundColor(showContributions ? .appSecondary : .secondary)
.foregroundStyle(showContributions ? Color.appSecondary : .secondary)
.background(
Capsule().fill(showContributions ? Color.appSecondary.opacity(0.16) : Color(.systemGray6))
)
@@ -1307,7 +1311,7 @@ struct EvolutionChartView: View {
if series.count > 1 {
Text(String(format: "%+.1f%%", deltaPct))
.font(.caption.weight(.bold))
.foregroundColor(deltaPct >= 0 ? .positiveGreen : .negativeRed)
.foregroundStyle(deltaPct >= 0 ? Color.positiveGreen : Color.negativeRed)
.padding(.horizontal, 7)
.padding(.vertical, 3)
.background(
@@ -1317,7 +1321,7 @@ struct EvolutionChartView: View {
}
Text(subtitle)
.font(.caption)
.foregroundColor(.secondary)
.foregroundStyle(.secondary)
}
Spacer()
@@ -1326,7 +1330,7 @@ struct EvolutionChartView: View {
showGoalLines.toggle()
} label: {
Image(systemName: showGoalLines ? "target" : "slash.circle")
.foregroundColor(.secondary)
.foregroundStyle(.secondary)
}
.disabled(goals.isEmpty)
.accessibilityLabel(showGoalLines ? "Hide goals" : "Show goals")
@@ -1349,8 +1353,8 @@ struct EvolutionChartView: View {
chartView
} else {
Text("Not enough data")
.foregroundColor(.secondary)
.frame(height: 300)
.foregroundStyle(.secondary)
.chartFrame(height: 300)
}
}
@@ -1414,7 +1418,7 @@ struct EvolutionChartView: View {
}
}
}
.frame(height: 300)
.chartFrame(height: 300)
.zoomableTimeSeries(dates: data.map(\.date), zoom: $zoom)
// Performance: GPU rendering for smoother scrolling on older devices
.chartDrawingGroup(disabledForExport: chartImageExport)
@@ -1435,7 +1439,7 @@ struct EvolutionChartView: View {
series: .value("Series", "total")
)
.foregroundStyle(
LinearGradient(colors: [.appPrimary, .cyan],
LinearGradient(colors: [Color.appPrimary, .cyan],
startPoint: .leading, endPoint: .trailing)
)
.lineStyle(StrokeStyle(lineWidth: 2.2, lineCap: .round, lineJoin: .round))
@@ -1571,8 +1575,8 @@ struct ContributionsChartView: View {
if data.isEmpty {
Text("No contributions yet.")
.foregroundColor(.secondary)
.frame(height: 260)
.foregroundStyle(.secondary)
.chartFrame(height: 260)
} else {
Chart {
ForEach(data, id: \.date) { item in
@@ -1581,7 +1585,7 @@ struct ContributionsChartView: View {
y: .value("Amount", NSDecimalNumber(decimal: item.amount).doubleValue)
)
.foregroundStyle(Color.appSecondary)
.cornerRadius(6)
.clipShape(RoundedRectangle(cornerRadius: 6))
}
}
.chartXAxis {
@@ -1599,14 +1603,14 @@ struct ContributionsChartView: View {
}
}
}
.frame(height: 260)
.chartFrame(height: 260)
.zoomableTimeSeries(dates: data.map(\.date), zoom: $zoom)
ChartStatsRow(stats: contributionStats).padding(.top, 4)
}
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(AppConstants.UI.cornerRadius)
.clipShape(RoundedRectangle(cornerRadius: AppConstants.UI.cornerRadius))
.shadow(color: .black.opacity(0.05), radius: 8, y: 2)
}
@@ -1649,8 +1653,8 @@ struct RollingReturnChartView: View {
if data.isEmpty {
Text("Not enough data for rolling returns.")
.foregroundColor(.secondary)
.frame(height: 260)
.foregroundStyle(.secondary)
.chartFrame(height: 260)
} else {
Chart {
ForEach(Array(data.enumerated()), id: \.element.date) { pair in
@@ -1706,7 +1710,7 @@ struct RollingReturnChartView: View {
}
}
}
.frame(height: 260)
.chartFrame(height: 260)
.zoomableTimeSeries(dates: data.map(\.date), zoom: $zoom)
ChartStatsRow(stats: rollingStats).padding(.top, 4)
ChartDataTable(
@@ -1719,7 +1723,7 @@ struct RollingReturnChartView: View {
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(AppConstants.UI.cornerRadius)
.clipShape(RoundedRectangle(cornerRadius: AppConstants.UI.cornerRadius))
.shadow(color: .black.opacity(0.05), radius: 8, y: 2)
}
@@ -1771,8 +1775,8 @@ struct RiskReturnChartView: View {
if data.isEmpty {
Text("Not enough data to compare categories.")
.foregroundColor(.secondary)
.frame(height: 260)
.foregroundStyle(.secondary)
.chartFrame(height: 260)
} else {
Chart {
ForEach(data, id: \.category) { item in
@@ -1785,7 +1789,7 @@ struct RiskReturnChartView: View {
.annotation(position: .top) {
Text(item.category)
.font(.caption2)
.foregroundColor(.secondary)
.foregroundStyle(.secondary)
}
}
}
@@ -1809,12 +1813,12 @@ struct RiskReturnChartView: View {
}
}
}
.frame(height: 260)
.chartFrame(height: 260)
}
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(AppConstants.UI.cornerRadius)
.clipShape(RoundedRectangle(cornerRadius: AppConstants.UI.cornerRadius))
.shadow(color: .black.opacity(0.05), radius: 8, y: 2)
}
}
@@ -1832,8 +1836,8 @@ struct CashflowStackedChartView: View {
if data.isEmpty {
Text("Not enough data to compare cashflow.")
.foregroundColor(.secondary)
.frame(height: 260)
.foregroundStyle(.secondary)
.chartFrame(height: 260)
} else {
Chart {
ForEach(data, id: \.date) { item in
@@ -1871,14 +1875,14 @@ struct CashflowStackedChartView: View {
}
}
}
.frame(height: 260)
.chartFrame(height: 260)
.zoomableTimeSeries(dates: data.map(\.date), zoom: $zoom)
ChartStatsRow(stats: cashflowStats).padding(.top, 4)
}
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(AppConstants.UI.cornerRadius)
.clipShape(RoundedRectangle(cornerRadius: AppConstants.UI.cornerRadius))
.shadow(color: .black.opacity(0.05), radius: 8, y: 2)
}
@@ -1934,14 +1938,14 @@ struct AllocationEvolutionChart: View {
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(AppConstants.UI.cornerRadius)
.clipShape(RoundedRectangle(cornerRadius: AppConstants.UI.cornerRadius))
.shadow(color: .black.opacity(0.05), radius: 8, y: 2)
}
private var emptyView: some View {
Text("Not enough data to show allocation evolution.")
.foregroundColor(.secondary)
.frame(height: 260)
.foregroundStyle(.secondary)
.chartFrame(height: 260)
}
/// Categories in stable order (preserving the ViewModel's sort: largest overall first)
@@ -1989,7 +1993,7 @@ struct AllocationEvolutionChart: View {
}
}
}
.frame(height: 260)
.chartFrame(height: 260)
.chartDrawingGroup(disabledForExport: chartImageExport)
}
}
@@ -32,7 +32,7 @@ struct ComparisonChartView: View {
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(AppConstants.UI.cornerRadius)
.clipShape(RoundedRectangle(cornerRadius: AppConstants.UI.cornerRadius))
.shadow(color: .black.opacity(0.05), radius: 8, y: 2)
}
@@ -66,7 +66,7 @@ struct ComparisonChartView: View {
RoundedRectangle(cornerRadius: 16)
.stroke(isSelected ? chipColor : Color.clear, lineWidth: 1.5)
)
.cornerRadius(16)
.clipShape(RoundedRectangle(cornerRadius: 16))
}
.buttonStyle(.plain)
}
@@ -92,23 +92,23 @@ struct ComparisonChartView: View {
VStack(spacing: 12) {
Image(systemName: "chart.xyaxis.line")
.font(.system(size: 36))
.foregroundColor(.secondary)
.foregroundStyle(.secondary)
Text("Select 2+ sources to compare")
.font(.subheadline)
.foregroundColor(.secondary)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
}
.frame(maxWidth: .infinity)
.frame(height: 260)
.chartFrame(height: 260)
}
private var noDataView: some View {
Text("No data available for selected sources in this period.")
.font(.subheadline)
.foregroundColor(.secondary)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
.frame(maxWidth: .infinity)
.frame(height: 260)
.chartFrame(height: 260)
}
/// Multi-series comparison: labelling every point of every series overlaps
@@ -186,7 +186,7 @@ struct ComparisonChartView: View {
.foregroundStyle(Color.secondary.opacity(0.15))
}
}
.frame(height: 260)
.chartFrame(height: 260)
.chartDrawingGroup(disabledForExport: chartImageExport)
legend
@@ -203,7 +203,7 @@ struct ComparisonChartView: View {
.frame(width: 20, height: 3)
Text(series.name)
.font(.caption)
.foregroundColor(.secondary)
.foregroundStyle(.secondary)
}
}
}
@@ -38,16 +38,16 @@ struct DrawdownChart: View {
VStack(alignment: .trailing, spacing: 2) {
Text("Max Drawdown")
.font(.caption)
.foregroundColor(.secondary)
.foregroundStyle(.secondary)
Text(String(format: "%.1f%%", maxDrawdown))
.font(.subheadline.weight(.semibold))
.foregroundColor(.negativeRed)
.foregroundStyle(Color.negativeRed)
}
}
Text("Shows percentage decline from peak values")
.font(.caption)
.foregroundColor(.secondary)
.foregroundStyle(.secondary)
if data.count >= 2 {
Chart {
@@ -119,7 +119,7 @@ struct DrawdownChart: View {
}
}
.chartYScale(domain: (data.map { $0.drawdown }.min() ?? -50)...0)
.frame(height: 250)
.chartFrame(height: 250)
.zoomableTimeSeries(dates: data.map(\.date), zoom: $zoom)
// Statistics
@@ -152,14 +152,14 @@ struct DrawdownChart: View {
.padding(.top, 4)
} else {
Text("Not enough data for drawdown analysis")
.foregroundColor(.secondary)
.frame(height: 250)
.foregroundStyle(.secondary)
.chartFrame(height: 250)
.frame(maxWidth: .infinity)
}
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(AppConstants.UI.cornerRadius)
.clipShape(RoundedRectangle(cornerRadius: AppConstants.UI.cornerRadius))
.shadow(color: .black.opacity(0.05), radius: 8, y: 2)
}
@@ -197,11 +197,11 @@ struct DrawdownStatView: View {
VStack(spacing: 4) {
Text(title)
.font(.caption)
.foregroundColor(.secondary)
.foregroundStyle(.secondary)
Text(value)
.font(.subheadline.weight(.semibold))
.foregroundColor(isHighlighted ? .negativeRed : .primary)
.foregroundStyle(isHighlighted ? Color.negativeRed : .primary)
}
.frame(maxWidth: .infinity)
}
@@ -247,7 +247,7 @@ struct VolatilityChartView: View {
Text("Measures price variability over time")
.font(.caption)
.foregroundColor(.secondary)
.foregroundStyle(.secondary)
if data.count >= 2 {
Chart {
@@ -293,7 +293,7 @@ struct VolatilityChartView: View {
.annotation(position: .top, alignment: .leading) {
Text("Avg: \(String(format: "%.1f%%", averageVolatility))")
.font(.caption2)
.foregroundColor(.secondary)
.foregroundStyle(.secondary)
}
if let sel = selectedPoint {
@@ -328,7 +328,7 @@ struct VolatilityChartView: View {
}
}
}
.frame(height: 250)
.chartFrame(height: 250)
.zoomableTimeSeries(dates: data.map(\.date), zoom: $zoom)
ChartStatsRow(stats: [
@@ -346,14 +346,14 @@ struct VolatilityChartView: View {
)
} else {
Text("Not enough data for volatility analysis")
.foregroundColor(.secondary)
.frame(height: 250)
.foregroundStyle(.secondary)
.chartFrame(height: 250)
.frame(maxWidth: .infinity)
}
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(AppConstants.UI.cornerRadius)
.clipShape(RoundedRectangle(cornerRadius: AppConstants.UI.cornerRadius))
.shadow(color: .black.opacity(0.05), radius: 8, y: 2)
}
@@ -400,7 +400,7 @@ struct VolatilityLevelView: View {
.font(.caption2)
Text(range)
.font(.caption2)
.foregroundColor(.secondary)
.foregroundStyle(.secondary)
}
}
}
@@ -22,7 +22,7 @@ struct PerformanceBarChart: View {
Text("Compound Annual Growth Rate (CAGR)")
.font(.caption)
.foregroundColor(.secondary)
.foregroundStyle(.secondary)
if !data.isEmpty {
ChartStatsRow(stats: perfStats)
@@ -44,14 +44,14 @@ struct PerformanceBarChart: View {
.padding(.top, 8)
} else {
Text("No performance data available")
.foregroundColor(.secondary)
.frame(height: 250)
.foregroundStyle(.secondary)
.chartFrame(height: 250)
.frame(maxWidth: .infinity)
}
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(AppConstants.UI.cornerRadius)
.clipShape(RoundedRectangle(cornerRadius: AppConstants.UI.cornerRadius))
.shadow(color: .black.opacity(0.05), radius: 8, y: 2)
}
@@ -62,11 +62,11 @@ struct PerformanceBarChart: View {
y: .value("CAGR", item.cagr)
)
.foregroundStyle(Color(hex: item.color) ?? .gray)
.cornerRadius(4)
.clipShape(RoundedRectangle(cornerRadius: 4))
.annotation(position: item.cagr >= 0 ? .top : .bottom) {
Text(String(format: "%.1f%%", item.cagr))
.font(.caption2)
.foregroundColor(item.cagr >= 0 ? .positiveGreen : .negativeRed)
.foregroundStyle(item.cagr >= 0 ? Color.positiveGreen : Color.negativeRed)
}
}
.chartXAxis {
@@ -91,7 +91,7 @@ struct PerformanceBarChart: View {
}
}
}
.frame(height: 250)
.chartFrame(height: 250)
}
private var horizontalChart: some View {
@@ -101,11 +101,11 @@ struct PerformanceBarChart: View {
y: .value("Category", item.category)
)
.foregroundStyle(Color(hex: item.color) ?? .gray)
.cornerRadius(4)
.clipShape(RoundedRectangle(cornerRadius: 4))
.annotation(position: item.cagr >= 0 ? .trailing : .leading) {
Text(String(format: "%.1f%%", item.cagr))
.font(.caption2)
.foregroundColor(item.cagr >= 0 ? .positiveGreen : .negativeRed)
.foregroundStyle(item.cagr >= 0 ? Color.positiveGreen : Color.negativeRed)
}
}
.chartXAxis {
@@ -147,12 +147,12 @@ struct PerformanceBarChart: View {
Text(String(format: "%.2f%%", item.cagr))
.font(.subheadline.weight(.semibold))
.foregroundColor(item.cagr >= 0 ? .positiveGreen : .negativeRed)
.foregroundStyle(item.cagr >= 0 ? Color.positiveGreen : Color.negativeRed)
if onDrillDown != nil {
Image(systemName: "chevron.right")
.font(.caption2.weight(.semibold))
.foregroundColor(.secondary)
.foregroundStyle(.secondary)
}
}
if let onDrillDown {
@@ -213,7 +213,7 @@ struct HorizontalPerformanceChart: View {
Text(String(format: "%.2f%%", item.cagr))
.font(.subheadline.weight(.semibold))
.foregroundColor(item.cagr >= 0 ? .positiveGreen : .negativeRed)
.foregroundStyle(item.cagr >= 0 ? Color.positiveGreen : Color.negativeRed)
}
GeometryReader { geometry in
@@ -224,12 +224,12 @@ struct HorizontalPerformanceChart: View {
Rectangle()
.fill(Color.gray.opacity(0.1))
.frame(height: 8)
.cornerRadius(4)
.clipShape(RoundedRectangle(cornerRadius: 4))
Rectangle()
.fill(item.cagr >= 0 ? Color.positiveGreen : Color.negativeRed)
.frame(width: barWidth, height: 8)
.cornerRadius(4)
.clipShape(RoundedRectangle(cornerRadius: 4))
}
}
.frame(height: 8)
@@ -238,7 +238,7 @@ struct HorizontalPerformanceChart: View {
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(AppConstants.UI.cornerRadius)
.clipShape(RoundedRectangle(cornerRadius: AppConstants.UI.cornerRadius))
.shadow(color: .black.opacity(0.05), radius: 8, y: 2)
}
}
@@ -28,7 +28,7 @@ struct PeriodComparisonChartView: View {
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(AppConstants.UI.cornerRadius)
.clipShape(RoundedRectangle(cornerRadius: AppConstants.UI.cornerRadius))
.shadow(color: .black.opacity(0.05), radius: 8, y: 2)
}
@@ -49,7 +49,7 @@ struct PeriodComparisonChartView: View {
endPoint: .bottomTrailing
)
)
.cornerRadius(12)
.clipShape(RoundedRectangle(cornerRadius: 12))
.overlay(
RoundedRectangle(cornerRadius: 12)
.stroke(Color.secondary.opacity(0.1), lineWidth: 1)
@@ -64,7 +64,7 @@ struct PeriodComparisonChartView: View {
Text(label)
.font(.caption.weight(.bold))
.foregroundColor(color)
.foregroundStyle(color)
.frame(width: 60, alignment: .leading)
DatePicker(
@@ -81,7 +81,7 @@ struct PeriodComparisonChartView: View {
Text("")
.font(.caption)
.foregroundColor(.secondary)
.foregroundStyle(.secondary)
DatePicker(
"",
@@ -132,28 +132,28 @@ struct PeriodComparisonChartView: View {
.frame(width: 6, height: 6)
Text(series.label)
.font(.caption2.weight(.medium))
.foregroundColor(.secondary)
.foregroundStyle(.secondary)
.lineLimit(1)
}
Text(String(format: "%+.2f%%", finalReturn))
.font(.title2.weight(.bold))
.foregroundColor(finalReturn >= 0 ? .positiveGreen : .negativeRed)
.foregroundStyle(finalReturn >= 0 ? Color.positiveGreen : Color.negativeRed)
HStack(spacing: 10) {
HStack(spacing: 3) {
Image(systemName: "arrow.up")
.font(.caption2.weight(.semibold))
.foregroundColor(.positiveGreen)
.foregroundStyle(Color.positiveGreen)
Text(String(format: "%.1f%%", maxReturn))
.font(.caption2)
.foregroundColor(.positiveGreen)
.foregroundStyle(Color.positiveGreen)
}
HStack(spacing: 3) {
Image(systemName: "arrow.down")
.font(.caption2.weight(.semibold))
.foregroundColor(.negativeRed)
.foregroundStyle(Color.negativeRed)
Text(String(format: "%.1f%%", minReturn))
.font(.caption2)
.foregroundColor(.negativeRed)
.foregroundStyle(Color.negativeRed)
}
}
}
@@ -175,14 +175,14 @@ struct PeriodComparisonChartView: View {
VStack(spacing: 12) {
Image(systemName: "calendar.badge.clock")
.font(.system(size: 36))
.foregroundColor(.secondary)
.foregroundStyle(.secondary)
Text("No data available for the selected periods.")
.font(.subheadline)
.foregroundColor(.secondary)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
}
.frame(maxWidth: .infinity)
.frame(height: 260)
.chartFrame(height: 260)
}
// MARK: - Flat data for Chart
@@ -303,7 +303,7 @@ struct PeriodComparisonChartView: View {
.foregroundStyle(Color.secondary.opacity(0.15))
}
}
.frame(height: 260)
.chartFrame(height: 260)
.chartDrawingGroup(disabledForExport: chartImageExport)
.onChange(of: seriesIds) { _, _ in }
}
@@ -317,7 +317,7 @@ struct PeriodComparisonChartView: View {
.frame(width: 20, height: 3)
Text(series.label)
.font(.caption)
.foregroundColor(.secondary)
.foregroundStyle(.secondary)
}
}
Spacer()
@@ -36,10 +36,10 @@ struct PredictionChartView: View {
VStack(alignment: .trailing, spacing: 2) {
Text("Forecast")
.font(.caption)
.foregroundColor(.secondary)
.foregroundStyle(.secondary)
Text(lastPrediction.formattedValue)
.font(.subheadline.weight(.semibold))
.foregroundColor(.appPrimary)
.foregroundStyle(Color.appPrimary)
}
}
}
@@ -47,7 +47,7 @@ struct PredictionChartView: View {
if let algorithm = predictions.first?.algorithm {
Text("Algorithm: \(algorithm.displayName)")
.font(.caption)
.foregroundColor(.secondary)
.foregroundStyle(.secondary)
}
if !predictions.isEmpty && historicalData.count >= 2 {
@@ -185,7 +185,7 @@ struct PredictionChartView: View {
}
}
}
.frame(height: 280)
.chartFrame(height: 280)
// Stats row
if let currentValue = historicalData.last?.value,
@@ -206,7 +206,7 @@ struct PredictionChartView: View {
.frame(width: 20, height: 3)
Text("Historical")
.font(.caption)
.foregroundColor(.secondary)
.foregroundStyle(.secondary)
}
HStack(spacing: 6) {
@@ -223,7 +223,7 @@ struct PredictionChartView: View {
)
Text("Prediction")
.font(.caption)
.foregroundColor(.secondary)
.foregroundStyle(.secondary)
}
HStack(spacing: 6) {
@@ -232,7 +232,7 @@ struct PredictionChartView: View {
.frame(width: 20, height: 10)
Text("Confidence")
.font(.caption)
.foregroundColor(.secondary)
.foregroundStyle(.secondary)
}
HStack(spacing: 6) {
@@ -241,7 +241,7 @@ struct PredictionChartView: View {
.frame(width: 20, height: 2)
Text("Bull")
.font(.caption)
.foregroundColor(.secondary)
.foregroundStyle(.secondary)
}
HStack(spacing: 6) {
@@ -250,7 +250,7 @@ struct PredictionChartView: View {
.frame(width: 20, height: 2)
Text("Bear")
.font(.caption)
.foregroundColor(.secondary)
.foregroundStyle(.secondary)
}
}
@@ -274,7 +274,7 @@ struct PredictionChartView: View {
Spacer()
Text(lastPrediction.formattedConfidenceRange)
.font(.caption)
.foregroundColor(.secondary)
.foregroundStyle(.secondary)
}
if let currentValue = historicalData.last?.value {
@@ -293,7 +293,7 @@ struct PredictionChartView: View {
Text(String(format: "%+.1f%%", changePercent))
.font(.subheadline.weight(.medium))
}
.foregroundColor(change >= 0 ? .positiveGreen : .negativeRed)
.foregroundStyle(change >= 0 ? Color.positiveGreen : Color.negativeRed)
}
}
}
@@ -310,24 +310,24 @@ struct PredictionChartView: View {
VStack(spacing: 12) {
Image(systemName: "wand.and.stars")
.font(.system(size: 40))
.foregroundColor(.secondary)
.foregroundStyle(.secondary)
Text("Not enough data for predictions")
.font(.subheadline)
.foregroundColor(.secondary)
.foregroundStyle(.secondary)
Text("Add at least 3 snapshots to generate predictions")
.font(.caption)
.foregroundColor(.secondary)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
}
.frame(height: 280)
.chartFrame(height: 280)
.frame(maxWidth: .infinity)
}
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(AppConstants.UI.cornerRadius)
.clipShape(RoundedRectangle(cornerRadius: AppConstants.UI.cornerRadius))
.shadow(color: .black.opacity(0.05), radius: 8, y: 2)
}
@@ -30,9 +30,9 @@ struct YearOverYearChartView: View {
if allYears.isEmpty {
Text(String(localized: "chart_yoy_empty"))
.font(.subheadline)
.foregroundColor(.secondary)
.foregroundStyle(.secondary)
.frame(maxWidth: .infinity, alignment: .center)
.frame(height: 200)
.chartFrame(height: 200)
} else {
yearSelector
if !selectedSeries.isEmpty {
@@ -44,7 +44,7 @@ struct YearOverYearChartView: View {
if hasEstimate {
Text(String(localized: "chart_yoy_estimated_note"))
.font(.caption2)
.foregroundColor(.secondary)
.foregroundStyle(.secondary)
}
// Detalle debajo (#147)
chartBody
@@ -57,7 +57,7 @@ struct YearOverYearChartView: View {
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(AppConstants.UI.cornerRadius)
.clipShape(RoundedRectangle(cornerRadius: AppConstants.UI.cornerRadius))
.shadow(color: .black.opacity(0.05), radius: 8, y: 2)
}
@@ -82,12 +82,12 @@ struct YearOverYearChartView: View {
.padding(.horizontal, 12)
.padding(.vertical, 6)
.background(isSelected ? color.opacity(0.15) : Color.gray.opacity(0.1))
.foregroundColor(isSelected ? color : .secondary)
.foregroundStyle(isSelected ? color : .secondary)
.overlay(
RoundedRectangle(cornerRadius: 16)
.stroke(isSelected ? color : Color.clear, lineWidth: 1.5)
)
.cornerRadius(16)
.clipShape(RoundedRectangle(cornerRadius: 16))
}
.buttonStyle(.plain)
}
@@ -181,11 +181,11 @@ struct YearOverYearChartView: View {
AxisGridLine()
}
}
.frame(height: 220)
.chartFrame(height: 220)
} else {
Text("iOS 16+ required for chart")
.foregroundColor(.secondary)
.frame(height: 220)
.foregroundStyle(.secondary)
.chartFrame(height: 220)
}
}
@@ -202,7 +202,7 @@ struct YearOverYearChartView: View {
.frame(width: 20, height: 3)
Text(String(yearSeries.year))
.font(.caption)
.foregroundColor(.secondary)
.foregroundStyle(.secondary)
}
}
}
@@ -221,16 +221,16 @@ struct YearOverYearChartView: View {
VStack(alignment: .center, spacing: 3) {
Text(String(yearSeries.year))
.font(.caption2)
.foregroundColor(.secondary)
.foregroundStyle(.secondary)
Text(String(format: "%+.1f%%", end.value) + (end.estimated ? "*" : ""))
.font(.subheadline.weight(.semibold))
.foregroundColor(end.value >= 0 ? color : .negativeRed)
.foregroundStyle(end.value >= 0 ? color : Color.negativeRed)
}
.frame(minWidth: 60)
.padding(.horizontal, 12)
.padding(.vertical, 8)
.background(Color(.systemGray6))
.cornerRadius(8)
.clipShape(RoundedRectangle(cornerRadius: 8))
}
}
}