SPB Git

spb/ka-ios

Public
10.3 KB · 269 lines swift
1// Auteur : Simon-Pierre Boucher — contact@spboucher.ai2// UniverseShowcase.swift — le rendu LÉGENDAIRE d'un univers (onglet Explorer) :3// bande héros teintée à l'accent avec logo en filigrane, pouls de l'univers4// (compteurs roulants), villes rapides filtrantes, VITRINE immersive du5// premier item, carrousel « En vedette » et mosaïque magazine 2 colonnes.6// S'adapte : univers sans photos (Job·Ka, Trouve·Ka) → liste éditoriale.7import SwiftUI89// MARK: - Bande héros : gradient accent + logo en filigrane autour du SiteHero1011struct UniverseHeroBand<Content: View>: View {12    let universe: Universe13    @ViewBuilder var content: Content14    @Environment(\.colorScheme) private var scheme1516    var body: some View {17        ZStack(alignment: .topTrailing) {18            RoundedRectangle(cornerRadius: 18, style: .continuous)19                .fill(LinearGradient(20                    colors: [universe.accent.opacity(scheme == .dark ? 0.22 : 0.16),21                             universe.accent.opacity(0.03)],22                    startPoint: .topTrailing, endPoint: .bottomLeading))23            KALogo(universe: universe, size: 150)24                .opacity(scheme == .dark ? 0.10 : 0.07)25                .rotationEffect(.degrees(8))26                .offset(x: 34, y: -26)27                .allowsHitTesting(false)28                .accessibilityHidden(true)29            content30                .padding(14)31        }32        .clipShape(RoundedRectangle(cornerRadius: 18, style: .continuous))33        .overlay(RoundedRectangle(cornerRadius: 18, style: .continuous)34            .strokeBorder(universe.accent.opacity(0.25), lineWidth: 1))35    }36}3738// MARK: - En-tête de section éditorial (carré accent + mono uppercase)3940struct UniverseSectionHeader: View {41    let title: String42    let accent: Color43    var detail: String? = nil44    @Environment(\.colorScheme) private var scheme4546    var body: some View {47        HStack(spacing: 8) {48            Rectangle().fill(accent).frame(width: 8, height: 8)49            Text(title.uppercased())50                .font(KAFont.mono(10))51                .kerning(1.0)52                .foregroundStyle(KATheme.ink(scheme))53            if let d = detail {54                Text(d)55                    .font(KAFont.mono(9, bold: false))56                    .foregroundStyle(KATheme.ink2(scheme))57            }58            Spacer()59        }60        .padding(.top, 6)61        .accessibilityElement(children: .combine)62        .accessibilityAddTraits(.isHeader)63    }64}6566// MARK: - Le pouls de l'univers (tuiles mono, compteurs roulants)6768struct UniverseStatStrip: View {69    let universe: Universe70    let liveTotal: Int?71    let shown: Int72    let cityCount: Int73    @Environment(\.colorScheme) private var scheme7475    var body: some View {76        HStack(spacing: 10) {77            if let n = liveTotal {78                tile { KACountUp(value: n, font: KAFont.display(17), color: universe.accent) }79                    label: { universe.unit }80            }81            tile { Text("\(shown)").font(KAFont.display(17)).foregroundStyle(KATheme.ink(scheme)) }82                label: { "affichés" }83            if cityCount > 1 {84                tile { Text("\(cityCount)").font(KAFont.display(17)).foregroundStyle(KATheme.ink(scheme)) }85                    label: { "villes" }86            }87            Spacer(minLength: 0)88        }89    }9091    private func tile(@ViewBuilder _ value: () -> some View, label: () -> String) -> some View {92        VStack(alignment: .leading, spacing: 1) {93            value()94            Text(label().uppercased())95                .font(KAFont.mono(8))96                .foregroundStyle(KATheme.ink2(scheme))97        }98        .padding(.horizontal, 12).padding(.vertical, 8)99        .kaCard(accent: universe.accent)100        .accessibilityElement(children: .combine)101    }102}103104// MARK: - Villes rapides (filtre en un tap)105106struct UniverseCityChips: View {107    let cities: [String]108    @Binding var selected: String?109    let accent: Color110    var onChange: () -> Void111    @Environment(\.colorScheme) private var scheme112113    var body: some View {114        ScrollView(.horizontal, showsIndicators: false) {115            HStack(spacing: 8) {116                ForEach(cities, id: \.self) { city in117                    let on = selected == city118                    Button {119                        Haptics.tap()120                        selected = on ? nil : city121                        onChange()122                    } label: {123                        Text(city)124                            .font(.system(.caption, design: .rounded, weight: .bold))125                            .padding(.horizontal, 12).padding(.vertical, 7)126                            .background(on ? accent : KATheme.surface(scheme), in: Capsule())127                            .foregroundStyle(on ? .white : KATheme.ink(scheme))128                            .overlay(Capsule().strokeBorder(129                                on ? accent : KATheme.ink(scheme).opacity(0.3), lineWidth: 1.2))130                    }131                    .accessibilityLabel("\(on ? "Retirer le filtre" : "Filtrer sur") \(city)")132                }133            }134            .padding(.vertical, 2)135        }136    }137}138139// MARK: - VITRINE : le premier item en pleine largeur, prix sur la photo140141struct UniverseShowcaseCard: View {142    let item: KAItem143    let universe: Universe144    @Environment(\.colorScheme) private var scheme145146    var body: some View {147        ZStack(alignment: .bottomLeading) {148            KAImage(url: item.imageURL, accent: universe.accent, symbol: universe.symbol)149                .frame(height: 224)150                .frame(maxWidth: .infinity)151                .clipped()152            LinearGradient(colors: [.clear, .black.opacity(0.05), .black.opacity(0.78)],153                           startPoint: .top, endPoint: .bottom)154            VStack(alignment: .leading, spacing: 5) {155                Text("À LA UNE")156                    .font(KAFont.mono(8.5))157                    .kerning(1.2)158                    .padding(.horizontal, 8).padding(.vertical, 4)159                    .background(universe.accent, in: Capsule())160                    .foregroundStyle(.white)161                Text(item.title)162                    .font(KAFont.display(19))163                    .foregroundStyle(.white)164                    .lineLimit(2)165                    .multilineTextAlignment(.leading)166                HStack(spacing: 8) {167                    if let p = item.priceLabel {168                        Text(p)169                            .font(.system(.subheadline, design: .rounded, weight: .heavy))170                            .foregroundStyle(KATheme.lime)171                    }172                    if let c = item.city {173                        Text(c).font(.caption).foregroundStyle(.white.opacity(0.75))174                    }175                }176            }177            .padding(14)178        }179        .clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous))180        .overlay(RoundedRectangle(cornerRadius: 16, style: .continuous)181            .strokeBorder(KATheme.ink(scheme).opacity(scheme == .dark ? 0.35 : 0.85), lineWidth: 1.5))182        .background(RoundedRectangle(cornerRadius: 16, style: .continuous)183            .fill(universe.accent.opacity(scheme == .dark ? 0.35 : 1))184            .offset(x: 6, y: 6))185        .accessibilityElement(children: .combine)186        .accessibilityLabel("À la une : \(item.title)\(item.priceLabel.map { ", \($0)" } ?? "")")187    }188}189190// MARK: - Carte du carrousel « En vedette »191192struct UniverseFeaturedCard: View {193    let item: KAItem194    let universe: Universe195    @Environment(\.colorScheme) private var scheme196197    var body: some View {198        VStack(alignment: .leading, spacing: 0) {199            KAImage(url: item.imageURL, accent: universe.accent, symbol: universe.symbol)200                .frame(width: 196, height: 112)201                .clipped()202            VStack(alignment: .leading, spacing: 4) {203                Text(item.title)204                    .font(.subheadline.weight(.semibold))205                    .lineLimit(2, reservesSpace: true)206                    .multilineTextAlignment(.leading)207                HStack {208                    if let p = item.priceLabel {209                        Text(p)210                            .font(.system(.caption, design: .rounded).weight(.bold))211                            .foregroundStyle(universe.accent)212                            .lineLimit(1)213                    }214                    Spacer()215                    if let c = item.city {216                        Text(c).font(.caption2).foregroundStyle(.tertiary).lineLimit(1)217                    }218                }219            }220            .padding(10)221        }222        .frame(width: 196, alignment: .topLeading)223        .kaCard(accent: universe.accent)224    }225}226227// MARK: - Cellule de la mosaïque magazine (grille 2 colonnes)228229struct UniverseMosaicCell: View {230    let item: KAItem231    let universe: Universe232    @Environment(\.colorScheme) private var scheme233234    var body: some View {235        VStack(alignment: .leading, spacing: 0) {236            if item.imageURL != nil {237                KAImage(url: item.imageURL, accent: universe.accent, symbol: universe.symbol)238                    .frame(height: 108)239                    .frame(maxWidth: .infinity)240                    .clipped()241            } else {242                Rectangle()243                    .fill(universe.accent.opacity(0.12))244                    .frame(height: 54)245                    .overlay(Image(systemName: universe.symbol)246                        .foregroundStyle(universe.accent.opacity(0.7)))247            }248            VStack(alignment: .leading, spacing: 3) {249                Text(item.title)250                    .font(.caption.weight(.semibold))251                    .lineLimit(2, reservesSpace: true)252                    .multilineTextAlignment(.leading)253                if let p = item.priceLabel {254                    Text(p)255                        .font(.system(.caption2, design: .rounded).weight(.bold))256                        .foregroundStyle(universe.accent)257                        .lineLimit(1)258                }259                if let c = item.city {260                    Text(c).font(.caption2).foregroundStyle(.tertiary).lineLimit(1)261                }262            }263            .padding(9)264        }265        .frame(maxWidth: .infinity, alignment: .topLeading)266        .kaCard(accent: universe.accent)267    }268}269
Files · 49720973fdf7a4e5315f9bb1e1fdf61c629116cd
Menu

Theme