Swift 100%
1// Auteur : Simon-Pierre Boucher — contact@spboucher.ai2// ForYouSection.swift — le feed « Pour vous » de l'accueil : un mélange3// CLASSÉ (pas chronologique) des univers que l'utilisateur suit vraiment,4// en grandes cartes immersives pleine largeur, chaque carte expliquant5// honnêtement pourquoi elle est là. Apparaît seulement quand le profil a6// assez de signal — jamais de fausse personnalisation à la première ouverture.7import SwiftUI89struct ForYouSection: View {10 let entries: [ForYouEntry]11 @Environment(\.colorScheme) private var scheme1213 var body: some View {14 VStack(alignment: .leading, spacing: 12) {15 HStack(spacing: 8) {16 Image(systemName: "sparkles")17 .font(.subheadline.weight(.bold))18 .foregroundStyle(KATheme.lime)19 .frame(width: 28, height: 28)20 .background(KATheme.inkLight, in: RoundedRectangle(cornerRadius: 9, style: .continuous))21 VStack(alignment: .leading, spacing: 0) {22 Text("Pour vous").font(KAFont.display(22))23 .foregroundStyle(KATheme.ink(scheme))24 Text("Selon vos visites — tout reste sur votre appareil")25 .font(.caption2).foregroundStyle(.secondary)26 }27 Spacer()28 }29 .accessibilityElement(children: .combine)30 .accessibilityAddTraits(.isHeader)3132 ForEach(entries) { entry in33 NavigationLink(value: entry.item) {34 ForYouCard(entry: entry)35 }36 .buttonStyle(KAPressStyle())37 .kaScrollPop(axis: .vertical)38 }39 }40 }41}4243/// Grande carte immersive : photo pleine largeur, raison en badge accent,44/// titre display sur scrim. Sans photo : rangée éditoriale avec pastille.45struct ForYouCard: View {46 let entry: ForYouEntry47 @Environment(\.colorScheme) private var scheme4849 private var universe: Universe? { Ecosystem.universe(entry.item.universeID) }50 private var accent: Color { universe?.accent ?? KATheme.green }5152 var body: some View {53 Group {54 if entry.item.imageURL != nil {55 immersive56 } else {57 row58 }59 }60 .accessibilityElement(children: .combine)61 .accessibilityLabel("\(entry.reason). \(entry.item.title)\(entry.item.priceLabel.map { ", \($0)" } ?? "")")62 }6364 private var immersive: some View {65 ZStack(alignment: .bottomLeading) {66 // l'image vit en OVERLAY d'un cadre fixe : sa largeur idéale67 // (panoramas!) ne se propage jamais à la mise en page68 Color.clear69 .frame(height: 200)70 .frame(maxWidth: .infinity)71 .overlay(KAImage(url: entry.item.imageURL, accent: accent,72 symbol: universe?.symbol ?? "sparkles"))73 .clipped()74 LinearGradient(colors: [.clear, .black.opacity(0.10), .black.opacity(0.82)],75 startPoint: .top, endPoint: .bottom)76 VStack(alignment: .leading, spacing: 6) {77 HStack(spacing: 5) {78 Image(systemName: "sparkle").font(.system(size: 8, weight: .bold))79 Text(entry.reason.uppercased())80 .font(KAFont.mono(8))81 .kerning(0.8)82 .lineLimit(1)83 .minimumScaleFactor(0.8)84 }85 .padding(.horizontal, 9).padding(.vertical, 5)86 .background(accent, in: Capsule())87 .foregroundStyle(.white)88 Text(entry.item.title)89 .font(KAFont.display(19))90 .foregroundStyle(.white)91 .lineLimit(2)92 .multilineTextAlignment(.leading)93 HStack(spacing: 8) {94 if let p = entry.item.priceLabel {95 Text(p)96 .font(.system(.subheadline, design: .rounded, weight: .heavy))97 .foregroundStyle(KATheme.lime)98 }99 if let c = entry.item.city {100 Text(c).font(.caption).foregroundStyle(.white.opacity(0.78))101 }102 Spacer(minLength: 0)103 if let u = universe {104 Text(u.wordmark)105 .font(KAFont.mono(9))106 .foregroundStyle(.white)107 .padding(.horizontal, 8).padding(.vertical, 4)108 .background(.white.opacity(0.18), in: Capsule())109 }110 }111 }112 .padding(14)113 }114 .clipShape(RoundedRectangle(cornerRadius: 18, style: .continuous))115 .overlay(RoundedRectangle(cornerRadius: 18, style: .continuous)116 .strokeBorder(KATheme.ink(scheme).opacity(scheme == .dark ? 0.35 : 0.85), lineWidth: 1.5))117 .background(RoundedRectangle(cornerRadius: 18, style: .continuous)118 .fill(accent.opacity(scheme == .dark ? 0.35 : 1))119 .offset(x: 6, y: 6))120 }121122 private var row: some View {123 HStack(alignment: .top, spacing: 12) {124 RoundedRectangle(cornerRadius: 8, style: .continuous)125 .fill(accent)126 .frame(width: 5)127 .padding(.vertical, 2)128 VStack(alignment: .leading, spacing: 4) {129 HStack(spacing: 5) {130 Image(systemName: "sparkle").font(.system(size: 8, weight: .bold))131 Text(entry.reason.uppercased())132 .font(KAFont.mono(8))133 .kerning(0.6)134 .lineLimit(1)135 .minimumScaleFactor(0.85)136 }137 .foregroundStyle(accent)138 Text(entry.item.title)139 .font(.subheadline.weight(.bold))140 .foregroundStyle(KATheme.ink(scheme))141 .lineLimit(2)142 .multilineTextAlignment(.leading)143 HStack(spacing: 8) {144 if let p = entry.item.priceLabel {145 Text(p)146 .font(.system(.caption, design: .rounded).weight(.bold))147 .foregroundStyle(accent)148 .lineLimit(1)149 }150 if let c = entry.item.city {151 Text(c).font(.caption2).foregroundStyle(.tertiary).lineLimit(1)152 }153 Spacer(minLength: 0)154 if let u = universe {155 KAChip(text: u.wordmark, accent: u.accent)156 }157 }158 }159 }160 .padding(12)161 .kaCard(accent: accent)162 }163}164