diff --git a/website/public/data/site.json b/website/public/data/site.json
index 9a2373d..0698883 100644
--- a/website/public/data/site.json
+++ b/website/public/data/site.json
@@ -227,6 +227,8 @@
"labelPlaceholder": "Libellé de l'image",
"labelAriaLabel": "Libellé de l'image",
"previewAriaLabel": "Agrandir l'image",
+ "reorderAriaLabel": "Déplacer l'image",
+ "reorderTitle": "Déplacer",
"annotateAriaLabel": "Annoter l'image",
"annotateTitle": "Annoter",
"deleteAriaLabel": "Supprimer l'image",
diff --git a/website/public/static/icons/clock.svg b/website/public/static/icons/clock.svg
new file mode 100644
index 0000000..8734376
--- /dev/null
+++ b/website/public/static/icons/clock.svg
@@ -0,0 +1,4 @@
+
+
\ No newline at end of file
diff --git a/website/src/features/toolboxes/modules/ImagesModule.jsx b/website/src/features/toolboxes/modules/ImagesModule.jsx
index 9e2c41c..0d01765 100644
--- a/website/src/features/toolboxes/modules/ImagesModule.jsx
+++ b/website/src/features/toolboxes/modules/ImagesModule.jsx
@@ -1,12 +1,33 @@
// Rôle : fournit l'outil images avec import, collage, libellés et annotation rapide.
import { useState } from "react";
import { Icon } from "../../../components/Icon.jsx";
+import { usePointerReorder } from "../../../hooks/usePointerReorder.js";
export function ImagesModule({ toolboxId, moduleId, context, editing }) {
const data = context.getModuleData(toolboxId, moduleId, { images: [] });
const textContent = context.moduleText?.images || {};
const [dragOver, setDragOver] = useState(false);
const pastePlaceholder = textContent.pastePlaceholder || "Coller une image ici";
+ const {
+ draggingId: draggingImageId,
+ dropTarget,
+ startDrag
+ } = usePointerReorder({
+ targetSelector: "[data-image-id]",
+ getTargetId: (target) => target.dataset.imageId,
+ canDropOn: (target) => target.dataset.imageModuleId === moduleId,
+ getPlacement: (event, target) => {
+ const rect = target.getBoundingClientRect();
+ return event.clientX > rect.left + rect.width / 2 ? "after" : "before";
+ },
+ onMove: (draggingId, targetId, placement) => {
+ const nextImages = data.images.filter((image) => image.id !== draggingId);
+ const targetIndex = nextImages.findIndex((image) => image.id === targetId);
+ if (targetIndex === -1) return;
+ nextImages.splice(placement === "after" ? targetIndex + 1 : targetIndex, 0, data.images.find((image) => image.id === draggingId));
+ context.setModuleData(toolboxId, moduleId, { images: nextImages.filter(Boolean) });
+ }
+ });
async function addFiles(files) {
if (await context.addImageFiles(toolboxId, moduleId, files)) {
@@ -68,40 +89,59 @@ export function ImagesModule({ toolboxId, moduleId, context, editing }) {
)}
- {data.images.map((image) => (
-
-
- {editing ? (
- updateImage(image.id, { label: event.target.value })}
- aria-label={textContent.labelAriaLabel || "Libellé de l'image"}
- />
- ) : image.label ? (
- {image.label}
- ) : null}
-
+ {data.images.map((image) => {
+ const isDragging = draggingImageId === image.id;
+ const isDropTarget = dropTarget.id === image.id;
+ const className = [
+ isDragging ? "is-dragging" : "",
+ isDropTarget ? "is-drop-target" : "",
+ isDropTarget && dropTarget.placement === "after" ? "drop-after" : ""
+ ].filter(Boolean).join(" ");
+
+ return (
+
-
-
- ))}
+ {editing ? (
+
updateImage(image.id, { label: event.target.value })}
+ aria-label={textContent.labelAriaLabel || "Libellé de l'image"}
+ />
+ ) : image.label ? (
+
{image.label}
+ ) : null}
+
+ context.createImageAnnotationModule?.(image.dataUrl)}
+ aria-label={textContent.annotateAriaLabel || "Annoter l'image"}
+ title={textContent.annotateTitle || "Annoter"}
+ >
+
+
+ {
+ context.setModuleData(toolboxId, moduleId, { images: data.images.filter((item) => item.id !== image.id) });
+ }} aria-label={textContent.deleteAriaLabel || "Supprimer l'image"} title={textContent.deleteTitle || "Supprimer"}>
+
+
+
+
+ );
+ })}
>
);
diff --git a/website/src/styles/_toolboxes.scss b/website/src/styles/_toolboxes.scss
index a19e456..a45769e 100644
--- a/website/src/styles/_toolboxes.scss
+++ b/website/src/styles/_toolboxes.scss
@@ -1720,6 +1720,60 @@ textarea:focus {
background: rgba(5, 7, 17, 0.38);
}
+.images figure.is-dragging {
+ opacity: 0.42;
+}
+
+.images figure.is-drop-target {
+ border-color: rgba(246, 196, 83, 0.72);
+ box-shadow: 0 0 0 1px rgba(246, 196, 83, 0.26), 0 0 18px rgba(246, 196, 83, 0.12);
+}
+
+.images figure.is-drop-target.drop-after {
+ border-color: rgba(112, 89, 255, 0.82);
+ box-shadow: 0 0 0 1px rgba(164, 124, 255, 0.3), 0 0 18px rgba(112, 89, 255, 0.16);
+}
+
+.image-drag-handle {
+ position: absolute;
+ z-index: 4;
+ top: 12px;
+ left: 12px;
+ display: grid;
+ width: 32px;
+ height: 32px;
+ place-items: center;
+ padding: 0;
+ border: 1px solid rgba(138, 150, 197, 0.22);
+ border-radius: var(--radius-md);
+ background: rgba(9, 13, 29, 0.76);
+ color: rgba(229, 235, 255, 0.78);
+ cursor: grab;
+ opacity: 0;
+ transition: border-color var(--transition-fast), background var(--transition-fast), color var(--transition-fast), opacity var(--transition-fast), box-shadow var(--transition-fast);
+}
+
+.image-drag-handle .ui-icon {
+ width: 16px;
+ height: 16px;
+}
+
+.images figure:hover .image-drag-handle,
+.images figure:focus-within .image-drag-handle {
+ opacity: 1;
+}
+
+.image-drag-handle:hover {
+ border-color: rgba(246, 196, 83, 0.56);
+ background: rgba(20, 28, 55, 0.86);
+ color: var(--color-accent-gold);
+ box-shadow: 0 0 14px rgba(246, 196, 83, 0.12);
+}
+
+.image-drag-handle:active {
+ cursor: grabbing;
+}
+
.image-preview {
display: block;
width: 100%;