v2.0.0 — logos officiels des 13 marques + sites web intégrés (WKWebView)
- Logos ka-ui (512 px) embarqués en ressources SwiftPM (Bundle.module), helper KALogo avec repli symbole SF : sidebar, accueil, héros d'explorateur, pouls du MenuBarExtra. - SiteView : navigateur WKWebView intégré (retour/avancer, recharger, domaine courant, partager, ouvrir dans le navigateur, progression teintée accent) ; tel:/mailto: vers le système ; target=_blank gardé dans la vue. - Bascule « Données | Site » dans la barre d'outils de chaque univers ; entrée sidebar + menu Univers « groupe-ka.com » (hub intégré). - package-app.sh copie le bundle de ressources SPM ; version 2.0.0 (2). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
23 changed files +295 −43
modified
Package.swift
+1 −0
@@ -11,6 +11,7 @@ let package = Package( | |||||
| 11 | .executableTarget( | 11 | .executableTarget( | ||
| 12 | name: "KA", | 12 | name: "KA", | ||
| 13 | path: "Sources/KA", | 13 | path: "Sources/KA", | ||
| 14 | + resources: [.copy("Resources/Logos")], | ||||
| 14 | linkerSettings: [.linkedFramework("Carbon")] | 15 | linkerSettings: [.linkedFramework("Carbon")] | ||
| 15 | ) | 16 | ) | ||
| 16 | ] | 17 | ] | ||
modified
Sources/KA/App/KAApp.swift
+15 −0
@@ -12,6 +12,8 @@ enum SidebarSelection: Hashable { | |||||
| 12 | case home, search, map, agent, status | 12 | case home, search, map, agent, status | ||
| 13 | case favorites, history, alerts | 13 | case favorites, history, alerts | ||
| 14 | case universe(String) | 14 | case universe(String) | ||
| 15 | + /// Le site du hub groupe-ka.com, intégré (WKWebView). | ||||
| 16 | + case hub | ||||
| 15 | } | 17 | } | ||
| 16 | 18 | ||||
| 17 | @MainActor | 19 | @MainActor | ||
@@ -178,6 +180,8 @@ struct KAApp: App { | |||||
| 178 | ForEach(Array(Ecosystem.all.enumerated()), id: \.element.id) { i, u in | 180 | ForEach(Array(Ecosystem.all.enumerated()), id: \.element.id) { i, u in | ||
| 179 | UniverseCommand(universe: u, index: i) | 181 | UniverseCommand(universe: u, index: i) | ||
| 180 | } | 182 | } | ||
| 183 | + Divider() | ||||
| 184 | + HubCommand() | ||||
| 181 | } | 185 | } | ||
| 182 | } | 186 | } | ||
| 183 | 187 | ||||
@@ -255,6 +259,17 @@ private struct SearchCommands: View { | |||||
| 255 | } | 259 | } | ||
| 256 | } | 260 | } | ||
| 257 | 261 | ||||
| 262 | +/// Le hub groupe-ka.com, intégré dans l'app (menu Univers). | ||||
| 263 | +private struct HubCommand: View { | ||||
| 264 | + @Environment(\.openWindow) private var openWindow | ||||
| 265 | + var body: some View { | ||||
| 266 | + Button("groupe-ka.com — le hub") { | ||||
| 267 | + focusMainWindow(openWindow) | ||||
| 268 | + AppState.shared.selection = .hub | ||||
| 269 | + } | ||||
| 270 | + } | ||||
| 271 | +} | ||||
| 272 | + | ||||
| 258 | /// Menu « Univers » : ⌘1..⌘9 pour les 9 premiers. | 273 | /// Menu « Univers » : ⌘1..⌘9 pour les 9 premiers. | ||
| 259 | private struct UniverseCommand: View { | 274 | private struct UniverseCommand: View { | ||
| 260 | let universe: Universe | 275 | let universe: Universe | ||
modified
Sources/KA/DesignMac/Theme.swift
+41 −0
@@ -95,6 +95,47 @@ struct GroupeKAMark: View { | |||||
| 95 | } | 95 | } | ||
| 96 | } | 96 | } | ||
| 97 | 97 | ||||
| 98 | +// MARK: - Logo officiel d'une marque (PNG ka-ui embarqué, repli symbole SF) | ||||
| 99 | + | ||||
| 100 | +/// Vrai logo de la marque (tuile encre + « Ka » accent, rendu depuis ka-ui). | ||||
| 101 | +/// `id` = id d'univers (« lou-ka »…) ou « groupe-ka » pour le hub. | ||||
| 102 | +struct KALogo: View { | ||||
| 103 | + let id: String | ||||
| 104 | + var size: CGFloat = 20 | ||||
| 105 | + /// Repli si le PNG manque (ex. id inconnu) — symbole SF teinté accent. | ||||
| 106 | + var fallbackSymbol: String = "square.grid.2x2" | ||||
| 107 | + var fallbackAccent: Color = .gray | ||||
| 108 | + | ||||
| 109 | + private static var cache: [String: NSImage] = [:] | ||||
| 110 | + | ||||
| 111 | + static func image(_ id: String) -> NSImage? { | ||||
| 112 | + if let hit = cache[id] { return hit } | ||||
| 113 | + guard let url = Bundle.module.url(forResource: "logo-\(id)", withExtension: "png", | ||||
| 114 | + subdirectory: "Logos"), | ||||
| 115 | + let img = NSImage(contentsOf: url) else { return nil } | ||||
| 116 | + cache[id] = img | ||||
| 117 | + return img | ||||
| 118 | + } | ||||
| 119 | + | ||||
| 120 | + var body: some View { | ||||
| 121 | + if let img = Self.image(id) { | ||||
| 122 | + Image(nsImage: img) | ||||
| 123 | + .resizable() | ||||
| 124 | + .interpolation(.high) | ||||
| 125 | + .aspectRatio(contentMode: .fit) | ||||
| 126 | + .frame(width: size, height: size) | ||||
| 127 | + .clipShape(RoundedRectangle(cornerRadius: size * 0.22, style: .continuous)) | ||||
| 128 | + .accessibilityHidden(true) | ||||
| 129 | + } else { | ||||
| 130 | + Image(systemName: fallbackSymbol) | ||||
| 131 | + .font(.system(size: size * 0.6, weight: .semibold)) | ||||
| 132 | + .foregroundStyle(fallbackAccent) | ||||
| 133 | + .frame(width: size, height: size) | ||||
| 134 | + .accessibilityHidden(true) | ||||
| 135 | + } | ||||
| 136 | + } | ||||
| 137 | +} | ||||
| 138 | + | ||||
| 98 | // MARK: - Puce mono (klabel) | 139 | // MARK: - Puce mono (klabel) | ||
| 99 | 140 | ||||
| 100 | struct KAChip: View { | 141 | struct KAChip: View { | ||
modified
Sources/KA/Features/HomeView.swift
+2 −5
@@ -138,11 +138,8 @@ struct HomeView: View { | |||||
| 138 | } label: { | 138 | } label: { | ||
| 139 | VStack(alignment: .leading, spacing: 9) { | 139 | VStack(alignment: .leading, spacing: 9) { | ||
| 140 | HStack { | 140 | HStack { | ||
| 141 | − Image(systemName: u.symbol) | 141 | + KALogo(id: u.id, size: 38, | ||
| 142 | − .font(.title3.weight(.semibold)) | 142 | + fallbackSymbol: u.symbol, fallbackAccent: u.accent) | ||
| 143 | − .foregroundStyle(u.accent) | ||||
| 144 | − .frame(width: 38, height: 38) | ||||
| 145 | − .background(u.accent.opacity(0.14), in: RoundedRectangle(cornerRadius: 10, style: .continuous)) | ||||
| 146 | Spacer() | 143 | Spacer() | ||
| 147 | Image(systemName: "chevron.right").font(.caption).foregroundStyle(.tertiary) | 144 | Image(systemName: "chevron.right").font(.caption).foregroundStyle(.tertiary) | ||
| 148 | } | 145 | } | ||
modified
Sources/KA/Features/MainWindow.swift
+12 −4
@@ -43,10 +43,8 @@ struct MainWindowView: View { | |||||
| 43 | Section("Univers") { | 43 | Section("Univers") { | ||
| 44 | ForEach(Ecosystem.all) { u in | 44 | ForEach(Ecosystem.all) { u in | ||
| 45 | HStack(spacing: 8) { | 45 | HStack(spacing: 8) { | ||
| 46 | − Image(systemName: u.symbol) | 46 | + KALogo(id: u.id, size: 18, | ||
| 47 | − .font(.caption.weight(.semibold)) | 47 | + fallbackSymbol: u.symbol, fallbackAccent: u.accent) | ||
| 48 | − .foregroundStyle(u.accent) | ||||
| 49 | − .frame(width: 18) | ||||
| 50 | KAWordmark(universe: u, size: 12) | 48 | KAWordmark(universe: u, size: 12) | ||
| 51 | Spacer(minLength: 4) | 49 | Spacer(minLength: 4) | ||
| 52 | if let n = pulse.totals[u.id] { | 50 | if let n = pulse.totals[u.id] { | ||
@@ -58,6 +56,13 @@ struct MainWindowView: View { | |||||
| 58 | } | 56 | } | ||
| 59 | .tag(SidebarSelection.universe(u.id)) | 57 | .tag(SidebarSelection.universe(u.id)) | ||
| 60 | } | 58 | } | ||
| 59 | + HStack(spacing: 8) { | ||||
| 60 | + KALogo(id: "groupe-ka", size: 18) | ||||
| 61 | + Text("groupe-ka.com") | ||||
| 62 | + .font(.system(size: 12, weight: .bold, design: .rounded)) | ||||
| 63 | + } | ||||
| 64 | + .tag(SidebarSelection.hub) | ||||
| 65 | + .help("Le hub Groupe KA — le site, intégré dans l'app") | ||||
| 61 | } | 66 | } | ||
| 62 | 67 | ||||
| 63 | Section("Bibliothèque") { | 68 | Section("Bibliothèque") { | ||
@@ -113,6 +118,9 @@ struct MainWindowView: View { | |||||
| 113 | case .favorites: FavoritesView() | 118 | case .favorites: FavoritesView() | ||
| 114 | case .history: HistoryView() | 119 | case .history: HistoryView() | ||
| 115 | case .alerts: AlertsView() | 120 | case .alerts: AlertsView() | ||
| 121 | + case .hub: | ||||
| 122 | + SiteView(url: Ecosystem.hubURL, title: "Groupe KA", | ||||
| 123 | + accent: KATheme.green, logoID: "groupe-ka") | ||||
| 116 | case .universe(let id): | 124 | case .universe(let id): | ||
| 117 | if let u = Ecosystem.universe(id) { | 125 | if let u = Ecosystem.universe(id) { | ||
| 118 | UniverseExplorerView(universe: u) | 126 | UniverseExplorerView(universe: u) | ||
modified
Sources/KA/Features/MenuBarView.swift
+2 −4
@@ -73,10 +73,8 @@ struct MenuBarView: View { | |||||
| 73 | focusMainWindow(openWindow) | 73 | focusMainWindow(openWindow) | ||
| 74 | } label: { | 74 | } label: { | ||
| 75 | HStack(spacing: 6) { | 75 | HStack(spacing: 6) { | ||
| 76 | − Image(systemName: u.symbol) | 76 | + KALogo(id: u.id, size: 14, | ||
| 77 | − .font(.caption2.weight(.semibold)) | 77 | + fallbackSymbol: u.symbol, fallbackAccent: u.accent) | ||
| 78 | − .foregroundStyle(u.accent) | ||||
| 79 | − .frame(width: 14) | ||||
| 80 | KAWordmark(universe: u, size: 10) | 78 | KAWordmark(universe: u, size: 10) | ||
| 81 | Spacer(minLength: 2) | 79 | Spacer(minLength: 2) | ||
| 82 | Text(pulse.totals[u.id].map { Double($0).compact } ?? "—") | 80 | Text(pulse.totals[u.id].map { Double($0).compact } ?? "—") | ||
added
Sources/KA/Features/SiteView.swift
+184 −0
@@ -0,0 +1,184 @@ | |||||
| 1 | +// Auteur : Simon-Pierre Boucher — contact@spboucher.ai | ||||
| 2 | +// SiteView.swift — le SITE WEB d'une plateforme Ka, intégré dans l'app : | ||||
| 3 | +// WKWebView plein volet avec chrome minimal (retour/avancer, recharger, | ||||
| 4 | +// domaine courant, partager, ouvrir dans le navigateur) et barre de | ||||
| 5 | +// progression teintée à l'accent de l'univers. Les liens externes non | ||||
| 6 | +// web (tel:, mailto:…) partent vers le système. | ||||
| 7 | +import SwiftUI | ||||
| 8 | +import WebKit | ||||
| 9 | + | ||||
| 10 | +// MARK: - Modèle observable du WKWebView (progression, historique, titre) | ||||
| 11 | + | ||||
| 12 | +@MainActor | ||||
| 13 | +final class SiteWebModel: NSObject, ObservableObject, WKNavigationDelegate, WKUIDelegate { | ||||
| 14 | + let webView: WKWebView | ||||
| 15 | + @Published var progress: Double = 0 | ||||
| 16 | + @Published var isLoading = false | ||||
| 17 | + @Published var canGoBack = false | ||||
| 18 | + @Published var canGoForward = false | ||||
| 19 | + @Published var currentURL: URL? | ||||
| 20 | + | ||||
| 21 | + private var observers: [NSKeyValueObservation] = [] | ||||
| 22 | + | ||||
| 23 | + init(url: URL) { | ||||
| 24 | + let config = WKWebViewConfiguration() | ||||
| 25 | + config.defaultWebpagePreferences.allowsContentJavaScript = true | ||||
| 26 | + webView = WKWebView(frame: .zero, configuration: config) | ||||
| 27 | + webView.allowsBackForwardNavigationGestures = true | ||||
| 28 | + webView.allowsMagnification = true | ||||
| 29 | + super.init() | ||||
| 30 | + webView.navigationDelegate = self | ||||
| 31 | + webView.uiDelegate = self | ||||
| 32 | + observers = [ | ||||
| 33 | + webView.observe(\.estimatedProgress) { [weak self] wv, _ in | ||||
| 34 | + Task { @MainActor in self?.progress = wv.estimatedProgress } | ||||
| 35 | + }, | ||||
| 36 | + webView.observe(\.isLoading) { [weak self] wv, _ in | ||||
| 37 | + Task { @MainActor in self?.isLoading = wv.isLoading } | ||||
| 38 | + }, | ||||
| 39 | + webView.observe(\.canGoBack) { [weak self] wv, _ in | ||||
| 40 | + Task { @MainActor in self?.canGoBack = wv.canGoBack } | ||||
| 41 | + }, | ||||
| 42 | + webView.observe(\.canGoForward) { [weak self] wv, _ in | ||||
| 43 | + Task { @MainActor in self?.canGoForward = wv.canGoForward } | ||||
| 44 | + }, | ||||
| 45 | + webView.observe(\.url) { [weak self] wv, _ in | ||||
| 46 | + Task { @MainActor in self?.currentURL = wv.url } | ||||
| 47 | + }, | ||||
| 48 | + ] | ||||
| 49 | + currentURL = url | ||||
| 50 | + webView.load(URLRequest(url: url)) | ||||
| 51 | + } | ||||
| 52 | + | ||||
| 53 | + // Les schémas non web (tel:, mailto:, facetime:…) partent vers le système. | ||||
| 54 | + nonisolated func webView(_ webView: WKWebView, | ||||
| 55 | + decidePolicyFor navigationAction: WKNavigationAction, | ||||
| 56 | + decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { | ||||
| 57 | + guard let url = navigationAction.request.url else { return decisionHandler(.allow) } | ||||
| 58 | + if let scheme = url.scheme?.lowercased(), scheme != "http", scheme != "https", scheme != "about" { | ||||
| 59 | + NSWorkspace.shared.open(url) | ||||
| 60 | + return decisionHandler(.cancel) | ||||
| 61 | + } | ||||
| 62 | + decisionHandler(.allow) | ||||
| 63 | + } | ||||
| 64 | + | ||||
| 65 | + // target="_blank" : on reste dans la même vue plutôt que d'ouvrir une fenêtre. | ||||
| 66 | + nonisolated func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, | ||||
| 67 | + for navigationAction: WKNavigationAction, | ||||
| 68 | + windowFeatures: WKWindowFeatures) -> WKWebView? { | ||||
| 69 | + if let url = navigationAction.request.url { | ||||
| 70 | + Task { @MainActor in webView.load(URLRequest(url: url)) } | ||||
| 71 | + } | ||||
| 72 | + return nil | ||||
| 73 | + } | ||||
| 74 | +} | ||||
| 75 | + | ||||
| 76 | +private struct SiteWebViewRepresentable: NSViewRepresentable { | ||||
| 77 | + let model: SiteWebModel | ||||
| 78 | + func makeNSView(context: Context) -> WKWebView { model.webView } | ||||
| 79 | + func updateNSView(_ nsView: WKWebView, context: Context) {} | ||||
| 80 | +} | ||||
| 81 | + | ||||
| 82 | +// MARK: - Le navigateur intégré (chrome + progression + web) | ||||
| 83 | + | ||||
| 84 | +struct SiteView: View { | ||||
| 85 | + let url: URL | ||||
| 86 | + var title: String | ||||
| 87 | + var accent: Color | ||||
| 88 | + var logoID: String? | ||||
| 89 | + @StateObject private var model: SiteWebModel | ||||
| 90 | + @Environment(\.colorScheme) private var scheme | ||||
| 91 | + | ||||
| 92 | + init(url: URL, title: String, accent: Color, logoID: String? = nil) { | ||||
| 93 | + self.url = url | ||||
| 94 | + self.title = title | ||||
| 95 | + self.accent = accent | ||||
| 96 | + self.logoID = logoID | ||||
| 97 | + _model = StateObject(wrappedValue: SiteWebModel(url: url)) | ||||
| 98 | + } | ||||
| 99 | + | ||||
| 100 | + init(universe: Universe) { | ||||
| 101 | + self.init(url: universe.baseURL, title: universe.name, | ||||
| 102 | + accent: universe.accent, logoID: universe.id) | ||||
| 103 | + } | ||||
| 104 | + | ||||
| 105 | + var body: some View { | ||||
| 106 | + VStack(spacing: 0) { | ||||
| 107 | + chrome | ||||
| 108 | + ZStack(alignment: .top) { | ||||
| 109 | + SiteWebViewRepresentable(model: model) | ||||
| 110 | + if model.isLoading { | ||||
| 111 | + GeometryReader { geo in | ||||
| 112 | + Rectangle() | ||||
| 113 | + .fill(accent) | ||||
| 114 | + .frame(width: geo.size.width * max(model.progress, 0.06), height: 2.5) | ||||
| 115 | + .animation(.easeOut(duration: 0.25), value: model.progress) | ||||
| 116 | + } | ||||
| 117 | + .frame(height: 2.5) | ||||
| 118 | + .accessibilityHidden(true) | ||||
| 119 | + } | ||||
| 120 | + } | ||||
| 121 | + } | ||||
| 122 | + .background(KATheme.paper(scheme)) | ||||
| 123 | + } | ||||
| 124 | + | ||||
| 125 | + private var chrome: some View { | ||||
| 126 | + HStack(spacing: 10) { | ||||
| 127 | + if let logoID { | ||||
| 128 | + KALogo(id: logoID, size: 20, fallbackAccent: accent) | ||||
| 129 | + } | ||||
| 130 | + Button { model.webView.goBack() } label: { | ||||
| 131 | + Image(systemName: "chevron.left") | ||||
| 132 | + } | ||||
| 133 | + .buttonStyle(.plain) | ||||
| 134 | + .disabled(!model.canGoBack) | ||||
| 135 | + .help("Page précédente") | ||||
| 136 | + Button { model.webView.goForward() } label: { | ||||
| 137 | + Image(systemName: "chevron.right") | ||||
| 138 | + } | ||||
| 139 | + .buttonStyle(.plain) | ||||
| 140 | + .disabled(!model.canGoForward) | ||||
| 141 | + .help("Page suivante") | ||||
| 142 | + Button { | ||||
| 143 | + if model.isLoading { model.webView.stopLoading() } | ||||
| 144 | + else { model.webView.reload() } | ||||
| 145 | + } label: { | ||||
| 146 | + Image(systemName: model.isLoading ? "xmark" : "arrow.clockwise") | ||||
| 147 | + } | ||||
| 148 | + .buttonStyle(.plain) | ||||
| 149 | + .help(model.isLoading ? "Arrêter" : "Recharger") | ||||
| 150 | + | ||||
| 151 | + // Domaine courant, façon champ d'adresse (lecture seule) | ||||
| 152 | + HStack(spacing: 6) { | ||||
| 153 | + Image(systemName: "lock.fill").font(.caption2).foregroundStyle(.secondary) | ||||
| 154 | + Text(model.currentURL?.host() ?? url.host() ?? "") | ||||
| 155 | + .font(.system(.caption, design: .monospaced).weight(.semibold)) | ||||
| 156 | + .lineLimit(1) | ||||
| 157 | + .truncationMode(.middle) | ||||
| 158 | + } | ||||
| 159 | + .padding(.horizontal, 10).padding(.vertical, 5) | ||||
| 160 | + .frame(maxWidth: .infinity) | ||||
| 161 | + .background(KATheme.surface(scheme), in: Capsule()) | ||||
| 162 | + .overlay(Capsule().strokeBorder(KATheme.ink(scheme).opacity(0.35), lineWidth: 1)) | ||||
| 163 | + .accessibilityLabel("Adresse : \(model.currentURL?.absoluteString ?? url.absoluteString)") | ||||
| 164 | + | ||||
| 165 | + if let current = model.currentURL { | ||||
| 166 | + ShareLink(item: current, subject: Text(title)) { | ||||
| 167 | + Image(systemName: "square.and.arrow.up") | ||||
| 168 | + } | ||||
| 169 | + .buttonStyle(.plain) | ||||
| 170 | + .help("Partager la page") | ||||
| 171 | + } | ||||
| 172 | + Button { | ||||
| 173 | + NSWorkspace.shared.open(model.currentURL ?? url) | ||||
| 174 | + } label: { | ||||
| 175 | + Image(systemName: "safari") | ||||
| 176 | + } | ||||
| 177 | + .buttonStyle(.plain) | ||||
| 178 | + .help("Ouvrir dans le navigateur") | ||||
| 179 | + } | ||||
| 180 | + .font(.body) | ||||
| 181 | + .padding(.horizontal, 12).padding(.vertical, 8) | ||||
| 182 | + .background(KATheme.paper(scheme)) | ||||
| 183 | + } | ||||
| 184 | +} | ||||
modified
Sources/KA/Features/UniverseExplorerView.swift
+33 −27
@@ -22,44 +22,45 @@ struct UniverseExplorerView: View { | |||||
| 22 | @State private var errorText: String? | 22 | @State private var errorText: String? | ||
| 23 | @State private var asGrid = true | 23 | @State private var asGrid = true | ||
| 24 | @State private var savedFlash = false | 24 | @State private var savedFlash = false | ||
| 25 | + /// Bascule Données (explorateur natif) | Site (le vrai site web, intégré). | ||||
| 26 | + enum Mode: String, CaseIterable { case donnees = "Données", site = "Site" } | ||||
| 27 | + @State private var mode: Mode | ||||
| 28 | + | ||||
| 29 | + init(universe: Universe) { | ||||
| 30 | + self.universe = universe | ||||
| 31 | + // Les univers sans liste native s'ouvrent directement sur leur site. | ||||
| 32 | + let nativeless = universe.listPath == nil || universe.map == nil | ||||
| 33 | + let hasCustom = universe.id == "vrai-prix" || universe.id == "api-ka" | ||||
| 34 | + _mode = State(initialValue: nativeless && !hasCustom ? .site : .donnees) | ||||
| 35 | + } | ||||
| 25 | 36 | ||||
| 26 | var body: some View { | 37 | var body: some View { | ||
| 27 | Group { | 38 | Group { | ||
| 28 | − if universe.id == "vrai-prix" { | 39 | + if mode == .site { | ||
| 40 | + SiteView(universe: universe) | ||||
| 41 | + } else if universe.id == "vrai-prix" { | ||||
| 29 | VraiPrixExplorer(universe: universe) | 42 | VraiPrixExplorer(universe: universe) | ||
| 30 | } else if universe.id == "api-ka" { | 43 | } else if universe.id == "api-ka" { | ||
| 31 | ApiPlaygroundView(universe: universe) | 44 | ApiPlaygroundView(universe: universe) | ||
| 32 | } else if universe.listPath == nil || universe.map == nil { | 45 | } else if universe.listPath == nil || universe.map == nil { | ||
| 33 | − webUniverse | 46 | + SiteView(universe: universe) | ||
| 34 | } else { | 47 | } else { | ||
| 35 | explorer | 48 | explorer | ||
| 36 | } | 49 | } | ||
| 37 | } | 50 | } | ||
| 38 | − } | 51 | + .toolbar { | ||
| 39 | − | 52 | + ToolbarItem(placement: .principal) { | ||
| 40 | − // MARK: univers sans liste native → renvoi au site | 53 | + Picker("Présentation de \(universe.name)", selection: $mode) { | ||
| 41 | − | 54 | + ForEach(Mode.allCases, id: \.self) { m in | ||
| 42 | − private var webUniverse: some View { | 55 | + Text(m.rawValue).tag(m) | ||
| 43 | − VStack(spacing: 18) { | 56 | + } | ||
| 44 | − Image(systemName: universe.symbol).font(.system(size: 52)).foregroundStyle(universe.accent) | 57 | + } | ||
| 45 | − KAWordmark(universe: universe, size: 30) | 58 | + .pickerStyle(.segmented) | ||
| 46 | − Text(universe.tagline).font(.title3).foregroundStyle(.secondary) | 59 | + .labelsHidden() | ||
| 47 | − .multilineTextAlignment(.center) | 60 | + .frame(width: 170) | ||
| 48 | − if let n = pulse.totals[universe.id] { | 61 | + .help("Données = explorateur natif ; Site = le site web \(universe.domain), intégré") | ||
| 49 | − Text("\(n.fr) \(universe.unit)") | ||||
| 50 | − .font(.system(.subheadline, design: .monospaced).weight(.bold)) | ||||
| 51 | − .foregroundStyle(universe.accent) | ||||
| 52 | − } | ||||
| 53 | − Link(destination: universe.baseURL) { | ||||
| 54 | − Label("Ouvrir \(universe.name)", systemImage: "arrow.up.right.square") | ||||
| 55 | − .font(.headline).padding(.horizontal, 22).padding(.vertical, 12) | ||||
| 56 | − .background(universe.accent, in: Capsule()) | ||||
| 57 | − .foregroundStyle(.white) | ||||
| 58 | } | 62 | } | ||
| 59 | − .buttonStyle(.plain) | ||||
| 60 | } | 63 | } | ||
| 61 | − .padding(30) | ||||
| 62 | − .frame(maxWidth: .infinity, maxHeight: .infinity) | ||||
| 63 | } | 64 | } | ||
| 64 | 65 | ||||
| 65 | // MARK: explorateur grille/liste + fiche | 66 | // MARK: explorateur grille/liste + fiche | ||
@@ -101,6 +102,9 @@ struct UniverseExplorerView: View { | |||||
| 101 | private var hero: some View { | 102 | private var hero: some View { | ||
| 102 | VStack(alignment: .leading, spacing: 10) { | 103 | VStack(alignment: .leading, spacing: 10) { | ||
| 103 | HStack(alignment: .firstTextBaseline, spacing: 12) { | 104 | HStack(alignment: .firstTextBaseline, spacing: 12) { | ||
| 105 | + KALogo(id: universe.id, size: 26, | ||||
| 106 | + fallbackSymbol: universe.symbol, fallbackAccent: universe.accent) | ||||
| 107 | + .alignmentGuide(.firstTextBaseline) { $0[VerticalAlignment.center] + 8 } | ||||
| 104 | KAWordmark(universe: universe, size: 24) | 108 | KAWordmark(universe: universe, size: 24) | ||
| 105 | Text(universe.tagline) | 109 | Text(universe.tagline) | ||
| 106 | .font(.system(.subheadline, design: .rounded).weight(.bold)) | 110 | .font(.system(.subheadline, design: .rounded).weight(.bold)) | ||
@@ -121,11 +125,13 @@ struct UniverseExplorerView: View { | |||||
| 121 | .pickerStyle(.segmented) | 125 | .pickerStyle(.segmented) | ||
| 122 | .labelsHidden() | 126 | .labelsHidden() | ||
| 123 | .frame(width: 90) | 127 | .frame(width: 90) | ||
| 124 | − Link(destination: universe.baseURL) { | 128 | + Button { mode = .site } label: { | ||
| 125 | − Label("Ouvrir le site", systemImage: "safari") | 129 | + Label("Voir le site", systemImage: "safari") | ||
| 126 | .font(.caption.weight(.semibold)) | 130 | .font(.caption.weight(.semibold)) | ||
| 127 | } | 131 | } | ||
| 132 | + .buttonStyle(.plain) | ||||
| 128 | .foregroundStyle(universe.accent) | 133 | .foregroundStyle(universe.accent) | ||
| 134 | + .help("Ouvrir \(universe.domain) dans l'app") | ||||
| 129 | } | 135 | } | ||
| 130 | HStack(spacing: 8) { | 136 | HStack(spacing: 8) { | ||
| 131 | KASearchField(prompt: "Chercher dans \(universe.name)…", text: $query) { | 137 | KASearchField(prompt: "Chercher dans \(universe.name)…", text: $query) { | ||
added
Sources/KA/Resources/Logos/logo-api-ka.png
+0 −0
Binary file not shown.
added
Sources/KA/Resources/Logos/logo-auto-ka.png
+0 −0
Binary file not shown.
added
Sources/KA/Resources/Logos/logo-crea-ka.png
+0 −0
Binary file not shown.
added
Sources/KA/Resources/Logos/logo-fabri-ka.png
+0 −0
Binary file not shown.
added
Sources/KA/Resources/Logos/logo-food-ka.png
+0 −0
Binary file not shown.
added
Sources/KA/Resources/Logos/logo-groupe-ka.png
+0 −0
Binary file not shown.
added
Sources/KA/Resources/Logos/logo-immo-ka.png
+0 −0
Binary file not shown.
added
Sources/KA/Resources/Logos/logo-job-ka.png
+0 −0
Binary file not shown.
added
Sources/KA/Resources/Logos/logo-lou-ka.png
+0 −0
Binary file not shown.
added
Sources/KA/Resources/Logos/logo-resto-ka.png
+0 −0
Binary file not shown.
added
Sources/KA/Resources/Logos/logo-sorti-ka.png
+0 −0
Binary file not shown.
added
Sources/KA/Resources/Logos/logo-trouve-ka.png
+0 −0
Binary file not shown.
added
Sources/KA/Resources/Logos/logo-vrai-prix.png
+0 −0
Binary file not shown.
modified
scripts/notarize.sh
+1 −1
@@ -9,7 +9,7 @@ cd "$(dirname "$0")/.." | |||||
| 9 | IDENTITY="Developer ID Application: Simon-Pierre Boucher (3YM54G49SN)" | 9 | IDENTITY="Developer ID Application: Simon-Pierre Boucher (3YM54G49SN)" | ||
| 10 | KEYCHAIN_PROFILE="MacLustr-Notarize" | 10 | KEYCHAIN_PROFILE="MacLustr-Notarize" | ||
| 11 | APP_DIR="dist/KA.app" | 11 | APP_DIR="dist/KA.app" | ||
| 12 | −DMG_NAME="dist/KA-macos-1.0.0.dmg" # publié en téléchargement sur groupe-ka.com | 12 | +DMG_NAME="dist/KA-macos-2.0.0.dmg" # publié en téléchargement sur groupe-ka.com | ||
| 13 | 13 | ||||
| 14 | ./scripts/package-app.sh release | 14 | ./scripts/package-app.sh release | ||
| 15 | 15 | ||||
modified
scripts/package-app.sh
+4 −2
@@ -17,6 +17,8 @@ mkdir -p "$APP_DIR/Contents/MacOS" "$APP_DIR/Contents/Resources" | |||||
| 17 | cp "$BIN" "$APP_DIR/Contents/MacOS/KA" | 17 | cp "$BIN" "$APP_DIR/Contents/MacOS/KA" | ||
| 18 | if [ ! -f Assets/AppIcon.icns ]; then ./scripts/generate-icon.sh; fi | 18 | if [ ! -f Assets/AppIcon.icns ]; then ./scripts/generate-icon.sh; fi | ||
| 19 | cp Assets/AppIcon.icns "$APP_DIR/Contents/Resources/AppIcon.icns" | 19 | cp Assets/AppIcon.icns "$APP_DIR/Contents/Resources/AppIcon.icns" | ||
| 20 | +# Bundle de ressources SwiftPM (logos des 13 marques) — requis par Bundle.module | ||||
| 21 | +cp -R ".build/$CONFIG/KA_KA.bundle" "$APP_DIR/Contents/Resources/" | ||||
| 20 | 22 | ||||
| 21 | cat > "$APP_DIR/Contents/Info.plist" <<'PLIST' | 23 | cat > "$APP_DIR/Contents/Info.plist" <<'PLIST' | ||
| 22 | <?xml version="1.0" encoding="UTF-8"?> | 24 | <?xml version="1.0" encoding="UTF-8"?> | ||
@@ -27,8 +29,8 @@ cat > "$APP_DIR/Contents/Info.plist" <<'PLIST' | |||||
| 27 | <key>CFBundleDisplayName</key><string>KA</string> | 29 | <key>CFBundleDisplayName</key><string>KA</string> | ||
| 28 | <key>CFBundleIdentifier</key><string>com.groupeka.ka.mac</string> | 30 | <key>CFBundleIdentifier</key><string>com.groupeka.ka.mac</string> | ||
| 29 | <key>CFBundleExecutable</key><string>KA</string> | 31 | <key>CFBundleExecutable</key><string>KA</string> | ||
| 30 | − <key>CFBundleVersion</key><string>1</string> | 32 | + <key>CFBundleVersion</key><string>2</string> | ||
| 31 | − <key>CFBundleShortVersionString</key><string>1.0.0</string> | 33 | + <key>CFBundleShortVersionString</key><string>2.0.0</string> | ||
| 32 | <key>CFBundlePackageType</key><string>APPL</string> | 34 | <key>CFBundlePackageType</key><string>APPL</string> | ||
| 33 | <key>LSMinimumSystemVersion</key><string>14.0</string> | 35 | <key>LSMinimumSystemVersion</key><string>14.0</string> | ||
| 34 | <key>LSApplicationCategoryType</key><string>public.app-category.lifestyle</string> | 36 | <key>LSApplicationCategoryType</key><string>public.app-category.lifestyle</string> | ||
| 35 | 37 | ||||