Corrections d'ordre graphique / ergonomique :
- Lorsqu'on part de zéro : la création de dossier / page / template ce fait de manière plus fluide à la création d'un lore (par exemple création de page sans template et dossier : parcours facilité) - Ajout d'un bouton "+" dans le header templates - Harmonisation création / modification template Correction de tests unitaires
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
<aside class="secondary-sidebar" [class.collapsed]="isCollapsed">
|
||||
<aside class="secondary-sidebar"
|
||||
[class.collapsed]="isCollapsed"
|
||||
[style.width.px]="isCollapsed ? null : width">
|
||||
|
||||
<div class="collapse-toggle" (click)="toggleCollapse()">
|
||||
<lucide-icon [img]="isCollapsed ? PanelLeftOpen : PanelLeftClose" [size]="16"></lucide-icon>
|
||||
@@ -61,42 +63,38 @@
|
||||
[title]="a.label"
|
||||
[attr.aria-label]="a.label"
|
||||
(click)="runCreateAction($event, a)">
|
||||
<lucide-icon [img]="iconForAction(a)" [size]="12"></lucide-icon>
|
||||
<lucide-icon [img]="iconForAction(a)" [size]="16"></lucide-icon>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<div class="tree-children" *ngIf="isExpanded(item.id) && (hasChildren(item) || item.createActions?.length)">
|
||||
<div class="tree-children" *ngIf="isExpanded(item.id) && hasChildren(item)">
|
||||
<ng-container *ngFor="let child of item.children">
|
||||
<ng-container *ngTemplateOutlet="treeNode; context: { $implicit: child, level: level + 1 }"></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>
|
||||
</ng-template>
|
||||
|
||||
<!-- Panneau bas (ex: Templates) ------------------------------------ -->
|
||||
<section class="bottom-panel" *ngIf="bottomPanel">
|
||||
<button class="panel-header" (click)="togglePanel()">
|
||||
<span class="panel-title">{{ bottomPanel.title }}</span>
|
||||
<lucide-icon
|
||||
[img]="panelOpen ? ChevronDown : ChevronRight"
|
||||
[size]="14">
|
||||
</lucide-icon>
|
||||
</button>
|
||||
<div class="panel-header-row">
|
||||
<button class="panel-header" (click)="togglePanel()">
|
||||
<span class="panel-title">{{ bottomPanel.title }}</span>
|
||||
<lucide-icon
|
||||
[img]="panelOpen ? ChevronDown : ChevronRight"
|
||||
[size]="14">
|
||||
</lucide-icon>
|
||||
</button>
|
||||
<button
|
||||
*ngIf="bottomPanel.headerAction as action"
|
||||
type="button"
|
||||
class="panel-header-action"
|
||||
[title]="action.label"
|
||||
[attr.aria-label]="action.label"
|
||||
(click)="runPanelHeaderAction($event, action)">
|
||||
<lucide-icon [img]="Plus" [size]="14"></lucide-icon>
|
||||
</button>
|
||||
</div>
|
||||
<ul class="panel-list" *ngIf="panelOpen">
|
||||
<li *ngFor="let item of bottomPanel.items">
|
||||
<button
|
||||
@@ -112,4 +110,9 @@
|
||||
|
||||
</ng-container>
|
||||
|
||||
<!-- Poignée de redimensionnement sur le bord droit (masquée si replié) -->
|
||||
<div class="resize-handle"
|
||||
*ngIf="!isCollapsed"
|
||||
(mousedown)="startResize($event)"
|
||||
title="Glissez pour redimensionner"></div>
|
||||
</aside>
|
||||
|
||||
@@ -8,15 +8,33 @@
|
||||
padding: 1.25rem 0.75rem;
|
||||
gap: 0.75rem;
|
||||
overflow-y: auto;
|
||||
transition: width 0.25s ease;
|
||||
position: relative;
|
||||
// Pas de transition sur la largeur : sinon le drag de resize "traîne" derrière la souris.
|
||||
// L'animation d'expand/collapse est gérée uniquement par la classe .collapsed ci-dessous.
|
||||
|
||||
&.collapsed {
|
||||
width: 44px;
|
||||
width: 44px !important;
|
||||
padding: 1.25rem 0.5rem;
|
||||
overflow: hidden;
|
||||
transition: width 0.25s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.resize-handle {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: -3px;
|
||||
width: 6px;
|
||||
height: 100%;
|
||||
cursor: col-resize;
|
||||
z-index: 10;
|
||||
background: transparent;
|
||||
transition: background 0.15s ease;
|
||||
|
||||
&:hover,
|
||||
&:active { background: #6c63ff; }
|
||||
}
|
||||
|
||||
.collapse-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -142,7 +160,7 @@
|
||||
.node-actions {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.1rem;
|
||||
gap: 0.2rem;
|
||||
margin-left: auto;
|
||||
flex-shrink: 0;
|
||||
opacity: 0;
|
||||
@@ -160,17 +178,17 @@
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
background: transparent;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
background: rgba(55, 65, 81, 0.6);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
color: #9ca3af;
|
||||
color: #d1d5db;
|
||||
padding: 0;
|
||||
transition: background 0.12s, color 0.12s;
|
||||
|
||||
&:hover { background: #2a2a3d; color: #c7d2fe; }
|
||||
&:hover { background: #4338ca; color: #ffffff; }
|
||||
}
|
||||
|
||||
.chevron-btn {
|
||||
@@ -214,11 +232,36 @@
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.panel-header-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.panel-header-action {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
background: rgba(108, 99, 255, 0.15);
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
color: #c7d2fe;
|
||||
padding: 0;
|
||||
flex-shrink: 0;
|
||||
transition: background 0.12s, color 0.12s;
|
||||
|
||||
&:hover { background: #6c63ff; color: #ffffff; }
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #a5b4fc;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, Input, Output, EventEmitter } from '@angular/core';
|
||||
import { Component, Input, Output, EventEmitter, HostListener, OnDestroy, ElementRef } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Router } from '@angular/router';
|
||||
import { LucideAngularModule, ChevronRight, ChevronDown, PanelLeftClose, PanelLeftOpen, Plus, FolderPlus, FilePlus, LucideIconData } from 'lucide-angular';
|
||||
@@ -12,7 +12,7 @@ import { resolveIcon } from '../../lore/lore-icons';
|
||||
templateUrl: './secondary-sidebar.component.html',
|
||||
styleUrls: ['./secondary-sidebar.component.scss']
|
||||
})
|
||||
export class SecondarySidebarComponent {
|
||||
export class SecondarySidebarComponent implements OnDestroy {
|
||||
@Input() title = '';
|
||||
@Input() createActions: SidebarAction[] = [];
|
||||
@Input() bottomPanel: BottomPanel | null = null;
|
||||
@@ -31,6 +31,17 @@ export class SecondarySidebarComponent {
|
||||
|
||||
isCollapsed = false;
|
||||
|
||||
// --- Resize (étirement horizontal) -------------------------------------
|
||||
/** Clé localStorage pour persister la largeur choisie par l'utilisateur. */
|
||||
private static readonly WIDTH_STORAGE_KEY = 'secondary-sidebar-width';
|
||||
private static readonly MIN_WIDTH = 180;
|
||||
private static readonly MAX_WIDTH = 600;
|
||||
private static readonly DEFAULT_WIDTH = 220;
|
||||
|
||||
/** Largeur courante en px (bindée en [style.width.px]). */
|
||||
width = SecondarySidebarComponent.DEFAULT_WIDTH;
|
||||
private isResizing = false;
|
||||
|
||||
private _items: TreeItem[] = [];
|
||||
|
||||
@Input() set items(value: TreeItem[]) {
|
||||
@@ -39,7 +50,65 @@ export class SecondarySidebarComponent {
|
||||
}
|
||||
get items(): TreeItem[] { return this._items; }
|
||||
|
||||
constructor(private router: Router, private layoutService: LayoutService) {}
|
||||
constructor(
|
||||
private router: Router,
|
||||
private layoutService: LayoutService,
|
||||
private elementRef: ElementRef<HTMLElement>
|
||||
) {
|
||||
try {
|
||||
const stored = localStorage.getItem(SecondarySidebarComponent.WIDTH_STORAGE_KEY);
|
||||
const parsed = stored ? parseInt(stored, 10) : NaN;
|
||||
if (!isNaN(parsed)) {
|
||||
this.width = Math.min(
|
||||
Math.max(parsed, SecondarySidebarComponent.MIN_WIDTH),
|
||||
SecondarySidebarComponent.MAX_WIDTH
|
||||
);
|
||||
}
|
||||
} catch { /* storage indisponible : on garde la valeur par défaut */ }
|
||||
}
|
||||
|
||||
/** Début du resize — on active le flag et on désactive la sélection texte le temps du drag. */
|
||||
startResize(event: MouseEvent): void {
|
||||
if (this.isCollapsed) return;
|
||||
event.preventDefault();
|
||||
this.isResizing = true;
|
||||
document.body.style.userSelect = 'none';
|
||||
document.body.style.cursor = 'col-resize';
|
||||
}
|
||||
|
||||
@HostListener('document:mousemove', ['$event'])
|
||||
onResizeMove(event: MouseEvent): void {
|
||||
if (!this.isResizing) return;
|
||||
// La sidebar peut être précédée par la sidebar primaire : on calcule la largeur
|
||||
// cible à partir du bord gauche du composant, pas de la fenêtre. Sinon le
|
||||
// curseur et la poignée se désynchronisent.
|
||||
const rect = this.elementRef.nativeElement.getBoundingClientRect();
|
||||
const delta = event.clientX - rect.left;
|
||||
const next = Math.min(
|
||||
Math.max(delta, SecondarySidebarComponent.MIN_WIDTH),
|
||||
SecondarySidebarComponent.MAX_WIDTH
|
||||
);
|
||||
this.width = next;
|
||||
}
|
||||
|
||||
@HostListener('document:mouseup')
|
||||
onResizeEnd(): void {
|
||||
if (!this.isResizing) return;
|
||||
this.isResizing = false;
|
||||
document.body.style.userSelect = '';
|
||||
document.body.style.cursor = '';
|
||||
try {
|
||||
localStorage.setItem(SecondarySidebarComponent.WIDTH_STORAGE_KEY, String(this.width));
|
||||
} catch { /* storage indisponible : on ignore */ }
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
// Sécurité : si le composant est détruit en plein drag, on restaure le curseur global.
|
||||
if (this.isResizing) {
|
||||
document.body.style.userSelect = '';
|
||||
document.body.style.cursor = '';
|
||||
}
|
||||
}
|
||||
|
||||
runAction(action: SidebarAction): void {
|
||||
if (action.route) { this.router.navigate([action.route]); }
|
||||
@@ -80,6 +149,12 @@ export class SecondarySidebarComponent {
|
||||
if (item.route) { this.router.navigate([item.route]); }
|
||||
}
|
||||
|
||||
/** Clic sur le "+" du header : navigue sans toggler le panneau (stopPropagation). */
|
||||
runPanelHeaderAction(event: Event, action: { route: string }): void {
|
||||
event.stopPropagation();
|
||||
this.router.navigate([action.route]);
|
||||
}
|
||||
|
||||
/** Résout la clé d'icône d'un TreeItem en icône lucide pour le template. */
|
||||
iconFor(item: TreeItem): LucideIconData | null {
|
||||
return item.iconKey ? resolveIcon(item.iconKey) : null;
|
||||
@@ -108,12 +183,9 @@ export class SecondarySidebarComponent {
|
||||
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).
|
||||
*/
|
||||
/** True si le chevron doit s'afficher — seulement quand le noeud a de vrais enfants. */
|
||||
isExpandable(item: TreeItem): boolean {
|
||||
return this.hasChildren(item) || (item.createActions?.length ?? 0) > 0;
|
||||
return this.hasChildren(item);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user