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

@@ -1,85 +1,92 @@
<div class="folder-view" *ngIf="node">
<!-- Fil d'Ariane : Lore → ancêtres → dossier courant -->
<nav class="breadcrumb" aria-label="Fil d'Ariane">
<button type="button" class="crumb" (click)="navigateToLoreRoot()" *ngIf="lore">
{{ lore.name }}
</button>
<ng-container *ngFor="let ancestor of ancestors">
@if (node) {
<div class="folder-view">
<!-- Fil d'Ariane : Lore → ancêtres → dossier courant -->
<nav class="breadcrumb" aria-label="Fil d'Ariane">
@if (lore) {
<button type="button" class="crumb" (click)="navigateToLoreRoot()">
{{ lore.name }}
</button>
}
@for (ancestor of ancestors; track ancestor) {
<lucide-icon [img]="ChevronRight" [size]="12" class="crumb-sep"></lucide-icon>
<button type="button" class="crumb" (click)="navigateToSubfolder(ancestor.id!)">
{{ ancestor.name }}
</button>
}
<lucide-icon [img]="ChevronRight" [size]="12" class="crumb-sep"></lucide-icon>
<button type="button" class="crumb" (click)="navigateToSubfolder(ancestor.id!)">
{{ ancestor.name }}
</button>
</ng-container>
<lucide-icon [img]="ChevronRight" [size]="12" class="crumb-sep"></lucide-icon>
<span class="crumb current">{{ node.name }}</span>
</nav>
<!-- Header : icône + nom + actions -->
<div class="detail-header">
<div class="header-texts">
<h1>
<lucide-icon [img]="folderIcon" [size]="24" class="title-icon"></lucide-icon>
{{ node.name }}
</h1>
<p class="description">
{{ subfolders.length }} sous-dossier(s) · {{ pages.length }} page(s)
</p>
</div>
<div class="header-actions">
<button type="button" class="btn-secondary" (click)="navigateToEdit()" title="Modifier le dossier">
<lucide-icon [img]="Pencil" [size]="14"></lucide-icon>
Modifier
</button>
<button type="button" class="btn-danger" (click)="delete()" title="Supprimer le dossier et tout son contenu">
<lucide-icon [img]="Trash2" [size]="14"></lucide-icon>
Supprimer
</button>
<span class="crumb current">{{ node.name }}</span>
</nav>
<!-- Header : icône + nom + actions -->
<div class="detail-header">
<div class="header-texts">
<h1>
<lucide-icon [img]="folderIcon" [size]="24" class="title-icon"></lucide-icon>
{{ node.name }}
</h1>
<p class="description">
{{ subfolders.length }} sous-dossier(s) · {{ pages.length }} page(s)
</p>
</div>
<div class="header-actions">
<button type="button" class="btn-secondary" (click)="navigateToEdit()" title="Modifier le dossier">
<lucide-icon [img]="Pencil" [size]="14"></lucide-icon>
Modifier
</button>
<button type="button" class="btn-danger" (click)="delete()" title="Supprimer le dossier et tout son contenu">
<lucide-icon [img]="Trash2" [size]="14"></lucide-icon>
Supprimer
</button>
</div>
</div>
<!-- Sous-dossiers -->
<section class="detail-section">
<div class="section-header">
<h2>Sous-dossiers</h2>
<button class="btn-add" (click)="navigateToCreateSubfolder()">
<lucide-icon [img]="Plus" [size]="14"></lucide-icon>
Nouveau sous-dossier
</button>
</div>
@if (subfolders.length > 0) {
<div class="items-grid">
@for (sub of subfolders; track sub) {
<div class="node-card" (click)="navigateToSubfolder(sub.id!)">
<lucide-icon [img]="Folder" [size]="24" class="node-icon"></lucide-icon>
<span class="node-name">{{ sub.name }}</span>
</div>
}
</div>
}
@if (subfolders.length === 0) {
<div class="empty-state">
<p>Aucun sous-dossier.</p>
</div>
}
</section>
<!-- Pages -->
<section class="detail-section">
<div class="section-header">
<h2>Pages</h2>
<button class="btn-add" (click)="navigateToCreatePage()">
<lucide-icon [img]="Plus" [size]="14"></lucide-icon>
Nouvelle page
</button>
</div>
@if (pages.length > 0) {
<div class="items-grid">
@for (page of pages; track page) {
<div class="node-card" (click)="navigateToPage(page.id!)">
<lucide-icon [img]="FileText" [size]="24" class="node-icon"></lucide-icon>
<span class="node-name">{{ page.title }}</span>
</div>
}
</div>
}
@if (pages.length === 0) {
<div class="empty-state">
<p>Aucune page dans ce dossier.</p>
</div>
}
</section>
</div>
<!-- Sous-dossiers -->
<section class="detail-section">
<div class="section-header">
<h2>Sous-dossiers</h2>
<button class="btn-add" (click)="navigateToCreateSubfolder()">
<lucide-icon [img]="Plus" [size]="14"></lucide-icon>
Nouveau sous-dossier
</button>
</div>
<div class="items-grid" *ngIf="subfolders.length > 0">
<div class="node-card" *ngFor="let sub of subfolders" (click)="navigateToSubfolder(sub.id!)">
<lucide-icon [img]="Folder" [size]="24" class="node-icon"></lucide-icon>
<span class="node-name">{{ sub.name }}</span>
</div>
</div>
<div class="empty-state" *ngIf="subfolders.length === 0">
<p>Aucun sous-dossier.</p>
</div>
</section>
<!-- Pages -->
<section class="detail-section">
<div class="section-header">
<h2>Pages</h2>
<button class="btn-add" (click)="navigateToCreatePage()">
<lucide-icon [img]="Plus" [size]="14"></lucide-icon>
Nouvelle page
</button>
</div>
<div class="items-grid" *ngIf="pages.length > 0">
<div class="node-card" *ngFor="let page of pages" (click)="navigateToPage(page.id!)">
<lucide-icon [img]="FileText" [size]="24" class="node-icon"></lucide-icon>
<span class="node-name">{{ page.title }}</span>
</div>
</div>
<div class="empty-state" *ngIf="pages.length === 0">
<p>Aucune page dans ce dossier.</p>
</div>
</section>
</div>
}

View File

@@ -1,5 +1,5 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ActivatedRoute, Router } from '@angular/router';
import { forkJoin } from 'rxjs';
import { LucideAngularModule, LucideIconData, Folder, FileText, Pencil, Trash2, Plus, ChevronRight } from 'lucide-angular';
@@ -24,7 +24,7 @@ import { ConfirmDialogService } from '../../shared/confirm-dialog/confirm-dialog
*/
@Component({
selector: 'app-folder-view',
imports: [CommonModule, LucideAngularModule],
imports: [LucideAngularModule],
templateUrl: './folder-view.component.html',
styleUrls: ['./folder-view.component.scss']
})

View File

@@ -1,11 +1,11 @@
import { Component, EventEmitter, Output } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { LucideAngularModule, BookCopy, X } from 'lucide-angular';
@Component({
selector: 'app-lore-create',
imports: [CommonModule, ReactiveFormsModule, LucideAngularModule],
imports: [ReactiveFormsModule, LucideAngularModule],
templateUrl: './lore-create.component.html',
styleUrls: ['./lore-create.component.scss']
})

View File

@@ -1,70 +1,78 @@
<div class="lore-detail" *ngIf="lore">
<!-- ============ Header : mode lecture ============ -->
<div class="detail-header" *ngIf="!editing">
<div class="header-texts">
<h1>{{ lore.name }}</h1>
<p class="description">{{ lore.description }}</p>
</div>
<div class="header-actions">
<button type="button" class="btn-secondary" (click)="startEdit()" title="Modifier le Lore">
<lucide-icon [img]="Pencil" [size]="14"></lucide-icon>
Modifier
</button>
<button type="button" class="btn-danger" (click)="deleteLore()" title="Supprimer le Lore">
<lucide-icon [img]="Trash2" [size]="14"></lucide-icon>
Supprimer
</button>
</div>
</div>
<!-- ============ Header : mode édition inline ============ -->
<div class="detail-header edit-mode" *ngIf="editing">
<div class="field">
<label for="lore-detail-edit-name">Nom</label>
<input id="lore-detail-edit-name" type="text" [(ngModel)]="editName" name="editName" required />
</div>
<div class="field">
<label for="lore-detail-edit-description">Description</label>
<textarea id="lore-detail-edit-description" [(ngModel)]="editDescription" name="editDescription" rows="3"></textarea>
</div>
<div class="header-actions">
<button type="button" class="btn-primary" (click)="saveEdit()" [disabled]="!editName.trim()">
Sauvegarder
</button>
<button type="button" class="btn-secondary" (click)="cancelEdit()">
Annuler
</button>
</div>
</div>
<!-- ============ Grille des dossiers racine ============ -->
<section class="detail-section nodes-section" *ngIf="!editing">
<div class="section-header">
<h2>Dossiers</h2>
<button class="btn-add" (click)="navigateToCreateNode()">
<lucide-icon [img]="Plus" [size]="14"></lucide-icon>
Nouveau dossier
</button>
</div>
<!-- rootNodes : uniquement les dossiers racine (pas les sous-dossiers,
qui sont visibles dans l'arbre de la sidebar). -->
<div class="nodes-grid" *ngIf="rootNodes.length > 0">
<div class="node-card" *ngFor="let node of rootNodes" (click)="navigateToFolder(node.id!)">
<lucide-icon [img]="Folder" [size]="24" class="node-icon"></lucide-icon>
<span class="node-name">{{ node.name }}</span>
@if (lore) {
<div class="lore-detail">
<!-- ============ Header : mode lecture ============ -->
@if (!editing) {
<div class="detail-header">
<div class="header-texts">
<h1>{{ lore.name }}</h1>
<p class="description">{{ lore.description }}</p>
</div>
<div class="header-actions">
<button type="button" class="btn-secondary" (click)="startEdit()" title="Modifier le Lore">
<lucide-icon [img]="Pencil" [size]="14"></lucide-icon>
Modifier
</button>
<button type="button" class="btn-danger" (click)="deleteLore()" title="Supprimer le Lore">
<lucide-icon [img]="Trash2" [size]="14"></lucide-icon>
Supprimer
</button>
</div>
</div>
</div>
<div class="empty-state" *ngIf="rootNodes.length === 0">
<lucide-icon [img]="Folder" [size]="40" class="empty-icon"></lucide-icon>
<p>Aucun dossier pour le moment.</p>
<button class="btn-add-first" (click)="navigateToCreateNode()">
<lucide-icon [img]="Plus" [size]="14"></lucide-icon>
Créer votre premier dossier
</button>
</div>
</section>
</div>
}
<!-- ============ Header : mode édition inline ============ -->
@if (editing) {
<div class="detail-header edit-mode">
<div class="field">
<label for="lore-detail-edit-name">Nom</label>
<input id="lore-detail-edit-name" type="text" [(ngModel)]="editName" name="editName" required />
</div>
<div class="field">
<label for="lore-detail-edit-description">Description</label>
<textarea id="lore-detail-edit-description" [(ngModel)]="editDescription" name="editDescription" rows="3"></textarea>
</div>
<div class="header-actions">
<button type="button" class="btn-primary" (click)="saveEdit()" [disabled]="!editName.trim()">
Sauvegarder
</button>
<button type="button" class="btn-secondary" (click)="cancelEdit()">
Annuler
</button>
</div>
</div>
}
<!-- ============ Grille des dossiers racine ============ -->
@if (!editing) {
<section class="detail-section nodes-section">
<div class="section-header">
<h2>Dossiers</h2>
<button class="btn-add" (click)="navigateToCreateNode()">
<lucide-icon [img]="Plus" [size]="14"></lucide-icon>
Nouveau dossier
</button>
</div>
<!-- rootNodes : uniquement les dossiers racine (pas les sous-dossiers,
qui sont visibles dans l'arbre de la sidebar). -->
@if (rootNodes.length > 0) {
<div class="nodes-grid">
@for (node of rootNodes; track node) {
<div class="node-card" (click)="navigateToFolder(node.id!)">
<lucide-icon [img]="Folder" [size]="24" class="node-icon"></lucide-icon>
<span class="node-name">{{ node.name }}</span>
</div>
}
</div>
}
@if (rootNodes.length === 0) {
<div class="empty-state">
<lucide-icon [img]="Folder" [size]="40" class="empty-icon"></lucide-icon>
<p>Aucun dossier pour le moment.</p>
<button class="btn-add-first" (click)="navigateToCreateNode()">
<lucide-icon [img]="Plus" [size]="14"></lucide-icon>
Créer votre premier dossier
</button>
</div>
}
</section>
}
</div>
}

View File

@@ -1,5 +1,5 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { LucideAngularModule, Folder, Plus, Pencil, Trash2 } from 'lucide-angular';
@@ -14,7 +14,7 @@ import { ConfirmDialogService } from '../../shared/confirm-dialog/confirm-dialog
@Component({
selector: 'app-lore-detail',
imports: [CommonModule, FormsModule, LucideAngularModule],
imports: [FormsModule, LucideAngularModule],
templateUrl: './lore-detail.component.html',
styleUrls: ['./lore-detail.component.scss']
})

View File

@@ -14,14 +14,16 @@
formControlName="name"
placeholder="Ex: Personnages, Créatures..."
[class.invalid]="form.get('name')?.invalid && form.get('name')?.touched"
/>
/>
</div>
<div class="field">
<label>Dossier parent <span class="optional">(optionnel)</span></label>
<select formControlName="parentId">
<option value="">— Racine du Lore —</option>
<option *ngFor="let parent of availableParents" [value]="parent.id">{{ parent.name }}</option>
@for (parent of availableParents; track parent) {
<option [value]="parent.id">{{ parent.name }}</option>
}
</select>
<p class="hint">Laissez vide pour créer un dossier à la racine du lore</p>
</div>
@@ -29,14 +31,15 @@
<div class="field">
<label>Icône</label>
<div class="icon-grid">
<button
type="button"
class="icon-btn"
*ngFor="let option of iconOptions"
[class.selected]="selectedIcon === option.key"
(click)="selectIcon(option.key)">
<lucide-icon [img]="option.icon" [size]="18"></lucide-icon>
</button>
@for (option of iconOptions; track option) {
<button
type="button"
class="icon-btn"
[class.selected]="selectedIcon === option.key"
(click)="selectIcon(option.key)">
<lucide-icon [img]="option.icon" [size]="18"></lucide-icon>
</button>
}
</div>
</div>

View File

@@ -1,5 +1,5 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { LucideAngularModule, LucideIconData } from 'lucide-angular';
@@ -14,7 +14,7 @@ import { LORE_ICON_OPTIONS, IconOption, resolveIcon } from '../lore-icons';
@Component({
selector: 'app-lore-node-create',
imports: [CommonModule, ReactiveFormsModule, LucideAngularModule],
imports: [ReactiveFormsModule, LucideAngularModule],
templateUrl: './lore-node-create.component.html',
styleUrls: ['./lore-node-create.component.scss']
})

View File

@@ -1,54 +1,52 @@
<div class="page" *ngIf="node">
<header class="page-header">
<div>
<h1>Éditer le dossier</h1>
<p class="subtitle">
{{ childFolderCount }} sous-dossier(s) · {{ pageCount }} page(s)
</p>
</div>
<div class="header-actions">
<button type="button" class="btn-secondary" (click)="cancel()">Annuler</button>
<button
type="submit"
class="btn-primary"
[disabled]="form.invalid"
(click)="save()">
Sauvegarder
</button>
</div>
</header>
<form [formGroup]="form" class="edit-form">
<div class="field">
<label>Nom du dossier *</label>
<input type="text" formControlName="name" />
</div>
<div class="field">
<label>Dossier parent <span class="optional">(optionnel)</span></label>
<select formControlName="parentId">
<option value="">— Racine du Lore —</option>
<option *ngFor="let parent of availableParents" [value]="parent.id">{{ parent.name }}</option>
</select>
<p class="hint">Vous ne pouvez pas choisir un sous-dossier du dossier courant (cycle interdit)</p>
</div>
<div class="field">
<label>Icône</label>
<div class="icon-grid">
@if (node) {
<div class="page">
<header class="page-header">
<div>
<h1>Éditer le dossier</h1>
<p class="subtitle">
{{ childFolderCount }} sous-dossier(s) · {{ pageCount }} page(s)
</p>
</div>
<div class="header-actions">
<button type="button" class="btn-secondary" (click)="cancel()">Annuler</button>
<button
type="button"
class="icon-btn"
*ngFor="let option of iconOptions"
[class.selected]="selectedIcon === option.key"
(click)="selectIcon(option.key)">
<lucide-icon [img]="option.icon" [size]="18"></lucide-icon>
type="submit"
class="btn-primary"
[disabled]="form.invalid"
(click)="save()">
Sauvegarder
</button>
</div>
</div>
</form>
</div>
</header>
<form [formGroup]="form" class="edit-form">
<div class="field">
<label>Nom du dossier *</label>
<input type="text" formControlName="name" />
</div>
<div class="field">
<label>Dossier parent <span class="optional">(optionnel)</span></label>
<select formControlName="parentId">
<option value="">— Racine du Lore —</option>
@for (parent of availableParents; track parent) {
<option [value]="parent.id">{{ parent.name }}</option>
}
</select>
<p class="hint">Vous ne pouvez pas choisir un sous-dossier du dossier courant (cycle interdit)</p>
</div>
<div class="field">
<label>Icône</label>
<div class="icon-grid">
@for (option of iconOptions; track option) {
<button
type="button"
class="icon-btn"
[class.selected]="selectedIcon === option.key"
(click)="selectIcon(option.key)">
<lucide-icon [img]="option.icon" [size]="18"></lucide-icon>
</button>
}
</div>
</div>
</form>
</div>
}

View File

@@ -1,5 +1,5 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { forkJoin } from 'rxjs';
@@ -32,7 +32,7 @@ import { LORE_ICON_OPTIONS, IconOption } from '../lore-icons';
*/
@Component({
selector: 'app-lore-node-edit',
imports: [CommonModule, ReactiveFormsModule, LucideAngularModule],
imports: [ReactiveFormsModule, LucideAngularModule],
templateUrl: './lore-node-edit.component.html',
styleUrls: ['./lore-node-edit.component.scss']
})

View File

@@ -8,18 +8,20 @@
<div class="lore-grid">
<div class="lore-card" *ngFor="let lore of lores" (click)="navigateToDetail(lore.id!)">
<div class="card-header">
<lucide-icon [img]="Folder" [size]="20" class="card-icon"></lucide-icon>
<span class="card-date">Il y a 2h</span>
@for (lore of lores; track lore) {
<div class="lore-card" (click)="navigateToDetail(lore.id!)">
<div class="card-header">
<lucide-icon [img]="Folder" [size]="20" class="card-icon"></lucide-icon>
<span class="card-date">Il y a 2h</span>
</div>
<h2>{{ lore.name }}</h2>
<p class="card-description">{{ lore.description }}</p>
<div class="card-stats">
<span>📄 {{ lore.pageCount || 0 }} pages</span>
<span>🌳 {{ lore.nodeCount || 0 }} dossiers</span>
</div>
</div>
<h2>{{ lore.name }}</h2>
<p class="card-description">{{ lore.description }}</p>
<div class="card-stats">
<span>📄 {{ lore.pageCount || 0 }} pages</span>
<span>🌳 {{ lore.nodeCount || 0 }} dossiers</span>
</div>
</div>
}
<div class="lore-card card-new" (click)="openCreateModal()">
<div class="new-icon">
@@ -35,8 +37,9 @@
</div>
<app-lore-create
*ngIf="showCreateModal"
(close)="onModalClose()"
(created)="onLoreCreated($event)">
</app-lore-create>
@if (showCreateModal) {
<app-lore-create
(close)="onModalClose()"
(created)="onLoreCreated($event)">
</app-lore-create>
}

View File

@@ -1,5 +1,5 @@
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Router } from '@angular/router';
import { LucideAngularModule, BookOpen, Folder, Plus } from 'lucide-angular';
import { LoreService } from '../services/lore.service';
@@ -9,7 +9,7 @@ import { LoreCreateComponent } from './lore-create/lore-create.component';
@Component({
selector: 'app-lore',
imports: [CommonModule, LucideAngularModule, LoreCreateComponent],
imports: [LucideAngularModule, LoreCreateComponent],
templateUrl: './lore.component.html',
styleUrls: ['./lore.component.scss']
})

View File

@@ -17,64 +17,66 @@
<div class="field">
<label>Template *</label>
<div class="templates-grid" *ngIf="templates.length; else emptyTemplates">
<button
type="button"
class="template-card"
*ngFor="let t of templates"
[class.selected]="selectedTemplateId === t.id"
(click)="selectTemplate(t)">
<div class="template-card-head">
<lucide-icon [img]="FileText" [size]="16"></lucide-icon>
<span class="template-name">{{ t.name }}</span>
</div>
<p class="template-description">{{ t.description || '—' }}</p>
</button>
<!-- Carte "+" : sauvegarde le brouillon et part creer un nouveau template ;
template-create renverra ici via le mecanisme returnTo. -->
<a
class="template-card template-card-create"
[routerLink]="['/lore', loreId, 'templates', 'create']"
[queryParams]="{ returnTo: 'page-create' }"
(click)="saveDraft()"
title="Créer un nouveau template pour ce Lore">
<div class="template-card-head">
<lucide-icon [img]="Plus" [size]="16"></lucide-icon>
<span class="template-name">Créer un template</span>
</div>
<p class="template-description">
Vous reviendrez ici automatiquement, votre saisie sera conservée.
</p>
</a>
</div>
<ng-template #emptyTemplates>
@if (templates.length) {
<div class="templates-grid">
@for (t of templates; track t) {
<button
type="button"
class="template-card"
[class.selected]="selectedTemplateId === t.id"
(click)="selectTemplate(t)">
<div class="template-card-head">
<lucide-icon [img]="FileText" [size]="16"></lucide-icon>
<span class="template-name">{{ t.name }}</span>
</div>
<p class="template-description">{{ t.description || '—' }}</p>
</button>
}
<!-- Carte "+" : sauvegarde le brouillon et part creer un nouveau template ;
template-create renverra ici via le mecanisme returnTo. -->
<a
class="template-card template-card-create"
[routerLink]="['/lore', loreId, 'templates', 'create']"
[queryParams]="{ returnTo: 'page-create' }"
(click)="saveDraft()"
title="Créer un nouveau template pour ce Lore">
<div class="template-card-head">
<lucide-icon [img]="Plus" [size]="16"></lucide-icon>
<span class="template-name">Créer un template</span>
</div>
<p class="template-description">
Vous reviendrez ici automatiquement, votre saisie sera conservée.
</p>
</a>
</div>
} @else {
<p class="empty-hint">
Aucun template défini pour ce Lore.
<a [routerLink]="['/lore', loreId, 'templates', 'create']" [queryParams]="{ returnTo: 'page-create' }" (click)="saveDraft()">Créer un template</a> d'abord.
</p>
</ng-template>
}
</div>
<!-- Dossier de destination -->
<div class="field">
<label for="page-node">Dossier de destination *</label>
<ng-container *ngIf="nodes.length; else emptyFolders">
@if (nodes.length) {
<select id="page-node" formControlName="nodeId">
<option value="" disabled>Sélectionnez un dossier</option>
<option *ngFor="let node of nodes" [value]="node.id">{{ node.name }}</option>
@for (node of nodes; track node) {
<option [value]="node.id">{{ node.name }}</option>
}
</select>
<p class="hint">La page sera créée dans ce dossier</p>
</ng-container>
<ng-template #emptyFolders>
} @else {
<p class="empty-hint">
Aucun dossier dans ce Lore.
<a [routerLink]="['/lore', loreId, 'nodes', 'create']" [queryParams]="{ returnTo: 'page-create' }" (click)="saveDraft()">Créer un dossier</a> d'abord.
</p>
</ng-template>
}
</div>
<!-- Aide contextuelle -->
@@ -84,13 +86,15 @@
</div>
<!-- Erreur wizard (parsing &lt;values&gt; ou échec HTTP) -->
<div class="wizard-error" *ngIf="wizardError" role="alert">{{ wizardError }}</div>
@if (wizardError) {
<div class="wizard-error" role="alert">{{ wizardError }}</div>
}
<!-- Actions -->
<div class="actions-row">
<button type="button" class="btn-secondary" (click)="cancel()">Annuler</button>
<button type="button" class="btn-ai" (click)="openWizard()" [disabled]="!canSubmit"
title="Ouvrir l'assistant IA pour pré-remplir les champs">
title="Ouvrir l'assistant IA pour pré-remplir les champs">
<lucide-icon [img]="Sparkles" [size]="14"></lucide-icon>
Créer avec l'IA
</button>

View File

@@ -1,5 +1,5 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import { LucideAngularModule, FileText, Sparkles, Plus } from 'lucide-angular';
@@ -26,7 +26,7 @@ import { AiChatDrawerComponent, ChatPrimaryAction } from '../../shared/ai-chat-d
*/
@Component({
selector: 'app-page-create',
imports: [CommonModule, ReactiveFormsModule, RouterModule, LucideAngularModule, AiChatDrawerComponent],
imports: [ReactiveFormsModule, RouterModule, LucideAngularModule, AiChatDrawerComponent],
templateUrl: './page-create.component.html',
styleUrls: ['./page-create.component.scss']
})

View File

@@ -1,116 +1,114 @@
<div class="page" *ngIf="page">
<app-breadcrumb [items]="breadcrumbItems"></app-breadcrumb>
<header class="page-header">
<div>
<h1>{{ page.title }}</h1>
<p class="subtitle">{{ template?.name || 'Page' }}</p>
</div>
<div class="header-actions">
<button type="button" class="btn-ai"
(click)="toggleChat()"
[disabled]="aiLoading"
[class.active]="chatOpen"
title="Ouvrir l'Assistant IA (chat ou remplissage automatique)">
<lucide-icon [img]="Sparkles" [size]="14"></lucide-icon>
{{ aiLoading ? 'Génération…' : 'Assistant IA' }}
</button>
<button type="button" class="btn-secondary" [routerLink]="['/lore', loreId, 'pages', pageId]">Annuler</button>
<button type="button" class="btn-danger" (click)="delete()">Supprimer</button>
<button type="button" class="btn-primary" (click)="save()" [disabled]="!title.trim()">
Sauvegarder
</button>
</div>
</header>
<div *ngIf="aiError" class="ai-error-banner" role="alert">
<span>{{ aiError }}</span>
<button type="button" class="ai-error-dismiss" (click)="aiError = null" aria-label="Fermer">×</button>
@if (page) {
<div class="page">
<app-breadcrumb [items]="breadcrumbItems"></app-breadcrumb>
<header class="page-header">
<div>
<h1>{{ page.title }}</h1>
<p class="subtitle">{{ template?.name || 'Page' }}</p>
</div>
<div class="header-actions">
<button type="button" class="btn-ai"
(click)="toggleChat()"
[disabled]="aiLoading"
[class.active]="chatOpen"
title="Ouvrir l'Assistant IA (chat ou remplissage automatique)">
<lucide-icon [img]="Sparkles" [size]="14"></lucide-icon>
{{ aiLoading ? 'Génération…' : 'Assistant IA' }}
</button>
<button type="button" class="btn-secondary" [routerLink]="['/lore', loreId, 'pages', pageId]">Annuler</button>
<button type="button" class="btn-danger" (click)="delete()">Supprimer</button>
<button type="button" class="btn-primary" (click)="save()" [disabled]="!title.trim()">
Sauvegarder
</button>
</div>
</header>
@if (aiError) {
<div class="ai-error-banner" role="alert">
<span>{{ aiError }}</span>
<button type="button" class="ai-error-dismiss" (click)="aiError = null" aria-label="Fermer">×</button>
</div>
}
<form class="edit-form">
<!-- Identité ----------------------------------------------------- -->
<div class="field">
<label>Nom</label>
<input type="text" [(ngModel)]="title" name="title" required />
</div>
<div class="field">
<label>Dossier</label>
<select [(ngModel)]="nodeId" name="nodeId">
@for (node of nodes; track node) {
<option [value]="node.id">{{ node.name }}</option>
}
</select>
<p class="hint">Déplacez cette page dans un autre dossier</p>
</div>
<!-- Champs dynamiques du template -------------------------------- -->
@if (template?.fields?.length) {
<h2 class="section-title">Champs</h2>
@for (field of template!.fields; track field) {
<!-- Champ TEXT : textarea editable -->
@if (field.type === 'TEXT') {
<div class="field">
<label>{{ field.name }}</label>
<textarea
[(ngModel)]="values[field.name]"
[name]="'value_' + field.name"
rows="4"
[placeholder]="'Valeur pour ' + field.name + '...'">
</textarea>
</div>
}
<!-- Champ IMAGE : galerie editable. -->
@if (field.type === 'IMAGE') {
<div class="field">
<label>{{ field.name }}</label>
<app-image-gallery
[imageIds]="imageValues[field.name] || []"
[editable]="true"
[layout]="field.layout ?? 'GALLERY'"
(imageIdsChange)="imageValues[field.name] = $event">
</app-image-gallery>
</div>
}
}
}
<!-- Tags --------------------------------------------------------- -->
<h2 class="section-title">Tags</h2>
<div class="field">
<app-chips-input
[value]="tags"
(valueChange)="tags = $event"
placeholder="Ajouter un tag (Entrée pour valider)...">
</app-chips-input>
<p class="hint">Mots-clés libres pour classer et retrouver cette page</p>
</div>
<!-- Liens vers d'autres pages ----------------------------------- -->
<h2 class="section-title">Pages liées</h2>
<div class="field">
<app-lore-link-picker
[value]="relatedPageIds"
[availablePages]="allPages"
[excludePageId]="pageId"
[loreId]="loreId"
(valueChange)="relatedPageIds = $event">
</app-lore-link-picker>
<p class="hint">Cliquez sur un lien pour ouvrir la page associée</p>
</div>
<!-- Notes privées ----------------------------------------------- -->
<h2 class="section-title">Notes privées</h2>
<div class="field">
<textarea
[(ngModel)]="notes"
name="notes"
rows="4"
placeholder="Notes personnelles (non exportées vers FoundryVTT)">
</textarea>
<p class="hint">Visibles uniquement par vous. Utiles pour préparer vos sessions.</p>
</div>
</form>
</div>
<form class="edit-form">
<!-- Identité ----------------------------------------------------- -->
<div class="field">
<label>Nom</label>
<input type="text" [(ngModel)]="title" name="title" required />
</div>
<div class="field">
<label>Dossier</label>
<select [(ngModel)]="nodeId" name="nodeId">
<option *ngFor="let node of nodes" [value]="node.id">{{ node.name }}</option>
</select>
<p class="hint">Déplacez cette page dans un autre dossier</p>
</div>
<!-- Champs dynamiques du template -------------------------------- -->
<ng-container *ngIf="template?.fields?.length">
<h2 class="section-title">Champs</h2>
<ng-container *ngFor="let field of template!.fields">
<!-- Champ TEXT : textarea editable -->
<div class="field" *ngIf="field.type === 'TEXT'">
<label>{{ field.name }}</label>
<textarea
[(ngModel)]="values[field.name]"
[name]="'value_' + field.name"
rows="4"
[placeholder]="'Valeur pour ' + field.name + '...'">
</textarea>
</div>
<!-- Champ IMAGE : galerie editable. -->
<div class="field" *ngIf="field.type === 'IMAGE'">
<label>{{ field.name }}</label>
<app-image-gallery
[imageIds]="imageValues[field.name] || []"
[editable]="true"
[layout]="field.layout ?? 'GALLERY'"
(imageIdsChange)="imageValues[field.name] = $event">
</app-image-gallery>
</div>
</ng-container>
</ng-container>
<!-- Tags --------------------------------------------------------- -->
<h2 class="section-title">Tags</h2>
<div class="field">
<app-chips-input
[value]="tags"
(valueChange)="tags = $event"
placeholder="Ajouter un tag (Entrée pour valider)...">
</app-chips-input>
<p class="hint">Mots-clés libres pour classer et retrouver cette page</p>
</div>
<!-- Liens vers d'autres pages ----------------------------------- -->
<h2 class="section-title">Pages liées</h2>
<div class="field">
<app-lore-link-picker
[value]="relatedPageIds"
[availablePages]="allPages"
[excludePageId]="pageId"
[loreId]="loreId"
(valueChange)="relatedPageIds = $event">
</app-lore-link-picker>
<p class="hint">Cliquez sur un lien pour ouvrir la page associée</p>
</div>
<!-- Notes privées ----------------------------------------------- -->
<h2 class="section-title">Notes privées</h2>
<div class="field">
<textarea
[(ngModel)]="notes"
name="notes"
rows="4"
placeholder="Notes personnelles (non exportées vers FoundryVTT)">
</textarea>
<p class="hint">Visibles uniquement par vous. Utiles pour préparer vos sessions.</p>
</div>
</form>
</div>
}
<!-- Drawer chat IA (hors du .page pour pouvoir couvrir le viewport côté droit) -->
<app-ai-chat-drawer

View File

@@ -1,5 +1,5 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { forkJoin } from 'rxjs';
@@ -36,7 +36,7 @@ import { ConfirmDialogService } from '../../shared/confirm-dialog/confirm-dialog
*/
@Component({
selector: 'app-page-edit',
imports: [CommonModule, FormsModule, RouterLink, LucideAngularModule, ChipsInputComponent, LoreLinkPickerComponent, BreadcrumbComponent, AiChatDrawerComponent, ImageGalleryComponent],
imports: [FormsModule, RouterLink, LucideAngularModule, ChipsInputComponent, LoreLinkPickerComponent, BreadcrumbComponent, AiChatDrawerComponent, ImageGalleryComponent],
templateUrl: './page-edit.component.html',
styleUrls: ['./page-edit.component.scss']
})

View File

@@ -1,72 +1,81 @@
<div class="view-page" *ngIf="page">
<app-breadcrumb [items]="breadcrumbItems"></app-breadcrumb>
<header class="view-header">
<div>
<h1>{{ page.title }}</h1>
<p class="view-subtitle">{{ template?.name || 'Page' }}</p>
</div>
<div class="view-actions">
<button type="button" class="btn-primary" (click)="editMode()">
<lucide-icon [img]="Pencil" [size]="14"></lucide-icon>
Modifier
</button>
<button type="button" class="btn-danger" (click)="deletePage()" title="Supprimer la page">
<lucide-icon [img]="Trash2" [size]="14"></lucide-icon>
Supprimer
</button>
</div>
</header>
<!-- Champs dynamiques du template (seuls les champs TEXT sont rendus ici ;
le support complet des champs IMAGE arrive a l'etape 5). -->
<ng-container *ngIf="template?.fields?.length">
<ng-container *ngFor="let field of template!.fields">
<section class="view-section" *ngIf="field.type === 'TEXT'">
<h2 class="view-section-title">{{ field.name }}</h2>
<p class="view-section-body" *ngIf="valueOf(field.name); else emptyField">{{ valueOf(field.name) }}</p>
<ng-template #emptyField>
<p class="view-section-empty">Non renseigné</p>
</ng-template>
@if (page) {
<div class="view-page">
<app-breadcrumb [items]="breadcrumbItems"></app-breadcrumb>
<header class="view-header">
<div>
<h1>{{ page.title }}</h1>
<p class="view-subtitle">{{ template?.name || 'Page' }}</p>
</div>
<div class="view-actions">
<button type="button" class="btn-primary" (click)="editMode()">
<lucide-icon [img]="Pencil" [size]="14"></lucide-icon>
Modifier
</button>
<button type="button" class="btn-danger" (click)="deletePage()" title="Supprimer la page">
<lucide-icon [img]="Trash2" [size]="14"></lucide-icon>
Supprimer
</button>
</div>
</header>
<!-- Champs dynamiques du template (seuls les champs TEXT sont rendus ici ;
le support complet des champs IMAGE arrive a l'etape 5). -->
@if (template?.fields?.length) {
@for (field of template!.fields; track field) {
@if (field.type === 'TEXT') {
<section class="view-section">
<h2 class="view-section-title">{{ field.name }}</h2>
@if (valueOf(field.name)) {
<p class="view-section-body">{{ valueOf(field.name) }}</p>
} @else {
<p class="view-section-empty">Non renseigné</p>
}
</section>
}
@if (field.type === 'IMAGE') {
<section class="view-section">
<h2 class="view-section-title">{{ field.name }}</h2>
<app-image-gallery
[imageIds]="imageIdsOf(field.name)"
[layout]="field.layout ?? 'GALLERY'">
</app-image-gallery>
</section>
}
}
}
<!-- Tags -->
@if ((page.tags?.length ?? 0) > 0) {
<section class="view-section">
<h2 class="view-section-title">Tags</h2>
<div class="view-chips">
@for (tag of page.tags; track tag) {
<span class="view-chip view-chip--tag">{{ tag }}</span>
}
</div>
</section>
<section class="view-section" *ngIf="field.type === 'IMAGE'">
<h2 class="view-section-title">{{ field.name }}</h2>
<app-image-gallery
[imageIds]="imageIdsOf(field.name)"
[layout]="field.layout ?? 'GALLERY'">
</app-image-gallery>
}
<!-- Pages liées -->
@if ((page.relatedPageIds?.length ?? 0) > 0) {
<section class="view-section">
<h2 class="view-section-title">Pages liées</h2>
<div class="view-chips">
@for (relId of page.relatedPageIds; track relId) {
<a class="view-chip"
[routerLink]="['/lore', loreId, 'pages', relId]">
{{ titleOfRelated(relId) }}
</a>
}
</div>
</section>
</ng-container>
</ng-container>
<!-- Tags -->
<section class="view-section" *ngIf="(page.tags?.length ?? 0) > 0">
<h2 class="view-section-title">Tags</h2>
<div class="view-chips">
<span class="view-chip view-chip--tag" *ngFor="let tag of page.tags">{{ tag }}</span>
</div>
</section>
<!-- Pages liées -->
<section class="view-section" *ngIf="(page.relatedPageIds?.length ?? 0) > 0">
<h2 class="view-section-title">Pages liées</h2>
<div class="view-chips">
<a class="view-chip"
*ngFor="let relId of page.relatedPageIds"
[routerLink]="['/lore', loreId, 'pages', relId]">
{{ titleOfRelated(relId) }}
</a>
</div>
</section>
<!-- Notes privées MJ -->
<section class="view-section view-section--private" *ngIf="page.notes?.trim()">
<h2 class="view-section-title">
<span class="view-section-icon">🔒</span>
Notes privées
</h2>
<p class="view-section-body">{{ page.notes }}</p>
</section>
</div>
}
<!-- Notes privées MJ -->
@if (page.notes?.trim()) {
<section class="view-section view-section--private">
<h2 class="view-section-title">
<span class="view-section-icon">🔒</span>
Notes privées
</h2>
<p class="view-section-body">{{ page.notes }}</p>
</section>
}
</div>
}

View File

@@ -1,5 +1,5 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import { forkJoin } from 'rxjs';
import { LucideAngularModule, Pencil, Trash2 } from 'lucide-angular';
@@ -28,7 +28,7 @@ import { ConfirmDialogService } from '../../shared/confirm-dialog/confirm-dialog
*/
@Component({
selector: 'app-page-view',
imports: [CommonModule, RouterModule, LucideAngularModule, BreadcrumbComponent, ImageGalleryComponent],
imports: [RouterModule, LucideAngularModule, BreadcrumbComponent, ImageGalleryComponent],
templateUrl: './page-view.component.html',
styleUrls: ['./page-view.component.scss']
})

View File

@@ -23,22 +23,23 @@
<div class="field">
<label for="template-default-node">Dossier par défaut *</label>
<ng-container *ngIf="nodes.length; else emptyFolders">
@if (nodes.length) {
<select id="template-default-node" formControlName="defaultNodeId">
<option value="" disabled>Sélectionnez un dossier</option>
<option *ngFor="let node of nodes" [value]="node.id">{{ node.name }}</option>
@for (node of nodes; track node) {
<option [value]="node.id">{{ node.name }}</option>
}
</select>
<p class="hint">Les pages créées avec ce template seront placées dans ce dossier</p>
</ng-container>
<ng-template #emptyFolders>
} @else {
<p class="empty-hint">
Aucun dossier dans ce Lore.
<a [routerLink]="['/lore', loreId, 'nodes', 'create']"
[queryParams]="{ returnTo: nodeCreateReturnTo }"
(click)="saveDraft()">Créer un dossier</a> d'abord.
[queryParams]="{ returnTo: nodeCreateReturnTo }"
(click)="saveDraft()">Créer un dossier</a> d'abord.
</p>
</ng-template>
}
</div>
</div>
@@ -49,47 +50,51 @@
<label class="section-label">Champs du template *</label>
<ul class="fields-list">
<li class="field-row" *ngFor="let f of fields; let i = index; let first = first; let last = last">
<div class="reorder-stack">
<button type="button" class="btn-icon btn-reorder"
(click)="moveField(i, -1)"
[disabled]="first"
aria-label="Monter d'un cran" title="Monter">
<lucide-icon [img]="ChevronUp" [size]="12"></lucide-icon>
@for (f of fields; track f; let i = $index; let first = $first; let last = $last) {
<li class="field-row">
<div class="reorder-stack">
<button type="button" class="btn-icon btn-reorder"
(click)="moveField(i, -1)"
[disabled]="first"
aria-label="Monter d'un cran" title="Monter">
<lucide-icon [img]="ChevronUp" [size]="12"></lucide-icon>
</button>
<button type="button" class="btn-icon btn-reorder"
(click)="moveField(i, 1)"
[disabled]="last"
aria-label="Descendre d'un cran" title="Descendre">
<lucide-icon [img]="ChevronDown" [size]="12"></lucide-icon>
</button>
</div>
<span class="field-chip" [class.field-chip-image]="f.type === 'IMAGE'">
<lucide-icon [img]="f.type === 'IMAGE' ? ImageIcon : Type" [size]="12"></lucide-icon>
{{ f.name }}
</span>
<button type="button"
class="btn-icon btn-type-toggle"
(click)="toggleFieldType(i)"
[attr.aria-label]="'Basculer vers ' + (f.type === 'TEXT' ? 'Image' : 'Texte')"
[title]="f.type === 'TEXT' ? 'Transformer en champ Image' : 'Transformer en champ Texte'">
{{ f.type === 'TEXT' ? 'Texte' : 'Image' }}
</button>
<button type="button" class="btn-icon btn-reorder"
(click)="moveField(i, 1)"
[disabled]="last"
aria-label="Descendre d'un cran" title="Descendre">
<lucide-icon [img]="ChevronDown" [size]="12"></lucide-icon>
@if (f.type === 'IMAGE') {
<select
class="layout-select"
[ngModel]="f.layout ?? 'GALLERY'"
[ngModelOptions]="{ standalone: true }"
(ngModelChange)="setFieldLayout(i, $event)"
title="Mise en page des images">
<option value="GALLERY">Grille</option>
<option value="HERO">Heros</option>
<option value="MASONRY">Mosaique</option>
<option value="CAROUSEL">Carrousel</option>
</select>
}
<button type="button" class="btn-icon" (click)="removeField(i)" aria-label="Supprimer">
<lucide-icon [img]="Trash2" [size]="14"></lucide-icon>
</button>
</div>
<span class="field-chip" [class.field-chip-image]="f.type === 'IMAGE'">
<lucide-icon [img]="f.type === 'IMAGE' ? ImageIcon : Type" [size]="12"></lucide-icon>
{{ f.name }}
</span>
<button type="button"
class="btn-icon btn-type-toggle"
(click)="toggleFieldType(i)"
[attr.aria-label]="'Basculer vers ' + (f.type === 'TEXT' ? 'Image' : 'Texte')"
[title]="f.type === 'TEXT' ? 'Transformer en champ Image' : 'Transformer en champ Texte'">
{{ f.type === 'TEXT' ? 'Texte' : 'Image' }}
</button>
<select *ngIf="f.type === 'IMAGE'"
class="layout-select"
[ngModel]="f.layout ?? 'GALLERY'"
[ngModelOptions]="{ standalone: true }"
(ngModelChange)="setFieldLayout(i, $event)"
title="Mise en page des images">
<option value="GALLERY">Grille</option>
<option value="HERO">Heros</option>
<option value="MASONRY">Mosaique</option>
<option value="CAROUSEL">Carrousel</option>
</select>
<button type="button" class="btn-icon" (click)="removeField(i)" aria-label="Supprimer">
<lucide-icon [img]="Trash2" [size]="14"></lucide-icon>
</button>
</li>
</li>
}
</ul>
<div class="field-row add-row">

View File

@@ -1,5 +1,5 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule, ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import { LucideAngularModule, Plus, Trash2, Type, Image as ImageIcon, ChevronUp, ChevronDown } from 'lucide-angular';
@@ -20,7 +20,7 @@ import { popReturnTo } from '../return-stack.helper';
*/
@Component({
selector: 'app-template-create',
imports: [CommonModule, FormsModule, ReactiveFormsModule, RouterModule, LucideAngularModule],
imports: [FormsModule, ReactiveFormsModule, RouterModule, LucideAngularModule],
templateUrl: './template-create.component.html',
styleUrls: ['./template-create.component.scss']
})

View File

@@ -1,117 +1,110 @@
<div class="page" *ngIf="template">
<header class="page-header">
<div>
<h1>{{ template.name }}</h1>
<p class="subtitle">Template</p>
</div>
<div class="header-actions">
<button type="button" class="btn-danger" (click)="delete()">Supprimer</button>
<button type="button" class="btn-primary" (click)="save()" [disabled]="form.invalid">Sauvegarder</button>
</div>
</header>
<form [formGroup]="form" class="template-form">
<!-- Colonne gauche ---------------------------------------------- -->
<div class="col-left">
<div class="field">
<label for="template-edit-name">Nom</label>
<input id="template-edit-name" type="text" formControlName="name" />
@if (template) {
<div class="page">
<header class="page-header">
<div>
<h1>{{ template.name }}</h1>
<p class="subtitle">Template</p>
</div>
<div class="field">
<label for="template-edit-default-node">Dossier par défaut</label>
<select id="template-edit-default-node" formControlName="defaultNodeId">
<option value="">-- Aucun --</option>
<option *ngFor="let node of nodes" [value]="node.id">{{ node.name }}</option>
</select>
<p class="hint">Les pages créées avec ce template seront placées dans ce dossier par défaut</p>
<div class="header-actions">
<button type="button" class="btn-danger" (click)="delete()">Supprimer</button>
<button type="button" class="btn-primary" (click)="save()" [disabled]="form.invalid">Sauvegarder</button>
</div>
<div class="field">
<label for="template-edit-description">Description</label>
<textarea id="template-edit-description" formControlName="description" rows="6"></textarea>
</header>
<form [formGroup]="form" class="template-form">
<!-- Colonne gauche ---------------------------------------------- -->
<div class="col-left">
<div class="field">
<label for="template-edit-name">Nom</label>
<input id="template-edit-name" type="text" formControlName="name" />
</div>
<div class="field">
<label for="template-edit-default-node">Dossier par défaut</label>
<select id="template-edit-default-node" formControlName="defaultNodeId">
<option value="">-- Aucun --</option>
@for (node of nodes; track node) {
<option [value]="node.id">{{ node.name }}</option>
}
</select>
<p class="hint">Les pages créées avec ce template seront placées dans ce dossier par défaut</p>
</div>
<div class="field">
<label for="template-edit-description">Description</label>
<textarea id="template-edit-description" formControlName="description" rows="6"></textarea>
</div>
</div>
</div>
<!-- Colonne droite --------------------------------------------- -->
<div class="col-right">
<label class="section-label">Champs du template</label>
<ul class="fields-list">
<li class="field-row" *ngFor="let f of fields; let i = index; let first = first; let last = last">
<div class="reorder-stack">
<button type="button" class="btn-icon-ghost btn-reorder"
(click)="moveField(i, -1)"
[disabled]="first"
aria-label="Monter d'un cran" title="Monter">
<lucide-icon [img]="ChevronUp" [size]="12"></lucide-icon>
</button>
<button type="button" class="btn-icon-ghost btn-reorder"
(click)="moveField(i, 1)"
[disabled]="last"
aria-label="Descendre d'un cran" title="Descendre">
<lucide-icon [img]="ChevronDown" [size]="12"></lucide-icon>
</button>
</div>
<span class="field-chip"
<!-- Colonne droite --------------------------------------------- -->
<div class="col-right">
<label class="section-label">Champs du template</label>
<ul class="fields-list">
@for (f of fields; track f; let i = $index; let first = $first; let last = $last) {
<li class="field-row">
<div class="reorder-stack">
<button type="button" class="btn-icon-ghost btn-reorder"
(click)="moveField(i, -1)"
[disabled]="first"
aria-label="Monter d'un cran" title="Monter">
<lucide-icon [img]="ChevronUp" [size]="12"></lucide-icon>
</button>
<button type="button" class="btn-icon-ghost btn-reorder"
(click)="moveField(i, 1)"
[disabled]="last"
aria-label="Descendre d'un cran" title="Descendre">
<lucide-icon [img]="ChevronDown" [size]="12"></lucide-icon>
</button>
</div>
<span class="field-chip"
[class.field-chip-image]="f.type === 'IMAGE'"
[class.field-chip-existing]="f.type !== 'IMAGE' && isExistingField(f)"
[class.field-chip-new]="f.type !== 'IMAGE' && !isExistingField(f)">
<lucide-icon [img]="f.type === 'IMAGE' ? ImageIcon : Type" [size]="12"></lucide-icon>
{{ f.name }}
</span>
<button type="button"
class="btn-icon-ghost btn-type-toggle"
(click)="toggleFieldType(i)"
[title]="f.type === 'TEXT' ? 'Transformer en champ Image' : 'Transformer en champ Texte'">
{{ f.type === 'TEXT' ? 'Texte' : 'Image' }}
</button>
<select *ngIf="f.type === 'IMAGE'"
<lucide-icon [img]="f.type === 'IMAGE' ? ImageIcon : Type" [size]="12"></lucide-icon>
{{ f.name }}
</span>
<button type="button"
class="btn-icon-ghost btn-type-toggle"
(click)="toggleFieldType(i)"
[title]="f.type === 'TEXT' ? 'Transformer en champ Image' : 'Transformer en champ Texte'">
{{ f.type === 'TEXT' ? 'Texte' : 'Image' }}
</button>
@if (f.type === 'IMAGE') {
<select
class="layout-select"
[ngModel]="f.layout ?? 'GALLERY'"
[ngModelOptions]="{ standalone: true }"
(ngModelChange)="setFieldLayout(i, $event)"
title="Mise en page des images">
<option value="GALLERY">Grille</option>
<option value="HERO">Heros</option>
<option value="MASONRY">Mosaique</option>
<option value="CAROUSEL">Carrousel</option>
<option value="GALLERY">Grille</option>
<option value="HERO">Heros</option>
<option value="MASONRY">Mosaique</option>
<option value="CAROUSEL">Carrousel</option>
</select>
}
<button type="button" class="btn-icon-danger" (click)="removeField(i)" aria-label="Supprimer">
<lucide-icon [img]="Trash2" [size]="14"></lucide-icon>
</button>
</li>
}
</ul>
<div class="field-row add-row">
<input
type="text"
[(ngModel)]="newFieldName"
[ngModelOptions]="{ standalone: true }"
placeholder="+ Ajouter un champ"
(keydown.enter)="$event.preventDefault(); addField()" />
<select
class="type-select"
[(ngModel)]="newFieldType"
[ngModelOptions]="{ standalone: true }"
aria-label="Type du champ">
<option value="TEXT">Texte</option>
<option value="IMAGE">Image</option>
</select>
<button type="button" class="btn-icon-danger" (click)="removeField(i)" aria-label="Supprimer">
<lucide-icon [img]="Trash2" [size]="14"></lucide-icon>
<button type="button" class="btn-add" (click)="addField()" title="Ajouter le champ">
<lucide-icon [img]="Plus" [size]="14"></lucide-icon>
</button>
</li>
</ul>
<div class="field-row add-row">
<input
type="text"
[(ngModel)]="newFieldName"
[ngModelOptions]="{ standalone: true }"
placeholder="+ Ajouter un champ"
(keydown.enter)="$event.preventDefault(); addField()" />
<select
class="type-select"
[(ngModel)]="newFieldType"
[ngModelOptions]="{ standalone: true }"
aria-label="Type du champ">
<option value="TEXT">Texte</option>
<option value="IMAGE">Image</option>
</select>
<button type="button" class="btn-add" (click)="addField()" title="Ajouter le champ">
<lucide-icon [img]="Plus" [size]="14"></lucide-icon>
</button>
</div>
<p class="hint">Texte = zone editable + generable par l'IA. Image = galerie d'illustrations.</p>
</div>
<p class="hint">Texte = zone editable + generable par l'IA. Image = galerie d'illustrations.</p>
</div>
</form>
</div>
</form>
</div>
}

View File

@@ -1,5 +1,5 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule, ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { forkJoin, Subject } from 'rxjs';
@@ -21,7 +21,7 @@ import { ConfirmDialogService } from '../../shared/confirm-dialog/confirm-dialog
*/
@Component({
selector: 'app-template-edit',
imports: [CommonModule, FormsModule, ReactiveFormsModule, LucideAngularModule],
imports: [FormsModule, ReactiveFormsModule, LucideAngularModule],
templateUrl: './template-edit.component.html',
styleUrls: ['./template-edit.component.scss']
})