Use semantic colors for light/dark mode support in settings components

This commit is contained in:
Matt Bruce 2026-01-10 17:39:58 -06:00
parent b84e32c04a
commit 9fcb698b7b
7 changed files with 70 additions and 26 deletions

View File

@ -868,12 +868,12 @@ SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
|-----------|----------|----------------| |-----------|----------|----------------|
| `SettingsCard` | Group related settings | `backgroundColor`, `borderColor` | | `SettingsCard` | Group related settings | `backgroundColor`, `borderColor` |
| `SettingsSectionHeader` | Section titles | `title`, `systemImage`, `accentColor` | | `SettingsSectionHeader` | Section titles | `title`, `systemImage`, `accentColor` |
| `SettingsToggle` | Boolean settings | `title`, `subtitle`, `isOn`, `titleAccessory` | | `SettingsToggle` | Boolean settings | `title`, `subtitle`, `isOn`, `accentColor` |
| `SettingsSlider` | Numeric settings | `value`, `range`, `format`, icons | | `SettingsSlider` | Numeric settings | `value`, `range`, `format`, `accentColor` |
| `SettingsNavigationRow` | Navigate to detail | `title`, `subtitle`, `destination` | | `SettingsNavigationRow` | Navigate to detail | `title`, `subtitle`, `backgroundColor` |
| `SettingsSegmentedPicker` | Option selection | `title`, `subtitle`, `options`, `titleAccessory` | | `SettingsSegmentedPicker` | Option selection | `title`, `subtitle`, `options`, `accentColor` |
| `SegmentedPicker` | Low-level picker | `title`, `options`, `selection` | | `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` | | `SelectableRow` | Card selection | `title`, `isSelected`, `badge` |
| `BadgePill` | Price/tag badges | `text`, `isSelected` | | `BadgePill` | Price/tag badges | `text`, `isSelected` |
| `iCloudSyncSettingsView` | iCloud sync controls | `viewModel: CloudSyncable`, colors | | `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 ## Color Relationship Guide
| Surface Level | Use Case | Visual Depth | | Surface Level | Use Case | Visual Depth |

View File

@ -12,12 +12,23 @@ import SwiftUI
/// Use this for settings that navigate to a detail view. /// Use this for settings that navigate to a detail view.
/// ///
/// ```swift /// ```swift
/// // Dark theme (default)
/// SettingsNavigationRow( /// SettingsNavigationRow(
/// title: "Open Source Licenses", /// title: "Open Source Licenses",
/// subtitle: "Third-party libraries used in this app" /// subtitle: "Third-party libraries used in this app"
/// ) { /// ) {
/// LicensesView() /// 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 { public struct SettingsNavigationRow<Destination: View>: View {
/// The row title. /// The row title.
@ -58,12 +69,12 @@ public struct SettingsNavigationRow<Destination: View>: View {
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) { VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
Text(title) Text(title)
.font(.system(size: Design.BaseFontSize.body, weight: .medium)) .font(.system(size: Design.BaseFontSize.body, weight: .medium))
.foregroundStyle(.white) .foregroundStyle(.primary)
if let subtitle { if let subtitle {
Text(subtitle) Text(subtitle)
.font(.system(size: Design.BaseFontSize.caption)) .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") Image(systemName: "chevron.right")
.font(.system(size: Design.BaseFontSize.caption)) .font(.system(size: Design.BaseFontSize.caption))
.foregroundStyle(.white.opacity(Design.Opacity.medium)) .foregroundStyle(.secondary)
} }
.padding(Design.Spacing.medium) .padding(Design.Spacing.medium)
.background(backgroundColor, in: RoundedRectangle(cornerRadius: Design.CornerRadius.medium)) .background(backgroundColor, in: RoundedRectangle(cornerRadius: Design.CornerRadius.medium))

View File

@ -28,6 +28,13 @@ public struct SettingsRow<Accessory: View>: View {
public let action: () -> Void public let action: () -> Void
/// Creates a settings row. /// 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( public init(
systemImage: String, systemImage: String,
title: String, title: String,
@ -56,14 +63,14 @@ public struct SettingsRow<Accessory: View>: View {
Text(title) Text(title)
.font(.system(size: Design.BaseFontSize.medium)) .font(.system(size: Design.BaseFontSize.medium))
.foregroundStyle(.white) .foregroundStyle(.primary)
Spacer() Spacer()
if let value { if let value {
Text(value) Text(value)
.font(.system(size: Design.BaseFontSize.body)) .font(.system(size: Design.BaseFontSize.body))
.foregroundStyle(.white.opacity(Design.Opacity.medium)) .foregroundStyle(.secondary)
} }
if let accessory { if let accessory {
@ -71,12 +78,12 @@ public struct SettingsRow<Accessory: View>: View {
} else { } else {
Image(systemName: "chevron.right") Image(systemName: "chevron.right")
.font(.system(size: Design.BaseFontSize.body, weight: .medium)) .font(.system(size: Design.BaseFontSize.body, weight: .medium))
.foregroundStyle(.white.opacity(Design.Opacity.light)) .foregroundStyle(.tertiary)
} }
} }
.padding(.vertical, Design.Spacing.medium) .padding(.vertical, Design.Spacing.medium)
.padding(.horizontal, Design.Spacing.medium) .padding(.horizontal, Design.Spacing.medium)
.background(Color.Surface.card) .background(Color(.secondarySystemGroupedBackground))
.clipShape(.rect(cornerRadius: Design.CornerRadius.small)) .clipShape(.rect(cornerRadius: Design.CornerRadius.small))
} }
.buttonStyle(.plain) .buttonStyle(.plain)

View File

@ -23,7 +23,11 @@ public struct SettingsSectionHeader: View {
/// - title: The section title. /// - title: The section title.
/// - systemImage: Optional SF Symbol name. /// - systemImage: Optional SF Symbol name.
/// - accentColor: The accent color (default: primary accent). /// - 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.title = title
self.systemImage = systemImage self.systemImage = systemImage
self.accentColor = accentColor self.accentColor = accentColor
@ -39,7 +43,7 @@ public struct SettingsSectionHeader: View {
Text(title) Text(title)
.font(.system(size: Design.BaseFontSize.caption, weight: .semibold)) .font(.system(size: Design.BaseFontSize.caption, weight: .semibold))
.foregroundStyle(.white.opacity(Design.Opacity.accent)) .foregroundStyle(.secondary)
.textCase(.uppercase) .textCase(.uppercase)
.tracking(0.5) .tracking(0.5)

View File

@ -81,7 +81,7 @@ public struct SettingsSegmentedPicker<T: Equatable, Accessory: View>: View {
HStack(spacing: Design.Spacing.xSmall) { HStack(spacing: Design.Spacing.xSmall) {
Text(title) Text(title)
.font(.system(size: Design.BaseFontSize.medium, weight: .medium)) .font(.system(size: Design.BaseFontSize.medium, weight: .medium))
.foregroundStyle(.white) .foregroundStyle(.primary)
if let titleAccessory { if let titleAccessory {
titleAccessory titleAccessory
@ -92,7 +92,7 @@ public struct SettingsSegmentedPicker<T: Equatable, Accessory: View>: View {
// Subtitle // Subtitle
Text(subtitle) Text(subtitle)
.font(.system(size: Design.BaseFontSize.caption)) .font(.system(size: Design.BaseFontSize.caption))
.foregroundStyle(.white.opacity(Design.Opacity.medium)) .foregroundStyle(.secondary)
// Segmented buttons // Segmented buttons
HStack(spacing: Design.Spacing.small) { HStack(spacing: Design.Spacing.small) {
@ -103,12 +103,12 @@ public struct SettingsSegmentedPicker<T: Equatable, Accessory: View>: View {
} label: { } label: {
Text(option.0) Text(option.0)
.font(.system(size: Design.BaseFontSize.body, weight: .medium)) .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) .padding(.vertical, Design.Spacing.small)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.background( .background(
Capsule() 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) .buttonStyle(.plain)

View File

@ -112,24 +112,24 @@ public struct SettingsSlider<Value: BinaryFloatingPoint & Sendable>: View where
HStack { HStack {
Text(title) Text(title)
.font(.system(size: Design.BaseFontSize.medium, weight: .medium)) .font(.system(size: Design.BaseFontSize.medium, weight: .medium))
.foregroundStyle(.white) .foregroundStyle(.primary)
Spacer() Spacer()
Text(format(value)) Text(format(value))
.font(.system(size: Design.BaseFontSize.body, weight: .medium, design: .rounded)) .font(.system(size: Design.BaseFontSize.body, weight: .medium, design: .rounded))
.foregroundStyle(.white.opacity(Design.Opacity.medium)) .foregroundStyle(.secondary)
} }
Text(subtitle) Text(subtitle)
.font(.system(size: Design.BaseFontSize.caption)) .font(.system(size: Design.BaseFontSize.caption))
.foregroundStyle(.white.opacity(Design.Opacity.medium)) .foregroundStyle(.secondary)
HStack(spacing: Design.Spacing.medium) { HStack(spacing: Design.Spacing.medium) {
if let leadingIcon { if let leadingIcon {
leadingIcon leadingIcon
.font(.system(size: Design.BaseFontSize.small)) .font(.system(size: Design.BaseFontSize.small))
.foregroundStyle(.white.opacity(Design.Opacity.medium)) .foregroundStyle(.secondary)
} }
Slider(value: $value, in: range, step: step) Slider(value: $value, in: range, step: step)
@ -138,7 +138,7 @@ public struct SettingsSlider<Value: BinaryFloatingPoint & Sendable>: View where
if let trailingIcon { if let trailingIcon {
trailingIcon trailingIcon
.font(.system(size: Design.BaseFontSize.large)) .font(.system(size: Design.BaseFontSize.large))
.foregroundStyle(.white.opacity(Design.Opacity.medium)) .foregroundStyle(.secondary)
} }
} }
} }

View File

@ -12,13 +12,22 @@ import SwiftUI
/// Use this for boolean settings that can be turned on or off. /// Use this for boolean settings that can be turned on or off.
/// ///
/// ```swift /// ```swift
/// // Basic toggle /// // Basic toggle (dark theme)
/// SettingsToggle( /// SettingsToggle(
/// title: "Dark Mode", /// title: "Dark Mode",
/// subtitle: "Use dark appearance", /// subtitle: "Use dark appearance",
/// isOn: $settings.darkMode /// 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 /// // Premium toggle with crown icon
/// SettingsToggle( /// SettingsToggle(
/// title: "Flash Sync", /// title: "Flash Sync",
@ -71,7 +80,7 @@ public struct SettingsToggle<Accessory: View>: View {
HStack(spacing: Design.Spacing.xSmall) { HStack(spacing: Design.Spacing.xSmall) {
Text(title) Text(title)
.font(.system(size: Design.BaseFontSize.medium, weight: .medium)) .font(.system(size: Design.BaseFontSize.medium, weight: .medium))
.foregroundStyle(.white) .foregroundStyle(.primary)
if let titleAccessory { if let titleAccessory {
titleAccessory titleAccessory
@ -81,7 +90,7 @@ public struct SettingsToggle<Accessory: View>: View {
Text(subtitle) Text(subtitle)
.font(.system(size: Design.BaseFontSize.body)) .font(.system(size: Design.BaseFontSize.body))
.foregroundStyle(.white.opacity(Design.Opacity.medium)) .foregroundStyle(.secondary)
} }
} }
.tint(accentColor) .tint(accentColor)