Improve calculator progress tracking and responsive layout
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
3f86d863ac
commit
51853245ad
15 changed files with 329 additions and 75 deletions
|
|
@ -210,6 +210,7 @@ Exemples actuels :
|
||||||
Regles :
|
Regles :
|
||||||
|
|
||||||
- ratio de base 35 / 65 entre controles et resultats ;
|
- 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 gauche compact, sans hauteur artificielle inutile ;
|
||||||
- panneau droit scrollable uniquement quand l'utilisateur active le switch dedie ;
|
- panneau droit scrollable uniquement quand l'utilisateur active le switch dedie ;
|
||||||
- hauteur scrollable cible : environ 450 px ;
|
- hauteur scrollable cible : environ 450 px ;
|
||||||
|
|
|
||||||
|
|
@ -1088,8 +1088,9 @@ Type : `calculator`
|
||||||
"id": "calc2",
|
"id": "calc2",
|
||||||
"parentId": "calc1",
|
"parentId": "calc1",
|
||||||
"label": "Minerais de fer",
|
"label": "Minerais de fer",
|
||||||
"value": 500,
|
"value": 480,
|
||||||
"formula": "base*5"
|
"formula": "base*5",
|
||||||
|
"current": 20
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -1101,7 +1102,8 @@ Stockage compact :
|
||||||
- `parentId` est omis si vide.
|
- `parentId` est omis si vide.
|
||||||
- `label` peut être vide.
|
- `label` peut être vide.
|
||||||
- `value` est numérique.
|
- `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.
|
- Si un `parentId` ne pointe vers aucune entrée existante, il est réinitialisé à vide.
|
||||||
|
|
||||||
## Outil Tableau
|
## Outil Tableau
|
||||||
|
|
@ -1194,7 +1196,7 @@ Formats :
|
||||||
- `checklist` : sections `# Catégorie`, lignes `Item: quantité cible`.
|
- `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.
|
- `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.
|
- `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.
|
- `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]`.
|
- `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`.
|
- `taskPlanner` : sections `# Catégorie`, lignes indentées `- daily Titre`, `- weekly Titre` ou `- unique Titre`, metas `@description`, `@dailyResetTime`, `@weeklyResetDay`, `@prerequisite`.
|
||||||
|
|
|
||||||
|
|
@ -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();
|
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 }) => {
|
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"]);
|
await context.grantPermissions(["clipboard-read", "clipboard-write"]);
|
||||||
const tool = await setup(page);
|
const tool = await setup(page);
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,24 @@ async function columns(locator, count) {
|
||||||
await expect.poll(async () => (await computed(locator, "grid-template-columns")).split(" ").length).toBe(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, `<article class="module" data-module-type="calculator" style="width: 900px"><div class="calculator-module"><form class="calculator-form"><div class="calculator-card"><div class="calculator-field">Formula</div><div class="calculator-current-field">Current</div><div class="calculator-result">Result</div><label class="calculator-label-field">Label</label><button class="calculator-save-button">Save</button></div></form><div>Entries</div></div></article>`);
|
||||||
|
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]) {
|
for (const width of [1280, 900, 390]) {
|
||||||
test(`home, about and library retain their responsive layout at ${width}px`, async ({ page }) => {
|
test(`home, about and library retain their responsive layout at ${width}px`, async ({ page }) => {
|
||||||
await page.setViewportSize({ width, height: 900 });
|
await page.setViewportSize({ width, height: 900 });
|
||||||
|
|
@ -66,7 +84,7 @@ for (const width of [1280, 390]) {
|
||||||
await mountStyles(page, `<main>
|
await mountStyles(page, `<main>
|
||||||
<div class="game-layout"><aside class="game-filters"><div class="filter-panel nds-filter-panel">Filter</div></aside><div class="game-grid"><article>Game</article><article>Game</article></div></div>
|
<div class="game-layout"><aside class="game-filters"><div class="filter-panel nds-filter-panel">Filter</div></aside><div class="game-grid"><article>Game</article><article>Game</article></div></div>
|
||||||
<div class="modules" data-layout-cols="2"><div class="module-column"><article class="module">A</article></div><div class="module-column"><article class="module">B</article></div></div>
|
<div class="modules" data-layout-cols="2"><div class="module-column"><article class="module">A</article></div><div class="module-column"><article class="module">B</article></div></div>
|
||||||
<div class="calculator-module"><div>Formula</div><div class="calculator-result"><strong>12</strong></div></div>
|
<article class="module layout-calculator" data-module-type="calculator"><div class="calculator-module"><div>Formula</div><div class="calculator-result"><strong>12</strong></div></div></article>
|
||||||
<div class="task-planner-module"><div class="task-planner-settings"><label>Reset</label><label>Time</label></div></div>
|
<div class="task-planner-module"><div class="task-planner-settings"><label>Reset</label><label>Time</label></div></div>
|
||||||
<div class="equipment-planner-summary"><div class="equipment-planner-summary-source-grid"><div>Equipment</div><div>Materials</div></div></div>
|
<div class="equipment-planner-summary"><div class="equipment-planner-summary-source-grid"><div>Equipment</div><div>Materials</div></div></div>
|
||||||
<div class="toolbox-actions-row"><div>Actions</div><div>Preferences</div></div>
|
<div class="toolbox-actions-row"><div>Actions</div><div>Preferences</div></div>
|
||||||
|
|
@ -75,7 +93,7 @@ for (const width of [1280, 390]) {
|
||||||
await expect(page.locator(".game-grid")).toHaveCSS("display", "grid");
|
await expect(page.locator(".game-grid")).toHaveCSS("display", "grid");
|
||||||
await columns(page.locator(".modules"), width <= 760 ? 1 : 2);
|
await columns(page.locator(".modules"), width <= 760 ? 1 : 2);
|
||||||
await expect(page.locator(".module-column").first()).toHaveCSS("display", width <= 760 ? "contents" : "grid");
|
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(".calculator-result")).toHaveCSS("min-height", "72px");
|
||||||
await expect(page.locator(".task-planner-module")).toHaveCSS("display", "grid");
|
await expect(page.locator(".task-planner-module")).toHaveCSS("display", "grid");
|
||||||
await columns(page.locator(".task-planner-settings"), width <= 760 ? 1 : 2);
|
await columns(page.locator(".task-planner-settings"), width <= 760 ? 1 : 2);
|
||||||
|
|
|
||||||
|
|
@ -574,6 +574,10 @@ export function validateSiteContent(site) {
|
||||||
"toolboxes.modules.combos.deleteComboTitle",
|
"toolboxes.modules.combos.deleteComboTitle",
|
||||||
"toolboxes.modules.calculator.expressionLabel",
|
"toolboxes.modules.calculator.expressionLabel",
|
||||||
"toolboxes.modules.calculator.expressionPlaceholder",
|
"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.resultLabel",
|
||||||
"toolboxes.modules.calculator.labelLabel",
|
"toolboxes.modules.calculator.labelLabel",
|
||||||
"toolboxes.modules.calculator.labelPlaceholder",
|
"toolboxes.modules.calculator.labelPlaceholder",
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { exportModuleText, importModuleText, parseColonImportLines } from "../we
|
||||||
import { buildInsertedMixTagValue, normalizeMixTagSpacing, normalizeMixTagText, removeMixTagAtOffset, tokenizeMixTagValue } from "../website/src/utils/mixTagsInputUtils.js";
|
import { buildInsertedMixTagValue, normalizeMixTagSpacing, normalizeMixTagText, removeMixTagAtOffset, tokenizeMixTagValue } from "../website/src/utils/mixTagsInputUtils.js";
|
||||||
import { getEffectiveGoalTreeProgressMode, getVisibleGoalTreeEdges } from "../website/src/features/toolboxes/modules/goalTreeDisplay.js";
|
import { getEffectiveGoalTreeProgressMode, getVisibleGoalTreeEdges } from "../website/src/features/toolboxes/modules/goalTreeDisplay.js";
|
||||||
import { calculateGoalTreeFormula, evaluateGoalTreeNodes } from "../website/src/features/toolboxes/modules/goalTreeUtils.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 { 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 { 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";
|
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,
|
scrollResults: true,
|
||||||
entries: [
|
entries: [
|
||||||
{ id: "a", label: "Ore", value: 100, formula: "100" },
|
{ 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);
|
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.scrollResults, true);
|
||||||
assert.equal(calculatorImport.data.entries[1].parentId, calculatorImport.data.entries[0].id);
|
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].formula, "base*5");
|
||||||
|
assert.equal(calculatorImport.data.entries[1].current, 20);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("calculator helpers evaluate formulas and preserve tree order", () => {
|
test("calculator helpers evaluate formulas and preserve tree order", () => {
|
||||||
const entries = evaluateCalculatorEntries([
|
const entries = evaluateCalculatorEntries([
|
||||||
{ id: "root", label: "Root", value: 10, formula: "10" },
|
{ id: "root", label: "Root", value: 8, formula: "10", current: 2 },
|
||||||
{ id: "child", parentId: "root", label: "Child", value: 0, formula: "base*3" },
|
{ id: "child", parentId: "root", label: "Child", value: 0, formula: "base*10", current: 20 },
|
||||||
{ id: "sibling", label: "Sibling", value: 2, formula: "1+1" }
|
{ id: "sibling", label: "Sibling", value: 2, formula: "1+1" }
|
||||||
]);
|
]);
|
||||||
assert.equal(calculateExpression("2 + 3 * 4"), 14);
|
assert.equal(calculateExpression("2 + 3 * 4"), 14);
|
||||||
assert.equal(calculateExpression("base / 2", 10), 5);
|
assert.equal(calculateExpression("base / 2", 10), 5);
|
||||||
assert.equal(calculateExpression("window.alert(1)"), null);
|
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("+5", 10), "base+5");
|
||||||
assert.equal(createStoredFormula("10*2", 10), "base*2");
|
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"]);
|
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", () => {
|
test("calendar layout keeps week compaction and overlapping lanes deterministic", () => {
|
||||||
|
|
|
||||||
|
|
@ -844,6 +844,7 @@
|
||||||
"text": "Saisie d'une expression, calcul du résultat et sauvegarde avec un libellé.",
|
"text": "Saisie d'une expression, calcul du résultat et sauvegarde avec un libellé.",
|
||||||
"details": [
|
"details": [
|
||||||
"Le champ expression accepte les opérations de base et calcule le résultat avant sauvegarde.",
|
"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.",
|
"Le libellé sert à nommer un coût, une ressource, un total ou une conversion.",
|
||||||
"Un résultat enregistré peut être renommé ou supprimé."
|
"Un résultat enregistré peut être renommé ou supprimé."
|
||||||
]
|
]
|
||||||
|
|
@ -869,7 +870,7 @@
|
||||||
],
|
],
|
||||||
"importFormat": {
|
"importFormat": {
|
||||||
"text": "Une ligne par résultat. L'indentation de deux espaces conserve les liens parent/enfant.",
|
"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": {
|
"goalTree": {
|
||||||
|
|
@ -2305,7 +2306,7 @@
|
||||||
"deleteComboTitle": "Supprimer le combo"
|
"deleteComboTitle": "Supprimer le combo"
|
||||||
},
|
},
|
||||||
"calculator": {
|
"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",
|
"importOpenButton": "Importer du texte",
|
||||||
"exportOpenButton": "Exporter en format texte",
|
"exportOpenButton": "Exporter en format texte",
|
||||||
"importModalTitle": "Importer des résultats",
|
"importModalTitle": "Importer des résultats",
|
||||||
|
|
@ -2317,6 +2318,10 @@
|
||||||
"importError": "Format de calculateur invalide.",
|
"importError": "Format de calculateur invalide.",
|
||||||
"expressionLabel": "Calcul",
|
"expressionLabel": "Calcul",
|
||||||
"expressionPlaceholder": "10*10",
|
"expressionPlaceholder": "10*10",
|
||||||
|
"totalLabel": "Total",
|
||||||
|
"currentLabel": "Actuel",
|
||||||
|
"currentPlaceholder": "0",
|
||||||
|
"subtractCurrentTitle": "Soustraire l'actuel",
|
||||||
"resultLabel": "Résultat",
|
"resultLabel": "Résultat",
|
||||||
"labelLabel": "Libellé",
|
"labelLabel": "Libellé",
|
||||||
"labelPlaceholder": "ex: Lingots de fer",
|
"labelPlaceholder": "ex: Lingots de fer",
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { CalculatorEntries, FormattedCalculatorNumber } from "./CalculatorEntrie
|
||||||
import { TextExchangeActions } from "./TextExchangeActions.jsx";
|
import { TextExchangeActions } from "./TextExchangeActions.jsx";
|
||||||
import {
|
import {
|
||||||
BASE_TOKEN,
|
BASE_TOKEN,
|
||||||
calculateExpression,
|
calculateRemainingValue,
|
||||||
createStoredFormula,
|
createStoredFormula,
|
||||||
evaluateCalculatorEntries,
|
evaluateCalculatorEntries,
|
||||||
formatDisplayResult,
|
formatDisplayResult,
|
||||||
|
|
@ -15,7 +15,8 @@ import {
|
||||||
getDescendantIds,
|
getDescendantIds,
|
||||||
getEditableFormula,
|
getEditableFormula,
|
||||||
getEntriesInTreeOrder,
|
getEntriesInTreeOrder,
|
||||||
getResultLengthClass
|
getResultLengthClass,
|
||||||
|
parseCalculatorCurrent
|
||||||
} from "./calculatorUtils.js";
|
} from "./calculatorUtils.js";
|
||||||
|
|
||||||
function focusExpressionInputAtEnd(element) {
|
function focusExpressionInputAtEnd(element) {
|
||||||
|
|
@ -41,6 +42,8 @@ export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpe
|
||||||
const calculatedData = useMemo(() => ({ ...data, entries: calculatedEntries }), [data, calculatedEntries]);
|
const calculatedData = useMemo(() => ({ ...data, entries: calculatedEntries }), [data, calculatedEntries]);
|
||||||
const textContent = context.moduleText?.calculator || {};
|
const textContent = context.moduleText?.calculator || {};
|
||||||
const [expression, setExpression] = useState("");
|
const [expression, setExpression] = useState("");
|
||||||
|
const [current, setCurrent] = useState("");
|
||||||
|
const [showCurrent, setShowCurrent] = useState(false);
|
||||||
const [label, setLabel] = useState("");
|
const [label, setLabel] = useState("");
|
||||||
const [activeParentId, setActiveParentId] = useState("");
|
const [activeParentId, setActiveParentId] = useState("");
|
||||||
const [editingEntryId, setEditingEntryId] = useState("");
|
const [editingEntryId, setEditingEntryId] = useState("");
|
||||||
|
|
@ -48,12 +51,13 @@ export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpe
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
const calculatorCardRef = useRef(null);
|
const calculatorCardRef = useRef(null);
|
||||||
const expressionInputRef = useRef(null);
|
const expressionInputRef = useRef(null);
|
||||||
|
const currentInputRef = useRef(null);
|
||||||
const pendingExpressionFocusRef = useRef(false);
|
const pendingExpressionFocusRef = useRef(false);
|
||||||
const editingEntry = calculatedEntries.find((entry) => entry.id === editingEntryId);
|
const editingEntry = calculatedEntries.find((entry) => entry.id === editingEntryId);
|
||||||
const formulaParentId = editingEntry ? editingEntry.parentId || "" : activeParentId;
|
const formulaParentId = editingEntry ? editingEntry.parentId || "" : activeParentId;
|
||||||
const formulaParent = calculatedEntries.find((entry) => entry.id === formulaParentId);
|
const formulaParent = calculatedEntries.find((entry) => entry.id === formulaParentId);
|
||||||
const formMode = editingEntry ? "edit" : formulaParent ? "child" : "root";
|
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 scopeParentId = (parentId = "") => `${moduleId}:${parentId}`;
|
||||||
const reorder = useGroupedReorder({
|
const reorder = useGroupedReorder({
|
||||||
namespace: "calculator",
|
namespace: "calculator",
|
||||||
|
|
@ -99,13 +103,16 @@ export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpe
|
||||||
if (result == null) return;
|
if (result == null) return;
|
||||||
const cleanLabel = label.trim();
|
const cleanLabel = label.trim();
|
||||||
const storedFormula = createStoredFormula(expression, formulaParent?.value);
|
const storedFormula = createStoredFormula(expression, formulaParent?.value);
|
||||||
|
const storedCurrent = parseCalculatorCurrent(current);
|
||||||
|
if (storedCurrent == null) return;
|
||||||
|
|
||||||
if (editingEntry) {
|
if (editingEntry) {
|
||||||
save(calculatedEntries.map((entry) => entry.id === editingEntry.id ? {
|
save(calculatedEntries.map((entry) => entry.id === editingEntry.id ? {
|
||||||
...entry,
|
...entry,
|
||||||
label: cleanLabel,
|
label: cleanLabel,
|
||||||
value: result,
|
value: result,
|
||||||
formula: storedFormula
|
formula: storedFormula,
|
||||||
|
current: storedCurrent
|
||||||
} : entry));
|
} : entry));
|
||||||
setLabel(cleanLabel);
|
setLabel(cleanLabel);
|
||||||
return;
|
return;
|
||||||
|
|
@ -118,10 +125,13 @@ export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpe
|
||||||
parentId: formulaParentId,
|
parentId: formulaParentId,
|
||||||
label: cleanLabel,
|
label: cleanLabel,
|
||||||
value: result,
|
value: result,
|
||||||
formula: storedFormula
|
formula: storedFormula,
|
||||||
|
current: storedCurrent
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
setExpression(formulaParent ? BASE_TOKEN : "");
|
setExpression(formulaParent ? BASE_TOKEN : "");
|
||||||
|
setCurrent("");
|
||||||
|
setShowCurrent(false);
|
||||||
setLabel("");
|
setLabel("");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -129,6 +139,8 @@ export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpe
|
||||||
setEditingEntryId(entry.id);
|
setEditingEntryId(entry.id);
|
||||||
setActiveParentId("");
|
setActiveParentId("");
|
||||||
setExpression(getEditableFormula(entry));
|
setExpression(getEditableFormula(entry));
|
||||||
|
setCurrent(entry.current ? formatResult(entry.current) : "");
|
||||||
|
setShowCurrent(Boolean(entry.current));
|
||||||
setLabel(entry.label || "");
|
setLabel(entry.label || "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -141,7 +153,8 @@ export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpe
|
||||||
parentId: entry.id,
|
parentId: entry.id,
|
||||||
label: "",
|
label: "",
|
||||||
value: Number.isFinite(entry.value) ? entry.value : 0,
|
value: Number.isFinite(entry.value) ? entry.value : 0,
|
||||||
formula: BASE_TOKEN
|
formula: BASE_TOKEN,
|
||||||
|
current: 0
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
setEditingEntryId("");
|
setEditingEntryId("");
|
||||||
|
|
@ -149,6 +162,8 @@ export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpe
|
||||||
setEditingEntryId(childId);
|
setEditingEntryId(childId);
|
||||||
setActiveParentId("");
|
setActiveParentId("");
|
||||||
setExpression(BASE_TOKEN);
|
setExpression(BASE_TOKEN);
|
||||||
|
setCurrent("");
|
||||||
|
setShowCurrent(false);
|
||||||
setLabel("");
|
setLabel("");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -162,12 +177,16 @@ export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpe
|
||||||
setEditingEntryId("");
|
setEditingEntryId("");
|
||||||
setLabel("");
|
setLabel("");
|
||||||
setExpression("");
|
setExpression("");
|
||||||
|
setCurrent("");
|
||||||
|
setShowCurrent(false);
|
||||||
}
|
}
|
||||||
save(calculatedEntries.filter((entry) => !deletedIds.has(entry.id)));
|
save(calculatedEntries.filter((entry) => !deletedIds.has(entry.id)));
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetCalculator() {
|
function resetCalculator() {
|
||||||
setExpression("");
|
setExpression("");
|
||||||
|
setCurrent("");
|
||||||
|
setShowCurrent(false);
|
||||||
setLabel("");
|
setLabel("");
|
||||||
setActiveParentId("");
|
setActiveParentId("");
|
||||||
setEditingEntryId("");
|
setEditingEntryId("");
|
||||||
|
|
@ -177,9 +196,21 @@ export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpe
|
||||||
setActiveParentId("");
|
setActiveParentId("");
|
||||||
setEditingEntryId("");
|
setEditingEntryId("");
|
||||||
setExpression("");
|
setExpression("");
|
||||||
|
setCurrent("");
|
||||||
|
setShowCurrent(false);
|
||||||
setLabel("");
|
setLabel("");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleCurrent() {
|
||||||
|
if (showCurrent) {
|
||||||
|
setCurrent("");
|
||||||
|
setShowCurrent(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setShowCurrent(true);
|
||||||
|
window.requestAnimationFrame(() => currentInputRef.current?.focus());
|
||||||
|
}
|
||||||
|
|
||||||
async function copyChecklistImport() {
|
async function copyChecklistImport() {
|
||||||
const text = getEntriesInTreeOrder(calculatedEntries)
|
const text = getEntriesInTreeOrder(calculatedEntries)
|
||||||
.map((entry) => `${entry.label || formatResult(entry.value)}:${formatResult(entry.value)}`)
|
.map((entry) => `${entry.label || formatResult(entry.value)}:${formatResult(entry.value)}`)
|
||||||
|
|
@ -201,6 +232,8 @@ export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpe
|
||||||
<div className="calculator-card" ref={calculatorCardRef}>
|
<div className="calculator-card" ref={calculatorCardRef}>
|
||||||
<div className="calculator-field">
|
<div className="calculator-field">
|
||||||
<span>{textContent.expressionLabel || "Calcul"}</span>
|
<span>{textContent.expressionLabel || "Calcul"}</span>
|
||||||
|
<div className={`calculator-expression-row ${showCurrent ? "has-current" : ""}`}>
|
||||||
|
{showCurrent && <span className="calculator-total-label">{textContent.totalLabel || "Total"}</span>}
|
||||||
{formulaParent ? (
|
{formulaParent ? (
|
||||||
<MixTagsInput
|
<MixTagsInput
|
||||||
aria-label={textContent.expressionLabel || "Calcul"}
|
aria-label={textContent.expressionLabel || "Calcul"}
|
||||||
|
|
@ -244,18 +277,46 @@ export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpe
|
||||||
value={expression}
|
value={expression}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
<button
|
||||||
|
className="calculator-current-toggle nds-icon-button"
|
||||||
|
data-size="sm"
|
||||||
|
type="button"
|
||||||
|
onClick={toggleCurrent}
|
||||||
|
aria-label={textContent.subtractCurrentTitle || "Soustraire l'actuel"}
|
||||||
|
aria-pressed={showCurrent}
|
||||||
|
title={textContent.subtractCurrentTitle || "Soustraire l'actuel"}
|
||||||
|
>
|
||||||
|
<span aria-hidden="true">−</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
{showCurrent && (
|
||||||
|
<div className="calculator-current-field">
|
||||||
|
<span>{textContent.currentLabel || "Actuel"}</span>
|
||||||
|
<input
|
||||||
|
aria-label={textContent.currentLabel || "Actuel"}
|
||||||
|
className="nds-input"
|
||||||
|
data-size="sm"
|
||||||
|
ref={currentInputRef}
|
||||||
|
type="text"
|
||||||
|
inputMode="decimal"
|
||||||
|
value={current}
|
||||||
|
onChange={(event) => setCurrent(event.target.value)}
|
||||||
|
placeholder={textContent.currentPlaceholder || "0"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className={`calculator-result ${result == null ? "" : getResultLengthClass(result)}`} aria-live="polite">
|
<div className={`calculator-result ${result == null ? "" : getResultLengthClass(result)}`} aria-live="polite">
|
||||||
<span>{textContent.resultLabel || "Résultat"}</span>
|
<span>{textContent.resultLabel || "Résultat"}</span>
|
||||||
<strong aria-label={result == null ? "-" : formatDisplayResult(result)}>
|
<strong aria-label={result == null ? "-" : formatDisplayResult(result)}>
|
||||||
<FormattedCalculatorNumber value={result} />
|
<FormattedCalculatorNumber value={result} />
|
||||||
</strong>
|
</strong>
|
||||||
</div>
|
</div>
|
||||||
<label>
|
<label className="calculator-label-field">
|
||||||
<span>{textContent.labelLabel || "Libellé"}</span>
|
<span>{textContent.labelLabel || "Libellé"}</span>
|
||||||
<input className="nds-input" data-size="sm" type="text" value={label} onChange={(event) => setLabel(event.target.value)} placeholder={textContent.labelPlaceholder || "Lingots de fer"} />
|
<input className="nds-input" data-size="sm" type="text" value={label} onChange={(event) => setLabel(event.target.value)} placeholder={textContent.labelPlaceholder || "Lingots de fer"} />
|
||||||
</label>
|
</label>
|
||||||
<button className="nds-button" data-size="sm" data-variant="primary" disabled={result == null}>{formMode === "edit" ? textContent.updateButton || "Mettre à jour" : textContent.saveButton || "Enregistrer"}</button>
|
<button className="calculator-save-button nds-button" data-size="sm" data-variant="primary" disabled={result == null}>{formMode === "edit" ? textContent.updateButton || "Mettre à jour" : textContent.saveButton || "Enregistrer"}</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,20 @@ export function calculateExpression(expression, baseValue = null) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function parseCalculatorCurrent(value) {
|
||||||
|
const normalized = String(value ?? "").replaceAll(",", ".").trim();
|
||||||
|
if (!normalized) return 0;
|
||||||
|
const parsed = Number(normalized);
|
||||||
|
return Number.isFinite(parsed) ? parsed : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function calculateRemainingValue(expression, currentValue = 0, baseValue = null) {
|
||||||
|
const total = calculateExpression(expression, baseValue);
|
||||||
|
const current = parseCalculatorCurrent(currentValue);
|
||||||
|
if (total == null || current == null) return null;
|
||||||
|
return total - current;
|
||||||
|
}
|
||||||
|
|
||||||
export function formatResult(value) {
|
export function formatResult(value) {
|
||||||
if (!Number.isFinite(value)) return "";
|
if (!Number.isFinite(value)) return "";
|
||||||
return Number.parseFloat(value.toFixed(6)).toString();
|
return Number.parseFloat(value.toFixed(6)).toString();
|
||||||
|
|
@ -58,7 +72,8 @@ export function getEntryFormula(entry) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getEditableFormula(entry) {
|
export function getEditableFormula(entry) {
|
||||||
return getEntryFormula(entry) || formatResult(entry.value);
|
const current = Number(entry?.current);
|
||||||
|
return getEntryFormula(entry) || formatResult(entry.value + (Number.isFinite(current) ? current : 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createStoredFormula(expression, parentValue) {
|
export function createStoredFormula(expression, parentValue) {
|
||||||
|
|
@ -82,7 +97,7 @@ export function evaluateCalculatorEntries(entries) {
|
||||||
function visit(parentId = "", parentValue = null) {
|
function visit(parentId = "", parentValue = null) {
|
||||||
return (byParent.get(parentId) || []).flatMap((entry) => {
|
return (byParent.get(parentId) || []).flatMap((entry) => {
|
||||||
const formula = getEntryFormula(entry);
|
const formula = getEntryFormula(entry);
|
||||||
const calculatedValue = formula ? calculateExpression(formula, parentValue) : entry.value;
|
const calculatedValue = formula ? calculateRemainingValue(formula, entry.current, parentValue) : entry.value;
|
||||||
const nextEntry = { ...entry, value: calculatedValue ?? entry.value };
|
const nextEntry = { ...entry, value: calculatedValue ?? entry.value };
|
||||||
return [nextEntry, ...visit(entry.id, nextEntry.value)];
|
return [nextEntry, ...visit(entry.id, nextEntry.value)];
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -244,7 +244,8 @@ export function exportCalculatorText(data) {
|
||||||
function visit(parentId = "", depth = 0) {
|
function visit(parentId = "", depth = 0) {
|
||||||
entries.filter((entry) => (entry.parentId || "") === parentId).forEach((entry) => {
|
entries.filter((entry) => (entry.parentId || "") === parentId).forEach((entry) => {
|
||||||
const formula = String(entry.formula || "").trim();
|
const formula = String(entry.formula || "").trim();
|
||||||
lines.push(`${" ".repeat(depth)}${entry.label || ""}: ${entry.value}${formula ? ` | formula=${formula}` : ""}`);
|
const current = Number(entry.current) || 0;
|
||||||
|
lines.push(`${" ".repeat(depth)}${entry.label || ""}: ${entry.value}${formula ? ` | formula=${formula}` : ""}${current ? ` | current=${current}` : ""}`);
|
||||||
visit(entry.id, depth + 1);
|
visit(entry.id, depth + 1);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -268,10 +269,23 @@ export function importCalculatorText(text, context) {
|
||||||
const separatorIndex = line.indexOf(":");
|
const separatorIndex = line.indexOf(":");
|
||||||
if (separatorIndex < 0) throw lineError(index, `ligne calculateur invalide: ${line.trim()}`);
|
if (separatorIndex < 0) throw lineError(index, `ligne calculateur invalide: ${line.trim()}`);
|
||||||
const label = line.slice(0, separatorIndex).trim();
|
const label = line.slice(0, separatorIndex).trim();
|
||||||
const [rawValue, rawFormula = ""] = line.slice(separatorIndex + 1).split(/\s+\|\s+formula=/);
|
const [rawValue, ...rawMetadata] = line.slice(separatorIndex + 1).split(/\s+\|\s+/);
|
||||||
const value = parseNumber(rawValue, Number.NaN);
|
const value = parseNumber(rawValue, Number.NaN);
|
||||||
if (!Number.isFinite(value)) throw lineError(index, `valeur calculateur invalide: ${line.trim()}`);
|
if (!Number.isFinite(value)) throw lineError(index, `valeur calculateur invalide: ${line.trim()}`);
|
||||||
const entry = { id: context.uid("calc"), parentId: parentStack[depth - 1] || "", label, value, formula: rawFormula.trim() };
|
let formula = "";
|
||||||
|
let current = 0;
|
||||||
|
rawMetadata.forEach((metadata) => {
|
||||||
|
if (metadata.startsWith("formula=")) {
|
||||||
|
formula = metadata.slice("formula=".length).trim();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (metadata.startsWith("current=")) {
|
||||||
|
current = parseNumber(metadata.slice("current=".length), Number.NaN);
|
||||||
|
if (Number.isFinite(current)) return;
|
||||||
|
}
|
||||||
|
throw lineError(index, `option calculateur invalide: ${metadata.trim()}`);
|
||||||
|
});
|
||||||
|
const entry = { id: context.uid("calc"), parentId: parentStack[depth - 1] || "", label, value, formula, current };
|
||||||
entries.push(entry);
|
entries.push(entry);
|
||||||
parentStack[depth] = entry.id;
|
parentStack[depth] = entry.id;
|
||||||
parentStack.length = depth + 1;
|
parentStack.length = depth + 1;
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,8 @@ export function normalizeCalculatorData(data) {
|
||||||
parentId: String(entry?.parentId || ""),
|
parentId: String(entry?.parentId || ""),
|
||||||
label: String(entry?.label || "").trim(),
|
label: String(entry?.label || "").trim(),
|
||||||
value: normalizeCalculatorValue(entry?.value),
|
value: normalizeCalculatorValue(entry?.value),
|
||||||
formula: normalizeCalculatorFormula(entry?.formula)
|
formula: normalizeCalculatorFormula(entry?.formula),
|
||||||
|
current: normalizeCalculatorValue(entry?.current)
|
||||||
}));
|
}));
|
||||||
const entryIds = new Set(entries.map((entry) => entry.id));
|
const entryIds = new Set(entries.map((entry) => entry.id));
|
||||||
return {
|
return {
|
||||||
|
|
@ -32,6 +33,7 @@ export function compactCalculatorDataForStorage(value) {
|
||||||
const compact = { id: entry.id, label: entry.label, value: entry.value };
|
const compact = { id: entry.id, label: entry.label, value: entry.value };
|
||||||
if (entry.parentId) compact.parentId = entry.parentId;
|
if (entry.parentId) compact.parentId = entry.parentId;
|
||||||
if (entry.formula) compact.formula = entry.formula;
|
if (entry.formula) compact.formula = entry.formula;
|
||||||
|
if (entry.current) compact.current = entry.current;
|
||||||
return compact;
|
return compact;
|
||||||
});
|
});
|
||||||
if (!entries.length && !normalized.scrollResults) return null;
|
if (!entries.length && !normalized.scrollResults) return null;
|
||||||
|
|
|
||||||
|
|
@ -309,7 +309,6 @@
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.calculator-module,
|
|
||||||
.timer-module {
|
.timer-module {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
.calculator-form label,
|
.calculator-form label,
|
||||||
.calculator-field,
|
.calculator-field,
|
||||||
|
.calculator-current-field,
|
||||||
.calculator-result {
|
.calculator-result {
|
||||||
display: grid;
|
display: grid;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
@ -21,6 +22,29 @@
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.calculator-expression-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) 34px;
|
||||||
|
min-width: 0;
|
||||||
|
align-items: center;
|
||||||
|
gap: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calculator-expression-row.has-current,
|
||||||
|
.calculator-current-field {
|
||||||
|
grid-template-columns: 48px minmax(0, 1fr) 34px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calculator-current-toggle > span {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calculator-current-field {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
.calculator-entry.has-drag-handle {
|
.calculator-entry.has-drag-handle {
|
||||||
grid-template-columns: 30px minmax(0, 1fr) 34px 34px;
|
grid-template-columns: 30px minmax(0, 1fr) 34px 34px;
|
||||||
}
|
}
|
||||||
|
|
@ -87,6 +111,8 @@
|
||||||
|
|
||||||
.calculator-form label span,
|
.calculator-form label span,
|
||||||
.calculator-field > span,
|
.calculator-field > span,
|
||||||
|
.calculator-total-label,
|
||||||
|
.calculator-current-field > span,
|
||||||
.calculator-result span,
|
.calculator-result span,
|
||||||
.tool-split-root-button {
|
.tool-split-root-button {
|
||||||
color: var(--color-text-secondary);
|
color: var(--color-text-secondary);
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,62 @@
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.module[data-module-type="calculator"] {
|
||||||
|
container-name: calculator-tool;
|
||||||
|
container-type: inline-size;
|
||||||
|
}
|
||||||
|
|
||||||
|
@container calculator-tool (max-width: 820px) {
|
||||||
|
.calculator-module {
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calculator-card {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.calculator-field {
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calculator-current-field {
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calculator-result {
|
||||||
|
grid-column: 2;
|
||||||
|
grid-row: 1 / 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calculator-label-field,
|
||||||
|
.calculator-save-button {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@container calculator-tool (max-width: 560px) {
|
||||||
|
.calculator-card {
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calculator-field,
|
||||||
|
.calculator-current-field,
|
||||||
|
.calculator-result,
|
||||||
|
.calculator-label-field,
|
||||||
|
.calculator-save-button {
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@supports not (container-type: inline-size) {
|
||||||
|
.calculator-module {
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.calculator-card,
|
.calculator-card,
|
||||||
.tool-split-results,
|
.tool-split-results,
|
||||||
.timer-control-card,
|
.timer-control-card,
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
min-height: 44px;
|
min-height: 36px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
|
|
@ -29,18 +29,18 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
height: 42px;
|
height: 36px;
|
||||||
min-height: 42px;
|
min-height: 36px;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
padding: 10px 4px 4px 7px;
|
padding: 7px 4px 4px 7px;
|
||||||
border: 0;
|
border: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
caret-color: var(--color-text-primary);
|
caret-color: var(--color-text-primary);
|
||||||
font: inherit;
|
font: inherit;
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
line-height: 20px;
|
line-height: 21px;
|
||||||
outline: none;
|
outline: none;
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue