Add planner-aware memos and unified tool configuration panels
All checks were successful
Deploy Sokko G / deploy (push) Successful in 18s
All checks were successful
Deploy Sokko G / deploy (push) Successful in 18s
This commit is contained in:
parent
acd9b2fe11
commit
808610dcf2
53 changed files with 842 additions and 329 deletions
|
|
@ -69,3 +69,7 @@
|
||||||
{"id":"int-4b71144d1e015bb3fa6cf94d5fb2c90b","kind":"field_change","created_at":"2026-10-01T13:08:30.477630845Z","actor":"Shinuwa","issue_id":"sokko-g-mag","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Delivered on main in the commits recorded in the issue description; reconstructed in Beads on 2026-10-01."}}
|
{"id":"int-4b71144d1e015bb3fa6cf94d5fb2c90b","kind":"field_change","created_at":"2026-10-01T13:08:30.477630845Z","actor":"Shinuwa","issue_id":"sokko-g-mag","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Delivered on main in the commits recorded in the issue description; reconstructed in Beads on 2026-10-01."}}
|
||||||
{"id":"int-73ab6f8b2af7fdd86148531fd1eacb65","kind":"field_change","created_at":"2026-10-01T13:08:36.827446402Z","actor":"Shinuwa","issue_id":"sokko-g-fpd","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Delivered on main in the commits recorded in the issue description; reconstructed in Beads on 2026-10-01."}}
|
{"id":"int-73ab6f8b2af7fdd86148531fd1eacb65","kind":"field_change","created_at":"2026-10-01T13:08:36.827446402Z","actor":"Shinuwa","issue_id":"sokko-g-fpd","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Delivered on main in the commits recorded in the issue description; reconstructed in Beads on 2026-10-01."}}
|
||||||
{"id":"int-1ee4fbc06ba1ca47d57a3938ce5c647a","kind":"field_change","created_at":"2026-10-01T13:17:28.985204569Z","actor":"Shinuwa","issue_id":"sokko-g-aut","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Backfilled 13 retrospective issues covering all 23 commits, validated the repository, initialized the empty Dolt remote, and published the complete Beads history."}}
|
{"id":"int-1ee4fbc06ba1ca47d57a3938ce5c647a","kind":"field_change","created_at":"2026-10-01T13:17:28.985204569Z","actor":"Shinuwa","issue_id":"sokko-g-aut","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Backfilled 13 retrospective issues covering all 23 commits, validated the repository, initialized the empty Dolt remote, and published the complete Beads history."}}
|
||||||
|
{"id":"int-e642f21f88bfa85d4b4ba7746e994cd3","kind":"field_change","created_at":"2026-10-03T11:31:45.398002709Z","actor":"Shinuwa","issue_id":"sokko-g-noo","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Implemented Memos planner linking, reset warning ticker items, deletion cleanup, import/export remapping, documentation, unit coverage, and Playwright coverage. npm run check and the focused Memos E2E suite pass."}}
|
||||||
|
{"id":"int-e4050bca9a4c650f7447b7dac6c81ebb","kind":"field_change","created_at":"2026-10-03T13:02:57.878785474Z","actor":"Shinuwa","issue_id":"sokko-g-8wa","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Implemented shared toolbox configuration panels, migrated tool settings and text exchange, updated responsive styles, docs, and tests."}}
|
||||||
|
{"id":"int-c4a9c3aa90751550c16cf7e201af7259","kind":"field_change","created_at":"2026-10-03T13:40:04.048932123Z","actor":"Shinuwa","issue_id":"sokko-g-0x3","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Refined Configuration panel into a compact integrated layout with Organisation and Data on the first row, full-width tool settings below, shorter visible data actions, responsive stacking, documentation, and E2E coverage."}}
|
||||||
|
{"id":"int-11873ba8e71cce0b835e8d8d2124359b","kind":"field_change","created_at":"2026-10-03T13:48:06.348778272Z","actor":"Shinuwa","issue_id":"sokko-g-mwn","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Aligned the Espace select and import/export actions at small size, removed the redundant visible move label, retained accessible naming, and updated documentation and tests."}}
|
||||||
|
|
|
||||||
|
|
@ -200,7 +200,12 @@ Style courant :
|
||||||
- hover violet/violet clair moins intense que les cards ;
|
- hover violet/violet clair moins intense que les cards ;
|
||||||
- pas de mouvement au hover ;
|
- pas de mouvement au hover ;
|
||||||
- contenu interne au-dessus des pseudo-bordures via `z-index`.
|
- contenu interne au-dessus des pseudo-bordures via `z-index`.
|
||||||
- chevron de repli tout à droite de l'en-tête ; l'état replié conserve uniquement l'en-tête visible et retire son séparateur bas.
|
- l'en-tête suit toujours l'ordre poignée de déplacement, icône, titre, switch de scroll éventuel, ajout éventuel, Configuration, suppression, chevron ; le titre peut se réduire mais les actions ne s'étirent pas ;
|
||||||
|
- le chevron de repli reste tout à droite ; l'état replié conserve l'en-tête et peut afficher le panneau Configuration sans révéler le contenu métier ;
|
||||||
|
- l'engrenage Configuration ouvre un panneau pleine largeur sous l'en-tête, nommé par `aria-controls` / `aria-expanded`. Il regroupe conditionnellement Espace, Guide, réglages propres à l'outil et Données (import/export) ; ouvrir l'ajout ferme Configuration et réciproquement ;
|
||||||
|
- dans Configuration, Espace et Données partagent la première ligne en deux colonnes. Espace affiche directement le sélecteur compact dont le placeholder décrit l'action. Les réglages propres à l'outil et la liaison Guide occupent toute la largeur en dessous ; si Espace est absent, Données utilise toute la première ligne ;
|
||||||
|
- le panneau Configuration reste une surface intégrée à l'outil : un seul contour discret, des sous-sections sans effet de carte imbriquée, séparées uniquement par des filets légers, avec libellés et contrôles compacts ;
|
||||||
|
- sur mobile, l'en-tête se répartit sur deux lignes compactes et les sections/champs de Configuration passent en une colonne, dans l'ordre Espace, Données puis réglages spécifiques.
|
||||||
|
|
||||||
Le mot visible cote utilisateur est **outil**, pas **module**. Le terme module reste acceptable uniquement dans le code.
|
Le mot visible cote utilisateur est **outil**, pas **module**. Le terme module reste acceptable uniquement dans le code.
|
||||||
|
|
||||||
|
|
@ -296,7 +301,7 @@ Regles UI :
|
||||||
- en vue mensuelle, les evenements multi-jours s'affichent comme une barre horizontale et ne sont pas dupliques dans chaque cellule ;
|
- en vue mensuelle, les evenements multi-jours s'affichent comme une barre horizontale et ne sont pas dupliques dans chaque cellule ;
|
||||||
- les vues hebdomadaire et mensuelle affichent un indicateur discret du moment actuel.
|
- les vues hebdomadaire et mensuelle affichent un indicateur discret du moment actuel.
|
||||||
- en vue hebdomadaire, un switch standard peut masquer les longues plages horaires vides ; la tranche masquée reste indiquée dans la grille, pas dans un texte qui déborde de la colonne horaire.
|
- en vue hebdomadaire, un switch standard peut masquer les longues plages horaires vides ; la tranche masquée reste indiquée dans la grille, pas dans un texte qui déborde de la colonne horaire.
|
||||||
- le calendrier supporte l'import/export texte via le toggle import/export du header, pour les vues hebdomadaire et mensuelle.
|
- le calendrier supporte l'import/export texte via la section Données du panneau Configuration, pour les vues hebdomadaire et mensuelle.
|
||||||
|
|
||||||
### Arbre D'objectifs
|
### Arbre D'objectifs
|
||||||
|
|
||||||
|
|
@ -383,7 +388,7 @@ Regles UI :
|
||||||
- palette de couleurs compacte reprenant les swatches du Bloc Note ;
|
- palette de couleurs compacte reprenant les swatches du Bloc Note ;
|
||||||
- la creation de mémo est masquee par defaut et s'ouvre via le bouton `+` du header d'outil ;
|
- la creation de mémo est masquee par defaut et s'ouvre via le bouton `+` du header d'outil ;
|
||||||
- réorganisation des cartes par poignée de drag & drop compacte ;
|
- réorganisation des cartes par poignée de drag & drop compacte ;
|
||||||
- bande deroulante visible uniquement quand elle est active et qu'au moins un mémo existe ; elle affiche tous les mémos dans l'outil et reste fixée en bas des pages complètes de cette toolbox ainsi que des pages du jeu qui lui est associé ;
|
- bande deroulante visible uniquement quand elle est active et qu'au moins un mémo ou une alerte de planificateur existe ; elle affiche tous les mémos dans l'outil, peut intégrer les tâches non effectuées proches du reset depuis un ou tous les planificateurs de la même toolbox, et reste fixée en bas des pages complètes de cette toolbox ainsi que des pages du jeu qui lui est associé ;
|
||||||
- la bande fixe laisse le contenu verticalement accessible, suit la largeur disponible et remonte au-dessus de la navigation mobile.
|
- la bande fixe laisse le contenu verticalement accessible, suit la largeur disponible et remonte au-dessus de la navigation mobile.
|
||||||
|
|
||||||
### Calque De Dessin
|
### Calque De Dessin
|
||||||
|
|
@ -871,11 +876,10 @@ Date picker et menus compacts :
|
||||||
|
|
||||||
### Liaison des guides aux checklists
|
### Liaison des guides aux checklists
|
||||||
|
|
||||||
Dans la toolbox latérale, chaque checklist compatible avec le guide ouvert affiche
|
Dans la toolbox latérale, chaque checklist compatible avec le guide ouvert expose
|
||||||
un bouton icône de lien à côté du « + » dans son en-tête. Ce toggle NDS affiche
|
sa liaison dans la section Guide du panneau Configuration commun. L'engrenage
|
||||||
ou masque la section `guide-checklist-controls`, masquée par défaut, sans désactiver
|
conserve un marqueur d'état lorsque la liste est associée et expose `aria-expanded`
|
||||||
le suivi. Il expose `aria-expanded`, `aria-pressed` et `aria-controls` ; son icône
|
et `aria-controls`. Le bouton NDS
|
||||||
reste dorée lorsque la liste est associée. Le bouton NDS
|
|
||||||
« Associer à la page » / « Dissocier » expose `aria-pressed` ; activer une liste
|
« Associer à la page » / « Dissocier » expose `aria-pressed` ; activer une liste
|
||||||
dissocie la précédente. Le switch « Masquer les cochés » est désactivé sur les listes
|
dissocie la précédente. Le switch « Masquer les cochés » est désactivé sur les listes
|
||||||
non associées. Les commandes sont centrées et reviennent à la ligne sur mobile ;
|
non associées. Les commandes sont centrées et reviennent à la ligne sur mobile ;
|
||||||
|
|
|
||||||
|
|
@ -17,10 +17,12 @@ Checklist à suivre lors de l'ajout ou de la modification d'un outil, d'une page
|
||||||
- Vérifier qu'une erreur d'ouverture ou de lecture au démarrage bloque les écritures, créations, imports et exports sans afficher une fausse liste vide ; plusieurs tentatives puis une reprise réussie doivent retrouver les données initiales sans recharger la page ni les effacer.
|
- Vérifier qu'une erreur d'ouverture ou de lecture au démarrage bloque les écritures, créations, imports et exports sans afficher une fausse liste vide ; plusieurs tentatives puis une reprise réussie doivent retrouver les données initiales sans recharger la page ni les effacer.
|
||||||
- Pour Images et Annotation d'images, vérifier la lecture différée en page complète et panneau latéral, la reprise après échec local et l'absence d'écrasement pendant le chargement. Les alertes des autres outils doivent rester actives sans ouvrir les médias.
|
- Pour Images et Annotation d'images, vérifier la lecture différée en page complète et panneau latéral, la reprise après échec local et l'absence d'écrasement pendant le chargement. Les alertes des autres outils doivent rester actives sans ouvrir les médias.
|
||||||
- Vérifier qu'une création, un import ou une suppression préserve les médias non lus des autres outils ; les exports doivent les inclure et refuser un téléchargement partiel en cas d'échec. Une lecture en cours ne doit pas réintroduire des données supprimées.
|
- Vérifier qu'une création, un import ou une suppression préserve les médias non lus des autres outils ; les exports doivent les inclure et refuser un téléchargement partiel en cas d'échec. Une lecture en cours ne doit pas réintroduire des données supprimées.
|
||||||
- Si l'outil a un contenu représentable en texte, ajouter ou vérifier l'import/export texte round-trip depuis le toggle import/export du header.
|
- Si l'outil a un contenu représentable en texte, ajouter ou vérifier l'import/export texte round-trip depuis la section Données du panneau Configuration.
|
||||||
- Si l'outil porte un état de progression, vérifier que l'import texte repart de l'état initial attendu.
|
- Si l'outil porte un état de progression, vérifier que l'import texte repart de l'état initial attendu.
|
||||||
- Vérifier l'affichage dans la page toolbox complète.
|
- Vérifier l'affichage dans la page toolbox complète.
|
||||||
- Vérifier l'affichage dans le panneau latéral.
|
- Vérifier l'affichage dans le panneau latéral.
|
||||||
|
- Vérifier l'ordre compact du header en une et deux colonnes et sur mobile : déplacement, icône, titre, scroll éventuel, ajout éventuel, Configuration, suppression, chevron. Le panneau Configuration doit prendre toute la largeur, placer Espace et Données sur deux colonnes puis les réglages spécifiques sur toute la ligne, et empiler ces sections dans cet ordre sur mobile.
|
||||||
|
- Vérifier que Configuration et ajout se ferment mutuellement, qu'Échap ferme Configuration et qu'un outil replié peut l'ouvrir sans afficher son contenu métier.
|
||||||
- Pour le tutoriel toolbox, vérifier le démarrage unique, la fermeture et le changement de toolbox en cours de visite ; une autre toolbox doit démarrer à la première étape et ne pas consommer les indicateurs de la précédente.
|
- Pour le tutoriel toolbox, vérifier le démarrage unique, la fermeture et le changement de toolbox en cours de visite ; une autre toolbox doit démarrer à la première étape et ne pas consommer les indicateurs de la précédente.
|
||||||
- Pour le Bloc Note, vérifier la sélection, l'ajout, le renommage et la suppression de pages en page complète, panneau latéral et mobile.
|
- Pour le Bloc Note, vérifier la sélection, l'ajout, le renommage et la suppression de pages en page complète, panneau latéral et mobile.
|
||||||
- Vérifier qu'un changement de page avec le focus actif remplace immédiatement le contenu de l'éditeur ; une mise à jour externe de la même page attend le blur sans déplacer la sélection pendant la saisie ni reprendre le focus ensuite.
|
- Vérifier qu'un changement de page avec le focus actif remplace immédiatement le contenu de l'éditeur ; une mise à jour externe de la même page attend le blur sans déplacer la sélection pendant la saisie ni reprendre le focus ensuite.
|
||||||
|
|
@ -33,7 +35,7 @@ Checklist à suivre lors de l'ajout ou de la modification d'un outil, d'une page
|
||||||
- Les parcours `e2e/table.spec.js` vérifient les en-têtes éditables sans changer les références des formules, le redimensionnement sans restauration des cellules supprimées, la navigation et la sélection clavier, la copie des valeurs TSV, le collage et l'effacement de blocs, le déplacement à la souris et l'insertion d'une plage dans une formule. Les valeurs et dimensions doivent survivre au rechargement.
|
- Les parcours `e2e/table.spec.js` vérifient les en-têtes éditables sans changer les références des formules, le redimensionnement sans restauration des cellules supprimées, la navigation et la sélection clavier, la copie des valeurs TSV, le collage et l'effacement de blocs, le déplacement à la souris et l'insertion d'une plage dans une formule. Les valeurs et dimensions doivent survivre au rechargement.
|
||||||
- Les parcours `e2e/text-exchange.spec.js` vérifient les modales partagées via Tableau et Calculateur : export désactivé à vide, validation des imports, conservation des données après erreur, brouillon effacé à la réouverture, export en lecture seule et copie exacte. L'import doit persister avec ses formules, libellés et relations ; Annuler, Fermer, Échap et le fond de modale doivent fermer correctement, avec restitution du défilement.
|
- Les parcours `e2e/text-exchange.spec.js` vérifient les modales partagées via Tableau et Calculateur : export désactivé à vide, validation des imports, conservation des données après erreur, brouillon effacé à la réouverture, export en lecture seule et copie exacte. L'import doit persister avec ses formules, libellés et relations ; Annuler, Fermer, Échap et le fond de modale doivent fermer correctement, avec restitution du défilement.
|
||||||
- Les parcours `e2e/task-planner.spec.js` couvrent création, renommage, description multiligne, catégorie, suppression et persistance. Un parent transmet sa catégorie ; un prérequis manquant avertit sans bloquer la validation. Supprimer un parent doit conserver l'enfant sans relation orpheline. Avec une horloge contrôlée, les tâches quotidiennes et hebdomadaires se réinitialisent, les ponctuelles restent cochées ; masquage et nettoyage doivent préserver les autres tâches.
|
- Les parcours `e2e/task-planner.spec.js` couvrent création, renommage, description multiligne, catégorie, suppression et persistance. Un parent transmet sa catégorie ; un prérequis manquant avertit sans bloquer la validation. Supprimer un parent doit conserver l'enfant sans relation orpheline. Avec une horloge contrôlée, les tâches quotidiennes et hebdomadaires se réinitialisent, les ponctuelles restent cochées ; masquage et nettoyage doivent préserver les autres tâches.
|
||||||
- Pour Mémos, vérifier l'édition courte, les couleurs, la réorganisation et la bande déroulante.
|
- Pour Mémos, vérifier l'édition courte, les couleurs, la réorganisation et la bande déroulante ; vérifier aussi les liaisons Aucun / Tous / planificateur précis, les alertes de tâches proches du reset et le retour à Aucun après suppression du dernier planificateur lié.
|
||||||
- Les parcours `e2e/calculator.spec.js` vérifient la validation des formules, l'édition parent/enfant avec recalcul et persistance, la suppression d'une branche active, le retour à un calcul racine, la réinitialisation du brouillon sans effacer les résultats, la copie pour checklist et la persistance du défilement. La réorganisation des racines doit conserver leurs descendants et refuser de changer le parent d'un enfant.
|
- Les parcours `e2e/calculator.spec.js` vérifient la validation des formules, l'édition parent/enfant avec recalcul et persistance, la suppression d'une branche active, le retour à un calcul racine, la réinitialisation du brouillon sans effacer les résultats, la copie pour checklist et la persistance du défilement. La réorganisation des racines doit conserver leurs descendants et refuser de changer le parent d'un enfant.
|
||||||
- Vérifier qu'une notification ou une mise à jour sans rapport ne remplace pas un brouillon Mémos avant sa validation au blur.
|
- Vérifier qu'une notification ou une mise à jour sans rapport ne remplace pas un brouillon Mémos avant sa validation au blur.
|
||||||
- Pour l'arbre d'objectifs, vérifier la sélection après suppression/import et le retour à Base quand le nœud sélectionné devient une étape sans onglet Contenu.
|
- Pour l'arbre d'objectifs, vérifier la sélection après suppression/import et le retour à Base quand le nœud sélectionné devient une étape sans onglet Contenu.
|
||||||
|
|
|
||||||
|
|
@ -237,6 +237,7 @@ Type : `memos`
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"tickerEnabled": true,
|
"tickerEnabled": true,
|
||||||
|
"plannerLink": "mod-task-planner",
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
"id": "memo1",
|
"id": "memo1",
|
||||||
|
|
@ -252,6 +253,8 @@ Stockage compact :
|
||||||
|
|
||||||
- `tickerEnabled` vaut `true` par défaut et est omis dans ce cas ;
|
- `tickerEnabled` vaut `true` par défaut et est omis dans ce cas ;
|
||||||
- lorsqu'il est actif, la bande est aussi affichée en position fixe sur la page complète de la toolbox et sur les pages du jeu lié ;
|
- lorsqu'il est actif, la bande est aussi affichée en position fixe sur la page complète de la toolbox et sur les pages du jeu lié ;
|
||||||
|
- `plannerLink` vaut `""` par défaut, `"all"` pour suivre tous les planificateurs de la même toolbox, ou l'identifiant d'un module `taskPlanner` précis ;
|
||||||
|
- la liaison ajoute à la bande les tâches quotidiennes ou hebdomadaires non effectuées proches de leur reset ; une référence supprimée revient à `""` ;
|
||||||
- `updatedAt` est omis si vide ou invalide ;
|
- `updatedAt` est omis si vide ou invalide ;
|
||||||
- les mémos sans texte sont ignorés ;
|
- les mémos sans texte sont ignorés ;
|
||||||
- si aucun mémo n'est valide et que la bande est active par défaut, l'entrée de module peut être supprimée.
|
- si aucun mémo n'est valide et que la bande est active par défaut, l'entrée de module peut être supprimée.
|
||||||
|
|
@ -262,9 +265,8 @@ Stockage compact :
|
||||||
|
|
||||||
Les pages Aniimo, monstres MHW, faune MHW et affixes Diablo peuvent refléter une
|
Les pages Aniimo, monstres MHW, faune MHW et affixes Diablo peuvent refléter une
|
||||||
checklist de la toolbox actuellement associée au jeu. Chaque checklist compatible
|
checklist de la toolbox actuellement associée au jeu. Chaque checklist compatible
|
||||||
affiche un toggle de lien à côté du « + » de son en-tête dans le panneau latéral.
|
affiche sa liaison dans la section Guide du panneau Configuration commun :
|
||||||
Il ouvre une section masquée par défaut en haut du contenu :
|
bouton d'association/dissociation et switch de masquage, actif seulement pour
|
||||||
bouton toggle d'association/dissociation et switch de masquage, actif seulement pour
|
|
||||||
la liste associée. Associer une autre liste remplace la précédente et conserve le
|
la liste associée. Associer une autre liste remplace la précédente et conserve le
|
||||||
réglage de masquage ; le sélecteur affiché dans l'en-tête du guide dès qu'une liste
|
réglage de masquage ; le sélecteur affiché dans l'en-tête du guide dès qu'une liste
|
||||||
compatible existe permet le même changement ; sans association, les autres actions
|
compatible existe permet le même changement ; sans association, les autres actions
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import { normalizeChecklistData } from "../website/src/features/toolboxes/storag
|
||||||
const route = "/#/games/aniimo/aniimos";
|
const route = "/#/games/aniimo/aniimos";
|
||||||
const storageKey = "sokkog:guide-checklists";
|
const storageKey = "sokkog:guide-checklists";
|
||||||
const controls = (page, id) => page.locator('[data-module-type="checklist"]').filter({ has: page.getByRole("button", { name: `Retirer ${id}`, exact: true }) }).locator(".guide-checklist-controls");
|
const controls = (page, id) => page.locator('[data-module-type="checklist"]').filter({ has: page.getByRole("button", { name: `Retirer ${id}`, exact: true }) }).locator(".guide-checklist-controls");
|
||||||
const controlsToggle = (page, id) => page.getByRole("button", { name: `Lien avec le guide — ${id}`, exact: true });
|
const controlsToggle = (page, id) => page.locator('[data-module-type="checklist"]').filter({ has: page.getByRole("button", { name: `Retirer ${id}`, exact: true }) }).getByRole("button", { name: "Configuration", exact: true });
|
||||||
const linkButton = (page, id) => controls(page, id).locator(".guide-checklist-link");
|
const linkButton = (page, id) => controls(page, id).locator(".guide-checklist-link");
|
||||||
const activeButtons = (page) => page.locator('.guide-checklist-link[aria-pressed="true"]');
|
const activeButtons = (page) => page.locator('.guide-checklist-link[aria-pressed="true"]');
|
||||||
const hiddenSwitch = (page) => page.locator('.guide-checklist-controls[data-linked="true"]').getByRole("button", { name: "Masquer les cochés", exact: true });
|
const hiddenSwitch = (page) => page.locator('.guide-checklist-controls[data-linked="true"]').getByRole("button", { name: "Masquer les cochés", exact: true });
|
||||||
|
|
@ -97,7 +97,7 @@ async function closeDrawer(page) {
|
||||||
await page.locator("#toolbox-drawer").getByRole("button", { name: "Fermer", exact: true }).click();
|
await page.locator("#toolbox-drawer").getByRole("button", { name: "Fermer", exact: true }).click();
|
||||||
}
|
}
|
||||||
async function openControls(page, id) {
|
async function openControls(page, id) {
|
||||||
const toggle = id ? controlsToggle(page, id) : page.locator('.module-guide-button[data-linked="true"]');
|
const toggle = id ? controlsToggle(page, id) : page.locator('.module-configuration-button[data-guide-linked="true"]');
|
||||||
await expect(toggle).toBeVisible();
|
await expect(toggle).toBeVisible();
|
||||||
if (await toggle.getAttribute("aria-expanded") === "false") await toggle.click();
|
if (await toggle.getAttribute("aria-expanded") === "false") await toggle.click();
|
||||||
}
|
}
|
||||||
|
|
@ -117,8 +117,8 @@ async function toggleHidden(page) {
|
||||||
}
|
}
|
||||||
async function assertLink(page, id, hidden) {
|
async function assertLink(page, id, hidden) {
|
||||||
await openDrawer(page);
|
await openDrawer(page);
|
||||||
await expect(activeButtons(page)).toHaveCount(id ? 1 : 0);
|
|
||||||
if (id) await openControls(page, id);
|
if (id) await openControls(page, id);
|
||||||
|
await expect(activeButtons(page)).toHaveCount(id ? 1 : 0);
|
||||||
if (id) await expect(linkButton(page, id)).toHaveAttribute("aria-pressed", "true");
|
if (id) await expect(linkButton(page, id)).toHaveAttribute("aria-pressed", "true");
|
||||||
if (id) await expect(hiddenSwitch(page)).toHaveAttribute("aria-pressed", String(hidden));
|
if (id) await expect(hiddenSwitch(page)).toHaveAttribute("aria-pressed", String(hidden));
|
||||||
else await expect(hiddenSwitch(page)).toHaveCount(0);
|
else await expect(hiddenSwitch(page)).toHaveCount(0);
|
||||||
|
|
@ -206,11 +206,11 @@ for (const width of [1280, 390]) {
|
||||||
await expect(pageControls.getByRole("button")).toHaveCount(0);
|
await expect(pageControls.getByRole("button")).toHaveCount(0);
|
||||||
await openDrawer(page);
|
await openDrawer(page);
|
||||||
await expect(activeButtons(page)).toHaveCount(0);
|
await expect(activeButtons(page)).toHaveCount(0);
|
||||||
await expect(page.locator(".guide-checklist-controls")).toHaveCount(3);
|
await expect(page.locator(".guide-checklist-controls")).toHaveCount(0);
|
||||||
await expect(page.locator(".guide-checklist-controls select")).toHaveCount(0);
|
await expect(page.locator(".guide-checklist-controls select")).toHaveCount(0);
|
||||||
await expect(controls(page, "unrelated")).toHaveCount(0);
|
await expect(controls(page, "unrelated")).toHaveCount(0);
|
||||||
await expect(controlsToggle(page, "unrelated")).toHaveCount(0);
|
await expect(controlsToggle(page, "unrelated")).toHaveCount(1);
|
||||||
await expect(controls(page, "capture")).toBeHidden();
|
await expect(controls(page, "capture")).toHaveCount(0);
|
||||||
await openControls(page, "capture");
|
await openControls(page, "capture");
|
||||||
await expect(controls(page, "capture").getByRole("button", { name: "Masquer les cochés" })).toBeDisabled();
|
await expect(controls(page, "capture").getByRole("button", { name: "Masquer les cochés" })).toBeDisabled();
|
||||||
const before = await readModule(page, "forms");
|
const before = await readModule(page, "forms");
|
||||||
|
|
@ -218,15 +218,14 @@ for (const width of [1280, 390]) {
|
||||||
await expect(heading).toHaveAttribute("aria-expanded", "false");
|
await expect(heading).toHaveAttribute("aria-expanded", "false");
|
||||||
expect(await heading.evaluate((button) => button.previousElementSibling.classList.contains("module-edit-button"))).toBe(true);
|
expect(await heading.evaluate((button) => button.previousElementSibling.classList.contains("module-edit-button"))).toBe(true);
|
||||||
await heading.press("Enter");
|
await heading.press("Enter");
|
||||||
await expect(heading).toHaveAttribute("aria-pressed", "true");
|
await expect(heading).toHaveAttribute("aria-expanded", "true");
|
||||||
await expect(controls(page, "forms")).toHaveAttribute("id", await heading.getAttribute("aria-controls"));
|
await expect(page.locator(`[id="${await heading.getAttribute("aria-controls")}"]`)).toHaveClass(/module-configuration-panel/);
|
||||||
await linkButton(page, "forms").press("Enter");
|
await linkButton(page, "forms").press("Enter");
|
||||||
await expect(linkButton(page, "forms")).toHaveAttribute("aria-pressed", "true");
|
await expect(linkButton(page, "forms")).toHaveAttribute("aria-pressed", "true");
|
||||||
const panel = controls(page, "forms");
|
const panel = controls(page, "forms");
|
||||||
await heading.press("Space");
|
await heading.press("Space");
|
||||||
await expect(heading).toHaveAttribute("aria-expanded", "false");
|
await expect(heading).toHaveAttribute("aria-expanded", "false");
|
||||||
await expect(heading).toHaveAttribute("aria-pressed", "false");
|
await expect(heading).toHaveAttribute("data-guide-linked", "true");
|
||||||
await expect(heading).toHaveAttribute("data-linked", "true");
|
|
||||||
await expect(linkButton(page, "forms")).toBeHidden();
|
await expect(linkButton(page, "forms")).toBeHidden();
|
||||||
await expect(card).toHaveAttribute("data-checklist-complete", "true");
|
await expect(card).toHaveAttribute("data-checklist-complete", "true");
|
||||||
await heading.press("Enter");
|
await heading.press("Enter");
|
||||||
|
|
@ -357,7 +356,7 @@ test("renames retain a zero-match selection; changing toolbox clears the associa
|
||||||
await dialog.getByRole("combobox").selectOption("other");
|
await dialog.getByRole("combobox").selectOption("other");
|
||||||
await dialog.getByRole("button", { name: "Valider", exact: true }).click();
|
await dialog.getByRole("button", { name: "Valider", exact: true }).click();
|
||||||
await expect(activeButtons(page)).toHaveCount(0);
|
await expect(activeButtons(page)).toHaveCount(0);
|
||||||
await expect(page.locator(".guide-checklist-controls")).toHaveCount(1);
|
await expect(page.locator(".guide-checklist-controls")).toHaveCount(0);
|
||||||
expect(await page.evaluate((key) => globalThis.localStorage.getItem(key), storageKey)).toBeNull();
|
expect(await page.evaluate((key) => globalThis.localStorage.getItem(key), storageKey)).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -398,7 +397,7 @@ test("unavailable local storage retains links in memory across navigation; overv
|
||||||
await page.goto("/#/toolbox/guide");
|
await page.goto("/#/toolbox/guide");
|
||||||
await expect(page.locator('[data-module-type="checklist"]')).toHaveCount(4);
|
await expect(page.locator('[data-module-type="checklist"]')).toHaveCount(4);
|
||||||
await expect(page.locator(".guide-checklist-controls")).toHaveCount(0);
|
await expect(page.locator(".guide-checklist-controls")).toHaveCount(0);
|
||||||
await expect(page.locator(".module-guide-button")).toHaveCount(0);
|
await expect(page.locator('.module-configuration-button[data-guide-linked="true"]')).toHaveCount(0);
|
||||||
await page.goto(route);
|
await page.goto(route);
|
||||||
await assertLink(page, "perfect", true);
|
await assertLink(page, "perfect", true);
|
||||||
await openDrawer(page);
|
await openDrawer(page);
|
||||||
|
|
|
||||||
|
|
@ -10,14 +10,41 @@ async function seedMemosToolbox(page) {
|
||||||
request.onsuccess = () => {
|
request.onsuccess = () => {
|
||||||
const db = request.result;
|
const db = request.result;
|
||||||
const tx = db.transaction(["kv", "modules"], "readwrite");
|
const tx = db.transaction(["kv", "modules"], "readwrite");
|
||||||
|
const soonReset = new Date(Date.now() + 60 * 60 * 1000);
|
||||||
|
const resetTime = `${String(soonReset.getHours()).padStart(2, "0")}:${String(soonReset.getMinutes()).padStart(2, "0")}`;
|
||||||
tx.objectStore("kv").put([
|
tx.objectStore("kv").put([
|
||||||
{ id: "memo-toolbox", name: "Toolbox mémos", modules: [{ id: "memos", type: "memos", title: "Mémos" }], moduleColumns: 1 }
|
{
|
||||||
|
id: "memo-toolbox",
|
||||||
|
name: "Toolbox mémos",
|
||||||
|
modules: [
|
||||||
|
{ id: "memos", type: "memos", title: "Mémos" },
|
||||||
|
{ id: "planner-one", type: "taskPlanner", title: "Urgent planner" },
|
||||||
|
{ id: "planner-two", type: "taskPlanner", title: "Other planner" }
|
||||||
|
],
|
||||||
|
moduleColumns: 1
|
||||||
|
}
|
||||||
], "toolboxes");
|
], "toolboxes");
|
||||||
tx.objectStore("kv").put({ mhwilds: "memo-toolbox" }, "links");
|
tx.objectStore("kv").put({ mhwilds: "memo-toolbox" }, "links");
|
||||||
tx.objectStore("modules").put({
|
tx.objectStore("modules").put({
|
||||||
key: "memo-toolbox:memos",
|
key: "memo-toolbox:memos",
|
||||||
data: { items: [{ id: "memo-1", text: "Préparer les potions", color: "#f6c453" }] }
|
data: { items: [{ id: "memo-1", text: "Préparer les potions", color: "#f6c453" }] }
|
||||||
});
|
});
|
||||||
|
tx.objectStore("modules").put({
|
||||||
|
key: "memo-toolbox:planner-one",
|
||||||
|
data: {
|
||||||
|
resetTime,
|
||||||
|
tasks: [
|
||||||
|
{ id: "category-parent", title: "Raid principal", type: "unique", category: "Raid" },
|
||||||
|
{ id: "daily-task", title: "Faire le donjon", type: "daily" },
|
||||||
|
{ id: "done-task", title: "Déjà terminée", type: "daily", checked: true, checkedAt: Date.now() }
|
||||||
|
],
|
||||||
|
relations: [{ id: "relation", fromTaskId: "daily-task", toTaskId: "category-parent" }]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
tx.objectStore("modules").put({
|
||||||
|
key: "memo-toolbox:planner-two",
|
||||||
|
data: { tasks: [{ id: "unique-task", title: "Ponctuelle", type: "unique" }] }
|
||||||
|
});
|
||||||
tx.oncomplete = () => { db.close(); resolve(); };
|
tx.oncomplete = () => { db.close(); resolve(); };
|
||||||
tx.onerror = () => reject(tx.error);
|
tx.onerror = () => reject(tx.error);
|
||||||
};
|
};
|
||||||
|
|
@ -54,3 +81,34 @@ test("shows and persists the memo ticker on linked game and full toolbox pages",
|
||||||
await page.goto("/#/toolboxes");
|
await page.goto("/#/toolboxes");
|
||||||
await expect(globalTicker).toHaveCount(0);
|
await expect(globalTicker).toHaveCount(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("links a planner, shows its reset warning and clears the link when it is removed", async ({ page }) => {
|
||||||
|
await seedMemosToolbox(page);
|
||||||
|
await page.goto("/#/toolbox/memo-toolbox");
|
||||||
|
|
||||||
|
const memos = page.locator('[data-module-id="memos"]');
|
||||||
|
await memos.getByRole("button", { name: "Configuration", exact: true }).click();
|
||||||
|
const dataSectionBox = await memos.locator(".module-configuration-data").boundingBox();
|
||||||
|
const settingsSectionBox = await memos.locator(".module-configuration-specific").boundingBox();
|
||||||
|
expect(settingsSectionBox.y).toBeGreaterThan(dataSectionBox.y + dataSectionBox.height);
|
||||||
|
expect(settingsSectionBox.width).toBeCloseTo(dataSectionBox.width, 0);
|
||||||
|
const plannerSelect = memos.getByRole("combobox", { name: "Planificateur lié", exact: true });
|
||||||
|
await expect(plannerSelect).toHaveValue("");
|
||||||
|
await plannerSelect.selectOption("planner-one");
|
||||||
|
|
||||||
|
const globalTicker = page.locator(".global-memos-ticker");
|
||||||
|
await expect(globalTicker).toContainText("Tâche : Raid Faire le donjon expire bientôt");
|
||||||
|
await expect(globalTicker).not.toContainText("Déjà terminée");
|
||||||
|
await expect(globalTicker.locator(".memos-ticker-item.is-task-warning .ui-icon-warning")).toHaveCount(2);
|
||||||
|
|
||||||
|
const planner = page.locator('[data-module-id="planner-one"]');
|
||||||
|
await planner.getByRole("button", { name: "Retirer Urgent planner", exact: true }).click();
|
||||||
|
await page.getByRole("dialog").getByRole("button", { name: "Retirer", exact: true }).click();
|
||||||
|
await expect(planner).toHaveCount(0);
|
||||||
|
await expect(plannerSelect).toHaveValue("");
|
||||||
|
await expect(globalTicker).not.toContainText("Faire le donjon");
|
||||||
|
|
||||||
|
await page.reload();
|
||||||
|
await memos.getByRole("button", { name: "Configuration", exact: true }).click();
|
||||||
|
await expect(plannerSelect).toHaveValue("");
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -97,7 +97,7 @@ test("recurring resets preserve unique completions and cleanup deletes only comp
|
||||||
await expect(hide).toHaveAttribute("aria-pressed", "true");
|
await expect(hide).toHaveAttribute("aria-pressed", "true");
|
||||||
await hide.click();
|
await hide.click();
|
||||||
await expect(tool.getByRole("checkbox", { name: "Marquer non effectué Parent", exact: true })).toBeChecked();
|
await expect(tool.getByRole("checkbox", { name: "Marquer non effectué Parent", exact: true })).toBeChecked();
|
||||||
await tool.getByRole("button", { name: "Réglages du planificateur", exact: true }).click();
|
await tool.getByRole("button", { name: "Configuration", exact: true }).click();
|
||||||
const cleanup = tool.getByRole("button", { name: "Nettoyer les tâches ponctuelles effectuées", exact: true });
|
const cleanup = tool.getByRole("button", { name: "Nettoyer les tâches ponctuelles effectuées", exact: true });
|
||||||
await cleanup.click();
|
await cleanup.click();
|
||||||
await expect(cleanup).toBeDisabled();
|
await expect(cleanup).toBeDisabled();
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ test("table text import/export preserves formulas and labels, copies read-only t
|
||||||
const table = page.locator('[data-module-type="table"]');
|
const table = page.locator('[data-module-type="table"]');
|
||||||
const importButton = table.getByRole("button", { name: "Importer TSV", exact: true });
|
const importButton = table.getByRole("button", { name: "Importer TSV", exact: true });
|
||||||
await expect(importButton).toBeHidden();
|
await expect(importButton).toBeHidden();
|
||||||
await table.getByRole("button", { name: "Afficher l'import/export de Tableau", exact: true }).click();
|
await table.getByRole("button", { name: "Configuration", exact: true }).click();
|
||||||
await expect(table.getByRole("button", { name: "Exporter TSV", exact: true })).toBeDisabled();
|
await expect(table.getByRole("button", { name: "Exporter TSV", exact: true })).toBeDisabled();
|
||||||
const originalOverflow = await page.locator("body").evaluate((body) => body.style.overflow);
|
const originalOverflow = await page.locator("body").evaluate((body) => body.style.overflow);
|
||||||
await importButton.click();
|
await importButton.click();
|
||||||
|
|
@ -49,7 +49,7 @@ test("table text import/export preserves formulas and labels, copies read-only t
|
||||||
await expect(table.getByRole("textbox", { name: "Intitulé de colonne A", exact: true })).toHaveValue("Quantity");
|
await expect(table.getByRole("textbox", { name: "Intitulé de colonne A", exact: true })).toHaveValue("Quantity");
|
||||||
await expect(table.getByRole("textbox", { name: "Intitulé de ligne 1", exact: true })).toHaveValue("Ore");
|
await expect(table.getByRole("textbox", { name: "Intitulé de ligne 1", exact: true })).toHaveValue("Ore");
|
||||||
await expect(table.getByRole("textbox", { name: "Cellule B1", exact: true })).toHaveValue("6");
|
await expect(table.getByRole("textbox", { name: "Cellule B1", exact: true })).toHaveValue("6");
|
||||||
await table.getByRole("button", { name: "Afficher l'import/export de Tableau", exact: true }).click();
|
await table.getByRole("button", { name: "Configuration", exact: true }).click();
|
||||||
await table.getByRole("button", { name: "Exporter TSV", exact: true }).click();
|
await table.getByRole("button", { name: "Exporter TSV", exact: true }).click();
|
||||||
const exported = page.getByRole("dialog", { name: "Exporter un tableau TSV", exact: true });
|
const exported = page.getByRole("dialog", { name: "Exporter un tableau TSV", exact: true });
|
||||||
await expect(exported.getByRole("textbox")).toHaveAttribute("readonly", "");
|
await expect(exported.getByRole("textbox")).toHaveAttribute("readonly", "");
|
||||||
|
|
@ -60,14 +60,15 @@ test("table text import/export preserves formulas and labels, copies read-only t
|
||||||
await exported.getByRole("textbox").press("Escape");
|
await exported.getByRole("textbox").press("Escape");
|
||||||
await expect(exported).toBeHidden();
|
await expect(exported).toBeHidden();
|
||||||
await expect.poll(() => page.locator("body").evaluate((body) => body.style.overflow)).toBe(originalOverflow);
|
await expect.poll(() => page.locator("body").evaluate((body) => body.style.overflow)).toBe(originalOverflow);
|
||||||
await table.getByRole("button", { name: "Masquer l'import/export de Tableau", exact: true }).click();
|
await table.getByRole("button", { name: "Configuration", exact: true }).click();
|
||||||
await expect(importButton).toBeHidden();
|
await expect(importButton).toBeHidden();
|
||||||
|
await expect(table.getByRole("button", { name: "Configuration", exact: true })).toHaveAttribute("aria-expanded", "false");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("invalid calculator imports preserve existing results and a corrected import replaces them with a persistent hierarchy", async ({ page }) => {
|
test("invalid calculator imports preserve existing results and a corrected import replaces them with a persistent hierarchy", async ({ page }) => {
|
||||||
await setup(page);
|
await setup(page);
|
||||||
const calc = page.locator('[data-module-type="calculator"]');
|
const calc = page.locator('[data-module-type="calculator"]');
|
||||||
await calc.getByRole("button", { name: "Afficher l'import/export de Calculateur", exact: true }).click();
|
await calc.getByRole("button", { name: "Configuration", exact: true }).click();
|
||||||
const openImport = calc.getByRole("button", { name: "Importer du texte", exact: true });
|
const openImport = calc.getByRole("button", { name: "Importer du texte", exact: true });
|
||||||
await openImport.click();
|
await openImport.click();
|
||||||
const dialog = page.getByRole("dialog", { name: "Importer des résultats", exact: true });
|
const dialog = page.getByRole("dialog", { name: "Importer des résultats", exact: true });
|
||||||
|
|
@ -78,7 +79,7 @@ test("invalid calculator imports preserve existing results and a corrected impor
|
||||||
await dialog.getByRole("button", { name: "Fermer", exact: true }).click();
|
await dialog.getByRole("button", { name: "Fermer", exact: true }).click();
|
||||||
await page.reload();
|
await page.reload();
|
||||||
await expect(calc.locator(".calculator-entry em")).toHaveText(["Original"]);
|
await expect(calc.locator(".calculator-entry em")).toHaveText(["Original"]);
|
||||||
await calc.getByRole("button", { name: "Afficher l'import/export de Calculateur", exact: true }).click();
|
await calc.getByRole("button", { name: "Configuration", exact: true }).click();
|
||||||
await openImport.click();
|
await openImport.click();
|
||||||
await expect(dialog.getByRole("textbox")).toHaveValue("");
|
await expect(dialog.getByRole("textbox")).toHaveValue("");
|
||||||
await expect(dialog.getByRole("alert")).toHaveCount(0);
|
await expect(dialog.getByRole("alert")).toHaveCount(0);
|
||||||
|
|
@ -97,7 +98,7 @@ test("invalid calculator imports preserve existing results and a corrected impor
|
||||||
await page.reload();
|
await page.reload();
|
||||||
await expect(calc.locator(".calculator-entry em")).toHaveText(["Ore", "Bars"]);
|
await expect(calc.locator(".calculator-entry em")).toHaveText(["Ore", "Bars"]);
|
||||||
await expect(calc.getByRole("button", { name: "Liste scrollable", exact: true })).toHaveAttribute("aria-pressed", "true");
|
await expect(calc.getByRole("button", { name: "Liste scrollable", exact: true })).toHaveAttribute("aria-pressed", "true");
|
||||||
await calc.getByRole("button", { name: "Afficher l'import/export de Calculateur", exact: true }).click();
|
await calc.getByRole("button", { name: "Configuration", exact: true }).click();
|
||||||
await calc.getByRole("button", { name: "Exporter en format texte", exact: true }).click();
|
await calc.getByRole("button", { name: "Exporter en format texte", exact: true }).click();
|
||||||
const exported = page.getByRole("dialog", { name: "Exporter des résultats", exact: true });
|
const exported = page.getByRole("dialog", { name: "Exporter des résultats", exact: true });
|
||||||
await expect(exported.getByRole("textbox")).toHaveValue(text);
|
await expect(exported.getByRole("textbox")).toHaveValue(text);
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,18 @@ test("creates at most five spaces and moves tools with the accessible selector",
|
||||||
await expect(page.getByRole("tab", { name: "Divers" })).toHaveAttribute("aria-selected", "true");
|
await expect(page.getByRole("tab", { name: "Divers" })).toHaveAttribute("aria-selected", "true");
|
||||||
|
|
||||||
await page.getByRole("tab", { name: "Défaut" }).click();
|
await page.getByRole("tab", { name: "Défaut" }).click();
|
||||||
await page.getByRole("combobox", { name: "Déplacer vers… — Alpha" }).selectOption({ label: "Divers" });
|
const alpha = page.locator("[data-module-id='a']");
|
||||||
|
await alpha.getByRole("button", { name: "Configuration", exact: true }).click();
|
||||||
|
const organization = alpha.locator(".module-configuration-organization");
|
||||||
|
const data = alpha.locator(".module-configuration-data");
|
||||||
|
await expect(organization).toBeVisible();
|
||||||
|
await expect(data).toBeVisible();
|
||||||
|
const organizationBox = await organization.boundingBox();
|
||||||
|
const dataBox = await data.boundingBox();
|
||||||
|
expect(dataBox.y).toBeCloseTo(organizationBox.y, 0);
|
||||||
|
expect(dataBox.x).toBeGreaterThan(organizationBox.x + organizationBox.width);
|
||||||
|
expect(dataBox.width).toBeCloseTo(organizationBox.width, 0);
|
||||||
|
await alpha.getByRole("combobox", { name: "Déplacer vers un autre espace", exact: true }).selectOption({ label: "Divers" });
|
||||||
await expect(page.locator("[data-module-id='a']")).toHaveCount(0);
|
await expect(page.locator("[data-module-id='a']")).toHaveCount(0);
|
||||||
await page.getByRole("tab", { name: "Divers" }).click();
|
await page.getByRole("tab", { name: "Divers" }).click();
|
||||||
await expect(page.locator("[data-module-id='a']")).toBeVisible();
|
await expect(page.locator("[data-module-id='a']")).toBeVisible();
|
||||||
|
|
@ -66,6 +77,52 @@ test("creates at most five spaces and moves tools with the accessible selector",
|
||||||
await expect(page.getByRole("tab", { name: "Défaut" })).toHaveAttribute("aria-selected", "true");
|
await expect(page.getByRole("tab", { name: "Défaut" })).toHaveAttribute("aria-selected", "true");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("keeps the compact header order and isolates configuration from add and collapsed content", async ({ page }) => {
|
||||||
|
await page.setViewportSize({ width: 390, height: 900 });
|
||||||
|
await seedToolbox(page, {
|
||||||
|
id: "module-configuration",
|
||||||
|
name: "Module configuration",
|
||||||
|
modules: [{ id: "a", type: "counters", title: "Alpha" }],
|
||||||
|
moduleOrder: { one: ["a"], two: [["a"], []] }
|
||||||
|
});
|
||||||
|
|
||||||
|
const tool = page.locator("[data-module-id='a']");
|
||||||
|
await expect(tool.locator(".module-header-actions > button")).toHaveCount(5);
|
||||||
|
expect(await tool.locator(".module-header-actions > button").evaluateAll((buttons) => buttons.map((button) => button.className.split(" ")[0]))).toEqual([
|
||||||
|
"module-scroll-button",
|
||||||
|
"module-edit-button",
|
||||||
|
"module-configuration-button",
|
||||||
|
"module-delete-button",
|
||||||
|
"module-collapse-button"
|
||||||
|
]);
|
||||||
|
|
||||||
|
const add = tool.getByRole("button", { name: "Afficher l'ajout de Alpha", exact: true });
|
||||||
|
const configuration = tool.getByRole("button", { name: "Configuration", exact: true });
|
||||||
|
await add.click();
|
||||||
|
await expect(tool.locator(".module-add-panel")).toBeVisible();
|
||||||
|
await configuration.click();
|
||||||
|
await expect(add).toHaveAttribute("aria-pressed", "false");
|
||||||
|
await expect(tool.locator(".module-add-panel")).toHaveCount(0);
|
||||||
|
const panel = tool.getByRole("region", { name: "Configuration de Alpha", exact: true });
|
||||||
|
await expect(panel).toBeVisible();
|
||||||
|
await expect(panel.getByRole("button", { name: "Importer du texte", exact: true })).toBeVisible();
|
||||||
|
const toolBox = await tool.boundingBox();
|
||||||
|
const panelBox = await panel.boundingBox();
|
||||||
|
expect(panelBox.width).toBeGreaterThan(toolBox.width - 24);
|
||||||
|
expect(panelBox.width).toBeLessThan(toolBox.width);
|
||||||
|
|
||||||
|
await tool.getByRole("button", { name: "Masquer Alpha", exact: true }).click();
|
||||||
|
await expect(panel).toHaveCount(0);
|
||||||
|
await expect(tool.locator(".module-content")).toHaveCount(0);
|
||||||
|
await configuration.click();
|
||||||
|
await expect(panel).toBeVisible();
|
||||||
|
await expect(tool.locator(".module-content")).toBeHidden();
|
||||||
|
await expect(panel.getByRole("button", { name: "Importer du texte", exact: true })).toBeVisible();
|
||||||
|
await configuration.press("Escape");
|
||||||
|
await expect(panel).toHaveCount(0);
|
||||||
|
await expect(configuration).toHaveAttribute("aria-expanded", "false");
|
||||||
|
});
|
||||||
|
|
||||||
test("shows the tab insertion indicator on the correct side while reordering", async ({ page }) => {
|
test("shows the tab insertion indicator on the correct side while reordering", async ({ page }) => {
|
||||||
await seedToolbox(page, {
|
await seedToolbox(page, {
|
||||||
id: "reorder-spaces",
|
id: "reorder-spaces",
|
||||||
|
|
|
||||||
|
|
@ -248,6 +248,14 @@ export function validateSiteContent(site) {
|
||||||
"toolboxes.spaces.moveSuccess",
|
"toolboxes.spaces.moveSuccess",
|
||||||
"toolboxes.spaces.emptyText",
|
"toolboxes.spaces.emptyText",
|
||||||
"toolboxes.spaces.limitReached",
|
"toolboxes.spaces.limitReached",
|
||||||
|
"toolboxes.moduleConfiguration.buttonLabel",
|
||||||
|
"toolboxes.moduleConfiguration.panelLabel",
|
||||||
|
"toolboxes.moduleConfiguration.organizationTitle",
|
||||||
|
"toolboxes.moduleConfiguration.spaceLabel",
|
||||||
|
"toolboxes.moduleConfiguration.guideTitle",
|
||||||
|
"toolboxes.moduleConfiguration.dataTitle",
|
||||||
|
"toolboxes.moduleConfiguration.importButtonLabel",
|
||||||
|
"toolboxes.moduleConfiguration.exportButtonLabel",
|
||||||
"toolboxes.drawer.emptyText",
|
"toolboxes.drawer.emptyText",
|
||||||
"toolboxes.drawer.createLabel",
|
"toolboxes.drawer.createLabel",
|
||||||
"toolboxes.drawer.linkExistingTitle",
|
"toolboxes.drawer.linkExistingTitle",
|
||||||
|
|
@ -345,6 +353,12 @@ export function validateSiteContent(site) {
|
||||||
"toolboxes.modules.memos.tickerLabel",
|
"toolboxes.modules.memos.tickerLabel",
|
||||||
"toolboxes.modules.memos.hideTickerTitle",
|
"toolboxes.modules.memos.hideTickerTitle",
|
||||||
"toolboxes.modules.memos.showTickerTitle",
|
"toolboxes.modules.memos.showTickerTitle",
|
||||||
|
"toolboxes.modules.memos.settingsTitle",
|
||||||
|
"toolboxes.modules.memos.plannerLinkLabel",
|
||||||
|
"toolboxes.modules.memos.plannerNoneLabel",
|
||||||
|
"toolboxes.modules.memos.plannerAllLabel",
|
||||||
|
"toolboxes.modules.memos.plannerDefaultLabel",
|
||||||
|
"toolboxes.modules.memos.taskAlertTemplate",
|
||||||
"toolboxes.modules.memos.importPlaceholder",
|
"toolboxes.modules.memos.importPlaceholder",
|
||||||
"toolboxes.modules.memos.importOpenButton",
|
"toolboxes.modules.memos.importOpenButton",
|
||||||
"toolboxes.modules.memos.exportOpenButton",
|
"toolboxes.modules.memos.exportOpenButton",
|
||||||
|
|
@ -830,7 +844,6 @@ export function validateSiteContent(site) {
|
||||||
"toolboxes.modules.calendar.requiredError",
|
"toolboxes.modules.calendar.requiredError",
|
||||||
"toolboxes.modules.calendar.invalidRangeError",
|
"toolboxes.modules.calendar.invalidRangeError",
|
||||||
"toolboxes.modules.taskPlanner.settingsTitle",
|
"toolboxes.modules.taskPlanner.settingsTitle",
|
||||||
"toolboxes.modules.taskPlanner.settingsButtonLabel",
|
|
||||||
"toolboxes.modules.taskPlanner.hideCompletedTitle",
|
"toolboxes.modules.taskPlanner.hideCompletedTitle",
|
||||||
"toolboxes.modules.taskPlanner.globalWeeklyResetDayLabel",
|
"toolboxes.modules.taskPlanner.globalWeeklyResetDayLabel",
|
||||||
"toolboxes.modules.taskPlanner.resetTimeLabel",
|
"toolboxes.modules.taskPlanner.resetTimeLabel",
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { test } from "node:test";
|
||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import { SOCKET_SHAPES, formatValue, getSocketItemName, setItemCategory } from "../website/src/features/toolboxes/modules/equipmentPlannerOptions.js";
|
import { SOCKET_SHAPES, formatValue, getSocketItemName, setItemCategory } from "../website/src/features/toolboxes/modules/equipmentPlannerOptions.js";
|
||||||
import { applyDueResets, getTaskParentMap, getTaskTreeIds, isTaskResetSoon, moveTask } from "../website/src/features/toolboxes/modules/taskPlannerUtils.js";
|
import { applyDueResets, getTaskParentMap, getTaskTreeIds, isTaskResetSoon, moveTask } from "../website/src/features/toolboxes/modules/taskPlannerUtils.js";
|
||||||
|
import { getMemosDataAfterPlannerRemoval, getPlannerTaskTickerItems, resolveMemosPlannerLink } from "../website/src/features/toolboxes/modules/memosPlannerUtils.js";
|
||||||
import { createInput, getInputLabel, normalizeCustomKey, parseHoldSeconds } from "../website/src/features/toolboxes/modules/combosUtils.js";
|
import { createInput, getInputLabel, normalizeCustomKey, parseHoldSeconds } from "../website/src/features/toolboxes/modules/combosUtils.js";
|
||||||
import { getBoundsFromStartAndSize, getSelectionBounds, moveSelectionCells, parseTsvBlock, pasteCellBlock } from "../website/src/features/toolboxes/modules/tableUtils.js";
|
import { getBoundsFromStartAndSize, getSelectionBounds, moveSelectionCells, parseTsvBlock, pasteCellBlock } from "../website/src/features/toolboxes/modules/tableUtils.js";
|
||||||
import { canToggleAutoRefresh, getCountdownValidationError, isAlertGuarded } from "../website/src/features/toolboxes/modules/timerOptions.js";
|
import { canToggleAutoRefresh, getCountdownValidationError, isAlertGuarded } from "../website/src/features/toolboxes/modules/timerOptions.js";
|
||||||
|
|
@ -97,7 +98,8 @@ test("toolbox module registry and modules expose expected behavior", async () =>
|
||||||
const toolboxPages = await readFile("website/src/features/toolboxes/ToolboxPages.jsx", "utf8");
|
const toolboxPages = await readFile("website/src/features/toolboxes/ToolboxPages.jsx", "utf8");
|
||||||
const toolboxModules = await readFile("website/src/features/toolboxes/modules/ToolboxModules.jsx", "utf8");
|
const toolboxModules = await readFile("website/src/features/toolboxes/modules/ToolboxModules.jsx", "utf8");
|
||||||
const moduleShell = await readFile("website/src/features/toolboxes/modules/ModuleShell.jsx", "utf8");
|
const moduleShell = await readFile("website/src/features/toolboxes/modules/ModuleShell.jsx", "utf8");
|
||||||
const moduleRegistryBundle = `${moduleRegistry}\n${moduleComponents}\n${addToolControls}\n${toolboxModules}\n${moduleShell}`;
|
const moduleConfiguration = await readFile("website/src/features/toolboxes/modules/ModuleConfiguration.jsx", "utf8");
|
||||||
|
const moduleRegistryBundle = `${moduleRegistry}\n${moduleComponents}\n${addToolControls}\n${toolboxModules}\n${moduleShell}\n${moduleConfiguration}`;
|
||||||
const moduleMetadataRegistry = await readFile("website/src/features/toolboxes/toolboxModuleRegistry.js", "utf8");
|
const moduleMetadataRegistry = await readFile("website/src/features/toolboxes/toolboxModuleRegistry.js", "utf8");
|
||||||
const notepadModule = await readFile("website/src/features/toolboxes/modules/NotepadModule.jsx", "utf8");
|
const notepadModule = await readFile("website/src/features/toolboxes/modules/NotepadModule.jsx", "utf8");
|
||||||
const notepadToolbar = await readFile("website/src/features/toolboxes/modules/NotepadToolbar.jsx", "utf8");
|
const notepadToolbar = await readFile("website/src/features/toolboxes/modules/NotepadToolbar.jsx", "utf8");
|
||||||
|
|
@ -139,7 +141,7 @@ test("toolbox module registry and modules expose expected behavior", async () =>
|
||||||
const taskPlannerReorder = await readFile("website/src/features/toolboxes/modules/useTaskPlannerReorder.js", "utf8");
|
const taskPlannerReorder = await readFile("website/src/features/toolboxes/modules/useTaskPlannerReorder.js", "utf8");
|
||||||
const taskPlannerTopbar = await readFile("website/src/features/toolboxes/modules/TaskPlannerTopbar.jsx", "utf8");
|
const taskPlannerTopbar = await readFile("website/src/features/toolboxes/modules/TaskPlannerTopbar.jsx", "utf8");
|
||||||
const taskPlannerSettings = await readFile("website/src/features/toolboxes/modules/TaskPlannerSettings.jsx", "utf8");
|
const taskPlannerSettings = await readFile("website/src/features/toolboxes/modules/TaskPlannerSettings.jsx", "utf8");
|
||||||
const taskPlannerBundle = `${taskPlannerBranch}\n${taskPlannerItem}\n${taskPlannerItemPanels}\n${taskPlannerReorder}\n${taskPlannerTopbar}\n${taskPlannerSettings}`;
|
const taskPlannerBundle = `${taskPlannerModule}\n${taskPlannerBranch}\n${taskPlannerItem}\n${taskPlannerItemPanels}\n${taskPlannerReorder}\n${taskPlannerTopbar}\n${taskPlannerSettings}`;
|
||||||
const goalTreeModule = await readFile("website/src/features/toolboxes/modules/GoalTreeModule.jsx", "utf8");
|
const goalTreeModule = await readFile("website/src/features/toolboxes/modules/GoalTreeModule.jsx", "utf8");
|
||||||
const goalTreeEditor = await readFile("website/src/features/toolboxes/modules/GoalTreeEditor.jsx", "utf8");
|
const goalTreeEditor = await readFile("website/src/features/toolboxes/modules/GoalTreeEditor.jsx", "utf8");
|
||||||
const goalTreeParts = await readFile("website/src/features/toolboxes/modules/GoalTreeParts.jsx", "utf8");
|
const goalTreeParts = await readFile("website/src/features/toolboxes/modules/GoalTreeParts.jsx", "utf8");
|
||||||
|
|
@ -240,12 +242,18 @@ test("toolbox module registry and modules expose expected behavior", async () =>
|
||||||
assert.match(moduleShell, /module-scroll-button/);
|
assert.match(moduleShell, /module-scroll-button/);
|
||||||
assert.match(moduleShell, /module-collapse-button/);
|
assert.match(moduleShell, /module-collapse-button/);
|
||||||
assert.match(moduleShell, /aria-expanded=\{!collapsed\}/);
|
assert.match(moduleShell, /aria-expanded=\{!collapsed\}/);
|
||||||
assert.match(moduleShell, /!collapsed &&/);
|
assert.match(moduleShell, /!collapsed \|\| configurationOpen/);
|
||||||
|
assert.match(moduleShell, /module-configuration-button/);
|
||||||
|
assert.match(moduleShell, /aria-controls=\{configurationId\}/);
|
||||||
|
assert.match(moduleShell, /module-configuration-panel/);
|
||||||
|
assert.match(moduleShell, /hidden=\{collapsed\}/);
|
||||||
|
assert.match(moduleConfiguration, /createPortal/);
|
||||||
|
assert.doesNotMatch(moduleShell, /module-exchange-button/);
|
||||||
assert.match(addToolControls, /Icon name="dropdown"/);
|
assert.match(addToolControls, /Icon name="dropdown"/);
|
||||||
assert.match(addToolControls, /CompactDropdown/);
|
assert.match(addToolControls, /CompactDropdown/);
|
||||||
assert.doesNotMatch(source, /<select value="" onChange=\{\(event\) => addModule/);
|
assert.doesNotMatch(source, /<select value="" onChange=\{\(event\) => addModule/);
|
||||||
assert.match(notepadModule, /export function NotepadModule/);
|
assert.match(notepadModule, /export function NotepadModule/);
|
||||||
assert.match(notepadModule, /TextExchangeActions/);
|
assert.match(notepadModule, /ModuleTextExchangeConfiguration/);
|
||||||
assert.match(notepadModule, /type="notepad"/);
|
assert.match(notepadModule, /type="notepad"/);
|
||||||
assert.match(notepadToolbar, /export function NotepadToolbar/);
|
assert.match(notepadToolbar, /export function NotepadToolbar/);
|
||||||
assert.match(notepadToolbar, /FormattingColorDropdown/);
|
assert.match(notepadToolbar, /FormattingColorDropdown/);
|
||||||
|
|
@ -300,7 +308,7 @@ test("toolbox module registry and modules expose expected behavior", async () =>
|
||||||
assert.match(timerBundle, /useInlineEdit/);
|
assert.match(timerBundle, /useInlineEdit/);
|
||||||
assert.match(timerModule, /useDraftForm/);
|
assert.match(timerModule, /useDraftForm/);
|
||||||
assert.match(timerModule, /normalizeTimerData/);
|
assert.match(timerModule, /normalizeTimerData/);
|
||||||
assert.match(timerModule, /TextExchangeActions/);
|
assert.match(timerModule, /ModuleTextExchangeConfiguration/);
|
||||||
assert.match(calendarModule, /export function CalendarModule/);
|
assert.match(calendarModule, /export function CalendarModule/);
|
||||||
assert.match(calendarModule, /normalizeCalendarData/);
|
assert.match(calendarModule, /normalizeCalendarData/);
|
||||||
assert.match(calendarBundle, /calendar-mode-choice/);
|
assert.match(calendarBundle, /calendar-mode-choice/);
|
||||||
|
|
@ -322,7 +330,7 @@ test("toolbox module registry and modules expose expected behavior", async () =>
|
||||||
assert.match(calendarBundle, /data-size=\{eventSurfaceSize\}/);
|
assert.match(calendarBundle, /data-size=\{eventSurfaceSize\}/);
|
||||||
assert.match(calendarBundle, /is-compact/);
|
assert.match(calendarBundle, /is-compact/);
|
||||||
assert.match(calendarController, /handleKeyDown/);
|
assert.match(calendarController, /handleKeyDown/);
|
||||||
assert.match(calendarContent, /TextExchangeActions/);
|
assert.match(calendarContent, /ModuleTextExchangeConfiguration/);
|
||||||
assert.match(calendarContent, /type="calendar"/);
|
assert.match(calendarContent, /type="calendar"/);
|
||||||
assert.match(calendarBundle, /repeatLabel/);
|
assert.match(calendarBundle, /repeatLabel/);
|
||||||
assert.match(calendarBundle, /CalendarAlertModeSwitch/);
|
assert.match(calendarBundle, /CalendarAlertModeSwitch/);
|
||||||
|
|
@ -342,7 +350,7 @@ test("toolbox module registry and modules expose expected behavior", async () =>
|
||||||
assert.match(goalTreeModule, /export function GoalTreeModule/);
|
assert.match(goalTreeModule, /export function GoalTreeModule/);
|
||||||
assert.match(goalTreeBundle, /normalizeGoalTreeData/);
|
assert.match(goalTreeBundle, /normalizeGoalTreeData/);
|
||||||
assert.match(goalTreeBundle, /applyGoalTreeDueResets/);
|
assert.match(goalTreeBundle, /applyGoalTreeDueResets/);
|
||||||
assert.match(goalTreeModule, /TextExchangeActions/);
|
assert.match(goalTreeModule, /ModuleTextExchangeConfiguration/);
|
||||||
assert.match(goalTreeBundle, /DiagramConnectors/);
|
assert.match(goalTreeBundle, /DiagramConnectors/);
|
||||||
assert.match(goalTreeEditor, /export function GoalTreeEditor/);
|
assert.match(goalTreeEditor, /export function GoalTreeEditor/);
|
||||||
assert.match(goalTreeEditor, /Tabs/);
|
assert.match(goalTreeEditor, /Tabs/);
|
||||||
|
|
@ -388,14 +396,14 @@ test("toolbox module registry and modules expose expected behavior", async () =>
|
||||||
assert.match(equipmentPlannerEditors, /export \{ CharacteristicsEditor \}/);
|
assert.match(equipmentPlannerEditors, /export \{ CharacteristicsEditor \}/);
|
||||||
assert.match(equipmentPlannerEditors, /export \{ MaterialsEditor \}/);
|
assert.match(equipmentPlannerEditors, /export \{ MaterialsEditor \}/);
|
||||||
assert.match(equipmentPlannerEditors, /export \{ SocketItemsEditor \}/);
|
assert.match(equipmentPlannerEditors, /export \{ SocketItemsEditor \}/);
|
||||||
assert.match(equipmentPlannerModule, /TextExchangeActions/);
|
assert.match(equipmentPlannerModule, /ModuleTextExchangeConfiguration/);
|
||||||
assert.match(equipmentPlannerItem, /export function EquipmentItem/);
|
assert.match(equipmentPlannerItem, /export function EquipmentItem/);
|
||||||
assert.match(equipmentPlannerPickers, /export function IconChoiceDropdown/);
|
assert.match(equipmentPlannerPickers, /export function IconChoiceDropdown/);
|
||||||
assert.match(equipmentPlannerSummary, /export function EquipmentPlannerSummary/);
|
assert.match(equipmentPlannerSummary, /export function EquipmentPlannerSummary/);
|
||||||
assert.match(equipmentPlannerOptions, /export const EQUIPMENT_ICONS/);
|
assert.match(equipmentPlannerOptions, /export const EQUIPMENT_ICONS/);
|
||||||
assert.match(equipmentPlannerOptions, /export function formatValue/);
|
assert.match(equipmentPlannerOptions, /export function formatValue/);
|
||||||
assert.match(checklistModule, /export function ChecklistModule/);
|
assert.match(checklistModule, /export function ChecklistModule/);
|
||||||
assert.match(checklistModule, /TextExchangeActions/);
|
assert.match(checklistModule, /ModuleTextExchangeConfiguration/);
|
||||||
assert.match(checklistModule, /editing &&/);
|
assert.match(checklistModule, /editing &&/);
|
||||||
assert.match(checklistParts, /ChecklistItem/);
|
assert.match(checklistParts, /ChecklistItem/);
|
||||||
assert.match(checklistParts, /export function ChecklistSection/);
|
assert.match(checklistParts, /export function ChecklistSection/);
|
||||||
|
|
@ -420,7 +428,7 @@ test("toolbox module registry and modules expose expected behavior", async () =>
|
||||||
assert.match(imagesModule, /editing &&/);
|
assert.match(imagesModule, /editing &&/);
|
||||||
assert.match(imagesModule, /clipboardData/);
|
assert.match(imagesModule, /clipboardData/);
|
||||||
assert.match(imagesModule, /createImageAnnotationModule/);
|
assert.match(imagesModule, /createImageAnnotationModule/);
|
||||||
assert.match(imagesModule, /TextExchangeActions/);
|
assert.match(imagesModule, /ModuleTextExchangeConfiguration/);
|
||||||
assert.match(imagesModule, /image-annotate-button/);
|
assert.match(imagesModule, /image-annotate-button/);
|
||||||
assert.match(imagesModule, /kind: "collection"/);
|
assert.match(imagesModule, /kind: "collection"/);
|
||||||
assert.match(imagesModule, /galleryButton/);
|
assert.match(imagesModule, /galleryButton/);
|
||||||
|
|
@ -432,15 +440,15 @@ test("toolbox module registry and modules expose expected behavior", async () =>
|
||||||
assert.match(imageAnnotationModule, /context\.setImage/);
|
assert.match(imageAnnotationModule, /context\.setImage/);
|
||||||
assert.match(imageAnnotationModule, /createMarkerId/);
|
assert.match(imageAnnotationModule, /createMarkerId/);
|
||||||
assert.match(imageAnnotationModule, /<Icon name="zoom"/);
|
assert.match(imageAnnotationModule, /<Icon name="zoom"/);
|
||||||
assert.match(imageAnnotationModule, /TextExchangeActions/);
|
assert.match(imageAnnotationModule, /ModuleTextExchangeConfiguration/);
|
||||||
assert.match(linksModule, /TextExchangeActions/);
|
assert.match(linksModule, /ModuleTextExchangeConfiguration/);
|
||||||
assert.match(linksModule, /normalizeUrl/);
|
assert.match(linksModule, /normalizeUrl/);
|
||||||
assert.match(linksModule, /useGroupedReorder/);
|
assert.match(linksModule, /useGroupedReorder/);
|
||||||
assert.match(linksModule, /link-drag-handle/);
|
assert.match(linksModule, /link-drag-handle/);
|
||||||
assert.match(countersModule, /useGroupedReorder/);
|
assert.match(countersModule, /useGroupedReorder/);
|
||||||
assert.match(countersModule, /orientation: "horizontal"/);
|
assert.match(countersModule, /orientation: "horizontal"/);
|
||||||
assert.match(countersModule, /counter-drag-handle/);
|
assert.match(countersModule, /counter-drag-handle/);
|
||||||
assert.match(countersModule, /TextExchangeActions/);
|
assert.match(countersModule, /ModuleTextExchangeConfiguration/);
|
||||||
assert.match(timerModule, /useGroupedReorder/);
|
assert.match(timerModule, /useGroupedReorder/);
|
||||||
assert.match(timerModule, /reorderEnabled={!data\.sortResults}/);
|
assert.match(timerModule, /reorderEnabled={!data\.sortResults}/);
|
||||||
assert.match(timerModule, /parentId=\{moduleId\}/);
|
assert.match(timerModule, /parentId=\{moduleId\}/);
|
||||||
|
|
@ -510,6 +518,41 @@ test("task planner helpers keep hierarchy, reorder and reset behavior stable", (
|
||||||
assert.equal(isTaskResetSoon({ id: "h", title: "H", type: "unique", checked: false }, resetData, new Date(2026, 0, 6, 7, 0).getTime()), false);
|
assert.equal(isTaskResetSoon({ id: "h", title: "H", type: "unique", checked: false }, resetData, new Date(2026, 0, 6, 7, 0).getTime()), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("memos planner links select reset alerts and clear removed targets", () => {
|
||||||
|
const modules = [
|
||||||
|
{ id: "planner-a", type: "taskPlanner", title: "Raids" },
|
||||||
|
{ id: "planner-b", type: "taskPlanner", title: "Farm" },
|
||||||
|
{ id: "notes", type: "memos" }
|
||||||
|
];
|
||||||
|
const plannerData = {
|
||||||
|
"planner-a": {
|
||||||
|
resetTime: "06:00",
|
||||||
|
weeklyResetDay: 1,
|
||||||
|
tasks: [
|
||||||
|
{ id: "parent", title: "Parent", type: "unique", category: "Raid" },
|
||||||
|
{ id: "soon", title: "Donjon", type: "daily" },
|
||||||
|
{ id: "done", title: "Déjà faite", type: "daily", checked: true, checkedAt: Date.UTC(2026, 0, 2, 0) }
|
||||||
|
],
|
||||||
|
relations: [{ id: "relation", fromTaskId: "soon", toTaskId: "parent" }]
|
||||||
|
},
|
||||||
|
"planner-b": { resetTime: "18:00", tasks: [{ id: "later", title: "Récolte", type: "daily" }] }
|
||||||
|
};
|
||||||
|
const alerts = getPlannerTaskTickerItems({
|
||||||
|
plannerLink: "all",
|
||||||
|
modules,
|
||||||
|
getPlannerData: (plannerId) => plannerData[plannerId],
|
||||||
|
nowMs: Date.UTC(2026, 0, 2, 1),
|
||||||
|
alertTemplate: "Tâche : {category}{title} expire bientôt"
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(alerts.map((item) => item.text), ["Tâche : Raid Donjon expire bientôt"]);
|
||||||
|
assert.equal(alerts[0].kind, "task-warning");
|
||||||
|
assert.equal(resolveMemosPlannerLink("missing", modules), "");
|
||||||
|
assert.equal(getMemosDataAfterPlannerRemoval({ plannerLink: "planner-a" }, "planner-a", modules.slice(1)).plannerLink, "");
|
||||||
|
assert.equal(getMemosDataAfterPlannerRemoval({ plannerLink: "all" }, "planner-a", modules.slice(1)).plannerLink, "all");
|
||||||
|
assert.equal(getMemosDataAfterPlannerRemoval({ plannerLink: "all" }, "planner-b", [{ id: "notes", type: "memos" }]).plannerLink, "");
|
||||||
|
});
|
||||||
|
|
||||||
test("equipment planner options expose stable formatting and socket helpers", () => {
|
test("equipment planner options expose stable formatting and socket helpers", () => {
|
||||||
assert.deepEqual(SOCKET_SHAPES, ["ball", "jewel"]);
|
assert.deepEqual(SOCKET_SHAPES, ["ball", "jewel"]);
|
||||||
assert.equal(formatValue(1.23456), "1.235");
|
assert.equal(formatValue(1.23456), "1.235");
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,34 @@ import { formatUpdatedAt, isHtmlEmpty, queryBlockFormat, queryCommandState } fro
|
||||||
import { createBalancedTeams, drawWeightedEntries, filterExclusionsForPlayers, getDiceShape, rollDice } from "../website/src/features/toolboxes/modules/randomizerUtils.js";
|
import { createBalancedTeams, drawWeightedEntries, filterExclusionsForPlayers, getDiceShape, rollDice } from "../website/src/features/toolboxes/modules/randomizerUtils.js";
|
||||||
import { calculateAtLeastOneProbability, calculateDrawsForChance, calculateRngCostSummary, calculateRngRealDrawsSummary, calculateRngSummary, getAveragePositionStatus, getDurationPresetsForAverageHours, getEffectiveRate, normalizeCostString, normalizeRateString, simulateRngDraws } from "../website/src/features/toolboxes/modules/rngSimulatorUtils.js";
|
import { calculateAtLeastOneProbability, calculateDrawsForChance, calculateRngCostSummary, calculateRngRealDrawsSummary, calculateRngSummary, getAveragePositionStatus, getDurationPresetsForAverageHours, getEffectiveRate, normalizeCostString, normalizeRateString, simulateRngDraws } from "../website/src/features/toolboxes/modules/rngSimulatorUtils.js";
|
||||||
import { getTimePatternRecurrenceMs, getTimePatternTargetMs } from "../website/src/features/toolboxes/modules/timerUtils.js";
|
import { getTimePatternRecurrenceMs, getTimePatternTargetMs } from "../website/src/features/toolboxes/modules/timerUtils.js";
|
||||||
import { compactModuleDataForStorage, createToolboxExportPayload, moduleStorageKey, normalizeCalculatorData, normalizeChecklistData, normalizeCombosData, normalizeCountersData, normalizeEquipmentPlannerData, normalizeGoalTreeData, normalizeImageAnnotationData, normalizeLinksData, normalizeMemosData, normalizeModuleOrder, normalizeNotepadData, normalizeRandomizerData, normalizeRngSimulatorData, normalizeTableData, normalizeTaskPlannerData, normalizeTimerData, normalizeToolbox, normalizeUrl, summarizeEquipmentPlannerData } from "../website/src/features/toolboxes/storage/toolboxStorage.js";
|
import { compactModuleDataForStorage, createToolboxExportPayload, moduleStorageKey, normalizeCalculatorData, normalizeChecklistData, normalizeCombosData, normalizeCountersData, normalizeEquipmentPlannerData, normalizeGoalTreeData, normalizeImageAnnotationData, normalizeLinksData, normalizeMemosData, normalizeModuleOrder, normalizeNotepadData, normalizeRandomizerData, normalizeRngSimulatorData, normalizeTableData, normalizeTaskPlannerData, normalizeTimerData, normalizeToolbox, normalizeUrl, remapMemosPlannerLink, summarizeEquipmentPlannerData } from "../website/src/features/toolboxes/storage/toolboxStorage.js";
|
||||||
import { createToolboxSpace, deleteToolboxSpace, moveToolboxModule, reorderToolboxSpaces } from "../website/src/features/toolboxes/toolboxSpaceUtils.js";
|
import { createToolboxSpace, deleteToolboxSpace, moveToolboxModule, reorderToolboxSpaces } from "../website/src/features/toolboxes/toolboxSpaceUtils.js";
|
||||||
import { applyGroupedReorderOperation, completeGroupOrder, getBoundaryItemId, getGroupedEntries, moveGroupOrder, moveGroupOrderToEnd, moveGroupOrderToStart, moveItem, moveItemGroup } from "../website/src/hooks/useGroupedReorder.js";
|
import { applyGroupedReorderOperation, completeGroupOrder, getBoundaryItemId, getGroupedEntries, moveGroupOrder, moveGroupOrderToEnd, moveGroupOrderToStart, moveItem, moveItemGroup } from "../website/src/hooks/useGroupedReorder.js";
|
||||||
|
|
||||||
|
test("memos persist and export a remapped task planner link", () => {
|
||||||
|
const normalized = normalizeMemosData({ plannerLink: "planner", items: [] });
|
||||||
|
assert.equal(normalized.plannerLink, "planner");
|
||||||
|
assert.equal(compactModuleDataForStorage("memos", normalized).plannerLink, "planner");
|
||||||
|
assert.equal(remapMemosPlannerLink({ plannerLink: "planner" }, (id) => `new-${id}`).plannerLink, "new-planner");
|
||||||
|
assert.equal(remapMemosPlannerLink({ plannerLink: "all" }, () => "unused").plannerLink, "all");
|
||||||
|
|
||||||
|
const toolbox = normalizeToolbox({
|
||||||
|
id: "toolbox",
|
||||||
|
name: "Linked planners",
|
||||||
|
modules: [
|
||||||
|
{ id: "memos", type: "memos" },
|
||||||
|
{ id: "planner", type: "taskPlanner" }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
const payload = createToolboxExportPayload(toolbox, {
|
||||||
|
[moduleStorageKey(toolbox.id, "memos")]: { plannerLink: "planner" },
|
||||||
|
[moduleStorageKey(toolbox.id, "planner")]: { tasks: [] }
|
||||||
|
});
|
||||||
|
const exportedMemos = payload.toolbox.modules.find((module) => module.type === "memos");
|
||||||
|
const exportedPlanner = payload.toolbox.modules.find((module) => module.type === "taskPlanner");
|
||||||
|
assert.equal(payload.modules[exportedMemos.id].plannerLink, exportedPlanner.id);
|
||||||
|
});
|
||||||
|
|
||||||
function createTextImportContext() {
|
function createTextImportContext() {
|
||||||
let index = 0;
|
let index = 0;
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -96,6 +96,15 @@
|
||||||
"kind": "iconButton",
|
"kind": "iconButton",
|
||||||
"icon": "eye-open"
|
"icon": "eye-open"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group": "Bande",
|
||||||
|
"icon": "settings",
|
||||||
|
"title": "Lier un ou tous les planificateurs de tâches",
|
||||||
|
"preview": {
|
||||||
|
"kind": "iconButton",
|
||||||
|
"icon": "settings"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"features": [
|
"features": [
|
||||||
|
|
@ -112,6 +121,7 @@
|
||||||
"text": "Tous les mémos apparaissent dans une bande compacte quand la bande est active.",
|
"text": "Tous les mémos apparaissent dans une bande compacte quand la bande est active.",
|
||||||
"details": [
|
"details": [
|
||||||
"La bande peut être masquée sans retirer les mémos.",
|
"La bande peut être masquée sans retirer les mémos.",
|
||||||
|
"Les paramètres permettent d'ajouter les tâches non effectuées qui approchent de leur reset depuis un planificateur précis ou tous ceux de la toolbox.",
|
||||||
"Les couleurs restent des accents localisés pour conserver une lecture calme."
|
"Les couleurs restent des accents localisés pour conserver une lecture calme."
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -1832,6 +1842,16 @@
|
||||||
"emptyText": "Cet espace est vide. Ajoutez-y un outil pour commencer.",
|
"emptyText": "Cet espace est vide. Ajoutez-y un outil pour commencer.",
|
||||||
"limitReached": "Limite de 5 espaces atteinte"
|
"limitReached": "Limite de 5 espaces atteinte"
|
||||||
},
|
},
|
||||||
|
"moduleConfiguration": {
|
||||||
|
"buttonLabel": "Configuration",
|
||||||
|
"panelLabel": "Configuration de",
|
||||||
|
"organizationTitle": "Espace",
|
||||||
|
"spaceLabel": "Déplacer vers un autre espace",
|
||||||
|
"guideTitle": "Guide",
|
||||||
|
"dataTitle": "Données",
|
||||||
|
"importButtonLabel": "Importer",
|
||||||
|
"exportButtonLabel": "Exporter"
|
||||||
|
},
|
||||||
"eyebrow": "Données locales",
|
"eyebrow": "Données locales",
|
||||||
"title": "Toolboxes",
|
"title": "Toolboxes",
|
||||||
"newButton": "Nouvelle toolbox",
|
"newButton": "Nouvelle toolbox",
|
||||||
|
|
@ -2004,6 +2024,12 @@
|
||||||
"tickerLabel": "Mémos",
|
"tickerLabel": "Mémos",
|
||||||
"hideTickerTitle": "Masquer la bande",
|
"hideTickerTitle": "Masquer la bande",
|
||||||
"showTickerTitle": "Afficher la bande",
|
"showTickerTitle": "Afficher la bande",
|
||||||
|
"settingsTitle": "Paramètres des mémos",
|
||||||
|
"plannerLinkLabel": "Planificateur lié",
|
||||||
|
"plannerNoneLabel": "Aucun",
|
||||||
|
"plannerAllLabel": "Tous les planificateurs",
|
||||||
|
"plannerDefaultLabel": "Planificateur de tâches",
|
||||||
|
"taskAlertTemplate": "Tâche : {category}{title} expire bientôt",
|
||||||
"importPlaceholder": "- Pour la reproduction, combine Pierre Stase + Nœud Destin pour transmettre nature et IV. | color=#f6c453\n- Les IV obtenus par Hyper Training ne sont pas transmis aux oeufs. | color=#8b5cf6\n- Pour une équipe Distorsion, vise 0 IV en Vitesse sur les Pokémon lents. | color=#22d3ee\n- Utilise les objets Pouvoir pour cibler précisément les EV à entraîner. | color=#d946ef",
|
"importPlaceholder": "- Pour la reproduction, combine Pierre Stase + Nœud Destin pour transmettre nature et IV. | color=#f6c453\n- Les IV obtenus par Hyper Training ne sont pas transmis aux oeufs. | color=#8b5cf6\n- Pour une équipe Distorsion, vise 0 IV en Vitesse sur les Pokémon lents. | color=#22d3ee\n- Utilise les objets Pouvoir pour cibler précisément les EV à entraîner. | color=#d946ef",
|
||||||
"importOpenButton": "Importer du texte",
|
"importOpenButton": "Importer du texte",
|
||||||
"exportOpenButton": "Exporter en format texte",
|
"exportOpenButton": "Exporter en format texte",
|
||||||
|
|
@ -2639,7 +2665,6 @@
|
||||||
"exportSuccess": "Export de tâches copié.",
|
"exportSuccess": "Export de tâches copié.",
|
||||||
"importError": "Format de tâches invalide.",
|
"importError": "Format de tâches invalide.",
|
||||||
"settingsTitle": "Réglages du planificateur",
|
"settingsTitle": "Réglages du planificateur",
|
||||||
"settingsButtonLabel": "Paramètres des réinitialisations",
|
|
||||||
"hideCompletedTitle": "Masquer les tâches effectuées",
|
"hideCompletedTitle": "Masquer les tâches effectuées",
|
||||||
"globalWeeklyResetDayLabel": "Reset hebdo",
|
"globalWeeklyResetDayLabel": "Reset hebdo",
|
||||||
"resetTimeLabel": "Heure de reset",
|
"resetTimeLabel": "Heure de reset",
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,14 @@
|
||||||
// Rôle : expose le suivi du guide dans chaque checklist compatible et marque les cartes terminées.
|
// Rôle : expose le suivi du guide dans chaque checklist compatible et marque les cartes terminées.
|
||||||
import { Icon } from "../../components/Icon.jsx";
|
import { Icon } from "../../components/Icon.jsx";
|
||||||
|
|
||||||
export function GuideChecklistToggle({ checklist, panel, label }) {
|
|
||||||
if (!panel.list) return null;
|
|
||||||
return <button
|
|
||||||
className={`module-guide-button nds-icon-button ${panel.open ? "active" : ""}`}
|
|
||||||
data-size="md"
|
|
||||||
data-linked={checklist.selected?.id === panel.list.id || undefined}
|
|
||||||
type="button"
|
|
||||||
aria-label={`${checklist.content.label} — ${label}`}
|
|
||||||
title={checklist.content.label}
|
|
||||||
aria-expanded={panel.open}
|
|
||||||
aria-pressed={panel.open}
|
|
||||||
aria-controls={panel.id}
|
|
||||||
onClick={panel.toggle}
|
|
||||||
><Icon name="link" /></button>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function GuideChecklistControls({ checklist, list, id, open }) {
|
export function GuideChecklistControls({ checklist, list, id, open }) {
|
||||||
if (!list) return null;
|
if (!list) return null;
|
||||||
const { content, selected, hideCompleted } = checklist;
|
const { content, selected, hideCompleted } = checklist;
|
||||||
const active = selected?.id === list.id;
|
const active = selected?.id === list.id;
|
||||||
return <div id={id} hidden={!open} className="guide-checklist-controls" data-linked={active || undefined}>
|
return <div id={id} hidden={!open} className="guide-checklist-controls" data-linked={active || undefined}>
|
||||||
<div className="guide-checklist-actions">
|
<div className="guide-checklist-actions">
|
||||||
<button type="button" className="guide-checklist-link nds-button" data-size="sm" data-variant={active ? "primary" : "secondary"} aria-pressed={active} onClick={() => checklist.associate(active ? "" : list.id)}>{active ? content.unlink : content.associate}</button>
|
<button type="button" className="guide-checklist-link nds-button" data-size="xs" data-variant={active ? "primary" : "secondary"} aria-pressed={active} onClick={() => checklist.associate(active ? "" : list.id)}>{active ? content.unlink : content.associate}</button>
|
||||||
<button type="button" className="nds-switch" disabled={!active} aria-pressed={active && hideCompleted} onClick={checklist.toggleHidden}><Icon name="hide" /><span>{content.hideCompleted}</span><i aria-hidden="true" /></button>
|
<button type="button" className="nds-switch" data-size="xs" disabled={!active} aria-pressed={active && hideCompleted} onClick={checklist.toggleHidden}><Icon name="hide" /><span>{content.hideCompleted}</span><i aria-hidden="true" /></button>
|
||||||
</div>
|
</div>
|
||||||
<p>{list.count ? `${list.count} ${content.matches}` : content.noMatches}</p>
|
<p>{list.count ? `${list.count} ${content.matches}` : content.noMatches}</p>
|
||||||
</div>;
|
</div>;
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,13 @@
|
||||||
// Rôle : partage la progression réactive et les commandes de liaison entre les guides.
|
// Rôle : partage la progression réactive et les commandes de liaison entre les guides.
|
||||||
import { createContext, useContext, useId, useMemo, useState } from "react";
|
import { createContext, useContext, useId, useMemo } from "react";
|
||||||
import { getGuideChecklistEntries, matchGuideChecklist, toggleGuideChecklistRows } from "./guideChecklist.js";
|
import { getGuideChecklistEntries, matchGuideChecklist, toggleGuideChecklistRows } from "./guideChecklist.js";
|
||||||
|
|
||||||
export const GuideChecklistContext = createContext(null);
|
export const GuideChecklistContext = createContext(null);
|
||||||
|
|
||||||
export function useGuideChecklistPanel(checklist, module) {
|
export function useGuideChecklistPanel(checklist, module) {
|
||||||
const id = useId();
|
const id = useId();
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
const list = module.type === "checklist" ? checklist?.lists.find((item) => item.id === module.id) : undefined;
|
const list = module.type === "checklist" ? checklist?.lists.find((item) => item.id === module.id) : undefined;
|
||||||
return { id, open, list, toggle: () => setOpen((value) => !value) };
|
return { id, list };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useGuideChecklist(requestedGuideId) {
|
export function useGuideChecklist(requestedGuideId) {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
// Rôle : affiche en bas de page les mémos actifs de la toolbox courante ou liée au jeu.
|
// Rôle : affiche en bas de page les mémos et alertes de tâches de la toolbox courante ou liée au jeu.
|
||||||
import { useMemo } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { moduleStorageKey, normalizeMemosData } from "./storage/toolboxStorage.js";
|
import { moduleStorageKey, normalizeMemosData } from "./storage/toolboxStorage.js";
|
||||||
import { MemosTicker } from "./modules/MemosTicker.jsx";
|
import { MemosTicker } from "./modules/MemosTicker.jsx";
|
||||||
|
import { getPlannerTaskTickerItems, resolveMemosPlannerLink } from "./modules/memosPlannerUtils.js";
|
||||||
|
|
||||||
function getRouteToolboxId(route, links) {
|
function getRouteToolboxId(route, links) {
|
||||||
const toolboxMatch = route.match(/^\/toolbox\/([^/]+)/);
|
const toolboxMatch = route.match(/^\/toolbox\/([^/]+)/);
|
||||||
|
|
@ -11,6 +12,13 @@ function getRouteToolboxId(route, links) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GlobalMemosTicker({ route, toolboxes, links, moduleData, textContent }) {
|
export function GlobalMemosTicker({ route, toolboxes, links, moduleData, textContent }) {
|
||||||
|
const [nowMs, setNowMs] = useState(() => Date.now());
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const intervalId = window.setInterval(() => setNowMs(Date.now()), 60000);
|
||||||
|
return () => window.clearInterval(intervalId);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const items = useMemo(() => {
|
const items = useMemo(() => {
|
||||||
const toolboxId = getRouteToolboxId(route, links);
|
const toolboxId = getRouteToolboxId(route, links);
|
||||||
const toolbox = toolboxes.find((item) => item.id === toolboxId);
|
const toolbox = toolboxes.find((item) => item.id === toolboxId);
|
||||||
|
|
@ -20,9 +28,20 @@ export function GlobalMemosTicker({ route, toolboxes, links, moduleData, textCon
|
||||||
.flatMap((module) => {
|
.flatMap((module) => {
|
||||||
const data = normalizeMemosData(moduleData[moduleStorageKey(toolbox.id, module.id)]);
|
const data = normalizeMemosData(moduleData[moduleStorageKey(toolbox.id, module.id)]);
|
||||||
if (!data.tickerEnabled) return [];
|
if (!data.tickerEnabled) return [];
|
||||||
return data.items.map((memo) => ({ ...memo, id: `${module.id}:${memo.id}` }));
|
const plannerLink = resolveMemosPlannerLink(data.plannerLink, toolbox.modules);
|
||||||
|
const taskItems = getPlannerTaskTickerItems({
|
||||||
|
plannerLink,
|
||||||
|
modules: toolbox.modules,
|
||||||
|
getPlannerData: (plannerId) => moduleData[moduleStorageKey(toolbox.id, plannerId)],
|
||||||
|
nowMs,
|
||||||
|
alertTemplate: textContent?.taskAlertTemplate
|
||||||
});
|
});
|
||||||
}, [route, toolboxes, links, moduleData]);
|
return [
|
||||||
|
...data.items.map((memo) => ({ ...memo, id: `${module.id}:${memo.id}` })),
|
||||||
|
...taskItems.map((item) => ({ ...item, id: `${module.id}:${item.id}` }))
|
||||||
|
];
|
||||||
|
});
|
||||||
|
}, [route, toolboxes, links, moduleData, nowMs, textContent?.taskAlertTemplate]);
|
||||||
|
|
||||||
if (!items.length) return null;
|
if (!items.length) return null;
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import { useInlineEdit } from "../../hooks/useInlineEdit.js";
|
||||||
import { compressImage } from "../../utils/imageCompression.js";
|
import { compressImage } from "../../utils/imageCompression.js";
|
||||||
import { getGameCardCover, ToolboxCard, ToolboxGameIcon, ToolboxIconPicker } from "./ToolboxCard.jsx";
|
import { getGameCardCover, ToolboxCard, ToolboxGameIcon, ToolboxIconPicker } from "./ToolboxCard.jsx";
|
||||||
import { AddToolControls, ToolboxModules } from "./modules/index.jsx";
|
import { AddToolControls, ToolboxModules } from "./modules/index.jsx";
|
||||||
|
import { getMemosDataAfterPlannerRemoval } from "./modules/memosPlannerUtils.js";
|
||||||
import { ToolboxSpaces } from "./ToolboxSpaces.jsx";
|
import { ToolboxSpaces } from "./ToolboxSpaces.jsx";
|
||||||
import {
|
import {
|
||||||
addModuleToToolboxSpace,
|
addModuleToToolboxSpace,
|
||||||
|
|
@ -195,7 +196,9 @@ function ToolboxView({ siteContent, toolbox, toolboxGame, embedded, actions, sto
|
||||||
isModuleDataReady: actions.isModuleDataReady,
|
isModuleDataReady: actions.isModuleDataReady,
|
||||||
storageStatus: siteContent.toolboxes.storageStatus,
|
storageStatus: siteContent.toolboxes.storageStatus,
|
||||||
storageReady,
|
storageReady,
|
||||||
|
toolboxModules: toolbox.modules,
|
||||||
moduleText,
|
moduleText,
|
||||||
|
moduleConfigurationText: siteContent.toolboxes.moduleConfiguration,
|
||||||
normalizeChecklistData,
|
normalizeChecklistData,
|
||||||
normalizeCombosData,
|
normalizeCombosData,
|
||||||
normalizeLinksData,
|
normalizeLinksData,
|
||||||
|
|
@ -403,6 +406,7 @@ function ToolboxView({ siteContent, toolbox, toolboxGame, embedded, actions, sto
|
||||||
context={moduleContext}
|
context={moduleContext}
|
||||||
reorder={moduleReorder}
|
reorder={moduleReorder}
|
||||||
spacesText={spacesText}
|
spacesText={spacesText}
|
||||||
|
configurationText={siteContent.toolboxes.moduleConfiguration}
|
||||||
scrollToModuleId={pendingModuleId}
|
scrollToModuleId={pendingModuleId}
|
||||||
onModuleScrolled={() => setPendingModuleId("")}
|
onModuleScrolled={() => setPendingModuleId("")}
|
||||||
onRename={(moduleId, title) => updateToolbox({ ...toolbox, modules: toolbox.modules.map((module) => module.id === moduleId ? { ...module, title } : module) })}
|
onRename={(moduleId, title) => updateToolbox({ ...toolbox, modules: toolbox.modules.map((module) => module.id === moduleId ? { ...module, title } : module) })}
|
||||||
|
|
@ -419,6 +423,15 @@ function ToolboxView({ siteContent, toolbox, toolboxGame, embedded, actions, sto
|
||||||
danger: true,
|
danger: true,
|
||||||
onResolve: (confirmed) => {
|
onResolve: (confirmed) => {
|
||||||
if (!confirmed) return;
|
if (!confirmed) return;
|
||||||
|
const removedModule = toolbox.modules.find((module) => module.id === moduleId);
|
||||||
|
const remainingModules = toolbox.modules.filter((module) => module.id !== moduleId);
|
||||||
|
if (removedModule?.type === "taskPlanner") {
|
||||||
|
toolbox.modules.filter((module) => module.type === "memos").forEach((memosModule) => {
|
||||||
|
const memosData = normalizeMemosData(getModuleData(toolbox.id, memosModule.id, { items: [] }));
|
||||||
|
const nextMemosData = getMemosDataAfterPlannerRemoval(memosData, moduleId, remainingModules);
|
||||||
|
if (nextMemosData !== memosData) updateModuleData(toolbox.id, memosModule.id, nextMemosData, "memos");
|
||||||
|
});
|
||||||
|
}
|
||||||
actions.removeModuleData(toolbox.id, moduleId);
|
actions.removeModuleData(toolbox.id, moduleId);
|
||||||
updateToolbox(removeModuleFromToolboxSpaces(toolbox, moduleId));
|
updateToolbox(removeModuleFromToolboxSpaces(toolbox, moduleId));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { Icon } from "../../../components/Icon.jsx";
|
||||||
import { MixTagsInput } from "../../../components/MixTagsInput.jsx";
|
import { MixTagsInput } from "../../../components/MixTagsInput.jsx";
|
||||||
import { moveItem, useGroupedReorder } from "../../../hooks/useGroupedReorder.js";
|
import { moveItem, useGroupedReorder } from "../../../hooks/useGroupedReorder.js";
|
||||||
import { CalculatorEntries, FormattedCalculatorNumber } from "./CalculatorEntries.jsx";
|
import { CalculatorEntries, FormattedCalculatorNumber } from "./CalculatorEntries.jsx";
|
||||||
import { TextExchangeActions } from "./TextExchangeActions.jsx";
|
import { ModuleTextExchangeConfiguration } from "./ModuleConfiguration.jsx";
|
||||||
import {
|
import {
|
||||||
BASE_TOKEN,
|
BASE_TOKEN,
|
||||||
calculateRemainingValue,
|
calculateRemainingValue,
|
||||||
|
|
@ -36,7 +36,7 @@ function focusExpressionInputAtEnd(element) {
|
||||||
selection?.addRange(range);
|
selection?.addRange(range);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpen }) {
|
export function CalculatorModule({ toolboxId, moduleId, context, configurationTarget }) {
|
||||||
const data = context.normalizeCalculatorData(context.getModuleData(toolboxId, moduleId, { entries: [] }));
|
const data = context.normalizeCalculatorData(context.getModuleData(toolboxId, moduleId, { entries: [] }));
|
||||||
const calculatedEntries = useMemo(() => evaluateCalculatorEntries(data.entries), [data.entries]);
|
const calculatedEntries = useMemo(() => evaluateCalculatorEntries(data.entries), [data.entries]);
|
||||||
const calculatedData = useMemo(() => ({ ...data, entries: calculatedEntries }), [data, calculatedEntries]);
|
const calculatedData = useMemo(() => ({ ...data, entries: calculatedEntries }), [data, calculatedEntries]);
|
||||||
|
|
@ -223,11 +223,7 @@ export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpe
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="calculator-module">
|
<div className="calculator-module">
|
||||||
{textExchangeOpen && (
|
<ModuleTextExchangeConfiguration target={configurationTarget} title={context.moduleConfigurationText?.dataTitle} type="calculator" data={calculatedData} context={context} textContent={textContent} onImport={(nextData) => context.setModuleData(toolboxId, moduleId, nextData, "calculator")} />
|
||||||
<div className="module-text-exchange-panel">
|
|
||||||
<TextExchangeActions type="calculator" data={calculatedData} context={context} textContent={textContent} onImport={(nextData) => context.setModuleData(toolboxId, moduleId, nextData, "calculator")} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<form className="calculator-form" onSubmit={saveResult}>
|
<form className="calculator-form" onSubmit={saveResult}>
|
||||||
<div className="calculator-card" ref={calculatorCardRef}>
|
<div className="calculator-card" ref={calculatorCardRef}>
|
||||||
<div className="calculator-field">
|
<div className="calculator-field">
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
import { Icon } from "../../../components/Icon.jsx";
|
import { Icon } from "../../../components/Icon.jsx";
|
||||||
import { CalendarEventForm } from "./CalendarEventForm.jsx";
|
import { CalendarEventForm } from "./CalendarEventForm.jsx";
|
||||||
import { CalendarMonthView, CalendarWeekGrid } from "./CalendarViews.jsx";
|
import { CalendarMonthView, CalendarWeekGrid } from "./CalendarViews.jsx";
|
||||||
import { TextExchangeActions } from "./TextExchangeActions.jsx";
|
import { ModuleTextExchangeConfiguration } from "./ModuleConfiguration.jsx";
|
||||||
import {
|
import {
|
||||||
addMonths,
|
addMonths,
|
||||||
formatLocalDateKey,
|
formatLocalDateKey,
|
||||||
|
|
@ -31,7 +31,7 @@ export function CalendarContent({
|
||||||
nowDayIndex,
|
nowDayIndex,
|
||||||
nowMinutes,
|
nowMinutes,
|
||||||
hideEmptyHours,
|
hideEmptyHours,
|
||||||
textExchangeOpen,
|
configurationTarget,
|
||||||
onKeyDown,
|
onKeyDown,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
onDraftChange,
|
onDraftChange,
|
||||||
|
|
@ -74,11 +74,7 @@ export function CalendarContent({
|
||||||
onDisplayedMonthChange={onDisplayedMonthChange}
|
onDisplayedMonthChange={onDisplayedMonthChange}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{textExchangeOpen && (
|
<ModuleTextExchangeConfiguration target={configurationTarget} title={context.moduleConfigurationText?.dataTitle} type="calendar" data={data} context={context} textContent={textContent} onImport={onImport} />
|
||||||
<div className="module-text-exchange-panel">
|
|
||||||
<TextExchangeActions type="calendar" data={data} context={context} textContent={textContent} onImport={onImport} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{data.viewMode === "week" && (
|
{data.viewMode === "week" && (
|
||||||
<CalendarWeekOptions textContent={textContent} hideEmptyHours={hideEmptyHours} onToggle={onHideEmptyHoursToggle} />
|
<CalendarWeekOptions textContent={textContent} hideEmptyHours={hideEmptyHours} onToggle={onHideEmptyHoursToggle} />
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { CalendarContent } from "./CalendarContent.jsx";
|
||||||
import { CalendarModeChoice } from "./CalendarModeChoice.jsx";
|
import { CalendarModeChoice } from "./CalendarModeChoice.jsx";
|
||||||
import { useCalendarController } from "./useCalendarController.js";
|
import { useCalendarController } from "./useCalendarController.js";
|
||||||
|
|
||||||
export function CalendarModule({ toolboxId, moduleId, context, editing = false, setEditing, textExchangeOpen }) {
|
export function CalendarModule({ toolboxId, moduleId, context, editing = false, setEditing, configurationTarget }) {
|
||||||
const data = context.normalizeCalendarData(context.getModuleData(toolboxId, moduleId, { initialized: false, viewMode: "week", events: [] }));
|
const data = context.normalizeCalendarData(context.getModuleData(toolboxId, moduleId, { initialized: false, viewMode: "week", events: [] }));
|
||||||
const textContent = context.moduleText?.calendar || {};
|
const textContent = context.moduleText?.calendar || {};
|
||||||
const controller = useCalendarController({ data, context, toolboxId, moduleId, editing, setEditing, textContent });
|
const controller = useCalendarController({ data, context, toolboxId, moduleId, editing, setEditing, textContent });
|
||||||
|
|
@ -18,5 +18,5 @@ export function CalendarModule({ toolboxId, moduleId, context, editing = false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <CalendarContent {...controller.contentProps} textExchangeOpen={textExchangeOpen} />;
|
return <CalendarContent {...controller.contentProps} configurationTarget={configurationTarget} />;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { Icon } from "../../../components/Icon.jsx";
|
||||||
import { useGroupedReorder, moveItem } from "../../../hooks/useGroupedReorder.js";
|
import { useGroupedReorder, moveItem } from "../../../hooks/useGroupedReorder.js";
|
||||||
import { ChecklistSection } from "./ChecklistParts.jsx";
|
import { ChecklistSection } from "./ChecklistParts.jsx";
|
||||||
import { ModuleEmptyMessage } from "./ModuleEmptyMessage.jsx";
|
import { ModuleEmptyMessage } from "./ModuleEmptyMessage.jsx";
|
||||||
import { TextExchangeActions } from "./TextExchangeActions.jsx";
|
import { ModuleTextExchangeConfiguration } from "./ModuleConfiguration.jsx";
|
||||||
import {
|
import {
|
||||||
appendChecklistItems,
|
appendChecklistItems,
|
||||||
getChecklistReorderItems,
|
getChecklistReorderItems,
|
||||||
|
|
@ -17,7 +17,7 @@ import {
|
||||||
updateChecklistItem
|
updateChecklistItem
|
||||||
} from "./checklistUtils.js";
|
} from "./checklistUtils.js";
|
||||||
|
|
||||||
export function ChecklistModule({ toolboxId, moduleId, context, editing, textExchangeOpen }) {
|
export function ChecklistModule({ toolboxId, moduleId, context, editing, configurationTarget }) {
|
||||||
const data = context.normalizeChecklistData(context.getModuleData(toolboxId, moduleId, { items: [] }));
|
const data = context.normalizeChecklistData(context.getModuleData(toolboxId, moduleId, { items: [] }));
|
||||||
const textContent = context.moduleText?.checklist || {};
|
const textContent = context.moduleText?.checklist || {};
|
||||||
const [label, setLabel] = useState("");
|
const [label, setLabel] = useState("");
|
||||||
|
|
@ -123,11 +123,7 @@ export function ChecklistModule({ toolboxId, moduleId, context, editing, textExc
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{textExchangeOpen && (
|
<ModuleTextExchangeConfiguration target={configurationTarget} title={context.moduleConfigurationText?.dataTitle} type="checklist" data={data} context={context} textContent={textContent} onImport={(nextData) => context.setModuleData(toolboxId, moduleId, nextData, "checklist")} />
|
||||||
<div className="module-text-exchange-panel">
|
|
||||||
<TextExchangeActions type="checklist" data={data} context={context} textContent={textContent} onImport={(nextData) => context.setModuleData(toolboxId, moduleId, nextData, "checklist")} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="checklist-toolbar">
|
<div className="checklist-toolbar">
|
||||||
<button
|
<button
|
||||||
className={`checklist-hide-completed-toggle nds-switch ${data.hideCompletedItems ? "active" : ""}`}
|
className={`checklist-hide-completed-toggle nds-switch ${data.hideCompletedItems ? "active" : ""}`}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
// Rôle : fournit l'outil Combos avec palettes d'inputs, rendu visuel et échange texte.
|
// Rôle : fournit l'outil Combos avec palettes d'inputs, rendu visuel et échange texte.
|
||||||
import { toggleListValue } from "../../../utils/collectionUtils.js";
|
import { toggleListValue } from "../../../utils/collectionUtils.js";
|
||||||
import { getGroupedEntries } from "../../../hooks/useGroupedReorder.js";
|
import { getGroupedEntries } from "../../../hooks/useGroupedReorder.js";
|
||||||
import { TextExchangeActions } from "./TextExchangeActions.jsx";
|
import { ModuleTextExchangeConfiguration } from "./ModuleConfiguration.jsx";
|
||||||
import { CombosEditor } from "./CombosEditor.jsx";
|
import { CombosEditor } from "./CombosEditor.jsx";
|
||||||
import { CombosList } from "./CombosList.jsx";
|
import { CombosList } from "./CombosList.jsx";
|
||||||
import {
|
import {
|
||||||
|
|
@ -10,7 +10,7 @@ import {
|
||||||
import { useCombosEditorState } from "./useCombosEditorState.js";
|
import { useCombosEditorState } from "./useCombosEditorState.js";
|
||||||
import { useCombosReorder } from "./useCombosReorder.js";
|
import { useCombosReorder } from "./useCombosReorder.js";
|
||||||
|
|
||||||
export function CombosModule({ toolboxId, moduleId, context, editing, textExchangeOpen }) {
|
export function CombosModule({ toolboxId, moduleId, context, editing, configurationTarget }) {
|
||||||
const data = context.normalizeCombosData(context.getModuleData(toolboxId, moduleId, { combos: [] }));
|
const data = context.normalizeCombosData(context.getModuleData(toolboxId, moduleId, { combos: [] }));
|
||||||
const textContent = context.moduleText?.combos || {};
|
const textContent = context.moduleText?.combos || {};
|
||||||
const categoryOptions = [
|
const categoryOptions = [
|
||||||
|
|
@ -86,11 +86,7 @@ export function CombosModule({ toolboxId, moduleId, context, editing, textExchan
|
||||||
onComboEditCancel={editorState.cancelComboEdit}
|
onComboEditCancel={editorState.cancelComboEdit}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{textExchangeOpen && (
|
<ModuleTextExchangeConfiguration target={configurationTarget} title={context.moduleConfigurationText?.dataTitle} type="combos" data={data} context={context} textContent={textContent} onImport={(nextData) => save(nextData)} />
|
||||||
<div className="module-text-exchange-panel">
|
|
||||||
<TextExchangeActions type="combos" data={data} context={context} textContent={textContent} onImport={(nextData) => save(nextData)} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!data.combos.length && (
|
{!data.combos.length && (
|
||||||
<p className="empty-state">{textContent.emptyCombos || "Aucun combo enregistré."}</p>
|
<p className="empty-state">{textContent.emptyCombos || "Aucun combo enregistré."}</p>
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,10 @@
|
||||||
import { Icon } from "../../../components/Icon.jsx";
|
import { Icon } from "../../../components/Icon.jsx";
|
||||||
import { useDraftForm } from "../../../hooks/useDraftForm.js";
|
import { useDraftForm } from "../../../hooks/useDraftForm.js";
|
||||||
import { moveItem, useGroupedReorder } from "../../../hooks/useGroupedReorder.js";
|
import { moveItem, useGroupedReorder } from "../../../hooks/useGroupedReorder.js";
|
||||||
import { TextExchangeActions } from "./TextExchangeActions.jsx";
|
import { ModuleTextExchangeConfiguration } from "./ModuleConfiguration.jsx";
|
||||||
import { ModuleEmptyMessage } from "./ModuleEmptyMessage.jsx";
|
import { ModuleEmptyMessage } from "./ModuleEmptyMessage.jsx";
|
||||||
|
|
||||||
export function CountersModule({ toolboxId, moduleId, context, editing, textExchangeOpen }) {
|
export function CountersModule({ toolboxId, moduleId, context, editing, configurationTarget }) {
|
||||||
const data = context.normalizeCountersData(context.getModuleData(toolboxId, moduleId, { counters: [] }));
|
const data = context.normalizeCountersData(context.getModuleData(toolboxId, moduleId, { counters: [] }));
|
||||||
const textContent = context.moduleText?.counters || {};
|
const textContent = context.moduleText?.counters || {};
|
||||||
const counterDraft = useDraftForm({ label: "" });
|
const counterDraft = useDraftForm({ label: "" });
|
||||||
|
|
@ -53,11 +53,7 @@ export function CountersModule({ toolboxId, moduleId, context, editing, textExch
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{textExchangeOpen && (
|
<ModuleTextExchangeConfiguration target={configurationTarget} title={context.moduleConfigurationText?.dataTitle} type="counters" data={data} context={context} textContent={textContent} onImport={(nextData) => context.setModuleData(toolboxId, moduleId, nextData, "counters")} />
|
||||||
<div className="module-text-exchange-panel">
|
|
||||||
<TextExchangeActions type="counters" data={data} context={context} textContent={textContent} onImport={(nextData) => context.setModuleData(toolboxId, moduleId, nextData, "counters")} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{!data.counters.length && <ModuleEmptyMessage>{textContent.emptyCounters || "Aucun compteur."}</ModuleEmptyMessage>}
|
{!data.counters.length && <ModuleEmptyMessage>{textContent.emptyCounters || "Aucun compteur."}</ModuleEmptyMessage>}
|
||||||
<div className="counters-grid">
|
<div className="counters-grid">
|
||||||
{data.counters.map((counter) => {
|
{data.counters.map((counter) => {
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
// Rôle : fournit l'outil de planification d'équipements, sertissages, résumé et échange texte.
|
// Rôle : fournit l'outil de planification d'équipements, sertissages, résumé et échange texte.
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { moveItem, useGroupedReorder } from "../../../hooks/useGroupedReorder.js";
|
import { moveItem, useGroupedReorder } from "../../../hooks/useGroupedReorder.js";
|
||||||
import { TextExchangeActions } from "./TextExchangeActions.jsx";
|
import { ModuleTextExchangeConfiguration } from "./ModuleConfiguration.jsx";
|
||||||
import { EQUIPMENT_ICONS } from "./equipmentPlannerOptions.js";
|
import { EQUIPMENT_ICONS } from "./equipmentPlannerOptions.js";
|
||||||
import { EquipmentItem } from "./EquipmentPlannerItem.jsx";
|
import { EquipmentItem } from "./EquipmentPlannerItem.jsx";
|
||||||
import { IconChoiceDropdown } from "./EquipmentPlannerPickers.jsx";
|
import { IconChoiceDropdown } from "./EquipmentPlannerPickers.jsx";
|
||||||
import { EquipmentPlannerSummary } from "./EquipmentPlannerSummary.jsx";
|
import { EquipmentPlannerSummary } from "./EquipmentPlannerSummary.jsx";
|
||||||
|
|
||||||
export function EquipmentPlannerModule({ toolboxId, moduleId, context, editing, textExchangeOpen }) {
|
export function EquipmentPlannerModule({ toolboxId, moduleId, context, editing, configurationTarget }) {
|
||||||
const data = context.normalizeEquipmentPlannerData(context.getModuleData(toolboxId, moduleId, { types: [], equipments: [] }));
|
const data = context.normalizeEquipmentPlannerData(context.getModuleData(toolboxId, moduleId, { types: [], equipments: [] }));
|
||||||
const summary = useMemo(() => context.summarizeEquipmentPlannerData(data), [context, data]);
|
const summary = useMemo(() => context.summarizeEquipmentPlannerData(data), [context, data]);
|
||||||
const textContent = context.moduleText?.equipmentPlanner || {};
|
const textContent = context.moduleText?.equipmentPlanner || {};
|
||||||
|
|
@ -114,11 +114,7 @@ export function EquipmentPlannerModule({ toolboxId, moduleId, context, editing,
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{textExchangeOpen && (
|
<ModuleTextExchangeConfiguration target={configurationTarget} title={context.moduleConfigurationText?.dataTitle} type="equipmentPlanner" data={data} context={context} textContent={textContent} onImport={(nextData) => save(nextData)} />
|
||||||
<div className="module-text-exchange-panel">
|
|
||||||
<TextExchangeActions type="equipmentPlanner" data={data} context={context} textContent={textContent} onImport={(nextData) => save(nextData)} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<EquipmentPlannerSummary summary={summary} textContent={textContent} />
|
<EquipmentPlannerSummary summary={summary} textContent={textContent} />
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
// Rôle : fournit l'outil arbre d'objectifs avec quantités, formules, resets et échange texte.
|
// Rôle : fournit l'outil arbre d'objectifs avec quantités, formules, resets et échange texte.
|
||||||
import { GoalTreeEditor } from "./GoalTreeEditor.jsx";
|
import { GoalTreeEditor } from "./GoalTreeEditor.jsx";
|
||||||
import { GoalTreeView } from "./GoalTreeView.jsx";
|
import { GoalTreeView } from "./GoalTreeView.jsx";
|
||||||
import { TextExchangeActions } from "./TextExchangeActions.jsx";
|
import { ModuleTextExchangeConfiguration } from "./ModuleConfiguration.jsx";
|
||||||
import { useGoalTreeController } from "./useGoalTreeController.js";
|
import { useGoalTreeController } from "./useGoalTreeController.js";
|
||||||
|
|
||||||
export function GoalTreeModule({ toolboxId, moduleId, context, editing, setEditing = () => {}, openEditor = () => {}, textExchangeOpen }) {
|
export function GoalTreeModule({ toolboxId, moduleId, context, editing, setEditing = () => {}, openEditor = () => {}, configurationTarget }) {
|
||||||
const controller = useGoalTreeController({ toolboxId, moduleId, context, editing, setEditing, openEditor });
|
const controller = useGoalTreeController({ toolboxId, moduleId, context, editing, setEditing, openEditor });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -30,11 +30,7 @@ export function GoalTreeModule({ toolboxId, moduleId, context, editing, setEditi
|
||||||
onToggleLinkedParent={controller.toggleLinkedParent}
|
onToggleLinkedParent={controller.toggleLinkedParent}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{textExchangeOpen && (
|
<ModuleTextExchangeConfiguration target={configurationTarget} title={context.moduleConfigurationText?.dataTitle} type="goalTree" data={controller.evaluatedData} context={context} textContent={controller.textContent} onImport={controller.importData} />
|
||||||
<div className="module-text-exchange-panel">
|
|
||||||
<TextExchangeActions type="goalTree" data={controller.evaluatedData} context={context} textContent={controller.textContent} onImport={controller.importData} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<GoalTreeView
|
<GoalTreeView
|
||||||
evaluatedNodes={controller.evaluatedNodes}
|
evaluatedNodes={controller.evaluatedNodes}
|
||||||
|
|
@ -46,7 +42,6 @@ export function GoalTreeModule({ toolboxId, moduleId, context, editing, setEditi
|
||||||
verticalView={controller.verticalView}
|
verticalView={controller.verticalView}
|
||||||
viewScale={controller.viewScale}
|
viewScale={controller.viewScale}
|
||||||
storageReady={controller.storageReady}
|
storageReady={controller.storageReady}
|
||||||
textExchangeOpen={textExchangeOpen}
|
|
||||||
onSelect={controller.setSelectedNodeId}
|
onSelect={controller.setSelectedNodeId}
|
||||||
onAdd={controller.addNode}
|
onAdd={controller.addNode}
|
||||||
onAddContentItem={controller.addNodeContentItem}
|
onAddContentItem={controller.addNodeContentItem}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@ export function GoalTreeView({
|
||||||
verticalView,
|
verticalView,
|
||||||
viewScale,
|
viewScale,
|
||||||
storageReady,
|
storageReady,
|
||||||
textExchangeOpen,
|
|
||||||
onSelect,
|
onSelect,
|
||||||
onAdd,
|
onAdd,
|
||||||
onAddContentItem,
|
onAddContentItem,
|
||||||
|
|
@ -57,7 +56,7 @@ export function GoalTreeView({
|
||||||
timeoutIds.forEach(clearTimeout);
|
timeoutIds.forEach(clearTimeout);
|
||||||
window.removeEventListener("load", refreshConnectors);
|
window.removeEventListener("load", refreshConnectors);
|
||||||
};
|
};
|
||||||
}, [storageReady, connectorTopologyKey, evaluatedNodes.length, editing, textExchangeOpen]);
|
}, [storageReady, connectorTopologyKey, evaluatedNodes.length, editing]);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
const canvas = treeViewRef.current;
|
const canvas = treeViewRef.current;
|
||||||
|
|
@ -82,7 +81,7 @@ export function GoalTreeView({
|
||||||
cancelAnimationFrame(frameId);
|
cancelAnimationFrame(frameId);
|
||||||
resizeObserver.disconnect();
|
resizeObserver.disconnect();
|
||||||
};
|
};
|
||||||
}, [connectorTopologyKey, editing, textExchangeOpen, viewScale]);
|
}, [connectorTopologyKey, editing, viewScale]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={`goal-tree-view tool-split-results ${verticalView ? "is-vertical" : "is-horizontal"} ${editing ? "is-editing" : ""}`} aria-label={textContent.treeTitle || "Arbre d'objectifs"}>
|
<section className={`goal-tree-view tool-split-results ${verticalView ? "is-vertical" : "is-horizontal"} ${editing ? "is-editing" : ""}`} aria-label={textContent.treeTitle || "Arbre d'objectifs"}>
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { Icon } from "../../../components/Icon.jsx";
|
import { Icon } from "../../../components/Icon.jsx";
|
||||||
import { Tooltip } from "../../../components/Tooltip.jsx";
|
import { Tooltip } from "../../../components/Tooltip.jsx";
|
||||||
import { DrawingOverlay } from "./DrawingOverlay.jsx";
|
import { DrawingOverlay } from "./DrawingOverlay.jsx";
|
||||||
import { TextExchangeActions } from "./TextExchangeActions.jsx";
|
import { ModuleTextExchangeConfiguration } from "./ModuleConfiguration.jsx";
|
||||||
import { useKeyedState } from "../../../hooks/useKeyedState.js";
|
import { useKeyedState } from "../../../hooks/useKeyedState.js";
|
||||||
|
|
||||||
function readTemporaryDrawings(sessionKey) {
|
function readTemporaryDrawings(sessionKey) {
|
||||||
|
|
@ -19,7 +19,7 @@ function markerLabel(index, marker, textContent) {
|
||||||
return marker.label || `${textContent.markerPrefix || "Marqueur"} ${index + 1}`;
|
return marker.label || `${textContent.markerPrefix || "Marqueur"} ${index + 1}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ImageAnnotationModule({ toolboxId, moduleId, context, editing, textExchangeOpen }) {
|
export function ImageAnnotationModule({ toolboxId, moduleId, context, editing, configurationTarget }) {
|
||||||
const data = context.normalizeImageAnnotationData(context.getModuleData(toolboxId, moduleId, { image: "", markers: [] }));
|
const data = context.normalizeImageAnnotationData(context.getModuleData(toolboxId, moduleId, { image: "", markers: [] }));
|
||||||
const textContent = context.moduleText?.imageAnnotation || {};
|
const textContent = context.moduleText?.imageAnnotation || {};
|
||||||
const notify = context.notify;
|
const notify = context.notify;
|
||||||
|
|
@ -123,14 +123,10 @@ export function ImageAnnotationModule({ toolboxId, moduleId, context, editing, t
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{textExchangeOpen && (
|
<ModuleTextExchangeConfiguration target={configurationTarget} title={context.moduleConfigurationText?.dataTitle} type="imageAnnotation" data={data} context={context} textContent={textContent} onImport={(nextData) => {
|
||||||
<div className="module-text-exchange-panel">
|
|
||||||
<TextExchangeActions type="imageAnnotation" data={data} context={context} textContent={textContent} onImport={(nextData) => {
|
|
||||||
save(nextData);
|
save(nextData);
|
||||||
setTemporaryDrawings({ strokes: [] });
|
setTemporaryDrawings({ strokes: [] });
|
||||||
}} />
|
}} />
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{data.image ? (
|
{data.image ? (
|
||||||
<div className="annotation-workspace">
|
<div className="annotation-workspace">
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,9 @@ import { useState } from "react";
|
||||||
import { Icon } from "../../../components/Icon.jsx";
|
import { Icon } from "../../../components/Icon.jsx";
|
||||||
import { moveItem, useGroupedReorder } from "../../../hooks/useGroupedReorder.js";
|
import { moveItem, useGroupedReorder } from "../../../hooks/useGroupedReorder.js";
|
||||||
import { ModuleEmptyMessage } from "./ModuleEmptyMessage.jsx";
|
import { ModuleEmptyMessage } from "./ModuleEmptyMessage.jsx";
|
||||||
import { TextExchangeActions } from "./TextExchangeActions.jsx";
|
import { ModuleTextExchangeConfiguration } from "./ModuleConfiguration.jsx";
|
||||||
|
|
||||||
export function ImagesModule({ toolboxId, moduleId, context, editing, textExchangeOpen }) {
|
export function ImagesModule({ toolboxId, moduleId, context, editing, configurationTarget }) {
|
||||||
const data = context.getModuleData(toolboxId, moduleId, { images: [] });
|
const data = context.getModuleData(toolboxId, moduleId, { images: [] });
|
||||||
const textContent = context.moduleText?.images || {};
|
const textContent = context.moduleText?.images || {};
|
||||||
const [dragOver, setDragOver] = useState(false);
|
const [dragOver, setDragOver] = useState(false);
|
||||||
|
|
@ -99,11 +99,7 @@ export function ImagesModule({ toolboxId, moduleId, context, editing, textExchan
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{textExchangeOpen && (
|
<ModuleTextExchangeConfiguration target={configurationTarget} title={context.moduleConfigurationText?.dataTitle} type="images" data={data} context={context} textContent={textContent} onImport={(nextData) => context.setModuleData(toolboxId, moduleId, nextData, "images")} />
|
||||||
<div className="module-text-exchange-panel">
|
|
||||||
<TextExchangeActions type="images" data={data} context={context} textContent={textContent} onImport={(nextData) => context.setModuleData(toolboxId, moduleId, nextData, "images")} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{data.images.length > 0 && (
|
{data.images.length > 0 && (
|
||||||
<div className="images-toolbar">
|
<div className="images-toolbar">
|
||||||
<button className="nds-button" data-size="sm" type="button" onClick={() => openCollection("gallery")}>
|
<button className="nds-button" data-size="sm" type="button" onClick={() => openCollection("gallery")}>
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { CategoryAutocompleteInput } from "../../../components/CategoryAutocompl
|
||||||
import { Icon } from "../../../components/Icon.jsx";
|
import { Icon } from "../../../components/Icon.jsx";
|
||||||
import { useDraftForm } from "../../../hooks/useDraftForm.js";
|
import { useDraftForm } from "../../../hooks/useDraftForm.js";
|
||||||
import { applyGroupedReorderOperation, useGroupedReorder } from "../../../hooks/useGroupedReorder.js";
|
import { applyGroupedReorderOperation, useGroupedReorder } from "../../../hooks/useGroupedReorder.js";
|
||||||
import { TextExchangeActions } from "./TextExchangeActions.jsx";
|
import { ModuleTextExchangeConfiguration } from "./ModuleConfiguration.jsx";
|
||||||
import { ModuleEmptyMessage } from "./ModuleEmptyMessage.jsx";
|
import { ModuleEmptyMessage } from "./ModuleEmptyMessage.jsx";
|
||||||
|
|
||||||
function getLinkCategory(link) {
|
function getLinkCategory(link) {
|
||||||
|
|
@ -32,7 +32,7 @@ function getLinkEntries(data) {
|
||||||
return [...ungrouped, ...categories];
|
return [...ungrouped, ...categories];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LinksModule({ toolboxId, moduleId, context, editing, textExchangeOpen }) {
|
export function LinksModule({ toolboxId, moduleId, context, editing, configurationTarget }) {
|
||||||
const data = context.normalizeLinksData(context.getModuleData(toolboxId, moduleId, { links: [] }));
|
const data = context.normalizeLinksData(context.getModuleData(toolboxId, moduleId, { links: [] }));
|
||||||
const textContent = context.moduleText?.links || {};
|
const textContent = context.moduleText?.links || {};
|
||||||
const linkDraft = useDraftForm({ title: "", url: "", category: "" });
|
const linkDraft = useDraftForm({ title: "", url: "", category: "" });
|
||||||
|
|
@ -169,11 +169,7 @@ export function LinksModule({ toolboxId, moduleId, context, editing, textExchang
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{textExchangeOpen && (
|
<ModuleTextExchangeConfiguration target={configurationTarget} title={context.moduleConfigurationText?.dataTitle} type="links" data={data} context={context} textContent={textContent} onImport={(nextData) => context.setModuleData(toolboxId, moduleId, nextData, "links")} />
|
||||||
<div className="module-text-exchange-panel">
|
|
||||||
<TextExchangeActions type="links" data={data} context={context} textContent={textContent} onImport={(nextData) => context.setModuleData(toolboxId, moduleId, nextData, "links")} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{!data.links.length && <ModuleEmptyMessage>{textContent.emptyLinks || "Aucun lien."}</ModuleEmptyMessage>}
|
{!data.links.length && <ModuleEmptyMessage>{textContent.emptyLinks || "Aucun lien."}</ModuleEmptyMessage>}
|
||||||
<div className="links-category-list">
|
<div className="links-category-list">
|
||||||
{getLinkEntries(data).map((entry) => {
|
{getLinkEntries(data).map((entry) => {
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,35 @@
|
||||||
// Rôle : fournit l'outil de mémos courts avec post-it, bande déroulante et tri manuel.
|
// Rôle : fournit les mémos courts, leur tri et les alertes de planificateur dans la bande déroulante.
|
||||||
import { CompactDropdown } from "../../../components/CompactDropdown.jsx";
|
import { CompactDropdown } from "../../../components/CompactDropdown.jsx";
|
||||||
import { Icon } from "../../../components/Icon.jsx";
|
import { Icon } from "../../../components/Icon.jsx";
|
||||||
import { moveItem, useGroupedReorder } from "../../../hooks/useGroupedReorder.js";
|
import { moveItem, useGroupedReorder } from "../../../hooks/useGroupedReorder.js";
|
||||||
import { useMemo } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import {
|
||||||
|
ALL_TASK_PLANNERS_LINK,
|
||||||
|
getPlannerTaskTickerItems,
|
||||||
|
getTaskPlannerModules,
|
||||||
|
resolveMemosPlannerLink
|
||||||
|
} from "./memosPlannerUtils.js";
|
||||||
import { MemoTextEditor } from "./MemoTextEditor.jsx";
|
import { MemoTextEditor } from "./MemoTextEditor.jsx";
|
||||||
import { MemosTicker } from "./MemosTicker.jsx";
|
import { MemosTicker } from "./MemosTicker.jsx";
|
||||||
|
import { ModuleConfigurationPortal } from "./ModuleConfiguration.jsx";
|
||||||
import { TextExchangeActions } from "./TextExchangeActions.jsx";
|
import { TextExchangeActions } from "./TextExchangeActions.jsx";
|
||||||
import { TEXT_COLORS } from "./notepadUtils.js";
|
import { TEXT_COLORS } from "./notepadUtils.js";
|
||||||
|
|
||||||
export function MemosModule({ toolboxId, moduleId, context, editing, textExchangeOpen }) {
|
export function MemosModule({ toolboxId, moduleId, context, editing, configurationTarget }) {
|
||||||
const storedData = context.getModuleData(toolboxId, moduleId, { items: [] });
|
const storedData = context.getModuleData(toolboxId, moduleId, { items: [] });
|
||||||
const data = useMemo(() => context.normalizeMemosData(storedData), [context, storedData]);
|
const data = useMemo(() => context.normalizeMemosData(storedData), [context, storedData]);
|
||||||
const textContent = context.moduleText?.memos || {};
|
const textContent = context.moduleText?.memos || {};
|
||||||
const tickerItems = data.tickerEnabled ? data.items : [];
|
const [nowMs, setNowMs] = useState(() => Date.now());
|
||||||
|
const plannerModules = useMemo(() => getTaskPlannerModules(context.toolboxModules), [context.toolboxModules]);
|
||||||
|
const plannerLink = resolveMemosPlannerLink(data.plannerLink, plannerModules);
|
||||||
|
const plannerTickerItems = useMemo(() => getPlannerTaskTickerItems({
|
||||||
|
plannerLink,
|
||||||
|
modules: plannerModules,
|
||||||
|
getPlannerData: (plannerId) => context.getModuleData(toolboxId, plannerId, { tasks: [] }),
|
||||||
|
nowMs,
|
||||||
|
alertTemplate: textContent.taskAlertTemplate
|
||||||
|
}), [context, nowMs, plannerLink, plannerModules, textContent.taskAlertTemplate, toolboxId]);
|
||||||
|
const tickerItems = data.tickerEnabled ? [...data.items, ...plannerTickerItems] : [];
|
||||||
const {
|
const {
|
||||||
itemReorder,
|
itemReorder,
|
||||||
getItemProps,
|
getItemProps,
|
||||||
|
|
@ -37,6 +54,15 @@ export function MemosModule({ toolboxId, moduleId, context, editing, textExchang
|
||||||
save({ ...data, items });
|
save({ ...data, items });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const intervalId = window.setInterval(() => setNowMs(Date.now()), 60000);
|
||||||
|
return () => window.clearInterval(intervalId);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data.plannerLink && !plannerLink) context.setModuleData(toolboxId, moduleId, { ...data, plannerLink: "" }, "memos");
|
||||||
|
}, [context, data, moduleId, plannerLink, toolboxId]);
|
||||||
|
|
||||||
function addMemo() {
|
function addMemo() {
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
save({
|
save({
|
||||||
|
|
@ -71,11 +97,33 @@ export function MemosModule({ toolboxId, moduleId, context, editing, textExchang
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="memos-module">
|
<div className="memos-module">
|
||||||
{textExchangeOpen && (
|
<ModuleConfigurationPortal target={configurationTarget}>
|
||||||
<div className="module-text-exchange-panel">
|
<section className="module-configuration-section module-configuration-specific">
|
||||||
<TextExchangeActions type="memos" data={data} context={context} textContent={textContent} onImport={(nextData) => save(nextData)} />
|
<h3>{textContent.settingsTitle || "Paramètres des mémos"}</h3>
|
||||||
|
<div className="memos-settings">
|
||||||
|
<label>
|
||||||
|
<span>{textContent.plannerLinkLabel || "Planificateur lié"}</span>
|
||||||
|
<select
|
||||||
|
className="nds-select"
|
||||||
|
data-size="xs"
|
||||||
|
value={plannerLink}
|
||||||
|
onChange={(event) => save({ ...data, plannerLink: event.target.value })}
|
||||||
|
aria-label={textContent.plannerLinkLabel || "Planificateur lié"}
|
||||||
|
>
|
||||||
|
<option value="">{textContent.plannerNoneLabel || "Aucun"}</option>
|
||||||
|
{plannerModules.length > 0 && <option value={ALL_TASK_PLANNERS_LINK}>{textContent.plannerAllLabel || "Tous les planificateurs"}</option>}
|
||||||
|
{plannerModules.map((planner) => (
|
||||||
|
<option key={planner.id} value={planner.id}>{planner.title || textContent.plannerDefaultLabel || "Planificateur de tâches"}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</section>
|
||||||
|
<section className="module-configuration-section module-configuration-data">
|
||||||
|
<h3>{context.moduleConfigurationText?.dataTitle || "Données"}</h3>
|
||||||
|
<TextExchangeActions type="memos" data={data} context={context} textContent={textContent} onImport={(nextData) => save({ ...data, items: nextData.items })} />
|
||||||
|
</section>
|
||||||
|
</ModuleConfigurationPortal>
|
||||||
|
|
||||||
{editing && (
|
{editing && (
|
||||||
<div className="module-add-panel">
|
<div className="module-add-panel">
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,14 @@
|
||||||
// Rôle : affiche la bande déroulante partagée par l'outil Mémos et les pages associées.
|
// Rôle : affiche la bande déroulante partagée pour les mémos et alertes de tâches.
|
||||||
|
import { Icon } from "../../../components/Icon.jsx";
|
||||||
|
|
||||||
export function MemosTicker({ items, label = "Mémos", className = "" }) {
|
export function MemosTicker({ items, label = "Mémos", className = "" }) {
|
||||||
if (!items?.length) return null;
|
if (!items?.length) return null;
|
||||||
return (
|
return (
|
||||||
<div className={["memos-ticker", className].filter(Boolean).join(" ")} aria-label={label}>
|
<div className={["memos-ticker", className].filter(Boolean).join(" ")} aria-label={label}>
|
||||||
<div className="memos-ticker-track">
|
<div className="memos-ticker-track">
|
||||||
{[...items, ...items].map((memo, index) => (
|
{[...items, ...items].map((memo, index) => (
|
||||||
<span className="memos-ticker-item" key={`${memo.id}-${index}`} style={{ "--memo-color": memo.color }}>
|
<span className={`memos-ticker-item ${memo.kind === "task-warning" ? "is-task-warning" : ""}`} key={`${memo.id}-${index}`} style={{ "--memo-color": memo.color }}>
|
||||||
|
{memo.kind === "task-warning" && <Icon name="warning" />}
|
||||||
{memo.text}
|
{memo.text}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
// Rôle : déplace les réglages propres aux outils dans le panneau Configuration commun.
|
||||||
|
import { createPortal } from "react-dom";
|
||||||
|
import { TextExchangeActions } from "./TextExchangeActions.jsx";
|
||||||
|
|
||||||
|
export function ModuleConfigurationPortal({ target, children }) {
|
||||||
|
return target ? createPortal(children, target) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ModuleTextExchangeConfiguration({ target, title = "Données", ...props }) {
|
||||||
|
return (
|
||||||
|
<ModuleConfigurationPortal target={target}>
|
||||||
|
<section className="module-configuration-section module-configuration-data">
|
||||||
|
<h3>{title}</h3>
|
||||||
|
<TextExchangeActions {...props} />
|
||||||
|
</section>
|
||||||
|
</ModuleConfigurationPortal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,23 +1,26 @@
|
||||||
// Rôle : rend l'en-tête, les contrôles et le contenu repliable d'un outil toolbox.
|
// Rôle : rend l'en-tête, les contrôles et le contenu repliable d'un outil toolbox.
|
||||||
import { Suspense, useEffect, useRef, useState } from "react";
|
import { Suspense, useEffect, useId, useRef, useState } from "react";
|
||||||
import { Icon } from "../../../components/Icon.jsx";
|
import { Icon } from "../../../components/Icon.jsx";
|
||||||
import { useInlineEdit } from "../../../hooks/useInlineEdit.js";
|
import { useInlineEdit } from "../../../hooks/useInlineEdit.js";
|
||||||
import { GuideChecklistControls, GuideChecklistToggle } from "../../games/GuideChecklistControls.jsx";
|
import { GuideChecklistControls } from "../../games/GuideChecklistControls.jsx";
|
||||||
import { useGuideChecklistPanel } from "../../games/useGuideChecklist.js";
|
import { useGuideChecklistPanel } from "../../games/useGuideChecklist.js";
|
||||||
import { ModuleDataGate, ModuleLoadingFallback } from "./ModuleDataGate.jsx";
|
import { ModuleDataGate, ModuleLoadingFallback } from "./ModuleDataGate.jsx";
|
||||||
import { supportsTextExchange } from "./textImport.js";
|
import { supportsTextExchange } from "./textImport.js";
|
||||||
import { MODULE_COMPONENTS } from "./toolboxModuleComponents.jsx";
|
import { MODULE_COMPONENTS } from "./toolboxModuleComponents.jsx";
|
||||||
|
|
||||||
export function ModuleShell({ toolbox, module, context, reorder, itemGroup = "", spaces = [], activeSpaceId = "", moveText = "Déplacer vers", scrollIntoView = false, onScrolledIntoView, onRename, onUpdateModule, onDelete, onMoveToSpace }) {
|
export function ModuleShell({ toolbox, module, context, reorder, itemGroup = "", spaces = [], activeSpaceId = "", moveText = "Déplacer vers", configurationText = {}, scrollIntoView = false, onScrolledIntoView, onRename, onUpdateModule, onDelete, onMoveToSpace }) {
|
||||||
const definition = MODULE_COMPONENTS[module.type] || MODULE_COMPONENTS.notepad;
|
const definition = MODULE_COMPONENTS[module.type] || MODULE_COMPONENTS.notepad;
|
||||||
const Component = definition.Component;
|
const Component = definition.Component;
|
||||||
const label = definition.label || module.type;
|
const label = definition.label || module.type;
|
||||||
const [editing, setEditing] = useState(false);
|
const [editing, setEditing] = useState(false);
|
||||||
const [textExchangeOpen, setTextExchangeOpen] = useState(false);
|
const [configurationOpen, setConfigurationOpen] = useState(false);
|
||||||
|
const [configurationTarget, setConfigurationTarget] = useState(null);
|
||||||
|
const configurationId = useId();
|
||||||
const moduleRef = useRef(null);
|
const moduleRef = useRef(null);
|
||||||
const guidePanel = useGuideChecklistPanel(context.guideChecklist, module);
|
const guidePanel = useGuideChecklistPanel(context.guideChecklist, module);
|
||||||
const hasAddPanel = definition.editable && definition.addable !== false;
|
const hasAddPanel = definition.editable && definition.addable !== false;
|
||||||
const hasTextExchange = supportsTextExchange(module.type);
|
const hasTextExchange = supportsTextExchange(module.type);
|
||||||
|
const hasConfiguration = spaces.length > 1 || Boolean(guidePanel.list) || hasTextExchange;
|
||||||
const scrollable = definition.scrollable && module.scrollable === true;
|
const scrollable = definition.scrollable && module.scrollable === true;
|
||||||
const collapsed = module.collapsed === true;
|
const collapsed = module.collapsed === true;
|
||||||
const className = [
|
const className = [
|
||||||
|
|
@ -35,10 +38,10 @@ export function ModuleShell({ toolbox, module, context, reorder, itemGroup = "",
|
||||||
editing,
|
editing,
|
||||||
setEditing,
|
setEditing,
|
||||||
openEditor: () => {
|
openEditor: () => {
|
||||||
setTextExchangeOpen(false);
|
setConfigurationOpen(false);
|
||||||
setEditing(true);
|
setEditing(true);
|
||||||
},
|
},
|
||||||
textExchangeOpen
|
configurationTarget
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -50,10 +53,19 @@ export function ModuleShell({ toolbox, module, context, reorder, itemGroup = "",
|
||||||
return () => window.cancelAnimationFrame(frame);
|
return () => window.cancelAnimationFrame(frame);
|
||||||
}, [onScrolledIntoView, scrollIntoView]);
|
}, [onScrolledIntoView, scrollIntoView]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!configurationOpen) return undefined;
|
||||||
|
const closeOnEscape = (event) => {
|
||||||
|
if (event.key === "Escape" && !document.querySelector('[aria-modal="true"]')) setConfigurationOpen(false);
|
||||||
|
};
|
||||||
|
window.addEventListener("keydown", closeOnEscape);
|
||||||
|
return () => window.removeEventListener("keydown", closeOnEscape);
|
||||||
|
}, [configurationOpen]);
|
||||||
|
|
||||||
function toggleCollapsed() {
|
function toggleCollapsed() {
|
||||||
if (!collapsed) {
|
if (!collapsed) {
|
||||||
setEditing(false);
|
setEditing(false);
|
||||||
setTextExchangeOpen(false);
|
setConfigurationOpen(false);
|
||||||
}
|
}
|
||||||
onUpdateModule(module.id, (currentModule) => {
|
onUpdateModule(module.id, (currentModule) => {
|
||||||
const nextModule = { ...currentModule };
|
const nextModule = { ...currentModule };
|
||||||
|
|
@ -74,8 +86,8 @@ export function ModuleShell({ toolbox, module, context, reorder, itemGroup = "",
|
||||||
data-module-id={module.id}
|
data-module-id={module.id}
|
||||||
{...reorder.getItemProps({ itemId: module.id, groupId: itemGroup, parentId: toolbox.id })}
|
{...reorder.getItemProps({ itemId: module.id, groupId: itemGroup, parentId: toolbox.id })}
|
||||||
>
|
>
|
||||||
<header>
|
<header className="module-header">
|
||||||
<div>
|
<div className="module-header-main">
|
||||||
<button
|
<button
|
||||||
className="module-drag-handle nds-icon-button"
|
className="module-drag-handle nds-icon-button"
|
||||||
data-size="md"
|
data-size="md"
|
||||||
|
|
@ -92,20 +104,7 @@ export function ModuleShell({ toolbox, module, context, reorder, itemGroup = "",
|
||||||
</span>
|
</span>
|
||||||
<EditableModuleTitle value={module.title || label} fallback={label} tutorialTarget={module.type === "memos" ? "moduleTitle" : ""} onSave={(title) => onRename(module.id, title)} />
|
<EditableModuleTitle value={module.title || label} fallback={label} tutorialTarget={module.type === "memos" ? "moduleTitle" : ""} onSave={(title) => onRename(module.id, title)} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="module-header-actions">
|
||||||
{spaces.length > 1 && (
|
|
||||||
<select
|
|
||||||
className="module-space-select nds-select"
|
|
||||||
data-size="sm"
|
|
||||||
value=""
|
|
||||||
aria-label={`${moveText} — ${module.title || label}`}
|
|
||||||
title={moveText}
|
|
||||||
onChange={(event) => onMoveToSpace(module.id, event.target.value)}
|
|
||||||
>
|
|
||||||
<option value="" disabled>{moveText}</option>
|
|
||||||
{spaces.filter((space) => space.id !== activeSpaceId).map((space) => <option key={space.id} value={space.id}>{space.name}</option>)}
|
|
||||||
</select>
|
|
||||||
)}
|
|
||||||
{definition.scrollable && (
|
{definition.scrollable && (
|
||||||
<button
|
<button
|
||||||
className={`module-scroll-button nds-switch ${scrollable ? "active" : ""}`}
|
className={`module-scroll-button nds-switch ${scrollable ? "active" : ""}`}
|
||||||
|
|
@ -133,7 +132,7 @@ export function ModuleShell({ toolbox, module, context, reorder, itemGroup = "",
|
||||||
data-size="md"
|
data-size="md"
|
||||||
onClick={() => setEditing((value) => {
|
onClick={() => setEditing((value) => {
|
||||||
const nextEditing = !value;
|
const nextEditing = !value;
|
||||||
if (nextEditing) setTextExchangeOpen(false);
|
if (nextEditing) setConfigurationOpen(false);
|
||||||
return nextEditing;
|
return nextEditing;
|
||||||
})}
|
})}
|
||||||
aria-label={`${editing ? "Masquer" : "Afficher"} l'ajout de ${module.title || label}`}
|
aria-label={`${editing ? "Masquer" : "Afficher"} l'ajout de ${module.title || label}`}
|
||||||
|
|
@ -143,21 +142,23 @@ export function ModuleShell({ toolbox, module, context, reorder, itemGroup = "",
|
||||||
<Icon name="add" />
|
<Icon name="add" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<GuideChecklistToggle checklist={context.guideChecklist} panel={guidePanel} label={module.title || label} />
|
{hasConfiguration && (
|
||||||
{hasTextExchange && (
|
|
||||||
<button
|
<button
|
||||||
className={`module-exchange-button nds-icon-button ${textExchangeOpen ? "active" : ""}`}
|
className={`module-configuration-button nds-icon-button ${configurationOpen ? "active" : ""}`}
|
||||||
data-size="md"
|
data-size="md"
|
||||||
onClick={() => setTextExchangeOpen((value) => {
|
data-guide-linked={guidePanel.list && context.guideChecklist?.selected?.id === guidePanel.list.id || undefined}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setConfigurationOpen((value) => {
|
||||||
const nextOpen = !value;
|
const nextOpen = !value;
|
||||||
if (nextOpen) setEditing(false);
|
if (nextOpen) setEditing(false);
|
||||||
return nextOpen;
|
return nextOpen;
|
||||||
})}
|
})}
|
||||||
aria-label={`${textExchangeOpen ? "Masquer" : "Afficher"} l'import/export de ${module.title || label}`}
|
aria-label={configurationText.buttonLabel || "Configuration"}
|
||||||
aria-pressed={textExchangeOpen}
|
aria-expanded={configurationOpen}
|
||||||
title={textExchangeOpen ? "Masquer import/export" : "Import / export"}
|
aria-controls={configurationId}
|
||||||
|
title={configurationText.buttonLabel || "Configuration"}
|
||||||
>
|
>
|
||||||
<Icon name="import" />
|
<Icon name="settings" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<button className="module-delete-button nds-icon-button" data-size="md" data-variant="danger" onClick={() => onDelete(module.id)} aria-label={`Retirer ${module.title || label}`} data-toolbox-tutorial-target={module.type === "links" ? "deleteTool" : undefined} title="Retirer">
|
<button className="module-delete-button nds-icon-button" data-size="md" data-variant="danger" onClick={() => onDelete(module.id)} aria-label={`Retirer ${module.title || label}`} data-toolbox-tutorial-target={module.type === "links" ? "deleteTool" : undefined} title="Retirer">
|
||||||
|
|
@ -176,15 +177,46 @@ export function ModuleShell({ toolbox, module, context, reorder, itemGroup = "",
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
{!collapsed && (
|
{configurationOpen && (
|
||||||
|
<section
|
||||||
|
id={configurationId}
|
||||||
|
className="module-configuration-panel"
|
||||||
|
aria-label={`${configurationText.panelLabel || "Configuration de"} ${module.title || label}`}
|
||||||
|
>
|
||||||
|
{spaces.length > 1 && (
|
||||||
|
<section className="module-configuration-section module-configuration-organization">
|
||||||
|
<h3>{configurationText.organizationTitle || "Espace"}</h3>
|
||||||
|
<select
|
||||||
|
className="module-space-select nds-select"
|
||||||
|
data-size="sm"
|
||||||
|
value=""
|
||||||
|
aria-label={configurationText.spaceLabel || moveText}
|
||||||
|
onChange={(event) => onMoveToSpace(module.id, event.target.value)}
|
||||||
|
>
|
||||||
|
<option value="" disabled>{moveText}</option>
|
||||||
|
{spaces.filter((space) => space.id !== activeSpaceId).map((space) => <option key={space.id} value={space.id}>{space.name}</option>)}
|
||||||
|
</select>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
{guidePanel.list && (
|
||||||
|
<section className="module-configuration-section module-configuration-specific">
|
||||||
|
<h3>{configurationText.guideTitle || "Guide"}</h3>
|
||||||
|
<GuideChecklistControls checklist={context.guideChecklist} {...guidePanel} open />
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
<div className="module-configuration-target" ref={setConfigurationTarget} />
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
{(!collapsed || configurationOpen) && (
|
||||||
|
<div className="module-component-host" hidden={collapsed}>
|
||||||
<div className={`module-content ${scrollable ? "is-scrollable legacy-scrollbar" : ""}`}>
|
<div className={`module-content ${scrollable ? "is-scrollable legacy-scrollbar" : ""}`}>
|
||||||
<GuideChecklistControls checklist={context.guideChecklist} {...guidePanel} />
|
|
||||||
<Suspense fallback={<ModuleLoadingFallback label={label} />}>
|
<Suspense fallback={<ModuleLoadingFallback label={label} />}>
|
||||||
<ModuleDataGate key={`${toolbox.id}:${module.id}`} context={context} toolboxId={toolbox.id} moduleId={module.id}>
|
<ModuleDataGate key={`${toolbox.id}:${module.id}`} context={context} toolboxId={toolbox.id} moduleId={module.id}>
|
||||||
<Component {...componentProps} />
|
<Component {...componentProps} />
|
||||||
</ModuleDataGate>
|
</ModuleDataGate>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</article>
|
</article>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { Icon } from "../../../components/Icon.jsx";
|
||||||
import { DrawingControls } from "./DrawingControls.jsx";
|
import { DrawingControls } from "./DrawingControls.jsx";
|
||||||
import { DrawingOverlay } from "./DrawingOverlay.jsx";
|
import { DrawingOverlay } from "./DrawingOverlay.jsx";
|
||||||
import { NotepadToolbar } from "./NotepadToolbar.jsx";
|
import { NotepadToolbar } from "./NotepadToolbar.jsx";
|
||||||
import { TextExchangeActions } from "./TextExchangeActions.jsx";
|
import { ModuleTextExchangeConfiguration } from "./ModuleConfiguration.jsx";
|
||||||
import { normalizeNotepadData } from "../storage/toolboxStorage.js";
|
import { normalizeNotepadData } from "../storage/toolboxStorage.js";
|
||||||
import { useKeyedState } from "../../../hooks/useKeyedState.js";
|
import { useKeyedState } from "../../../hooks/useKeyedState.js";
|
||||||
import {
|
import {
|
||||||
|
|
@ -31,7 +31,7 @@ function readTemporaryStrokes(sessionKey) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NotepadModule({ toolboxId, moduleId, context, textExchangeOpen }) {
|
export function NotepadModule({ toolboxId, moduleId, context, configurationTarget }) {
|
||||||
const storedData = context.getModuleData(toolboxId, moduleId, EMPTY_NOTEPAD_DATA);
|
const storedData = context.getModuleData(toolboxId, moduleId, EMPTY_NOTEPAD_DATA);
|
||||||
const normalizedData = useMemo(() => normalizeNotepadData(storedData), [storedData]);
|
const normalizedData = useMemo(() => normalizeNotepadData(storedData), [storedData]);
|
||||||
const notepadDataRef = useRef(normalizedData);
|
const notepadDataRef = useRef(normalizedData);
|
||||||
|
|
@ -295,11 +295,7 @@ export function NotepadModule({ toolboxId, moduleId, context, textExchangeOpen }
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="notepad-module">
|
<div className="notepad-module">
|
||||||
{textExchangeOpen && (
|
<ModuleTextExchangeConfiguration target={configurationTarget} title={context.moduleConfigurationText?.dataTitle} type="notepad" data={normalizedData} context={context} textContent={textContent} onImport={(nextData) => context.setModuleData(toolboxId, moduleId, nextData, "notepad")} />
|
||||||
<div className="module-text-exchange-panel">
|
|
||||||
<TextExchangeActions type="notepad" data={normalizedData} context={context} textContent={textContent} onImport={(nextData) => context.setModuleData(toolboxId, moduleId, nextData, "notepad")} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<CompactDropdown
|
<CompactDropdown
|
||||||
className="notepad-pages-dropdown"
|
className="notepad-pages-dropdown"
|
||||||
menuClassName="notepad-pages-menu"
|
menuClassName="notepad-pages-menu"
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
// Rôle : orchestre les onglets de l'outil Randomizer sans porter les sous-vues.
|
// Rôle : orchestre les onglets de l'outil Randomizer sans porter les sous-vues.
|
||||||
import { Tabs } from "../../../components/Tabs.jsx";
|
import { Tabs } from "../../../components/Tabs.jsx";
|
||||||
import { TextExchangeActions } from "./TextExchangeActions.jsx";
|
import { ModuleTextExchangeConfiguration } from "./ModuleConfiguration.jsx";
|
||||||
import { RANDOMIZER_TABS } from "./randomizerUtils.js";
|
import { RANDOMIZER_TABS } from "./randomizerUtils.js";
|
||||||
import { DiceRandomizer } from "./randomizer/RandomizerDice.jsx";
|
import { DiceRandomizer } from "./randomizer/RandomizerDice.jsx";
|
||||||
import { TeamsRandomizer } from "./randomizer/RandomizerTeams.jsx";
|
import { TeamsRandomizer } from "./randomizer/RandomizerTeams.jsx";
|
||||||
import { WheelRandomizer } from "./randomizer/RandomizerWheel.jsx";
|
import { WheelRandomizer } from "./randomizer/RandomizerWheel.jsx";
|
||||||
|
|
||||||
export function RandomizerModule({ toolboxId, moduleId, context, textExchangeOpen }) {
|
export function RandomizerModule({ toolboxId, moduleId, context, configurationTarget }) {
|
||||||
const data = context.normalizeRandomizerData(context.getModuleData(toolboxId, moduleId, {}));
|
const data = context.normalizeRandomizerData(context.getModuleData(toolboxId, moduleId, {}));
|
||||||
const textContent = context.moduleText?.randomizer || {};
|
const textContent = context.moduleText?.randomizer || {};
|
||||||
|
|
||||||
|
|
@ -20,11 +20,7 @@ export function RandomizerModule({ toolboxId, moduleId, context, textExchangeOpe
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{textExchangeOpen && (
|
<ModuleTextExchangeConfiguration target={configurationTarget} title={context.moduleConfigurationText?.dataTitle} type="randomizer" data={data} context={context} textContent={textContent} onImport={(nextData) => context.setModuleData(toolboxId, moduleId, nextData, "randomizer")} />
|
||||||
<div className="module-text-exchange-panel">
|
|
||||||
<TextExchangeActions type="randomizer" data={data} context={context} textContent={textContent} onImport={(nextData) => context.setModuleData(toolboxId, moduleId, nextData, "randomizer")} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="randomizer-tool">
|
<div className="randomizer-tool">
|
||||||
<Tabs
|
<Tabs
|
||||||
tabs={RANDOMIZER_TABS.map((tab) => ({ ...tab, label: textContent[`${tab.id}Tab`] || tab.label }))}
|
tabs={RANDOMIZER_TABS.map((tab) => ({ ...tab, label: textContent[`${tab.id}Tab`] || tab.label }))}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { Icon } from "../../../components/Icon.jsx";
|
||||||
import { Tabs } from "../../../components/Tabs.jsx";
|
import { Tabs } from "../../../components/Tabs.jsx";
|
||||||
import { useDraftForm } from "../../../hooks/useDraftForm.js";
|
import { useDraftForm } from "../../../hooks/useDraftForm.js";
|
||||||
import { useKeyedState } from "../../../hooks/useKeyedState.js";
|
import { useKeyedState } from "../../../hooks/useKeyedState.js";
|
||||||
import { TextExchangeActions } from "./TextExchangeActions.jsx";
|
import { ModuleTextExchangeConfiguration } from "./ModuleConfiguration.jsx";
|
||||||
import {
|
import {
|
||||||
RNG_SIMULATOR_DEFAULT_RATE,
|
RNG_SIMULATOR_DEFAULT_RATE,
|
||||||
RNG_SIMULATOR_HOURLY_DURATION_PRESETS,
|
RNG_SIMULATOR_HOURLY_DURATION_PRESETS,
|
||||||
|
|
@ -146,7 +146,7 @@ function getToneFromDelta(value) {
|
||||||
return value > 0 ? "positive" : "negative";
|
return value > 0 ? "positive" : "negative";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RngSimulatorModule({ toolboxId, moduleId, context, editing = false, setEditing = () => {}, textExchangeOpen }) {
|
export function RngSimulatorModule({ toolboxId, moduleId, context, editing = false, setEditing = () => {}, configurationTarget }) {
|
||||||
const rawData = context.getModuleData(toolboxId, moduleId, {});
|
const rawData = context.getModuleData(toolboxId, moduleId, {});
|
||||||
const data = useMemo(() => context.normalizeRngSimulatorData(rawData), [context, rawData]);
|
const data = useMemo(() => context.normalizeRngSimulatorData(rawData), [context, rawData]);
|
||||||
const textContent = context.moduleText?.rngSimulator || {};
|
const textContent = context.moduleText?.rngSimulator || {};
|
||||||
|
|
@ -405,11 +405,7 @@ export function RngSimulatorModule({ toolboxId, moduleId, context, editing = fal
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{textExchangeOpen && (
|
<ModuleTextExchangeConfiguration target={configurationTarget} title={context.moduleConfigurationText?.dataTitle} type="rngSimulator" data={data} context={context} textContent={textContent} onImport={(nextData) => context.setModuleData(toolboxId, moduleId, nextData, "rngSimulator")} />
|
||||||
<div className="module-text-exchange-panel">
|
|
||||||
<TextExchangeActions type="rngSimulator" data={data} context={context} textContent={textContent} onImport={(nextData) => context.setModuleData(toolboxId, moduleId, nextData, "rngSimulator")} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="rng-simulator-tool">
|
<div className="rng-simulator-tool">
|
||||||
<section className="rng-simulator-panel rng-simulator-rates-panel">
|
<section className="rng-simulator-panel rng-simulator-rates-panel">
|
||||||
<header>
|
<header>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
// Rôle : fournit l'outil tableau avec cellules libres, formules simples et échange TSV.
|
// Rôle : fournit l'outil tableau avec cellules libres, formules simples et échange TSV.
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { TextExchangeActions } from "./TextExchangeActions.jsx";
|
import { ModuleTextExchangeConfiguration } from "./ModuleConfiguration.jsx";
|
||||||
import { columnIndexToName, evaluateTable } from "./tableFormulaEngine.js";
|
import { columnIndexToName, evaluateTable } from "./tableFormulaEngine.js";
|
||||||
import { TableGrid } from "./TableGrid.jsx";
|
import { TableGrid } from "./TableGrid.jsx";
|
||||||
import { TableToolbar } from "./TableToolbar.jsx";
|
import { TableToolbar } from "./TableToolbar.jsx";
|
||||||
|
|
@ -14,7 +14,7 @@ import {
|
||||||
} from "./tableUtils.js";
|
} from "./tableUtils.js";
|
||||||
import { useTableInteractions } from "./useTableInteractions.js";
|
import { useTableInteractions } from "./useTableInteractions.js";
|
||||||
|
|
||||||
export function TableModule({ toolboxId, moduleId, context, textExchangeOpen }) {
|
export function TableModule({ toolboxId, moduleId, context, configurationTarget }) {
|
||||||
const rawData = context.getModuleData(toolboxId, moduleId, { rows: DEFAULT_ROWS, columns: DEFAULT_COLUMNS, cells: {} });
|
const rawData = context.getModuleData(toolboxId, moduleId, { rows: DEFAULT_ROWS, columns: DEFAULT_COLUMNS, cells: {} });
|
||||||
const data = useMemo(() => context.normalizeTableData(rawData), [context, rawData]);
|
const data = useMemo(() => context.normalizeTableData(rawData), [context, rawData]);
|
||||||
const textContent = context.moduleText?.table || {};
|
const textContent = context.moduleText?.table || {};
|
||||||
|
|
@ -61,11 +61,7 @@ export function TableModule({ toolboxId, moduleId, context, textExchangeOpen })
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="table-module">
|
<div className="table-module">
|
||||||
{textExchangeOpen && (
|
<ModuleTextExchangeConfiguration target={configurationTarget} title={context.moduleConfigurationText?.dataTitle} type="table" data={data} context={context} textContent={textContent} exportLabel="Exporter TSV" onImport={(nextData) => context.setModuleData(toolboxId, moduleId, nextData, "table")} />
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
<TableToolbar rows={data.rows} columns={data.columns} textContent={textContent} onResize={resize} onCopy={tableInteractions.copyTsv} />
|
<TableToolbar rows={data.rows} columns={data.columns} textContent={textContent} onResize={resize} onCopy={tableInteractions.copyTsv} />
|
||||||
<TableGrid
|
<TableGrid
|
||||||
data={data}
|
data={data}
|
||||||
|
|
|
||||||
|
|
@ -3,15 +3,17 @@ import { toggleListValue } from "../../../utils/collectionUtils.js";
|
||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useDraftForm } from "../../../hooks/useDraftForm.js";
|
import { useDraftForm } from "../../../hooks/useDraftForm.js";
|
||||||
import { TaskPlannerBranch } from "./TaskPlannerBranch.jsx";
|
import { TaskPlannerBranch } from "./TaskPlannerBranch.jsx";
|
||||||
|
import { ModuleConfigurationPortal } from "./ModuleConfiguration.jsx";
|
||||||
import { TaskPlannerSettings } from "./TaskPlannerSettings.jsx";
|
import { TaskPlannerSettings } from "./TaskPlannerSettings.jsx";
|
||||||
import { TaskPlannerTopbar } from "./TaskPlannerTopbar.jsx";
|
import { TaskPlannerTopbar } from "./TaskPlannerTopbar.jsx";
|
||||||
|
import { TextExchangeActions } from "./TextExchangeActions.jsx";
|
||||||
import {
|
import {
|
||||||
applyDueResets,
|
applyDueResets,
|
||||||
getTaskParentMap
|
getTaskParentMap
|
||||||
} from "./taskPlannerUtils.js";
|
} from "./taskPlannerUtils.js";
|
||||||
import { useTaskPlannerReorder } from "./useTaskPlannerReorder.js";
|
import { useTaskPlannerReorder } from "./useTaskPlannerReorder.js";
|
||||||
|
|
||||||
export function TaskPlannerModule({ toolboxId, moduleId, context, editing, textExchangeOpen }) {
|
export function TaskPlannerModule({ toolboxId, moduleId, context, editing, configurationTarget }) {
|
||||||
const data = context.normalizeTaskPlannerData(context.getModuleData(toolboxId, moduleId, { tasks: [] }));
|
const data = context.normalizeTaskPlannerData(context.getModuleData(toolboxId, moduleId, { tasks: [] }));
|
||||||
const textContent = context.moduleText?.taskPlanner || {};
|
const textContent = context.moduleText?.taskPlanner || {};
|
||||||
const taskDraft = useDraftForm({ title: "", type: "daily", category: "" });
|
const taskDraft = useDraftForm({ title: "", type: "daily", category: "" });
|
||||||
|
|
@ -20,7 +22,6 @@ export function TaskPlannerModule({ toolboxId, moduleId, context, editing, textE
|
||||||
...data.tasks.map((task) => task.category).filter(Boolean).filter((category) => !data.categoryOrder.includes(category))
|
...data.tasks.map((task) => task.category).filter(Boolean).filter((category) => !data.categoryOrder.includes(category))
|
||||||
], [data.categoryOrder, data.tasks]);
|
], [data.categoryOrder, data.tasks]);
|
||||||
const [nowMs, setNowMs] = useState(() => Date.now());
|
const [nowMs, setNowMs] = useState(() => Date.now());
|
||||||
const [settingsOpen, setSettingsOpen] = useState(false);
|
|
||||||
const [openDescriptions, setOpenDescriptions] = useState(() => new Set());
|
const [openDescriptions, setOpenDescriptions] = useState(() => new Set());
|
||||||
const [openTaskSettings, setOpenTaskSettings] = useState(() => new Set());
|
const [openTaskSettings, setOpenTaskSettings] = useState(() => new Set());
|
||||||
const [prerequisiteNoticeTaskIds, setPrerequisiteNoticeTaskIds] = useState(() => new Set());
|
const [prerequisiteNoticeTaskIds, setPrerequisiteNoticeTaskIds] = useState(() => new Set());
|
||||||
|
|
@ -170,22 +171,9 @@ export function TaskPlannerModule({ toolboxId, moduleId, context, editing, textE
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="task-planner-module">
|
<div className="task-planner-module">
|
||||||
<TaskPlannerTopbar
|
<ModuleConfigurationPortal target={configurationTarget}>
|
||||||
data={data}
|
<section className="module-configuration-section module-configuration-specific">
|
||||||
context={context}
|
<h3>{textContent.settingsTitle || "Réglages du planificateur"}</h3>
|
||||||
editing={editing}
|
|
||||||
settingsOpen={settingsOpen}
|
|
||||||
categoryOptions={categoryOptions}
|
|
||||||
taskDraft={taskDraft}
|
|
||||||
textContent={textContent}
|
|
||||||
textExchangeOpen={textExchangeOpen}
|
|
||||||
onAddTask={addTask}
|
|
||||||
onHideCompletedToggle={() => save({ ...data, hideCompleted: !data.hideCompleted })}
|
|
||||||
onSettingsToggle={() => setSettingsOpen((value) => !value)}
|
|
||||||
onImport={(nextData) => save(nextData)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{settingsOpen && (
|
|
||||||
<TaskPlannerSettings
|
<TaskPlannerSettings
|
||||||
data={data}
|
data={data}
|
||||||
textContent={textContent}
|
textContent={textContent}
|
||||||
|
|
@ -193,7 +181,21 @@ export function TaskPlannerModule({ toolboxId, moduleId, context, editing, textE
|
||||||
onResetTimeChange={(resetTime) => save({ ...data, resetTime })}
|
onResetTimeChange={(resetTime) => save({ ...data, resetTime })}
|
||||||
onClearCompletedUniqueTasks={clearCompletedUniqueTasks}
|
onClearCompletedUniqueTasks={clearCompletedUniqueTasks}
|
||||||
/>
|
/>
|
||||||
)}
|
</section>
|
||||||
|
<section className="module-configuration-section module-configuration-data">
|
||||||
|
<h3>{context.moduleConfigurationText?.dataTitle || "Données"}</h3>
|
||||||
|
<TextExchangeActions type="taskPlanner" data={data} context={context} textContent={textContent} onImport={(nextData) => save(nextData)} />
|
||||||
|
</section>
|
||||||
|
</ModuleConfigurationPortal>
|
||||||
|
<TaskPlannerTopbar
|
||||||
|
data={data}
|
||||||
|
editing={editing}
|
||||||
|
categoryOptions={categoryOptions}
|
||||||
|
taskDraft={taskDraft}
|
||||||
|
textContent={textContent}
|
||||||
|
onAddTask={addTask}
|
||||||
|
onHideCompletedToggle={() => save({ ...data, hideCompleted: !data.hideCompleted })}
|
||||||
|
/>
|
||||||
|
|
||||||
<ul className="task-planner-list">
|
<ul className="task-planner-list">
|
||||||
{data.tasks.length ? (
|
{data.tasks.length ? (
|
||||||
|
|
|
||||||
|
|
@ -7,18 +7,18 @@ export function TaskPlannerSettings({ data, textContent, onWeeklyResetDayChange,
|
||||||
<section className="task-planner-settings" aria-label={textContent.settingsTitle || "Réglages du planificateur"}>
|
<section className="task-planner-settings" aria-label={textContent.settingsTitle || "Réglages du planificateur"}>
|
||||||
<label>
|
<label>
|
||||||
<span>{textContent.globalWeeklyResetDayLabel || "Reset hebdo"}</span>
|
<span>{textContent.globalWeeklyResetDayLabel || "Reset hebdo"}</span>
|
||||||
<select className="nds-select" data-size="sm" value={data.weeklyResetDay} onChange={(event) => onWeeklyResetDayChange(Number(event.target.value))}>
|
<select className="nds-select" data-size="xs" value={data.weeklyResetDay} onChange={(event) => onWeeklyResetDayChange(Number(event.target.value))}>
|
||||||
{WEEK_DAYS.map((day) => <option key={day.value} value={day.value}>{day.label}</option>)}
|
{WEEK_DAYS.map((day) => <option key={day.value} value={day.value}>{day.label}</option>)}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<span>{textContent.resetTimeLabel || "Heure de reset"}</span>
|
<span>{textContent.resetTimeLabel || "Heure de reset"}</span>
|
||||||
<input className="nds-time" data-size="sm" type="time" value={data.resetTime} onChange={(event) => onResetTimeChange(event.target.value || "00:00")} />
|
<input className="nds-time" data-size="xs" type="time" value={data.resetTime} onChange={(event) => onResetTimeChange(event.target.value || "00:00")} />
|
||||||
</label>
|
</label>
|
||||||
<div className="task-planner-settings-actions">
|
<div className="task-planner-settings-actions">
|
||||||
<button
|
<button
|
||||||
className="task-planner-clear-completed-button nds-button"
|
className="task-planner-clear-completed-button nds-button"
|
||||||
data-size="sm"
|
data-size="xs"
|
||||||
data-variant="danger"
|
data-variant="danger"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onClearCompletedUniqueTasks}
|
onClick={onClearCompletedUniqueTasks}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
// Rôle : rend la barre haute du task planner, l'ajout rapide et l'échange texte.
|
// Rôle : rend les contrôles fréquents et l'ajout rapide du planificateur de tâches.
|
||||||
import { CategoryAutocompleteInput } from "../../../components/CategoryAutocompleteInput.jsx";
|
import { CategoryAutocompleteInput } from "../../../components/CategoryAutocompleteInput.jsx";
|
||||||
import { Icon } from "../../../components/Icon.jsx";
|
import { Icon } from "../../../components/Icon.jsx";
|
||||||
import { TextExchangeActions } from "./TextExchangeActions.jsx";
|
|
||||||
import {
|
import {
|
||||||
TASK_TYPES,
|
TASK_TYPES,
|
||||||
getTaskTypeLabel
|
getTaskTypeLabel
|
||||||
|
|
@ -9,17 +8,12 @@ import {
|
||||||
|
|
||||||
export function TaskPlannerTopbar({
|
export function TaskPlannerTopbar({
|
||||||
data,
|
data,
|
||||||
context,
|
|
||||||
editing,
|
editing,
|
||||||
settingsOpen,
|
|
||||||
categoryOptions,
|
categoryOptions,
|
||||||
taskDraft,
|
taskDraft,
|
||||||
textContent,
|
textContent,
|
||||||
textExchangeOpen,
|
|
||||||
onAddTask,
|
onAddTask,
|
||||||
onHideCompletedToggle,
|
onHideCompletedToggle
|
||||||
onSettingsToggle,
|
|
||||||
onImport
|
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="task-planner-topbar">
|
<div className="task-planner-topbar">
|
||||||
|
|
@ -36,18 +30,6 @@ export function TaskPlannerTopbar({
|
||||||
<Icon name={data.hideCompleted ? "eye-closed" : "eye-open"} />
|
<Icon name={data.hideCompleted ? "eye-closed" : "eye-open"} />
|
||||||
<i aria-hidden="true" />
|
<i aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
className={`task-planner-settings-toggle nds-button ${settingsOpen ? "active" : ""}`}
|
|
||||||
data-size="sm"
|
|
||||||
type="button"
|
|
||||||
onClick={onSettingsToggle}
|
|
||||||
aria-expanded={settingsOpen}
|
|
||||||
aria-label={textContent.settingsTitle || "Réglages du planificateur"}
|
|
||||||
title={textContent.settingsTitle || "Réglages du planificateur"}
|
|
||||||
>
|
|
||||||
<Icon name="settings" />
|
|
||||||
<span>{textContent.settingsButtonLabel || "Paramètres des réinitialisations"}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
{editing && (
|
{editing && (
|
||||||
<div className="task-planner-add-stack">
|
<div className="task-planner-add-stack">
|
||||||
|
|
@ -61,11 +43,6 @@ export function TaskPlannerTopbar({
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{textExchangeOpen && (
|
|
||||||
<div className="module-text-exchange-panel">
|
|
||||||
<TextExchangeActions type="taskPlanner" data={data} context={context} textContent={textContent} onImport={onImport} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,17 +5,20 @@ import { useTextExchange } from "./useTextExchange.js";
|
||||||
|
|
||||||
export function TextExchangeActions({ type, data, context, onImport, textContent = {}, exportLabel = "" }) {
|
export function TextExchangeActions({ type, data, context, onImport, textContent = {}, exportLabel = "" }) {
|
||||||
const exchange = useTextExchange({ type, data, context, onImport, textContent });
|
const exchange = useTextExchange({ type, data, context, onImport, textContent });
|
||||||
|
const importAccessibleLabel = textContent.importOpenButton || "Importer du texte";
|
||||||
|
const exportAccessibleLabel = textContent.exportOpenButton || exportLabel || "Exporter en format texte";
|
||||||
|
const configurationText = context.moduleConfigurationText || {};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="text-import-actions">
|
<div className="text-import-actions">
|
||||||
<button className="nds-button" data-size="sm" type="button" onClick={exchange.openImport}>
|
<button className="nds-button" data-size="sm" type="button" onClick={exchange.openImport} aria-label={importAccessibleLabel}>
|
||||||
<Icon name="import" />
|
<Icon name="import" />
|
||||||
<span>{textContent.importOpenButton || "Importer du texte"}</span>
|
<span>{configurationText.importButtonLabel || "Importer"}</span>
|
||||||
</button>
|
</button>
|
||||||
<button className="nds-button" data-size="sm" type="button" onClick={exchange.openExport} disabled={!exchange.canExport}>
|
<button className="nds-button" data-size="sm" type="button" onClick={exchange.openExport} disabled={!exchange.canExport} aria-label={exportAccessibleLabel}>
|
||||||
<Icon name="export" />
|
<Icon name="export" />
|
||||||
<span>{textContent.exportOpenButton || exportLabel || "Exporter en format texte"}</span>
|
<span>{configurationText.exportButtonLabel || "Exporter"}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{exchange.mode === "import" && (
|
{exchange.mode === "import" && (
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,10 @@ import { lockBodyScroll } from "../../../utils/bodyScrollLock.js";
|
||||||
export function TextImportModal({ title, value, placeholder, submitLabel, readOnly = false, error = "", info = "", onChange, onSubmit, onCopy, onClose }) {
|
export function TextImportModal({ title, value, placeholder, submitLabel, readOnly = false, error = "", info = "", onChange, onSubmit, onCopy, onClose }) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function handleKeyDown(event) {
|
function handleKeyDown(event) {
|
||||||
if (event.key === "Escape") onClose();
|
if (event.key === "Escape") {
|
||||||
|
event.stopPropagation();
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const unlock = lockBodyScroll();
|
const unlock = lockBodyScroll();
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ import {
|
||||||
timePartsToDurationMs,
|
timePartsToDurationMs,
|
||||||
timePartsToString
|
timePartsToString
|
||||||
} from "./timerUtils.js";
|
} from "./timerUtils.js";
|
||||||
import { TextExchangeActions } from "./TextExchangeActions.jsx";
|
import { ModuleTextExchangeConfiguration } from "./ModuleConfiguration.jsx";
|
||||||
import { CountdownControls, CountdownList } from "./TimerCountdown.jsx";
|
import { CountdownControls, CountdownList } from "./TimerCountdown.jsx";
|
||||||
import { StopwatchControls, StopwatchLapList } from "./TimerStopwatch.jsx";
|
import { StopwatchControls, StopwatchLapList } from "./TimerStopwatch.jsx";
|
||||||
import {
|
import {
|
||||||
|
|
@ -30,7 +30,7 @@ import {
|
||||||
isAlertGuarded
|
isAlertGuarded
|
||||||
} from "./timerOptions.js";
|
} from "./timerOptions.js";
|
||||||
|
|
||||||
export function TimerModule({ toolboxId, moduleId, context, textExchangeOpen }) {
|
export function TimerModule({ toolboxId, moduleId, context, configurationTarget }) {
|
||||||
const rawData = context.getModuleData(toolboxId, moduleId, { activeTab: "stopwatch" });
|
const rawData = context.getModuleData(toolboxId, moduleId, { activeTab: "stopwatch" });
|
||||||
const data = useMemo(() => context.normalizeTimerData(rawData), [context, rawData]);
|
const data = useMemo(() => context.normalizeTimerData(rawData), [context, rawData]);
|
||||||
const textContent = context.moduleText?.timer || {};
|
const textContent = context.moduleText?.timer || {};
|
||||||
|
|
@ -252,11 +252,7 @@ export function TimerModule({ toolboxId, moduleId, context, textExchangeOpen })
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="timer-module">
|
<div className="timer-module">
|
||||||
{textExchangeOpen && (
|
<ModuleTextExchangeConfiguration target={configurationTarget} title={context.moduleConfigurationText?.dataTitle} type="timer" data={data} context={context} textContent={textContent} onImport={(nextData) => save(nextData)} />
|
||||||
<div className="module-text-exchange-panel">
|
|
||||||
<TextExchangeActions type="timer" data={data} context={context} textContent={textContent} onImport={(nextData) => save(nextData)} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<section className="timer-control-card">
|
<section className="timer-control-card">
|
||||||
<Tabs
|
<Tabs
|
||||||
className="timer-tabs"
|
className="timer-tabs"
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ function getModulesByIds(modules, ids) {
|
||||||
return (Array.isArray(ids) ? ids : []).map((id) => moduleMap.get(id)).filter(Boolean);
|
return (Array.isArray(ids) ? ids : []).map((id) => moduleMap.get(id)).filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ToolboxModules({ toolbox, space, moduleColumns, context, reorder, spacesText, scrollToModuleId = "", onModuleScrolled, onRename, onUpdateModule, onDelete, onMoveToSpace }) {
|
export function ToolboxModules({ toolbox, space, moduleColumns, context, reorder, spacesText, configurationText, scrollToModuleId = "", onModuleScrolled, onRename, onUpdateModule, onDelete, onMoveToSpace }) {
|
||||||
const twoColumnOrder = useMemo(() => space.moduleOrder?.two || [[], []], [space.moduleOrder?.two]);
|
const twoColumnOrder = useMemo(() => space.moduleOrder?.two || [[], []], [space.moduleOrder?.two]);
|
||||||
const orderedModules = useMemo(() => getModulesByIds(toolbox.modules, space.moduleOrder?.one || []), [toolbox.modules, space.moduleOrder?.one]);
|
const orderedModules = useMemo(() => getModulesByIds(toolbox.modules, space.moduleOrder?.one || []), [toolbox.modules, space.moduleOrder?.one]);
|
||||||
const columns = useMemo(() => [
|
const columns = useMemo(() => [
|
||||||
|
|
@ -32,6 +32,7 @@ export function ToolboxModules({ toolbox, space, moduleColumns, context, reorder
|
||||||
spaces={toolbox.spaces}
|
spaces={toolbox.spaces}
|
||||||
activeSpaceId={space.id}
|
activeSpaceId={space.id}
|
||||||
moveText={spacesText.moveLabel}
|
moveText={spacesText.moveLabel}
|
||||||
|
configurationText={configurationText}
|
||||||
onMoveToSpace={onMoveToSpace}
|
onMoveToSpace={onMoveToSpace}
|
||||||
scrollIntoView={module.id === scrollToModuleId}
|
scrollIntoView={module.id === scrollToModuleId}
|
||||||
onScrolledIntoView={onModuleScrolled}
|
onScrolledIntoView={onModuleScrolled}
|
||||||
|
|
@ -65,6 +66,7 @@ export function ToolboxModules({ toolbox, space, moduleColumns, context, reorder
|
||||||
spaces={toolbox.spaces}
|
spaces={toolbox.spaces}
|
||||||
activeSpaceId={space.id}
|
activeSpaceId={space.id}
|
||||||
moveText={spacesText.moveLabel}
|
moveText={spacesText.moveLabel}
|
||||||
|
configurationText={configurationText}
|
||||||
onMoveToSpace={onMoveToSpace}
|
onMoveToSpace={onMoveToSpace}
|
||||||
scrollIntoView={module.id === scrollToModuleId}
|
scrollIntoView={module.id === scrollToModuleId}
|
||||||
onScrolledIntoView={onModuleScrolled}
|
onScrolledIntoView={onModuleScrolled}
|
||||||
|
|
|
||||||
62
website/src/features/toolboxes/modules/memosPlannerUtils.js
Normal file
62
website/src/features/toolboxes/modules/memosPlannerUtils.js
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
// Rôle : résout les liaisons de Mémos vers les planificateurs et produit leurs alertes de reset.
|
||||||
|
import { normalizeTaskPlannerData } from "../storage/modules/taskPlanner.js";
|
||||||
|
import {
|
||||||
|
applyDueResets,
|
||||||
|
getEffectiveTaskCategory,
|
||||||
|
getTaskParentMap,
|
||||||
|
isTaskResetSoon
|
||||||
|
} from "./taskPlannerUtils.js";
|
||||||
|
|
||||||
|
export const ALL_TASK_PLANNERS_LINK = "all";
|
||||||
|
|
||||||
|
export function getTaskPlannerModules(modules) {
|
||||||
|
return (Array.isArray(modules) ? modules : []).filter((module) => module.type === "taskPlanner");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveMemosPlannerLink(plannerLink, modules) {
|
||||||
|
const value = String(plannerLink || "");
|
||||||
|
const planners = getTaskPlannerModules(modules);
|
||||||
|
if (value === ALL_TASK_PLANNERS_LINK) return planners.length ? value : "";
|
||||||
|
return planners.some((planner) => planner.id === value) ? value : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLinkedTaskPlannerModules(plannerLink, modules) {
|
||||||
|
const planners = getTaskPlannerModules(modules);
|
||||||
|
if (plannerLink === ALL_TASK_PLANNERS_LINK) return planners;
|
||||||
|
return planners.filter((planner) => planner.id === plannerLink);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMemosDataAfterPlannerRemoval(data, removedPlannerId, remainingModules) {
|
||||||
|
const remainingPlanners = getTaskPlannerModules(remainingModules);
|
||||||
|
const link = String(data?.plannerLink || "");
|
||||||
|
if (link === removedPlannerId || (link === ALL_TASK_PLANNERS_LINK && !remainingPlanners.length)) {
|
||||||
|
return { ...data, plannerLink: "" };
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTaskAlert(task, category, template) {
|
||||||
|
return String(template || "Tâche : {category}{title} expire bientôt")
|
||||||
|
.replace("{category}", category ? `${category} ` : "")
|
||||||
|
.replace("{title}", task.title);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPlannerTaskTickerItems({
|
||||||
|
plannerLink,
|
||||||
|
modules,
|
||||||
|
getPlannerData,
|
||||||
|
nowMs = Date.now(),
|
||||||
|
alertTemplate
|
||||||
|
}) {
|
||||||
|
return getLinkedTaskPlannerModules(plannerLink, modules).flatMap((planner) => {
|
||||||
|
const data = applyDueResets(normalizeTaskPlannerData(getPlannerData(planner.id)), nowMs);
|
||||||
|
const parentMap = getTaskParentMap(data.tasks, data.relations);
|
||||||
|
return data.tasks
|
||||||
|
.filter((task) => isTaskResetSoon(task, data, nowMs))
|
||||||
|
.map((task) => ({
|
||||||
|
id: `planner:${planner.id}:${task.id}`,
|
||||||
|
kind: "task-warning",
|
||||||
|
text: formatTaskAlert(task, getEffectiveTaskCategory(task, data.tasks, parentMap), alertTemplate)
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
// Rôle : normalise et compacte les mémos courts des toolboxes.
|
// Rôle : normalise et compacte les mémos courts et leur liaison éventuelle aux planificateurs.
|
||||||
import { NOTEPAD_ALLOWED_COLORS } from "./drawing.js";
|
import { NOTEPAD_ALLOWED_COLORS } from "./drawing.js";
|
||||||
import { normalizeIsoDate, uid } from "./shared.js";
|
import { normalizeIsoDate, uid } from "./shared.js";
|
||||||
|
|
||||||
|
|
@ -20,6 +20,7 @@ export function normalizeMemosData(data) {
|
||||||
const source = data && typeof data === "object" ? data : {};
|
const source = data && typeof data === "object" ? data : {};
|
||||||
return {
|
return {
|
||||||
tickerEnabled: source.tickerEnabled !== false,
|
tickerEnabled: source.tickerEnabled !== false,
|
||||||
|
plannerLink: String(source.plannerLink || "").trim(),
|
||||||
items: (Array.isArray(source.items) ? source.items : []).map(normalizeMemoItem).filter(Boolean)
|
items: (Array.isArray(source.items) ? source.items : []).map(normalizeMemoItem).filter(Boolean)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -28,6 +29,7 @@ export function compactMemosDataForStorage(value) {
|
||||||
const normalized = normalizeMemosData(value);
|
const normalized = normalizeMemosData(value);
|
||||||
const compact = {};
|
const compact = {};
|
||||||
if (normalized.tickerEnabled === false) compact.tickerEnabled = false;
|
if (normalized.tickerEnabled === false) compact.tickerEnabled = false;
|
||||||
|
if (normalized.plannerLink) compact.plannerLink = normalized.plannerLink;
|
||||||
if (normalized.items.length) {
|
if (normalized.items.length) {
|
||||||
compact.items = normalized.items.map((item) => {
|
compact.items = normalized.items.map((item) => {
|
||||||
const compactItem = {
|
const compactItem = {
|
||||||
|
|
|
||||||
|
|
@ -242,6 +242,11 @@ export function compactModuleDataForStorage(type, value) {
|
||||||
return compactor ? compactor(value) : value;
|
return compactor ? compactor(value) : value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function remapMemosPlannerLink(data, resolveModuleId) {
|
||||||
|
if (!data || typeof data !== "object" || !data.plannerLink || data.plannerLink === "all") return data;
|
||||||
|
return { ...data, plannerLink: resolveModuleId(data.plannerLink) || "" };
|
||||||
|
}
|
||||||
|
|
||||||
export function prepareModuleDataForStorage(toolboxes, toolboxId, moduleId, value, moduleType = "") {
|
export function prepareModuleDataForStorage(toolboxes, toolboxId, moduleId, value, moduleType = "") {
|
||||||
const module = toolboxes.find((toolbox) => toolbox.id === toolboxId)?.modules.find((item) => item.id === moduleId);
|
const module = toolboxes.find((toolbox) => toolbox.id === toolboxId)?.modules.find((item) => item.id === moduleId);
|
||||||
return compactModuleDataForStorage(moduleType || module?.type, value);
|
return compactModuleDataForStorage(moduleType || module?.type, value);
|
||||||
|
|
@ -255,7 +260,7 @@ function createExportIdFactory() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function remapModuleDataForExport(type, data, nextId) {
|
function remapModuleDataForExport(type, data, nextId, moduleIdMap) {
|
||||||
const compact = compactModuleDataForStorage(type, data);
|
const compact = compactModuleDataForStorage(type, data);
|
||||||
if (!compact) return null;
|
if (!compact) return null;
|
||||||
if (type === "checklist") {
|
if (type === "checklist") {
|
||||||
|
|
@ -332,6 +337,8 @@ function remapModuleDataForExport(type, data, nextId) {
|
||||||
if (type === "memos") {
|
if (type === "memos") {
|
||||||
const remapped = {};
|
const remapped = {};
|
||||||
if (compact.tickerEnabled === false) remapped.tickerEnabled = false;
|
if (compact.tickerEnabled === false) remapped.tickerEnabled = false;
|
||||||
|
if (compact.plannerLink === "all") remapped.plannerLink = "all";
|
||||||
|
else if (compact.plannerLink && moduleIdMap.has(compact.plannerLink)) remapped.plannerLink = moduleIdMap.get(compact.plannerLink);
|
||||||
if (compact.items) remapped.items = compact.items.map((memo) => ({ ...memo, id: nextId("memo") }));
|
if (compact.items) remapped.items = compact.items.map((memo) => ({ ...memo, id: nextId("memo") }));
|
||||||
return remapped;
|
return remapped;
|
||||||
}
|
}
|
||||||
|
|
@ -490,7 +497,7 @@ export function createToolboxExportPayload(toolbox, moduleData) {
|
||||||
const modules = Object.fromEntries(source.modules
|
const modules = Object.fromEntries(source.modules
|
||||||
.map((module) => [
|
.map((module) => [
|
||||||
moduleIdMap.get(module.id),
|
moduleIdMap.get(module.id),
|
||||||
remapModuleDataForExport(module.type, moduleData[moduleStorageKey(source.id, module.id)] || null, nextId)
|
remapModuleDataForExport(module.type, moduleData[moduleStorageKey(source.id, module.id)] || null, nextId, moduleIdMap)
|
||||||
])
|
])
|
||||||
.filter(([, data]) => data != null));
|
.filter(([, data]) => data != null));
|
||||||
return { version: 2, toolbox: exportedToolbox, modules };
|
return { version: 2, toolbox: exportedToolbox, modules };
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import {
|
||||||
labelFromFileName,
|
labelFromFileName,
|
||||||
MAX_TOOLBOX_SPACES,
|
MAX_TOOLBOX_SPACES,
|
||||||
normalizeToolbox,
|
normalizeToolbox,
|
||||||
|
remapMemosPlannerLink,
|
||||||
remapModuleOrderIds,
|
remapModuleOrderIds,
|
||||||
uid
|
uid
|
||||||
} from "./storage/toolboxStorage.js";
|
} from "./storage/toolboxStorage.js";
|
||||||
|
|
@ -207,7 +208,8 @@ export function useToolboxActions({ store, notify, creationTemplate, setConfirmM
|
||||||
const nextId = moduleIdMap.get(oldId);
|
const nextId = moduleIdMap.get(oldId);
|
||||||
const module = normalizedImport.modules.find((item) => item.id === nextId);
|
const module = normalizedImport.modules.find((item) => item.id === nextId);
|
||||||
if (nextId) {
|
if (nextId) {
|
||||||
const compact = compactModuleDataForStorage(module?.type, data);
|
const remappedData = module?.type === "memos" ? remapMemosPlannerLink(data, (plannerId) => moduleIdMap.get(plannerId)) : data;
|
||||||
|
const compact = compactModuleDataForStorage(module?.type, remappedData);
|
||||||
if (compact) nextModuleData[`${normalizedImport.id}:${nextId}`] = compact;
|
if (compact) nextModuleData[`${normalizedImport.id}:${nextId}`] = compact;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -270,7 +272,10 @@ export function useToolboxActions({ store, notify, creationTemplate, setConfirmM
|
||||||
const nextModuleId = moduleIdMap.get(key);
|
const nextModuleId = moduleIdMap.get(key);
|
||||||
const toolbox = importedToolboxes.find((item) => item.id === nextToolboxId);
|
const toolbox = importedToolboxes.find((item) => item.id === nextToolboxId);
|
||||||
const module = toolbox?.modules.find((item) => item.id === nextModuleId);
|
const module = toolbox?.modules.find((item) => item.id === nextModuleId);
|
||||||
const compact = compactModuleDataForStorage(module?.type, data);
|
const remappedData = module?.type === "memos"
|
||||||
|
? remapMemosPlannerLink(data, (plannerId) => moduleIdMap.get(globalModuleKey(oldToolboxId, plannerId)))
|
||||||
|
: data;
|
||||||
|
const compact = compactModuleDataForStorage(module?.type, remappedData);
|
||||||
if (nextToolboxId && nextModuleId && compact) nextModuleData[`${nextToolboxId}:${nextModuleId}`] = compact;
|
if (nextToolboxId && nextModuleId && compact) nextModuleData[`${nextToolboxId}:${nextModuleId}`] = compact;
|
||||||
});
|
});
|
||||||
const saved = await store.persistToolboxSnapshot((current) => ({
|
const saved = await store.persistToolboxSnapshot((current) => ({
|
||||||
|
|
|
||||||
|
|
@ -74,11 +74,41 @@
|
||||||
.topbar,
|
.topbar,
|
||||||
.page-heading,
|
.page-heading,
|
||||||
.toolbox-head,
|
.toolbox-head,
|
||||||
.module header {
|
.module .module-header {
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.module .module-header-actions {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-configuration-panel {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-configuration-organization,
|
||||||
|
.module-configuration-data,
|
||||||
|
.module-configuration-panel:not(:has(.module-configuration-organization)) .module-configuration-data,
|
||||||
|
.module-configuration-specific {
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-configuration-organization {
|
||||||
|
order: 0;
|
||||||
|
padding-right: 6px;
|
||||||
|
border-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-configuration-data {
|
||||||
|
order: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-configuration-specific {
|
||||||
|
order: 2;
|
||||||
|
}
|
||||||
|
|
||||||
.topbar {
|
.topbar {
|
||||||
padding: var(--space-4);
|
padding: var(--space-4);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
.nds-switch { max-width: 100%; }
|
.nds-switch { max-width: 100%; }
|
||||||
.guide-checklist-actions { display: flex; flex-wrap: wrap; justify-content: center; align-items: center; gap: var(--space-2); padding-top: var(--space-2); }
|
.guide-checklist-actions { display: flex; flex-wrap: wrap; justify-content: center; align-items: center; gap: var(--space-2); padding-top: var(--space-2); }
|
||||||
}
|
}
|
||||||
.module-guide-button[data-linked] .ui-icon { color: var(--color-accent-gold); }
|
.module-configuration-button[data-guide-linked] .ui-icon { color: var(--color-accent-gold); }
|
||||||
.game-heading-actions {
|
.game-heading-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-self: stretch;
|
align-self: stretch;
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,28 @@
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.memos-settings {
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.memos-settings label {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(120px, auto) minmax(180px, 1fr);
|
||||||
|
gap: var(--space-3);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.memos-settings label > span {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
font-weight: 900;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.memos-settings select {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.memo-card.is-drop-target {
|
.memo-card.is-drop-target {
|
||||||
border-color: rgba(246, 196, 83, 0.48);
|
border-color: rgba(246, 196, 83, 0.48);
|
||||||
background: rgba(246, 196, 83, 0.1);
|
background: rgba(246, 196, 83, 0.1);
|
||||||
|
|
@ -74,6 +96,10 @@
|
||||||
bottom: 84px;
|
bottom: 84px;
|
||||||
left: 12px;
|
left: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.memos-settings label {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.memos-ticker-track {
|
.memos-ticker-track {
|
||||||
|
|
@ -104,6 +130,19 @@
|
||||||
box-shadow: 0 0 10px color-mix(in srgb, var(--memo-color) 42%, transparent);
|
box-shadow: 0 0 10px color-mix(in srgb, var(--memo-color) 42%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.memos-ticker-item.is-task-warning {
|
||||||
|
color: var(--color-warning, #f6c453);
|
||||||
|
}
|
||||||
|
|
||||||
|
.memos-ticker-item.is-task-warning::before {
|
||||||
|
content: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.memos-ticker-item.is-task-warning .ui-icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.memos-list {
|
.memos-list {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
@use "../design-system/controls";
|
@use "../design-system/controls";
|
||||||
@use "../mixins";
|
@use "../mixins";
|
||||||
|
|
||||||
.module header {
|
.module .module-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
@ -11,26 +11,43 @@
|
||||||
border-bottom: 1px solid var(--color-border);
|
border-bottom: 1px solid var(--color-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.module header > div {
|
.module .module-header-main,
|
||||||
|
.module .module-header-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.module header h2 {
|
.module .module-header-main {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module .module-header-actions {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module .module-header h2 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 17px;
|
font-size: 17px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-space-select {
|
.module-space-select {
|
||||||
width: auto;
|
width: 100%;
|
||||||
max-width: 150px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-content {
|
.module-content {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.module-component-host {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-component-host[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.module.is-collapsed header {
|
.module.is-collapsed header {
|
||||||
border-bottom-color: transparent;
|
border-bottom-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
@ -78,11 +95,14 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-title {
|
.module-title {
|
||||||
min-width: 80px;
|
min-width: 0;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
padding: 4px 6px;
|
padding: 4px 6px;
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
cursor: text;
|
cursor: text;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-title:focus {
|
.module-title:focus {
|
||||||
|
|
@ -92,14 +112,14 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-edit-button,
|
.module-edit-button,
|
||||||
.module-exchange-button,
|
.module-configuration-button,
|
||||||
.module-delete-button,
|
.module-delete-button,
|
||||||
.module-collapse-button {
|
.module-collapse-button {
|
||||||
flex: 0 0 40px;
|
flex: 0 0 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-edit-button,
|
.module-edit-button,
|
||||||
.module-exchange-button,
|
.module-configuration-button,
|
||||||
.module-collapse-button {
|
.module-collapse-button {
|
||||||
color: var(--color-text-secondary);
|
color: var(--color-text-secondary);
|
||||||
}
|
}
|
||||||
|
|
@ -122,10 +142,88 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-edit-button.active,
|
.module-edit-button.active,
|
||||||
.module-exchange-button.active {
|
.module-configuration-button.active {
|
||||||
@include mixins.temporary-toggle-active;
|
@include mixins.temporary-toggle-active;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.module-configuration-panel {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
column-gap: var(--space-4);
|
||||||
|
row-gap: var(--space-2);
|
||||||
|
margin: var(--space-1) var(--space-2) var(--space-2);
|
||||||
|
padding: 10px 12px 12px;
|
||||||
|
border: 1px solid rgba(165, 180, 252, 0.1);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background:
|
||||||
|
linear-gradient(135deg, rgba(139, 92, 246, 0.035), transparent 56%),
|
||||||
|
rgba(7, 10, 24, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-configuration-target {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-configuration-section {
|
||||||
|
display: grid;
|
||||||
|
min-width: 0;
|
||||||
|
align-content: start;
|
||||||
|
gap: var(--space-2);
|
||||||
|
padding: 4px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-configuration-organization {
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 1;
|
||||||
|
padding-right: var(--space-4);
|
||||||
|
border-right: 1px solid rgba(165, 180, 252, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-configuration-data {
|
||||||
|
grid-column: 2;
|
||||||
|
grid-row: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-configuration-panel:not(:has(.module-configuration-organization)) .module-configuration-data {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-configuration-specific {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
padding-top: var(--space-3);
|
||||||
|
border-top: 1px solid rgba(165, 180, 252, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-configuration-section > h3 {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font-size: 0.6875rem;
|
||||||
|
font-weight: 900;
|
||||||
|
letter-spacing: 0.07em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-configuration-section .module-text-exchange-panel,
|
||||||
|
.module-configuration-section .guide-checklist-controls {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-configuration-panel .text-import-actions {
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-configuration-panel .text-import-actions button {
|
||||||
|
min-height: 34px;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-configuration-panel .text-import-actions .ui-icon {
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
.module-icon {
|
.module-icon {
|
||||||
display: grid;
|
display: grid;
|
||||||
width: 34px;
|
width: 34px;
|
||||||
|
|
|
||||||
|
|
@ -29,29 +29,14 @@
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-planner-settings-toggle {
|
|
||||||
grid-template-columns: 16px minmax(0, auto);
|
|
||||||
width: auto;
|
|
||||||
max-width: 100%;
|
|
||||||
padding: 0 12px;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-planner-settings-toggle span {
|
|
||||||
overflow: hidden;
|
|
||||||
font-size: var(--font-size-xs);
|
|
||||||
font-weight: 900;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-planner-clear-completed-button {
|
.task-planner-clear-completed-button {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
min-height: 38px;
|
min-height: 30px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 8px;
|
gap: 6px;
|
||||||
padding: 0 12px;
|
padding: 0 10px;
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -73,6 +58,12 @@
|
||||||
rgba(7, 10, 24, 0.34);
|
rgba(7, 10, 24, 0.34);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.module-configuration-specific .task-planner-settings {
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
|
||||||
.task-planner-settings-actions {
|
.task-planner-settings-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
|
|
@ -108,12 +99,18 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-planner-settings input,
|
.task-planner-settings input {
|
||||||
|
@include controls.field-control(xs);
|
||||||
|
}
|
||||||
|
|
||||||
.task-planner-reset-override input {
|
.task-planner-reset-override input {
|
||||||
@include controls.field-control(sm);
|
@include controls.field-control(sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-planner-settings input[type="time"],
|
.task-planner-settings input[type="time"] {
|
||||||
|
@include controls.time-control(xs);
|
||||||
|
}
|
||||||
|
|
||||||
.task-planner-reset-override input[type="time"] {
|
.task-planner-reset-override input[type="time"] {
|
||||||
@include controls.time-control(sm);
|
@include controls.time-control(sm);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue