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>
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.