This commit is contained in:
parent
ef5f977f8a
commit
d7871736c8
10 changed files with 566 additions and 8 deletions
87
AGENTS.md
Normal file
87
AGENTS.md
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
# Agent Instructions
|
||||||
|
|
||||||
|
Sokko G est une webapp React + Vite + Sass centrée sur des toolboxes locales et des pages de jeu.
|
||||||
|
|
||||||
|
## Références projet
|
||||||
|
|
||||||
|
Avant de modifier un outil toolbox, le stockage IndexedDB, l'import/export ou les formats de données, lire :
|
||||||
|
|
||||||
|
- `docs/STORAGE_SCHEMA.md`
|
||||||
|
- `docs/FEATURE_CHECKLIST.md`
|
||||||
|
|
||||||
|
Après ce type de modification, mettre à jour ces documents si le format ou le workflow change.
|
||||||
|
|
||||||
|
## Contenu éditable
|
||||||
|
|
||||||
|
Le contenu éditorial du site doit rester dans :
|
||||||
|
|
||||||
|
- `website/public/data/site.json`
|
||||||
|
- `website/public/data/<gameId>/...`
|
||||||
|
|
||||||
|
Ne pas réintroduire de fallback massif type `DEFAULT_SITE_CONTENT` dans le code React. `site.json` est la source de vérité et `npm run check` doit échouer si le contenu requis est invalide.
|
||||||
|
|
||||||
|
## Outils toolbox
|
||||||
|
|
||||||
|
Chaque outil toolbox doit rester dans son propre fichier dans :
|
||||||
|
|
||||||
|
```text
|
||||||
|
website/src/features/toolboxes/modules/
|
||||||
|
```
|
||||||
|
|
||||||
|
Quand un outil est ajouté ou modifié :
|
||||||
|
|
||||||
|
- déclarer l'outil dans `website/src/features/toolboxes/modules/index.jsx` ;
|
||||||
|
- ajouter les textes dans `website/public/data/site.json` ;
|
||||||
|
- mettre à jour la validation dans `tests/helpers/data-validation.mjs` ;
|
||||||
|
- mettre à jour la structure IndexedDB dans `docs/STORAGE_SCHEMA.md` si nécessaire ;
|
||||||
|
- vérifier l'affichage page toolbox et panneau latéral.
|
||||||
|
|
||||||
|
## Style
|
||||||
|
|
||||||
|
Le style est en Sass dans `website/src/styles/`.
|
||||||
|
|
||||||
|
Privilégier :
|
||||||
|
|
||||||
|
- les tokens existants ;
|
||||||
|
- les classes de boutons existantes ;
|
||||||
|
- les patterns visuels déjà présents pour les cards, panels, badges, scrollbars et hovers.
|
||||||
|
|
||||||
|
Éviter les styles isolés qui ne réutilisent pas le thème.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
Avant de terminer une modification significative, lancer :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run check
|
||||||
|
```
|
||||||
|
|
||||||
|
Le check couvre la génération des index de listes, les tests et le build de production.
|
||||||
|
|
||||||
|
## Pré-push check
|
||||||
|
|
||||||
|
Quand l'utilisateur indique qu'il va push, faire une passe globale avant de conclure.
|
||||||
|
|
||||||
|
Vérifier et corriger si nécessaire :
|
||||||
|
|
||||||
|
- code mort, imports inutilisés, fonctions inutilisées, styles devenus obsolètes ;
|
||||||
|
- duplication de code ou de style pouvant être factorisée sans complexifier le projet ;
|
||||||
|
- composants qui utilisent des classes trop spécifiques alors qu'un pattern général existe déjà ;
|
||||||
|
- incohérences entre boutons, badges, cards, panels, hovers, focus states et scrollbars ;
|
||||||
|
- cohérence entre `website/public/data/site.json`, les validations et les textes utilisés ;
|
||||||
|
- cohérence entre `docs/STORAGE_SCHEMA.md` et les normalisations IndexedDB réelles ;
|
||||||
|
- import/export des toolboxes quand la structure de données a changé ;
|
||||||
|
- champs inutiles persistés dans IndexedDB ou dans les exports ;
|
||||||
|
- fichiers générés attendus, notamment les index de listes ;
|
||||||
|
- optimisations simples de performance : recalculs au render, listeners non nettoyés, observers, effets CSS lourds appliqués en masse.
|
||||||
|
|
||||||
|
Utiliser en priorité :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rg
|
||||||
|
git status --short
|
||||||
|
git diff --stat
|
||||||
|
npm run check
|
||||||
|
```
|
||||||
|
|
||||||
|
Ne pas faire de refactor large sans bénéfice clair. Garder les corrections pré-push ciblées, vérifiables et faciles à relire.
|
||||||
60
docs/FEATURE_CHECKLIST.md
Normal file
60
docs/FEATURE_CHECKLIST.md
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
# Feature Checklist
|
||||||
|
|
||||||
|
Checklist à suivre lors de l'ajout ou de la modification d'un outil, d'une page jeu ou d'un format de données.
|
||||||
|
|
||||||
|
## Outil Toolbox
|
||||||
|
|
||||||
|
- Ajouter ou modifier le composant dans `website/src/features/toolboxes/modules/`.
|
||||||
|
- Déclarer l'outil dans `website/src/features/toolboxes/modules/index.jsx`.
|
||||||
|
- Ajouter les textes nécessaires dans `website/public/data/site.json`.
|
||||||
|
- Mettre à jour la validation de `site.json` dans `tests/helpers/data-validation.mjs`.
|
||||||
|
- Mettre à jour la normalisation et le stockage compact dans `website/src/main.jsx`.
|
||||||
|
- Mettre à jour `docs/STORAGE_SCHEMA.md`.
|
||||||
|
- Vérifier l'import et l'export si l'outil stocke des données.
|
||||||
|
- Vérifier l'affichage dans la page toolbox complète.
|
||||||
|
- Vérifier l'affichage dans le panneau latéral.
|
||||||
|
- Vérifier le mode une colonne et deux colonnes.
|
||||||
|
- Vérifier le quota de stockage si l'outil manipule des données lourdes.
|
||||||
|
|
||||||
|
## Page Jeu
|
||||||
|
|
||||||
|
- Ajouter les données dans `website/public/data/`.
|
||||||
|
- Ajouter les images dans `website/public/static/img/games/<gameId>/`.
|
||||||
|
- Déclarer le jeu dans `website/public/data/site.json`.
|
||||||
|
- Vérifier la page `/games`.
|
||||||
|
- Vérifier la page `/games/<gameId>`.
|
||||||
|
- Vérifier le bouton toolbox et le panneau latéral.
|
||||||
|
- Vérifier le breadcrumb.
|
||||||
|
- Vérifier les textes avec retours à la ligne.
|
||||||
|
|
||||||
|
## Listes de jeu
|
||||||
|
|
||||||
|
- Ajouter la liste JSON dans `website/public/data/<gameId>/lists/`.
|
||||||
|
- Lancer `npm run lists:index`.
|
||||||
|
- Vérifier que `index.json` est régénéré.
|
||||||
|
- Vérifier que `nom` est présent sur chaque élément.
|
||||||
|
- Vérifier que `description` et `quantite` restent facultatifs.
|
||||||
|
- Vérifier la copie au format checklist.
|
||||||
|
- Vérifier la création directe de checklist quand une toolbox est associée.
|
||||||
|
|
||||||
|
## Style
|
||||||
|
|
||||||
|
- Réutiliser les tokens existants avant d'ajouter une nouvelle couleur ou taille.
|
||||||
|
- Réutiliser les classes de boutons existantes.
|
||||||
|
- Vérifier les boutons icon, danger, primary et secondary.
|
||||||
|
- Vérifier les cartes, panels et scrollbars.
|
||||||
|
- Vérifier les états hover/focus.
|
||||||
|
- Vérifier mobile et desktop quand le changement touche la mise en page.
|
||||||
|
|
||||||
|
## Validation avant push
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run check
|
||||||
|
```
|
||||||
|
|
||||||
|
Le check doit passer avant de pousser :
|
||||||
|
|
||||||
|
- syntaxe serveur et Vite ;
|
||||||
|
- génération des index de listes ;
|
||||||
|
- tests Node ;
|
||||||
|
- build de production.
|
||||||
313
docs/STORAGE_SCHEMA.md
Normal file
313
docs/STORAGE_SCHEMA.md
Normal file
|
|
@ -0,0 +1,313 @@
|
||||||
|
# Storage Schema
|
||||||
|
|
||||||
|
Ce document décrit les données persistées dans IndexedDB pour les toolboxes.
|
||||||
|
Il doit être mis à jour à chaque ajout ou modification d'outil.
|
||||||
|
|
||||||
|
## IndexedDB
|
||||||
|
|
||||||
|
Base : `sokkog`
|
||||||
|
|
||||||
|
Object stores :
|
||||||
|
|
||||||
|
- `kv` : données globales simples.
|
||||||
|
- `modules` : données des outils, séparées par toolbox et par module.
|
||||||
|
|
||||||
|
Clés principales dans `kv` :
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"toolboxes": [],
|
||||||
|
"links": {},
|
||||||
|
"setting:toolboxOrder": [],
|
||||||
|
"setting:storageBudgetBytes": 536870912
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Entrée type dans `modules` :
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"key": "tbx1:mod1",
|
||||||
|
"data": {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Toolbox
|
||||||
|
|
||||||
|
Les toolboxes sont stockées dans `kv/toolboxes`.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "tbx1",
|
||||||
|
"name": "Monster Hunter Wilds",
|
||||||
|
"gameId": "mhwilds",
|
||||||
|
"icon": "/static/img/toolbox-icons/toolbox.png",
|
||||||
|
"modules": [
|
||||||
|
{
|
||||||
|
"id": "mod1",
|
||||||
|
"type": "checklist",
|
||||||
|
"title": "Armures"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"layout": "two",
|
||||||
|
"updatedAt": "2026-07-25T12:00:00.000Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Notes :
|
||||||
|
|
||||||
|
- `gameId` est vide pour une toolbox libre.
|
||||||
|
- `icon` est utilisé uniquement pour les toolboxes libres.
|
||||||
|
- `layout` vaut généralement `one` ou `two`.
|
||||||
|
- Les données lourdes des outils ne sont pas stockées dans la toolbox, mais dans `modules`.
|
||||||
|
|
||||||
|
## Liens Jeu / Toolbox
|
||||||
|
|
||||||
|
Les associations entre pages jeux et toolboxes sont stockées dans `kv/links`.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mhwilds": "tbx1",
|
||||||
|
"diablo4": "tbx2"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Outil Bloc Notes
|
||||||
|
|
||||||
|
Type : `notepad`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"text": "Notes rapides..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Stockage compact :
|
||||||
|
|
||||||
|
- si `text` est vide, l'entrée de module peut être supprimée.
|
||||||
|
|
||||||
|
## Outil Checklist
|
||||||
|
|
||||||
|
Type : `checklist`
|
||||||
|
|
||||||
|
Format simple, sans catégorie :
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "item1",
|
||||||
|
"label": "Potion",
|
||||||
|
"qtyTarget": 10,
|
||||||
|
"qtyCurrent": 3
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Format avec catégories :
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"hideCompletedSections": true,
|
||||||
|
"hideCompletedSectionsFully": false,
|
||||||
|
"sections": [
|
||||||
|
{
|
||||||
|
"id": "section1",
|
||||||
|
"title": "Rey Dau alpha",
|
||||||
|
"hideWhenComplete": false,
|
||||||
|
"collapsed": false,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "item1",
|
||||||
|
"label": "Casque",
|
||||||
|
"qtyTarget": 1,
|
||||||
|
"qtyCurrent": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Stockage compact :
|
||||||
|
|
||||||
|
- `qtyTarget` est omis si la valeur vaut `1`.
|
||||||
|
- `qtyCurrent` est omis si la valeur vaut `0`.
|
||||||
|
- `hideCompletedSections` est omis si `false`.
|
||||||
|
- `hideCompletedSectionsFully` est omis si `false`.
|
||||||
|
- `title` est omis si vide.
|
||||||
|
- `hideWhenComplete` est omis si non défini.
|
||||||
|
- `collapsed` est omis si `false`.
|
||||||
|
|
||||||
|
## Outil Images
|
||||||
|
|
||||||
|
Type : `screenshots`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"shots": [
|
||||||
|
{
|
||||||
|
"id": "shot1",
|
||||||
|
"label": "Map zone nord",
|
||||||
|
"dataUrl": "data:image/webp;base64,..."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Stockage compact :
|
||||||
|
|
||||||
|
- `label` est facultatif.
|
||||||
|
- `label` est omis si vide.
|
||||||
|
- `createdAt` n'est pas stocké.
|
||||||
|
- Le nom de fichier original n'est pas stocké.
|
||||||
|
|
||||||
|
## Outil Annotation d'images
|
||||||
|
|
||||||
|
Type : `imageAnnotation`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"image": "data:image/webp;base64,...",
|
||||||
|
"markers": [
|
||||||
|
{
|
||||||
|
"id": "marker1",
|
||||||
|
"x": 42.5,
|
||||||
|
"y": 68,
|
||||||
|
"label": "Entrée"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Stockage compact :
|
||||||
|
|
||||||
|
- un seul visuel par outil.
|
||||||
|
- `x` et `y` sont des pourcentages entre `0` et `100`.
|
||||||
|
- `label` est facultatif.
|
||||||
|
- `label` est omis si vide.
|
||||||
|
|
||||||
|
## Outil Liens
|
||||||
|
|
||||||
|
Type : `links`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"id": "link1",
|
||||||
|
"title": "Build rogue",
|
||||||
|
"url": "https://example.com/build"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Stockage compact :
|
||||||
|
|
||||||
|
- `title` est facultatif.
|
||||||
|
- `title` est omis si vide.
|
||||||
|
- `url` est obligatoire.
|
||||||
|
|
||||||
|
## Outil Compteurs
|
||||||
|
|
||||||
|
Type : `counters`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"counters": [
|
||||||
|
{
|
||||||
|
"id": "counter1",
|
||||||
|
"label": "Victoire",
|
||||||
|
"value": 12
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "counter2",
|
||||||
|
"label": "Défaite",
|
||||||
|
"value": -2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Notes :
|
||||||
|
|
||||||
|
- `value` est numérique.
|
||||||
|
- Les valeurs négatives sont acceptées.
|
||||||
|
- Les compteurs sans libellé ne sont pas conservés.
|
||||||
|
|
||||||
|
## Outil Calculateur
|
||||||
|
|
||||||
|
Type : `calculator`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"scrollResults": true,
|
||||||
|
"entries": [
|
||||||
|
{
|
||||||
|
"id": "calc1",
|
||||||
|
"label": "Lingots de fer",
|
||||||
|
"value": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "calc2",
|
||||||
|
"parentId": "calc1",
|
||||||
|
"label": "Minerais de fer",
|
||||||
|
"value": 500
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Stockage compact :
|
||||||
|
|
||||||
|
- `scrollResults` est omis si `false`.
|
||||||
|
- `parentId` est omis si vide.
|
||||||
|
- `label` peut être vide.
|
||||||
|
- `value` est numérique.
|
||||||
|
- Si un `parentId` ne pointe vers aucune entrée existante, il est réinitialisé à vide.
|
||||||
|
|
||||||
|
## Export
|
||||||
|
|
||||||
|
L'export d'une toolbox remappe les identifiants pour produire des IDs courts et indépendants.
|
||||||
|
|
||||||
|
Exemple :
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"toolbox": {
|
||||||
|
"id": "t1",
|
||||||
|
"name": "MHW",
|
||||||
|
"modules": [
|
||||||
|
{
|
||||||
|
"id": "m1",
|
||||||
|
"type": "checklist",
|
||||||
|
"title": "Armures"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"updatedAt": "2026-07-25T12:00:00.000Z"
|
||||||
|
},
|
||||||
|
"modules": {
|
||||||
|
"m1": {
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "i1",
|
||||||
|
"label": "Casque"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## A maintenir à chaque update
|
||||||
|
|
||||||
|
Quand un outil change de structure :
|
||||||
|
|
||||||
|
- mettre à jour ce document ;
|
||||||
|
- mettre à jour les normalisations dans `website/src/main.jsx` ;
|
||||||
|
- mettre à jour les textes dans `website/public/data/site.json` si nécessaire ;
|
||||||
|
- mettre à jour la validation dans `tests/helpers/data-validation.mjs` si nécessaire ;
|
||||||
|
- vérifier import/export ;
|
||||||
|
- lancer `npm run check`.
|
||||||
|
|
@ -106,6 +106,8 @@ export function validateSiteContent(site) {
|
||||||
"toolboxes.modules.screenshots.pastePlaceholder",
|
"toolboxes.modules.screenshots.pastePlaceholder",
|
||||||
"toolboxes.modules.screenshots.pasteAriaLabel",
|
"toolboxes.modules.screenshots.pasteAriaLabel",
|
||||||
"toolboxes.modules.screenshots.imageAlt",
|
"toolboxes.modules.screenshots.imageAlt",
|
||||||
|
"toolboxes.modules.screenshots.labelPlaceholder",
|
||||||
|
"toolboxes.modules.screenshots.labelAriaLabel",
|
||||||
"toolboxes.modules.screenshots.previewAriaLabel",
|
"toolboxes.modules.screenshots.previewAriaLabel",
|
||||||
"toolboxes.modules.screenshots.annotateAriaLabel",
|
"toolboxes.modules.screenshots.annotateAriaLabel",
|
||||||
"toolboxes.modules.screenshots.annotateTitle",
|
"toolboxes.modules.screenshots.annotateTitle",
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,19 @@ test("vite entrypoint loads the react application", async () => {
|
||||||
assert.match(html, /\/src\/main\.jsx/);
|
assert.match(html, /\/src\/main\.jsx/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("project documentation tracks storage and feature updates", async () => {
|
||||||
|
const storageSchema = await readFile("docs/STORAGE_SCHEMA.md", "utf8");
|
||||||
|
const featureChecklist = await readFile("docs/FEATURE_CHECKLIST.md", "utf8");
|
||||||
|
|
||||||
|
assert.match(storageSchema, /IndexedDB/);
|
||||||
|
assert.match(storageSchema, /Outil Checklist/);
|
||||||
|
assert.match(storageSchema, /Outil Images/);
|
||||||
|
assert.match(storageSchema, /Outil Annotation d'images/);
|
||||||
|
assert.match(storageSchema, /Outil Calculateur/);
|
||||||
|
assert.match(featureChecklist, /Mettre à jour `docs\/STORAGE_SCHEMA\.md`/);
|
||||||
|
assert.match(featureChecklist, /npm run check/);
|
||||||
|
});
|
||||||
|
|
||||||
test("react application defines the expected local toolbox primitives", async () => {
|
test("react application defines the expected local toolbox primitives", async () => {
|
||||||
const source = await readFile("website/src/main.jsx", "utf8");
|
const source = await readFile("website/src/main.jsx", "utf8");
|
||||||
const styles = await readFile("website/src/styles/main.scss", "utf8");
|
const styles = await readFile("website/src/styles/main.scss", "utf8");
|
||||||
|
|
|
||||||
|
|
@ -224,6 +224,8 @@
|
||||||
"pastePlaceholder": "Coller une image ici",
|
"pastePlaceholder": "Coller une image ici",
|
||||||
"pasteAriaLabel": "Coller une image depuis le presse-papiers",
|
"pasteAriaLabel": "Coller une image depuis le presse-papiers",
|
||||||
"imageAlt": "Image",
|
"imageAlt": "Image",
|
||||||
|
"labelPlaceholder": "Libellé de l'image",
|
||||||
|
"labelAriaLabel": "Libellé de l'image",
|
||||||
"previewAriaLabel": "Agrandir l'image",
|
"previewAriaLabel": "Agrandir l'image",
|
||||||
"annotateAriaLabel": "Annoter l'image",
|
"annotateAriaLabel": "Annoter l'image",
|
||||||
"annotateTitle": "Annoter",
|
"annotateTitle": "Annoter",
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,12 @@ export function ScreenshotsModule({ toolboxId, moduleId, context, editing }) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateShot(shotId, patch) {
|
||||||
|
context.setModuleData(toolboxId, moduleId, {
|
||||||
|
shots: data.shots.map((shot) => shot.id === shotId ? { ...shot, ...patch } : shot)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{editing && (
|
{editing && (
|
||||||
|
|
@ -66,6 +72,17 @@ export function ScreenshotsModule({ toolboxId, moduleId, context, editing }) {
|
||||||
<button className="shot-preview" onClick={() => context.setScreenshot(shot)} aria-label={textContent.previewAriaLabel || "Agrandir l'image"}>
|
<button className="shot-preview" onClick={() => context.setScreenshot(shot)} aria-label={textContent.previewAriaLabel || "Agrandir l'image"}>
|
||||||
<img src={shot.dataUrl} alt={textContent.imageAlt || "Image"} />
|
<img src={shot.dataUrl} alt={textContent.imageAlt || "Image"} />
|
||||||
</button>
|
</button>
|
||||||
|
{editing ? (
|
||||||
|
<input
|
||||||
|
className="shot-label-input"
|
||||||
|
value={shot.label || ""}
|
||||||
|
placeholder={textContent.labelPlaceholder || "Libellé de l'image"}
|
||||||
|
onChange={(event) => updateShot(shot.id, { label: event.target.value })}
|
||||||
|
aria-label={textContent.labelAriaLabel || "Libellé de l'image"}
|
||||||
|
/>
|
||||||
|
) : shot.label ? (
|
||||||
|
<figcaption className="shot-label">{shot.label}</figcaption>
|
||||||
|
) : null}
|
||||||
<div className="shot-actions">
|
<div className="shot-actions">
|
||||||
<button
|
<button
|
||||||
className="shot-annotate-button"
|
className="shot-annotate-button"
|
||||||
|
|
|
||||||
|
|
@ -241,6 +241,11 @@ function normalizeCalculatorValue(value) {
|
||||||
return Number.isFinite(parsed) ? parsed : 0;
|
return Number.isFinite(parsed) ? parsed : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function labelFromFileName(name) {
|
||||||
|
if (!name || name === "image.png") return "";
|
||||||
|
return name.replace(/\.[^.]+$/, "").trim();
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeCalculatorData(data) {
|
function normalizeCalculatorData(data) {
|
||||||
const entries = (data?.entries || [])
|
const entries = (data?.entries || [])
|
||||||
.map((entry) => ({
|
.map((entry) => ({
|
||||||
|
|
@ -313,7 +318,11 @@ function compactModuleDataForStorage(type, value) {
|
||||||
if (type === "screenshots") {
|
if (type === "screenshots") {
|
||||||
const shots = (Array.isArray(value?.shots) ? value.shots : [])
|
const shots = (Array.isArray(value?.shots) ? value.shots : [])
|
||||||
.filter((shot) => shot?.dataUrl)
|
.filter((shot) => shot?.dataUrl)
|
||||||
.map((shot) => ({ id: shot.id || uid("shot"), dataUrl: shot.dataUrl }));
|
.map((shot) => {
|
||||||
|
const compact = { id: shot.id || uid("shot"), dataUrl: shot.dataUrl };
|
||||||
|
if (shot.label) compact.label = shot.label;
|
||||||
|
return compact;
|
||||||
|
});
|
||||||
return shots.length ? { shots } : null;
|
return shots.length ? { shots } : null;
|
||||||
}
|
}
|
||||||
if (type === "imageAnnotation") {
|
if (type === "imageAnnotation") {
|
||||||
|
|
@ -798,7 +807,10 @@ function App() {
|
||||||
if (!imageFiles.length) return false;
|
if (!imageFiles.length) return false;
|
||||||
const data = store.getModuleData(toolboxId, moduleId, { shots: [] });
|
const data = store.getModuleData(toolboxId, moduleId, { shots: [] });
|
||||||
for (const file of imageFiles) {
|
for (const file of imageFiles) {
|
||||||
data.shots.unshift({ id: uid("shot"), dataUrl: await compressImage(file) });
|
const shot = { id: uid("shot"), dataUrl: await compressImage(file) };
|
||||||
|
const label = labelFromFileName(file.name);
|
||||||
|
if (label) shot.label = label;
|
||||||
|
data.shots.unshift(shot);
|
||||||
}
|
}
|
||||||
return store.updateModuleData(toolboxId, moduleId, data);
|
return store.updateModuleData(toolboxId, moduleId, data);
|
||||||
}
|
}
|
||||||
|
|
@ -1812,10 +1824,13 @@ function ScreenshotViewer({ shot, onClose }) {
|
||||||
<div className="screenshot-viewer-backdrop" onClick={onClose} />
|
<div className="screenshot-viewer-backdrop" onClick={onClose} />
|
||||||
<section ref={viewerRef} 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>
|
||||||
{!canAnnotate && !hasMarkers && (
|
{shot.label && <strong className="screenshot-viewer-title">{shot.label}</strong>}
|
||||||
<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>
|
<div className="screenshot-viewer-actions">
|
||||||
)}
|
{!canAnnotate && !hasMarkers && (
|
||||||
<button className="screenshot-viewer-button" onClick={onClose} aria-label="Fermer" title="Fermer"><Icon name="close" /></button>
|
<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>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div className={`screenshot-viewer-body ${canAnnotate ? "has-annotation-side" : ""}`}>
|
<div className={`screenshot-viewer-body ${canAnnotate ? "has-annotation-side" : ""}`}>
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
|
|
@ -31,8 +31,22 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.screenshot-viewer header {
|
.screenshot-viewer header {
|
||||||
display: flex;
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.screenshot-viewer-title {
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--color-text);
|
||||||
|
font-size: var(--font-size-md);
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.screenshot-viewer-actions {
|
||||||
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
gap: var(--space-2);
|
gap: var(--space-2);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1710,6 +1710,8 @@ textarea:focus {
|
||||||
|
|
||||||
.shots figure {
|
.shots figure {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
|
|
@ -1732,6 +1734,40 @@ textarea:focus {
|
||||||
box-shadow: 0 0 0 2px rgba(246, 196, 83, 0.28);
|
box-shadow: 0 0 0 2px rgba(246, 196, 83, 0.28);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.shot-label,
|
||||||
|
.shot-label-input {
|
||||||
|
min-width: 0;
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shot-label {
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font-weight: 700;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shot-label-input {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 36px;
|
||||||
|
padding: 0 10px;
|
||||||
|
border: 1px solid rgba(138, 150, 197, 0.2);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background: rgba(8, 13, 30, 0.62);
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shot-label-input::placeholder {
|
||||||
|
color: rgba(185, 194, 215, 0.48);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shot-label-input:focus {
|
||||||
|
border-color: rgba(246, 196, 83, 0.72);
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 0 3px rgba(246, 196, 83, 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
.shots img {
|
.shots img {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
@ -1744,7 +1780,6 @@ textarea:focus {
|
||||||
.shot-actions {
|
.shot-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--space-2);
|
gap: var(--space-2);
|
||||||
margin-top: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.shot-actions .shot-annotate-button,
|
.shot-actions .shot-annotate-button,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue