// Auteur : Simon-Pierre Boucher — contact@spboucher.ai // ForYouSection.swift — le feed « Pour vous » de l'accueil : un mélange // CLASSÉ (pas chronologique) des univers que l'utilisateur suit vraiment, // en grandes cartes immersives pleine largeur, chaque carte expliquant // honnêtement pourquoi elle est là. Apparaît seulement quand le profil a // assez de signal — jamais de fausse personnalisation à la première ouverture. import SwiftUI struct ForYouSection: View { let entries: [ForYouEntry] @Environment(\.colorScheme) private var scheme var body: some View { VStack(alignment: .leading, spacing: 12) { HStack(spacing: 8) { Image(systemName: "sparkles") .font(.subheadline.weight(.bold)) .foregroundStyle(KATheme.lime) .frame(width: 28, height: 28) .background(KATheme.inkLight, in: RoundedRectangle(cornerRadius: 9, style: .continuous)) VStack(alignment: .leading, spacing: 0) { Text("Pour vous").font(KAFont.display(22)) .foregroundStyle(KATheme.ink(scheme)) Text("Selon vos visites — tout reste sur votre appareil") .font(.caption2).foregroundStyle(.secondary) } Spacer() } .accessibilityElement(children: .combine) .accessibilityAddTraits(.isHeader) ForEach(entries) { entry in NavigationLink(value: entry.item) { ForYouCard(entry: entry) } .buttonStyle(KAPressStyle()) .kaScrollPop(axis: .vertical) } } } } /// Grande carte immersive : photo pleine largeur, raison en badge accent, /// titre display sur scrim. Sans photo : rangée éditoriale avec pastille. struct ForYouCard: View { let entry: ForYouEntry @Environment(\.colorScheme) private var scheme private var universe: Universe? { Ecosystem.universe(entry.item.universeID) } private var accent: Color { universe?.accent ?? KATheme.green } var body: some View { Group { if entry.item.imageURL != nil { immersive } else { row } } .accessibilityElement(children: .combine) .accessibilityLabel("\(entry.reason). \(entry.item.title)\(entry.item.priceLabel.map { ", \($0)" } ?? "")") } private var immersive: some View { ZStack(alignment: .bottomLeading) { // l'image vit en OVERLAY d'un cadre fixe : sa largeur idéale // (panoramas!) ne se propage jamais à la mise en page Color.clear .frame(height: 200) .frame(maxWidth: .infinity) .overlay(KAImage(url: entry.item.imageURL, accent: accent, symbol: universe?.symbol ?? "sparkles")) .clipped() LinearGradient(colors: [.clear, .black.opacity(0.10), .black.opacity(0.82)], startPoint: .top, endPoint: .bottom) VStack(alignment: .leading, spacing: 6) { HStack(spacing: 5) { Image(systemName: "sparkle").font(.system(size: 8, weight: .bold)) Text(entry.reason.uppercased()) .font(KAFont.mono(8)) .kerning(0.8) .lineLimit(1) .minimumScaleFactor(0.8) } .padding(.horizontal, 9).padding(.vertical, 5) .background(accent, in: Capsule()) .foregroundStyle(.white) Text(entry.item.title) .font(KAFont.display(19)) .foregroundStyle(.white) .lineLimit(2) .multilineTextAlignment(.leading) HStack(spacing: 8) { if let p = entry.item.priceLabel { Text(p) .font(.system(.subheadline, design: .rounded, weight: .heavy)) .foregroundStyle(KATheme.lime) } if let c = entry.item.city { Text(c).font(.caption).foregroundStyle(.white.opacity(0.78)) } Spacer(minLength: 0) if let u = universe { Text(u.wordmark) .font(KAFont.mono(9)) .foregroundStyle(.white) .padding(.horizontal, 8).padding(.vertical, 4) .background(.white.opacity(0.18), in: Capsule()) } } } .padding(14) } .clipShape(RoundedRectangle(cornerRadius: 18, style: .continuous)) .overlay(RoundedRectangle(cornerRadius: 18, style: .continuous) .strokeBorder(KATheme.ink(scheme).opacity(scheme == .dark ? 0.35 : 0.85), lineWidth: 1.5)) .background(RoundedRectangle(cornerRadius: 18, style: .continuous) .fill(accent.opacity(scheme == .dark ? 0.35 : 1)) .offset(x: 6, y: 6)) } private var row: some View { HStack(alignment: .top, spacing: 12) { RoundedRectangle(cornerRadius: 8, style: .continuous) .fill(accent) .frame(width: 5) .padding(.vertical, 2) VStack(alignment: .leading, spacing: 4) { HStack(spacing: 5) { Image(systemName: "sparkle").font(.system(size: 8, weight: .bold)) Text(entry.reason.uppercased()) .font(KAFont.mono(8)) .kerning(0.6) .lineLimit(1) .minimumScaleFactor(0.85) } .foregroundStyle(accent) Text(entry.item.title) .font(.subheadline.weight(.bold)) .foregroundStyle(KATheme.ink(scheme)) .lineLimit(2) .multilineTextAlignment(.leading) HStack(spacing: 8) { if let p = entry.item.priceLabel { Text(p) .font(.system(.caption, design: .rounded).weight(.bold)) .foregroundStyle(accent) .lineLimit(1) } if let c = entry.item.city { Text(c).font(.caption2).foregroundStyle(.tertiary).lineLimit(1) } Spacer(minLength: 0) if let u = universe { KAChip(text: u.wordmark, accent: u.accent) } } } } .padding(12) .kaCard(accent: accent) } }