From a9b5ca423a001baaeb5a0b0d1a80bfa1064fc338 Mon Sep 17 00:00:00 2001 From: Shinuwa Date: Sun, 9 Aug 2026 13:34:08 +0200 Subject: [PATCH] Refactor tool catalog into documentation layout --- DESIGN_SYSTEM.md | 21 +- tests/helpers/data-validation.mjs | 53 + website/public/data/site.json | 1076 +++++++++++++++++ website/public/static/icons/book.svg | 2 + website/src/components/Shell.jsx | 5 +- .../src/features/toolboxes/ToolboxPages.jsx | 2 +- website/src/pages/AboutPage.jsx | 3 +- website/src/pages/LibraryPage.jsx | 407 +++++-- website/src/styles/_home.scss | 439 ++++++- website/src/styles/_icons.scss | 5 + website/src/styles/_responsive.scss | 13 +- website/src/styles/_shell.scss | 13 +- 12 files changed, 1925 insertions(+), 114 deletions(-) create mode 100644 website/public/static/icons/book.svg diff --git a/DESIGN_SYSTEM.md b/DESIGN_SYSTEM.md index cdcaf58..f8c3d66 100644 --- a/DESIGN_SYSTEM.md +++ b/DESIGN_SYSTEM.md @@ -267,7 +267,26 @@ Style courant : - pas de hover sur les panneaux de lecture ; - implementation via le mixin `gold-panel-frame` dans `website/src/styles/_mixins.scss`. -## 10. Boutons +## 10. Catalogue Documentation + +La page catalogue des outils utilise un layout de documentation. + +Regles : + +- hero court, puis contenu principal en deux colonnes sur desktop ; +- sommaire a gauche en panneau sticky, avec categories puis liens d'outils de la categorie active ; +- une seule categorie d'usage est affichee a droite a la fois ; +- la categorie active commence par un mini-hero descriptif, sans compteur decoratif ; +- chaque fiche commence par le titre, l'icone, le badge de categorie et une description directe de l'outil ; +- l'exemple interactif reste visible en permanence et centre horizontalement ; +- les reperes de boutons propres a l'outil sont places a gauche de l'exemple, puis empiles au-dessus sur mobile ; +- les reperes sont groupes par categorie de controles de l'outil, avec apercu visuel du bouton et label court ; +- les descriptions de reperes sont reservees aux comportements non evidents, par exemple un bouton qui agit sur toute la ligne ou une action de drag & drop ; +- les fonctionnalites detaillees et formats d'import texte sont presentes sous l'exemple dans un panneau repliable pour ne pas alourdir la lecture avant manipulation ; +- les controles propres au catalogue, comme la reinitialisation d'une demo, ne sont pas documentes comme fonctionnalites de l'outil ; +- sur mobile, le sommaire devient un bloc normal place avant les fiches, sans position sticky. + +## 11. Boutons ### Primary diff --git a/tests/helpers/data-validation.mjs b/tests/helpers/data-validation.mjs index a039c9b..ed24898 100644 --- a/tests/helpers/data-validation.mjs +++ b/tests/helpers/data-validation.mjs @@ -53,8 +53,22 @@ export function validateSiteContent(site) { "library.summaryLabel", "library.tocLabel", "library.tocTitle", + "library.docTocTitle", "library.tocDescription", "library.toolsLabel", + "library.categoryLabel", + "library.featureHeading", + "library.controlLegendHeading", + "library.categories.notesTracking", + "library.categories.references", + "library.categories.calculationData", + "library.categories.timeRoutines", + "library.categories.buildsCommands", + "library.categoryDescriptions.notesTracking", + "library.categoryDescriptions.references", + "library.categoryDescriptions.calculationData", + "library.categoryDescriptions.timeRoutines", + "library.categoryDescriptions.buildsCommands", "home.hero.eyebrow", "home.hero.title", "home.hero.description", @@ -442,6 +456,45 @@ export function validateSiteContent(site) { assert.equal(typeof item, "object", `library.summaryItems[${index}] must be an object`); }); + const libraryToolDocs = valueAt(site, "library.toolDocs"); + ["notepad", "checklist", "counters", "images", "imageAnnotation", "links", "calculator", "table", "timer", "taskPlanner", "combos", "equipmentPlanner"].forEach((toolType) => { + const docs = libraryToolDocs?.[toolType]; + assert.equal(typeof docs, "object", `library.toolDocs.${toolType} must be an object`); + assert.ok(Array.isArray(docs.controls), `library.toolDocs.${toolType}.controls must be an array`); + assert.ok(docs.controls.length >= 1, `library.toolDocs.${toolType}.controls must not be empty`); + docs.controls.forEach((control, index) => { + assert.equal(typeof control, "object", `library.toolDocs.${toolType}.controls[${index}] must be an object`); + assertNonEmptyString({ control }, "control.group"); + assertNonEmptyString({ control }, "control.icon"); + assertNonEmptyString({ control }, "control.title"); + if (control.text) assertNonEmptyString({ control }, "control.text"); + if (control.preview) { + assert.equal(typeof control.preview, "object", `library.toolDocs.${toolType}.controls[${index}].preview must be an object`); + assertNonEmptyString({ preview: control.preview }, "preview.kind"); + } + }); + assert.ok(Array.isArray(docs.features), `library.toolDocs.${toolType}.features must be an array`); + assert.ok(docs.features.length >= 1, `library.toolDocs.${toolType}.features must not be empty`); + docs.features.forEach((feature, index) => { + assertNonEmptyString({ feature }, "feature.title"); + assertNonEmptyString({ feature }, "feature.text"); + assert.equal(typeof feature, "object", `library.toolDocs.${toolType}.features[${index}] must be an object`); + if (feature.details) { + assert.ok(Array.isArray(feature.details), `library.toolDocs.${toolType}.features[${index}].details must be an array`); + assert.ok(feature.details.length >= 1, `library.toolDocs.${toolType}.features[${index}].details must not be empty`); + feature.details.forEach((detail, detailIndex) => { + assert.equal(typeof detail, "string", `library.toolDocs.${toolType}.features[${index}].details[${detailIndex}] must be a string`); + assert.ok(detail.trim(), `library.toolDocs.${toolType}.features[${index}].details[${detailIndex}] must not be empty`); + }); + } + }); + if (docs.importFormat) { + assertNonEmptyString({ importFormat: docs.importFormat }, "importFormat.title"); + assertNonEmptyString({ importFormat: docs.importFormat }, "importFormat.text"); + assertNonEmptyString({ importFormat: docs.importFormat }, "importFormat.example"); + } + }); + const aboutLimits = valueAt(site, "about.limits"); assert.ok(Array.isArray(aboutLimits), "about.limits must be an array"); assert.ok(aboutLimits.length >= 3, "about.limits must contain at least 3 items"); diff --git a/website/public/data/site.json b/website/public/data/site.json index 7c69651..a104738 100644 --- a/website/public/data/site.json +++ b/website/public/data/site.json @@ -36,8 +36,1079 @@ "summaryLabel": "Repères de la librairie", "tocLabel": "Sommaire des outils", "tocTitle": "Sommaire", + "docTocTitle": "Catalogue des outils", "tocDescription": "Accédez directement à un exemple d’outil dans la page.", "toolsLabel": "Exemples d'outils disponibles", + "categoryLabel": "Usage", + "featureHeading": "Fonctionnalités détaillées", + "controlLegendHeading": "Repères", + "categories": { + "notesTracking": "Notes & suivi", + "references": "Références", + "calculationData": "Calcul & données", + "timeRoutines": "Temps & routines", + "buildsCommands": "Builds & commandes" + }, + "categoryDescriptions": { + "notesTracking": "Les outils pour noter rapidement une information, suivre des objectifs et garder un état de session lisible.", + "references": "Les outils pour centraliser cartes, captures, images annotées et liens externes utiles pendant une partie.", + "calculationData": "Les outils pour manipuler des valeurs, comparer des données et structurer des informations en tableau.", + "timeRoutines": "Les outils pour gérer les temps de session, les resets, les tâches récurrentes et les routines quotidiennes ou hebdomadaires.", + "buildsCommands": "Les outils pour documenter des builds, organiser des équipements et conserver des séquences de commandes." + }, + "toolDocs": { + "notepad": { + "controls": [ + { + "group": "Édition de texte", + "icon": "edit", + "title": "Styles de texte", + "preview": { + "kind": "textFormats" + } + }, + { + "group": "Édition de texte", + "icon": "edit", + "title": "Titre", + "text": "S’applique a toute la ligne courante.", + "preview": { + "kind": "textButton", + "label": "T" + } + }, + { + "group": "Édition de texte", + "icon": "dropdown", + "title": "Liste à puces / Liste numérotée", + "preview": { + "kind": "listDropdown" + } + }, + { + "group": "Édition de texte", + "icon": "dropdown", + "title": "Couleur / surlignage", + "preview": { + "kind": "color" + } + }, + { + "group": "Dessin", + "icon": "edit", + "title": "Dessiner", + "preview": { + "kind": "iconButton", + "icon": "edit" + } + }, + { + "group": "Dessin", + "icon": "rubber", + "title": "Gommer", + "preview": { + "kind": "iconButton", + "icon": "rubber" + } + }, + { + "group": "Dessin", + "icon": "dropdown", + "title": "Couleur du trait", + "preview": { + "kind": "color" + } + }, + { + "group": "Dessin", + "icon": "dropdown", + "title": "Épaisseur du trait", + "preview": { + "kind": "drawingWidth", + "width": 4 + } + }, + { + "group": "Dessin", + "icon": "back", + "title": "Annuler le dernier trait", + "preview": { + "kind": "iconButton", + "icon": "back" + } + }, + { + "group": "Dessin", + "icon": "trash", + "title": "Effacer les dessins", + "preview": { + "kind": "iconButton", + "icon": "trash", + "danger": true + } + }, + { + "group": "Dessin", + "icon": "save", + "title": "Sauvegarde du dessin", + "preview": { + "kind": "switch", + "icon": "save", + "active": true + } + } + ], + "features": [ + { + "title": "Mise en page de texte", + "text": "Rédaction riche avec gras, italique, souligné, barré, titres, listes à puces, listes numérotées, couleurs de texte et surlignage.", + "details": [ + "Les boutons de mise en forme agissent sur le texte sélectionné ou sur le mot en cours de saisie.", + "Le menu listes regroupe les listes simples et numérotées pour éviter d'avoir deux boutons permanents.", + "Les couleurs de texte et de surlignage sont séparées pour distinguer une information importante d'un simple repère visuel." + ] + }, + { + "title": "Dessin manuel", + "text": "Ajout de traits au-dessus de la note, avec couleur, épaisseur, gomme, annulation et mode temporaire ou permanent.", + "details": [ + "Le mode dessin place un calque transparent au-dessus de la note sans remplacer le texte.", + "La gomme retire les traits dessinés, tandis que l'annulation retire le dernier tracé.", + "Le mode temporaire sert aux annotations de session; le mode permanent sauvegarde les traits avec la note." + ] + }, + { + "title": "Sauvegarde locale", + "text": "Le contenu est nettoyé puis gardé dans le navigateur, avec une date de modification visible dans l'outil.", + "details": [ + "La note conserve le texte riche autorisé, pas du HTML libre non contrôlé.", + "La date de dernière modification permet de vérifier si une note a bien été mise à jour.", + "Les données restent dans la toolbox locale et ne nécessitent pas de compte." + ] + } + ] + }, + "checklist": { + "controls": [ + { + "group": "Contrôles", + "icon": "settings", + "title": "Décrémenter / Incrémenter", + "preview": { + "kind": "buttonGroup", + "buttons": [ + { + "label": "-" + }, + { + "label": "+" + } + ] + } + }, + { + "group": "Contrôles", + "icon": "eye-open", + "title": "Visible quand terminée", + "preview": { + "kind": "iconButton", + "icon": "eye-open" + } + }, + { + "group": "Contrôles", + "icon": "hide", + "title": "Réduit quand terminée", + "preview": { + "kind": "iconButton", + "icon": "hide" + } + }, + { + "group": "Contrôles", + "icon": "eye-closed", + "title": "Masqué quand terminée", + "preview": { + "kind": "iconButton", + "icon": "eye-closed" + } + }, + { + "group": "Contrôles", + "icon": "drag", + "title": "Déplacer", + "preview": { + "kind": "iconButton", + "icon": "drag" + } + }, + { + "group": "Contrôles", + "icon": "edit", + "title": "Modifier", + "preview": { + "kind": "iconButton", + "icon": "edit" + } + }, + { + "group": "Contrôles", + "icon": "trash", + "title": "Supprimer", + "preview": { + "kind": "iconButton", + "icon": "trash", + "danger": true + } + } + ], + "features": [ + { + "title": "Objectifs et quantités", + "text": "Chaque item possède une quantité cible et une quantité actuelle. Les boutons + et - font progresser l'item jusqu'à validation complète.", + "details": [ + "Un item sans quantité spéciale utilise 1 comme objectif.", + "Un item de type Potion:10 demande dix validations avant d'être terminé.", + "La case reflète la progression réelle : cocher complète l'objectif, les boutons + et - ajustent le nombre atteint." + ] + }, + { + "title": "Catégories terminées", + "text": "Une catégorie est considérée comme terminée quand tous ses items atteignent leur quantité cible. Elle peut rester visible, passer en mode réduit ou être masquée.", + "details": [ + "Visible garde tous les items affichés, même terminés.", + "Réduit conserve le titre de catégorie pour garder un repère sans occuper toute la hauteur.", + "Caché retire les catégories terminées de la vue courante pour se concentrer sur ce qui reste à faire." + ] + }, + { + "title": "Réorganisation", + "text": "Déplacement des catégories et des items par drag & drop pour garder la liste dans l'ordre voulu.", + "details": [ + "Une catégorie peut être déplacée avant ou après une autre catégorie.", + "Un item peut être déplacé dans la même catégorie ou envoyé dans une autre catégorie.", + "Les zones de dépôt apparaissent pendant le déplacement pour montrer où l'item sera placé." + ] + } + ], + "importFormat": { + "title": "Import texte", + "text": "Une ligne commençant par # crée une catégorie. Une ligne item peut contenir une quantité avec le format Nom:quantité.", + "example": "# Potions\nPotion:10\nMéga potion:5\n\n# Armures\nCasque Rey Dau:1" + } + }, + "counters": { + "controls": [ + { + "group": "Contrôles", + "icon": "settings", + "title": "Décrémenter / Incrémenter", + "preview": { + "kind": "buttonGroup", + "buttons": [ + { + "label": "-" + }, + { + "label": "+" + } + ] + } + }, + { + "group": "Contrôles", + "icon": "rubber", + "title": "Remettre à zéro", + "preview": { + "kind": "iconButton", + "icon": "rubber" + } + }, + { + "group": "Contrôles", + "icon": "trash", + "title": "Supprimer", + "preview": { + "kind": "iconButton", + "icon": "trash", + "danger": true + } + } + ], + "features": [ + { + "title": "Compteurs rapides", + "text": "Ajout de compteurs nommés pour suivre essais, ressources, scores, victoires ou stocks.", + "details": [ + "Chaque compteur a son propre nom et sa propre valeur.", + "Les compteurs sont utiles pour suivre des essais de drop, des morts, des ressources ou des manches.", + "L'outil reste volontairement simple : pas de catégorie, pas de configuration obligatoire." + ] + }, + { + "title": "Actions immédiates", + "text": "Chaque compteur peut être incrémenté, décrémenté, réinitialisé ou supprimé sans configuration avancée.", + "details": [ + "Le bouton + ajoute une unité au compteur.", + "Le bouton - retire une unité si une valeur a été ajoutée par erreur.", + "Réinitialiser remet la valeur à zéro sans supprimer le compteur; supprimer retire le compteur entier." + ] + } + ] + }, + "images": { + "controls": [ + { + "group": "Contrôles", + "icon": "zoom", + "title": "Agrandir", + "preview": { + "kind": "iconButton", + "icon": "zoom" + } + }, + { + "group": "Contrôles", + "icon": "map", + "title": "Envoyer vers annotation d’image", + "preview": { + "kind": "iconButton", + "icon": "map" + } + }, + { + "group": "Contrôles", + "icon": "trash", + "title": "Supprimer", + "preview": { + "kind": "iconButton", + "icon": "trash", + "danger": true + } + } + ], + "features": [ + { + "title": "Galerie locale", + "text": "Ajout de captures, cartes ou références visuelles depuis un fichier ou par collage.", + "details": [ + "Le bouton d'ajout accepte des fichiers image depuis l'appareil.", + "La zone de collage permet d'ajouter rapidement une capture copiée depuis le presse-papiers.", + "Plusieurs images peuvent cohabiter dans la même galerie de toolbox." + ] + }, + { + "title": "Prévisualisation", + "text": "Chaque image peut être ouverte en grand et renommée pour retrouver rapidement la bonne référence.", + "details": [ + "Le bouton d'aperçu ouvre l'image sans sortir de l'application.", + "Le libellé peut être modifié pour remplacer un nom de fichier peu lisible.", + "La suppression retire seulement l'image concernée de l'outil courant." + ] + }, + { + "title": "Annotation rapide", + "text": "Une image peut être envoyée vers l'outil d'annotation pour ajouter marqueurs et dessins.", + "details": [ + "Annoter copie l'image vers l'outil Annotation d'images de la même toolbox.", + "L'image devient alors le support de marqueurs, libellés et dessins.", + "La galerie garde l'image originale pour conserver la référence source." + ] + } + ] + }, + "imageAnnotation": { + "controls": [ + { + "group": "Image et marqueurs", + "icon": "zoom", + "title": "Plein écran", + "preview": { + "kind": "iconButton", + "icon": "zoom" + } + }, + { + "group": "Image et marqueurs", + "icon": "trash", + "title": "Supprimer", + "preview": { + "kind": "iconButton", + "icon": "trash", + "danger": true + } + }, + { + "group": "Dessin", + "icon": "edit", + "title": "Dessiner", + "preview": { + "kind": "iconButton", + "icon": "edit" + } + }, + { + "group": "Dessin", + "icon": "rubber", + "title": "Gommer", + "preview": { + "kind": "iconButton", + "icon": "rubber" + } + }, + { + "group": "Dessin", + "icon": "dropdown", + "title": "Couleur du trait", + "preview": { + "kind": "color" + } + }, + { + "group": "Dessin", + "icon": "dropdown", + "title": "Épaisseur du trait", + "preview": { + "kind": "drawingWidth", + "width": 4 + } + }, + { + "group": "Dessin", + "icon": "back", + "title": "Annuler le dernier trait", + "preview": { + "kind": "iconButton", + "icon": "back" + } + }, + { + "group": "Dessin", + "icon": "trash", + "title": "Effacer les dessins", + "preview": { + "kind": "iconButton", + "icon": "trash", + "danger": true + } + }, + { + "group": "Dessin", + "icon": "save", + "title": "Sauvegarde du dessin", + "preview": { + "kind": "switch", + "icon": "save", + "active": true + } + } + ], + "features": [ + { + "title": "Image unique annotable", + "text": "Préparation d'une carte ou d'un plan avec une image principale, des marqueurs et des libellés.", + "details": [ + "Une seule image sert de plan de travail pour éviter de mélanger plusieurs cartes.", + "Les marqueurs sont placés relativement à l'image et suivent son redimensionnement.", + "Chaque marqueur peut porter un libellé court pour nommer une zone, un objectif ou un repère." + ] + }, + { + "title": "Dessin plein écran", + "text": "Les traits sont ajoutés en édition plein écran et restent alignés avec l'image redimensionnée.", + "details": [ + "Le plein écran donne plus de précision pour tracer sur une carte ou une capture.", + "Les traits utilisent des coordonnées proportionnelles pour rester au bon endroit après redimensionnement.", + "La couleur et l'épaisseur permettent de distinguer plusieurs types d'information." + ] + }, + { + "title": "Annotations temporaires ou permanentes", + "text": "Les dessins peuvent rester limités à la session ou être sauvegardés dans l'outil.", + "details": [ + "Le mode temporaire sert aux notes rapides qui ne doivent pas polluer l'image à long terme.", + "Le mode permanent sauvegarde les traits dans l'outil.", + "La gomme, l'annulation et l'effacement permettent de corriger la carte pendant l'édition." + ] + } + ] + }, + "links": { + "controls": [ + { + "group": "Contrôles", + "icon": "copy", + "title": "Copier", + "preview": { + "kind": "iconButton", + "icon": "copy" + } + }, + { + "group": "Contrôles", + "icon": "trash", + "title": "Supprimer", + "preview": { + "kind": "iconButton", + "icon": "trash", + "danger": true + } + } + ], + "features": [ + { + "title": "Raccourcis de session", + "text": "Regroupement de guides, builds, cartes interactives et ressources externes avec titres lisibles.", + "details": [ + "Chaque lien associe un titre lisible et une URL.", + "Le domaine est affiché pour identifier rapidement la source.", + "L'outil sert à garder les ressources externes accessibles depuis la toolbox sans rechercher à nouveau." + ] + }, + { + "title": "Copie rapide", + "text": "Chaque URL peut être copiée directement depuis la liste.", + "details": [ + "Copier envoie l'URL complète dans le presse-papiers.", + "Supprimer retire uniquement le lien concerné.", + "L'import texte permet de préparer plusieurs liens hors de l'application puis de les coller en une seule fois." + ] + } + ], + "importFormat": { + "title": "Import texte", + "text": "Un lien par ligne. Séparez le titre et l'URL avec deux-points.", + "example": "Build:https://example.com/build?class=rogue\nMap:https://example.com/map" + } + }, + "calculator": { + "controls": [ + { + "group": "Contrôles", + "icon": "rubber", + "title": "Tout effacer", + "preview": { + "kind": "iconButton", + "icon": "rubber", + "danger": true + } + }, + { + "group": "Contrôles", + "icon": "copy", + "title": "Copie pour créer une checklist", + "preview": { + "kind": "iconButton", + "icon": "copy" + } + }, + { + "group": "Contrôles", + "icon": "edit", + "title": "Renommer", + "preview": { + "kind": "iconButton", + "icon": "edit" + } + }, + { + "group": "Contrôles", + "icon": "trash", + "title": "Supprimer", + "preview": { + "kind": "iconButton", + "icon": "trash", + "danger": true + } + } + ], + "features": [ + { + "title": "Calculs enregistrés", + "text": "Saisie d'une expression, calcul du résultat et sauvegarde avec un libellé.", + "details": [ + "Le champ expression accepte les opérations de base et calcule le résultat avant sauvegarde.", + "Le libellé sert à nommer un coût, une ressource, un total ou une conversion.", + "Un résultat enregistré peut être renommé ou supprimé." + ] + }, + { + "title": "Arborescence", + "text": "Un résultat peut servir de base à des calculs enfants pour décomposer coûts, crafts ou conversions.", + "details": [ + "Utiliser comme base crée un lien parent/enfant entre le résultat choisi et les calculs suivants.", + "La racine revient au niveau principal pour recommencer une autre branche.", + "Ce fonctionnement aide à détailler un craft ou un total en plusieurs sous-calculs." + ] + }, + { + "title": "Copie checklist", + "text": "Les résultats peuvent être copiés dans un format exploitable par l'import texte de checklist.", + "details": [ + "La copie génère un texte de type Nom:quantité.", + "Ce texte peut être collé directement dans l'import de l'outil Checklist.", + "Le calculateur sert donc à préparer des objectifs chiffrés avant de les suivre comme tâches." + ] + } + ] + }, + "table": { + "controls": [ + { + "group": "Contrôles", + "icon": "add-row", + "title": "Ajouter / Supprimer ligne", + "preview": { + "kind": "buttonGroup", + "buttons": [ + { + "icon": "add-row" + }, + { + "icon": "remove-row", + "danger": true + } + ] + } + }, + { + "group": "Contrôles", + "icon": "add-column", + "title": "Ajouter / Supprimer colonne", + "preview": { + "kind": "buttonGroup", + "buttons": [ + { + "icon": "add-column" + }, + { + "icon": "remove-column", + "danger": true + } + ] + } + }, + { + "group": "Contrôles", + "icon": "copy", + "title": "Exporter au format TSV", + "preview": { + "kind": "iconButton", + "icon": "copy" + } + } + ], + "features": [ + { + "title": "Grille éditable", + "text": "Ajout et retrait de lignes ou colonnes, édition des en-têtes et sélection de cellules.", + "details": [ + "Les boutons ligne et colonne modifient la taille de la grille sans recréer le tableau.", + "Les en-têtes peuvent être renommés pour donner un sens aux données.", + "La cellule active sert de repère pour certaines actions, comme appliquer une formule." + ] + }, + { + "title": "Formules", + "text": "Les cellules peuvent contenir des formules avec références à d'autres cellules.", + "details": [ + "Une formule peut agréger ou transformer les valeurs d'une ligne, d'une colonne ou d'une cellule.", + "Les références de cellules permettent de construire des calculs liés à d'autres données du tableau.", + "Les erreurs de formule sont signalées dans la grille au lieu d'être ignorées silencieusement." + ] + }, + { + "title": "Export TSV", + "text": "Le tableau peut être copié en TSV pour être collé dans un tableur.", + "details": [ + "Le format TSV sépare les colonnes par tabulation et les lignes par retour à la ligne.", + "Il peut être collé dans un tableur externe en conservant la structure de grille.", + "Cette copie sert d'export rapide, sans modifier le stockage local de la toolbox." + ] + } + ] + }, + "timer": { + "controls": [ + { + "group": "Chronomètre", + "icon": "play-circle", + "title": "Démarrer / Pause / Stop", + "preview": { + "kind": "buttonGroup", + "buttons": [ + { + "icon": "play-circle" + }, + { + "icon": "pause-circle" + }, + { + "icon": "stop-circle" + } + ] + } + }, + { + "group": "Chronomètre", + "icon": "record-circle", + "title": "Étape", + "preview": { + "kind": "iconButton", + "icon": "record-circle" + } + }, + { + "group": "Chronomètre", + "icon": "rubber", + "title": "Tout effacer", + "preview": { + "kind": "iconButton", + "icon": "rubber", + "danger": true + } + }, + { + "group": "Compte à rebours", + "icon": "refresh", + "title": "Réinitialiser", + "preview": { + "kind": "iconButton", + "icon": "refresh" + } + }, + { + "group": "Compte à rebours", + "icon": "refresh", + "title": "Reset / relance automatique", + "preview": { + "kind": "switch", + "icon": "refresh", + "active": true + } + }, + { + "group": "Compte à rebours", + "icon": "sound-mute", + "title": "Alerte désactivée", + "preview": { + "kind": "iconButton", + "icon": "sound-mute" + } + }, + { + "group": "Compte à rebours", + "icon": "sound-min", + "title": "Alerte sur la page", + "preview": { + "kind": "iconButton", + "icon": "sound-min" + } + }, + { + "group": "Compte à rebours", + "icon": "sound-max", + "title": "Alerte sur le site", + "preview": { + "kind": "iconButton", + "icon": "sound-max" + } + }, + { + "group": "Compte à rebours", + "icon": "trash", + "title": "Supprimer", + "preview": { + "kind": "iconButton", + "icon": "trash", + "danger": true + } + } + ], + "features": [ + { + "title": "Chronomètre", + "text": "Suivi d'un temps avec pause, reset et étapes nommées.", + "details": [ + "Démarrer lance le chrono, pause suspend le temps, reset remet le chrono à zéro.", + "Le bouton étape enregistre un temps intermédiaire avec un libellé.", + "Les étapes permettent de comparer des runs, phases ou rotations dans une session." + ] + }, + { + "title": "Comptes à rebours", + "text": "Création de timers par durée, heure précise, pattern horaire ou intervalle.", + "details": [ + "Durée lance un timer simple, par exemple 00:30:00.", + "Heure précise cible une heure donnée de la journée.", + "Pattern horaire et Intervalle servent aux resets ou événements récurrents." + ] + }, + { + "title": "Alertes", + "text": "Les alertes peuvent rester liées à l'outil affiché ou être actives sur tout le site.", + "details": [ + "Aucune alerte garde le timer silencieux.", + "Alerte visible prévient seulement quand la toolbox concernée est ouverte.", + "Alerte globale site peut prévenir depuis une autre page de Sokko G, avec garde-fou sur les répétitions trop fréquentes." + ] + } + ] + }, + "taskPlanner": { + "controls": [ + { + "group": "Contrôles", + "icon": "eye-closed", + "title": "Masquer terminé", + "preview": { + "kind": "iconButton", + "icon": "eye-closed" + } + }, + { + "group": "Contrôles", + "icon": "settings", + "title": "Paramètres", + "preview": { + "kind": "iconButton", + "icon": "settings" + } + }, + { + "group": "Contrôles", + "icon": "drag", + "title": "Déplacer", + "preview": { + "kind": "iconButton", + "icon": "drag" + } + }, + { + "group": "Contrôles", + "icon": "edit", + "title": "Modifier", + "preview": { + "kind": "iconButton", + "icon": "edit" + } + }, + { + "group": "Contrôles", + "icon": "trash", + "title": "Supprimer", + "preview": { + "kind": "iconButton", + "icon": "trash", + "danger": true + } + } + ], + "features": [ + { + "title": "Tâches de routine", + "text": "Gestion de tâches ponctuelles, quotidiennes ou hebdomadaires avec resets configurables.", + "details": [ + "Une tâche unique se coche une fois et peut être nettoyée manuellement.", + "Une tâche quotidienne se réactive selon l'heure de reset configurée.", + "Une tâche hebdomadaire se réactive selon le jour et l'heure choisis." + ] + }, + { + "title": "Sous-tâches et prérequis", + "text": "Une tâche peut dépendre d'un parent et signaler les prérequis non effectués.", + "details": [ + "Le lien parent/enfant permet de regrouper une tâche sous une autre.", + "Les prérequis indiquent qu'une tâche ne devrait pas être faite avant une autre.", + "Les avertissements de prérequis restent visibles pour éviter de valider une routine dans le mauvais ordre." + ] + }, + { + "title": "Catégories", + "text": "Les tâches se regroupent par catégories réorganisables pour suivre une routine de session.", + "details": [ + "Les catégories peuvent être renommées et déplacées.", + "Une tâche peut changer de catégorie pendant la réorganisation.", + "Masquer les tâches terminées réduit la vue aux objectifs restants." + ] + } + ] + }, + "combos": { + "controls": [ + { + "group": "Contrôles", + "icon": "settings", + "title": "Successif / Simultané", + "preview": { + "kind": "buttonGroup", + "buttons": [ + { + "label": "A › B" + }, + { + "label": "A + B" + } + ] + } + }, + { + "group": "Contrôles", + "icon": "add", + "title": "Ajouter input", + "preview": { + "kind": "iconButton", + "icon": "add" + } + }, + { + "group": "Contrôles", + "icon": "clock", + "title": "Hold", + "preview": { + "kind": "iconButton", + "icon": "clock" + } + }, + { + "group": "Contrôles", + "icon": "back", + "title": "Retirer touche / étape", + "preview": { + "kind": "buttonGroup", + "buttons": [ + { + "icon": "back" + }, + { + "icon": "trash", + "danger": true + } + ] + } + }, + { + "group": "Contrôles", + "icon": "edit", + "title": "Modifier", + "text": "Ouvre la fiche d'une pièce pour ajuster stats, craft, source et sockets.", + "preview": { + "kind": "iconButton", + "icon": "edit" + } + }, + { + "group": "Contrôles", + "icon": "trash", + "title": "Supprimer", + "preview": { + "kind": "iconButton", + "icon": "trash", + "danger": true + } + } + ], + "features": [ + { + "title": "Séquences de touches", + "text": "Un combo est une suite d'étapes. Une étape peut contenir une seule touche ou plusieurs touches simultanées, affichées ensemble avec un +.", + "details": [ + "Cliquer sur une touche de la palette ajoute une nouvelle étape à la fin de la séquence.", + "En mode successif, A puis B crée deux étapes séparées : A, puis B.", + "En mode simultané, A puis B crée une seule étape groupée : A + B." + ] + }, + { + "title": "Création successive ou simultanée", + "text": "Le mode par défaut ajoute les inputs les uns après les autres. Le mode simultané ajoute les prochains inputs dans la même étape; le drag & drop permet aussi de fusionner deux étapes déjà créées.", + "details": [ + "Le switch A + B sert à décider si les prochaines touches doivent entrer dans l'étape active.", + "Déposer une touche sur une étape existante transforme cette étape en combinaison simultanée.", + "Ce geste permet de corriger une séquence après coup sans supprimer puis recréer tout le combo." + ] + }, + { + "title": "Catégories de combos", + "text": "Les combos peuvent être regroupés, édités, déplacés et supprimés par catégorie.", + "details": [ + "Le champ catégorie place automatiquement le combo dans le bon groupe.", + "Les catégories se replient pour masquer des groupes déjà connus.", + "Les poignées de déplacement servent à réordonner les catégories ou les combos dans une catégorie." + ] + }, + { + "title": "Périphériques et touches personnalisées", + "text": "La palette change selon la manette ou le clavier. Une touche personnalisée peut être ajoutée quand la commande du jeu n'existe pas dans la palette.", + "details": [ + "Les palettes PlayStation, Xbox, Switch et Nintendo 64 affichent les boutons propres à chaque manette.", + "Le mode clavier/souris propose les touches courantes, la souris et une bascule AZERTY/QWERTY.", + "Le champ touche personnalisée accepte une commande courte pour documenter une touche absente de la palette." + ] + } + ] + }, + "equipmentPlanner": { + "controls": [ + { + "group": "Contrôles", + "icon": "edit", + "title": "Modifier", + "preview": { + "kind": "iconButton", + "icon": "edit" + } + }, + { + "group": "Contrôles", + "icon": "drag", + "title": "Déplacer", + "preview": { + "kind": "iconButton", + "icon": "drag" + } + }, + { + "group": "Contrôles", + "icon": "trash", + "title": "Supprimer", + "preview": { + "kind": "iconButton", + "icon": "trash", + "danger": true + } + } + ], + "features": [ + { + "title": "Build d'équipement", + "text": "Organisation d'équipements par type avec icônes, état actif et détails d'obtention.", + "details": [ + "Chaque équipement appartient à un type, par exemple arme, casque ou anneau.", + "L'état actif décide si la pièce compte dans le résumé du build.", + "Les détails d'obtention servent à noter où récupérer ou fabriquer l'équipement." + ] + }, + { + "title": "Stats et matériaux", + "text": "Suivi des caractéristiques, bonus, matériaux de craft et totaux agrégés.", + "details": [ + "Les statistiques numériques sont additionnées dans le résumé.", + "Les bonus textuels restent listés à part pour ne pas perdre les effets non chiffrés.", + "Les matériaux de craft permettent de préparer une liste de ressources à récupérer." + ] + }, + { + "title": "Sertissages", + "text": "Ajout d'objets sertis, bonus associés et liens entre sockets.", + "details": [ + "Les sockets décrivent les emplacements disponibles sur une pièce.", + "Un objet serti peut ajouter des stats ou des bonus propres.", + "Les totaux de sockets aident à vérifier rapidement ce qui est déjà prévu dans le build." + ] + } + ] + } + }, "summaryItems": [ { "icon": "toolbox", @@ -201,6 +1272,11 @@ "icon": "tasklist", "name": "Planificateur de tâches", "description": "Organisez des tâches ponctuelles, quotidiennes ou hebdomadaires avec catégories, sous-tâches et dépendances parent/enfant." + }, + { + "icon": "chest-armor", + "name": "Planificateur d'équipements", + "description": "Planifiez un build par types d'équipement, avec pièces actives, statistiques, matériaux de craft, bonus et sertissages." } ], "limitsTitle": "À retenir", diff --git a/website/public/static/icons/book.svg b/website/public/static/icons/book.svg new file mode 100644 index 0000000..1140045 --- /dev/null +++ b/website/public/static/icons/book.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/website/src/components/Shell.jsx b/website/src/components/Shell.jsx index 1f557d6..dd80741 100644 --- a/website/src/components/Shell.jsx +++ b/website/src/components/Shell.jsx @@ -22,7 +22,10 @@ export function Shell({ route, content, games, toolboxes, links, actions, childr - {content.navigation.about} +
+ {content.navigation.about} + {content.library.eyebrow} +
{content.sidebar.badge}

{content.sidebar.note}

diff --git a/website/src/features/toolboxes/ToolboxPages.jsx b/website/src/features/toolboxes/ToolboxPages.jsx index ec76aa0..406c619 100644 --- a/website/src/features/toolboxes/ToolboxPages.jsx +++ b/website/src/features/toolboxes/ToolboxPages.jsx @@ -72,7 +72,7 @@ export function ToolboxesPage({ siteContent, toolboxes, getToolboxGame, actions,

{content.eyebrow}

{content.title}

{content.summary}

- {content.libraryLink} + {content.libraryLink}
diff --git a/website/src/pages/AboutPage.jsx b/website/src/pages/AboutPage.jsx index 1397047..04e1b6c 100644 --- a/website/src/pages/AboutPage.jsx +++ b/website/src/pages/AboutPage.jsx @@ -1,5 +1,6 @@ // Rôle : présente le projet, son fonctionnement local et les repères de contribution. import React, { useEffect } from "react"; +import { Icon } from "../components/Icon.jsx"; import { RichText } from "../components/RichText.jsx"; export function AboutPage({ siteContent }) { @@ -65,7 +66,7 @@ export function AboutPage({ siteContent }) {

Toolbox

{content.toolsTitle}

- {content.toolsLibraryLink} + {content.toolsLibraryLink}
{content.tools.map((tool) => ( diff --git a/website/src/pages/LibraryPage.jsx b/website/src/pages/LibraryPage.jsx index 221c3f9..4e45269 100644 --- a/website/src/pages/LibraryPage.jsx +++ b/website/src/pages/LibraryPage.jsx @@ -49,6 +49,14 @@ const MODULE_COMPONENTS = { imageAnnotation: { label: "Annotation d'images", mode: "Image + marqueurs", icon: "map", Component: ImageAnnotationModule, editable: true } }; +const LIBRARY_CATEGORY_DEFINITIONS = [ + { key: "notesTracking", types: ["notepad", "checklist", "counters"] }, + { key: "references", types: ["images", "imageAnnotation", "links"] }, + { key: "calculationData", types: ["calculator", "table"] }, + { key: "timeRoutines", types: ["timer", "taskPlanner"] }, + { key: "buildsCommands", types: ["combos", "equipmentPlanner"] } +]; + function cloneData(value) { if (value === undefined) return {}; if (typeof structuredClone === "function") return structuredClone(value); @@ -75,6 +83,10 @@ function getLibraryAnchorId(module) { return `library-tool-${String(module?.id || module?.type || "outil").replace(/[^a-z0-9_-]+/gi, "-").toLowerCase()}`; } +function getLibraryCategoryAnchorId(category) { + return `library-category-${category.key}`; +} + function getCurrentLibraryAnchor() { const match = location.hash.match(/^#\/library#(.+)$/); return match ? decodeURIComponent(match[1]) : ""; @@ -88,16 +100,36 @@ function scrollToLibraryTool(anchorId, replace = false) { window.scrollTo({ top: Math.max(0, top), behavior: "smooth" }); } +function findCategoryForAnchor(categories, anchorId) { + return categories.find((category) => ( + getLibraryCategoryAnchorId(category) === anchorId || + category.modules.some((module) => getLibraryAnchorId(module) === anchorId) + )) || null; +} + function getToolDescription(siteContent, definition) { const tools = siteContent.about?.tools || []; return tools.find((tool) => tool.name === definition.label)?.description || ""; } +function getLibraryCategories(modules, content) { + const modulesByType = new Map((modules || []).map((module) => [module.type, module])); + return LIBRARY_CATEGORY_DEFINITIONS.map((category) => ({ + ...category, + title: content.categories?.[category.key] || category.key, + description: content.categoryDescriptions?.[category.key] || "", + modules: category.types.map((type) => modulesByType.get(type)).filter(Boolean) + })).filter((category) => category.modules.length); +} + export function LibraryPage({ siteContent, actions }) { const content = siteContent.library; const [libraryPayload, setLibraryPayload] = useState(null); const [libraryError, setLibraryError] = useState(""); const [moduleData, setModuleData] = useState({}); + const [activeCategoryKey, setActiveCategoryKey] = useState(""); + const libraryCategories = useMemo(() => getLibraryCategories(libraryPayload?.toolbox?.modules || [], content), [content, libraryPayload]); + const activeCategory = libraryCategories.find((category) => category.key === activeCategoryKey) || libraryCategories[0] || null; useEffect(() => { let cancelled = false; @@ -118,11 +150,13 @@ export function LibraryPage({ siteContent, actions }) { }, []); useEffect(() => { - if (!libraryPayload) return; + if (!libraryPayload || !libraryCategories.length) return; const anchorId = getCurrentLibraryAnchor(); + const category = findCategoryForAnchor(libraryCategories, anchorId) || libraryCategories[0]; + setActiveCategoryKey(category.key); if (!anchorId) return; requestAnimationFrame(() => scrollToLibraryTool(anchorId)); - }, [libraryPayload]); + }, [libraryPayload, libraryCategories]); const moduleContext = useMemo(() => ({ getModuleData: (toolboxId, moduleId, fallback) => moduleData[`${toolboxId}:${moduleId}`] || fallback, @@ -203,52 +237,94 @@ export function LibraryPage({ siteContent, actions }) { {!libraryError && !libraryPayload &&

Chargement des exemples...

} {libraryPayload && ( <> - -
- {libraryPayload.toolbox.modules.map((module) => ( - - ))} -
+
+ { + setActiveCategoryKey(category.key); + requestAnimationFrame(() => scrollToLibraryTool(anchorId, true)); + }} + /> +
+ {activeCategory && ( +
+
+
+

{content.categoryLabel || "Catégorie"}

+

{activeCategory.title}

+ {activeCategory.description &&

{activeCategory.description}

} +
+
+ {activeCategory.modules.map((module) => ( + + ))} +
+ )} +
+
)}
); } -function LibraryToolExample({ module, sourceData, description, context }) { +function LibraryDocNav({ content, categories, activeCategoryKey, onSelectCategory }) { + return ( + + ); +} + +function LibraryToolExample({ module, sourceData, description, categoryTitle, docs, featureHeading, controlLegendHeading, context }) { const definition = MODULE_COMPONENTS[module.type] || MODULE_COMPONENTS.notepad; const Component = definition.Component; const [scrollable, setScrollable] = useState(false); @@ -259,55 +335,220 @@ function LibraryToolExample({ module, sourceData, description, context }) {
-

{title}

- {description &&

{description}

} -
-
-
-
-
+
+ {categoryTitle} + {description &&

{description}

} +
+ +
+ +
+

{definition.label}

+

{definition.mode}

-
-
- {definition.scrollable && ( -