From f189f67aaf1ee9847e57b0ec694018d1f3f7ea80 Mon Sep 17 00:00:00 2001 From: "IETM_FIXE\\ietm6" Date: Wed, 22 Apr 2026 13:31:06 +0200 Subject: [PATCH] Mise en place d'un bouton + au hover plutot qu'un affichage constant --- web/src/app/campaigns/campaign-tree.helper.ts | 28 ++++++------- web/src/app/lore/lore-sidebar.helper.ts | 30 +++++++------- web/src/app/services/layout.service.ts | 15 +++++++ .../secondary-sidebar.component.html | 34 ++++++++++++++-- .../secondary-sidebar.component.scss | 39 +++++++++++++++++++ .../secondary-sidebar.component.ts | 38 +++++++++++++++++- 6 files changed, 151 insertions(+), 33 deletions(-) diff --git a/web/src/app/campaigns/campaign-tree.helper.ts b/web/src/app/campaigns/campaign-tree.helper.ts index 109a20e..7631fc1 100644 --- a/web/src/app/campaigns/campaign-tree.helper.ts +++ b/web/src/app/campaigns/campaign-tree.helper.ts @@ -80,30 +80,30 @@ export function buildCampaignTree(campaignId: string, data: CampaignTreeData): T label: sc.name, route: `/campaigns/${campaignId}/arcs/${arc.id}/chapters/${ch.id}/scenes/${sc.id}` })); - sceneItems.push({ - id: `new-scene-${ch.id}`, - label: '+ Nouvelle scène', - isAction: true, - route: `/campaigns/${campaignId}/arcs/${arc.id}/chapters/${ch.id}/scenes/create` - }); return { id: `chapter-${ch.id}`, label: ch.name, children: sceneItems, - route: `/campaigns/${campaignId}/arcs/${arc.id}/chapters/${ch.id}` + route: `/campaigns/${campaignId}/arcs/${arc.id}/chapters/${ch.id}`, + createActions: [{ + id: `new-scene-${ch.id}`, + label: 'Nouvelle scène', + route: `/campaigns/${campaignId}/arcs/${arc.id}/chapters/${ch.id}/scenes/create`, + actionIcon: 'plus' + }] }; }); - chapterItems.push({ - id: `new-chapter-${arc.id}`, - label: '+ Nouveau chapitre', - isAction: true, - route: `/campaigns/${campaignId}/arcs/${arc.id}/chapters/create` - }); return { id: `arc-${arc.id}`, label: arc.name, children: chapterItems, - route: `/campaigns/${campaignId}/arcs/${arc.id}` + route: `/campaigns/${campaignId}/arcs/${arc.id}`, + createActions: [{ + id: `new-chapter-${arc.id}`, + label: 'Nouveau chapitre', + route: `/campaigns/${campaignId}/arcs/${arc.id}/chapters/create`, + actionIcon: 'plus' + }] }; }); } diff --git a/web/src/app/lore/lore-sidebar.helper.ts b/web/src/app/lore/lore-sidebar.helper.ts index b59e32b..874fc9b 100644 --- a/web/src/app/lore/lore-sidebar.helper.ts +++ b/web/src/app/lore/lore-sidebar.helper.ts @@ -75,19 +75,7 @@ export function buildLoreSidebarConfig(data: LoreSidebarData): SecondarySidebarC id: `page-${p.id}`, label: p.title, route: `/lore/${lore.id}/pages/${p.id}` - })), - { - id: `create-folder-${node.id}`, - label: '+ Nouveau dossier', - isAction: true, - route: `/lore/${lore.id}/folders/${node.id}/create` - }, - { - id: `create-page-${node.id}`, - label: '+ Nouvelle page', - isAction: true, - route: `/lore/${lore.id}/nodes/${node.id}/pages/create` - } + })) ]; // IDs préfixés par type — chaque entité a sa propre séquence IDENTITY en base, // donc node.id=1 et page.id=1 peuvent coexister et collisionner dans le @@ -98,7 +86,21 @@ export function buildLoreSidebarConfig(data: LoreSidebarData): SecondarySidebarC iconKey: node.icon ?? undefined, route: `/lore/${lore.id}/folders/${node.id}/edit`, meta: nodePages.length > 0 ? String(nodePages.length) : undefined, - children + children, + createActions: [ + { + id: `create-folder-${node.id}`, + label: 'Nouveau sous-dossier', + route: `/lore/${lore.id}/folders/${node.id}/create`, + actionIcon: 'folder-plus' + }, + { + id: `create-page-${node.id}`, + label: 'Nouvelle page', + route: `/lore/${lore.id}/nodes/${node.id}/pages/create`, + actionIcon: 'file-plus' + } + ] }; }; diff --git a/web/src/app/services/layout.service.ts b/web/src/app/services/layout.service.ts index 844a68e..2911ab3 100644 --- a/web/src/app/services/layout.service.ts +++ b/web/src/app/services/layout.service.ts @@ -11,6 +11,21 @@ export interface TreeItem { iconKey?: string; /** Petit badge affiché à droite (ex: "3" pour compter les pages d'un dossier). */ meta?: string; + /** + * Actions de creation contextuelles (ex: "+ Nouveau chapitre" sur un arc). + * Affichees comme boutons icone au survol du noeud (repli visuel), et en + * pleine largeur si le noeud est expanded sans aucun enfant reel + * (empty-state inline, meilleur des deux mondes). + */ + createActions?: TreeCreateAction[]; +} + +export interface TreeCreateAction { + id: string; + label: string; // tooltip au hover, texte complet en empty-state + route: string; + /** Cle d'icone cote sidebar (plus | folder-plus | file-plus). */ + actionIcon?: 'plus' | 'folder-plus' | 'file-plus'; } export interface GlobalItem { diff --git a/web/src/app/shared/secondary-sidebar/secondary-sidebar.component.html b/web/src/app/shared/secondary-sidebar/secondary-sidebar.component.html index 15b4c0a..e2a0523 100644 --- a/web/src/app/shared/secondary-sidebar/secondary-sidebar.component.html +++ b/web/src/app/shared/secondary-sidebar/secondary-sidebar.component.html @@ -30,7 +30,7 @@
- + + + + + +
-
+
+ + +
+
+ + +
+
+
diff --git a/web/src/app/shared/secondary-sidebar/secondary-sidebar.component.scss b/web/src/app/shared/secondary-sidebar/secondary-sidebar.component.scss index 58be4db..762942b 100644 --- a/web/src/app/shared/secondary-sidebar/secondary-sidebar.component.scss +++ b/web/src/app/shared/secondary-sidebar/secondary-sidebar.component.scss @@ -132,6 +132,45 @@ align-items: center; gap: 0.15rem; width: 100%; + position: relative; + + // Hover-reveal : les boutons d'action de creation n'apparaissent qu'au + // survol de la ligne. Pattern Notion/VS Code. + &:hover .node-actions { opacity: 1; pointer-events: auto; } +} + +.node-actions { + display: inline-flex; + align-items: center; + gap: 0.1rem; + margin-left: auto; + flex-shrink: 0; + opacity: 0; + pointer-events: none; + transition: opacity 0.15s ease; + // Superpose les boutons d'action sur le .tree-item-meta eventuel + // (compteur de pages) : au hover on affiche les actions, au repos le meta. + position: absolute; + right: 0.25rem; + background: linear-gradient(to right, transparent, #1f2937 30%); + padding-left: 1rem; +} + +.node-action-btn { + display: inline-flex; + align-items: center; + justify-content: center; + width: 22px; + height: 22px; + background: transparent; + border: none; + border-radius: 4px; + cursor: pointer; + color: #9ca3af; + padding: 0; + transition: background 0.12s, color 0.12s; + + &:hover { background: #2a2a3d; color: #c7d2fe; } } .chevron-btn { diff --git a/web/src/app/shared/secondary-sidebar/secondary-sidebar.component.ts b/web/src/app/shared/secondary-sidebar/secondary-sidebar.component.ts index d46eb89..dbf4973 100644 --- a/web/src/app/shared/secondary-sidebar/secondary-sidebar.component.ts +++ b/web/src/app/shared/secondary-sidebar/secondary-sidebar.component.ts @@ -1,8 +1,8 @@ import { Component, Input, Output, EventEmitter } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Router } from '@angular/router'; -import { LucideAngularModule, ChevronRight, ChevronDown, PanelLeftClose, PanelLeftOpen, LucideIconData } from 'lucide-angular'; -import { TreeItem, SidebarAction, BottomPanel, BottomPanelItem, LayoutService } from '../../services/layout.service'; +import { LucideAngularModule, ChevronRight, ChevronDown, PanelLeftClose, PanelLeftOpen, Plus, FolderPlus, FilePlus, LucideIconData } from 'lucide-angular'; +import { TreeItem, TreeCreateAction, SidebarAction, BottomPanel, BottomPanelItem, LayoutService } from '../../services/layout.service'; import { resolveIcon } from '../../lore/lore-icons'; @Component({ @@ -25,6 +25,9 @@ export class SecondarySidebarComponent { readonly ChevronRight = ChevronRight; readonly PanelLeftClose = PanelLeftClose; readonly PanelLeftOpen = PanelLeftOpen; + readonly Plus = Plus; + readonly FolderPlus = FolderPlus; + readonly FilePlus = FilePlus; isCollapsed = false; @@ -82,6 +85,37 @@ export class SecondarySidebarComponent { return item.iconKey ? resolveIcon(item.iconKey) : null; } + /** Resolution d'icone pour un TreeCreateAction (hover + empty-state). */ + iconForAction(action: TreeCreateAction): LucideIconData { + switch (action.actionIcon) { + case 'folder-plus': return FolderPlus; + case 'file-plus': return FilePlus; + default: return Plus; + } + } + + /** + * Declenche une action de creation contextuelle. stopPropagation pour eviter + * que le clic ne remonte au bouton parent (qui navigue ou toggle). + */ + runCreateAction(event: Event, action: TreeCreateAction): void { + event.stopPropagation(); + this.router.navigate([action.route]); + } + + /** True si le noeud a au moins un vrai enfant (utile pour le chevron). */ + hasChildren(item: TreeItem): boolean { + return !!item.children && item.children.length > 0; + } + + /** + * True si le chevron doit s'afficher : soit il y a des enfants, soit le + * noeud a des createActions (dans ce cas deplier revele l'empty-state). + */ + isExpandable(item: TreeItem): boolean { + return this.hasChildren(item) || (item.createActions?.length ?? 0) > 0; + } + /** * Auto-déplie la chaîne d'ancêtres du item dont `route` matche l'URL active. * Nécessaire car la sidebar est détruite/recréée à chaque navigation (ngIf