Add branded SelfieCamTheme with visual improvements to settings

- Create SelfieCamTheme.swift with rose/magenta-tinted surface colors matching app branding
- Add App-prefixed typealiases (AppSurface, AppAccent, AppStatus, etc.) to avoid Bedrock conflicts
- Add SettingsCard container for visual grouping of related settings
- Update SettingsView with card containers and accent-colored section headers
- Update all settings-related files to use new theme colors
- Pro section uses warning color, Debug section uses error color for visual distinction
This commit is contained in:
Matt Bruce 2026-01-04 16:54:23 -06:00
parent 66736d39f9
commit 255f3c2d68
9 changed files with 499 additions and 273 deletions

View File

@ -302,6 +302,61 @@ The codebase is structured for future extraction into reusable packages:
--- ---
## Known Issues / TODO
### Camera Control Button Light Press - NOT WORKING
**Status:** ❌ Broken - Needs Investigation
The Camera Control button (iPhone 16+) **full press works** for photo capture, but the **light press (secondary action) does NOT work**.
Testing revealed that the "secondary" events in logs were actually triggered by **volume button**, not Camera Control light press. The volume button works because `onCameraCaptureEvent` handles all hardware capture buttons.
#### What Works:
- ✅ Camera Control full press → triggers photo capture
- ✅ Volume up/down → triggers secondary event (focus lock)
#### What Doesn't Work:
- ❌ Camera Control light press → no event received at all
- ❌ Camera Control swipe gestures (zoom) → Apple-exclusive API
#### User Action Required - Check Accessibility Settings:
**Settings > Accessibility > Camera Control**:
- Ensure **Camera Control** is enabled
- Ensure **Light-Press** is turned ON
- Adjust **Light-Press Force** if needed
- Check **Double Light-Press Speed**
These system settings may affect third-party apps differently than Apple Camera.
#### Investigation Areas:
1. **Accessibility settings may block third-party light press**
- User reports light press works in Apple Camera but not SelfieCam
- System may require explicit light-press enablement per-app
2. **MijickCamera session configuration**
- The third-party camera framework may interfere with light press detection
- MijickCamera manages its own AVCaptureSession - may conflict
- Try testing with raw AVCaptureSession to isolate the issue
3. **`onCameraCaptureEvent` secondaryAction limitations**
- The `secondaryAction` closure receives volume button events correctly
- Camera Control light press may use different event pathway
- Apple may internally route light press to their Camera app exclusively
4. **Light press may require AVCapturePhotoOutput configuration**
- Secondary events might need specific photo output settings
- Check if `AVCapturePhotoSettings` has light-press related properties
5. **Possible Apple restriction (most likely)**
- Light press and swipe gestures appear restricted to first-party apps
- Similar to swipe-to-zoom which is Apple-exclusive
- No public API documentation confirms light press availability
---
## Future Enhancements ## Future Enhancements
Potential areas for expansion: Potential areas for expansion:
@ -312,6 +367,7 @@ Potential areas for expansion:
- [ ] Apple Watch remote trigger - [ ] Apple Watch remote trigger
- [ ] Export presets (aspect ratios, watermarks) - [ ] Export presets (aspect ratios, watermarks)
- [ ] Social sharing integrations - [ ] Social sharing integrations
- [ ] Camera Control button swipe-to-zoom (if Apple makes API public)
--- ---

View File

@ -2,7 +2,7 @@
// CaptureEventInteraction.swift // CaptureEventInteraction.swift
// SelfieCam // SelfieCam
// //
// Handles hardware capture events using AVCaptureEventInteraction (iOS 17.2+). // Handles hardware capture events using AVCaptureEventInteraction.
// Supports Camera Control button (iPhone 16+), Action button, and volume buttons. // Supports Camera Control button (iPhone 16+), Action button, and volume buttons.
// //
@ -18,18 +18,20 @@ import UIKit
/// - **Camera Control button** (iPhone 16+): Full press to capture, light press for focus/exposure /// - **Camera Control button** (iPhone 16+): Full press to capture, light press for focus/exposure
/// - **Action button** (iPhone 15 Pro+): When configured for camera /// - **Action button** (iPhone 15 Pro+): When configured for camera
/// - **Volume buttons**: Hardware shutter via system integration /// - **Volume buttons**: Hardware shutter via system integration
///
/// - Note: Requires iOS 17.2+ for `AVCaptureEventInteraction` API
@Observable @MainActor @Observable @MainActor
final class CaptureEventManager { final class CaptureEventManager {
private(set) var isActive = false private(set) var isActive = false
private(set) var isFocusLocked = false private(set) var isFocusLocked = false
// Store as Any to avoid availability issues with the type declaration private var eventInteraction: AVCaptureEventInteraction?
private var eventInteraction: AnyObject?
private var onCapture: (() -> Void)? private var onCapture: (() -> Void)?
private var onFocusLock: ((Bool) -> Void)? private var onFocusLock: ((Bool) -> Void)?
/// Pre-initialized haptic generators for immediate response
private let lightHaptic = UIImpactFeedbackGenerator(style: .light)
private let mediumHaptic = UIImpactFeedbackGenerator(style: .medium)
private let heavyHaptic = UIImpactFeedbackGenerator(style: .heavy)
/// Sets up capture event handling with the provided callbacks /// Sets up capture event handling with the provided callbacks
/// - Parameters: /// - Parameters:
/// - onCapture: Called when a capture event is triggered (full press) /// - onCapture: Called when a capture event is triggered (full press)
@ -42,19 +44,10 @@ final class CaptureEventManager {
self.onFocusLock = onFocusLock self.onFocusLock = onFocusLock
} }
/// Creates and returns the UIInteraction to be added to a view /// Creates and returns the AVCaptureEventInteraction to be added to a view
/// - Returns: The configured interaction, or nil if not available func createInteraction() -> AVCaptureEventInteraction {
func createInteraction() -> (any UIInteraction)? { Design.debugLog("📸 [CameraControl] Creating AVCaptureEventInteraction...")
guard #available(iOS 17.2, *) else {
Design.debugLog("CaptureEventInteraction: iOS 17.2+ required")
return nil
}
return createCaptureEventInteraction()
}
@available(iOS 17.2, *)
private func createCaptureEventInteraction() -> AVCaptureEventInteraction {
let interaction = AVCaptureEventInteraction( let interaction = AVCaptureEventInteraction(
primary: { [weak self] event in primary: { [weak self] event in
Task { @MainActor [weak self] in Task { @MainActor [weak self] in
@ -68,9 +61,16 @@ final class CaptureEventManager {
} }
) )
// Must explicitly enable the interaction
interaction.isEnabled = true
// Prepare haptic engines for immediate response
prepareHaptics()
self.eventInteraction = interaction self.eventInteraction = interaction
self.isActive = true self.isActive = true
Design.debugLog("CaptureEventInteraction: Hardware capture events enabled") Design.debugLog("📸 [CameraControl] ✅ Hardware capture events ENABLED")
Design.debugLog("📸 [CameraControl] Supported: Full press (capture), Light press (focus lock)")
return interaction return interaction
} }
@ -80,156 +80,82 @@ final class CaptureEventManager {
eventInteraction = nil eventInteraction = nil
isActive = false isActive = false
isFocusLocked = false isFocusLocked = false
Design.debugLog("CaptureEventInteraction: Hardware capture events disabled") Design.debugLog("📸 [CameraControl] Hardware capture events disabled")
} }
// MARK: - Event Handlers // MARK: - Event Handlers
@available(iOS 17.2, *)
private func handlePrimaryEvent(_ event: AVCaptureEvent) { private func handlePrimaryEvent(_ event: AVCaptureEvent) {
Design.debugLog("📸 [CameraControl] PRIMARY - phase: \(event.phase.rawValue)")
switch event.phase { switch event.phase {
case .began: case .began:
Design.debugLog("CaptureEventInteraction: Primary event began (press detected)") Design.debugLog("📸 [CameraControl] PRIMARY BEGAN - Full press started")
// Haptic feedback on press triggerHaptic(.medium)
triggerHaptic(.light)
case .ended: case .ended:
Design.debugLog("CaptureEventInteraction: Primary event ended (capture)") Design.debugLog("📸 [CameraControl] PRIMARY ENDED - Triggering capture!")
triggerHaptic(.heavy)
onCapture?() onCapture?()
case .cancelled: case .cancelled:
Design.debugLog("CaptureEventInteraction: Primary event cancelled") Design.debugLog("📸 [CameraControl] PRIMARY CANCELLED")
@unknown default: @unknown default:
break Design.debugLog("📸 [CameraControl] PRIMARY UNKNOWN phase: \(event.phase.rawValue)")
} }
} }
@available(iOS 17.2, *)
private func handleSecondaryEvent(_ event: AVCaptureEvent) { private func handleSecondaryEvent(_ event: AVCaptureEvent) {
Design.debugLog("🔒 [CameraControl] SECONDARY - phase: \(event.phase.rawValue)")
switch event.phase { switch event.phase {
case .began: case .began:
Design.debugLog("CaptureEventInteraction: Secondary event began (focus lock)") Design.debugLog("🔒 [CameraControl] SECONDARY BEGAN - Light press! Locking focus...")
isFocusLocked = true isFocusLocked = true
onFocusLock?(true) onFocusLock?(true)
triggerHaptic(.light) triggerHaptic(.medium)
case .ended: case .ended:
Design.debugLog("CaptureEventInteraction: Secondary event ended (focus unlock)") Design.debugLog("🔒 [CameraControl] SECONDARY ENDED - Unlocking focus")
isFocusLocked = false isFocusLocked = false
onFocusLock?(false) onFocusLock?(false)
triggerHaptic(.light)
case .cancelled: case .cancelled:
Design.debugLog("CaptureEventInteraction: Secondary event cancelled") Design.debugLog("🔒 [CameraControl] SECONDARY CANCELLED")
isFocusLocked = false isFocusLocked = false
onFocusLock?(false) onFocusLock?(false)
@unknown default: @unknown default:
break Design.debugLog("🔒 [CameraControl] SECONDARY UNKNOWN phase: \(event.phase.rawValue)")
} }
} }
// MARK: - Haptic Feedback // MARK: - Haptic Feedback
/// Prepares haptic engines for low-latency feedback
func prepareHaptics() {
lightHaptic.prepare()
mediumHaptic.prepare()
heavyHaptic.prepare()
}
private func triggerHaptic(_ style: UIImpactFeedbackGenerator.FeedbackStyle) { private func triggerHaptic(_ style: UIImpactFeedbackGenerator.FeedbackStyle) {
let generator = UIImpactFeedbackGenerator(style: style) switch style {
generator.impactOccurred() case .light:
} lightHaptic.impactOccurred()
} lightHaptic.prepare()
case .medium:
// MARK: - SwiftUI View for Hosting the Interaction mediumHaptic.impactOccurred()
mediumHaptic.prepare()
/// A SwiftUI view that hosts AVCaptureEventInteraction for Camera Control button support case .heavy:
/// heavyHaptic.impactOccurred()
/// Add this view to your camera hierarchy to enable hardware capture events. heavyHaptic.prepare()
/// The view is invisible and passes through all touches. default:
/// let generator = UIImpactFeedbackGenerator(style: style)
/// Usage: generator.impactOccurred()
/// ```swift
/// ZStack {
/// CameraPreview()
/// CaptureEventInteractionView(
/// onCapture: { performCapture() },
/// onFocusLock: { locked in handleFocusLock(locked) }
/// )
/// }
/// ```
struct CaptureEventInteractionView: UIViewRepresentable {
/// Called when primary capture event occurs (full press)
let onCapture: () -> Void
/// Called when focus/exposure lock state changes (light press)
var onFocusLock: ((Bool) -> Void)?
func makeUIView(context: Context) -> CaptureEventHostView {
let view = CaptureEventHostView()
view.backgroundColor = .clear
view.isUserInteractionEnabled = true
// Configure and add the interaction
context.coordinator.manager.configure(
onCapture: onCapture,
onFocusLock: onFocusLock
)
if let interaction = context.coordinator.manager.createInteraction() {
view.addInteraction(interaction)
context.coordinator.interaction = interaction
} }
return view
}
func updateUIView(_ uiView: CaptureEventHostView, context: Context) {
// Update callbacks if they change
context.coordinator.manager.configure(
onCapture: onCapture,
onFocusLock: onFocusLock
)
}
func makeCoordinator() -> Coordinator {
Coordinator()
}
static func dismantleUIView(_ uiView: CaptureEventHostView, coordinator: Coordinator) {
if let interaction = coordinator.interaction {
uiView.removeInteraction(interaction)
}
coordinator.manager.invalidate()
}
@MainActor
class Coordinator {
let manager = CaptureEventManager()
var interaction: (any UIInteraction)?
}
}
/// UIView subclass that hosts AVCaptureEventInteraction
///
/// This view is transparent and allows touches to pass through while
/// still receiving hardware capture events.
final class CaptureEventHostView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setup()
}
private func setup() {
backgroundColor = .clear
isUserInteractionEnabled = true
}
// Allow touches to pass through to underlying views
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
// Return nil to pass touches through, but the interaction still receives hardware events
return nil
} }
} }
@ -237,27 +163,27 @@ final class CaptureEventHostView: UIView {
extension CaptureEventManager { extension CaptureEventManager {
/// Checks if the device supports Camera Control button /// Checks if the device supports Camera Control button
/// Note: The API is always available on iOS 18+, but the physical button is iPhone 16+ only
static var isCameraControlSupported: Bool { static var isCameraControlSupported: Bool {
guard #available(iOS 17.2, *) else { return false }
// Camera Control is available on iPhone 16 series
// We check for iOS 17.2+ as the API requirement
// The actual button is hardware-dependent
return true return true
} }
/// Returns a description of supported hardware capture methods /// Returns a description of supported hardware capture methods
static var supportedHardwareMethods: String { static var supportedHardwareMethods: String {
var methods: [String] = [] return "Camera Control (iPhone 16+), Action Button, Volume Buttons"
}
if #available(iOS 17.2, *) { }
methods.append("Camera Control (iPhone 16+)")
methods.append("Action Button") // MARK: - AVCaptureEventPhase Extension
methods.append("Volume Buttons")
} else { extension AVCaptureEventPhase {
methods.append("Volume Buttons (legacy)") /// Converts AVCaptureEventPhase to our CaptureEventPhase enum
} var toCaptureEventPhase: CaptureEventPhase {
switch self {
return methods.joined(separator: ", ") case .began: return .began
case .ended: return .ended
case .cancelled: return .cancelled
@unknown default: return .cancelled
}
} }
} }

View File

@ -6,6 +6,7 @@
// //
import AVFoundation import AVFoundation
import AVKit
import SwiftUI import SwiftUI
import Bedrock import Bedrock
import MijickCamera import MijickCamera
@ -52,6 +53,18 @@ struct CustomCameraScreen: MCameraScreen {
createCameraOutputView() createCameraOutputView()
.ignoresSafeArea() .ignoresSafeArea()
.scaleEffect(x: cameraSettings.isMirrorFlipped ? -1 : 1, y: 1) // Apply horizontal mirror flip .scaleEffect(x: cameraSettings.isMirrorFlipped ? -1 : 1, y: 1) // Apply horizontal mirror flip
// Camera Control button - applied directly to camera preview for best event handling
.onCameraCaptureEvent(
isEnabled: true,
primaryAction: { event in
Design.debugLog("📸 [onCameraCaptureEvent] primaryAction received!")
handlePrimaryCaptureEvent(event)
},
secondaryAction: { event in
Design.debugLog("🔒 [onCameraCaptureEvent] secondaryAction received!")
handleSecondaryCaptureEvent(event)
}
)
// Pinch to zoom // Pinch to zoom
.gesture( .gesture(
MagnificationGesture() MagnificationGesture()
@ -167,16 +180,6 @@ struct CustomCameraScreen: MCameraScreen {
// Hidden volume view to prevent system HUD when using volume buttons as shutter // Hidden volume view to prevent system HUD when using volume buttons as shutter
HiddenVolumeView() HiddenVolumeView()
.frame(width: 0, height: 0) .frame(width: 0, height: 0)
// Camera Control button and hardware capture event support (iOS 17.2+)
CaptureEventInteractionView(
onCapture: { performCapture() },
onFocusLock: { locked in
handleFocusLock(locked)
}
)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.allowsHitTesting(false)
} }
.animation(.easeInOut(duration: 0.05), value: isShowingScreenFlash) .animation(.easeInOut(duration: 0.05), value: isShowingScreenFlash)
.animation(.easeInOut(duration: 0.3), value: isCountdownActive) .animation(.easeInOut(duration: 0.3), value: isCountdownActive)
@ -422,16 +425,69 @@ struct CustomCameraScreen: MCameraScreen {
} }
} }
// MARK: - Camera Control Button Handlers
/// Handles PRIMARY Camera Control event (full press - capture photo)
private func handlePrimaryCaptureEvent(_ event: AVCaptureEvent) {
Design.debugLog("📸 [CameraControl] PRIMARY - phase: \(event.phase.rawValue)")
switch event.phase {
case .began:
Design.debugLog("📸 [CameraControl] PRIMARY BEGAN - Press detected")
triggerHaptic(.medium)
case .ended:
Design.debugLog("📸 [CameraControl] PRIMARY ENDED - Capturing photo!")
triggerHaptic(.heavy)
performCapture()
case .cancelled:
Design.debugLog("📸 [CameraControl] PRIMARY CANCELLED")
@unknown default:
Design.debugLog("📸 [CameraControl] PRIMARY UNKNOWN phase")
}
}
/// Handles SECONDARY Camera Control event (light press - focus/exposure lock)
private func handleSecondaryCaptureEvent(_ event: AVCaptureEvent) {
Design.debugLog("🔒 [CameraControl] SECONDARY - phase: \(event.phase.rawValue)")
switch event.phase {
case .began:
Design.debugLog("🔒 [CameraControl] SECONDARY BEGAN - Light press! Locking focus...")
triggerHaptic(.medium)
handleFocusLock(true)
case .ended:
Design.debugLog("🔒 [CameraControl] SECONDARY ENDED - Unlocking focus")
triggerHaptic(.light)
handleFocusLock(false)
case .cancelled:
Design.debugLog("🔒 [CameraControl] SECONDARY CANCELLED")
handleFocusLock(false)
@unknown default:
Design.debugLog("🔒 [CameraControl] SECONDARY UNKNOWN phase")
}
}
// MARK: - Focus/Exposure Lock (Camera Control Light Press) // MARK: - Focus/Exposure Lock (Camera Control Light Press)
/// Handles focus/exposure lock from Camera Control button light press /// Handles focus/exposure lock from Camera Control button light press
/// - Parameter locked: Whether focus/exposure is being locked /// - Parameter locked: Whether focus/exposure is being locked
private func handleFocusLock(_ locked: Bool) { private func handleFocusLock(_ locked: Bool) {
Design.debugLog("🔒 [FocusLock] handleFocusLock called with locked=\(locked)")
guard let device = AVCaptureDevice.default( guard let device = AVCaptureDevice.default(
.builtInWideAngleCamera, .builtInWideAngleCamera,
for: .video, for: .video,
position: cameraPosition == .front ? .front : .back position: cameraPosition == .front ? .front : .back
) else { return } ) else {
Design.debugLog("🔒 [FocusLock] ⚠️ No capture device found!")
return
}
do { do {
try device.lockForConfiguration() try device.lockForConfiguration()
@ -440,25 +496,31 @@ struct CustomCameraScreen: MCameraScreen {
// Lock current focus and exposure // Lock current focus and exposure
if device.isFocusModeSupported(.locked) { if device.isFocusModeSupported(.locked) {
device.focusMode = .locked device.focusMode = .locked
Design.debugLog("🔒 [FocusLock] ✅ Focus LOCKED")
} else {
Design.debugLog("🔒 [FocusLock] ⚠️ Focus lock not supported")
} }
if device.isExposureModeSupported(.locked) { if device.isExposureModeSupported(.locked) {
device.exposureMode = .locked device.exposureMode = .locked
Design.debugLog("🔒 [FocusLock] ✅ Exposure LOCKED")
} else {
Design.debugLog("🔒 [FocusLock] ⚠️ Exposure lock not supported")
} }
Design.debugLog("Focus/Exposure locked via Camera Control")
} else { } else {
// Restore continuous autofocus and auto exposure // Restore continuous autofocus and auto exposure
if device.isFocusModeSupported(.continuousAutoFocus) { if device.isFocusModeSupported(.continuousAutoFocus) {
device.focusMode = .continuousAutoFocus device.focusMode = .continuousAutoFocus
Design.debugLog("🔒 [FocusLock] ✅ Focus UNLOCKED (continuous)")
} }
if device.isExposureModeSupported(.continuousAutoExposure) { if device.isExposureModeSupported(.continuousAutoExposure) {
device.exposureMode = .continuousAutoExposure device.exposureMode = .continuousAutoExposure
Design.debugLog("🔒 [FocusLock] ✅ Exposure UNLOCKED (continuous)")
} }
Design.debugLog("Focus/Exposure unlocked via Camera Control")
} }
device.unlockForConfiguration() device.unlockForConfiguration()
} catch { } catch {
Design.debugLog("Failed to set focus/exposure lock: \(error)") Design.debugLog("🔒 [FocusLock] ❌ Error: \(error)")
} }
} }

View File

@ -62,7 +62,7 @@ struct ProPaywallView: View {
} }
.padding(Design.Spacing.large) .padding(Design.Spacing.large)
} }
.background(Color.Surface.overlay) .background(AppSurface.overlay)
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbar { .toolbar {
ToolbarItem(placement: .cancellationAction) { ToolbarItem(placement: .cancellationAction) {
@ -102,11 +102,11 @@ private struct ProductPackageButton: View {
} }
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.padding(Design.Spacing.large) .padding(Design.Spacing.large)
.background(Color.Accent.primary.opacity(Design.Opacity.medium)) .background(AppAccent.primary.opacity(Design.Opacity.medium))
.clipShape(.rect(cornerRadius: Design.CornerRadius.large)) .clipShape(.rect(cornerRadius: Design.CornerRadius.large))
.overlay( .overlay(
RoundedRectangle(cornerRadius: Design.CornerRadius.large) RoundedRectangle(cornerRadius: Design.CornerRadius.large)
.strokeBorder(Color.Accent.primary, lineWidth: Design.LineWidth.thin) .strokeBorder(AppAccent.primary, lineWidth: Design.LineWidth.thin)
) )
} }
.accessibilityLabel(String(localized: "Subscribe to \(package.storeProduct.localizedTitle) for \(package.localizedPriceString)")) .accessibilityLabel(String(localized: "Subscribe to \(package.storeProduct.localizedTitle) for \(package.localizedPriceString)"))
@ -123,7 +123,7 @@ struct BenefitRow: View {
HStack(spacing: Design.Spacing.medium) { HStack(spacing: Design.Spacing.medium) {
Image(systemName: image) Image(systemName: image)
.font(.title2) .font(.title2)
.foregroundStyle(Color.Accent.primary) .foregroundStyle(AppAccent.primary)
.frame(width: Design.IconSize.xLarge) .frame(width: Design.IconSize.xLarge)
Text(text) Text(text)

View File

@ -31,7 +31,7 @@ struct ColorPresetButton: View {
.overlay( .overlay(
Circle() Circle()
.strokeBorder( .strokeBorder(
isSelected ? Color.Accent.primary : Color.Border.subtle, isSelected ? AppAccent.primary : AppBorder.subtle,
lineWidth: isSelected ? Design.LineWidth.thick : Design.LineWidth.thin lineWidth: isSelected ? Design.LineWidth.thick : Design.LineWidth.thin
) )
) )
@ -61,13 +61,13 @@ struct ColorPresetButton: View {
if preset.isPremium { if preset.isPremium {
Image(systemName: isPremiumUnlocked ? "crown.fill" : "crown") Image(systemName: isPremiumUnlocked ? "crown.fill" : "crown")
.font(.system(size: Design.BaseFontSize.xxSmall)) .font(.system(size: Design.BaseFontSize.xxSmall))
.foregroundStyle(isPremiumUnlocked ? Color.Status.warning : Color.Status.warning.opacity(Design.Opacity.medium)) .foregroundStyle(isPremiumUnlocked ? AppStatus.warning : AppStatus.warning.opacity(Design.Opacity.medium))
} }
} }
.padding(Design.Spacing.xSmall) .padding(Design.Spacing.xSmall)
.background( .background(
RoundedRectangle(cornerRadius: Design.CornerRadius.small) RoundedRectangle(cornerRadius: Design.CornerRadius.small)
.fill(isSelected ? Color.Accent.primary.opacity(Design.Opacity.subtle) : Color.clear) .fill(isSelected ? AppAccent.primary.opacity(Design.Opacity.subtle) : Color.clear)
) )
} }
.buttonStyle(.plain) .buttonStyle(.plain)

View File

@ -36,7 +36,7 @@ struct CustomColorPickerButton: View {
.overlay( .overlay(
Circle() Circle()
.strokeBorder( .strokeBorder(
isSelected ? Color.Accent.primary : Color.Border.subtle, isSelected ? AppAccent.primary : AppBorder.subtle,
lineWidth: isSelected ? Design.LineWidth.thick : Design.LineWidth.thin lineWidth: isSelected ? Design.LineWidth.thick : Design.LineWidth.thin
) )
) )
@ -53,12 +53,12 @@ struct CustomColorPickerButton: View {
Image(systemName: "crown.fill") Image(systemName: "crown.fill")
.font(.system(size: Design.BaseFontSize.xxSmall)) .font(.system(size: Design.BaseFontSize.xxSmall))
.foregroundStyle(Color.Status.warning) .foregroundStyle(AppStatus.warning)
} }
.padding(Design.Spacing.xSmall) .padding(Design.Spacing.xSmall)
.background( .background(
RoundedRectangle(cornerRadius: Design.CornerRadius.small) RoundedRectangle(cornerRadius: Design.CornerRadius.small)
.fill(isSelected ? Color.Accent.primary.opacity(Design.Opacity.subtle) : Color.clear) .fill(isSelected ? AppAccent.primary.opacity(Design.Opacity.subtle) : Color.clear)
) )
.accessibilityLabel(String(localized: "Custom color")) .accessibilityLabel(String(localized: "Custom color"))
.accessibilityAddTraits(isSelected ? .isSelected : []) .accessibilityAddTraits(isSelected ? .isSelected : [])
@ -78,7 +78,7 @@ struct CustomColorPickerButton: View {
.frame(width: Design.Size.avatarSmall, height: Design.Size.avatarSmall) .frame(width: Design.Size.avatarSmall, height: Design.Size.avatarSmall)
.overlay( .overlay(
Circle() Circle()
.strokeBorder(Color.Border.subtle, lineWidth: Design.LineWidth.thin) .strokeBorder(AppBorder.subtle, lineWidth: Design.LineWidth.thin)
) )
// Lock overlay // Lock overlay
@ -99,7 +99,7 @@ struct CustomColorPickerButton: View {
Image(systemName: "crown") Image(systemName: "crown")
.font(.system(size: Design.BaseFontSize.xxSmall)) .font(.system(size: Design.BaseFontSize.xxSmall))
.foregroundStyle(Color.Status.warning.opacity(Design.Opacity.medium)) .foregroundStyle(AppStatus.warning.opacity(Design.Opacity.medium))
} }
.padding(Design.Spacing.xSmall) .padding(Design.Spacing.xSmall)
} }

View File

@ -32,7 +32,7 @@ struct LicensesView: View {
} }
.padding(Design.Spacing.large) .padding(Design.Spacing.large)
} }
.background(Color.Surface.overlay) .background(AppSurface.overlay)
.navigationTitle(String(localized: "Open Source Licenses")) .navigationTitle(String(localized: "Open Source Licenses"))
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
} }
@ -50,7 +50,7 @@ struct LicensesView: View {
HStack { HStack {
Label(license, systemImage: "doc.text") Label(license, systemImage: "doc.text")
.font(.system(size: Design.BaseFontSize.xSmall)) .font(.system(size: Design.BaseFontSize.xSmall))
.foregroundStyle(Color.Accent.primary) .foregroundStyle(AppAccent.primary)
Spacer() Spacer()
@ -58,13 +58,13 @@ struct LicensesView: View {
Link(destination: linkURL) { Link(destination: linkURL) {
Label(String(localized: "View on GitHub"), systemImage: "arrow.up.right.square") Label(String(localized: "View on GitHub"), systemImage: "arrow.up.right.square")
.font(.system(size: Design.BaseFontSize.xSmall)) .font(.system(size: Design.BaseFontSize.xSmall))
.foregroundStyle(Color.Accent.primary) .foregroundStyle(AppAccent.primary)
} }
} }
} }
.padding(.top, Design.Spacing.xSmall) .padding(.top, Design.Spacing.xSmall)
} }
.padding(Design.Spacing.medium) .padding(Design.Spacing.medium)
.background(Color.Surface.primary, in: RoundedRectangle(cornerRadius: Design.CornerRadius.medium)) .background(AppSurface.primary, in: RoundedRectangle(cornerRadius: Design.CornerRadius.medium))
} }
} }

View File

@ -28,124 +28,139 @@ struct SettingsView: View {
// MARK: - Ring Light Section // MARK: - Ring Light Section
SettingsSectionHeader(title: "Ring Light", systemImage: "light.max") SettingsSectionHeader(title: "Ring Light", systemImage: "light.max", accentColor: AppAccent.primary)
// Ring Light Enabled SettingsCard {
SettingsToggle( // Ring Light Enabled
title: String(localized: "Enable Ring Light"), SettingsToggle(
subtitle: String(localized: "Show colored light ring around camera preview"), title: String(localized: "Enable Ring Light"),
isOn: $viewModel.isRingLightEnabled subtitle: String(localized: "Show colored light ring around camera preview"),
) isOn: $viewModel.isRingLightEnabled,
.accessibilityHint(String(localized: "Enables or disables the ring light overlay")) accentColor: AppAccent.primary
)
.accessibilityHint(String(localized: "Enables or disables the ring light overlay"))
// Ring Size Slider // Ring Size Slider
ringSizeSlider ringSizeSlider
// Color Preset // Color Preset
colorPresetSection colorPresetSection
// Ring Light Brightness // Ring Light Brightness
ringLightBrightnessSlider ringLightBrightnessSlider
}
// MARK: - Camera Controls Section // MARK: - Camera Controls Section
SettingsSectionHeader(title: "Camera Controls", systemImage: "camera") SettingsSectionHeader(title: "Camera Controls", systemImage: "camera", accentColor: AppAccent.primary)
// Camera Position SettingsCard {
cameraPositionPicker // Camera Position
cameraPositionPicker
// Flash Mode // Flash Mode
flashModePicker flashModePicker
// Flash Sync (premium) // Flash Sync (premium)
premiumToggle( premiumToggle(
title: String(localized: "Flash Sync"), title: String(localized: "Flash Sync"),
subtitle: String(localized: "Use ring light color for screen flash"), subtitle: String(localized: "Use ring light color for screen flash"),
isOn: $viewModel.isFlashSyncedWithRingLight, isOn: $viewModel.isFlashSyncedWithRingLight,
accessibilityHint: String(localized: "Syncs flash color with ring light color") accessibilityHint: String(localized: "Syncs flash color with ring light color")
) )
// HDR Mode // HDR Mode
hdrModePicker hdrModePicker
// Center Stage (premium feature) // Center Stage (premium feature)
centerStageToggle centerStageToggle
// Photo Quality // Photo Quality
photoQualityPicker photoQualityPicker
}
// MARK: - Display Section // MARK: - Display Section
SettingsSectionHeader(title: "Display", systemImage: "eye") SettingsSectionHeader(title: "Display", systemImage: "eye", accentColor: AppAccent.primary)
// True Mirror (premium) SettingsCard {
premiumToggle( // True Mirror (premium)
title: String(localized: "True Mirror"), premiumToggle(
subtitle: String(localized: "Shows horizontally flipped preview like a real mirror"), title: String(localized: "True Mirror"),
isOn: $viewModel.isMirrorFlipped, subtitle: String(localized: "Shows horizontally flipped preview like a real mirror"),
accessibilityHint: String(localized: "Flips the camera preview horizontally") isOn: $viewModel.isMirrorFlipped,
) accessibilityHint: String(localized: "Flips the camera preview horizontally")
)
SettingsToggle( SettingsToggle(
title: String(localized: "Grid Overlay"), title: String(localized: "Grid Overlay"),
subtitle: String(localized: "Shows rule of thirds grid for composition"), subtitle: String(localized: "Shows rule of thirds grid for composition"),
isOn: $viewModel.isGridVisible isOn: $viewModel.isGridVisible,
) accentColor: AppAccent.primary
.accessibilityHint(String(localized: "Shows a grid overlay to help compose your shot")) )
.accessibilityHint(String(localized: "Shows a grid overlay to help compose your shot"))
// Skin Smoothing (premium) // Skin Smoothing (premium)
premiumToggle( premiumToggle(
title: String(localized: "Skin Smoothing"), title: String(localized: "Skin Smoothing"),
subtitle: String(localized: "Applies subtle real-time skin smoothing"), subtitle: String(localized: "Applies subtle real-time skin smoothing"),
isOn: $viewModel.isSkinSmoothingEnabled, isOn: $viewModel.isSkinSmoothingEnabled,
accessibilityHint: String(localized: "Applies light skin smoothing to the camera preview") accessibilityHint: String(localized: "Applies light skin smoothing to the camera preview")
) )
}
// MARK: - Capture Section // MARK: - Capture Section
SettingsSectionHeader(title: "Capture", systemImage: "photo.on.rectangle") SettingsSectionHeader(title: "Capture", systemImage: "photo.on.rectangle", accentColor: AppAccent.primary)
// Timer Selection SettingsCard {
timerPicker // Timer Selection
timerPicker
SettingsToggle( SettingsToggle(
title: String(localized: "Auto-Save"), title: String(localized: "Auto-Save"),
subtitle: String(localized: "Automatically save captures to Photo Library"), subtitle: String(localized: "Automatically save captures to Photo Library"),
isOn: $viewModel.isAutoSaveEnabled isOn: $viewModel.isAutoSaveEnabled,
) accentColor: AppAccent.primary
.accessibilityHint(String(localized: "When enabled, photos and videos are saved immediately after capture")) )
.accessibilityHint(String(localized: "When enabled, photos and videos are saved immediately after capture"))
}
// MARK: - Pro Section // MARK: - Pro Section
SettingsSectionHeader(title: "Pro", systemImage: "crown") SettingsSectionHeader(title: "Pro", systemImage: "crown", accentColor: AppStatus.warning)
proSection proSection
// MARK: - Sync Section // MARK: - Sync Section
SettingsSectionHeader(title: String(localized: "iCloud Sync"), systemImage: "icloud") SettingsSectionHeader(title: String(localized: "iCloud Sync"), systemImage: "icloud", accentColor: AppAccent.primary)
iCloudSyncSection SettingsCard {
iCloudSyncSection
}
// MARK: - About Section // MARK: - About Section
SettingsSectionHeader(title: "About", systemImage: "info.circle") SettingsSectionHeader(title: "About", systemImage: "info.circle", accentColor: AppAccent.primary)
acknowledgmentsSection acknowledgmentsSection
// MARK: - Debug Section // MARK: - Debug Section
#if DEBUG #if DEBUG
SettingsSectionHeader(title: "Debug", systemImage: "ant.fill") SettingsSectionHeader(title: "Debug", systemImage: "ant.fill", accentColor: AppStatus.error)
brandingDebugSection SettingsCard {
brandingDebugSection
}
#endif #endif
Spacer(minLength: Design.Spacing.xxxLarge) Spacer(minLength: Design.Spacing.xxxLarge)
} }
.padding(.horizontal, Design.Spacing.large) .padding(.horizontal, Design.Spacing.large)
} }
.background(Color.Surface.overlay) .background(AppSurface.primary)
.navigationTitle(String(localized: "Settings")) .navigationTitle(String(localized: "Settings"))
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbar { .toolbar {
@ -153,7 +168,7 @@ struct SettingsView: View {
Button(String(localized: "Done")) { Button(String(localized: "Done")) {
dismiss() dismiss()
} }
.foregroundStyle(Color.Accent.primary) .foregroundStyle(AppAccent.primary)
} }
} }
} }
@ -186,7 +201,7 @@ struct SettingsView: View {
in: SettingsViewModel.minRingSize...SettingsViewModel.maxRingSize, in: SettingsViewModel.minRingSize...SettingsViewModel.maxRingSize,
step: 5 step: 5
) )
.tint(Color.Accent.primary) .tint(AppAccent.primary)
// Large ring icon // Large ring icon
Image(systemName: "circle") Image(systemName: "circle")
@ -286,7 +301,7 @@ struct SettingsView: View {
Image(systemName: "crown.fill") Image(systemName: "crown.fill")
.font(.system(size: Design.BaseFontSize.small)) .font(.system(size: Design.BaseFontSize.small))
.foregroundStyle(Color.Status.warning) .foregroundStyle(AppStatus.warning)
} }
Text(String(localized: "High Dynamic Range for better lighting in photos")) Text(String(localized: "High Dynamic Range for better lighting in photos"))
@ -319,7 +334,7 @@ struct SettingsView: View {
Image(systemName: "crown.fill") Image(systemName: "crown.fill")
.font(.system(size: Design.BaseFontSize.small)) .font(.system(size: Design.BaseFontSize.small))
.foregroundStyle(Color.Status.warning) .foregroundStyle(AppStatus.warning)
} }
Text(isPremiumUnlocked Text(isPremiumUnlocked
@ -354,7 +369,7 @@ struct SettingsView: View {
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.background( .background(
Capsule() Capsule()
.fill(viewModel.photoQuality == quality ? Color.Accent.primary : Color.white.opacity(Design.Opacity.subtle)) .fill(viewModel.photoQuality == quality ? AppAccent.primary : Color.white.opacity(Design.Opacity.subtle))
) )
} }
.buttonStyle(.plain) .buttonStyle(.plain)
@ -415,7 +430,7 @@ struct SettingsView: View {
in: 0.1...1.0, in: 0.1...1.0,
step: 0.05 step: 0.05
) )
.tint(Color.Accent.primary) .tint(AppAccent.primary)
Image(systemName: "sun.max.fill") Image(systemName: "sun.max.fill")
.font(.system(size: Design.BaseFontSize.large)) .font(.system(size: Design.BaseFontSize.large))
@ -443,7 +458,7 @@ struct SettingsView: View {
Image(systemName: "crown.fill") Image(systemName: "crown.fill")
.font(.system(size: Design.BaseFontSize.small)) .font(.system(size: Design.BaseFontSize.small))
.foregroundStyle(Color.Status.warning) .foregroundStyle(AppStatus.warning)
} }
Text(String(localized: "Automatically keeps you centered in the frame")) Text(String(localized: "Automatically keeps you centered in the frame"))
@ -451,7 +466,7 @@ struct SettingsView: View {
.foregroundStyle(.white.opacity(Design.Opacity.medium)) .foregroundStyle(.white.opacity(Design.Opacity.medium))
} }
} }
.tint(Color.Accent.primary) .tint(AppAccent.primary)
.padding(.vertical, Design.Spacing.xSmall) .padding(.vertical, Design.Spacing.xSmall)
.disabled(!isPremiumUnlocked) .disabled(!isPremiumUnlocked)
.accessibilityLabel(String(localized: "Enable Center Stage")) .accessibilityLabel(String(localized: "Enable Center Stage"))
@ -477,7 +492,7 @@ struct SettingsView: View {
Image(systemName: "crown.fill") Image(systemName: "crown.fill")
.font(.system(size: Design.BaseFontSize.small)) .font(.system(size: Design.BaseFontSize.small))
.foregroundStyle(Color.Status.warning) .foregroundStyle(AppStatus.warning)
} }
Text(isPremiumUnlocked Text(isPremiumUnlocked
@ -512,7 +527,7 @@ struct SettingsView: View {
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.background( .background(
Capsule() Capsule()
.fill(viewModel.selectedTimer == option ? Color.Accent.primary : Color.white.opacity(Design.Opacity.subtle)) .fill(viewModel.selectedTimer == option ? AppAccent.primary : Color.white.opacity(Design.Opacity.subtle))
) )
} }
.buttonStyle(.plain) .buttonStyle(.plain)
@ -536,7 +551,7 @@ struct SettingsView: View {
HStack(spacing: Design.Spacing.medium) { HStack(spacing: Design.Spacing.medium) {
Image(systemName: "crown.fill") Image(systemName: "crown.fill")
.font(.title2) .font(.title2)
.foregroundStyle(Color.Status.warning) .foregroundStyle(AppStatus.warning)
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) { VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
Text(String(localized: "Upgrade to Pro")) Text(String(localized: "Upgrade to Pro"))
@ -557,8 +572,8 @@ struct SettingsView: View {
.padding(Design.Spacing.medium) .padding(Design.Spacing.medium)
.background( .background(
RoundedRectangle(cornerRadius: Design.CornerRadius.medium) RoundedRectangle(cornerRadius: Design.CornerRadius.medium)
.fill(Color.Accent.primary.opacity(Design.Opacity.subtle)) .fill(AppAccent.primary.opacity(Design.Opacity.subtle))
.strokeBorder(Color.Accent.primary.opacity(Design.Opacity.light), lineWidth: Design.LineWidth.thin) .strokeBorder(AppAccent.primary.opacity(Design.Opacity.light), lineWidth: Design.LineWidth.thin)
) )
} }
.buttonStyle(.plain) .buttonStyle(.plain)
@ -584,7 +599,7 @@ struct SettingsView: View {
.foregroundStyle(.white.opacity(Design.Opacity.medium)) .foregroundStyle(.white.opacity(Design.Opacity.medium))
} }
} }
.tint(Color.Accent.primary) .tint(AppAccent.primary)
.padding(.vertical, Design.Spacing.xSmall) .padding(.vertical, Design.Spacing.xSmall)
.disabled(!viewModel.iCloudAvailable) .disabled(!viewModel.iCloudAvailable)
.accessibilityHint(String(localized: "Syncs settings across all your devices via iCloud")) .accessibilityHint(String(localized: "Syncs settings across all your devices via iCloud"))
@ -607,7 +622,7 @@ struct SettingsView: View {
} label: { } label: {
Text(String(localized: "Sync Now")) Text(String(localized: "Sync Now"))
.font(.system(size: Design.BaseFontSize.caption, weight: .medium)) .font(.system(size: Design.BaseFontSize.caption, weight: .medium))
.foregroundStyle(Color.Accent.primary) .foregroundStyle(AppAccent.primary)
} }
} }
.padding(.top, Design.Spacing.xSmall) .padding(.top, Design.Spacing.xSmall)
@ -626,9 +641,9 @@ struct SettingsView: View {
private var syncStatusColor: Color { private var syncStatusColor: Color {
if !viewModel.hasCompletedInitialSync { if !viewModel.hasCompletedInitialSync {
return Color.Status.warning return AppStatus.warning
} }
return Color.Status.success return AppStatus.success
} }
private var syncStatusText: String { private var syncStatusText: String {
@ -672,7 +687,7 @@ struct SettingsView: View {
.foregroundStyle(.white.opacity(Design.Opacity.medium)) .foregroundStyle(.white.opacity(Design.Opacity.medium))
} }
.padding(Design.Spacing.medium) .padding(Design.Spacing.medium)
.background(Color.Surface.primary, in: RoundedRectangle(cornerRadius: Design.CornerRadius.medium)) .background(AppSurface.primary, in: RoundedRectangle(cornerRadius: Design.CornerRadius.medium))
} }
.buttonStyle(.plain) .buttonStyle(.plain)
} }
@ -703,7 +718,7 @@ struct SettingsView: View {
Image(systemName: "crown.fill") Image(systemName: "crown.fill")
.font(.system(size: Design.BaseFontSize.small)) .font(.system(size: Design.BaseFontSize.small))
.foregroundStyle(Color.Status.warning) .foregroundStyle(AppStatus.warning)
} }
Text(subtitle) Text(subtitle)
@ -711,7 +726,7 @@ struct SettingsView: View {
.foregroundStyle(.white.opacity(Design.Opacity.medium)) .foregroundStyle(.white.opacity(Design.Opacity.medium))
} }
} }
.tint(Color.Accent.primary) .tint(AppAccent.primary)
.padding(.vertical, Design.Spacing.xSmall) .padding(.vertical, Design.Spacing.xSmall)
.disabled(!isPremiumUnlocked) .disabled(!isPremiumUnlocked)
.accessibilityHint(accessibilityHint) .accessibilityHint(accessibilityHint)
@ -744,7 +759,7 @@ struct SettingsView: View {
Image(systemName: "crown.fill") Image(systemName: "crown.fill")
.font(.system(size: Design.BaseFontSize.small)) .font(.system(size: Design.BaseFontSize.small))
.foregroundStyle(Color.Status.warning) .foregroundStyle(AppStatus.warning)
Spacer() Spacer()
} }
@ -764,7 +779,7 @@ struct SettingsView: View {
subtitle: "Unlock all premium features for testing", subtitle: "Unlock all premium features for testing",
isOn: $viewModel.isDebugPremiumEnabled isOn: $viewModel.isDebugPremiumEnabled
) )
.tint(Color.Status.warning) .tint(AppStatus.warning)
// Icon Generator // Icon Generator
NavigationLink { NavigationLink {
IconGeneratorView(config: .selfieCam, appName: "SelfieCam") IconGeneratorView(config: .selfieCam, appName: "SelfieCam")
@ -787,7 +802,7 @@ struct SettingsView: View {
.foregroundStyle(.white.opacity(Design.Opacity.medium)) .foregroundStyle(.white.opacity(Design.Opacity.medium))
} }
.padding(Design.Spacing.medium) .padding(Design.Spacing.medium)
.background(Color.Surface.primary, in: RoundedRectangle(cornerRadius: Design.CornerRadius.medium)) .background(AppSurface.primary, in: RoundedRectangle(cornerRadius: Design.CornerRadius.medium))
} }
.buttonStyle(.plain) .buttonStyle(.plain)
@ -817,7 +832,7 @@ struct SettingsView: View {
.foregroundStyle(.white.opacity(Design.Opacity.medium)) .foregroundStyle(.white.opacity(Design.Opacity.medium))
} }
.padding(Design.Spacing.medium) .padding(Design.Spacing.medium)
.background(Color.Surface.primary, in: RoundedRectangle(cornerRadius: Design.CornerRadius.medium)) .background(AppSurface.primary, in: RoundedRectangle(cornerRadius: Design.CornerRadius.medium))
} }
.buttonStyle(.plain) .buttonStyle(.plain)
} }
@ -827,6 +842,29 @@ struct SettingsView: View {
// MARK: - Settings Card Container
/// A card container that provides visual grouping for settings sections.
/// Uses the app's branded surface colors for separation from the background.
private 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)
)
}
}
#Preview { #Preview {
SettingsView(viewModel: SettingsViewModel(), showPaywall: .constant(false)) SettingsView(viewModel: SettingsViewModel(), showPaywall: .constant(false))
.preferredColorScheme(.dark) .preferredColorScheme(.dark)

View File

@ -0,0 +1,144 @@
//
// SelfieCamTheme.swift
// SelfieCam
//
// Custom color theme for SelfieCam matching the app's branding.
// Uses magenta/rose tinted surfaces with branded accent colors.
//
import SwiftUI
import Bedrock
// MARK: - SelfieCam Surface Colors
/// Surface colors with a subtle rose/magenta tint to match app branding.
/// Provides more visual separation than neutral grays.
public enum SelfieCamSurfaceColors: SurfaceColorProvider {
/// Primary background - deep rose-tinted dark
public static let primary = Color(red: 0.08, green: 0.06, blue: 0.10)
/// Secondary/elevated surface - slightly lighter with rose tint
public static let secondary = Color(red: 0.12, green: 0.08, blue: 0.14)
/// Tertiary/card surface - more elevated
public static let tertiary = Color(red: 0.16, green: 0.11, blue: 0.18)
/// Overlay background (for sheets/modals) - deep with subtle rose
public static let overlay = Color(red: 0.10, green: 0.07, blue: 0.12)
/// Card/grouped element background - distinct from primary
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 - slightly more visible
public static let sectionFill = Color(red: 0.16, green: 0.12, blue: 0.18)
}
// MARK: - SelfieCam Text Colors
/// Text colors optimized for rose-tinted dark backgrounds.
public enum SelfieCamTextColors: 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
}
// MARK: - SelfieCam Accent Colors
/// Accent colors derived from the app's branding magenta/rose.
public enum SelfieCamAccentColors: AccentColorProvider {
/// Primary accent - bright magenta/rose from branding
public static let primary = Color(red: 0.85, green: 0.25, blue: 0.45)
/// Light variant - softer pink
public static let light = Color(red: 0.95, green: 0.45, blue: 0.60)
/// Dark variant - deeper magenta
public static let dark = Color(red: 0.65, green: 0.18, blue: 0.35)
/// Secondary accent - soft cream/warm white for contrast
public static let secondary = Color(red: 1.0, green: 0.95, blue: 0.90)
}
// MARK: - SelfieCam Button Colors
/// Button colors matching the branded theme.
public enum SelfieCamButtonColors: 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)
}
// MARK: - SelfieCam Status Colors
/// Standard semantic status colors.
public enum SelfieCamStatusColors: 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)
}
// MARK: - SelfieCam Border Colors
/// Border colors for the rose-tinted theme.
public enum SelfieCamBorderColors: 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 = SelfieCamAccentColors.primary.opacity(Design.Opacity.medium)
}
// MARK: - SelfieCam Interactive Colors
/// Interactive state colors for the theme.
public enum SelfieCamInteractiveColors: InteractiveColorProvider {
public static let selected = SelfieCamAccentColors.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 = SelfieCamAccentColors.light
}
// MARK: - SelfieCam Theme
/// The complete SelfieCam color theme.
///
/// Use this theme for consistent branded colors throughout the app:
/// ```swift
/// .background(SelfieCamTheme.Surface.primary)
/// .foregroundStyle(SelfieCamTheme.Accent.primary)
/// ```
public enum SelfieCamTheme: AppColorTheme {
public typealias Surface = SelfieCamSurfaceColors
public typealias Text = SelfieCamTextColors
public typealias Accent = SelfieCamAccentColors
public typealias Button = SelfieCamButtonColors
public typealias Status = SelfieCamStatusColors
public typealias Border = SelfieCamBorderColors
public typealias Interactive = SelfieCamInteractiveColors
}
// MARK: - Convenience Typealiases
/// 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 = SelfieCamSurfaceColors
typealias AppTextColors = SelfieCamTextColors
typealias AppAccent = SelfieCamAccentColors
typealias AppButtonColors = SelfieCamButtonColors
typealias AppStatus = SelfieCamStatusColors
typealias AppBorder = SelfieCamBorderColors
typealias AppInteractive = SelfieCamInteractiveColors