Files
_Assistant_Lead_Tech/knowledge/ux/patterns/general.md
T
MaksTinyWorkshop 81fde91259 docs(knowledge): capitalisation workflow + ux — intégration du triage local (mai-juin 2026)
Triage et intégration des propositions workflow et UX du buffer 95_a_capitaliser.md.

WORKFLOW :
- risques/story-tracking.md : 24 risques de suivi de story (enabler AC non-bloquant,
  tests plumbing vs scénario, reformat hors scope, xit sans story de suivi, re-scope mid-PR,
  statut migré non vérifié, périmètre auto-déclaré vs git diff, composant/page livré sans
  câblage — reciblages venus de backend #21 et frontend #257)
- patterns/general.md : audit cartographique pré-chantier, Go/No-Go par lot, sub-agent review
  fresh-context, sweep read-only délégué (#156), revue adverse de spec, audit-first migration

UX (domaine amorcé — était vide) :
- patterns/general.md : 9 patterns (mount-based read, fiche détail single-scroll, FAB étendu,
  ligne de contexte filtres, état read-only caché, avatar par hash, audit a11y touch targets)
- risques/general.md : 5 risques (bouton retour dans ScrollView #108, lien sans handler,
  wording cross-écran divergent, token sans détection, padding multi-écrans)
- READMEs ux/workflow mis à jour

Vérifié : aucun doublon d'ancre/titre, fichiers racine 40_/90_ non modifiés (propositions
réservées pour validation séparée). Source 95_ non purgée (purge en fin de capitalisation).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 15:48:53 +02:00

206 lines
13 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.
---
title: UX — Patterns validés : Général
domain: ux
bucket: patterns
tags: [mobile, navigation, cta, fab, liste, etat, i18n, a11y, notifications]
applies_to: [design, implementation, review]
severity: medium
validated_on: 2026-06-25
source_projects: [app-alexandrie]
---
# UX — Patterns validés : Général
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/ux/patterns/README.md` pour l'index complet.
---
<a id="pattern-ux-mark-read-au-mount"></a>
## Pattern : Marquage "lu" au mount de l'écran cible (mount-based, pas click-based)
- Objectif : éviter les "clics accidentels qui consomment" une notification — marquer comme lu uniquement quand l'utilisateur a réellement vu le contenu.
- Contexte : liste de notifications qui navigue vers un écran cible (thread, conversation, achievements).
- Quand l'utiliser : tout flux notification → écran cible où le clic ne prouve pas la lecture.
- Quand l'éviter : action où le clic EST l'acte de lecture (ex. dépliage inline).
### Description
Mauvais pattern : `onPress(item) → markRead(item.id); navigate(target)` — un clic accidentel puis back immédiat consomme la notif sans qu'elle ait été vue ; la notif disparaît avant que l'écran cible monte.
Bon pattern : déléguer le mark-read à l'écran cible, déclenché au mount via query param (`?notificationId=`). Un hook `useMarkNotificationReadOnMount()` lit le param et appelle `markRead` dans un `useEffect`, avec un guard d'idempotence (`alreadyMarkedRef`) contre les re-renders (focus refresh, hot reload).
Comportements obtenus : cas nominal (tap → écran monte → lu), cas annulation (tap → back avant mount → reste non lu), deep-link analytics-friendly et testable.
- Validé le : 28-05-2026 — app-alexandrie (IA-v2.8 AC4)
---
<a id="pattern-ux-fiche-detail-immersive"></a>
## Pattern : Fiche détail — single-scroll immersif, méta-infos en tête, chip drill-down
- Objectif : structurer une fiche de contenu/produit/praticien/événement sans "rooms vides" ni dispersion des infos.
- Contexte : fiche détail mobile/web avec contenu de profondeur variable.
- Quand l'utiliser : fiche détail à contenu court ou partiellement livré.
- Quand l'éviter : fiche à forte profondeur réelle (chapitres + avis nombreux) où les tabs sont justifiés.
### Trois patterns indépendants
1. **Single-scroll vs tabs Udemy/Masterclass** : pour une fiche courte (pas de chapitres, pas d'avis livrés), préférer un single-scroll vertical. Les tabs créent des "rooms vides" qui sapent la crédibilité (onglet "Chapitres" vide, "Avis" avec un faux 5 étoiles). Réintégrer les sections en single-scroll est plus simple que remplir artificiellement des tabs.
2. **Méta-infos groupées en tête** : regrouper toutes les méta-infos dans un bloc dense entre le hero et le contenu (overline catégorie > titre H1 > InfoChips > ligne progression compacte). Évite la dispersion ; donne un signal fort sur "ce qu'est ce contenu" avant de plonger. La progression tient sur une ligne (icon + label + date), c'est une info légère.
3. **Chip cliquable drill-down** : plutôt qu'une section vide (avis sans backend, stats peu lues), exposer l'info en chip dans la zone méta (état neutre si vide : "☆ Aucun avis") qui devient l'entrée vers une page dédiée. Évite les empty states verbeux.
- Validé le : 29-05-2026 — app-alexandrie (ux-cleanup-7)
---
<a id="pattern-ux-fab-vs-sticky-bottom"></a>
## Pattern : Préférer le FAB étendu au sticky bottom plein-largeur sur écran à nav persistante
- Objectif : éviter le piège de positionnement à 3 couches synchronisées (sticky `bottom` + paddingBottom ScrollView + safeArea) du CTA sticky.
- Contexte : écran avec nav persistante (TabBar/BottomBar en `position: absolute, bottom: 0`) ET un CTA principal contextuel.
- Quand l'utiliser : écran à nav persistante avec CTA (commencer, reprendre, créer).
- Quand l'éviter : écran SANS nav persistante (modal, full-screen) où le sticky devient naturel ; ou CTA à label très long ; ou page de paiement validée maquette par maquette.
### Description
Un sticky bottom plein-largeur exige de re-synchroniser 3 hauteurs à chaque évolution de la nav, et un drift → le CTA chevauche la TabBar (bug observé sur device, invisible en tests Jest). Préférer un **FAB étendu Material 3** (icon + label, bottom-right) qui se positionne via UNE seule valeur (`bottom = NAV_HEIGHT + spacing + safeArea`) gérée par le composant lui-même, n'ajoute pas de surface visuelle redondante, et est immune aux évolutions de la nav.
Anti-pattern : `<View position="absolute" bottom={const}>` avec un Button plein-largeur — semble simple mais oblige à recalculer 3 hauteurs à chaque changement de nav.
- Validé le : 29-05-2026 — app-alexandrie (ux-cleanup-7, le sticky a généré ~3 cycles de bugs sur device)
---
<a id="pattern-ux-ligne-contexte-sous-filtres"></a>
## Pattern : Ligne de contexte sous les filtres de liste
- Objectif : permettre à l'utilisateur de comprendre instantanément ce qu'il regarde (combien, quel filtre) et pourquoi la liste est vide.
- Contexte : toute liste filtrée (annuaire, feed, bibliothèque).
- Quand l'utiliser : liste avec filtres et/ou recherche.
- Quand l'éviter : liste sans filtre ni recherche.
### Description
Afficher une ligne de contexte juste sous les filtres et au-dessus du premier résultat. Elle : compte les résultats (singulier/pluriel correct), mentionne le filtre actif ("Ma cohorte • 5 membres"), contextualise la query ("Aucun résultat pour « pnl »"), explicite le vide ("Aucun membre dans votre cohorte" plutôt qu'une chaîne générique), et ne s'affiche PAS pendant le 1er chargement (count=0 + isLoading) pour éviter le flash "Aucun X".
Implémentation type : helper pur `formatXxxContext({filter, count, query, isLoading}) → string` (testable à 100% en node) + composant léger en `textSecondary` 13px.
Anti-pattern : empty state global (centré, gros) sur une liste filtrée — il masque les filtres alors que l'utilisateur veut comprendre POURQUOI c'est vide.
- Validé le : 29-05-2026 — app-alexandrie (ux-cleanup-9)
---
<a id="pattern-ux-cta-toggle-differencie"></a>
## Pattern : CTA toggle différencié au-delà du libellé
- Objectif : rendre l'état d'un CTA toggle (Suivre/Suivi, Souscrire/Souscrit) perceptible au premier regard.
- Contexte : CTA qui bascule entre deux états (action / état "fait").
- Quand l'utiliser : tout bouton à état binaire persistant.
- Quand l'éviter : bouton d'action one-shot sans état.
### Description
Deux éléments visuels distincts au-delà du libellé :
- **État inactif (action disponible)** : ghost button (fond transparent, bordure + texte `colors.primary`, libellé verbe infinitif "Suivre").
- **État actif ("fait")** : filled tinted (fond `${colors.primary}1F` ≈ 12% d'alpha, bordure transparente, icône remplie alignée gauche, libellé participe passé "Suivi" — l'icône porte l'info, pas une coche dans le texte).
**Pourquoi `${color}1F` plutôt qu'`opacity: 0.12`** : `opacity` fade TOUT l'enfant (icône + label), `${color}1F` n'applique l'alpha qu'au background — icône et label gardent leur saturation pleine.
A11y obligatoire : `accessibilityState={{ selected: isActive }}` + `accessibilityLabel` explicitant état + action.
Anti-pattern : différencier UNIQUEMENT par le libellé (Suivre → Suivi ✓) — le ✓ Unicode est faible visuellement et inaccessible.
- Validé le : 29-05-2026 — app-alexandrie (ux-cleanup-9 AC3)
---
<a id="pattern-ux-wordings-inclusifs-i18n"></a>
## Pattern : Table i18n de wordings inclusifs
- Objectif : centraliser les wordings pour qu'ils soient auditables et inclusifs, et empêcher le composant d'inventer ses propres libellés.
- Contexte : app francophone avec wordings genrés potentiels (notifications, messages).
- Quand l'utiliser : tout domaine produisant des wordings adressés à l'utilisateur.
- Quand l'éviter : —
### Structure
Un fichier `i18n/<domain>.ts` par domaine exposant : un `Record<TypeEnum, string>` interne `WORDING_FALLBACK` (générique), une fonction `<domain>Wording(type, ctx?)` qui combine type + contexte, et un `.spec.ts` qui vérifie fallbacks, cas avec contexte, dégradation gracieuse, et la règle d'inclusivité via un garde-fou regex anti-régression (`expect(wording).not.toMatch(/mentionnée(?![\w·])/)`).
### Règles d'inclusivité française
1. **Point milieu `·e`** pour les participes : "mentionné·e", "invité·e".
2. **Nom commun neutre** quand possible : "abonné" plutôt que "follower".
3. **Passé composé impersonnel** sans acteur nommé : "Nouvelle réponse à votre fil" plutôt que "Vous avez été répondu".
4. **Nommer l'acteur quand on l'a** : "Bob a répondu à votre fil" (plus actionnable).
Anti-patterns : wording inline dans le composant (inaudit­able) ; wording dans le push payload backend sans miroir dans la table mobile (divergence lockscreen vs in-app) ; test "smoke" qui vérifie juste que le wording n'est pas vide (rate les régressions de genre).
- Validé le : 29-05-2026 — app-alexandrie
---
<a id="pattern-ux-etat-readonly-cacher-vs-griser"></a>
## Pattern : État read-only — cacher l'interactif plutôt que griser
- Objectif : éviter le grisage avec placeholder ("Action désactivée"), un anti-pattern d'invitation qui fait essayer puis échouer l'utilisateur.
- Contexte : élément interactif (composer DM, bouton "Acheter", formulaire) indisponible pour cause d'état métier (lecture seule, compte suspendu, plan inactif).
- Quand l'utiliser : indisponibilité durable liée à un état métier.
- Quand l'éviter : désactivation transitoire pendant un async court (préférer un loading).
### Description
Ne pas griser — **cacher** l'élément et afficher en remplacement un **bandeau neutre** qui explique l'état.
Pourquoi cacher est meilleur : pas d'invitation à essayer ; explicite > implicite (le bandeau explique POURQUOI) ; a11y (`accessibilityRole="alert"` annonce l'état au mount, un input `editable={false}` ne signale rien) ; visuel (1 zone au lieu de composer grisé + bandeau redondant).
Pattern de code : `{isReadOnly ? <ReadOnlyBanner message="..." /> : <Composer ... />}`. Ton neutre (`textSecondary` / `surfaceContainerLow`), pas `error` (réservé aux vraies erreurs réversibles).
Anti-patterns : placeholder "Envoi désactivé" dans un TextInput non éditable ; `opacity: 0.5` (reste cliquable visuellement) ; bandeau ET composer affichés ensemble.
- Validé le : 29-05-2026 — app-alexandrie (ux-cleanup-11 AC4)
---
<a id="pattern-ux-avatar-pastel-buckets-distincts"></a>
## Pattern : Avatar coloré par hash — buckets distincts du fallback
- Objectif : éviter qu'un utilisateur tombant dans un bucket trop neutre soit confondu avec un compte supprimé.
- Contexte : avatar coloré "hash du nom" avec un bucket fallback (compte supprimé/anonyme).
- Quand l'utiliser : avatars générés par hash.
- Quand l'éviter : avatars uploadés / sans fallback identitaire.
### Description
Ne mettre dans le pool hashable QUE des accents colorés (`primary`/`secondary`/`tertiary` + leurs containers). Le fallback (`outlineVariant`) reste réservé aux fallbacks identitaires (compte supprimé, handle vide).
Anti-pattern : inclure un token "surface" très clair (background-like) dans le pool — il ressemble au fallback grisé en light theme. Validation : tester visuellement les 8-12 tokens du pool sous light + dark ; retirer tout token proche du fallback.
- Validé le : 30-05-2026 — app-alexandrie (ux-cleanup-11 M1)
---
<a id="pattern-ux-audit-a11y-touch-targets"></a>
## Pattern : Audit a11y des touch targets mobile RN
- Objectif : garantir des cibles tactiles conformes (EAA 2025) sans linter a11y RN mainstream.
- Contexte : app mobile React Native / Expo avec composants interactifs denses.
- Quand l'utiliser : audit a11y ou story de polish touch targets.
- Quand l'éviter : —
### Cible et méthode
Cible : 44×44pt iOS (Apple HIG) / 48×48dp Android (Material). Méthode (grep + calcul, pas de linter) : grep tous les `Pressable`/`TouchableOpacity`/`onPress` ; pour chacun, taille effective = `max(width, iconSize + paddingH*2) × max(height, fontSize + paddingV*2)` + `hitSlop` ; marquer non conforme si une dimension < 44pt ; rapport ordonné par gravité.
### Décision hitSlop vs padding
| Choisir... | Quand... |
|---|---|
| `hitSlop` | Composant qui DOIT rester visuellement compact (chip dense, icône + label adjacent, pill en liste serrée), réutilisé multi-écrans (changer le padding casse les callers), pas de feedback visuel attendu au-delà de la cible (chevron, lien "Voir tout") |
| `padding` / `minHeight: 44` | Composant qui peut grossir sans gêne (CTA EmptyState, bouton primaire), feedback visuel attendu sur la zone élargie, composant isolé sans dépendance layout serrée |
Syntaxe : `hitSlop={{ top: N, bottom: N, left: M, right: M }}` — N pour atteindre exactement 44pt en hauteur ; `left/right` plus petits (4-8) si chips côte-à-côte pour éviter l'overlap. Limite acceptée : audit visuel + grep seul (cf. risque associé dans `ux/risques/general.md`).
- Validé le : 31-05-2026 — app-alexandrie (ux-cleanup-14)