Add goal tree toolbox module
All checks were successful
Deploy Sokko G / deploy (push) Successful in 15s
|
|
@ -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);
|
||||
|
|
|
|||