Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>

This commit is contained in:
Matt Bruce 2026-02-09 17:03:33 -06:00
parent 499889d373
commit 8f01d78146
7 changed files with 159 additions and 1246 deletions

1
PRD.md
View File

@ -44,6 +44,7 @@ SelfieCam is a professional-grade selfie camera app featuring a customizable scr
1. **Protocol-Oriented Programming (POP)** - All shared capabilities defined via protocols before concrete types 1. **Protocol-Oriented Programming (POP)** - All shared capabilities defined via protocols before concrete types
2. **MVVM-lite** - Views are "dumb" renderers; all logic lives in `@Observable` view models 2. **MVVM-lite** - Views are "dumb" renderers; all logic lives in `@Observable` view models
3. **Bedrock Design System** - Centralized design tokens, no magic numbers 3. **Bedrock Design System** - Centralized design tokens, no magic numbers
- Settings layout contract: `SettingsCard` owns horizontal insets, custom rows use `SettingsCardRow`, and in-card separators use `SettingsDivider`
4. **Full Accessibility** - Dynamic Type, VoiceOver labels/hints/traits/announcements 4. **Full Accessibility** - Dynamic Type, VoiceOver labels/hints/traits/announcements
5. **Modern Swift & SwiftUI** - Swift 6 concurrency, `@MainActor`, modern APIs 5. **Modern Swift & SwiftUI** - Swift 6 concurrency, `@MainActor`, modern APIs
6. **Testable & Reusable Design** - Protocols enable mocking and future package extraction 6. **Testable & Reusable Design** - Protocols enable mocking and future package extraction

View File

@ -53,6 +53,7 @@ Perfect for low-light selfies, content creation, video calls, makeup application
- Dynamic Type and ScaledMetric for readable text at all sizes - Dynamic Type and ScaledMetric for readable text at all sizes
- String Catalog localization ready (`.xcstrings`) - String Catalog localization ready (`.xcstrings`)
- Consistent design system using Bedrock framework - Consistent design system using Bedrock framework
- Settings cards follow Bedrocks row contract (`SettingsCard` + `SettingsCardRow` + `SettingsDivider`) for consistent insets and row rhythm
- Prevents screen dimming during camera use - Prevents screen dimming during camera use
## Screenshots ## Screenshots

View File

@ -442,7 +442,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.2;
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER)"; PRODUCT_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER)";
PRODUCT_NAME = "$(PRODUCT_NAME)"; PRODUCT_NAME = "$(PRODUCT_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = YES; STRING_CATALOG_GENERATE_SYMBOLS = YES;
@ -472,7 +472,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.2;
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER)"; PRODUCT_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER)";
PRODUCT_NAME = "$(PRODUCT_NAME)"; PRODUCT_NAME = "$(PRODUCT_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = YES; STRING_CATALOG_GENERATE_SYMBOLS = YES;

View File

@ -39,6 +39,7 @@ struct OnboardingSettingsView: View {
isOn: $viewModel.isRingLightEnabled, isOn: $viewModel.isRingLightEnabled,
accentColor: AppAccent.primary accentColor: AppAccent.primary
) )
SettingsDivider(color: AppBorder.subtle)
// Camera Position using Bedrock SettingsSegmentedPicker // Camera Position using Bedrock SettingsSegmentedPicker
SettingsSegmentedPicker( SettingsSegmentedPicker(

View File

@ -43,12 +43,17 @@ struct SettingsView: View {
accentColor: AppAccent.primary accentColor: AppAccent.primary
) )
.accessibilityHint(String(localized: "Enables or disables the ring light overlay")) .accessibilityHint(String(localized: "Enables or disables the ring light overlay"))
SettingsDivider(color: AppBorder.subtle)
// Ring Size Slider // Ring Size Slider
ringSizeSlider ringSizeSlider
SettingsDivider(color: AppBorder.subtle)
// Color Preset // Color Preset
SettingsCardRow {
colorPresetSection colorPresetSection
}
SettingsDivider(color: AppBorder.subtle)
// Ring Light Brightness // Ring Light Brightness
ringLightBrightnessSlider ringLightBrightnessSlider
@ -61,9 +66,11 @@ struct SettingsView: View {
SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) { SettingsCard(backgroundColor: AppSurface.card, borderColor: AppBorder.subtle) {
// Camera Position // Camera Position
cameraPositionPicker cameraPositionPicker
SettingsDivider(color: AppBorder.subtle)
// Flash Mode // Flash Mode
flashModePicker flashModePicker
SettingsDivider(color: AppBorder.subtle)
// Flash Sync (premium) // Flash Sync (premium)
premiumToggle( premiumToggle(
@ -72,9 +79,11 @@ struct SettingsView: View {
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")
) )
SettingsDivider(color: AppBorder.subtle)
// HDR Mode // HDR Mode
hdrModePicker hdrModePicker
SettingsDivider(color: AppBorder.subtle)
// Center Stage (premium feature) // Center Stage (premium feature)
premiumToggle( premiumToggle(
@ -83,10 +92,13 @@ struct SettingsView: View {
isOn: $viewModel.isCenterStageEnabled, isOn: $viewModel.isCenterStageEnabled,
accessibilityHint: String(localized: "Automatically adjusts camera to keep subject centered") accessibilityHint: String(localized: "Automatically adjusts camera to keep subject centered")
) )
SettingsDivider(color: AppBorder.subtle)
// Photo Quality // Photo Quality
SettingsCardRow {
photoQualityPicker photoQualityPicker
} }
}
// MARK: - Display Section // MARK: - Display Section
@ -101,6 +113,7 @@ struct SettingsView: View {
accentColor: AppAccent.primary 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"))
SettingsDivider(color: AppBorder.subtle)
// True Mirror (premium) // True Mirror (premium)
premiumToggle( premiumToggle(
@ -109,6 +122,7 @@ struct SettingsView: View {
isOn: $viewModel.isMirrorFlipped, isOn: $viewModel.isMirrorFlipped,
accessibilityHint: String(localized: "Flips the camera preview horizontally") accessibilityHint: String(localized: "Flips the camera preview horizontally")
) )
SettingsDivider(color: AppBorder.subtle)
// Skin Smoothing (premium) // Skin Smoothing (premium)
premiumToggle( premiumToggle(
@ -132,9 +146,12 @@ struct SettingsView: View {
accentColor: AppAccent.primary 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"))
SettingsDivider(color: AppBorder.subtle)
// Timer Selection // Timer Selection
SettingsCardRow {
timerPicker timerPicker
}
} }
@ -258,7 +275,6 @@ struct SettingsView: View {
) )
} }
} }
.padding(.vertical, Design.Spacing.xSmall)
} }
// MARK: - Flash Mode Picker // MARK: - Flash Mode Picker
@ -585,8 +601,8 @@ struct SettingsView: View {
// MARK: - Branding Debug Section // MARK: - Branding Debug Section
#if DEBUG #if DEBUG
@ViewBuilder
private var brandingDebugSection: some View { private var brandingDebugSection: some View {
VStack(spacing: Design.Spacing.small) {
// Debug Premium Toggle // Debug Premium Toggle
SettingsToggle( SettingsToggle(
title: "Enable Debug Premium", title: "Enable Debug Premium",
@ -594,24 +610,29 @@ struct SettingsView: View {
isOn: $viewModel.isDebugPremiumEnabled, isOn: $viewModel.isDebugPremiumEnabled,
accentColor: AppAccent.primary accentColor: AppAccent.primary
) )
SettingsDivider(color: AppBorder.subtle)
// Reset Onboarding Button // Reset Onboarding Button
SettingsCardRow {
ResetOnboardingButton() ResetOnboardingButton()
}
SettingsDivider(color: AppBorder.subtle)
// Icon Generator // Icon Generator
SettingsNavigationRow( SettingsNavigationRow(
title: "Icon Generator", title: "Icon Generator",
subtitle: "Generate and save app icon to Files", subtitle: "Generate and save app icon to Files",
backgroundColor: AppSurface.primary backgroundColor: .clear
) { ) {
IconGeneratorView(config: .selfieCam, appName: "SelfieCam") IconGeneratorView(config: .selfieCam, appName: "SelfieCam")
} }
SettingsDivider(color: AppBorder.subtle)
// Branding Preview // Branding Preview
SettingsNavigationRow( SettingsNavigationRow(
title: "Branding Preview", title: "Branding Preview",
subtitle: "Preview app icon and launch screen", subtitle: "Preview app icon and launch screen",
backgroundColor: AppSurface.primary backgroundColor: .clear
) { ) {
BrandingPreviewView( BrandingPreviewView(
iconConfig: .selfieCam, iconConfig: .selfieCam,
@ -620,7 +641,6 @@ struct SettingsView: View {
) )
} }
} }
}
#endif #endif
} }

File diff suppressed because it is too large Load Diff