Files
vibn-agent-runner/design-templates/VIBN (2)/vibn-crm/crm-pages.jsx

689 lines
42 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ============================================================
// crm-pages.jsx — Cadence CRM · in-app screens
// ------------------------------------------------------------
// Every page renders INSIDE SidebarShell (far-left sidebar).
// Built on vibn-ai-templates components. Light/minimal theme.
//
// Pages: CRMHome, CRMPeople, CRMRecord, CRMPipeline,
// CRMInbox, CRMReports, CRMSettings.
// ============================================================
const CRM_USER = { name: "Mira Reyes", email: "mira@northwind.io", color: "#d4b8a8" };
// Sidebar config — pass the active id, get the sections array.
const crmSections = (active) => [
{ items: [
{ id: "home", label: "Home", icon: "home", active: active === "home" },
{ id: "inbox", label: "Inbox", icon: "inbox", count: 8, active: active === "inbox" },
{ id: "tasks", label: "My tasks", icon: "check", count: 3, active: active === "tasks" },
]},
{ title: "Records", items: [
{ id: "companies", label: "Companies", icon: "building", active: active === "companies" },
{ id: "people", label: "People", icon: "people", active: active === "people" },
{ id: "deals", label: "Deals", icon: "target", count: 12, active: active === "deals" },
]},
{ title: "Workspace", items: [
{ id: "reports", label: "Reports", icon: "bar", active: active === "reports" },
{ id: "automations", label: "Automations", icon: "workflow", active: active === "automations" },
{ id: "settings", label: "Settings", icon: "settings", active: active === "settings" },
]},
];
// Wrap a page body in the shell with the right nav item active
const CRMShell = ({ active, children }) => (
<SidebarShell brand={{ name: "Northwind", mark: <CadenceMark size={22}/> }}
sections={crmSections(active)} user={CRM_USER} search="Search or jump to…">
{children}
</SidebarShell>
);
// Reusable page header bar (title + actions row)
const PageBar = ({ title, sub, breadcrumb, actions }) => (
<div style={{
padding: "16px 28px", borderBottom: "1px solid var(--divider)",
background: "var(--surface)", display: "flex",
justifyContent: "space-between", alignItems: "center", gap: 16,
}}>
<div style={{ minWidth: 0 }}>
{breadcrumb && <div style={{ fontSize: 12, color: "var(--text-3)", marginBottom: 3 }}>{breadcrumb}</div>}
<h1 style={{ margin: 0, fontSize: 20, fontWeight: 600, letterSpacing: "-0.01em" }}>{title}</h1>
{sub && <div style={{ fontSize: 13, color: "var(--text-2)", marginTop: 2 }}>{sub}</div>}
</div>
{actions && <div style={{ display: "flex", gap: 8, flexShrink: 0 }}>{actions}</div>}
</div>
);
const Scroll = ({ children, pad = 28 }) => (
<div style={{ flex: 1, overflowY: "auto", padding: pad }}>{children}</div>
);
// ── small stat tile ──────────────────────────────────────────
const Stat = ({ label, value, delta, up, spark }) => (
<Card padding={18}>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline" }}>
<span style={{ fontSize: 12, color: "var(--text-2)" }}>{label}</span>
{delta && <span style={{ fontSize: 11, fontWeight: 600, color: up ? "var(--success)" : "var(--danger)" }}>
{up ? "↑" : "↓"} {delta}</span>}
</div>
<div style={{ fontSize: 28, fontWeight: 600, letterSpacing: "-0.02em", marginTop: 8 }}>{value}</div>
{spark && (
<svg viewBox="0 0 100 28" preserveAspectRatio="none" style={{ width: "100%", height: 24, marginTop: 6, display: "block" }}>
<polyline points={spark} fill="none" stroke={up ? "var(--success)" : "var(--danger)"} strokeWidth="1.5" vectorEffect="non-scaling-stroke"/>
</svg>
)}
</Card>
);
const sparkUp = "0,24 12,20 24,22 36,15 48,17 60,10 72,12 84,6 100,2";
const sparkDn = "0,6 14,8 28,7 42,12 56,11 70,16 84,15 100,20";
// ============================================================
// 1 · HOME
// ============================================================
const CRMHome = () => (
<CRMShell active="home">
<PageBar title="Good morning, Mira"
sub="3 deals need a nudge today · 8 unread in Inbox · 1 task overdue"
actions={<>
<Button variant="secondary" leadingIcon={<Icon name="bell" size={13}/>}>Notifications</Button>
<Button leadingIcon={<Icon name="plus" size={13}/>}>New deal</Button>
</>}/>
<Scroll>
{/* KPI row */}
<div style={{ display: "grid", gridTemplateColumns: "repeat(4, 1fr)", gap: 14, marginBottom: 20 }}>
<Stat label="Open pipeline" value="$1.24M" delta="12%" up spark={sparkUp}/>
<Stat label="Won · this month" value="$284K" delta="8%" up spark={sparkUp}/>
<Stat label="Win rate · 90d" value="31%" delta="2pt" up spark={sparkUp}/>
<Stat label="Avg. response" value="2.4h" delta="0.6h" up={false} spark={sparkDn}/>
</div>
<div style={{ display: "grid", gridTemplateColumns: "1.5fr 1fr", gap: 20 }}>
{/* Pipeline by stage */}
<Card padding={0}>
<CardHeader title="Pipeline by stage" subtitle="Q2 · 12 active deals"
style={{ padding: "16px 20px", margin: 0, borderBottom: "1px solid var(--divider)" }}
action={<Button variant="ghost" size="sm">View board </Button>}/>
<div style={{ padding: "12px 20px 18px" }}>
{[
["Lead", 5, "$420K", 100],
["Qualified", 3, "$365K", 78],
["Proposal", 2, "$280K", 60],
["Negotiation", 1, "$148K", 32],
["Won", 1, "$96K", 22],
].map(([name, n, val, w]) => (
<div key={name} style={{ padding: "8px 0" }}>
<div style={{ display: "flex", justifyContent: "space-between", marginBottom: 6, fontSize: 13 }}>
<span>{name}</span>
<span style={{ color: "var(--text-2)" }}>{n} deals · <b style={{ color: "var(--text)" }}>{val}</b></span>
</div>
<div style={{ height: 8, borderRadius: 4, background: "var(--surface-alt)", overflow: "hidden" }}>
<div style={{ width: `${w}%`, height: "100%", background: "linear-gradient(90deg, var(--accent), color-mix(in srgb, var(--accent) 55%, transparent))", borderRadius: 4 }}/>
</div>
</div>
))}
</div>
</Card>
{/* Today's tasks */}
<Card padding={0}>
<CardHeader title="Today" subtitle="3 tasks"
style={{ padding: "16px 20px", margin: 0, borderBottom: "1px solid var(--divider)" }}
action={<Button variant="ghost" size="sm">All tasks</Button>}/>
<div style={{ padding: "6px 12px 12px" }}>
{[
["Follow up with Acme Robotics", "Overdue · 2d", true, "danger"],
["Send proposal to Halcyon", "Due 2:00pm", false, "warn"],
["Call Sun at Northstar", "Due 4:30pm", false, "neutral"],
["Prep QBR deck for Kestrel", "Tomorrow", false, "neutral"],
].map(([t, when, overdue, tone], i) => (
<div key={i} style={{ display: "flex", alignItems: "center", gap: 10, padding: "9px 8px", borderBottom: i < 3 ? "1px solid var(--divider)" : "none" }}>
<Checkbox checked={false}/>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: 13, fontWeight: 500 }}>{t}</div>
<div style={{ fontSize: 11, color: overdue ? "var(--danger)" : "var(--text-3)", marginTop: 1 }}>{when}</div>
</div>
</div>
))}
</div>
</Card>
</div>
{/* Recent activity */}
<Card padding={0} style={{ marginTop: 20 }}>
<CardHeader title="Recent activity"
style={{ padding: "16px 20px", margin: 0, borderBottom: "1px solid var(--divider)" }}
action={<Tabs variant="pill" items={[{ label: "All" }, { label: "Deals" }, { label: "Emails" }]} active="All"/>}/>
<div style={{ padding: "8px 20px 16px" }}>
{[
["MR", "#d4b8a8", "Mira Reyes", "moved", "Acme — Renewal '26 to Negotiation", "12m"],
["TR", "#c8e8a8", "Theo Roux", "logged a call with", "Sun Kim · Northstar", "1h"],
["DP", "#a8c8e8", "Devi Patel", "won", "Halcyon · Pro renewal · $24K", "3h"],
["SK", "#e8a87c", "Sun Ortiz", "added 4 contacts to", "Kestrel", "Yesterday"],
].map(([i, c, who, verb, obj, t], idx) => (
<div key={idx} style={{ display: "flex", alignItems: "center", gap: 12, padding: "10px 0", borderTop: idx ? "1px solid var(--divider)" : "none" }}>
<Avatar name={who} color={c} size={28}/>
<div style={{ flex: 1, fontSize: 13 }}>
<b style={{ fontWeight: 600 }}>{who}</b>
<span style={{ color: "var(--text-2)" }}> {verb} </span>
<b style={{ fontWeight: 500 }}>{obj}</b>
</div>
<span style={{ fontSize: 11, color: "var(--text-3)" }}>{t}</span>
</div>
))}
</div>
</Card>
</Scroll>
</CRMShell>
);
// ============================================================
// 2 · PEOPLE (contacts table)
// ============================================================
const CRMPeople = () => {
const people = [
{ id: 1, name: "Iris Tanaka", color: "#e8a87c", title: "Head of Engineering", company: "Acme Robotics", stage: "Customer", owner: "MR", last: "2h" },
{ id: 2, name: "Daniel Owusu", color: "#a8c8e8", title: "VP Product", company: "Acme Robotics", stage: "Customer", owner: "MR", last: "Yesterday" },
{ id: 3, name: "Sun Kim", color: "#c8e8a8", title: "VP Operations", company: "Northstar", stage: "Lead", owner: "TR", last: "3d" },
{ id: 4, name: "Priya Nair", color: "#c8a8e8", title: "COO", company: "Halcyon", stage: "Prospect", owner: "DP", last: "1w" },
{ id: 5, name: "Marco Lindqvist", color: "#e8c8a8", title: "Procurement", company: "Kestrel", stage: "Customer", owner: "MR", last: "4d" },
{ id: 6, name: "Naila Choudhury", color: "#a8e8c8", title: "CFO", company: "Mossbank", stage: "Lead", owner: "TR", last: "2w" },
{ id: 7, name: "Henri Lamarck", color: "#e8a8c8", title: "Founder", company: "Verra", stage: "Prospect", owner: "DP", last: "5d" },
{ id: 8, name: "Emi Hara", color: "#d4b8a8", title: "Head of Sales", company: "Tide Co.", stage: "Customer", owner: "MR", last: "1d" },
];
const stageTone = { Customer: "success", Lead: "accent", Prospect: "warn" };
return (
<CRMShell active="people">
<PageBar title="People" sub="2,418 contacts"
actions={<>
<Button variant="secondary" leadingIcon={<Icon name="doc" size={13}/>}>Import</Button>
<Button leadingIcon={<Icon name="plus" size={13}/>}>New contact</Button>
</>}/>
{/* Filter row */}
<div style={{ padding: "12px 28px", borderBottom: "1px solid var(--divider)", display: "flex", alignItems: "center", gap: 10, background: "var(--surface)" }}>
<Input placeholder="Search people" leadingIcon={<Icon name="search" size={13}/>} style={{ width: 260, padding: "6px 10px" }}/>
{["Stage", "Owner", "Company", "Last activity"].map(f => (
<div key={f} style={{
display: "flex", alignItems: "center", gap: 6, padding: "6px 10px",
border: "1px dashed var(--border)", borderRadius: "var(--radius-sm)",
fontSize: 12, color: "var(--text-2)", cursor: "pointer",
}}>{f}<Icon name="chevDown" size={11}/></div>
))}
<div style={{ flex: 1 }}/>
<FieldGroup options={["Table", "Board"]} value="Table"/>
</div>
<Scroll pad={20}>
<Table
selectable selected={[1, 5]}
columns={[
{ key: "name", label: "Name", render: r => (
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
<Avatar name={r.name} color={r.color} size={28}/>
<div>
<div style={{ fontWeight: 500 }}>{r.name}</div>
<div style={{ fontSize: 11, color: "var(--text-3)" }}>{r.title}</div>
</div>
</div>
)},
{ key: "company", label: "Company", render: r => (
<span style={{ display: "inline-flex", alignItems: "center", gap: 8 }}>
<span style={{ width: 20, height: 20, borderRadius: 5, background: "var(--surface-alt)", display: "inline-flex", alignItems: "center", justifyContent: "center", fontSize: 10, fontWeight: 700, color: "var(--text-2)" }}>{r.company[0]}</span>
{r.company}
</span>
)},
{ key: "stage", label: "Stage", render: r => <Badge tone={stageTone[r.stage]} dot>{r.stage}</Badge> },
{ key: "owner", label: "Owner", render: r => <Avatar name={r.owner} size={24}/> },
{ key: "last", label: "Last activity" },
{ key: "act", label: "", align: "right", width: 40, render: () => <IconButton name="more" size="sm" label="More"/> },
]}
rows={people}
/>
</Scroll>
</CRMShell>
);
};
// ============================================================
// 3 · COMPANY RECORD (detail)
// ============================================================
const CRMRecord = () => (
<CRMShell active="companies">
<PageBar breadcrumb="Companies Acme Robotics" title="Acme Robotics"
actions={<>
<Button variant="secondary" leadingIcon={<Icon name="star" size={13}/>}>Follow</Button>
<Button variant="secondary">Share</Button>
<Button leadingIcon={<Icon name="plus" size={13}/>}>Log activity</Button>
</>}/>
<div style={{ display: "grid", gridTemplateColumns: "320px 1fr", flex: 1, overflow: "hidden" }}>
{/* Details rail */}
<div style={{ borderRight: "1px solid var(--divider)", overflowY: "auto", padding: "20px 22px" }}>
<div style={{ display: "flex", alignItems: "center", gap: 14, marginBottom: 18 }}>
<div style={{ width: 52, height: 52, borderRadius: 12, background: "linear-gradient(135deg, #ff8a3a, #f43f5e)", color: "#fff", display: "flex", alignItems: "center", justifyContent: "center", fontSize: 22, fontWeight: 700 }}>A</div>
<div>
<div style={{ display: "flex", gap: 6 }}>
<Badge tone="success" dot>Customer</Badge>
<Badge tone="accent">Tier 1</Badge>
</div>
<div style={{ fontSize: 12, color: "var(--text-3)", marginTop: 6 }}>acme-robotics.io</div>
</div>
</div>
{[
["Industry", "Industrial automation"],
["Employees", "210"],
["Owner", "Mira Reyes"],
["Renewal", "Sept 1, 2026"],
["ARR", "$148,000"],
["Health", "Strong"],
].map(([k, v]) => (
<div key={k} style={{ display: "grid", gridTemplateColumns: "100px 1fr", gap: 10, padding: "8px 0", fontSize: 13, borderBottom: "1px solid var(--divider)" }}>
<span style={{ color: "var(--text-3)" }}>{k}</span>
<span style={{ fontWeight: 500 }}>{v}</span>
</div>
))}
<div style={{ fontSize: 11, color: "var(--text-3)", textTransform: "uppercase", letterSpacing: "0.06em", fontWeight: 500, margin: "18px 0 8px" }}>Open deals · 3</div>
{[
["Renewal '26", "$148K", "Negotiation", 70],
["Vision platform", "$62K", "Discovery", 30],
["Edge SDK pilot", "$24K", "Proposal", 45],
].map(([n, v, s, p]) => (
<div key={n} style={{ padding: "10px 12px", borderRadius: "var(--radius)", background: "var(--surface)", border: "1px solid var(--border)", marginBottom: 8 }}>
<div style={{ display: "flex", justifyContent: "space-between", gap: 8, fontSize: 13, marginBottom: 6 }}>
<span style={{ fontWeight: 500, minWidth: 0, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{n}</span>
<span style={{ color: "var(--text-2)", flexShrink: 0 }}>{v}</span>
</div>
<div style={{ display: "flex", justifyContent: "space-between", fontSize: 11, color: "var(--text-3)", marginBottom: 4 }}>
<span>{s}</span><span>{p}%</span>
</div>
<div style={{ height: 3, borderRadius: 2, background: "var(--surface-alt)", overflow: "hidden" }}>
<div style={{ width: `${p}%`, height: "100%", background: p > 60 ? "var(--success)" : "var(--accent)" }}/>
</div>
</div>
))}
</div>
{/* Main: tabs + activity */}
<div style={{ display: "flex", flexDirection: "column", overflow: "hidden" }}>
<div style={{ padding: "0 28px", borderBottom: "1px solid var(--divider)", background: "var(--surface)" }}>
<Tabs items={[{ label: "Activity", count: 28 }, { label: "Notes", count: 7 }, { label: "People", count: 4 }, { label: "Files" }, { label: "Emails" }]} active="Activity"/>
</div>
<Scroll>
{/* KPIs */}
<div style={{ display: "grid", gridTemplateColumns: "repeat(4, 1fr)", gap: 12, marginBottom: 22 }}>
{[["Pipeline", "$234K", "+$12K 30d"], ["Lifetime", "$420K", "won"], ["Open deals", "3", "1 stalled"], ["Health", "82/100", "stable"]].map(([l, v, s]) => (
<Card key={l} padding={14}>
<div style={{ fontSize: 11, color: "var(--text-3)" }}>{l}</div>
<div style={{ fontSize: 20, fontWeight: 600, marginTop: 4 }}>{v}</div>
<div style={{ fontSize: 11, color: "var(--text-2)", marginTop: 2 }}>{s}</div>
</Card>
))}
</div>
{/* Composer */}
<div style={{ display: "flex", gap: 10, padding: 12, borderRadius: "var(--radius)", border: "1px solid var(--border)", background: "var(--surface)", marginBottom: 20 }}>
<Avatar name="Mira Reyes" color="#d4b8a8" size={28}/>
<input placeholder="Log a note, call, or email…" style={{ flex: 1, border: "none", outline: "none", background: "transparent", fontSize: 13, fontFamily: "var(--font-sans)", color: "var(--text)" }}/>
<Button size="sm">Log</Button>
</div>
{/* Timeline */}
<div style={{ position: "relative", paddingLeft: 22 }}>
<div style={{ position: "absolute", left: 9, top: 6, bottom: 6, width: 1, background: "var(--border)" }}/>
{[
["var(--success)", "Deal moved to Negotiation", "Mira · Renewal '26 · $148,000", "2h ago"],
["var(--accent)", "Email sent · proposal v4", "To Iris, Daniel — opened 6 times", "Yesterday"],
["var(--warn)", "Call logged · 32 min", "Theo — walkthrough with ops lead", "2d ago"],
["var(--text-3)", "Note added", "They need SSO + SCIM by Sept — gating item", "4d ago"],
].map(([dot, t, sub, when], i) => (
<div key={i} style={{ position: "relative", marginBottom: 18 }}>
<span style={{ position: "absolute", left: -19, top: 3, width: 11, height: 11, borderRadius: "50%", background: dot, boxShadow: "0 0 0 3px var(--bg)" }}/>
<div style={{ display: "flex", justifyContent: "space-between" }}>
<span style={{ fontSize: 13, fontWeight: 500 }}>{t}</span>
<span style={{ fontSize: 11, color: "var(--text-3)" }}>{when}</span>
</div>
<div style={{ fontSize: 12, color: "var(--text-2)", marginTop: 2 }}>{sub}</div>
</div>
))}
</div>
</Scroll>
</div>
</div>
</CRMShell>
);
// ============================================================
// 4 · PIPELINE (deals kanban)
// ============================================================
const CRMPipeline = () => {
const columns = [
{ name: "Lead", total: "$420K", tone: "var(--text-3)", deals: [
{ co: "Mossbank", title: "New logo · Platform", val: "$120K", owner: "TR", color: "#a8e8c8", days: 3, tags: ["Inbound"] },
{ co: "Verra", title: "Vision pilot", val: "$84K", owner: "DP", color: "#e8a8c8", days: 8 },
{ co: "Tide Co.", title: "Expansion", val: "$96K", owner: "MR", color: "#d4b8a8", days: 1, tags: ["Warm"] },
]},
{ name: "Qualified", total: "$365K", tone: "var(--accent)", deals: [
{ co: "Northstar", title: "Carrier API", val: "$148K", owner: "MR", color: "#c8e8a8", days: 5, tags: ["Champion"] },
{ co: "Kestrel", title: "Renewal + seats", val: "$120K", owner: "TR", color: "#a8c8e8", days: 12 },
]},
{ name: "Proposal", total: "$280K", tone: "var(--warn)", deals: [
{ co: "Halcyon", title: "Pro renewal", val: "$180K", owner: "DP", color: "#c8a8e8", days: 2, tags: ["Sent"] },
{ co: "Acme", title: "Edge SDK pilot", val: "$24K", owner: "MR", color: "#e8c8a8", days: 6 },
]},
{ name: "Negotiation", total: "$148K", tone: "#f59e0b", deals: [
{ co: "Acme Robotics", title: "Renewal '26", val: "$148K", owner: "MR", color: "#e8a87c", days: 1, tags: ["Hot", "Closing"] },
]},
{ name: "Won", total: "$96K", tone: "var(--success)", deals: [
{ co: "Lowell Works", title: "Annual plan", val: "$96K", owner: "TR", color: "#a8c8e8", days: 0, tags: ["Closed"] },
]},
];
return (
<CRMShell active="deals">
<PageBar title="Deals" sub="12 active · $1.24M open pipeline"
actions={<>
<Button variant="secondary" leadingIcon={<Icon name="bar" size={13}/>}>Forecast</Button>
<Button leadingIcon={<Icon name="plus" size={13}/>}>New deal</Button>
</>}/>
<div style={{ padding: "12px 28px", borderBottom: "1px solid var(--divider)", display: "flex", alignItems: "center", gap: 10, background: "var(--surface)" }}>
<Input placeholder="Search deals" leadingIcon={<Icon name="search" size={13}/>} style={{ width: 240, padding: "6px 10px" }}/>
{["Owner", "Close date", "Value"].map(f => (
<div key={f} style={{ display: "flex", alignItems: "center", gap: 6, padding: "6px 10px", border: "1px dashed var(--border)", borderRadius: "var(--radius-sm)", fontSize: 12, color: "var(--text-2)", cursor: "pointer" }}>{f}<Icon name="chevDown" size={11}/></div>
))}
</div>
<div style={{ flex: 1, overflow: "auto", padding: 20, background: "var(--bg)" }}>
<div style={{ display: "grid", gridTemplateColumns: "repeat(5, minmax(240px, 1fr))", gap: 14, height: "100%", minWidth: "max-content" }}>
{columns.map(col => (
<div key={col.name} style={{ display: "flex", flexDirection: "column", minWidth: 0 }}>
<div style={{ display: "flex", alignItems: "center", gap: 8, padding: "0 4px 10px" }}>
<span style={{ width: 8, height: 8, borderRadius: "50%", background: col.tone }}/>
<span style={{ fontSize: 13, fontWeight: 600 }}>{col.name}</span>
<span style={{ fontSize: 12, color: "var(--text-3)" }}>{col.deals.length}</span>
<span style={{ flex: 1 }}/>
<span style={{ fontSize: 12, color: "var(--text-2)", fontVariantNumeric: "tabular-nums" }}>{col.total}</span>
</div>
<div style={{ display: "flex", flexDirection: "column", gap: 10, flex: 1, overflowY: "auto", paddingBottom: 8 }}>
{col.deals.map((d, i) => (
<div key={i} style={{ padding: 14, borderRadius: "var(--radius)", background: "var(--surface)", border: "1px solid var(--border)", boxShadow: "var(--shadow-sm)", cursor: "grab" }}>
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 8 }}>
<span style={{ width: 22, height: 22, borderRadius: 6, background: "var(--surface-alt)", display: "flex", alignItems: "center", justifyContent: "center", fontSize: 10, fontWeight: 700, color: "var(--text-2)" }}>{d.co[0]}</span>
<span style={{ fontSize: 12, color: "var(--text-2)", flex: 1, minWidth: 0, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{d.co}</span>
<IconButton name="more" size="sm" label="More"/>
</div>
<div style={{ fontSize: 14, fontWeight: 600, letterSpacing: "-0.01em" }}>{d.title}</div>
<div style={{ fontSize: 18, fontWeight: 600, marginTop: 6, fontVariantNumeric: "tabular-nums" }}>{d.val}</div>
{d.tags && <div style={{ display: "flex", gap: 6, marginTop: 10, flexWrap: "wrap" }}>
{d.tags.map(t => <Badge key={t} tone={t === "Hot" || t === "Closing" ? "danger" : t === "Closed" ? "success" : "neutral"}>{t}</Badge>)}
</div>}
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginTop: 12, paddingTop: 10, borderTop: "1px solid var(--divider)" }}>
<Avatar name={d.owner} color={d.color} size={22}/>
<span style={{ fontSize: 11, color: d.days <= 1 ? "var(--danger)" : "var(--text-3)" }}>
{d.days === 0 ? "Closed" : d.days === 1 ? "Closes today" : `${d.days}d to close`}
</span>
</div>
</div>
))}
<button style={{ padding: "8px", borderRadius: "var(--radius)", border: "1px dashed var(--border)", background: "transparent", color: "var(--text-3)", fontSize: 12, fontFamily: "var(--font-sans)", cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center", gap: 6 }}>
<Icon name="plus" size={12}/> Add deal
</button>
</div>
</div>
))}
</div>
</div>
</CRMShell>
);
};
// ============================================================
// 5 · INBOX
// ============================================================
const CRMInbox = () => {
const threads = [
["IT", "#e8a87c", "Iris Tanaka", "Acme Robotics", "Re: Renewal terms — forwarding to Marco to start paper.", "10:42", 1, true],
["SK", "#c8e8a8", "Sun Kim", "Northstar", "Could we move the demo to Thursday?", "9:18", 1, false],
["DP", "#a8c8e8", "Devi Patel", "Internal", "Closed Halcyon! 🎉 Logging it now.", "Tue", 0, false],
["PN", "#c8a8e8", "Priya Nair", "Halcyon", "Thanks for the deck — a few questions inside.", "Mon", 0, false],
["ML", "#e8c8a8", "Marco Lindqvist", "Kestrel", "Procurement needs the security packet.", "May 28", 0, false],
];
return (
<CRMShell active="inbox">
<div style={{ display: "grid", gridTemplateColumns: "340px 1fr", flex: 1, overflow: "hidden" }}>
{/* List */}
<div style={{ borderRight: "1px solid var(--divider)", display: "flex", flexDirection: "column", overflow: "hidden" }}>
<div style={{ padding: "16px 18px 10px" }}>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 10 }}>
<h1 style={{ margin: 0, fontSize: 20, fontWeight: 600 }}>Inbox</h1>
<Button size="sm" leadingIcon={<Icon name="plus" size={12}/>}>Compose</Button>
</div>
<Input placeholder="Search messages" leadingIcon={<Icon name="search" size={13}/>}/>
</div>
<div style={{ padding: "0 12px 6px", display: "flex", gap: 6 }}>
{["All", "Unread", "Assigned", "Deals"].map((t, i) => (
<span key={t} style={{ padding: "5px 10px", borderRadius: 999, fontSize: 12, fontWeight: 500, cursor: "pointer", background: i === 0 ? "var(--text)" : "transparent", color: i === 0 ? "var(--bg)" : "var(--text-2)" }}>{t}</span>
))}
</div>
<div style={{ flex: 1, overflowY: "auto", padding: "8px 8px" }}>
{threads.map((th, i) => (
<div key={i} style={{ display: "flex", gap: 12, padding: 12, borderRadius: "var(--radius)", cursor: "pointer", marginBottom: 2, background: th[7] ? "var(--surface)" : "transparent", boxShadow: th[7] ? "var(--shadow-sm)" : "none" }}>
<Avatar name={th[2]} color={th[1]} size={38}/>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ display: "flex", justifyContent: "space-between", gap: 6 }}>
<span style={{ fontSize: 13, fontWeight: th[6] ? 700 : 500, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{th[2]}</span>
<span style={{ fontSize: 11, color: "var(--text-3)", flexShrink: 0 }}>{th[5]}</span>
</div>
<div style={{ fontSize: 11, color: "var(--accent)", fontWeight: 500, marginTop: 1 }}>{th[3]}</div>
<div style={{ fontSize: 12, color: th[6] ? "var(--text)" : "var(--text-2)", marginTop: 2, display: "-webkit-box", WebkitLineClamp: 1, WebkitBoxOrient: "vertical", overflow: "hidden" }}>{th[4]}</div>
</div>
{th[6] > 0 && <span style={{ width: 8, height: 8, borderRadius: "50%", background: "var(--accent)", alignSelf: "center", flexShrink: 0 }}/>}
</div>
))}
</div>
</div>
{/* Conversation */}
<div style={{ display: "flex", flexDirection: "column", overflow: "hidden" }}>
<div style={{ padding: "14px 24px", borderBottom: "1px solid var(--divider)", display: "flex", alignItems: "center", gap: 14, background: "var(--surface)" }}>
<Avatar name="Iris Tanaka" color="#e8a87c" size={40}/>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: 15, fontWeight: 600 }}>Iris Tanaka</div>
<div style={{ fontSize: 12, color: "var(--text-3)" }}>iris@acme.io · Head of Engineering</div>
</div>
<Button variant="secondary" size="sm">Open record</Button>
<IconButton name="more" variant="secondary" label="More"/>
</div>
{/* Linked deal */}
<div style={{ padding: "12px 24px 0" }}>
<div style={{ display: "flex", gap: 12, padding: 12, borderRadius: "var(--radius)", background: "var(--surface-2)", border: "1px solid var(--border)", alignItems: "center" }}>
<span style={{ width: 36, height: 36, borderRadius: 8, background: "linear-gradient(135deg, #ff8a3a, #f43f5e)", color: "#fff", display: "flex", alignItems: "center", justifyContent: "center", fontWeight: 700 }}>A</span>
<div style={{ flex: 1 }}>
<div style={{ fontSize: 13, fontWeight: 600 }}>Acme Renewal '26</div>
<div style={{ fontSize: 11, color: "var(--text-3)" }}>Negotiation · $148,000 · closes Jun 12</div>
</div>
<Badge tone="warn" dot>Closing soon</Badge>
</div>
</div>
{/* Messages */}
<div style={{ flex: 1, overflowY: "auto", padding: "20px 24px" }}>
<div style={{ textAlign: "center", fontSize: 11, color: "var(--text-3)", margin: "0 0 16px" }}>Today</div>
{[
[false, "Hi Mira — the team reviewed the renewal terms and they look great. I'm forwarding to Marco in procurement to start the paperwork.", "10:14"],
[true, "That's wonderful news, Iris. I'll send Marco the order form today. Anything he needs from our side to move quickly?", "10:28"],
[false, "Just the security packet (SOC 2 + the SSO/SCIM details). If you can get that over, we should be able to close by the 12th.", "10:42"],
].map(([mine, body, time], i) => (
<div key={i} style={{ display: "flex", flexDirection: mine ? "row-reverse" : "row", gap: 10, alignItems: "flex-end", marginBottom: 12 }}>
{!mine && <Avatar name="Iris Tanaka" color="#e8a87c" size={28}/>}
<div style={{ maxWidth: "62%" }}>
<div style={{ padding: "10px 14px", borderRadius: 16, background: mine ? "var(--text)" : "var(--surface-2)", color: mine ? "var(--bg)" : "var(--text)", fontSize: 13, lineHeight: 1.45, borderBottomRightRadius: mine ? 4 : 16, borderBottomLeftRadius: mine ? 16 : 4, boxShadow: "var(--shadow-sm)" }}>{body}</div>
<div style={{ fontSize: 11, color: "var(--text-3)", marginTop: 4, textAlign: mine ? "right" : "left" }}>{time}</div>
</div>
</div>
))}
</div>
{/* Composer */}
<div style={{ padding: 16, borderTop: "1px solid var(--divider)" }}>
<div style={{ display: "flex", alignItems: "flex-end", gap: 10, padding: 8, borderRadius: 14, background: "var(--surface)", border: "1px solid var(--border)" }}>
<IconButton name="plus" size="sm" variant="ghost" label="Attach"/>
<textarea placeholder="Reply to Iris" rows="1" style={{ flex: 1, border: "none", outline: "none", background: "transparent", fontFamily: "var(--font-sans)", fontSize: 13, color: "var(--text)", resize: "none", padding: "6px 0" }}/>
<Button size="sm">Send</Button>
</div>
</div>
</div>
</div>
</CRMShell>
);
};
// ============================================================
// 6 · REPORTS
// ============================================================
const CRMReports = () => {
const months = ["Dec","Jan","Feb","Mar","Apr","May"];
const won = [180, 220, 195, 260, 240, 284];
const goal = 250;
const maxV = 320;
return (
<CRMShell active="reports">
<PageBar title="Reports" sub="Sales performance · last 6 months"
actions={<>
<Select value="Last 6 months"/>
<Button variant="secondary">Export</Button>
<Button leadingIcon={<Icon name="plus" size={13}/>}>New report</Button>
</>}/>
<Scroll>
<div style={{ display: "grid", gridTemplateColumns: "repeat(4, 1fr)", gap: 14, marginBottom: 20 }}>
<Stat label="Bookings · MTD" value="$284K" delta="18%" up spark={sparkUp}/>
<Stat label="New pipeline" value="$612K" delta="9%" up spark={sparkUp}/>
<Stat label="Avg deal size" value="$84K" delta="2%" up={false} spark={sparkDn}/>
<Stat label="Sales cycle" value="34d" delta="3d" up spark={sparkUp}/>
</div>
<div style={{ display: "grid", gridTemplateColumns: "1.5fr 1fr", gap: 20 }}>
{/* Bookings vs goal */}
<Card padding={24}>
<CardHeader title="Bookings vs goal" subtitle="Closed-won by month"
action={<Tabs variant="pill" items={[{ label: "Monthly" }, { label: "Quarterly" }]} active="Monthly"/>}/>
<div style={{ position: "relative", height: 220, marginTop: 8 }}>
{/* goal line */}
<div style={{ position: "absolute", left: 0, right: 0, bottom: `${(goal / maxV) * 100}%`, borderTop: "2px dashed var(--accent)", zIndex: 1 }}>
<span style={{ position: "absolute", right: 0, top: -18, fontSize: 11, color: "var(--accent)", fontWeight: 600, background: "var(--surface)", padding: "0 4px" }}>Goal ${goal}K</span>
</div>
<div style={{ position: "absolute", inset: 0, display: "flex", alignItems: "flex-end", gap: 18, paddingRight: 4 }}>
{won.map((v, i) => (
<div key={i} style={{ flex: 1, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "flex-end", height: "100%", gap: 8 }}>
<div style={{ width: "100%", maxWidth: 44, height: `${(v / maxV) * 100}%`, borderRadius: "6px 6px 0 0", background: v >= goal ? "linear-gradient(180deg, var(--success), color-mix(in srgb, var(--success) 50%, transparent))" : "linear-gradient(180deg, var(--accent), color-mix(in srgb, var(--accent) 45%, transparent))" }}/>
<span style={{ fontSize: 11, color: "var(--text-3)" }}>{months[i]}</span>
</div>
))}
</div>
</div>
</Card>
{/* Leaderboard */}
<Card padding={0}>
<CardHeader title="Team leaderboard" subtitle="This month"
style={{ padding: "16px 20px", margin: 0, borderBottom: "1px solid var(--divider)" }}/>
<div style={{ padding: "6px 16px 12px" }}>
{[
["MR", "#d4b8a8", "Mira Reyes", "$124K", 100],
["DP", "#a8c8e8", "Devi Patel", "$86K", 70],
["TR", "#c8e8a8", "Theo Roux", "$62K", 50],
["SK", "#e8a87c", "Sun Ortiz", "$48K", 39],
].map(([i, c, n, v, p], idx) => (
<div key={idx} style={{ display: "grid", gridTemplateColumns: "20px 28px 1fr auto", gap: 10, alignItems: "center", padding: "9px 0", borderBottom: idx < 3 ? "1px solid var(--divider)" : "none" }}>
<span style={{ fontSize: 12, color: "var(--text-3)" }}>#{idx + 1}</span>
<Avatar name={n} color={c} size={28}/>
<div style={{ minWidth: 0 }}>
<div style={{ fontSize: 13, fontWeight: 500 }}>{n}</div>
<div style={{ height: 3, borderRadius: 2, background: "var(--surface-alt)", overflow: "hidden", marginTop: 4 }}>
<div style={{ width: `${p}%`, height: "100%", background: "var(--accent)" }}/>
</div>
</div>
<span style={{ fontSize: 13, fontWeight: 600 }}>{v}</span>
</div>
))}
</div>
</Card>
</div>
</Scroll>
</CRMShell>
);
};
// ============================================================
// 7 · SETTINGS · members (admin)
// ============================================================
const CRMSettings = () => {
const members = [
{ id: 1, name: "Mira Reyes", email: "mira@northwind.io", color: "#d4b8a8", role: "Owner", status: "Active", last: "now" },
{ id: 2, name: "Theo Roux", email: "theo@northwind.io", color: "#c8e8a8", role: "Admin", status: "Active", last: "12 min" },
{ id: 3, name: "Devi Patel", email: "devi@northwind.io", color: "#a8c8e8", role: "Admin", status: "Active", last: "1 hour" },
{ id: 4, name: "Sun Ortiz", email: "sun@northwind.io", color: "#e8a87c", role: "Member", status: "Active", last: "today" },
{ id: 5, name: "Linnea Berg", email: "linnea@northwind.io", color: "#c8a8e8", role: "Member", status: "Invited", last: "" },
{ id: 6, name: "Jamal Frost", email: "jamal@partner.co", color: "#a8e8c8", role: "Guest", status: "Active", last: "3 days" },
];
const roleTone = { Owner: "accent", Admin: "info", Member: "success", Guest: "neutral" };
return (
<CRMShell active="settings">
<div style={{ display: "grid", gridTemplateColumns: "200px 1fr", flex: 1, overflow: "hidden" }}>
{/* Settings subnav */}
<div style={{ borderRight: "1px solid var(--divider)", padding: "18px 12px", overflowY: "auto" }}>
<div style={{ fontSize: 11, color: "var(--text-3)", textTransform: "uppercase", letterSpacing: "0.06em", fontWeight: 500, padding: "0 10px 8px" }}>Workspace</div>
{["General", "Members", "Roles", "Pipeline stages", "Integrations", "Billing", "API"].map((s, i) => (
<div key={s} style={{ padding: "7px 10px", borderRadius: "var(--radius-sm)", fontSize: 13, cursor: "pointer", marginBottom: 2, color: i === 1 ? "var(--text)" : "var(--text-2)", fontWeight: i === 1 ? 500 : 400, background: i === 1 ? "var(--surface)" : "transparent", boxShadow: i === 1 ? "var(--shadow-sm)" : "none" }}>{s}</div>
))}
<div style={{ fontSize: 11, color: "var(--text-3)", textTransform: "uppercase", letterSpacing: "0.06em", fontWeight: 500, padding: "16px 10px 8px" }}>Personal</div>
{["Profile", "Notifications", "Sessions"].map(s => (
<div key={s} style={{ padding: "7px 10px", borderRadius: "var(--radius-sm)", fontSize: 13, color: "var(--text-2)", cursor: "pointer", marginBottom: 2 }}>{s}</div>
))}
</div>
{/* Members panel */}
<div style={{ display: "flex", flexDirection: "column", overflow: "hidden" }}>
<PageBar breadcrumb="Settings" title="Members"
sub="Manage who can access the Northwind workspace."
actions={<>
<Button variant="secondary">Export CSV</Button>
<Button leadingIcon={<Icon name="plus" size={13}/>}>Invite people</Button>
</>}/>
<div style={{ padding: "12px 28px", borderBottom: "1px solid var(--divider)", display: "flex", alignItems: "center", gap: 10, background: "var(--surface)" }}>
<Input placeholder="Search members" leadingIcon={<Icon name="search" size={13}/>} style={{ width: 260, padding: "6px 10px" }}/>
<div style={{ flex: 1 }}/>
<span style={{ fontSize: 12, color: "var(--text-2)" }}><b style={{ color: "var(--text)" }}>6</b> members · 1 invited</span>
</div>
<Scroll pad={20}>
<Table
columns={[
{ key: "name", label: "Name", render: r => (
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
<Avatar name={r.name} color={r.color} size={28}/>
<div>
<div style={{ fontWeight: 500 }}>{r.name}</div>
<div style={{ fontSize: 11, color: "var(--text-3)" }}>{r.email}</div>
</div>
</div>
)},
{ key: "role", label: "Role", render: r => (
<span style={{ display: "inline-flex", alignItems: "center", gap: 6, padding: "3px 9px", borderRadius: "var(--radius-sm)", background: "var(--accent-soft)", color: "var(--accent)", fontSize: 12, fontWeight: 500, cursor: "pointer" }}>
{r.role}<Icon name="chevDown" size={11}/>
</span>
)},
{ key: "status", label: "Status", render: r => (
<Badge dot tone={r.status === "Active" ? "success" : "warn"}>{r.status}</Badge>
)},
{ key: "last", label: "Last active" },
{ key: "act", label: "", align: "right", width: 40, render: () => <IconButton name="more" size="sm" label="More"/> },
]}
rows={members}
/>
<div style={{ marginTop: 18 }}>
<Banner tone="warn" title="1 invitation pending"
action={<Button size="sm" variant="secondary">Resend</Button>}>
linnea@northwind.io hasn't accepted yet sent 3 days ago.
</Banner>
</div>
</Scroll>
</div>
</div>
</CRMShell>
);
};
Object.assign(window, {
CRMHome, CRMPeople, CRMRecord, CRMPipeline, CRMInbox, CRMReports, CRMSettings,
});