feat(settings): add dark/light/system appearance mode toggle

- Add AppearanceMode enum with system, light, and dark options
- Add AppearanceManager singleton to persist user preference
- Add appearance section in SettingsView with icon and description
- Apply preferredColorScheme at app root for immediate effect
- Include appearance mode in reset to defaults

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-01-14 13:08:38 -06:00
parent 3d4952e5ff
commit f7f1bbd87a
4 changed files with 116 additions and 0 deletions

View File

@@ -17,6 +17,9 @@ struct SettingsView: View {
// Subscription
subscriptionSection
// Appearance Mode (Light/Dark/System)
appearanceSection
// Theme Selection
themeSection
@@ -55,6 +58,57 @@ struct SettingsView: View {
}
}
// MARK: - Appearance Section
private var appearanceSection: some View {
Section {
ForEach(AppearanceMode.allCases) { mode in
Button {
withAnimation(.easeInOut(duration: 0.2)) {
AppearanceManager.shared.currentMode = mode
}
} label: {
HStack(spacing: 12) {
// Icon
ZStack {
Circle()
.fill(Theme.warmOrange.opacity(0.15))
.frame(width: 32, height: 32)
Image(systemName: mode.iconName)
.font(.system(size: 16))
.foregroundStyle(Theme.warmOrange)
}
VStack(alignment: .leading, spacing: 2) {
Text(mode.displayName)
.font(.body)
.foregroundStyle(.primary)
Text(mode.description)
.font(.caption)
.foregroundStyle(.secondary)
}
Spacer()
if AppearanceManager.shared.currentMode == mode {
Image(systemName: "checkmark.circle.fill")
.foregroundStyle(Theme.warmOrange)
.font(.title3)
}
}
.contentShape(Rectangle())
}
.buttonStyle(.plain)
}
} header: {
Text("Appearance")
} footer: {
Text("Choose light, dark, or follow your device settings.")
}
.listRowBackground(Theme.cardBackground(colorScheme))
}
// MARK: - Theme Section
private var themeSection: some View {