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>
This commit is contained in:
MaksTinyWorkshop
2026-06-25 15:48:53 +02:00
parent 5f5c87296e
commit 81fde91259
7 changed files with 1089 additions and 6 deletions
+205
View File
@@ -0,0 +1,205 @@
---
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)