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.xcodeproj/project.pbxproj b/Savely.xcodeproj/project.pbxproj index 1a5292b..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 */; }; @@ -61,16 +62,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 */; }; @@ -138,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 = ""; }; @@ -156,17 +155,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 +258,6 @@ path = Utilities; sourceTree = ""; }; - 1DAF5D012CED9475001C0AF3 /* Buttons */ = { - isa = PBXGroup; - children = ( - 1DE4B4D72CE7B10E006C8F65 /* PrimaryButton.swift */, - ); - path = Buttons; - sourceTree = ""; - }; 1DAF5D022CED9481001C0AF3 /* CameraScanner */ = { isa = PBXGroup; children = ( @@ -381,7 +369,6 @@ 1DC014042CDE00F7009A46E1 /* ProfileTab */ = { isa = PBXGroup; children = ( - 1DE9FE112D0650150069AA64 /* NotificationTimePicker.swift */, 1DAF5D1F2CEE9767001C0AF3 /* TipHistoryView.swift */, 1DC2583B2CC081D90002DDE3 /* ProfileView.swift */, 1DF722932F9C19E600320B38 /* AchievementsView.swift */, @@ -434,6 +421,7 @@ 1D3669842CC62EDD00E84FF9 /* ViewModels */, 1DC258072CC07DCC0002DDE3 /* SavelyApp.swift */, 1DC2580B2CC07DCF0002DDE3 /* Assets.xcassets */, + A11A0009C0DE5AFE10000009 /* AppIcon.icon */, 1DC2580D2CC07DCF0002DDE3 /* Preview Content */, 1DAF5D212CEE9989001C0AF3 /* Info.plist */, 1DAF5D232CEE9AF4001C0AF3 /* Config.plist */, @@ -492,12 +480,10 @@ isa = PBXGroup; children = ( F38DAD27B5231BDC080F36DA /* SproutMark.swift */, - 1DF579C42F673A8B009A41B3 /* InsightRow.swift */, 1DEACCDD2D0BE6340084CC39 /* NetworkErrorView.swift */, 1DAF5D042CED94F1001C0AF3 /* Tips */, 1DAF5D032CED94DD001C0AF3 /* TextFields */, 1DAF5D022CED9481001C0AF3 /* CameraScanner */, - 1DAF5D012CED9475001C0AF3 /* Buttons */, ); path = Components; sourceTree = ""; @@ -642,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 */, ); @@ -700,7 +687,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 +696,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/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 fa19be2..0000000 Binary files a/Savely/Assets.xcassets/AppIcon.appiconset/icon-dark.png and /dev/null differ 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 9a5b98e..0000000 Binary files a/Savely/Assets.xcassets/AppIcon.appiconset/icon-light.png and /dev/null differ diff --git a/Savely/Assets.xcassets/AppIcon.appiconset/icon-tinted.png b/Savely/Assets.xcassets/AppIcon.appiconset/icon-tinted.png deleted file mode 100644 index 24e60e2..0000000 Binary files a/Savely/Assets.xcassets/AppIcon.appiconset/icon-tinted.png and /dev/null differ 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 345ca1f..e03c773 100644 --- a/Savely/Resources/Color+Warm.swift +++ b/Savely/Resources/Color+Warm.swift @@ -1,22 +1,81 @@ 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: 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) + + // 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: 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) + + // Category tints — a glyph on a soft tile, never a fill. + 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) + 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) + /// The raised "+" shell's shadow (deeper than the hero lift). + static let warmShellShadow = Color(light: 0x000000, dark: 0x000000, lightAlpha: 0.18, darkAlpha: 0.60) +} + +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 + 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 + ) + } } diff --git a/Savely/Resources/Localizable.xcstrings b/Savely/Resources/Localizable.xcstrings index 82de2d0..e20014f 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", @@ -814,139 +511,265 @@ "en" : { "stringUnit" : { "state" : "new", - "value" : "Create Account" + "value" : "Create Account" + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Crear una cuenta" + } + } + } + }, + "create_account_title" : { + "comment" : "Create Account Title", + "extractionState" : "stale", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Create your account!" + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "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" : "Crear una cuenta" + "value" : "Ha pagado una deuda importante." } } } }, - "create_account_title" : { - "comment" : "Create Account Title", + "debt_paid_off_title" : { + "comment" : "Title for paying off debt", "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { "state" : "new", - "value" : "Create your account!" + "value" : "Debt Paid Off" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "¡Crea tú cuenta!" + "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", @@ -983,9 +806,6 @@ } } } - }, - "Display Name" : { - }, "download_weekly_report_button" : { "comment" : "Download Weekly Report Button", @@ -1010,9 +830,6 @@ }, "Edit anything" : { - }, - "Edit Profile" : { - }, "email_placeholder_title" : { "comment" : "Email Placeholder Label", @@ -1659,6 +1476,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 +1649,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 +1854,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 +1938,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 +2247,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 +2331,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 +2688,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", @@ -3033,9 +3012,6 @@ }, "TO WHICH GOAL" : { - }, - "Total income logged in Savely" : { - }, "track_expenses_label" : { "comment" : "Track Expenses 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/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/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 } 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/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/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..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 + /// 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 diff --git a/Savely/Views/DashboardTab/DashboardView.swift b/Savely/Views/DashboardTab/DashboardView.swift index 6765205..35adb32 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 — @@ -198,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)) @@ -251,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) } @@ -272,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) } @@ -363,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 787d57e..926c174 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,77 +76,37 @@ 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) { 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) - Text("Total income logged in Savely") + .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) @@ -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()) 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 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` | — |