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:
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - Chart height scaling
|
||||
//
|
||||
// Charts declare a base height for compact width. Containers with a wider
|
||||
// canvas (regular width: iPad, iPhone Duo inner screen, Mac windows) raise the
|
||||
// scale so plots gain vertical room instead of just stretching horizontally.
|
||||
|
||||
extension EnvironmentValues {
|
||||
@Entry var chartHeightScale: CGFloat = 1
|
||||
}
|
||||
|
||||
private struct ChartFrameModifier: ViewModifier {
|
||||
@Environment(\.chartHeightScale) private var scale
|
||||
let base: CGFloat
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content.frame(height: base * scale)
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
/// Fixed chart height that follows `chartHeightScale` from the environment.
|
||||
func chartFrame(height: CGFloat) -> some View {
|
||||
modifier(ChartFrameModifier(base: height))
|
||||
}
|
||||
|
||||
/// Floating label background: Liquid Glass on iOS 26+, material fallback.
|
||||
@ViewBuilder
|
||||
func floatingPillBackground() -> some View {
|
||||
if #available(iOS 26.0, *) {
|
||||
self.glassEffect(.regular, in: .capsule)
|
||||
} else {
|
||||
self
|
||||
.background(.regularMaterial, in: Capsule())
|
||||
.shadow(color: .black.opacity(0.12), radius: 6, y: 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -77,7 +77,7 @@ struct DashboardView: View {
|
||||
}
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
Button {
|
||||
QuickUpdateTip().invalidate(reason: .actionPerformed)
|
||||
showingQuickUpdate = true
|
||||
@@ -86,7 +86,7 @@ struct DashboardView: View {
|
||||
}
|
||||
.popoverTip(QuickUpdateTip())
|
||||
}
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
Button {
|
||||
balancePrivacy.toggleHidden()
|
||||
} label: {
|
||||
@@ -94,10 +94,10 @@ struct DashboardView: View {
|
||||
}
|
||||
.accessibilityLabel(balancePrivacy.balancesHidden ? Text("Show balances") : Text("Hide balances"))
|
||||
}
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
accountFilterMenu
|
||||
}
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
ToolbarItem(placement: .topBarLeading) {
|
||||
Button {
|
||||
showingCustomize = true
|
||||
} label: {
|
||||
@@ -143,10 +143,6 @@ struct DashboardView: View {
|
||||
.sheet(isPresented: $showingCustomize) {
|
||||
DashboardCustomizeView(configs: $sectionConfigs)
|
||||
}
|
||||
.sheet(isPresented: $showingQuickUpdate) {
|
||||
QuickUpdateView()
|
||||
.environment(\.managedObjectContext, CoreDataStack.shared.viewContext)
|
||||
}
|
||||
.sheet(isPresented: $viewModel.showingPaywall) {
|
||||
PaywallView()
|
||||
}
|
||||
@@ -175,6 +171,18 @@ struct DashboardView: View {
|
||||
if !viewModel.hasData { showingAddSource = true }
|
||||
}
|
||||
}
|
||||
// Quick Update lives in an inspector: a trailing panel in regular width
|
||||
// (iPad, iPhone Duo inner screen) so the dashboard charts stay visible
|
||||
// while values are typed; the system presents it as a sheet in compact.
|
||||
.inspector(isPresented: $showingQuickUpdate) {
|
||||
QuickUpdateView()
|
||||
.environment(\.managedObjectContext, CoreDataStack.shared.viewContext)
|
||||
.environmentObject(iapService)
|
||||
.environmentObject(accountStore)
|
||||
.environmentObject(tabSelection)
|
||||
.environmentObject(balancePrivacy)
|
||||
.inspectorColumnWidth(min: 340, ideal: 400, max: 480)
|
||||
}
|
||||
}
|
||||
|
||||
private var accountFilterMenu: some View {
|
||||
@@ -478,20 +486,20 @@ struct TotalValueCard: View {
|
||||
HStack {
|
||||
Text("Total Portfolio Value")
|
||||
.font(.subheadline.weight(.medium))
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
Spacer()
|
||||
if let onShareTap {
|
||||
Button(action: onShareTap) {
|
||||
Image(systemName: "square.and.arrow.up")
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text(totalValue)
|
||||
.font(.system(size: 44, weight: .bold, design: .rounded))
|
||||
.foregroundColor(.primary)
|
||||
.foregroundStyle(.primary)
|
||||
.minimumScaleFactor(0.6)
|
||||
.lineLimit(1)
|
||||
.hiddenBalance(balancesHidden)
|
||||
@@ -503,7 +511,7 @@ struct TotalValueCard: View {
|
||||
Text(changeText)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
}
|
||||
.foregroundColor(isPositive ? .positiveGreen : .negativeRed)
|
||||
.foregroundStyle(isPositive ? Color.positiveGreen : Color.negativeRed)
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.vertical, 5)
|
||||
.background((isPositive ? Color.positiveGreen : Color.negativeRed).opacity(0.12))
|
||||
@@ -511,7 +519,7 @@ struct TotalValueCard: View {
|
||||
|
||||
Text(changeLabel)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
if sparklineData.count >= 2 {
|
||||
@@ -544,10 +552,10 @@ struct TotalValueCard: View {
|
||||
Image(systemName: "lock.fill").font(.caption2)
|
||||
Text("12m forecast").font(.caption2)
|
||||
}
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
Text("Unlock")
|
||||
.font(.caption.weight(.semibold))
|
||||
.foregroundColor(.appPrimary)
|
||||
.foregroundStyle(Color.appPrimary)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
@@ -559,17 +567,17 @@ struct TotalValueCard: View {
|
||||
.padding(20)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(Color(.secondarySystemGroupedBackground))
|
||||
.cornerRadius(AppConstants.UI.cornerRadius)
|
||||
.clipShape(RoundedRectangle(cornerRadius: AppConstants.UI.cornerRadius))
|
||||
}
|
||||
|
||||
private func heroMetric(label: String, value: String, positive: Bool) -> some View {
|
||||
VStack(spacing: 2) {
|
||||
Text(label)
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
Text(value)
|
||||
.font(.caption.weight(.semibold))
|
||||
.foregroundColor(positive ? .positiveGreen : .negativeRed)
|
||||
.foregroundStyle(positive ? Color.positiveGreen : Color.negativeRed)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
@@ -760,15 +768,10 @@ struct MonthlyCheckInCard: View {
|
||||
}
|
||||
.padding(isMonthComplete ? 14 : 20)
|
||||
.background(Color(.secondarySystemGroupedBackground))
|
||||
.cornerRadius(AppConstants.UI.cornerRadius)
|
||||
.background(
|
||||
NavigationLink(isActive: $startDestinationActive) {
|
||||
MonthlyCheckInView(referenceDate: navigationReferenceDate)
|
||||
} label: {
|
||||
EmptyView()
|
||||
}
|
||||
.opacity(0)
|
||||
)
|
||||
.clipShape(RoundedRectangle(cornerRadius: AppConstants.UI.cornerRadius))
|
||||
.navigationDestination(isPresented: $startDestinationActive) {
|
||||
MonthlyCheckInView(referenceDate: navigationReferenceDate)
|
||||
}
|
||||
// Calm 2.0 Fase 2: the ritual runs as a guided full-screen flow — one
|
||||
// source per step, reflection, closing summary.
|
||||
.fullScreenCover(isPresented: $showingGuidedFlow) {
|
||||
@@ -782,14 +785,14 @@ struct MonthlyCheckInCard: View {
|
||||
HStack(spacing: 10) {
|
||||
Image(systemName: "checkmark.seal.fill")
|
||||
.font(.title3)
|
||||
.foregroundColor(.positiveGreen)
|
||||
.foregroundStyle(Color.positiveGreen)
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(String(format: String(localized: "checkin_done_title"), currentMonthLabel))
|
||||
.font(.subheadline.weight(.semibold))
|
||||
if let nextDate = nextCheckInDate {
|
||||
Text(String(format: String(localized: "checkin_done_next"), nextDate.mediumDateString))
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
@@ -800,7 +803,7 @@ struct MonthlyCheckInCard: View {
|
||||
Text("\(streak)")
|
||||
.font(.caption.weight(.bold))
|
||||
}
|
||||
.foregroundColor(.orange)
|
||||
.foregroundStyle(.orange)
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.vertical, 5)
|
||||
.background(Color.orange.opacity(0.12))
|
||||
@@ -810,7 +813,7 @@ struct MonthlyCheckInCard: View {
|
||||
Button(action: onShareSummary) {
|
||||
Image(systemName: "square.and.arrow.up")
|
||||
.font(.caption.weight(.semibold))
|
||||
.foregroundColor(.appPrimary)
|
||||
.foregroundStyle(Color.appPrimary)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel(Text("Share monthly summary"))
|
||||
@@ -820,7 +823,7 @@ struct MonthlyCheckInCard: View {
|
||||
} label: {
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.caption.weight(.semibold))
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
@@ -836,7 +839,7 @@ struct MonthlyCheckInCard: View {
|
||||
.font(.headline)
|
||||
Text(currentMonthLabel)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
Spacer()
|
||||
if streak >= 2 {
|
||||
@@ -844,7 +847,7 @@ struct MonthlyCheckInCard: View {
|
||||
Image(systemName: "flame.fill").font(.caption)
|
||||
Text("\(streak)").font(.caption.weight(.bold))
|
||||
}
|
||||
.foregroundColor(.orange)
|
||||
.foregroundStyle(.orange)
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.vertical, 5)
|
||||
.background(Color.orange.opacity(0.12))
|
||||
@@ -867,7 +870,7 @@ struct MonthlyCheckInCard: View {
|
||||
)
|
||||
} label: {
|
||||
Image(systemName: "calendar.badge.plus")
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.font(.subheadline.weight(.semibold))
|
||||
}
|
||||
@@ -879,7 +882,7 @@ struct MonthlyCheckInCard: View {
|
||||
.tint(progressBarTint)
|
||||
Text(String(format: String(localized: "checkin_sources_progress"), updatedCount, totalSources))
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
} else {
|
||||
ProgressView(value: checkInProgress)
|
||||
@@ -890,15 +893,15 @@ struct MonthlyCheckInCard: View {
|
||||
HStack(spacing: 6) {
|
||||
Image(systemName: "calendar")
|
||||
.font(.caption)
|
||||
.foregroundColor(isOverdue ? .red : .secondary)
|
||||
.foregroundStyle(isOverdue ? .red : .secondary)
|
||||
Text(String(format: String(localized: "checkin_next_due"), nextDate.mediumDateString))
|
||||
.font(.caption)
|
||||
.foregroundColor(isOverdue ? .red : .secondary)
|
||||
.foregroundStyle(isOverdue ? .red : .secondary)
|
||||
Spacer()
|
||||
if !deadlineBadgeText.isEmpty {
|
||||
Text(deadlineBadgeText)
|
||||
.font(.caption2.weight(.semibold))
|
||||
.foregroundColor(progressBarTint)
|
||||
.foregroundStyle(progressBarTint)
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 3)
|
||||
.background(progressBarTint.opacity(0.12))
|
||||
@@ -908,7 +911,7 @@ struct MonthlyCheckInCard: View {
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.vertical, 8)
|
||||
.background(Color(.tertiarySystemFill))
|
||||
.cornerRadius(10)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||
}
|
||||
|
||||
Button {
|
||||
@@ -919,8 +922,8 @@ struct MonthlyCheckInCard: View {
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 12)
|
||||
.background(Color.appPrimary)
|
||||
.foregroundColor(.white)
|
||||
.cornerRadius(AppConstants.UI.cornerRadius)
|
||||
.foregroundStyle(.white)
|
||||
.clipShape(RoundedRectangle(cornerRadius: AppConstants.UI.cornerRadius))
|
||||
}
|
||||
.accessibilityIdentifier("checkin_start_cta")
|
||||
}
|
||||
@@ -962,11 +965,11 @@ struct MomentumStreaksCard: View {
|
||||
if stats.totalCheckIns > 0 {
|
||||
Text(String(format: NSLocalizedString("on_time_rate", comment: ""), onTimeRateText))
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.foregroundColor(.appSecondary)
|
||||
.foregroundStyle(Color.appSecondary)
|
||||
} else {
|
||||
Text("Log a check-in to start a streak")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1002,7 +1005,7 @@ struct MomentumStreaksCard: View {
|
||||
HStack {
|
||||
Text("On-time score")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
Spacer()
|
||||
Text(
|
||||
String(
|
||||
@@ -1012,7 +1015,7 @@ struct MomentumStreaksCard: View {
|
||||
)
|
||||
)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
if let closest = stats.closestCutoffDays {
|
||||
@@ -1023,7 +1026,7 @@ struct MomentumStreaksCard: View {
|
||||
)
|
||||
)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
if !stats.achievements.isEmpty && !compact {
|
||||
@@ -1053,7 +1056,7 @@ struct MomentumStreaksCard: View {
|
||||
Spacer()
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.padding(.vertical, 8)
|
||||
.contentShape(Rectangle())
|
||||
@@ -1062,7 +1065,7 @@ struct MomentumStreaksCard: 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)
|
||||
.onAppear(perform: refreshStats)
|
||||
.onReceive(NotificationCenter.default.publisher(for: .NSManagedObjectContextObjectsDidChange)) { _ in
|
||||
@@ -1097,14 +1100,14 @@ struct MomentumStreaksCard: View {
|
||||
.lineLimit(1)
|
||||
Text(title)
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(1)
|
||||
.minimumScaleFactor(0.8)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 10)
|
||||
.background(Color.gray.opacity(0.08))
|
||||
.cornerRadius(AppConstants.UI.smallCornerRadius)
|
||||
.clipShape(RoundedRectangle(cornerRadius: AppConstants.UI.smallCornerRadius))
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
@@ -1112,17 +1115,17 @@ struct MomentumStreaksCard: View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(title)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
Text(value)
|
||||
.font(.title3.weight(.semibold))
|
||||
Text(subtitle)
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(Color.gray.opacity(0.08))
|
||||
.cornerRadius(AppConstants.UI.smallCornerRadius)
|
||||
.clipShape(RoundedRectangle(cornerRadius: AppConstants.UI.smallCornerRadius))
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
@@ -1130,18 +1133,18 @@ struct MomentumStreaksCard: View {
|
||||
HStack(alignment: .top, spacing: 8) {
|
||||
Image(systemName: achievement.icon)
|
||||
.font(.headline)
|
||||
.foregroundColor(.appSecondary)
|
||||
.foregroundStyle(Color.appSecondary)
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(achievement.title)
|
||||
.font(.caption.weight(.semibold))
|
||||
Text(achievement.detail)
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
.padding(10)
|
||||
.background(Color.appSecondary.opacity(0.12))
|
||||
.cornerRadius(AppConstants.UI.smallCornerRadius)
|
||||
.clipShape(RoundedRectangle(cornerRadius: AppConstants.UI.smallCornerRadius))
|
||||
}
|
||||
|
||||
private func formattedDaysText(for days: Double?) -> String {
|
||||
@@ -1191,7 +1194,7 @@ struct MonthlySummaryCard: View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("Contributions")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
Text(summary.formattedContributions)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.hiddenBalance()
|
||||
@@ -1202,17 +1205,17 @@ struct MonthlySummaryCard: View {
|
||||
VStack(alignment: .trailing, spacing: 4) {
|
||||
Text("Net Performance")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
Text("\(summary.formattedNetPerformance) (\(summary.formattedNetPerformancePercentage))")
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.foregroundColor(summary.netPerformance >= 0 ? .positiveGreen : .negativeRed)
|
||||
.foregroundStyle(summary.netPerformance >= 0 ? Color.positiveGreen : Color.negativeRed)
|
||||
.hiddenBalance()
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(Color(.systemBackground))
|
||||
.cornerRadius(AppConstants.UI.cornerRadius)
|
||||
.clipShape(RoundedRectangle(cornerRadius: AppConstants.UI.cornerRadius))
|
||||
.shadow(color: .black.opacity(0.05), radius: 8, y: 2)
|
||||
}
|
||||
}
|
||||
@@ -1233,7 +1236,7 @@ struct DashboardCustomizeView: View {
|
||||
.font(.subheadline.weight(.semibold))
|
||||
Text(config.isCollapsed ? "Compact" : "Expanded")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
@@ -1256,10 +1259,10 @@ struct DashboardCustomizeView: View {
|
||||
}
|
||||
.navigationTitle("Customize Dashboard")
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
ToolbarItem(placement: .topBarLeading) {
|
||||
EditButton()
|
||||
}
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
Button("Done") {
|
||||
DashboardLayoutStore.save(configs)
|
||||
dismiss()
|
||||
@@ -1286,18 +1289,18 @@ struct CompactCard: View {
|
||||
if subtitleIsBalance {
|
||||
Text(subtitle)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
.hiddenBalance()
|
||||
} else {
|
||||
Text(subtitle)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(Color(.systemBackground))
|
||||
.cornerRadius(AppConstants.UI.cornerRadius)
|
||||
.clipShape(RoundedRectangle(cornerRadius: AppConstants.UI.cornerRadius))
|
||||
.shadow(color: .black.opacity(0.05), radius: 8, y: 2)
|
||||
}
|
||||
}
|
||||
@@ -1312,7 +1315,7 @@ struct EvolutionCompactCard: View {
|
||||
if let last = data.last {
|
||||
Text(last.value.compactCurrencyString)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
SparklineView(data: data, color: .appPrimary)
|
||||
.frame(height: 40)
|
||||
@@ -1320,7 +1323,7 @@ struct EvolutionCompactCard: View {
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(Color(.systemBackground))
|
||||
.cornerRadius(AppConstants.UI.cornerRadius)
|
||||
.clipShape(RoundedRectangle(cornerRadius: AppConstants.UI.cornerRadius))
|
||||
.shadow(color: .black.opacity(0.05), radius: 8, y: 2)
|
||||
}
|
||||
}
|
||||
@@ -1367,7 +1370,7 @@ struct PeriodReturnsCard: 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)
|
||||
}
|
||||
}
|
||||
@@ -1381,11 +1384,11 @@ struct ReturnPeriodView: View {
|
||||
VStack(spacing: 4) {
|
||||
Text(period)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
Text(change)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.foregroundColor(isPositive ? .positiveGreen : .negativeRed)
|
||||
.foregroundStyle(isPositive ? Color.positiveGreen : Color.negativeRed)
|
||||
.lineLimit(1)
|
||||
.minimumScaleFactor(0.8)
|
||||
}
|
||||
@@ -1404,14 +1407,14 @@ struct EmptyDashboardView: View {
|
||||
VStack(spacing: 20) {
|
||||
Image(systemName: "chart.pie")
|
||||
.font(.system(size: 60))
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
Text("Welcome to Portfolio Journal")
|
||||
.font(.custom("Avenir Next", size: 24).weight(.semibold))
|
||||
|
||||
Text("Start by adding your first investment source to track your portfolio.")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
Button {
|
||||
@@ -1419,11 +1422,11 @@ struct EmptyDashboardView: View {
|
||||
} label: {
|
||||
Label("Add Investment Source", systemImage: "plus")
|
||||
.font(.headline)
|
||||
.foregroundColor(.white)
|
||||
.foregroundStyle(.white)
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(Color.appPrimary)
|
||||
.cornerRadius(AppConstants.UI.cornerRadius)
|
||||
.clipShape(RoundedRectangle(cornerRadius: AppConstants.UI.cornerRadius))
|
||||
}
|
||||
|
||||
HStack(spacing: 12) {
|
||||
@@ -1435,8 +1438,8 @@ struct EmptyDashboardView: View {
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
.background(Color.appPrimary.opacity(0.1))
|
||||
.foregroundColor(.appPrimary)
|
||||
.cornerRadius(AppConstants.UI.cornerRadius)
|
||||
.foregroundStyle(Color.appPrimary)
|
||||
.clipShape(RoundedRectangle(cornerRadius: AppConstants.UI.cornerRadius))
|
||||
}
|
||||
|
||||
Button {
|
||||
@@ -1447,8 +1450,8 @@ struct EmptyDashboardView: View {
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
.background(Color.appSecondary.opacity(0.1))
|
||||
.foregroundColor(.appSecondary)
|
||||
.cornerRadius(AppConstants.UI.cornerRadius)
|
||||
.foregroundStyle(Color.appSecondary)
|
||||
.clipShape(RoundedRectangle(cornerRadius: AppConstants.UI.cornerRadius))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1465,24 +1468,24 @@ struct PendingUpdatesAlertBanner: View {
|
||||
var body: some View {
|
||||
HStack(spacing: 10) {
|
||||
Image(systemName: "exclamationmark.triangle.fill")
|
||||
.foregroundColor(.white)
|
||||
.foregroundStyle(.white)
|
||||
|
||||
Text("\(count) source\(count == 1 ? "" : "s") pending update")
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.foregroundColor(.white)
|
||||
.foregroundStyle(.white)
|
||||
|
||||
Spacer()
|
||||
|
||||
Button(action: onDismiss) {
|
||||
Image(systemName: "xmark")
|
||||
.font(.caption.weight(.bold))
|
||||
.foregroundColor(.white.opacity(0.8))
|
||||
.foregroundStyle(.white.opacity(0.8))
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 12)
|
||||
.background(Color.appWarning)
|
||||
.cornerRadius(AppConstants.UI.cornerRadius)
|
||||
.clipShape(RoundedRectangle(cornerRadius: AppConstants.UI.cornerRadius))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1496,13 +1499,13 @@ struct PendingUpdatesCard: View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
HStack {
|
||||
Image(systemName: "bell.badge.fill")
|
||||
.foregroundColor(.appWarning)
|
||||
.foregroundStyle(Color.appWarning)
|
||||
Text("Pending Updates")
|
||||
.font(.headline)
|
||||
Spacer()
|
||||
Text("\(sources.count)")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
ForEach(sources.prefix(3), id: \.objectID) { source in
|
||||
@@ -1519,11 +1522,11 @@ struct PendingUpdatesCard: View {
|
||||
|
||||
Text(source.latestSnapshot?.date.relativeDescription ?? "Never")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
@@ -1532,12 +1535,12 @@ struct PendingUpdatesCard: View {
|
||||
if sources.count > 3 {
|
||||
Text("+ \(sources.count - 3) more")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(Color(.systemBackground))
|
||||
.cornerRadius(AppConstants.UI.cornerRadius)
|
||||
.clipShape(RoundedRectangle(cornerRadius: AppConstants.UI.cornerRadius))
|
||||
.shadow(color: .black.opacity(0.05), radius: 8, y: 2)
|
||||
}
|
||||
}
|
||||
@@ -1593,7 +1596,7 @@ struct GoalsSummaryCard: View {
|
||||
Text(category.name)
|
||||
.font(.caption2.weight(.semibold))
|
||||
}
|
||||
.foregroundColor(category.color)
|
||||
.foregroundStyle(category.color)
|
||||
}
|
||||
}
|
||||
HStack(spacing: 4) {
|
||||
@@ -1601,22 +1604,22 @@ struct GoalsSummaryCard: View {
|
||||
.font(.caption.weight(.semibold))
|
||||
Text("of \(goal.targetDecimal.compactCurrencyString)")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
if let targetDate = goal.targetDate {
|
||||
Text("Target: \(targetDate.mediumDateString)")
|
||||
.font(.caption2.weight(.semibold))
|
||||
.foregroundColor(targetUrgency == .critical ? .negativeRed : (targetUrgency == .warning ? .appWarning : .secondary))
|
||||
.foregroundStyle(targetUrgency == .critical ? Color.negativeRed : (targetUrgency == .warning ? Color.appWarning : .secondary))
|
||||
}
|
||||
if let etaText = etaProvider(goal) {
|
||||
Text(etaText)
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
if let paceStatus {
|
||||
Text(paceStatus.statusText)
|
||||
.font(.caption2.weight(.semibold))
|
||||
.foregroundColor(paceStatus.isBehind ? .appWarning : .positiveGreen)
|
||||
.foregroundStyle(paceStatus.isBehind ? Color.appWarning : Color.positiveGreen)
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
@@ -1630,14 +1633,14 @@ struct GoalsSummaryCard: View {
|
||||
} label: {
|
||||
Image(systemName: "square.and.arrow.up")
|
||||
.font(.caption)
|
||||
.foregroundColor(.appPrimary)
|
||||
.foregroundStyle(Color.appPrimary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(Color(.systemBackground))
|
||||
.cornerRadius(AppConstants.UI.cornerRadius)
|
||||
.clipShape(RoundedRectangle(cornerRadius: AppConstants.UI.cornerRadius))
|
||||
.shadow(color: .black.opacity(0.05), radius: 8, y: 2)
|
||||
}
|
||||
}
|
||||
@@ -1657,11 +1660,11 @@ struct EmptyGoalsCard: View {
|
||||
}
|
||||
Text("Add a milestone like 1M and track your progress each month.")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.padding()
|
||||
.background(Color(.systemBackground))
|
||||
.cornerRadius(AppConstants.UI.cornerRadius)
|
||||
.clipShape(RoundedRectangle(cornerRadius: AppConstants.UI.cornerRadius))
|
||||
.shadow(color: .black.opacity(0.05), radius: 8, y: 2)
|
||||
}
|
||||
}
|
||||
@@ -1706,7 +1709,7 @@ struct ContributionsVsReturnsCard: View {
|
||||
Circle().fill(Color.appSecondary).frame(width: 10, height: 10)
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(String(localized: "contributions_vs_returns_invested"))
|
||||
.font(.caption).foregroundColor(.secondary)
|
||||
.font(.caption).foregroundStyle(.secondary)
|
||||
Text(totalContributions.currencyString)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
}
|
||||
@@ -1716,18 +1719,18 @@ struct ContributionsVsReturnsCard: View {
|
||||
Circle().fill(isReturnPositive ? Color.positiveGreen : Color.negativeRed).frame(width: 10, height: 10)
|
||||
VStack(alignment: .trailing, spacing: 2) {
|
||||
Text(String(localized: "contributions_vs_returns_returns"))
|
||||
.font(.caption).foregroundColor(.secondary)
|
||||
.font(.caption).foregroundStyle(.secondary)
|
||||
let prefix = totalReturns >= 0 ? "+" : ""
|
||||
Text("\(prefix)\(totalReturns.currencyString)")
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.foregroundColor(isReturnPositive ? .positiveGreen : .negativeRed)
|
||||
.foregroundStyle(isReturnPositive ? 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,64 +10,45 @@ struct JournalView: View {
|
||||
@State private var selectedDate: Date?
|
||||
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
||||
|
||||
/// Month list + inline check-in: two columns in regular width (iPad, iPhone
|
||||
/// Duo inner screen), a collapsing stack in compact width. The selected
|
||||
/// month is the only navigation state, so it survives the Duo opening/closing.
|
||||
@State private var columnVisibility: NavigationSplitViewVisibility = .all
|
||||
|
||||
var body: some View {
|
||||
if horizontalSizeClass == .regular {
|
||||
iPadJournalLayout
|
||||
} else {
|
||||
iPhoneJournalLayout
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - iPad Layout (list + inline detail)
|
||||
|
||||
private var iPadJournalLayout: some View {
|
||||
HStack(spacing: 0) {
|
||||
// Left: month list
|
||||
NavigationStack {
|
||||
journalListContent(isPad: true)
|
||||
.navigationTitle("Journal")
|
||||
.searchable(text: $searchText, prompt: "Search monthly notes")
|
||||
.onAppear { viewModel.refresh() }
|
||||
}
|
||||
.frame(width: 320)
|
||||
|
||||
Divider()
|
||||
|
||||
// Right: monthly check-in detail
|
||||
NavigationSplitView(columnVisibility: $columnVisibility) {
|
||||
journalListContent
|
||||
.navigationTitle("Journal")
|
||||
.searchable(text: $searchText, prompt: "Search monthly notes")
|
||||
.onAppear { viewModel.refresh() }
|
||||
.navigationSplitViewColumnWidth(min: 300, ideal: 340, max: 420)
|
||||
} detail: {
|
||||
NavigationStack {
|
||||
if let date = selectedDate {
|
||||
MonthlyCheckInView(referenceDate: date)
|
||||
.id(date)
|
||||
} else {
|
||||
noMonthSelectedView
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - iPhone Layout (push navigation)
|
||||
|
||||
private var iPhoneJournalLayout: some View {
|
||||
NavigationStack {
|
||||
journalListContent(isPad: false)
|
||||
.navigationTitle("Journal")
|
||||
.searchable(text: $searchText, prompt: "Search monthly notes")
|
||||
.onAppear { viewModel.refresh() }
|
||||
}
|
||||
.navigationSplitViewStyle(.balanced)
|
||||
}
|
||||
|
||||
// MARK: - Shared List Content
|
||||
|
||||
private func journalListContent(isPad: Bool) -> some View {
|
||||
private var journalListContent: some View {
|
||||
ScrollViewReader { proxy in
|
||||
ZStack {
|
||||
AppBackground()
|
||||
|
||||
List {
|
||||
List(selection: $selectedDate) {
|
||||
// Calm 2.0: the habit story (streaks, achievements) lives in
|
||||
// the Journal — this is the emotional home of the ritual.
|
||||
if !viewModel.monthlyNotes.isEmpty && searchText.isEmpty {
|
||||
Section {
|
||||
MomentumStreaksCard(compact: isPad)
|
||||
// Narrow sidebar column in regular width → compact stat tiles.
|
||||
MomentumStreaksCard(compact: horizontalSizeClass == .regular)
|
||||
}
|
||||
.listRowInsets(EdgeInsets())
|
||||
.listRowBackground(Color.clear)
|
||||
@@ -80,12 +61,12 @@ struct JournalView: View {
|
||||
VStack(spacing: 12) {
|
||||
Image(systemName: "book.closed")
|
||||
.font(.system(size: 36))
|
||||
.foregroundColor(.appSecondary)
|
||||
.foregroundStyle(Color.appSecondary)
|
||||
Text(String(localized: "journal_empty_title"))
|
||||
.font(.headline)
|
||||
Text(String(localized: "journal_empty_body"))
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
.padding(.vertical, 16)
|
||||
@@ -93,34 +74,14 @@ struct JournalView: View {
|
||||
} else {
|
||||
Text("No matching notes.")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
} else {
|
||||
ForEach(filteredMonthlyNotes) { entry in
|
||||
if isPad {
|
||||
Button {
|
||||
selectedDate = entry.date
|
||||
} label: {
|
||||
monthlyNoteRow(entry)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.listRowBackground(
|
||||
selectedDate.map { $0.isSameMonth(as: entry.date) } == true
|
||||
? Color.appPrimary.opacity(0.08)
|
||||
: Color.clear
|
||||
)
|
||||
monthlyNoteRow(entry)
|
||||
.tag(entry.date)
|
||||
.id(entry.date)
|
||||
.onAppear { currentVisibleMonth = entry.date }
|
||||
} else {
|
||||
NavigationLink {
|
||||
MonthlyCheckInView(referenceDate: entry.date)
|
||||
} label: {
|
||||
monthlyNoteRow(entry)
|
||||
}
|
||||
.id(entry.date)
|
||||
.onAppear { currentVisibleMonth = entry.date }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -156,7 +117,7 @@ struct JournalView: View {
|
||||
|
||||
Image(systemName: "book.closed.fill")
|
||||
.font(.system(size: 50, weight: .light))
|
||||
.foregroundColor(.white)
|
||||
.foregroundStyle(.white)
|
||||
}
|
||||
|
||||
VStack(spacing: 10) {
|
||||
@@ -164,14 +125,15 @@ struct JournalView: View {
|
||||
.font(.title2.weight(.semibold))
|
||||
|
||||
Text("Choose a month from the list on the\nleft to view your check-in notes.")
|
||||
.frame(maxWidth: 360)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
|
||||
Label("Select from the list", systemImage: "arrow.left")
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.foregroundColor(.appSecondary)
|
||||
.foregroundStyle(Color.appSecondary)
|
||||
.padding(.horizontal, 22)
|
||||
.padding(.vertical, 11)
|
||||
.background(Color.appSecondary.opacity(0.1))
|
||||
@@ -203,7 +165,7 @@ struct JournalView: View {
|
||||
.frame(width: 40, height: 40)
|
||||
Image(systemName: entry.mood?.iconName ?? "book.closed")
|
||||
.font(.system(size: 16))
|
||||
.foregroundColor(entry.mood?.tint ?? .secondary)
|
||||
.foregroundStyle(entry.mood?.tint ?? .secondary)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
@@ -220,12 +182,12 @@ struct JournalView: View {
|
||||
if let mood = entry.mood {
|
||||
Text(mood.title)
|
||||
.font(.caption.weight(.medium))
|
||||
.foregroundColor(mood.tint)
|
||||
.foregroundStyle(mood.tint)
|
||||
}
|
||||
|
||||
Text(entry.note.isEmpty ? String(localized: "journal_no_note") : entry.note)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(2)
|
||||
}
|
||||
}
|
||||
@@ -237,7 +199,7 @@ struct JournalView: View {
|
||||
return HStack(spacing: 4) {
|
||||
ForEach(1...5, id: \.self) { index in
|
||||
Image(systemName: index <= value ? "star.fill" : "star")
|
||||
.foregroundColor(index <= value ? .yellow : .secondary)
|
||||
.foregroundStyle(index <= value ? .yellow : .secondary)
|
||||
}
|
||||
}
|
||||
.accessibilityLabel(
|
||||
@@ -251,7 +213,7 @@ struct JournalView: View {
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.vertical, 6)
|
||||
.background(Color.gray.opacity(0.15))
|
||||
.foregroundColor(.primary)
|
||||
.foregroundStyle(.primary)
|
||||
.clipShape(Capsule())
|
||||
}
|
||||
|
||||
@@ -269,9 +231,7 @@ struct JournalView: View {
|
||||
.font(.caption.weight(.semibold))
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.vertical, 6)
|
||||
.background(Color(.systemBackground).opacity(0.95))
|
||||
.cornerRadius(12)
|
||||
.shadow(color: .black.opacity(0.12), radius: 6, y: 2)
|
||||
.floatingPillBackground()
|
||||
.offset(x: -16)
|
||||
}
|
||||
|
||||
@@ -280,9 +240,7 @@ struct JournalView: View {
|
||||
.font(.caption.weight(.semibold))
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.vertical, 6)
|
||||
.background(Color(.systemBackground).opacity(0.95))
|
||||
.cornerRadius(12)
|
||||
.shadow(color: .black.opacity(0.12), radius: 6, y: 2)
|
||||
.floatingPillBackground()
|
||||
.offset(x: -16, y: scrubberOffset - height / 2)
|
||||
}
|
||||
|
||||
@@ -339,13 +297,13 @@ struct MonthlyNoteEditorView: View {
|
||||
.frame(minHeight: 200)
|
||||
.padding(8)
|
||||
.background(Color.gray.opacity(0.08))
|
||||
.cornerRadius(12)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 12))
|
||||
}
|
||||
.padding()
|
||||
.navigationTitle("Monthly Note")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
Button("Save") {
|
||||
MonthlyCheckInStore.setNote(note, for: date)
|
||||
dismiss()
|
||||
|
||||
@@ -7,70 +7,21 @@ struct SourceListView: View {
|
||||
@Environment(\.managedObjectContext) private var context
|
||||
@StateObject private var viewModel: SourceListViewModel
|
||||
@State private var sourceToDelete: InvestmentSource?
|
||||
@State private var navigationPath = NavigationPath()
|
||||
@State private var showingSearch = false
|
||||
@State private var selectedSourceID: NSManagedObjectID?
|
||||
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
||||
|
||||
init(iapService: IAPService) {
|
||||
_viewModel = StateObject(wrappedValue: SourceListViewModel(iapService: iapService))
|
||||
}
|
||||
|
||||
/// Sidebar/detail split: two columns in regular width (iPad, iPhone Duo
|
||||
/// inner screen), a single collapsing stack in compact width. Selection is
|
||||
/// the only navigation state, so it survives the size-class change when the
|
||||
/// Duo opens or closes.
|
||||
@State private var columnVisibility: NavigationSplitViewVisibility = .all
|
||||
|
||||
var body: some View {
|
||||
if horizontalSizeClass == .regular {
|
||||
iPadSourcesLayout
|
||||
} else {
|
||||
iPhoneSourcesLayout
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - iPad Layout (master-detail)
|
||||
|
||||
private var iPadSourcesLayout: some View {
|
||||
HStack(spacing: 0) {
|
||||
// Left: source list panel
|
||||
NavigationStack {
|
||||
ZStack {
|
||||
AppBackground()
|
||||
if viewModel.isEmpty { emptyStateView } else { sourcesList }
|
||||
}
|
||||
.navigationTitle("Sources")
|
||||
.toolbar { sourcesToolbar }
|
||||
.sheet(isPresented: $viewModel.showingAddSource) { AddSourceView() }
|
||||
.sheet(isPresented: $viewModel.showingPaywall) { PaywallView() }
|
||||
.onAppear { syncAccountState() }
|
||||
.onReceive(accountStore.$selectedAccount) { viewModel.selectedAccount = $0 }
|
||||
.onReceive(accountStore.$showAllAccounts) { viewModel.showAllAccounts = $0 }
|
||||
.confirmationDialog(
|
||||
"Delete Source",
|
||||
isPresented: Binding(get: { sourceToDelete != nil }, set: { if !$0 { sourceToDelete = nil } }),
|
||||
titleVisibility: .visible
|
||||
) { deleteSourceDialogButtons } message: { deleteSourceDialogMessage }
|
||||
}
|
||||
.frame(width: 340)
|
||||
|
||||
Divider()
|
||||
|
||||
// Right: detail panel
|
||||
NavigationStack {
|
||||
Group {
|
||||
if let id = selectedSourceID,
|
||||
let source = try? context.existingObject(with: id) as? InvestmentSource,
|
||||
!source.isDeleted {
|
||||
SourceDetailView(source: source, iapService: iapService)
|
||||
.id(id)
|
||||
} else {
|
||||
noSourceSelectedView
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - iPhone Layout (push navigation)
|
||||
|
||||
private var iPhoneSourcesLayout: some View {
|
||||
NavigationStack(path: $navigationPath) {
|
||||
NavigationSplitView(columnVisibility: $columnVisibility) {
|
||||
ZStack {
|
||||
AppBackground()
|
||||
if viewModel.isEmpty { emptyStateView } else { sourcesList }
|
||||
@@ -82,19 +33,32 @@ struct SourceListView: View {
|
||||
.onAppear { syncAccountState() }
|
||||
.onReceive(accountStore.$selectedAccount) { viewModel.selectedAccount = $0 }
|
||||
.onReceive(accountStore.$showAllAccounts) { viewModel.showAllAccounts = $0 }
|
||||
.navigationDestination(for: NSManagedObjectID.self) { objectID in
|
||||
if let source = try? context.existingObject(with: objectID) as? InvestmentSource,
|
||||
!source.isDeleted {
|
||||
SourceDetailView(source: source, iapService: iapService)
|
||||
} else {
|
||||
MissingSourceView()
|
||||
}
|
||||
}
|
||||
.confirmationDialog(
|
||||
"Delete Source",
|
||||
isPresented: Binding(get: { sourceToDelete != nil }, set: { if !$0 { sourceToDelete = nil } }),
|
||||
titleVisibility: .visible
|
||||
) { deleteSourceDialogButtons } message: { deleteSourceDialogMessage }
|
||||
.navigationSplitViewColumnWidth(min: 300, ideal: 340, max: 420)
|
||||
} detail: {
|
||||
NavigationStack {
|
||||
detailContent
|
||||
}
|
||||
}
|
||||
.navigationSplitViewStyle(.balanced)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var detailContent: some View {
|
||||
if let id = selectedSourceID {
|
||||
if let source = try? context.existingObject(with: id) as? InvestmentSource,
|
||||
!source.isDeleted {
|
||||
SourceDetailView(source: source, iapService: iapService)
|
||||
.id(id)
|
||||
} else {
|
||||
MissingSourceView()
|
||||
}
|
||||
} else {
|
||||
noSourceSelectedView
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,21 +71,21 @@ struct SourceListView: View {
|
||||
|
||||
@ToolbarContentBuilder
|
||||
private var sourcesToolbar: some ToolbarContent {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
Button { viewModel.addSourceTapped() } label: {
|
||||
Image(systemName: "plus")
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
Button {
|
||||
withAnimation(.easeInOut(duration: 0.2)) { showingSearch.toggle() }
|
||||
if !showingSearch { viewModel.searchText = "" }
|
||||
} label: {
|
||||
Image(systemName: showingSearch ? "xmark.circle.fill" : "magnifyingglass")
|
||||
.foregroundColor(showingSearch ? .secondary : .primary)
|
||||
.foregroundStyle(showingSearch ? .secondary : .primary)
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
ToolbarItem(placement: .topBarLeading) {
|
||||
accountFilterMenu
|
||||
}
|
||||
}
|
||||
@@ -163,7 +127,7 @@ struct SourceListView: View {
|
||||
|
||||
Image(systemName: "chart.line.uptrend.xyaxis")
|
||||
.font(.system(size: 50, weight: .light))
|
||||
.foregroundColor(.white)
|
||||
.foregroundStyle(.white)
|
||||
}
|
||||
|
||||
VStack(spacing: 10) {
|
||||
@@ -171,15 +135,16 @@ struct SourceListView: View {
|
||||
.font(.title2.weight(.semibold))
|
||||
|
||||
Text("Pick an investment source from the\npanel on the left to see its details.")
|
||||
.frame(maxWidth: 360)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
|
||||
// Call-to-action pill
|
||||
Label("Select from the list", systemImage: "arrow.left")
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.foregroundColor(.appPrimary)
|
||||
.foregroundStyle(Color.appPrimary)
|
||||
.padding(.horizontal, 22)
|
||||
.padding(.vertical, 11)
|
||||
.background(Color.appPrimary.opacity(0.1))
|
||||
@@ -194,7 +159,7 @@ struct SourceListView: View {
|
||||
// MARK: - Sources List
|
||||
|
||||
private var sourcesList: some View {
|
||||
List {
|
||||
List(selection: $selectedSourceID) {
|
||||
// Filter Bar
|
||||
Section {
|
||||
filterBarContent
|
||||
@@ -210,7 +175,7 @@ struct SourceListView: View {
|
||||
VStack(alignment: .leading) {
|
||||
Text("Total Value")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
Text(viewModel.formattedTotalValue)
|
||||
.font(.title2.weight(.bold))
|
||||
.hiddenBalance()
|
||||
@@ -221,7 +186,7 @@ struct SourceListView: View {
|
||||
VStack(alignment: .trailing) {
|
||||
Text("Sources")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
Text("\(viewModel.sources.count)")
|
||||
.font(.title2.weight(.bold))
|
||||
}
|
||||
@@ -235,7 +200,7 @@ struct SourceListView: View {
|
||||
Section {
|
||||
HStack {
|
||||
Image(systemName: "exclamationmark.triangle.fill")
|
||||
.foregroundColor(.appWarning)
|
||||
.foregroundStyle(Color.appWarning)
|
||||
|
||||
Text("Source limit reached. Upgrade to Premium for unlimited sources.")
|
||||
.font(.subheadline)
|
||||
@@ -246,7 +211,7 @@ struct SourceListView: View {
|
||||
viewModel.showingPaywall = true
|
||||
}
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.foregroundColor(.appPrimary)
|
||||
.foregroundStyle(Color.appPrimary)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -255,19 +220,7 @@ struct SourceListView: View {
|
||||
Section {
|
||||
ForEach(viewModel.sources, id: \.objectID) { source in
|
||||
SourceRowView(source: source)
|
||||
.contentShape(Rectangle())
|
||||
.background(
|
||||
horizontalSizeClass == .regular && selectedSourceID == source.objectID
|
||||
? Color.appPrimary.opacity(0.08)
|
||||
: Color.clear
|
||||
)
|
||||
.onTapGesture {
|
||||
if horizontalSizeClass == .regular {
|
||||
selectedSourceID = source.objectID
|
||||
} else {
|
||||
navigationPath.append(source.objectID)
|
||||
}
|
||||
}
|
||||
.tag(source.objectID)
|
||||
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
|
||||
Button(role: .destructive) {
|
||||
sourceToDelete = source
|
||||
@@ -302,14 +255,14 @@ struct SourceListView: View {
|
||||
VStack(spacing: 20) {
|
||||
Image(systemName: "tray")
|
||||
.font(.system(size: 60))
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
Text("No Investment Sources")
|
||||
.font(.custom("Avenir Next", size: 24).weight(.semibold))
|
||||
|
||||
Text("Add your first investment source to start tracking your portfolio.")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal, 40)
|
||||
|
||||
@@ -318,11 +271,11 @@ struct SourceListView: View {
|
||||
} label: {
|
||||
Label("Add Source", systemImage: "plus")
|
||||
.font(.headline)
|
||||
.foregroundColor(.white)
|
||||
.foregroundStyle(.white)
|
||||
.padding()
|
||||
.frame(maxWidth: 200)
|
||||
.background(Color.appPrimary)
|
||||
.cornerRadius(AppConstants.UI.cornerRadius)
|
||||
.clipShape(RoundedRectangle(cornerRadius: AppConstants.UI.cornerRadius))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -358,7 +311,7 @@ struct SourceListView: View {
|
||||
if showingSearch {
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: "magnifyingglass")
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
TextField("Search sources", text: $viewModel.searchText)
|
||||
.textFieldStyle(.plain)
|
||||
@@ -369,14 +322,14 @@ struct SourceListView: View {
|
||||
viewModel.searchText = ""
|
||||
} label: {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 9)
|
||||
.background(Color(.systemGray6))
|
||||
.cornerRadius(10)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.bottom, 10)
|
||||
.transition(.move(edge: .top).combined(with: .opacity))
|
||||
@@ -452,8 +405,8 @@ private struct CategoryChip: View {
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 6)
|
||||
.background(isSelected ? Color.appPrimary : Color(.systemGray5))
|
||||
.foregroundColor(isSelected ? .white : .primary)
|
||||
.cornerRadius(20)
|
||||
.foregroundStyle(isSelected ? .white : .primary)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 20))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
@@ -464,12 +417,12 @@ private struct MissingSourceView: View {
|
||||
VStack(spacing: 12) {
|
||||
Image(systemName: "exclamationmark.triangle")
|
||||
.font(.system(size: 42))
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
Text("Source not available")
|
||||
.font(.headline)
|
||||
Text("This source was deleted.")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
@@ -490,7 +443,7 @@ struct SourceRowView: View {
|
||||
|
||||
Image(systemName: source.category?.icon ?? "questionmark")
|
||||
.font(.system(size: 18))
|
||||
.foregroundColor(source.category?.color ?? .gray)
|
||||
.foregroundStyle(source.category?.color ?? .gray)
|
||||
}
|
||||
|
||||
// Source info
|
||||
@@ -501,12 +454,12 @@ struct SourceRowView: View {
|
||||
HStack(spacing: 8) {
|
||||
Text(source.category?.name ?? "Uncategorized")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
if source.needsUpdate {
|
||||
Label("Needs update", systemImage: "clock.fill")
|
||||
.font(.caption2.weight(.semibold))
|
||||
.foregroundColor(.appWarning)
|
||||
.foregroundStyle(Color.appWarning)
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 3)
|
||||
.background(Color.appWarning.opacity(0.12))
|
||||
@@ -529,7 +482,7 @@ struct SourceRowView: View {
|
||||
Text(String(format: "%.1f%%", NSDecimalNumber(decimal: source.totalReturn).doubleValue))
|
||||
.font(.caption)
|
||||
}
|
||||
.foregroundColor(source.totalReturn >= 0 ? .positiveGreen : .negativeRed)
|
||||
.foregroundStyle(source.totalReturn >= 0 ? Color.positiveGreen : Color.negativeRed)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
|
||||
Reference in New Issue
Block a user