From c1114749a4116c1a3a23f996ff362e1fa5e806a3 Mon Sep 17 00:00:00 2001 From: Belli Date: Sat, 15 Aug 2026 22:03:38 -0700 Subject: [PATCH 1/8] chore(strings): catalog re-sorted by Xcode, extracts the percent value --- Savely/Resources/Localizable.xcstrings | 765 +++++++++++++------------ 1 file changed, 384 insertions(+), 381 deletions(-) diff --git a/Savely/Resources/Localizable.xcstrings b/Savely/Resources/Localizable.xcstrings index 82de2d0..8417589 100755 --- a/Savely/Resources/Localizable.xcstrings +++ b/Savely/Resources/Localizable.xcstrings @@ -54,6 +54,9 @@ } } } + }, + "%lld percent" : { + }, "%lld%%" : { @@ -63,348 +66,6 @@ }, "+$" : { - }, - "all_achievements_unlocked_label" : { - "comment" : "Shown when nothing is left to unlock", - "extractionState" : "extracted_with_value", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Every achievement unlocked" - } - }, - "es-419" : { - "stringUnit" : { - "state" : "translated", - "value" : "Todos los logros desbloqueados" - } - } - } - }, - "cancel_button" : { - "comment" : "Cancel Button", - "extractionState" : "extracted_with_value", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Cancel" - } - }, - "es-419" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cancelar" - } - } - } - }, - "data_privacy_title" : { - "comment" : "Section header", - "extractionState" : "extracted_with_value", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Data & privacy" - } - }, - "es-419" : { - "stringUnit" : { - "state" : "translated", - "value" : "Datos y privacidad" - } - } - } - }, - "data_stays_local_label" : { - "comment" : "One-line intro of the Data & privacy section", - "extractionState" : "extracted_with_value", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Your data never leaves this iPhone." - } - }, - "es-419" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tus datos nunca salen de este iPhone." - } - } - } - }, - "delete_all_data_confirm_button" : { - "comment" : "Destructive confirm button", - "extractionState" : "extracted_with_value", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Delete everything" - } - }, - "es-419" : { - "stringUnit" : { - "state" : "translated", - "value" : "Borrar todo" - } - } - } - }, - "delete_all_data_confirm_message" : { - "comment" : "Confirmation dialog message", - "extractionState" : "extracted_with_value", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Every income, expense and goal will be removed from this iPhone. Your name and reminder settings stay. This cannot be undone." - } - }, - "es-419" : { - "stringUnit" : { - "state" : "translated", - "value" : "Se eliminarán todos los ingresos, gastos y metas de este iPhone. Tu nombre y tus recordatorios se conservan. Esto no se puede deshacer." - } - } - } - }, - "delete_all_data_confirm_title" : { - "comment" : "Confirmation dialog title", - "extractionState" : "extracted_with_value", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Delete all data?" - } - }, - "es-419" : { - "stringUnit" : { - "state" : "translated", - "value" : "¿Borrar todos los datos?" - } - } - } - }, - "delete_all_data_label" : { - "comment" : "Destructive row", - "extractionState" : "extracted_with_value", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Delete all data" - } - }, - "es-419" : { - "stringUnit" : { - "state" : "translated", - "value" : "Borrar todos los datos" - } - } - } - }, - "delete_data_failed_message" : { - "comment" : "Alert when deletion fails", - "extractionState" : "extracted_with_value", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Couldn't delete your data. Please try again." - } - }, - "es-419" : { - "stringUnit" : { - "state" : "translated", - "value" : "No se pudieron borrar tus datos. Inténtalo de nuevo." - } - } - } - }, - "edit_name_hint" : { - "comment" : "Accessibility hint on the identity card", - "extractionState" : "extracted_with_value", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Tap to edit your name" - } - }, - "es-419" : { - "stringUnit" : { - "state" : "translated", - "value" : "Toca para editar tu nombre" - } - } - } - }, - "just_started_label" : { - "comment" : "Subtitle under the display name when nothing is logged yet", - "extractionState" : "extracted_with_value", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Just getting started" - } - }, - "es-419" : { - "stringUnit" : { - "state" : "translated", - "value" : "Apenas empezando" - } - } - } - }, - "next_achievement_label" : { - "comment" : "Kicker above the closest locked achievement", - "extractionState" : "extracted_with_value", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Next up" - } - }, - "es-419" : { - "stringUnit" : { - "state" : "translated", - "value" : "Siguiente" - } - } - } - }, - "notifications_denied_hint" : { - "comment" : "Shown instead of the reminder switches when permission is denied", - "extractionState" : "extracted_with_value", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Notifications are off for Savely in iOS Settings." - } - }, - "es-419" : { - "stringUnit" : { - "state" : "translated", - "value" : "Las notificaciones de Savely están desactivadas en Ajustes de iOS." - } - } - } - }, - "open_settings_button" : { - "comment" : "Button that opens the iOS Settings app", - "extractionState" : "extracted_with_value", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Open Settings" - } - }, - "es-419" : { - "stringUnit" : { - "state" : "translated", - "value" : "Abrir Ajustes" - } - } - } - }, - "reminder_time_label" : { - "comment" : "Label next to the reminder time picker", - "extractionState" : "extracted_with_value", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Time" - } - }, - "es-419" : { - "stringUnit" : { - "state" : "translated", - "value" : "Hora" - } - } - } - }, - "saving_since_label" : { - "comment" : "Subtitle under the display name; %@ is a month and year", - "extractionState" : "extracted_with_value", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Saving since %@" - } - }, - "es-419" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ahorrando desde %@" - } - } - } - }, - "stat_active_goals_label" : { - "comment" : "Stat cell: goals not yet complete", - "extractionState" : "extracted_with_value", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Active goals" - } - }, - "es-419" : { - "stringUnit" : { - "state" : "translated", - "value" : "Metas activas" - } - } - } - }, - "stat_movements_label" : { - "comment" : "Stat cell: number of incomes + expenses logged", - "extractionState" : "extracted_with_value", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Movements" - } - }, - "es-419" : { - "stringUnit" : { - "state" : "translated", - "value" : "Movimientos" - } - } - } - }, - "stat_saved_label" : { - "comment" : "Stat cell: total saved across goals", - "extractionState" : "extracted_with_value", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Saved" - } - }, - "es-419" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ahorrado" - } - } - } }, "~$%lld/mo" : { @@ -576,6 +237,24 @@ } } }, + "all_achievements_unlocked_label" : { + "comment" : "Shown when nothing is left to unlock", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Every achievement unlocked" + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Todos los logros desbloqueados" + } + } + } + }, "ALMOST THERE" : { }, @@ -671,6 +350,24 @@ }, "Cancel" : { + }, + "cancel_button" : { + "comment" : "Cancel Button", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Cancel" + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cancelar" + } + } + } }, "category_placeholder" : { "comment" : "Category Placeholder", @@ -838,115 +535,241 @@ "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "¡Crea tú cuenta!" + "value" : "¡Crea tú cuenta!" + } + } + } + }, + "creating_account_label" : { + "comment" : "Creating Account Label", + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "By creating an account, I accept Savely's" + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Al crear una cuenta, acepto los" + } + } + } + }, + "dark_mode_label" : { + "comment" : "Dark Mode Label", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Dark Mode" + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Modo Oscuro" + } + } + } + }, + "dashboard_tab_string" : { + "comment" : "Dashboard Tab String", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Dashboard" + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Panel" + } + } + } + }, + "data_privacy_title" : { + "comment" : "Section header", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Data & privacy" + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Datos y privacidad" + } + } + } + }, + "data_stays_local_label" : { + "comment" : "One-line intro of the Data & privacy section", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Your data never leaves this iPhone." + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tus datos nunca salen de este iPhone." + } + } + } + }, + "Day one" : { + + }, + "debt_paid_off_description" : { + "comment" : "Description for paying off debt", + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "You have paid off a major debt." + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ha pagado una deuda importante." + } + } + } + }, + "debt_paid_off_title" : { + "comment" : "Title for paying off debt", + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Debt Paid Off" + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deuda Saldada" } } } }, - "creating_account_label" : { - "comment" : "Creating Account Label", - "extractionState" : "stale", + "Delete" : { + + }, + "Delete Expense" : { + + }, + "Delete Goal" : { + + }, + "Delete Income" : { + + }, + "delete_all_data_confirm_button" : { + "comment" : "Destructive confirm button", + "extractionState" : "extracted_with_value", "localizations" : { "en" : { "stringUnit" : { "state" : "new", - "value" : "By creating an account, I accept Savely's" + "value" : "Delete everything" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Al crear una cuenta, acepto los" + "value" : "Borrar todo" } } } }, - "dark_mode_label" : { - "comment" : "Dark Mode Label", + "delete_all_data_confirm_message" : { + "comment" : "Confirmation dialog message", "extractionState" : "extracted_with_value", "localizations" : { "en" : { "stringUnit" : { "state" : "new", - "value" : "Dark Mode" + "value" : "Every income, expense and goal will be removed from this iPhone. Your name and reminder settings stay. This cannot be undone." } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Modo Oscuro" + "value" : "Se eliminarán todos los ingresos, gastos y metas de este iPhone. Tu nombre y tus recordatorios se conservan. Esto no se puede deshacer." } } } }, - "dashboard_tab_string" : { - "comment" : "Dashboard Tab String", + "delete_all_data_confirm_title" : { + "comment" : "Confirmation dialog title", "extractionState" : "extracted_with_value", "localizations" : { "en" : { "stringUnit" : { "state" : "new", - "value" : "Dashboard" + "value" : "Delete all data?" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Panel" + "value" : "¿Borrar todos los datos?" } } } }, - "Day one" : { - - }, - "debt_paid_off_description" : { - "comment" : "Description for paying off debt", - "extractionState" : "stale", + "delete_all_data_label" : { + "comment" : "Destructive row", + "extractionState" : "extracted_with_value", "localizations" : { "en" : { "stringUnit" : { "state" : "new", - "value" : "You have paid off a major debt." + "value" : "Delete all data" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Ha pagado una deuda importante." + "value" : "Borrar todos los datos" } } } }, - "debt_paid_off_title" : { - "comment" : "Title for paying off debt", - "extractionState" : "stale", + "delete_data_failed_message" : { + "comment" : "Alert when deletion fails", + "extractionState" : "extracted_with_value", "localizations" : { "en" : { "stringUnit" : { "state" : "new", - "value" : "Debt Paid Off" + "value" : "Couldn't delete your data. Please try again." } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Deuda Saldada" + "value" : "No se pudieron borrar tus datos. Inténtalo de nuevo." } } } - }, - "Delete" : { - - }, - "Delete Expense" : { - - }, - "Delete Goal" : { - - }, - "Delete Income" : { - }, "description_placeholder_label" : { "comment" : "Description Placeholder Label", @@ -1013,6 +836,24 @@ }, "Edit Profile" : { + }, + "edit_name_hint" : { + "comment" : "Accessibility hint on the identity card", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Tap to edit your name" + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Toca para editar tu nombre" + } + } + } }, "email_placeholder_title" : { "comment" : "Email Placeholder Label", @@ -1659,6 +1500,24 @@ } } }, + "just_started_label" : { + "comment" : "Subtitle under the display name when nothing is logged yet", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Just getting started" + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apenas empezando" + } + } + } + }, "LIFETIME INCOME" : { }, @@ -1814,6 +1673,24 @@ }, "New goal · " : { + }, + "next_achievement_label" : { + "comment" : "Kicker above the closest locked achievement", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Next up" + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Siguiente" + } + } + } }, "next_button" : { "comment" : "Next Button", @@ -2001,6 +1878,24 @@ } } }, + "notifications_denied_hint" : { + "comment" : "Shown instead of the reminder switches when permission is denied", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Notifications are off for Savely in iOS Settings." + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Las notificaciones de Savely están desactivadas en Ajustes de iOS." + } + } + } + }, "notifications_title" : { "comment" : "Notifications Title", "extractionState" : "extracted_with_value", @@ -2067,6 +1962,24 @@ } } }, + "open_settings_button" : { + "comment" : "Button that opens the iOS Settings app", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Open Settings" + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Abrir Ajustes" + } + } + } + }, "Optional note" : { }, @@ -2358,6 +2271,24 @@ } } }, + "reminder_time_label" : { + "comment" : "Label next to the reminder time picker", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Time" + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hora" + } + } + } + }, "reports_tab_string" : { "comment" : "Reports Tab String", "extractionState" : "extracted_with_value", @@ -2424,6 +2355,24 @@ } } }, + "saving_since_label" : { + "comment" : "Subtitle under the display name; %@ is a month and year", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Saving since %@" + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ahorrando desde %@" + } + } + } + }, "savings_placeholder" : { "comment" : "Savings Placeholcer\nSavings Placeholder", "extractionState" : "extracted_with_value", @@ -2763,6 +2712,60 @@ } } }, + "stat_active_goals_label" : { + "comment" : "Stat cell: goals not yet complete", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Active goals" + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Metas activas" + } + } + } + }, + "stat_movements_label" : { + "comment" : "Stat cell: number of incomes + expenses logged", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Movements" + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Movimientos" + } + } + } + }, + "stat_saved_label" : { + "comment" : "Stat cell: total saved across goals", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Saved" + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ahorrado" + } + } + } + }, "subscription_cleanup_description" : { "comment" : "Description for canceling unused subscriptions", "extractionState" : "stale", From 0d060ac4399f0f5af4857cd46d5ecaac388cfa5a Mon Sep 17 00:00:00 2001 From: Belli Date: Sat, 15 Aug 2026 22:05:19 -0700 Subject: [PATCH 2/8] refactor: drop the placeholder "User" identity There is no account, so the monogram + "User" card on Profile and the person.fill circle in the Dashboard greeting stood in for an identity the app deliberately does not have. - Profile: identity card removed; its one useful line ("Saving since " / "Just getting started") is now the lifetime income card's subtitle. - Dashboard: avatar circle removed from the greeting. - Display-name plumbing deleted since nothing reaches it anymore: EditProfileSheet, ProfileViewModel.displayName / updatePersonalInformation(), AppViewModel.currentUserName / updateDisplayName(), the "displayName" UserDefaults key, and the orphaned InnerHeightPreferenceKey. Strings.Profile.editNameHint and its catalog entry go with them. --- Savely/Resources/Localizable.xcstrings | 18 ---- Savely/Resources/Strings.swift | 4 - Savely/ViewModels/AppViewModel.swift | 11 +-- .../ProfileTab/ProfileViewModel.swift | 10 --- Savely/Views/DashboardTab/DashboardView.swift | 29 +++---- Savely/Views/ProfileTab/ProfileView.swift | 82 +------------------ 6 files changed, 16 insertions(+), 138 deletions(-) diff --git a/Savely/Resources/Localizable.xcstrings b/Savely/Resources/Localizable.xcstrings index 8417589..752565d 100755 --- a/Savely/Resources/Localizable.xcstrings +++ b/Savely/Resources/Localizable.xcstrings @@ -836,24 +836,6 @@ }, "Edit Profile" : { - }, - "edit_name_hint" : { - "comment" : "Accessibility hint on the identity card", - "extractionState" : "extracted_with_value", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Tap to edit your name" - } - }, - "es-419" : { - "stringUnit" : { - "state" : "translated", - "value" : "Toca para editar tu nombre" - } - } - } }, "email_placeholder_title" : { "comment" : "Email Placeholder Label", diff --git a/Savely/Resources/Strings.swift b/Savely/Resources/Strings.swift index e1bf57e..a3c525e 100755 --- a/Savely/Resources/Strings.swift +++ b/Savely/Resources/Strings.swift @@ -277,10 +277,6 @@ struct Strings { "just_started_label", value: "Just getting started", comment: "Subtitle under the display name when nothing is logged yet") - static let editNameHint = NSLocalizedString( - "edit_name_hint", - value: "Tap to edit your name", - comment: "Accessibility hint on the identity card") static let statSavedLabel = NSLocalizedString( "stat_saved_label", value: "Saved", diff --git a/Savely/ViewModels/AppViewModel.swift b/Savely/ViewModels/AppViewModel.swift index 8fd2c44..bb12dce 100644 --- a/Savely/ViewModels/AppViewModel.swift +++ b/Savely/ViewModels/AppViewModel.swift @@ -9,11 +9,10 @@ import Foundation import Network /// App-level state. Local-first since the auth removal (2026-08): there is -/// no account — the app goes straight from launch to onboarding (first run) -/// or the main tabs. Profile data (display name) lives in UserDefaults. +/// no account and no profile — the app goes straight from launch to +/// onboarding (first run) or the main tabs. class AppViewModel: ObservableObject { @Published var isOnboardingComplete: Bool - @Published var currentUserName: String @Published var showNetworkWarning: Bool = false // For modal warning about limited features private let monitor = NWPathMonitor() @@ -25,7 +24,6 @@ class AppViewModel: ObservableObject { init() { self.isOnboardingComplete = UserDefaults.standard.bool(forKey: "isOnboardingComplete") - self.currentUserName = UserDefaults.standard.string(forKey: "displayName") ?? "" self.startNetworkMonitoring() } @@ -38,11 +36,6 @@ class AppViewModel: ObservableObject { UserDefaults.standard.set(true, forKey: "isOnboardingComplete") } - func updateDisplayName(_ name: String) { - currentUserName = name - UserDefaults.standard.set(name, forKey: "displayName") - } - private func startNetworkMonitoring() { monitor.pathUpdateHandler = { path in DispatchQueue.main.async { diff --git a/Savely/ViewModels/ProfileTab/ProfileViewModel.swift b/Savely/ViewModels/ProfileTab/ProfileViewModel.swift index 08fc683..e255bd6 100644 --- a/Savely/ViewModels/ProfileTab/ProfileViewModel.swift +++ b/Savely/ViewModels/ProfileTab/ProfileViewModel.swift @@ -14,7 +14,6 @@ import SwiftData @MainActor class ProfileViewModel: ObservableObject { - @Published var displayName: String = "" @AppStorage("darkModeEnabled") var darkMode: Bool = false /// The persisted reminder choices. Mutate through the `setReminder…` @@ -41,7 +40,6 @@ class ProfileViewModel: ObservableObject { self.reminderStore = reminderStore self.notifications = notifications self.reminders = reminderStore.load() - self.displayName = UserDefaults.standard.string(forKey: "displayName") ?? "" if modelContext != nil { fetchWeeklyReportData( startDate: Calendar.current.startOfWeek(for: Date()), @@ -242,12 +240,4 @@ class ProfileViewModel: ObservableObject { } } - - /// Local-only since the auth removal: the display name lives in - /// UserDefaults ("displayName", shared with AppViewModel), nowhere else. - func updatePersonalInformation() { - UserDefaults.standard.set(displayName, forKey: "displayName") - alertMessage = "Your personal information has been updated successfully." - showAlert = true - } } diff --git a/Savely/Views/DashboardTab/DashboardView.swift b/Savely/Views/DashboardTab/DashboardView.swift index 6765205..d0d4e8f 100755 --- a/Savely/Views/DashboardTab/DashboardView.swift +++ b/Savely/Views/DashboardTab/DashboardView.swift @@ -52,26 +52,17 @@ struct DashboardView: View { var body: some View { ScrollView { VStack(spacing: 16) { - // — Greeting — - HStack(alignment: .center) { - VStack(alignment: .leading, spacing: 2) { - Text(formattedDate) - .font(.system(size: 13)) - .foregroundStyle(Color.warmInkMuted) - Text(greeting + ".") - .font(.system(size: 30, weight: .regular, design: .serif)) - .foregroundStyle(Color.warmInk) - } - Spacer() - Circle() - .fill(Color.warmAmberSoft) - .frame(width: 40, height: 40) - .overlay( - Image(systemName: "person.fill") - .font(.system(size: 18)) - .foregroundStyle(Color.warmAmber) - ) + // — Greeting — (no avatar: there is no account, so no + // person to represent) + VStack(alignment: .leading, spacing: 2) { + Text(formattedDate) + .font(.system(size: 13)) + .foregroundStyle(Color.warmInkMuted) + Text(greeting + ".") + .font(.system(size: 30, weight: .regular, design: .serif)) + .foregroundStyle(Color.warmInk) } + .frame(maxWidth: .infinity, alignment: .leading) .padding(.top, 8) // — Hero Goal Card — diff --git a/Savely/Views/ProfileTab/ProfileView.swift b/Savely/Views/ProfileTab/ProfileView.swift index 787d57e..c7d3bc2 100755 --- a/Savely/Views/ProfileTab/ProfileView.swift +++ b/Savely/Views/ProfileTab/ProfileView.swift @@ -9,20 +9,16 @@ struct ProfileView: View { @Query(sort: \IncomeModel.date, order: .reverse) private var incomes: [IncomeModel] @Query private var expenses: [ExpenseModel] @Query private var goals: [GoalModel] - @State private var showingEditProfile = false @State private var showingAchievements = false @State private var showingTipHistory = false @State private var showingDeleteDialog = false @State private var showingDeleteFinalConfirm = false private var lifetimeIncome: Double { incomes.reduce(0) { $0 + $1.amount } } - private var displayInitial: String { - viewModel.displayName.first.map { String($0).uppercased() } ?? "U" - } // "Saving since " — the first movement ever logged. Nothing // logged yet is a deliberate state, not an empty label. - private var identitySubtitle: String { + private var savingSinceSubtitle: String { let firstDate = (incomes.map(\.date) + expenses.map(\.date)).min() guard let firstDate else { return Strings.Profile.justStartedLabel } let month = firstDate.formatted(.dateTime.month(.wide).year()) @@ -36,10 +32,8 @@ struct ProfileView: View { var body: some View { ScrollView { VStack(spacing: 14) { - identityCard - .padding(.top, 8) - lifetimeIncomeCard + .padding(.top, 8) statsStrip @@ -82,53 +76,13 @@ struct ProfileView: View { } message: { Text(Strings.Profile.deleteAllDataConfirmMessage) } - .sheet(isPresented: $showingEditProfile) { EditProfileSheet(viewModel: viewModel) } .navigationDestination(isPresented: $showingAchievements) { AchievementsView() } .navigationDestination(isPresented: $showingTipHistory) { TipHistoryView() } .onAppear { viewModel.setModelContext(modelContext) } .task { await viewModel.refreshReminderState() } } - // MARK: - Identity - - private var identityCard: some View { - Button(action: { showingEditProfile = true }) { - HStack(spacing: 16) { - RoundedRectangle(cornerRadius: 20) - .fill(Color.warmAmberSoft) - .frame(width: 64, height: 64) - .overlay( - Text(displayInitial) - .font(.system(size: 30, weight: .regular, design: .serif)) - .foregroundStyle(Color.warmAmber) - ) - .accessibilityHidden(true) - - VStack(alignment: .leading, spacing: 3) { - Text(viewModel.displayName.isEmpty ? "User" : viewModel.displayName) - .font(.system(size: 22, weight: .regular, design: .serif)) - .foregroundStyle(Color.warmInk) - Text(identitySubtitle) - .font(.system(size: 13)) - .foregroundStyle(Color.warmInkSoft) - } - Spacer() - Image(systemName: "chevron.right") - .font(.system(size: 13)) - .foregroundStyle(Color.warmInkMuted) - .accessibilityHidden(true) - } - .padding(20) - .background(Color.warmSurface) - .cornerRadius(22) - .overlay(RoundedRectangle(cornerRadius: 22).stroke(Color.warmLine, lineWidth: 1)) - .contentShape(Rectangle()) - } - .buttonStyle(.plain) - .accessibilityHint(Strings.Profile.editNameHint) - } - - // MARK: - Lifetime income (unchanged) + // MARK: - Lifetime income private var lifetimeIncomeCard: some View { ZStack(alignment: .topTrailing) { @@ -145,7 +99,7 @@ struct ProfileView: View { Text(formattedAmount(lifetimeIncome)) .font(.system(size: 40, weight: .regular, design: .serif)) .foregroundStyle(.white) - Text("Total income logged in Savely") + Text(savingSinceSubtitle) .font(.system(size: 13)) .foregroundStyle(Color.white.opacity(0.75)) } @@ -532,34 +486,6 @@ struct SettingsNavRow: View { } } -// MARK: - Edit Profile Sheet (unchanged) - -struct EditProfileSheet: View { - @ObservedObject var viewModel: ProfileViewModel - @Environment(\.dismiss) private var dismiss - var body: some View { - NavigationView { - Form { - Section { - TextField("Display Name", text: $viewModel.displayName) - } - } - .navigationTitle("Edit Profile").navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .cancellationAction) { Button("Cancel") { dismiss() } } - ToolbarItem(placement: .confirmationAction) { - Button("Save") { viewModel.updatePersonalInformation(); dismiss() }.fontWeight(.semibold) - } - } - } - } -} - -struct InnerHeightPreferenceKey: PreferenceKey { - static let defaultValue: CGFloat = .zero - static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { value = nextValue() } -} - #Preview { NavigationStack { ProfileView() } .environmentObject(AppViewModel()) From 48ed597ee53982ac9565c860f5a80b58a036004e Mon Sep 17 00:00:00 2001 From: Belli Date: Sat, 15 Aug 2026 22:07:00 -0700 Subject: [PATCH 3/8] feat(palette): trait-aware Warm Meadow tokens with a dark value each Every Color.warm* token now resolves per trait collection through a UIColor dynamic provider, so call sites never branch on colorScheme. Light values are unchanged; dark values are the same hues at low chroma ("the notebook under a lamp"), verified against WCAG AA in docs/plans/pr-d2-adaptive-palette.md. New role tokens for what call sites used to hardcode: warmGreenFill (green as a surface, stays deep in dark so white content keeps 5:1), warmOnGreen, warmOnInk (ink flips light in dark, so its content flips to the ground), warmTrack, warmShadow, warmLilac/warmLilacSoft. No call sites change in this commit. --- Savely/Resources/Color+Warm.swift | 87 +++++++++++++++++++++++++------ 1 file changed, 70 insertions(+), 17 deletions(-) diff --git a/Savely/Resources/Color+Warm.swift b/Savely/Resources/Color+Warm.swift index 345ca1f..c30d84e 100644 --- a/Savely/Resources/Color+Warm.swift +++ b/Savely/Resources/Color+Warm.swift @@ -1,22 +1,75 @@ import SwiftUI +import UIKit // MARK: - Warm Meadow design system palette +// +// Every color in the app comes from here. Each token carries a light and a +// dark value; SwiftUI resolves them per trait collection, so call sites +// never branch on `colorScheme`. Dark is the same notebook under a lamp — +// same hues, low chroma, one green — not a neutral-gray inversion. The +// light values are the ones documented in DESIGN.md; the dark values and +// their AA contrast checks live in docs/plans/pr-d2-adaptive-palette.md. extension Color { - static let warmBg = Color(red: 0.965, green: 0.957, blue: 0.933) // #f6f4ee - static let warmSurface = Color.white - static let warmInk = Color(red: 0.102, green: 0.102, blue: 0.090) // #1a1a17 - static let warmInkSoft = Color(red: 0.333, green: 0.322, blue: 0.298) // #55524c - static let warmInkMuted = Color(red: 0.549, green: 0.533, blue: 0.502) // #8c8880 - static let warmLine = Color(red: 0.118, green: 0.098, blue: 0.059).opacity(0.08) - static let warmLineSoft = Color(red: 0.118, green: 0.098, blue: 0.059).opacity(0.04) - static let warmGreen = Color(red: 0.184, green: 0.420, blue: 0.290) // #2f6b4a - static let warmGreenDeep = Color(red: 0.122, green: 0.290, blue: 0.200) // #1f4a33 - static let warmGreenSoft = Color(red: 0.910, green: 0.941, blue: 0.918) // #e8f0ea - static let warmGreenTint = Color(red: 0.949, green: 0.969, blue: 0.953) // #f2f7f3 - static let warmAmber = Color(red: 0.769, green: 0.541, blue: 0.165) // #c48a2a - static let warmAmberSoft = Color(red: 0.965, green: 0.925, blue: 0.839) // #f6ecd6 - static let warmClay = Color(red: 0.722, green: 0.361, blue: 0.259) // #b85c42 - static let warmClaySoft = Color(red: 0.965, green: 0.882, blue: 0.847) // #f6e1d8 - static let warmSky = Color(red: 0.290, green: 0.482, blue: 0.651) // #4a7ba6 - static let warmSkySoft = Color(red: 0.863, green: 0.910, blue: 0.949) // #dce8f2 + // Ground & ink + static let warmBg = Color(light: 0xF6F4EE, dark: 0x161512) + static let warmSurface = Color(light: 0xFFFFFF, dark: 0x262420) + static let warmInk = Color(light: 0x1A1A17, dark: 0xF2EFE7) + static let warmInkSoft = Color(light: 0x55524C, dark: 0xC6C1B5) + static let warmInkMuted = Color(light: 0x8C8880, dark: 0x9D978A) + static let warmLine = Color(light: 0x1E190F, dark: 0xF2EFE7, lightAlpha: 0.08, darkAlpha: 0.10) + static let warmLineSoft = Color(light: 0x1E190F, dark: 0xF2EFE7, lightAlpha: 0.04, darkAlpha: 0.05) + + // The one green. `warmGreen` is green *as ink* (text, glyphs, rings, + // bars, toggles) and brightens in dark; `warmGreenFill` is green *as a + // surface* (CTAs, the income card, the selected radio) that carries + // white content, and stays deep in both schemes. + static let warmGreen = Color(light: 0x2F6B4A, dark: 0x78B58F) + static let warmGreenFill = Color(light: 0x2F6B4A, dark: 0x357A55) + static let warmGreenDeep = Color(light: 0x1F4A33, dark: 0xB6DCC4) + static let warmGreenSoft = Color(light: 0xE8F0EA, dark: 0x22382C) + static let warmGreenTint = Color(light: 0xF2F7F3, dark: 0x1B2A21) + + // Category tints — a glyph on a soft tile, never a fill. + static let warmAmber = Color(light: 0xC48A2A, dark: 0xDEA64A) + static let warmAmberSoft = Color(light: 0xF6ECD6, dark: 0x3A2F19) + static let warmClay = Color(light: 0xB85C42, dark: 0xD9846A) + static let warmClaySoft = Color(light: 0xF6E1D8, dark: 0x3D2721) + static let warmSky = Color(light: 0x4A7BA6, dark: 0x7EA9D0) + static let warmSkySoft = Color(light: 0xDCE8F2, dark: 0x1E2D3B) + static let warmLilac = Color(light: 0x7D6AB8, dark: 0xB3A4DC) + static let warmLilacSoft = Color(light: 0xECE8F4, dark: 0x2E2740) + + // Roles that used to be hardcoded at call sites. + /// Content drawn on `warmGreenFill` (button labels, the income card copy). + static let warmOnGreen = Color(light: 0xFFFFFF, dark: 0xFFFFFF) + /// Content drawn on a `warmInk` fill (the "+" shell, the scan banner). + /// Ink flips to light in dark mode, so its content flips to the ground. + static let warmOnInk = Color(light: 0xFFFFFF, dark: 0x161512) + /// The goal ring's track and the empty part of progress bars. + static let warmTrack = Color(light: 0xEDE6D4, dark: 0x3A362E) + /// The hero-card lift and the "+" shell shadow — the only shadows by rule. + static let warmShadow = Color(light: 0x000000, dark: 0x000000, lightAlpha: 0.06, darkAlpha: 0.45) +} + +private extension Color { + /// A trait-aware color from two hex values (0xRRGGBB) and per-scheme + /// alphas. Resolved by UIKit at draw time, so it follows the window's + /// `preferredColorScheme` and system appearance without any view code. + init(light: UInt32, dark: UInt32, lightAlpha: CGFloat = 1, darkAlpha: CGFloat = 1) { + self.init(uiColor: UIColor { traits in + let isDark = traits.userInterfaceStyle == .dark + return UIColor(hex: isDark ? dark : light, alpha: isDark ? darkAlpha : lightAlpha) + }) + } +} + +private extension UIColor { + convenience init(hex: UInt32, alpha: CGFloat) { + self.init( + red: CGFloat((hex >> 16) & 0xFF) / 255, + green: CGFloat((hex >> 8) & 0xFF) / 255, + blue: CGFloat(hex & 0xFF) / 255, + alpha: alpha + ) + } } From aac3cdcaf72a120035a41bd8afbaf7215c3157ac Mon Sep 17 00:00:00 2001 From: Belli Date: Sat, 15 Aug 2026 22:14:00 -0700 Subject: [PATCH 4/8] refactor: route every hardcoded color through the Warm Meadow tokens MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The sweep that makes the adaptive palette actually reach the screen. Of the 97 hardcoded color sites outside Color+Warm.swift, every one now resolves through a token; the handful that stay literal are deliberate and say so in a comment (the splash frame, the sprout brand mark, white controls drawn over the live camera feed, and the PDF, which is paper). Semantics, not search-and-replace: - Green *surfaces* (CTAs, the income card, the Planted screen, the selected radio and calendar day) use warmGreenFill; green *as ink* (text, glyphs, rings, bars, toggles, page dots) keeps warmGreen. - White content on those surfaces is warmOnGreen; white on an ink fill (the "+" shell, the scan banner, the Goals "+" buttons) is warmOnInk, which flips to the ground in dark because ink flips light. - The goal ring track is warmTrack; the "New goal" quick action is warmLilac/warmLilacSoft; the amber banner copy is warmAmberDeep. - GoalColor: each of the twelve goal accents and their tracks now carries a dark variant (accents a step lighter, ≥3:1 against the dark surface; tracks a deep tint of the same hue). Light values unchanged. - Shadows: the hero-card lifts and the "+" shell keep theirs through warmShadow / warmShellShadow; the rest were legacy and are gone. - Legacy: the ColorPalette asset catalog (primaryGreen, cardBackground…) is deleted, along with the four unreferenced files that still pointed at it or at nothing (NotificationTimePicker, InsightRow, QuickActionButton, PrimaryButton — dead since the auth removal / PR A) and the unused UIConstants.UIShadow. NetworkErrorView, ReportsView and the camera confirmation card are moved onto the tokens rather than deleted. --- Savely.xcodeproj/project.pbxproj | 20 ---- .../Contents.json | 47 ---------- .../ColorPalette/Contents.json | 6 -- .../PrimaryGreen.colorset/Contents.json | 47 ---------- .../backgroundColor.colorset/Contents.json | 47 ---------- .../Contents.json | 47 ---------- .../primaryBlue.colorset/Contents.json | 47 ---------- .../primaryPurple.colorset/Contents.json | 47 ---------- .../primaryRed.colorset/Contents.json | 47 ---------- .../primaryYellow.colorset/Contents.json | 47 ---------- .../secondaryBlue.colorset/Contents.json | 47 ---------- .../secondaryGreen.colorset/Contents.json | 47 ---------- .../terciaryGreen.colorset/Contents.json | 47 ---------- Savely/Models/GoalModel.swift | 38 ++++---- Savely/Resources/Color+Warm.swift | 10 +- Savely/Resources/UIConstants.swift | 5 - Savely/Utilities/ReportsPDFGenerator.swift | 2 +- .../Components/Buttons/PrimaryButton.swift | 33 ------- .../Buttons/QuickActionButton.swift | 42 --------- .../Components/CameraScanner/CameraView.swift | 7 +- .../CameraScanner/TotalConfirmationView.swift | 8 +- Savely/Views/Components/InsightRow.swift | 91 ------------------- .../Views/Components/NetworkErrorView.swift | 6 +- Savely/Views/Components/SproutMark.swift | 2 +- Savely/Views/DashboardTab/DashboardView.swift | 12 +-- Savely/Views/DashboardTab/ReportsView.swift | 7 +- .../ExpensesTab/ExpenseTrackerView.swift | 14 +-- Savely/Views/GoalsTab/AddGoalFlow.swift | 67 +++++++------- Savely/Views/GoalsTab/GoalDetailView.swift | 4 +- Savely/Views/GoalsTab/GoalsView.swift | 12 +-- .../Views/IncomesTab/IncomesTrackerView.swift | 4 +- Savely/Views/MainNavigationView.swift | 23 +++-- Savely/Views/Onboarding/OnboardingView.swift | 4 +- .../ProfileTab/NotificationTimePicker.swift | 58 ------------ Savely/Views/ProfileTab/ProfileView.swift | 10 +- Savely/Views/SplashScreenView.swift | 3 + 36 files changed, 119 insertions(+), 886 deletions(-) delete mode 100644 Savely/Assets.xcassets/ColorPalette/CardBackgroundColor.colorset/Contents.json delete mode 100644 Savely/Assets.xcassets/ColorPalette/Contents.json delete mode 100644 Savely/Assets.xcassets/ColorPalette/PrimaryGreen.colorset/Contents.json delete mode 100644 Savely/Assets.xcassets/ColorPalette/backgroundColor.colorset/Contents.json delete mode 100644 Savely/Assets.xcassets/ColorPalette/listBackgroundColor.colorset/Contents.json delete mode 100644 Savely/Assets.xcassets/ColorPalette/primaryBlue.colorset/Contents.json delete mode 100644 Savely/Assets.xcassets/ColorPalette/primaryPurple.colorset/Contents.json delete mode 100644 Savely/Assets.xcassets/ColorPalette/primaryRed.colorset/Contents.json delete mode 100644 Savely/Assets.xcassets/ColorPalette/primaryYellow.colorset/Contents.json delete mode 100644 Savely/Assets.xcassets/ColorPalette/secondaryBlue.colorset/Contents.json delete mode 100644 Savely/Assets.xcassets/ColorPalette/secondaryGreen.colorset/Contents.json delete mode 100644 Savely/Assets.xcassets/ColorPalette/terciaryGreen.colorset/Contents.json delete mode 100644 Savely/Views/Components/Buttons/PrimaryButton.swift delete mode 100755 Savely/Views/Components/Buttons/QuickActionButton.swift delete mode 100644 Savely/Views/Components/InsightRow.swift delete mode 100644 Savely/Views/ProfileTab/NotificationTimePicker.swift diff --git a/Savely.xcodeproj/project.pbxproj b/Savely.xcodeproj/project.pbxproj index 1a5292b..11853e9 100644 --- a/Savely.xcodeproj/project.pbxproj +++ b/Savely.xcodeproj/project.pbxproj @@ -61,16 +61,13 @@ 1DE4B4D02CE5F2D6006C8F65 /* OnboardingData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DE4B4CF2CE5F2D6006C8F65 /* OnboardingData.swift */; }; 1DE4B4D42CE60032006C8F65 /* HybridTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DE4B4D32CE60032006C8F65 /* HybridTextField.swift */; }; 1DE4B4D62CE7AF93006C8F65 /* CustomTextfield.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DE4B4D52CE7AF93006C8F65 /* CustomTextfield.swift */; }; - 1DE4B4D82CE7B10E006C8F65 /* PrimaryButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DE4B4D72CE7B10E006C8F65 /* PrimaryButton.swift */; }; 1DE4B4EC2CEC4EE3006C8F65 /* AppViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DE4B4EB2CEC4EE3006C8F65 /* AppViewModel.swift */; }; 1DE4B4EF2CEC514B006C8F65 /* ProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DE4B4EE2CEC514B006C8F65 /* ProfileViewModel.swift */; }; 1DE9FE0E2D063AF20069AA64 /* NotificationSettingsStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DE9FE0D2D063AF20069AA64 /* NotificationSettingsStepView.swift */; }; 1DE9FE102D063BC80069AA64 /* NotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DE9FE0F2D063BC80069AA64 /* NotificationManager.swift */; }; - 1DE9FE122D0650150069AA64 /* NotificationTimePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DE9FE112D0650150069AA64 /* NotificationTimePicker.swift */; }; 1DE9FE152D0664A60069AA64 /* ReportsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DE9FE142D0664A60069AA64 /* ReportsViewModel.swift */; }; 1DE9FE172D066EE20069AA64 /* ReportsPDFGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DE9FE162D066EE20069AA64 /* ReportsPDFGenerator.swift */; }; 1DEACCDE2D0BE6340084CC39 /* NetworkErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DEACCDD2D0BE6340084CC39 /* NetworkErrorView.swift */; }; - 1DF579C52F673A8B009A41B3 /* InsightRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DF579C42F673A8B009A41B3 /* InsightRow.swift */; }; 1DF7228E2F9C182300320B38 /* Color+Warm.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DF7228D2F9C182300320B38 /* Color+Warm.swift */; }; 1DF722902F9C18FE00320B38 /* GoalDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DF7228F2F9C18FE00320B38 /* GoalDetailView.swift */; }; 1DF722922F9C19CE00320B38 /* MoneyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DF722912F9C19CE00320B38 /* MoneyView.swift */; }; @@ -156,17 +153,14 @@ 1DE4B4CF2CE5F2D6006C8F65 /* OnboardingData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingData.swift; sourceTree = ""; }; 1DE4B4D32CE60032006C8F65 /* HybridTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HybridTextField.swift; sourceTree = ""; }; 1DE4B4D52CE7AF93006C8F65 /* CustomTextfield.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTextfield.swift; sourceTree = ""; }; - 1DE4B4D72CE7B10E006C8F65 /* PrimaryButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryButton.swift; sourceTree = ""; }; 1DE4B4EA2CEC037E006C8F65 /* Savely.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Savely.entitlements; sourceTree = ""; }; 1DE4B4EB2CEC4EE3006C8F65 /* AppViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppViewModel.swift; sourceTree = ""; }; 1DE4B4EE2CEC514B006C8F65 /* ProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewModel.swift; sourceTree = ""; }; 1DE9FE0D2D063AF20069AA64 /* NotificationSettingsStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsStepView.swift; sourceTree = ""; }; 1DE9FE0F2D063BC80069AA64 /* NotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = ""; }; - 1DE9FE112D0650150069AA64 /* NotificationTimePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationTimePicker.swift; sourceTree = ""; }; 1DE9FE142D0664A60069AA64 /* ReportsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportsViewModel.swift; sourceTree = ""; }; 1DE9FE162D066EE20069AA64 /* ReportsPDFGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportsPDFGenerator.swift; sourceTree = ""; }; 1DEACCDD2D0BE6340084CC39 /* NetworkErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkErrorView.swift; sourceTree = ""; }; - 1DF579C42F673A8B009A41B3 /* InsightRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InsightRow.swift; sourceTree = ""; }; 1DF7228D2F9C182300320B38 /* Color+Warm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+Warm.swift"; sourceTree = ""; }; 1DF7228F2F9C18FE00320B38 /* GoalDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoalDetailView.swift; sourceTree = ""; }; 1DF722912F9C19CE00320B38 /* MoneyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoneyView.swift; sourceTree = ""; }; @@ -262,14 +256,6 @@ path = Utilities; sourceTree = ""; }; - 1DAF5D012CED9475001C0AF3 /* Buttons */ = { - isa = PBXGroup; - children = ( - 1DE4B4D72CE7B10E006C8F65 /* PrimaryButton.swift */, - ); - path = Buttons; - sourceTree = ""; - }; 1DAF5D022CED9481001C0AF3 /* CameraScanner */ = { isa = PBXGroup; children = ( @@ -381,7 +367,6 @@ 1DC014042CDE00F7009A46E1 /* ProfileTab */ = { isa = PBXGroup; children = ( - 1DE9FE112D0650150069AA64 /* NotificationTimePicker.swift */, 1DAF5D1F2CEE9767001C0AF3 /* TipHistoryView.swift */, 1DC2583B2CC081D90002DDE3 /* ProfileView.swift */, 1DF722932F9C19E600320B38 /* AchievementsView.swift */, @@ -492,12 +477,10 @@ isa = PBXGroup; children = ( F38DAD27B5231BDC080F36DA /* SproutMark.swift */, - 1DF579C42F673A8B009A41B3 /* InsightRow.swift */, 1DEACCDD2D0BE6340084CC39 /* NetworkErrorView.swift */, 1DAF5D042CED94F1001C0AF3 /* Tips */, 1DAF5D032CED94DD001C0AF3 /* TextFields */, 1DAF5D022CED9481001C0AF3 /* CameraScanner */, - 1DAF5D012CED9475001C0AF3 /* Buttons */, ); path = Components; sourceTree = ""; @@ -700,7 +683,6 @@ 1DC014132CE5B0C5009A46E1 /* ContentView.swift in Sources */, 1DE4B4D42CE60032006C8F65 /* HybridTextField.swift in Sources */, 1DC013FB2CDB575F009A46E1 /* IncomesTrackerView.swift in Sources */, - 1DE4B4D82CE7B10E006C8F65 /* PrimaryButton.swift in Sources */, 1D3669872CC62F8200E84FF9 /* Strings.swift in Sources */, 1DF7228E2F9C182300320B38 /* Color+Warm.swift in Sources */, 1DE9FE0E2D063AF20069AA64 /* NotificationSettingsStepView.swift in Sources */, @@ -710,13 +692,11 @@ 1DC258082CC07DCC0002DDE3 /* SavelyApp.swift in Sources */, 1DF722962F9C2FA400320B38 /* AddGoalFlow.swift in Sources */, 1DAF5D202CEE9767001C0AF3 /* TipHistoryView.swift in Sources */, - 1DF579C52F673A8B009A41B3 /* InsightRow.swift in Sources */, 82D33CE42CEC66F200BE005E /* TotalConfirmationView.swift in Sources */, 1DE9FE172D066EE20069AA64 /* ReportsPDFGenerator.swift in Sources */, 1D3669892CC62F9100E84FF9 /* UIConstants.swift in Sources */, 1DAF5D0C2CEDA6C0001C0AF3 /* IncomesTrackerViewModel.swift in Sources */, 1DF722922F9C19CE00320B38 /* MoneyView.swift in Sources */, - 1DE9FE122D0650150069AA64 /* NotificationTimePicker.swift in Sources */, 1D88B7D12CC6EBB4009B6538 /* CameraView.swift in Sources */, 1DEACCDE2D0BE6340084CC39 /* NetworkErrorView.swift in Sources */, 1DAF5D1B2CEDC118001C0AF3 /* GoalsViewModel.swift in Sources */, diff --git a/Savely/Assets.xcassets/ColorPalette/CardBackgroundColor.colorset/Contents.json b/Savely/Assets.xcassets/ColorPalette/CardBackgroundColor.colorset/Contents.json deleted file mode 100644 index a1a9e89..0000000 --- a/Savely/Assets.xcassets/ColorPalette/CardBackgroundColor.colorset/Contents.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "colors" : [ - { - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "light" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xFF", - "green" : "0xFF", - "red" : "0xFF" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x2A", - "green" : "0x2A", - "red" : "0x2A" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Savely/Assets.xcassets/ColorPalette/Contents.json b/Savely/Assets.xcassets/ColorPalette/Contents.json deleted file mode 100644 index 73c0059..0000000 --- a/Savely/Assets.xcassets/ColorPalette/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Savely/Assets.xcassets/ColorPalette/PrimaryGreen.colorset/Contents.json b/Savely/Assets.xcassets/ColorPalette/PrimaryGreen.colorset/Contents.json deleted file mode 100644 index f353238..0000000 --- a/Savely/Assets.xcassets/ColorPalette/PrimaryGreen.colorset/Contents.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "colors" : [ - { - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "light" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x28", - "green" : "0xA7", - "red" : "0x45" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x1C", - "green" : "0x87", - "red" : "0x30" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Savely/Assets.xcassets/ColorPalette/backgroundColor.colorset/Contents.json b/Savely/Assets.xcassets/ColorPalette/backgroundColor.colorset/Contents.json deleted file mode 100644 index 498d7e6..0000000 --- a/Savely/Assets.xcassets/ColorPalette/backgroundColor.colorset/Contents.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "colors" : [ - { - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "light" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xF5", - "green" : "0xF5", - "red" : "0xF5" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x12", - "green" : "0x12", - "red" : "0x12" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Savely/Assets.xcassets/ColorPalette/listBackgroundColor.colorset/Contents.json b/Savely/Assets.xcassets/ColorPalette/listBackgroundColor.colorset/Contents.json deleted file mode 100644 index 33e1f3d..0000000 --- a/Savely/Assets.xcassets/ColorPalette/listBackgroundColor.colorset/Contents.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "colors" : [ - { - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "light" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xF2", - "green" : "0xF2", - "red" : "0xF2" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x1F", - "green" : "0x1F", - "red" : "0x1F" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Savely/Assets.xcassets/ColorPalette/primaryBlue.colorset/Contents.json b/Savely/Assets.xcassets/ColorPalette/primaryBlue.colorset/Contents.json deleted file mode 100644 index c619cca..0000000 --- a/Savely/Assets.xcassets/ColorPalette/primaryBlue.colorset/Contents.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "colors" : [ - { - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "light" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xFF", - "green" : "0x7A", - "red" : "0x00" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xFF", - "green" : "0x84", - "red" : "0x0A" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Savely/Assets.xcassets/ColorPalette/primaryPurple.colorset/Contents.json b/Savely/Assets.xcassets/ColorPalette/primaryPurple.colorset/Contents.json deleted file mode 100644 index b168efb..0000000 --- a/Savely/Assets.xcassets/ColorPalette/primaryPurple.colorset/Contents.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "colors" : [ - { - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "light" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xF2", - "green" : "0x63", - "red" : "0xA4" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xA6", - "green" : "0x31", - "red" : "0x62" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Savely/Assets.xcassets/ColorPalette/primaryRed.colorset/Contents.json b/Savely/Assets.xcassets/ColorPalette/primaryRed.colorset/Contents.json deleted file mode 100644 index 1c726cc..0000000 --- a/Savely/Assets.xcassets/ColorPalette/primaryRed.colorset/Contents.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "colors" : [ - { - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "light" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x30", - "green" : "0x3B", - "red" : "0xFF" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x3A", - "green" : "0x45", - "red" : "0xFF" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Savely/Assets.xcassets/ColorPalette/primaryYellow.colorset/Contents.json b/Savely/Assets.xcassets/ColorPalette/primaryYellow.colorset/Contents.json deleted file mode 100644 index 918d1bf..0000000 --- a/Savely/Assets.xcassets/ColorPalette/primaryYellow.colorset/Contents.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "colors" : [ - { - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "light" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x00", - "green" : "0xCC", - "red" : "0xFF" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x0A", - "green" : "0xD6", - "red" : "0xFF" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Savely/Assets.xcassets/ColorPalette/secondaryBlue.colorset/Contents.json b/Savely/Assets.xcassets/ColorPalette/secondaryBlue.colorset/Contents.json deleted file mode 100644 index 72a262e..0000000 --- a/Savely/Assets.xcassets/ColorPalette/secondaryBlue.colorset/Contents.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "colors" : [ - { - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "light" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xE2", - "green" : "0x90", - "red" : "0x4A" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x9E", - "green" : "0x5A", - "red" : "0x1C" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Savely/Assets.xcassets/ColorPalette/secondaryGreen.colorset/Contents.json b/Savely/Assets.xcassets/ColorPalette/secondaryGreen.colorset/Contents.json deleted file mode 100644 index 141f32a..0000000 --- a/Savely/Assets.xcassets/ColorPalette/secondaryGreen.colorset/Contents.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "colors" : [ - { - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "light" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x59", - "green" : "0xC7", - "red" : "0x34" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x58", - "green" : "0xD1", - "red" : "0x30" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Savely/Assets.xcassets/ColorPalette/terciaryGreen.colorset/Contents.json b/Savely/Assets.xcassets/ColorPalette/terciaryGreen.colorset/Contents.json deleted file mode 100644 index 7a5ad13..0000000 --- a/Savely/Assets.xcassets/ColorPalette/terciaryGreen.colorset/Contents.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "colors" : [ - { - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "light" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xE1", - "green" : "0xF6", - "red" : "0xDF" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x2F", - "green" : "0x3E", - "red" : "0x2C" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Savely/Models/GoalModel.swift b/Savely/Models/GoalModel.swift index 049a5dc..feb73d7 100755 --- a/Savely/Models/GoalModel.swift +++ b/Savely/Models/GoalModel.swift @@ -21,37 +21,39 @@ enum GoalColor: String, Codable, CaseIterable, Identifiable { var color: Color { switch self { + // Dark variants sit a step lighter (≥3:1 against the dark surface, + // like the light ones against paper); see pr-d2-adaptive-palette.md. case .green: return .warmGreen - case .sage: return Color(red: 0.357, green: 0.549, blue: 0.408) - case .teal: return Color(red: 0.196, green: 0.498, blue: 0.498) + case .sage: return Color(light: 0x5B8C68, dark: 0x75A582) + case .teal: return Color(light: 0x327F7F, dark: 0x40A4A4) case .blue: return .warmSky - case .navy: return Color(red: 0.212, green: 0.318, blue: 0.537) - case .purple: return Color(red: 0.584, green: 0.357, blue: 0.537) - case .lavender: return Color(red: 0.490, green: 0.416, blue: 0.675) - case .rose: return Color(red: 0.651, green: 0.337, blue: 0.455) + case .navy: return Color(light: 0x365189, dark: 0x4A6EB7) + case .purple: return Color(light: 0x955B89, dark: 0xAC77A1) + case .lavender: return Color(light: 0x7D6AAC, dark: 0x9A8BBE) + case .rose: return Color(light: 0xA65674, dark: 0xB8778F) case .yellow: return .warmAmber - case .coral: return Color(red: 0.780, green: 0.424, blue: 0.306) + case .coral: return Color(light: 0xC76C4E, dark: 0xD38C75) case .red: return .warmClay - case .brown: return Color(red: 0.565, green: 0.369, blue: 0.235) - case .olive: return Color(red: 0.431, green: 0.490, blue: 0.196) + case .brown: return Color(light: 0x905E3C, dark: 0xB4764B) + case .olive: return Color(light: 0x6E7D32, dark: 0x8EA141) } } var trackColor: Color { switch self { case .green: return .warmGreenSoft - case .sage: return Color(red: 0.855, green: 0.922, blue: 0.878) - case .teal: return Color(red: 0.820, green: 0.925, blue: 0.925) + case .sage: return Color(light: 0xDAEBE0, dark: 0x283E2E) + case .teal: return Color(light: 0xD1ECEC, dark: 0x283E3E) case .blue: return .warmSkySoft - case .navy: return Color(red: 0.820, green: 0.855, blue: 0.941) - case .purple: return Color(red: 0.937, green: 0.886, blue: 0.937) - case .lavender: return Color(red: 0.882, green: 0.871, blue: 0.961) - case .rose: return Color(red: 0.949, green: 0.867, blue: 0.898) + case .navy: return Color(light: 0xD1DAF0, dark: 0x282F3E) + case .purple: return Color(light: 0xEFE2EF, dark: 0x3E283A) + case .lavender: return Color(light: 0xE1DEF5, dark: 0x2E283E) + case .rose: return Color(light: 0xF2DDE5, dark: 0x3E2830) case .yellow: return .warmAmberSoft - case .coral: return Color(red: 0.965, green: 0.886, blue: 0.859) + case .coral: return Color(light: 0xF6E2DB, dark: 0x3E2D28) case .red: return .warmClaySoft - case .brown: return Color(red: 0.937, green: 0.890, blue: 0.855) - case .olive: return Color(red: 0.898, green: 0.929, blue: 0.839) + case .brown: return Color(light: 0xEFE3DA, dark: 0x3E3128) + case .olive: return Color(light: 0xE5EDD6, dark: 0x3A3E28) } } diff --git a/Savely/Resources/Color+Warm.swift b/Savely/Resources/Color+Warm.swift index c30d84e..74437f6 100644 --- a/Savely/Resources/Color+Warm.swift +++ b/Savely/Resources/Color+Warm.swift @@ -24,7 +24,7 @@ extension Color { // surface* (CTAs, the income card, the selected radio) that carries // white content, and stays deep in both schemes. static let warmGreen = Color(light: 0x2F6B4A, dark: 0x78B58F) - static let warmGreenFill = Color(light: 0x2F6B4A, dark: 0x357A55) + static let warmGreenFill = Color(light: 0x2F6B4A, dark: 0x357A55) // also "green that stays deep" as ink on white pills static let warmGreenDeep = Color(light: 0x1F4A33, dark: 0xB6DCC4) static let warmGreenSoft = Color(light: 0xE8F0EA, dark: 0x22382C) static let warmGreenTint = Color(light: 0xF2F7F3, dark: 0x1B2A21) @@ -32,6 +32,8 @@ extension Color { // Category tints — a glyph on a soft tile, never a fill. static let warmAmber = Color(light: 0xC48A2A, dark: 0xDEA64A) static let warmAmberSoft = Color(light: 0xF6ECD6, dark: 0x3A2F19) + /// Amber as *text* on an amber-soft banner (the accent itself is too light to read at 12pt). + static let warmAmberDeep = Color(light: 0x7A5618, dark: 0xEBC27A) static let warmClay = Color(light: 0xB85C42, dark: 0xD9846A) static let warmClaySoft = Color(light: 0xF6E1D8, dark: 0x3D2721) static let warmSky = Color(light: 0x4A7BA6, dark: 0x7EA9D0) @@ -49,12 +51,16 @@ extension Color { static let warmTrack = Color(light: 0xEDE6D4, dark: 0x3A362E) /// The hero-card lift and the "+" shell shadow — the only shadows by rule. static let warmShadow = Color(light: 0x000000, dark: 0x000000, lightAlpha: 0.06, darkAlpha: 0.45) + /// The raised "+" shell's shadow (deeper than the hero lift). + static let warmShellShadow = Color(light: 0x000000, dark: 0x000000, lightAlpha: 0.18, darkAlpha: 0.60) } -private extension Color { +extension Color { /// A trait-aware color from two hex values (0xRRGGBB) and per-scheme /// alphas. Resolved by UIKit at draw time, so it follows the window's /// `preferredColorScheme` and system appearance without any view code. + /// Internal so `GoalColor` (Models/GoalModel.swift) can define its + /// per-goal accents the same way; views should use the named tokens. init(light: UInt32, dark: UInt32, lightAlpha: CGFloat = 1, darkAlpha: CGFloat = 1) { self.init(uiColor: UIColor { traits in let isDark = traits.userInterfaceStyle == .dark diff --git a/Savely/Resources/UIConstants.swift b/Savely/Resources/UIConstants.swift index 6ee1a19..a599efd 100755 --- a/Savely/Resources/UIConstants.swift +++ b/Savely/Resources/UIConstants.swift @@ -30,9 +30,4 @@ struct UIConstants { static let smallBody = Font.custom("Roboto", size: 12) } - enum UIShadow { - static let shadow = 2.0 - static let shadowMedium = 5.0 - static let shadowBig = 10.0 - } } diff --git a/Savely/Utilities/ReportsPDFGenerator.swift b/Savely/Utilities/ReportsPDFGenerator.swift index 572b79f..820667d 100644 --- a/Savely/Utilities/ReportsPDFGenerator.swift +++ b/Savely/Utilities/ReportsPDFGenerator.swift @@ -51,7 +51,7 @@ struct ReportsPDFGenerator { let attributes: [NSAttributedString.Key: Any] = [ .font: UIFont.boldSystemFont(ofSize: 28), .paragraphStyle: paragraphStyle, - .foregroundColor: UIColor.white + .foregroundColor: UIColor.white // deliberate: PDF is paper, not the app ] title.draw(in: CGRect(x: 0, y: 300, width: pageWidth, height: 50), withAttributes: attributes) diff --git a/Savely/Views/Components/Buttons/PrimaryButton.swift b/Savely/Views/Components/Buttons/PrimaryButton.swift deleted file mode 100644 index 4584929..0000000 --- a/Savely/Views/Components/Buttons/PrimaryButton.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// PrimaryButton.swift -// Savely -// -// Created by Ivan Lorenzana Belli on 15/11/24. -// - -import SwiftUI - -struct PrimaryButton: View { - let action: () -> Void - let text: String - - var body: some View { - Button { - action() - } label: { - Text(text) - .frame(maxWidth: .infinity) - .padding() - .background(Color.green) - .foregroundStyle(.white) - .fontWeight(.bold) - .cornerRadius(UIConstants.UICornerRadius.cornerRadius) - } - } -} - -#Preview { - PrimaryButton(action: { - print("Hola") - }, text: "Hola") -} diff --git a/Savely/Views/Components/Buttons/QuickActionButton.swift b/Savely/Views/Components/Buttons/QuickActionButton.swift deleted file mode 100755 index 10b87f7..0000000 --- a/Savely/Views/Components/Buttons/QuickActionButton.swift +++ /dev/null @@ -1,42 +0,0 @@ -// -// QuickActionButton.swift -// Savely -// -// Created by Ivan Lorenzana Belli on 20/10/24. -// - -import SwiftUI - -struct QuickActionButton: View { - var iconName: String - var label: String - var backgroundColor: Color - let action: () -> Void - - var body: some View { - Button(action: { - action() - }, label: { - VStack { - Image(systemName: iconName) - .resizable() - .scaledToFit() - .frame(width: 24, height: 24) - .foregroundStyle(.white) - Text(label) - .font(.footnote) - .foregroundStyle(.white) - } - .padding() - .frame(maxWidth: .infinity, minHeight: 50) - .background(backgroundColor) - .cornerRadius(UIConstants.UICornerRadius.cornerRadiusMedium) - }) - } -} - -#Preview { - QuickActionButton(iconName: "camera", label: "Add Expense", backgroundColor: Color.red, action: { - print("Button pressed") - }) -} diff --git a/Savely/Views/Components/CameraScanner/CameraView.swift b/Savely/Views/Components/CameraScanner/CameraView.swift index 264b699..aa4107d 100755 --- a/Savely/Views/Components/CameraScanner/CameraView.swift +++ b/Savely/Views/Components/CameraScanner/CameraView.swift @@ -25,7 +25,7 @@ struct CameraView: View { Image(systemName: "xmark.circle.fill") .resizable() .frame(width: 40, height: 40) - .foregroundColor(.white) + .foregroundColor(.white) // deliberate: drawn over the live camera feed .padding() } Spacer() @@ -39,7 +39,7 @@ struct CameraView: View { Image(systemName: "camera.circle.fill") .resizable() .frame(width: 70, height: 70) - .foregroundColor(.white) + .foregroundColor(.white) // deliberate: drawn over the live camera feed } .padding(.bottom, 30) Spacer() @@ -59,9 +59,8 @@ struct CameraView: View { } ) .frame(width: 300, height: 400) - .background(Color.white) + .background(Color.warmSurface) .cornerRadius(UIConstants.UICornerRadius.cornerRadiusMedium) - .shadow(radius: UIConstants.UIShadow.shadowBig) } } .onAppear { diff --git a/Savely/Views/Components/CameraScanner/TotalConfirmationView.swift b/Savely/Views/Components/CameraScanner/TotalConfirmationView.swift index a7cb537..69c1a7c 100644 --- a/Savely/Views/Components/CameraScanner/TotalConfirmationView.swift +++ b/Savely/Views/Components/CameraScanner/TotalConfirmationView.swift @@ -39,8 +39,8 @@ struct TotalConfirmationView: View { Text(Strings.Buttons.noButton) .padding() .frame(maxWidth: .infinity) - .background(Color.gray) - .foregroundStyle(Color.white) + .background(Color.warmInkMuted) + .foregroundStyle(Color.warmOnGreen) .cornerRadius(10) } @@ -50,8 +50,8 @@ struct TotalConfirmationView: View { Text(Strings.Buttons.yesButton) .padding() .frame(maxWidth: .infinity) - .background(Color.green) - .foregroundStyle(Color.white) + .background(Color.warmGreenFill) + .foregroundStyle(Color.warmOnGreen) .cornerRadius(10) } } diff --git a/Savely/Views/Components/InsightRow.swift b/Savely/Views/Components/InsightRow.swift deleted file mode 100644 index 40299c0..0000000 --- a/Savely/Views/Components/InsightRow.swift +++ /dev/null @@ -1,91 +0,0 @@ -// -// InsightRow.swift -// Savely -// -// Created by Ivan Lorenzana Belli on 15/03/26. -// - -import SwiftUI - -struct InsightRow: View { - let icon: String - let iconColor: Color - let title: String - let subtitle: String - let accessory: AccessoryType - - enum AccessoryType { - case chevron - case badge(String) - } - - var body: some View { - HStack(spacing: 16) { - // Icon - ZStack { - RoundedRectangle(cornerRadius: 12) - .fill(iconColor.opacity(0.15)) - .frame(width: 48, height: 48) - - Image(systemName: icon) - .font(.system(size: 20)) - .foregroundStyle(iconColor) - } - - // Title and Subtitle - VStack(alignment: .leading, spacing: 4) { - Text(title) - .font(.body) - .fontWeight(.semibold) - .foregroundStyle(.primary) - - Text(subtitle) - .font(.caption) - .foregroundStyle(.secondary) - } - - Spacer() - - // Accessory - accessoryView - } - .padding(16) - .background(Color(.systemBackground)) - .cornerRadius(12) - .shadow(color: Color.black.opacity(0.05), radius: 4, x: 0, y: 2) - } - - @ViewBuilder - private var accessoryView: some View { - switch accessory { - case .chevron: - Image(systemName: "chevron.right") - .font(.caption) - .foregroundStyle(.secondary) - - case .badge(let text): - HStack(spacing: 4) { - Image(systemName: "doc.fill") - .font(.caption) - Text(text) - .font(.caption) - .fontWeight(.medium) - } - .foregroundStyle(iconColor) - .padding(.horizontal, 12) - .padding(.vertical, 6) - .background(iconColor.opacity(0.1)) - .cornerRadius(8) - } - } -} - -#Preview { - InsightRow( - icon: "lightbulb.fill", - iconColor: Color(red: 0.3, green: 0.5, blue: 0.4), - title: "Tip History", - subtitle: "3 new saving tips", - accessory: .chevron - ) -} diff --git a/Savely/Views/Components/NetworkErrorView.swift b/Savely/Views/Components/NetworkErrorView.swift index 1cd029c..5da556f 100644 --- a/Savely/Views/Components/NetworkErrorView.swift +++ b/Savely/Views/Components/NetworkErrorView.swift @@ -35,15 +35,15 @@ struct NetworkErrorView: View { .frame(maxWidth: .infinity) .padding() .fontWeight(.bold) - .background(Color("primaryGreen")) - .foregroundColor(.white) + .background(Color.warmGreenFill) + .foregroundColor(Color.warmOnGreen) .cornerRadius(UIConstants.UICornerRadius.cornerRadius) } .padding(.horizontal) } .padding() .frame(maxWidth: .infinity, maxHeight: .infinity) - .background(Color(UIColor.systemBackground)) + .background(Color.warmBg) .edgesIgnoringSafeArea(.all) } } diff --git a/Savely/Views/Components/SproutMark.swift b/Savely/Views/Components/SproutMark.swift index 6f4939b..b46839f 100644 --- a/Savely/Views/Components/SproutMark.swift +++ b/Savely/Views/Components/SproutMark.swift @@ -83,7 +83,7 @@ struct SproutLeafShape: Shape { /// (deep greens on whatever background the caller provides). struct SproutMark: View { var leafPrimary: Color = .warmGreen - var leafSecondary: Color = Color(red: 0.216, green: 0.475, blue: 0.353) // #37795a + var leafSecondary: Color = Color(red: 0.216, green: 0.475, blue: 0.353) // #37795a — brand mark, deliberately fixed var seed: Color = .warmAmber /// Stem line width as a fraction of the mark's width (icon uses 5.5/120). var stemWidthRatio: CGFloat = 5.5 / 120 diff --git a/Savely/Views/DashboardTab/DashboardView.swift b/Savely/Views/DashboardTab/DashboardView.swift index d0d4e8f..35adb32 100755 --- a/Savely/Views/DashboardTab/DashboardView.swift +++ b/Savely/Views/DashboardTab/DashboardView.swift @@ -189,7 +189,7 @@ struct HeroGoalCard: View { HStack(alignment: .center, spacing: 20) { ZStack { Circle() - .stroke(Color(red: 0.93, green: 0.90, blue: 0.83), lineWidth: 12) + .stroke(Color.warmTrack, lineWidth: 12) Circle() .trim(from: 0, to: goal.progress) .stroke(Color.warmGreen, style: StrokeStyle(lineWidth: 12, lineCap: .round)) @@ -242,10 +242,10 @@ struct HeroGoalCard: View { Text("Add deposit") .font(.system(size: 15, weight: .semibold)) } - .foregroundStyle(.white) + .foregroundStyle(Color.warmOnGreen) .frame(maxWidth: .infinity) .frame(height: 48) - .background(Color.warmGreen) + .background(Color.warmGreenFill) .cornerRadius(14) } @@ -263,7 +263,7 @@ struct HeroGoalCard: View { .background(Color.warmSurface) .cornerRadius(24) .overlay(RoundedRectangle(cornerRadius: 24).stroke(Color.warmLine, lineWidth: 1)) - .shadow(color: Color.black.opacity(0.04), radius: 12, x: 0, y: 4) + .shadow(color: Color.warmShadow, radius: 12, x: 0, y: 4) .sheet(isPresented: $showDepositSheet) { DepositSheet(goal: goal, modelContext: modelContext) } @@ -354,10 +354,10 @@ struct DepositSheet: View { Button(action: saveDeposit) { Text("Add \(amountText.isEmpty ? "" : "$\(amountText)") to \(goal.name.components(separatedBy: ",").first ?? goal.name)") .font(.system(size: 15, weight: .semibold)) - .foregroundStyle(.white) + .foregroundStyle(Color.warmOnGreen) .frame(maxWidth: .infinity) .frame(height: 50) - .background(Color.warmGreen) + .background(Color.warmGreenFill) .cornerRadius(14) } .disabled(depositAmount == nil) diff --git a/Savely/Views/DashboardTab/ReportsView.swift b/Savely/Views/DashboardTab/ReportsView.swift index 33eef01..41ade69 100755 --- a/Savely/Views/DashboardTab/ReportsView.swift +++ b/Savely/Views/DashboardTab/ReportsView.swift @@ -42,8 +42,8 @@ struct ReportsView: View { } } .padding() - .background(Color("primaryBlue")) - .foregroundColor(.white) + .background(Color.warmSky) + .foregroundColor(Color.warmOnGreen) .cornerRadius(UIConstants.UICornerRadius.cornerRadius) Spacer() } @@ -93,9 +93,8 @@ struct ReportsView: View { } } .padding() - .background(Color("listBackgroundColor")) + .background(Color.warmSurface) .cornerRadius(UIConstants.UICornerRadius.cornerRadiusMedium) - .shadow(radius: UIConstants.UIShadow.shadow) .onAppear { viewModel.setModelContext(modelContext) viewModel.fetchReportData() diff --git a/Savely/Views/ExpensesTab/ExpenseTrackerView.swift b/Savely/Views/ExpensesTab/ExpenseTrackerView.swift index a8bfc02..65f7830 100755 --- a/Savely/Views/ExpensesTab/ExpenseTrackerView.swift +++ b/Savely/Views/ExpensesTab/ExpenseTrackerView.swift @@ -57,21 +57,21 @@ struct ExpenseTrackerView: View { Button(action: { showCameraView = true }) { HStack(spacing: 14) { RoundedRectangle(cornerRadius: 12) - .fill(Color.white.opacity(0.1)) + .fill(Color.warmOnInk.opacity(0.1)) .frame(width: 44, height: 44) - .overlay(Image(systemName: "camera.fill").font(.system(size: 18)).foregroundStyle(.white)) + .overlay(Image(systemName: "camera.fill").font(.system(size: 18)).foregroundStyle(Color.warmOnInk)) VStack(alignment: .leading, spacing: 2) { Text("Scan a receipt") .font(.system(size: 15, weight: .semibold)) - .foregroundStyle(.white) + .foregroundStyle(Color.warmOnInk) Text("We'll read the total and category") .font(.system(size: 12)) - .foregroundStyle(Color.white.opacity(0.65)) + .foregroundStyle(Color.warmOnInk.opacity(0.65)) } Spacer() Image(systemName: "chevron.right") .font(.system(size: 14)) - .foregroundStyle(Color.white.opacity(0.6)) + .foregroundStyle(Color.warmOnInk.opacity(0.6)) } .padding(16) .background(Color.warmInk) @@ -113,9 +113,9 @@ struct ExpenseTrackerView: View { Button(action: { viewModel.addExpense(); focusedField = nil }) { Image(systemName: "plus") .font(.system(size: 15, weight: .semibold)) - .foregroundStyle(.white) + .foregroundStyle(Color.warmOnGreen) .frame(width: 40, height: 40) - .background(Color.warmGreen) + .background(Color.warmGreenFill) .cornerRadius(10) } } diff --git a/Savely/Views/GoalsTab/AddGoalFlow.swift b/Savely/Views/GoalsTab/AddGoalFlow.swift index 7ee4224..1099b5a 100644 --- a/Savely/Views/GoalsTab/AddGoalFlow.swift +++ b/Savely/Views/GoalsTab/AddGoalFlow.swift @@ -176,7 +176,6 @@ struct AddGoalStep1View: View { .background(Color.warmSurface) .clipShape(Capsule()) .overlay(Capsule().stroke(Color.warmLine, lineWidth: 1)) - .shadow(color: .black.opacity(0.04), radius: 12, y: 4) Spacer() } .padding(.bottom, 20) @@ -203,7 +202,6 @@ struct AddGoalStep1View: View { RoundedRectangle(cornerRadius: 16) .stroke(nameFocused ? Color.warmGreen : Color.warmLine, lineWidth: nameFocused ? 1.5 : 1) ) - .shadow(color: nameFocused ? Color.warmGreenSoft : .clear, radius: 0, x: 0, y: 0) Text("Tip: be specific. Try a specific name like Kyoto trip '26.") .font(.system(size: 12)).foregroundStyle(Color.warmInkMuted).padding(.leading, 4) } @@ -245,9 +243,8 @@ struct AddGoalStep1View: View { .fill(gc.color) .frame(width: 38, height: 38) .overlay( - Circle().stroke(Color.white, lineWidth: isOn ? 3 : 0) + Circle().stroke(Color.warmSurface, lineWidth: isOn ? 3 : 0) ) - .shadow(color: isOn ? gc.color.opacity(0.5) : .clear, radius: 4, y: 2) } .buttonStyle(.plain) .animation(.easeInOut(duration: 0.15), value: state.color) @@ -258,9 +255,9 @@ struct AddGoalStep1View: View { Button(action: onNext) { Text("Continue") - .font(.system(size: 15, weight: .semibold)).foregroundStyle(.white) + .font(.system(size: 15, weight: .semibold)).foregroundStyle(Color.warmOnGreen) .frame(maxWidth: .infinity).frame(height: 50) - .background(state.name.isEmpty ? Color.warmInkMuted : Color.warmGreen) + .background(state.name.isEmpty ? Color.warmInkMuted : Color.warmGreenFill) .cornerRadius(14) } .disabled(state.name.isEmpty) @@ -357,9 +354,9 @@ struct AddGoalStep2View: View { WarmKeypad(amountStr: $state.amountStr) Button(action: onNext) { Text("Continue") - .font(.system(size: 15, weight: .semibold)).foregroundStyle(.white) + .font(.system(size: 15, weight: .semibold)).foregroundStyle(Color.warmOnGreen) .frame(maxWidth: .infinity).frame(height: 50) - .background(state.amount > 0 ? Color.warmGreen : Color.warmInkMuted) + .background(state.amount > 0 ? Color.warmGreenFill : Color.warmInkMuted) .cornerRadius(14) } .disabled(state.amount <= 0) @@ -403,24 +400,24 @@ struct AddGoalStep3View: View { VStack(alignment: .leading, spacing: 8) { HStack(spacing: 6) { Image(systemName: "sparkles") - .font(.system(size: 11, weight: .bold)).foregroundStyle(.white.opacity(0.85)) + .font(.system(size: 11, weight: .bold)).foregroundStyle(Color.warmOnGreen.opacity(0.85)) Text("SUGGESTED PACE") - .font(.system(size: 11, weight: .bold)).foregroundStyle(.white.opacity(0.85)).tracking(1.2) + .font(.system(size: 11, weight: .bold)).foregroundStyle(Color.warmOnGreen.opacity(0.85)).tracking(1.2) } HStack(alignment: .firstTextBaseline) { Text("$\(Int(state.weeklyPace))") - .font(.system(size: 38, weight: .regular, design: .serif)).foregroundStyle(.white) + .font(.system(size: 38, weight: .regular, design: .serif)).foregroundStyle(Color.warmOnGreen) Text("/wk") - .font(.system(size: 18)).foregroundStyle(.white.opacity(0.7)).padding(.leading, 2) + .font(.system(size: 18)).foregroundStyle(Color.warmOnGreen.opacity(0.7)).padding(.leading, 2) Spacer() Text("~$\(Int(state.monthlyPace))/mo") - .font(.system(size: 13)).foregroundStyle(.white.opacity(0.85)) + .font(.system(size: 13)).foregroundStyle(Color.warmOnGreen.opacity(0.85)) } Text("To hit **$\(Int(state.amount))** by **\(formattedDeadline)**.") - .font(.system(size: 13)).foregroundStyle(.white.opacity(0.85)) + .font(.system(size: 13)).foregroundStyle(Color.warmOnGreen.opacity(0.85)) } .padding(18) - .background(Color.warmGreen) + .background(Color.warmGreenFill) .cornerRadius(22) .padding(.horizontal, 20).padding(.bottom, 16) @@ -482,9 +479,9 @@ struct AddGoalStep3View: View { Button(action: onNext) { Text("Continue") - .font(.system(size: 15, weight: .semibold)).foregroundStyle(.white) + .font(.system(size: 15, weight: .semibold)).foregroundStyle(Color.warmOnGreen) .frame(maxWidth: .infinity).frame(height: 50) - .background(Color.warmGreen).cornerRadius(14) + .background(Color.warmGreenFill).cornerRadius(14) } .padding(.horizontal, 20).padding(.bottom, 32) } @@ -532,9 +529,9 @@ struct MiniCalendarView: View { Button(action: { selectDay(day) }) { Text("\(day)") .font(.system(size: 13, weight: isSelected ? .semibold : .regular)) - .foregroundStyle(isSelected ? .white : Color.warmInk) + .foregroundStyle(isSelected ? Color.warmOnGreen : Color.warmInk) .frame(maxWidth: .infinity).frame(height: 32) - .background(isSelected ? Color.warmGreen : Color.clear) + .background(isSelected ? Color.warmGreenFill : Color.clear) .cornerRadius(10) } .buttonStyle(.plain) @@ -651,7 +648,7 @@ struct AddGoalStep4View: View { .background(Color.warmSurface) .cornerRadius(24) .overlay(RoundedRectangle(cornerRadius: 24).stroke(Color.warmLine, lineWidth: 1)) - .shadow(color: .black.opacity(0.05), radius: 16, y: 6) + .shadow(color: Color.warmShadow, radius: 16, y: 6) .padding(.horizontal, 20).padding(.bottom, 20) // Recap toggles @@ -672,7 +669,7 @@ struct AddGoalStep4View: View { Image(systemName: "sparkles") .font(.system(size: 13)).foregroundStyle(Color.warmAmber) Text("That's **about one less takeout per week**. Doable.") - .font(.system(size: 12)).foregroundStyle(Color(red: 0.478, green: 0.337, blue: 0.094)) + .font(.system(size: 12)).foregroundStyle(Color.warmAmberDeep) } .padding(16) .background(Color.warmAmberSoft) @@ -688,9 +685,9 @@ struct AddGoalStep4View: View { Text("Plant goal") .font(.system(size: 15, weight: .semibold)) } - .foregroundStyle(.white) + .foregroundStyle(Color.warmOnGreen) .frame(maxWidth: .infinity).frame(height: 50) - .background(Color.warmGreen).cornerRadius(14) + .background(Color.warmGreenFill).cornerRadius(14) } Button(action: onBack) { Text("Edit anything") @@ -744,12 +741,12 @@ struct AddGoalSuccessView: View { var body: some View { ZStack { - Color.warmGreen.ignoresSafeArea() + Color.warmGreenFill.ignoresSafeArea() GeometryReader { geo in ForEach(Array(dots.enumerated()), id: \.offset) { _, dot in Circle() - .fill(dot.3 ? Color.warmAmber : Color.white) + .fill(dot.3 ? Color.warmAmber : Color.warmOnGreen) .frame(width: dot.2, height: dot.2) .opacity(0.5) .position(x: geo.size.width * dot.0, y: geo.size.height * dot.1) @@ -760,24 +757,24 @@ struct AddGoalSuccessView: View { Spacer() ZStack { - Circle().fill(Color.white.opacity(0.14)).frame(width: 132, height: 132) - Circle().fill(Color.white).frame(width: 88, height: 88) + Circle().fill(Color.warmOnGreen.opacity(0.14)).frame(width: 132, height: 132) + Circle().fill(Color.warmOnGreen).frame(width: 88, height: 88) .overlay( Image(systemName: "checkmark") .font(.system(size: 36, weight: .semibold)) - .foregroundStyle(Color.warmGreen) + .foregroundStyle(Color.warmGreenFill) ) } .padding(.bottom, 28) Text("Planted.") .font(.system(size: 42, weight: .regular, design: .serif)) - .foregroundStyle(.white) + .foregroundStyle(Color.warmOnGreen) .padding(.bottom, 12) Text("\(state.name) is on your home screen.\nAdd the first dollar?") .font(.system(size: 15)) - .foregroundStyle(.white.opacity(0.85)) + .foregroundStyle(Color.warmOnGreen.opacity(0.85)) .multilineTextAlignment(.center) .lineSpacing(4) .padding(.horizontal, 40) @@ -790,16 +787,16 @@ struct AddGoalSuccessView: View { Image(systemName: "plus").font(.system(size: 14, weight: .semibold)) Text("Add a deposit").font(.system(size: 15, weight: .semibold)) } - .foregroundStyle(Color.warmGreen) + .foregroundStyle(Color.warmGreenFill) .frame(maxWidth: .infinity).frame(height: 52) - .background(Color.white).cornerRadius(14) + .background(Color.warmOnGreen).cornerRadius(14) } Button(action: onHome) { Text("Back to home") - .font(.system(size: 15, weight: .medium)).foregroundStyle(.white) + .font(.system(size: 15, weight: .medium)).foregroundStyle(Color.warmOnGreen) .frame(maxWidth: .infinity).frame(height: 52) - .background(Color.white.opacity(0.15)).cornerRadius(14) - .overlay(RoundedRectangle(cornerRadius: 14).stroke(Color.white.opacity(0.25), lineWidth: 1)) + .background(Color.warmOnGreen.opacity(0.15)).cornerRadius(14) + .overlay(RoundedRectangle(cornerRadius: 14).stroke(Color.warmOnGreen.opacity(0.25), lineWidth: 1)) } } .padding(.horizontal, 28).padding(.bottom, 52) diff --git a/Savely/Views/GoalsTab/GoalDetailView.swift b/Savely/Views/GoalsTab/GoalDetailView.swift index f6ad03a..32171f9 100644 --- a/Savely/Views/GoalsTab/GoalDetailView.swift +++ b/Savely/Views/GoalsTab/GoalDetailView.swift @@ -214,7 +214,7 @@ struct DepositCard: View { Button(action: saveDeposit) { Text(amountText.isEmpty ? "Add deposit" : "Add $\(amountText) to \(goalFirstName)") .font(.system(size: 15, weight: .semibold)) - .foregroundStyle(.white) + .foregroundStyle(Color.warmOnGreen) .frame(maxWidth: .infinity) .frame(height: 50) .background(goal.color) @@ -227,7 +227,7 @@ struct DepositCard: View { .background(Color.warmSurface) .cornerRadius(24) .overlay(RoundedRectangle(cornerRadius: 24).stroke(Color.warmLine, lineWidth: 1)) - .shadow(color: .black.opacity(0.06), radius: 16, x: 0, y: 8) + .shadow(color: Color.warmShadow, radius: 16, x: 0, y: 8) } private var depositAmount: Double? { Double(amountText) } diff --git a/Savely/Views/GoalsTab/GoalsView.swift b/Savely/Views/GoalsTab/GoalsView.swift index 6c327d0..968ec90 100755 --- a/Savely/Views/GoalsTab/GoalsView.swift +++ b/Savely/Views/GoalsTab/GoalsView.swift @@ -49,7 +49,7 @@ struct GoalsView: View { Spacer() Button(action: { showAddGoalFlow = true }) { Image(systemName: "plus") - .font(.system(size: 18, weight: .semibold)).foregroundStyle(.white) + .font(.system(size: 18, weight: .semibold)).foregroundStyle(Color.warmOnInk) .frame(width: 40, height: 40).background(Color.warmInk).cornerRadius(14) } } @@ -124,7 +124,7 @@ struct GoalsEmptyStateView: View { Spacer() Button(action: onCreateGoal) { Image(systemName: "plus") - .font(.system(size: 18, weight: .semibold)).foregroundStyle(.white) + .font(.system(size: 18, weight: .semibold)).foregroundStyle(Color.warmOnInk) .frame(width: 40, height: 40).background(Color.warmInk).cornerRadius(14) } } @@ -136,7 +136,7 @@ struct GoalsEmptyStateView: View { ZStack(alignment: .topTrailing) { ZStack { Circle() - .stroke(Color(red: 0.89, green: 0.867, blue: 0.816), lineWidth: 14) + .stroke(Color.warmTrack, lineWidth: 14) .frame(width: 160, height: 160) ZStack { Circle().fill(Color.warmGreenSoft).frame(width: 64, height: 64) @@ -168,9 +168,9 @@ struct GoalsEmptyStateView: View { Image(systemName: "plus").font(.system(size: 14, weight: .semibold)) Text("Create your first goal").font(.system(size: 15, weight: .semibold)) } - .foregroundStyle(.white) + .foregroundStyle(Color.warmOnGreen) .frame(maxWidth: .infinity).frame(height: 50) - .background(Color.warmGreen).cornerRadius(14) + .background(Color.warmGreenFill).cornerRadius(14) } // Suggestion chips @@ -221,7 +221,7 @@ struct WarmGoalCard: View { .fill(goal.color).frame(width: 44, height: 44) .overlay( Text(goal.name.prefix(1).uppercased()) - .font(.system(size: 22, weight: .regular, design: .serif)).foregroundStyle(.white) + .font(.system(size: 22, weight: .regular, design: .serif)).foregroundStyle(Color.warmOnGreen) ) VStack(alignment: .leading, spacing: 3) { HStack(spacing: 5) { diff --git a/Savely/Views/IncomesTab/IncomesTrackerView.swift b/Savely/Views/IncomesTab/IncomesTrackerView.swift index 9b16503..3f7d18c 100755 --- a/Savely/Views/IncomesTab/IncomesTrackerView.swift +++ b/Savely/Views/IncomesTab/IncomesTrackerView.swift @@ -107,9 +107,9 @@ struct IncomesTrackerView: View { Button(action: { viewModel.addIncome(); focusedField = nil }) { Image(systemName: "plus") .font(.system(size: 15, weight: .semibold)) - .foregroundStyle(.white) + .foregroundStyle(Color.warmOnGreen) .frame(width: 40, height: 40) - .background(Color.warmGreen) + .background(Color.warmGreenFill) .cornerRadius(10) } } diff --git a/Savely/Views/MainNavigationView.swift b/Savely/Views/MainNavigationView.swift index 7c56b10..526435a 100755 --- a/Savely/Views/MainNavigationView.swift +++ b/Savely/Views/MainNavigationView.swift @@ -73,12 +73,12 @@ struct WarmTabBar: View { Button(action: onAddTapped) { Image(systemName: "plus") .font(.system(size: 22, weight: .semibold)) - .foregroundStyle(.white) + .foregroundStyle(isExpanded ? Color.warmOnGreen : Color.warmOnInk) .frame(width: 52, height: 52) - .background(isExpanded ? Color.warmGreen : Color.warmInk) + .background(isExpanded ? Color.warmGreenFill : Color.warmInk) .cornerRadius(18) .shadow( - color: (isExpanded ? Color.warmGreen : .black).opacity(isExpanded ? 0.38 : 0.18), + color: isExpanded ? Color.warmGreenFill.opacity(0.38) : Color.warmShellShadow, radius: 10, x: 0, y: 4 ) .rotationEffect(.degrees(isExpanded ? 45 : 0)) @@ -172,8 +172,7 @@ private let quickActions: [QuickAction] = [ QuickAction(icon: "target", label: "Deposit to a goal", sub: "Move money toward a goal", bg: .warmSkySoft, fg: .warmSky, target: .deposit, isNewGoal: false), QuickAction(icon: "camera", label: "Scan a receipt", sub: "We'll read the total", bg: .warmClaySoft, fg: .warmClay, target: .expense, isNewGoal: false), QuickAction(icon: "plus", label: "New goal", sub: "Start something new", - bg: Color(red: 0.925, green: 0.910, blue: 0.957), - fg: Color(red: 0.490, green: 0.416, blue: 0.722), target: nil, isNewGoal: true), + bg: .warmLilacSoft, fg: .warmLilac, target: nil, isNewGoal: true), ] struct WarmActionSheet: View { @@ -526,9 +525,9 @@ struct WarmQuickIncomeView: View { } } .font(.system(size: 11, weight: .bold)) - .foregroundStyle(autoMoveArmed ? Color.warmGreenDeep : .white) + .foregroundStyle(autoMoveArmed ? Color.warmGreenDeep : Color.warmOnGreen) .padding(.horizontal, 10).padding(.vertical, 6) - .background(autoMoveArmed ? Color.warmSurface : Color.warmGreen) + .background(autoMoveArmed ? Color.warmSurface : Color.warmGreenFill) .clipShape(Capsule()) .overlay(Capsule().stroke(autoMoveArmed ? Color.warmGreen : Color.clear, lineWidth: 1)) } @@ -649,7 +648,7 @@ struct WarmQuickDepositView: View { .overlay( Text(goal.name.prefix(1).uppercased()) .font(.system(size: 16, weight: .regular, design: .serif)) - .foregroundStyle(.white) + .foregroundStyle(Color.warmOnGreen) ) VStack(alignment: .leading, spacing: 4) { Text(goal.name) @@ -669,9 +668,9 @@ struct WarmQuickDepositView: View { Circle().stroke(isOn ? Color.warmGreen : Color.warmLine, lineWidth: 2) .frame(width: 22, height: 22) if isOn { - Circle().fill(Color.warmGreen).frame(width: 22, height: 22) + Circle().fill(Color.warmGreenFill).frame(width: 22, height: 22) Image(systemName: "checkmark") - .font(.system(size: 10, weight: .bold)).foregroundStyle(.white) + .font(.system(size: 10, weight: .bold)).foregroundStyle(Color.warmOnGreen) } } } @@ -700,9 +699,9 @@ struct WarmQuickDepositView: View { Button(action: saveDeposit) { let goalName = selectedGoal.map { $0.name.components(separatedBy: ",").first ?? $0.name } Text(goalName.map { "Add $\(Int(selectedPreset)) to \($0)" } ?? "Select a goal") - .font(.system(size: 15, weight: .semibold)).foregroundStyle(.white) + .font(.system(size: 15, weight: .semibold)).foregroundStyle(Color.warmOnGreen) .frame(maxWidth: .infinity).frame(height: 50) - .background(selectedGoal != nil ? Color.warmGreen : Color.warmInkMuted) + .background(selectedGoal != nil ? Color.warmGreenFill : Color.warmInkMuted) .cornerRadius(14) } .disabled(selectedGoal == nil) diff --git a/Savely/Views/Onboarding/OnboardingView.swift b/Savely/Views/Onboarding/OnboardingView.swift index 1a76c7b..330e6b9 100644 --- a/Savely/Views/Onboarding/OnboardingView.swift +++ b/Savely/Views/Onboarding/OnboardingView.swift @@ -47,8 +47,8 @@ struct OnboardingView: View { .font(.system(size: 16, weight: .semibold)) .frame(maxWidth: .infinity) .padding(.vertical, 16) - .background(Color.warmGreen) - .foregroundColor(.white) + .background(Color.warmGreenFill) + .foregroundColor(Color.warmOnGreen) .cornerRadius(16) } .padding(.horizontal, 20) diff --git a/Savely/Views/ProfileTab/NotificationTimePicker.swift b/Savely/Views/ProfileTab/NotificationTimePicker.swift deleted file mode 100644 index d20269a..0000000 --- a/Savely/Views/ProfileTab/NotificationTimePicker.swift +++ /dev/null @@ -1,58 +0,0 @@ -// -// NotificationTimePicker.swift -// Savely -// -// Created by Ivan Lorenzana Belli on 08/12/24. -// - -import SwiftUI - -struct NotificationTimePicker: View { - var title: String - @Binding var selectedTime: Date - var onSave: (Date) -> Void - - var body: some View { - VStack(spacing: 20) { - Text(title) - .font(.title2) - .fontWeight(.bold) - .multilineTextAlignment(.center) - - DatePicker( - Strings.Placeholders.selectTimePLaceholder, - selection: $selectedTime, - displayedComponents: .hourAndMinute - ) - .datePickerStyle(WheelDatePickerStyle()) - .labelsHidden() - .frame(height: 200) - - Button(action: { - onSave(selectedTime) - }) { - Text(Strings.Buttons.saveButton) - .fontWeight(.bold) - .frame(maxWidth: .infinity) - .padding() - .background(Color("primaryGreen")) - .foregroundColor(.white) - .cornerRadius(UIConstants.UICornerRadius.cornerRadius) - } - .padding() - } - .padding() - .background(Color("cardBackgroundColor")) - .cornerRadius(UIConstants.UICornerRadius.cornerRadius) - .shadow(radius: 5) - } -} - -struct NotificationTimePicker_Previews: PreviewProvider { - static var previews: some View { - NotificationTimePicker( - title: "Select Reminder Time", - selectedTime: .constant(Date()) - ) { _ in } - } -} diff --git a/Savely/Views/ProfileTab/ProfileView.swift b/Savely/Views/ProfileTab/ProfileView.swift index c7d3bc2..926c174 100755 --- a/Savely/Views/ProfileTab/ProfileView.swift +++ b/Savely/Views/ProfileTab/ProfileView.swift @@ -87,26 +87,26 @@ struct ProfileView: View { private var lifetimeIncomeCard: some View { ZStack(alignment: .topTrailing) { Circle() - .fill(Color.white.opacity(0.08)) + .fill(Color.warmOnGreen.opacity(0.08)) .frame(width: 140, height: 140) .offset(x: 30, y: -20) VStack(alignment: .leading, spacing: 4) { Text("LIFETIME INCOME") .font(.system(size: 11, weight: .semibold)) - .foregroundStyle(Color.white.opacity(0.7)) + .foregroundStyle(Color.warmOnGreen.opacity(0.7)) .tracking(0.8) Text(formattedAmount(lifetimeIncome)) .font(.system(size: 40, weight: .regular, design: .serif)) - .foregroundStyle(.white) + .foregroundStyle(Color.warmOnGreen) Text(savingSinceSubtitle) .font(.system(size: 13)) - .foregroundStyle(Color.white.opacity(0.75)) + .foregroundStyle(Color.warmOnGreen.opacity(0.75)) } .frame(maxWidth: .infinity, alignment: .leading) .padding(20) } - .background(Color.warmGreen) + .background(Color.warmGreenFill) .cornerRadius(22) .clipped() .accessibilityElement(children: .combine) diff --git a/Savely/Views/SplashScreenView.swift b/Savely/Views/SplashScreenView.swift index eab1965..d7fe546 100755 --- a/Savely/Views/SplashScreenView.swift +++ b/Savely/Views/SplashScreenView.swift @@ -26,6 +26,9 @@ struct SplashScreenView: View { private let letters = Array("Savely") // Dark-variant palette (matches AppIcon's dark appearance) + // Deliberate constants: the splash is the same dark-green frame in both + // appearances (it is the brand moment, not a screen), so it does not use + // the adaptive tokens. private let bgInk = Color(red: 0.122, green: 0.169, blue: 0.137) // #1f2b23 private let leafLight = Color(red: 0.490, green: 0.722, blue: 0.596) // #7db898 private let leafShaded = Color(red: 0.365, green: 0.612, blue: 0.486) // #5d9c7c From 10ae9c3e2c5a73698b5c7fee73a7f685fe716009 Mon Sep 17 00:00:00 2001 From: Belli Date: Sat, 15 Aug 2026 22:14:15 -0700 Subject: [PATCH 5/8] feat: turn the Dark Mode switch back on MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The palette now resolves per appearance, so the switch does what it says. Kept as an absolute override (off = light, on = dark) rather than following the system when off — owner's call. --- Savely/Utilities/FeatureFlags.swift | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/Savely/Utilities/FeatureFlags.swift b/Savely/Utilities/FeatureFlags.swift index 4fad451..8331f7c 100644 --- a/Savely/Utilities/FeatureFlags.swift +++ b/Savely/Utilities/FeatureFlags.swift @@ -31,10 +31,9 @@ enum FeatureFlags { /// release; this flag remains as the kill switch. static let autoMoveSuggestionsEnabled = true - /// The Dark Mode switch in Profile. The palette in `Color+Warm.swift` - /// is still light-only, so flipping the switch changed nothing except - /// the status bar. Hidden until the adaptive palette lands - /// (docs/plans/pr-d2-adaptive-palette.md), which flips this back on. - /// The `@AppStorage("darkModeEnabled")` plumbing stays in place. - static let darkModeEnabled = false + /// The Dark Mode switch in Profile. ON since the adaptive palette + /// (Color+Warm.swift carries a dark value per token). The switch is an + /// absolute override — off forces light, on forces dark; the system + /// appearance is not followed (owner's decision, 2026-08-16). + static let darkModeEnabled = true } From 9f6bf631709348feb332e10bb2b4866f17972253 Mon Sep 17 00:00:00 2001 From: Belli Date: Sat, 15 Aug 2026 22:14:32 -0700 Subject: [PATCH 6/8] fix(palette): two light-mode AA corrections warmInkMuted #8c8880 -> #746f66: 3.21:1 -> 4.54:1 on the ground and 3.53 -> 4.99 on paper. It carries every 12-13pt metadata line, which is body text under WCAG, and it failed AA. Same warm-ink family, one step darker. warmAmber #c48a2a -> #a8741c: 2.55:1 -> 3.45:1 as a glyph on amber-soft, clearing 1.4.11 for graphical objects. Dark values untouched. Owner accepted both (2026-08-16). --- Savely/Resources/Color+Warm.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Savely/Resources/Color+Warm.swift b/Savely/Resources/Color+Warm.swift index 74437f6..962f1a0 100644 --- a/Savely/Resources/Color+Warm.swift +++ b/Savely/Resources/Color+Warm.swift @@ -15,7 +15,7 @@ extension Color { static let warmSurface = Color(light: 0xFFFFFF, dark: 0x262420) static let warmInk = Color(light: 0x1A1A17, dark: 0xF2EFE7) static let warmInkSoft = Color(light: 0x55524C, dark: 0xC6C1B5) - static let warmInkMuted = Color(light: 0x8C8880, dark: 0x9D978A) + static let warmInkMuted = Color(light: 0x746F66, dark: 0x9D978A) // light was #8c8880 (3.2:1); #746f66 clears AA text at 4.5:1 static let warmLine = Color(light: 0x1E190F, dark: 0xF2EFE7, lightAlpha: 0.08, darkAlpha: 0.10) static let warmLineSoft = Color(light: 0x1E190F, dark: 0xF2EFE7, lightAlpha: 0.04, darkAlpha: 0.05) @@ -30,7 +30,7 @@ extension Color { static let warmGreenTint = Color(light: 0xF2F7F3, dark: 0x1B2A21) // Category tints — a glyph on a soft tile, never a fill. - static let warmAmber = Color(light: 0xC48A2A, dark: 0xDEA64A) + static let warmAmber = Color(light: 0xA8741C, dark: 0xDEA64A) // light was #c48a2a (2.6:1 on amber-soft); #a8741c clears 3:1 for glyphs static let warmAmberSoft = Color(light: 0xF6ECD6, dark: 0x3A2F19) /// Amber as *text* on an amber-soft banner (the accent itself is too light to read at 12pt). static let warmAmberDeep = Color(light: 0x7A5618, dark: 0xEBC27A) From bdadad0cc794cba7ac9f78dedac4ffa89989eb20 Mon Sep 17 00:00:00 2001 From: Belli Date: Sat, 15 Aug 2026 22:18:33 -0700 Subject: [PATCH 7/8] docs: DESIGN.md carries the dark values; green-fill dark nudged to #367c56 Frontmatter now lists every token's light and dark value (light stays normative for the look). Prose updated for the ink-muted and amber AA corrections, the green ink/fill split, and the on-green/on-ink roles. warmGreenFill dark #357a55 -> #367c56: its ratio against the dark surface rounded to 3.00 but sat at 2.9995; the nudge clears 3:1 (3.08) with white content still at 5.03:1. --- DESIGN.md | 52 ++++++++++++++++++++++------ Savely/Resources/Color+Warm.swift | 2 +- docs/plans/pr-d2-adaptive-palette.md | 2 +- 3 files changed, 44 insertions(+), 12 deletions(-) diff --git a/DESIGN.md b/DESIGN.md index 0d7f109..afb5e0a 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -2,23 +2,54 @@ name: Savely description: A warm, patient, local-first savings app — the Warm Meadow system colors: + # light / dark — every token is trait-aware (Color+Warm.swift). Light is normative for the look; dark is "the notebook under a lamp". bg: "#f6f4ee" + bg-dark: "#161512" surface: "#ffffff" + surface-dark: "#262420" ink: "#1a1a17" + ink-dark: "#f2efe7" ink-soft: "#55524c" - ink-muted: "#8c8880" + ink-soft-dark: "#c6c1b5" + ink-muted: "#746f66" + ink-muted-dark: "#9d978a" line: "rgba(30, 25, 15, 0.08)" + line-dark: "rgba(242, 239, 231, 0.10)" line-soft: "rgba(30, 25, 15, 0.04)" + line-soft-dark: "rgba(242, 239, 231, 0.05)" green: "#2f6b4a" + green-dark: "#78b58f" + green-fill: "#2f6b4a" + green-fill-dark: "#367c56" green-deep: "#1f4a33" + green-deep-dark: "#b6dcc4" green-soft: "#e8f0ea" + green-soft-dark: "#22382c" green-tint: "#f2f7f3" - amber: "#c48a2a" + green-tint-dark: "#1b2a21" + amber: "#a8741c" + amber-dark: "#dea64a" + amber-deep: "#7a5618" + amber-deep-dark: "#ebc27a" amber-soft: "#f6ecd6" + amber-soft-dark: "#3a2f19" clay: "#b85c42" + clay-dark: "#d9846a" clay-soft: "#f6e1d8" + clay-soft-dark: "#3d2721" sky: "#4a7ba6" + sky-dark: "#7ea9d0" sky-soft: "#dce8f2" + sky-soft-dark: "#1e2d3b" + lilac: "#7d6ab8" + lilac-dark: "#b3a4dc" + lilac-soft: "#ece8f4" + lilac-soft-dark: "#2e2740" + on-green: "#ffffff" + on-ink: "#ffffff" + on-ink-dark: "#161512" + track: "#ede6d4" + track-dark: "#3a362e" typography: display: fontFamily: "system serif (New York)" @@ -66,13 +97,13 @@ spacing: hero: "24pt" components: button-primary: - backgroundColor: "{colors.green}" - textColor: "#ffffff" + backgroundColor: "{colors.green-fill}" + textColor: "{colors.on-green}" rounded: "{rounded.control}" height: "48–50pt" button-primary-dark-shell: backgroundColor: "{colors.ink}" - textColor: "#ffffff" + textColor: "{colors.on-ink}" rounded: "18pt" size: "52pt" button-icon-outline: @@ -141,16 +172,17 @@ KPI grids and pie charts; cold gray-on-white spreadsheets. ## Colors -A warm neutral ground with a single committed green; everything else is a soft tint that holds an icon. +A warm neutral ground with a single committed green; everything else is a soft tint that holds an icon. Every token is trait-aware: dark mode is the same notebook under a lamp — same hues at low chroma, ink flips light, the `-soft` tints become deep tints of their own hue, and the one green splits into *ink* (brightens) and *fill* (stays deep). Depth in dark is the hairline (`line-dark`, warm white 10%), exactly as in light. ### Primary -- **Meadow Green** (`green`, #2f6b4a): the only saturated color. Primary buttons, income figures, active tab, progress rings, selected-state text. It is what "growing" looks like. +- **Meadow Green** (`green`, #2f6b4a): the only saturated color, used *as ink* — income figures, active tab, progress rings and bars, selected-state text, toggle tint. It brightens to #78b58f in dark. It is what "growing" looks like. +- **Meadow Green Fill** (`green-fill`, #2f6b4a light / #367c56 dark): green *as a surface* — primary buttons, the income card, the Planted screen, the selected radio. Stays deep in dark so its white content (`on-green`) keeps ≥5:1. - **Meadow Green Deep** (`green-deep`, #1f4a33): text on green-soft chips and banners; darker end of the ring gradient. - **Meadow Green Soft** (`green-soft`, #e8f0ea): selected chip fill, income icon tile, the auto-move banner. Never text. - **Meadow Green Tint** (`green-tint`, #f2f7f3): selected row background in goal pickers; the tip-of-the-day card. The quietest possible "this one". ### Secondary (category tints — icons and chips only) -- **Amber** (`amber`, #c48a2a) on **Amber Soft** (`amber-soft`, #f6ecd6): coffee / expense category, the "favorite" star, the identity monogram tile. +- **Amber** (`amber`, #a8741c) on **Amber Soft** (`amber-soft`, #f6ecd6): coffee / expense category, the "favorite" star. Was #c48a2a (2.6:1 on its tile); corrected in PR D2 to 3.5:1 for glyphs. **Amber Deep** (`amber-deep`, #7a5618) is amber as *text* on an amber-soft banner. - **Clay** (`clay`, #b85c42) on **Clay Soft** (`clay-soft`, #f6e1d8): shopping / receipt scanning tile; the *only* negative-trend color (income badge going down). - **Sky** (`sky`, #4a7ba6) on **Sky Soft** (`sky-soft`, #dce8f2): goals / transit tile. @@ -159,7 +191,7 @@ A warm neutral ground with a single committed green; everything else is a soft t - **Paper** (`surface`, #ffffff): cards, rows, action sheets, tab bar. Sits on the ground with a hairline, never a shadow. - **Warm Ink** (`ink`, #1a1a17): all display and title type; primary body; the dark "+" shell and the scan-receipt banner. - **Warm Ink Soft** (`ink-soft`, #55524c): body copy under a title, unselected chip text, settings icons. -- **Warm Ink Muted** (`ink-muted`, #8c8880): metadata — dates, "August · $500", uppercase section labels, placeholders. **Measured 3.2:1 on ground / 3.5:1 on paper: passes AA only as large text (≥18pt or bold ≥14pt).** Its current use at 12–13pt regular is a known AA gap, tracked in `docs/plans/pr-d-profile.md`. +- **Warm Ink Muted** (`ink-muted`, #746f66): metadata — dates, "August · $500", uppercase section labels, placeholders. Was #8c8880 (3.2:1, failing AA at 12–13pt); corrected in PR D2 to 4.5:1 on the ground / 5.0 on paper — same warm-ink family, one step darker. - **Hairline** (`line`, 8% warm black) and **Hairline Soft** (`line-soft`, 4%): card borders and row dividers respectively. This *is* the depth system. ### Named Rules @@ -270,5 +302,5 @@ A 36–44pt rounded square (10–12pt radius) in a `-soft` tint holding one 14 - **Don't** fill a button with amber, clay or sky, and don't use a `-soft` tint as a card background. - **Don't** bold the serif or use it below 18pt. - **Don't** introduce gradients, glass, or a second accent hue. -- **Don't** hardcode `.white` / `Color(red:…)` in a view — every color goes through `Color+Warm.swift`, which is the single place a dark scheme can be introduced. +- **Don't** hardcode `.white` / `Color(red:…)` in a view — every color goes through `Color+Warm.swift`, where each token carries its light and dark value. White content on a green fill is `on-green`; on an ink fill it is `on-ink` (which flips to the ground in dark). The only literals left are deliberate and commented: the splash frame, the sprout mark, camera-overlay controls, the PDF. - **Don't** invent a new kicker or eyebrow style; the existing 11pt uppercase tracked label is the one and only. diff --git a/Savely/Resources/Color+Warm.swift b/Savely/Resources/Color+Warm.swift index 962f1a0..e03c773 100644 --- a/Savely/Resources/Color+Warm.swift +++ b/Savely/Resources/Color+Warm.swift @@ -24,7 +24,7 @@ extension Color { // surface* (CTAs, the income card, the selected radio) that carries // white content, and stays deep in both schemes. static let warmGreen = Color(light: 0x2F6B4A, dark: 0x78B58F) - static let warmGreenFill = Color(light: 0x2F6B4A, dark: 0x357A55) // also "green that stays deep" as ink on white pills + static let warmGreenFill = Color(light: 0x2F6B4A, dark: 0x367C56) // also "green that stays deep" as ink on white pills static let warmGreenDeep = Color(light: 0x1F4A33, dark: 0xB6DCC4) static let warmGreenSoft = Color(light: 0xE8F0EA, dark: 0x22382C) static let warmGreenTint = Color(light: 0xF2F7F3, dark: 0x1B2A21) diff --git a/docs/plans/pr-d2-adaptive-palette.md b/docs/plans/pr-d2-adaptive-palette.md index b722d7a..c6e9b6d 100644 --- a/docs/plans/pr-d2-adaptive-palette.md +++ b/docs/plans/pr-d2-adaptive-palette.md @@ -97,7 +97,7 @@ pairs ≥ 3.0. | line | warm black 8% | warm white 10% | — | | line-soft | warm black 4% | warm white 5% | — | | green (text/icon) | `#2f6b4a` | `#78b58f` | 7.7 / 6.9 ✓ | -| green-fill (CTA, income card) | `#2f6b4a` | `#2f6b4a` (unchanged, white text 6.3 ✓) | ✓ | +| green-fill (CTA, income card) | `#2f6b4a` | `#367c56` (white text 5.0 ✓, ≥3:1 vs surface) | ✓ | | green-deep (text on green-soft) | `#1f4a33` | `#b6dcc4` | 8.4 on green-soft ✓ | | green-soft | `#e8f0ea` | `#22382c` | — | | green-tint | `#f2f7f3` | `#1b2a21` | — | From 630371195107616e3d9f02d0abc2159b0194ea07 Mon Sep 17 00:00:00 2001 From: Belli Date: Sat, 15 Aug 2026 22:41:48 -0700 Subject: [PATCH 8/8] feat(icon): Icon Composer app icon with light, dark and tinted appearances On iOS 26+ the Home Screen takes its appearance variants from an Icon Composer document, not from the PNGs in a legacy .appiconset. Savely shipped dark/tinted PNGs that the system no longer reads, so the icon stayed light whatever the Home Screen's icon appearance was. - Savely/AppIcon.icon: the sprout as three SVG layers (light, dark, mono) drawn from the same 120-unit geometry as SproutMark.swift, on a warm cream gradient in light and the green-ink #27382d in dark; the sprout is opaque (the mark is flat) with a soft neutral shadow. Validated by rendering Default, Dark and TintedDark with Icon Composer's ictool; the built Assets.car carries an IconImageStack per appearance. - The legacy AppIcon.appiconset is removed (it would collide on the "AppIcon" name, and the deployment floor is iOS 26). - SproutMark's shaded leaf becomes trait-aware (#37795a light / #5d9c7c dark) so the in-app mark reads on the dark ground too. --- Savely.xcodeproj/project.pbxproj | 4 ++ Savely/AppIcon.icon/Assets/sprout-dark.svg | 6 ++ Savely/AppIcon.icon/Assets/sprout-light.svg | 6 ++ Savely/AppIcon.icon/Assets/sprout-mono.svg | 6 ++ Savely/AppIcon.icon/icon.json | 54 ++++++++++++++++++ .../AppIcon.appiconset/Contents.json | 38 ------------ .../AppIcon.appiconset/icon-dark.png | Bin 23159 -> 0 bytes .../AppIcon.appiconset/icon-light.png | Bin 23912 -> 0 bytes .../AppIcon.appiconset/icon-tinted.png | Bin 21761 -> 0 bytes Savely/Resources/Localizable.xcstrings | 9 --- Savely/Views/Components/SproutMark.swift | 3 +- 11 files changed, 78 insertions(+), 48 deletions(-) create mode 100644 Savely/AppIcon.icon/Assets/sprout-dark.svg create mode 100644 Savely/AppIcon.icon/Assets/sprout-light.svg create mode 100644 Savely/AppIcon.icon/Assets/sprout-mono.svg create mode 100644 Savely/AppIcon.icon/icon.json delete mode 100755 Savely/Assets.xcassets/AppIcon.appiconset/Contents.json delete mode 100644 Savely/Assets.xcassets/AppIcon.appiconset/icon-dark.png delete mode 100644 Savely/Assets.xcassets/AppIcon.appiconset/icon-light.png delete mode 100644 Savely/Assets.xcassets/AppIcon.appiconset/icon-tinted.png diff --git a/Savely.xcodeproj/project.pbxproj b/Savely.xcodeproj/project.pbxproj index 11853e9..a1ea5b7 100644 --- a/Savely.xcodeproj/project.pbxproj +++ b/Savely.xcodeproj/project.pbxproj @@ -45,6 +45,7 @@ 1DC014132CE5B0C5009A46E1 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DC014122CE5B0C5009A46E1 /* ContentView.swift */; }; 1DC258082CC07DCC0002DDE3 /* SavelyApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DC258072CC07DCC0002DDE3 /* SavelyApp.swift */; }; 1DC2580C2CC07DCF0002DDE3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1DC2580B2CC07DCF0002DDE3 /* Assets.xcassets */; }; + A11A000AC0DE5AFE1000000A /* AppIcon.icon in Resources */ = {isa = PBXBuildFile; fileRef = A11A0009C0DE5AFE10000009 /* AppIcon.icon */; }; 1DC2580F2CC07DCF0002DDE3 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1DC2580E2CC07DCF0002DDE3 /* Preview Assets.xcassets */; }; 1DC258192CC07DCF0002DDE3 /* SavelyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DC258182CC07DCF0002DDE3 /* SavelyTests.swift */; }; 1DC258232CC07DCF0002DDE3 /* SavelyUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DC258222CC07DCF0002DDE3 /* SavelyUITests.swift */; }; @@ -135,6 +136,7 @@ 1DC258042CC07DCC0002DDE3 /* Savely.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Savely.app; sourceTree = BUILT_PRODUCTS_DIR; }; 1DC258072CC07DCC0002DDE3 /* SavelyApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavelyApp.swift; sourceTree = ""; }; 1DC2580B2CC07DCF0002DDE3 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + A11A0009C0DE5AFE10000009 /* AppIcon.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = AppIcon.icon; sourceTree = ""; }; 1DC2580E2CC07DCF0002DDE3 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 1DC258142CC07DCF0002DDE3 /* SavelyTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SavelyTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 1DC258182CC07DCF0002DDE3 /* SavelyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavelyTests.swift; sourceTree = ""; }; @@ -419,6 +421,7 @@ 1D3669842CC62EDD00E84FF9 /* ViewModels */, 1DC258072CC07DCC0002DDE3 /* SavelyApp.swift */, 1DC2580B2CC07DCF0002DDE3 /* Assets.xcassets */, + A11A0009C0DE5AFE10000009 /* AppIcon.icon */, 1DC2580D2CC07DCF0002DDE3 /* Preview Content */, 1DAF5D212CEE9989001C0AF3 /* Info.plist */, 1DAF5D232CEE9AF4001C0AF3 /* Config.plist */, @@ -625,6 +628,7 @@ files = ( 1DC2580F2CC07DCF0002DDE3 /* Preview Assets.xcassets in Resources */, 1DC2580C2CC07DCF0002DDE3 /* Assets.xcassets in Resources */, + A11A000AC0DE5AFE1000000A /* AppIcon.icon in Resources */, 1DAF5D242CEE9AF4001C0AF3 /* Config.plist in Resources */, 1D36698B2CC62FD700E84FF9 /* Localizable.xcstrings in Resources */, ); diff --git a/Savely/AppIcon.icon/Assets/sprout-dark.svg b/Savely/AppIcon.icon/Assets/sprout-dark.svg new file mode 100644 index 0000000..c9749c9 --- /dev/null +++ b/Savely/AppIcon.icon/Assets/sprout-dark.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/Savely/AppIcon.icon/Assets/sprout-light.svg b/Savely/AppIcon.icon/Assets/sprout-light.svg new file mode 100644 index 0000000..9a8f3b2 --- /dev/null +++ b/Savely/AppIcon.icon/Assets/sprout-light.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/Savely/AppIcon.icon/Assets/sprout-mono.svg b/Savely/AppIcon.icon/Assets/sprout-mono.svg new file mode 100644 index 0000000..ed592db --- /dev/null +++ b/Savely/AppIcon.icon/Assets/sprout-mono.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/Savely/AppIcon.icon/icon.json b/Savely/AppIcon.icon/icon.json new file mode 100644 index 0000000..9325a04 --- /dev/null +++ b/Savely/AppIcon.icon/icon.json @@ -0,0 +1,54 @@ +{ + "fill": { + "linear-gradient": [ + "srgb:0.980,0.973,0.953,1.000", + "srgb:0.933,0.918,0.878,1.000" + ], + "specializations": [ + { + "appearance": "dark", + "value": { + "solid": "srgb:0.153,0.220,0.176,1.000" + } + } + ] + }, + "groups": [ + { + "layers": [ + { + "image-name": "sprout-light.svg", + "name": "sprout", + "specializations": [ + { + "appearance": "dark", + "value": { + "image-name": "sprout-dark.svg" + } + }, + { + "appearance": "tinted", + "value": { + "image-name": "sprout-mono.svg" + } + } + ] + } + ], + "shadow": { + "kind": "neutral", + "opacity": 0.3 + }, + "translucency": { + "enabled": false, + "value": 0.0 + } + } + ], + "supported-platforms": { + "circles": [ + "watchOS" + ], + "squares": "shared" + } +} \ No newline at end of file diff --git a/Savely/Assets.xcassets/AppIcon.appiconset/Contents.json b/Savely/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100755 index 2eaa70c..0000000 --- a/Savely/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "images" : [ - { - "filename" : "icon-light.png", - "idiom" : "universal", - "platform" : "ios", - "size" : "1024x1024" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "icon-dark.png", - "idiom" : "universal", - "platform" : "ios", - "size" : "1024x1024" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "tinted" - } - ], - "filename" : "icon-tinted.png", - "idiom" : "universal", - "platform" : "ios", - "size" : "1024x1024" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Savely/Assets.xcassets/AppIcon.appiconset/icon-dark.png b/Savely/Assets.xcassets/AppIcon.appiconset/icon-dark.png deleted file mode 100644 index fa19be238950b1db9b32e5e6d9ceb9f978dc8717..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 23159 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 RaiO<}jg`G+p}F_vUjbhO+6Djs diff --git a/Savely/Assets.xcassets/AppIcon.appiconset/icon-light.png b/Savely/Assets.xcassets/AppIcon.appiconset/icon-light.png deleted file mode 100644 index 9a5b98ea23aafa705804687d992f3221623e4511..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 23912 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 QCnALK>tXY(U&z1zKc~@r(EtDd diff --git a/Savely/Resources/Localizable.xcstrings b/Savely/Resources/Localizable.xcstrings index 752565d..e20014f 100755 --- a/Savely/Resources/Localizable.xcstrings +++ b/Savely/Resources/Localizable.xcstrings @@ -806,9 +806,6 @@ } } } - }, - "Display Name" : { - }, "download_weekly_report_button" : { "comment" : "Download Weekly Report Button", @@ -833,9 +830,6 @@ }, "Edit anything" : { - }, - "Edit Profile" : { - }, "email_placeholder_title" : { "comment" : "Email Placeholder Label", @@ -3018,9 +3012,6 @@ }, "TO WHICH GOAL" : { - }, - "Total income logged in Savely" : { - }, "track_expenses_label" : { "comment" : "Track Expenses Label", diff --git a/Savely/Views/Components/SproutMark.swift b/Savely/Views/Components/SproutMark.swift index b46839f..046693f 100644 --- a/Savely/Views/Components/SproutMark.swift +++ b/Savely/Views/Components/SproutMark.swift @@ -83,7 +83,8 @@ struct SproutLeafShape: Shape { /// (deep greens on whatever background the caller provides). struct SproutMark: View { var leafPrimary: Color = .warmGreen - var leafSecondary: Color = Color(red: 0.216, green: 0.475, blue: 0.353) // #37795a — brand mark, deliberately fixed + /// The shaded leaf: the icon's #37795a in light, its dark-icon #5d9c7c in dark. + var leafSecondary: Color = Color(light: 0x37795A, dark: 0x5D9C7C) var seed: Color = .warmAmber /// Stem line width as a fraction of the mark's width (icon uses 5.5/120). var stemWidthRatio: CGFloat = 5.5 / 120