From 14f1b79cbcc53381d6fa495a077841deeba60560 Mon Sep 17 00:00:00 2001 From: Shinuwa Date: Thu, 24 Sep 2026 08:26:48 +0200 Subject: [PATCH] Move guide checklist controls into a collapsible header toggle --- DESIGN_SYSTEM.md | 13 +++- docs/FEATURE_CHECKLIST.md | 2 +- docs/STORAGE_SCHEMA.md | 11 ++- e2e/guide-checklist.spec.js | 77 +++++++++++++++---- tests/helpers/data-validation.mjs | 5 +- website/public/data/site.json | 7 +- .../features/games/GuideChecklistControls.jsx | 50 ++++++------ .../src/features/games/useGuideChecklist.js | 20 +++-- .../src/features/toolboxes/ToolboxPages.jsx | 10 ++- .../toolboxes/modules/ModuleDataGate.jsx | 6 +- .../toolboxes/modules/ToolboxModules.jsx | 17 ++-- website/src/styles/games/_checklist.scss | 15 ++-- 12 files changed, 144 insertions(+), 89 deletions(-) diff --git a/DESIGN_SYSTEM.md b/DESIGN_SYSTEM.md index 97f0f45..5ba82b2 100644 --- a/DESIGN_SYSTEM.md +++ b/DESIGN_SYSTEM.md @@ -822,9 +822,16 @@ Date picker et menus compacts : ### Liaison des guides aux checklists -Les guides à cartes partagent un panneau `guide-checklist-controls` dans la toolbox -latérale du jeu, au-dessus de ses outils : panneau NDS, select avec label, bouton de dissociation et switch -« Masquer les cochés ». Le label et le sélecteur occupent toute la largeur ; les actions sont centrées et reviennent à la ligne sur mobile. Le panneau ne concerne que le guide à cartes actuellement ouvert. +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 +« 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 ; +le nombre de correspondances figure dessous. Une liste active devenue incompatible +garde ses commandes avec un message explicite. Aucun sélecteur global n'est affiché. L'état terminé désature et atténue uniquement les portraits (ou les icônes de catégorie Diablo), avec une coche accessible « Coché ». Les textes restent lisibles et les indicateurs absolus ne modifient pas la taille des cartes. diff --git a/docs/FEATURE_CHECKLIST.md b/docs/FEATURE_CHECKLIST.md index 6ce9c40..29be803 100644 --- a/docs/FEATURE_CHECKLIST.md +++ b/docs/FEATURE_CHECKLIST.md @@ -52,7 +52,7 @@ Checklist à suivre lors de l'ajout ou de la modification d'un outil, d'une page - Vérifier le bouton toolbox et le panneau latéral. - Vérifier le breadcrumb. - Vérifier les textes avec retours à la ligne. -- Pour les guides à cartes, vérifier le sélecteur de checklist dans le panneau latéral de la toolbox liée, les listes partielles et la mise à jour immédiate depuis le panneau latéral. Les guides ne doivent jamais modifier la progression. +- Pour les guides à cartes, vérifier le toggle de lien à côté du « + » dans l'en-tête de chaque checklist compatible : il affiche les commandes, masquées par défaut. Vérifier l'association exclusive, le masquage désactivé sur les autres listes et la mise à jour immédiate depuis le panneau latéral. Refermer les commandes conserve l'association. Les guides ne doivent jamais modifier la progression. - Vérifier le changement de checklist, la dissociation, la restauration de session, la suppression d'un module et le changement de toolbox. La réinitialisation des filtres conserve la liaison ; une liste devenue sans correspondance reste sélectionnée avec un message. - Pour Aniimo, vérifier la copie des espèces ou des formes selon l'affichage, les noms français/anglais, la priorité des formes et le masquage avant regroupement. Compteurs et copie doivent correspondre aux résultats affichés. diff --git a/docs/STORAGE_SCHEMA.md b/docs/STORAGE_SCHEMA.md index 02cce4d..eb32165 100644 --- a/docs/STORAGE_SCHEMA.md +++ b/docs/STORAGE_SCHEMA.md @@ -211,9 +211,14 @@ Stockage compact : ### Association temporaire aux guides Les pages Aniimo, monstres MHW, faune MHW et affixes Diablo peuvent refléter une -checklist de la toolbox actuellement associée au jeu. Le sélecteur, la dissociation -et le masquage se trouvent dans le panneau latéral de cette toolbox, au-dessus des outils. Cette liaison ne modifie -ni les données de checklist ni les liens persistants jeu/toolbox. +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 +la liste associée. Associer une autre liste remplace la précédente et conserve le +réglage de masquage ; dissocier réaffiche tous les éléments. Replier les commandes +ne modifie pas l'association ; cet état d'affichage reste local au composant. +Cette liaison ne modifie ni les données de checklist ni les liens persistants jeu/toolbox. `sessionStorage["sokkog:guide-checklists"]` contient un objet indexé par `aniimo/aniimos`, `mhwilds/monsters`, `mhwilds/endemic` ou `diablo4/affixes`. diff --git a/e2e/guide-checklist.spec.js b/e2e/guide-checklist.spec.js index 1dfbf6c..9719491 100644 --- a/e2e/guide-checklist.spec.js +++ b/e2e/guide-checklist.spec.js @@ -7,8 +7,11 @@ import { normalizeChecklistData } from "../website/src/features/toolboxes/storag const route = "/#/games/aniimo/aniimos"; const sessionKey = "sokkog:guide-checklists"; -const selector = (page) => page.getByRole("combobox", { name: "Checklist associée", exact: true }); -const hiddenSwitch = (page) => page.getByRole("button", { name: "Masquer les cochés", exact: true }); +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 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 }); const json = (path) => JSON.parse(readFileSync(`website/public/data/${path}`, "utf8")); const row = (label, qtyCurrent = 0) => ({ id: label, label, qtyCurrent }); @@ -64,19 +67,30 @@ async function openDrawer(page) { 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"]'); + await expect(toggle).toBeVisible(); + if (await toggle.getAttribute("aria-expanded") === "false") await toggle.click(); +} async function choose(page, id) { await openDrawer(page); - await selector(page).selectOption(id); + await openControls(page, id); + await linkButton(page, id).click(); + await expect(linkButton(page, id)).toHaveAttribute("aria-pressed", "true"); + await expect(activeButtons(page)).toHaveCount(1); await closeDrawer(page); } async function toggleHidden(page) { await openDrawer(page); + await openControls(page); await hiddenSwitch(page).click(); await closeDrawer(page); } async function assertLink(page, id, hidden) { await openDrawer(page); - await expect(selector(page)).toHaveValue(id); + await expect(activeButtons(page)).toHaveCount(id ? 1 : 0); + if (id) await openControls(page, id); + 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); await closeDrawer(page); @@ -118,14 +132,35 @@ for (const width of [1280, 390]) { await page.getByRole("searchbox").fill("emberpup"); const card = page.locator(".aniimo-result"); await openDrawer(page); - await expect(selector(page)).toHaveValue(""); - await expect(selector(page).locator("option")).toHaveCount(4); + await expect(activeButtons(page)).toHaveCount(0); + await expect(page.locator(".guide-checklist-controls")).toHaveCount(3); + 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 openControls(page, "capture"); + await expect(controls(page, "capture").getByRole("button", { name: "Masquer les cochés" })).toBeDisabled(); const before = await readModule(page, "forms"); - await selector(page).selectOption("forms"); - const panel = page.locator(".guide-checklist-controls"); + const heading = controlsToggle(page, "forms"); + 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 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(linkButton(page, "forms")).toBeHidden(); + await expect(card).toHaveAttribute("data-checklist-complete", "true"); + await heading.press("Enter"); + await expect(linkButton(page, "forms")).toBeVisible(); const bounds = await panel.boundingBox(); - const selectBounds = await selector(page).boundingBox(); - expect(selectBounds.x + selectBounds.width / 2).toBeCloseTo(bounds.x + bounds.width / 2, 0); + const actionsBounds = await panel.locator(".guide-checklist-actions").boundingBox(); + expect(actionsBounds.x + actionsBounds.width / 2).toBeCloseTo(bounds.x + bounds.width / 2, 0); await page.screenshot({ path: test.info().outputPath("linked-drawer.png") }); await closeDrawer(page); await expect(card).toHaveAttribute("data-checklist-complete", "true"); @@ -146,14 +181,17 @@ for (const width of [1280, 390]) { await page.getByRole("searchbox").fill("emberpup"); await expect(card).toHaveCount(0); await openDrawer(page); + await openControls(page, "capture"); await hiddenSwitch(page).press("Space"); await closeDrawer(page); await expect(card).toHaveAttribute("data-checklist-complete", "true"); await page.getByRole("button", { name: "Réinitialiser les filtres", exact: true }).click(); await openDrawer(page); - await expect(selector(page)).toHaveValue("capture"); - await page.getByRole("button", { name: "Dissocier", exact: true }).click(); - await expect(selector(page)).toHaveValue(""); + await openControls(page, "capture"); + await expect(linkButton(page, "capture")).toHaveAttribute("aria-pressed", "true"); + await linkButton(page, "capture").click(); + await expect(linkButton(page, "capture")).toHaveAttribute("aria-pressed", "false"); + await expect(activeButtons(page)).toHaveCount(0); await expect(hiddenSwitch(page)).toHaveCount(0); await closeDrawer(page); await expect(page.locator('[data-checklist-complete="true"]')).toHaveCount(0); @@ -192,17 +230,18 @@ test("renames retain a zero-match selection; changing toolbox clears the associa await choose(page, "capture"); const drawer = await openDrawer(page); const checklist = drawer.locator('[data-module-type="checklist"]').filter({ has: page.getByRole("button", { name: "Retirer capture", exact: true }) }); + await openControls(page, "capture"); await checklist.getByRole("button", { name: "Modifier Loufeuteau", exact: true }).click(); await checklist.getByRole("textbox", { name: "Libellé", exact: true }).fill("Unrelated now"); await checklist.getByRole("textbox", { name: "Libellé", exact: true }).press("Enter"); - await expect(selector(page)).toHaveValue("capture"); + await expect(linkButton(page, "capture")).toHaveAttribute("aria-pressed", "true"); await expect(page.getByText("Cette checklist ne contient plus de correspondance avec le guide.")).toBeVisible(); await drawer.getByRole("button", { name: "Lier une toolbox", exact: true }).click(); const dialog = page.getByRole("dialog", { name: "Lier une toolbox" }); await dialog.getByRole("combobox").selectOption("other"); await dialog.getByRole("button", { name: "Valider", exact: true }).click(); - await expect(selector(page)).toHaveValue(""); - await expect(selector(page).locator("option")).toHaveCount(2); + await expect(activeButtons(page)).toHaveCount(0); + await expect(page.locator(".guide-checklist-controls")).toHaveCount(1); expect(await page.evaluate((key) => globalThis.sessionStorage.getItem(key), sessionKey)).toBeNull(); }); @@ -214,7 +253,7 @@ test("deleting a selected checklist clears its link immediately", async ({ page const drawer = await openDrawer(page); await drawer.getByRole("button", { name: "Retirer capture", exact: true }).click(); await page.getByRole("dialog").getByRole("button", { name: "Retirer", exact: true }).click(); - await expect(selector(page)).toHaveValue(""); + await expect(activeButtons(page)).toHaveCount(0); await expect(hiddenSwitch(page)).toHaveCount(0); await page.reload(); await assertLink(page, "", false); @@ -240,6 +279,10 @@ test("unavailable session storage retains links across navigation; overview has await openDrawer(page); await expect(page.locator(".guide-checklist-controls")).toHaveCount(0); await closeDrawer(page); + 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 page.goto(route); await assertLink(page, "perfect", true); }); diff --git a/tests/helpers/data-validation.mjs b/tests/helpers/data-validation.mjs index ea35d56..2899047 100644 --- a/tests/helpers/data-validation.mjs +++ b/tests/helpers/data-validation.mjs @@ -90,10 +90,7 @@ export function validateSiteContent(site) { "gamesPage.emptyTitle", "gamesPage.emptyText", "guideChecklist.label", - "guideChecklist.untitled", - "guideChecklist.none", - "guideChecklist.linkToolbox", - "guideChecklist.noCompatible", + "guideChecklist.associate", "guideChecklist.noMatches", "guideChecklist.matches", "guideChecklist.unlink", diff --git a/website/public/data/site.json b/website/public/data/site.json index 65b341a..4132657 100644 --- a/website/public/data/site.json +++ b/website/public/data/site.json @@ -2720,11 +2720,8 @@ "emptyText": "Créez une première toolbox pour stocker vos outils dans ce navigateur." }, "guideChecklist": { - "label": "Checklist associée", - "untitled": "Checklist sans titre", - "none": "Aucune checklist", - "linkToolbox": "Associer une toolbox pour suivre une checklist", - "noCompatible": "Aucune checklist de cette toolbox ne correspond aux éléments du guide.", + "label": "Lien avec le guide", + "associate": "Associer à la page", "noMatches": "Cette checklist ne contient plus de correspondance avec le guide.", "matches": "correspondances", "unlink": "Dissocier", diff --git a/website/src/features/games/GuideChecklistControls.jsx b/website/src/features/games/GuideChecklistControls.jsx index 3ed4ec1..49cd32d 100644 --- a/website/src/features/games/GuideChecklistControls.jsx +++ b/website/src/features/games/GuideChecklistControls.jsx @@ -1,32 +1,32 @@ -// Rôle : affiche le sélecteur de checklist et le masquage commun aux pages guides. +// Rôle : expose le suivi du guide dans chaque checklist compatible et marque les cartes terminées. import { Icon } from "../../components/Icon.jsx"; -import { useGuideChecklist } from "./useGuideChecklist.js"; -export function CurrentGuideChecklistControls({ gameId }) { - const checklist = useGuideChecklist(); - return checklist.available && checklist.guideId.split("/")[0] === gameId ? : null; +export function GuideChecklistToggle({ checklist, panel, label }) { + if (!panel.list) return null; + return ; } -export function GuideChecklistControls({ checklist }) { - const { content, toolbox, selected, lists, hideCompleted } = checklist; - return
- {!toolbox ? : <> - - {!lists.length &&

{content.noCompatible}

} - {selected && <> -
- -