mirror of
https://github.com/MaksTinyWorkshop/_Assistant_Lead_Tech
synced 2026-06-28 01:53:40 +02:00
5f5c87296e
Triage et intégration des propositions frontend du buffer 95_a_capitaliser.md (lot local RL799_V2/Vue3 + app-alexandrie/RN-Expo, mai-juin 2026). ~73 entrées intégrées sur knowledge/frontend/ + 1 nouveau fichier, dont : - patterns/state.md : race-token partagé latest-wins (fusion 3 props), capture sync anti-race, event bus timestamp, clé cache composite, état dérivé = computed - risques/state.md : 9 risques Zustand/store (fetchId reset, useRef remount, re-fetch infini sur [], flag optimiste écrasé, cache détail/liste stale, latch sans reset, :key index) - patterns/navigation.md : Expo Router (tab bar, useFocusEffect, Href typé, routing pur fusionné) - patterns/general.md : helpers temps purs, composants génériques + skeleton, fail-fast, touch target - risques/general.md : 24 risques (sweep statique, filtre client liste paginée, hooks avant return, a11y VoiceOver/disabled, redirection allowlist, RangeError toISOString, section i18n...) - design-tokens (cluster theming light/dark MD3), tests, performance, react-native, nextjs - NOUVEAU risques/responsive.md (gating par capacité d'input + checklist régressions mobile) - READMEs patterns/risques mis à jour Doublons inter-fichiers évités (vérifié : aucune ancre dupliquée introduite). Rejets (doublons 91/9/87), reciblages workflow (156/257) et bloc 32 (CLAUDE projet) non intégrés ici. Source 95_a_capitaliser.md non purgée (purge en fin de capitalisation complète). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
746 lines
32 KiB
Markdown
746 lines
32 KiB
Markdown
# Frontend — Patterns : Général
|
||
|
||
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/frontend/patterns/README.md` pour l'index complet.
|
||
|
||
---
|
||
|
||
<a id="pattern-focus-visible-interne-overflow-clip"></a>
|
||
## Pattern : Focus visible interne pour champs sous overflow clip
|
||
|
||
### Synthèse
|
||
|
||
- **Objectif** : préserver le focus visible des champs `<input>` / `<select>` quand l'app applique `overflow-x: clip` sur ses conteneurs (PageShell, panel, layout) — le focus outline natif est dessiné **hors** de la box du champ et se fait clipper.
|
||
- **Contexte** : app mobile-first où `overflow-x: clip` (ou `hidden`) est fréquent sur les conteneurs pour empêcher le débordement horizontal.
|
||
- **Quand l'utiliser** : tout projet avec une chaîne de parents en `overflow: hidden|clip` autour des champs de saisie.
|
||
- **Quand l'éviter** : si la chaîne de parents n'a pas d'`overflow: hidden|clip` — l'outline natif suffit.
|
||
|
||
### Analyse
|
||
|
||
- **Avantages** :
|
||
- aucun pixel ne sort de la box, donc aucun clip possible
|
||
- `box-shadow: inset` + `border-color` est portable Chrome/Firefox/Safari
|
||
- **Limites / vigilance** :
|
||
- `outline-offset: -2px` marche sur Chrome mais le rendu varie : Firefox/Safari peuvent ignorer selon la combinaison `outline-style`
|
||
|
||
### Validation
|
||
|
||
- Validé le : 27-04-2026
|
||
- Contexte technique : CSS / mobile-first — RL799_V2
|
||
|
||
### Pattern correctif (par composant)
|
||
|
||
```css
|
||
.my-input:focus-visible,
|
||
.my-select:focus-visible {
|
||
outline: none;
|
||
border-color: var(--color-accent);
|
||
box-shadow: inset 0 0 0 1px var(--color-accent);
|
||
}
|
||
```
|
||
|
||
### Application globale au thème (recommandée pour mobile-first)
|
||
|
||
Plutôt que de répéter le pattern dans chaque composant, le pousser dans le fichier de thème global. Tous les inputs/selects/textareas en bénéficient automatiquement.
|
||
|
||
```css
|
||
/* Dans theme/<theme>.css ou globals.css, hors :root */
|
||
input[type='text']:focus-visible,
|
||
input[type='search']:focus-visible,
|
||
input[type='email']:focus-visible,
|
||
input[type='tel']:focus-visible,
|
||
input[type='url']:focus-visible,
|
||
input[type='number']:focus-visible,
|
||
input[type='password']:focus-visible,
|
||
input[type='date']:focus-visible,
|
||
input[type='datetime-local']:focus-visible,
|
||
input[type='time']:focus-visible,
|
||
input[type='month']:focus-visible,
|
||
input[type='week']:focus-visible,
|
||
select:focus-visible,
|
||
textarea:focus-visible {
|
||
outline: none;
|
||
border-color: var(--color-accent);
|
||
box-shadow: inset 0 0 0 1px var(--color-accent);
|
||
}
|
||
```
|
||
|
||
### Pourquoi sélecteurs d'attribut explicites et pas `input:focus-visible`
|
||
|
||
`input` couvre aussi `type='checkbox'`, `type='radio'`, `type='file'`, `type='range'`, `type='color'` qui ont leur propre design natif. La règle pourrait casser leur rendu (carrés or autour des cases à cocher, etc.). On liste explicitement les types texte/saisie.
|
||
|
||
### Conflits avec composants ayant leur propre `:focus`
|
||
|
||
Un composant qui a déjà `.my-input:focus { ... }` (sans `-visible`) gardera la priorité par spécificité (classe > tag). La règle globale ne joue que pour les composants qui n'ont rien défini → sûr à introduire en mid-projet.
|
||
|
||
---
|
||
|
||
<a id="pattern-restyle-input-date-sans-wrapper-js"></a>
|
||
## Pattern : Restyle global de `<input type="date">` sans wrapper JS
|
||
|
||
### Synthèse
|
||
|
||
- **Objectif** : aligner les inputs date HTML5 sur l'identité visuelle du thème via une règle CSS globale, sans wrapper JS custom.
|
||
- **Contexte** : projet avec un thème dark/light custom où les inputs date natifs (icône calendrier blanche, placeholder gris OS, popover light par défaut) cassent le design.
|
||
- **Quand l'utiliser** : 80 % des cas (audit log, formulaires admin, profils) où la datepicker custom serait du sur-engineering.
|
||
- **Quand l'éviter** :
|
||
- validation custom synchrone (range, dates blackout, format spécifique)
|
||
- format d'affichage différent (DD/MM vs MM/DD vs ISO)
|
||
- intégration profonde dans un design system
|
||
|
||
### Analyse
|
||
|
||
- **Avantages** :
|
||
- `color-scheme` + `accent-color` donnent un popover natif cohérent gratuitement
|
||
- filtre SVG pour teinter l'icône calendrier
|
||
- zéro JavaScript, zéro bundle additionnel
|
||
- **Limites / vigilance** :
|
||
- **Firefox** : `accent-color` respecté, mais pas de pseudo-element pour customiser le placeholder
|
||
- **Safari iOS** : popover sheet OS, peu personnalisable. Acceptable car cohérent avec le reste des UI iOS natives
|
||
- filtre `filter()` à calibrer pour matcher la couleur du thème — chaque thème nécessite son tuning
|
||
|
||
### Validation
|
||
|
||
- Validé le : 27-04-2026
|
||
- Contexte technique : CSS / inputs date HTML5 — RL799_V2
|
||
|
||
### Pattern minimal
|
||
|
||
```css
|
||
/* Toutes les variantes de pickers HTML5 */
|
||
input[type='date'],
|
||
input[type='datetime-local'],
|
||
input[type='time'],
|
||
input[type='month'],
|
||
input[type='week'] {
|
||
color-scheme: dark; /* ou 'light' selon le thème de l'app */
|
||
accent-color: var(--color-accent);
|
||
}
|
||
|
||
/* Place-holder OS (jj/mm/aaaa) — couleur soft */
|
||
input[type='date']::-webkit-datetime-edit-fields-wrapper,
|
||
input[type='datetime-local']::-webkit-datetime-edit-fields-wrapper {
|
||
color: var(--color-text-soft);
|
||
}
|
||
|
||
/* Une fois saisie, couleur normale */
|
||
input[type='date']:not(:placeholder-shown)::-webkit-datetime-edit-fields-wrapper {
|
||
color: var(--color-text-primary);
|
||
}
|
||
|
||
/* Icône calendrier teintée (filtre SVG noir → couleur d'accent soft) */
|
||
input[type='date']::-webkit-calendar-picker-indicator,
|
||
input[type='datetime-local']::-webkit-calendar-picker-indicator,
|
||
input[type='time']::-webkit-calendar-picker-indicator,
|
||
input[type='month']::-webkit-calendar-picker-indicator,
|
||
input[type='week']::-webkit-calendar-picker-indicator {
|
||
/* Calibrer pour matcher la couleur du thème */
|
||
filter: invert(70%) sepia(40%) saturate(450%) hue-rotate(5deg) brightness(95%);
|
||
cursor: pointer;
|
||
opacity: 0.85;
|
||
transition: opacity 0.15s ease;
|
||
}
|
||
```
|
||
|
||
### Anti-patterns
|
||
|
||
- Construire un mini calendrier JS custom pour gagner 5 % d'esthétique → effort énorme (a11y clavier, focus management, mobile, edge cases), bénéfice marginal
|
||
- Hardcoder les couleurs `filter()` au lieu d'utiliser des tokens du thème
|
||
- Restyler sans `color-scheme: dark/light` → le popover natif reste en mode clair sur thème sombre
|
||
|
||
---
|
||
|
||
<a id="pattern-ui-journaux-audit-logs"></a>
|
||
## Pattern : UI pour journaux / audit logs / timelines
|
||
|
||
### Synthèse
|
||
|
||
- **Objectif** : passer d'un rendu naïf en cards uniformes "acteur · code · cible (uuid) · metadata" à une lecture rapide pour l'admin en surveillance, sans toucher au backend.
|
||
- **Contexte** : tout projet finit par afficher un journal d'événements (audit, activité, historique, timeline) avec metadata variable.
|
||
- **Quand l'utiliser** : journal avec ≥ 10 types d'actions et besoin de scanner rapidement.
|
||
- **Quand l'éviter** : log technique brut destiné aux devs (un `<pre>` peut suffire).
|
||
|
||
### Analyse
|
||
|
||
- **Avantages** :
|
||
- 5 patterns combinables qui améliorent radicalement la scanabilité
|
||
- aucun changement backend (le DTO reste plat)
|
||
- **Limites / vigilance** :
|
||
- reset de l'état d'expansion à chaque rechargement (l'expansion est éphémère, pas une préférence durable)
|
||
|
||
### Validation
|
||
|
||
- Validé le : 27-04-2026
|
||
- Contexte technique : Vue 3 / CSS — RL799_V2 (Journal d'audit admin, 45+ types d'actions)
|
||
|
||
### 1. `<optgroup>` dérivé du préfixe label
|
||
|
||
Quand l'API retourne un catalogue d'actions avec convention `Catégorie — Libellé` (ex : `Soirée — annulation`, `Tenue — création`), dériver les groupes côté front au lieu de modifier le DTO.
|
||
|
||
```typescript
|
||
const groupsFromLabels = computed(() => {
|
||
const groups = new Map<string, Entry[]>();
|
||
for (const opt of catalog.value) {
|
||
const sep = opt.label.indexOf(' — ');
|
||
const category = sep > 0 ? opt.label.slice(0, sep) : 'Divers';
|
||
groups.set(category, [...(groups.get(category) ?? []), opt]);
|
||
}
|
||
return Array.from(groups.entries())
|
||
.sort(([a], [b]) => a.localeCompare(b, 'fr', { sensitivity: 'base' }));
|
||
});
|
||
```
|
||
|
||
```html
|
||
<select>
|
||
<option value="">Toutes les actions</option>
|
||
<optgroup v-for="[cat, opts] in groupsFromLabels" :label="cat" :key="cat">
|
||
<option v-for="o in opts" :value="o.value" :key="o.value">{{ o.label }}</option>
|
||
</optgroup>
|
||
</select>
|
||
```
|
||
|
||
Bénéfice : 1 seul clic pour filtrer (vs cascade 2 selects), accessible natif, zéro modif backend.
|
||
|
||
### 2. Code couleur sémantique par catégorie
|
||
|
||
Barre de couleur de 3 px à gauche de chaque card de la liste, mappée sur la catégorie de l'événement. Transforme un mur de cards uniformes en lecture instantanée.
|
||
|
||
```css
|
||
.log-item {
|
||
border-left: 3px solid var(--log-cat-color, var(--color-border));
|
||
}
|
||
.log-item--cat-soiree { --log-cat-color: var(--color-accent-primary); }
|
||
.log-item--cat-rgpd { --log-cat-color: var(--color-accent-danger); }
|
||
```
|
||
|
||
Pourquoi pas le fond complet : trop bruyant, perd la sobriété d'un journal admin. La barre latérale signale sans crier.
|
||
|
||
### 3. UUIDs rétrogradés en monospace soft
|
||
|
||
Les UUIDs / IDs techniques affichés en plein texte cassent la lecture humaine. Les détecter via regex et les rendre en font monospace + couleur soft + taille réduite, sans les masquer (utiles pour forensics).
|
||
|
||
```typescript
|
||
const UUID_RE = /[0-9a-f]{8}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{12}/gi;
|
||
```
|
||
|
||
```css
|
||
.uuid {
|
||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||
font-size: 0.78em;
|
||
color: var(--color-text-soft);
|
||
word-break: break-all;
|
||
}
|
||
```
|
||
|
||
### 4. Date relative + absolue en tooltip
|
||
|
||
Pour la lecture humaine, "il y a 5 min" / "hier" / "il y a 3 j" bat toujours "27 avril 2026 à 08:37". La date absolue reste accessible en `title` du `<time>` pour les forensics.
|
||
|
||
```typescript
|
||
const formatRelative = (iso: string) => {
|
||
const diff = (Date.now() - new Date(iso).getTime()) / 1000;
|
||
if (diff < 60) return 'à l\'instant';
|
||
if (diff < 3600) return `il y a ${Math.round(diff / 60)} min`;
|
||
if (diff < 86400) return `il y a ${Math.round(diff / 3600)} h`;
|
||
if (diff < 172800) return 'hier';
|
||
if (diff < 604800) return `il y a ${Math.round(diff / 86400)} j`;
|
||
return new Date(iso).toLocaleDateString('fr-FR', {
|
||
day: 'numeric', month: 'short', year: 'numeric',
|
||
});
|
||
};
|
||
```
|
||
|
||
### 5. Détails techniques repliables
|
||
|
||
La metadata détaillée (clés/valeurs verboses, IDs internes, raisons null) sert à l'investigation, pas à la lecture courante. La masquer derrière un `Voir les détails / Masquer les détails`, et reset l'état d'expansion à chaque rechargement.
|
||
|
||
```typescript
|
||
const expanded = ref<Set<string>>(new Set());
|
||
const toggle = (id: string) => {
|
||
const next = new Set(expanded.value);
|
||
next.has(id) ? next.delete(id) : next.add(id);
|
||
expanded.value = next;
|
||
};
|
||
// Reset après chaque load
|
||
const loadList = async () => {
|
||
/* … */
|
||
expanded.value = new Set();
|
||
};
|
||
```
|
||
|
||
Le bouton "voir détails" n'apparaît **que si** la card a effectivement de la metadata. Un toggle vide pollue la grille visuelle.
|
||
|
||
### Anti-patterns à éviter
|
||
|
||
- Cards uniformes en couleur/border quel que soit le type d'événement → tue la scanabilité
|
||
- Date absolue toujours visible (`27 avril 2026 à 08:37`) sur des cards serrées → bruit cognitif inutile
|
||
- UUIDs en plein texte sans rétrogradation visuelle
|
||
- Cascade 2 selects quand un `<optgroup>` natif suffit
|
||
- Bouton "voir détails" affiché même sans détails à voir
|
||
- Persister l'expansion entre navigations / paginations → état orphelin
|
||
|
||
---
|
||
|
||
<a id="pattern-structuration-pages-admin"></a>
|
||
## Pattern : Structuration de pages admin (eyebrows + grille filtres + variante danger)
|
||
|
||
### Synthèse
|
||
|
||
- **Objectif** : poser une grammaire visuelle commune sur les écrans admin (filtres + liste/form + sections multiples) sans framework lourd.
|
||
- **Contexte** : module Admin avec plusieurs panels (Audit, Utilisateurs, Corbeille, etc.) qui partagent la même structure.
|
||
- **Quand l'utiliser** : ≥ 3 panels admin avec structure similaire.
|
||
- **Quand l'éviter** : page admin unique sans cohérence inter-panels à maintenir.
|
||
|
||
### Validation
|
||
|
||
- Validé le : 27-04-2026
|
||
- Contexte technique : Vue 3 / CSS — RL799_V2 (5 panels admin)
|
||
|
||
### 1. Eyebrows de section
|
||
|
||
Au lieu de titres H2/H3 trop forts, utiliser des "eyebrows" : mini-labels uppercase, letter-spacing élargi, taille `caption`, couleur d'accent.
|
||
|
||
```css
|
||
.eyebrow {
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.2em;
|
||
font-size: var(--font-size-caption);
|
||
color: var(--color-accent);
|
||
margin: 0;
|
||
}
|
||
```
|
||
|
||
```html
|
||
<p class="eyebrow">Filtres</p>
|
||
<div class="my-filters"><!-- … --></div>
|
||
|
||
<p class="eyebrow">Résultats <span class="eyebrow-count">· {{ total }} entrées</span></p>
|
||
<div class="my-list"><!-- … --></div>
|
||
```
|
||
|
||
Le compteur (`· N entrées`) est en `font-weight: normal`, sans uppercase, en `color-text-secondary` — typographie en cascade.
|
||
|
||
Bénéfice : structure visuelle sans concurrencer un éventuel titre de page. Cohérence inter-écrans dans tout un module si l'eyebrow est utilisé partout.
|
||
|
||
Quand ne pas afficher l'eyebrow `Résultats` : sur loading / error / empty state. Le `StateBlock` (ou équivalent) prend le relais.
|
||
|
||
### 2. Grille de filtres hiérarchique
|
||
|
||
Quand 3+ filtres dont l'un est dominant (typiquement une recherche), au lieu d'un flex-wrap chaotique, utiliser une grille avec le filtre primaire en pleine largeur :
|
||
|
||
```html
|
||
<div class="filters">
|
||
<label class="filters__label filters__label--full">Recherche
|
||
<input type="search">
|
||
</label>
|
||
<label class="filters__label">Statut <select><!-- … --></select></label>
|
||
<label class="filters__label">Rôle <select><!-- … --></select></label>
|
||
</div>
|
||
```
|
||
|
||
```css
|
||
.filters {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
align-items: end;
|
||
gap: var(--space-2);
|
||
}
|
||
.filters__label--full {
|
||
grid-column: 1 / -1;
|
||
}
|
||
```
|
||
|
||
### 3. Variante `danger` pour actions destructives
|
||
|
||
Sur un écran qui mélange actions constructives et destructives (ex : saisie initiale + bypass admin) :
|
||
|
||
```css
|
||
.btn--danger {
|
||
border-color: color-mix(in srgb, var(--color-danger) 48%, transparent);
|
||
background: color-mix(in srgb, var(--color-danger) 12%, transparent);
|
||
color: var(--color-danger);
|
||
}
|
||
|
||
.card--danger {
|
||
border-left: 3px solid var(--color-danger);
|
||
}
|
||
```
|
||
|
||
`color-mix` produit un rouge **soft** (12-20 % saturation), pas un flash rouge violent.
|
||
|
||
**Anti-pattern** : `background: red` / `color: red` directs ou hex hardcodés (`#ef4444`). Toujours via tokens du thème.
|
||
|
||
### Anti-patterns à éviter
|
||
|
||
- Filtres en `flex-wrap` qui produisent 3 lignes asymétriques selon le viewport
|
||
- Hint général qui répète ce que l'empty state dit déjà → 1 message, 1 niveau d'info
|
||
- Confondre "titre de page" (l'onglet actif suffit souvent) et "structure de section" (eyebrows)
|
||
- Action destructive en variant primary or → danger explicite
|
||
|
||
---
|
||
|
||
<a id="pattern-reset-defensif-pointer-events"></a>
|
||
## Pattern : Reset défensif du state pointer events au `pointerdown`
|
||
|
||
### Synthèse
|
||
|
||
- **Objectif** : éviter qu'un composant swipe/drag reste figé dans un état corrompu (`dragging = true`) quand un `pointerup` est "volé" par une modale ou une navigation post-action.
|
||
- **Contexte** : composant gérant `@pointerdown / @pointermove / @pointerup` qui émet une action ouvrant une modale au `pointerup` (la modale prend le focus → le pointer quitte la card → `pointerup` ne remonte jamais).
|
||
- **Quand l'utiliser** : tout composant à geste pointer émettant une action qui change de surface (modale, nav).
|
||
- **Quand l'éviter** : geste pur sans effet de bord de navigation.
|
||
|
||
### Analyse
|
||
|
||
- **Avantages** : robuste face aux modales/navigations/focus switches ; un seul point d'entrée à protéger ; pas de logique conditionnelle complexe.
|
||
- **Limites / vigilance** : ne couvre pas le démontage du composant pendant le drag (géré par le cycle de vie Vue). Pattern complémentaire : flag anti-click synthétique pour ne pas déclencher un `@click` natif après le geste.
|
||
|
||
### Validation
|
||
|
||
- Validé le : 05-05-2026
|
||
- Contexte technique : Vue 3 / pointer events — RL799 (`ProfaneListCard`)
|
||
|
||
### Implémentation
|
||
|
||
```typescript
|
||
const handlePointerDown = (event: PointerEvent) => {
|
||
if (event.pointerType === 'mouse' && event.button !== 0) return;
|
||
resetSwipe(); // défense en profondeur : un pointerup précédent a pu être "volé"
|
||
activePointerId.value = event.pointerId;
|
||
// …
|
||
};
|
||
|
||
// flag anti-click synthétique : posé en pointerup, consommé dans @click
|
||
const handleClick = (): void => {
|
||
if (swipeJustHandled.value) { swipeJustHandled.value = false; return; }
|
||
emit('select');
|
||
};
|
||
```
|
||
|
||
---
|
||
|
||
<a id="pattern-modale-dediee-vs-partagee"></a>
|
||
## Pattern : Modale dédiée vs partagée — décider à la 2ᵉ contrainte
|
||
|
||
### Synthèse
|
||
|
||
- **Objectif** : décider en amont s'il faut étendre une modale partagée (`v-if`/`forcedType`/`mode`) ou créer une modale dédiée quand un type métier se décline en variantes aux contraintes d'édition différentes.
|
||
- **Contexte** : type métier (Document) décliné en variantes (planche, helper) avec champs obligatoires/validations/types autorisés divergents.
|
||
- **Quand l'utiliser** : dès qu'une variante diverge d'un cas standard.
|
||
- **Quand l'éviter** : variantes ne différant que par le wording (préférer alors la factorisation par mode).
|
||
|
||
### Analyse
|
||
|
||
- **Critère de décision** : modale **dédiée dès que ≥ 2 contraintes divergent** (type figé vs éditable, champ exclusif, champs masqués, validation conditionnelle). 1 seule contrainte → prop conditionnelle sur la modale partagée.
|
||
- **Avantages dédiée** : impossible de corrompre la catégorie (type figé en TS, jamais dans le patch) ; pas d'explosion combinatoire de `v-if`.
|
||
- **Limites / vigilance** :
|
||
- une modale partagée avec select à fallback peut **silencieusement** basculer une ressource d'une catégorie à l'autre (cas vécu : `TYPE_OPTIONS` sans `'helpers'` → fallback `'planches'` au save)
|
||
- duplication ~80 % (a11y, focus trap, layout) : extraire un composable partagé pour le focus trap si identique, classes BEM dédiées sinon
|
||
- **Détection en revue** : `grep "forcedType\|forcedMode\|isVariantX"` dans une modale partagée — si > 3 occurrences, refactorer en modale dédiée.
|
||
|
||
### Validation
|
||
|
||
- Validé le : 06-05-2026
|
||
- Contexte technique : Vue 3 — RL799 (`DocumentEditModal` → `HelperEditModal`/`HelperUploadModal`)
|
||
|
||
---
|
||
|
||
<a id="pattern-fail-fast-branche-unreachable-dev"></a>
|
||
## Pattern : Fail-fast sur branche unreachable en `__DEV__`
|
||
|
||
### Synthèse
|
||
|
||
- **Objectif** : éviter qu'un `return;` no-op silencieux sur une branche unreachable (garantie par un early-return en amont) ne devienne un bug fantôme si un refactor casse l'invariant.
|
||
- **Contexte** : `switch`/`match` dont un case est inatteignable par construction (ex. CTA paywall jamais atteint car `SUBSCRIPTION_REQUIRED` fait un early-return avant le rendu).
|
||
- **Quand l'utiliser** : toute branche unreachable par invariant.
|
||
- **Quand l'éviter** : branche réellement atteignable (gérer le cas normalement).
|
||
|
||
### Analyse
|
||
|
||
- **Avantages** : tout refactor cassant l'invariant fait crasher l'app en dev (feedback immédiat) ; la prod reste safe (`return;` no-op) ; le commentaire documente l'invariant et la story.
|
||
- **Limites / vigilance** : anti-pattern `// no-op pour l'instant (cas marginal)` sans throw — le cas marginal devient un bug silencieux le jour où il se réalise.
|
||
|
||
### Validation
|
||
|
||
- Validé le : 29-05-2026
|
||
- Contexte technique : React Native — app-alexandrie (ux-cleanup-7, CTA "Débloquer ce pack")
|
||
|
||
### Implémentation
|
||
|
||
```typescript
|
||
switch (model.action) {
|
||
case 'paywall':
|
||
if (__DEV__) throw new Error('[story-X] CTA paywall atteint — invariant cassé ?');
|
||
return; // prod : no-op safe
|
||
// …
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
<a id="pattern-extension-retrocompatible-composant"></a>
|
||
## Pattern : Extension rétrocompatible d'un composant vs création d'un sibling
|
||
|
||
### Synthèse
|
||
|
||
- **Objectif** : étendre un composant existant via une prop optionnelle plutôt que créer un sibling quasi-dupliqué, quand le composant convient à ~80 % d'un nouveau use-case.
|
||
- **Contexte** : composant UI (ex. FAB circulaire icon-only) à étendre pour un nouveau cas (porter un label).
|
||
- **Quand l'utiliser** : quand 4 critères tiennent (voir Analyse).
|
||
- **Quand l'éviter** : logique métier divergente, ou props mutuellement exclusives qui s'accumulent (signal de 2 composants déguisés).
|
||
|
||
### Analyse
|
||
|
||
- **Critères pour étendre** : (1) la partie variante (label, icon, taille) ≠ la partie invariante (positionnement, ombre, animation, a11y) ; (2) l'invariant DOIT rester partagé (un seul fichier à toucher quand la nav change) ; (3) la prop d'extension est optionnelle avec un défaut préservant le comportement actuel ; (4) pas de logique métier divergente.
|
||
- **Avantages** : 1 fichier, 1 batch de tests, call-sites existants intacts (0 régression).
|
||
- **Limites / vigilance** : si le rendu fait `if (mode === 'X') … else …` plus de 2-3 fois → deux composants déguisés, séparer. Anti-pattern : `Fab.tsx` + `FabExtended.tsx` avec ~80 % de styles dupliqués (un bug de positionnement se corrige dans un seul fichier et ressurgit sur l'autre écran).
|
||
|
||
### Validation
|
||
|
||
- Validé le : 29-05-2026
|
||
- Contexte technique : React Native — app-alexandrie (FAB partagé Messages/Contenu/Forum)
|
||
|
||
### Implémentation
|
||
|
||
```typescript
|
||
type FabProps = {
|
||
icon: MaterialIconName;
|
||
onPress: () => void;
|
||
accessibilityLabel: string;
|
||
label?: string; // optionnel → bascule en mode extended si présent
|
||
bottomOffset?: number;
|
||
};
|
||
const isExtended = typeof label === 'string' && label.length > 0;
|
||
// call-site existant inchangé : <Fab icon="add" onPress={...} accessibilityLabel="Nouveau message" />
|
||
```
|
||
|
||
---
|
||
|
||
<a id="pattern-cta-toggle-async-preserver-etat"></a>
|
||
## Pattern : CTA toggle async — préserver l'état pendant le loading
|
||
|
||
### Synthèse
|
||
|
||
- **Objectif** : garder visible l'indicateur de l'état pré-toggle (ex. icône check "Suivi") pendant le chargement async, pour ne pas perdre le repère visuel.
|
||
- **Contexte** : bouton toggle d'état (Suivre/Suivi, like/unlike) avec action async.
|
||
- **Quand l'utiliser** : tout CTA toggle dont l'action est asynchrone.
|
||
- **Quand l'éviter** : action synchrone instantanée.
|
||
|
||
### Analyse
|
||
|
||
- **Règle** : ne pas remplacer la **totalité** de l'état visible par un spinner — garder au moins un indicateur de l'état pré-toggle pour la durée du loading.
|
||
|
||
### Validation
|
||
|
||
- Validé le : 29-05-2026
|
||
- Contexte technique : React Native — app-alexandrie (`directory-follow-button.tsx`, ux-cleanup-9)
|
||
|
||
### Implémentation
|
||
|
||
```tsx
|
||
// ✅ l'icône check reste visible, hors du ternaire isLoading
|
||
<View style={containerStyle}>
|
||
{isFollowing && <Icon name="check" />}
|
||
{isLoading ? <Spinner /> : <Label>{label}</Label>}
|
||
</View>
|
||
```
|
||
|
||
---
|
||
|
||
<a id="pattern-helpers-fail-safe-data-backend"></a>
|
||
## Pattern : Helpers fail-safe face aux bugs data backend
|
||
|
||
### Synthèse
|
||
|
||
- **Objectif** : qu'un helper de transformation donnée → affichage traite les cas dégénérés (date future, 0, NaN, format invalide) avec un comportement neutre, sans amplifier le bug visuellement.
|
||
- **Contexte** : helpers de formatage/dérivation consommant une donnée backend potentiellement bugguée.
|
||
- **Quand l'utiliser** : tout helper qui mappe une donnée externe vers de l'UI.
|
||
- **Quand l'éviter** : donnée 100 % maîtrisée localement.
|
||
|
||
### Analyse
|
||
|
||
- **Anti-patterns** : `createdAt` futur → "tout récent" → tous les contenus affichent "Nouveau" (cascade) ; `duration === 0` → "moins d'1 min" trompeur.
|
||
- **Règle** : retour neutre (`false` / `''` / `'—'`) + `console.warn` en `__DEV__` pour remonter la régression API sans polluer la prod.
|
||
|
||
### Validation
|
||
|
||
- Validé le : 30-05-2026
|
||
- Contexte technique : React Native — app-alexandrie (ux-cleanup-15, `isContentRecent`/`formatDuration`)
|
||
|
||
### Implémentation
|
||
|
||
```ts
|
||
export function isContentRecent(createdAt, now = new Date()) {
|
||
if (!createdAt) return false;
|
||
const created = parse(createdAt);
|
||
if (Number.isNaN(created.getTime())) return false;
|
||
const ageMs = now.getTime() - created.getTime();
|
||
if (ageMs < 0) {
|
||
if (__DEV__) console.warn('[isContentRecent] createdAt futur:', createdAt);
|
||
return false; // neutre, n'amplifie pas le bug
|
||
}
|
||
return ageMs < THRESHOLD;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
<a id="pattern-props-raccourci-composant-generique"></a>
|
||
## Pattern : Prop de raccourci pour composant générique
|
||
|
||
### Synthèse
|
||
|
||
- **Objectif** : éviter qu'un cas d'usage dominant ne force chaque callsite à un wrapper répétitif, en ajoutant une prop de raccourci.
|
||
- **Contexte** : composant générique (`<EmptyState>`) appelé partout avec le même wrapper (`icon={<ThemedText style={{fontSize:48}}>📭</ThemedText>}`).
|
||
- **Quand l'utiliser** : wrapper identique répété sur plusieurs callsites.
|
||
- **Quand l'éviter** : variété réelle de wrappers (garder la prop générique seule).
|
||
|
||
### Analyse
|
||
|
||
- **Règle** : la prop générique (`icon?: ReactNode`) reste prioritaire pour les cas custom ; la prop raccourci (`emoji?: string`) injecte un wrapper standard.
|
||
- **Garde-fou** : 1-2 props de raccourci max. Si le cas devient complexe, exposer un sous-composant (`<EmptyState.Emoji>`) ou refactorer.
|
||
|
||
### Validation
|
||
|
||
- Validé le : 30-05-2026
|
||
- Contexte technique : React Native — app-alexandrie (`EmptyState`, ux-cleanup-15)
|
||
|
||
---
|
||
|
||
<a id="pattern-compositions-semantiques-utilisees"></a>
|
||
## Pattern : Compositions sémantiques — la migration doit les utiliser
|
||
|
||
### Synthèse
|
||
|
||
- **Objectif** : éviter de définir des compositions sémantiques (`typography.body`, `colors.actionPrimary`) que la migration n'utilise pas, laissant les tokens atomiques s'imposer et le sémantique devenir du code mort.
|
||
- **Contexte** : migration vers un design system avec compositions + tokens atomiques.
|
||
- **Quand l'utiliser** : toute migration introduisant des compositions sémantiques.
|
||
|
||
### Analyse
|
||
|
||
- **Règle** : la migration trie — composition existante pour le besoin → l'utiliser ; sinon token atomique + signaler en review qu'une composition pourrait être créée.
|
||
- **Ratio cible** : > 50 % d'usages de compositions. Si < 30 %, soit elles sont mal nommées, soit à supprimer (sur-engineering).
|
||
|
||
### Validation
|
||
|
||
- Validé le : 29-05-2026
|
||
- Contexte technique : React Native — app-alexandrie (ux-cleanup-12/13)
|
||
|
||
---
|
||
|
||
<a id="pattern-touch-target-44-minwidth-minheight"></a>
|
||
## Pattern : Touch target 44pt — imposer `minWidth` ET `minHeight`
|
||
|
||
### Synthèse
|
||
|
||
- **Objectif** : garantir une zone tap 44×44 (HIG iOS), pas 44×N — `minHeight: 44` seul laisse la largeur < 44 sur un label court.
|
||
- **Contexte** : composants génériques (`<EmptyState ctaButton>`, `<Button>`) où le caller fournit le label.
|
||
- **Quand l'utiliser** : tout CTA dont le label peut être court ("OK", "Annuler", chip).
|
||
- **Quand l'éviter** : zone tap déjà large par construction.
|
||
|
||
### Analyse
|
||
|
||
- **Cas d'oubli typique** : CTA court → label étroit + padding insuffisant → zone < 44 en width malgré `minHeight: 44` (miss-tap fréquent sur iPhone SE / gants).
|
||
- **Trade-off** : `minWidth: 44` est un floor invisible, rarement contraignant pour des labels longs.
|
||
|
||
### Validation
|
||
|
||
- Validé le : 31-05-2026
|
||
- Contexte technique : React Native — app-alexandrie (ux-cleanup-14)
|
||
|
||
### Implémentation
|
||
|
||
```tsx
|
||
ctaButton: { minHeight: 44, minWidth: 44, justifyContent: 'center', alignItems: 'center' }
|
||
```
|
||
|
||
---
|
||
|
||
<a id="pattern-helpers-temps-date-purs"></a>
|
||
## Pattern : Helpers temps/date purs centralisés (formatage relatif, groupement)
|
||
|
||
### Synthèse
|
||
|
||
- **Objectif** : centraliser le formatage temporel (date relative, groupement par blocs) dans des helpers purs testables env node, pour éviter la divergence de wording entre écrans et entre projets.
|
||
- **Contexte** : écrans multiples affichant des dates (Notifications, Messages, Feed) ou un timeline de messages séquencés (chat, commentaires).
|
||
- **Quand l'utiliser** : dès que > 1 écran formate des dates relatives, ou qu'une liste temporelle sature de timestamps.
|
||
- **Quand l'éviter** : un seul affichage de date trivial.
|
||
|
||
### Analyse
|
||
|
||
- **Avantages** : helper pur, `now` injectable → tests déterministes ; 0 dépendance lib (Intl natif, -50 Ko vs date-fns/dayjs) ; le 1er dev d'une story partagée écrit le helper, les suivants consomment.
|
||
- **Décisions structurantes** :
|
||
- bornes "hier"/jours semaine = **jours calendaires** (`startOfDay(now) − startOfDay(date)`), pas deltas en ms (sinon 24h pile ≠ perception humaine)
|
||
- pas de "dans N min" pour une date future (clock skew) → retomber sur "à l'instant"
|
||
- tolérance gracieuse : `string` ISO invalide → fallback, jamais de crash
|
||
- groupement chat : séparateur centré quand delta > 5 min OU changement de jour ; 1er message toujours précédé d'un séparateur ; la fonction attend l'ordre **croissant**, le caller reverse si liste `inverted` (ne pas coupler la logique à l'index FlatList)
|
||
|
||
### Validation
|
||
|
||
- Validé le : 29-05-2026
|
||
- Contexte technique : React Native — app-alexandrie (`relative-time.ts`, `timestamp-grouping.ts`, ux-cleanup-10/11)
|
||
|
||
### API
|
||
|
||
```ts
|
||
formatRelativeTime(input: Date | string, now: Date = new Date()): string
|
||
// → "à l'instant" / "il y a N min" / "il y a N h" / "hier" / "lun" / "12 mars" / "12 mars 2025"
|
||
|
||
groupMessagesByTime<T>(messages: T[], now?: Date): GroupedItem<T>[]
|
||
// GroupedItem = { kind: 'timestamp' | 'message'; ... }
|
||
```
|
||
|
||
---
|
||
|
||
<a id="pattern-avatar-initiale-pastel-deterministe"></a>
|
||
## Pattern : Avatar circulaire avec initiale pastel déterministe
|
||
|
||
### Synthèse
|
||
|
||
- **Objectif** : différencier visuellement des utilisateurs sans photo (early adopters, B2B interne) via une initiale colorée déterministe.
|
||
- **Contexte** : listes d'utilisateurs où les photos de profil sont rares.
|
||
- **Quand l'utiliser** : ~quelques centaines d'utilisateurs, photos optionnelles.
|
||
- **Quand l'éviter** : photos systématiques, ou besoin d'unicité forte (collisions visuelles assumées ici).
|
||
|
||
### Analyse
|
||
|
||
- **Implémentation** : palette ~7 couleurs mappées sur tokens design system + 1 fallback grisé ; hash trivial (somme charCodes du nom trimmé modulo `palette.length - 1`) ; 1ère lettre majuscule (préserver accents) ; `null`/vide → fallback "?" grisé.
|
||
- **Avantages** : 0 dépendance, testable env node, suit light/dark, déterministe (un nom = toujours la même couleur).
|
||
- **Anti-patterns** : couleur aléatoire au mount (casse la reconnaissance) ; hash sur UUID (opaque, change au renommage — hasher le nom affiché) ; lib crypto pour le hash (surdimensionné).
|
||
|
||
### Validation
|
||
|
||
- Validé le : 29-05-2026
|
||
- Contexte technique : React Native — app-alexandrie (`avatar-initial.tsx`, ux-cleanup-11)
|
||
|
||
### API
|
||
|
||
```ts
|
||
<AvatarInitial name={"charlie" | null} size={40} />
|
||
```
|
||
|
||
---
|
||
|
||
<a id="pattern-composants-generiques-etats-chargement"></a>
|
||
## Pattern : Composants UI génériques + états de chargement (EmptyState, Skeleton)
|
||
|
||
### Synthèse
|
||
|
||
- **Objectif** : factoriser le layout des écrans liste (vide / chargement) dans des composants génériques tout en laissant le wording au caller pour respecter la voix produit.
|
||
- **Contexte** : N écrans liste partageant un layout d'état vide et un skeleton de premier chargement.
|
||
- **Quand l'utiliser** : ≥ 2 écrans liste avec états vide/chargement similaires.
|
||
- **Quand l'éviter** : écran unique sans réutilisation.
|
||
|
||
### Analyse
|
||
|
||
- **`<EmptyState>`** : API `{ icon?, emoji?, title, description?, cta? }`. Le caller fournit le wording (calme, bienveillant, non alarmiste) et un CTA **contextuel uniquement quand utile** (pas de "Voir tout" sur une liste vide qui ne pointe nulle part).
|
||
- **`<SkeletonScreen>`** : API `{ variant: 'card'|'list-item'|'text-line'|'header', count }`, shimmer animé (une seule shared value pour synchroniser les sous-blocs).
|
||
- **Règle CRITIQUE des 3 états de chargement** :
|
||
- **premier chargement** (`isLoading && items.length === 0`) → `<SkeletonScreen>`
|
||
- **refresh** (`isLoading && items.length > 0`) → `RefreshControl` natif
|
||
- **pagination** (`isLoadingMore`) → `ListFooterComponent` (ActivityIndicator)
|
||
- **Anti-patterns** : skeleton/spinner plein écran pendant un refresh (l'utilisateur perd ses items, clignotement) ; skeleton statique sans animation (signal "ça travaille" perdu) ; skeleton avec wording "Chargement…" (double signal redondant) ; wording technique ("Liste de 0 éléments").
|
||
- **Tests env node** : `react-native-reanimated` doit être mocké (hooks no-op, `Animated.View` → string).
|
||
|
||
### Validation
|
||
|
||
- Validé le : 31-05-2026
|
||
- Contexte technique : React Native — app-alexandrie (`EmptyState.tsx`, `SkeletonScreen.tsx`, ux-cleanup-13/15)
|