Co-MJ v1 : quêtes de première classe, guidage de préparation, assistant IA,
All checks were successful
Build & Push Images / build (brain) (push) Successful in 1m38s
Build & Push Images / build (web) (push) Successful in 1m53s
Build & Push Images / build (core) (push) Successful in 3m40s
Build & Push Images / build-switcher (push) Successful in 16s

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:
2026-07-03 15:29:23 +02:00
parent 268f4721ce
commit b2c3800bf8
278 changed files with 15570 additions and 1451 deletions

View File

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

View File

@@ -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; }
}

View File

@@ -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 });
}
}