zurich_kotak/src/layout/UserMenu.jsx
Yashas 79863e2a5f console: the Lead Desk redesign — one colour rule for who holds the work
Implements the design in zurich-kotak-leaddesk across every screen.

Foundation: Source Sans 3 bundled from src/assets/fonts (no Google Fonts
request), 15px root, navy/blue/amber/teal/red palette where blue is the AI,
amber a person, teal the customer and red is reserved for risk; flat white
10px cards on a grey canvas, no shadows.

Shell: 56px navy masthead (Z wordmark, Lead Desk, user), 232px white nav
grouped Needs you / Scheduled / History with coloured count pills and the
closed stages folded.

Overview: the hero says who holds each open lead (stacked bar + legend)
beside the renewals-at-risk card; five KPIs incl. Closed to date; Waiting
on a person queue with owner dots and "N more with the AI"; Pipeline by
stage with owner-coloured bars; Renewal exposure with tabs, dropdown
filters and a Holding it column.

Lead: back button, id line, holder pill; one attention banner (amber your
turn / blocked, red stalled, blue with the AI, teal with the customer) that
carries its button; four facts with a confidence bar; navy steps rail
whose current step takes its holder's colour; timeline as an avatar
gutter with title, stage pill, by-line + AI tag and text-link actions
(See reasoning · N steps, Hear the call, Open thread); side column flows.

Lists: card with search + hide-closed toggle, columns Customer / Stage /
Renewal due / Holding it / Premium / Last activity, stalled rows get a red
inset, closed rows dim, person-gated queues split into Your turn / With the
AI group rows.

src/api/holder.js is the single rule for holder tone + label so the table
cells, the overview and the lead header cannot disagree.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 23:56:29 +05:30

92 lines
3.2 KiB
JavaScript

import { useEffect, useRef, useState } from 'react'
import { useZino } from '../api/provider.jsx'
import './UserMenu.css'
/** First letters of the first two words — a name is all we have for an avatar. */
function initials(user) {
const from = user?.name || user?.email || ''
const parts = from.replace(/@.*$/, '').split(/[\s._-]+/).filter(Boolean)
if (!parts.length) return '?'
return (parts[0][0] + (parts[1]?.[0] || '')).toUpperCase()
}
/**
* The signed-in identity, with sign-out folded inside it.
*
* The role stays visible on the trigger rather than only inside the menu: RBAC is
* the demo, so who you are signed in as should never take a click to find out.
*/
export default function UserMenu() {
const { user, signOut } = useZino()
const [open, setOpen] = useState(false)
const wrapRef = useRef(null)
const triggerRef = useRef(null)
// Close on an outside click or Escape. Bound only while open, so the shell
// costs nothing at rest.
useEffect(() => {
if (!open) return
function onDown(e) {
if (!wrapRef.current?.contains(e.target)) setOpen(false)
}
function onKey(e) {
if (e.key !== 'Escape') return
setOpen(false)
triggerRef.current?.focus()
}
document.addEventListener('mousedown', onDown)
document.addEventListener('keydown', onKey)
return () => {
document.removeEventListener('mousedown', onDown)
document.removeEventListener('keydown', onKey)
}
}, [open])
const name = user?.name || user?.email || 'Signed in'
// Role slugs read as titles: partner_agent -> Partner agent.
const roles = (user?.roles || [])
// `sme_underwriter` → "SME underwriter": the acronyms the roles here use stay upper.
.map((r) => String(r).replace(/_/g, ' ').replace(/^./, (c) => c.toUpperCase())
.replace(/\b(sme|uw|rm|posp|csr|ops|kyc)\b/gi, (m) => m.toUpperCase()))
.join(', ')
return (
<div className="um" ref={wrapRef}>
<button
ref={triggerRef}
type="button"
className={'um__trigger' + (open ? ' is-open' : '')}
onClick={() => setOpen((o) => !o)}
aria-haspopup="menu"
aria-expanded={open}
>
<span className="um__avatar" aria-hidden="true">{initials(user)}</span>
<span className="um__id">
<strong>{name}</strong>
<span>{roles || user?.email}</span>
</span>
<svg className="um__caret" width="12" height="12" viewBox="0 0 12 12" aria-hidden="true">
<path d="M2.5 4.5 6 8l3.5-3.5" fill="none" stroke="currentColor" strokeWidth="1.4"
strokeLinecap="round" strokeLinejoin="round" />
</svg>
</button>
{open ? (
<div className="um__menu" role="menu">
<div className="um__head">
<span className="um__avatar um__avatar--lg" aria-hidden="true">{initials(user)}</span>
<div className="um__headid">
<strong>{name}</strong>
{user?.email ? <span>{user.email}</span> : null}
{roles ? <span className="um__roles">{roles}</span> : null}
</div>
</div>
<button type="button" role="menuitem" className="um__item" onClick={signOut}>
Sign out
</button>
</div>
) : null}
</div>
)
}