SPB Git forge

spb/lou-ka-ios

Public

Lou·Ka iOS — app SwiftUI native de l'agrégateur de logements du Québec : filtres avancés, carte, stats, et mode Découverte (swipe) avec recommandation on-device

3commits 1branches 0releases
4.7 MBsize
maindefault branch
1 mo agolast push
Swift 100%

Identité visuelle officielle Lou-Ka — Theme.swift : palette marine #0B1330 / bleu vif #2258FF / orange #FF6A00 (alias lime/green conservés pour les vues), SF Pro au lieu de Space Grotesk/JetBrains Mono, cartes coins 18 px + ombre douce (fin des ombres décalées), BrandMark « Lou-Ka » (Ka orange, sans chip), badge type orange/blanc, chips sélection bleue, ticker marine/blanc, kicker tiret orange + texte bleu

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Simon-Pierre Boucher committed 1 mo ago (Aug 15, 2026) parent 2757adb

1 changed file +62 −77

modified LouKa/Theme.swift +62 −77
@@ -1,10 +1,11 @@
1 1 // -----------------------------------------------------------------------------
2 2 // Lou-Ka — Agrégateur de logements à louer (province de Québec)
3 3 // Auteur : Simon-Pierre Boucher — contact@spboucher.ai
4 −// Theme.swift : système de design « éditorial sharp » porté en SwiftUI
5 −// · palette papier / encre / vert profond / lime électrique (styles.css)
6 −// · typographie de marque : Space Grotesk (display) + JetBrains Mono (micro)
7 −// · signature : bordures encre + ombres décalées (néo-brutalisme raffiné)
4 +// Theme.swift : identité visuelle officielle Lou-Ka portée en SwiftUI
5 +// · palette : marine #0B1330 · bleu vif #2258FF · orange #FF6A00
6 +// gris pâle #F2F2F2 · blanc #FFFFFF (styles.css)
7 +// · typographie : SF Pro (système) — équivalent natif d'Inter
8 +// · signature : coins très arrondis, pill-shaped, ombres douces diffuses
8 9 // · thème clair UNIQUEMENT (comme le site) — voir .preferredColorScheme
9 10 // · formatage fr-CA (prix, dates, distances)
10 11 // -----------------------------------------------------------------------------
@@ -23,105 +24,95 @@ extension Color {
23 24 }
24 25
25 26 enum LK {
26 − static let paper = Color(hex: 0xF5F3EE)
27 + // palette officielle Lou-Ka
28 + static let navy = Color(hex: 0x0B1330)
29 + static let blue = Color(hex: 0x2258FF)
30 + static let orange = Color(hex: 0xFF6A00)
31 + static let grey = Color(hex: 0xF2F2F2)
32 +
33 + static let paper = Color.white
27 34 static let surface = Color.white
28 − static let surface2 = Color(hex: 0xFAF9F5)
29 − static let ink = Color(hex: 0x141814)
30 − static let ink2 = Color(hex: 0x4D5551)
31 − static let ink3 = Color(hex: 0x8B928C)
32 − static let green = Color(hex: 0x1C5C41)
33 − static let greenDeep = Color(hex: 0x123F2E)
34 − static let lime = Color(hex: 0xD9F26B)
35 − static let limeSoft = Color(hex: 0xF0F9D2)
36 − static let amber = Color(hex: 0xE8A33D)
37 − static let amberSoft = Color(hex: 0xFDF3E2)
38 − static let danger = Color(hex: 0xB3423A)
39 − static let line = Color(hex: 0x141814).opacity(0.14)
35 + static let surface2 = Color(hex: 0xF2F2F2)
36 + static let ink = navy
37 + static let ink2 = Color(hex: 0x3E4560)
38 + static let ink3 = Color(hex: 0x8A90A5)
39 + // alias hérités (les vues existantes restent cohérentes)
40 + static let green = blue
41 + static let greenDeep = navy
42 + static let lime = orange
43 + static let limeSoft = Color(hex: 0xFFF1E6)
44 + static let amber = orange
45 + static let amberSoft = Color(hex: 0xFFF1E6)
46 + static let danger = Color(hex: 0xD64545)
47 + static let line = navy.opacity(0.10)
40 48 }
41 49
42 50 // MARK: - Typographie de marque
43 51
44 −/// Polices embarquées (Fonts/) avec repli système si l'enregistrement échoue.
52 +/// Typographie officielle : SF Pro (système) — l'équivalent natif d'Inter.
45 53 enum LKFont {
46 − private static let hasGrotesk = UIFont(name: "SpaceGrotesk-Bold", size: 12) != nil
47 − private static let hasMono = UIFont(name: "JetBrainsMono-Regular", size: 12) != nil
48 −
49 − /// Titres & prix — Space Grotesk (géométrique, signature du site)
54 + /// Titres & prix — SF Pro Display
50 55 static func display(_ size: CGFloat, _ weight: Font.Weight = .bold) -> Font {
51 − guard hasGrotesk else { return .system(size: size, weight: weight) }
52 − switch weight {
53 − case .bold, .heavy, .black: return .custom("SpaceGrotesk-Bold", size: size)
54 − case .medium, .semibold: return .custom("SpaceGrotesk-Medium", size: size)
55 − default: return .custom("SpaceGrotesk-Regular", size: size)
56 − }
56 + .system(size: size, weight: weight)
57 57 }
58 58
59 − /// Micro-étiquettes, chiffres, tags — JetBrains Mono
59 + /// Micro-étiquettes, chiffres, tags — SF Pro (plus de mono)
60 60 static func mono(_ size: CGFloat, _ weight: Font.Weight = .regular) -> Font {
61 − guard hasMono else { return .system(size: size, weight: weight, design: .monospaced) }
62 − switch weight {
63 − case .bold, .heavy, .black: return .custom("JetBrainsMono-Bold", size: size)
64 − case .medium, .semibold: return .custom("JetBrainsMono-Medium", size: size)
65 − default: return .custom("JetBrainsMono-Regular", size: size)
66 − }
61 + .system(size: size, weight: weight == .regular ? .medium : weight)
67 62 }
68 63 }
69 64
70 −// MARK: - Carte à ombre décalée (signature visuelle)
65 +// MARK: - Carte aux coins très arrondis + ombre douce (signature visuelle)
71 66
72 67 struct LKCardModifier: ViewModifier {
73 − var radius: CGFloat = 10
74 − var offset: CGFloat = 5
75 − var borderWidth: CGFloat = 1.8
68 + var radius: CGFloat = 18
69 + var offset: CGFloat = 5 // conservé pour compatibilité (ignoré)
70 + var borderWidth: CGFloat = 1
76 71
77 72 func body(content: Content) -> some View {
78 73 content
79 74 .background(LK.surface)
80 75 .clipShape(RoundedRectangle(cornerRadius: radius))
81 − .overlay(RoundedRectangle(cornerRadius: radius).stroke(LK.ink, lineWidth: borderWidth))
82 − .background(
83 − RoundedRectangle(cornerRadius: radius)
84 − .fill(LK.ink)
85 − .offset(x: offset, y: offset)
86 − )
76 + .overlay(RoundedRectangle(cornerRadius: radius).stroke(LK.line, lineWidth: borderWidth))
77 + .shadow(color: LK.navy.opacity(0.08), radius: 12, x: 0, y: 6)
87 78 }
88 79 }
89 80
90 81 extension View {
91 − func lkCard(radius: CGFloat = 10, offset: CGFloat = 5, borderWidth: CGFloat = 1.8) -> some View {
82 + func lkCard(radius: CGFloat = 18, offset: CGFloat = 5, borderWidth: CGFloat = 1) -> some View {
92 83 modifier(LKCardModifier(radius: radius, offset: offset, borderWidth: borderWidth))
93 84 }
94 85 }
95 86
96 87 // MARK: - Micro-composants
97 88
98 −/// Étiquette mono majuscule avec tiret vert — le « kicker » du site web
89 +/// Étiquette majuscule avec tiret orange — le « kicker » du site web
99 90 struct Kicker: View {
100 91 let text: String
101 92
102 93 var body: some View {
103 94 HStack(spacing: 8) {
104 − Rectangle().fill(LK.green).frame(width: 22, height: 2)
95 + Capsule().fill(LK.orange).frame(width: 22, height: 3)
105 96 Text(text.uppercased())
106 − .font(LKFont.mono(11, .medium))
97 + .font(LKFont.mono(11, .semibold))
107 98 .kerning(1.4)
108 − .foregroundStyle(LK.green)
99 + .foregroundStyle(LK.blue)
109 100 }
110 101 }
111 102 }
112 103
113 −/// Pastille type de logement (encre sur lime) — ex. « 4½ »
104 +/// Pastille type de logement (blanc sur orange) — ex. « 4½ »
114 105 struct UnitTypeBadge: View {
115 106 let type: String
116 107
117 108 var body: some View {
118 109 Text(type)
119 110 .font(LKFont.mono(12, .bold))
120 − .padding(.horizontal, 8)
111 + .padding(.horizontal, 10)
121 112 .padding(.vertical, 4)
122 − .background(LK.ink)
123 − .foregroundStyle(LK.lime)
124 − .clipShape(RoundedRectangle(cornerRadius: 5))
113 + .background(LK.orange)
114 + .foregroundStyle(.white)
115 + .clipShape(Capsule())
125 116 }
126 117 }
127 118
@@ -134,12 +125,12 @@ struct Chip: View {
134 125 var body: some View {
135 126 let core = Text(label)
136 127 .font(LKFont.display(13.5, .medium))
137 − .padding(.horizontal, 14)
128 + .padding(.horizontal, 16)
138 129 .padding(.vertical, 8)
139 − .background(selected ? LK.ink : LK.surface)
140 − .foregroundStyle(selected ? LK.lime : LK.ink)
130 + .background(selected ? LK.blue : LK.surface)
131 + .foregroundStyle(selected ? .white : LK.navy)
141 132 .clipShape(Capsule())
142 − .overlay(Capsule().stroke(selected ? LK.ink : LK.ink.opacity(0.35), lineWidth: 1.5))
133 + .overlay(Capsule().stroke(selected ? LK.blue : LK.navy.opacity(0.18), lineWidth: 1))
143 134 if let action {
144 135 Button(action: action) { core }.buttonStyle(.plain)
145 136 } else {
@@ -148,7 +139,7 @@ struct Chip: View {
148 139 }
149 140 }
150 141
151 −/// Ruban défilant encre/lime — équivalent du « ticker » du site
142 +/// Ruban défilant marine/blanc — équivalent du « ticker » du site
152 143 struct TickerBar: View {
153 144 let text: String
154 145 @State private var segWidth: CGFloat = 0
@@ -157,7 +148,7 @@ struct TickerBar: View {
157 148 Text("\(text) ◆ ")
158 149 .font(LKFont.mono(11, .medium))
159 150 .kerning(1.2)
160 − .foregroundStyle(LK.lime)
151 + .foregroundStyle(.white.opacity(0.85))
161 152 .fixedSize()
162 153 }
163 154
@@ -185,31 +176,25 @@ struct TickerBar: View {
185 176 }
186 177 .frame(maxWidth: .infinity, alignment: .leading)
187 178 .padding(.vertical, 8)
188 − .background(LK.ink)
189 − .clipShape(RoundedRectangle(cornerRadius: 7))
190 − .overlay(RoundedRectangle(cornerRadius: 7).stroke(LK.ink, lineWidth: 1.5))
179 + .background(LK.navy)
180 + .clipShape(RoundedRectangle(cornerRadius: 12))
191 181 }
192 182 }
193 183
194 −/// Marque « Lou·Ka » — le « Ka » en encre sur lime, légèrement incliné
184 +/// Marque « Lou-Ka » — « Lou- » marine, « Ka » orange (wordmark officiel)
195 185 struct BrandMark: View {
196 186 var size: CGFloat = 30
197 187
198 188 var body: some View {
199 − HStack(alignment: .center, spacing: 3) {
200 − Text("Lou")
189 + HStack(alignment: .center, spacing: 0) {
190 + Text("Lou-")
201 191 .font(LKFont.display(size, .bold))
202 − .kerning(-1)
203 − .foregroundStyle(LK.ink)
192 + .kerning(-0.5)
193 + .foregroundStyle(LK.navy)
204 194 Text("Ka")
205 − .font(LKFont.display(size * 0.86, .bold))
206 − .kerning(-1)
207 − .padding(.horizontal, size * 0.24)
208 − .padding(.vertical, 2)
209 − .background(LK.ink)
210 − .foregroundStyle(LK.lime)
211 − .clipShape(RoundedRectangle(cornerRadius: 6))
212 − .rotationEffect(.degrees(-2))
195 + .font(LKFont.display(size, .bold))
196 + .kerning(-0.5)
197 + .foregroundStyle(LK.orange)
213 198 }
214 199 }
215 200 }
216 201