Ajout de tests playwright et correction de tests non passant (pour les tests ajoutés : partie game system ).
Correction de plusieurs anomalies : problème de switch entre 2 templates (par exemple si on était sur un template 1 et qu'on voulait passer directement au 2, ce dernier ne chargeait pas) ; correction du soucis d'apparition de la sidebar à gauche qui disparaissait sans explication ; problème de redirection : lorsqu'on terminait de créer un PJ / PNJ ; on arrivait sur l'accueil de la campagne au lieu de voir le résultat de la création. Problème de redirection également lors du clique sur un PNJ / PJ sur le coté : on arrivait sur l'édition au lieu de la présentation. Correction de la première lettre stylisée : tout est au même style comme ça plus de probleme de lecture. Nouveautées : stylisation des modales (notamment suppression, warning.....) avec en prime l'ajout d'un warning lors du changement de système pour avertir que les fiches persos ne sont pas conservées. Ajout d'une option pour créer un game system directement à la création d'une campagne afin de faciliter la mise en place de cette dernière. Ajout d'un bouton pour créer un nouveau template directement lorsqu'on créer une page : ça permet de créer un template et de revenir sur la page qu'on était en train de créer sans perdre le titre. Passage en bêta 0.8.4
This commit is contained in:
@@ -7,6 +7,7 @@ import { AiChatService, ChatMessage, ChatUsage, NarrativeEntityType } from '../.
|
||||
import { Conversation, ConversationContext } from '../../services/conversation.model';
|
||||
import { ConversationService } from '../../services/conversation.service';
|
||||
import { MarkdownPipe } from '../markdown.pipe';
|
||||
import { ConfirmDialogService } from '../confirm-dialog/confirm-dialog.service';
|
||||
|
||||
/**
|
||||
* Action primaire optionnelle rendue en gros bouton au-dessus des suggestions.
|
||||
@@ -119,6 +120,7 @@ export class AiChatDrawerComponent implements OnInit, OnChanges, OnDestroy {
|
||||
constructor(
|
||||
private readonly chatService: AiChatService,
|
||||
private readonly conversationService: ConversationService,
|
||||
private readonly confirmDialog: ConfirmDialogService,
|
||||
) {}
|
||||
|
||||
// --- Jauge de contexte --------------------------------------------------
|
||||
@@ -312,12 +314,19 @@ export class AiChatDrawerComponent implements OnInit, OnChanges, OnDestroy {
|
||||
deleteConversation(conv: Conversation, event: Event): void {
|
||||
event.stopPropagation();
|
||||
if (this.isStreaming) return;
|
||||
if (!confirm(`Supprimer la conversation "${conv.title}" ?`)) return;
|
||||
this.conversationService.delete(conv.id).subscribe({
|
||||
next: () => {
|
||||
this.conversations = this.conversations.filter((c) => c.id !== conv.id);
|
||||
if (this.currentConversationId === conv.id) this.resetConversationState();
|
||||
},
|
||||
this.confirmDialog.confirm({
|
||||
title: 'Supprimer la conversation',
|
||||
message: `Supprimer la conversation "${conv.title}" ?`,
|
||||
confirmLabel: 'Supprimer',
|
||||
variant: 'danger'
|
||||
}).then(ok => {
|
||||
if (!ok) return;
|
||||
this.conversationService.delete(conv.id).subscribe({
|
||||
next: () => {
|
||||
this.conversations = this.conversations.filter((c) => c.id !== conv.id);
|
||||
if (this.currentConversationId === conv.id) this.resetConversationState();
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ConfirmDialogComponent } from './confirm-dialog.component';
|
||||
import { ConfirmDialogService } from './confirm-dialog.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-confirm-dialog-host',
|
||||
standalone: true,
|
||||
imports: [CommonModule, ConfirmDialogComponent],
|
||||
template: `
|
||||
<app-confirm-dialog
|
||||
*ngIf="(svc.state$ | async) as s"
|
||||
[open]="s.open"
|
||||
[title]="s.title"
|
||||
[message]="s.message"
|
||||
[details]="s.details"
|
||||
[confirmLabel]="s.confirmLabel"
|
||||
[cancelLabel]="s.cancelLabel"
|
||||
[variant]="s.variant"
|
||||
(confirmed)="svc.resolve(true)"
|
||||
(cancelled)="svc.resolve(false)">
|
||||
</app-confirm-dialog>
|
||||
`
|
||||
})
|
||||
export class ConfirmDialogHostComponent {
|
||||
constructor(public svc: ConfirmDialogService) {}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<div class="confirm-backdrop" *ngIf="open" (click)="onCancel()">
|
||||
<div class="confirm-modal"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
[attr.aria-label]="title"
|
||||
[class.variant-warning]="variant === 'warning'"
|
||||
[class.variant-danger]="variant === 'danger'"
|
||||
[class.variant-info]="variant === 'info'"
|
||||
(click)="$event.stopPropagation()">
|
||||
|
||||
<div class="confirm-header">
|
||||
<div class="confirm-icon">
|
||||
<lucide-icon [img]="TriangleAlert" [size]="22"></lucide-icon>
|
||||
</div>
|
||||
<h2>{{ title }}</h2>
|
||||
<button type="button" class="btn-close" (click)="onCancel()" aria-label="Fermer">
|
||||
<lucide-icon [img]="X" [size]="18"></lucide-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="confirm-body">
|
||||
<p class="confirm-message">{{ message }}</p>
|
||||
<ul *ngIf="details.length > 0" class="confirm-details">
|
||||
<li *ngFor="let line of details">{{ line }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="confirm-actions">
|
||||
<button type="button" class="btn-secondary" (click)="onCancel()">{{ cancelLabel }}</button>
|
||||
<button type="button" class="btn-confirm" (click)="onConfirm()">{{ confirmLabel }}</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
127
web/src/app/shared/confirm-dialog/confirm-dialog.component.scss
Normal file
127
web/src/app/shared/confirm-dialog/confirm-dialog.component.scss
Normal file
@@ -0,0 +1,127 @@
|
||||
.confirm-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.75);
|
||||
backdrop-filter: blur(2px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 200;
|
||||
}
|
||||
|
||||
.confirm-modal {
|
||||
background: #111827;
|
||||
border: 1px solid #1f2937;
|
||||
border-radius: 16px;
|
||||
width: 100%;
|
||||
max-width: 520px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
|
||||
overflow: hidden;
|
||||
|
||||
&.variant-warning { border-top: 4px solid #eab308; }
|
||||
&.variant-danger { border-top: 4px solid #ef4444; }
|
||||
&.variant-info { border-top: 4px solid #6c63ff; }
|
||||
}
|
||||
|
||||
.confirm-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 1.25rem 1.5rem;
|
||||
border-bottom: 1px solid #1f2937;
|
||||
|
||||
h2 {
|
||||
flex: 1;
|
||||
color: white;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.confirm-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
|
||||
.variant-warning & { background: rgba(234, 179, 8, 0.15); color: #eab308; }
|
||||
.variant-danger & { background: rgba(239, 68, 68, 0.15); color: #ef4444; }
|
||||
.variant-info & { background: rgba(108, 99, 255, 0.15); color: #6c63ff; }
|
||||
}
|
||||
|
||||
.btn-close {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #6b7280;
|
||||
cursor: pointer;
|
||||
padding: 0.25rem;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
transition: color 0.2s;
|
||||
|
||||
&:hover { color: white; }
|
||||
}
|
||||
|
||||
.confirm-body {
|
||||
padding: 1.25rem 1.5rem;
|
||||
color: #d1d5db;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.confirm-message {
|
||||
margin: 0;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.confirm-details {
|
||||
margin: 0.875rem 0 0 1.25rem;
|
||||
padding: 0;
|
||||
color: #9ca3af;
|
||||
font-size: 0.85rem;
|
||||
|
||||
li { margin-bottom: 0.25rem; }
|
||||
}
|
||||
|
||||
.confirm-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.75rem;
|
||||
padding: 1rem 1.5rem;
|
||||
background: #0d121d;
|
||||
border-top: 1px solid #1f2937;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
padding: 0.6rem 1.25rem;
|
||||
background: #1f2937;
|
||||
color: #d1d5db;
|
||||
border: 1px solid #374151;
|
||||
border-radius: 8px;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
|
||||
&:hover { background: #374151; }
|
||||
}
|
||||
|
||||
.btn-confirm {
|
||||
padding: 0.6rem 1.25rem;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
|
||||
.variant-warning & { background: #eab308; color: #1f1300; &:hover { background: #d4a106; } }
|
||||
.variant-danger & { background: #ef4444; &:hover { background: #dc2626; } }
|
||||
.variant-info & { background: #6c63ff; &:hover { background: #5b52e0; } }
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { LucideAngularModule, TriangleAlert, X } from 'lucide-angular';
|
||||
|
||||
export type ConfirmDialogVariant = 'warning' | 'danger' | 'info';
|
||||
|
||||
@Component({
|
||||
selector: 'app-confirm-dialog',
|
||||
standalone: true,
|
||||
imports: [CommonModule, LucideAngularModule],
|
||||
templateUrl: './confirm-dialog.component.html',
|
||||
styleUrls: ['./confirm-dialog.component.scss']
|
||||
})
|
||||
export class ConfirmDialogComponent {
|
||||
readonly TriangleAlert = TriangleAlert;
|
||||
readonly X = X;
|
||||
|
||||
@Input() open = false;
|
||||
@Input() title = 'Confirmation';
|
||||
@Input() message = '';
|
||||
@Input() details: string[] = [];
|
||||
@Input() confirmLabel = 'Confirmer';
|
||||
@Input() cancelLabel = 'Annuler';
|
||||
@Input() variant: ConfirmDialogVariant = 'warning';
|
||||
|
||||
@Output() confirmed = new EventEmitter<void>();
|
||||
@Output() cancelled = new EventEmitter<void>();
|
||||
|
||||
onConfirm(): void { this.confirmed.emit(); }
|
||||
onCancel(): void { this.cancelled.emit(); }
|
||||
}
|
||||
59
web/src/app/shared/confirm-dialog/confirm-dialog.service.ts
Normal file
59
web/src/app/shared/confirm-dialog/confirm-dialog.service.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { ConfirmDialogVariant } from './confirm-dialog.component';
|
||||
|
||||
export interface ConfirmDialogOptions {
|
||||
title?: string;
|
||||
message: string;
|
||||
details?: string[];
|
||||
confirmLabel?: string;
|
||||
cancelLabel?: string;
|
||||
variant?: ConfirmDialogVariant;
|
||||
}
|
||||
|
||||
export interface ConfirmDialogState extends Required<Omit<ConfirmDialogOptions, 'details'>> {
|
||||
details: string[];
|
||||
open: boolean;
|
||||
}
|
||||
|
||||
const CLOSED_STATE: ConfirmDialogState = {
|
||||
open: false,
|
||||
title: 'Confirmation',
|
||||
message: '',
|
||||
details: [],
|
||||
confirmLabel: 'Confirmer',
|
||||
cancelLabel: 'Annuler',
|
||||
variant: 'warning'
|
||||
};
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class ConfirmDialogService {
|
||||
readonly state$ = new BehaviorSubject<ConfirmDialogState>(CLOSED_STATE);
|
||||
private resolver: ((value: boolean) => void) | null = null;
|
||||
|
||||
confirm(opts: ConfirmDialogOptions): Promise<boolean> {
|
||||
// Si un dialog precedent est encore ouvert, on le resout en "false"
|
||||
// avant d'en ouvrir un nouveau pour eviter une fuite de Promise.
|
||||
if (this.resolver) {
|
||||
this.resolver(false);
|
||||
this.resolver = null;
|
||||
}
|
||||
this.state$.next({
|
||||
open: true,
|
||||
title: opts.title ?? 'Confirmation',
|
||||
message: opts.message,
|
||||
details: opts.details ?? [],
|
||||
confirmLabel: opts.confirmLabel ?? 'Confirmer',
|
||||
cancelLabel: opts.cancelLabel ?? 'Annuler',
|
||||
variant: opts.variant ?? 'warning'
|
||||
});
|
||||
return new Promise<boolean>((resolve) => { this.resolver = resolve; });
|
||||
}
|
||||
|
||||
resolve(value: boolean): void {
|
||||
const r = this.resolver;
|
||||
this.resolver = null;
|
||||
this.state$.next(CLOSED_STATE);
|
||||
if (r) r(value);
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,7 @@
|
||||
<section *ngIf="s.kind === 'TEXT'" class="pv-section">
|
||||
<h2 class="pv-section-title">{{ s.name }}</h2>
|
||||
<div class="pv-section-body">
|
||||
<p [class.with-dropcap]="s.name === firstTextSectionName" class="pv-paragraph">
|
||||
<p class="pv-paragraph">
|
||||
{{ firstParagraph(s.value) }}
|
||||
</p>
|
||||
<p *ngIf="restAfterFirstParagraph(s.value)" class="pv-paragraph">
|
||||
|
||||
@@ -290,17 +290,6 @@
|
||||
.pv-paragraph {
|
||||
margin: 0 0 14px;
|
||||
white-space: pre-wrap;
|
||||
|
||||
&.with-dropcap::first-letter {
|
||||
float: left;
|
||||
font-family: 'Cinzel', 'EB Garamond', Georgia, serif;
|
||||
font-size: 3.5rem;
|
||||
line-height: 0.9;
|
||||
font-weight: 700;
|
||||
color: #d1a878;
|
||||
padding: 4px 8px 0 0;
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Etat vide --------------------------------------------------------------
|
||||
|
||||
@@ -111,15 +111,7 @@ export class PersonaViewComponent {
|
||||
return this.rendered().sections;
|
||||
}
|
||||
|
||||
/** Pour la drop cap : seul le 1er TEXT la recoit. */
|
||||
get firstTextSectionName(): string | null {
|
||||
for (const s of this.orderedSections) {
|
||||
if (s.kind === 'TEXT') return s.name;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Premier paragraphe d'un texte (utilise pour la drop cap). */
|
||||
/** Premier paragraphe d'un texte (separe pour permettre un styling specifique). */
|
||||
firstParagraph(text: string): string {
|
||||
if (!text) return '';
|
||||
const paragraphs = text.split(/\n\s*\n/);
|
||||
|
||||
Reference in New Issue
Block a user