SafeBite/docs/flux-UX.md
2026-04-25 10:26:13 -04:00

1085 lines
41 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 📱 APPLICATION IOS/ANDROID SPÉCIFICATION UX/UI DÉTAILLÉE
**Document technique pour implémentation • Version 1.0 • 25 avril 2026**
---
## 🎯 PRINCIPES FONDATEURS (GUIDING PRINCIPLES)
> **Ces règles priment sur toute décision d'implémentation.**
| # | Principe | Règle stricte |
|---|----------|---------------|
| P1 | **2 taps max** | L'action scanner est accessible en ≤ 2 taps depuis n'importe quel écran |
| P2 | **Verdict immédiat** | Après scan, le verdict de sécurité s'affiche en < 500ms (perçues) |
| P3 | **Feu tricolore** | 3 couleurs sémantiques max + neutres jamais de bleu |
| P4 | **Icônes + couleurs** | Aucune information critique ne repose uniquement sur la couleur |
| P5 | **Guidage positif** | Pas de messages d'erreur bruts toujours une action de repli proposée |
| P6 | **Mobile-first** | Conception pour une main, pouce accessible (zone de confort inférieure) |
---
## 🎨 DESIGN SYSTEM
### 2.1 Palette de couleurs
```
COULEURS SÉMANTIQUES (Système feu tricolore)
─────────────────────────────────────────────
🟢 Vert sécurité #2ECC71 → Produit compatible, OK famille
🟠 Orange attention #E67E22 → Allergène détecté, vigilance
🔴 Rouge danger #E74C3C → Allergène critique, interdit
NEUTRES
─────────────────────────────────────────────
Fond principal #F5F5F0 → Gris chaud (réduit la fatigue oculaire)
Surface carte #FFFFFF → Blanc pur pour les cartes
Texte principal #2D3436 → Noir doux (pas #000, trop agressif)
Texte secondaire #636E72 → Gris moyen
Séparateurs #DFE6E9 → Gris clair
ÉTATS
─────────────────────────────────────────────
Appui (pressed) Assombrir de 15% la couleur de base
Désactivé Opacité 40% + grisé
Focus (accessibilité) Anneau #2ECC71 de 2px (offset 2px)
```
### 2.2 Typographie
```yaml
platform_android:
font_family: "Google Sans" # ou Inter en fallback
scale:
display: {size: 28sp, weight: 700, line_height: 36sp, usage: "Titre écran"}
headline: {size: 22sp, weight: 600, line_height: 28sp, usage: "Titre section"}
body: {size: 16sp, weight: 400, line_height: 24sp, usage: "Texte principal"}
caption: {size: 13sp, weight: 400, line_height: 18sp, usage: "Légende, méta"}
button: {size: 15sp, weight: 600, line_height: 20sp, usage: "CTA, boutons"}
platform_ios:
font_family: "SF Pro"
# Mêmes échelles, adaptées en pt
```
### 2.3 Iconographie
```yaml
system_icons:
✅ sécurité_ok: "checkmark.shield.fill" # Vert - Produit sûr
⚠️ vigilance: "exclamationmark.shield.fill" # Orange - Attention
❌ danger: "xmark.shield.fill" # Rouge - Interdit
📷 scanner: "camera.viewfinder" # Action principale
📋 listes: "list.clipboard" # Onglet listes
📊 suivi: "chart.bar.fill" # Onglet suivi
👤 profil: "person.circle" # Onglet famille/profil
icon_principles:
- Toujours accompagner la couleur d'une forme distincte
- Formes : cercle (OK), triangle (⚠️), losange (❌)
- Taille minimale des zones tactiles : 48x48dp (WCAG)
```
### 2.4 Élévation & Ombres (Android)
```css
/* Carte standard */
.card {
elevation: 2dp;
/* Ombre: 0 1px 3px rgba(0,0,0,0.08) */
}
/* Bouton flottant (FAB) */
.fab {
elevation: 6dp;
/* Ombre: 0 3px 8px rgba(0,0,0,0.16) */
}
/* Bottom sheet */
.bottom-sheet {
elevation: 16dp;
/* Ombre: 0 8px 24px rgba(0,0,0,0.20) */
}
```
---
## 🏗️ ARCHITECTURE DE NAVIGATION
### 3.1 Structure globale
```
┌─────────────────────────────────────────────────────┐
│ APPLICATION │
├─────────────────────────────────────────────────────┤
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────┐ │
│ │ Scanner │ │ Listes │ │ Suivi │ │Famille│ │
│ │ 📷 │ │ 📋 │ │ 📊 │ │ 👤 │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────┘ │
│ ▲ │
│ │ │
│ ┌────┴─────────┐ <-- Bouton flottant central │
│ │ SCANNER │ (FAB, 56dp, toujours visible)│
│ └──────────────┘ Sauf pendant le scan lui-même│
└─────────────────────────────────────────────────────┘
Navigation: BottomNavigationView (4 items)
FAB: FloatingActionButton centré, légèrement au-dessus de la barre
```
### 3.2 Bottom Navigation Bar Spécification
```kotlin
// Spécification technique BottomNavigation
data class BottomNavItem(
val id: String,
val icon: ImageVector, // Icône remplie (selected) / outline (unselected)
val label: String,
val contentDescription: String // Obligatoire pour TalkBack
)
val bottomNavItems = listOf(
BottomNavItem("scanner", Icons.Filled.Camera, "Scanner", "Scanner un produit"),
BottomNavItem("listes", Icons.Filled.List, "Listes", "Mes listes de courses"),
BottomNavItem("suivi", Icons.Filled.Chart, "Suivi", "Statistiques et historique"),
BottomNavItem("famille", Icons.Filled.Person, "Famille", "Profils et réglages")
)
// État visuel
// Selected : icon color = #2D3436 (noir doux), label visible, indicator top
// Unselected : icon color = #B2BEC3, label visible
// Badge : point rouge 8dp si notification non lue
```
### 3.3 FAB (Floating Action Button) Spécification
```yaml
fab:
size: 56dp # Standard Material Design
icon: 📷 camera_viewfinder (24dp)
color: "#2D3436" # Noir doux contraste avec le fond
icon_color: "#FFFFFF"
elevation: 6dp
position:
horizontal: center # Centré horizontalement
vertical_offset: -28dp # Chevauchant la bottom bar
behavior:
- Visible sur tous les onglets SAUF pendant le scan actif
- Disparition: scale_down + fade_out (200ms)
- Apparition: scale_up + fade_in (200ms)
- Haptique légère (15ms) au tap
accessibility:
label: "Scanner un produit"
hint: "Appuyez deux fois pour ouvrir le scanner"
```
---
## 📱 FLUX UTILISATEUR DÉTAILLÉS
### FLOW 1 : Premier lancement (Onboarding)
```
[ÉCRAN 1] —────—— "Bienvenue !" —────—— [ÉCRAN 2]
"Qui êtes-vous ?" "Des allergies ?"
┌──────────────────┐ ┌──────────────────┐
│ Logo │ │ Grille photos │
│ │ │ (pas de texte) │
│ "Votre prénom" │ │ │
│ [Input text] │ │ [🥜] [🥛] [🍞] │
│ │ │ [🦐] [🥚] [🐟] │
│ [Continuer →] │ │ │
│ │ │ [Continuer →] │
└──────────────────┘ └──────────────────┘
│ │
▼ ▼
[ÉCRAN 3] —────—— "Un objectif ?" —────—— [ÉCRAN 4]
"Scannez !"
┌──────────────────┐ ┌──────────────────┐
│ │ │ Animation │
│ ○ Mieux manger │ │ scanner qui │
│ ○ Éviter additifs│ │ pulse │
│ ○ Sans importance│ │ │
│ │ │ Pointez l'appareil│
│ [Ignorer] [OK] │ │ vers un code- │
│ │ │ barres │
│ │ │ │
└──────────────────┘ │ [Commencer] │
│ └──────────────────┘
▼ │
└──────────┬───────────────────┘
[DASHBOARD]
```
**Règles onboarding :**
- Maximum 4 écrans, 60 secondes cumulées
- Progression : indicateur en haut (4 points)
- Skip possible à chaque étape (sauf écran 4 : CTA seul)
- Pas de permissions demandées avant le scan effectif
- Stockage local uniquement (pas de compte obligatoire)
---
### FLOW 2 : Scan produit (Parcours principal 80% des usages)
```yaml
flow_scan_complete:
# ── ÉTAPE 1 : Activation scanner ──
step_1_open:
trigger: "Tap sur FAB OU tap sur onglet Scanner"
animation: "Expansion depuis le centre (Material ContainerTransform)"
duration: 300ms
state: |
┌──────────────────────────┐
│ Zone de scan (70% écran) │
│ │
│ ┌──────────────┐ │
│ │ [reticule] │ │ Camera preview
│ │ │ │ + overlay réticule
│ └──────────────┘ │
│ │
│ "Placez le code-barres │
│ dans le cadre" │
│ │
│ ⚡ Détection auto │
├──────────────────────────┤
│ [Saisie manuelle] [💡] │ Bottom bar locale
└──────────────────────────┘
permissions:
camera:
rationale: "Pour scanner les codes-barres"
on_denied: "Saisie manuelle proposée"
on_permanently_denied: "Lien vers Réglages + saisie manuelle"
# ── ÉTAPE 2 : Détection et analyse ──
step_2_processing:
trigger: "Code-barres ou OCR détecté automatiquement"
duration_max: 500ms # Perçues
states:
detection:
haptique: "Vibration légère 15ms"
son: "Déclic discret (optionnel, désactivé par défaut)"
animation: |
Le réticule pulse brièvement (scale 1.0 → 1.05 → 1.0, 200ms)
Puis transition vers l'écran résultat
loading:
type: "SKELETON SCREEN (jamais de spinner)"
layout_skeleton: |
┌──────────────────────────┐
│ ████████████ (nom) │
│ ██████ (marque) │
│ ░░░░░░░░░░░░ (verdict) │ Background coloré
│ │
│ ████████████ │
│ ██████████ │
└──────────────────────────┘
animation: "Shimmer wave, gauche → droite, 1.5s loop"
timeout:
after_3s: "Afficher 'Analyse en cours...' + option annuler"
after_8s: "→ FLOW 7 (Gestion d'erreur)"
# ── ÉTAPE 3 : Verdict immédiat ──
step_3_verdict:
transition: "Slide up from bottom, 250ms, ease-out"
layout: |
┌──────────────────────────────┐
│ ← Retour scan ⋮ Options │ Top bar
├──────────────────────────────┤
│ │
│ [Image produit] │
│ (120dp, coins ronds) │
│ │
│ Biscuit Choco Lait │
│ Marque X 200g │
│ │
│ ┌──────────────────────────┐ │
│ │ ✅ OK pour toute la │ │ ← Verdict banner
│ │ famille │ │ Fond vert #2ECC71
│ └──────────────────────────┘ │ Icône cercle blanc
│ │
│ [→ Voir détails] │
│ [→ Alternatives] │
│ [→ Ajouter à la liste] │
│ │
└──────────────────────────────┘
verdict_variants:
ok: |
✅ "OK pour toute la famille"
Fond: #E8F8F5 (vert très clair)
Icône: checkmark.shield.fill, #2ECC71
warning: |
⚠️ "Contient : NOISETTES"
"⚠️ Attention pour Julie"
Fond: #FEF5E7 (orange très clair)
Icône: exclamationmark.shield.fill, #E67E22
Allergène affiché en bold, #E67E22
danger: |
❌ "Contient : ARACHIDES"
"❌ Interdit pour Julie (anaphylaxie)"
Fond: #FDEDEC (rouge très clair)
Icône: xmark.shield.fill, #E74C3C
Allergène affiché en bold, #E74C3C
+ Message explicite : "Ne pas consommer"
actions_disponibles:
- {id: "details", label: "Voir détails", icon: "info.circle", priority: primary}
- {id: "alternatives", label: "Voir alternatives", icon: "arrow.triangle.swap", priority: secondary}
- {id: "add_list", label: "Ajouter à une liste", icon: "plus.circle", priority: secondary}
- {id: "scan_again", label: "Scanner un autre produit", icon: "camera", priority: tertiary}
accessibility:
verdict_announcement: "Verdict : {status}. {details}. Actions disponibles : voir détails, voir alternatives, ajouter à la liste."
# Ex: "Verdict : Attention pour Julie. Contient noisettes. Actions disponibles : ..."
```
---
### FLOW 3 : Fiche produit détaillée
```yaml
flow_product_details:
trigger: "Tap 'Voir détails' depuis le verdict OU depuis l'historique"
transition: "Slide up (Bottom Sheet) → Plein écran au scroll"
structure_tabs:
- id: "resume"
label: "Résumé"
icon: "list.bullet"
content: |
- Verdict sécurité (répété en haut)
- Nutri-Score visuel (A à E, grandes pastilles colorées)
- Calories / 100g
- Taux de sucre, sel, gras (jauges visuelles horizontales)
- id: "allergenes"
label: "Allergènes"
icon: "exclamationmark.shield"
content: |
- Liste des 14 allergènes réglementaires
- Chaque allergène :
→ Présent ❌ / Traces ⚠️ / Absent ✅
- Les allergènes de la famille sont mis en avant (highlight #FEF5E7)
- id: "additifs"
label: "Additifs"
icon: "flask"
content: |
- Liste des additifs avec code E
- Barème couleur : Vert (naturel) / Orange (discutable) / Rouge (éviter)
- Description courte de chaque additif
- Lien "En savoir plus" → WebView interne
- id: "alternatives"
label: "Alternatives"
icon: "arrow.triangle.swap"
priority: "high" # Affiché si verdict != OK
content: |
- Carousel horizontal de produits alternatifs
- Critère : même catégorie, sans l'allergène détecté
- Chaque carte : photo, nom, verdict mini (couleur fond)
- Tap → Verdict du produit alternatif
```
---
### FLOW 4 : Dashboard contextuel
```yaml
flow_dashboard:
# Le dashboard n'est plus un onglet mais un overlay intelligent
# accessible depuis l'onglet "Scanner" (écran principal)
states:
store_mode: |
# Détecté via géolocalisation ou heure (8h-20h en semaine)
┌──────────────────────────────┐
│ 🛒 Vous êtes en magasin ? │
│ │
│ [Scanner rapide] ← Large │
│ │
│ Votre liste en cours : │
│ ┌──────────────────────┐ │
│ │ 🥛 Lait demi-écrémé │ │
│ │ 🍞 Pain complet │ │
│ │ 🍎 Pommes x6 │ │
│ └──────────────────────┘ │
│ │
│ "3 produits restants" │
└──────────────────────────────┘
home_mode: |
# Soirée / weekend
┌──────────────────────────────┐
│ 👋 Bonjour, Sophie │
│ │
│ 📊 Cette semaine : │
│ ████████░░ 78% produits OK │
│ │
│ 🔍 Derniers scans : │
│ ✅ Biscuit Choco │
│ ⚠️ Sauce Curry │
│ │
│ [Scanner] [Mes listes] │
└──────────────────────────────┘
first_time: |
# Dashboard vide, guidé
┌──────────────────────────────┐
│ 🎉 Prêt à commencer ! │
│ │
│ 📷 Scannez votre premier │
│ produit │
│ │
│ [Commencer →] │
└──────────────────────────────┘
```
---
### FLOW 5 : Listes intelligentes
```yaml
flow_lists:
trigger: "Onglet 📋 Listes"
default_view: |
┌──────────────────────────────┐
│ Listes + Nouvelle │
├──────────────────────────────┤
│ ┌────────────────────────┐ │
│ │ 🛒 Courses semaine │ │
│ │ 8 produits • 3 achetés │ │
│ │ ██████░░░░ 37% │ │
│ └────────────────────────┘ │
│ ┌────────────────────────┐ │
│ │ 🎉 Anniv Lucas │ │
│ │ 12 produits • 0 acheté │ │
│ └────────────────────────┘ │
└──────────────────────────────┘
list_detail: |
┌──────────────────────────────┐
│ ← Courses semaine ⋮ │
├──────────────────────────────┤
│ 🔍 Filtrer par rayon │ ← Chips horizontaux
│ [Tous] [Frais] [Épicerie] │
│ │
│ ☐ Lait demi-écrémé ✅ │
│ ☐ Pain complet ⚠️ │ → Attention allergies
│ ☑ Pommes x6 ✅ │ → Coché = acheté
│ ☐ Yaourt nature ✅ │
│ ☐ Sauce tomate ❌ │ → Interdit (allergène)
│ │
│ [Tout décocher] [Partager] │
└──────────────────────────────┘
swipe_actions:
swipe_right: "Cocher/décocher (acheté)"
swipe_left: "Supprimer de la liste (avec undo 5s)"
smart_features:
auto_categorize: "Détection rayon automatique (frais, épicerie, etc.)"
allergy_alert: "Icône ⚠️/❌ visible directement sur la ligne"
merge_lists: "Fusion de plusieurs listes en une"
share: "Partage via lien ou PDF (sms, email, etc.)"
```
---
### FLOW 6 : Suivi & Historique
```yaml
flow_tracking:
trigger: "Onglet 📊 Suivi"
layout: |
┌──────────────────────────────┐
│ Suivi Période ▼│
├──────────────────────────────┤
│ │
│ ┌────────────────────┐ │
│ │ 78% produits OK │ │ ← Cercle de progression
│ │ cette semaine │ │ (donut chart)
│ └────────────────────┘ │
│ │
│ 📈 Évolution (graphique) │
│ ░░░░░░░░░░░░░░░░░░░░ │ ← Sparkline ou mini graph
│ │
│ 🏷️ Top allergènes détectés │
│ 🥜 Noisettes : 4 fois │
│ 🥛 Lactose : 2 fois │
│ │
│ 📋 Historique récent │
│ ┌────────────────────────┐ │
│ │ ✅ Biscuit Choco │ │
│ │ Il y a 2h │ │
│ ├────────────────────────┤ │
│ │ ⚠️ Sauce Curry │ │
│ │ Hier │ │
│ └────────────────────────┘ │
└──────────────────────────────┘
time_filters:
- {id: "week", label: "Cette semaine"}
- {id: "month", label: "Ce mois"}
- {id: "year", label: "Cette année"}
- {id: "all", label: "Tout"}
empty_state: |
┌──────────────────────────────┐
│ 📊 │
│ Aucune statistique │
│ Scannez vos premiers │
│ produits ! │
│ │
│ [Scanner →] │
└──────────────────────────────┘
```
---
### FLOW 7 : Gestion des erreurs & cas limites
```yaml
flow_errors:
# ── CAS 1 : Produit non trouvé en base ──
product_not_found:
trigger: "Code-barres scanné, mais absent de la base"
layout: |
┌──────────────────────────────┐
│ 📷 │
│ │
│ "Produit non reconnu" │
│ │
│ "Photographiez l'étiquette │
│ pour nous aider à │
│ l'identifier" │
│ │
│ ┌──────────────────────┐ │
│ │ [Zone photo étiquette]│ │
│ └──────────────────────┘ │
│ │
│ [Prendre une photo] │
│ [Saisie manuelle] │
│ [Annuler] │
└──────────────────────────────┘
actions:
photo_label:
camera: "Prendre photo étiquette (ingrédients)"
crop_guide: "Cadrez la liste d'ingrédients"
on_success: "Merci ! Nous analysons votre photo. Le produit sera ajouté sous 24h."
manual_entry:
fields: ["Nom produit", "Marque", "Code-barres (pré-rempli)"]
on_submit: "Produit enregistré localement. Analyse basique des ingrédients si fournis."
# ── CAS 2 : Pas de connexion ──
no_connection:
detection: "Vérification avant chaque scan"
behavior: |
Mode dégradé transparent :
- Scan fonctionne avec base locale (produits déjà scannés)
- Nouveau produit : message "Connexion requise pour les nouveaux produits"
- Listes : synchronisation différée
- Pas de popup bloquante, un bandeau discret en haut
banner: |
┌──────────────────────────────┐
│ 📡 Mode hors-ligne │ ← Bandeau 40dp, fond #FEF5E7
│ Produits déjà scannés dispo. │ Non bloquant
└──────────────────────────────┘
# ── CAS 3 : OCR illisible ──
ocr_unreadable:
trigger: "L'OCR n'a pas pu lire les ingrédients sur la photo"
layout: |
┌──────────────────────────────┐
│ 💡 │
│ │
│ "L'image est trop floue" │
│ │
│ "Essayez avec plus de │
│ lumière ou rapprochez-vous │
│ de l'étiquette" │
│ │
│ [Réessayer] │
│ [Saisie manuelle] │
│ [Ignorer] │
└──────────────────────────────┘
tips_displayed:
- "Placez le produit sur une surface plane"
- "Évitez les reflets"
- "Utilisez le flash si nécessaire"
# ── CAS 4 : Permissions refusées ──
permission_denied:
camera:
rationale_dialog: |
Titre: "Accès à l'appareil photo"
Message: "L'appareil photo est nécessaire pour scanner les codes-barres."
Boutons: [Aller aux Réglages] [Pas maintenant]
fallback: "Saisie manuelle du code-barres toujours disponible"
notification:
rationale: |
Titre: "Notifications de sécurité"
Message: "Recevez les alertes uniquement en cas d'allergène critique détecté."
Boutons: [Activer] [Plus tard]
behavior: "Fonctionnalités non-bloquantes sans notification"
```
---
### FLOW 8 : Profils famille
```yaml
flow_family:
trigger: "Onglet 👤 Famille"
layout: |
┌──────────────────────────────┐
│ Ma famille │
├──────────────────────────────┤
│ │
│ ┌────────────────────────┐ │
│ │ 👩 Sophie (moi) │ │ ← Profil principal
│ │ Allergies: - │ │
│ └────────────────────────┘ │
│ ┌────────────────────────┐ │
│ │ 👧 Julie, 7 ans │ │
│ │ ⚠️ Noisettes, arachides│ │
│ │ ❌ Anaphylaxie arachide│ │
│ └────────────────────────┘ │
│ ┌────────────────────────┐ │
│ │ 👦 Lucas, 10 ans │ │
│ │ ⚠️ Lactose │ │
│ └────────────────────────┘ │
│ │
│ [+ Ajouter un membre] │
└──────────────────────────────┘
profile_detail: |
┌──────────────────────────────┐
│ ← Julie │
├──────────────────────────────┤
│ Photo [📷] │
│ Prénom [Julie] │
│ Âge [7] │
│ │
│ ALLERGIES │
│ ┌────────────────────────┐ │
│ │ 🥜 Noisettes [⚠️] │ │ ← Niveau : traces (⚠️)
│ │ 🥜 Arachides [❌] │ │ ou critique (❌)
│ └────────────────────────┘ │
│ [+ Ajouter une allergie] │
│ │
│ ALERTES │
│ ☑ Alerte critique (anaphyl.)│
│ ☐ Alerte traces │
└──────────────────────────────┘
allergen_selection: |
# Sélecteur visuel (pas de liste texte)
Grille 3x5 de :
┌───┐ ┌───┐ ┌───┐
│🥜│ │🥛│ │🍞│
└───┘ └───┘ └───┘
┌───┐ ┌───┐ ┌───┐
│🦐│ │🥚│ │🐟│
└───┘ └───┘ └───┘
...
Tap once: sélectionne (⚠️ traces/intolérance)
Tap again: critique (❌ allergie sévère)
Tap third: désélectionne
Couleur fond change selon sélection (⚠️ #FEF5E7, ❌ #FDEDEC)
```
---
### FLOW 9 : Notifications
```yaml
flow_notifications:
# Règle : seules les notifications de SÉCURITÉ sont push
# Tout le reste est in-app uniquement
push_notifications:
security_alert:
trigger: "Allergène critique détecté pour un membre de la famille"
title: "⚠️ Attention : {allergène} détecté"
body: "{nom_produit} contient {allergène}. Interdit pour {membre}."
tap_action: "Ouvre la fiche produit avec le verdict danger"
new_alternative:
trigger: "Nouveau produit alternatif disponible"
title: "💡 Alternative trouvée"
body: "Un produit similaire sans {allergène} est disponible."
frequency: "Max 1/semaine"
user_control: "Désactivable dans Réglages"
in_app_notifications:
types:
- "Rappel liste de courses (si produits restants)"
- "Résumé hebdomadaire (chaque lundi matin)"
- "Nouvelle fonctionnalité"
display: "Centre de notifications intégré (cloche dans top bar)"
```
---
## ⚡ PERFORMANCE & ANIMATIONS
### 5.1 Spécifications de performance perçue
| Métrique | Cible | Méthode |
|----------|-------|---------|
| Temps d'ouverture scanner | < 300ms | Pré-initialiser la caméra en arrière-plan |
| Affichage résultat scan | < 500ms | Skeleton screen immédiat, données async |
| Transition entre écrans | 200-300ms | Courbes ease-out, jamais > 400ms |
| Scroll FPS | 60fps constants | RecyclerView + pagination |
| Taille APK | < 25 Mo | Optimisation ressources, ProGuard |
### 5.2 Animations standardisées
```yaml
animations:
screen_transition:
push: "Slide from right, 250ms, decelerate"
pop: "Slide to right, 200ms, accelerate"
fab_morph:
to_scanner: "ContainerTransform (Material), 300ms"
scan_success:
reticule_pulse: "scale 1.0 → 1.08 → 1.0, 200ms, ease-in-out"
haptic: "Vibration 15ms, légère"
verdict_appear:
type: "Slide up + fade in"
duration: 250ms
stagger: "Contenu principal 0ms, actions +50ms chaque"
list_check:
type: "Strikethrough animé + scale down + coche apparition"
duration: 300ms
skeleton:
type: "Shimmer gradient animé"
gradient: "transparent → rgba(255,255,255,0.4) → transparent"
duration: "1.5s, boucle infinie"
```
### 5.3 Transitions partagées (Shared Elements)
```yaml
shared_elements:
product_card_to_detail:
elements:
- product_image: "Corner radius animé (8dp → 16dp)"
- product_name: "Position et taille animées"
duration: 300ms
interpolator: "FastOutSlowIn (Material standard)"
```
---
## ♿ ACCESSIBILITÉ (OBLIGATOIRE)
### 6.1 Checklist WCAG 2.1 AA
```yaml
accessibility_checklist:
contrast:
text_normal: "Ratio ≥ 4.5:1"
text_large: "Ratio ≥ 3:1"
ui_components: "Ratio ≥ 3:1"
# Validation : test avec Accessibility Scanner (Android)
color_blindness:
deuteranopia: "Vert → Orange OK" # Distinction testée
protanopia: "Vert → Orange OK"
tritanopia: "Rare, mais testé"
# Règle : icônes + formes + couleurs, jamais couleur seule
touch_targets:
min_size: "48dp x 48dp" # Obligatoire
min_spacing: "8dp entre zones tactiles"
screen_reader:
talkback:
- "Tout élément interactif a un contentDescription"
- "Les images décoratives ont contentDescription = null"
- "Les changements d'état sont annoncés (verdict, chargement)"
- "Ordre de focus logique (gauche→droite, haut→bas)"
announcements:
scan_complete: "Produit scanné. {nom_produit}. {verdict}."
verdict_change: "Attention : verdict modifié. {nouveau_verdict}."
loading: "Analyse en cours, veuillez patienter."
dynamic_text:
scale: "Jusqu'à 200% sans perte de contenu"
implementation: "sp (Android) / Dynamic Type (iOS)"
testing: "Testé avec la plus grande taille système"
keyboard_navigation:
android: "DPad et clavier externe Bluetooth"
focus_indicators: "Visibles (anneau #2ECC71, 2dp)"
```
### 6.2 Icônes & formes (Système daltonien)
```
VERT (#2ECC71) ORANGE (#E67E22) ROUGE (#E74C3C)
───── ──────── ──────
[CERCLE] [TRIANGLE] [LOSANGE]
✅ checkmark ⚠️ exclamation ❌ croix
Jamais l'un sans l'autre : forme + couleur + icône.
```
---
## 📐 GRILLE & LAYOUT
### 7.1 Système de grille
```yaml
grid_system:
type: "Grille 4 colonnes (mobile)"
margins: "16dp gauche/droite"
gutter: "12dp entre colonnes"
responsive:
phone_portrait: "4 colonnes, margins 16dp"
phone_landscape: "6 colonnes, margins 24dp"
tablet: "8 colonnes, margins 32dp, max-width 840dp"
```
### 7.2 Composants standardisés
```yaml
components:
product_card_compact:
height: 88dp
layout: |
┌──────────────────────────────┐
│ [IMG] Nom produit │
│ 48dp Marque • Poids │
│ ┌────────┐ │
│ │ ✅ │ ← Verdict │
│ └────────┘ miniature│
└──────────────────────────────┘
verdict_banner:
height: 56dp
padding: 16dp horizontal, 12dp vertical
radius: 12dp
icon_size: 24dp
background: "Dépend du verdict (voir couleur de fond)"
action_button:
height: 48dp
radius: 12dp
padding: 16dp horizontal
text: "Button style (15sp, weight 600)"
variants:
primary: "Fond #2D3436, texte blanc"
secondary: "Fond transparent, bordure #DFE6E9, texte #2D3436"
danger: "Fond #E74C3C, texte blanc"
chip_filter:
height: 32dp
radius: 16dp (pill)
padding: 8dp horizontal
selected: "Fond #2D3436, texte blanc"
unselected: "Fond #F5F5F0, texte #636E72"
input_field:
height: 56dp
radius: 12dp
background: "#FFFFFF"
border: "1dp #DFE6E9"
focus_border: "2dp #2ECC71"
label: "caption style, animé vers le haut au focus"
```
---
## 🗄️ SPÉCIFICATIONS TECHNIQUES
### 8.1 Architecture recommandée
```yaml
architecture:
pattern: "MVVM + Clean Architecture"
layers:
presentation:
- "Jetpack Compose (Android) / SwiftUI (iOS)"
- "ViewModel par écran"
- "StateFlow / @Published pour état UI"
domain:
- "UseCases (ScanProductUseCase, GetAlternativesUseCase...)"
- "Modèles métier (Product, Allergen, FamilyMember...)"
data:
- "Repository pattern"
- "Room (base locale) pour historique et listes"
- "Retrofit (API distante) pour base produits"
- "DataStore pour préférences utilisateur"
key_dependencies:
android:
camera: "CameraX (Google officiel)"
barcode: "ML Kit Barcode Scanning (Google)"
ocr: "ML Kit Text Recognition"
navigation: "Compose Navigation"
di: "Hilt"
ios:
camera: "AVFoundation"
barcode: "Vision Framework"
ocr: "Vision Framework"
navigation: "NavigationStack"
```
### 8.2 Flux de données Scan
```kotlin
// Flux simplifié pour le scan
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) : 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
}
```
---
## 🧪 TESTS UX (Checklist validation)
```yaml
validation_checklist:
user_testing:
- "5 utilisateurs, scenario 'scanner un produit dangereux'"
- "Mesurer : temps pour comprendre le verdict"
- "Objectif : < 2 secondes pour identifier le danger"
- "Test daltonien : 1 utilisateur daltonien minimum"
edge_cases:
- "Scan rapide de 10 produits d'affilée"
- "Rotation écran pendant le scan"
- "Appel téléphonique interrompt le scan"
- "Batterie faible (mode économie d'énergie)"
- "Stockage presque plein"
accessibility_audit:
- "Test avec TalkBack activé, parcours complet"
- "Test avec taille de texte maximale"
- "Contraste vérifié avec Accessibility Scanner"
```
---
## 📋 DÉVELOPPEMENT V1 Scope minimal
```yaml
v1_mvp_scope:
must_have:
- module: "Scanner code-barres + verdict immédiat"
- module: "4 onglets navigation"
- module: "Profils famille (multi-membres avec allergies)"
- module: "Listes de courses basiques"
- module: "Système de couleurs feu tricolore complet"
- module: "Accessibilité de base (contraste + TalkBack)"
nice_to_have_v1:
- module: "OCR ingrédients sur photo"
- module: "Dashboard contextuel (heure/lieu)"
- module: "Alternatives intelligentes"
- module: "Suivi statistiques"
v2_planned:
- "Partage liste en temps réel (collaboratif)"
- "Scanner rayon (reconnaissance multiple)"
- "Intégration drive/click&collect"
- "Badge / scoring produit personnalisé"
```
---
## 🎨 FICHIERS À PRODUIRE
```
assets/
├── design_system/
│ ├── colors.xml / Colors.xcassets
│ ├── typography.xml / Typography.swift
│ ├── shapes.xml / Shapes.swift
│ └── icons/ (SVG export 24dp, 48dp)
├── screens/
│ ├── onboarding_1_welcome.png
│ ├── onboarding_2_allergies.png
│ ├── onboarding_3_goal.png
│ ├── onboarding_4_scan_tuto.png
│ ├── scan_active.png
│ ├── scan_skeleton.png
│ ├── verdict_ok.png
│ ├── verdict_warning.png
│ ├── verdict_danger.png
│ ├── product_detail.png
│ ├── lists_empty.png
│ ├── lists_detail.png
│ ├── tracking.png
│ ├── family_profiles.png
│ └── family_member_detail.png
├── components/
│ ├── fab_spec.png
│ ├── bottom_nav_spec.png
│ ├── verdict_banner_variants.png
│ ├── product_card_compact.png
│ └── error_states_all.png
└── flows/
├── flow_scan_complete.pdf
├── flow_onboarding.pdf
└── flow_error_handling.pdf
```
---
**Ce document constitue la spécification UX/UI de référence. Toute ambiguïté doit être remontée avant implémentation. Les principes fondateurs (section 1) prévalent sur les détails d'implémentation en cas de conflit.**