diff --git a/DESIGN_SYSTEM.md b/DESIGN_SYSTEM.md index 2e4ba2c..e24a47f 100644 --- a/DESIGN_SYSTEM.md +++ b/DESIGN_SYSTEM.md @@ -890,6 +890,11 @@ masquage. La colonne d'actions occupe la hauteur existante du header afin que cet encart n'ajoute pas de ligne ou d'espace vertical. Cette association est conservée dans `localStorage` entre les sessions ; elle est retirée si la checklist, la toolbox liée ou la liaison jeu/toolbox disparaît. +Le bouton checklist des pages de résultats filtrables ouvre une modale NDS qui +rappelle que les filtres, le regroupement et le masquage déterminent les éléments +exportés. La copie texte reste disponible sans toolbox liée ; l'action de création +est alors désactivée et accompagnée d'une notice inline. Avec une toolbox liée, la +modale affiche son nom et crée la checklist dans son espace actif. L'état terminé désature et atténue uniquement les portraits (ou les icônes de catégorie Diablo). Le recto utilise uniquement la checkbox interactive ; la coche accessible « Coché » reste sur le verso des cartes retournables. Les textes restent diff --git a/docs/FEATURE_CHECKLIST.md b/docs/FEATURE_CHECKLIST.md index 36e4eca..76f6780 100644 --- a/docs/FEATURE_CHECKLIST.md +++ b/docs/FEATURE_CHECKLIST.md @@ -58,7 +58,8 @@ Checklist à suivre lors de l'ajout ou de la modification d'un outil, d'une page - Vérifier les textes avec retours à la ligne. - 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. La checkbox des cartes met à jour toutes les lignes exactes correspondantes, respecte les quantités cibles et ne retourne pas les cartes. - 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 les résultats filtrables Aniimo, MHW et Diablo, vérifier que l'action checklist ouvre une modale rappelant que seuls les éléments visibles sont utilisés. La copie reste disponible sans toolbox liée ; la création est alors désactivée avec une indication explicite. Avec une toolbox liée, la création ajoute une nouvelle checklist dans son espace actif sans remplacer l'association guide/checklist existante. +- Pour Aniimo, vérifier la copie et la création des espèces ou des formes selon l'affichage, les noms français/anglais, la priorité des formes et le masquage avant regroupement. Compteurs, copie et création doivent correspondre aux résultats affichés. ## Listes de jeu diff --git a/docs/STORAGE_SCHEMA.md b/docs/STORAGE_SCHEMA.md index ed310fe..7308e13 100644 --- a/docs/STORAGE_SCHEMA.md +++ b/docs/STORAGE_SCHEMA.md @@ -298,7 +298,11 @@ affichage séparé. Les noms et formes français et anglais sont reconnus. Une l sans forme couvre toutes les variantes ; une ligne spécifique à une forme prend priorité sur la ligne générale. Le masquage des cochés précède le regroupement ; le style d'une carte groupée dépend de la variante active. La copie et les -compteurs portent sur les résultats effectivement affichés. +compteurs portent sur les résultats effectivement affichés. Les pages de résultats +filtrables peuvent aussi créer une nouvelle checklist dans l'espace actif de la +toolbox liée au jeu. Cette création reprend uniquement les éléments visibles, +initialisés à `qtyTarget: 1` et `qtyCurrent: 0`, et ne modifie pas l'association +guide/checklist conservée dans `localStorage`. ### Données persistantes diff --git a/e2e/guide-checklist.spec.js b/e2e/guide-checklist.spec.js index 3686fd5..d3ca153 100644 --- a/e2e/guide-checklist.spec.js +++ b/e2e/guide-checklist.spec.js @@ -58,6 +58,35 @@ async function readModule(page, moduleId) { }), moduleId); } +async function readCreatedChecklist(page, title) { + return page.evaluate(async (title) => new Promise((resolve, reject) => { + const request = globalThis.indexedDB.open("sokkog", 1); + request.onsuccess = () => { + const db = request.result; + const tx = db.transaction(["kv", "modules"], "readonly"); + const toolboxesRequest = tx.objectStore("kv").get("toolboxes"); + const modulesRequest = tx.objectStore("modules").getAll(); + tx.oncomplete = () => { + const toolbox = toolboxesRequest.result.find((item) => item.id === "guide"); + const module = toolbox.modules.find((item) => item.type === "checklist" && item.title === title); + const data = modulesRequest.result.find((entry) => entry.key === `guide:${module?.id}`)?.data; + const activeSpace = toolbox.spaces.find((space) => space.id === toolbox.activeSpaceId); + db.close(); + resolve({ module, data, activeModuleIds: activeSpace?.moduleOrder.one || [] }); + }; + tx.onerror = () => reject(tx.error); + }; + request.onerror = () => reject(request.error); + }), title); +} + +async function copyVisibleResults(page) { + await page.locator(".results-copy-button").click(); + const dialog = page.getByRole("dialog", { name: "Utiliser les résultats visibles" }); + await dialog.getByRole("button", { name: "Copier le format texte", exact: true }).click(); + await expect(dialog).toHaveCount(0); +} + async function openDrawer(page) { const drawer = page.locator("#toolbox-drawer"); if (!await drawer.locator(".drawer-panel").isVisible()) await page.locator(".topbar-actions button").click(); @@ -105,25 +134,62 @@ test("Aniimo copies visible species or forms and round-trips through checklist i const copy = page.locator(".results-copy-button"); await expect(copy).toHaveAccessibleName(/sans les variantes/); await expect(page.locator('[data-copy-target="true"]')).toHaveText("1 / 88 Aniimos"); - await copy.click(); + await copyVisibleResults(page); await expect.poll(() => page.evaluate(() => globalThis.navigator.clipboard.readText())).toBe("Loufeuteau:1"); await page.getByRole("button", { name: "Regrouper les variantes", exact: true }).click(); await expect(copy).toHaveAccessibleName(/toutes les variantes/); await expect(page.locator('[data-copy-target="true"]')).toHaveText("3 / 208 variantes"); - await copy.click(); + await copyVisibleResults(page); const text = await page.evaluate(() => globalThis.navigator.clipboard.readText()); const imported = importChecklistText(text, { uid: (prefix) => `${prefix}-${Math.random()}`, normalizeChecklistData }); expect(imported.items.map((item) => item.label)).toEqual(["Loufeuteau — Forme de base", "Loufeuteau — Forme des montagnes boisées", "Loufeuteau — Hautes-plaines"]); expect(imported.items.every((item) => item.qtyTarget === 1 && item.qtyCurrent === 0)).toBe(true); await choose(page, "forms"); await toggleHidden(page); - await copy.click(); + await copyVisibleResults(page); expect(await page.evaluate(() => globalThis.navigator.clipboard.readText())).not.toContain("Forme de base"); await choose(page, "perfect"); await expect(copy).toBeDisabled(); await expect(page.getByRole("heading", { name: "Aucun résultat" })).toBeVisible(); }); +test("filtered results explain unavailable creation when no toolbox is linked", async ({ page, context }) => { + await context.grantPermissions(["clipboard-read", "clipboard-write"]); + await page.setViewportSize({ width: 390, height: 844 }); + await resetSokkoStorage(page); + await page.goto(route); + await page.getByRole("searchbox").fill("emberpup"); + await page.locator(".results-copy-button").click(); + const dialog = page.getByRole("dialog", { name: "Utiliser les résultats visibles" }); + await expect(dialog.getByText("Liez une toolbox à cette page pour créer directement la checklist.")).toBeVisible(); + await expect(dialog.getByRole("button", { name: "Créer dans la toolbox", exact: true })).toBeDisabled(); + expect(await page.evaluate(() => globalThis.document.documentElement.scrollWidth <= globalThis.innerWidth)).toBe(true); + await dialog.getByRole("button", { name: "Copier le format texte", exact: true }).click(); + await expect.poll(() => page.evaluate(() => globalThis.navigator.clipboard.readText())).toBe("Loufeuteau:1"); +}); + +test("filtered results create a checklist in the linked active space without replacing the guide association", async ({ page }) => { + await seed(page); + await page.goto(route); + await choose(page, "forms"); + await page.getByRole("searchbox").fill("emberpup"); + const associationBefore = await page.evaluate((key) => globalThis.localStorage.getItem(key), storageKey); + await page.locator(".results-copy-button").click(); + const dialog = page.getByRole("dialog", { name: "Utiliser les résultats visibles" }); + await expect(dialog.getByText("Guide toolbox", { exact: true })).toBeVisible(); + await expect(dialog.getByText("Aniimos", { exact: true })).toBeVisible(); + await dialog.getByRole("button", { name: "Créer dans la toolbox", exact: true }).click(); + await expect(dialog).toHaveCount(0); + await expect(page.getByText("Checklist créée dans la toolbox associée.")).toBeVisible(); + + await expect.poll(async () => (await readCreatedChecklist(page, "Aniimos")).module?.id).toBeTruthy(); + const created = await readCreatedChecklist(page, "Aniimos"); + expect(created.activeModuleIds).toContain(created.module.id); + expect(normalizeChecklistData(created.data).items.map((item) => item.label)).toEqual(["Loufeuteau"]); + expect(normalizeChecklistData(created.data).items.every((item) => item.qtyTarget === 1 && item.qtyCurrent === 0)).toBe(true); + expect(await page.evaluate((key) => globalThis.localStorage.getItem(key), storageKey)).toBe(associationBefore); +}); + for (const width of [1280, 390]) { test(`drawer linking, page controls and persistent restoration at ${width}px`, async ({ page }) => { test.setTimeout(60000); @@ -239,10 +305,10 @@ for (const width of [1280, 390]) { }); } -for (const [gameId, category, cardClass, dataPath, dataKey] of [ - ["mhwilds", "monsters", ".monster-card", "mhwilds/monsters.json", "monsters"], - ["mhwilds", "endemic", ".endemic-card", "mhwilds/endemic_life.json", "endemicLife"], - ["diablo4", "affixes", ".diablo4-affix-card", "diablo4/affixes_types.json", "affixes"] +for (const [gameId, category, cardClass, dataPath, dataKey, checklistTitle] of [ + ["mhwilds", "monsters", ".monster-card", "mhwilds/monsters.json", "monsters", "Monstres"], + ["mhwilds", "endemic", ".endemic-card", "mhwilds/endemic_life.json", "endemicLife", "Faune endémique"], + ["diablo4", "affixes", ".diablo4-affix-card", "diablo4/affixes_types.json", "affixes", "Affixes"] ]) { test(`${gameId}/${category} links from its drawer and hides completed cards`, async ({ page }) => { const item = json(dataPath)[dataKey][0]; @@ -250,6 +316,10 @@ for (const [gameId, category, cardClass, dataPath, dataKey] of [ await seed(page, gameId, { progress: [row(label, 1)] }); await page.goto(`/#/games/${gameId}/${category}`); await expect(page.locator(cardClass).first()).toBeVisible(); + await page.locator(".results-copy-button").click(); + const resultsDialog = page.getByRole("dialog", { name: "Utiliser les résultats visibles" }); + await expect(resultsDialog.getByText(checklistTitle, { exact: true })).toBeVisible(); + await resultsDialog.getByRole("button", { name: "Fermer", exact: true }).click(); const count = await page.locator(cardClass).count(); await choose(page, "progress"); await expect(page.locator(`${cardClass}[data-checklist-complete="true"]`)).toHaveCount(1); diff --git a/tests/helpers/data-validation.mjs b/tests/helpers/data-validation.mjs index a0af777..ab9f772 100644 --- a/tests/helpers/data-validation.mjs +++ b/tests/helpers/data-validation.mjs @@ -100,8 +100,21 @@ export function validateSiteContent(site) { "guideChecklist.completed", "guideChecklist.checkCard", "guideChecklist.uncheckCard", + "guideChecklist.resultsModalTitle", + "guideChecklist.resultsModalDescription", + "guideChecklist.resultsChecklistLabel", + "guideChecklist.resultsTargetLabel", + "guideChecklist.resultsNoToolbox", + "guideChecklist.resultsCopyLabel", + "guideChecklist.resultsCreateLabel", + "guideChecklist.resultsCreatingLabel", + "guideChecklist.resultsClose", + "guideChecklist.resultsCopied", + "guideChecklist.resultsCopyError", + "guideChecklist.resultsCreateError", "aniimo.copyChecklist", "aniimo.copyVariantsChecklist", + "aniimo.variantsChecklistTitle", "aniimo.statsTitle", "aniimo.statsTotal", "aniimo.stats.hp", diff --git a/tests/static-games.test.mjs b/tests/static-games.test.mjs index 7a5c099..90df53c 100644 --- a/tests/static-games.test.mjs +++ b/tests/static-games.test.mjs @@ -49,6 +49,10 @@ test("generic game routes and shared game components are wired", async () => { assert.match(checklistCopyButton, /# \$\{title\}/); assert.match(checklistCopyButton, /navigator\.clipboard\.writeText/); assert.match(checklistCopyButton, /results-copy-button/); + assert.match(checklistCopyButton, /results-checklist-modal/); + assert.match(checklistCopyButton, /filteredResults/); + assert.match(checklistCopyButton, /Créer dans la toolbox/); + assert.match(checklistCopyButton, /resultsNoToolbox/); assert.match(gameListsPage, /export function GameListsPage/); assert.match(gameListsPage, /game-list-selector/); assert.match(gameListsPage, /game-list-create-button/); @@ -78,6 +82,8 @@ test("mhwilds and diablo pages expose expected route pieces", async () => { assert.match(diablo4Overview, /export function Diablo4Overview/); assert.match(diablo4Listing, /export function Diablo4Listing/); assert.match(diablo4Listing, /CopyChecklistItemsButton/); + assert.match(diablo4Listing, /checklistTitle="Affixes"/); + assert.match(diablo4Listing, /filteredResults/); assert.match(diablo4Filters, /export function Diablo4Filters/); assert.match(diablo4Filters, /GameFiltersPanel/); assert.match(diablo4Filters, /variant="horizontal-menu"/); @@ -92,11 +98,15 @@ test("mhwilds and diablo pages expose expected route pieces", async () => { assert.match(mhwildsListing, /export function MhwildsListing/); assert.match(mhwildsListing, /GameBreadcrumb/); assert.match(mhwildsListing, /CopyChecklistItemsButton/); + assert.match(mhwildsListing, /checklistTitle=\{label\}/); + assert.match(mhwildsListing, /filteredResults/); assert.match(mhwildsListing, /endemic_life/); assert.match(mhwildsLists, /export function MhwildsLists/); assert.match(mhwildsLists, /GameListsPage/); assert.match(mhwildsLists, /selectedListId/); assert.match(aniimoPage, /AniimoLists/); + assert.match(aniimoPage, /content\.variantsChecklistTitle/); + assert.match(aniimoPage, /filteredResults/); assert.match(aniimoPage, /games\/aniimo\/lists/); assert.match(aniimoFilters, /data-variant="horizontal-menu"/); assert.match(aniimoFilters, /GameFilterDropdown/); diff --git a/tests/static-toolboxes.test.mjs b/tests/static-toolboxes.test.mjs index d67c144..c9958c7 100644 --- a/tests/static-toolboxes.test.mjs +++ b/tests/static-toolboxes.test.mjs @@ -34,6 +34,9 @@ test("toolbox cards and pages retain their public UI conventions", async () => { assert.match(toolboxActions, /type: "memos"/); assert.match(toolboxActions, /type: "links"/); assert.match(toolboxActions, /#\/library/); + assert.match(toolboxActions, /async function createChecklistFromResults/); + assert.match(toolboxActions, /persistToolboxSnapshot/); + assert.match(toolboxActions, /createChecklistFromList/); assert.match(toolboxStorage, /TOOLBOX_ICON_FILES/); assert.match(toolboxStorage, /export function normalizeToolboxIcon/); assert.match(toolboxStorage, /export function normalizeToolboxCustomCover/); diff --git a/website/public/data/site.json b/website/public/data/site.json index d2d62c3..8605234 100644 --- a/website/public/data/site.json +++ b/website/public/data/site.json @@ -2808,14 +2808,27 @@ "hideCompleted": "Masquer les cochés", "completed": "Coché", "checkCard": "Cocher dans la checklist", - "uncheckCard": "Décocher dans la checklist" + "uncheckCard": "Décocher dans la checklist", + "resultsModalTitle": "Utiliser les résultats visibles", + "resultsModalDescription": "Seuls les éléments actuellement visibles sur la page seront utilisés, après application des filtres, du regroupement et du masquage des éléments cochés.", + "resultsChecklistLabel": "Nouvelle checklist", + "resultsTargetLabel": "Toolbox cible", + "resultsNoToolbox": "Liez une toolbox à cette page pour créer directement la checklist.", + "resultsCopyLabel": "Copier le format texte", + "resultsCreateLabel": "Créer dans la toolbox", + "resultsCreatingLabel": "Création…", + "resultsClose": "Fermer", + "resultsCopied": "Copié dans le presse-papiers.", + "resultsCopyError": "Impossible de copier les résultats dans le presse-papiers.", + "resultsCreateError": "Impossible de créer la checklist." }, "aniimo": { "title": "Aniimos", "description": "Filtrez les variantes par nom, éléments, rôle et compétences de logis, puis retournez les cartes pour consulter leurs capacités.", "homeDescription": "Recherche par nom, rôle, éléments et compétences de logis.", - "copyChecklist": "Copier les Aniimos visibles sans les variantes pour checklist", - "copyVariantsChecklist": "Copier toutes les variantes visibles pour checklist", + "copyChecklist": "Utiliser les Aniimos visibles sans les variantes pour une checklist", + "copyVariantsChecklist": "Utiliser toutes les variantes visibles pour une checklist", + "variantsChecklistTitle": "Variantes Aniimo", "statsTitle": "Statistiques", "statsTotal": "Total", "stats": { "hp": "PV", "atk": "Attaque", "p_def": "Déf. phys.", "m_def": "Déf. mag.", "break": "Rupture", "regen": "Régén." }, diff --git a/website/src/features/games/CopyChecklistItemsButton.jsx b/website/src/features/games/CopyChecklistItemsButton.jsx index 752bbfe..314bfff 100644 --- a/website/src/features/games/CopyChecklistItemsButton.jsx +++ b/website/src/features/games/CopyChecklistItemsButton.jsx @@ -1,7 +1,9 @@ -// Rôle : copie des données filtrées au format compatible avec l'import checklist. +// Rôle : copie ou crée en checklist les données visibles des pages de jeu. /* eslint-disable react-refresh/only-export-components -- expose aussi les formatters publics testés pour l'import checklist. */ -import { useState } from "react"; +import { useEffect, useId, useState } from "react"; +import { createPortal } from "react-dom"; import { Icon } from "../../components/Icon.jsx"; +import { lockBodyScroll } from "../../utils/bodyScrollLock.js"; async function copyText(value) { try { @@ -44,8 +46,105 @@ export function formatChecklistImportSections(sections = []) { .join("\n\n"); } -export function CopyChecklistItemsButton({ labels, sections, title = "Copier pour checklist" }) { +function ChecklistResultsModal({ checklistTitle, content, linkedToolbox, text, onCreate, onClose }) { + const titleId = useId(); + const descriptionId = useId(); + const [creating, setCreating] = useState(false); + const [error, setError] = useState(""); + + useEffect(() => { + function handleKeyDown(event) { + if (event.key === "Escape" && !creating) onClose(); + } + + const unlock = lockBodyScroll(); + document.addEventListener("keydown", handleKeyDown); + return () => { + unlock(); + document.removeEventListener("keydown", handleKeyDown); + }; + }, [creating, onClose]); + + async function copyItems() { + setError(""); + if (!await copyText(text)) { + setError(content.resultsCopyError || "Impossible de copier les résultats dans le presse-papiers."); + return; + } + notify(content.resultsCopied || "Copié dans le presse-papiers."); + onClose(); + } + + async function createChecklist() { + if (!linkedToolbox || creating) return; + setCreating(true); + setError(""); + try { + if (await onCreate()) onClose(); + else setError(content.resultsCreateError || "Impossible de créer la checklist."); + } catch { + setError(content.resultsCreateError || "Impossible de créer la checklist."); + } finally { + setCreating(false); + } + } + + return createPortal( +
+
!creating && onClose()} /> +
+
+

{content.resultsModalTitle || "Utiliser les résultats visibles"}

+ +
+

+ {content.resultsModalDescription || "Seuls les éléments actuellement visibles sur la page seront utilisés, après application des filtres."} +

+
+ {content.resultsChecklistLabel || "Checklist"} + {checklistTitle} +
+ {linkedToolbox ? ( +
+ {content.resultsTargetLabel || "Toolbox cible"} + {linkedToolbox.name} +
+ ) : ( +

+ + {content.resultsNoToolbox || "Liez une toolbox à cette page pour créer directement la checklist."} +

+ )} + {error &&

{error}

} +
+ + +
+
+
, + document.body + ); +} + +export function CopyChecklistItemsButton({ + labels, + sections, + title = "Copier pour checklist", + filteredResults = false, + checklistTitle = "Checklist", + linkedToolbox, + content = {}, + onCreateChecklist +}) { const [copied, setCopied] = useState(false); + const [open, setOpen] = useState(false); const text = sections ? formatChecklistImportSections(sections) : formatChecklistImportItems(labels); async function copyItems() { @@ -55,17 +154,37 @@ export function CopyChecklistItemsButton({ labels, sections, title = "Copier pou window.setTimeout(() => setCopied(false), 1400); } + async function createChecklist() { + if (!onCreateChecklist) return false; + return onCreateChecklist({ + title: checklistTitle, + ...(sections ? { sections } : { items: labels || [] }) + }); + } + return ( - + <> + + {filteredResults && open && ( + setOpen(false)} + /> + )} + ); } diff --git a/website/src/features/games/aniimo/AniimoPage.jsx b/website/src/features/games/aniimo/AniimoPage.jsx index cc862c1..5abe308 100644 --- a/website/src/features/games/aniimo/AniimoPage.jsx +++ b/website/src/features/games/aniimo/AniimoPage.jsx @@ -28,7 +28,15 @@ function AniimoListing({ game, aniimo, filters, setFilters, content }) {

{content.title}

{groups.length} / {totalAniimos} {content.title}{" · "}{visible.length} / {aniimo.variants.length} {content.variants.toLowerCase()} - t(variant.name)) : visible.map((variant) => `${t(variant.name)} — ${t(variant.form)}`)} title={active.grouped ? content.copyChecklist : content.copyVariantsChecklist} /> + t(variant.name)) : visible.map((variant) => `${t(variant.name)} — ${t(variant.form)}`)} + title={active.grouped ? content.copyChecklist : content.copyVariantsChecklist} + filteredResults + checklistTitle={active.grouped ? content.title : content.variantsChecklistTitle} + linkedToolbox={checklist.toolbox} + content={checklist.content} + onCreateChecklist={checklist.createChecklistFromResults} + />

{content.description}

diff --git a/website/src/features/games/diablo4/Diablo4Listing.jsx b/website/src/features/games/diablo4/Diablo4Listing.jsx index 69cf4ab..946963f 100644 --- a/website/src/features/games/diablo4/Diablo4Listing.jsx +++ b/website/src/features/games/diablo4/Diablo4Listing.jsx @@ -27,7 +27,15 @@ export function Diablo4Listing({ game, diablo4, filters, setFilters }) {

Affixes

{visible.length} / {diablo4.affixes.length} - affix.label)} title="Copier les affixes visibles pour checklist" /> + affix.label)} + title="Utiliser les affixes visibles pour une checklist" + filteredResults + checklistTitle="Affixes" + linkedToolbox={checklist.toolbox} + content={checklist.content} + onCreateChecklist={checklist.createChecklistFromResults} + />

Filtrez les affixes par nom et catégories pour retrouver rapidement les statistiques utiles à votre build.

diff --git a/website/src/features/games/mhwilds/MhwildsListing.jsx b/website/src/features/games/mhwilds/MhwildsListing.jsx index e9119b5..e480721 100644 --- a/website/src/features/games/mhwilds/MhwildsListing.jsx +++ b/website/src/features/games/mhwilds/MhwildsListing.jsx @@ -29,7 +29,15 @@ export function MhwildsListing({ category, game, mhwilds, filters, setFilters, t

{label}

{visible.length} / {items.length} - t(item.name, { capitalize: true }))} title={`Copier les ${label.toLowerCase()} visibles pour checklist`} /> + t(item.name, { capitalize: true }))} + title={`Utiliser les ${label.toLowerCase()} visibles pour une checklist`} + filteredResults + checklistTitle={label} + linkedToolbox={checklist.toolbox} + content={checklist.content} + onCreateChecklist={checklist.createChecklistFromResults} + />

{isMonsters ? "Filtrez les monstres par nom et faiblesses, puis consultez leurs dégâts détaillés." : "Filtrez la faune par nom et zones d'apparition."}

diff --git a/website/src/features/games/useGuideChecklist.js b/website/src/features/games/useGuideChecklist.js index a8b1eb5..46d08f9 100644 --- a/website/src/features/games/useGuideChecklist.js +++ b/website/src/features/games/useGuideChecklist.js @@ -38,6 +38,7 @@ export function useGuideChecklist(requestedGuideId) { content, toolbox, selected, hideCompleted, isCompleted, canToggle, toggleCompleted, guideId, available: entries !== null, lists: lists.filter((list) => list.count || list.id === selected?.id), isVisible: (key) => !hideCompleted || !isCompleted(key), + createChecklistFromResults: (checklist) => actions.createChecklistFromResults(gameId, checklist), associate: (moduleId) => guideChecklistStorage.setAssociation(guideId, moduleId ? { toolboxId: toolbox.id, moduleId, hideCompleted } : null), toggleHidden: () => guideChecklistStorage.setAssociation(guideId, { ...association, hideCompleted: !hideCompleted }) }; diff --git a/website/src/features/toolboxes/useToolboxActions.js b/website/src/features/toolboxes/useToolboxActions.js index 6345b0c..0dec52e 100644 --- a/website/src/features/toolboxes/useToolboxActions.js +++ b/website/src/features/toolboxes/useToolboxActions.js @@ -116,37 +116,65 @@ export function useToolboxActions({ store, notify, creationTemplate, setConfirmM }); } - function createChecklistFromList(gameId, list) { + async function createChecklistFromResults(gameId, checklist) { const toolboxId = store.links[gameId] || ""; const toolbox = store.toolboxes.find((item) => item.id === toolboxId); - if (!toolbox || !list) return false; + if (!toolbox || !checklist) return false; const moduleId = uid("mod"); - const sections = (list.categories || []).map((category) => ({ + const sourceSections = Array.isArray(checklist.sections) + ? checklist.sections + : [{ title: "", items: checklist.items || [] }]; + const sections = sourceSections.map((category) => ({ id: uid("section"), title: category.title || "", items: (category.items || []).map((item) => ({ id: uid("item"), - label: item.name, - qtyTarget: Math.max(1, Number.parseInt(item.quantity, 10) || 1), + label: typeof item === "object" && item !== null ? String(item.label || item.name || "").trim() : String(item || "").trim(), + qtyTarget: Math.max(1, Number.parseInt(item?.quantity ?? item?.qtyTarget, 10) || 1), qtyCurrent: 0 })).filter((item) => item.label) })).filter((section) => section.items.length); if (!sections.length) return false; + const title = String(checklist.title || "Checklist").trim() || "Checklist"; + const updatedAt = new Date().toISOString(); + const data = compactModuleDataForStorage("checklist", { sections }); + let created = false; + const saved = await store.persistToolboxSnapshot((current) => { + if (current.links[gameId] !== toolboxId || !current.toolboxes.some((item) => item.id === toolboxId)) return current; + created = true; + return { + ...current, + toolboxes: current.toolboxes.map((item) => item.id === toolboxId + ? { ...addModuleToToolboxSpace(item, { id: moduleId, type: "checklist", title }, item.activeSpaceId), updatedAt } + : item), + moduleData: { ...current.moduleData, [globalModuleKey(toolboxId, moduleId)]: data } + }; + }); + if (saved && created) notify("Checklist créée dans la toolbox associée."); + return Boolean(saved && created); + } + + function createChecklistFromList(gameId, list) { + const toolboxId = store.links[gameId] || ""; + const toolbox = store.toolboxes.find((item) => item.id === toolboxId); + if (!toolbox || !list) return false; + setConfirmModal({ title: "Créer une checklist", message: `Créer la checklist "${list.title || "Checklist"}" dans la toolbox "${toolbox.name}" ?`, confirmLabel: "Créer", onResolve: (confirmed) => { if (!confirmed) return; - const updatedAt = new Date().toISOString(); - store.setToolboxes((toolboxes) => toolboxes.map((item) => item.id === toolboxId - ? { ...addModuleToToolboxSpace(item, { id: moduleId, type: "checklist", title: list.title || "Checklist" }, item.activeSpaceId), updatedAt } - : item)); - store.updateModuleData(toolboxId, moduleId, { sections }, "checklist"); - notify("Checklist créée dans la toolbox associée."); + void createChecklistFromResults(gameId, { + title: list.title || "Checklist", + sections: (list.categories || []).map((category) => ({ + title: category.title || "", + items: category.items || [] + })) + }); } }); return true; @@ -263,6 +291,7 @@ export function useToolboxActions({ store, notify, creationTemplate, setConfirmM setLinkModalGameId, setDrawerGameId, setImage, + createChecklistFromResults, createChecklistFromList, addImageFiles, ensureModuleData: store.ensureModuleData, diff --git a/website/src/styles/games/_lists.scss b/website/src/styles/games/_lists.scss index 1fecee2..9de0fbb 100644 --- a/website/src/styles/games/_lists.scss +++ b/website/src/styles/games/_lists.scss @@ -19,6 +19,88 @@ min-height: 32px; } +.results-checklist-modal.nds-modal { + width: min(560px, calc(100vw - 32px)); +} + +.results-checklist-modal-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: var(--space-4); +} + +.results-checklist-modal-header h2, +.results-checklist-modal .results-checklist-modal-description, +.results-checklist-modal .results-checklist-modal-notice, +.results-checklist-modal .results-checklist-modal-error { + margin-bottom: 0; +} + +.results-checklist-modal-description { + margin-top: var(--space-3); + color: var(--color-text-secondary); + line-height: 1.55; +} + +.results-checklist-modal-summary, +.results-checklist-modal-target { + display: grid; + gap: 4px; + margin-top: var(--space-4); + padding: var(--space-3) var(--space-4); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + background: rgba(5, 7, 17, 0.32); +} + +.results-checklist-modal-summary span, +.results-checklist-modal-target span { + color: var(--color-text-muted); + font-size: var(--font-size-xs); + font-weight: 800; + text-transform: uppercase; +} + +.results-checklist-modal-summary strong, +.results-checklist-modal-target strong { + color: var(--color-text-primary); + overflow-wrap: anywhere; +} + +.results-checklist-modal-notice, +.results-checklist-modal-error { + margin-top: var(--space-4); +} + +.results-checklist-modal-actions { + margin-top: var(--space-5); +} + +.results-checklist-modal-actions .module-icon { + width: 20px; + height: 20px; + border: 0; + background: transparent; + box-shadow: none; +} + +.results-checklist-modal-actions .module-icon-svg { + width: 18px; + height: 18px; +} + +@media (max-width: 600px) { + .results-checklist-modal-actions { + align-items: stretch; + flex-direction: column; + } + + .results-checklist-modal-actions .nds-button { + width: 100%; + } +} + .game-list-help-link { width: fit-content; margin-top: var(--space-3);