spb/vrai-prix-ios Public
App iOS native de Vrai-Prix — estimation immobilière transparente pour le Québec (SwiftUI, MapKit, Swift Charts)
Swift 100%
1// Auteur : Simon-Pierre Boucher — contact@spboucher.ai2// Thème « éditorial sharp » de Vrai-Prix : papier grainé, encre, accent rouge vif,3// bordures encre 1,5 pt, ombres décalées dures, display uppercase, micro-étiquettes mono.4import SwiftUI56extension Color {7 init(hex: UInt32) {8 self.init(9 .sRGB,10 red: Double((hex >> 16) & 0xff) / 255,11 green: Double((hex >> 8) & 0xff) / 255,12 blue: Double(hex & 0xff) / 25513 )14 }1516 static let vpPaper = Color(hex: 0xF5F3EE)17 static let vpSurface = Color(hex: 0xFFFFFF)18 static let vpSurface2 = Color(hex: 0xFAF9F5)19 static let vpInk = Color(hex: 0x141814)20 static let vpInk2 = Color(hex: 0x4D5551)21 static let vpInk3 = Color(hex: 0x8B928C)22 static let vpGreen = Color(hex: 0x9E2A25) // bordeaux (nom hérité du site)23 static let vpGreenDeep = Color(hex: 0x771F1B)24 static let vpLime = Color(hex: 0xFF5148) // accent rouge vif (nom hérité du site)25 static let vpLimeSoft = Color(hex: 0xFFE3E0)26}2728extension Font {29 /// Display — Space Grotesk Bold (titres, chiffres clés)30 static func vpDisplay(_ size: CGFloat) -> Font { .custom("SpaceGrotesk-Bold", size: size) }31 /// Display medium — Space Grotesk Medium32 static func vpDisplayMedium(_ size: CGFloat) -> Font { .custom("SpaceGrotesk-Medium", size: size) }33 /// Corps — Inter Regular34 static func vpBody(_ size: CGFloat) -> Font { .custom("Inter-Regular", size: size) }35 /// Micro-étiquettes — JetBrains Mono36 static func vpMono(_ size: CGFloat) -> Font { .custom("JetBrainsMono-Regular", size: size) }37 static func vpMonoBold(_ size: CGFloat) -> Font { .custom("JetBrainsMono-Bold", size: size) }38}3940/// Carte signature : fond blanc, bordure encre 1,5 pt, ombre décalée dure.41struct VPCard: ViewModifier {42 var corner: CGFloat = 1443 func body(content: Content) -> some View {44 content45 .background(RoundedRectangle(cornerRadius: corner).fill(Color.vpSurface))46 .overlay(RoundedRectangle(cornerRadius: corner).stroke(Color.vpInk, lineWidth: 1.5))47 .background(48 RoundedRectangle(cornerRadius: corner)49 .fill(Color.vpInk.opacity(0.22))50 .offset(x: 5, y: 5)51 )52 }53}5455extension View {56 func vpCard(corner: CGFloat = 14) -> some View { modifier(VPCard(corner: corner)) }57}5859/// « Kicker » : petite étiquette mono bordeaux précédée d'un trait — signature du site.60struct Kicker: View {61 let text: String62 var body: some View {63 HStack(spacing: 8) {64 Rectangle().fill(Color.vpGreen).frame(width: 22, height: 2)65 Text(text.uppercased())66 .font(.vpMonoBold(10.5))67 .tracking(1.6)68 .foregroundStyle(Color.vpGreen)69 }70 }71}7273/// Bouton principal : encre pleine, texte accent, ombre décalée.74struct VPPrimaryButtonStyle: ButtonStyle {75 func makeBody(configuration: Configuration) -> some View {76 configuration.label77 .font(.vpDisplay(15))78 .foregroundStyle(Color.vpLime)79 .padding(.horizontal, 20)80 .padding(.vertical, 13)81 .frame(maxWidth: .infinity)82 .background(83 RoundedRectangle(cornerRadius: 12)84 .fill(configuration.isPressed ? Color.vpGreenDeep : Color.vpInk)85 )86 .background(87 RoundedRectangle(cornerRadius: 12)88 .fill(Color.vpInk.opacity(0.25))89 .offset(x: 4, y: 4)90 )91 .scaleEffect(configuration.isPressed ? 0.985 : 1)92 }93}9495/// Bouton fantôme : bordure encre, fond transparent.96struct VPGhostButtonStyle: ButtonStyle {97 func makeBody(configuration: Configuration) -> some View {98 configuration.label99 .font(.vpDisplay(15))100 .foregroundStyle(Color.vpInk)101 .padding(.horizontal, 20)102 .padding(.vertical, 13)103 .frame(maxWidth: .infinity)104 .background(105 RoundedRectangle(cornerRadius: 12)106 .fill(configuration.isPressed ? Color.vpLime : Color.clear)107 )108 .overlay(RoundedRectangle(cornerRadius: 12).stroke(Color.vpInk, lineWidth: 1.5))109 }110}111112/// Wordmark « Vrai [Prix] » — reproduction du logo de l'en-tête du site.113struct Wordmark: View {114 var size: CGFloat = 24115 var body: some View {116 HStack(spacing: size * 0.18) {117 Text("Vrai")118 .font(.vpDisplay(size))119 .foregroundStyle(Color.vpInk)120 Text("Prix")121 .font(.vpDisplay(size))122 .foregroundStyle(Color.vpLime)123 .padding(.horizontal, size * 0.3)124 .padding(.vertical, size * 0.1)125 .background(RoundedRectangle(cornerRadius: size * 0.26).fill(Color.vpInk))126 .rotationEffect(.degrees(-2))127 }128 }129}130131/// Champ de saisie signature : bordure, focus accent.132struct VPFieldStyle: ViewModifier {133 func body(content: Content) -> some View {134 content135 .font(.vpBody(16))136 .foregroundStyle(Color.vpInk)137 .padding(.horizontal, 14)138 .padding(.vertical, 12)139 .background(RoundedRectangle(cornerRadius: 12).fill(Color.vpSurface2))140 .overlay(RoundedRectangle(cornerRadius: 12).stroke(Color.vpInk.opacity(0.35), lineWidth: 1.5))141 }142}143144extension View {145 func vpField() -> some View { modifier(VPFieldStyle()) }146}147