Hints become a question mark, not a line of prose

"01 - YTD Sales · 02 - Open Orders can't move — the forecast absorbs
it" was printed under its toggle, and a line of prose beside a toggle is
skipped while still costing the dock a row. Every Field's hint is now a
? next to the control: hovering gives it (a native title, which nothing
clips), clicking pins it in a small popover.

The Per slice row goes with them -- what it said belongs to Apply, whose
help now covers both modes, and the ledger already marks its rows (all)
and (each).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Paul Trowbridge 2026-09-23 22:36:24 -04:00
parent 91b88cd809
commit e88f52203c

View File

@ -93,6 +93,30 @@ function Button({ onClick, active, children, title, disabled }) {
// "copy rows from" sitting above "scale cloned rows by" above "tag". // "copy rows from" sitting above "scale cloned rows by" above "tag".
const LABEL_W = 'w-24' const LABEL_W = 'w-24'
// Context on demand. The explanations used to be printed under the controls,
// which put a line of prose beside every toggle -- and a line of prose beside a
// toggle is skipped, while still costing the dock a row. Hovering gives it
// (native title, which nothing can clip), clicking pins it.
function Help({ text }) {
const [open, setOpen] = useState(false)
if (!text) return null
return (
<span className="relative inline-flex shrink-0">
<button type="button" title={text} onClick={() => setOpen(o => !o)} onBlur={() => setOpen(false)}
aria-label={text}
className={`w-4 h-4 rounded-full border leading-none flex items-center justify-center transition-colors ${
open ? 'border-blue-400 text-blue-600 bg-blue-50' : 'border-gray-300 text-gray-500 hover:text-blue-600 hover:border-blue-400'}`}
style={{ fontSize: '10px' }}>?</button>
{open && (
<span className="absolute z-20 left-0 top-5 w-64 rounded border border-gray-200 bg-white shadow-lg px-2 py-1.5
text-gray-600 leading-snug normal-case tracking-normal">
{text}
</span>
)}
</span>
)
}
// `stack` puts the label above its control instead of beside it. The aligned // `stack` puts the label above its control instead of beside it. The aligned
// label column costs 6rem, which is most of a narrow zone -- the tag field in // label column costs 6rem, which is most of a narrow zone -- the tag field in
// the bottom dock had 12rem of box left to type an initiative name into. // the bottom dock had 12rem of box left to type an initiative name into.
@ -100,18 +124,17 @@ function Field({ label, children, hint, stack }) {
if (stack) { if (stack) {
return ( return (
<div className="flex flex-col gap-1 min-w-0"> <div className="flex flex-col gap-1 min-w-0">
<span className="text-gray-600 whitespace-nowrap">{label}</span> <span className="text-gray-600 whitespace-nowrap flex items-center gap-1.5">{label}<Help text={hint} /></span>
<div className="flex items-center gap-2 min-w-0">{children}</div> <div className="flex items-center gap-2 min-w-0">{children}</div>
{hint && <p className="text-gray-500 text-[11px] leading-snug">{hint}</p>}
</div> </div>
) )
} }
return ( return (
<div className="flex items-start gap-2"> <div className="flex items-start gap-2">
<span className={`text-gray-600 whitespace-nowrap shrink-0 pt-1 ${LABEL_W}`}>{label}</span> <span className={`text-gray-600 whitespace-nowrap shrink-0 pt-1 ${LABEL_W}`}>{label}</span>
<div className="flex flex-col gap-1 min-w-0"> <div className="flex items-center gap-2 flex-wrap min-w-0">
<div className="flex items-center gap-2 flex-wrap">{children}</div> {children}
{hint && <p className="text-gray-500 text-[11px] leading-snug max-w-xs">{hint}</p>} <Help text={hint} />
</div> </div>
</div> </div>
) )
@ -1063,11 +1086,7 @@ function ScaleLedger({ currentTotals, scaleInputs, setScaleInputs, scalePlug, se
</Field> </Field>
)} )}
{perSlice && (
<Field label="Per slice">
<span className="text-gray-500">figures above are combined totals</span>
</Field>
)}
</div> </div>
)} )}
</div> </div>
@ -1342,7 +1361,10 @@ export default function OperationPanel({
// it is one line, they are one line, and stacked they cost three rows of a // it is one line, they are one line, and stacked they cost three rows of a
// dock that is 260px tall to begin with. // dock that is 260px tall to begin with.
const applyField = multi ? ( const applyField = multi ? (
<Field label="Apply"> <Field label="Apply"
hint={applyMode === 'each'
? `Each of the ${slices.length} slices reaches the target on its own, and each gets its own log entry. The ledger's figures are combined totals, so a slice's own change will differ from them.`
: `The ${slices.length} slices are one pool: a target is the new combined total, and each slice keeps its share of the mix. One log entry.`}>
<ApplyModeChooser op={activeOp} applyMode={applyMode} setApplyMode={setApplyMode} count={slices.length} /> <ApplyModeChooser op={activeOp} applyMode={applyMode} setApplyMode={setApplyMode} count={slices.length} />
</Field> </Field>
) : null ) : null