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>
This commit is contained in:
MaksTinyWorkshop
2026-05-02 22:12:44 +02:00
parent 02ad0de258
commit b3417ad77b
31 changed files with 5370 additions and 12 deletions

View File

@@ -128,3 +128,166 @@ setItems((prev) => [...prev, created]); // pas de router.refresh()
```
**Pour les entités avec relations :** utiliser un helper `findItemById(tenantId, id)` appelé après la mutation pour retourner la forme complète avec les relations résolues.
---
<a id="pattern-app-input-outlined-material-dark"></a>
## Pattern : AppInput Outlined Material adapté thème dark
### Synthèse
- **Objectif** : homogénéiser tous les inputs de l'app avec un design "outlined Material" adapté à un thème dark custom (label flottant, encoche opaque calée sur la card parente).
- **Contexte** : projet Vue/React avec un thème dark où les inputs natifs cassent le design system (couleur d'encoche, débordement Safari iOS, fond input transparent).
- **Quand l'utiliser** : design system app-wide où tous les inputs doivent suivre la même grammaire visuelle.
- **Quand l'éviter** : design strictement neutre (inputs natifs OS-style) ou framework UI déjà opinioné (Vuetify, Material UI).
### Analyse
- **Avantages** :
- design cohérent sur tous les formulaires (login, profil, modales)
- encoche calée sur la **card parente**, pas sur le bg global → fusion visuelle propre
- `appearance: none` + `min-width: 0` + `min-height: 48px` corrigent les inputs date Safari iOS
- **Limites / vigilance** :
- les pièges (couleur d'encoche, débordement, fond transparent) sont non-évidents et coûtent du temps à chaque itération si non documentés
- `inheritAttrs: false` + séparation manuelle class/style obligatoires pour permettre le layout grid externe sans fuite des attrs HTML
### Validation
- Validé le : 01-05-2026
- Contexte technique : Vue 3 Composition API — RL799_V2
### Composant central
```vue
<script setup lang="ts">
defineOptions({ inheritAttrs: false });
const props = defineProps<{
modelValue?: string | number;
label: string; // requis — pas d'input sans label
type?: string;
staticLabel?: boolean; // label toujours haut (utile pour selects)
}>();
const attrs = useAttrs();
// Séparation manuelle class/style — permet layout grid externe (--col-2)
// sans que les attrs HTML fuitent sur le wrapper
const wrapperClass = computed(() => attrs.class);
const wrapperStyle = computed(() => attrs.style);
const inputAttrs = computed(() => {
const { class: _c, style: _s, ...rest } = attrs;
return rest;
});
</script>
<template>
<label :class="['app-input', wrapperClass]" :style="wrapperStyle">
<input
:value="modelValue"
placeholder=" "
v-bind="inputAttrs"
class="app-input__control"
@input="$emit('update:modelValue', $event.target.value)"
/>
<span class="app-input__label">{{ label }}</span>
</label>
</template>
<style scoped>
.app-input__control {
width: 100%;
min-width: 0; /* CRITIQUE Safari iOS — sinon débordement */
min-height: 48px; /* homogénéise date/datetime-local */
padding: 12px;
background: transparent; /* fusion totale avec la card parente */
border: 1px solid var(--color-border-base);
border-radius: 6px;
appearance: none; /* CRITIQUE iOS pour datetime-local */
-webkit-appearance: none;
}
/* Label flottant quand input rempli OU focus */
.app-input__control:focus + .app-input__label,
.app-input__control:not(:placeholder-shown) + .app-input__label {
top: 0;
font-size: 0.75rem;
/* Encoche opaque calée sur la CARD parente, pas le bg global */
background: var(--app-input-notch-bg, var(--color-surface-raised));
}
</style>
```
### Pièges documentés
1. **Encoche du mauvais bg** : utiliser `--color-bg-elevated` (canvas global) au lieu de `--color-surface-raised` (card) → patch coloré visible. Toujours caler sur la couleur de la card parente. Si l'input vit dans un contexte différent (modale, header), exposer `--app-input-notch-bg` en custom property pour override.
2. **Bordure traverse le label** : si le label flottant n'a pas de `background` opaque, la bordure passe derrière le texte. L'encoche n'est pas optionnelle.
3. **Fond transparent obligatoire** : si le fond de l'input est différent de la card, l'encoche révèle un patch. Solution : `background: transparent` → fusion totale.
4. **`placeholder=" "` imposé** : le sélecteur `:placeholder-shown` ne marche que si un placeholder existe. Un espace suffit, n'apparaît pas visuellement.
5. **`inheritAttrs: false` + séparation class/style** : sans ça, un parent qui pose `class="--col-2"` voit cette classe ET tous les attrs HTML atterrir sur le wrapper.
### Variantes
- **`AppSelect`** : même base, label toujours flottant haut. Chevron SVG `stroke="currentColor"` 1.5px.
- **`AppTextarea`** : label toujours flottant haut, pas de slot trailing.
---
<a id="pattern-fusion-dry-composants-jumeaux"></a>
## Pattern : Fusion DRY de composants jumeaux par prop discriminante
### Synthèse
- **Objectif** : factoriser deux composants partageant la même UI à 80 %+ avec des contextes d'appel différents, sans extraire un 3ᵉ composant `Body` qui multiplie les fichiers et les indirections.
- **Contexte** : ex `ConvocationResponseCard` (autonome, charge ses données) + `ConvocationResponseForm` (reçoit la convocation du parent) — même UI, deux modes de consommation.
- **Quand l'utiliser** : diff entre les deux composants tient en une dizaine de `computed`/`v-if` discriminés sur **un seul flag de mode**.
- **Quand l'éviter** :
- cycles de vie ou stores différents (un consomme un store Pinia, l'autre est purement contrôlé) — le `computed` discriminant pollue tout le composant
- UI diverge à > 30 % (sections présentes dans l'un, absentes dans l'autre)
### Analyse
- **Avantages** :
- une seule source de vérité pour markup et styles
- tests structurels consolidés sur un seul fichier
- évolution UX synchronisée par construction
- **Limites / vigilance** :
- **anti-pattern à refuser** : extraire un 3ᵉ composant `Body` partagé entre les deux composants originaux. Multiplie les fichiers, ajoute une couche d'indirection (props drilling, events bubbling) sans réduire la complexité réelle
### Validation
- Validé le : 01-05-2026
- Contexte technique : Vue 3 — RL799_V2 (530 lignes dupliquées → 310 lignes uniques)
### Implémentation
```vue
<script setup lang="ts">
const props = defineProps<{
// Mode A : Card autonome (consomme un dataset complet)
data?: ProchaineTenueData;
// Mode B : Form simple (reçoit juste l'objet à éditer)
convocation?: ProchaineTenueConvocation;
}>();
const isCardMode = computed(() => props.data !== undefined);
// Convocation dérivée selon le mode → le reste du composant manipule
// uniquement `convocation.value`, sans se soucier du mode
const convocation = computed(() => {
if (isCardMode.value) {
const primary = props.data!.primaryGrade;
return props.data!.gradeInfos[primary]?.convocation;
}
return props.convocation;
});
// Émission typée en union — chaque mode émet son type approprié
const emit = defineEmits<{
updated: [ProchaineTenueData | ConvocationResponseData];
}>();
</script>
```
### Critère de décision
Si le diff entre les deux composants tient en une dizaine de `computed`/`v-if` discriminés sur **un seul flag de mode**, fusionner. Si ça déborde, garder distincts.