diff --git a/DESIGN_SYSTEM.md b/DESIGN_SYSTEM.md index c8c18bd..c4067d4 100644 --- a/DESIGN_SYSTEM.md +++ b/DESIGN_SYSTEM.md @@ -624,7 +624,7 @@ Modales : - panneau centre ; - fermeture au clic exterieur quand l'action le permet ; - 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 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 visionneuse d'un outil Images peut recevoir la collection du bloc courant : sa galerie utilise de grandes cards commutables entre `4:3` et `1:1`, avec image non recadrée et libellé toujours visible, dans un flux responsive centré et scrollable uniquement verticalement ; un bouton texte affiche le ratio actif, tandis que les boutons `−` et `+` règlent leur largeur de 180 à 420 px par paliers de 40 px ; le ratio et la largeur sont mémorisés comme préférences locales ; 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 ; - une visionneuse d'image seule, notamment pour les annotations, ne doit afficher ni galerie ni navigation de collection. diff --git a/docs/FEATURE_CHECKLIST.md b/docs/FEATURE_CHECKLIST.md index a61ca92..3e5f2a8 100644 --- a/docs/FEATURE_CHECKLIST.md +++ b/docs/FEATURE_CHECKLIST.md @@ -27,7 +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 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 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 la galerie d'images, vérifier les boutons de taille `−` / `+`, leur blocage aux limites, le switch de ratio `4:3` / `1:1`, la persistance de ces préférences après rechargement, les images et libellés contenus dans les deux formats, 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. - 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. diff --git a/docs/STORAGE_SCHEMA.md b/docs/STORAGE_SCHEMA.md index af74158..c83298a 100644 --- a/docs/STORAGE_SCHEMA.md +++ b/docs/STORAGE_SCHEMA.md @@ -10,6 +10,10 @@ des aperçus de la galerie d'images. La valeur est bornée de 180 à 420 px, ali 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. +`localStorage["sokkog:image-gallery-card-ratio"]` conserve le format des cards de +la galerie. Les valeurs acceptées sont `4:3`, utilisé par défaut, et `1:1`. Cette +préférence d'interface globale n'est pas incluse dans les exports de toolboxes. + ## IndexedDB Base : `sokkog` diff --git a/e2e/basic-tools.spec.js b/e2e/basic-tools.spec.js index b14d089..790e9aa 100644 --- a/e2e/basic-tools.spec.js +++ b/e2e/basic-tools.spec.js @@ -153,7 +153,20 @@ test("images open a gallery and navigate the current collection without looping" await expect(dialog).toBeVisible(); const thumbnails = dialog.locator(".image-viewer-gallery-item"); const gallery = dialog.locator(".image-viewer-gallery"); + const ratioButton = dialog.locator(".image-viewer-gallery-ratio-button"); await expect(thumbnails).toHaveCount(3); + await expect(ratioButton).toHaveText("4:3"); + await expect(ratioButton).toHaveAccessibleName(/Passer au format 1:1/); + const initialCardBox = await thumbnails.first().boundingBox(); + expect(initialCardBox.width / initialCardBox.height).toBeCloseTo(4 / 3, 2); + await ratioButton.click(); + await expect(ratioButton).toHaveText("1:1"); + await expect(ratioButton).toHaveAccessibleName(/Passer au format 4:3/); + await expect.poll(async () => { + const box = await thumbnails.first().boundingBox(); + return box.width / box.height; + }).toBeCloseTo(1, 2); + expect(await page.evaluate(() => globalThis.localStorage.getItem("sokkog:image-gallery-card-ratio"))).toBe("1:1"); 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); @@ -163,6 +176,10 @@ test("images open a gallery and navigate the current collection without looping" 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 expect(ratioButton).toHaveText("1:1"); + await ratioButton.click(); + await expect(ratioButton).toHaveText("4:3"); + expect(await page.evaluate(() => globalThis.localStorage.getItem("sokkog:image-gallery-card-ratio"))).toBe("4:3"); 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(); @@ -188,6 +205,8 @@ test("images open a gallery and navigate the current collection without looping" expect(galleryBox.y).toBeGreaterThanOrEqual(0); expect(galleryBox.x + galleryBox.width).toBeLessThanOrEqual(390); expect(galleryBox.y + galleryBox.height).toBeLessThanOrEqual(844); + await ratioButton.click(); + await expect(ratioButton).toHaveText("1:1"); 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(); diff --git a/tests/helpers/data-validation.mjs b/tests/helpers/data-validation.mjs index 458f9fd..2fe84fe 100644 --- a/tests/helpers/data-validation.mjs +++ b/tests/helpers/data-validation.mjs @@ -361,6 +361,9 @@ export function validateSiteContent(site) { "toolboxes.modules.images.galleryZoomLabel", "toolboxes.modules.images.galleryZoomOutTitle", "toolboxes.modules.images.galleryZoomInTitle", + "toolboxes.modules.images.galleryRatioLabel", + "toolboxes.modules.images.galleryRatioSquareTitle", + "toolboxes.modules.images.galleryRatioLandscapeTitle", "toolboxes.modules.images.previousImageTitle", "toolboxes.modules.images.nextImageTitle", "toolboxes.modules.images.positionLabel", diff --git a/tests/static-toolboxes.test.mjs b/tests/static-toolboxes.test.mjs index f599bd0..86c3472 100644 --- a/tests/static-toolboxes.test.mjs +++ b/tests/static-toolboxes.test.mjs @@ -469,6 +469,9 @@ test("toolbox module registry and modules expose expected behavior", async () => assert.match(imageViewer, /sokkog:image-gallery-thumbnail-width/); assert.match(imageViewer, /galleryZoomOutTitle/); assert.match(imageViewer, /galleryZoomInTitle/); + assert.match(imageViewer, /sokkog:image-gallery-card-ratio/); + assert.match(imageViewer, /galleryRatioSquareTitle/); + assert.match(imageViewer, /galleryRatioLandscapeTitle/); assert.match(imageGallery, /image-viewer-gallery-item/); assert.match(moduleEmptyMessage, /toolbox-module-empty/); }); diff --git a/website/public/data/site.json b/website/public/data/site.json index 9a97bab..1d9cf13 100644 --- a/website/public/data/site.json +++ b/website/public/data/site.json @@ -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}", diff --git a/website/src/components/ImageGallery.jsx b/website/src/components/ImageGallery.jsx index d81bbcc..dd5d1d1 100644 --- a/website/src/components/ImageGallery.jsx +++ b/website/src/components/ImageGallery.jsx @@ -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 ( -
+
{images.map((image, index) => { const position = `${index + 1} / ${images.length}`; const label = image.label || `${textContent.imageAlt || "Image"} ${index + 1}`; diff --git a/website/src/components/ImageViewer.jsx b/website/src/components/ImageViewer.jsx index 052b82d..1068f10 100644 --- a/website/src/components/ImageViewer.jsx +++ b/website/src/components/ImageViewer.jsx @@ -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.
{view === "gallery" && ( -
- - -
+ <> + +
+ + +
+ )} {isCollection && view === "detail" && ( @@ -241,7 +266,7 @@ export function ImageViewer({ image, onClose, createMarkerId = () => globalThis.
{view === "gallery" ? ( - + ) :
1 ? "has-navigation" : ""}`}> {isCollection && collection.length > 1 && (