SPB Git forge

spb/ka-ios

Public
20commits 1branches 0releases
17.2 MBsize
maindefault branch
28 days agolast push
Swift 100%

v3.0.0 — accueil LÉGENDAIRE v2 : HÉROS ENCRE pleine largeur (kicker date fr_CA + pastille LIVE, salutation display blanche, pilule de recherche centrale → onglet Recherche via .kaSwitchTab, ticker du pouls intégré, lueur lime + filigrane KA, ombre dure lime), RAMPE DE LANCEMENT des 12 univers (logos officiels, ombres décalées accent, favoris d'abord), POULS redessiné (logo + compteur roulant display + unité mono), vitrine « LE CHOIX DU MOMENT » (UniverseShowcaseCard réutilisée, dédupliquée des sections), en-têtes de section mono unifiés — 12 tests verts

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

2 changed files +188 −37

modified KA/App/KAApp.swift +11 −0
@@ -74,6 +74,12 @@ struct RootView: View {
74 74 .animation(.spring(response: 0.4, dampingFraction: 0.8), value: trajet.navigating)
75 75 .animation(.spring(response: 0.4, dampingFraction: 0.8), value: trajet.discovering)
76 76 .onChange(of: tab) { _, t in loaded.insert(t) }
77 + // navigation demandée par une vue (ex. pilule de recherche de l'accueil)
78 + .onReceive(NotificationCenter.default.publisher(for: .kaSwitchTab)) { note in
79 + if let i = note.object as? Int, (0..<6).contains(i) {
80 + withAnimation(.spring(response: 0.35, dampingFraction: 0.8)) { tab = i }
81 + }
82 + }
77 83 .task { await recents.refreshAlerts() }
78 84 .sheet(isPresented: $showAgent) {
79 85 AgentChatView()
@@ -92,6 +98,11 @@ struct RootView: View {
92 98 }
93 99 }
94 100
101 +extension Notification.Name {
102 + /// Demande de changement d'onglet (object = index Int)
103 + static let kaSwitchTab = Notification.Name("ka.switch.tab")
104 +}
105 +
95 106 /// Onglet Carte : la carte unifiée directement (pas de fermeture — c'est un onglet)
96 107 struct MapTab: View {
97 108 var body: some View {
modified KA/Features/HomeView.swift +177 −37
@@ -18,13 +18,15 @@ struct HomeView: View {
18 18 var body: some View {
19 19 NavigationStack {
20 20 ScrollView {
21 − VStack(alignment: .leading, spacing: 22) {
22 − header
23 − tickerStrip
24 − pulseStrip
21 + VStack(alignment: .leading, spacing: 24) {
22 + heroCard
23 + launcher
24 + pulseSection
25 25 resumeStrip
26 + spotlight
26 27 ForEach(featured, id: \.universe.id) { section in
27 − featuredSection(section.universe, section.items)
28 + featuredSection(section.universe,
29 + section.items.filter { $0.id != spotlightPick?.1.id })
28 30 }
29 31 editorialSection
30 32 if loading && featured.isEmpty {
@@ -70,57 +72,195 @@ struct HomeView: View {
70 72 }
71 73 }
72 74
73 − private var header: some View {
74 − VStack(alignment: .leading, spacing: 8) {
75 − // kicker mono — la micro-étiquette signature des sites
76 − HStack(spacing: 6) {
75 + // MARK: héros encre — la pièce maîtresse de l'accueil
76 +
77 + private var dateKicker: String {
78 + Date.now.formatted(.dateTime.weekday(.wide).day().month(.wide)
79 + .locale(Locale(identifier: "fr_CA"))).uppercased() + " · QUÉBEC EN DIRECT"
80 + }
81 +
82 + private var heroCard: some View {
83 + VStack(alignment: .leading, spacing: 13) {
84 + HStack(spacing: 8) {
77 85 Rectangle().fill(KATheme.lime).frame(width: 8, height: 8)
78 − Text("GROUPE KA — L'ÉCOSYSTÈME QUÉBÉCOIS")
79 − .font(KAFont.mono(10))
80 − .foregroundStyle(KATheme.ink2(scheme))
86 + Text(dateKicker)
87 + .font(KAFont.mono(9.5))
88 + .kerning(1.1)
89 + .foregroundStyle(KATheme.inkDark.opacity(0.75))
90 + .lineLimit(1)
91 + .minimumScaleFactor(0.8)
92 + Spacer(minLength: 0)
93 + HStack(spacing: 5) {
94 + Circle().fill(KATheme.lime).frame(width: 6, height: 6)
95 + Text("LIVE").font(KAFont.mono(9)).foregroundStyle(KATheme.lime)
96 + }
81 97 }
82 − .accessibilityHidden(true)
83 98 Text(greeting.title)
84 99 .font(KAFont.display(34))
85 − .kaHighlighted()
86 − Text(greeting.sub).font(.subheadline).foregroundStyle(KATheme.ink2(scheme))
100 + .foregroundStyle(.white)
101 + Text(greeting.sub)
102 + .font(.subheadline)
103 + .foregroundStyle(KATheme.inkDark.opacity(0.72))
104 +
105 + // la pilule de recherche — LE geste central de la super-app
106 + Button {
107 + Haptics.tap()
108 + NotificationCenter.default.post(name: .kaSwitchTab, object: 1)
109 + } label: {
110 + HStack(spacing: 10) {
111 + Image(systemName: "magnifyingglass")
112 + .font(.subheadline.weight(.bold))
113 + .foregroundStyle(KATheme.green)
114 + Text("Chercher dans tout le Québec…")
115 + .font(.subheadline.weight(.semibold))
116 + .foregroundStyle(KATheme.inkLight.opacity(0.7))
117 + Spacer(minLength: 0)
118 + Text("12 UNIVERS")
119 + .font(KAFont.mono(8.5))
120 + .foregroundStyle(.white)
121 + .padding(.horizontal, 8).padding(.vertical, 4)
122 + .background(KATheme.green, in: Capsule())
123 + }
124 + .padding(.horizontal, 14)
125 + .frame(height: 46)
126 + .background(.white, in: Capsule())
127 + }
128 + .buttonStyle(KAPressStyle())
129 + .accessibilityLabel("Chercher dans les 12 univers du Québec")
130 +
131 + // le pouls live qui défile, intégré au héros
132 + KATicker(items: tickerItems)
133 + }
134 + .padding(18)
135 + .background {
136 + ZStack(alignment: .topTrailing) {
137 + RoundedRectangle(cornerRadius: 22, style: .continuous)
138 + .fill(KATheme.inkLight)
139 + Circle().fill(KATheme.lime.opacity(0.20))
140 + .frame(width: 240).blur(radius: 60)
141 + .offset(x: 70, y: -80)
142 + Text("KA")
143 + .font(KAFont.display(130))
144 + .foregroundStyle(.white.opacity(0.05))
145 + .offset(x: 6, y: -28)
146 + .accessibilityHidden(true)
147 + }
148 + .clipShape(RoundedRectangle(cornerRadius: 22, style: .continuous))
87 149 }
150 + .overlay(RoundedRectangle(cornerRadius: 22, style: .continuous)
151 + .strokeBorder(.white.opacity(0.09), lineWidth: 1))
152 + .background(RoundedRectangle(cornerRadius: 22, style: .continuous)
153 + .fill(KATheme.lime.opacity(scheme == .dark ? 0.30 : 0.9))
154 + .offset(x: 6, y: 6))
88 155 }
89 156
90 − /// Ticker encre : le pouls live de l'écosystème qui défile, comme les héros des sites
91 − private var tickerStrip: some View {
92 − KATicker(items: pulse.prefix(8).map {
157 + private var tickerItems: [String] {
158 + pulse.prefix(8).map {
93 159 "\($0.1.formatted(.number.locale(Locale(identifier: "fr_CA")))) \($0.0.unit)"
94 − })
160 + }
95 161 }
96 162
97 − private var pulseStrip: some View {
98 − ScrollView(.horizontal, showsIndicators: false) {
99 − HStack(spacing: 10) {
100 − ForEach(pulse, id: \.0.id) { (u, n) in
101 − NavigationLink(value: u.id) {
102 − VStack(alignment: .leading, spacing: 3) {
103 − KACountUp(value: n,
104 − font: .system(.headline, design: .rounded).weight(.bold),
105 − color: u.accent)
106 − Text(u.unit).font(KAFont.mono(9)).textCase(.uppercase)
107 − .foregroundStyle(.secondary)
163 + // MARK: rampe de lancement des 12 univers (favoris d'abord)
164 +
165 + private var launcherUniverses: [Universe] {
166 + let favs = favUniversesRaw.split(separator: ",").map(String.init)
167 + let favU = favs.compactMap(Ecosystem.universe)
168 + return favU + Ecosystem.all.filter { !favs.contains($0.id) }
169 + }
170 +
171 + private var launcher: some View {
172 + VStack(alignment: .leading, spacing: 10) {
173 + UniverseSectionHeader(title: "Vos univers", accent: KATheme.green, detail: "12")
174 + ScrollView(.horizontal, showsIndicators: false) {
175 + HStack(spacing: 13) {
176 + ForEach(launcherUniverses) { u in
177 + NavigationLink(value: u.id) {
178 + VStack(spacing: 7) {
179 + KALogo(universe: u, size: 44)
180 + .padding(10)
181 + .background(KATheme.surface(scheme),
182 + in: RoundedRectangle(cornerRadius: 15, style: .continuous))
183 + .overlay(RoundedRectangle(cornerRadius: 15, style: .continuous)
184 + .strokeBorder(KATheme.ink(scheme).opacity(0.25), lineWidth: 1.2))
185 + .background(RoundedRectangle(cornerRadius: 15, style: .continuous)
186 + .fill(u.accent.opacity(scheme == .dark ? 0.45 : 0.95))
187 + .offset(x: 3.5, y: 3.5))
188 + Text(u.name)
189 + .font(.caption2.weight(.bold))
190 + .foregroundStyle(KATheme.ink(scheme))
191 + .lineLimit(1)
192 + }
108 193 }
109 − .padding(.horizontal, 13).padding(.vertical, 9)
110 − .kaCard(accent: u.accent)
194 + .buttonStyle(KAPressStyle())
195 + .kaScrollPop()
111 196 }
112 − .buttonStyle(.plain)
113 − .kaScrollPop()
114 197 }
115 − if pulse.isEmpty {
116 − Text("Le pouls de l'écosystème…").font(.caption).foregroundStyle(.tertiary).padding(10)
198 + .padding(.vertical, 5)
199 + .padding(.horizontal, 2)
200 + }
201 + }
202 + }
203 +
204 + // MARK: le pouls — compteurs roulants avec les logos officiels
205 +
206 + private var pulseSection: some View {
207 + VStack(alignment: .leading, spacing: 10) {
208 + UniverseSectionHeader(title: "Le pouls de l'écosystème", accent: KATheme.green,
209 + detail: "EN DIRECT")
210 + ScrollView(.horizontal, showsIndicators: false) {
211 + HStack(spacing: 10) {
212 + ForEach(pulse, id: \.0.id) { (u, n) in
213 + NavigationLink(value: u.id) {
214 + HStack(spacing: 10) {
215 + KALogo(universe: u, size: 30)
216 + VStack(alignment: .leading, spacing: 1) {
217 + KACountUp(value: n, font: KAFont.display(17), color: u.accent)
218 + Text(u.unit.uppercased())
219 + .font(KAFont.mono(8))
220 + .foregroundStyle(.secondary)
221 + .lineLimit(1)
222 + }
223 + }
224 + .padding(.horizontal, 13).padding(.vertical, 10)
225 + .kaCard(accent: u.accent)
226 + }
227 + .buttonStyle(.plain)
228 + .kaScrollPop()
229 + }
230 + if pulse.isEmpty {
231 + Text("Le pouls de l'écosystème…")
232 + .font(.caption).foregroundStyle(.tertiary).padding(10)
233 + }
117 234 }
235 + .padding(.vertical, 4)
118 236 }
119 − .padding(.vertical, 4)
120 237 }
121 238 .accessibilityLabel("Le pouls de l'écosystème en direct")
122 239 }
123 240
241 + // MARK: le choix du moment — vitrine pleine largeur
242 +
243 + private var spotlightPick: (Universe, KAItem)? {
244 + for s in featured {
245 + if let it = s.items.first(where: { $0.imageURL != nil }) { return (s.universe, it) }
246 + }
247 + return nil
248 + }
249 +
250 + @ViewBuilder
251 + private var spotlight: some View {
252 + if let (u, star) = spotlightPick {
253 + VStack(alignment: .leading, spacing: 10) {
254 + UniverseSectionHeader(title: "Le choix du moment", accent: u.accent, detail: u.name)
255 + NavigationLink(value: star) {
256 + UniverseShowcaseCard(item: star, universe: u)
257 + }
258 + .buttonStyle(KAPressStyle())
259 + .kaScrollPop(axis: .vertical)
260 + }
261 + }
262 + }
263 +
124 264 private func featuredSection(_ u: Universe, _ items: [KAItem]) -> some View {
125 265 VStack(alignment: .leading, spacing: 10) {
126 266 HStack(spacing: 8) {
127 267