Co-MJ v1 : quêtes de première classe, guidage de préparation, assistant IA,
All checks were successful
All checks were successful
mode séance, battlemaps multiples et export Foundry ciblé
- Quêtes (Niveau 1) : entité Quest orthogonale à l'arbre, rattachée à un arc HUB
ou libre (migrations V9-V12, V18, V20 ; réconciliation des jumeaux V10).
Fusion quête/conteneur dans la sidebar, progression par partie (statuts
disponible/en cours/terminée), et quêtes libres avec espace de scènes créé
automatiquement (arc technique « Quêtes libres », V21 : levée de la
contrainte arcs.type héritée du baseline).
- Guidage (Pilier B) : bilan de préparation 100 % dérivé (règles arc/chapitre/
scène/quête), panneau « Prochaines étapes » avec boutons « Corriger », et
pastilles détaillées (tooltip des manques) dans l'arbre.
- Assistant IA (Pilier A) : étoffage champ par champ (scène, chapitre, arc) et
brouillons de scènes en propose→applique (brain : narrative-fields,
scene-drafts).
- Horloges & menaces (V15-V17) : clocks à segments avec déclencheurs, fronts.
- Mode séance : préparation de séance (readiness + quêtes dispo), scène
épinglée (V19), récap « précédemment » (brain : session-recap), onglet
« Partie » du panneau de référence, graphe amélioré (pan/zoom, éditeur de
liens).
- Perf : endpoint agrégé GET /api/campaigns/{id}/tree — la sidebar charge en
1 requête au lieu de ~15.
- Battlemaps multiples par scène (variantes jour/nuit, étages…) : liste JSON
étiquetée (V22, reprise automatique de la carte existante), rendu PDF avec
légendes, export/import rétro-compatible.
- Export Foundry ciblé : modale de périmètre (cartes+ennemis / journaux /
tables), bundle filtré côté serveur (zip allégé), module Foundry à jour
(respect du périmètre + une Scene Foundry par variante de carte,
rétro-compatible anciens bundles).
This commit is contained in:
@@ -54,6 +54,10 @@
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<!-- ============ Guidage co-MJ : « Prochaines étapes » (Pilier B) ============ -->
|
||||
@if (!editing && campaign.id) {
|
||||
<app-readiness-panel [campaignId]="campaign.id" [assessment]="readiness"></app-readiness-panel>
|
||||
}
|
||||
<!-- ============ Header : mode édition inline ============ -->
|
||||
@if (editing) {
|
||||
<div class="detail-header edit-mode">
|
||||
@@ -187,13 +191,19 @@
|
||||
<div class="section-header">
|
||||
<h2>{{ 'campaignDetail.arcsTitle' | translate }}</h2>
|
||||
<div class="section-header-actions">
|
||||
@if (showQuickAddScene) {
|
||||
<button class="btn-add" (click)="quickAddScene()" [disabled]="quickSceneInFlight">
|
||||
<lucide-icon [img]="Plus" [size]="14"></lucide-icon>
|
||||
{{ 'campaignDetail.quickScene' | translate }}
|
||||
</button>
|
||||
}
|
||||
<button class="btn-add" (click)="createArc()">
|
||||
<lucide-icon [img]="Plus" [size]="14"></lucide-icon>
|
||||
{{ 'campaignDetail.newArc' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@if (arcs.length > 0) {
|
||||
@if (arcs.length > 0 && !showQuickAddScene) {
|
||||
<div class="arcs-grid" cdkDropList cdkDropListOrientation="mixed"
|
||||
(cdkDropListDropped)="dropArc($event)">
|
||||
@for (arc of arcs; track arc.id) {
|
||||
@@ -205,13 +215,23 @@
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<!-- Mode plat (un seul arc d'un seul chapitre) : on masque la carte de
|
||||
l'arc par défaut, comme dans la sidebar. Les scènes vivent dans le volet. -->
|
||||
@if (arcs.length > 0 && showQuickAddScene) {
|
||||
<div class="empty-state empty-state--compact">
|
||||
<p>{{ 'campaignDetail.flatModeHint' | translate }}</p>
|
||||
</div>
|
||||
}
|
||||
@if (arcs.length === 0) {
|
||||
<div class="empty-state">
|
||||
<lucide-icon [img]="Swords" [size]="40" class="empty-icon"></lucide-icon>
|
||||
<p>{{ 'campaignDetail.noArc' | translate }}</p>
|
||||
<button class="btn-add-first" (click)="createArc()">
|
||||
<button class="btn-add-first" (click)="quickAddScene()" [disabled]="quickSceneInFlight">
|
||||
<lucide-icon [img]="Plus" [size]="14"></lucide-icon>
|
||||
{{ 'campaignDetail.createFirstArc' | translate }}
|
||||
{{ 'campaignDetail.quickSceneFirst' | translate }}
|
||||
</button>
|
||||
<button type="button" class="empty-alt-action" (click)="createArc()">
|
||||
{{ 'campaignDetail.organizeInArcs' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
@@ -277,3 +297,10 @@
|
||||
(chapter-edit > « Conditions de déblocage »). -->
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Choix du périmètre de l'export Foundry (tout / cartes+ennemis / journaux / tables). -->
|
||||
<app-foundry-export-dialog
|
||||
[open]="foundryDialogOpen"
|
||||
[exporting]="exportingFoundry"
|
||||
(cancelled)="foundryDialogOpen = false"
|
||||
(confirmed)="onFoundryExportConfirmed($event)"></app-foundry-export-dialog>
|
||||
|
||||
@@ -486,6 +486,19 @@
|
||||
&:hover { background: #5b52e0; }
|
||||
}
|
||||
|
||||
// Action secondaire de l'état vide (« ou créer un arc ») — bouton texte discret.
|
||||
.empty-alt-action {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
color: #8a7bc8;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover { text-decoration: underline; }
|
||||
}
|
||||
|
||||
// ─────────────── Sessions de jeu (Play Context) ───────────────
|
||||
.sessions-grid {
|
||||
display: grid;
|
||||
|
||||
@@ -9,9 +9,9 @@ import { CdkDropList, CdkDrag, CdkDragDrop, moveItemInArray } from '@angular/cdk
|
||||
import { LucideAngularModule, Swords, Plus, Globe, Pencil, Trash2, Dices, Drama, Check, Play, Upload, Sparkles, Download, FileText, ChevronDown, ChevronRight, X } from 'lucide-angular';
|
||||
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
|
||||
import { Router, RouterLink } from '@angular/router';
|
||||
import { forkJoin, of } from 'rxjs';
|
||||
import { forkJoin, of, Observable } from 'rxjs';
|
||||
import { catchError, switchMap, filter, map } from 'rxjs/operators';
|
||||
import { CampaignService } from '../../../services/campaign.service';
|
||||
import { CampaignService, FoundryExportOptions } from '../../../services/campaign.service';
|
||||
import { LoreService } from '../../../services/lore.service';
|
||||
import { GameSystemService } from '../../../services/game-system.service';
|
||||
import { GameSystem } from '../../../services/game-system.model';
|
||||
@@ -30,10 +30,13 @@ import { Lore } from '../../../services/lore.model';
|
||||
import { loadCampaignTreeData, buildCampaignSidebarConfig, CampaignTreeData } from '../../campaign-tree.helper';
|
||||
import { ConfirmDialogService } from '../../../shared/confirm-dialog/confirm-dialog.service';
|
||||
import { FolderGroup, groupByFolder, byOrder } from '../../../shared/folder-grouping.util';
|
||||
import { CampaignReadinessAssessment } from '../../../services/readiness.model';
|
||||
import { ReadinessPanelComponent } from '../../../shared/readiness-panel/readiness-panel.component';
|
||||
import { FoundryExportDialogComponent } from '../foundry-export-dialog/foundry-export-dialog.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-campaign-detail',
|
||||
imports: [FormsModule, LucideAngularModule, RouterLink, TranslatePipe, CdkDropList, CdkDrag],
|
||||
imports: [FormsModule, LucideAngularModule, RouterLink, TranslatePipe, CdkDropList, CdkDrag, ReadinessPanelComponent, FoundryExportDialogComponent],
|
||||
templateUrl: './campaign-detail.component.html',
|
||||
styleUrls: ['./campaign-detail.component.scss']
|
||||
})
|
||||
@@ -57,6 +60,8 @@ export class CampaignDetailComponent implements OnInit, OnDestroy {
|
||||
|
||||
/** Export Foundry en cours (anti double-clic). */
|
||||
exportingFoundry = false;
|
||||
/** Modale de périmètre de l'export Foundry. */
|
||||
foundryDialogOpen = false;
|
||||
/** Export PDF en cours (anti double-clic). */
|
||||
exportingPdf = false;
|
||||
|
||||
@@ -100,6 +105,9 @@ export class CampaignDetailComponent implements OnInit, OnDestroy {
|
||||
/** Parties (Playthroughs) de cette campagne. */
|
||||
playthroughs: Playthrough[] = [];
|
||||
|
||||
/** Bilan de préparation (Pilier B — guidage) : alimente le panneau + les pastilles. */
|
||||
readiness: CampaignReadinessAssessment | null = null;
|
||||
|
||||
/** Mode édition inline. */
|
||||
editing = false;
|
||||
editName = '';
|
||||
@@ -139,7 +147,8 @@ export class CampaignDetailComponent implements OnInit, OnDestroy {
|
||||
// on recharge les cartes pour rester cohérent sans rafraîchir la page.
|
||||
this.dataSync.onChange(this.destroyRef, () => {
|
||||
if (this.campaign?.id) {
|
||||
this.campaignService.getArcs(this.campaign.id).subscribe(a => this.arcs = [...a].sort(byOrder));
|
||||
this.campaignService.getArcs(this.campaign.id).subscribe(a =>
|
||||
this.arcs = [...a].sort(byOrder).filter(x => x.type !== 'SYSTEM'));
|
||||
this.loadNpcs(this.campaign.id); // PNJ regroupés/ordonnés
|
||||
this.campaignSidebar.show(this.campaign.id); // recharge l'arbre aussi
|
||||
}
|
||||
@@ -193,11 +202,14 @@ export class CampaignDetailComponent implements OnInit, OnDestroy {
|
||||
this.campaign = campaign;
|
||||
this.editing = false;
|
||||
this.playthroughs = playthroughs;
|
||||
// Le bilan complet arrive avec l'arbre agrégé — plus de fetch séparé.
|
||||
this.readiness = treeData.readiness ?? null;
|
||||
this.loadLinkedLore(campaign);
|
||||
this.loadLinkedGameSystem(campaign);
|
||||
this.loadNpcs(campaign.id!);
|
||||
this.loadSessions(campaign.id!);
|
||||
this.arcs = [...treeData.arcs].sort(byOrder);
|
||||
// L'arc SYSTEM (« Quêtes libres ») est de la plomberie : pas de carte d'arc pour lui.
|
||||
this.arcs = [...treeData.arcs].sort(byOrder).filter(a => a.type !== 'SYSTEM');
|
||||
this.chapterCountByArc = this.computeChapterCounts(treeData);
|
||||
this.showLayout(allCampaigns, treeData);
|
||||
this.pageTitleService.set(campaign.name);
|
||||
@@ -315,11 +327,71 @@ export class CampaignDetailComponent implements OnInit, OnDestroy {
|
||||
this.router.navigate(['/campaigns', this.campaign.id, 'arcs', 'create']);
|
||||
}
|
||||
|
||||
/** Télécharge le bundle Foundry de la campagne via un lien temporaire. */
|
||||
// ─────────────── Ajout rapide d'une scène (conteneur par défaut) ───────────────
|
||||
// Niveau 0 : on ne force plus à créer un arc puis un chapitre avant de poser une
|
||||
// scène. Si la campagne n'a pas encore de structure, on provisionne un
|
||||
// « Arc principal / Chapitre 1 » implicite (masqué dans l'arbre tant qu'il est
|
||||
// seul), puis on ouvre la création de scène. Visible seulement quand la
|
||||
// structure est simple (vide, ou un seul arc d'au plus un chapitre).
|
||||
quickSceneInFlight = false;
|
||||
|
||||
get showQuickAddScene(): boolean {
|
||||
if (this.arcs.length === 0) return true;
|
||||
if (this.arcs.length === 1) return (this.chapterCountByArc[this.arcs[0].id!] ?? 0) <= 1;
|
||||
return false;
|
||||
}
|
||||
|
||||
quickAddScene(): void {
|
||||
if (!this.campaign || this.quickSceneInFlight) return;
|
||||
this.quickSceneInFlight = true;
|
||||
const campaignId = this.campaign.id!;
|
||||
this.ensureDefaultChapter(campaignId).subscribe({
|
||||
next: ({ arcId, chapterId }) => {
|
||||
this.quickSceneInFlight = false;
|
||||
this.router.navigate(['/campaigns', campaignId, 'arcs', arcId, 'chapters', chapterId, 'scenes', 'create']);
|
||||
},
|
||||
error: () => {
|
||||
this.quickSceneInFlight = false;
|
||||
console.error('Erreur lors de la préparation de la scène rapide');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Garantit l'existence d'un chapitre cible pour une scène : réutilise le premier
|
||||
* arc / chapitre s'ils existent, sinon crée « Arc principal » puis « Chapitre 1 ».
|
||||
*/
|
||||
private ensureDefaultChapter(campaignId: string): Observable<{ arcId: string; chapterId: string }> {
|
||||
const newChapter = (arcId: string) => this.campaignService.createChapter({
|
||||
name: this.translate.instant('campaignDetail.defaultChapterName'),
|
||||
description: '', arcId, order: 1, icon: null
|
||||
}).pipe(map(ch => ({ arcId, chapterId: ch.id! })));
|
||||
|
||||
const firstArc = this.arcs[0];
|
||||
if (!firstArc) {
|
||||
return this.campaignService.createArc({
|
||||
name: this.translate.instant('campaignDetail.defaultArcName'),
|
||||
description: '', campaignId, order: 1, type: 'LINEAR', icon: null
|
||||
}).pipe(switchMap(arc => newChapter(arc.id!)));
|
||||
}
|
||||
return this.campaignService.getChapters(firstArc.id!).pipe(
|
||||
switchMap(chs => chs.length
|
||||
? of({ arcId: firstArc.id!, chapterId: chs[0].id! })
|
||||
: newChapter(firstArc.id!))
|
||||
);
|
||||
}
|
||||
|
||||
/** Ouvre la modale de choix du périmètre de l'export Foundry. */
|
||||
exportFoundry(): void {
|
||||
if (!this.campaign?.id || this.exportingFoundry) return;
|
||||
this.foundryDialogOpen = true;
|
||||
}
|
||||
|
||||
/** Télécharge le bundle Foundry (périmètre choisi) via un lien temporaire. */
|
||||
onFoundryExportConfirmed(opts: FoundryExportOptions): void {
|
||||
if (!this.campaign?.id || this.exportingFoundry) return;
|
||||
this.exportingFoundry = true;
|
||||
this.campaignService.exportFoundry(this.campaign.id).subscribe({
|
||||
this.campaignService.exportFoundry(this.campaign.id, opts).subscribe({
|
||||
next: (blob) => {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
@@ -333,11 +405,13 @@ export class CampaignDetailComponent implements OnInit, OnDestroy {
|
||||
// Révocation différée : libère l'URL sans annuler le téléchargement en cours.
|
||||
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
||||
this.exportingFoundry = false;
|
||||
this.foundryDialogOpen = false;
|
||||
},
|
||||
error: (err) => {
|
||||
// Ne pas avaler l'erreur en silence : visible en console pour diagnostic.
|
||||
console.error('Échec de l\'export Foundry', err);
|
||||
this.exportingFoundry = false;
|
||||
this.foundryDialogOpen = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -407,7 +481,8 @@ export class CampaignDetailComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
private showLayout(allCampaigns: Campaign[], data: CampaignTreeData): void {
|
||||
this.layoutService.show(buildCampaignSidebarConfig(this.campaign!, allCampaigns, data, this.campaign!.id!, this.translate));
|
||||
this.layoutService.show(buildCampaignSidebarConfig(
|
||||
this.campaign!, allCampaigns, data, this.campaign!.id!, this.translate, this.readiness?.gaps ?? []));
|
||||
}
|
||||
|
||||
// ─────────────── Édition / suppression de la Campagne ───────────────
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
@if (open) {
|
||||
<div class="confirm-backdrop" (click)="onCancel()">
|
||||
<div class="confirm-modal variant-info"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
[attr.aria-label]="'foundryDialog.title' | translate"
|
||||
(click)="$event.stopPropagation()">
|
||||
<div class="confirm-header">
|
||||
<div class="confirm-icon">
|
||||
<lucide-icon [img]="Download" [size]="20"></lucide-icon>
|
||||
</div>
|
||||
<h2>{{ 'foundryDialog.title' | translate }}</h2>
|
||||
<button type="button" class="btn-close" (click)="onCancel()" [attr.aria-label]="'common.close' | translate">
|
||||
<lucide-icon [img]="X" [size]="18"></lucide-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="confirm-body">
|
||||
<p class="confirm-message">{{ 'foundryDialog.intro' | translate }}</p>
|
||||
|
||||
<div class="fx-presets">
|
||||
<button type="button" (click)="presetAll()">{{ 'foundryDialog.presetAll' | translate }}</button>
|
||||
<button type="button" (click)="presetMapsOnly()">{{ 'foundryDialog.presetMapsOnly' | translate }}</button>
|
||||
</div>
|
||||
|
||||
<label class="fx-option">
|
||||
<input type="checkbox" [checked]="maps" (change)="maps = !maps" />
|
||||
<span>
|
||||
<strong>{{ 'foundryDialog.optMaps' | translate }}</strong>
|
||||
<small>{{ 'foundryDialog.optMapsHint' | translate }}</small>
|
||||
</span>
|
||||
</label>
|
||||
<label class="fx-option">
|
||||
<input type="checkbox" [checked]="journals" (change)="journals = !journals" />
|
||||
<span>
|
||||
<strong>{{ 'foundryDialog.optJournals' | translate }}</strong>
|
||||
<small>{{ 'foundryDialog.optJournalsHint' | translate }}</small>
|
||||
</span>
|
||||
</label>
|
||||
<label class="fx-option">
|
||||
<input type="checkbox" [checked]="tables" (change)="tables = !tables" />
|
||||
<span>
|
||||
<strong>{{ 'foundryDialog.optTables' | translate }}</strong>
|
||||
<small>{{ 'foundryDialog.optTablesHint' | translate }}</small>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="confirm-actions">
|
||||
<button type="button" class="btn-secondary" (click)="onCancel()" [disabled]="exporting">
|
||||
{{ 'common.cancel' | translate }}
|
||||
</button>
|
||||
<button type="button" class="btn-confirm" (click)="onConfirm()" [disabled]="nothingSelected || exporting">
|
||||
{{ (exporting ? 'campaignDetail.foundryExporting' : 'foundryDialog.confirm') | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
// Même squelette visuel que confirm-dialog (backdrop/modal/header/actions),
|
||||
// avec en plus les cases à cocher du périmètre et les raccourcis.
|
||||
.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-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;
|
||||
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 0 0.75rem; }
|
||||
|
||||
.fx-presets {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.9rem;
|
||||
|
||||
button {
|
||||
background: #1a1a2e;
|
||||
border: 1px solid #2a2a3d;
|
||||
border-radius: 999px;
|
||||
color: #9ca3af;
|
||||
padding: 0.3rem 0.8rem;
|
||||
font-size: 0.8rem;
|
||||
cursor: pointer;
|
||||
transition: color 0.15s, border-color 0.15s;
|
||||
|
||||
&:hover { color: #d1d5db; border-color: #6c63ff; }
|
||||
}
|
||||
}
|
||||
|
||||
.fx-option {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.65rem;
|
||||
padding: 0.55rem 0.6rem;
|
||||
border: 1px solid #1f2937;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
|
||||
& + & { margin-top: 0.5rem; }
|
||||
|
||||
input { margin-top: 0.2rem; accent-color: #6c63ff; }
|
||||
|
||||
span { display: flex; flex-direction: column; }
|
||||
strong { color: white; font-size: 0.9rem; font-weight: 600; }
|
||||
small { color: #9ca3af; font-size: 0.8rem; line-height: 1.4; }
|
||||
}
|
||||
|
||||
.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; }
|
||||
&:disabled { opacity: 0.6; cursor: default; }
|
||||
}
|
||||
|
||||
.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;
|
||||
background: #6c63ff;
|
||||
|
||||
&:hover { background: #5b52e0; }
|
||||
&:disabled { opacity: 0.6; cursor: default; }
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { LucideAngularModule, Download, X } from 'lucide-angular';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { FoundryExportOptions } from '../../../services/campaign.service';
|
||||
|
||||
/**
|
||||
* Modale de choix du PÉRIMÈTRE de l'export Foundry : tout, ou seulement les
|
||||
* cartes + ennemis (usage table de jeu), ou seulement les journaux… Trois cases
|
||||
* indépendantes + raccourcis. Le parent déclenche le téléchargement au confirm.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-foundry-export-dialog',
|
||||
imports: [LucideAngularModule, TranslatePipe],
|
||||
templateUrl: './foundry-export-dialog.component.html',
|
||||
styleUrls: ['./foundry-export-dialog.component.scss']
|
||||
})
|
||||
export class FoundryExportDialogComponent {
|
||||
readonly Download = Download;
|
||||
readonly X = X;
|
||||
|
||||
@Input() open = false;
|
||||
@Input() exporting = false;
|
||||
@Output() cancelled = new EventEmitter<void>();
|
||||
@Output() confirmed = new EventEmitter<FoundryExportOptions>();
|
||||
|
||||
maps = true;
|
||||
journals = true;
|
||||
tables = true;
|
||||
|
||||
get nothingSelected(): boolean { return !this.maps && !this.journals && !this.tables; }
|
||||
|
||||
/** Raccourci « usage table de jeu » : uniquement les cartes + ennemis. */
|
||||
presetMapsOnly(): void {
|
||||
this.maps = true;
|
||||
this.journals = false;
|
||||
this.tables = false;
|
||||
}
|
||||
|
||||
presetAll(): void {
|
||||
this.maps = true;
|
||||
this.journals = true;
|
||||
this.tables = true;
|
||||
}
|
||||
|
||||
onCancel(): void {
|
||||
if (this.exporting) return;
|
||||
this.cancelled.emit();
|
||||
}
|
||||
|
||||
onConfirm(): void {
|
||||
if (this.nothingSelected || this.exporting) return;
|
||||
this.confirmed.emit({ maps: this.maps, journals: this.journals, tables: this.tables });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user