diff --git a/tests/helpers/data-validation.mjs b/tests/helpers/data-validation.mjs
index 7b70265..f457a2f 100644
--- a/tests/helpers/data-validation.mjs
+++ b/tests/helpers/data-validation.mjs
@@ -67,6 +67,16 @@ export function validateSiteContent(site) {
"toolboxes.summary",
"toolboxes.storageHelp.title",
"toolboxes.storageHelp.text",
+ "toolboxes.modules.notepad.placeholder",
+ "toolboxes.modules.checklist.itemPlaceholder",
+ "toolboxes.modules.checklist.importPlaceholder",
+ "toolboxes.modules.links.titlePlaceholder",
+ "toolboxes.modules.links.urlPlaceholder",
+ "toolboxes.modules.links.importPlaceholder",
+ "toolboxes.modules.counters.labelPlaceholder",
+ "toolboxes.modules.calculator.expressionPlaceholder",
+ "toolboxes.modules.calculator.labelPlaceholder",
+ "toolboxes.modules.calculator.scrollableTitle",
"toolboxes.emptyTitle",
"toolboxes.emptyText"
].forEach((path) => assertNonEmptyString(site, path));
diff --git a/tests/static-smoke.test.mjs b/tests/static-smoke.test.mjs
index c27dad0..b7bb879 100644
--- a/tests/static-smoke.test.mjs
+++ b/tests/static-smoke.test.mjs
@@ -26,6 +26,7 @@ test("react application defines the expected local toolbox primitives", async ()
const iconComponent = await readFile("website/src/components/Icon.jsx", "utf8");
const gamesPage = await readFile("website/src/features/games/GamesPage.jsx", "utf8");
const gameRoute = await readFile("website/src/features/games/GameRoute.jsx", "utf8");
+ const checklistCopyButton = await readFile("website/src/features/games/CopyChecklistItemsButton.jsx", "utf8");
const gameLoaders = await readFile("website/src/features/games/loaders.js", "utf8");
const gameFiltersPanel = await readFile("website/src/features/games/GameFiltersPanel.jsx", "utf8");
const diablo4Page = await readFile("website/src/features/games/diablo4/Diablo4Page.jsx", "utf8");
@@ -42,10 +43,12 @@ test("react application defines the expected local toolbox primitives", async ()
const damageTable = await readFile("website/src/features/games/mhwilds/cards/DamageTable.jsx", "utf8");
const moduleRegistry = await readFile("website/src/features/toolboxes/modules/index.jsx", "utf8");
const notepadModule = await readFile("website/src/features/toolboxes/modules/NotepadModule.jsx", "utf8");
+ const calculatorModule = await readFile("website/src/features/toolboxes/modules/CalculatorModule.jsx", "utf8");
const checklistModule = await readFile("website/src/features/toolboxes/modules/ChecklistModule.jsx", "utf8");
const screenshotsModule = await readFile("website/src/features/toolboxes/modules/ScreenshotsModule.jsx", "utf8");
const linksModule = await readFile("website/src/features/toolboxes/modules/LinksModule.jsx", "utf8");
const countersModule = await readFile("website/src/features/toolboxes/modules/CountersModule.jsx", "utf8");
+ const textImport = await readFile("website/src/features/toolboxes/modules/textImport.js", "utf8");
const reorderHook = await readFile("website/src/hooks/usePointerReorder.js", "utf8");
assert.match(source, /sokkog:toolboxes/);
@@ -64,11 +67,18 @@ test("react application defines the expected local toolbox primitives", async ()
assert.match(styleHome, /\.dialogue-line i/);
assert.match(styleHome, /font-style: italic/);
assert.match(styleToolboxes, /\.module-icon-abacus/);
+ assert.match(styleToolboxes, /\.module-icon-calculator/);
+ assert.match(styleToolboxes, /\.calculator-module/);
assert.match(styleToolboxes, /\.toolbox-icon-picker/);
assert.match(styleToolboxes, /\.toolbox-game-icon/);
+ assert.match(styleToolboxes, /\.toolbox-hero-link/);
assert.match(styleToolboxes, /\.toolbox-actions-row/);
+ assert.match(styleToolboxes, /\.text-import-form/);
assert.match(styleIcons, /ui-icon-drag/);
assert.match(styleIcons, /ui-icon-dropdown/);
+ assert.match(styleIcons, /ui-icon-enter/);
+ assert.match(styleIcons, /ui-icon-calculator/);
+ assert.match(styleIcons, /ui-icon-scrollable/);
assert.match(styleIcons, /ui-icon-flip/);
assert.match(styleIcons, /ui-icon-add/);
assert.match(styleMhwilds, /\.mhwilds-card/);
@@ -91,6 +101,10 @@ test("react application defines the expected local toolbox primitives", async ()
assert.match(gameRoute, /export function GameRoute/);
assert.match(gameRoute, /MhwildsPage/);
assert.match(gameRoute, /Diablo4Page/);
+ assert.match(checklistCopyButton, /formatChecklistImportItems/);
+ assert.match(checklistCopyButton, /\$\{label\}:1/);
+ assert.match(checklistCopyButton, /navigator\.clipboard\.writeText/);
+ assert.match(checklistCopyButton, /results-copy-button/);
assert.match(gameLoaders, /export async function loadMhwildsData/);
assert.match(gameLoaders, /export async function loadDiablo4Data/);
assert.match(gameLoaders, /\/data\/diablo4\/affixes_types\.json/);
@@ -100,6 +114,7 @@ test("react application defines the expected local toolbox primitives", async ()
assert.match(diablo4Page, /export function Diablo4Page/);
assert.match(diablo4Overview, /export function Diablo4Overview/);
assert.match(diablo4Listing, /export function Diablo4Listing/);
+ assert.match(diablo4Listing, /CopyChecklistItemsButton/);
assert.match(diablo4Filters, /export function Diablo4Filters/);
assert.match(diablo4Filters, /GameFiltersPanel/);
assert.match(diablo4AffixCard, /export function Diablo4AffixCard/);
@@ -108,6 +123,7 @@ test("react application defines the expected local toolbox primitives", async ()
assert.match(diablo4Utils, /getCategoryIconStyle/);
assert.match(mhwildsPage, /export function MhwildsPage/);
assert.match(mhwildsListing, /export function MhwildsListing/);
+ assert.match(mhwildsListing, /CopyChecklistItemsButton/);
assert.match(mhwildsListing, /endemic_life/);
assert.match(mhwildsFilters, /export function MhwildsFilters/);
assert.match(mhwildsFilters, /GameFiltersPanel/);
@@ -124,13 +140,32 @@ test("react application defines the expected local toolbox primitives", async ()
assert.match(moduleRegistry, /screenshots:/);
assert.match(moduleRegistry, /links:/);
assert.match(moduleRegistry, /counters:/);
+ assert.match(moduleRegistry, /calculator:/);
assert.match(moduleRegistry, /editable: false/);
assert.match(moduleRegistry, /module-edit-button/);
assert.match(notepadModule, /export function NotepadModule/);
+ assert.match(calculatorModule, /export function CalculatorModule/);
+ assert.match(calculatorModule, /calculateExpression/);
+ assert.match(calculatorModule, /activeParentId/);
+ assert.match(calculatorModule, /parentId/);
+ assert.match(calculatorModule, /scrollResults/);
+ assert.match(calculatorModule, /calculator-scroll-toggle/);
+ assert.match(calculatorModule, /EditableCalculatorLabel/);
+ assert.match(calculatorModule, /calculator-entry-label/);
+ assert.match(calculatorModule, /copyChecklistImport/);
+ assert.match(calculatorModule, /resetCalculator/);
+ assert.match(calculatorModule, /Quantité non modifiable/);
+ assert.doesNotMatch(styleToolboxes, /lecture seule/);
+ assert.match(calculatorModule, /entry\.label \|\| formatResult\(entry\.value\)/);
+ assert.match(calculatorModule, /ResizeObserver/);
+ assert.match(calculatorModule, /--calculator-scroll-height/);
+ assert.match(calculatorModule, /CalculatorEntries/);
assert.match(checklistModule, /export function ChecklistModule/);
assert.match(screenshotsModule, /export function ScreenshotsModule/);
assert.match(linksModule, /export function LinksModule/);
assert.match(countersModule, /export function CountersModule/);
+ assert.match(textImport, /export function parseColonImportLines/);
+ assert.match(textImport, /line\.indexOf\(":\"\)/);
assert.match(checklistModule, /editing &&/);
assert.match(screenshotsModule, /editing &&/);
assert.match(linksModule, /editing &&/);
@@ -163,6 +198,11 @@ test("react application defines the expected local toolbox primitives", async ()
assert.match(source, /value\.split\("\\n"\)/);
assert.match(source, / {
await readFile("website/public/static/icons/columns.svg", "utf8");
await readFile("website/public/static/icons/copy.svg", "utf8");
await readFile("website/public/static/icons/abacus.svg", "utf8");
+ await readFile("website/public/static/icons/calculator.svg", "utf8");
+ await readFile("website/public/static/icons/enter.svg", "utf8");
await readFile("website/public/static/icons/export.svg", "utf8");
+ await readFile("website/public/static/icons/scrollable.svg", "utf8");
await readFile("website/public/static/icons/notepad.svg", "utf8");
await readFile("website/public/static/icons/picture.svg", "utf8");
await readFile("website/public/static/icons/rows.svg", "utf8");
diff --git a/tests/toolbox-modules.test.mjs b/tests/toolbox-modules.test.mjs
new file mode 100644
index 0000000..4e9a01a
--- /dev/null
+++ b/tests/toolbox-modules.test.mjs
@@ -0,0 +1,16 @@
+import { test } from "node:test";
+import assert from "node:assert/strict";
+import { parseColonImportLines } from "../website/src/features/toolboxes/modules/textImport.js";
+
+test("colon text import keeps urls intact after the first separator", () => {
+ assert.deepEqual(parseColonImportLines("Build:https://example.com/build?class=rogue\nPotion:10"), [
+ { label: "Build", value: "https://example.com/build?class=rogue" },
+ { label: "Potion", value: "10" }
+ ]);
+});
+
+test("colon text import accepts labels without value", () => {
+ assert.deepEqual(parseColonImportLines("Simple item"), [
+ { label: "Simple item", value: "" }
+ ]);
+});
diff --git a/website/public/data/site.json b/website/public/data/site.json
index 5bf00f6..fe40310 100644
--- a/website/public/data/site.json
+++ b/website/public/data/site.json
@@ -98,6 +98,28 @@
"Faites un export global régulier pour garder une sauvegarde."
]
},
+ "modules": {
+ "notepad": {
+ "placeholder": "Notes rapides..."
+ },
+ "checklist": {
+ "itemPlaceholder": "Nouvel item",
+ "importPlaceholder": "Importer plusieurs items...\nPotion:10\nMéga potion:5"
+ },
+ "links": {
+ "titlePlaceholder": "Nom du lien",
+ "urlPlaceholder": "https://...",
+ "importPlaceholder": "Importer plusieurs liens...\nBuild:https://example.com/build?class=rogue\nMap:https://example.com/map"
+ },
+ "counters": {
+ "labelPlaceholder": "Nom du compteur"
+ },
+ "calculator": {
+ "expressionPlaceholder": "10*10",
+ "labelPlaceholder": "ex: Lingots de fer",
+ "scrollableTitle": "Liste scrollable"
+ }
+ },
"emptyTitle": "Aucune toolbox",
"emptyText": "Créez une première toolbox pour stocker vos outils dans ce navigateur."
}
diff --git a/website/public/static/icons/calculator.svg b/website/public/static/icons/calculator.svg
new file mode 100644
index 0000000..213ccd4
--- /dev/null
+++ b/website/public/static/icons/calculator.svg
@@ -0,0 +1,4 @@
+
+
\ No newline at end of file
diff --git a/website/public/static/icons/enter.svg b/website/public/static/icons/enter.svg
new file mode 100644
index 0000000..2858f40
--- /dev/null
+++ b/website/public/static/icons/enter.svg
@@ -0,0 +1,8 @@
+
+
+
\ No newline at end of file
diff --git a/website/public/static/icons/scrollable.svg b/website/public/static/icons/scrollable.svg
new file mode 100644
index 0000000..dd020f3
--- /dev/null
+++ b/website/public/static/icons/scrollable.svg
@@ -0,0 +1,16 @@
+
+
+
+
\ No newline at end of file
diff --git a/website/public/static/img/diablo4/wing.svg b/website/public/static/img/diablo4/wing.svg
index 810aedc..39869f8 100644
--- a/website/public/static/img/diablo4/wing.svg
+++ b/website/public/static/img/diablo4/wing.svg
@@ -1,2 +1,3 @@
-
-
\ No newline at end of file
+
diff --git a/website/src/features/games/CopyChecklistItemsButton.jsx b/website/src/features/games/CopyChecklistItemsButton.jsx
new file mode 100644
index 0000000..2ed20f7
--- /dev/null
+++ b/website/src/features/games/CopyChecklistItemsButton.jsx
@@ -0,0 +1,43 @@
+import { useState } from "react";
+import { Icon } from "../../components/Icon.jsx";
+
+async function copyText(value) {
+ try {
+ await navigator.clipboard.writeText(value);
+ return true;
+ } catch {
+ return false;
+ }
+}
+
+export function formatChecklistImportItems(labels) {
+ return labels
+ .map((label) => String(label || "").trim())
+ .filter(Boolean)
+ .map((label) => `${label}:1`)
+ .join("\n");
+}
+
+export function CopyChecklistItemsButton({ labels, title = "Copier pour checklist" }) {
+ const [copied, setCopied] = useState(false);
+ const text = formatChecklistImportItems(labels);
+
+ async function copyItems() {
+ if (!text || !await copyText(text)) return;
+ setCopied(true);
+ window.setTimeout(() => setCopied(false), 1400);
+ }
+
+ return (
+
+ );
+}
diff --git a/website/src/features/games/diablo4/Diablo4Listing.jsx b/website/src/features/games/diablo4/Diablo4Listing.jsx
index 244ba45..5bc27a6 100644
--- a/website/src/features/games/diablo4/Diablo4Listing.jsx
+++ b/website/src/features/games/diablo4/Diablo4Listing.jsx
@@ -1,4 +1,5 @@
import { useMemo } from "react";
+import { CopyChecklistItemsButton } from "../CopyChecklistItemsButton.jsx";
import { Diablo4AffixCard } from "./Diablo4AffixCard.jsx";
import { Diablo4Filters } from "./Diablo4Filters.jsx";
import { getCategoryLabel, getFilteredDiablo4Affixes } from "./utils.js";
@@ -19,6 +20,7 @@ export function Diablo4Listing({ diablo4, filters, setFilters }) {
Affixes
{visible.length} / {diablo4.affixes.length}
+ affix.label)} title="Copier les affixes visibles pour checklist" />
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 2a3a8fe..97e511a 100644
--- a/website/src/features/games/mhwilds/MhwildsListing.jsx
+++ b/website/src/features/games/mhwilds/MhwildsListing.jsx
@@ -1,6 +1,7 @@
import { useMemo } from "react";
import { EndemicCard } from "./cards/EndemicCard.jsx";
import { MonsterCard } from "./cards/MonsterCard.jsx";
+import { CopyChecklistItemsButton } from "../CopyChecklistItemsButton.jsx";
import { MhwildsFilters } from "./MhwildsFilters.jsx";
import { getFilteredMhwildsItems, getMhwildsFilterKey, getUniqueConditionValues } from "./utils.js";
@@ -22,6 +23,7 @@ export function MhwildsListing({ category, mhwilds, filters, setFilters, t }) {
{label}
{visible.length} / {items.length}
+ t(item.name, { capitalize: true }))} title={`Copier les ${label.toLowerCase()} visibles pour checklist`} />
{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."}