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

@@ -2,33 +2,43 @@
<div #messagesContainer class="messages-area">
<div class="welcome-hint" *ngIf="messages.length === 0 && !currentAssistantText && !error">
<lucide-icon [img]="Sparkles" [size]="18"></lucide-icon>
<p>Pose une question à l'IA pendant la partie.</p>
<p class="welcome-sub">
Elle connaît ton univers, ta campagne, les règles du système et tout ce qui a été noté dans le journal.
</p>
</div>
@if (messages.length === 0 && !currentAssistantText && !error) {
<div class="welcome-hint">
<lucide-icon [img]="Sparkles" [size]="18"></lucide-icon>
<p>Pose une question à l'IA pendant la partie.</p>
<p class="welcome-sub">
Elle connaît ton univers, ta campagne, les règles du système et tout ce qui a été noté dans le journal.
</p>
</div>
}
<div *ngFor="let m of messages" class="msg" [class.msg--user]="m.role === 'user'" [class.msg--assistant]="m.role === 'assistant'">
<div class="msg-content">{{ m.content }}</div>
<button *ngIf="m.role === 'assistant'"
type="button"
class="msg-action"
[disabled]="!canSaveToJournal"
[title]="canSaveToJournal ? 'Ajouter cette réponse au journal' : 'Session terminée'"
(click)="onSaveToJournal(m.content)">
<lucide-icon [img]="BookmarkPlus" [size]="12"></lucide-icon>
Au journal
</button>
</div>
@for (m of messages; track m) {
<div class="msg" [class.msg--user]="m.role === 'user'" [class.msg--assistant]="m.role === 'assistant'">
<div class="msg-content">{{ m.content }}</div>
@if (m.role === 'assistant') {
<button
type="button"
class="msg-action"
[disabled]="!canSaveToJournal"
[title]="canSaveToJournal ? 'Ajouter cette réponse au journal' : 'Session terminée'"
(click)="onSaveToJournal(m.content)">
<lucide-icon [img]="BookmarkPlus" [size]="12"></lucide-icon>
Au journal
</button>
}
</div>
}
<!-- Stream en cours : on affiche les tokens au fil de l'eau. -->
<div *ngIf="currentAssistantText" class="msg msg--assistant msg--streaming">
<div class="msg-content">{{ currentAssistantText }}<span class="cursor"></span></div>
</div>
@if (currentAssistantText) {
<div class="msg msg--assistant msg--streaming">
<div class="msg-content">{{ currentAssistantText }}<span class="cursor"></span></div>
</div>
}
<p class="error-hint" *ngIf="error">{{ error }}</p>
@if (error) {
<p class="error-hint">{{ error }}</p>
}
</div>
<div class="composer">
@@ -39,33 +49,37 @@
rows="2"
[placeholder]="isStreaming ? 'LIA répond' : 'Demande une idée, un rebondissement, une description'"
[disabled]="isStreaming"
(keydown.control.enter)="send()"></textarea>
(keydown.control.enter)="send()"></textarea>
<div class="composer-actions">
<button type="button"
class="btn-link"
[disabled]="messages.length === 0 && !currentAssistantText"
(click)="clearConversation()"
title="Effacer la conversation">
class="btn-link"
[disabled]="messages.length === 0 && !currentAssistantText"
(click)="clearConversation()"
title="Effacer la conversation">
<lucide-icon [img]="Trash2" [size]="12"></lucide-icon>
</button>
<button *ngIf="!isStreaming"
type="button"
class="btn-primary btn-send"
[disabled]="!input.trim()"
(click)="send()">
<lucide-icon [img]="Send" [size]="14"></lucide-icon>
Envoyer
</button>
@if (!isStreaming) {
<button
type="button"
class="btn-primary btn-send"
[disabled]="!input.trim()"
(click)="send()">
<lucide-icon [img]="Send" [size]="14"></lucide-icon>
Envoyer
</button>
}
<button *ngIf="isStreaming"
type="button"
class="btn-secondary btn-send"
(click)="cancelStream()">
<lucide-icon [img]="Square" [size]="14"></lucide-icon>
Stop
</button>
@if (isStreaming) {
<button
type="button"
class="btn-secondary btn-send"
(click)="cancelStream()">
<lucide-icon [img]="Square" [size]="14"></lucide-icon>
Stop
</button>
}
</div>
</div>

View File

@@ -2,7 +2,7 @@ import {
Component, EventEmitter, Input, OnChanges, OnDestroy, Output, SimpleChanges,
ElementRef, ViewChild
} from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import {
LucideAngularModule, Send, Sparkles, Trash2, BookmarkPlus, Square
@@ -23,7 +23,7 @@ import { AiChatService, ChatMessage } from '../../services/ai-chat.service';
*/
@Component({
selector: 'app-session-ai-chat-panel',
imports: [CommonModule, FormsModule, LucideAngularModule],
imports: [FormsModule, LucideAngularModule],
templateUrl: './session-ai-chat-panel.component.html',
styleUrls: ['./session-ai-chat-panel.component.scss']
})

View File

@@ -1,190 +1,193 @@
<div class="session-detail" *ngIf="session">
<a class="back-link" [routerLink]="campaignId ? ['/campaigns', campaignId] : ['/campaigns']">
<lucide-icon [img]="ArrowLeft" [size]="14"></lucide-icon>
Retour à la campagne
</a>
<div class="detail-header">
<div class="header-texts">
<div class="title-row" *ngIf="!editingName">
<h1>
<lucide-icon [img]="Dices" [size]="24"></lucide-icon>
{{ session.name }}
</h1>
<button type="button" class="btn-icon" (click)="startRename()" title="Renommer la session">
<lucide-icon [img]="Pencil" [size]="14"></lucide-icon>
</button>
@if (session) {
<div class="session-detail">
<a class="back-link" [routerLink]="campaignId ? ['/campaigns', campaignId] : ['/campaigns']">
<lucide-icon [img]="ArrowLeft" [size]="14"></lucide-icon>
Retour à la campagne
</a>
<div class="detail-header">
<div class="header-texts">
@if (!editingName) {
<div class="title-row">
<h1>
<lucide-icon [img]="Dices" [size]="24"></lucide-icon>
{{ session.name }}
</h1>
<button type="button" class="btn-icon" (click)="startRename()" title="Renommer la session">
<lucide-icon [img]="Pencil" [size]="14"></lucide-icon>
</button>
</div>
}
@if (editingName) {
<div class="title-row edit-mode">
<input type="text"
[(ngModel)]="editName"
name="editName"
(keydown.enter)="saveRename()"
(keydown.escape)="cancelRename()"
autofocus />
<button type="button" class="btn-icon" (click)="saveRename()" [disabled]="!editName.trim()" title="Valider">
<lucide-icon [img]="Check" [size]="14"></lucide-icon>
</button>
<button type="button" class="btn-icon" (click)="cancelRename()" title="Annuler">
Annuler
</button>
</div>
}
<div class="meta">
<span class="badge" [class.badge-active]="session.active">
{{ session.active ? 'En cours' : 'Terminée' }}
</span>
<span class="badge badge-muted">Démarrée le {{ session.startedAt | date:'dd/MM/yyyy HH:mm' }}</span>
@if (session.endedAt) {
<span class="badge badge-muted">
Terminée le {{ session.endedAt | date:'dd/MM/yyyy HH:mm' }}
</span>
}
</div>
</div>
<div class="title-row edit-mode" *ngIf="editingName">
<input type="text"
[(ngModel)]="editName"
name="editName"
(keydown.enter)="saveRename()"
(keydown.escape)="cancelRename()"
autofocus />
<button type="button" class="btn-icon" (click)="saveRename()" [disabled]="!editName.trim()" title="Valider">
<lucide-icon [img]="Check" [size]="14"></lucide-icon>
<div class="header-actions">
@if (session.active) {
<button type="button" class="btn-secondary" (click)="endSession()">
<lucide-icon [img]="Square" [size]="14"></lucide-icon>
Terminer la session
</button>
}
<button type="button" class="btn-danger" (click)="deleteSession()">
<lucide-icon [img]="Trash2" [size]="14"></lucide-icon>
Supprimer
</button>
<button type="button" class="btn-icon" (click)="cancelRename()" title="Annuler">
Annuler
</button>
</div>
<div class="meta">
<span class="badge" [class.badge-active]="session.active">
{{ session.active ? 'En cours' : 'Terminée' }}
</span>
<span class="badge badge-muted">Démarrée le {{ session.startedAt | date:'dd/MM/yyyy HH:mm' }}</span>
<span class="badge badge-muted" *ngIf="session.endedAt">
Terminée le {{ session.endedAt | date:'dd/MM/yyyy HH:mm' }}
</span>
</div>
</div>
<div class="header-actions">
<button *ngIf="session.active" type="button" class="btn-secondary" (click)="endSession()">
<lucide-icon [img]="Square" [size]="14"></lucide-icon>
Terminer la session
</button>
<button type="button" class="btn-danger" (click)="deleteSession()">
<lucide-icon [img]="Trash2" [size]="14"></lucide-icon>
Supprimer
</button>
</div>
</div>
<!-- ============ Mode jeu : 2 colonnes (journal + panneau référence) ============ -->
<div class="play-grid">
<!-- Colonne gauche : journal -->
<div class="play-main">
<!-- Ajouter une entrée -->
<section class="detail-section add-entry-section" *ngIf="session.active">
<div class="type-selector">
<button *ngFor="let type of entryTypes"
<!-- ============ Mode jeu : 2 colonnes (journal + panneau référence) ============ -->
<div class="play-grid">
<!-- Colonne gauche : journal -->
<div class="play-main">
<!-- Ajouter une entrée -->
@if (session.active) {
<section class="detail-section add-entry-section">
<div class="type-selector">
@for (type of entryTypes; track type) {
<button
type="button"
class="type-chip"
[class.type-chip--active]="newEntryType === type"
[style.--type-color]="entryTypeMeta[type].color"
(click)="newEntryType = type">
<lucide-icon [img]="typeIcons[type]" [size]="14"></lucide-icon>
{{ entryTypeMeta[type].label }}
</button>
</div>
<textarea class="entry-input"
[(ngModel)]="newEntryContent"
name="newEntryContent"
rows="3"
[placeholder]="'Ajouter une ' + entryTypeMeta[newEntryType].label.toLowerCase() + ''"
(keydown.control.enter)="submitNewEntry()"></textarea>
<div class="entry-input-footer">
<span class="hint">Ctrl + Entrée pour ajouter</span>
<button type="button"
class="btn-primary"
[disabled]="!newEntryContent.trim() || submittingEntry"
(click)="submitNewEntry()">
<lucide-icon [img]="Plus" [size]="14"></lucide-icon>
Ajouter
</button>
</div>
</section>
<!-- Timeline -->
<section class="detail-section timeline-section">
<h2>Journal de session</h2>
<div class="empty-state" *ngIf="entries.length === 0">
<p>Aucune entrée pour le moment.</p>
<p class="hint" *ngIf="session.active">
Saisis une note, un évènement ou un jet ci-dessus pour commencer le journal.
</p>
</div>
<ul class="timeline" *ngIf="entries.length > 0">
<li class="timeline-entry"
*ngFor="let entry of entries"
[style.--type-color]="entryTypeMeta[entry.type].color">
<div class="entry-marker">
<lucide-icon [img]="typeIcons[entry.type]" [size]="14"></lucide-icon>
<lucide-icon [img]="typeIcons[type]" [size]="14"></lucide-icon>
{{ entryTypeMeta[type].label }}
</button>
}
</div>
<div class="entry-body">
<!-- Mode lecture -->
<ng-container *ngIf="editingEntryId !== entry.id">
<div class="entry-header">
<span class="entry-type">{{ entryTypeMeta[entry.type].label }}</span>
<span class="entry-time">{{ entry.occurredAt | date:'HH:mm — dd/MM/yyyy' }}</span>
<div class="entry-actions">
<button type="button" class="btn-icon" (click)="startEditEntry(entry)" title="Modifier">
<lucide-icon [img]="Pencil" [size]="12"></lucide-icon>
</button>
<button type="button" class="btn-icon btn-icon--danger" (click)="deleteEntry(entry)" title="Supprimer">
<lucide-icon [img]="Trash2" [size]="12"></lucide-icon>
</button>
<textarea class="entry-input"
[(ngModel)]="newEntryContent"
name="newEntryContent"
rows="3"
[placeholder]="'Ajouter une ' + entryTypeMeta[newEntryType].label.toLowerCase() + ''"
(keydown.control.enter)="submitNewEntry()"></textarea>
<div class="entry-input-footer">
<span class="hint">Ctrl + Entrée pour ajouter</span>
<button type="button"
class="btn-primary"
[disabled]="!newEntryContent.trim() || submittingEntry"
(click)="submitNewEntry()">
<lucide-icon [img]="Plus" [size]="14"></lucide-icon>
Ajouter
</button>
</div>
</section>
}
<!-- Timeline -->
<section class="detail-section timeline-section">
<h2>Journal de session</h2>
@if (entries.length === 0) {
<div class="empty-state">
<p>Aucune entrée pour le moment.</p>
@if (session.active) {
<p class="hint">
Saisis une note, un évènement ou un jet ci-dessus pour commencer le journal.
</p>
}
</div>
}
@if (entries.length > 0) {
<ul class="timeline">
@for (entry of entries; track entry) {
<li class="timeline-entry"
[style.--type-color]="entryTypeMeta[entry.type].color">
<div class="entry-marker">
<lucide-icon [img]="typeIcons[entry.type]" [size]="14"></lucide-icon>
</div>
</div>
<p class="entry-content">{{ entry.content }}</p>
</ng-container>
<!-- Mode édition -->
<ng-container *ngIf="editingEntryId === entry.id">
<div class="type-selector type-selector--compact">
<button *ngFor="let type of entryTypes"
type="button"
class="type-chip"
[class.type-chip--active]="editEntryType === type"
[style.--type-color]="entryTypeMeta[type].color"
(click)="editEntryType = type">
<lucide-icon [img]="typeIcons[type]" [size]="12"></lucide-icon>
{{ entryTypeMeta[type].label }}
</button>
</div>
<textarea class="entry-input"
[(ngModel)]="editEntryContent"
name="editEntryContent"
rows="3"
(keydown.control.enter)="saveEditEntry(entry)"
(keydown.escape)="cancelEditEntry()"></textarea>
<div class="entry-input-footer">
<button type="button" class="btn-secondary btn-sm" (click)="cancelEditEntry()">
<lucide-icon [img]="X" [size]="12"></lucide-icon>
Annuler
</button>
<button type="button"
<div class="entry-body">
<!-- Mode lecture -->
@if (editingEntryId !== entry.id) {
<div class="entry-header">
<span class="entry-type">{{ entryTypeMeta[entry.type].label }}</span>
<span class="entry-time">{{ entry.occurredAt | date:'HH:mm — dd/MM/yyyy' }}</span>
<div class="entry-actions">
<button type="button" class="btn-icon" (click)="startEditEntry(entry)" title="Modifier">
<lucide-icon [img]="Pencil" [size]="12"></lucide-icon>
</button>
<button type="button" class="btn-icon btn-icon--danger" (click)="deleteEntry(entry)" title="Supprimer">
<lucide-icon [img]="Trash2" [size]="12"></lucide-icon>
</button>
</div>
</div>
<p class="entry-content">{{ entry.content }}</p>
}
<!-- Mode édition -->
@if (editingEntryId === entry.id) {
<div class="type-selector type-selector--compact">
@for (type of entryTypes; track type) {
<button
type="button"
class="type-chip"
[class.type-chip--active]="editEntryType === type"
[style.--type-color]="entryTypeMeta[type].color"
(click)="editEntryType = type">
<lucide-icon [img]="typeIcons[type]" [size]="12"></lucide-icon>
{{ entryTypeMeta[type].label }}
</button>
}
</div>
<textarea class="entry-input"
[(ngModel)]="editEntryContent"
name="editEntryContent"
rows="3"
(keydown.control.enter)="saveEditEntry(entry)"
(keydown.escape)="cancelEditEntry()"></textarea>
<div class="entry-input-footer">
<button type="button" class="btn-secondary btn-sm" (click)="cancelEditEntry()">
<lucide-icon [img]="X" [size]="12"></lucide-icon>
Annuler
</button>
<button type="button"
class="btn-primary btn-sm"
[disabled]="!editEntryContent.trim()"
(click)="saveEditEntry(entry)">
<lucide-icon [img]="Check" [size]="12"></lucide-icon>
Sauvegarder
</button>
</div>
</ng-container>
</div>
</li>
</ul>
</section>
<lucide-icon [img]="Check" [size]="12"></lucide-icon>
Sauvegarder
</button>
</div>
}
</div>
</li>
}
</ul>
}
</section>
</div>
<!-- Colonne droite : panneau référence (Dés / Personnages / Scènes) -->
<aside class="play-aside">
<app-session-reference-panel
[campaignId]="campaignId ?? ''"
[playthroughId]="session.playthroughId"
[sessionId]="session.id"
[canAddToJournal]="session.active"
(rolled)="onDiceRolled($event)"
(aiReplyToJournal)="onAiReplyToJournal($event)"
(noteToJournal)="onItemNoteToJournal($event)">
</app-session-reference-panel>
</aside>
</div>
<!-- Colonne droite : panneau référence (Dés / Personnages / Scènes) -->
<aside class="play-aside">
<app-session-reference-panel
[campaignId]="campaignId ?? ''"
[playthroughId]="session.playthroughId"
[sessionId]="session.id"
[canAddToJournal]="session.active"
(rolled)="onDiceRolled($event)"
(aiReplyToJournal)="onAiReplyToJournal($event)"
(noteToJournal)="onItemNoteToJournal($event)">
</app-session-reference-panel>
</aside>
</div>
</div>
}

View File

@@ -2,13 +2,15 @@
<div class="dice-controls">
<div class="face-grid">
<button *ngFor="let f of faces"
type="button"
class="face-chip"
[class.face-chip--active]="selectedFace === f"
(click)="selectedFace = f">
d{{ f }}
</button>
@for (f of faces; track f) {
<button
type="button"
class="face-chip"
[class.face-chip--active]="selectedFace === f"
(click)="selectedFace = f">
d{{ f }}
</button>
}
</div>
<div class="dice-inputs">
@@ -28,34 +30,41 @@
</button>
</div>
<div class="dice-history" *ngIf="history.length > 0">
<div class="history-header">
<span>Derniers jets</span>
<button type="button" class="btn-link" (click)="clearHistory()" title="Vider l'historique local">
<lucide-icon [img]="Trash2" [size]="12"></lucide-icon>
</button>
</div>
<ul class="history-list">
<li *ngFor="let r of history" class="history-item">
<div class="history-text">
<span class="history-notation">{{ r.notation }}</span>
<span class="history-detail" *ngIf="r.rolls.length > 1">[{{ r.rolls.join(', ') }}]</span>
<span class="history-total">= {{ r.total }}</span>
</div>
<button type="button"
class="btn-icon"
[disabled]="!canAddToJournal"
[title]="canAddToJournal ? 'Ajouter au journal' : 'Session terminée'"
(click)="addToJournal(r)">
<lucide-icon [img]="BookmarkPlus" [size]="12"></lucide-icon>
@if (history.length > 0) {
<div class="dice-history">
<div class="history-header">
<span>Derniers jets</span>
<button type="button" class="btn-link" (click)="clearHistory()" title="Vider l'historique local">
<lucide-icon [img]="Trash2" [size]="12"></lucide-icon>
</button>
</li>
</ul>
</div>
</div>
<ul class="history-list">
@for (r of history; track r) {
<li class="history-item">
<div class="history-text">
<span class="history-notation">{{ r.notation }}</span>
@if (r.rolls.length > 1) {
<span class="history-detail">[{{ r.rolls.join(', ') }}]</span>
}
<span class="history-total">= {{ r.total }}</span>
</div>
<button type="button"
class="btn-icon"
[disabled]="!canAddToJournal"
[title]="canAddToJournal ? 'Ajouter au journal' : 'Session terminée'"
(click)="addToJournal(r)">
<lucide-icon [img]="BookmarkPlus" [size]="12"></lucide-icon>
</button>
</li>
}
</ul>
</div>
}
<p class="placeholder-hint" *ngIf="history.length === 0">
Choisis un dé et lance.
</p>
@if (history.length === 0) {
<p class="placeholder-hint">
Choisis un dé et lance.
</p>
}
</div>

View File

@@ -1,5 +1,5 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { LucideAngularModule, Dices, BookmarkPlus, Trash2 } from 'lucide-angular';
@@ -26,7 +26,7 @@ export interface DiceRollResult {
*/
@Component({
selector: 'app-session-dice-panel',
imports: [CommonModule, FormsModule, LucideAngularModule],
imports: [FormsModule, LucideAngularModule],
templateUrl: './session-dice-panel.component.html',
styleUrls: ['./session-dice-panel.component.scss']
})

View File

@@ -1,38 +1,57 @@
<div class="sic-panel">
<p class="loading-hint" *ngIf="loading">Chargement…</p>
@if (loading) {
<p class="loading-hint">Chargement…</p>
}
<!-- Liste des catalogues -->
<div *ngIf="!loading && !selected" class="sic-list">
<p class="empty-hint" *ngIf="catalogs.length === 0">
Aucun catalogue d'objets dans cette campagne.
</p>
<button *ngFor="let c of catalogs" type="button" class="sic-item" (click)="select(c)">
<lucide-icon [img]="Package" [size]="13"></lucide-icon>
<span class="sic-item-name">{{ c.name }}</span>
<span class="sic-item-count">{{ c.items.length }}</span>
</button>
</div>
@if (!loading && !selected) {
<div class="sic-list">
@if (catalogs.length === 0) {
<p class="empty-hint">
Aucun catalogue d'objets dans cette campagne.
</p>
}
@for (c of catalogs; track c) {
<button type="button" class="sic-item" (click)="select(c)">
<lucide-icon [img]="Package" [size]="13"></lucide-icon>
<span class="sic-item-name">{{ c.name }}</span>
<span class="sic-item-count">{{ c.items.length }}</span>
</button>
}
</div>
}
<!-- Catalogue sélectionné -->
<div *ngIf="selected" class="sic-detail">
<button type="button" class="sic-back" (click)="backToList()">
<lucide-icon [img]="ChevronLeft" [size]="13"></lucide-icon> Catalogues
</button>
<h4>{{ selected.name }}</h4>
<div class="sic-group" *ngFor="let g of groups">
<div class="sic-group-title" *ngIf="g.category !== '—'">{{ g.category }}</div>
<div class="sic-obj" *ngFor="let it of g.items">
<div class="sic-obj-main">
<span class="sic-obj-name">{{ it.name }}</span>
<span class="sic-obj-price" *ngIf="it.price">{{ it.price }}</span>
</div>
<div class="sic-obj-desc" *ngIf="it.description">{{ it.description }}</div>
<button type="button" class="sic-note" (click)="note(it)" [disabled]="!canAddToJournal"
@if (selected) {
<div class="sic-detail">
<button type="button" class="sic-back" (click)="backToList()">
<lucide-icon [img]="ChevronLeft" [size]="13"></lucide-icon> Catalogues
</button>
<h4>{{ selected.name }}</h4>
@for (g of groups; track g) {
<div class="sic-group">
@if (g.category !== '—') {
<div class="sic-group-title">{{ g.category }}</div>
}
@for (it of g.items; track it) {
<div class="sic-obj">
<div class="sic-obj-main">
<span class="sic-obj-name">{{ it.name }}</span>
@if (it.price) {
<span class="sic-obj-price">{{ it.price }}</span>
}
</div>
@if (it.description) {
<div class="sic-obj-desc">{{ it.description }}</div>
}
<button type="button" class="sic-note" (click)="note(it)" [disabled]="!canAddToJournal"
title="Consigner au journal (ex. achat)">
<lucide-icon [img]="BookmarkPlus" [size]="12"></lucide-icon> Journal
</button>
</div>
<lucide-icon [img]="BookmarkPlus" [size]="12"></lucide-icon> Journal
</button>
</div>
}
</div>
}
</div>
</div>
}
</div>

View File

@@ -1,5 +1,5 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { CommonModule } from '@angular/common';
import { LucideAngularModule, Package, BookmarkPlus, ChevronLeft } from 'lucide-angular';
import { catchError, of } from 'rxjs';
import { ItemCatalogService } from '../../services/item-catalog.service';
@@ -13,7 +13,7 @@ interface ItemGroup { category: string; items: CatalogItem[]; }
*/
@Component({
selector: 'app-session-item-catalogs-panel',
imports: [CommonModule, LucideAngularModule],
imports: [LucideAngularModule],
templateUrl: './session-item-catalogs-panel.component.html',
styleUrls: ['./session-item-catalogs-panel.component.scss']
})

View File

@@ -1,51 +1,67 @@
<div class="srt-panel">
<p class="loading-hint" *ngIf="loading">Chargement…</p>
@if (loading) {
<p class="loading-hint">Chargement…</p>
}
<!-- Liste des tables -->
<div *ngIf="!loading && !selected" class="srt-list">
<p class="empty-hint" *ngIf="tables.length === 0">
Aucune table aléatoire dans cette campagne. Crée-en une depuis la sidebar.
</p>
<button *ngFor="let t of tables" type="button" class="srt-item" (click)="select(t)">
<lucide-icon [img]="Dices" [size]="13"></lucide-icon>
<span class="srt-item-name">{{ t.name }}</span>
<span class="srt-item-formula">{{ t.diceFormula }}</span>
</button>
</div>
@if (!loading && !selected) {
<div class="srt-list">
@if (tables.length === 0) {
<p class="empty-hint">
Aucune table aléatoire dans cette campagne. Crée-en une depuis la sidebar.
</p>
}
@for (t of tables; track t) {
<button type="button" class="srt-item" (click)="select(t)">
<lucide-icon [img]="Dices" [size]="13"></lucide-icon>
<span class="srt-item-name">{{ t.name }}</span>
<span class="srt-item-formula">{{ t.diceFormula }}</span>
</button>
}
</div>
}
<!-- Table sélectionnée : jet -->
<div *ngIf="selected" class="srt-detail">
<button type="button" class="srt-back" (click)="backToList()">
<lucide-icon [img]="ChevronLeft" [size]="13"></lucide-icon> Tables
</button>
<h4>{{ selected.name }}</h4>
<button type="button" class="srt-roll" (click)="roll()">
<lucide-icon [img]="Dices" [size]="15"></lucide-icon>
Lancer {{ selected.diceFormula }}
</button>
<div class="srt-result" *ngIf="lastRoll">
<div class="srt-total">
{{ lastRoll.total }}
<span class="srt-rolls" *ngIf="lastRoll.rolls.length > 1">({{ lastRoll.rolls.join(' + ') }})</span>
<strong *ngIf="matched">{{ matched.label }}</strong>
<em *ngIf="!matched">aucune entrée</em>
</div>
<div class="srt-detail-text" *ngIf="matched?.detail">{{ matched?.detail }}</div>
<div class="srt-actions">
<button type="button" class="srt-act" (click)="addToJournal()" [disabled]="!canAddToJournal">
<lucide-icon [img]="BookmarkPlus" [size]="13"></lucide-icon> Journal
</button>
<button type="button" class="srt-act srt-act--ai" (click)="improvise()"
[disabled]="!matched || improvising || !canAddToJournal">
<lucide-icon [img]="Sparkles" [size]="13"></lucide-icon>
{{ improvising ? '…' : 'IA improvise' }}
</button>
</div>
@if (selected) {
<div class="srt-detail">
<button type="button" class="srt-back" (click)="backToList()">
<lucide-icon [img]="ChevronLeft" [size]="13"></lucide-icon> Tables
</button>
<h4>{{ selected.name }}</h4>
<button type="button" class="srt-roll" (click)="roll()">
<lucide-icon [img]="Dices" [size]="15"></lucide-icon>
Lancer {{ selected.diceFormula }}
</button>
@if (lastRoll) {
<div class="srt-result">
<div class="srt-total">
{{ lastRoll.total }}
@if (lastRoll.rolls.length > 1) {
<span class="srt-rolls">({{ lastRoll.rolls.join(' + ') }})</span>
}
@if (matched) {
<strong>{{ matched.label }}</strong>
}
@if (!matched) {
<em>aucune entrée</em>
}
</div>
@if (matched?.detail) {
<div class="srt-detail-text">{{ matched?.detail }}</div>
}
<div class="srt-actions">
<button type="button" class="srt-act" (click)="addToJournal()" [disabled]="!canAddToJournal">
<lucide-icon [img]="BookmarkPlus" [size]="13"></lucide-icon> Journal
</button>
<button type="button" class="srt-act srt-act--ai" (click)="improvise()"
[disabled]="!matched || improvising || !canAddToJournal">
<lucide-icon [img]="Sparkles" [size]="13"></lucide-icon>
{{ improvising ? '…' : 'IA improvise' }}
</button>
</div>
</div>
}
</div>
</div>
}
</div>

View File

@@ -1,5 +1,5 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { CommonModule } from '@angular/common';
import { LucideAngularModule, Dices, BookmarkPlus, Sparkles, ChevronLeft } from 'lucide-angular';
import { catchError, of } from 'rxjs';
import { RandomTableService } from '../../services/random-table.service';
@@ -18,7 +18,7 @@ import { DiceRollResult } from '../session-dice-panel/session-dice-panel.compone
*/
@Component({
selector: 'app-session-random-tables-panel',
imports: [CommonModule, LucideAngularModule],
imports: [LucideAngularModule],
templateUrl: './session-random-tables-panel.component.html',
styleUrls: ['./session-random-tables-panel.component.scss']
})

View File

@@ -2,44 +2,44 @@
<nav class="ref-tabs">
<button type="button"
class="ref-tab"
[class.ref-tab--active]="activeTab === 'ai'"
(click)="selectTab('ai')">
class="ref-tab"
[class.ref-tab--active]="activeTab === 'ai'"
(click)="selectTab('ai')">
<lucide-icon [img]="Sparkles" [size]="14"></lucide-icon>
IA
</button>
<button type="button"
class="ref-tab"
[class.ref-tab--active]="activeTab === 'dice'"
(click)="selectTab('dice')">
class="ref-tab"
[class.ref-tab--active]="activeTab === 'dice'"
(click)="selectTab('dice')">
<lucide-icon [img]="Dices" [size]="14"></lucide-icon>
Dés
</button>
<button type="button"
class="ref-tab"
[class.ref-tab--active]="activeTab === 'tables'"
(click)="selectTab('tables')">
class="ref-tab"
[class.ref-tab--active]="activeTab === 'tables'"
(click)="selectTab('tables')">
<lucide-icon [img]="Table2" [size]="14"></lucide-icon>
Tables
</button>
<button type="button"
class="ref-tab"
[class.ref-tab--active]="activeTab === 'objects'"
(click)="selectTab('objects')">
class="ref-tab"
[class.ref-tab--active]="activeTab === 'objects'"
(click)="selectTab('objects')">
<lucide-icon [img]="Package" [size]="14"></lucide-icon>
Objets
</button>
<button type="button"
class="ref-tab"
[class.ref-tab--active]="activeTab === 'characters'"
(click)="selectTab('characters')">
class="ref-tab"
[class.ref-tab--active]="activeTab === 'characters'"
(click)="selectTab('characters')">
<lucide-icon [img]="User" [size]="14"></lucide-icon>
PJ/PNJ
</button>
<button type="button"
class="ref-tab"
[class.ref-tab--active]="activeTab === 'scenes'"
(click)="selectTab('scenes')">
class="ref-tab"
[class.ref-tab--active]="activeTab === 'scenes'"
(click)="selectTab('scenes')">
<lucide-icon [img]="Swords" [size]="14"></lucide-icon>
Scènes
</button>
@@ -48,104 +48,130 @@
<div class="ref-content" [class.ref-content--fill]="activeTab === 'ai'">
<!-- ====== IA ====== -->
<app-session-ai-chat-panel
*ngIf="activeTab === 'ai'"
[sessionId]="sessionId"
[canSaveToJournal]="canAddToJournal"
(saveToJournal)="onAiSaveToJournal($event)">
</app-session-ai-chat-panel>
@if (activeTab === 'ai') {
<app-session-ai-chat-panel
[sessionId]="sessionId"
[canSaveToJournal]="canAddToJournal"
(saveToJournal)="onAiSaveToJournal($event)">
</app-session-ai-chat-panel>
}
<!-- ====== Dés ====== -->
<app-session-dice-panel
*ngIf="activeTab === 'dice'"
[canAddToJournal]="canAddToJournal"
(rolled)="onDiceRolled($event)">
</app-session-dice-panel>
@if (activeTab === 'dice') {
<app-session-dice-panel
[canAddToJournal]="canAddToJournal"
(rolled)="onDiceRolled($event)">
</app-session-dice-panel>
}
<!-- ====== Tables aléatoires ====== -->
<app-session-random-tables-panel
*ngIf="activeTab === 'tables'"
[campaignId]="campaignId"
[canAddToJournal]="canAddToJournal"
(rolled)="onDiceRolled($event)"
(aiReplyToJournal)="onAiSaveToJournal($event)">
</app-session-random-tables-panel>
@if (activeTab === 'tables') {
<app-session-random-tables-panel
[campaignId]="campaignId"
[canAddToJournal]="canAddToJournal"
(rolled)="onDiceRolled($event)"
(aiReplyToJournal)="onAiSaveToJournal($event)">
</app-session-random-tables-panel>
}
<!-- ====== Catalogues d'objets ====== -->
<app-session-item-catalogs-panel
*ngIf="activeTab === 'objects'"
[campaignId]="campaignId"
[canAddToJournal]="canAddToJournal"
(noteToJournal)="onItemNote($event)">
</app-session-item-catalogs-panel>
@if (activeTab === 'objects') {
<app-session-item-catalogs-panel
[campaignId]="campaignId"
[canAddToJournal]="canAddToJournal"
(noteToJournal)="onItemNote($event)">
</app-session-item-catalogs-panel>
}
<!-- ====== Personnages (PJ + PNJ) ====== -->
<div *ngIf="activeTab === 'characters'" class="ref-list">
<p class="loading-hint" *ngIf="loadingChars">Chargement…</p>
<div *ngIf="!loadingChars">
<div class="ref-group" *ngIf="characters.length > 0">
<h4>
<lucide-icon [img]="User" [size]="13"></lucide-icon>
Personnages joueurs
</h4>
<button *ngFor="let c of characters"
type="button"
class="ref-item"
(click)="openInNewTab(['campaigns', campaignId, 'playthroughs', playthroughId!, 'characters', c.id!])">
<span class="ref-item-name">{{ c.name }}</span>
<lucide-icon [img]="ExternalLink" [size]="12" class="ref-item-icon"></lucide-icon>
</button>
</div>
<div class="ref-group" *ngIf="npcs.length > 0">
<h4>
<lucide-icon [img]="Drama" [size]="13"></lucide-icon>
Personnages non-joueurs
</h4>
<button *ngFor="let n of npcs"
type="button"
class="ref-item"
(click)="openInNewTab(['campaigns', campaignId, 'npcs', n.id!])">
<span class="ref-item-name">{{ n.name }}</span>
<lucide-icon [img]="ExternalLink" [size]="12" class="ref-item-icon"></lucide-icon>
</button>
</div>
<p class="empty-hint" *ngIf="characters.length === 0 && npcs.length === 0">
Aucun personnage dans cette campagne.
</p>
@if (activeTab === 'characters') {
<div class="ref-list">
@if (loadingChars) {
<p class="loading-hint">Chargement…</p>
}
@if (!loadingChars) {
<div>
@if (characters.length > 0) {
<div class="ref-group">
<h4>
<lucide-icon [img]="User" [size]="13"></lucide-icon>
Personnages joueurs
</h4>
@for (c of characters; track c) {
<button
type="button"
class="ref-item"
(click)="openInNewTab(['campaigns', campaignId, 'playthroughs', playthroughId!, 'characters', c.id!])">
<span class="ref-item-name">{{ c.name }}</span>
<lucide-icon [img]="ExternalLink" [size]="12" class="ref-item-icon"></lucide-icon>
</button>
}
</div>
}
@if (npcs.length > 0) {
<div class="ref-group">
<h4>
<lucide-icon [img]="Drama" [size]="13"></lucide-icon>
Personnages non-joueurs
</h4>
@for (n of npcs; track n) {
<button
type="button"
class="ref-item"
(click)="openInNewTab(['campaigns', campaignId, 'npcs', n.id!])">
<span class="ref-item-name">{{ n.name }}</span>
<lucide-icon [img]="ExternalLink" [size]="12" class="ref-item-icon"></lucide-icon>
</button>
}
</div>
}
@if (characters.length === 0 && npcs.length === 0) {
<p class="empty-hint">
Aucun personnage dans cette campagne.
</p>
}
</div>
}
</div>
</div>
}
<!-- ====== Scènes (arborescence aplatie) ====== -->
<div *ngIf="activeTab === 'scenes'" class="ref-list">
<p class="loading-hint" *ngIf="loadingTree">Chargement…</p>
<ng-container *ngIf="!loadingTree && treeData">
<p class="empty-hint" *ngIf="treeData.arcs.length === 0">
Aucun arc narratif. Construis le scénario de ta campagne pour le retrouver ici.
</p>
<div *ngFor="let arc of treeData.arcs" class="ref-group">
<h4>
<lucide-icon [img]="Swords" [size]="13"></lucide-icon>
{{ arc.name }}
</h4>
<div *ngFor="let chapter of chaptersOf(arc)" class="ref-subgroup">
<span class="ref-subgroup-title">{{ chapter.name }}</span>
<button *ngFor="let scene of scenesOf(chapter)"
type="button"
class="ref-item ref-item--nested"
(click)="openInNewTab(['campaigns', campaignId, 'arcs', arc.id!, 'chapters', chapter.id!, 'scenes', scene.id!])">
<span class="ref-item-name">{{ scene.name }}</span>
<lucide-icon [img]="ExternalLink" [size]="12" class="ref-item-icon"></lucide-icon>
</button>
</div>
</div>
</ng-container>
</div>
@if (activeTab === 'scenes') {
<div class="ref-list">
@if (loadingTree) {
<p class="loading-hint">Chargement…</p>
}
@if (!loadingTree && treeData) {
@if (treeData.arcs.length === 0) {
<p class="empty-hint">
Aucun arc narratif. Construis le scénario de ta campagne pour le retrouver ici.
</p>
}
@for (arc of treeData.arcs; track arc) {
<div class="ref-group">
<h4>
<lucide-icon [img]="Swords" [size]="13"></lucide-icon>
{{ arc.name }}
</h4>
@for (chapter of chaptersOf(arc); track chapter) {
<div class="ref-subgroup">
<span class="ref-subgroup-title">{{ chapter.name }}</span>
@for (scene of scenesOf(chapter); track scene) {
<button
type="button"
class="ref-item ref-item--nested"
(click)="openInNewTab(['campaigns', campaignId, 'arcs', arc.id!, 'chapters', chapter.id!, 'scenes', scene.id!])">
<span class="ref-item-name">{{ scene.name }}</span>
<lucide-icon [img]="ExternalLink" [size]="12" class="ref-item-icon"></lucide-icon>
</button>
}
</div>
}
</div>
}
}
</div>
}
</div>
</div>

View File

@@ -1,5 +1,5 @@
import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
import { CommonModule } from '@angular/common';
import { LucideAngularModule, User, Drama, Swords, Dices, ExternalLink, Sparkles, Table2, Package } from 'lucide-angular';
import { catchError, of } from 'rxjs';
import { CampaignService } from '../../services/campaign.service';
@@ -30,7 +30,7 @@ type TabId = 'dice' | 'tables' | 'objects' | 'characters' | 'scenes' | 'ai';
*/
@Component({
selector: 'app-session-reference-panel',
imports: [CommonModule, LucideAngularModule, SessionDicePanelComponent, SessionAiChatPanelComponent, SessionRandomTablesPanelComponent, SessionItemCatalogsPanelComponent],
imports: [LucideAngularModule, SessionDicePanelComponent, SessionAiChatPanelComponent, SessionRandomTablesPanelComponent, SessionItemCatalogsPanelComponent],
templateUrl: './session-reference-panel.component.html',
styleUrls: ['./session-reference-panel.component.scss']
})