diff --git a/knowledge/frontend/patterns/README.md b/knowledge/frontend/patterns/README.md index 9fa4216..cb9587a 100644 --- a/knowledge/frontend/patterns/README.md +++ b/knowledge/frontend/patterns/README.md @@ -8,10 +8,10 @@ Avant toute proposition frontend, identifie le fichier dont le nom et la descrip | Fichier | Domaine | Entrées clés | |---------|---------|--------------| -| `state.md` | State management, UI states, Zustand, listes paginées, refactor monolithe Vue | États UI loading/empty/error, séparation server/client state, refresh idempotent, UI admin légère, refactor monolithe Vue sous-lots Go/No-Go, convention `pages//`, `styles.css` partagé non-scoped, annuaire client-side TTL | +| `state.md` | State management, UI states, Zustand, listes paginées, refactor monolithe Vue | États UI loading/empty/error, séparation server/client state, refresh idempotent, UI admin légère, refactor monolithe Vue sous-lots Go/No-Go, convention `pages//`, `styles.css` partagé non-scoped, annuaire client-side TTL, race-token partagé (latest-wins), capture synchrone before async, event bus via timestamp, clé de cache composite, loadings séparés initial/pagination, flags d'état séparés par préoccupation, dérivé = `computed`, une source deux vues lecture inerte, noyau visuel générique + variants | | `forms.md` | Formulaires, validation, Server Actions, optimistic UI | Formulaire robuste, toggle optimiste rollback, Server Action retourne entité, AppInput Outlined Material thème dark, fusion DRY composants jumeaux par prop discriminante | -| `navigation.md` | Navigation, routing, Expo Router, intégrations tierces | Navigation réactive post-action async, link-out page locale canonique, factorisation page mode dynamique via `meta.mode` typé | -| `design-tokens.md` | Design tokens, typographie, spacing, Tailwind, RN StyleSheet | Tokens TypeScript Expo/RN, typography sémantique, export styles composant, grilles 2 colonnes | +| `navigation.md` | Navigation, routing, Expo Router, intégrations tierces | Navigation réactive post-action async, link-out page locale canonique, factorisation page mode dynamique via `meta.mode` typé, tab bar native cachée (wiring routing pur), état UI éphémère via `useFocusEffect`, routes typées `Href`, stack indépendant par tab, routing décorrélé du rendu (builder pur) | +| `design-tokens.md` | Design tokens, typographie, spacing, Tailwind, RN StyleSheet | Tokens TypeScript Expo/RN, typography sémantique, export styles composant, grilles 2 colonnes, palette light/dark MD3 + `useThemedColors` + dual export, map sémantique slug → token, migration tokens typo formalisés | | `nextjs.md` | Next.js App Router, embeds, ESLint | Click-to-load embeds tiers, ESLint flat config Next.js | | `tests.md` | Tests styles React Native, smoke checks, mount + mock composable | Tests de styles sans renderer JSX, smoke checks `readFileSync`, classe CSS modifier vs texte, cleanup E2E best-effort, helpers SW purs, mount + mock composable, assertions React Email, garde-fous de non-activation feature parking Later | -| `general.md` | Focus visible, inputs date HTML5, journaux/audit logs, pages admin | Focus visible interne pour overflow clip, restyle global ``, UI patterns journaux d'audit, structuration pages admin (eyebrows + grille filtres + variante danger) | +| `general.md` | Focus visible, inputs date HTML5, journaux/audit logs, pages admin, composants génériques | Focus visible interne pour overflow clip, restyle global ``, UI patterns journaux d'audit, structuration pages admin, reset défensif pointer events, modale dédiée vs partagée, fail-fast branche unreachable `__DEV__`, extension rétrocompatible vs sibling, CTA toggle async préserver l'état, helpers fail-safe data backend, prop de raccourci, compositions sémantiques utilisées, touch target 44 minWidth+minHeight, helpers temps/date purs, avatar initiale pastel, composants génériques + états de chargement (EmptyState/Skeleton) | diff --git a/knowledge/frontend/patterns/design-tokens.md b/knowledge/frontend/patterns/design-tokens.md index 3c0259f..33efd28 100644 --- a/knowledge/frontend/patterns/design-tokens.md +++ b/knowledge/frontend/patterns/design-tokens.md @@ -184,3 +184,106 @@ it('variante primary utilise colors.primary', () => { 1. `.spec.ts` (node) : tokens, valeurs, logique pure 2. `.spec.tsx` (config séparée avec renderer) : rendu visuel, interactions + +--- + + +## Pattern : Palette light/dark MD3 + hook `useThemedColors` + dual export + +### Synthèse + +- **Objectif** : poser un theming light/dark/system complet sur une codebase qui n'avait qu'une palette unique, sans casser les imports historiques. +- **Contexte** : React Native / Expo avec préférence utilisateur light/dark/system. +- **Quand l'utiliser** : introduction d'un 2ᵉ thème sur une base existante. +- **Quand l'éviter** : thème unique sans besoin de bascule (le hook serait du sucre sans bénéfice). + +### Analyse + +- **Avantages** : + - compat ascendante : `export const colors = dark` garde les imports `import { colors }` compilants + - le hook `useThemedColors` rend le reste mécanique (le composant ne code jamais le scheme en dur) + - dual export pour les styles legacy : `export const fooStyles = makeStyles(colors)` (snapshot dark) reste synchronisé avec la fonction dynamique +- **Limites / vigilance** : + - anti-pattern : palette plate sans typage par mode → force à coder le scheme en dur ou à dupliquer les styleSheets + - tokens "fixed" partagés light/dark (`primaryContainer`, `primaryFixedDim`) pour la continuité du branding ; `inverse-primary` d'un mode = `primary` de l'autre + - shadow : `#000000` OK en dark, mais en light un tinted neutre (ex. `outline` à 8 %), jamais noir pur + - validation WCAG : README documentant chaque token + ratio mesuré (texte ≥ 4.5:1, UI ≥ 3:1) + +### Validation + +- Validé le : 29-05-2026 +- Contexte technique : React Native / Expo — app-alexandrie (ux-cleanup-8, ~95 fichiers migrés) + +### Implémentation + +```ts +// theme/colors.ts +const dark = { background: '#131316', primary: '#cdbdff' } as const; +const light = { background: '#fbf8fe', primary: '#4c00c9' } as const; +export const colors = dark; // compat ascendante (imports historiques) +export const colorsLight = light; +export const colorsDark = dark; + +// theme/use-themed-colors.ts +export function useThemedColors() { + return useEffectiveColorScheme() === 'light' ? colorsLight : colorsDark; +} + +// composant +const themed = useThemedColors(); +const styles = useMemo(() => makeStyles(themed), [themed]); +function makeStyles(c: ReturnType) { + return StyleSheet.create({ container: { backgroundColor: c.background } }); +} + +// styles exportés (compat tests) — source unique +export const fooStyles = makeStyles(colors); // snapshot dark +``` + +> Les pièges de migration associés (composants tiers, `` sans `color`, scheme effectif, ThemeProvider) sont documentés dans `risques/design-tokens.md#risque-theming-light-dark-pieges-caches`. + +--- + + +## Pattern : Map sémantique slug → token + icône + +### Synthèse + +- **Objectif** : différencier visuellement des entités d'un même type (forums, packs, badges) via une map slug → token résolue au runtime, sans hex en dur. +- **Contexte** : entités multiples d'un même type nécessitant un code couleur/icône cohérent light/dark. +- **Quand l'utiliser** : ≥ 2 entités à différencier visuellement. +- **Quand l'éviter** : entité unique ou différenciation purement textuelle. + +### Analyse + +- **Structure** : `getEntityBadgeTokens(slug) → { bgColorKey, fgColorKey, icon }`, le caller résout via `useThemedColors` (suit le thème). +- **Règles** : (1) toujours des `ColorKey` de `useThemedColors`, jamais de hex ; (2) toujours un fallback neutre (un slug futur ne casse pas l'UI) ; (3) icônes outline légères ; (4) tests env node sur map + fallback + distinctness. +- **Avantages** : vs if/else inline → 0 duplication ; vs hex map → suit le thème ; vs composant → reste un helper pur testable env node. + +### Validation + +- Validé le : 29-05-2026 +- Contexte technique : React Native — app-alexandrie (`forum-badge.ts`, ux-cleanup-15) + +--- + + +## Pattern : Migration tokens typo formalisés + +### Synthèse + +- **Objectif** : formaliser et migrer les valeurs typographiques en dur (`fontSize`, `fontWeight`, `fontFamily`) vers des tokens, comme le sweep tokens couleur. +- **Contexte** : codebase avec ~100 occurrences de valeurs typo en dur sur des dizaines de fichiers. +- **Quand l'utiliser** : volumétrie significative de valeurs typo dispersées. +- **Quand l'éviter** : poignée d'usages localisés. + +### Analyse + +- **Méthodologie en 4 étapes** : (1) audit grep + rapport stats ; (2) étendre `typography.ts` SANS renommer les tokens existants (rétro-compat) — ajouter des aliases sémantiques ; (3) migration mécanique (perl/sed) + imports, typecheck après chaque batch ; (4) doc README (table sémantique + anti-patterns). +- **Cas dégénérés à laisser en dur** : `fontSize: 48/64` sur un emoji/hero = taille d'icône, pas de la typo → ne pas créer un token `iconLarge` (documenter l'exception). +- **Anti-patterns** : codemod auto sans review visuel (un `15 → 14` casse un layout) ; renommer un token existant (`fontSize.tab` utilisé partout → casse 50+ fichiers) ; créer `useThemedTypography()` si la typo ne varie pas light/dark (sucre sans bénéfice, garder l'import statique). + +### Validation + +- Validé le : 30-05-2026 +- Contexte technique : React Native — app-alexandrie (ux-cleanup-12, 110 occurrences / 33 fichiers, 0 régression) diff --git a/knowledge/frontend/patterns/general.md b/knowledge/frontend/patterns/general.md index 6cda0b8..faf8c94 100644 --- a/knowledge/frontend/patterns/general.md +++ b/knowledge/frontend/patterns/general.md @@ -375,3 +375,371 @@ Sur un écran qui mélange actions constructives et destructives (ex : saisie in - 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 + +--- + + +## 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'); +}; +``` + +--- + + +## 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`) + +--- + + +## 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 + // … +} +``` + +--- + + +## 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é : +``` + +--- + + +## 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 + + {isFollowing && } + {isLoading ? : } + +``` + +--- + + +## 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; +} +``` + +--- + + +## 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 (``) appelé partout avec le même wrapper (`icon={📭}`). +- **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 (``) ou refactorer. + +### Validation + +- Validé le : 30-05-2026 +- Contexte technique : React Native — app-alexandrie (`EmptyState`, ux-cleanup-15) + +--- + + +## 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) + +--- + + +## 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 (``, `