Use semantic colors for light/dark mode support in settings components
This commit is contained in:
parent
b84e32c04a
commit
9fcb698b7b
@ -868,12 +868,12 @@ SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
|
||||
|-----------|----------|----------------|
|
||||
| `SettingsCard` | Group related settings | `backgroundColor`, `borderColor` |
|
||||
| `SettingsSectionHeader` | Section titles | `title`, `systemImage`, `accentColor` |
|
||||
| `SettingsToggle` | Boolean settings | `title`, `subtitle`, `isOn`, `titleAccessory` |
|
||||
| `SettingsSlider` | Numeric settings | `value`, `range`, `format`, icons |
|
||||
| `SettingsNavigationRow` | Navigate to detail | `title`, `subtitle`, `destination` |
|
||||
| `SettingsSegmentedPicker` | Option selection | `title`, `subtitle`, `options`, `titleAccessory` |
|
||||
| `SettingsToggle` | Boolean settings | `title`, `subtitle`, `isOn`, `accentColor` |
|
||||
| `SettingsSlider` | Numeric settings | `value`, `range`, `format`, `accentColor` |
|
||||
| `SettingsNavigationRow` | Navigate to detail | `title`, `subtitle`, `backgroundColor` |
|
||||
| `SettingsSegmentedPicker` | Option selection | `title`, `subtitle`, `options`, `accentColor` |
|
||||
| `SegmentedPicker` | Low-level picker | `title`, `options`, `selection` |
|
||||
| `SettingsRow` | Action rows | `systemImage`, `title`, `action` |
|
||||
| `SettingsRow` | Action rows | `systemImage`, `title`, `iconColor` |
|
||||
| `SelectableRow` | Card selection | `title`, `isSelected`, `badge` |
|
||||
| `BadgePill` | Price/tag badges | `text`, `isSelected` |
|
||||
| `iCloudSyncSettingsView` | iCloud sync controls | `viewModel: CloudSyncable`, colors |
|
||||
@ -881,6 +881,19 @@ SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
|
||||
|
||||
---
|
||||
|
||||
## Light & Dark Mode Support
|
||||
|
||||
All settings components use SwiftUI's semantic colors (`.primary`, `.secondary`, `.tertiary`) which automatically adapt to the system appearance. No extra configuration needed—components work in both light and dark mode out of the box.
|
||||
|
||||
To force a specific appearance for your settings screen:
|
||||
|
||||
```swift
|
||||
SettingsView()
|
||||
.preferredColorScheme(.dark) // Force dark mode
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Color Relationship Guide
|
||||
|
||||
| Surface Level | Use Case | Visual Depth |
|
||||
|
||||
@ -12,12 +12,23 @@ import SwiftUI
|
||||
/// Use this for settings that navigate to a detail view.
|
||||
///
|
||||
/// ```swift
|
||||
/// // Dark theme (default)
|
||||
/// SettingsNavigationRow(
|
||||
/// title: "Open Source Licenses",
|
||||
/// subtitle: "Third-party libraries used in this app"
|
||||
/// ) {
|
||||
/// LicensesView()
|
||||
/// }
|
||||
///
|
||||
/// // Light theme
|
||||
/// SettingsNavigationRow(
|
||||
/// title: "Privacy Policy",
|
||||
/// subtitle: "How we handle your data",
|
||||
/// textColor: AppText.primary,
|
||||
/// secondaryTextColor: AppText.secondary
|
||||
/// ) {
|
||||
/// PrivacyView()
|
||||
/// }
|
||||
/// ```
|
||||
public struct SettingsNavigationRow<Destination: View>: View {
|
||||
/// The row title.
|
||||
@ -58,12 +69,12 @@ public struct SettingsNavigationRow<Destination: View>: View {
|
||||
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
|
||||
Text(title)
|
||||
.font(.system(size: Design.BaseFontSize.body, weight: .medium))
|
||||
.foregroundStyle(.white)
|
||||
.foregroundStyle(.primary)
|
||||
|
||||
if let subtitle {
|
||||
Text(subtitle)
|
||||
.font(.system(size: Design.BaseFontSize.caption))
|
||||
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
@ -71,7 +82,7 @@ public struct SettingsNavigationRow<Destination: View>: View {
|
||||
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.system(size: Design.BaseFontSize.caption))
|
||||
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.padding(Design.Spacing.medium)
|
||||
.background(backgroundColor, in: RoundedRectangle(cornerRadius: Design.CornerRadius.medium))
|
||||
|
||||
@ -28,6 +28,13 @@ public struct SettingsRow<Accessory: View>: View {
|
||||
public let action: () -> Void
|
||||
|
||||
/// Creates a settings row.
|
||||
/// - Parameters:
|
||||
/// - systemImage: SF Symbol name for the icon.
|
||||
/// - title: The row title.
|
||||
/// - value: Optional value text.
|
||||
/// - iconColor: The icon background color (default: primary accent).
|
||||
/// - accessory: Optional accessory view.
|
||||
/// - action: Action when tapped.
|
||||
public init(
|
||||
systemImage: String,
|
||||
title: String,
|
||||
@ -56,14 +63,14 @@ public struct SettingsRow<Accessory: View>: View {
|
||||
|
||||
Text(title)
|
||||
.font(.system(size: Design.BaseFontSize.medium))
|
||||
.foregroundStyle(.white)
|
||||
.foregroundStyle(.primary)
|
||||
|
||||
Spacer()
|
||||
|
||||
if let value {
|
||||
Text(value)
|
||||
.font(.system(size: Design.BaseFontSize.body))
|
||||
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
if let accessory {
|
||||
@ -71,12 +78,12 @@ public struct SettingsRow<Accessory: View>: View {
|
||||
} else {
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.system(size: Design.BaseFontSize.body, weight: .medium))
|
||||
.foregroundStyle(.white.opacity(Design.Opacity.light))
|
||||
.foregroundStyle(.tertiary)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, Design.Spacing.medium)
|
||||
.padding(.horizontal, Design.Spacing.medium)
|
||||
.background(Color.Surface.card)
|
||||
.background(Color(.secondarySystemGroupedBackground))
|
||||
.clipShape(.rect(cornerRadius: Design.CornerRadius.small))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
@ -23,7 +23,11 @@ public struct SettingsSectionHeader: View {
|
||||
/// - title: The section title.
|
||||
/// - systemImage: Optional SF Symbol name.
|
||||
/// - accentColor: The accent color (default: primary accent).
|
||||
public init(title: String, systemImage: String? = nil, accentColor: Color = .Accent.primary) {
|
||||
public init(
|
||||
title: String,
|
||||
systemImage: String? = nil,
|
||||
accentColor: Color = .Accent.primary
|
||||
) {
|
||||
self.title = title
|
||||
self.systemImage = systemImage
|
||||
self.accentColor = accentColor
|
||||
@ -39,7 +43,7 @@ public struct SettingsSectionHeader: View {
|
||||
|
||||
Text(title)
|
||||
.font(.system(size: Design.BaseFontSize.caption, weight: .semibold))
|
||||
.foregroundStyle(.white.opacity(Design.Opacity.accent))
|
||||
.foregroundStyle(.secondary)
|
||||
.textCase(.uppercase)
|
||||
.tracking(0.5)
|
||||
|
||||
|
||||
@ -81,7 +81,7 @@ public struct SettingsSegmentedPicker<T: Equatable, Accessory: View>: View {
|
||||
HStack(spacing: Design.Spacing.xSmall) {
|
||||
Text(title)
|
||||
.font(.system(size: Design.BaseFontSize.medium, weight: .medium))
|
||||
.foregroundStyle(.white)
|
||||
.foregroundStyle(.primary)
|
||||
|
||||
if let titleAccessory {
|
||||
titleAccessory
|
||||
@ -92,7 +92,7 @@ public struct SettingsSegmentedPicker<T: Equatable, Accessory: View>: View {
|
||||
// Subtitle
|
||||
Text(subtitle)
|
||||
.font(.system(size: Design.BaseFontSize.caption))
|
||||
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
// Segmented buttons
|
||||
HStack(spacing: Design.Spacing.small) {
|
||||
@ -103,12 +103,12 @@ public struct SettingsSegmentedPicker<T: Equatable, Accessory: View>: View {
|
||||
} label: {
|
||||
Text(option.0)
|
||||
.font(.system(size: Design.BaseFontSize.body, weight: .medium))
|
||||
.foregroundStyle(selection == option.1 ? .black : .white.opacity(Design.Opacity.strong))
|
||||
.foregroundStyle(selection == option.1 ? Color.white : .primary)
|
||||
.padding(.vertical, Design.Spacing.small)
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(
|
||||
Capsule()
|
||||
.fill(selection == option.1 ? accentColor : Color.white.opacity(Design.Opacity.subtle))
|
||||
.fill(selection == option.1 ? accentColor : Color.secondary.opacity(Design.Opacity.subtle))
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
@ -112,24 +112,24 @@ public struct SettingsSlider<Value: BinaryFloatingPoint & Sendable>: View where
|
||||
HStack {
|
||||
Text(title)
|
||||
.font(.system(size: Design.BaseFontSize.medium, weight: .medium))
|
||||
.foregroundStyle(.white)
|
||||
.foregroundStyle(.primary)
|
||||
|
||||
Spacer()
|
||||
|
||||
Text(format(value))
|
||||
.font(.system(size: Design.BaseFontSize.body, weight: .medium, design: .rounded))
|
||||
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
Text(subtitle)
|
||||
.font(.system(size: Design.BaseFontSize.caption))
|
||||
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
HStack(spacing: Design.Spacing.medium) {
|
||||
if let leadingIcon {
|
||||
leadingIcon
|
||||
.font(.system(size: Design.BaseFontSize.small))
|
||||
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
Slider(value: $value, in: range, step: step)
|
||||
@ -138,7 +138,7 @@ public struct SettingsSlider<Value: BinaryFloatingPoint & Sendable>: View where
|
||||
if let trailingIcon {
|
||||
trailingIcon
|
||||
.font(.system(size: Design.BaseFontSize.large))
|
||||
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,13 +12,22 @@ import SwiftUI
|
||||
/// Use this for boolean settings that can be turned on or off.
|
||||
///
|
||||
/// ```swift
|
||||
/// // Basic toggle
|
||||
/// // Basic toggle (dark theme)
|
||||
/// SettingsToggle(
|
||||
/// title: "Dark Mode",
|
||||
/// subtitle: "Use dark appearance",
|
||||
/// isOn: $settings.darkMode
|
||||
/// )
|
||||
///
|
||||
/// // Light theme toggle
|
||||
/// SettingsToggle(
|
||||
/// title: "Notifications",
|
||||
/// subtitle: "Receive push notifications",
|
||||
/// isOn: $settings.notifications,
|
||||
/// textColor: AppText.primary,
|
||||
/// secondaryTextColor: AppText.secondary
|
||||
/// )
|
||||
///
|
||||
/// // Premium toggle with crown icon
|
||||
/// SettingsToggle(
|
||||
/// title: "Flash Sync",
|
||||
@ -71,7 +80,7 @@ public struct SettingsToggle<Accessory: View>: View {
|
||||
HStack(spacing: Design.Spacing.xSmall) {
|
||||
Text(title)
|
||||
.font(.system(size: Design.BaseFontSize.medium, weight: .medium))
|
||||
.foregroundStyle(.white)
|
||||
.foregroundStyle(.primary)
|
||||
|
||||
if let titleAccessory {
|
||||
titleAccessory
|
||||
@ -81,7 +90,7 @@ public struct SettingsToggle<Accessory: View>: View {
|
||||
|
||||
Text(subtitle)
|
||||
.font(.system(size: Design.BaseFontSize.body))
|
||||
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
.tint(accentColor)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user