content & style opti, new tool annotation
All checks were successful
Deploy Sokko G / deploy (push) Successful in 6s

This commit is contained in:
Shinuwa 2026-07-24 09:37:32 +02:00
parent 0db35a0d2c
commit 6e1472d2fb
21 changed files with 1112 additions and 194 deletions

View file

@ -0,0 +1,128 @@
import { useState } from "react";
import { Icon } from "../../../components/Icon.jsx";
function markerLabel(index, marker, textContent) {
return marker.label || `${textContent.markerPrefix || "Marqueur"} ${index + 1}`;
}
export function ImageAnnotationModule({ toolboxId, moduleId, context, editing }) {
const data = context.normalizeImageAnnotationData(context.getModuleData(toolboxId, moduleId, { image: "", markers: [] }));
const textContent = context.moduleText?.imageAnnotation || {};
const [dragOver, setDragOver] = useState(false);
const markers = data.markers;
const pastePlaceholder = textContent.pastePlaceholder || "Coller une image ici";
function save(nextData) {
context.setModuleData(toolboxId, moduleId, nextData);
}
async function setImageFromFiles(files) {
const file = [...files].find((item) => item?.type?.startsWith("image/"));
if (!file) return;
save({ image: await context.compressImageFile(file), markers: [] });
setDragOver(false);
}
function removeImage() {
save({ image: "", markers: [] });
}
return (
<div className="image-annotation-module">
{editing && (
<div className="module-add-panel">
<label
className={`dropzone annotation-dropzone ${dragOver ? "is-drag-over" : ""}`}
onDragOver={(event) => {
event.preventDefault();
event.dataTransfer.dropEffect = "copy";
setDragOver(true);
}}
onDragLeave={() => setDragOver(false)}
onDrop={(event) => {
event.preventDefault();
setImageFromFiles(event.dataTransfer.files);
}}
>
<input type="file" accept="image/*" hidden onChange={(event) => setImageFromFiles(event.target.files)} />
{data.image ? textContent.replaceImage || "Remplacer l'image" : textContent.addImage || "Ajouter une image"}
</label>
<div
className="paste-target"
contentEditable
suppressContentEditableWarning
role="textbox"
aria-label={textContent.pasteAriaLabel || "Coller une image à annoter depuis le presse-papiers"}
onFocus={(event) => {
if (event.currentTarget.textContent.trim() === pastePlaceholder) event.currentTarget.textContent = "";
}}
onBlur={(event) => {
if (!event.currentTarget.textContent.trim()) event.currentTarget.textContent = pastePlaceholder;
}}
onPaste={(event) => {
const files = [...(event.clipboardData?.items || [])]
.filter((item) => item.type.startsWith("image/"))
.map((item) => item.getAsFile())
.filter(Boolean);
if (!files.length) return;
event.preventDefault();
event.currentTarget.textContent = pastePlaceholder;
setImageFromFiles(files);
}}
>
{pastePlaceholder}
</div>
</div>
)}
{data.image ? (
<div className="annotation-workspace">
<div className="annotation-stage annotation-stage-preview" aria-label={textContent.stageAriaLabel || "Image annotée"}>
<div className="annotation-media">
<img src={data.image} alt={textContent.imageAlt || "Image annotée"} draggable="false" />
<div className="annotation-preview-actions">
<button
className="calculator-action-button"
type="button"
onClick={() => context.setScreenshot({
dataUrl: data.image,
markers,
alt: textContent.imageAlt || "Image annotée",
canAnnotate: true,
createMarkerId: () => context.uid("marker"),
markerPrefix: textContent.markerPrefix || "Marqueur",
addMarkerAriaLabel: textContent.addMarkerAriaLabel || "Ajouter un marqueur sur l'image",
markerPlaceholder: textContent.markerPlaceholder || "Libellé du marqueur",
deleteMarkerTitle: textContent.deleteMarkerTitle || "Supprimer",
onChangeMarkers: (nextMarkers) => save({ ...data, markers: nextMarkers })
})}
aria-label={textContent.previewAriaLabel || "Agrandir l'image annotée"}
title={textContent.previewTitle || "Agrandir"}
>
<Icon name="zoom" />
</button>
{editing && (
<button className="calculator-action-button danger" type="button" onClick={removeImage} aria-label={textContent.deleteImageTitle || "Supprimer l'image"} title={textContent.deleteImageTitle || "Supprimer l'image"}>
<Icon name="trash" />
</button>
)}
</div>
{markers.map((marker, index) => (
<span
key={marker.id}
className="annotation-marker"
style={{ "--marker-x": `${marker.x}%`, "--marker-y": `${marker.y}%` }}
title={markerLabel(index, marker, textContent)}
>
{index + 1}
</span>
))}
</div>
</div>
</div>
) : (
<p className="annotation-empty muted">{textContent.emptyImage || "Ajoutez une image pour commencer l'annotation."}</p>
)}
</div>
);
}