// Auteur : Simon-Pierre Boucher — contact@spboucher.ai // KAApp.swift — point d'entrée : onboarding animé au premier lancement, puis // la super-app v3 : 6 onglets (dont Ka Trajet, le GPS maison) servis par le // dock flottant signature (encre + lime) avec KA Agent intégré. import SwiftUI @main struct KAApp: App { @AppStorage("ka.onboarded") private var onboarded = false @AppStorage("ka.appearance") private var appearance = "clair" @StateObject private var favorites = FavoritesStore.shared init() { // cache réseau généreux : images et JSON fluides, consultation hors ligne URLCache.shared = URLCache(memoryCapacity: 64 * 1024 * 1024, diskCapacity: 512 * 1024 * 1024) } var body: some Scene { WindowGroup { Group { if onboarded { RootView() } else { OnboardingView() } } .environmentObject(favorites) .environmentObject(RecentsStore.shared) .preferredColorScheme(appearance == "clair" ? .light : appearance == "sombre" ? .dark : nil) } } } // MARK: - Racine : 6 onglets servis par le dock flottant signature struct RootView: View { @State private var showAgent = false // onglet initial pilotable en debug — JAMAIS pendant les tests (les threads // MapLibre de la carte feraient une course avec le exit() de XCTest) @State private var tab: Int // onglets déjà visités : montés une fois, gardés vivants (état préservé, // cartes MapLibre non réinitialisées) — les autres restent paresseux @State private var loaded: Set @StateObject private var recents = RecentsStore.shared @StateObject private var trajet = TrajetModel() init() { let initial = ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil ? 0 : UserDefaults.standard.integer(forKey: "ka.debug.tab") _tab = State(initialValue: initial) _loaded = State(initialValue: [initial]) } /// Le dock s'efface dans les modes immersifs de Trajet /// (conduite et Découvrir plein écran) private var dockVisible: Bool { !trajet.navigating && !trajet.discovering } @Environment(\.colorScheme) private var scheme var body: some View { // le dock occupe SON espace, sous le contenu : rien ne passe jamais // dessous, sur aucune page — c'est une vraie barre, pas un flotteur VStack(spacing: 0) { ZStack { pane(0) { HomeView() } pane(1) { SearchView() } pane(2) { UniversesView() } pane(3) { MapTab() } pane(4) { TrajetView().environmentObject(trajet) } pane(5) { MoreTab() } } if dockVisible { KADock(tab: $tab, alertCount: recents.alertCount) { showAgent = true } .padding(.top, 8) .padding(.bottom, 2) .transition(.move(edge: .bottom).combined(with: .opacity)) } } .background(KATheme.paper(scheme).ignoresSafeArea()) .tint(KATheme.green) .animation(.spring(response: 0.4, dampingFraction: 0.8), value: trajet.navigating) .animation(.spring(response: 0.4, dampingFraction: 0.8), value: trajet.discovering) .onChange(of: tab) { _, t in loaded.insert(t) } // navigation demandée par une vue (ex. pilule de recherche de l'accueil) .onReceive(NotificationCenter.default.publisher(for: .kaSwitchTab)) { note in if let i = note.object as? Int, (0..<6).contains(i) { withAnimation(.spring(response: 0.35, dampingFraction: 0.8)) { tab = i } } } .task { await recents.refreshAlerts() } .sheet(isPresented: $showAgent) { AgentChatView() .presentationDetents([.large]) } } @ViewBuilder private func pane(_ i: Int, @ViewBuilder content: () -> some View) -> some View { if loaded.contains(i) { content() .opacity(tab == i ? 1 : 0) .allowsHitTesting(tab == i) .accessibilityHidden(tab != i) } } } extension Notification.Name { /// Demande de changement d'onglet (object = index Int) static let kaSwitchTab = Notification.Name("ka.switch.tab") } /// Onglet Carte : la carte unifiée directement (pas de fermeture — c'est un onglet) struct MapTab: View { var body: some View { UnifiedMapView(embedded: true) } } /// Onglet Profil — inclut Favoris, Historique et Recherches sauvegardées. struct MoreTab: View { var body: some View { ProfileView() } }