SPB Git forge

spb/ka-android

Public
4commits 1branches 0releases
1.6 MBsize
maindefault branch
1 mo agolast push
Kotlin 100%

v3.0.0 (4) — chaque univers ressemble à son site : polices officielles, tokens fidèles, héros de site

Space Grotesk + JetBrains Mono embarquées (res/font, OFL), kaCard aux tokens
exacts des sites (rayon 10 dp, bordure encre 1,5 dp, ombre décalée dure 6×6
pleine encre), wordmark boîte ACCENT texte blanc comme l'en-tête des sites,
HeroCopy des 13 marques (contenu réel extrait des pages d'accueil) et
composable SiteHero (ticker encre, kicker vert mono, titre surligné accent,
pilule live, bouton VOIR LE SITE ↗ vers le site intégré) en tête de chaque
univers ; salutation de l'accueil en Space Grotesk ; versionCode 4.

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

10 changed files +258 −55

modified app/build.gradle.kts +1 −1
@@ -14,7 +14,7 @@ android {
14 14 applicationId = "com.groupeka.ka"
15 15 minSdk = 26
16 16 targetSdk = 34
17 versionCode = 3
17 + versionCode = 4
18 18 versionName = "3.0.0"
19 19 }
20 20
added app/src/main/java/com/groupeka/ka/core/HeroCopy.kt +99 −0
@@ -0,0 +1,99 @@
1 +// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
2 +// HeroCopy.kt — le VRAI contenu héros de chaque site du Groupe KA (extrait des
3 +// 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 +package com.groupeka.ka.core
7 +
8 +data class HeroCopy(
9 + val kicker: String?,
10 + val headline: String,
11 + /** Segment du titre affiché SUR l'accent (fragment de headline) */
12 + val highlight: String?,
13 + val sub: String,
14 +)
15 +
16 +object KAHeroes {
17 + fun hero(id: String): HeroCopy? = all[id]
18 +
19 + val all: Map<String, HeroCopy> = mapOf(
20 + "lou-ka" to HeroCopy(
21 + kicker = "La porte d'entrée vers votre prochain chez-vous.",
22 + headline = "Trouvez votre prochain chez-vous.",
23 + highlight = "chez-vous.",
24 + 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.",
25 + ),
26 + "immo-ka" to HeroCopy(
27 + kicker = "Agrégateur — toutes les agences du Québec",
28 + headline = "Toutes les propriétés à vendre, un seul endroit.",
29 + highlight = "un seul",
30 + 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.",
31 + ),
32 + "vrai-prix" to HeroCopy(
33 + kicker = "Québec · 2021-2026",
34 + headline = "La vraie valeur. Sans boîte noire.",
35 + highlight = "Sans boîte noire.",
36 + 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.",
37 + ),
38 + "auto-ka" to HeroCopy(
39 + kicker = "Agrégateur indépendant — direct des concessionnaires",
40 + headline = "Toutes les voitures usagées à vendre au Québec. Un seul endroit.",
41 + highlight = "Un seul endroit.",
42 + 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.",
43 + ),
44 + "fabri-ka" to HeroCopy(
45 + kicker = "Le répertoire des produits d'ici",
46 + headline = "Tous les produits québécois. Un seul endroit.",
47 + highlight = "Un seul endroit.",
48 + sub = "Fabri-Ka rassemble les catalogues des boutiques en ligne d'ici — fabriqué, conçu et vendu au Québec.",
49 + ),
50 + "food-ka" to HeroCopy(
51 + kicker = "Agrégateur — épicerie du Québec",
52 + headline = "Tous les prix d'épicerie du Québec. Un seul endroit.",
53 + highlight = "un seul",
54 + 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.",
55 + ),
56 + "resto-ka" to HeroCopy(
57 + kicker = "Agrégateur — 17 régions du Québec",
58 + headline = "Tous les restos. Tous les menus. Tous les prix.",
59 + highlight = "Tous les prix.",
60 + 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é.",
61 + ),
62 + "sorti-ka" to HeroCopy(
63 + kicker = "Agrégateur indépendant · mise à jour horaire",
64 + headline = "Toutes les sorties du Québec, un seul endroit.",
65 + highlight = "un seul endroit.",
66 + 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.",
67 + ),
68 + "crea-ka" to HeroCopy(
69 + kicker = "Annuaire des créateurs du Québec",
70 + headline = "Chaque créateur québécois. Tous ses liens.",
71 + highlight = "liens.",
72 + 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.",
73 + ),
74 + "job-ka" to HeroCopy(
75 + kicker = "Offres directement des employeurs",
76 + headline = "L'emploi au Québec, à la source.",
77 + highlight = "à la source.",
78 + 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.",
79 + ),
80 + "trouve-ka" to HeroCopy(
81 + kicker = "Le moteur de recherche du web québécois",
82 + headline = "Trouve KA",
83 + highlight = "KA",
84 + sub = "Le moteur de recherche indépendant du web québécois.",
85 + ),
86 + "api-ka" to HeroCopy(
87 + kicker = "Plateforme centrale · node M3U96b",
88 + headline = "Toutes les données KA, centralisées chaque jour.",
89 + highlight = "KA",
90 + 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.",
91 + ),
92 + "groupe-ka" to HeroCopy(
93 + kicker = "Holding d'agrégateurs automatisés · Québec",
94 + headline = "Mille sites. Un seul ·KA.",
95 + highlight = "·KA.",
96 + 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.",
97 + ),
98 + )
99 +}
modified app/src/main/java/com/groupeka/ka/ui/HomeScreen.kt +2 −1
@@ -58,6 +58,7 @@ import com.groupeka.ka.encodeNavArg
58 58 import com.groupeka.ka.ui.theme.GroupeKAMark
59 59 import com.groupeka.ka.ui.theme.KAImage
60 60 import com.groupeka.ka.ui.theme.KALogo
61 +import com.groupeka.ka.ui.theme.KADisplay
61 62 import com.groupeka.ka.ui.theme.KATheme
62 63 import com.groupeka.ka.ui.theme.KAWordmark
63 64 import com.groupeka.ka.ui.theme.LocalKADark
@@ -150,7 +151,7 @@ fun HomeScreen(nav: NavHostController) {
150 151 Column(verticalArrangement = Arrangement.spacedBy(5.dp)) {
151 152 GroupeKAMark(size = 16f)
152 153 Spacer(Modifier.height(10.dp))
153 Text(title, fontSize = 30.sp, fontWeight = FontWeight.Bold, color = KATheme.ink(dark))
154 + Text(title, fontSize = 32.sp, fontFamily = KADisplay, fontWeight = FontWeight.Bold, color = KATheme.ink(dark))
154 155 Text(sub, style = MaterialTheme.typography.bodyMedium, color = KATheme.ink2(dark))
155 156 }
156 157 }
modified app/src/main/java/com/groupeka/ka/ui/UniversesScreen.kt +123 −40
@@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.Row
15 15 import androidx.compose.foundation.layout.Spacer
16 16 import androidx.compose.foundation.layout.fillMaxSize
17 17 import androidx.compose.foundation.layout.fillMaxWidth
18 +import androidx.compose.foundation.layout.height
18 19 import androidx.compose.foundation.layout.padding
19 20 import androidx.compose.foundation.layout.size
20 21 import androidx.compose.foundation.layout.width
@@ -58,10 +59,16 @@ import androidx.compose.runtime.rememberCoroutineScope
58 59 import androidx.compose.runtime.setValue
59 60 import androidx.compose.ui.Alignment
60 61 import androidx.compose.ui.Modifier
62 +import androidx.compose.ui.draw.drawBehind
63 +import androidx.compose.ui.geometry.CornerRadius
64 +import androidx.compose.ui.geometry.Offset
61 65 import androidx.compose.ui.graphics.Color
66 +import androidx.compose.ui.graphics.drawscope.Stroke
62 67 import androidx.compose.ui.platform.LocalSoftwareKeyboardController
63 import androidx.compose.ui.text.font.FontFamily
68 +import androidx.compose.ui.text.SpanStyle
69 +import androidx.compose.ui.text.buildAnnotatedString
64 70 import androidx.compose.ui.text.font.FontWeight
71 +import androidx.compose.ui.text.withStyle
65 72 import androidx.compose.ui.text.input.ImeAction
66 73 import androidx.compose.ui.text.style.TextOverflow
67 74 import androidx.compose.ui.unit.dp
@@ -69,6 +76,7 @@ import androidx.compose.ui.unit.sp
69 76 import androidx.navigation.NavHostController
70 77 import com.groupeka.ka.core.Ecosystem
71 78 import com.groupeka.ka.core.FilterCatalog
79 +import com.groupeka.ka.core.KAHeroes
72 80 import com.groupeka.ka.core.KAFilter
73 81 import com.groupeka.ka.core.KAItem
74 82 import com.groupeka.ka.core.Universe
@@ -76,9 +84,11 @@ import com.groupeka.ka.core.UniverseService
76 84 import com.groupeka.ka.core.frFormatted
77 85 import com.groupeka.ka.core.money0
78 86 import com.groupeka.ka.encodeNavArg
87 +import com.groupeka.ka.ui.theme.KADisplay
79 88 import com.groupeka.ka.ui.theme.KAEmptyState
80 89 import com.groupeka.ka.ui.theme.KAItemRow
81 90 import com.groupeka.ka.ui.theme.KALogo
91 +import com.groupeka.ka.ui.theme.KAMono
82 92 import com.groupeka.ka.ui.theme.KASkeletonRow
83 93 import com.groupeka.ka.ui.theme.KATheme
84 94 import com.groupeka.ka.ui.theme.KAWordmark
@@ -147,13 +157,13 @@ private fun UniverseCard(universe: Universe, total: Int?, modifier: Modifier = M
147 157 if (total != null) {
148 158 Text(
149 159 "${total.frFormatted} ${universe.unit}",
150 fontFamily = FontFamily.Monospace,
160 + fontFamily = KAMono,
151 161 fontSize = 10.sp,
152 162 fontWeight = FontWeight.Bold,
153 163 color = universe.accent,
154 164 )
155 165 } else {
156 Text("—", fontFamily = FontFamily.Monospace, fontSize = 10.sp, color = KATheme.ink2(dark))
166 + Text("—", fontFamily = KAMono, fontSize = 10.sp, color = KATheme.ink2(dark))
157 167 }
158 168 }
159 169 }
@@ -249,40 +259,9 @@ private fun UniverseList(universe: Universe, nav: NavHostController) {
249 259 modifier = Modifier.fillMaxSize(),
250 260 ) {
251 261 item {
252 // barre titre : logo officiel + wordmark + site intégré
253 Row(verticalAlignment = Alignment.CenterVertically) {
254 KALogo(universe.id, size = 34.dp)
255 Spacer(Modifier.width(10.dp))
256 KAWordmark(universe, size = 22f)
257 Spacer(Modifier.weight(1f))
258 IconButton(onClick = { nav.navigate("site/${universe.id}") }) {
259 Icon(Icons.Filled.OpenInNew, "Ouvrir le site ${universe.name}", tint = universe.accent)
260 }
261 }
262 }
263 item {
264 // héros : tagline + compteur live
265 Row(verticalAlignment = Alignment.CenterVertically) {
266 Text(
267 universe.tagline,
268 style = MaterialTheme.typography.bodyMedium,
269 fontWeight = FontWeight.Bold,
270 color = KATheme.ink(dark),
271 modifier = Modifier.weight(1f),
272 )
273 if (liveTotal != null) {
274 Text(
275 "${liveTotal!!.frFormatted} ${universe.unit}",
276 fontFamily = FontFamily.Monospace,
277 fontSize = 10.sp,
278 fontWeight = FontWeight.Bold,
279 color = universe.accent,
280 modifier = Modifier
281 .background(universe.accent.copy(alpha = 0.15f), CircleShape)
282 .padding(horizontal = 9.dp, vertical = 4.dp),
283 )
284 }
285 }
262 + // héros FIDÈLE au site de l'univers : ticker encre, kicker vert mono,
263 + // titre Space Grotesk surligné accent, sous-titre, pilule live.
264 + SiteHero(universe, liveTotal) { nav.navigate("site/${universe.id}") }
286 265 }
287 266 item {
288 267 OutlinedTextField(
@@ -358,6 +337,110 @@ private fun UniverseList(universe: Universe, nav: NavHostController) {
358 337 }
359 338 }
360 339
340 +// MARK: - Héros de site (reproduction fidèle de la page d'accueil de l'univers)
341 +
342 +@Composable
343 +fun SiteHero(universe: Universe, liveTotal: Int?, onOpenSite: () -> Unit) {
344 + val dark = LocalKADark.current
345 + val copy = KAHeroes.hero(universe.id)
346 + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
347 + // bandeau ticker encre — la signature du haut des sites
348 + Row(
349 + Modifier
350 + .fillMaxWidth()
351 + .background(KATheme.inkLight, RoundedCornerShape(8.dp))
352 + .padding(horizontal = 12.dp, vertical = 8.dp),
353 + verticalAlignment = Alignment.CenterVertically,
354 + horizontalArrangement = Arrangement.spacedBy(8.dp),
355 + ) {
356 + KALogo(universe.id, size = 18.dp)
357 + Text(universe.unit.uppercase(), fontFamily = KAMono, fontWeight = FontWeight.Bold,
358 + fontSize = 9.5.sp, color = Color.White.copy(alpha = 0.9f), maxLines = 1)
359 + Text("◆", fontFamily = KAMono, fontSize = 8.sp, color = universe.accent)
360 + Text("EN DIRECT", fontFamily = KAMono, fontWeight = FontWeight.Bold,
361 + fontSize = 9.5.sp, color = universe.accent)
362 + Text("◆", fontFamily = KAMono, fontSize = 8.sp, color = universe.accent)
363 + Text(universe.domain.removePrefix("www.").uppercase(), fontFamily = KAMono,
364 + fontWeight = FontWeight.Bold, fontSize = 9.5.sp,
365 + color = Color.White.copy(alpha = 0.75f), maxLines = 1, overflow = TextOverflow.Ellipsis)
366 + }
367 + if (copy != null) {
368 + if (copy.kicker != null) {
369 + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp)) {
370 + Box(Modifier.width(26.dp).height(2.5.dp).background(KATheme.green))
371 + Text(
372 + copy.kicker.uppercase(),
373 + fontFamily = KAMono, fontWeight = FontWeight.Bold, fontSize = 10.sp,
374 + letterSpacing = 1.1.sp,
375 + color = if (dark) KATheme.lime else KATheme.green,
376 + maxLines = 2,
377 + )
378 + }
379 + }
380 + // titre display avec le segment surligné SUR l'accent (comme le h1 du site)
381 + Text(
382 + buildAnnotatedString {
383 + val h = copy.headline
384 + val hl = copy.highlight
385 + val idx = hl?.let { h.indexOf(it, ignoreCase = true) } ?: -1
386 + if (idx < 0 || hl == null) { append(h) } else {
387 + append(h.substring(0, idx))
388 + withStyle(SpanStyle(background = universe.accent, color = Color.White)) {
389 + append(h.substring(idx, idx + hl.length))
390 + }
391 + append(h.substring(idx + hl.length))
392 + }
393 + },
394 + fontFamily = KADisplay, fontWeight = FontWeight.Bold,
395 + fontSize = 27.sp, lineHeight = 33.sp,
396 + color = KATheme.ink(dark),
397 + )
398 + Text(
399 + copy.sub,
400 + style = MaterialTheme.typography.bodyMedium,
401 + color = KATheme.ink2(dark),
402 + maxLines = 4, overflow = TextOverflow.Ellipsis,
403 + )
404 + }
405 + Row(verticalAlignment = Alignment.CenterVertically) {
406 + // pilule stat à ombre dure
407 + Row(
408 + Modifier
409 + .drawBehind {
410 + val r = CornerRadius(size.height / 2, size.height / 2)
411 + if (!dark) drawRoundRect(color = KATheme.inkLight,
412 + topLeft = Offset(4.dp.toPx(), 4.dp.toPx()), size = size, cornerRadius = r)
413 + drawRoundRect(color = if (dark) KATheme.paperDark else Color.White, cornerRadius = r)
414 + drawRoundRect(color = KATheme.ink(dark).copy(alpha = if (dark) 0.35f else 0.85f),
415 + cornerRadius = r, style = Stroke(width = 1.5.dp.toPx()))
416 + }
417 + .padding(horizontal = 13.dp, vertical = 8.dp),
418 + verticalAlignment = Alignment.CenterVertically,
419 + horizontalArrangement = Arrangement.spacedBy(7.dp),
420 + ) {
421 + Box(Modifier.size(8.dp).background(universe.accent, CircleShape))
422 + if (liveTotal != null) {
423 + Text(liveTotal.frFormatted, fontFamily = KADisplay,
424 + fontWeight = FontWeight.Bold, fontSize = 15.sp, color = KATheme.ink(dark))
425 + Text(universe.unit, style = MaterialTheme.typography.labelSmall, color = KATheme.ink2(dark))
426 + } else {
427 + Text("En direct", style = MaterialTheme.typography.labelSmall, color = KATheme.ink2(dark))
428 + }
429 + }
430 + Spacer(Modifier.weight(1f))
431 + Text(
432 + "VOIR LE SITE ↗",
433 + fontFamily = KAMono, fontWeight = FontWeight.Bold, fontSize = 10.sp,
434 + color = universe.accent,
435 + modifier = Modifier
436 + .background(KATheme.inkLight, RoundedCornerShape(6.dp))
437 + .clickable { onOpenSite() }
438 + .padding(horizontal = 12.dp, vertical = 8.dp),
439 + )
440 + }
441 + }
442 +}
443 +
361 444 // MARK: - Barre + feuille de filtres métier
362 445
363 446 @Composable
@@ -392,7 +475,7 @@ fun FilterBar(
392 475 if (activeCount > 0) "Filtres · $activeCount" else "Filtres",
393 476 fontSize = 11.sp,
394 477 fontWeight = FontWeight.Bold,
395 fontFamily = FontFamily.Monospace,
478 + fontFamily = KAMono,
396 479 color = if (activeCount > 0) Color.White else KATheme.ink(dark),
397 480 )
398 481 }
@@ -406,7 +489,7 @@ fun FilterBar(
406 489 f.label,
407 490 fontSize = 11.sp,
408 491 fontWeight = FontWeight.Bold,
409 fontFamily = FontFamily.Monospace,
492 + fontFamily = KAMono,
410 493 color = if (on) Color.White else KATheme.ink(dark),
411 494 modifier = Modifier
412 495 .background(if (on) universe.accent else KATheme.surface(dark), CircleShape)
@@ -424,7 +507,7 @@ fun FilterBar(
424 507 "✕ Effacer",
425 508 fontSize = 10.sp,
426 509 fontWeight = FontWeight.Bold,
427 fontFamily = FontFamily.Monospace,
510 + fontFamily = KAMono,
428 511 color = KATheme.ink2(dark),
429 512 modifier = Modifier
430 513 .clickable { onParams(emptyMap()) }
modified app/src/main/java/com/groupeka/ka/ui/theme/Theme.kt +33 −13
@@ -64,8 +64,10 @@ import androidx.compose.ui.hapticfeedback.HapticFeedbackType
64 64 import androidx.compose.ui.layout.ContentScale
65 65 import androidx.compose.ui.platform.LocalHapticFeedback
66 66 import androidx.compose.ui.res.painterResource
67 +import androidx.compose.ui.text.font.Font
67 68 import androidx.compose.ui.text.font.FontFamily
68 69 import androidx.compose.ui.text.font.FontWeight
70 +import com.groupeka.ka.R
69 71 import androidx.compose.ui.text.style.TextOverflow
70 72 import androidx.compose.ui.unit.Dp
71 73 import androidx.compose.ui.unit.dp
@@ -137,6 +139,20 @@ fun KAAppTheme(content: @Composable () -> Unit) {
137 139 }
138 140 }
139 141
142 +// MARK: - Polices OFFICIELLES des sites (tokens.css) : display Space Grotesk,
143 +// micro-étiquettes JetBrains Mono (embarquées, OFL). Le corps reste Roboto.
144 +
145 +val KADisplay = FontFamily(
146 + Font(R.font.space_grotesk_regular, FontWeight.Normal),
147 + Font(R.font.space_grotesk_medium, FontWeight.Medium),
148 + Font(R.font.space_grotesk_bold, FontWeight.Bold),
149 +)
150 +
151 +val KAMono = FontFamily(
152 + Font(R.font.jetbrains_mono_regular, FontWeight.Normal),
153 + Font(R.font.jetbrains_mono_bold, FontWeight.Bold),
154 +)
155 +
140 156 // MARK: - Haptique
141 157
142 158 object Haptics {
@@ -147,27 +163,29 @@ object Haptics {
147 163 }
148 164 }
149 165
150 // MARK: - Carte « éditorial sharp » (bordure encre + ombre décalée dure)
166 +// MARK: - Carte « éditorial sharp » — tokens EXACTS des sites (tokens.css) :
167 +// rayon 10 dp, bordure encre 1,5 dp, ombre décalée DURE 6×6 pleine encre.
151 168
152 fun Modifier.kaCard(accent: Color? = null, radius: Dp = 14.dp): Modifier = composed {
169 +fun Modifier.kaCard(accent: Color? = null, radius: Dp = 10.dp): Modifier = composed {
153 170 val dark = LocalKADark.current
154 171 val ink = KATheme.ink(dark)
155 172 val surface = KATheme.surface(dark)
156 val shadow = (accent ?: ink).copy(alpha = if (dark) 0.25f else 0.16f)
173 + val shadow = if (dark) (accent ?: KATheme.inkDark).copy(alpha = 0.25f) else KATheme.inkLight
157 174 val border = ink.copy(alpha = if (dark) 0.35f else 0.85f)
158 175 this.drawBehind {
159 176 val r = CornerRadius(radius.toPx(), radius.toPx())
160 val off = 5.dp.toPx()
161 // ombre décalée dure
177 + val off = 6.dp.toPx()
178 + // ombre décalée dure — la signature du groupe
162 179 drawRoundRect(color = shadow, topLeft = Offset(off, off), size = size, cornerRadius = r)
163 180 // fond de carte
164 181 drawRoundRect(color = surface, cornerRadius = r)
165 182 // bordure encre
166 drawRoundRect(color = border, cornerRadius = r, style = Stroke(width = 1.4.dp.toPx()))
183 + drawRoundRect(color = border, cornerRadius = r, style = Stroke(width = 1.5.dp.toPx()))
167 184 }
168 185 }
169 186
170 // MARK: - Wordmark (« Lou » + boîte [Ka] accent, rotation -2°)
187 +// MARK: - Wordmark — fidèle à l'en-tête des sites : « Lou- » encre Space
188 +// Grotesk + boîte ACCENT au texte BLANC, sans rotation.
171 189
172 190 @Composable
173 191 fun KAWordmark(universe: Universe, size: Float = 22f) {
@@ -175,8 +193,9 @@ fun KAWordmark(universe: Universe, size: Float = 22f) {
175 193 val parts = universe.wordmark.split("·", limit = 2)
176 194 Row(verticalAlignment = Alignment.CenterVertically) {
177 195 Text(
178 text = parts.firstOrNull() ?: universe.wordmark,
196 + text = if (parts.size > 1) parts[0] + "-" else universe.wordmark,
179 197 fontSize = size.sp,
198 + fontFamily = KADisplay,
180 199 fontWeight = FontWeight.Bold,
181 200 color = KATheme.ink(dark),
182 201 )
@@ -185,11 +204,11 @@ fun KAWordmark(universe: Universe, size: Float = 22f) {
185 204 Text(
186 205 text = parts[1],
187 206 fontSize = (size * 0.86f).sp,
207 + fontFamily = KADisplay,
188 208 fontWeight = FontWeight.Bold,
189 color = universe.accent,
209 + color = Color.White,
190 210 modifier = Modifier
191 .rotate(-2f)
192 .background(KATheme.inkLight, RoundedCornerShape((size * 0.26f).dp))
211 + .background(universe.accent, RoundedCornerShape((size * 0.26f).dp))
193 212 .padding(horizontal = (size * 0.28f).dp, vertical = (size * 0.08f).dp),
194 213 )
195 214 }
@@ -200,11 +219,12 @@ fun KAWordmark(universe: Universe, size: Float = 22f) {
200 219 fun GroupeKAMark(size: Float = 24f) {
201 220 val dark = LocalKADark.current
202 221 Row(verticalAlignment = Alignment.CenterVertically) {
203 Text("Groupe", fontSize = size.sp, fontWeight = FontWeight.Bold, color = KATheme.ink(dark))
222 + Text("Groupe", fontSize = size.sp, fontFamily = KADisplay, fontWeight = FontWeight.Bold, color = KATheme.ink(dark))
204 223 Spacer(Modifier.width(4.dp))
205 224 Text(
206 225 "KA",
207 226 fontSize = (size * 0.9f).sp,
227 + fontFamily = KADisplay,
208 228 fontWeight = FontWeight.Bold,
209 229 color = KATheme.lime,
210 230 modifier = Modifier
@@ -225,7 +245,7 @@ fun KAChip(text: String, accent: Color? = null) {
225 245 text = text.uppercase(),
226 246 fontSize = 10.sp,
227 247 fontWeight = FontWeight.Bold,
228 fontFamily = FontFamily.Monospace,
248 + fontFamily = KAMono,
229 249 color = ink,
230 250 maxLines = 1,
231 251 modifier = Modifier
added app/src/main/res/font/jetbrains_mono_bold.ttf +0 −0

Binary file not shown.

added app/src/main/res/font/jetbrains_mono_regular.ttf +0 −0

Binary file not shown.

added app/src/main/res/font/space_grotesk_bold.ttf +0 −0

Binary file not shown.

added app/src/main/res/font/space_grotesk_medium.ttf +0 −0

Binary file not shown.

added app/src/main/res/font/space_grotesk_regular.ttf +0 −0

Binary file not shown.