Add persistent gallery zoom and proportional card layout
All checks were successful
Deploy Sokko G / deploy (push) Successful in 18s
All checks were successful
Deploy Sokko G / deploy (push) Successful in 18s
This commit is contained in:
parent
386212041f
commit
273bc5d528
11 changed files with 158 additions and 19 deletions
|
|
@ -2046,6 +2046,9 @@
|
|||
"galleryTitle": "Galerie d'images",
|
||||
"galleryImageAriaLabel": "Afficher en grand",
|
||||
"returnGalleryTitle": "Revenir à la galerie",
|
||||
"galleryZoomLabel": "Taille des aperçus",
|
||||
"galleryZoomOutTitle": "Réduire les aperçus",
|
||||
"galleryZoomInTitle": "Agrandir les aperçus",
|
||||
"previousImageTitle": "Image précédente",
|
||||
"nextImageTitle": "Image suivante",
|
||||
"positionLabel": "Image {current} sur {total}",
|
||||
|
|
|
|||
|
|
@ -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, onSelect, textContent = {} }) {
|
||||
export function ImageGallery({ images, activeIndex, focusIndex = -1, thumbnailWidth = 260, onSelect, textContent = {} }) {
|
||||
const galleryRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -13,7 +13,7 @@ export function ImageGallery({ images, activeIndex, focusIndex = -1, onSelect, t
|
|||
}, [focusIndex]);
|
||||
|
||||
return (
|
||||
<div ref={galleryRef} className="image-viewer-gallery themed-scrollbar">
|
||||
<div ref={galleryRef} className="image-viewer-gallery themed-scrollbar" 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}`;
|
||||
|
|
|
|||
|
|
@ -8,6 +8,28 @@ import { Icon } from "./Icon.jsx";
|
|||
import { ImageGallery } from "./ImageGallery.jsx";
|
||||
import { Tooltip } from "./Tooltip.jsx";
|
||||
|
||||
const IMAGE_GALLERY_SIZE_STORAGE_KEY = "sokkog:image-gallery-thumbnail-width";
|
||||
const IMAGE_GALLERY_SIZE_MIN = 180;
|
||||
const IMAGE_GALLERY_SIZE_MAX = 420;
|
||||
const IMAGE_GALLERY_SIZE_STEP = 40;
|
||||
const IMAGE_GALLERY_SIZE_DEFAULT = 260;
|
||||
|
||||
function normalizeGalleryThumbnailWidth(value) {
|
||||
const parsed = Number(value);
|
||||
if (!Number.isFinite(parsed)) return IMAGE_GALLERY_SIZE_DEFAULT;
|
||||
const clamped = Math.min(IMAGE_GALLERY_SIZE_MAX, Math.max(IMAGE_GALLERY_SIZE_MIN, parsed));
|
||||
return IMAGE_GALLERY_SIZE_MIN + Math.round((clamped - IMAGE_GALLERY_SIZE_MIN) / IMAGE_GALLERY_SIZE_STEP) * IMAGE_GALLERY_SIZE_STEP;
|
||||
}
|
||||
|
||||
function readGalleryThumbnailWidth() {
|
||||
try {
|
||||
const stored = localStorage.getItem(IMAGE_GALLERY_SIZE_STORAGE_KEY);
|
||||
return stored == null || stored === "" ? IMAGE_GALLERY_SIZE_DEFAULT : normalizeGalleryThumbnailWidth(stored);
|
||||
} catch {
|
||||
return IMAGE_GALLERY_SIZE_DEFAULT;
|
||||
}
|
||||
}
|
||||
|
||||
function clampPercent(value) {
|
||||
const parsed = Number(value);
|
||||
if (!Number.isFinite(parsed)) return 0;
|
||||
|
|
@ -48,6 +70,7 @@ export function ImageViewer({ image, onClose, createMarkerId = () => globalThis.
|
|||
const [activeIndex, setActiveIndex] = useState(initialIndex);
|
||||
const [view, setView] = useState(initialView);
|
||||
const [galleryFocusIndex, setGalleryFocusIndex] = useState(-1);
|
||||
const [galleryThumbnailWidth, setGalleryThumbnailWidth] = useState(readGalleryThumbnailWidth);
|
||||
const [previousRequest, setPreviousRequest] = useState(image);
|
||||
if (previousRequest !== image) {
|
||||
setPreviousRequest(image);
|
||||
|
|
@ -172,6 +195,14 @@ export function ImageViewer({ image, onClose, createMarkerId = () => globalThis.
|
|||
setView("detail");
|
||||
}
|
||||
|
||||
function resizeGalleryThumbnail(direction) {
|
||||
const next = normalizeGalleryThumbnailWidth(galleryThumbnailWidth + direction * IMAGE_GALLERY_SIZE_STEP);
|
||||
setGalleryThumbnailWidth(next);
|
||||
try {
|
||||
localStorage.setItem(IMAGE_GALLERY_SIZE_STORAGE_KEY, String(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);
|
||||
|
||||
function updateViewerMarker(markerId, updater) {
|
||||
|
|
@ -194,6 +225,12 @@ export function ImageViewer({ image, onClose, createMarkerId = () => globalThis.
|
|||
{isCollection && view === "detail" && <span className="image-viewer-position" aria-label={positionLabel} title={positionLabel} aria-live="polite">{activeIndex + 1} / {collection.length}</span>}
|
||||
</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>
|
||||
)}
|
||||
{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>
|
||||
)}
|
||||
|
|
@ -204,7 +241,7 @@ export function ImageViewer({ image, onClose, createMarkerId = () => globalThis.
|
|||
</div>
|
||||
</header>
|
||||
{view === "gallery" ? (
|
||||
<ImageGallery images={collection} activeIndex={activeIndex} focusIndex={galleryFocusIndex} onSelect={openGalleryImage} textContent={textContent} />
|
||||
<ImageGallery images={collection} activeIndex={activeIndex} focusIndex={galleryFocusIndex} thumbnailWidth={galleryThumbnailWidth} 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"}>
|
||||
|
|
|
|||
|
|
@ -265,13 +265,8 @@
|
|||
}
|
||||
|
||||
.image-viewer-gallery {
|
||||
grid-template-columns: repeat(auto-fill, minmax(min(220px, 100%), 1fr));
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.image-viewer-gallery-frame {
|
||||
min-height: 180px;
|
||||
height: min(42vh, 260px);
|
||||
padding: var(--space-3);
|
||||
}
|
||||
|
||||
.module-column {
|
||||
|
|
|
|||
|
|
@ -70,6 +70,17 @@
|
|||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.image-viewer-gallery-zoom {
|
||||
display: inline-flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.image-viewer-gallery-zoom-button > span {
|
||||
font-size: 20px;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.image-viewer-button {
|
||||
flex: 0 0 40px;
|
||||
}
|
||||
|
|
@ -196,25 +207,39 @@
|
|||
}
|
||||
|
||||
.image-viewer.is-gallery {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
height: min(860px, calc(100vh - 48px));
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.image-viewer-gallery {
|
||||
display: grid;
|
||||
--image-gallery-thumbnail-width: 260px;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
min-height: 0;
|
||||
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
||||
min-width: 0;
|
||||
flex-wrap: wrap;
|
||||
align-content: start;
|
||||
gap: var(--space-4);
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
padding: 2px 6px 2px 2px;
|
||||
padding: 20px 28px 28px;
|
||||
}
|
||||
|
||||
.image-viewer-gallery-item {
|
||||
display: grid;
|
||||
width: min(100%, var(--image-gallery-thumbnail-width));
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
gap: 10px;
|
||||
aspect-ratio: 4 / 3;
|
||||
flex: 0 1 var(--image-gallery-thumbnail-width);
|
||||
grid-template-rows: minmax(0, 1fr) auto;
|
||||
align-self: flex-start;
|
||||
gap: 8px;
|
||||
padding: 10px;
|
||||
color: var(--color-text);
|
||||
text-align: left;
|
||||
|
|
@ -228,8 +253,9 @@
|
|||
|
||||
.image-viewer-gallery-frame {
|
||||
display: grid;
|
||||
min-height: 200px;
|
||||
height: clamp(200px, 24vh, 280px);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
place-items: center;
|
||||
overflow: hidden;
|
||||
border-radius: var(--radius-md);
|
||||
|
|
@ -245,6 +271,7 @@
|
|||
|
||||
.image-viewer-gallery-caption {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue