console: the surface pass — display face, depth, and whose move it is
The layout was right but the surface still read as a flat corporate form. Three levers, all from the approved mockup: - A DISPLAY FACE (Bricolage Grotesque) on the things the eye lands on — the lead name, panel headings, and every figure. Zurich Sans stays the body voice; this gives the numbers and titles the tighter, more current character the plain grotesque could not. Loaded from Google Fonts with Zurich Sans as the fallback, so a blocked CDN degrades to the brand face rather than breaking. - DEPTH. Panels and the metric tiles lift from the barely-there --sh-sm to a real --sh-md with rounder corners, so cards read as objects on a surface rather than boxes drawn on paper. - WHOSE MOVE IT IS. The header said "STAGE / Quote Presented" — a label. It now carries a coloured pill: "Right now · With the customer" (teal), "With the AI" (blue), "Waiting on a person" (amber). The stage name is in the rail below; the pill answers the question an operator actually opens a lead to ask. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
900f34d11e
commit
83b5795b22
@ -13,6 +13,13 @@
|
||||
<!--BASE_HREF-->
|
||||
<link rel="icon" type="image/webp" href="./brand/zurich_logo.webp" />
|
||||
<title>Zurich Kotak | Lead Desk</title>
|
||||
<!-- Display face for headings and figures. Zurich Sans (the brand face,
|
||||
loaded from ./brand) stays the body voice; this gives the numbers and
|
||||
titles the tighter, more current character the flat corporate grotesque
|
||||
could not. Falls back to Zurich Sans if the CDN is blocked. -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,500;12..96,600;12..96,700&display=swap" />
|
||||
<!-- Written next to the artifact at placement; carries this environment's
|
||||
VITE_ZINO_API_URL, which src/runtimeConfig.js reads off
|
||||
window.__RUNTIME_CONFIG__. Without it requireConfigValue throws and the
|
||||
|
||||
@ -30,6 +30,11 @@
|
||||
--zk-blue-mid: #5495cf;
|
||||
--zk-blue-light: #91bfe3;
|
||||
--zk-cyan: #c7ebf9;
|
||||
--zk-teal: #0e7c74;
|
||||
--zk-teal-ink: #0b5f59;
|
||||
--zk-teal-tint: #e3f4f2;
|
||||
--zk-teal-line: #a5d8d3;
|
||||
--zk-good: #1f9d63;
|
||||
|
||||
--zk-ink: #2b2e31;
|
||||
--zk-muted: #5d6162;
|
||||
@ -119,6 +124,7 @@
|
||||
--sp-8: 40px;
|
||||
--sp-9: 56px;
|
||||
|
||||
--font-display: 'Bricolage Grotesque', 'Zurich Sans', 'Trebuchet MS', sans-serif;
|
||||
--font-zurich: 'Zurich Sans', Arial, Helvetica, sans-serif;
|
||||
--font-mono: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, monospace;
|
||||
|
||||
|
||||
@ -169,6 +169,25 @@ function fmt(k, v) {
|
||||
return String(v)
|
||||
}
|
||||
|
||||
/**
|
||||
* Whose move it is, right now — the question an operator opens a lead to
|
||||
* answer, said as a coloured pill in the header. Derived from the stage's own
|
||||
* kind, so it stays in step with the workflow: an AI-run stage is with the AI,
|
||||
* a customer-gated one is with the customer, a person-gated one is waiting on
|
||||
* someone, and a closed lead holds nobody.
|
||||
*/
|
||||
function holdsOf(stage) {
|
||||
if (!stage) return null
|
||||
switch (stage.kind) {
|
||||
case 'auto': return { tone: 'ai', label: 'With the AI' }
|
||||
case 'customer': return { tone: 'grey', pillTone: 'cust', label: 'With the customer' }
|
||||
case 'needs': return { tone: 'person', label: stage.approval ? 'Awaiting your approval' : 'Waiting on a person' }
|
||||
case 'waiting': return { tone: 'grey', label: 'In nurture' }
|
||||
case 'end': return { tone: 'grey', label: 'Closed' }
|
||||
default: return null
|
||||
}
|
||||
}
|
||||
|
||||
export default function Lead() {
|
||||
const { instanceId } = useParams()
|
||||
const { client, user } = useZino()
|
||||
@ -295,6 +314,7 @@ export default function Lead() {
|
||||
// The state, then what is really happening inside it — see phaseOf. Actions
|
||||
// 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)
|
||||
// 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.
|
||||
@ -342,7 +362,6 @@ export default function Lead() {
|
||||
}, 0)
|
||||
|
||||
const isClosed = stage?.kind === 'end'
|
||||
const heldByOthers = !isClosed && actions.length === 0
|
||||
// An automated stage that has stopped for a stated reason. Checked before
|
||||
// the "in progress" strip below, which would otherwise keep promising that
|
||||
// something is happening for as long as the lead is left alone.
|
||||
@ -384,20 +403,28 @@ export default function Lead() {
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="lead__stage">
|
||||
<span className="lead__stagelabel">Stage</span>
|
||||
<strong className={stage?.kind === 'end' ? 'is-closed' : undefined}>{stateName || '—'}</strong>
|
||||
{/* An end state runs no activity. That is worth one line next to the
|
||||
stage, not a panel of its own where the actions would be. */}
|
||||
{isClosed ? <span className="lead__closed">Closed — no further activity</span> : null}
|
||||
{heldByOthers ? (
|
||||
<span className="lead__closed">
|
||||
{stage?.kind === 'auto'
|
||||
? `In progress with ${stage.by}`
|
||||
: `Assigned to ${stage?.by ?? 'another role'}`}
|
||||
{/* 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
|
||||
really being asked. The customer tone reuses the teal cust palette. */}
|
||||
{holds ? (
|
||||
<div className={'holds holds--' + (holds.pillTone === 'cust' ? 'cust' : holds.tone)}>
|
||||
<span className="holds__puck" aria-hidden="true">
|
||||
{holds.tone === 'ai' ? (
|
||||
<svg width="20" height="20" viewBox="0 0 20 20"><path d="M6 3a2 2 0 0 0-2 2 2 2 0 0 0-1 3.6A2 2 0 0 0 4 12a2 2 0 0 0 2 2M14 3a2 2 0 0 1 2 2 2 2 0 0 1 1 3.6A2 2 0 0 1 16 12a2 2 0 0 1-2 2M10 3.5v12" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round"/></svg>
|
||||
) : holds.pillTone === 'cust' ? (
|
||||
<svg width="20" height="20" viewBox="0 0 20 20"><path d="M4 5h12v8h-6l-3 2.5V13H4z" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinejoin="round"/></svg>
|
||||
) : holds.tone === 'person' ? (
|
||||
<svg width="20" height="20" viewBox="0 0 20 20"><circle cx="10" cy="6.5" r="2.8" fill="none" stroke="currentColor" strokeWidth="1.5"/><path d="M4.5 15.5a5.5 5.5 0 0 1 11 0" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/></svg>
|
||||
) : (
|
||||
<svg width="20" height="20" viewBox="0 0 20 20"><path d="M10 5.5v4.5l3 2" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/><circle cx="10" cy="10" r="6.5" fill="none" stroke="currentColor" strokeWidth="1.5"/></svg>
|
||||
)}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<span className="holds__txt">
|
||||
<small>Right now</small>
|
||||
<b>{holds.label}</b>
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
</header>
|
||||
|
||||
{/* Measures, not fields. Age and dwell time are computed — neither is on
|
||||
|
||||
@ -311,8 +311,8 @@
|
||||
.panel {
|
||||
background: var(--zk-white);
|
||||
border: 1px solid var(--zk-line-soft);
|
||||
border-radius: var(--r-lg);
|
||||
box-shadow: var(--sh-sm);
|
||||
border-radius: var(--r-xl);
|
||||
box-shadow: var(--sh-md);
|
||||
padding: var(--sp-7);
|
||||
margin-bottom: var(--sp-5);
|
||||
}
|
||||
@ -1091,8 +1091,8 @@
|
||||
padding: var(--sp-6) var(--sp-7);
|
||||
background: var(--zk-white);
|
||||
border: 1px solid var(--zk-line-soft);
|
||||
border-radius: var(--r-lg);
|
||||
box-shadow: var(--sh-sm);
|
||||
border-radius: var(--r-xl);
|
||||
box-shadow: var(--sh-md);
|
||||
margin-bottom: var(--sp-5);
|
||||
}
|
||||
|
||||
@ -1719,3 +1719,74 @@
|
||||
color: var(--zk-grey);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
|
||||
/* ── modern surface pass ──────────────────────────────────────────────────
|
||||
The layout was right but the surface still read as a flat corporate form.
|
||||
Two levers close most of the gap: a display face on the things the eye
|
||||
lands on — the page title, panel headings and every figure — and one warm,
|
||||
coloured signal for who holds the lead right now. */
|
||||
.page__title,
|
||||
.panel__title,
|
||||
.lead__stage strong,
|
||||
.lm__v,
|
||||
.kpi__v,
|
||||
.now__v,
|
||||
.fig .v {
|
||||
font-family: var(--font-display);
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.page__title { letter-spacing: -0.03em; }
|
||||
|
||||
/* WHO HOLDS THE LEAD, as a coloured pill — the first thing the eye should
|
||||
catch. The stage name lives in the rail below; this says whose move it is,
|
||||
which is the question an operator actually opens a lead to answer. */
|
||||
.holds {
|
||||
flex: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-3);
|
||||
padding: var(--sp-2) var(--sp-5) var(--sp-2) var(--sp-2);
|
||||
border-radius: var(--r-pill);
|
||||
border: 1px solid var(--zk-teal-line);
|
||||
background: var(--zk-teal-tint);
|
||||
}
|
||||
.holds__puck {
|
||||
flex: none;
|
||||
width: 40px; height: 40px;
|
||||
border-radius: 50%;
|
||||
display: grid; place-items: center;
|
||||
background: var(--zk-teal); color: #fff;
|
||||
}
|
||||
.holds__txt { display: flex; flex-direction: column; line-height: 1.15; }
|
||||
.holds__txt small {
|
||||
font-size: var(--fs-3xs);
|
||||
font-weight: var(--fw-semi);
|
||||
letter-spacing: .09em;
|
||||
text-transform: uppercase;
|
||||
color: var(--zk-teal-ink);
|
||||
opacity: .9;
|
||||
}
|
||||
.holds__txt b {
|
||||
font-family: var(--font-display);
|
||||
font-weight: var(--fw-semi);
|
||||
font-size: var(--fs-md);
|
||||
color: var(--zk-ink);
|
||||
}
|
||||
|
||||
.holds--ai { background: var(--zk-tint-blue); border-color: var(--zk-blue-light); }
|
||||
.holds--ai .holds__puck { background: var(--zk-blue); }
|
||||
.holds--ai .holds__txt small { color: var(--zk-blue-dark); }
|
||||
|
||||
.holds--person { background: var(--zk-amber-tint); border-color: var(--zk-amber-line); }
|
||||
.holds--person .holds__puck { background: var(--zk-amber); }
|
||||
.holds--person .holds__txt small { color: var(--zk-amber-ink); }
|
||||
|
||||
.holds--grey { background: var(--zk-tint); border-color: var(--zk-line); }
|
||||
.holds--grey .holds__puck { background: var(--zk-grey); }
|
||||
.holds--grey .holds__txt small { color: var(--zk-muted); }
|
||||
|
||||
.holds--cust { background: var(--zk-teal-tint); border-color: var(--zk-teal-line); }
|
||||
.holds--cust .holds__puck { background: var(--zk-teal); }
|
||||
.holds--cust .holds__txt small { color: var(--zk-teal-ink); }
|
||||
|
||||
Loading…
Reference in New Issue
Block a user