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
12
README.md
12
README.md
|
|
@ -7,7 +7,7 @@ Sokko G propose des toolboxes modulaires stockées dans le navigateur, ainsi que
|
|||
## Fonctionnalités
|
||||
|
||||
- Toolboxes locales stockées dans `localStorage`.
|
||||
- Outils disponibles : bloc notes, checklist avec quantités, screenshots.
|
||||
- Outils disponibles : bloc notes, checklist avec quantités, images, annotation d’images, liens, compteurs et calculateur.
|
||||
- Import/export d’une toolbox.
|
||||
- Import/export global de toutes les toolboxes.
|
||||
- Association locale entre une page jeu et une toolbox.
|
||||
|
|
@ -70,6 +70,7 @@ Les contenus maintenus à la main sont regroupés dans `website/public/data`.
|
|||
| `website/public/data/mhwilds/endemic_life.json` | Données de la faune Monster Hunter Wilds. |
|
||||
| `website/public/data/mhwilds/i18n/fr.json` | Traductions françaises des données MHWilds. |
|
||||
| `website/public/data/mhwilds/i18n/en.json` | Traductions anglaises des données MHWilds. |
|
||||
| `website/public/data/diablo4/affixes_types.json` | Données et filtres des affixes Diablo IV. |
|
||||
| `website/public/data/toolbox-icons.json` | Manifeste des icônes SVG réutilisables. |
|
||||
|
||||
Les images publiques sont dans `website/public/static`.
|
||||
|
|
@ -81,6 +82,7 @@ Les pages jeux sont dans `website/src/features/games`.
|
|||
- `website/src/features/games/GamesPage.jsx` : liste des jeux disponibles.
|
||||
- `website/src/features/games/GameRoute.jsx` : route vers la page du jeu demandé.
|
||||
- `website/src/features/games/mhwilds/` : vues et composants propres à Monster Hunter Wilds.
|
||||
- `website/src/features/games/diablo4/` : vues et composants propres à Diablo IV.
|
||||
|
||||
Pour ajouter un jeu :
|
||||
|
||||
|
|
@ -95,9 +97,11 @@ Chaque outil possède son propre fichier de composant :
|
|||
|
||||
- `website/src/features/toolboxes/modules/NotepadModule.jsx`
|
||||
- `website/src/features/toolboxes/modules/ChecklistModule.jsx`
|
||||
- `website/src/features/toolboxes/modules/ScreenshotsModule.jsx`
|
||||
- `website/src/features/toolboxes/modules/ScreenshotsModule.jsx` : outil Images, conservé sous ce nom technique pour le stockage existant.
|
||||
- `website/src/features/toolboxes/modules/LinksModule.jsx`
|
||||
- `website/src/features/toolboxes/modules/CountersModule.jsx`
|
||||
- `website/src/features/toolboxes/modules/CalculatorModule.jsx`
|
||||
- `website/src/features/toolboxes/modules/ImageAnnotationModule.jsx`
|
||||
|
||||
Pour ajouter ou maintenir un outil :
|
||||
|
||||
|
|
@ -124,16 +128,20 @@ Pour ajouter ou maintenir un outil :
|
|||
├── components/
|
||||
├── features/
|
||||
│ ├── games/
|
||||
│ │ ├── diablo4/
|
||||
│ │ └── mhwilds/
|
||||
│ └── toolboxes/
|
||||
│ └── modules/
|
||||
└── styles/
|
||||
├── _base.scss
|
||||
├── _cards.scss
|
||||
├── _diablo4.scss
|
||||
├── _games.scss
|
||||
├── _home.scss
|
||||
├── _icons.scss
|
||||
├── _mixins.scss
|
||||
├── _mhwilds.scss
|
||||
├── _nebula.scss
|
||||
├── _overlays.scss
|
||||
├── _responsive.scss
|
||||
├── _shell.scss
|
||||
|
|
|
|||
|
|
@ -69,14 +69,70 @@ export function validateSiteContent(site) {
|
|||
"toolboxes.storageHelp.text",
|
||||
"toolboxes.modules.notepad.placeholder",
|
||||
"toolboxes.modules.checklist.itemPlaceholder",
|
||||
"toolboxes.modules.checklist.quantityLabel",
|
||||
"toolboxes.modules.checklist.addButton",
|
||||
"toolboxes.modules.checklist.importPlaceholder",
|
||||
"toolboxes.modules.checklist.importButton",
|
||||
"toolboxes.modules.checklist.decrementLabel",
|
||||
"toolboxes.modules.checklist.incrementLabel",
|
||||
"toolboxes.modules.checklist.currentQuantityLabel",
|
||||
"toolboxes.modules.checklist.deleteTitle",
|
||||
"toolboxes.modules.screenshots.addImages",
|
||||
"toolboxes.modules.screenshots.pastePlaceholder",
|
||||
"toolboxes.modules.screenshots.pasteAriaLabel",
|
||||
"toolboxes.modules.screenshots.imageAlt",
|
||||
"toolboxes.modules.screenshots.previewAriaLabel",
|
||||
"toolboxes.modules.screenshots.annotateAriaLabel",
|
||||
"toolboxes.modules.screenshots.annotateTitle",
|
||||
"toolboxes.modules.screenshots.deleteAriaLabel",
|
||||
"toolboxes.modules.screenshots.deleteTitle",
|
||||
"toolboxes.modules.links.titlePlaceholder",
|
||||
"toolboxes.modules.links.urlPlaceholder",
|
||||
"toolboxes.modules.links.addButton",
|
||||
"toolboxes.modules.links.importPlaceholder",
|
||||
"toolboxes.modules.links.importButton",
|
||||
"toolboxes.modules.links.copyTitle",
|
||||
"toolboxes.modules.links.copiedTitle",
|
||||
"toolboxes.modules.links.deleteTitle",
|
||||
"toolboxes.modules.counters.labelPlaceholder",
|
||||
"toolboxes.modules.counters.addButton",
|
||||
"toolboxes.modules.counters.decrementLabel",
|
||||
"toolboxes.modules.counters.incrementLabel",
|
||||
"toolboxes.modules.counters.resetTitle",
|
||||
"toolboxes.modules.counters.deleteTitle",
|
||||
"toolboxes.modules.calculator.expressionLabel",
|
||||
"toolboxes.modules.calculator.expressionPlaceholder",
|
||||
"toolboxes.modules.calculator.resultLabel",
|
||||
"toolboxes.modules.calculator.labelLabel",
|
||||
"toolboxes.modules.calculator.labelPlaceholder",
|
||||
"toolboxes.modules.calculator.saveButton",
|
||||
"toolboxes.modules.calculator.rootButton",
|
||||
"toolboxes.modules.calculator.resetTitle",
|
||||
"toolboxes.modules.calculator.copyTitle",
|
||||
"toolboxes.modules.calculator.copiedTitle",
|
||||
"toolboxes.modules.calculator.scrollableTitle",
|
||||
"toolboxes.modules.calculator.emptyResults",
|
||||
"toolboxes.modules.calculator.readonlyValueTitle",
|
||||
"toolboxes.modules.calculator.useEntryTitle",
|
||||
"toolboxes.modules.calculator.renameTitle",
|
||||
"toolboxes.modules.calculator.deleteTitle",
|
||||
"toolboxes.modules.imageAnnotation.addImage",
|
||||
"toolboxes.modules.imageAnnotation.replaceImage",
|
||||
"toolboxes.modules.imageAnnotation.pastePlaceholder",
|
||||
"toolboxes.modules.imageAnnotation.pasteAriaLabel",
|
||||
"toolboxes.modules.imageAnnotation.stageAriaLabel",
|
||||
"toolboxes.modules.imageAnnotation.addMarkerAriaLabel",
|
||||
"toolboxes.modules.imageAnnotation.imageAlt",
|
||||
"toolboxes.modules.imageAnnotation.previewTitle",
|
||||
"toolboxes.modules.imageAnnotation.previewAriaLabel",
|
||||
"toolboxes.modules.imageAnnotation.markerPrefix",
|
||||
"toolboxes.modules.imageAnnotation.markersSingular",
|
||||
"toolboxes.modules.imageAnnotation.markersPlural",
|
||||
"toolboxes.modules.imageAnnotation.markerPlaceholder",
|
||||
"toolboxes.modules.imageAnnotation.emptyImage",
|
||||
"toolboxes.modules.imageAnnotation.emptyMarkers",
|
||||
"toolboxes.modules.imageAnnotation.deleteImageTitle",
|
||||
"toolboxes.modules.imageAnnotation.deleteMarkerTitle",
|
||||
"toolboxes.emptyTitle",
|
||||
"toolboxes.emptyText"
|
||||
].forEach((path) => assertNonEmptyString(site, path));
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ test("react application defines the expected local toolbox primitives", async ()
|
|||
const styleCards = await readFile("website/src/styles/_cards.scss", "utf8");
|
||||
const styleHome = await readFile("website/src/styles/_home.scss", "utf8");
|
||||
const styleToolboxes = await readFile("website/src/styles/_toolboxes.scss", "utf8");
|
||||
const styleOverlays = await readFile("website/src/styles/_overlays.scss", "utf8");
|
||||
const styleIcons = await readFile("website/src/styles/_icons.scss", "utf8");
|
||||
const styleGames = await readFile("website/src/styles/_games.scss", "utf8");
|
||||
const styleMhwilds = await readFile("website/src/styles/_mhwilds.scss", "utf8");
|
||||
|
|
@ -46,10 +47,12 @@ test("react application defines the expected local toolbox primitives", async ()
|
|||
const calculatorModule = await readFile("website/src/features/toolboxes/modules/CalculatorModule.jsx", "utf8");
|
||||
const checklistModule = await readFile("website/src/features/toolboxes/modules/ChecklistModule.jsx", "utf8");
|
||||
const screenshotsModule = await readFile("website/src/features/toolboxes/modules/ScreenshotsModule.jsx", "utf8");
|
||||
const imageAnnotationModule = await readFile("website/src/features/toolboxes/modules/ImageAnnotationModule.jsx", "utf8");
|
||||
const linksModule = await readFile("website/src/features/toolboxes/modules/LinksModule.jsx", "utf8");
|
||||
const countersModule = await readFile("website/src/features/toolboxes/modules/CountersModule.jsx", "utf8");
|
||||
const textImport = await readFile("website/src/features/toolboxes/modules/textImport.js", "utf8");
|
||||
const reorderHook = await readFile("website/src/hooks/usePointerReorder.js", "utf8");
|
||||
const bodyScrollLock = await readFile("website/src/utils/bodyScrollLock.js", "utf8");
|
||||
|
||||
assert.match(source, /sokkog:toolboxes/);
|
||||
assert.match(source, /sokkog:game-toolbox-links/);
|
||||
|
|
@ -60,7 +63,9 @@ test("react application defines the expected local toolbox primitives", async ()
|
|||
assert.match(styles, /@use "mhwilds"/);
|
||||
assert.match(styles, /@use "diablo4"/);
|
||||
assert.match(styleTokens, /--gradient-brand/);
|
||||
assert.match(styleBase, /\.card-icon-button,\n\.drawer-action-button/);
|
||||
assert.match(styleBase, /:is\(/);
|
||||
assert.match(styleBase, /\.card-icon-button/);
|
||||
assert.match(styleBase, /\.drawer-action-button/);
|
||||
assert.match(styleNebula, /@function star-field/);
|
||||
assert.match(styleNebula, /body::after/);
|
||||
assert.match(styleBase, /\.import-icon-button/);
|
||||
|
|
@ -141,6 +146,8 @@ test("react application defines the expected local toolbox primitives", async ()
|
|||
assert.match(moduleRegistry, /links:/);
|
||||
assert.match(moduleRegistry, /counters:/);
|
||||
assert.match(moduleRegistry, /calculator:/);
|
||||
assert.match(moduleRegistry, /imageAnnotation:/);
|
||||
assert.match(moduleRegistry, /Annotation d'images/);
|
||||
assert.match(moduleRegistry, /editable: false/);
|
||||
assert.match(moduleRegistry, /module-edit-button/);
|
||||
assert.match(notepadModule, /export function NotepadModule/);
|
||||
|
|
@ -162,6 +169,7 @@ test("react application defines the expected local toolbox primitives", async ()
|
|||
assert.match(calculatorModule, /CalculatorEntries/);
|
||||
assert.match(checklistModule, /export function ChecklistModule/);
|
||||
assert.match(screenshotsModule, /export function ScreenshotsModule/);
|
||||
assert.match(imageAnnotationModule, /export function ImageAnnotationModule/);
|
||||
assert.match(linksModule, /export function LinksModule/);
|
||||
assert.match(countersModule, /export function CountersModule/);
|
||||
assert.match(textImport, /export function parseColonImportLines/);
|
||||
|
|
@ -225,18 +233,43 @@ test("react application defines the expected local toolbox primitives", async ()
|
|||
assert.match(source, /normalizeLinksData/);
|
||||
assert.match(source, /normalizeCountersData/);
|
||||
assert.match(source, /normalizeCalculatorData/);
|
||||
assert.match(source, /normalizeImageAnnotationData/);
|
||||
assert.match(source, /compactModuleDataForStorage\(type, value\)/);
|
||||
assert.match(source, /scrollResults/);
|
||||
assert.match(checklistModule, /ChecklistItem/);
|
||||
assert.match(checklistModule, /parseColonImportLines/);
|
||||
assert.match(checklistModule, /Number\.parseInt/);
|
||||
assert.match(checklistModule, /checklist-qty-current/);
|
||||
assert.match(checklistModule, /checklist-delete-button danger/);
|
||||
assert.match(linksModule, /parseColonImportLines/);
|
||||
assert.match(linksModule, /context\.normalizeUrl/);
|
||||
assert.match(screenshotsModule, /clipboardData/);
|
||||
assert.match(screenshotsModule, /createImageAnnotationModule/);
|
||||
assert.match(screenshotsModule, /shot-annotate-button/);
|
||||
assert.match(imageAnnotationModule, /annotation-marker/);
|
||||
assert.match(imageAnnotationModule, /setImageFromFiles/);
|
||||
assert.match(imageAnnotationModule, /context\.compressImageFile/);
|
||||
assert.match(imageAnnotationModule, /context\.setScreenshot/);
|
||||
assert.match(imageAnnotationModule, /createMarkerId/);
|
||||
assert.match(source, /uid\("marker"\)/);
|
||||
assert.match(source, /async function addScreenshotFiles/);
|
||||
assert.match(source, /function ScreenshotViewer/);
|
||||
assert.match(source, /useModalScrollLock/);
|
||||
assert.match(bodyScrollLock, /modalLocks/);
|
||||
assert.match(bodyScrollLock, /is-modal-open/);
|
||||
assert.match(moduleRegistry, /lockBodyScroll/);
|
||||
assert.match(source, /screenshot-viewer-media/);
|
||||
assert.match(source, /screenshot-viewer-image-frame/);
|
||||
assert.match(source, /screenshot-viewer-marker/);
|
||||
assert.match(source, /function openImageInNewTab/);
|
||||
assert.match(source, /!hasMarkers &&/);
|
||||
assert.match(source, /<Icon name="zoom"/);
|
||||
assert.match(imageAnnotationModule, /<Icon name="zoom"/);
|
||||
assert.match(styleBase, /themed-scrollbar/);
|
||||
assert.match(styleBase, /body\.is-modal-open/);
|
||||
assert.match(styleOverlays, /overscroll-behavior: contain/);
|
||||
assert.match(source, /annotation-marker-list themed-scrollbar/);
|
||||
assert.match(styleIcons, /ui-icon-map/);
|
||||
assert.match(screenshotsModule, /dataTransfer\.files/);
|
||||
assert.doesNotMatch(`${mhwildsListing}\n${diablo4Listing}\n${diablo4Overview}`, /mhwilds-(home-grid|home-card|heading|title-row|layout|filters|results|grid)/);
|
||||
});
|
||||
|
|
@ -265,6 +298,7 @@ test("legacy svg icons are available for reuse", async () => {
|
|||
await readFile("website/public/static/icons/copy.svg", "utf8");
|
||||
await readFile("website/public/static/icons/abacus.svg", "utf8");
|
||||
await readFile("website/public/static/icons/calculator.svg", "utf8");
|
||||
await readFile("website/public/static/icons/map.svg", "utf8");
|
||||
await readFile("website/public/static/icons/enter.svg", "utf8");
|
||||
await readFile("website/public/static/icons/export.svg", "utf8");
|
||||
await readFile("website/public/static/icons/scrollable.svg", "utf8");
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@
|
|||
"items": [
|
||||
"Capturer les drops rares",
|
||||
"Mettre à jour les crafts suivis",
|
||||
"Archiver les screenshots importants"
|
||||
"Archiver les images importantes"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -88,13 +88,13 @@
|
|||
"importAll": "Importer tout",
|
||||
"exportAll": "Exporter tout",
|
||||
"importOne": "Importer une toolbox",
|
||||
"summary": "Créez des espaces modulaires pour regrouper vos outils de session : notes, suivis, références, captures et futurs modules restent accessibles rapidement, en local, par jeu ou par usage.",
|
||||
"summary": "Créez des espaces modulaires pour regrouper vos outils de session : notes, suivis, références, captures et futurs outils restent accessibles rapidement, en local, par jeu ou par usage.",
|
||||
"storageHelp": {
|
||||
"title": "Sauvegardes locales",
|
||||
"text": "Vos toolboxes restent sur cet appareil, dans ce navigateur. Rien n’est envoyé sur un serveur.",
|
||||
"items": [
|
||||
"Un nettoyage du navigateur ou un changement d’appareil peut supprimer les données.",
|
||||
"Surveillez le quota, surtout si vous ajoutez des screenshots.",
|
||||
"Surveillez le quota, surtout si vous ajoutez des images.",
|
||||
"Faites un export global régulier pour garder une sauvegarde."
|
||||
]
|
||||
},
|
||||
|
|
@ -104,20 +104,80 @@
|
|||
},
|
||||
"checklist": {
|
||||
"itemPlaceholder": "Nouvel item",
|
||||
"importPlaceholder": "Importer plusieurs items...\nPotion:10\nMéga potion:5"
|
||||
"quantityLabel": "Quantité cible",
|
||||
"addButton": "Ajouter",
|
||||
"importPlaceholder": "Importer plusieurs items...\nPotion:10\nMéga potion:5",
|
||||
"importButton": "Importer le texte",
|
||||
"decrementLabel": "Retirer une quantité",
|
||||
"incrementLabel": "Ajouter une quantité",
|
||||
"currentQuantityLabel": "Quantité actuelle",
|
||||
"deleteTitle": "Supprimer"
|
||||
},
|
||||
"screenshots": {
|
||||
"addImages": "Ajouter des images",
|
||||
"pastePlaceholder": "Coller une image ici",
|
||||
"pasteAriaLabel": "Coller une image depuis le presse-papiers",
|
||||
"imageAlt": "Image",
|
||||
"previewAriaLabel": "Agrandir l'image",
|
||||
"annotateAriaLabel": "Annoter l'image",
|
||||
"annotateTitle": "Annoter",
|
||||
"deleteAriaLabel": "Supprimer l'image",
|
||||
"deleteTitle": "Supprimer"
|
||||
},
|
||||
"links": {
|
||||
"titlePlaceholder": "Nom du lien",
|
||||
"urlPlaceholder": "https://...",
|
||||
"importPlaceholder": "Importer plusieurs liens...\nBuild:https://example.com/build?class=rogue\nMap:https://example.com/map"
|
||||
"addButton": "Ajouter",
|
||||
"importPlaceholder": "Importer plusieurs liens...\nBuild:https://example.com/build?class=rogue\nMap:https://example.com/map",
|
||||
"importButton": "Importer le texte",
|
||||
"copyTitle": "Copier",
|
||||
"copiedTitle": "Copié",
|
||||
"deleteTitle": "Supprimer"
|
||||
},
|
||||
"counters": {
|
||||
"labelPlaceholder": "Nom du compteur"
|
||||
"labelPlaceholder": "Nom du compteur",
|
||||
"addButton": "Ajouter",
|
||||
"decrementLabel": "Décrémenter",
|
||||
"incrementLabel": "Incrémenter",
|
||||
"resetTitle": "Réinitialiser",
|
||||
"deleteTitle": "Supprimer"
|
||||
},
|
||||
"calculator": {
|
||||
"expressionLabel": "Calcul",
|
||||
"expressionPlaceholder": "10*10",
|
||||
"resultLabel": "Résultat",
|
||||
"labelLabel": "Libellé",
|
||||
"labelPlaceholder": "ex: Lingots de fer",
|
||||
"scrollableTitle": "Liste scrollable"
|
||||
"saveButton": "Enregistrer",
|
||||
"rootButton": "Revenir à la racine",
|
||||
"resetTitle": "Réinitialiser",
|
||||
"copyTitle": "Copier pour checklist",
|
||||
"copiedTitle": "Copié",
|
||||
"scrollableTitle": "Liste scrollable",
|
||||
"emptyResults": "Aucun résultat enregistré.",
|
||||
"readonlyValueTitle": "Quantité non modifiable",
|
||||
"useEntryTitle": "Utiliser comme base",
|
||||
"renameTitle": "Renommer",
|
||||
"deleteTitle": "Supprimer"
|
||||
},
|
||||
"imageAnnotation": {
|
||||
"addImage": "Ajouter une image",
|
||||
"replaceImage": "Remplacer l'image",
|
||||
"pastePlaceholder": "Coller une image ici",
|
||||
"pasteAriaLabel": "Coller une image à annoter depuis le presse-papiers",
|
||||
"stageAriaLabel": "Ajouter un marqueur sur l'image",
|
||||
"addMarkerAriaLabel": "Ajouter un marqueur sur l'image",
|
||||
"imageAlt": "Image annotée",
|
||||
"previewTitle": "Agrandir",
|
||||
"previewAriaLabel": "Agrandir l'image annotée",
|
||||
"markerPrefix": "Marqueur",
|
||||
"markersSingular": "marqueur",
|
||||
"markersPlural": "marqueurs",
|
||||
"markerPlaceholder": "Libellé du marqueur",
|
||||
"emptyImage": "Ajoutez une image pour commencer l'annotation.",
|
||||
"emptyMarkers": "Agrandissez l'image pour ajouter un marqueur.",
|
||||
"deleteImageTitle": "Supprimer l'image",
|
||||
"deleteMarkerTitle": "Supprimer"
|
||||
}
|
||||
},
|
||||
"emptyTitle": "Aucune toolbox",
|
||||
|
|
|
|||
4
website/public/static/icons/map.svg
Normal file
4
website/public/static/icons/map.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 6H12.01M9 20L3 17V4L5 5M9 20L15 17M9 20V14M15 17L21 20V7L19 6M15 17V14M15 6.2C15 7.96731 13.5 9.4 12 11C10.5 9.4 9 7.96731 9 6.2C9 4.43269 10.3431 3 12 3C13.6569 3 15 4.43269 15 6.2Z" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 510 B |
|
|
@ -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 className="shot-preview" onClick={() => context.setScreenshot(shot)} aria-label={textContent.previewAriaLabel || "Agrandir l'image"}>
|
||||
<img src={shot.dataUrl} alt={textContent.imageAlt || "Image"} />
|
||||
</button>
|
||||
<button className="shot-delete-button danger" onClick={() => {
|
||||
<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="Supprimer le screenshot" title="Supprimer">
|
||||
<span className="ui-icon ui-icon-trash" aria-hidden="true" />
|
||||
}} 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;
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { GamesPage } from "./features/games/GamesPage.jsx";
|
|||
import { INITIAL_DIABLO4_STATE, INITIAL_MHWILDS_STATE, loadDiablo4Data, loadMhwildsData } from "./features/games/loaders.js";
|
||||
import { AddToolControls, ToolboxModules, TOOLBOX_MODULES } from "./features/toolboxes/modules/index.jsx";
|
||||
import { usePointerReorder } from "./hooks/usePointerReorder.js";
|
||||
import { lockBodyScroll } from "./utils/bodyScrollLock.js";
|
||||
|
||||
const STORAGE_KEYS = {
|
||||
registry: "sokkog:toolboxes",
|
||||
|
|
@ -16,7 +17,7 @@ const STORAGE_KEYS = {
|
|||
const APP_STORAGE_PREFIX = "sokkog:";
|
||||
const APP_STORAGE_LIMIT_BYTES = 5 * 1024 * 1024;
|
||||
const APP_STORAGE_WARNING_RATIO = 0.85;
|
||||
const ID_PREFIXES = { tbx: "t", mod: "m", item: "i", shot: "s", link: "l", counter: "c", calc: "r" };
|
||||
const ID_PREFIXES = { tbx: "t", mod: "m", item: "i", shot: "s", link: "l", counter: "c", calc: "r", marker: "k" };
|
||||
const ID_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
||||
const TOOLBOX_ICON_BASE = "/static/img/toolbox-icons/";
|
||||
const TOOLBOX_ICON_FILES = [
|
||||
|
|
@ -99,18 +100,80 @@ const DEFAULT_SITE_CONTENT = {
|
|||
notepad: { placeholder: "Notes rapides..." },
|
||||
checklist: {
|
||||
itemPlaceholder: "Nouvel item",
|
||||
importPlaceholder: "Importer plusieurs items...\nPotion:10\nMéga potion:5"
|
||||
quantityLabel: "Quantité cible",
|
||||
addButton: "Ajouter",
|
||||
importPlaceholder: "Importer plusieurs items...\nPotion:10\nMéga potion:5",
|
||||
importButton: "Importer le texte",
|
||||
decrementLabel: "Retirer une quantité",
|
||||
incrementLabel: "Ajouter une quantité",
|
||||
currentQuantityLabel: "Quantité actuelle",
|
||||
deleteTitle: "Supprimer"
|
||||
},
|
||||
screenshots: {
|
||||
addImages: "Ajouter des images",
|
||||
pastePlaceholder: "Coller une image ici",
|
||||
pasteAriaLabel: "Coller une image depuis le presse-papiers",
|
||||
imageAlt: "Image",
|
||||
previewAriaLabel: "Agrandir l'image",
|
||||
annotateAriaLabel: "Annoter l'image",
|
||||
annotateTitle: "Annoter",
|
||||
deleteAriaLabel: "Supprimer l'image",
|
||||
deleteTitle: "Supprimer"
|
||||
},
|
||||
links: {
|
||||
titlePlaceholder: "Nom du lien",
|
||||
urlPlaceholder: "https://...",
|
||||
importPlaceholder: "Importer plusieurs liens...\nBuild:https://example.com/build?class=rogue\nMap:https://example.com/map"
|
||||
addButton: "Ajouter",
|
||||
importPlaceholder: "Importer plusieurs liens...\nBuild:https://example.com/build?class=rogue\nMap:https://example.com/map",
|
||||
importButton: "Importer le texte",
|
||||
copyTitle: "Copier",
|
||||
copiedTitle: "Copié",
|
||||
deleteTitle: "Supprimer"
|
||||
},
|
||||
counters: {
|
||||
labelPlaceholder: "Nom du compteur",
|
||||
addButton: "Ajouter",
|
||||
decrementLabel: "Décrémenter",
|
||||
incrementLabel: "Incrémenter",
|
||||
resetTitle: "Réinitialiser",
|
||||
deleteTitle: "Supprimer"
|
||||
},
|
||||
counters: { labelPlaceholder: "Nom du compteur" },
|
||||
calculator: {
|
||||
expressionLabel: "Calcul",
|
||||
expressionPlaceholder: "10*10",
|
||||
resultLabel: "Résultat",
|
||||
labelLabel: "Libellé",
|
||||
labelPlaceholder: "Lingots de fer",
|
||||
scrollableTitle: "Liste scrollable"
|
||||
saveButton: "Enregistrer",
|
||||
rootButton: "Revenir à la racine",
|
||||
resetTitle: "Réinitialiser",
|
||||
copyTitle: "Copier pour checklist",
|
||||
copiedTitle: "Copié",
|
||||
scrollableTitle: "Liste scrollable",
|
||||
emptyResults: "Aucun résultat enregistré.",
|
||||
readonlyValueTitle: "Quantité non modifiable",
|
||||
useEntryTitle: "Utiliser comme base",
|
||||
renameTitle: "Renommer",
|
||||
deleteTitle: "Supprimer"
|
||||
},
|
||||
imageAnnotation: {
|
||||
addImage: "Ajouter une image",
|
||||
replaceImage: "Remplacer l'image",
|
||||
pastePlaceholder: "Coller une image ici",
|
||||
pasteAriaLabel: "Coller une image à annoter depuis le presse-papiers",
|
||||
stageAriaLabel: "Ajouter un marqueur sur l'image",
|
||||
addMarkerAriaLabel: "Ajouter un marqueur sur l'image",
|
||||
imageAlt: "Image annotée",
|
||||
previewTitle: "Agrandir",
|
||||
previewAriaLabel: "Agrandir l'image annotée",
|
||||
markerPrefix: "Marqueur",
|
||||
markersSingular: "marqueur",
|
||||
markersPlural: "marqueurs",
|
||||
markerPlaceholder: "Libellé du marqueur",
|
||||
emptyImage: "Ajoutez une image pour commencer l'annotation.",
|
||||
emptyMarkers: "Agrandissez l'image pour ajouter un marqueur.",
|
||||
deleteImageTitle: "Supprimer l'image",
|
||||
deleteMarkerTitle: "Supprimer"
|
||||
}
|
||||
},
|
||||
emptyTitle: "Aucune toolbox",
|
||||
|
|
@ -365,6 +428,24 @@ function normalizeCalculatorData(data) {
|
|||
};
|
||||
}
|
||||
|
||||
function clampPercent(value) {
|
||||
const parsed = Number(value);
|
||||
if (!Number.isFinite(parsed)) return 0;
|
||||
return Math.min(100, Math.max(0, parsed));
|
||||
}
|
||||
|
||||
function normalizeImageAnnotationData(data) {
|
||||
const image = String(data?.image || data?.dataUrl || "");
|
||||
const markers = (data?.markers || [])
|
||||
.map((marker) => ({
|
||||
id: marker?.id || uid("marker"),
|
||||
x: clampPercent(marker?.x),
|
||||
y: clampPercent(marker?.y),
|
||||
label: String(marker?.label || "").trim()
|
||||
}));
|
||||
return { image, markers };
|
||||
}
|
||||
|
||||
function compactChecklistItemForStorage(item) {
|
||||
const normalized = normalizeChecklistItem(item);
|
||||
const compact = { id: normalized.id, label: normalized.label };
|
||||
|
|
@ -388,6 +469,18 @@ function compactModuleDataForStorage(type, value) {
|
|||
.map((shot) => ({ id: shot.id || uid("shot"), dataUrl: shot.dataUrl }));
|
||||
return shots.length ? { shots } : null;
|
||||
}
|
||||
if (type === "imageAnnotation") {
|
||||
const normalized = normalizeImageAnnotationData(value);
|
||||
if (!normalized.image) return null;
|
||||
return {
|
||||
image: normalized.image,
|
||||
markers: normalized.markers.map((marker) => {
|
||||
const compact = { id: marker.id, x: marker.x, y: marker.y };
|
||||
if (marker.label) compact.label = marker.label;
|
||||
return compact;
|
||||
})
|
||||
};
|
||||
}
|
||||
if (type === "links") {
|
||||
const links = normalizeLinksData(value).links.map((link) => {
|
||||
const compact = {
|
||||
|
|
@ -424,9 +517,9 @@ function compactModuleDataForStorage(type, value) {
|
|||
return value;
|
||||
}
|
||||
|
||||
function setModuleData(toolboxes, toolboxId, moduleId, value) {
|
||||
function setModuleData(toolboxes, toolboxId, moduleId, value, moduleType = "") {
|
||||
const module = toolboxes.find((toolbox) => toolbox.id === toolboxId)?.modules.find((item) => item.id === moduleId);
|
||||
const compact = compactModuleDataForStorage(module?.type, value);
|
||||
const compact = compactModuleDataForStorage(moduleType || module?.type, value);
|
||||
const key = moduleStorageKey(toolboxId, moduleId);
|
||||
if (compact == null) {
|
||||
localStorage.removeItem(key);
|
||||
|
|
@ -453,6 +546,13 @@ function remapModuleDataForExport(type, data, nextId) {
|
|||
return { shots: compact.shots.map((shot) => ({ ...shot, id: nextId("shot") })) };
|
||||
}
|
||||
|
||||
if (type === "imageAnnotation") {
|
||||
return {
|
||||
...compact,
|
||||
markers: compact.markers.map((marker) => ({ ...marker, id: nextId("marker") }))
|
||||
};
|
||||
}
|
||||
|
||||
if (type === "links") {
|
||||
return { links: compact.links.map((link) => ({ ...link, id: nextId("link") })) };
|
||||
}
|
||||
|
|
@ -603,6 +703,10 @@ function useHashRoute() {
|
|||
return route;
|
||||
}
|
||||
|
||||
function useModalScrollLock() {
|
||||
useEffect(() => lockBodyScroll(), []);
|
||||
}
|
||||
|
||||
function useLocalToolboxes(onError) {
|
||||
const [toolboxes, setToolboxesState] = useState(loadToolboxes);
|
||||
const [links, setLinksState] = useState(loadLinks);
|
||||
|
|
@ -636,9 +740,9 @@ function useLocalToolboxes(onError) {
|
|||
}
|
||||
}
|
||||
|
||||
function updateModuleData(toolboxId, moduleId, value) {
|
||||
function updateModuleData(toolboxId, moduleId, value, moduleType = "") {
|
||||
try {
|
||||
setModuleData(toolboxes, toolboxId, moduleId, value);
|
||||
setModuleData(toolboxes, toolboxId, moduleId, value, moduleType);
|
||||
refreshQuota();
|
||||
return true;
|
||||
} catch (error) {
|
||||
|
|
@ -1286,13 +1390,16 @@ function ToolboxView({ siteContent, toolbox, toolboxGame, embedded, actions, upd
|
|||
normalizeLinksData,
|
||||
normalizeCountersData,
|
||||
normalizeCalculatorData,
|
||||
normalizeImageAnnotationData,
|
||||
normalizeUrl,
|
||||
hostnameFromUrl,
|
||||
copyText,
|
||||
compressImageFile: compressImage,
|
||||
clampQty,
|
||||
uid,
|
||||
setModuleData: updateModuleData,
|
||||
addScreenshotFiles,
|
||||
createImageAnnotationModule,
|
||||
setScreenshot: actions.setScreenshot,
|
||||
refresh: () => {}
|
||||
};
|
||||
|
|
@ -1302,6 +1409,13 @@ function ToolboxView({ siteContent, toolbox, toolboxGame, embedded, actions, upd
|
|||
updateToolbox({ ...toolbox, modules: [...toolbox.modules, { id: uid("mod"), type }] });
|
||||
}
|
||||
|
||||
function createImageAnnotationModule(dataUrl) {
|
||||
if (!dataUrl) return;
|
||||
const moduleId = uid("mod");
|
||||
updateToolbox({ ...toolbox, modules: [...toolbox.modules, { id: moduleId, type: "imageAnnotation" }] });
|
||||
updateModuleData(toolbox.id, moduleId, { image: dataUrl, markers: [] }, "imageAnnotation");
|
||||
}
|
||||
|
||||
function moveModule(fromModuleId, toModuleId, placement = "before") {
|
||||
if (!fromModuleId || !toModuleId || fromModuleId === toModuleId) return;
|
||||
const modules = [...toolbox.modules];
|
||||
|
|
@ -1465,6 +1579,7 @@ function EditableTitle({ value, fallback, onSave, className = "module-title" })
|
|||
}
|
||||
|
||||
function ConfirmModal({ title, message, confirmLabel = "Confirmer", cancelLabel = "Annuler", danger = false, onClose }) {
|
||||
useModalScrollLock();
|
||||
return (
|
||||
<div className="confirm-modal-root">
|
||||
<div className="confirm-backdrop" onClick={() => onClose(false)} />
|
||||
|
|
@ -1481,6 +1596,7 @@ function ConfirmModal({ title, message, confirmLabel = "Confirmer", cancelLabel
|
|||
}
|
||||
|
||||
function CreateToolboxModal({ gameId, onClose }) {
|
||||
useModalScrollLock();
|
||||
const [name, setName] = useState("");
|
||||
return (
|
||||
<div className="confirm-modal-root">
|
||||
|
|
@ -1500,6 +1616,7 @@ function CreateToolboxModal({ gameId, onClose }) {
|
|||
}
|
||||
|
||||
function LinkToolboxModal({ toolboxes, selectedId, onClose }) {
|
||||
useModalScrollLock();
|
||||
const [toolboxId, setToolboxId] = useState(selectedId);
|
||||
return (
|
||||
<div className="confirm-modal-root">
|
||||
|
|
@ -1519,6 +1636,38 @@ function LinkToolboxModal({ toolboxes, selectedId, onClose }) {
|
|||
}
|
||||
|
||||
function ScreenshotViewer({ shot, onClose }) {
|
||||
useModalScrollLock();
|
||||
const canAnnotate = Boolean(shot.canAnnotate && shot.onChangeMarkers);
|
||||
const [viewerMarkers, setViewerMarkers] = useState(() => Array.isArray(shot.markers) ? shot.markers : []);
|
||||
const viewerRef = useRef(null);
|
||||
const mediaRef = useRef(null);
|
||||
const hasMarkers = viewerMarkers.length > 0;
|
||||
|
||||
useEffect(() => {
|
||||
setViewerMarkers(Array.isArray(shot.markers) ? shot.markers : []);
|
||||
}, [shot]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!viewerRef.current || !mediaRef.current) return undefined;
|
||||
const viewer = viewerRef.current;
|
||||
const frame = mediaRef.current;
|
||||
|
||||
function syncImageHeight() {
|
||||
const maxHeight = Math.max(400, window.innerHeight * 0.8);
|
||||
const height = Math.min(maxHeight, Math.max(400, Math.round(frame.getBoundingClientRect().height)));
|
||||
viewer.style.setProperty("--viewer-image-height", `${height}px`);
|
||||
}
|
||||
|
||||
syncImageHeight();
|
||||
const observer = new ResizeObserver(syncImageHeight);
|
||||
observer.observe(frame);
|
||||
window.addEventListener("resize", syncImageHeight);
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
window.removeEventListener("resize", syncImageHeight);
|
||||
};
|
||||
}, [shot.dataUrl, canAnnotate]);
|
||||
|
||||
useEffect(() => {
|
||||
const onKeyDown = (event) => {
|
||||
if (event.key === "Escape") onClose();
|
||||
|
|
@ -1526,15 +1675,96 @@ function ScreenshotViewer({ shot, onClose }) {
|
|||
document.addEventListener("keydown", onKeyDown);
|
||||
return () => document.removeEventListener("keydown", onKeyDown);
|
||||
}, [onClose]);
|
||||
|
||||
function updateViewerMarkers(nextMarkers) {
|
||||
setViewerMarkers(nextMarkers);
|
||||
shot.onChangeMarkers?.(nextMarkers);
|
||||
}
|
||||
|
||||
function addViewerMarker(event) {
|
||||
if (!canAnnotate || !mediaRef.current) return;
|
||||
const rect = mediaRef.current.getBoundingClientRect();
|
||||
const nextMarkers = [
|
||||
...viewerMarkers,
|
||||
{
|
||||
id: shot.createMarkerId?.() || uid("marker"),
|
||||
label: "",
|
||||
x: clampPercent(((event.clientX - rect.left) / rect.width) * 100),
|
||||
y: clampPercent(((event.clientY - rect.top) / rect.height) * 100)
|
||||
}
|
||||
];
|
||||
updateViewerMarkers(nextMarkers);
|
||||
}
|
||||
|
||||
function updateViewerMarker(markerId, updater) {
|
||||
updateViewerMarkers(viewerMarkers.map((marker) => marker.id === markerId ? updater(marker) : marker));
|
||||
}
|
||||
|
||||
function removeViewerMarker(markerId) {
|
||||
updateViewerMarkers(viewerMarkers.filter((marker) => marker.id !== markerId));
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="screenshot-viewer-root">
|
||||
<div className="screenshot-viewer-backdrop" onClick={onClose} />
|
||||
<section className="screenshot-viewer" role="dialog" aria-modal="true" aria-label="Screenshot">
|
||||
<section ref={viewerRef} className="screenshot-viewer" role="dialog" aria-modal="true" aria-label="Screenshot">
|
||||
<header>
|
||||
<button className="screenshot-viewer-button" onClick={() => openImageInNewTab(shot.dataUrl)} aria-label="Ouvrir le screenshot en taille réelle" title="Taille réelle"><Icon name="zoom" /></button>
|
||||
{!canAnnotate && !hasMarkers && (
|
||||
<button className="screenshot-viewer-button" onClick={() => openImageInNewTab(shot.dataUrl)} aria-label="Ouvrir l'image en taille réelle" title="Taille réelle"><Icon name="zoom" /></button>
|
||||
)}
|
||||
<button className="screenshot-viewer-button" onClick={onClose} aria-label="Fermer" title="Fermer"><Icon name="close" /></button>
|
||||
</header>
|
||||
<img src={shot.dataUrl} alt="Screenshot" />
|
||||
<div className={`screenshot-viewer-body ${canAnnotate ? "has-annotation-side" : ""}`}>
|
||||
<div
|
||||
className={`screenshot-viewer-media ${canAnnotate ? "can-annotate" : ""}`}
|
||||
>
|
||||
<div
|
||||
ref={mediaRef}
|
||||
className="screenshot-viewer-image-frame"
|
||||
onClick={addViewerMarker}
|
||||
role={canAnnotate ? "button" : undefined}
|
||||
tabIndex={canAnnotate ? 0 : undefined}
|
||||
aria-label={canAnnotate ? shot.addMarkerAriaLabel || "Ajouter un marqueur sur l'image" : undefined}
|
||||
>
|
||||
<img src={shot.dataUrl} alt={shot.alt || "Screenshot"} />
|
||||
{viewerMarkers.map((marker, index) => (
|
||||
<span
|
||||
className="annotation-marker screenshot-viewer-marker"
|
||||
key={marker.id || `${marker.x}-${marker.y}-${index}`}
|
||||
style={{ "--marker-x": `${marker.x}%`, "--marker-y": `${marker.y}%` }}
|
||||
title={marker.label || `${shot.markerPrefix || "Marqueur"} ${index + 1}`}
|
||||
>
|
||||
{index + 1}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{canAnnotate && (
|
||||
<aside className="screenshot-viewer-side">
|
||||
<strong>{viewerMarkers.length} {viewerMarkers.length > 1 ? "marqueurs" : "marqueur"}</strong>
|
||||
{viewerMarkers.length ? (
|
||||
<ol className="annotation-marker-list themed-scrollbar">
|
||||
{viewerMarkers.map((marker, index) => (
|
||||
<li key={marker.id}>
|
||||
<span>{index + 1}</span>
|
||||
<input
|
||||
value={marker.label}
|
||||
placeholder={shot.markerPlaceholder || "Libellé du marqueur"}
|
||||
onChange={(event) => updateViewerMarker(marker.id, (item) => ({ ...item, label: event.target.value }))}
|
||||
aria-label={`Libellé du marqueur ${index + 1}`}
|
||||
/>
|
||||
<button className="calculator-action-button danger" type="button" onClick={() => removeViewerMarker(marker.id)} aria-label={`${shot.deleteMarkerTitle || "Supprimer"} ${marker.label || `${shot.markerPrefix || "Marqueur"} ${index + 1}`}`} title={shot.deleteMarkerTitle || "Supprimer"}>
|
||||
<Icon name="trash" />
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
) : (
|
||||
<p className="muted">Cliquez sur l'image pour ajouter un marqueur.</p>
|
||||
)}
|
||||
</aside>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -145,9 +145,16 @@ input[type="checkbox"]:focus-visible {
|
|||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
.themed-scrollbar,
|
||||
.legacy-scrollbar {
|
||||
scrollbar-color: rgb(79, 26, 114) transparent;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.filter-options::-webkit-scrollbar,
|
||||
.drawer-panel::-webkit-scrollbar,
|
||||
.damage-table-wrap::-webkit-scrollbar,
|
||||
.themed-scrollbar::-webkit-scrollbar,
|
||||
.legacy-scrollbar::-webkit-scrollbar {
|
||||
background-color: transparent;
|
||||
width: 6px;
|
||||
|
|
@ -158,6 +165,7 @@ input[type="checkbox"]:focus-visible {
|
|||
.filter-options::-webkit-scrollbar-track,
|
||||
.drawer-panel::-webkit-scrollbar-track,
|
||||
.damage-table-wrap::-webkit-scrollbar-track,
|
||||
.themed-scrollbar::-webkit-scrollbar-track,
|
||||
.legacy-scrollbar::-webkit-scrollbar-track {
|
||||
background-color: transparent;
|
||||
border-radius: 0;
|
||||
|
|
@ -167,11 +175,16 @@ input[type="checkbox"]:focus-visible {
|
|||
.filter-options::-webkit-scrollbar-thumb,
|
||||
.drawer-panel::-webkit-scrollbar-thumb,
|
||||
.damage-table-wrap::-webkit-scrollbar-thumb,
|
||||
.themed-scrollbar::-webkit-scrollbar-thumb,
|
||||
.legacy-scrollbar::-webkit-scrollbar-thumb {
|
||||
background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0.5, rgb(79, 26, 114)), color-stop(1, rgb(44, 1, 135)));
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
body.is-modal-open {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.primary {
|
||||
border-color: rgba(246, 196, 83, 0.34);
|
||||
background:
|
||||
|
|
@ -224,18 +237,22 @@ input[type="checkbox"]:focus-visible {
|
|||
inset 0 1px 0 rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.sidebar-action-button,
|
||||
.import-icon-button,
|
||||
.toolbox-icon-button,
|
||||
.card-icon-button,
|
||||
.drawer-action-button,
|
||||
.drawer-close-button,
|
||||
.filter-reset-button,
|
||||
.screenshot-viewer-button,
|
||||
.module-edit-button,
|
||||
.module-delete-button,
|
||||
.shot-delete-button,
|
||||
.tool-quick-add-button {
|
||||
:is(
|
||||
.sidebar-action-button,
|
||||
.import-icon-button,
|
||||
.toolbox-icon-button,
|
||||
.card-icon-button,
|
||||
.drawer-action-button,
|
||||
.drawer-close-button,
|
||||
.filter-reset-button,
|
||||
.screenshot-viewer-button,
|
||||
.module-edit-button,
|
||||
.module-delete-button,
|
||||
.shot-annotate-button,
|
||||
.shot-delete-button,
|
||||
.checklist-delete-button,
|
||||
.tool-quick-add-button
|
||||
) {
|
||||
display: inline-flex;
|
||||
width: 40px;
|
||||
min-width: 40px;
|
||||
|
|
@ -257,35 +274,43 @@ input[type="checkbox"]:focus-visible {
|
|||
box-shadow var(--duration-fast) var(--ease-standard);
|
||||
}
|
||||
|
||||
.sidebar-action-button .ui-icon,
|
||||
.import-icon-button .ui-icon,
|
||||
.toolbox-icon-button .ui-icon,
|
||||
.card-icon-button .ui-icon,
|
||||
.drawer-action-button .ui-icon,
|
||||
.drawer-close-button .ui-icon,
|
||||
.filter-reset-button .ui-icon,
|
||||
.screenshot-viewer-button .ui-icon,
|
||||
.module-edit-button .ui-icon,
|
||||
.module-delete-button .ui-icon,
|
||||
.shot-delete-button .ui-icon,
|
||||
.tool-quick-add-button .ui-icon {
|
||||
:is(
|
||||
.sidebar-action-button,
|
||||
.import-icon-button,
|
||||
.toolbox-icon-button,
|
||||
.card-icon-button,
|
||||
.drawer-action-button,
|
||||
.drawer-close-button,
|
||||
.filter-reset-button,
|
||||
.screenshot-viewer-button,
|
||||
.module-edit-button,
|
||||
.module-delete-button,
|
||||
.shot-annotate-button,
|
||||
.shot-delete-button,
|
||||
.checklist-delete-button,
|
||||
.tool-quick-add-button
|
||||
) .ui-icon {
|
||||
transition:
|
||||
background-color var(--duration-fast) var(--ease-standard),
|
||||
filter var(--duration-fast) var(--ease-standard);
|
||||
}
|
||||
|
||||
.sidebar-action-button:hover,
|
||||
.import-icon-button:hover,
|
||||
.toolbox-icon-button:hover,
|
||||
.card-icon-button:hover,
|
||||
.drawer-action-button:hover,
|
||||
.drawer-close-button:hover,
|
||||
.filter-reset-button:hover,
|
||||
.screenshot-viewer-button:hover,
|
||||
.module-edit-button:hover,
|
||||
.module-delete-button:hover,
|
||||
.shot-delete-button:hover,
|
||||
.tool-quick-add-button:hover {
|
||||
:is(
|
||||
.sidebar-action-button,
|
||||
.import-icon-button,
|
||||
.toolbox-icon-button,
|
||||
.card-icon-button,
|
||||
.drawer-action-button,
|
||||
.drawer-close-button,
|
||||
.filter-reset-button,
|
||||
.screenshot-viewer-button,
|
||||
.module-edit-button,
|
||||
.module-delete-button,
|
||||
.shot-annotate-button,
|
||||
.shot-delete-button,
|
||||
.checklist-delete-button,
|
||||
.tool-quick-add-button
|
||||
):hover {
|
||||
border-color: rgba(196, 181, 253, 0.32);
|
||||
background:
|
||||
linear-gradient(135deg, rgba(31, 37, 68, 0.94), rgba(18, 22, 44, 0.96)) padding-box,
|
||||
|
|
@ -296,18 +321,21 @@ input[type="checkbox"]:focus-visible {
|
|||
inset 0 1px 0 rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.sidebar-action-button.primary,
|
||||
.import-icon-button.primary,
|
||||
.toolbox-icon-button.primary,
|
||||
.card-icon-button.primary,
|
||||
.drawer-action-button.primary,
|
||||
.drawer-close-button.primary,
|
||||
.filter-reset-button.primary,
|
||||
.screenshot-viewer-button.primary,
|
||||
.module-edit-button.primary,
|
||||
.module-delete-button.primary,
|
||||
.shot-delete-button.primary,
|
||||
.tool-quick-add-button.primary {
|
||||
:is(
|
||||
.sidebar-action-button,
|
||||
.import-icon-button,
|
||||
.toolbox-icon-button,
|
||||
.card-icon-button,
|
||||
.drawer-action-button,
|
||||
.drawer-close-button,
|
||||
.filter-reset-button,
|
||||
.screenshot-viewer-button,
|
||||
.module-edit-button,
|
||||
.module-delete-button,
|
||||
.shot-annotate-button,
|
||||
.shot-delete-button,
|
||||
.tool-quick-add-button
|
||||
).primary {
|
||||
border-color: rgba(246, 196, 83, 0.34);
|
||||
background:
|
||||
linear-gradient(135deg, rgba(72, 43, 160, 0.96), rgba(45, 52, 146, 0.94)) padding-box,
|
||||
|
|
@ -318,18 +346,21 @@ input[type="checkbox"]:focus-visible {
|
|||
inset 0 1px 0 rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.sidebar-action-button.primary:hover,
|
||||
.import-icon-button.primary:hover,
|
||||
.toolbox-icon-button.primary:hover,
|
||||
.card-icon-button.primary:hover,
|
||||
.drawer-action-button.primary:hover,
|
||||
.drawer-close-button.primary:hover,
|
||||
.filter-reset-button.primary:hover,
|
||||
.screenshot-viewer-button.primary:hover,
|
||||
.module-edit-button.primary:hover,
|
||||
.module-delete-button.primary:hover,
|
||||
.shot-delete-button.primary:hover,
|
||||
.tool-quick-add-button.primary:hover {
|
||||
:is(
|
||||
.sidebar-action-button,
|
||||
.import-icon-button,
|
||||
.toolbox-icon-button,
|
||||
.card-icon-button,
|
||||
.drawer-action-button,
|
||||
.drawer-close-button,
|
||||
.filter-reset-button,
|
||||
.screenshot-viewer-button,
|
||||
.module-edit-button,
|
||||
.module-delete-button,
|
||||
.shot-annotate-button,
|
||||
.shot-delete-button,
|
||||
.tool-quick-add-button
|
||||
).primary:hover {
|
||||
border-color: rgba(246, 196, 83, 0.72);
|
||||
background:
|
||||
linear-gradient(135deg, rgba(109, 40, 217, 0.98), rgba(67, 56, 202, 0.96)) padding-box,
|
||||
|
|
@ -342,30 +373,32 @@ input[type="checkbox"]:focus-visible {
|
|||
inset 0 1px 0 rgba(255, 255, 255, 0.18);
|
||||
}
|
||||
|
||||
.sidebar-action-button:hover .ui-icon,
|
||||
.import-icon-button:hover .ui-icon,
|
||||
.toolbox-icon-button:hover .ui-icon,
|
||||
.card-icon-button:hover .ui-icon,
|
||||
.drawer-action-button:hover .ui-icon,
|
||||
.drawer-close-button:hover .ui-icon,
|
||||
.filter-reset-button:hover .ui-icon,
|
||||
.screenshot-viewer-button:hover .ui-icon,
|
||||
.module-edit-button:hover .ui-icon,
|
||||
.tool-quick-add-button:hover .ui-icon {
|
||||
:is(
|
||||
.sidebar-action-button,
|
||||
.import-icon-button,
|
||||
.toolbox-icon-button,
|
||||
.card-icon-button,
|
||||
.drawer-action-button,
|
||||
.drawer-close-button,
|
||||
.filter-reset-button,
|
||||
.screenshot-viewer-button,
|
||||
.module-edit-button,
|
||||
.module-delete-button,
|
||||
.shot-annotate-button,
|
||||
.shot-delete-button,
|
||||
.checklist-delete-button,
|
||||
.tool-quick-add-button
|
||||
):hover .ui-icon {
|
||||
background-color: #c4b5fd;
|
||||
filter: drop-shadow(0 0 6px rgba(196, 181, 253, 0.28));
|
||||
}
|
||||
|
||||
.card-icon-button.danger,
|
||||
.module-delete-button.danger,
|
||||
.shot-delete-button.danger {
|
||||
:is(.card-icon-button, .module-delete-button, .shot-delete-button, .checklist-delete-button).danger {
|
||||
border-color: rgba(251, 113, 133, 0.2);
|
||||
color: var(--color-danger);
|
||||
}
|
||||
|
||||
.card-icon-button.danger:hover,
|
||||
.module-delete-button.danger:hover,
|
||||
.shot-delete-button.danger:hover {
|
||||
:is(.card-icon-button, .module-delete-button, .shot-delete-button, .checklist-delete-button).danger:hover {
|
||||
border-color: rgba(251, 113, 133, 0.56);
|
||||
background:
|
||||
linear-gradient(135deg, rgba(80, 25, 42, 0.72), rgba(48, 18, 34, 0.82)) padding-box,
|
||||
|
|
@ -376,9 +409,7 @@ input[type="checkbox"]:focus-visible {
|
|||
inset 0 1px 0 rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.card-icon-button.danger:hover .ui-icon,
|
||||
.module-delete-button.danger:hover .ui-icon,
|
||||
.shot-delete-button.danger:hover .ui-icon {
|
||||
:is(.card-icon-button, .module-delete-button, .shot-delete-button, .checklist-delete-button).danger:hover .ui-icon {
|
||||
background-color: #fff;
|
||||
filter: drop-shadow(0 0 6px rgba(251, 113, 133, 0.26));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -195,13 +195,26 @@
|
|||
.filter-logic button {
|
||||
min-height: 30px;
|
||||
padding: 0 12px;
|
||||
background: var(--color-primary-soft);
|
||||
border-color: rgba(246, 196, 83, 0.24);
|
||||
background:
|
||||
linear-gradient(135deg, rgba(72, 43, 160, 0.72), rgba(45, 52, 146, 0.68)) padding-box,
|
||||
linear-gradient(135deg, rgba(246, 196, 83, 0.46), rgba(139, 92, 246, 0.28)) border-box;
|
||||
color: var(--color-text-primary);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 800;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.filter-logic button:hover {
|
||||
border-color: rgba(246, 196, 83, 0.52);
|
||||
background:
|
||||
linear-gradient(135deg, rgba(109, 40, 217, 0.86), rgba(67, 56, 202, 0.82)) padding-box,
|
||||
linear-gradient(135deg, rgba(246, 196, 83, 0.72), rgba(196, 181, 253, 0.42)) border-box;
|
||||
box-shadow:
|
||||
0 0 14px rgba(139, 92, 246, 0.14),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.filter-options {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
|
|
|
|||
|
|
@ -66,6 +66,11 @@
|
|||
-webkit-mask-image: url("/static/icons/calculator.svg");
|
||||
}
|
||||
|
||||
.ui-icon-map {
|
||||
mask-image: url("/static/icons/map.svg");
|
||||
-webkit-mask-image: url("/static/icons/map.svg");
|
||||
}
|
||||
|
||||
.ui-icon-scrollable {
|
||||
mask-image: url("/static/icons/scrollable.svg");
|
||||
-webkit-mask-image: url("/static/icons/scrollable.svg");
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
display: grid;
|
||||
place-items: center;
|
||||
padding: var(--space-5);
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
.screenshot-viewer-backdrop {
|
||||
|
|
@ -15,9 +16,11 @@
|
|||
}
|
||||
|
||||
.screenshot-viewer {
|
||||
--viewer-content-max-height: min(80vh, calc(100vh - 140px));
|
||||
--viewer-image-height: 400px;
|
||||
position: relative;
|
||||
display: grid;
|
||||
width: min(1120px, calc(100vw - 32px));
|
||||
width: min(1500px, calc(100vw - 32px));
|
||||
max-height: calc(100vh - 48px);
|
||||
gap: var(--space-4);
|
||||
padding: var(--space-4);
|
||||
|
|
@ -38,15 +41,106 @@
|
|||
flex: 0 0 40px;
|
||||
}
|
||||
|
||||
.screenshot-viewer img {
|
||||
display: block;
|
||||
.screenshot-viewer-body {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
justify-items: center;
|
||||
}
|
||||
|
||||
.screenshot-viewer-body.has-annotation-side {
|
||||
grid-template-columns: minmax(0, 1fr) minmax(240px, 340px);
|
||||
gap: var(--space-4);
|
||||
align-items: start;
|
||||
justify-items: stretch;
|
||||
}
|
||||
|
||||
.screenshot-viewer-media {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
max-height: calc(100vh - 170px);
|
||||
justify-self: center;
|
||||
justify-content: center;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.screenshot-viewer-media.can-annotate {
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.screenshot-viewer-image-frame {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
border-radius: var(--radius-md);
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
.screenshot-viewer-image-frame img {
|
||||
display: block;
|
||||
width: auto;
|
||||
height: auto;
|
||||
max-width: 100%;
|
||||
max-height: var(--viewer-content-max-height);
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.screenshot-viewer-marker {
|
||||
display: inline-grid;
|
||||
width: 30px;
|
||||
min-width: 30px;
|
||||
height: 30px;
|
||||
min-height: 30px;
|
||||
place-items: center;
|
||||
padding: 0;
|
||||
border: 1px solid rgba(246, 196, 83, 0.72);
|
||||
border-radius: var(--radius-pill);
|
||||
background:
|
||||
linear-gradient(135deg, rgba(72, 43, 160, 0.96), rgba(45, 52, 146, 0.94)) padding-box,
|
||||
linear-gradient(135deg, rgba(246, 196, 83, 0.9), rgba(196, 181, 253, 0.5)) border-box;
|
||||
color: #fff;
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 900;
|
||||
line-height: 1;
|
||||
box-shadow:
|
||||
0 0 0 2px rgba(5, 7, 17, 0.72),
|
||||
0 0 16px rgba(246, 196, 83, 0.22);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.screenshot-viewer-side {
|
||||
display: grid;
|
||||
align-content: start;
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
gap: var(--space-3);
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
max-width: 340px;
|
||||
height: min(var(--viewer-image-height), var(--viewer-content-max-height));
|
||||
max-height: min(var(--viewer-image-height), var(--viewer-content-max-height));
|
||||
overflow: hidden;
|
||||
padding: 12px;
|
||||
border: 1px solid rgba(165, 180, 252, 0.12);
|
||||
border-radius: var(--radius-md);
|
||||
background:
|
||||
linear-gradient(135deg, rgba(139, 92, 246, 0.045), transparent 62%),
|
||||
rgba(7, 10, 24, 0.38);
|
||||
}
|
||||
|
||||
.screenshot-viewer-side strong {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.screenshot-viewer-side .annotation-marker-list {
|
||||
min-width: 0;
|
||||
max-height: 100%;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.drawer[aria-hidden="true"] {
|
||||
display: none;
|
||||
}
|
||||
|
|
@ -166,6 +260,7 @@ body.is-resizing-drawer * {
|
|||
display: grid;
|
||||
place-items: center;
|
||||
padding: var(--space-5);
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
.tool-add-modal-root {
|
||||
|
|
@ -175,6 +270,7 @@ body.is-resizing-drawer * {
|
|||
display: grid;
|
||||
place-items: center;
|
||||
padding: var(--space-5);
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
.confirm-backdrop {
|
||||
|
|
|
|||
|
|
@ -139,6 +139,14 @@
|
|||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.screenshot-viewer-body.has-annotation-side {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.screenshot-viewer-side {
|
||||
height: min(280px, 32vh);
|
||||
}
|
||||
|
||||
.module-column {
|
||||
display: contents;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -148,7 +148,17 @@
|
|||
min-height: 42px;
|
||||
padding: 0;
|
||||
border-color: var(--color-border);
|
||||
background: rgba(21, 26, 48, 0.72);
|
||||
background: rgba(21, 26, 48, 0.88);
|
||||
}
|
||||
|
||||
.toolbox-icon-picker-menu button:hover {
|
||||
border-color: rgba(196, 181, 253, 0.32);
|
||||
background:
|
||||
linear-gradient(135deg, rgba(31, 37, 68, 0.94), rgba(18, 22, 44, 0.96)) padding-box,
|
||||
linear-gradient(135deg, rgba(139, 92, 246, 0.3), rgba(196, 181, 253, 0.2)) border-box;
|
||||
box-shadow:
|
||||
0 0 12px rgba(139, 92, 246, 0.1),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.toolbox-icon-picker-menu button.active {
|
||||
|
|
@ -208,11 +218,11 @@
|
|||
top: calc(100% + 8px);
|
||||
right: 0;
|
||||
z-index: 300;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 40px);
|
||||
justify-content: flex-end;
|
||||
width: max-content;
|
||||
max-width: min(280px, calc(100vw - 48px));
|
||||
max-width: calc(100vw - 48px);
|
||||
gap: 6px;
|
||||
padding: 8px;
|
||||
border: 1px solid var(--color-border);
|
||||
|
|
@ -425,8 +435,18 @@
|
|||
font-weight: 900;
|
||||
}
|
||||
|
||||
.layout-switch button:hover {
|
||||
border-color: rgba(196, 181, 253, 0.22);
|
||||
background: rgba(21, 26, 48, 0.5);
|
||||
color: var(--color-text-primary);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.layout-switch button.active {
|
||||
background: var(--gradient-brand);
|
||||
border-color: rgba(246, 196, 83, 0.34);
|
||||
background:
|
||||
linear-gradient(135deg, rgba(72, 43, 160, 0.96), rgba(45, 52, 146, 0.94)) padding-box,
|
||||
linear-gradient(135deg, rgba(246, 196, 83, 0.66), rgba(139, 92, 246, 0.36)) border-box;
|
||||
color: white;
|
||||
box-shadow: var(--shadow-primary);
|
||||
}
|
||||
|
|
@ -544,6 +564,11 @@
|
|||
-webkit-mask-image: url("/static/icons/calculator.svg");
|
||||
}
|
||||
|
||||
.module-icon-map {
|
||||
mask-image: url("/static/icons/map.svg");
|
||||
-webkit-mask-image: url("/static/icons/map.svg");
|
||||
}
|
||||
|
||||
.notepad {
|
||||
display: block;
|
||||
width: calc(100% - 32px);
|
||||
|
|
@ -673,6 +698,10 @@ textarea:focus {
|
|||
min-width: 28px;
|
||||
min-height: 28px;
|
||||
padding: 0;
|
||||
border: 1px solid rgba(165, 180, 252, 0.12);
|
||||
border-radius: var(--radius-md);
|
||||
background: rgba(21, 26, 48, 0.88);
|
||||
color: var(--color-text-secondary);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
|
|
@ -781,9 +810,6 @@ textarea:focus {
|
|||
}
|
||||
|
||||
.counter-actions button {
|
||||
min-width: 0;
|
||||
min-height: 34px;
|
||||
padding: 0;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
|
|
@ -903,7 +929,10 @@ textarea:focus {
|
|||
background: rgba(5, 7, 17, 0.24);
|
||||
}
|
||||
|
||||
.calculator-action-button {
|
||||
.calculator-action-button,
|
||||
.calculator-entry > button:not(.calculator-entry-summary, .calculator-entry-value),
|
||||
.counter-actions button,
|
||||
.link-item button {
|
||||
display: inline-flex;
|
||||
width: 34px;
|
||||
min-width: 34px;
|
||||
|
|
@ -913,7 +942,7 @@ textarea:focus {
|
|||
padding: 0;
|
||||
border: 1px solid rgba(165, 180, 252, 0.12);
|
||||
border-radius: var(--radius-md);
|
||||
background: rgba(5, 7, 17, 0.24);
|
||||
background: rgba(21, 26, 48, 0.88);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
|
|
@ -922,21 +951,6 @@ textarea:focus {
|
|||
opacity: 0.48;
|
||||
}
|
||||
|
||||
.calculator-action-button.danger {
|
||||
border-color: rgba(251, 113, 133, 0.2);
|
||||
background:
|
||||
linear-gradient(135deg, rgba(251, 113, 133, 0.1), rgba(7, 10, 24, 0.32));
|
||||
color: #fecdd3;
|
||||
}
|
||||
|
||||
.calculator-action-button.danger:hover {
|
||||
border-color: rgba(251, 113, 133, 0.54);
|
||||
background:
|
||||
linear-gradient(135deg, rgba(251, 113, 133, 0.2), rgba(7, 10, 24, 0.44));
|
||||
color: #fff1f2;
|
||||
box-shadow: 0 0 14px rgba(251, 113, 133, 0.16);
|
||||
}
|
||||
|
||||
.calculator-action-button .ui-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
|
|
@ -995,14 +1009,58 @@ textarea:focus {
|
|||
|
||||
.calculator-root-button:hover,
|
||||
.calculator-action-button:hover:not(:disabled),
|
||||
.checklist-qty-controls button:hover,
|
||||
.calculator-entry > button:not(.calculator-entry-summary, .calculator-entry-value, .danger):hover,
|
||||
.counter-actions button:not(.danger):hover,
|
||||
.link-item button:not(.danger):hover,
|
||||
.calculator-scroll-toggle:hover,
|
||||
.calculator-scroll-toggle.active {
|
||||
border-color: rgba(196, 181, 253, 0.32);
|
||||
background:
|
||||
linear-gradient(135deg, rgba(31, 37, 68, 0.94), rgba(18, 22, 44, 0.96)) padding-box,
|
||||
linear-gradient(135deg, rgba(139, 92, 246, 0.3), rgba(196, 181, 253, 0.2)) border-box;
|
||||
color: var(--color-text-primary);
|
||||
box-shadow:
|
||||
0 0 12px rgba(139, 92, 246, 0.1),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.calculator-action-button.danger,
|
||||
.calculator-entry > button.danger,
|
||||
.counter-actions button.danger,
|
||||
.link-item button.danger {
|
||||
border-color: rgba(251, 113, 133, 0.2);
|
||||
background: rgba(21, 26, 48, 0.88);
|
||||
color: var(--color-danger);
|
||||
}
|
||||
|
||||
.calculator-action-button.danger:hover:not(:disabled),
|
||||
.calculator-entry > button.danger:hover,
|
||||
.counter-actions button.danger:hover,
|
||||
.link-item button.danger:hover {
|
||||
border-color: rgba(251, 113, 133, 0.56);
|
||||
background:
|
||||
linear-gradient(135deg, rgba(80, 25, 42, 0.72), rgba(48, 18, 34, 0.82)) padding-box,
|
||||
linear-gradient(135deg, rgba(251, 113, 133, 0.56), rgba(196, 181, 253, 0.16)) border-box;
|
||||
color: #fff;
|
||||
box-shadow:
|
||||
0 0 14px rgba(251, 113, 133, 0.14),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.calculator-action-button.danger:hover .ui-icon,
|
||||
.calculator-entry > button.danger:hover .ui-icon,
|
||||
.counter-actions button.danger:hover .ui-icon,
|
||||
.link-item button.danger:hover .ui-icon {
|
||||
background-color: #fff;
|
||||
filter: drop-shadow(0 0 6px rgba(251, 113, 133, 0.26));
|
||||
}
|
||||
|
||||
.calculator-scroll-toggle.active {
|
||||
background: rgba(139, 92, 246, 0.18);
|
||||
border-color: rgba(246, 196, 83, 0.34);
|
||||
background:
|
||||
linear-gradient(135deg, rgba(72, 43, 160, 0.82), rgba(45, 52, 146, 0.8)) padding-box,
|
||||
linear-gradient(135deg, rgba(246, 196, 83, 0.58), rgba(139, 92, 246, 0.32)) border-box;
|
||||
}
|
||||
|
||||
.calculator-scroll-toggle.active i {
|
||||
|
|
@ -1156,16 +1214,8 @@ textarea:focus {
|
|||
opacity: 0.72;
|
||||
}
|
||||
|
||||
.calculator-entry > button.danger {
|
||||
min-width: 34px;
|
||||
.calculator-entry > button:not(.calculator-entry-summary, .calculator-entry-value) {
|
||||
min-height: 36px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.calculator-entry > button:not(.calculator-entry-summary, .calculator-entry-value, .danger) {
|
||||
min-width: 34px;
|
||||
min-height: 36px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.calculator-entry-list li.active > .calculator-entry > .calculator-entry-summary,
|
||||
|
|
@ -1221,9 +1271,7 @@ textarea:focus {
|
|||
.link-item button {
|
||||
width: 40px;
|
||||
min-width: 40px;
|
||||
height: 40px;
|
||||
min-height: 40px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.dropzone {
|
||||
|
|
@ -1250,6 +1298,10 @@ textarea:focus {
|
|||
box-shadow: 0 0 0 3px rgba(246, 196, 83, 0.12);
|
||||
}
|
||||
|
||||
.annotation-dropzone {
|
||||
min-height: 58px;
|
||||
}
|
||||
|
||||
.paste-target {
|
||||
min-height: 46px;
|
||||
margin: 0 var(--space-4) var(--space-4);
|
||||
|
|
@ -1267,11 +1319,157 @@ textarea:focus {
|
|||
box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.16);
|
||||
}
|
||||
|
||||
.image-annotation-module {
|
||||
display: grid;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.annotation-workspace {
|
||||
display: grid;
|
||||
gap: var(--space-4);
|
||||
padding: var(--space-4);
|
||||
}
|
||||
|
||||
.annotation-stage {
|
||||
position: relative;
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
place-items: center;
|
||||
border: 1px solid rgba(165, 180, 252, 0.12);
|
||||
border-radius: var(--radius-lg);
|
||||
background:
|
||||
radial-gradient(circle at 100% 0%, rgba(246, 196, 83, 0.08), transparent 38%),
|
||||
rgba(5, 7, 17, 0.48);
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.annotation-media {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
.annotation-stage-preview {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.annotation-preview-actions {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
z-index: 3;
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.annotation-preview-actions .calculator-action-button {
|
||||
background: rgba(21, 26, 48, 0.94);
|
||||
box-shadow:
|
||||
0 6px 18px rgba(5, 7, 17, 0.28),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.annotation-stage-preview .annotation-marker:hover {
|
||||
z-index: 2;
|
||||
border-color: rgba(196, 181, 253, 0.9);
|
||||
box-shadow:
|
||||
0 0 0 2px rgba(5, 7, 17, 0.72),
|
||||
0 0 14px rgba(139, 92, 246, 0.34),
|
||||
0 0 24px rgba(196, 181, 253, 0.22);
|
||||
filter: brightness(1.12);
|
||||
}
|
||||
|
||||
.annotation-stage img,
|
||||
.annotation-media img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-height: min(62vh, 620px);
|
||||
object-fit: contain;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.annotation-marker {
|
||||
position: absolute;
|
||||
display: inline-grid;
|
||||
left: clamp(0px, calc(var(--marker-x, 50%) - 15px), calc(100% - 30px));
|
||||
top: clamp(0px, calc(var(--marker-y, 50%) - 15px), calc(100% - 30px));
|
||||
width: 30px;
|
||||
min-width: 30px;
|
||||
height: 30px;
|
||||
min-height: 30px;
|
||||
place-items: center;
|
||||
padding: 0;
|
||||
border: 1px solid rgba(246, 196, 83, 0.72);
|
||||
border-color: rgba(246, 196, 83, 0.72);
|
||||
border-radius: var(--radius-pill);
|
||||
background:
|
||||
linear-gradient(135deg, rgba(72, 43, 160, 0.96), rgba(45, 52, 146, 0.94)) padding-box,
|
||||
linear-gradient(135deg, rgba(246, 196, 83, 0.9), rgba(196, 181, 253, 0.5)) border-box;
|
||||
color: #fff;
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 900;
|
||||
line-height: 1;
|
||||
box-shadow:
|
||||
0 0 0 2px rgba(5, 7, 17, 0.72),
|
||||
0 0 16px rgba(246, 196, 83, 0.22);
|
||||
cursor: grab;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.annotation-marker:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.annotation-marker-list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.annotation-marker-list li {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
grid-template-columns: 28px minmax(0, 1fr) auto;
|
||||
gap: var(--space-2);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.annotation-marker-list li > span {
|
||||
display: inline-grid;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
place-items: center;
|
||||
border: 1px solid rgba(246, 196, 83, 0.32);
|
||||
border-radius: var(--radius-pill);
|
||||
background: rgba(246, 196, 83, 0.1);
|
||||
color: var(--color-accent-gold);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.annotation-marker-list input {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
min-height: 34px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.annotation-empty {
|
||||
margin: 0;
|
||||
padding: var(--space-4);
|
||||
}
|
||||
|
||||
.shots {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
gap: var(--space-3);
|
||||
padding: 0 var(--space-4) var(--space-4);
|
||||
padding: var(--space-4);
|
||||
}
|
||||
|
||||
.shots figure {
|
||||
|
|
@ -1307,6 +1505,13 @@ textarea:focus {
|
|||
background: var(--color-bg-deep);
|
||||
}
|
||||
|
||||
.shot-delete-button {
|
||||
.shot-actions {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.shot-actions .shot-annotate-button,
|
||||
.shot-actions .shot-delete-button {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
|
|
|||
15
website/src/utils/bodyScrollLock.js
Normal file
15
website/src/utils/bodyScrollLock.js
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
export function lockBodyScroll() {
|
||||
const currentLocks = Number(document.body.dataset.modalLocks || 0);
|
||||
document.body.dataset.modalLocks = String(currentLocks + 1);
|
||||
document.body.classList.add("is-modal-open");
|
||||
|
||||
return () => {
|
||||
const nextLocks = Math.max(0, Number(document.body.dataset.modalLocks || 0) - 1);
|
||||
if (nextLocks) {
|
||||
document.body.dataset.modalLocks = String(nextLocks);
|
||||
return;
|
||||
}
|
||||
delete document.body.dataset.modalLocks;
|
||||
document.body.classList.remove("is-modal-open");
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue