From 1008a58cc39668c5f22ecf29437d7cce3b176a13 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 29 May 2026 16:27:25 +0000 Subject: [PATCH 01/16] Extract app colors to AppColors.swift Centralize theme color definitions in a single AppColors enum and update views to reference named colors instead of inline system colors. No visual changes in this commit. Co-authored-by: Greg --- Bedtime/Bedtime/Models/SleepData.swift | 10 +++--- Bedtime/Bedtime/Utils/AppColors.swift | 33 +++++++++++++++++++ .../Views/BedtimeRecommendationCard.swift | 4 +-- .../Views/Components/ProgressBar.swift | 6 ++-- .../Views/Components/SleepDayGroup.swift | 2 +- .../Views/HealthKitAuthorizationCard.swift | 4 +-- Bedtime/Bedtime/Views/LastNightCard.swift | 6 ++-- .../Views/RecentSleepSessionsCard.swift | 2 +- Bedtime/Bedtime/Views/SettingsView.swift | 12 +++---- Bedtime/Bedtime/Views/SleepBankCard.swift | 6 ++-- 10 files changed, 59 insertions(+), 26 deletions(-) create mode 100644 Bedtime/Bedtime/Utils/AppColors.swift diff --git a/Bedtime/Bedtime/Models/SleepData.swift b/Bedtime/Bedtime/Models/SleepData.swift index 0020e59..6c40eda 100644 --- a/Bedtime/Bedtime/Models/SleepData.swift +++ b/Bedtime/Bedtime/Models/SleepData.swift @@ -74,11 +74,11 @@ extension HKCategoryValueSleepAnalysis { var color: Color { 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 .asleepDeep: return AppColors.sleepDeep + case .asleepREM: return AppColors.sleepREM + case .asleepCore: return AppColors.sleepCore + case .awake: return AppColors.sleepAwake + case .inBed: return AppColors.sleepInBed case .asleepUnspecified: return Color.secondary @unknown default: return Color.secondary } diff --git a/Bedtime/Bedtime/Utils/AppColors.swift b/Bedtime/Bedtime/Utils/AppColors.swift new file mode 100644 index 0000000..a2d0d43 --- /dev/null +++ b/Bedtime/Bedtime/Utils/AppColors.swift @@ -0,0 +1,33 @@ +// +// AppColors.swift +// Bedtime +// + +import SwiftUI + +enum AppColors { + // MARK: - Accent + + static let accent = Color.blue + + // MARK: - Card accents + + static let bedtime = Color.blue + static let lastNight = Color.blue + static let recentSleep = Color.purple + static let healthKit = Color.red + + // MARK: - Status + + static let positive = Color.green + static let negative = Color.red + static let warning = Color.orange + + // MARK: - Sleep stages + + static let sleepDeep = Color.blue + static let sleepREM = Color.purple + static let sleepCore = Color.indigo + static let sleepAwake = Color.orange + static let sleepInBed = Color.gray +} diff --git a/Bedtime/Bedtime/Views/BedtimeRecommendationCard.swift b/Bedtime/Bedtime/Views/BedtimeRecommendationCard.swift index 8614134..63854b6 100644 --- a/Bedtime/Bedtime/Views/BedtimeRecommendationCard.swift +++ b/Bedtime/Bedtime/Views/BedtimeRecommendationCard.swift @@ -22,7 +22,7 @@ struct BedtimeRecommendationCard: View { HStack { Image(systemName: "bed.double.fill") .font(.title2) - .foregroundColor(.blue) + .foregroundColor(AppColors.bedtime) .frame(width: Constants.iconWidth) VStack(alignment: .leading, spacing: 4) { @@ -48,7 +48,7 @@ struct BedtimeRecommendationCard: View { Text(timeFormatter.string(from: recommendation.recommendedBedtime)) .font(.title) .fontWeight(.bold) - .foregroundColor(.blue) + .foregroundColor(AppColors.bedtime) } Spacer() diff --git a/Bedtime/Bedtime/Views/Components/ProgressBar.swift b/Bedtime/Bedtime/Views/Components/ProgressBar.swift index e647cfa..7dae95d 100644 --- a/Bedtime/Bedtime/Views/Components/ProgressBar.swift +++ b/Bedtime/Bedtime/Views/Components/ProgressBar.swift @@ -28,13 +28,13 @@ struct ProgressBar: View { #Preview(traits: .sizeThatFitsLayout) { VStack { ProgressBar(value: 0.2) - .tint(Gradient(colors: [.orange, .red])) + .tint(Gradient(colors: [AppColors.warning, AppColors.negative])) ProgressBar(value: 6, total: 8) - .tint(Gradient(colors: [.purple, .blue])) + .tint(Gradient(colors: [AppColors.recentSleep, AppColors.bedtime])) ProgressBar(value: 8, total: 10) - .tint(LinearGradient(colors: [.green, .cyan], startPoint: .leading, endPoint: .trailing)) + .tint(LinearGradient(colors: [AppColors.positive, .cyan], 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 ce2a9cf..53888bc 100644 --- a/Bedtime/Bedtime/Views/Components/SleepDayGroup.swift +++ b/Bedtime/Bedtime/Views/Components/SleepDayGroup.swift @@ -29,7 +29,7 @@ struct SleepDayGroup: View { private var balanceImpact: (value: Double, isPositive: Bool, color: Color) { let totalSleepHours = sessions.map(\.durationInHours).reduce(0, +) let difference = totalSleepHours - sleepGoal - let color = difference >= 0 ? Color.green : difference < -0.5 ? Color.red : Color.secondary + let color = difference >= 0 ? AppColors.positive : difference < -0.5 ? AppColors.negative : Color.secondary return (abs(difference), difference >= 0, color) } diff --git a/Bedtime/Bedtime/Views/HealthKitAuthorizationCard.swift b/Bedtime/Bedtime/Views/HealthKitAuthorizationCard.swift index 8d5e223..731196c 100644 --- a/Bedtime/Bedtime/Views/HealthKitAuthorizationCard.swift +++ b/Bedtime/Bedtime/Views/HealthKitAuthorizationCard.swift @@ -16,7 +16,7 @@ struct HealthKitAuthorizationCard: View { HStack { Image(systemName: "heart.text.square") .font(.title2) - .foregroundColor(.red) + .foregroundColor(AppColors.healthKit) .frame(width: Constants.iconWidth) VStack(alignment: .leading, spacing: 4) { @@ -39,7 +39,7 @@ struct HealthKitAuthorizationCard: View { if let errorMessage = healthKitManager.errorMessage { Text(errorMessage) .font(.caption) - .foregroundColor(.red) + .foregroundColor(AppColors.healthKit) .multilineTextAlignment(.leading) } diff --git a/Bedtime/Bedtime/Views/LastNightCard.swift b/Bedtime/Bedtime/Views/LastNightCard.swift index ce02cbc..d460e95 100644 --- a/Bedtime/Bedtime/Views/LastNightCard.swift +++ b/Bedtime/Bedtime/Views/LastNightCard.swift @@ -27,7 +27,7 @@ struct LastNightCard: View { HStack { Image(systemName: "calendar") .font(.title2) - .foregroundColor(.blue) + .foregroundColor(AppColors.lastNight) .frame(width: Constants.iconWidth) VStack(alignment: .leading, spacing: 4) { Text("Last Night") @@ -64,7 +64,7 @@ struct LastNightCard: View { Text(String(format: "%.1f", durationInHours!)) .font(.title2) .fontWeight(.bold) - .foregroundStyle(durationInHours! > goal ? .green : .red) + .foregroundStyle(durationInHours! > goal ? AppColors.positive : AppColors.negative) Text("hours") .font(.subheadline) .foregroundColor(.secondary) @@ -74,7 +74,7 @@ struct LastNightCard: View { // Progress bar ProgressBar(value: durationInHours!, total: goal) - .tint(durationInHours! > goal ? .green : .red) + .tint(durationInHours! > goal ? AppColors.positive : AppColors.negative) } else { Text("No sleep data available") diff --git a/Bedtime/Bedtime/Views/RecentSleepSessionsCard.swift b/Bedtime/Bedtime/Views/RecentSleepSessionsCard.swift index e061641..83a4eb5 100644 --- a/Bedtime/Bedtime/Views/RecentSleepSessionsCard.swift +++ b/Bedtime/Bedtime/Views/RecentSleepSessionsCard.swift @@ -28,7 +28,7 @@ struct RecentSleepSessionsCard: View { HStack { Image(systemName: "chart.line.uptrend.xyaxis") .font(.title2) - .foregroundColor(.purple) + .foregroundColor(AppColors.recentSleep) .frame(width: Constants.iconWidth) VStack(alignment: .leading, spacing: 4) { diff --git a/Bedtime/Bedtime/Views/SettingsView.swift b/Bedtime/Bedtime/Views/SettingsView.swift index b38f34a..6b53847 100644 --- a/Bedtime/Bedtime/Views/SettingsView.swift +++ b/Bedtime/Bedtime/Views/SettingsView.swift @@ -38,7 +38,7 @@ struct SettingsView: View { Slider(value: $preferences.sleepGoalHours, in: 6...12, step: 0.25) { EmptyView() } - .accentColor(.blue) + .accentColor(AppColors.accent) } } @@ -62,7 +62,7 @@ struct SettingsView: View { Slider(value: sleepBankDaysBinding, in: 3...14, step: 1) { EmptyView() } - .accentColor(.blue) + .accentColor(AppColors.accent) Text("How many recent days to include in your sleep bank calculation") .font(.caption) @@ -82,7 +82,7 @@ struct SettingsView: View { Slider(value: $preferences.maxSleepHoursPerNight, in: 8...16, step: 1) { Text("Max") } - .accentColor(.blue) + .accentColor(AppColors.accent) HStack { Text("Min sleep hours per night") @@ -94,7 +94,7 @@ struct SettingsView: View { Slider(value: $preferences.minSleepHoursPerNight, in: 2...10, step: 1) { Text("Min") } - .accentColor(.blue) + .accentColor(AppColors.accent) } } @@ -134,14 +134,14 @@ struct SettingsView: View { }) { Text("Source \(bundleIdentifier) is no longer available. Tap to delete.") .font(.caption) - .foregroundColor(.orange) + .foregroundColor(AppColors.warning) } } if allExcluded { Text("No sources selected. Sleep data will not be displayed.") .font(.caption) - .foregroundColor(.orange) + .foregroundColor(AppColors.warning) } } else { diff --git a/Bedtime/Bedtime/Views/SleepBankCard.swift b/Bedtime/Bedtime/Views/SleepBankCard.swift index de8daa7..888f896 100644 --- a/Bedtime/Bedtime/Views/SleepBankCard.swift +++ b/Bedtime/Bedtime/Views/SleepBankCard.swift @@ -16,7 +16,7 @@ struct SleepBankCard: View { HStack { Image(systemName: sleepBank.isInDebt ? "moon.zzz.fill" : "moon.stars.fill") .font(.title2) - .foregroundColor(sleepBank.averageHours == nil ? .secondary : sleepBank.isInDebt ? .red : .green) + .foregroundColor(sleepBank.averageHours == nil ? .secondary : sleepBank.isInDebt ? AppColors.negative : AppColors.positive) .frame(width: Constants.iconWidth) VStack(alignment: .leading, spacing: 4) { @@ -44,7 +44,7 @@ struct SleepBankCard: View { Text(String(format: "%.1f", averageHours)) .font(.title2) .fontWeight(.bold) - .foregroundColor(sleepBank.isInDebt ? .red : .green) + .foregroundColor(sleepBank.isInDebt ? AppColors.negative : AppColors.positive) Text("hours") .font(.subheadline) .foregroundColor(.secondary) @@ -69,7 +69,7 @@ struct SleepBankCard: View { Text(String(format: "%.1f", abs(sleepBank.currentBalance))) .font(.title2) .fontWeight(.bold) - .foregroundColor(sleepBank.isInDebt ? .red : .green) + .foregroundColor(sleepBank.isInDebt ? AppColors.negative : AppColors.positive) Text("hours \(sleepBank.isInDebt ? "behind" : "ahead")") .font(.subheadline) From b9d9e891cbb0351c54f9488a584d861791e2ad15 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 29 May 2026 16:28:53 +0000 Subject: [PATCH 02/16] Update colors to cozy theme matching app icon Replace the default system blues and purples with a warm palette inspired by the app icon: coral accents, cream backgrounds, dusty mauve and brown sleep-stage colors, and sage/terracotta status tones. Asset catalog colors now use adaptive light/dark cozy values. Co-authored-by: Greg --- .../AccentColor.colorset/Contents.json | 27 ++++++++++ .../Contents.json | 18 +++++-- .../CardBackground.colorset/Contents.json | 18 +++++-- Bedtime/Bedtime/Utils/AppColors.swift | 52 ++++++++++++++----- .../Views/Components/ProgressBar.swift | 2 +- 5 files changed, 95 insertions(+), 22 deletions(-) diff --git a/Bedtime/Bedtime/Assets.xcassets/AccentColor.colorset/Contents.json b/Bedtime/Bedtime/Assets.xcassets/AccentColor.colorset/Contents.json index eb87897..7a778b5 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" : "0.486", + "green" : "0.596", + "red" : "0.973" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.573", + "green" : "0.659", + "red" : "0.961" + } + }, "idiom" : "universal" } ], diff --git a/Bedtime/Bedtime/Assets.xcassets/BackgroundBehindCards.colorset/Contents.json b/Bedtime/Bedtime/Assets.xcassets/BackgroundBehindCards.colorset/Contents.json index adc86b2..adfe964 100644 --- a/Bedtime/Bedtime/Assets.xcassets/BackgroundBehindCards.colorset/Contents.json +++ b/Bedtime/Bedtime/Assets.xcassets/BackgroundBehindCards.colorset/Contents.json @@ -2,8 +2,13 @@ "colors" : [ { "color" : { - "platform" : "ios", - "reference" : "secondarySystemBackgroundColor" + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.929", + "green" : "0.957", + "red" : "0.984" + } }, "idiom" : "universal" }, @@ -15,8 +20,13 @@ } ], "color" : { - "platform" : "ios", - "reference" : "systemBackgroundColor" + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.118", + "green" : "0.129", + "red" : "0.165" + } }, "idiom" : "universal" } diff --git a/Bedtime/Bedtime/Assets.xcassets/CardBackground.colorset/Contents.json b/Bedtime/Bedtime/Assets.xcassets/CardBackground.colorset/Contents.json index d551d24..5e4763b 100644 --- a/Bedtime/Bedtime/Assets.xcassets/CardBackground.colorset/Contents.json +++ b/Bedtime/Bedtime/Assets.xcassets/CardBackground.colorset/Contents.json @@ -2,8 +2,13 @@ "colors" : [ { "color" : { - "platform" : "ios", - "reference" : "systemBackgroundColor" + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.953", + "green" : "0.973", + "red" : "0.992" + } }, "idiom" : "universal" }, @@ -15,8 +20,13 @@ } ], "color" : { - "platform" : "ios", - "reference" : "tertiarySystemBackgroundColor" + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.173", + "green" : "0.196", + "red" : "0.239" + } }, "idiom" : "universal" } diff --git a/Bedtime/Bedtime/Utils/AppColors.swift b/Bedtime/Bedtime/Utils/AppColors.swift index a2d0d43..0c86d10 100644 --- a/Bedtime/Bedtime/Utils/AppColors.swift +++ b/Bedtime/Bedtime/Utils/AppColors.swift @@ -8,26 +8,52 @@ import SwiftUI enum AppColors { // MARK: - Accent - static let accent = Color.blue + /// Warm coral from the app icon (#f8987c). + static let accent = cozyColor(light: (0.973, 0.596, 0.486), dark: (0.961, 0.659, 0.573)) // MARK: - Card accents - static let bedtime = Color.blue - static let lastNight = Color.blue - static let recentSleep = Color.purple - static let healthKit = Color.red + static let bedtime = accent + static let lastNight = accent + + /// Dusty mauve brown from the icon (#9b786d). + static let recentSleep = cozyColor(light: (0.608, 0.471, 0.427), dark: (0.729, 0.588, 0.533)) + + /// Soft terracotta rose for health prompts. + static let healthKit = cozyColor(light: (0.788, 0.482, 0.420), dark: (0.878, 0.573, 0.506)) // MARK: - Status - static let positive = Color.green - static let negative = Color.red - static let warning = Color.orange + /// Muted sage green. + static let positive = cozyColor(light: (0.541, 0.671, 0.478), dark: (0.620, 0.749, 0.557)) + + /// Warm terracotta red. + static let negative = cozyColor(light: (0.788, 0.482, 0.420), dark: (0.878, 0.573, 0.506)) + + /// Soft amber peach. + static let warning = cozyColor(light: (0.910, 0.620, 0.451), dark: (0.941, 0.698, 0.541)) // MARK: - Sleep stages - static let sleepDeep = Color.blue - static let sleepREM = Color.purple - static let sleepCore = Color.indigo - static let sleepAwake = Color.orange - static let sleepInBed = Color.gray + /// Deep warm brown (#826056). + static let sleepDeep = cozyColor(light: (0.510, 0.376, 0.337), dark: (0.639, 0.502, 0.463)) + + static let sleepREM = recentSleep + + /// Plum brown for core sleep. + static let sleepCore = cozyColor(light: (0.478, 0.345, 0.380), dark: (0.588, 0.455, 0.490)) + + static let sleepAwake = cozyColor(light: (0.969, 0.592, 0.482), dark: (0.980, 0.667, 0.573)) + + /// Warm taupe (#584744). + static let sleepInBed = cozyColor(light: (0.345, 0.278, 0.267), dark: (0.478, 0.404, 0.384)) + + private static func cozyColor(light: (Double, Double, Double), dark: (Double, Double, Double)) -> Color { + Color( + uiColor: UIColor { traits in + let rgb = traits.userInterfaceStyle == .dark ? dark : light + return UIColor(red: rgb.0, green: rgb.1, blue: rgb.2, alpha: 1) + } + ) + } } diff --git a/Bedtime/Bedtime/Views/Components/ProgressBar.swift b/Bedtime/Bedtime/Views/Components/ProgressBar.swift index 7dae95d..6308f7a 100644 --- a/Bedtime/Bedtime/Views/Components/ProgressBar.swift +++ b/Bedtime/Bedtime/Views/Components/ProgressBar.swift @@ -34,7 +34,7 @@ struct ProgressBar: View { .tint(Gradient(colors: [AppColors.recentSleep, AppColors.bedtime])) ProgressBar(value: 8, total: 10) - .tint(LinearGradient(colors: [AppColors.positive, .cyan], startPoint: .leading, endPoint: .trailing)) + .tint(LinearGradient(colors: [AppColors.positive, AppColors.accent], startPoint: .leading, endPoint: .trailing)) } .frame(height: 64) .padding() From 2ef5fea68dedd51da8e89a38778f388590450859 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 14 Aug 2026 20:34:56 +0000 Subject: [PATCH 03/16] Make theme a Settings option: System, Cozy, and Cozy (Black) Add an AppTheme enum (system / cozy / cozyBlack) and thread it through the app via EnvironmentValues.appTheme, so every color decision reacts to the selected theme instead of a fixed constant: - AppColors.swift: every color accessor now takes an AppTheme and returns either the original system colors (blue/purple/red/green/orange/indigo/gray, as they were before the cozy palette existed) or the cozy warm palette. Background/card colors move here too (previously baked into the AccentColor/BackgroundBehindCards/CardBackground asset catalog entries), so they can vary by theme: cozy keeps the existing warm dark-brown dark mode, cozyBlack swaps it for a true black background with a near-black card color for OLED/maximum-contrast displays. - UserPreferences gains a persisted theme property (stored as a raw string, defaulting to cozy, falling back to cozy on an unrecognized value). - SettingsView adds an Appearance section with a theme picker. - ContentView injects the selected theme into the environment and applies it to the background and the app-wide tint. - Every view/model that previously read a fixed AppColors constant now takes an AppTheme parameter (via the appTheme environment key, or for SettingsView directly from the model to avoid depending on environment propagation into its sheet/inspector presentation). --- Bedtime/Bedtime/Constants.swift | 10 +- Bedtime/Bedtime/ContentView.swift | 4 +- Bedtime/Bedtime/Models/SleepData.swift | 16 +- Bedtime/Bedtime/Models/UserPreferences.swift | 12 +- Bedtime/Bedtime/Utils/AppColors.swift | 163 ++++++++++++++++-- Bedtime/Bedtime/Utils/AppTheme.swift | 40 +++++ .../Bedtime/Views/BalanceWaterfallChart.swift | 7 +- .../Views/BedtimeRecommendationCard.swift | 5 +- .../Views/Components/CardComponent.swift | 3 +- .../Components/IncludedDaysRangeHandle.swift | 13 +- .../Views/Components/ProgressBar.swift | 6 +- .../Views/Components/SleepDayGroup.swift | 6 +- .../Views/Components/SleepSessionRow.swift | 5 +- .../SleepSourceComparisonView.swift | 3 +- .../Views/HealthKitAuthorizationCard.swift | 5 +- Bedtime/Bedtime/Views/LastNightCard.swift | 6 +- .../Views/RecentSleepSessionsCard.swift | 5 +- Bedtime/Bedtime/Views/SettingsView.swift | 19 +- Bedtime/Bedtime/Views/SleepBankCard.swift | 7 +- Bedtime/Bedtime/Views/SleepInsightsCard.swift | 9 +- 20 files changed, 273 insertions(+), 71 deletions(-) create mode 100644 Bedtime/Bedtime/Utils/AppTheme.swift diff --git a/Bedtime/Bedtime/Constants.swift b/Bedtime/Bedtime/Constants.swift index df48636..7d87c5e 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 AppColors.positive } - if difference < -sleepGoalGraceHours { return AppColors.negative } + static func sleepGoalColor(difference: Double, graceColor: Color, theme: AppTheme) -> Color { + if difference >= 0 { return AppColors.positive(theme) } + if difference < -sleepGoalGraceHours { return AppColors.negative(theme) } return graceColor } - static func sleepDurationColor(hours: Double, goal: Double, graceColor: Color) -> Color { - sleepGoalColor(difference: hours - goal, graceColor: graceColor) + static func sleepDurationColor(hours: Double, goal: Double, graceColor: Color, theme: AppTheme) -> Color { + sleepGoalColor(difference: hours - goal, graceColor: graceColor, theme: theme) } } diff --git a/Bedtime/Bedtime/ContentView.swift b/Bedtime/Bedtime/ContentView.swift index c3044d4..4142cdd 100644 --- a/Bedtime/Bedtime/ContentView.swift +++ b/Bedtime/Bedtime/ContentView.swift @@ -188,7 +188,8 @@ struct ContentView: View { .frame(maxWidth: .infinity) } .environment(\.durationDisplayStyle, userPreferences.durationDisplayStyle) - .background(Color.backgroundBehindCards) + .environment(\.appTheme, userPreferences.theme) + .background(AppColors.background(userPreferences.theme)) .navigationTitle("Bedger") .toolbar { ToolbarItem(placement: .navigationBarTrailing) { @@ -219,6 +220,7 @@ struct ContentView: View { ) } } + .tint(AppColors.accent(userPreferences.theme)) .task { try? await healthKitManager.fetchSleepData() } diff --git a/Bedtime/Bedtime/Models/SleepData.swift b/Bedtime/Bedtime/Models/SleepData.swift index 426d056..cb9f31c 100644 --- a/Bedtime/Bedtime/Models/SleepData.swift +++ b/Bedtime/Bedtime/Models/SleepData.swift @@ -86,13 +86,13 @@ extension HKCategoryValueSleepAnalysis { } } - var color: Color { + func color(for theme: AppTheme) -> Color { switch self { - case .asleepDeep: return AppColors.sleepDeep - case .asleepREM: return AppColors.sleepREM - case .asleepCore: return AppColors.sleepCore - case .awake: return AppColors.sleepAwake - case .inBed: return AppColors.sleepInBed + case .asleepDeep: return AppColors.sleepDeep(theme) + case .asleepREM: return AppColors.sleepREM(theme) + case .asleepCore: return AppColors.sleepCore(theme) + case .awake: return AppColors.sleepAwake(theme) + case .inBed: return AppColors.sleepInBed(theme) case .asleepUnspecified: return Color.secondary @unknown default: return Color.secondary } @@ -127,9 +127,9 @@ struct SleepBank: Equatable { return currentBalance < 0 } - var statusColor: Color { + func statusColor(for theme: AppTheme) -> Color { guard averageHours != nil else { return .secondary } - return Constants.sleepGoalColor(difference: currentBalance, graceColor: .primary) + return Constants.sleepGoalColor(difference: currentBalance, graceColor: .primary, theme: theme) } var debtHours: Double { diff --git a/Bedtime/Bedtime/Models/UserPreferences.swift b/Bedtime/Bedtime/Models/UserPreferences.swift index bb35d5b..f097ec3 100644 --- a/Bedtime/Bedtime/Models/UserPreferences.swift +++ b/Bedtime/Bedtime/Models/UserPreferences.swift @@ -17,17 +17,26 @@ 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 var durationDisplayStyle: DurationDisplayStyle { 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 + useDecimalDurations: Bool = false, + theme: AppTheme = .cozy ) { self.sleepGoalHours = sleepGoalHours self.wakeTime = wakeTime @@ -35,6 +44,7 @@ final class UserPreferences { self.lastUpdated = Date() self.earliestReasonableBedtime = earliestReasonableBedtime self.useDecimalDurations = useDecimalDurations + self.themeRawValue = theme.rawValue } /// Convenience accessor for the nominal sleep-window length from this diff --git a/Bedtime/Bedtime/Utils/AppColors.swift b/Bedtime/Bedtime/Utils/AppColors.swift index 0c86d10..50b120b 100644 --- a/Bedtime/Bedtime/Utils/AppColors.swift +++ b/Bedtime/Bedtime/Utils/AppColors.swift @@ -5,48 +5,165 @@ import SwiftUI +/// Every color the app uses, keyed by the active `AppTheme`. Callers grab an +/// `@Environment(\.appTheme)` and pass it through rather than reading a fixed constant, so +/// switching themes in Settings updates the whole UI without any other code changes. enum AppColors { // MARK: - Accent - /// Warm coral from the app icon (#f8987c). - static let accent = cozyColor(light: (0.973, 0.596, 0.486), dark: (0.961, 0.659, 0.573)) + static func accent(_ theme: AppTheme) -> Color { + switch theme { + case .system: return .blue + case .cozy, .cozyBlack: return cozyAccent + } + } // MARK: - Card accents - static let bedtime = accent - static let lastNight = accent + static func bedtime(_ theme: AppTheme) -> Color { accent(theme) } + static func lastNight(_ theme: AppTheme) -> Color { accent(theme) } - /// Dusty mauve brown from the icon (#9b786d). - static let recentSleep = cozyColor(light: (0.608, 0.471, 0.427), dark: (0.729, 0.588, 0.533)) + static func recentSleep(_ theme: AppTheme) -> Color { + switch theme { + case .system: return .purple + case .cozy, .cozyBlack: return cozyRecentSleep + } + } - /// Soft terracotta rose for health prompts. - static let healthKit = cozyColor(light: (0.788, 0.482, 0.420), dark: (0.878, 0.573, 0.506)) + static func healthKit(_ theme: AppTheme) -> Color { + switch theme { + case .system: return .red + case .cozy, .cozyBlack: return cozyHealthKit + } + } // MARK: - Status + static func positive(_ theme: AppTheme) -> Color { + switch theme { + case .system: return .green + case .cozy, .cozyBlack: return cozyPositive + } + } + + static func negative(_ theme: AppTheme) -> Color { + switch theme { + case .system: return .red + case .cozy, .cozyBlack: return cozyNegative + } + } + + static func warning(_ theme: AppTheme) -> Color { + switch theme { + case .system: return .orange + case .cozy, .cozyBlack: return cozyWarning + } + } + + // MARK: - Sleep stages + + static func sleepDeep(_ theme: AppTheme) -> Color { + switch theme { + case .system: return .blue + case .cozy, .cozyBlack: return cozySleepDeep + } + } + + static func sleepREM(_ theme: AppTheme) -> Color { + switch theme { + case .system: return .purple + case .cozy, .cozyBlack: return recentSleep(theme) + } + } + + static func sleepCore(_ theme: AppTheme) -> Color { + switch theme { + case .system: return .indigo + case .cozy, .cozyBlack: return cozySleepCore + } + } + + static func sleepAwake(_ theme: AppTheme) -> Color { + switch theme { + case .system: return .orange + case .cozy, .cozyBlack: return cozySleepAwake + } + } + + static func sleepInBed(_ theme: AppTheme) -> Color { + switch theme { + case .system: return .gray + case .cozy, .cozyBlack: return cozySleepInBed + } + } + + // MARK: - Backgrounds + + static func background(_ theme: AppTheme) -> Color { + switch theme { + case .system: + return dynamicColor(light: .secondarySystemBackground, dark: .systemBackground) + case .cozy: + return cozyColor(light: creamBackground, dark: cozyDarkBackground) + case .cozyBlack: + return cozyColor(light: creamBackground, dark: pureBlack) + } + } + + static func cardBackground(_ theme: AppTheme) -> Color { + switch theme { + case .system: + return dynamicColor(light: .systemBackground, dark: .tertiarySystemBackground) + case .cozy: + return cozyColor(light: creamCardBackground, dark: cozyDarkCardBackground) + case .cozyBlack: + return cozyColor(light: creamCardBackground, dark: nearBlackCardBackground) + } + } + + // MARK: - Cozy palette + + /// Warm coral from the app icon (#f8987c). + private static let cozyAccent = cozyColor(light: (0.973, 0.596, 0.486), dark: (0.961, 0.659, 0.573)) + + /// Dusty mauve brown from the icon (#9b786d). + private static let cozyRecentSleep = cozyColor(light: (0.608, 0.471, 0.427), dark: (0.729, 0.588, 0.533)) + + /// Soft terracotta rose for health prompts. + private static let cozyHealthKit = cozyColor(light: (0.788, 0.482, 0.420), dark: (0.878, 0.573, 0.506)) + /// Muted sage green. - static let positive = cozyColor(light: (0.541, 0.671, 0.478), dark: (0.620, 0.749, 0.557)) + private static let cozyPositive = cozyColor(light: (0.541, 0.671, 0.478), dark: (0.620, 0.749, 0.557)) /// Warm terracotta red. - static let negative = cozyColor(light: (0.788, 0.482, 0.420), dark: (0.878, 0.573, 0.506)) + private static let cozyNegative = cozyColor(light: (0.788, 0.482, 0.420), dark: (0.878, 0.573, 0.506)) /// Soft amber peach. - static let warning = cozyColor(light: (0.910, 0.620, 0.451), dark: (0.941, 0.698, 0.541)) - - // MARK: - Sleep stages + private static let cozyWarning = cozyColor(light: (0.910, 0.620, 0.451), dark: (0.941, 0.698, 0.541)) /// Deep warm brown (#826056). - static let sleepDeep = cozyColor(light: (0.510, 0.376, 0.337), dark: (0.639, 0.502, 0.463)) - - static let sleepREM = recentSleep + private static let cozySleepDeep = cozyColor(light: (0.510, 0.376, 0.337), dark: (0.639, 0.502, 0.463)) /// Plum brown for core sleep. - static let sleepCore = cozyColor(light: (0.478, 0.345, 0.380), dark: (0.588, 0.455, 0.490)) + private static let cozySleepCore = cozyColor(light: (0.478, 0.345, 0.380), dark: (0.588, 0.455, 0.490)) - static let sleepAwake = cozyColor(light: (0.969, 0.592, 0.482), dark: (0.980, 0.667, 0.573)) + private static let cozySleepAwake = cozyColor(light: (0.969, 0.592, 0.482), dark: (0.980, 0.667, 0.573)) /// Warm taupe (#584744). - static let sleepInBed = cozyColor(light: (0.345, 0.278, 0.267), dark: (0.478, 0.404, 0.384)) + private static let cozySleepInBed = cozyColor(light: (0.345, 0.278, 0.267), dark: (0.478, 0.404, 0.384)) + + /// Cream, shared by both cozy variants in light mode. + private static let creamBackground: (Double, Double, Double) = (0.984, 0.957, 0.929) + private static let creamCardBackground: (Double, Double, Double) = (0.992, 0.973, 0.953) + + /// Dark warm brown, used behind cards in the standard cozy theme's dark mode. + private static let cozyDarkBackground: (Double, Double, Double) = (0.165, 0.129, 0.118) + private static let cozyDarkCardBackground: (Double, Double, Double) = (0.239, 0.196, 0.173) + + /// True black, used behind cards in the cozy-black theme's dark mode. + private static let pureBlack: (Double, Double, Double) = (0, 0, 0) + /// Slightly lighter than pure black so cards stay visible against the black background. + private static let nearBlackCardBackground: (Double, Double, Double) = (0.110, 0.110, 0.110) private static func cozyColor(light: (Double, Double, Double), dark: (Double, Double, Double)) -> Color { Color( @@ -56,4 +173,12 @@ enum AppColors { } ) } + + private static func dynamicColor(light: UIColor, dark: UIColor) -> Color { + Color( + uiColor: UIColor { traits in + traits.userInterfaceStyle == .dark ? dark : light + } + ) + } } diff --git a/Bedtime/Bedtime/Utils/AppTheme.swift b/Bedtime/Bedtime/Utils/AppTheme.swift new file mode 100644 index 0000000..44c4327 --- /dev/null +++ b/Bedtime/Bedtime/Utils/AppTheme.swift @@ -0,0 +1,40 @@ +// +// 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: coral accents, cream backgrounds, dusty + /// mauve/brown sleep-stage colors. + case cozy + /// The cozy palette with a true-black background and cards in dark mode, for OLED + /// displays and users who prefer maximum contrast. + case cozyBlack + + var id: String { rawValue } + + var displayName: String { + switch self { + case .system: return "System" + case .cozy: return "Cozy" + case .cozyBlack: return "Cozy (Black)" + } + } +} + +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/Views/BalanceWaterfallChart.swift b/Bedtime/Bedtime/Views/BalanceWaterfallChart.swift index fcf4a1b..a8e703b 100644 --- a/Bedtime/Bedtime/Views/BalanceWaterfallChart.swift +++ b/Bedtime/Bedtime/Views/BalanceWaterfallChart.swift @@ -29,6 +29,7 @@ struct BalanceWaterfallChart: View { let domain: ClosedRange @Binding var selectedDays: Int var daysRange: ClosedRange = Constants.sleepBankDaysRange + @Environment(\.appTheme) private var theme private var impactByDate: [Date: BalanceDayImpact] { Dictionary(uniqueKeysWithValues: impacts.map { ($0.date, $0) }) @@ -102,10 +103,10 @@ struct BalanceWaterfallChart: View { let dividerY = min(max(yPosition(for: endingBalance, in: size.height), 0), size.height) return VStack(spacing: 0) { Rectangle() - .fill(AppColors.negative.opacity(0.1)) + .fill(AppColors.negative(theme).opacity(0.1)) .frame(height: dividerY) Rectangle() - .fill(AppColors.positive.opacity(0.1)) + .fill(AppColors.positive(theme).opacity(0.1)) } } @@ -127,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 ? AppColors.positive : AppColors.negative) + .fill(step.isGain ? AppColors.positive(theme) : AppColors.negative(theme)) .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 12e91fe..c2237f0 100644 --- a/Bedtime/Bedtime/Views/BedtimeRecommendationCard.swift +++ b/Bedtime/Bedtime/Views/BedtimeRecommendationCard.swift @@ -13,13 +13,14 @@ struct BedtimeRecommendationCard: View { var wakeTime: Binding? = nil @State private var isEditingWakeTime = false + @Environment(\.appTheme) private var theme var body: some View { CardComponent { VStack(spacing: 16) { CardHeader( icon: "bed.double.fill", - iconColor: AppColors.bedtime, + iconColor: AppColors.bedtime(theme), title: "Tonight's Recommendation" ) { Text("Based on your sleep bank") @@ -38,7 +39,7 @@ struct BedtimeRecommendationCard: View { Text(TimeFormatter.formatTimeOfDay(recommendation.recommendedBedtime)) .font(.title) .fontWeight(.bold) - .foregroundColor(AppColors.bedtime) + .foregroundColor(AppColors.bedtime(theme)) } Spacer() diff --git a/Bedtime/Bedtime/Views/Components/CardComponent.swift b/Bedtime/Bedtime/Views/Components/CardComponent.swift index a2c1916..b73b465 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(AppColors.cardBackground(theme)) .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..f596def 100644 --- a/Bedtime/Bedtime/Views/Components/IncludedDaysRangeHandle.swift +++ b/Bedtime/Bedtime/Views/Components/IncludedDaysRangeHandle.swift @@ -15,6 +15,7 @@ struct IncludedDaysRangeHandle: View { var rowStride: CGFloat = 64 @GestureState private var dragTranslation: CGFloat = 0 + @Environment(\.appTheme) private var theme private var displayedDays: Int { clamped(days + dayDelta(for: dragTranslation)) @@ -24,7 +25,7 @@ struct IncludedDaysRangeHandle: View { ZStack(alignment: .leading) { VStack(spacing: 0) { Rectangle() - .fill(Color.accentColor) + .fill(AppColors.accent(theme)) Rectangle() .fill(Color.secondary.opacity(0.2)) } @@ -32,13 +33,13 @@ struct IncludedDaysRangeHandle: View { ZStack(alignment: .leading) { Capsule() - .fill(Color.accentColor.opacity(0.45)) + .fill(AppColors.accent(theme).opacity(0.45)) .frame(height: 2) HStack(spacing: 6) { ZStack { Circle() - .fill(Color.accentColor) + .fill(AppColors.accent(theme)) Image(systemName: "arrow.up.and.down") .font(.caption2.weight(.bold)) .foregroundStyle(.white) @@ -49,16 +50,16 @@ struct IncludedDaysRangeHandle: View { Text("\(displayedDays) days included") .font(.caption.weight(.semibold)) - .foregroundStyle(Color.accentColor) + .foregroundStyle(AppColors.accent(theme)) .padding(.horizontal, 10) .padding(.vertical, 6) .background( Capsule() - .fill(Color.cardBackground) + .fill(AppColors.cardBackground(theme)) ) .overlay( Capsule() - .stroke(Color.accentColor.opacity(0.35), lineWidth: 1) + .stroke(AppColors.accent(theme).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 6308f7a..f0e6ed5 100644 --- a/Bedtime/Bedtime/Views/Components/ProgressBar.swift +++ b/Bedtime/Bedtime/Views/Components/ProgressBar.swift @@ -28,13 +28,13 @@ struct ProgressBar: View { #Preview(traits: .sizeThatFitsLayout) { VStack { ProgressBar(value: 0.2) - .tint(Gradient(colors: [AppColors.warning, AppColors.negative])) + .tint(Gradient(colors: [AppColors.warning(.cozy), AppColors.negative(.cozy)])) ProgressBar(value: 6, total: 8) - .tint(Gradient(colors: [AppColors.recentSleep, AppColors.bedtime])) + .tint(Gradient(colors: [AppColors.recentSleep(.cozy), AppColors.bedtime(.cozy)])) ProgressBar(value: 8, total: 10) - .tint(LinearGradient(colors: [AppColors.positive, AppColors.accent], startPoint: .leading, endPoint: .trailing)) + .tint(LinearGradient(colors: [AppColors.positive(.cozy), AppColors.accent(.cozy)], 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..01c776b 100644 --- a/Bedtime/Bedtime/Views/Components/SleepDayGroup.swift +++ b/Bedtime/Bedtime/Views/Components/SleepDayGroup.swift @@ -18,6 +18,7 @@ struct SleepDayGroup: View { var isIncludedInSleepBank: Bool = true let onToggle: () -> Void @Environment(\.durationDisplayStyle) private var durationStyle + @Environment(\.appTheme) private var theme private var dateFormatter: DateFormatter { let formatter = DateFormatter() @@ -37,7 +38,8 @@ struct SleepDayGroup: View { Constants.sleepDurationColor( hours: totalSleepHours, goal: sleepGoal, - graceColor: .secondary + graceColor: .secondary, + theme: theme ) ) } @@ -49,7 +51,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 ? AppColors.accent(theme) : 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..dd842a3 100644 --- a/Bedtime/Bedtime/Views/Components/SleepSessionRow.swift +++ b/Bedtime/Bedtime/Views/Components/SleepSessionRow.swift @@ -11,12 +11,13 @@ import HealthKit struct SleepSessionRow: View { let session: SleepSession @Environment(\.durationDisplayStyle) private var durationStyle + @Environment(\.appTheme) private var theme var body: some View { HStack(spacing: 6) { Image(systemName: session.sleepType.icon) .font(.caption) - .foregroundColor(Color(session.sleepType.color)) + .foregroundColor(session.sleepType.color(for: theme)) VStack(alignment: .leading, spacing: 2) { Text("\(TimeFormatter.formatTimeOfDay(session.startDate)) - \(TimeFormatter.formatTimeOfDay(session.endDate))") @@ -25,7 +26,7 @@ struct SleepSessionRow: View { Text(session.sleepType.displayName) .font(.caption2) - .foregroundColor(Color(session.sleepType.color)) + .foregroundColor(session.sleepType.color(for: theme)) } Spacer() diff --git a/Bedtime/Bedtime/Views/Components/SleepSourceComparisonView.swift b/Bedtime/Bedtime/Views/Components/SleepSourceComparisonView.swift index 1e2c379..53bd18c 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(for: theme)) .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 e63c84c..03844f1 100644 --- a/Bedtime/Bedtime/Views/HealthKitAuthorizationCard.swift +++ b/Bedtime/Bedtime/Views/HealthKitAuthorizationCard.swift @@ -9,13 +9,14 @@ import SwiftUI struct HealthKitAuthorizationCard: View { @ObservedObject var healthKitManager: HealthKitManager + @Environment(\.appTheme) private var theme var body: some View { CardComponent { VStack(spacing: 16) { CardHeader( icon: "heart.text.square", - iconColor: AppColors.healthKit, + iconColor: AppColors.healthKit(theme), title: "HealthKit Access Required" ) { Text("We need access to your sleep data") @@ -31,7 +32,7 @@ struct HealthKitAuthorizationCard: View { if let errorMessage = healthKitManager.errorMessage { Text(errorMessage) .font(.caption) - .foregroundColor(AppColors.healthKit) + .foregroundColor(AppColors.healthKit(theme)) .multilineTextAlignment(.leading) } diff --git a/Bedtime/Bedtime/Views/LastNightCard.swift b/Bedtime/Bedtime/Views/LastNightCard.swift index f433db0..8560a00 100644 --- a/Bedtime/Bedtime/Views/LastNightCard.swift +++ b/Bedtime/Bedtime/Views/LastNightCard.swift @@ -12,6 +12,7 @@ struct LastNightCard: View { let goal: TimeInterval let sourceAppLinks: [SleepSourceAppLink] @Environment(\.durationDisplayStyle) private var durationStyle + @Environment(\.appTheme) private var theme var durationInHours: TimeInterval? { sleepSessions?.map(\.durationInHours).reduce(0, +) @@ -22,7 +23,8 @@ struct LastNightCard: View { return Constants.sleepDurationColor( hours: durationInHours, goal: goal, - graceColor: .primary + graceColor: .primary, + theme: theme ) } @@ -31,7 +33,7 @@ struct LastNightCard: View { VStack(spacing: 16) { CardHeader( icon: "calendar", - iconColor: AppColors.lastNight, + iconColor: AppColors.lastNight(theme), title: "Last Night" ) diff --git a/Bedtime/Bedtime/Views/RecentSleepSessionsCard.swift b/Bedtime/Bedtime/Views/RecentSleepSessionsCard.swift index 89f40b5..41ae5c0 100644 --- a/Bedtime/Bedtime/Views/RecentSleepSessionsCard.swift +++ b/Bedtime/Bedtime/Views/RecentSleepSessionsCard.swift @@ -34,6 +34,7 @@ struct RecentSleepSessionsCard: View { let sleepGoal: Double let excludedSourceIDs: Set @Binding var sleepBankDays: Int + @Environment(\.appTheme) private var theme @State private var expandedNights: Set = [] @@ -59,7 +60,7 @@ struct RecentSleepSessionsCard: View { VStack(spacing: 16) { CardHeader( icon: "chart.line.uptrend.xyaxis", - iconColor: AppColors.recentSleep, + iconColor: AppColors.recentSleep(theme), title: "Recent Sleep" ) @@ -96,7 +97,7 @@ struct RecentSleepSessionsCard: View { .padding(.leading, 11) .overlay(alignment: .leading) { Rectangle() - .fill(isIncluded ? Color.accentColor : Color.secondary.opacity(0.2)) + .fill(isIncluded ? AppColors.accent(theme) : Color.secondary.opacity(0.2)) .frame(width: 3) .accessibilityHidden(true) } diff --git a/Bedtime/Bedtime/Views/SettingsView.swift b/Bedtime/Bedtime/Views/SettingsView.swift index 88220a6..cdc47a5 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 theme: AppTheme { preferences.theme } private var sleepBankDaysBinding: Binding { Binding( @@ -36,6 +39,14 @@ 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) + } + } + } + Section("Sleep Goal") { VStack(alignment: .leading, spacing: 8) { HStack { @@ -56,7 +67,7 @@ struct SettingsView: View { ) { EmptyView() } - .accentColor(AppColors.accent) + .accentColor(AppColors.accent(theme)) } } @@ -101,7 +112,7 @@ struct SettingsView: View { ) { EmptyView() } - .accentColor(AppColors.accent) + .accentColor(AppColors.accent(theme)) Text("How many recent days to include in your sleep bank calculation") .font(.caption) @@ -157,14 +168,14 @@ struct SettingsView: View { }) { Text("Source \(bundleIdentifier) is no longer available. Tap to delete.") .font(.caption) - .foregroundColor(AppColors.warning) + .foregroundColor(AppColors.warning(theme)) } } if allExcluded { Text("No sources selected. Sleep data will not be displayed.") .font(.caption) - .foregroundColor(AppColors.warning) + .foregroundColor(AppColors.warning(theme)) } } else if isLoadingSources { diff --git a/Bedtime/Bedtime/Views/SleepBankCard.swift b/Bedtime/Bedtime/Views/SleepBankCard.swift index 034daa1..31dde05 100644 --- a/Bedtime/Bedtime/Views/SleepBankCard.swift +++ b/Bedtime/Bedtime/Views/SleepBankCard.swift @@ -17,6 +17,7 @@ struct SleepBankCard: View { /// When provided, the goal is tappable and edits this value directly. var sleepGoalHours: Binding? = nil @Environment(\.durationDisplayStyle) private var durationStyle + @Environment(\.appTheme) private var theme @State private var isEditingGoal = false @@ -46,7 +47,7 @@ struct SleepBankCard: View { VStack(spacing: 10) { CardHeader( icon: sleepBank.isInDebt ? "moon.zzz.fill" : "moon.stars.fill", - iconColor: sleepBank.statusColor, + iconColor: sleepBank.statusColor(for: theme), title: "Sleep Balance" ) { Text(sleepBank.statusDescription(days: sleepBankDays)) @@ -66,7 +67,7 @@ struct SleepBankCard: View { Text(TimeFormatter.formatHours(averageHours, style: durationStyle)) .font(.title2) .fontWeight(.bold) - .foregroundColor(sleepBank.statusColor) + .foregroundColor(sleepBank.statusColor(for: theme)) } else { Text("unknown") .font(.subheadline) @@ -86,7 +87,7 @@ struct SleepBankCard: View { Text(TimeFormatter.formatHours(sleepBank.currentBalance, style: durationStyle)) .font(.title2) .fontWeight(.bold) - .foregroundColor(sleepBank.statusColor) + .foregroundColor(sleepBank.statusColor(for: theme)) } else { Text("unknown") .font(.subheadline) diff --git a/Bedtime/Bedtime/Views/SleepInsightsCard.swift b/Bedtime/Bedtime/Views/SleepInsightsCard.swift index ca4b336..bee73ee 100644 --- a/Bedtime/Bedtime/Views/SleepInsightsCard.swift +++ b/Bedtime/Bedtime/Views/SleepInsightsCard.swift @@ -14,12 +14,13 @@ 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 accentColor: Color { if insight.congratulationWindow != nil { - return AppColors.positive + return AppColors.positive(theme) } - return AppColors.warning + return AppColors.warning(theme) } private var iconName: String { @@ -70,7 +71,7 @@ struct SleepInsightsCard: View { .frame(maxWidth: .infinity) } .buttonStyle(.bordered) - .tint(AppColors.warning) + .tint(AppColors.warning(theme)) .padding(.top, 4) } @@ -86,7 +87,7 @@ struct SleepInsightsCard: View { .frame(maxWidth: .infinity) } .buttonStyle(.bordered) - .tint(AppColors.positive) + .tint(AppColors.positive(theme)) .padding(.top, 4) } } From 43615ca12115bec77961fb58ba1217f43621fa87 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 15 Aug 2026 02:39:41 +0000 Subject: [PATCH 04/16] Rework theming as per-theme palette structs conforming to a protocol Replace the theme-parameterized AppColors functions with a ThemeColorPalette protocol and one conforming struct per theme (SystemThemeColors, CozyThemeColors, CozyBlackThemeColors). AppTheme.colors hands back the palette for the active case, so call sites read colors.accent / theme.colors.background etc. instead of passing the theme into a switch on every access. CozyBlackThemeColors composes CozyThemeColors and only overrides background/cardBackground, so the two palettes can never drift apart on the colors they share. Where a color choice itself varies per case of some other type (sleep stage colors), that type now returns a KeyPath instead of switching on the theme, and the call site resolves it against the active palette (HKCategoryValueSleepAnalysis.color(in:), replacing color(for:)). Verified with a standalone Swift 6 snippet that keypaths rooted in an existential protocol resolve correctly against different conforming struct instances, including under strict concurrency checking. Constants.sleepGoalColor/sleepDurationColor and SleepBank.statusColor now take the resolved palette directly (positive/negative colors, not a keypath, since they also need to fall back to a caller-supplied grace color that isn't part of the palette). SettingsView keeps resolving preferences.theme.colors directly rather than through the appTheme environment key, since it's presented as a sheet/inspector and this avoids depending on environment propagation into that presentation. --- Bedtime/Bedtime/Constants.swift | 10 +- Bedtime/Bedtime/ContentView.swift | 4 +- Bedtime/Bedtime/Models/SleepData.swift | 27 ++- Bedtime/Bedtime/Utils/AppColors.swift | 184 ------------------ Bedtime/Bedtime/Utils/AppTheme.swift | 10 + .../Bedtime/Utils/CozyBlackThemeColors.swift | 30 +++ Bedtime/Bedtime/Utils/CozyThemeColors.swift | 50 +++++ Bedtime/Bedtime/Utils/SystemThemeColors.swift | 23 +++ Bedtime/Bedtime/Utils/ThemeColorPalette.swift | 53 +++++ .../Bedtime/Views/BalanceWaterfallChart.swift | 8 +- .../Views/BedtimeRecommendationCard.swift | 6 +- .../Views/Components/CardComponent.swift | 2 +- .../Components/IncludedDaysRangeHandle.swift | 14 +- .../Views/Components/ProgressBar.swift | 7 +- .../Views/Components/SleepDayGroup.swift | 6 +- .../Views/Components/SleepSessionRow.swift | 6 +- .../SleepSourceComparisonView.swift | 2 +- .../Views/HealthKitAuthorizationCard.swift | 6 +- Bedtime/Bedtime/Views/LastNightCard.swift | 6 +- .../Views/RecentSleepSessionsCard.swift | 6 +- Bedtime/Bedtime/Views/SettingsView.swift | 10 +- Bedtime/Bedtime/Views/SleepBankCard.swift | 8 +- Bedtime/Bedtime/Views/SleepInsightsCard.swift | 10 +- 23 files changed, 249 insertions(+), 239 deletions(-) delete mode 100644 Bedtime/Bedtime/Utils/AppColors.swift create mode 100644 Bedtime/Bedtime/Utils/CozyBlackThemeColors.swift create mode 100644 Bedtime/Bedtime/Utils/CozyThemeColors.swift create mode 100644 Bedtime/Bedtime/Utils/SystemThemeColors.swift create mode 100644 Bedtime/Bedtime/Utils/ThemeColorPalette.swift diff --git a/Bedtime/Bedtime/Constants.swift b/Bedtime/Bedtime/Constants.swift index 7d87c5e..6908b5c 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, theme: AppTheme) -> Color { - if difference >= 0 { return AppColors.positive(theme) } - if difference < -sleepGoalGraceHours { return AppColors.negative(theme) } + static func sleepGoalColor(difference: Double, graceColor: Color, palette: any ThemeColorPalette) -> Color { + if difference >= 0 { return palette.positive } + if difference < -sleepGoalGraceHours { return palette.negative } return graceColor } - static func sleepDurationColor(hours: Double, goal: Double, graceColor: Color, theme: AppTheme) -> Color { - sleepGoalColor(difference: hours - goal, graceColor: graceColor, theme: theme) + static func sleepDurationColor(hours: Double, goal: Double, graceColor: Color, palette: any ThemeColorPalette) -> Color { + sleepGoalColor(difference: hours - goal, graceColor: graceColor, palette: palette) } } diff --git a/Bedtime/Bedtime/ContentView.swift b/Bedtime/Bedtime/ContentView.swift index 4142cdd..a8f3584 100644 --- a/Bedtime/Bedtime/ContentView.swift +++ b/Bedtime/Bedtime/ContentView.swift @@ -189,7 +189,7 @@ struct ContentView: View { } .environment(\.durationDisplayStyle, userPreferences.durationDisplayStyle) .environment(\.appTheme, userPreferences.theme) - .background(AppColors.background(userPreferences.theme)) + .background(userPreferences.theme.colors.background) .navigationTitle("Bedger") .toolbar { ToolbarItem(placement: .navigationBarTrailing) { @@ -220,7 +220,7 @@ struct ContentView: View { ) } } - .tint(AppColors.accent(userPreferences.theme)) + .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 cb9f31c..980f4a1 100644 --- a/Bedtime/Bedtime/Models/SleepData.swift +++ b/Bedtime/Bedtime/Models/SleepData.swift @@ -86,17 +86,24 @@ extension HKCategoryValueSleepAnalysis { } } - func color(for theme: AppTheme) -> 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 AppColors.sleepDeep(theme) - case .asleepREM: return AppColors.sleepREM(theme) - case .asleepCore: return AppColors.sleepCore(theme) - case .awake: return AppColors.sleepAwake(theme) - case .inBed: return AppColors.sleepInBed(theme) - 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 } - func statusColor(for theme: AppTheme) -> Color { + func statusColor(in palette: any ThemeColorPalette) -> Color { guard averageHours != nil else { return .secondary } - return Constants.sleepGoalColor(difference: currentBalance, graceColor: .primary, theme: theme) + return Constants.sleepGoalColor(difference: currentBalance, graceColor: .primary, palette: palette) } var debtHours: Double { diff --git a/Bedtime/Bedtime/Utils/AppColors.swift b/Bedtime/Bedtime/Utils/AppColors.swift deleted file mode 100644 index 50b120b..0000000 --- a/Bedtime/Bedtime/Utils/AppColors.swift +++ /dev/null @@ -1,184 +0,0 @@ -// -// AppColors.swift -// Bedtime -// - -import SwiftUI - -/// Every color the app uses, keyed by the active `AppTheme`. Callers grab an -/// `@Environment(\.appTheme)` and pass it through rather than reading a fixed constant, so -/// switching themes in Settings updates the whole UI without any other code changes. -enum AppColors { - // MARK: - Accent - - static func accent(_ theme: AppTheme) -> Color { - switch theme { - case .system: return .blue - case .cozy, .cozyBlack: return cozyAccent - } - } - - // MARK: - Card accents - - static func bedtime(_ theme: AppTheme) -> Color { accent(theme) } - static func lastNight(_ theme: AppTheme) -> Color { accent(theme) } - - static func recentSleep(_ theme: AppTheme) -> Color { - switch theme { - case .system: return .purple - case .cozy, .cozyBlack: return cozyRecentSleep - } - } - - static func healthKit(_ theme: AppTheme) -> Color { - switch theme { - case .system: return .red - case .cozy, .cozyBlack: return cozyHealthKit - } - } - - // MARK: - Status - - static func positive(_ theme: AppTheme) -> Color { - switch theme { - case .system: return .green - case .cozy, .cozyBlack: return cozyPositive - } - } - - static func negative(_ theme: AppTheme) -> Color { - switch theme { - case .system: return .red - case .cozy, .cozyBlack: return cozyNegative - } - } - - static func warning(_ theme: AppTheme) -> Color { - switch theme { - case .system: return .orange - case .cozy, .cozyBlack: return cozyWarning - } - } - - // MARK: - Sleep stages - - static func sleepDeep(_ theme: AppTheme) -> Color { - switch theme { - case .system: return .blue - case .cozy, .cozyBlack: return cozySleepDeep - } - } - - static func sleepREM(_ theme: AppTheme) -> Color { - switch theme { - case .system: return .purple - case .cozy, .cozyBlack: return recentSleep(theme) - } - } - - static func sleepCore(_ theme: AppTheme) -> Color { - switch theme { - case .system: return .indigo - case .cozy, .cozyBlack: return cozySleepCore - } - } - - static func sleepAwake(_ theme: AppTheme) -> Color { - switch theme { - case .system: return .orange - case .cozy, .cozyBlack: return cozySleepAwake - } - } - - static func sleepInBed(_ theme: AppTheme) -> Color { - switch theme { - case .system: return .gray - case .cozy, .cozyBlack: return cozySleepInBed - } - } - - // MARK: - Backgrounds - - static func background(_ theme: AppTheme) -> Color { - switch theme { - case .system: - return dynamicColor(light: .secondarySystemBackground, dark: .systemBackground) - case .cozy: - return cozyColor(light: creamBackground, dark: cozyDarkBackground) - case .cozyBlack: - return cozyColor(light: creamBackground, dark: pureBlack) - } - } - - static func cardBackground(_ theme: AppTheme) -> Color { - switch theme { - case .system: - return dynamicColor(light: .systemBackground, dark: .tertiarySystemBackground) - case .cozy: - return cozyColor(light: creamCardBackground, dark: cozyDarkCardBackground) - case .cozyBlack: - return cozyColor(light: creamCardBackground, dark: nearBlackCardBackground) - } - } - - // MARK: - Cozy palette - - /// Warm coral from the app icon (#f8987c). - private static let cozyAccent = cozyColor(light: (0.973, 0.596, 0.486), dark: (0.961, 0.659, 0.573)) - - /// Dusty mauve brown from the icon (#9b786d). - private static let cozyRecentSleep = cozyColor(light: (0.608, 0.471, 0.427), dark: (0.729, 0.588, 0.533)) - - /// Soft terracotta rose for health prompts. - private static let cozyHealthKit = cozyColor(light: (0.788, 0.482, 0.420), dark: (0.878, 0.573, 0.506)) - - /// Muted sage green. - private static let cozyPositive = cozyColor(light: (0.541, 0.671, 0.478), dark: (0.620, 0.749, 0.557)) - - /// Warm terracotta red. - private static let cozyNegative = cozyColor(light: (0.788, 0.482, 0.420), dark: (0.878, 0.573, 0.506)) - - /// Soft amber peach. - private static let cozyWarning = cozyColor(light: (0.910, 0.620, 0.451), dark: (0.941, 0.698, 0.541)) - - /// Deep warm brown (#826056). - private static let cozySleepDeep = cozyColor(light: (0.510, 0.376, 0.337), dark: (0.639, 0.502, 0.463)) - - /// Plum brown for core sleep. - private static let cozySleepCore = cozyColor(light: (0.478, 0.345, 0.380), dark: (0.588, 0.455, 0.490)) - - private static let cozySleepAwake = cozyColor(light: (0.969, 0.592, 0.482), dark: (0.980, 0.667, 0.573)) - - /// Warm taupe (#584744). - private static let cozySleepInBed = cozyColor(light: (0.345, 0.278, 0.267), dark: (0.478, 0.404, 0.384)) - - /// Cream, shared by both cozy variants in light mode. - private static let creamBackground: (Double, Double, Double) = (0.984, 0.957, 0.929) - private static let creamCardBackground: (Double, Double, Double) = (0.992, 0.973, 0.953) - - /// Dark warm brown, used behind cards in the standard cozy theme's dark mode. - private static let cozyDarkBackground: (Double, Double, Double) = (0.165, 0.129, 0.118) - private static let cozyDarkCardBackground: (Double, Double, Double) = (0.239, 0.196, 0.173) - - /// True black, used behind cards in the cozy-black theme's dark mode. - private static let pureBlack: (Double, Double, Double) = (0, 0, 0) - /// Slightly lighter than pure black so cards stay visible against the black background. - private static let nearBlackCardBackground: (Double, Double, Double) = (0.110, 0.110, 0.110) - - private static func cozyColor(light: (Double, Double, Double), dark: (Double, Double, Double)) -> Color { - Color( - uiColor: UIColor { traits in - let rgb = traits.userInterfaceStyle == .dark ? dark : light - return UIColor(red: rgb.0, green: rgb.1, blue: rgb.2, alpha: 1) - } - ) - } - - private static func dynamicColor(light: UIColor, dark: UIColor) -> Color { - Color( - uiColor: UIColor { traits in - traits.userInterfaceStyle == .dark ? dark : light - } - ) - } -} diff --git a/Bedtime/Bedtime/Utils/AppTheme.swift b/Bedtime/Bedtime/Utils/AppTheme.swift index 44c4327..7b5e9c2 100644 --- a/Bedtime/Bedtime/Utils/AppTheme.swift +++ b/Bedtime/Bedtime/Utils/AppTheme.swift @@ -26,6 +26,16 @@ enum AppTheme: String, CaseIterable, Identifiable, Codable { case .cozyBlack: return "Cozy (Black)" } } + + /// 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() + case .cozyBlack: return CozyBlackThemeColors() + } + } } private struct AppThemeKey: EnvironmentKey { diff --git a/Bedtime/Bedtime/Utils/CozyBlackThemeColors.swift b/Bedtime/Bedtime/Utils/CozyBlackThemeColors.swift new file mode 100644 index 0000000..c941977 --- /dev/null +++ b/Bedtime/Bedtime/Utils/CozyBlackThemeColors.swift @@ -0,0 +1,30 @@ +// +// CozyBlackThemeColors.swift +// Bedtime +// + +import SwiftUI + +/// The cozy palette with a true-black background and near-black cards in dark mode, for +/// OLED displays and users who prefer maximum contrast. Everything but the backgrounds is +/// forwarded to `CozyThemeColors` so the two stay in sync automatically. +struct CozyBlackThemeColors: ThemeColorPalette { + /// Slightly lighter than pure black so cards stay visible against the black background. + private static let nearBlackCard: (Double, Double, Double) = (0.110, 0.110, 0.110) + + private let base = CozyThemeColors() + + var accent: Color { base.accent } + var recentSleep: Color { base.recentSleep } + var healthKit: Color { base.healthKit } + var positive: Color { base.positive } + var negative: Color { base.negative } + var warning: Color { base.warning } + var sleepDeep: Color { base.sleepDeep } + var sleepCore: Color { base.sleepCore } + var sleepAwake: Color { base.sleepAwake } + var sleepInBed: Color { base.sleepInBed } + + let background = dynamicColor(light: CozyThemeColors.cream, dark: (0, 0, 0)) + let cardBackground = dynamicColor(light: CozyThemeColors.creamCard, dark: Self.nearBlackCard) +} diff --git a/Bedtime/Bedtime/Utils/CozyThemeColors.swift b/Bedtime/Bedtime/Utils/CozyThemeColors.swift new file mode 100644 index 0000000..acf678c --- /dev/null +++ b/Bedtime/Bedtime/Utils/CozyThemeColors.swift @@ -0,0 +1,50 @@ +// +// CozyThemeColors.swift +// Bedtime +// + +import SwiftUI + +/// Warm palette inspired by the app icon: coral accents, cream backgrounds, dusty +/// mauve/brown sleep-stage colors. +struct CozyThemeColors: ThemeColorPalette { + /// Cream, shared with `CozyBlackThemeColors` in light mode. + static let cream: (Double, Double, Double) = (0.984, 0.957, 0.929) + static let creamCard: (Double, Double, Double) = (0.992, 0.973, 0.953) + + /// Dark warm brown, used behind cards in dark mode. + private static let darkBrown: (Double, Double, Double) = (0.165, 0.129, 0.118) + private static let darkBrownCard: (Double, Double, Double) = (0.239, 0.196, 0.173) + + /// Warm coral from the app icon (#f8987c). + let accent = dynamicColor(light: (0.973, 0.596, 0.486), dark: (0.961, 0.659, 0.573)) + + /// Dusty mauve brown from the icon (#9b786d). + let recentSleep = dynamicColor(light: (0.608, 0.471, 0.427), dark: (0.729, 0.588, 0.533)) + + /// Soft terracotta rose for health prompts. + let healthKit = dynamicColor(light: (0.788, 0.482, 0.420), dark: (0.878, 0.573, 0.506)) + + /// Muted sage green. + let positive = dynamicColor(light: (0.541, 0.671, 0.478), dark: (0.620, 0.749, 0.557)) + + /// Warm terracotta red. + let negative = dynamicColor(light: (0.788, 0.482, 0.420), dark: (0.878, 0.573, 0.506)) + + /// Soft amber peach. + let warning = dynamicColor(light: (0.910, 0.620, 0.451), dark: (0.941, 0.698, 0.541)) + + /// Deep warm brown (#826056). + let sleepDeep = dynamicColor(light: (0.510, 0.376, 0.337), dark: (0.639, 0.502, 0.463)) + + /// Plum brown for core sleep. + let sleepCore = dynamicColor(light: (0.478, 0.345, 0.380), dark: (0.588, 0.455, 0.490)) + + let sleepAwake = dynamicColor(light: (0.969, 0.592, 0.482), dark: (0.980, 0.667, 0.573)) + + /// Warm taupe (#584744). + let sleepInBed = dynamicColor(light: (0.345, 0.278, 0.267), dark: (0.478, 0.404, 0.384)) + + let background = dynamicColor(light: Self.cream, dark: Self.darkBrown) + let cardBackground = dynamicColor(light: Self.creamCard, dark: Self.darkBrownCard) +} diff --git a/Bedtime/Bedtime/Utils/SystemThemeColors.swift b/Bedtime/Bedtime/Utils/SystemThemeColors.swift new file mode 100644 index 0000000..947453f --- /dev/null +++ b/Bedtime/Bedtime/Utils/SystemThemeColors.swift @@ -0,0 +1,23 @@ +// +// SystemThemeColors.swift +// Bedtime +// + +import SwiftUI + +/// The original default iOS system colors, exactly as they were before the cozy palette +/// existed (see the app's git history for the extraction commit that predates it). +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) +} diff --git a/Bedtime/Bedtime/Utils/ThemeColorPalette.swift b/Bedtime/Bedtime/Utils/ThemeColorPalette.swift new file mode 100644 index 0000000..a062fe8 --- /dev/null +++ b/Bedtime/Bedtime/Utils/ThemeColorPalette.swift @@ -0,0 +1,53 @@ +// +// 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 } +} + +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 resolves to `light` or `dark` RGB depending on the current +/// `UIUserInterfaceStyle`, the same way asset-catalog colors do. +func dynamicColor(light: (Double, Double, Double), dark: (Double, Double, Double)) -> Color { + Color( + uiColor: UIColor { traits in + let rgb = traits.userInterfaceStyle == .dark ? dark : light + return UIColor(red: rgb.0, green: rgb.1, blue: rgb.2, alpha: 1) + } + ) +} + +/// 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 a8e703b..c1c1116 100644 --- a/Bedtime/Bedtime/Views/BalanceWaterfallChart.swift +++ b/Bedtime/Bedtime/Views/BalanceWaterfallChart.swift @@ -31,6 +31,8 @@ struct BalanceWaterfallChart: View { 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) }) } @@ -103,10 +105,10 @@ struct BalanceWaterfallChart: View { let dividerY = min(max(yPosition(for: endingBalance, in: size.height), 0), size.height) return VStack(spacing: 0) { Rectangle() - .fill(AppColors.negative(theme).opacity(0.1)) + .fill(colors.negative.opacity(0.1)) .frame(height: dividerY) Rectangle() - .fill(AppColors.positive(theme).opacity(0.1)) + .fill(colors.positive.opacity(0.1)) } } @@ -128,7 +130,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 ? AppColors.positive(theme) : AppColors.negative(theme)) + .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 c2237f0..d13840f 100644 --- a/Bedtime/Bedtime/Views/BedtimeRecommendationCard.swift +++ b/Bedtime/Bedtime/Views/BedtimeRecommendationCard.swift @@ -15,12 +15,14 @@ struct BedtimeRecommendationCard: View { @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: AppColors.bedtime(theme), + iconColor: colors.bedtime, title: "Tonight's Recommendation" ) { Text("Based on your sleep bank") @@ -39,7 +41,7 @@ struct BedtimeRecommendationCard: View { Text(TimeFormatter.formatTimeOfDay(recommendation.recommendedBedtime)) .font(.title) .fontWeight(.bold) - .foregroundColor(AppColors.bedtime(theme)) + .foregroundColor(colors.bedtime) } Spacer() diff --git a/Bedtime/Bedtime/Views/Components/CardComponent.swift b/Bedtime/Bedtime/Views/Components/CardComponent.swift index b73b465..e38ecb6 100644 --- a/Bedtime/Bedtime/Views/Components/CardComponent.swift +++ b/Bedtime/Bedtime/Views/Components/CardComponent.swift @@ -64,7 +64,7 @@ struct CardComponent: View { .padding() .background { RoundedRectangle(cornerRadius: 12) - .fill(AppColors.cardBackground(theme)) + .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 f596def..7ba60d3 100644 --- a/Bedtime/Bedtime/Views/Components/IncludedDaysRangeHandle.swift +++ b/Bedtime/Bedtime/Views/Components/IncludedDaysRangeHandle.swift @@ -17,6 +17,8 @@ struct IncludedDaysRangeHandle: View { @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)) } @@ -25,7 +27,7 @@ struct IncludedDaysRangeHandle: View { ZStack(alignment: .leading) { VStack(spacing: 0) { Rectangle() - .fill(AppColors.accent(theme)) + .fill(colors.accent) Rectangle() .fill(Color.secondary.opacity(0.2)) } @@ -33,13 +35,13 @@ struct IncludedDaysRangeHandle: View { ZStack(alignment: .leading) { Capsule() - .fill(AppColors.accent(theme).opacity(0.45)) + .fill(colors.accent.opacity(0.45)) .frame(height: 2) HStack(spacing: 6) { ZStack { Circle() - .fill(AppColors.accent(theme)) + .fill(colors.accent) Image(systemName: "arrow.up.and.down") .font(.caption2.weight(.bold)) .foregroundStyle(.white) @@ -50,16 +52,16 @@ struct IncludedDaysRangeHandle: View { Text("\(displayedDays) days included") .font(.caption.weight(.semibold)) - .foregroundStyle(AppColors.accent(theme)) + .foregroundStyle(colors.accent) .padding(.horizontal, 10) .padding(.vertical, 6) .background( Capsule() - .fill(AppColors.cardBackground(theme)) + .fill(colors.cardBackground) ) .overlay( Capsule() - .stroke(AppColors.accent(theme).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 f0e6ed5..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: [AppColors.warning(.cozy), AppColors.negative(.cozy)])) + .tint(Gradient(colors: [colors.warning, colors.negative])) ProgressBar(value: 6, total: 8) - .tint(Gradient(colors: [AppColors.recentSleep(.cozy), AppColors.bedtime(.cozy)])) + .tint(Gradient(colors: [colors.recentSleep, colors.bedtime])) ProgressBar(value: 8, total: 10) - .tint(LinearGradient(colors: [AppColors.positive(.cozy), AppColors.accent(.cozy)], 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 01c776b..6418d34 100644 --- a/Bedtime/Bedtime/Views/Components/SleepDayGroup.swift +++ b/Bedtime/Bedtime/Views/Components/SleepDayGroup.swift @@ -19,6 +19,8 @@ struct SleepDayGroup: View { 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() @@ -39,7 +41,7 @@ struct SleepDayGroup: View { hours: totalSleepHours, goal: sleepGoal, graceColor: .secondary, - theme: theme + palette: colors ) ) } @@ -51,7 +53,7 @@ struct SleepDayGroup: View { var body: some View { HStack(alignment: .top, spacing: 8) { Rectangle() - .fill(isIncludedInSleepBank ? AppColors.accent(theme) : 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 dd842a3..793b334 100644 --- a/Bedtime/Bedtime/Views/Components/SleepSessionRow.swift +++ b/Bedtime/Bedtime/Views/Components/SleepSessionRow.swift @@ -12,12 +12,14 @@ 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(session.sleepType.color(for: theme)) + .foregroundColor(session.sleepType.color(in: colors)) VStack(alignment: .leading, spacing: 2) { Text("\(TimeFormatter.formatTimeOfDay(session.startDate)) - \(TimeFormatter.formatTimeOfDay(session.endDate))") @@ -26,7 +28,7 @@ struct SleepSessionRow: View { Text(session.sleepType.displayName) .font(.caption2) - .foregroundColor(session.sleepType.color(for: theme)) + .foregroundColor(session.sleepType.color(in: colors)) } Spacer() diff --git a/Bedtime/Bedtime/Views/Components/SleepSourceComparisonView.swift b/Bedtime/Bedtime/Views/Components/SleepSourceComparisonView.swift index 53bd18c..96bb519 100644 --- a/Bedtime/Bedtime/Views/Components/SleepSourceComparisonView.swift +++ b/Bedtime/Bedtime/Views/Components/SleepSourceComparisonView.swift @@ -104,7 +104,7 @@ struct SleepStageTimelineBar: View { let width = session.duration / rangeDuration Rectangle() - .fill(session.sleepType.color(for: theme)) + .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 03844f1..fedcc7b 100644 --- a/Bedtime/Bedtime/Views/HealthKitAuthorizationCard.swift +++ b/Bedtime/Bedtime/Views/HealthKitAuthorizationCard.swift @@ -10,13 +10,15 @@ 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: AppColors.healthKit(theme), + iconColor: colors.healthKit, title: "HealthKit Access Required" ) { Text("We need access to your sleep data") @@ -32,7 +34,7 @@ struct HealthKitAuthorizationCard: View { if let errorMessage = healthKitManager.errorMessage { Text(errorMessage) .font(.caption) - .foregroundColor(AppColors.healthKit(theme)) + .foregroundColor(colors.healthKit) .multilineTextAlignment(.leading) } diff --git a/Bedtime/Bedtime/Views/LastNightCard.swift b/Bedtime/Bedtime/Views/LastNightCard.swift index 8560a00..57e215e 100644 --- a/Bedtime/Bedtime/Views/LastNightCard.swift +++ b/Bedtime/Bedtime/Views/LastNightCard.swift @@ -13,6 +13,8 @@ struct LastNightCard: View { 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, +) @@ -24,7 +26,7 @@ struct LastNightCard: View { hours: durationInHours, goal: goal, graceColor: .primary, - theme: theme + palette: colors ) } @@ -33,7 +35,7 @@ struct LastNightCard: View { VStack(spacing: 16) { CardHeader( icon: "calendar", - iconColor: AppColors.lastNight(theme), + iconColor: colors.lastNight, title: "Last Night" ) diff --git a/Bedtime/Bedtime/Views/RecentSleepSessionsCard.swift b/Bedtime/Bedtime/Views/RecentSleepSessionsCard.swift index 41ae5c0..43902ab 100644 --- a/Bedtime/Bedtime/Views/RecentSleepSessionsCard.swift +++ b/Bedtime/Bedtime/Views/RecentSleepSessionsCard.swift @@ -35,6 +35,8 @@ struct RecentSleepSessionsCard: View { let excludedSourceIDs: Set @Binding var sleepBankDays: Int @Environment(\.appTheme) private var theme + + private var colors: any ThemeColorPalette { theme.colors } @State private var expandedNights: Set = [] @@ -60,7 +62,7 @@ struct RecentSleepSessionsCard: View { VStack(spacing: 16) { CardHeader( icon: "chart.line.uptrend.xyaxis", - iconColor: AppColors.recentSleep(theme), + iconColor: colors.recentSleep, title: "Recent Sleep" ) @@ -97,7 +99,7 @@ struct RecentSleepSessionsCard: View { .padding(.leading, 11) .overlay(alignment: .leading) { Rectangle() - .fill(isIncluded ? AppColors.accent(theme) : Color.secondary.opacity(0.2)) + .fill(isIncluded ? colors.accent : Color.secondary.opacity(0.2)) .frame(width: 3) .accessibilityHidden(true) } diff --git a/Bedtime/Bedtime/Views/SettingsView.swift b/Bedtime/Bedtime/Views/SettingsView.swift index cdc47a5..af7a697 100644 --- a/Bedtime/Bedtime/Views/SettingsView.swift +++ b/Bedtime/Bedtime/Views/SettingsView.swift @@ -18,7 +18,7 @@ struct SettingsView: View { @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 theme: AppTheme { preferences.theme } + private var colors: any ThemeColorPalette { preferences.theme.colors } private var sleepBankDaysBinding: Binding { Binding( @@ -67,7 +67,7 @@ struct SettingsView: View { ) { EmptyView() } - .accentColor(AppColors.accent(theme)) + .accentColor(colors.accent) } } @@ -112,7 +112,7 @@ struct SettingsView: View { ) { EmptyView() } - .accentColor(AppColors.accent(theme)) + .accentColor(colors.accent) Text("How many recent days to include in your sleep bank calculation") .font(.caption) @@ -168,14 +168,14 @@ struct SettingsView: View { }) { Text("Source \(bundleIdentifier) is no longer available. Tap to delete.") .font(.caption) - .foregroundColor(AppColors.warning(theme)) + .foregroundColor(colors.warning) } } if allExcluded { Text("No sources selected. Sleep data will not be displayed.") .font(.caption) - .foregroundColor(AppColors.warning(theme)) + .foregroundColor(colors.warning) } } else if isLoadingSources { diff --git a/Bedtime/Bedtime/Views/SleepBankCard.swift b/Bedtime/Bedtime/Views/SleepBankCard.swift index 31dde05..2ba4c57 100644 --- a/Bedtime/Bedtime/Views/SleepBankCard.swift +++ b/Bedtime/Bedtime/Views/SleepBankCard.swift @@ -19,6 +19,8 @@ struct SleepBankCard: View { @Environment(\.durationDisplayStyle) private var durationStyle @Environment(\.appTheme) private var theme + private var colors: any ThemeColorPalette { theme.colors } + @State private var isEditingGoal = false private var formattedGoal: String { @@ -47,7 +49,7 @@ struct SleepBankCard: View { VStack(spacing: 10) { CardHeader( icon: sleepBank.isInDebt ? "moon.zzz.fill" : "moon.stars.fill", - iconColor: sleepBank.statusColor(for: theme), + iconColor: sleepBank.statusColor(in: colors), title: "Sleep Balance" ) { Text(sleepBank.statusDescription(days: sleepBankDays)) @@ -67,7 +69,7 @@ struct SleepBankCard: View { Text(TimeFormatter.formatHours(averageHours, style: durationStyle)) .font(.title2) .fontWeight(.bold) - .foregroundColor(sleepBank.statusColor(for: theme)) + .foregroundColor(sleepBank.statusColor(in: colors)) } else { Text("unknown") .font(.subheadline) @@ -87,7 +89,7 @@ struct SleepBankCard: View { Text(TimeFormatter.formatHours(sleepBank.currentBalance, style: durationStyle)) .font(.title2) .fontWeight(.bold) - .foregroundColor(sleepBank.statusColor(for: theme)) + .foregroundColor(sleepBank.statusColor(in: colors)) } else { Text("unknown") .font(.subheadline) diff --git a/Bedtime/Bedtime/Views/SleepInsightsCard.swift b/Bedtime/Bedtime/Views/SleepInsightsCard.swift index bee73ee..e4414c9 100644 --- a/Bedtime/Bedtime/Views/SleepInsightsCard.swift +++ b/Bedtime/Bedtime/Views/SleepInsightsCard.swift @@ -16,11 +16,13 @@ struct SleepInsightsCard: View { @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 AppColors.positive(theme) + return colors.positive } - return AppColors.warning(theme) + return colors.warning } private var iconName: String { @@ -71,7 +73,7 @@ struct SleepInsightsCard: View { .frame(maxWidth: .infinity) } .buttonStyle(.bordered) - .tint(AppColors.warning(theme)) + .tint(colors.warning) .padding(.top, 4) } @@ -87,7 +89,7 @@ struct SleepInsightsCard: View { .frame(maxWidth: .infinity) } .buttonStyle(.bordered) - .tint(AppColors.positive(theme)) + .tint(colors.positive) .padding(.top, 4) } } From c10ff3e642d03d3aaa4854dedefbb6a8252d962a Mon Sep 17 00:00:00 2001 From: Greg Date: Sat, 15 Aug 2026 20:45:03 -0700 Subject: [PATCH 05/16] remove separate pure black --- Bedtime/Bedtime/Utils/AppTheme.swift | 5 ---- .../Bedtime/Utils/CozyBlackThemeColors.swift | 30 ------------------- 2 files changed, 35 deletions(-) delete mode 100644 Bedtime/Bedtime/Utils/CozyBlackThemeColors.swift diff --git a/Bedtime/Bedtime/Utils/AppTheme.swift b/Bedtime/Bedtime/Utils/AppTheme.swift index 7b5e9c2..b99916a 100644 --- a/Bedtime/Bedtime/Utils/AppTheme.swift +++ b/Bedtime/Bedtime/Utils/AppTheme.swift @@ -13,9 +13,6 @@ enum AppTheme: String, CaseIterable, Identifiable, Codable { /// Warm palette inspired by the app icon: coral accents, cream backgrounds, dusty /// mauve/brown sleep-stage colors. case cozy - /// The cozy palette with a true-black background and cards in dark mode, for OLED - /// displays and users who prefer maximum contrast. - case cozyBlack var id: String { rawValue } @@ -23,7 +20,6 @@ enum AppTheme: String, CaseIterable, Identifiable, Codable { switch self { case .system: return "System" case .cozy: return "Cozy" - case .cozyBlack: return "Cozy (Black)" } } @@ -33,7 +29,6 @@ enum AppTheme: String, CaseIterable, Identifiable, Codable { switch self { case .system: return SystemThemeColors() case .cozy: return CozyThemeColors() - case .cozyBlack: return CozyBlackThemeColors() } } } diff --git a/Bedtime/Bedtime/Utils/CozyBlackThemeColors.swift b/Bedtime/Bedtime/Utils/CozyBlackThemeColors.swift deleted file mode 100644 index c941977..0000000 --- a/Bedtime/Bedtime/Utils/CozyBlackThemeColors.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// CozyBlackThemeColors.swift -// Bedtime -// - -import SwiftUI - -/// The cozy palette with a true-black background and near-black cards in dark mode, for -/// OLED displays and users who prefer maximum contrast. Everything but the backgrounds is -/// forwarded to `CozyThemeColors` so the two stay in sync automatically. -struct CozyBlackThemeColors: ThemeColorPalette { - /// Slightly lighter than pure black so cards stay visible against the black background. - private static let nearBlackCard: (Double, Double, Double) = (0.110, 0.110, 0.110) - - private let base = CozyThemeColors() - - var accent: Color { base.accent } - var recentSleep: Color { base.recentSleep } - var healthKit: Color { base.healthKit } - var positive: Color { base.positive } - var negative: Color { base.negative } - var warning: Color { base.warning } - var sleepDeep: Color { base.sleepDeep } - var sleepCore: Color { base.sleepCore } - var sleepAwake: Color { base.sleepAwake } - var sleepInBed: Color { base.sleepInBed } - - let background = dynamicColor(light: CozyThemeColors.cream, dark: (0, 0, 0)) - let cardBackground = dynamicColor(light: CozyThemeColors.creamCard, dark: Self.nearBlackCard) -} From f81b41af7ca66896787b2dfd284c78a6a031e405 Mon Sep 17 00:00:00 2001 From: Greg Date: Sat, 15 Aug 2026 20:45:32 -0700 Subject: [PATCH 06/16] keypath instead of having to pass it around --- Bedtime/Bedtime/Constants.swift | 12 ++++++------ Bedtime/Bedtime/Models/SleepData.swift | 6 +++--- Bedtime/Bedtime/Utils/CozyThemeColors.swift | 7 +++++-- Bedtime/Bedtime/Utils/SystemThemeColors.swift | 2 ++ Bedtime/Bedtime/Utils/ThemeColorPalette.swift | 2 ++ Bedtime/Bedtime/Views/Components/SleepDayGroup.swift | 11 +++++------ Bedtime/Bedtime/Views/LastNightCard.swift | 9 ++++----- Bedtime/Bedtime/Views/SleepBankCard.swift | 6 +++--- 8 files changed, 30 insertions(+), 25 deletions(-) diff --git a/Bedtime/Bedtime/Constants.swift b/Bedtime/Bedtime/Constants.swift index 6908b5c..ba57ec6 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, palette: any ThemeColorPalette) -> Color { - if difference >= 0 { return palette.positive } - if difference < -sleepGoalGraceHours { return palette.negative } - return graceColor + static func sleepGoalColor(difference: Double) -> KeyPath? { + if difference >= 0 { return \.positive } + if difference < -sleepGoalGraceHours { return \.negative } + return nil } - static func sleepDurationColor(hours: Double, goal: Double, graceColor: Color, palette: any ThemeColorPalette) -> Color { - sleepGoalColor(difference: hours - goal, graceColor: graceColor, palette: palette) + static func sleepDurationColor(hours: Double, goal: Double) -> KeyPath? { + sleepGoalColor(difference: hours - goal) } } diff --git a/Bedtime/Bedtime/Models/SleepData.swift b/Bedtime/Bedtime/Models/SleepData.swift index 980f4a1..d905fec 100644 --- a/Bedtime/Bedtime/Models/SleepData.swift +++ b/Bedtime/Bedtime/Models/SleepData.swift @@ -134,9 +134,9 @@ struct SleepBank: Equatable { return currentBalance < 0 } - func statusColor(in palette: any ThemeColorPalette) -> Color { - guard averageHours != nil else { return .secondary } - return Constants.sleepGoalColor(difference: currentBalance, graceColor: .primary, palette: palette) + var statusColor: KeyPath { + guard averageHours != nil else { return \.secondary } + return Constants.sleepGoalColor(difference: currentBalance) ?? \.secondary } var debtHours: Double { diff --git a/Bedtime/Bedtime/Utils/CozyThemeColors.swift b/Bedtime/Bedtime/Utils/CozyThemeColors.swift index acf678c..242dac5 100644 --- a/Bedtime/Bedtime/Utils/CozyThemeColors.swift +++ b/Bedtime/Bedtime/Utils/CozyThemeColors.swift @@ -45,6 +45,9 @@ struct CozyThemeColors: ThemeColorPalette { /// Warm taupe (#584744). let sleepInBed = dynamicColor(light: (0.345, 0.278, 0.267), dark: (0.478, 0.404, 0.384)) - let background = dynamicColor(light: Self.cream, dark: Self.darkBrown) - let cardBackground = dynamicColor(light: Self.creamCard, dark: Self.darkBrownCard) + let background = dynamicColor(light: CozyThemeColors.cream, dark: (0, 0, 0)) + let cardBackground = dynamicColor(light: CozyThemeColors.creamCard, dark: Self.creamCard) + + let primary: Color = .primary + let secondary: Color = .secondary } diff --git a/Bedtime/Bedtime/Utils/SystemThemeColors.swift b/Bedtime/Bedtime/Utils/SystemThemeColors.swift index 947453f..c9adb93 100644 --- a/Bedtime/Bedtime/Utils/SystemThemeColors.swift +++ b/Bedtime/Bedtime/Utils/SystemThemeColors.swift @@ -20,4 +20,6 @@ struct SystemThemeColors: ThemeColorPalette { 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 index a062fe8..8749b03 100644 --- a/Bedtime/Bedtime/Utils/ThemeColorPalette.swift +++ b/Bedtime/Bedtime/Utils/ThemeColorPalette.swift @@ -21,6 +21,8 @@ protocol ThemeColorPalette { var sleepInBed: Color { get } var background: Color { get } var cardBackground: Color { get } + var primary: Color { get } + var secondary: Color { get } } extension ThemeColorPalette { diff --git a/Bedtime/Bedtime/Views/Components/SleepDayGroup.swift b/Bedtime/Bedtime/Views/Components/SleepDayGroup.swift index 6418d34..c58b58f 100644 --- a/Bedtime/Bedtime/Views/Components/SleepDayGroup.swift +++ b/Bedtime/Bedtime/Views/Components/SleepDayGroup.swift @@ -34,15 +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, - palette: colors - ) + colors[keyPath: keypath] ) } diff --git a/Bedtime/Bedtime/Views/LastNightCard.swift b/Bedtime/Bedtime/Views/LastNightCard.swift index 57e215e..20fb253 100644 --- a/Bedtime/Bedtime/Views/LastNightCard.swift +++ b/Bedtime/Bedtime/Views/LastNightCard.swift @@ -22,12 +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, - palette: colors - ) + goal: goal + ) ?? \.primary + return colors[keyPath: keypath] } var body: some View { diff --git a/Bedtime/Bedtime/Views/SleepBankCard.swift b/Bedtime/Bedtime/Views/SleepBankCard.swift index c7d2805..19ebf58 100644 --- a/Bedtime/Bedtime/Views/SleepBankCard.swift +++ b/Bedtime/Bedtime/Views/SleepBankCard.swift @@ -51,7 +51,7 @@ struct SleepBankCard: View { VStack(spacing: 10) { CardHeader( icon: sleepBank.isInDebt ? "moon.zzz.fill" : "moon.stars.fill", - iconColor: sleepBank.statusColor(in: colors), + iconColor: colors[keyPath: sleepBank.statusColor], title: "Sleep Balance" ) { Text(sleepBank.statusDescription(days: sleepBankDays)) @@ -71,7 +71,7 @@ struct SleepBankCard: View { Text(TimeFormatter.formatHours(averageHours, style: durationStyle)) .font(.title2) .fontWeight(.bold) - .foregroundColor(sleepBank.statusColor(in: colors)) + .foregroundColor(colors[keyPath: sleepBank.statusColor]) } else { Text("unknown") .font(.subheadline) @@ -91,7 +91,7 @@ struct SleepBankCard: View { Text(TimeFormatter.formatHours(sleepBank.currentBalance, style: durationStyle)) .font(.title2) .fontWeight(.bold) - .foregroundColor(sleepBank.statusColor(in: colors)) + .foregroundColor(colors[keyPath: sleepBank.statusColor]) } else { Text("unknown") .font(.subheadline) From 5bfacd91305601f76334221c92342b2aa76ddbb0 Mon Sep 17 00:00:00 2001 From: Greg Date: Sat, 15 Aug 2026 20:48:50 -0700 Subject: [PATCH 07/16] restore near back --- Bedtime/Bedtime/Utils/CozyThemeColors.swift | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Bedtime/Bedtime/Utils/CozyThemeColors.swift b/Bedtime/Bedtime/Utils/CozyThemeColors.swift index 242dac5..71d86af 100644 --- a/Bedtime/Bedtime/Utils/CozyThemeColors.swift +++ b/Bedtime/Bedtime/Utils/CozyThemeColors.swift @@ -12,9 +12,7 @@ struct CozyThemeColors: ThemeColorPalette { static let cream: (Double, Double, Double) = (0.984, 0.957, 0.929) static let creamCard: (Double, Double, Double) = (0.992, 0.973, 0.953) - /// Dark warm brown, used behind cards in dark mode. - private static let darkBrown: (Double, Double, Double) = (0.165, 0.129, 0.118) - private static let darkBrownCard: (Double, Double, Double) = (0.239, 0.196, 0.173) + private static let nearBlackCard: (Double, Double, Double) = (0.110, 0.110, 0.110) /// Warm coral from the app icon (#f8987c). let accent = dynamicColor(light: (0.973, 0.596, 0.486), dark: (0.961, 0.659, 0.573)) @@ -46,7 +44,7 @@ struct CozyThemeColors: ThemeColorPalette { let sleepInBed = dynamicColor(light: (0.345, 0.278, 0.267), dark: (0.478, 0.404, 0.384)) let background = dynamicColor(light: CozyThemeColors.cream, dark: (0, 0, 0)) - let cardBackground = dynamicColor(light: CozyThemeColors.creamCard, dark: Self.creamCard) + let cardBackground = dynamicColor(light: CozyThemeColors.creamCard, dark: Self.nearBlackCard) let primary: Color = .primary let secondary: Color = .secondary From f29657d8ac6fc90b0e887fb9ee6fdb5f9d8d52e9 Mon Sep 17 00:00:00 2001 From: Greg Date: Sat, 15 Aug 2026 20:50:21 -0700 Subject: [PATCH 08/16] fixup --- Bedtime/Bedtime/Models/SleepData.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Bedtime/Bedtime/Models/SleepData.swift b/Bedtime/Bedtime/Models/SleepData.swift index d905fec..11b0363 100644 --- a/Bedtime/Bedtime/Models/SleepData.swift +++ b/Bedtime/Bedtime/Models/SleepData.swift @@ -136,7 +136,7 @@ struct SleepBank: Equatable { var statusColor: KeyPath { guard averageHours != nil else { return \.secondary } - return Constants.sleepGoalColor(difference: currentBalance) ?? \.secondary + return Constants.sleepGoalColor(difference: currentBalance) ?? \.primary } var debtHours: Double { From a237d90b46e3c556622cd057b1bff8c5d15c501d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 16 Aug 2026 03:55:13 +0000 Subject: [PATCH 09/16] Remove stale doc comment referencing deleted CozyBlackThemeColors Co-authored-by: Greg --- Bedtime/Bedtime/Utils/CozyThemeColors.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Bedtime/Bedtime/Utils/CozyThemeColors.swift b/Bedtime/Bedtime/Utils/CozyThemeColors.swift index 71d86af..2ae7737 100644 --- a/Bedtime/Bedtime/Utils/CozyThemeColors.swift +++ b/Bedtime/Bedtime/Utils/CozyThemeColors.swift @@ -8,7 +8,6 @@ import SwiftUI /// Warm palette inspired by the app icon: coral accents, cream backgrounds, dusty /// mauve/brown sleep-stage colors. struct CozyThemeColors: ThemeColorPalette { - /// Cream, shared with `CozyBlackThemeColors` in light mode. static let cream: (Double, Double, Double) = (0.984, 0.957, 0.929) static let creamCard: (Double, Double, Double) = (0.992, 0.973, 0.953) From e8433f3cb230797b3247e7f80b6928dae99a4fb1 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 16 Aug 2026 04:02:48 +0000 Subject: [PATCH 10/16] Move the cozy palette into asset-catalog colors Add one colorset per cozy color (Assets.xcassets/Cozy*.colorset) with the same light/dark RGB values that were previously inline in CozyThemeColors.swift, and switch CozyThemeColors to read the generated Color extensions (Color.cozyAccent, Color.cozyBackground, etc.) instead of building UIColor dynamic providers from hardcoded tuples. This only touches the cozy theme: SystemThemeColors intentionally mirrors the OS's own semantic colors (.blue, .secondarySystemBackground, etc.), so there's nothing there to make tunable via an asset. The generated symbols already work today because the project has ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS enabled, and the .xcodeproj uses a file-system-synchronized group, so the new colorsets don't need any project.pbxproj changes to be picked up. Removed the now-unused RGB-tuple dynamicColor(light:dark:) helper from ThemeColorPalette.swift; only the UIColor-pair overload (used by SystemThemeColors) is still needed. Net effect: tweaking the cozy palette (e.g. lightening a sleep-stage color, or adjusting how dark the near-black card is) is now a matter of picking a new color in Xcode's asset catalog editor, with live light/dark preview, rather than editing RGB tuples in code. --- .../CozyAccent.colorset/Contents.json | 38 ++++++++++++ .../CozyBackground.colorset/Contents.json | 38 ++++++++++++ .../CozyCardBackground.colorset/Contents.json | 38 ++++++++++++ .../CozyHealthKit.colorset/Contents.json | 38 ++++++++++++ .../CozyNegative.colorset/Contents.json | 38 ++++++++++++ .../CozyPositive.colorset/Contents.json | 38 ++++++++++++ .../CozyRecentSleep.colorset/Contents.json | 38 ++++++++++++ .../CozySleepAwake.colorset/Contents.json | 38 ++++++++++++ .../CozySleepCore.colorset/Contents.json | 38 ++++++++++++ .../CozySleepDeep.colorset/Contents.json | 38 ++++++++++++ .../CozySleepInBed.colorset/Contents.json | 38 ++++++++++++ .../CozyWarning.colorset/Contents.json | 38 ++++++++++++ Bedtime/Bedtime/Utils/CozyThemeColors.swift | 58 ++++++------------- Bedtime/Bedtime/Utils/ThemeColorPalette.swift | 11 ---- 14 files changed, 475 insertions(+), 50 deletions(-) create mode 100644 Bedtime/Bedtime/Assets.xcassets/CozyAccent.colorset/Contents.json create mode 100644 Bedtime/Bedtime/Assets.xcassets/CozyBackground.colorset/Contents.json create mode 100644 Bedtime/Bedtime/Assets.xcassets/CozyCardBackground.colorset/Contents.json create mode 100644 Bedtime/Bedtime/Assets.xcassets/CozyHealthKit.colorset/Contents.json create mode 100644 Bedtime/Bedtime/Assets.xcassets/CozyNegative.colorset/Contents.json create mode 100644 Bedtime/Bedtime/Assets.xcassets/CozyPositive.colorset/Contents.json create mode 100644 Bedtime/Bedtime/Assets.xcassets/CozyRecentSleep.colorset/Contents.json create mode 100644 Bedtime/Bedtime/Assets.xcassets/CozySleepAwake.colorset/Contents.json create mode 100644 Bedtime/Bedtime/Assets.xcassets/CozySleepCore.colorset/Contents.json create mode 100644 Bedtime/Bedtime/Assets.xcassets/CozySleepDeep.colorset/Contents.json create mode 100644 Bedtime/Bedtime/Assets.xcassets/CozySleepInBed.colorset/Contents.json create mode 100644 Bedtime/Bedtime/Assets.xcassets/CozyWarning.colorset/Contents.json diff --git a/Bedtime/Bedtime/Assets.xcassets/CozyAccent.colorset/Contents.json b/Bedtime/Bedtime/Assets.xcassets/CozyAccent.colorset/Contents.json new file mode 100644 index 0000000..69900bf --- /dev/null +++ b/Bedtime/Bedtime/Assets.xcassets/CozyAccent.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors": [ + { + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "blue": "0.486", + "green": "0.596", + "red": "0.973" + } + }, + "idiom": "universal" + }, + { + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ], + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "blue": "0.573", + "green": "0.659", + "red": "0.961" + } + }, + "idiom": "universal" + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} diff --git a/Bedtime/Bedtime/Assets.xcassets/CozyBackground.colorset/Contents.json b/Bedtime/Bedtime/Assets.xcassets/CozyBackground.colorset/Contents.json new file mode 100644 index 0000000..e1d7aa9 --- /dev/null +++ b/Bedtime/Bedtime/Assets.xcassets/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/CozyCardBackground.colorset/Contents.json b/Bedtime/Bedtime/Assets.xcassets/CozyCardBackground.colorset/Contents.json new file mode 100644 index 0000000..c43309f --- /dev/null +++ b/Bedtime/Bedtime/Assets.xcassets/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/CozyHealthKit.colorset/Contents.json b/Bedtime/Bedtime/Assets.xcassets/CozyHealthKit.colorset/Contents.json new file mode 100644 index 0000000..673262a --- /dev/null +++ b/Bedtime/Bedtime/Assets.xcassets/CozyHealthKit.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors": [ + { + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "blue": "0.420", + "green": "0.482", + "red": "0.788" + } + }, + "idiom": "universal" + }, + { + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ], + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "blue": "0.506", + "green": "0.573", + "red": "0.878" + } + }, + "idiom": "universal" + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} diff --git a/Bedtime/Bedtime/Assets.xcassets/CozyNegative.colorset/Contents.json b/Bedtime/Bedtime/Assets.xcassets/CozyNegative.colorset/Contents.json new file mode 100644 index 0000000..673262a --- /dev/null +++ b/Bedtime/Bedtime/Assets.xcassets/CozyNegative.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors": [ + { + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "blue": "0.420", + "green": "0.482", + "red": "0.788" + } + }, + "idiom": "universal" + }, + { + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ], + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "blue": "0.506", + "green": "0.573", + "red": "0.878" + } + }, + "idiom": "universal" + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} diff --git a/Bedtime/Bedtime/Assets.xcassets/CozyPositive.colorset/Contents.json b/Bedtime/Bedtime/Assets.xcassets/CozyPositive.colorset/Contents.json new file mode 100644 index 0000000..e2082da --- /dev/null +++ b/Bedtime/Bedtime/Assets.xcassets/CozyPositive.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors": [ + { + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "blue": "0.478", + "green": "0.671", + "red": "0.541" + } + }, + "idiom": "universal" + }, + { + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ], + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "blue": "0.557", + "green": "0.749", + "red": "0.620" + } + }, + "idiom": "universal" + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} diff --git a/Bedtime/Bedtime/Assets.xcassets/CozyRecentSleep.colorset/Contents.json b/Bedtime/Bedtime/Assets.xcassets/CozyRecentSleep.colorset/Contents.json new file mode 100644 index 0000000..5a9fdfa --- /dev/null +++ b/Bedtime/Bedtime/Assets.xcassets/CozyRecentSleep.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors": [ + { + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "blue": "0.427", + "green": "0.471", + "red": "0.608" + } + }, + "idiom": "universal" + }, + { + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ], + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "blue": "0.533", + "green": "0.588", + "red": "0.729" + } + }, + "idiom": "universal" + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} diff --git a/Bedtime/Bedtime/Assets.xcassets/CozySleepAwake.colorset/Contents.json b/Bedtime/Bedtime/Assets.xcassets/CozySleepAwake.colorset/Contents.json new file mode 100644 index 0000000..6da444d --- /dev/null +++ b/Bedtime/Bedtime/Assets.xcassets/CozySleepAwake.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors": [ + { + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "blue": "0.482", + "green": "0.592", + "red": "0.969" + } + }, + "idiom": "universal" + }, + { + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ], + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "blue": "0.573", + "green": "0.667", + "red": "0.980" + } + }, + "idiom": "universal" + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} diff --git a/Bedtime/Bedtime/Assets.xcassets/CozySleepCore.colorset/Contents.json b/Bedtime/Bedtime/Assets.xcassets/CozySleepCore.colorset/Contents.json new file mode 100644 index 0000000..57d4b1a --- /dev/null +++ b/Bedtime/Bedtime/Assets.xcassets/CozySleepCore.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors": [ + { + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "blue": "0.380", + "green": "0.345", + "red": "0.478" + } + }, + "idiom": "universal" + }, + { + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ], + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "blue": "0.490", + "green": "0.455", + "red": "0.588" + } + }, + "idiom": "universal" + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} diff --git a/Bedtime/Bedtime/Assets.xcassets/CozySleepDeep.colorset/Contents.json b/Bedtime/Bedtime/Assets.xcassets/CozySleepDeep.colorset/Contents.json new file mode 100644 index 0000000..a811bb1 --- /dev/null +++ b/Bedtime/Bedtime/Assets.xcassets/CozySleepDeep.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors": [ + { + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "blue": "0.337", + "green": "0.376", + "red": "0.510" + } + }, + "idiom": "universal" + }, + { + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ], + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "blue": "0.463", + "green": "0.502", + "red": "0.639" + } + }, + "idiom": "universal" + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} diff --git a/Bedtime/Bedtime/Assets.xcassets/CozySleepInBed.colorset/Contents.json b/Bedtime/Bedtime/Assets.xcassets/CozySleepInBed.colorset/Contents.json new file mode 100644 index 0000000..04d1172 --- /dev/null +++ b/Bedtime/Bedtime/Assets.xcassets/CozySleepInBed.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors": [ + { + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "blue": "0.267", + "green": "0.278", + "red": "0.345" + } + }, + "idiom": "universal" + }, + { + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ], + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "blue": "0.384", + "green": "0.404", + "red": "0.478" + } + }, + "idiom": "universal" + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} diff --git a/Bedtime/Bedtime/Assets.xcassets/CozyWarning.colorset/Contents.json b/Bedtime/Bedtime/Assets.xcassets/CozyWarning.colorset/Contents.json new file mode 100644 index 0000000..6ffcf08 --- /dev/null +++ b/Bedtime/Bedtime/Assets.xcassets/CozyWarning.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors": [ + { + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "blue": "0.451", + "green": "0.620", + "red": "0.910" + } + }, + "idiom": "universal" + }, + { + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ], + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "blue": "0.541", + "green": "0.698", + "red": "0.941" + } + }, + "idiom": "universal" + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} diff --git a/Bedtime/Bedtime/Utils/CozyThemeColors.swift b/Bedtime/Bedtime/Utils/CozyThemeColors.swift index 2ae7737..f3339e5 100644 --- a/Bedtime/Bedtime/Utils/CozyThemeColors.swift +++ b/Bedtime/Bedtime/Utils/CozyThemeColors.swift @@ -5,46 +5,26 @@ import SwiftUI -/// Warm palette inspired by the app icon: coral accents, cream backgrounds, dusty -/// mauve/brown sleep-stage colors. +/// Warm palette inspired by the app icon: coral accents, cream backgrounds in light mode, a +/// true-black background with near-black cards in dark mode, and dusty mauve/brown +/// sleep-stage colors. +/// +/// Every color here comes from the asset catalog (`Assets.xcassets/Cozy*.colorset`) rather +/// than inline RGB values, so the palette can be tweaked visually in Xcode's color picker +/// without touching code. struct CozyThemeColors: ThemeColorPalette { - static let cream: (Double, Double, Double) = (0.984, 0.957, 0.929) - static let creamCard: (Double, Double, Double) = (0.992, 0.973, 0.953) - - private static let nearBlackCard: (Double, Double, Double) = (0.110, 0.110, 0.110) - - /// Warm coral from the app icon (#f8987c). - let accent = dynamicColor(light: (0.973, 0.596, 0.486), dark: (0.961, 0.659, 0.573)) - - /// Dusty mauve brown from the icon (#9b786d). - let recentSleep = dynamicColor(light: (0.608, 0.471, 0.427), dark: (0.729, 0.588, 0.533)) - - /// Soft terracotta rose for health prompts. - let healthKit = dynamicColor(light: (0.788, 0.482, 0.420), dark: (0.878, 0.573, 0.506)) - - /// Muted sage green. - let positive = dynamicColor(light: (0.541, 0.671, 0.478), dark: (0.620, 0.749, 0.557)) - - /// Warm terracotta red. - let negative = dynamicColor(light: (0.788, 0.482, 0.420), dark: (0.878, 0.573, 0.506)) - - /// Soft amber peach. - let warning = dynamicColor(light: (0.910, 0.620, 0.451), dark: (0.941, 0.698, 0.541)) - - /// Deep warm brown (#826056). - let sleepDeep = dynamicColor(light: (0.510, 0.376, 0.337), dark: (0.639, 0.502, 0.463)) - - /// Plum brown for core sleep. - let sleepCore = dynamicColor(light: (0.478, 0.345, 0.380), dark: (0.588, 0.455, 0.490)) - - let sleepAwake = dynamicColor(light: (0.969, 0.592, 0.482), dark: (0.980, 0.667, 0.573)) - - /// Warm taupe (#584744). - let sleepInBed = dynamicColor(light: (0.345, 0.278, 0.267), dark: (0.478, 0.404, 0.384)) - - let background = dynamicColor(light: CozyThemeColors.cream, dark: (0, 0, 0)) - let cardBackground = dynamicColor(light: CozyThemeColors.creamCard, dark: Self.nearBlackCard) - + 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 = .primary let secondary: Color = .secondary } diff --git a/Bedtime/Bedtime/Utils/ThemeColorPalette.swift b/Bedtime/Bedtime/Utils/ThemeColorPalette.swift index 8749b03..15d5637 100644 --- a/Bedtime/Bedtime/Utils/ThemeColorPalette.swift +++ b/Bedtime/Bedtime/Utils/ThemeColorPalette.swift @@ -33,17 +33,6 @@ extension ThemeColorPalette { var sleepREM: Color { recentSleep } } -/// Builds a `Color` that resolves to `light` or `dark` RGB depending on the current -/// `UIUserInterfaceStyle`, the same way asset-catalog colors do. -func dynamicColor(light: (Double, Double, Double), dark: (Double, Double, Double)) -> Color { - Color( - uiColor: UIColor { traits in - let rgb = traits.userInterfaceStyle == .dark ? dark : light - return UIColor(red: rgb.0, green: rgb.1, blue: rgb.2, alpha: 1) - } - ) -} - /// 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 { From 11de36184cc23d6a47b2de7c345b4cb55337f0f6 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 16 Aug 2026 04:10:44 +0000 Subject: [PATCH 11/16] Use Color.primary/secondary instead of leading-dot syntax for consistency with SystemThemeColors Co-authored-by: Greg --- Bedtime/Bedtime/Utils/CozyThemeColors.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Bedtime/Bedtime/Utils/CozyThemeColors.swift b/Bedtime/Bedtime/Utils/CozyThemeColors.swift index f3339e5..3d78b42 100644 --- a/Bedtime/Bedtime/Utils/CozyThemeColors.swift +++ b/Bedtime/Bedtime/Utils/CozyThemeColors.swift @@ -25,6 +25,6 @@ struct CozyThemeColors: ThemeColorPalette { let sleepInBed = Color.cozySleepInBed let background = Color.cozyBackground let cardBackground = Color.cozyCardBackground - let primary: Color = .primary - let secondary: Color = .secondary + let primary = Color.primary + let secondary = Color.secondary } From 696bb569876bd267df8d7e98e1835420410803c6 Mon Sep 17 00:00:00 2001 From: Greg Date: Sat, 15 Aug 2026 21:16:20 -0700 Subject: [PATCH 12/16] folder --- Bedtime/Bedtime/Assets.xcassets/CozyColors/Contents.json | 6 ++++++ .../{ => CozyColors}/CozyAccent.colorset/Contents.json | 0 .../{ => CozyColors}/CozyBackground.colorset/Contents.json | 0 .../CozyCardBackground.colorset/Contents.json | 0 .../{ => CozyColors}/CozyHealthKit.colorset/Contents.json | 0 .../{ => CozyColors}/CozyNegative.colorset/Contents.json | 0 .../{ => CozyColors}/CozyPositive.colorset/Contents.json | 0 .../{ => CozyColors}/CozyRecentSleep.colorset/Contents.json | 0 .../{ => CozyColors}/CozySleepAwake.colorset/Contents.json | 0 .../{ => CozyColors}/CozySleepCore.colorset/Contents.json | 0 .../{ => CozyColors}/CozySleepDeep.colorset/Contents.json | 0 .../{ => CozyColors}/CozySleepInBed.colorset/Contents.json | 0 .../{ => CozyColors}/CozyWarning.colorset/Contents.json | 0 13 files changed, 6 insertions(+) create mode 100644 Bedtime/Bedtime/Assets.xcassets/CozyColors/Contents.json rename Bedtime/Bedtime/Assets.xcassets/{ => CozyColors}/CozyAccent.colorset/Contents.json (100%) rename Bedtime/Bedtime/Assets.xcassets/{ => CozyColors}/CozyBackground.colorset/Contents.json (100%) rename Bedtime/Bedtime/Assets.xcassets/{ => CozyColors}/CozyCardBackground.colorset/Contents.json (100%) rename Bedtime/Bedtime/Assets.xcassets/{ => CozyColors}/CozyHealthKit.colorset/Contents.json (100%) rename Bedtime/Bedtime/Assets.xcassets/{ => CozyColors}/CozyNegative.colorset/Contents.json (100%) rename Bedtime/Bedtime/Assets.xcassets/{ => CozyColors}/CozyPositive.colorset/Contents.json (100%) rename Bedtime/Bedtime/Assets.xcassets/{ => CozyColors}/CozyRecentSleep.colorset/Contents.json (100%) rename Bedtime/Bedtime/Assets.xcassets/{ => CozyColors}/CozySleepAwake.colorset/Contents.json (100%) rename Bedtime/Bedtime/Assets.xcassets/{ => CozyColors}/CozySleepCore.colorset/Contents.json (100%) rename Bedtime/Bedtime/Assets.xcassets/{ => CozyColors}/CozySleepDeep.colorset/Contents.json (100%) rename Bedtime/Bedtime/Assets.xcassets/{ => CozyColors}/CozySleepInBed.colorset/Contents.json (100%) rename Bedtime/Bedtime/Assets.xcassets/{ => CozyColors}/CozyWarning.colorset/Contents.json (100%) 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/CozyAccent.colorset/Contents.json b/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozyAccent.colorset/Contents.json similarity index 100% rename from Bedtime/Bedtime/Assets.xcassets/CozyAccent.colorset/Contents.json rename to Bedtime/Bedtime/Assets.xcassets/CozyColors/CozyAccent.colorset/Contents.json diff --git a/Bedtime/Bedtime/Assets.xcassets/CozyBackground.colorset/Contents.json b/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozyBackground.colorset/Contents.json similarity index 100% rename from Bedtime/Bedtime/Assets.xcassets/CozyBackground.colorset/Contents.json rename to Bedtime/Bedtime/Assets.xcassets/CozyColors/CozyBackground.colorset/Contents.json diff --git a/Bedtime/Bedtime/Assets.xcassets/CozyCardBackground.colorset/Contents.json b/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozyCardBackground.colorset/Contents.json similarity index 100% rename from Bedtime/Bedtime/Assets.xcassets/CozyCardBackground.colorset/Contents.json rename to Bedtime/Bedtime/Assets.xcassets/CozyColors/CozyCardBackground.colorset/Contents.json diff --git a/Bedtime/Bedtime/Assets.xcassets/CozyHealthKit.colorset/Contents.json b/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozyHealthKit.colorset/Contents.json similarity index 100% rename from Bedtime/Bedtime/Assets.xcassets/CozyHealthKit.colorset/Contents.json rename to Bedtime/Bedtime/Assets.xcassets/CozyColors/CozyHealthKit.colorset/Contents.json diff --git a/Bedtime/Bedtime/Assets.xcassets/CozyNegative.colorset/Contents.json b/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozyNegative.colorset/Contents.json similarity index 100% rename from Bedtime/Bedtime/Assets.xcassets/CozyNegative.colorset/Contents.json rename to Bedtime/Bedtime/Assets.xcassets/CozyColors/CozyNegative.colorset/Contents.json diff --git a/Bedtime/Bedtime/Assets.xcassets/CozyPositive.colorset/Contents.json b/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozyPositive.colorset/Contents.json similarity index 100% rename from Bedtime/Bedtime/Assets.xcassets/CozyPositive.colorset/Contents.json rename to Bedtime/Bedtime/Assets.xcassets/CozyColors/CozyPositive.colorset/Contents.json diff --git a/Bedtime/Bedtime/Assets.xcassets/CozyRecentSleep.colorset/Contents.json b/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozyRecentSleep.colorset/Contents.json similarity index 100% rename from Bedtime/Bedtime/Assets.xcassets/CozyRecentSleep.colorset/Contents.json rename to Bedtime/Bedtime/Assets.xcassets/CozyColors/CozyRecentSleep.colorset/Contents.json diff --git a/Bedtime/Bedtime/Assets.xcassets/CozySleepAwake.colorset/Contents.json b/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozySleepAwake.colorset/Contents.json similarity index 100% rename from Bedtime/Bedtime/Assets.xcassets/CozySleepAwake.colorset/Contents.json rename to Bedtime/Bedtime/Assets.xcassets/CozyColors/CozySleepAwake.colorset/Contents.json diff --git a/Bedtime/Bedtime/Assets.xcassets/CozySleepCore.colorset/Contents.json b/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozySleepCore.colorset/Contents.json similarity index 100% rename from Bedtime/Bedtime/Assets.xcassets/CozySleepCore.colorset/Contents.json rename to Bedtime/Bedtime/Assets.xcassets/CozyColors/CozySleepCore.colorset/Contents.json diff --git a/Bedtime/Bedtime/Assets.xcassets/CozySleepDeep.colorset/Contents.json b/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozySleepDeep.colorset/Contents.json similarity index 100% rename from Bedtime/Bedtime/Assets.xcassets/CozySleepDeep.colorset/Contents.json rename to Bedtime/Bedtime/Assets.xcassets/CozyColors/CozySleepDeep.colorset/Contents.json diff --git a/Bedtime/Bedtime/Assets.xcassets/CozySleepInBed.colorset/Contents.json b/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozySleepInBed.colorset/Contents.json similarity index 100% rename from Bedtime/Bedtime/Assets.xcassets/CozySleepInBed.colorset/Contents.json rename to Bedtime/Bedtime/Assets.xcassets/CozyColors/CozySleepInBed.colorset/Contents.json diff --git a/Bedtime/Bedtime/Assets.xcassets/CozyWarning.colorset/Contents.json b/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozyWarning.colorset/Contents.json similarity index 100% rename from Bedtime/Bedtime/Assets.xcassets/CozyWarning.colorset/Contents.json rename to Bedtime/Bedtime/Assets.xcassets/CozyColors/CozyWarning.colorset/Contents.json From 48878dcad78510785ac5d23af0c9d07b10e27c1d Mon Sep 17 00:00:00 2001 From: Greg Date: Sat, 15 Aug 2026 21:23:27 -0700 Subject: [PATCH 13/16] Clean up non-cozy colors --- .../AccentColor.colorset/Contents.json | 12 +++--- .../Contents.json | 38 ------------------- .../CardBackground.colorset/Contents.json | 38 ------------------- Bedtime/Bedtime/Utils/SystemThemeColors.swift | 2 - .../Views/BedtimeRecommendationCard.swift | 2 +- Bedtime/Bedtime/Views/LastNightCard.swift | 8 ++-- .../Views/RecentSleepSessionsCard.swift | 2 +- Bedtime/Bedtime/Views/SleepBankCard.swift | 2 +- Bedtime/Bedtime/Views/SleepInsightsCard.swift | 6 +-- 9 files changed, 16 insertions(+), 94 deletions(-) delete mode 100644 Bedtime/Bedtime/Assets.xcassets/BackgroundBehindCards.colorset/Contents.json delete mode 100644 Bedtime/Bedtime/Assets.xcassets/CardBackground.colorset/Contents.json diff --git a/Bedtime/Bedtime/Assets.xcassets/AccentColor.colorset/Contents.json b/Bedtime/Bedtime/Assets.xcassets/AccentColor.colorset/Contents.json index 7a778b5..c88e0dd 100644 --- a/Bedtime/Bedtime/Assets.xcassets/AccentColor.colorset/Contents.json +++ b/Bedtime/Bedtime/Assets.xcassets/AccentColor.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0.486", - "green" : "0.596", - "red" : "0.973" + "blue" : "1.000", + "green" : "0.000", + "red" : "1.000" } }, "idiom" : "universal" @@ -23,9 +23,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0.573", - "green" : "0.659", - "red" : "0.961" + "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 adfe964..0000000 --- a/Bedtime/Bedtime/Assets.xcassets/BackgroundBehindCards.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "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.118", - "green" : "0.129", - "red" : "0.165" - } - }, - "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 5e4763b..0000000 --- a/Bedtime/Bedtime/Assets.xcassets/CardBackground.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "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.173", - "green" : "0.196", - "red" : "0.239" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Bedtime/Bedtime/Utils/SystemThemeColors.swift b/Bedtime/Bedtime/Utils/SystemThemeColors.swift index c9adb93..f7c7c64 100644 --- a/Bedtime/Bedtime/Utils/SystemThemeColors.swift +++ b/Bedtime/Bedtime/Utils/SystemThemeColors.swift @@ -5,8 +5,6 @@ import SwiftUI -/// The original default iOS system colors, exactly as they were before the cozy palette -/// existed (see the app's git history for the extraction commit that predates it). struct SystemThemeColors: ThemeColorPalette { let accent = Color.blue let recentSleep = Color.purple diff --git a/Bedtime/Bedtime/Views/BedtimeRecommendationCard.swift b/Bedtime/Bedtime/Views/BedtimeRecommendationCard.swift index d13840f..2aa24c6 100644 --- a/Bedtime/Bedtime/Views/BedtimeRecommendationCard.swift +++ b/Bedtime/Bedtime/Views/BedtimeRecommendationCard.swift @@ -144,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/LastNightCard.swift b/Bedtime/Bedtime/Views/LastNightCard.swift index 20fb253..7b7a8bf 100644 --- a/Bedtime/Bedtime/Views/LastNightCard.swift +++ b/Bedtime/Bedtime/Views/LastNightCard.swift @@ -122,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") { @@ -151,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 43902ab..6b3b3f0 100644 --- a/Bedtime/Bedtime/Views/RecentSleepSessionsCard.swift +++ b/Bedtime/Bedtime/Views/RecentSleepSessionsCard.swift @@ -158,7 +158,7 @@ private struct RecentSleepSessionsCardPreview: View { ) .padding() } - .background(Color.backgroundBehindCards) + .background(AppTheme.cozy.colors.background) } } diff --git a/Bedtime/Bedtime/Views/SleepBankCard.swift b/Bedtime/Bedtime/Views/SleepBankCard.swift index 19ebf58..d12962e 100644 --- a/Bedtime/Bedtime/Views/SleepBankCard.swift +++ b/Bedtime/Bedtime/Views/SleepBankCard.swift @@ -209,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 e4414c9..c6c9a73 100644 --- a/Bedtime/Bedtime/Views/SleepInsightsCard.swift +++ b/Bedtime/Bedtime/Views/SleepInsightsCard.swift @@ -127,7 +127,7 @@ struct SleepInsightsCard: View { onApplyDays: { _ in } ) .padding() - .background(Color.backgroundBehindCards) + .background(AppTheme.cozy.colors.background) } #Preview("Motivator only") { @@ -155,7 +155,7 @@ struct SleepInsightsCard: View { onApplyDays: { _ in } ) .padding() - .background(Color.backgroundBehindCards) + .background(AppTheme.cozy.colors.background) } #Preview("Caught up everywhere") { @@ -190,5 +190,5 @@ struct SleepInsightsCard: View { onRaiseGoal: { _ in } ) .padding() - .background(Color.backgroundBehindCards) + .background(AppTheme.cozy.colors.background) } From 705ac3a820344fa0907436546b13311984eb9113 Mon Sep 17 00:00:00 2001 From: Greg Date: Sat, 15 Aug 2026 21:32:13 -0700 Subject: [PATCH 14/16] more differentiated cozy colors --- .../CozyAccent.colorset/Contents.json | 12 +++--- .../CozyHealthKit.colorset/Contents.json | 12 +++--- .../CozyNegative.colorset/Contents.json | 12 +++--- .../CozyPositive.colorset/Contents.json | 12 +++--- .../CozyPrimary.colorset/Contents.json | 38 +++++++++++++++++++ .../CozyRecentSleep.colorset/Contents.json | 12 +++--- .../CozySecondary.colorset/Contents.json | 38 +++++++++++++++++++ .../CozySleepAwake.colorset/Contents.json | 12 +++--- .../CozySleepCore.colorset/Contents.json | 12 +++--- .../CozySleepDeep.colorset/Contents.json | 12 +++--- .../CozySleepInBed.colorset/Contents.json | 12 +++--- .../CozyWarning.colorset/Contents.json | 12 +++--- Bedtime/Bedtime/Utils/AppTheme.swift | 4 +- Bedtime/Bedtime/Utils/CozyThemeColors.swift | 17 +++++---- 14 files changed, 147 insertions(+), 70 deletions(-) create mode 100644 Bedtime/Bedtime/Assets.xcassets/CozyColors/CozyPrimary.colorset/Contents.json create mode 100644 Bedtime/Bedtime/Assets.xcassets/CozyColors/CozySecondary.colorset/Contents.json diff --git a/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozyAccent.colorset/Contents.json b/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozyAccent.colorset/Contents.json index 69900bf..da787d0 100644 --- a/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozyAccent.colorset/Contents.json +++ b/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozyAccent.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space": "srgb", "components": { "alpha": "1.000", - "blue": "0.486", - "green": "0.596", - "red": "0.973" + "red": "0.431", + "green": "0.549", + "blue": "0.667" } }, "idiom": "universal" @@ -23,9 +23,9 @@ "color-space": "srgb", "components": { "alpha": "1.000", - "blue": "0.573", - "green": "0.659", - "red": "0.961" + "red": "0.576", + "green": "0.686", + "blue": "0.788" } }, "idiom": "universal" diff --git a/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozyHealthKit.colorset/Contents.json b/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozyHealthKit.colorset/Contents.json index 673262a..c63b335 100644 --- a/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozyHealthKit.colorset/Contents.json +++ b/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozyHealthKit.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space": "srgb", "components": { "alpha": "1.000", - "blue": "0.420", - "green": "0.482", - "red": "0.788" + "red": "0.757", + "green": "0.333", + "blue": "0.290" } }, "idiom": "universal" @@ -23,9 +23,9 @@ "color-space": "srgb", "components": { "alpha": "1.000", - "blue": "0.506", - "green": "0.573", - "red": "0.878" + "red": "0.871", + "green": "0.518", + "blue": "0.471" } }, "idiom": "universal" diff --git a/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozyNegative.colorset/Contents.json b/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozyNegative.colorset/Contents.json index 673262a..c63b335 100644 --- a/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozyNegative.colorset/Contents.json +++ b/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozyNegative.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space": "srgb", "components": { "alpha": "1.000", - "blue": "0.420", - "green": "0.482", - "red": "0.788" + "red": "0.757", + "green": "0.333", + "blue": "0.290" } }, "idiom": "universal" @@ -23,9 +23,9 @@ "color-space": "srgb", "components": { "alpha": "1.000", - "blue": "0.506", - "green": "0.573", - "red": "0.878" + "red": "0.871", + "green": "0.518", + "blue": "0.471" } }, "idiom": "universal" diff --git a/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozyPositive.colorset/Contents.json b/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozyPositive.colorset/Contents.json index e2082da..5a2f3e4 100644 --- a/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozyPositive.colorset/Contents.json +++ b/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozyPositive.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space": "srgb", "components": { "alpha": "1.000", - "blue": "0.478", - "green": "0.671", - "red": "0.541" + "red": "0.431", + "green": "0.608", + "blue": "0.369" } }, "idiom": "universal" @@ -23,9 +23,9 @@ "color-space": "srgb", "components": { "alpha": "1.000", - "blue": "0.557", - "green": "0.749", - "red": "0.620" + "red": "0.588", + "green": "0.761", + "blue": "0.533" } }, "idiom": "universal" 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 index 5a9fdfa..26c7491 100644 --- a/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozyRecentSleep.colorset/Contents.json +++ b/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozyRecentSleep.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space": "srgb", "components": { "alpha": "1.000", - "blue": "0.427", - "green": "0.471", - "red": "0.608" + "red": "0.694", + "green": "0.361", + "blue": "0.455" } }, "idiom": "universal" @@ -23,9 +23,9 @@ "color-space": "srgb", "components": { "alpha": "1.000", - "blue": "0.533", - "green": "0.588", - "red": "0.729" + "red": "0.820", + "green": "0.545", + "blue": "0.627" } }, "idiom": "universal" 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 index 6da444d..c1600a9 100644 --- a/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozySleepAwake.colorset/Contents.json +++ b/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozySleepAwake.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space": "srgb", "components": { "alpha": "1.000", - "blue": "0.482", - "green": "0.592", - "red": "0.969" + "red": "0.851", + "green": "0.482", + "blue": "0.310" } }, "idiom": "universal" @@ -23,9 +23,9 @@ "color-space": "srgb", "components": { "alpha": "1.000", - "blue": "0.573", - "green": "0.667", - "red": "0.980" + "red": "0.929", + "green": "0.627", + "blue": "0.475" } }, "idiom": "universal" diff --git a/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozySleepCore.colorset/Contents.json b/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozySleepCore.colorset/Contents.json index 57d4b1a..c383507 100644 --- a/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozySleepCore.colorset/Contents.json +++ b/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozySleepCore.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space": "srgb", "components": { "alpha": "1.000", - "blue": "0.380", - "green": "0.345", - "red": "0.478" + "red": "0.545", + "green": "0.435", + "blue": "0.361" } }, "idiom": "universal" @@ -23,9 +23,9 @@ "color-space": "srgb", "components": { "alpha": "1.000", - "blue": "0.490", - "green": "0.455", - "red": "0.588" + "red": "0.690", + "green": "0.576", + "blue": "0.494" } }, "idiom": "universal" diff --git a/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozySleepDeep.colorset/Contents.json b/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozySleepDeep.colorset/Contents.json index a811bb1..1e7a7ef 100644 --- a/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozySleepDeep.colorset/Contents.json +++ b/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozySleepDeep.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space": "srgb", "components": { "alpha": "1.000", - "blue": "0.337", - "green": "0.376", - "red": "0.510" + "red": "0.357", + "green": "0.282", + "blue": "0.439" } }, "idiom": "universal" @@ -23,9 +23,9 @@ "color-space": "srgb", "components": { "alpha": "1.000", - "blue": "0.463", - "green": "0.502", - "red": "0.639" + "red": "0.545", + "green": "0.463", + "blue": "0.639" } }, "idiom": "universal" diff --git a/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozySleepInBed.colorset/Contents.json b/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozySleepInBed.colorset/Contents.json index 04d1172..c8434ed 100644 --- a/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozySleepInBed.colorset/Contents.json +++ b/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozySleepInBed.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space": "srgb", "components": { "alpha": "1.000", - "blue": "0.267", - "green": "0.278", - "red": "0.345" + "red": "0.420", + "green": "0.365", + "blue": "0.337" } }, "idiom": "universal" @@ -23,9 +23,9 @@ "color-space": "srgb", "components": { "alpha": "1.000", - "blue": "0.384", - "green": "0.404", - "red": "0.478" + "red": "0.580", + "green": "0.522", + "blue": "0.471" } }, "idiom": "universal" diff --git a/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozyWarning.colorset/Contents.json b/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozyWarning.colorset/Contents.json index 6ffcf08..05f3017 100644 --- a/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozyWarning.colorset/Contents.json +++ b/Bedtime/Bedtime/Assets.xcassets/CozyColors/CozyWarning.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space": "srgb", "components": { "alpha": "1.000", - "blue": "0.451", - "green": "0.620", - "red": "0.910" + "red": "0.851", + "green": "0.627", + "blue": "0.231" } }, "idiom": "universal" @@ -23,9 +23,9 @@ "color-space": "srgb", "components": { "alpha": "1.000", - "blue": "0.541", - "green": "0.698", - "red": "0.941" + "red": "0.918", + "green": "0.745", + "blue": "0.447" } }, "idiom": "universal" diff --git a/Bedtime/Bedtime/Utils/AppTheme.swift b/Bedtime/Bedtime/Utils/AppTheme.swift index b99916a..2893186 100644 --- a/Bedtime/Bedtime/Utils/AppTheme.swift +++ b/Bedtime/Bedtime/Utils/AppTheme.swift @@ -10,8 +10,8 @@ import SwiftUI enum AppTheme: String, CaseIterable, Identifiable, Codable { /// The original look: default iOS system colors and backgrounds. case system - /// Warm palette inspired by the app icon: coral accents, cream backgrounds, dusty - /// mauve/brown sleep-stage colors. + /// 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 } diff --git a/Bedtime/Bedtime/Utils/CozyThemeColors.swift b/Bedtime/Bedtime/Utils/CozyThemeColors.swift index 3d78b42..1eba5e1 100644 --- a/Bedtime/Bedtime/Utils/CozyThemeColors.swift +++ b/Bedtime/Bedtime/Utils/CozyThemeColors.swift @@ -5,13 +5,14 @@ import SwiftUI -/// Warm palette inspired by the app icon: coral accents, cream backgrounds in light mode, a -/// true-black background with near-black cards in dark mode, and dusty mauve/brown -/// sleep-stage colors. +/// 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/Cozy*.colorset`) rather -/// than inline RGB values, so the palette can be tweaked visually in Xcode's color picker -/// without touching code. +/// 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 @@ -25,6 +26,6 @@ struct CozyThemeColors: ThemeColorPalette { let sleepInBed = Color.cozySleepInBed let background = Color.cozyBackground let cardBackground = Color.cozyCardBackground - let primary = Color.primary - let secondary = Color.secondary + let primary = Color.cozyPrimary + let secondary = Color.cozySecondary } From 33644989a12e00b060e77ed7e489d4a36a93f263 Mon Sep 17 00:00:00 2001 From: Greg Date: Sat, 15 Aug 2026 21:38:32 -0700 Subject: [PATCH 15/16] minor comment --- Bedtime/Bedtime/Constants.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Bedtime/Bedtime/Constants.swift b/Bedtime/Bedtime/Constants.swift index ba57ec6..cda0215 100644 --- a/Bedtime/Bedtime/Constants.swift +++ b/Bedtime/Bedtime/Constants.swift @@ -55,7 +55,7 @@ class Constants { static func sleepGoalColor(difference: Double) -> KeyPath? { if difference >= 0 { return \.positive } if difference < -sleepGoalGraceHours { return \.negative } - return nil + return nil // "grace" coloring when you're pretty close } static func sleepDurationColor(hours: Double, goal: Double) -> KeyPath? { From ec3d89c902d3f30b9fd392a9c65d08028edc0119 Mon Sep 17 00:00:00 2001 From: Greg Date: Mon, 24 Aug 2026 17:45:06 -0700 Subject: [PATCH 16/16] background --- .../xcshareddata/xcschemes/Bedtime.xcscheme | 3 +++ Bedtime/Bedtime/Views/SettingsView.swift | 10 ++++++++++ 2 files changed, 13 insertions(+) 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/Views/SettingsView.swift b/Bedtime/Bedtime/Views/SettingsView.swift index ce73ad8..86c9e81 100644 --- a/Bedtime/Bedtime/Views/SettingsView.swift +++ b/Bedtime/Bedtime/Views/SettingsView.swift @@ -46,6 +46,7 @@ struct SettingsView: View { } } } + .listRowBackground(colors.cardBackground) Section("Sleep Goal") { VStack(alignment: .leading, spacing: 8) { @@ -70,6 +71,7 @@ struct SettingsView: View { .accentColor(colors.accent) } } + .listRowBackground(colors.cardBackground) Section("Wake Time") { DatePicker( @@ -78,6 +80,7 @@ struct SettingsView: View { displayedComponents: .hourAndMinute ) } + .listRowBackground(colors.cardBackground) Section { Toggle(isOn: $preferences.useDecimalDurations) { @@ -97,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) { @@ -121,6 +125,7 @@ struct SettingsView: View { .foregroundColor(.secondary) } } + .listRowBackground(colors.cardBackground) Section("Sleep Limits") { DatePicker( @@ -133,6 +138,7 @@ struct SettingsView: View { .font(.caption) .foregroundColor(.secondary) } + .listRowBackground(colors.cardBackground) Section("Data Sources") { if let availableSources = healthKitManager.availableSources { @@ -188,6 +194,7 @@ struct SettingsView: View { .foregroundColor(.secondary) } } + .listRowBackground(colors.cardBackground) #if DEBUG Section("Developer") { @@ -233,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.