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
|
|
@ -623,7 +623,7 @@ Modales :
|
||||||
- panneau centre ;
|
- panneau centre ;
|
||||||
- fermeture au clic exterieur quand l'action le permet ;
|
- fermeture au clic exterieur quand l'action le permet ;
|
||||||
- boutons icones pour fermer/zoomer quand approprie.
|
- boutons icones pour fermer/zoomer quand approprie.
|
||||||
- la visionneuse d'un outil Images peut recevoir la collection du bloc courant : sa galerie utilise de grandes vignettes non recadrees dans une grille responsive et scrollable ;
|
- la visionneuse d'un outil Images peut recevoir la collection du bloc courant : sa galerie utilise de grandes cards `4:3`, avec image non recadrée et libellé toujours visible, dans un flux responsive centré et scrollable uniquement verticalement ; les boutons `−` et `+` règlent leur largeur de 180 à 420 px par paliers de 40 px ; les gouttières entre cards restent compactes tandis que le bord de la galerie conserve un padding plus généreux ;
|
||||||
- la vue detaillee expose la position, un retour explicite a la galerie et des boutons precedent/suivant bloques aux extremites ; les fleches gauche/droite reprennent cette navigation ;
|
- la vue detaillee expose la position, un retour explicite a la galerie et des boutons precedent/suivant bloques aux extremites ; les fleches gauche/droite reprennent cette navigation ;
|
||||||
- une visionneuse d'image seule, notamment pour les annotations, ne doit afficher ni galerie ni navigation de collection.
|
- une visionneuse d'image seule, notamment pour les annotations, ne doit afficher ni galerie ni navigation de collection.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ Checklist à suivre lors de l'ajout ou de la modification d'un outil, d'une page
|
||||||
- Pour les dessins temporaires et l'historique du simulateur de chance, vérifier l'isolation par clé de session lors des changements de page/outil et après rechargement ; une navigation seule ne doit pas écraser les données de l'autre clé.
|
- Pour les dessins temporaires et l'historique du simulateur de chance, vérifier l'isolation par clé de session lors des changements de page/outil et après rechargement ; une navigation seule ne doit pas écraser les données de l'autre clé.
|
||||||
- Pour le simulateur de chance, vérifier que les brouillons suivent le taux et l'outil actifs, restent intacts lors d'un rendu sans rapport et se valident au blur selon le champ.
|
- Pour le simulateur de chance, vérifier que les brouillons suivent le taux et l'outil actifs, restent intacts lors d'un rendu sans rapport et se valident au blur selon le champ.
|
||||||
- Pour le visualiseur d'images, vérifier la remise à zéro des marqueurs et des modes dessin/gomme au remplacement de l'image.
|
- Pour le visualiseur d'images, vérifier la remise à zéro des marqueurs et des modes dessin/gomme au remplacement de l'image.
|
||||||
|
- Pour la galerie d'images, vérifier les boutons de taille `−` / `+`, leur blocage aux limites, la persistance après rechargement, les cards `4:3` avec image et libellé contenus, la redistribution centrée des vignettes, le padding périphérique et l'absence de scroll horizontal.
|
||||||
- Pour l'éditeur de couverture, vérifier les décodages concurrents, les erreurs après fermeture et la libération des bitmaps/URLs ; une image en chargement ne doit pas permettre de valider la couverture précédente.
|
- Pour l'éditeur de couverture, vérifier les décodages concurrents, les erreurs après fermeture et la libération des bitmaps/URLs ; une image en chargement ne doit pas permettre de valider la couverture précédente.
|
||||||
- Les parcours `e2e/toolbox-cover.spec.js` vérifient le choix d'icône, l'enregistrement d'une couverture 620 × 248, sa persistance après rechargement et import/export, l'annulation et les fichiers illisibles. L'association à un jeu doit afficher sa couverture sans perdre l'image personnalisée, restaurée après dissociation ; choisir une icône doit supprimer la couverture personnalisée.
|
- Les parcours `e2e/toolbox-cover.spec.js` vérifient le choix d'icône, l'enregistrement d'une couverture 620 × 248, sa persistance après rechargement et import/export, l'annulation et les fichiers illisibles. L'association à un jeu doit afficher sa couverture sans perdre l'image personnalisée, restaurée après dissociation ; choisir une icône doit supprimer la couverture personnalisée.
|
||||||
- Les parcours `e2e/table.spec.js` vérifient les en-têtes éditables sans changer les références des formules, le redimensionnement sans restauration des cellules supprimées, la navigation et la sélection clavier, la copie des valeurs TSV, le collage et l'effacement de blocs, le déplacement à la souris et l'insertion d'une plage dans une formule. Les valeurs et dimensions doivent survivre au rechargement.
|
- Les parcours `e2e/table.spec.js` vérifient les en-têtes éditables sans changer les références des formules, le redimensionnement sans restauration des cellules supprimées, la navigation et la sélection clavier, la copie des valeurs TSV, le collage et l'effacement de blocs, le déplacement à la souris et l'insertion d'une plage dans une formule. Les valeurs et dimensions doivent survivre au rechargement.
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,13 @@
|
||||||
Ce document décrit les données persistées dans IndexedDB pour les toolboxes.
|
Ce document décrit les données persistées dans IndexedDB pour les toolboxes.
|
||||||
Il doit être mis à jour à chaque ajout ou modification d'outil.
|
Il doit être mis à jour à chaque ajout ou modification d'outil.
|
||||||
|
|
||||||
|
## Préférences locales hors toolbox
|
||||||
|
|
||||||
|
`localStorage["sokkog:image-gallery-thumbnail-width"]` conserve la largeur cible
|
||||||
|
des aperçus de la galerie d'images. La valeur est bornée de 180 à 420 px, alignée
|
||||||
|
sur des paliers de 40 px et vaut 260 px par défaut. Cette préférence d'interface
|
||||||
|
globale n'est pas incluse dans les exports de toolboxes.
|
||||||
|
|
||||||
## IndexedDB
|
## IndexedDB
|
||||||
|
|
||||||
Base : `sokkog`
|
Base : `sokkog`
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
// Rôle : couvre les mutations persistantes des liens, compteurs et checklists.
|
// Rôle : couvre les parcours persistants des liens, compteurs, checklists et galeries d'images.
|
||||||
import { expect, test } from "@playwright/test";
|
import { expect, test } from "@playwright/test";
|
||||||
import { Buffer } from "node:buffer";
|
import { Buffer } from "node:buffer";
|
||||||
|
|
||||||
|
|
@ -151,7 +151,20 @@ test("images open a gallery and navigate the current collection without looping"
|
||||||
await tool.getByRole("button", { name: "Galerie", exact: true }).click();
|
await tool.getByRole("button", { name: "Galerie", exact: true }).click();
|
||||||
const dialog = page.getByRole("dialog", { name: "Galerie d'images", exact: true });
|
const dialog = page.getByRole("dialog", { name: "Galerie d'images", exact: true });
|
||||||
await expect(dialog).toBeVisible();
|
await expect(dialog).toBeVisible();
|
||||||
await expect(dialog.locator(".image-viewer-gallery-item")).toHaveCount(3);
|
const thumbnails = dialog.locator(".image-viewer-gallery-item");
|
||||||
|
const gallery = dialog.locator(".image-viewer-gallery");
|
||||||
|
await expect(thumbnails).toHaveCount(3);
|
||||||
|
const initialThumbnailWidth = (await thumbnails.first().boundingBox()).width;
|
||||||
|
await dialog.getByRole("button", { name: "Agrandir les aperçus", exact: true }).click();
|
||||||
|
await expect.poll(async () => (await thumbnails.first().boundingBox()).width).toBeCloseTo(initialThumbnailWidth + 40, 0);
|
||||||
|
expect(await page.evaluate(() => globalThis.localStorage.getItem("sokkog:image-gallery-thumbnail-width"))).toBe("300");
|
||||||
|
expect(await gallery.evaluate((element) => element.scrollWidth <= element.clientWidth)).toBe(true);
|
||||||
|
await dialog.getByRole("button", { name: "Fermer", exact: true }).click();
|
||||||
|
await page.reload();
|
||||||
|
await tool.getByRole("button", { name: "Galerie", exact: true }).click();
|
||||||
|
await expect.poll(async () => (await thumbnails.first().boundingBox()).width).toBeCloseTo(300, 0);
|
||||||
|
await dialog.getByRole("button", { name: "Réduire les aperçus", exact: true }).click();
|
||||||
|
await expect.poll(async () => (await thumbnails.first().boundingBox()).width).toBeCloseTo(260, 0);
|
||||||
await dialog.getByRole("button", { name: "Afficher en grand : Second", exact: true }).click();
|
await dialog.getByRole("button", { name: "Afficher en grand : Second", exact: true }).click();
|
||||||
await expect(page.locator(".image-viewer-title")).toHaveText("Second");
|
await expect(page.locator(".image-viewer-title")).toHaveText("Second");
|
||||||
await expect(page.locator(".image-viewer-position")).toHaveText("2 / 3");
|
await expect(page.locator(".image-viewer-position")).toHaveText("2 / 3");
|
||||||
|
|
@ -175,6 +188,11 @@ test("images open a gallery and navigate the current collection without looping"
|
||||||
expect(galleryBox.y).toBeGreaterThanOrEqual(0);
|
expect(galleryBox.y).toBeGreaterThanOrEqual(0);
|
||||||
expect(galleryBox.x + galleryBox.width).toBeLessThanOrEqual(390);
|
expect(galleryBox.x + galleryBox.width).toBeLessThanOrEqual(390);
|
||||||
expect(galleryBox.y + galleryBox.height).toBeLessThanOrEqual(844);
|
expect(galleryBox.y + galleryBox.height).toBeLessThanOrEqual(844);
|
||||||
|
const zoomIn = dialog.getByRole("button", { name: "Agrandir les aperçus", exact: true });
|
||||||
|
for (let step = 0; step < 4; step += 1) await zoomIn.click();
|
||||||
|
await expect(zoomIn).toBeDisabled();
|
||||||
|
expect(await page.evaluate(() => globalThis.localStorage.getItem("sokkog:image-gallery-thumbnail-width"))).toBe("420");
|
||||||
|
expect(await gallery.evaluate((element) => element.scrollWidth <= element.clientWidth)).toBe(true);
|
||||||
await dialog.getByRole("button", { name: "Fermer", exact: true }).click();
|
await dialog.getByRole("button", { name: "Fermer", exact: true }).click();
|
||||||
|
|
||||||
await tool.locator(".image-preview").nth(2).click();
|
await tool.locator(".image-preview").nth(2).click();
|
||||||
|
|
@ -182,3 +200,48 @@ test("images open a gallery and navigate the current collection without looping"
|
||||||
await expect(page.getByRole("button", { name: "Image suivante", exact: true })).toBeDisabled();
|
await expect(page.getByRole("button", { name: "Image suivante", exact: true })).toBeDisabled();
|
||||||
await page.getByRole("button", { name: "Fermer", exact: true }).click();
|
await page.getByRole("button", { name: "Fermer", exact: true }).click();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("image gallery keeps proportional cards and compact centered rows at maximum zoom", async ({ page }) => {
|
||||||
|
await page.setViewportSize({ width: 1550, height: 900 });
|
||||||
|
const makeImage = (index) => `data:image/svg+xml,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" width="640" height="360"><rect width="640" height="360" fill="hsl(${index * 45} 70% 45%)"/></svg>`)}`;
|
||||||
|
const tool = await setup(page, "images", {
|
||||||
|
images: Array.from({ length: 7 }, (_, index) => ({ id: `image-${index}`, label: `Image ${index + 1}`, dataUrl: makeImage(index) }))
|
||||||
|
});
|
||||||
|
await page.evaluate(() => globalThis.localStorage.setItem("sokkog:image-gallery-thumbnail-width", "420"));
|
||||||
|
await tool.getByRole("button", { name: "Galerie", exact: true }).click();
|
||||||
|
|
||||||
|
const dialog = page.getByRole("dialog", { name: "Galerie d'images", exact: true });
|
||||||
|
const gallery = dialog.locator(".image-viewer-gallery");
|
||||||
|
const cards = gallery.locator(".image-viewer-gallery-item");
|
||||||
|
await expect(cards).toHaveCount(7);
|
||||||
|
await expect.poll(() => cards.first().locator("img").evaluate((image) => image.naturalWidth)).toBe(640);
|
||||||
|
const boxes = await cards.evaluateAll((elements) => elements.map((element) => {
|
||||||
|
const box = element.getBoundingClientRect();
|
||||||
|
return { x: box.x, y: box.y, width: box.width, height: box.height };
|
||||||
|
}));
|
||||||
|
expect(boxes[0].width / boxes[0].height).toBeCloseTo(4 / 3, 2);
|
||||||
|
expect(boxes.slice(0, 3).every((box) => Math.abs(box.y - boxes[0].y) < 1)).toBe(true);
|
||||||
|
expect(boxes.slice(3, 6).every((box) => Math.abs(box.y - boxes[3].y) < 1)).toBe(true);
|
||||||
|
expect(boxes[3].y).toBeGreaterThan(boxes[0].y + boxes[0].height);
|
||||||
|
expect(boxes[6].y).toBeGreaterThan(boxes[3].y + boxes[3].height);
|
||||||
|
|
||||||
|
const gap = boxes[1].x - boxes[0].x - boxes[0].width;
|
||||||
|
const galleryBox = await gallery.boundingBox();
|
||||||
|
expect(gap).toBeGreaterThanOrEqual(10);
|
||||||
|
expect(gap).toBeLessThanOrEqual(14);
|
||||||
|
expect(boxes[0].x - galleryBox.x).toBeGreaterThan(gap * 2);
|
||||||
|
expect(galleryBox.x + galleryBox.width - boxes[2].x - boxes[2].width).toBeGreaterThan(gap * 2);
|
||||||
|
expect(boxes[6].x + boxes[6].width / 2).toBeCloseTo(galleryBox.x + galleryBox.width / 2, 0);
|
||||||
|
|
||||||
|
const firstCard = cards.first();
|
||||||
|
const frameBox = await firstCard.locator(".image-viewer-gallery-frame").boundingBox();
|
||||||
|
const imageBox = await firstCard.locator("img").boundingBox();
|
||||||
|
const captionBox = await firstCard.locator(".image-viewer-gallery-caption").boundingBox();
|
||||||
|
expect(imageBox.x).toBeGreaterThanOrEqual(frameBox.x);
|
||||||
|
expect(imageBox.y).toBeGreaterThanOrEqual(frameBox.y);
|
||||||
|
expect(imageBox.x + imageBox.width).toBeLessThanOrEqual(frameBox.x + frameBox.width);
|
||||||
|
expect(imageBox.y + imageBox.height).toBeLessThanOrEqual(frameBox.y + frameBox.height);
|
||||||
|
expect(captionBox.y + captionBox.height).toBeLessThanOrEqual(boxes[0].y + boxes[0].height);
|
||||||
|
expect(await gallery.evaluate((element) => element.scrollWidth <= element.clientWidth)).toBe(true);
|
||||||
|
expect(await gallery.evaluate((element) => element.scrollHeight > element.clientHeight)).toBe(true);
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -356,6 +356,9 @@ export function validateSiteContent(site) {
|
||||||
"toolboxes.modules.images.galleryTitle",
|
"toolboxes.modules.images.galleryTitle",
|
||||||
"toolboxes.modules.images.galleryImageAriaLabel",
|
"toolboxes.modules.images.galleryImageAriaLabel",
|
||||||
"toolboxes.modules.images.returnGalleryTitle",
|
"toolboxes.modules.images.returnGalleryTitle",
|
||||||
|
"toolboxes.modules.images.galleryZoomLabel",
|
||||||
|
"toolboxes.modules.images.galleryZoomOutTitle",
|
||||||
|
"toolboxes.modules.images.galleryZoomInTitle",
|
||||||
"toolboxes.modules.images.previousImageTitle",
|
"toolboxes.modules.images.previousImageTitle",
|
||||||
"toolboxes.modules.images.nextImageTitle",
|
"toolboxes.modules.images.nextImageTitle",
|
||||||
"toolboxes.modules.images.positionLabel",
|
"toolboxes.modules.images.positionLabel",
|
||||||
|
|
|
||||||
|
|
@ -466,6 +466,9 @@ test("toolbox module registry and modules expose expected behavior", async () =>
|
||||||
assert.match(imageViewer, /ArrowLeft/);
|
assert.match(imageViewer, /ArrowLeft/);
|
||||||
assert.match(imageViewer, /ArrowRight/);
|
assert.match(imageViewer, /ArrowRight/);
|
||||||
assert.match(imageViewer, /<ImageGallery/);
|
assert.match(imageViewer, /<ImageGallery/);
|
||||||
|
assert.match(imageViewer, /sokkog:image-gallery-thumbnail-width/);
|
||||||
|
assert.match(imageViewer, /galleryZoomOutTitle/);
|
||||||
|
assert.match(imageViewer, /galleryZoomInTitle/);
|
||||||
assert.match(imageGallery, /image-viewer-gallery-item/);
|
assert.match(imageGallery, /image-viewer-gallery-item/);
|
||||||
assert.match(moduleEmptyMessage, /toolbox-module-empty/);
|
assert.match(moduleEmptyMessage, /toolbox-module-empty/);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -2046,6 +2046,9 @@
|
||||||
"galleryTitle": "Galerie d'images",
|
"galleryTitle": "Galerie d'images",
|
||||||
"galleryImageAriaLabel": "Afficher en grand",
|
"galleryImageAriaLabel": "Afficher en grand",
|
||||||
"returnGalleryTitle": "Revenir à la galerie",
|
"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",
|
"previousImageTitle": "Image précédente",
|
||||||
"nextImageTitle": "Image suivante",
|
"nextImageTitle": "Image suivante",
|
||||||
"positionLabel": "Image {current} sur {total}",
|
"positionLabel": "Image {current} sur {total}",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
// Rôle : affiche la collection d'un outil Images dans la galerie de la visionneuse.
|
// Rôle : affiche la collection d'un outil Images dans la galerie de la visionneuse.
|
||||||
import { useEffect, useRef } from "react";
|
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);
|
const galleryRef = useRef(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -13,7 +13,7 @@ export function ImageGallery({ images, activeIndex, focusIndex = -1, onSelect, t
|
||||||
}, [focusIndex]);
|
}, [focusIndex]);
|
||||||
|
|
||||||
return (
|
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) => {
|
{images.map((image, index) => {
|
||||||
const position = `${index + 1} / ${images.length}`;
|
const position = `${index + 1} / ${images.length}`;
|
||||||
const label = image.label || `${textContent.imageAlt || "Image"} ${index + 1}`;
|
const label = image.label || `${textContent.imageAlt || "Image"} ${index + 1}`;
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,28 @@ import { Icon } from "./Icon.jsx";
|
||||||
import { ImageGallery } from "./ImageGallery.jsx";
|
import { ImageGallery } from "./ImageGallery.jsx";
|
||||||
import { Tooltip } from "./Tooltip.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) {
|
function clampPercent(value) {
|
||||||
const parsed = Number(value);
|
const parsed = Number(value);
|
||||||
if (!Number.isFinite(parsed)) return 0;
|
if (!Number.isFinite(parsed)) return 0;
|
||||||
|
|
@ -48,6 +70,7 @@ export function ImageViewer({ image, onClose, createMarkerId = () => globalThis.
|
||||||
const [activeIndex, setActiveIndex] = useState(initialIndex);
|
const [activeIndex, setActiveIndex] = useState(initialIndex);
|
||||||
const [view, setView] = useState(initialView);
|
const [view, setView] = useState(initialView);
|
||||||
const [galleryFocusIndex, setGalleryFocusIndex] = useState(-1);
|
const [galleryFocusIndex, setGalleryFocusIndex] = useState(-1);
|
||||||
|
const [galleryThumbnailWidth, setGalleryThumbnailWidth] = useState(readGalleryThumbnailWidth);
|
||||||
const [previousRequest, setPreviousRequest] = useState(image);
|
const [previousRequest, setPreviousRequest] = useState(image);
|
||||||
if (previousRequest !== image) {
|
if (previousRequest !== image) {
|
||||||
setPreviousRequest(image);
|
setPreviousRequest(image);
|
||||||
|
|
@ -172,6 +195,14 @@ export function ImageViewer({ image, onClose, createMarkerId = () => globalThis.
|
||||||
setView("detail");
|
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);
|
const positionLabel = formatPositionLabel(textContent.positionLabel, activeIndex + 1, collection.length);
|
||||||
|
|
||||||
function updateViewerMarker(markerId, updater) {
|
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>}
|
{isCollection && view === "detail" && <span className="image-viewer-position" aria-label={positionLabel} title={positionLabel} aria-live="polite">{activeIndex + 1} / {collection.length}</span>}
|
||||||
</div>
|
</div>
|
||||||
<div className="image-viewer-actions">
|
<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" && (
|
{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>
|
<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>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
{view === "gallery" ? (
|
{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" : ""}`}>
|
) : <div className={`image-viewer-body ${canAnnotate ? "has-annotation-side" : ""} ${isCollection && collection.length > 1 ? "has-navigation" : ""}`}>
|
||||||
{isCollection && collection.length > 1 && (
|
{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"}>
|
<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 {
|
.image-viewer-gallery {
|
||||||
grid-template-columns: repeat(auto-fill, minmax(min(220px, 100%), 1fr));
|
|
||||||
gap: var(--space-3);
|
gap: var(--space-3);
|
||||||
}
|
padding: var(--space-3);
|
||||||
|
|
||||||
.image-viewer-gallery-frame {
|
|
||||||
min-height: 180px;
|
|
||||||
height: min(42vh, 260px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-column {
|
.module-column {
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,17 @@
|
||||||
gap: var(--space-2);
|
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 {
|
.image-viewer-button {
|
||||||
flex: 0 0 40px;
|
flex: 0 0 40px;
|
||||||
}
|
}
|
||||||
|
|
@ -196,25 +207,39 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-viewer.is-gallery {
|
.image-viewer.is-gallery {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
height: min(860px, calc(100vh - 48px));
|
height: min(860px, calc(100vh - 48px));
|
||||||
grid-template-rows: auto minmax(0, 1fr);
|
grid-template-rows: auto minmax(0, 1fr);
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-viewer-gallery {
|
.image-viewer-gallery {
|
||||||
display: grid;
|
--image-gallery-thumbnail-width: 260px;
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
min-width: 0;
|
||||||
|
flex-wrap: wrap;
|
||||||
align-content: start;
|
align-content: start;
|
||||||
gap: var(--space-4);
|
justify-content: center;
|
||||||
|
gap: 12px;
|
||||||
|
overflow-x: hidden;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overscroll-behavior: contain;
|
overscroll-behavior: contain;
|
||||||
padding: 2px 6px 2px 2px;
|
padding: 20px 28px 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-viewer-gallery-item {
|
.image-viewer-gallery-item {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
width: min(100%, var(--image-gallery-thumbnail-width));
|
||||||
|
max-width: 100%;
|
||||||
min-width: 0;
|
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;
|
padding: 10px;
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
|
@ -228,8 +253,9 @@
|
||||||
|
|
||||||
.image-viewer-gallery-frame {
|
.image-viewer-gallery-frame {
|
||||||
display: grid;
|
display: grid;
|
||||||
min-height: 200px;
|
width: 100%;
|
||||||
height: clamp(200px, 24vh, 280px);
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
|
|
@ -245,6 +271,7 @@
|
||||||
|
|
||||||
.image-viewer-gallery-caption {
|
.image-viewer-gallery-caption {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
width: 100%;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
grid-template-columns: minmax(0, 1fr) auto;
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue