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