Add settings theming guide and fix Color typealias conflicts

- Add SETTINGS_GUIDE.md with comprehensive documentation for creating branded settings screens
- Rename Color.Text to Color.TextColors to avoid conflict with SwiftUI Text view
- Rename Color.Button to Color.ButtonColors to avoid conflict with SwiftUI Button view
- Add accentColor parameter to SettingsSectionHeader for customizable section icons
- Update README and PulsingModifier to use new TextColors typealias
This commit is contained in:
Matt Bruce 2026-01-04 16:54:04 -06:00
parent c7c507c8f7
commit cc650c68d4
5 changed files with 408 additions and 7 deletions

View File

@ -67,10 +67,10 @@ Bedrock includes neutral default colors that work out of the box:
```swift
Text("Primary Text")
.foregroundStyle(Color.Text.primary)
.foregroundStyle(Color.TextColors.primary)
Text("Secondary Text")
.foregroundStyle(Color.Text.secondary)
.foregroundStyle(Color.TextColors.secondary)
VStack { }
.background(Color.Surface.primary)

View File

@ -113,18 +113,23 @@ public enum DefaultTheme: AppColorTheme {
/// These provide the familiar `Color.Surface.primary` syntax using the
/// default theme. Apps using custom themes should access colors through
/// their theme type directly (e.g., `CasinoTheme.Surface.primary`).
///
/// Note: `TextColors` and `ButtonColors` are used instead of `Text` and `Button`
/// to avoid conflicts with SwiftUI's `Text` and `Button` views.
public extension Color {
/// Default surface colors.
typealias Surface = DefaultSurfaceColors
/// Default text colors.
typealias Text = DefaultTextColors
/// Note: Named `TextColors` to avoid conflict with SwiftUI's `Text` view.
typealias TextColors = DefaultTextColors
/// Default accent colors.
typealias Accent = DefaultAccentColors
/// Default button colors.
typealias Button = DefaultButtonColors
/// Note: Named `ButtonColors` to avoid conflict with SwiftUI's `Button` view.
typealias ButtonColors = DefaultButtonColors
/// Default status colors.
typealias Status = DefaultStatusColors

View File

@ -74,7 +74,7 @@ public extension View {
.pulsing(isActive: true)
Text("Pulsing highlights interactive areas")
.foregroundStyle(Color.Text.secondary)
.foregroundStyle(Color.TextColors.secondary)
.font(.caption)
}
}

View File

@ -0,0 +1,391 @@
# Settings View Setup Guide
This guide explains how to create a branded settings screen using Bedrock's theming system and settings components.
## Overview
Bedrock provides:
1. **Color protocols** for consistent theming (`SurfaceColorProvider`, `AccentColorProvider`, etc.)
2. **Reusable settings components** (`SettingsToggle`, `SettingsCard`, `SegmentedPicker`, etc.)
3. **Design constants** for spacing, typography, and animations
By creating a custom theme, your app gets a unique visual identity while reusing all the settings UI components.
---
## Step 1: Create Your App's Theme
Create a file called `[AppName]Theme.swift` in your app's `Shared/` folder.
### Define Surface Colors
Surface colors create visual depth and separation. Use a subtle tint that matches your brand:
```swift
import SwiftUI
import Bedrock
/// Surface colors with a subtle [brand]-tint.
public enum MyAppSurfaceColors: SurfaceColorProvider {
/// Primary background - darkest level
public static let primary = Color(red: 0.08, green: 0.06, blue: 0.10)
/// Secondary/elevated surface
public static let secondary = Color(red: 0.12, green: 0.08, blue: 0.14)
/// Tertiary/card surface - most elevated
public static let tertiary = Color(red: 0.16, green: 0.11, blue: 0.18)
/// Overlay background (for sheets/modals)
public static let overlay = Color(red: 0.10, green: 0.07, blue: 0.12)
/// Card/grouped element background
public static let card = Color(red: 0.14, green: 0.10, blue: 0.16)
/// Subtle fill for grouped content sections
public static let groupedFill = Color(red: 0.12, green: 0.09, blue: 0.14)
/// Section fill for list sections
public static let sectionFill = Color(red: 0.16, green: 0.12, blue: 0.18)
}
```
### Define Accent Colors
These are your brand's primary interactive colors:
```swift
public enum MyAppAccentColors: AccentColorProvider {
/// Primary accent - your main brand color
public static let primary = Color(red: 0.85, green: 0.25, blue: 0.45)
/// Light variant
public static let light = Color(red: 0.95, green: 0.45, blue: 0.60)
/// Dark variant
public static let dark = Color(red: 0.65, green: 0.18, blue: 0.35)
/// Secondary accent for contrast
public static let secondary = Color(red: 1.0, green: 0.95, blue: 0.90)
}
```
### Define Other Color Providers
Complete the theme with text, button, status, border, and interactive colors:
```swift
public enum MyAppTextColors: TextColorProvider {
public static let primary = Color.white
public static let secondary = Color.white.opacity(Design.Opacity.accent)
public static let tertiary = Color.white.opacity(Design.Opacity.medium)
public static let disabled = Color.white.opacity(Design.Opacity.light)
public static let placeholder = Color.white.opacity(Design.Opacity.overlay)
public static let inverse = Color.black
}
public enum MyAppButtonColors: ButtonColorProvider {
public static let primaryLight = Color(red: 0.95, green: 0.40, blue: 0.55)
public static let primaryDark = Color(red: 0.75, green: 0.20, blue: 0.40)
public static let secondary = Color.white.opacity(Design.Opacity.subtle)
public static let destructive = Color.red.opacity(Design.Opacity.heavy)
public static let cancelText = Color.white.opacity(Design.Opacity.strong)
}
public enum MyAppStatusColors: StatusColorProvider {
public static let success = Color(red: 0.2, green: 0.8, blue: 0.4)
public static let warning = Color(red: 1.0, green: 0.75, blue: 0.2)
public static let error = Color(red: 0.9, green: 0.3, blue: 0.3)
public static let info = Color(red: 0.5, green: 0.7, blue: 0.95)
}
public enum MyAppBorderColors: BorderColorProvider {
public static let subtle = Color.white.opacity(Design.Opacity.subtle)
public static let standard = Color.white.opacity(Design.Opacity.hint)
public static let emphasized = Color.white.opacity(Design.Opacity.light)
public static let selected = MyAppAccentColors.primary.opacity(Design.Opacity.medium)
}
public enum MyAppInteractiveColors: InteractiveColorProvider {
public static let selected = MyAppAccentColors.primary.opacity(Design.Opacity.selection)
public static let hover = Color.white.opacity(Design.Opacity.subtle)
public static let pressed = Color.white.opacity(Design.Opacity.hint)
public static let focus = MyAppAccentColors.light
}
```
### Combine Into a Theme
```swift
public enum MyAppTheme: AppColorTheme {
public typealias Surface = MyAppSurfaceColors
public typealias Text = MyAppTextColors
public typealias Accent = MyAppAccentColors
public typealias Button = MyAppButtonColors
public typealias Status = MyAppStatusColors
public typealias Border = MyAppBorderColors
public typealias Interactive = MyAppInteractiveColors
}
```
### Add Convenience Typealiases
Create top-level typealiases with an `App` prefix to avoid conflicts with Bedrock's defaults:
```swift
/// Short typealiases for cleaner usage throughout the app.
/// These avoid conflicts with Bedrock's default typealiases by using unique names.
///
/// Usage:
/// ```swift
/// .background(AppSurface.primary)
/// .foregroundStyle(AppAccent.primary)
/// ```
typealias AppSurface = MyAppSurfaceColors
typealias AppTextColors = MyAppTextColors
typealias AppAccent = MyAppAccentColors
typealias AppButtonColors = MyAppButtonColors
typealias AppStatus = MyAppStatusColors
typealias AppBorder = MyAppBorderColors
typealias AppInteractive = MyAppInteractiveColors
```
> **Important**: Do NOT add typealiases inside `extension Color { }` as they will conflict with Bedrock's defaults and cause "ambiguous use" compiler errors. Use top-level `App`-prefixed typealiases instead.
---
## Step 2: Create a Settings Card Container
Add a reusable card container for grouping related settings:
```swift
/// A card container that provides visual grouping for settings sections.
struct SettingsCard<Content: View>: View {
@ViewBuilder let content: Content
var body: some View {
VStack(alignment: .leading, spacing: Design.Spacing.medium) {
content
}
.padding(Design.Spacing.medium)
.background(
RoundedRectangle(cornerRadius: Design.CornerRadius.large)
.fill(AppSurface.card)
)
.overlay(
RoundedRectangle(cornerRadius: Design.CornerRadius.large)
.strokeBorder(AppBorder.subtle, lineWidth: Design.LineWidth.thin)
)
}
}
```
---
## Step 3: Build Your Settings View
Use Bedrock's components with your theme colors:
```swift
import SwiftUI
import Bedrock
struct SettingsView: View {
@Bindable var viewModel: SettingsViewModel
@Environment(\.dismiss) private var dismiss
var body: some View {
NavigationStack {
ScrollView {
VStack(spacing: Design.Spacing.medium) {
// Section with header and card
SettingsSectionHeader(
title: "Appearance",
systemImage: "paintbrush",
accentColor: AppAccent.primary
)
SettingsCard {
SettingsToggle(
title: "Dark Mode",
subtitle: "Use dark appearance",
isOn: $viewModel.darkMode,
accentColor: AppAccent.primary
)
SegmentedPicker(
title: "Theme",
options: [("Light", "light"), ("Dark", "dark"), ("System", "system")],
selection: $viewModel.theme,
accentColor: AppAccent.primary
)
}
// Another section
SettingsSectionHeader(
title: "Notifications",
systemImage: "bell",
accentColor: AppAccent.primary
)
SettingsCard {
SettingsToggle(
title: "Push Notifications",
subtitle: "Receive alerts and updates",
isOn: $viewModel.pushEnabled,
accentColor: AppAccent.primary
)
}
Spacer(minLength: Design.Spacing.xxxLarge)
}
.padding(.horizontal, Design.Spacing.large)
}
.background(AppSurface.primary)
.navigationTitle("Settings")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button("Done") { dismiss() }
.foregroundStyle(AppAccent.primary)
}
}
}
}
}
```
---
## Available Components
### SettingsSectionHeader
A section header with optional icon and accent color.
```swift
SettingsSectionHeader(
title: "Account",
systemImage: "person.circle",
accentColor: Color.Accent.primary
)
```
### SettingsToggle
A toggle row with title and subtitle.
```swift
SettingsToggle(
title: "Sound Effects",
subtitle: "Play sounds for events",
isOn: $viewModel.soundEnabled,
accentColor: Color.Accent.primary
)
```
### SegmentedPicker
A horizontal capsule-style picker.
```swift
SegmentedPicker(
title: "Quality",
options: [("Low", 0), ("Medium", 1), ("High", 2)],
selection: $viewModel.quality,
accentColor: Color.Accent.primary
)
```
### SelectableRow
A card-like row for option selection.
```swift
SelectableRow(
title: "Premium",
subtitle: "Unlock all features",
isSelected: plan == .premium,
accentColor: Color.Accent.primary,
badge: { BadgePill(text: "$9.99") }
) {
plan = .premium
}
```
### VolumePicker
A slider with speaker icons for volume/percentage values.
```swift
VolumePicker(
label: "Volume",
volume: $viewModel.volume,
accentColor: Color.Accent.primary
)
```
### SettingsRow
A navigation-style row with icon and chevron.
```swift
SettingsRow(
systemImage: "star.fill",
title: "Rate App",
iconColor: Color.Status.warning
) {
openAppStore()
}
```
### BadgePill
A capsule badge for values or tags.
```swift
BadgePill(
text: "$4.99",
isSelected: isCurrentPlan,
accentColor: Color.Accent.primary
)
```
---
## Color Relationship Guide
| Surface Level | Use Case | Visual Depth |
|---------------|----------|--------------|
| `AppSurface.primary` | Main background | Darkest |
| `AppSurface.overlay` | Sheet/modal backgrounds | Slightly elevated |
| `AppSurface.card` | Settings cards | Distinct from background |
| `AppSurface.sectionFill` | Section containers | Most elevated |
| Accent Purpose | Color |
|----------------|-------|
| Interactive elements | `AppAccent.primary` |
| Highlights | `AppAccent.light` |
| Pressed states | `AppAccent.dark` |
| Pro/Premium sections | `AppStatus.warning` |
| Debug/Error sections | `AppStatus.error` |
---
## Tips
1. **Derive surface colors from your brand**: Add a subtle RGB tint (e.g., if brand is pink, surfaces should have a rose undertone).
2. **Use consistent accent colors**: Pass `accentColor: AppAccent.primary` to all Bedrock components.
3. **Group related settings**: Wrap related toggles/pickers in a `SettingsCard` for visual hierarchy.
4. **Section-specific accents**: Use `AppStatus.warning` for premium sections, `AppStatus.error` for debug.
5. **Test with Dynamic Type**: Bedrock uses `Design.BaseFontSize` values that scale properly.
6. **Avoid `Color.` typealiases**: Use `App`-prefixed typealiases to prevent conflicts with Bedrock's defaults.
---
## Example Apps
- **SelfieCam**: Rose/magenta theme with camera-focused settings
- **SelfieRingLight**: Similar structure with ring light controls
- **CameraTester**: Neutral theme for testing/debugging
See each app's `[AppName]Theme.swift` for implementation examples.

View File

@ -412,13 +412,18 @@ public struct SettingsSectionHeader: View {
/// Optional system image name.
public let systemImage: String?
/// The accent color for the header.
public let accentColor: Color
/// Creates a section header.
/// - Parameters:
/// - title: The section title.
/// - systemImage: Optional SF Symbol name.
public init(title: String, systemImage: String? = nil) {
/// - accentColor: The accent color (default: primary accent).
public init(title: String, systemImage: String? = nil, accentColor: Color = .Accent.primary) {
self.title = title
self.systemImage = systemImage
self.accentColor = accentColor
}
public var body: some View {
@ -426,7 +431,7 @@ public struct SettingsSectionHeader: View {
if let systemImage {
Image(systemName: systemImage)
.font(.system(size: Design.BaseFontSize.medium))
.foregroundStyle(.white.opacity(Design.Opacity.accent))
.foregroundStyle(accentColor.opacity(Design.Opacity.strong))
}
Text(title)