docs(knowledge): capitalisation frontend — intégration du triage local (mai-juin 2026)

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>
This commit is contained in:
MaksTinyWorkshop
2026-06-25 15:31:53 +02:00
parent f1b783407a
commit 5f5c87296e
15 changed files with 2439 additions and 12 deletions
+563
View File
@@ -819,3 +819,566 @@ Pour toute extension d'un payload backend (ajout de champ), appliquer une **chec
- Contexte technique : Next.js / Server Actions / TypeScript / Zod — app-template-resto 25-06-2026
---
<a id="risque-fichier-modifie-pas-fichier-propre"></a>
## "Fichier modifié" ≠ "fichier propre"
### Risques
- Un écran ancien (avant adoption du design system) reste 100 % styles inline / hex hardcodés / magic numbers. Une story tactique qui n'y modifie que 3 lignes le fait apparaître dans la `File List` sans nettoyer le reste → faux signal "fichier traité dans la story", dette intacte
### Symptômes
- Fichier "récent" en apparence mais saturé de `style={{ … }}`, `#666`, `spacing` en dur, strings non i18n
### Bonnes pratiques / mitigations
- En review, ne pas conclure qu'un fichier est conforme parce qu'il est dans la `File List`. Vérifier explicitement : styles inline, hex hardcodés, magic numbers, strings en dur
- Si la dette dépasse le scope de la story, la **capitaliser** comme dette à refondre (story dédiée), ne pas l'absorber implicitement
- Contexte technique : React Native — app-alexandrie (`thread/[threadId].tsx`), 27-05-2026
---
<a id="risque-sweep-grep-amorce-vs-liste-finale"></a>
## Sweep statique : grep d'amorce ≠ liste finale de candidats
### Risques
- Un sweep préventif (audit, refacto large) propage la liste **brute** du grep d'amorce comme liste finale, sans appliquer le critère discriminant qui justifie le sweep
- Appliquer un fix à tous les hits de l'amorce → modifications no-op ou nuisibles sur les faux positifs
### Symptômes
- Rapport listant N "fichiers suspectés affectés" alors qu'une validation montre k vrais positifs (ex. amorce `contentContainer:` → 28 hits, vrai pattern = + `flexDirection: 'row'` → 5 fichiers)
### Bonnes pratiques / mitigations
1. Publier **deux listes distinctes** : amorce (grep brut) et finale (filtrée par critère discriminant, vérifiée fichier par fichier)
2. Intégrer au rapport le `awk`/`grep` exact qui produit la liste finale (re-vérifiable en 30 s)
3. **Stop condition** : ne pas fixer les fichiers de l'amorce absents de la liste finale
4. En review : tout écart entre liste finale du rapport et liste recalculée = finding HIGH
- Contexte technique : React Native — app-alexandrie (ux-cleanup-1), 28-05-2026
---
<a id="risque-sticky-bottom-nav-persistante-positionnement"></a>
## Sticky bottom + nav persistante : positionnement à recalculer en dynamique
### Risques
- Un sticky `position: absolute` au-dessus d'une nav persistante calcule son `bottom` depuis une constante arbitraire (`BottomTabInset = 50/80`) déconnectée de la hauteur réelle de la nav et du safe-area
- Le CTA est coupé par la nav. Reproduit sur device, invisible en tests Jest
### Symptômes
- Bouton sticky chevauché/coupé par la BottomBar
### Bonnes pratiques / mitigations
```typescript
// ✅ hauteur réelle de la nav (exportée par le composant nav) + safeArea bottom
const insets = useSafeAreaInsets();
<View style={[styles.stickyCta, { bottom: PERSISTENT_TAB_BAR_HEIGHT + insets.bottom }]} />
```
- Exposer la hauteur réelle depuis le composant nav (`export const PERSISTENT_TAB_BAR_HEIGHT = 64`), ne jamais utiliser une constante "à peu près correcte"
- Si cohabitation avec un ScrollView : `paddingBottom = STICKY_HEIGHT + NAV_HEIGHT + insets.bottom` au `contentContainerStyle`
- Contexte technique : React Native — app-alexandrie (ux-cleanup-7), 29-05-2026
---
<a id="risque-filtre-client-side-liste-paginee"></a>
## Filtrer client-side sur une liste paginée → l'écran ment
### Risques
- Un filtre/recherche via `items.filter(...)` sur une liste paginée côté serveur : les items chargés ne matchent pas, mais des pages non chargées le feraient
- L'utilisateur voit une liste vide et conclut faussement "il n'y a rien de ce type" (inversion d'attribution coûteuse en confiance)
### Symptômes
- `const filtered = items.filter(...)` + `ListEmptyComponent="Aucun résultat"` dans un contexte paginé
### Bonnes pratiques / mitigations
1. **Préférer un param API** (`type`, `category`, `tag`) dans la query de pagination — seule solution propre
2. Si non faisable court terme : message UX explicite — quand la liste filtrée tombe à 0 et `hasMore === true`, afficher "Charger plus pour ce filtre" plutôt qu'un "Aucun résultat" trompeur
3. Ne jamais filtrer client en silence sur une liste paginée
- Contexte technique : React Native — app-alexandrie (ux-cleanup-5), 29-05-2026
---
<a id="risque-bucket-defaut-par-negation"></a>
## Bucket "défaut" défini par négation des autres → zone morte invisible
### Risques
- Définir un 3ᵉ bucket par négation (`isNotStarted = !isCompleted && !isInProgress`) crée une dépendance implicite entre prédicats
- Si la sémantique d'un prédicat change (ou si un input désynchronisé arrive), un item peut tomber dans **aucun** bucket et disparaître silencieusement de l'UI (invisible en typecheck/lint/test)
### Symptômes
- Contenu existant absent de toutes les sections de la liste après un changement de sémantique
### Bonnes pratiques / mitigations
```typescript
// ✅ chaque bucket par sa propre condition affirmative
function isNotStarted(c) {
return c.state === 'NOT_STARTED' && (c.completedAt ?? null) === null;
}
```
- Test "désync" : forcer les 3 prédicats à `false` simultanément → s'il existe un cas, l'invariant "exactement 1 bucket" est cassé (le test documente l'invariant)
- Contexte technique : React Native — app-alexandrie (ux-cleanup-6), 29-05-2026
---
<a id="risque-code-smells-js-ts-review"></a>
## Code smells JS/TS à rechercher en review
### Risques
- Ternaire identique des deux côtés (`cond ? a : a`), `??`/`||` avec deux opérandes identiques, variable affectée puis non utilisée différemment de sa valeur initiale → dead code ou intention incomplète
- Ni le compilateur ni le lint (s'il n'est pas configuré) ne signalent ces cas
### Symptômes
- `const labelColor = isFollowing ? c.primary : c.primary;`
### Bonnes pratiques / mitigations
- Détectables via `eslint-plugin-no-constant-binary-expression` / `no-constant-condition` étendu
- Garde-fou review : grep des ternaires/coalescences à opérandes identiques
- Contexte technique : React Native — app-alexandrie (ux-cleanup-9), 29-05-2026
---
<a id="risque-pressable-disabled-accessibility-state"></a>
## `Pressable` disabled sans `accessibilityState.disabled`
### Risques
- Sur React Native, `<Pressable disabled>` empêche la touche mais le screen reader l'annonce toujours comme tappable si `accessibilityState` n'est pas déclaré
### Symptômes
- VoiceOver/TalkBack annonce "Bouton" au lieu de "Bouton estompé" sur un Pressable désactivé
### Bonnes pratiques / mitigations
- Tout `Pressable` avec `disabled` conditionnel doit passer `accessibilityState={{ disabled }}` (comme `selected` pour les chips actifs)
- Contexte technique : React Native — app-alexandrie (ux-cleanup-9, `directory-user-item.tsx`), 29-05-2026
---
<a id="risque-redirection-controlee-par-data-backend"></a>
## Redirection contrôlée par data backend sans allowlist
### Risques
- Un helper qui transforme un champ backend en path de navigation et accepte `targetId.startsWith('/')` ouvre une redirection vers tout écran (`/auth/reset?token=x`, `/community/admin/secret`) si la table backend est corrompue
- Zéro défense en profondeur côté client face à une compromission d'un tier intermédiaire (Redis, queue, service notifs)
### Symptômes
- `if (targetId.startsWith('/')) return targetId;` dans un résolveur de route
### Bonnes pratiques / mitigations
```ts
const ALLOWED_PREFIXES = ['/notifications', '/content/', '/user/', '/messages/', '/profile'];
const isAllowed = (path: string) => ALLOWED_PREFIXES.some((p) => path === p || path.startsWith(p));
```
- Coût du fix = 1 fonction + 1 constante
- Contexte technique : React Native — app-alexandrie (ux-cleanup-10 H1, notif SYSTEM), 29-05-2026
---
<a id="risque-helper-usage-futur-code-mort"></a>
## Helper livré "pour usage futur" sans JSDoc de statut → code mort
### Risques
- Un helper testé mais importé par aucun composant (livré en avance pour une dépendance arrière non encore livrée) crée du code mort et un faux signal "AC livré" reposant en réalité sur une autre couche
### Symptômes
- Helper avec une suite de tests mais 0 import en runtime (détecté par un audit "unused exports")
### Bonnes pratiques / mitigations
- Tout helper livré "pour usage futur" doit avoir une JSDoc documentant son statut + la story qui le branchera
- Contexte technique : React Native — app-alexandrie (ux-cleanup-10 H3, `i18n/notifications.ts`), 29-05-2026
---
<a id="risque-helper-heure-courante-fige-sans-refresh"></a>
## Helper dépendant de l'heure courante figé sans mécanisme de refresh
### Risques
- Un helper basé sur l'heure (`getTimeBasedGreeting`, `formatRelativeTime`) appelé inline dans le JSX fige sa valeur tant que le composant ne re-render pas
- Sur un écran à durée de vie longue, "Bonjour ☀️" reste affiché après 18h
### Symptômes
- `<Text>{getTimeBasedGreeting()}</Text>` sans state ni effet
### Bonnes pratiques / mitigations
```tsx
const [greeting, setGreeting] = useState(() => getTimeBasedGreeting());
useFocusEffect(useCallback(() => { setGreeting(getTimeBasedGreeting()); }, []));
```
- `useFocusEffect` (rafraîchit à chaque ré-ouverture d'onglet) suffit pour des salutations ; `setInterval` pour des compteurs temps réel ("il y a 2 min" → "3 min")
- Contexte technique : React Native / Expo Router — app-alexandrie (ux-cleanup-15 H1), 30-05-2026
---
<a id="risque-migration-tokens-composant-core-oublie"></a>
## Migration de tokens : le composant CORE de référence oublié
### Risques
- Lors d'une migration de tokens (`typography.ts`, `colors.ts`), le composant CORE qui consomme ces tokens (`<ThemedText>`, `<ThemedView>`) est l'oublié — migré partiellement, ou ignoré car l'audit grep cible les "consommateurs" et pas les "définisseurs"
### Symptômes
- Le fichier qui définit les types garde des `fontWeight: 500` / `fontSize: 48` en dur alors que tout le reste est migré
### Bonnes pratiques / mitigations
```bash
grep -rnE "fontSize: [0-9]+\b|fontWeight: [0-9]+\b" src/ --include="*.tsx"
grep "from '@/theme'" src/components/themed-text.tsx # le CORE consomme-t-il les tokens ?
```
- Auditer EN PREMIER le composant CORE
- Contexte technique : React Native — app-alexandrie (ux-cleanup-12 H1, `themed-text.tsx`), 30-05-2026
---
<a id="risque-animation-switch-binaire-clignotement"></a>
## Animation "shimmer/fade/pulse" via switch binaire → clignotement perçu comme bug
### Risques
- Un `value < 0.5 ? colorA : colorB` produit un blink on/off perçu comme un glitch d'affichage, pas comme une animation
### Symptômes
- Skeleton/placeholder qui clignote au lieu de fondre
### Bonnes pratiques / mitigations
```tsx
// ✅ interpolation = transition douce (reanimated)
backgroundColor: interpolateColor(progress.value, [0, 0.5, 1], [colorA, colorB, colorA])
```
- Contexte technique : React Native / reanimated — app-alexandrie (ux-cleanup-13 H1, `SkeletonScreen.tsx`), 31-05-2026
---
<a id="risque-as-unknown-as-bypass-typage"></a>
## `as unknown as X` — signal d'alarme, la lib a souvent le type prévu
### Risques
- Face à une erreur de typage avec une lib, `as unknown as X` bypasse complètement TypeScript et masque le vrai problème (la lib expose un type spécifique à utiliser, ex. `AnimatedStyle<ViewStyle>` de reanimated)
### Symptômes
- `const animatedStyle = useAnimatedStyle(() => ({…})) as unknown as ViewStyle;`
### Bonnes pratiques / mitigations
- Importer et propager le type strict de la lib
- Tout `as unknown as` / `as any as` doit être justifié par un commentaire démontrant que (a) la lib n'expose pas le type adéquat et (b) le contrat runtime est garanti par ailleurs
- Contexte technique : React Native / reanimated — app-alexandrie (ux-cleanup-13 M1), 31-05-2026
---
<a id="risque-hitslop-isolation-vs-layout"></a>
## `hitSlop` calculé en isolation → chevauchement des voisins
### Risques
- Un `hitSlop` calculé pour atteindre 44pt sans tenir compte du layout peut chevaucher les éléments voisins en layout dense (ScrollView horizontal avec gap)
- Taper entre deux éléments active le mauvais
### Symptômes
- Chips avec `hitSlop left/right=6` et gap=8 → cumul 12 sur 8 → chevauchement 4pt
### Bonnes pratiques / mitigations
- Pour atteindre 44pt en height : augmenter top/bottom est sûr ; pour width, plafonner left/right à `gap / 2`
- SectionHeader suivi d'une liste : `hitSlop bottom``marginBottom` du container (ou 8pt par défaut)
- Contexte technique : React Native — app-alexandrie (ux-cleanup-14 M1/M5), 31-05-2026
---
<a id="risque-hooks-apres-early-return"></a>
## Hook appelé après un early return → "Rendered more hooks" (crash bloquant)
### Risques
- Un Hook (`useRouter`) appelé après un early return conditionnel : quand le composant transitionne vers le mode où le Hook est appelé, React crash `Rendered more hooks than during the previous render`
- Invisible aux tests Jest env=node sans renderer JSX
### Symptômes
- `useThemedColors()``if (isHidden) return …``const router = useRouter();`
### Bonnes pratiques / mitigations
- **Tous les Hooks AVANT tout early return** (règle stricte React) — le coût d'un Hook inutilisé est négligeable, le coût du crash est bloquant
- ESLint `react-hooks/rules-of-hooks` en `error` partout, jamais `warn`
- Garde-fou review : grep `const … = use[A-Z]` après un `if … return` dans la fonction
- Contexte technique : React Native — app-alexandrie (ux-cleanup-17 H1, `ThreadCard`), 31-05-2026
---
<a id="risque-useeffect-une-fois-booleen-derive"></a>
## `useEffect` "une fois" piloté par un booléen dérivé du state → re-fire au cycle reset/remplit
### Risques
- Un `useEffect` censé déclencher une action UNE FOIS, gardé par `threadsLoaded = threads.length > 0`, re-fire à chaque cycle "reset puis remplit" du state (un `fetchThreads` qui commence par `set({ threads: [] })`)
- `markForumSeen` rejoué à chaque pull-to-refresh / changement de catégorie → marque comme lus des threads jamais vus
### Symptômes
- Action idempotente (markSeen/markRead) rejouée silencieusement à chaque refetch
### Bonnes pratiques / mitigations
```typescript
// ✅ tracker l'identité de la ressource dans une ref (pas de re-render, reset au unmount)
const markedRef = useRef<Set<string>>(new Set());
useEffect(() => {
if (!threadsLoaded || markedRef.current.has(forumSlug)) return;
markedRef.current.add(forumSlug);
void markForumSeen(forumSlug);
}, [threadsLoaded, forumSlug, markForumSeen]);
```
- Cas typiques : analytics (`screen_viewed`), idempotency mark, one-time API calls
- Contexte technique : React Native / Zustand — app-alexandrie (ux-cleanup-22 H1), 31-05-2026
---
<a id="risque-double-announce-voiceover-parent-enfants"></a>
## Double-announce VoiceOver : `accessibilityLabel` parent + enfants accessibles
### Risques
- Un wrapper avec un `accessibilityLabel` couvrant tout son texte ET des enfants interactifs (`Pressable`, `Text onPress`, `role="link"`) avec leurs propres labels → VoiceOver lit le parent entier puis chaque enfant (double lecture)
### Symptômes
- Bulle DM avec liens : l'URL est lue dans le texte du parent puis dans `Lien : URL` de l'enfant
### Bonnes pratiques / mitigations
```tsx
// ✅ si enfants interactifs → wrapper non-accessible, VoiceOver navigue dans les enfants
const hasInteractiveChildren = segments.some((s) => s.type === 'url');
<View accessible={!hasInteractiveChildren}
accessibilityLabel={hasInteractiveChildren ? undefined : `Message : ${text}`} />
```
- Règle : si descendants interactifs avec labels propres → pas de `accessibilityLabel` global + `accessible={false}` sur le wrapper
- Contexte technique : React Native — app-alexandrie (ux-cleanup-19 M1, `message-bubble.tsx`), 02-06-2026
---
<a id="risque-prop-generique-usage-unique-dead-code"></a>
## Prop "générique" introduite pour un usage unique = dead code latent
### Risques
- Justifier un prop d'extension par "ça pourra resservir" crée une API orpheline dès que son unique consommateur disparaît
- Ni typecheck ni lint ne signalent un prop optionnel non utilisé (il reste valide isolément)
### Symptômes
- Prop `trailing` sur un composant + son rendu + ses tests, sans aucun appelant après suppression du seul consommateur
### Bonnes pratiques / mitigations
- Un prop introduit pour UN seul appelant disparaît avec lui : à la suppression d'un consommateur, grep l'usage du prop dans `src/` ; si zéro → retirer prop + rendu + tests
- La généricité ne se présume pas, elle se constate (≥ 2 usages)
- Contexte technique : React Native — app-alexandrie (`ContentInfoChips.trailing`), 02-06-2026
---
<a id="risque-icone-unicode-symbol-other-polychrome"></a>
## Icônes unicode navbar — éviter les caractères "Symbol, Other" (So) polychromes
### Risques
- Les caractères Unicode catégorie "So" (`⚕` U+2695, `☯`, `☢`) peuvent rendre en polychrome sur Android/iOS selon la police système, comme les emojis
- Des caractères visuellement proches ont des catégories très différentes
### Symptômes
- Icône navbar/FAB rendue en couleur au lieu de suivre `currentColor`
### Bonnes pratiques / mitigations
- Pour les items nav/FAB/chrome, rester sur les catégories sûres : "Po" (`✦ ✧ ◇ ◉`), "Sm", "Ps/Pe", ou SVG inline `stroke="currentColor"`
- Vérifier la catégorie Unicode sur unicode.org avant de choisir un caractère décoratif
- Contexte technique : Vue 3 — RL799 (`⚕``♡` dans `AppLayout.vue`, v2-4-1), 20-06-2026
---
<a id="risque-recherche-normalisation-filtrage-vs-alignement"></a>
## Recherche client accent-insensible : séparer normalisation de filtrage et d'alignement
### Risques
- Une seule fonction de normalisation sert au FILTRAGE (matcher) ET au SURLIGNAGE (aligner des index sur le texte original)
- Les ligatures (`œ→oe`, `æ→ae`) sont une expansion 1→N : indispensables au filtrage, mais elles cassent l'alignement d'index du surlignage
### Symptômes
- "oeuvres" ne matche pas "œuvres", ou les fragments `<mark>` sont décalés
### Bonnes pratiques / mitigations
- `normalizeForSearch` (filtrage) : minuscule + diacritiques + **ligatures expansées** (longueur peut changer)
- `normalizeForHighlight` (surlignage) : minuscule + diacritiques en mapping **strictement 1:1** (longueur préservée), pour `indexOf`/`slice` sur le texte original
- Ne jamais utiliser `normalize('NFD')` pour l'alignement (change la longueur). Surligner via segmentation `<mark>`, jamais `v-html`
- Compromis assumé : un terme sans ligature remonte l'article ligaturé mais le mot ligaturé n'est pas surligné
- Contexte technique : Vue 3 — RL799 (recherche corpus d'autorité), 22-06-2026
---
<a id="risque-deeplink-details-imbriques-racine"></a>
## Deep-link vers un arbre `<details>` imbriqués : lier `:open` à TOUS les niveaux, racine comprise
### Risques
- Un état `openPath` ouvre les nœuds intermédiaires mais pas le `<details>` RACINE → la cible reste sous un conteneur replié
- `scrollIntoView` vise alors un élément à offsetParent null → scroll silencieusement inopérant (aucune erreur)
### Symptômes
- "Rien ne se passe" après un deep-link recherche→sommaire ; les tests passent car ils vérifient les enfants ouverts, pas l'ancêtre racine
### Bonnes pratiques / mitigations
- Le prédicat d'ouverture doit couvrir le niveau racine : `openPath === key || openPath.startsWith(\`${key}::\`)` appliqué uniformément à chaque profondeur
- Test de non-régression : asserter `element.open === true` sur le `<details>` racine ET chaque ancêtre du chemin, pas seulement les feuilles
- Contexte technique : Vue 3 — RL799, 22-06-2026
---
<a id="risque-retry-form-sans-refetch-contexte"></a>
## Bouton "Réessayer" qui réaffiche un formulaire sans re-fetcher le contexte
### Risques
- Une page à machine d'état (loading→choose→error) où l'erreur vient du GET d'hydratation : un `retry()` qui fait juste `status = 'choose'` réaffiche le formulaire sans données → écran fonctionnel mais vide
### Symptômes
- Formulaire vide après "Réessayer" suite à une erreur de chargement
### Bonnes pratiques / mitigations
- `retry()` doit distinguer "erreur au chargement (contexte jamais hydraté)" de "erreur à la soumission (contexte déjà là)" via un flag `contextLoaded` : si faux → relancer le fetch de contexte ; si vrai → réafficher le formulaire
- Tester explicitement le chemin erreur-au-GET-puis-retry (souvent oublié)
- Contexte technique : Vue 3 — RL799 (page publique sondage, v2-6-4), 24-06-2026
---
<a id="risque-format-date-sans-heure-options-ambigues"></a>
## Format de date sans heure → options de créneaux ambiguës
### Risques
- Une liste de créneaux formatée en jour/mois/année seulement affiche deux options identiques si deux dates tombent le même jour à des heures différentes
- La contrainte d'unicité backend est souvent "par instant (timestamp ms)", pas "par jour" → deux créneaux le même jour sont légaux et distincts en données mais indistinguables à l'écran
### Symptômes
- Deux radios/checkboxes affichant le même libellé dans une liste de créneaux
### Bonnes pratiques / mitigations
- Formater AVEC l'heure (et le fuseau, ex. Europe/Paris) dès que l'unicité backend est à la milliseconde
- Aligner le format sur celui du canal d'origine (mail qui affichait déjà l'heure) pour la cohérence cross-surface
- Vérifier la granularité du dédoublonnage backend avant de choisir le format d'affichage
- Contexte technique : Vue 3 — RL799 (v2-6-4), 24-06-2026
---
<a id="risque-toisostring-throw-invalid-time-value"></a>
## `new Date(x).toISOString()` peut throw `RangeError` → form figé sans feedback
### Risques
- Une string de date NON VIDE mais non parsable (`<input type="datetime-local">`) donne `Invalid Date` ; `.toISOString()` **lève** une exception (contrairement à `.getTime()` qui renvoie `NaN` sans throw)
- Si la conversion est dans un `.map()` avant l'appel async et que le `@submit` n'a pas de `.catch`, la promesse rejette en silence → aucun `error` posé → formulaire figé sans message
### Symptômes
- Soumission qui ne fait "rien" sur une date invalide non-vide ; l'attribut `required` ne couvre pas ce cas
### Bonnes pratiques / mitigations
- Valider chaque date via `Number.isNaN(new Date(x).getTime())` dans la validation cliente AVANT toute conversion `toISOString()`
- Contexte technique : Vue 3 — RL799 (`InstructionForm.vue`)
---
<a id="risque-composant-icone-inheritattrs-svg-100"></a>
## Composant icône `inheritAttrs: false` + SVG `100%` : class ignorée + débordement
### Risques
- Un composant icône avec `defineOptions({ inheritAttrs: false })` n'applique pas la `class` passée (les attributs non-props ne sont pas posés sur le nœud racine) → toute règle CSS la ciblant est inerte
- SVG internes en `width/height: 100%` sans parent dimensionné → l'icône grandit sans borne
### Symptômes
- On passe `class="…"` pour dimensionner l'icône, la règle ne s'applique pas, l'icône déborde de son conteneur
### Bonnes pratiques / mitigations
- Envelopper le composant dans un élément natif (`<span>`) dimensionné explicitement (`width/height: 1.4em`) — l'élément natif reçoit bien la classe — et contraindre le SVG interne via `:deep(.classe-interne) { width:100%; height:100% }`
- Ne jamais compter sur une `class` passée directement à un composant en `inheritAttrs: false`
- Valider le rendu visuel (screenshot/app) avant de committer une intégration d'icône réutilisée
- Contexte technique : Vue 3 — RL799, 22-06-2026
---
<a id="risque-i18n-francisation-incoherente"></a>
## i18n / francisation : cohérence label / a11y / comportement / modèle de données
### Risques
- Composant "à moitié internationalisé" : `accessibilityLabel` en FR mais texte visible en EN (`SectionHeader` : a11y `Voir tout — ${title}` vs texte `See All`). Un sweep qui ne regarde que le texte visible rate l'incohérence ; le fix naïf crée une 2ᵉ chaîne FR qui peut diverger
- Langue UI non vérifiée par le typage : les strings en dur ne sont ni typées ni testées → dette d'anglais qui s'accumule écran par écran
- Label vs comportement réel d'un CTA : "GET ENROLL" appelait en réalité `markDetailConsumed` (= marquer terminé) — franciser littéralement en "Commencer" aurait livré un bouton trompeur
- Catégories/filtres UI déconnectés du modèle : chips `Vidéo/Texte/Audio` alors que l'enum backend ne connaît que `TEXT`/`VIDEO` → un chip sur un type inexistant = résultat toujours vide
### Symptômes
- Incohérence FR/EN entre texte et a11y ; CTA dont le label ment ; filtre toujours vide
### Bonnes pratiques / mitigations
- Lors de la francisation d'un composant partagé, vérifier ensemble : texte visible + `accessibilityLabel` + props de surcharge (`seeAllLabel?`), et faire converger vers une **source unique** (`{label ?? 'défaut'}`) plutôt que dupliquer
- Toujours lire le **handler** avant de traduire/relabeller un CTA, pas seulement le texte
- Valider toute taxonomie d'UI (chips, filtres) contre le schéma de données réel (contracts) avant de l'implémenter
- Filet : sweep grep périodique + revue visuelle ; grep des consommateurs avant tout fix transversal
- Contexte technique : React Native — app-alexandrie (ux-cleanup-5), 28-05-2026
---