Migration Angular 20 -> 21 : fin de la migration securite (0 vulnerabilite npm)

- ng update @angular/core@21 @angular/cli@21 (corrige GHSA-jrmj-c5cx-3cw6, XSS SVG)
- Migration automatique des templates vers le control flow natif (@if/@for, 138 fichiers)
- Correction des 5 `track` invalides generes par la migration (trackBy a 1 argument
  -> track $index) + suppression des fonctions trackBy mortes
- TypeScript 5.9, zone.js 0.15 ; npm audit : 0 vulnerabilite

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-10 14:55:36 +02:00
parent 6acad41672
commit 23878f1c63
142 changed files with 7055 additions and 6292 deletions

View File

@@ -10,16 +10,17 @@
<lucide-icon [img]="Drama" [size]="22"></lucide-icon>
{{ npcId ? 'Éditer le PNJ' : 'Nouveau PNJ' }}
</h1>
<button
*ngIf="npcId"
type="button"
class="btn-ai"
(click)="toggleChat()"
[class.active]="chatOpen"
title="Ouvrir l'Assistant IA pour dialoguer autour de ce PNJ">
<lucide-icon [img]="Sparkles" [size]="14"></lucide-icon>
Assistant IA
</button>
@if (npcId) {
<button
type="button"
class="btn-ai"
(click)="toggleChat()"
[class.active]="chatOpen"
title="Ouvrir l'Assistant IA pour dialoguer autour de ce PNJ">
<lucide-icon [img]="Sparkles" [size]="14"></lucide-icon>
Assistant IA
</button>
}
</div>
</div>
@@ -33,85 +34,89 @@
[(ngModel)]="name"
name="name"
placeholder="Ex: Borin le forgeron, Dame Elara, Kael l'aubergiste..."
/>
</div>
<div class="field">
<label for="npc-folder">Dossier</label>
<input
id="npc-folder"
type="text"
[(ngModel)]="folder"
name="folder"
list="npc-folders"
placeholder="Ex: Bard's Gate, Faction des Pipers… (laisser vide = non classé)"
/>
<datalist id="npc-folders">
<option *ngFor="let f of existingFolders" [value]="f"></option>
</datalist>
</div>
<div class="field-row image-row">
<div class="field portrait-field">
<label>Portrait</label>
<app-single-image-picker
[imageId]="portraitImageId"
aspectRatio="1 / 1"
hint="Carre conseille (400×400)."
(imageIdChange)="portraitImageId = $event">
</app-single-image-picker>
/>
</div>
<div class="field header-field">
<label>Bandeau / Header</label>
<app-single-image-picker
[imageId]="headerImageId"
aspectRatio="3 / 1"
hint="Format paysage conseille (1200×400)."
(imageIdChange)="headerImageId = $event">
</app-single-image-picker>
<div class="field">
<label for="npc-folder">Dossier</label>
<input
id="npc-folder"
type="text"
[(ngModel)]="folder"
name="folder"
list="npc-folders"
placeholder="Ex: Bard's Gate, Faction des Pipers… (laisser vide = non classé)"
/>
<datalist id="npc-folders">
@for (f of existingFolders; track f) {
<option [value]="f"></option>
}
</datalist>
</div>
<div class="field-row image-row">
<div class="field portrait-field">
<label>Portrait</label>
<app-single-image-picker
[imageId]="portraitImageId"
aspectRatio="1 / 1"
hint="Carre conseille (400×400)."
(imageIdChange)="portraitImageId = $event">
</app-single-image-picker>
</div>
<div class="field header-field">
<label>Bandeau / Header</label>
<app-single-image-picker
[imageId]="headerImageId"
aspectRatio="3 / 1"
hint="Format paysage conseille (1200×400)."
(imageIdChange)="headerImageId = $event">
</app-single-image-picker>
</div>
</div>
<div class="template-fields">
<app-dynamic-fields-form
[fields]="templateFields"
[values]="values"
[imageValues]="imageValues"
[keyValueValues]="keyValueValues"
(valuesChange)="values = $event"
(imageValuesChange)="imageValues = $event"
(keyValueValuesChange)="keyValueValues = $event">
</app-dynamic-fields-form>
</div>
<div class="actions">
<button type="button" class="btn-primary" [disabled]="!name.trim()" (click)="submit()">
<lucide-icon [img]="Save" [size]="16"></lucide-icon>
{{ npcId ? 'Enregistrer' : 'Créer' }}
</button>
<button type="button" class="btn-secondary" (click)="back()">Annuler</button>
<span class="spacer"></span>
@if (npcId) {
<button
type="button"
class="btn-danger"
(click)="deleteNpc()">
<lucide-icon [img]="Trash2" [size]="14"></lucide-icon>
Supprimer
</button>
}
</div>
</div>
</div>
<div class="template-fields">
<app-dynamic-fields-form
[fields]="templateFields"
[values]="values"
[imageValues]="imageValues"
[keyValueValues]="keyValueValues"
(valuesChange)="values = $event"
(imageValuesChange)="imageValues = $event"
(keyValueValuesChange)="keyValueValues = $event">
</app-dynamic-fields-form>
</div>
<div class="actions">
<button type="button" class="btn-primary" [disabled]="!name.trim()" (click)="submit()">
<lucide-icon [img]="Save" [size]="16"></lucide-icon>
{{ npcId ? 'Enregistrer' : 'Créer' }}
</button>
<button type="button" class="btn-secondary" (click)="back()">Annuler</button>
<span class="spacer"></span>
<button
*ngIf="npcId"
type="button"
class="btn-danger"
(click)="deleteNpc()">
<lucide-icon [img]="Trash2" [size]="14"></lucide-icon>
Supprimer
</button>
</div>
</div>
</div>
<app-ai-chat-drawer
*ngIf="npcId && campaignId"
[campaignId]="campaignId"
entityType="npc"
[entityId]="npcId"
[isOpen]="chatOpen"
welcomeMessage="Je vois cette fiche de PNJ. Demande-moi de proposer apparence, motivations, secrets, ou répliques signatures."
[quickSuggestions]="chatQuickSuggestions"
(close)="chatOpen = false">
</app-ai-chat-drawer>
@if (npcId && campaignId) {
<app-ai-chat-drawer
[campaignId]="campaignId"
entityType="npc"
[entityId]="npcId"
[isOpen]="chatOpen"
welcomeMessage="Je vois cette fiche de PNJ. Demande-moi de proposer apparence, motivations, secrets, ou répliques signatures."
[quickSuggestions]="chatQuickSuggestions"
(close)="chatOpen = false">
</app-ai-chat-drawer>
}