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` |
| `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 |

View File

@ -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))

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)
}
}
}

View File

@ -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)