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
2. **MVVM-lite** - Views are "dumb" renderers; all logic lives in `@Observable` view models
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
5. **Modern Swift & SwiftUI** - Swift 6 concurrency, `@MainActor`, modern APIs
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
- String Catalog localization ready (`.xcstrings`)
- 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
## Screenshots

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff