// 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(KATheme.paper(scheme)) .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.") } .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) } }