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
|
||||
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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`.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 ? <GuideChecklistControls checklist={checklist} /> : null;
|
||||
export function GuideChecklistToggle({ checklist, panel, label }) {
|
||||
if (!panel.list) return 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 }) {
|
||||
const { content, toolbox, selected, lists, hideCompleted } = checklist;
|
||||
return <div className="guide-checklist-controls nds-panel" aria-label={content.label}>
|
||||
{!toolbox ? <button type="button" className="nds-button" data-size="sm" onClick={checklist.linkToolbox}>{content.linkToolbox}</button> : <>
|
||||
<label className="field compact">
|
||||
<span>{content.label}</span>
|
||||
<select className="nds-select" data-size="sm" value={selected?.id || ""} disabled={!lists.length} onChange={(event) => checklist.associate(event.target.value)}>
|
||||
<option value="">{content.none}</option>
|
||||
{lists.map((list) => <option key={list.id} value={list.id}>{list.title} · {list.count} {content.matches}</option>)}
|
||||
</select>
|
||||
</label>
|
||||
{!lists.length && <p>{content.noCompatible}</p>}
|
||||
{selected && <>
|
||||
export function GuideChecklistControls({ checklist, list, id, open }) {
|
||||
if (!list) return null;
|
||||
const { content, selected, hideCompleted } = checklist;
|
||||
const active = selected?.id === list.id;
|
||||
return <div id={id} hidden={!open} className="guide-checklist-controls" data-linked={active || undefined}>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
{!selected.count && <p>{content.noMatches}</p>}
|
||||
</>}
|
||||
</>}
|
||||
<p>{list.count ? `${list.count} ${content.matches}` : content.noMatches}</p>
|
||||
</div>;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,21 +1,28 @@
|
|||
// 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";
|
||||
|
||||
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) {
|
||||
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 entries = useMemo(() => getGuideChecklistEntries(guideId, { aniimo, mhwilds, diablo4 }), [guideId, aniimo, mhwilds, diablo4]);
|
||||
const gameId = guideId.split("/")[0];
|
||||
const content = siteContent.guideChecklist;
|
||||
const toolbox = toolboxes.find((item) => item.id === links[gameId]);
|
||||
const association = guideChecklistSession.associations[guideId];
|
||||
const lists = useMemo(() => (toolbox?.modules || []).filter((module) => module.type === "checklist").map((module, index) => ({
|
||||
id: module.id, title: module.title || `${content.untitled} ${index + 1}`,
|
||||
const lists = useMemo(() => (toolbox?.modules || []).filter((module) => module.type === "checklist").map((module) => ({
|
||||
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 hideCompleted = Boolean(selected && association.hideCompleted);
|
||||
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),
|
||||
isVisible: (key) => !hideCompleted || !isCompleted(key),
|
||||
associate: (moduleId) => guideChecklistSession.setAssociation(guideId, moduleId ? { toolboxId: toolbox.id, moduleId, hideCompleted } : null),
|
||||
toggleHidden: () => guideChecklistSession.setAssociation(guideId, { ...association, hideCompleted: !hideCompleted }),
|
||||
linkToolbox: () => actions.setLinkModalGameId(gameId)
|
||||
toggleHidden: () => guideChecklistSession.setAssociation(guideId, { ...association, hideCompleted: !hideCompleted })
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { useEffect, useRef, useState } from "react";
|
|||
import { GuidedTooltipTour } from "../../components/GuidedTooltipTour.jsx";
|
||||
import { Icon } from "../../components/Icon.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 { ToastPositionSwitch } from "../../components/ToastPositionSwitch.jsx";
|
||||
import { useGroupedReorder } from "../../hooks/useGroupedReorder.js";
|
||||
|
|
@ -180,7 +180,7 @@ function shouldShowTutorial(toolbox, pendingTutorialToolboxId) {
|
|||
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 moduleColumns = embedded || Number(toolbox.moduleColumns) === 1 ? 1 : 2;
|
||||
const toolboxGameCover = getGameCardCover(toolboxGame);
|
||||
|
|
@ -194,6 +194,7 @@ function ToolboxView({ siteContent, toolbox, toolboxGame, embedded, actions, sto
|
|||
if (previousTutorial.toolboxId !== toolboxId || tutorialRequested) setShowTutorial(tutorialRequested);
|
||||
}
|
||||
const moduleContext = {
|
||||
guideChecklist,
|
||||
getModuleData,
|
||||
ensureModuleData: actions.ensureModuleData,
|
||||
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 }) {
|
||||
const checklist = useGuideChecklist();
|
||||
const guideChecklist = checklist.available && checklist.guideId.split("/")[0] === gameId ? checklist : undefined;
|
||||
const selectedId = links[gameId] || "";
|
||||
const toolbox = toolboxes.find((item) => item.id === selectedId);
|
||||
const [width, setWidth] = useState("");
|
||||
|
|
@ -400,8 +403,7 @@ export function ToolboxDrawer({ gameId, game, siteContent, toolboxes, links, act
|
|||
</div>
|
||||
<button className="drawer-close-button nds-icon-button" data-size="sm" onClick={() => actions.setDrawerGameId("")} aria-label="Fermer" title="Fermer"><Icon name="close" /></button>
|
||||
</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} /> : (
|
||||
{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} /> : (
|
||||
<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} />
|
||||
|
|
|
|||
|
|
@ -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 { 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 }) {
|
||||
const ready = context.isModuleDataReady?.(toolboxId, moduleId) ?? true;
|
||||
const load = context.ensureModuleData;
|
||||
|
|
|
|||
|
|
@ -1,20 +1,18 @@
|
|||
// Rôle : rend les modules d'une toolbox, leur shell commun et leur réorganisation.
|
||||
import { Suspense, useMemo, useState } from "react";
|
||||
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 { useInlineEdit } from "../../../hooks/useInlineEdit.js";
|
||||
import { supportsTextExchange } from "./textImport.js";
|
||||
import { MODULE_COMPONENTS } from "./toolboxModuleComponents.jsx";
|
||||
import { ModuleDataGate } from "./ModuleDataGate.jsx";
|
||||
import { ModuleDataGate, ModuleLoadingFallback } from "./ModuleDataGate.jsx";
|
||||
|
||||
const MODULE_COLUMN_IDS = ["left", "right"];
|
||||
|
||||
function getModuleMap(modules) {
|
||||
return new Map(modules.map((module) => [module.id, module]));
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
@ -157,6 +155,7 @@ function ModuleShell({ toolbox, module, context, reorder, itemGroup = "", onRena
|
|||
const label = definition.label || module.type;
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [textExchangeOpen, setTextExchangeOpen] = useState(false);
|
||||
const guidePanel = useGuideChecklistPanel(context.guideChecklist, module);
|
||||
const hasAddPanel = definition.editable && definition.addable !== false;
|
||||
const hasTextExchange = supportsTextExchange(module.type);
|
||||
const scrollable = definition.scrollable && module.scrollable === true;
|
||||
|
|
@ -242,6 +241,7 @@ function ModuleShell({ toolbox, module, context, reorder, itemGroup = "", onRena
|
|||
<Icon name="add" />
|
||||
</button>
|
||||
)}
|
||||
<GuideChecklistToggle checklist={context.guideChecklist} panel={guidePanel} label={module.title || label} />
|
||||
{hasTextExchange && (
|
||||
<button
|
||||
className={`module-exchange-button nds-icon-button ${textExchangeOpen ? "active" : ""}`}
|
||||
|
|
@ -264,6 +264,7 @@ function ModuleShell({ toolbox, module, context, reorder, itemGroup = "", onRena
|
|||
</div>
|
||||
</header>
|
||||
<div className={`module-content ${scrollable ? "is-scrollable legacy-scrollbar" : ""}`}>
|
||||
<GuideChecklistControls checklist={context.guideChecklist} {...guidePanel} />
|
||||
<Suspense fallback={<ModuleLoadingFallback label={label} />}>
|
||||
<ModuleDataGate key={`${toolbox.id}:${module.id}`} context={context} toolboxId={toolbox.id} moduleId={module.id}>
|
||||
<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 = "" }) {
|
||||
const inlineEdit = useInlineEdit({
|
||||
value,
|
||||
|
|
|
|||
|
|
@ -1,18 +1,15 @@
|
|||
// Rôle : présente la liaison checklist et l'état terminé communs aux cartes de guides.
|
||||
.guide-checklist-controls {
|
||||
display: grid;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
gap: var(--space-3);
|
||||
margin-bottom: var(--space-4);
|
||||
padding: var(--space-3);
|
||||
margin-bottom: var(--space-3);
|
||||
padding-bottom: var(--space-3);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
min-width: 0;
|
||||
.field { min-width: 0; margin: 0; }
|
||||
select { width: 100%; min-width: 0; }
|
||||
p { margin: 0; color: var(--color-text-muted); font-size: var(--font-size-sm); }
|
||||
p { margin: var(--space-2) 0 0; color: var(--color-text-muted); font-size: var(--font-size-xs); }
|
||||
.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 {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue