console: the immediate action lives top-right, not below the fold

"What do I do now" was answered only by scrolling past the metrics and stage
rail to the Next step panel. When this user owns the next step it now sits in
the header top-right as a prominent amber button — where the eye goes for the
next move — and pressing it opens the form and scrolls to it. When nothing is
owed, the passive status stays there instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Yashas 2026-09-09 01:42:36 +05:30
parent 60f6c97201
commit 17561f3f73
2 changed files with 64 additions and 2 deletions

View File

@ -319,6 +319,25 @@ export default function Lead() {
// anyway; showing them a row of buttons that all 403 reads as a broken app // anyway; showing them a row of buttons that all 403 reads as a broken app
// rather than as a control. // rather than as a control.
const actions = actionsFor(roles, stage?.uid) const actions = actionsFor(roles, stage?.uid)
// THE ONE THING THIS USER MUST DO, promoted to the header. "What do I do
// now" is the first question an operator asks of a lead, and it was answered
// only by scrolling past the metrics and the stage rail to the Next step
// panel. When this user has an action, it belongs top-right, where the eye
// goes for the next move; when they do not, the passive status stays there
// instead. `role: 'do'` is the committing step never a re-run or a
// recovery lever, which must not be dressed as the thing to do.
const acceptedAlready = Boolean(row.acceptance_ref)
const primaryAction = actions.find(
(a) => a.role === 'do' && !(acceptedAlready && a.uid === 'zk-act-accept'),
) || null
const goToAction = () => {
if (!primaryAction) return
setOpen(primaryAction.uid)
// Let the form mount, then bring the working surface into view.
requestAnimationFrame(() =>
document.getElementById('lead-next')?.scrollIntoView({ behavior: 'smooth', block: 'center' }))
}
const fieldCount = groups.reduce((n, [, shown]) => n + shown.length, 0) const fieldCount = groups.reduce((n, [, shown]) => n + shown.length, 0)
// Built from the SAME rows the full file renders, so the two can never // Built from the SAME rows the full file renders, so the two can never
@ -406,7 +425,18 @@ export default function Lead() {
{/* WHOSE MOVE IT IS. The stage name is in the rail below; this says {/* WHOSE MOVE IT IS. The stage name is in the rail below; this says
who holds the lead right now, which is the question the header is who holds the lead right now, which is the question the header is
really being asked. The customer tone reuses the teal cust palette. */} really being asked. The customer tone reuses the teal cust palette. */}
{holds ? ( {primaryAction ? (
/* YOUR MOVE. When this user owns the next step, the header carries it
as the prominent thing amber, because it is waiting on a person
and pressing it opens the form below and scrolls to it. */
<button type="button" className="cta" onClick={goToAction}>
<span className="cta__txt">
<small>Your move</small>
<b>{primaryAction.label}</b>
</span>
<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M3 8h9M8.5 4.5 12 8l-3.5 3.5" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round"/></svg>
</button>
) : holds ? (
<div className={'holds holds--' + (holds.pillTone === 'cust' ? 'cust' : holds.tone)}> <div className={'holds holds--' + (holds.pillTone === 'cust' ? 'cust' : holds.tone)}>
<span className="holds__puck" aria-hidden="true"> <span className="holds__puck" aria-hidden="true">
{holds.tone === 'ai' ? ( {holds.tone === 'ai' ? (
@ -622,7 +652,7 @@ export default function Lead() {
if (!step.length && !again.length && !extras.length) return null if (!step.length && !again.length && !extras.length) return null
return ( return (
<div className="panel"> <div className="panel" id="lead-next">
<div className="panel__head"> <div className="panel__head">
<div> <div>
<h2 className="panel__title">Next step</h2> <h2 className="panel__title">Next step</h2>

View File

@ -1798,3 +1798,35 @@
.holds--cust .holds__puck { background: var(--zk-teal); box-shadow: 0 0 0 4px var(--zk-teal-tint); } .holds--cust .holds__puck { background: var(--zk-teal); box-shadow: 0 0 0 4px var(--zk-teal-tint); }
.holds--cust .holds__txt small { color: var(--zk-teal-ink); } .holds--cust .holds__txt small { color: var(--zk-teal-ink); }
/* Your move: the immediate action, in the header
The one thing this user must do, promoted top-right where the eye looks for
the next move. Amber because it is waiting on a person; pressing it opens
the form and scrolls to it. When nothing is owed, the passive holds status
sits here instead. */
.cta {
flex: none;
display: inline-flex;
align-items: center;
gap: var(--sp-3);
padding: var(--sp-3) var(--sp-4) var(--sp-3) var(--sp-5);
border: 0;
border-radius: var(--r-md);
background: var(--zk-amber);
color: #fff;
cursor: pointer;
box-shadow: var(--sh-sm);
transition: background var(--t-fast), box-shadow var(--t-fast), transform var(--t-fast);
}
.cta:hover { background: var(--zk-amber-ink); box-shadow: var(--sh-md); }
.cta:focus-visible { outline: 2px solid var(--zk-amber-ink); outline-offset: 2px; }
.cta__txt { display: flex; flex-direction: column; align-items: flex-start; line-height: 1.2; }
.cta__txt small {
font-size: var(--fs-3xs);
font-weight: var(--fw-semi);
letter-spacing: .08em;
text-transform: uppercase;
opacity: .82;
}
.cta__txt b { font-size: var(--fs-sm); font-weight: var(--fw-semi); }
.cta svg { width: 16px; height: 16px; flex: none; }