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

This commit is contained in:
Matt Bruce 2026-02-10 20:18:21 -06:00
parent cfa1e17b3e
commit 79d69a5495
44 changed files with 876 additions and 281 deletions

View File

@ -7,6 +7,7 @@
objects = {
/* Begin PBXBuildFile section */
EA69DC822F3C199C00592220 /* Bedrock in Frameworks */ = {isa = PBXBuildFile; productRef = EA69DC812F3C199C00592220 /* Bedrock */; };
EA837E672F107D6800077F87 /* Bedrock in Frameworks */ = {isa = PBXBuildFile; productRef = EA837E662F107D6800077F87 /* Bedrock */; };
EAAE892A2F12DE110075BC8A /* BusinessCardWatch Watch App.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = EA837F982F11B16400077F87 /* BusinessCardWatch Watch App.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
EACLIP0012F200000000001 /* BusinessCardClip.app in Embed App Clips */ = {isa = PBXBuildFile; fileRef = EACLIP0012F200000000002 /* BusinessCardClip.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
@ -135,6 +136,7 @@
buildActionMask = 2147483647;
files = (
EA837E672F107D6800077F87 /* Bedrock in Frameworks */,
EA69DC822F3C199C00592220 /* Bedrock in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -236,6 +238,7 @@
name = BusinessCard;
packageProductDependencies = (
EA837E662F107D6800077F87 /* Bedrock */,
EA69DC812F3C199C00592220 /* Bedrock */,
);
productName = BusinessCard;
productReference = EA8379232F105F2600077F87 /* BusinessCard.app */;
@ -372,7 +375,7 @@
mainGroup = EA83791A2F105F2600077F87;
minimizedProjectReferenceProxies = 1;
packageReferences = (
EA837E652F107D6800077F87 /* XCLocalSwiftPackageReference "../Frameworks/Bedrock" */,
EA69DC802F3C199C00592220 /* XCLocalSwiftPackageReference "../Bedrock" */,
);
preferredProjectObjectVersion = 77;
productRefGroup = EA8379242F105F2600077F87 /* Products */;
@ -524,7 +527,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)";
DEVELOPMENT_TEAM = 6R7KLBPBLZ;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
@ -589,7 +592,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)";
DEVELOPMENT_TEAM = 6R7KLBPBLZ;
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
@ -628,8 +631,8 @@
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "BusinessCard uses your photo library to add a profile photo to your business card.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_BackgroundColor = LaunchBackground;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
"INFOPLIST_KEY_UILaunchScreen_BackgroundColor" = LaunchBackground;
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@ -665,8 +668,8 @@
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "BusinessCard uses your photo library to add a profile photo to your business card.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_BackgroundColor = LaunchBackground;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
"INFOPLIST_KEY_UILaunchScreen_BackgroundColor" = LaunchBackground;
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@ -852,8 +855,8 @@
INFOPLIST_KEY_CFBundleDisplayName = BusinessCard;
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_BackgroundColor = LaunchBackground;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
"INFOPLIST_KEY_UILaunchScreen_BackgroundColor" = LaunchBackground;
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
LD_RUNPATH_SEARCH_PATHS = (
@ -890,8 +893,8 @@
INFOPLIST_KEY_CFBundleDisplayName = BusinessCard;
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_BackgroundColor = LaunchBackground;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
"INFOPLIST_KEY_UILaunchScreen_BackgroundColor" = LaunchBackground;
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
LD_RUNPATH_SEARCH_PATHS = (
@ -973,13 +976,17 @@
/* End XCConfigurationList section */
/* Begin XCLocalSwiftPackageReference section */
EA837E652F107D6800077F87 /* XCLocalSwiftPackageReference "../Frameworks/Bedrock" */ = {
EA69DC802F3C199C00592220 /* XCLocalSwiftPackageReference "../Bedrock" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = ../Frameworks/Bedrock;
relativePath = ../Bedrock;
};
/* End XCLocalSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
EA69DC812F3C199C00592220 /* Bedrock */ = {
isa = XCSwiftPackageProductDependency;
productName = Bedrock;
};
EA837E662F107D6800077F87 /* Bedrock */ = {
isa = XCSwiftPackageProductDependency;
productName = Bedrock;

View File

@ -7,17 +7,17 @@
<key>BusinessCard.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>1</integer>
<integer>2</integer>
</dict>
<key>BusinessCardClip.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>2</integer>
<integer>0</integer>
</dict>
<key>BusinessCardWatch Watch App.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>3</integer>
<integer>1</integer>
</dict>
</dict>
</dict>

View File

@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.860",
"green" : "0.910",
"red" : "0.950"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.160",
"green" : "0.190",
"red" : "0.230"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.940",
"green" : "0.960",
"red" : "0.970"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.160",
"green" : "0.130",
"red" : "0.110"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "1.000",
"green" : "1.000",
"red" : "1.000"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.250",
"green" : "0.200",
"red" : "0.180"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.950",
"green" : "0.950",
"red" : "0.950"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.200",
"green" : "0.160",
"red" : "0.140"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.980",
"green" : "0.980",
"red" : "0.980"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.170",
"green" : "0.140",
"red" : "0.120"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.170",
"green" : "0.140",
"red" : "0.140"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.960",
"green" : "0.940",
"red" : "0.930"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.400",
"green" : "0.340",
"red" : "0.320"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.820",
"green" : "0.770",
"red" : "0.740"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.620",
"green" : "0.580",
"red" : "0.560"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.700",
"green" : "0.640",
"red" : "0.600"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.970",
"green" : "0.950",
"red" : "0.940"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.230",
"green" : "0.200",
"red" : "0.180"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "1.000",
"green" : "0.990",
"red" : "0.980"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.300",
"green" : "0.260",
"red" : "0.240"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.920",
"green" : "0.880",
"red" : "0.860"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.360",
"green" : "0.320",
"red" : "0.300"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.470",
"green" : "0.410",
"red" : "0.380"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.750",
"green" : "0.720",
"red" : "0.700"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.170",
"green" : "0.140",
"red" : "0.120"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.970",
"green" : "0.960",
"red" : "0.960"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -10,6 +10,15 @@ struct BusinessCardApp: App {
init() {
let schema = Schema([BusinessCard.self, Contact.self, ContactField.self])
// Register app theme for Bedrock semantic text/surface colors.
Theme.register(
text: AppThemeText.self,
surface: AppSurface.self,
accent: AppThemeAccent.self,
status: AppStatus.self
)
Theme.register(border: AppBorder.self)
// Primary strategy: App Group for watch sync (without CloudKit for now)
// CloudKit can be enabled once properly configured in Xcode
var container: ModelContainer?
@ -80,7 +89,7 @@ struct BusinessCardApp: App {
AppLaunchView(config: .businessCard) {
RootTabView()
.environment(appState)
.preferredColorScheme(.light)
.preferredColorScheme(appState.preferredColorScheme)
}
}
}

View File

@ -2,8 +2,8 @@
// BusinessCardTheme.swift
// BusinessCard
//
// App-specific theme conforming to Bedrock's color protocols.
// This light theme uses warm, professional tones.
// App-specific adaptive theme conforming to Bedrock's color protocols.
// Uses warm light colors and deep slate dark colors.
//
import SwiftUI
@ -11,39 +11,39 @@ import Bedrock
// MARK: - Surface Colors
/// Surface colors with warm off-white tones for a professional light theme.
/// Surface colors with warm off-white light tones and deep slate dark tones.
public enum BusinessCardSurfaceColors: SurfaceColorProvider {
/// Primary background - warm off-white base
public static let primary = Color(red: 0.97, green: 0.96, blue: 0.94)
/// Primary background
public static let primary = Color.AppBackground.base
/// Secondary/elevated surface
public static let secondary = Color(red: 0.95, green: 0.95, blue: 0.95)
public static let secondary = Color.AppBackground.secondary
/// Tertiary/card surface - most elevated
public static let tertiary = Color(red: 1.0, green: 1.0, blue: 1.0)
public static let tertiary = Color.AppBackground.elevated
/// Overlay background (for sheets/modals)
public static let overlay = Color(red: 0.97, green: 0.96, blue: 0.94)
public static let overlay = Color.AppBackground.base
/// Card/grouped element background
public static let card = Color(red: 1.0, green: 1.0, blue: 1.0)
public static let card = Color.AppBackground.card
/// Subtle fill for grouped content sections
public static let groupedFill = Color(red: 0.95, green: 0.94, blue: 0.92)
public static let groupedFill = Color.AppBackground.accent
/// Section fill for list sections
public static let sectionFill = Color(red: 0.93, green: 0.92, blue: 0.90)
public static let sectionFill = Color.AppBackground.secondary
}
// MARK: - Text Colors
public enum BusinessCardTextColors: TextColorProvider {
public static let primary = Color(red: 0.14, green: 0.14, blue: 0.17)
public static let secondary = Color(red: 0.32, green: 0.34, blue: 0.40)
public static let tertiary = Color(red: 0.56, green: 0.58, blue: 0.62)
public static let disabled = Color(red: 0.70, green: 0.72, blue: 0.75)
public static let placeholder = Color(red: 0.60, green: 0.62, blue: 0.66)
public static let inverse = Color(red: 0.98, green: 0.98, blue: 0.98)
public static let primary = Color.AppText.primary
public static let secondary = Color.AppText.secondary
public static let tertiary = Color.AppText.tertiary
public static let disabled = Color.AppText.tertiary.opacity(Design.Opacity.strong)
public static let placeholder = Color.AppText.tertiary
public static let inverse = Color.AppText.inverted
}
// MARK: - Accent Colors
@ -84,9 +84,9 @@ public enum BusinessCardStatusColors: StatusColorProvider {
// MARK: - Border Colors
public enum BusinessCardBorderColors: BorderColorProvider {
public static let subtle = Color(red: 0.14, green: 0.14, blue: 0.17).opacity(Design.Opacity.subtle)
public static let standard = Color(red: 0.14, green: 0.14, blue: 0.17).opacity(Design.Opacity.hint)
public static let emphasized = Color(red: 0.14, green: 0.14, blue: 0.17).opacity(Design.Opacity.light)
public static let subtle = Color.AppText.tertiary.opacity(Design.Opacity.subtle)
public static let standard = Color.AppText.tertiary.opacity(Design.Opacity.hint)
public static let emphasized = Color.AppText.secondary.opacity(Design.Opacity.light)
public static let selected = BusinessCardAccentColors.primary.opacity(Design.Opacity.medium)
}

View File

@ -105,15 +105,14 @@ extension Design.Shadow {
/// BusinessCard's light theme color palette.
/// Uses warm, professional tones suitable for a business card app.
extension Color {
// MARK: - App Backgrounds (Light Theme)
// MARK: - App Backgrounds
enum AppBackground {
static let base = Color(red: 0.97, green: 0.96, blue: 0.94)
static let secondary = Color(red: 0.95, green: 0.95, blue: 0.95)
static let elevated = Color(red: 1.0, green: 1.0, blue: 1.0)
static let card = Color(red: 1.0, green: 1.0, blue: 1.0)
static let accent = Color(red: 0.95, green: 0.91, blue: 0.86)
static let base = Color("AppBackgroundBase")
static let secondary = Color("AppBackgroundSecondary")
static let elevated = Color("AppBackgroundElevated")
static let card = elevated
static let accent = Color("AppBackgroundAccent")
}
// MARK: - Card Theme Palette
@ -142,13 +141,13 @@ extension Color {
static let slate = Color(red: 0.29, green: 0.33, blue: 0.4)
}
// MARK: - App Text Colors (Light Theme)
// MARK: - App Text Colors
enum AppText {
static let primary = Color(red: 0.14, green: 0.14, blue: 0.17)
static let secondary = Color(red: 0.32, green: 0.34, blue: 0.4)
static let tertiary = Color(red: 0.56, green: 0.58, blue: 0.62)
static let inverted = Color(red: 0.98, green: 0.98, blue: 0.98)
static let primary = Color("AppTextPrimary")
static let secondary = Color("AppTextSecondary")
static let tertiary = Color("AppTextTertiary")
static let inverted = Color("AppTextInverted")
}
// MARK: - Badge Colors
@ -158,14 +157,14 @@ extension Color {
static let neutral = Color(red: 0.89, green: 0.89, blue: 0.9)
}
// MARK: - Share Sheet Dark Theme
// MARK: - Share Sheet Theme
enum ShareSheet {
static let background = Color(red: 0.18, green: 0.20, blue: 0.23)
static let cardBackground = Color(red: 0.24, green: 0.26, blue: 0.30)
static let rowBackground = Color(red: 0.30, green: 0.32, blue: 0.36)
static let text = Color(red: 0.96, green: 0.96, blue: 0.97)
static let secondaryText = Color(red: 0.70, green: 0.72, blue: 0.75)
static let background = Color("ShareSheetBackground")
static let cardBackground = Color("ShareSheetCardBackground")
static let rowBackground = Color("ShareSheetRowBackground")
static let text = Color("ShareSheetText")
static let secondaryText = Color("ShareSheetSecondaryText")
}
// MARK: - Social Media Brand Colors

View File

@ -1,19 +1,53 @@
import Foundation
import Observation
import SwiftData
import SwiftUI
enum AppAppearance: String, CaseIterable, Sendable {
case system
case light
case dark
var preferredColorScheme: ColorScheme? {
switch self {
case .system: nil
case .light: .light
case .dark: .dark
}
}
}
@Observable
@MainActor
final class AppState {
private enum DefaultsKey {
static let appearance = "appAppearance"
}
var selectedTab: AppTab = .cards
var cardStore: CardStore
var contactsStore: ContactsStore
let shareLinkService: ShareLinkProviding
var appearance: AppAppearance {
didSet {
UserDefaults.standard.set(appearance.rawValue, forKey: DefaultsKey.appearance)
}
}
var preferredColorScheme: ColorScheme? {
appearance.preferredColorScheme
}
init(modelContext: ModelContext) {
self.cardStore = CardStore(modelContext: modelContext)
self.contactsStore = ContactsStore(modelContext: modelContext)
self.shareLinkService = ShareLinkService()
if let rawValue = UserDefaults.standard.string(forKey: DefaultsKey.appearance),
let savedAppearance = AppAppearance(rawValue: rawValue) {
self.appearance = savedAppearance
} else {
self.appearance = .system
}
// Clean up expired shared cards on launch (best-effort, non-blocking)
Task {

View File

@ -76,9 +76,9 @@ private struct ProfileBannerContent: View {
VStack(spacing: Design.Spacing.xSmall) {
Image(systemName: "person.fill")
.font(.system(size: Design.BaseFontSize.display, weight: .bold))
.typography(.title2Bold)
Text("Profile")
.font(.title3)
.typography(.title3)
.bold()
}
.foregroundStyle(card.theme.textColor.opacity(Design.Opacity.medium))
@ -111,9 +111,9 @@ private struct LogoBannerContent: View {
} else {
VStack(spacing: Design.Spacing.xSmall) {
Image(systemName: "building.2.fill")
.font(.system(size: Design.BaseFontSize.display, weight: .bold))
.typography(.title2Bold)
Text("Logo")
.font(.title3)
.typography(.title3)
.bold()
}
.foregroundStyle(card.theme.textColor.opacity(Design.Opacity.medium))
@ -146,9 +146,9 @@ private struct CoverBannerContent: View {
VStack(spacing: Design.Spacing.xSmall) {
Image(systemName: "photo.fill")
.font(.system(size: Design.BaseFontSize.display, weight: .bold))
.typography(.title2Bold)
Text("Cover")
.font(.title3)
.typography(.title3)
.bold()
}
.foregroundStyle(card.theme.textColor.opacity(Design.Opacity.medium))
@ -173,28 +173,28 @@ private struct CardContentView: View {
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
HStack(spacing: Design.Spacing.xSmall) {
Text(card.fullName)
.font(.title2)
.typography(.title2)
.bold()
.foregroundStyle(textColor)
if !card.pronouns.isEmpty {
Text("(\(card.pronouns))")
.font(.subheadline)
.typography(.subheading)
.foregroundStyle(Color.Text.secondary)
}
}
Text(card.role)
.font(.headline)
.typography(.heading)
.foregroundStyle(textColor)
Text(card.company)
.font(.subheadline)
.typography(.subheading)
.foregroundStyle(Color.Text.secondary)
if !card.headline.isEmpty {
Text(card.headline)
.font(.caption)
.typography(.caption)
.foregroundStyle(Color.Text.secondary)
.padding(.top, Design.Spacing.xxSmall)
}
@ -249,7 +249,7 @@ private struct ProfileAvatarView: View {
.scaledToFill()
} else {
Image(systemName: card.avatarSystemName)
.font(.system(size: Design.BaseFontSize.title))
.typography(.title3)
.foregroundStyle(card.theme.textColor)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(card.theme.accentColor)
@ -277,9 +277,9 @@ private struct LogoBadgeView: View {
} else {
VStack(spacing: Design.Spacing.xxSmall) {
Image(systemName: "building.2")
.font(.system(size: Design.BaseFontSize.body))
.typography(.body)
Text("Logo")
.font(.caption2)
.typography(.caption2)
}
.foregroundStyle(card.theme.textColor)
.frame(maxWidth: .infinity, maxHeight: .infinity)
@ -310,9 +310,9 @@ private struct LogoRectangleView: View {
} else {
VStack(spacing: Design.Spacing.xxSmall) {
Image(systemName: "building.2")
.font(.system(size: Design.BaseFontSize.body))
.typography(.body)
Text("Logo")
.font(.caption2)
.typography(.caption2)
}
.foregroundStyle(card.theme.textColor)
.frame(maxWidth: .infinity, maxHeight: .infinity)
@ -354,7 +354,7 @@ private struct ContactFieldRowView: View {
Button(action: action) {
HStack(alignment: .top, spacing: Design.Spacing.medium) {
field.iconImage()
.font(.body)
.typography(.body)
.foregroundStyle(.white)
.frame(width: Design.CardSize.socialIconSize, height: Design.CardSize.socialIconSize)
.background(themeColor)
@ -362,12 +362,12 @@ private struct ContactFieldRowView: View {
VStack(alignment: .leading, spacing: 0) {
Text(field.displayValue)
.font(.subheadline)
.typography(.subheading)
.foregroundStyle(Color.Text.primary)
.multilineTextAlignment(.leading)
Text(field.title.isEmpty ? field.displayName : field.title)
.font(.caption)
.typography(.caption)
.foregroundStyle(Color.Text.secondary)
.lineLimit(1)
}
@ -375,7 +375,7 @@ private struct ContactFieldRowView: View {
Spacer()
Image(systemName: "chevron.right")
.font(.caption)
.typography(.caption)
.foregroundStyle(Color.Text.tertiary)
}
.contentShape(.rect)

View File

@ -408,7 +408,7 @@ private struct CustomColorSwatch: View {
// Center icon to indicate it's a picker
if customColor == nil {
Image(systemName: "eyedropper")
.font(.caption)
.typography(.caption)
.foregroundStyle(.white)
.shadow(color: .black.opacity(Design.Opacity.medium), radius: Design.Shadow.radiusSmall)
}
@ -443,7 +443,7 @@ private struct CustomColorPickerSheet: View {
.frame(height: Design.CardSize.bannerHeight)
.overlay(
Text("Preview")
.font(.headline)
.typography(.heading)
.foregroundStyle(selectedColor.contrastingTextColor)
)
.padding(.horizontal, Design.Spacing.large)
@ -572,24 +572,24 @@ private struct ImageLayoutRow: View {
Button(action: onSelectLayout) {
HStack(spacing: Design.Spacing.medium) {
Image(systemName: selectedHeaderLayout.iconName)
.font(.title3)
.typography(.title3)
.foregroundStyle(Color.accentColor)
.frame(width: Design.CardSize.socialIconSize)
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
Text("Header Layout")
.font(.subheadline)
.typography(.subheading)
.foregroundStyle(Color.Text.primary)
Text(selectedHeaderLayout.displayName)
.font(.caption)
.typography(.caption)
.foregroundStyle(Color.Text.secondary)
}
Spacer()
Image(systemName: "chevron.right")
.font(.caption)
.typography(.caption)
.foregroundStyle(Color.Text.tertiary)
}
.padding(.vertical, Design.Spacing.xSmall)
@ -654,9 +654,9 @@ private struct EditorBannerPreviewView: View {
VStack(spacing: Design.Spacing.xSmall) {
Image(systemName: "person.fill")
.font(.system(size: Design.BaseFontSize.display, weight: .bold))
.typography(.title2Bold)
Text("Profile")
.font(.title3)
.typography(.title3)
.bold()
}
.foregroundStyle(selectedTheme.textColor.opacity(Design.Opacity.medium))
@ -678,9 +678,9 @@ private struct EditorBannerPreviewView: View {
} else {
VStack(spacing: Design.Spacing.xSmall) {
Image(systemName: "building.2.fill")
.font(.system(size: Design.BaseFontSize.display, weight: .bold))
.typography(.title2Bold)
Text("Logo")
.font(.title3)
.typography(.title3)
.bold()
}
.foregroundStyle(selectedTheme.textColor.opacity(Design.Opacity.medium))
@ -703,9 +703,9 @@ private struct EditorBannerPreviewView: View {
VStack(spacing: Design.Spacing.xSmall) {
Image(systemName: "photo.fill")
.font(.system(size: Design.BaseFontSize.display, weight: .bold))
.typography(.title2Bold)
Text("Cover")
.font(.title3)
.typography(.title3)
.bold()
}
.foregroundStyle(selectedTheme.textColor.opacity(Design.Opacity.medium))
@ -742,7 +742,7 @@ private struct EditorLogoBadgeView: View {
.padding(Design.Spacing.small)
} else {
Image(systemName: "building.2")
.font(.system(size: Design.BaseFontSize.title))
.typography(.title3)
.foregroundStyle(theme.textColor)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
@ -781,7 +781,7 @@ private struct EditorLogoRectangleView: View {
.padding(Design.Spacing.small)
} else {
Image(systemName: "building.2")
.font(.system(size: Design.BaseFontSize.title))
.typography(.title3)
.foregroundStyle(theme.textColor)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
@ -859,24 +859,24 @@ private struct ImageActionRow: View {
Button(action: onTap) {
HStack(spacing: Design.Spacing.medium) {
Image(systemName: systemImage)
.font(.title3)
.typography(.title3)
.foregroundStyle(hasImage ? Color.accentColor : Color.Text.secondary)
.frame(width: Design.CardSize.socialIconSize)
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
Text(title)
.font(.subheadline)
.typography(.subheading)
.foregroundStyle(Color.Text.primary)
Text(subtitle)
.font(.caption)
.typography(.caption)
.foregroundStyle(Color.Text.secondary)
}
Spacer()
Image(systemName: "chevron.right")
.font(.caption)
.typography(.caption)
.foregroundStyle(Color.Text.tertiary)
}
.padding(.vertical, Design.Spacing.xSmall)
@ -909,7 +909,7 @@ private struct ProfilePhotoView: View {
.scaledToFill()
} else {
Image(systemName: avatarSystemName)
.font(.title)
.typography(.title)
.foregroundStyle(theme.textColor)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(theme.accentColor)
@ -933,7 +933,7 @@ private struct ContactFieldRowView: View {
HStack(spacing: Design.Spacing.medium) {
// Drag handle
Image(systemName: "line.3.horizontal")
.font(.subheadline)
.typography(.subheading)
.foregroundStyle(Color.Text.tertiary)
.accessibilityHidden(true)
@ -943,19 +943,19 @@ private struct ContactFieldRowView: View {
.frame(width: Design.CardSize.avatarSize, height: Design.CardSize.avatarSize)
.overlay(
field.fieldType.iconImage()
.font(.title3)
.typography(.title3)
.foregroundStyle(.white)
)
// Content
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
Text(field.value.isEmpty ? field.fieldType.valuePlaceholder : field.shortDisplayValue)
.font(.subheadline)
.typography(.subheading)
.foregroundStyle(field.value.isEmpty ? Color.Text.secondary : Color.Text.primary)
.lineLimit(1)
Text(field.title.isEmpty ? field.fieldType.displayName : field.title)
.font(.caption)
.typography(.caption)
.foregroundStyle(Color.Text.secondary)
.lineLimit(1)
}
@ -992,7 +992,7 @@ private struct AccreditationsRow: View {
addAccreditation()
} label: {
Image(systemName: "plus.circle.fill")
.font(.title2)
.typography(.title2)
.foregroundStyle(Color.accentColor)
}
.buttonStyle(.plain)
@ -1006,12 +1006,12 @@ private struct AccreditationsRow: View {
ForEach(accreditationsList, id: \.self) { tag in
HStack(spacing: Design.Spacing.xSmall) {
Text(tag)
.font(.subheadline)
.typography(.subheading)
Button {
removeAccreditation(tag)
} label: {
Image(systemName: "xmark.circle.fill")
.font(.caption)
.typography(.caption)
.foregroundStyle(Color.secondary)
}
.buttonStyle(.plain)
@ -1052,7 +1052,7 @@ private struct PreviewCardButton: View {
var body: some View {
Button(action: action) {
Text("Preview card")
.font(.headline)
.typography(.heading)
.foregroundStyle(.white)
.frame(maxWidth: .infinity)
.padding(Design.Spacing.medium)

View File

@ -118,17 +118,17 @@ private struct EmptyCardsView: View {
Spacer()
Image(systemName: "rectangle.stack.badge.plus")
.font(.system(size: Design.BaseFontSize.display))
.typography(.title2)
.foregroundStyle(Color.Text.secondary)
VStack(spacing: Design.Spacing.small) {
Text("Create your first card")
.font(.title2)
.typography(.title2)
.bold()
.foregroundStyle(Color.Text.primary)
Text("Design and share polished digital business cards for every context.")
.font(.subheadline)
.typography(.subheading)
.foregroundStyle(Color.Text.secondary)
.multilineTextAlignment(.center)
}

View File

@ -49,12 +49,12 @@ struct ActionRowContent: View {
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
Text(title)
.font(.headline)
.typography(.heading)
.foregroundStyle(Color.Text.primary)
if let subtitle {
Text(subtitle)
.font(.subheadline)
.typography(.subheading)
.foregroundStyle(Color.Text.secondary)
}
}

View File

@ -105,19 +105,19 @@ private struct FieldRowPreview: View {
.frame(width: Design.CardSize.avatarSize, height: Design.CardSize.avatarSize)
.overlay(
field.fieldType.iconImage()
.font(.title3)
.typography(.title3)
.foregroundStyle(.white)
)
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
Text(field.value.isEmpty ? field.fieldType.displayName : field.shortDisplayValue)
.font(.subheadline)
.typography(.subheading)
.foregroundStyle(Color.Text.primary)
.lineLimit(1)
if !field.title.isEmpty {
Text(field.title)
.font(.caption)
.typography(.caption)
.foregroundStyle(Color.Text.secondary)
.lineLimit(1)
}
@ -141,7 +141,7 @@ private struct FieldRow: View {
HStack(spacing: Design.Spacing.medium) {
// Drag handle
Image(systemName: "line.3.horizontal")
.font(.caption)
.typography(.caption)
.foregroundStyle(Color.Text.tertiary)
.frame(width: Design.Spacing.large)
@ -151,7 +151,7 @@ private struct FieldRow: View {
.frame(width: Design.CardSize.avatarSize, height: Design.CardSize.avatarSize)
.overlay(
field.fieldType.iconImage()
.font(.title3)
.typography(.title3)
.foregroundStyle(.white)
)
@ -159,12 +159,12 @@ private struct FieldRow: View {
Button(action: onTap) {
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
Text(field.value.isEmpty ? field.fieldType.valuePlaceholder : field.shortDisplayValue)
.font(.subheadline)
.typography(.subheading)
.foregroundStyle(field.value.isEmpty ? Color.Text.secondary : Color.Text.primary)
.lineLimit(1)
Text(field.title.isEmpty ? field.fieldType.displayName : field.title)
.font(.caption)
.typography(.caption)
.foregroundStyle(Color.Text.secondary)
.lineLimit(1)
}
@ -175,7 +175,7 @@ private struct FieldRow: View {
// Delete button
Button(action: onDelete) {
Image(systemName: "xmark.circle.fill")
.font(.title3)
.typography(.title3)
.foregroundStyle(Color.Text.secondary)
}
.buttonStyle(.plain)

View File

@ -68,7 +68,7 @@ private struct AddressTextField: View {
var body: some View {
VStack(alignment: .leading, spacing: Design.Spacing.xSmall) {
Text(label)
.font(.caption)
.typography(.caption)
.foregroundStyle(Color.Text.secondary)
TextField(placeholder, text: $text)
@ -98,7 +98,7 @@ private struct AddressTextField: View {
Section("Preview") {
Text(address.formattedString)
.font(.subheadline)
.typography(.subheading)
}
}
}

View File

@ -12,13 +12,13 @@ struct ContactFieldPickerView: View {
VStack(alignment: .leading, spacing: Design.Spacing.medium) {
HStack {
Text("Tap a field below to add it")
.font(.subheadline)
.typography(.subheading)
.foregroundStyle(Color.Text.primary)
Spacer()
Image(systemName: "plus")
.font(.caption)
.typography(.caption)
.foregroundStyle(Color.Text.secondary)
}
.padding(.horizontal, Design.Spacing.medium)
@ -52,12 +52,12 @@ private struct FieldTypeButton: View {
.frame(width: Design.CardSize.avatarSize, height: Design.CardSize.avatarSize)
.overlay(
fieldType.iconImage()
.font(.title3)
.typography(.title3)
.foregroundStyle(.white)
)
Text(fieldType.displayName)
.font(.caption)
.typography(.caption)
.foregroundStyle(Color.Text.primary)
.multilineTextAlignment(.center)
.lineLimit(2)

View File

@ -88,7 +88,7 @@ struct HeaderLayoutPickerView: View {
dismiss()
} label: {
Text("Confirm layout")
.font(.headline)
.typography(.heading)
.foregroundStyle(.white)
.frame(maxWidth: .infinity)
.padding(.vertical, Design.Spacing.large)
@ -107,7 +107,7 @@ struct HeaderLayoutPickerView: View {
dismiss()
} label: {
Image(systemName: "xmark")
.font(.body)
.typography(.body)
.foregroundStyle(Color.Text.primary)
}
}
@ -206,9 +206,9 @@ private struct LayoutPreviewCard: View {
VStack(spacing: Design.Spacing.xxSmall) {
Image(systemName: "person.fill")
.font(.system(size: Design.BaseFontSize.title))
.typography(.title3)
Text("Profile")
.font(.caption)
.typography(.caption)
.bold()
}
.foregroundStyle(theme.textColor.opacity(Design.Opacity.medium))
@ -227,9 +227,9 @@ private struct LayoutPreviewCard: View {
} else {
VStack(spacing: Design.Spacing.xxSmall) {
Image(systemName: "building.2.fill")
.font(.system(size: Design.BaseFontSize.title))
.typography(.title3)
Text("Logo")
.font(.caption)
.typography(.caption)
.bold()
}
.foregroundStyle(theme.textColor.opacity(Design.Opacity.medium))
@ -249,9 +249,9 @@ private struct LayoutPreviewCard: View {
VStack(spacing: Design.Spacing.xxSmall) {
Image(systemName: "photo.fill")
.font(.system(size: Design.BaseFontSize.title))
.typography(.title3)
Text("Cover")
.font(.caption)
.typography(.caption)
.bold()
}
.foregroundStyle(Color.Text.tertiary)
@ -351,9 +351,9 @@ private struct LayoutPreviewCard: View {
} else {
VStack(spacing: Design.Spacing.xxSmall) {
Image(systemName: "building.2")
.font(.caption)
.typography(.caption)
Text("Logo")
.font(.system(size: 8))
.typography(.caption2)
}
.foregroundStyle(theme.textColor)
.frame(maxWidth: .infinity, maxHeight: .infinity)
@ -375,9 +375,9 @@ private struct LayoutPreviewCard: View {
} else {
VStack(spacing: Design.Spacing.xxSmall) {
Image(systemName: "building.2")
.font(.caption)
.typography(.caption)
Text("Logo")
.font(.system(size: 8))
.typography(.caption2)
}
.foregroundStyle(theme.textColor)
.frame(maxWidth: .infinity, maxHeight: .infinity)
@ -411,9 +411,9 @@ private struct LayoutBadge: View {
var body: some View {
HStack(spacing: Design.Spacing.xSmall) {
Image(systemName: iconName)
.font(.caption2)
.typography(.caption2)
Text(text)
.font(.caption2)
.typography(.caption2)
.bold()
}
.padding(.horizontal, Design.Spacing.small)

View File

@ -11,10 +11,10 @@ struct IconRowView: View {
var body: some View {
HStack(spacing: Design.Spacing.xSmall) {
Image(systemName: systemImage)
.font(.caption)
.typography(.caption)
.foregroundStyle(textColor.opacity(Design.Opacity.heavy))
Text(text)
.font(.caption)
.typography(.caption)
.foregroundStyle(textColor)
.lineLimit(1)
}

View File

@ -121,7 +121,7 @@ struct ImageEditorFlow: View {
onComplete(nil)
} label: {
Image(systemName: "xmark")
.font(.body.bold())
.typography(.bodyEmphasis)
.foregroundStyle(Color.Text.primary)
}
}
@ -319,12 +319,12 @@ private struct OptionRow: View {
Button(action: action) {
HStack(spacing: Design.Spacing.medium) {
Image(systemName: icon)
.font(.body)
.typography(.body)
.foregroundStyle(isDestructive ? Color.red : Color.Text.secondary)
.frame(width: Design.CardSize.socialIconSize)
Text(title)
.font(.body)
.typography(.body)
.foregroundStyle(isDestructive ? Color.red : Color.Text.primary)
Spacer()

View File

@ -9,7 +9,7 @@ struct LabelBadgeView: View {
var body: some View {
Text(String.localized(label))
.font(.caption)
.typography(.caption)
.bold()
.foregroundStyle(textColor)
.padding(.horizontal, Design.Spacing.small)

View File

@ -116,7 +116,7 @@ struct PhotoSourcePicker: View {
dismiss()
} label: {
Image(systemName: "xmark")
.font(.body.bold())
.typography(.bodyEmphasis)
.foregroundStyle(Color.Text.primary)
}
}
@ -139,12 +139,12 @@ private struct OptionRow: View {
Button(action: action) {
HStack(spacing: Design.Spacing.medium) {
Image(systemName: icon)
.font(.body)
.typography(.body)
.foregroundStyle(isDestructive ? Color.red : Color.Text.secondary)
.frame(width: Design.CardSize.socialIconSize)
Text(title)
.font(.body)
.typography(.body)
.foregroundStyle(isDestructive ? Color.red : Color.Text.primary)
Spacer()

View File

@ -40,21 +40,21 @@ struct ContactDetailView: View {
VStack(alignment: .leading, spacing: Design.Spacing.large) {
// Name
Text(contact.name.isEmpty ? String.localized("Contact") : contact.name)
.font(.largeTitle)
.typography(.hero)
.bold()
.foregroundStyle(Color.Text.primary)
// Connection details
VStack(alignment: .leading, spacing: Design.Spacing.small) {
Text("Connection details")
.font(.subheadline)
.typography(.subheading)
.foregroundStyle(Color.Text.tertiary)
HStack(spacing: Design.Spacing.small) {
Image(systemName: "calendar")
.foregroundStyle(Color.Text.primary)
Text(contact.lastSharedDate, format: .dateTime.day().month().year().hour().minute())
.font(.subheadline)
.typography(.subheading)
.foregroundStyle(Color.Text.primary)
}
}
@ -71,7 +71,7 @@ struct ContactDetailView: View {
showingAddTag = true
} label: {
Label(String.localized("Add tag"), systemImage: "plus")
.font(.subheadline)
.typography(.subheading)
.bold()
.foregroundStyle(Color.AppText.inverted)
.padding(.horizontal, Design.Spacing.medium)
@ -87,7 +87,7 @@ struct ContactDetailView: View {
// Notes section
VStack(alignment: .leading, spacing: Design.Spacing.medium) {
Text("Notes")
.font(.headline)
.typography(.heading)
.bold()
.foregroundStyle(Color.Text.primary)
@ -95,7 +95,7 @@ struct ContactDetailView: View {
NotesEmptyState()
} else {
Text(contact.notes)
.font(.body)
.typography(.body)
.foregroundStyle(Color.Text.secondary)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(Design.Spacing.medium)
@ -107,7 +107,7 @@ struct ContactDetailView: View {
showingAddNote = true
} label: {
Label(String.localized("Add note"), systemImage: "plus")
.font(.subheadline)
.typography(.subheading)
.foregroundStyle(Color.Text.primary)
.frame(maxWidth: .infinity)
.padding(.vertical, Design.Spacing.medium)
@ -314,9 +314,9 @@ private struct ContactBannerView: View {
// Initials
VStack(spacing: Design.Spacing.xxSmall) {
Text(String(initials.prefix(1)))
.font(.system(size: Design.BaseFontSize.display, weight: .light))
.typography(.title2)
Text(String(initials.dropFirst().prefix(1)))
.font(.system(size: Design.BaseFontSize.display, weight: .light))
.typography(.title2)
}
.foregroundStyle(Color.white.opacity(Design.Opacity.accent))
}
@ -327,7 +327,7 @@ private struct ContactBannerView: View {
Spacer()
Button(action: onEditPhoto) {
Image(systemName: contact.photoData == nil ? "camera.fill" : "pencil")
.font(.body)
.typography(.body)
.foregroundStyle(.white)
.padding(Design.Spacing.medium)
.background(.ultraThinMaterial)
@ -354,12 +354,12 @@ private struct TagPill: View {
var body: some View {
HStack(spacing: Design.Spacing.xSmall) {
Text(text)
.font(.subheadline)
.typography(.subheading)
Button {
onDelete()
} label: {
Image(systemName: "xmark")
.font(.caption2)
.typography(.caption2)
}
}
.foregroundStyle(Color.Text.primary)
@ -461,7 +461,7 @@ private struct ContactFieldInfoRow: View {
HStack(alignment: .top, spacing: Design.Spacing.medium) {
// Icon circle
field.iconImage()
.font(.body)
.typography(.body)
.foregroundStyle(Color.white)
.frame(width: Design.CardSize.avatarSize, height: Design.CardSize.avatarSize)
.background(Color.CardPalette.coral)
@ -470,12 +470,12 @@ private struct ContactFieldInfoRow: View {
// Text
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
Text(field.displayValue)
.font(.body)
.typography(.body)
.foregroundStyle(Color.Text.primary)
.multilineTextAlignment(.leading)
Text(field.title.isEmpty ? field.displayName : field.title)
.font(.caption)
.typography(.caption)
.foregroundStyle(Color.Text.tertiary)
}
@ -500,7 +500,7 @@ private struct ContactInfoRow: View {
HStack(spacing: Design.Spacing.medium) {
// Icon circle
Image(systemName: icon)
.font(.body)
.typography(.body)
.foregroundStyle(Color.white)
.frame(width: Design.CardSize.avatarSize, height: Design.CardSize.avatarSize)
.background(Color.CardPalette.coral)
@ -509,10 +509,10 @@ private struct ContactInfoRow: View {
// Text
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
Text(value)
.font(.body)
.typography(.body)
.foregroundStyle(Color.Text.primary)
Text(label)
.font(.caption)
.typography(.caption)
.foregroundStyle(Color.Text.tertiary)
}
@ -530,11 +530,11 @@ private struct NotesEmptyState: View {
var body: some View {
VStack(spacing: Design.Spacing.medium) {
Image(systemName: "note.text")
.font(.system(size: Design.BaseFontSize.display))
.typography(.title2)
.foregroundStyle(Color.CardPalette.coral.opacity(Design.Opacity.medium))
Text("Write down a memorable reminder about your contact")
.font(.subheadline)
.typography(.subheading)
.foregroundStyle(Color.Text.tertiary)
.multilineTextAlignment(.center)
}
@ -556,7 +556,7 @@ private struct BottomActionBar: View {
HStack(spacing: Design.Spacing.medium) {
Button(action: onMore) {
Text("More...")
.font(.subheadline)
.typography(.subheading)
.bold()
.foregroundStyle(Color.Text.primary)
.frame(maxWidth: .infinity)
@ -567,7 +567,7 @@ private struct BottomActionBar: View {
Button(action: onAddTag) {
Text("Add tag")
.font(.subheadline)
.typography(.subheading)
.bold()
.foregroundStyle(Color.AppText.inverted)
.frame(maxWidth: .infinity)
@ -578,7 +578,7 @@ private struct BottomActionBar: View {
Button(action: onAddNote) {
Text("Add note")
.font(.subheadline)
.typography(.subheading)
.bold()
.foregroundStyle(Color.AppText.inverted)
.frame(maxWidth: .infinity)

View File

@ -53,15 +53,15 @@ private struct EmptyContactsView: View {
var body: some View {
VStack(spacing: Design.Spacing.large) {
Image(systemName: "person.2.slash")
.font(.system(size: Design.BaseFontSize.display))
.typography(.title2)
.foregroundStyle(Color.Text.secondary)
Text("No contacts yet")
.font(.headline)
.typography(.heading)
.foregroundStyle(Color.Text.primary)
Text("Tap + to add a contact, scan a QR code, or track who you share your card with.")
.font(.subheadline)
.typography(.subheading)
.foregroundStyle(Color.Text.secondary)
.multilineTextAlignment(.center)
.padding(.horizontal, Design.Spacing.xLarge)
@ -126,7 +126,7 @@ private struct ContactsListView: View {
}
} header: {
Text("Shared With")
.font(.headline)
.typography(.heading)
.bold()
}
}
@ -149,25 +149,25 @@ private struct ContactRowView: View {
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
HStack(spacing: Design.Spacing.xSmall) {
Text(contact.name)
.font(.headline)
.typography(.heading)
.foregroundStyle(Color.Text.primary)
if contact.isReceivedCard {
Image(systemName: "arrow.down.circle.fill")
.font(.caption)
.typography(.caption)
.foregroundStyle(Color.Accent.mint)
}
if contact.hasFollowUp {
Image(systemName: contact.isFollowUpOverdue ? "exclamationmark.circle.fill" : "clock.fill")
.font(.caption)
.typography(.caption)
.foregroundStyle(contact.isFollowUpOverdue ? Color.Accent.red : Color.Accent.gold)
}
}
if !contact.role.isEmpty || !contact.company.isEmpty {
Text("\(contact.role)\(contact.role.isEmpty || contact.company.isEmpty ? "" : " · ")\(contact.company)")
.font(.subheadline)
.typography(.subheading)
.foregroundStyle(Color.Text.secondary)
.lineLimit(1)
}
@ -176,7 +176,7 @@ private struct ContactRowView: View {
HStack(spacing: Design.Spacing.xSmall) {
ForEach(contact.tagList.prefix(2), id: \.self) { tag in
Text(tag)
.font(.caption2)
.typography(.caption2)
.padding(.horizontal, Design.Spacing.xSmall)
.padding(.vertical, Design.Spacing.xxSmall)
.background(Color.AppBackground.accent)
@ -190,10 +190,10 @@ private struct ContactRowView: View {
VStack(alignment: .trailing, spacing: Design.Spacing.xxSmall) {
Text(relativeDate)
.font(.caption)
.typography(.caption)
.foregroundStyle(Color.Text.secondary)
Text(String.localized(contact.cardLabel))
.font(.caption)
.typography(.caption)
.padding(.horizontal, Design.Spacing.small)
.padding(.vertical, Design.Spacing.xxSmall)
.background(Color.AppBackground.base)
@ -218,7 +218,7 @@ private struct ContactAvatarView: View {
.clipShape(.rect(cornerRadius: Design.CornerRadius.medium))
} else {
Image(systemName: contact.avatarSystemName)
.font(.title2)
.typography(.title2)
.foregroundStyle(Color.Accent.red)
.frame(width: Design.CardSize.avatarSize, height: Design.CardSize.avatarSize)
.background(Color.AppBackground.accent)

View File

@ -8,10 +8,10 @@ struct EmptyStateView: View {
var body: some View {
VStack(spacing: Design.Spacing.small) {
Text(title)
.font(.headline)
.typography(.heading)
.foregroundStyle(Color.Text.primary)
Text(message)
.font(.subheadline)
.typography(.subheading)
.foregroundStyle(Color.Text.secondary)
.multilineTextAlignment(.center)
}

View File

@ -106,7 +106,7 @@ private struct ScannerOverlayView: View {
Spacer()
Text("Point at a QR code")
.font(.headline)
.typography(.heading)
.foregroundStyle(Color.Text.inverted)
.padding(Design.Spacing.medium)
.background(Color.black.opacity(Design.Opacity.medium))
@ -140,25 +140,25 @@ private struct ScannedResultView: View {
var body: some View {
VStack(spacing: Design.Spacing.xLarge) {
Image(systemName: isVCard ? "person.crop.circle.badge.checkmark" : "qrcode")
.font(.system(size: Design.BaseFontSize.display * 2))
.font(.system(size: Design.IconSize.xxxLarge))
.foregroundStyle(Color.Accent.red)
if isVCard {
VStack(spacing: Design.Spacing.small) {
Text("Card Found!")
.font(.title2)
.typography(.title2)
.bold()
.foregroundStyle(Color.Text.primary)
if let name = parsedName {
Text(name)
.font(.headline)
.typography(.heading)
.foregroundStyle(Color.Text.secondary)
}
}
} else {
Text("QR Code Scanned")
.font(.title2)
.typography(.title2)
.bold()
.foregroundStyle(Color.Text.primary)
}
@ -172,7 +172,7 @@ private struct ScannedResultView: View {
.controlSize(.large)
} else {
Text("This doesn't appear to be a business card QR code.")
.font(.subheadline)
.typography(.subheading)
.foregroundStyle(Color.Text.secondary)
.multilineTextAlignment(.center)
.padding(.horizontal, Design.Spacing.xLarge)

View File

@ -50,7 +50,7 @@ private struct FloatingShareButton: View {
var body: some View {
Button(action: action) {
Image(systemName: "qrcode")
.font(.title2)
.typography(.title2)
.fontWeight(.semibold)
.foregroundStyle(.white)
.frame(width: Design.CardSize.floatingButtonSize, height: Design.CardSize.floatingButtonSize)

View File

@ -2,13 +2,16 @@
// SettingsView.swift
// BusinessCard
//
// App settings screen using Bedrock components.
// App settings screen using Bedrock settings layout contract:
// SettingsCard owns horizontal inset, custom rows use SettingsCardRow,
// and in-card separators use SettingsDivider.
//
import SwiftUI
import Bedrock
struct SettingsView: View {
@Environment(AppState.self) private var appState
@State private var settingsState = SettingsState()
var body: some View {
@ -17,7 +20,7 @@ struct SettingsView: View {
VStack(spacing: Design.Spacing.large) {
// MARK: - About Section
appearanceSection
aboutSection
// MARK: - Debug Section
@ -31,12 +34,44 @@ struct SettingsView: View {
.padding(.horizontal, Design.Spacing.large)
.padding(.top, Design.Spacing.medium)
}
.background(Color(.systemGroupedBackground))
.background(Color.AppBackground.base)
.navigationTitle(String.localized("Settings"))
.navigationBarTitleDisplayMode(.large)
}
}
// MARK: - Appearance Section
private var appearanceSection: some View {
VStack(alignment: .leading, spacing: Design.Spacing.small) {
SettingsSectionHeader(
title: "Appearance",
systemImage: "paintbrush",
accentColor: AppThemeAccent.primary
)
SettingsCard(
backgroundColor: Color.AppBackground.secondary,
borderColor: .clear
) {
SettingsSegmentedPicker(
title: "Theme",
subtitle: "Choose app theme",
options: [
("System", AppAppearance.system),
("Light", AppAppearance.light),
("Dark", AppAppearance.dark)
],
selection: Binding(
get: { appState.appearance },
set: { appState.appearance = $0 }
),
accentColor: AppThemeAccent.primary
)
}
}
}
// MARK: - About Section
private var aboutSection: some View {
@ -48,43 +83,49 @@ struct SettingsView: View {
)
SettingsCard(
backgroundColor: Color(.secondarySystemGroupedBackground),
backgroundColor: Color.AppBackground.secondary,
borderColor: .clear
) {
VStack(alignment: .leading, spacing: Design.Spacing.medium) {
// App Name
SettingsCardRow {
HStack {
Text(settingsState.appName)
.font(.system(size: Design.BaseFontSize.title, weight: .semibold))
.foregroundStyle(.primary)
.typography(.title3Bold)
.foregroundStyle(Color.AppText.primary)
Spacer()
}
}
// Version
SettingsDivider(color: AppBorder.subtle)
SettingsCardRow {
HStack {
Text(String.localized("Version"))
.font(.system(size: Design.BaseFontSize.body, weight: .medium))
.foregroundStyle(.primary)
.typography(.bodyEmphasis)
.foregroundStyle(Color.AppText.primary)
Spacer()
Text(settingsState.versionString)
.font(.system(size: Design.BaseFontSize.body, design: .monospaced))
.foregroundStyle(.secondary)
.typography(.body)
.fontDesign(.monospaced)
.foregroundStyle(Color.AppText.secondary)
}
}
// Developer
SettingsDivider(color: AppBorder.subtle)
SettingsCardRow {
HStack {
Text(String.localized("Developer"))
.font(.system(size: Design.BaseFontSize.body, weight: .medium))
.foregroundStyle(.primary)
.typography(.bodyEmphasis)
.foregroundStyle(Color.AppText.primary)
Spacer()
Text("Matt Bruce")
.font(.system(size: Design.BaseFontSize.body))
.foregroundStyle(.secondary)
.typography(.body)
.foregroundStyle(Color.AppText.secondary)
}
}
}
@ -103,10 +144,9 @@ struct SettingsView: View {
)
SettingsCard(
backgroundColor: Color(.secondarySystemGroupedBackground),
backgroundColor: Color.AppBackground.secondary,
borderColor: .clear
) {
VStack(spacing: Design.Spacing.medium) {
SettingsToggle(
title: "Enable Debug Premium",
subtitle: "Unlock all premium features for testing",
@ -117,18 +157,22 @@ struct SettingsView: View {
accentColor: AppStatus.warning
)
SettingsDivider(color: AppBorder.subtle)
SettingsNavigationRow(
title: "Icon Generator",
subtitle: "Generate and save app icon to Files",
backgroundColor: Color(.tertiarySystemGroupedBackground)
backgroundColor: .clear
) {
IconGeneratorView(config: .businessCard, appName: "BusinessCard")
}
SettingsDivider(color: AppBorder.subtle)
SettingsNavigationRow(
title: "Branding Preview",
subtitle: "Preview app icon and launch screen",
backgroundColor: Color(.tertiarySystemGroupedBackground)
backgroundColor: .clear
) {
BrandingPreviewView(
iconConfig: .businessCard,
@ -139,7 +183,6 @@ struct SettingsView: View {
}
}
}
}
#endif
}

View File

@ -105,7 +105,7 @@ private struct QRCodeSection: View {
// Instruction text
Text("Point your camera at the QR code to receive the card")
.font(.subheadline)
.typography(.subheading)
.foregroundStyle(Color.ShareSheet.secondaryText)
.multilineTextAlignment(.center)
}
@ -127,10 +127,10 @@ private struct AppClipSection: View {
// Header
HStack {
Image(systemName: "app.gift")
.font(.headline)
.typography(.heading)
.foregroundStyle(Color.ShareSheet.text)
Text("App Clip (includes photo)")
.font(.headline)
.typography(.heading)
.foregroundStyle(Color.ShareSheet.text)
}
@ -150,7 +150,7 @@ private struct AppClipSection: View {
// Expiration notice
Text("Expires in 7 days")
.font(.caption)
.typography(.caption)
.foregroundStyle(Color.ShareSheet.secondaryText)
// Reset button
@ -158,7 +158,7 @@ private struct AppClipSection: View {
appClipState.reset()
} label: {
Text("Generate New Link")
.font(.subheadline)
.typography(.subheading)
.foregroundStyle(Color.ShareSheet.text)
}
} else {
@ -170,7 +170,7 @@ private struct AppClipSection: View {
Image(systemName: "qrcode")
Text("Generate App Clip Link")
}
.font(.headline)
.typography(.heading)
.foregroundStyle(Color.ShareSheet.background)
.padding(.horizontal, Design.Spacing.xLarge)
.padding(.vertical, Design.Spacing.medium)
@ -180,7 +180,7 @@ private struct AppClipSection: View {
// Description
Text("Creates a link that opens a mini-app for recipients to preview and save your card with photo.")
.font(.caption)
.typography(.caption)
.foregroundStyle(Color.ShareSheet.secondaryText)
.multilineTextAlignment(.center)
}
@ -188,7 +188,7 @@ private struct AppClipSection: View {
// Error message
if let error = appClipState.errorMessage {
Text(error)
.font(.caption)
.typography(.caption)
.foregroundStyle(.red)
.multilineTextAlignment(.center)
}
@ -279,15 +279,15 @@ private struct EmptyShareState: View {
var body: some View {
VStack(spacing: Design.Spacing.large) {
Image(systemName: "rectangle.on.rectangle.slash")
.font(.system(size: Design.BaseFontSize.display))
.typography(.title2)
.foregroundStyle(Color.ShareSheet.secondaryText)
Text("No card selected")
.font(.headline)
.typography(.heading)
.foregroundStyle(Color.ShareSheet.text)
Text("Choose a card in the My Cards tab to start sharing.")
.font(.subheadline)
.typography(.subheading)
.foregroundStyle(Color.ShareSheet.secondaryText)
.multilineTextAlignment(.center)
}
@ -305,7 +305,7 @@ private struct RowContent: View {
var body: some View {
HStack(spacing: Design.Spacing.medium) {
Image(systemName: systemImage)
.font(.body)
.typography(.body)
.foregroundStyle(iconColor)
.frame(width: Design.Spacing.xLarge)

View File

@ -280,7 +280,7 @@ private struct ContactPhotoRow: View {
.scaledToFill()
} else {
Image(systemName: "person.crop.circle.fill")
.font(.system(size: Design.BaseFontSize.display))
.typography(.title2)
.foregroundStyle(Color.Text.tertiary)
}
}
@ -290,18 +290,18 @@ private struct ContactPhotoRow: View {
VStack(alignment: .leading, spacing: Design.Spacing.xSmall) {
Text("Profile Photo")
.font(.subheadline)
.typography(.subheading)
.foregroundStyle(Color.Text.primary)
Text(photoData == nil ? String.localized("Add a photo") : String.localized("Tap to change"))
.font(.caption)
.typography(.caption)
.foregroundStyle(Color.Text.secondary)
}
Spacer()
Image(systemName: "chevron.right")
.font(.caption)
.typography(.caption)
.foregroundStyle(Color.Text.tertiary)
}
.padding(.vertical, Design.Spacing.xSmall)
@ -342,7 +342,7 @@ private struct LabeledFieldRow: View {
Text(entry.label)
.foregroundStyle(Color.accentColor)
Image(systemName: "chevron.up.chevron.down")
.font(.caption2)
.typography(.caption2)
.foregroundStyle(Color.secondary)
}
}

View File

@ -76,7 +76,7 @@ struct ContactFieldEditorSheet: View {
} else {
VStack(alignment: .leading, spacing: Design.Spacing.small) {
Text(fieldType.valueLabel)
.font(.subheadline)
.typography(.subheading)
.foregroundStyle(Color.Text.primary)
TextField(fieldType.valuePlaceholder, text: $value)
@ -91,7 +91,7 @@ struct ContactFieldEditorSheet: View {
// Title field
VStack(alignment: .leading, spacing: Design.Spacing.small) {
Text("Title (optional)")
.font(.subheadline)
.typography(.subheading)
.foregroundStyle(Color.Text.primary)
TextField(String(localized: "e.g. Work, Personal"), text: $title)
@ -102,7 +102,7 @@ struct ContactFieldEditorSheet: View {
if !fieldType.titleSuggestions.isEmpty {
VStack(alignment: .leading, spacing: Design.Spacing.small) {
Text("Here are some suggestions for your title:")
.font(.caption)
.typography(.caption)
.foregroundStyle(Color.Text.secondary)
FlowLayout(spacing: Design.Spacing.small) {
@ -191,12 +191,12 @@ private struct FieldHeaderView: View {
.frame(width: Design.CardSize.avatarSize, height: Design.CardSize.avatarSize)
.overlay(
fieldType.iconImage()
.font(.title3)
.typography(.title3)
.foregroundStyle(.white)
)
Text(fieldType.displayName)
.font(.headline)
.typography(.heading)
.foregroundStyle(Color.Text.primary)
Spacer()
@ -215,7 +215,7 @@ private struct SuggestionChip: View {
var body: some View {
Button(action: action) {
Text(text)
.font(.subheadline)
.typography(.subheading)
.padding(.horizontal, Design.Spacing.medium)
.padding(.vertical, Design.Spacing.small)
.background(Color.AppBackground.elevated)

View File

@ -47,7 +47,7 @@ struct LogoEditorSheet: View {
onComplete(nil)
} label: {
Image(systemName: "xmark")
.font(.body.bold())
.typography(.bodyEmphasis)
.foregroundStyle(Color.Text.primary)
}
}
@ -106,19 +106,19 @@ struct LogoEditorSheet: View {
private var zoomSliderSection: some View {
VStack(alignment: .leading, spacing: Design.Spacing.small) {
Text("Zoom in/out")
.font(.subheadline)
.typography(.subheading)
.foregroundStyle(Color.Text.secondary)
HStack(spacing: Design.Spacing.medium) {
Image(systemName: "minus.magnifyingglass")
.font(.body)
.typography(.body)
.foregroundStyle(Color.Text.tertiary)
Slider(value: $zoomScale, in: minZoom...maxZoom)
.tint(Color.accentColor)
Image(systemName: "plus.magnifyingglass")
.font(.body)
.typography(.body)
.foregroundStyle(Color.Text.tertiary)
}
}
@ -129,13 +129,13 @@ struct LogoEditorSheet: View {
private var backgroundColorSection: some View {
VStack(alignment: .leading, spacing: Design.Spacing.medium) {
Text("Change background color")
.font(.subheadline)
.typography(.subheading)
.foregroundStyle(Color.Text.secondary)
HStack(spacing: Design.Spacing.large) {
// Suggested label and colors
Text("Suggested")
.font(.subheadline)
.typography(.subheading)
.foregroundStyle(Color.Text.primary)
HStack(spacing: Design.Spacing.small) {
@ -158,7 +158,7 @@ struct LogoEditorSheet: View {
} label: {
HStack(spacing: Design.Spacing.small) {
Text("Custom color")
.font(.subheadline)
.typography(.subheading)
.foregroundStyle(Color.Text.primary)
ColorSwatchButton(
@ -171,7 +171,7 @@ struct LogoEditorSheet: View {
Spacer()
Image(systemName: "chevron.right")
.font(.caption)
.typography(.caption)
.foregroundStyle(Color.Text.tertiary)
}
.contentShape(.rect)

View File

@ -29,7 +29,7 @@ struct RecordContactSheet: View {
Section {
Text("This person will appear in your Contacts tab so you can track who has your card.")
.font(.footnote)
.typography(.footnote)
.foregroundStyle(Color.Text.secondary)
}
}

View File

@ -10,7 +10,7 @@ struct WidgetsView: View {
ScrollView {
VStack(spacing: Design.Spacing.large) {
Text("Share using widgets on your phone or watch")
.font(.title2)
.typography(.title2)
.bold()
.foregroundStyle(Color.Text.primary)
@ -44,7 +44,7 @@ private struct PhoneWidgetPreview: View {
var body: some View {
VStack(alignment: .leading, spacing: Design.Spacing.medium) {
Text("Phone Widget")
.font(.headline)
.typography(.heading)
.bold()
.foregroundStyle(Color.Text.primary)
@ -54,13 +54,13 @@ private struct PhoneWidgetPreview: View {
VStack(alignment: .leading, spacing: Design.Spacing.xSmall) {
Text(card.fullName)
.font(.headline)
.typography(.heading)
.foregroundStyle(Color.Text.primary)
Text(card.role)
.font(.subheadline)
.typography(.subheading)
.foregroundStyle(Color.Text.secondary)
Text("Tap to share")
.font(.caption)
.typography(.caption)
.foregroundStyle(Color.Text.secondary)
}
}
@ -77,7 +77,7 @@ private struct WatchWidgetPreview: View {
var body: some View {
VStack(alignment: .leading, spacing: Design.Spacing.medium) {
Text("Watch Widget")
.font(.headline)
.typography(.heading)
.bold()
.foregroundStyle(Color.Text.primary)
@ -87,10 +87,10 @@ private struct WatchWidgetPreview: View {
VStack(alignment: .leading, spacing: Design.Spacing.xSmall) {
Text("Ready to scan")
.font(.subheadline)
.typography(.subheading)
.foregroundStyle(Color.Text.secondary)
Text("Open on Apple Watch")
.font(.caption)
.typography(.caption)
.foregroundStyle(Color.Text.secondary)
}
}

View File

@ -141,6 +141,15 @@ The app uses the [Bedrock](ssh://git@192.168.1.128:220/mbrucedogs/Bedrock.git) p
App-specific extensions are in `Design/DesignConstants.swift`.
### Settings Layout Contract
`SettingsCard` is the single owner of horizontal row inset in `SettingsView`.
- Use `SettingsCardRow` for custom in-card rows (`HStack`, status/info rows, custom content).
- Use `SettingsDivider` between in-card rows (instead of `Divider`/manual lines).
- Use `SettingsNavigationRow(..., backgroundColor: .clear)` for standard in-card navigation rows.
- Avoid child `.padding(.horizontal, ...)` inside `SettingsCard` unless intentional indentation is required.
## Project Structure
```