mirror of
https://github.com/warmbly/warmbly.git
synced 2026-09-09 16:04:41 +00:00
feat: add crm pipeline landing mock
Show how positive replies become pipeline deals with stages, owners, notes, and tasks.
This commit is contained in:
@@ -0,0 +1,390 @@
|
||||
---
|
||||
/**
|
||||
* Pixel-faithful mock of the Warmbly /app/crm/deals screen (the CRM kanban board).
|
||||
*
|
||||
* Maps 1:1 to the real components:
|
||||
* - web/src/app/app/crm/deals/page.tsx (the board page: PageTopbar / StatStrip / SectionBar / Board)
|
||||
* - web/src/components/layout/Page.tsx (PageTopbar h-12, StatStrip, Stat, SectionBar, TopbarAction)
|
||||
* - the Board / Column / DealCard primitives inside deals/page.tsx
|
||||
*
|
||||
* Renders only the in-app screen content (page topbar + stat strip + section bar
|
||||
* + the horizontal kanban). The marketing page wraps this in its own browser
|
||||
* frame, so there is no global sidebar / app header / window chrome here. The
|
||||
* board overflows the frame on purpose; the parent crops with overflow-hidden,
|
||||
* so the load-bearing content sits top-left.
|
||||
*
|
||||
* Tokens copied verbatim from the real source:
|
||||
* - PageTopbar: h-12 px-5 border-b border-slate-200, eyebrow text-[10px] uppercase
|
||||
* tracking-[0.14em] text-slate-400, h-4 w-px divider, subtitle text-[12.5px]
|
||||
* - TopbarAction: h-7 px-2.5 rounded-md bg-sky-600 hover:bg-sky-700 text-white text-[12px]
|
||||
* - Stat: label text-[10px] uppercase tracking-[0.14em], value text-[26px] font-light
|
||||
* - Column head: h-9 px-3, size-1.5 color dot, name text-[11px] uppercase tracking-[0.1em]
|
||||
* font-semibold text-slate-700, count font-mono text-[10.5px]
|
||||
* - DealCard: rounded-lg bg-white border border-slate-200, name text-[12.5px] font-medium,
|
||||
* value font-mono text-[12px] text-slate-700
|
||||
*/
|
||||
|
||||
// Money formatter, mirrors formatMoney() in deals/page.tsx.
|
||||
const money = (n: number) =>
|
||||
new Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
maximumFractionDigits: 0,
|
||||
}).format(n);
|
||||
|
||||
// ── Sample dataset: one pipeline, four stages, eight deals ─────────
|
||||
type Tag = { color: string; label: string };
|
||||
type Deal = {
|
||||
name: string;
|
||||
value: number;
|
||||
contact: string;
|
||||
owner: string; // initials for the avatar
|
||||
gradient: string; // tailwind gradient classes for the owner avatar
|
||||
status?: "won";
|
||||
close?: string;
|
||||
tags: Tag[];
|
||||
};
|
||||
|
||||
type Stage = {
|
||||
name: string;
|
||||
color: string; // hex dot, mirrors STAGE_COLORS in pipelines/page.tsx
|
||||
won?: boolean;
|
||||
deals: Deal[];
|
||||
};
|
||||
|
||||
const stages: Stage[] = [
|
||||
{
|
||||
name: "Lead",
|
||||
color: "#94a3b8", // slate-400
|
||||
deals: [
|
||||
{
|
||||
name: "Acme Co",
|
||||
value: 12000,
|
||||
contact: "dana@acme.co",
|
||||
owner: "MO",
|
||||
gradient: "from-violet-400 to-indigo-500",
|
||||
close: "Jun 18",
|
||||
tags: [{ color: "#3b82f6", label: "Inbound" }],
|
||||
},
|
||||
{
|
||||
name: "Northwind",
|
||||
value: 8500,
|
||||
contact: "ravi@northwind.dev",
|
||||
owner: "JS",
|
||||
gradient: "from-sky-400 to-blue-500",
|
||||
close: "Jun 21",
|
||||
tags: [{ color: "#f59e0b", label: "Outbound" }],
|
||||
},
|
||||
{
|
||||
name: "Spruce Bio",
|
||||
value: 6200,
|
||||
contact: "lena@sprucebio.com",
|
||||
owner: "AM",
|
||||
gradient: "from-rose-400 to-pink-500",
|
||||
tags: [{ color: "#3b82f6", label: "Inbound" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Qualified",
|
||||
color: "#0ea5e9", // sky-500
|
||||
deals: [
|
||||
{
|
||||
name: "Helio",
|
||||
value: 24000,
|
||||
contact: "founder@helio.gen",
|
||||
owner: "MO",
|
||||
gradient: "from-violet-400 to-indigo-500",
|
||||
close: "Jul 02",
|
||||
tags: [
|
||||
{ color: "#8b5cf6", label: "Enterprise" },
|
||||
{ color: "#3b82f6", label: "Inbound" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Tidewater Labs",
|
||||
value: 15500,
|
||||
contact: "ops@tidewater.io",
|
||||
owner: "CR",
|
||||
gradient: "from-teal-400 to-emerald-500",
|
||||
close: "Jul 09",
|
||||
tags: [{ color: "#f59e0b", label: "Outbound" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Proposal",
|
||||
color: "#8b5cf6", // violet-500
|
||||
deals: [
|
||||
{
|
||||
name: "Brightfold",
|
||||
value: 31000,
|
||||
contact: "priya@brightfold.com",
|
||||
owner: "JS",
|
||||
gradient: "from-sky-400 to-blue-500",
|
||||
close: "Jul 15",
|
||||
tags: [
|
||||
{ color: "#8b5cf6", label: "Enterprise" },
|
||||
{ color: "#ef4444", label: "Hot" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Maple & Co",
|
||||
value: 9800,
|
||||
contact: "sam@mapleco.com",
|
||||
owner: "AM",
|
||||
gradient: "from-rose-400 to-pink-500",
|
||||
close: "Jul 20",
|
||||
tags: [{ color: "#3b82f6", label: "Inbound" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Won",
|
||||
color: "#10b981", // emerald-500
|
||||
won: true,
|
||||
deals: [
|
||||
{
|
||||
name: "Cobalt Group",
|
||||
value: 42000,
|
||||
contact: "ceo@cobaltgroup.com",
|
||||
owner: "MO",
|
||||
gradient: "from-violet-400 to-indigo-500",
|
||||
status: "won",
|
||||
close: "Jun 03",
|
||||
tags: [
|
||||
{ color: "#8b5cf6", label: "Enterprise" },
|
||||
{ color: "#10b981", label: "Closed" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// ── Derived strip values, mirrors the page's reduce()s ─────────────
|
||||
const allDeals = stages.flatMap((s) => s.deals);
|
||||
const totalValue = allDeals.reduce((a, d) => a + d.value, 0);
|
||||
const openCount = allDeals.filter((d) => d.status !== "won").length;
|
||||
const wonCount = allDeals.filter((d) => d.status === "won").length;
|
||||
const stageTotal = (s: Stage) => s.deals.reduce((a, d) => a + d.value, 0);
|
||||
|
||||
const stats = [
|
||||
{ label: "Open", value: String(openCount), sub: "active deals", last: false },
|
||||
{ label: "Pipeline value", value: money(totalValue), sub: "all filtered", last: false },
|
||||
{ label: "Won", value: String(wonCount), sub: "this view", last: false },
|
||||
{ label: "Stages", value: String(stages.length), sub: "on this pipeline", last: true },
|
||||
];
|
||||
---
|
||||
<div class="relative w-full flex flex-col bg-white text-slate-900" style="min-height:500px;">
|
||||
<!-- ─────────────────────────────────────────────────────────────
|
||||
1) PAGE TOPBAR (PageTopbar, h-12)
|
||||
────────────────────────────────────────────────────────────── -->
|
||||
<div class="h-12 px-5 border-b border-slate-200 flex items-center gap-3 shrink-0 bg-white">
|
||||
<span class="text-[10px] uppercase tracking-[0.14em] text-slate-400 font-medium">Deals</span>
|
||||
<div class="h-4 w-px bg-slate-200"></div>
|
||||
<span class="text-[12.5px] text-slate-600 truncate">8 deals on Sales pipeline</span>
|
||||
|
||||
<div class="ml-auto flex items-center gap-1.5">
|
||||
<!-- pipeline picker (PipelinePicker) -->
|
||||
<button
|
||||
type="button"
|
||||
class="h-7 px-2.5 rounded-md border border-slate-200 text-[12px] text-slate-700 inline-flex items-center gap-1.5"
|
||||
>
|
||||
Sales pipeline
|
||||
<span class="text-slate-400">▾</span>
|
||||
</button>
|
||||
<!-- total-value pill -->
|
||||
<span
|
||||
class="h-7 px-2.5 rounded-md border border-emerald-200 bg-emerald-50 text-emerald-700 text-[12px] font-mono tabular-nums inline-flex items-center gap-1.5"
|
||||
>
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M16 8h-6a2 2 0 1 0 0 4h4a2 2 0 1 1 0 4H8"/><path d="M12 18V6"/></svg>
|
||||
{money(totalValue)}
|
||||
</span>
|
||||
<!-- search pill (SearchPill) -->
|
||||
<div class="h-7 px-2 rounded-md border border-slate-200 bg-white flex items-center gap-1.5">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="text-slate-400" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
|
||||
<span class="w-[120px] text-[12px] text-slate-400">Search deals…</span>
|
||||
</div>
|
||||
<!-- primary action (TopbarAction, sky-600) -->
|
||||
<button
|
||||
type="button"
|
||||
class="h-7 px-2.5 rounded-md inline-flex items-center gap-1.5 text-[12px] font-medium bg-sky-600 text-white"
|
||||
>
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
||||
Add deal
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ─────────────────────────────────────────────────────────────
|
||||
2) STAT STRIP (StatStrip cols=4)
|
||||
────────────────────────────────────────────────────────────── -->
|
||||
<div class="grid grid-cols-4 border-b border-slate-200 shrink-0 bg-white">
|
||||
{stats.map((s, i) => (
|
||||
<div class={`group px-5 py-4 ${!s.last ? "border-r border-slate-200" : ""}`}>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-[10px] uppercase tracking-[0.14em] text-slate-400 font-medium">{s.label}</span>
|
||||
{i === 1 && <span class="size-1.5 rounded-full bg-sky-500 animate-pulse"></span>}
|
||||
</div>
|
||||
<div class="text-[26px] text-slate-900 font-light leading-none mt-2 tabular-nums">{s.value}</div>
|
||||
<div class="text-[10px] text-slate-400 mt-1.5 font-mono truncate">{s.sub}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<!-- ─────────────────────────────────────────────────────────────
|
||||
3) SECTION BAR (SectionBar, h-9) with a live dot
|
||||
────────────────────────────────────────────────────────────── -->
|
||||
<div class="h-9 px-5 border-b border-slate-200/60 flex items-center gap-2.5 shrink-0">
|
||||
<span class="text-[10px] uppercase tracking-[0.14em] text-slate-400 font-medium">4 stages</span>
|
||||
<span class="font-mono text-[10.5px] text-slate-400 tabular-nums">8</span>
|
||||
<div class="ml-auto inline-flex items-center gap-1.5 text-[11px] text-emerald-600 font-medium">
|
||||
<span class="relative flex size-1.5">
|
||||
<span class="absolute inline-flex h-full w-full rounded-full bg-emerald-500 opacity-60 animate-ping"></span>
|
||||
<span class="relative inline-flex size-1.5 rounded-full bg-emerald-500"></span>
|
||||
</span>
|
||||
live
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ─────────────────────────────────────────────────────────────
|
||||
4) KANBAN BOARD (Board / Column / DealCard)
|
||||
(real: grid grid-flow-col auto-cols-[280px]; here we use flex
|
||||
gap-3 p-5 so columns sit edge to edge and the parent crops)
|
||||
────────────────────────────────────────────────────────────── -->
|
||||
<div class="flex-1 min-h-0 flex gap-3 p-5 overflow-hidden">
|
||||
{stages.map((stage) => (
|
||||
<div
|
||||
class={`flex flex-col rounded-md min-h-[300px] w-[280px] shrink-0 border ${
|
||||
stage.won ? "bg-emerald-50/50 border-emerald-200" : "bg-slate-50 border-slate-200"
|
||||
}`}
|
||||
>
|
||||
<!-- Column header -->
|
||||
<div class="h-9 px-3 flex items-center gap-2 border-b border-slate-200">
|
||||
<span class="size-1.5 rounded-full shrink-0" style={`background-color:${stage.color};`}></span>
|
||||
<span class="text-[11px] uppercase tracking-[0.1em] font-semibold text-slate-700 truncate">{stage.name}</span>
|
||||
<span class="ml-auto font-mono text-[10.5px] text-slate-400 tabular-nums">{stage.deals.length}</span>
|
||||
</div>
|
||||
|
||||
<!-- Stage total value -->
|
||||
<div class={`px-3 py-1 border-b ${stage.won ? "border-emerald-200/60 bg-emerald-50/40" : "border-slate-200/60 bg-white"}`}>
|
||||
<span class="text-[10.5px] text-emerald-700 font-mono tabular-nums">{money(stageTotal(stage))} total</span>
|
||||
</div>
|
||||
|
||||
<!-- Deal cards -->
|
||||
<div class="p-2 space-y-1.5 flex-1">
|
||||
{stage.deals.map((d) => (
|
||||
<div class="rounded-lg bg-white border border-slate-200 hover:border-slate-300 shadow-sm transition-all p-3">
|
||||
<!-- name + won badge -->
|
||||
<div class="flex items-center gap-1.5 mb-1">
|
||||
<div class="text-[12.5px] font-medium text-slate-900 truncate flex-1">{d.name}</div>
|
||||
{d.status === "won" && (
|
||||
<span class="text-[9.5px] uppercase tracking-[0.08em] font-semibold text-emerald-700">Won</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<!-- deal value -->
|
||||
<div class="font-mono text-[12px] text-slate-700 tabular-nums mb-1.5">{money(d.value)}</div>
|
||||
|
||||
<!-- contact line -->
|
||||
<div class="flex items-center gap-1 text-[10.5px] text-slate-500 truncate mb-2">
|
||||
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="shrink-0" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
|
||||
<span class="truncate">{d.contact}</span>
|
||||
</div>
|
||||
|
||||
<!-- owner avatar + tag chips -->
|
||||
<div class="flex items-center gap-1.5 flex-wrap">
|
||||
<span class={`size-6 rounded-full bg-gradient-to-br ${d.gradient} flex items-center justify-center shrink-0 text-[9px] font-semibold text-white`}>{d.owner}</span>
|
||||
{d.tags.map((t) => (
|
||||
<span
|
||||
class="inline-flex items-center gap-1 h-4 pl-1 pr-1.5 rounded-sm border bg-white text-[10px] font-medium text-slate-700 truncate max-w-[110px]"
|
||||
style={`border-color:${t.color}60;background-color:${t.color}12;`}
|
||||
>
|
||||
<span class="block size-2 rounded-full shrink-0" style={`background-color:${t.color};`}></span>
|
||||
{t.label}
|
||||
</span>
|
||||
))}
|
||||
{d.close && (
|
||||
<span class="ml-auto inline-flex items-center gap-1 text-[10px] text-slate-400 shrink-0">
|
||||
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2v4"/><path d="M16 2v4"/><rect width="18" height="18" x="3" y="4" rx="2"/><path d="M3 10h18"/></svg>
|
||||
{d.close}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<!-- ghost 5th column (parent crops it) so the board reads as scrollable -->
|
||||
<div class="flex flex-col rounded-md min-h-[300px] w-[280px] shrink-0 border border-dashed border-slate-200 bg-slate-50/40">
|
||||
<div class="h-9 px-3 flex items-center gap-2 border-b border-slate-200/60">
|
||||
<span class="size-1.5 rounded-full shrink-0 bg-slate-300"></span>
|
||||
<span class="text-[11px] uppercase tracking-[0.1em] font-semibold text-slate-400 truncate">Lost</span>
|
||||
<span class="ml-auto font-mono text-[10.5px] text-slate-300 tabular-nums">0</span>
|
||||
</div>
|
||||
<div class="p-2 flex-1">
|
||||
<div class="h-20 rounded-md border border-dashed border-slate-200 flex items-center justify-center text-[10.5px] text-slate-400">
|
||||
Drop deals here
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ─────────────────────────────────────────────────────────────
|
||||
DEMO SCENE: cursor glides to "Add deal", presses it, and a new
|
||||
deal card appears at the top of the Lead column (cause → effect).
|
||||
Static Astro only; pure CSS, 11s infinite loop.
|
||||
────────────────────────────────────────────────────────────── -->
|
||||
|
||||
<!-- (b) reaction overlay: a NEW Beacon Labs deal card near the Lead column top -->
|
||||
<div class="cr-react" style="left:6.5%;top:33%;width:248px;">
|
||||
<div class="rounded-lg bg-white border border-slate-200 shadow-sm p-3">
|
||||
<div class="flex items-center gap-1.5 mb-1">
|
||||
<div class="text-[12.5px] font-medium text-slate-900 truncate flex-1">Beacon Labs</div>
|
||||
<span class="text-[9.5px] uppercase tracking-[0.08em] font-semibold text-sky-600">New</span>
|
||||
</div>
|
||||
<div class="font-mono text-[12px] text-slate-700 tabular-nums mb-1.5">$9,400</div>
|
||||
<div class="flex items-center gap-1 text-[10.5px] text-slate-500 truncate mb-2">
|
||||
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="shrink-0" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
|
||||
<span class="truncate">ops@beaconlabs.io</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 flex-wrap">
|
||||
<span class="size-6 rounded-full bg-gradient-to-br from-sky-400 to-blue-500 flex items-center justify-center shrink-0 text-[9px] font-semibold text-white">MO</span>
|
||||
<span class="inline-flex items-center gap-1 h-4 pl-1 pr-1.5 rounded-sm border bg-white text-[10px] font-medium text-slate-700 truncate max-w-[110px]" style="border-color:#3b82f660;background-color:#3b82f612;">
|
||||
<span class="block size-2 rounded-full shrink-0" style="background-color:#3b82f6;"></span>
|
||||
Inbound
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- tiny "Deal added" toast, rides the same reaction reveal -->
|
||||
<div class="cr-toast" style="left:6.5%;top:25%;">
|
||||
<span class="inline-flex items-center gap-1.5 h-7 px-2.5 rounded-md bg-slate-900 text-white text-[11px] font-medium shadow-sm">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
|
||||
Deal added
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- (a) cursor -->
|
||||
<div class="cr-cursor" aria-hidden="true">
|
||||
<span class="cr-ripple"></span>
|
||||
<svg width="23" height="27" viewBox="0 0 23 27" fill="none"><path d="M3 2.2 L3 20.4 L7.7 16 L10.9 23.2 L14 21.8 L10.9 14.8 L17.2 14.6 Z" fill="#ffffff" stroke="#0f172a" stroke-width="1.4" stroke-linejoin="round"/></svg>
|
||||
</div>
|
||||
|
||||
<!-- (c) one style block -->
|
||||
<style>
|
||||
.cr-cursor{position:absolute;z-index:50;width:23px;height:27px;pointer-events:none;filter:drop-shadow(0 4px 6px rgba(15,23,42,.4));animation:cr-move 11s cubic-bezier(.5,0,.2,1) infinite;}
|
||||
.cr-cursor svg{display:block;transform-origin:3px 2px;animation:cr-press 11s ease-in-out infinite;}
|
||||
.cr-ripple{position:absolute;left:-6px;top:-6px;width:32px;height:32px;border-radius:9999px;background:rgba(2,132,199,.5);transform:scale(0);opacity:0;animation:cr-rip 11s ease-out infinite;}
|
||||
.cr-react{position:absolute;z-index:40;opacity:0;animation:cr-react 11s ease-out infinite;}
|
||||
.cr-toast{position:absolute;z-index:45;opacity:0;animation:cr-react 11s ease-out infinite;}
|
||||
@keyframes cr-move{0%{left:46%;top:62%}22%{left:84%;top:7%}32%{left:84%;top:7%}58%{left:11%;top:46%}68%{left:11%;top:46%}88%{left:46%;top:62%}100%{left:46%;top:62%}}
|
||||
@keyframes cr-press{0%,26%{transform:scale(1)}28%{transform:scale(.82)}31%,62%{transform:scale(1)}64%{transform:scale(.82)}67%,100%{transform:scale(1)}}
|
||||
@keyframes cr-rip{0%,26%{transform:scale(0);opacity:0}29%{transform:scale(.25);opacity:.7}41%{transform:scale(1.9);opacity:0}60%{transform:scale(0);opacity:0}63%{transform:scale(.25);opacity:.7}75%{transform:scale(1.9);opacity:0}100%{transform:scale(0);opacity:0}}
|
||||
@keyframes cr-react{0%,29%{opacity:0;transform:scale(.92) translateY(-6px)}33%{opacity:1;transform:scale(1) translateY(0)}88%{opacity:1;transform:scale(1) translateY(0)}95%{opacity:0;transform:scale(.92) translateY(-6px)}100%{opacity:0;transform:scale(.92) translateY(-6px)}}
|
||||
@media (prefers-reduced-motion: reduce){.cr-cursor{display:none}.cr-react{display:none}.cr-toast{display:none}}
|
||||
</style>
|
||||
</div>
|
||||
Reference in New Issue
Block a user