mirror of
https://github.com/MaksTinyWorkshop/_Assistant_Lead_Tech
synced 2026-06-28 01:53:40 +02:00
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:
@@ -475,3 +475,398 @@ const useEntityDirectory = (options: { ttlMs?: number } = {}) => {
|
||||
return { directory, lastFetchedAt, loadDirectory, refresh };
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-race-token-partage-latest-wins"></a>
|
||||
## Pattern : Race-token partagé (latest-wins) dans un store paginé
|
||||
|
||||
### Synthèse
|
||||
|
||||
- **Objectif** : garantir qu'un store paginé applique toujours le résultat de la dernière intention utilisateur, même si les réponses réseau reviennent dans le désordre.
|
||||
- **Contexte** : store de domaine (Zustand, Pinia…) où plusieurs actions mutent la même liste (`fetchFeed`, `refresh`, `loadMore`) et peuvent être déclenchées en parallèle (changement de filtre pendant un load, spam de chips).
|
||||
- **Quand l'utiliser** : dès qu'un changement de filtre/onglet peut survenir pendant un chargement déjà en vol.
|
||||
- **Quand l'éviter** : action unique sans concurrence possible, ou actions coûteuses où il vaut mieux annuler les requêtes stale (voir vigilance).
|
||||
|
||||
### Analyse
|
||||
|
||||
- **Avantages** :
|
||||
- latest-wins : la dernière intention gagne quel que soit l'ordre de résolution réseau
|
||||
- un seul compteur partagé protège toutes les actions de la liste, pas seulement `fetchFeed`
|
||||
- propre pour tests + HMR si le compteur vit dans la closure du `create` (par-instance)
|
||||
- **Limites / vigilance** :
|
||||
- un guard `if (get().isLoading) return;` sur `fetchFeed` est un **anti-pattern** : il drop silencieusement un changement de filtre survenu pendant le load initial
|
||||
- race-token appliqué **uniquement** à `fetchFeed` est insuffisant : un `loadMore` in-flight peut résoudre après un `fetchFeed` plus récent et concaténer des items de l'ancien filtre
|
||||
- N taps → N requêtes réseau (acceptable pour chips/toggles ; pour actions coûteuses, préférer `AbortController` côté http-client)
|
||||
- `reset()` doit remettre le compteur à 0 (sinon pollution entre tests / après HMR)
|
||||
|
||||
### Validation
|
||||
|
||||
- Validé le : 20-05-2026
|
||||
- Contexte technique : React Native / Expo / Zustand — app-alexandrie (stories 11.2 / 11.3)
|
||||
|
||||
### Implémentation
|
||||
|
||||
```typescript
|
||||
export const useFeedStore = create((set, get) => {
|
||||
let lastFetchId = 0; // partagé entre toutes les actions, vit dans la closure
|
||||
return {
|
||||
async fetchFeed(token, opts) {
|
||||
const myId = ++lastFetchId;
|
||||
set({ isLoading: true });
|
||||
try {
|
||||
const data = await service.getFeed(token, opts);
|
||||
if (myId !== lastFetchId) return; // stale → drop
|
||||
set({ items: data.items, isLoading: false });
|
||||
} catch (err) {
|
||||
if (myId !== lastFetchId) return;
|
||||
set({ error: String(err), isLoading: false });
|
||||
}
|
||||
},
|
||||
async refresh(token, opts) {
|
||||
if (get().isRefreshing) return;
|
||||
const myId = ++lastFetchId; // même compteur
|
||||
// … idem, applique seulement si myId === lastFetchId
|
||||
},
|
||||
async loadMore(token, opts) {
|
||||
const { hasMore, nextCursor, isLoadingMore } = get();
|
||||
if (!hasMore || !nextCursor || isLoadingMore) return;
|
||||
const myId = ++lastFetchId; // même compteur
|
||||
// … append uniquement si myId === lastFetchId (sinon stale → drop)
|
||||
},
|
||||
reset: () => { lastFetchId = 0; set({ ...initialState }); },
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
### Documenter le design par un test (pas par un commentaire)
|
||||
|
||||
L'absence de guard `isLoading` ressemble à un bug en review. Le test EST la spec : il faut un test qui démontre le latest-wins, sinon la prochaine revue interprétera (à tort) la suppression du guard comme une régression.
|
||||
|
||||
```typescript
|
||||
it('3 fetchFeed concurrents résolus dans le désordre : seul le dernier applique', async () => {
|
||||
// lancer p1, p2, p3 avant qu'aucun ne résolve
|
||||
// résoudre dans l'ordre p2 → p1 → p3
|
||||
// assert : items reflètent uniquement p3
|
||||
});
|
||||
```
|
||||
|
||||
### Checklist
|
||||
|
||||
- [ ] Compteur `lastFetchId` partagé entre `fetchFeed` / `refresh` / `loadMore`
|
||||
- [ ] Chaque action drop son résultat si `myId !== lastFetchId`
|
||||
- [ ] `reset()` remet le compteur à 0
|
||||
- [ ] Test "réponses dans le désordre → dernier gagne"
|
||||
- [ ] Test "loadMore in-flight pendant fetchFeed → loadMore drop"
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-capture-synchrone-before-async-zustand"></a>
|
||||
## Pattern : Capture synchrone "before" dans une action async (anti race latch)
|
||||
|
||||
### Synthèse
|
||||
|
||||
- **Objectif** : pour une transition monotone (latch) calculée à partir de l'état précédent, figer l'état observé **avant** le `await` pour éviter qu'un appel concurrent ne corrompe le calcul.
|
||||
- **Contexte** : action async d'un store qui dérive un nouvel état de l'ancien (`previousIsActive`, latch d'abonnement…) et qui peut être appelée en parallèle.
|
||||
- **Quand l'utiliser** : transitions monotones / latch lus depuis `get()` puis recombinés après `await`.
|
||||
- **Quand l'éviter** : action sans dépendance à l'état précédent, ou action non-monotone (préférer alors le race-token latest-wins).
|
||||
|
||||
### Analyse
|
||||
|
||||
- **Avantages** :
|
||||
- le calcul observe un snapshot cohérent, immunisé contre une mutation concurrente
|
||||
- complémentaire du race-token (qui résout les actions non-monotones)
|
||||
- **Limites / vigilance** :
|
||||
- lire `get()` **après** le `await` expose à un état déjà muté par un second appel
|
||||
|
||||
### Validation
|
||||
|
||||
- Validé le : 27-05-2026
|
||||
- Contexte technique : React Native / Zustand — app-alexandrie (`entitlements.store.ts`, fix H1 IA-v2.5)
|
||||
|
||||
### Implémentation
|
||||
|
||||
```typescript
|
||||
// ❌ MAUVAIS — état lu APRÈS await, peut être muté par un appel concurrent
|
||||
fetchEntitlements: async (token) => {
|
||||
set({ isLoading: true });
|
||||
const data = await service.getMe(token);
|
||||
const wasActive = get().subscription?.isActive ?? false; // lu trop tard
|
||||
set({ subscription: data.subscription, previousIsActive: wasActive ? true : null });
|
||||
}
|
||||
|
||||
// ✅ BON — capture synchrone "before" avant l'await
|
||||
fetchEntitlements: async (token) => {
|
||||
const before = get();
|
||||
const wasActive = before.subscription?.isActive ?? false;
|
||||
const wasLatched = before.previousIsActive === true;
|
||||
set({ isLoading: true });
|
||||
const data = await service.getMe(token);
|
||||
const isNowActive = data.subscription.isActive;
|
||||
const previousIsActive = wasLatched || wasActive || isNowActive ? true : null;
|
||||
set({ subscription: data.subscription, previousIsActive });
|
||||
}
|
||||
```
|
||||
|
||||
**Test associé** : `Promise.all([store.action(), store.action()])` doit produire le même état final que deux appels séquentiels.
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-event-bus-zustand-timestamp"></a>
|
||||
## Pattern : Event bus via timestamp pour signaux UI inter-composants
|
||||
|
||||
### Synthèse
|
||||
|
||||
- **Objectif** : envoyer un signal d'un composant à un autre non-parent (ex. BottomBar → écran de l'onglet actif "rafraîchis-toi") sans prop drilling, sans EventEmitter à nettoyer, sans Context qui re-render tout le sous-arbre.
|
||||
- **Contexte** : store minimal posant un `timestamp` par cible ; un hook consommateur déclenche un callback au changement de timestamp.
|
||||
- **Quand l'utiliser** : signal fire-and-forget ponctuel entre composants découplés.
|
||||
- **Quand l'éviter** : flux de données continu (préférer un state dérivé) ou parent-enfant direct (props/events suffisent).
|
||||
|
||||
### Analyse
|
||||
|
||||
- **Avantages** :
|
||||
- le timestamp seul déclenche le `useEffect` (pas un state data → pas de boucle)
|
||||
- le ref vivant sur le callback dispense l'appelant de `useCallback`
|
||||
- testable comme un store classique (`setState`, assertions sur la map)
|
||||
- **Limites / vigilance** :
|
||||
- ❌ booléen + reset : race entre consommateurs, reset dur à placer
|
||||
- ❌ EventEmitter Node-style : pas de garantie de re-render, cleanup à gérer
|
||||
- ❌ Context React : re-render tout le sous-arbre à chaque tap
|
||||
|
||||
### Validation
|
||||
|
||||
- Validé le : 27-05-2026
|
||||
- Contexte technique : React Native / Zustand — app-alexandrie (IA-v2.8 AC1)
|
||||
|
||||
### Implémentation
|
||||
|
||||
```typescript
|
||||
type RefreshableTab = 'explore' | 'community' | 'messages' | 'library';
|
||||
export const useTabActionStore = create<{
|
||||
refreshTimestamps: Partial<Record<RefreshableTab, number>>;
|
||||
requestRefresh: (tab: RefreshableTab) => void;
|
||||
}>((set) => ({
|
||||
refreshTimestamps: {},
|
||||
requestRefresh: (tab) =>
|
||||
set((s) => ({ refreshTimestamps: { ...s.refreshTimestamps, [tab]: Date.now() } })),
|
||||
}));
|
||||
|
||||
// émetteur — fire & forget
|
||||
useTabActionStore.getState().requestRefresh('explore');
|
||||
|
||||
// consommateur — hook réutilisable
|
||||
export function useTabRefresh(tab: RefreshableTab, onRefresh: () => void) {
|
||||
const timestamp = useTabActionStore((s) => s.refreshTimestamps[tab]);
|
||||
const lastSeenRef = useRef<number | undefined>(undefined);
|
||||
const onRefreshRef = useRef(onRefresh);
|
||||
onRefreshRef.current = onRefresh; // ref vivant, pas de boucle deps
|
||||
useEffect(() => {
|
||||
if (timestamp === undefined || timestamp === lastSeenRef.current) return;
|
||||
lastSeenRef.current = timestamp;
|
||||
onRefreshRef.current();
|
||||
}, [timestamp]);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-cache-zustand-cle-composite"></a>
|
||||
## Pattern : Clé de cache composite sur action async paramétrée
|
||||
|
||||
### Synthèse
|
||||
|
||||
- **Objectif** : éviter de servir un cache stale quand une action async indexe son résultat sur un seul paramètre alors que d'autres paramètres modifient le résultat.
|
||||
- **Contexte** : store qui mémorise un résultat par paramètre métier (`slug`, `id`) et qui court-circuite le refetch via une égalité de clé.
|
||||
- **Quand l'utiliser** : dès qu'une action de fetch prend plus d'un paramètre influençant le résultat.
|
||||
- **Quand l'éviter** : action mono-paramètre, ou cache géré par une lib (React Query) qui clé déjà sur l'ensemble des args.
|
||||
|
||||
### Analyse
|
||||
|
||||
- **Avantages** :
|
||||
- le cache reflète exactement les paramètres qui produisent le résultat
|
||||
- pas de dépendance fragile à un `clearXxx()` externe
|
||||
- **Limites / vigilance** :
|
||||
- clé partielle (`packSlug` seul) → navigation A→B du même pack sert B avec l'exclusion de A toujours active
|
||||
|
||||
### Validation
|
||||
|
||||
- Validé le : 29-05-2026
|
||||
- Contexte technique : Zustand — app-alexandrie (code review ux-cleanup-7)
|
||||
|
||||
### Implémentation
|
||||
|
||||
```typescript
|
||||
// ❌ Cache stale si excludeId change mais pas packSlug
|
||||
async fetchPackContents(token, packSlug, excludeId) {
|
||||
if (get().packContentsSlug === packSlug) return;
|
||||
// …
|
||||
}
|
||||
|
||||
// ✅ Clé composite : un champ d'état par paramètre métier
|
||||
async fetchPackContents(token, packSlug, excludeId) {
|
||||
const normalizedExcludeId = excludeId ?? null;
|
||||
const sameKey =
|
||||
get().packContentsSlug === packSlug &&
|
||||
get().packContentsExcludeId === normalizedExcludeId;
|
||||
if (sameKey && (get().isLoading || get().items.length > 0)) return;
|
||||
// …
|
||||
set({ packContentsSlug: packSlug, packContentsExcludeId: normalizedExcludeId });
|
||||
}
|
||||
```
|
||||
|
||||
**Règle** : N paramètres métier influençant le résultat → N champs de clé dans l'état.
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-loadings-separes-initial-pagination"></a>
|
||||
## Pattern : Loadings séparés (fetch initial vs pagination)
|
||||
|
||||
### Synthèse
|
||||
|
||||
- **Objectif** : éviter que le pull-to-refresh tourne pendant l'infinite scroll en distinguant deux flags de chargement de natures différentes.
|
||||
- **Contexte** : store qui supporte à la fois fetch initial/refresh ET pagination (`fetchNextPage`).
|
||||
- **Quand l'utiliser** : toute liste avec `RefreshControl` + chargement de pages.
|
||||
- **Quand l'éviter** : liste sans pagination, ou sans pull-to-refresh.
|
||||
|
||||
### Analyse
|
||||
|
||||
- **Avantages** : le `RefreshControl` ne s'anime que pour le refresh ; le spinner de bas de liste pour la pagination.
|
||||
- **Limites / vigilance** : un seul `isLoading` partagé → l'écran clignote, le refresh tourne pendant le scroll. Côté anti-pattern, voir aussi `risques/state.md#risque-flag-isloading-unique-nature-differente`.
|
||||
|
||||
### Validation
|
||||
|
||||
- Validé le : 29-05-2026
|
||||
- Contexte technique : React Native / Zustand — app-alexandrie (ux-cleanup-10 M4, `notifications.store.ts`)
|
||||
|
||||
### Implémentation
|
||||
|
||||
```typescript
|
||||
type State = {
|
||||
items: T[];
|
||||
isLoading: boolean; // fetch initial OU pull-to-refresh
|
||||
isLoadingMore: boolean; // pagination (fetchNextPage)
|
||||
};
|
||||
```
|
||||
|
||||
```tsx
|
||||
<FlatList
|
||||
refreshing={isLoading} // pull-to-refresh seulement
|
||||
ListFooterComponent={isLoadingMore ? <Spinner/> : null}
|
||||
onEndReached={() => store.fetchNextPage(...)}
|
||||
/>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-flags-etat-separes-par-preoccupation"></a>
|
||||
## Pattern : Flags d'état séparés par préoccupation (liste / création / mutation-par-item)
|
||||
|
||||
### Synthèse
|
||||
|
||||
- **Objectif** : éviter qu'un `isSubmitting`/`error` unique partagé entre `create`, `update(id)` et `remove(id)` ne fasse passer tous les boutons d'une liste en `loading` quand on agit sur une seule ligne.
|
||||
- **Contexte** : store qui gère liste + création + mutation-par-item (agnostique Pinia/Vuex/Redux/Zustand).
|
||||
- **Quand l'utiliser** : dès qu'une liste a des actions par item ET un formulaire de création.
|
||||
- **Quand l'éviter** : store mono-action sans liste interactive.
|
||||
|
||||
### Analyse
|
||||
|
||||
- **Avantages** : chaque préoccupation a son flag → boutons ciblés, actions enchaînables, erreurs au bon endroit.
|
||||
- **Limites / vigilance** : un `isSubmitting` unique provoque (1) tous les boutons de toutes les lignes en `loading`, (2) actions non-enchaînables, (3) erreur de mutation affichée dans un formulaire de création sans rapport. Pour le variant booléen-vs-`Set`, voir `risques/state.md#risque-flag-global-actions-paralleles`.
|
||||
|
||||
### Validation
|
||||
|
||||
- Validé le : 13-06-2026
|
||||
- Contexte technique : Vue 3 / Pinia — RL799_V2 (`instructionsStore.ts`, code review)
|
||||
|
||||
### Implémentation
|
||||
|
||||
```typescript
|
||||
// ❌ partagé : :loading="store.isSubmitting" sur chaque ligne
|
||||
// ✅ séparé par préoccupation
|
||||
isLoading / loadError // chargement de la liste
|
||||
isCreating / createError // formulaire de création
|
||||
submittingId: string | null // mutation par item
|
||||
mutationError // erreur de mutation
|
||||
// UI ligne : :loading="store.submittingId === item.id"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-derive-computed-pas-ref-resync"></a>
|
||||
## Pattern : État dérivé = `computed`, jamais un `ref` resynchronisé à la main
|
||||
|
||||
### Synthèse
|
||||
|
||||
- **Objectif** : supprimer les désyncs silencieuses d'un compteur/dérivé recalculé manuellement à chaque chemin de mutation.
|
||||
- **Contexte** : composable Vue 3 exposant une valeur toujours dérivée d'un autre état réactif (`size`, `length`, total, booléen `isEmpty`).
|
||||
- **Quand l'utiliser** : toute valeur jamais assignée indépendamment, toujours recalculée depuis une source.
|
||||
- **Quand l'éviter** : valeur réellement indépendante (saisie utilisateur, état piloté en propre).
|
||||
|
||||
### Analyse
|
||||
|
||||
- **Avantages** : recalcul automatique à chaque changement de la source, zéro synchro manuelle, impossible d'oublier un chemin.
|
||||
- **Limites / vigilance** :
|
||||
- anti-pattern : `const count = ref(set.value.size)` + `syncCount()` rappelé dans chaque mutation → un oubli produit un compteur faux **sans erreur**
|
||||
- exposer le dérivé en `ComputedRef<T>` (pas `Ref<T>`) pour signaler le read-only
|
||||
- réactivité `Set`/`Map` : la mutation in-place (`set.add()`) ne déclenche rien — réassigner (`checked.value = new Set(checked.value)`) ; un `computed(() => checked.value.size)` se met alors à jour (il dépend de l'identité du Set)
|
||||
- **Garde-fou de revue** : dans un composable, tout `ref` toujours recalculé depuis un autre `ref` est un `computed` déguisé.
|
||||
|
||||
### Validation
|
||||
|
||||
- Validé le : 18-06-2026
|
||||
- Contexte technique : Vue 3 / Composition API — RL799_V2 (`useMcChecklist`, code review v2-2-2)
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-une-source-deux-vues-lecture-inerte"></a>
|
||||
## Pattern : Une source, deux vues — la vue lecture inerte par construction
|
||||
|
||||
### Synthèse
|
||||
|
||||
- **Objectif** : présenter la même donnée en mode édition ET en mode lecture/présentation sans dupliquer la donnée ni la logique, en prouvant l'inertie du mode lecture **par construction** (aucun mutateur câblé) plutôt que par un garde runtime.
|
||||
- **Contexte** : écran consultation/édition partageant un même fetch (préparation vs pédagogique, édition vs consultation).
|
||||
- **Quand l'utiliser** : deux rendus d'une même donnée pilotés par un `viewMode` local.
|
||||
- **Quand l'éviter** : modes aux données réellement disjointes.
|
||||
|
||||
### Analyse
|
||||
|
||||
- **Avantages** :
|
||||
- un seul fetch, deux rendus via `v-if/v-else` sur un `ref<'edit'|'read'>`
|
||||
- le mode lecture ne **référence** aucun mutateur → inertie garantie, pas besoin de `if (mode === 'read') return` dans chaque handler
|
||||
- un composable d'état qui **lit** au mount reste inerte tant que ses mutateurs ne sont pas appelés (la lecture n'est pas un effet de bord) → il peut être instancié pour les deux modes
|
||||
- **Limites / vigilance** :
|
||||
- `viewMode` est un état d'affichage **local non persisté** (pas de la donnée métier)
|
||||
- préférer un toggle in-place (un `viewMode` + `v-if` dans la page) à l'extraction d'un composant partagé tant qu'une autre story touche le même rendu — surface de merge minimale, factoriser à la 3ᵉ vraie divergence
|
||||
- **Prouver l'inertie par test** : monter en mode lecture, spy sur `localStorage.setItem`/`fetch`, cliquer les éléments lecture, asserter aucun appel + clé localStorage restée nulle.
|
||||
|
||||
### Validation
|
||||
|
||||
- Validé le : 18-06-2026
|
||||
- Contexte technique : Vue 3 — RL799_V2 (vue pédagogique MC, segmented control préparation↔pédagogique, code review v2-2-3)
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-noyau-visuel-partage-variants"></a>
|
||||
## Pattern : Noyau visuel générique + comportements variants greffés
|
||||
|
||||
### Synthèse
|
||||
|
||||
- **Objectif** : faire servir un même artefact visuel positionnel (plan, diagramme, grille de placement) à deux finalités différentes sans dupliquer le rendu ni coupler les intentions via un `variant`.
|
||||
- **Contexte** : un visuel (SVG, carte, plateau) doit afficher un état ET servir de sélecteur de navigation.
|
||||
- **Quand l'utiliser** : à la **2ᵉ** utilisation de la même géométrie (pas avant — règle « factoriser à la Nᵉ utilisation »).
|
||||
- **Quand l'éviter** : usage unique, ou intentions trop divergentes pour partager un contrat de props minimal.
|
||||
|
||||
### Analyse
|
||||
|
||||
- **Avantages** :
|
||||
- le noyau ne connaît QUE l'invariant : géométrie, accessibilité clavier, contrat `nodes: {id, state}`, un seul événement neutre `select(id)`
|
||||
- les consommateurs sont des **adaptateurs minces** : ils mappent leurs données métier vers `nodes` et retraduisent `select(id)`
|
||||
- le 1er consommateur garde une signature publique inchangée (zéro régression, prouvée par typecheck + test de mount), le 2ᵉ usage est purement additif
|
||||
- **Limites / vigilance** :
|
||||
- mettre l'état « inerte » (plateau non cliquable) dans le **noyau** (garde de non-émission au clic ET au clavier), pas seulement en CSS ni dans chaque adaptateur
|
||||
- dériver `nodes` d'une source unique exhaustive (ex. `RITUAL_OFFICER_ROLES`) plutôt qu'une liste en dur → un ajout futur apparaît automatiquement, pas de nœud manquant silencieux
|
||||
|
||||
### Validation
|
||||
|
||||
- Validé le : 13-06-2026
|
||||
- Contexte technique : Vue 3 / SVG — RL799_V2 (`LodgeFloorPlanBase` consommé par `LodgeFloorPlan` collège + `ModuleFloorPlan` navigation, chantier surveillant-mobile-floorplan)
|
||||
|
||||
Reference in New Issue
Block a user