Chapter 14 / 15 · App · CRM-flavoured patterns

App pipeline. Move work.

The patterns that make the CRM feel like a CRM. Drag-and-drop kanban for deals. A pipeline bar that doubles as a quick-stage-move. Activity timelines with six row types because real life is messy. Inbox rows you can dispatch with one keystroke. Onboarding lists with playbook structure. And the MRR composition chart, finally vanilla SVG.

14.1 Kanban column & draggable card

The deals view's spine. Each column = a pipeline stage with header (stage · count · sum value) and a scrollable list of cards. Each card has industry chip, value, company, primary contact, an activity-glyphs row, and the fit/intent rings (from 7.15) so “is this real?” reads at a glance. Drag-drop with mouse and keyboard (focus a card, space to pick up, arrows to move, space to drop). Multi-select with shift/ctrl-click. Reduced motion: hard cuts, no rotation overshoot.

Four-stage pipeline (one card lifted, one column flagged as drop)

.kanban · .kb-col · .kb-card

Showing the visual states: hover (one card raised), selected (one with the strong accent ring), dragging (one rotated and elevated), drop-target (one column highlighted). Real drag-drop wiring is the consumer's job — this is the visual contract.

Qualified 12
$148k pipeline
Healthcare $48k

BlueRock Health

Alicia Chen · CTO

Logistics $32k

Northpeak Logistics

Marcus Reid · Head of Ops

Industrial $68k

Skyhook Manufacturing

Sara Kim · VP Sales

Discovery 7
$92k pipeline
Tech $22k

Verkada Networks

James Park · Product

Finance $70k

DataHub Ledger

Fran McAllister · CFO

Negotiation 4
$184k pipeline
Healthcare $56k

CareSync Clinical

Dr. Liu · Director

Logistics $128k

Globalfreight Co.

Esra Dogan · COO

Won 3
$96k closed
Closed $96k

FastLane Couriers

Won by Alicia · 3d ago

<div class="kanban">
  <div class="kb-col">
    <div class="kb-col-head">
      <span class="kb-col-stage">Qualified</span>
      <span class="kb-col-count">12</span>
    </div>
    <div class="kb-col-sum">$148k pipeline</div>
    <div class="kb-cards">
      <div class="kb-card" tabindex="0">
        <div class="kb-card-row1">
          <span class="kb-card-industry">Healthcare</span>
          <span class="kb-card-value">$48k</span>
        </div>
        <h4 class="kb-card-name">BlueRock Health</h4>
        <p class="kb-card-contact">Alicia Chen · CTO</p>
        <div class="kb-card-meta">
          <span class="kb-card-glyphs">...activity icons...</span>
          <span class="kb-card-rings">
            <span class="score-ring score-ring--xs" data-band="high">...</span>
            <span class="score-ring score-ring--xs score-ring--accent">...</span>
          </span>
        </div>
      </div>
    </div>
    <button class="kb-col-add">+ Add deal</button>
  </div>

  <!-- Highlight a column as drop target during a drag -->
  <div class="kb-col is-drop-target">...</div>
</div>
.kanban { display: grid;
  grid-template-columns: repeat(4, minmax(220px, 1fr));
  gap: var(--s-4); align-items: flex-start; }

.kb-col { background: var(--bg-sunk);
  border: 1px solid var(--hair); border-radius: var(--r-lg);
  padding: var(--s-3); }
.kb-col.is-drop-target { background: var(--accent-soft);
  border-color: var(--accent); border-style: dashed; }

.kb-card { background: var(--bg-paper); cursor: grab;
  border: 1px solid var(--hair); border-radius: var(--r-md);
  padding: var(--s-3) var(--s-4); }
.kb-card:hover { border-color: var(--accent);
  transform: translateY(-2px); box-shadow: var(--sh-2); }
.kb-card.is-selected { box-shadow: var(--ring-accent-strong);
  border-color: var(--accent); }
.kb-card.is-dragging { transform: rotate(-2deg) translateY(-2px);
  box-shadow: var(--sh-3); cursor: grabbing; }

@media (prefers-reduced-motion: reduce) {
  .kb-card.is-dragging { transform: none; }
}

14.2 Pipeline stage segmented bar

Sits in a deal's page header. Past stages get a soft ink wash; the current stage fills in --accent; future stages stay transparent. Each cell takes a stage name plus an optional one-line metric (e.g. 2d spent, Day 4 current) — same body font throughout, no mono cruft. Click a future stage to advance (with a confirmation modal); click a past stage to revert (also with confirmation).

Five-stage segmented bar

.pip-bar · .pip-stage

Drives both visual feedback (where am I?) and quick action (move me). Pair with the kanban (14.1) on a record-detail page so the user has both the high-level “deals” view AND a per-deal stage indicator.

Default — at "Negotiation" (3 of 5)
Minimal — labels only, current stage highlighted
<div class="pip-bar" role="navigation" aria-label="Pipeline stages">
  <button class="pip-stage" data-state="past">
    <span class="pip-stage-label">Lead</span>
    <span class="pip-stage-days">2d</span>
  </button>
  <button class="pip-stage" data-state="past">
    <span class="pip-stage-label">Qualified</span>
    <span class="pip-stage-days">5d</span>
  </button>
  <button class="pip-stage" data-state="current">
    <span class="pip-stage-label">Negotiation</span>
    <span class="pip-stage-days">Day 4</span>
  </button>
  <button class="pip-stage" data-state="future">
    <span class="pip-stage-label">Won</span>
  </button>
</div>
.pip-bar { display: flex; background: var(--bg-paper);
  border: 1px solid var(--hair); border-radius: var(--r-pill);
  overflow: hidden; }
.pip-stage { flex: 1; min-height: 64px; padding: 14px 12px;
  background: transparent; border: 0;
  display: flex; flex-direction: column; align-items: center;
  justify-content: center; gap: 4px; cursor: pointer; }
.pip-stage:not(:last-child) { box-shadow: inset -1px 0 0 var(--hair); }
.pip-stage .pip-stage-label { font: 500 13px var(--f-body); color: var(--fg); }
.pip-stage .pip-stage-days  { font: 400 11px var(--f-body); color: var(--fg-faint); }
.pip-stage[data-state="past"] {
  background: color-mix(in oklab, var(--ink) 5%, transparent); }
.pip-stage[data-state="past"] .pip-stage-label { color: var(--fg-dim); }
.pip-stage[data-state="current"] { background: var(--accent); }
.pip-stage[data-state="current"] .pip-stage-label { color: var(--paper); font-weight: 600; }
.pip-stage[data-state="future"] .pip-stage-label { color: var(--fg-dim); }

14.3 Activity timeline

The heart of the contact / company / deal page. A vertical hairline rail with iconified nodes per event. Filter chips at the top scope by type. Date dividers (“Today”, “Yesterday”) keep the chronology readable. Six row variants — email, conversation, meeting, note, stage-change, custom — each with collapsed and expanded states.

All six row variants in one feed

.act-timeline · .act-row[data-type]

Email rows expand to show body + attachments + open/click meters. Conversation rows show transcript snippet. Meeting rows surface attendees + conference link. Notes render markdown. Stage-change shows from→to chips. Custom is the generic catch-all.

Today
Stage moved by Alicia
Qualified Negotiation
2:14 pm
Email · “Re: Renewal proposal” to Alicia Chen
Hi Alicia — thanks for the quick turn. Attached is the proposal we discussed. Happy to walk through it on Thursday if helpful…

From: jay@magicblocks.ai  To: alicia@bluerock.health  CC: marcus@bluerock.health

Hi Alicia — thanks for the quick turn. Attached is the proposal we discussed. Happy to walk through it on Thursday if helpful. Let me know what works for the team.

Best,
Jay

proposal-bluerock-q2.pdf · 412 KB
2 opens 1 click ✓ replied 23m ago
11:42 am
Yesterday · Apr 25
Conversation · 8m on chat with Alicia Chen
Chat · Outcome: Demo booked · Sage handled · escalated to Alicia at min 4

“…does this integrate with our Epic instance?” Sage routed the question to Alicia after recognising it as billing-system-specific. Alicia confirmed integration available, booked a 30-min demo for Thursday.

Open transcript →

4:08 pm
Meeting · “Discovery call · BlueRock x MagicBlocks”
Thu Apr 25 · 2:00–2:30 pm · Attendees: Alicia, Marcus, Jay · meet.google.com/abc-defg
2:00 pm
Note by Jay
Alicia mentioned procurement adds 2 weeks to any signed contract. Plan close-date with that buffer. Their security review usually clears in 5 days — faster than most.
10:15 am
Apr 24
Custom · Renewal reminder triggered
Automation “90-day renewal sequence” fired. Next touchpoint: scheduled Apr 30.
9:00 am
Email · “Quarterly check-in” to Alicia Chen
Quick note as we head into Q2 — wanted to share a couple of patterns we're seeing…

Full body would expand here. Click the row again to collapse.

3:21 pm
<div class="act-timeline">
  <div class="act-filters">
    <button class="act-filter is-active">All</button>
    <button class="act-filter">Email</button>
    ...
  </div>

  <div class="act-divider">Today</div>

  <!-- Stage-change row -->
  <div class="act-row" data-type="stage-change">
    <span class="act-icon">...</span>
    <div class="act-body">
      <div class="act-title">Stage moved <span class="act-by">by Alicia</span></div>
      <div class="act-meta-row">
        <span class="act-chip">Qualified</span>→
        <span class="act-chip">Negotiation</span>
      </div>
    </div>
    <span class="act-time">2:14 pm</span>
  </div>

  <!-- Email row (uses native <details> for expand/collapse) -->
  <div class="act-row" data-type="email" data-expandable="true">
    <span class="act-icon">...</span>
    <details open>
      <summary>
        <div class="act-body">
          <div class="act-title">Email · "Re: Renewal proposal"</div>
          <div class="act-snippet">Hi Alicia...</div>
        </div>
      </summary>
      <div class="act-expand">
        ...full body, attachments, open/click meters...
      </div>
    </details>
    <span class="act-time">11:42 am</span>
  </div>

  <!-- 6 sub-types: email, conversation, meeting, note, stage-change, custom -->
</div>
.act-row { display: grid;
  grid-template-columns: 28px 1fr auto;
  gap: var(--s-3); padding: var(--s-3) 0; position: relative; }

/* Vertical rail */
.act-row::before { content: ""; position: absolute;
  left: 13px; top: 0; bottom: 0; width: 1px; background: var(--hair); }
.act-row:last-child::before { bottom: 50%; }

.act-row .act-icon { width: 28px; height: 28px; border-radius: 50%;
  background: var(--bg-paper); border: 2px solid var(--hair);
  position: relative; z-index: 1; }

/* Type-specific icon tints */
.act-row[data-type="email"] .act-icon { background: var(--info-soft); color: var(--info-text); }
.act-row[data-type="conversation"] .act-icon { background: var(--accent-soft); color: var(--accent-text); }
.act-row[data-type="meeting"] .act-icon { background: color-mix(in oklab, var(--ink) 8%, transparent); }
.act-row[data-type="note"] .act-icon { background: var(--warning-soft); color: var(--warning-text); }
.act-row[data-type="stage-change"] .act-icon { background: var(--success-soft); color: var(--success-text); }

14.4 Inbox row

The unified inbox is the most-touched view. Each row is one task to dispatch: avatar of the related contact, title (the action verb-phrase), sub-line (company · industry · last activity), due time + priority chip on the right, snooze + complete buttons on hover. States: open · snoozed (muted) · overdue (red dot) · completed (struck-through, fades). Keyboard: e to complete, s to snooze, click to open.

All four states in one inbox

.inbox · .inbox-row[data-state]

The row honours --row-h so it auto-densifies when body[data-density="compact"] is set (see 13.3). Overdue rows get the red dot indicator on the left. Completed rows fade so they're visible-but-de-emphasised before scrolling out.

CV
Follow up on Verkada renewal — pending 8 days
Verkada Networks · tech · last touched Apr 18
overdue · 8d High
BR
Send the renewal proposal to BlueRock
BlueRock Health · healthcare · meeting Thu 2pm
today · 4:30 pm High
NP
Reply to Marcus' question about pricing
Northpeak Logistics · logistics · last activity 4h ago
tomorrow Med
SK
Confirm meeting with Skyhook for next week
Skyhook Manufacturing · industrial · snoozed until Apr 28
snoozed · Apr 28 Low
JS
Send NPS survey to top-10 accounts
internal · sent earlier today
done · 1h ago Med
DH
Renew DataHub contract before May 5
DataHub Ledger · finance · 11d remaining
May 5 High
<div class="inbox">
  <div class="inbox-row" data-state="overdue" tabindex="0">
    <span class="av">CV</span>
    <div class="ix-body">
      <div class="ix-title">Follow up on Verkada renewal</div>
      <div class="ix-sub">Verkada Networks · tech · last Apr 18</div>
    </div>
    <span class="ix-due">overdue · 8d</span>
    <span class="ix-priority" data-p="high">High</span>
  </div>

  <!-- States: open | snoozed | overdue | completed -->
  <!-- Keyboard: e = complete · s = snooze -->
</div>
.inbox-row { display: grid;
  grid-template-columns: 36px 1fr auto auto;
  gap: var(--s-3); align-items: center;
  padding: 0 var(--s-4);
  height: var(--row-h, var(--row-h-comfortable));
  /* honours density mode automatically */ }

.inbox-row[data-state="overdue"]::before {
  content: ""; position: absolute; left: 0; top: 50%;
  transform: translateY(-50%);
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--error);
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--error) 20%, transparent); }
.inbox-row[data-state="snoozed"] { opacity: 0.55; }
.inbox-row[data-state="completed"] .ix-title {
  text-decoration: line-through; color: var(--fg-faint); }

14.5 Onboarding checklist

Used for customer onboarding, renewal, and customer-success playbooks. Vertical list with tickbox · title · description · due-date · assignee · “more” menu. Progress bar at the top. Item states: open · done (struck-through, green tick) · skipped (italic, dimmed). Drag-to-reorder is admin-only.

Customer-onboarding playbook (10 items, 4 done)

.checklist

Click the tickbox to toggle done/open. The progress bar updates from the count of done items. Description wraps; long descriptions get truncated with a “Show more” affordance in real use.

Customer onboarding · BlueRock Health

4 of 10 done
40%
Kickoff call scheduled
30-min intro with the customer's exec sponsor + DRI. Send the agenda 24h ahead.
Apr 8 AC
Account provisioned
Workspace created · admin invited · SAML configured for the customer's IdP.
Apr 9 SK
Product training (live)
90-min interactive walkthrough for the customer's team. Recording shared after.
Apr 12 JS
Integration: Slack connected
Sage notifications routed to #customer-success + per-customer channels.
Apr 14 JP
First Sage workflow live
At least one automation running in production for the customer (lead-routing or follow-up).
overdue · Apr 22 MR
Knowledge base seeded
10+ articles uploaded so Sage can answer customer-specific questions.
May 1 AC
30-day check-in scheduled
First success review with metrics + roadmap discussion.
May 8 JS
Custom domain (skipped)
Customer opted out — will use default subdomain.
First quarterly business review
QBR with customer's exec sponsor. Cover usage, ROI, roadmap, expansion.
Jul 10 JS
Renewal conversation kickoff
90 days before contract end. Pull renewal forecast and surface expansion signals.
Jan 1, 2027 AC
<div class="checklist">
  <div class="checklist-head">
    <h4>Customer onboarding · BlueRock Health</h4>
    <div class="checklist-progress">
      <span><strong>4</strong> of 10 done</span>
      <div class="checklist-progress-bar">
        <div class="checklist-progress-fill" style="width: 40%;"></div>
      </div>
      <span>40%</span>
    </div>
  </div>
  <div class="checklist-items">
    <div class="checklist-item" data-state="done">
      <span class="checklist-tick" role="checkbox" aria-checked="true">
        <svg ...check.../>
      </span>
      <div class="checklist-body">
        <div class="checklist-title">Kickoff call scheduled</div>
        <div class="checklist-desc">30-min intro...</div>
      </div>
      <div class="checklist-item-meta">
        <span class="checklist-due">Apr 8</span>
        <span class="av">AC</span>
      </div>
    </div>

    <!-- states: open | done | skipped -->
  </div>
</div>
.checklist-tick { width: 22px; height: 22px;
  border: 1.8px solid var(--hair); border-radius: 50%;
  background: var(--bg-paper); cursor: pointer; }
.checklist-item[data-state="done"] .checklist-tick {
  background: var(--success); border-color: var(--success);
  color: var(--paper); }
.checklist-item[data-state="done"] .checklist-title {
  text-decoration: line-through; color: var(--fg-faint); }
.checklist-item[data-state="skipped"] {
  opacity: 0.5; }
.checklist-item[data-state="skipped"] .checklist-title {
  font-style: italic; }

14.6 MRR composition chart

A vanilla SVG diverging-bar chart for the revenue dashboard. Positive components (New + Expansion) stack above the zero line; negative components (Contraction + Churn) stack below it. The hero number is the current month’s net; the breakdown row directly under it shows the math. An operator can scan the bars and instantly see which months were good, which had spiking churn, and whether expansion is keeping up with cancellations.

12-month MRR composition

.mrr-chart

Static SVG demo. In production, render the bars from your monthly data — the visual contract is what matters: positive stacks above zero, negative stacks below, bold zero rule, hairline gridlines, mono axis labels.

Net new MRR · Apr 2026
Net = New + Expansion − Contraction − Churn
+$48.2k +12.4% vs Mar
This month’s net new
+58.0k New +12.0k Expansion −7.5k Contraction −14.3k Churn = +48.2k Net
+75k +50k +25k 0 −25k MayJun JulAug SepOct NovDec JanFeb Mar Apr
New (acquisition) Expansion (upgrades) Contraction (downgrades) Churn (cancellations)
<div class="mrr-chart">
  <div class="mrr-chart-head">
    <div>
      <div class="mrr-chart-title">Net new MRR · Last 12 months</div>
    </div>
    <div class="mrr-chart-value">$48.2k
      <span class="mrr-chart-delta">+12.4%</span>
    </div>
  </div>

  <!-- Breakdown row: shows the math under the headline number -->
  <div class="mrr-breakdown">
    <span class="mrr-bd"><i class="sw" style="background:var(--success);"></i>+58.0k <small>New</small></span>
    <span class="mrr-bd"><i class="sw" style="background:var(--accent);"></i>+12.0k <small>Exp</small></span>
    <span class="mrr-bd"><i class="sw" style="background:var(--warning);"></i>−7.5k <small>Contr</small></span>
    <span class="mrr-bd"><i class="sw" style="background:var(--error);"></i>−14.3k <small>Churn</small></span>
    <span class="mrr-bd-equals">=</span>
    <span class="mrr-bd is-net">+48.2k <small>Net</small></span>
  </div>

  <!-- Diverging bars: positives stack ABOVE the zero rule, negatives BELOW.
       Scale: 2 px per $1k. Zero line at y=200. Each month is a 53px column
       with a 36px bar. Per month emit 4 rects in the bar group. -->
  <svg viewBox="0 0 720 320">
    <line x1="60" y1="200" x2="696" y2="200" stroke="var(--hair)" stroke-width="1.2"/> <!-- zero rule -->

    <g> <!-- one <g> per month: New, Expansion, Contraction, Churn -->
      <rect x="68"  y="84"  width="36" height="116" fill="var(--success)"/> <!-- New      (above 0) -->
      <rect x="68"  y="60"  width="36" height="24"  fill="var(--accent)"/>  <!-- Expansion (stacked) -->
      <rect x="68"  y="200" width="36" height="15"  fill="var(--warning)"/> <!-- Contraction (below 0) -->
      <rect x="68"  y="215" width="36" height="29"  fill="var(--error)"/>   <!-- Churn (stacked below) -->
    </g>
    <!-- ... 11 more month groups ... -->
  </svg>

  <div class="mrr-legend">
    <span class="mrr-legend-item">
      <span class="mrr-legend-swatch" style="background:var(--success);"></span>
      New <small>(acquisition)</small>
    </span> ...
  </div>
</div>
.mrr-chart { background: var(--bg-paper); padding: var(--s-5);
  border: 1px solid var(--hair); border-radius: var(--r-lg); }
.mrr-chart svg { width: 100%; height: auto; display: block; }

/* Breakdown — the math under the hero number */
.mrr-breakdown {
  display: flex; align-items: center; gap: var(--s-4);
  padding: var(--s-3) var(--s-4); margin-bottom: var(--s-4);
  background: var(--warm-3); border-radius: var(--r-md);
  font: 500 12px var(--f-mono); font-variant-numeric: tabular-nums;
}
body[data-theme="dark"] .mrr-breakdown { background: var(--bg-sunk); }
.mrr-bd { display: inline-flex; align-items: center; gap: 6px; }
.mrr-bd .sw { width: 10px; height: 10px; border-radius: 2px; }
.mrr-bd small { color: var(--fg-dim); text-transform: uppercase; letter-spacing: 0.06em; }
.mrr-bd.is-net { font-weight: 700; }

.mrr-legend-item { display: inline-flex; align-items: center;
  gap: 6px; font: 500 12px var(--f-body); cursor: pointer; }
.mrr-legend-item.is-off { opacity: 0.4; }
.mrr-legend-item.is-off .mrr-legend-swatch { filter: grayscale(0.8); }

/* In real use, render bars from your monthly data: per month, emit
   four rects — two stacked above y=0 (New, Expansion), two stacked
   below (Contraction, Churn). Scale = px-per-dollar. */