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.1.0 — chaque univers ressemble à son site : polices officielles, tokens fidèles, héros de site

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Simon-Pierre Boucher committed 1 mo ago (Aug 21, 2026) parent 92dff38

14 changed files +283 −62

modified Package.swift +1 −1
@@ -11,7 +11,7 @@ let package = Package(
11 11 .executableTarget(
12 12 name: "KA",
13 13 path: "Sources/KA",
14 − resources: [.copy("Resources/Logos")],
14 + resources: [.copy("Resources/Logos"), .copy("Resources/Fonts")],
15 15 linkerSettings: [.linkedFramework("Carbon")]
16 16 )
17 17 ]
modified Sources/KA/App/KAApp.swift +5 −0
@@ -140,6 +140,11 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
140 140 @main
141 141 struct KAApp: App {
142 142 @NSApplicationDelegateAdaptor(AppDelegate.self) private var delegate
143 +
144 + init() {
145 + // Polices officielles des sites (Space Grotesk, JetBrains Mono)
146 + KAFont.register()
147 + }
143 148 @StateObject private var pulse = EcosystemPulse.shared
144 149 @StateObject private var state = AppState.shared
145 150 @StateObject private var favorites = FavoritesStore.shared
added Sources/KA/Core/HeroCopy.swift +85 −0
@@ -0,0 +1,85 @@
1 +// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
2 +// HeroCopy.swift (macOS) — 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 Sources/KA/DesignMac/KAFonts.swift +34 −0
@@ -0,0 +1,34 @@
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é macOS).
5 +// Spec : app iOS KA (~/Desktop/KA/KA/Design/KAFonts.swift).
6 +import SwiftUI
7 +import CoreText
8 +
9 +enum KAFont {
10 + /// Enregistre les TTF embarquées dans le bundle SPM — à appeler UNE fois
11 + /// au démarrage (avant le premier rendu SwiftUI).
12 + static func register() {
13 + for name in ["SpaceGrotesk-Regular", "SpaceGrotesk-Medium", "SpaceGrotesk-Bold",
14 + "JetBrainsMono-Regular", "JetBrainsMono-Bold"] {
15 + guard let url = Bundle.module.url(forResource: name, withExtension: "ttf",
16 + subdirectory: "Fonts") else { continue }
17 + CTFontManagerRegisterFontsForURL(url as CFURL, .process, nil)
18 + }
19 + }
20 +
21 + /// Display « Space Grotesk » — titres, wordmarks, chiffres héros
22 + static func display(_ size: CGFloat, _ weight: Font.Weight = .bold) -> Font {
23 + switch weight {
24 + case .bold, .heavy, .black: return .custom("SpaceGrotesk-Bold", size: size)
25 + case .medium, .semibold: return .custom("SpaceGrotesk-Medium", size: size)
26 + default: return .custom("SpaceGrotesk-Regular", size: size)
27 + }
28 + }
29 +
30 + /// Micro-étiquettes « JetBrains Mono » — kickers, compteurs, puces
31 + static func mono(_ size: CGFloat, bold: Bool = true) -> Font {
32 + .custom(bold ? "JetBrainsMono-Bold" : "JetBrainsMono-Regular", size: size)
33 + }
34 +}
modified Sources/KA/DesignMac/Theme.swift +29 −24
@@ -21,7 +21,8 @@ enum KATheme {
21 21 static func ink2(_ scheme: ColorScheme) -> Color { scheme == .dark ? Color(hex: "#a9b0a9") : Color(hex: "#4d5551") }
22 22 }
23 23
24 −// MARK: - Carte « éditorial sharp » (bordure encre + ombre décalée)
24 +// MARK: - Carte « éditorial sharp » — tokens EXACTS des sites (tokens.css) :
25 +// rayon 10 px, bordure encre 1,5 px, ombre décalée DURE 6×6 pleine encre.
25 26
26 27 struct KACard: ViewModifier {
27 28 @Environment(\.colorScheme) private var scheme
@@ -30,17 +31,19 @@ struct KACard: ViewModifier {
30 31 func body(content: Content) -> some View {
31 32 content
32 33 .background(KATheme.surface(scheme))
33 − .clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
34 + .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous))
34 35 .overlay(
35 − RoundedRectangle(cornerRadius: 12, style: .continuous)
36 − .strokeBorder(KATheme.ink(scheme).opacity(scheme == .dark ? 0.35 : 0.85), lineWidth: 1.3)
36 + RoundedRectangle(cornerRadius: 10, style: .continuous)
37 + .strokeBorder(KATheme.ink(scheme).opacity(scheme == .dark ? 0.35 : 0.85), lineWidth: 1.5)
37 38 )
38 39 .background(
39 − RoundedRectangle(cornerRadius: 12, style: .continuous)
40 − .fill((accent ?? KATheme.ink(scheme)).opacity(scheme == .dark ? 0.25 : 0.16))
41 − .offset(x: 4, y: 4)
40 + RoundedRectangle(cornerRadius: 10, style: .continuous)
41 + .fill(scheme == .dark
42 + ? (accent ?? KATheme.inkDark).opacity(0.25)
43 + : KATheme.inkLight)
44 + .offset(x: 6, y: 6)
42 45 )
43 − .padding(.trailing, 4).padding(.bottom, 4)
46 + .padding(.trailing, 6).padding(.bottom, 6)
44 47 }
45 48 }
46 49
@@ -56,18 +59,18 @@ struct KAWordmark: View {
56 59 @Environment(\.colorScheme) private var scheme
57 60
58 61 var body: some View {
62 + // fidèle à l'en-tête des sites : « Lou- » encre + boîte ACCENT au texte blanc
59 63 let parts = universe.wordmark.split(separator: "·", maxSplits: 1)
60 64 HStack(alignment: .firstTextBaseline, spacing: 3) {
61 − Text(parts.first.map(String.init) ?? universe.wordmark)
62 − .font(.system(size: size, weight: .bold, design: .rounded))
65 + Text(parts.count > 1 ? String(parts[0]) + "-" : universe.wordmark)
66 + .font(KAFont.display(size))
63 67 if parts.count > 1 {
64 68 Text(String(parts[1]))
65 − .font(.system(size: size * 0.86, weight: .bold, design: .rounded))
66 − .foregroundStyle(universe.accent)
69 + .font(KAFont.display(size * 0.86))
70 + .foregroundStyle(.white)
67 71 .padding(.horizontal, size * 0.28)
68 72 .padding(.vertical, size * 0.08)
69 − .background(KATheme.inkLight, in: RoundedRectangle(cornerRadius: size * 0.26, style: .continuous))
70 − .rotationEffect(.degrees(-2))
73 + .background(universe.accent, in: RoundedRectangle(cornerRadius: size * 0.26, style: .continuous))
71 74 }
72 75 }
73 76 .foregroundStyle(KATheme.ink(scheme))
@@ -81,9 +84,9 @@ struct GroupeKAMark: View {
81 84 @Environment(\.colorScheme) private var scheme
82 85 var body: some View {
83 86 HStack(alignment: .firstTextBaseline, spacing: 4) {
84 − Text("Groupe").font(.system(size: size, weight: .bold, design: .rounded))
87 + Text("Groupe").font(KAFont.display(size))
85 88 Text("KA")
86 − .font(.system(size: size * 0.9, weight: .bold, design: .rounded))
89 + .font(KAFont.display(size * 0.9))
87 90 .foregroundStyle(KATheme.lime)
88 91 .padding(.horizontal, size * 0.3).padding(.vertical, size * 0.1)
89 92 .background(KATheme.inkLight, in: RoundedRectangle(cornerRadius: size * 0.28, style: .continuous))
@@ -144,7 +147,7 @@ struct KAChip: View {
144 147 @Environment(\.colorScheme) private var scheme
145 148 var body: some View {
146 149 Text(text)
147 − .font(.system(.caption2, design: .monospaced).weight(.bold))
150 + .font(KAFont.mono(10))
148 151 .textCase(.uppercase)
149 152 .padding(.horizontal, 9).padding(.vertical, 4)
150 153 .background((accent ?? KATheme.ink(scheme)).opacity(0.12), in: Capsule())
@@ -280,19 +283,21 @@ struct KAItemCard: View {
280 283 .padding(10)
281 284 }
282 285 .background(KATheme.surface(scheme))
283 − .clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
286 + .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous))
284 287 .overlay(
285 − RoundedRectangle(cornerRadius: 12, style: .continuous)
288 + RoundedRectangle(cornerRadius: 10, style: .continuous)
286 289 .strokeBorder(selected ? (universe?.accent ?? KATheme.ink(scheme))
287 290 : KATheme.ink(scheme).opacity(scheme == .dark ? 0.35 : 0.85),
288 − lineWidth: selected ? 2.6 : 1.3)
291 + lineWidth: selected ? 2.6 : 1.5)
289 292 )
290 293 .background(
291 − RoundedRectangle(cornerRadius: 12, style: .continuous)
292 − .fill((universe?.accent ?? KATheme.ink(scheme)).opacity(scheme == .dark ? 0.25 : 0.16))
293 − .offset(x: 4, y: 4)
294 + RoundedRectangle(cornerRadius: 10, style: .continuous)
295 + .fill(scheme == .dark
296 + ? (universe?.accent ?? KATheme.inkDark).opacity(0.25)
297 + : KATheme.inkLight)
298 + .offset(x: 6, y: 6)
294 299 )
295 − .padding(.trailing, 4).padding(.bottom, 4)
300 + .padding(.trailing, 6).padding(.bottom, 6)
296 301 }
297 302 }
298 303
modified Sources/KA/Features/HomeView.swift +5 −5
@@ -41,18 +41,18 @@ struct HomeView: View {
41 41 VStack(alignment: .leading, spacing: 8) {
42 42 GroupeKAMark(size: 30)
43 43 Text("Le Québec entier, lu à la source.")
44 − .font(.system(.title2, design: .rounded).weight(.bold))
44 + .font(KAFont.display(22))
45 45 HStack(spacing: 8) {
46 46 if totalAll > 0 {
47 47 Text("\(totalAll.fr) éléments en direct")
48 − .font(.system(.caption, design: .monospaced).weight(.bold))
48 + .font(KAFont.mono(11))
49 49 .padding(.horizontal, 10).padding(.vertical, 5)
50 50 .background(KATheme.green.opacity(0.12), in: Capsule())
51 51 .foregroundStyle(KATheme.green)
52 52 .contentTransition(.numericText())
53 53 }
54 54 Text("\(pulse.servicesUp)/\(pulse.health.count) services en ligne")
55 − .font(.system(.caption, design: .monospaced).weight(.bold))
55 + .font(KAFont.mono(11))
56 56 .padding(.horizontal, 10).padding(.vertical, 5)
57 57 .background((pulse.servicesUp == pulse.health.count ? KATheme.green : .orange).opacity(0.12), in: Capsule())
58 58 .foregroundStyle(pulse.servicesUp == pulse.health.count ? KATheme.green : .orange)
@@ -150,11 +150,11 @@ struct HomeView: View {
150 150 .multilineTextAlignment(.leading)
151 151 if let total = pulse.totals[u.id] {
152 152 Text("\(total.fr) \(u.unit)")
153 − .font(.system(.caption2, design: .monospaced).weight(.bold))
153 + .font(KAFont.mono(10))
154 154 .foregroundStyle(u.accent)
155 155 .contentTransition(.numericText())
156 156 } else {
157 − Text("—").font(.system(.caption2, design: .monospaced))
157 + Text("—").font(KAFont.mono(10, bold: false))
158 158 .foregroundStyle(.tertiary)
159 159 }
160 160 }
modified Sources/KA/Features/UniverseExplorerView.swift +121 −29
@@ -100,24 +100,9 @@ struct UniverseExplorerView: View {
100 100 }
101 101
102 102 private var hero: some View {
103 − VStack(alignment: .leading, spacing: 10) {
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 }
108 − KAWordmark(universe: universe, size: 24)
109 − Text(universe.tagline)
110 − .font(.system(.subheadline, design: .rounded).weight(.bold))
111 − .foregroundStyle(KATheme.ink2(scheme))
112 − Spacer()
113 − if let n = pulse.totals[universe.id] {
114 − Text("\(n.fr) \(universe.unit)")
115 − .font(.system(.caption, design: .monospaced).weight(.bold))
116 − .padding(.horizontal, 9).padding(.vertical, 4)
117 − .background(universe.accent.opacity(0.15), in: Capsule())
118 − .foregroundStyle(universe.accent)
119 − .contentTransition(.numericText())
120 − }
103 + VStack(alignment: .leading, spacing: 12) {
104 + SiteHero(universe: universe, liveTotal: pulse.totals[universe.id]) { mode = .site }
105 + HStack(spacing: 8) {
121 106 Picker("Présentation", selection: $asGrid) {
122 107 Image(systemName: "square.grid.2x2").tag(true)
123 108 Image(systemName: "list.bullet").tag(false)
@@ -125,15 +110,6 @@ struct UniverseExplorerView: View {
125 110 .pickerStyle(.segmented)
126 111 .labelsHidden()
127 112 .frame(width: 90)
128 − Button { mode = .site } label: {
129 − Label("Voir le site", systemImage: "safari")
130 − .font(.caption.weight(.semibold))
131 − }
132 − .buttonStyle(.plain)
133 − .foregroundStyle(universe.accent)
134 − .help("Ouvrir \(universe.domain) dans l'app")
135 − }
136 − HStack(spacing: 8) {
137 113 KASearchField(prompt: "Chercher dans \(universe.name)…", text: $query) {
138 114 Task { await load() }
139 115 }
@@ -194,10 +170,10 @@ struct UniverseExplorerView: View {
194 170 Button { selected = item } label: {
195 171 KAItemRow(item: item)
196 172 .overlay(
197 − RoundedRectangle(cornerRadius: 12, style: .continuous)
173 + RoundedRectangle(cornerRadius: 10, style: .continuous)
198 174 .strokeBorder(universe.accent,
199 175 lineWidth: selected?.id == item.id ? 2.4 : 0)
200 − .padding(.trailing, 4).padding(.bottom, 4)
176 + .padding(.trailing, 6).padding(.bottom, 6)
201 177 )
202 178 }
203 179 .buttonStyle(KAPressStyle())
@@ -242,6 +218,122 @@ struct UniverseExplorerView: View {
242 218 }
243 219 }
244 220
221 +// MARK: - Héros de site (reproduction fidèle de la page d'accueil de l'univers)
222 +// Spec : app iOS KA (UniversesView.swift, struct SiteHero) — adapté desktop.
223 +
224 +struct SiteHero: View {
225 + let universe: Universe
226 + let liveTotal: Int?
227 + var openSite: (() -> Void)? = nil
228 + @Environment(\.colorScheme) private var scheme
229 +
230 + private var copy: HeroCopy? { KAHeroes.hero(universe.id) }
231 +
232 + var body: some View {
233 + VStack(alignment: .leading, spacing: 10) {
234 + ticker
235 + if let c = copy {
236 + if let k = c.kicker {
237 + HStack(spacing: 10) {
238 + Rectangle().fill(KATheme.green).frame(width: 26, height: 2.5)
239 + Text(k.uppercased())
240 + .font(KAFont.mono(10))
241 + .foregroundStyle(scheme == .dark ? KATheme.lime : KATheme.green)
242 + .kerning(1.1)
243 + .lineLimit(1)
244 + }
245 + }
246 + HStack(alignment: .top, spacing: 18) {
247 + VStack(alignment: .leading, spacing: 7) {
248 + headline(c)
249 + Text(c.sub)
250 + .font(.subheadline)
251 + .foregroundStyle(KATheme.ink2(scheme))
252 + .lineLimit(3)
253 + }
254 + Spacer(minLength: 0)
255 + VStack(alignment: .trailing, spacing: 9) {
256 + statPill
257 + if let openSite {
258 + Button(action: openSite) {
259 + Text("VOIR LE SITE ↗")
260 + .font(KAFont.mono(10))
261 + .foregroundStyle(universe.accent)
262 + .padding(.horizontal, 12).padding(.vertical, 8)
263 + .background(KATheme.inkLight, in: RoundedRectangle(cornerRadius: 6, style: .continuous))
264 + }
265 + .buttonStyle(KAPressStyle())
266 + .help("Ouvrir \(universe.domain) dans l'app")
267 + .accessibilityLabel("Voir le site \(universe.name) dans l'app")
268 + }
269 + }
270 + }
271 + }
272 + }
273 + }
274 +
275 + /// Bandeau ticker encre — la signature du haut des sites
276 + private var ticker: some View {
277 + HStack(spacing: 9) {
278 + KALogo(id: universe.id, size: 18,
279 + fallbackSymbol: universe.symbol, fallbackAccent: universe.accent)
280 + Text(universe.unit.uppercased())
281 + .font(KAFont.mono(9.5))
282 + .foregroundStyle(.white.opacity(0.9))
283 + Text("◆").font(KAFont.mono(8)).foregroundStyle(universe.accent)
284 + Text("EN DIRECT")
285 + .font(KAFont.mono(9.5))
286 + .foregroundStyle(universe.accent)
287 + Text("◆").font(KAFont.mono(8)).foregroundStyle(universe.accent)
288 + Text(universe.domain.replacingOccurrences(of: "www.", with: "").uppercased())
289 + .font(KAFont.mono(9.5))
290 + .foregroundStyle(.white.opacity(0.75))
291 + .lineLimit(1)
292 + Text("◆").font(KAFont.mono(8)).foregroundStyle(universe.accent)
293 + Text(universe.tagline.uppercased())
294 + .font(KAFont.mono(9.5))
295 + .foregroundStyle(.white.opacity(0.6))
296 + .lineLimit(1)
297 + Spacer(minLength: 0)
298 + }
299 + .padding(.horizontal, 12).padding(.vertical, 8)
300 + .background(KATheme.inkLight, in: RoundedRectangle(cornerRadius: 8, style: .continuous))
301 + }
302 +
303 + /// Titre display avec le segment surligné SUR l'accent (comme le h1 du site)
304 + private func headline(_ c: HeroCopy) -> some View {
305 + var attr = AttributedString(c.headline)
306 + attr.font = KAFont.display(24)
307 + attr.foregroundColor = KATheme.ink(scheme)
308 + if let hl = c.highlight,
309 + let range = attr.range(of: hl, options: .caseInsensitive) {
310 + attr[range].backgroundColor = universe.accent
311 + attr[range].foregroundColor = .white
312 + }
313 + return Text(attr).lineSpacing(2)
314 + }
315 +
316 + private var statPill: some View {
317 + HStack(spacing: 7) {
318 + Circle().fill(universe.accent).frame(width: 8, height: 8)
319 + if let n = liveTotal {
320 + Text(n.fr)
321 + .font(KAFont.display(14))
322 + .contentTransition(.numericText())
323 + Text(universe.unit)
324 + .font(.caption).foregroundStyle(KATheme.ink2(scheme))
325 + } else {
326 + Text("En direct").font(.caption).foregroundStyle(KATheme.ink2(scheme))
327 + }
328 + }
329 + .padding(.horizontal, 12).padding(.vertical, 7)
330 + .background(KATheme.surface(scheme), in: Capsule())
331 + .overlay(Capsule().strokeBorder(KATheme.ink(scheme).opacity(scheme == .dark ? 0.35 : 0.85), lineWidth: 1.5))
332 + .background(Capsule().fill(scheme == .dark ? Color.clear : KATheme.inkLight).offset(x: 4, y: 4))
333 + .padding(.trailing, 4).padding(.bottom, 4)
334 + }
335 +}
336 +
245 337 // MARK: - Fiche universelle (panneau de droite)
246 338
247 339 struct ItemDetailPane: View {
added Sources/KA/Resources/Fonts/JetBrainsMono-Bold.ttf +0 −0

Binary file not shown.

added Sources/KA/Resources/Fonts/JetBrainsMono-Regular.ttf +0 −0

Binary file not shown.

added Sources/KA/Resources/Fonts/SpaceGrotesk-Bold.ttf +0 −0

Binary file not shown.

added Sources/KA/Resources/Fonts/SpaceGrotesk-Medium.ttf +0 −0

Binary file not shown.

added Sources/KA/Resources/Fonts/SpaceGrotesk-Regular.ttf +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-2.0.0.dmg" # publié en téléchargement sur groupe-ka.com
12 +DMG_NAME="dist/KA-macos-2.1.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 +2 −2
@@ -29,8 +29,8 @@ cat > "$APP_DIR/Contents/Info.plist" <<'PLIST'
29 29 <key>CFBundleDisplayName</key><string>KA</string>
30 30 <key>CFBundleIdentifier</key><string>com.groupeka.ka.mac</string>
31 31 <key>CFBundleExecutable</key><string>KA</string>
32 − <key>CFBundleVersion</key><string>2</string>
33 − <key>CFBundleShortVersionString</key><string>2.0.0</string>
32 + <key>CFBundleVersion</key><string>3</string>
33 + <key>CFBundleShortVersionString</key><string>2.1.0</string>
34 34 <key>CFBundlePackageType</key><string>APPL</string>
35 35 <key>LSMinimumSystemVersion</key><string>14.0</string>
36 36 <key>LSApplicationCategoryType</key><string>public.app-category.lifestyle</string>
37 37