15.1 Detail page 3-column shell
The host pattern for every record-detail page in the app — deal, contact, company, ticket. Left rail is a 260px summary card (who/what is this), centre is a flexible body with sticky tabbed header, right rail is 320px of contextual cards (AI suggestions, custom fields, related records). Right rail collapses to an icon strip on demand. Below 1100px the whole thing stacks.
Three-column detail
.detail-shellDefault state — left summary · centre body with tabs · right context. Click the chevron in the right-rail header to collapse it down to icons (gives the centre column more breathing room without losing context entirely).
BlueRock Health · Renewal Q2
<div class="detail-shell">
<aside class="ds-rail"> <!-- 260px summary -->
<div class="ds-summary-card">
<span class="av">BR</span>
<div class="ds-name">BlueRock Health</div>
<div class="ds-sub">Healthcare · Enterprise</div>
</div>
<div class="ds-kv">
<div class="ds-kv-row"><div class="ds-kv-key">Owner</div><div class="ds-kv-val">Alicia Chen</div></div>
<div class="ds-kv-row"><div class="ds-kv-key">Stage</div><div class="ds-kv-val">Negotiation</div></div>
<div class="ds-kv-row"><div class="ds-kv-key">Value</div><div class="ds-kv-val">$48,000 ARR</div></div>
<div class="ds-kv-row"><div class="ds-kv-key">Close date</div><div class="ds-kv-val">May 28</div></div>
<div class="ds-kv-row"><div class="ds-kv-key">Source</div><div class="ds-kv-val">Inbound · Demo</div></div>
</div>
</aside>
<section class="ds-main"> <!-- centre, sticky header -->
<div class="ds-header">
<div class="ds-h-eyebrow">Deal</div>
<h1 class="ds-h-title">BlueRock <em>Health</em> · Renewal Q2</h1>
<div class="ds-h-meta">
<span>Health 87</span> ·
<span class="risk-badge" data-risk="low">On track</span> ·
<span>Last touched 3 days ago by Alicia</span>
</div>
<div class="ds-tabs" role="tablist">
<button class="ds-tab is-active" role="tab" aria-selected="true">Activity</button>
<button class="ds-tab" role="tab">Notes</button>
<button class="ds-tab" role="tab">Tasks</button>
<button class="ds-tab" role="tab">Files</button>
<button class="ds-tab" role="tab">Conversations</button>
</div>
</div>
<div class="ds-body">
<!-- Active tab content — e.g. activity timeline (14.3), notes editor, task list -->
</div>
</section>
<aside class="ds-rail ds-rail--right"> <!-- 320px context -->
<button class="ds-rail-toggle" type="button" aria-label="Collapse right rail">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.6"><path d="M5 3l4 4-4 4" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<div class="ds-context-card">
<div class="ds-cc-eyebrow">✦ Sage suggests</div>
<h4>Send the renewal proposal today</h4>
<p>BlueRock's renewal date is in 23 days. The pattern from past closes: send proposal > 14 days out for a 70% acceptance rate.</p>
</div>
<div class="ds-context-card">
<h4>People (4)</h4>
<p>Alicia Chen · Marcus Reid · James Park · Sara Kim</p>
</div>
</aside>
</div>
<!-- Click .ds-rail-toggle to add .is-rail-collapsed; right rail
becomes a 56px icon strip without re-flowing the centre. -->.detail-shell { display: grid;
grid-template-columns: 260px minmax(0, 1fr) 320px;
gap: var(--s-5); }
.detail-shell .ds-rail { background: var(--bg-paper);
padding: var(--s-5); border-right: 1px solid var(--hair); }
.detail-shell .ds-header { position: sticky; top: 0;
background: var(--bg); z-index: 2; }
/* Right-rail collapse */
.detail-shell.is-rail-collapsed { grid-template-columns: 260px 1fr 56px; }
.detail-shell.is-rail-collapsed .ds-rail--right
> *:not(.ds-rail-toggle) { display: none; }
@media (max-width: 1100px) {
.detail-shell { grid-template-columns: 1fr; }
}import { DetailShell } from "@magicblocksai/ui";
<DetailShell
summary={
<>
<div className="ds-summary-card">
<span className="av">BR</span>
<div className="ds-name">BlueRock Health</div>
<div className="ds-sub">Healthcare · Enterprise</div>
</div>
<div className="ds-kv">/* …rows… */</div>
</>
}
header={
<>
<div className="ds-h-eyebrow">Deal</div>
<h1 className="ds-h-title">BlueRock <em>Health</em> · Renewal Q2</h1>
<div className="ds-h-meta">Health 87 · On track</div>
</>
}
tabs={[
{ id: "activity", label: "Activity", content: <ActivityFeed /> },
{ id: "notes", label: "Notes", content: <NotesEditor /> },
{ id: "tasks", label: "Tasks", content: <TaskList /> },
{ id: "files", label: "Files", content: <FileList /> },
]}
context={
<SageSuggestions deal={deal} />
}
/>15.2 Command palette
Open with ⌘K / Ctrl+K. Centred modal, paper surface, fuzzy filter on input. Result groups: Recent · Records · Actions · Reports · KB · Settings. Each row is icon + title + sub-label + shortcut hint. ↑/↓ navigates; Enter commits; Esc closes. Recent items are pinned when the input is empty.
Default state — empty input
.cmdkEmpty input shows recent items + top actions. The first row gets .is-focused (left accent stripe + soft pink background) so Enter commits the most-likely action.
<div class="cmdk" role="dialog" aria-label="Command palette">
<div class="cmdk-input-wrap">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="11" cy="11" r="7"/><line x1="21" y1="21" x2="16.5" y2="16.5"/></svg>
<input class="cmdk-input" placeholder="Find anything...">
<span class="cmdk-kbd">esc</span>
</div>
<div class="cmdk-results" role="listbox">
<div class="cmdk-group">
<div class="cmdk-group-label">Recent</div>
<button class="cmdk-row is-focused" role="option">
<span class="cmdk-row-icon"><svg viewBox="0 0 14 14" fill="currentColor"><circle cx="7" cy="7" r="3"/></svg></span>
<span class="cmdk-row-body">
<span class="cmdk-row-title">BlueRock Health · Renewal Q2</span>
<span class="cmdk-row-sub">Deal · $48k · Negotiation</span>
</span>
<span class="cmdk-row-meta">↩</span>
</button>
...more rows...
</div>
<div class="cmdk-group">...Actions group...</div>
</div>
<div class="cmdk-foot">
<kbd>↑</kbd><kbd>↓</kbd>navigate · <kbd>↩</kbd>open · <kbd>esc</kbd>close
</div>
</div>.cmdk { background: var(--bg-paper);
border: 1px solid var(--hair);
border-radius: var(--r-lg); box-shadow: var(--sh-3);
max-width: 560px; overflow: hidden; }
.cmdk-row { display: grid;
grid-template-columns: 28px 1fr auto;
gap: var(--s-3); padding: 8px var(--s-5);
cursor: pointer; background: transparent; border: 0; }
.cmdk-row:hover, .cmdk-row.is-focused {
background: var(--accent-soft); }
.cmdk-row.is-focused { box-shadow: inset 3px 0 0 var(--accent); }
/* In real use, debounce input by 80ms; pin recent items
when input is empty; fuzzy-match on type. */import { useState } from "react";
import { CommandPalette } from "@magicblocksai/ui";
const [open, setOpen] = useState(false);
<CommandPalette
open={open}
onOpenChange={setOpen}
keyboardShortcut="meta+k"
groups={[
{
id: "recent",
label: "Recent",
items: [
{
id: "bluerock",
title: "BlueRock Health · Renewal Q2",
sub: "Deal · $48k · Negotiation",
shortcut: "↩",
onSelect: () => router.push("/deals/bluerock"),
},
],
},
{
id: "actions",
label: "Actions",
items: [
{ id: "new-deal", title: "Create deal", shortcut: "D", onSelect: openNewDeal },
{ id: "compose", title: "Compose email", shortcut: "⌘ E", onSelect: openCompose },
],
},
]}
/>15.3 Density mode
A global toggle (admin-set default + per-user override) that shrinks row heights by 16px, card paddings by 4px, small captions by 1px. Implemented via body[data-density="compact"] overriding --row-h from comfortable (52px) to compact (36px). Components reference var(--row-h) so they switch automatically. The demo below scopes the toggle to one wrapper so you can see both modes side-by-side.
Comfortable vs compact
.density-row · --row-hToggle the chip below to compare. The same row markup honours both densities — only the row height (and proportional padding scale) changes. Compact is for power users who want more rows on screen; comfortable is the default for everyone else.
data-density on the demo wrapper below; in real use, set on <body>.
<!-- Toggle UI (settings panel or per-user pref) -->
<div class="density-toggle" role="group">
<button class="is-active" data-density-set="comfortable">Comfortable</button>
<button data-density-set="compact">Compact</button>
</div>
<!-- Components reference var(--row-h); when body[data-density="compact"]
is set, --row-h flips from --row-h-comfortable to --row-h-compact -->
<div class="density-row">
<span class="av">AC</span>
<div>
<strong>Alicia Chen</strong><br>
Last touched 2h ago
</div>
<span class="density-row-meta">2h</span>
</div>/* In components/_shared.css: */
:root {
--row-h-comfortable: 52px;
--row-h-compact: 36px;
--row-h: var(--row-h-comfortable);
}
body[data-density="compact"] {
--row-h: var(--row-h-compact);
}
/* In any component CSS: */
.density-row { height: var(--row-h, var(--row-h-comfortable)); }
/* JS to drive the toggle: */
document.querySelectorAll('[data-density-set]').forEach(b => {
b.addEventListener('click', () => {
document.body.dataset.density = b.dataset.densitySet;
localStorage.setItem('mb-density', b.dataset.densitySet);
});
});import { DensityProvider, DensityToggle, useDensity } from "@magicblocksai/ui";
// Wrap your app (or a sub-tree) in the provider — it sets data-density
// on its root, which flips --row-h for every CSS rule that consumes it.
<DensityProvider defaultDensity="comfortable">
<DensityToggle />
<Inbox />
</DensityProvider>
// Or read / write density from anywhere inside the provider:
function DensityChip() {
const { density, setDensity } = useDensity();
return (
<button onClick={() => setDensity(density === "compact" ? "comfortable" : "compact")}>
Density: {density}
</button>
);
}15.4 Empty states
Five canonical CRM-flavoured empty states with on-brand line-drawn illustrations. Every absent surface gets one of these — never leave a list, table, or feed blank. Anatomy: small illustration · headline · 1-line lede · primary CTA + secondary “Import” link.
No contacts · No deals · No tickets · No KB · No partners
.empty-cardEach lede is short and warm. The CTA is the single most-likely action; the secondary link is the fallback (usually “Import”). Illustrations are pure SVG — ink + accent only, no gradients.
Bring some people in.
Your contacts list is empty. Import from a CSV or add your first contact directly.
Your pipeline is hungry.
No deals yet. Add one manually or pull them in from a connected source.
Nice — no fires today.
The ticket queue is empty. Customers are probably happy, or your team is on top of it.
Teach Sage what you know.
No knowledge base articles yet. Sage gets sharper with every doc you add.
Find a few good partners.
No partners in your network yet. Invite an agency, integrator, or reseller to get started.
<div class="empty-card">
<div class="empty-illo" aria-hidden="true">
<svg viewBox="0 0 96 96" fill="none">
<circle cx="36" cy="40" r="14" stroke="var(--ink)" stroke-width="2"/>
<path d="M14 78c0-12 10-22 22-22s22 10 22 22" stroke="var(--ink)" stroke-width="2" stroke-linecap="round"/>
<circle cx="68" cy="34" r="10" stroke="var(--accent)" stroke-width="2"/>
<path d="M52 70c0-9 7-16 16-16s16 7 16 16" stroke="var(--accent)" stroke-width="2" stroke-linecap="round" opacity="0.8"/>
<circle cx="80" cy="20" r="3" fill="var(--accent)"/>
</svg>
</div>
<h3>Bring some <em>people</em> in.</h3>
<p>Your contacts list is empty. Import from a CSV
or add your first contact directly.</p>
<div class="empty-actions">
<button class="empty-cta">+ New contact</button>
<a class="empty-link" href="#">Import CSV</a>
</div>
</div>.empty-card { background: var(--bg-paper);
border: 1px solid var(--hair); border-radius: var(--r-lg);
padding: var(--s-7) var(--s-5); text-align: center;
display: flex; flex-direction: column; align-items: center; gap: var(--s-3); }
.empty-card .empty-illo { width: 96px; height: 96px; }
.empty-card h3 { font: 700 18px var(--f-display); }
.empty-card h3 em { font-family: var(--f-serif); font-style: italic;
color: var(--accent-text); font-variation-settings: "SOFT" 80; }
.empty-card .empty-cta { background: var(--accent); color: var(--paper);
padding: 10px 18px; border-radius: var(--r-pill);
box-shadow: var(--sh-pink); }import { EmptyAppCard } from "@magicblocksai/ui";
<EmptyAppCard
tone="no-contacts"
title={<>Bring some <em>people</em> in.</>}
lede="Your contacts list is empty. Import from a CSV or add your first contact directly."
primaryAction={<button className="empty-cta">+ New contact</button>}
secondaryAction={<a className="empty-link" href="/import">Import CSV</a>}
/>
// Five built-in tones: no-contacts · no-deals · no-tickets · no-kb · no-partners.
// Each ships with a line-drawn ink+accent illustration. Pass `illustration` to
// override with your own SVG for vertical-specific empty states.
<EmptyAppCard
tone="no-tickets"
title={<>Nice — no <em>fires</em> today.</>}
lede="The ticket queue is empty. Customers are probably happy."
primaryAction={<button className="empty-cta">Create ticket</button>}
/>15.5 AppShell
Full-viewport product-app chrome — sidebar + main column with an optional sticky topbar. Sibling to <DashboardShell> but built for whole-app surfaces (100vh, no border, no border-radius) rather than embedded marketing demos. The sidebar is <aside role="navigation">, the main column is <main role="main">, and the topbar is position: sticky; top: 0 inside main. Mobile (≤ 960px) hides the sidebar in-grid and opens it as a slide-in drawer when data-mobile-nav="open" is set on the root.
Sidebar + topbar + main
.app-shellA resting AppShell with a sample nav rail, a thin topbar (search slot + quick-add slot), and a main-column body. The demo frame is capped at 360px tall so the chrome reads inside the chapter stage; in shipping use the root is 100vh.
Main column scrolls independently. Use this slot for the route’s <PageHeader> and content cards.
<div class="app-shell">
<aside class="app-shell-side" role="navigation">
<p class="dash-nav-label mono">Workspace</p>
<a href="#" class="dash-nav-item is-active">
<span class="dash-nav-item-label">Overview</span>
</a>
<a href="#" class="dash-nav-item">
<span class="dash-nav-item-label">Contacts</span>
<span class="badge">128</span>
</a>
<!-- …more nav items… -->
</aside>
<main class="app-shell-main" role="main">
<div class="app-shell-topbar">
<input type="text" placeholder="Search…">
<button class="btn btn-primary">Quick add</button>
</div>
<div class="app-shell-main-body">
Main column scrolls independently.
</div>
</main>
</div>
.app-shell {
display: grid;
grid-template-columns: var(--app-shell-side, 248px) 1fr;
height: 100vh;
width: 100%;
background: var(--bg-paper);
color: var(--fg);
overflow: hidden;
/* v1.8.0 (R5-4): expose layout dimensions as CSS custom properties so
consumers can write fit-to-viewport calcs without magic numbers.
`--app-topbar-h` defaults to 56px (matches the ~56px the kit's own
topbar slot renders at with default padding). Override on .app-shell
when your topbar is a different height — `<AppShell style={{ '--app-topbar-h': '64px' }}>`.
`--app-shell-side-w` is an alias of `--app-shell-side` for clarity
and consistency with `--app-topbar-h`. */
--app-topbar-h: 56px;
--app-shell-side-w: var(--app-shell-side, 248px);
}
.app-shell-side {
/* Structural — applies in every theme. */
border-right: 1px solid var(--hair);
padding: var(--s-4);
overflow-y: auto;
overflow-x: hidden;
/* v1.8.0 (R5-2): always paint a paper background so a repositioned
sidebar (slide-in panel, drawer, overlay) doesn't render transparent
in dark mode. The light-mode warm-cream override below takes
precedence in light theme. Without this, dark-mode sidebars only
painted via the surrounding .app-shell's bg — fine in-grid (paper
on paper) but invisible when the consumer reparents the sidebar to
`position: fixed` for mobile. */
background: var(--bg-paper);
}
body:not([data-theme="dark"]) .app-shell-side {
background: var(--warm-3);
--fg: var(--ink);
--fg-soft: color-mix(in oklab, var(--ink) 68%, transparent);
--fg-dim: color-mix(in oklab, var(--ink) 48%, transparent);
--hair: rgba(25, 30, 50, 0.09);
color: var(--fg);
}
.app-shell-main {
display: flex;
flex-direction: column;
overflow-y: auto;
overflow-x: hidden;
/* v3.0.0 (warm ladder): the light default is `--bg-canvas` (--warm-1) —
the recessed warm canvas that cards float on is the standard now, not
an opt-in. `pageWash` is kept for API stability (resolves to the same
--bg-canvas — a no-op in light). Dark mode is preserved: default stays
`--bg-paper` via a dark override, and pageWash flips it to `--ink`. */
background: var(--app-page-bg, var(--bg-canvas));
}
:is(html, body)[data-theme="dark"] .app-shell-main {
background: var(--app-page-bg, var(--bg-paper));
}
/* …additional rules trimmed for brevity — see _shared.css */
import { AppShell, Button, Input } from "@magicblocksai/ui";
<AppShell
sidebar={
<>
<p className="dash-nav-label mono">Workspace</p>
<a href="/overview" className="dash-nav-item is-active">
<span className="dash-nav-item-label">Overview</span>
</a>
<a href="/contacts" className="dash-nav-item">
<span className="dash-nav-item-label">Contacts</span>
<span className="badge">128</span>
</a>
</>
}
topbar={
<div style={{ display: "flex", alignItems: "center", gap: 12, padding: "10px 16px" }}>
<Input placeholder="Search…" />
<Button variant="primary">Quick add</Button>
</div>
}
>
<div style={{ padding: "var(--s-6)" }}>
{/* page content */}
</div>
</AppShell>
Collapsed icon-band — sidebarMode
.app-shell[data-sidebar-mode] · .ws-nav-iconThe same shell, run as a collapsible icon-band. Pass sidebarMode="collapsed" for the 56px icon rail (hover an icon for its slide-out label) or "expanded" for the 220px labelled rail. The sidebar composes the kit’s <Logo> mark, <WorkspaceNavIcon> items, and the workspace-switcher / user-menu chrome. The collapse toggle is consumer-owned — click the chevron to flip it. This is the canonical shell the agent builder (18.7) now uses.
Collapsed icon rail on the left. Click the chevron beside the logo to expand into the labelled rail; the column animates between 56px and 220px.
<div class="app-shell" data-sidebar-mode="collapsed">
<aside class="app-shell-side" role="navigation">
<div class="app-shell-side-head">
<span class="logo logo-md"><svg class="logomark" …>…</svg><span class="logo-wordmark">magicblocks</span></span>
<button class="app-shell-side-toggle" data-sidebar-toggle aria-label="Toggle sidebar">
<svg viewBox="0 0 14 14" …><path d="M5 3 L9 7 L5 11"/></svg>
</button>
</div>
<nav class="ws-nav">
<a href="/app" class="ws-nav-icon is-active" data-tooltip="Dashboard" aria-label="Dashboard" aria-current="page">
<svg …>…</svg><span class="ws-nav-icon-label">Dashboard</span>
</a>
<a href="/sessions" class="ws-nav-icon" data-tooltip="Sessions" aria-label="Sessions">
<svg …>…</svg><span class="ws-nav-icon-label">Sessions</span><span class="ws-nav-icon-count">412</span>
</a>
<!-- …more items… -->
</nav>
<span class="app-shell-side-spacer"></span>
<span class="app-shell-side-divider"></span>
<a href="/settings" class="ws-nav-icon" data-tooltip="Settings" aria-label="Settings">…</a>
<button class="ws-user"><span class="ws-user-avatar">JS</span>
<span class="ws-user-meta"><span class="ws-user-name">Jay Stockwell</span><span class="ws-user-sub">Owner</span></span>
</button>
</aside>
<main class="app-shell-main" role="main">…</main>
</div>
<!-- Toggle: flip data-sidebar-mode between "collapsed"/"expanded".
The kit's _shared.js wires any [data-sidebar-toggle] button. -->
/* Opt-in icon-band — keys off [data-sidebar-mode] (see _shared.css). */
.app-shell[data-sidebar-mode="collapsed"] { --app-shell-side: 56px; }
.app-shell[data-sidebar-mode="expanded"] { --app-shell-side: 220px; }
.ws-nav-icon { /* 36×36 button; icon-only when collapsed */
position: relative; width: 36px; height: 36px; border-radius: 8px;
display: inline-flex; align-items: center; justify-content: center;
}
.app-shell[data-sidebar-mode="expanded"] .ws-nav-icon {
width: 100%; justify-content: flex-start; padding: 0 var(--s-3);
}
.ws-nav-icon.is-active { background: var(--accent-soft); color: var(--accent); }
.ws-nav-icon.is-active::before { /* leading accent stripe */
content: ""; position: absolute; left: -2px; top: 8px; bottom: 8px;
width: 3px; border-radius: 0 2px 2px 0; background: var(--accent);
}
.ws-nav-icon-label { display: none; } /* hidden when collapsed */
.app-shell[data-sidebar-mode="expanded"] .ws-nav-icon-label { display: inline; }
/* …slide-out tooltip + chrome — full rules in _shared.css */
import { useState } from "react";
import { AppShell, WorkspaceNavIcon, Logo } from "@magicblocksai/ui";
function Shell() {
const [collapsed, setCollapsed] = useState(true);
return (
<AppShell
sidebarMode={collapsed ? "collapsed" : "expanded"}
sidebarWidthCollapsed="56px"
sidebar={
<>
<div className="app-shell-side-head">
<Logo />
<button type="button" className="app-shell-side-toggle"
aria-label="Toggle sidebar" onClick={() => setCollapsed((c) => !c)}>
<svg viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="1.4"
strokeLinecap="round" strokeLinejoin="round"><path d="M5 3 L9 7 L5 11" /></svg>
</button>
</div>
<nav className="ws-nav">
<WorkspaceNavIcon icon={<DashboardIcon />} label="Dashboard" href="/app" active />
<WorkspaceNavIcon icon={<SessionsIcon />} label="Sessions" count={412} href="/sessions" />
<WorkspaceNavIcon icon={<ContactsIcon />} label="Contacts" count="1,284" href="/contacts" />
</nav>
<span className="app-shell-side-spacer" />
<span className="app-shell-side-divider" aria-hidden="true" />
<WorkspaceNavIcon icon={<SettingsIcon />} label="Settings" href="/settings" />
<button type="button" className="ws-user">
<span className="ws-user-avatar">JS</span>
<span className="ws-user-meta">
<span className="ws-user-name">Jay Stockwell</span>
<span className="ws-user-sub">Owner</span>
</span>
</button>
</>
}
>
{/* page content */}
</AppShell>
);
}
15.6 PageHeader
The canonical eyebrow · title · summary · actions rhythm at the top of every product-app route. Bakes the type scale, gap, mobile wrap behaviour, and the icon-in-h1 pattern so the rhythm stays consistent across routes without hand-rolled CSS in every page. At ≤ 640px the title font drops from 28px to 22px and the actions container wraps to its own row.
PageHeader
.page-headerA resting page header for the Deals route. The icon slot sits inline with the title; the summary line sits below; a primary action sits right-aligned (and wraps below the title on mobile).
Deals
42 open · $1.2M weighted
<header class="page-header">
<div class="page-header-text">
<span class="page-header-eyebrow mono">Sales</span>
<h1 class="page-header-title">
<span class="page-header-title-icon" aria-hidden="true">
<svg>…</svg>
</span>
<span class="page-header-title-text">Deals</span>
</h1>
<p class="page-header-summary">42 open · $1.2M weighted</p>
</div>
<div class="page-header-actions">
<button class="btn btn-primary">+ New deal</button>
</div>
</header>
.page-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: var(--s-5);
flex-wrap: wrap;
margin-bottom: var(--s-6);
padding-bottom: var(--s-5);
border-bottom: 1px solid var(--hair-soft);
}
.page-header-text {
display: flex;
flex-direction: column;
gap: var(--s-2);
min-width: 0;
flex: 1;
}
.page-header-eyebrow {
font: 500 11.5px/1 var(--f-mono);
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--accent-text);
}
.page-header-title {
display: inline-flex;
align-items: center;
gap: var(--s-3);
margin: 0;
font: 600 28px/1.2 var(--f-display);
letter-spacing: -0.015em;
color: var(--fg);
}
.page-header-title-icon {
display: inline-flex;
align-items: center;
color: var(--accent-text);
flex-shrink: 0;
}
.page-header-title-text { min-width: 0; }
.page-header-summary {
margin: 0;
font: 400 14.5px/1.5 var(--f-body);
color: var(--fg-soft);
max-width: 64ch;
}
.page-header-actions {
display: inline-flex;
align-items: center;
gap: var(--s-2);
flex-shrink: 0;
}
@media (max-width: 640px) {
.page-header-title { font-size: 22px; }
/* v1.14.0 (R18-8): summary line wrap + actions stacking refinements.
Pre-1.14.0 the summary stayed inline with the title and actions
wrapped to a single full-basis line. At narrow widths with multiple
action buttons that single line could still overflow. This rule
gives summary a proper line-break beneath the title and lets
each action take the full width when there are multiple. */
.page-header-text { flex-basis: 100%; }
.page-header-summary { max-width: 100%; }
.page-header-actions {
flex-basis: 100%;
width: 100%;
flex-wrap: wrap;
}
.page-header-actions > * { flex: 1 1 auto; }
}
.page-header-title-text .inline-headline,
.page-header-title-text .inline-headline-display {
width: auto;
max-width: 100%;
}
.page-header-title-text .inline-headline-text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
word-break: normal;
}
import { PageHeader, Button, BriefcaseIcon } from "@magicblocksai/ui";
<PageHeader
eyebrow="Sales"
title="Deals"
icon={<BriefcaseIcon size={26} />}
summary="42 open · $1.2M weighted"
actions={<Button variant="primary">+ New deal</Button>}
/>
// Click-to-edit title (v1.23.0) — pass `onTitleSave` to swap the
// static h1 for an InlineHeadline editor that commits on Enter/blur.
<PageHeader
eyebrow="Tickets"
title={ticket.subject}
onTitleSave={async (next) => { await api.patch(`/tickets/${ticket.id}`, { subject: next }); }}
actions={<Button variant="primary">Resolve</Button>}
/>
15.7 SettingsShell
Two-column settings shell — 200px nav rail + fluid content pane. Rows are dividing (not carded) so the controls do the work. The shell wraps its body in a <form> that auto-flips dirty=true on any input change inside; a sticky save bar slides in at the bottom and fires onSave / onCancel. Pass dirty + onDirtyChange for controlled-state form libraries.
Two-column settings
.settingsA resting Profile screen — two nav groups on the left, three rows on the right. The save bar is hidden in this resting state (no edits made yet); flip .settings-save on to preview the dirty-row pattern.
<div class="settings">
<aside class="settings-nav">
<div class="settings-group">
<div class="settings-group-label">Account</div>
<a class="settings-nav-link is-active">Profile</a>
<a class="settings-nav-link">Notifications</a>
</div>
</aside>
<form class="settings-pane">
<header class="settings-head">
<h2 class="settings-title">Profile</h2>
<p class="settings-lede">This is how the team sees you.</p>
</header>
<div class="settings-row">
<div class="settings-row-meta">
<div class="settings-row-label">Display name</div>
</div>
<div class="settings-row-control">
<input class="input" value="Priya Raman">
</div>
</div>
<!-- When dirty: -->
<div class="settings-save">
<button class="btn btn-ghost">Cancel</button>
<button class="btn btn-primary">Save changes</button>
</div>
</form>
</div>
.settings { display: grid; grid-template-columns: 200px 1fr; gap: var(--s-6); max-width: min(1080px, 100%); margin: 0 auto; width: 100%; }
@media (max-width: 720px) { .settings { grid-template-columns: 1fr; } }
.settings-nav { display: flex; flex-direction: column; gap: var(--s-4); }
.settings-group { display: flex; flex-direction: column; gap: 2px; }
.settings-group-label { font: 500 11px/1 var(--f-mono); text-transform: uppercase; letter-spacing: 0.08em; color: var(--fg-dim); margin-bottom: var(--s-2); }
.settings-nav-link { padding: 8px 12px; border-radius: var(--r-xs); font: 500 14px/1 var(--f-body); color: var(--fg-soft); text-decoration: none; cursor: pointer; }
.settings-nav-link:hover { background: var(--bg-sunk); color: var(--fg); }
.settings-nav-link.is-active { background: var(--accent-soft); color: var(--accent-text); }
.settings-pane { min-width: 0; }
.settings-head { margin-bottom: var(--s-5); padding-bottom: var(--s-4); border-bottom: 1px solid var(--hair); }
.settings-title { font: 700 24px/1.2 var(--f-display); color: var(--fg); margin: 0; }
.settings-lede { font: 400 14px/1.55 var(--f-body); color: var(--fg-soft); margin: var(--s-1) 0 0; }
.settings-row { display: grid; grid-template-columns: 1fr auto; gap: var(--s-5); align-items: center; padding: var(--s-4) 0; border-bottom: 1px solid var(--hair); }
.settings-row-meta { min-width: 0; }
.settings-row-label { font: 600 14px/1.3 var(--f-body); color: var(--fg); }
.settings-row-desc { font: 400 13px/1.5 var(--f-body); color: var(--fg-soft); margin-top: 2px; }
.settings-switch { width: 40px; height: 22px; border-radius: 999px; background: var(--hair); border: 0; position: relative; cursor: pointer; padding: 0; }
.settings-switch.is-on { background: var(--accent); }
.settings-switch-knob { position: absolute; top: 2px; left: 2px; width: 18px; height: 18px; border-radius: 50%; background: var(--paper); box-shadow: var(--sh-1); transition: transform var(--dur-2) var(--ease); }
.settings-switch.is-on .settings-switch-knob { transform: translateX(18px); }
@media (prefers-reduced-motion: reduce) {
.settings-switch-knob { transition: none; }
}
/* …additional rules trimmed for brevity — see _shared.css */
import { SettingsShell, Input, Button } from "@magicblocksai/ui";
<SettingsShell
nav={
<>
<div className="settings-group">
<div className="settings-group-label">Account</div>
<a className="settings-nav-link is-active">Profile</a>
<a className="settings-nav-link">Notifications</a>
<a className="settings-nav-link">Billing</a>
</div>
</>
}
onSave={async () => { await save(form); }}
onCancel={() => form.reset()}
>
<header className="settings-head">
<h2 className="settings-title">Profile</h2>
<p className="settings-lede">This is how the team sees you.</p>
</header>
<div className="settings-row">
<div className="settings-row-meta">
<div className="settings-row-label">Display name</div>
</div>
<div className="settings-row-control">
<Input defaultValue="Priya Raman" />
</div>
</div>
</SettingsShell>
15.8 SavedViewsRail
Named query views rail — labeled rows with optional counts, icons, group headers, and starring. Backs the Sessions left-rail saved views (All / Favorites / Goal Conversions / Principles Fixed / Missing Knowledge / Negative Sentiment) and the Dashboard Latest-Sessions saved-view tabs. Presentation-only: the consumer wires value + onValueChange to a query / router.
SavedViewsRail
.saved-views-railA rail with a title row + meta slot, two grouped sections (Saved / Built-in), and an active “All sessions” row. The active item carries aria-current="page"; counts render in a soft trailing pill.
<div class="saved-views-rail" role="navigation">
<div class="saved-views-rail-head">
<div class="saved-views-rail-title">Sessions</div>
</div>
<div class="saved-views-rail-group">
<div class="saved-views-rail-item is-on">
<button class="saved-views-rail-button" aria-current="page">
<span class="saved-views-rail-label">All sessions</span>
<span class="saved-views-rail-count">158</span>
</button>
</div>
</div>
<div class="saved-views-rail-group">
<div class="saved-views-rail-group-name">Built-in</div>
<div class="saved-views-rail-item">
<button class="saved-views-rail-button">
<span class="saved-views-rail-label">Goal conversions</span>
<span class="saved-views-rail-count">22</span>
</button>
</div>
</div>
</div>
.saved-views-rail { display: flex; flex-direction: column; gap: var(--s-2); }
.saved-views-rail-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--s-2);
padding: 0 var(--s-2);
}
.saved-views-rail-title {
font: 600 10.5px/1 var(--f-mono);
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--fg-faint);
}
.saved-views-rail-meta { display: inline-flex; align-items: center; gap: var(--s-1); }
.saved-views-rail-group { display: flex; flex-direction: column; gap: 2px; }
.saved-views-rail-group + .saved-views-rail-group { margin-top: var(--s-2); }
.saved-views-rail-group-name {
font: 600 10.5px/1 var(--f-mono);
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--fg-faint);
padding: var(--s-1) var(--s-2);
}
.saved-views-rail-item {
display: grid;
grid-template-columns: 1fr auto;
align-items: center;
border-radius: var(--r-sm);
}
.saved-views-rail-item.is-on { background: var(--bg-warm); }
.saved-views-rail-item.is-disabled { opacity: 0.55; }
.saved-views-rail-button {
appearance: none;
background: transparent;
border: 0;
text-align: left;
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
gap: var(--s-2);
padding: 6px 10px;
color: var(--fg);
font: 400 13px/1.3 var(--f-body);
cursor: pointer;
border-radius: var(--r-sm);
width: 100%;
}
.saved-views-rail-button:hover:not(:disabled) { background: var(--bg-warm); }
.saved-views-rail.is-compact .saved-views-rail-button { padding: 4px 8px; font-size: 12.5px; }
.saved-views-rail-item.is-on .saved-views-rail-button { color: var(--fg); font-weight: 500; }
.saved-views-rail-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
color: var(--fg-soft);
}
.saved-views-rail-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.saved-views-rail-count {
font: 500 11.5px/1 var(--f-mono);
color: var(--fg-faint);
font-variant-numeric: tabular-nums;
padding-left: 4px;
}
.saved-views-rail-star {
appearance: none;
background: transparent;
border: 0;
width: 28px; height: 28px;
display: inline-flex;
align-items: center;
justify-content: center;
color: var(--fg-faint);
cursor: pointer;
border-radius: var(--r-xs);
}
/* …additional rules trimmed for brevity — see _shared.css */
import { useState } from "react";
import { SavedViewsRail, Button } from "@magicblocksai/ui";
import type { SavedView } from "@magicblocksai/ui";
const views: SavedView[] = [
{ id: "all", label: "All sessions", count: 158 },
{ id: "favorites", label: "Favorites", count: 4, group: "Saved" },
{ id: "goals", label: "Goal conversions", count: 22, group: "Built-in" },
{ id: "negative", label: "Negative sentiment", count: 9, group: "Built-in" },
];
function Example() {
const [view, setView] = useState<string | null>("all");
return (
<SavedViewsRail
title="Sessions"
meta={<Button variant="ghost">+ New</Button>}
views={views}
value={view}
onValueChange={setView}
/>
);
}
15.9 HelpBubble
Floating help / chat launcher — the global bottom-right “Magic on Magic” bubble that appears on every page of the Next Gen app. The trigger is a circular 48px button with an optional unread-count badge; the panel opens on click and dismisses on Escape + outside-click. Open state is fully local; the consumer controls the panel contents via title / caption / children / footer slots.
HelpBubble
.help-bubbleThe bubble in its open state — a title / caption header, a body slot, and a footer slot (typically a Send-message form). In shipping use the wrapper is position: fixed; the chapter demo scopes it to a 360px-tall frame so both the trigger and the open panel render inside the stage.
Hi Jay — what can I help with today?
<div class="help-bubble is-anchor-bottom-right is-open">
<button class="help-bubble-trigger" aria-haspopup="dialog" aria-expanded="true">
<svg>…</svg>
<span class="help-bubble-badge">2</span>
</button>
<div class="help-bubble-panel" role="dialog">
<div class="help-bubble-head">
<div class="help-bubble-title">Magic on Magic</div>
<div class="help-bubble-caption">Ask anything about your agent setup.</div>
</div>
<div class="help-bubble-body">
<p>Hi Jay — what can I help with today?</p>
</div>
<div class="help-bubble-footer">
<input class="input" placeholder="Ask a question…">
<button class="btn btn-primary">Send</button>
</div>
</div>
</div>
.help-bubble { position: fixed; z-index: 60; }
.help-bubble.is-anchor-bottom-right { right: 24px; bottom: 24px; }
.help-bubble.is-anchor-bottom-left { left: 24px; bottom: 24px; }
.help-bubble.is-anchor-top-right { right: 24px; top: 24px; }
.help-bubble.is-anchor-top-left { left: 24px; top: 24px; }
.help-bubble-trigger {
appearance: none;
width: 48px;
height: 48px;
border-radius: 999px;
background: var(--accent);
color: var(--paper);
border: 0;
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
position: relative;
box-shadow: 0 6px 16px color-mix(in oklab, var(--ink) 22%, transparent);
transition: transform var(--dur-2) var(--ease),
box-shadow var(--dur-2) var(--ease);
}
.help-bubble-trigger:hover { transform: translateY(-1px); }
@media (prefers-reduced-motion: reduce) {
.help-bubble-trigger { transition: none; }
.help-bubble-trigger:hover { transform: none; }
}
.help-bubble-badge {
position: absolute;
top: -2px;
right: -2px;
min-width: 18px;
height: 18px;
padding: 0 5px;
border-radius: 999px;
background: var(--ink);
color: var(--paper);
font: 600 10.5px/1 var(--f-mono);
display: inline-flex;
align-items: center;
justify-content: center;
border: 2px solid var(--accent);
}
.help-bubble-panel {
position: absolute;
width: 360px;
max-height: 520px;
background: var(--bg-paper);
border: 1px solid var(--hair);
border-radius: var(--r-lg);
box-shadow: 0 20px 48px color-mix(in oklab, var(--ink) 18%, transparent);
display: flex;
flex-direction: column;
overflow: hidden;
}
.help-bubble.is-anchor-bottom-right .help-bubble-panel { bottom: 64px; right: 0; }
.help-bubble.is-anchor-bottom-left .help-bubble-panel { bottom: 64px; left: 0; }
.help-bubble.is-anchor-top-right .help-bubble-panel { top: 64px; right: 0; }
.help-bubble.is-anchor-top-left .help-bubble-panel { top: 64px; left: 0; }
.help-bubble-head {
padding: var(--s-4);
border-bottom: 1px solid var(--hair-soft);
}
.help-bubble-title { font: 600 15px/1.3 var(--f-display); color: var(--fg); }
.help-bubble-caption { font: 400 12.5px/1.4 var(--f-body); color: var(--fg-soft); margin-top: 2px; }
.help-bubble-body {
padding: var(--s-4);
overflow-y: auto;
flex: 1;
min-height: 0;
}
/* …additional rules trimmed for brevity — see _shared.css */
import { HelpBubble, Input, Button } from "@magicblocksai/ui";
<HelpBubble
count={2}
title="Magic on Magic"
caption="Ask anything about your agent setup."
footer={
<form onSubmit={(e) => e.preventDefault()} style={{ display: "flex", gap: 8 }}>
<Input placeholder="Ask a question…" />
<Button type="submit">Send</Button>
</form>
}
>
<p>Hi Jay 👋 — what can I help with today?</p>
</HelpBubble>
15.11 Settings header block
The per-page header for settings pages. Eyebrow + title + description + actions row. Layers atop <PageHeader> thematically but with a settings-specific shape (eyebrow above the title, smaller title size, description on a 60ch line). Stateless and slot-driven. Below 480px the actions row stacks beneath the text and stretches to full width.
API keys page header
.settings-header-blockThe settings-page header shape: eyebrow (parent group), title, description (60ch max), and a right-aligned actions row. Drop into the body slot of <SettingsShell> above the page content.
Account
API keys
Tokens for programmatic access to your workspace. Revealed once at creation — copy and store securely.
<header class="settings-header-block">
<div class="settings-header-block-text">
<p class="settings-header-block-eyebrow">Account</p>
<h1 class="settings-header-block-title">API keys</h1>
<p class="settings-header-block-desc">Tokens for programmatic access to your workspace. Revealed once at creation — copy and store securely.</p>
</div>
<div class="settings-header-block-actions">
<button type="button" class="btn">New key</button>
</div>
</header>
.settings-header-block {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: var(--s-5);
padding-bottom: var(--s-5);
border-bottom: 1px solid var(--hair);
margin-bottom: var(--s-6);
}
.settings-header-block-text {
display: flex;
flex-direction: column;
gap: var(--s-2);
min-width: 0;
}
.settings-header-block-eyebrow {
font: 500 11px/1 var(--f-mono);
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--fg-faint);
margin: 0;
}
.settings-header-block-title {
font: 600 22px/1.2 var(--f-display);
letter-spacing: -0.01em;
color: var(--fg);
margin: 0;
}
.settings-header-block-desc {
font: 400 14px/1.5 var(--f-body);
color: var(--fg-soft);
margin: 0;
max-width: 60ch;
}
.settings-header-block-actions {
display: flex;
gap: var(--s-2);
flex-shrink: 0;
}
@media (max-width: 480px) {
.settings-header-block {
flex-direction: column;
gap: var(--s-4);
}
.settings-header-block-title { font-size: 20px; }
.settings-header-block-actions { width: 100%; }
}
import { SettingsHeaderBlock } from "@magicblocksai/ui";
<SettingsHeaderBlock
eyebrow="Account"
title="API keys"
description="Tokens for programmatic access to your workspace. Revealed once at creation — copy and store securely."
actions={<button type="button" className="btn">New key</button>}
/>
15.12 Preference toggle row
A labelled row with a switch on the right. Used 5–20× per settings page for boolean preferences. Composes the kit’s <Switch> primitive (chapter 03) — label and optional description on the left, switch pinned to the right. The label is bound to the switch’s underlying <input> via for/id so clicking the label focuses and toggles it. .is-disabled dims the row and disables the switch.
Notification preferences
.preference-toggle-rowThree rows demonstrating the canonical states: on (email digest, defaultChecked), off (push notifications), and disabled (SMS alerts, gated to a higher plan). Each row sits on a hairline bottom border; the last row in a stack drops the border.
Daily summary of new activity in your workspace.
Real-time alerts on your devices.
Available on the Pro plan and above.
<div class="preference-toggle-row">
<div class="preference-toggle-row-text">
<label for="email-digest" class="preference-toggle-row-label">Email digest</label>
<p class="preference-toggle-row-desc">Daily summary of new activity in your workspace.</p>
</div>
<label class="switch">
<input type="checkbox" id="email-digest" checked>
<span class="switch-track" aria-hidden="true"></span>
</label>
</div>
<div class="preference-toggle-row is-disabled">
<div class="preference-toggle-row-text">
<label for="sms-alerts" class="preference-toggle-row-label">SMS alerts</label>
<p class="preference-toggle-row-desc">Available on the Pro plan and above.</p>
</div>
<label class="switch">
<input type="checkbox" id="sms-alerts" disabled>
<span class="switch-track" aria-hidden="true"></span>
</label>
</div>
.preference-toggle-row {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: var(--s-5);
padding: var(--s-4) 0;
border-bottom: 1px solid var(--hair);
}
.preference-toggle-row:last-child { border-bottom: 0; }
.preference-toggle-row.is-disabled { opacity: 0.5; pointer-events: none; }
.preference-toggle-row-text {
display: flex;
flex-direction: column;
gap: var(--s-1);
min-width: 0;
}
.preference-toggle-row-label {
font: 500 14px/1.4 var(--f-body);
color: var(--fg);
cursor: pointer;
}
.preference-toggle-row-desc {
font: 400 13px/1.5 var(--f-body);
color: var(--fg-soft);
margin: 0;
max-width: 60ch;
}
@media (max-width: 480px) {
.preference-toggle-row {
gap: var(--s-3);
}
}
import { PreferenceToggleRow } from "@magicblocksai/ui";
<PreferenceToggleRow
label="Email digest"
description="Daily summary of new activity in your workspace."
defaultChecked
/>
<PreferenceToggleRow
label="Push notifications"
description="Real-time alerts on your devices."
/>
<PreferenceToggleRow
label="SMS alerts"
description="Available on the Pro plan and above."
disabled
/>
15.13 API key card
A card displaying an API key with a masked-by-default token, reveal + copy + revoke actions, and last-used / created metadata. The token sits in a sunken row with a monospaced code element; action buttons (Reveal, Copy) pin to the right and stack below the token on narrow viewports. Used inside the API keys settings page; pairs with <SettingsHeaderBlock> above and a list of these cards below.
Production key
.api-key-cardMasked-by-default state — the token shows a 4-char prefix + bullets + 4-char suffix until Reveal is clicked. Copy writes the raw token to the system clipboard and briefly flips its label to Copied for screen readers (aria-live="polite"). The footer Revoke button picks up the danger tint (var(--error-text)) and a soft red hover background.
mb_l••••••••••••AB12
<div class="api-key-card">
<div class="api-key-card-head">
<div class="api-key-card-name">Production API key</div>
<div class="api-key-card-meta">
<span>Last used <strong>2 hours ago</strong></span>
<span>Created <strong>14 Mar 2026</strong></span>
</div>
</div>
<div class="api-key-card-token">
<code class="api-key-card-token-value">mb_l••••••••••••AB12</code>
<div class="api-key-card-token-actions">
<button type="button" class="btn btn-ghost btn-sm" aria-pressed="false">Reveal</button>
<button type="button" class="btn btn-ghost btn-sm" aria-live="polite">Copy</button>
</div>
</div>
<div class="api-key-card-foot">
<button type="button" class="btn btn-ghost btn-sm api-key-card-revoke">Revoke</button>
</div>
</div>
.api-key-card {
background: var(--bg-paper);
border: 1px solid var(--hair);
border-radius: var(--r-md);
padding: var(--s-4) var(--s-5);
display: flex;
flex-direction: column;
gap: var(--s-3);
}
.api-key-card-head {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: var(--s-4);
flex-wrap: wrap;
}
.api-key-card-name {
font: 600 14px/1.3 var(--f-body);
color: var(--fg);
}
.api-key-card-meta {
display: flex;
gap: var(--s-4);
font: 400 12px/1.4 var(--f-body);
color: var(--fg-dim);
}
.api-key-card-meta strong {
color: var(--fg-soft);
font-weight: 500;
}
.api-key-card-token {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--s-3);
background: var(--bg-sunk);
border: 1px solid var(--hair);
border-radius: var(--r-sm);
padding: var(--s-2) var(--s-3);
}
.api-key-card-token-value {
font: 500 13px/1.4 var(--f-mono);
color: var(--fg);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
min-width: 0;
}
.api-key-card-token-actions {
display: flex;
gap: 4px;
flex-shrink: 0;
}
.api-key-card-foot {
display: flex;
justify-content: flex-end;
}
.api-key-card-revoke {
color: var(--error-text);
}
.api-key-card-revoke:hover {
background: var(--error-soft);
}
@media (max-width: 480px) {
.api-key-card-token {
flex-direction: column;
align-items: stretch;
}
.api-key-card-token-actions {
justify-content: flex-end;
}
}
import { ApiKeyCard } from "@magicblocksai/ui";
<ApiKeyCard
name="Production API key"
token="mb_live_a7f8c4d6AB12"
lastUsed="2 hours ago"
createdAt="14 Mar 2026"
onRevoke={() => console.log("revoke")}
/>
15.14 Session list
A list of active sessions: device + (optional) location + last-active timestamp, with a per-session Revoke button on the right. The current session is marked with .is-current, an accent-soft background, and a “Current session” pill — and never shows a Revoke button. Used on the Sessions settings page; pairs with <SettingsHeaderBlock> above. Below 480px the row grid reflows so the Revoke button drops to its own row aligned to the right edge.
Active sessions
.session-listThree rows demonstrating the canonical states: the current session (with the accent-soft background and pill, non-revocable), and two revocable rows on different devices. The Revoke button picks up the danger tint (var(--error-text)) and a soft red hover background.
-
MacBook Pro · ChromeCurrent session
-
iPhone 15 · Safari
-
Windows · Edge
<ul class="session-list" aria-label="Active sessions">
<li class="session-row is-current">
<div class="session-row-text">
<div class="session-row-device">MacBook Pro · Chrome
<span class="session-row-current-pill">Current session</span>
</div>
<div class="session-row-meta">
<span>Sydney, AU</span><span>Active now</span>
</div>
</div>
</li>
<li class="session-row">
<div class="session-row-text">
<div class="session-row-device">iPhone 15 · Safari</div>
<div class="session-row-meta">
<span>Sydney, AU</span><span>2 hours ago</span>
</div>
</div>
<button type="button" class="btn btn-ghost btn-sm session-row-revoke">Revoke</button>
</li>
</ul>
.session-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
background: var(--bg-paper);
border: 1px solid var(--hair);
border-radius: var(--r-md);
overflow: hidden;
}
import { SessionList, SessionRow } from "@magicblocksai/ui";
<SessionList aria-label="Active sessions">
<SessionRow
device="MacBook Pro · Chrome"
location="Sydney, AU"
lastActive="Active now"
current
/>
<SessionRow
device="iPhone 15 · Safari"
location="Sydney, AU"
lastActive="2 hours ago"
onRevoke={() => console.log("revoke iphone")}
/>
<SessionRow
device="Windows · Edge"
location="Melbourne, AU"
lastActive="3 days ago"
onRevoke={() => console.log("revoke windows")}
/>
</SessionList>
15.15 Danger zone block
Red-bordered footer container for destructive actions. Takes children — consumers compose any number of .danger-zone-action rows inside. Used as a footer block on Account, Workspace, and Integration settings pages. Border + soft red background use the kit's var(--error) token; the title text picks up var(--error-text). Below 480px each action row reflows: the CTA drops below the text and stretches to full width.
Account · destructive actions
.danger-zone-blockThree stacked action rows: Transfer ownership and Export and delete data use the soft .btn-secondary styling; Delete account uses the hard .btn-danger red. Each row gets a hairline bottom-divider; the last row's divider is suppressed.
Danger zone
These actions are permanent and can affect other people on your workspace. Read carefully before confirming.
Move workspace ownership to another admin. You’ll lose admin privileges immediately.
Download a full archive of your workspace data, then permanently delete it from MagicBlocks.
Permanently delete this account and all associated data. This action cannot be undone.
<section class="danger-zone-block" aria-label="Danger zone">
<header class="danger-zone-block-head">
<h2 class="danger-zone-block-title">Danger zone</h2>
<p class="danger-zone-block-desc">These actions are permanent…</p>
</header>
<div class="danger-zone-block-actions">
<div class="danger-zone-action">
<div class="danger-zone-action-text">
<div class="danger-zone-action-title">Transfer ownership</div>
<p class="danger-zone-action-desc">Move workspace ownership…</p>
</div>
<div class="danger-zone-action-cta">
<button type="button" class="btn btn-secondary">Transfer</button>
</div>
</div>
<div class="danger-zone-action">
<div class="danger-zone-action-text">
<div class="danger-zone-action-title">Delete account</div>
<p class="danger-zone-action-desc">Permanently delete…</p>
</div>
<div class="danger-zone-action-cta">
<button type="button" class="btn btn-danger">Delete</button>
</div>
</div>
</div>
</section>
.danger-zone-block {
border: 1px solid var(--error);
border-radius: var(--r-md);
background: color-mix(in oklab, var(--error) 4%, var(--bg-paper));
display: flex;
flex-direction: column;
}
.danger-zone-block-head {
padding: var(--s-4) var(--s-5);
border-bottom: 1px solid color-mix(in oklab, var(--error) 25%, var(--hair));
display: flex;
flex-direction: column;
gap: var(--s-2);
}
.danger-zone-block-title {
font: 600 14px/1.3 var(--f-body);
color: var(--error-text);
margin: 0;
}
.danger-zone-block-desc {
font: 400 13px/1.5 var(--f-body);
color: var(--fg-soft);
margin: 0;
max-width: 60ch;
}
.danger-zone-block-actions {
display: flex;
flex-direction: column;
}
.danger-zone-action {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--s-4);
padding: var(--s-4) var(--s-5);
border-bottom: 1px solid color-mix(in oklab, var(--error) 12%, var(--hair));
}
.danger-zone-action:last-child { border-bottom: 0; }
.danger-zone-action-text {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.danger-zone-action-title {
font: 500 14px/1.3 var(--f-body);
color: var(--fg);
}
.danger-zone-action-desc {
font: 400 13px/1.5 var(--f-body);
color: var(--fg-soft);
margin: 0;
max-width: 60ch;
}
.danger-zone-action-cta { flex-shrink: 0; }
@media (max-width: 480px) {
.danger-zone-action {
flex-direction: column;
align-items: stretch;
gap: var(--s-3);
}
.danger-zone-action-cta { width: 100%; }
}
import { DangerZoneBlock, DangerZoneAction } from "@magicblocksai/ui";
<DangerZoneBlock
description="These actions are permanent and can affect other people on your workspace. Read carefully before confirming."
>
<DangerZoneAction
title="Transfer ownership"
description="Move workspace ownership to another admin. You'll lose admin privileges immediately."
action={<button type="button" className="btn btn-secondary">Transfer</button>}
/>
<DangerZoneAction
title="Export and delete data"
description="Download a full archive of your workspace data, then permanently delete it from MagicBlocks."
action={<button type="button" className="btn btn-secondary">Export</button>}
/>
<DangerZoneAction
title="Delete account"
description="Permanently delete this account and all associated data. This action cannot be undone."
action={<button type="button" className="btn btn-danger">Delete</button>}
/>
</DangerZoneBlock>
15.16 Unsaved changes bar
Sticky bottom bar with Save + Discard actions. Tracks dirty state via the controlled dirty prop — consumers wire their own form state (React Hook Form, Formik, plain useState). Portal-mounted in production so it floats above page scroll at position: fixed along the bottom edge. Below 480px the inner pill reflows: message above, action row stretched full-width. The slide-up entrance animation is gated on @media (prefers-reduced-motion: no-preference).
Floating save bar (static replica)
.unsaved-changes-barThe bar is portal-mounted in production — here we render a static replica inside a position: relative wrapper so you can see the pill shape and chrome without it being yanked to the bottom of the viewport. The ink-coloured pill background holds in both light and dark mode (ink doesn't flip).
<!-- In production: render via the React component which uses Portal. -->
<!-- The HTML below is what gets injected at the bottom of the page. -->
<div class="unsaved-changes-bar" role="region" aria-label="Unsaved changes" aria-live="polite">
<div class="unsaved-changes-bar-inner">
<span class="unsaved-changes-bar-message">You have unsaved changes</span>
<div class="unsaved-changes-bar-actions">
<button type="button" class="btn btn-ghost btn-sm">Discard</button>
<button type="button" class="btn btn-primary btn-sm">Save changes</button>
</div>
</div>
</div>
.unsaved-changes-bar {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 90;
padding: var(--s-3);
pointer-events: none;
display: flex;
justify-content: center;
}
.unsaved-changes-bar-inner {
pointer-events: auto;
display: flex;
align-items: center;
gap: var(--s-5);
padding: var(--s-3) var(--s-4);
background: var(--ink);
color: var(--paper);
border-radius: var(--r-pill);
box-shadow: var(--sh-3);
max-width: 720px;
width: 100%;
}
.unsaved-changes-bar-message {
font: 500 13px/1.4 var(--f-body);
flex: 1;
min-width: 0;
}
.unsaved-changes-bar-actions {
display: flex;
gap: var(--s-2);
flex-shrink: 0;
}
.unsaved-changes-bar .btn-ghost { color: var(--paper); }
.unsaved-changes-bar .btn-ghost:hover { background: color-mix(in oklab, var(--paper) 12%, transparent); }
@media (max-width: 480px) {
.unsaved-changes-bar-inner {
flex-direction: column;
align-items: stretch;
gap: var(--s-3);
border-radius: var(--r-md);
}
.unsaved-changes-bar-actions { width: 100%; justify-content: stretch; }
.unsaved-changes-bar-actions .btn { flex: 1; }
}
@media (prefers-reduced-motion: no-preference) {
.unsaved-changes-bar { animation: unsaved-bar-in 240ms var(--ease) both; }
}
import { useState } from "react";
import { UnsavedChangesBar } from "@magicblocksai/ui";
function SettingsForm() {
const [dirty, setDirty] = useState(false);
const [saving, setSaving] = useState(false);
async function handleSave() {
setSaving(true);
await fetch("/api/settings", { method: "POST" });
setSaving(false);
setDirty(false);
}
return (
<>
{/* …your form fields, each setDirty(true) on change… */}
<UnsavedChangesBar
dirty={dirty}
saving={saving}
onSave={handleSave}
onDiscard={() => setDirty(false)}
/>
</>
);
}
15.17 Settings account page
The page-shaped wrapper: <SettingsShell> + <SettingsNavRail> + <SettingsHeaderBlock> + your body content. Drop-in for the common API-keys / Profile / Sessions settings page shape — consumers pass groups + activeKey and the wrapper hands them through to the rail, plus header slots and children for the body. The underlying shell provides the two-column grid (200px nav + fluid pane) and reflows to a single column at ≤720px.
API keys page composition
.settings-account-pageA realistic API-keys page: the rail on the left lists Profile / API keys (active, count 3) / Sessions; the body opens with a <SettingsHeaderBlock> + a New key action, then two <ApiKeyCard>s stacked vertically. At ≤720px the rail collapses above the body via the shell's grid breakpoint; at ≤480px the cards stack their token actions below the token row.
<!-- See 13.10 (nav rail), 13.11 (header), 13.13 (API key card) for the inner markup. -->
<div class="settings-account-page">
<div class="settings">
<aside class="settings-nav">
<nav class="settings-nav-rail" aria-label="Settings"><!-- groups + items --></nav>
</aside>
<form class="settings-pane">
<header class="settings-header-block"><!-- eyebrow + title + actions --></header>
<!-- body: e.g. a list of .api-key-card -->
</form>
</div>
</div>
.settings-account-page {
display: flex;
flex-direction: column;
min-height: 480px;
}
import { SettingsAccountPage, ApiKeyCard } from "@magicblocksai/ui";
const groups = [
{
label: "Account",
items: [
{ key: "profile", label: "Profile" },
{ key: "api-keys", label: "API keys", count: 3 },
{ key: "sessions", label: "Sessions" },
],
},
];
<SettingsAccountPage
groups={groups}
defaultActiveKey="api-keys"
eyebrow="Account"
title="API keys"
description="Tokens for programmatic access to your workspace. Revealed once at creation — copy and store securely."
actions={<button type="button" className="btn">New key</button>}
>
<ApiKeyCard name="Production key" token="mb_live_…AB12" lastUsed="2 hours ago" />
<ApiKeyCard name="Staging key" token="mb_test_…CD34" lastUsed="3 days ago" />
</SettingsAccountPage>
15.18 Route progress
Indeterminate route-load bar. A 2px gradient slides across the anchored edge while a navigation is in flight; fades out smoothly when it lands. position: absolute on the bar means its parent must establish a positioning context — every topbar already does. v1.60.0 (app-team R1).
Active state
.route-progress[data-active]The mock topbar shows the bar's resting position. In production it appears only while RR7's useNavigation() is non-idle.
<header class="topbar">
<div class="route-progress route-progress-tone-accent route-progress-pos-top"
data-active style="--rp-h: 2px" role="progressbar"></div>
<!-- ...rest of the topbar... -->
</header>
/* The parent must establish a positioning context. */
.topbar { position: relative; }
/* Drop the [data-active] attribute to fade the bar out. */
.route-progress[data-active] { opacity: 1; }
// In a vanilla shell, toggle [data-active] from your navigation handler:
function setNavLoading(active) {
document.querySelector('.route-progress')
.toggleAttribute('data-active', active);
}
import { useNavigation } from "react-router-dom";
import { RouteProgress } from "@magicblocksai/ui";
export function Topbar() {
const isNavigating = useNavigation().state !== "idle";
return (
<header className="topbar">
<RouteProgress active={isNavigating} />
{/* ...rest of the topbar... */}
</header>
);
}
15.19 Dashboard composition
A worked, screen-level composition for the workspace Dashboard — the “feel the pulse” page operators land on every morning. Composes existing primitives only (KpiDeltaTile, sparklines, TimeSeriesChart frame, SavedViewsRail tabs, SessionList, status pills, filter pills) into a holistic operator layout. The page lives behind AppShell in production; here the shell is inlined so the composition reads end-to-end. Real screen, real data — tiles carry deltas and 14-day sparklines, sessions carry sentiment-coloured leading bars, the title runs a live workspace pulse. The left rail is the expanded mode of the AppShell sidebar (same nav set as chapter 18.7’s collapsed icon-band, just with labels + counts visible).
Workspace dashboard — past 14 days
.dash-screenHolistic composition: collapsible left rail, hero head with live pulse + filter pills, 8-tile KPI strip with sparklines + deltas, multi-series analytics panel with toggleable series + density tabs, two-column feed grid (Latest Contacts · Latest Sessions with saved-view tabs). Stacks to one column below 1100px.
Good morning, Jay. Three agents are live.
Pulse · live · 412 sessions in flight across past 14 days · Synced 2m ago
Analytics past 14 days
Latest contacts 10
Latest sessions
That's a really important question, and I appreciate you asking! Choosing a mortgage lender is a big decision, and we want you to feel confident in your choice…
I understand the frustration. Let me see if I can connect you with a human teammate — can you tell me the best email to reach you on?
Booked — 26 May 10:30am AEST. I'll send a confirmation to [email protected] and add the calendar invite. Anything else before we wrap?
No worries — happy to walk you through our 2023 Tyrells Hunter Valley Shiraz. Would you prefer the case price or the per-bottle breakdown first?
<!-- Holistic composition. Inline the AppShell sidebar + topbar
in production; here the dash-screen wrapper inlines both for
a self-contained demo. -->
<div class="dash-screen">
<aside class="dash-aside">…</aside>
<main class="dash-main">
<header class="dash-head">
<div class="dash-head-title">
<h2>Good morning, Jay. <em>Three agents</em> are live.</h2>
<p class="dash-head-sub">
<span class="dash-pulse">Pulse · live</span> · 412 sessions in flight…
</p>
</div>
<div class="dash-toolbar">…filter pills…</div>
</header>
<div class="dash-kpis">
<article class="dash-kpi is-positive">
<div class="dash-kpi-head">
<span class="dash-kpi-kicker">Sessions</span>
<span class="dash-kpi-delta is-up">▲ 18.4%</span>
</div>
<div class="dash-kpi-val">1,038</div>
<svg class="dash-kpi-spark"><path class="spark-line" d="…"/></svg>
</article>
…7 more tiles…
</div>
<section class="dash-panel">…analytics chart…</section>
<div class="dash-feeds">
<section class="dash-panel">…Latest contacts…</section>
<section class="dash-panel">…Latest sessions w/ sentiment bars…</section>
</div>
</main>
</div>
/* Layout only — the primitives (.kpi-delta-tile, .time-series-chart,
.session-list, .saved-views-rail) carry their own styling from
_shared.css. This block defines the page-level grid. */
.dash-screen {
display: grid;
grid-template-columns: 220px 1fr;
min-height: 760px;
background: var(--bg);
border: 1px solid var(--hair);
border-radius: var(--r-lg);
}
.dash-kpis { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--s-4); }
.dash-feeds { display: grid; grid-template-columns: 1fr 1.15fr; gap: var(--s-4); }
.dash-kpi-val em { font: 400 italic 30px/1 var(--f-serif); color: var(--accent); }
.dash-pulse::before {
content: ""; width: 7px; height: 7px; border-radius: 50%;
background: #10A37F; animation: dash-pulse-blink 2.4s var(--ease) infinite;
}
@media (prefers-reduced-motion: reduce) {
.dash-pulse::before { animation: none; }
}
@media (max-width: 1100px) {
.dash-screen { grid-template-columns: 1fr; }
.dash-kpis { grid-template-columns: repeat(2, 1fr); }
.dash-feeds { grid-template-columns: 1fr; }
}
/* PROVISIONAL — pending kit additions flagged in
.scratch/app-screens-component-gaps.md. The composition below uses
the existing primitive components; the layout wrapper would ship as
a new <DashboardPage> page-shape (chapter 13) that composes them. */
import {
AppShell, KpiDeltaTile, TimeSeriesChart,
SavedViewsRail, SessionList, FilterChipGroup,
DateRangePicker, SyncStatus, Sparkline,
} from "@magicblocksai/ui";
export function DashboardPage({ tiles, series, contacts, sessions }) {
return (
<AppShell sidebar={<WorkspaceNav active="dashboard" />}>
<header className="dash-head">
<h2>Good morning, Jay. <em>Three agents</em> are live.</h2>
<Toolbar>
<FilterChipGroup ... />
<DateRangePicker ... />
</Toolbar>
</header>
<Grid columns={4} gap={4}>
{tiles.map(t =>
<KpiDeltaTile key={t.id} {...t} spark={<Sparkline data={t.spark} />} />
)}
</Grid>
<TimeSeriesChart series={series} grouping="day" toggleable />
<Grid columns={[1, 1.15]} gap={4}>
<LatestContactsPanel contacts={contacts} />
<LatestSessionsPanel
sessions={sessions}
tabs={["Latest", "With goal", "Negative"]}
/>
</Grid>
</AppShell>
);
}
15.20 RoomHeader
The three-line room hero — eyebrow · display heading · lede — that opens every agent-builder room (Overview · Persona · Knowledge · …). Wraps <Eyebrow> + <Heading display> + <Lede> so the rhythm stays identical across rooms without hand-rolled markup. The title accepts an <em> for the Fraunces-italic flourish, and the heading defaults to <h2> (rooms sit beneath the page <PageHeader> h1 — keeps heading order clean).
RoomHeader
.room-headerA resting room hero for the Persona room — eyebrow, display heading with an italic flourish on “is”, and a one-line lede.
Persona
Who Marcus is.
Personas are reusable across agents — define the voice once, attach it anywhere.
<header class="room-header">
<p class="eyebrow">Persona</p>
<h2 class="display">Who Marcus <em>is</em>.</h2>
<p class="lede">Personas are reusable across agents — define the voice once, attach it anywhere.</p>
</header>
.room-header {
display: flex;
flex-direction: column;
gap: var(--s-2);
}
import { RoomHeader } from "@magicblocksai/ui";
<RoomHeader
eyebrow="Persona"
title={<>Who Marcus <em>is</em>.</>}
description="Personas are reusable across agents — define the voice once, attach it anywhere."
/>
15.21 SidebarHead
The top band of a workspace rail — brand lockup on the left, a collapse-toggle chevron on the right. The chevron rotates off the .app-shell[data-sidebar-mode] ancestor, so it points the right way in both modes without JS; the collapsed↔expanded state itself is owned by <WorkspaceShell>. Promoted from the chapter-private agent-builder chrome so every operator app reaches for the same rail head.
SidebarHead
.app-shell-side-headBrand (<Logo>) + collapse toggle. The toggle fires onToggle; the shell owns the state.
<div class="app-shell-side-head">
<!-- brand lockup — see <Logo> / chapter 13.x -->
<span class="logo logo-md">…<span class="logo-wordmark">magicblocks</span></span>
<button type="button" class="app-shell-side-toggle" data-sidebar-toggle aria-label="Toggle sidebar">
<svg viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.4"
stroke-linecap="round" stroke-linejoin="round"><path d="M5 3 L9 7 L5 11"/></svg>
</button>
</div>
.app-shell-side-head {
display: flex;
align-items: center;
justify-content: space-between;
}
/* the chevron rotates 180° when the rail is collapsed */
.app-shell[data-sidebar-mode="collapsed"] .app-shell-side-toggle svg {
transform: rotate(180deg);
}
import { SidebarHead, Logo } from "@magicblocksai/ui";
<SidebarHead
brand={<Logo size="md" />}
onToggle={() => setCollapsed((c) => !c)}
/>
15.22 WorkspaceSwitcher
The workspace identity control at the top of the rail — letter-box (or uploaded-logo) avatar + workspace name + sub-line. Distinct from the MagicBlocks brand mark in <SidebarHead> above it: this is the customer's workspace. Opens a switcher menu when wired; in collapsed mode the meta hides and only the avatar shows.
WorkspaceSwitcher
.ws-switcherA resting switcher for “Charlie Co.” on the Scale plan, plus two carrying an unread badge — a bare dot and a count — pinned to the avatar’s top-right corner.
<button type="button" class="ws-switcher" aria-label="Charlie Co.">
<span class="ws-switcher-avatar" aria-hidden="true">C</span>
<span class="ws-switcher-meta">
<span class="ws-switcher-name">Charlie Co.</span>
<span class="ws-switcher-sub">Scale · US</span>
</span>
</button>
<!-- unread badge: wrap the avatar in a non-clipping frame -->
<button type="button" class="ws-switcher" aria-label="Globex">
<span class="ws-switcher-avatar-frame">
<span class="ws-switcher-avatar" aria-hidden="true">G</span>
<span class="ws-switcher-badge">3</span> <!-- empty span = bare dot -->
</span>
<span class="ws-switcher-meta">…</span>
</button>
.ws-switcher {
display: flex;
align-items: center;
gap: var(--s-2);
width: 100%;
}
/* collapsed rail — hide the meta, centre the avatar */
.app-shell[data-sidebar-mode="collapsed"] .ws-switcher-meta { display: none; }
/* unread badge — the frame escapes the avatar's overflow:hidden */
.ws-switcher-avatar-frame { position: relative; display: inline-flex; }
.ws-switcher-badge {
position: absolute; top: -3px; right: -3px;
min-width: 8px; height: 8px; padding: 0 3px;
background: var(--accent); color: var(--on-accent);
border-radius: var(--r-pill); box-shadow: 0 0 0 2px var(--bg-paper);
}
.ws-switcher-badge:empty { width: 8px; min-width: 0; padding: 0; }
import { WorkspaceSwitcher } from "@magicblocksai/ui";
<WorkspaceSwitcher
name="Charlie Co."
sub="Scale · US"
onClick={openSwitcher}
/>
// unread indicator on the workspace avatar
<WorkspaceSwitcher name="Globex" badge /> // bare dot
<WorkspaceSwitcher name="Globex" badge={3} /> // count
15.24 WorkspaceUser
The signed-in user control pinned to the bottom of the rail — initials (or avatar) + name + sub-line, opening a user menu when wired. This is where the light/dark toggle and profile live in a sidebar-first app, so the top bar is free to be optional. Collapsed mode shows the avatar only.
WorkspaceUser
.ws-userA resting user control for the workspace owner.
<button type="button" class="ws-user" aria-label="Jay Stockwell">
<span class="ws-user-avatar" aria-hidden="true">JS</span>
<span class="ws-user-meta">
<span class="ws-user-name">Jay Stockwell</span>
<span class="ws-user-sub">Owner</span>
</span>
</button>
.ws-user {
display: flex;
align-items: center;
gap: var(--s-2);
width: 100%;
}
.app-shell[data-sidebar-mode="collapsed"] .ws-user-meta { display: none; }
import { WorkspaceUser } from "@magicblocksai/ui";
<WorkspaceUser
name="Jay Stockwell"
sub="Owner"
onClick={openUserMenu}
/>
15.25 WorkspaceThemeToggle
The in-rail light/dark control — what lets a sidebar-first app drop the top bar entirely (the old top bar's only real job was the theme switch). Renders as a standard .ws-nav-icon rail item so it sits flush with the nav; toggles data-theme="dark" on <html> and persists the choice to localStorage.
WorkspaceThemeToggle
.ws-nav-iconThe resting (light-mode) control — a moon glyph offering “switch to dark”. Click swaps to a sun.
<button type="button" class="ws-nav-icon" data-tooltip="Theme" aria-label="Theme" aria-pressed="false">
<svg viewBox="0 0 18 18" …><path d="M14.5 10.6 A5.6 5.6 0 1 1 7.4 3.5 A4.3 4.3 0 0 0 14.5 10.6 Z"/></svg>
<span class="ws-nav-icon-label">Theme</span>
</button>
/* uses the shared .ws-nav-icon rail-item chrome — no bespoke CSS */
btn.addEventListener("click", () => {
const root = document.documentElement;
const dark = root.getAttribute("data-theme") !== "dark";
root.toggleAttribute("data-theme", false);
if (dark) root.setAttribute("data-theme", "dark");
localStorage.setItem("mb_theme", dark ? "dark" : "light");
});
import { WorkspaceThemeToggle } from "@magicblocksai/ui";
<WorkspaceThemeToggle />
15.26 WorkspaceBar
The optional compact top bar (~48px). Sidebar-first apps omit it entirely — the rail's bottom cluster already carries settings/theme/profile. Apps that want a bar (a global search, a breadcrumb — Spark-style) pass it as <WorkspaceShell topbar={…}>. Three zones: title (left) · middle (search/command) · actions (right). This is the kit's operator top bar; the marketing <TopNav> is a separate, marketing-only primitive.
WorkspaceBar
.ws-barA bar titled “Agents” with a primary action on the right.
<div class="ws-bar">
<div class="ws-bar-title">Agents</div>
<!-- optional middle: <div class="ws-bar-mid">…search…</div> -->
<div class="ws-bar-actions">
<button type="button" class="btn btn-primary">New agent</button>
</div>
</div>
.ws-bar {
display: flex;
align-items: center;
gap: var(--s-3);
height: 48px;
padding: 0 var(--s-4);
background: var(--bg-paper);
border-bottom: 1px solid var(--hair);
}
.ws-bar-actions { margin-left: auto; }
import { WorkspaceBar, Button } from "@magicblocksai/ui";
<WorkspaceBar
title="Agents"
actions={<Button variant="primary">New agent</Button>}
/>
15.27 WorkspaceShell
The batteries-included NextGen app shell — the canonical, standalone composition of everything above. It wraps the bare <AppShell> with a fully-assembled collapsible rail (brand · workspace switcher · grouped nav · settings · theme · user) and owns the collapse toggle, so a drop-in needs zero wiring. Sidebar-first: with no top bar the rail's bottom cluster carries settings/theme/profile and the main runs full-height; pass an optional <WorkspaceBar> only when an app wants one. The inner section is just children — the shell hosts any view, so the agent builder is one section among many, not the chrome itself. Mobile-aware: at ≤960px the rail folds into an off-canvas drawer and the shell renders its own hamburger to reveal it — still zero wiring; tapping a nav item closes it.
WorkspaceShell
.app-shell + .ws-*The expanded rail with a Live/Build nav, the Dashboard active, and the settings/theme/profile bottom cluster — wrapping a sample section.
<!-- The assembled shell — see 13.21–13.26 for each rail primitive. -->
<div class="app-shell" data-sidebar-mode="expanded">
<aside class="app-shell-side">
<div class="app-shell-side-head">…logo + toggle…</div>
<button class="ws-switcher">…</button>
<nav class="ws-nav">…WorkspaceNavIcon items…</nav>
<span class="app-shell-side-spacer"></span>
<span class="app-shell-side-divider"></span>
<button class="ws-nav-icon">…Settings…</button>
<button class="ws-nav-icon">…Theme…</button>
<button class="ws-user">…</button>
</aside>
<main class="app-shell-main">…inner section…</main>
</div>
import { WorkspaceShell, Logo } from "@magicblocksai/ui";
<WorkspaceShell
brand={<Logo />}
workspace={{ name: "Charlie Co.", sub: "Scale · US" }}
nav={[
{ section: "Live", items: [
{ icon: dashboardIcon, label: "Dashboard", href: "/", active: true },
{ icon: sessionsIcon, label: "Sessions", count: 412, href: "/sessions" },
] },
{ section: "Build", items: [
{ icon: agentsIcon, label: "Agents", count: 7, href: "/agents" },
] },
]}
settings={{ href: "/settings" }}
theme
actions={[{ icon: bellIcon, label: "Notifications", onClick: openInbox, badge: true }]}
linkComponent={RailLink} /* ({ href, ...p }) => <NavLink to={href} {...p} /> */
user={{
name: "Jay Stockwell", sub: "Owner",
menu: [{ label: "Sign out", onSelect: signOut, danger: true }],
}}
>
{section}
</WorkspaceShell>
Slim rail with the Sage launcher
.ws-nav-icon.is-accent · actions[].accentThe redesigned NextGen default: the rail starts collapsed to a slim icon band (defaultCollapsed) and the bottom cluster pins Ask Sage as an accent-tinted launcher (accent: true) above Settings — one tap (or ⌘K) from anywhere in the app. Hover any icon for its slide-out tooltip; the head toggle expands the rail as usual.
<!-- Collapsed rail; the Sage launcher is a bottom-cluster action with .is-accent. -->
<div class="app-shell" data-sidebar-mode="collapsed">
<aside class="app-shell-side">
…rail head + switcher + nav…
<span class="app-shell-side-spacer"></span>
<span class="app-shell-side-divider"></span>
<button class="ws-nav-icon is-accent" data-tooltip="Ask Sage (⌘K)" aria-label="Ask Sage (⌘K)">…</button>
<button class="ws-nav-icon">…Settings…</button>
<button class="ws-user">…</button>
</aside>
<main class="app-shell-main">…inner section…</main>
</div>
import { WorkspaceShell, Logo, WandIcon } from "@magicblocksai/ui";
<WorkspaceShell
brand={<Logo />}
nav={nav}
defaultCollapsed
actions={[{ icon: <WandIcon />, label: "Ask Sage (⌘K)", onClick: openSage, accent: true }]}
settings={{ href: "/settings" }}
user={{ name: "Jay Stockwell", sub: "Owner" }}
>
{section}
</WorkspaceShell>
15.28 SectionHeader
The canonical inner-section header — back · eyebrow · title · subtitle · actions · optional tabs. The one header every standard section reaches for; it supersedes the overlapping <PageHeader> and <RoomTabs> roles (both now deprecated in favour of this). Pairs with <SectionView> as its header slot; full-bleed sections like the agent builder keep their own <AgentBuilderHead>.
SectionHeader
.section-headerThe Agents section header — back chevron, Build eyebrow, title, subtitle, and a primary action.
<header class="section-header">
<div class="section-header-main">
<a class="section-header-back" href="/" aria-label="Back"><svg …><path d="M10 3 L5 8 L10 13"/></svg></a>
<div class="section-header-text">
<p class="section-header-eyebrow">Build</p>
<h1 class="section-header-title">Agents</h1>
<p class="section-header-subtitle">Design, test, and ship the agents that run your workspace.</p>
</div>
<div class="section-header-actions"><button type="button" class="btn btn-primary">New agent</button></div>
</div>
</header>
.section-header { display: flex; flex-direction: column; gap: var(--s-3); }
.section-header-main { display: flex; align-items: flex-start; gap: var(--s-4); }
.section-header-text { flex: 1; min-width: 0; }
.section-header-title { margin: 0; font: 700 24px/1.15 var(--f-display); }
.section-header-actions { flex: none; }
import { SectionHeader, Button } from "@magicblocksai/ui";
<SectionHeader
eyebrow="Build"
title="Agents"
subtitle="Design, test, and ship the agents that run your workspace."
back={{ href: "/" }}
actions={<Button variant="primary">New agent</Button>}
/>
15.29 SectionView
The thin, consistent frame for a standard inner section — a pinned header (a <SectionHeader>), a scrollable body with page gutters, and an optional pinned footer (e.g. an unsaved-changes bar). It gives every section the same scaffolding without dictating the body, which is composed from any kit primitives. Full-bleed sections (the agent builder, the live tester) mount directly instead — they own their whole frame. This is how inner sections stay independently-designed yet consistent.
SectionView
.section-viewA Contacts section — header pinned, body scrolls, a discard footer pinned to the bottom.
Contacts
The section body scrolls here — built from any kit primitives (tables, cards, forms). The header stays pinned at the top; the optional footer pins to the bottom.
<section class="section-view">
<div class="section-view-head"><!-- <SectionHeader> --></div>
<div class="section-view-body"><!-- any kit primitives --></div>
<div class="section-view-foot"><!-- optional, e.g. unsaved bar --></div>
</section>
.section-view { display: flex; flex-direction: column; height: 100%; min-height: 0; }
.section-view-head { padding: var(--s-6) var(--s-7) 0; flex: none; }
.section-view-body { flex: 1; min-height: 0; overflow-y: auto; padding: var(--s-6) var(--s-7); }
.section-view-foot { flex: none; border-top: 1px solid var(--hair); padding: var(--s-3) var(--s-7); }
import { SectionView, SectionHeader, Button } from "@magicblocksai/ui";
<SectionView
header={<SectionHeader title="Contacts" actions={<Button variant="primary">New contact</Button>} />}
footer={<Button variant="ghost">Discard</Button>}
>
{/* any kit primitives */}
</SectionView>