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

This commit is contained in:
Matt Bruce 2026-01-27 12:17:14 -06:00
parent 492e088cef
commit 657ca6bc5c
3 changed files with 181 additions and 0 deletions

View File

@ -37,6 +37,15 @@
"comment" : "A heading for the instructions section of the IconGeneratorView.", "comment" : "A heading for the instructions section of the IconGeneratorView.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"Appearance" : {
"comment" : "Title for the theme picker setting."
},
"Choose light, dark, or follow system" : {
"comment" : "Subtitle for the theme picker setting."
},
"Dark" : {
"comment" : "Theme option for dark mode appearance."
},
"App Icon" : { "App Icon" : {
"comment" : "A heading for the app icon preview section.", "comment" : "A heading for the app icon preview section.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
@ -73,6 +82,9 @@
"comment" : "A tab label for the launch screen preview.", "comment" : "A tab label for the launch screen preview.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"Light" : {
"comment" : "Theme option for light mode appearance."
},
"Note: iOS uses a single 1024px icon" : { "Note: iOS uses a single 1024px icon" : {
"comment" : "A note explaining that iOS uses a single 1024px icon.", "comment" : "A note explaining that iOS uses a single 1024px icon.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
@ -109,6 +121,9 @@
"comment" : "Text indicating that the user's data is up-to-date and synced.", "comment" : "Text indicating that the user's data is up-to-date and synced.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"System" : {
"comment" : "Theme option that follows the device system appearance."
},
"Syncing..." : { "Syncing..." : {
"comment" : "Text displayed in the iCloud sync status label when the initial sync is not yet complete.", "comment" : "Text displayed in the iCloud sync status label when the initial sync is not yet complete.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true

View File

@ -0,0 +1,50 @@
//
// AppTheme.swift
// Bedrock
//
// App appearance theme options.
//
import SwiftUI
/// The available appearance themes for the app.
///
/// Use with `ThemeProviding` protocol and `SettingsThemePicker` for a complete
/// theme selection UI in your settings.
///
/// ## Example
///
/// ```swift
/// // In your settings store
/// var theme: AppTheme {
/// get { cloudSync.data.theme }
/// set { update { $0.theme = newValue } }
/// }
///
/// // In your app's main scene
/// .preferredColorScheme(settingsStore.theme.colorScheme)
/// ```
public enum AppTheme: String, Codable, CaseIterable, Sendable {
case system
case light
case dark
/// The display name for the theme option.
public var displayName: String {
switch self {
case .system: String(localized: "System", bundle: .module)
case .light: String(localized: "Light", bundle: .module)
case .dark: String(localized: "Dark", bundle: .module)
}
}
/// Converts to SwiftUI's ColorScheme.
/// Returns nil for system (follows device setting).
public var colorScheme: ColorScheme? {
switch self {
case .system: nil
case .light: .light
case .dark: .dark
}
}
}

View File

@ -0,0 +1,116 @@
//
// SettingsThemePicker.swift
// Bedrock
//
// A reusable theme picker for settings screens.
//
import SwiftUI
// MARK: - ThemeProviding Protocol
/// Protocol for view models that support theme selection.
///
/// Conform your settings view model to this protocol to use `SettingsThemePicker`.
///
/// ```swift
/// @Observable @MainActor
/// final class SettingsStore: ThemeProviding {
/// var theme: AppTheme {
/// get { cloudSync.data.theme }
/// set { update { $0.theme = newValue } }
/// }
/// }
/// ```
@MainActor
public protocol ThemeProviding: AnyObject, Observable {
/// The current app theme selection.
var theme: AppTheme { get set }
}
// MARK: - Settings Theme Picker
/// A reusable theme picker for settings screens.
///
/// Use this in settings screens to provide appearance theme controls.
///
/// ```swift
/// SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
/// SettingsThemePicker(
/// store: settingsStore,
/// accentColor: AppAccent.primary
/// )
/// }
/// ```
public struct SettingsThemePicker<Store: ThemeProviding>: View {
/// The store conforming to ThemeProviding.
@Bindable public var store: Store
/// The accent color for the selected option.
public let accentColor: Color
/// The title text.
public let title: String?
/// The subtitle text.
public let subtitle: String?
/// Creates a settings theme picker.
/// - Parameters:
/// - store: The store conforming to ThemeProviding.
/// - accentColor: Accent color for selected option (default: primary accent).
/// - title: Title text (default: "Appearance").
/// - subtitle: Subtitle text (default: "Choose light, dark, or follow system").
public init(
store: Store,
accentColor: Color = .Accent.primary,
title: String? = nil,
subtitle: String? = nil
) {
self.store = store
self.accentColor = accentColor
self.title = title
self.subtitle = subtitle
}
// MARK: - Localized Defaults
private var resolvedTitle: String {
title ?? String(localized: "Appearance", bundle: .module)
}
private var resolvedSubtitle: String {
subtitle ?? String(localized: "Choose light, dark, or follow system", bundle: .module)
}
public var body: some View {
SettingsSegmentedPicker(
title: resolvedTitle,
subtitle: resolvedSubtitle,
options: AppTheme.allCases.map { ($0.displayName, $0) },
selection: $store.theme,
accentColor: accentColor
)
}
}
// MARK: - Preview
#Preview {
@Previewable @State var theme: AppTheme = .system
VStack(spacing: Design.Spacing.medium) {
Text("Theme Picker")
.font(.headline)
.foregroundStyle(.white)
SettingsSegmentedPicker(
title: "Appearance",
subtitle: "Choose light, dark, or follow system",
options: AppTheme.allCases.map { ($0.displayName, $0) },
selection: $theme
)
}
.padding()
.background(Color.Surface.overlay)
}