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>
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 | |