diff --git a/PortfolioJournal.xcodeproj/project.pbxproj b/PortfolioJournal.xcodeproj/project.pbxproj index 3984c2a..0b368c8 100644 --- a/PortfolioJournal.xcodeproj/project.pbxproj +++ b/PortfolioJournal.xcodeproj/project.pbxproj @@ -541,7 +541,7 @@ CODE_SIGN_ENTITLEMENTS = PortfolioJournal/PortfolioJournalDebug.entitlements; ENABLE_USER_SCRIPT_SANDBOXING = NO; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 52; + CURRENT_PROJECT_VERSION = 53; DEVELOPMENT_ASSET_PATHS = PortfolioJournal/Assets.xcassets; DEVELOPMENT_TEAM = 2825Q76T7H; ENABLE_PREVIEWS = YES; @@ -582,7 +582,7 @@ CODE_SIGN_IDENTITY = "Apple Distribution"; CODE_SIGN_STYLE = Manual; PROVISIONING_PROFILE_SPECIFIER = "porfoliojournal"; - CURRENT_PROJECT_VERSION = 52; + CURRENT_PROJECT_VERSION = 53; DEVELOPMENT_ASSET_PATHS = PortfolioJournal/Assets.xcassets; DEVELOPMENT_TEAM = 2825Q76T7H; ENABLE_PREVIEWS = YES; @@ -739,7 +739,7 @@ ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; CODE_SIGN_ENTITLEMENTS = PortfolioJournalWidgetExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 52; + CURRENT_PROJECT_VERSION = 53; DEVELOPMENT_ASSET_PATHS = PortfolioJournalWidget/Assets.xcassets; DEVELOPMENT_TEAM = 2825Q76T7H; GENERATE_INFOPLIST_FILE = NO; @@ -774,7 +774,7 @@ CODE_SIGN_IDENTITY = "Apple Distribution"; CODE_SIGN_STYLE = Manual; PROVISIONING_PROFILE_SPECIFIER = "Portfolio Journalwidget"; - CURRENT_PROJECT_VERSION = 52; + CURRENT_PROJECT_VERSION = 53; DEVELOPMENT_ASSET_PATHS = PortfolioJournalWidget/Assets.xcassets; DEVELOPMENT_TEAM = 2825Q76T7H; GENERATE_INFOPLIST_FILE = NO; @@ -805,7 +805,7 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 52; + CURRENT_PROJECT_VERSION = 53; DEVELOPMENT_TEAM = 2825Q76T7H; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 17.6; @@ -829,7 +829,7 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 52; + CURRENT_PROJECT_VERSION = 53; DEVELOPMENT_TEAM = 2825Q76T7H; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 17.6; @@ -852,7 +852,7 @@ isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 52; + CURRENT_PROJECT_VERSION = 53; DEVELOPMENT_TEAM = 2825Q76T7H; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 17.6; @@ -875,7 +875,7 @@ isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 52; + CURRENT_PROJECT_VERSION = 53; DEVELOPMENT_TEAM = 2825Q76T7H; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 17.6; @@ -899,7 +899,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = PortfolioJournalQuickUpdateExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 52; + CURRENT_PROJECT_VERSION = 53; DEVELOPMENT_TEAM = 2825Q76T7H; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = PortfolioJournalQuickUpdate/Info.plist; @@ -927,7 +927,7 @@ CODE_SIGN_ENTITLEMENTS = PortfolioJournalQuickUpdateExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 52; + CURRENT_PROJECT_VERSION = 53; DEVELOPMENT_TEAM = 2825Q76T7H; PROVISIONING_PROFILE_SPECIFIER = "PortfolioJournal QuickUpdate AppStore"; GENERATE_INFOPLIST_FILE = NO; diff --git a/PortfolioJournal/Resources/de.lproj/Localizable.strings b/PortfolioJournal/Resources/de.lproj/Localizable.strings index 634a394..46271f5 100644 --- a/PortfolioJournal/Resources/de.lproj/Localizable.strings +++ b/PortfolioJournal/Resources/de.lproj/Localizable.strings @@ -488,3 +488,4 @@ "guided_streak" = "%d Monate in Folge"; "guided_share_month" = "Diesen Monat teilen"; "guided_done_cta" = "Fertig"; +"journal_no_note" = "Noch keine Notiz."; diff --git a/PortfolioJournal/Resources/en.lproj/Localizable.strings b/PortfolioJournal/Resources/en.lproj/Localizable.strings index 56dc995..96be0eb 100644 --- a/PortfolioJournal/Resources/en.lproj/Localizable.strings +++ b/PortfolioJournal/Resources/en.lproj/Localizable.strings @@ -489,3 +489,4 @@ "guided_streak" = "%d months in a row"; "guided_share_month" = "Share this month"; "guided_done_cta" = "Done"; +"journal_no_note" = "No note yet."; diff --git a/PortfolioJournal/Resources/es-ES.lproj/Localizable.strings b/PortfolioJournal/Resources/es-ES.lproj/Localizable.strings index cb384ce..ce5625d 100644 --- a/PortfolioJournal/Resources/es-ES.lproj/Localizable.strings +++ b/PortfolioJournal/Resources/es-ES.lproj/Localizable.strings @@ -440,3 +440,4 @@ "guided_streak" = "%d meses seguidos"; "guided_share_month" = "Compartir este mes"; "guided_done_cta" = "Hecho"; +"journal_no_note" = "Sin nota todavía."; diff --git a/PortfolioJournal/Resources/fr.lproj/Localizable.strings b/PortfolioJournal/Resources/fr.lproj/Localizable.strings index 40a671f..4042ec0 100644 --- a/PortfolioJournal/Resources/fr.lproj/Localizable.strings +++ b/PortfolioJournal/Resources/fr.lproj/Localizable.strings @@ -488,3 +488,4 @@ "guided_streak" = "%d mois d'affilée"; "guided_share_month" = "Partager ce mois"; "guided_done_cta" = "Terminé"; +"journal_no_note" = "Pas encore de note."; diff --git a/PortfolioJournal/Resources/it.lproj/Localizable.strings b/PortfolioJournal/Resources/it.lproj/Localizable.strings index 17dec1e..0ffdd86 100644 --- a/PortfolioJournal/Resources/it.lproj/Localizable.strings +++ b/PortfolioJournal/Resources/it.lproj/Localizable.strings @@ -488,3 +488,4 @@ "guided_streak" = "%d mesi di fila"; "guided_share_month" = "Condividi questo mese"; "guided_done_cta" = "Fatto"; +"journal_no_note" = "Ancora nessuna nota."; diff --git a/PortfolioJournal/Resources/ja.lproj/Localizable.strings b/PortfolioJournal/Resources/ja.lproj/Localizable.strings index 7ea26b6..f0c21d9 100644 --- a/PortfolioJournal/Resources/ja.lproj/Localizable.strings +++ b/PortfolioJournal/Resources/ja.lproj/Localizable.strings @@ -488,3 +488,4 @@ "guided_streak" = "%dか月連続"; "guided_share_month" = "今月をシェア"; "guided_done_cta" = "完了"; +"journal_no_note" = "まだメモがありません。"; diff --git a/PortfolioJournal/Resources/pt-BR.lproj/Localizable.strings b/PortfolioJournal/Resources/pt-BR.lproj/Localizable.strings index 184edfc..ed90269 100644 --- a/PortfolioJournal/Resources/pt-BR.lproj/Localizable.strings +++ b/PortfolioJournal/Resources/pt-BR.lproj/Localizable.strings @@ -488,3 +488,4 @@ "guided_streak" = "%d meses seguidos"; "guided_share_month" = "Compartilhar este mês"; "guided_done_cta" = "Concluído"; +"journal_no_note" = "Ainda sem nota."; diff --git a/PortfolioJournal/Views/Components/ProgressRing.swift b/PortfolioJournal/Views/Components/ProgressRing.swift new file mode 100644 index 0000000..cb713ed --- /dev/null +++ b/PortfolioJournal/Views/Components/ProgressRing.swift @@ -0,0 +1,30 @@ +import SwiftUI + +/// Fitness-style progress ring: track + rounded-cap arc + centered percentage. +/// Color is semantic (pace/achievement), passed by the caller. +struct ProgressRing: View { + let progress: Double + var tint: Color = .appPrimary + var size: CGFloat = 56 + var lineWidth: CGFloat = 6 + + private var clamped: Double { min(max(progress, 0), 1) } + + var body: some View { + ZStack { + Circle() + .stroke(tint.opacity(0.15), lineWidth: lineWidth) + Circle() + .trim(from: 0, to: clamped) + .stroke(tint, style: StrokeStyle(lineWidth: lineWidth, lineCap: .round)) + .rotationEffect(.degrees(-90)) + .animation(.easeOut(duration: 0.6), value: clamped) + Text("\(Int((clamped * 100).rounded()))%") + .font(.system(size: size * 0.26, weight: .bold, design: .rounded)) + .foregroundColor(tint) + .minimumScaleFactor(0.7) + } + .frame(width: size, height: size) + .accessibilityLabel(Text("\(Int((clamped * 100).rounded()))%")) + } +} diff --git a/PortfolioJournal/Views/Dashboard/DashboardView.swift b/PortfolioJournal/Views/Dashboard/DashboardView.swift index 8e05348..3312ff7 100644 --- a/PortfolioJournal/Views/Dashboard/DashboardView.swift +++ b/PortfolioJournal/Views/Dashboard/DashboardView.swift @@ -12,8 +12,6 @@ struct DashboardView: View { @State private var showingCustomize = false @State private var sectionConfigs = DashboardLayoutStore.load() @AppStorage("showForecast") private var showForecast = true - @State private var fullScreenSection: DashboardSection? = nil - @State private var dragTargetID: String? = nil @State private var showingQuickUpdate = false @Environment(\.horizontalSizeClass) private var horizontalSizeClass @@ -53,14 +51,7 @@ struct DashboardView: View { } .padding() } - - if horizontalSizeClass == .regular, let section = fullScreenSection { - iPadFullScreenOverlay(for: section) - .transition(.opacity.combined(with: .scale(scale: 0.98))) - .zIndex(10) - } } - .animation(.easeInOut(duration: 0.25), value: fullScreenSection) .navigationTitle("Home") .refreshable { viewModel.refreshData() @@ -174,135 +165,19 @@ struct DashboardView: View { // MARK: - iPad Layout Helpers - /// Hero and check-in render full-width above the grid (state + action zones); - /// only context cards flow into the 2-column grid. - private var iPadGridSections: [DashboardSectionConfig] { - visibleSections.filter { - $0.id != DashboardSection.totalValue.id && $0.id != DashboardSection.monthlyCheckIn.id - } - } - @ViewBuilder private var iPadDashboardLayout: some View { - // Total Portfolio Value — always full width, never in the grid - let totalConfig = sectionConfigs.first(where: { $0.id == DashboardSection.totalValue.id }) - ?? DashboardSectionConfig(id: DashboardSection.totalValue.id, isVisible: true, isCollapsed: false) - sectionView(for: DashboardSectionConfig(id: totalConfig.id, isVisible: true, isCollapsed: totalConfig.isCollapsed)) - - if let checkInConfig = visibleSections.first(where: { $0.id == DashboardSection.monthlyCheckIn.id }) { - sectionView(for: checkInConfig) - } - - if !viewModel.insights.isEmpty { - InsightsRow(insights: viewModel.insights) - } - - // Other cards — 2-column grid with drag-and-drop - if !iPadGridSections.isEmpty { - LazyVGrid( - columns: [GridItem(.flexible()), GridItem(.flexible())], - spacing: 16 - ) { - ForEach(iPadGridSections) { config in - sectionView(for: config) - .gridCellColumns(config.columnSpan) - .overlay { - if dragTargetID == config.id { - RoundedRectangle(cornerRadius: AppConstants.UI.cornerRadius) - .stroke(Color.appPrimary, lineWidth: 2) - } - } - .draggable(config.id) - .dropDestination(for: String.self) { items, _ in - guard let fromID = items.first else { return false } - return handleIPadDrop(fromID: fromID, toID: config.id) - } isTargeted: { isTargeted in - dragTargetID = isTargeted ? config.id : nil - } - .contextMenu { - Button { - toggleColumnSpan(for: config.id) - } label: { - Label( - config.columnSpan == 2 ? "Normal width" : "Expand to full width", - systemImage: config.columnSpan == 2 ? "rectangle" : "rectangle.expand.diagonal" - ) - } - Button { - withAnimation(.easeInOut(duration: 0.25)) { - fullScreenSection = DashboardSection(rawValue: config.id) - } - } label: { - Label("View full screen", systemImage: "arrow.up.left.and.arrow.down.right") - } - } + VStack(spacing: 16) { + ForEach(visibleSections) { config in + sectionView(for: config) + if config.id == DashboardSection.monthlyCheckIn.id, + !viewModel.insights.isEmpty { + InsightsRow(insights: viewModel.insights) } } } - } - - @ViewBuilder - private func iPadFullScreenOverlay(for section: DashboardSection) -> some View { - ZStack { - Color(.systemBackground) - .opacity(0.97) - .ignoresSafeArea() - - VStack(spacing: 0) { - HStack { - Text(section.title) - .font(.title3.weight(.semibold)) - Spacer() - Button { - withAnimation(.easeInOut(duration: 0.25)) { - fullScreenSection = nil - } - } label: { - Image(systemName: "xmark.circle.fill") - .font(.title2) - .foregroundColor(.secondary) - } - } - .padding() - - Divider() - - ScrollView { - VStack(spacing: 20) { - let totalConfig = sectionConfigs.first(where: { $0.id == DashboardSection.totalValue.id }) - ?? DashboardSectionConfig(id: DashboardSection.totalValue.id, isVisible: true, isCollapsed: false) - sectionView(for: DashboardSectionConfig(id: totalConfig.id, isVisible: true, isCollapsed: totalConfig.isCollapsed)) - - if let config = sectionConfigs.first(where: { $0.id == section.id }) { - sectionView(for: DashboardSectionConfig(id: config.id, isVisible: true, isCollapsed: false)) - } - } - .padding() - .frame(maxWidth: 800) - .frame(maxWidth: .infinity) - } - } - } - } - - private func handleIPadDrop(fromID: String, toID: String) -> Bool { - guard fromID != toID, - let fromIndex = sectionConfigs.firstIndex(where: { $0.id == fromID }), - let toIndex = sectionConfigs.firstIndex(where: { $0.id == toID }) else { return false } - withAnimation { - sectionConfigs.move(fromOffsets: IndexSet(integer: fromIndex), toOffset: toIndex > fromIndex ? toIndex + 1 : toIndex) - } - DashboardLayoutStore.save(sectionConfigs) - dragTargetID = nil - return true - } - - private func toggleColumnSpan(for id: String) { - guard let index = sectionConfigs.firstIndex(where: { $0.id == id }) else { return } - withAnimation { - sectionConfigs[index].columnSpan = sectionConfigs[index].columnSpan == 2 ? 1 : 2 - } - DashboardLayoutStore.save(sectionConfigs) + .frame(maxWidth: 720) + .frame(maxWidth: .infinity) } @ViewBuilder @@ -1464,52 +1339,51 @@ struct GoalsSummaryCard: View { isBehind: paceStatus?.isBehind ?? false, isAchieved: isAchieved ) - VStack(alignment: .leading, spacing: 6) { - HStack { + HStack(spacing: 14) { + ProgressRing( + progress: progressProvider(goal), + tint: isAchieved ? .appSuccess : (paceStatus?.isBehind == true ? .appWarning : .appPrimary), + size: 48, + lineWidth: 5 + ) + VStack(alignment: .leading, spacing: 3) { Text(goal.name) .font(.subheadline.weight(.semibold)) - Spacer() - Button { - GoalShareService.shared.shareGoal( - name: goal.name, - progress: progressProvider(goal), - currentValue: currentValue, - targetValue: goal.targetDecimal - ) - } label: { - Image(systemName: "square.and.arrow.up") + HStack(spacing: 4) { + Text(currentValue.compactCurrencyString) + .font(.caption.weight(.semibold)) + Text("of \(goal.targetDecimal.compactCurrencyString)") .font(.caption) - .foregroundColor(.appPrimary) + .foregroundColor(.secondary) + } + if let targetDate = goal.targetDate { + Text("Target: \(targetDate.mediumDateString)") + .font(.caption2.weight(.semibold)) + .foregroundColor(targetUrgency == .critical ? .negativeRed : (targetUrgency == .warning ? .appWarning : .secondary)) + } + if let etaText = etaProvider(goal) { + Text(etaText) + .font(.caption2) + .foregroundColor(.secondary) + } + if let paceStatus { + Text(paceStatus.statusText) + .font(.caption2.weight(.semibold)) + .foregroundColor(paceStatus.isBehind ? .appWarning : .positiveGreen) } } - - GoalProgressBar(progress: progressProvider(goal), tint: .appSecondary, iconColor: .appSecondary) - - HStack { - Text(currentValue.compactCurrencyString) - .font(.caption.weight(.semibold)) - Spacer() - Text("of \(goal.targetDecimal.compactCurrencyString)") + Spacer() + Button { + GoalShareService.shared.shareGoal( + name: goal.name, + progress: progressProvider(goal), + currentValue: currentValue, + targetValue: goal.targetDecimal + ) + } label: { + Image(systemName: "square.and.arrow.up") .font(.caption) - .foregroundColor(.secondary) - } - - if let targetDate = goal.targetDate { - Text("Target: \(targetDate.mediumDateString)") - .font(.caption2.weight(.semibold)) - .foregroundColor(targetUrgency == .critical ? .negativeRed : (targetUrgency == .warning ? .appWarning : .secondary)) - } - - if let etaText = etaProvider(goal) { - Text(etaText) - .font(.caption2) - .foregroundColor(.secondary) - } - - if let paceStatus { - Text(paceStatus.statusText) - .font(.caption2.weight(.semibold)) - .foregroundColor(paceStatus.isBehind ? .negativeRed : .positiveGreen) + .foregroundColor(.appPrimary) } } } diff --git a/PortfolioJournal/Views/Goals/GoalsView.swift b/PortfolioJournal/Views/Goals/GoalsView.swift index fe721ce..8c9c51b 100644 --- a/PortfolioJournal/Views/Goals/GoalsView.swift +++ b/PortfolioJournal/Views/Goals/GoalsView.swift @@ -244,62 +244,61 @@ struct GoalRowView: View { } } + /// Semantic ring color: green when achieved or ahead, orange when behind, + /// accent while simply on the way. + private var ringTint: Color { + if isAchieved { return .appSuccess } + if paceStatus?.isBehind == true { return .appWarning } + return .appPrimary + } + var body: some View { ZStack(alignment: .topTrailing) { Button(action: onEdit) { - VStack(alignment: .leading, spacing: 12) { - HStack { - Text(goal.name) - .font(.headline) - .foregroundColor(isAchieved ? .appSecondary : .primary) - if isAchieved { - Text("Achieved") - .font(.caption2.weight(.bold)) - .foregroundColor(.white) - .padding(.horizontal, 8) - .padding(.vertical, 4) - .background(Color.appSecondary) - .clipShape(Capsule()) + HStack(spacing: 16) { + ProgressRing(progress: progress, tint: ringTint) + + VStack(alignment: .leading, spacing: 4) { + HStack(spacing: 8) { + Text(goal.name) + .font(.headline) + if isAchieved { + Text("Achieved") + .font(.caption2.weight(.bold)) + .foregroundColor(.white) + .padding(.horizontal, 8) + .padding(.vertical, 3) + .background(Color.appSuccess) + .clipShape(Capsule()) + } + } + + HStack(spacing: 4) { + Text(totalValue.currencyString) + .font(.subheadline.weight(.semibold)) + Text("of \(goal.targetDecimal.currencyString)") + .font(.subheadline) + .foregroundColor(.secondary) + } + + if let targetDate = goal.targetDate { + Text("Target date: \(targetDate.mediumDateString)") + .font(.caption) + .foregroundColor(targetDateColor) + } + + if let paceStatus { + Text(paceStatus.statusText) + .font(.caption.weight(.semibold)) + .foregroundColor( + isAchieved ? .appSuccess : (paceStatus.isBehind ? .appWarning : .positiveGreen) + ) } } - - GoalProgressBar( - progress: progress, - tint: isAchieved ? .appSuccess : .appSecondary, - iconColor: isAchieved ? .appSuccess : .appSecondary - ) - - HStack { - Text(totalValue.currencyString) - .font(.subheadline.weight(.semibold)) - Spacer() - Text("of \(goal.targetDecimal.currencyString)") - .font(.subheadline) - .foregroundColor(.secondary) - } - - if let targetDate = goal.targetDate { - Text("Target date: \(targetDate.mediumDateString)") - .font(.caption) - .foregroundColor(targetDateColor) - } - - if let paceStatus { - Text(paceStatus.statusText) - .font(.caption.weight(.semibold)) - .foregroundColor( - isAchieved ? .appSuccess : (paceStatus.isBehind ? .negativeRed : .positiveGreen) - ) - } + Spacer(minLength: 0) } .frame(maxWidth: .infinity, alignment: .leading) - .padding(12) - .background(isAchieved ? Color.appSuccess.opacity(0.10) : Color.clear) - .overlay( - RoundedRectangle(cornerRadius: 12) - .stroke(isAchieved ? Color.appSuccess.opacity(0.35) : Color.clear, lineWidth: 1) - ) - .cornerRadius(12) + .padding(.vertical, 8) } .buttonStyle(.plain) diff --git a/PortfolioJournal/Views/Journal/JournalView.swift b/PortfolioJournal/Views/Journal/JournalView.swift index b25c8c0..3fcde77 100644 --- a/PortfolioJournal/Views/Journal/JournalView.swift +++ b/PortfolioJournal/Views/Journal/JournalView.swift @@ -63,6 +63,17 @@ struct JournalView: View { AppBackground() List { + // 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() + } + .listRowInsets(EdgeInsets()) + .listRowBackground(Color.clear) + .listSectionSeparator(.hidden) + } + Section("Monthly Check-ins") { if filteredMonthlyNotes.isEmpty { if searchText.isEmpty { @@ -183,36 +194,42 @@ struct JournalView: View { } } + /// Timeline-style row: the mood is the visual anchor of each month. private func monthlyNoteRow(_ entry: MonthlyNoteItem) -> some View { - VStack(alignment: .leading, spacing: 8) { - HStack { - Text(entry.date.monthYearString) - .font(.subheadline.weight(.semibold)) - Spacer() + HStack(alignment: .top, spacing: 12) { + ZStack { + Circle() + .fill((entry.mood?.tint ?? Color.secondary).opacity(0.14)) + .frame(width: 40, height: 40) + Image(systemName: entry.mood?.iconName ?? "book.closed") + .font(.system(size: 16)) + .foregroundColor(entry.mood?.tint ?? .secondary) + } + + VStack(alignment: .leading, spacing: 4) { + HStack { + Text(entry.date.monthYearString) + .font(.subheadline.weight(.semibold)) + Spacer() + if entry.rating != nil { + starRow(rating: entry.rating) + .font(.caption2) + } + } + if let mood = entry.mood { - moodPill(mood) - } else { - Text("Mood not set") - .font(.caption) - .foregroundColor(.secondary) + Text(mood.title) + .font(.caption.weight(.medium)) + .foregroundColor(mood.tint) } - } - HStack(spacing: 6) { - starRow(rating: entry.rating) - if entry.rating == nil { - Text("No rating") - .font(.caption) - .foregroundColor(.secondary) - } + Text(entry.note.isEmpty ? String(localized: "journal_no_note") : entry.note) + .font(.subheadline) + .foregroundColor(.secondary) + .lineLimit(2) } - - Text(entry.note.isEmpty ? "No note yet." : entry.note) - .font(.subheadline) - .foregroundColor(.secondary) - .lineLimit(2) } - .padding(.vertical, 4) + .padding(.vertical, 6) } private func starRow(rating: Int?) -> some View { @@ -339,6 +356,19 @@ struct MonthlyNoteEditorView: View { } } +extension MonthlyCheckInMood { + /// Semantic tint per mood — the timeline's color language. + var tint: Color { + switch self { + case .energized: return .orange + case .confident: return .positiveGreen + case .balanced: return .teal + case .cautious: return .yellow + case .stressed: return .negativeRed + } + } +} + #Preview { NavigationStack { JournalView() diff --git a/PortfolioJournal/Views/Sources/SourceListView.swift b/PortfolioJournal/Views/Sources/SourceListView.swift index 5c4c8bb..ac267c5 100644 --- a/PortfolioJournal/Views/Sources/SourceListView.swift +++ b/PortfolioJournal/Views/Sources/SourceListView.swift @@ -503,9 +503,13 @@ struct SourceRowView: View { .foregroundColor(.secondary) if source.needsUpdate { - Label("Needs update", systemImage: "bell.badge.fill") - .font(.caption2) + Label("Needs update", systemImage: "clock.fill") + .font(.caption2.weight(.semibold)) .foregroundColor(.appWarning) + .padding(.horizontal, 8) + .padding(.vertical, 3) + .background(Color.appWarning.opacity(0.12)) + .clipShape(Capsule()) } } } @@ -515,7 +519,7 @@ struct SourceRowView: View { // Value VStack(alignment: .trailing, spacing: 4) { Text(source.latestValue.compactCurrencyString) - .font(.subheadline.weight(.semibold)) + .font(.system(.subheadline, design: .rounded).weight(.semibold)) HStack(spacing: 2) { Image(systemName: source.totalReturn >= 0 ? "arrow.up.right" : "arrow.down.right")