Swift 98.3%
Shell 1.7%
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(KAFont.display(22))45 HStack(spacing: 8) {46 if totalAll > 0 {47 Text("\(totalAll.fr) éléments en direct")48 .font(KAFont.mono(11))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(KAFont.mono(11))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 KALogo(id: u.id, size: 38,142 fallbackSymbol: u.symbol, fallbackAccent: u.accent)143 Spacer()144 Image(systemName: "chevron.right").font(.caption).foregroundStyle(.tertiary)145 }146 KAWordmark(universe: u, size: 16)147 Text(u.tagline)148 .font(.caption).foregroundStyle(KATheme.ink2(scheme))149 .lineLimit(2, reservesSpace: true)150 .multilineTextAlignment(.leading)151 if let total = pulse.totals[u.id] {152 Text("\(total.fr) \(u.unit)")153 .font(KAFont.mono(10))154 .foregroundStyle(u.accent)155 .contentTransition(.numericText())156 } else {157 Text("—").font(KAFont.mono(10, bold: false))158 .foregroundStyle(.tertiary)159 }160 }161 .padding(13)162 .frame(maxWidth: .infinity, alignment: .leading)163 .kaCard(accent: u.accent)164 }165 .buttonStyle(KAPressStyle())166 .accessibilityLabel("\(u.name) — \(u.tagline)")167 }168 }169 }170 }171172 // MARK: reprise (derniers consultés)173174 private var recentStrip: some View {175 VStack(alignment: .leading, spacing: 10) {176 HStack {177 Text("Reprendre où vous étiez").font(.title3.weight(.bold))178 Spacer()179 Button("Tout l'historique") { state.selection = .history }180 .buttonStyle(.plain)181 .font(.caption.weight(.semibold))182 .foregroundStyle(KATheme.green)183 }184 ScrollView(.horizontal, showsIndicators: false) {185 HStack(spacing: 11) {186 ForEach(recents.viewed.prefix(8)) { item in187 Button {188 state.openUniverse(item.universeID, query: item.title)189 } label: {190 KAItemCard(item: item)191 .frame(width: 230)192 }193 .buttonStyle(KAPressStyle())194 }195 }196 .padding(.vertical, 2)197 }198 }199 }200201 private var footer: some View {202 Text(Ecosystem.disclaimer)203 .font(.caption2).foregroundStyle(.tertiary)204 .padding(.top, 4)205 }206}207