// Auteur : Simon-Pierre Boucher — contact@spboucher.ai // Thème « éditorial sharp » de Vrai-Prix : papier grainé, encre, accent rouge vif, // bordures encre 1,5 pt, ombres décalées dures, display uppercase, micro-étiquettes mono. import SwiftUI extension Color { init(hex: UInt32) { self.init( .sRGB, red: Double((hex >> 16) & 0xff) / 255, green: Double((hex >> 8) & 0xff) / 255, blue: Double(hex & 0xff) / 255 ) } static let vpPaper = Color(hex: 0xF5F3EE) static let vpSurface = Color(hex: 0xFFFFFF) static let vpSurface2 = Color(hex: 0xFAF9F5) static let vpInk = Color(hex: 0x141814) static let vpInk2 = Color(hex: 0x4D5551) static let vpInk3 = Color(hex: 0x8B928C) static let vpGreen = Color(hex: 0x9E2A25) // bordeaux (nom hérité du site) static let vpGreenDeep = Color(hex: 0x771F1B) static let vpLime = Color(hex: 0xFF5148) // accent rouge vif (nom hérité du site) static let vpLimeSoft = Color(hex: 0xFFE3E0) } extension Font { /// Display — Space Grotesk Bold (titres, chiffres clés) static func vpDisplay(_ size: CGFloat) -> Font { .custom("SpaceGrotesk-Bold", size: size) } /// Display medium — Space Grotesk Medium static func vpDisplayMedium(_ size: CGFloat) -> Font { .custom("SpaceGrotesk-Medium", size: size) } /// Corps — Inter Regular static func vpBody(_ size: CGFloat) -> Font { .custom("Inter-Regular", size: size) } /// Micro-étiquettes — JetBrains Mono static func vpMono(_ size: CGFloat) -> Font { .custom("JetBrainsMono-Regular", size: size) } static func vpMonoBold(_ size: CGFloat) -> Font { .custom("JetBrainsMono-Bold", size: size) } } /// Carte signature : fond blanc, bordure encre 1,5 pt, ombre décalée dure. struct VPCard: ViewModifier { var corner: CGFloat = 14 func body(content: Content) -> some View { content .background(RoundedRectangle(cornerRadius: corner).fill(Color.vpSurface)) .overlay(RoundedRectangle(cornerRadius: corner).stroke(Color.vpInk, lineWidth: 1.5)) .background( RoundedRectangle(cornerRadius: corner) .fill(Color.vpInk.opacity(0.22)) .offset(x: 5, y: 5) ) } } extension View { func vpCard(corner: CGFloat = 14) -> some View { modifier(VPCard(corner: corner)) } } /// « Kicker » : petite étiquette mono bordeaux précédée d'un trait — signature du site. struct Kicker: View { let text: String var body: some View { HStack(spacing: 8) { Rectangle().fill(Color.vpGreen).frame(width: 22, height: 2) Text(text.uppercased()) .font(.vpMonoBold(10.5)) .tracking(1.6) .foregroundStyle(Color.vpGreen) } } } /// Bouton principal : encre pleine, texte accent, ombre décalée. struct VPPrimaryButtonStyle: ButtonStyle { func makeBody(configuration: Configuration) -> some View { configuration.label .font(.vpDisplay(15)) .foregroundStyle(Color.vpLime) .padding(.horizontal, 20) .padding(.vertical, 13) .frame(maxWidth: .infinity) .background( RoundedRectangle(cornerRadius: 12) .fill(configuration.isPressed ? Color.vpGreenDeep : Color.vpInk) ) .background( RoundedRectangle(cornerRadius: 12) .fill(Color.vpInk.opacity(0.25)) .offset(x: 4, y: 4) ) .scaleEffect(configuration.isPressed ? 0.985 : 1) } } /// Bouton fantôme : bordure encre, fond transparent. struct VPGhostButtonStyle: ButtonStyle { func makeBody(configuration: Configuration) -> some View { configuration.label .font(.vpDisplay(15)) .foregroundStyle(Color.vpInk) .padding(.horizontal, 20) .padding(.vertical, 13) .frame(maxWidth: .infinity) .background( RoundedRectangle(cornerRadius: 12) .fill(configuration.isPressed ? Color.vpLime : Color.clear) ) .overlay(RoundedRectangle(cornerRadius: 12).stroke(Color.vpInk, lineWidth: 1.5)) } } /// Wordmark « Vrai [Prix] » — reproduction du logo de l'en-tête du site. struct Wordmark: View { var size: CGFloat = 24 var body: some View { HStack(spacing: size * 0.18) { Text("Vrai") .font(.vpDisplay(size)) .foregroundStyle(Color.vpInk) Text("Prix") .font(.vpDisplay(size)) .foregroundStyle(Color.vpLime) .padding(.horizontal, size * 0.3) .padding(.vertical, size * 0.1) .background(RoundedRectangle(cornerRadius: size * 0.26).fill(Color.vpInk)) .rotationEffect(.degrees(-2)) } } } /// Champ de saisie signature : bordure, focus accent. struct VPFieldStyle: ViewModifier { func body(content: Content) -> some View { content .font(.vpBody(16)) .foregroundStyle(Color.vpInk) .padding(.horizontal, 14) .padding(.vertical, 12) .background(RoundedRectangle(cornerRadius: 12).fill(Color.vpSurface2)) .overlay(RoundedRectangle(cornerRadius: 12).stroke(Color.vpInk.opacity(0.35), lineWidth: 1.5)) } } extension View { func vpField() -> some View { modifier(VPFieldStyle()) } }