diff --git a/DESIGN_SYSTEM.md b/DESIGN_SYSTEM.md
index 3b753a1..c8c18bd 100644
--- a/DESIGN_SYSTEM.md
+++ b/DESIGN_SYSTEM.md
@@ -370,7 +370,8 @@ 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.
+- 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é ;
+- la bande fixe laisse le contenu verticalement accessible, suit la largeur disponible et remonte au-dessus de la navigation mobile.
### Calque De Dessin
@@ -859,10 +860,13 @@ reste dorée lorsque la liste est associée. Le bouton NDS
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é.
-Quand une liste est associée, un encart compact placé en haut à droite du header
-du guide expose directement le switch « Masquer les cochés » et l'action
-« Dissocier la checklist ». La colonne d'actions occupe la hauteur existante du
+garde ses commandes avec un message explicite.
+Un encart compact placé en haut à droite du header du guide expose un sélecteur dès
+qu'au moins une checklist compatible existe. Sans association, lui seul est affiché.
+Une fois une liste choisie, l'encart ajoute le switch « Masquer les cochés » et
+l'action « Dissocier la checklist ». Changer la
+valeur du sélecteur remplace immédiatement l'association sans perdre le réglage de
+masquage. La colonne d'actions occupe la hauteur existante du
header afin que cet encart n'ajoute pas de ligne ou d'espace vertical. Cette
association est conservée dans `localStorage` entre les sessions ; elle est retirée
si la checklist, la toolbox liée ou la liaison jeu/toolbox disparaît.
diff --git a/docs/STORAGE_SCHEMA.md b/docs/STORAGE_SCHEMA.md
index 2bd3885..af74158 100644
--- a/docs/STORAGE_SCHEMA.md
+++ b/docs/STORAGE_SCHEMA.md
@@ -237,6 +237,7 @@ Type : `memos`
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é ;
- `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.
@@ -251,7 +252,9 @@ affiche un toggle de lien à côté du « + » de son en-tête dans le panneau l
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
+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
+de l'encart restent masquées. 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.
diff --git a/docs/TEST_STRATEGY.md b/docs/TEST_STRATEGY.md
index ea4ef51..dd417c4 100644
--- a/docs/TEST_STRATEGY.md
+++ b/docs/TEST_STRATEGY.md
@@ -24,6 +24,7 @@ not bugs, missing scenarios or a percentage of work remaining.
| Checklist: quantities, clamping, completed item/category visibility and persistence | `e2e/basic-tools.spec.js` |
| Images: gallery, detail navigation, keyboard controls and focus restoration | `e2e/basic-tools.spec.js` |
| Guide checklists: persistent association, page controls, drawer scope and live completion | `e2e/guide-checklist.spec.js` |
+| Mémos : bande fixe, portée par route et persistance du toggle | `e2e/memos-ticker.spec.js` |
| Planner: editing, recurrence, prerequisites, subtree reorder and persistence | `e2e/task-planner.spec.js` |
| Representative visual states and responsive layouts | `e2e/style-layouts.spec.js`, `tool-style-states.spec.js`, `card-surfaces.spec.js`, `nds-controls.spec.js`, `nds-patterns.spec.js` |
| Production onboarding | `e2e/production/onboarding.spec.js` |
diff --git a/e2e/guide-checklist.spec.js b/e2e/guide-checklist.spec.js
index 8102a31..3686fd5 100644
--- a/e2e/guide-checklist.spec.js
+++ b/e2e/guide-checklist.spec.js
@@ -132,6 +132,12 @@ for (const width of [1280, 390]) {
await page.goto(route);
await page.getByRole("searchbox").fill("emberpup");
const card = page.locator(".aniimo-result");
+ const pageControls = page.locator(".guide-checklist-page-controls");
+ const unlinkedSelect = pageControls.getByRole("combobox", { name: "Checklist associée", exact: true });
+ await expect(pageControls).toBeVisible();
+ await expect(unlinkedSelect).toHaveValue("");
+ await expect(unlinkedSelect).toContainText("Choisir une checklist");
+ 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);
@@ -164,7 +170,6 @@ for (const width of [1280, 390]) {
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);
- const pageControls = page.locator(".guide-checklist-page-controls");
await expect(pageControls).toBeVisible();
const pageControlsBounds = await pageControls.boundingBox();
const pageHeadingBounds = await page.locator(".page-heading").boundingBox();
@@ -175,6 +180,14 @@ for (const width of [1280, 390]) {
expect(headingActionsBounds.height).toBeCloseTo(pageHeadingBounds.height, 0);
expect(pageControlsBounds.y).toBeCloseTo(pageHeadingBounds.y, 0);
}
+ const checklistSelect = pageControls.getByRole("combobox", { name: "Checklist associée", exact: true });
+ await expect(checklistSelect).toHaveValue("forms");
+ await expect(checklistSelect.locator("option")).toHaveCount(3);
+ await expect(checklistSelect.locator('option[value="unrelated"]')).toHaveCount(0);
+ await checklistSelect.selectOption("capture");
+ await expect(checklistSelect).toHaveValue("capture");
+ await checklistSelect.selectOption("forms");
+ await expect(checklistSelect).toHaveValue("forms");
await expect(pageControls.getByRole("button", { name: "Masquer les cochés", exact: true })).toHaveAttribute("aria-pressed", "false");
await expect(pageControls.getByRole("button", { name: "Dissocier la checklist", exact: true })).toBeVisible();
await expect(card).toHaveAttribute("data-checklist-complete", "true");
diff --git a/e2e/memos-ticker.spec.js b/e2e/memos-ticker.spec.js
new file mode 100644
index 0000000..58ae37c
--- /dev/null
+++ b/e2e/memos-ticker.spec.js
@@ -0,0 +1,56 @@
+// Rôle : vérifie la bande de mémos fixe sur les pages de toolbox et de jeu associées.
+import { test, expect } from "@playwright/test";
+import { resetSokkoStorage } from "./helpers/storage.js";
+
+async function seedMemosToolbox(page) {
+ await resetSokkoStorage(page);
+ await page.evaluate(async () => {
+ await new Promise((resolve, reject) => {
+ const request = globalThis.indexedDB.open("sokkog", 1);
+ request.onsuccess = () => {
+ const db = request.result;
+ const tx = db.transaction(["kv", "modules"], "readwrite");
+ tx.objectStore("kv").put([
+ { id: "memo-toolbox", name: "Toolbox mémos", modules: [{ id: "memos", type: "memos", title: "Mémos" }], 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.oncomplete = () => { db.close(); resolve(); };
+ tx.onerror = () => reject(tx.error);
+ };
+ request.onerror = () => reject(request.error);
+ });
+ });
+ await page.reload();
+}
+
+test("shows and persists the memo ticker on linked game and full toolbox pages", async ({ page }) => {
+ await seedMemosToolbox(page);
+
+ await page.goto("/#/games/mhwilds/monsters");
+ const globalTicker = page.locator(".global-memos-ticker");
+ await expect(globalTicker).toBeVisible();
+ await expect(globalTicker).toContainText("Préparer les potions");
+ await expect(globalTicker).toHaveCSS("position", "fixed");
+
+ await page.goto("/#/games/aniimo/aniimos");
+ await expect(globalTicker).toHaveCount(0);
+
+ await page.goto("/#/toolbox/memo-toolbox");
+ await expect(globalTicker).toBeVisible();
+ const toggle = page.locator(".memos-ticker-toggle");
+ await expect(toggle).toHaveAccessibleName("Masquer la bande");
+ await toggle.click();
+ await expect(globalTicker).toHaveCount(0);
+ await page.reload();
+ await expect(globalTicker).toHaveCount(0);
+ await expect(toggle).toHaveAccessibleName("Afficher la bande");
+ await toggle.click();
+ await expect(globalTicker).toBeVisible();
+
+ await page.goto("/#/toolboxes");
+ await expect(globalTicker).toHaveCount(0);
+});
diff --git a/tests/helpers/data-validation.mjs b/tests/helpers/data-validation.mjs
index 4dc22b1..458f9fd 100644
--- a/tests/helpers/data-validation.mjs
+++ b/tests/helpers/data-validation.mjs
@@ -90,6 +90,8 @@ export function validateSiteContent(site) {
"gamesPage.emptyTitle",
"gamesPage.emptyText",
"guideChecklist.label",
+ "guideChecklist.selectLabel",
+ "guideChecklist.selectPlaceholder",
"guideChecklist.associate",
"guideChecklist.noMatches",
"guideChecklist.matches",
diff --git a/website/public/data/site.json b/website/public/data/site.json
index 6bbef3a..9a97bab 100644
--- a/website/public/data/site.json
+++ b/website/public/data/site.json
@@ -2771,6 +2771,8 @@
},
"guideChecklist": {
"label": "Lien avec le guide",
+ "selectLabel": "Checklist associée",
+ "selectPlaceholder": "Choisir une checklist",
"associate": "Associer à la page",
"noMatches": "Cette checklist ne contient plus de correspondance avec le guide.",
"matches": "correspondances",
diff --git a/website/src/features/games/GuideChecklistControls.jsx b/website/src/features/games/GuideChecklistControls.jsx
index 901b7a2..ec5ac4a 100644
--- a/website/src/features/games/GuideChecklistControls.jsx
+++ b/website/src/features/games/GuideChecklistControls.jsx
@@ -31,16 +31,27 @@ export function GuideChecklistControls({ checklist, list, id, open }) {
}
export function GuideChecklistPageControls({ checklist }) {
- if (!checklist.selected) return null;
+ if (!checklist.lists.length) return null;
const { content, hideCompleted } = checklist;
return (
);
}
diff --git a/website/src/features/games/useGuideChecklist.js b/website/src/features/games/useGuideChecklist.js
index 16374b5..a8b1eb5 100644
--- a/website/src/features/games/useGuideChecklist.js
+++ b/website/src/features/games/useGuideChecklist.js
@@ -21,6 +21,7 @@ export function useGuideChecklist(requestedGuideId) {
const association = guideChecklistStorage.associations[guideId];
const lists = useMemo(() => (toolbox?.modules || []).filter((module) => module.type === "checklist").map((module) => ({
id: module.id,
+ title: module.title || "Checklist",
...matchGuideChecklist(entries || [], moduleData[`${toolbox.id}:${module.id}`])
})), [entries, toolbox, moduleData]);
const selected = lists.find((list) => list.id === association?.moduleId);
diff --git a/website/src/features/toolboxes/GlobalMemosTicker.jsx b/website/src/features/toolboxes/GlobalMemosTicker.jsx
new file mode 100644
index 0000000..fa79d41
--- /dev/null
+++ b/website/src/features/toolboxes/GlobalMemosTicker.jsx
@@ -0,0 +1,33 @@
+// Rôle : affiche en bas de page les mémos actifs de la toolbox courante ou liée au jeu.
+import { useMemo } from "react";
+import { moduleStorageKey, normalizeMemosData } from "./storage/toolboxStorage.js";
+import { MemosTicker } from "./modules/MemosTicker.jsx";
+
+function getRouteToolboxId(route, links) {
+ const toolboxMatch = route.match(/^\/toolbox\/([^/]+)/);
+ if (toolboxMatch) return decodeURIComponent(toolboxMatch[1]);
+ const gameMatch = route.match(/^\/games\/([^/]+)/);
+ return gameMatch ? links[gameMatch[1]] || "" : "";
+}
+
+export function GlobalMemosTicker({ route, toolboxes, links, moduleData, textContent }) {
+ const items = useMemo(() => {
+ const toolboxId = getRouteToolboxId(route, links);
+ const toolbox = toolboxes.find((item) => item.id === toolboxId);
+ if (!toolbox) return [];
+ return toolbox.modules
+ .filter((module) => module.type === "memos")
+ .flatMap((module) => {
+ const data = normalizeMemosData(moduleData[moduleStorageKey(toolbox.id, module.id)]);
+ if (!data.tickerEnabled) return [];
+ return data.items.map((memo) => ({ ...memo, id: `${module.id}:${memo.id}` }));
+ });
+ }, [route, toolboxes, links, moduleData]);
+
+ if (!items.length) return null;
+ return (
+
+ );
+}
diff --git a/website/src/features/toolboxes/modules/MemosModule.jsx b/website/src/features/toolboxes/modules/MemosModule.jsx
index d2ecbcd..f72b658 100644
--- a/website/src/features/toolboxes/modules/MemosModule.jsx
+++ b/website/src/features/toolboxes/modules/MemosModule.jsx
@@ -4,6 +4,7 @@ import { Icon } from "../../../components/Icon.jsx";
import { moveItem, useGroupedReorder } from "../../../hooks/useGroupedReorder.js";
import { useMemo } from "react";
import { MemoTextEditor } from "./MemoTextEditor.jsx";
+import { MemosTicker } from "./MemosTicker.jsx";
import { TextExchangeActions } from "./TextExchangeActions.jsx";
import { TEXT_COLORS } from "./notepadUtils.js";
@@ -92,21 +93,12 @@ export function MemosModule({ toolboxId, moduleId, context, editing, textExchang
type="button"
onClick={() => save({ ...data, tickerEnabled: !data.tickerEnabled })}
aria-pressed={data.tickerEnabled}
+ aria-label={data.tickerEnabled ? textContent.hideTickerTitle || "Masquer la bande" : textContent.showTickerTitle || "Afficher la bande"}
title={data.tickerEnabled ? textContent.hideTickerTitle || "Masquer la bande" : textContent.showTickerTitle || "Afficher la bande"}
>