Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
492e088cef
commit
657ca6bc5c
@ -37,6 +37,15 @@
|
||||
"comment" : "A heading for the instructions section of the IconGeneratorView.",
|
||||
"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" : {
|
||||
"comment" : "A heading for the app icon preview section.",
|
||||
"isCommentAutoGenerated" : true
|
||||
@ -73,6 +82,9 @@
|
||||
"comment" : "A tab label for the launch screen preview.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Light" : {
|
||||
"comment" : "Theme option for light mode appearance."
|
||||
},
|
||||
"Note: iOS uses a single 1024px icon" : {
|
||||
"comment" : "A note explaining that iOS uses a single 1024px icon.",
|
||||
"isCommentAutoGenerated" : true
|
||||
@ -109,6 +121,9 @@
|
||||
"comment" : "Text indicating that the user's data is up-to-date and synced.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"System" : {
|
||||
"comment" : "Theme option that follows the device system appearance."
|
||||
},
|
||||
"Syncing..." : {
|
||||
"comment" : "Text displayed in the iCloud sync status label when the initial sync is not yet complete.",
|
||||
"isCommentAutoGenerated" : true
|
||||
|
||||
50
Sources/Bedrock/Theme/AppTheme.swift
Normal file
50
Sources/Bedrock/Theme/AppTheme.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
116
Sources/Bedrock/Views/Settings/SettingsThemePicker.swift
Normal file
116
Sources/Bedrock/Views/Settings/SettingsThemePicker.swift
Normal 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)
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user