From 51853245adb7a7afe458bfc696cdc748a60baacb Mon Sep 17 00:00:00 2001 From: Shinuwa Date: Tue, 29 Sep 2026 11:19:11 +0200 Subject: [PATCH] Improve calculator progress tracking and responsive layout --- DESIGN_SYSTEM.md | 1 + docs/STORAGE_SCHEMA.md | 10 +- e2e/calculator.spec.js | 43 +++++ e2e/style-layouts.spec.js | 22 ++- tests/helpers/data-validation.mjs | 4 + tests/toolbox-modules.test.mjs | 18 +- website/public/data/site.json | 9 +- .../toolboxes/modules/CalculatorModule.jsx | 161 ++++++++++++------ .../toolboxes/modules/calculatorUtils.js | 19 ++- .../toolboxes/modules/textImport/basic.js | 20 ++- .../toolboxes/storage/modules/calculator.js | 4 +- website/src/styles/_responsive.scss | 1 - .../styles/toolboxes/_calculator-results.scss | 26 +++ website/src/styles/toolboxes/_calculator.scss | 56 ++++++ .../toolboxes/shared-controls/_mix-tags.scss | 10 +- 15 files changed, 329 insertions(+), 75 deletions(-) diff --git a/DESIGN_SYSTEM.md b/DESIGN_SYSTEM.md index c4067d4..42d4dd6 100644 --- a/DESIGN_SYSTEM.md +++ b/DESIGN_SYSTEM.md @@ -210,6 +210,7 @@ Exemples actuels : Regles : - ratio de base 35 / 65 entre controles et resultats ; +- lorsque son outil mesure au plus 820 px de large, le calculateur empile la saisie au-dessus de la liste, avec Calcul/Actuel à gauche et Résultat à droite dans la carte ; sous 560 px, cette carte revient à une seule colonne ; - panneau gauche compact, sans hauteur artificielle inutile ; - panneau droit scrollable uniquement quand l'utilisateur active le switch dedie ; - hauteur scrollable cible : environ 450 px ; diff --git a/docs/STORAGE_SCHEMA.md b/docs/STORAGE_SCHEMA.md index c83298a..e3d21b6 100644 --- a/docs/STORAGE_SCHEMA.md +++ b/docs/STORAGE_SCHEMA.md @@ -1088,8 +1088,9 @@ Type : `calculator` "id": "calc2", "parentId": "calc1", "label": "Minerais de fer", - "value": 500, - "formula": "base*5" + "value": 480, + "formula": "base*5", + "current": 20 } ] } @@ -1101,7 +1102,8 @@ Stockage compact : - `parentId` est omis si vide. - `label` peut être vide. - `value` est numérique. -- `formula` est omis si vide ; dans un calcul enfant, le mot `base` référence la valeur recalculée du parent. +- `formula` est omis si vide ; dans un calcul enfant, le mot `base` référence la valeur restante recalculée du parent. +- `current` contient la quantité déjà acquise et est omis si elle vaut `0` ; `value` correspond au total produit par `formula`, moins `current`. - Si un `parentId` ne pointe vers aucune entrée existante, il est réinitialisé à vide. ## Outil Tableau @@ -1194,7 +1196,7 @@ Formats : - `checklist` : sections `# Catégorie`, lignes `Item: quantité cible`. - `links` : sections `# Catégorie`, lignes `Titre: https://...` ou URL seule; le suffixe optionnel `| category=Catégorie` reste accepté à l'import. - `counters` : lignes `Libellé: valeur`, valeurs négatives acceptées. -- `calculator` : lignes `Libellé: valeur | formula=base*5`, indentation de deux espaces pour les enfants, meta `@scrollResults: true`. Le suffixe `formula=...` est optionnel et les anciens exports `Libellé: valeur` restent acceptés. +- `calculator` : lignes `Libellé: valeur | formula=base*5 | current=20`, indentation de deux espaces pour les enfants, meta `@scrollResults: true`. Les suffixes `formula=...` et `current=...` sont optionnels, et les exports `Libellé: valeur` restent acceptés. - `table` : TSV pur accepté ; l'export peut ajouter `@size`, `@columns`, `@rows` puis `@tsv` pour préserver dimensions et intitulés. - `combos` : sections `# Catégorie`, lignes `Nom | device=... | kind:value > kind:value+kind:value`, avec suffixes `[hold]` ou `[2s]`. - `taskPlanner` : sections `# Catégorie`, lignes indentées `- daily Titre`, `- weekly Titre` ou `- unique Titre`, metas `@description`, `@dailyResetTime`, `@weeklyResetDay`, `@prerequisite`. diff --git a/e2e/calculator.spec.js b/e2e/calculator.spec.js index 3f52129..4bbe59f 100644 --- a/e2e/calculator.spec.js +++ b/e2e/calculator.spec.js @@ -54,6 +54,49 @@ test("parent edits recalculate descendants and survive reload; deleting a parent await expect(tool.getByText("Aucun résultat enregistré.", { exact: true })).toBeVisible(); }); +test("current values are subtracted from totals and kept during recalculation", async ({ page }) => { + const tool = await setup(page); + const expression = tool.getByRole("textbox", { name: "Calcul", exact: true }); + const current = tool.getByLabel("Actuel", { exact: true }); + const totalLabel = tool.getByText("Total", { exact: true }); + await expect(current).toHaveCount(0); + await expect(totalLabel).toHaveCount(0); + const subtractCurrent = tool.getByRole("button", { name: "Soustraire l'actuel", exact: true }); + await expect(subtractCurrent).toHaveAttribute("aria-pressed", "false"); + await subtractCurrent.click(); + await expect(subtractCurrent).toHaveAttribute("aria-pressed", "true"); + await expect(current).toBeFocused(); + await expect(totalLabel).toBeVisible(); + const expressionBounds = await expression.boundingBox(); + const currentBounds = await current.boundingBox(); + expect(currentBounds.x).toBeCloseTo(expressionBounds.x, 0); + expect(currentBounds.width).toBeCloseTo(expressionBounds.width, 0); + await expression.fill("10"); + await current.fill("2"); + await tool.getByLabel("Libellé", { exact: true }).fill("Parent"); + await expect(tool.locator(".calculator-result strong")).toHaveAttribute("aria-label", "8"); + await tool.getByRole("button", { name: "Enregistrer", exact: true }).click(); + await tool.getByRole("button", { name: "Ajouter un calcul enfant Parent", exact: true }).click(); + await expression.fill("base * 10"); + await subtractCurrent.click(); + await current.fill("20"); + await tool.getByLabel("Libellé", { exact: true }).fill("Child"); + await expect(tool.locator(".calculator-result strong")).toHaveAttribute("aria-label", "60"); + await tool.getByRole("button", { name: "Mettre à jour", exact: true }).click(); + await entry(tool, "Parent").getByTitle("Modifier", { exact: true }).click(); + await expect(current).toHaveValue("2"); + await expression.fill("20"); + await tool.getByRole("button", { name: "Mettre à jour", exact: true }).click(); + await expect(entry(tool, "Child").locator("strong")).toHaveAttribute("aria-label", "160"); + await page.reload(); + await entry(tool, "Child").getByTitle("Modifier", { exact: true }).click(); + await expect(current).toHaveValue("20"); + await expect(tool.locator(".calculator-result strong")).toHaveAttribute("aria-label", "160"); + await subtractCurrent.click(); + await expect(current).toHaveCount(0); + await expect(tool.locator(".calculator-result strong")).toHaveAttribute("aria-label", "180"); +}); + test("reset clears only the draft, new calculation returns to root, and clipboard and scroll preferences persist", async ({ page, context }) => { await context.grantPermissions(["clipboard-read", "clipboard-write"]); const tool = await setup(page); diff --git a/e2e/style-layouts.spec.js b/e2e/style-layouts.spec.js index 7f8fa75..7dc8ffe 100644 --- a/e2e/style-layouts.spec.js +++ b/e2e/style-layouts.spec.js @@ -7,6 +7,24 @@ async function columns(locator, count) { await expect.poll(async () => (await computed(locator, "grid-template-columns")).split(" ").length).toBe(count); } +test("calculator layout follows the tool width rather than its location", async ({ page }) => { + await page.setViewportSize({ width: 1280, height: 900 }); + await mountStyles(page, `
Formula
Current
Result
Entries
`); + const tool = page.locator('[data-module-type="calculator"]'); + const calculator = tool.locator(".calculator-module"); + const card = tool.locator(".calculator-card"); + await expect(tool).toHaveCSS("container-type", "inline-size"); + await columns(calculator, 2); + await columns(card, 1); + await tool.evaluate((element) => { element.style.width = "700px"; }); + await columns(calculator, 1); + await columns(card, 2); + await expect(tool.locator(".calculator-field")).toHaveCSS("grid-column-start", "1"); + await expect(tool.locator(".calculator-result")).toHaveCSS("grid-column-start", "2"); + await tool.evaluate((element) => { element.style.width = "480px"; }); + await columns(card, 1); +}); + for (const width of [1280, 900, 390]) { test(`home, about and library retain their responsive layout at ${width}px`, async ({ page }) => { await page.setViewportSize({ width, height: 900 }); @@ -66,7 +84,7 @@ for (const width of [1280, 390]) { await mountStyles(page, `
Game
Game
A
B
-
Formula
12
+
Formula
12
Equipment
Materials
Actions
Preferences
@@ -75,7 +93,7 @@ for (const width of [1280, 390]) { await expect(page.locator(".game-grid")).toHaveCSS("display", "grid"); await columns(page.locator(".modules"), width <= 760 ? 1 : 2); await expect(page.locator(".module-column").first()).toHaveCSS("display", width <= 760 ? "contents" : "grid"); - await columns(page.locator(".calculator-module"), width <= 760 ? 1 : 2); + await columns(page.locator(".layout-calculator .calculator-module"), width <= 820 ? 1 : 2); await expect(page.locator(".calculator-result")).toHaveCSS("min-height", "72px"); await expect(page.locator(".task-planner-module")).toHaveCSS("display", "grid"); await columns(page.locator(".task-planner-settings"), width <= 760 ? 1 : 2); diff --git a/tests/helpers/data-validation.mjs b/tests/helpers/data-validation.mjs index 2fe84fe..b678e26 100644 --- a/tests/helpers/data-validation.mjs +++ b/tests/helpers/data-validation.mjs @@ -574,6 +574,10 @@ export function validateSiteContent(site) { "toolboxes.modules.combos.deleteComboTitle", "toolboxes.modules.calculator.expressionLabel", "toolboxes.modules.calculator.expressionPlaceholder", + "toolboxes.modules.calculator.totalLabel", + "toolboxes.modules.calculator.currentLabel", + "toolboxes.modules.calculator.currentPlaceholder", + "toolboxes.modules.calculator.subtractCurrentTitle", "toolboxes.modules.calculator.resultLabel", "toolboxes.modules.calculator.labelLabel", "toolboxes.modules.calculator.labelPlaceholder", diff --git a/tests/toolbox-modules.test.mjs b/tests/toolbox-modules.test.mjs index 2d0d29a..2b156ed 100644 --- a/tests/toolbox-modules.test.mjs +++ b/tests/toolbox-modules.test.mjs @@ -6,7 +6,7 @@ import { exportModuleText, importModuleText, parseColonImportLines } from "../we import { buildInsertedMixTagValue, normalizeMixTagSpacing, normalizeMixTagText, removeMixTagAtOffset, tokenizeMixTagValue } from "../website/src/utils/mixTagsInputUtils.js"; import { getEffectiveGoalTreeProgressMode, getVisibleGoalTreeEdges } from "../website/src/features/toolboxes/modules/goalTreeDisplay.js"; import { calculateGoalTreeFormula, evaluateGoalTreeNodes } from "../website/src/features/toolboxes/modules/goalTreeUtils.js"; -import { calculateExpression, createStoredFormula, evaluateCalculatorEntries, getEntriesInTreeOrder } from "../website/src/features/toolboxes/modules/calculatorUtils.js"; +import { calculateExpression, calculateRemainingValue, createStoredFormula, evaluateCalculatorEntries, getEntriesInTreeOrder, parseCalculatorCurrent } from "../website/src/features/toolboxes/modules/calculatorUtils.js"; import { createDefaultDraft, getDateSpanDays, getDraftSelection, normalizeDraftAlertOffsets, normalizeDraftMonthlyRepeatBy } from "../website/src/features/toolboxes/modules/calendarDraft.js"; import { getMonthOccurrenceSegments, getWeekGridLayout, getWeekMinutePosition, layoutConcurrentOccurrences } from "../website/src/features/toolboxes/modules/calendarLayout.js"; import { appendChecklistItems, moveChecklistItem, moveChecklistItemToCategory, renameChecklistSection } from "../website/src/features/toolboxes/modules/checklistUtils.js"; @@ -479,7 +479,7 @@ test("text exchange imports table TSV and calculator hierarchy", () => { scrollResults: true, entries: [ { id: "a", label: "Ore", value: 100, formula: "100" }, - { id: "b", parentId: "a", label: "Shard", value: 500, formula: "base*5" } + { id: "b", parentId: "a", label: "Shard", value: 480, formula: "base*5", current: 20 } ] }); const calculatorImport = importModuleText("calculator", exportModuleText("calculator", calculator), context); @@ -487,21 +487,29 @@ test("text exchange imports table TSV and calculator hierarchy", () => { assert.equal(calculatorImport.data.scrollResults, true); assert.equal(calculatorImport.data.entries[1].parentId, calculatorImport.data.entries[0].id); assert.equal(calculatorImport.data.entries[1].formula, "base*5"); + assert.equal(calculatorImport.data.entries[1].current, 20); }); test("calculator helpers evaluate formulas and preserve tree order", () => { const entries = evaluateCalculatorEntries([ - { id: "root", label: "Root", value: 10, formula: "10" }, - { id: "child", parentId: "root", label: "Child", value: 0, formula: "base*3" }, + { id: "root", label: "Root", value: 8, formula: "10", current: 2 }, + { id: "child", parentId: "root", label: "Child", value: 0, formula: "base*10", current: 20 }, { id: "sibling", label: "Sibling", value: 2, formula: "1+1" } ]); assert.equal(calculateExpression("2 + 3 * 4"), 14); assert.equal(calculateExpression("base / 2", 10), 5); assert.equal(calculateExpression("window.alert(1)"), null); + assert.equal(calculateRemainingValue("base * 8", "20", 10), 60); + assert.equal(calculateRemainingValue("80", "20,5"), 59.5); + assert.equal(calculateRemainingValue("80", "invalid"), null); + assert.equal(parseCalculatorCurrent(""), 0); assert.equal(createStoredFormula("+5", 10), "base+5"); assert.equal(createStoredFormula("10*2", 10), "base*2"); - assert.deepEqual(entries.map((entry) => [entry.id, entry.value]), [["root", 10], ["child", 30], ["sibling", 2]]); + assert.deepEqual(entries.map((entry) => [entry.id, entry.value]), [["root", 8], ["child", 60], ["sibling", 2]]); assert.deepEqual(getEntriesInTreeOrder(entries).map((entry) => entry.id), ["root", "child", "sibling"]); + const compact = compactModuleDataForStorage("calculator", normalizeCalculatorData({ entries })); + assert.equal(compact.entries[0].current, 2); + assert.equal(compact.entries[2].current, undefined); }); test("calendar layout keeps week compaction and overlapping lanes deterministic", () => { diff --git a/website/public/data/site.json b/website/public/data/site.json index 1d9cf13..1c8b89b 100644 --- a/website/public/data/site.json +++ b/website/public/data/site.json @@ -844,6 +844,7 @@ "text": "Saisie d'une expression, calcul du résultat et sauvegarde avec un libellé.", "details": [ "Le champ expression accepte les opérations de base et calcule le résultat avant sauvegarde.", + "Le bouton − affiche le champ Actuel, qui soustrait automatiquement la quantité déjà acquise du total calculé.", "Le libellé sert à nommer un coût, une ressource, un total ou une conversion.", "Un résultat enregistré peut être renommé ou supprimé." ] @@ -869,7 +870,7 @@ ], "importFormat": { "text": "Une ligne par résultat. L'indentation de deux espaces conserve les liens parent/enfant.", - "example": "Lingot de champignite: 100\n Lingot de chlorophyte: 100 | formula=base\n Champignon luisant: 1500 | formula=base*15\nLingot de chlorophyte: 100\n Minerai de chlorophyte: 500 | formula=base*5" + "example": "Lingot de champignite: 80 | formula=100 | current=20\n Lingot de chlorophyte: 80 | formula=base\n Champignon luisant: 1200 | formula=base*15\nLingot de chlorophyte: 100\n Minerai de chlorophyte: 500 | formula=base*5" } }, "goalTree": { @@ -2305,7 +2306,7 @@ "deleteComboTitle": "Supprimer le combo" }, "calculator": { - "importPlaceholder": "@scrollResults: true\nLingots de fer: 100 | formula=100\n Minerais de fer: 500 | formula=base*5", + "importPlaceholder": "@scrollResults: true\nLingots de fer: 80 | formula=100 | current=20\n Minerais de fer: 400 | formula=base*5", "importOpenButton": "Importer du texte", "exportOpenButton": "Exporter en format texte", "importModalTitle": "Importer des résultats", @@ -2317,6 +2318,10 @@ "importError": "Format de calculateur invalide.", "expressionLabel": "Calcul", "expressionPlaceholder": "10*10", + "totalLabel": "Total", + "currentLabel": "Actuel", + "currentPlaceholder": "0", + "subtractCurrentTitle": "Soustraire l'actuel", "resultLabel": "Résultat", "labelLabel": "Libellé", "labelPlaceholder": "ex: Lingots de fer", diff --git a/website/src/features/toolboxes/modules/CalculatorModule.jsx b/website/src/features/toolboxes/modules/CalculatorModule.jsx index 9c5ffff..4afb250 100644 --- a/website/src/features/toolboxes/modules/CalculatorModule.jsx +++ b/website/src/features/toolboxes/modules/CalculatorModule.jsx @@ -7,7 +7,7 @@ import { CalculatorEntries, FormattedCalculatorNumber } from "./CalculatorEntrie import { TextExchangeActions } from "./TextExchangeActions.jsx"; import { BASE_TOKEN, - calculateExpression, + calculateRemainingValue, createStoredFormula, evaluateCalculatorEntries, formatDisplayResult, @@ -15,7 +15,8 @@ import { getDescendantIds, getEditableFormula, getEntriesInTreeOrder, - getResultLengthClass + getResultLengthClass, + parseCalculatorCurrent } from "./calculatorUtils.js"; function focusExpressionInputAtEnd(element) { @@ -41,6 +42,8 @@ export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpe const calculatedData = useMemo(() => ({ ...data, entries: calculatedEntries }), [data, calculatedEntries]); const textContent = context.moduleText?.calculator || {}; const [expression, setExpression] = useState(""); + const [current, setCurrent] = useState(""); + const [showCurrent, setShowCurrent] = useState(false); const [label, setLabel] = useState(""); const [activeParentId, setActiveParentId] = useState(""); const [editingEntryId, setEditingEntryId] = useState(""); @@ -48,12 +51,13 @@ export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpe const [copied, setCopied] = useState(false); const calculatorCardRef = useRef(null); const expressionInputRef = useRef(null); + const currentInputRef = useRef(null); const pendingExpressionFocusRef = useRef(false); const editingEntry = calculatedEntries.find((entry) => entry.id === editingEntryId); const formulaParentId = editingEntry ? editingEntry.parentId || "" : activeParentId; const formulaParent = calculatedEntries.find((entry) => entry.id === formulaParentId); const formMode = editingEntry ? "edit" : formulaParent ? "child" : "root"; - const result = useMemo(() => calculateExpression(expression, formulaParent?.value), [formulaParent?.value, expression]); + const result = useMemo(() => calculateRemainingValue(expression, current, formulaParent?.value), [current, formulaParent?.value, expression]); const scopeParentId = (parentId = "") => `${moduleId}:${parentId}`; const reorder = useGroupedReorder({ namespace: "calculator", @@ -99,13 +103,16 @@ export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpe if (result == null) return; const cleanLabel = label.trim(); const storedFormula = createStoredFormula(expression, formulaParent?.value); + const storedCurrent = parseCalculatorCurrent(current); + if (storedCurrent == null) return; if (editingEntry) { save(calculatedEntries.map((entry) => entry.id === editingEntry.id ? { ...entry, label: cleanLabel, value: result, - formula: storedFormula + formula: storedFormula, + current: storedCurrent } : entry)); setLabel(cleanLabel); return; @@ -118,10 +125,13 @@ export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpe parentId: formulaParentId, label: cleanLabel, value: result, - formula: storedFormula + formula: storedFormula, + current: storedCurrent } ]); setExpression(formulaParent ? BASE_TOKEN : ""); + setCurrent(""); + setShowCurrent(false); setLabel(""); } @@ -129,6 +139,8 @@ export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpe setEditingEntryId(entry.id); setActiveParentId(""); setExpression(getEditableFormula(entry)); + setCurrent(entry.current ? formatResult(entry.current) : ""); + setShowCurrent(Boolean(entry.current)); setLabel(entry.label || ""); } @@ -141,7 +153,8 @@ export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpe parentId: entry.id, label: "", value: Number.isFinite(entry.value) ? entry.value : 0, - formula: BASE_TOKEN + formula: BASE_TOKEN, + current: 0 } ]); setEditingEntryId(""); @@ -149,6 +162,8 @@ export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpe setEditingEntryId(childId); setActiveParentId(""); setExpression(BASE_TOKEN); + setCurrent(""); + setShowCurrent(false); setLabel(""); } @@ -162,12 +177,16 @@ export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpe setEditingEntryId(""); setLabel(""); setExpression(""); + setCurrent(""); + setShowCurrent(false); } save(calculatedEntries.filter((entry) => !deletedIds.has(entry.id))); } function resetCalculator() { setExpression(""); + setCurrent(""); + setShowCurrent(false); setLabel(""); setActiveParentId(""); setEditingEntryId(""); @@ -177,9 +196,21 @@ export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpe setActiveParentId(""); setEditingEntryId(""); setExpression(""); + setCurrent(""); + setShowCurrent(false); setLabel(""); } + function toggleCurrent() { + if (showCurrent) { + setCurrent(""); + setShowCurrent(false); + return; + } + setShowCurrent(true); + window.requestAnimationFrame(() => currentInputRef.current?.focus()); + } + async function copyChecklistImport() { const text = getEntriesInTreeOrder(calculatedEntries) .map((entry) => `${entry.label || formatResult(entry.value)}:${formatResult(entry.value)}`) @@ -201,61 +232,91 @@ export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpe
{textContent.expressionLabel || "Calcul"} - {formulaParent ? ( - { - if (event.key !== "Enter") return; - event.preventDefault(); - event.currentTarget.closest("form")?.requestSubmit(); - }} - formulaSpacing - insertToken={{ - value: "base", - label: textContent.baseTokenLabel || "Valeur du parent", - icon: "parent", - title: textContent.insertBaseTitle || "Ajouter la valeur du parent" - }} - placeholder={textContent.formulaPlaceholder || "base * 3"} - tokens={[{ - value: "base", - label: textContent.baseTokenLabel || "Valeur du parent" - }]} - value={expression} - /> - ) : ( - + {showCurrent && {textContent.totalLabel || "Total"}} + {formulaParent ? ( + { + if (event.key !== "Enter") return; + event.preventDefault(); + event.currentTarget.closest("form")?.requestSubmit(); + }} + formulaSpacing + insertToken={{ + value: "base", + label: textContent.baseTokenLabel || "Valeur du parent", + icon: "parent", + title: textContent.insertBaseTitle || "Ajouter la valeur du parent" + }} + placeholder={textContent.formulaPlaceholder || "base * 3"} + tokens={[{ + value: "base", + label: textContent.baseTokenLabel || "Valeur du parent" + }]} + value={expression} + /> + ) : ( + setExpression(event.target.value)} + onKeyDown={(event) => { + if (event.key !== "Enter") return; + event.preventDefault(); + event.currentTarget.closest("form")?.requestSubmit(); + }} + inputMode="decimal" + placeholder={textContent.expressionPlaceholder || "10*10"} + value={expression} + /> + )} + +
+ {showCurrent && ( +
+ {textContent.currentLabel || "Actuel"} + setCurrent(event.target.value)} + placeholder={textContent.currentPlaceholder || "0"} + /> +
+ )}
{textContent.resultLabel || "Résultat"}
-