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 ?
{content.noCompatible}
} - {selected && <> -{content.noMatches}
} - >} - >} +export function GuideChecklistControls({ checklist, list, id, open }) { + if (!list) return null; + const { content, selected, hideCompleted } = checklist; + const active = selected?.id === list.id; + return{list.count ? `${list.count} ${content.matches}` : content.noMatches}
Aucune toolbox associée à cette page jeu.