mirror of
https://github.com/MaksTinyWorkshop/_Assistant_Lead_Tech
synced 2026-05-18 08:18:15 +02:00
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>
12 KiB
12 KiB
Frontend — Patterns : Forms
Extrait de la base de connaissance Lead_tech. Voir
knowledge/frontend/patterns/README.mdpour l'index complet.
Pattern : Formulaire robuste avec validation et erreurs explicites
Synthèse
- Objectif : garantir des formulaires fiables, compréhensibles et maintenables.
- Contexte : toute interface avec saisie utilisateur et règles métier.
- Quand l'utiliser : dès qu'un formulaire dépasse un simple champ isolé.
- Quand l'éviter : formulaires ultra-simples sans validation réelle.
Analyse
- Avantages :
- UX claire (l'utilisateur sait quoi corriger)
- Moins d'erreurs silencieuses
- Base saine pour tests et accessibilité
- Limites / vigilance :
- Peut sembler verbeux sans discipline
- Risque de duplication si mal factorisé
Validation
- Validé le : 25-01-2026
- Contexte technique : Front-end agnostique, API HTTP
Implémentation (exemple minimal)
- Validation côté client (format, champs requis)
- Validation côté serveur (règles métier)
- Mapping explicite des erreurs serveur → champs UI
- Aucun submit silencieux
Checklist
- Messages d'erreur compréhensibles et localisés
- Validation client + serveur cohérente
- Focus automatique sur le champ en erreur
- États loading / disabled gérés
- Tests sur cas valides et invalides
Pattern : Toggle optimiste avec rollback (React Server Action)
Synthèse
- Objectif : masquer la latence serveur sur un toggle boolean en mettant à jour l'UI immédiatement, avec rollback en cas d'erreur.
- Contexte : toggles boolean (visibilité, disponibilité, settings) où la latence doit être masquée.
- Quand l'utiliser : toggles sans besoin de re-fetcher l'entité entière après mutation.
- Quand l'éviter : mutations qui retournent des données complexes → préférer le pattern "Server Action retournant l'entité".
Validation
- Validé le : 21-03-2026
- Contexte technique : React / Next.js App Router — app-template-resto
Implémentation
const [optimistic, setOptimistic] = useState(initialValue);
async function handleToggle() {
const prev = optimistic;
setOptimistic(!prev); // update immédiat
try {
await toggleAction(!prev);
router.refresh(); // synchronise le Server Component parent
} catch {
setOptimistic(prev); // rollback si erreur
}
}
Pattern : Server Action retournant l'entité — élimination de router.refresh() sur create/edit
Synthèse
- Objectif : mettre à jour l'état local directement avec les données réelles retournées par le serveur, sans round-trip SSR supplémentaire.
- Contexte : liste d'items managée côté client (
useState) avec création et modification via Server Actions. - Quand l'utiliser : create et edit d'entités dans une liste. Plus performant que toggle optimiste +
router.refresh(). - Quand l'éviter : simples toggles boolean → le pattern optimiste avec rollback suffit.
Analyse
- Avantages vs toggle optimiste +
router.refresh():- Zéro aller-retour SSR supplémentaire (~500ms–2s économisés sur mobile)
- État local garanti cohérent avec la DB (données réelles, pas calculées localement)
- Pas de flash de rechargement
- Limites / vigilance :
revalidatePathreste nécessaire pour invalider le cache des pages publiques
Validation
- Validé le : 22-03-2026
- Contexte technique : React / Next.js App Router — app-template-resto story 3.8
Implémentation
// Repository — retourne l'entité complète
export async function createItem(tenantId: string, data: Input): Promise<ItemRow> {
return prisma.item.create({ data: { tenantId, ...data }, select: { ...fullSelect } });
}
// Action — retourne la donnée au client
export async function createItemAction(formData: FormData): Promise<ItemRow> {
const actor = await requireOwner();
const item = await createItem(actor.tenantId, input);
revalidatePath("/dashboard/..."); // invalider cache pages publiques
return item; // ← clé : retourner l'entité
}
// Client — mise à jour locale sans round-trip SSR
const created = await createItemAction(formData);
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.
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: 48pxcorrigent 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
<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
- 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-bgen custom property pour override. - Bordure traverse le label : si le label flottant n'a pas de
backgroundopaque, la bordure passe derrière le texte. L'encoche n'est pas optionnelle. - 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. placeholder=" "imposé : le sélecteur:placeholder-shownne marche que si un placeholder existe. Un espace suffit, n'apparaît pas visuellement.inheritAttrs: false+ séparation class/style : sans ça, un parent qui poseclass="--col-2"voit cette classe ET tous les attrs HTML atterrir sur le wrapper.
Variantes
AppSelect: même base, label toujours flottant haut. Chevron SVGstroke="currentColor"1.5px.AppTextarea: label toujours flottant haut, pas de slot trailing.
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
Bodyqui 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-ifdiscriminé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
computeddiscriminant pollue tout le composant - UI diverge à > 30 % (sections présentes dans l'un, absentes dans l'autre)
- cycles de vie ou stores différents (un consomme un store Pinia, l'autre est purement contrôlé) — le
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
Bodypartagé entre les deux composants originaux. Multiplie les fichiers, ajoute une couche d'indirection (props drilling, events bubbling) sans réduire la complexité réelle
- anti-pattern à refuser : extraire un 3ᵉ composant
Validation
- Validé le : 01-05-2026
- Contexte technique : Vue 3 — RL799_V2 (530 lignes dupliquées → 310 lignes uniques)
Implémentation
<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.