// Auteur : Simon-Pierre Boucher — contact@spboucher.ai // HomeView.swift — l'Accueil : le pouls de l'écosystème (compteurs live), // collections éditoriales (requêtes RÉELLES vers les API — aucun contenu // inventé), grille des 12 univers, reprise de l'historique récent. import SwiftUI struct HomeView: View { @EnvironmentObject private var pulse: EcosystemPulse @EnvironmentObject private var state: AppState @EnvironmentObject private var recents: RecentsStore @Environment(\.colorScheme) private var scheme private var totalAll: Int { // le pouls global = somme des compteurs connus (sans les pages indexées ni le parc Vrai-Prix, hors offre "consommable") Ecosystem.all .filter { !["trouve-ka", "vrai-prix", "api-ka"].contains($0.id) } .compactMap { pulse.totals[$0.id] } .reduce(0, +) } var body: some View { ScrollView { VStack(alignment: .leading, spacing: 22) { header if recents.alertCount > 0 { alertBanner } editorial universes if !recents.viewed.isEmpty { recentStrip } footer } .padding(20) .frame(maxWidth: 1300) .frame(maxWidth: .infinity) } .background(KATheme.paper(scheme)) } // MARK: en-tête private var header: some View { VStack(alignment: .leading, spacing: 8) { GroupeKAMark(size: 30) Text("Le Québec entier, lu à la source.") .font(KAFont.display(22)) HStack(spacing: 8) { if totalAll > 0 { Text("\(totalAll.fr) éléments en direct") .font(KAFont.mono(11)) .padding(.horizontal, 10).padding(.vertical, 5) .background(KATheme.green.opacity(0.12), in: Capsule()) .foregroundStyle(KATheme.green) .contentTransition(.numericText()) } Text("\(pulse.servicesUp)/\(pulse.health.count) services en ligne") .font(KAFont.mono(11)) .padding(.horizontal, 10).padding(.vertical, 5) .background((pulse.servicesUp == pulse.health.count ? KATheme.green : .orange).opacity(0.12), in: Capsule()) .foregroundStyle(pulse.servicesUp == pulse.health.count ? KATheme.green : .orange) Spacer() Button { state.openUniversalSearch() } label: { Label("Recherche universelle", systemImage: "magnifyingglass") .font(.caption.weight(.bold)) .padding(.horizontal, 12).padding(.vertical, 7) .background(KATheme.inkLight, in: Capsule()) .foregroundStyle(KATheme.lime) } .buttonStyle(.plain) .help("⌘K — une barre pour tous les univers") } } } private var alertBanner: some View { Button { state.selection = .alerts } label: { HStack(spacing: 10) { Image(systemName: "bell.badge.fill").foregroundStyle(.red) Text("**+\(recents.alertCount) nouveauté\(recents.alertCount > 1 ? "s" : "")** dans vos recherches sauvegardées") .font(.subheadline) Spacer() Image(systemName: "chevron.right").font(.caption).foregroundStyle(.secondary) } .padding(13) .frame(maxWidth: .infinity, alignment: .leading) .kaCard(accent: .red) } .buttonStyle(KAPressStyle()) } // MARK: collections éditoriales (requêtes réelles) private var editorial: some View { VStack(alignment: .leading, spacing: 10) { Text("En ce moment").font(.title3.weight(.bold)) LazyVGrid(columns: [GridItem(.adaptive(minimum: 270), spacing: 12)], spacing: 12) { ForEach(Editorial.collections) { c in let u = Ecosystem.universe(c.universeID) Button { state.openUniverse(c.universeID, query: c.query ?? "", params: c.params) } label: { HStack(spacing: 12) { Image(systemName: c.symbol) .font(.title3.weight(.semibold)) .foregroundStyle(u?.accent ?? .gray) .frame(width: 42, height: 42) .background((u?.accent ?? .gray).opacity(0.13), in: RoundedRectangle(cornerRadius: 11, style: .continuous)) VStack(alignment: .leading, spacing: 2) { Text(c.title).font(.subheadline.weight(.bold)) Text(c.subtitle).font(.caption).foregroundStyle(KATheme.ink2(scheme)) .lineLimit(2) } Spacer() Image(systemName: "chevron.right").font(.caption).foregroundStyle(.tertiary) } .padding(12) .frame(maxWidth: .infinity, alignment: .leading) .kaCard(accent: u?.accent) } .buttonStyle(KAPressStyle()) } } } } // MARK: grille des univers private var universes: some View { VStack(alignment: .leading, spacing: 10) { Text("Les univers").font(.title3.weight(.bold)) LazyVGrid(columns: [GridItem(.adaptive(minimum: 215), spacing: 13)], spacing: 13) { ForEach(Ecosystem.all) { u in Button { state.selection = .universe(u.id) } label: { VStack(alignment: .leading, spacing: 9) { HStack { KALogo(id: u.id, size: 38, fallbackSymbol: u.symbol, fallbackAccent: u.accent) Spacer() Image(systemName: "chevron.right").font(.caption).foregroundStyle(.tertiary) } KAWordmark(universe: u, size: 16) Text(u.tagline) .font(.caption).foregroundStyle(KATheme.ink2(scheme)) .lineLimit(2, reservesSpace: true) .multilineTextAlignment(.leading) if let total = pulse.totals[u.id] { Text("\(total.fr) \(u.unit)") .font(KAFont.mono(10)) .foregroundStyle(u.accent) .contentTransition(.numericText()) } else { Text("—").font(KAFont.mono(10, bold: false)) .foregroundStyle(.tertiary) } } .padding(13) .frame(maxWidth: .infinity, alignment: .leading) .kaCard(accent: u.accent) } .buttonStyle(KAPressStyle()) .accessibilityLabel("\(u.name) — \(u.tagline)") } } } } // MARK: reprise (derniers consultés) private var recentStrip: some View { VStack(alignment: .leading, spacing: 10) { HStack { Text("Reprendre où vous étiez").font(.title3.weight(.bold)) Spacer() Button("Tout l'historique") { state.selection = .history } .buttonStyle(.plain) .font(.caption.weight(.semibold)) .foregroundStyle(KATheme.green) } ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 11) { ForEach(recents.viewed.prefix(8)) { item in Button { state.openUniverse(item.universeID, query: item.title) } label: { KAItemCard(item: item) .frame(width: 230) } .buttonStyle(KAPressStyle()) } } .padding(.vertical, 2) } } } private var footer: some View { Text(Ecosystem.disclaimer) .font(.caption2).foregroundStyle(.tertiary) .padding(.top, 4) } }