SPB Git

spb/ka-ios

Public
5.5 KB · 129 lines swift
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
Files · 49784a20c598d4fc9dfbc6fcc0a419e9854e3a5f
Menu

Theme