content & style opti, new tool annotation
All checks were successful
Deploy Sokko G / deploy (push) Successful in 6s
All checks were successful
Deploy Sokko G / deploy (push) Successful in 6s
This commit is contained in:
parent
0db35a0d2c
commit
6e1472d2fb
21 changed files with 1112 additions and 194 deletions
|
|
@ -119,18 +119,18 @@ export function CalculatorModule({ toolboxId, moduleId, context }) {
|
|||
<form className="calculator-form" onSubmit={saveResult}>
|
||||
<div className="calculator-card" ref={calculatorCardRef}>
|
||||
<label>
|
||||
<span>Calcul</span>
|
||||
<span>{textContent.expressionLabel || "Calcul"}</span>
|
||||
<input value={expression} onChange={(event) => setExpression(event.target.value)} placeholder={textContent.expressionPlaceholder || "10*10"} inputMode="decimal" />
|
||||
</label>
|
||||
<div className="calculator-result" aria-live="polite">
|
||||
<span>Résultat</span>
|
||||
<span>{textContent.resultLabel || "Résultat"}</span>
|
||||
<strong>{result == null ? "-" : formatResult(result)}</strong>
|
||||
</div>
|
||||
<label>
|
||||
<span>Libellé</span>
|
||||
<span>{textContent.labelLabel || "Libellé"}</span>
|
||||
<input value={label} onChange={(event) => setLabel(event.target.value)} placeholder={textContent.labelPlaceholder || "Lingots de fer"} />
|
||||
</label>
|
||||
<button className="primary" disabled={result == null}>Enregistrer</button>
|
||||
<button className="primary" disabled={result == null}>{textContent.saveButton || "Enregistrer"}</button>
|
||||
</div>
|
||||
</form>
|
||||
<div
|
||||
|
|
@ -140,13 +140,13 @@ export function CalculatorModule({ toolboxId, moduleId, context }) {
|
|||
<div className="calculator-result-actions">
|
||||
{activeParent && (
|
||||
<button className="calculator-root-button" type="button" onClick={() => setActiveParentId("")}>
|
||||
Revenir à la racine
|
||||
{textContent.rootButton || "Revenir à la racine"}
|
||||
</button>
|
||||
)}
|
||||
<button className="calculator-action-button danger" type="button" onClick={resetCalculator} aria-label="Réinitialiser le calculateur" title="Réinitialiser">
|
||||
<button className="calculator-action-button danger" type="button" onClick={resetCalculator} aria-label={textContent.resetTitle || "Réinitialiser"} title={textContent.resetTitle || "Réinitialiser"}>
|
||||
<Icon name="rubber" />
|
||||
</button>
|
||||
<button className="calculator-action-button" type="button" onClick={copyChecklistImport} disabled={!data.entries.length} aria-label="Copier pour checklist" title={copied ? "Copié" : "Copier pour checklist"}>
|
||||
<button className="calculator-action-button" type="button" onClick={copyChecklistImport} disabled={!data.entries.length} aria-label={textContent.copyTitle || "Copier pour checklist"} title={copied ? textContent.copiedTitle || "Copié" : textContent.copyTitle || "Copier pour checklist"}>
|
||||
<Icon name="copy" />
|
||||
</button>
|
||||
<button
|
||||
|
|
@ -163,9 +163,9 @@ export function CalculatorModule({ toolboxId, moduleId, context }) {
|
|||
</div>
|
||||
<div className={`calculator-tree ${data.scrollResults ? "is-scrollable legacy-scrollbar" : ""}`}>
|
||||
{data.entries.length ? (
|
||||
<CalculatorEntries entries={data.entries} parentId="" activeParentId={activeParentId} onUse={useEntry} onRename={renameEntry} onDelete={deleteEntry} />
|
||||
<CalculatorEntries entries={data.entries} parentId="" activeParentId={activeParentId} textContent={textContent} onUse={useEntry} onRename={renameEntry} onDelete={deleteEntry} />
|
||||
) : (
|
||||
<p className="muted">Aucun résultat enregistré.</p>
|
||||
<p className="muted">{textContent.emptyResults || "Aucun résultat enregistré."}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -173,7 +173,7 @@ export function CalculatorModule({ toolboxId, moduleId, context }) {
|
|||
);
|
||||
}
|
||||
|
||||
function CalculatorEntries({ entries, parentId, activeParentId, onUse, onRename, onDelete }) {
|
||||
function CalculatorEntries({ entries, parentId, activeParentId, textContent, onUse, onRename, onDelete }) {
|
||||
const children = getChildren(entries, parentId);
|
||||
const [editingId, setEditingId] = useState("");
|
||||
if (!children.length) return null;
|
||||
|
|
@ -185,31 +185,31 @@ function CalculatorEntries({ entries, parentId, activeParentId, onUse, onRename,
|
|||
<div className="calculator-entry">
|
||||
{entry.id === editingId ? (
|
||||
<>
|
||||
<span className="calculator-entry-value is-readonly" title="Quantité non modifiable">
|
||||
<span className="calculator-entry-value is-readonly" title={textContent.readonlyValueTitle || "Quantité non modifiable"}>
|
||||
<strong>{formatResult(entry.value)}</strong>
|
||||
</span>
|
||||
<EditableCalculatorLabel entry={entry} onRename={onRename} onDone={() => setEditingId("")} />
|
||||
<EditableCalculatorLabel entry={entry} textContent={textContent} onRename={onRename} onDone={() => setEditingId("")} />
|
||||
</>
|
||||
) : (
|
||||
<button className="calculator-entry-summary" type="button" onClick={() => onUse(entry)} title="Utiliser comme base">
|
||||
<button className="calculator-entry-summary" type="button" onClick={() => onUse(entry)} title={textContent.useEntryTitle || "Utiliser comme base"}>
|
||||
<span><strong>{formatResult(entry.value)}</strong> {entry.label}</span>
|
||||
</button>
|
||||
)}
|
||||
<button type="button" onClick={() => setEditingId(entry.id === editingId ? "" : entry.id)} aria-label={`Renommer ${entry.label}`} title="Renommer">
|
||||
<button type="button" onClick={() => setEditingId(entry.id === editingId ? "" : entry.id)} aria-label={`${textContent.renameTitle || "Renommer"} ${entry.label}`} title={textContent.renameTitle || "Renommer"}>
|
||||
<Icon name="edit" />
|
||||
</button>
|
||||
<button type="button" className="danger" onClick={() => onDelete(entry.id)} aria-label={`Supprimer ${entry.label}`} title="Supprimer">
|
||||
<button type="button" className="danger" onClick={() => onDelete(entry.id)} aria-label={`${textContent.deleteTitle || "Supprimer"} ${entry.label}`} title={textContent.deleteTitle || "Supprimer"}>
|
||||
<Icon name="trash" />
|
||||
</button>
|
||||
</div>
|
||||
<CalculatorEntries entries={entries} parentId={entry.id} activeParentId={activeParentId} onUse={onUse} onRename={onRename} onDelete={onDelete} />
|
||||
<CalculatorEntries entries={entries} parentId={entry.id} activeParentId={activeParentId} textContent={textContent} onUse={onUse} onRename={onRename} onDelete={onDelete} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
function EditableCalculatorLabel({ entry, onRename, onDone }) {
|
||||
function EditableCalculatorLabel({ entry, textContent, onRename, onDone }) {
|
||||
const [label, setLabel] = useState(entry.label);
|
||||
const inputRef = useRef(null);
|
||||
|
||||
|
|
@ -237,7 +237,7 @@ function EditableCalculatorLabel({ entry, onRename, onDone }) {
|
|||
onDone();
|
||||
}
|
||||
}}
|
||||
aria-label={`Renommer ${entry.label}`}
|
||||
aria-label={`${textContent.renameTitle || "Renommer"} ${entry.label}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useState } from "react";
|
||||
import { Icon } from "../../../components/Icon.jsx";
|
||||
import { parseColonImportLines } from "./textImport.js";
|
||||
|
||||
export function ChecklistModule({ toolboxId, moduleId, context, editing }) {
|
||||
|
|
@ -40,8 +41,8 @@ export function ChecklistModule({ toolboxId, moduleId, context, editing }) {
|
|||
<div className="module-add-panel">
|
||||
<form className="inline-form checklist-add-form" onSubmit={addItem}>
|
||||
<input name="label" placeholder={textContent.itemPlaceholder || "Nouvel item"} value={label} onChange={(event) => setLabel(event.target.value)} />
|
||||
<input className="checklist-qty-input" name="qty" type="number" min="1" value={qty} onChange={(event) => setQty(event.target.value)} aria-label="Quantité cible" />
|
||||
<button className="primary">Ajouter</button>
|
||||
<input className="checklist-qty-input" name="qty" type="number" min="1" value={qty} onChange={(event) => setQty(event.target.value)} aria-label={textContent.quantityLabel || "Quantité cible"} />
|
||||
<button className="primary">{textContent.addButton || "Ajouter"}</button>
|
||||
</form>
|
||||
<form className="text-import-form" onSubmit={importItems}>
|
||||
<textarea
|
||||
|
|
@ -50,7 +51,7 @@ export function ChecklistModule({ toolboxId, moduleId, context, editing }) {
|
|||
placeholder={textContent.importPlaceholder || "Importer plusieurs items...\nPotion:10\nMéga potion:5"}
|
||||
rows={3}
|
||||
/>
|
||||
<button type="submit">Importer le texte</button>
|
||||
<button type="submit">{textContent.importButton || "Importer le texte"}</button>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -82,7 +83,7 @@ function ChecklistItem({ item, context, items, save }) {
|
|||
/>
|
||||
) : (
|
||||
<div className="checklist-qty-controls" aria-label={`Quantité ${item.label}`}>
|
||||
<button type="button" onClick={() => updateItem((entry) => ({ ...entry, qtyCurrent: context.clampQty(entry.qtyCurrent - 1, entry.qtyTarget) }))} aria-label="Retirer une quantité">-</button>
|
||||
<button type="button" onClick={() => updateItem((entry) => ({ ...entry, qtyCurrent: context.clampQty(entry.qtyCurrent - 1, entry.qtyTarget) }))} aria-label={context.moduleText?.checklist?.decrementLabel || "Retirer une quantité"}>-</button>
|
||||
<label className="checklist-qty-current">
|
||||
<input
|
||||
type="number"
|
||||
|
|
@ -90,16 +91,18 @@ function ChecklistItem({ item, context, items, save }) {
|
|||
value={context.clampQty(item.qtyCurrent, item.qtyTarget)}
|
||||
style={{ "--qty-current-digits": String(context.clampQty(item.qtyCurrent, item.qtyTarget)).length }}
|
||||
onChange={(event) => updateItem((entry) => ({ ...entry, qtyCurrent: context.clampQty(event.target.value, entry.qtyTarget) }))}
|
||||
aria-label={`Quantité actuelle pour ${item.label}`}
|
||||
aria-label={`${context.moduleText?.checklist?.currentQuantityLabel || "Quantité actuelle"} ${item.label}`}
|
||||
/>
|
||||
<span>/ {item.qtyTarget}</span>
|
||||
</label>
|
||||
<button type="button" onClick={() => updateItem((entry) => ({ ...entry, qtyCurrent: context.clampQty(entry.qtyCurrent + 1, entry.qtyTarget) }))} aria-label="Ajouter une quantité">+</button>
|
||||
<button type="button" onClick={() => updateItem((entry) => ({ ...entry, qtyCurrent: context.clampQty(entry.qtyCurrent + 1, entry.qtyTarget) }))} aria-label={context.moduleText?.checklist?.incrementLabel || "Ajouter une quantité"}>+</button>
|
||||
</div>
|
||||
)}
|
||||
<span>{item.label}</span>
|
||||
</div>
|
||||
<button onClick={() => save(items.filter((entry) => entry.id !== item.id))} aria-label={`Supprimer ${item.label}`}>×</button>
|
||||
<button className="checklist-delete-button danger" onClick={() => save(items.filter((entry) => entry.id !== item.id))} aria-label={`${context.moduleText?.checklist?.deleteTitle || "Supprimer"} ${item.label}`} title={context.moduleText?.checklist?.deleteTitle || "Supprimer"}>
|
||||
<Icon name="trash" />
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ export function CountersModule({ toolboxId, moduleId, context, editing }) {
|
|||
{editing && (
|
||||
<form className="inline-form counters-add-form module-add-panel" onSubmit={addCounter}>
|
||||
<input name="label" placeholder={textContent.labelPlaceholder || "Nom du compteur"} value={label} onChange={(event) => setLabel(event.target.value)} />
|
||||
<button className="primary">Ajouter</button>
|
||||
<button className="primary">{textContent.addButton || "Ajouter"}</button>
|
||||
</form>
|
||||
)}
|
||||
<div className="counters-grid">
|
||||
|
|
@ -39,12 +39,12 @@ export function CountersModule({ toolboxId, moduleId, context, editing }) {
|
|||
<span>{counter.label}</span>
|
||||
</div>
|
||||
<div className="counter-actions">
|
||||
<button type="button" onClick={() => updateCounter(counter.id, (item) => ({ ...item, value: item.value - 1 }))} aria-label={`Décrémenter ${counter.label}`}>-</button>
|
||||
<button type="button" onClick={() => updateCounter(counter.id, (item) => ({ ...item, value: item.value + 1 }))} aria-label={`Incrémenter ${counter.label}`}>+</button>
|
||||
<button type="button" onClick={() => updateCounter(counter.id, (item) => ({ ...item, value: 0 }))} aria-label={`Réinitialiser ${counter.label}`} title="Réinitialiser">
|
||||
<button type="button" onClick={() => updateCounter(counter.id, (item) => ({ ...item, value: item.value - 1 }))} aria-label={`${textContent.decrementLabel || "Décrémenter"} ${counter.label}`}>-</button>
|
||||
<button type="button" onClick={() => updateCounter(counter.id, (item) => ({ ...item, value: item.value + 1 }))} aria-label={`${textContent.incrementLabel || "Incrémenter"} ${counter.label}`}>+</button>
|
||||
<button type="button" onClick={() => updateCounter(counter.id, (item) => ({ ...item, value: 0 }))} aria-label={`${textContent.resetTitle || "Réinitialiser"} ${counter.label}`} title={textContent.resetTitle || "Réinitialiser"}>
|
||||
<Icon name="rubber" />
|
||||
</button>
|
||||
<button type="button" className="danger" onClick={() => save(data.counters.filter((item) => item.id !== counter.id))} aria-label={`Supprimer ${counter.label}`} title="Supprimer">
|
||||
<button type="button" className="danger" onClick={() => save(data.counters.filter((item) => item.id !== counter.id))} aria-label={`${textContent.deleteTitle || "Supprimer"} ${counter.label}`} title={textContent.deleteTitle || "Supprimer"}>
|
||||
<Icon name="trash" />
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
128
website/src/features/toolboxes/modules/ImageAnnotationModule.jsx
Normal file
128
website/src/features/toolboxes/modules/ImageAnnotationModule.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -59,7 +59,7 @@ export function LinksModule({ toolboxId, moduleId, context, editing }) {
|
|||
<form className="inline-form links-add-form" onSubmit={addLink}>
|
||||
<input name="title" placeholder={textContent.titlePlaceholder || "Nom du lien"} value={title} onChange={(event) => setTitle(event.target.value)} />
|
||||
<input name="url" placeholder={textContent.urlPlaceholder || "https://..."} value={url} onChange={(event) => setUrl(event.target.value)} />
|
||||
<button className="primary">Ajouter</button>
|
||||
<button className="primary">{textContent.addButton || "Ajouter"}</button>
|
||||
</form>
|
||||
<form className="text-import-form" onSubmit={importLinks}>
|
||||
<textarea
|
||||
|
|
@ -68,7 +68,7 @@ export function LinksModule({ toolboxId, moduleId, context, editing }) {
|
|||
placeholder={textContent.importPlaceholder || "Importer plusieurs liens...\nBuild:https://example.com/build?class=rogue\nMap:https://example.com/map"}
|
||||
rows={3}
|
||||
/>
|
||||
<button type="submit">Importer le texte</button>
|
||||
<button type="submit">{textContent.importButton || "Importer le texte"}</button>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -80,10 +80,10 @@ export function LinksModule({ toolboxId, moduleId, context, editing }) {
|
|||
<span>{link.url}</span>
|
||||
</a>
|
||||
<div>
|
||||
<button type="button" onClick={() => copyUrl(link)} aria-label={`Copier ${link.title || link.url}`} title={copiedId === link.id ? "Copié" : "Copier"}>
|
||||
<button type="button" onClick={() => copyUrl(link)} aria-label={`${textContent.copyTitle || "Copier"} ${link.title || link.url}`} title={copiedId === link.id ? textContent.copiedTitle || "Copié" : textContent.copyTitle || "Copier"}>
|
||||
<Icon name="copy" />
|
||||
</button>
|
||||
<button type="button" className="danger" onClick={() => save(data.links.filter((item) => item.id !== link.id))} aria-label={`Supprimer ${link.title || link.url}`} title="Supprimer">
|
||||
<button type="button" className="danger" onClick={() => save(data.links.filter((item) => item.id !== link.id))} aria-label={`${textContent.deleteTitle || "Supprimer"} ${link.title || link.url}`} title={textContent.deleteTitle || "Supprimer"}>
|
||||
<Icon name="trash" />
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
import { useState } from "react";
|
||||
import { Icon } from "../../../components/Icon.jsx";
|
||||
|
||||
export function ScreenshotsModule({ toolboxId, moduleId, context, editing }) {
|
||||
const data = context.getModuleData(toolboxId, moduleId, { shots: [] });
|
||||
const textContent = context.moduleText?.screenshots || {};
|
||||
const [dragOver, setDragOver] = useState(false);
|
||||
const pastePlaceholder = textContent.pastePlaceholder || "Coller une image ici";
|
||||
|
||||
async function addFiles(files) {
|
||||
if (await context.addScreenshotFiles(toolboxId, moduleId, files)) {
|
||||
|
|
@ -28,19 +31,19 @@ export function ScreenshotsModule({ toolboxId, moduleId, context, editing }) {
|
|||
}}
|
||||
>
|
||||
<input type="file" accept="image/*" multiple hidden onChange={(event) => addFiles(event.target.files)} />
|
||||
Ajouter des screenshots
|
||||
{textContent.addImages || "Ajouter des images"}
|
||||
</label>
|
||||
<div
|
||||
className="paste-target"
|
||||
contentEditable
|
||||
suppressContentEditableWarning
|
||||
role="textbox"
|
||||
aria-label="Coller une image depuis le presse-papiers"
|
||||
aria-label={textContent.pasteAriaLabel || "Coller une image depuis le presse-papiers"}
|
||||
onFocus={(event) => {
|
||||
if (event.currentTarget.textContent.trim() === "Coller une image ici") event.currentTarget.textContent = "";
|
||||
if (event.currentTarget.textContent.trim() === pastePlaceholder) event.currentTarget.textContent = "";
|
||||
}}
|
||||
onBlur={(event) => {
|
||||
if (!event.currentTarget.textContent.trim()) event.currentTarget.textContent = "Coller une image ici";
|
||||
if (!event.currentTarget.textContent.trim()) event.currentTarget.textContent = pastePlaceholder;
|
||||
}}
|
||||
onPaste={(event) => {
|
||||
const files = [...(event.clipboardData?.items || [])]
|
||||
|
|
@ -49,25 +52,36 @@ export function ScreenshotsModule({ toolboxId, moduleId, context, editing }) {
|
|||
.filter(Boolean);
|
||||
if (!files.length) return;
|
||||
event.preventDefault();
|
||||
event.currentTarget.textContent = "Coller une image ici";
|
||||
event.currentTarget.textContent = pastePlaceholder;
|
||||
addFiles(files);
|
||||
}}
|
||||
>
|
||||
Coller une image ici
|
||||
{pastePlaceholder}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="shots">
|
||||
{data.shots.map((shot) => (
|
||||
<figure key={shot.id}>
|
||||
<button className="shot-preview" onClick={() => context.setScreenshot(shot)} aria-label="Agrandir le screenshot">
|
||||
<img src={shot.dataUrl} alt="Screenshot" />
|
||||
</button>
|
||||
<button className="shot-delete-button danger" onClick={() => {
|
||||
context.setModuleData(toolboxId, moduleId, { shots: data.shots.filter((item) => item.id !== shot.id) });
|
||||
}} aria-label="Supprimer le screenshot" title="Supprimer">
|
||||
<span className="ui-icon ui-icon-trash" aria-hidden="true" />
|
||||
<button className="shot-preview" onClick={() => context.setScreenshot(shot)} aria-label={textContent.previewAriaLabel || "Agrandir l'image"}>
|
||||
<img src={shot.dataUrl} alt={textContent.imageAlt || "Image"} />
|
||||
</button>
|
||||
<div className="shot-actions">
|
||||
<button
|
||||
className="shot-annotate-button"
|
||||
type="button"
|
||||
onClick={() => context.createImageAnnotationModule?.(shot.dataUrl)}
|
||||
aria-label={textContent.annotateAriaLabel || "Annoter l'image"}
|
||||
title={textContent.annotateTitle || "Annoter"}
|
||||
>
|
||||
<Icon name="map" />
|
||||
</button>
|
||||
<button className="shot-delete-button danger" type="button" onClick={() => {
|
||||
context.setModuleData(toolboxId, moduleId, { shots: data.shots.filter((item) => item.id !== shot.id) });
|
||||
}} aria-label={textContent.deleteAriaLabel || "Supprimer l'image"} title={textContent.deleteTitle || "Supprimer"}>
|
||||
<Icon name="trash" />
|
||||
</button>
|
||||
</div>
|
||||
</figure>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,9 +2,11 @@ import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|||
import { createPortal } from "react-dom";
|
||||
import { Icon } from "../../../components/Icon.jsx";
|
||||
import { usePointerReorder } from "../../../hooks/usePointerReorder.js";
|
||||
import { lockBodyScroll } from "../../../utils/bodyScrollLock.js";
|
||||
import { CalculatorModule } from "./CalculatorModule.jsx";
|
||||
import { ChecklistModule } from "./ChecklistModule.jsx";
|
||||
import { CountersModule } from "./CountersModule.jsx";
|
||||
import { ImageAnnotationModule } from "./ImageAnnotationModule.jsx";
|
||||
import { LinksModule } from "./LinksModule.jsx";
|
||||
import { NotepadModule } from "./NotepadModule.jsx";
|
||||
import { ScreenshotsModule } from "./ScreenshotsModule.jsx";
|
||||
|
|
@ -12,10 +14,11 @@ import { ScreenshotsModule } from "./ScreenshotsModule.jsx";
|
|||
const MODULE_COMPONENTS = {
|
||||
notepad: { label: "Bloc notes", icon: "notepad", Component: NotepadModule, editable: false },
|
||||
checklist: { label: "Checklist", icon: "checklist", Component: ChecklistModule, editable: true },
|
||||
screenshots: { label: "Screenshots", icon: "picture", Component: ScreenshotsModule, editable: true },
|
||||
screenshots: { label: "Images", icon: "picture", Component: ScreenshotsModule, editable: true },
|
||||
links: { label: "Liens", icon: "link", Component: LinksModule, editable: true },
|
||||
counters: { label: "Compteurs", icon: "abacus", Component: CountersModule, editable: true },
|
||||
calculator: { label: "Calculateur", icon: "calculator", Component: CalculatorModule, editable: false }
|
||||
calculator: { label: "Calculateur", icon: "calculator", Component: CalculatorModule, editable: false },
|
||||
imageAnnotation: { label: "Annotation d'images", icon: "map", Component: ImageAnnotationModule, editable: true }
|
||||
};
|
||||
|
||||
const MODULE_COLUMN_GAP_PX = 16;
|
||||
|
|
@ -71,6 +74,11 @@ export function AddToolControls({ onAdd }) {
|
|||
const controlsRef = useRef(null);
|
||||
const modules = Object.entries(TOOLBOX_MODULES);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return undefined;
|
||||
return lockBodyScroll();
|
||||
}, [open]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!quickOpen) return undefined;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue