Line the panel's buttons up, and cut the prose around them
Every button group now reads `label [ A | B ]` on one label column, so nothing is staggered: change, apply, absorbs, target, per slice. The strip and the ledger zone share the same left edge, so the alignment holds across the two. - Together/Each sits under the operation it qualifies rather than loose in the middle of the strip, where it read as a peer of the operation - The apply-mode explanation is gone; the detail is on each option's tooltip. A sentence under a control is read once and skipped after, and this one cost the strip its height on every render - The plug, target, per-slice and clone hints are a few words each Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
d01dd2dbc8
commit
20ed8e7484
@ -834,17 +834,17 @@ function ScaleLedger({ currentTotals, scaleInputs, setScaleInputs, scalePlug, se
|
||||
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 && v) return { value: null, live: false, why: `Both ${valueCol} and ${unitsCol} are named, so price falls out of the two — this is the deliberate price change.` }
|
||||
if (u) return { value: null, live: false, why: `Price holds; ${valueCol} follows ${unitsCol} in proportion. Name the dollars as well to move price instead.`, 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.' }
|
||||
if (u && p) return { value: null, live: false, why: `${unitsCol} and price named — ${valueCol} follows` }
|
||||
if (u && v) return { value: null, live: false, why: `${valueCol} and ${unitsCol} named — price follows` }
|
||||
if (u) return { value: null, live: false, why: `price holds — ${valueCol} follows ${unitsCol}`, unpin: 'units' }
|
||||
if (p) return { value: 'volume', live: false, why: `price pinned — ${unitsCol} absorbs it`, unpin: 'price' }
|
||||
if (!v) return { value: null, live: false, why: `for a ${valueCol} figure on its own` }
|
||||
return {
|
||||
value: scalePlug,
|
||||
live: true,
|
||||
why: scalePlug === 'price'
|
||||
? `${unitsCol} holds; price moves to reach the number.`
|
||||
: `Price holds; ${unitsCol} scales with the dollars.`,
|
||||
? `${unitsCol} holds — price moves`
|
||||
: `price holds — ${unitsCol} moves`,
|
||||
}
|
||||
})()
|
||||
|
||||
@ -1046,7 +1046,7 @@ function ScaleLedger({ currentTotals, scaleInputs, setScaleInputs, scalePlug, se
|
||||
<div className="flex flex-col gap-2 mt-3 pt-3 border-t border-gray-100">
|
||||
|
||||
{plugState && (
|
||||
<Field label="Absorbed by" hint={plugState.why}>
|
||||
<Field label="absorbs" hint={plugState.why}>
|
||||
<Segmented
|
||||
value={plugState.value}
|
||||
onChange={setScalePlug}
|
||||
@ -1066,10 +1066,10 @@ function ScaleLedger({ currentTotals, scaleInputs, setScaleInputs, scalePlug, se
|
||||
)}
|
||||
|
||||
{hasExcl && (
|
||||
<Field label="Target on"
|
||||
<Field label="target"
|
||||
hint={onTotal
|
||||
? `${exclName} can't change, so the adjustable rows absorb the difference. The pivot will show your target.`
|
||||
: `${exclName} ignored. The pivot will show your target plus ${fmtNum(excl.value)}.`}>
|
||||
? `${exclName} can't move — the forecast absorbs it`
|
||||
: `${exclName} left out — the pivot will show your number plus ${fmtNum(excl.value)}`}>
|
||||
{/* "Adjustable" and "Selected total" are the writer's words for
|
||||
it -- which rows the SQL may touch. What a rep is choosing is
|
||||
whether the number they type is the forecast alone or the
|
||||
@ -1087,9 +1087,8 @@ function ScaleLedger({ currentTotals, scaleInputs, setScaleInputs, scalePlug, se
|
||||
)}
|
||||
|
||||
{perSlice && (
|
||||
<Field label="Per slice"
|
||||
hint="The figures above are combined totals, so each slice's own change will differ.">
|
||||
<span className="text-gray-500">Applied to each slice separately</span>
|
||||
<Field label="per slice">
|
||||
<span className="text-gray-500">figures above are combined totals</span>
|
||||
</Field>
|
||||
)}
|
||||
</div>
|
||||
@ -1233,6 +1232,9 @@ function MovingTotal({ currentTotals, verb, factor, includeExcluded }) {
|
||||
}
|
||||
|
||||
// ── Apply mode ──────────────────────────────────────────────────────────────
|
||||
// The detail lives on each option's tooltip. It used to be a sentence printed
|
||||
// under the control, and a sentence under a control is read once and skipped
|
||||
// forever after -- while costing the top strip its height on every render.
|
||||
function ApplyModeChooser({ op, applyMode, setApplyMode, count }) {
|
||||
const options = op === 'scale'
|
||||
? [
|
||||
@ -1243,14 +1245,7 @@ function ApplyModeChooser({ op, applyMode, setApplyMode, count }) {
|
||||
['prorate', 'Together', `One operation over all ${count} slices — a single log entry.`],
|
||||
['each', 'Each', `One operation per slice — ${count} log entries, undoable separately.`],
|
||||
]
|
||||
const active = options.find(([v]) => v === applyMode)
|
||||
return (
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Segmented value={applyMode} onChange={setApplyMode}
|
||||
options={options.map(([v, l, t]) => [v, l, t])} />
|
||||
{active && <p className="text-gray-600 leading-snug max-w-xs">{active[2]}</p>}
|
||||
</div>
|
||||
)
|
||||
return <Segmented value={applyMode} onChange={setApplyMode} options={options} />
|
||||
}
|
||||
|
||||
function RequestPreview({ payload }) {
|
||||
@ -1351,9 +1346,7 @@ export default function OperationPanel({
|
||||
which is the point: a period with no baseline borrows its
|
||||
shape from prior year or plan. */}
|
||||
<Field label="rows from"
|
||||
hint={cloneFrom
|
||||
? 'Only rows from that segment, out of everything selected.'
|
||||
: undefined}>
|
||||
hint={cloneFrom ? 'only that segment, out of the selection' : undefined}>
|
||||
<select value={cloneFrom} onChange={e => setCloneFrom(e.target.value)}
|
||||
className={`${TEXT} w-48`}>
|
||||
<option value="">the whole selection</option>
|
||||
@ -1369,7 +1362,7 @@ export default function OperationPanel({
|
||||
|
||||
<Field label="shift dates"
|
||||
hint={shifting
|
||||
? 'Every date column moves; season and month are re-derived from the calendar. So select the period you are copying from, not the one you are filling.'
|
||||
? 'every date moves — select the period you copy FROM'
|
||||
: undefined}>
|
||||
<input value={cloneOffset} list="pf-clone-offsets"
|
||||
onChange={e => setCloneOffset(e.target.value)}
|
||||
@ -1444,35 +1437,37 @@ export default function OperationPanel({
|
||||
The operation used to be a row of tabs buried in the middle column
|
||||
with no heading over it, so the panel never said what it was about
|
||||
to do until you read the button. */}
|
||||
<div className="flex items-center gap-3 flex-wrap px-3 py-2 border-b border-gray-200 bg-gray-50/60">
|
||||
<div className="inline-flex rounded border border-gray-200 divide-x divide-gray-200 overflow-hidden">
|
||||
{['scale', 'recode', 'clone'].map(op => (
|
||||
<Button key={op} onClick={() => setActiveOp(op)} active={activeOp === op}>
|
||||
<span className="capitalize">{op}</span>
|
||||
</Button>
|
||||
))}
|
||||
<div className="flex flex-col gap-2 px-3 py-2 border-b border-gray-200 bg-gray-50/60">
|
||||
<div className="flex items-start gap-4 flex-wrap">
|
||||
<Field label="change">
|
||||
<div className="inline-flex rounded border border-gray-200 divide-x divide-gray-200 overflow-hidden">
|
||||
{['scale', 'recode', 'clone'].map(op => (
|
||||
<Button key={op} onClick={() => setActiveOp(op)} active={activeOp === op}>
|
||||
<span className="capitalize">{op}</span>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</Field>
|
||||
|
||||
{hasSlice && (
|
||||
<div className="ml-auto flex items-center gap-3 pt-0.5">
|
||||
<SliceSummary slices={slices} viewScope={viewScope} distinctSlices={distinctSlices}
|
||||
currentTotals={currentTotals} open={peek} onToggle={() => setPeek(o => !o)} />
|
||||
{opBusy && <span className="text-gray-500">writing rows…</span>}
|
||||
<Submit onClick={() => submitOp(activeOp)} busy={!!opBusy}>
|
||||
{opBusy ? POSTING : POST}
|
||||
</Submit>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Under the operation it qualifies, on the same label column as every
|
||||
other button row -- it used to sit loose in the middle of the strip,
|
||||
where it read as a peer of the operation rather than a setting on it. */}
|
||||
{hasSlice && multi && (
|
||||
<ApplyModeChooser op={activeOp} applyMode={applyMode} setApplyMode={setApplyMode} count={slices.length} />
|
||||
)}
|
||||
|
||||
{hasSlice && (
|
||||
<SliceSummary slices={slices} viewScope={viewScope} distinctSlices={distinctSlices}
|
||||
currentTotals={currentTotals} open={peek} onToggle={() => setPeek(o => !o)} />
|
||||
)}
|
||||
|
||||
{hasSlice && (
|
||||
<div className="ml-auto flex items-center gap-3">
|
||||
{opBusy && (
|
||||
<span className="text-gray-500 leading-snug">
|
||||
Writing rows — the pivot updates when it finishes.
|
||||
</span>
|
||||
)}
|
||||
<Submit onClick={() => submitOp(activeOp)} busy={!!opBusy}>
|
||||
{opBusy ? POSTING : POST}
|
||||
</Submit>
|
||||
</div>
|
||||
<Field label="apply">
|
||||
<ApplyModeChooser op={activeOp} applyMode={applyMode} setApplyMode={setApplyMode} count={slices.length} />
|
||||
</Field>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user