Add goal tree toolbox module
All checks were successful
Deploy Sokko G / deploy (push) Successful in 15s
|
|
@ -212,7 +212,45 @@ Regles UI :
|
|||
- en vue mensuelle, les evenements multi-jours s'affichent comme une barre horizontale et ne sont pas dupliques dans chaque cellule ;
|
||||
- les vues hebdomadaire et mensuelle affichent un indicateur discret du moment actuel.
|
||||
- en vue hebdomadaire, un switch standard peut masquer les longues plages horaires vides ; la tranche masquée reste indiquée dans la grille, pas dans un texte qui déborde de la colonne horaire.
|
||||
- le calendrier supporte l'import/export texte via le panneau d'ajout, pour les vues hebdomadaire et mensuelle.
|
||||
- le calendrier supporte l'import/export texte via le toggle import/export du header, pour les vues hebdomadaire et mensuelle.
|
||||
|
||||
### Arbre D'objectifs
|
||||
|
||||
L'arbre d'objectifs représente des chaînes de craft, de farm ou de prérequis sous forme d'arborescence dépliable.
|
||||
|
||||
Regles UI :
|
||||
|
||||
- utiliser le ratio `tool-split-*` 35 / 65 quand l'édition est visible ;
|
||||
- le panneau gauche édite uniquement le nœud sélectionné via les onglets `Base`, `Contenu` quand le type le nécessite, et `Avancé` ;
|
||||
- le panneau gauche ne doit pas porter les boutons d'ajout racine ou sous-étape ; l'ajout racine se fait via un bouton dans le schéma, long en haut en mode vertical et haut/étroit à gauche en mode horizontal, et les sous-étapes via le bouton `+` des cards ;
|
||||
- l'onglet `Base` contient le type `Étape`, `Texte`, `Calculateur` ou `Checklist`, puis le titre ; pour une étape, il expose aussi l'icône et la couleur d'icône ;
|
||||
- l'onglet `Contenu` est réservé aux types `Texte`, `Calculateur` et `Checklist` ; les étapes ne doivent pas afficher cet onglet ;
|
||||
- l'onglet `Avancé` contient le reset par nœud ; pour une étape seulement, il ajoute les sections `Quantité` et `Progression`, avec un switch où l'état actif correspond à la progression automatique par les enfants ;
|
||||
- le switch de progression expose une aide courte sous le contrôle afin de différencier calcul automatique et mise à jour manuelle ;
|
||||
- une étape dont tous les enfants directs ont une cible de `1` se comporte par défaut en mise à jour manuelle, sauf choix explicite du mode automatique ;
|
||||
- le panneau droit reste la source visuelle principale, avec branches compactes, liaisons SVG mesurées et barres de progression ;
|
||||
- le panneau droit expose un switch avec icône `rotate` pour alterner entre organisation horizontale et verticale ; l'orientation est persistée par outil ;
|
||||
- les liaisons entre nœuds doivent être dessinées par une couche dédiée basée sur les positions réelles des cartes, pas par des pseudo-éléments CSS dépendants de largeurs fixes ;
|
||||
- une card peut avoir plusieurs prérequis visibles : elle garde un parent principal pour le placement, mais des liaisons supplémentaires peuvent converger vers elle ;
|
||||
- tous les enfants restent rendus comme des nœuds d'organigramme, même sans sous-enfant, afin que l'ajout ultérieur d'un enfant ne change pas brutalement la structure ;
|
||||
- l'ouverture d'une branche doit replier les branches sœurs du même niveau et leurs descendants, afin de limiter la largeur déployée en mode deux colonnes ;
|
||||
- quand une branche ouverte contient un objectif à plusieurs parents, les branches nécessaires aux autres parents visibles doivent rester ouvertes afin que la convergence reste compréhensible ;
|
||||
- les nœuds affichent une icône principale et révèlent leur titre via `Tooltip`, afin de garder une lecture proche organigramme ;
|
||||
- les types `Texte`, `Calculateur` et `Checklist` sont des mini-contenus internes au nœud : leurs lignes doivent être visibles directement dans la carte, avec une zone d'ajout compacte ;
|
||||
- les cards de liste doivent laisser leur preview interne occuper la largeur disponible de la card, sans largeur fixe plus étroite que la carte ;
|
||||
- ces mini-contenus ne remplacent pas les sous-objectifs : les sous-objectifs restent des enfants d'organigramme séparés ;
|
||||
- les lignes `Checklist` suivent le comportement de l'outil checklist : cible `1` affiche une checkbox ; cible supérieure à `1` affiche une quantité avec contrôles `-` et `+` ;
|
||||
- les lignes `Calculateur` exposent une formule et un résultat ; la variable `base` référence la cible calculée du parent direct ;
|
||||
- les cards peuvent exposer des actions secondaires au hover/focus, notamment modification et suppression, sans les rendre visibles au repos ;
|
||||
- le bouton `+` d'une card ajoute toujours une sous-étape ; les nœuds `Texte`, `Calculateur` et `Checklist` exposent un bouton long dédié sous leur preview pour ajouter une entrée interne ;
|
||||
- le bouton long d'entrée interne doit avoir une tooltip explicite, car son libellé visible reste volontairement compact ;
|
||||
- l'accent de couleur d'un nœud doit s'appliquer à toute la card, y compris au hover, sans bordure latérale d'une couleur différente ;
|
||||
- la card sélectionnée en mode édition doit être clairement identifiable par un contour et un halo plus présents que l'état sélectionné en lecture ;
|
||||
- le drag & drop des nœuds doit afficher son indicateur selon l'orientation de la liste : gauche/droite en horizontal, haut/bas en vertical ;
|
||||
- les actions de nœud utilisent des boutons icônes : ajout, repli, suppression, déplacement ;
|
||||
- le bouton de déplacement doit rester clairement visible sur les cards compactes, notamment les nœuds `Texte` et `Checklist` ;
|
||||
- la progression d'un parent doit rester agrégée et lisible sans afficher une formule détaillée dans la carte ;
|
||||
- en panneau latéral ou mobile, l'arbre passe en une colonne sans scroll horizontal.
|
||||
|
||||
### Combos
|
||||
|
||||
|
|
@ -320,6 +358,18 @@ Regles :
|
|||
|
||||
## 11. Boutons
|
||||
|
||||
### Selects Des Outils
|
||||
|
||||
Tous les `<select>` rendus dans le contenu d'un outil doivent utiliser le style factorise de `.module-content select`, defini dans `website/src/styles/toolboxes/_shared-controls.scss`.
|
||||
|
||||
Regles :
|
||||
|
||||
- flèche custom violet clair, doree au hover/focus ;
|
||||
- fond sombre en couches legerement violet/indigo ;
|
||||
- hauteur compacte cible 36 px ;
|
||||
- options sur fond `#11182f` ;
|
||||
- les fichiers d'outil peuvent definir uniquement les contraintes de layout necessaires : largeur, grille, flex, ou typographie locale.
|
||||
|
||||
### Primary
|
||||
|
||||
Les boutons primary sont plus contrastes que les boutons standards.
|
||||
|
|
@ -532,6 +582,9 @@ Regles :
|
|||
- declarer ces images dans `games.json` via `images.cardCover` et `images.heroBg` ;
|
||||
- les toolboxes libres utilisent une icone de `website/public/static/img/toolbox-icons` ;
|
||||
- `toolbox.png` est l'icone par defaut ;
|
||||
- les SVG utilises comme icones ou masques UI doivent etre ranges directement dans `website/public/static/icons` ;
|
||||
- en cas de collision de nom, reutiliser l'icone existante quand elle couvre le meme usage ; sinon prefixer le fichier par son domaine plutot que creer un sous-dossier ;
|
||||
- `website/public/static/img` reste reserve aux images bitmap, covers, illustrations, captures et visuels editoriaux ;
|
||||
- les categories de jeu utilisent des images representatives, entieres et bien cadrees ;
|
||||
- eviter les images trop grandes dans les cards.
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ Checklist à suivre lors de l'ajout ou de la modification d'un outil, d'une page
|
|||
- Mettre à jour la normalisation et le stockage compact dans `website/src/features/toolboxes/storage/modules/<outil>.js`, puis vérifier l'export public dans `website/src/features/toolboxes/storage/toolboxStorage.js`.
|
||||
- Mettre à jour `docs/STORAGE_SCHEMA.md`.
|
||||
- Vérifier l'import et l'export si l'outil stocke des données.
|
||||
- Si l'outil a un contenu représentable en texte, ajouter ou vérifier l'import/export texte round-trip dans le panneau d'ajout.
|
||||
- Si l'outil a un contenu représentable en texte, ajouter ou vérifier l'import/export texte round-trip depuis le toggle import/export du header.
|
||||
- Si l'outil porte un état de progression, vérifier que l'import texte repart de l'état initial attendu.
|
||||
- Vérifier l'affichage dans la page toolbox complète.
|
||||
- Vérifier l'affichage dans le panneau latéral.
|
||||
|
|
|
|||
|
|
@ -244,6 +244,115 @@ Type : `equipmentPlanner`
|
|||
}
|
||||
```
|
||||
|
||||
## Outil Arbre D'objectifs
|
||||
|
||||
Type : `goalTree`
|
||||
|
||||
```json
|
||||
{
|
||||
"lastResetAt": 1786226400000,
|
||||
"orientation": "horizontal",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "goal1",
|
||||
"parentId": "",
|
||||
"title": "Breuvage de rage",
|
||||
"kind": "step",
|
||||
"icon": "cube",
|
||||
"color": "gold",
|
||||
"qtyTarget": 10,
|
||||
"qtyCurrent": 0,
|
||||
"formula": "",
|
||||
"progressMode": "auto",
|
||||
"resetType": "none",
|
||||
"resetTime": "00:00",
|
||||
"weeklyResetDay": 1,
|
||||
"checkedAt": 0,
|
||||
"collapsed": false
|
||||
},
|
||||
{
|
||||
"id": "goal2",
|
||||
"parentId": "goal1",
|
||||
"parentIds": ["goal1"],
|
||||
"title": "Élixir de destruction",
|
||||
"kind": "step",
|
||||
"icon": "flask",
|
||||
"color": "cyan",
|
||||
"qtyTarget": 1,
|
||||
"qtyCurrent": 0,
|
||||
"formula": "base * 3",
|
||||
"resetType": "weekly",
|
||||
"resetTime": "05:00",
|
||||
"weeklyResetDay": 1,
|
||||
"checkedAt": 0,
|
||||
"collapsed": false
|
||||
},
|
||||
{
|
||||
"id": "goal3",
|
||||
"parentId": "goal1",
|
||||
"title": "Liste de farm",
|
||||
"kind": "checklist",
|
||||
"items": [
|
||||
{
|
||||
"id": "item1",
|
||||
"label": "Élément",
|
||||
"formula": "1",
|
||||
"qtyTarget": 1,
|
||||
"qtyCurrent": 1
|
||||
},
|
||||
{
|
||||
"id": "item2",
|
||||
"label": "Faire cela",
|
||||
"formula": "base",
|
||||
"qtyTarget": 5,
|
||||
"qtyCurrent": 2
|
||||
}
|
||||
],
|
||||
"resetType": "none",
|
||||
"resetTime": "00:00",
|
||||
"weeklyResetDay": 1,
|
||||
"checkedAt": 0,
|
||||
"collapsed": false
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Stockage compact :
|
||||
|
||||
- `lastResetAt` est omis si vide.
|
||||
- `orientation` est omis si la valeur vaut `horizontal`.
|
||||
- `orientation` vaut `horizontal` ou `vertical`.
|
||||
- `parentId` est omis pour les racines.
|
||||
- `parentId` reste le parent principal de placement dans le schéma.
|
||||
- `parentIds` contient les parents/prérequis pris en compte pour les liaisons de convergence ; il est omis si aucun prérequis supplémentaire n'existe.
|
||||
- les calculs `base`, le placement, la suppression de branche et l'agrégation de progression restent basés sur `parentId`.
|
||||
- un objectif peut donc être placé sous un parent principal tout en ayant d'autres prérequis, afin de représenter une convergence de branches.
|
||||
- `kind` est omis si la valeur vaut `step`.
|
||||
- `kind` vaut `step`, `text` ou `checklist`.
|
||||
- `icon` est omis si la valeur vaut `cube`.
|
||||
- `icon` vaut `cube`, `fire`, `flask`, `grass`, `leaf`, `meat`, `powder`, `tear` ou `ball`.
|
||||
- `color` est omis si la valeur vaut `gold`.
|
||||
- `color` vaut `gold`, `cyan`, `violet`, `pink`, `green`, `gray`, `red`, `orange`, `yellow`, `blue` ou `indigo`.
|
||||
- `qtyTarget` est omis si la valeur vaut `1`.
|
||||
- `qtyCurrent` est omis si la valeur vaut `0`.
|
||||
- `formula` est omis si vide.
|
||||
- `progressMode` est utilisé uniquement par les objectifs `step`; il vaut `auto` ou `manual`.
|
||||
- `progressMode` est omis tant qu'aucun choix explicite n'a été fait; par défaut, une étape avec enfants calcule sa progression automatiquement, sauf si tous ses enfants directs ont une cible de `1`, auquel cas elle se comporte en mise à jour manuelle.
|
||||
- en `auto`, une étape avec enfants calcule sa progression depuis ses enfants; en `manual`, elle utilise sa propre `qtyCurrent`.
|
||||
- `items` est omis si vide ou si `kind` vaut `step`.
|
||||
- `items[].label` contient la ligne affichee dans un noeud `text` ou `checklist`; il peut etre vide pendant l'edition.
|
||||
- `items[].formula` est utilise uniquement par `checklist` pour calculer le total de la ligne ; la variable `base` reference la cible calculee du parent direct.
|
||||
- `items[].qtyTarget` est utilise uniquement par `checklist` et omis si la valeur vaut `1`.
|
||||
- `items[].qtyCurrent` est utilise uniquement par `checklist` et omis si la valeur vaut `0`.
|
||||
- `resetType` est omis si la valeur vaut `none`.
|
||||
- `resetType` vaut `none`, `daily` ou `weekly`.
|
||||
- `resetTime` est omis si la valeur vaut `00:00`.
|
||||
- `weeklyResetDay` est omis si la valeur vaut `1`.
|
||||
- `checkedAt` est omis si vide.
|
||||
- `collapsed` est omis si `false`.
|
||||
- Les parents invalides ou cycliques sont ramenés à la racine pendant la normalisation.
|
||||
|
||||
Stockage compact :
|
||||
|
||||
- les types sans équipement sont ignorés ;
|
||||
|
|
@ -757,7 +866,7 @@ Exemple :
|
|||
|
||||
## Import / Export texte des outils
|
||||
|
||||
Les outils compatibles proposent aussi un échange par copier-coller depuis leur panneau d'ajout. Ce format est un format UI distinct du stockage IndexedDB et de l'export JSON global.
|
||||
Les outils compatibles proposent aussi un échange par copier-coller depuis le toggle import/export de leur header. Ce format est un format UI distinct du stockage IndexedDB et de l'export JSON global.
|
||||
|
||||
Règles communes :
|
||||
|
||||
|
|
|
|||
|
|
@ -101,9 +101,9 @@ test("diablo4 data and route are available", async () => {
|
|||
assert.match(listingSource, /diablo4\.filterOptions\.affixes/);
|
||||
await readFile("website/public/static/img/games/diablo4/card-cover.png");
|
||||
await readFile("website/public/static/img/games/diablo4/hero-bg.png");
|
||||
await readFile("website/public/static/img/diablo4/axe.svg");
|
||||
await readFile("website/public/static/img/diablo4/cube.svg");
|
||||
await readFile("website/public/static/img/diablo4/fire.svg");
|
||||
await readFile("website/public/static/img/diablo4/shield.svg");
|
||||
await readFile("website/public/static/img/diablo4/wing.svg");
|
||||
await readFile("website/public/static/icons/axe.svg");
|
||||
await readFile("website/public/static/icons/cube.svg");
|
||||
await readFile("website/public/static/icons/fire.svg");
|
||||
await readFile("website/public/static/icons/shield.svg");
|
||||
await readFile("website/public/static/icons/wing.svg");
|
||||
});
|
||||
|
|
|
|||
|
|
@ -269,6 +269,67 @@ export function validateSiteContent(site) {
|
|||
"toolboxes.modules.calculator.useEntryTitle",
|
||||
"toolboxes.modules.calculator.renameTitle",
|
||||
"toolboxes.modules.calculator.deleteTitle",
|
||||
"toolboxes.modules.goalTree.editorTitle",
|
||||
"toolboxes.modules.goalTree.treeTitle",
|
||||
"toolboxes.modules.goalTree.addRootButton",
|
||||
"toolboxes.modules.goalTree.addRootSchemaButton",
|
||||
"toolboxes.modules.goalTree.addChildButton",
|
||||
"toolboxes.modules.goalTree.addChildTitle",
|
||||
"toolboxes.modules.goalTree.addContentItemTitle",
|
||||
"toolboxes.modules.goalTree.addContentItemTooltip",
|
||||
"toolboxes.modules.goalTree.verticalViewTitle",
|
||||
"toolboxes.modules.goalTree.horizontalViewTitle",
|
||||
"toolboxes.modules.goalTree.cardActionsLabel",
|
||||
"toolboxes.modules.goalTree.editTitle",
|
||||
"toolboxes.modules.goalTree.baseTab",
|
||||
"toolboxes.modules.goalTree.contentTab",
|
||||
"toolboxes.modules.goalTree.advancedTab",
|
||||
"toolboxes.modules.goalTree.resetTimeLabel",
|
||||
"toolboxes.modules.goalTree.weeklyResetDayLabel",
|
||||
"toolboxes.modules.goalTree.titleLabel",
|
||||
"toolboxes.modules.goalTree.listTitleLabel",
|
||||
"toolboxes.modules.goalTree.kindLabel",
|
||||
"toolboxes.modules.goalTree.stepKind",
|
||||
"toolboxes.modules.goalTree.textKind",
|
||||
"toolboxes.modules.goalTree.checklistKind",
|
||||
"toolboxes.modules.goalTree.iconLabel",
|
||||
"toolboxes.modules.goalTree.iconColorLabel",
|
||||
"toolboxes.modules.goalTree.targetQuantityLabel",
|
||||
"toolboxes.modules.goalTree.quantitySectionTitle",
|
||||
"toolboxes.modules.goalTree.progressSectionTitle",
|
||||
"toolboxes.modules.goalTree.autoProgressLabel",
|
||||
"toolboxes.modules.goalTree.manualProgressLabel",
|
||||
"toolboxes.modules.goalTree.autoProgressTitle",
|
||||
"toolboxes.modules.goalTree.manualProgressTitle",
|
||||
"toolboxes.modules.goalTree.autoProgressHint",
|
||||
"toolboxes.modules.goalTree.manualProgressHint",
|
||||
"toolboxes.modules.goalTree.contentSectionTitle",
|
||||
"toolboxes.modules.goalTree.itemLabelLabel",
|
||||
"toolboxes.modules.goalTree.itemLabelPlaceholder",
|
||||
"toolboxes.modules.goalTree.itemCheckedLabel",
|
||||
"toolboxes.modules.goalTree.itemTotalLabel",
|
||||
"toolboxes.modules.goalTree.itemTotalPlaceholder",
|
||||
"toolboxes.modules.goalTree.itemTotalHelp",
|
||||
"toolboxes.modules.goalTree.prerequisitesSectionTitle",
|
||||
"toolboxes.modules.goalTree.contentTabHint",
|
||||
"toolboxes.modules.goalTree.stepContentUnavailable",
|
||||
"toolboxes.modules.goalTree.decrementItemTitle",
|
||||
"toolboxes.modules.goalTree.incrementItemTitle",
|
||||
"toolboxes.modules.goalTree.addContentItemButton",
|
||||
"toolboxes.modules.goalTree.formulaPlaceholder",
|
||||
"toolboxes.modules.goalTree.formulaHelp",
|
||||
"toolboxes.modules.goalTree.resetTypeLabel",
|
||||
"toolboxes.modules.goalTree.noneReset",
|
||||
"toolboxes.modules.goalTree.dailyReset",
|
||||
"toolboxes.modules.goalTree.weeklyReset",
|
||||
"toolboxes.modules.goalTree.closeButton",
|
||||
"toolboxes.modules.goalTree.deleteButton",
|
||||
"toolboxes.modules.goalTree.deleteTitle",
|
||||
"toolboxes.modules.goalTree.reorderTitle",
|
||||
"toolboxes.modules.goalTree.showChildrenTitle",
|
||||
"toolboxes.modules.goalTree.hideChildrenTitle",
|
||||
"toolboxes.modules.goalTree.emptySelection",
|
||||
"toolboxes.modules.goalTree.emptyTree",
|
||||
"toolboxes.modules.table.toolbarLabel",
|
||||
"toolboxes.modules.table.gridLabel",
|
||||
"toolboxes.modules.table.cellLabel",
|
||||
|
|
@ -557,7 +618,7 @@ export function validateSiteContent(site) {
|
|||
});
|
||||
|
||||
const libraryToolDocs = valueAt(site, "library.toolDocs");
|
||||
["notepad", "checklist", "counters", "images", "imageAnnotation", "links", "calculator", "table", "timer", "calendar", "taskPlanner", "combos", "equipmentPlanner"].forEach((toolType) => {
|
||||
["notepad", "checklist", "counters", "images", "imageAnnotation", "links", "calculator", "table", "goalTree", "timer", "calendar", "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`);
|
||||
|
|
@ -770,7 +831,7 @@ export function validateLibraryData(payload) {
|
|||
assert.deepEqual(new Set(twoColumnIds), moduleIds, "library.toolbox.moduleOrder.two must contain every module exactly once");
|
||||
assert.equal(twoColumnIds.length, moduleIds.size, "library.toolbox.moduleOrder.two must not contain duplicate modules");
|
||||
|
||||
["notepad", "checklist", "images", "links", "counters", "combos", "calculator", "table", "timer", "calendar", "taskPlanner", "equipmentPlanner", "imageAnnotation"].forEach((type) => {
|
||||
["notepad", "checklist", "images", "links", "counters", "combos", "calculator", "table", "goalTree", "timer", "calendar", "taskPlanner", "equipmentPlanner", "imageAnnotation"].forEach((type) => {
|
||||
assert.ok(payload.toolbox.modules.some((module) => module.type === type), `library must include a ${type} example`);
|
||||
});
|
||||
|
||||
|
|
@ -790,6 +851,11 @@ export function validateLibraryData(payload) {
|
|||
assert.ok(Array.isArray(taskData.relations), "library task planner example must contain relations");
|
||||
assert.ok(taskData.relations.length >= 1, "library task planner example must include a parent/child relation");
|
||||
|
||||
const goalTreeModule = payload.toolbox.modules.find((module) => module.type === "goalTree");
|
||||
const goalTreeData = payload.modules[goalTreeModule.id];
|
||||
assert.ok(Array.isArray(goalTreeData.nodes), "library goal tree example must contain nodes");
|
||||
assert.ok(goalTreeData.nodes.some((node) => node.parentId), "library goal tree example must include a parent/child node");
|
||||
|
||||
const equipmentModule = payload.toolbox.modules.find((module) => module.type === "equipmentPlanner");
|
||||
const equipmentData = payload.modules[equipmentModule.id];
|
||||
assert.ok(Array.isArray(equipmentData.types), "library equipment planner example must contain types");
|
||||
|
|
|
|||
|
|
@ -128,6 +128,7 @@ test("legacy icons and primary images are available", async () => {
|
|||
await readFile("website/public/static/icons/copy.svg", "utf8");
|
||||
await readFile("website/public/static/icons/abacus.svg", "utf8");
|
||||
await readFile("website/public/static/icons/calculator.svg", "utf8");
|
||||
await readFile("website/public/static/icons/diagram.svg", "utf8");
|
||||
await readFile("website/public/static/icons/map.svg", "utf8");
|
||||
await readFile("website/public/static/icons/enter.svg", "utf8");
|
||||
await readFile("website/public/static/icons/export.svg", "utf8");
|
||||
|
|
@ -142,6 +143,11 @@ test("legacy icons and primary images are available", async () => {
|
|||
await readFile("website/public/static/icons/trashcan.svg", "utf8");
|
||||
await readFile("website/public/static/icons/zoom.svg", "utf8");
|
||||
await readFile("website/public/static/icons/warning.svg", "utf8");
|
||||
await readFile("website/public/static/icons/axe.svg", "utf8");
|
||||
await readFile("website/public/static/icons/cube.svg", "utf8");
|
||||
await readFile("website/public/static/icons/fire.svg", "utf8");
|
||||
await readFile("website/public/static/icons/shield.svg", "utf8");
|
||||
await readFile("website/public/static/icons/wing.svg", "utf8");
|
||||
await readFile("website/public/favicon.ico");
|
||||
await readFile("website/public/static/img/dragon-home.png");
|
||||
await readFile("website/public/static/img/dragon-sidebar.png");
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ test("toolbox storage, cards and pages are wired", async () => {
|
|||
assert.match(toolboxStorage, /normalizeTimerData/);
|
||||
assert.match(toolboxStorage, /normalizeCalendarData/);
|
||||
assert.match(toolboxStorage, /normalizeTaskPlannerData/);
|
||||
assert.match(toolboxStorage, /normalizeGoalTreeData/);
|
||||
assert.match(toolboxStorage, /normalizeEquipmentPlannerData/);
|
||||
assert.match(toolboxStorage, /summarizeEquipmentPlannerData/);
|
||||
assert.match(toolboxStorage, /normalizeImageAnnotationData/);
|
||||
|
|
@ -69,6 +70,7 @@ test("toolbox storage, cards and pages are wired", async () => {
|
|||
assert.match(storageCompactorRegistry, /export const MODULE_DATA_COMPACTORS/);
|
||||
assert.match(storageCompactorRegistry, /timer: compactTimerDataForStorage/);
|
||||
assert.match(storageCompactorRegistry, /calendar: compactCalendarDataForStorage/);
|
||||
assert.match(storageCompactorRegistry, /goalTree: compactGoalTreeDataForStorage/);
|
||||
assert.match(timerStorage, /scrollResults/);
|
||||
assert.match(calendarStorage, /normalizeCalendarData/);
|
||||
assert.match(toolboxStorage, /nextId\("calendar"\)/);
|
||||
|
|
@ -153,6 +155,8 @@ test("toolbox module registry and modules expose expected behavior", async () =>
|
|||
const taskPlannerModule = await readFile("website/src/features/toolboxes/modules/TaskPlannerModule.jsx", "utf8");
|
||||
const taskPlannerItems = await readFile("website/src/features/toolboxes/modules/TaskPlannerItems.jsx", "utf8");
|
||||
const taskPlannerUtils = await readFile("website/src/features/toolboxes/modules/taskPlannerUtils.js", "utf8");
|
||||
const goalTreeModule = await readFile("website/src/features/toolboxes/modules/GoalTreeModule.jsx", "utf8");
|
||||
const goalTreeUtils = await readFile("website/src/features/toolboxes/modules/goalTreeUtils.js", "utf8");
|
||||
const equipmentPlannerModule = await readFile("website/src/features/toolboxes/modules/EquipmentPlannerModule.jsx", "utf8");
|
||||
const equipmentPlannerEditors = await readFile("website/src/features/toolboxes/modules/EquipmentPlannerEditors.jsx", "utf8");
|
||||
const equipmentPlannerItem = await readFile("website/src/features/toolboxes/modules/EquipmentPlannerItem.jsx", "utf8");
|
||||
|
|
@ -189,6 +193,7 @@ test("toolbox module registry and modules expose expected behavior", async () =>
|
|||
assert.match(moduleRegistry, /timer:/);
|
||||
assert.match(moduleRegistry, /calendar:/);
|
||||
assert.match(moduleRegistry, /taskPlanner:/);
|
||||
assert.match(moduleRegistry, /goalTree:/);
|
||||
assert.match(moduleRegistry, /equipmentPlanner:/);
|
||||
assert.match(moduleMetadataRegistry, /Planificateur d'équipements/);
|
||||
assert.match(moduleRegistry, /imageAnnotation:/);
|
||||
|
|
@ -346,6 +351,38 @@ test("toolbox module registry and modules expose expected behavior", async () =>
|
|||
assert.match(taskPlannerUtils, /export function applyDueResets/);
|
||||
assert.doesNotMatch(taskPlannerModule, /canMoveItem:/);
|
||||
assert.doesNotMatch(taskPlannerModule, /canMoveGroup:/);
|
||||
assert.match(goalTreeModule, /export function GoalTreeModule/);
|
||||
assert.match(goalTreeModule, /normalizeGoalTreeData/);
|
||||
assert.match(goalTreeModule, /applyGoalTreeDueResets/);
|
||||
assert.match(goalTreeModule, /TextExchangeActions/);
|
||||
assert.match(goalTreeModule, /DiagramConnectors/);
|
||||
assert.match(goalTreeModule, /Tooltip/);
|
||||
assert.match(goalTreeModule, /GoalTreeIconPicker/);
|
||||
assert.match(goalTreeModule, /GoalTreeContentEditor/);
|
||||
assert.match(goalTreeModule, /GoalTreeNodeContentPreview/);
|
||||
assert.match(goalTreeModule, /GoalTreeChecklistInlineControl/);
|
||||
assert.match(goalTreeModule, /GoalTreeStepLeafControl/);
|
||||
assert.match(goalTreeModule, /GoalTreePrerequisitePicker/);
|
||||
assert.match(goalTreeModule, /function toggleBranch/);
|
||||
assert.match(goalTreeModule, /function editNode/);
|
||||
assert.match(goalTreeModule, /function addNodeContentItem/);
|
||||
assert.match(goalTreeModule, /closedSiblingIds/);
|
||||
assert.match(goalTreeModule, /useGroupedReorder/);
|
||||
assert.match(goalTreeModule, /const treeOrientation = data\.orientation \|\| "horizontal"/);
|
||||
assert.match(goalTreeModule, /const reorderOrientation = treeOrientation === "horizontal" \? "vertical" : "horizontal"/);
|
||||
assert.match(goalTreeModule, /orientation: reorderOrientation/);
|
||||
assert.match(goalTreeModule, /orientation={treeOrientation}/);
|
||||
assert.match(goalTreeModule, /module-scroll-button goal-tree-orientation-switch/);
|
||||
assert.match(goalTreeModule, /Icon name="rotate"/);
|
||||
assert.match(goalTreeModule, /function toggleLinkedParent/);
|
||||
assert.match(goalTreeModule, /grouped: false/);
|
||||
assert.match(goalTreeModule, /getGoalTreeConvergenceKeepOpenIds/);
|
||||
assert.match(goalTreeModule, /progress\.current/);
|
||||
assert.match(goalTreeModule, /label: textContent\.itemLabelPlaceholder \|\| "Élément"/);
|
||||
assert.doesNotMatch(goalTreeUtils, /"calculator"/);
|
||||
assert.match(goalTreeModule, /getItemProps\(\{ itemId: node\.id, parentId \}\)/);
|
||||
assert.match(goalTreeUtils, /export function calculateGoalTreeFormula/);
|
||||
assert.match(goalTreeUtils, /export function evaluateGoalTreeNodes/);
|
||||
assert.match(equipmentPlannerModule, /export function EquipmentPlannerModule/);
|
||||
assert.match(equipmentPlannerModule, /normalizeEquipmentPlannerData/);
|
||||
assert.match(equipmentPlannerModule, /summarizeEquipmentPlannerData/);
|
||||
|
|
@ -439,6 +476,8 @@ test("toolbox module registry and modules expose expected behavior", async () =>
|
|||
assert.match(textImport, /line\.indexOf\(":"\)/);
|
||||
assert.match(textImport, /exportCalendarText/);
|
||||
assert.match(textImport, /importCalendarText/);
|
||||
assert.match(textImport, /exportGoalTreeText/);
|
||||
assert.match(textImport, /importGoalTreeText/);
|
||||
assert.match(textImport, /CALENDAR_WEEKDAYS/);
|
||||
assert.match(imageViewer, /image-viewer-media/);
|
||||
assert.match(imageViewer, /image-viewer-image-frame/);
|
||||
|
|
|
|||
|
|
@ -3,8 +3,9 @@ import { test } from "node:test";
|
|||
import assert from "node:assert/strict";
|
||||
import { evaluateTableCell } from "../website/src/features/toolboxes/modules/tableFormulaEngine.js";
|
||||
import { exportModuleText, importModuleText, parseColonImportLines } from "../website/src/features/toolboxes/modules/textImport.js";
|
||||
import { calculateGoalTreeFormula, evaluateGoalTreeNodes } from "../website/src/features/toolboxes/modules/goalTreeUtils.js";
|
||||
import { getTimePatternRecurrenceMs, getTimePatternTargetMs } from "../website/src/features/toolboxes/modules/timerUtils.js";
|
||||
import { compactModuleDataForStorage, createToolboxExportPayload, normalizeCalculatorData, normalizeChecklistData, normalizeCombosData, normalizeCountersData, normalizeEquipmentPlannerData, normalizeImageAnnotationData, normalizeLinksData, normalizeModuleOrder, normalizeNotepadData, normalizeTableData, normalizeTaskPlannerData, normalizeTimerData, normalizeToolbox, normalizeUrl, summarizeEquipmentPlannerData } from "../website/src/features/toolboxes/storage/toolboxStorage.js";
|
||||
import { compactModuleDataForStorage, createToolboxExportPayload, normalizeCalculatorData, normalizeChecklistData, normalizeCombosData, normalizeCountersData, normalizeEquipmentPlannerData, normalizeGoalTreeData, normalizeImageAnnotationData, normalizeLinksData, normalizeModuleOrder, normalizeNotepadData, normalizeTableData, normalizeTaskPlannerData, normalizeTimerData, normalizeToolbox, normalizeUrl, summarizeEquipmentPlannerData } from "../website/src/features/toolboxes/storage/toolboxStorage.js";
|
||||
import { applyGroupedReorderOperation, completeGroupOrder, getBoundaryItemId, getGroupedEntries, moveGroupOrder, moveGroupOrderToEnd, moveGroupOrderToStart, moveItem, moveItemGroup } from "../website/src/hooks/useGroupedReorder.js";
|
||||
|
||||
function createTextImportContext() {
|
||||
|
|
@ -20,6 +21,7 @@ function createTextImportContext() {
|
|||
normalizeTableData,
|
||||
normalizeTimerData,
|
||||
normalizeTaskPlannerData,
|
||||
normalizeGoalTreeData,
|
||||
normalizeEquipmentPlannerData,
|
||||
normalizeImageAnnotationData,
|
||||
normalizeUrl
|
||||
|
|
@ -118,6 +120,22 @@ test("text exchange imports table TSV and calculator hierarchy", () => {
|
|||
assert.equal(calculatorImport.data.entries[1].formula, "base*5");
|
||||
});
|
||||
|
||||
test("goal tree text exchange imports formulas and hierarchy", () => {
|
||||
const context = createTextImportContext();
|
||||
const imported = importModuleText("goalTree", "- Breuvage de rage x10\n - Élixir de destruction = base * 3\n - Poudre de flamme x5\n - Sève de cèdre x7\n - Catalyseur magique x10", context);
|
||||
assert.equal(imported.ok, true);
|
||||
assert.equal(imported.data.nodes.length, 5);
|
||||
assert.equal(imported.data.nodes[0].kind, "step");
|
||||
assert.equal(imported.data.nodes[1].parentId, imported.data.nodes[0].id);
|
||||
assert.equal(imported.data.nodes[1].formula, "base * 3");
|
||||
assert.equal(imported.data.nodes[2].kind, "step");
|
||||
assert.equal(imported.data.nodes[2].qtyCurrent, 0);
|
||||
|
||||
const exported = exportModuleText("goalTree", imported.data);
|
||||
assert.match(exported, /Breuvage de rage x10/);
|
||||
assert.match(exported, /Élixir de destruction = base \* 3/);
|
||||
});
|
||||
|
||||
test("text exchange round trips combos, tasks, equipment, images and timers", () => {
|
||||
const context = createTextImportContext();
|
||||
const combos = normalizeCombosData({
|
||||
|
|
@ -910,3 +928,107 @@ test("task planner storage compacts defaults and export remaps task relations",
|
|||
assert.equal(exportedPlanner.relations[0].toTaskId, "a2");
|
||||
assert.equal(exportedPlanner.relations[0].prerequisite, true);
|
||||
});
|
||||
|
||||
test("goal tree data normalizes, compacts, calculates and remaps nodes", () => {
|
||||
assert.equal(calculateGoalTreeFormula("base * 3", 10), 30);
|
||||
assert.equal(calculateGoalTreeFormula("base / 2", 10), 5);
|
||||
assert.equal(calculateGoalTreeFormula("base - 12", 10, { clamp: false }), -2);
|
||||
assert.equal(calculateGoalTreeFormula("base,5", 10), null);
|
||||
assert.equal(calculateGoalTreeFormula("alert(1)", 10), null);
|
||||
|
||||
const normalized = normalizeGoalTreeData({
|
||||
orientation: "vertical",
|
||||
nodes: [
|
||||
{ id: "root", title: "Root", kind: "step", icon: "fire", color: "cyan", qtyTarget: 10, qtyCurrent: 1 },
|
||||
{ id: "child", parentId: "root", title: "Child", formula: "base*3", qtyCurrent: 15, resetType: "daily", resetTime: "09:30", checkedAt: 99 },
|
||||
{ id: "bad", parentId: "missing", title: "Bad", kind: "weird", resetType: "later" },
|
||||
{ id: "list", title: "List", kind: "checklist", items: [{ id: "i1", label: "Done", qtyCurrent: 1 }, { id: "i2", label: "Todo", formula: "5", qtyCurrent: 2 }, { id: "i3", label: "" }] },
|
||||
{ id: "formulaList", parentId: "root", parentIds: ["root", "list"], title: "Formula list", kind: "checklist", items: [{ id: "i4", label: "Triple", formula: "base * 3", qtyCurrent: 30 }, { id: "i5", label: "Delta", formula: "base - 12" }] }
|
||||
]
|
||||
});
|
||||
assert.equal(normalized.orientation, "vertical");
|
||||
assert.equal(normalized.nodes[2].parentId, "");
|
||||
assert.equal(normalized.nodes[2].kind, "step");
|
||||
assert.equal(normalized.nodes[2].resetType, "none");
|
||||
assert.equal(normalized.nodes[1].resetTime, "09:30");
|
||||
assert.deepEqual(normalized.nodes[4].parentIds, ["root", "list"]);
|
||||
|
||||
const evaluated = evaluateGoalTreeNodes(normalized.nodes);
|
||||
assert.equal(evaluated[1].computedQtyTarget, 30);
|
||||
assert.equal(evaluated[1].progress.ratio, 0.5);
|
||||
assert.equal(evaluated[0].progress.current, 45);
|
||||
assert.equal(evaluated[0].progress.target, 61);
|
||||
assert.equal(evaluated[3].progress.current, 3);
|
||||
assert.equal(evaluated[3].progress.target, 7);
|
||||
assert.equal(evaluated[4].items[0].qtyTarget, 30);
|
||||
assert.equal(evaluated[4].items[1].qtyTarget, 1);
|
||||
assert.equal(evaluated[4].progress.current, 30);
|
||||
|
||||
const compact = compactModuleDataForStorage("goalTree", normalized);
|
||||
assert.equal(compact.settings, undefined);
|
||||
assert.equal(compact.orientation, "vertical");
|
||||
assert.equal(compact.nodes[0].kind, undefined);
|
||||
assert.equal(compact.nodes[0].icon, "fire");
|
||||
assert.equal(compact.nodes[0].color, "cyan");
|
||||
assert.equal(compact.nodes[0].qtyCurrent, 1);
|
||||
assert.equal(compact.nodes[1].formula, "base*3");
|
||||
assert.equal(compact.nodes[1].resetType, "daily");
|
||||
assert.equal(compact.nodes[1].resetTime, "09:30");
|
||||
assert.deepEqual(compact.nodes[3].items, [{ id: "i1", label: "Done", qtyCurrent: 1 }, { id: "i2", label: "Todo", formula: "5", qtyCurrent: 2 }, { id: "i3", label: "" }]);
|
||||
assert.deepEqual(compact.nodes[4].parentIds, ["root", "list"]);
|
||||
assert.deepEqual(compact.nodes[4].items, [{ id: "i4", label: "Triple", formula: "base * 3", qtyCurrent: 30 }, { id: "i5", label: "Delta", formula: "base - 12" }]);
|
||||
|
||||
const exported = createToolboxExportPayload(
|
||||
{ id: "toolbox1", name: "Goals", modules: [{ id: "module1", type: "goalTree" }], updatedAt: "2026-01-01T00:00:00.000Z" },
|
||||
{ "toolbox1:module1": compact }
|
||||
);
|
||||
const exportedTree = exported.modules.m1;
|
||||
assert.deepEqual(exportedTree.nodes.map((node) => node.id), ["g1", "g2", "g3", "g4", "g5"]);
|
||||
assert.equal(exportedTree.nodes[1].parentId, "g1");
|
||||
assert.deepEqual(exportedTree.nodes[4].parentIds, ["g1", "g4"]);
|
||||
assert.deepEqual(exportedTree.nodes[3].items.map((item) => item.id), ["g6", "g7", "g8"]);
|
||||
assert.deepEqual(exportedTree.nodes[4].items.map((item) => item.id), ["g9", "ga"]);
|
||||
});
|
||||
|
||||
test("goal tree parent progress uses base-scaled child bottlenecks", () => {
|
||||
const normalized = normalizeGoalTreeData({
|
||||
nodes: [
|
||||
{ id: "root", title: "Potion de vie", kind: "step", qtyTarget: 10 },
|
||||
{
|
||||
id: "ingredients",
|
||||
parentId: "root",
|
||||
title: "Ingrédients",
|
||||
kind: "checklist",
|
||||
items: [
|
||||
{ id: "herb", label: "Herbe magique", formula: "base", qtyCurrent: 7 },
|
||||
{ id: "water", label: "Eau rajeunissante", formula: "base", qtyCurrent: 5 }
|
||||
]
|
||||
},
|
||||
{ id: "manual", title: "Objectif manuel", kind: "step", qtyTarget: 10, qtyCurrent: 3, progressMode: "manual" },
|
||||
{ id: "manualChild", parentId: "manual", title: "Enfant ignoré", kind: "step", formula: "base", qtyCurrent: 10 },
|
||||
{ id: "defaultManual", title: "Validation manuelle par défaut", kind: "step", qtyTarget: 1, qtyCurrent: 1 },
|
||||
{ id: "defaultManualChild", parentId: "defaultManual", title: "Validation enfant", kind: "step", qtyTarget: 1, qtyCurrent: 0 },
|
||||
{ id: "forcedAuto", title: "Validation auto forcée", kind: "step", qtyTarget: 1, qtyCurrent: 1, progressMode: "auto" },
|
||||
{ id: "forcedAutoChild", parentId: "forcedAuto", title: "Validation enfant auto", kind: "step", qtyTarget: 1, qtyCurrent: 0 }
|
||||
]
|
||||
});
|
||||
const evaluated = evaluateGoalTreeNodes(normalized.nodes);
|
||||
|
||||
assert.equal(evaluated[0].progress.current, 5);
|
||||
assert.equal(evaluated[0].progress.target, 10);
|
||||
assert.equal(evaluated[0].progress.ratio, 0.5);
|
||||
assert.equal(evaluated[1].progress.current, 12);
|
||||
assert.equal(evaluated[1].progress.target, 20);
|
||||
assert.equal(evaluated[2].progress.current, 3);
|
||||
assert.equal(evaluated[2].progress.target, 10);
|
||||
assert.equal(evaluated[4].progress.current, 1);
|
||||
assert.equal(evaluated[4].progress.target, 1);
|
||||
assert.equal(evaluated[6].progress.current, 0);
|
||||
assert.equal(evaluated[6].progress.target, 1);
|
||||
|
||||
const compact = compactModuleDataForStorage("goalTree", normalized);
|
||||
assert.equal(compact.nodes[0].progressMode, undefined);
|
||||
assert.equal(compact.nodes[2].progressMode, "manual");
|
||||
assert.equal(compact.nodes[4].progressMode, undefined);
|
||||
assert.equal(compact.nodes[6].progressMode, "auto");
|
||||
});
|
||||
|
|
|
|||
|
|
@ -36,6 +36,10 @@
|
|||
"id": "m8",
|
||||
"type": "table"
|
||||
},
|
||||
{
|
||||
"id": "mf",
|
||||
"type": "goalTree"
|
||||
},
|
||||
{
|
||||
"id": "m9",
|
||||
"type": "timer"
|
||||
|
|
@ -213,8 +217,8 @@
|
|||
}
|
||||
],
|
||||
"moduleOrder": {
|
||||
"one": ["m1", "m2", "m3", "m4", "m5", "m6", "m7", "m8", "m9", "md", "me", "ma", "mb", "mc"],
|
||||
"two": [["m1", "m2", "m3", "m4", "m5", "m6", "ma"], ["m7", "m8", "m9", "md", "me", "mb", "mc"]]
|
||||
"one": ["m1", "m2", "m3", "m4", "m5", "m6", "m7", "m8", "mf", "m9", "md", "me", "ma", "mb", "mc"],
|
||||
"two": [["m1", "m2", "m3", "m4", "m5", "m6", "ma"], ["m7", "m8", "mf", "m9", "md", "me", "mb", "mc"]]
|
||||
},
|
||||
"updatedAt": "2026-08-09T07:34:31.425Z",
|
||||
"moduleColumns": 1
|
||||
|
|
@ -1436,6 +1440,103 @@
|
|||
"3": "Valeur"
|
||||
}
|
||||
},
|
||||
"mf": {
|
||||
"lastResetAt": 1787263200000,
|
||||
"orientation": "vertical",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "goal1",
|
||||
"title": "Potion d'invincibilité",
|
||||
"icon": "flask",
|
||||
"color": "green",
|
||||
"qtyTarget": 10
|
||||
},
|
||||
{
|
||||
"id": "goal2",
|
||||
"title": "Potion de vie",
|
||||
"parentId": "goal1",
|
||||
"icon": "flask",
|
||||
"color": "red",
|
||||
"formula": "base"
|
||||
},
|
||||
{
|
||||
"id": "goal3",
|
||||
"title": "Ingredients",
|
||||
"parentId": "goal2",
|
||||
"kind": "checklist",
|
||||
"items": [
|
||||
{
|
||||
"id": "xn2okr5",
|
||||
"label": "Herbe magique",
|
||||
"formula": "base",
|
||||
"qtyTarget": 10,
|
||||
"qtyCurrent": 7
|
||||
},
|
||||
{
|
||||
"id": "xm5h5uu",
|
||||
"label": "Eau rajeunissante",
|
||||
"formula": "base",
|
||||
"qtyTarget": 10,
|
||||
"qtyCurrent": 5
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "goal4",
|
||||
"title": "Potion de résistance",
|
||||
"parentId": "goal1",
|
||||
"icon": "flask",
|
||||
"color": "blue",
|
||||
"formula": "base"
|
||||
},
|
||||
{
|
||||
"id": "goal5",
|
||||
"title": "Ingredients supp.",
|
||||
"parentId": "goal1",
|
||||
"kind": "checklist",
|
||||
"items": [
|
||||
{
|
||||
"id": "x3mlygm",
|
||||
"label": "Poudre magique",
|
||||
"formula": "base",
|
||||
"qtyTarget": 10
|
||||
},
|
||||
{
|
||||
"id": "x1dqx6w",
|
||||
"label": "Herbe magique",
|
||||
"formula": "base*2",
|
||||
"qtyTarget": 20
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "goal6",
|
||||
"title": "Parler a l'alchimiste",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "goal7",
|
||||
"title": "Acheter une station d'alchimie",
|
||||
"parentId": "goal6",
|
||||
"formula": "base",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "goal8",
|
||||
"title": "Apprendre la recette de potion d'invicibilité",
|
||||
"parentId": "goal7",
|
||||
"formula": "base",
|
||||
"collapsed": true
|
||||
},
|
||||
{
|
||||
"id": "goal9",
|
||||
"title": "S'inscrire à l'academie d'alchimie",
|
||||
"parentId": "goal6",
|
||||
"formula": "base",
|
||||
"collapsed": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"m9": {
|
||||
"activeTab": "countdown",
|
||||
"stopwatch": {
|
||||
|
|
|
|||
|
|
@ -673,6 +673,81 @@
|
|||
"example": "Lingot de champignite: 100\n Lingot de chlorophyte: 100\n Champignon luisant: 1500\nLingot de chlorophyte: 100\n Minerai de chlorophyte: 500"
|
||||
}
|
||||
},
|
||||
"goalTree": {
|
||||
"controls": [
|
||||
{
|
||||
"group": "Arbre",
|
||||
"icon": "rotate",
|
||||
"title": "Changer le mode d'affichage",
|
||||
"preview": {
|
||||
"kind": "switch",
|
||||
"icon": "rotate",
|
||||
"active": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"group": "Arbre",
|
||||
"icon": "chevron-down",
|
||||
"title": "Déployer l'objectif",
|
||||
"preview": {
|
||||
"kind": "iconButton",
|
||||
"icon": "chevron-down"
|
||||
}
|
||||
},
|
||||
{
|
||||
"group": "Objectifs",
|
||||
"icon": "edit",
|
||||
"title": "Éditer l'objectif",
|
||||
"preview": {
|
||||
"kind": "iconButton",
|
||||
"icon": "edit"
|
||||
}
|
||||
},
|
||||
{
|
||||
"group": "Objectifs",
|
||||
"icon": "trash",
|
||||
"title": "Supprimer l'objectif et ces étapes",
|
||||
"preview": {
|
||||
"kind": "iconButton",
|
||||
"icon": "trash",
|
||||
"danger": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"features": [
|
||||
{
|
||||
"title": "Décomposition visuelle",
|
||||
"text": "Un objectif peut être ouvert en étapes, sous-étapes et ressources finales.",
|
||||
"details": [
|
||||
"Chaque bloc peut être sélectionné pour éditer ses quantités et son type.",
|
||||
"Les branches se replient pour garder une vue lisible quand l'arbre grandit.",
|
||||
"Le même outil peut représenter un craft, une route de farm ou un objectif long."
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Quantités liées",
|
||||
"text": "Les sous-étapes peuvent utiliser une formule basée sur la quantité du parent.",
|
||||
"details": [
|
||||
"La variable base référence la cible calculée du parent.",
|
||||
"Les feuilles suivent leur quantité actuelle, tandis que les parents affichent une progression agrégée.",
|
||||
"Les formules restent limitées aux opérations numériques simples."
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Resets optionnels",
|
||||
"text": "Un nœud peut être ponctuel, quotidien ou hebdomadaire sans imposer un reset à tout l'arbre.",
|
||||
"details": [
|
||||
"Les resets remettent seulement la quantité actuelle à zéro.",
|
||||
"Le jour hebdomadaire et l'heure de reset se règlent au niveau de l'outil.",
|
||||
"La structure de l'arbre reste inchangée après une réinitialisation."
|
||||
]
|
||||
}
|
||||
],
|
||||
"importFormat": {
|
||||
"text": "Une ligne par nœud avec indentation de deux espaces. Utilisez xN pour une quantité simple ou = base * N pour une formule.",
|
||||
"example": "- Breuvage de rage x10\n - Élixir de destruction = base * 3\n - Poudre de flamme x5\n - Sève de cèdre x7\n - Catalyseur magique x10"
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
"controls": [
|
||||
{
|
||||
|
|
@ -1792,6 +1867,101 @@
|
|||
"renameTitle": "Renommer",
|
||||
"deleteTitle": "Supprimer"
|
||||
},
|
||||
"goalTree": {
|
||||
"importPlaceholder": "- Breuvage de rage x10\n - Élixir de destruction = base * 3\n - Poudre de flamme x5\n - Sève de cèdre x7\n - Catalyseur magique x10",
|
||||
"importOpenButton": "Importer du texte",
|
||||
"exportOpenButton": "Exporter en format texte",
|
||||
"importModalTitle": "Importer un arbre d'objectifs",
|
||||
"exportModalTitle": "Exporter un arbre d'objectifs",
|
||||
"importButton": "Importer le texte",
|
||||
"exportButton": "Copier le texte",
|
||||
"importSuccess": "Import d'arbre terminé.",
|
||||
"exportSuccess": "Export d'arbre copié.",
|
||||
"importError": "Format d'arbre d'objectifs invalide.",
|
||||
"editorTitle": "Édition de l'arbre d'objectifs",
|
||||
"treeTitle": "Arbre d'objectifs",
|
||||
"addRootButton": "Objectif",
|
||||
"addRootSchemaButton": "Ajouter un objectif a la racine",
|
||||
"addChildButton": "Sous-étape",
|
||||
"addChildTitle": "Ajouter une sous-étape",
|
||||
"addContentItemTitle": "Ajouter une entrée",
|
||||
"addContentItemTooltip": "Ajouter une entrée interne à cette carte.",
|
||||
"verticalViewTitle": "Afficher verticalement",
|
||||
"horizontalViewTitle": "Afficher horizontalement",
|
||||
"cardActionsLabel": "Actions de l'objectif",
|
||||
"editTitle": "Modifier",
|
||||
"baseTab": "Base",
|
||||
"contentTab": "Contenu",
|
||||
"advancedTab": "Avancé",
|
||||
"defaultRootTitle": "Nouvel objectif",
|
||||
"defaultChildTitle": "Nouvelle étape",
|
||||
"resetTimeLabel": "Heure de reset",
|
||||
"weeklyResetDayLabel": "Jour de reset",
|
||||
"titleLabel": "Titre de l'objectif",
|
||||
"listTitleLabel": "Titre de la liste",
|
||||
"kindLabel": "Type de l'objectif",
|
||||
"stepKind": "Étape",
|
||||
"textKind": "Texte",
|
||||
"checklistKind": "Checklist",
|
||||
"iconLabel": "Icône",
|
||||
"iconColorLabel": "Couleur de l'icône",
|
||||
"iconcube": "Cube",
|
||||
"iconfire": "Feu",
|
||||
"iconflask": "Fiole",
|
||||
"icongrass": "Herbe",
|
||||
"iconleaf": "Feuille",
|
||||
"iconmeat": "Viande",
|
||||
"iconpowder": "Poudre",
|
||||
"icontear": "Goutte",
|
||||
"iconball": "Boule",
|
||||
"colorgold": "Or",
|
||||
"colorcyan": "Cyan",
|
||||
"colorviolet": "Violet",
|
||||
"colorpink": "Rose",
|
||||
"colorgreen": "Vert",
|
||||
"colorgray": "Gris",
|
||||
"colorred": "Rouge",
|
||||
"colororange": "Orange",
|
||||
"coloryellow": "Jaune",
|
||||
"colorblue": "Bleu",
|
||||
"colorindigo": "Indigo",
|
||||
"targetQuantityLabel": "Cible",
|
||||
"quantitySectionTitle": "Quantité",
|
||||
"progressSectionTitle": "Progression",
|
||||
"autoProgressLabel": "Automatique",
|
||||
"manualProgressLabel": "Manuelle",
|
||||
"autoProgressTitle": "Progression automatique",
|
||||
"manualProgressTitle": "Mise à jour manuelle",
|
||||
"autoProgressHint": "Calcule la progression depuis les enfants.",
|
||||
"manualProgressHint": "Affiche une checkbox ou une quantité à mettre à jour directement.",
|
||||
"contentSectionTitle": "Contenu",
|
||||
"itemLabelLabel": "Libellé",
|
||||
"itemLabelPlaceholder": "Élément",
|
||||
"itemCheckedLabel": "État",
|
||||
"itemTotalLabel": "Total",
|
||||
"itemTotalPlaceholder": "base",
|
||||
"itemTotalHelp": "Saisissez un nombre ou une formule avec base pour référencer la cible du parent.",
|
||||
"prerequisitesSectionTitle": "Prérequis supplémentaires",
|
||||
"contentTabHint": "Le contenu se modifie dans l'onglet Contenu.",
|
||||
"stepContentUnavailable": "Les étapes n'ont pas de contenu interne.",
|
||||
"decrementItemTitle": "Retirer",
|
||||
"incrementItemTitle": "Ajouter",
|
||||
"addContentItemButton": "Ajouter une ligne",
|
||||
"formulaPlaceholder": "base * 3",
|
||||
"formulaHelp": "Utilisez base pour référencer la cible du parent.",
|
||||
"resetTypeLabel": "Type de reset",
|
||||
"noneReset": "Aucun",
|
||||
"dailyReset": "Quotidien",
|
||||
"weeklyReset": "Hebdomadaire",
|
||||
"closeButton": "Fermer",
|
||||
"deleteButton": "Supprimer la branche",
|
||||
"deleteTitle": "Supprimer",
|
||||
"reorderTitle": "Déplacer",
|
||||
"showChildrenTitle": "Afficher la branche",
|
||||
"hideChildrenTitle": "Réduire la branche",
|
||||
"emptySelection": "Sélectionnez ou créez un objectif.",
|
||||
"emptyTree": "Aucun objectif."
|
||||
},
|
||||
"table": {
|
||||
"importPlaceholder": "Collez un TSV pur ou un TSV exporté par Sokko G.",
|
||||
"importOpenButton": "Importer TSV",
|
||||
|
|
|
|||
|
|
@ -3,22 +3,22 @@
|
|||
{
|
||||
"id": "default",
|
||||
"label": "Toolbox",
|
||||
"src": "/static/img/toolbox-icons/default.svg"
|
||||
"src": "/static/icons/toolbox.svg"
|
||||
},
|
||||
{
|
||||
"id": "controller",
|
||||
"label": "Controller",
|
||||
"src": "/static/img/toolbox-icons/controller.svg"
|
||||
"src": "/static/icons/controller.svg"
|
||||
},
|
||||
{
|
||||
"id": "quest",
|
||||
"label": "Quest",
|
||||
"src": "/static/img/toolbox-icons/quest.svg"
|
||||
"src": "/static/icons/tasklist.svg"
|
||||
},
|
||||
{
|
||||
"id": "target",
|
||||
"label": "Target",
|
||||
"src": "/static/img/toolbox-icons/target.svg"
|
||||
"src": "/static/icons/diagram.svg"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 821 B After Width: | Height: | Size: 821 B |
|
Before Width: | Height: | Size: 923 B After Width: | Height: | Size: 923 B |
4
website/public/static/icons/diagram.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 16 16" xmlns="http://www.w3.org/2000/svg" fill="#000000" class="bi bi-diagram-3">
|
||||
<path fill-rule="evenodd" d="M6 3.5A1.5 1.5 0 0 1 7.5 2h1A1.5 1.5 0 0 1 10 3.5v1A1.5 1.5 0 0 1 8.5 6v1H14a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-1 0V8h-5v.5a.5.5 0 0 1-1 0V8h-5v.5a.5.5 0 0 1-1 0v-1A.5.5 0 0 1 2 7h5.5V6A1.5 1.5 0 0 1 6 4.5v-1zM8.5 5a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1zM0 11.5A1.5 1.5 0 0 1 1.5 10h1A1.5 1.5 0 0 1 4 11.5v1A1.5 1.5 0 0 1 2.5 14h-1A1.5 1.5 0 0 1 0 12.5v-1zm1.5-.5a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-1zm4.5.5A1.5 1.5 0 0 1 7.5 10h1a1.5 1.5 0 0 1 1.5 1.5v1A1.5 1.5 0 0 1 8.5 14h-1A1.5 1.5 0 0 1 6 12.5v-1zm1.5-.5a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-1zm4.5.5a1.5 1.5 0 0 1 1.5-1.5h1a1.5 1.5 0 0 1 1.5 1.5v1a1.5 1.5 0 0 1-1.5 1.5h-1a1.5 1.5 0 0 1-1.5-1.5v-1zm1.5-.5a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-1z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
4
website/public/static/icons/flask.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="M10 4V10L5.20285 16.8531C4.27496 18.1786 5.22327 20 6.84131 20H17.1587C18.7767 20 19.725 18.1786 18.7972 16.8531L14 10V4M10 4H14M10 4H8M14 4H16" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 467 B |
6
website/public/static/icons/grass.svg
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<?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" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>grass</title>
|
||||
<path d="M5,2c.82.82,5.61,2.88,6,18,0,.65,0,1.31,0,2h3C14,2.12,7,2,5,2ZM19,5a7,7,0,0,0-5.56,3,16.3,16.3,0,0,1,1.12,4C15.74,6.79,18.39,5.61,19,5ZM2,9c.77.77,4.72,3.09,5,12,0,.32,0,.66,0,1h3C10,13.52,7.5,9.22,2,9ZM22,9c-2,0-7,2.25-7,13h3C18,13,21.12,9.88,22,9Z"/>
|
||||
<rect width="24" height="24" fill="none"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 552 B |
5
website/public/static/icons/leaf.svg
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<?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="M4.44893 17.009C-0.246384 7.83762 7.34051 0.686125 19.5546 3.61245C20.416 3.81881 21.0081 4.60984 20.965 5.49452C20.5862 13.288 17.0341 17.7048 6.13252 17.9857C5.43022 18.0038 4.76908 17.6344 4.44893 17.009Z" stroke="#323232" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M3.99999 21C5.50005 15.5 6 12.5 12 9.99997" stroke="#323232" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 668 B |
49
website/public/static/icons/meat.svg
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg height="800px" width="800px" version="1.1" id="_x32_" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 0 512 512" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#000000;}
|
||||
</style>
|
||||
<g>
|
||||
<path class="st0" d="M110.26,202.977c-4.636-3.246-10.289-5.311-16.284-5.303c-6.003-0.008-11.664,2.057-16.3,5.303
|
||||
c-6.979,4.912-12.054,12.192-15.584,20.725c-3.514,8.548-5.433,18.464-5.45,29.061c0.032,14.128,3.416,27.036,9.516,37.13
|
||||
c3.058,5.035,6.849,9.395,11.517,12.648c4.636,3.254,10.297,5.327,16.3,5.312c5.995,0.016,11.648-2.058,16.284-5.312
|
||||
c6.996-4.888,12.063-12.192,15.584-20.708c3.514-8.549,5.45-18.48,5.45-29.07c-0.024-14.128-3.4-27.045-9.5-37.13
|
||||
C118.719,210.597,114.93,206.23,110.26,202.977z M105.185,279.791c-1.936,3.181-4.083,5.45-6.018,6.8
|
||||
c-1.968,1.334-3.571,1.806-5.19,1.806c-1.627,0-3.229-0.472-5.189-1.806c-2.928-1.992-6.296-6.198-8.703-12.16
|
||||
c-2.457-5.929-4.002-13.478-3.986-21.668c-0.033-10.923,2.774-20.708,6.654-27.036c1.936-3.156,4.083-5.441,6.027-6.783
|
||||
c1.968-1.35,3.57-1.814,5.197-1.83c1.619,0.016,3.222,0.48,5.19,1.83c2.912,1.976,6.279,6.189,8.703,12.144
|
||||
c2.44,5.945,3.986,13.486,3.986,21.676C111.879,263.686,109.073,273.455,105.185,279.791z"/>
|
||||
<path class="st0" d="M512,223.221c0-17.105-6.963-32.729-18.204-43.93c-11.176-11.208-26.808-18.203-43.922-18.187
|
||||
c-16.674-0.016-31.949,6.646-43.044,17.382c-7.499,7.223-13.127,16.39-16.25,26.662h-21.994c-1.464-1.521-3.124-3.277-4.88-5.164
|
||||
c-3.92-4.23-8.247-9.11-10.655-12.233c-12.623-16.284-24.328-31.339-37.334-44.947c-19.44-20.432-42.279-37.488-73.562-48.567
|
||||
c-31.298-11.118-70.502-16.634-124.34-16.65c-5.726,0-11.623,0.065-17.706,0.195v0.016C84.298,77.856,69.576,84.2,57.432,94.155
|
||||
c-9.239,7.516-17.284,17.089-24.279,28.183C22.66,138.997,14.47,159.185,8.784,181.821C3.123,204.473,0,229.549,0,256
|
||||
c0.041,47.037,9.761,89.747,26.508,122.046c8.402,16.146,18.618,29.769,30.924,39.823c12.16,9.939,26.882,16.291,42.702,16.356
|
||||
c6.385,0.13,12.558,0.187,18.553,0.187c37.936,0,68.396-2.481,93.814-7.434c19.066-3.726,35.292-8.858,49.477-15.438
|
||||
c21.302-9.826,37.765-22.921,51.837-37.871c13.949-14.794,25.784-31.249,38.992-49.062l16.072-17.732h21.701
|
||||
c3.123,10.273,8.751,19.44,16.25,26.67c11.095,10.728,26.37,17.382,43.044,17.382c17.113,0,32.746-6.987,43.922-18.195
|
||||
c11.241-11.2,18.204-26.816,18.204-43.93c0-12.022-3.514-23.262-9.468-32.788C508.486,246.492,512,235.243,512,223.221z
|
||||
M127.292,396.819c-2.863,2.131-5.726,3.758-8.573,5.19c-0.016,0-0.024,0-0.032,0c-5.774,0-11.737-0.065-17.91-0.171l-0.162-0.025
|
||||
h-0.162c-7.32-0.016-14.641-2.709-22.563-9.085c-5.888-4.774-11.843-11.64-17.316-20.334c-8.24-13.054-15.381-30.225-20.351-50.079
|
||||
c-4.969-19.87-7.816-42.417-7.816-66.314c-0.049-42.442,9.094-80.743,22.855-107.081c6.832-13.176,14.771-23.286,22.628-29.638
|
||||
c7.922-6.386,15.243-9.061,22.563-9.093h0.179h0.163c5.856-0.131,11.534-0.188,17.023-0.188c0.309,0,0.61,0.008,0.92,0.008
|
||||
c2.838,1.424,5.694,3.066,8.556,5.189c13.534,9.988,26.613,28.102,36,52.365c9.435,24.239,15.275,54.512,15.258,88.438
|
||||
c0.032,45.256-10.411,83.98-25.759,110.585C145.138,379.908,136.305,390.141,127.292,396.819z M347.666,276.887
|
||||
c-3.4,6.515-8.459,13.9-15.421,22.148l-4.522,4.994l-0.472,0.635c-1.138,1.26-2.163,2.472-3.383,3.766l0.342,0.341
|
||||
c-10.753,14.535-20.261,27.427-29.957,38.286c-7.962,8.938-16.056,16.584-25.32,23.181c-13.901,9.874-30.583,17.65-54.431,23.189
|
||||
c-15.991,3.7-35.284,6.271-58.611,7.572c11.404-12.762,21.05-29.127,28.542-48.331c10.565-27.207,16.796-60.14,16.796-96.668
|
||||
c-0.017-48.712-11.029-90.992-28.769-121.884c-4.962-8.622-10.501-16.251-16.479-22.97c17.332,1.115,32.469,2.994,45.686,5.62
|
||||
c16.61,3.286,30.274,7.686,42.051,13.014c17.666,8.044,31.242,18.146,44.264,31.022c12.168,12.014,23.799,26.622,36.846,43.442
|
||||
l-0.951,0.903c8.735,9.167,16.584,17.537,21.92,25.581c5.368,8.085,8.215,15.446,8.231,23.677
|
||||
C354.027,260.717,352.319,267.949,347.666,276.887z M470.648,267.582c5.548,5.474,8.931,12.86,8.948,21.221
|
||||
c-0.017,8.264-3.302,15.576-8.704,21.017c-5.433,5.4-12.753,8.694-21.017,8.703c-8.036-0.008-15.178-3.139-20.546-8.296
|
||||
c-5.352-5.189-8.752-12.249-9.061-20.203l-0.618-15.544h-49.876c2.408-6.913,3.709-13.624,3.693-20.074
|
||||
c0.016-5.962-1.026-11.558-2.75-16.861h48.932l0.618-15.544c0.309-7.963,3.709-15.015,9.061-20.196
|
||||
c5.368-5.166,12.51-8.289,20.546-8.297c8.264,0.008,15.584,3.302,21.017,8.703c5.401,5.424,8.687,12.746,8.704,21.009
|
||||
c-0.017,8.361-3.4,15.747-8.931,21.213l-11.826,11.582L470.648,267.582z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.6 KiB |
50
website/public/static/icons/powder.svg
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
viewBox="0 0 189.99999 190.00002"
|
||||
version="1.1"
|
||||
id="svg5"
|
||||
width="190"
|
||||
height="190"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs5" />
|
||||
<path
|
||||
d="m 22.960347,162.04213 q -7,0 -10,-7 -2.9999996,-7 3,-13 l 60,-61.999999 q 20,-21 40.000003,0 l 60,61.999999 q 6,6 3,13 -3,7 -10,7 z"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="14"
|
||||
stroke-linejoin="round"
|
||||
stroke-linecap="round"
|
||||
id="path1" />
|
||||
<circle
|
||||
cx="95.96035"
|
||||
cy="94.04213"
|
||||
r="7"
|
||||
fill="currentColor"
|
||||
id="circle1" />
|
||||
<circle
|
||||
cx="75.96035"
|
||||
cy="114.04213"
|
||||
r="7"
|
||||
fill="currentColor"
|
||||
id="circle2" />
|
||||
<circle
|
||||
cx="115.96036"
|
||||
cy="114.04213"
|
||||
r="7"
|
||||
fill="currentColor"
|
||||
id="circle3" />
|
||||
<circle
|
||||
cx="63.960346"
|
||||
cy="134.04213"
|
||||
r="7"
|
||||
fill="currentColor"
|
||||
id="circle4" />
|
||||
<circle
|
||||
cx="127.96036"
|
||||
cy="134.04213"
|
||||
r="7"
|
||||
fill="currentColor"
|
||||
id="circle5" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
4
website/public/static/icons/rotate.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 fill="#000000" width="800px" height="800px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.7071068,4 L17.5,4 C18.8807119,4 20,5.11928813 20,6.5 L20,9.29289322 L21.1464466,8.14644661 C21.3417088,7.95118446 21.6582912,7.95118446 21.8535534,8.14644661 C22.0488155,8.34170876 22.0488155,8.65829124 21.8535534,8.85355339 L19.8535534,10.8535534 C19.6582912,11.0488155 19.3417088,11.0488155 19.1464466,10.8535534 L17.1464466,8.85355339 C16.9511845,8.65829124 16.9511845,8.34170876 17.1464466,8.14644661 C17.3417088,7.95118446 17.6582912,7.95118446 17.8535534,8.14644661 L19,9.29289322 L19,6.5 C19,5.67157288 18.3284271,5 17.5,5 L14.7071068,5 L15.8535534,6.14644661 C16.0488155,6.34170876 16.0488155,6.65829124 15.8535534,6.85355339 C15.6582912,7.04881554 15.3417088,7.04881554 15.1464466,6.85355339 L13.1464466,4.85355339 C12.9511845,4.65829124 12.9511845,4.34170876 13.1464466,4.14644661 L15.1464466,2.14644661 C15.3417088,1.95118446 15.6582912,1.95118446 15.8535534,2.14644661 C16.0488155,2.34170876 16.0488155,2.65829124 15.8535534,2.85355339 L14.7071068,4 Z M4.5,2 L9.5,2 C10.8807119,2 12,3.11928813 12,4.5 L12,15.5 C12,16.8807119 10.8807119,18 9.5,18 L4.5,18 C3.11928813,18 2,16.8807119 2,15.5 L2,4.5 C2,3.11928813 3.11928813,2 4.5,2 Z M4.5,3 C3.67157288,3 3,3.67157288 3,4.5 L3,15.5 C3,16.3284271 3.67157288,17 4.5,17 L9.5,17 C10.3284271,17 11,16.3284271 11,15.5 L11,4.5 C11,3.67157288 10.3284271,3 9.5,3 L4.5,3 Z M16.5,13 C16.2238576,13 16,12.7761424 16,12.5 C16,12.2238576 16.2238576,12 16.5,12 L17.5,12 C17.7761424,12 18,12.2238576 18,12.5 C18,12.7761424 17.7761424,13 17.5,13 L16.5,13 Z M13.5,13 C13.2238576,13 13,12.7761424 13,12.5 C13,12.2238576 13.2238576,12 13.5,12 L14.5,12 C14.7761424,12 15,12.2238576 15,12.5 C15,12.7761424 14.7761424,13 14.5,13 L13.5,13 Z M16.5,22 C16.2238576,22 16,21.7761424 16,21.5 C16,21.2238576 16.2238576,21 16.5,21 L17.5,21 C17.7761424,21 18,21.2238576 18,21.5 C18,21.7761424 17.7761424,22 17.5,22 L16.5,22 Z M13.5,22 C13.2238576,22 13,21.7761424 13,21.5 C13,21.2238576 13.2238576,21 13.5,21 L14.5,21 C14.7761424,21 15,21.2238576 15,21.5 C15,21.7761424 14.7761424,22 14.5,22 L13.5,22 Z M10.5,22 C10.2238576,22 10,21.7761424 10,21.5 C10,21.2238576 10.2238576,21 10.5,21 L11.5,21 C11.7761424,21 12,21.2238576 12,21.5 C12,21.7761424 11.7761424,22 11.5,22 L10.5,22 Z M19.5,13 C19.2238576,13 19,12.7761424 19,12.5 C19,12.2238576 19.2238576,12 19.5,12 C20.8807119,12 22,13.1192881 22,14.5 C22,14.7761424 21.7761424,15 21.5,15 C21.2238576,15 21,14.7761424 21,14.5 C21,13.6715729 20.3284271,13 19.5,13 Z M21,16.5 C21,16.2238576 21.2238576,16 21.5,16 C21.7761424,16 22,16.2238576 22,16.5 L22,17.5 C22,17.7761424 21.7761424,18 21.5,18 C21.2238576,18 21,17.7761424 21,17.5 L21,16.5 Z M21,19.5 C21,19.2238576 21.2238576,19 21.5,19 C21.7761424,19 22,19.2238576 22,19.5 C22,20.8807119 20.8807119,22 19.5,22 C19.2238576,22 19,21.7761424 19,21.5 C19,21.2238576 19.2238576,21 19.5,21 C20.3284271,21 21,20.3284271 21,19.5 Z M8.5,21 C8.77614237,21 9,21.2238576 9,21.5 C9,21.7761424 8.77614237,22 8.5,22 C7.11928813,22 6,20.8807119 6,19.5 C6,19.2238576 6.22385763,19 6.5,19 C6.77614237,19 7,19.2238576 7,19.5 C7,20.3284271 7.67157288,21 8.5,21 Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
2
website/public/static/icons/tear.svg
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg fill="#000000" width="800px" height="800px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" data-name="Layer 1"><path d="M12.56,2.17a1,1,0,0,0-1.12,0c-.3.2-7.19,5-7.19,12.08a7.75,7.75,0,0,0,15.5,0C19.75,7.05,12.85,2.36,12.56,2.17ZM12,20a5.76,5.76,0,0,1-5.75-5.75c0-5,4.21-8.77,5.75-10,1.55,1.21,5.75,5,5.75,10A5.76,5.76,0,0,1,12,20Z"/></svg>
|
||||
|
After Width: | Height: | Size: 472 B |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
|
|
@ -1,4 +0,0 @@
|
|||
<?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 fill-rule="evenodd" clip-rule="evenodd" d="M10.1548 3.74068C9.13306 4.24326 7.70274 4.75901 5.07021 4.93689C4.43314 4.97993 4.01738 5.47217 4.03533 5.965C4.13502 8.70059 4.45127 11.4217 5.0553 13.6538C5.67349 15.9383 6.53465 17.4879 7.55469 18.1679C9.1615 19.2392 10.4837 20.1681 11.3705 20.8038C11.7447 21.072 12.2553 21.072 12.6294 20.8038C13.5162 20.1681 14.8385 19.2392 16.4453 18.1679C17.4653 17.4879 18.3265 15.9383 18.9447 13.6539C19.5487 11.4218 19.865 8.70066 19.9646 5.965C19.9826 5.47217 19.5668 4.97993 18.9298 4.93689C16.2972 4.75901 14.8669 4.24326 13.8452 3.74068C13.5533 3.5971 13.3276 3.47746 13.1423 3.37923C12.6352 3.11045 12.4305 3.00195 12 3.00195C11.5694 3.00195 11.3647 3.11045 10.8577 3.37923C10.6724 3.47746 10.4467 3.5971 10.1548 3.74068ZM9.68627 1.72824C10.3021 1.39456 11.0268 1.00195 12 1.00195C12.9732 1.00195 13.6978 1.39456 14.3137 1.72825C14.4571 1.80594 14.5946 1.88044 14.728 1.94605C15.503 2.32729 16.6774 2.78014 19.0646 2.94144C20.6316 3.04732 22.0258 4.32298 21.9633 6.03782C21.8605 8.8586 21.5333 11.7445 20.8752 14.1763C20.2313 16.5559 19.2117 18.7274 17.5547 19.8321C15.97 20.8885 14.6667 21.8042 13.7947 22.4293C12.724 23.1969 11.276 23.1969 10.2053 22.4293C9.33329 21.8042 8.02992 20.8885 6.44528 19.8321C4.78826 18.7274 3.76867 16.5559 3.12474 14.1763C2.46666 11.7444 2.13944 8.85853 2.03666 6.03783C1.97417 4.32298 3.36833 3.04732 4.93538 2.94144C7.32256 2.78014 8.49696 2.32729 9.27198 1.94605C9.40536 1.88044 9.54287 1.80594 9.68627 1.72824Z" fill="#0F0F0F"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.7 KiB |
224
website/src/components/DiagramConnectors.jsx
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
// Rôle : dessine des liaisons SVG entre éléments DOM mesurés dans un conteneur scrollable.
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
const MAX_LAYOUT_RETRIES = 12;
|
||||
|
||||
function attributeSelector(name, value) {
|
||||
return `[${name}="${String(value).replaceAll("\\", "\\\\").replaceAll("\"", "\\\"")}"]`;
|
||||
}
|
||||
|
||||
function buildConnectorPath(source, target, orientation = "vertical") {
|
||||
if (orientation === "horizontal") {
|
||||
const startX = source.left + source.width;
|
||||
const startY = source.top + source.height / 2;
|
||||
const endX = target.left;
|
||||
const endY = target.top + target.height / 2;
|
||||
const midX = startX + Math.max(18, (endX - startX) / 2);
|
||||
return `M ${startX} ${startY} H ${midX} V ${endY} H ${endX}`;
|
||||
}
|
||||
const startX = source.left + source.width / 2;
|
||||
const startY = source.top + source.height;
|
||||
const endX = target.left + target.width / 2;
|
||||
const endY = target.top;
|
||||
const midY = startY + Math.max(18, (endY - startY) / 2);
|
||||
return `M ${startX} ${startY} V ${midY} H ${endX} V ${endY}`;
|
||||
}
|
||||
|
||||
function buildHorizontalDirectConnectorPath(source, target, preferSourceEnd = false) {
|
||||
const sourceCenterX = source.left + source.width / 2;
|
||||
const targetCenterX = target.left + target.width / 2;
|
||||
const sourceBeforeTarget = preferSourceEnd || sourceCenterX <= targetCenterX;
|
||||
const startX = sourceBeforeTarget ? source.left + source.width : source.left;
|
||||
const startY = source.top + source.height / 2;
|
||||
const endX = sourceBeforeTarget ? target.left : target.left + target.width;
|
||||
const endY = target.top + target.height / 2;
|
||||
const distance = Math.abs(endX - startX);
|
||||
const midX = sourceBeforeTarget
|
||||
? startX + Math.max(18, distance / 2)
|
||||
: startX - Math.max(18, distance / 2);
|
||||
return `M ${startX} ${startY} H ${midX} V ${endY} H ${endX}`;
|
||||
}
|
||||
|
||||
function buildVerticalDirectConnectorPath(source, target, preferSourceEnd = false) {
|
||||
const sourceCenterY = source.top + source.height / 2;
|
||||
const targetCenterY = target.top + target.height / 2;
|
||||
const sourceAboveTarget = preferSourceEnd || sourceCenterY <= targetCenterY;
|
||||
const startX = source.left + source.width / 2;
|
||||
const startY = sourceAboveTarget ? source.top + source.height : source.top;
|
||||
const endX = target.left + target.width / 2;
|
||||
const endY = sourceAboveTarget ? target.top : target.top + target.height;
|
||||
const distance = Math.abs(endY - startY);
|
||||
const midY = sourceAboveTarget
|
||||
? startY + Math.max(18, distance / 2)
|
||||
: startY - Math.max(18, distance / 2);
|
||||
return `M ${startX} ${startY} V ${midY} H ${endX} V ${endY}`;
|
||||
}
|
||||
|
||||
function buildDirectConnectorPath(source, target, orientation = "vertical") {
|
||||
if (orientation === "horizontal") {
|
||||
return buildHorizontalDirectConnectorPath(source, target, true);
|
||||
}
|
||||
return buildVerticalDirectConnectorPath(source, target, true);
|
||||
}
|
||||
|
||||
function buildGroupedConnectorPath(source, targets, orientation = "vertical") {
|
||||
if (targets.length === 1) return buildConnectorPath(source, targets[0], orientation);
|
||||
if (orientation === "horizontal") {
|
||||
const startX = source.left + source.width;
|
||||
const startY = source.top + source.height / 2;
|
||||
const targetPoints = targets.map((target) => ({
|
||||
x: target.left,
|
||||
y: target.top + target.height / 2
|
||||
}));
|
||||
const leftTargetX = Math.min(...targetPoints.map((target) => target.x));
|
||||
const midX = startX + Math.max(18, (leftTargetX - startX) / 2);
|
||||
const minY = Math.min(startY, ...targetPoints.map((target) => target.y));
|
||||
const maxY = Math.max(startY, ...targetPoints.map((target) => target.y));
|
||||
const childBranches = targetPoints.map((target) => `M ${midX} ${target.y} H ${target.x}`).join(" ");
|
||||
return `M ${startX} ${startY} H ${midX} M ${midX} ${minY} V ${maxY} ${childBranches}`;
|
||||
}
|
||||
const startX = source.left + source.width / 2;
|
||||
const startY = source.top + source.height;
|
||||
const targetPoints = targets.map((target) => ({
|
||||
x: target.left + target.width / 2,
|
||||
y: target.top
|
||||
}));
|
||||
const topTargetY = Math.min(...targetPoints.map((target) => target.y));
|
||||
const midY = startY + Math.max(18, (topTargetY - startY) / 2);
|
||||
const minX = Math.min(startX, ...targetPoints.map((target) => target.x));
|
||||
const maxX = Math.max(startX, ...targetPoints.map((target) => target.x));
|
||||
const childBranches = targetPoints.map((target) => `M ${target.x} ${midY} V ${target.y}`).join(" ");
|
||||
return `M ${startX} ${startY} V ${midY} M ${minX} ${midY} H ${maxX} ${childBranches}`;
|
||||
}
|
||||
|
||||
export function DiagramConnectors({ containerRef, edges, nodeAttribute = "data-diagram-node", className = "", stroke = "currentColor", refreshKey = "", scale = 1, orientation = "vertical" }) {
|
||||
const [layout, setLayout] = useState({ width: 0, height: 0, paths: [] });
|
||||
|
||||
useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
if (!container) return undefined;
|
||||
let frameId = 0;
|
||||
let settleFrameId = 0;
|
||||
let cancelled = false;
|
||||
const observedElements = new Set();
|
||||
|
||||
function observeCurrentNodes(resizeObserver) {
|
||||
container.querySelectorAll(`[${nodeAttribute}]`).forEach((element) => {
|
||||
if (observedElements.has(element)) return;
|
||||
observedElements.add(element);
|
||||
resizeObserver.observe(element);
|
||||
});
|
||||
}
|
||||
|
||||
function measure(resizeObserver) {
|
||||
observeCurrentNodes(resizeObserver);
|
||||
const normalizedScale = Number.isFinite(scale) && scale > 0 ? scale : 1;
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
const rects = new Map();
|
||||
const expectedNodeIds = new Set();
|
||||
let hasIncompleteNode = false;
|
||||
edges.forEach((edge) => {
|
||||
[edge.from, edge.to].forEach((nodeId) => {
|
||||
expectedNodeIds.add(nodeId);
|
||||
if (rects.has(nodeId)) return;
|
||||
const element = container.querySelector(attributeSelector(nodeAttribute, nodeId));
|
||||
if (!element) {
|
||||
hasIncompleteNode = true;
|
||||
return;
|
||||
}
|
||||
const rect = element.getBoundingClientRect();
|
||||
if (!rect.width || !rect.height) hasIncompleteNode = true;
|
||||
rects.set(nodeId, {
|
||||
left: (rect.left - containerRect.left) / normalizedScale + container.scrollLeft,
|
||||
top: (rect.top - containerRect.top) / normalizedScale + container.scrollTop,
|
||||
width: rect.width / normalizedScale,
|
||||
height: rect.height / normalizedScale
|
||||
});
|
||||
});
|
||||
});
|
||||
const width = Math.max(container.scrollWidth, container.offsetWidth, container.clientWidth);
|
||||
const height = Math.max(container.scrollHeight, container.offsetHeight, container.clientHeight);
|
||||
const groupedEdges = edges.filter((edge) => edge.grouped !== false);
|
||||
const directEdges = edges.filter((edge) => edge.grouped === false);
|
||||
const edgesBySource = new Map();
|
||||
groupedEdges.forEach((edge) => {
|
||||
edgesBySource.set(edge.from, [...(edgesBySource.get(edge.from) || []), edge.to]);
|
||||
});
|
||||
const paths = [
|
||||
...Array.from(edgesBySource.entries())
|
||||
.map(([sourceId, targetIds]) => {
|
||||
const source = rects.get(sourceId);
|
||||
const targets = targetIds.map((targetId) => rects.get(targetId)).filter(Boolean);
|
||||
return source && targets.length ? { id: `${sourceId}:${targetIds.join(":")}`, d: buildGroupedConnectorPath(source, targets, orientation) } : null;
|
||||
}),
|
||||
...directEdges.map((edge) => {
|
||||
const source = rects.get(edge.from);
|
||||
const target = rects.get(edge.to);
|
||||
return source && target ? { id: `${edge.from}:${edge.to}:direct`, d: buildDirectConnectorPath(source, target, orientation) } : null;
|
||||
})
|
||||
].filter(Boolean);
|
||||
setLayout({
|
||||
width,
|
||||
height,
|
||||
paths
|
||||
});
|
||||
return !hasIncompleteNode && rects.size === expectedNodeIds.size;
|
||||
}
|
||||
|
||||
function scheduleMeasure(attempt = 0) {
|
||||
cancelAnimationFrame(frameId);
|
||||
frameId = requestAnimationFrame(() => {
|
||||
const complete = measure(resizeObserver);
|
||||
if (!cancelled && !complete && attempt < MAX_LAYOUT_RETRIES) scheduleMeasure(attempt + 1);
|
||||
});
|
||||
}
|
||||
|
||||
function scheduleSettledMeasure() {
|
||||
cancelAnimationFrame(settleFrameId);
|
||||
settleFrameId = requestAnimationFrame(() => {
|
||||
settleFrameId = requestAnimationFrame(() => {
|
||||
if (!cancelled) measure(resizeObserver);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const resizeObserver = new ResizeObserver(scheduleMeasure);
|
||||
const mutationObserver = new MutationObserver(() => {
|
||||
observeCurrentNodes(resizeObserver);
|
||||
scheduleMeasure();
|
||||
});
|
||||
resizeObserver.observe(container);
|
||||
observeCurrentNodes(resizeObserver);
|
||||
mutationObserver.observe(container, { childList: true, subtree: true, attributes: true, attributeFilter: [nodeAttribute, "class", "style"] });
|
||||
container.addEventListener("scroll", scheduleMeasure, { passive: true });
|
||||
window.addEventListener("resize", scheduleMeasure);
|
||||
scheduleMeasure();
|
||||
scheduleSettledMeasure();
|
||||
document.fonts?.ready.then(() => {
|
||||
if (!cancelled) scheduleSettledMeasure();
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
cancelAnimationFrame(frameId);
|
||||
cancelAnimationFrame(settleFrameId);
|
||||
resizeObserver.disconnect();
|
||||
mutationObserver.disconnect();
|
||||
container.removeEventListener("scroll", scheduleMeasure);
|
||||
window.removeEventListener("resize", scheduleMeasure);
|
||||
};
|
||||
}, [containerRef, edges, nodeAttribute, refreshKey, scale, orientation]);
|
||||
|
||||
return (
|
||||
<svg
|
||||
className={`diagram-connectors ${className}`.trim()}
|
||||
width={layout.width || 1}
|
||||
height={layout.height || 1}
|
||||
viewBox={`0 0 ${layout.width || 1} ${layout.height || 1}`}
|
||||
style={{ width: `${layout.width || 1}px`, height: `${layout.height || 1}px` }}
|
||||
aria-hidden="true"
|
||||
>
|
||||
{layout.paths.map((path) => <path key={path.id} d={path.d} fill="none" stroke={stroke} />)}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
|
@ -17,7 +17,7 @@ export function getCategoryLabel(category) {
|
|||
|
||||
export function getCategoryIconStyle(category) {
|
||||
const icon = typeof category === "string" ? "" : category?.icon;
|
||||
return icon ? { "--diablo4-category-icon": `url("/static/img/diablo4/${icon}.svg")` } : undefined;
|
||||
return icon ? { "--diablo4-category-icon": `url("/static/icons/${icon}.svg")` } : undefined;
|
||||
}
|
||||
|
||||
export function getFilteredDiablo4Affixes({ affixes, filters }) {
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import {
|
|||
normalizeCombosData,
|
||||
normalizeCountersData,
|
||||
normalizeEquipmentPlannerData,
|
||||
normalizeGoalTreeData,
|
||||
normalizeImageAnnotationData,
|
||||
normalizeLinksData,
|
||||
normalizeModuleOrder,
|
||||
|
|
@ -175,7 +176,7 @@ function shouldShowTutorial(toolbox, pendingTutorialToolboxId) {
|
|||
return toolbox?.tutorial === true || pendingTutorialToolboxId === toolbox?.id;
|
||||
}
|
||||
|
||||
function ToolboxView({ siteContent, toolbox, toolboxGame, embedded, actions, storageUsage, getModuleData, updateToolbox, updateModuleData, addImageFiles, pendingTutorialToolboxId = "", setPendingTutorialToolboxId }) {
|
||||
function ToolboxView({ siteContent, toolbox, toolboxGame, embedded, actions, storageUsage, storageReady = true, getModuleData, updateToolbox, updateModuleData, addImageFiles, pendingTutorialToolboxId = "", setPendingTutorialToolboxId }) {
|
||||
const toolboxId = toolbox.id;
|
||||
const toolboxTutorial = toolbox.tutorial;
|
||||
const moduleColumns = embedded || Number(toolbox.moduleColumns) === 1 ? 1 : 2;
|
||||
|
|
@ -185,12 +186,14 @@ function ToolboxView({ siteContent, toolbox, toolboxGame, embedded, actions, sto
|
|||
const currentToolboxIdRef = useRef(toolboxId);
|
||||
const moduleContext = {
|
||||
getModuleData,
|
||||
storageReady,
|
||||
moduleText,
|
||||
normalizeChecklistData,
|
||||
normalizeCombosData,
|
||||
normalizeLinksData,
|
||||
normalizeCountersData,
|
||||
normalizeEquipmentPlannerData,
|
||||
normalizeGoalTreeData,
|
||||
normalizeCalculatorData,
|
||||
normalizeCalendarData,
|
||||
normalizeImageAnnotationData,
|
||||
|
|
@ -329,7 +332,7 @@ function ToolboxView({ siteContent, toolbox, toolboxGame, embedded, actions, sto
|
|||
);
|
||||
}
|
||||
|
||||
export function ToolboxDrawer({ gameId, game, siteContent, toolboxes, links, actions, storageUsage, getModuleData, updateToolbox, updateModuleData, addImageFiles }) {
|
||||
export function ToolboxDrawer({ gameId, game, siteContent, toolboxes, links, actions, storageUsage, storageReady = true, getModuleData, updateToolbox, updateModuleData, addImageFiles }) {
|
||||
const selectedId = links[gameId] || "";
|
||||
const toolbox = toolboxes.find((item) => item.id === selectedId);
|
||||
const [width, setWidth] = useState("");
|
||||
|
|
@ -389,7 +392,7 @@ export function ToolboxDrawer({ gameId, game, siteContent, toolboxes, links, act
|
|||
</div>
|
||||
<button className="drawer-close-button" onClick={() => actions.setDrawerGameId("")} aria-label="Fermer" title="Fermer"><Icon name="close" /></button>
|
||||
</header>
|
||||
{toolbox ? <ToolboxView siteContent={siteContent} toolbox={toolbox} toolboxGame={game} embedded actions={actions} storageUsage={storageUsage} getModuleData={getModuleData} updateToolbox={updateToolbox} updateModuleData={updateModuleData} addImageFiles={addImageFiles} /> : (
|
||||
{toolbox ? <ToolboxView siteContent={siteContent} toolbox={toolbox} toolboxGame={game} embedded actions={actions} storageUsage={storageUsage} storageReady={storageReady} getModuleData={getModuleData} updateToolbox={updateToolbox} updateModuleData={updateModuleData} addImageFiles={addImageFiles} /> : (
|
||||
<div className="empty"><p>Aucune toolbox associée à cette page jeu.</p><button className="primary" onClick={() => actions.setCreateModal({ gameId })}>Créer et associer</button></div>
|
||||
)}
|
||||
<StorageQuota usage={storageUsage} />
|
||||
|
|
|
|||
|
|
@ -116,7 +116,7 @@ function FormattedCalculatorNumber({ value }) {
|
|||
});
|
||||
}
|
||||
|
||||
export function CalculatorModule({ toolboxId, moduleId, context, editing }) {
|
||||
export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpen }) {
|
||||
const data = context.normalizeCalculatorData(context.getModuleData(toolboxId, moduleId, { entries: [] }));
|
||||
const calculatedEntries = useMemo(() => evaluateCalculatorEntries(data.entries), [data.entries]);
|
||||
const calculatedData = useMemo(() => ({ ...data, entries: calculatedEntries }), [data, calculatedEntries]);
|
||||
|
|
@ -235,8 +235,8 @@ export function CalculatorModule({ toolboxId, moduleId, context, editing }) {
|
|||
|
||||
return (
|
||||
<div className="calculator-module">
|
||||
{editing && (
|
||||
<div className="module-add-panel calculator-exchange-panel">
|
||||
{textExchangeOpen && (
|
||||
<div className="module-text-exchange-panel">
|
||||
<TextExchangeActions type="calculator" data={calculatedData} context={context} textContent={textContent} onImport={(nextData) => context.setModuleData(toolboxId, moduleId, nextData, "calculator")} />
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -289,7 +289,7 @@ export function CalculatorModule({ toolboxId, moduleId, context, editing }) {
|
|||
</div>
|
||||
<div className={`tool-split-tree calculator-tree ${data.scrollResults ? "is-scrollable legacy-scrollbar" : ""}`}>
|
||||
{calculatedEntries.length ? (
|
||||
<CalculatorEntries entries={calculatedEntries} parentId="" getScopedParentId={scopeParentId} activeParentId={activeParentId} textContent={textContent} editing={editing} reorder={reorder} onUse={useEntry} onRename={renameEntry} onUpdateFormula={updateEntryFormula} onDelete={deleteEntry} />
|
||||
<CalculatorEntries entries={calculatedEntries} parentId="" getScopedParentId={scopeParentId} activeParentId={activeParentId} textContent={textContent} reorder={reorder} onUse={useEntry} onRename={renameEntry} onUpdateFormula={updateEntryFormula} onDelete={deleteEntry} />
|
||||
) : (
|
||||
<p className="muted">{textContent.emptyResults || "Aucun résultat enregistré."}</p>
|
||||
)}
|
||||
|
|
@ -299,7 +299,7 @@ export function CalculatorModule({ toolboxId, moduleId, context, editing }) {
|
|||
);
|
||||
}
|
||||
|
||||
function CalculatorEntries({ entries, parentId, getScopedParentId, activeParentId, textContent, editing, reorder, onUse, onRename, onUpdateFormula, onDelete }) {
|
||||
function CalculatorEntries({ entries, parentId, getScopedParentId, activeParentId, textContent, reorder, onUse, onRename, onUpdateFormula, onDelete }) {
|
||||
const children = getChildren(entries, parentId);
|
||||
const [editingId, setEditingId] = useState("");
|
||||
useEffect(() => {
|
||||
|
|
@ -342,8 +342,8 @@ function CalculatorEntries({ entries, parentId, getScopedParentId, activeParentI
|
|||
</button>
|
||||
{entry.id === editingId ? (
|
||||
<span className="calculator-entry-edit-fields" data-calculator-edit-fields={entry.id}>
|
||||
<EditableCalculatorFormula entry={entry} entries={entries} textContent={textContent} onUpdateFormula={onUpdateFormula} autoFocus={editing} />
|
||||
<EditableCalculatorLabel entry={entry} textContent={textContent} onRename={onRename} autoFocus={!editing} />
|
||||
<EditableCalculatorFormula entry={entry} entries={entries} textContent={textContent} onUpdateFormula={onUpdateFormula} />
|
||||
<EditableCalculatorLabel entry={entry} textContent={textContent} onRename={onRename} />
|
||||
</span>
|
||||
) : (
|
||||
<button className="tool-split-entry-summary" type="button" onClick={() => onUse(entry)} title={textContent.useEntryTitle || "Utiliser comme base"}>
|
||||
|
|
@ -362,7 +362,7 @@ function CalculatorEntries({ entries, parentId, getScopedParentId, activeParentI
|
|||
<Icon name="trash" />
|
||||
</button>
|
||||
</div>
|
||||
<CalculatorEntries entries={entries} parentId={entry.id} getScopedParentId={getScopedParentId} activeParentId={activeParentId} textContent={textContent} editing={editing} reorder={reorder} onUse={onUse} onRename={onRename} onUpdateFormula={onUpdateFormula} onDelete={onDelete} />
|
||||
<CalculatorEntries entries={entries} parentId={entry.id} getScopedParentId={getScopedParentId} activeParentId={activeParentId} textContent={textContent} reorder={reorder} onUse={onUse} onRename={onRename} onUpdateFormula={onUpdateFormula} onDelete={onDelete} />
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -306,7 +306,7 @@ function getMonthOccurrenceSegments(days, occurrences) {
|
|||
});
|
||||
}
|
||||
|
||||
export function CalendarModule({ toolboxId, moduleId, context, editing = false, setEditing }) {
|
||||
export function CalendarModule({ toolboxId, moduleId, context, editing = false, setEditing, textExchangeOpen }) {
|
||||
const data = context.normalizeCalendarData(context.getModuleData(toolboxId, moduleId, { initialized: false, viewMode: "week", events: [] }));
|
||||
const textContent = context.moduleText?.calendar || {};
|
||||
const [now, setNow] = useState(() => new Date());
|
||||
|
|
@ -780,8 +780,8 @@ export function CalendarModule({ toolboxId, moduleId, context, editing = false,
|
|||
</div>
|
||||
{error && <p className="timer-form-error calendar-form-error" role="alert"><Icon name="warning" /><span>{error}</span></p>}
|
||||
</form>}
|
||||
{editing && (
|
||||
<div className="calendar-exchange-panel">
|
||||
{textExchangeOpen && (
|
||||
<div className="module-text-exchange-panel">
|
||||
<TextExchangeActions type="calendar" data={data} context={context} textContent={textContent} onImport={(nextData) => save(nextData)} />
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { useGroupedReorder, moveItem } from "../../../hooks/useGroupedReorder.js
|
|||
import { useInlineEdit } from "../../../hooks/useInlineEdit.js";
|
||||
import { TextExchangeActions } from "./TextExchangeActions.jsx";
|
||||
|
||||
export function ChecklistModule({ toolboxId, moduleId, context, editing }) {
|
||||
export function ChecklistModule({ toolboxId, moduleId, context, editing, textExchangeOpen }) {
|
||||
const data = context.normalizeChecklistData(context.getModuleData(toolboxId, moduleId, { items: [] }));
|
||||
const textContent = context.moduleText?.checklist || {};
|
||||
const [label, setLabel] = useState("");
|
||||
|
|
@ -117,6 +117,10 @@ export function ChecklistModule({ toolboxId, moduleId, context, editing }) {
|
|||
<input className="checklist-qty-input" name="qty" type="number" min="1" value={qty} onChange={(event) => setQty(event.target.value)} aria-label={textContent.quantityLabel || "Quantité cible"} />
|
||||
<button className="primary">{textContent.addButton || "Ajouter"}</button>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
{textExchangeOpen && (
|
||||
<div className="module-text-exchange-panel">
|
||||
<TextExchangeActions type="checklist" data={data} context={context} textContent={textContent} onImport={(nextData) => context.setModuleData(toolboxId, moduleId, nextData, "checklist")} />
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import {
|
|||
setComboItemCategory
|
||||
} from "./combosUtils.js";
|
||||
|
||||
export function CombosModule({ toolboxId, moduleId, context, editing }) {
|
||||
export function CombosModule({ toolboxId, moduleId, context, editing, textExchangeOpen }) {
|
||||
const data = context.normalizeCombosData(context.getModuleData(toolboxId, moduleId, { combos: [] }));
|
||||
const textContent = context.moduleText?.combos || {};
|
||||
const [comboCategory, setComboCategory] = useState("");
|
||||
|
|
@ -589,9 +589,11 @@ export function CombosModule({ toolboxId, moduleId, context, editing }) {
|
|||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{editing && !editingComboId && (
|
||||
<TextExchangeActions type="combos" data={data} context={context} textContent={textContent} onImport={(nextData) => save(nextData)} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{textExchangeOpen && (
|
||||
<div className="module-text-exchange-panel">
|
||||
<TextExchangeActions type="combos" data={data} context={context} textContent={textContent} onImport={(nextData) => save(nextData)} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { useDraftForm } from "../../../hooks/useDraftForm.js";
|
|||
import { moveItem, useGroupedReorder } from "../../../hooks/useGroupedReorder.js";
|
||||
import { TextExchangeActions } from "./TextExchangeActions.jsx";
|
||||
|
||||
export function CountersModule({ toolboxId, moduleId, context, editing }) {
|
||||
export function CountersModule({ toolboxId, moduleId, context, editing, textExchangeOpen }) {
|
||||
const data = context.normalizeCountersData(context.getModuleData(toolboxId, moduleId, { counters: [] }));
|
||||
const textContent = context.moduleText?.counters || {};
|
||||
const counterDraft = useDraftForm({ label: "" });
|
||||
|
|
@ -50,6 +50,10 @@ export function CountersModule({ toolboxId, moduleId, context, editing }) {
|
|||
<input {...counterDraft.getFieldProps("label", { placeholder: textContent.labelPlaceholder || "Nom du compteur" })} />
|
||||
<button className="primary">{textContent.addButton || "Ajouter"}</button>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
{textExchangeOpen && (
|
||||
<div className="module-text-exchange-panel">
|
||||
<TextExchangeActions type="counters" data={data} context={context} textContent={textContent} onImport={(nextData) => context.setModuleData(toolboxId, moduleId, nextData, "counters")} />
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { EquipmentItem } from "./EquipmentPlannerItem.jsx";
|
|||
import { IconChoiceDropdown } from "./EquipmentPlannerPickers.jsx";
|
||||
import { EquipmentPlannerSummary } from "./EquipmentPlannerSummary.jsx";
|
||||
|
||||
export function EquipmentPlannerModule({ toolboxId, moduleId, context, editing }) {
|
||||
export function EquipmentPlannerModule({ toolboxId, moduleId, context, editing, textExchangeOpen }) {
|
||||
const data = context.normalizeEquipmentPlannerData(context.getModuleData(toolboxId, moduleId, { types: [], equipments: [] }));
|
||||
const summary = useMemo(() => context.summarizeEquipmentPlannerData(data), [context, data]);
|
||||
const textContent = context.moduleText?.equipmentPlanner || {};
|
||||
|
|
@ -112,6 +112,10 @@ export function EquipmentPlannerModule({ toolboxId, moduleId, context, editing }
|
|||
<input value={equipmentName} onChange={(event) => setEquipmentName(event.target.value)} placeholder={textContent.equipmentPlaceholder || "Nouvel équipement"} />
|
||||
<button className="primary">{textContent.addEquipmentButton || "Ajouter"}</button>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
{textExchangeOpen && (
|
||||
<div className="module-text-exchange-panel">
|
||||
<TextExchangeActions type="equipmentPlanner" data={data} context={context} textContent={textContent} onImport={(nextData) => save(nextData)} />
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
1020
website/src/features/toolboxes/modules/GoalTreeModule.jsx
Normal file
|
|
@ -9,7 +9,7 @@ function markerLabel(index, marker, textContent) {
|
|||
return marker.label || `${textContent.markerPrefix || "Marqueur"} ${index + 1}`;
|
||||
}
|
||||
|
||||
export function ImageAnnotationModule({ toolboxId, moduleId, context, editing }) {
|
||||
export function ImageAnnotationModule({ toolboxId, moduleId, context, editing, textExchangeOpen }) {
|
||||
const data = context.normalizeImageAnnotationData(context.getModuleData(toolboxId, moduleId, { image: "", markers: [] }));
|
||||
const textContent = context.moduleText?.imageAnnotation || {};
|
||||
const previewMediaRef = useRef(null);
|
||||
|
|
@ -105,6 +105,10 @@ export function ImageAnnotationModule({ toolboxId, moduleId, context, editing })
|
|||
>
|
||||
{pastePlaceholder}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{textExchangeOpen && (
|
||||
<div className="module-text-exchange-panel">
|
||||
<TextExchangeActions type="imageAnnotation" data={data} context={context} textContent={textContent} onImport={(nextData) => {
|
||||
save(nextData);
|
||||
setTemporaryDrawings({ strokes: [] });
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { Icon } from "../../../components/Icon.jsx";
|
|||
import { moveItem, useGroupedReorder } from "../../../hooks/useGroupedReorder.js";
|
||||
import { TextExchangeActions } from "./TextExchangeActions.jsx";
|
||||
|
||||
export function ImagesModule({ toolboxId, moduleId, context, editing }) {
|
||||
export function ImagesModule({ toolboxId, moduleId, context, editing, textExchangeOpen }) {
|
||||
const data = context.getModuleData(toolboxId, moduleId, { images: [] });
|
||||
const textContent = context.moduleText?.images || {};
|
||||
const [dragOver, setDragOver] = useState(false);
|
||||
|
|
@ -86,6 +86,10 @@ export function ImagesModule({ toolboxId, moduleId, context, editing }) {
|
|||
>
|
||||
{pastePlaceholder}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{textExchangeOpen && (
|
||||
<div className="module-text-exchange-panel">
|
||||
<TextExchangeActions type="images" data={data} context={context} textContent={textContent} onImport={(nextData) => context.setModuleData(toolboxId, moduleId, nextData, "images")} />
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { useDraftForm } from "../../../hooks/useDraftForm.js";
|
|||
import { moveItem, useGroupedReorder } from "../../../hooks/useGroupedReorder.js";
|
||||
import { TextExchangeActions } from "./TextExchangeActions.jsx";
|
||||
|
||||
export function LinksModule({ toolboxId, moduleId, context, editing }) {
|
||||
export function LinksModule({ toolboxId, moduleId, context, editing, textExchangeOpen }) {
|
||||
const data = context.normalizeLinksData(context.getModuleData(toolboxId, moduleId, { links: [] }));
|
||||
const textContent = context.moduleText?.links || {};
|
||||
const linkDraft = useDraftForm({ title: "", url: "" });
|
||||
|
|
@ -63,6 +63,10 @@ export function LinksModule({ toolboxId, moduleId, context, editing }) {
|
|||
<input {...linkDraft.getFieldProps("url", { placeholder: textContent.urlPlaceholder || "https://..." })} />
|
||||
<button className="primary">{textContent.addButton || "Ajouter"}</button>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
{textExchangeOpen && (
|
||||
<div className="module-text-exchange-panel">
|
||||
<TextExchangeActions type="links" data={data} context={context} textContent={textContent} onImport={(nextData) => context.setModuleData(toolboxId, moduleId, nextData, "links")} />
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -145,7 +145,7 @@ function ListTypeDropdown({ textContent, activeFormats, onCommand }) {
|
|||
);
|
||||
}
|
||||
|
||||
export function NotepadModule({ toolboxId, moduleId, context, editing = false }) {
|
||||
export function NotepadModule({ toolboxId, moduleId, context, textExchangeOpen }) {
|
||||
const storedData = context.getModuleData(toolboxId, moduleId, { text: "" });
|
||||
const normalizedData = useMemo(() => context.normalizeNotepadData(storedData), [context, storedData]);
|
||||
const textContent = context.moduleText?.notepad || {};
|
||||
|
|
@ -360,8 +360,8 @@ export function NotepadModule({ toolboxId, moduleId, context, editing = false })
|
|||
|
||||
return (
|
||||
<div className="notepad-module">
|
||||
{editing && (
|
||||
<div className="module-add-panel notepad-exchange-panel">
|
||||
{textExchangeOpen && (
|
||||
<div className="module-text-exchange-panel">
|
||||
<TextExchangeActions type="notepad" data={normalizedData} context={context} textContent={textContent} onImport={(nextData) => context.setModuleData(toolboxId, moduleId, nextData, "notepad")} />
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ import {
|
|||
pasteCellBlock
|
||||
} from "./tableUtils.js";
|
||||
|
||||
export function TableModule({ toolboxId, moduleId, context, editing }) {
|
||||
export function TableModule({ toolboxId, moduleId, context, textExchangeOpen }) {
|
||||
const data = context.normalizeTableData(context.getModuleData(toolboxId, moduleId, { rows: DEFAULT_ROWS, columns: DEFAULT_COLUMNS, cells: {} }));
|
||||
const textContent = context.moduleText?.table || {};
|
||||
const [editingCell, setEditingCell] = useState("");
|
||||
|
|
@ -406,8 +406,8 @@ export function TableModule({ toolboxId, moduleId, context, editing }) {
|
|||
|
||||
return (
|
||||
<div className="table-module">
|
||||
{editing && (
|
||||
<div className="module-add-panel table-exchange-panel">
|
||||
{textExchangeOpen && (
|
||||
<div className="module-text-exchange-panel">
|
||||
<TextExchangeActions type="table" data={data} context={context} textContent={textContent} exportLabel="Exporter TSV" onImport={(nextData) => context.setModuleData(toolboxId, moduleId, nextData, "table")} />
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import {
|
|||
moveTask
|
||||
} from "./taskPlannerUtils.js";
|
||||
|
||||
export function TaskPlannerModule({ toolboxId, moduleId, context, editing }) {
|
||||
export function TaskPlannerModule({ toolboxId, moduleId, context, editing, textExchangeOpen }) {
|
||||
const data = context.normalizeTaskPlannerData(context.getModuleData(toolboxId, moduleId, { tasks: [] }));
|
||||
const textContent = context.moduleText?.taskPlanner || {};
|
||||
const taskDraft = useDraftForm({ title: "", type: "daily" });
|
||||
|
|
@ -314,6 +314,10 @@ export function TaskPlannerModule({ toolboxId, moduleId, context, editing }) {
|
|||
</select>
|
||||
<button className="primary">{textContent.addButton || "Ajouter"}</button>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
{textExchangeOpen && (
|
||||
<div className="module-text-exchange-panel">
|
||||
<TextExchangeActions type="taskPlanner" data={data} context={context} textContent={textContent} onImport={(nextData) => save(nextData)} />
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ import {
|
|||
isAlertGuarded
|
||||
} from "./timerOptions.js";
|
||||
|
||||
export function TimerModule({ toolboxId, moduleId, context, editing }) {
|
||||
export function TimerModule({ toolboxId, moduleId, context, textExchangeOpen }) {
|
||||
const data = context.normalizeTimerData(context.getModuleData(toolboxId, moduleId, { activeTab: "stopwatch" }));
|
||||
const textContent = context.moduleText?.timer || {};
|
||||
const activeTab = data.activeTab;
|
||||
|
|
@ -247,8 +247,8 @@ export function TimerModule({ toolboxId, moduleId, context, editing }) {
|
|||
|
||||
return (
|
||||
<div className="timer-module">
|
||||
{editing && (
|
||||
<div className="module-add-panel timer-exchange-panel">
|
||||
{textExchangeOpen && (
|
||||
<div className="module-text-exchange-panel">
|
||||
<TextExchangeActions type="timer" data={data} context={context} textContent={textContent} onImport={(nextData) => save(nextData)} />
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
269
website/src/features/toolboxes/modules/goalTreeUtils.js
Normal file
|
|
@ -0,0 +1,269 @@
|
|||
// Rôle : regroupe les calculs purs de l'arbre d'objectifs.
|
||||
export const GOAL_TREE_KINDS = ["step", "text", "checklist"];
|
||||
export const GOAL_TREE_RESET_TYPES = ["none", "daily", "weekly"];
|
||||
export const GOAL_TREE_ICON_NAMES = ["cube", "fire", "flask", "grass", "leaf", "meat", "powder", "tear", "ball"];
|
||||
export const GOAL_TREE_COLORS = ["gold", "cyan", "violet", "pink", "green", "gray", "red", "orange", "yellow", "blue", "indigo"];
|
||||
export const GOAL_TREE_COLOR_VALUES = {
|
||||
gold: "#f6c453",
|
||||
cyan: "#22d3ee",
|
||||
violet: "#8b5cf6",
|
||||
pink: "#d946ef",
|
||||
green: "#34d399",
|
||||
gray: "#94a3b8",
|
||||
red: "#ef4444",
|
||||
orange: "#f97316",
|
||||
yellow: "#eab308",
|
||||
blue: "#3b82f6",
|
||||
indigo: "#6366f1"
|
||||
};
|
||||
export const GOAL_TREE_WEEK_DAYS = [
|
||||
{ value: 1, label: "Lundi" },
|
||||
{ value: 2, label: "Mardi" },
|
||||
{ value: 3, label: "Mercredi" },
|
||||
{ value: 4, label: "Jeudi" },
|
||||
{ value: 5, label: "Vendredi" },
|
||||
{ value: 6, label: "Samedi" },
|
||||
{ value: 0, label: "Dimanche" }
|
||||
];
|
||||
|
||||
function formatResult(value) {
|
||||
if (!Number.isFinite(value)) return "";
|
||||
return Number.parseFloat(value.toFixed(6)).toString();
|
||||
}
|
||||
|
||||
export function calculateGoalTreeFormula(expression, baseValue = null, { clamp = true } = {}) {
|
||||
const normalized = String(expression || "").replaceAll(",", ".").trim();
|
||||
if (!normalized) return null;
|
||||
const withBase = normalized.replace(/\bbase\b/gi, () => Number.isFinite(baseValue) ? `(${formatResult(baseValue)})` : "NaN");
|
||||
if (!/^[\d\s+\-*/().]+$/.test(withBase)) return null;
|
||||
|
||||
try {
|
||||
const value = Function(`"use strict"; return (${withBase})`)();
|
||||
if (!Number.isFinite(value)) return null;
|
||||
return clamp ? Math.max(0, value) : value;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function formatGoalTreeNumber(value) {
|
||||
if (!Number.isFinite(value)) return "0";
|
||||
return new Intl.NumberFormat("fr-FR", { maximumFractionDigits: 2 }).format(Number.parseFloat(value.toFixed(2)));
|
||||
}
|
||||
|
||||
export function getGoalTreeChildren(nodes, parentId) {
|
||||
return nodes.filter((node) => (node.parentId || "") === parentId);
|
||||
}
|
||||
|
||||
export function getGoalTreeParentIds(node) {
|
||||
return [...new Set([node?.parentId || "", ...(Array.isArray(node?.parentIds) ? node.parentIds : [])].filter(Boolean))];
|
||||
}
|
||||
|
||||
export function getGoalTreeDescendantIds(nodes, parentId) {
|
||||
return getGoalTreeChildren(nodes, parentId).flatMap((child) => [child.id, ...getGoalTreeDescendantIds(nodes, child.id)]);
|
||||
}
|
||||
|
||||
export function getGoalTreeParentMap(nodes) {
|
||||
const ids = new Set(nodes.map((node) => node.id));
|
||||
return new Map(nodes.map((node) => [node.id, ids.has(node.parentId) ? node.parentId : ""]));
|
||||
}
|
||||
|
||||
function isBaseScaledFormula(expression) {
|
||||
const normalized = String(expression || "").replaceAll(",", ".").trim();
|
||||
return /\bbase\b/i.test(normalized) && /^[\d\s*/().base]+$/i.test(normalized);
|
||||
}
|
||||
|
||||
export function evaluateGoalTreeNodes(nodes) {
|
||||
const nodeMap = new Map(nodes.map((node) => [node.id, node]));
|
||||
const childrenByParent = new Map();
|
||||
nodes.forEach((node) => {
|
||||
const parentId = node.parentId || "";
|
||||
childrenByParent.set(parentId, [...(childrenByParent.get(parentId) || []), node.id]);
|
||||
});
|
||||
const computedTargets = new Map();
|
||||
const progressMap = new Map();
|
||||
|
||||
function computeTarget(nodeId, seen = new Set()) {
|
||||
if (computedTargets.has(nodeId)) return computedTargets.get(nodeId);
|
||||
if (seen.has(nodeId)) return 1;
|
||||
const node = nodeMap.get(nodeId);
|
||||
if (!node) return 1;
|
||||
const parentTarget = node.parentId ? computeTarget(node.parentId, new Set([...seen, nodeId])) : null;
|
||||
const formulaTarget = node.formula ? calculateGoalTreeFormula(node.formula, parentTarget) : null;
|
||||
const target = Math.max(0, formulaTarget ?? node.qtyTarget ?? 1);
|
||||
computedTargets.set(nodeId, target);
|
||||
return target;
|
||||
}
|
||||
|
||||
nodes.forEach((node) => computeTarget(node.id));
|
||||
|
||||
function evaluateChecklistItems(node) {
|
||||
const parentTarget = node.parentId ? computedTargets.get(node.parentId) : null;
|
||||
return (node.items || []).map((item) => {
|
||||
const target = calculateGoalTreeFormula(item.formula || "1", parentTarget) ?? item.qtyTarget ?? 1;
|
||||
return { ...item, qtyTarget: Math.max(1, target) };
|
||||
});
|
||||
}
|
||||
|
||||
function getNodeProgressTarget(node) {
|
||||
if (node?.kind === "checklist") {
|
||||
return evaluateChecklistItems(node).reduce((sum, item) => sum + Math.max(1, item.qtyTarget || 1), 0) || 1;
|
||||
}
|
||||
if (node?.kind === "text") return Math.max(1, node.items?.length || 1);
|
||||
return Math.max(1, computedTargets.get(node?.id) ?? node?.qtyTarget ?? 1);
|
||||
}
|
||||
|
||||
function getEffectiveStepProgressMode(node, childIds) {
|
||||
if (node?.kind !== "step") return "auto";
|
||||
if (node?.progressMode) return node.progressMode;
|
||||
if (!childIds.length) return "manual";
|
||||
return childIds.every((childId) => getNodeProgressTarget(nodeMap.get(childId)) <= 1) ? "manual" : "auto";
|
||||
}
|
||||
|
||||
function getBaseScaledRequirementCount(childNode, childProgress, parentTarget) {
|
||||
if (!parentTarget) return null;
|
||||
if (childNode.kind === "checklist") {
|
||||
const items = evaluateChecklistItems(childNode);
|
||||
if (!items.length || !items.every((item) => isBaseScaledFormula(item.formula))) return null;
|
||||
return items.map((item) => {
|
||||
const target = Math.max(1, item.qtyTarget || 1);
|
||||
const ratio = target / parentTarget;
|
||||
if (!ratio) return null;
|
||||
return Math.max(0, item.qtyCurrent || 0) / ratio;
|
||||
});
|
||||
}
|
||||
if (!isBaseScaledFormula(childNode.formula)) return null;
|
||||
const target = Math.max(0, computedTargets.get(childNode.id) ?? childProgress.target ?? 0);
|
||||
const ratio = target / parentTarget;
|
||||
if (!ratio) return null;
|
||||
return [Math.max(0, childProgress.current || 0) / ratio];
|
||||
}
|
||||
|
||||
function computeBaseScaledChildrenProgress(node, childIds, childProgress) {
|
||||
const target = Math.max(1, computedTargets.get(node.id) ?? node.qtyTarget ?? 1);
|
||||
const requirementCounts = [];
|
||||
for (let index = 0; index < childIds.length; index += 1) {
|
||||
const childNode = nodeMap.get(childIds[index]);
|
||||
const counts = childNode ? getBaseScaledRequirementCount(childNode, childProgress[index], target) : null;
|
||||
if (!counts) return null;
|
||||
requirementCounts.push(...counts);
|
||||
}
|
||||
if (!requirementCounts.length) return null;
|
||||
const current = Math.min(target, Math.floor(Math.min(...requirementCounts)));
|
||||
return {
|
||||
current,
|
||||
target,
|
||||
ratio: target ? Math.min(1, current / target) : 0,
|
||||
complete: current >= target
|
||||
};
|
||||
}
|
||||
|
||||
function computeProgress(nodeId, seen = new Set()) {
|
||||
if (progressMap.has(nodeId)) return progressMap.get(nodeId);
|
||||
if (seen.has(nodeId)) return { current: 0, target: 1, ratio: 0, complete: false };
|
||||
const node = nodeMap.get(nodeId);
|
||||
const childIds = childrenByParent.get(nodeId) || [];
|
||||
let progress;
|
||||
if (childIds.length && getEffectiveStepProgressMode(node, childIds) !== "manual") {
|
||||
const childProgress = childIds.map((childId) => computeProgress(childId, new Set([...seen, nodeId])));
|
||||
progress = computeBaseScaledChildrenProgress(node, childIds, childProgress);
|
||||
if (!progress) {
|
||||
const target = childProgress.reduce((sum, item) => sum + item.target, 0);
|
||||
const current = childProgress.reduce((sum, item) => sum + Math.min(item.current, item.target), 0);
|
||||
progress = {
|
||||
current,
|
||||
target: target || 1,
|
||||
ratio: target ? Math.min(1, current / target) : 0,
|
||||
complete: childProgress.length > 0 && childProgress.every((item) => item.complete)
|
||||
};
|
||||
}
|
||||
} else {
|
||||
const target = computedTargets.get(nodeId) ?? 1;
|
||||
const current = Math.max(0, node?.qtyCurrent || 0);
|
||||
if (node?.kind === "checklist") {
|
||||
const items = evaluateChecklistItems(node);
|
||||
const total = items.reduce((sum, item) => sum + Math.max(0, item.qtyTarget || 1), 0);
|
||||
const currentTotal = items.reduce((sum, item) => sum + Math.min(Math.max(0, item.qtyCurrent || 0), Math.max(0, item.qtyTarget || 1)), 0);
|
||||
progress = {
|
||||
current: currentTotal,
|
||||
target: total || 1,
|
||||
ratio: total ? currentTotal / total : 0,
|
||||
complete: items.length > 0 && currentTotal >= total
|
||||
};
|
||||
} else if (node?.kind === "text") {
|
||||
const count = (node.items || []).length;
|
||||
progress = {
|
||||
current: count,
|
||||
target: count || 1,
|
||||
ratio: count ? 1 : 0,
|
||||
complete: count > 0
|
||||
};
|
||||
} else {
|
||||
progress = {
|
||||
current,
|
||||
target,
|
||||
ratio: target ? Math.min(1, current / target) : 1,
|
||||
complete: current >= target
|
||||
};
|
||||
}
|
||||
}
|
||||
progressMap.set(nodeId, progress);
|
||||
return progress;
|
||||
}
|
||||
|
||||
nodes.forEach((node) => computeProgress(node.id));
|
||||
return nodes.map((node) => ({
|
||||
...node,
|
||||
items: node.kind === "checklist" ? evaluateChecklistItems(node) : node.items,
|
||||
computedQtyTarget: computedTargets.get(node.id) ?? node.qtyTarget,
|
||||
progress: progressMap.get(node.id) || { current: 0, target: 1, ratio: 0, complete: false }
|
||||
}));
|
||||
}
|
||||
|
||||
function parseResetTime(value) {
|
||||
const [hours, minutes] = String(value || "00:00").split(":").map((part) => Number.parseInt(part, 10));
|
||||
return {
|
||||
hours: Number.isInteger(hours) ? Math.min(23, Math.max(0, hours)) : 0,
|
||||
minutes: Number.isInteger(minutes) ? Math.min(59, Math.max(0, minutes)) : 0
|
||||
};
|
||||
}
|
||||
|
||||
function getLatestDailyReset(now, resetTime) {
|
||||
const { hours, minutes } = parseResetTime(resetTime);
|
||||
const reset = new Date(now);
|
||||
reset.setHours(hours, minutes, 0, 0);
|
||||
if (reset.getTime() > now) reset.setDate(reset.getDate() - 1);
|
||||
return reset.getTime();
|
||||
}
|
||||
|
||||
function getLatestWeeklyReset(now, resetTime, weekDay) {
|
||||
const { hours, minutes } = parseResetTime(resetTime);
|
||||
const reset = new Date(now);
|
||||
reset.setHours(hours, minutes, 0, 0);
|
||||
const dayDiff = (reset.getDay() - weekDay + 7) % 7;
|
||||
reset.setDate(reset.getDate() - dayDiff);
|
||||
if (reset.getTime() > now) reset.setDate(reset.getDate() - 7);
|
||||
return reset.getTime();
|
||||
}
|
||||
|
||||
export function applyGoalTreeDueResets(data, now = Date.now()) {
|
||||
let latestResetAt = data.lastResetAt || 0;
|
||||
let changed = false;
|
||||
const nodes = data.nodes.map((node) => {
|
||||
if (!["daily", "weekly"].includes(node.resetType)) return node;
|
||||
const resetAt = node.resetType === "daily"
|
||||
? getLatestDailyReset(now, node.resetTime)
|
||||
: getLatestWeeklyReset(now, node.resetTime, node.weeklyResetDay);
|
||||
latestResetAt = Math.max(latestResetAt, resetAt);
|
||||
if (resetAt <= (node.checkedAt || data.lastResetAt || 0)) return node;
|
||||
if (node.kind === "checklist" && node.items?.some((item) => item.qtyCurrent)) {
|
||||
changed = true;
|
||||
return { ...node, items: node.items.map((item) => ({ ...item, qtyCurrent: 0 })), checkedAt: 0 };
|
||||
}
|
||||
if (!node.qtyCurrent) return node;
|
||||
changed = true;
|
||||
return { ...node, qtyCurrent: 0, checkedAt: 0 };
|
||||
});
|
||||
if (latestResetAt > (data.lastResetAt || 0)) changed = true;
|
||||
return changed ? { ...data, lastResetAt: latestResetAt, nodes } : data;
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ import { useGroupedReorder } from "../../../hooks/useGroupedReorder.js";
|
|||
import { useInlineEdit } from "../../../hooks/useInlineEdit.js";
|
||||
import { lockBodyScroll } from "../../../utils/bodyScrollLock.js";
|
||||
import { TOOLBOX_MODULE_DEFINITIONS, TOOLBOX_MODULES } from "../toolboxModuleRegistry.js";
|
||||
import { supportsTextExchange } from "./textImport.js";
|
||||
import { CalculatorModule } from "./CalculatorModule.jsx";
|
||||
import { CalendarModule } from "./CalendarModule.jsx";
|
||||
import { ChecklistModule } from "./ChecklistModule.jsx";
|
||||
|
|
@ -14,6 +15,7 @@ import { CombosModule } from "./CombosModule.jsx";
|
|||
import { CountersModule } from "./CountersModule.jsx";
|
||||
import { ImageAnnotationModule } from "./ImageAnnotationModule.jsx";
|
||||
import { EquipmentPlannerModule } from "./EquipmentPlannerModule.jsx";
|
||||
import { GoalTreeModule } from "./GoalTreeModule.jsx";
|
||||
import { LinksModule } from "./LinksModule.jsx";
|
||||
import { NotepadModule } from "./NotepadModule.jsx";
|
||||
import { ImagesModule } from "./ImagesModule.jsx";
|
||||
|
|
@ -31,6 +33,7 @@ const MODULE_COMPONENTS = {
|
|||
calculator: { ...TOOLBOX_MODULE_DEFINITIONS.calculator, Component: CalculatorModule },
|
||||
calendar: { ...TOOLBOX_MODULE_DEFINITIONS.calendar, Component: CalendarModule },
|
||||
table: { ...TOOLBOX_MODULE_DEFINITIONS.table, Component: TableModule },
|
||||
goalTree: { ...TOOLBOX_MODULE_DEFINITIONS.goalTree, Component: GoalTreeModule },
|
||||
timer: { ...TOOLBOX_MODULE_DEFINITIONS.timer, Component: TimerModule },
|
||||
taskPlanner: { ...TOOLBOX_MODULE_DEFINITIONS.taskPlanner, Component: TaskPlannerModule },
|
||||
equipmentPlanner: { ...TOOLBOX_MODULE_DEFINITIONS.equipmentPlanner, Component: EquipmentPlannerModule },
|
||||
|
|
@ -286,6 +289,9 @@ function ModuleShell({ toolbox, module, context, reorder, itemGroup = "", onRena
|
|||
const Component = definition.Component;
|
||||
const label = definition.label || module.type;
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [textExchangeOpen, setTextExchangeOpen] = useState(false);
|
||||
const hasAddPanel = definition.editable && definition.addable !== false;
|
||||
const hasTextExchange = supportsTextExchange(module.type);
|
||||
const scrollable = definition.scrollable && module.scrollable === true;
|
||||
const className = [
|
||||
"module",
|
||||
|
|
@ -298,6 +304,7 @@ function ModuleShell({ toolbox, module, context, reorder, itemGroup = "", onRena
|
|||
return (
|
||||
<article
|
||||
className={className}
|
||||
data-module-type={module.type}
|
||||
{...reorder.getItemProps({ itemId: module.id, groupId: itemGroup, parentId: toolbox.id })}
|
||||
>
|
||||
<header>
|
||||
|
|
@ -337,10 +344,14 @@ function ModuleShell({ toolbox, module, context, reorder, itemGroup = "", onRena
|
|||
<i aria-hidden="true" />
|
||||
</button>
|
||||
)}
|
||||
{definition.editable && (
|
||||
{hasAddPanel && (
|
||||
<button
|
||||
className={`module-edit-button ${editing ? "active" : ""}`}
|
||||
onClick={() => setEditing((value) => !value)}
|
||||
onClick={() => setEditing((value) => {
|
||||
const nextEditing = !value;
|
||||
if (nextEditing) setTextExchangeOpen(false);
|
||||
return nextEditing;
|
||||
})}
|
||||
aria-label={`${editing ? "Masquer" : "Afficher"} l'ajout de ${module.title || label}`}
|
||||
aria-pressed={editing}
|
||||
title={editing ? "Masquer l'ajout" : "Ajouter"}
|
||||
|
|
@ -348,13 +359,39 @@ function ModuleShell({ toolbox, module, context, reorder, itemGroup = "", onRena
|
|||
<Icon name="add" />
|
||||
</button>
|
||||
)}
|
||||
{hasTextExchange && (
|
||||
<button
|
||||
className={`module-exchange-button ${textExchangeOpen ? "active" : ""}`}
|
||||
onClick={() => setTextExchangeOpen((value) => {
|
||||
const nextOpen = !value;
|
||||
if (nextOpen) setEditing(false);
|
||||
return nextOpen;
|
||||
})}
|
||||
aria-label={`${textExchangeOpen ? "Masquer" : "Afficher"} l'import/export de ${module.title || label}`}
|
||||
aria-pressed={textExchangeOpen}
|
||||
title={textExchangeOpen ? "Masquer import/export" : "Import / export"}
|
||||
>
|
||||
<Icon name="import" />
|
||||
</button>
|
||||
)}
|
||||
<button className="module-delete-button danger" onClick={() => onDelete(module.id)} aria-label={`Retirer ${module.title || label}`} data-toolbox-tutorial-target={module.type === "links" ? "deleteTool" : undefined} title="Retirer">
|
||||
<span className="ui-icon ui-icon-trash" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<div className={`module-content ${scrollable ? "is-scrollable legacy-scrollbar" : ""}`}>
|
||||
<Component toolboxId={toolbox.id} moduleId={module.id} context={context} editing={editing} setEditing={setEditing} />
|
||||
<Component
|
||||
toolboxId={toolbox.id}
|
||||
moduleId={module.id}
|
||||
context={context}
|
||||
editing={editing}
|
||||
setEditing={setEditing}
|
||||
openEditor={() => {
|
||||
setTextExchangeOpen(false);
|
||||
setEditing(true);
|
||||
}}
|
||||
textExchangeOpen={textExchangeOpen}
|
||||
/>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ const SUPPORTED_TEXT_EXCHANGE_TYPES = new Set([
|
|||
"timer",
|
||||
"calendar",
|
||||
"taskPlanner",
|
||||
"goalTree",
|
||||
"equipmentPlanner",
|
||||
"images",
|
||||
"imageAnnotation"
|
||||
|
|
@ -61,6 +62,7 @@ export function hasTextExchangeContent(type, data) {
|
|||
if (type === "timer") return Boolean(data?.stopwatch?.laps?.length || data?.countdowns?.length || data?.stopwatch?.elapsedMs);
|
||||
if (type === "calendar") return Boolean(data?.events?.length);
|
||||
if (type === "taskPlanner") return Boolean(data?.tasks?.length);
|
||||
if (type === "goalTree") return Boolean(data?.nodes?.length);
|
||||
if (type === "equipmentPlanner") return Boolean(data?.equipments?.length);
|
||||
if (type === "images") return Boolean(data?.images?.length);
|
||||
if (type === "imageAnnotation") return Boolean(data?.image);
|
||||
|
|
@ -78,6 +80,7 @@ export function exportModuleText(type, data) {
|
|||
if (type === "timer") return exportTimerText(data);
|
||||
if (type === "calendar") return exportCalendarText(data);
|
||||
if (type === "taskPlanner") return exportTaskPlannerText(data);
|
||||
if (type === "goalTree") return exportGoalTreeText(data);
|
||||
if (type === "equipmentPlanner") return exportEquipmentPlannerText(data);
|
||||
if (type === "images") return exportImagesText(data);
|
||||
if (type === "imageAnnotation") return exportImageAnnotationText(data);
|
||||
|
|
@ -99,6 +102,7 @@ export function importModuleText(type, text, context) {
|
|||
else if (type === "timer") data = importTimerText(value, context);
|
||||
else if (type === "calendar") data = importCalendarText(value, context);
|
||||
else if (type === "taskPlanner") data = importTaskPlannerText(value, context);
|
||||
else if (type === "goalTree") data = importGoalTreeText(value, context);
|
||||
else if (type === "equipmentPlanner") data = importEquipmentPlannerText(value, context);
|
||||
else if (type === "images") data = importImagesText(value, context);
|
||||
else if (type === "imageAnnotation") data = importImageAnnotationText(value, context);
|
||||
|
|
@ -586,6 +590,90 @@ function importTaskPlannerText(text, context) {
|
|||
return context.normalizeTaskPlannerData(data);
|
||||
}
|
||||
|
||||
function exportGoalTreeText(data) {
|
||||
const lines = [];
|
||||
if (data.lastResetAt) lines.push(`@lastResetAt: ${data.lastResetAt}`);
|
||||
function visit(parentId = "", depth = 0) {
|
||||
(data.nodes || []).filter((node) => (node.parentId || "") === parentId).forEach((node) => {
|
||||
const prefix = " ".repeat(depth);
|
||||
const formula = String(node.formula || "").trim();
|
||||
const qty = Number.isFinite(node.computedQtyTarget) ? node.computedQtyTarget : node.qtyTarget;
|
||||
const suffix = node.kind === "step" ? (formula ? ` = ${formula}` : ` x${qty}`) : "";
|
||||
const meta = formatMeta({
|
||||
kind: node.kind !== "step" ? node.kind : "",
|
||||
icon: node.kind === "step" && node.icon && node.icon !== "cube" ? node.icon : "",
|
||||
color: node.kind === "step" && node.color && node.color !== "gold" ? node.color : "",
|
||||
items: node.kind !== "step" && node.items?.length ? encodeURIComponent(JSON.stringify(node.items)) : "",
|
||||
current: node.qtyCurrent || "",
|
||||
reset: node.resetType !== "none" ? node.resetType : "",
|
||||
resetTime: node.resetTime && node.resetTime !== "00:00" ? node.resetTime : "",
|
||||
weeklyResetDay: node.weeklyResetDay !== 1 ? node.weeklyResetDay : "",
|
||||
collapsed: node.collapsed
|
||||
});
|
||||
lines.push(`${prefix}- ${node.title}${suffix}${meta ? ` | ${meta}` : ""}`);
|
||||
visit(node.id, depth + 1);
|
||||
});
|
||||
}
|
||||
visit();
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function importGoalTreeText(text, context) {
|
||||
const nodes = [];
|
||||
const stack = [];
|
||||
const data = { lastResetAt: 0, nodes };
|
||||
splitLines(text).forEach((line, index) => {
|
||||
if (!line.trim()) return;
|
||||
const cleanLine = line.trim();
|
||||
const rootMeta = parseMetaLine(cleanLine);
|
||||
if (rootMeta && !stack.length) {
|
||||
if (rootMeta.key === "lastResetAt") data.lastResetAt = parseInteger(rootMeta.value, 0);
|
||||
return;
|
||||
}
|
||||
const depth = Math.floor((line.match(/^ */)?.[0].length || 0) / 2);
|
||||
const match = cleanLine.match(/^-\s+(.+)$/);
|
||||
if (!match) throw lineError(index, `ligne arbre d'objectifs invalide: ${cleanLine}`);
|
||||
const { title: rawTitle, meta } = parsePipeParts(match[1]);
|
||||
const formulaIndex = rawTitle.lastIndexOf("=");
|
||||
const quantityMatch = rawTitle.match(/\s+x(\d+(?:[,.]\d+)?)\s*$/i);
|
||||
const hasFormula = formulaIndex >= 0;
|
||||
const title = (hasFormula ? rawTitle.slice(0, formulaIndex) : rawTitle.replace(/\s+x(\d+(?:[,.]\d+)?)\s*$/i, "")).trim();
|
||||
if (!title) throw lineError(index, "titre manquant.");
|
||||
const parent = stack[depth - 1];
|
||||
const node = {
|
||||
id: context.uid("goal"),
|
||||
parentId: parent?.id || "",
|
||||
title,
|
||||
kind: meta.kind || "step",
|
||||
icon: meta.icon || "cube",
|
||||
color: meta.color || "gold",
|
||||
qtyTarget: hasFormula ? 1 : Math.max(0, parseNumber(quantityMatch?.[1], 1)) || 1,
|
||||
qtyCurrent: 0,
|
||||
formula: hasFormula ? rawTitle.slice(formulaIndex + 1).trim() : "",
|
||||
items: parseGoalTreeItemsMeta(meta.items),
|
||||
resetType: meta.reset || "none",
|
||||
resetTime: meta.resetTime || "00:00",
|
||||
weeklyResetDay: parseInteger(meta.weeklyResetDay, 1),
|
||||
checkedAt: 0,
|
||||
collapsed: parseBoolean(meta.collapsed)
|
||||
};
|
||||
nodes.push(node);
|
||||
stack[depth] = node;
|
||||
stack.length = depth + 1;
|
||||
});
|
||||
return context.normalizeGoalTreeData(data);
|
||||
}
|
||||
|
||||
function parseGoalTreeItemsMeta(value) {
|
||||
if (!value) return [];
|
||||
try {
|
||||
const parsed = JSON.parse(decodeURIComponent(value));
|
||||
return Array.isArray(parsed) ? parsed : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function exportTimerText(data) {
|
||||
const lines = [];
|
||||
if (data.activeTab && data.activeTab !== "stopwatch") lines.push(`@activeTab: ${data.activeTab}`);
|
||||
|
|
|
|||
163
website/src/features/toolboxes/storage/modules/goalTree.js
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
// Rôle : normalise et compacte les données persistées de l'arbre d'objectifs.
|
||||
import { normalizeTimestamp, uid } from "./shared.js";
|
||||
|
||||
export const DEFAULT_GOAL_TREE_WEEKLY_RESET_DAY = 1;
|
||||
export const DEFAULT_GOAL_TREE_RESET_TIME = "00:00";
|
||||
const NODE_KINDS = new Set(["step", "text", "checklist"]);
|
||||
const NODE_ICONS = new Set(["cube", "fire", "flask", "grass", "leaf", "meat", "powder", "tear", "ball"]);
|
||||
const NODE_COLORS = new Set(["gold", "cyan", "violet", "pink", "green", "gray", "red", "orange", "yellow", "blue", "indigo"]);
|
||||
const RESET_TYPES = new Set(["none", "daily", "weekly"]);
|
||||
const ORIENTATIONS = new Set(["horizontal", "vertical"]);
|
||||
const PROGRESS_MODES = new Set(["auto", "manual"]);
|
||||
|
||||
function isClockTimeString(value) {
|
||||
if (!/^\d{2}:\d{2}$/.test(String(value || ""))) return false;
|
||||
const [hours, minutes] = String(value).split(":").map(Number);
|
||||
return Number.isInteger(hours) && Number.isInteger(minutes) && hours >= 0 && hours <= 23 && minutes >= 0 && minutes <= 59;
|
||||
}
|
||||
|
||||
function normalizeWeekDay(value, fallback = DEFAULT_GOAL_TREE_WEEKLY_RESET_DAY) {
|
||||
const parsed = Number.parseInt(value, 10);
|
||||
return Number.isInteger(parsed) && parsed >= 0 && parsed <= 6 ? parsed : fallback;
|
||||
}
|
||||
|
||||
function normalizeQuantity(value, fallback = 0) {
|
||||
const parsed = Number(String(value ?? "").replace(",", "."));
|
||||
return Number.isFinite(parsed) ? Math.max(0, parsed) : fallback;
|
||||
}
|
||||
|
||||
function normalizeFormula(value) {
|
||||
return String(value || "").replaceAll(",", ".").trim();
|
||||
}
|
||||
|
||||
function normalizeNodeItems(kind, items) {
|
||||
if (!["text", "checklist"].includes(kind)) return [];
|
||||
return (Array.isArray(items) ? items : [])
|
||||
.map((item) => {
|
||||
const label = String(item?.label || item?.text || "");
|
||||
const normalized = { id: item?.id || uid("goalItem"), label };
|
||||
if (kind === "checklist") {
|
||||
normalized.formula = normalizeFormula(item?.formula || item?.qty || item?.qtyTarget || "1") || "1";
|
||||
normalized.qtyTarget = normalizeQuantity(item?.qtyTarget, item?.checked === true ? 1 : 1) || 1;
|
||||
normalized.qtyCurrent = normalizeQuantity(item?.qtyCurrent, item?.checked === true ? 1 : 0);
|
||||
}
|
||||
return normalized;
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeParentIds(value) {
|
||||
return [...new Set((Array.isArray(value) ? value : [])
|
||||
.map((id) => String(id || "").trim())
|
||||
.filter(Boolean))];
|
||||
}
|
||||
|
||||
function normalizeGoalTreeNode(node) {
|
||||
const title = String(node?.title || "") || "Objectif";
|
||||
const kind = NODE_KINDS.has(node?.kind) ? node.kind : "step";
|
||||
const resetType = RESET_TYPES.has(node?.resetType) ? node.resetType : "none";
|
||||
const resetTime = isClockTimeString(node?.resetTime) ? node.resetTime : DEFAULT_GOAL_TREE_RESET_TIME;
|
||||
const isStep = kind === "step";
|
||||
return {
|
||||
id: node?.id || uid("goal"),
|
||||
parentId: String(node?.parentId || ""),
|
||||
parentIds: normalizeParentIds(node?.parentIds),
|
||||
title,
|
||||
kind,
|
||||
icon: NODE_ICONS.has(node?.icon) ? node.icon : "cube",
|
||||
color: NODE_COLORS.has(node?.color) ? node.color : "gold",
|
||||
qtyTarget: isStep ? normalizeQuantity(node?.qtyTarget, 1) || 1 : 1,
|
||||
qtyCurrent: isStep ? normalizeQuantity(node?.qtyCurrent, 0) : 0,
|
||||
formula: isStep ? normalizeFormula(node?.formula) : "",
|
||||
progressMode: isStep && PROGRESS_MODES.has(node?.progressMode) ? node.progressMode : "",
|
||||
items: normalizeNodeItems(kind, node?.items),
|
||||
resetType,
|
||||
resetTime,
|
||||
weeklyResetDay: normalizeWeekDay(node?.weeklyResetDay),
|
||||
checkedAt: normalizeTimestamp(node?.checkedAt),
|
||||
collapsed: node?.collapsed === true
|
||||
};
|
||||
}
|
||||
|
||||
function clearInvalidParents(nodes) {
|
||||
const nodeIds = new Set(nodes.map((node) => node.id));
|
||||
const parentById = new Map(nodes.map((node) => [node.id, nodeIds.has(node.parentId) ? node.parentId : ""]));
|
||||
nodes.forEach((node) => {
|
||||
const visited = new Set([node.id]);
|
||||
let parentId = parentById.get(node.id);
|
||||
while (parentId) {
|
||||
if (visited.has(parentId)) {
|
||||
parentById.set(node.id, "");
|
||||
return;
|
||||
}
|
||||
visited.add(parentId);
|
||||
parentId = parentById.get(parentId);
|
||||
}
|
||||
});
|
||||
const normalizedPrimary = nodes.map((node) => ({ ...node, parentId: parentById.get(node.id) || "" }));
|
||||
const rawParentIdsById = new Map(normalizedPrimary.map((node) => [
|
||||
node.id,
|
||||
[...new Set([node.parentId, ...node.parentIds].filter((parentId) => nodeIds.has(parentId) && parentId !== node.id))]
|
||||
]));
|
||||
function wouldCreateCycle(nodeId, parentId) {
|
||||
const visited = new Set([nodeId]);
|
||||
const stack = [parentId];
|
||||
while (stack.length) {
|
||||
const currentId = stack.pop();
|
||||
if (currentId === nodeId) return true;
|
||||
if (!currentId || visited.has(currentId)) continue;
|
||||
visited.add(currentId);
|
||||
stack.push(...(rawParentIdsById.get(currentId) || []));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return normalizedPrimary.map((node) => ({
|
||||
...node,
|
||||
parentIds: (rawParentIdsById.get(node.id) || []).filter((parentId) => !wouldCreateCycle(node.id, parentId))
|
||||
}));
|
||||
}
|
||||
|
||||
export function normalizeGoalTreeData(data) {
|
||||
const nodes = clearInvalidParents((Array.isArray(data?.nodes) ? data.nodes : []).map(normalizeGoalTreeNode));
|
||||
return {
|
||||
lastResetAt: normalizeTimestamp(data?.lastResetAt),
|
||||
orientation: ORIENTATIONS.has(data?.orientation) ? data.orientation : "horizontal",
|
||||
nodes
|
||||
};
|
||||
}
|
||||
|
||||
export function compactGoalTreeDataForStorage(value) {
|
||||
const normalized = normalizeGoalTreeData(value);
|
||||
const compact = {};
|
||||
if (normalized.lastResetAt) compact.lastResetAt = normalized.lastResetAt;
|
||||
if (normalized.orientation !== "horizontal") compact.orientation = normalized.orientation;
|
||||
if (normalized.nodes.length) {
|
||||
compact.nodes = normalized.nodes.map((node) => {
|
||||
const item = { id: node.id, title: node.title };
|
||||
if (node.parentId) item.parentId = node.parentId;
|
||||
if (node.parentIds.some((parentId) => parentId !== node.parentId)) item.parentIds = node.parentIds;
|
||||
if (node.kind !== "step") item.kind = node.kind;
|
||||
if (node.icon !== "cube") item.icon = node.icon;
|
||||
if (node.color !== "gold") item.color = node.color;
|
||||
if (node.kind === "step" && node.qtyTarget !== 1) item.qtyTarget = node.qtyTarget;
|
||||
if (node.kind === "step" && node.qtyCurrent) item.qtyCurrent = node.qtyCurrent;
|
||||
if (node.kind === "step" && node.formula) item.formula = node.formula;
|
||||
if (node.kind === "step" && node.progressMode) item.progressMode = node.progressMode;
|
||||
if (node.kind !== "step" && node.items.length) item.items = node.items.map((entry) => {
|
||||
const compactEntry = { id: entry.id, label: entry.label };
|
||||
if (node.kind === "checklist") {
|
||||
if (entry.formula !== "1") compactEntry.formula = entry.formula;
|
||||
if (entry.qtyTarget !== 1) compactEntry.qtyTarget = entry.qtyTarget;
|
||||
if (entry.qtyCurrent) compactEntry.qtyCurrent = entry.qtyCurrent;
|
||||
}
|
||||
return compactEntry;
|
||||
});
|
||||
if (node.resetType !== "none") item.resetType = node.resetType;
|
||||
if (node.resetTime !== DEFAULT_GOAL_TREE_RESET_TIME) item.resetTime = node.resetTime;
|
||||
if (node.weeklyResetDay !== DEFAULT_GOAL_TREE_WEEKLY_RESET_DAY) item.weeklyResetDay = node.weeklyResetDay;
|
||||
if (node.checkedAt) item.checkedAt = node.checkedAt;
|
||||
if (node.collapsed) item.collapsed = true;
|
||||
return item;
|
||||
});
|
||||
}
|
||||
return Object.keys(compact).length ? compact : null;
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import { compactChecklistDataForStorage } from "./checklist.js";
|
|||
import { compactCombosDataForStorage } from "./combos.js";
|
||||
import { compactCountersDataForStorage } from "./counters.js";
|
||||
import { compactEquipmentPlannerDataForStorage } from "./equipmentPlanner.js";
|
||||
import { compactGoalTreeDataForStorage } from "./goalTree.js";
|
||||
import { compactImageAnnotationDataForStorage, compactImagesDataForStorage } from "./imageAnnotation.js";
|
||||
import { compactLinksDataForStorage } from "./links.js";
|
||||
import { compactNotepadDataForStorage } from "./notepad.js";
|
||||
|
|
@ -25,5 +26,6 @@ export const MODULE_DATA_COMPACTORS = {
|
|||
table: compactTableDataForStorage,
|
||||
timer: compactTimerDataForStorage,
|
||||
taskPlanner: compactTaskPlannerDataForStorage,
|
||||
goalTree: compactGoalTreeDataForStorage,
|
||||
equipmentPlanner: compactEquipmentPlannerDataForStorage
|
||||
};
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ export const ID_PREFIXES = {
|
|||
marker: "k",
|
||||
timer: "z",
|
||||
task: "a",
|
||||
goal: "g",
|
||||
relation: "e",
|
||||
stroke: "d",
|
||||
equipmentType: "y",
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ export { normalizeCombosData } from "./modules/combos.js";
|
|||
export { normalizeCountersData } from "./modules/counters.js";
|
||||
export { normalizeDrawingData } from "./modules/drawing.js";
|
||||
export { normalizeEquipmentPlannerData, summarizeEquipmentPlannerData } from "./modules/equipmentPlanner.js";
|
||||
export { normalizeGoalTreeData } from "./modules/goalTree.js";
|
||||
export { labelFromFileName, normalizeImageAnnotationData } from "./modules/imageAnnotation.js";
|
||||
export { hostnameFromUrl, normalizeLinksData, normalizeUrl } from "./modules/links.js";
|
||||
export { normalizeNotepadData, sanitizeNotepadHtml } from "./modules/notepad.js";
|
||||
|
|
@ -275,6 +276,22 @@ function remapModuleDataForExport(type, data, nextId) {
|
|||
return remapped;
|
||||
}
|
||||
|
||||
if (type === "goalTree") {
|
||||
const nodeIdMap = new Map();
|
||||
compact.nodes?.forEach((node) => nodeIdMap.set(node.id, nextId("goal")));
|
||||
const remapped = { ...compact };
|
||||
if (compact.nodes) {
|
||||
remapped.nodes = compact.nodes.map((node) => {
|
||||
const nextNode = { ...node, id: nodeIdMap.get(node.id) };
|
||||
if (node.parentId) nextNode.parentId = nodeIdMap.get(node.parentId) || "";
|
||||
if (node.parentIds) nextNode.parentIds = node.parentIds.map((parentId) => nodeIdMap.get(parentId)).filter(Boolean);
|
||||
if (node.items) nextNode.items = node.items.map((item) => ({ ...item, id: nextId("goal") }));
|
||||
return nextNode;
|
||||
});
|
||||
}
|
||||
return remapped;
|
||||
}
|
||||
|
||||
if (type === "equipmentPlanner") {
|
||||
const typeIdMap = new Map();
|
||||
const equipmentIdMap = new Map();
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ export const TOOLBOX_MODULE_DEFINITIONS = {
|
|||
mode: "Texte riche",
|
||||
icon: "notepad",
|
||||
editable: true,
|
||||
addable: false,
|
||||
libraryCategory: "notesTracking"
|
||||
},
|
||||
checklist: {
|
||||
|
|
@ -59,6 +60,7 @@ export const TOOLBOX_MODULE_DEFINITIONS = {
|
|||
mode: "Arborescence",
|
||||
icon: "calculator",
|
||||
editable: true,
|
||||
addable: false,
|
||||
libraryCategory: "calculationData"
|
||||
},
|
||||
table: {
|
||||
|
|
@ -67,6 +69,16 @@ export const TOOLBOX_MODULE_DEFINITIONS = {
|
|||
mode: "Grille + formules",
|
||||
icon: "table",
|
||||
editable: true,
|
||||
addable: false,
|
||||
scrollable: true,
|
||||
libraryCategory: "calculationData"
|
||||
},
|
||||
goalTree: {
|
||||
label: "Arbre d'objectifs",
|
||||
defaultTitle: "Arbre d'objectifs",
|
||||
mode: "Objectifs + quantités",
|
||||
icon: "diagram",
|
||||
editable: true,
|
||||
scrollable: true,
|
||||
libraryCategory: "calculationData"
|
||||
},
|
||||
|
|
@ -76,6 +88,7 @@ export const TOOLBOX_MODULE_DEFINITIONS = {
|
|||
mode: "Chrono + comptes à rebours",
|
||||
icon: "clock",
|
||||
editable: true,
|
||||
addable: false,
|
||||
libraryCategory: "timeRoutines"
|
||||
},
|
||||
calendar: {
|
||||
|
|
@ -123,7 +136,7 @@ export const TOOLBOX_MODULES = Object.fromEntries(Object.entries(TOOLBOX_MODULE_
|
|||
export const LIBRARY_CATEGORY_DEFINITIONS = [
|
||||
{ key: "notesTracking", types: ["notepad", "checklist", "counters"] },
|
||||
{ key: "references", types: ["images", "imageAnnotation", "links"] },
|
||||
{ key: "calculationData", types: ["calculator", "table"] },
|
||||
{ key: "calculationData", types: ["calculator", "table", "goalTree"] },
|
||||
{ key: "timeRoutines", types: ["timer", "calendar", "taskPlanner"] },
|
||||
{ key: "buildsCommands", types: ["combos", "equipmentPlanner"] }
|
||||
];
|
||||
|
|
|
|||
|
|
@ -119,6 +119,7 @@ function App() {
|
|||
getToolboxGame={getToolboxGame}
|
||||
actions={actions}
|
||||
storageUsage={store.storageUsage}
|
||||
storageReady={store.ready}
|
||||
toastPosition={store.toastPosition}
|
||||
setToastPosition={store.setToastPosition}
|
||||
getModuleData={store.getModuleData}
|
||||
|
|
@ -138,6 +139,7 @@ function App() {
|
|||
links={store.links}
|
||||
actions={actions}
|
||||
storageUsage={store.storageUsage}
|
||||
storageReady={store.ready}
|
||||
getModuleData={store.getModuleData}
|
||||
updateToolbox={actions.updateToolbox}
|
||||
updateModuleData={store.updateModuleData}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
normalizeCombosData,
|
||||
normalizeCountersData,
|
||||
normalizeEquipmentPlannerData,
|
||||
normalizeGoalTreeData,
|
||||
normalizeImageAnnotationData,
|
||||
normalizeLinksData,
|
||||
normalizeNotepadData,
|
||||
|
|
@ -34,6 +35,7 @@ const MODULE_COMPONENTS = {
|
|||
calculator: lazy(() => import("../features/toolboxes/modules/CalculatorModule.jsx").then((module) => ({ default: module.CalculatorModule }))),
|
||||
calendar: lazy(() => import("../features/toolboxes/modules/CalendarModule.jsx").then((module) => ({ default: module.CalendarModule }))),
|
||||
table: lazy(() => import("../features/toolboxes/modules/TableModule.jsx").then((module) => ({ default: module.TableModule }))),
|
||||
goalTree: lazy(() => import("../features/toolboxes/modules/GoalTreeModule.jsx").then((module) => ({ default: module.GoalTreeModule }))),
|
||||
timer: lazy(() => import("../features/toolboxes/modules/TimerModule.jsx").then((module) => ({ default: module.TimerModule }))),
|
||||
taskPlanner: lazy(() => import("../features/toolboxes/modules/TaskPlannerModule.jsx").then((module) => ({ default: module.TaskPlannerModule }))),
|
||||
equipmentPlanner: lazy(() => import("../features/toolboxes/modules/EquipmentPlannerModule.jsx").then((module) => ({ default: module.EquipmentPlannerModule }))),
|
||||
|
|
@ -153,6 +155,7 @@ export function LibraryPage({ siteContent, actions }) {
|
|||
normalizeLinksData,
|
||||
normalizeCountersData,
|
||||
normalizeEquipmentPlannerData,
|
||||
normalizeGoalTreeData,
|
||||
normalizeCalculatorData,
|
||||
normalizeCalendarData,
|
||||
normalizeImageAnnotationData,
|
||||
|
|
@ -361,7 +364,7 @@ function LibraryToolExample({ module, sourceData, description, categoryTitle, do
|
|||
<i aria-hidden="true" />
|
||||
</button>
|
||||
)}
|
||||
{definition.editable && (
|
||||
{definition.editable && definition.addable !== false && (
|
||||
<button
|
||||
className={`module-edit-button ${editing ? "active" : ""}`}
|
||||
type="button"
|
||||
|
|
@ -380,7 +383,14 @@ function LibraryToolExample({ module, sourceData, description, categoryTitle, do
|
|||
</header>
|
||||
<div className={`module-content ${scrollable ? "is-scrollable legacy-scrollbar" : ""}`}>
|
||||
<Suspense fallback={<div className="empty"><p>Chargement de l'outil...</p></div>}>
|
||||
<Component toolboxId={LIBRARY_TOOLBOX_ID} moduleId={module.id} context={context} editing={editing} />
|
||||
<Component
|
||||
toolboxId={LIBRARY_TOOLBOX_ID}
|
||||
moduleId={module.id}
|
||||
context={context}
|
||||
editing={editing}
|
||||
setEditing={setEditing}
|
||||
openEditor={() => setEditing(true)}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
</article>
|
||||
|
|
|
|||
|
|
@ -266,6 +266,7 @@ body.is-modal-open {
|
|||
.filter-reset-button,
|
||||
.image-viewer-button,
|
||||
.module-edit-button,
|
||||
.module-exchange-button,
|
||||
.module-delete-button,
|
||||
.image-annotate-button,
|
||||
.image-delete-button,
|
||||
|
|
@ -303,6 +304,7 @@ body.is-modal-open {
|
|||
.filter-reset-button,
|
||||
.image-viewer-button,
|
||||
.module-edit-button,
|
||||
.module-exchange-button,
|
||||
.module-delete-button,
|
||||
.image-annotate-button,
|
||||
.image-delete-button,
|
||||
|
|
@ -324,6 +326,7 @@ body.is-modal-open {
|
|||
.filter-reset-button,
|
||||
.image-viewer-button,
|
||||
.module-edit-button,
|
||||
.module-exchange-button,
|
||||
.module-delete-button,
|
||||
.image-annotate-button,
|
||||
.image-delete-button,
|
||||
|
|
@ -350,6 +353,7 @@ body.is-modal-open {
|
|||
.filter-reset-button,
|
||||
.image-viewer-button,
|
||||
.module-edit-button,
|
||||
.module-exchange-button,
|
||||
.module-delete-button,
|
||||
.image-annotate-button,
|
||||
.image-delete-button,
|
||||
|
|
@ -375,6 +379,7 @@ body.is-modal-open {
|
|||
.filter-reset-button,
|
||||
.image-viewer-button,
|
||||
.module-edit-button,
|
||||
.module-exchange-button,
|
||||
.module-delete-button,
|
||||
.image-annotate-button,
|
||||
.image-delete-button,
|
||||
|
|
@ -402,6 +407,7 @@ body.is-modal-open {
|
|||
.filter-reset-button,
|
||||
.image-viewer-button,
|
||||
.module-edit-button,
|
||||
.module-exchange-button,
|
||||
.module-delete-button,
|
||||
.image-annotate-button,
|
||||
.image-delete-button,
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@
|
|||
height: 82px;
|
||||
background: currentColor;
|
||||
filter: drop-shadow(0 16px 24px rgba(0, 0, 0, 0.44));
|
||||
mask: url("/static/img/diablo4/cube.svg") center / contain no-repeat;
|
||||
mask: url("/static/icons/cube.svg") center / contain no-repeat;
|
||||
}
|
||||
|
||||
.diablo4-affix-grid {
|
||||
|
|
|
|||
|
|
@ -82,6 +82,61 @@
|
|||
-webkit-mask-image: url("/static/icons/tasklist.svg");
|
||||
}
|
||||
|
||||
.ui-icon-notepad {
|
||||
mask-image: url("/static/icons/notepad.svg");
|
||||
-webkit-mask-image: url("/static/icons/notepad.svg");
|
||||
}
|
||||
|
||||
.ui-icon-checklist {
|
||||
mask-image: url("/static/icons/checklist.svg");
|
||||
-webkit-mask-image: url("/static/icons/checklist.svg");
|
||||
}
|
||||
|
||||
.ui-icon-diagram {
|
||||
mask-image: url("/static/icons/diagram.svg");
|
||||
-webkit-mask-image: url("/static/icons/diagram.svg");
|
||||
}
|
||||
|
||||
.ui-icon-cube {
|
||||
mask-image: url("/static/icons/cube.svg");
|
||||
-webkit-mask-image: url("/static/icons/cube.svg");
|
||||
}
|
||||
|
||||
.ui-icon-fire {
|
||||
mask-image: url("/static/icons/fire.svg");
|
||||
-webkit-mask-image: url("/static/icons/fire.svg");
|
||||
}
|
||||
|
||||
.ui-icon-flask {
|
||||
mask-image: url("/static/icons/flask.svg");
|
||||
-webkit-mask-image: url("/static/icons/flask.svg");
|
||||
}
|
||||
|
||||
.ui-icon-grass {
|
||||
mask-image: url("/static/icons/grass.svg");
|
||||
-webkit-mask-image: url("/static/icons/grass.svg");
|
||||
}
|
||||
|
||||
.ui-icon-leaf {
|
||||
mask-image: url("/static/icons/leaf.svg");
|
||||
-webkit-mask-image: url("/static/icons/leaf.svg");
|
||||
}
|
||||
|
||||
.ui-icon-meat {
|
||||
mask-image: url("/static/icons/meat.svg");
|
||||
-webkit-mask-image: url("/static/icons/meat.svg");
|
||||
}
|
||||
|
||||
.ui-icon-powder {
|
||||
mask-image: url("/static/icons/powder.svg");
|
||||
-webkit-mask-image: url("/static/icons/powder.svg");
|
||||
}
|
||||
|
||||
.ui-icon-tear {
|
||||
mask-image: url("/static/icons/tear.svg");
|
||||
-webkit-mask-image: url("/static/icons/tear.svg");
|
||||
}
|
||||
|
||||
.ui-icon-edit {
|
||||
mask-image: url("/static/icons/edit.svg");
|
||||
-webkit-mask-image: url("/static/icons/edit.svg");
|
||||
|
|
@ -177,6 +232,11 @@
|
|||
-webkit-mask-image: url("/static/icons/scrollable.svg");
|
||||
}
|
||||
|
||||
.ui-icon-rotate {
|
||||
mask-image: url("/static/icons/rotate.svg");
|
||||
-webkit-mask-image: url("/static/icons/rotate.svg");
|
||||
}
|
||||
|
||||
.ui-icon-sort-time {
|
||||
mask-image: url("/static/icons/sort-time.svg");
|
||||
-webkit-mask-image: url("/static/icons/sort-time.svg");
|
||||
|
|
|
|||
|
|
@ -12,13 +12,6 @@
|
|||
padding: var(--space-4);
|
||||
}
|
||||
|
||||
.calculator-exchange-panel,
|
||||
.timer-exchange-panel,
|
||||
.table-exchange-panel {
|
||||
grid-column: 1 / -1;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.calculator-form {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
|
|
|
|||
|
|
@ -115,47 +115,6 @@
|
|||
.calendar-event-form select {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
min-height: 36px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
background-color: rgba(15, 23, 42, 0.64);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.calendar-event-form select {
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
overflow: hidden;
|
||||
padding-right: 34px;
|
||||
text-overflow: ellipsis;
|
||||
background-image:
|
||||
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23c4b5fd' viewBox='-6.5 0 32 32'%3E%3Cpath d='M18.813 11.406l-7.906 9.906c-.75.906-1.906.906-2.625 0L.376 11.406c-.75-.938-.375-1.656.781-1.656h16.875c1.188 0 1.531.719.781 1.656z'/%3E%3C/svg%3E"),
|
||||
linear-gradient(135deg, rgba(139, 92, 246, 0.04), rgba(31, 41, 78, 0.08)),
|
||||
linear-gradient(rgba(21, 26, 48, 0.38), rgba(21, 26, 48, 0.38));
|
||||
background-position:
|
||||
calc(100% - 10px) 50%,
|
||||
0 0,
|
||||
0 0;
|
||||
background-size:
|
||||
11px 11px,
|
||||
auto,
|
||||
auto;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.calendar-event-form select:hover,
|
||||
.calendar-event-form select:focus {
|
||||
border-color: rgba(196, 181, 253, 0.22);
|
||||
background-image:
|
||||
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23f6c453' viewBox='-6.5 0 32 32'%3E%3Cpath d='M18.813 11.406l-7.906 9.906c-.75.906-1.906.906-2.625 0L.376 11.406c-.75-.938-.375-1.656.781-1.656h16.875c1.188 0 1.531.719.781 1.656z'/%3E%3C/svg%3E"),
|
||||
linear-gradient(135deg, rgba(246, 196, 83, 0.06), rgba(139, 92, 246, 0.08)),
|
||||
linear-gradient(rgba(21, 26, 48, 0.58), rgba(21, 26, 48, 0.58));
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.calendar-event-form select option {
|
||||
background: #11182f;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.calendar-month-date-dropdown {
|
||||
|
|
@ -518,11 +477,6 @@
|
|||
align-self: end;
|
||||
}
|
||||
|
||||
.calendar-exchange-panel .text-import-actions {
|
||||
justify-content: flex-end;
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
.calendar-form-actions {
|
||||
display: flex;
|
||||
align-items: end;
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
@use "combos";
|
||||
@use "counters";
|
||||
@use "calculator";
|
||||
@use "goal-tree";
|
||||
@use "timer";
|
||||
@use "calendar";
|
||||
@use "calculator-results";
|
||||
|
|
|
|||
1193
website/src/styles/toolboxes/_goal-tree.scss
Normal file
|
|
@ -108,11 +108,13 @@
|
|||
}
|
||||
|
||||
.module-edit-button,
|
||||
.module-exchange-button,
|
||||
.module-delete-button {
|
||||
flex: 0 0 40px;
|
||||
}
|
||||
|
||||
.module-edit-button {
|
||||
.module-edit-button,
|
||||
.module-exchange-button {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
|
|
@ -162,7 +164,8 @@
|
|||
box-shadow var(--duration-fast) var(--ease-standard);
|
||||
}
|
||||
|
||||
.module-edit-button.active {
|
||||
.module-edit-button.active,
|
||||
.module-exchange-button.active {
|
||||
@include mixins.temporary-toggle-active;
|
||||
}
|
||||
|
||||
|
|
@ -280,6 +283,21 @@
|
|||
-webkit-mask-image: url("/static/icons/table.svg");
|
||||
}
|
||||
|
||||
.module-icon-diagram {
|
||||
mask-image: url("/static/icons/diagram.svg");
|
||||
-webkit-mask-image: url("/static/icons/diagram.svg");
|
||||
}
|
||||
|
||||
.module-icon-cube {
|
||||
mask-image: url("/static/icons/cube.svg");
|
||||
-webkit-mask-image: url("/static/icons/cube.svg");
|
||||
}
|
||||
|
||||
.module-icon-settings {
|
||||
mask-image: url("/static/icons/settings.svg");
|
||||
-webkit-mask-image: url("/static/icons/settings.svg");
|
||||
}
|
||||
|
||||
.module-icon-controller {
|
||||
mask-image: url("/static/icons/controller.svg");
|
||||
-webkit-mask-image: url("/static/icons/controller.svg");
|
||||
|
|
|
|||
|
|
@ -1,6 +1,46 @@
|
|||
// Rôle : gère les contrôles partagés des outils en deux colonnes et leurs états.
|
||||
@use "../mixins";
|
||||
|
||||
.module-content select {
|
||||
min-height: 36px;
|
||||
overflow: hidden;
|
||||
padding-right: 34px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
color: var(--color-text-primary);
|
||||
text-overflow: ellipsis;
|
||||
background-image:
|
||||
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23c4b5fd' viewBox='-6.5 0 32 32'%3E%3Cpath d='M18.813 11.406l-7.906 9.906c-.75.906-1.906.906-2.625 0L.376 11.406c-.75-.938-.375-1.656.781-1.656h16.875c1.188 0 1.531.719.781 1.656z'/%3E%3C/svg%3E"),
|
||||
linear-gradient(135deg, rgba(139, 92, 246, 0.04), rgba(31, 41, 78, 0.08)),
|
||||
linear-gradient(rgba(21, 26, 48, 0.38), rgba(21, 26, 48, 0.38));
|
||||
background-position:
|
||||
calc(100% - 10px) 50%,
|
||||
0 0,
|
||||
0 0;
|
||||
background-size:
|
||||
11px 11px,
|
||||
auto,
|
||||
auto;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.module-content select:hover,
|
||||
.module-content select:focus {
|
||||
border-color: rgba(196, 181, 253, 0.22);
|
||||
background-image:
|
||||
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23f6c453' viewBox='-6.5 0 32 32'%3E%3Cpath d='M18.813 11.406l-7.906 9.906c-.75.906-1.906.906-2.625 0L.376 11.406c-.75-.938-.375-1.656.781-1.656h16.875c1.188 0 1.531.719.781 1.656z'/%3E%3C/svg%3E"),
|
||||
linear-gradient(135deg, rgba(246, 196, 83, 0.06), rgba(139, 92, 246, 0.08)),
|
||||
linear-gradient(rgba(21, 26, 48, 0.58), rgba(21, 26, 48, 0.58));
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.module-content select option {
|
||||
background: #11182f;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.tool-split-results {
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
gap: var(--space-3);
|
||||
|
|
|
|||
|
|
@ -22,10 +22,6 @@
|
|||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.task-planner-add-stack .text-import-actions {
|
||||
padding-top: var(--space-2);
|
||||
}
|
||||
|
||||
.task-planner-global-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
|
@ -121,17 +117,21 @@
|
|||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.task-planner-settings select,
|
||||
.task-planner-settings input,
|
||||
.task-planner-reset-override input,
|
||||
.task-planner-settings select,
|
||||
.task-planner-add-form select,
|
||||
.task-planner-type-select,
|
||||
.task-planner-reset-override select,
|
||||
.task-planner-reset-override input,
|
||||
.task-planner-relations select,
|
||||
.combos-device-row select,
|
||||
.equipment-planner-socket-links select,
|
||||
.equipment-planner-socket-controls select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.task-planner-settings input,
|
||||
.task-planner-reset-override input {
|
||||
min-height: 36px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
|
|
@ -139,18 +139,6 @@
|
|||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.task-planner-settings select option,
|
||||
.task-planner-add-form select option,
|
||||
.task-planner-type-select option,
|
||||
.task-planner-reset-override select option,
|
||||
.task-planner-relations select option,
|
||||
.combos-device-row select option,
|
||||
.equipment-planner-socket-links select option,
|
||||
.equipment-planner-socket-controls select option {
|
||||
background: #11182f;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.task-planner-add-form {
|
||||
grid-template-columns: minmax(0, 1fr) minmax(120px, 0.36fr) auto;
|
||||
}
|
||||
|
|
@ -445,42 +433,12 @@
|
|||
}
|
||||
|
||||
.task-planner-type-select {
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
width: 100%;
|
||||
min-height: 36px;
|
||||
overflow: hidden;
|
||||
padding: 0 30px 0 10px;
|
||||
border-color: rgba(165, 180, 252, 0.09);
|
||||
text-overflow: ellipsis;
|
||||
background-image:
|
||||
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23c4b5fd' viewBox='-6.5 0 32 32'%3E%3Cpath d='M18.813 11.406l-7.906 9.906c-.75.906-1.906.906-2.625 0L.376 11.406c-.75-.938-.375-1.656.781-1.656h16.875c1.188 0 1.531.719.781 1.656z'/%3E%3C/svg%3E"),
|
||||
linear-gradient(135deg, rgba(139, 92, 246, 0.04), rgba(31, 41, 78, 0.08)),
|
||||
linear-gradient(rgba(21, 26, 48, 0.38), rgba(21, 26, 48, 0.38));
|
||||
background-position:
|
||||
calc(100% - 10px) 50%,
|
||||
0 0,
|
||||
0 0;
|
||||
background-size:
|
||||
11px 11px,
|
||||
auto,
|
||||
auto;
|
||||
background-repeat: no-repeat;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.task-planner-type-select:hover,
|
||||
.task-planner-type-select:focus {
|
||||
border-color: rgba(196, 181, 253, 0.22);
|
||||
background-image:
|
||||
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23f6c453' viewBox='-6.5 0 32 32'%3E%3Cpath d='M18.813 11.406l-7.906 9.906c-.75.906-1.906.906-2.625 0L.376 11.406c-.75-.938-.375-1.656.781-1.656h16.875c1.188 0 1.531.719.781 1.656z'/%3E%3C/svg%3E"),
|
||||
linear-gradient(135deg, rgba(246, 196, 83, 0.06), rgba(139, 92, 246, 0.08)),
|
||||
linear-gradient(rgba(21, 26, 48, 0.58), rgba(21, 26, 48, 0.58));
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.task-planner-warning {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
|
@ -744,6 +702,5 @@
|
|||
}
|
||||
|
||||
.task-planner-relations select {
|
||||
min-height: 36px;
|
||||
padding-inline: 10px;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,13 @@
|
|||
padding: 0;
|
||||
}
|
||||
|
||||
.module-text-exchange-panel {
|
||||
display: grid;
|
||||
grid-column: 1 / -1;
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-4);
|
||||
}
|
||||
|
||||
.text-import-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
|
|
|||
|
|
@ -47,90 +47,6 @@
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
.timer-countdown-form select {
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
overflow: hidden;
|
||||
padding-right: 34px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--color-text-primary);
|
||||
text-overflow: ellipsis;
|
||||
background-image:
|
||||
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23ffffff' viewBox='-6.5 0 32 32'%3E%3Cpath d='M18.813 11.406l-7.906 9.906c-.75.906-1.906.906-2.625 0L.376 11.406c-.75-.938-.375-1.656.781-1.656h16.875c1.188 0 1.531.719.781 1.656z'/%3E%3C/svg%3E"),
|
||||
linear-gradient(135deg, rgba(139, 92, 246, 0.08), rgba(31, 41, 78, 0.12)),
|
||||
linear-gradient(rgba(15, 23, 42, 0.72), rgba(15, 23, 42, 0.72));
|
||||
background-position:
|
||||
calc(100% - 14px) 50%,
|
||||
0 0,
|
||||
0 0;
|
||||
background-size:
|
||||
12px 12px,
|
||||
auto,
|
||||
auto;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.timer-countdown-form select option {
|
||||
background: #11182f;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.timer-countdown-form select,
|
||||
.task-planner-settings select,
|
||||
.task-planner-add-form select,
|
||||
.task-planner-type-select,
|
||||
.task-planner-reset-override select,
|
||||
.task-planner-relations select,
|
||||
.combos-device-row select,
|
||||
.equipment-planner-socket-links select,
|
||||
.equipment-planner-socket-controls select {
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
overflow: hidden;
|
||||
padding-right: 34px;
|
||||
text-overflow: ellipsis;
|
||||
background-image:
|
||||
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23c4b5fd' viewBox='-6.5 0 32 32'%3E%3Cpath d='M18.813 11.406l-7.906 9.906c-.75.906-1.906.906-2.625 0L.376 11.406c-.75-.938-.375-1.656.781-1.656h16.875c1.188 0 1.531.719.781 1.656z'/%3E%3C/svg%3E"),
|
||||
linear-gradient(135deg, rgba(139, 92, 246, 0.04), rgba(31, 41, 78, 0.08)),
|
||||
linear-gradient(rgba(21, 26, 48, 0.38), rgba(21, 26, 48, 0.38));
|
||||
background-position:
|
||||
calc(100% - 10px) 50%,
|
||||
0 0,
|
||||
0 0;
|
||||
background-size:
|
||||
11px 11px,
|
||||
auto,
|
||||
auto;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.timer-countdown-form select:hover,
|
||||
.timer-countdown-form select:focus,
|
||||
.task-planner-settings select:hover,
|
||||
.task-planner-settings select:focus,
|
||||
.task-planner-add-form select:hover,
|
||||
.task-planner-add-form select:focus,
|
||||
.task-planner-type-select:hover,
|
||||
.task-planner-type-select:focus,
|
||||
.task-planner-reset-override select:hover,
|
||||
.task-planner-reset-override select:focus,
|
||||
.task-planner-relations select:hover,
|
||||
.task-planner-relations select:focus,
|
||||
.combos-device-row select:hover,
|
||||
.combos-device-row select:focus,
|
||||
.equipment-planner-socket-links select:hover,
|
||||
.equipment-planner-socket-links select:focus,
|
||||
.equipment-planner-socket-controls select:hover,
|
||||
.equipment-planner-socket-controls select:focus {
|
||||
border-color: rgba(196, 181, 253, 0.22);
|
||||
background-image:
|
||||
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23f6c453' viewBox='-6.5 0 32 32'%3E%3Cpath d='M18.813 11.406l-7.906 9.906c-.75.906-1.906.906-2.625 0L.376 11.406c-.75-.938-.375-1.656.781-1.656h16.875c1.188 0 1.531.719.781 1.656z'/%3E%3C/svg%3E"),
|
||||
linear-gradient(135deg, rgba(246, 196, 83, 0.06), rgba(139, 92, 246, 0.08)),
|
||||
linear-gradient(rgba(21, 26, 48, 0.58), rgba(21, 26, 48, 0.58));
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.time-parts-input {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(34px, 1fr) 4px minmax(34px, 1fr) 4px minmax(34px, 1fr);
|
||||
|
|
|
|||