v2.0.0 (8) — MAJOR : l'app ressemble aux sites — logos officiels des 13 marques (ka-ui), sites web intégrés (WKWebView SiteBrowser par univers + hub), design system fidèle (Space Grotesk/JetBrains Mono embarquées, ombres dures 6×6, rayon 10, wordmark boîte accent), héros de site par univers (ticker encre, kicker, titre surligné, pilule live, contenu réel des sites)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
42 changed files +616 −62
modified
KA/App/KAApp.swift
+4 −2
@@ -35,8 +35,10 @@ struct KAApp: App { | ||
| 35 | 35 | |
| 36 | 36 | struct RootView: View { |
| 37 | 37 | @State private var showAgent = false |
| 38 | − // onglet initial pilotable pour les tests (defaults write com.groupeka.ka ka.debug.tab -int 3) | |
| 39 | − @State private var tab = UserDefaults.standard.integer(forKey: "ka.debug.tab") | |
| 38 | + // onglet initial pilotable en debug — JAMAIS pendant les tests (les threads | |
| 39 | + // MapLibre de la carte feraient une course avec le exit() de XCTest) | |
| 40 | + @State private var tab = ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil | |
| 41 | + ? 0 : UserDefaults.standard.integer(forKey: "ka.debug.tab") | |
| 40 | 42 | @StateObject private var recents = RecentsStore.shared |
| 41 | 43 | |
| 42 | 44 | var body: some View { |
added
KA/Core/HeroCopy.swift
+85 −0
@@ -0,0 +1,85 @@ | ||
| 1 | +// Auteur : Simon-Pierre Boucher — contact@spboucher.ai | |
| 2 | +// HeroCopy.swift — le VRAI contenu héros de chaque site du Groupe KA (extrait | |
| 3 | +// des pages d'accueil le 2026-08-21, source ka-ui/hero-copy.json) : kicker mono, | |
| 4 | +// titre display avec segment surligné en boîte accent, sous-titre. C'est ce qui | |
| 5 | +// fait que chaque univers de l'app ressemble à SON site. | |
| 6 | + | |
| 7 | +struct HeroCopy { | |
| 8 | + let kicker: String? | |
| 9 | + let headline: String | |
| 10 | + /// Segment du titre affiché SUR l'accent (suffixe ou fragment de headline) | |
| 11 | + let highlight: String? | |
| 12 | + let sub: String | |
| 13 | +} | |
| 14 | + | |
| 15 | +enum KAHeroes { | |
| 16 | + static func hero(_ id: String) -> HeroCopy? { all[id] } | |
| 17 | + | |
| 18 | + static let all: [String: HeroCopy] = [ | |
| 19 | + "lou-ka": .init( | |
| 20 | + kicker: "La porte d'entrée vers votre prochain chez-vous.", | |
| 21 | + headline: "Trouvez votre prochain chez-vous.", | |
| 22 | + highlight: "chez-vous.", | |
| 23 | + sub: "Tous les logements à louer au Québec, regroupés au même endroit — mis à jour automatiquement, avec toutes les photos et un lien direct vers l'annonce originale."), | |
| 24 | + "immo-ka": .init( | |
| 25 | + kicker: "Agrégateur — toutes les agences du Québec", | |
| 26 | + headline: "Toutes les propriétés à vendre, un seul endroit.", | |
| 27 | + highlight: "un seul", | |
| 28 | + sub: "Immo-Ka rassemble les propriétés à vendre affichées par les agences de courtage immobilier du Québec — RE/MAX, Sutton, Via Capitale, Proprio Direct et plus — avec toutes les photos et un lien direct vers l'annonce originale."), | |
| 29 | + "vrai-prix": .init( | |
| 30 | + kicker: "Québec · 2021-2026", | |
| 31 | + headline: "La vraie valeur. Sans boîte noire.", | |
| 32 | + highlight: "Sans boîte noire.", | |
| 33 | + sub: "Estimation basée sur 3,7 millions d'unités d'évaluation et 745 000 ventes réelles au Québec (2021-2026). Pas de boîte noire : on vous montre le calcul."), | |
| 34 | + "auto-ka": .init( | |
| 35 | + kicker: "Agrégateur indépendant — direct des concessionnaires", | |
| 36 | + headline: "Toutes les voitures usagées à vendre au Québec. Un seul endroit.", | |
| 37 | + highlight: "Un seul endroit.", | |
| 38 | + sub: "Auto-Ka visite les sites des concessionnaires et marchands d'occasion de toutes les régions, normalise chaque annonce et détecte les nouveautés, les ventes et les baisses de prix — automatiquement."), | |
| 39 | + "fabri-ka": .init( | |
| 40 | + kicker: "Le répertoire des produits d'ici", | |
| 41 | + headline: "Tous les produits québécois. Un seul endroit.", | |
| 42 | + highlight: "Un seul endroit.", | |
| 43 | + sub: "Fabri-Ka rassemble les catalogues des boutiques en ligne d'ici — fabriqué, conçu et vendu au Québec."), | |
| 44 | + "food-ka": .init( | |
| 45 | + kicker: "Agrégateur — épicerie du Québec", | |
| 46 | + headline: "Tous les prix d'épicerie du Québec. Un seul endroit.", | |
| 47 | + highlight: "un seul", | |
| 48 | + sub: "Food·Ka rassemble les produits affichés par les bannières d'épicerie du Québec — Metro, IGA, Maxi, Super C, Provigo et plus — mis à jour automatiquement, avec prix unitaires et lien direct vers la fiche originale."), | |
| 49 | + "resto-ka": .init( | |
| 50 | + kicker: "Agrégateur — 17 régions du Québec", | |
| 51 | + headline: "Tous les restos. Tous les menus. Tous les prix.", | |
| 52 | + highlight: "Tous les prix.", | |
| 53 | + sub: "Resto-Ka rassemble les restaurants du Québec avec leurs menus complets et leurs prix réels — cherchez un resto ou un plat, comparez, le contexte du prix (salle, emporter, livraison) est toujours affiché."), | |
| 54 | + "sorti-ka": .init( | |
| 55 | + kicker: "Agrégateur indépendant · mise à jour horaire", | |
| 56 | + headline: "Toutes les sorties du Québec, un seul endroit.", | |
| 57 | + highlight: "un seul endroit.", | |
| 58 | + sub: "Concerts, festivals, spectacles, expositions, sport, famille — agrégés depuis des billetteries et des sources ouvertes officielles, dans les 17 régions. Chaque fiche renvoie vers la billetterie ou la source originale."), | |
| 59 | + "crea-ka": .init( | |
| 60 | + kicker: "Annuaire des créateurs du Québec", | |
| 61 | + headline: "Chaque créateur québécois. Tous ses liens.", | |
| 62 | + highlight: "liens.", | |
| 63 | + sub: "Un « Linktree collectif » du Québec : influenceurs, youtubeurs, streamers, podcasteurs — leurs comptes regroupés et découvrables par niche, région, langue et taille d'audience."), | |
| 64 | + "job-ka": .init( | |
| 65 | + kicker: "Offres directement des employeurs", | |
| 66 | + headline: "L'emploi au Québec, à la source.", | |
| 67 | + highlight: "à la source.", | |
| 68 | + sub: "Job·Ka agrège les offres directement depuis les pages carrières des employeurs québécois. Salaires transparents, lien vers l'offre originale, aucun intermédiaire."), | |
| 69 | + "trouve-ka": .init( | |
| 70 | + kicker: "Le moteur de recherche du web québécois", | |
| 71 | + headline: "Trouve KA", | |
| 72 | + highlight: "KA", | |
| 73 | + sub: "Le moteur de recherche indépendant du web québécois."), | |
| 74 | + "api-ka": .init( | |
| 75 | + kicker: "Plateforme centrale · node M3U96b", | |
| 76 | + headline: "Toutes les données KA, centralisées chaque jour.", | |
| 77 | + highlight: "KA", | |
| 78 | + sub: "API-Ka collecte, sauvegarde et historise quotidiennement les données de l'écosystème Groupe KA, puis les expose via une API publique, versionnée et documentée."), | |
| 79 | + "groupe-ka": .init( | |
| 80 | + kicker: "Holding d'agrégateurs automatisés · Québec", | |
| 81 | + headline: "Mille sites. Un seul ·KA.", | |
| 82 | + highlight: "·KA.", | |
| 83 | + sub: "Ka, c'est l'agrégation. Des centaines de connecteurs lisent les sites à la source, normalisent la donnée et se resynchronisent seuls, jour et nuit."), | |
| 84 | + ] | |
| 85 | +} | |
added
KA/Design/KAFonts.swift
+21 −0
@@ -0,0 +1,21 @@ | ||
| 1 | +// Auteur : Simon-Pierre Boucher — contact@spboucher.ai | |
| 2 | +// KAFonts.swift — les polices OFFICIELLES des sites du Groupe KA (tokens.css) : | |
| 3 | +// display Space Grotesk · micro-étiquettes JetBrains Mono (embarquées, OFL). | |
| 4 | +// Le corps de texte reste SF (équivalent d'Inter côté iOS). | |
| 5 | +import SwiftUI | |
| 6 | + | |
| 7 | +enum KAFont { | |
| 8 | + /// Display « Space Grotesk » — titres, wordmarks, chiffres héros | |
| 9 | + static func display(_ size: CGFloat, _ weight: Font.Weight = .bold) -> Font { | |
| 10 | + switch weight { | |
| 11 | + case .bold, .heavy, .black: return .custom("SpaceGrotesk-Bold", size: size) | |
| 12 | + case .medium, .semibold: return .custom("SpaceGrotesk-Medium", size: size) | |
| 13 | + default: return .custom("SpaceGrotesk-Regular", size: size) | |
| 14 | + } | |
| 15 | + } | |
| 16 | + | |
| 17 | + /// Micro-étiquettes « JetBrains Mono » — kickers, compteurs, puces | |
| 18 | + static func mono(_ size: CGFloat, bold: Bool = true) -> Font { | |
| 19 | + .custom(bold ? "JetBrainsMono-Bold" : "JetBrainsMono-Regular", size: size) | |
| 20 | + } | |
| 21 | +} | |
modified
KA/Design/Theme.swift
+43 −16
@@ -29,7 +29,8 @@ enum Haptics { | ||
| 29 | 29 | static func rigid() { UIImpactFeedbackGenerator(style: .rigid).impactOccurred() } |
| 30 | 30 | } |
| 31 | 31 | |
| 32 | −// MARK: - Carte « éditorial sharp » (bordure encre + ombre décalée) | |
| 32 | +// 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. | |
| 33 | 34 | |
| 34 | 35 | struct KACard: ViewModifier { |
| 35 | 36 | @Environment(\.colorScheme) private var scheme |
@@ -38,15 +39,17 @@ struct KACard: ViewModifier { | ||
| 38 | 39 | func body(content: Content) -> some View { |
| 39 | 40 | content |
| 40 | 41 | .background(KATheme.surface(scheme)) |
| 41 | − .clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous)) | |
| 42 | + .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous)) | |
| 42 | 43 | .overlay( |
| 43 | − RoundedRectangle(cornerRadius: 14, style: .continuous) | |
| 44 | − .strokeBorder(KATheme.ink(scheme).opacity(scheme == .dark ? 0.35 : 0.85), lineWidth: 1.4) | |
| 44 | + RoundedRectangle(cornerRadius: 10, style: .continuous) | |
| 45 | + .strokeBorder(KATheme.ink(scheme).opacity(scheme == .dark ? 0.35 : 0.85), lineWidth: 1.5) | |
| 45 | 46 | ) |
| 46 | 47 | .background( |
| 47 | − RoundedRectangle(cornerRadius: 14, style: .continuous) | |
| 48 | − .fill((accent ?? KATheme.ink(scheme)).opacity(scheme == .dark ? 0.25 : 0.16)) | |
| 49 | − .offset(x: 5, y: 5) | |
| 48 | + RoundedRectangle(cornerRadius: 10, style: .continuous) | |
| 49 | + .fill(scheme == .dark | |
| 50 | + ? (accent ?? KATheme.inkDark).opacity(0.25) | |
| 51 | + : KATheme.inkLight) | |
| 52 | + .offset(x: 6, y: 6) | |
| 50 | 53 | ) |
| 51 | 54 | } |
| 52 | 55 | } |
@@ -63,18 +66,18 @@ struct KAWordmark: View { | ||
| 63 | 66 | @Environment(\.colorScheme) private var scheme |
| 64 | 67 | |
| 65 | 68 | var body: some View { |
| 69 | + // fidèle à l'en-tête des sites : « Lou- » encre + boîte ACCENT au texte blanc | |
| 66 | 70 | let parts = universe.wordmark.split(separator: "·", maxSplits: 1) |
| 67 | 71 | HStack(alignment: .firstTextBaseline, spacing: 3) { |
| 68 | − Text(parts.first.map(String.init) ?? universe.wordmark) | |
| 69 | − .font(.system(size: size, weight: .bold, design: .rounded)) | |
| 72 | + Text(parts.count > 1 ? String(parts[0]) + "-" : universe.wordmark) | |
| 73 | + .font(KAFont.display(size)) | |
| 70 | 74 | if parts.count > 1 { |
| 71 | 75 | Text(String(parts[1])) |
| 72 | − .font(.system(size: size * 0.86, weight: .bold, design: .rounded)) | |
| 73 | − .foregroundStyle(universe.accent) | |
| 76 | + .font(KAFont.display(size * 0.86)) | |
| 77 | + .foregroundStyle(.white) | |
| 74 | 78 | .padding(.horizontal, size * 0.28) |
| 75 | 79 | .padding(.vertical, size * 0.08) |
| 76 | − .background(KATheme.inkLight, in: RoundedRectangle(cornerRadius: size * 0.26, style: .continuous)) | |
| 77 | − .rotationEffect(.degrees(-2)) | |
| 80 | + .background(universe.accent, in: RoundedRectangle(cornerRadius: size * 0.26, style: .continuous)) | |
| 78 | 81 | } |
| 79 | 82 | } |
| 80 | 83 | .foregroundStyle(KATheme.ink(scheme)) |
@@ -82,14 +85,38 @@ struct KAWordmark: View { | ||
| 82 | 85 | } |
| 83 | 86 | } |
| 84 | 87 | |
| 88 | +// MARK: - Logo officiel (tuile de marque ka-ui, PNG 512 aux coins déjà arrondis) | |
| 89 | + | |
| 90 | +struct KALogo: View { | |
| 91 | + let assetID: String // « lou-ka » … « groupe-ka » | |
| 92 | + var size: CGFloat = 40 | |
| 93 | + | |
| 94 | + init(universe: Universe, size: CGFloat = 40) { | |
| 95 | + self.assetID = universe.id | |
| 96 | + self.size = size | |
| 97 | + } | |
| 98 | + init(assetID: String, size: CGFloat = 40) { | |
| 99 | + self.assetID = assetID | |
| 100 | + self.size = size | |
| 101 | + } | |
| 102 | + | |
| 103 | + 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 | +} | |
| 111 | + | |
| 85 | 112 | struct GroupeKAMark: View { |
| 86 | 113 | var size: CGFloat = 24 |
| 87 | 114 | @Environment(\.colorScheme) private var scheme |
| 88 | 115 | var body: some View { |
| 89 | 116 | HStack(alignment: .firstTextBaseline, spacing: 4) { |
| 90 | − Text("Groupe").font(.system(size: size, weight: .bold, design: .rounded)) | |
| 117 | + Text("Groupe").font(KAFont.display(size)) | |
| 91 | 118 | Text("KA") |
| 92 | − .font(.system(size: size * 0.9, weight: .bold, design: .rounded)) | |
| 119 | + .font(KAFont.display(size * 0.9)) | |
| 93 | 120 | .foregroundStyle(KATheme.lime) |
| 94 | 121 | .padding(.horizontal, size * 0.3).padding(.vertical, size * 0.1) |
| 95 | 122 | .background(KATheme.inkLight, in: RoundedRectangle(cornerRadius: size * 0.28, style: .continuous)) |
@@ -109,7 +136,7 @@ struct KAChip: View { | ||
| 109 | 136 | @Environment(\.colorScheme) private var scheme |
| 110 | 137 | var body: some View { |
| 111 | 138 | Text(text) |
| 112 | − .font(.system(.caption2, design: .monospaced).weight(.bold)) | |
| 139 | + .font(KAFont.mono(10)) | |
| 113 | 140 | .textCase(.uppercase) |
| 114 | 141 | .padding(.horizontal, 9).padding(.vertical, 4) |
| 115 | 142 | .background((accent ?? KATheme.ink(scheme)).opacity(0.12), in: Capsule()) |
modified
KA/Features/HomeView.swift
+3 −2
@@ -64,7 +64,7 @@ struct HomeView: View { | ||
| 64 | 64 | |
| 65 | 65 | private var header: some View { |
| 66 | 66 | VStack(alignment: .leading, spacing: 5) { |
| 67 | − Text(greeting.title).font(.system(.largeTitle, design: .rounded).weight(.bold)) | |
| 67 | + Text(greeting.title).font(KAFont.display(32)) | |
| 68 | 68 | Text(greeting.sub).font(.subheadline).foregroundStyle(KATheme.ink2(scheme)) |
| 69 | 69 | } |
| 70 | 70 | } |
@@ -96,7 +96,8 @@ struct HomeView: View { | ||
| 96 | 96 | |
| 97 | 97 | private func featuredSection(_ u: Universe, _ items: [KAItem]) -> some View { |
| 98 | 98 | VStack(alignment: .leading, spacing: 10) { |
| 99 | − HStack { | |
| 99 | + HStack(spacing: 8) { | |
| 100 | + KALogo(universe: u, size: 24) | |
| 100 | 101 | KAWordmark(universe: u, size: 17) |
| 101 | 102 | Spacer() |
| 102 | 103 | NavigationLink(value: u.id) { |
modified
KA/Features/ProfileView.swift
+11 −3
@@ -10,6 +10,7 @@ struct ProfileView: View { | ||
| 10 | 10 | @StateObject private var kaid = KAIDManager.shared |
| 11 | 11 | @EnvironmentObject private var favorites: FavoritesStore |
| 12 | 12 | @EnvironmentObject private var recents: RecentsStore |
| 13 | + @State private var showHubSite = false | |
| 13 | 14 | |
| 14 | 15 | private var favIDs: Set<String> { |
| 15 | 16 | Set(favUniversesRaw.split(separator: ",").map(String.init)) |
@@ -88,7 +89,7 @@ struct ProfileView: View { | ||
| 88 | 89 | Haptics.tap() |
| 89 | 90 | } label: { |
| 90 | 91 | HStack { |
| 91 | − Image(systemName: u.symbol).foregroundStyle(u.accent).frame(width: 26) | |
| 92 | + KALogo(universe: u, size: 26) | |
| 92 | 93 | Text(u.name).foregroundStyle(.primary) |
| 93 | 94 | Spacer() |
| 94 | 95 | if favIDs.contains(u.id) { |
@@ -120,8 +121,11 @@ struct ProfileView: View { | ||
| 120 | 121 | } |
| 121 | 122 | } |
| 122 | 123 | } |
| 123 | − Link(destination: Ecosystem.hubURL) { | |
| 124 | − Label("groupe-ka.com — le portail", systemImage: "globe") | |
| 124 | + Button { showHubSite = true } label: { | |
| 125 | + HStack(spacing: 10) { | |
| 126 | + KALogo(assetID: "groupe-ka", size: 22) | |
| 127 | + Text("groupe-ka.com — le portail") | |
| 128 | + } | |
| 125 | 129 | } |
| 126 | 130 | Link(destination: Ecosystem.statusURL) { |
| 127 | 131 | Label("État des services en direct", systemImage: "waveform.path.ecg") |
@@ -148,6 +152,10 @@ struct ProfileView: View { | ||
| 148 | 152 | .navigationTitle("Profil") |
| 149 | 153 | .scrollContentBackground(.hidden) |
| 150 | 154 | .background(KATheme.paper(scheme)) |
| 155 | + .fullScreenCover(isPresented: $showHubSite) { | |
| 156 | + SiteBrowserCover(title: "Groupe KA", accent: KATheme.green, | |
| 157 | + logoID: "groupe-ka", url: Ecosystem.hubURL) | |
| 158 | + } | |
| 151 | 159 | } |
| 152 | 160 | } |
| 153 | 161 | } |
added
KA/Features/SiteBrowserView.swift
+201 −0
@@ -0,0 +1,201 @@ | ||
| 1 | +// Auteur : Simon-Pierre Boucher — contact@spboucher.ai | |
| 2 | +// SiteBrowserView.swift — les sites du Groupe KA DANS l'app : navigateur | |
| 3 | +// WKWebView intégré aux couleurs de l'univers (progression, retour/avancer, | |
| 4 | +// recharger, partager, ouvrir dans Safari). Utilisé en plein écran depuis | |
| 5 | +// chaque univers et inline pour les univers sans liste native (hub). | |
| 6 | +import SwiftUI | |
| 7 | +import WebKit | |
| 8 | + | |
| 9 | +// MARK: - Modèle observable du WKWebView | |
| 10 | + | |
| 11 | +final class SiteWebModel: NSObject, ObservableObject, WKNavigationDelegate { | |
| 12 | + let webView: WKWebView | |
| 13 | + @Published var progress: Double = 0 | |
| 14 | + @Published var loading = false | |
| 15 | + @Published var canGoBack = false | |
| 16 | + @Published var canGoForward = false | |
| 17 | + @Published var currentURL: URL | |
| 18 | + | |
| 19 | + private var observers: [NSKeyValueObservation] = [] | |
| 20 | + | |
| 21 | + init(start: URL) { | |
| 22 | + currentURL = start | |
| 23 | + let config = WKWebViewConfiguration() | |
| 24 | + config.allowsInlineMediaPlayback = true | |
| 25 | + config.defaultWebpagePreferences.allowsContentJavaScript = true | |
| 26 | + webView = WKWebView(frame: .zero, configuration: config) | |
| 27 | + super.init() | |
| 28 | + webView.navigationDelegate = self | |
| 29 | + webView.allowsBackForwardNavigationGestures = true | |
| 30 | + webView.isOpaque = false | |
| 31 | + observers = [ | |
| 32 | + webView.observe(\.estimatedProgress) { [weak self] w, _ in | |
| 33 | + DispatchQueue.main.async { self?.progress = w.estimatedProgress } | |
| 34 | + }, | |
| 35 | + webView.observe(\.isLoading) { [weak self] w, _ in | |
| 36 | + DispatchQueue.main.async { self?.loading = w.isLoading } | |
| 37 | + }, | |
| 38 | + webView.observe(\.canGoBack) { [weak self] w, _ in | |
| 39 | + DispatchQueue.main.async { self?.canGoBack = w.canGoBack } | |
| 40 | + }, | |
| 41 | + webView.observe(\.canGoForward) { [weak self] w, _ in | |
| 42 | + DispatchQueue.main.async { self?.canGoForward = w.canGoForward } | |
| 43 | + }, | |
| 44 | + webView.observe(\.url) { [weak self] w, _ in | |
| 45 | + DispatchQueue.main.async { if let u = w.url { self?.currentURL = u } } | |
| 46 | + }, | |
| 47 | + ] | |
| 48 | + webView.load(URLRequest(url: start)) | |
| 49 | + } | |
| 50 | + | |
| 51 | + // http(s) reste dans la vue ; tel:/mailto:/itms… → le système | |
| 52 | + func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, | |
| 53 | + decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { | |
| 54 | + if let u = navigationAction.request.url, | |
| 55 | + let scheme = u.scheme?.lowercased(), | |
| 56 | + !["http", "https", "about", "blob", "data"].contains(scheme) { | |
| 57 | + UIApplication.shared.open(u) | |
| 58 | + decisionHandler(.cancel) | |
| 59 | + return | |
| 60 | + } | |
| 61 | + decisionHandler(.allow) | |
| 62 | + } | |
| 63 | + | |
| 64 | + // les target="_blank" s'ouvrent dans la même vue | |
| 65 | + func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, | |
| 66 | + for navigationAction: WKNavigationAction, | |
| 67 | + windowFeatures: WKWindowFeatures) -> WKWebView? { | |
| 68 | + if let u = navigationAction.request.url { webView.load(URLRequest(url: u)) } | |
| 69 | + return nil | |
| 70 | + } | |
| 71 | +} | |
| 72 | + | |
| 73 | +private struct WebViewContainer: UIViewRepresentable { | |
| 74 | + let webView: WKWebView | |
| 75 | + func makeUIView(context: Context) -> WKWebView { webView } | |
| 76 | + func updateUIView(_ uiView: WKWebView, context: Context) {} | |
| 77 | +} | |
| 78 | + | |
| 79 | +// MARK: - Écran navigateur (embarquable : web + progression + barre de contrôle) | |
| 80 | + | |
| 81 | +struct SiteBrowserScreen: View { | |
| 82 | + let accent: Color | |
| 83 | + @ObservedObject var model: SiteWebModel | |
| 84 | + @Environment(\.colorScheme) private var scheme | |
| 85 | + | |
| 86 | + var body: some View { | |
| 87 | + VStack(spacing: 0) { | |
| 88 | + WebViewContainer(webView: model.webView) | |
| 89 | + .overlay(alignment: .top) { | |
| 90 | + if model.loading { | |
| 91 | + ProgressView(value: max(model.progress, 0.06)) | |
| 92 | + .progressViewStyle(.linear) | |
| 93 | + .tint(accent) | |
| 94 | + } | |
| 95 | + } | |
| 96 | + controls | |
| 97 | + } | |
| 98 | + .background(KATheme.paper(scheme)) | |
| 99 | + } | |
| 100 | + | |
| 101 | + private var controls: some View { | |
| 102 | + HStack(spacing: 26) { | |
| 103 | + Button { Haptics.tap(); model.webView.goBack() } label: { | |
| 104 | + Image(systemName: "chevron.backward") | |
| 105 | + } | |
| 106 | + .disabled(!model.canGoBack) | |
| 107 | + .accessibilityLabel("Page précédente") | |
| 108 | + Button { Haptics.tap(); model.webView.goForward() } label: { | |
| 109 | + Image(systemName: "chevron.forward") | |
| 110 | + } | |
| 111 | + .disabled(!model.canGoForward) | |
| 112 | + .accessibilityLabel("Page suivante") | |
| 113 | + Spacer() | |
| 114 | + Text(model.currentURL.host()?.replacingOccurrences(of: "www.", with: "") ?? "") | |
| 115 | + .font(.system(.caption2, design: .monospaced).weight(.bold)) | |
| 116 | + .foregroundStyle(.secondary) | |
| 117 | + .lineLimit(1) | |
| 118 | + Spacer() | |
| 119 | + ShareLink(item: model.currentURL) { Image(systemName: "square.and.arrow.up") } | |
| 120 | + .accessibilityLabel("Partager la page") | |
| 121 | + Button { | |
| 122 | + Haptics.tap() | |
| 123 | + if model.loading { model.webView.stopLoading() } else { model.webView.reload() } | |
| 124 | + } label: { | |
| 125 | + Image(systemName: model.loading ? "xmark" : "arrow.clockwise") | |
| 126 | + } | |
| 127 | + .accessibilityLabel(model.loading ? "Arrêter" : "Recharger") | |
| 128 | + Link(destination: model.currentURL) { Image(systemName: "safari") } | |
| 129 | + .accessibilityLabel("Ouvrir dans Safari") | |
| 130 | + } | |
| 131 | + .font(.body.weight(.semibold)) | |
| 132 | + .tint(accent) | |
| 133 | + .padding(.horizontal, 18) | |
| 134 | + .padding(.vertical, 10) | |
| 135 | + .background(.bar) | |
| 136 | + } | |
| 137 | +} | |
| 138 | + | |
| 139 | +// MARK: - Site embarqué inline (univers sans liste native, ex. le hub) | |
| 140 | + | |
| 141 | +struct EmbeddedSite: View { | |
| 142 | + let universe: Universe | |
| 143 | + @StateObject private var model: SiteWebModel | |
| 144 | + | |
| 145 | + init(universe: Universe) { | |
| 146 | + self.universe = universe | |
| 147 | + _model = StateObject(wrappedValue: SiteWebModel(start: universe.baseURL)) | |
| 148 | + } | |
| 149 | + | |
| 150 | + var body: some View { | |
| 151 | + SiteBrowserScreen(accent: universe.accent, model: model) | |
| 152 | + } | |
| 153 | +} | |
| 154 | + | |
| 155 | +// MARK: - Plein écran (couverture) : titre + logo + fermer | |
| 156 | + | |
| 157 | +struct SiteBrowserCover: View { | |
| 158 | + let title: String | |
| 159 | + let accent: Color | |
| 160 | + let logoID: String | |
| 161 | + @StateObject private var model: SiteWebModel | |
| 162 | + @Environment(\.dismiss) private var dismiss | |
| 163 | + | |
| 164 | + init(universe: Universe, path: String? = nil) { | |
| 165 | + title = universe.name | |
| 166 | + accent = universe.accent | |
| 167 | + logoID = universe.id | |
| 168 | + let url = path.map { universe.baseURL.appendingPathComponent($0) } ?? universe.baseURL | |
| 169 | + _model = StateObject(wrappedValue: SiteWebModel(start: url)) | |
| 170 | + } | |
| 171 | + | |
| 172 | + init(title: String, accent: Color, logoID: String, url: URL) { | |
| 173 | + self.title = title | |
| 174 | + self.accent = accent | |
| 175 | + self.logoID = logoID | |
| 176 | + _model = StateObject(wrappedValue: SiteWebModel(start: url)) | |
| 177 | + } | |
| 178 | + | |
| 179 | + var body: some View { | |
| 180 | + NavigationStack { | |
| 181 | + SiteBrowserScreen(accent: accent, model: model) | |
| 182 | + .toolbar { | |
| 183 | + ToolbarItem(placement: .topBarLeading) { | |
| 184 | + Button { dismiss() } label: { | |
| 185 | + Image(systemName: "xmark") | |
| 186 | + } | |
| 187 | + .accessibilityLabel("Fermer le site") | |
| 188 | + } | |
| 189 | + ToolbarItem(placement: .principal) { | |
| 190 | + HStack(spacing: 8) { | |
| 191 | + KALogo(assetID: logoID, size: 24) | |
| 192 | + Text(title).font(.headline) | |
| 193 | + } | |
| 194 | + } | |
| 195 | + } | |
| 196 | + .navigationBarTitleDisplayMode(.inline) | |
| 197 | + .toolbarBackground(.visible, for: .navigationBar) | |
| 198 | + } | |
| 199 | + .tint(accent) | |
| 200 | + } | |
| 201 | +} | |
modified
KA/Features/UniversesView.swift
+119 −37
@@ -102,11 +102,7 @@ struct UniverseCard: View { | ||
| 102 | 102 | var body: some View { |
| 103 | 103 | VStack(alignment: .leading, spacing: 10) { |
| 104 | 104 | HStack { |
| 105 | − Image(systemName: universe.symbol) | |
| 106 | − .font(.title3.weight(.semibold)) | |
| 107 | − .foregroundStyle(universe.accent) | |
| 108 | − .frame(width: 40, height: 40) | |
| 109 | − .background(universe.accent.opacity(0.14), in: RoundedRectangle(cornerRadius: 11, style: .continuous)) | |
| 105 | + KALogo(universe: universe, size: 40) | |
| 110 | 106 | Spacer() |
| 111 | 107 | Image(systemName: "chevron.right").font(.caption).foregroundStyle(.tertiary) |
| 112 | 108 | } |
@@ -116,11 +112,11 @@ struct UniverseCard: View { | ||
| 116 | 112 | .lineLimit(2, reservesSpace: true) |
| 117 | 113 | if let total { |
| 118 | 114 | Text("\(total.formatted(.number.locale(Locale(identifier: "fr_CA")))) \(universe.unit)") |
| 119 | − .font(.system(.caption2, design: .monospaced).weight(.bold)) | |
| 115 | + .font(KAFont.mono(10)) | |
| 120 | 116 | .foregroundStyle(universe.accent) |
| 121 | 117 | .contentTransition(.numericText()) |
| 122 | 118 | } else { |
| 123 | − Text("—").font(.system(.caption2, design: .monospaced)) | |
| 119 | + Text("—").font(KAFont.mono(10, bold: false)) | |
| 124 | 120 | .foregroundStyle(.tertiary) |
| 125 | 121 | } |
| 126 | 122 | } |
@@ -131,6 +127,109 @@ struct UniverseCard: View { | ||
| 131 | 127 | } |
| 132 | 128 | } |
| 133 | 129 | |
| 130 | +// MARK: - Héros de site (reproduction fidèle de la page d'accueil de l'univers) | |
| 131 | + | |
| 132 | +struct SiteHero: View { | |
| 133 | + let universe: Universe | |
| 134 | + let liveTotal: Int? | |
| 135 | + var openSite: (() -> Void)? = nil | |
| 136 | + @Environment(\.colorScheme) private var scheme | |
| 137 | + | |
| 138 | + private var copy: HeroCopy? { KAHeroes.hero(universe.id) } | |
| 139 | + | |
| 140 | + var body: some View { | |
| 141 | + VStack(alignment: .leading, spacing: 12) { | |
| 142 | + ticker | |
| 143 | + if let c = copy { | |
| 144 | + if let k = c.kicker { | |
| 145 | + HStack(spacing: 10) { | |
| 146 | + Rectangle().fill(KATheme.green).frame(width: 26, height: 2.5) | |
| 147 | + Text(k.uppercased()) | |
| 148 | + .font(KAFont.mono(10)) | |
| 149 | + .foregroundStyle(scheme == .dark ? KATheme.lime : KATheme.green) | |
| 150 | + .kerning(1.1) | |
| 151 | + .lineLimit(2) | |
| 152 | + } | |
| 153 | + } | |
| 154 | + headline(c) | |
| 155 | + Text(c.sub) | |
| 156 | + .font(.subheadline) | |
| 157 | + .foregroundStyle(KATheme.ink2(scheme)) | |
| 158 | + .lineLimit(4) | |
| 159 | + } | |
| 160 | + HStack(spacing: 10) { | |
| 161 | + statPill | |
| 162 | + Spacer() | |
| 163 | + if let openSite { | |
| 164 | + Button(action: { Haptics.tap(); openSite() }) { | |
| 165 | + Text("VOIR LE SITE ↗") | |
| 166 | + .font(KAFont.mono(10)) | |
| 167 | + .foregroundStyle(universe.accent) | |
| 168 | + .padding(.horizontal, 12).padding(.vertical, 8) | |
| 169 | + .background(KATheme.inkLight, in: RoundedRectangle(cornerRadius: 6, style: .continuous)) | |
| 170 | + } | |
| 171 | + .buttonStyle(KAPressStyle()) | |
| 172 | + .accessibilityLabel("Voir le site \(universe.name) dans l'app") | |
| 173 | + } | |
| 174 | + } | |
| 175 | + } | |
| 176 | + } | |
| 177 | + | |
| 178 | + /// Bandeau ticker encre — la signature du haut des sites | |
| 179 | + private var ticker: some View { | |
| 180 | + HStack(spacing: 8) { | |
| 181 | + KALogo(universe: universe, size: 18) | |
| 182 | + Text(universe.unit.uppercased()) | |
| 183 | + .font(KAFont.mono(9.5)) | |
| 184 | + .foregroundStyle(.white.opacity(0.9)) | |
| 185 | + Text("◆").font(KAFont.mono(8)).foregroundStyle(universe.accent) | |
| 186 | + Text("EN DIRECT") | |
| 187 | + .font(KAFont.mono(9.5)) | |
| 188 | + .foregroundStyle(universe.accent) | |
| 189 | + Text("◆").font(KAFont.mono(8)).foregroundStyle(universe.accent) | |
| 190 | + Text(universe.domain.replacingOccurrences(of: "www.", with: "").uppercased()) | |
| 191 | + .font(KAFont.mono(9.5)) | |
| 192 | + .foregroundStyle(.white.opacity(0.75)) | |
| 193 | + .lineLimit(1) | |
| 194 | + Spacer(minLength: 0) | |
| 195 | + } | |
| 196 | + .padding(.horizontal, 12).padding(.vertical, 8) | |
| 197 | + .background(KATheme.inkLight, in: RoundedRectangle(cornerRadius: 8, style: .continuous)) | |
| 198 | + } | |
| 199 | + | |
| 200 | + /// Titre display avec le segment surligné SUR l'accent (comme le h1 du site) | |
| 201 | + private func headline(_ c: HeroCopy) -> some View { | |
| 202 | + var attr = AttributedString(c.headline) | |
| 203 | + attr.font = KAFont.display(27) | |
| 204 | + attr.foregroundColor = KATheme.ink(scheme) | |
| 205 | + if let hl = c.highlight, | |
| 206 | + let range = attr.range(of: hl, options: .caseInsensitive) { | |
| 207 | + attr[range].backgroundColor = universe.accent | |
| 208 | + attr[range].foregroundColor = .white | |
| 209 | + } | |
| 210 | + return Text(attr).lineSpacing(2) | |
| 211 | + } | |
| 212 | + | |
| 213 | + private var statPill: some View { | |
| 214 | + HStack(spacing: 7) { | |
| 215 | + Circle().fill(universe.accent).frame(width: 8, height: 8) | |
| 216 | + if let n = liveTotal { | |
| 217 | + Text(n.formatted(.number.locale(Locale(identifier: "fr_CA")))) | |
| 218 | + .font(KAFont.display(15)) | |
| 219 | + .contentTransition(.numericText()) | |
| 220 | + Text(universe.unit) | |
| 221 | + .font(.caption).foregroundStyle(KATheme.ink2(scheme)) | |
| 222 | + } else { | |
| 223 | + Text("En direct").font(.caption).foregroundStyle(KATheme.ink2(scheme)) | |
| 224 | + } | |
| 225 | + } | |
| 226 | + .padding(.horizontal, 13).padding(.vertical, 8) | |
| 227 | + .background(KATheme.surface(scheme), in: Capsule()) | |
| 228 | + .overlay(Capsule().strokeBorder(KATheme.ink(scheme).opacity(scheme == .dark ? 0.35 : 0.85), lineWidth: 1.5)) | |
| 229 | + .background(Capsule().fill(scheme == .dark ? Color.clear : KATheme.inkLight).offset(x: 4, y: 4)) | |
| 230 | + } | |
| 231 | +} | |
| 232 | + | |
| 134 | 233 | // MARK: - Explorateur d'un univers |
| 135 | 234 | |
| 136 | 235 | struct UniverseHomeView: View { |
@@ -141,6 +240,7 @@ struct UniverseHomeView: View { | ||
| 141 | 240 | @State private var errorText: String? |
| 142 | 241 | @State private var filterParams: [String: String] = [:] |
| 143 | 242 | @State private var liveTotal: Int? |
| 243 | + @State private var showSite = false | |
| 144 | 244 | @Environment(\.colorScheme) private var scheme |
| 145 | 245 | |
| 146 | 246 | var body: some View { |
@@ -160,49 +260,31 @@ struct UniverseHomeView: View { | ||
| 160 | 260 | .navigationBarTitleDisplayMode(.inline) |
| 161 | 261 | .toolbar { |
| 162 | 262 | ToolbarItem(placement: .topBarTrailing) { |
| 163 | − Link(destination: universe.baseURL) { | |
| 164 | − Image(systemName: "safari").accessibilityLabel("Ouvrir le site \(universe.name)") | |
| 263 | + Button { Haptics.tap(); showSite = true } label: { | |
| 264 | + Image(systemName: "globe").accessibilityLabel("Voir le site \(universe.name) dans l'app") | |
| 165 | 265 | } |
| 166 | 266 | } |
| 167 | 267 | } |
| 268 | + .fullScreenCover(isPresented: $showSite) { | |
| 269 | + SiteBrowserCover(universe: universe) | |
| 270 | + } | |
| 168 | 271 | .tint(universe.accent) |
| 169 | 272 | } |
| 170 | 273 | |
| 274 | + /// Univers sans liste native : le site lui-même, embarqué | |
| 171 | 275 | private var webUniverse: some View { |
| 172 | − VStack(spacing: 18) { | |
| 173 | − Image(systemName: universe.symbol).font(.system(size: 52)).foregroundStyle(universe.accent) | |
| 174 | − KAWordmark(universe: universe, size: 30) | |
| 175 | − Text(universe.tagline).font(.subheadline).foregroundStyle(.secondary) | |
| 176 | − .multilineTextAlignment(.center) | |
| 177 | − Link(destination: universe.baseURL) { | |
| 178 | − Label("Ouvrir \(universe.name)", systemImage: "arrow.up.right.square") | |
| 179 | − .font(.headline).padding(.horizontal, 22).padding(.vertical, 13) | |
| 180 | − .background(universe.accent, in: Capsule()) | |
| 181 | − .foregroundStyle(.white) | |
| 182 | − } | |
| 183 | − } | |
| 184 | − .padding(30) | |
| 185 | − .frame(maxWidth: .infinity, maxHeight: .infinity) | |
| 276 | + EmbeddedSite(universe: universe) | |
| 186 | 277 | } |
| 187 | 278 | |
| 188 | − /// En-tête façon héros de site : tagline + compteur live + filtres métier | |
| 279 | + /// En-tête FIDÈLE au site de l'univers : ticker encre, kicker vert mono, | |
| 280 | + /// titre Space Grotesk avec segment surligné accent, sous-titre, pilule live. | |
| 189 | 281 | private var hero: some View { |
| 190 | − VStack(alignment: .leading, spacing: 10) { | |
| 191 | − HStack(alignment: .firstTextBaseline) { | |
| 192 | − Text(universe.tagline) | |
| 193 | − .font(.system(.subheadline, design: .rounded).weight(.bold)) | |
| 194 | − Spacer() | |
| 195 | − if let n = liveTotal { | |
| 196 | − Text("\(n.formatted(.number.locale(Locale(identifier: "fr_CA")))) \(universe.unit)") | |
| 197 | − .font(.system(.caption2, design: .monospaced).weight(.bold)) | |
| 198 | − .padding(.horizontal, 9).padding(.vertical, 4) | |
| 199 | − .background(universe.accent.opacity(0.15), in: Capsule()) | |
| 200 | − .foregroundStyle(universe.accent) | |
| 201 | − } | |
| 202 | − } | |
| 282 | + VStack(alignment: .leading, spacing: 0) { | |
| 283 | + SiteHero(universe: universe, liveTotal: liveTotal) { showSite = true } | |
| 203 | 284 | FilterBar(universe: universe, params: $filterParams) { |
| 204 | 285 | Task { await load() } |
| 205 | 286 | } |
| 287 | + .padding(.top, 12) | |
| 206 | 288 | } |
| 207 | 289 | } |
| 208 | 290 | |
modified
KA/Info.plist
+8 −0
@@ -31,6 +31,14 @@ | ||
| 31 | 31 | <string>1</string> |
| 32 | 32 | <key>ITSAppUsesNonExemptEncryption</key> |
| 33 | 33 | <false/> |
| 34 | + <key>UIAppFonts</key> | |
| 35 | + <array> | |
| 36 | + <string>SpaceGrotesk-Regular.ttf</string> | |
| 37 | + <string>SpaceGrotesk-Medium.ttf</string> | |
| 38 | + <string>SpaceGrotesk-Bold.ttf</string> | |
| 39 | + <string>JetBrainsMono-Regular.ttf</string> | |
| 40 | + <string>JetBrainsMono-Bold.ttf</string> | |
| 41 | + </array> | |
| 34 | 42 | <key>UILaunchScreen</key> |
| 35 | 43 | <dict/> |
| 36 | 44 | <key>UISupportedInterfaceOrientations</key> |
added
KA/Resources/Assets.xcassets/logo-api-ka.imageset/Contents.json
+7 −0
@@ -0,0 +1,7 @@ | ||
| 1 | +{ | |
| 2 | + "images" : [ | |
| 3 | + { "filename" : "logo-512.png", "idiom" : "universal" } | |
| 4 | + ], | |
| 5 | + "info" : { "author" : "xcode", "version" : 1 }, | |
| 6 | + "properties" : { "preserves-vector-representation" : false } | |
| 7 | +} | |
added
KA/Resources/Assets.xcassets/logo-api-ka.imageset/logo-512.png
+0 −0
Binary file not shown.
added
KA/Resources/Assets.xcassets/logo-auto-ka.imageset/Contents.json
+7 −0
@@ -0,0 +1,7 @@ | ||
| 1 | +{ | |
| 2 | + "images" : [ | |
| 3 | + { "filename" : "logo-512.png", "idiom" : "universal" } | |
| 4 | + ], | |
| 5 | + "info" : { "author" : "xcode", "version" : 1 }, | |
| 6 | + "properties" : { "preserves-vector-representation" : false } | |
| 7 | +} | |
added
KA/Resources/Assets.xcassets/logo-auto-ka.imageset/logo-512.png
+0 −0
Binary file not shown.
added
KA/Resources/Assets.xcassets/logo-crea-ka.imageset/Contents.json
+7 −0
@@ -0,0 +1,7 @@ | ||
| 1 | +{ | |
| 2 | + "images" : [ | |
| 3 | + { "filename" : "logo-512.png", "idiom" : "universal" } | |
| 4 | + ], | |
| 5 | + "info" : { "author" : "xcode", "version" : 1 }, | |
| 6 | + "properties" : { "preserves-vector-representation" : false } | |
| 7 | +} | |
added
KA/Resources/Assets.xcassets/logo-crea-ka.imageset/logo-512.png
+0 −0
Binary file not shown.
added
KA/Resources/Assets.xcassets/logo-fabri-ka.imageset/Contents.json
+7 −0
@@ -0,0 +1,7 @@ | ||
| 1 | +{ | |
| 2 | + "images" : [ | |
| 3 | + { "filename" : "logo-512.png", "idiom" : "universal" } | |
| 4 | + ], | |
| 5 | + "info" : { "author" : "xcode", "version" : 1 }, | |
| 6 | + "properties" : { "preserves-vector-representation" : false } | |
| 7 | +} | |
added
KA/Resources/Assets.xcassets/logo-fabri-ka.imageset/logo-512.png
+0 −0
Binary file not shown.
added
KA/Resources/Assets.xcassets/logo-food-ka.imageset/Contents.json
+7 −0
@@ -0,0 +1,7 @@ | ||
| 1 | +{ | |
| 2 | + "images" : [ | |
| 3 | + { "filename" : "logo-512.png", "idiom" : "universal" } | |
| 4 | + ], | |
| 5 | + "info" : { "author" : "xcode", "version" : 1 }, | |
| 6 | + "properties" : { "preserves-vector-representation" : false } | |
| 7 | +} | |
added
KA/Resources/Assets.xcassets/logo-food-ka.imageset/logo-512.png
+0 −0
Binary file not shown.
added
KA/Resources/Assets.xcassets/logo-groupe-ka.imageset/Contents.json
+7 −0
@@ -0,0 +1,7 @@ | ||
| 1 | +{ | |
| 2 | + "images" : [ | |
| 3 | + { "filename" : "logo-512.png", "idiom" : "universal" } | |
| 4 | + ], | |
| 5 | + "info" : { "author" : "xcode", "version" : 1 }, | |
| 6 | + "properties" : { "preserves-vector-representation" : false } | |
| 7 | +} | |
added
KA/Resources/Assets.xcassets/logo-groupe-ka.imageset/logo-512.png
+0 −0
Binary file not shown.
added
KA/Resources/Assets.xcassets/logo-immo-ka.imageset/Contents.json
+7 −0
@@ -0,0 +1,7 @@ | ||
| 1 | +{ | |
| 2 | + "images" : [ | |
| 3 | + { "filename" : "logo-512.png", "idiom" : "universal" } | |
| 4 | + ], | |
| 5 | + "info" : { "author" : "xcode", "version" : 1 }, | |
| 6 | + "properties" : { "preserves-vector-representation" : false } | |
| 7 | +} | |
added
KA/Resources/Assets.xcassets/logo-immo-ka.imageset/logo-512.png
+0 −0
Binary file not shown.
added
KA/Resources/Assets.xcassets/logo-job-ka.imageset/Contents.json
+7 −0
@@ -0,0 +1,7 @@ | ||
| 1 | +{ | |
| 2 | + "images" : [ | |
| 3 | + { "filename" : "logo-512.png", "idiom" : "universal" } | |
| 4 | + ], | |
| 5 | + "info" : { "author" : "xcode", "version" : 1 }, | |
| 6 | + "properties" : { "preserves-vector-representation" : false } | |
| 7 | +} | |
added
KA/Resources/Assets.xcassets/logo-job-ka.imageset/logo-512.png
+0 −0
Binary file not shown.
added
KA/Resources/Assets.xcassets/logo-lou-ka.imageset/Contents.json
+7 −0
@@ -0,0 +1,7 @@ | ||
| 1 | +{ | |
| 2 | + "images" : [ | |
| 3 | + { "filename" : "logo-512.png", "idiom" : "universal" } | |
| 4 | + ], | |
| 5 | + "info" : { "author" : "xcode", "version" : 1 }, | |
| 6 | + "properties" : { "preserves-vector-representation" : false } | |
| 7 | +} | |
added
KA/Resources/Assets.xcassets/logo-lou-ka.imageset/logo-512.png
+0 −0
Binary file not shown.
added
KA/Resources/Assets.xcassets/logo-resto-ka.imageset/Contents.json
+7 −0
@@ -0,0 +1,7 @@ | ||
| 1 | +{ | |
| 2 | + "images" : [ | |
| 3 | + { "filename" : "logo-512.png", "idiom" : "universal" } | |
| 4 | + ], | |
| 5 | + "info" : { "author" : "xcode", "version" : 1 }, | |
| 6 | + "properties" : { "preserves-vector-representation" : false } | |
| 7 | +} | |
added
KA/Resources/Assets.xcassets/logo-resto-ka.imageset/logo-512.png
+0 −0
Binary file not shown.
added
KA/Resources/Assets.xcassets/logo-sorti-ka.imageset/Contents.json
+7 −0
@@ -0,0 +1,7 @@ | ||
| 1 | +{ | |
| 2 | + "images" : [ | |
| 3 | + { "filename" : "logo-512.png", "idiom" : "universal" } | |
| 4 | + ], | |
| 5 | + "info" : { "author" : "xcode", "version" : 1 }, | |
| 6 | + "properties" : { "preserves-vector-representation" : false } | |
| 7 | +} | |
added
KA/Resources/Assets.xcassets/logo-sorti-ka.imageset/logo-512.png
+0 −0
Binary file not shown.
added
KA/Resources/Assets.xcassets/logo-trouve-ka.imageset/Contents.json
+7 −0
@@ -0,0 +1,7 @@ | ||
| 1 | +{ | |
| 2 | + "images" : [ | |
| 3 | + { "filename" : "logo-512.png", "idiom" : "universal" } | |
| 4 | + ], | |
| 5 | + "info" : { "author" : "xcode", "version" : 1 }, | |
| 6 | + "properties" : { "preserves-vector-representation" : false } | |
| 7 | +} | |
added
KA/Resources/Assets.xcassets/logo-trouve-ka.imageset/logo-512.png
+0 −0
Binary file not shown.
added
KA/Resources/Assets.xcassets/logo-vrai-prix.imageset/Contents.json
+7 −0
@@ -0,0 +1,7 @@ | ||
| 1 | +{ | |
| 2 | + "images" : [ | |
| 3 | + { "filename" : "logo-512.png", "idiom" : "universal" } | |
| 4 | + ], | |
| 5 | + "info" : { "author" : "xcode", "version" : 1 }, | |
| 6 | + "properties" : { "preserves-vector-representation" : false } | |
| 7 | +} | |
added
KA/Resources/Assets.xcassets/logo-vrai-prix.imageset/logo-512.png
+0 −0
Binary file not shown.
added
KA/Resources/Fonts/JetBrainsMono-Bold.ttf
+0 −0
Binary file not shown.
added
KA/Resources/Fonts/JetBrainsMono-Regular.ttf
+0 −0
Binary file not shown.
added
KA/Resources/Fonts/SpaceGrotesk-Bold.ttf
+0 −0
Binary file not shown.
added
KA/Resources/Fonts/SpaceGrotesk-Medium.ttf
+0 −0
Binary file not shown.
added
KA/Resources/Fonts/SpaceGrotesk-Regular.ttf
+0 −0
Binary file not shown.
modified
KATests/KATests.swift
+22 −0
@@ -3,6 +3,7 @@ | ||
| 3 | 3 | // KAItem pour chaque adaptateur d'univers (échantillons réels des API), |
| 4 | 4 | // construction des URL de liste, favoris/collections. |
| 5 | 5 | import XCTest |
| 6 | +import UIKit | |
| 6 | 7 | @testable import KA |
| 7 | 8 | |
| 8 | 9 | final class AdapterTests: XCTestCase { |
@@ -153,3 +154,24 @@ final class RefonteTests: XCTestCase { | ||
| 153 | 154 | XCTAssertEqual(store.savedSearches[0].lastTotal, 112, "le total vu absorbe les nouveautés") |
| 154 | 155 | } |
| 155 | 156 | } |
| 157 | + | |
| 158 | +// MARK: - v2.0.0 : logos officiels + sites intégrés | |
| 159 | + | |
| 160 | +final class SitesLogosTests: XCTestCase { | |
| 161 | + /// Chaque univers (+ le hub) a son logo officiel dans le catalogue d'assets. | |
| 162 | + func testEveryUniverseHasItsOfficialLogo() { | |
| 163 | + for id in Ecosystem.all.map(\.id) + ["groupe-ka"] { | |
| 164 | + XCTAssertNotNil(UIImage(named: "logo-\(id)"), | |
| 165 | + "logo manquant dans Assets.xcassets : logo-\(id)") | |
| 166 | + } | |
| 167 | + } | |
| 168 | + | |
| 169 | + /// Le navigateur intégré garde http(s) et connaît son URL de départ. | |
| 170 | + @MainActor | |
| 171 | + func testSiteWebModelStartsOnUniverseSite() { | |
| 172 | + guard let u = Ecosystem.universe("lou-ka") else { return XCTFail("lou-ka absent") } | |
| 173 | + let model = SiteWebModel(start: u.baseURL) | |
| 174 | + XCTAssertEqual(model.currentURL, u.baseURL) | |
| 175 | + XCTAssertTrue(model.webView.allowsBackForwardNavigationGestures) | |
| 176 | + } | |
| 177 | +} | |
modified
project.yml
+8 −2
@@ -9,8 +9,8 @@ options: | ||
| 9 | 9 | settings: |
| 10 | 10 | base: |
| 11 | 11 | SWIFT_VERSION: "5.0" |
| 12 | − MARKETING_VERSION: "1.0.0" | |
| 13 | − CURRENT_PROJECT_VERSION: "7" | |
| 12 | + MARKETING_VERSION: "2.0.0" | |
| 13 | + CURRENT_PROJECT_VERSION: "8" | |
| 14 | 14 | DEVELOPMENT_TEAM: "3YM54G49SN" |
| 15 | 15 | CODE_SIGN_STYLE: Automatic |
| 16 | 16 | GENERATE_INFOPLIST_FILE: true |
@@ -52,6 +52,12 @@ targets: | ||
| 52 | 52 | - UIInterfaceOrientationLandscapeRight |
| 53 | 53 | UILaunchScreen: {} |
| 54 | 54 | ITSAppUsesNonExemptEncryption: false |
| 55 | + UIAppFonts: | |
| 56 | + - SpaceGrotesk-Regular.ttf | |
| 57 | + - SpaceGrotesk-Medium.ttf | |
| 58 | + - SpaceGrotesk-Bold.ttf | |
| 59 | + - JetBrainsMono-Regular.ttf | |
| 60 | + - JetBrainsMono-Bold.ttf | |
| 55 | 61 | KAWidgets: |
| 56 | 62 | type: app-extension |
| 57 | 63 | platform: iOS |
| 58 | 64 | |