SPB Git forge

spb/ka-ios

Public
20commits 1branches 0releases
17.2 MBsize
maindefault branch
28 days agolast push
Swift 100%
10.4 KB · 271 lines swift
Raw Blame History
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            // image en overlay d'un cadre fixe : jamais de débordement de layout149            Color.clear150                .frame(height: 224)151                .frame(maxWidth: .infinity)152                .overlay(KAImage(url: item.imageURL, accent: universe.accent, symbol: universe.symbol))153                .clipped()154            LinearGradient(colors: [.clear, .black.opacity(0.05), .black.opacity(0.78)],155                           startPoint: .top, endPoint: .bottom)156            VStack(alignment: .leading, spacing: 5) {157                Text("À LA UNE")158                    .font(KAFont.mono(8.5))159                    .kerning(1.2)160                    .padding(.horizontal, 8).padding(.vertical, 4)161                    .background(universe.accent, in: Capsule())162                    .foregroundStyle(.white)163                Text(item.title)164                    .font(KAFont.display(19))165                    .foregroundStyle(.white)166                    .lineLimit(2)167                    .multilineTextAlignment(.leading)168                HStack(spacing: 8) {169                    if let p = item.priceLabel {170                        Text(p)171                            .font(.system(.subheadline, design: .rounded, weight: .heavy))172                            .foregroundStyle(KATheme.lime)173                    }174                    if let c = item.city {175                        Text(c).font(.caption).foregroundStyle(.white.opacity(0.75))176                    }177                }178            }179            .padding(14)180        }181        .clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous))182        .overlay(RoundedRectangle(cornerRadius: 16, style: .continuous)183            .strokeBorder(KATheme.ink(scheme).opacity(scheme == .dark ? 0.35 : 0.85), lineWidth: 1.5))184        .background(RoundedRectangle(cornerRadius: 16, style: .continuous)185            .fill(universe.accent.opacity(scheme == .dark ? 0.35 : 1))186            .offset(x: 6, y: 6))187        .accessibilityElement(children: .combine)188        .accessibilityLabel("À la une : \(item.title)\(item.priceLabel.map { ", \($0)" } ?? "")")189    }190}191192// MARK: - Carte du carrousel « En vedette »193194struct UniverseFeaturedCard: View {195    let item: KAItem196    let universe: Universe197    @Environment(\.colorScheme) private var scheme198199    var body: some View {200        VStack(alignment: .leading, spacing: 0) {201            KAImage(url: item.imageURL, accent: universe.accent, symbol: universe.symbol)202                .frame(width: 196, height: 112)203                .clipped()204            VStack(alignment: .leading, spacing: 4) {205                Text(item.title)206                    .font(.subheadline.weight(.semibold))207                    .lineLimit(2, reservesSpace: true)208                    .multilineTextAlignment(.leading)209                HStack {210                    if let p = item.priceLabel {211                        Text(p)212                            .font(.system(.caption, design: .rounded).weight(.bold))213                            .foregroundStyle(universe.accent)214                            .lineLimit(1)215                    }216                    Spacer()217                    if let c = item.city {218                        Text(c).font(.caption2).foregroundStyle(.tertiary).lineLimit(1)219                    }220                }221            }222            .padding(10)223        }224        .frame(width: 196, alignment: .topLeading)225        .kaCard(accent: universe.accent)226    }227}228229// MARK: - Cellule de la mosaïque magazine (grille 2 colonnes)230231struct UniverseMosaicCell: View {232    let item: KAItem233    let universe: Universe234    @Environment(\.colorScheme) private var scheme235236    var body: some View {237        VStack(alignment: .leading, spacing: 0) {238            if item.imageURL != nil {239                KAImage(url: item.imageURL, accent: universe.accent, symbol: universe.symbol)240                    .frame(height: 108)241                    .frame(maxWidth: .infinity)242                    .clipped()243            } else {244                Rectangle()245                    .fill(universe.accent.opacity(0.12))246                    .frame(height: 54)247                    .overlay(Image(systemName: universe.symbol)248                        .foregroundStyle(universe.accent.opacity(0.7)))249            }250            VStack(alignment: .leading, spacing: 3) {251                Text(item.title)252                    .font(.caption.weight(.semibold))253                    .lineLimit(2, reservesSpace: true)254                    .multilineTextAlignment(.leading)255                if let p = item.priceLabel {256                    Text(p)257                        .font(.system(.caption2, design: .rounded).weight(.bold))258                        .foregroundStyle(universe.accent)259                        .lineLimit(1)260                }261                if let c = item.city {262                    Text(c).font(.caption2).foregroundStyle(.tertiary).lineLimit(1)263                }264            }265            .padding(9)266        }267        .frame(maxWidth: .infinity, alignment: .topLeading)268        .kaCard(accent: universe.accent)269    }270}271