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
|
## Fonctionnalités
|
||||||
|
|
||||||
- Toolboxes locales stockées dans `localStorage`.
|
- 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 d’une toolbox.
|
||||||
- Import/export global de toutes les toolboxes.
|
- Import/export global de toutes les toolboxes.
|
||||||
- Association locale entre une page jeu et une toolbox.
|
- 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/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/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/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. |
|
| `website/public/data/toolbox-icons.json` | Manifeste des icônes SVG réutilisables. |
|
||||||
|
|
||||||
Les images publiques sont dans `website/public/static`.
|
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/GamesPage.jsx` : liste des jeux disponibles.
|
||||||
- `website/src/features/games/GameRoute.jsx` : route vers la page du jeu demandé.
|
- `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/mhwilds/` : vues et composants propres à Monster Hunter Wilds.
|
||||||
|
- `website/src/features/games/diablo4/` : vues et composants propres à Diablo IV.
|
||||||
|
|
||||||
Pour ajouter un jeu :
|
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/NotepadModule.jsx`
|
||||||
- `website/src/features/toolboxes/modules/ChecklistModule.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/LinksModule.jsx`
|
||||||
- `website/src/features/toolboxes/modules/CountersModule.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 :
|
Pour ajouter ou maintenir un outil :
|
||||||
|
|
||||||
|
|
@ -124,16 +128,20 @@ Pour ajouter ou maintenir un outil :
|
||||||
├── components/
|
├── components/
|
||||||
├── features/
|
├── features/
|
||||||
│ ├── games/
|
│ ├── games/
|
||||||
|
│ │ ├── diablo4/
|
||||||
│ │ └── mhwilds/
|
│ │ └── mhwilds/
|
||||||
│ └── toolboxes/
|
│ └── toolboxes/
|
||||||
│ └── modules/
|
│ └── modules/
|
||||||
└── styles/
|
└── styles/
|
||||||
├── _base.scss
|
├── _base.scss
|
||||||
├── _cards.scss
|
├── _cards.scss
|
||||||
|
├── _diablo4.scss
|
||||||
├── _games.scss
|
├── _games.scss
|
||||||
├── _home.scss
|
├── _home.scss
|
||||||
├── _icons.scss
|
├── _icons.scss
|
||||||
|
├── _mixins.scss
|
||||||
├── _mhwilds.scss
|
├── _mhwilds.scss
|
||||||
|
├── _nebula.scss
|
||||||
├── _overlays.scss
|
├── _overlays.scss
|
||||||
├── _responsive.scss
|
├── _responsive.scss
|
||||||
├── _shell.scss
|
├── _shell.scss
|
||||||
|
|
|
||||||
|
|
@ -69,14 +69,70 @@ export function validateSiteContent(site) {
|
||||||
"toolboxes.storageHelp.text",
|
"toolboxes.storageHelp.text",
|
||||||
"toolboxes.modules.notepad.placeholder",
|
"toolboxes.modules.notepad.placeholder",
|
||||||
"toolboxes.modules.checklist.itemPlaceholder",
|
"toolboxes.modules.checklist.itemPlaceholder",
|
||||||
|
"toolboxes.modules.checklist.quantityLabel",
|
||||||
|
"toolboxes.modules.checklist.addButton",
|
||||||
"toolboxes.modules.checklist.importPlaceholder",
|
"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.titlePlaceholder",
|
||||||
"toolboxes.modules.links.urlPlaceholder",
|
"toolboxes.modules.links.urlPlaceholder",
|
||||||
|
"toolboxes.modules.links.addButton",
|
||||||
"toolboxes.modules.links.importPlaceholder",
|
"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.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.expressionPlaceholder",
|
||||||
|
"toolboxes.modules.calculator.resultLabel",
|
||||||
|
"toolboxes.modules.calculator.labelLabel",
|
||||||
"toolboxes.modules.calculator.labelPlaceholder",
|
"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.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.emptyTitle",
|
||||||
"toolboxes.emptyText"
|
"toolboxes.emptyText"
|
||||||
].forEach((path) => assertNonEmptyString(site, path));
|
].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 styleCards = await readFile("website/src/styles/_cards.scss", "utf8");
|
||||||
const styleHome = await readFile("website/src/styles/_home.scss", "utf8");
|
const styleHome = await readFile("website/src/styles/_home.scss", "utf8");
|
||||||
const styleToolboxes = await readFile("website/src/styles/_toolboxes.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 styleIcons = await readFile("website/src/styles/_icons.scss", "utf8");
|
||||||
const styleGames = await readFile("website/src/styles/_games.scss", "utf8");
|
const styleGames = await readFile("website/src/styles/_games.scss", "utf8");
|
||||||
const styleMhwilds = await readFile("website/src/styles/_mhwilds.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 calculatorModule = await readFile("website/src/features/toolboxes/modules/CalculatorModule.jsx", "utf8");
|
||||||
const checklistModule = await readFile("website/src/features/toolboxes/modules/ChecklistModule.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 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 linksModule = await readFile("website/src/features/toolboxes/modules/LinksModule.jsx", "utf8");
|
||||||
const countersModule = await readFile("website/src/features/toolboxes/modules/CountersModule.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 textImport = await readFile("website/src/features/toolboxes/modules/textImport.js", "utf8");
|
||||||
const reorderHook = await readFile("website/src/hooks/usePointerReorder.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:toolboxes/);
|
||||||
assert.match(source, /sokkog:game-toolbox-links/);
|
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 "mhwilds"/);
|
||||||
assert.match(styles, /@use "diablo4"/);
|
assert.match(styles, /@use "diablo4"/);
|
||||||
assert.match(styleTokens, /--gradient-brand/);
|
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, /@function star-field/);
|
||||||
assert.match(styleNebula, /body::after/);
|
assert.match(styleNebula, /body::after/);
|
||||||
assert.match(styleBase, /\.import-icon-button/);
|
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, /links:/);
|
||||||
assert.match(moduleRegistry, /counters:/);
|
assert.match(moduleRegistry, /counters:/);
|
||||||
assert.match(moduleRegistry, /calculator:/);
|
assert.match(moduleRegistry, /calculator:/);
|
||||||
|
assert.match(moduleRegistry, /imageAnnotation:/);
|
||||||
|
assert.match(moduleRegistry, /Annotation d'images/);
|
||||||
assert.match(moduleRegistry, /editable: false/);
|
assert.match(moduleRegistry, /editable: false/);
|
||||||
assert.match(moduleRegistry, /module-edit-button/);
|
assert.match(moduleRegistry, /module-edit-button/);
|
||||||
assert.match(notepadModule, /export function NotepadModule/);
|
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(calculatorModule, /CalculatorEntries/);
|
||||||
assert.match(checklistModule, /export function ChecklistModule/);
|
assert.match(checklistModule, /export function ChecklistModule/);
|
||||||
assert.match(screenshotsModule, /export function ScreenshotsModule/);
|
assert.match(screenshotsModule, /export function ScreenshotsModule/);
|
||||||
|
assert.match(imageAnnotationModule, /export function ImageAnnotationModule/);
|
||||||
assert.match(linksModule, /export function LinksModule/);
|
assert.match(linksModule, /export function LinksModule/);
|
||||||
assert.match(countersModule, /export function CountersModule/);
|
assert.match(countersModule, /export function CountersModule/);
|
||||||
assert.match(textImport, /export function parseColonImportLines/);
|
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, /normalizeLinksData/);
|
||||||
assert.match(source, /normalizeCountersData/);
|
assert.match(source, /normalizeCountersData/);
|
||||||
assert.match(source, /normalizeCalculatorData/);
|
assert.match(source, /normalizeCalculatorData/);
|
||||||
|
assert.match(source, /normalizeImageAnnotationData/);
|
||||||
|
assert.match(source, /compactModuleDataForStorage\(type, value\)/);
|
||||||
assert.match(source, /scrollResults/);
|
assert.match(source, /scrollResults/);
|
||||||
assert.match(checklistModule, /ChecklistItem/);
|
assert.match(checklistModule, /ChecklistItem/);
|
||||||
assert.match(checklistModule, /parseColonImportLines/);
|
assert.match(checklistModule, /parseColonImportLines/);
|
||||||
assert.match(checklistModule, /Number\.parseInt/);
|
assert.match(checklistModule, /Number\.parseInt/);
|
||||||
assert.match(checklistModule, /checklist-qty-current/);
|
assert.match(checklistModule, /checklist-qty-current/);
|
||||||
|
assert.match(checklistModule, /checklist-delete-button danger/);
|
||||||
assert.match(linksModule, /parseColonImportLines/);
|
assert.match(linksModule, /parseColonImportLines/);
|
||||||
assert.match(linksModule, /context\.normalizeUrl/);
|
assert.match(linksModule, /context\.normalizeUrl/);
|
||||||
assert.match(screenshotsModule, /clipboardData/);
|
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, /async function addScreenshotFiles/);
|
||||||
assert.match(source, /function ScreenshotViewer/);
|
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, /function openImageInNewTab/);
|
||||||
|
assert.match(source, /!hasMarkers &&/);
|
||||||
assert.match(source, /<Icon name="zoom"/);
|
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.match(screenshotsModule, /dataTransfer\.files/);
|
||||||
assert.doesNotMatch(`${mhwildsListing}\n${diablo4Listing}\n${diablo4Overview}`, /mhwilds-(home-grid|home-card|heading|title-row|layout|filters|results|grid)/);
|
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/copy.svg", "utf8");
|
||||||
await readFile("website/public/static/icons/abacus.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/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/enter.svg", "utf8");
|
||||||
await readFile("website/public/static/icons/export.svg", "utf8");
|
await readFile("website/public/static/icons/export.svg", "utf8");
|
||||||
await readFile("website/public/static/icons/scrollable.svg", "utf8");
|
await readFile("website/public/static/icons/scrollable.svg", "utf8");
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@
|
||||||
"items": [
|
"items": [
|
||||||
"Capturer les drops rares",
|
"Capturer les drops rares",
|
||||||
"Mettre à jour les crafts suivis",
|
"Mettre à jour les crafts suivis",
|
||||||
"Archiver les screenshots importants"
|
"Archiver les images importantes"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -88,13 +88,13 @@
|
||||||
"importAll": "Importer tout",
|
"importAll": "Importer tout",
|
||||||
"exportAll": "Exporter tout",
|
"exportAll": "Exporter tout",
|
||||||
"importOne": "Importer une toolbox",
|
"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": {
|
"storageHelp": {
|
||||||
"title": "Sauvegardes locales",
|
"title": "Sauvegardes locales",
|
||||||
"text": "Vos toolboxes restent sur cet appareil, dans ce navigateur. Rien n’est envoyé sur un serveur.",
|
"text": "Vos toolboxes restent sur cet appareil, dans ce navigateur. Rien n’est envoyé sur un serveur.",
|
||||||
"items": [
|
"items": [
|
||||||
"Un nettoyage du navigateur ou un changement d’appareil peut supprimer les données.",
|
"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."
|
"Faites un export global régulier pour garder une sauvegarde."
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
@ -104,20 +104,80 @@
|
||||||
},
|
},
|
||||||
"checklist": {
|
"checklist": {
|
||||||
"itemPlaceholder": "Nouvel item",
|
"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": {
|
"links": {
|
||||||
"titlePlaceholder": "Nom du lien",
|
"titlePlaceholder": "Nom du lien",
|
||||||
"urlPlaceholder": "https://...",
|
"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": {
|
"counters": {
|
||||||
"labelPlaceholder": "Nom du compteur"
|
"labelPlaceholder": "Nom du compteur",
|
||||||
|
"addButton": "Ajouter",
|
||||||
|
"decrementLabel": "Décrémenter",
|
||||||
|
"incrementLabel": "Incrémenter",
|
||||||
|
"resetTitle": "Réinitialiser",
|
||||||
|
"deleteTitle": "Supprimer"
|
||||||
},
|
},
|
||||||
"calculator": {
|
"calculator": {
|
||||||
|
"expressionLabel": "Calcul",
|
||||||
"expressionPlaceholder": "10*10",
|
"expressionPlaceholder": "10*10",
|
||||||
|
"resultLabel": "Résultat",
|
||||||
|
"labelLabel": "Libellé",
|
||||||
"labelPlaceholder": "ex: Lingots de fer",
|
"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",
|
"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}>
|
<form className="calculator-form" onSubmit={saveResult}>
|
||||||
<div className="calculator-card" ref={calculatorCardRef}>
|
<div className="calculator-card" ref={calculatorCardRef}>
|
||||||
<label>
|
<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" />
|
<input value={expression} onChange={(event) => setExpression(event.target.value)} placeholder={textContent.expressionPlaceholder || "10*10"} inputMode="decimal" />
|
||||||
</label>
|
</label>
|
||||||
<div className="calculator-result" aria-live="polite">
|
<div className="calculator-result" aria-live="polite">
|
||||||
<span>Résultat</span>
|
<span>{textContent.resultLabel || "Résultat"}</span>
|
||||||
<strong>{result == null ? "-" : formatResult(result)}</strong>
|
<strong>{result == null ? "-" : formatResult(result)}</strong>
|
||||||
</div>
|
</div>
|
||||||
<label>
|
<label>
|
||||||
<span>Libellé</span>
|
<span>{textContent.labelLabel || "Libellé"}</span>
|
||||||
<input value={label} onChange={(event) => setLabel(event.target.value)} placeholder={textContent.labelPlaceholder || "Lingots de fer"} />
|
<input value={label} onChange={(event) => setLabel(event.target.value)} placeholder={textContent.labelPlaceholder || "Lingots de fer"} />
|
||||||
</label>
|
</label>
|
||||||
<button className="primary" disabled={result == null}>Enregistrer</button>
|
<button className="primary" disabled={result == null}>{textContent.saveButton || "Enregistrer"}</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<div
|
<div
|
||||||
|
|
@ -140,13 +140,13 @@ export function CalculatorModule({ toolboxId, moduleId, context }) {
|
||||||
<div className="calculator-result-actions">
|
<div className="calculator-result-actions">
|
||||||
{activeParent && (
|
{activeParent && (
|
||||||
<button className="calculator-root-button" type="button" onClick={() => setActiveParentId("")}>
|
<button className="calculator-root-button" type="button" onClick={() => setActiveParentId("")}>
|
||||||
Revenir à la racine
|
{textContent.rootButton || "Revenir à la racine"}
|
||||||
</button>
|
</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" />
|
<Icon name="rubber" />
|
||||||
</button>
|
</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" />
|
<Icon name="copy" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
|
@ -163,9 +163,9 @@ export function CalculatorModule({ toolboxId, moduleId, context }) {
|
||||||
</div>
|
</div>
|
||||||
<div className={`calculator-tree ${data.scrollResults ? "is-scrollable legacy-scrollbar" : ""}`}>
|
<div className={`calculator-tree ${data.scrollResults ? "is-scrollable legacy-scrollbar" : ""}`}>
|
||||||
{data.entries.length ? (
|
{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>
|
||||||
</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 children = getChildren(entries, parentId);
|
||||||
const [editingId, setEditingId] = useState("");
|
const [editingId, setEditingId] = useState("");
|
||||||
if (!children.length) return null;
|
if (!children.length) return null;
|
||||||
|
|
@ -185,31 +185,31 @@ function CalculatorEntries({ entries, parentId, activeParentId, onUse, onRename,
|
||||||
<div className="calculator-entry">
|
<div className="calculator-entry">
|
||||||
{entry.id === editingId ? (
|
{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>
|
<strong>{formatResult(entry.value)}</strong>
|
||||||
</span>
|
</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>
|
<span><strong>{formatResult(entry.value)}</strong> {entry.label}</span>
|
||||||
</button>
|
</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" />
|
<Icon name="edit" />
|
||||||
</button>
|
</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" />
|
<Icon name="trash" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function EditableCalculatorLabel({ entry, onRename, onDone }) {
|
function EditableCalculatorLabel({ entry, textContent, onRename, onDone }) {
|
||||||
const [label, setLabel] = useState(entry.label);
|
const [label, setLabel] = useState(entry.label);
|
||||||
const inputRef = useRef(null);
|
const inputRef = useRef(null);
|
||||||
|
|
||||||
|
|
@ -237,7 +237,7 @@ function EditableCalculatorLabel({ entry, onRename, onDone }) {
|
||||||
onDone();
|
onDone();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
aria-label={`Renommer ${entry.label}`}
|
aria-label={`${textContent.renameTitle || "Renommer"} ${entry.label}`}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { Icon } from "../../../components/Icon.jsx";
|
||||||
import { parseColonImportLines } from "./textImport.js";
|
import { parseColonImportLines } from "./textImport.js";
|
||||||
|
|
||||||
export function ChecklistModule({ toolboxId, moduleId, context, editing }) {
|
export function ChecklistModule({ toolboxId, moduleId, context, editing }) {
|
||||||
|
|
@ -40,8 +41,8 @@ export function ChecklistModule({ toolboxId, moduleId, context, editing }) {
|
||||||
<div className="module-add-panel">
|
<div className="module-add-panel">
|
||||||
<form className="inline-form checklist-add-form" onSubmit={addItem}>
|
<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 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" />
|
<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">Ajouter</button>
|
<button className="primary">{textContent.addButton || "Ajouter"}</button>
|
||||||
</form>
|
</form>
|
||||||
<form className="text-import-form" onSubmit={importItems}>
|
<form className="text-import-form" onSubmit={importItems}>
|
||||||
<textarea
|
<textarea
|
||||||
|
|
@ -50,7 +51,7 @@ export function ChecklistModule({ toolboxId, moduleId, context, editing }) {
|
||||||
placeholder={textContent.importPlaceholder || "Importer plusieurs items...\nPotion:10\nMéga potion:5"}
|
placeholder={textContent.importPlaceholder || "Importer plusieurs items...\nPotion:10\nMéga potion:5"}
|
||||||
rows={3}
|
rows={3}
|
||||||
/>
|
/>
|
||||||
<button type="submit">Importer le texte</button>
|
<button type="submit">{textContent.importButton || "Importer le texte"}</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -82,7 +83,7 @@ function ChecklistItem({ item, context, items, save }) {
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="checklist-qty-controls" aria-label={`Quantité ${item.label}`}>
|
<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">
|
<label className="checklist-qty-current">
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
|
|
@ -90,16 +91,18 @@ function ChecklistItem({ item, context, items, save }) {
|
||||||
value={context.clampQty(item.qtyCurrent, item.qtyTarget)}
|
value={context.clampQty(item.qtyCurrent, item.qtyTarget)}
|
||||||
style={{ "--qty-current-digits": String(context.clampQty(item.qtyCurrent, item.qtyTarget)).length }}
|
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) }))}
|
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>
|
<span>/ {item.qtyTarget}</span>
|
||||||
</label>
|
</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>
|
</div>
|
||||||
)}
|
)}
|
||||||
<span>{item.label}</span>
|
<span>{item.label}</span>
|
||||||
</div>
|
</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>
|
</li>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ export function CountersModule({ toolboxId, moduleId, context, editing }) {
|
||||||
{editing && (
|
{editing && (
|
||||||
<form className="inline-form counters-add-form module-add-panel" onSubmit={addCounter}>
|
<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)} />
|
<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>
|
</form>
|
||||||
)}
|
)}
|
||||||
<div className="counters-grid">
|
<div className="counters-grid">
|
||||||
|
|
@ -39,12 +39,12 @@ export function CountersModule({ toolboxId, moduleId, context, editing }) {
|
||||||
<span>{counter.label}</span>
|
<span>{counter.label}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="counter-actions">
|
<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={`${textContent.decrementLabel || "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: 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={`Réinitialiser ${counter.label}`} title="Réinitialiser">
|
<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" />
|
<Icon name="rubber" />
|
||||||
</button>
|
</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" />
|
<Icon name="trash" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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}>
|
<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="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)} />
|
<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>
|
||||||
<form className="text-import-form" onSubmit={importLinks}>
|
<form className="text-import-form" onSubmit={importLinks}>
|
||||||
<textarea
|
<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"}
|
placeholder={textContent.importPlaceholder || "Importer plusieurs liens...\nBuild:https://example.com/build?class=rogue\nMap:https://example.com/map"}
|
||||||
rows={3}
|
rows={3}
|
||||||
/>
|
/>
|
||||||
<button type="submit">Importer le texte</button>
|
<button type="submit">{textContent.importButton || "Importer le texte"}</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -80,10 +80,10 @@ export function LinksModule({ toolboxId, moduleId, context, editing }) {
|
||||||
<span>{link.url}</span>
|
<span>{link.url}</span>
|
||||||
</a>
|
</a>
|
||||||
<div>
|
<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" />
|
<Icon name="copy" />
|
||||||
</button>
|
</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" />
|
<Icon name="trash" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { Icon } from "../../../components/Icon.jsx";
|
||||||
|
|
||||||
export function ScreenshotsModule({ toolboxId, moduleId, context, editing }) {
|
export function ScreenshotsModule({ toolboxId, moduleId, context, editing }) {
|
||||||
const data = context.getModuleData(toolboxId, moduleId, { shots: [] });
|
const data = context.getModuleData(toolboxId, moduleId, { shots: [] });
|
||||||
|
const textContent = context.moduleText?.screenshots || {};
|
||||||
const [dragOver, setDragOver] = useState(false);
|
const [dragOver, setDragOver] = useState(false);
|
||||||
|
const pastePlaceholder = textContent.pastePlaceholder || "Coller une image ici";
|
||||||
|
|
||||||
async function addFiles(files) {
|
async function addFiles(files) {
|
||||||
if (await context.addScreenshotFiles(toolboxId, moduleId, 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)} />
|
<input type="file" accept="image/*" multiple hidden onChange={(event) => addFiles(event.target.files)} />
|
||||||
Ajouter des screenshots
|
{textContent.addImages || "Ajouter des images"}
|
||||||
</label>
|
</label>
|
||||||
<div
|
<div
|
||||||
className="paste-target"
|
className="paste-target"
|
||||||
contentEditable
|
contentEditable
|
||||||
suppressContentEditableWarning
|
suppressContentEditableWarning
|
||||||
role="textbox"
|
role="textbox"
|
||||||
aria-label="Coller une image depuis le presse-papiers"
|
aria-label={textContent.pasteAriaLabel || "Coller une image depuis le presse-papiers"}
|
||||||
onFocus={(event) => {
|
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) => {
|
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) => {
|
onPaste={(event) => {
|
||||||
const files = [...(event.clipboardData?.items || [])]
|
const files = [...(event.clipboardData?.items || [])]
|
||||||
|
|
@ -49,25 +52,36 @@ export function ScreenshotsModule({ toolboxId, moduleId, context, editing }) {
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
if (!files.length) return;
|
if (!files.length) return;
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.currentTarget.textContent = "Coller une image ici";
|
event.currentTarget.textContent = pastePlaceholder;
|
||||||
addFiles(files);
|
addFiles(files);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Coller une image ici
|
{pastePlaceholder}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="shots">
|
<div className="shots">
|
||||||
{data.shots.map((shot) => (
|
{data.shots.map((shot) => (
|
||||||
<figure key={shot.id}>
|
<figure key={shot.id}>
|
||||||
<button className="shot-preview" onClick={() => context.setScreenshot(shot)} aria-label="Agrandir le screenshot">
|
<button className="shot-preview" onClick={() => context.setScreenshot(shot)} aria-label={textContent.previewAriaLabel || "Agrandir l'image"}>
|
||||||
<img src={shot.dataUrl} alt="Screenshot" />
|
<img src={shot.dataUrl} alt={textContent.imageAlt || "Image"} />
|
||||||
</button>
|
</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) });
|
context.setModuleData(toolboxId, moduleId, { shots: data.shots.filter((item) => item.id !== shot.id) });
|
||||||
}} aria-label="Supprimer le screenshot" title="Supprimer">
|
}} aria-label={textContent.deleteAriaLabel || "Supprimer l'image"} title={textContent.deleteTitle || "Supprimer"}>
|
||||||
<span className="ui-icon ui-icon-trash" aria-hidden="true" />
|
<Icon name="trash" />
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
</figure>
|
</figure>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,11 @@ import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
import { Icon } from "../../../components/Icon.jsx";
|
import { Icon } from "../../../components/Icon.jsx";
|
||||||
import { usePointerReorder } from "../../../hooks/usePointerReorder.js";
|
import { usePointerReorder } from "../../../hooks/usePointerReorder.js";
|
||||||
|
import { lockBodyScroll } from "../../../utils/bodyScrollLock.js";
|
||||||
import { CalculatorModule } from "./CalculatorModule.jsx";
|
import { CalculatorModule } from "./CalculatorModule.jsx";
|
||||||
import { ChecklistModule } from "./ChecklistModule.jsx";
|
import { ChecklistModule } from "./ChecklistModule.jsx";
|
||||||
import { CountersModule } from "./CountersModule.jsx";
|
import { CountersModule } from "./CountersModule.jsx";
|
||||||
|
import { ImageAnnotationModule } from "./ImageAnnotationModule.jsx";
|
||||||
import { LinksModule } from "./LinksModule.jsx";
|
import { LinksModule } from "./LinksModule.jsx";
|
||||||
import { NotepadModule } from "./NotepadModule.jsx";
|
import { NotepadModule } from "./NotepadModule.jsx";
|
||||||
import { ScreenshotsModule } from "./ScreenshotsModule.jsx";
|
import { ScreenshotsModule } from "./ScreenshotsModule.jsx";
|
||||||
|
|
@ -12,10 +14,11 @@ import { ScreenshotsModule } from "./ScreenshotsModule.jsx";
|
||||||
const MODULE_COMPONENTS = {
|
const MODULE_COMPONENTS = {
|
||||||
notepad: { label: "Bloc notes", icon: "notepad", Component: NotepadModule, editable: false },
|
notepad: { label: "Bloc notes", icon: "notepad", Component: NotepadModule, editable: false },
|
||||||
checklist: { label: "Checklist", icon: "checklist", Component: ChecklistModule, editable: true },
|
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 },
|
links: { label: "Liens", icon: "link", Component: LinksModule, editable: true },
|
||||||
counters: { label: "Compteurs", icon: "abacus", Component: CountersModule, 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;
|
const MODULE_COLUMN_GAP_PX = 16;
|
||||||
|
|
@ -71,6 +74,11 @@ export function AddToolControls({ onAdd }) {
|
||||||
const controlsRef = useRef(null);
|
const controlsRef = useRef(null);
|
||||||
const modules = Object.entries(TOOLBOX_MODULES);
|
const modules = Object.entries(TOOLBOX_MODULES);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) return undefined;
|
||||||
|
return lockBodyScroll();
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!quickOpen) return undefined;
|
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 { 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 { AddToolControls, ToolboxModules, TOOLBOX_MODULES } from "./features/toolboxes/modules/index.jsx";
|
||||||
import { usePointerReorder } from "./hooks/usePointerReorder.js";
|
import { usePointerReorder } from "./hooks/usePointerReorder.js";
|
||||||
|
import { lockBodyScroll } from "./utils/bodyScrollLock.js";
|
||||||
|
|
||||||
const STORAGE_KEYS = {
|
const STORAGE_KEYS = {
|
||||||
registry: "sokkog:toolboxes",
|
registry: "sokkog:toolboxes",
|
||||||
|
|
@ -16,7 +17,7 @@ const STORAGE_KEYS = {
|
||||||
const APP_STORAGE_PREFIX = "sokkog:";
|
const APP_STORAGE_PREFIX = "sokkog:";
|
||||||
const APP_STORAGE_LIMIT_BYTES = 5 * 1024 * 1024;
|
const APP_STORAGE_LIMIT_BYTES = 5 * 1024 * 1024;
|
||||||
const APP_STORAGE_WARNING_RATIO = 0.85;
|
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 ID_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
||||||
const TOOLBOX_ICON_BASE = "/static/img/toolbox-icons/";
|
const TOOLBOX_ICON_BASE = "/static/img/toolbox-icons/";
|
||||||
const TOOLBOX_ICON_FILES = [
|
const TOOLBOX_ICON_FILES = [
|
||||||
|
|
@ -99,18 +100,80 @@ const DEFAULT_SITE_CONTENT = {
|
||||||
notepad: { placeholder: "Notes rapides..." },
|
notepad: { placeholder: "Notes rapides..." },
|
||||||
checklist: {
|
checklist: {
|
||||||
itemPlaceholder: "Nouvel item",
|
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: {
|
links: {
|
||||||
titlePlaceholder: "Nom du lien",
|
titlePlaceholder: "Nom du lien",
|
||||||
urlPlaceholder: "https://...",
|
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: {
|
calculator: {
|
||||||
|
expressionLabel: "Calcul",
|
||||||
expressionPlaceholder: "10*10",
|
expressionPlaceholder: "10*10",
|
||||||
|
resultLabel: "Résultat",
|
||||||
|
labelLabel: "Libellé",
|
||||||
labelPlaceholder: "Lingots de fer",
|
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",
|
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) {
|
function compactChecklistItemForStorage(item) {
|
||||||
const normalized = normalizeChecklistItem(item);
|
const normalized = normalizeChecklistItem(item);
|
||||||
const compact = { id: normalized.id, label: normalized.label };
|
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 }));
|
.map((shot) => ({ id: shot.id || uid("shot"), dataUrl: shot.dataUrl }));
|
||||||
return shots.length ? { shots } : null;
|
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") {
|
if (type === "links") {
|
||||||
const links = normalizeLinksData(value).links.map((link) => {
|
const links = normalizeLinksData(value).links.map((link) => {
|
||||||
const compact = {
|
const compact = {
|
||||||
|
|
@ -424,9 +517,9 @@ function compactModuleDataForStorage(type, value) {
|
||||||
return 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 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);
|
const key = moduleStorageKey(toolboxId, moduleId);
|
||||||
if (compact == null) {
|
if (compact == null) {
|
||||||
localStorage.removeItem(key);
|
localStorage.removeItem(key);
|
||||||
|
|
@ -453,6 +546,13 @@ function remapModuleDataForExport(type, data, nextId) {
|
||||||
return { shots: compact.shots.map((shot) => ({ ...shot, id: nextId("shot") })) };
|
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") {
|
if (type === "links") {
|
||||||
return { links: compact.links.map((link) => ({ ...link, id: nextId("link") })) };
|
return { links: compact.links.map((link) => ({ ...link, id: nextId("link") })) };
|
||||||
}
|
}
|
||||||
|
|
@ -603,6 +703,10 @@ function useHashRoute() {
|
||||||
return route;
|
return route;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function useModalScrollLock() {
|
||||||
|
useEffect(() => lockBodyScroll(), []);
|
||||||
|
}
|
||||||
|
|
||||||
function useLocalToolboxes(onError) {
|
function useLocalToolboxes(onError) {
|
||||||
const [toolboxes, setToolboxesState] = useState(loadToolboxes);
|
const [toolboxes, setToolboxesState] = useState(loadToolboxes);
|
||||||
const [links, setLinksState] = useState(loadLinks);
|
const [links, setLinksState] = useState(loadLinks);
|
||||||
|
|
@ -636,9 +740,9 @@ function useLocalToolboxes(onError) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateModuleData(toolboxId, moduleId, value) {
|
function updateModuleData(toolboxId, moduleId, value, moduleType = "") {
|
||||||
try {
|
try {
|
||||||
setModuleData(toolboxes, toolboxId, moduleId, value);
|
setModuleData(toolboxes, toolboxId, moduleId, value, moduleType);
|
||||||
refreshQuota();
|
refreshQuota();
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -1286,13 +1390,16 @@ function ToolboxView({ siteContent, toolbox, toolboxGame, embedded, actions, upd
|
||||||
normalizeLinksData,
|
normalizeLinksData,
|
||||||
normalizeCountersData,
|
normalizeCountersData,
|
||||||
normalizeCalculatorData,
|
normalizeCalculatorData,
|
||||||
|
normalizeImageAnnotationData,
|
||||||
normalizeUrl,
|
normalizeUrl,
|
||||||
hostnameFromUrl,
|
hostnameFromUrl,
|
||||||
copyText,
|
copyText,
|
||||||
|
compressImageFile: compressImage,
|
||||||
clampQty,
|
clampQty,
|
||||||
uid,
|
uid,
|
||||||
setModuleData: updateModuleData,
|
setModuleData: updateModuleData,
|
||||||
addScreenshotFiles,
|
addScreenshotFiles,
|
||||||
|
createImageAnnotationModule,
|
||||||
setScreenshot: actions.setScreenshot,
|
setScreenshot: actions.setScreenshot,
|
||||||
refresh: () => {}
|
refresh: () => {}
|
||||||
};
|
};
|
||||||
|
|
@ -1302,6 +1409,13 @@ function ToolboxView({ siteContent, toolbox, toolboxGame, embedded, actions, upd
|
||||||
updateToolbox({ ...toolbox, modules: [...toolbox.modules, { id: uid("mod"), type }] });
|
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") {
|
function moveModule(fromModuleId, toModuleId, placement = "before") {
|
||||||
if (!fromModuleId || !toModuleId || fromModuleId === toModuleId) return;
|
if (!fromModuleId || !toModuleId || fromModuleId === toModuleId) return;
|
||||||
const modules = [...toolbox.modules];
|
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 }) {
|
function ConfirmModal({ title, message, confirmLabel = "Confirmer", cancelLabel = "Annuler", danger = false, onClose }) {
|
||||||
|
useModalScrollLock();
|
||||||
return (
|
return (
|
||||||
<div className="confirm-modal-root">
|
<div className="confirm-modal-root">
|
||||||
<div className="confirm-backdrop" onClick={() => onClose(false)} />
|
<div className="confirm-backdrop" onClick={() => onClose(false)} />
|
||||||
|
|
@ -1481,6 +1596,7 @@ function ConfirmModal({ title, message, confirmLabel = "Confirmer", cancelLabel
|
||||||
}
|
}
|
||||||
|
|
||||||
function CreateToolboxModal({ gameId, onClose }) {
|
function CreateToolboxModal({ gameId, onClose }) {
|
||||||
|
useModalScrollLock();
|
||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
return (
|
return (
|
||||||
<div className="confirm-modal-root">
|
<div className="confirm-modal-root">
|
||||||
|
|
@ -1500,6 +1616,7 @@ function CreateToolboxModal({ gameId, onClose }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function LinkToolboxModal({ toolboxes, selectedId, onClose }) {
|
function LinkToolboxModal({ toolboxes, selectedId, onClose }) {
|
||||||
|
useModalScrollLock();
|
||||||
const [toolboxId, setToolboxId] = useState(selectedId);
|
const [toolboxId, setToolboxId] = useState(selectedId);
|
||||||
return (
|
return (
|
||||||
<div className="confirm-modal-root">
|
<div className="confirm-modal-root">
|
||||||
|
|
@ -1519,6 +1636,38 @@ function LinkToolboxModal({ toolboxes, selectedId, onClose }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function ScreenshotViewer({ shot, 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(() => {
|
useEffect(() => {
|
||||||
const onKeyDown = (event) => {
|
const onKeyDown = (event) => {
|
||||||
if (event.key === "Escape") onClose();
|
if (event.key === "Escape") onClose();
|
||||||
|
|
@ -1526,15 +1675,96 @@ function ScreenshotViewer({ shot, onClose }) {
|
||||||
document.addEventListener("keydown", onKeyDown);
|
document.addEventListener("keydown", onKeyDown);
|
||||||
return () => document.removeEventListener("keydown", onKeyDown);
|
return () => document.removeEventListener("keydown", onKeyDown);
|
||||||
}, [onClose]);
|
}, [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 (
|
return (
|
||||||
<div className="screenshot-viewer-root">
|
<div className="screenshot-viewer-root">
|
||||||
<div className="screenshot-viewer-backdrop" onClick={onClose} />
|
<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>
|
<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>
|
<button className="screenshot-viewer-button" onClick={onClose} aria-label="Fermer" title="Fermer"><Icon name="close" /></button>
|
||||||
</header>
|
</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>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -145,9 +145,16 @@ input[type="checkbox"]:focus-visible {
|
||||||
outline-offset: 3px;
|
outline-offset: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.themed-scrollbar,
|
||||||
|
.legacy-scrollbar {
|
||||||
|
scrollbar-color: rgb(79, 26, 114) transparent;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
}
|
||||||
|
|
||||||
.filter-options::-webkit-scrollbar,
|
.filter-options::-webkit-scrollbar,
|
||||||
.drawer-panel::-webkit-scrollbar,
|
.drawer-panel::-webkit-scrollbar,
|
||||||
.damage-table-wrap::-webkit-scrollbar,
|
.damage-table-wrap::-webkit-scrollbar,
|
||||||
|
.themed-scrollbar::-webkit-scrollbar,
|
||||||
.legacy-scrollbar::-webkit-scrollbar {
|
.legacy-scrollbar::-webkit-scrollbar {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
width: 6px;
|
width: 6px;
|
||||||
|
|
@ -158,6 +165,7 @@ input[type="checkbox"]:focus-visible {
|
||||||
.filter-options::-webkit-scrollbar-track,
|
.filter-options::-webkit-scrollbar-track,
|
||||||
.drawer-panel::-webkit-scrollbar-track,
|
.drawer-panel::-webkit-scrollbar-track,
|
||||||
.damage-table-wrap::-webkit-scrollbar-track,
|
.damage-table-wrap::-webkit-scrollbar-track,
|
||||||
|
.themed-scrollbar::-webkit-scrollbar-track,
|
||||||
.legacy-scrollbar::-webkit-scrollbar-track {
|
.legacy-scrollbar::-webkit-scrollbar-track {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
|
|
@ -167,11 +175,16 @@ input[type="checkbox"]:focus-visible {
|
||||||
.filter-options::-webkit-scrollbar-thumb,
|
.filter-options::-webkit-scrollbar-thumb,
|
||||||
.drawer-panel::-webkit-scrollbar-thumb,
|
.drawer-panel::-webkit-scrollbar-thumb,
|
||||||
.damage-table-wrap::-webkit-scrollbar-thumb,
|
.damage-table-wrap::-webkit-scrollbar-thumb,
|
||||||
|
.themed-scrollbar::-webkit-scrollbar-thumb,
|
||||||
.legacy-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)));
|
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;
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body.is-modal-open {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.primary {
|
.primary {
|
||||||
border-color: rgba(246, 196, 83, 0.34);
|
border-color: rgba(246, 196, 83, 0.34);
|
||||||
background:
|
background:
|
||||||
|
|
@ -224,18 +237,22 @@ input[type="checkbox"]:focus-visible {
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.06);
|
inset 0 1px 0 rgba(255, 255, 255, 0.06);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-action-button,
|
:is(
|
||||||
.import-icon-button,
|
.sidebar-action-button,
|
||||||
.toolbox-icon-button,
|
.import-icon-button,
|
||||||
.card-icon-button,
|
.toolbox-icon-button,
|
||||||
.drawer-action-button,
|
.card-icon-button,
|
||||||
.drawer-close-button,
|
.drawer-action-button,
|
||||||
.filter-reset-button,
|
.drawer-close-button,
|
||||||
.screenshot-viewer-button,
|
.filter-reset-button,
|
||||||
.module-edit-button,
|
.screenshot-viewer-button,
|
||||||
.module-delete-button,
|
.module-edit-button,
|
||||||
.shot-delete-button,
|
.module-delete-button,
|
||||||
.tool-quick-add-button {
|
.shot-annotate-button,
|
||||||
|
.shot-delete-button,
|
||||||
|
.checklist-delete-button,
|
||||||
|
.tool-quick-add-button
|
||||||
|
) {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
width: 40px;
|
width: 40px;
|
||||||
min-width: 40px;
|
min-width: 40px;
|
||||||
|
|
@ -257,35 +274,43 @@ input[type="checkbox"]:focus-visible {
|
||||||
box-shadow var(--duration-fast) var(--ease-standard);
|
box-shadow var(--duration-fast) var(--ease-standard);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-action-button .ui-icon,
|
:is(
|
||||||
.import-icon-button .ui-icon,
|
.sidebar-action-button,
|
||||||
.toolbox-icon-button .ui-icon,
|
.import-icon-button,
|
||||||
.card-icon-button .ui-icon,
|
.toolbox-icon-button,
|
||||||
.drawer-action-button .ui-icon,
|
.card-icon-button,
|
||||||
.drawer-close-button .ui-icon,
|
.drawer-action-button,
|
||||||
.filter-reset-button .ui-icon,
|
.drawer-close-button,
|
||||||
.screenshot-viewer-button .ui-icon,
|
.filter-reset-button,
|
||||||
.module-edit-button .ui-icon,
|
.screenshot-viewer-button,
|
||||||
.module-delete-button .ui-icon,
|
.module-edit-button,
|
||||||
.shot-delete-button .ui-icon,
|
.module-delete-button,
|
||||||
.tool-quick-add-button .ui-icon {
|
.shot-annotate-button,
|
||||||
|
.shot-delete-button,
|
||||||
|
.checklist-delete-button,
|
||||||
|
.tool-quick-add-button
|
||||||
|
) .ui-icon {
|
||||||
transition:
|
transition:
|
||||||
background-color var(--duration-fast) var(--ease-standard),
|
background-color var(--duration-fast) var(--ease-standard),
|
||||||
filter var(--duration-fast) var(--ease-standard);
|
filter var(--duration-fast) var(--ease-standard);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-action-button:hover,
|
:is(
|
||||||
.import-icon-button:hover,
|
.sidebar-action-button,
|
||||||
.toolbox-icon-button:hover,
|
.import-icon-button,
|
||||||
.card-icon-button:hover,
|
.toolbox-icon-button,
|
||||||
.drawer-action-button:hover,
|
.card-icon-button,
|
||||||
.drawer-close-button:hover,
|
.drawer-action-button,
|
||||||
.filter-reset-button:hover,
|
.drawer-close-button,
|
||||||
.screenshot-viewer-button:hover,
|
.filter-reset-button,
|
||||||
.module-edit-button:hover,
|
.screenshot-viewer-button,
|
||||||
.module-delete-button:hover,
|
.module-edit-button,
|
||||||
.shot-delete-button:hover,
|
.module-delete-button,
|
||||||
.tool-quick-add-button:hover {
|
.shot-annotate-button,
|
||||||
|
.shot-delete-button,
|
||||||
|
.checklist-delete-button,
|
||||||
|
.tool-quick-add-button
|
||||||
|
):hover {
|
||||||
border-color: rgba(196, 181, 253, 0.32);
|
border-color: rgba(196, 181, 253, 0.32);
|
||||||
background:
|
background:
|
||||||
linear-gradient(135deg, rgba(31, 37, 68, 0.94), rgba(18, 22, 44, 0.96)) padding-box,
|
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);
|
inset 0 1px 0 rgba(255, 255, 255, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-action-button.primary,
|
:is(
|
||||||
.import-icon-button.primary,
|
.sidebar-action-button,
|
||||||
.toolbox-icon-button.primary,
|
.import-icon-button,
|
||||||
.card-icon-button.primary,
|
.toolbox-icon-button,
|
||||||
.drawer-action-button.primary,
|
.card-icon-button,
|
||||||
.drawer-close-button.primary,
|
.drawer-action-button,
|
||||||
.filter-reset-button.primary,
|
.drawer-close-button,
|
||||||
.screenshot-viewer-button.primary,
|
.filter-reset-button,
|
||||||
.module-edit-button.primary,
|
.screenshot-viewer-button,
|
||||||
.module-delete-button.primary,
|
.module-edit-button,
|
||||||
.shot-delete-button.primary,
|
.module-delete-button,
|
||||||
.tool-quick-add-button.primary {
|
.shot-annotate-button,
|
||||||
|
.shot-delete-button,
|
||||||
|
.tool-quick-add-button
|
||||||
|
).primary {
|
||||||
border-color: rgba(246, 196, 83, 0.34);
|
border-color: rgba(246, 196, 83, 0.34);
|
||||||
background:
|
background:
|
||||||
linear-gradient(135deg, rgba(72, 43, 160, 0.96), rgba(45, 52, 146, 0.94)) padding-box,
|
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);
|
inset 0 1px 0 rgba(255, 255, 255, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-action-button.primary:hover,
|
:is(
|
||||||
.import-icon-button.primary:hover,
|
.sidebar-action-button,
|
||||||
.toolbox-icon-button.primary:hover,
|
.import-icon-button,
|
||||||
.card-icon-button.primary:hover,
|
.toolbox-icon-button,
|
||||||
.drawer-action-button.primary:hover,
|
.card-icon-button,
|
||||||
.drawer-close-button.primary:hover,
|
.drawer-action-button,
|
||||||
.filter-reset-button.primary:hover,
|
.drawer-close-button,
|
||||||
.screenshot-viewer-button.primary:hover,
|
.filter-reset-button,
|
||||||
.module-edit-button.primary:hover,
|
.screenshot-viewer-button,
|
||||||
.module-delete-button.primary:hover,
|
.module-edit-button,
|
||||||
.shot-delete-button.primary:hover,
|
.module-delete-button,
|
||||||
.tool-quick-add-button.primary:hover {
|
.shot-annotate-button,
|
||||||
|
.shot-delete-button,
|
||||||
|
.tool-quick-add-button
|
||||||
|
).primary:hover {
|
||||||
border-color: rgba(246, 196, 83, 0.72);
|
border-color: rgba(246, 196, 83, 0.72);
|
||||||
background:
|
background:
|
||||||
linear-gradient(135deg, rgba(109, 40, 217, 0.98), rgba(67, 56, 202, 0.96)) padding-box,
|
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);
|
inset 0 1px 0 rgba(255, 255, 255, 0.18);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-action-button:hover .ui-icon,
|
:is(
|
||||||
.import-icon-button:hover .ui-icon,
|
.sidebar-action-button,
|
||||||
.toolbox-icon-button:hover .ui-icon,
|
.import-icon-button,
|
||||||
.card-icon-button:hover .ui-icon,
|
.toolbox-icon-button,
|
||||||
.drawer-action-button:hover .ui-icon,
|
.card-icon-button,
|
||||||
.drawer-close-button:hover .ui-icon,
|
.drawer-action-button,
|
||||||
.filter-reset-button:hover .ui-icon,
|
.drawer-close-button,
|
||||||
.screenshot-viewer-button:hover .ui-icon,
|
.filter-reset-button,
|
||||||
.module-edit-button:hover .ui-icon,
|
.screenshot-viewer-button,
|
||||||
.tool-quick-add-button:hover .ui-icon {
|
.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;
|
background-color: #c4b5fd;
|
||||||
filter: drop-shadow(0 0 6px rgba(196, 181, 253, 0.28));
|
filter: drop-shadow(0 0 6px rgba(196, 181, 253, 0.28));
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-icon-button.danger,
|
:is(.card-icon-button, .module-delete-button, .shot-delete-button, .checklist-delete-button).danger {
|
||||||
.module-delete-button.danger,
|
|
||||||
.shot-delete-button.danger {
|
|
||||||
border-color: rgba(251, 113, 133, 0.2);
|
border-color: rgba(251, 113, 133, 0.2);
|
||||||
color: var(--color-danger);
|
color: var(--color-danger);
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-icon-button.danger:hover,
|
:is(.card-icon-button, .module-delete-button, .shot-delete-button, .checklist-delete-button).danger:hover {
|
||||||
.module-delete-button.danger:hover,
|
|
||||||
.shot-delete-button.danger:hover {
|
|
||||||
border-color: rgba(251, 113, 133, 0.56);
|
border-color: rgba(251, 113, 133, 0.56);
|
||||||
background:
|
background:
|
||||||
linear-gradient(135deg, rgba(80, 25, 42, 0.72), rgba(48, 18, 34, 0.82)) padding-box,
|
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);
|
inset 0 1px 0 rgba(255, 255, 255, 0.06);
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-icon-button.danger:hover .ui-icon,
|
:is(.card-icon-button, .module-delete-button, .shot-delete-button, .checklist-delete-button).danger:hover .ui-icon {
|
||||||
.module-delete-button.danger:hover .ui-icon,
|
|
||||||
.shot-delete-button.danger:hover .ui-icon {
|
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
filter: drop-shadow(0 0 6px rgba(251, 113, 133, 0.26));
|
filter: drop-shadow(0 0 6px rgba(251, 113, 133, 0.26));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -195,13 +195,26 @@
|
||||||
.filter-logic button {
|
.filter-logic button {
|
||||||
min-height: 30px;
|
min-height: 30px;
|
||||||
padding: 0 12px;
|
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);
|
color: var(--color-text-primary);
|
||||||
font-size: var(--font-size-sm);
|
font-size: var(--font-size-sm);
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
text-transform: uppercase;
|
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 {
|
.filter-options {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,11 @@
|
||||||
-webkit-mask-image: url("/static/icons/calculator.svg");
|
-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 {
|
.ui-icon-scrollable {
|
||||||
mask-image: url("/static/icons/scrollable.svg");
|
mask-image: url("/static/icons/scrollable.svg");
|
||||||
-webkit-mask-image: url("/static/icons/scrollable.svg");
|
-webkit-mask-image: url("/static/icons/scrollable.svg");
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
padding: var(--space-5);
|
padding: var(--space-5);
|
||||||
|
overscroll-behavior: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
.screenshot-viewer-backdrop {
|
.screenshot-viewer-backdrop {
|
||||||
|
|
@ -15,9 +16,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.screenshot-viewer {
|
.screenshot-viewer {
|
||||||
|
--viewer-content-max-height: min(80vh, calc(100vh - 140px));
|
||||||
|
--viewer-image-height: 400px;
|
||||||
position: relative;
|
position: relative;
|
||||||
display: grid;
|
display: grid;
|
||||||
width: min(1120px, calc(100vw - 32px));
|
width: min(1500px, calc(100vw - 32px));
|
||||||
max-height: calc(100vh - 48px);
|
max-height: calc(100vh - 48px);
|
||||||
gap: var(--space-4);
|
gap: var(--space-4);
|
||||||
padding: var(--space-4);
|
padding: var(--space-4);
|
||||||
|
|
@ -38,15 +41,106 @@
|
||||||
flex: 0 0 40px;
|
flex: 0 0 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.screenshot-viewer img {
|
.screenshot-viewer-body {
|
||||||
display: block;
|
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-width: 100%;
|
||||||
max-height: calc(100vh - 170px);
|
|
||||||
justify-self: center;
|
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);
|
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;
|
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"] {
|
.drawer[aria-hidden="true"] {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
@ -166,6 +260,7 @@ body.is-resizing-drawer * {
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
padding: var(--space-5);
|
padding: var(--space-5);
|
||||||
|
overscroll-behavior: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tool-add-modal-root {
|
.tool-add-modal-root {
|
||||||
|
|
@ -175,6 +270,7 @@ body.is-resizing-drawer * {
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
padding: var(--space-5);
|
padding: var(--space-5);
|
||||||
|
overscroll-behavior: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
.confirm-backdrop {
|
.confirm-backdrop {
|
||||||
|
|
|
||||||
|
|
@ -139,6 +139,14 @@
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.screenshot-viewer-body.has-annotation-side {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.screenshot-viewer-side {
|
||||||
|
height: min(280px, 32vh);
|
||||||
|
}
|
||||||
|
|
||||||
.module-column {
|
.module-column {
|
||||||
display: contents;
|
display: contents;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -148,7 +148,17 @@
|
||||||
min-height: 42px;
|
min-height: 42px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border-color: var(--color-border);
|
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 {
|
.toolbox-icon-picker-menu button.active {
|
||||||
|
|
@ -208,11 +218,11 @@
|
||||||
top: calc(100% + 8px);
|
top: calc(100% + 8px);
|
||||||
right: 0;
|
right: 0;
|
||||||
z-index: 300;
|
z-index: 300;
|
||||||
display: flex;
|
display: grid;
|
||||||
flex-wrap: wrap;
|
grid-template-columns: repeat(5, 40px);
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
width: max-content;
|
width: max-content;
|
||||||
max-width: min(280px, calc(100vw - 48px));
|
max-width: calc(100vw - 48px);
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
|
|
@ -425,8 +435,18 @@
|
||||||
font-weight: 900;
|
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 {
|
.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;
|
color: white;
|
||||||
box-shadow: var(--shadow-primary);
|
box-shadow: var(--shadow-primary);
|
||||||
}
|
}
|
||||||
|
|
@ -544,6 +564,11 @@
|
||||||
-webkit-mask-image: url("/static/icons/calculator.svg");
|
-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 {
|
.notepad {
|
||||||
display: block;
|
display: block;
|
||||||
width: calc(100% - 32px);
|
width: calc(100% - 32px);
|
||||||
|
|
@ -673,6 +698,10 @@ textarea:focus {
|
||||||
min-width: 28px;
|
min-width: 28px;
|
||||||
min-height: 28px;
|
min-height: 28px;
|
||||||
padding: 0;
|
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;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -781,9 +810,6 @@ textarea:focus {
|
||||||
}
|
}
|
||||||
|
|
||||||
.counter-actions button {
|
.counter-actions button {
|
||||||
min-width: 0;
|
|
||||||
min-height: 34px;
|
|
||||||
padding: 0;
|
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -903,7 +929,10 @@ textarea:focus {
|
||||||
background: rgba(5, 7, 17, 0.24);
|
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;
|
display: inline-flex;
|
||||||
width: 34px;
|
width: 34px;
|
||||||
min-width: 34px;
|
min-width: 34px;
|
||||||
|
|
@ -913,7 +942,7 @@ textarea:focus {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border: 1px solid rgba(165, 180, 252, 0.12);
|
border: 1px solid rgba(165, 180, 252, 0.12);
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
background: rgba(5, 7, 17, 0.24);
|
background: rgba(21, 26, 48, 0.88);
|
||||||
color: var(--color-text-secondary);
|
color: var(--color-text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -922,21 +951,6 @@ textarea:focus {
|
||||||
opacity: 0.48;
|
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 {
|
.calculator-action-button .ui-icon {
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
|
|
@ -995,14 +1009,58 @@ textarea:focus {
|
||||||
|
|
||||||
.calculator-root-button:hover,
|
.calculator-root-button:hover,
|
||||||
.calculator-action-button:hover:not(:disabled),
|
.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:hover,
|
||||||
.calculator-scroll-toggle.active {
|
.calculator-scroll-toggle.active {
|
||||||
border-color: rgba(196, 181, 253, 0.32);
|
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);
|
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 {
|
.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 {
|
.calculator-scroll-toggle.active i {
|
||||||
|
|
@ -1156,16 +1214,8 @@ textarea:focus {
|
||||||
opacity: 0.72;
|
opacity: 0.72;
|
||||||
}
|
}
|
||||||
|
|
||||||
.calculator-entry > button.danger {
|
.calculator-entry > button:not(.calculator-entry-summary, .calculator-entry-value) {
|
||||||
min-width: 34px;
|
|
||||||
min-height: 36px;
|
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,
|
.calculator-entry-list li.active > .calculator-entry > .calculator-entry-summary,
|
||||||
|
|
@ -1221,9 +1271,7 @@ textarea:focus {
|
||||||
.link-item button {
|
.link-item button {
|
||||||
width: 40px;
|
width: 40px;
|
||||||
min-width: 40px;
|
min-width: 40px;
|
||||||
height: 40px;
|
|
||||||
min-height: 40px;
|
min-height: 40px;
|
||||||
padding: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropzone {
|
.dropzone {
|
||||||
|
|
@ -1250,6 +1298,10 @@ textarea:focus {
|
||||||
box-shadow: 0 0 0 3px rgba(246, 196, 83, 0.12);
|
box-shadow: 0 0 0 3px rgba(246, 196, 83, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.annotation-dropzone {
|
||||||
|
min-height: 58px;
|
||||||
|
}
|
||||||
|
|
||||||
.paste-target {
|
.paste-target {
|
||||||
min-height: 46px;
|
min-height: 46px;
|
||||||
margin: 0 var(--space-4) var(--space-4);
|
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);
|
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 {
|
.shots {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||||
gap: var(--space-3);
|
gap: var(--space-3);
|
||||||
padding: 0 var(--space-4) var(--space-4);
|
padding: var(--space-4);
|
||||||
}
|
}
|
||||||
|
|
||||||
.shots figure {
|
.shots figure {
|
||||||
|
|
@ -1307,6 +1505,13 @@ textarea:focus {
|
||||||
background: var(--color-bg-deep);
|
background: var(--color-bg-deep);
|
||||||
}
|
}
|
||||||
|
|
||||||
.shot-delete-button {
|
.shot-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-2);
|
||||||
margin-top: 8px;
|
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