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` |
|
| `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 |
|
||||||
|
|||||||
@ -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))
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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)
|
||||||
|
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user