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

13 KiB
Raw Blame History


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.


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)

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)

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)

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)

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)

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

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)

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)

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)