Add persistent gallery card ratio switch
All checks were successful
Deploy Sokko G / deploy (push) Successful in 18s

This commit is contained in:
Shinuwa 2026-09-28 10:09:58 +02:00
parent bd4059c9c1
commit 3f86d863ac
10 changed files with 72 additions and 10 deletions

View file

@ -2049,6 +2049,9 @@
"galleryZoomLabel": "Taille des aperçus",
"galleryZoomOutTitle": "Réduire les aperçus",
"galleryZoomInTitle": "Agrandir les aperçus",
"galleryRatioLabel": "Format des cartes",
"galleryRatioSquareTitle": "Passer au format 1:1",
"galleryRatioLandscapeTitle": "Passer au format 4:3",
"previousImageTitle": "Image précédente",
"nextImageTitle": "Image suivante",
"positionLabel": "Image {current} sur {total}",

View file

@ -1,7 +1,7 @@
// Rôle : affiche la collection d'un outil Images dans la galerie de la visionneuse.
import { useEffect, useRef } from "react";
export function ImageGallery({ images, activeIndex, focusIndex = -1, thumbnailWidth = 260, onSelect, textContent = {} }) {
export function ImageGallery({ images, activeIndex, focusIndex = -1, thumbnailWidth = 260, cardRatio = "4:3", onSelect, textContent = {} }) {
const galleryRef = useRef(null);
useEffect(() => {
@ -13,7 +13,7 @@ export function ImageGallery({ images, activeIndex, focusIndex = -1, thumbnailWi
}, [focusIndex]);
return (
<div ref={galleryRef} className="image-viewer-gallery themed-scrollbar" style={{ "--image-gallery-thumbnail-width": `${thumbnailWidth}px` }}>
<div ref={galleryRef} className="image-viewer-gallery themed-scrollbar" data-card-ratio={cardRatio} style={{ "--image-gallery-thumbnail-width": `${thumbnailWidth}px` }}>
{images.map((image, index) => {
const position = `${index + 1} / ${images.length}`;
const label = image.label || `${textContent.imageAlt || "Image"} ${index + 1}`;

View file

@ -9,6 +9,7 @@ import { ImageGallery } from "./ImageGallery.jsx";
import { Tooltip } from "./Tooltip.jsx";
const IMAGE_GALLERY_SIZE_STORAGE_KEY = "sokkog:image-gallery-thumbnail-width";
const IMAGE_GALLERY_RATIO_STORAGE_KEY = "sokkog:image-gallery-card-ratio";
const IMAGE_GALLERY_SIZE_MIN = 180;
const IMAGE_GALLERY_SIZE_MAX = 420;
const IMAGE_GALLERY_SIZE_STEP = 40;
@ -30,6 +31,14 @@ function readGalleryThumbnailWidth() {
}
}
function readGalleryCardRatio() {
try {
return localStorage.getItem(IMAGE_GALLERY_RATIO_STORAGE_KEY) === "1:1" ? "1:1" : "4:3";
} catch {
return "4:3";
}
}
function clampPercent(value) {
const parsed = Number(value);
if (!Number.isFinite(parsed)) return 0;
@ -71,6 +80,7 @@ export function ImageViewer({ image, onClose, createMarkerId = () => globalThis.
const [view, setView] = useState(initialView);
const [galleryFocusIndex, setGalleryFocusIndex] = useState(-1);
const [galleryThumbnailWidth, setGalleryThumbnailWidth] = useState(readGalleryThumbnailWidth);
const [galleryCardRatio, setGalleryCardRatio] = useState(readGalleryCardRatio);
const [previousRequest, setPreviousRequest] = useState(image);
if (previousRequest !== image) {
setPreviousRequest(image);
@ -203,7 +213,19 @@ export function ImageViewer({ image, onClose, createMarkerId = () => globalThis.
} catch { /* La préférence reste active pour la modale courante si le stockage est indisponible. */ }
}
function toggleGalleryCardRatio() {
const next = galleryCardRatio === "4:3" ? "1:1" : "4:3";
setGalleryCardRatio(next);
try {
localStorage.setItem(IMAGE_GALLERY_RATIO_STORAGE_KEY, next);
} catch { /* La préférence reste active pour la modale courante si le stockage est indisponible. */ }
}
const positionLabel = formatPositionLabel(textContent.positionLabel, activeIndex + 1, collection.length);
const nextGalleryCardRatio = galleryCardRatio === "4:3" ? "1:1" : "4:3";
const galleryRatioAction = nextGalleryCardRatio === "1:1"
? textContent.galleryRatioSquareTitle || "Passer au format 1:1"
: textContent.galleryRatioLandscapeTitle || "Passer au format 4:3";
function updateViewerMarker(markerId, updater) {
updateViewerMarkers(viewerMarkers.map((marker) => marker.id === markerId ? updater(marker) : marker));
@ -226,10 +248,13 @@ export function ImageViewer({ image, onClose, createMarkerId = () => globalThis.
</div>
<div className="image-viewer-actions">
{view === "gallery" && (
<div className="image-viewer-gallery-zoom" role="group" aria-label={textContent.galleryZoomLabel || "Taille des aperçus"}>
<button className="image-viewer-gallery-zoom-button nds-icon-button" data-size="sm" type="button" disabled={galleryThumbnailWidth <= IMAGE_GALLERY_SIZE_MIN} onClick={() => resizeGalleryThumbnail(-1)} aria-label={textContent.galleryZoomOutTitle || "Réduire les aperçus"} title={textContent.galleryZoomOutTitle || "Réduire les aperçus"}><span aria-hidden="true">−</span></button>
<button className="image-viewer-gallery-zoom-button nds-icon-button" data-size="sm" type="button" disabled={galleryThumbnailWidth >= IMAGE_GALLERY_SIZE_MAX} onClick={() => resizeGalleryThumbnail(1)} aria-label={textContent.galleryZoomInTitle || "Agrandir les aperçus"} title={textContent.galleryZoomInTitle || "Agrandir les aperçus"}><span aria-hidden="true">+</span></button>
</div>
<>
<button className="image-viewer-gallery-ratio-button nds-button" data-size="sm" data-variant="secondary" type="button" onClick={toggleGalleryCardRatio} aria-label={`${textContent.galleryRatioLabel || "Format des cartes"} : ${galleryCardRatio}. ${galleryRatioAction}`} title={galleryRatioAction}>{galleryCardRatio}</button>
<div className="image-viewer-gallery-zoom" role="group" aria-label={textContent.galleryZoomLabel || "Taille des aperçus"}>
<button className="image-viewer-gallery-zoom-button nds-icon-button" data-size="sm" type="button" disabled={galleryThumbnailWidth <= IMAGE_GALLERY_SIZE_MIN} onClick={() => resizeGalleryThumbnail(-1)} aria-label={textContent.galleryZoomOutTitle || "Réduire les aperçus"} title={textContent.galleryZoomOutTitle || "Réduire les aperçus"}><span aria-hidden="true">−</span></button>
<button className="image-viewer-gallery-zoom-button nds-icon-button" data-size="sm" type="button" disabled={galleryThumbnailWidth >= IMAGE_GALLERY_SIZE_MAX} onClick={() => resizeGalleryThumbnail(1)} aria-label={textContent.galleryZoomInTitle || "Agrandir les aperçus"} title={textContent.galleryZoomInTitle || "Agrandir les aperçus"}><span aria-hidden="true">+</span></button>
</div>
</>
)}
{isCollection && view === "detail" && (
<button className="image-viewer-button nds-icon-button" data-size="sm" onClick={openGallery} aria-label={textContent.returnGalleryTitle || "Revenir à la galerie"} title={textContent.returnGalleryTitle || "Revenir à la galerie"}><Icon name="picture" /></button>
@ -241,7 +266,7 @@ export function ImageViewer({ image, onClose, createMarkerId = () => globalThis.
</div>
</header>
{view === "gallery" ? (
<ImageGallery images={collection} activeIndex={activeIndex} focusIndex={galleryFocusIndex} thumbnailWidth={galleryThumbnailWidth} onSelect={openGalleryImage} textContent={textContent} />
<ImageGallery images={collection} activeIndex={activeIndex} focusIndex={galleryFocusIndex} thumbnailWidth={galleryThumbnailWidth} cardRatio={galleryCardRatio} onSelect={openGalleryImage} textContent={textContent} />
) : <div className={`image-viewer-body ${canAnnotate ? "has-annotation-side" : ""} ${isCollection && collection.length > 1 ? "has-navigation" : ""}`}>
{isCollection && collection.length > 1 && (
<button className="image-viewer-nav image-viewer-nav-previous nds-icon-button" data-size="md" type="button" disabled={activeIndex === 0} onClick={() => setActiveIndex((index) => index - 1)} aria-label={textContent.previousImageTitle || "Image précédente"} title={textContent.previousImageTitle || "Image précédente"}>

View file

@ -215,6 +215,7 @@
.image-viewer-gallery {
--image-gallery-thumbnail-width: 260px;
--image-gallery-card-ratio: 4 / 3;
display: flex;
width: 100%;
max-width: 100%;
@ -230,12 +231,16 @@
padding: 20px 28px 28px;
}
.image-viewer-gallery[data-card-ratio="1:1"] {
--image-gallery-card-ratio: 1 / 1;
}
.image-viewer-gallery-item {
display: grid;
width: min(100%, var(--image-gallery-thumbnail-width));
max-width: 100%;
min-width: 0;
aspect-ratio: 4 / 3;
aspect-ratio: var(--image-gallery-card-ratio);
flex: 0 1 var(--image-gallery-thumbnail-width);
grid-template-rows: minmax(0, 1fr) auto;
align-self: flex-start;