diff --git a/ui/src/components/OperationPanel.jsx b/ui/src/components/OperationPanel.jsx
index b0f8c56..566ff1a 100644
--- a/ui/src/components/OperationPanel.jsx
+++ b/ui/src/components/OperationPanel.jsx
@@ -61,6 +61,34 @@ function Button({ onClick, active, children, title }) {
)
}
+// A label and its control, on a grid. Every row in the panel uses the same label
+// width, so the controls line up down the column instead of each row starting
+// wherever its label happens to end -- which was the whole problem with
+// "copy rows from" sitting above "scale cloned rows by" above "tag".
+const LABEL_W = 'w-24'
+
+function Field({ label, children, hint }) {
+ return (
+
+ {label}
+
+
{children}
+ {hint &&
{hint}
}
+
+
+ )
+}
+
+// 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 }) {
+ return (
+
@@ -639,9 +667,16 @@ function DimForm({ dimCols, setObj, setSet, slices, lookupDerivedCols, dimMember
// Recode and clone change dimensions rather than amounts, but you still want to
// see how much is on the move — and for clone, what it becomes after scaling.
-function MovingTotal({ currentTotals, verb, factor }) {
- const t = currentTotals?.total
- if (!t) return null
+// includeExcluded: clone reads reference rows too, so its preview has to count
+// them. Reporting the adjustable total alone said "Copying 0.00" for a selection
+// made entirely of prior year or plan -- the exact case clone exists for.
+function MovingTotal({ currentTotals, verb, factor, includeExcluded }) {
+ const adj = currentTotals?.total
+ if (!adj) return null
+ const ex = currentTotals?.excluded
+ const t = includeExcluded && ex
+ ? { value: (adj.value || 0) + (ex.value || 0), units: (adj.units || 0) + (ex.units || 0) }
+ : adj
const { valueCol, unitsCol } = currentTotals
const scaled = factor != null && factor !== 1
return (
@@ -725,6 +760,7 @@ export default function OperationPanel({
const horizontal = dock === 'bottom'
const note = activeOp === 'scale' ? scaleNote : activeOp === 'recode' ? recodeNote : cloneNote
+ const shifting = !!cloneOffset && cloneOffset.trim() !== '' && cloneOffset.trim() !== '0 days'
const setNote = activeOp === 'scale' ? setScaleNote : activeOp === 'recode' ? setRecodeNote : setCloneNote
const OP_LABEL = { scale: 'Apply Scale', recode: 'Apply Recode', clone: 'Apply Clone' }
@@ -779,24 +815,32 @@ export default function OperationPanel({
{activeOp === 'recode' && (
} />
+ extra={
+
+
+
+ } />
)}
{activeOp === 'clone' && (
- {/* The selection is the SOURCE, not the destination: pick
- the cells you want to copy -- last December, say -- and
- the shift below is what lands them in the target period.
- Naming a segment narrows that selection to one entry,
- including the 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. */}
-
- copy rows from
+
+ source
+
+ {/* The selection is the SOURCE, not the destination: pick the
+ cells you want to copy -- last December, say -- and the
+ shift is what lands them in the target period. Naming a
+ segment narrows that selection to one entry, including the
+ 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. */}
+
-
+
- {/* Always shown. This was gated on picking a segment, from
- when naming one was the only way to reach reference rows
- -- once clone could read them directly, the offset became
- unreachable in the ordinary case. */}
-
- The cells you have selected are what gets copied. Shifting
- moves every date column, and the season and month
- dimensions are re-derived from the calendar to match — so
- select the period you are copying from, not the
- one you are filling.
-
- {!cloneFrom && (
+ onChange={e => setCloneScale(e.target.value)}
+ className={`${INPUT} w-28`} />
+ ×
+
+
+ {/* Sits under the controls it reflects rather than floating
+ after them, and counts the reference rows clone can now
+ read -- it used to report the adjustable total, which is
+ zero when the selection is entirely prior year or plan. */}
+
+ label this change
+
{/* 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. */}
-
- tag
+
setOpTag(e.target.value)}
list="pf-tag-options" placeholder="initiative, e.g. reduce_spend"
@@ -865,27 +908,35 @@ export default function OperationPanel({
)}
-
+
+
+ {/* Under the tag field and inside the same column, so they read as
+ values for it rather than as a row of unexplained buttons. */}
{knownTags.length > 0 && (
-