add label to pictures
All checks were successful
Deploy Sokko G / deploy (push) Successful in 6s

This commit is contained in:
Shinuwa 2026-07-25 13:39:35 +02:00
parent ef5f977f8a
commit d7871736c8
10 changed files with 566 additions and 8 deletions

87
AGENTS.md Normal file
View 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
View 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
View 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`.

View file

@ -106,6 +106,8 @@ export function validateSiteContent(site) {
"toolboxes.modules.screenshots.pastePlaceholder",
"toolboxes.modules.screenshots.pasteAriaLabel",
"toolboxes.modules.screenshots.imageAlt",
"toolboxes.modules.screenshots.labelPlaceholder",
"toolboxes.modules.screenshots.labelAriaLabel",
"toolboxes.modules.screenshots.previewAriaLabel",
"toolboxes.modules.screenshots.annotateAriaLabel",
"toolboxes.modules.screenshots.annotateTitle",

View file

@ -10,6 +10,19 @@ test("vite entrypoint loads the react application", async () => {
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 () => {
const source = await readFile("website/src/main.jsx", "utf8");
const styles = await readFile("website/src/styles/main.scss", "utf8");

View file

@ -224,6 +224,8 @@
"pastePlaceholder": "Coller une image ici",
"pasteAriaLabel": "Coller une image depuis le presse-papiers",
"imageAlt": "Image",
"labelPlaceholder": "Libellé de l'image",
"labelAriaLabel": "Libellé de l'image",
"previewAriaLabel": "Agrandir l'image",
"annotateAriaLabel": "Annoter l'image",
"annotateTitle": "Annoter",

View file

@ -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 (
<>
{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"}>
<img src={shot.dataUrl} alt={textContent.imageAlt || "Image"} />
</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">
<button
className="shot-annotate-button"

View file

@ -241,6 +241,11 @@ function normalizeCalculatorValue(value) {
return Number.isFinite(parsed) ? parsed : 0;
}
function labelFromFileName(name) {
if (!name || name === "image.png") return "";
return name.replace(/\.[^.]+$/, "").trim();
}
function normalizeCalculatorData(data) {
const entries = (data?.entries || [])
.map((entry) => ({
@ -313,7 +318,11 @@ function compactModuleDataForStorage(type, value) {
if (type === "screenshots") {
const shots = (Array.isArray(value?.shots) ? value.shots : [])
.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;
}
if (type === "imageAnnotation") {
@ -798,7 +807,10 @@ function App() {
if (!imageFiles.length) return false;
const data = store.getModuleData(toolboxId, moduleId, { shots: [] });
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);
}
@ -1812,10 +1824,13 @@ function ScreenshotViewer({ shot, onClose }) {
<div className="screenshot-viewer-backdrop" onClick={onClose} />
<section ref={viewerRef} className="screenshot-viewer" role="dialog" aria-modal="true" aria-label="Screenshot">
<header>
{shot.label && <strong className="screenshot-viewer-title">{shot.label}</strong>}
<div className="screenshot-viewer-actions">
{!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>
</div>
</header>
<div className={`screenshot-viewer-body ${canAnnotate ? "has-annotation-side" : ""}`}>
<div

View file

@ -31,8 +31,22 @@
}
.screenshot-viewer header {
display: flex;
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
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;
gap: var(--space-2);
}

View file

@ -1710,6 +1710,8 @@ textarea:focus {
.shots figure {
position: relative;
display: grid;
gap: 8px;
margin: 0;
padding: 8px;
border: 1px solid var(--color-border);
@ -1732,6 +1734,40 @@ textarea:focus {
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 {
display: block;
width: 100%;
@ -1744,7 +1780,6 @@ textarea:focus {
.shot-actions {
display: flex;
gap: var(--space-2);
margin-top: 8px;
}
.shot-actions .shot-annotate-button,