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 {
/// Primary gradient color (warm coral/red).
/// 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).
static let secondary = Color(red: 0.65, green: 0.18, blue: 0.15)
static let secondary = Color("BrandingSecondary")
/// 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,
secondaryColor: Color.Branding.secondary,
accentColor: Color.Branding.accent,
titleColor: .white,
titleColor: Color.Branding.accent,
iconSize: 52,
titleSize: 32,
iconSpacing: 12,

View File

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

View File

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

View File

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

View File

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

View File

@ -91,6 +91,42 @@ extension Design {
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
@ -118,27 +154,27 @@ extension Color {
// MARK: - Card Theme Palette
enum CardPalette {
static let coral = Color(red: 0.95, green: 0.35, blue: 0.33)
static let midnight = Color(red: 0.12, green: 0.16, blue: 0.22)
static let ocean = Color(red: 0.08, green: 0.45, blue: 0.56)
static let lime = Color(red: 0.73, green: 0.82, blue: 0.34)
static let violet = Color(red: 0.42, green: 0.36, blue: 0.62)
static let forest = Color(red: 0.13, green: 0.37, blue: 0.31)
static let rose = Color(red: 0.95, green: 0.68, blue: 0.73)
static let slate = Color(red: 0.38, green: 0.44, blue: 0.50)
static let amber = Color(red: 0.98, green: 0.75, blue: 0.28)
static let plum = Color(red: 0.56, green: 0.27, blue: 0.52)
static let sand = Color(red: 0.93, green: 0.83, blue: 0.68)
static let coral = Color("CardPaletteCoral")
static let midnight = Color("CardPaletteMidnight")
static let ocean = Color("CardPaletteOcean")
static let lime = Color("CardPaletteLime")
static let violet = Color("CardPaletteViolet")
static let forest = Color("CardPaletteForest")
static let rose = Color("CardPaletteRose")
static let slate = Color("CardPaletteSlate")
static let amber = Color("CardPaletteAmber")
static let plum = Color("CardPalettePlum")
static let sand = Color("CardPaletteSand")
}
// MARK: - App Accent Colors
enum AppAccent {
static let red = Color(red: 0.95, green: 0.33, blue: 0.28)
static let gold = Color(red: 0.95, green: 0.75, blue: 0.25)
static let mint = Color(red: 0.2, green: 0.65, blue: 0.55)
static let ink = Color(red: 0.12, green: 0.12, blue: 0.14)
static let slate = Color(red: 0.29, green: 0.33, blue: 0.4)
static let red = Color("AppAccentRed")
static let gold = Color("AppAccentGold")
static let mint = Color("AppAccentMint")
static let ink = Color("AppAccentInk")
static let slate = Color("AppAccentSlate")
}
// MARK: - App Text Colors
@ -153,8 +189,8 @@ extension Color {
// MARK: - Badge Colors
enum Badge {
static let star = Color(red: 0.98, green: 0.82, blue: 0.34)
static let neutral = Color(red: 0.89, green: 0.89, blue: 0.9)
static let star = Color("BadgeStar")
static let neutral = Color("BadgeNeutral")
}
// MARK: - Share Sheet Theme
@ -170,17 +206,17 @@ extension Color {
// MARK: - Social Media Brand Colors
enum Social {
static let linkedIn = Color(red: 0.0, green: 0.47, blue: 0.71)
static let twitter = Color(red: 0.11, green: 0.63, blue: 0.95)
static let instagram = Color(red: 0.88, green: 0.19, blue: 0.42)
static let facebook = Color(red: 0.26, green: 0.40, blue: 0.70)
static let tiktok = Color(red: 0.0, green: 0.0, blue: 0.0)
static let github = Color(red: 0.13, green: 0.13, blue: 0.13)
static let threads = Color(red: 0.0, green: 0.0, blue: 0.0)
static let telegram = Color(red: 0.16, green: 0.63, blue: 0.89)
static let whatsapp = Color(red: 0.15, green: 0.68, blue: 0.38)
static let venmo = Color(red: 0.22, green: 0.53, blue: 0.79)
static let cashApp = Color(red: 0.0, green: 0.82, blue: 0.35)
static let linkedIn = Color("SocialLinkedIn")
static let twitter = Color("SocialTwitter")
static let instagram = Color("SocialInstagram")
static let facebook = Color("SocialFacebook")
static let tiktok = Color("SocialTikTok")
static let github = Color("SocialGitHub")
static let threads = Color("SocialThreads")
static let telegram = Color("SocialTelegram")
static let whatsapp = Color("SocialWhatsApp")
static let venmo = Color("SocialVenmo")
static let cashApp = Color("SocialCashApp")
}
}

View File

@ -1,4 +1,5 @@
import SwiftUI
import Bedrock
/// Card theme configuration supporting both preset themes and custom colors.
///
@ -88,8 +89,11 @@ struct CardTheme: Identifiable, Hashable, Sendable {
var requiresDarkText: Bool {
if let rgb = customRGB {
// Calculate perceived luminance for custom colors
let luminance = 0.299 * rgb.0 + 0.587 * rgb.1 + 0.114 * rgb.2
return luminance > 0.5
let luminance =
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 }
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)
@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 {
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 {
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.
@MainActor var textColor: Color {
Color(red: textRGB.0, green: textRGB.1, blue: textRGB.2)
requiresDarkText ? Color.Text.primary : Color.AppText.inverted
}
// MARK: - Hashable & Equatable

View File

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

View File

@ -1,10 +1,11 @@
import SwiftUI
import UIKit
import Bedrock
extension UIImage {
/// Extracts dominant colors from the image using pixel sampling.
/// - 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] {
guard let cgImage = self.cgImage else {
return defaultColors(count: count)
@ -18,14 +19,14 @@ extension UIImage {
}
// Create a smaller sample size for performance
let sampleSize = 50
let sampleSize = Design.ColorExtraction.sampleSize
guard let context = CGContext(
data: nil,
width: sampleSize,
height: sampleSize,
bitsPerComponent: 8,
bytesPerRow: sampleSize * 4,
bytesPerRow: sampleSize * Design.ColorExtraction.rgbaBytesPerPixel,
space: CGColorSpaceCreateDeviceRGB(),
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
) else {
@ -38,7 +39,10 @@ extension UIImage {
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
var colorCounts: [ColorBucket: Int] = [:]
@ -52,17 +56,18 @@ extension UIImage {
let a = data[offset + 3]
// 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)
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)
let bucket = ColorBucket(
r: UInt8((Int(r) / 32) * 32),
g: UInt8((Int(g) / 32) * 32),
b: UInt8((Int(b) / 32) * 32)
r: UInt8((Int(r) / Design.ColorExtraction.bucketQuantizationStep) * Design.ColorExtraction.bucketQuantizationStep),
g: UInt8((Int(g) / Design.ColorExtraction.bucketQuantizationStep) * Design.ColorExtraction.bucketQuantizationStep),
b: UInt8((Int(b) / Design.ColorExtraction.bucketQuantizationStep) * Design.ColorExtraction.bucketQuantizationStep)
)
colorCounts[bucket, default: 0] += 1
@ -77,9 +82,9 @@ extension UIImage {
// Add top colors, ensuring they're distinct
for (bucket, _) in sortedColors {
let color = Color(
red: Double(bucket.r) / 255.0,
green: Double(bucket.g) / 255.0,
blue: Double(bucket.b) / 255.0
red: Double(bucket.r) / Design.ColorExtraction.rgbDenominator,
green: Double(bucket.g) / Design.ColorExtraction.rgbDenominator,
blue: Double(bucket.b) / Design.ColorExtraction.rgbDenominator
)
// Check if this color is distinct enough from existing ones
@ -92,20 +97,20 @@ extension UIImage {
}
}
// Always include white as first option
var result: [Color] = [.white]
// Always include high-contrast app fallback colors first.
var result: [Color] = [Color.AppText.inverted]
result.append(contentsOf: extractedColors)
// Add black if we have room
// Add primary text color if we have room.
if result.count < count {
result.append(.black)
result.append(Color.Text.primary)
}
return Array(result.prefix(count))
}
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))
}
}
@ -122,7 +127,7 @@ private struct ColorBucket: Hashable {
private extension Color {
/// 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,
let otherComponents = other.cgColor?.components,
selfComponents.count >= 3,
@ -137,4 +142,3 @@ private extension Color {
return rDiff < threshold && gDiff < threshold && bDiff < threshold
}
}

View File

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

View File

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

View File

@ -573,7 +573,7 @@ private struct ContactFieldRowView: View {
.background(Color.AppBackground.base.opacity(0.82))
.overlay(
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))
.contentShape(.rect)

View File

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

View File

@ -26,7 +26,7 @@ struct ContactFieldEditorSheet: View {
fieldType: ContactFieldType,
initialValue: 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,
onDelete: (() -> Void)? = nil
) {

View File

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

View File

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

View File

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

View File

@ -89,7 +89,7 @@ struct PhotoCropperSheet: View {
GeometryReader { geometry in
ZStack {
// Dark background
Color.black.ignoresSafeArea()
Color.AppBackground.base.ignoresSafeArea()
// Image with gestures
if let uiImage {
@ -129,7 +129,7 @@ struct PhotoCropperSheet: View {
dismiss()
}
}
.foregroundStyle(.white)
.foregroundStyle(Color.AppText.inverted)
}
ToolbarItem(placement: .principal) {
HStack(spacing: Design.Spacing.xLarge) {
@ -138,7 +138,7 @@ struct PhotoCropperSheet: View {
rotate90Left()
} label: {
Image(systemName: "rotate.left")
.foregroundStyle(.white)
.foregroundStyle(Color.AppText.inverted)
}
// Reset all transforms
@ -146,7 +146,7 @@ struct PhotoCropperSheet: View {
resetTransform()
} label: {
Image(systemName: "arrow.counterclockwise")
.foregroundStyle(.white)
.foregroundStyle(Color.AppText.inverted)
}
// Aspect ratio picker (only for logo workflow)
@ -155,7 +155,7 @@ struct PhotoCropperSheet: View {
showingAspectRatioPicker = true
} label: {
Image(systemName: "aspectratio")
.foregroundStyle(.white)
.foregroundStyle(Color.AppText.inverted)
}
}
@ -164,7 +164,7 @@ struct PhotoCropperSheet: View {
rotate90Right()
} label: {
Image(systemName: "rotate.right")
.foregroundStyle(.white)
.foregroundStyle(Color.AppText.inverted)
}
}
}
@ -173,7 +173,7 @@ struct PhotoCropperSheet: View {
cropAndSave()
}
.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)
}
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 {
HStack(spacing: Design.Spacing.medium) {
ContactAvatarView(contact: contact)
@ -54,10 +58,14 @@ struct ContactRowView: View {
ForEach(contact.tagList.prefix(2), id: \.self) { tag in
Text(tag)
.typography(.caption2)
.foregroundStyle(Color.Text.secondary)
.foregroundStyle(chipTextColor)
.padding(.horizontal, Design.Spacing.xSmall)
.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))
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -3,7 +3,7 @@ import Bedrock
/// Grid view for selecting contact field types to add
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
private let columns = Array(repeating: GridItem(.flexible(), spacing: Design.Spacing.medium), count: 3)

View File

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

View File

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

View File

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

View File

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

View File

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