diff --git a/brain/app/main.py b/brain/app/main.py index 8ee2a32..24f22f5 100644 --- a/brain/app/main.py +++ b/brain/app/main.py @@ -40,7 +40,7 @@ from app.infrastructure.onemin_adapter import OneMinAiLLMProvider app = FastAPI( title="LoreMind Brain", description="Backend IA pour la génération de contenu narratif.", - version="0.6.2", + version="0.6.5", ) diff --git a/core/pom.xml b/core/pom.xml index 8ad74d4..750ab69 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -14,7 +14,7 @@ com.loremind loremind-core - 0.6.2 + 0.6.5 LoreMind Core Backend Core - Architecture Hexagonale diff --git a/core/src/main/java/com/loremind/application/campaigncontext/ArcService.java b/core/src/main/java/com/loremind/application/campaigncontext/ArcService.java index 1aabcbb..933f06e 100644 --- a/core/src/main/java/com/loremind/application/campaigncontext/ArcService.java +++ b/core/src/main/java/com/loremind/application/campaigncontext/ArcService.java @@ -36,11 +36,16 @@ public class ArcService { public record DeletionImpact(int chapters, int scenes) {} public Arc createArc(String name, String description, String campaignId, int order) { + return createArc(name, description, campaignId, order, null); + } + + public Arc createArc(String name, String description, String campaignId, int order, String icon) { Arc arc = Arc.builder() .name(name) .description(description) .campaignId(campaignId) .order(order) + .icon(icon) .build(); return arcRepository.save(arc); } diff --git a/core/src/main/java/com/loremind/application/campaigncontext/ChapterService.java b/core/src/main/java/com/loremind/application/campaigncontext/ChapterService.java index f60d6f5..18fda92 100644 --- a/core/src/main/java/com/loremind/application/campaigncontext/ChapterService.java +++ b/core/src/main/java/com/loremind/application/campaigncontext/ChapterService.java @@ -30,11 +30,16 @@ public class ChapterService { public record DeletionImpact(int scenes) {} public Chapter createChapter(String name, String description, String arcId, int order) { + return createChapter(name, description, arcId, order, null); + } + + public Chapter createChapter(String name, String description, String arcId, int order, String icon) { Chapter chapter = Chapter.builder() .name(name) .description(description) .arcId(arcId) .order(order) + .icon(icon) .build(); return chapterRepository.save(chapter); } diff --git a/core/src/main/java/com/loremind/application/campaigncontext/SceneService.java b/core/src/main/java/com/loremind/application/campaigncontext/SceneService.java index 1175c76..b6d6ac3 100644 --- a/core/src/main/java/com/loremind/application/campaigncontext/SceneService.java +++ b/core/src/main/java/com/loremind/application/campaigncontext/SceneService.java @@ -26,11 +26,16 @@ public class SceneService { } public Scene createScene(String name, String description, String chapterId, int order) { + return createScene(name, description, chapterId, order, null); + } + + public Scene createScene(String name, String description, String chapterId, int order, String icon) { Scene scene = Scene.builder() .name(name) .description(description) .chapterId(chapterId) .order(order) + .icon(icon) .build(); return sceneRepository.save(scene); } diff --git a/core/src/main/java/com/loremind/domain/campaigncontext/Arc.java b/core/src/main/java/com/loremind/domain/campaigncontext/Arc.java index 3c49055..d5ced15 100644 --- a/core/src/main/java/com/loremind/domain/campaigncontext/Arc.java +++ b/core/src/main/java/com/loremind/domain/campaigncontext/Arc.java @@ -21,6 +21,9 @@ public class Arc { private String campaignId; // Référence vers la Campaign parente private int order; // Ordre de l'arc dans la campagne + /** Cle d'icone choisie par l'utilisateur (cf. CAMPAIGN_ICON_OPTIONS cote front). */ + private String icon; + // Champs narratifs enrichis (voir docs/maquettes/campagne/détail/) private String themes; // Thèmes principaux explorés dans cet arc private String stakes; // Enjeux globaux pour les personnages diff --git a/core/src/main/java/com/loremind/domain/campaigncontext/Chapter.java b/core/src/main/java/com/loremind/domain/campaigncontext/Chapter.java index 2fe92b6..391358d 100644 --- a/core/src/main/java/com/loremind/domain/campaigncontext/Chapter.java +++ b/core/src/main/java/com/loremind/domain/campaigncontext/Chapter.java @@ -21,6 +21,9 @@ public class Chapter { private String arcId; // Référence vers l'Arc parent private int order; // Ordre du chapitre dans l'arc + /** Cle d'icone choisie par l'utilisateur (cf. CAMPAIGN_ICON_OPTIONS cote front). */ + private String icon; + // Champs narratifs enrichis (voir docs/maquettes/campagne/détail/) private String gmNotes; // Notes privées du MJ (non exportées vers FoundryVTT) private String playerObjectives; // Objectifs des joueurs dans ce chapitre diff --git a/core/src/main/java/com/loremind/domain/campaigncontext/Scene.java b/core/src/main/java/com/loremind/domain/campaigncontext/Scene.java index 4f65310..3c7298c 100644 --- a/core/src/main/java/com/loremind/domain/campaigncontext/Scene.java +++ b/core/src/main/java/com/loremind/domain/campaigncontext/Scene.java @@ -21,6 +21,9 @@ public class Scene { private String chapterId; // Référence vers le Chapter parent private int order; // Ordre de la scène dans le chapitre + /** Cle d'icone choisie par l'utilisateur (cf. CAMPAIGN_ICON_OPTIONS cote front). */ + private String icon; + // === Contexte et ambiance === private String location; // Lieu de la scène (ex: Taverne du Dragon d'Or) private String timing; // Moment (ex: Soir, à la tombée de la nuit) diff --git a/core/src/main/java/com/loremind/infrastructure/persistence/entity/ArcJpaEntity.java b/core/src/main/java/com/loremind/infrastructure/persistence/entity/ArcJpaEntity.java index 832c90c..37f471b 100644 --- a/core/src/main/java/com/loremind/infrastructure/persistence/entity/ArcJpaEntity.java +++ b/core/src/main/java/com/loremind/infrastructure/persistence/entity/ArcJpaEntity.java @@ -37,6 +37,9 @@ public class ArcJpaEntity { @Column(name = "\"order\"", nullable = false) private int order; + @Column + private String icon; + // Champs narratifs enrichis — ajoutés automatiquement par Hibernate DDL (ddl-auto=update) @Column(columnDefinition = "TEXT") private String themes; diff --git a/core/src/main/java/com/loremind/infrastructure/persistence/entity/ChapterJpaEntity.java b/core/src/main/java/com/loremind/infrastructure/persistence/entity/ChapterJpaEntity.java index 83dfca5..cf9325b 100644 --- a/core/src/main/java/com/loremind/infrastructure/persistence/entity/ChapterJpaEntity.java +++ b/core/src/main/java/com/loremind/infrastructure/persistence/entity/ChapterJpaEntity.java @@ -37,6 +37,9 @@ public class ChapterJpaEntity { @Column(name = "\"order\"", nullable = false) private int order; + @Column + private String icon; + // Champs narratifs enrichis — ajoutés automatiquement par Hibernate DDL (ddl-auto=update) @Column(name = "gm_notes", columnDefinition = "TEXT") private String gmNotes; diff --git a/core/src/main/java/com/loremind/infrastructure/persistence/entity/SceneJpaEntity.java b/core/src/main/java/com/loremind/infrastructure/persistence/entity/SceneJpaEntity.java index bf1220f..f1077b9 100644 --- a/core/src/main/java/com/loremind/infrastructure/persistence/entity/SceneJpaEntity.java +++ b/core/src/main/java/com/loremind/infrastructure/persistence/entity/SceneJpaEntity.java @@ -39,6 +39,9 @@ public class SceneJpaEntity { @Column(name = "\"order\"", nullable = false) private int order; + @Column + private String icon; + // Champs narratifs enrichis — ajoutés automatiquement par Hibernate (ddl-auto=update) // Contexte et ambiance diff --git a/core/src/main/java/com/loremind/infrastructure/persistence/postgres/PostgresArcRepository.java b/core/src/main/java/com/loremind/infrastructure/persistence/postgres/PostgresArcRepository.java index 1395d81..2f7b963 100644 --- a/core/src/main/java/com/loremind/infrastructure/persistence/postgres/PostgresArcRepository.java +++ b/core/src/main/java/com/loremind/infrastructure/persistence/postgres/PostgresArcRepository.java @@ -71,6 +71,7 @@ public class PostgresArcRepository implements ArcRepository { .description(jpaEntity.getDescription()) .campaignId(jpaEntity.getCampaignId().toString()) .order(jpaEntity.getOrder()) + .icon(jpaEntity.getIcon()) .themes(jpaEntity.getThemes()) .stakes(jpaEntity.getStakes()) .gmNotes(jpaEntity.getGmNotes()) @@ -99,6 +100,7 @@ public class PostgresArcRepository implements ArcRepository { .description(arc.getDescription()) .campaignId(Long.parseLong(arc.getCampaignId())) .order(arc.getOrder()) + .icon(arc.getIcon()) .themes(arc.getThemes()) .stakes(arc.getStakes()) .gmNotes(arc.getGmNotes()) diff --git a/core/src/main/java/com/loremind/infrastructure/persistence/postgres/PostgresChapterRepository.java b/core/src/main/java/com/loremind/infrastructure/persistence/postgres/PostgresChapterRepository.java index 8fc7491..afe90cf 100644 --- a/core/src/main/java/com/loremind/infrastructure/persistence/postgres/PostgresChapterRepository.java +++ b/core/src/main/java/com/loremind/infrastructure/persistence/postgres/PostgresChapterRepository.java @@ -71,6 +71,7 @@ public class PostgresChapterRepository implements ChapterRepository { .description(jpaEntity.getDescription()) .arcId(jpaEntity.getArcId().toString()) .order(jpaEntity.getOrder()) + .icon(jpaEntity.getIcon()) .gmNotes(jpaEntity.getGmNotes()) .playerObjectives(jpaEntity.getPlayerObjectives()) .narrativeStakes(jpaEntity.getNarrativeStakes()) @@ -96,6 +97,7 @@ public class PostgresChapterRepository implements ChapterRepository { .description(chapter.getDescription()) .arcId(Long.parseLong(chapter.getArcId())) .order(chapter.getOrder()) + .icon(chapter.getIcon()) .gmNotes(chapter.getGmNotes()) .playerObjectives(chapter.getPlayerObjectives()) .narrativeStakes(chapter.getNarrativeStakes()) diff --git a/core/src/main/java/com/loremind/infrastructure/persistence/postgres/PostgresSceneRepository.java b/core/src/main/java/com/loremind/infrastructure/persistence/postgres/PostgresSceneRepository.java index 4dd162e..57260a7 100644 --- a/core/src/main/java/com/loremind/infrastructure/persistence/postgres/PostgresSceneRepository.java +++ b/core/src/main/java/com/loremind/infrastructure/persistence/postgres/PostgresSceneRepository.java @@ -71,6 +71,7 @@ public class PostgresSceneRepository implements SceneRepository { .description(jpaEntity.getDescription()) .chapterId(jpaEntity.getChapterId().toString()) .order(jpaEntity.getOrder()) + .icon(jpaEntity.getIcon()) .location(jpaEntity.getLocation()) .timing(jpaEntity.getTiming()) .atmosphere(jpaEntity.getAtmosphere()) @@ -104,6 +105,7 @@ public class PostgresSceneRepository implements SceneRepository { .description(scene.getDescription()) .chapterId(Long.parseLong(scene.getChapterId())) .order(scene.getOrder()) + .icon(scene.getIcon()) .location(scene.getLocation()) .timing(scene.getTiming()) .atmosphere(scene.getAtmosphere()) diff --git a/core/src/main/java/com/loremind/infrastructure/web/controller/ArcController.java b/core/src/main/java/com/loremind/infrastructure/web/controller/ArcController.java index 75196be..64fd40b 100644 --- a/core/src/main/java/com/loremind/infrastructure/web/controller/ArcController.java +++ b/core/src/main/java/com/loremind/infrastructure/web/controller/ArcController.java @@ -28,7 +28,7 @@ public class ArcController { @PostMapping public ResponseEntity createArc(@RequestBody ArcDTO arcDTO) { Arc arc = arcMapper.toDomain(arcDTO); - Arc createdArc = arcService.createArc(arc.getName(), arc.getDescription(), arc.getCampaignId(), arc.getOrder()); + Arc createdArc = arcService.createArc(arc.getName(), arc.getDescription(), arc.getCampaignId(), arc.getOrder(), arc.getIcon()); return ResponseEntity.ok(arcMapper.toDTO(createdArc)); } diff --git a/core/src/main/java/com/loremind/infrastructure/web/controller/ChapterController.java b/core/src/main/java/com/loremind/infrastructure/web/controller/ChapterController.java index f029a8d..0970a4e 100644 --- a/core/src/main/java/com/loremind/infrastructure/web/controller/ChapterController.java +++ b/core/src/main/java/com/loremind/infrastructure/web/controller/ChapterController.java @@ -28,7 +28,7 @@ public class ChapterController { @PostMapping public ResponseEntity createChapter(@RequestBody ChapterDTO chapterDTO) { Chapter chapter = chapterMapper.toDomain(chapterDTO); - Chapter createdChapter = chapterService.createChapter(chapter.getName(), chapter.getDescription(), chapter.getArcId(), chapter.getOrder()); + Chapter createdChapter = chapterService.createChapter(chapter.getName(), chapter.getDescription(), chapter.getArcId(), chapter.getOrder(), chapter.getIcon()); return ResponseEntity.ok(chapterMapper.toDTO(createdChapter)); } diff --git a/core/src/main/java/com/loremind/infrastructure/web/controller/SceneController.java b/core/src/main/java/com/loremind/infrastructure/web/controller/SceneController.java index f76a838..f986901 100644 --- a/core/src/main/java/com/loremind/infrastructure/web/controller/SceneController.java +++ b/core/src/main/java/com/loremind/infrastructure/web/controller/SceneController.java @@ -28,7 +28,7 @@ public class SceneController { @PostMapping public ResponseEntity createScene(@RequestBody SceneDTO sceneDTO) { Scene scene = sceneMapper.toDomain(sceneDTO); - Scene createdScene = sceneService.createScene(scene.getName(), scene.getDescription(), scene.getChapterId(), scene.getOrder()); + Scene createdScene = sceneService.createScene(scene.getName(), scene.getDescription(), scene.getChapterId(), scene.getOrder(), scene.getIcon()); return ResponseEntity.ok(sceneMapper.toDTO(createdScene)); } diff --git a/core/src/main/java/com/loremind/infrastructure/web/dto/campaigncontext/ArcDTO.java b/core/src/main/java/com/loremind/infrastructure/web/dto/campaigncontext/ArcDTO.java index 76fe8ee..19b5e68 100644 --- a/core/src/main/java/com/loremind/infrastructure/web/dto/campaigncontext/ArcDTO.java +++ b/core/src/main/java/com/loremind/infrastructure/web/dto/campaigncontext/ArcDTO.java @@ -17,6 +17,9 @@ public class ArcDTO { private String campaignId; private int order; + /** Cle d'icone (cf. CAMPAIGN_ICON_OPTIONS cote front). */ + private String icon; + // Champs narratifs enrichis private String themes; private String stakes; diff --git a/core/src/main/java/com/loremind/infrastructure/web/dto/campaigncontext/ChapterDTO.java b/core/src/main/java/com/loremind/infrastructure/web/dto/campaigncontext/ChapterDTO.java index 4c11713..54a654e 100644 --- a/core/src/main/java/com/loremind/infrastructure/web/dto/campaigncontext/ChapterDTO.java +++ b/core/src/main/java/com/loremind/infrastructure/web/dto/campaigncontext/ChapterDTO.java @@ -17,6 +17,9 @@ public class ChapterDTO { private String arcId; private int order; + /** Cle d'icone (cf. CAMPAIGN_ICON_OPTIONS cote front). */ + private String icon; + // Champs narratifs enrichis private String gmNotes; private String playerObjectives; diff --git a/core/src/main/java/com/loremind/infrastructure/web/dto/campaigncontext/SceneDTO.java b/core/src/main/java/com/loremind/infrastructure/web/dto/campaigncontext/SceneDTO.java index ead1693..674b86a 100644 --- a/core/src/main/java/com/loremind/infrastructure/web/dto/campaigncontext/SceneDTO.java +++ b/core/src/main/java/com/loremind/infrastructure/web/dto/campaigncontext/SceneDTO.java @@ -17,6 +17,9 @@ public class SceneDTO { private String chapterId; private int order; + /** Cle d'icone (cf. CAMPAIGN_ICON_OPTIONS cote front). */ + private String icon; + // Champs narratifs enrichis private String location; private String timing; diff --git a/core/src/main/java/com/loremind/infrastructure/web/mapper/ArcMapper.java b/core/src/main/java/com/loremind/infrastructure/web/mapper/ArcMapper.java index 16e0617..5308ffa 100644 --- a/core/src/main/java/com/loremind/infrastructure/web/mapper/ArcMapper.java +++ b/core/src/main/java/com/loremind/infrastructure/web/mapper/ArcMapper.java @@ -24,6 +24,7 @@ public class ArcMapper { dto.setDescription(arc.getDescription()); dto.setCampaignId(arc.getCampaignId()); dto.setOrder(arc.getOrder()); + dto.setIcon(arc.getIcon()); dto.setThemes(arc.getThemes()); dto.setStakes(arc.getStakes()); dto.setGmNotes(arc.getGmNotes()); @@ -46,6 +47,7 @@ public class ArcMapper { .description(dto.getDescription()) .campaignId(dto.getCampaignId()) .order(dto.getOrder()) + .icon(dto.getIcon()) .themes(dto.getThemes()) .stakes(dto.getStakes()) .gmNotes(dto.getGmNotes()) diff --git a/core/src/main/java/com/loremind/infrastructure/web/mapper/ChapterMapper.java b/core/src/main/java/com/loremind/infrastructure/web/mapper/ChapterMapper.java index 767c29e..41afd6e 100644 --- a/core/src/main/java/com/loremind/infrastructure/web/mapper/ChapterMapper.java +++ b/core/src/main/java/com/loremind/infrastructure/web/mapper/ChapterMapper.java @@ -24,6 +24,7 @@ public class ChapterMapper { dto.setDescription(chapter.getDescription()); dto.setArcId(chapter.getArcId()); dto.setOrder(chapter.getOrder()); + dto.setIcon(chapter.getIcon()); dto.setGmNotes(chapter.getGmNotes()); dto.setPlayerObjectives(chapter.getPlayerObjectives()); dto.setNarrativeStakes(chapter.getNarrativeStakes()); @@ -44,6 +45,7 @@ public class ChapterMapper { .description(dto.getDescription()) .arcId(dto.getArcId()) .order(dto.getOrder()) + .icon(dto.getIcon()) .gmNotes(dto.getGmNotes()) .playerObjectives(dto.getPlayerObjectives()) .narrativeStakes(dto.getNarrativeStakes()) diff --git a/core/src/main/java/com/loremind/infrastructure/web/mapper/SceneMapper.java b/core/src/main/java/com/loremind/infrastructure/web/mapper/SceneMapper.java index d66b4fe..4cf57e8 100644 --- a/core/src/main/java/com/loremind/infrastructure/web/mapper/SceneMapper.java +++ b/core/src/main/java/com/loremind/infrastructure/web/mapper/SceneMapper.java @@ -27,6 +27,7 @@ public class SceneMapper { dto.setDescription(scene.getDescription()); dto.setChapterId(scene.getChapterId()); dto.setOrder(scene.getOrder()); + dto.setIcon(scene.getIcon()); dto.setLocation(scene.getLocation()); dto.setTiming(scene.getTiming()); dto.setAtmosphere(scene.getAtmosphere()); @@ -59,6 +60,7 @@ public class SceneMapper { .description(dto.getDescription()) .chapterId(dto.getChapterId()) .order(dto.getOrder()) + .icon(dto.getIcon()) .location(dto.getLocation()) .timing(dto.getTiming()) .atmosphere(dto.getAtmosphere()) diff --git a/web/package-lock.json b/web/package-lock.json index 8ab487d..4f4279d 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,12 +1,12 @@ { "name": "loremind-web", - "version": "0.6.1", + "version": "0.6.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "loremind-web", - "version": "0.6.1", + "version": "0.6.5", "dependencies": { "@angular/animations": "^17.0.0", "@angular/common": "^17.0.0", diff --git a/web/package.json b/web/package.json index 245f8b6..a0ee19a 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "loremind-web", - "version": "0.6.2", + "version": "0.6.5", "description": "LoreMind Frontend - Angular", "scripts": { "ng": "ng", diff --git a/web/src/app/app.component.scss b/web/src/app/app.component.scss index d89a34a..b8d5308 100644 --- a/web/src/app/app.component.scss +++ b/web/src/app/app.component.scss @@ -5,6 +5,10 @@ .main-content { flex: 1; - padding: 2rem; + // padding-top: 0 — sinon le contenu defile dans la zone de padding + // au-dessus du `.page-header` sticky (top: 0 pin sur l'edge interne du + // padding-box). Chaque page-wrapper definit deja son propre padding-top + // qui devient l'unique source d'espacement haut. + padding: 0 2rem 2rem; overflow-y: auto; } diff --git a/web/src/app/campaigns/arc-create/arc-create.component.html b/web/src/app/campaigns/arc-create/arc-create.component.html index 7b04d28..3e8a2b2 100644 --- a/web/src/app/campaigns/arc-create/arc-create.component.html +++ b/web/src/app/campaigns/arc-create/arc-create.component.html @@ -27,6 +27,11 @@ +
+ + +
+
+ + +
@@ -63,6 +71,11 @@ +
+ + +
+
@@ -136,17 +149,6 @@
-
- - - -
-
diff --git a/web/src/app/campaigns/arc-edit/arc-edit.component.ts b/web/src/app/campaigns/arc-edit/arc-edit.component.ts index 31af867..1d04b08 100644 --- a/web/src/app/campaigns/arc-edit/arc-edit.component.ts +++ b/web/src/app/campaigns/arc-edit/arc-edit.component.ts @@ -16,6 +16,8 @@ import { loadCampaignTreeData, buildCampaignTree } from '../campaign-tree.helper import { LoreLinkPickerComponent } from '../../shared/lore-link-picker/lore-link-picker.component'; import { AiChatDrawerComponent } from '../../shared/ai-chat-drawer/ai-chat-drawer.component'; import { ImageGalleryComponent } from '../../shared/image-gallery/image-gallery.component'; +import { IconPickerComponent } from '../../shared/icon-picker/icon-picker.component'; +import { CAMPAIGN_ICON_OPTIONS } from '../campaign-icons'; /** * Écran de détail/modification d'un Arc. @@ -29,13 +31,15 @@ import { ImageGalleryComponent } from '../../shared/image-gallery/image-gallery. @Component({ selector: 'app-arc-edit', standalone: true, - imports: [CommonModule, ReactiveFormsModule, LucideAngularModule, LoreLinkPickerComponent, AiChatDrawerComponent, ImageGalleryComponent], + imports: [CommonModule, ReactiveFormsModule, LucideAngularModule, LoreLinkPickerComponent, AiChatDrawerComponent, ImageGalleryComponent, IconPickerComponent], templateUrl: './arc-edit.component.html', styleUrls: ['./arc-edit.component.scss'] }) export class ArcEditComponent implements OnInit, OnDestroy { readonly Trash2 = Trash2; readonly Sparkles = Sparkles; + readonly campaignIconOptions = CAMPAIGN_ICON_OPTIONS; + selectedIcon: string | null = null; /** État drawer chat IA (b5.7 — intégration Campagne). */ chatOpen = false; @@ -122,6 +126,7 @@ export class ArcEditComponent implements OnInit, OnDestroy { this.loreId = loreId; this.availablePages = pages; this.relatedPageIds = [...(arc.relatedPageIds ?? [])]; + this.selectedIcon = arc.icon ?? null; this.illustrationImageIds = [...(arc.illustrationImageIds ?? [])]; this.mapImageIds = [...(arc.mapImageIds ?? [])]; this.pageTitleService.set(arc.name); @@ -167,7 +172,8 @@ export class ArcEditComponent implements OnInit, OnDestroy { resolution: this.form.value.resolution, relatedPageIds: this.relatedPageIds, illustrationImageIds: this.illustrationImageIds, - mapImageIds: this.mapImageIds + mapImageIds: this.mapImageIds, + icon: this.selectedIcon }).subscribe({ next: () => this.router.navigate(['/campaigns', this.campaignId, 'arcs', this.arcId]), error: () => console.error('Erreur lors de la sauvegarde') diff --git a/web/src/app/campaigns/arc-view/arc-view.component.html b/web/src/app/campaigns/arc-view/arc-view.component.html index a1c1d3d..639fe17 100644 --- a/web/src/app/campaigns/arc-view/arc-view.component.html +++ b/web/src/app/campaigns/arc-view/arc-view.component.html @@ -2,7 +2,10 @@
-

{{ arc.name }}

+

+ + {{ arc.name }} +

Arc narratif

diff --git a/web/src/app/campaigns/arc-view/arc-view.component.ts b/web/src/app/campaigns/arc-view/arc-view.component.ts index dfad88c..cda5b31 100644 --- a/web/src/app/campaigns/arc-view/arc-view.component.ts +++ b/web/src/app/campaigns/arc-view/arc-view.component.ts @@ -4,6 +4,7 @@ import { ActivatedRoute, Router, RouterModule } from '@angular/router'; import { forkJoin, of } from 'rxjs'; import { switchMap } from 'rxjs/operators'; import { LucideAngularModule, Pencil, Trash2 } from 'lucide-angular'; +import { resolveCampaignIcon } from '../campaign-icons'; import { CampaignService } from '../../services/campaign.service'; import { CharacterService } from '../../services/character.service'; import { PageService } from '../../services/page.service'; @@ -29,6 +30,7 @@ import { ImageGalleryComponent } from '../../shared/image-gallery/image-gallery. export class ArcViewComponent implements OnInit, OnDestroy { readonly Pencil = Pencil; readonly Trash2 = Trash2; + readonly resolveCampaignIcon = resolveCampaignIcon; campaignId = ''; arcId = ''; diff --git a/web/src/app/campaigns/campaign-icons.ts b/web/src/app/campaigns/campaign-icons.ts new file mode 100644 index 0000000..7ff93bb --- /dev/null +++ b/web/src/app/campaigns/campaign-icons.ts @@ -0,0 +1,66 @@ +import { + Bookmark, + Theater, Drama, Compass, Flag, Calendar, Map, Castle, Tent, + Swords, Skull, Crown, Heart, Eye, Footprints, Dice5, Hourglass, + Flame, Snowflake, Cloud, Sun, Moon, Star, Zap, Target, + Music, MessageCircle, Lock, Key as KeyIcon, Coins, Gift, + LucideIconData +} from 'lucide-angular'; + +// Type local equivalent a IconOption de lore-icons. Duplique volontairement +// pour eviter une dependance circulaire (lore-icons importe ce fichier). +export interface CampaignIconOption { + key: string; + icon: LucideIconData; +} + +/** + * Banque d'icones dediee aux entites narratives d'une campagne (arc, chapitre, scene). + * + * Pourquoi separe de LORE_ICON_OPTIONS ? Les icones lore sont plutot orientees + * "objets de monde" (chateau, foret, dragon...). Ici on est sur du sequencement + * narratif et de la mise en scene : ambiances, actes, decisions. Cles uniques + * (prefixees `c-` quand un nom existerait deja dans le lore) pour eviter les + * collisions avec LORE_ICON_OPTIONS — le resolver consulte les deux registres. + */ +export const CAMPAIGN_ICON_OPTIONS: CampaignIconOption[] = [ + { key: 'c-theater', icon: Theater }, + { key: 'c-drama', icon: Drama }, + { key: 'c-compass', icon: Compass }, + { key: 'c-flag', icon: Flag }, + { key: 'c-calendar', icon: Calendar }, + { key: 'c-map', icon: Map }, + { key: 'c-castle', icon: Castle }, + { key: 'c-tent', icon: Tent }, + { key: 'c-swords', icon: Swords }, + { key: 'c-skull', icon: Skull }, + { key: 'c-crown', icon: Crown }, + { key: 'c-heart', icon: Heart }, + { key: 'c-eye', icon: Eye }, + { key: 'c-footprints', icon: Footprints }, + { key: 'c-dice', icon: Dice5 }, + { key: 'c-hourglass', icon: Hourglass }, + { key: 'c-flame', icon: Flame }, + { key: 'c-snowflake', icon: Snowflake }, + { key: 'c-cloud', icon: Cloud }, + { key: 'c-sun', icon: Sun }, + { key: 'c-moon', icon: Moon }, + { key: 'c-star', icon: Star }, + { key: 'c-zap', icon: Zap }, + { key: 'c-target', icon: Target }, + { key: 'c-music', icon: Music }, + { key: 'c-message', icon: MessageCircle }, + { key: 'c-lock', icon: Lock }, + { key: 'c-key', icon: KeyIcon }, + { key: 'c-coins', icon: Coins }, + { key: 'c-gift', icon: Gift }, +]; + +/** Icone par defaut quand une entite narrative n'en a pas. */ +export const DEFAULT_CAMPAIGN_ICON: LucideIconData = Bookmark; + +/** Resolveur dedie. Prefere passer par `resolveIcon` dans lore-icons qui consulte les deux. */ +export function resolveCampaignIcon(key: string | null | undefined): LucideIconData { + if (!key) return DEFAULT_CAMPAIGN_ICON; + return CAMPAIGN_ICON_OPTIONS.find(o => o.key === key)?.icon ?? DEFAULT_CAMPAIGN_ICON; +} diff --git a/web/src/app/campaigns/campaign-tree.helper.ts b/web/src/app/campaigns/campaign-tree.helper.ts index 9830882..b0a94ba 100644 --- a/web/src/app/campaigns/campaign-tree.helper.ts +++ b/web/src/app/campaigns/campaign-tree.helper.ts @@ -109,11 +109,13 @@ export function buildCampaignTree(campaignId: string, data: CampaignTreeData): T const sceneItems: TreeItem[] = sortedScenes.map(sc => ({ id: `scene-${sc.id}`, label: sc.name, + iconKey: sc.icon ?? undefined, route: `/campaigns/${campaignId}/arcs/${arc.id}/chapters/${ch.id}/scenes/${sc.id}` })); return { id: `chapter-${ch.id}`, label: ch.name, + iconKey: ch.icon ?? undefined, children: sceneItems, route: `/campaigns/${campaignId}/arcs/${arc.id}/chapters/${ch.id}`, createActions: [{ @@ -127,6 +129,7 @@ export function buildCampaignTree(campaignId: string, data: CampaignTreeData): T return { id: `arc-${arc.id}`, label: arc.name, + iconKey: arc.icon ?? undefined, children: chapterItems, route: `/campaigns/${campaignId}/arcs/${arc.id}`, sectionHeaderBefore: idx === 0 ? 'Narration' : undefined, diff --git a/web/src/app/campaigns/chapter-create/chapter-create.component.html b/web/src/app/campaigns/chapter-create/chapter-create.component.html index 560e39e..91441e6 100644 --- a/web/src/app/campaigns/chapter-create/chapter-create.component.html +++ b/web/src/app/campaigns/chapter-create/chapter-create.component.html @@ -28,6 +28,11 @@
+
+ + +
+
+ + +
@@ -63,6 +71,11 @@ +
+ + +
+
+
+ + +
+
+ + +
@@ -63,6 +71,11 @@ +
+ + +
+
@@ -218,17 +231,6 @@
-
- - - -
- diff --git a/web/src/app/campaigns/scene-edit/scene-edit.component.ts b/web/src/app/campaigns/scene-edit/scene-edit.component.ts index 6c502b7..adae064 100644 --- a/web/src/app/campaigns/scene-edit/scene-edit.component.ts +++ b/web/src/app/campaigns/scene-edit/scene-edit.component.ts @@ -17,6 +17,8 @@ import { ExpandableSectionComponent } from '../../shared/expandable-section/expa import { LoreLinkPickerComponent } from '../../shared/lore-link-picker/lore-link-picker.component'; import { AiChatDrawerComponent } from '../../shared/ai-chat-drawer/ai-chat-drawer.component'; import { ImageGalleryComponent } from '../../shared/image-gallery/image-gallery.component'; +import { IconPickerComponent } from '../../shared/icon-picker/icon-picker.component'; +import { CAMPAIGN_ICON_OPTIONS } from '../campaign-icons'; /** * Écran de détail/modification d'une Scène. @@ -25,13 +27,15 @@ import { ImageGalleryComponent } from '../../shared/image-gallery/image-gallery. @Component({ selector: 'app-scene-edit', standalone: true, - imports: [CommonModule, ReactiveFormsModule, LucideAngularModule, ExpandableSectionComponent, LoreLinkPickerComponent, AiChatDrawerComponent, ImageGalleryComponent], + imports: [CommonModule, ReactiveFormsModule, LucideAngularModule, ExpandableSectionComponent, LoreLinkPickerComponent, AiChatDrawerComponent, ImageGalleryComponent, IconPickerComponent], templateUrl: './scene-edit.component.html', styleUrls: ['./scene-edit.component.scss'] }) export class SceneEditComponent implements OnInit, OnDestroy { readonly Trash2 = Trash2; readonly Sparkles = Sparkles; + readonly campaignIconOptions = CAMPAIGN_ICON_OPTIONS; + selectedIcon: string | null = null; /** État drawer chat IA (b5.7 — intégration Campagne). */ chatOpen = false; @@ -131,6 +135,7 @@ export class SceneEditComponent implements OnInit, OnDestroy { this.loreId = loreId; this.availablePages = pages; this.relatedPageIds = [...(scene.relatedPageIds ?? [])]; + this.selectedIcon = scene.icon ?? null; this.illustrationImageIds = [...(scene.illustrationImageIds ?? [])]; this.mapImageIds = [...(scene.mapImageIds ?? [])]; this.siblingScenes = chapterScenes.filter(s => s.id !== this.sceneId); @@ -184,7 +189,8 @@ export class SceneEditComponent implements OnInit, OnDestroy { relatedPageIds: this.relatedPageIds, illustrationImageIds: this.illustrationImageIds, mapImageIds: this.mapImageIds, - branches: this.branches + branches: this.branches, + icon: this.selectedIcon }).subscribe({ next: () => this.router.navigate(['/campaigns', this.campaignId, 'arcs', this.arcId, 'chapters', this.chapterId, 'scenes', this.sceneId]), error: () => console.error('Erreur lors de la sauvegarde') diff --git a/web/src/app/campaigns/scene-view/scene-view.component.html b/web/src/app/campaigns/scene-view/scene-view.component.html index b9c65fb..2642dbc 100644 --- a/web/src/app/campaigns/scene-view/scene-view.component.html +++ b/web/src/app/campaigns/scene-view/scene-view.component.html @@ -2,7 +2,10 @@
-

{{ scene.name }}

+

+ + {{ scene.name }} +

Scène

diff --git a/web/src/app/campaigns/scene-view/scene-view.component.ts b/web/src/app/campaigns/scene-view/scene-view.component.ts index 84a3b9a..ff1a988 100644 --- a/web/src/app/campaigns/scene-view/scene-view.component.ts +++ b/web/src/app/campaigns/scene-view/scene-view.component.ts @@ -4,6 +4,7 @@ import { ActivatedRoute, Router, RouterModule } from '@angular/router'; import { forkJoin, of } from 'rxjs'; import { switchMap } from 'rxjs/operators'; import { LucideAngularModule, Pencil, Trash2 } from 'lucide-angular'; +import { resolveCampaignIcon } from '../campaign-icons'; import { CampaignService } from '../../services/campaign.service'; import { CharacterService } from '../../services/character.service'; import { PageService } from '../../services/page.service'; @@ -28,6 +29,7 @@ import { ImageGalleryComponent } from '../../shared/image-gallery/image-gallery. export class SceneViewComponent implements OnInit, OnDestroy { readonly Pencil = Pencil; readonly Trash2 = Trash2; + readonly resolveCampaignIcon = resolveCampaignIcon; campaignId = ''; arcId = ''; diff --git a/web/src/app/lore/lore-icons.ts b/web/src/app/lore/lore-icons.ts index aa03c69..3096a7f 100644 --- a/web/src/app/lore/lore-icons.ts +++ b/web/src/app/lore/lore-icons.ts @@ -4,6 +4,7 @@ import { BookOpen, Scroll, Wand2, Sparkles, TreePine, Mountain, Ship, Flame, Star, Moon, Key, Globe, Compass, LucideIconData } from 'lucide-angular'; +import { CAMPAIGN_ICON_OPTIONS } from '../campaigns/campaign-icons'; /** * Registre partagé d'icônes disponibles pour les dossiers (LoreNode). @@ -46,8 +47,15 @@ export const LORE_ICON_OPTIONS: IconOption[] = [ /** Icône par défaut pour un dossier sans icône. */ export const DEFAULT_FOLDER_ICON: LucideIconData = Folder; -/** Résout une clé d'icône en LucideIconData. Fallback : icône dossier par défaut. */ +/** + * Résout une clé d'icône en LucideIconData. Consulte LORE_ICON_OPTIONS puis + * CAMPAIGN_ICON_OPTIONS pour permettre à la sidebar partagée d'afficher + * indifféremment des icônes de dossiers (lore) ou d'arcs/chapitres/scènes + * (campagne). Fallback : icône dossier par défaut. + */ export function resolveIcon(key: string | null | undefined): LucideIconData { if (!key) return DEFAULT_FOLDER_ICON; - return LORE_ICON_OPTIONS.find(o => o.key === key)?.icon ?? DEFAULT_FOLDER_ICON; + const loreMatch = LORE_ICON_OPTIONS.find(o => o.key === key); + if (loreMatch) return loreMatch.icon; + return CAMPAIGN_ICON_OPTIONS.find(o => o.key === key)?.icon ?? DEFAULT_FOLDER_ICON; } diff --git a/web/src/app/services/campaign.model.ts b/web/src/app/services/campaign.model.ts index 5be4e1d..4ac6b11 100644 --- a/web/src/app/services/campaign.model.ts +++ b/web/src/app/services/campaign.model.ts @@ -29,6 +29,9 @@ export interface Arc { order?: number; chapterCount?: number; + /** Cle d'icone choisie par l'utilisateur (cf. CAMPAIGN_ICON_OPTIONS). */ + icon?: string | null; + // Champs narratifs enrichis themes?: string; stakes?: string; @@ -52,6 +55,7 @@ export interface ArcCreate { description?: string; campaignId: string; order: number; + icon?: string | null; themes?: string; stakes?: string; @@ -70,6 +74,7 @@ export interface Chapter { description?: string; arcId: string; order?: number; + icon?: string | null; // Champs narratifs enrichis gmNotes?: string; @@ -86,6 +91,7 @@ export interface ChapterCreate { description?: string; arcId: string; order: number; + icon?: string | null; gmNotes?: string; playerObjectives?: string; @@ -112,6 +118,7 @@ export interface Scene { description?: string; // = Description courte dans l'UI chapterId: string; order?: number; + icon?: string | null; // Champs narratifs enrichis location?: string; @@ -136,6 +143,7 @@ export interface SceneCreate { description?: string; chapterId: string; order: number; + icon?: string | null; location?: string; timing?: string; diff --git a/web/src/app/shared/icon-picker/icon-picker.component.scss b/web/src/app/shared/icon-picker/icon-picker.component.scss new file mode 100644 index 0000000..aeed173 --- /dev/null +++ b/web/src/app/shared/icon-picker/icon-picker.component.scss @@ -0,0 +1,31 @@ +.icon-grid { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + background: #1f2937; + border: 1px solid #374151; + border-radius: 8px; + padding: 0.75rem; +} + +.icon-btn { + width: 36px; + height: 36px; + display: flex; + align-items: center; + justify-content: center; + background: transparent; + border: 1px solid transparent; + border-radius: 6px; + color: #9ca3af; + cursor: pointer; + transition: all 0.15s; + + &:hover { background: #374151; color: white; } + + &.selected { + background: #1e1b4b; + border-color: #6c63ff; + color: #a5b4fc; + } +} diff --git a/web/src/app/shared/icon-picker/icon-picker.component.ts b/web/src/app/shared/icon-picker/icon-picker.component.ts new file mode 100644 index 0000000..b05892d --- /dev/null +++ b/web/src/app/shared/icon-picker/icon-picker.component.ts @@ -0,0 +1,54 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { LucideAngularModule, LucideIconData } from 'lucide-angular'; + +export interface IconPickerOption { + key: string; + icon: LucideIconData; +} + +/** + * Petit composant reutilisable : grille d'icones cliquables avec selection unique. + * Utilise par les formulaires de creation / edition d'arcs, chapitres et scenes + * (ainsi que potentiellement les dossiers du Lore plus tard). + * + * Usage : + * + * + * Le composant est purement presentationnel : il n'interroge pas le registre + * d'icones lui-meme — l'appelant lui passe la banque a afficher. + */ +@Component({ + selector: 'app-icon-picker', + standalone: true, + imports: [CommonModule, LucideAngularModule], + template: ` +
+ +
+ `, + styleUrls: ['./icon-picker.component.scss'] +}) +export class IconPickerComponent { + @Input() options: IconPickerOption[] = []; + @Input() selected: string | null = null; + /** Permet le binding two-way `[(selected)]`. */ + @Output() selectedChange = new EventEmitter(); + /** Si true, recliquer sur l'icone actuellement selectionnee la deselectionne. */ + @Input() allowDeselect = true; + + pick(key: string): void { + const next = this.allowDeselect && this.selected === key ? null : key; + this.selected = next; + this.selectedChange.emit(next); + } +} diff --git a/web/src/styles/_view.scss b/web/src/styles/_view.scss index 299050e..a5e88d3 100644 --- a/web/src/styles/_view.scss +++ b/web/src/styles/_view.scss @@ -35,6 +35,26 @@ color: white; margin: 0 0 0.3rem; line-height: 1.2; + display: flex; + align-items: center; + gap: 0.6rem; + + // Pastille violette autour de l'icone. est inline par + // defaut : sans inline-flex le SVG se cale en haut-gauche de la boite + // au lieu d'etre centre. On fixe aussi la taille pour que la pastille + // soit toujours carree quelle que soit l'icone. + .title-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; + color: #a5b4fc; + background: #1e1b4b; + border: 1px solid #4338ca; + border-radius: 8px; + flex-shrink: 0; + } } .view-subtitle { color: #9ca3af;