SPB Git

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%
5.5 KB · 147 lines swift
Raw Blame History
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