// Auteur : Simon-Pierre Boucher — contact@spboucher.ai // OnboardingView.swift — le « waouh » de la première ouverture : animation du // logo KA (boîte encre qui se redresse, lime qui s'allume), pitch en 3 lignes, // choix des univers favoris, puis c'est parti. Court, spectaculaire, utile. // Reduce Motion respecté (l'animation se fige proprement). import SwiftUI struct OnboardingView: View { @AppStorage("ka.onboarded") private var onboarded = false @AppStorage("ka.favUniverses") private var favUniversesRaw = "" @Environment(\.accessibilityReduceMotion) private var reduceMotion @Environment(\.colorScheme) private var scheme @State private var step = 0 @State private var logoRotation: Double = -14 @State private var logoScale: CGFloat = 0.6 @State private var glow = false @State private var picked: Set = [] var body: some View { VStack(spacing: 0) { Spacer() logo Spacer() if step == 0 { pitch } else { pickUniverses } actions } .padding(24) .background { ZStack { KATheme.paper(scheme) KAAurora() } .ignoresSafeArea() } .onAppear { guard !reduceMotion else { logoRotation = -4; logoScale = 1; glow = true; return } withAnimation(.spring(response: 0.9, dampingFraction: 0.55).delay(0.15)) { logoRotation = -4 logoScale = 1 } withAnimation(.easeInOut(duration: 1.1).delay(0.5)) { glow = true } } } private var logo: some View { VStack(spacing: 18) { ZStack { RoundedRectangle(cornerRadius: 30, style: .continuous) .fill(KATheme.inkLight) .frame(width: 128, height: 128) .shadow(color: KATheme.lime.opacity(glow ? 0.55 : 0), radius: glow ? 36 : 0) Text("KA") .font(.system(size: 56, weight: .bold, design: .rounded)) .foregroundStyle(KATheme.lime) } .rotationEffect(.degrees(logoRotation)) .scaleEffect(logoScale) .accessibilityHidden(true) Text("Mille sites. Un seul KA.") .font(.system(.title2, design: .rounded).weight(.bold)) .multilineTextAlignment(.center) } } private var pitch: some View { VStack(alignment: .leading, spacing: 14) { pitchRow("magnifyingglass", "Une recherche, tout le Québec", "Logements, propriétés, autos, emplois, restos, sorties — interrogés d'un coup.") pitchRow("heart.fill", "Des favoris qui traversent les univers", "Un 4½, une auto et un resto dans une même collection.") pitchRow("sparkles", "KA Agent, votre IA d'ici", "Posez une question, il fouille les vraies données de l'écosystème.") pitchRow("location.north.fill", "Ka Trajet, le GPS maison", "Itinéraires, conduite 3D et les adresses de l'écosystème sur la carte.") } .padding(.bottom, 12) } private func pitchRow(_ symbol: String, _ title: String, _ sub: String) -> some View { HStack(alignment: .top, spacing: 12) { Image(systemName: symbol) .font(.headline).foregroundStyle(KATheme.green) .frame(width: 34, height: 34) .background(KATheme.green.opacity(0.12), in: RoundedRectangle(cornerRadius: 9)) VStack(alignment: .leading, spacing: 2) { Text(title).font(.subheadline.weight(.bold)) Text(sub).font(.caption).foregroundStyle(.secondary) } } } private var pickUniverses: some View { VStack(alignment: .leading, spacing: 10) { Text("Vos univers du quotidien ?") .font(.headline) Text("Ils remonteront en tête de votre accueil (modifiable dans Profil).") .font(.caption).foregroundStyle(.secondary) LazyVGrid(columns: [GridItem(.adaptive(minimum: 105), spacing: 8)], spacing: 8) { ForEach(Ecosystem.all) { u in let on = picked.contains(u.id) Button { Haptics.tap() if on { picked.remove(u.id) } else { picked.insert(u.id) } } label: { VStack(spacing: 4) { Image(systemName: u.symbol).font(.subheadline) Text(u.name).font(.caption2.weight(.bold)).lineLimit(1) } .frame(maxWidth: .infinity).padding(.vertical, 10) .background(on ? u.accent : KATheme.surface(scheme), in: RoundedRectangle(cornerRadius: 11, style: .continuous)) .foregroundStyle(on ? .white : .primary) .overlay(RoundedRectangle(cornerRadius: 11).strokeBorder(.primary.opacity(0.2), lineWidth: 1)) } } } } .padding(.bottom, 12) } private var actions: some View { Button { Haptics.success() if step == 0 { withAnimation(.snappy) { step = 1 } } else { favUniversesRaw = picked.sorted().joined(separator: ",") withAnimation(.easeOut) { onboarded = true } } } label: { Text(step == 0 ? "Continuer" : picked.isEmpty ? "Explorer sans choisir" : "C'est parti 🚀") .font(.headline) .frame(maxWidth: .infinity).padding(.vertical, 15) .background(KATheme.inkLight, in: RoundedRectangle(cornerRadius: 14, style: .continuous)) .foregroundStyle(KATheme.lime) } .padding(.top, 4) } }