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

This commit is contained in:
Matt Bruce 2026-02-11 17:24:11 -06:00
parent 97d8b648e4
commit 9233d779a3
62 changed files with 911 additions and 237 deletions

View File

@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.250",
"green" : "0.750",
"red" : "0.950"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.140",
"green" : "0.120",
"red" : "0.120"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.550",
"green" : "0.650",
"red" : "0.200"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.280",
"green" : "0.330",
"red" : "0.950"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.400",
"green" : "0.330",
"red" : "0.290"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.900",
"green" : "0.890",
"red" : "0.890"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.340",
"green" : "0.820",
"red" : "0.980"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "1.000",
"green" : "1.000",
"red" : "1.000"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.278",
"green" : "0.329",
"red" : "0.949"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.150",
"green" : "0.180",
"red" : "0.650"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.280",
"green" : "0.750",
"red" : "0.980"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.330",
"green" : "0.350",
"red" : "0.950"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.310",
"green" : "0.370",
"red" : "0.130"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.340",
"green" : "0.820",
"red" : "0.730"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.220",
"green" : "0.160",
"red" : "0.120"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.560",
"green" : "0.450",
"red" : "0.080"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.520",
"green" : "0.270",
"red" : "0.560"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.730",
"green" : "0.680",
"red" : "0.950"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.680",
"green" : "0.830",
"red" : "0.930"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.500",
"green" : "0.440",
"red" : "0.380"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.620",
"green" : "0.360",
"red" : "0.420"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.350",
"green" : "0.820",
"red" : "0.000"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.700",
"green" : "0.400",
"red" : "0.260"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.130",
"green" : "0.130",
"red" : "0.130"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.420",
"green" : "0.190",
"red" : "0.880"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.710",
"green" : "0.470",
"red" : "0.000"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.890",
"green" : "0.630",
"red" : "0.160"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.000",
"green" : "0.000",
"red" : "0.000"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.000",
"green" : "0.000",
"red" : "0.000"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.950",
"green" : "0.630",
"red" : "0.110"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.790",
"green" : "0.530",
"red" : "0.220"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.380",
"green" : "0.680",
"red" : "0.150"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -15,13 +15,13 @@ extension Color {
enum Branding { enum Branding {
/// Primary gradient color (warm coral/red). /// Primary gradient color (warm coral/red).
/// Must match LaunchBackground.colorset exactly to prevent flash. /// Must match LaunchBackground.colorset exactly to prevent flash.
static let primary = Color(red: 0.949, green: 0.329, blue: 0.278) static let primary = Color("BrandingPrimary")
/// Secondary gradient color (darker red). /// Secondary gradient color (darker red).
static let secondary = Color(red: 0.65, green: 0.18, blue: 0.15) static let secondary = Color("BrandingSecondary")
/// Accent color for icons and highlights. /// Accent color for icons and highlights.
static let accent = Color.white static let accent = Color("BrandingAccent")
} }
} }
@ -54,7 +54,7 @@ extension LaunchScreenConfig {
primaryColor: Color.Branding.primary, primaryColor: Color.Branding.primary,
secondaryColor: Color.Branding.secondary, secondaryColor: Color.Branding.secondary,
accentColor: Color.Branding.accent, accentColor: Color.Branding.accent,
titleColor: .white, titleColor: Color.Branding.accent,
iconSize: 52, iconSize: 52,
titleSize: 32, titleSize: 32,
iconSpacing: 12, iconSpacing: 12,

View File

@ -2,8 +2,8 @@ import SwiftUI
import Bedrock import Bedrock
public enum BusinessCardAccentColors: AccentColorProvider { public enum BusinessCardAccentColors: AccentColorProvider {
public static let primary = Color(red: 0.95, green: 0.33, blue: 0.28) public static let primary = Color.Accent.red
public static let light = Color(red: 0.98, green: 0.50, blue: 0.45) public static let light = Color("CardPaletteRose")
public static let dark = Color(red: 0.75, green: 0.25, blue: 0.22) public static let dark = Color("BrandingSecondary")
public static let secondary = Color(red: 0.12, green: 0.12, blue: 0.14) public static let secondary = Color.Accent.ink
} }

View File

@ -2,9 +2,9 @@ import SwiftUI
import Bedrock import Bedrock
public enum BusinessCardButtonColors: ButtonColorProvider { public enum BusinessCardButtonColors: ButtonColorProvider {
public static let primaryLight = Color(red: 0.98, green: 0.45, blue: 0.40) public static let primaryLight = Color("CardPaletteRose")
public static let primaryDark = Color(red: 0.85, green: 0.28, blue: 0.24) public static let primaryDark = Color("BrandingSecondary")
public static let secondary = Color(red: 0.14, green: 0.14, blue: 0.17).opacity(Design.Opacity.subtle) public static let secondary = Color.AppBackground.accent.opacity(Design.Opacity.subtle)
public static let destructive = Color.red.opacity(Design.Opacity.heavy) public static let destructive = Color.Accent.red.opacity(Design.Opacity.heavy)
public static let cancelText = Color(red: 0.32, green: 0.34, blue: 0.40) public static let cancelText = Color.Text.secondary
} }

View File

@ -3,7 +3,7 @@ import Bedrock
public enum BusinessCardInteractiveColors: InteractiveColorProvider { public enum BusinessCardInteractiveColors: InteractiveColorProvider {
public static let selected = BusinessCardAccentColors.primary.opacity(Design.Opacity.selection) public static let selected = BusinessCardAccentColors.primary.opacity(Design.Opacity.selection)
public static let hover = Color(red: 0.14, green: 0.14, blue: 0.17).opacity(Design.Opacity.subtle) public static let hover = Color.AppBackground.accent.opacity(Design.Opacity.subtle)
public static let pressed = Color(red: 0.14, green: 0.14, blue: 0.17).opacity(Design.Opacity.hint) public static let pressed = Color.AppBackground.accent.opacity(Design.Opacity.hint)
public static let focus = BusinessCardAccentColors.light public static let focus = BusinessCardAccentColors.light
} }

View File

@ -2,8 +2,8 @@ import SwiftUI
import Bedrock import Bedrock
public enum BusinessCardStatusColors: StatusColorProvider { public enum BusinessCardStatusColors: StatusColorProvider {
public static let success = Color(red: 0.2, green: 0.75, blue: 0.4) public static let success = Color.Accent.mint
public static let warning = Color(red: 0.95, green: 0.75, blue: 0.25) public static let warning = Color.Accent.gold
public static let error = Color(red: 0.9, green: 0.3, blue: 0.3) public static let error = Color.Accent.red
public static let info = Color(red: 0.3, green: 0.6, blue: 0.9) public static let info = Color("SocialTwitter")
} }

View File

@ -91,6 +91,42 @@ extension Design {
DeviceSizeTier.isTablet ? 500 : nil DeviceSizeTier.isTablet ? 500 : nil
} }
} }
/// Contacts feature constants (list behavior, row layout, note UI).
enum Contacts {
static let detailFieldIconWidth: CGFloat = 28
static let detailHeaderAvatarSize: CGFloat = 92
static let sectionIndexMinContacts = 20
static let sectionIndexMinSections = 6
static let sectionIndexLetterSpacing: CGFloat = 2
static let sectionIndexLetterWidth: CGFloat = 16
static let sectionIndexLetterHeight: CGFloat = 12
static let sectionIndexScrollAnimationDuration: Double = 0.2
static let notePreviewLineLimit = 2
}
/// Image color extraction constants.
enum ColorExtraction {
static let sampleSize = 50
static let rgbaBytesPerPixel = 4
static let alphaVisibilityThreshold: UInt8 = 128
static let minBrightness = 30
static let maxBrightness = 225
static let bucketQuantizationStep = 32
static let rgbDenominator = 255.0
static let colorSimilarityThreshold = 0.15
}
/// Card theme math constants.
enum ThemeMath {
static let luminanceRedWeight = 0.299
static let luminanceGreenWeight = 0.587
static let luminanceBlueWeight = 0.114
static let requiresDarkTextThreshold = 0.5
static let darkThemeLuminanceThreshold = 0.3
static let customLightenAmount = 0.15
static let customDarkenAmount = 0.12
}
} }
// MARK: - Shadow Extensions // MARK: - Shadow Extensions
@ -118,27 +154,27 @@ extension Color {
// MARK: - Card Theme Palette // MARK: - Card Theme Palette
enum CardPalette { enum CardPalette {
static let coral = Color(red: 0.95, green: 0.35, blue: 0.33) static let coral = Color("CardPaletteCoral")
static let midnight = Color(red: 0.12, green: 0.16, blue: 0.22) static let midnight = Color("CardPaletteMidnight")
static let ocean = Color(red: 0.08, green: 0.45, blue: 0.56) static let ocean = Color("CardPaletteOcean")
static let lime = Color(red: 0.73, green: 0.82, blue: 0.34) static let lime = Color("CardPaletteLime")
static let violet = Color(red: 0.42, green: 0.36, blue: 0.62) static let violet = Color("CardPaletteViolet")
static let forest = Color(red: 0.13, green: 0.37, blue: 0.31) static let forest = Color("CardPaletteForest")
static let rose = Color(red: 0.95, green: 0.68, blue: 0.73) static let rose = Color("CardPaletteRose")
static let slate = Color(red: 0.38, green: 0.44, blue: 0.50) static let slate = Color("CardPaletteSlate")
static let amber = Color(red: 0.98, green: 0.75, blue: 0.28) static let amber = Color("CardPaletteAmber")
static let plum = Color(red: 0.56, green: 0.27, blue: 0.52) static let plum = Color("CardPalettePlum")
static let sand = Color(red: 0.93, green: 0.83, blue: 0.68) static let sand = Color("CardPaletteSand")
} }
// MARK: - App Accent Colors // MARK: - App Accent Colors
enum AppAccent { enum AppAccent {
static let red = Color(red: 0.95, green: 0.33, blue: 0.28) static let red = Color("AppAccentRed")
static let gold = Color(red: 0.95, green: 0.75, blue: 0.25) static let gold = Color("AppAccentGold")
static let mint = Color(red: 0.2, green: 0.65, blue: 0.55) static let mint = Color("AppAccentMint")
static let ink = Color(red: 0.12, green: 0.12, blue: 0.14) static let ink = Color("AppAccentInk")
static let slate = Color(red: 0.29, green: 0.33, blue: 0.4) static let slate = Color("AppAccentSlate")
} }
// MARK: - App Text Colors // MARK: - App Text Colors
@ -153,8 +189,8 @@ extension Color {
// MARK: - Badge Colors // MARK: - Badge Colors
enum Badge { enum Badge {
static let star = Color(red: 0.98, green: 0.82, blue: 0.34) static let star = Color("BadgeStar")
static let neutral = Color(red: 0.89, green: 0.89, blue: 0.9) static let neutral = Color("BadgeNeutral")
} }
// MARK: - Share Sheet Theme // MARK: - Share Sheet Theme
@ -170,17 +206,17 @@ extension Color {
// MARK: - Social Media Brand Colors // MARK: - Social Media Brand Colors
enum Social { enum Social {
static let linkedIn = Color(red: 0.0, green: 0.47, blue: 0.71) static let linkedIn = Color("SocialLinkedIn")
static let twitter = Color(red: 0.11, green: 0.63, blue: 0.95) static let twitter = Color("SocialTwitter")
static let instagram = Color(red: 0.88, green: 0.19, blue: 0.42) static let instagram = Color("SocialInstagram")
static let facebook = Color(red: 0.26, green: 0.40, blue: 0.70) static let facebook = Color("SocialFacebook")
static let tiktok = Color(red: 0.0, green: 0.0, blue: 0.0) static let tiktok = Color("SocialTikTok")
static let github = Color(red: 0.13, green: 0.13, blue: 0.13) static let github = Color("SocialGitHub")
static let threads = Color(red: 0.0, green: 0.0, blue: 0.0) static let threads = Color("SocialThreads")
static let telegram = Color(red: 0.16, green: 0.63, blue: 0.89) static let telegram = Color("SocialTelegram")
static let whatsapp = Color(red: 0.15, green: 0.68, blue: 0.38) static let whatsapp = Color("SocialWhatsApp")
static let venmo = Color(red: 0.22, green: 0.53, blue: 0.79) static let venmo = Color("SocialVenmo")
static let cashApp = Color(red: 0.0, green: 0.82, blue: 0.35) static let cashApp = Color("SocialCashApp")
} }
} }

View File

@ -1,4 +1,5 @@
import SwiftUI import SwiftUI
import Bedrock
/// Card theme configuration supporting both preset themes and custom colors. /// Card theme configuration supporting both preset themes and custom colors.
/// ///
@ -88,8 +89,11 @@ struct CardTheme: Identifiable, Hashable, Sendable {
var requiresDarkText: Bool { var requiresDarkText: Bool {
if let rgb = customRGB { if let rgb = customRGB {
// Calculate perceived luminance for custom colors // Calculate perceived luminance for custom colors
let luminance = 0.299 * rgb.0 + 0.587 * rgb.1 + 0.114 * rgb.2 let luminance =
return luminance > 0.5 Design.ThemeMath.luminanceRedWeight * rgb.0
+ Design.ThemeMath.luminanceGreenWeight * rgb.1
+ Design.ThemeMath.luminanceBlueWeight * rgb.2
return luminance > Design.ThemeMath.requiresDarkTextThreshold
} }
guard let preset else { return false } guard let preset else { return false }
switch preset { switch preset {
@ -100,106 +104,82 @@ struct CardTheme: Identifiable, Hashable, Sendable {
} }
} }
// MARK: - RGB Values
private var primaryRGB: (Double, Double, Double) {
if let rgb = customRGB { return rgb }
guard let preset else { return (0.95, 0.35, 0.33) }
switch preset {
case .coral: return (0.95, 0.35, 0.33)
case .midnight: return (0.12, 0.16, 0.22)
case .ocean: return (0.08, 0.45, 0.56)
case .lime: return (0.73, 0.82, 0.34)
case .violet: return (0.42, 0.36, 0.62)
case .forest: return (0.13, 0.37, 0.31)
case .rose: return (0.95, 0.68, 0.73)
case .slate: return (0.38, 0.44, 0.50)
case .amber: return (0.98, 0.75, 0.28)
case .plum: return (0.56, 0.27, 0.52)
}
}
private var secondaryRGB: (Double, Double, Double) {
if let rgb = customRGB {
// Calculate luminance to determine if we should lighten or darken
let luminance = 0.299 * rgb.0 + 0.587 * rgb.1 + 0.114 * rgb.2
if luminance < 0.3 {
// Dark color: lighten uniformly to avoid color shifts
let lightenAmount = 0.15
return (
min(1.0, rgb.0 + lightenAmount),
min(1.0, rgb.1 + lightenAmount),
min(1.0, rgb.2 + lightenAmount)
)
} else {
// Light color: darken uniformly
let darkenAmount = 0.12
return (
max(0.0, rgb.0 - darkenAmount),
max(0.0, rgb.1 - darkenAmount),
max(0.0, rgb.2 - darkenAmount)
)
}
}
guard let preset else { return (0.93, 0.83, 0.68) }
switch preset {
case .coral: return (0.93, 0.83, 0.68)
case .midnight: return (0.29, 0.33, 0.4)
case .ocean: return (0.2, 0.65, 0.55)
case .lime: return (0.93, 0.83, 0.68)
case .violet: return (0.29, 0.33, 0.4)
case .forest: return (0.22, 0.52, 0.42)
case .rose: return (0.98, 0.85, 0.88)
case .slate: return (0.52, 0.58, 0.64)
case .amber: return (0.99, 0.88, 0.55)
case .plum: return (0.72, 0.45, 0.68)
}
}
private var accentRGB: (Double, Double, Double) {
if customRGB != nil {
// For custom colors, use a contrasting accent (gold or dark)
return requiresDarkText ? (0.12, 0.12, 0.14) : (0.95, 0.75, 0.25)
}
guard let preset else { return (0.95, 0.33, 0.28) }
switch preset {
case .coral: return (0.95, 0.33, 0.28)
case .midnight: return (0.95, 0.75, 0.25)
case .ocean: return (0.95, 0.75, 0.25)
case .lime: return (0.12, 0.12, 0.14)
case .violet: return (0.95, 0.75, 0.25)
case .forest: return (0.95, 0.75, 0.25)
case .rose: return (0.75, 0.25, 0.35)
case .slate: return (0.95, 0.75, 0.25)
case .amber: return (0.12, 0.12, 0.14)
case .plum: return (0.95, 0.75, 0.25)
}
}
private var textRGB: (Double, Double, Double) {
requiresDarkText
? (0.14, 0.14, 0.17) // Dark text for light backgrounds
: (0.98, 0.98, 0.98) // Light text for dark backgrounds
}
// MARK: - Colors (MainActor) // MARK: - Colors (MainActor)
@MainActor var primaryColor: Color { @MainActor var primaryColor: Color {
Color(red: primaryRGB.0, green: primaryRGB.1, blue: primaryRGB.2) if let rgb = customRGB {
return Color(red: rgb.0, green: rgb.1, blue: rgb.2)
}
guard let preset else { return Color.CardPalette.coral }
switch preset {
case .coral: return Color.CardPalette.coral
case .midnight: return Color.CardPalette.midnight
case .ocean: return Color.CardPalette.ocean
case .lime: return Color.CardPalette.lime
case .violet: return Color.CardPalette.violet
case .forest: return Color.CardPalette.forest
case .rose: return Color.CardPalette.rose
case .slate: return Color.CardPalette.slate
case .amber: return Color.CardPalette.amber
case .plum: return Color.CardPalette.plum
}
} }
@MainActor var secondaryColor: Color { @MainActor var secondaryColor: Color {
Color(red: secondaryRGB.0, green: secondaryRGB.1, blue: secondaryRGB.2) if let rgb = customRGB {
// Calculate luminance to determine if we should lighten or darken.
let luminance =
Design.ThemeMath.luminanceRedWeight * rgb.0
+ Design.ThemeMath.luminanceGreenWeight * rgb.1
+ Design.ThemeMath.luminanceBlueWeight * rgb.2
if luminance < Design.ThemeMath.darkThemeLuminanceThreshold {
return Color(
red: min(1.0, rgb.0 + Design.ThemeMath.customLightenAmount),
green: min(1.0, rgb.1 + Design.ThemeMath.customLightenAmount),
blue: min(1.0, rgb.2 + Design.ThemeMath.customLightenAmount)
)
} else {
return Color(
red: max(0.0, rgb.0 - Design.ThemeMath.customDarkenAmount),
green: max(0.0, rgb.1 - Design.ThemeMath.customDarkenAmount),
blue: max(0.0, rgb.2 - Design.ThemeMath.customDarkenAmount)
)
}
}
guard let preset else { return Color.CardPalette.sand }
switch preset {
case .coral, .lime: return Color.CardPalette.sand
case .midnight, .violet: return Color.Accent.slate
case .ocean: return Color.Accent.mint
case .forest: return Color("SocialWhatsApp")
case .rose: return Color("CardPaletteRose")
case .slate: return Color("CardPaletteSlate")
case .amber: return Color("CardPaletteSand")
case .plum: return Color("CardPalettePlum")
}
} }
@MainActor var accentColor: Color { @MainActor var accentColor: Color {
Color(red: accentRGB.0, green: accentRGB.1, blue: accentRGB.2) if customRGB != nil {
return requiresDarkText ? Color.Accent.ink : Color.Accent.gold
}
guard let preset else { return Color.Accent.red }
switch preset {
case .lime, .amber:
return Color.Accent.ink
case .rose:
return Color("BrandingSecondary")
case .coral, .midnight, .ocean, .violet, .forest, .slate, .plum:
return Color.Accent.gold
}
} }
/// The appropriate text color for content displayed on this theme's background. /// The appropriate text color for content displayed on this theme's background.
@MainActor var textColor: Color { @MainActor var textColor: Color {
Color(red: textRGB.0, green: textRGB.1, blue: textRGB.2) requiresDarkText ? Color.Text.primary : Color.AppText.inverted
} }
// MARK: - Hashable & Equatable // MARK: - Hashable & Equatable

View File

@ -117,7 +117,7 @@ extension ContactFieldType {
id: "phone", id: "phone",
displayName: String(localized: "Phone Number"), displayName: String(localized: "Phone Number"),
systemImage: "phone.fill", systemImage: "phone.fill",
iconColor: Color(red: 0.2, green: 0.2, blue: 0.2), iconColor: Color.Text.secondary,
category: .contact, category: .contact,
valueLabel: String(localized: "Phone Number"), valueLabel: String(localized: "Phone Number"),
valuePlaceholder: "+1 (555) 123-4567", valuePlaceholder: "+1 (555) 123-4567",
@ -134,7 +134,7 @@ extension ContactFieldType {
id: "email", id: "email",
displayName: String(localized: "Email"), displayName: String(localized: "Email"),
systemImage: "envelope.fill", systemImage: "envelope.fill",
iconColor: Color(red: 0.2, green: 0.2, blue: 0.2), iconColor: Color.Text.secondary,
category: .contact, category: .contact,
valueLabel: String(localized: "Email"), valueLabel: String(localized: "Email"),
valuePlaceholder: "you@example.com", valuePlaceholder: "you@example.com",
@ -147,7 +147,7 @@ extension ContactFieldType {
id: "website", id: "website",
displayName: String(localized: "Company Website"), displayName: String(localized: "Company Website"),
systemImage: "globe", systemImage: "globe",
iconColor: Color(red: 0.2, green: 0.2, blue: 0.2), iconColor: Color.Text.secondary,
category: .contact, category: .contact,
valueLabel: String(localized: "Website URL"), valueLabel: String(localized: "Website URL"),
valuePlaceholder: "https://company.com", valuePlaceholder: "https://company.com",
@ -160,7 +160,7 @@ extension ContactFieldType {
id: "address", id: "address",
displayName: String(localized: "Address"), displayName: String(localized: "Address"),
systemImage: "location.fill", systemImage: "location.fill",
iconColor: Color(red: 0.2, green: 0.2, blue: 0.2), iconColor: Color.Text.secondary,
category: .contact, category: .contact,
valueLabel: String(localized: "Address"), valueLabel: String(localized: "Address"),
valuePlaceholder: "123 Main St, City, State", valuePlaceholder: "123 Main St, City, State",
@ -187,7 +187,7 @@ extension ContactFieldType {
displayName: "LinkedIn", displayName: "LinkedIn",
systemImage: "linkedin", systemImage: "linkedin",
isCustomSymbol: true, isCustomSymbol: true,
iconColor: Color(red: 0.0, green: 0.0, blue: 0.0), iconColor: Color.Text.primary,
category: .social, category: .social,
valueLabel: String(localized: "Username/Link"), valueLabel: String(localized: "Username/Link"),
valuePlaceholder: "linkedin.com/in/username", valuePlaceholder: "linkedin.com/in/username",
@ -201,7 +201,7 @@ extension ContactFieldType {
displayName: "X", displayName: "X",
systemImage: "x-twitter", systemImage: "x-twitter",
isCustomSymbol: true, isCustomSymbol: true,
iconColor: Color(red: 0.0, green: 0.0, blue: 0.0), iconColor: Color.Text.primary,
category: .social, category: .social,
valueLabel: String(localized: "Username/Link"), valueLabel: String(localized: "Username/Link"),
valuePlaceholder: "x.com/username", valuePlaceholder: "x.com/username",
@ -215,7 +215,7 @@ extension ContactFieldType {
displayName: "Instagram", displayName: "Instagram",
systemImage: "instagram", systemImage: "instagram",
isCustomSymbol: true, isCustomSymbol: true,
iconColor: Color(red: 0.0, green: 0.0, blue: 0.0), iconColor: Color.Text.primary,
category: .social, category: .social,
valueLabel: String(localized: "Username/Link"), valueLabel: String(localized: "Username/Link"),
valuePlaceholder: "instagram.com/username", valuePlaceholder: "instagram.com/username",
@ -229,7 +229,7 @@ extension ContactFieldType {
displayName: "Facebook", displayName: "Facebook",
systemImage: "facebook", systemImage: "facebook",
isCustomSymbol: true, isCustomSymbol: true,
iconColor: Color(red: 0.0, green: 0.0, blue: 0.0), iconColor: Color.Text.primary,
category: .social, category: .social,
valueLabel: String(localized: "Username/Link"), valueLabel: String(localized: "Username/Link"),
valuePlaceholder: "facebook.com/username", valuePlaceholder: "facebook.com/username",
@ -243,7 +243,7 @@ extension ContactFieldType {
displayName: "TikTok", displayName: "TikTok",
systemImage: "tiktok", systemImage: "tiktok",
isCustomSymbol: true, isCustomSymbol: true,
iconColor: Color(red: 0.0, green: 0.0, blue: 0.0), iconColor: Color.Text.primary,
category: .social, category: .social,
valueLabel: String(localized: "Username/Link"), valueLabel: String(localized: "Username/Link"),
valuePlaceholder: "tiktok.com/@username", valuePlaceholder: "tiktok.com/@username",
@ -257,7 +257,7 @@ extension ContactFieldType {
displayName: "Threads", displayName: "Threads",
systemImage: "threads", systemImage: "threads",
isCustomSymbol: true, isCustomSymbol: true,
iconColor: Color(red: 0.0, green: 0.0, blue: 0.0), iconColor: Color.Text.primary,
category: .social, category: .social,
valueLabel: String(localized: "Username/Link"), valueLabel: String(localized: "Username/Link"),
valuePlaceholder: "threads.net/@username", valuePlaceholder: "threads.net/@username",
@ -271,7 +271,7 @@ extension ContactFieldType {
displayName: "YouTube", displayName: "YouTube",
systemImage: "youtube", systemImage: "youtube",
isCustomSymbol: true, isCustomSymbol: true,
iconColor: Color(red: 0.0, green: 0.0, blue: 0.0), iconColor: Color.Text.primary,
category: .social, category: .social,
valueLabel: String(localized: "Username/Link"), valueLabel: String(localized: "Username/Link"),
valuePlaceholder: "youtube.com/@channel", valuePlaceholder: "youtube.com/@channel",
@ -284,7 +284,7 @@ extension ContactFieldType {
id: "snapchat", id: "snapchat",
displayName: "Snapchat", displayName: "Snapchat",
systemImage: "camera.fill", systemImage: "camera.fill",
iconColor: Color(red: 0.0, green: 0.0, blue: 0.0), iconColor: Color.Text.primary,
category: .social, category: .social,
valueLabel: String(localized: "Username/Link"), valueLabel: String(localized: "Username/Link"),
valuePlaceholder: "snapchat.com/add/username", valuePlaceholder: "snapchat.com/add/username",
@ -297,7 +297,7 @@ extension ContactFieldType {
id: "pinterest", id: "pinterest",
displayName: "Pinterest", displayName: "Pinterest",
systemImage: "pin.fill", systemImage: "pin.fill",
iconColor: Color(red: 0.0, green: 0.0, blue: 0.0), iconColor: Color.Text.primary,
category: .social, category: .social,
valueLabel: String(localized: "Username/Link"), valueLabel: String(localized: "Username/Link"),
valuePlaceholder: "pinterest.com/username", valuePlaceholder: "pinterest.com/username",
@ -311,7 +311,7 @@ extension ContactFieldType {
displayName: "Twitch", displayName: "Twitch",
systemImage: "twitch", systemImage: "twitch",
isCustomSymbol: true, isCustomSymbol: true,
iconColor: Color(red: 0.0, green: 0.0, blue: 0.0), iconColor: Color.Text.primary,
category: .social, category: .social,
valueLabel: String(localized: "Username/Link"), valueLabel: String(localized: "Username/Link"),
valuePlaceholder: "twitch.tv/username", valuePlaceholder: "twitch.tv/username",
@ -325,7 +325,7 @@ extension ContactFieldType {
displayName: "Bluesky", displayName: "Bluesky",
systemImage: "bluesky", systemImage: "bluesky",
isCustomSymbol: true, isCustomSymbol: true,
iconColor: Color(red: 0.0, green: 0.0, blue: 0.0), iconColor: Color.Text.primary,
category: .social, category: .social,
valueLabel: String(localized: "Username/Link"), valueLabel: String(localized: "Username/Link"),
valuePlaceholder: "bsky.app/profile/username", valuePlaceholder: "bsky.app/profile/username",
@ -339,7 +339,7 @@ extension ContactFieldType {
displayName: "Mastodon", displayName: "Mastodon",
systemImage: "mastodon", systemImage: "mastodon",
isCustomSymbol: true, isCustomSymbol: true,
iconColor: Color(red: 0.0, green: 0.0, blue: 0.0), iconColor: Color.Text.primary,
category: .social, category: .social,
valueLabel: String(localized: "Username/Link"), valueLabel: String(localized: "Username/Link"),
valuePlaceholder: "mastodon.social/@username", valuePlaceholder: "mastodon.social/@username",
@ -362,7 +362,7 @@ extension ContactFieldType {
displayName: "Reddit", displayName: "Reddit",
systemImage: "reddit", systemImage: "reddit",
isCustomSymbol: true, isCustomSymbol: true,
iconColor: Color(red: 0.0, green: 0.0, blue: 0.0), iconColor: Color.Text.primary,
category: .social, category: .social,
valueLabel: String(localized: "Username/Link"), valueLabel: String(localized: "Username/Link"),
valuePlaceholder: "reddit.com/user/username", valuePlaceholder: "reddit.com/user/username",
@ -378,7 +378,7 @@ extension ContactFieldType {
displayName: "GitHub", displayName: "GitHub",
systemImage: "github.fill", systemImage: "github.fill",
isCustomSymbol: true, isCustomSymbol: true,
iconColor: Color(red: 0.0, green: 0.0, blue: 0.0), iconColor: Color.Text.primary,
category: .developer, category: .developer,
valueLabel: String(localized: "Username/Link"), valueLabel: String(localized: "Username/Link"),
valuePlaceholder: "github.com/username", valuePlaceholder: "github.com/username",
@ -391,7 +391,7 @@ extension ContactFieldType {
id: "gitlab", id: "gitlab",
displayName: "GitLab", displayName: "GitLab",
systemImage: "chevron.left.forwardslash.chevron.right", systemImage: "chevron.left.forwardslash.chevron.right",
iconColor: Color(red: 0.0, green: 0.0, blue: 0.0), iconColor: Color.Text.primary,
category: .developer, category: .developer,
valueLabel: String(localized: "Username/Link"), valueLabel: String(localized: "Username/Link"),
valuePlaceholder: "gitlab.com/username", valuePlaceholder: "gitlab.com/username",
@ -404,7 +404,7 @@ extension ContactFieldType {
id: "stackoverflow", id: "stackoverflow",
displayName: "Stack Overflow", displayName: "Stack Overflow",
systemImage: "text.bubble.fill", systemImage: "text.bubble.fill",
iconColor: Color(red: 0.0, green: 0.0, blue: 0.0), iconColor: Color.Text.primary,
category: .developer, category: .developer,
valueLabel: String(localized: "Username/Link"), valueLabel: String(localized: "Username/Link"),
valuePlaceholder: "stackoverflow.com/users/id", valuePlaceholder: "stackoverflow.com/users/id",
@ -420,7 +420,7 @@ extension ContactFieldType {
displayName: "Telegram", displayName: "Telegram",
systemImage: "telegram", systemImage: "telegram",
isCustomSymbol: true, isCustomSymbol: true,
iconColor: Color(red: 0.0, green: 0.0, blue: 0.0), iconColor: Color.Text.primary,
category: .messaging, category: .messaging,
valueLabel: String(localized: "Username/Link"), valueLabel: String(localized: "Username/Link"),
valuePlaceholder: "t.me/username", valuePlaceholder: "t.me/username",
@ -433,7 +433,7 @@ extension ContactFieldType {
id: "whatsapp", id: "whatsapp",
displayName: "WhatsApp", displayName: "WhatsApp",
systemImage: "message.fill", systemImage: "message.fill",
iconColor: Color(red: 0.0, green: 0.0, blue: 0.0), iconColor: Color.Text.primary,
category: .messaging, category: .messaging,
valueLabel: String(localized: "Username/Link"), valueLabel: String(localized: "Username/Link"),
valuePlaceholder: "+1 555 123 4567", valuePlaceholder: "+1 555 123 4567",
@ -449,7 +449,7 @@ extension ContactFieldType {
id: "signal", id: "signal",
displayName: "Signal", displayName: "Signal",
systemImage: "bubble.left.fill", systemImage: "bubble.left.fill",
iconColor: Color(red: 0.0, green: 0.0, blue: 0.0), iconColor: Color.Text.primary,
category: .messaging, category: .messaging,
valueLabel: String(localized: "Username/Link"), valueLabel: String(localized: "Username/Link"),
valuePlaceholder: "+1 555 123 4567", valuePlaceholder: "+1 555 123 4567",
@ -466,7 +466,7 @@ extension ContactFieldType {
displayName: "Discord", displayName: "Discord",
systemImage: "discord", systemImage: "discord",
isCustomSymbol: true, isCustomSymbol: true,
iconColor: Color(red: 0.0, green: 0.0, blue: 0.0), iconColor: Color.Text.primary,
category: .messaging, category: .messaging,
valueLabel: String(localized: "Username/Link"), valueLabel: String(localized: "Username/Link"),
valuePlaceholder: "discord.gg/invite", valuePlaceholder: "discord.gg/invite",
@ -480,7 +480,7 @@ extension ContactFieldType {
displayName: "Slack", displayName: "Slack",
systemImage: "slack", systemImage: "slack",
isCustomSymbol: true, isCustomSymbol: true,
iconColor: Color(red: 0.0, green: 0.0, blue: 0.0), iconColor: Color.Text.primary,
category: .messaging, category: .messaging,
valueLabel: String(localized: "Username/Link"), valueLabel: String(localized: "Username/Link"),
valuePlaceholder: "yourworkspace.slack.com", valuePlaceholder: "yourworkspace.slack.com",
@ -494,7 +494,7 @@ extension ContactFieldType {
displayName: "Matrix", displayName: "Matrix",
systemImage: "matrix", systemImage: "matrix",
isCustomSymbol: true, isCustomSymbol: true,
iconColor: Color(red: 0.0, green: 0.0, blue: 0.0), iconColor: Color.Text.primary,
category: .messaging, category: .messaging,
valueLabel: String(localized: "Username/Link"), valueLabel: String(localized: "Username/Link"),
valuePlaceholder: "@username:matrix.org", valuePlaceholder: "@username:matrix.org",
@ -515,7 +515,7 @@ extension ContactFieldType {
id: "venmo", id: "venmo",
displayName: "Venmo", displayName: "Venmo",
systemImage: "dollarsign.circle.fill", systemImage: "dollarsign.circle.fill",
iconColor: Color(red: 0.0, green: 0.0, blue: 0.0), iconColor: Color.Text.primary,
category: .payment, category: .payment,
valueLabel: String(localized: "Username"), valueLabel: String(localized: "Username"),
valuePlaceholder: "@username", valuePlaceholder: "@username",
@ -528,7 +528,7 @@ extension ContactFieldType {
id: "cashApp", id: "cashApp",
displayName: "Cash App", displayName: "Cash App",
systemImage: "dollarsign.square.fill", systemImage: "dollarsign.square.fill",
iconColor: Color(red: 0.0, green: 0.0, blue: 0.0), iconColor: Color.Text.primary,
category: .payment, category: .payment,
valueLabel: String(localized: "Username"), valueLabel: String(localized: "Username"),
valuePlaceholder: "$cashtag", valuePlaceholder: "$cashtag",
@ -541,7 +541,7 @@ extension ContactFieldType {
id: "paypal", id: "paypal",
displayName: "PayPal", displayName: "PayPal",
systemImage: "creditcard.fill", systemImage: "creditcard.fill",
iconColor: Color(red: 0.0, green: 0.0, blue: 0.0), iconColor: Color.Text.primary,
category: .payment, category: .payment,
valueLabel: String(localized: "Email or Username"), valueLabel: String(localized: "Email or Username"),
valuePlaceholder: "paypal.me/username", valuePlaceholder: "paypal.me/username",
@ -554,7 +554,7 @@ extension ContactFieldType {
id: "zelle", id: "zelle",
displayName: "Zelle", displayName: "Zelle",
systemImage: "dollarsign.arrow.circlepath", systemImage: "dollarsign.arrow.circlepath",
iconColor: Color(red: 0.0, green: 0.0, blue: 0.0), iconColor: Color.Text.primary,
category: .payment, category: .payment,
valueLabel: String(localized: "Phone or Email"), valueLabel: String(localized: "Phone or Email"),
valuePlaceholder: "email@example.com", valuePlaceholder: "email@example.com",
@ -570,7 +570,7 @@ extension ContactFieldType {
displayName: "Patreon", displayName: "Patreon",
systemImage: "patreon.fill", systemImage: "patreon.fill",
isCustomSymbol: true, isCustomSymbol: true,
iconColor: Color(red: 0.0, green: 0.0, blue: 0.0), iconColor: Color.Text.primary,
category: .creator, category: .creator,
valueLabel: String(localized: "Profile Link"), valueLabel: String(localized: "Profile Link"),
valuePlaceholder: "patreon.com/username", valuePlaceholder: "patreon.com/username",
@ -584,7 +584,7 @@ extension ContactFieldType {
displayName: "Ko-fi", displayName: "Ko-fi",
systemImage: "ko-fi", systemImage: "ko-fi",
isCustomSymbol: true, isCustomSymbol: true,
iconColor: Color(red: 0.0, green: 0.0, blue: 0.0), iconColor: Color.Text.primary,
category: .creator, category: .creator,
valueLabel: String(localized: "Profile Link"), valueLabel: String(localized: "Profile Link"),
valuePlaceholder: "ko-fi.com/username", valuePlaceholder: "ko-fi.com/username",
@ -599,7 +599,7 @@ extension ContactFieldType {
id: "calendly", id: "calendly",
displayName: "Calendly", displayName: "Calendly",
systemImage: "calendar", systemImage: "calendar",
iconColor: Color(red: 0.0, green: 0.0, blue: 0.0), iconColor: Color.Text.primary,
category: .scheduling, category: .scheduling,
valueLabel: String(localized: "Calendly Link"), valueLabel: String(localized: "Calendly Link"),
valuePlaceholder: "calendly.com/username", valuePlaceholder: "calendly.com/username",
@ -614,7 +614,7 @@ extension ContactFieldType {
id: "customLink", id: "customLink",
displayName: String(localized: "Link"), displayName: String(localized: "Link"),
systemImage: "link", systemImage: "link",
iconColor: Color(red: 0.2, green: 0.2, blue: 0.2), iconColor: Color.Text.secondary,
category: .other, category: .other,
valueLabel: String(localized: "URL"), valueLabel: String(localized: "URL"),
valuePlaceholder: "https://example.com", valuePlaceholder: "https://example.com",

View File

@ -1,10 +1,11 @@
import SwiftUI import SwiftUI
import UIKit import UIKit
import Bedrock
extension UIImage { extension UIImage {
/// Extracts dominant colors from the image using pixel sampling. /// Extracts dominant colors from the image using pixel sampling.
/// - Parameter count: Number of colors to extract (default 3) /// - Parameter count: Number of colors to extract (default 3)
/// - Returns: Array of SwiftUI Colors, always includes white and black as fallbacks /// - Returns: Array of SwiftUI Colors, always includes app theme fallback colors
func dominantColors(count: Int = 3) -> [Color] { func dominantColors(count: Int = 3) -> [Color] {
guard let cgImage = self.cgImage else { guard let cgImage = self.cgImage else {
return defaultColors(count: count) return defaultColors(count: count)
@ -18,14 +19,14 @@ extension UIImage {
} }
// Create a smaller sample size for performance // Create a smaller sample size for performance
let sampleSize = 50 let sampleSize = Design.ColorExtraction.sampleSize
guard let context = CGContext( guard let context = CGContext(
data: nil, data: nil,
width: sampleSize, width: sampleSize,
height: sampleSize, height: sampleSize,
bitsPerComponent: 8, bitsPerComponent: 8,
bytesPerRow: sampleSize * 4, bytesPerRow: sampleSize * Design.ColorExtraction.rgbaBytesPerPixel,
space: CGColorSpaceCreateDeviceRGB(), space: CGColorSpaceCreateDeviceRGB(),
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
) else { ) else {
@ -38,7 +39,10 @@ extension UIImage {
return defaultColors(count: count) return defaultColors(count: count)
} }
let data = pixelData.bindMemory(to: UInt8.self, capacity: sampleSize * sampleSize * 4) let data = pixelData.bindMemory(
to: UInt8.self,
capacity: sampleSize * sampleSize * Design.ColorExtraction.rgbaBytesPerPixel
)
// Collect colors with their frequencies // Collect colors with their frequencies
var colorCounts: [ColorBucket: Int] = [:] var colorCounts: [ColorBucket: Int] = [:]
@ -52,17 +56,18 @@ extension UIImage {
let a = data[offset + 3] let a = data[offset + 3]
// Skip transparent or near-transparent pixels // Skip transparent or near-transparent pixels
guard a > 128 else { continue } guard a > Design.ColorExtraction.alphaVisibilityThreshold else { continue }
// Skip near-white and near-black (we'll add those as defaults) // Skip near-white and near-black (we'll add those as defaults)
let brightness = (Int(r) + Int(g) + Int(b)) / 3 let brightness = (Int(r) + Int(g) + Int(b)) / 3
guard brightness > 30 && brightness < 225 else { continue } guard brightness > Design.ColorExtraction.minBrightness
&& brightness < Design.ColorExtraction.maxBrightness else { continue }
// Bucket colors to reduce noise (group similar colors) // Bucket colors to reduce noise (group similar colors)
let bucket = ColorBucket( let bucket = ColorBucket(
r: UInt8((Int(r) / 32) * 32), r: UInt8((Int(r) / Design.ColorExtraction.bucketQuantizationStep) * Design.ColorExtraction.bucketQuantizationStep),
g: UInt8((Int(g) / 32) * 32), g: UInt8((Int(g) / Design.ColorExtraction.bucketQuantizationStep) * Design.ColorExtraction.bucketQuantizationStep),
b: UInt8((Int(b) / 32) * 32) b: UInt8((Int(b) / Design.ColorExtraction.bucketQuantizationStep) * Design.ColorExtraction.bucketQuantizationStep)
) )
colorCounts[bucket, default: 0] += 1 colorCounts[bucket, default: 0] += 1
@ -77,9 +82,9 @@ extension UIImage {
// Add top colors, ensuring they're distinct // Add top colors, ensuring they're distinct
for (bucket, _) in sortedColors { for (bucket, _) in sortedColors {
let color = Color( let color = Color(
red: Double(bucket.r) / 255.0, red: Double(bucket.r) / Design.ColorExtraction.rgbDenominator,
green: Double(bucket.g) / 255.0, green: Double(bucket.g) / Design.ColorExtraction.rgbDenominator,
blue: Double(bucket.b) / 255.0 blue: Double(bucket.b) / Design.ColorExtraction.rgbDenominator
) )
// Check if this color is distinct enough from existing ones // Check if this color is distinct enough from existing ones
@ -92,20 +97,20 @@ extension UIImage {
} }
} }
// Always include white as first option // Always include high-contrast app fallback colors first.
var result: [Color] = [.white] var result: [Color] = [Color.AppText.inverted]
result.append(contentsOf: extractedColors) result.append(contentsOf: extractedColors)
// Add black if we have room // Add primary text color if we have room.
if result.count < count { if result.count < count {
result.append(.black) result.append(Color.Text.primary)
} }
return Array(result.prefix(count)) return Array(result.prefix(count))
} }
private func defaultColors(count: Int) -> [Color] { private func defaultColors(count: Int) -> [Color] {
let defaults: [Color] = [.white, .black, Color(red: 0.2, green: 0.2, blue: 0.2)] let defaults: [Color] = [Color.AppText.inverted, Color.Text.primary, Color.Text.secondary]
return Array(defaults.prefix(count)) return Array(defaults.prefix(count))
} }
} }
@ -122,7 +127,7 @@ private struct ColorBucket: Hashable {
private extension Color { private extension Color {
/// Checks if two colors are visually similar. /// Checks if two colors are visually similar.
func isClose(to other: Color, threshold: Double = 0.15) -> Bool { func isClose(to other: Color, threshold: Double = Design.ColorExtraction.colorSimilarityThreshold) -> Bool {
guard let selfComponents = self.cgColor?.components, guard let selfComponents = self.cgColor?.components,
let otherComponents = other.cgColor?.components, let otherComponents = other.cgColor?.components,
selfComponents.count >= 3, selfComponents.count >= 3,
@ -137,4 +142,3 @@ private extension Color {
return rDiff < threshold && gDiff < threshold && bDiff < threshold return rDiff < threshold && gDiff < threshold && bDiff < threshold
} }
} }

View File

@ -9,7 +9,7 @@ struct FloatingShareButton: View {
Image(systemName: "qrcode") Image(systemName: "qrcode")
.typography(.title2) .typography(.title2)
.fontWeight(.semibold) .fontWeight(.semibold)
.foregroundStyle(.white) .foregroundStyle(Color.AppText.inverted)
.frame(width: Design.CardSize.floatingButtonSize, height: Design.CardSize.floatingButtonSize) .frame(width: Design.CardSize.floatingButtonSize, height: Design.CardSize.floatingButtonSize)
.background( .background(
Circle() Circle()

View File

@ -32,6 +32,8 @@ struct CardsHomeView: View {
} }
.navigationTitle(String.localized("My Cards")) .navigationTitle(String.localized("My Cards"))
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.ultraThinMaterial, for: .navigationBar)
.toolbarBackground(.visible, for: .navigationBar)
.toolbar { .toolbar {
ToolbarItem(placement: .topBarLeading) { ToolbarItem(placement: .topBarLeading) {
Button(String.localized("Add Card"), systemImage: "plus") { Button(String.localized("Add Card"), systemImage: "plus") {

View File

@ -573,7 +573,7 @@ private struct ContactFieldRowView: View {
.background(Color.AppBackground.base.opacity(0.82)) .background(Color.AppBackground.base.opacity(0.82))
.overlay( .overlay(
RoundedRectangle(cornerRadius: Design.CornerRadius.medium) RoundedRectangle(cornerRadius: Design.CornerRadius.medium)
.stroke(Color.white.opacity(0.08), lineWidth: 1) .stroke(Color.Text.tertiary.opacity(Design.Opacity.light), lineWidth: Design.LineWidth.thin)
) )
.clipShape(.rect(cornerRadius: Design.CornerRadius.medium)) .clipShape(.rect(cornerRadius: Design.CornerRadius.medium))
.contentShape(.rect) .contentShape(.rect)

View File

@ -410,8 +410,8 @@ private struct CustomColorSwatch: View {
if customColor == nil { if customColor == nil {
Image(systemName: "eyedropper") Image(systemName: "eyedropper")
.typography(.caption) .typography(.caption)
.foregroundStyle(.white) .foregroundStyle(Color.AppText.inverted)
.shadow(color: .black.opacity(Design.Opacity.medium), radius: Design.Shadow.radiusSmall) .shadow(color: Color.AppBackground.base.opacity(Design.Opacity.medium), radius: Design.Shadow.radiusSmall)
} }
} }
.frame(width: Design.CardSize.colorSwatchSize, height: Design.CardSize.colorSwatchSize) .frame(width: Design.CardSize.colorSwatchSize, height: Design.CardSize.colorSwatchSize)
@ -945,7 +945,7 @@ private struct ContactFieldRowView: View {
.overlay( .overlay(
field.fieldType.iconImage() field.fieldType.iconImage()
.typography(.title3) .typography(.title3)
.foregroundStyle(.white) .foregroundStyle(Color.AppText.inverted)
) )
// Content // Content
@ -1064,8 +1064,8 @@ private struct PreviewCardButton: View {
RoundedRectangle(cornerRadius: Design.CornerRadius.large) RoundedRectangle(cornerRadius: Design.CornerRadius.large)
.stroke( .stroke(
colorScheme == .dark colorScheme == .dark
? Color.white.opacity(Design.Opacity.subtle) ? Color.AppText.inverted.opacity(Design.Opacity.subtle)
: Color.black.opacity(Design.Opacity.light), : Color.Text.primary.opacity(Design.Opacity.light),
lineWidth: Design.LineWidth.thin lineWidth: Design.LineWidth.thin
) )
) )

View File

@ -26,7 +26,7 @@ struct ContactFieldEditorSheet: View {
fieldType: ContactFieldType, fieldType: ContactFieldType,
initialValue: String = "", initialValue: String = "",
initialTitle: String = "", initialTitle: String = "",
themeColor: Color = Color(red: 0.2, green: 0.2, blue: 0.2), themeColor: Color = Color.Text.secondary,
onSave: @escaping (String, String) -> Void, onSave: @escaping (String, String) -> Void,
onDelete: (() -> Void)? = nil onDelete: (() -> Void)? = nil
) { ) {

View File

@ -9,7 +9,7 @@ struct CropGridLines: View {
HStack(spacing: cropSize.width / 3 - Design.LineWidth.thin) { HStack(spacing: cropSize.width / 3 - Design.LineWidth.thin) {
ForEach(0..<2, id: \.self) { _ in ForEach(0..<2, id: \.self) { _ in
Rectangle() Rectangle()
.fill(Color.white.opacity(Design.Opacity.light)) .fill(Color.AppText.inverted.opacity(Design.Opacity.light))
.frame(width: Design.LineWidth.thin, height: cropSize.height) .frame(width: Design.LineWidth.thin, height: cropSize.height)
} }
} }
@ -17,7 +17,7 @@ struct CropGridLines: View {
VStack(spacing: cropSize.height / 3 - Design.LineWidth.thin) { VStack(spacing: cropSize.height / 3 - Design.LineWidth.thin) {
ForEach(0..<2, id: \.self) { _ in ForEach(0..<2, id: \.self) { _ in
Rectangle() Rectangle()
.fill(Color.white.opacity(Design.Opacity.light)) .fill(Color.AppText.inverted.opacity(Design.Opacity.light))
.frame(width: cropSize.width, height: Design.LineWidth.thin) .frame(width: cropSize.width, height: Design.LineWidth.thin)
} }
} }

View File

@ -8,7 +8,7 @@ struct CropOverlay: View {
var body: some View { var body: some View {
ZStack { ZStack {
Rectangle() Rectangle()
.fill(Color.black.opacity(Design.Opacity.accent)) .fill(Color.AppBackground.base.opacity(Design.Opacity.accent))
Rectangle() Rectangle()
.fill(Color.clear) .fill(Color.clear)
@ -19,7 +19,7 @@ struct CropOverlay: View {
.allowsHitTesting(false) .allowsHitTesting(false)
Rectangle() Rectangle()
.stroke(Color.white, lineWidth: Design.LineWidth.thin) .stroke(Color.AppText.inverted, lineWidth: Design.LineWidth.thin)
.frame(width: cropSize.width, height: cropSize.height) .frame(width: cropSize.width, height: cropSize.height)
.allowsHitTesting(false) .allowsHitTesting(false)
} }

View File

@ -13,7 +13,7 @@ struct FieldHeaderView: View {
.overlay( .overlay(
fieldType.iconImage() fieldType.iconImage()
.typography(.title3) .typography(.title3)
.foregroundStyle(.white) .foregroundStyle(Color.AppText.inverted)
) )
Text(fieldType.displayName) Text(fieldType.displayName)

View File

@ -89,7 +89,7 @@ struct PhotoCropperSheet: View {
GeometryReader { geometry in GeometryReader { geometry in
ZStack { ZStack {
// Dark background // Dark background
Color.black.ignoresSafeArea() Color.AppBackground.base.ignoresSafeArea()
// Image with gestures // Image with gestures
if let uiImage { if let uiImage {
@ -129,7 +129,7 @@ struct PhotoCropperSheet: View {
dismiss() dismiss()
} }
} }
.foregroundStyle(.white) .foregroundStyle(Color.AppText.inverted)
} }
ToolbarItem(placement: .principal) { ToolbarItem(placement: .principal) {
HStack(spacing: Design.Spacing.xLarge) { HStack(spacing: Design.Spacing.xLarge) {
@ -138,7 +138,7 @@ struct PhotoCropperSheet: View {
rotate90Left() rotate90Left()
} label: { } label: {
Image(systemName: "rotate.left") Image(systemName: "rotate.left")
.foregroundStyle(.white) .foregroundStyle(Color.AppText.inverted)
} }
// Reset all transforms // Reset all transforms
@ -146,7 +146,7 @@ struct PhotoCropperSheet: View {
resetTransform() resetTransform()
} label: { } label: {
Image(systemName: "arrow.counterclockwise") Image(systemName: "arrow.counterclockwise")
.foregroundStyle(.white) .foregroundStyle(Color.AppText.inverted)
} }
// Aspect ratio picker (only for logo workflow) // Aspect ratio picker (only for logo workflow)
@ -155,7 +155,7 @@ struct PhotoCropperSheet: View {
showingAspectRatioPicker = true showingAspectRatioPicker = true
} label: { } label: {
Image(systemName: "aspectratio") Image(systemName: "aspectratio")
.foregroundStyle(.white) .foregroundStyle(Color.AppText.inverted)
} }
} }
@ -164,7 +164,7 @@ struct PhotoCropperSheet: View {
rotate90Right() rotate90Right()
} label: { } label: {
Image(systemName: "rotate.right") Image(systemName: "rotate.right")
.foregroundStyle(.white) .foregroundStyle(Color.AppText.inverted)
} }
} }
} }
@ -173,7 +173,7 @@ struct PhotoCropperSheet: View {
cropAndSave() cropAndSave()
} }
.bold() .bold()
.foregroundStyle(.white) .foregroundStyle(Color.AppText.inverted)
} }
} }
} }

View File

@ -18,6 +18,10 @@ struct ContactRowView: View {
return trimmed.isEmpty ? String.localized("Shared") : String.localized(trimmed) return trimmed.isEmpty ? String.localized("Shared") : String.localized(trimmed)
} }
private var chipBackground: Color { Color.AppBackground.accent }
private var chipTextColor: Color { Color.Text.primary }
private var chipStroke: Color { Color.Text.tertiary.opacity(Design.Opacity.light) }
var body: some View { var body: some View {
HStack(spacing: Design.Spacing.medium) { HStack(spacing: Design.Spacing.medium) {
ContactAvatarView(contact: contact) ContactAvatarView(contact: contact)
@ -54,10 +58,14 @@ struct ContactRowView: View {
ForEach(contact.tagList.prefix(2), id: \.self) { tag in ForEach(contact.tagList.prefix(2), id: \.self) { tag in
Text(tag) Text(tag)
.typography(.caption2) .typography(.caption2)
.foregroundStyle(Color.Text.secondary) .foregroundStyle(chipTextColor)
.padding(.horizontal, Design.Spacing.xSmall) .padding(.horizontal, Design.Spacing.xSmall)
.padding(.vertical, Design.Spacing.xxSmall) .padding(.vertical, Design.Spacing.xxSmall)
.background(Color.AppBackground.accent.opacity(0.5)) .background(chipBackground)
.overlay(
RoundedRectangle(cornerRadius: Design.CornerRadius.small)
.stroke(chipStroke, lineWidth: Design.LineWidth.thin)
)
.clipShape(.rect(cornerRadius: Design.CornerRadius.small)) .clipShape(.rect(cornerRadius: Design.CornerRadius.small))
} }
} }

View File

@ -33,7 +33,8 @@ struct ContactsListView: View {
} }
private var showsJumpIndex: Bool { private var showsJumpIndex: Bool {
sortedContacts.count >= 20 && sectionTitles.count >= 6 sortedContacts.count >= Design.Contacts.sectionIndexMinContacts
&& sectionTitles.count >= Design.Contacts.sectionIndexMinSections
} }
var body: some View { var body: some View {
@ -67,17 +68,20 @@ struct ContactsListView: View {
} }
if showsJumpIndex { if showsJumpIndex {
VStack(spacing: 2) { VStack(spacing: Design.Contacts.sectionIndexLetterSpacing) {
ForEach(sectionTitles, id: \.self) { title in ForEach(sectionTitles, id: \.self) { title in
Button { Button {
withAnimation(.easeInOut(duration: 0.2)) { withAnimation(.easeInOut(duration: Design.Contacts.sectionIndexScrollAnimationDuration)) {
proxy.scrollTo(title, anchor: .top) proxy.scrollTo(title, anchor: .top)
} }
} label: { } label: {
Text(title) Text(title)
.typography(.caption2) .typography(.caption2)
.foregroundStyle(Color.Text.secondary) .foregroundStyle(Color.Text.secondary)
.frame(width: 16, height: 12) .frame(
width: Design.Contacts.sectionIndexLetterWidth,
height: Design.Contacts.sectionIndexLetterHeight
)
} }
.buttonStyle(.plain) .buttonStyle(.plain)
} }

View File

@ -334,7 +334,7 @@ private struct ContactBannerView: View {
.overlay(alignment: .bottomTrailing) { .overlay(alignment: .bottomTrailing) {
Image(systemName: "pencil") Image(systemName: "pencil")
.typography(.caption) .typography(.caption)
.foregroundStyle(Color.white) .foregroundStyle(Color.AppText.inverted)
.padding(Design.Spacing.xSmall) .padding(Design.Spacing.xSmall)
.background(Color.CardPalette.coral) .background(Color.CardPalette.coral)
.clipShape(.circle) .clipShape(.circle)
@ -383,7 +383,7 @@ private struct ContactProfileAvatarView: View {
.background(Color.CardPalette.coral) .background(Color.CardPalette.coral)
} }
} }
.frame(width: 92, height: 92) .frame(width: Design.Contacts.detailHeaderAvatarSize, height: Design.Contacts.detailHeaderAvatarSize)
.clipShape(.circle) .clipShape(.circle)
.overlay(Circle().stroke(Color.AppBackground.elevated, lineWidth: Design.LineWidth.thick)) .overlay(Circle().stroke(Color.AppBackground.elevated, lineWidth: Design.LineWidth.thick))
.shadow( .shadow(
@ -451,7 +451,7 @@ private struct ContactInfoCard: View {
if index < allContactFields.count - 1 || hasLegacyFields { if index < allContactFields.count - 1 || hasLegacyFields {
Divider() Divider()
.padding(.leading, 28 + Design.Spacing.medium) .padding(.leading, Design.Contacts.detailFieldIconWidth + Design.Spacing.medium)
} }
} }
@ -466,7 +466,7 @@ private struct ContactInfoCard: View {
if !contact.email.isEmpty && contact.emailAddresses.isEmpty { if !contact.email.isEmpty && contact.emailAddresses.isEmpty {
Divider() Divider()
.padding(.leading, 28 + Design.Spacing.medium) .padding(.leading, Design.Contacts.detailFieldIconWidth + Design.Spacing.medium)
} }
} }
@ -501,14 +501,14 @@ private struct ContactFieldInfoRow: View {
field.iconImage() field.iconImage()
.typography(.subheading) .typography(.subheading)
.foregroundStyle(Color.CardPalette.coral) .foregroundStyle(Color.CardPalette.coral)
.frame(width: 28) .frame(width: Design.Contacts.detailFieldIconWidth)
VStack(alignment: .leading, spacing: Design.Spacing.xxxSmall) { VStack(alignment: .leading, spacing: Design.Spacing.xxxSmall) {
Text(field.displayValue) Text(field.displayValue)
.typography(.subheading) .typography(.subheading)
.foregroundStyle(Color.Text.primary) .foregroundStyle(Color.Text.primary)
.multilineTextAlignment(.leading) .multilineTextAlignment(.leading)
.lineLimit(2) .lineLimit(Design.Contacts.notePreviewLineLimit)
Text(field.title.isEmpty ? field.displayName : field.title) Text(field.title.isEmpty ? field.displayName : field.title)
.typography(.caption) .typography(.caption)
@ -539,14 +539,14 @@ private struct ContactInfoRow: View {
Image(systemName: icon) Image(systemName: icon)
.typography(.subheading) .typography(.subheading)
.foregroundStyle(Color.CardPalette.coral) .foregroundStyle(Color.CardPalette.coral)
.frame(width: 28) .frame(width: Design.Contacts.detailFieldIconWidth)
VStack(alignment: .leading, spacing: Design.Spacing.xxxSmall) { VStack(alignment: .leading, spacing: Design.Spacing.xxxSmall) {
Text(value) Text(value)
.typography(.subheading) .typography(.subheading)
.foregroundStyle(Color.Text.primary) .foregroundStyle(Color.Text.primary)
.multilineTextAlignment(.leading) .multilineTextAlignment(.leading)
.lineLimit(2) .lineLimit(Design.Contacts.notePreviewLineLimit)
Text(label) Text(label)
.typography(.caption) .typography(.caption)
.foregroundStyle(Color.Text.secondary) .foregroundStyle(Color.Text.secondary)

View File

@ -7,7 +7,7 @@ struct ScannerOverlayView: View {
let size = min(geometry.size.width, geometry.size.height) * 0.7 let size = min(geometry.size.width, geometry.size.height) * 0.7
ZStack { ZStack {
Color.black.opacity(Design.Opacity.medium) Color.AppBackground.base.opacity(Design.Opacity.medium)
RoundedRectangle(cornerRadius: Design.CornerRadius.large) RoundedRectangle(cornerRadius: Design.CornerRadius.large)
.frame(width: size, height: size) .frame(width: size, height: size)
@ -27,7 +27,7 @@ struct ScannerOverlayView: View {
.typography(.heading) .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.AppBackground.base.opacity(Design.Opacity.medium))
.clipShape(.rect(cornerRadius: Design.CornerRadius.medium)) .clipShape(.rect(cornerRadius: Design.CornerRadius.medium))
.padding(.bottom, Design.Spacing.xxxLarge) .padding(.bottom, Design.Spacing.xxxLarge)
} }

View File

@ -101,7 +101,7 @@ private struct QRCodeSection: View {
QRCodeView(payload: card.vCardPayload) QRCodeView(payload: card.vCardPayload)
.frame(width: Design.CardSize.qrSizeLarge, height: Design.CardSize.qrSizeLarge) .frame(width: Design.CardSize.qrSizeLarge, height: Design.CardSize.qrSizeLarge)
.padding(Design.Spacing.large) .padding(Design.Spacing.large)
.background(Color.white) .background(Color.ShareSheet.rowBackground)
.clipShape(.rect(cornerRadius: Design.CornerRadius.large)) .clipShape(.rect(cornerRadius: Design.CornerRadius.large))
// Instruction text // Instruction text
@ -146,7 +146,7 @@ private struct AppClipSection: View {
QRCodeView(payload: result.appClipURL.absoluteString) QRCodeView(payload: result.appClipURL.absoluteString)
.frame(width: Design.CardSize.qrSizeLarge, height: Design.CardSize.qrSizeLarge) .frame(width: Design.CardSize.qrSizeLarge, height: Design.CardSize.qrSizeLarge)
.padding(Design.Spacing.large) .padding(Design.Spacing.large)
.background(Color.white) .background(Color.ShareSheet.rowBackground)
.clipShape(.rect(cornerRadius: Design.CornerRadius.large)) .clipShape(.rect(cornerRadius: Design.CornerRadius.large))
// Expiration notice // Expiration notice
@ -190,7 +190,7 @@ private struct AppClipSection: View {
if let error = appClipState.errorMessage { if let error = appClipState.errorMessage {
Text(error) Text(error)
.typography(.caption) .typography(.caption)
.foregroundStyle(.red) .foregroundStyle(Color.Accent.red)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
} }
} }

View File

@ -4,7 +4,7 @@ import Bedrock
/// Displays a vertical list of added contact fields with tap to edit and drag to reorder /// Displays a vertical list of added contact fields with tap to edit and drag to reorder
struct AddedContactFieldsView: View { struct AddedContactFieldsView: View {
@Binding var fields: [AddedContactField] @Binding var fields: [AddedContactField]
var themeColor: Color = Color(red: 0.2, green: 0.2, blue: 0.2) var themeColor: Color = Color.Text.secondary
let onEdit: (AddedContactField) -> Void let onEdit: (AddedContactField) -> Void
@State private var draggingField: AddedContactField? @State private var draggingField: AddedContactField?

View File

@ -3,7 +3,7 @@ import Bedrock
/// Grid view for selecting contact field types to add /// Grid view for selecting contact field types to add
struct ContactFieldPickerView: View { struct ContactFieldPickerView: View {
var themeColor: Color = Color(red: 0.2, green: 0.2, blue: 0.2) var themeColor: Color = Color.Text.secondary
let onSelect: (ContactFieldType) -> Void let onSelect: (ContactFieldType) -> Void
private let columns = Array(repeating: GridItem(.flexible(), spacing: Design.Spacing.medium), count: 3) private let columns = Array(repeating: GridItem(.flexible(), spacing: Design.Spacing.medium), count: 3)

View File

@ -21,7 +21,7 @@ struct FieldRow: View {
.overlay( .overlay(
field.fieldType.iconImage() field.fieldType.iconImage()
.typography(.title3) .typography(.title3)
.foregroundStyle(.white) .foregroundStyle(Color.AppText.inverted)
) )
Button(action: onTap) { Button(action: onTap) {

View File

@ -14,7 +14,7 @@ struct FieldRowPreview: View {
.overlay( .overlay(
field.fieldType.iconImage() field.fieldType.iconImage()
.typography(.title3) .typography(.title3)
.foregroundStyle(.white) .foregroundStyle(Color.AppText.inverted)
) )
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) { VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {

View File

@ -15,7 +15,7 @@ struct FieldTypeButton: View {
.overlay( .overlay(
fieldType.iconImage() fieldType.iconImage()
.typography(.title3) .typography(.title3)
.foregroundStyle(.white) .foregroundStyle(Color.AppText.inverted)
) )
Text(fieldType.displayName) Text(fieldType.displayName)

View File

@ -89,10 +89,10 @@ struct HeaderLayoutPickerView: View {
} label: { } label: {
Text("Confirm layout") Text("Confirm layout")
.typography(.heading) .typography(.heading)
.foregroundStyle(.white) .foregroundStyle(Color.AppText.inverted)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.padding(.vertical, Design.Spacing.large) .padding(.vertical, Design.Spacing.large)
.background(.black) .background(Color.Accent.ink)
.clipShape(.rect(cornerRadius: Design.CornerRadius.medium)) .clipShape(.rect(cornerRadius: Design.CornerRadius.medium))
} }
.padding(.horizontal, Design.Spacing.xLarge) .padding(.horizontal, Design.Spacing.xLarge)

View File

@ -17,7 +17,7 @@ struct LayoutBadge: View {
.padding(.horizontal, Design.Spacing.small) .padding(.horizontal, Design.Spacing.small)
.padding(.vertical, Design.Spacing.xSmall) .padding(.vertical, Design.Spacing.xSmall)
.background(backgroundColor) .background(backgroundColor)
.foregroundStyle(.white) .foregroundStyle(Color.AppText.inverted)
.clipShape(.capsule) .clipShape(.capsule)
} }
} }