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

View File

@ -7,17 +7,17 @@
<key>BusinessCard.xcscheme_^#shared#^_</key> <key>BusinessCard.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>1</integer> <integer>2</integer>
</dict> </dict>
<key>BusinessCardClip.xcscheme_^#shared#^_</key> <key>BusinessCardClip.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>2</integer> <integer>0</integer>
</dict> </dict>
<key>BusinessCardWatch Watch App.xcscheme_^#shared#^_</key> <key>BusinessCardWatch Watch App.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>3</integer> <integer>1</integer>
</dict> </dict>
</dict> </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() { init() {
let schema = Schema([BusinessCard.self, Contact.self, ContactField.self]) 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) // Primary strategy: App Group for watch sync (without CloudKit for now)
// CloudKit can be enabled once properly configured in Xcode // CloudKit can be enabled once properly configured in Xcode
var container: ModelContainer? var container: ModelContainer?
@ -80,7 +89,7 @@ struct BusinessCardApp: App {
AppLaunchView(config: .businessCard) { AppLaunchView(config: .businessCard) {
RootTabView() RootTabView()
.environment(appState) .environment(appState)
.preferredColorScheme(.light) .preferredColorScheme(appState.preferredColorScheme)
} }
} }
} }

View File

@ -2,8 +2,8 @@
// BusinessCardTheme.swift // BusinessCardTheme.swift
// BusinessCard // BusinessCard
// //
// App-specific theme conforming to Bedrock's color protocols. // App-specific adaptive theme conforming to Bedrock's color protocols.
// This light theme uses warm, professional tones. // Uses warm light colors and deep slate dark colors.
// //
import SwiftUI import SwiftUI
@ -11,39 +11,39 @@ import Bedrock
// MARK: - Surface Colors // 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 { public enum BusinessCardSurfaceColors: SurfaceColorProvider {
/// Primary background - warm off-white base /// Primary background
public static let primary = Color(red: 0.97, green: 0.96, blue: 0.94) public static let primary = Color.AppBackground.base
/// Secondary/elevated surface /// 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 /// 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) /// 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 /// 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 /// 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 /// 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 // MARK: - Text Colors
public enum BusinessCardTextColors: TextColorProvider { public enum BusinessCardTextColors: TextColorProvider {
public static let primary = Color(red: 0.14, green: 0.14, blue: 0.17) public static let primary = Color.AppText.primary
public static let secondary = Color(red: 0.32, green: 0.34, blue: 0.40) public static let secondary = Color.AppText.secondary
public static let tertiary = Color(red: 0.56, green: 0.58, blue: 0.62) public static let tertiary = Color.AppText.tertiary
public static let disabled = Color(red: 0.70, green: 0.72, blue: 0.75) public static let disabled = Color.AppText.tertiary.opacity(Design.Opacity.strong)
public static let placeholder = Color(red: 0.60, green: 0.62, blue: 0.66) public static let placeholder = Color.AppText.tertiary
public static let inverse = Color(red: 0.98, green: 0.98, blue: 0.98) public static let inverse = Color.AppText.inverted
} }
// MARK: - Accent Colors // MARK: - Accent Colors
@ -84,9 +84,9 @@ public enum BusinessCardStatusColors: StatusColorProvider {
// MARK: - Border Colors // MARK: - Border Colors
public enum BusinessCardBorderColors: BorderColorProvider { 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 subtle = Color.AppText.tertiary.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 standard = Color.AppText.tertiary.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 emphasized = Color.AppText.secondary.opacity(Design.Opacity.light)
public static let selected = BusinessCardAccentColors.primary.opacity(Design.Opacity.medium) 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. /// BusinessCard's light theme color palette.
/// Uses warm, professional tones suitable for a business card app. /// Uses warm, professional tones suitable for a business card app.
extension Color { extension Color {
// MARK: - App Backgrounds
// MARK: - App Backgrounds (Light Theme)
enum AppBackground { enum AppBackground {
static let base = Color(red: 0.97, green: 0.96, blue: 0.94) static let base = Color("AppBackgroundBase")
static let secondary = Color(red: 0.95, green: 0.95, blue: 0.95) static let secondary = Color("AppBackgroundSecondary")
static let elevated = Color(red: 1.0, green: 1.0, blue: 1.0) static let elevated = Color("AppBackgroundElevated")
static let card = Color(red: 1.0, green: 1.0, blue: 1.0) static let card = elevated
static let accent = Color(red: 0.95, green: 0.91, blue: 0.86) static let accent = Color("AppBackgroundAccent")
} }
// MARK: - Card Theme Palette // MARK: - Card Theme Palette
@ -142,13 +141,13 @@ extension Color {
static let slate = Color(red: 0.29, green: 0.33, blue: 0.4) 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 { enum AppText {
static let primary = Color(red: 0.14, green: 0.14, blue: 0.17) static let primary = Color("AppTextPrimary")
static let secondary = Color(red: 0.32, green: 0.34, blue: 0.4) static let secondary = Color("AppTextSecondary")
static let tertiary = Color(red: 0.56, green: 0.58, blue: 0.62) static let tertiary = Color("AppTextTertiary")
static let inverted = Color(red: 0.98, green: 0.98, blue: 0.98) static let inverted = Color("AppTextInverted")
} }
// MARK: - Badge Colors // MARK: - Badge Colors
@ -158,14 +157,14 @@ extension Color {
static let neutral = Color(red: 0.89, green: 0.89, blue: 0.9) static let neutral = Color(red: 0.89, green: 0.89, blue: 0.9)
} }
// MARK: - Share Sheet Dark Theme // MARK: - Share Sheet Theme
enum ShareSheet { enum ShareSheet {
static let background = Color(red: 0.18, green: 0.20, blue: 0.23) static let background = Color("ShareSheetBackground")
static let cardBackground = Color(red: 0.24, green: 0.26, blue: 0.30) static let cardBackground = Color("ShareSheetCardBackground")
static let rowBackground = Color(red: 0.30, green: 0.32, blue: 0.36) static let rowBackground = Color("ShareSheetRowBackground")
static let text = Color(red: 0.96, green: 0.96, blue: 0.97) static let text = Color("ShareSheetText")
static let secondaryText = Color(red: 0.70, green: 0.72, blue: 0.75) static let secondaryText = Color("ShareSheetSecondaryText")
} }
// MARK: - Social Media Brand Colors // MARK: - Social Media Brand Colors

View File

@ -1,19 +1,53 @@
import Foundation import Foundation
import Observation import Observation
import SwiftData 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 @Observable
@MainActor @MainActor
final class AppState { final class AppState {
private enum DefaultsKey {
static let appearance = "appAppearance"
}
var selectedTab: AppTab = .cards var selectedTab: AppTab = .cards
var cardStore: CardStore var cardStore: CardStore
var contactsStore: ContactsStore var contactsStore: ContactsStore
let shareLinkService: ShareLinkProviding let shareLinkService: ShareLinkProviding
var appearance: AppAppearance {
didSet {
UserDefaults.standard.set(appearance.rawValue, forKey: DefaultsKey.appearance)
}
}
var preferredColorScheme: ColorScheme? {
appearance.preferredColorScheme
}
init(modelContext: ModelContext) { init(modelContext: ModelContext) {
self.cardStore = CardStore(modelContext: modelContext) self.cardStore = CardStore(modelContext: modelContext)
self.contactsStore = ContactsStore(modelContext: modelContext) self.contactsStore = ContactsStore(modelContext: modelContext)
self.shareLinkService = ShareLinkService() 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) // Clean up expired shared cards on launch (best-effort, non-blocking)
Task { Task {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,13 +2,16 @@
// SettingsView.swift // SettingsView.swift
// BusinessCard // 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 SwiftUI
import Bedrock import Bedrock
struct SettingsView: View { struct SettingsView: View {
@Environment(AppState.self) private var appState
@State private var settingsState = SettingsState() @State private var settingsState = SettingsState()
var body: some View { var body: some View {
@ -17,7 +20,7 @@ struct SettingsView: View {
VStack(spacing: Design.Spacing.large) { VStack(spacing: Design.Spacing.large) {
// MARK: - About Section // MARK: - About Section
appearanceSection
aboutSection aboutSection
// MARK: - Debug Section // MARK: - Debug Section
@ -31,12 +34,44 @@ struct SettingsView: View {
.padding(.horizontal, Design.Spacing.large) .padding(.horizontal, Design.Spacing.large)
.padding(.top, Design.Spacing.medium) .padding(.top, Design.Spacing.medium)
} }
.background(Color(.systemGroupedBackground)) .background(Color.AppBackground.base)
.navigationTitle(String.localized("Settings")) .navigationTitle(String.localized("Settings"))
.navigationBarTitleDisplayMode(.large) .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 // MARK: - About Section
private var aboutSection: some View { private var aboutSection: some View {
@ -48,43 +83,49 @@ struct SettingsView: View {
) )
SettingsCard( SettingsCard(
backgroundColor: Color(.secondarySystemGroupedBackground), backgroundColor: Color.AppBackground.secondary,
borderColor: .clear borderColor: .clear
) { ) {
VStack(alignment: .leading, spacing: Design.Spacing.medium) { SettingsCardRow {
// App Name
HStack { HStack {
Text(settingsState.appName) Text(settingsState.appName)
.font(.system(size: Design.BaseFontSize.title, weight: .semibold)) .typography(.title3Bold)
.foregroundStyle(.primary) .foregroundStyle(Color.AppText.primary)
Spacer() Spacer()
} }
}
// Version SettingsDivider(color: AppBorder.subtle)
SettingsCardRow {
HStack { HStack {
Text(String.localized("Version")) Text(String.localized("Version"))
.font(.system(size: Design.BaseFontSize.body, weight: .medium)) .typography(.bodyEmphasis)
.foregroundStyle(.primary) .foregroundStyle(Color.AppText.primary)
Spacer() Spacer()
Text(settingsState.versionString) Text(settingsState.versionString)
.font(.system(size: Design.BaseFontSize.body, design: .monospaced)) .typography(.body)
.foregroundStyle(.secondary) .fontDesign(.monospaced)
.foregroundStyle(Color.AppText.secondary)
}
} }
// Developer SettingsDivider(color: AppBorder.subtle)
SettingsCardRow {
HStack { HStack {
Text(String.localized("Developer")) Text(String.localized("Developer"))
.font(.system(size: Design.BaseFontSize.body, weight: .medium)) .typography(.bodyEmphasis)
.foregroundStyle(.primary) .foregroundStyle(Color.AppText.primary)
Spacer() Spacer()
Text("Matt Bruce") Text("Matt Bruce")
.font(.system(size: Design.BaseFontSize.body)) .typography(.body)
.foregroundStyle(.secondary) .foregroundStyle(Color.AppText.secondary)
} }
} }
} }
@ -103,10 +144,9 @@ struct SettingsView: View {
) )
SettingsCard( SettingsCard(
backgroundColor: Color(.secondarySystemGroupedBackground), backgroundColor: Color.AppBackground.secondary,
borderColor: .clear borderColor: .clear
) { ) {
VStack(spacing: Design.Spacing.medium) {
SettingsToggle( SettingsToggle(
title: "Enable Debug Premium", title: "Enable Debug Premium",
subtitle: "Unlock all premium features for testing", subtitle: "Unlock all premium features for testing",
@ -117,18 +157,22 @@ struct SettingsView: View {
accentColor: AppStatus.warning accentColor: AppStatus.warning
) )
SettingsDivider(color: AppBorder.subtle)
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: Color(.tertiarySystemGroupedBackground) backgroundColor: .clear
) { ) {
IconGeneratorView(config: .businessCard, appName: "BusinessCard") IconGeneratorView(config: .businessCard, appName: "BusinessCard")
} }
SettingsDivider(color: AppBorder.subtle)
SettingsNavigationRow( SettingsNavigationRow(
title: "Branding Preview", title: "Branding Preview",
subtitle: "Preview app icon and launch screen", subtitle: "Preview app icon and launch screen",
backgroundColor: Color(.tertiarySystemGroupedBackground) backgroundColor: .clear
) { ) {
BrandingPreviewView( BrandingPreviewView(
iconConfig: .businessCard, iconConfig: .businessCard,
@ -139,7 +183,6 @@ struct SettingsView: View {
} }
} }
} }
}
#endif #endif
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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