Move guide checklist controls into a collapsible header toggle
All checks were successful
Deploy Sokko G / deploy (push) Successful in 18s
All checks were successful
Deploy Sokko G / deploy (push) Successful in 18s
This commit is contained in:
parent
ef0c592c11
commit
14f1b79cbc
12 changed files with 144 additions and 89 deletions
|
|
@ -822,9 +822,16 @@ Date picker et menus compacts :
|
||||||
|
|
||||||
### Liaison des guides aux checklists
|
### Liaison des guides aux checklists
|
||||||
|
|
||||||
Les guides à cartes partagent un panneau `guide-checklist-controls` dans la toolbox
|
Dans la toolbox latérale, chaque checklist compatible avec le guide ouvert affiche
|
||||||
latérale du jeu, au-dessus de ses outils : panneau NDS, select avec label, bouton de dissociation et switch
|
un bouton icône de lien à côté du « + » dans son en-tête. Ce toggle NDS affiche
|
||||||
« 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.
|
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
|
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
|
catégorie Diablo), avec une coche accessible « Coché ». Les textes restent
|
||||||
lisibles et les indicateurs absolus ne modifient pas la taille des cartes.
|
lisibles et les indicateurs absolus ne modifient pas la taille des cartes.
|
||||||
|
|
|
||||||
|
|
@ -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 bouton toolbox et le panneau latéral.
|
||||||
- Vérifier le breadcrumb.
|
- Vérifier le breadcrumb.
|
||||||
- Vérifier les textes avec retours à la ligne.
|
- 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.
|
- 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.
|
- 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.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -211,9 +211,14 @@ Stockage compact :
|
||||||
### Association temporaire aux guides
|
### Association temporaire aux guides
|
||||||
|
|
||||||
Les pages Aniimo, monstres MHW, faune MHW et affixes Diablo peuvent refléter une
|
Les pages Aniimo, monstres MHW, faune MHW et affixes Diablo peuvent refléter une
|
||||||
checklist de la toolbox actuellement associée au jeu. Le sélecteur, la dissociation
|
checklist de la toolbox actuellement associée au jeu. Chaque checklist compatible
|
||||||
et le masquage se trouvent dans le panneau latéral de cette toolbox, au-dessus des outils. Cette liaison ne modifie
|
affiche un toggle de lien à côté du « + » de son en-tête dans le panneau latéral.
|
||||||
ni les données de checklist ni les liens persistants jeu/toolbox.
|
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
|
`sessionStorage["sokkog:guide-checklists"]` contient un objet indexé par
|
||||||
`aniimo/aniimos`, `mhwilds/monsters`, `mhwilds/endemic` ou `diablo4/affixes`.
|
`aniimo/aniimos`, `mhwilds/monsters`, `mhwilds/endemic` ou `diablo4/affixes`.
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,11 @@ import { normalizeChecklistData } from "../website/src/features/toolboxes/storag
|
||||||
|
|
||||||
const route = "/#/games/aniimo/aniimos";
|
const route = "/#/games/aniimo/aniimos";
|
||||||
const sessionKey = "sokkog:guide-checklists";
|
const sessionKey = "sokkog:guide-checklists";
|
||||||
const selector = (page) => page.getByRole("combobox", { name: "Checklist associée", 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 hiddenSwitch = (page) => page.getByRole("button", { name: "Masquer les cochés", exact: true });
|
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 json = (path) => JSON.parse(readFileSync(`website/public/data/${path}`, "utf8"));
|
||||||
const row = (label, qtyCurrent = 0) => ({ id: label, label, qtyCurrent });
|
const row = (label, qtyCurrent = 0) => ({ id: label, label, qtyCurrent });
|
||||||
|
|
||||||
|
|
@ -64,19 +67,30 @@ async function openDrawer(page) {
|
||||||
async function closeDrawer(page) {
|
async function closeDrawer(page) {
|
||||||
await page.locator("#toolbox-drawer").getByRole("button", { name: "Fermer", exact: true }).click();
|
await page.locator("#toolbox-drawer").getByRole("button", { name: "Fermer", exact: true }).click();
|
||||||
}
|
}
|
||||||
|
async function openControls(page, id) {
|
||||||
|
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) {
|
async function choose(page, id) {
|
||||||
await openDrawer(page);
|
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);
|
await closeDrawer(page);
|
||||||
}
|
}
|
||||||
async function toggleHidden(page) {
|
async function toggleHidden(page) {
|
||||||
await openDrawer(page);
|
await openDrawer(page);
|
||||||
|
await openControls(page);
|
||||||
await hiddenSwitch(page).click();
|
await hiddenSwitch(page).click();
|
||||||
await closeDrawer(page);
|
await closeDrawer(page);
|
||||||
}
|
}
|
||||||
async function assertLink(page, id, hidden) {
|
async function assertLink(page, id, hidden) {
|
||||||
await openDrawer(page);
|
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));
|
if (id) await expect(hiddenSwitch(page)).toHaveAttribute("aria-pressed", String(hidden));
|
||||||
else await expect(hiddenSwitch(page)).toHaveCount(0);
|
else await expect(hiddenSwitch(page)).toHaveCount(0);
|
||||||
await closeDrawer(page);
|
await closeDrawer(page);
|
||||||
|
|
@ -118,14 +132,35 @@ for (const width of [1280, 390]) {
|
||||||
await page.getByRole("searchbox").fill("emberpup");
|
await page.getByRole("searchbox").fill("emberpup");
|
||||||
const card = page.locator(".aniimo-result");
|
const card = page.locator(".aniimo-result");
|
||||||
await openDrawer(page);
|
await openDrawer(page);
|
||||||
await expect(selector(page)).toHaveValue("");
|
await expect(activeButtons(page)).toHaveCount(0);
|
||||||
await expect(selector(page).locator("option")).toHaveCount(4);
|
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");
|
const before = await readModule(page, "forms");
|
||||||
await selector(page).selectOption("forms");
|
const heading = controlsToggle(page, "forms");
|
||||||
const panel = page.locator(".guide-checklist-controls");
|
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 bounds = await panel.boundingBox();
|
||||||
const selectBounds = await selector(page).boundingBox();
|
const actionsBounds = await panel.locator(".guide-checklist-actions").boundingBox();
|
||||||
expect(selectBounds.x + selectBounds.width / 2).toBeCloseTo(bounds.x + bounds.width / 2, 0);
|
expect(actionsBounds.x + actionsBounds.width / 2).toBeCloseTo(bounds.x + bounds.width / 2, 0);
|
||||||
await page.screenshot({ path: test.info().outputPath("linked-drawer.png") });
|
await page.screenshot({ path: test.info().outputPath("linked-drawer.png") });
|
||||||
await closeDrawer(page);
|
await closeDrawer(page);
|
||||||
await expect(card).toHaveAttribute("data-checklist-complete", "true");
|
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 page.getByRole("searchbox").fill("emberpup");
|
||||||
await expect(card).toHaveCount(0);
|
await expect(card).toHaveCount(0);
|
||||||
await openDrawer(page);
|
await openDrawer(page);
|
||||||
|
await openControls(page, "capture");
|
||||||
await hiddenSwitch(page).press("Space");
|
await hiddenSwitch(page).press("Space");
|
||||||
await closeDrawer(page);
|
await closeDrawer(page);
|
||||||
await expect(card).toHaveAttribute("data-checklist-complete", "true");
|
await expect(card).toHaveAttribute("data-checklist-complete", "true");
|
||||||
await page.getByRole("button", { name: "Réinitialiser les filtres", exact: true }).click();
|
await page.getByRole("button", { name: "Réinitialiser les filtres", exact: true }).click();
|
||||||
await openDrawer(page);
|
await openDrawer(page);
|
||||||
await expect(selector(page)).toHaveValue("capture");
|
await openControls(page, "capture");
|
||||||
await page.getByRole("button", { name: "Dissocier", exact: true }).click();
|
await expect(linkButton(page, "capture")).toHaveAttribute("aria-pressed", "true");
|
||||||
await expect(selector(page)).toHaveValue("");
|
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 expect(hiddenSwitch(page)).toHaveCount(0);
|
||||||
await closeDrawer(page);
|
await closeDrawer(page);
|
||||||
await expect(page.locator('[data-checklist-complete="true"]')).toHaveCount(0);
|
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");
|
await choose(page, "capture");
|
||||||
const drawer = await openDrawer(page);
|
const drawer = await openDrawer(page);
|
||||||
const checklist = drawer.locator('[data-module-type="checklist"]').filter({ has: page.getByRole("button", { name: "Retirer capture", exact: true }) });
|
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("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 }).fill("Unrelated now");
|
||||||
await checklist.getByRole("textbox", { name: "Libellé", exact: true }).press("Enter");
|
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 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();
|
await drawer.getByRole("button", { name: "Lier une toolbox", exact: true }).click();
|
||||||
const dialog = page.getByRole("dialog", { name: "Lier une toolbox" });
|
const dialog = page.getByRole("dialog", { name: "Lier une toolbox" });
|
||||||
await dialog.getByRole("combobox").selectOption("other");
|
await dialog.getByRole("combobox").selectOption("other");
|
||||||
await dialog.getByRole("button", { name: "Valider", exact: true }).click();
|
await dialog.getByRole("button", { name: "Valider", exact: true }).click();
|
||||||
await expect(selector(page)).toHaveValue("");
|
await expect(activeButtons(page)).toHaveCount(0);
|
||||||
await expect(selector(page).locator("option")).toHaveCount(2);
|
await expect(page.locator(".guide-checklist-controls")).toHaveCount(1);
|
||||||
expect(await page.evaluate((key) => globalThis.sessionStorage.getItem(key), sessionKey)).toBeNull();
|
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);
|
const drawer = await openDrawer(page);
|
||||||
await drawer.getByRole("button", { name: "Retirer capture", exact: true }).click();
|
await drawer.getByRole("button", { name: "Retirer capture", exact: true }).click();
|
||||||
await page.getByRole("dialog").getByRole("button", { name: "Retirer", 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 expect(hiddenSwitch(page)).toHaveCount(0);
|
||||||
await page.reload();
|
await page.reload();
|
||||||
await assertLink(page, "", false);
|
await assertLink(page, "", false);
|
||||||
|
|
@ -240,6 +279,10 @@ test("unavailable session storage retains links across navigation; overview has
|
||||||
await openDrawer(page);
|
await openDrawer(page);
|
||||||
await expect(page.locator(".guide-checklist-controls")).toHaveCount(0);
|
await expect(page.locator(".guide-checklist-controls")).toHaveCount(0);
|
||||||
await closeDrawer(page);
|
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 page.goto(route);
|
||||||
await assertLink(page, "perfect", true);
|
await assertLink(page, "perfect", true);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -90,10 +90,7 @@ export function validateSiteContent(site) {
|
||||||
"gamesPage.emptyTitle",
|
"gamesPage.emptyTitle",
|
||||||
"gamesPage.emptyText",
|
"gamesPage.emptyText",
|
||||||
"guideChecklist.label",
|
"guideChecklist.label",
|
||||||
"guideChecklist.untitled",
|
"guideChecklist.associate",
|
||||||
"guideChecklist.none",
|
|
||||||
"guideChecklist.linkToolbox",
|
|
||||||
"guideChecklist.noCompatible",
|
|
||||||
"guideChecklist.noMatches",
|
"guideChecklist.noMatches",
|
||||||
"guideChecklist.matches",
|
"guideChecklist.matches",
|
||||||
"guideChecklist.unlink",
|
"guideChecklist.unlink",
|
||||||
|
|
|
||||||
|
|
@ -2720,11 +2720,8 @@
|
||||||
"emptyText": "Créez une première toolbox pour stocker vos outils dans ce navigateur."
|
"emptyText": "Créez une première toolbox pour stocker vos outils dans ce navigateur."
|
||||||
},
|
},
|
||||||
"guideChecklist": {
|
"guideChecklist": {
|
||||||
"label": "Checklist associée",
|
"label": "Lien avec le guide",
|
||||||
"untitled": "Checklist sans titre",
|
"associate": "Associer à la page",
|
||||||
"none": "Aucune checklist",
|
|
||||||
"linkToolbox": "Associer une toolbox pour suivre une checklist",
|
|
||||||
"noCompatible": "Aucune checklist de cette toolbox ne correspond aux éléments du guide.",
|
|
||||||
"noMatches": "Cette checklist ne contient plus de correspondance avec le guide.",
|
"noMatches": "Cette checklist ne contient plus de correspondance avec le guide.",
|
||||||
"matches": "correspondances",
|
"matches": "correspondances",
|
||||||
"unlink": "Dissocier",
|
"unlink": "Dissocier",
|
||||||
|
|
|
||||||
|
|
@ -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 { Icon } from "../../components/Icon.jsx";
|
||||||
import { useGuideChecklist } from "./useGuideChecklist.js";
|
|
||||||
|
|
||||||
export function CurrentGuideChecklistControls({ gameId }) {
|
export function GuideChecklistToggle({ checklist, panel, label }) {
|
||||||
const checklist = useGuideChecklist();
|
if (!panel.list) return null;
|
||||||
return checklist.available && checklist.guideId.split("/")[0] === gameId ? <GuideChecklistControls checklist={checklist} /> : null;
|
return <button
|
||||||
|
className={`module-guide-button nds-icon-button ${panel.open ? "active" : ""}`}
|
||||||
|
data-size="md"
|
||||||
|
data-linked={checklist.selected?.id === panel.list.id || undefined}
|
||||||
|
type="button"
|
||||||
|
aria-label={`${checklist.content.label} — ${label}`}
|
||||||
|
title={checklist.content.label}
|
||||||
|
aria-expanded={panel.open}
|
||||||
|
aria-pressed={panel.open}
|
||||||
|
aria-controls={panel.id}
|
||||||
|
onClick={panel.toggle}
|
||||||
|
><Icon name="link" /></button>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GuideChecklistControls({ checklist }) {
|
export function GuideChecklistControls({ checklist, list, id, open }) {
|
||||||
const { content, toolbox, selected, lists, hideCompleted } = checklist;
|
if (!list) return null;
|
||||||
return <div className="guide-checklist-controls nds-panel" aria-label={content.label}>
|
const { content, selected, hideCompleted } = checklist;
|
||||||
{!toolbox ? <button type="button" className="nds-button" data-size="sm" onClick={checklist.linkToolbox}>{content.linkToolbox}</button> : <>
|
const active = selected?.id === list.id;
|
||||||
<label className="field compact">
|
return <div id={id} hidden={!open} className="guide-checklist-controls" data-linked={active || undefined}>
|
||||||
<span>{content.label}</span>
|
<div className="guide-checklist-actions">
|
||||||
<select className="nds-select" data-size="sm" value={selected?.id || ""} disabled={!lists.length} onChange={(event) => checklist.associate(event.target.value)}>
|
<button type="button" className="guide-checklist-link nds-button" data-size="sm" data-variant={active ? "primary" : "secondary"} aria-pressed={active} onClick={() => checklist.associate(active ? "" : list.id)}>{active ? content.unlink : content.associate}</button>
|
||||||
<option value="">{content.none}</option>
|
<button type="button" className="nds-switch" disabled={!active} aria-pressed={active && hideCompleted} onClick={checklist.toggleHidden}><Icon name="hide" /><span>{content.hideCompleted}</span><i aria-hidden="true" /></button>
|
||||||
{lists.map((list) => <option key={list.id} value={list.id}>{list.title} · {list.count} {content.matches}</option>)}
|
</div>
|
||||||
</select>
|
<p>{list.count ? `${list.count} ${content.matches}` : content.noMatches}</p>
|
||||||
</label>
|
|
||||||
{!lists.length && <p>{content.noCompatible}</p>}
|
|
||||||
{selected && <>
|
|
||||||
<div className="guide-checklist-actions">
|
|
||||||
<button type="button" className="nds-button" data-size="sm" onClick={() => checklist.associate("")}>{content.unlink}</button>
|
|
||||||
<button type="button" className="nds-switch" aria-pressed={hideCompleted} onClick={checklist.toggleHidden}><Icon name="hide" /><span>{content.hideCompleted}</span><i aria-hidden="true" /></button>
|
|
||||||
</div>
|
|
||||||
{!selected.count && <p>{content.noMatches}</p>}
|
|
||||||
</>}
|
|
||||||
</>}
|
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,28 @@
|
||||||
// Rôle : partage la progression réactive et les commandes de liaison entre les guides.
|
// Rôle : partage la progression réactive et les commandes de liaison entre les guides.
|
||||||
import { createContext, useContext, useMemo } from "react";
|
import { createContext, useContext, useId, useMemo, useState } from "react";
|
||||||
import { getGuideChecklistEntries, matchGuideChecklist } from "./guideChecklist.js";
|
import { getGuideChecklistEntries, matchGuideChecklist } from "./guideChecklist.js";
|
||||||
|
|
||||||
export const GuideChecklistContext = createContext(null);
|
export const GuideChecklistContext = createContext(null);
|
||||||
|
|
||||||
|
export function useGuideChecklistPanel(checklist, module) {
|
||||||
|
const id = useId();
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const list = module.type === "checklist" ? checklist?.lists.find((item) => item.id === module.id) : undefined;
|
||||||
|
return { id, open, list, toggle: () => setOpen((value) => !value) };
|
||||||
|
}
|
||||||
|
|
||||||
export function useGuideChecklist(requestedGuideId) {
|
export function useGuideChecklist(requestedGuideId) {
|
||||||
const { toolboxes, links, moduleData, guideChecklistSession, actions, siteContent, route, aniimo, mhwilds, diablo4 } = useContext(GuideChecklistContext);
|
const { toolboxes, links, moduleData, guideChecklistSession, siteContent, route, aniimo, mhwilds, diablo4 } = useContext(GuideChecklistContext);
|
||||||
const guideId = requestedGuideId || route.replace(/^\/games\//, "");
|
const guideId = requestedGuideId || route.replace(/^\/games\//, "");
|
||||||
const entries = useMemo(() => getGuideChecklistEntries(guideId, { aniimo, mhwilds, diablo4 }), [guideId, aniimo, mhwilds, diablo4]);
|
const entries = useMemo(() => getGuideChecklistEntries(guideId, { aniimo, mhwilds, diablo4 }), [guideId, aniimo, mhwilds, diablo4]);
|
||||||
const gameId = guideId.split("/")[0];
|
const gameId = guideId.split("/")[0];
|
||||||
const content = siteContent.guideChecklist;
|
const content = siteContent.guideChecklist;
|
||||||
const toolbox = toolboxes.find((item) => item.id === links[gameId]);
|
const toolbox = toolboxes.find((item) => item.id === links[gameId]);
|
||||||
const association = guideChecklistSession.associations[guideId];
|
const association = guideChecklistSession.associations[guideId];
|
||||||
const lists = useMemo(() => (toolbox?.modules || []).filter((module) => module.type === "checklist").map((module, index) => ({
|
const lists = useMemo(() => (toolbox?.modules || []).filter((module) => module.type === "checklist").map((module) => ({
|
||||||
id: module.id, title: module.title || `${content.untitled} ${index + 1}`,
|
id: module.id,
|
||||||
...matchGuideChecklist(entries || [], moduleData[`${toolbox.id}:${module.id}`])
|
...matchGuideChecklist(entries || [], moduleData[`${toolbox.id}:${module.id}`])
|
||||||
})), [entries, toolbox, moduleData, content.untitled]);
|
})), [entries, toolbox, moduleData]);
|
||||||
const selected = lists.find((list) => list.id === association?.moduleId);
|
const selected = lists.find((list) => list.id === association?.moduleId);
|
||||||
const hideCompleted = Boolean(selected && association.hideCompleted);
|
const hideCompleted = Boolean(selected && association.hideCompleted);
|
||||||
const isCompleted = (key) => Boolean(selected?.completed.has(key));
|
const isCompleted = (key) => Boolean(selected?.completed.has(key));
|
||||||
|
|
@ -24,7 +31,6 @@ export function useGuideChecklist(requestedGuideId) {
|
||||||
lists: lists.filter((list) => list.count || list.id === selected?.id),
|
lists: lists.filter((list) => list.count || list.id === selected?.id),
|
||||||
isVisible: (key) => !hideCompleted || !isCompleted(key),
|
isVisible: (key) => !hideCompleted || !isCompleted(key),
|
||||||
associate: (moduleId) => guideChecklistSession.setAssociation(guideId, moduleId ? { toolboxId: toolbox.id, moduleId, hideCompleted } : null),
|
associate: (moduleId) => guideChecklistSession.setAssociation(guideId, moduleId ? { toolboxId: toolbox.id, moduleId, hideCompleted } : null),
|
||||||
toggleHidden: () => guideChecklistSession.setAssociation(guideId, { ...association, hideCompleted: !hideCompleted }),
|
toggleHidden: () => guideChecklistSession.setAssociation(guideId, { ...association, hideCompleted: !hideCompleted })
|
||||||
linkToolbox: () => actions.setLinkModalGameId(gameId)
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { useEffect, useRef, useState } from "react";
|
||||||
import { GuidedTooltipTour } from "../../components/GuidedTooltipTour.jsx";
|
import { GuidedTooltipTour } from "../../components/GuidedTooltipTour.jsx";
|
||||||
import { Icon } from "../../components/Icon.jsx";
|
import { Icon } from "../../components/Icon.jsx";
|
||||||
import { ImportButton } from "../../components/ImportButton.jsx";
|
import { ImportButton } from "../../components/ImportButton.jsx";
|
||||||
import { CurrentGuideChecklistControls } from "../games/GuideChecklistControls.jsx";
|
import { useGuideChecklist } from "../games/useGuideChecklist.js";
|
||||||
import { StorageQuota } from "../../components/StorageQuota.jsx";
|
import { StorageQuota } from "../../components/StorageQuota.jsx";
|
||||||
import { ToastPositionSwitch } from "../../components/ToastPositionSwitch.jsx";
|
import { ToastPositionSwitch } from "../../components/ToastPositionSwitch.jsx";
|
||||||
import { useGroupedReorder } from "../../hooks/useGroupedReorder.js";
|
import { useGroupedReorder } from "../../hooks/useGroupedReorder.js";
|
||||||
|
|
@ -180,7 +180,7 @@ function shouldShowTutorial(toolbox, pendingTutorialToolboxId) {
|
||||||
return toolbox?.tutorial === true || pendingTutorialToolboxId === toolbox?.id;
|
return toolbox?.tutorial === true || pendingTutorialToolboxId === toolbox?.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ToolboxView({ siteContent, toolbox, toolboxGame, embedded, actions, storageUsage, storageReady = true, getModuleData, updateToolbox, updateModuleData, addImageFiles, pendingTutorialToolboxId = "", setPendingTutorialToolboxId }) {
|
function ToolboxView({ siteContent, toolbox, toolboxGame, embedded, actions, storageUsage, storageReady = true, getModuleData, updateToolbox, updateModuleData, addImageFiles, pendingTutorialToolboxId = "", setPendingTutorialToolboxId, guideChecklist }) {
|
||||||
const toolboxId = toolbox.id;
|
const toolboxId = toolbox.id;
|
||||||
const moduleColumns = embedded || Number(toolbox.moduleColumns) === 1 ? 1 : 2;
|
const moduleColumns = embedded || Number(toolbox.moduleColumns) === 1 ? 1 : 2;
|
||||||
const toolboxGameCover = getGameCardCover(toolboxGame);
|
const toolboxGameCover = getGameCardCover(toolboxGame);
|
||||||
|
|
@ -194,6 +194,7 @@ function ToolboxView({ siteContent, toolbox, toolboxGame, embedded, actions, sto
|
||||||
if (previousTutorial.toolboxId !== toolboxId || tutorialRequested) setShowTutorial(tutorialRequested);
|
if (previousTutorial.toolboxId !== toolboxId || tutorialRequested) setShowTutorial(tutorialRequested);
|
||||||
}
|
}
|
||||||
const moduleContext = {
|
const moduleContext = {
|
||||||
|
guideChecklist,
|
||||||
getModuleData,
|
getModuleData,
|
||||||
ensureModuleData: actions.ensureModuleData,
|
ensureModuleData: actions.ensureModuleData,
|
||||||
isModuleDataReady: actions.isModuleDataReady,
|
isModuleDataReady: actions.isModuleDataReady,
|
||||||
|
|
@ -341,6 +342,8 @@ function ToolboxView({ siteContent, toolbox, toolboxGame, embedded, actions, sto
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ToolboxDrawer({ gameId, game, siteContent, toolboxes, links, actions, storageUsage, storageReady = true, getModuleData, updateToolbox, updateModuleData, addImageFiles }) {
|
export function ToolboxDrawer({ gameId, game, siteContent, toolboxes, links, actions, storageUsage, storageReady = true, getModuleData, updateToolbox, updateModuleData, addImageFiles }) {
|
||||||
|
const checklist = useGuideChecklist();
|
||||||
|
const guideChecklist = checklist.available && checklist.guideId.split("/")[0] === gameId ? checklist : undefined;
|
||||||
const selectedId = links[gameId] || "";
|
const selectedId = links[gameId] || "";
|
||||||
const toolbox = toolboxes.find((item) => item.id === selectedId);
|
const toolbox = toolboxes.find((item) => item.id === selectedId);
|
||||||
const [width, setWidth] = useState("");
|
const [width, setWidth] = useState("");
|
||||||
|
|
@ -400,8 +403,7 @@ export function ToolboxDrawer({ gameId, game, siteContent, toolboxes, links, act
|
||||||
</div>
|
</div>
|
||||||
<button className="drawer-close-button nds-icon-button" data-size="sm" onClick={() => actions.setDrawerGameId("")} aria-label="Fermer" title="Fermer"><Icon name="close" /></button>
|
<button className="drawer-close-button nds-icon-button" data-size="sm" onClick={() => actions.setDrawerGameId("")} aria-label="Fermer" title="Fermer"><Icon name="close" /></button>
|
||||||
</header>
|
</header>
|
||||||
<CurrentGuideChecklistControls gameId={gameId} />
|
{toolbox ? <ToolboxView siteContent={siteContent} toolbox={toolbox} toolboxGame={game} embedded actions={actions} storageUsage={storageUsage} storageReady={storageReady} getModuleData={getModuleData} updateToolbox={updateToolbox} updateModuleData={updateModuleData} addImageFiles={addImageFiles} guideChecklist={guideChecklist} /> : (
|
||||||
{toolbox ? <ToolboxView siteContent={siteContent} toolbox={toolbox} toolboxGame={game} embedded actions={actions} storageUsage={storageUsage} storageReady={storageReady} getModuleData={getModuleData} updateToolbox={updateToolbox} updateModuleData={updateModuleData} addImageFiles={addImageFiles} /> : (
|
|
||||||
<div className="empty nds-empty-state"><p>Aucune toolbox associée à cette page jeu.</p><button className="nds-button" data-size="sm" data-variant="primary" onClick={() => actions.setCreateModal({ gameId })}>Créer et associer</button></div>
|
<div className="empty nds-empty-state"><p>Aucune toolbox associée à cette page jeu.</p><button className="nds-button" data-size="sm" data-variant="primary" onClick={() => actions.setCreateModal({ gameId })}>Créer et associer</button></div>
|
||||||
)}
|
)}
|
||||||
<StorageQuota usage={storageUsage} />
|
<StorageQuota usage={storageUsage} />
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
// Rôle : attend les médias persistés avant de monter un outil éditable et propose une reprise locale en cas d'échec.
|
// Rôle : affiche le chargement des outils et attend leurs médias persistés, avec reprise locale en cas d'échec.
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Icon } from "../../../components/Icon.jsx";
|
import { Icon } from "../../../components/Icon.jsx";
|
||||||
|
|
||||||
|
export function ModuleLoadingFallback({ label }) {
|
||||||
|
return <div className="module-loading-fallback">Chargement de {label}...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
export function ModuleDataGate({ context, toolboxId, moduleId, children }) {
|
export function ModuleDataGate({ context, toolboxId, moduleId, children }) {
|
||||||
const ready = context.isModuleDataReady?.(toolboxId, moduleId) ?? true;
|
const ready = context.isModuleDataReady?.(toolboxId, moduleId) ?? true;
|
||||||
const load = context.ensureModuleData;
|
const load = context.ensureModuleData;
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,18 @@
|
||||||
// Rôle : rend les modules d'une toolbox, leur shell commun et leur réorganisation.
|
// Rôle : rend les modules d'une toolbox, leur shell commun et leur réorganisation.
|
||||||
import { Suspense, useMemo, useState } from "react";
|
import { Suspense, useMemo, useState } from "react";
|
||||||
import { Icon } from "../../../components/Icon.jsx";
|
import { Icon } from "../../../components/Icon.jsx";
|
||||||
|
import { GuideChecklistControls, GuideChecklistToggle } from "../../games/GuideChecklistControls.jsx";
|
||||||
|
import { useGuideChecklistPanel } from "../../games/useGuideChecklist.js";
|
||||||
import { useGroupedReorder } from "../../../hooks/useGroupedReorder.js";
|
import { useGroupedReorder } from "../../../hooks/useGroupedReorder.js";
|
||||||
import { useInlineEdit } from "../../../hooks/useInlineEdit.js";
|
import { useInlineEdit } from "../../../hooks/useInlineEdit.js";
|
||||||
import { supportsTextExchange } from "./textImport.js";
|
import { supportsTextExchange } from "./textImport.js";
|
||||||
import { MODULE_COMPONENTS } from "./toolboxModuleComponents.jsx";
|
import { MODULE_COMPONENTS } from "./toolboxModuleComponents.jsx";
|
||||||
import { ModuleDataGate } from "./ModuleDataGate.jsx";
|
import { ModuleDataGate, ModuleLoadingFallback } from "./ModuleDataGate.jsx";
|
||||||
|
|
||||||
const MODULE_COLUMN_IDS = ["left", "right"];
|
const MODULE_COLUMN_IDS = ["left", "right"];
|
||||||
|
|
||||||
function getModuleMap(modules) {
|
|
||||||
return new Map(modules.map((module) => [module.id, module]));
|
|
||||||
}
|
|
||||||
|
|
||||||
function getModulesByIds(modules, ids) {
|
function getModulesByIds(modules, ids) {
|
||||||
const moduleMap = getModuleMap(modules);
|
const moduleMap = new Map(modules.map((module) => [module.id, module]));
|
||||||
return (Array.isArray(ids) ? ids : []).map((id) => moduleMap.get(id)).filter(Boolean);
|
return (Array.isArray(ids) ? ids : []).map((id) => moduleMap.get(id)).filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -157,6 +155,7 @@ function ModuleShell({ toolbox, module, context, reorder, itemGroup = "", onRena
|
||||||
const label = definition.label || module.type;
|
const label = definition.label || module.type;
|
||||||
const [editing, setEditing] = useState(false);
|
const [editing, setEditing] = useState(false);
|
||||||
const [textExchangeOpen, setTextExchangeOpen] = useState(false);
|
const [textExchangeOpen, setTextExchangeOpen] = useState(false);
|
||||||
|
const guidePanel = useGuideChecklistPanel(context.guideChecklist, module);
|
||||||
const hasAddPanel = definition.editable && definition.addable !== false;
|
const hasAddPanel = definition.editable && definition.addable !== false;
|
||||||
const hasTextExchange = supportsTextExchange(module.type);
|
const hasTextExchange = supportsTextExchange(module.type);
|
||||||
const scrollable = definition.scrollable && module.scrollable === true;
|
const scrollable = definition.scrollable && module.scrollable === true;
|
||||||
|
|
@ -242,6 +241,7 @@ function ModuleShell({ toolbox, module, context, reorder, itemGroup = "", onRena
|
||||||
<Icon name="add" />
|
<Icon name="add" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
<GuideChecklistToggle checklist={context.guideChecklist} panel={guidePanel} label={module.title || label} />
|
||||||
{hasTextExchange && (
|
{hasTextExchange && (
|
||||||
<button
|
<button
|
||||||
className={`module-exchange-button nds-icon-button ${textExchangeOpen ? "active" : ""}`}
|
className={`module-exchange-button nds-icon-button ${textExchangeOpen ? "active" : ""}`}
|
||||||
|
|
@ -264,6 +264,7 @@ function ModuleShell({ toolbox, module, context, reorder, itemGroup = "", onRena
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div className={`module-content ${scrollable ? "is-scrollable legacy-scrollbar" : ""}`}>
|
<div className={`module-content ${scrollable ? "is-scrollable legacy-scrollbar" : ""}`}>
|
||||||
|
<GuideChecklistControls checklist={context.guideChecklist} {...guidePanel} />
|
||||||
<Suspense fallback={<ModuleLoadingFallback label={label} />}>
|
<Suspense fallback={<ModuleLoadingFallback label={label} />}>
|
||||||
<ModuleDataGate key={`${toolbox.id}:${module.id}`} context={context} toolboxId={toolbox.id} moduleId={module.id}>
|
<ModuleDataGate key={`${toolbox.id}:${module.id}`} context={context} toolboxId={toolbox.id} moduleId={module.id}>
|
||||||
<Component {...componentProps} />
|
<Component {...componentProps} />
|
||||||
|
|
@ -274,10 +275,6 @@ function ModuleShell({ toolbox, module, context, reorder, itemGroup = "", onRena
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ModuleLoadingFallback({ label }) {
|
|
||||||
return <div className="module-loading-fallback">Chargement de {label}...</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function EditableModuleTitle({ value, fallback, onSave, tutorialTarget = "" }) {
|
function EditableModuleTitle({ value, fallback, onSave, tutorialTarget = "" }) {
|
||||||
const inlineEdit = useInlineEdit({
|
const inlineEdit = useInlineEdit({
|
||||||
value,
|
value,
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,15 @@
|
||||||
// Rôle : présente la liaison checklist et l'état terminé communs aux cartes de guides.
|
// Rôle : présente la liaison checklist et l'état terminé communs aux cartes de guides.
|
||||||
.guide-checklist-controls {
|
.guide-checklist-controls {
|
||||||
display: grid;
|
|
||||||
align-items: center;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
gap: var(--space-3);
|
margin-bottom: var(--space-3);
|
||||||
margin-bottom: var(--space-4);
|
padding-bottom: var(--space-3);
|
||||||
padding: var(--space-3);
|
border-bottom: 1px solid var(--color-border);
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
.field { min-width: 0; margin: 0; }
|
p { margin: var(--space-2) 0 0; color: var(--color-text-muted); font-size: var(--font-size-xs); }
|
||||||
select { width: 100%; min-width: 0; }
|
|
||||||
p { margin: 0; color: var(--color-text-muted); font-size: var(--font-size-sm); }
|
|
||||||
.nds-switch { max-width: 100%; }
|
.nds-switch { max-width: 100%; }
|
||||||
.guide-checklist-actions { display: flex; flex-wrap: wrap; justify-content: center; align-items: center; gap: var(--space-3); }
|
.guide-checklist-actions { display: flex; flex-wrap: wrap; justify-content: center; align-items: center; gap: var(--space-2); padding-top: var(--space-2); }
|
||||||
}
|
}
|
||||||
|
.module-guide-button[data-linked] .ui-icon { color: var(--color-accent-gold); }
|
||||||
.guide-checklist-mark {
|
.guide-checklist-mark {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 10px;
|
top: 10px;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue