Mise en place d'un bouton + au hover plutot qu'un affichage constant
This commit is contained in:
@@ -80,30 +80,30 @@ export function buildCampaignTree(campaignId: string, data: CampaignTreeData): T
|
|||||||
label: sc.name,
|
label: sc.name,
|
||||||
route: `/campaigns/${campaignId}/arcs/${arc.id}/chapters/${ch.id}/scenes/${sc.id}`
|
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 {
|
return {
|
||||||
id: `chapter-${ch.id}`,
|
id: `chapter-${ch.id}`,
|
||||||
label: ch.name,
|
label: ch.name,
|
||||||
children: sceneItems,
|
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 {
|
return {
|
||||||
id: `arc-${arc.id}`,
|
id: `arc-${arc.id}`,
|
||||||
label: arc.name,
|
label: arc.name,
|
||||||
children: chapterItems,
|
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'
|
||||||
|
}]
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,19 +75,7 @@ export function buildLoreSidebarConfig(data: LoreSidebarData): SecondarySidebarC
|
|||||||
id: `page-${p.id}`,
|
id: `page-${p.id}`,
|
||||||
label: p.title,
|
label: p.title,
|
||||||
route: `/lore/${lore.id}/pages/${p.id}`
|
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,
|
// 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
|
// 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,
|
iconKey: node.icon ?? undefined,
|
||||||
route: `/lore/${lore.id}/folders/${node.id}/edit`,
|
route: `/lore/${lore.id}/folders/${node.id}/edit`,
|
||||||
meta: nodePages.length > 0 ? String(nodePages.length) : undefined,
|
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'
|
||||||
|
}
|
||||||
|
]
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,21 @@ export interface TreeItem {
|
|||||||
iconKey?: string;
|
iconKey?: string;
|
||||||
/** Petit badge affiché à droite (ex: "3" pour compter les pages d'un dossier). */
|
/** Petit badge affiché à droite (ex: "3" pour compter les pages d'un dossier). */
|
||||||
meta?: string;
|
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 {
|
export interface GlobalItem {
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
<div class="tree-item" [style.padding-left.px]="level * 12">
|
<div class="tree-item" [style.padding-left.px]="level * 12">
|
||||||
<div class="tree-row">
|
<div class="tree-row">
|
||||||
<button
|
<button
|
||||||
*ngIf="!item.isAction && item.children?.length"
|
*ngIf="!item.isAction && isExpandable(item)"
|
||||||
type="button"
|
type="button"
|
||||||
class="chevron-btn"
|
class="chevron-btn"
|
||||||
(click)="clickChevron($event, item)">
|
(click)="clickChevron($event, item)">
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
[size]="12">
|
[size]="12">
|
||||||
</lucide-icon>
|
</lucide-icon>
|
||||||
</button>
|
</button>
|
||||||
<span *ngIf="item.isAction || !item.children?.length" class="chevron-spacer"></span>
|
<span *ngIf="item.isAction || !isExpandable(item)" class="chevron-spacer"></span>
|
||||||
|
|
||||||
<button type="button" class="tree-btn" [class.action]="item.isAction" (click)="clickItem(item)">
|
<button type="button" class="tree-btn" [class.action]="item.isAction" (click)="clickItem(item)">
|
||||||
<lucide-icon
|
<lucide-icon
|
||||||
@@ -51,11 +51,39 @@
|
|||||||
{{ item.label }}
|
{{ item.label }}
|
||||||
<span class="tree-item-meta" *ngIf="!item.isAction && item.meta">{{ item.meta }}</span>
|
<span class="tree-item-meta" *ngIf="!item.isAction && item.meta">{{ item.meta }}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<!-- Actions de creation contextuelles, revelees au survol de la ligne -->
|
||||||
|
<span class="node-actions" *ngIf="item.createActions?.length">
|
||||||
|
<button
|
||||||
|
*ngFor="let a of item.createActions"
|
||||||
|
type="button"
|
||||||
|
class="node-action-btn"
|
||||||
|
[title]="a.label"
|
||||||
|
[attr.aria-label]="a.label"
|
||||||
|
(click)="runCreateAction($event, a)">
|
||||||
|
<lucide-icon [img]="iconForAction(a)" [size]="12"></lucide-icon>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="tree-children" *ngIf="isExpanded(item.id) && item.children?.length">
|
<div class="tree-children" *ngIf="isExpanded(item.id) && (hasChildren(item) || item.createActions?.length)">
|
||||||
<ng-container *ngFor="let child of item.children">
|
<ng-container *ngFor="let child of item.children">
|
||||||
<ng-container *ngTemplateOutlet="treeNode; context: { $implicit: child, level: level + 1 }"></ng-container>
|
<ng-container *ngTemplateOutlet="treeNode; context: { $implicit: child, level: level + 1 }"></ng-container>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
<!-- Empty-state inline : createActions affichees en pleine largeur
|
||||||
|
UNIQUEMENT si le noeud n'a aucun vrai enfant (sinon le hover-reveal
|
||||||
|
sur le parent suffit, pas de pollution visuelle). -->
|
||||||
|
<ng-container *ngIf="!hasChildren(item) && item.createActions?.length">
|
||||||
|
<div class="tree-item empty-action" *ngFor="let a of item.createActions"
|
||||||
|
[style.padding-left.px]="(level + 1) * 12">
|
||||||
|
<div class="tree-row">
|
||||||
|
<span class="chevron-spacer"></span>
|
||||||
|
<button type="button" class="tree-btn action" (click)="runCreateAction($event, a)">
|
||||||
|
<lucide-icon [img]="iconForAction(a)" [size]="12" class="item-icon"></lucide-icon>
|
||||||
|
+ {{ a.label }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|||||||
@@ -132,6 +132,45 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.15rem;
|
gap: 0.15rem;
|
||||||
width: 100%;
|
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 {
|
.chevron-btn {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Component, Input, Output, EventEmitter } from '@angular/core';
|
import { Component, Input, Output, EventEmitter } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { LucideAngularModule, ChevronRight, ChevronDown, PanelLeftClose, PanelLeftOpen, LucideIconData } from 'lucide-angular';
|
import { LucideAngularModule, ChevronRight, ChevronDown, PanelLeftClose, PanelLeftOpen, Plus, FolderPlus, FilePlus, LucideIconData } from 'lucide-angular';
|
||||||
import { TreeItem, SidebarAction, BottomPanel, BottomPanelItem, LayoutService } from '../../services/layout.service';
|
import { TreeItem, TreeCreateAction, SidebarAction, BottomPanel, BottomPanelItem, LayoutService } from '../../services/layout.service';
|
||||||
import { resolveIcon } from '../../lore/lore-icons';
|
import { resolveIcon } from '../../lore/lore-icons';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -25,6 +25,9 @@ export class SecondarySidebarComponent {
|
|||||||
readonly ChevronRight = ChevronRight;
|
readonly ChevronRight = ChevronRight;
|
||||||
readonly PanelLeftClose = PanelLeftClose;
|
readonly PanelLeftClose = PanelLeftClose;
|
||||||
readonly PanelLeftOpen = PanelLeftOpen;
|
readonly PanelLeftOpen = PanelLeftOpen;
|
||||||
|
readonly Plus = Plus;
|
||||||
|
readonly FolderPlus = FolderPlus;
|
||||||
|
readonly FilePlus = FilePlus;
|
||||||
|
|
||||||
isCollapsed = false;
|
isCollapsed = false;
|
||||||
|
|
||||||
@@ -82,6 +85,37 @@ export class SecondarySidebarComponent {
|
|||||||
return item.iconKey ? resolveIcon(item.iconKey) : null;
|
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.
|
* 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
|
* Nécessaire car la sidebar est détruite/recréée à chaque navigation (ngIf
|
||||||
|
|||||||
Reference in New Issue
Block a user