8.1 Data table
The primary surface for listing records. Mono uppercase headers (sortable), avatar+name+sub cell for people, right-aligned tabular numbers, and a row-actions button that appears on hover.
Standard table
.tblSort indicator in pink on the active column. Warm row hover. Row divider uses hair-soft.
| Contact | Stage | Last touch | Value | |
|---|---|---|---|---|
|
AC
Alicia Chen alicia@northpeak.co |
Qualified | 2h ago | $48,000 | |
|
MR
Marcus Reyes marcus@trailhaus.io |
Negotiation | Yesterday | $120,000 | |
|
JP
Jordan Park jordan@lumenfin.com |
Won | 3d ago | $64,500 | |
|
SK
Sam Kowalski sam@ridgepine.co |
Contacted | 1w ago | $12,750 |
<div class="tbl-wrap">
<table class="tbl">
<thead>
<tr>
<th class="tbl-sort is-active">Contact <span class="tbl-sort-ic"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="m6 15 6-6 6 6"/></svg></span></th>
<th class="tbl-sort">Stage <span class="tbl-sort-ic"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m7 15 5 5 5-5"/><path d="m7 9 5-5 5 5"/></svg></span></th>
<th class="tbl-sort">Last touch <span class="tbl-sort-ic"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m7 15 5 5 5-5"/><path d="m7 9 5-5 5 5"/></svg></span></th>
<th class="tbl-right tbl-sort">Value <span class="tbl-sort-ic"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m7 15 5 5 5-5"/><path d="m7 9 5-5 5 5"/></svg></span></th>
<th aria-label="Actions"></th>
</tr>
</thead>
<tbody>
<tr>
<td>
<div class="tbl-person">
<span class="av" style="background:var(--info-soft); color: var(--info-text);">AC</span>
<div><div class="tbl-name">Alicia Chen</div><div class="tbl-sub">alicia@northpeak.co</div></div>
</div>
</td>
<td><span class="chip chip-blue">Qualified</span></td>
<td><span class="mono tbl-mono">2h ago</span></td>
<td class="tbl-right tbl-num">$48,000</td>
<td class="tbl-right"><button class="tbl-icon" aria-label="Row actions"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="1"/><circle cx="12" cy="5" r="1"/><circle cx="12" cy="19" r="1"/></svg></button></td>
</tr>
<tr>
<td>
<div class="tbl-person">
<span class="av" style="background:#FFE2EC; color:var(--accent-text);">MR</span>
<div><div class="tbl-name">Marcus Reyes</div><div class="tbl-sub">marcus@trailhaus.io</div></div>
</div>
</td>
<td><span class="chip chip-pink">Negotiation</span></td>
<td><span class="mono tbl-mono">Yesterday</span></td>
<td class="tbl-right tbl-num">$120,000</td>
<td class="tbl-right"><button class="tbl-icon" aria-label="Row actions"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="1"/><circle cx="12" cy="5" r="1"/><circle cx="12" cy="19" r="1"/></svg></button></td>
</tr>
<tr>
<td>
<div class="tbl-person">
<span class="av" style="background:#DDEFDD; color:#1E6B44;">JP</span>
<div><div class="tbl-name">Jordan Park</div><div class="tbl-sub">jordan@lumenfin.com</div></div>
</div>
</td>
<td><span class="chip chip-green">Won</span></td>
<td><span class="mono tbl-mono">3d ago</span></td>
<td class="tbl-right tbl-num">$64,500</td>
<td class="tbl-right"><button class="tbl-icon" aria-label="Row actions"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="1"/><circle cx="12" cy="5" r="1"/><circle cx="12" cy="19" r="1"/></svg></button></td>
</tr>
<tr>
<td>
<div class="tbl-person">
<span class="av" style="background:#F2E6FF; color:#5C2CA8;">SK</span>
<div><div class="tbl-name">Sam Kowalski</div><div class="tbl-sub">sam@ridgepine.co</div></div>
</div>
</td>
<td><span class="chip">Contacted</span></td>
<td><span class="mono tbl-mono">1w ago</span></td>
<td class="tbl-right tbl-num">$12,750</td>
<td class="tbl-right"><button class="tbl-icon" aria-label="Row actions"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="1"/><circle cx="12" cy="5" r="1"/><circle cx="12" cy="19" r="1"/></svg></button></td>
</tr>
</tbody>
</table>
</div>
.tbl-wrap {
background: var(--bg-paper); border: 1px solid var(--hair);
border-radius: var(--r-lg); overflow: hidden;
box-shadow: var(--sh-1);
}
.tbl { width: 100%; border-collapse: separate; border-spacing: 0; font: 14px/1.4 var(--f-body); }
.tbl thead th {
text-align: left; padding: 12px var(--s-4);
font: 500 11px/1 var(--f-mono);
text-transform: uppercase; letter-spacing: 0.08em;
color: var(--fg-dim); background: var(--bg-sunken);
border-bottom: 1px solid var(--hair);
white-space: nowrap;
}
.tbl thead th.tbl-sort { cursor: pointer; user-select: none; }
.tbl thead th.tbl-sort:hover { color: var(--fg-soft); }
.tbl thead th.tbl-sort.is-active { color: var(--fg); }
.tbl-sort-ic { display: inline-flex; vertical-align: -2px; margin-left: 4px; color: var(--fg-faint); }
.tbl-sort.is-active .tbl-sort-ic { color: var(--accent-text); }
.tbl tbody tr { border-bottom: 1px solid var(--hair-soft); transition: background var(--dur-1) var(--ease); }
.tbl tbody tr:last-child { border-bottom: 0; }
.tbl tbody tr:hover { background: var(--bg-warm); }
.tbl td { padding: 14px var(--s-4); color: var(--fg); vertical-align: middle; }
.tbl-right { text-align: right; }
.tbl-num { font-variant-numeric: tabular-nums; font-weight: 600; }
.tbl-mono { font-size: 12.5px; color: var(--fg-soft); }
.tbl-person { display: flex; align-items: center; gap: var(--s-3); }
.tbl-name { font-weight: 600; color: var(--fg); }
.tbl-sub { font-size: 12px; color: var(--fg-dim); margin-top: 1px; }
.tbl-icon {
background: transparent; border: 1px solid transparent; color: var(--fg-dim);
width: 28px; height: 28px; border-radius: var(--r-sm);
display: inline-flex; align-items: center; justify-content: center; cursor: pointer;
}
.tbl-icon:hover { background: var(--bg-sunken); color: var(--fg); border-color: var(--hair); }
.av {
width: 32px; height: 32px; border-radius: 50%;
display: inline-flex; align-items: center; justify-content: center;
font: 600 12px/1 var(--f-display); letter-spacing: 0.02em;
flex-shrink: 0;
}
// Table is plain markup — use the kit CSS classes directly.
import { Chip } from "@magicblocksai/ui";
<div className="tbl-wrap">
<table className="tbl">
<thead><tr>
<th className="tbl-sort is-active">Contact</th>
<th className="tbl-sort">Stage</th>
<th className="tbl-sort">Last touch</th>
<th className="tbl-right tbl-sort">Value</th>
<th aria-label="Actions" />
</tr></thead>
<tbody>
<tr>
<td><div className="tbl-person">
<span className="av">AC</span>
<div><div className="tbl-name">Alicia Chen</div><div className="tbl-sub">alicia@northpeak.co</div></div>
</div></td>
<td><Chip tone="blue">Qualified</Chip></td>
<td><span className="mono tbl-mono">2h ago</span></td>
<td className="tbl-right tbl-num">$48,000</td>
</tr>
<tr><td><div className="tbl-person"><span className="av">MR</span><div><div className="tbl-name">Marcus Reyes</div><div className="tbl-sub">marcus@trailhaus.io</div></div></div></td><td><Chip tone="pink">Negotiation</Chip></td><td><span className="mono tbl-mono">Yesterday</span></td><td className="tbl-right tbl-num">$120,000</td></tr>
<tr><td><div className="tbl-person"><span className="av">JP</span><div><div className="tbl-name">Jordan Park</div><div className="tbl-sub">jordan@lumenfin.com</div></div></div></td><td><Chip tone="green">Won</Chip></td><td><span className="mono tbl-mono">3d ago</span></td><td className="tbl-right tbl-num">$64,500</td></tr>
<tr><td><div className="tbl-person"><span className="av">SK</span><div><div className="tbl-name">Sam Kowalski</div><div className="tbl-sub">sam@ridgepine.co</div></div></div></td><td><Chip>Contacted</Chip></td><td><span className="mono tbl-mono">1w ago</span></td><td className="tbl-right tbl-num">$12,750</td></tr>
</tbody>
</table>
</div>8.2 Table variants
Zebra striping with warm-3 on odd rows for longer analytic tables. Compact layout for dense activity logs where information density matters more than breathing room.
Striped rows
.tbl-stripedOdd rows get a whisper of warm-3. Use for analytic data with 5+ columns.
| Channel | Sent | Opened | Reply rate |
|---|---|---|---|
| 1,204 | 812 | 12.4% | |
| SMS | 340 | 298 | 28.1% |
| Voice | 92 | 74 | 41.3% |
| DM | 176 | 130 | 18.8% |
<div class="tbl-wrap">
<table class="tbl tbl-striped">
<thead>
<tr>
<th>Channel</th><th>Sent</th><th>Opened</th><th class="tbl-right">Reply rate</th>
</tr>
</thead>
<tbody>
<tr><td>Email</td><td class="tbl-num">1,204</td><td class="tbl-num">812</td><td class="tbl-right tbl-num">12.4%</td></tr>
<tr><td>SMS</td><td class="tbl-num">340</td><td class="tbl-num">298</td><td class="tbl-right tbl-num">28.1%</td></tr>
<tr><td>Voice</td><td class="tbl-num">92</td><td class="tbl-num">74</td><td class="tbl-right tbl-num">41.3%</td></tr>
<tr><td>DM</td><td class="tbl-num">176</td><td class="tbl-num">130</td><td class="tbl-right tbl-num">18.8%</td></tr>
</tbody>
</table>
</div>
.tbl-striped tbody tr:nth-child(odd) { background: color-mix(in oklab, var(--warm-3) 40%, transparent); }
.tbl-striped tbody tr:hover { background: var(--bg-warm); }
// Striped table — plain markup with the .tbl-striped modifier.
<div className="tbl-wrap">
<table className="tbl tbl-striped">
<thead><tr>
<th>Channel</th><th>Sent</th><th>Opened</th>
<th className="tbl-right">Reply rate</th>
</tr></thead>
<tbody>
<tr><td>Email</td><td className="tbl-num">1,204</td><td className="tbl-num">812</td><td className="tbl-right tbl-num">12.4%</td></tr>
<tr><td>SMS</td><td className="tbl-num">340</td><td className="tbl-num">298</td><td className="tbl-right tbl-num">28.1%</td></tr>
<tr><td>Voice</td><td className="tbl-num">92</td><td className="tbl-num">74</td><td className="tbl-right tbl-num">41.3%</td></tr>
<tr><td>DM</td><td className="tbl-num">176</td><td className="tbl-num">130</td><td className="tbl-right tbl-num">18.8%</td></tr>
</tbody>
</table>
</div>Compact rows
.tbl-compactTighter padding, smaller type. Use for activity logs, changelogs, audit trails.
| Event | When | By |
|---|---|---|
| Lead qualified | 2m | Agent |
| Email opened | 14m | Alicia C. |
| Reply sent | 48m | Agent |
| Meeting booked | 1h | Alicia C. |
| Notes added | 2h | Marcus R. |
<div class="tbl-wrap">
<table class="tbl tbl-compact">
<thead>
<tr>
<th>Event</th><th>When</th><th class="tbl-right">By</th>
</tr>
</thead>
<tbody>
<tr><td>Lead qualified</td><td class="mono tbl-mono">2m</td><td class="tbl-right">Agent</td></tr>
<tr><td>Email opened</td><td class="mono tbl-mono">14m</td><td class="tbl-right">Alicia C.</td></tr>
<tr><td>Reply sent</td><td class="mono tbl-mono">48m</td><td class="tbl-right">Agent</td></tr>
<tr><td>Meeting booked</td><td class="mono tbl-mono">1h</td><td class="tbl-right">Alicia C.</td></tr>
<tr><td>Notes added</td><td class="mono tbl-mono">2h</td><td class="tbl-right">Marcus R.</td></tr>
</tbody>
</table>
</div>
.tbl-compact thead th { padding: 8px var(--s-3); }
.tbl-compact td { padding: 8px var(--s-3); font-size: 13px; }
// Compact table — same primitives, .tbl-compact modifier.
<div className="tbl-wrap">
<table className="tbl tbl-compact">
<thead><tr>
<th>Event</th><th>When</th><th className="tbl-right">By</th>
</tr></thead>
<tbody>
<tr><td>Lead qualified</td><td className="mono tbl-mono">2m</td><td className="tbl-right">Agent</td></tr>
<tr><td>Email opened</td><td className="mono tbl-mono">14m</td><td className="tbl-right">Alicia C.</td></tr>
<tr><td>Reply sent</td><td className="mono tbl-mono">48m</td><td className="tbl-right">Agent</td></tr>
<tr><td>Meeting booked</td><td className="mono tbl-mono">1h</td><td className="tbl-right">Alicia C.</td></tr>
<tr><td>Notes added</td><td className="mono tbl-mono">2h</td><td className="tbl-right">Marcus R.</td></tr>
</tbody>
</table>
</div>8.3 Chips & tags
Small, pill-shaped labels for categories, statuses, and filters. Soft-tint backgrounds with matching hairline borders. Default is neutral sunken; colour variants map to semantic meaning.
Chip variants
.chipSeven tones. Keep to one chip per piece of metadata so colour retains meaning.
<div class="chip-row">
<span class="chip">Default</span>
<span class="chip chip-pink">Primary</span>
<span class="chip chip-blue">Qualified</span>
<span class="chip chip-green">Won</span>
<span class="chip chip-amber">At risk</span>
<span class="chip chip-red">Lost</span>
<span class="chip chip-purple">Nurture</span>
</div>
.chip {
display: inline-flex; align-items: center; gap: 6px;
padding: 4px 10px;
font: 500 12px/1.2 var(--f-body);
background: var(--bg-sunken); color: var(--fg-soft);
border: 1px solid var(--hair);
border-radius: var(--r-pill);
letter-spacing: 0.01em;
white-space: nowrap;
}
.chip-pink { background: var(--accent-soft); color: var(--accent-text); border-color: color-mix(in oklab, var(--accent) 25%, transparent); }
.chip-blue { background: var(--info-soft); color: var(--info-text); border-color: color-mix(in oklab, var(--info) 25%, transparent); }
.chip-green { background: #E3F2E3; color: #1E6B44; border-color: color-mix(in oklab, #1E6B44 15%, transparent); }
.chip-amber { background: #FFF0D6; color: #8A5A00; border-color: color-mix(in oklab, #8A5A00 15%, transparent); }
.chip-red { background: var(--error-soft); color: var(--error-text); border-color: color-mix(in oklab, var(--error) 25%, transparent); }
.chip-purple { background: #EFE4FF; color: #5C2CA8; border-color: color-mix(in oklab, #5C2CA8 15%, transparent); }
.chip-row { display: flex; flex-wrap: wrap; gap: var(--s-2); }
import { Chip } from "@magicblocksai/ui";
<div className="chip-row">
<Chip>Default</Chip>
<Chip tone="pink">Primary</Chip>
<Chip tone="blue">Qualified</Chip>
<Chip tone="green">Won</Chip>
<Chip tone="amber">At risk</Chip>
<Chip tone="red">Lost</Chip>
<Chip tone="purple">Nurture</Chip>
</div>Dismissible chip
.chip-dismissUsed for removable filters and selected tags. The × sits outside the text baseline so it doesn't disrupt the word.
<div class="chip-row">
<span class="chip chip-dismiss">SaaS <button aria-label="Remove">×</button></span>
<span class="chip chip-dismiss">Fintech <button aria-label="Remove">×</button></span>
<span class="chip chip-dismiss chip-pink">Inbound <button aria-label="Remove">×</button></span>
</div>
.chip-dismiss button {
background: transparent; border: 0; padding: 0;
width: 16px; height: 16px; border-radius: 50%;
display: inline-flex; align-items: center; justify-content: center;
font: 700 14px/1 var(--f-display); color: inherit; opacity: 0.6;
cursor: pointer; margin-left: 2px; margin-right: -4px;
transition: opacity var(--dur-1) var(--ease), background var(--dur-1) var(--ease);
}
.chip-dismiss button:hover { opacity: 1; background: color-mix(in oklab, currentColor 15%, transparent); }
import { Chip } from "@magicblocksai/ui";
export default function Demo() {
return (
<div className="chip-row">
<Chip onDismiss={() => {}}>SaaS</Chip>
<Chip onDismiss={() => {}}>Fintech</Chip>
<Chip tone="pink" onDismiss={() => {}}>Inbound</Chip>
</div>
);
}8.4 Badges & status dots
Smaller than chips and purpose-built for status. A status dot with a glow halo reads instantly at small sizes. Count badges are ink on paper (or pink for notification counts).
Status + count + label badges
.badgeStatus dots include a faint halo so they remain legible on warm backgrounds. Count badges use tabular mono for alignment.
<div class="badge-row">
<span class="badge"><span class="dot dot-green"></span> Live</span>
<span class="badge"><span class="dot dot-amber"></span> Degraded</span>
<span class="badge"><span class="dot dot-red"></span> Down</span>
<span class="badge"><span class="dot dot-blue"></span> Syncing</span>
<span class="badge"><span class="dot dot-grey"></span> Paused</span>
</div>
<div class="badge-row" style="margin-top: var(--s-4);">
<span class="badge badge-count">12</span>
<span class="badge badge-count badge-pink">3</span>
<span class="badge badge-count badge-dot"><span class="dot dot-pink"></span></span>
<span class="badge badge-new">New</span>
<span class="badge badge-beta">Beta</span>
</div>
.badge {
display: inline-flex; align-items: center; gap: 8px;
padding: 5px 10px;
min-height: 22px; /* unify height across every badge variant */
box-sizing: border-box;
font: 500 12px/1 var(--f-body); color: var(--fg);
background: var(--bg-paper);
border: 1px solid var(--hair);
border-radius: var(--r-pill);
}
.badge-row { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-2); }
/* .dot + .dot-{green,amber,red,blue,grey,pink} now live in
_shared.css — drop into any chapter via <span class="dot dot-X">. */
.badge-count {
min-width: 22px; height: 22px; padding: 0 7px;
border-radius: 999px;
background: var(--fg); color: var(--bg); /* pair --fg with --bg so BOTH flip in dark mode (warm-on-ink, ink-on-white) */
font: 600 11px/1 var(--f-mono); letter-spacing: 0.02em;
display: inline-flex; align-items: center; justify-content: center;
border: 0;
}
.badge-pink { background: var(--accent); }
/* Dot-only count badge — keep the 22×22 footprint so the row stays aligned;
the inner .dot carries its own visible pulse/halo. */
.badge-dot {
background: transparent; border: 0; padding: 0;
min-width: 22px; width: 22px; height: 22px;
display: inline-flex; align-items: center; justify-content: center;
}
.badge-new {
background: var(--accent); color: var(--paper); border: 0;
font-family: var(--f-mono); font-size: 10.5px; text-transform: uppercase;
letter-spacing: 0.1em; padding: 4px 10px;
}
.badge-beta {
background: transparent; color: var(--accent-text);
border: 1px solid var(--accent);
font-family: var(--f-mono); font-size: 10.5px; text-transform: uppercase;
letter-spacing: 0.1em; padding: 3px 10px;
}
import { Badge } from "@magicblocksai/ui";
<div className="badge-row">
<Badge tone="success">Live</Badge>
<Badge tone="warning">Degraded</Badge>
<Badge tone="danger">Down</Badge>
<Badge tone="info">Syncing</Badge>
<Badge>Paused</Badge>
</div>8.5 Avatars
Circle with initials and a soft tinted background. Five sizes (xs–xl) cover density from inline metadata chips to large profile heroes. Stacks overlap by 8px with a paper ring between layers.
Sizes, ring, stack
.av · .av-stackTint background + darker text at ~2.5x ratio keeps contrast accessible. Use .av-ring for 'current user' or 'selected'. Use +N overflow for groups >4.
<div class="av-row">
<span class="av av-xs" style="background:var(--info-soft); color: var(--info-text);">AC</span>
<span class="av av-sm" style="background:#FFE2EC; color:var(--accent-text);">MR</span>
<span class="av" style="background:#DDEFDD; color:#1E6B44;">JP</span>
<span class="av av-lg" style="background:#F2E6FF; color:#5C2CA8;">SK</span>
<span class="av av-xl" style="background:var(--ink); color:var(--paper);">MB</span>
<span class="av av-lg av-ring" style="background:var(--accent-soft); color: var(--accent-text);">JS</span>
</div>
<div style="margin-top: var(--s-5);">
<div class="av-stack">
<span class="av av-sm" style="background:var(--info-soft); color: var(--info-text);">AC</span>
<span class="av av-sm" style="background:#FFE2EC; color:var(--accent-text);">MR</span>
<span class="av av-sm" style="background:#DDEFDD; color:#1E6B44;">JP</span>
<span class="av av-sm" style="background:#F2E6FF; color:#5C2CA8;">SK</span>
<span class="av av-sm av-more">+7</span>
</div>
</div>
import { Avatar, AvatarStack } from "@magicblocksai/ui";
<div className="av-row">
<Avatar name="Alicia Chen" size="xs" />
<Avatar name="Marcus Reyes" size="sm" />
<Avatar name="Jordan Park" />
<Avatar name="Sarah Kim" size="lg" />
<Avatar name="Jay Stockwell" size="lg" ring />
</div>
<AvatarStack>
<Avatar name="Alicia Chen" size="sm" />
<Avatar name="Marcus Reyes" size="sm" />
<Avatar name="Jordan Park" size="sm" />
<Avatar name="Sarah Kim" size="sm" />
</AvatarStack>
.av-row { display: flex; align-items: center; gap: var(--s-3); }
.av-xs { width: 20px; height: 20px; font-size: 9px; }
.av-sm { width: 26px; height: 26px; font-size: 10.5px; }
.av-lg { width: 40px; height: 40px; font-size: 14px; }
.av-xl { width: 56px; height: 56px; font-size: 19px; }
.av-ring { box-shadow: 0 0 0 2px var(--bg-paper), 0 0 0 4px var(--accent); }
.av-stack { display: inline-flex; }
.av-stack .av { margin-left: -8px; box-shadow: 0 0 0 2px var(--bg-paper); }
.av-stack .av:first-child { margin-left: 0; }
.av-more {
background: var(--bg-sunken) !important; color: var(--fg-soft) !important;
font-family: var(--f-mono); font-weight: 600;
}8.6 Data lists & key-value
When a table is too heavy, use a list-row surface: avatar + primary + sub + status chip per row. For record details, use the key-value grid — mono uppercase keys, plain-text values.
List rows
.list-rowsSame surface language as the table but optimized for single-record-per-row scanning.
-
AC
QualifiedAlicia ChenReplied 2h ago · via SMS
-
MR
BookedMarcus ReyesMeeting booked · yesterday
-
JP
NurtureJordan ParkFollowed up · 3 days ago
<ul class="list-rows">
<li>
<span class="av" style="background:var(--info-soft); color: var(--info-text);">AC</span>
<div class="lr-main">
<div class="lr-name">Alicia Chen</div>
<div class="lr-sub">Replied 2h ago · via SMS</div>
</div>
<span class="chip chip-green">Qualified</span>
</li>
<li>
<span class="av" style="background:#FFE2EC; color:var(--accent-text);">MR</span>
<div class="lr-main">
<div class="lr-name">Marcus Reyes</div>
<div class="lr-sub">Meeting booked · yesterday</div>
</div>
<span class="chip chip-blue">Booked</span>
</li>
<li>
<span class="av" style="background:#DDEFDD; color:#1E6B44;">JP</span>
<div class="lr-main">
<div class="lr-name">Jordan Park</div>
<div class="lr-sub">Followed up · 3 days ago</div>
</div>
<span class="chip">Nurture</span>
</li>
</ul>
.list-rows {
list-style: none; padding: 0; margin: 0;
background: var(--bg-paper); border: 1px solid var(--hair);
border-radius: var(--r-lg); overflow: hidden;
box-shadow: var(--sh-1);
}
.list-rows li {
display: flex; align-items: center; gap: var(--s-3);
padding: 14px var(--s-4);
border-bottom: 1px solid var(--hair-soft);
transition: background var(--dur-1) var(--ease);
}
.list-rows li:last-child { border-bottom: 0; }
.list-rows li:hover { background: var(--bg-warm); }
.lr-main { flex: 1; min-width: 0; }
.lr-name { font-weight: 600; color: var(--fg); font-size: 14px; }
.lr-sub { font-size: 12.5px; color: var(--fg-dim); margin-top: 2px; }
import { Chip } from "@magicblocksai/ui";
<ul className="list-rows">
<li>
<span className="av">AC</span>
<div className="lr-main">
<div className="lr-name">Alicia Chen</div>
<div className="lr-sub">Replied 2h ago · via SMS</div>
</div>
<Chip tone="green">Qualified</Chip>
</li>
<li>
<span className="av">MR</span>
<div className="lr-main">
<div className="lr-name">Marcus Reyes</div>
<div className="lr-sub">Meeting booked · yesterday</div>
</div>
<Chip tone="blue">Booked</Chip>
</li>
<li>
<span className="av">JP</span>
<div className="lr-main">
<div className="lr-name">Jordan Park</div>
<div className="lr-sub">Followed up · 3 days ago</div>
</div>
<Chip>Nurture</Chip>
</li>
</ul>Key-value grid
.kvTwo-column grid with hairline dividers. Keys render in mono uppercase so the eye can scan labels and values independently.
- Account
- Northpeak Mortgage
- Owner
- Alicia Chen
- Created
- 2026-04-18
- Pipeline value
- $48,000
- Stage
- Qualified
- Last activity
- Replied by SMS · 2h ago
<dl class="kv">
<div><dt>Account</dt><dd>Northpeak Mortgage</dd></div>
<div><dt>Owner</dt><dd>Alicia Chen</dd></div>
<div><dt>Created</dt><dd><span class="mono">2026-04-18</span></dd></div>
<div><dt>Pipeline value</dt><dd class="tbl-num">$48,000</dd></div>
<div><dt>Stage</dt><dd><span class="chip chip-blue">Qualified</span></dd></div>
<div><dt>Last activity</dt><dd>Replied by SMS <span class="fg-dim">· 2h ago</span></dd></div>
</dl>
import { KeyValue } from "@magicblocksai/ui";
<KeyValue
pairs={[
{ key: "Account", value: "Northpeak Mortgage" },
{ key: "Owner", value: "Alicia Chen" },
{ key: "Created", value: "2026-04-18" },
{ key: "Pipeline value", value: "$48,000" },
]}
/>
.kv {
display: grid; grid-template-columns: 1fr 1fr; gap: 1px;
background: var(--hair-soft); border: 1px solid var(--hair);
border-radius: var(--r-lg); overflow: hidden;
margin: 0;
}
.kv > div {
display: grid; grid-template-columns: 140px 1fr; gap: var(--s-3);
padding: 12px var(--s-4);
background: var(--bg-paper);
align-items: baseline;
}
.kv dt {
font: 500 11px/1 var(--f-mono);
text-transform: uppercase; letter-spacing: 0.08em;
color: var(--fg-dim);
}
.kv dd { margin: 0; color: var(--fg); font-size: 14px; }
.fg-dim { color: var(--fg-dim); }
@media (max-width: 640px) { .kv { grid-template-columns: 1fr; } }8.7 Progress
Linear bars for single-dimension progress (task completion, quota, loading). Rings for at-a-glance percentages in dashboards, where the space is small and the number is the hero.
Linear bar
.prog6px track, pink fill by default. Semantic variants green (positive) and amber (attention). Label sits above, tabular-number value aligned right.
<div class="prog-demo">
<div class="prog">
<div class="prog-head"><span class="prog-label">Pipeline coverage</span><span class="prog-val mono">72%</span></div>
<div class="prog-track"><div class="prog-fill" style="width: 72%;"></div></div>
</div>
<div class="prog">
<div class="prog-head"><span class="prog-label">Reply rate</span><span class="prog-val mono">41%</span></div>
<div class="prog-track"><div class="prog-fill prog-green" style="width: 41%;"></div></div>
</div>
<div class="prog">
<div class="prog-head"><span class="prog-label">Quota at risk</span><span class="prog-val mono">18%</span></div>
<div class="prog-track"><div class="prog-fill prog-amber" style="width: 18%;"></div></div>
</div>
</div>
import { ProgressBar } from "@magicblocksai/ui";
<div className="prog-demo">
<ProgressBar value={72} label="Pipeline coverage" showValue />
<ProgressBar value={41} tone="success" label="Reply rate" showValue />
<ProgressBar value={18} tone="warning" label="Quota at risk" showValue />
</div>
.prog-demo { display: flex; flex-direction: column; gap: var(--s-4); width: 100%; max-width: 480px; }
.prog-head { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 8px; }
.prog-label { font: 500 13px/1 var(--f-body); color: var(--fg-soft); }
.prog-val { font-size: 13px; color: var(--fg); font-variant-numeric: tabular-nums; }
.prog-track {
height: 6px; background: var(--bg-sunken);
border-radius: var(--r-pill); overflow: hidden;
position: relative;
}
.prog-fill {
height: 100%; background: var(--accent);
border-radius: inherit;
transition: width var(--dur-4) var(--ease);
}
.prog-green { background: var(--success); }
.prog-amber { background: var(--warning); }Progress ring
.ringSVG stroke technique — dashoffset controls the filled arc. Dasharray = 2πr. Paired with a tabular number and mono label for dashboard blocks.
<div class="rings">
<div class="ring-item">
<svg class="ring" width="72" height="72" viewBox="0 0 72 72">
<circle class="ring-bg" cx="36" cy="36" r="30" fill="none" stroke-width="6"/>
<circle class="ring-fg" cx="36" cy="36" r="30" fill="none" stroke-width="6"
stroke-dasharray="188.5" stroke-dashoffset="52.8" transform="rotate(-90 36 36)"/>
</svg>
<div>
<div class="ring-val">72<span class="ring-unit">%</span></div>
<div class="ring-label">Coverage</div>
</div>
</div>
<div class="ring-item">
<svg class="ring" width="72" height="72" viewBox="0 0 72 72">
<circle class="ring-bg" cx="36" cy="36" r="30" fill="none" stroke-width="6"/>
<circle class="ring-fg ring-green" cx="36" cy="36" r="30" fill="none" stroke-width="6"
stroke-dasharray="188.5" stroke-dashoffset="111.2" transform="rotate(-90 36 36)"/>
</svg>
<div>
<div class="ring-val">41<span class="ring-unit">%</span></div>
<div class="ring-label">Reply rate</div>
</div>
</div>
<div class="ring-item">
<svg class="ring" width="72" height="72" viewBox="0 0 72 72">
<circle class="ring-bg" cx="36" cy="36" r="30" fill="none" stroke-width="6"/>
<circle class="ring-fg ring-amber" cx="36" cy="36" r="30" fill="none" stroke-width="6"
stroke-dasharray="188.5" stroke-dashoffset="154.6" transform="rotate(-90 36 36)"/>
</svg>
<div>
<div class="ring-val">18<span class="ring-unit">%</span></div>
<div class="ring-label">At risk</div>
</div>
</div>
</div>
import { ProgressRing } from "@magicblocksai/ui";
<div className="rings">
<ProgressRing value={72} label="Coverage" />
<ProgressRing value={41} tone="success" label="Reply rate" />
<ProgressRing value={18} tone="warning" label="At risk" />
</div>
.rings { display: flex; gap: var(--s-6); flex-wrap: wrap; }
.ring-item { display: flex; align-items: center; gap: var(--s-3); }
.ring-bg { stroke: var(--bg-sunken); }
.ring-fg { stroke: var(--accent); stroke-linecap: round; transition: stroke-dashoffset var(--dur-4) var(--ease); }
.ring-green { stroke: var(--success); }
.ring-amber { stroke: var(--warning); }
.ring-val { font: 600 22px/1 var(--f-display); color: var(--fg); font-variant-numeric: tabular-nums; letter-spacing: -0.01em; }
.ring-unit { font-size: 13px; color: var(--fg-dim); margin-left: 2px; }
.ring-label { font: 500 11px/1 var(--f-mono); text-transform: uppercase; letter-spacing: 0.08em; color: var(--fg-dim); margin-top: 4px; }8.8 Timeline / activity feed
Vertical activity log. Each item is a dot on a hairline rail, with a timestamp on the right. State is encoded in the dot: filled ink = done, pink with halo = now, ring = scheduled.
Activity timeline
.timelineUse for contact histories, deal events, agent action logs. Pair with the compact table for full audit context.
-
Lead qualified2h ago
Agent confirmed intent, budget, and timeline. Routed to Alicia.
-
Follow-up sent1h ago
Personalised summary + calendar link sent by SMS.
-
Awaiting replynow
Agent is monitoring the thread for a response.
-
Re-engage in 48hscheduled
If no reply, agent sends a soft nudge with a different angle.
<ol class="timeline">
<li class="tl-item tl-done">
<div class="tl-marker"><span class="tl-dot"></span></div>
<div class="tl-body">
<div class="tl-head"><strong>Lead qualified</strong><span class="tl-time mono">2h ago</span></div>
<p class="tl-desc">Agent confirmed intent, budget, and timeline. Routed to Alicia.</p>
</div>
</li>
<li class="tl-item tl-done">
<div class="tl-marker"><span class="tl-dot"></span></div>
<div class="tl-body">
<div class="tl-head"><strong>Follow-up sent</strong><span class="tl-time mono">1h ago</span></div>
<p class="tl-desc">Personalised summary + calendar link sent by SMS.</p>
</div>
</li>
<li class="tl-item tl-now">
<div class="tl-marker"><span class="tl-dot"></span></div>
<div class="tl-body">
<div class="tl-head"><strong>Awaiting reply</strong><span class="tl-time mono">now</span></div>
<p class="tl-desc">Agent is monitoring the thread for a response.</p>
</div>
</li>
<li class="tl-item tl-next">
<div class="tl-marker"><span class="tl-dot"></span></div>
<div class="tl-body">
<div class="tl-head"><strong>Re-engage in 48h</strong><span class="tl-time mono">scheduled</span></div>
<p class="tl-desc">If no reply, agent sends a soft nudge with a different angle.</p>
</div>
</li>
</ol>
import { Timeline } from "@magicblocksai/ui";
<Timeline
items={[
{ state: "done", title: "Lead qualified", timestamp: "2h ago",
description: "Agent confirmed intent, budget, and timeline." },
{ state: "done", title: "Follow-up sent", timestamp: "1h ago",
description: "Personalised summary + calendar link sent by SMS." },
{ state: "now", title: "Awaiting reply", timestamp: "now" },
{ state: "next", title: "Re-engage in 48h", timestamp: "scheduled" },
]}
/>
.timeline {
list-style: none; padding: 0; margin: 0;
position: relative;
}
.tl-item {
display: grid; grid-template-columns: 28px 1fr; gap: var(--s-3);
position: relative;
padding-bottom: var(--s-5);
}
.tl-item:last-child { padding-bottom: 0; }
.tl-marker {
position: relative;
display: flex; justify-content: center;
padding-top: 4px;
}
.tl-dot {
width: 10px; height: 10px; border-radius: 50%;
background: var(--fg-faint);
box-shadow: 0 0 0 3px var(--bg-paper), 0 0 0 4px var(--hair);
position: relative; z-index: 1;
}
.tl-item::before {
content: ""; position: absolute;
left: 13px; top: 18px; bottom: 0;
width: 1px; background: var(--hair);
}
.tl-item:last-child::before { display: none; }
.tl-done .tl-dot { background: var(--fg); box-shadow: 0 0 0 3px var(--bg-paper), 0 0 0 4px var(--fg); }
.tl-now .tl-dot {
background: var(--accent);
box-shadow: 0 0 0 3px var(--bg-paper), 0 0 0 4px var(--accent), 0 0 0 9px var(--accent-soft);
}
.tl-next .tl-dot { background: var(--bg-paper); box-shadow: 0 0 0 3px var(--bg-paper), inset 0 0 0 2px var(--hair); }
.tl-body { padding-top: 0; }
.tl-head { display: flex; justify-content: space-between; align-items: baseline; gap: var(--s-3); }
.tl-head strong { font: 600 14px/1.3 var(--f-display); color: var(--fg); letter-spacing: -0.005em; }
.tl-time { font-size: 12px; color: var(--fg-dim); flex-shrink: 0; }
.tl-desc { font-size: 13.5px; color: var(--fg-soft); margin: 4px 0 0; }8.9 Code block
Ink surface with a header bar — language pill in pink, filename in muted mono, copy button on the right. Syntax colours are desaturated and warm; the pink keyword picks up the brand accent even in code.
Code snippet
.codeblkKeeps ink background contained to the block itself — do NOT let ink bleed into surrounding surfaces. Use the mono-uppercase language pill in pink as a small brand hit.
const agent = await magicblocks.spawn({
persona: "SDR",
channel: "sms",
goal: "qualify + book meeting",
});
await agent.handle(lead);
<figure class="codeblk">
<figcaption>
<span class="codeblk-lang mono">js</span>
<span class="codeblk-file mono">agents/route.js</span>
<button class="codeblk-copy mono">copy</button>
</figcaption>
<pre><code><span class="c-k">const</span> <span class="c-v">agent</span> = <span class="c-k">await</span> magicblocks.<span class="c-f">spawn</span>({
persona: <span class="c-s">"SDR"</span>,
channel: <span class="c-s">"sms"</span>,
goal: <span class="c-s">"qualify + book meeting"</span>,
});
<span class="c-k">await</span> agent.<span class="c-f">handle</span>(lead);</code></pre>
</figure>
.codeblk {
background: var(--ink); color: var(--paper);
border-radius: var(--r-lg); overflow: hidden;
margin: 0; box-shadow: var(--sh-2);
}
.codeblk figcaption {
display: flex; align-items: center; gap: var(--s-3);
padding: 10px var(--s-4);
border-bottom: 1px solid color-mix(in oklab, var(--paper) 10%, transparent);
background: color-mix(in oklab, var(--paper) 4%, var(--ink));
}
.codeblk-lang {
padding: 2px 7px; font-size: 10.5px;
background: var(--accent); color: var(--paper);
border-radius: var(--r-xs); text-transform: uppercase; letter-spacing: 0.08em;
font-weight: 600;
}
.codeblk-file { font-size: 12px; color: color-mix(in oklab, var(--paper) 60%, transparent); flex: 1; }
.codeblk-copy {
background: transparent; border: 1px solid color-mix(in oklab, var(--paper) 20%, transparent);
color: color-mix(in oklab, var(--paper) 70%, transparent);
font-size: 11px; padding: 3px 10px; border-radius: var(--r-xs); cursor: pointer;
transition: color var(--dur-1) var(--ease), border-color var(--dur-1) var(--ease);
}
.codeblk-copy:hover { color: var(--paper); border-color: color-mix(in oklab, var(--paper) 40%, transparent); }
.codeblk pre {
margin: 0; padding: var(--s-4) var(--s-5);
font: 13px/1.7 var(--f-mono); color: color-mix(in oklab, var(--paper) 92%, transparent);
overflow-x: auto;
}
.c-k { color: #FF8BB0; }
.c-v { color: #FFE090; }
.c-f { color: #8CD8FF; }
.c-s { color: #B8F0A6; }
// Code block — plain figure markup with the kit's .codeblk classes.
<figure className="codeblk">
<figcaption>
<span className="codeblk-lang mono">js</span>
<span className="codeblk-file mono">agents/route.js</span>
<button className="codeblk-copy mono">copy</button>
</figcaption>
<pre><code>
<span className="c-k">const</span> <span className="c-v">agent</span> = <span className="c-k">await</span> magicblocks.<span className="c-f">spawn</span>({{"\n"}}
persona: <span className="c-s">"SDR"</span>,{{"\n"}}
channel: <span className="c-s">"sms"</span>,{{"\n"}}
goal: <span className="c-s">"qualify + book meeting"</span>,{{"\n"}}
}});{{"\n\n"}}
<span className="c-k">await</span> agent.<span className="c-f">handle</span>(lead);
</code></pre>
</figure>8.10 Stat grid
A row of KPI tiles across the top of a dashboard or an analytics page. Numbers use tabular-nums; deltas use arrow glyph + colour. Responsive: 4 across at desktop, 2 across at tablet, 1 at mobile.
Four-up stat grid
.stat-gridEach cell is a .stat tile. Label, value, delta. Sits on the warm page with hair borders.
<div class="stat-grid">
<div class="stat-tile">
<div class="stat-label">Leads</div>
<div class="stat-value">1,284</div>
<div class="stat-delta is-up">↑ 12.4%</div>
</div>
</div>.stat-grid { display: grid; grid-template-columns: repeat(4, 1fr);
gap: var(--s-3); width: 100%; }
@media (max-width: 720px) { .stat-grid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 480px) { .stat-grid { grid-template-columns: 1fr; } }
.stat-tile { padding: var(--s-4); background: var(--bg-paper);
border: 1px solid var(--hair); border-radius: var(--r-md); }
.stat-label { font: 500 11px/1 var(--f-mono); text-transform: uppercase;
letter-spacing: 0.08em; color: var(--fg-dim); }
.stat-value { font: 700 24px/1 var(--f-display);
font-variant-numeric: tabular-nums; color: var(--fg); margin: var(--s-2) 0; }
.stat-delta { font: 600 12px/1 var(--f-mono); }
.stat-delta.is-up { color: var(--success-text); }
.stat-delta.is-down { color: var(--error-text); }
import { StatTile } from "@magicblocksai/ui";
// `` is the kit's KPI primitive (chapter-07 .stat-tile).
// Three variants: default (24px value), "big" (36px), "moment"
// (ink-on-accent for the one wow number per page). Sibling
// `` renders the chapter-05 .stat-card surface; reach
// for that when you want the warm-shoulder card chrome instead.
<div className="stat-grid">
<StatTile label="Leads" value="1,284" delta="↑ 12.4%" direction="up" />
<StatTile label="Qualified" value="412" delta="↑ 8.1%" direction="up" />
<StatTile label="Meetings" value="87" delta="↓ 3.2%" direction="down" />
<StatTile label="Revenue" value="$48.2k" delta="↑ 21.6%" direction="up" />
</div> 8.11 Chart
Simple on-brand bar + line charts. Pink for the focus series, lavender for comparison. Grid lines are hair; axis labels are mono small. Drop-in SVG; swap numbers at render.
Bar chart
.chart-bar7-day pattern. Bars use --accent. Y-axis is implicit via gridlines at 0/50/100.
<figure class="chart-bar">
<svg viewBox="0 0 320 174" role="img" aria-label="Weekly leads">
<line x1="0" y1="150" x2="320" y2="150" stroke="var(--hair)"/>
<line x1="0" y1="100" x2="320" y2="100" stroke="var(--hair)" stroke-dasharray="2 4"/>
<line x1="0" y1="50" x2="320" y2="50" stroke="var(--hair)" stroke-dasharray="2 4"/>
<g fill="var(--accent)">
<rect x="10" y="80" width="28" height="70" rx="3"/>
<rect x="54" y="60" width="28" height="90" rx="3"/>
<rect x="98" y="100" width="28" height="50" rx="3"/>
<rect x="142" y="30" width="28" height="120" rx="3"/>
<rect x="186" y="70" width="28" height="80" rx="3"/>
<rect x="230" y="20" width="28" height="130" rx="3"/>
<rect x="274" y="50" width="28" height="100" rx="3"/>
</g>
<g font-size="10" fill="var(--fg-dim)" text-anchor="middle">
<text x="24" y="168">M</text><text x="68" y="168">T</text><text x="112" y="168">W</text>
<text x="156" y="168">T</text><text x="200" y="168">F</text><text x="244" y="168">S</text><text x="288" y="168">S</text>
</g>
</svg>
<figcaption class="chart-cap">Leads · last 7 days</figcaption>
</figure>.chart-bar { padding: var(--s-4); background: var(--bg-paper);
border: 1px solid var(--hair); border-radius: var(--r-md); max-width: 420px; }
.chart-cap { font: 400 12px/1.3 var(--f-mono); color: var(--fg-dim);
margin-top: var(--s-2); text-align: center; }
// Bar chart — drop-in SVG. Use the kit's .chart-bar wrapper class.
const bars = [
{ x: 10, y: 80, h: 70 },
{ x: 54, y: 60, h: 90 },
{ x: 98, y: 100, h: 50 },
{ x: 142, y: 30, h: 120 },
{ x: 186, y: 70, h: 80 },
{ x: 230, y: 20, h: 130 },
{ x: 274, y: 50, h: 100 },
];
const labels = ["M", "T", "W", "T", "F", "S", "S"];
<figure className="chart-bar">
<svg viewBox="0 0 320 174" role="img" aria-label="Weekly leads">
<line x1="0" y1="150" x2="320" y2="150" stroke="var(--hair)" />
<line x1="0" y1="100" x2="320" y2="100" stroke="var(--hair)" strokeDasharray="2 4" />
<line x1="0" y1="50" x2="320" y2="50" stroke="var(--hair)" strokeDasharray="2 4" />
<g fill="var(--accent)">
{bars.map((b, i) => <rect key={i} x={b.x} y={b.y} width="28" height={b.h} rx="3" />)}
</g>
<g fontSize="10" fill="var(--fg-dim)" textAnchor="middle">
{labels.map((l, i) => <text key={i} x={24 + i * 44} y="168">{l}</text>)}
</g>
</svg>
<figcaption className="chart-cap">Leads · last 7 days</figcaption>
</figure>8.12 Tree
Hierarchy display — file tree, org chart, topic taxonomy. Rows expand/collapse with a chevron; nested children indent. Dotted guide lines keep the relationship visible.
Collapsible tree
.treeUses <details>/<summary> for native keyboard + a11y; each branch gets a folder icon, each leaf gets a doc icon.
Sales
North America
- Acme Corp
- Globex
- Initech
Europe
- Hooli GmbH
- Umbrella plc
Support
- Open tickets
- Backlog
<ul class="tree">
<li><details open>
<summary><span class="tree-icon">▸</span> Sales</summary>
<ul>
<li>Acme Corp</li>
</ul>
</details></li>
</ul>.tree, .tree ul { list-style: none; margin: 0; padding: 0; }
.tree ul { padding-left: var(--s-5); border-left: 1px dashed var(--hair);
margin-left: var(--s-2); }
.tree summary, .tree li { font: 400 14px/1.8 var(--f-body); color: var(--fg);
cursor: pointer; padding: 2px 0; list-style: none; }
.tree summary::-webkit-details-marker { display: none; }
.tree-icon { display: inline-block; width: 14px; color: var(--fg-dim);
transition: transform var(--dur-2) var(--ease); }
.tree details[open] > summary .tree-icon { transform: rotate(90deg); }
// Tree — uses native <details>/<summary> for keyboard + a11y.
<ul className="tree">
<li><details open>
<summary><span className="tree-icon">▸</span> Sales</summary>
<ul>
<li><details>
<summary><span className="tree-icon">▸</span> North America</summary>
<ul><li>Acme Corp</li><li>Globex</li><li>Initech</li></ul>
</details></li>
<li><details open>
<summary><span className="tree-icon">▸</span> Europe</summary>
<ul><li>Hooli GmbH</li><li>Umbrella plc</li></ul>
</details></li>
</ul>
</details></li>
<li><details>
<summary><span className="tree-icon">▸</span> Support</summary>
<ul><li>Open tickets</li><li>Backlog</li></ul>
</details></li>
</ul>8.13 Empty state
What the user sees when a table, list, or feed has nothing to show. Warm icon chip, short title, single-sentence explanation, and ONE primary action + one secondary link.
Table / list empty
.emptyNever leave a surface blank. Always explain why it's empty and give the user a way out. Dashed border reads as 'placeholder' rather than a real record.
No leads yet
Your agent hasn't been routed any leads. Connect a source and your first conversation will appear here.
<div class="empty">
<div class="empty-ic"><svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="22 12 16 12 14 15 10 15 8 12 2 12"/><path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/></svg></div>
<h3 class="empty-title">No leads yet</h3>
<p class="empty-lede">Your agent hasn't been routed any leads. Connect a source and your first conversation will appear here.</p>
<div class="empty-actions">
<a href="#" class="empty-btn">Connect a source</a>
<a href="#" class="empty-link">Read the quickstart →</a>
</div>
</div>
import { EmptyState } from "@magicblocksai/ui";
<EmptyState
icon={
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
<polyline points="22 12 16 12 14 15 10 15 8 12 2 12" />
</svg>
}
title="No leads yet"
description="Your agent hasn't been routed any leads. Connect a source and your first conversation will appear here."
primaryAction={<a href="#" className="empty-btn">Connect a source</a>}
secondaryAction={<a href="#" className="empty-link">Read the quickstart →</a>}
/>
.empty {
display: flex; flex-direction: column; align-items: center;
text-align: center; padding: var(--s-9) var(--s-5);
background: var(--bg-paper); border: 1px dashed var(--hair);
border-radius: var(--r-lg);
width: 100%; /* let it fill its slot in .emp-grid / column */
max-width: min(640px, 100%); /* but don't balloon on very wide single-column pages */
margin: 0 auto;
}
.empty-ic {
width: 72px; height: 72px; border-radius: 50%;
display: inline-flex; align-items: center; justify-content: center;
background: var(--warm-3);
color: color-mix(in oklab, var(--ink) 55%, transparent);
margin-bottom: var(--s-4);
}
.empty-title { font: 600 20px/1.2 var(--f-display); letter-spacing: -0.01em; margin: 0 0 6px; color: var(--fg); }
.empty-lede { font: 400 14.5px/1.55 var(--f-body); color: var(--fg-soft); margin: 0 0 var(--s-5); max-width: 36ch; }
.empty-actions { display: flex; gap: var(--s-4); align-items: center; flex-wrap: wrap; justify-content: center; }
.empty-btn {
background: var(--accent); color: var(--paper);
font: 600 13.5px/1 var(--f-display);
padding: 10px var(--s-4); border-radius: var(--r-md);
text-decoration: none; box-shadow: var(--sh-pink);
transition: transform var(--dur-2) var(--ease);
}
.empty-btn:hover { transform: translateY(-1px); color: var(--paper); }
.empty-link { color: var(--fg-soft); font: 500 13.5px/1 var(--f-body); text-decoration: none; }
.empty-link:hover { color: var(--accent-text); }8.14 Anatomy — table row
The five moving parts that make a data row feel like MagicBlocks. Strip any of these away and it starts to look like every other SaaS table.
Five required parts
| 1Contact 2 | Stage | Value |
|---|---|---|
|
3
AC
Alicia Chen alicia@northpeak.co |
4Qualified | 5$48,000 |
- 1Mono uppercase header
11px JetBrains Mono, tracking 0.08em, sunken background. Sort arrow turns pink when active. - 2Sort indicator
Pink chevron on the active column signals direction; grey double-arrow on sortable-but-idle columns. - 3Avatar + primary + sub
32px circle · 14px semibold name · 12px dim email. Person cells always use this triplet. - 4Status chip
One chip per row for stage. Blue = qualified, pink = negotiation, green = won. Don't combine. - 5Right-aligned tabular number
Money and counts right-align so digits stack. 600 weight for emphasis, tabular-nums turns on.
8.15 Score ring
At-a-glance 0–100 score for fit, intent, health, or any band-meaningful metric. Either banded by score range (low/medium/high) or pinned to a metric-specific colour. Four sizes (xs · sm · md · lg) cover inline metadata chips through hero dashboard tiles.
Sizes & bands
.score-ringUse data-band for semantic colour (low/medium/high) or modifier classes (--accent, --ink, --info) for metric-pinned colours. Stroke length encodes value 0–100. Centre integer; mono uppercase label below.
<!-- Banded by score: low (red) · medium (amber) · high (green) -->
<span class="score-ring" data-band="high" aria-label="Score 87">
<svg viewBox="0 0 36 36">
<circle class="track" cx="18" cy="18" r="15"/>
<circle class="fill" cx="18" cy="18" r="15"
stroke-dasharray="94.2" stroke-dashoffset="12.2"/>
</svg>
<span class="v">87</span>
</span>
<!-- Metric-pinned colour: --accent (intent) · --ink (fit) · --info -->
<span class="score-ring score-ring--lg score-ring--accent" aria-label="Intent score 72">
<svg viewBox="0 0 36 36">
<circle class="track" cx="18" cy="18" r="15"/>
<circle class="fill" cx="18" cy="18" r="15"
stroke-dasharray="94.2" stroke-dashoffset="26.4"/>
</svg>
<span class="v">72</span>
</span>
import { ScoreRing } from "@magicblocksai/ui";
// Banded by score (low / medium / high → red / amber / green)
<ScoreRing value={87} band="high" />
// Metric-pinned colour (accent / ink / info)
<ScoreRing value={72} size="lg" variant="accent" label="intent" />
/* The fill stroke-dashoffset = (1 - score/100) * circumference.
Circle r=15 in a 36×36 box → circumference ≈ 94.2.
So 87% → dashoffset 94.2 × (1 - 0.87) = 12.2. */
.score-ring { --sr-size: 40px; --sr-stroke: 4px; --sr-fill: var(--accent); }
.score-ring--xs { --sr-size: 16px; --sr-stroke: 2px; }
.score-ring--sm { --sr-size: 24px; --sr-stroke: 3px; }
.score-ring--lg { --sr-size: 64px; --sr-stroke: 5px; }
.score-ring[data-band="low"] { --sr-fill: var(--score-low); }
.score-ring[data-band="medium"] { --sr-fill: var(--score-medium); }
.score-ring[data-band="high"] { --sr-fill: var(--score-high); }8.16 SLA countdown ring
Time-to-breach indicator for tickets and customer-success queues. Fill grows clockwise as elapsed-fraction grows; colour ramps green → amber → red as the deadline approaches. On breach, gentle shake (skipped under reduced motion). Three sizes: sm (inline), md (card), lg (ticket page hero).
Sizes & states
.sla-ringSet data-state to ok (<50% used) · warn (50–80%) · danger (80%+) · breach. Centre text holds remaining time or "BREACHED". Live-update the dasharray every 30s in the consumer.
<span class="sla-ring" data-state="warn">
<svg viewBox="0 0 36 36">
<circle class="track" cx="18" cy="18" r="15"/>
<circle class="fill" cx="18" cy="18" r="15"
stroke-dasharray="94.2" stroke-dashoffset="32"/>
</svg>
<span class="t">1h 23m</span>
</span>
<!-- breach state animates a shake (no animation under reduced-motion) -->
<span class="sla-ring sla-ring--lg" data-state="breach" aria-label="SLA breached">
<svg viewBox="0 0 36 36">
<circle class="track" cx="18" cy="18" r="15"/>
<circle class="fill" cx="18" cy="18" r="15"
stroke-dasharray="94.2" stroke-dashoffset="0"/>
</svg>
<span class="t">BREACHED</span>
</span>
import { SLARing } from "@magicblocksai/ui";
// state auto-derives from elapsedFraction (ok < 0.5 < warn < 0.8 < danger < 1 ≤ breach)
<SLARing elapsedFraction={0.66} display="1h 23m" />
// Override the inferred state when needed
<SLARing elapsedFraction={1.05} display="BREACHED" size="lg" state="breach" />
.sla-ring[data-state="ok"] { --sla-fill: var(--success); }
.sla-ring[data-state="warn"] { --sla-fill: var(--warning); }
.sla-ring[data-state="danger"] { --sla-fill: var(--error); }
.sla-ring[data-state="breach"] { --sla-fill: var(--error);
animation: sla-shake 0.4s var(--ease) 0s 1; }
@media (prefers-reduced-motion: reduce) {
.sla-ring[data-state="breach"] { animation: none; }
}
/* Live update: in JS, set --sla-fill and the fill circle's
stroke-dashoffset every 30s based on elapsed/total. */8.17 Health sparkline + risk badge
Tiny inline trend (90-day default) for company health, NPS, MRR, account heat, etc. No axes, no gridlines, just the line + an end-dot. Pairs with a risk badge that encodes the same colour band as a label. Three sizes: inline (12px tall, list-row use), card (40px), page (full width 120px with axis labels in the consumer).
Sparkline + risk badge
.spark · .risk-badgeStroke colour follows data-risk. Stroke width auto-bumps in dark mode (1.2px → 1.5px). Always pair with the risk badge in lists so the colour reads explicitly, not just as ambient tone.
<!-- Inline sparkline + risk badge (use side-by-side in list rows) -->
<span class="spark spark--inline" data-risk="low">
<svg viewBox="0 0 64 12" preserveAspectRatio="none">
<polyline class="line" points="1,8 8,9 14,7 21,8 28,5 35,6 42,4 49,3 56,2 63,2"/>
<circle class="end-dot" cx="63" cy="2" r="1.6"/>
</svg>
</span>
<span class="risk-badge" data-risk="low">Healthy · 87</span>
<!-- Risk levels: none · low · medium · high · critical -->
import { Sparkline, RiskBadge } from "@magicblocksai/ui";
// Inline (list-row) variant
<Sparkline values={[82, 81, 83, 84, 86, 86, 87]} risk="low" />
<RiskBadge level="low">Healthy · 87</RiskBadge>
// Card variant — wider, taller, with optional area fill
<Sparkline values={series} variant="card" risk="medium" showArea />
.spark[data-risk="low"] { --spark-color: var(--score-low); }
.spark[data-risk="medium"] { --spark-color: var(--score-medium); }
.spark[data-risk="high"] { --spark-color: var(--score-high); }
.spark[data-risk="critical"] { --spark-color: var(--score-high); }
.spark .line { stroke: var(--spark-color);
stroke-width: var(--spark-stroke); /* 1.2px light, 1.5px dark */ }
.risk-badge[data-risk="low"] { background: var(--badge-bg-success);
color: var(--success-text); }
.risk-badge[data-risk="medium"] { background: var(--badge-bg-warning);
color: var(--warning-text); }
.risk-badge[data-risk="high"] { background: var(--badge-bg-danger);
color: var(--error-text); }8.18 Stat tile — moment + sparkline-behind
Two extensions of the existing .stat-tile primitive (7.10): the moment variant for hero KPIs that need to dominate (deep ink bg, accent number), and the sparkline-behind variant for ambient trend context without taking up extra real estate.
Variants
.stat-tile--moment · .stat-tile--sparkReach for the moment variant when ONE number is the headline of a dashboard ("$1.2M ARR this quarter"). Pair with the spark variant when the trend is as important as the value. Both honour the existing 4-up .stat-grid layout.
<!-- Moment variant: ink bg, accent number -->
<div class="stat-tile stat-tile--moment">
<div class="stat-label">Net new ARR · this quarter</div>
<div class="stat-value">$1.2M</div>
<div class="stat-delta is-up">↑ 24% YoY</div>
</div>
<!-- Sparkline-behind variant: ambient trend at the bottom -->
<div class="stat-tile">
<div class="stat-label">Open tickets</div>
<div class="stat-value">87</div>
<div class="stat-delta is-down">↓ 12 vs last week</div>
<span class="spark stat-spark spark--inline" data-risk="low">
<svg viewBox="0 0 64 12" preserveAspectRatio="none">
<polyline class="line" points="1,4 8,5 14,6 21,5 28,7 35,7 42,8 49,9 56,9 63,10"/>
</svg>
</span>
</div>.stat-tile--moment { background: var(--ink); color: var(--paper);
border-color: rgba(244, 236, 228, 0.12); }
.stat-tile--moment .stat-label { color: rgba(244, 236, 228, 0.65); }
.stat-tile--moment .stat-value { color: var(--accent-text);
font-size: 36px; font-weight: 700; }
/* Sparkline as ambient background — bottom-right, low opacity */
.stat-tile { position: relative; overflow: hidden; }
.stat-tile .stat-spark { position: absolute; right: 0; bottom: 0;
left: 40%; height: 50%; opacity: 0.18; pointer-events: none; }
import { Sparkline } from "@magicblocksai/ui";
// Two extensions of .stat-tile: --moment (ink bg) and sparkline-behind.
<div className="stat-grid">
{/* Sparkline-behind variant */}
<div className="stat-tile">
<div className="stat-label">Open tickets</div>
<div className="stat-value">87</div>
<div className="stat-delta is-down">↓ 12 vs last week</div>
<Sparkline
values={[95, 93, 92, 93, 90, 90, 89, 88, 88, 87]}
risk="low" className="stat-spark" />
</div>
<div className="stat-tile">
<div className="stat-label">Avg first response</div>
<div className="stat-value">4m 12s</div>
<div className="stat-delta is-up">↑ within SLA</div>
<Sparkline
values={[8, 7, 6, 7, 5, 6, 5, 4, 3, 3]}
risk="low" className="stat-spark" />
</div>
{/* Moment variant — hero KPI */}
<div className="stat-tile stat-tile--moment">
<div className="stat-label">Net new ARR · this quarter</div>
<div className="stat-value">$1.2M</div>
<div className="stat-delta is-up">↑ 24% YoY</div>
</div>
<div className="stat-tile stat-tile--moment">
<div className="stat-label">Pipeline forecast</div>
<div className="stat-value">$3.8M</div>
<div className="stat-delta is-up">↑ 11% MoM</div>
</div>
</div>8.19 DataTable
The kit’s sortable, sticky-header, cursor-paginated tabular surface — built on a CSS grid (not <table>) so the sticky header works inside any scroll container. Pairs every consumer-facing CRM list page (Leads, Contacts, Tickets, Broadcasts). Each header is a button wrapped in the same .data-table-th-sortable shell so non-sortable columns align with sortable ones.
Live data table — sort · resize · reorder · show/hide
.data-table · tableKeySortable, sticky-header table. With tableKey set, every column is user-customisable, by mouse or keyboard: resize by dragging a header edge or focusing the grip and pressing ←/→ (Shift = bigger step, Home/End = min/max); reorder by dragging a header or focusing it and pressing Space then ←/→; and toggle column visibility from the ⋮ menu — all persisted to localStorage. The static markup below is the no-JS fallback.
<div class="data-table">
<div role="table" class="data-table-grid data-table-sticky">
<div role="row" class="data-table-row data-table-head">
<div role="columnheader" aria-sort="ascending"
class="data-table-cell data-table-th data-table-th-sortable">
<button class="data-table-sort">
<span>Name</span>
<span class="data-table-sort-ic">↑</span>
</button>
</div>
<!-- …Owner, Due headers… -->
</div>
<div role="row" class="data-table-row">
<div role="cell" class="data-table-cell">Acme Corp</div>
<div role="cell" class="data-table-cell">Jay Stockwell</div>
<div role="cell" class="data-table-cell data-table-align-right">Apr 22</div>
</div>
</div>
</div>
.section-card-body .inbox,
.section-card-body .checklist,
.section-card-body .calendar,
.section-card-body .panel,
.section-card-body > .data-table {
border: 0;
border-radius: 0;
background: transparent;
overflow: visible;
box-shadow: none;
}
.section-card-body > .data-table > .data-table-grid {
border: 0;
border-radius: 0;
background: transparent;
}
.section-card-body > .inbox > .inbox-row:last-child,
.section-card-body > .checklist > li:last-child,
.section-card-body > .data-table .data-table-row:last-child {
border-bottom: 0;
}
.data-table {
/* `position: relative` anchors the absolute-positioned `.data-table-chrome`
overlay (the columns kebab) to the table's top-right corner — see the
chrome rule below. v1.34.0 (Spark Round R36). */
position: relative;
display: flex;
flex-direction: column;
gap: var(--s-3);
width: 100%;
font-family: var(--f-body);
}
.data-table-grid {
display: grid;
/* The outer grid is just a vertical stack of rows — each row has
its own `display: grid` + column tracks. The actual priority-aware
column-template magic (v1.21.0 R19-1) lives on `.data-table-row`
where the inline-style CSS variables are set; see below. */
grid-template-columns: 1fr;
background: var(--bg-paper);
border: 1px solid var(--hair);
border-radius: var(--r-md);
overflow: hidden;
}
@media (max-width: 720px) {
.data-table-grid {
overflow-x: auto;
overflow-y: hidden;
-webkit-overflow-scrolling: touch;
/* Right-edge fade as a scroll cue. Falls back to no-mask on
browsers without mask-image support. */
mask-image: linear-gradient(to right, black calc(100% - 28px), transparent);
-webkit-mask-image: linear-gradient(to right, black calc(100% - 28px), transparent);
}
.data-table-grid .data-table-row {
grid-template-columns: var(--data-table-cols-tablet, var(--data-table-cols-desktop, 1fr));
}
.data-table-cell[data-priority="tertiary"] { display: none; }
}
@media (max-width: 520px) {
.data-table-grid .data-table-row {
grid-template-columns: var(--data-table-cols-mobile, var(--data-table-cols-tablet, var(--data-table-cols-desktop, 1fr)));
}
.data-table-cell[data-priority="secondary"] { display: none; }
}
/* …additional rules trimmed for brevity — see _shared.css */
import { useState } from "react";
import { DataTable } from "@magicblocksai/ui";
import type { DataTableColumn } from "@magicblocksai/ui";
type Contact = { id: string; name: string; owner: string; due: string };
const rows: Contact[] = [
{ id: "c1", name: "Acme Corp", owner: "Jay Stockwell", due: "Apr 22" },
{ id: "c2", name: "Globex Holdings", owner: "Aisha Mwangi", due: "Apr 28" },
];
const columns: DataTableColumn<Contact>[] = [
{ key: "name", label: "Name", sortable: true, width: "minmax(0, 1fr)" },
{ key: "owner", label: "Owner", sortable: true, width: "180px" },
{ key: "due", label: "Due", sortable: true, width: "140px", align: "right" },
];
function Example() {
const [sortBy, setSortBy] = useState<string>("name");
const [sortDir, setSortDir] = useState<"asc" | "desc">("asc");
return (
<DataTable
columns={columns}
rows={rows}
rowId={(r) => r.id}
sortBy={sortBy}
sortDir={sortDir}
onSortChange={(f, d) => { setSortBy(f); setSortDir(d); }}
tableKey="contacts" {/* unlocks drag/keyboard resize · reorder · show-hide, persisted */}
/>
);
}
Compact density
density="compact"Halves horizontal cell padding from 16px to 8px so a wide numeric-heavy table buys back column budget. Row height stays at 12px vertical padding so the row rhythm remains scan-friendly.
<div class="data-table">
<div role="table" data-density="compact" class="data-table-grid">
<!-- header + rows render with halved horizontal cell padding -->
</div>
</div>
.section-card-body .inbox,
.section-card-body .checklist,
.section-card-body .calendar,
.section-card-body .panel,
.section-card-body > .data-table {
border: 0;
border-radius: 0;
background: transparent;
overflow: visible;
box-shadow: none;
}
.section-card-body > .data-table > .data-table-grid {
border: 0;
border-radius: 0;
background: transparent;
}
.section-card-body > .inbox > .inbox-row:last-child,
.section-card-body > .checklist > li:last-child,
.section-card-body > .data-table .data-table-row:last-child {
border-bottom: 0;
}
.data-table {
/* `position: relative` anchors the absolute-positioned `.data-table-chrome`
overlay (the columns kebab) to the table's top-right corner — see the
chrome rule below. v1.34.0 (Spark Round R36). */
position: relative;
display: flex;
flex-direction: column;
gap: var(--s-3);
width: 100%;
font-family: var(--f-body);
}
.data-table-grid {
display: grid;
/* The outer grid is just a vertical stack of rows — each row has
its own `display: grid` + column tracks. The actual priority-aware
column-template magic (v1.21.0 R19-1) lives on `.data-table-row`
where the inline-style CSS variables are set; see below. */
grid-template-columns: 1fr;
background: var(--bg-paper);
border: 1px solid var(--hair);
border-radius: var(--r-md);
overflow: hidden;
}
@media (max-width: 720px) {
.data-table-grid {
overflow-x: auto;
overflow-y: hidden;
-webkit-overflow-scrolling: touch;
/* Right-edge fade as a scroll cue. Falls back to no-mask on
browsers without mask-image support. */
mask-image: linear-gradient(to right, black calc(100% - 28px), transparent);
-webkit-mask-image: linear-gradient(to right, black calc(100% - 28px), transparent);
}
.data-table-grid .data-table-row {
grid-template-columns: var(--data-table-cols-tablet, var(--data-table-cols-desktop, 1fr));
}
.data-table-cell[data-priority="tertiary"] { display: none; }
}
@media (max-width: 520px) {
.data-table-grid .data-table-row {
grid-template-columns: var(--data-table-cols-mobile, var(--data-table-cols-tablet, var(--data-table-cols-desktop, 1fr)));
}
.data-table-cell[data-priority="secondary"] { display: none; }
}
/* …additional rules trimmed for brevity — see _shared.css */
import { DataTable } from "@magicblocksai/ui";
import type { DataTableColumn } from "@magicblocksai/ui";
<DataTable
density="compact"
columns={columns}
rows={rows}
rowId={(r) => r.id}
/>
8.20 FilterChipGroup
Multi-select chip cluster with a leading All chip. Wraps the kit’s existing .chip styles so the tints stay consistent with the rest of the chapter. Each chip is a role="checkbox" button so screen readers announce selection state; the All chip is active when nothing else is selected.
FilterChipGroup
.filter-chip-groupResting state — nothing selected, so the leading All chip is active. Counts render to the right of each chip label.
<div role="group" aria-label="Status" class="filter-chip-group">
<span class="filter-chip-group-label">Status</span>
<div class="filter-chip-group-chips">
<button role="checkbox" aria-checked="true"
class="chip chip-button chip-active">All</button>
<button role="checkbox" aria-checked="false"
class="chip chip-blue chip-button">
Open<span class="chip-count">24</span>
</button>
<!-- …Won, Lost… -->
</div>
</div>
.filter-chip-group {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: var(--s-3);
}
.filter-chip-group-label {
font: 500 13px/1.3 var(--f-body);
color: var(--fg-soft);
}
.filter-chip-group-chips {
display: inline-flex;
flex-wrap: wrap;
gap: var(--s-2);
align-items: center;
}
.filter-chip-group-overflow {
display: flex;
flex-direction: column;
gap: var(--s-2);
min-width: 180px;
}
import { useState } from "react";
import { FilterChipGroup } from "@magicblocksai/ui";
function Example() {
const [status, setStatus] = useState<string[]>([]);
return (
<FilterChipGroup
label="Status"
options={[
{ value: "open", label: "Open", count: 24, tint: "info" },
{ value: "won", label: "Won", count: 12, tint: "success" },
{ value: "lost", label: "Lost", count: 4, tint: "error" },
]}
selected={status}
onChange={setStatus}
/>
);
}
8.21 AvatarGroup
Stacked avatar pile with a +N overflow pill. The more-featured cousin of the bare .av-stack in 7.5 — renders the first max avatars inline, then collapses the rest into an overflow pill that lists their names in a tooltip.
AvatarGroup
.avatar-groupThree avatars visible, the fourth and fifth collapsed into a +2 pill. Hover the pill in the live app for the joined-names tooltip.
<div role="group" data-size="md" class="avatar-group">
<span class="av">AC</span>
<span class="av">MR</span>
<span class="av">JS</span>
<span class="avatar-group-more"
role="img" aria-label="2 more">+2</span>
</div>
.avatar-group { display: inline-flex; align-items: center; }
.avatar-group .av { margin-left: -8px; box-shadow: 0 0 0 2px var(--bg-paper); }
.avatar-group .av:first-child { margin-left: 0; }
.avatar-group-more {
margin-left: -8px;
width: 32px; height: 32px;
border-radius: 50%;
background: var(--bg-sunken);
color: var(--fg-soft);
font: 600 11px/1 var(--f-mono); letter-spacing: 0.02em;
display: inline-flex; align-items: center; justify-content: center;
box-shadow: 0 0 0 2px var(--bg-paper);
flex-shrink: 0;
}
.avatar-group[data-size="xs"] .avatar-group-more { width: 20px; height: 20px; font-size: 9px; margin-left: -6px; }
.avatar-group[data-size="sm"] .avatar-group-more { width: 26px; height: 26px; font-size: 10px; margin-left: -6px; }
.avatar-group[data-size="lg"] .avatar-group-more { width: 40px; height: 40px; font-size: 12px; margin-left: -10px; }
.avatar-group[data-size="xl"] .avatar-group-more { width: 56px; height: 56px; font-size: 14px; margin-left: -12px; }
import { AvatarGroup, Avatar } from "@magicblocksai/ui";
<AvatarGroup max={3}>
<Avatar name="Aisha Chen" />
<Avatar name="Marco Rossi" />
<Avatar name="Jay Stockwell" />
<Avatar name="Priya Patel" />
<Avatar name="Diego Lopez" />
</AvatarGroup>
8.22 LifecyclePill
Typed pill for a contact / account lifecycle stage. Wraps the kit’s .badge with a value-driven tint and label across 17 stages spanning two tiers: top-of-funnel (HubSpot-style contact funnel) and operational (full sales-to-renewal). Each pill carries data-lifecycle + data-lifecycle-tier so consumers can disambiguate near-duplicates in CSS.
LifecyclePill
.badge[data-lifecycle]A sample across both tiers. Reuses the existing kit tints — no new colours introduced.
<span class="badge pill-warm"
data-lifecycle="lead" data-lifecycle-tier="funnel">Lead</span>
<span class="badge badge-success"
data-lifecycle="customer" data-lifecycle-tier="funnel">Customer</span>
<span class="badge pill-purple"
data-lifecycle="success_sprint"
data-lifecycle-tier="operational">Success sprint</span>
.badge {
display: inline-flex; align-items: center; gap: 4px;
padding: 3px 8px;
border-radius: var(--r-pill);
font: 500 11.5px/1.2 var(--f-body);
background: var(--hair-soft);
color: var(--fg);
border: 1px solid transparent;
}
.badge-row { display: inline-flex; flex-wrap: wrap; align-items: center; gap: var(--s-2); }
.badge-accent { background: var(--accent-soft); color: var(--accent-text); }
.badge-success { background: var(--success-soft); color: var(--success-text); }
.badge-warning { background: var(--warning-soft); color: var(--warning-text); }
.badge-danger { background: var(--error-soft); color: var(--error-text); }
.badge-info { background: var(--info-soft); color: var(--info-text); }
.badge-count {
min-width: 20px; height: 20px; padding: 0 6px;
justify-content: center;
font-size: 11px; font-weight: 600; font-variant-numeric: tabular-nums;
}
import { LifecyclePill } from "@magicblocksai/ui";
<LifecyclePill value="lead" />
<LifecyclePill value="customer" />
<LifecyclePill value="success_sprint" />
8.23 TicketStatusPill
Typed pill for a support ticket’s workflow status. Six values: new, open, pending, snoozed (paused, will resurface), resolved, closed. The snoozed purple is distinct from the muted-grey closed and the warning-yellow pending.
TicketStatusPill
.badge[data-ticket-status]All six values side-by-side.
<span class="badge badge-accent" data-ticket-status="open">Open</span>
<span class="badge pill-purple" data-ticket-status="snoozed">Snoozed</span>
<span class="badge badge-success" data-ticket-status="resolved">Resolved</span>
<span class="badge pill-muted" data-ticket-status="closed">Closed</span>
.badge {
display: inline-flex; align-items: center; gap: 4px;
padding: 3px 8px;
border-radius: var(--r-pill);
font: 500 11.5px/1.2 var(--f-body);
background: var(--hair-soft);
color: var(--fg);
border: 1px solid transparent;
}
.badge-row { display: inline-flex; flex-wrap: wrap; align-items: center; gap: var(--s-2); }
.badge-accent { background: var(--accent-soft); color: var(--accent-text); }
.badge-success { background: var(--success-soft); color: var(--success-text); }
.badge-warning { background: var(--warning-soft); color: var(--warning-text); }
.badge-danger { background: var(--error-soft); color: var(--error-text); }
.badge-info { background: var(--info-soft); color: var(--info-text); }
.badge-count {
min-width: 20px; height: 20px; padding: 0 6px;
justify-content: center;
font-size: 11px; font-weight: 600; font-variant-numeric: tabular-nums;
}
import { TicketStatusPill } from "@magicblocksai/ui";
<TicketStatusPill value="open" />
<TicketStatusPill value="snoozed" />
<TicketStatusPill value="resolved" />
8.24 DealStagePill
Typed pill for a deal’s pipeline stage. Stage names are workspace-defined by the consumer; the color token maps to one of six kit tints. Use for pipeline kanbans, deal record headers, and any list-row where the colour needs to encode the workspace’s deal stage.
DealStagePill
.badge[data-deal-stage-color]All six colour tokens applied to a sample of workspace-defined stage names.
<span class="badge badge-info"
data-deal-stage-color="blue">Negotiation</span>
<span class="badge badge-success"
data-deal-stage-color="green">Closed — won</span>
<span class="badge pill-muted"
data-deal-stage-color="gray">Closed — lost</span>
.badge {
display: inline-flex; align-items: center; gap: 4px;
padding: 3px 8px;
border-radius: var(--r-pill);
font: 500 11.5px/1.2 var(--f-body);
background: var(--hair-soft);
color: var(--fg);
border: 1px solid transparent;
}
.badge-row { display: inline-flex; flex-wrap: wrap; align-items: center; gap: var(--s-2); }
.badge-accent { background: var(--accent-soft); color: var(--accent-text); }
.badge-success { background: var(--success-soft); color: var(--success-text); }
.badge-warning { background: var(--warning-soft); color: var(--warning-text); }
.badge-danger { background: var(--error-soft); color: var(--error-text); }
.badge-info { background: var(--info-soft); color: var(--info-text); }
.badge-count {
min-width: 20px; height: 20px; padding: 0 6px;
justify-content: center;
font-size: 11px; font-weight: 600; font-variant-numeric: tabular-nums;
}
import { DealStagePill } from "@magicblocksai/ui";
<DealStagePill stage={{ name: "Negotiation", color: "blue" }} />
<DealStagePill stage={{ name: "Closed — won", color: "green" }} />
<DealStagePill stage={{ name: "Closed — lost", color: "gray" }} />
8.25 RiskPill
Typed pill for a risk / severity level — four-level scale (low, medium, high, urgent). Use for churn risk, deal risk, escalation severity, or ticket priority. urgent layers .pill-urgent on top of .badge-danger for a bolder treatment used by escalation banners. Heads up: low renders green here (low risk is good) — opposite to <ScoreRing band="low"> in 7.15 (low score is bad).
RiskPill
.badge[data-risk]All four levels.
<span class="badge badge-success" data-risk="low">Low</span>
<span class="badge badge-warning" data-risk="medium">Medium</span>
<span class="badge badge-danger" data-risk="high">High</span>
<span class="badge badge-danger pill-urgent" data-risk="urgent">Urgent</span>
.badge {
display: inline-flex; align-items: center; gap: 4px;
padding: 3px 8px;
border-radius: var(--r-pill);
font: 500 11.5px/1.2 var(--f-body);
background: var(--hair-soft);
color: var(--fg);
border: 1px solid transparent;
}
.badge-row { display: inline-flex; flex-wrap: wrap; align-items: center; gap: var(--s-2); }
.badge-accent { background: var(--accent-soft); color: var(--accent-text); }
.badge-success { background: var(--success-soft); color: var(--success-text); }
.badge-warning { background: var(--warning-soft); color: var(--warning-text); }
.badge-danger { background: var(--error-soft); color: var(--error-text); }
.badge-info { background: var(--info-soft); color: var(--info-text); }
.badge-count {
min-width: 20px; height: 20px; padding: 0 6px;
justify-content: center;
font-size: 11px; font-weight: 600; font-variant-numeric: tabular-nums;
}
import { RiskPill } from "@magicblocksai/ui";
<RiskPill value="low" />
<RiskPill value="medium" />
<RiskPill value="high" />
<RiskPill value="urgent" />
8.26 SyncStatus
“Synced 3 minutes ago · Refresh” status line. Recurs on every list page (Contacts, Sessions, Knowledge, Library, Channels, Settings). Pure presentation — the consumer owns the refresh action and provides the timestamp; relative-time formatting ticks once a minute internally.
SyncStatus
.sync-statusThe status text sits left; the Refresh button sits right with a soft spinner glyph that spins under is-busy.
<div class="sync-status">
<span class="sync-status-text">Synced 3 minutes ago</span>
<button type="button" class="sync-status-refresh" aria-label="Refresh">
<span class="sync-status-spinner" aria-hidden="true"></span>
<span class="sync-status-refresh-label">Refresh</span>
</button>
</div>
.sync-status {
display: inline-flex;
align-items: center;
gap: var(--s-2);
font: 400 12px/1.4 var(--f-body);
color: var(--fg-soft);
}
.sync-status-text { white-space: nowrap; }
.sync-status-refresh {
appearance: none;
display: inline-flex;
align-items: center;
gap: 6px;
background: transparent;
border: 0;
padding: 4px 6px;
border-radius: var(--r-xs);
color: var(--accent);
font: 500 12px/1 var(--f-body);
cursor: pointer;
transition: background var(--dur-2) var(--ease);
}
.sync-status-refresh:hover:not(:disabled) { background: var(--bg-warm); }
.sync-status-refresh:disabled { cursor: not-allowed; color: var(--fg-faint); }
.sync-status-spinner {
width: 12px;
height: 12px;
border-radius: 999px;
border: 1.5px solid currentColor;
border-right-color: transparent;
opacity: 0;
}
.sync-status.is-busy .sync-status-spinner {
opacity: 1;
animation: sync-status-spin 0.7s linear infinite;
}
.sync-status.is-busy .sync-status-refresh-label { opacity: 0.6; }
@media (prefers-reduced-motion: reduce) {
.sync-status.is-busy .sync-status-spinner { animation: none; opacity: 0.5; }
}
import { useState } from "react";
import { SyncStatus } from "@magicblocksai/ui";
function Example({ refetch }: { refetch: () => Promise<void> }) {
const [syncedAt, setSyncedAt] = useState(new Date());
const [busy, setBusy] = useState(false);
return (
<SyncStatus
syncedAt={syncedAt}
busy={busy}
onRefresh={async () => {
setBusy(true);
await refetch();
setSyncedAt(new Date());
setBusy(false);
}}
/>
);
}
8.27 KpiDeltaTile
KPI tile with an explicit period-comparison delta — the platform’s “+12% new contacts vs last 7 days” shape. Pairs with the existing .stat-tile family in 7.10 for the simpler no-delta case. direction drives the colour: up green (good), down red (bad), flat neutral. Pass invertDirection for metrics where up is bad (error rate, churn).
KpiDeltaTile
.kpi-delta-tileA good-direction tile and a bad-direction tile. The arrow glyph is decorative — the colour carries the meaning.
<div class="kpi-delta-tile">
<div class="kpi-delta-tile-head">
<span class="kpi-delta-tile-label">New contacts</span>
</div>
<div class="kpi-delta-tile-value">248</div>
<div class="kpi-delta-tile-foot">
<span class="kpi-delta-tile-delta is-tone-good is-dir-up">
<span class="kpi-delta-tile-arrow">↑</span>+12%
</span>
<span class="kpi-delta-tile-compare">vs last 7 days</span>
</div>
</div>
.kpi-delta-tile {
display: flex;
flex-direction: column;
gap: var(--s-2);
padding: var(--s-4);
background: var(--bg-paper);
border: 1px solid var(--hair);
border-radius: var(--r-lg);
}
.kpi-delta-tile-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--s-2);
}
.kpi-delta-tile-label {
font: 600 10.5px/1 var(--f-mono);
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--fg-faint);
}
.kpi-delta-tile-meta { display: inline-flex; align-items: center; gap: var(--s-1); }
.kpi-delta-tile-value {
font: 600 28px/1.05 var(--f-display);
color: var(--fg);
font-variant-numeric: tabular-nums;
}
.kpi-delta-tile-foot {
display: flex;
align-items: baseline;
gap: var(--s-2);
}
.kpi-delta-tile-delta {
display: inline-flex;
align-items: center;
gap: 4px;
font: 500 12.5px/1 var(--f-body);
font-variant-numeric: tabular-nums;
}
.kpi-delta-tile-delta.is-tone-good { color: var(--success-text, #1A6A3F); }
.kpi-delta-tile-delta.is-tone-bad { color: var(--error-text, #8B2417); }
.kpi-delta-tile-delta.is-tone-neutral { color: var(--fg-soft); }
.kpi-delta-tile-arrow { font-size: 11px; }
.kpi-delta-tile-compare { font: 400 12px/1 var(--f-body); color: var(--fg-faint); }
import { KpiDeltaTile } from "@magicblocksai/ui";
<KpiDeltaTile
label="New contacts"
value="248"
delta="+12%"
direction="up"
compareLabel="vs last 7 days"
/>
<KpiDeltaTile
label="Negative sentiment"
value="3.4%"
delta="+0.6 pts"
direction="up"
invertDirection
compareLabel="vs previous period"
/>
8.28 TimeSeriesChart
Multi-series line chart for dashboards and analytics panes. Pure inline SVG — no recharts, no chart.js, no d3. SSR-safe via viewBox rendering. Handles auto Y-axis scaling with “nice” tick intervals, hover tooltip, toggleable legend chips, optional area fills, and optional smooth (Catmull-Rom) curves.
TimeSeriesChart
.time-series-chartA representative 14-day shape with three series — one with an area fill, one solid, one dashed. The legend renders below the chart; clicking a chip toggles that series.
<div class="time-series-chart">
<div class="time-series-chart-head">…title + caption…</div>
<div class="time-series-chart-svg-wrap">
<svg class="time-series-chart-svg" viewBox="0 0 800 240"
preserveAspectRatio="none" role="img">
<!-- gridlines + axis + one <path> per series -->
</svg>
</div>
<div class="time-series-chart-legend">…legend chips…</div>
</div>
.time-series-chart {
display: flex;
flex-direction: column;
gap: var(--s-3);
font: 400 13px/1.4 var(--f-body);
}
.time-series-chart-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: var(--s-3);
}
.time-series-chart-head-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.time-series-chart-title {
font: 600 14px/1.3 var(--f-display);
color: var(--fg);
}
.time-series-chart-caption {
font: 400 12.5px/1.4 var(--f-body);
color: var(--fg-soft);
}
.time-series-chart-meta { display: inline-flex; align-items: center; gap: var(--s-2); }
.time-series-chart-svg-wrap {
position: relative;
width: 100%;
}
.time-series-chart-svg {
width: 100%;
height: 100%;
display: block;
overflow: visible;
}
.time-series-chart-empty {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
color: var(--fg-soft);
font: 400 13px/1.4 var(--f-body);
}
.time-series-chart-gridline {
stroke: var(--hair);
stroke-width: 1;
shape-rendering: crispEdges;
opacity: 0.7;
}
.time-series-chart-axis-label {
font: 400 10.5px/1 var(--f-mono);
fill: var(--fg-faint);
font-variant-numeric: tabular-nums;
}
.time-series-chart-line {
transition: opacity var(--dur-2) var(--ease);
}
.time-series-chart-hover-line {
stroke: color-mix(in oklab, var(--ink) 20%, transparent);
stroke-width: 1;
stroke-dasharray: 3 3;
pointer-events: none;
}
.time-series-chart-hover-dot {
pointer-events: none;
}
.time-series-chart-tooltip {
position: absolute;
top: 8px;
transform: translateX(-50%);
pointer-events: none;
background: var(--bg-paper);
border: 1px solid var(--hair);
border-radius: var(--r-md);
box-shadow: 0 6px 16px color-mix(in oklab, var(--ink) 14%, transparent);
padding: 8px 10px;
min-width: 140px;
z-index: 5;
font: 400 12px/1.4 var(--f-body);
}
.time-series-chart-tooltip-time {
font: 600 10.5px/1 var(--f-mono);
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--fg-faint);
margin-bottom: 4px;
}
.time-series-chart-tooltip-rows { display: flex; flex-direction: column; gap: 2px; }
.time-series-chart-tooltip-row {
display: grid;
grid-template-columns: 8px 1fr auto;
align-items: center;
gap: 6px;
font: 400 12px/1.3 var(--f-body);
color: var(--fg);
}
/* …additional rules trimmed for brevity — see _shared.css */
import { TimeSeriesChart } from "@magicblocksai/ui";
import type { TimeSeriesSeries } from "@magicblocksai/ui";
const day = 24 * 60 * 60 * 1000;
const today = Date.parse("2026-05-18T00:00:00Z");
const series: TimeSeriesSeries[] = [
{
id: "agents",
name: "Active agents",
color: "var(--accent)",
area: true,
data: Array.from({ length: 14 }, (_, i) => ({
x: today - (13 - i) * day,
y: 12 + Math.round(Math.sin(i / 2) * 4) + i / 2,
})),
},
{
id: "sessions",
name: "Sessions started",
color: "#2563EB",
data: Array.from({ length: 14 }, (_, i) => ({
x: today - (13 - i) * day,
y: 80 + i * 5 + Math.round(Math.cos(i) * 12),
})),
},
{
id: "goals",
name: "Goals completed",
color: "#1A8754",
dashed: true,
data: Array.from({ length: 14 }, (_, i) => ({
x: today - (13 - i) * day,
y: 12 + i * 2,
})),
},
];
<TimeSeriesChart
title="Last 14 days"
caption="All workspaces · UTC."
series={series}
height={260}
smooth
valueFormat={(v) => v.toLocaleString()}
/>
8.29 PayloadPreview
Formatted JSON payload preview card with a copy button and optional highlight pills. Use it to render the live request body the platform will send — HubSpot deal payload, Webhook destination, MCP tool argument shape, event-stream sample. Display-only; for an editable payload compose <CodeBlock> with a textarea.
PayloadPreview
.payload-previewA webhook payload with two highlighted top-level keys surfaced as gutter pills above the scroll area.
{
"event": "session.completed",
"contact": {
"id": "ct_01H9…",
"first_name": "Jay",
"email": "jay@magicblocks.ai"
},
"facts": {
"loan_purpose": "Purchase",
"down_payment": 50000
}
}
<div class="payload-preview">
<div class="payload-preview-head">…title + caption…</div>
<div class="payload-preview-highlight-row">
<span class="payload-preview-highlight">event</span>
<span class="payload-preview-highlight">contact</span>
</div>
<div class="payload-preview-body">
<pre class="payload-preview-pre"><code>{ …JSON… }</code></pre>
<button class="payload-preview-copy">Copy</button>
</div>
</div>
.payload-preview {
border: 1px solid var(--hair);
border-radius: var(--r-md);
background: var(--bg-paper);
overflow: hidden;
}
.payload-preview-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--s-3);
padding: var(--s-3) var(--s-4);
border-bottom: 1px solid var(--hair-soft);
}
.payload-preview-head-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.payload-preview-title { font: 500 13px/1.3 var(--f-body); color: var(--fg); }
.payload-preview-caption { font: 400 12px/1.4 var(--f-body); color: var(--fg-soft); }
.payload-preview-meta { display: inline-flex; align-items: center; gap: var(--s-2); }
.payload-preview-highlight-row {
display: flex;
flex-wrap: wrap;
gap: 4px;
padding: var(--s-2) var(--s-4);
border-bottom: 1px solid var(--hair-soft);
}
.payload-preview-highlight {
font: 500 11px/1 var(--f-mono);
padding: 4px 8px;
border-radius: 999px;
background: color-mix(in oklab, var(--accent) 12%, var(--bg-paper));
color: var(--accent);
border: 1px solid color-mix(in oklab, var(--accent) 25%, transparent);
}
.payload-preview-body {
position: relative;
overflow: auto;
}
.payload-preview-pre {
margin: 0;
padding: var(--s-3) var(--s-4);
font: 400 12.5px/1.55 var(--f-mono);
color: var(--fg);
background: var(--bg-paper);
white-space: pre;
}
.payload-preview-copy {
position: absolute;
top: 6px;
right: 6px;
appearance: none;
background: var(--bg-paper);
border: 1px solid var(--hair);
border-radius: var(--r-xs);
padding: 3px 8px;
font: 500 11.5px/1 var(--f-body);
color: var(--fg);
cursor: pointer;
transition: background var(--dur-2) var(--ease);
}
.payload-preview-copy:hover { background: var(--bg-warm); }
import { PayloadPreview } from "@magicblocksai/ui";
const payload = {
event: "session.completed",
contact: {
id: "ct_01H9…",
first_name: "Jay",
email: "jay@magicblocks.ai",
},
facts: {
loan_purpose: "Purchase",
down_payment: 50000,
},
};
<PayloadPreview
title="Webhook payload"
caption="POST → https://hooks.example.com/lead"
payload={payload}
highlight={["event", "contact"]}
maxHeight={320}
/>
8.30 Icon chip
Tinted icon-in-square primitive for feature / channel / status tiles. Six tones; consumer renders the icon inside (typical: half the chip size). v1.61.0 (app-team R2).
Six tones
.icon-chip / .icon-chip-{tone}Same 40px chip in six tones; the kit picks the soft background + matching text colour.
<span class="icon-chip icon-chip-accent" style="width:40px;height:40px">
<svg ...></svg>
</span>
/* See _shared.css — .icon-chip + six .icon-chip-{tone} variants. */
// No JS — pure markup.
import { IconChip, GlobeIcon } from "@magicblocksai/ui";
<IconChip tone="accent" size={40}>
<GlobeIcon size={20} />
</IconChip>
8.31 Rate card
Metric + tonal progress card — the “rate strip” pattern. Eyebrow + sub + tone-coloured big number + optional 4 px tinted progress bar. For count / duration metrics, reach for <KpiTile> instead. v1.61.0 (app-team R2).
4-up rate strip
.rate-card / .rate-card-tone-{tone}Four tones across a row: success / info / warning / danger.
<div class="rate-card rate-card-tone-success">
<div class="rate-card-label">Worked lead rate</div>
<div class="rate-card-sub">of 248 leads</div>
<div class="rate-card-value">73%</div>
<div class="rate-card-bar" role="progressbar" aria-valuenow="73" aria-valuemin="0" aria-valuemax="100">
<div class="rate-card-bar-fill" style="width:73%"></div>
</div>
</div>
/* See _shared.css — .rate-card + .rate-card-tone-{success|warning|danger|info|neutral}. */
// No JS — pure markup. Use the React component to bind dynamic values.
import { RateCard } from "@magicblocksai/ui";
<RateCard
label="Worked lead rate"
sub="of 248 leads"
value="73%"
tone="success"
pct={73}
/>
8.32 Source row
A resource row — tinted icon chip + name + metadata sub-line + status + trailing toggle. The canonical layout for connected sources you can see the health of and switch on/off: knowledge collections, MCP tool connections, channels. Composes <IconChip>; drop a <Switch> into trailing and a <Chip> into status. v1.65.0 (app-team R3).
SourceRow
.source-row<div class="source-row">
<span class="icon-chip icon-chip-info source-row-icon"><svg>…</svg></span>
<div class="source-row-body">
<span class="source-row-name">Product knowledge</span>
<span class="source-row-meta">42 documents · synced 4 min ago</span>
</div>
<div class="source-row-status"><span class="chip chip-green">Healthy</span></div>
<div class="source-row-trailing"><label class="switch">…</label></div>
</div>
.source-row {
display: flex;
align-items: center;
gap: var(--s-3);
padding: var(--s-3) var(--s-4);
border: 1px solid var(--hair);
border-radius: var(--r-md);
background: var(--bg-paper);
}
import { SourceRow, Chip, Switch } from "@magicblocksai/ui";
<SourceRow
icon={<BookIcon size={18} />}
tone="info"
name="Product knowledge"
meta="42 documents · synced 4 min ago · 99.4% coverage"
status={<Chip tone="green">Healthy</Chip>}
trailing={<Switch defaultChecked />}
/>