Compare commits
No commits in common. "ee57184fa2391663e1237b4a6003d9d6eb611208" and "e21e3949f50594a39bc700c3b25ff7895dcb532f" have entirely different histories.
ee57184fa2
...
e21e3949f5
@ -18,7 +18,7 @@
|
||||
kept in the fallback stack, and Geist Mono carries refs and figures. -->
|
||||
<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=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=Geist+Mono:wght@400;500&display=swap" />
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&family=Geist+Mono:wght@400;500&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
|
||||
|
||||
@ -5,12 +5,11 @@
|
||||
align-items: center;
|
||||
gap: var(--sp-6);
|
||||
flex-wrap: wrap;
|
||||
margin: 0 0 var(--sp-5);
|
||||
padding: var(--sp-6) var(--sp-7);
|
||||
border: 1px solid var(--zk-line-soft);
|
||||
border-radius: var(--r-md);
|
||||
box-shadow: var(--sh-sm);
|
||||
background: var(--zk-white);
|
||||
margin: var(--sp-6) 0;
|
||||
padding: var(--sp-2) 0 var(--sp-5);
|
||||
border: 0;
|
||||
border-bottom: 1px solid var(--zk-line);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.rail ol {
|
||||
|
||||
101
src/index.css
101
src/index.css
@ -22,69 +22,64 @@
|
||||
}
|
||||
|
||||
:root {
|
||||
/* ── PALETTE — assured-inspired ──────────────────────────────
|
||||
A clean white ground, warm near-black ink, and ONE confident accent:
|
||||
a magenta-pink, reserved for action and active state. The token names
|
||||
keep their history (the accent still lives under the --zk-blue-* names
|
||||
the whole app references), but the values are the new scheme. Teal is
|
||||
the customer, amber a person; the pink is "act / here", nothing else. */
|
||||
--zk-blue: #e5227f; /* THE ACCENT (was brand blue) — pink */
|
||||
--zk-blue-dark: #c30f68;
|
||||
--zk-blue-deep: #c30f68;
|
||||
--zk-navy: #2a1d3d; /* a deep plum-ink for the logo / darkest text */
|
||||
--zk-blue-mid: #ee5fa0;
|
||||
--zk-blue-light: #f6cfe3; /* pink hairline / ring */
|
||||
--zk-cyan: #fdebf4; /* pink-soft */
|
||||
--zk-teal: #0e9e8f;
|
||||
--zk-teal-ink: #0a6b60;
|
||||
--zk-teal-tint: #e2f5f2;
|
||||
--zk-teal-line: #bfe6e1;
|
||||
--zk-good: #18a558;
|
||||
/* Brand palette lifted from zurichkotak.com */
|
||||
--zk-blue: #2167ae;
|
||||
--zk-blue-dark: #1a5490;
|
||||
--zk-blue-deep: #1a528b;
|
||||
--zk-navy: #23366f;
|
||||
--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: #1c1626; /* warm near-black */
|
||||
--zk-muted: #645d72;
|
||||
--zk-grey: #a49dae;
|
||||
--zk-line: #e3e1ea;
|
||||
--zk-line-soft: #ececf1;
|
||||
--zk-tint: #faf9fb; /* off-white section ground */
|
||||
--zk-sunk: #f5f3f8; /* a touch deeper — hover / inset */
|
||||
--zk-tint-blue: #fdebf4; /* pink-soft (was tint-blue) */
|
||||
--zk-ink: #2b2e31;
|
||||
--zk-muted: #5d6162;
|
||||
--zk-grey: #a6adaf;
|
||||
--zk-line: #dde4e3;
|
||||
--zk-line-soft: #eceeef;
|
||||
--zk-tint: #f4f9fc;
|
||||
--zk-tint-blue: #ebf3fb;
|
||||
--zk-white: #fff;
|
||||
--zk-danger: #d64545;
|
||||
--zk-danger: #d32f2f;
|
||||
|
||||
--zk-amber: #c77d1a;
|
||||
--zk-amber-ink: #9a5d10;
|
||||
--zk-amber-tint: #fbf0dd;
|
||||
--zk-amber-line: #ecd3a3;
|
||||
--zk-danger-ink: #b23636;
|
||||
--zk-danger-tint: #fbeaea;
|
||||
--zk-danger-line: #f0c9c9;
|
||||
/* Amber and red accents already used for people / lapsed renewals, named so
|
||||
they stop being magic hex literals scattered across four stylesheets. */
|
||||
--zk-amber: #b5761f;
|
||||
--zk-amber-ink: #93500f;
|
||||
--zk-amber-tint: #fdf1e7;
|
||||
--zk-amber-line: #f0c69a;
|
||||
--zk-danger-ink: #a3261f;
|
||||
--zk-danger-tint: #fdeceb;
|
||||
--zk-danger-line: #f0c2bd;
|
||||
|
||||
/* ── Shape, depth, motion ────────────────────────────────────
|
||||
Rounder than before and softer shadows — the assured cards read as
|
||||
objects that float a little, on a warm plum-ink low alpha rather than
|
||||
generic grey haze. */
|
||||
--r-xs: 8px;
|
||||
--r-sm: 10px;
|
||||
--r-md: 14px;
|
||||
--r-lg: 18px;
|
||||
--r-xl: 22px;
|
||||
One scale, used everywhere. The palette above is untouched; every shadow
|
||||
below is the brand navy at low alpha, so depth reads as part of the brand
|
||||
rather than as generic grey haze. */
|
||||
--r-xs: 6px;
|
||||
--r-sm: 8px;
|
||||
--r-md: 12px;
|
||||
--r-lg: 16px;
|
||||
--r-xl: 20px;
|
||||
--r-pill: 999px;
|
||||
|
||||
--sh-xs: 0 1px 2px rgba(28, 22, 38, 0.04);
|
||||
--sh-sm: 0 1px 2px rgba(28, 22, 38, 0.04), 0 4px 14px -8px rgba(28, 22, 38, 0.10);
|
||||
--sh-md: 0 1px 2px rgba(28, 22, 38, 0.04), 0 8px 24px -10px rgba(28, 22, 38, 0.12);
|
||||
--sh-lg: 0 2px 6px rgba(28, 22, 38, 0.06), 0 24px 48px -16px rgba(28, 22, 38, 0.22);
|
||||
--sh-accent: 0 6px 20px -6px rgba(229, 34, 127, 0.42);
|
||||
--sh-xs: 0 1px 2px rgba(35, 54, 111, 0.05);
|
||||
--sh-sm: 0 1px 2px rgba(35, 54, 111, 0.05), 0 2px 6px -2px rgba(35, 54, 111, 0.07);
|
||||
--sh-md: 0 1px 2px rgba(35, 54, 111, 0.05), 0 10px 24px -8px rgba(35, 54, 111, 0.14);
|
||||
--sh-lg: 0 2px 6px rgba(35, 54, 111, 0.06), 0 20px 42px -14px rgba(35, 54, 111, 0.22);
|
||||
|
||||
--ring: 0 0 0 3px rgba(229, 34, 127, 0.16);
|
||||
--ring: 0 0 0 3px rgba(33, 103, 174, 0.16);
|
||||
|
||||
--ease: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--t-fast: 0.14s var(--ease);
|
||||
--t: 0.2s var(--ease);
|
||||
|
||||
--grad-blue: linear-gradient(135deg, var(--zk-blue) 0%, var(--zk-blue-dark) 100%);
|
||||
--grad-blue-hover: linear-gradient(135deg, var(--zk-blue-dark) 0%, #a80c59 100%);
|
||||
--grad-blue: linear-gradient(135deg, var(--zk-blue) 0%, var(--zk-blue-deep) 100%);
|
||||
--grad-blue-hover: linear-gradient(135deg, var(--zk-blue-dark) 0%, var(--zk-navy) 100%);
|
||||
|
||||
/* ── TYPE SCALE ───────────────────────────────────────────────
|
||||
The app had FIFTY-FOUR distinct font sizes, and forty of them sat
|
||||
@ -119,8 +114,6 @@
|
||||
--fw-normal: 400;
|
||||
--fw-medium: 500;
|
||||
--fw-semi: 600;
|
||||
--fw-bold: 700;
|
||||
--fw-x: 800;
|
||||
|
||||
/* ── SPACING ──────────────────────────────────────────────────
|
||||
One rhythm. Panels breathe at 24-28px rather than 14-18px, which is
|
||||
@ -138,8 +131,8 @@
|
||||
/* Display and body are the SAME brand face — Zurich Sans has no bold,
|
||||
so headings and figures lean on weight 500 and tighter tracking, set per
|
||||
use, not on a second family. */
|
||||
--font-display: 'Plus Jakarta Sans', 'Geist', system-ui, sans-serif;
|
||||
--font-zurich: 'Plus Jakarta Sans', 'Geist', system-ui, -apple-system, sans-serif;
|
||||
--font-display: 'Geist', 'Zurich Sans', system-ui, sans-serif;
|
||||
--font-zurich: 'Geist', 'Zurich Sans', system-ui, -apple-system, sans-serif;
|
||||
--font-mono: 'Geist Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
|
||||
font-family: var(--font-zurich);
|
||||
|
||||
@ -301,6 +301,81 @@ export default function Lead() {
|
||||
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.
|
||||
@ -379,83 +454,6 @@ export default function Lead() {
|
||||
// the way out rather than going on promising two minutes.
|
||||
const stillFor = row.updated_at ? now - Date.parse(row.updated_at) : 0
|
||||
const stalled = !blocked && stage?.kind === 'auto' && stillFor > STALL_AFTER_MS
|
||||
|
||||
/* 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
|
||||
)
|
||||
|
||||
? { mins: Math.round(stillFor / 60000), nudge: nudgeFor(stage, row) }
|
||||
: null
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
.page__title {
|
||||
margin: 0;
|
||||
font-size: var(--fs-2xl);
|
||||
font-weight: var(--fw-x);
|
||||
font-weight: var(--fw-semi);
|
||||
letter-spacing: -0.022em;
|
||||
line-height: 1.2;
|
||||
color: var(--zk-ink);
|
||||
@ -309,14 +309,14 @@
|
||||
|
||||
/* ---- panel ---- */
|
||||
.panel {
|
||||
/* CARD. The assured look is clean white cards that float a little on the
|
||||
off-white ground — rounded, soft-shadowed, bounded. */
|
||||
background: var(--zk-white);
|
||||
border: 1px solid var(--zk-line-soft);
|
||||
border-radius: var(--r-lg);
|
||||
box-shadow: var(--sh-sm);
|
||||
padding: var(--sp-6) var(--sp-7);
|
||||
margin-bottom: var(--sp-5);
|
||||
/* FLAT. Was a shadowed white card; now a plain section on the white
|
||||
canvas, set off by its heading and by space, not by a box. */
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
padding: 0;
|
||||
margin-bottom: var(--sp-8);
|
||||
}
|
||||
|
||||
.panel__head {
|
||||
@ -330,7 +330,7 @@
|
||||
.panel__title {
|
||||
margin: 0;
|
||||
font-size: var(--fs-lg);
|
||||
font-weight: var(--fw-bold);
|
||||
font-weight: var(--fw-semi);
|
||||
letter-spacing: -0.018em;
|
||||
line-height: var(--lh-tight);
|
||||
}
|
||||
@ -404,14 +404,12 @@
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 14px;
|
||||
margin-bottom: var(--sp-5);
|
||||
padding: var(--sp-4) var(--sp-5);
|
||||
border-radius: var(--r-md);
|
||||
border: 1px solid var(--zk-teal-line);
|
||||
background: var(--zk-teal-tint);
|
||||
color: var(--zk-teal-ink);
|
||||
margin-bottom: 22px;
|
||||
padding: 16px 20px;
|
||||
border-radius: var(--r-lg);
|
||||
border: 1px solid var(--zk-blue-light);
|
||||
background: var(--zk-tint-blue);
|
||||
}
|
||||
.doing strong { color: var(--zk-teal-ink); }
|
||||
|
||||
.doing div {
|
||||
display: flex;
|
||||
@ -620,9 +618,9 @@
|
||||
|
||||
/* Rail panels are tighter than main panels — a 340px measure cannot afford
|
||||
32px of padding on each side. */
|
||||
.lead__side .panel { padding: var(--sp-5) var(--sp-6); margin-bottom: var(--sp-4); }
|
||||
.lead__side .panel { padding: 0; margin-bottom: var(--sp-6); }
|
||||
.lead__side .panel > .panel__head { padding: 0; margin-bottom: var(--sp-4); }
|
||||
.lead__side .panel--crew { border-bottom: 0; }
|
||||
.lead__side .panel--crew { padding: 0 0 var(--sp-6); border-bottom: 1px solid var(--zk-line); }
|
||||
/* rail headings sit at label size, not the big panel-title size — the rail is
|
||||
reference, and a 18px "Customer" heading would shout over the facts. */
|
||||
.lead__side .panel__title {
|
||||
@ -1109,26 +1107,16 @@
|
||||
22px against 12px labels. Premium and commission — the two numbers
|
||||
anyone actually came for — are bigger again. */
|
||||
.lmetrics {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: var(--sp-4);
|
||||
margin-bottom: var(--sp-5);
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--sp-6) var(--sp-8);
|
||||
padding: var(--sp-6) 0;
|
||||
border-top: 1px solid var(--zk-line);
|
||||
border-bottom: 1px solid var(--zk-line);
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.lm {
|
||||
min-width: 0;
|
||||
background: var(--zk-white);
|
||||
border: 1px solid var(--zk-line-soft);
|
||||
border-radius: var(--r-md);
|
||||
box-shadow: var(--sh-sm);
|
||||
padding: var(--sp-5);
|
||||
}
|
||||
/* Money leads: the two figures a renewal is about sit in the accent tint. */
|
||||
.lm--hero { background: var(--zk-tint-blue); border-color: var(--zk-blue-light); }
|
||||
.lm--hero .lm__v { color: var(--zk-blue-dark); }
|
||||
.lm--hero .lm__l { color: var(--zk-blue-dark); }
|
||||
|
||||
@media (max-width: 1040px) { .lmetrics { grid-template-columns: 1fr 1fr; } }
|
||||
.lm { min-width: 0; }
|
||||
|
||||
.lm__l {
|
||||
display: block;
|
||||
@ -1144,7 +1132,7 @@
|
||||
display: block;
|
||||
margin-bottom: var(--sp-1);
|
||||
font-size: var(--fs-xl);
|
||||
font-weight: var(--fw-x);
|
||||
font-weight: var(--fw-semi);
|
||||
line-height: var(--lh-tight);
|
||||
color: var(--zk-navy);
|
||||
font-variant-numeric: tabular-nums;
|
||||
@ -1833,17 +1821,17 @@
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-3);
|
||||
padding: var(--sp-3) var(--sp-5) var(--sp-3) var(--sp-5);
|
||||
padding: var(--sp-3) var(--sp-4) var(--sp-3) var(--sp-5);
|
||||
border: 0;
|
||||
border-radius: var(--r-md);
|
||||
background: var(--zk-blue);
|
||||
background: var(--zk-amber);
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
box-shadow: var(--sh-accent);
|
||||
box-shadow: var(--sh-sm);
|
||||
transition: background var(--t-fast), box-shadow var(--t-fast), transform var(--t-fast);
|
||||
}
|
||||
.cta:hover { background: var(--zk-blue-dark); transform: translateY(-1px); }
|
||||
.cta:focus-visible { outline: 2px solid var(--zk-blue-dark); outline-offset: 2px; }
|
||||
.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);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user