diff --git a/.beads/interactions.jsonl b/.beads/interactions.jsonl index 043468f..1520ef1 100644 --- a/.beads/interactions.jsonl +++ b/.beads/interactions.jsonl @@ -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-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-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."}} diff --git a/DESIGN_SYSTEM.md b/DESIGN_SYSTEM.md index e24a47f..74635ef 100644 --- a/DESIGN_SYSTEM.md +++ b/DESIGN_SYSTEM.md @@ -200,7 +200,12 @@ Style courant : - hover violet/violet clair moins intense que les cards ; - pas de mouvement au hover ; - 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. @@ -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 ; - les vues hebdomadaire et mensuelle affichent un indicateur discret du moment actuel. - en vue hebdomadaire, un switch standard peut masquer les longues plages horaires vides ; la tranche masquée reste indiquée dans la grille, pas dans un texte qui déborde de la colonne horaire. -- le calendrier supporte l'import/export texte via le 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 @@ -383,7 +388,7 @@ Regles UI : - 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 ; - 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. ### Calque De Dessin @@ -871,11 +876,10 @@ Date picker et menus compacts : ### Liaison des guides aux checklists -Dans la toolbox latérale, chaque checklist compatible avec le guide ouvert affiche -un bouton icône de lien à côté du « + » dans son en-tête. Ce toggle NDS affiche -ou masque la section `guide-checklist-controls`, masquée par défaut, sans désactiver -le suivi. Il expose `aria-expanded`, `aria-pressed` et `aria-controls` ; son icône -reste dorée lorsque la liste est associée. Le bouton NDS +Dans la toolbox latérale, chaque checklist compatible avec le guide ouvert expose +sa liaison dans la section Guide du panneau Configuration commun. L'engrenage +conserve un marqueur d'état lorsque la liste est associée et expose `aria-expanded` +et `aria-controls`. Le bouton NDS « 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 non associées. Les commandes sont centrées et reviennent à la ligne sur mobile ; diff --git a/docs/FEATURE_CHECKLIST.md b/docs/FEATURE_CHECKLIST.md index 0df2abe..e40af7c 100644 --- a/docs/FEATURE_CHECKLIST.md +++ b/docs/FEATURE_CHECKLIST.md @@ -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. - 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. -- 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. - Vérifier l'affichage dans la page toolbox complète. - 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 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. @@ -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/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. -- 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. - 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. diff --git a/docs/STORAGE_SCHEMA.md b/docs/STORAGE_SCHEMA.md index 7308e13..4a36a4a 100644 --- a/docs/STORAGE_SCHEMA.md +++ b/docs/STORAGE_SCHEMA.md @@ -237,6 +237,7 @@ Type : `memos` ```json { "tickerEnabled": true, + "plannerLink": "mod-task-planner", "items": [ { "id": "memo1", @@ -252,6 +253,8 @@ Stockage compact : - `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é ; +- `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 ; - 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. @@ -262,9 +265,8 @@ Stockage compact : 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 -affiche un toggle de lien à côté du « + » de son en-tête dans le panneau latéral. -Il ouvre une section masquée par défaut en haut du contenu : -bouton toggle d'association/dissociation et switch de masquage, actif seulement pour +affiche sa liaison dans la section Guide du panneau Configuration commun : +bouton 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 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 diff --git a/e2e/guide-checklist.spec.js b/e2e/guide-checklist.spec.js index d3ca153..318bd6a 100644 --- a/e2e/guide-checklist.spec.js +++ b/e2e/guide-checklist.spec.js @@ -8,7 +8,7 @@ import { normalizeChecklistData } from "../website/src/features/toolboxes/storag const route = "/#/games/aniimo/aniimos"; 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 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 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 }); @@ -97,7 +97,7 @@ async function closeDrawer(page) { await page.locator("#toolbox-drawer").getByRole("button", { name: "Fermer", exact: true }).click(); } 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(); if (await toggle.getAttribute("aria-expanded") === "false") await toggle.click(); } @@ -117,8 +117,8 @@ async function toggleHidden(page) { } async function assertLink(page, id, hidden) { await openDrawer(page); - await expect(activeButtons(page)).toHaveCount(id ? 1 : 0); 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(hiddenSwitch(page)).toHaveAttribute("aria-pressed", String(hidden)); 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 openDrawer(page); 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(controls(page, "unrelated")).toHaveCount(0); - await expect(controlsToggle(page, "unrelated")).toHaveCount(0); - await expect(controls(page, "capture")).toBeHidden(); + await expect(controlsToggle(page, "unrelated")).toHaveCount(1); + await expect(controls(page, "capture")).toHaveCount(0); await openControls(page, "capture"); await expect(controls(page, "capture").getByRole("button", { name: "Masquer les cochés" })).toBeDisabled(); const before = await readModule(page, "forms"); @@ -218,15 +218,14 @@ for (const width of [1280, 390]) { await expect(heading).toHaveAttribute("aria-expanded", "false"); expect(await heading.evaluate((button) => button.previousElementSibling.classList.contains("module-edit-button"))).toBe(true); await heading.press("Enter"); - await expect(heading).toHaveAttribute("aria-pressed", "true"); - await expect(controls(page, "forms")).toHaveAttribute("id", await heading.getAttribute("aria-controls")); + await expect(heading).toHaveAttribute("aria-expanded", "true"); + await expect(page.locator(`[id="${await heading.getAttribute("aria-controls")}"]`)).toHaveClass(/module-configuration-panel/); await linkButton(page, "forms").press("Enter"); await expect(linkButton(page, "forms")).toHaveAttribute("aria-pressed", "true"); const panel = controls(page, "forms"); await heading.press("Space"); await expect(heading).toHaveAttribute("aria-expanded", "false"); - await expect(heading).toHaveAttribute("aria-pressed", "false"); - await expect(heading).toHaveAttribute("data-linked", "true"); + await expect(heading).toHaveAttribute("data-guide-linked", "true"); await expect(linkButton(page, "forms")).toBeHidden(); await expect(card).toHaveAttribute("data-checklist-complete", "true"); 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("button", { name: "Valider", exact: true }).click(); 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(); }); @@ -398,7 +397,7 @@ test("unavailable local storage retains links in memory across navigation; overv await page.goto("/#/toolbox/guide"); await expect(page.locator('[data-module-type="checklist"]')).toHaveCount(4); 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 assertLink(page, "perfect", true); await openDrawer(page); diff --git a/e2e/memos-ticker.spec.js b/e2e/memos-ticker.spec.js index 58ae37c..766cdf2 100644 --- a/e2e/memos-ticker.spec.js +++ b/e2e/memos-ticker.spec.js @@ -10,14 +10,41 @@ async function seedMemosToolbox(page) { request.onsuccess = () => { const db = request.result; 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([ - { 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"); tx.objectStore("kv").put({ mhwilds: "memo-toolbox" }, "links"); tx.objectStore("modules").put({ key: "memo-toolbox:memos", 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.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 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(""); +}); diff --git a/e2e/task-planner.spec.js b/e2e/task-planner.spec.js index 1bb3e27..3152254 100644 --- a/e2e/task-planner.spec.js +++ b/e2e/task-planner.spec.js @@ -97,7 +97,7 @@ test("recurring resets preserve unique completions and cleanup deletes only comp await expect(hide).toHaveAttribute("aria-pressed", "true"); await hide.click(); 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 }); await cleanup.click(); await expect(cleanup).toBeDisabled(); diff --git a/e2e/text-exchange.spec.js b/e2e/text-exchange.spec.js index b2c4f05..d9e03ae 100644 --- a/e2e/text-exchange.spec.js +++ b/e2e/text-exchange.spec.js @@ -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 importButton = table.getByRole("button", { name: "Importer TSV", exact: true }); 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(); const originalOverflow = await page.locator("body").evaluate((body) => body.style.overflow); 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 ligne 1", exact: true })).toHaveValue("Ore"); 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(); const exported = page.getByRole("dialog", { name: "Exporter un tableau TSV", exact: true }); 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 expect(exported).toBeHidden(); 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(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 }) => { await setup(page); 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 }); await openImport.click(); 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 page.reload(); 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 expect(dialog.getByRole("textbox")).toHaveValue(""); 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 expect(calc.locator(".calculator-entry em")).toHaveText(["Ore", "Bars"]); 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(); const exported = page.getByRole("dialog", { name: "Exporter des résultats", exact: true }); await expect(exported.getByRole("textbox")).toHaveValue(text); diff --git a/e2e/toolbox-spaces.spec.js b/e2e/toolbox-spaces.spec.js index 0f6b570..7395c3c 100644 --- a/e2e/toolbox-spaces.spec.js +++ b/e2e/toolbox-spaces.spec.js @@ -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 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 page.getByRole("tab", { name: "Divers" }).click(); 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"); }); +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 }) => { await seedToolbox(page, { id: "reorder-spaces", diff --git a/tests/helpers/data-validation.mjs b/tests/helpers/data-validation.mjs index ac99302..7476926 100644 --- a/tests/helpers/data-validation.mjs +++ b/tests/helpers/data-validation.mjs @@ -248,6 +248,14 @@ export function validateSiteContent(site) { "toolboxes.spaces.moveSuccess", "toolboxes.spaces.emptyText", "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.createLabel", "toolboxes.drawer.linkExistingTitle", @@ -345,6 +353,12 @@ export function validateSiteContent(site) { "toolboxes.modules.memos.tickerLabel", "toolboxes.modules.memos.hideTickerTitle", "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.importOpenButton", "toolboxes.modules.memos.exportOpenButton", @@ -830,7 +844,6 @@ export function validateSiteContent(site) { "toolboxes.modules.calendar.requiredError", "toolboxes.modules.calendar.invalidRangeError", "toolboxes.modules.taskPlanner.settingsTitle", - "toolboxes.modules.taskPlanner.settingsButtonLabel", "toolboxes.modules.taskPlanner.hideCompletedTitle", "toolboxes.modules.taskPlanner.globalWeeklyResetDayLabel", "toolboxes.modules.taskPlanner.resetTimeLabel", diff --git a/tests/static-toolboxes.test.mjs b/tests/static-toolboxes.test.mjs index c9958c7..de01a90 100644 --- a/tests/static-toolboxes.test.mjs +++ b/tests/static-toolboxes.test.mjs @@ -4,6 +4,7 @@ import { test } from "node:test"; import assert from "node:assert/strict"; 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 { 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 { 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"; @@ -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 toolboxModules = await readFile("website/src/features/toolboxes/modules/ToolboxModules.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 notepadModule = await readFile("website/src/features/toolboxes/modules/NotepadModule.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 taskPlannerTopbar = await readFile("website/src/features/toolboxes/modules/TaskPlannerTopbar.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 goalTreeEditor = await readFile("website/src/features/toolboxes/modules/GoalTreeEditor.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-collapse-button/); 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, /CompactDropdown/); assert.doesNotMatch(source, / save({ ...data, plannerLink: event.target.value })} + aria-label={textContent.plannerLinkLabel || "Planificateur lié"} + > + + {plannerModules.length > 0 && } + {plannerModules.map((planner) => ( + + ))} + + + + +
+

{context.moduleConfigurationText?.dataTitle || "Données"}

+ save({ ...data, items: nextData.items })} /> +
+ {editing && (
diff --git a/website/src/features/toolboxes/modules/MemosTicker.jsx b/website/src/features/toolboxes/modules/MemosTicker.jsx index 04b235c..040cfc0 100644 --- a/website/src/features/toolboxes/modules/MemosTicker.jsx +++ b/website/src/features/toolboxes/modules/MemosTicker.jsx @@ -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 = "" }) { if (!items?.length) return null; return (
{[...items, ...items].map((memo, index) => ( - + + {memo.kind === "task-warning" && } {memo.text} ))} diff --git a/website/src/features/toolboxes/modules/ModuleConfiguration.jsx b/website/src/features/toolboxes/modules/ModuleConfiguration.jsx new file mode 100644 index 0000000..656a062 --- /dev/null +++ b/website/src/features/toolboxes/modules/ModuleConfiguration.jsx @@ -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 ( + +
+

{title}

+ +
+
+ ); +} diff --git a/website/src/features/toolboxes/modules/ModuleShell.jsx b/website/src/features/toolboxes/modules/ModuleShell.jsx index e0b6c8b..89e15d6 100644 --- a/website/src/features/toolboxes/modules/ModuleShell.jsx +++ b/website/src/features/toolboxes/modules/ModuleShell.jsx @@ -1,23 +1,26 @@ // 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 { 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 { ModuleDataGate, ModuleLoadingFallback } from "./ModuleDataGate.jsx"; import { supportsTextExchange } from "./textImport.js"; 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 Component = definition.Component; const label = definition.label || module.type; 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 guidePanel = useGuideChecklistPanel(context.guideChecklist, module); const hasAddPanel = definition.editable && definition.addable !== false; const hasTextExchange = supportsTextExchange(module.type); + const hasConfiguration = spaces.length > 1 || Boolean(guidePanel.list) || hasTextExchange; const scrollable = definition.scrollable && module.scrollable === true; const collapsed = module.collapsed === true; const className = [ @@ -35,10 +38,10 @@ export function ModuleShell({ toolbox, module, context, reorder, itemGroup = "", editing, setEditing, openEditor: () => { - setTextExchangeOpen(false); + setConfigurationOpen(false); setEditing(true); }, - textExchangeOpen + configurationTarget }; useEffect(() => { @@ -50,10 +53,19 @@ export function ModuleShell({ toolbox, module, context, reorder, itemGroup = "", return () => window.cancelAnimationFrame(frame); }, [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() { if (!collapsed) { setEditing(false); - setTextExchangeOpen(false); + setConfigurationOpen(false); } onUpdateModule(module.id, (currentModule) => { const nextModule = { ...currentModule }; @@ -74,8 +86,8 @@ export function ModuleShell({ toolbox, module, context, reorder, itemGroup = "", data-module-id={module.id} {...reorder.getItemProps({ itemId: module.id, groupId: itemGroup, parentId: toolbox.id })} > -
-
+
+
-
- {spaces.length > 1 && ( - - )} +
{definition.scrollable && ( )} - - {hasTextExchange && ( + {hasConfiguration && ( )}
- {!collapsed && ( -
- - }> - - - - + {configurationOpen && ( +
+ {spaces.length > 1 && ( +
+

{configurationText.organizationTitle || "Espace"}

+ +
+ )} + {guidePanel.list && ( +
+

{configurationText.guideTitle || "Guide"}

+ +
+ )} +
+
+ )} + {(!collapsed || configurationOpen) && ( + )} diff --git a/website/src/features/toolboxes/modules/NotepadModule.jsx b/website/src/features/toolboxes/modules/NotepadModule.jsx index a58e371..3002d11 100644 --- a/website/src/features/toolboxes/modules/NotepadModule.jsx +++ b/website/src/features/toolboxes/modules/NotepadModule.jsx @@ -5,7 +5,7 @@ import { Icon } from "../../../components/Icon.jsx"; import { DrawingControls } from "./DrawingControls.jsx"; import { DrawingOverlay } from "./DrawingOverlay.jsx"; import { NotepadToolbar } from "./NotepadToolbar.jsx"; -import { TextExchangeActions } from "./TextExchangeActions.jsx"; +import { ModuleTextExchangeConfiguration } from "./ModuleConfiguration.jsx"; import { normalizeNotepadData } from "../storage/toolboxStorage.js"; import { useKeyedState } from "../../../hooks/useKeyedState.js"; 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 normalizedData = useMemo(() => normalizeNotepadData(storedData), [storedData]); const notepadDataRef = useRef(normalizedData); @@ -295,11 +295,7 @@ export function NotepadModule({ toolboxId, moduleId, context, textExchangeOpen } return (
- {textExchangeOpen && ( -
- context.setModuleData(toolboxId, moduleId, nextData, "notepad")} /> -
- )} + context.setModuleData(toolboxId, moduleId, nextData, "notepad")} /> - {textExchangeOpen && ( -
- context.setModuleData(toolboxId, moduleId, nextData, "randomizer")} /> -
- )} + context.setModuleData(toolboxId, moduleId, nextData, "randomizer")} />
({ ...tab, label: textContent[`${tab.id}Tab`] || tab.label }))} diff --git a/website/src/features/toolboxes/modules/RngSimulatorModule.jsx b/website/src/features/toolboxes/modules/RngSimulatorModule.jsx index 045732e..0eaefb1 100644 --- a/website/src/features/toolboxes/modules/RngSimulatorModule.jsx +++ b/website/src/features/toolboxes/modules/RngSimulatorModule.jsx @@ -4,7 +4,7 @@ import { Icon } from "../../../components/Icon.jsx"; import { Tabs } from "../../../components/Tabs.jsx"; import { useDraftForm } from "../../../hooks/useDraftForm.js"; import { useKeyedState } from "../../../hooks/useKeyedState.js"; -import { TextExchangeActions } from "./TextExchangeActions.jsx"; +import { ModuleTextExchangeConfiguration } from "./ModuleConfiguration.jsx"; import { RNG_SIMULATOR_DEFAULT_RATE, RNG_SIMULATOR_HOURLY_DURATION_PRESETS, @@ -146,7 +146,7 @@ function getToneFromDelta(value) { 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 data = useMemo(() => context.normalizeRngSimulatorData(rawData), [context, rawData]); const textContent = context.moduleText?.rngSimulator || {}; @@ -405,11 +405,7 @@ export function RngSimulatorModule({ toolboxId, moduleId, context, editing = fal return ( <> - {textExchangeOpen && ( -
- context.setModuleData(toolboxId, moduleId, nextData, "rngSimulator")} /> -
- )} + context.setModuleData(toolboxId, moduleId, nextData, "rngSimulator")} />
diff --git a/website/src/features/toolboxes/modules/TableModule.jsx b/website/src/features/toolboxes/modules/TableModule.jsx index 3e5a062..8db83ca 100644 --- a/website/src/features/toolboxes/modules/TableModule.jsx +++ b/website/src/features/toolboxes/modules/TableModule.jsx @@ -1,6 +1,6 @@ // Rôle : fournit l'outil tableau avec cellules libres, formules simples et échange TSV. import { useMemo } from "react"; -import { TextExchangeActions } from "./TextExchangeActions.jsx"; +import { ModuleTextExchangeConfiguration } from "./ModuleConfiguration.jsx"; import { columnIndexToName, evaluateTable } from "./tableFormulaEngine.js"; import { TableGrid } from "./TableGrid.jsx"; import { TableToolbar } from "./TableToolbar.jsx"; @@ -14,7 +14,7 @@ import { } from "./tableUtils.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 data = useMemo(() => context.normalizeTableData(rawData), [context, rawData]); const textContent = context.moduleText?.table || {}; @@ -61,11 +61,7 @@ export function TableModule({ toolboxId, moduleId, context, textExchangeOpen }) return (
- {textExchangeOpen && ( -
- context.setModuleData(toolboxId, moduleId, nextData, "table")} /> -
- )} + context.setModuleData(toolboxId, moduleId, nextData, "table")} /> task.category).filter(Boolean).filter((category) => !data.categoryOrder.includes(category)) ], [data.categoryOrder, data.tasks]); const [nowMs, setNowMs] = useState(() => Date.now()); - const [settingsOpen, setSettingsOpen] = useState(false); const [openDescriptions, setOpenDescriptions] = useState(() => new Set()); const [openTaskSettings, setOpenTaskSettings] = useState(() => new Set()); const [prerequisiteNoticeTaskIds, setPrerequisiteNoticeTaskIds] = useState(() => new Set()); @@ -170,31 +171,32 @@ export function TaskPlannerModule({ toolboxId, moduleId, context, editing, textE return (
+ +
+

{textContent.settingsTitle || "Réglages du planificateur"}

+ save({ ...data, weeklyResetDay })} + onResetTimeChange={(resetTime) => save({ ...data, resetTime })} + onClearCompletedUniqueTasks={clearCompletedUniqueTasks} + /> +
+
+

{context.moduleConfigurationText?.dataTitle || "Données"}

+ save(nextData)} /> +
+
save({ ...data, hideCompleted: !data.hideCompleted })} - onSettingsToggle={() => setSettingsOpen((value) => !value)} - onImport={(nextData) => save(nextData)} /> - {settingsOpen && ( - save({ ...data, weeklyResetDay })} - onResetTimeChange={(resetTime) => save({ ...data, resetTime })} - onClearCompletedUniqueTasks={clearCompletedUniqueTasks} - /> - )} -
    {data.tasks.length ? (