SPB Git

spb/ka-ios

Public

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 (Aug 25, 2026) parent c55933b Browse files

2 changed files +188 −37

modified KA/App/KAApp.swift +11 −0
@@ -74,6 +74,12 @@ struct RootView: View {
74 .animation(.spring(response: 0.4, dampingFraction: 0.8), value: trajet.navigating) 74 .animation(.spring(response: 0.4, dampingFraction: 0.8), value: trajet.navigating)
75 .animation(.spring(response: 0.4, dampingFraction: 0.8), value: trajet.discovering) 75 .animation(.spring(response: 0.4, dampingFraction: 0.8), value: trajet.discovering)
76 .onChange(of: tab) { _, t in loaded.insert(t) } 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 .task { await recents.refreshAlerts() } 83 .task { await recents.refreshAlerts() }
78 .sheet(isPresented: $showAgent) { 84 .sheet(isPresented: $showAgent) {
79 AgentChatView() 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 /// Onglet Carte : la carte unifiée directement (pas de fermeture — c'est un onglet) 106 /// Onglet Carte : la carte unifiée directement (pas de fermeture — c'est un onglet)
96 struct MapTab: View { 107 struct MapTab: View {
97 var body: some View { 108 var body: some View {
modified KA/Features/HomeView.swift +177 −37
@@ -18,13 +18,15 @@ struct HomeView: View {
18 var body: some View { 18 var body: some View {
19 NavigationStack { 19 NavigationStack {
20 ScrollView { 20 ScrollView {
21− VStack(alignment: .leading, spacing: 22) { 21+ VStack(alignment: .leading, spacing: 24) {
22− header 22+ heroCard
23− tickerStrip 23+ launcher
24− pulseStrip 24+ pulseSection
25 resumeStrip 25 resumeStrip
26+ spotlight
26 ForEach(featured, id: \.universe.id) { section in 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 editorialSection 31 editorialSection
30 if loading && featured.isEmpty { 32 if loading && featured.isEmpty {
@@ -70,57 +72,195 @@ struct HomeView: View {
70 } 72 }
71 } 73 }
72 74
73− private var header: some View { 75+ // MARK: héros encre — la pièce maîtresse de l'accueil
74− VStack(alignment: .leading, spacing: 8) { 76+
75− // kicker mono — la micro-étiquette signature des sites 77+ private var dateKicker: String {
76− HStack(spacing: 6) { 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 Rectangle().fill(KATheme.lime).frame(width: 8, height: 8) 85 Rectangle().fill(KATheme.lime).frame(width: 8, height: 8)
78− Text("GROUPE KA — L'ÉCOSYSTÈME QUÉBÉCOIS") 86+ Text(dateKicker)
79− .font(KAFont.mono(10)) 87+ .font(KAFont.mono(9.5))
80− .foregroundStyle(KATheme.ink2(scheme)) 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 Text(greeting.title) 98 Text(greeting.title)
84 .font(KAFont.display(34)) 99 .font(KAFont.display(34))
85− .kaHighlighted() 100+ .foregroundStyle(.white)
86− Text(greeting.sub).font(.subheadline).foregroundStyle(KATheme.ink2(scheme)) 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 157+ private var tickerItems: [String] {
91− private var tickerStrip: some View { 158+ pulse.prefix(8).map {
92− KATicker(items: pulse.prefix(8).map {
93 "\($0.1.formatted(.number.locale(Locale(identifier: "fr_CA")))) \($0.0.unit)" 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 { 163+ // MARK: rampe de lancement des 12 univers (favoris d'abord)
98− ScrollView(.horizontal, showsIndicators: false) { 164+
99− HStack(spacing: 10) { 165+ private var launcherUniverses: [Universe] {
100− ForEach(pulse, id: \.0.id) { (u, n) in 166+ let favs = favUniversesRaw.split(separator: ",").map(String.init)
101− NavigationLink(value: u.id) { 167+ let favU = favs.compactMap(Ecosystem.universe)
102− VStack(alignment: .leading, spacing: 3) { 168+ return favU + Ecosystem.all.filter { !favs.contains($0.id) }
103− KACountUp(value: n, 169+ }
104− font: .system(.headline, design: .rounded).weight(.bold), 170+
105− color: u.accent) 171+ private var launcher: some View {
106− Text(u.unit).font(KAFont.mono(9)).textCase(.uppercase) 172+ VStack(alignment: .leading, spacing: 10) {
107− .foregroundStyle(.secondary) 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) 194+ .buttonStyle(KAPressStyle())
110− .kaCard(accent: u.accent) 195+ .kaScrollPop()
111 } 196 }
112− .buttonStyle(.plain)
113− .kaScrollPop()
114 } 197 }
115− if pulse.isEmpty { 198+ .padding(.vertical, 5)
116− Text("Le pouls de l'écosystème…").font(.caption).foregroundStyle(.tertiary).padding(10) 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 .accessibilityLabel("Le pouls de l'écosystème en direct") 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 private func featuredSection(_ u: Universe, _ items: [KAItem]) -> some View { 264 private func featuredSection(_ u: Universe, _ items: [KAItem]) -> some View {
125 VStack(alignment: .leading, spacing: 10) { 265 VStack(alignment: .leading, spacing: 10) {
126 HStack(spacing: 8) { 266 HStack(spacing: 8) {
127 267
Menu

Theme