diff --git a/Bedtime/Bedtime.xcodeproj/xcshareddata/xcschemes/Bedtime.xcscheme b/Bedtime/Bedtime.xcodeproj/xcshareddata/xcschemes/Bedtime.xcscheme index 5ec5c18..d2516b3 100644 --- a/Bedtime/Bedtime.xcodeproj/xcshareddata/xcschemes/Bedtime.xcscheme +++ b/Bedtime/Bedtime.xcodeproj/xcshareddata/xcschemes/Bedtime.xcscheme @@ -17,6 +17,7 @@ BuildableIdentifier = "primary" BlueprintIdentifier = "373AAEDC2E91B7E900D8ED84" BuildableName = "Bedger.app" + BlueprintName = "Bedtime" ReferencedContainer = "container:Bedtime.xcodeproj"> @@ -46,6 +47,7 @@ BuildableIdentifier = "primary" BlueprintIdentifier = "373AAEDC2E91B7E900D8ED84" BuildableName = "Bedger.app" + BlueprintName = "Bedtime" ReferencedContainer = "container:Bedtime.xcodeproj"> @@ -62,6 +64,7 @@ BuildableIdentifier = "primary" BlueprintIdentifier = "373AAEDC2E91B7E900D8ED84" BuildableName = "Bedger.app" + BlueprintName = "Bedtime" ReferencedContainer = "container:Bedtime.xcodeproj"> diff --git a/Bedtime/Bedtime/Assets.xcassets/AccentColor.colorset/Contents.json b/Bedtime/Bedtime/Assets.xcassets/AccentColor.colorset/Contents.json index eb87897..c88e0dd 100644 --- a/Bedtime/Bedtime/Assets.xcassets/AccentColor.colorset/Contents.json +++ b/Bedtime/Bedtime/Assets.xcassets/AccentColor.colorset/Contents.json @@ -1,6 +1,33 @@ { "colors" : [ { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "0.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "0.000", + "red" : "1.000" + } + }, "idiom" : "universal" } ], diff --git a/Bedtime/Bedtime/Assets.xcassets/BackgroundBehindCards.colorset/Contents.json b/Bedtime/Bedtime/Assets.xcassets/BackgroundBehindCards.colorset/Contents.json deleted file mode 100644 index adc86b2..0000000 --- a/Bedtime/Bedtime/Assets.xcassets/BackgroundBehindCards.colorset/Contents.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "platform" : "ios", - "reference" : "secondarySystemBackgroundColor" - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "platform" : "ios", - "reference" : "systemBackgroundColor" - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Bedtime/Bedtime/Assets.xcassets/CardBackground.colorset/Contents.json b/Bedtime/Bedtime/Assets.xcassets/CardBackground.colorset/Contents.json deleted file mode 100644 index d551d24..0000000 --- a/Bedtime/Bedtime/Assets.xcassets/CardBackground.colorset/Contents.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "platform" : "ios", - "reference" : "systemBackgroundColor" - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "platform" : "ios", - "reference" : "tertiarySystemBackgroundColor" - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Bedtime/Bedtime/Assets.xcassets/CozyColors/Contents.json b/Bedtime/Bedtime/Assets.xcassets/CozyColors/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Bedtime/Bedtime/Assets.xcassets/CozyColors/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozyAccent.colorset/Contents.json b/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozyAccent.colorset/Contents.json new file mode 100644 index 0000000..da787d0 --- /dev/null +++ b/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozyAccent.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors": [ + { + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "red": "0.431", + "green": "0.549", + "blue": "0.667" + } + }, + "idiom": "universal" + }, + { + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ], + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "red": "0.576", + "green": "0.686", + "blue": "0.788" + } + }, + "idiom": "universal" + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} diff --git a/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozyBackground.colorset/Contents.json b/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozyBackground.colorset/Contents.json new file mode 100644 index 0000000..e1d7aa9 --- /dev/null +++ b/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozyBackground.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors": [ + { + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "blue": "0.929", + "green": "0.957", + "red": "0.984" + } + }, + "idiom": "universal" + }, + { + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ], + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "blue": "0.000", + "green": "0.000", + "red": "0.000" + } + }, + "idiom": "universal" + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} diff --git a/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozyCardBackground.colorset/Contents.json b/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozyCardBackground.colorset/Contents.json new file mode 100644 index 0000000..c43309f --- /dev/null +++ b/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozyCardBackground.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors": [ + { + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "blue": "0.953", + "green": "0.973", + "red": "0.992" + } + }, + "idiom": "universal" + }, + { + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ], + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "blue": "0.110", + "green": "0.110", + "red": "0.110" + } + }, + "idiom": "universal" + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} diff --git a/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozyHealthKit.colorset/Contents.json b/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozyHealthKit.colorset/Contents.json new file mode 100644 index 0000000..c63b335 --- /dev/null +++ b/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozyHealthKit.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors": [ + { + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "red": "0.757", + "green": "0.333", + "blue": "0.290" + } + }, + "idiom": "universal" + }, + { + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ], + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "red": "0.871", + "green": "0.518", + "blue": "0.471" + } + }, + "idiom": "universal" + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} diff --git a/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozyNegative.colorset/Contents.json b/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozyNegative.colorset/Contents.json new file mode 100644 index 0000000..c63b335 --- /dev/null +++ b/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozyNegative.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors": [ + { + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "red": "0.757", + "green": "0.333", + "blue": "0.290" + } + }, + "idiom": "universal" + }, + { + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ], + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "red": "0.871", + "green": "0.518", + "blue": "0.471" + } + }, + "idiom": "universal" + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} diff --git a/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozyPositive.colorset/Contents.json b/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozyPositive.colorset/Contents.json new file mode 100644 index 0000000..5a2f3e4 --- /dev/null +++ b/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozyPositive.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors": [ + { + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "red": "0.431", + "green": "0.608", + "blue": "0.369" + } + }, + "idiom": "universal" + }, + { + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ], + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "red": "0.588", + "green": "0.761", + "blue": "0.533" + } + }, + "idiom": "universal" + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} diff --git a/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozyPrimary.colorset/Contents.json b/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozyPrimary.colorset/Contents.json new file mode 100644 index 0000000..2bec7b6 --- /dev/null +++ b/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozyPrimary.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors": [ + { + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "red": "0.169", + "green": "0.137", + "blue": "0.125" + } + }, + "idiom": "universal" + }, + { + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ], + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "red": "0.949", + "green": "0.914", + "blue": "0.871" + } + }, + "idiom": "universal" + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} diff --git a/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozyRecentSleep.colorset/Contents.json b/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozyRecentSleep.colorset/Contents.json new file mode 100644 index 0000000..26c7491 --- /dev/null +++ b/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozyRecentSleep.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors": [ + { + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "red": "0.694", + "green": "0.361", + "blue": "0.455" + } + }, + "idiom": "universal" + }, + { + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ], + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "red": "0.820", + "green": "0.545", + "blue": "0.627" + } + }, + "idiom": "universal" + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} diff --git a/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozySecondary.colorset/Contents.json b/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozySecondary.colorset/Contents.json new file mode 100644 index 0000000..5f15f27 --- /dev/null +++ b/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozySecondary.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors": [ + { + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "red": "0.486", + "green": "0.435", + "blue": "0.396" + } + }, + "idiom": "universal" + }, + { + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ], + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "red": "0.651", + "green": "0.592", + "blue": "0.529" + } + }, + "idiom": "universal" + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} diff --git a/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozySleepAwake.colorset/Contents.json b/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozySleepAwake.colorset/Contents.json new file mode 100644 index 0000000..c1600a9 --- /dev/null +++ b/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozySleepAwake.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors": [ + { + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "red": "0.851", + "green": "0.482", + "blue": "0.310" + } + }, + "idiom": "universal" + }, + { + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ], + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "red": "0.929", + "green": "0.627", + "blue": "0.475" + } + }, + "idiom": "universal" + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} diff --git a/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozySleepCore.colorset/Contents.json b/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozySleepCore.colorset/Contents.json new file mode 100644 index 0000000..c383507 --- /dev/null +++ b/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozySleepCore.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors": [ + { + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "red": "0.545", + "green": "0.435", + "blue": "0.361" + } + }, + "idiom": "universal" + }, + { + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ], + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "red": "0.690", + "green": "0.576", + "blue": "0.494" + } + }, + "idiom": "universal" + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} diff --git a/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozySleepDeep.colorset/Contents.json b/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozySleepDeep.colorset/Contents.json new file mode 100644 index 0000000..1e7a7ef --- /dev/null +++ b/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozySleepDeep.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors": [ + { + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "red": "0.357", + "green": "0.282", + "blue": "0.439" + } + }, + "idiom": "universal" + }, + { + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ], + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "red": "0.545", + "green": "0.463", + "blue": "0.639" + } + }, + "idiom": "universal" + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} diff --git a/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozySleepInBed.colorset/Contents.json b/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozySleepInBed.colorset/Contents.json new file mode 100644 index 0000000..c8434ed --- /dev/null +++ b/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozySleepInBed.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors": [ + { + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "red": "0.420", + "green": "0.365", + "blue": "0.337" + } + }, + "idiom": "universal" + }, + { + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ], + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "red": "0.580", + "green": "0.522", + "blue": "0.471" + } + }, + "idiom": "universal" + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} diff --git a/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozyWarning.colorset/Contents.json b/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozyWarning.colorset/Contents.json new file mode 100644 index 0000000..05f3017 --- /dev/null +++ b/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozyWarning.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors": [ + { + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "red": "0.851", + "green": "0.627", + "blue": "0.231" + } + }, + "idiom": "universal" + }, + { + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ], + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "red": "0.918", + "green": "0.745", + "blue": "0.447" + } + }, + "idiom": "universal" + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} diff --git a/Bedtime/Bedtime/Constants.swift b/Bedtime/Bedtime/Constants.swift index 358a686..cda0215 100644 --- a/Bedtime/Bedtime/Constants.swift +++ b/Bedtime/Bedtime/Constants.swift @@ -52,13 +52,13 @@ class Constants { return sleepGoalHoursRange.lowerBound + snapped * sleepGoalStepHours } - static func sleepGoalColor(difference: Double, graceColor: Color) -> Color { - if difference >= 0 { return .green } - if difference < -sleepGoalGraceHours { return .red } - return graceColor + static func sleepGoalColor(difference: Double) -> KeyPath? { + if difference >= 0 { return \.positive } + if difference < -sleepGoalGraceHours { return \.negative } + return nil // "grace" coloring when you're pretty close } - static func sleepDurationColor(hours: Double, goal: Double, graceColor: Color) -> Color { - sleepGoalColor(difference: hours - goal, graceColor: graceColor) + static func sleepDurationColor(hours: Double, goal: Double) -> KeyPath? { + sleepGoalColor(difference: hours - goal) } } diff --git a/Bedtime/Bedtime/ContentView.swift b/Bedtime/Bedtime/ContentView.swift index efaaf9c..846f119 100644 --- a/Bedtime/Bedtime/ContentView.swift +++ b/Bedtime/Bedtime/ContentView.swift @@ -189,7 +189,8 @@ struct ContentView: View { .frame(maxWidth: .infinity) } .environment(\.durationDisplayStyle, userPreferences.durationDisplayStyle) - .background(Color.backgroundBehindCards) + .environment(\.appTheme, userPreferences.theme) + .background(userPreferences.theme.colors.background) .navigationTitle("Bedger") .toolbar { ToolbarItem(placement: .navigationBarTrailing) { @@ -220,6 +221,7 @@ struct ContentView: View { ) } } + .tint(userPreferences.theme.colors.accent) .task { try? await healthKitManager.fetchSleepData() } diff --git a/Bedtime/Bedtime/Models/SleepData.swift b/Bedtime/Bedtime/Models/SleepData.swift index e1688ee..11b0363 100644 --- a/Bedtime/Bedtime/Models/SleepData.swift +++ b/Bedtime/Bedtime/Models/SleepData.swift @@ -86,17 +86,24 @@ extension HKCategoryValueSleepAnalysis { } } - var color: Color { + /// The palette color this sleep stage maps to, or `nil` when it has no themed color + /// (unspecified/unknown stages fall back to `Color.secondary` at the call site). + var paletteColor: KeyPath? { switch self { - case .asleepDeep: return Color.blue - case .asleepREM: return Color.purple - case .asleepCore: return Color.indigo - case .awake: return Color.orange - case .inBed: return Color.gray - case .asleepUnspecified: return Color.secondary - @unknown default: return Color.secondary + case .asleepDeep: return \.sleepDeep + case .asleepREM: return \.sleepREM + case .asleepCore: return \.sleepCore + case .awake: return \.sleepAwake + case .inBed: return \.sleepInBed + case .asleepUnspecified: return nil + @unknown default: return nil } } + + func color(in palette: any ThemeColorPalette) -> Color { + guard let paletteColor else { return .secondary } + return palette[keyPath: paletteColor] + } } struct NightSummary: Identifiable, Equatable { @@ -127,9 +134,9 @@ struct SleepBank: Equatable { return currentBalance < 0 } - var statusColor: Color { - guard averageHours != nil else { return .secondary } - return Constants.sleepGoalColor(difference: currentBalance, graceColor: .primary) + var statusColor: KeyPath { + guard averageHours != nil else { return \.secondary } + return Constants.sleepGoalColor(difference: currentBalance) ?? \.primary } var debtHours: Double { diff --git a/Bedtime/Bedtime/Models/UserPreferences.swift b/Bedtime/Bedtime/Models/UserPreferences.swift index 483e4a0..4597dbb 100644 --- a/Bedtime/Bedtime/Models/UserPreferences.swift +++ b/Bedtime/Bedtime/Models/UserPreferences.swift @@ -17,6 +17,9 @@ final class UserPreferences { var earliestReasonableBedtime: Date /// When true, durations use decimal hours ("5.1h"); otherwise "5h 6m". var useDecimalDurations: Bool = false + /// Stored as a raw string so an unrecognized value (e.g. from a future app version) + /// falls back to `.cozy` instead of failing to load. + private var themeRawValue: String = AppTheme.cozy.rawValue /// When true, the sleep balance waterfall chart is hidden from the Sleep Balance card. var hideSleepBankChart: Bool = false @@ -24,12 +27,18 @@ final class UserPreferences { useDecimalDurations ? .decimal : .hoursAndMinutes } + var theme: AppTheme { + get { AppTheme(rawValue: themeRawValue) ?? .cozy } + set { themeRawValue = newValue.rawValue } + } + init( sleepGoalHours: Double = 8.0, wakeTime: Date = Calendar.current.date(from: DateComponents(hour: 7, minute: 0)) ?? Date(), sleepBankDays: Int = 7, earliestReasonableBedtime: Date = Calendar.current.date(from: DateComponents(hour: 21, minute: 0)) ?? Date(), useDecimalDurations: Bool = false, + theme: AppTheme = .cozy, hideSleepBankChart: Bool = false ) { self.sleepGoalHours = sleepGoalHours @@ -38,6 +47,7 @@ final class UserPreferences { self.lastUpdated = Date() self.earliestReasonableBedtime = earliestReasonableBedtime self.useDecimalDurations = useDecimalDurations + self.themeRawValue = theme.rawValue self.hideSleepBankChart = hideSleepBankChart } diff --git a/Bedtime/Bedtime/Utils/AppTheme.swift b/Bedtime/Bedtime/Utils/AppTheme.swift new file mode 100644 index 0000000..2893186 --- /dev/null +++ b/Bedtime/Bedtime/Utils/AppTheme.swift @@ -0,0 +1,45 @@ +// +// AppTheme.swift +// Bedtime +// + +import SwiftUI + +/// The visual theme the app renders with. Stored on `UserPreferences` and threaded through +/// the view hierarchy via `EnvironmentValues.appTheme` so every color lookup can react to it. +enum AppTheme: String, CaseIterable, Identifiable, Codable { + /// The original look: default iOS system colors and backgrounds. + case system + /// Warm palette inspired by the app icon: a dusty blue accent from the sleep cap, cream + /// backgrounds, and a distinguishable spread of warm sleep-stage colors. + case cozy + + var id: String { rawValue } + + var displayName: String { + switch self { + case .system: return "System" + case .cozy: return "Cozy" + } + } + + /// The concrete color palette for this theme. Call sites read through this rather than + /// switching on the theme themselves. + var colors: any ThemeColorPalette { + switch self { + case .system: return SystemThemeColors() + case .cozy: return CozyThemeColors() + } + } +} + +private struct AppThemeKey: EnvironmentKey { + static let defaultValue: AppTheme = .cozy +} + +extension EnvironmentValues { + var appTheme: AppTheme { + get { self[AppThemeKey.self] } + set { self[AppThemeKey.self] = newValue } + } +} diff --git a/Bedtime/Bedtime/Utils/CozyThemeColors.swift b/Bedtime/Bedtime/Utils/CozyThemeColors.swift new file mode 100644 index 0000000..1eba5e1 --- /dev/null +++ b/Bedtime/Bedtime/Utils/CozyThemeColors.swift @@ -0,0 +1,31 @@ +// +// CozyThemeColors.swift +// Bedtime +// + +import SwiftUI + +/// Warm palette inspired by the app icon: a dusty slate blue accent lifted from the badger's +/// sleep cap, cream backgrounds in light mode, a true-black background with near-black cards +/// in dark mode, and a spread of warm sleep-stage colors (plum, brown, rose, orange, taupe) +/// chosen to stay distinguishable from one another and from the blue accent. +/// +/// Every color here comes from the asset catalog (`Assets.xcassets/CozyColors/*.colorset`) +/// rather than inline RGB values, so the palette can be tweaked visually in Xcode's color +/// picker without touching code. +struct CozyThemeColors: ThemeColorPalette { + let accent = Color.cozyAccent + let recentSleep = Color.cozyRecentSleep + let healthKit = Color.cozyHealthKit + let positive = Color.cozyPositive + let negative = Color.cozyNegative + let warning = Color.cozyWarning + let sleepDeep = Color.cozySleepDeep + let sleepCore = Color.cozySleepCore + let sleepAwake = Color.cozySleepAwake + let sleepInBed = Color.cozySleepInBed + let background = Color.cozyBackground + let cardBackground = Color.cozyCardBackground + let primary = Color.cozyPrimary + let secondary = Color.cozySecondary +} diff --git a/Bedtime/Bedtime/Utils/SystemThemeColors.swift b/Bedtime/Bedtime/Utils/SystemThemeColors.swift new file mode 100644 index 0000000..f7c7c64 --- /dev/null +++ b/Bedtime/Bedtime/Utils/SystemThemeColors.swift @@ -0,0 +1,23 @@ +// +// SystemThemeColors.swift +// Bedtime +// + +import SwiftUI + +struct SystemThemeColors: ThemeColorPalette { + let accent = Color.blue + let recentSleep = Color.purple + let healthKit = Color.red + let positive = Color.green + let negative = Color.red + let warning = Color.orange + let sleepDeep = Color.blue + let sleepCore = Color.indigo + let sleepAwake = Color.orange + let sleepInBed = Color.gray + let background = dynamicColor(light: .secondarySystemBackground, dark: .systemBackground) + let cardBackground = dynamicColor(light: .systemBackground, dark: .tertiarySystemBackground) + let primary = Color.primary + let secondary = Color.secondary +} diff --git a/Bedtime/Bedtime/Utils/ThemeColorPalette.swift b/Bedtime/Bedtime/Utils/ThemeColorPalette.swift new file mode 100644 index 0000000..15d5637 --- /dev/null +++ b/Bedtime/Bedtime/Utils/ThemeColorPalette.swift @@ -0,0 +1,44 @@ +// +// ThemeColorPalette.swift +// Bedtime +// + +import SwiftUI + +/// Every color a theme supplies. `AppTheme.colors` hands back the conforming palette for the +/// active theme, so callers read `theme.colors.accent` (or resolve a `KeyPath` picked elsewhere) instead of switching on the theme themselves. +protocol ThemeColorPalette { + var accent: Color { get } + var recentSleep: Color { get } + var healthKit: Color { get } + var positive: Color { get } + var negative: Color { get } + var warning: Color { get } + var sleepDeep: Color { get } + var sleepCore: Color { get } + var sleepAwake: Color { get } + var sleepInBed: Color { get } + var background: Color { get } + var cardBackground: Color { get } + var primary: Color { get } + var secondary: Color { get } +} + +extension ThemeColorPalette { + // These card accents and the REM sleep stage are visually just the accent/recent-sleep + // colors; conforming palettes don't need to restate them. + var bedtime: Color { accent } + var lastNight: Color { accent } + var sleepREM: Color { recentSleep } +} + +/// Builds a `Color` that switches between two `UIColor`s (typically semantic system colors) +/// depending on the current `UIUserInterfaceStyle`. +func dynamicColor(light: UIColor, dark: UIColor) -> Color { + Color( + uiColor: UIColor { traits in + traits.userInterfaceStyle == .dark ? dark : light + } + ) +} diff --git a/Bedtime/Bedtime/Views/BalanceWaterfallChart.swift b/Bedtime/Bedtime/Views/BalanceWaterfallChart.swift index dd4035e..9223e2d 100644 --- a/Bedtime/Bedtime/Views/BalanceWaterfallChart.swift +++ b/Bedtime/Bedtime/Views/BalanceWaterfallChart.swift @@ -31,6 +31,9 @@ struct BalanceWaterfallChart: View { let domain: ClosedRange @Binding var selectedDays: Int var daysRange: ClosedRange = Constants.sleepBankDaysRange + @Environment(\.appTheme) private var theme + + private var colors: any ThemeColorPalette { theme.colors } private var impactByDate: [Date: BalanceDayImpact] { Dictionary(uniqueKeysWithValues: impacts.map { ($0.date, $0) }) @@ -100,10 +103,10 @@ struct BalanceWaterfallChart: View { let dividerY = min(max(yPosition(for: baselineBalance, in: size.height), 0), size.height) return VStack(spacing: 0) { Rectangle() - .fill(Color.green.opacity(0.1)) + .fill(colors.positive.opacity(0.1)) .frame(height: dividerY) Rectangle() - .fill(Color.red.opacity(0.1)) + .fill(colors.negative.opacity(0.1)) } } @@ -125,7 +128,7 @@ struct BalanceWaterfallChart: View { let newY = yPosition(for: step.newBalance, in: size.height) let stepHeight = abs(newY - priorY) stepShape(isGain: step.isGain, stepHeight: stepHeight, chartWidth: size.width) - .fill(step.isGain ? Color.green : Color.red) + .fill(step.isGain ? colors.positive : colors.negative) .opacity(isIncluded ? 1 : 0.3) .frame(height: max(stepHeight, 1)) .offset(y: min(priorY, newY)) diff --git a/Bedtime/Bedtime/Views/BedtimeRecommendationCard.swift b/Bedtime/Bedtime/Views/BedtimeRecommendationCard.swift index fe1c060..2aa24c6 100644 --- a/Bedtime/Bedtime/Views/BedtimeRecommendationCard.swift +++ b/Bedtime/Bedtime/Views/BedtimeRecommendationCard.swift @@ -13,13 +13,16 @@ struct BedtimeRecommendationCard: View { var wakeTime: Binding? = nil @State private var isEditingWakeTime = false + @Environment(\.appTheme) private var theme + + private var colors: any ThemeColorPalette { theme.colors } var body: some View { CardComponent { VStack(spacing: 16) { CardHeader( icon: "bed.double.fill", - iconColor: .blue, + iconColor: colors.bedtime, title: "Tonight's Recommendation" ) { Text("Based on your sleep bank") @@ -38,7 +41,7 @@ struct BedtimeRecommendationCard: View { Text(TimeFormatter.formatTimeOfDay(recommendation.recommendedBedtime)) .font(.title) .fontWeight(.bold) - .foregroundColor(.blue) + .foregroundColor(colors.bedtime) } Spacer() @@ -141,7 +144,7 @@ private struct WakeTimeEditor: View { wakeTime: $wakeTime ) .padding() - .background(Color.backgroundBehindCards) + .background(AppTheme.cozy.colors.background) } } return PreviewHost() diff --git a/Bedtime/Bedtime/Views/Components/CardComponent.swift b/Bedtime/Bedtime/Views/Components/CardComponent.swift index a2c1916..e38ecb6 100644 --- a/Bedtime/Bedtime/Views/Components/CardComponent.swift +++ b/Bedtime/Bedtime/Views/Components/CardComponent.swift @@ -53,6 +53,7 @@ struct CardHeader: View { struct CardComponent: View { let content: Content + @Environment(\.appTheme) private var theme init(@ViewBuilder content: () -> Content) { self.content = content() @@ -63,7 +64,7 @@ struct CardComponent: View { .padding() .background { RoundedRectangle(cornerRadius: 12) - .fill(Color.cardBackground) + .fill(theme.colors.cardBackground) .shadow( color: Color.black.opacity(0.1), radius: 2, diff --git a/Bedtime/Bedtime/Views/Components/IncludedDaysRangeHandle.swift b/Bedtime/Bedtime/Views/Components/IncludedDaysRangeHandle.swift index ce65bcf..7ba60d3 100644 --- a/Bedtime/Bedtime/Views/Components/IncludedDaysRangeHandle.swift +++ b/Bedtime/Bedtime/Views/Components/IncludedDaysRangeHandle.swift @@ -15,6 +15,9 @@ struct IncludedDaysRangeHandle: View { var rowStride: CGFloat = 64 @GestureState private var dragTranslation: CGFloat = 0 + @Environment(\.appTheme) private var theme + + private var colors: any ThemeColorPalette { theme.colors } private var displayedDays: Int { clamped(days + dayDelta(for: dragTranslation)) @@ -24,7 +27,7 @@ struct IncludedDaysRangeHandle: View { ZStack(alignment: .leading) { VStack(spacing: 0) { Rectangle() - .fill(Color.accentColor) + .fill(colors.accent) Rectangle() .fill(Color.secondary.opacity(0.2)) } @@ -32,13 +35,13 @@ struct IncludedDaysRangeHandle: View { ZStack(alignment: .leading) { Capsule() - .fill(Color.accentColor.opacity(0.45)) + .fill(colors.accent.opacity(0.45)) .frame(height: 2) HStack(spacing: 6) { ZStack { Circle() - .fill(Color.accentColor) + .fill(colors.accent) Image(systemName: "arrow.up.and.down") .font(.caption2.weight(.bold)) .foregroundStyle(.white) @@ -49,16 +52,16 @@ struct IncludedDaysRangeHandle: View { Text("\(displayedDays) days included") .font(.caption.weight(.semibold)) - .foregroundStyle(Color.accentColor) + .foregroundStyle(colors.accent) .padding(.horizontal, 10) .padding(.vertical, 6) .background( Capsule() - .fill(Color.cardBackground) + .fill(colors.cardBackground) ) .overlay( Capsule() - .stroke(Color.accentColor.opacity(0.35), lineWidth: 1) + .stroke(colors.accent.opacity(0.35), lineWidth: 1) ) .offset(x: -12) } diff --git a/Bedtime/Bedtime/Views/Components/ProgressBar.swift b/Bedtime/Bedtime/Views/Components/ProgressBar.swift index e647cfa..f65f4e9 100644 --- a/Bedtime/Bedtime/Views/Components/ProgressBar.swift +++ b/Bedtime/Bedtime/Views/Components/ProgressBar.swift @@ -26,15 +26,16 @@ struct ProgressBar: View { } #Preview(traits: .sizeThatFitsLayout) { + let colors = AppTheme.cozy.colors VStack { ProgressBar(value: 0.2) - .tint(Gradient(colors: [.orange, .red])) + .tint(Gradient(colors: [colors.warning, colors.negative])) ProgressBar(value: 6, total: 8) - .tint(Gradient(colors: [.purple, .blue])) + .tint(Gradient(colors: [colors.recentSleep, colors.bedtime])) ProgressBar(value: 8, total: 10) - .tint(LinearGradient(colors: [.green, .cyan], startPoint: .leading, endPoint: .trailing)) + .tint(LinearGradient(colors: [colors.positive, colors.accent], startPoint: .leading, endPoint: .trailing)) } .frame(height: 64) .padding() diff --git a/Bedtime/Bedtime/Views/Components/SleepDayGroup.swift b/Bedtime/Bedtime/Views/Components/SleepDayGroup.swift index ab99370..c58b58f 100644 --- a/Bedtime/Bedtime/Views/Components/SleepDayGroup.swift +++ b/Bedtime/Bedtime/Views/Components/SleepDayGroup.swift @@ -18,6 +18,9 @@ struct SleepDayGroup: View { var isIncludedInSleepBank: Bool = true let onToggle: () -> Void @Environment(\.durationDisplayStyle) private var durationStyle + @Environment(\.appTheme) private var theme + + private var colors: any ThemeColorPalette { theme.colors } private var dateFormatter: DateFormatter { let formatter = DateFormatter() @@ -31,14 +34,14 @@ struct SleepDayGroup: View { private var balanceImpact: (value: Double, isPositive: Bool, color: Color) { let difference = totalSleepHours - sleepGoal + let keypath = Constants.sleepDurationColor( + hours: totalSleepHours, + goal: sleepGoal + ) ?? \.secondary return ( abs(difference), difference >= 0, - Constants.sleepDurationColor( - hours: totalSleepHours, - goal: sleepGoal, - graceColor: .secondary - ) + colors[keyPath: keypath] ) } @@ -49,7 +52,7 @@ struct SleepDayGroup: View { var body: some View { HStack(alignment: .top, spacing: 8) { Rectangle() - .fill(isIncludedInSleepBank ? Color.accentColor : Color.secondary.opacity(0.2)) + .fill(isIncludedInSleepBank ? colors.accent : Color.secondary.opacity(0.2)) .frame(width: 3) .accessibilityHidden(true) diff --git a/Bedtime/Bedtime/Views/Components/SleepSessionRow.swift b/Bedtime/Bedtime/Views/Components/SleepSessionRow.swift index b7cf1ba..793b334 100644 --- a/Bedtime/Bedtime/Views/Components/SleepSessionRow.swift +++ b/Bedtime/Bedtime/Views/Components/SleepSessionRow.swift @@ -11,12 +11,15 @@ import HealthKit struct SleepSessionRow: View { let session: SleepSession @Environment(\.durationDisplayStyle) private var durationStyle + @Environment(\.appTheme) private var theme + + private var colors: any ThemeColorPalette { theme.colors } var body: some View { HStack(spacing: 6) { Image(systemName: session.sleepType.icon) .font(.caption) - .foregroundColor(Color(session.sleepType.color)) + .foregroundColor(session.sleepType.color(in: colors)) VStack(alignment: .leading, spacing: 2) { Text("\(TimeFormatter.formatTimeOfDay(session.startDate)) - \(TimeFormatter.formatTimeOfDay(session.endDate))") @@ -25,7 +28,7 @@ struct SleepSessionRow: View { Text(session.sleepType.displayName) .font(.caption2) - .foregroundColor(Color(session.sleepType.color)) + .foregroundColor(session.sleepType.color(in: colors)) } Spacer() diff --git a/Bedtime/Bedtime/Views/Components/SleepSourceComparisonView.swift b/Bedtime/Bedtime/Views/Components/SleepSourceComparisonView.swift index 1e2c379..96bb519 100644 --- a/Bedtime/Bedtime/Views/Components/SleepSourceComparisonView.swift +++ b/Bedtime/Bedtime/Views/Components/SleepSourceComparisonView.swift @@ -88,6 +88,7 @@ struct SleepStageTimelineBar: View { let rangeStart: Date let rangeEnd: Date var isDimmed: Bool = false + @Environment(\.appTheme) private var theme private var rangeDuration: TimeInterval { max(rangeEnd.timeIntervalSince(rangeStart), 1) @@ -103,7 +104,7 @@ struct SleepStageTimelineBar: View { let width = session.duration / rangeDuration Rectangle() - .fill(session.sleepType.color) + .fill(session.sleepType.color(in: theme.colors)) .opacity(isDimmed ? 0.35 : 1) .frame(width: max(proxy.size.width * width, 1)) .offset(x: proxy.size.width * offset) diff --git a/Bedtime/Bedtime/Views/HealthKitAuthorizationCard.swift b/Bedtime/Bedtime/Views/HealthKitAuthorizationCard.swift index 4b30256..fedcc7b 100644 --- a/Bedtime/Bedtime/Views/HealthKitAuthorizationCard.swift +++ b/Bedtime/Bedtime/Views/HealthKitAuthorizationCard.swift @@ -9,13 +9,16 @@ import SwiftUI struct HealthKitAuthorizationCard: View { @ObservedObject var healthKitManager: HealthKitManager + @Environment(\.appTheme) private var theme + + private var colors: any ThemeColorPalette { theme.colors } var body: some View { CardComponent { VStack(spacing: 16) { CardHeader( icon: "heart.text.square", - iconColor: .red, + iconColor: colors.healthKit, title: "HealthKit Access Required" ) { Text("We need access to your sleep data") @@ -31,7 +34,7 @@ struct HealthKitAuthorizationCard: View { if let errorMessage = healthKitManager.errorMessage { Text(errorMessage) .font(.caption) - .foregroundColor(.red) + .foregroundColor(colors.healthKit) .multilineTextAlignment(.leading) } diff --git a/Bedtime/Bedtime/Views/LastNightCard.swift b/Bedtime/Bedtime/Views/LastNightCard.swift index b3d2c7e..7b7a8bf 100644 --- a/Bedtime/Bedtime/Views/LastNightCard.swift +++ b/Bedtime/Bedtime/Views/LastNightCard.swift @@ -12,6 +12,9 @@ struct LastNightCard: View { let goal: TimeInterval let sourceAppLinks: [SleepSourceAppLink] @Environment(\.durationDisplayStyle) private var durationStyle + @Environment(\.appTheme) private var theme + + private var colors: any ThemeColorPalette { theme.colors } var durationInHours: TimeInterval? { sleepSessions?.map(\.durationInHours).reduce(0, +) @@ -19,11 +22,11 @@ struct LastNightCard: View { private var durationColor: Color { guard let durationInHours else { return .secondary } - return Constants.sleepDurationColor( + let keypath = Constants.sleepDurationColor( hours: durationInHours, - goal: goal, - graceColor: .primary - ) + goal: goal + ) ?? \.primary + return colors[keyPath: keypath] } var body: some View { @@ -31,7 +34,7 @@ struct LastNightCard: View { VStack(spacing: 16) { CardHeader( icon: "calendar", - iconColor: .blue, + iconColor: colors.lastNight, title: "Last Night" ) @@ -119,19 +122,19 @@ private func previewNight(hours: Double) -> SleepSession { #Preview("Met goal") { LastNightCard(sleepSessions: [previewNight(hours: 8.2)], goal: 8, sourceAppLinks: []) .padding() - .background(Color.backgroundBehindCards) + .background(AppTheme.cozy.colors.background) } #Preview("Short night") { LastNightCard(sleepSessions: [previewNight(hours: 5.75)], goal: 8, sourceAppLinks: []) .padding() - .background(Color.backgroundBehindCards) + .background(AppTheme.cozy.colors.background) } #Preview("No data") { LastNightCard(sleepSessions: nil, goal: 8, sourceAppLinks: []) .padding() - .background(Color.backgroundBehindCards) + .background(AppTheme.cozy.colors.background) } #Preview("No data, with source apps") { @@ -148,7 +151,7 @@ private func previewNight(hours: Double) -> SleepSession { ] ) .padding() - .background(Color.backgroundBehindCards) + .background(AppTheme.cozy.colors.background) } #endif diff --git a/Bedtime/Bedtime/Views/RecentSleepSessionsCard.swift b/Bedtime/Bedtime/Views/RecentSleepSessionsCard.swift index b1d29cd..6b3b3f0 100644 --- a/Bedtime/Bedtime/Views/RecentSleepSessionsCard.swift +++ b/Bedtime/Bedtime/Views/RecentSleepSessionsCard.swift @@ -34,6 +34,9 @@ struct RecentSleepSessionsCard: View { let sleepGoal: Double let excludedSourceIDs: Set @Binding var sleepBankDays: Int + @Environment(\.appTheme) private var theme + + private var colors: any ThemeColorPalette { theme.colors } @State private var expandedNights: Set = [] @@ -59,7 +62,7 @@ struct RecentSleepSessionsCard: View { VStack(spacing: 16) { CardHeader( icon: "chart.line.uptrend.xyaxis", - iconColor: .purple, + iconColor: colors.recentSleep, title: "Recent Sleep" ) @@ -96,7 +99,7 @@ struct RecentSleepSessionsCard: View { .padding(.leading, 11) .overlay(alignment: .leading) { Rectangle() - .fill(isIncluded ? Color.accentColor : Color.secondary.opacity(0.2)) + .fill(isIncluded ? colors.accent : Color.secondary.opacity(0.2)) .frame(width: 3) .accessibilityHidden(true) } @@ -155,7 +158,7 @@ private struct RecentSleepSessionsCardPreview: View { ) .padding() } - .background(Color.backgroundBehindCards) + .background(AppTheme.cozy.colors.background) } } diff --git a/Bedtime/Bedtime/Views/SettingsView.swift b/Bedtime/Bedtime/Views/SettingsView.swift index 82e2a64..86c9e81 100644 --- a/Bedtime/Bedtime/Views/SettingsView.swift +++ b/Bedtime/Bedtime/Views/SettingsView.swift @@ -16,6 +16,9 @@ struct SettingsView: View { @Environment(\.dismiss) private var dismiss @Environment(\.horizontalSizeClass) private var horizontalSizeClass @State private var isLoadingSources = true + /// Reads straight from the model rather than `\.appTheme` so the picker inside this + /// sheet/inspector reflects edits immediately regardless of environment propagation. + private var colors: any ThemeColorPalette { preferences.theme.colors } private var sleepBankDaysBinding: Binding { Binding( @@ -36,6 +39,15 @@ struct SettingsView: View { var body: some View { NavigationStack { Form { + Section("Appearance") { + Picker("Theme", selection: $preferences.theme) { + ForEach(AppTheme.allCases) { option in + Text(option.displayName).tag(option) + } + } + } + .listRowBackground(colors.cardBackground) + Section("Sleep Goal") { VStack(alignment: .leading, spacing: 8) { HStack { @@ -56,9 +68,10 @@ struct SettingsView: View { ) { EmptyView() } - .accentColor(.blue) + .accentColor(colors.accent) } } + .listRowBackground(colors.cardBackground) Section("Wake Time") { DatePicker( @@ -67,6 +80,7 @@ struct SettingsView: View { displayedComponents: .hourAndMinute ) } + .listRowBackground(colors.cardBackground) Section { Toggle(isOn: $preferences.useDecimalDurations) { @@ -86,6 +100,7 @@ struct SettingsView: View { } footer: { Text("Off uses hours and minutes (5h 6m). On uses decimal hours (5.1h); sleep goals can show quarter hours like 7.25h.\n\nHiding the sleep balance chart keeps the Sleep Balance card's summary but removes the waterfall graph.") } + .listRowBackground(colors.cardBackground) Section("Sleep Bank Calculation") { VStack(alignment: .leading, spacing: 8) { @@ -103,13 +118,14 @@ struct SettingsView: View { ) { EmptyView() } - .accentColor(.blue) + .accentColor(colors.accent) Text("How many recent days to include in your sleep bank calculation") .font(.caption) .foregroundColor(.secondary) } } + .listRowBackground(colors.cardBackground) Section("Sleep Limits") { DatePicker( @@ -122,6 +138,7 @@ struct SettingsView: View { .font(.caption) .foregroundColor(.secondary) } + .listRowBackground(colors.cardBackground) Section("Data Sources") { if let availableSources = healthKitManager.availableSources { @@ -159,14 +176,14 @@ struct SettingsView: View { }) { Text("Source \(bundleIdentifier) is no longer available. Tap to delete.") .font(.caption) - .foregroundColor(.orange) + .foregroundColor(colors.warning) } } if allExcluded { Text("No sources selected. Sleep data will not be displayed.") .font(.caption) - .foregroundColor(.orange) + .foregroundColor(colors.warning) } } else if isLoadingSources { @@ -177,6 +194,7 @@ struct SettingsView: View { .foregroundColor(.secondary) } } + .listRowBackground(colors.cardBackground) #if DEBUG Section("Developer") { @@ -222,9 +240,12 @@ struct SettingsView: View { .font(.caption2) .foregroundColor(.secondary) } + .listRowBackground(colors.cardBackground) #endif } + .scrollContentBackground(.hidden) + .background(colors.background) .navigationTitle("Settings") .navigationBarTitleDisplayMode(.inline) // Force 12-hour clock labels in DatePickers to match app-wide time formatting. diff --git a/Bedtime/Bedtime/Views/SleepBankCard.swift b/Bedtime/Bedtime/Views/SleepBankCard.swift index 010c9b4..d12962e 100644 --- a/Bedtime/Bedtime/Views/SleepBankCard.swift +++ b/Bedtime/Bedtime/Views/SleepBankCard.swift @@ -19,6 +19,9 @@ struct SleepBankCard: View { /// When true, the balance waterfall chart is omitted from the card. var hideChart: Bool = false @Environment(\.durationDisplayStyle) private var durationStyle + @Environment(\.appTheme) private var theme + + private var colors: any ThemeColorPalette { theme.colors } @State private var isEditingGoal = false @@ -48,7 +51,7 @@ struct SleepBankCard: View { VStack(spacing: 10) { CardHeader( icon: sleepBank.isInDebt ? "moon.zzz.fill" : "moon.stars.fill", - iconColor: sleepBank.statusColor, + iconColor: colors[keyPath: sleepBank.statusColor], title: "Sleep Balance" ) { Text(sleepBank.statusDescription(days: sleepBankDays)) @@ -68,7 +71,7 @@ struct SleepBankCard: View { Text(TimeFormatter.formatHours(averageHours, style: durationStyle)) .font(.title2) .fontWeight(.bold) - .foregroundColor(sleepBank.statusColor) + .foregroundColor(colors[keyPath: sleepBank.statusColor]) } else { Text("unknown") .font(.subheadline) @@ -88,7 +91,7 @@ struct SleepBankCard: View { Text(TimeFormatter.formatHours(sleepBank.currentBalance, style: durationStyle)) .font(.title2) .fontWeight(.bold) - .foregroundColor(sleepBank.statusColor) + .foregroundColor(colors[keyPath: sleepBank.statusColor]) } else { Text("unknown") .font(.subheadline) @@ -206,7 +209,7 @@ private struct SleepBankCardPreview: View { ) .padding() } - .background(Color.backgroundBehindCards) + .background(AppTheme.cozy.colors.background) } } diff --git a/Bedtime/Bedtime/Views/SleepInsightsCard.swift b/Bedtime/Bedtime/Views/SleepInsightsCard.swift index f3240b7..c6c9a73 100644 --- a/Bedtime/Bedtime/Views/SleepInsightsCard.swift +++ b/Bedtime/Bedtime/Views/SleepInsightsCard.swift @@ -14,12 +14,15 @@ struct SleepInsightsCard: View { /// Called with the new goal in hours when the raise-goal button is tapped. var onRaiseGoal: ((Double) -> Void)? = nil @Environment(\.durationDisplayStyle) private var durationStyle + @Environment(\.appTheme) private var theme + + private var colors: any ThemeColorPalette { theme.colors } private var accentColor: Color { if insight.congratulationWindow != nil { - return .green + return colors.positive } - return .orange + return colors.warning } private var iconName: String { @@ -70,7 +73,7 @@ struct SleepInsightsCard: View { .frame(maxWidth: .infinity) } .buttonStyle(.bordered) - .tint(.orange) + .tint(colors.warning) .padding(.top, 4) } @@ -86,7 +89,7 @@ struct SleepInsightsCard: View { .frame(maxWidth: .infinity) } .buttonStyle(.bordered) - .tint(.green) + .tint(colors.positive) .padding(.top, 4) } } @@ -124,7 +127,7 @@ struct SleepInsightsCard: View { onApplyDays: { _ in } ) .padding() - .background(Color.backgroundBehindCards) + .background(AppTheme.cozy.colors.background) } #Preview("Motivator only") { @@ -152,7 +155,7 @@ struct SleepInsightsCard: View { onApplyDays: { _ in } ) .padding() - .background(Color.backgroundBehindCards) + .background(AppTheme.cozy.colors.background) } #Preview("Caught up everywhere") { @@ -187,5 +190,5 @@ struct SleepInsightsCard: View { onRaiseGoal: { _ in } ) .padding() - .background(Color.backgroundBehindCards) + .background(AppTheme.cozy.colors.background) }