Amélioration de l'UI : meilleur affichage des images que ce soit dans la partie lore ou la partie campagne (partie campagne : visualisation scrapbooking). Possibilité de réordonner les champs dans les templates...
Passage v0.3.0
This commit is contained in:
@@ -1,29 +1,183 @@
|
||||
<!-- Grille de vignettes + uploader si editable. -->
|
||||
<div class="gallery"
|
||||
*ngIf="imageIds.length > 0 || editable; else empty">
|
||||
<!-- Container avec classe dynamique selon le layout choisi. -->
|
||||
<div [ngSwitch]="effectiveLayout" class="gallery-root">
|
||||
|
||||
<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" />
|
||||
<!-- =================== HERO =================== -->
|
||||
<ng-container *ngSwitchCase="'HERO'">
|
||||
<div class="hero" *ngIf="imageIds.length > 0 || editable; else empty">
|
||||
<div class="hero-main"
|
||||
*ngIf="heroId"
|
||||
(click)="openLightbox(heroId)"
|
||||
role="button"
|
||||
tabindex="0">
|
||||
<img [src]="urlFor(heroId)" [alt]="'Illustration principale'" loading="lazy" />
|
||||
<button type="button"
|
||||
class="gallery-remove"
|
||||
*ngIf="editable"
|
||||
(click)="remove(heroId, $event)"
|
||||
aria-label="Retirer cette image">
|
||||
<lucide-icon [img]="X" [size]="14"></lucide-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<div class="hero-rest" *ngIf="restIds.length > 0 || editable">
|
||||
<div class="gallery-tile hero-thumb"
|
||||
*ngFor="let id of restIds"
|
||||
(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>
|
||||
<app-image-uploader *ngIf="editable" [compact]="true" (uploaded)="onUploaded($event)"></app-image-uploader>
|
||||
</div>
|
||||
|
||||
<!-- Si pas de hero mais editable, on montre au moins l'uploader. -->
|
||||
<div class="hero-rest" *ngIf="!heroId && editable">
|
||||
<app-image-uploader [compact]="true" (uploaded)="onUploaded($event)"></app-image-uploader>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<!-- =================== MASONRY =================== -->
|
||||
<ng-container *ngSwitchCase="'MASONRY'">
|
||||
<div class="masonry" *ngIf="imageIds.length > 0 || editable; else empty">
|
||||
<div class="masonry-item"
|
||||
*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>
|
||||
<div class="masonry-item masonry-uploader" *ngIf="editable">
|
||||
<app-image-uploader [compact]="true" (uploaded)="onUploaded($event)"></app-image-uploader>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<!-- =================== CAROUSEL =================== -->
|
||||
<ng-container *ngSwitchCase="'CAROUSEL'">
|
||||
<div class="carousel" *ngIf="imageIds.length > 0 || editable; else empty">
|
||||
<button type="button"
|
||||
class="carousel-nav carousel-prev"
|
||||
*ngIf="imageIds.length > 1"
|
||||
(click)="scrollCarousel(-1)"
|
||||
aria-label="Precedent">
|
||||
<lucide-icon [img]="ChevronLeft" [size]="20"></lucide-icon>
|
||||
</button>
|
||||
|
||||
<div class="carousel-track" #carouselTrack>
|
||||
<div class="carousel-slide"
|
||||
*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>
|
||||
<div class="carousel-slide carousel-uploader" *ngIf="editable">
|
||||
<app-image-uploader [compact]="true" (uploaded)="onUploaded($event)"></app-image-uploader>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="button"
|
||||
class="carousel-nav carousel-next"
|
||||
*ngIf="imageIds.length > 1"
|
||||
(click)="scrollCarousel(1)"
|
||||
aria-label="Suivant">
|
||||
<lucide-icon [img]="ChevronRight" [size]="20"></lucide-icon>
|
||||
</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<!-- =================== EDITORIAL =================== -->
|
||||
<!-- Rendu adaptatif facon magazine : 1 image → hero, 2 → diptyque, 3 → feature + 2 satellites, 4+ → feature + 3 satellites. -->
|
||||
<ng-container *ngSwitchCase="'EDITORIAL'">
|
||||
<div class="editorial" *ngIf="imageIds.length > 0 || editable; else empty">
|
||||
<div class="editorial-item"
|
||||
*ngFor="let id of imageIds; let i = index"
|
||||
[class.editorial-feature]="i === 0"
|
||||
(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>
|
||||
<div class="editorial-item editorial-uploader" *ngIf="editable">
|
||||
<app-image-uploader [compact]="true" (uploaded)="onUploaded($event)"></app-image-uploader>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<!-- =================== MAPS =================== -->
|
||||
<!-- Cartes / plans : grandes vignettes, ratio natif preserve (pas de crop). -->
|
||||
<ng-container *ngSwitchCase="'MAPS'">
|
||||
<div class="maps" *ngIf="imageIds.length > 0 || editable; else empty">
|
||||
<div class="map-tile"
|
||||
*ngFor="let id of imageIds"
|
||||
(click)="openLightbox(id)"
|
||||
role="button"
|
||||
tabindex="0">
|
||||
<img [src]="urlFor(id)" [alt]="'Carte ' + id" loading="lazy" />
|
||||
<button type="button"
|
||||
class="gallery-remove"
|
||||
*ngIf="editable"
|
||||
(click)="remove(id, $event)"
|
||||
aria-label="Retirer cette carte">
|
||||
<lucide-icon [img]="X" [size]="14"></lucide-icon>
|
||||
</button>
|
||||
</div>
|
||||
<div class="map-tile map-uploader" *ngIf="editable">
|
||||
<app-image-uploader [compact]="true" (uploaded)="onUploaded($event)"></app-image-uploader>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<!-- =================== GALLERY (default) =================== -->
|
||||
<ng-container *ngSwitchDefault>
|
||||
<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>
|
||||
<app-image-uploader *ngIf="editable" [compact]="true" (uploaded)="onUploaded($event)"></app-image-uploader>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<!-- 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). -->
|
||||
|
||||
@@ -1,33 +1,36 @@
|
||||
.gallery {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.8rem;
|
||||
align-items: flex-start;
|
||||
}
|
||||
// =============== Common tile / remove-button ===============
|
||||
// Partage par tous les layouts : vignette, survol, bouton X.
|
||||
|
||||
.gallery-tile {
|
||||
.gallery-tile,
|
||||
.masonry-item,
|
||||
.hero-thumb,
|
||||
.carousel-slide,
|
||||
.hero-main,
|
||||
.map-tile {
|
||||
position: relative;
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border-radius: 6px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: #1a1a2e;
|
||||
border: 1px solid #2a2a3d;
|
||||
cursor: zoom-in;
|
||||
transition: border-color 0.15s, transform 0.15s;
|
||||
transition: border-color 0.15s, transform 0.15s, box-shadow 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: #6c63ff;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(108, 99, 255, 0.18);
|
||||
|
||||
.gallery-remove { opacity: 1; }
|
||||
|
||||
img { transform: scale(1.04); }
|
||||
}
|
||||
|
||||
img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +50,7 @@
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s, background 0.15s;
|
||||
z-index: 2;
|
||||
|
||||
&:hover { background: #7f1d1d; color: white; }
|
||||
}
|
||||
@@ -60,7 +64,352 @@
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
// Lightbox plein ecran
|
||||
// =============== Layout: GALLERY (planche de contact) ===============
|
||||
// Grille stricte de carres identiques, effet "contact sheet" photo.
|
||||
.gallery {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
|
||||
gap: 0.35rem;
|
||||
padding: 0.35rem;
|
||||
background: #12121f;
|
||||
border-radius: 6px;
|
||||
max-width: 720px; // contient la grille pour ne pas etaler sur tout l'ecran
|
||||
}
|
||||
|
||||
.gallery-tile {
|
||||
width: auto;
|
||||
aspect-ratio: 1 / 1;
|
||||
border-radius: 2px; // carres vifs, presque sans radius
|
||||
}
|
||||
|
||||
// =============== Layout: HERO ===============
|
||||
.hero {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
.hero-main {
|
||||
width: 100%;
|
||||
aspect-ratio: 21 / 9;
|
||||
max-height: 360px;
|
||||
cursor: zoom-in;
|
||||
|
||||
img { object-position: center; }
|
||||
}
|
||||
|
||||
.hero-rest {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.6rem;
|
||||
}
|
||||
|
||||
.hero-thumb {
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
|
||||
// =============== Layout: MASONRY (Pinterest) ===============
|
||||
// Colonnes larges, hauteurs naturelles preservees. Effet tres visible si les
|
||||
// images n'ont pas toutes le meme ratio. Le border-radius genereux et les
|
||||
// ombres accentuent le cote "tableau d'inspiration".
|
||||
.masonry {
|
||||
column-count: 3;
|
||||
column-gap: 1.2rem;
|
||||
padding: 0.5rem 0;
|
||||
|
||||
@media (max-width: 900px) { column-count: 2; }
|
||||
@media (max-width: 500px) { column-count: 1; }
|
||||
}
|
||||
|
||||
.masonry-item {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
margin-bottom: 1.2rem;
|
||||
break-inside: avoid;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.45);
|
||||
|
||||
// Override de la transition par defaut pour un feel plus doux.
|
||||
transition: transform 0.25s ease, box-shadow 0.25s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 12px 32px rgba(108, 99, 255, 0.35);
|
||||
}
|
||||
|
||||
img {
|
||||
height: auto; // ratio natif preserve → hauteur variable entre les tuiles
|
||||
border-radius: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.masonry-uploader {
|
||||
aspect-ratio: 3 / 4; // slot vertical, bien different d'une tuile simple
|
||||
border-style: dashed;
|
||||
border-width: 2px;
|
||||
cursor: default;
|
||||
box-shadow: none;
|
||||
|
||||
&:hover { transform: none; box-shadow: none; }
|
||||
}
|
||||
|
||||
// =============== Layout: CAROUSEL (cinema) ===============
|
||||
// Bande horizontale facon affiche de film : grandes slides 16/9, ombres
|
||||
// marquees, fade sur les bords pour suggerer le defilement infini.
|
||||
.carousel {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
padding: 0.5rem 0;
|
||||
|
||||
// Fade gauche/droite pour signaler clairement "ca defile".
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 48px;
|
||||
pointer-events: none;
|
||||
z-index: 3;
|
||||
}
|
||||
&::before { left: 48px; background: linear-gradient(to right, #0f0f1e 0%, transparent 100%); }
|
||||
&::after { right: 48px; background: linear-gradient(to left, #0f0f1e 0%, transparent 100%); }
|
||||
}
|
||||
|
||||
.carousel-track {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
overflow-x: auto;
|
||||
scroll-behavior: smooth;
|
||||
scroll-snap-type: x mandatory;
|
||||
padding: 0.5rem 0.25rem;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
&::-webkit-scrollbar { height: 6px; }
|
||||
&::-webkit-scrollbar-track { background: transparent; }
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: #2a2a3d;
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.carousel-slide {
|
||||
flex: 0 0 auto;
|
||||
width: 360px;
|
||||
aspect-ratio: 16 / 9;
|
||||
height: auto;
|
||||
scroll-snap-align: start;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.55);
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-4px) scale(1.02);
|
||||
box-shadow: 0 16px 40px rgba(108, 99, 255, 0.4);
|
||||
}
|
||||
}
|
||||
|
||||
.carousel-uploader {
|
||||
width: 220px;
|
||||
aspect-ratio: 16 / 9;
|
||||
border-style: dashed;
|
||||
border-width: 2px;
|
||||
cursor: default;
|
||||
box-shadow: none;
|
||||
|
||||
&:hover { transform: none; box-shadow: none; }
|
||||
}
|
||||
|
||||
.carousel-nav {
|
||||
flex: 0 0 auto;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid #2a2a3d;
|
||||
border-radius: 50%;
|
||||
background: rgba(26, 26, 46, 0.9);
|
||||
color: #9ca3af;
|
||||
cursor: pointer;
|
||||
transition: color 0.15s, border-color 0.15s, background 0.15s;
|
||||
|
||||
&:hover {
|
||||
color: white;
|
||||
border-color: #6c63ff;
|
||||
background: #1f1b3a;
|
||||
}
|
||||
}
|
||||
|
||||
// =============== Layout: EDITORIAL (scrapbook polaroid) ===============
|
||||
// Rendu carnet de campagne : vignettes facon polaroid, legerement inclinees,
|
||||
// avec bande de papier collant (::before) et ombre portee. Au survol, la photo
|
||||
// se redresse et se souleve. Pas de grille rigide : flex-wrap laisse respirer.
|
||||
.editorial {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 2rem 1.25rem;
|
||||
padding: 1.25rem 0.5rem 0.5rem;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
// Fond kraft/parchemin tres discret pour suggerer le carnet.
|
||||
background:
|
||||
radial-gradient(ellipse at 20% 20%, rgba(180, 150, 100, 0.05), transparent 60%),
|
||||
radial-gradient(ellipse at 80% 70%, rgba(160, 120, 80, 0.04), transparent 60%);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.editorial-item {
|
||||
position: relative;
|
||||
flex: 0 0 220px;
|
||||
max-width: 100%;
|
||||
background: #f5efe0; // papier blanc casse
|
||||
padding: 10px 10px 34px 10px; // bas = bande blanche facon polaroid
|
||||
border-radius: 2px;
|
||||
cursor: zoom-in;
|
||||
box-shadow:
|
||||
0 2px 4px rgba(0, 0, 0, 0.35),
|
||||
0 14px 28px rgba(0, 0, 0, 0.45);
|
||||
transition: transform 0.3s cubic-bezier(0.2, 0.8, 0.3, 1),
|
||||
box-shadow 0.3s ease;
|
||||
|
||||
// Rotations pseudo-aleatoires pour casser l'effet grille.
|
||||
transform: rotate(-2deg);
|
||||
&:nth-child(2n) { transform: rotate(1.8deg); }
|
||||
&:nth-child(3n) { transform: rotate(-1.2deg); }
|
||||
&:nth-child(4n) { transform: rotate(2.5deg); }
|
||||
&:nth-child(5n) { transform: rotate(-2.8deg); }
|
||||
&:nth-child(7n) { transform: rotate(0.9deg); }
|
||||
|
||||
// Ruban adhesif en haut de la photo.
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -9px;
|
||||
left: 50%;
|
||||
width: 68px;
|
||||
height: 18px;
|
||||
transform: translateX(-50%) rotate(-4deg);
|
||||
background: rgba(255, 238, 200, 0.55);
|
||||
border-left: 1px dashed rgba(0, 0, 0, 0.08);
|
||||
border-right: 1px dashed rgba(0, 0, 0, 0.08);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.12);
|
||||
pointer-events: none;
|
||||
}
|
||||
&:nth-child(2n)::before { transform: translateX(-50%) rotate(3deg); }
|
||||
&:nth-child(3n)::before { transform: translateX(-50%) rotate(-7deg); left: 58%; }
|
||||
&:nth-child(4n)::before { transform: translateX(-50%) rotate(5deg); left: 42%; }
|
||||
|
||||
&:hover {
|
||||
transform: rotate(0deg) scale(1.05) translateY(-4px);
|
||||
z-index: 10;
|
||||
box-shadow:
|
||||
0 4px 8px rgba(0, 0, 0, 0.5),
|
||||
0 24px 48px rgba(0, 0, 0, 0.6);
|
||||
|
||||
.gallery-remove { opacity: 1; }
|
||||
}
|
||||
|
||||
img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
aspect-ratio: 1 / 1;
|
||||
object-fit: cover;
|
||||
background: #1a1a2e;
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// La premiere image (feature) est plus grande et en ratio 4/3 pour jouer le role d'affiche.
|
||||
.editorial-feature {
|
||||
flex: 0 0 420px;
|
||||
|
||||
img { aspect-ratio: 4 / 3; }
|
||||
}
|
||||
|
||||
// Bouton X : sur polaroid blanc, on renforce le contraste.
|
||||
.editorial-item .gallery-remove {
|
||||
top: 14px;
|
||||
right: 14px;
|
||||
background: rgba(17, 17, 30, 0.92);
|
||||
color: #fecaca;
|
||||
|
||||
&:hover { background: #7f1d1d; color: white; }
|
||||
}
|
||||
|
||||
// Uploader : meme cadre polaroid mais en "coller une photo ici" dashed.
|
||||
.editorial-uploader {
|
||||
background: rgba(245, 239, 224, 0.06);
|
||||
border: 2px dashed rgba(108, 99, 255, 0.7);
|
||||
padding: 0;
|
||||
cursor: default;
|
||||
|
||||
&::before { display: none; } // pas de scotch sur le slot vide
|
||||
&:hover {
|
||||
transform: rotate(0deg);
|
||||
box-shadow:
|
||||
0 2px 4px rgba(0, 0, 0, 0.35),
|
||||
0 14px 28px rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
// L'uploader interne doit remplir le slot.
|
||||
app-image-uploader { display: block; width: 100%; height: 100%; min-height: 180px; }
|
||||
}
|
||||
|
||||
// Responsive : on reduit la taille et on supprime les rotations sur mobile.
|
||||
@media (max-width: 780px) {
|
||||
.editorial-item { flex: 0 0 calc(50% - 0.75rem); }
|
||||
.editorial-feature { flex: 0 0 calc(100% - 0.5rem); }
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.editorial { gap: 1.25rem 0.75rem; }
|
||||
.editorial-item,
|
||||
.editorial-feature {
|
||||
flex: 0 0 100%;
|
||||
transform: rotate(0deg) !important;
|
||||
&::before { display: none; }
|
||||
}
|
||||
}
|
||||
|
||||
// =============== Layout: MAPS ===============
|
||||
// Plans et cartes : on ne CROP pas (une carte croppee ne sert a rien).
|
||||
// Grandes vignettes, ratio natif preserve via object-fit: contain.
|
||||
.maps {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.map-tile {
|
||||
aspect-ratio: 4 / 3;
|
||||
background:
|
||||
linear-gradient(45deg, #15152440 25%, transparent 25%),
|
||||
linear-gradient(-45deg, #15152440 25%, transparent 25%),
|
||||
linear-gradient(45deg, transparent 75%, #15152440 75%),
|
||||
linear-gradient(-45deg, transparent 75%, #15152440 75%),
|
||||
#1a1a2e;
|
||||
background-size: 20px 20px;
|
||||
background-position: 0 0, 0 10px, 10px -10px, -10px 0;
|
||||
|
||||
img {
|
||||
object-fit: contain; // Preserve le ratio natif, ajoute un padding visuel via le fond.
|
||||
padding: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.map-uploader {
|
||||
border-style: dashed;
|
||||
cursor: default;
|
||||
background: #1a1a2e;
|
||||
|
||||
&:hover { transform: none; box-shadow: none; }
|
||||
}
|
||||
|
||||
// =============== Lightbox (inchange) ===============
|
||||
.lightbox-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { LucideAngularModule, X, Image as ImageIcon } from 'lucide-angular';
|
||||
import { LucideAngularModule, X, Image as ImageIcon, ChevronLeft, ChevronRight } from 'lucide-angular';
|
||||
import { ImageService } from '../../services/image.service';
|
||||
import { Image } from '../../services/image.model';
|
||||
import { ImageLayout } from '../../services/template.model';
|
||||
import { ImageUploaderComponent } from '../image-uploader/image-uploader.component';
|
||||
|
||||
/**
|
||||
@@ -34,6 +35,8 @@ import { ImageUploaderComponent } from '../image-uploader/image-uploader.compone
|
||||
export class ImageGalleryComponent {
|
||||
readonly X = X;
|
||||
readonly ImageIcon = ImageIcon;
|
||||
readonly ChevronLeft = ChevronLeft;
|
||||
readonly ChevronRight = ChevronRight;
|
||||
|
||||
/** IDs d'images a afficher. */
|
||||
@Input() imageIds: string[] = [];
|
||||
@@ -41,14 +44,45 @@ export class ImageGalleryComponent {
|
||||
/** Mode edition : afficher le bouton d'ajout + les boutons de suppression. */
|
||||
@Input() editable = false;
|
||||
|
||||
/**
|
||||
* Variante de mise en page. Null/undefined = GALLERY (rendu historique).
|
||||
* HERO : premiere image en banniere pleine largeur, suivantes en petit dessous.
|
||||
* MASONRY : mosaique a hauteurs variables.
|
||||
* CAROUSEL : defilement horizontal avec fleches.
|
||||
*/
|
||||
@Input() layout: ImageLayout | null | undefined = 'GALLERY';
|
||||
|
||||
/** 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;
|
||||
|
||||
@ViewChild('carouselTrack') carouselTrack?: ElementRef<HTMLDivElement>;
|
||||
|
||||
constructor(private imageService: ImageService) {}
|
||||
|
||||
/** Layout effectif (null → GALLERY). */
|
||||
get effectiveLayout(): ImageLayout {
|
||||
return this.layout ?? 'GALLERY';
|
||||
}
|
||||
|
||||
/** Premiere image (pour le layout HERO). */
|
||||
get heroId(): string | null {
|
||||
return this.imageIds[0] ?? null;
|
||||
}
|
||||
|
||||
/** Images restantes apres la hero (pour le layout HERO). */
|
||||
get restIds(): string[] {
|
||||
return this.imageIds.slice(1);
|
||||
}
|
||||
|
||||
scrollCarousel(direction: -1 | 1): void {
|
||||
const el = this.carouselTrack?.nativeElement;
|
||||
if (!el) return;
|
||||
el.scrollBy({ left: direction * Math.max(240, el.clientWidth * 0.8), behavior: 'smooth' });
|
||||
}
|
||||
|
||||
/** URL absolue du binaire d'une image. */
|
||||
urlFor(id: string): string {
|
||||
return this.imageService.contentUrl(id);
|
||||
|
||||
Reference in New Issue
Block a user