Back-fill the measures an edit forces
Scaling dollars with price holding moves qty, but the qty column stayed blank until the Result row -- so the number you had just implied was the one thing not shown beside the number you typed. A measure with no edit of its own now reads all three ways from where the edit actually lands it. derived stays the edit-driven map, since it is what outcome is computed from and back-filling into it would feed the result into its own inputs; a separate shown map is what the rows render. Clicking into a back-filled figure pins it, which is the difference between "units may move" and "units are this". The write is the same either way when the pinned number is the implied one, so the cost of pinning by accident is nothing. Reverts the muted styling for the derived rows -- all three boxes look like boxes again. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
1035dae76b
commit
042c85d6c3
@ -237,15 +237,6 @@ const FIELDS = [
|
|||||||
['pct', '% change'],
|
['pct', '% change'],
|
||||||
]
|
]
|
||||||
|
|
||||||
// Exactly one box per measure is live -- scaleInputs is keyed by measure, so
|
|
||||||
// focusing another row hands that measure's edit over rather than adding a
|
|
||||||
// second one. The other two are the same number said differently, so they are
|
|
||||||
// drawn as output: no border, no ground, muted. They are still editable, and a
|
|
||||||
// border on hover is what says so.
|
|
||||||
//
|
|
||||||
// Both states used to be full input boxes differing by a border colour, which
|
|
||||||
// made three typed figures out of one typed figure and two calculations.
|
|
||||||
//
|
|
||||||
// given the active edit for a measure, what do the three rows read?
|
// given the active edit for a measure, what do the three rows read?
|
||||||
// dp is the measure's own precision, so the editable rows read the same way as
|
// dp is the measure's own precision, so the editable rows read the same way as
|
||||||
// the lines above them -- whole dollars against whole dollars, five places
|
// the lines above them -- whole dollars against whole dollars, five places
|
||||||
@ -337,16 +328,11 @@ function LedgerInput({ value, active, onChange, onFocus, suffix }) {
|
|||||||
onChange(el.value.replace(/,/g, ''))
|
onChange(el.value.replace(/,/g, ''))
|
||||||
}}
|
}}
|
||||||
onFocus={onFocus} placeholder="—"
|
onFocus={onFocus} placeholder="—"
|
||||||
title={active ? undefined : 'Derived — click to enter the number this way instead'}
|
|
||||||
className={`border rounded py-0.5 text-xs w-24 text-right font-mono tabular-nums
|
className={`border rounded py-0.5 text-xs w-24 text-right font-mono tabular-nums
|
||||||
transition-colors
|
|
||||||
${suffix ? 'pl-2 pr-5' : 'px-2'}
|
${suffix ? 'pl-2 pr-5' : 'px-2'}
|
||||||
${active
|
${active ? 'border-blue-400 bg-blue-50/40 text-gray-800' : 'border-gray-200 bg-white text-gray-700'}`} />
|
||||||
? 'border-blue-400 bg-blue-50/40 text-gray-900 font-medium'
|
|
||||||
: 'border-transparent bg-transparent text-gray-400 hover:border-gray-300 hover:bg-white'}`} />
|
|
||||||
{suffix && (
|
{suffix && (
|
||||||
<span className={`absolute right-1.5 pointer-events-none
|
<span className="absolute right-1.5 text-gray-400 pointer-events-none"
|
||||||
${active ? 'text-gray-500' : 'text-gray-300'}`}
|
|
||||||
style={{ fontSize: '10px' }}>{suffix}</span>
|
style={{ fontSize: '10px' }}>{suffix}</span>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
@ -501,6 +487,30 @@ function ScaleLedger({ currentTotals, scaleInputs, setScaleInputs, scalePlug, se
|
|||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
|
|
||||||
|
// The same three readings for a measure nobody typed into, taken from where
|
||||||
|
// the edit actually lands it. Scaling dollars with price holding moves qty,
|
||||||
|
// and leaving that column blank until the Result row meant the number you had
|
||||||
|
// just implied was the one thing not shown next to the one you typed.
|
||||||
|
const backfill = (current, next, dp) => {
|
||||||
|
if (next == null || !isFinite(next) || current == null) return { new: '', change: '', pct: '' }
|
||||||
|
const change = next - current
|
||||||
|
const pct = current === 0 ? null : (change / Math.abs(current)) * 100
|
||||||
|
return {
|
||||||
|
new: fmtNum(next, dp),
|
||||||
|
change: fmtNum(change, dp),
|
||||||
|
pct: pct == null ? '—' : fmtNum(pct, 1),
|
||||||
|
n: { new: next, change, pct },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// What the rows render. derived stays the edit-driven map -- it is what
|
||||||
|
// outcome is computed from, so back-filling into it would feed the result
|
||||||
|
// back into its own inputs.
|
||||||
|
const shown = Object.fromEntries(measures.map(m => [
|
||||||
|
m.key,
|
||||||
|
(filled(m.key) || !outcome) ? derived[m.key] : backfill(basisOf(m.key), outcome[m.key], m.dp),
|
||||||
|
]))
|
||||||
|
|
||||||
const setEdit = (key, field, raw) =>
|
const setEdit = (key, field, raw) =>
|
||||||
setScaleInputs(prev => ({ ...prev, [key]: { field, raw } }))
|
setScaleInputs(prev => ({ ...prev, [key]: { field, raw } }))
|
||||||
|
|
||||||
@ -510,9 +520,12 @@ function ScaleLedger({ currentTotals, scaleInputs, setScaleInputs, scalePlug, se
|
|||||||
setScaleInputs(prev => {
|
setScaleInputs(prev => {
|
||||||
const cur = prev[key]
|
const cur = prev[key]
|
||||||
if (cur && cur.field === field) return prev
|
if (cur && cur.field === field) return prev
|
||||||
const shown = derived[key]?.[field] ?? ''
|
// Whatever the box reads is what you get, including a back-filled
|
||||||
const raw = shown === '—' ? '' : String(shown).replace(/,/g, '')
|
// number: clicking into an implied figure is how you pin it, which is
|
||||||
return { ...prev, [key]: { field, raw: cur ? raw : '' } }
|
// the difference between "units may move" and "units are this".
|
||||||
|
const text = shown[key]?.[field] ?? ''
|
||||||
|
const raw = text === '—' ? '' : String(text).replace(/,/g, '')
|
||||||
|
return { ...prev, [key]: { field, raw } }
|
||||||
})
|
})
|
||||||
|
|
||||||
// A running total across the walk, in the primary measure. The lines sum to
|
// A running total across the walk, in the primary measure. The lines sum to
|
||||||
@ -661,7 +674,7 @@ function ScaleLedger({ currentTotals, scaleInputs, setScaleInputs, scalePlug, se
|
|||||||
return (
|
return (
|
||||||
<td key={m.key} className="px-2 py-0.5 text-right">
|
<td key={m.key} className="px-2 py-0.5 text-right">
|
||||||
<LedgerInput
|
<LedgerInput
|
||||||
value={derived[m.key]?.[field] ?? ''}
|
value={shown[m.key]?.[field] ?? ''}
|
||||||
active={active}
|
active={active}
|
||||||
onChange={(raw) => setEdit(m.key, field, raw)}
|
onChange={(raw) => setEdit(m.key, field, raw)}
|
||||||
onFocus={() => focusRow(m.key, field)}
|
onFocus={() => focusRow(m.key, field)}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user