Keep the price/volume choice on screen when it isn't the one choosing
"Absorbed by" was rendered only while the edit was dollars-only, which made it
absent in two quite different states: before anything had been typed, and after
a measure had been pinned. Pinning includes a *click* -- clicking a cell pins
whatever it reads -- so an accidental click in the qty column had no visible
effect except that the control disappeared, and the way back was a small x in a
table header.
It now stays put and greys out, still showing which measure absorbs the change,
because the pinned inputs answer the same question the toggle does:
qty pinned -> price is what moves
price pinned -> qty is what moves
so the inert control reads as the truth rather than a stale choice. The hint
says which case it is in, and a pinned measure gets an "unpin" next to the
toggle rather than only the x in the header.
Nothing about what gets written changes: `plug` is still sent only when the edit
is dollars-only, which is still the only case where price and volume are both
unknown.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7006014cc3
commit
9e880a65be
@ -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 (
|
||||
<button onClick={onClick} title={title}
|
||||
<button onClick={onClick} title={title} disabled={disabled}
|
||||
className={`px-3 py-1 rounded text-xs whitespace-nowrap transition-colors ${
|
||||
active ? 'bg-blue-600 text-white' : 'bg-white text-gray-700 hover:bg-gray-50'}`}>
|
||||
disabled
|
||||
? (active ? 'bg-gray-300 text-white cursor-default' : 'bg-white text-gray-400 cursor-default')
|
||||
: (active ? 'bg-blue-600 text-white' : 'bg-white text-gray-700 hover:bg-gray-50')}`}>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
@ -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 (
|
||||
<div className="inline-flex rounded border border-gray-200 overflow-hidden w-auto self-start">
|
||||
<div className={`inline-flex rounded border overflow-hidden w-auto self-start ${
|
||||
disabled ? 'border-gray-200 bg-gray-50' : 'border-gray-200'}`}>
|
||||
{options.map(([val, label, title]) => (
|
||||
<Button key={val} onClick={() => onChange(val)} active={value === val} title={title}>{label}</Button>
|
||||
<Button key={val} onClick={() => onChange(val)} active={value === val}
|
||||
title={title} disabled={disabled}>{label}</Button>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
@ -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 = <td className="p-0"><div className="border-t border-gray-300 my-1" /></td>
|
||||
|
||||
@ -909,27 +944,37 @@ function ScaleLedger({ currentTotals, scaleInputs, setScaleInputs, scalePlug, se
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{/* 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) && (
|
||||
<div className="flex flex-col gap-2 mt-3 pt-3 border-t border-gray-100">
|
||||
|
||||
{valueCol && unitsCol && dollarsOnly && (
|
||||
<Field label="Absorbed by">
|
||||
{plugState && (
|
||||
<Field label="Absorbed by" hint={plugState.why}>
|
||||
<Segmented
|
||||
value={scalePlug}
|
||||
value={plugState.value}
|
||||
onChange={setScalePlug}
|
||||
disabled={!plugState.live}
|
||||
options={[
|
||||
['price', 'Price', `${unitsCol} holds; price moves to reach the number`],
|
||||
['volume', 'Volume', `Price holds; ${unitsCol} scales with the dollars`],
|
||||
]}
|
||||
/>
|
||||
{plugState.unpin && (
|
||||
<button onClick={() => clearEdit(plugState.unpin)}
|
||||
title={`Stop pinning ${plugState.unpin === 'units' ? unitsCol : 'price'} and choose here instead`}
|
||||
className="text-blue-600 hover:underline">
|
||||
unpin {plugState.unpin === 'units' ? unitsCol : 'price'}
|
||||
</button>
|
||||
)}
|
||||
</Field>
|
||||
)}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user