Mise en ligne de la version 0.2.0
All checks were successful
Build & Push Images / build (brain) (push) Successful in 46s
Build & Push Images / build (core) (push) Successful in 1m21s
Build & Push Images / build (web) (push) Successful in 1m25s

This commit is contained in:
2026-04-21 14:25:17 +02:00
parent ebee8e106b
commit ba8a503b3e
300 changed files with 35329 additions and 1 deletions

View File

@@ -0,0 +1,47 @@
<!-- Grille de vignettes + uploader si editable. -->
<div class="gallery"
*ngIf="imageIds.length > 0 || editable; else empty">
<div class="gallery-tile"
*ngFor="let id of imageIds"
(click)="openLightbox(id)"
role="button"
tabindex="0">
<img [src]="urlFor(id)" [alt]="'Illustration ' + id" loading="lazy" />
<button type="button"
class="gallery-remove"
*ngIf="editable"
(click)="remove(id, $event)"
aria-label="Retirer cette image">
<lucide-icon [img]="X" [size]="14"></lucide-icon>
</button>
</div>
<!-- Bouton + (uploader compact), uniquement en mode edition -->
<app-image-uploader
*ngIf="editable"
[compact]="true"
(uploaded)="onUploaded($event)">
</app-image-uploader>
</div>
<!-- Etat vide (lecture uniquement). -->
<ng-template #empty>
<div class="gallery-empty">
<lucide-icon [img]="ImageIcon" [size]="20"></lucide-icon>
<span>Aucune illustration</span>
</div>
</ng-template>
<!-- Lightbox : image plein ecran sur fond noir, clic pour fermer. -->
<div class="lightbox-backdrop"
*ngIf="lightboxId"
(click)="closeLightbox()"
role="dialog"
aria-label="Image en plein ecran">
<img [src]="urlFor(lightboxId)" alt="Image agrandie" class="lightbox-image" />
<button type="button" class="lightbox-close" (click)="closeLightbox()" aria-label="Fermer">
<lucide-icon [img]="X" [size]="24"></lucide-icon>
</button>
</div>

View File

@@ -0,0 +1,107 @@
.gallery {
display: flex;
flex-wrap: wrap;
gap: 0.8rem;
align-items: flex-start;
}
.gallery-tile {
position: relative;
width: 120px;
height: 120px;
border-radius: 6px;
overflow: hidden;
background: #1a1a2e;
border: 1px solid #2a2a3d;
cursor: zoom-in;
transition: border-color 0.15s, transform 0.15s;
&:hover {
border-color: #6c63ff;
transform: translateY(-2px);
.gallery-remove { opacity: 1; }
}
img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
}
.gallery-remove {
position: absolute;
top: 6px;
right: 6px;
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border: none;
border-radius: 50%;
background: rgba(17, 17, 30, 0.85);
color: #fca5a5;
cursor: pointer;
opacity: 0;
transition: opacity 0.15s, background 0.15s;
&:hover { background: #7f1d1d; color: white; }
}
.gallery-empty {
display: inline-flex;
align-items: center;
gap: 0.5rem;
color: #4b5563;
font-size: 0.85rem;
font-style: italic;
}
// Lightbox plein ecran
.lightbox-backdrop {
position: fixed;
inset: 0;
z-index: 1000;
background: rgba(0, 0, 0, 0.88);
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
cursor: zoom-out;
animation: fade-in 0.15s ease-out;
}
.lightbox-image {
max-width: 95vw;
max-height: 90vh;
object-fit: contain;
border-radius: 4px;
box-shadow: 0 8px 40px rgba(0, 0, 0, 0.6);
}
.lightbox-close {
position: fixed;
top: 1rem;
right: 1rem;
width: 40px;
height: 40px;
display: inline-flex;
align-items: center;
justify-content: center;
border: none;
border-radius: 50%;
background: rgba(30, 30, 60, 0.8);
color: white;
cursor: pointer;
transition: background 0.15s;
&:hover { background: #6c63ff; }
}
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}

View File

@@ -0,0 +1,76 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { CommonModule } from '@angular/common';
import { LucideAngularModule, X, Image as ImageIcon } from 'lucide-angular';
import { ImageService } from '../../services/image.service';
import { Image } from '../../services/image.model';
import { ImageUploaderComponent } from '../image-uploader/image-uploader.component';
/**
* Composant reutilisable de galerie d'images.
*
* Deux modes :
* - editable=false (defaut) : affichage en grille, clic sur une image ouvre
* un lightbox plein ecran pour zoomer.
* - editable=true : bouton "+ ajouter" en fin de grille (via app-image-uploader),
* chaque vignette a un bouton "X" pour la retirer.
*
* La galerie raisonne sur une liste d'IDs d'images (string[]). Elle ne stocke
* pas les objets Image eux-memes : les thumbs utilisent `imageService.contentUrl(id)`
* directement comme src. Le navigateur cache les binaires via Cache-Control immutable
* pose par le backend, donc aucune requete redondante.
*
* Usage :
* <app-image-gallery [imageIds]="scene.illustrationImageIds"></app-image-gallery>
* <app-image-gallery [imageIds]="tempIds" [editable]="true"
* (imageIdsChange)="tempIds = $event"></app-image-gallery>
*/
@Component({
selector: 'app-image-gallery',
standalone: true,
imports: [CommonModule, LucideAngularModule, ImageUploaderComponent],
templateUrl: './image-gallery.component.html',
styleUrls: ['./image-gallery.component.scss']
})
export class ImageGalleryComponent {
readonly X = X;
readonly ImageIcon = ImageIcon;
/** IDs d'images a afficher. */
@Input() imageIds: string[] = [];
/** Mode edition : afficher le bouton d'ajout + les boutons de suppression. */
@Input() editable = false;
/** Emet la nouvelle liste quand l'utilisateur ajoute/retire une image. */
@Output() imageIdsChange = new EventEmitter<string[]>();
/** ID de l'image actuellement ouverte en lightbox (null = ferme). */
lightboxId: string | null = null;
constructor(private imageService: ImageService) {}
/** URL absolue du binaire d'une image. */
urlFor(id: string): string {
return this.imageService.contentUrl(id);
}
onUploaded(image: Image): void {
this.imageIdsChange.emit([...this.imageIds, image.id]);
}
remove(id: string, event: MouseEvent): void {
event.stopPropagation(); // Evite d'ouvrir le lightbox en cliquant sur X.
// On supprime aussi cote serveur pour ne pas laisser d'image orpheline.
// Best-effort : on n'attend pas le retour pour emettre la nouvelle liste.
this.imageService.delete(id).subscribe({ error: () => {} });
this.imageIdsChange.emit(this.imageIds.filter(i => i !== id));
}
openLightbox(id: string): void {
this.lightboxId = id;
}
closeLightbox(): void {
this.lightboxId = null;
}
}