SPB Git forge

spb/ka-macos

Public
3commits 1branches 0releases
6.1 MBsize
maindefault branch
1 mo agolast push
Swift 98.3% Shell 1.7%

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>
Simon-Pierre Boucher committed 1 mo ago (Aug 21, 2026) parent e2e01af

23 changed files +295 −43

modified Package.swift +1 −0
@@ -11,6 +11,7 @@ let package = Package(
11 11 .executableTarget(
12 12 name: "KA",
13 13 path: "Sources/KA",
14 + resources: [.copy("Resources/Logos")],
14 15 linkerSettings: [.linkedFramework("Carbon")]
15 16 )
16 17 ]
modified Sources/KA/App/KAApp.swift +15 −0
@@ -12,6 +12,8 @@ enum SidebarSelection: Hashable {
12 12 case home, search, map, agent, status
13 13 case favorites, history, alerts
14 14 case universe(String)
15 + /// Le site du hub groupe-ka.com, intégré (WKWebView).
16 + case hub
15 17 }
16 18
17 19 @MainActor
@@ -178,6 +180,8 @@ struct KAApp: App {
178 180 ForEach(Array(Ecosystem.all.enumerated()), id: \.element.id) { i, u in
179 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 273 /// Menu « Univers » : ⌘1..⌘9 pour les 9 premiers.
259 274 private struct UniverseCommand: View {
260 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 139 // MARK: - Puce mono (klabel)
99 140
100 141 struct KAChip: View {
modified Sources/KA/Features/HomeView.swift +2 −5
@@ -138,11 +138,8 @@ struct HomeView: View {
138 138 } label: {
139 139 VStack(alignment: .leading, spacing: 9) {
140 140 HStack {
141 − Image(systemName: u.symbol)
142 − .font(.title3.weight(.semibold))
143 − .foregroundStyle(u.accent)
144 − .frame(width: 38, height: 38)
145 − .background(u.accent.opacity(0.14), in: RoundedRectangle(cornerRadius: 10, style: .continuous))
141 + KALogo(id: u.id, size: 38,
142 + fallbackSymbol: u.symbol, fallbackAccent: u.accent)
146 143 Spacer()
147 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 43 Section("Univers") {
44 44 ForEach(Ecosystem.all) { u in
45 45 HStack(spacing: 8) {
46 − Image(systemName: u.symbol)
47 − .font(.caption.weight(.semibold))
48 − .foregroundStyle(u.accent)
49 − .frame(width: 18)
46 + KALogo(id: u.id, size: 18,
47 + fallbackSymbol: u.symbol, fallbackAccent: u.accent)
50 48 KAWordmark(universe: u, size: 12)
51 49 Spacer(minLength: 4)
52 50 if let n = pulse.totals[u.id] {
@@ -58,6 +56,13 @@ struct MainWindowView: View {
58 56 }
59 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 68 Section("Bibliothèque") {
@@ -113,6 +118,9 @@ struct MainWindowView: View {
113 118 case .favorites: FavoritesView()
114 119 case .history: HistoryView()
115 120 case .alerts: AlertsView()
121 + case .hub:
122 + SiteView(url: Ecosystem.hubURL, title: "Groupe KA",
123 + accent: KATheme.green, logoID: "groupe-ka")
116 124 case .universe(let id):
117 125 if let u = Ecosystem.universe(id) {
118 126 UniverseExplorerView(universe: u)
modified Sources/KA/Features/MenuBarView.swift +2 −4
@@ -73,10 +73,8 @@ struct MenuBarView: View {
73 73 focusMainWindow(openWindow)
74 74 } label: {
75 75 HStack(spacing: 6) {
76 − Image(systemName: u.symbol)
77 − .font(.caption2.weight(.semibold))
78 − .foregroundStyle(u.accent)
79 − .frame(width: 14)
76 + KALogo(id: u.id, size: 14,
77 + fallbackSymbol: u.symbol, fallbackAccent: u.accent)
80 78 KAWordmark(universe: u, size: 10)
81 79 Spacer(minLength: 2)
82 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 22 @State private var errorText: String?
23 23 @State private var asGrid = true
24 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 37 var body: some View {
27 38 Group {
28 − if universe.id == "vrai-prix" {
39 + if mode == .site {
40 + SiteView(universe: universe)
41 + } else if universe.id == "vrai-prix" {
29 42 VraiPrixExplorer(universe: universe)
30 43 } else if universe.id == "api-ka" {
31 44 ApiPlaygroundView(universe: universe)
32 45 } else if universe.listPath == nil || universe.map == nil {
33 − webUniverse
46 + SiteView(universe: universe)
34 47 } else {
35 48 explorer
36 49 }
37 50 }
38 − }
39 −
40 − // MARK: univers sans liste native → renvoi au site
41 −
42 − private var webUniverse: some View {
43 − VStack(spacing: 18) {
44 − Image(systemName: universe.symbol).font(.system(size: 52)).foregroundStyle(universe.accent)
45 − KAWordmark(universe: universe, size: 30)
46 − Text(universe.tagline).font(.title3).foregroundStyle(.secondary)
47 − .multilineTextAlignment(.center)
48 − if let n = pulse.totals[universe.id] {
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)
51 + .toolbar {
52 + ToolbarItem(placement: .principal) {
53 + Picker("Présentation de \(universe.name)", selection: $mode) {
54 + ForEach(Mode.allCases, id: \.self) { m in
55 + Text(m.rawValue).tag(m)
56 + }
57 + }
58 + .pickerStyle(.segmented)
59 + .labelsHidden()
60 + .frame(width: 170)
61 + .help("Données = explorateur natif ; Site = le site web \(universe.domain), intégré")
58 62 }
59 − .buttonStyle(.plain)
60 63 }
61 − .padding(30)
62 − .frame(maxWidth: .infinity, maxHeight: .infinity)
63 64 }
64 65
65 66 // MARK: explorateur grille/liste + fiche
@@ -101,6 +102,9 @@ struct UniverseExplorerView: View {
101 102 private var hero: some View {
102 103 VStack(alignment: .leading, spacing: 10) {
103 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 108 KAWordmark(universe: universe, size: 24)
105 109 Text(universe.tagline)
106 110 .font(.system(.subheadline, design: .rounded).weight(.bold))
@@ -121,11 +125,13 @@ struct UniverseExplorerView: View {
121 125 .pickerStyle(.segmented)
122 126 .labelsHidden()
123 127 .frame(width: 90)
124 − Link(destination: universe.baseURL) {
125 − Label("Ouvrir le site", systemImage: "safari")
128 + Button { mode = .site } label: {
129 + Label("Voir le site", systemImage: "safari")
126 130 .font(.caption.weight(.semibold))
127 131 }
132 + .buttonStyle(.plain)
128 133 .foregroundStyle(universe.accent)
134 + .help("Ouvrir \(universe.domain) dans l'app")
129 135 }
130 136 HStack(spacing: 8) {
131 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 9 IDENTITY="Developer ID Application: Simon-Pierre Boucher (3YM54G49SN)"
10 10 KEYCHAIN_PROFILE="MacLustr-Notarize"
11 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 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 17 cp "$BIN" "$APP_DIR/Contents/MacOS/KA"
18 18 if [ ! -f Assets/AppIcon.icns ]; then ./scripts/generate-icon.sh; fi
19 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 23 cat > "$APP_DIR/Contents/Info.plist" <<'PLIST'
22 24 <?xml version="1.0" encoding="UTF-8"?>
@@ -27,8 +29,8 @@ cat > "$APP_DIR/Contents/Info.plist" <<'PLIST'
27 29 <key>CFBundleDisplayName</key><string>KA</string>
28 30 <key>CFBundleIdentifier</key><string>com.groupeka.ka.mac</string>
29 31 <key>CFBundleExecutable</key><string>KA</string>
30 − <key>CFBundleVersion</key><string>1</string>
31 − <key>CFBundleShortVersionString</key><string>1.0.0</string>
32 + <key>CFBundleVersion</key><string>2</string>
33 + <key>CFBundleShortVersionString</key><string>2.0.0</string>
32 34 <key>CFBundlePackageType</key><string>APPL</string>
33 35 <key>LSMinimumSystemVersion</key><string>14.0</string>
34 36 <key>LSApplicationCategoryType</key><string>public.app-category.lifestyle</string>
35 37