1// Auteur : Simon-Pierre Boucher — contact@spboucher.ai2// OnboardingView.swift — le « waouh » de la première ouverture : animation du3// logo KA (boîte encre qui se redresse, lime qui s'allume), pitch en 3 lignes,4// choix des univers favoris, puis c'est parti. Court, spectaculaire, utile.5// Reduce Motion respecté (l'animation se fige proprement).6import SwiftUI78struct OnboardingView: View {9 @AppStorage("ka.onboarded") private var onboarded = false10 @AppStorage("ka.favUniverses") private var favUniversesRaw = ""11 @Environment(\.accessibilityReduceMotion) private var reduceMotion12 @Environment(\.colorScheme) private var scheme1314 @State private var step = 015 @State private var logoRotation: Double = -1416 @State private var logoScale: CGFloat = 0.617 @State private var glow = false18 @State private var picked: Set<String> = []1920 var body: some View {21 VStack(spacing: 0) {22 Spacer()23 logo24 Spacer()25 if step == 0 { pitch } else { pickUniverses }26 actions27 }28 .padding(24)29 .background(KATheme.paper(scheme))30 .onAppear {31 guard !reduceMotion else { logoRotation = -4; logoScale = 1; glow = true; return }32 withAnimation(.spring(response: 0.9, dampingFraction: 0.55).delay(0.15)) {33 logoRotation = -434 logoScale = 135 }36 withAnimation(.easeInOut(duration: 1.1).delay(0.5)) { glow = true }37 }38 }3940 private var logo: some View {41 VStack(spacing: 18) {42 ZStack {43 RoundedRectangle(cornerRadius: 30, style: .continuous)44 .fill(KATheme.inkLight)45 .frame(width: 128, height: 128)46 .shadow(color: KATheme.lime.opacity(glow ? 0.55 : 0), radius: glow ? 36 : 0)47 Text("KA")48 .font(.system(size: 56, weight: .bold, design: .rounded))49 .foregroundStyle(KATheme.lime)50 }51 .rotationEffect(.degrees(logoRotation))52 .scaleEffect(logoScale)53 .accessibilityHidden(true)54 Text("Mille sites. Un seul KA.")55 .font(.system(.title2, design: .rounded).weight(.bold))56 .multilineTextAlignment(.center)57 }58 }5960 private var pitch: some View {61 VStack(alignment: .leading, spacing: 14) {62 pitchRow("magnifyingglass", "Une recherche, tout le Québec", "Logements, propriétés, autos, emplois, restos, sorties — interrogés d'un coup.")63 pitchRow("heart.fill", "Des favoris qui traversent les univers", "Un 4½, une auto et un resto dans une même collection.")64 pitchRow("sparkles", "KA Agent, votre IA d'ici", "Posez une question, il fouille les vraies données de l'écosystème.")65 }66 .padding(.bottom, 12)67 }6869 private func pitchRow(_ symbol: String, _ title: String, _ sub: String) -> some View {70 HStack(alignment: .top, spacing: 12) {71 Image(systemName: symbol)72 .font(.headline).foregroundStyle(KATheme.green)73 .frame(width: 34, height: 34)74 .background(KATheme.green.opacity(0.12), in: RoundedRectangle(cornerRadius: 9))75 VStack(alignment: .leading, spacing: 2) {76 Text(title).font(.subheadline.weight(.bold))77 Text(sub).font(.caption).foregroundStyle(.secondary)78 }79 }80 }8182 private var pickUniverses: some View {83 VStack(alignment: .leading, spacing: 10) {84 Text("Vos univers du quotidien ?")85 .font(.headline)86 Text("Ils remonteront en tête de votre accueil (modifiable dans Profil).")87 .font(.caption).foregroundStyle(.secondary)88 LazyVGrid(columns: [GridItem(.adaptive(minimum: 105), spacing: 8)], spacing: 8) {89 ForEach(Ecosystem.all) { u in90 let on = picked.contains(u.id)91 Button {92 Haptics.tap()93 if on { picked.remove(u.id) } else { picked.insert(u.id) }94 } label: {95 VStack(spacing: 4) {96 Image(systemName: u.symbol).font(.subheadline)97 Text(u.name).font(.caption2.weight(.bold)).lineLimit(1)98 }99 .frame(maxWidth: .infinity).padding(.vertical, 10)100 .background(on ? u.accent : KATheme.surface(scheme), in: RoundedRectangle(cornerRadius: 11, style: .continuous))101 .foregroundStyle(on ? .white : .primary)102 .overlay(RoundedRectangle(cornerRadius: 11).strokeBorder(.primary.opacity(0.2), lineWidth: 1))103 }104 }105 }106 }107 .padding(.bottom, 12)108 }109110 private var actions: some View {111 Button {112 Haptics.success()113 if step == 0 {114 withAnimation(.snappy) { step = 1 }115 } else {116 favUniversesRaw = picked.sorted().joined(separator: ",")117 withAnimation(.easeOut) { onboarded = true }118 }119 } label: {120 Text(step == 0 ? "Continuer" : picked.isEmpty ? "Explorer sans choisir" : "C'est parti 🚀")121 .font(.headline)122 .frame(maxWidth: .infinity).padding(.vertical, 15)123 .background(KATheme.inkLight, in: RoundedRectangle(cornerRadius: 14, style: .continuous))124 .foregroundStyle(KATheme.lime)125 }126 .padding(.top, 4)127 }128}129