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

Personnages joueurs

-
+ -
+

Arcs narratifs

-
-
+
{{ 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 @@ -
+

Dossiers

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