Files
_Assistant_Lead_Tech/knowledge/frontend/risques/general.md
MaksTinyWorkshop b3417ad77b capitalisation: intégration ~60 entrées RL799_V2 (triage 2026-05-02)
Triage du 95_a_capitaliser.md (~75 propositions) :
- 60 entrées intégrées dans knowledge/ (backend, frontend, workflow)
- 4 nouveaux fichiers : backend/patterns/tests.md, backend/risques/tests.md,
  frontend/patterns/general.md, workflow/patterns/general.md
- 6 doublons rejetés
- Mise à jour des READMEs index pour refléter les nouvelles entrées
- 95_a_capitaliser.md restauré à sa structure initiale
- 40_decisions_et_archi.md : décision mono-tenant déployable vs SaaS multi-tenant
- 90_debug_et_postmortem.md : sub-agents Write indisponible, effet iceberg CI,
  prisma migrate diffs cosmétiques

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 22:12:44 +02:00

32 KiB

Frontend — Risques & vigilance : Général

Extrait de la base de connaissance Lead_tech. Voir knowledge/frontend/risques/README.md pour l'index complet.


Accessibilité oubliée (a11y)

Risques

  • App inutilisable au clavier/lecteur d'écran
  • Régressions silencieuses sur focus/labels

Symptômes

  • Modales impossibles à fermer au clavier
  • Inputs sans labels/erreurs non annoncées
  • Focus "perdu"

Bonnes pratiques / mitigations

  • Checklist a11y minimale sur chaque écran clé
  • Gestion de focus (modales, erreurs formulaire)
  • Labels/aria cohérents + tests simples

Regex globale /g en singleton — bug lastIndex stateful

Risques

  • Une regex avec flag /g ou /y définie comme constante au niveau module maintient un état lastIndex entre les appels
  • String.prototype.replace() réinitialise lastIndex, mais .test() ou .exec() ne le font pas → bug stateful difficile à détecter, souvent introduit par un refactor ultérieur

Symptômes

  • .test(str) retourne alternativement true / false sur la même chaîne selon l'ordre d'appel
  • Bug non reproductible en isolation, uniquement en séquence d'appels

Bonnes pratiques / mitigations

// ❌ RISQUÉ — regex globale partagée entre tous les appels
const LINK_PATTERN = /https?:\/\/\S+/gi;
function processLinks(content: string) {
  return content.replace(LINK_PATTERN, ...); // OK today
  // Mais si quelqu'un ajoute LINK_PATTERN.test(x) ailleurs → bug lastIndex
}

// ✅ SÛR — nouvelle instance à chaque appel, aucun état partagé
function makeLinkPattern(): RegExp {
  return /https?:\/\/\S+/gi;
}
function processLinks(content: string) {
  return content.replace(makeLinkPattern(), ...);
}
  • Règle : les regex avec flag /g ou /y utilisées pour transformation de strings → toujours créer via une factory, jamais en singleton de module

  • Contexte technique : TypeScript / React Native — app-alexandrie 24-03-2026


Alert.prompt iOS-only — fonctionnalité silencieusement cassée sur Android

Risques

  • Alert.prompt ne déclenche rien sur Android (retourne undefined silencieusement).
  • Les tests unitaires passent (mock), mais le flux ne s'exécute jamais sur 50 % des devices en production.

Symptômes

  • Flux de saisie utilisateur qui fonctionne sur simulateur iOS mais est inactif sur Android
  • Aucun message d'erreur côté dev ni côté utilisateur

Bonnes pratiques / mitigations

  1. Ne jamais utiliser Alert.prompt dans un projet Expo cross-platform.
  2. Remplacer par une modale custom : Modal + TextInput React Native — portable, accessible, testable.
  3. Wrapper le TextInput dans KeyboardAvoidingView avec behavior={Platform.OS === 'ios' ? 'padding' : 'height'}.
  • Contexte technique : React Native / Expo cross-platform — app-alexandrie 31-03-2026

Primitive UI couplée au contexte parent (layout ou namespace métier)

Risques

  • Une primitive générique (PageShell, ContentCard, SectionWrapper) qui embarque des classes de surface, de largeur ou de namespace métier devient non réutilisable hors de son premier contexte
  • Le couplage reste silencieux au lint et au typecheck, puis force l'ajout progressif de props variant, layout, width ou de classes externes contradictoires

Symptômes

  • La primitive applique directement des classes comme .card, .card--dashboard, .dashboard__item, .profile__card
  • Le parent doit contourner le style natif de la primitive pour l'utiliser dans un autre écran
  • Les classes namespace__element fuitent dans des composants supposés agnostiques du domaine

Bonnes pratiques / mitigations

  • Une primitive pose le squelette sémantique ; le parent pose la surface visuelle (card, width, background, espacement de contexte)
  • Ne pas injecter de classes de namespace métier sur une primitive générique via class
  • Si une variation réutilisable existe vraiment, l'exprimer via une API explicite et bornée (tone, variant) plutôt que par des classes métier ad hoc
  • Contexte technique : Vue 3 / CSS modulaire — RL799_V2, 02-04-2026

Migration partielle vers un composant standard — classes legacy conservées

Risques

  • La coexistence de classes legacy (.primary, .ghost, .danger) et de classes du nouveau composant (.app-btn--primary, .app-btn--ghost) crée une ambiguïté durable de convention
  • Les nouveaux développements continuent d'utiliser l'ancien système faute de règle claire, ce qui ralentit la standardisation

Symptômes

  • Deux façons de produire la même affordance coexistent dans le même repo
  • Un composant dédié existe, mais des liens ou boutons continuent d'utiliser les anciennes classes globales

Bonnes pratiques / mitigations

  • Lorsqu'un composant standardise une affordance, supprimer en même temps les classes CSS globales équivalentes
  • Si un reliquat legacy doit rester temporairement, documenter explicitement son périmètre et sa date de sortie attendue
  • En review, traiter toute nouvelle utilisation d'une classe legacy équivalente comme une régression de standardisation
  • Contexte technique : Vue 3 / design system léger — RL799_V2, 02-04-2026

ARIA roles sans comportement clavier associé

Risques

  • Poser role="menu" / role="menuitem" sur un composant sans implémenter le pattern clavier donne une fausse impression d'accessibilité
  • Les rôles ARIA trompent les lecteurs d'écran et violent WCAG 2.1 (4.1.2 Name, Role, Value)

Symptômes

  • role="menu" sans fermeture via Escape
  • Pas de navigation ArrowUp / ArrowDown ni de roving tabindex

Bonnes pratiques / mitigations

Poser role="menu" / role="menuitem" implique obligatoirement :

  • Fermeture via Escape
  • Navigation via ArrowUp / ArrowDown
  • Roving tabindex (tabindex="0" sur l'item actif, -1 sur les autres)
  • Focus automatique du premier item à l'ouverture

Règle : ne jamais poser un role ARIA de widget interactif sans implémenter le pattern clavier correspondant (cf. WAI-ARIA Authoring Practices)

  • Contexte technique : Vue 3 / accessibilité — RL799_V2 03-04-2026

Duplication de logique métier dans les composants UI (monorepo)

Risques

  • Dans un monorepo avec un package partagé (shared), les fonctions utilitaires métier (ex: conversion grade → rang) sont redéfinies localement dans les composants ou pages frontend
  • Ce type de duplication silencieuse provoque des divergences à terme

Symptômes

  • Fonction switch/case ou mapping identique à une fonction déjà exportée par shared
  • Même signature et même logique dans plusieurs fichiers de couches différentes (composant, page, service)

Bonnes pratiques / mitigations

  • Les fonctions utilitaires métier ne doivent jamais être redéfinies localement dans les composants ou pages frontend

  • Importer systématiquement depuis le package partagé (@monrepo/shared ou équivalent) plutôt que de copier-coller la logique

  • Signal review : grep des fonctions utilitaires existantes dans shared avant de valider un nouveau switch/case

  • Contexte technique : Vue 3 / monorepo — RL799_V2 06-04-2026


Event listeners globaux pour interactions modales

Risques

  • window.addEventListener('keydown') pour capturer Escape dans une modale crée un listener global qui peut confliter avec d'autres modales
  • Le listener fuit si le composant est mal démonté

Symptômes

  • window.addEventListener('keydown', handler) dans un composant modale
  • Cleanup dans onBeforeUnmount mais risque de fuite si le démontage échoue

Bonnes pratiques / mitigations

  • Utiliser @keydown.escape directement sur l'élément dialog avec tabindex="-1" + focus automatique à l'ouverture

  • Élimine le besoin de cleanup et scope l'interaction au composant

  • Signal review : dans tout composant modale, vérifier que les listeners clavier sont sur l'élément, pas sur window

  • Contexte technique : Vue 3 / modales — RL799_V2 06-04-2026


Boutons imbriqués dans les listes interactives

Risques

  • Un <button> ou <a> contenant un autre élément interactif (bouton, lien) est du HTML invalide
  • Casse l'accessibilité et produit un comportement imprévisible selon les navigateurs

Symptômes

  • <button> conteneur avec un <button> enfant (ex: étoile favori dans une carte cliquable)
  • Comportement de clic imprévisible, événements qui ne remontent pas correctement

Bonnes pratiques / mitigations

  • Utiliser un <div> conteneur avec des boutons séparés côte à côte

  • Si toute la ligne doit être cliquable, séparer la zone de clic principale (bouton content) de l'action secondaire (bouton étoile/action)

  • Signal review : dans tout composant liste avec actions inline, vérifier qu'aucun élément interactif n'est imbriqué dans un autre

  • Contexte technique : HTML / accessibilité — RL799_V2 06-04-2026


Fire-and-forget sans feedback sur actions non-critiques

Risques

  • Une action asynchrone non-critique (cache IndexedDB, analytics, sync) lancée en fire-and-forget sans feedback masque les échecs
  • L'utilisateur croit que l'action est faite (ex: document disponible hors-ligne) alors qu'elle a échoué

Symptômes

  • .then(...).catch(() => {}) sur une action secondaire
  • catch { /* ignore */ } sans log ni feedback visuel

Bonnes pratiques / mitigations

  • Même si l'action est non-bloquante, afficher un feedback discret en cas d'échec (toast, badge absent)

  • L'utilisateur doit pouvoir distinguer "fait" de "échoué silencieusement"

  • Signal review : tout .catch(() => {}) ou catch { /* ignore */ } mérite au minimum un log ou un feedback visuel

  • Contexte technique : frontend / actions async — RL799_V2 07-04-2026


Monorepo ESM — shim runtime .js désynchronisé de l'index TypeScript

Risques

  • Le typecheck passe mais le runtime navigateur casse (named export not found).

Symptômes

  • Erreur Vite/browser sur export absent alors que index.ts est correct.

Bonnes pratiques / mitigations

  • Si un shim .js est maintenu, imposer une mise à jour miroir à chaque nouvel export.

  • Ajouter un test/guard de cohérence exports TS vs JS shim.

  • Contexte technique : monorepo / ESM shim runtime — RL799_V2 15-04-2026


ESLint flat config TypeScript sans tsconfigRootDir

Risques

  • Erreurs de parsing massives en IDE/monorepo selon CWD d'exécution.

Symptômes

  • No TsConfigRootDir / Cannot read tsconfig.json alors que le build TS passe.

Bonnes pratiques / mitigations

  • Toujours définir tsconfigRootDir: import.meta.dirname quand parserOptions.project est utilisé.

  • Redémarrer le serveur ESLint après correction.

  • Contexte technique : tooling / ESLint flat config — RL799_V2 17-04-2026


Risques

  • Réponses sensibles servies depuis cache offline.
  • Comportement d'auth incohérent entre réseau/cached.

Symptômes

  • Session/app state divergents après activation SW ou reprise réseau.

Bonnes pratiques / mitigations

  • Exclure explicitement les routes authentifiées sensibles du cache persistant.

  • Définir une stratégie stricte par classe de route (auth, API privée, assets publics).

  • Contexte technique : PWA / service worker / auth cookie — RL799_V2 18-04-2026


PWA install prompt — capture tardive de beforeinstallprompt

Risques

  • Événement perdu au cold boot, prompt jamais proposé.

Symptômes

  • Implémentation correcte en apparence mais aucun déclenchement sur Android.

Bonnes pratiques / mitigations

  • Installer l'écouteur le plus tôt possible dans le cycle d'initialisation.

  • Ne pas baser la détection iOS uniquement sur l'UA (cas iPad en mode desktop).

  • Contexte technique : PWA / install prompt — RL799_V2 18-04-2026


Cache offline PWA + soft-delete — invalidation diff-based scopée

Risques

  • Une PWA qui cache des blobs en IndexedDB pour offline reading ne reçoit aucun signal automatique quand un document est soft-deleted côté serveur
  • Le contenu reste lisible hors-ligne indéfiniment. Pour des données réglementaires ou sensibles, c'est un gap de sécurité non négligeable

Symptômes

  • Document soft-deleted en base, encore consultable offline par les utilisateurs qui l'ont mis en cache
  • Aucun mécanisme automatique de purge

Bonnes pratiques / mitigations

Mitigation V1 (best-effort, diff-based) au prochain loadEntries online :

  1. Lire la liste serveur courante (filtrée deletedAt IS NULL)
  2. Lire les IDs cachés localement scopés au même périmètre (type+grade) — sinon on supprime à tort un doc d'un autre onglet
  3. Diff : cached - server = soft-deletedremoveCachedDocument(id) pour chaque
const bustCachedIfMissing = async (candidateIds: string[], serverIds: Set<string>) => {
  for (const id of candidateIds) {
    if (!serverIds.has(id)) await removeCachedDocument(id);
  }
};

Mitigation V2 (push server-initiated) : Service Worker abonné à un canal (postMessage / WebSocket / SSE), serveur publie { type: 'document-soft-deleted', id } sur soft-delete, SW intercepte et fait caches.delete() immédiat. Coût : infra push + gestion connectivité partielle. À garder en backlog si le risque devient critique (audit GDPR).

Tests recommandés :

  • doc caché + recharge avec serveur qui omet ce doc → assert removeCachedDocument appelé

  • doc caché + serveur qui retourne le doc → assert pas d'effet (non-régression)

  • doc caché pour scope rituels + recharge sur scope mementos qui omet ce doc → assert pas d'effet (scope isolation)

  • Contexte technique : PWA / IndexedDB — RL799_V2 20-04-2026


Duplication parent ↔ modal de la capture de focus

Risques

  • Quand un composant modal implémente correctement le pattern a11y previousActiveElement (capture à onMounted, restitution à close/submit), le composant parent ne doit PAS stocker un lastTrigger en parallèle
  • Code mort avec commentaire trompeur : un lecteur qui cherche à comprendre le flux focus va s'y perdre

Symptômes

  • Le parent a une ref lastFabTrigger / lastTrigger écrite au clic du trigger
  • Elle n'est jamais lue — le commentaire prétend "pour restitution du focus par la modal", mais la modal a son propre mécanisme

Bonnes pratiques / mitigations

  • Une seule responsabilité pour la restitution du focus : la modal

  • Le parent se contente d'ouvrir la modal (uploadModalOpen.value = true)

  • Le parent ne capture le focus QUE si la modal ne le fait pas (cas d'un overlay maison sans pattern previousActiveElement)

  • Repérage en code review : grep -n "lastTrigger\|previousActive" components/ pages/ → s'il y a des occurrences dans BOTH un parent et une modal du même flux, c'est le signal

  • Contexte technique : Vue 3 / a11y modales — RL799_V2 20-04-2026


touch-action: none sur card mobile bloque scroll vertical

Risques

  • Une card mobile gérant un swipe horizontal avec touch-action: none capture tout le toucher, laissant le JS gérer le scroll vertical
  • Le JS détecte scroll vs swipe via un seuil mais doit libérer l'événement après l'avoir analysé → un délai imperceptible s'installe, le scroll vertical devient saccadé et souvent ignoré
  • L'utilisateur trouve la liste "inscrollable" quand son pouce touche directement les cards

Symptômes

  • Poser le pouce sur une card puis scroller ne marche qu'une fois sur 20
  • Scroll OK dans les marges vides à côté
  • Aucune erreur console

Bonnes pratiques / mitigations

/* AVANT — bug : scroll vertical capturé */
.member-card {
  touch-action: none;
}

/* APRÈS — scroll natif OK, swipe horizontal toujours fonctionnel */
.member-card {
  touch-action: pan-y;
}

Combiné avec un handler JS qui preventDefault uniquement sur mouvement horizontal significatif (> 10 px) :

if (Math.abs(deltaX) > 10 && Math.abs(deltaX) > Math.abs(deltaY)) {
  event.preventDefault();
}

Règle :

  • Card qui gère swipe horizontal ET scroll vertical : touch-action: pan-y (le navigateur gère nativement le scroll vertical, seul l'axe horizontal est laissé au JS)
  • Card qui ne gère QUE du pan/zoom custom (rare) : touch-action: none peut se justifier
  • Tous les autres cas : laisser la valeur par défaut (auto)

Repérage en code review : grep -rn "touch-action: none" components/ → chaque occurrence est suspecte.

  • Contexte technique : CSS / mobile — RL799_V2 21-04-2026

<button> wrapper card — toujours color: inherit (reset user-agent)

Risques

  • Quand on utilise un <button> comme wrapper cliquable d'une card (pattern idiomatique pour l'a11y clavier), il hérite du color user-agent par défaut
  • Sur certains setups (Safari/dark mode notamment), ce color peut être bleu — le texte enfant hérite et contamine titres et paragraphes qui devraient prendre la couleur du thème

Symptômes

  • Titre de card en bleu sur fond sombre alors que le thème prévoit de l'or
  • Bug non visible en dev light mode sur Chrome — apparaît uniquement sur certains setups → difficile à reproduire

Bonnes pratiques / mitigations

.my-card-button {
  /* reset user-agent */
  color: inherit;
  font-family: inherit;
  border: 0;
  background: transparent;
  /* … */
}

Règle : tout <button> qui wrappe du contenu stylé par le thème (card, liste, ligne de tableau) doit reset color, font-family, font-size, border, background. C'est un bootstrap user-agent minimal à prévoir dans tout design system.

Alternative : <div role="button" tabindex="0"> avec @keydown.enter/space. Plus verbeux, mais évite les resets. Pattern valide si l'équipe est à l'aise avec les implications a11y.

  • Contexte technique : CSS / a11y — RL799_V2 21-04-2026

Erreur silencieuse = blanc indistinguable de "aucune donnée" — 4 états distincts (loading / empty / error / forbidden)

Risques

  • Un composant qui affiche le résultat d'un fetch sans distinguer ses 4 états produit du blanc dans 3 cas sur 4 — l'utilisateur ne peut pas savoir si la donnée est en chargement, légitimement vide, en erreur réseau, ou refusée par RBAC
  • Sur les flows critiques (rituel, opérations sensibles), un blanc silencieux est inacceptable : l'utilisateur prend des décisions sur la base de l'affichage

Symptômes

  • Plusieurs cards d'une vue qui auto-fetchent et tombent toutes en [] côté front quand l'API renvoie 403 — affichage indistinguable de "vide légitime"
  • Toast générique "Une erreur est survenue" sans corrélation avec un retry actionnable

Bonnes pratiques / mitigations

Tout composant qui fetch une ressource doit avoir 4 états distincts dans son rendu :

  1. Loading : skeleton, spinner, ou texte explicite (« Chargement… »)
  2. Empty (donnée légitimement vide) : message explicite « Aucune donnée enregistrée pour … »
  3. Error (réseau / serveur) : message + bouton retry. Ne jamais se contenter d'un blanc
  4. Forbidden (403) : message explicite « Vous n'avez pas accès à cette donnée » + suggestion d'action (recharger / contacter admin)

Le frontend doit savoir distinguer 403 des autres erreurs au niveau de son service HTTP, et propager l'info au composant. Ne pas traiter !response.ok en bloc avec un message générique.

export const getXxx = async (id: string) => {
  const response = await apiFetch(`/api/xxx/${id}`);
  if (response.status === 403) throw new ForbiddenError(...);
  if (response.status === 404) throw new NotFoundError(...);
  if (!response.ok) throw new Error(await parseError(response));
  return (await response.json()).data;
};

Le composant catche les types d'erreur et choisit le rendu approprié.

  • Contexte technique : Vue 3 / fetch — RL799_V2 27-04-2026

Service Worker invisible en accès non-secure (HTTP via IP réseau)

Risques

  • Les Service Workers exigent un secure context — HTTPS strict, OU URL http://localhost:* ou http://127.0.0.1:*
  • Un accès via IP réseau en HTTP (Tailscale 100.x.x.x, LAN 192.168.x.x) est non-secure → navigator.serviceWorker est undefined → la PWA fonctionne en mode "navigateur classique" mais sans cache offline, sans push, sans badge, sans installation
  • Les tests E2E en Tailscale loupent silencieusement les régressions SW

Symptômes

  • navigator.serviceWorker.register('/sw.js') lève Cannot read properties of undefined (reading 'register')
  • DevTools > Application > Service Workers ne montre rien
  • Tests "ça marche en LAN" qui ne reflètent pas la prod HTTPS

Bonnes pratiques / mitigations

// Détection
console.log(window.isSecureContext, location.protocol, location.hostname);
// true "https:" "..." → OK
// true "http:" "localhost" → OK
// false "http:" "192.168.1.42" → SW désactivé

Stratégies par contexte :

  1. Test local : utiliser http://localhost:<port> strict (jamais l'IP, même en LAN)
  2. Test réseau / mobile : reverse proxy HTTPS (Caddy/Traefik avec Let's Encrypt, ou tailscale cert pour le magicDNS Tailscale)
  3. Préview Vite : vite preview --https avec un certificat auto-signé (acceptable en dev test)

Préventif :

  • documenter dans le README projet que le SW exige HTTPS/localhost

  • en CI E2E, toujours utiliser localhost (le webServer Playwright tourne sur localhost par défaut)

  • ne PAS supposer qu'un test "ça marche en LAN" reflète la prod HTTPS

  • Contexte technique : PWA / Service Worker — RL799_V2 28-04-2026


Vite-plugin-pwa : bascule generateSWinjectManifest rend runtimeCaching inerte

Risques

  • En mode injectManifest, Vite PWA n'injecte PAS de runtime workbox — il bundle le src/sw.ts fourni tel quel
  • Toute la config workbox.* du vite.config.ts (sauf globPatterns/globIgnores déplacés sous injectManifest.*) est ignorée silencieusement, sans warning
  • Régression directe : leak de cookies API en cache, contenu sensible en cache, 404 transformés en index.html

Symptômes

  • Après bascule, les routes runtime configurées dans workbox.runtimeCaching n'ont plus aucun effet
  • Le build passe, aucune erreur visible, mais en DevTools > Application > Service Worker on ne voit pas les routes attendues

Bonnes pratiques / mitigations

Détection : examiner dist/sw.js après build — chercher des strings clés (/api/, uploads, manifest.webmanifest, addEventListener). Si elles sont absentes du SW custom, c'est qu'on a perdu la protection.

Mitigation : RÉIMPLÉMENTER À LA MAIN dans src/sw.ts toutes les routes runtime, denylist, et cleanup :

import { precacheAndRoute, cleanupOutdatedCaches } from 'workbox-precaching';
import { registerRoute, NavigationRoute } from 'workbox-routing';
import { NetworkOnly, NetworkFirst } from 'workbox-strategies';
import { ExpirationPlugin } from 'workbox-expiration';

cleanupOutdatedCaches();
precacheAndRoute(self.__WB_MANIFEST);

registerRoute(({ url }) => url.pathname.startsWith('/api/'), new NetworkOnly());
registerRoute(({ url }) => url.pathname.startsWith('/uploads/'), new NetworkOnly());
registerRoute(
  ({ url }) => url.pathname === '/manifest.webmanifest',
  new NetworkFirst({
    cacheName: 'manifest-cache',
    plugins: [new ExpirationPlugin({ maxAgeSeconds: 300, maxEntries: 1 })],
  }),
);

registerRoute(
  new NavigationRoute(async () => { /* fallback handler */ }, {
    denylist: [/^\/api\//, /^\/uploads\//, /^\/manifest\.webmanifest$/],
  }),
);

Vérifications obligatoires post-bascule (DevTools sur build/preview) :

  1. Application > Service Worker : sw.js activé
  2. Network : POST /api/auth/login → pas de "(from ServiceWorker)", pas de cache
  3. Network : GET /uploads/foo.pdf → réseau direct
  4. Network mode Offline : navigation /page → fallback index.html ; /api/foo → erreur réseau (PAS index.html)
  5. Déploiement v2 : ancien cache purgé après activation

setCatchHandler ne suffit PAS à remplacer navigateFallback — il ne se déclenche que si une route enregistrée throw. Pour le navigation fallback, utiliser NavigationRoute explicitement.

  • Contexte technique : Vite / vite-plugin-pwa / Workbox — RL799_V2 28-04-2026

TS strict — Uint8Array<ArrayBufferLike> non assignable à BufferSource

Risques

  • TS 5.7+ avec lib DOM récente paramètre Uint8Array par défaut sur ArrayBufferLike (qui inclut SharedArrayBuffer)
  • Beaucoup d'APIs DOM (Push API, WebCrypto certaines surfaces) attendent un BufferSource strict avec buffer: ArrayBuffer — d'où une erreur TS au build

Symptômes

Type 'Uint8Array<ArrayBufferLike>' is not assignable to type 'BufferSource'.
  Types of property 'buffer' are incompatible.
    Type 'ArrayBufferLike' is not assignable to type 'ArrayBuffer'.
      Type 'SharedArrayBuffer' is missing the following properties from type 'ArrayBuffer'…

L'erreur est au build, pas au runtime (le code marche en JS).

Bonnes pratiques / mitigations

Créer explicitement un ArrayBuffer strict, puis remplir via une vue Uint8Array :

// ❌ Ne compile pas en TS strict
const urlBase64ToUint8Array = (s: string): Uint8Array => {
  const raw = atob(s.replace(/-/g, '+').replace(/_/g, '/'));
  const out = new Uint8Array(raw.length);
  for (let i = 0; i < raw.length; i++) out[i] = raw.charCodeAt(i);
  return out;
};

// ✅ Compile et fonctionne identiquement
const urlBase64ToArrayBuffer = (s: string): ArrayBuffer => {
  const raw = atob(s.replace(/-/g, '+').replace(/_/g, '/'));
  const buf = new ArrayBuffer(raw.length);
  const view = new Uint8Array(buf);
  for (let i = 0; i < raw.length; i++) view[i] = raw.charCodeAt(i);
  return buf;
};

Alternative déconseillée : as ArrayBuffer cast — masque le problème, peut rater une vraie incompatibilité si la lib DOM évolue.

  • Contexte technique : TypeScript 5.x / lib DOM — RL799_V2 28-04-2026

Safe-areas iOS — viewport-fit=cover indispensable

Risques

  • Sur iPhone Pro/Max (notch + home indicator + Dynamic Island), padding-top: env(safe-area-inset-top) ou padding-bottom: env(safe-area-inset-bottom) retourne 0 sans <meta viewport content="… viewport-fit=cover">
  • Le développeur conclut à tort que le pattern ne fonctionne pas, alors qu'il manque juste l'opt-in

Symptômes

  • Header fixed rogné par le notch
  • Nav bottom rognée par le home indicator
  • env(safe-area-inset-*) retourne 0 sur iPhone Pro+

Bonnes pratiques / mitigations

Pattern complet (3 endroits) à appliquer ensemble :

<!-- 1) index.html — opt-in safe-areas -->
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
/* 2) Header sticky : top + safe-area-inset-top */
.app-header {
  position: fixed;
  top: 0;
  padding-top: env(safe-area-inset-top);
  height: calc(var(--size-header-height) + env(safe-area-inset-top));
}

/* 3) Nav bottom : bottom + safe-area-inset-bottom + safe-area-inset-left/right */
.app-nav {
  position: fixed;
  bottom: 0;
  padding-bottom: env(safe-area-inset-bottom);
  padding-left: env(safe-area-inset-left);
  padding-right: env(safe-area-inset-right);
  height: calc(var(--size-nav-height) + env(safe-area-inset-bottom));
}

/* FAB / menu flottant : bottom au-dessus de la nav + safe-area + offset */
.fab {
  bottom: calc(var(--size-nav-height) + env(safe-area-inset-bottom) + 16px);
  /* `max()` empêche les boutons d'être collés au bord rond du device */
  right: max(16px, env(safe-area-inset-right));
}

Règle de débogage : si env(safe-area-inset-bottom) semble retourner 0 sur iPhone Pro+, vérifier <meta viewport> AVANT de chercher ailleurs. C'est presque toujours la cause.

safe-area-inset-left et safe-area-inset-right ne sont non-nuls qu'en mode paysage (notch latéral). Garder le padding-left/right quand même → no-op en portrait, fix en paysage.

  • Contexte technique : CSS / iOS — RL799_V2 02-05-2026

<input type="date"> Safari iOS — appearance: none + min-width: 0 + min-height obligatoires

Risques

  • Sur Safari iOS (et Chrome iOS car webkit sous-jacent), un <input type="date"> ou datetime-local :
    • déborde de son conteneur sur la droite (largeur intrinsèque > 100 %)
    • apparaît plus mince que les autres inputs (hauteur intrinsèque différente)
    • affiche un styling natif iOS qui casse le design system

Symptômes

  • width: 100% ne suffit pas — la largeur intrinsèque écrase la contrainte
  • Bug non reproductible sur Chrome desktop, visible uniquement sur iPhone réel ou Safari Responsive Design Mode

Bonnes pratiques / mitigations

input[type="date"],
input[type="datetime-local"],
input[type="time"] {
  appearance: none;          /* neutralise le styling natif */
  -webkit-appearance: none;  /* Safari iOS — ne PAS oublier */
  min-width: 0;              /* permet à width: 100% de gagner */
  min-height: 48px;          /* aligne avec les autres inputs */
}

Les 4 propriétés sont nécessaires :

  • appearance: none seul : le styling natif disparaît mais la largeur intrinsèque reste → débordement

  • min-width: 0 seul : le styling natif reste, on a juste cassé sa hauteur

  • min-height: 48px : nécessaire pour homogénéiser avec les inputs text classiques

  • -webkit-appearance: none : redondant en théorie avec appearance: none mais nécessaire en pratique sur certaines versions Safari iOS

  • Contexte technique : CSS / Safari iOS — RL799_V2 01-05-2026


<fieldset> / <legend> cassent un layout flex inline

Risques

  • Pour un champ "label + valeur inline" (ex : Grade [GradeBadge] sur la même ligne), le réflexe sémantique est <fieldset> + <legend>
  • <legend> a un comportement natif particulier : interrompt visuellement le border du fieldset, son display est traité spécialement, il ne se comporte pas comme un enfant flex/grid normal
  • Le legend prend toute la largeur, l'input passe en dessous, impossible de les aligner sans hacks position: absolute

Symptômes

  • <fieldset style="display: flex"> avec <legend> + <input> qui ne s'alignent pas sur la même ligne

Bonnes pratiques / mitigations

Pour un groupe de champs avec layout custom (flex/grid inline), utiliser <div> + <span class="label"> + le champ :

<!-- ❌ Layout cassé : legend ne se comporte pas comme un enfant flex -->
<fieldset class="field-inline">
  <legend>Grade</legend>
  <GradeBadge :grade="grade" />
</fieldset>

<!-- ✅ Layout custom OK : div + span — perte sémantique mineure
     compensée par aria-labelledby si besoin -->
<div class="field-inline">
  <span class="field-inline__label" id="grade-label">Grade</span>
  <GradeBadge :grade="grade" aria-labelledby="grade-label" />
</div>

Garder <fieldset>/<legend> uniquement quand on accepte le rendu natif (groupe vertical avec border + legend qui chevauche le border supérieur — pattern formulaire admin classique).

Trade-off à assumer : <fieldset> apporte une sémantique a11y (groupe de champs liés). En remplaçant par <div>, on peut compenser via role="group" aria-labelledby="..." si le besoin d'a11y est fort. Pour un simple label+badge inline, c'est rarement nécessaire.

  • Contexte technique : HTML / a11y / CSS — RL799_V2 02-05-2026