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:
@@ -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