console: state before numbers; frame optional actions; flatten the overview band

Applying the lens from the status-strip critique across the surfaces.

LEAD PAGE
- "What is happening now" lifts above the metrics. An operator's first question
  is what is going on / what do I do, not what the premium is — so the status
  strip (an alert when blocked or stalled, the live state otherwise) sits
  directly under the header, before the four numbers, full-width.
- The optional "Add a document" action stopped floating: with no required step
  it read like the next move and sat under "nothing is waiting on you". A quiet
  "Optional" tag now frames it.

OVERVIEW
- The "right now" band was four heavy dark-navy shadowed cards — the
  cards-on-canvas language removed everywhere else, clashing on the flat white
  page. Flattened to light tiles read by a coloured left rule (blue AI, amber
  person, teal customer, grey risk), the figure taking that colour; a live
  person-queue or lapsed renewal warms its whole tile. Prominence from the big
  number and the accent, not a dark fill.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Yashas 2026-09-09 12:01:46 +05:30
parent d1ed66b7ae
commit e21e3949f5
2 changed files with 127 additions and 92 deletions

View File

@ -300,6 +300,82 @@ export default function Lead() {
// are still looked up on the state's own uid; only the presentation moves.
const stage = phaseOf(STAGES.find((s) => s.name === stateName), row)
const holds = holdsOf(stage)
/* WHAT IS HAPPENING NOW, lifted above the numbers. An operator's first
question about a lead is "what is going on / what do I do", not "what is
the premium" — so the status strip (an alert when blocked or stalled, the
live state otherwise) sits directly under the header, before the metrics.
Rendered full-width there rather than inside the narrow work column. */
const statusStrip = (
blocked ? (
<div className="doing doing--blocked">
<span className="doing__stop" aria-hidden="true" />
<div>
<strong>Stopped — {blocked.what}</strong>
<span>
{blocked.fix} The agents will pick the lead up again on their own once it
is there; nothing else is running in the meantime.
</span>
<button
type="button" className="doing__fix"
onClick={() => setOpen(blocked.via)}
>
Open Collect Documents
</button>
</div>
</div>
) : stalled ? (
<div className="doing doing--stalled">
<span className="doing__stop" aria-hidden="true" />
<div>
<strong>{stage.doing} — nothing for {stalled.mins} minutes</strong>
<span>
{stage.by} has not come back. That is usually the model provider being
slow or dropping a request, not a problem with this lead — the work so far
is safe and nothing has been lost.
{stalled.nudge
? ` Running ${stalled.nudge.label.toLowerCase()} wakes ${stalled.nudge.by} to try again.`
: ' There is no step to re-run from here; the actions below are the way on.'}
</span>
{stalled.nudge ? (
<button
type="button" className="doing__fix"
onClick={() => setOpen(stalled.nudge.uid)}
>
{stalled.nudge.label}
</button>
) : null}
</div>
</div>
) : stage?.kind === 'customer' && stage.doing ? (
/* Not an agent working, and not a task of yours either — the lead
is with someone outside the business. Without this the screen
went blank at exactly the stage where an operator is most likely
to wonder whether something has broken. */
<div className="doing doing--waiting">
<span className="doing__wait" aria-hidden="true" />
<div>
<strong>{stage.doing}</strong>
<span>
Their answer comes back to this lead on its own — Engage reads it and
records the acceptance. Nothing is waiting on you.
</span>
</div>
</div>
) : stage?.kind === 'auto' ? (
<div className="doing">
<span className="doing__pulse" aria-hidden="true" />
<div>
<strong>{stage.doing}</strong>
<span>
Handled by {stage.by}.
{stage.after ? ` The lead stays in ${stateName} until a quote exists — nothing is waiting on you.` : ''}
{' '}Typically completes within two minutes; this view refreshes automatically.
</span>
</div>
</div>
) : null
)
// Only what THIS user may run. The workflow refuses the rest server-side
// anyway; showing them a row of buttons that all 403 reads as a broken app
// rather than as a control.
@ -441,6 +517,8 @@ export default function Lead() {
) : null}
</header>
{statusStrip}
{/* Measures, not fields. Age and dwell time are computed — neither is on
the record — and they are the two that answer "is this lead moving?",
which no label/value pair on the page could. */}
@ -497,78 +575,6 @@ export default function Lead() {
</div>
) : null}
{/* An AI-carried stage has no empty action bar and no idle screen: it
says what is happening and who is doing it. The activities below it
stay available — ops can run them by hand — but they are not the
answer to "why is nothing moving?". */}
{blocked ? (
<div className="doing doing--blocked">
<span className="doing__stop" aria-hidden="true" />
<div>
<strong>Stopped — {blocked.what}</strong>
<span>
{blocked.fix} The agents will pick the lead up again on their own once it
is there; nothing else is running in the meantime.
</span>
<button
type="button" className="doing__fix"
onClick={() => setOpen(blocked.via)}
>
Open Collect Documents
</button>
</div>
</div>
) : stalled ? (
<div className="doing doing--stalled">
<span className="doing__stop" aria-hidden="true" />
<div>
<strong>{stage.doing} — nothing for {stalled.mins} minutes</strong>
<span>
{stage.by} has not come back. That is usually the model provider being
slow or dropping a request, not a problem with this lead — the work so far
is safe and nothing has been lost.
{stalled.nudge
? ` Running ${stalled.nudge.label.toLowerCase()} wakes ${stalled.nudge.by} to try again.`
: ' There is no step to re-run from here; the actions below are the way on.'}
</span>
{stalled.nudge ? (
<button
type="button" className="doing__fix"
onClick={() => setOpen(stalled.nudge.uid)}
>
{stalled.nudge.label}
</button>
) : null}
</div>
</div>
) : stage?.kind === 'customer' && stage.doing ? (
/* Not an agent working, and not a task of yours either — the lead
is with someone outside the business. Without this the screen
went blank at exactly the stage where an operator is most likely
to wonder whether something has broken. */
<div className="doing doing--waiting">
<span className="doing__wait" aria-hidden="true" />
<div>
<strong>{stage.doing}</strong>
<span>
Their answer comes back to this lead on its own — Engage reads it and
records the acceptance. Nothing is waiting on you.
</span>
</div>
</div>
) : stage?.kind === 'auto' ? (
<div className="doing">
<span className="doing__pulse" aria-hidden="true" />
<div>
<strong>{stage.doing}</strong>
<span>
Handled by {stage.by}.
{stage.after ? ` The lead stays in ${stateName} until a quote exists — nothing is waiting on you.` : ''}
{' '}Typically completes within two minutes; this view refreshes automatically.
</span>
</div>
</div>
) : null}
{/* WHAT TO DO NEXT, not what is permitted.
Every action carries a role: `do` is the step this stage is
@ -661,6 +667,10 @@ export default function Lead() {
{again.length ? (
<div className="alt">
{/* With no required step, these optional actions floated at
the top of the column reading like the next step. A quiet
tag frames them as optional without repeating the status. */}
{!step.length ? <span className="alt__note">Optional</span> : null}
{again.map((a) => (
<button key={a.uid} type="button"
className={'alt__btn' + (open === a.uid ? ' is-open' : '')}

View File

@ -888,31 +888,32 @@
margin-bottom: var(--sp-5);
}
/* FLAT, TO MATCH THE REST. This was four heavy dark-navy cards with shadows —
the cards-on-canvas language removed everywhere else, so on the flat white
page it clashed. Now light tiles, each read by a coloured left rule: blue
for the AI, amber for a person, teal for the customer, red for risk. The
number takes that colour, so the row is read as colour before text. Prominence
comes from a big figure and the accent, not from a dark fill. */
.now__cell {
position: relative;
padding: var(--sp-6) var(--sp-6) var(--sp-5);
border-radius: var(--r-lg);
background: var(--zk-navy);
color: var(--zk-white);
padding: var(--sp-5) var(--sp-5) var(--sp-5) var(--sp-6);
border-radius: var(--r-md);
background: var(--zk-white);
color: var(--zk-ink);
border: 1px solid var(--zk-line-soft);
overflow: hidden;
box-shadow: var(--sh-md);
}
.now__cell::before {
content: '';
position: absolute;
inset: 0 0 auto 0;
height: 4px;
background: var(--zk-blue-light);
inset: 0 auto 0 0;
width: 4px;
background: var(--zk-blue);
}
.now__cell--person::before { background: var(--zk-amber-line); }
.now__cell--cust::before { background: var(--zk-cyan); }
.now__cell--risk::before { background: var(--zk-danger-line); }
/* A live person-queue or a lapsed renewal is the one thing on this page that
is actually owed. It warms up; the rest stay navy. */
.now__cell--person.is-live { background: var(--zk-amber-ink); }
.now__cell--risk.is-live { background: var(--zk-danger-ink); }
.now__cell--person::before { background: var(--zk-amber); }
.now__cell--cust::before { background: var(--zk-teal); }
.now__cell--risk::before { background: var(--zk-grey); }
.now__l {
display: block;
@ -921,24 +922,35 @@
font-weight: var(--fw-semi);
letter-spacing: .09em;
text-transform: uppercase;
opacity: .78;
color: var(--zk-grey);
}
.now__v {
display: block;
margin-bottom: var(--sp-2);
font-size: var(--fs-3xl);
font-size: var(--fs-2xl);
font-weight: var(--fw-semi);
line-height: 1;
letter-spacing: -.03em;
letter-spacing: -.02em;
font-variant-numeric: tabular-nums;
color: var(--zk-blue-dark);
}
.now__cell--person .now__v { color: var(--zk-amber-ink); }
.now__cell--cust .now__v { color: var(--zk-teal-ink); }
.now__cell--risk .now__v { color: var(--zk-ink); }
/* A live person-queue or a lapsed renewal is the one thing actually owed. It
warms its whole tile; the rest stay plain white. */
.now__cell--person.is-live { background: var(--zk-amber-tint); border-color: var(--zk-amber-line); }
.now__cell--risk.is-live { background: var(--zk-danger-tint); border-color: var(--zk-danger-line); }
.now__cell--risk.is-live::before { background: var(--zk-danger); }
.now__cell--risk.is-live .now__v { color: var(--zk-danger-ink); }
.now__s {
display: block;
font-size: var(--fs-2xs);
line-height: var(--lh-snug);
opacity: .82;
color: var(--zk-muted);
}
/* KPI strip. Whitespace rather than rules — see .lmetrics for why the cage
@ -1830,3 +1842,16 @@
}
.cta__txt b { font-size: var(--fs-sm); font-weight: var(--fw-semi); }
.cta svg { width: 16px; height: 16px; flex: none; }
/* Frames optional (again) actions when there is no required step, so a
secondary button stops reading as the next move. */
.alt__note {
display: inline-block;
align-self: center;
margin-right: var(--sp-2);
font-size: var(--fs-3xs);
font-weight: var(--fw-semi);
letter-spacing: .07em;
text-transform: uppercase;
color: var(--zk-grey);
}