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.",
|
"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
|
||||||
|
|||||||
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