From 0db35a0d2cef0969be63b29c50e79f189b3cf35f Mon Sep 17 00:00:00 2001 From: Shinuwa Date: Thu, 23 Jul 2026 21:06:05 +0200 Subject: [PATCH] style / tools optis & new tools calculator --- tests/helpers/data-validation.mjs | 10 + tests/static-smoke.test.mjs | 50 ++ tests/toolbox-modules.test.mjs | 16 + website/public/data/site.json | 22 + website/public/static/icons/calculator.svg | 4 + website/public/static/icons/enter.svg | 8 + website/public/static/icons/scrollable.svg | 16 + website/public/static/img/diablo4/wing.svg | 5 +- .../games/CopyChecklistItemsButton.jsx | 43 ++ .../features/games/diablo4/Diablo4Listing.jsx | 2 + .../features/games/mhwilds/MhwildsListing.jsx | 2 + .../toolboxes/modules/CalculatorModule.jsx | 243 +++++++++ .../toolboxes/modules/ChecklistModule.jsx | 49 +- .../toolboxes/modules/CountersModule.jsx | 3 +- .../toolboxes/modules/LinksModule.jsx | 38 +- .../toolboxes/modules/NotepadModule.jsx | 3 +- .../src/features/toolboxes/modules/index.jsx | 4 +- .../features/toolboxes/modules/textImport.js | 15 + website/src/main.jsx | 91 +++- website/src/styles/_games.scss | 6 + website/src/styles/_icons.scss | 15 + website/src/styles/_responsive.scss | 12 + website/src/styles/_toolboxes.scss | 473 +++++++++++++++++- 23 files changed, 1106 insertions(+), 24 deletions(-) create mode 100644 tests/toolbox-modules.test.mjs create mode 100644 website/public/static/icons/calculator.svg create mode 100644 website/public/static/icons/enter.svg create mode 100644 website/public/static/icons/scrollable.svg create mode 100644 website/src/features/games/CopyChecklistItemsButton.jsx create mode 100644 website/src/features/toolboxes/modules/CalculatorModule.jsx create mode 100644 website/src/features/toolboxes/modules/textImport.js 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."}

diff --git a/website/src/features/toolboxes/modules/CalculatorModule.jsx b/website/src/features/toolboxes/modules/CalculatorModule.jsx new file mode 100644 index 0000000..4e87cb8 --- /dev/null +++ b/website/src/features/toolboxes/modules/CalculatorModule.jsx @@ -0,0 +1,243 @@ +import { useLayoutEffect, useMemo, useRef, useState } from "react"; +import { Icon } from "../../../components/Icon.jsx"; + +function calculateExpression(expression) { + const normalized = String(expression || "").replaceAll(",", ".").trim(); + if (!normalized || !/^[\d\s+\-*/().]+$/.test(normalized)) return null; + + try { + const value = Function(`"use strict"; return (${normalized})`)(); + return Number.isFinite(value) ? value : null; + } catch { + return null; + } +} + +function formatResult(value) { + if (!Number.isFinite(value)) return ""; + return Number.parseFloat(value.toFixed(6)).toString(); +} + +function getChildren(entries, parentId) { + return entries.filter((entry) => (entry.parentId || "") === parentId); +} + +function getDescendantIds(entries, parentId) { + const children = getChildren(entries, parentId); + return children.flatMap((child) => [child.id, ...getDescendantIds(entries, child.id)]); +} + +function getEntriesInTreeOrder(entries, parentId = "") { + return getChildren(entries, parentId).flatMap((entry) => [entry, ...getEntriesInTreeOrder(entries, entry.id)]); +} + +export function CalculatorModule({ toolboxId, moduleId, context }) { + const data = context.normalizeCalculatorData(context.getModuleData(toolboxId, moduleId, { entries: [] })); + const textContent = context.moduleText?.calculator || {}; + const [expression, setExpression] = useState(""); + const [label, setLabel] = useState(""); + const [activeParentId, setActiveParentId] = useState(""); + const [calculatorHeight, setCalculatorHeight] = useState(0); + const [copied, setCopied] = useState(false); + const calculatorCardRef = useRef(null); + const result = useMemo(() => calculateExpression(expression), [expression]); + const activeParent = data.entries.find((entry) => entry.id === activeParentId); + + useLayoutEffect(() => { + const element = calculatorCardRef.current; + if (!element) return undefined; + + function updateHeight() { + setCalculatorHeight(Math.ceil(element.getBoundingClientRect().height)); + } + + updateHeight(); + const observer = new ResizeObserver(updateHeight); + observer.observe(element); + return () => observer.disconnect(); + }, []); + + function save(entries) { + context.setModuleData(toolboxId, moduleId, { ...data, entries }); + } + + function setScrollResults(scrollResults) { + context.setModuleData(toolboxId, moduleId, { ...data, scrollResults }); + } + + function saveResult(event) { + event.preventDefault(); + if (result == null) return; + const cleanLabel = label.trim(); + + save([ + ...data.entries, + { + id: context.uid("calc"), + parentId: activeParentId, + label: cleanLabel, + value: result + } + ]); + setExpression(""); + setLabel(""); + } + + function useEntry(entry) { + setExpression(formatResult(entry.value)); + setActiveParentId(entry.id); + } + + function deleteEntry(entryId) { + const deletedIds = new Set([entryId, ...getDescendantIds(data.entries, entryId)]); + if (deletedIds.has(activeParentId)) setActiveParentId(""); + save(data.entries.filter((entry) => !deletedIds.has(entry.id))); + } + + function renameEntry(entryId, label) { + const cleanLabel = label.trim(); + save(data.entries.map((entry) => entry.id === entryId ? { ...entry, label: cleanLabel } : entry)); + } + + function resetCalculator() { + setExpression(""); + setLabel(""); + setActiveParentId(""); + } + + async function copyChecklistImport() { + const text = getEntriesInTreeOrder(data.entries) + .map((entry) => `${entry.label || formatResult(entry.value)}:${formatResult(entry.value)}`) + .join("\n"); + if (!text || !await context.copyText(text)) return; + setCopied(true); + window.setTimeout(() => setCopied(false), 1400); + } + + return ( +
+
+
+ +
+ Résultat + {result == null ? "-" : formatResult(result)} +
+ + +
+
+
+
+ {activeParent && ( + + )} + + +