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:
@@ -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 ? 'L’IA 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>
|
||||
|
||||
|
||||
@@ -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']
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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']
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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']
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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']
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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']
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user