41 KiB
41 KiB
📱 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
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
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)
/* 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
// 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
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)
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
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
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
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
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
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
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
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
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)
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
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
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
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
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
// 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)
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
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.