SPB Git forge

spb/ka-ios

Public
20commits 1branches 0releases
17.2 MBsize
maindefault branch
28 days agolast push
Swift 100%
10.5 KB · 287 lines swift
Raw Blame History
1// Auteur : Simon-Pierre Boucher — contact@spboucher.ai2// Theme.swift — design system Groupe-KA adapté iOS (HIG) : papier/encre en3// clair, encre/papier en sombre, accents par univers, composants signature4// (carte à ombre décalée, puce, wordmark) + haptique.5import SwiftUI6import UIKit78// MARK: - Palette910enum KATheme {11    static let paperLight = Color(hex: "#f5f3ee")12    static let paperDark = Color(hex: "#101410")13    static let inkLight = Color(hex: "#141814")14    static let inkDark = Color(hex: "#f0efe8")15    static let lime = Color(hex: "#d9f26b")16    static let green = Color(hex: "#1c5c41")1718    static func paper(_ scheme: ColorScheme) -> Color { scheme == .dark ? paperDark : paperLight }19    static func ink(_ scheme: ColorScheme) -> Color { scheme == .dark ? inkDark : inkLight }20    static func surface(_ scheme: ColorScheme) -> Color { scheme == .dark ? Color(hex: "#1a1f1a") : .white }21    static func ink2(_ scheme: ColorScheme) -> Color { scheme == .dark ? Color(hex: "#a9b0a9") : Color(hex: "#4d5551") }22}2324// MARK: - Haptique2526enum Haptics {27    static func tap() { UIImpactFeedbackGenerator(style: .light).impactOccurred() }28    static func success() { UINotificationFeedbackGenerator().notificationOccurred(.success) }29    static func rigid() { UIImpactFeedbackGenerator(style: .rigid).impactOccurred() }30}3132// MARK: - Carte « éditorial sharp » — tokens EXACTS des sites (tokens.css) :33// rayon 10 px, bordure encre 1,5 px, ombre décalée DURE 6×6 pleine encre.3435struct KACard: ViewModifier {36    @Environment(\.colorScheme) private var scheme37    var accent: Color? = nil3839    func body(content: Content) -> some View {40        content41            .background(KATheme.surface(scheme))42            .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous))43            .overlay(44                RoundedRectangle(cornerRadius: 10, style: .continuous)45                    .strokeBorder(KATheme.ink(scheme).opacity(scheme == .dark ? 0.35 : 0.85), lineWidth: 1.5)46            )47            .background(48                RoundedRectangle(cornerRadius: 10, style: .continuous)49                    .fill(scheme == .dark50                          ? (accent ?? KATheme.inkDark).opacity(0.25)51                          : KATheme.inkLight)52                    .offset(x: 6, y: 6)53            )54    }55}5657extension View {58    func kaCard(accent: Color? = nil) -> some View { modifier(KACard(accent: accent)) }59}6061// MARK: - Wordmark (« Lou » + boîte [Ka] accent)6263struct KAWordmark: View {64    let universe: Universe65    var size: CGFloat = 2266    @Environment(\.colorScheme) private var scheme6768    var body: some View {69        // fidèle à l'en-tête des sites : « Lou- » encre + boîte ACCENT au texte blanc70        let parts = universe.wordmark.split(separator: "·", maxSplits: 1)71        HStack(alignment: .firstTextBaseline, spacing: 3) {72            Text(parts.count > 1 ? String(parts[0]) + "-" : universe.wordmark)73                .font(KAFont.display(size))74            if parts.count > 1 {75                Text(String(parts[1]))76                    .font(KAFont.display(size * 0.86))77                    .foregroundStyle(.white)78                    .padding(.horizontal, size * 0.28)79                    .padding(.vertical, size * 0.08)80                    .background(universe.accent, in: RoundedRectangle(cornerRadius: size * 0.26, style: .continuous))81            }82        }83        .foregroundStyle(KATheme.ink(scheme))84        .accessibilityLabel(universe.name)85    }86}8788// MARK: - Logo officiel (tuile de marque ka-ui, PNG 512 aux coins déjà arrondis)8990struct KALogo: View {91    let assetID: String            // « lou-ka » … « groupe-ka »92    var size: CGFloat = 409394    init(universe: Universe, size: CGFloat = 40) {95        self.assetID = universe.id96        self.size = size97    }98    init(assetID: String, size: CGFloat = 40) {99        self.assetID = assetID100        self.size = size101    }102103    var body: some View {104        Image("logo-\(assetID)")105            .resizable()106            .aspectRatio(contentMode: .fit)107            .frame(width: size, height: size)108            .accessibilityHidden(true)109    }110}111112struct GroupeKAMark: View {113    var size: CGFloat = 24114    @Environment(\.colorScheme) private var scheme115    var body: some View {116        HStack(alignment: .firstTextBaseline, spacing: 4) {117            Text("Groupe").font(KAFont.display(size))118            Text("KA")119                .font(KAFont.display(size * 0.9))120                .foregroundStyle(KATheme.lime)121                .padding(.horizontal, size * 0.3).padding(.vertical, size * 0.1)122                .background(KATheme.inkLight, in: RoundedRectangle(cornerRadius: size * 0.28, style: .continuous))123                .rotationEffect(.degrees(-2))124        }125        .fixedSize()126        .foregroundStyle(KATheme.ink(scheme))127        .accessibilityLabel("Groupe KA")128    }129}130131// MARK: - Puce mono (klabel)132133struct KAChip: View {134    let text: String135    var accent: Color? = nil136    @Environment(\.colorScheme) private var scheme137    var body: some View {138        Text(text)139            .font(KAFont.mono(10))140            .textCase(.uppercase)141            .padding(.horizontal, 9).padding(.vertical, 4)142            .background((accent ?? KATheme.ink(scheme)).opacity(0.12), in: Capsule())143            .overlay(Capsule().strokeBorder(KATheme.ink(scheme).opacity(0.4), lineWidth: 1))144    }145}146147// MARK: - Rangée d'item universelle148149struct KAItemRow: View {150    let item: KAItem151    @Environment(\.colorScheme) private var scheme152    @EnvironmentObject private var favorites: FavoritesStore153154    private var universe: Universe? { Ecosystem.universe(item.universeID) }155156    var body: some View {157        HStack(alignment: .top, spacing: 12) {158            if let img = item.imageURL {159                KAImage(url: img, accent: universe?.accent ?? .gray, symbol: universe?.symbol ?? "photo")160                .frame(width: 74, height: 74)161                .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous))162                .overlay(RoundedRectangle(cornerRadius: 10, style: .continuous)163                    .strokeBorder(.primary.opacity(0.25), lineWidth: 1))164                .accessibilityHidden(true)165            } else {166                RoundedRectangle(cornerRadius: 8, style: .continuous)167                    .fill(universe?.accent.opacity(0.9) ?? .gray)168                    .frame(width: 5)169                    .padding(.vertical, 2)170            }171            VStack(alignment: .leading, spacing: 4) {172                Text(item.title)173                    .font(.headline)174                    .lineLimit(2)175                if let sub = item.subtitle, !sub.isEmpty {176                    Text(sub).font(.subheadline)177                        .foregroundStyle(KATheme.ink2(scheme)).lineLimit(2)178                }179                HStack(spacing: 8) {180                    if let price = item.priceLabel {181                        Text(price).font(.system(.subheadline, design: .rounded).weight(.bold))182                            .foregroundStyle(universe?.accent ?? .primary)183                    }184                    if let city = item.city, !city.isEmpty {185                        Text(city).font(.caption).foregroundStyle(KATheme.ink2(scheme))186                    }187                    Spacer(minLength: 0)188                    if let u = universe {189                        KAChip(text: u.wordmark, accent: u.accent)190                    }191                }192            }193            if favorites.isFavorite(item) {194                Image(systemName: "heart.fill")195                    .foregroundStyle(.red).font(.caption)196                    .accessibilityLabel("Dans vos favoris")197            }198        }199        .padding(12)200        .kaCard(accent: universe?.accent)201    }202}203204// MARK: - Image réseau fluide (fondu à l'arrivée, cache URLCache partagé)205206struct KAImage: View {207    let url: URL?208    var accent: Color = .gray209    var symbol: String = "photo"210211    var body: some View {212        AsyncImage(url: url, transaction: Transaction(animation: .easeOut(duration: 0.25))) { phase in213            switch phase {214            case .success(let image):215                image.resizable().aspectRatio(contentMode: .fill)216                    .transition(.opacity)217            case .failure:218                accent.opacity(0.1)219                    .overlay(Image(systemName: symbol).foregroundStyle(accent.opacity(0.6)))220            default:221                accent.opacity(0.08)222                    .overlay(Image(systemName: symbol).foregroundStyle(accent.opacity(0.35)))223            }224        }225    }226}227228// MARK: - Pression tactile (échelle + réactivité)229230struct KAPressStyle: ButtonStyle {231    func makeBody(configuration: Configuration) -> some View {232        configuration.label233            .scaleEffect(configuration.isPressed ? 0.97 : 1)234            .animation(.spring(response: 0.25, dampingFraction: 0.7), value: configuration.isPressed)235    }236}237238// MARK: - Squelette de chargement (liste)239240struct KASkeletonRow: View {241    @Environment(\.colorScheme) private var scheme242    var body: some View {243        HStack(alignment: .top, spacing: 12) {244            RoundedRectangle(cornerRadius: 10).fill(.quaternary).frame(width: 74, height: 74)245            VStack(alignment: .leading, spacing: 8) {246                RoundedRectangle(cornerRadius: 4).fill(.quaternary).frame(height: 14)247                RoundedRectangle(cornerRadius: 4).fill(.quaternary).frame(width: 180, height: 11)248                RoundedRectangle(cornerRadius: 4).fill(.quaternary).frame(width: 90, height: 11)249            }250        }251        .padding(12).kaCard()252        .redacted(reason: .placeholder)253        .shimmering()254    }255}256257struct Shimmer: ViewModifier {258    @State private var on = false259    func body(content: Content) -> some View {260        content261            .opacity(on ? 0.5 : 0.9)262            .animation(.easeInOut(duration: 0.8).repeatForever(autoreverses: true), value: on)263            .onAppear { on = true }264    }265}266extension View { func shimmering() -> some View { modifier(Shimmer()) } }267268// MARK: - États269270struct KAEmptyState: View {271    let symbol: String272    let title: String273    var message: String? = nil274    var body: some View {275        VStack(spacing: 10) {276            Image(systemName: symbol).font(.system(size: 40)).foregroundStyle(.secondary)277            Text(title).font(.headline)278            if let m = message {279                Text(m).font(.subheadline).foregroundStyle(.secondary)280                    .multilineTextAlignment(.center)281            }282        }283        .padding(30)284        .frame(maxWidth: .infinity)285    }286}287