Refactor calculator formula tagging workflow
All checks were successful
Deploy Sokko G / deploy (push) Successful in 15s

This commit is contained in:
Shinuwa 2026-08-24 14:14:57 +02:00
parent ae503c7260
commit abfa23fb3e
6 changed files with 118 additions and 191 deletions

View file

@ -257,19 +257,18 @@ export function validateSiteContent(site) {
"toolboxes.modules.calculator.labelLabel", "toolboxes.modules.calculator.labelLabel",
"toolboxes.modules.calculator.labelPlaceholder", "toolboxes.modules.calculator.labelPlaceholder",
"toolboxes.modules.calculator.saveButton", "toolboxes.modules.calculator.saveButton",
"toolboxes.modules.calculator.updateButton",
"toolboxes.modules.calculator.newButton",
"toolboxes.modules.calculator.rootButton", "toolboxes.modules.calculator.rootButton",
"toolboxes.modules.calculator.resetTitle", "toolboxes.modules.calculator.resetTitle",
"toolboxes.modules.calculator.copyTitle", "toolboxes.modules.calculator.copyTitle",
"toolboxes.modules.calculator.copiedTitle", "toolboxes.modules.calculator.copiedTitle",
"toolboxes.modules.calculator.scrollableTitle", "toolboxes.modules.calculator.scrollableTitle",
"toolboxes.modules.calculator.emptyResults", "toolboxes.modules.calculator.emptyResults",
"toolboxes.modules.calculator.formulaTitle",
"toolboxes.modules.calculator.childFormulaHelp",
"toolboxes.modules.calculator.rootFormulaHelp",
"toolboxes.modules.calculator.baseTokenLabel", "toolboxes.modules.calculator.baseTokenLabel",
"toolboxes.modules.calculator.insertBaseTitle", "toolboxes.modules.calculator.insertBaseTitle",
"toolboxes.modules.calculator.useEntryTitle", "toolboxes.modules.calculator.editTitle",
"toolboxes.modules.calculator.renameTitle", "toolboxes.modules.calculator.addChildTitle",
"toolboxes.modules.calculator.deleteTitle", "toolboxes.modules.calculator.deleteTitle",
"toolboxes.modules.goalTree.editorTitle", "toolboxes.modules.goalTree.editorTitle",
"toolboxes.modules.goalTree.treeTitle", "toolboxes.modules.goalTree.treeTitle",

View file

@ -245,18 +245,23 @@ test("toolbox module registry and modules expose expected behavior", async () =>
assert.match(calculatorModule, /calculateExpression/); assert.match(calculatorModule, /calculateExpression/);
assert.match(calculatorModule, /activeParentId/); assert.match(calculatorModule, /activeParentId/);
assert.match(calculatorModule, /parentId/); assert.match(calculatorModule, /parentId/);
assert.match(calculatorModule, /useInlineEdit/); assert.match(calculatorModule, /MixTagsInput/);
assert.match(calculatorModule, /useGroupedReorder/); assert.match(calculatorModule, /useGroupedReorder/);
assert.match(calculatorModule, /calculator-drag-handle/); assert.match(calculatorModule, /calculator-drag-handle/);
assert.match(calculatorModule, /scrollResults/); assert.match(calculatorModule, /scrollResults/);
assert.match(calculatorModule, /tool-split-scroll-toggle/); assert.match(calculatorModule, /tool-split-scroll-toggle/);
assert.match(calculatorModule, /EditableCalculatorLabel/);
assert.match(calculatorModule, /tool-split-entry-label/);
assert.match(calculatorModule, /copyChecklistImport/); assert.match(calculatorModule, /copyChecklistImport/);
assert.match(calculatorModule, /resetCalculator/); assert.match(calculatorModule, /resetCalculator/);
assert.match(calculatorModule, /returnToRoot/); assert.match(calculatorModule, /returnToRoot/);
assert.match(calculatorModule, /setExpression\(activeParent \? formatResult\(activeParent\.value\) : ""\)/); assert.match(calculatorModule, /createChildEntry/);
assert.match(calculatorModule, /EditableCalculatorFormula/); assert.match(calculatorModule, /const childId = context\.uid\("calc"\)/);
assert.match(calculatorModule, /parentId: entry\.id/);
assert.match(calculatorModule, /formula: BASE_TOKEN/);
assert.match(calculatorModule, /pendingExpressionFocusRef/);
assert.match(calculatorModule, /setExpression\(BASE_TOKEN\)/);
assert.match(calculatorModule, /editingEntryId/);
assert.match(calculatorModule, /updateButton/);
assert.match(calculatorModule, /onAddChild/);
assert.match(calculatorModule, /entry\.label \|\| formatResult\(entry\.value\)/); assert.match(calculatorModule, /entry\.label \|\| formatResult\(entry\.value\)/);
assert.match(calculatorModule, /ResizeObserver/); assert.match(calculatorModule, /ResizeObserver/);
assert.match(calculatorModule, /--calculator-scroll-height/); assert.match(calculatorModule, /--calculator-scroll-height/);

View file

@ -1854,19 +1854,18 @@
"labelLabel": "Libellé", "labelLabel": "Libellé",
"labelPlaceholder": "ex: Lingots de fer", "labelPlaceholder": "ex: Lingots de fer",
"saveButton": "Enregistrer", "saveButton": "Enregistrer",
"updateButton": "Mettre à jour",
"newButton": "Nouveau calcul",
"rootButton": "Revenir à la racine", "rootButton": "Revenir à la racine",
"resetTitle": "Réinitialiser", "resetTitle": "Réinitialiser",
"copyTitle": "Copier pour checklist", "copyTitle": "Copier pour checklist",
"copiedTitle": "Copié", "copiedTitle": "Copié",
"scrollableTitle": "Liste scrollable", "scrollableTitle": "Liste scrollable",
"emptyResults": "Aucun résultat enregistré.", "emptyResults": "Aucun résultat enregistré.",
"formulaTitle": "Modifier la formule",
"childFormulaHelp": "Utilisez base pour référencer le parent.",
"rootFormulaHelp": "Valeur ou formule racine.",
"baseTokenLabel": "Valeur du parent", "baseTokenLabel": "Valeur du parent",
"insertBaseTitle": "Ajouter la valeur du parent", "insertBaseTitle": "Ajouter la valeur du parent",
"useEntryTitle": "Utiliser comme base", "editTitle": "Modifier",
"renameTitle": "Renommer", "addChildTitle": "Ajouter un calcul enfant",
"deleteTitle": "Supprimer" "deleteTitle": "Supprimer"
}, },
"goalTree": { "goalTree": {

View file

@ -1,8 +1,7 @@
// Rôle : fournit l'outil calculateur avec résultats enregistrés en arborescence et échange texte. // Rôle : fournit l'outil calculateur avec résultats enregistrés en arborescence et échange texte.
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; import { useLayoutEffect, useMemo, useRef, useState } from "react";
import { Icon } from "../../../components/Icon.jsx"; import { Icon } from "../../../components/Icon.jsx";
import { MixTagsInput } from "../../../components/MixTagsInput.jsx"; import { MixTagsInput } from "../../../components/MixTagsInput.jsx";
import { useInlineEdit } from "../../../hooks/useInlineEdit.js";
import { moveItem, useGroupedReorder } from "../../../hooks/useGroupedReorder.js"; import { moveItem, useGroupedReorder } from "../../../hooks/useGroupedReorder.js";
import { TextExchangeActions } from "./TextExchangeActions.jsx"; import { TextExchangeActions } from "./TextExchangeActions.jsx";
@ -117,6 +116,23 @@ function FormattedCalculatorNumber({ value }) {
}); });
} }
function focusExpressionInputAtEnd(element) {
if (!element) return;
element.focus();
if (typeof element.setSelectionRange === "function") {
const length = element.value?.length || 0;
element.setSelectionRange(length, length);
return;
}
if (!element.isContentEditable) return;
const range = document.createRange();
range.selectNodeContents(element);
range.collapse(false);
const selection = window.getSelection();
selection?.removeAllRanges();
selection?.addRange(range);
}
export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpen }) { export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpen }) {
const data = context.normalizeCalculatorData(context.getModuleData(toolboxId, moduleId, { entries: [] })); const data = context.normalizeCalculatorData(context.getModuleData(toolboxId, moduleId, { entries: [] }));
const calculatedEntries = useMemo(() => evaluateCalculatorEntries(data.entries), [data.entries]); const calculatedEntries = useMemo(() => evaluateCalculatorEntries(data.entries), [data.entries]);
@ -125,11 +141,17 @@ export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpe
const [expression, setExpression] = useState(""); const [expression, setExpression] = useState("");
const [label, setLabel] = useState(""); const [label, setLabel] = useState("");
const [activeParentId, setActiveParentId] = useState(""); const [activeParentId, setActiveParentId] = useState("");
const [editingEntryId, setEditingEntryId] = useState("");
const [calculatorHeight, setCalculatorHeight] = useState(0); const [calculatorHeight, setCalculatorHeight] = useState(0);
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
const calculatorCardRef = useRef(null); const calculatorCardRef = useRef(null);
const activeParent = calculatedEntries.find((entry) => entry.id === activeParentId); const expressionInputRef = useRef(null);
const result = useMemo(() => calculateExpression(expression, activeParent?.value), [activeParent?.value, expression]); 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 scopeParentId = (parentId = "") => `${moduleId}:${parentId}`; const scopeParentId = (parentId = "") => `${moduleId}:${parentId}`;
const reorder = useGroupedReorder({ const reorder = useGroupedReorder({
namespace: "calculator", namespace: "calculator",
@ -156,6 +178,12 @@ export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpe
return () => observer.disconnect(); return () => observer.disconnect();
}, []); }, []);
useLayoutEffect(() => {
if (!pendingExpressionFocusRef.current || !editingEntryId) return;
pendingExpressionFocusRef.current = false;
window.requestAnimationFrame(() => focusExpressionInputAtEnd(expressionInputRef.current));
}, [editingEntryId]);
function save(entries) { function save(entries) {
context.setModuleData(toolboxId, moduleId, { ...data, entries: evaluateCalculatorEntries(entries) }); context.setModuleData(toolboxId, moduleId, { ...data, entries: evaluateCalculatorEntries(entries) });
} }
@ -168,24 +196,58 @@ export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpe
event.preventDefault(); event.preventDefault();
if (result == null) return; if (result == null) return;
const cleanLabel = label.trim(); const cleanLabel = label.trim();
const storedFormula = createStoredFormula(expression, formulaParent?.value);
if (editingEntry) {
save(calculatedEntries.map((entry) => entry.id === editingEntry.id ? {
...entry,
label: cleanLabel,
value: result,
formula: storedFormula
} : entry));
setLabel(cleanLabel);
return;
}
save([ save([
...data.entries, ...data.entries,
{ {
id: context.uid("calc"), id: context.uid("calc"),
parentId: activeParentId, parentId: formulaParentId,
label: cleanLabel, label: cleanLabel,
value: result, value: result,
formula: createStoredFormula(expression, activeParent?.value) formula: storedFormula
} }
]); ]);
setExpression(activeParent ? formatResult(activeParent.value) : ""); setExpression(formulaParent ? BASE_TOKEN : "");
setLabel(""); setLabel("");
} }
function useEntry(entry) { function editEntry(entry) {
setActiveParentId(entry.id); setEditingEntryId(entry.id);
setActiveParentId("");
setExpression(getEditableFormula(entry));
setLabel(entry.label || "");
}
function createChildEntry(entry) {
const childId = context.uid("calc");
save([
...data.entries,
{
id: childId,
parentId: entry.id,
label: "",
value: Number.isFinite(entry.value) ? entry.value : 0,
formula: BASE_TOKEN
}
]);
setEditingEntryId("");
pendingExpressionFocusRef.current = true;
setEditingEntryId(childId);
setActiveParentId("");
setExpression(BASE_TOKEN); setExpression(BASE_TOKEN);
setLabel("");
} }
function deleteEntry(entryId) { function deleteEntry(entryId) {
@ -194,34 +256,26 @@ export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpe
setActiveParentId(""); setActiveParentId("");
setExpression(""); setExpression("");
} }
if (deletedIds.has(editingEntryId)) {
setEditingEntryId("");
setLabel("");
setExpression("");
}
save(calculatedEntries.filter((entry) => !deletedIds.has(entry.id))); save(calculatedEntries.filter((entry) => !deletedIds.has(entry.id)));
} }
function renameEntry(entryId, label) {
const cleanLabel = label.trim();
save(calculatedEntries.map((entry) => entry.id === entryId ? { ...entry, label: cleanLabel } : entry));
}
function updateEntryFormula(entryId, formula) {
const nextEntries = calculatedEntries.map((entry) => {
if (entry.id !== entryId) return entry;
const parent = calculatedEntries.find((candidate) => candidate.id === entry.parentId);
const nextFormula = createStoredFormula(formula, parent?.value);
const nextValue = calculateExpression(nextFormula, parent?.value) ?? entry.value;
return { ...entry, formula: nextFormula, value: nextValue };
});
save(nextEntries);
}
function resetCalculator() { function resetCalculator() {
setExpression(""); setExpression("");
setLabel(""); setLabel("");
setActiveParentId(""); setActiveParentId("");
setEditingEntryId("");
} }
function returnToRoot() { function returnToRoot() {
setActiveParentId(""); setActiveParentId("");
setEditingEntryId("");
setExpression(""); setExpression("");
setLabel("");
} }
async function copyChecklistImport() { async function copyChecklistImport() {
@ -245,10 +299,11 @@ 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>
{activeParent ? ( {formulaParent ? (
<MixTagsInput <MixTagsInput
aria-label={textContent.expressionLabel || "Calcul"} aria-label={textContent.expressionLabel || "Calcul"}
className="calculator-expression-input" className="calculator-expression-input"
inputRef={expressionInputRef}
onChange={setExpression} onChange={setExpression}
onKeyDown={(event) => { onKeyDown={(event) => {
if (event.key !== "Enter") return; if (event.key !== "Enter") return;
@ -273,6 +328,7 @@ export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpe
<input <input
aria-label={textContent.expressionLabel || "Calcul"} aria-label={textContent.expressionLabel || "Calcul"}
className="calculator-expression-input" className="calculator-expression-input"
ref={expressionInputRef}
onChange={(event) => setExpression(event.target.value)} onChange={(event) => setExpression(event.target.value)}
onKeyDown={(event) => { onKeyDown={(event) => {
if (event.key !== "Enter") return; if (event.key !== "Enter") return;
@ -295,7 +351,7 @@ export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpe
<span>{textContent.labelLabel || "Libellé"}</span> <span>{textContent.labelLabel || "Libellé"}</span>
<input value={label} onChange={(event) => setLabel(event.target.value)} placeholder={textContent.labelPlaceholder || "Lingots de fer"} /> <input value={label} onChange={(event) => setLabel(event.target.value)} placeholder={textContent.labelPlaceholder || "Lingots de fer"} />
</label> </label>
<button className="primary" disabled={result == null}>{textContent.saveButton || "Enregistrer"}</button> <button className="primary" disabled={result == null}>{formMode === "edit" ? textContent.updateButton || "Mettre à jour" : textContent.saveButton || "Enregistrer"}</button>
</div> </div>
</form> </form>
<div <div
@ -303,9 +359,9 @@ export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpe
style={data.scrollResults && calculatorHeight ? { "--calculator-scroll-height": `${calculatorHeight}px` } : undefined} style={data.scrollResults && calculatorHeight ? { "--calculator-scroll-height": `${calculatorHeight}px` } : undefined}
> >
<div className="tool-split-actions"> <div className="tool-split-actions">
{activeParent && ( {(formulaParent || editingEntry) && (
<button className="tool-split-root-button" type="button" onClick={returnToRoot}> <button className="tool-split-root-button" type="button" onClick={returnToRoot}>
{textContent.rootButton || "Revenir à la racine"} {editingEntry ? textContent.newButton || "Nouveau calcul" : textContent.rootButton || "Revenir à la racine"}
</button> </button>
)} )}
<button className="tool-split-action-button danger" type="button" onClick={resetCalculator} aria-label={textContent.resetTitle || "Réinitialiser"} title={textContent.resetTitle || "Réinitialiser"}> <button className="tool-split-action-button danger" type="button" onClick={resetCalculator} aria-label={textContent.resetTitle || "Réinitialiser"} title={textContent.resetTitle || "Réinitialiser"}>
@ -328,7 +384,7 @@ export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpe
</div> </div>
<div className={`tool-split-tree calculator-tree ${data.scrollResults ? "is-scrollable legacy-scrollbar" : ""}`}> <div className={`tool-split-tree calculator-tree ${data.scrollResults ? "is-scrollable legacy-scrollbar" : ""}`}>
{calculatedEntries.length ? ( {calculatedEntries.length ? (
<CalculatorEntries entries={calculatedEntries} parentId="" getScopedParentId={scopeParentId} activeParentId={activeParentId} textContent={textContent} reorder={reorder} onUse={useEntry} onRename={renameEntry} onUpdateFormula={updateEntryFormula} onDelete={deleteEntry} /> <CalculatorEntries entries={calculatedEntries} parentId="" getScopedParentId={scopeParentId} activeEntryId={editingEntryId || activeParentId} textContent={textContent} reorder={reorder} onEdit={editEntry} onAddChild={createChildEntry} onDelete={deleteEntry} />
) : ( ) : (
<p className="muted">{textContent.emptyResults || "Aucun résultat enregistré."}</p> <p className="muted">{textContent.emptyResults || "Aucun résultat enregistré."}</p>
)} )}
@ -338,31 +394,15 @@ export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpe
); );
} }
function CalculatorEntries({ entries, parentId, getScopedParentId, activeParentId, textContent, reorder, onUse, onRename, onUpdateFormula, onDelete }) { function CalculatorEntries({ entries, parentId, getScopedParentId, activeEntryId, textContent, reorder, onEdit, onAddChild, onDelete }) {
const children = getChildren(entries, parentId); const children = getChildren(entries, parentId);
const [editingId, setEditingId] = useState("");
useEffect(() => {
if (!editingId) return undefined;
function closeOnOutsidePointer(event) {
if (event.target?.closest?.(`[data-calculator-edit-fields="${editingId}"]`)) return;
if (event.target?.closest?.("[data-calculator-edit-toggle]")) return;
setEditingId("");
}
document.addEventListener("pointerdown", closeOnOutsidePointer);
return () => document.removeEventListener("pointerdown", closeOnOutsidePointer);
}, [editingId]);
if (!children.length) return null; if (!children.length) return null;
return ( return (
<ul className="tool-split-entry-list calculator-entry-list"> <ul className="tool-split-entry-list calculator-entry-list">
{children.map((entry) => { {children.map((entry) => {
const className = [ const className = [
entry.id === activeParentId ? "active" : "", entry.id === activeEntryId ? "active" : "",
entry.id === editingId ? "is-editing" : "",
entry.parentId ? "can-edit-formula" : "",
reorder.isItemDragging(entry.id) ? "is-dragging" : "", reorder.isItemDragging(entry.id) ? "is-dragging" : "",
reorder.isItemDropTarget(entry.id) ? "is-drop-target" : "", reorder.isItemDropTarget(entry.id) ? "is-drop-target" : "",
reorder.getDropPlacement("item", entry.id, parentId) === "after" ? "drop-after" : "" reorder.getDropPlacement("item", entry.id, parentId) === "after" ? "drop-after" : ""
@ -380,121 +420,30 @@ function CalculatorEntries({ entries, parentId, getScopedParentId, activeParentI
> >
<Icon name="drag" /> <Icon name="drag" />
</button> </button>
{entry.id === editingId ? ( <button className="tool-split-entry-summary" type="button" onClick={() => onEdit(entry)} title={textContent.editTitle || "Modifier"}>
<span className="calculator-entry-edit-fields" data-calculator-edit-fields={entry.id}> <span>
<EditableCalculatorFormula entry={entry} entries={entries} textContent={textContent} onUpdateFormula={onUpdateFormula} /> <strong className={getEntryResultLengthClass(entry.value)} aria-label={formatDisplayResult(entry.value)}>
<EditableCalculatorLabel entry={entry} textContent={textContent} onRename={onRename} /> <FormattedCalculatorNumber value={entry.value} />
</strong>
{entry.label && <em>{entry.label}</em>}
</span> </span>
) : ( </button>
<button className="tool-split-entry-summary" type="button" onClick={() => onUse(entry)} title={textContent.useEntryTitle || "Utiliser comme base"}>
<span>
<strong className={getEntryResultLengthClass(entry.value)} aria-label={formatDisplayResult(entry.value)}>
<FormattedCalculatorNumber value={entry.value} />
</strong>
{entry.label && <em>{entry.label}</em>}
</span>
</button>
)}
<button <button
className={entry.id === editingId ? "active" : ""}
type="button" type="button"
onClick={() => setEditingId(entry.id === editingId ? "" : entry.id)} onClick={() => onAddChild(entry)}
aria-pressed={entry.id === editingId} aria-label={`${textContent.addChildTitle || "Ajouter un calcul enfant"} ${entry.label || formatResult(entry.value)}`}
aria-label={`${textContent.renameTitle || "Renommer"} ${entry.label}`} title={textContent.addChildTitle || "Ajouter un calcul enfant"}
data-calculator-edit-toggle
title={textContent.renameTitle || "Renommer"}
> >
<Icon name="edit" /> <Icon name="add" />
</button> </button>
<button type="button" className="danger" onClick={() => onDelete(entry.id)} aria-label={`${textContent.deleteTitle || "Supprimer"} ${entry.label}`} title={textContent.deleteTitle || "Supprimer"}> <button type="button" className="danger" onClick={() => onDelete(entry.id)} aria-label={`${textContent.deleteTitle || "Supprimer"} ${entry.label}`} title={textContent.deleteTitle || "Supprimer"}>
<Icon name="trash" /> <Icon name="trash" />
</button> </button>
</div> </div>
<CalculatorEntries entries={entries} parentId={entry.id} getScopedParentId={getScopedParentId} activeParentId={activeParentId} textContent={textContent} reorder={reorder} onUse={onUse} onRename={onRename} onUpdateFormula={onUpdateFormula} onDelete={onDelete} /> <CalculatorEntries entries={entries} parentId={entry.id} getScopedParentId={getScopedParentId} activeEntryId={activeEntryId} textContent={textContent} reorder={reorder} onEdit={onEdit} onAddChild={onAddChild} onDelete={onDelete} />
</li> </li>
); );
})} })}
</ul> </ul>
); );
} }
function EditableCalculatorLabel({ entry, textContent, onRename, onDone, autoFocus = true }) {
const inlineEdit = useInlineEdit({
value: entry.label,
onCommit: (label) => onRename(entry.id, label),
onDone,
autoFocus,
focusKey: entry.id
});
return (
<input
{...inlineEdit.getInputProps({
className: "tool-split-entry-label",
"aria-label": `${textContent.renameTitle || "Renommer"} ${entry.label}`
})}
/>
);
}
function EditableCalculatorFormula({ entry, entries, textContent, onUpdateFormula, autoFocus = false }) {
const parent = entries.find((candidate) => candidate.id === entry.parentId);
const parentValue = parent?.value;
const hasParent = Boolean(entry.parentId);
const inlineEdit = useInlineEdit({
value: getEditableFormula(entry),
onCommit: (formula) => {
const storedFormula = createStoredFormula(formula, parentValue);
if (calculateExpression(storedFormula, parentValue) != null) onUpdateFormula(entry.id, storedFormula);
},
autoFocus,
focusKey: `${entry.id}:formula`,
transform: (draft) => String(draft || "").replaceAll(",", ".").trim()
});
if (!hasParent) {
return (
<input
{...inlineEdit.getInputProps({
className: "tool-split-entry-label calculator-entry-formula",
"aria-label": `${textContent.formulaTitle || "Formule enregistrée"} ${entry.label || formatResult(entry.value)}`,
title: textContent.rootFormulaHelp || "Valeur ou formule racine."
})}
/>
);
}
return (
<MixTagsInput
aria-label={`${textContent.formulaTitle || "Formule enregistrée"} ${entry.label || formatResult(entry.value)}`}
className="tool-split-entry-label calculator-entry-formula"
onBlur={() => inlineEdit.commit()}
onChange={inlineEdit.setDraft}
onKeyDown={(event) => {
if (event.key === "Enter") {
event.preventDefault();
event.currentTarget.blur();
}
if (event.key === "Escape") {
event.preventDefault();
inlineEdit.cancel(event);
}
}}
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"}
size="small"
title={entry.parentId ? textContent.childFormulaHelp || "Utilisez base pour référencer le parent." : textContent.rootFormulaHelp || "Valeur ou formule racine."}
tokens={[{
value: "base",
label: textContent.baseTokenLabel || "Valeur du parent"
}]}
value={inlineEdit.draft}
/>
);
}

View file

@ -27,27 +27,6 @@
grid-template-columns: 30px minmax(0, 1fr) 34px 34px; grid-template-columns: 30px minmax(0, 1fr) 34px 34px;
} }
.tool-split-entry-list li.is-editing > .calculator-entry.has-drag-handle {
grid-template-columns: 30px minmax(0, 1fr) 34px 34px;
align-items: start;
}
.tool-split-entry-list li.is-editing > .calculator-entry.has-drag-handle .calculator-drag-handle {
grid-row: 1 / span 2;
}
.calculator-entry-formula {
font-variant-numeric: tabular-nums;
}
.calculator-entry-edit-fields {
display: grid;
min-width: 0;
gap: 6px;
grid-column: 2;
grid-row: 1 / span 2;
}
.calculator-drag-handle { .calculator-drag-handle {
width: 30px; width: 30px;
min-width: 30px; min-width: 30px;

View file

@ -658,10 +658,6 @@
min-height: 36px; min-height: 36px;
} }
.tool-split-entry > button.active:not(.tool-split-entry-summary, .tool-split-entry-value, .danger) {
@include mixins.temporary-toggle-active;
}
.tool-split-entry-list li.active > .tool-split-entry > .tool-split-entry-summary, .tool-split-entry-list li.active > .tool-split-entry > .tool-split-entry-summary,
.tool-split-entry-list li.active > .tool-split-entry > .tool-split-entry-value, .tool-split-entry-list li.active > .tool-split-entry > .tool-split-entry-value,
.tool-split-entry-list li.active > .tool-split-entry > .tool-split-entry-label { .tool-split-entry-list li.active > .tool-split-entry > .tool-split-entry-label {