817 lines
29 KiB
Markdown
817 lines
29 KiB
Markdown
# 🏗️ ARCHITECTURE UI-UX — SafeBite Android
|
|
|
|
**Document d'architecture pour la refonte UI/UX • Version 1.0 • 25 avril 2026**
|
|
|
|
---
|
|
|
|
## 📋 TABLE DES MATIÈRES
|
|
|
|
1. [État des lieux](#1-état-des-lieux)
|
|
2. [Principes directeurs](#2-principes-directeurs)
|
|
3. [Architecture de navigation cible](#3-architecture-de-navigation-cible)
|
|
4. [Design System](#4-design-system)
|
|
5. [Spécifications des écrans — Phase 1](#5-spécifications-des-écrans--phase-1)
|
|
6. [Composants réutilisables](#6-composants-réutilisables)
|
|
7. [Plan de migration](#7-plan-de-migration)
|
|
8. [Annexes](#8-annexes)
|
|
|
|
---
|
|
|
|
## 1. ÉTAT DES LIEUX
|
|
|
|
### 1.1 Architecture actuelle
|
|
|
|
L'application suit le pattern **MVVM + Clean Architecture** avec Jetpack Compose :
|
|
|
|
```
|
|
presentation/
|
|
├── MainActivity.kt # Point d'entrée
|
|
├── common/
|
|
│ ├── components/ # Composants UI réutilisables
|
|
│ │ ├── AppBars.kt # Barres de navigation
|
|
│ │ ├── Buttons.kt # Boutons standardisés
|
|
│ │ ├── Cards.kt # Cartes et surfaces
|
|
│ │ ├── Components.kt # Chips, banners, avatars
|
|
│ │ ├── Feedback.kt # Loading, erreurs
|
|
│ │ └── TextFields.kt # Champs de saisie
|
|
│ └── util/
|
|
│ └── UiState.kt # États UI génériques
|
|
├── navigation/
|
|
│ ├── NavGraph.kt # Navigation principale
|
|
│ └── Screen.kt # Routes
|
|
├── screen/
|
|
│ ├── home/HomeScreen.kt # Écran d'accueil (dashboard actuel)
|
|
│ ├── scanner/ScannerScreen.kt # Scanner code-barres
|
|
│ ├── result/ResultScreen.kt # Résultat d'analyse
|
|
│ ├── onboarding/ # Onboarding
|
|
│ ├── profile/ # Profils famille
|
|
│ ├── history/ # Historique
|
|
│ ├── settings/ # Paramètres
|
|
│ └── ocr/ # Capture OCR
|
|
└── theme/
|
|
├── Color.kt # Palette de couleurs
|
|
├── Type.kt # Typographie
|
|
├── Shape.kt # Formes
|
|
├── Dimens.kt # Espacements
|
|
├── StatusColors.kt # Couleurs de statut
|
|
└── Theme.kt # Thème Material 3
|
|
```
|
|
|
|
### 1.2 Écarts identifiés vs spec UX
|
|
|
|
| Élément | Spec UX (flux-UX.md) | Existant | Écart |
|
|
|---------|---------------------|----------|-------|
|
|
| **Navigation** | BottomNavigationView 4 onglets + FAB central | Navigation linéaire (Home → Scanner → Result) | 🔴 Majeur |
|
|
| **FAB Scanner** | FAB 56dp centré, chevauchant la bottom bar | Bouton dans HomeScreen | 🔴 Majeur |
|
|
| **Dashboard contextuel** | 3 modes (store/home/first) | HomeScreen basique | 🟠 Partiel |
|
|
| **Verdict immédiat** | Banner tricolore en < 500ms avec skeleton | ResultScreen complet mais sans skeleton | 🟠 Partiel |
|
|
| **Skeleton screen** | Shimmer animé pendant le loading | Spinner basique (LoadingIndicator) | 🟡 Mineur |
|
|
| **Couleurs** | Feu tricolore strict (#2ECC71, #E67E22, #E74C3C) | Palette Material 3 différente | 🟡 Mineur |
|
|
| **Animations** | Transitions standardisées (200-300ms) | Animations de base (fade + slide) | 🟡 Mineur |
|
|
| **Accessibilité** | TalkBack, contrastes WCAG AA | Partiel | 🟡 Mineur |
|
|
| **Onglet Listes** | 📋 Listes intelligentes | Absent | 🔴 Majeur |
|
|
| **Onglet Suivi** | 📊 Statistiques + historique | HistoryScreen basique | 🟠 Partiel |
|
|
|
|
### 1.3 Points forts de l'existant
|
|
|
|
- ✅ Architecture Clean Architecture bien structurée
|
|
- ✅ Jetpack Compose avec Material 3
|
|
- ✅ Thème light/dark fonctionnel
|
|
- ✅ Composants de base réutilisables
|
|
- ✅ Domain layer avec UseCases
|
|
- ✅ Repository pattern avec Room + Retrofit
|
|
- ✅ Hilt pour l'injection de dépendances
|
|
|
|
---
|
|
|
|
## 2. PRINCIPES DIRECTEURS
|
|
|
|
### 2.1 Règles prioritaires (de flux-UX.md §1)
|
|
|
|
| # | Principe | Implication technique |
|
|
|---|----------|----------------------|
|
|
| P1 | **2 taps max** | Le scanner doit être accessible depuis n'importe quel écran en ≤ 2 taps |
|
|
| P2 | **Verdict immédiat** | Affichage du verdict en < 500ms perçues (skeleton immédiat, données async) |
|
|
| P3 | **Feu tricolore** | 3 couleurs sémantiques max — jamais de bleu pour les statuts |
|
|
| P4 | **Icônes + couleurs** | Aucune info critique basée uniquement sur la couleur |
|
|
| P5 | **Guidage positif** | Pas d'erreurs brutes — toujours une action de repli |
|
|
| P6 | **Mobile-first** | Conception une main, pouce accessible (zone inférieure) |
|
|
|
|
### 2.2 Contraintes techniques
|
|
|
|
- **Minimum SDK** : API 26 (Android 8.0)
|
|
- **Framework UI** : Jetpack Compose (Material 3)
|
|
- **Navigation** : Compose Navigation
|
|
- **Caméra** : CameraX + ML Kit Barcode Scanning
|
|
- **Base locale** : Room
|
|
- **Taille APK cible** : < 25 Mo
|
|
|
|
---
|
|
|
|
## 3. ARCHITECTURE DE NAVIGATION CIBLE
|
|
|
|
### 3.1 Structure globale
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────┐
|
|
│ APPLICATION │
|
|
├─────────────────────────────────────────────────────┤
|
|
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────┐ │
|
|
│ │ Dashboard│ │ Listes │ │ Suivi │ │Famille│ │
|
|
│ │ 🏠 │ │ 📋 │ │ 📊 │ │ 👤 │ │
|
|
│ └──────────┘ └──────────┘ └──────────┘ └──────┘ │
|
|
│ ▲ │
|
|
│ │ │
|
|
│ ┌────┴─────────┐ <-- FAB central (56dp) │
|
|
│ │ SCANNER │ Chevauche la bottom bar │
|
|
│ └──────────────┘ Disparaît pendant le scan │
|
|
└─────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
### 3.2 Nouvelles routes
|
|
|
|
```kotlin
|
|
sealed class Screen(val route: String) {
|
|
// ── Onglets principaux (Bottom Navigation) ──
|
|
data object Dashboard : Screen("dashboard") // Remplace Home
|
|
data object Lists : Screen("lists") // Nouvel onglet
|
|
data object Tracking : Screen("tracking") // Remplace History
|
|
data object Family : Screen("family") // Remplace ProfileList
|
|
|
|
// ── Écrans de navigation (non dans bottom nav) ──
|
|
data object Scanner : Screen("scanner") // Plein écran
|
|
data object Result : Screen("result/{barcode}") // Bottom sheet → plein écran
|
|
data object ProductDetail : Screen("product/{id}")// Fiche détaillée avec tabs
|
|
data object Onboarding : Screen("onboarding")
|
|
data object Settings : Screen("settings")
|
|
|
|
// ── Sous-écrans ──
|
|
data object ListDetail : Screen("list/{id}")
|
|
data object ListEdit : Screen("list/edit?id={id}")
|
|
data object ProfileEdit : Screen("profile/edit?id={id}")
|
|
data object OcrCapture : Screen("ocr/capture")
|
|
data object OcrReview : Screen("ocr/review")
|
|
}
|
|
```
|
|
|
|
### 3.3 Bottom Navigation — Spécification
|
|
|
|
```kotlin
|
|
data class BottomNavItem(
|
|
val id: String,
|
|
val iconSelected: ImageVector, // Icône remplie
|
|
val iconUnselected: ImageVector, // Icône outline
|
|
val label: String,
|
|
val contentDescription: String, // TalkBack
|
|
val badge: StateFlow<Int> = MutableStateFlow(0) // Notifications non lues
|
|
)
|
|
|
|
val bottomNavItems = listOf(
|
|
BottomNavItem(
|
|
id = "dashboard",
|
|
iconSelected = Icons.Filled.Home,
|
|
iconUnselected = Icons.Outlined.Home,
|
|
label = "Accueil",
|
|
contentDescription = "Tableau de bord"
|
|
),
|
|
BottomNavItem(
|
|
id = "lists",
|
|
iconSelected = Icons.Filled.List,
|
|
iconUnselected = Icons.Outlined.List,
|
|
label = "Listes",
|
|
contentDescription = "Mes listes de courses"
|
|
),
|
|
BottomNavItem(
|
|
id = "tracking",
|
|
iconSelected = Icons.Filled.BarChart,
|
|
iconUnselected = Icons.Outlined.BarChart,
|
|
label = "Suivi",
|
|
contentDescription = "Statistiques et historique"
|
|
),
|
|
BottomNavItem(
|
|
id = "family",
|
|
iconSelected = Icons.Filled.People,
|
|
iconUnselected = Icons.Outlined.People,
|
|
label = "Famille",
|
|
contentDescription = "Profils et réglages"
|
|
)
|
|
)
|
|
```
|
|
|
|
### 3.4 FAB — Spécification technique
|
|
|
|
```yaml
|
|
fab_scanner:
|
|
size: 56dp
|
|
icon: Icons.Filled.QrCodeScanner (24dp)
|
|
container_color: "#2D3436" # Noir doux
|
|
icon_color: "#FFFFFF"
|
|
elevation: 6dp
|
|
corner_radius: 16dp # Material 3 standard
|
|
|
|
position:
|
|
horizontal: center
|
|
vertical: "bottom bar top - 28dp" # Chevauchement
|
|
|
|
behavior:
|
|
visible_on_tabs: ["dashboard", "lists", "tracking", "family"]
|
|
hidden_on: ["scanner", "result", "product_detail"]
|
|
animation_disappear: "scale(1→0.8) + alpha(1→0), 200ms"
|
|
animation_appear: "scale(0.8→1) + alpha(0→1), 200ms"
|
|
haptic_feedback: 15ms au tap
|
|
|
|
accessibility:
|
|
content_description: "Scanner un produit"
|
|
test_id: "fab_scanner"
|
|
```
|
|
|
|
---
|
|
|
|
## 4. DESIGN SYSTEM
|
|
|
|
### 4.1 Palette de couleurs — Alignement spec ↔ existant
|
|
|
|
La spec UX demande des couleurs spécifiques qui diffèrent de la palette Material 3 actuelle. Voici le mapping :
|
|
|
|
```kotlin
|
|
// ── COULEURS SÉMANTIQUES (Feu tricolore — hors M3) ──
|
|
// Ces couleurs restent indépendantes du thème M3 pour cohérence marque
|
|
|
|
object SemanticColors {
|
|
// Light mode
|
|
val Safe = Color(0xFF2ECC71) // Vert sécurité
|
|
val SafeContainer = Color(0xFFE8F8F5) // Fond très clair
|
|
val Warning = Color(0xFFE67E22) // Orange attention
|
|
val WarningContainer = Color(0xFFFEF5E7)
|
|
val Danger = Color(0xFFE74C3C) // Rouge danger
|
|
val DangerContainer = Color(0xFFFDEDEC)
|
|
|
|
// Dark mode
|
|
val SafeDark = Color(0xFF2ECC71)
|
|
val SafeContainerDark = Color(0xFF1A3A2A)
|
|
val WarningDark = Color(0xFFE67E22)
|
|
val WarningContainerDark = Color(0xFF3A2A1A)
|
|
val DangerDark = Color(0xFFE74C3C)
|
|
val DangerContainerDark = Color(0xFF3A1A1A)
|
|
}
|
|
|
|
// ── NEUTRES ──
|
|
object NeutralColors {
|
|
val Background = Color(0xFFF5F5F0) // Gris chaud (spec §2.1)
|
|
val Surface = Color(0xFFFFFFFF) // Blanc pur pour cartes
|
|
val TextPrimary = Color(0xFF2D3436) // Noir doux
|
|
val TextSecondary = Color(0xFF636E72) // Gris moyen
|
|
val Separator = Color(0xFFDFE6E9) // Gris clair
|
|
}
|
|
```
|
|
|
|
### 4.2 Typographie
|
|
|
|
```kotlin
|
|
object SafeBiteTypography {
|
|
val Display = TextStyle(
|
|
fontSize = 28.sp,
|
|
fontWeight = FontWeight.Bold,
|
|
lineHeight = 36.sp
|
|
)
|
|
val Headline = TextStyle(
|
|
fontSize = 22.sp,
|
|
fontWeight = FontWeight.SemiBold,
|
|
lineHeight = 28.sp
|
|
)
|
|
val Body = TextStyle(
|
|
fontSize = 16.sp,
|
|
fontWeight = FontWeight.Normal,
|
|
lineHeight = 24.sp
|
|
)
|
|
val Caption = TextStyle(
|
|
fontSize = 13.sp,
|
|
fontWeight = FontWeight.Normal,
|
|
lineHeight = 18.sp
|
|
)
|
|
val Button = TextStyle(
|
|
fontSize = 15.sp,
|
|
fontWeight = FontWeight.SemiBold,
|
|
lineHeight = 20.sp
|
|
)
|
|
}
|
|
```
|
|
|
|
### 4.3 Système d'icônes de statut (daltonien-safe)
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────┐
|
|
│ VERT (#2ECC71) ORANGE (#E67E22) ROUGE (#E74C3C) │
|
|
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
|
│ │ ⭕ │ │ 🔺 │ │ 🔷 │ │
|
|
│ │ ✅ │ │ ⚠️ │ │ ❌ │ │
|
|
│ │ CERCLE │ │ TRIANGLE │ │ LOSANGE │ │
|
|
│ └──────────┘ └──────────┘ └──────────┘ │
|
|
│ │
|
|
│ JAMAIS l'un sans l'autre : forme + couleur + icône │
|
|
└─────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
### 4.4 Élévation & Ombres
|
|
|
|
```kotlin
|
|
object ElevationTokens {
|
|
val Card = 2.dp // Ombre: 0 1px 3px rgba(0,0,0,0.08)
|
|
val FAB = 6.dp // Ombre: 0 3px 8px rgba(0,0,0,0.16)
|
|
val BottomSheet = 16.dp // Ombre: 0 8px 24px rgba(0,0,0,0.20)
|
|
val Dialog = 24.dp // Ombre: 0 12px 32px rgba(0,0,0,0.24)
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 5. SPÉCIFICATIONS DES ÉCRANS — PHASE 1
|
|
|
|
### 5.1 Écran Scanner (refonte)
|
|
|
|
**Fichier cible** : `app/src/main/java/com/safebite/app/presentation/screen/scanner/ScannerScreen.kt`
|
|
|
|
```yaml
|
|
screen_scanner:
|
|
type: "Plein écran (remplace le contenu de l'onglet actif)"
|
|
transition_in: "ContainerTransform depuis FAB, 300ms"
|
|
transition_out: "Reverse ContainerTransform, 250ms"
|
|
|
|
layout:
|
|
top_bar:
|
|
type: "Transparent overlay"
|
|
elements:
|
|
- {left: "← Retour", action: "popBackStack"}
|
|
- {right: "💡 Astuce", action: "showTips"}
|
|
|
|
camera_zone:
|
|
coverage: "70% de l'écran"
|
|
reticule:
|
|
size: "80% width, 30% height"
|
|
border: "4dp blanc, coins arrondis 12dp"
|
|
animation: "Ligne laser verte animée (haut → bas, 1.8s loop)"
|
|
|
|
instruction_text:
|
|
content: "Placez le code-barres dans le cadre"
|
|
position: "Sous le réticule, centré"
|
|
style: "Body, blanc, fond semi-transparent"
|
|
|
|
bottom_bar:
|
|
elements:
|
|
- {id: "manual_entry", label: "Saisie manuelle", icon: "keyboard"}
|
|
- {id: "torch", label: "Flash", icon: "flash_on/off", toggle: true}
|
|
|
|
states:
|
|
idle: "Camera preview + réticule + instructions"
|
|
scanning: "Réticule pulse (scale 1.0 → 1.05 → 1.0, 200ms)"
|
|
analyzing: "→ Transition vers skeleton (voir ResultScreen)"
|
|
error_camera: "Message + lien Réglages + saisie manuelle"
|
|
error_permission: "Dialog rationale + fallback saisie manuelle"
|
|
|
|
performance:
|
|
open_time: "< 300ms"
|
|
method: "Pré-initialiser CameraProvider en arrière-plan"
|
|
```
|
|
|
|
**Changements par rapport à l'existant** :
|
|
- ✅ Réticule animé existe déjà (ScanOverlay)
|
|
- ❌ Transition ContainerTransform depuis FAB à ajouter
|
|
- ❌ Top bar transparente overlay à ajouter
|
|
- ❌ Saisie manuelle du code-barres à ajouter
|
|
- ❌ Pré-initialisation caméra à implémenter
|
|
|
|
### 5.2 Écran Verdict (refonte majeure)
|
|
|
|
**Fichier cible** : `app/src/main/java/com/safebite/app/presentation/screen/result/ResultScreen.kt`
|
|
|
|
```yaml
|
|
screen_verdict:
|
|
type: "Bottom Sheet → Plein écran au scroll"
|
|
transition_in: "Slide up from bottom, 250ms, ease-out"
|
|
|
|
# ── PHASE 1 : Skeleton immédiat ──
|
|
skeleton:
|
|
type: "Shimmer gradient animé"
|
|
duration: "1.5s loop"
|
|
layout: |
|
|
┌──────────────────────────────┐
|
|
│ ████████████ (nom produit) │
|
|
│ ██████ (marque) │
|
|
│ ░░░░░░░░░░░░ (verdict) │ ← Background coloré
|
|
│ │
|
|
│ ████████████ │
|
|
│ ██████████ │
|
|
└──────────────────────────────┘
|
|
|
|
# ── PHASE 2 : Verdict ──
|
|
verdict_banner:
|
|
height: 56dp minimum
|
|
padding: "16dp horizontal, 12dp vertical"
|
|
radius: 12dp
|
|
icon_size: 24dp
|
|
|
|
variants:
|
|
ok:
|
|
text: "✅ OK pour toute la famille"
|
|
background: "#E8F8F5"
|
|
icon: "checkmark.shield.fill"
|
|
icon_color: "#2ECC71"
|
|
|
|
warning:
|
|
text: "⚠️ Contient : NOISETTES"
|
|
subtext: "⚠️ Attention pour Julie"
|
|
background: "#FEF5E7"
|
|
icon: "exclamationmark.shield.fill"
|
|
icon_color: "#E67E22"
|
|
allergene_style: "bold, #E67E22"
|
|
|
|
danger:
|
|
text: "❌ Contient : ARACHIDES"
|
|
subtext: "❌ Interdit pour Julie (anaphylaxie)"
|
|
extra: "Ne pas consommer"
|
|
background: "#FDEDEC"
|
|
icon: "xmark.shield.fill"
|
|
icon_color: "#E74C3C"
|
|
allergene_style: "bold, #E74C3C"
|
|
|
|
# ── PHASE 3 : Actions ──
|
|
actions:
|
|
stagger_animation: "Chaque action +50ms de décalage"
|
|
buttons:
|
|
- {id: "details", label: "Voir détails", priority: primary, icon: "info"}
|
|
- {id: "alternatives", label: "Voir alternatives", priority: secondary, icon: "swap"}
|
|
- {id: "add_to_list", label: "Ajouter à la liste", priority: secondary, icon: "plus"}
|
|
- {id: "scan_again", label: "Scanner un autre", priority: tertiary, icon: "camera"}
|
|
|
|
# ── Accessibilité ──
|
|
accessibility:
|
|
announcement: "Verdict : {status}. {details}. Actions : voir détails, alternatives, ajouter à la liste."
|
|
talkback_order: "Verdict banner → Product info → Actions (top to bottom)"
|
|
```
|
|
|
|
**Changements par rapport à l'existant** :
|
|
- ✅ SafetyStatusBanner existe déjà (à adapter aux nouvelles couleurs)
|
|
- ❌ Skeleton screen shimmer à ajouter (remplacer LoadingIndicator)
|
|
- ❌ Bottom sheet transition à ajouter
|
|
- ❌ Stagger animation sur les actions à ajouter
|
|
- ❌ Verdict variants (warning/danger) à enrichir avec noms profils
|
|
- ❌ Bouton "Alternatives" à ajouter
|
|
|
|
### 5.3 Dashboard contextuel (nouvel écran)
|
|
|
|
**Fichier cible** : `app/src/main/java/com/safebite/app/presentation/screen/dashboard/DashboardScreen.kt` (nouveau)
|
|
|
|
```yaml
|
|
screen_dashboard:
|
|
type: "Écran principal (onglet Accueil)"
|
|
replaces: "HomeScreen actuel"
|
|
|
|
# ── Détection contextuelle ──
|
|
context_detection:
|
|
store_mode:
|
|
trigger: "Géolocalisation magasin OU heure 8h-20h semaine"
|
|
confidence: "Score basé sur multiples signaux"
|
|
|
|
home_mode:
|
|
trigger: "Soirée (après 20h) OU weekend"
|
|
|
|
first_time:
|
|
trigger: "Aucun scan dans l'historique"
|
|
|
|
# ── Mode magasin ──
|
|
store_layout: |
|
|
┌──────────────────────────────┐
|
|
│ 🛒 Vous êtes en magasin ? │
|
|
│ │
|
|
│ [Scanner rapide] ← Large │
|
|
│ │
|
|
│ Votre liste en cours : │
|
|
│ ┌──────────────────────┐ │
|
|
│ │ 🥛 Lait demi-écrémé │ │
|
|
│ │ 🍞 Pain complet │ │
|
|
│ │ 🍎 Pommes x6 │ │
|
|
│ └──────────────────────┘ │
|
|
│ │
|
|
│ "3 produits restants" │
|
|
└──────────────────────────────┘
|
|
|
|
# ── Mode maison ──
|
|
home_layout: |
|
|
┌──────────────────────────────┐
|
|
│ 👋 Bonjour, Sophie │
|
|
│ │
|
|
│ 📊 Cette semaine : │
|
|
│ ████████░░ 78% produits OK │
|
|
│ │
|
|
│ 🔍 Derniers scans : │
|
|
│ ✅ Biscuit Choco │
|
|
│ ⚠️ Sauce Curry │
|
|
│ │
|
|
│ [Scanner] [Mes listes] │
|
|
└──────────────────────────────┘
|
|
|
|
# ── Premier lancement ──
|
|
first_time_layout: |
|
|
┌──────────────────────────────┐
|
|
│ 🎉 Prêt à commencer ! │
|
|
│ │
|
|
│ 📷 Scannez votre premier │
|
|
│ produit │
|
|
│ │
|
|
│ [Commencer →] │
|
|
└──────────────────────────────┘
|
|
|
|
viewmodel:
|
|
state: "DashboardUiState"
|
|
sealed_class: |
|
|
sealed class DashboardUiState {
|
|
object Loading : DashboardUiState()
|
|
data class StoreMode(val activeList: ShoppingList?, val remainingCount: Int) : DashboardUiState()
|
|
data class HomeMode(val userName: String, val weeklyStats: WeeklyStats, val recentScans: List<ScanHistory>) : DashboardUiState()
|
|
object FirstTime : DashboardUiState()
|
|
data class Error(val message: String) : DashboardUiState()
|
|
}
|
|
```
|
|
|
|
### 5.4 Fiche produit détaillée (nouvel écran)
|
|
|
|
**Fichier cible** : `app/src/main/java/com/safebite/app/presentation/screen/product/ProductDetailScreen.kt` (nouveau)
|
|
|
|
```yaml
|
|
screen_product_detail:
|
|
type: "Bottom Sheet → Plein écran au scroll"
|
|
trigger: "Tap 'Voir détails' depuis verdict OU historique"
|
|
|
|
tabs:
|
|
- id: "resume"
|
|
label: "Résumé"
|
|
icon: Icons.Filled.List
|
|
content:
|
|
- "Verdict sécurité (répété)"
|
|
- "Nutri-Score visuel (A-E, pastilles)"
|
|
- "Calories / 100g"
|
|
- "Jauges sucre/sel/gras"
|
|
|
|
- id: "allergenes"
|
|
label: "Allergènes"
|
|
icon: Icons.Filled.Warning
|
|
content:
|
|
- "14 allergènes réglementaires"
|
|
- "Statut : Présent ❌ / Traces ⚠️ / Absent ✅"
|
|
- "Allergènes famille highlightés"
|
|
|
|
- id: "additifs"
|
|
label: "Additifs"
|
|
icon: Icons.Filled.Science
|
|
content:
|
|
- "Liste additifs code E"
|
|
- "Couleur : Vert/Orange/Rouge"
|
|
- "Description courte"
|
|
- "Lien 'En savoir plus' → WebView"
|
|
|
|
- id: "alternatives"
|
|
label: "Alternatives"
|
|
icon: Icons.Filled.SwapHoriz
|
|
condition: "Affiché si verdict != OK"
|
|
content:
|
|
- "Carousel horizontal produits"
|
|
- "Critère : même catégorie, sans allergène"
|
|
- "Carte : photo + nom + verdict mini"
|
|
```
|
|
|
|
---
|
|
|
|
## 6. COMPOSANTS RÉUTILISABLES
|
|
|
|
### 6.1 Nouveaux composants à créer
|
|
|
|
```kotlin
|
|
// ── VerdictBanner ──
|
|
@Composable
|
|
fun VerdictBanner(
|
|
verdict: VerdictType, // SAFE, WARNING, DANGER
|
|
message: String,
|
|
subMessage: String? = null,
|
|
allergenName: String? = null,
|
|
profileName: String? = null,
|
|
modifier: Modifier = Modifier
|
|
)
|
|
|
|
// ── SkeletonScreen ──
|
|
@Composable
|
|
fun ProductSkeleton(
|
|
modifier: Modifier = Modifier
|
|
)
|
|
|
|
// ── ActionButton (standardisé) ──
|
|
@Composable
|
|
fun ActionButton(
|
|
text: String,
|
|
onClick: () -> Unit,
|
|
icon: ImageVector? = null,
|
|
variant: ButtonVariant = ButtonVariant.Primary, // Primary, Secondary, Danger, Tertiary
|
|
modifier: Modifier = Modifier
|
|
)
|
|
|
|
// ── VerdictMiniBadge (pour carrousels) ──
|
|
@Composable
|
|
fun VerdictMiniBadge(
|
|
verdict: VerdictType,
|
|
size: Dp = 24.dp
|
|
)
|
|
|
|
// ── ProgressBar circulaire (dashboard) ──
|
|
@Composable
|
|
fun CircularProgress(
|
|
progress: Float, // 0.0 - 1.0
|
|
label: String,
|
|
color: Color,
|
|
size: Dp = 120.dp
|
|
)
|
|
|
|
// ── Sparkline (graphique évolution) ──
|
|
@Composable
|
|
fun SparklineChart(
|
|
data: List<Float>,
|
|
color: Color = MaterialTheme.colorScheme.primary,
|
|
modifier: Modifier = Modifier
|
|
)
|
|
|
|
// ── AllergenGrid (sélection profils) ──
|
|
@Composable
|
|
fun AllergenSelectionGrid(
|
|
allergens: List<AllergenType>,
|
|
selections: Map<AllergenType, AllergenLevel>,
|
|
onSelectionChange: (AllergenType, AllergenLevel) -> Unit
|
|
)
|
|
|
|
enum class AllergenLevel {
|
|
NONE, // Non sélectionné
|
|
TRACE, // ⚠️ Intolérance/traces
|
|
SEVERE // ❌ Allergie sévère
|
|
}
|
|
```
|
|
|
|
### 6.2 Composants existants à adapter
|
|
|
|
| Composant | Fichier actuel | Modification nécessaire |
|
|
|-----------|---------------|------------------------|
|
|
| `SafetyStatusBanner` | `Components.kt:90` | Adapter couleurs spec + formes daltonien |
|
|
| `ProductCard` | `Components.kt:118` | Ajouter verdict mini badge |
|
|
| `AllergenChip` | `Components.kt:58` | Ajouter 3 états (absent/traces/présent) |
|
|
| `PrimaryButton` | `Buttons.kt` | Renommer en `ActionButton` avec variants |
|
|
| `OutlinedActionButton` | `Buttons.kt` | Intégrer dans `ActionButton` variant Secondary |
|
|
| `LoadingIndicator` | `Feedback.kt` | Remplacer par `ProductSkeleton` |
|
|
| `SafeBiteTopAppBar` | `AppBars.kt` | Ajouter support overlay transparent |
|
|
|
|
---
|
|
|
|
## 7. PLAN DE MIGRATION
|
|
|
|
### 7.1 Phase 1 — Scanner, Verdict, Dashboard (priorité haute)
|
|
|
|
| Étape | Action | Fichiers | Effort |
|
|
|-------|--------|----------|--------|
|
|
| 1.1 | Créer la Bottom Navigation | `NavGraph.kt`, `Screen.kt`, nouveau `MainScreen.kt` | Moyen |
|
|
| 1.2 | Créer le FAB central | `MainScreen.kt`, `components/Buttons.kt` | Moyen |
|
|
| 1.3 | Adapter les couleurs du Design System | `Color.kt`, `StatusColors.kt` | Faible |
|
|
| 1.4 | Refondre ScannerScreen (transitions, saisie manuelle) | `ScannerScreen.kt` | Moyen |
|
|
| 1.5 | Créer le Skeleton Screen | Nouveau `components/Feedback.kt` | Faible |
|
|
| 1.6 | Refondre VerdictBanner (3 variantes, accessibilité) | `Components.kt` | Moyen |
|
|
| 1.7 | Créer DashboardScreen (3 modes contextuels) | Nouveau `screen/dashboard/` | Élevé |
|
|
| 1.8 | Adapter ResultScreen (bottom sheet, stagger actions) | `ResultScreen.kt` | Moyen |
|
|
|
|
### 7.2 Phase 2 — Listes intelligentes
|
|
|
|
| Étape | Action | Fichiers |
|
|
|-------|--------|----------|
|
|
| 2.1 | Créer ListsScreen (liste des listes) | Nouveau `screen/lists/` |
|
|
| 2.2 | Créer ListDetailScreen (détail d'une liste) | Nouveau `screen/lists/` |
|
|
| 2.3 | Implémenter swipe actions | `ListDetailScreen.kt` |
|
|
| 2.4 | Intégrer alertes allergies dans les listes | `ListDetailScreen.kt` |
|
|
|
|
### 7.3 Phase 3 — Suivi & Statistiques
|
|
|
|
| Étape | Action | Fichiers |
|
|
|-------|--------|----------|
|
|
| 3.1 | Créer TrackingScreen (stats + historique) | Nouveau `screen/tracking/` |
|
|
| 3.2 | Implémenter CircularProgress | `components/Feedback.kt` |
|
|
| 3.3 | Implémenter SparklineChart | Nouveau `components/Charts.kt` |
|
|
| 3.4 | Migrer HistoryScreen vers TrackingScreen | `screen/history/` → `screen/tracking/` |
|
|
|
|
### 7.4 Phase 4 — Profils famille (améliorations)
|
|
|
|
| Étape | Action | Fichiers |
|
|
|-------|--------|----------|
|
|
| 4.1 | Refondre FamilyScreen (grille profils) | `screen/profile/` |
|
|
| 4.2 | Créer AllergenSelectionGrid | `components/` |
|
|
| 4.3 | Adapter ProfileEditScreen (3 états allergie) | `screen/profile/ProfileEditScreen.kt` |
|
|
|
|
---
|
|
|
|
## 8. ANNEXES
|
|
|
|
### 8.1 États UI — Scan (mis à jour)
|
|
|
|
```kotlin
|
|
sealed class ScanUiState {
|
|
object Idle : ScanUiState()
|
|
data class CameraReady(val isPermissionGranted: Boolean) : ScanUiState()
|
|
data class Scanning(val analyzedFrames: Int) : ScanUiState()
|
|
object Analyzing : ScanUiState() // → Skeleton screen
|
|
data class Verdict(
|
|
val product: Product,
|
|
val verdict: VerdictType,
|
|
val matchingProfiles: List<ProfileMatch>
|
|
) : ScanUiState()
|
|
data class Error(
|
|
val errorType: ScanError,
|
|
val recoveryAction: RecoveryAction
|
|
) : ScanUiState()
|
|
}
|
|
|
|
enum class VerdictType {
|
|
SAFE, // ✅ OK famille
|
|
WARNING, // ⚠️ Allergène traces/intolérance
|
|
DANGER // ❌ Allergène critique
|
|
}
|
|
|
|
data class ProfileMatch(
|
|
val profileName: String,
|
|
val matchedAllergen: AllergenType,
|
|
val severity: AllergenLevel
|
|
)
|
|
```
|
|
|
|
### 8.2 Checklist accessibilité
|
|
|
|
```yaml
|
|
accessibilite:
|
|
contrastes:
|
|
texte_normal: "Ratio ≥ 4.5:1"
|
|
texte_grand: "Ratio ≥ 3:1"
|
|
composants_ui: "Ratio ≥ 3:1"
|
|
validation: "Accessibility Scanner Android"
|
|
|
|
daltonisme:
|
|
regle: "Jamais couleur seule — toujours forme + icône + couleur"
|
|
test: "Utilisateur daltonien minimum"
|
|
|
|
zones_tactiles:
|
|
taille_min: "48dp x 48dp"
|
|
espacement_min: "8dp entre zones"
|
|
|
|
talkback:
|
|
content_description: "Tout élément interactif"
|
|
images_decoratives: "contentDescription = null"
|
|
annonces_etat: "Verdict, chargement, erreurs"
|
|
ordre_focus: "Gauche→droite, haut→bas"
|
|
|
|
texte_dynamique:
|
|
scale_max: "200% sans perte de contenu"
|
|
implementation: "sp (pas dp pour le texte)"
|
|
```
|
|
|
|
### 8.3 Performance perçue — Objectifs
|
|
|
|
| Métrique | Cible | Méthode |
|
|
|----------|-------|---------|
|
|
| Ouverture scanner | < 300ms | Pré-initialisation caméra |
|
|
| Affichage verdict | < 500ms | Skeleton immédiat + données async |
|
|
| Transition écrans | 200-300ms | ease-out, jamais > 400ms |
|
|
| Scroll FPS | 60fps | LazyColumn + pagination |
|
|
| Taille APK | < 25 Mo | R8/ProGuard, optimisation ressources |
|
|
|
|
### 8.4 Diagramme de flux — Scan complet
|
|
|
|
```mermaid
|
|
flowchart TD
|
|
A[FAB Scanner] -->|Tap| B[ScannerScreen]
|
|
B -->|Code-barres détecté| C[Vibration 15ms]
|
|
C -->|Transition| D[Skeleton Screen]
|
|
D -->|Données reçues| E{Verdict}
|
|
|
|
E -->|SAFE| F[VerdictBanner Vert]
|
|
E -->|WARNING| G[VerdictBanner Orange]
|
|
E -->|DANGER| H[VerdictBanner Rouge]
|
|
|
|
F --> I[Actions disponibles]
|
|
G --> I
|
|
H --> I
|
|
|
|
I -->|Voir détails| J[ProductDetailScreen]
|
|
I -->|Alternatives| K[Carousel Alternatives]
|
|
I -->|Ajouter liste| L[Dialog Sélection liste]
|
|
I -->|Scanner autre| B
|
|
|
|
D -->|Timeout 3s| M[Analyse en cours...]
|
|
M -->|Timeout 8s| N[ErrorScreen]
|
|
N -->|Réessayer| B
|
|
N -->|Saisie manuelle| O[Dialog Code-barres]
|
|
O -->|Valider| D
|
|
```
|
|
|
|
---
|
|
|
|
**Ce document constitue la référence pour la refonte UI-UX de l'application SafeBite Android. Il sera mis à jour au fur et à mesure de l'implémentation.**
|