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
|
|
@ -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 && <>
|
||||
<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>}
|
||||
</>}
|
||||
</>}
|
||||
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="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>
|
||||
<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