diff --git a/web/package-lock.json b/web/package-lock.json
index 92ecb87..2e6ff6f 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "loremind-web",
- "version": "1.0.0",
+ "version": "0.4.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "loremind-web",
- "version": "1.0.0",
+ "version": "0.4.0",
"dependencies": {
"@angular/animations": "^17.0.0",
"@angular/common": "^17.0.0",
@@ -16,7 +16,9 @@
"@angular/platform-browser": "^17.0.0",
"@angular/platform-browser-dynamic": "^17.0.0",
"@angular/router": "^17.0.0",
+ "dompurify": "^3.4.1",
"lucide-angular": "^1.0.0",
+ "marked": "^18.0.2",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.14.2"
@@ -3830,6 +3832,13 @@
"@types/node": "*"
}
},
+ "node_modules/@types/trusted-types": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
+ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
+ "license": "MIT",
+ "optional": true
+ },
"node_modules/@types/ws": {
"version": "8.18.1",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
@@ -5543,6 +5552,15 @@
"url": "https://github.com/fb55/domhandler?sponsor=1"
}
},
+ "node_modules/dompurify": {
+ "version": "3.4.1",
+ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.1.tgz",
+ "integrity": "sha512-JahakDAIg1gyOm7dlgWSDjV4n7Ip2PKR55NIT6jrMfIgLFgWo81vdr1/QGqWtFNRqXP9UV71oVePtjqS2ebnPw==",
+ "license": "(MPL-2.0 OR Apache-2.0)",
+ "optionalDependencies": {
+ "@types/trusted-types": "^2.0.7"
+ }
+ },
"node_modules/domutils": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
@@ -7679,6 +7697,18 @@
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
}
},
+ "node_modules/marked": {
+ "version": "18.0.2",
+ "resolved": "https://registry.npmjs.org/marked/-/marked-18.0.2.tgz",
+ "integrity": "sha512-NsmlUYBS/Zg57rgDWMYdnre6OTj4e+qq/JS2ot3KrYLSoHLw+sDu0Nm1ZGpRgYAq6c+b1ekaY5NzVchMCQnzcg==",
+ "license": "MIT",
+ "bin": {
+ "marked": "bin/marked.js"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
diff --git a/web/package.json b/web/package.json
index 948cc60..d8d5de9 100644
--- a/web/package.json
+++ b/web/package.json
@@ -19,7 +19,9 @@
"@angular/platform-browser": "^17.0.0",
"@angular/platform-browser-dynamic": "^17.0.0",
"@angular/router": "^17.0.0",
+ "dompurify": "^3.4.1",
"lucide-angular": "^1.0.0",
+ "marked": "^18.0.2",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.14.2"
diff --git a/web/src/app/campaigns/campaign-detail/campaign-detail.component.html b/web/src/app/campaigns/campaign-detail/campaign-detail.component.html
index 9ae9f62..3a20de5 100644
--- a/web/src/app/campaigns/campaign-detail/campaign-detail.component.html
+++ b/web/src/app/campaigns/campaign-detail/campaign-detail.component.html
@@ -70,7 +70,7 @@
-
+
+
-
+
0">
-
+
{{ arc.name }}
- {{ arc.chapterCount || 0 }} chapitres
+ {{ chapterCountByArc[arc.id!] || 0 }} chapitres
Aucun arc narratif pour le moment.
-
-
+
diff --git a/web/src/app/campaigns/campaign-detail/campaign-detail.component.scss b/web/src/app/campaigns/campaign-detail/campaign-detail.component.scss
index b02cf68..0589780 100644
--- a/web/src/app/campaigns/campaign-detail/campaign-detail.component.scss
+++ b/web/src/app/campaigns/campaign-detail/campaign-detail.component.scss
@@ -1,9 +1,23 @@
.campaign-detail {
padding: 2.5rem 2rem;
+ display: flex;
+ flex-direction: column;
+ gap: 1.5rem;
+}
+
+/**
+ * Chaque bloc (resume, PJ, arcs) est encapsule dans une carte distincte
+ * pour separer visuellement les zones. Le gap au niveau du parent gere
+ * les espacements — les sections ne portent plus de margin-bottom.
+ */
+.detail-section {
+ background: #0d1117;
+ border: 1px solid #1f2937;
+ border-radius: 12px;
+ padding: 1.5rem 1.75rem;
}
.detail-header {
- margin-bottom: 2.5rem;
h1 {
font-size: 1.75rem;
@@ -173,9 +187,6 @@
.arc-meta { color: #6b7280; font-size: 0.75rem; }
}
-.characters-section {
- margin-bottom: 2.5rem;
-}
.characters-grid {
display: grid;
diff --git a/web/src/app/campaigns/campaign-detail/campaign-detail.component.ts b/web/src/app/campaigns/campaign-detail/campaign-detail.component.ts
index b6cf157..52e2012 100644
--- a/web/src/app/campaigns/campaign-detail/campaign-detail.component.ts
+++ b/web/src/app/campaigns/campaign-detail/campaign-detail.component.ts
@@ -36,6 +36,8 @@ export class CampaignDetailComponent implements OnInit, OnDestroy {
campaign: Campaign | null = null;
arcs: Arc[] = [];
+ /** Nombre de chapitres par arc — alimente le compteur des cartes. */
+ chapterCountByArc: Record = {};
/** Lore associé si `campaign.loreId` est renseigné ; sinon null. */
linkedLore: Lore | null = null;
/** Lores disponibles pour changer l'association en mode édition. */
@@ -86,11 +88,20 @@ export class CampaignDetailComponent implements OnInit, OnDestroy {
this.loadLinkedGameSystem(campaign);
this.loadCharacters(campaign.id!);
this.arcs = treeData.arcs;
+ this.chapterCountByArc = this.computeChapterCounts(treeData);
this.showLayout(allCampaigns, treeData);
this.pageTitleService.set(campaign.name);
});
}
+ private computeChapterCounts(data: CampaignTreeData): Record {
+ const counts: Record = {};
+ for (const arcId of Object.keys(data.chaptersByArc)) {
+ counts[arcId] = data.chaptersByArc[arcId].length;
+ }
+ return counts;
+ }
+
/**
* Recharge explicitement après une mise à jour locale (ex: saveEdit).
* Contrairement au flux ngOnInit, on bypass le filter sur l'ID puisqu'on
@@ -110,6 +121,7 @@ export class CampaignDetailComponent implements OnInit, OnDestroy {
this.loadLinkedGameSystem(campaign);
this.loadCharacters(campaign.id!);
this.arcs = treeData.arcs;
+ this.chapterCountByArc = this.computeChapterCounts(treeData);
this.showLayout(allCampaigns, treeData);
this.pageTitleService.set(campaign.name);
});
@@ -157,6 +169,16 @@ export class CampaignDetailComponent implements OnInit, OnDestroy {
this.router.navigate(['/campaigns', this.campaign.id, 'characters', character.id, 'edit']);
}
+ createArc(): void {
+ if (!this.campaign) return;
+ this.router.navigate(['/campaigns', this.campaign.id, 'arcs', 'create']);
+ }
+
+ openArc(arc: Arc): void {
+ if (!this.campaign || !arc.id) return;
+ this.router.navigate(['/campaigns', this.campaign.id, 'arcs', arc.id]);
+ }
+
/** Extrait une ligne de résumé depuis le markdown (1re ligne non-vide, non-titre). */
characterSnippet(c: Character): string {
if (!c.markdownContent) return '(Fiche vide)';
diff --git a/web/src/app/lore/lore-detail/lore-detail.component.html b/web/src/app/lore/lore-detail/lore-detail.component.html
index d6d2513..792b2b9 100644
--- a/web/src/app/lore/lore-detail/lore-detail.component.html
+++ b/web/src/app/lore/lore-detail/lore-detail.component.html
@@ -39,7 +39,7 @@
-
+
+
diff --git a/web/src/app/lore/lore-detail/lore-detail.component.scss b/web/src/app/lore/lore-detail/lore-detail.component.scss
index 400df3d..6455e8a 100644
--- a/web/src/app/lore/lore-detail/lore-detail.component.scss
+++ b/web/src/app/lore/lore-detail/lore-detail.component.scss
@@ -2,6 +2,18 @@
padding: 2.5rem 2rem;
}
+/**
+ * Carte visuelle pour les sous-sections (Dossiers). Le header (titre + resume)
+ * reste en dehors d'une carte : il EST le lore, pas une section qui lui
+ * appartient. Meme pattern que campaign-detail.
+ */
+.detail-section {
+ background: #0d1117;
+ border: 1px solid #1f2937;
+ border-radius: 12px;
+ padding: 1.5rem 1.75rem;
+}
+
.detail-header {
display: flex;
align-items: flex-start;
diff --git a/web/src/app/shared/ai-chat-drawer/ai-chat-drawer.component.html b/web/src/app/shared/ai-chat-drawer/ai-chat-drawer.component.html
index 3149bec..606a3c5 100644
--- a/web/src/app/shared/ai-chat-drawer/ai-chat-drawer.component.html
+++ b/web/src/app/shared/ai-chat-drawer/ai-chat-drawer.component.html
@@ -1,4 +1,21 @@
-