Name every section, and give the buttons some weight
- Button labels are capitalised and font-medium, the segmented groups carry a border and a shadow of their own. An inactive segment used to be white text-on-white inside a group border, which is why none of them read as something you could press. - Each section says what it is: the operation strip, LEDGER (or RECODE TO / CLONE TO, which is what the form under those two operations is), SETTINGS over the toggles, LABEL THIS CHANGE. - Together/Each moves down beside Price/Volume and Forecast/Total, in a wrapping row. Three one-line settings stacked cost three rows of a dock that is 260px tall to begin with, and they are read together anyway: what absorbs the change, what it is measured against, and whether it applies once or per slice. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
4bce3bbd17
commit
91b88cd809
@ -78,10 +78,10 @@ function TextButton({ onClick, children, title, danger }) {
|
||||
function Button({ onClick, active, children, title, disabled }) {
|
||||
return (
|
||||
<button onClick={onClick} title={title} disabled={disabled}
|
||||
className={`px-3 py-1 rounded text-xs whitespace-nowrap transition-colors ${
|
||||
className={`px-3 py-1 text-xs font-medium whitespace-nowrap transition-colors ${
|
||||
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')}`}>
|
||||
: (active ? 'bg-blue-600 text-white' : 'bg-white text-gray-700 hover:bg-gray-100')}`}>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
@ -119,9 +119,9 @@ function Field({ label, children, hint, stack }) {
|
||||
|
||||
// Same 10px uppercase as the ledger table headers, so the groupings read as part
|
||||
// of the same family rather than as a second style.
|
||||
function SectionLabel({ children }) {
|
||||
function SectionLabel({ children, className = '' }) {
|
||||
return (
|
||||
<div className="text-gray-400 uppercase tracking-wide pt-1" style={{ fontSize: '10px' }}>
|
||||
<div className={`text-gray-500 uppercase tracking-wide pt-1 ${className}`} style={{ fontSize: '10px' }}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
@ -132,8 +132,8 @@ function SectionLabel({ children }) {
|
||||
// says the choice has been made elsewhere.
|
||||
function Segmented({ options, value, onChange, disabled }) {
|
||||
return (
|
||||
<div className={`inline-flex rounded border divide-x divide-gray-200 overflow-hidden w-auto self-start ${
|
||||
disabled ? 'border-gray-200 bg-gray-50' : 'border-gray-200'}`}>
|
||||
<div className={`inline-flex rounded border divide-x shadow-sm overflow-hidden w-auto self-start ${
|
||||
disabled ? 'border-gray-200 divide-gray-200 bg-gray-50' : 'border-gray-300 divide-gray-300'}`}>
|
||||
{options.map(([val, label, title]) => (
|
||||
<Button key={val} onClick={() => onChange(val)} active={value === val}
|
||||
title={title} disabled={disabled}>{label}</Button>
|
||||
@ -145,7 +145,7 @@ function Segmented({ options, value, onChange, disabled }) {
|
||||
function Submit({ onClick, children, disabled, busy }) {
|
||||
return (
|
||||
<button onClick={onClick} disabled={disabled || busy}
|
||||
className="self-start px-4 py-1.5 rounded text-xs font-medium bg-blue-600 text-white hover:bg-blue-700
|
||||
className="self-start px-4 py-1.5 rounded text-xs font-semibold shadow-sm bg-blue-600 text-white hover:bg-blue-700
|
||||
disabled:bg-gray-200 disabled:text-gray-600 disabled:cursor-not-allowed whitespace-nowrap
|
||||
inline-flex items-center gap-2">
|
||||
{busy && (
|
||||
@ -254,7 +254,7 @@ function IterBreakdown({ currentTotals }) {
|
||||
return (
|
||||
<div className="mt-2">
|
||||
<TextButton onClick={() => setOpen(o => !o)}>
|
||||
{open ? '▾' : '▸'} breakdown by iter
|
||||
{open ? '▾' : '▸'} Breakdown by iter
|
||||
</TextButton>
|
||||
{open && (
|
||||
<table className="mt-1 text-gray-500">
|
||||
@ -496,7 +496,7 @@ function TagInput({ value, onChange, tags = [], className }) {
|
||||
)
|
||||
}
|
||||
|
||||
function ScaleLedger({ currentTotals, scaleInputs, setScaleInputs, scalePlug, setScalePlug, targetBasis, setTargetBasis, logMeta = {}, multi, applyMode }) {
|
||||
function ScaleLedger({ currentTotals, scaleInputs, setScaleInputs, scalePlug, setScalePlug, targetBasis, setTargetBasis, logMeta = {}, multi, applyMode, settingsExtra }) {
|
||||
const valueCol = currentTotals?.valueCol
|
||||
const unitsCol = currentTotals?.unitsCol
|
||||
const total = currentTotals?.total || { value: 0, units: 0 }
|
||||
@ -884,7 +884,7 @@ function ScaleLedger({ currentTotals, scaleInputs, setScaleInputs, scalePlug, se
|
||||
{showAllLines && adjLines.length > MAX_ADJ_LINES + 1 && (
|
||||
<tr>
|
||||
<td colSpan={measures.length + 1 + (showRunning ? 1 : 0)} className="pr-3">
|
||||
<TextButton onClick={() => setShowAllLines(false)}>▾ collapse the smaller changes</TextButton>
|
||||
<TextButton onClick={() => setShowAllLines(false)}>▾ Collapse the smaller changes</TextButton>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
@ -1013,11 +1013,17 @@ function ScaleLedger({ currentTotals, scaleInputs, setScaleInputs, scalePlug, se
|
||||
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. */}
|
||||
{(plugState || hasExcl || perSlice) && (
|
||||
<div className="flex flex-col gap-2 mt-3 pt-3 border-t border-gray-100">
|
||||
{(plugState || hasExcl || perSlice || settingsExtra) && (
|
||||
// Side by side, not stacked: three one-line settings in a column cost
|
||||
// three rows of a dock that is 260px tall to begin with, and they are
|
||||
// read together anyway -- what absorbs the change, what it is measured
|
||||
// against, and whether it applies once or per slice.
|
||||
<div className="flex flex-wrap items-start gap-x-8 gap-y-2 mt-3 pt-3 border-t border-gray-200">
|
||||
<SectionLabel className="w-full">settings</SectionLabel>
|
||||
{settingsExtra}
|
||||
|
||||
{plugState && (
|
||||
<Field label="absorbs" hint={plugState.why}>
|
||||
<Field label="Absorbs" hint={plugState.why}>
|
||||
<Segmented
|
||||
value={plugState.value}
|
||||
onChange={setScalePlug}
|
||||
@ -1037,7 +1043,7 @@ function ScaleLedger({ currentTotals, scaleInputs, setScaleInputs, scalePlug, se
|
||||
)}
|
||||
|
||||
{hasExcl && (
|
||||
<Field label="target"
|
||||
<Field label="Target"
|
||||
hint={onTotal
|
||||
? `${exclName} can't move — the forecast absorbs it`
|
||||
: `${exclName} left out — the pivot will show your number plus ${fmtNum(excl.value)}`}>
|
||||
@ -1058,7 +1064,7 @@ function ScaleLedger({ currentTotals, scaleInputs, setScaleInputs, scalePlug, se
|
||||
)}
|
||||
|
||||
{perSlice && (
|
||||
<Field label="per slice">
|
||||
<Field label="Per slice">
|
||||
<span className="text-gray-500">figures above are combined totals</span>
|
||||
</Field>
|
||||
)}
|
||||
@ -1272,7 +1278,7 @@ function RequestPreview({ payload, op, versionId }) {
|
||||
<span className="font-medium text-gray-700 text-sm">Request</span>
|
||||
<span className="font-mono text-gray-600 text-xs truncate" title={line}>{line}</span>
|
||||
<div className="ml-auto flex items-center gap-3">
|
||||
<TextButton onClick={copy}>{copied ? 'copied' : 'copy'}</TextButton>
|
||||
<TextButton onClick={copy}>{copied ? 'Copied' : 'Copy'}</TextButton>
|
||||
<button onClick={() => setOpen(false)}
|
||||
className="text-gray-600 hover:text-gray-800 text-lg leading-none">×</button>
|
||||
</div>
|
||||
@ -1332,6 +1338,15 @@ export default function OperationPanel({
|
||||
// operation is named by the tabs beside it.
|
||||
const POST = 'Post', POSTING = 'Posting…'
|
||||
|
||||
// Sits with the ledger's own settings rather than under the operation tabs:
|
||||
// it is one line, they are one line, and stacked they cost three rows of a
|
||||
// dock that is 260px tall to begin with.
|
||||
const applyField = multi ? (
|
||||
<Field label="Apply">
|
||||
<ApplyModeChooser op={activeOp} applyMode={applyMode} setApplyMode={setApplyMode} count={slices.length} />
|
||||
</Field>
|
||||
) : null
|
||||
|
||||
const ledger = (
|
||||
<>
|
||||
{activeOp === 'scale' && (
|
||||
@ -1342,6 +1357,7 @@ export default function OperationPanel({
|
||||
targetBasis={targetBasis} setTargetBasis={setTargetBasis}
|
||||
logMeta={logMeta}
|
||||
multi={multi} applyMode={applyMode}
|
||||
settingsExtra={applyField}
|
||||
/>
|
||||
)}
|
||||
{activeOp === 'recode' && (
|
||||
@ -1367,7 +1383,7 @@ export default function OperationPanel({
|
||||
reference ones operations are normally kept away from,
|
||||
which is the point: a period with no baseline borrows its
|
||||
shape from prior year or plan. */}
|
||||
<Field label="rows from"
|
||||
<Field label="Rows from"
|
||||
hint={cloneFrom ? 'only that segment, out of the selection' : undefined}>
|
||||
<select value={cloneFrom} onChange={e => setCloneFrom(e.target.value)}
|
||||
className={`${TEXT} w-48`}>
|
||||
@ -1382,7 +1398,7 @@ export default function OperationPanel({
|
||||
|
||||
<SectionLabel>changes</SectionLabel>
|
||||
|
||||
<Field label="shift dates"
|
||||
<Field label="Shift dates"
|
||||
hint={shifting
|
||||
? 'every date moves — select the period you copy FROM'
|
||||
: undefined}>
|
||||
@ -1398,7 +1414,7 @@ export default function OperationPanel({
|
||||
</datalist>
|
||||
</Field>
|
||||
|
||||
<Field label="scale by">
|
||||
<Field label="Scale by">
|
||||
<input type="number" step="any" value={cloneScale}
|
||||
onChange={e => setCloneScale(e.target.value)}
|
||||
className={`${INPUT} w-28`} />
|
||||
@ -1417,6 +1433,12 @@ export default function OperationPanel({
|
||||
</div>
|
||||
} />
|
||||
)}
|
||||
{activeOp !== 'scale' && applyField && (
|
||||
<div className="flex flex-wrap items-start gap-x-8 gap-y-2 mt-3 pt-3 border-t border-gray-200">
|
||||
<SectionLabel className="w-full">settings</SectionLabel>
|
||||
{applyField}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
||||
@ -1425,7 +1447,7 @@ export default function OperationPanel({
|
||||
{/* Tag first: it is the field that gives an adjustment meaning later,
|
||||
in the ledger and in the bridge. Completes from initiatives already
|
||||
used on this source; free text is still accepted. */}
|
||||
<Field label="tag" stack>
|
||||
<Field label="Tag" stack>
|
||||
<TagInput value={opTag} onChange={setOpTag} tags={knownTags}
|
||||
className={`${TEXT} w-full`} />
|
||||
{opTag.trim() && (
|
||||
@ -1441,7 +1463,7 @@ export default function OperationPanel({
|
||||
border. The same tags complete from the input's datalist, which
|
||||
costs no space and is not capped at six. */}
|
||||
|
||||
<Field label="note" stack>
|
||||
<Field label="Note" stack>
|
||||
<input value={note} onChange={e => setNote(e.target.value)}
|
||||
placeholder="optional" className={`${TEXT} w-full`} />
|
||||
</Field>
|
||||
@ -1472,10 +1494,9 @@ 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 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">
|
||||
<div className="flex items-start gap-4 flex-wrap px-3 py-2 border-b border-gray-200 bg-gray-50/60">
|
||||
<Field label="Change">
|
||||
<div className="inline-flex rounded border border-gray-300 divide-x divide-gray-300 shadow-sm overflow-hidden">
|
||||
{['scale', 'recode', 'clone'].map(op => (
|
||||
<Button key={op} onClick={() => setActiveOp(op)} active={activeOp === op}>
|
||||
<span className="capitalize">{op}</span>
|
||||
@ -1493,16 +1514,6 @@ export default function OperationPanel({
|
||||
)}
|
||||
</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 && (
|
||||
<Field label="apply">
|
||||
<ApplyModeChooser op={activeOp} applyMode={applyMode} setApplyMode={setApplyMode} count={slices.length} />
|
||||
</Field>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{peek && hasSlice && (
|
||||
<div className="px-3 py-2 border-b border-gray-200 bg-gray-50/60">
|
||||
<SelectionList
|
||||
@ -1535,7 +1546,8 @@ export default function OperationPanel({
|
||||
style={horizontal
|
||||
? { display: 'grid', gridTemplateColumns: 'minmax(0, 1fr) minmax(15rem, 20rem)', alignItems: 'start' }
|
||||
: undefined}>
|
||||
<Block horizontal={horizontal} grow>{ledger}</Block>
|
||||
<Block title={activeOp === 'scale' ? 'Ledger' : `${activeOp} to`}
|
||||
horizontal={horizontal} grow>{ledger}</Block>
|
||||
<Block title="Label this change" horizontal={horizontal}>{labelZone}</Block>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user