Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>

This commit is contained in:
Matt Bruce 2026-01-27 12:17:21 -06:00
parent a1d3105261
commit d55271e921
20 changed files with 384 additions and 62 deletions

View File

@ -76,7 +76,7 @@ struct AndromidaApp: App {
} }
} }
.modelContainer(modelContainer) .modelContainer(modelContainer)
.preferredColorScheme(.dark) .preferredColorScheme(settingsStore.theme.colorScheme)
} }
} }
} }

View File

@ -8,6 +8,7 @@ struct AppSettingsData: PersistableData {
var hapticsEnabled: Bool = true var hapticsEnabled: Bool = true
var soundEnabled: Bool = true var soundEnabled: Bool = true
var remindersEnabled: Bool = false var remindersEnabled: Bool = false
var theme: AppTheme = .dark
var lastModified: Date = .now var lastModified: Date = .now
/// Sync priority - uses haptics as a simple indicator of user activity /// Sync priority - uses haptics as a simple indicator of user activity

View File

@ -4,7 +4,7 @@ import Bedrock
@MainActor @MainActor
@Observable @Observable
final class SettingsStore: CloudSyncable { final class SettingsStore: CloudSyncable, ThemeProviding {
@ObservationIgnored private let cloudSync = CloudSyncManager<AppSettingsData>() @ObservationIgnored private let cloudSync = CloudSyncManager<AppSettingsData>()
/// Observable copy of last sync date, updated when sync completes. /// Observable copy of last sync date, updated when sync completes.
@ -26,6 +26,11 @@ final class SettingsStore: CloudSyncable {
set { update { $0.soundEnabled = newValue } } set { update { $0.soundEnabled = newValue } }
} }
var theme: AppTheme {
get { cloudSync.data.theme }
set { update { $0.theme = newValue } }
}
var iCloudAvailable: Bool { cloudSync.iCloudAvailable } var iCloudAvailable: Bool { cloudSync.iCloudAvailable }
var iCloudEnabled: Bool { var iCloudEnabled: Bool {

View File

@ -45,6 +45,11 @@ struct SettingsView: View {
) )
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) { SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
SettingsThemePicker(
store: store,
accentColor: AppAccent.primary
)
if let categoryStore { if let categoryStore {
SettingsNavigationRow( SettingsNavigationRow(
title: String(localized: "Categories"), title: String(localized: "Categories"),

View File

@ -23,9 +23,9 @@
"color-space" : "srgb", "color-space" : "srgb",
"components" : { "components" : {
"alpha" : "1.0", "alpha" : "1.0",
"blue" : "0.53", "blue" : "0.40",
"green" : "0.63", "green" : "0.55",
"red" : "0.95" "red" : "0.93"
} }
}, },
"idiom" : "universal" "idiom" : "universal"

View File

@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.0",
"blue" : "0.25",
"green" : "0.38",
"red" : "0.75"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.0",
"blue" : "0.25",
"green" : "0.38",
"red" : "0.75"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.0",
"blue" : "0.55",
"green" : "0.70",
"red" : "0.98"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.0",
"blue" : "0.55",
"green" : "0.70",
"red" : "0.98"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.0",
"blue" : "0.80",
"green" : "0.90",
"red" : "0.95"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.0",
"blue" : "0.80",
"green" : "0.90",
"red" : "0.95"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -23,9 +23,9 @@
"color-space" : "srgb", "color-space" : "srgb",
"components" : { "components" : {
"alpha" : "1.0", "alpha" : "1.0",
"blue" : "0.09", "blue" : "0.08",
"green" : "0.10", "green" : "0.09",
"red" : "0.11" "red" : "0.12"
} }
}, },
"idiom" : "universal" "idiom" : "universal"

View File

@ -23,9 +23,9 @@
"color-space" : "srgb", "color-space" : "srgb",
"components" : { "components" : {
"alpha" : "1.0", "alpha" : "1.0",
"blue" : "0.12", "blue" : "0.10",
"green" : "0.14", "green" : "0.11",
"red" : "0.15" "red" : "0.14"
} }
}, },
"idiom" : "universal" "idiom" : "universal"

View File

@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.0",
"blue" : "0.82",
"green" : "0.87",
"red" : "0.90"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.0",
"blue" : "0.12",
"green" : "0.14",
"red" : "0.18"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -23,9 +23,9 @@
"color-space" : "srgb", "color-space" : "srgb",
"components" : { "components" : {
"alpha" : "1.0", "alpha" : "1.0",
"blue" : "0.14", "blue" : "0.11",
"green" : "0.16", "green" : "0.12",
"red" : "0.17" "red" : "0.16"
} }
}, },
"idiom" : "universal" "idiom" : "universal"

View File

@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.0",
"blue" : "0.25",
"green" : "0.25",
"red" : "0.85"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.0",
"blue" : "0.35",
"green" : "0.35",
"red" : "0.90"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.0",
"blue" : "0.85",
"green" : "0.62",
"red" : "0.40"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.0",
"blue" : "0.92",
"green" : "0.72",
"red" : "0.55"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -5,9 +5,9 @@
"color-space" : "srgb", "color-space" : "srgb",
"components" : { "components" : {
"alpha" : "1.0", "alpha" : "1.0",
"blue" : "0.56", "blue" : "0.45",
"green" : "0.62", "green" : "0.65",
"red" : "0.17" "red" : "0.15"
} }
}, },
"idiom" : "universal" "idiom" : "universal"
@ -23,9 +23,9 @@
"color-space" : "srgb", "color-space" : "srgb",
"components" : { "components" : {
"alpha" : "1.0", "alpha" : "1.0",
"blue" : "0.66", "blue" : "0.55",
"green" : "0.72", "green" : "0.75",
"red" : "0.24" "red" : "0.20"
} }
}, },
"idiom" : "universal" "idiom" : "universal"

View File

@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.0",
"blue" : "0.55",
"green" : "0.60",
"red" : "0.65"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.0",
"blue" : "0.38",
"green" : "0.42",
"red" : "0.46"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -23,9 +23,9 @@
"color-space" : "srgb", "color-space" : "srgb",
"components" : { "components" : {
"alpha" : "1.0", "alpha" : "1.0",
"blue" : "0.91", "blue" : "1.0",
"green" : "0.93", "green" : "1.0",
"red" : "0.95" "red" : "1.0"
} }
}, },
"idiom" : "universal" "idiom" : "universal"

View File

@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.0",
"blue" : "0.45",
"green" : "0.50",
"red" : "0.55"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.0",
"blue" : "0.55",
"green" : "0.58",
"red" : "0.62"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -5,9 +5,9 @@
"color-space" : "srgb", "color-space" : "srgb",
"components" : { "components" : {
"alpha" : "1.0", "alpha" : "1.0",
"blue" : "0.42", "blue" : "0.35",
"green" : "0.77", "green" : "0.70",
"red" : "0.91" "red" : "0.90"
} }
}, },
"idiom" : "universal" "idiom" : "universal"
@ -23,8 +23,8 @@
"color-space" : "srgb", "color-space" : "srgb",
"components" : { "components" : {
"alpha" : "1.0", "alpha" : "1.0",
"blue" : "0.52", "blue" : "0.45",
"green" : "0.82", "green" : "0.78",
"red" : "0.95" "red" : "0.95"
} }
}, },

View File

@ -3,71 +3,78 @@ import Bedrock
// MARK: - Rituals Surface Colors // MARK: - Rituals Surface Colors
/// Surface colors using Asset Catalog with light/dark variants.
public enum RitualsSurfaceColors: SurfaceColorProvider { public enum RitualsSurfaceColors: SurfaceColorProvider {
public static let primary = Color(red: 0.12, green: 0.09, blue: 0.08) public static let primary = Color("Background")
public static let secondary = Color(red: 0.14, green: 0.11, blue: 0.10) public static let secondary = Color("BackgroundAlt")
public static let tertiary = Color(red: 0.18, green: 0.14, blue: 0.12) public static let tertiary = Color("BackgroundTertiary")
public static let overlay = Color(red: 0.12, green: 0.09, blue: 0.08) public static let overlay = Color("Background")
public static let card = Color(red: 0.16, green: 0.12, blue: 0.11) public static let card = Color("Card")
public static let groupedFill = Color(red: 0.13, green: 0.10, blue: 0.09) public static let groupedFill = Color("BackgroundAlt")
public static let sectionFill = Color(red: 0.18, green: 0.14, blue: 0.12) public static let sectionFill = Color("BackgroundTertiary")
} }
// MARK: - Rituals Text Colors // MARK: - Rituals Text Colors
/// Text colors using Asset Catalog with light/dark variants.
public enum RitualsTextColors: TextColorProvider { public enum RitualsTextColors: TextColorProvider {
public static let primary = Color.white public static let primary = Color("TextPrimary")
public static let secondary = Color.white.opacity(Design.Opacity.accent) public static let secondary = Color("TextSecondary")
public static let tertiary = Color.white.opacity(Design.Opacity.medium) public static let tertiary = Color("TextTertiary")
public static let disabled = Color.white.opacity(Design.Opacity.light) public static let disabled = Color("TextDisabled")
public static let placeholder = Color.white.opacity(Design.Opacity.overlay) public static let placeholder = Color("TextTertiary")
public static let inverse = Color.black public static let inverse = Color("Background")
} }
// MARK: - Rituals Accent Colors // MARK: - Rituals Accent Colors
/// Accent colors using Asset Catalog with light/dark variants.
public enum RitualsAccentColors: AccentColorProvider { public enum RitualsAccentColors: AccentColorProvider {
public static let primary = Color(red: 0.93, green: 0.55, blue: 0.40) public static let primary = Color("Accent")
public static let light = Color(red: 0.98, green: 0.70, blue: 0.55) public static let light = Color("AccentLight")
public static let dark = Color(red: 0.75, green: 0.38, blue: 0.25) public static let dark = Color("AccentDark")
public static let secondary = Color(red: 0.95, green: 0.90, blue: 0.80) public static let secondary = Color("AccentSecondary")
} }
// MARK: - Rituals Button Colors // MARK: - Rituals Button Colors
/// Button colors using Asset Catalog with light/dark variants.
public enum RitualsButtonColors: ButtonColorProvider { public enum RitualsButtonColors: ButtonColorProvider {
public static let primaryLight = Color(red: 0.98, green: 0.70, blue: 0.55) public static let primaryLight = Color("AccentLight")
public static let primaryDark = Color(red: 0.75, green: 0.38, blue: 0.25) public static let primaryDark = Color("AccentDark")
public static let secondary = Color.white.opacity(Design.Opacity.subtle) public static let secondary = Color("Divider")
public static let destructive = Color.red.opacity(Design.Opacity.heavy) public static let destructive = Color("Error")
public static let cancelText = Color.white.opacity(Design.Opacity.strong) public static let cancelText = Color("TextSecondary")
} }
// MARK: - Rituals Status Colors // MARK: - Rituals Status Colors
/// Status colors using Asset Catalog with light/dark variants.
public enum RitualsStatusColors: StatusColorProvider { public enum RitualsStatusColors: StatusColorProvider {
public static let success = Color(red: 0.20, green: 0.75, blue: 0.55) public static let success = Color("Success")
public static let warning = Color(red: 0.95, green: 0.78, blue: 0.45) public static let warning = Color("Warning")
public static let error = Color(red: 0.90, green: 0.35, blue: 0.35) public static let error = Color("Error")
public static let info = Color(red: 0.55, green: 0.72, blue: 0.92) public static let info = Color("Info")
} }
// MARK: - Rituals Border Colors // MARK: - Rituals Border Colors
/// Border colors using Asset Catalog with light/dark variants.
public enum RitualsBorderColors: BorderColorProvider { public enum RitualsBorderColors: BorderColorProvider {
public static let subtle = Color.white.opacity(Design.Opacity.subtle) public static let subtle = Color("Divider")
public static let standard = Color.white.opacity(Design.Opacity.hint) public static let standard = Color("Divider")
public static let emphasized = Color.white.opacity(Design.Opacity.light) public static let emphasized = Color("TextTertiary")
public static let selected = RitualsAccentColors.primary.opacity(Design.Opacity.medium) public static let selected = Color("Accent").opacity(Design.Opacity.medium)
} }
// MARK: - Rituals Interactive Colors // MARK: - Rituals Interactive Colors
/// Interactive colors using Asset Catalog with light/dark variants.
public enum RitualsInteractiveColors: InteractiveColorProvider { public enum RitualsInteractiveColors: InteractiveColorProvider {
public static let selected = RitualsAccentColors.primary.opacity(Design.Opacity.selection) public static let selected = Color("Accent").opacity(Design.Opacity.selection)
public static let hover = Color.white.opacity(Design.Opacity.subtle) public static let hover = Color("Divider")
public static let pressed = Color.white.opacity(Design.Opacity.hint) public static let pressed = Color("TextTertiary").opacity(Design.Opacity.hint)
public static let focus = RitualsAccentColors.light public static let focus = Color("AccentLight")
} }
// MARK: - Rituals Theme // MARK: - Rituals Theme