1// Auteur : Simon-Pierre Boucher — contact@spboucher.ai2// HomeView.swift — l'Accueil : le pouls de l'écosystème (compteurs live),3// collections éditoriales (requêtes RÉELLES vers les API — aucun contenu4// inventé), grille des 12 univers, reprise de l'historique récent.5import SwiftUI67struct HomeView: View {8 @EnvironmentObject private var pulse: EcosystemPulse9 @EnvironmentObject private var state: AppState10 @EnvironmentObject private var recents: RecentsStore11 @Environment(\.colorScheme) private var scheme1213 private var totalAll: Int {14 // le pouls global = somme des compteurs connus (sans les pages indexées ni le parc Vrai-Prix, hors offre "consommable")15 Ecosystem.all16 .filter { !["trouve-ka", "vrai-prix", "api-ka"].contains($0.id) }17 .compactMap { pulse.totals[$0.id] }18 .reduce(0, +)19 }2021 var body: some View {22 ScrollView {23 VStack(alignment: .leading, spacing: 22) {24 header25 if recents.alertCount > 0 { alertBanner }26 editorial27 universes28 if !recents.viewed.isEmpty { recentStrip }29 footer30 }31 .padding(20)32 .frame(maxWidth: 1300)33 .frame(maxWidth: .infinity)34 }35 .background(KATheme.paper(scheme))36 }3738 // MARK: en-tête3940 private var header: some View {41 VStack(alignment: .leading, spacing: 8) {42 GroupeKAMark(size: 30)43 Text("Le Québec entier, lu à la source.")44 .font(.system(.title2, design: .rounded).weight(.bold))45 HStack(spacing: 8) {46 if totalAll > 0 {47 Text("\(totalAll.fr) éléments en direct")48 .font(.system(.caption, design: .monospaced).weight(.bold))49 .padding(.horizontal, 10).padding(.vertical, 5)50 .background(KATheme.green.opacity(0.12), in: Capsule())51 .foregroundStyle(KATheme.green)52 .contentTransition(.numericText())53 }54 Text("\(pulse.servicesUp)/\(pulse.health.count) services en ligne")55 .font(.system(.caption, design: .monospaced).weight(.bold))56 .padding(.horizontal, 10).padding(.vertical, 5)57 .background((pulse.servicesUp == pulse.health.count ? KATheme.green : .orange).opacity(0.12), in: Capsule())58 .foregroundStyle(pulse.servicesUp == pulse.health.count ? KATheme.green : .orange)59 Spacer()60 Button {61 state.openUniversalSearch()62 } label: {63 Label("Recherche universelle", systemImage: "magnifyingglass")64 .font(.caption.weight(.bold))65 .padding(.horizontal, 12).padding(.vertical, 7)66 .background(KATheme.inkLight, in: Capsule())67 .foregroundStyle(KATheme.lime)68 }69 .buttonStyle(.plain)70 .help("⌘K — une barre pour tous les univers")71 }72 }73 }7475 private var alertBanner: some View {76 Button {77 state.selection = .alerts78 } label: {79 HStack(spacing: 10) {80 Image(systemName: "bell.badge.fill").foregroundStyle(.red)81 Text("**+\(recents.alertCount) nouveauté\(recents.alertCount > 1 ? "s" : "")** dans vos recherches sauvegardées")82 .font(.subheadline)83 Spacer()84 Image(systemName: "chevron.right").font(.caption).foregroundStyle(.secondary)85 }86 .padding(13)87 .frame(maxWidth: .infinity, alignment: .leading)88 .kaCard(accent: .red)89 }90 .buttonStyle(KAPressStyle())91 }9293 // MARK: collections éditoriales (requêtes réelles)9495 private var editorial: some View {96 VStack(alignment: .leading, spacing: 10) {97 Text("En ce moment").font(.title3.weight(.bold))98 LazyVGrid(columns: [GridItem(.adaptive(minimum: 270), spacing: 12)], spacing: 12) {99 ForEach(Editorial.collections) { c in100 let u = Ecosystem.universe(c.universeID)101 Button {102 state.openUniverse(c.universeID, query: c.query ?? "", params: c.params)103 } label: {104 HStack(spacing: 12) {105 Image(systemName: c.symbol)106 .font(.title3.weight(.semibold))107 .foregroundStyle(u?.accent ?? .gray)108 .frame(width: 42, height: 42)109 .background((u?.accent ?? .gray).opacity(0.13),110 in: RoundedRectangle(cornerRadius: 11, style: .continuous))111 VStack(alignment: .leading, spacing: 2) {112 Text(c.title).font(.subheadline.weight(.bold))113 Text(c.subtitle).font(.caption).foregroundStyle(KATheme.ink2(scheme))114 .lineLimit(2)115 }116 Spacer()117 Image(systemName: "chevron.right").font(.caption).foregroundStyle(.tertiary)118 }119 .padding(12)120 .frame(maxWidth: .infinity, alignment: .leading)121 .kaCard(accent: u?.accent)122 }123 .buttonStyle(KAPressStyle())124 }125 }126 }127 }128129 // MARK: grille des univers130131 private var universes: some View {132 VStack(alignment: .leading, spacing: 10) {133 Text("Les univers").font(.title3.weight(.bold))134 LazyVGrid(columns: [GridItem(.adaptive(minimum: 215), spacing: 13)], spacing: 13) {135 ForEach(Ecosystem.all) { u in136 Button {137 state.selection = .universe(u.id)138 } label: {139 VStack(alignment: .leading, spacing: 9) {140 HStack {141 Image(systemName: u.symbol)142 .font(.title3.weight(.semibold))143 .foregroundStyle(u.accent)144 .frame(width: 38, height: 38)145 .background(u.accent.opacity(0.14), in: RoundedRectangle(cornerRadius: 10, style: .continuous))146 Spacer()147 Image(systemName: "chevron.right").font(.caption).foregroundStyle(.tertiary)148 }149 KAWordmark(universe: u, size: 16)150 Text(u.tagline)151 .font(.caption).foregroundStyle(KATheme.ink2(scheme))152 .lineLimit(2, reservesSpace: true)153 .multilineTextAlignment(.leading)154 if let total = pulse.totals[u.id] {155 Text("\(total.fr) \(u.unit)")156 .font(.system(.caption2, design: .monospaced).weight(.bold))157 .foregroundStyle(u.accent)158 .contentTransition(.numericText())159 } else {160 Text("—").font(.system(.caption2, design: .monospaced))161 .foregroundStyle(.tertiary)162 }163 }164 .padding(13)165 .frame(maxWidth: .infinity, alignment: .leading)166 .kaCard(accent: u.accent)167 }168 .buttonStyle(KAPressStyle())169 .accessibilityLabel("\(u.name) — \(u.tagline)")170 }171 }172 }173 }174175 // MARK: reprise (derniers consultés)176177 private var recentStrip: some View {178 VStack(alignment: .leading, spacing: 10) {179 HStack {180 Text("Reprendre où vous étiez").font(.title3.weight(.bold))181 Spacer()182 Button("Tout l'historique") { state.selection = .history }183 .buttonStyle(.plain)184 .font(.caption.weight(.semibold))185 .foregroundStyle(KATheme.green)186 }187 ScrollView(.horizontal, showsIndicators: false) {188 HStack(spacing: 11) {189 ForEach(recents.viewed.prefix(8)) { item in190 Button {191 state.openUniverse(item.universeID, query: item.title)192 } label: {193 KAItemCard(item: item)194 .frame(width: 230)195 }196 .buttonStyle(KAPressStyle())197 }198 }199 .padding(.vertical, 2)200 }201 }202 }203204 private var footer: some View {205 Text(Ecosystem.disclaimer)206 .font(.caption2).foregroundStyle(.tertiary)207 .padding(.top, 4)208 }209}210