diff --git a/ui/src/components/OperationPanel.jsx b/ui/src/components/OperationPanel.jsx index a033f50..723f8af 100644 --- a/ui/src/components/OperationPanel.jsx +++ b/ui/src/components/OperationPanel.jsx @@ -61,11 +61,13 @@ function Block({ title, hint, horizontal, grow, children }) { ) } -function Button({ onClick, active, children, title }) { +function Button({ onClick, active, children, title, disabled }) { return ( - ) @@ -99,11 +101,16 @@ function SectionLabel({ children }) { ) } -function Segmented({ options, value, onChange }) { +// `disabled` keeps the control in place and shows which way it is set rather +// than removing it. A control that vanishes reads as broken; one that greys out +// says the choice has been made elsewhere. +function Segmented({ options, value, onChange, disabled }) { return ( -
+
{options.map(([val, label, title]) => ( - + ))}
) @@ -731,6 +738,34 @@ function ScaleLedger({ currentTotals, scaleInputs, setScaleInputs, scalePlug, se ? (scalePlug === 'price' ? 'units' : 'price') : null + // Which measure is absorbing the change, whether or not the toggle chose it. + // + // The toggle used to be rendered only while the edit was dollars-only, so it + // was absent before anything was typed and disappeared the moment a measure + // was pinned -- including by a click, since clicking a cell pins whatever it + // reads. Two different states both looked like the control was missing, and + // an accidental click into the qty column had no visible effect except that + // the control went away. It now stays put in every state and greys out when + // the inputs have already answered it, which they genuinely have: + // qty pinned -> price is what moves + // price pinned -> qty is what moves + // so the inert control still shows the truth rather than a stale choice. + const plugState = (() => { + if (!(valueCol && unitsCol)) return null + const u = filled('units'), p = filled('price'), v = filled('value') + if (u && p) return { value: null, live: false, why: 'Both qty and price are named, so the dollars follow from them.' } + if (u) return { value: 'price', live: false, why: `${unitsCol} is pinned, so price absorbs the change.`, unpin: 'units' } + if (p) return { value: 'volume', live: false, why: `Price is pinned, so ${unitsCol} absorbs the change.`, unpin: 'price' } + if (!v) return { value: null, live: false, why: 'Applies to a dollar figure entered on its own — nothing else names price or qty.' } + return { + value: scalePlug, + live: true, + why: scalePlug === 'price' + ? `${unitsCol} holds; price moves to reach the number.` + : `Price holds; ${unitsCol} scales with the dollars.`, + } + })() + const numCell = 'text-right font-mono tabular-nums whitespace-nowrap px-2' const rule =
@@ -909,27 +944,37 @@ function ScaleLedger({ currentTotals, scaleInputs, setScaleInputs, scalePlug, se - {/* A dollar figure on its own does not say whether price or volume moved. - Only ask once the edit is actually dollars-only -- naming units or price - has already answered it, and the control would just be noise. */} + {/* A dollar figure on its own does not say whether price or volume moved, + so the toggle answers for it. Kept visible even when it is not the one + answering -- see plugState -- because a control that comes and goes + reads as a fault, and its absence was the only sign that a stray click + had pinned a measure. */} {/* Both of these are settings on the edit, so they use Field like every other one -- an aligned label column and a hint below the control. They used to hand-roll their own label row and hint, with two different type sizes and no shared label width, so the two stacked into a paragraph of loose words rather than reading as two settings. */} - {(valueCol && unitsCol && dollarsOnly || hasExcl || perSlice) && ( + {(plugState || hasExcl || perSlice) && (
- {valueCol && unitsCol && dollarsOnly && ( - + {plugState && ( + + {plugState.unpin && ( + + )} )}