Chapter 09 · Feedback & overlays

Feedback & overlays

How MagicBlocks tells the user what's happening. Transient toasts, persistent alerts, top-of-page banners, hover tooltips, rich popovers, blocking modals, slide-over drawers, shimmering skeletons, branded spinners, the imperative DialogHost, accordion disclosures, filter popovers, fork-choice dialogs, and data-freshness banners.

9.1 Toasts

Transient notifications that appear in a corner of the screen. Paper card with a coloured left rail for semantic type, tinted icon chip, title + short description, dismiss button. Auto-dismiss after 5–7 seconds except for errors.

Toast variants

.toast

Four tones: success (green), info (blue), warn (amber), error (pink). Left rail is the single colour signal — the rest of the toast stays on paper.

Meeting booked
Thursday, 2:30 PM with Alicia Chen.
Agent is warming up
New persona is training on your 12 call transcripts.
Webhook retry
Two events queued, next attempt in 30s.
Couldn't reach Twilio
Outbound SMS paused. View status
<div class="toast-demo">
  <div class="toast">
    <span class="toast-ic toast-ic-success"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg></span>
    <div class="toast-body">
      <div class="toast-title">Meeting booked</div>
      <div class="toast-desc">Thursday, 2:30 PM with Alicia Chen.</div>
    </div>
    <button class="toast-close" aria-label="Dismiss"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M18 6 6 18M6 6l12 12"/></svg></button>
  </div>
  <div class="toast toast-info">
    <span class="toast-ic toast-ic-info"><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="10"/><path d="M12 16v-4M12 8h.01"/></svg></span>
    <div class="toast-body">
      <div class="toast-title">Agent is warming up</div>
      <div class="toast-desc">New persona is training on your 12 call transcripts.</div>
    </div>
    <button class="toast-close" aria-label="Dismiss"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M18 6 6 18M6 6l12 12"/></svg></button>
  </div>
  <div class="toast toast-warn">
    <span class="toast-ic toast-ic-warn"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><path d="M12 9v4M12 17h.01"/></svg></span>
    <div class="toast-body">
      <div class="toast-title">Webhook retry</div>
      <div class="toast-desc">Two events queued, next attempt in 30s.</div>
    </div>
    <button class="toast-close" aria-label="Dismiss"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M18 6 6 18M6 6l12 12"/></svg></button>
  </div>
  <div class="toast toast-error">
    <span class="toast-ic toast-ic-error"><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="10"/><path d="M12 8v4M12 16h.01"/></svg></span>
    <div class="toast-body">
      <div class="toast-title">Couldn't reach Twilio</div>
      <div class="toast-desc">Outbound SMS paused. <a href="#">View status</a></div>
    </div>
    <button class="toast-close" aria-label="Dismiss"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M18 6 6 18M6 6l12 12"/></svg></button>
  </div>
</div>
.toast-demo { display: flex; flex-direction: column; gap: var(--s-3); width: 100%; max-width: 380px; }
.toast {
  display: flex; align-items: flex-start; gap: var(--s-3);
  padding: 12px var(--s-4);
  background: var(--bg-paper);
  border: 1px solid var(--hair);
  border-radius: var(--r-md);
  box-shadow: var(--sh-3);
  position: relative;
}
.toast::before {
  content: ""; position: absolute;
  left: 0; top: 10px; bottom: 10px; width: 3px;
  border-radius: 3px;
  background: var(--success);
}
.toast.toast-info::before  { background: var(--info); }
.toast.toast-warn::before  { background: var(--warning); }
.toast.toast-error::before { background: var(--accent); }

.toast-ic {
  width: 28px; height: 28px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  flex-shrink: 0; margin-top: 1px;
}
.toast-ic-success { background: var(--success-soft); color: var(--success-text); }
.toast-ic-info    { background: var(--info-soft);    color: var(--info-text); }
.toast-ic-warn    { background: var(--warning-soft); color: var(--warning-text); }
.toast-ic-error   { background: var(--accent-soft);  color: var(--accent-text); }

.toast-body { flex: 1; min-width: 0; }
.toast-title { font: 600 14px/1.3 var(--f-display); color: var(--fg); letter-spacing: -0.005em; }
.toast-desc { font: 400 13px/1.5 var(--f-body); color: var(--fg-soft); margin-top: 2px; }
.toast-desc a { color: var(--accent-text); text-decoration: none; font-weight: 500; }
.toast-desc a:hover { text-decoration: underline; }
.toast-close {
  background: transparent; border: 0; padding: 4px; margin: -4px;
  color: var(--fg-dim); cursor: pointer;
  border-radius: var(--r-xs);
  align-self: flex-start;
}
.toast-close:hover { color: var(--fg); background: var(--bg-sunken); }
import { Toast, Toaster, useToast, Button } from "@magicblocksai/ui";

// 1. Mount the <Toaster> once near the app root.
export function AppRoot({ children }) {
  return (
    <>
      {children}
      <Toaster position="top-right" duration={5000} />
    </>
  );
}

// 2. Anywhere in the tree, fire toasts via the useToast() hook.
export default function Demo() {
  const toast = useToast();
  return (
    <>
      <Button onClick={() => toast.success("Meeting booked", { title: "Thursday, 2:30 PM" })}>
        Save
      </Button>

      // 3. Or render a <Toast> visually (the primitive used inside <Toaster>).
      <Toast tone="success" title="Meeting booked" onDismiss={() => {}}>
        Thursday, 2:30 PM with Alicia Chen.
      </Toast>
    </>
  );
}

9.2 Inline alerts

Persistent, contextual messages that live inside a page surface (not floating). Bigger than toasts, smaller than modals. Good for 'your integration is connected', config warnings, or post-action confirmations.

Alert variants

.alert

Left rail in the semantic colour, tinted icon chip. Supports a title + paragraph + action row.

Connected to HubSpot

Contacts and deals will start syncing in the next few minutes.

This agent runs in simulation mode

Messages won't be sent to real contacts until you flip the switch to live.

Quiet hours will apply

Outbound SMS is paused between 9pm and 7am in each contact's timezone.

Couldn't verify your sending domain

DNS records are missing. Outbound email is paused until SPF and DKIM are in place.

<div class="alerts">
  <div class="alert alert-success">
    <span class="alert-ic"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg></span>
    <div class="alert-body">
      <div class="alert-title">Connected to HubSpot</div>
      <p>Contacts and deals will start syncing in the next few minutes.</p>
    </div>
  </div>
  <div class="alert alert-info">
    <span class="alert-ic"><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="10"/><path d="M12 16v-4M12 8h.01"/></svg></span>
    <div class="alert-body">
      <div class="alert-title">This agent runs in simulation mode</div>
      <p>Messages won't be sent to real contacts until you flip the switch to live.</p>
      <div class="alert-actions"><a href="#">Read the simulation guide →</a></div>
    </div>
  </div>
  <div class="alert alert-warn">
    <span class="alert-ic"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><path d="M12 9v4M12 17h.01"/></svg></span>
    <div class="alert-body">
      <div class="alert-title">Quiet hours will apply</div>
      <p>Outbound SMS is paused between 9pm and 7am in each contact's timezone.</p>
    </div>
  </div>
  <div class="alert alert-error">
    <span class="alert-ic"><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="10"/><path d="M12 8v4M12 16h.01"/></svg></span>
    <div class="alert-body">
      <div class="alert-title">Couldn't verify your sending domain</div>
      <p>DNS records are missing. Outbound email is paused until SPF and DKIM are in place.</p>
      <div class="alert-actions">
        <a href="#" class="alert-btn">Fix DNS</a>
        <a href="#">View log</a>
      </div>
    </div>
  </div>
</div>
.alerts { display: flex; flex-direction: column; gap: var(--s-4); }
.alert {
  display: flex; gap: var(--s-3);
  padding: var(--s-4);
  background: var(--bg-paper);
  border: 1px solid var(--hair);
  border-left: 3px solid var(--success);
  border-radius: var(--r-md);
}
.alert.alert-info  { border-left-color: var(--info); }
.alert.alert-warn  { border-left-color: var(--warning); }
.alert.alert-error { border-left-color: var(--accent); }

.alert-ic {
  width: 32px; height: 32px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  flex-shrink: 0;
}
.alert-success .alert-ic { background: var(--success-soft); color: var(--success-text); }
.alert-info    .alert-ic { background: var(--info-soft);    color: var(--info-text); }
.alert-warn    .alert-ic { background: var(--warning-soft); color: var(--warning-text); }
.alert-error .alert-ic   { background: var(--error-soft);   color: var(--error-text); }

.alert-body { flex: 1; }
.alert-title { font: 600 15px/1.3 var(--f-display); color: var(--fg); letter-spacing: -0.005em; }
.alert-body p { font: 400 14px/1.55 var(--f-body); color: var(--fg-soft); margin: 4px 0 0; }
.alert-actions { margin-top: var(--s-3); display: flex; gap: var(--s-4); align-items: center; }
.alert-actions a { font: 500 13px/1 var(--f-body); color: var(--accent-text); text-decoration: none; }
.alert-actions a:hover { text-decoration: underline; }
.alert-btn {
  background: var(--accent) !important; color: var(--paper) !important;
  padding: 8px 14px; border-radius: var(--r-md);
  box-shadow: var(--sh-pink);
  font: 600 13px/1 var(--f-display) !important;
}
.alert-btn:hover { text-decoration: none !important; transform: translateY(-1px); }
import { Alert } from "@magicblocksai/ui";

export default function Demo() {
  return (
    <Alert tone="success" title="Connected to HubSpot">
      Contacts and deals will start syncing in the next few minutes.
    </Alert>
  );
}

9.3 Top-of-page banners

System-wide announcements that span the full width of the app shell. Use sparingly — never more than one at a time. Reserved for launch news, outages, and account-wide notices.

Banner tones

.banner

Info (blue) for feature launches, warn (amber) for maintenance, accent (pink) for promo. Always dismissible.

<div class="banners">
  <div class="banner banner-info">
    <span class="banner-ic"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3 14 9l6 2-6 2-2 6-2-6-6-2 6-2z"/></svg></span>
    <div class="banner-body">
      <strong>Agent Composer</strong> is out of beta.
      <a href="#">See what changed →</a>
    </div>
    <button class="banner-close" aria-label="Dismiss"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M18 6 6 18M6 6l12 12"/></svg></button>
  </div>
  <div class="banner banner-warn">
    <span class="banner-ic"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><path d="M12 9v4M12 17h.01"/></svg></span>
    <div class="banner-body">
      Scheduled maintenance <strong>Thu 2:00 AM UTC</strong> — outbound voice paused ~15 min.
    </div>
    <button class="banner-close" aria-label="Dismiss"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M18 6 6 18M6 6l12 12"/></svg></button>
  </div>
  <div class="banner banner-accent">
    <div class="banner-body">
      <strong>Summer credit:</strong> first 1,000 agent minutes free when you upgrade this week.
      <a href="#">Apply credit →</a>
    </div>
    <button class="banner-close" aria-label="Dismiss"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M18 6 6 18M6 6l12 12"/></svg></button>
  </div>
</div>
.banners { display: flex; flex-direction: column; gap: var(--s-3); }
.banner {
  display: flex; align-items: center; gap: var(--s-3);
  padding: 10px var(--s-4);
  background: var(--bg-sunken);
  border: 1px solid var(--hair);
  border-radius: var(--r-md);
  font: 400 13.5px/1.5 var(--f-body); color: var(--fg);
}
.banner strong { font-weight: 600; }
.banner a { color: var(--accent-text); text-decoration: none; font-weight: 500; white-space: nowrap; }
.banner a:hover { text-decoration: underline; }
.banner-info   { background: var(--info-soft); border-color: color-mix(in oklab, var(--info) 30%, transparent); color: var(--info-text); }
.banner-info a { color: var(--info-text); }
.banner-warn  { background: #FFF0D6; border-color: color-mix(in oklab, #8A5A00 20%, transparent); color: #6B4300; }
.banner-warn a { color: #8A5A00; }
.banner-accent {
  background: var(--accent); color: var(--paper);
  border-color: transparent;
  box-shadow: var(--sh-pink);
}
.banner-accent a { color: var(--paper); text-decoration: underline; text-decoration-thickness: 2px; text-underline-offset: 3px; }
.banner-accent .banner-close { color: var(--paper); opacity: 0.7; }
.banner-accent .banner-close:hover { opacity: 1; }

.banner-ic {
  width: 24px; height: 24px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  flex-shrink: 0;
  background: color-mix(in oklab, currentColor 12%, transparent);
  color: inherit;
}
.banner-body { flex: 1; }
.banner-close {
  background: transparent; border: 0; padding: 4px; margin: -4px;
  color: inherit; cursor: pointer; opacity: 0.55;
  border-radius: var(--r-xs);
}
.banner-close:hover { opacity: 1; }
import { Banner } from "@magicblocksai/ui";

export default function Demo() {
  return (
    <Banner
      tone="warning"
      title="Connection at risk"
      onDismiss={() => {}}
    >
      Your inbox token expires in 24 hours.
    </Banner>
  );
}

9.4 Tooltips

Small, ink-coloured callouts that appear on hover or focus. Short labels only — if you need two sentences, use a popover instead. The tooltip ALWAYS has a pointing arrow.

Tooltip anchors

.tt

Default ink, pink for accent / brand-relevant actions. Include a tiny kbd pill when the action has a keyboard shortcut.

Keyboard shortcut ⌘K Switches to agent mode MagicBlocks agents run 24/7 and hand off to humans when the conversation gets complex.
<div class="tt-demo">
  <span class="tt-wrap">
    <button class="tt-trigger">Hover me</button>
    <span class="tt tt-top">Keyboard shortcut <kbd class="tt-kbd">⌘K</kbd></span>
  </span>
  <span class="tt-wrap">
    <button class="tt-trigger tt-trigger-accent">Pink anchor</button>
    <span class="tt tt-top tt-accent">Switches to agent mode</span>
  </span>
  <span class="tt-wrap">
    <button class="tt-trigger">Longer content</button>
    <span class="tt tt-top tt-wide">MagicBlocks agents run 24/7 and hand off to humans when the conversation gets complex.</span>
  </span>
</div>
.tt-demo { display: flex; gap: var(--s-5); align-items: center; flex-wrap: wrap; padding: var(--s-7) 0; }
.tt-wrap { position: relative; display: inline-block; }
.tt-trigger {
  background: var(--bg-paper); border: 1px solid var(--hair);
  padding: 8px 14px; border-radius: var(--r-md);
  font: 500 13.5px/1 var(--f-body); color: var(--fg); cursor: help;
}
.tt-trigger-accent { color: var(--accent-text); border-color: var(--accent); }
.tt {
  position: absolute; z-index: 50;
  background: var(--ink); color: var(--paper);
  padding: 7px 10px;
  font: 500 12.5px/1.4 var(--f-body);
  border-radius: var(--r-sm);
  box-shadow: var(--sh-2);
  pointer-events: none;
  opacity: 0; visibility: hidden;
  transform: translate(-50%, 4px);
  transition: opacity var(--dur-2) var(--ease), transform var(--dur-2) var(--ease), visibility var(--dur-2) var(--ease);
  white-space: nowrap;
}
.tt-top { left: 50%; bottom: calc(100% + 8px); }
.tt-top::after {
  content: ""; position: absolute; left: 50%; top: 100%;
  transform: translateX(-50%);
  border: 5px solid transparent; border-top-color: var(--ink);
}
.tt-wrap:hover .tt, .tt-trigger:focus + .tt {
  opacity: 1; visibility: visible;
  transform: translate(-50%, 0);
}
.tt-kbd {
  font: 600 11px/1 var(--f-mono);
  background: color-mix(in oklab, var(--paper) 15%, transparent);
  padding: 2px 5px; border-radius: var(--r-xs); margin-left: 4px;
  border: 1px solid color-mix(in oklab, var(--paper) 25%, transparent);
}
.tt-accent { background: var(--accent); }
.tt-accent::after { border-top-color: var(--accent); }
.tt-wide { white-space: normal; width: 240px; text-align: left; }
import { Tooltip } from "@magicblocksai/ui";

export default function Demo() {
  return (
    <Tooltip content="Copy link" side="top">
      <button>Hover me</button>
    </Tooltip>
  );
}

9.5 Popover

A richer cousin of the tooltip. Paper surface, pink eyebrow, title + paragraph + optional actions. Use for onboarding tours, in-context tips, and explain-this-feature moments.

Onboarding popover

.pop

Always anchored to a trigger with a little arrow. Keep copy tight — one paragraph max. One primary button + one dismiss link.

Tip

Set a goal for this agent

Agents work best when they have a single crisp objective. Start with "book a 15-minute call" and branch out from there.

<div class="pop-demo">
  <div class="pop">
    <div class="pop-arrow"></div>
    <div class="pop-head">
      <span class="pop-eyebrow mono">Tip</span>
      <button class="pop-close" aria-label="Dismiss"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M18 6 6 18M6 6l12 12"/></svg></button>
    </div>
    <h4 class="pop-title">Set a goal for this agent</h4>
    <p class="pop-desc">Agents work best when they have a single crisp objective. Start with "book a 15-minute call" and branch out from there.</p>
    <div class="pop-foot">
      <button class="pop-link">Skip tour</button>
      <button class="pop-btn">Got it</button>
    </div>
  </div>
</div>
.pop-demo { padding: var(--s-6) 0; }
.pop {
  position: relative;
  width: 320px;
  background: var(--bg-paper);
  border: 1px solid var(--hair);
  border-radius: var(--r-md);
  padding: var(--s-4);
  box-shadow: var(--sh-3);
}
.pop-arrow {
  position: absolute; top: -7px; left: 36px;
  width: 12px; height: 12px;
  background: var(--bg-paper);
  border-top: 1px solid var(--hair);
  border-left: 1px solid var(--hair);
  transform: rotate(45deg);
}
.pop-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--s-2); }
.pop-eyebrow { font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.1em; color: var(--accent-text); font-weight: 600; }
.pop-close { background: transparent; border: 0; color: var(--fg-dim); cursor: pointer; padding: 2px; margin: -2px; border-radius: var(--r-xs); }
.pop-close:hover { color: var(--fg); background: var(--bg-sunken); }
.pop-title { font: 600 15px/1.3 var(--f-display); color: var(--fg); letter-spacing: -0.005em; margin: 0 0 6px; }
.pop-desc { font: 400 13.5px/1.55 var(--f-body); color: var(--fg-soft); margin: 0 0 var(--s-4); }
.pop-foot { display: flex; justify-content: space-between; align-items: center; gap: var(--s-3); }
.pop-link { background: transparent; border: 0; color: var(--fg-soft); font: 500 13px/1 var(--f-body); cursor: pointer; padding: 6px 8px; margin-left: -8px; }
.pop-link:hover { color: var(--fg); }
.pop-btn {
  background: var(--accent); color: var(--paper); border: 0;
  font: 600 13px/1 var(--f-display); padding: 8px 14px;
  border-radius: var(--r-md); cursor: pointer;
  box-shadow: var(--sh-pink);
  transition: transform var(--dur-2) var(--ease);
}
.pop-btn:hover { transform: translateY(-1px); }
import { Popover, Button } from "@magicblocksai/ui";

export default function Demo() {
  return (
    <Popover
      content={
        <div>
          <strong>New: bulk reassign</strong>
          <p>Move many leads to a teammate in one click.</p>
        </div>
      }
    >
      <Button variant="secondary">Filters</Button>
    </Popover>
  );
}

9.7 Slide-over drawer

A right-anchored panel for detail views that don't warrant a full page. Same rules as a modal (backdrop, dismiss, focus trap) but slides in from the edge.

Contact drawer

.dr

Stacked layout: header, scroll body, sunken footer. Use for record previews, inspector panels, settings overlays.

<div class="dr-stage">
  <div class="dr-backdrop"></div>
  <aside class="dr" role="dialog" aria-labelledby="dr-title">
    <header class="dr-head">
      <div>
        <p class="dr-eyebrow mono">Contact</p>
        <h3 id="dr-title" class="dr-title">Alicia Chen</h3>
      </div>
      <button class="dr-close" aria-label="Close"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M18 6 6 18M6 6l12 12"/></svg></button>
    </header>
    <div class="dr-body">
      <dl class="dr-kv">
        <div><dt>Email</dt><dd>alicia@northpeak.co</dd></div>
        <div><dt>Phone</dt><dd><span class="mono">+1 415 555 0193</span></dd></div>
        <div><dt>Stage</dt><dd><span class="chip chip-blue">Qualified</span></dd></div>
        <div><dt>Owner</dt><dd>Jay Stockwell</dd></div>
      </dl>
      <h4 class="dr-section">Recent activity</h4>
      <ul class="dr-feed">
        <li><span class="dr-dot"></span> Replied by SMS <span class="dr-when mono">2h</span></li>
        <li><span class="dr-dot"></span> Opened follow-up email <span class="dr-when mono">4h</span></li>
        <li><span class="dr-dot"></span> Meeting booked for Thursday <span class="dr-when mono">1d</span></li>
      </ul>
    </div>
    <footer class="dr-foot">
      <button class="dr-cancel">Close</button>
      <button class="dr-primary">Send a message</button>
    </footer>
  </aside>
</div>
.dr-stage {
  position: relative;
  width: 100%; min-height: 520px;
  background: var(--bg-sunk);
  border: 1px dashed var(--hair);
  border-radius: var(--r-lg);
  overflow: hidden;
}
.dr-backdrop {
  position: absolute; inset: 0;
  background: color-mix(in oklab, var(--ink) 32%, transparent);
}
.dr {
  position: absolute; top: 0; right: 0; bottom: 0;
  width: min(440px, 92%);
  background: var(--bg-paper);
  border-left: 1px solid var(--hair);
  box-shadow: -20px 0 60px -20px color-mix(in oklab, var(--ink) 30%, transparent);
  display: flex; flex-direction: column;
  animation: dr-in var(--dur-3) var(--ease);
}
@keyframes dr-in { from { transform: translateX(100%); } to { transform: translateX(0); } }
.dr-head {
  display: flex; justify-content: space-between; align-items: flex-start;
  padding: var(--s-5) var(--s-5) var(--s-4);
  border-bottom: 1px solid var(--hair);
}
.dr-eyebrow { font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.1em; color: var(--fg-dim); font-weight: 600; }
.dr-title { font: 600 22px/1.2 var(--f-display); letter-spacing: -0.015em; color: var(--fg); margin: 4px 0 0; }
.dr-close { background: transparent; border: 0; color: var(--fg-dim); cursor: pointer; padding: 6px; margin: -6px; border-radius: var(--r-xs); }
.dr-close:hover { color: var(--fg); background: var(--bg-sunken); }
.dr-body { flex: 1; overflow: auto; padding: var(--s-4) var(--s-5); }
.dr-kv { margin: 0 0 var(--s-5); display: flex; flex-direction: column; gap: 10px; }
.dr-kv > div { display: grid; grid-template-columns: 90px 1fr; gap: var(--s-3); align-items: baseline; }
.dr-kv dt { font: 500 11px/1 var(--f-mono); text-transform: uppercase; letter-spacing: 0.08em; color: var(--fg-dim); }
.dr-kv dd { margin: 0; color: var(--fg); font-size: 14px; }
.dr-section { font: 500 11px/1 var(--f-mono); text-transform: uppercase; letter-spacing: 0.08em; color: var(--fg-dim); margin: 0 0 10px; }
.dr-feed { list-style: none; padding: 0; margin: 0; }
.dr-feed li {
  display: flex; align-items: center; gap: 10px;
  padding: 10px 0;
  font-size: 14px; color: var(--fg);
  border-bottom: 1px solid var(--hair-soft);
}
.dr-feed li:last-child { border-bottom: 0; }
.dr-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--accent); flex-shrink: 0; }
.dr-when { color: var(--fg-dim); margin-left: auto; font-size: 12.5px; }
.dr-foot {
  display: flex; justify-content: flex-end; gap: var(--s-3);
  padding: var(--s-4) var(--s-5);
  background: var(--bg-sunken);
  border-top: 1px solid var(--hair);
}
.dr-cancel { background: var(--bg-paper); border: 1px solid var(--hair); padding: 10px 16px; border-radius: var(--r-md); font: 500 14px/1 var(--f-display); color: var(--fg); cursor: pointer; }
.dr-primary { background: var(--accent); color: var(--paper); border: 0; padding: 10px 16px; border-radius: var(--r-md); font: 600 14px/1 var(--f-display); cursor: pointer; box-shadow: var(--sh-pink); }
import { useState } from "react";
import { Drawer, Button } from "@magicblocksai/ui";

export default function Demo() {
  const [open, setOpen] = useState(false);
  return (
    <>
      <Button onClick={() => setOpen(true)}>Open details</Button>
      <Drawer
        open={open}
        onClose={() => setOpen(false)}
        title="Alicia Chen"
        side="right"
      >
        Lead detail content goes here.
      </Drawer>
    </>
  );
}

9.8 Skeleton loaders

Shimmering placeholders that hold layout while content loads. Use instead of spinners when the shape of the result is predictable — it makes the wait feel shorter and prevents layout shift.

Skeleton

.skel

One shimmer block. Set width / height (number → px, string → any CSS length) and an optional radius.

<div class="skel" style="width: 16rem; height: 32px;"></div>
<div class="skel skel-r-md" style="width: 12rem; height: 48px;"></div>
<div class="skel skel-r-pill" style="width: 8rem; height: 20px;"></div>
import { Skeleton } from "@magicblocksai/ui";

<Skeleton width="16rem" height={32} />
<Skeleton width="12rem" height={48} radius="md" />
<Skeleton width="8rem" height={20} radius="pill" />

Skeleton text

.skel-text

lines shimmer rows for a paragraph or label; the last row is shortened.

<div class="skel-text">
  <div class="skel skel-line"></div>
  <div class="skel skel-line"></div>
  <div class="skel skel-line"></div>
</div>
import { SkeletonText } from "@magicblocksai/ui";

<SkeletonText lines={3} />

Skeleton avatar

.skel-av

Round shimmer for an avatar slot. Default 40px; set size for any diameter.

<div class="skel skel-av"></div>
<div class="skel skel-av" style="width: 64px; height: 64px;"></div>
import { SkeletonAvatar } from "@magicblocksai/ui";

<SkeletonAvatar />
<SkeletonAvatar size={64} />

Shimmer skeletons

.skel

Linear-gradient background with infinite position animation. Match the rough shape of the content that will replace it.

<div class="skel-demo">
  <div class="skel-card">
    <div class="skel skel-av"></div>
    <div class="skel-lines">
      <div class="skel skel-line" style="width: 45%;"></div>
      <div class="skel skel-line" style="width: 70%;"></div>
      <div class="skel skel-line" style="width: 62%;"></div>
    </div>
  </div>
  <div class="skel-card">
    <div class="skel-stack">
      <div class="skel skel-chip"></div>
      <div class="skel skel-line skel-title" style="width: 58%;"></div>
      <div class="skel skel-line" style="width: 90%;"></div>
      <div class="skel skel-line" style="width: 78%;"></div>
      <div class="skel skel-line" style="width: 40%;"></div>
    </div>
  </div>
</div>
.skel-demo { display: grid; grid-template-columns: 1fr 1fr; gap: var(--s-4); }
.skel-card {
  background: var(--bg-paper);
  border: 1px solid var(--hair);
  border-radius: var(--r-lg);
  padding: var(--s-4);
  display: flex;
  gap: var(--s-3);
}
.skel {
  background: linear-gradient(90deg,
    color-mix(in oklab, var(--fg) 6%, transparent) 0%,
    color-mix(in oklab, var(--fg) 10%, transparent) 50%,
    color-mix(in oklab, var(--fg) 6%, transparent) 100%);
  background-size: 200% 100%;
  border-radius: var(--r-sm);
  animation: skel-shimmer 1.8s ease-in-out infinite;
}
@keyframes skel-shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
.skel-av { width: 40px; height: 40px; border-radius: 50%; flex-shrink: 0; }
.skel-lines { flex: 1; display: flex; flex-direction: column; gap: 8px; justify-content: center; }
.skel-line { height: 10px; }
.skel-title { height: 14px; margin-bottom: 4px; }
.skel-chip { width: 64px; height: 22px; border-radius: var(--r-pill); }
.skel-stack { display: flex; flex-direction: column; gap: 8px; flex: 1; }
@media (max-width: 640px) { .skel-demo { grid-template-columns: 1fr; } }
import { SkeletonAvatar, SkeletonText } from "@magicblocksai/ui";

// Compose the primitives to match the content's shape.
<div className="skel-card">
  <SkeletonAvatar />
  <SkeletonText lines={3} />
</div>

9.9 Spinner & inline loader

Pink ring spinner for branded loading moments, ink variant for utility. Inline loader pairs the spinner with a word or two so the user knows what's happening.

Spinner sizes + inline

.spinner · .loader-inline

Default 18px, sm 12px (inline with text), lg 28px (centre-of-stage). Use skeletons, not spinners, when layout is predictable.

Default
Large
Ink on paper
Reaching Twilio…
<div class="spin-demo">
  <div class="spin-item">
    <span class="spinner"></span>
    <span class="spin-label">Default</span>
  </div>
  <div class="spin-item">
    <span class="spinner spinner-lg"></span>
    <span class="spin-label">Large</span>
  </div>
  <div class="spin-item">
    <span class="spinner spinner-ink"></span>
    <span class="spin-label">Ink on paper</span>
  </div>
  <div class="spin-item">
    <div class="loader-inline">
      <span class="spinner spinner-sm"></span>
      <span>Reaching Twilio…</span>
    </div>
  </div>
</div>
.spin-demo { display: flex; gap: var(--s-6); align-items: center; flex-wrap: wrap; }
.spin-item { display: flex; align-items: center; gap: var(--s-3); }
.spin-label { font: 500 12px/1 var(--f-mono); text-transform: uppercase; letter-spacing: 0.08em; color: var(--fg-dim); }
.spinner {
  width: 18px; height: 18px;
  border: 2px solid color-mix(in oklab, var(--accent) 20%, transparent);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: spin 0.8s linear infinite;
  display: inline-block;
}
.spinner-sm { width: 12px; height: 12px; border-width: 1.5px; }
.spinner-lg { width: 28px; height: 28px; border-width: 2.5px; }
.spinner-ink { border-color: color-mix(in oklab, var(--fg) 20%, transparent); border-top-color: var(--fg); }
@keyframes spin { to { transform: rotate(360deg); } }
.loader-inline {
  display: inline-flex; align-items: center; gap: 8px;
  padding: 8px 12px;
  background: var(--bg-warm);
  border: 1px solid var(--hair);
  border-radius: var(--r-pill);
  font: 500 13px/1 var(--f-body); color: var(--fg-soft);
}
// Spinner / loader-inline are CSS-only primitives — use raw markup.
export default function Demo() {
  return (
    <div className="spin-demo">
      <div className="spin-item">
        <span className="spinner" />
        <span className="spin-label">Default</span>
      </div>
      <div className="spin-item">
        <span className="spinner spinner-lg" />
        <span className="spin-label">Large</span>
      </div>
      <div className="spin-item">
        <span className="spinner spinner-ink" />
        <span className="spin-label">Ink on paper</span>
      </div>
      <div className="spin-item">
        <div className="loader-inline">
          <span className="spinner spinner-sm" />
          <span>Reaching Twilio…</span>
        </div>
      </div>
    </div>
  );
}

9.10 Inline message

Short feedback attached to a specific field or action — think "Saved", "Password too short", "Connection restored". Lives next to what it's describing; small, quiet, no icon by default.

Inline messages

.inline-msg

Four tones matching the alert palette, but a single row. Use for form feedback and micro-confirmations.

✓ Changes saved
✕ Email is already in use
! You have unsaved changes
i Verification email sent — check your inbox
<div class="inline-msg is-success">✓ Changes saved</div>
<div class="inline-msg is-error">✕ Email is already in use</div>
<div class="inline-msg is-warn">! You have unsaved changes</div>
<div class="inline-msg is-info">i Verification email sent</div>
.inline-msg { display: inline-flex; align-items: center; gap: var(--s-2);
  padding: 6px 10px; border-radius: var(--r-xs);
  font: 500 13px/1.4 var(--f-body); }
.inline-msg.is-success { color: var(--success-text); background: var(--success-soft); }
.inline-msg.is-error   { color: var(--error-text);   background: var(--error-soft);   }
.inline-msg.is-warn    { color: var(--warning-text); background: var(--warning-soft); }
.inline-msg.is-info    { color: var(--info-text);    background: var(--info-soft);    }
// Inline message is a CSS-only primitive — use raw markup.
export default function Demo() {
  return (
    <>
      <div className="inline-msg is-success">✓ Changes saved</div>
      <div className="inline-msg is-error">✕ Email is already in use</div>
      <div className="inline-msg is-warn">! You have unsaved changes</div>
      <div className="inline-msg is-info">i Verification email sent — check your inbox</div>
    </>
  );
}

9.11 Confirmation dialog

A short modal that stops a destructive action. Title states what's happening; body states the consequence. Primary button is the affirmative action, labelled as a verb — never just "OK".

Destructive confirm

.confirm

The primary button is pink for regular confirmations, red for destructive. Cancel is always ghost.

Delete this lead?

This will permanently remove Acme Corp and the 14 messages in this conversation. This can't be undone.

<div class="confirm" role="alertdialog"
     aria-labelledby="cf-title" aria-describedby="cf-body">
  <h3 class="confirm-title" id="cf-title">Delete this lead?</h3>
  <p class="confirm-body" id="cf-body">This can't be undone.</p>
  <div class="confirm-actions">
    <button class="btn btn-ghost">Cancel</button>
    <button class="btn confirm-danger">Delete lead</button>
  </div>
</div>
.confirm-backdrop { position: absolute; inset: 0;
  background: rgba(25,30,50,.35); backdrop-filter: blur(2px);
  border-radius: var(--r-md); }
.confirm { position: absolute; top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  width: calc(100% - 40px); max-width: 380px;
  background: var(--bg-paper); border-radius: var(--r-lg);
  box-shadow: var(--sh-3); padding: var(--s-5); }
.confirm-title { font: 700 18px/1.25 var(--f-display); color: var(--fg); margin: 0 0 var(--s-2); }
.confirm-body  { font: 400 14px/1.55 var(--f-body); color: var(--fg-soft); margin: 0 0 var(--s-4); }
.confirm-actions { display: flex; justify-content: flex-end; gap: var(--s-2); }
.confirm-danger { background: var(--error); color: var(--paper); border: 0; }
.confirm-danger:hover { background: color-mix(in oklab, var(--error) 85%, black); }
// Confirm dialog is a CSS-only primitive — use raw markup. For overlay behaviour,
// compose with <Modal> from @magicblocksai/ui.
export default function Demo() {
  return (
    <div className="confirm" role="alertdialog"
         aria-labelledby="cf-title" aria-describedby="cf-body">
      <h3 className="confirm-title" id="cf-title">Delete this lead?</h3>
      <p className="confirm-body" id="cf-body">
        This will permanently remove <strong>Acme Corp</strong> and the 14 messages
        in this conversation. This can’t be undone.
      </p>
      <div className="confirm-actions">
        <button className="btn btn-ghost">Cancel</button>
        <button className="btn confirm-danger">Delete lead</button>
      </div>
    </div>
  );
}

9.12 Snackbar

Bottom-centred transient message with an optional action. More utilitarian than a toast — ink surface, single line, one action button. Commonly paired with an "Undo" for reversible commits.

Snackbar + undo

.snackbar

Dark surface, paper text. The action is styled as a pink link. Snackbars auto-dismiss after 5s unless hovered.

Lead "Acme Corp" archived.
<div class="snackbar" role="status">
  <span class="snack-body">Lead "Acme Corp" archived.</span>
  <button class="snack-action">Undo</button>
</div>
.snackbar { display: inline-flex; align-items: center; gap: var(--s-4);
  padding: 12px 16px; background: var(--ink); color: var(--paper);
  border-radius: var(--r-md); box-shadow: var(--sh-3);
  font: 400 14px/1 var(--f-body); }
.snack-body { color: rgba(255,255,255,.92); }
.snack-action { background: transparent; border: 0;
  color: var(--accent-text); font: 600 13px/1 var(--f-body);
  text-transform: uppercase; letter-spacing: 0.06em; cursor: pointer; }
.snack-action:hover { color: #FE84A9; }
// Snackbar is a CSS-only primitive — use raw markup.
export default function Demo() {
  return (
    <div className="snackbar" role="status">
      <span className="snack-body">Lead “Acme Corp” archived.</span>
      <button className="snack-action">Undo</button>
    </div>
  );
}

9.13 Anatomy — toast

Every toast has exactly these five parts. Remove any of them and the component stops reading as 'a MagicBlocks notification' and starts reading as generic chrome.

Five required parts

Couldn't reach Twilio
Outbound SMS paused. View status
1
2
3
4
5
  1. 1
    Coloured left rail
    3px solid bar in the semantic colour — the only chroma signal on the surface.
  2. 2
    Tinted icon chip
    28px circle with a soft tint of the semantic colour and a stronger glyph tone.
  3. 3
    Title — 14px semibold display
    Bricolage 600, -0.005em tracking. Four to six words.
  4. 4
    Description + optional link
    13px body soft. Keep to one sentence. Action link in pink.
  5. 5
    Dismiss button
    Always dismissible for info/warn/success. Errors may auto-persist.

9.14 Bulk action bar

Sticky-bottom action bar that appears when one or more list rows or kanban cards are selected. Pinned to bottom-centre, ink surface, slides up + fades in. Pairs with a Snackbar (8.12) for "Undo" on destructive actions. Disabled actions stay visible but greyed; never hide an action you'd otherwise show.

Sticky bulk bar

.bulk-bar

Left: count + clear-selection. Centre: vertical divider. Right: action buttons. Show/hide with slide-up + fade. In real use, position fixed to viewport bottom with z-index: var(--z-sticky); here, contained in the demo stage.

↓ appears when rows are selected ↓
<div class="bulk-bar" role="toolbar" aria-label="Bulk actions">
  <span class="bb-count">7 selected</span>
  <button class="bb-clear" type="button">Clear</button>
  <span class="bb-spacer" aria-hidden="true"></span>
  <button class="bb-action" type="button">
    <svg viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true"><path d="M7 1v12M1 7h12" stroke-linecap="round"/></svg>
    <span>Assign</span>
  </button>
  <button class="bb-action" type="button">
    <svg viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true"><path d="M2 4l5 5 5-5" stroke-linecap="round" stroke-linejoin="round"/></svg>
    <span>Stage</span>
  </button>
  <button class="bb-action" type="button">
    <svg viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true"><circle cx="7" cy="7" r="5"/><path d="M7 4v3l2 2" stroke-linecap="round"/></svg>
    <span>Tag</span>
  </button>
  <button class="bb-action" type="button">
    <svg viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true"><path d="M2 4h10l-1 8H3L2 4zM5 4V2h4v2" stroke-linejoin="round"/></svg>
    <span>Archive</span>
  </button>
  <button class="bb-action is-danger" type="button">
    <svg viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true"><path d="M3 4h8m-7 0v8h6V4M5 4V2h4v2" stroke-linejoin="round" stroke-linecap="round"/></svg>
    <span>Delete</span>
  </button>
</div>

<!-- In real use, this is fixed to viewport bottom: -->
<style>
  .bulk-bar.is-pinned {
    position: fixed; bottom: 24px; left: 50%;
    transform: translateX(-50%);
    z-index: var(--z-sticky);
    animation: bulk-bar-slide-up 0.18s var(--ease) backwards;
  }
  @keyframes bulk-bar-slide-up {
    from { opacity: 0; transform: translate(-50%, 8px); }
    to   { opacity: 1; transform: translate(-50%, 0); }
  }
  @media (prefers-reduced-motion: reduce) {
    .bulk-bar.is-pinned { animation: none; }
  }
</style>
.bulk-bar { display: flex; align-items: center; gap: var(--s-4);
  padding: 10px 16px; background: var(--ink); color: var(--paper);
  border-radius: var(--r-pill); box-shadow: var(--sh-3); }

.bulk-bar .bb-count { padding: 6px 12px;
  background: rgba(255,255,255,0.08); border-radius: var(--r-pill); }
.bulk-bar .bb-spacer { flex: 1; height: 18px;
  border-left: 1px solid rgba(244,236,228,0.18); margin-left: var(--s-2); }
.bulk-bar .bb-action { background: transparent; border: 0; cursor: pointer;
  padding: 8px 12px; border-radius: var(--r-pill); }
.bulk-bar .bb-action:hover { background: rgba(255,255,255,0.08); }
.bulk-bar .bb-action.is-danger { color: #FFB8B8; }

@media (max-width: 640px) {
  .bulk-bar .bb-action span { display: none; }  /* icon-only on mobile */
}
import { useState } from "react";
import { BulkBar, Button } from "@magicblocksai/ui";

export default function Demo() {
  const [selected, setSelected] = useState(["a", "b", "c"]);
  if (selected.length === 0) return null;
  return (
    <BulkBar
      count={selected.length}
      onClear={() => setSelected([])}
      actions={
        <>
          <Button variant="secondary" size="sm">Assign</Button>
          <Button variant="secondary" size="sm">Tag</Button>
          <Button variant="danger" size="sm">Delete</Button>
        </>
      }
    />
  );
}

9.15 DialogHost

Imperative dialog API on top of <Modal>. Mount one <DialogHost /> at the app root, then call confirm(), alert(), or prompt() from anywhere — route loaders, network handlers, plain utilities. Each call returns a promise that resolves when the user confirms, cancels, or dismisses via Esc / backdrop click. Multiple dialogs stack and resolve independently.

DialogHost

.dialog-host

The host itself is invisible (it’s a display: contents wrapper). What you see is what the host renders when confirm({ destructive: true }) is called — a small <Modal size="sm"> with a title, a .dialog-body paragraph, and a Cancel + danger Confirm pair.

<!-- Mount once at the app root -->
<div class="dialog-host">
  <!-- DialogHost renders an open <Modal> per pending confirm/alert/prompt: -->
  <div class="modal-backdrop"></div>
  <div class="modal" role="dialog" aria-labelledby="dh-title" data-size="sm">
    <div class="modal-head">
      <h2 id="dh-title" class="modal-title">Delete this segment?</h2>
      <button class="modal-close" aria-label="Close">×</button>
    </div>
    <div class="modal-body">
      <p class="dialog-body">Members stay in your contact list. The segment definition is removed.</p>
    </div>
    <div class="modal-foot">
      <button class="btn btn-ghost">Cancel</button>
      <button class="btn btn-danger">Delete</button>
    </div>
  </div>
</div>
.dialog-host { display: contents; }
import { DialogHost, confirm } from "@magicblocksai/ui";

// Mount once at the app root (e.g. app/layout.tsx):
<body>
  {children}
  <DialogHost />
</body>

// Then call confirm() from anywhere — components, loaders, utilities:
async function handleDelete() {
  const ok = await confirm({
    title: "Delete this segment?",
    body: "Members stay in your contact list. The segment definition is removed.",
    confirmLabel: "Delete",
    destructive: true,
  });
  if (ok) await deleteSegment();
}

// alert() and prompt() are paired imperative helpers — same shape.
await alert({ title: "Saved", body: "Your changes are live." });
const name = await prompt({ title: "Rename segment", defaultValue: "High intent" });

9.16 Accordion

Single-section disclosure card — a chevron-led trigger with title + caption + optional right-aligned meta slot, and a hidden panel that flips on click. Compose multiple inside <AccordionGroup> for single-open or allowMultiple stacks. Backs the Guardrails Off/On panel stack, the per-block Advanced overrides (First action / Persona / Brain / Guardrails / Settings), and the Chat-appearance style-category panels.

AccordionGroup — single-open

.accordion-group

Four collapsible panels for the Guardrails settings page. The first panel is open by default; clicking another trigger closes the first and opens the new one. Pass allowMultiple on the group for parallel-open behaviour.

PII tokens matched against your active redaction rules are replaced with <redacted> before any message is stored or shown to a teammate.

<div class="accordion-group">
  <div class="accordion is-open">
    <button class="accordion-trigger" aria-expanded="true" aria-controls="redaction-panel">
      <span class="accordion-chevron" aria-hidden="true"></span>
      <span class="accordion-title-block">
        <span class="accordion-title">Redaction</span>
        <span class="accordion-caption">Strip names, emails, and phone numbers from logs.</span>
      </span>
      <span class="accordion-meta"><span class="badge">On</span></span>
    </button>
    <div id="redaction-panel" role="region" class="accordion-panel">
      <div class="accordion-panel-inner">
        <p>PII tokens matched against your active redaction rules…</p>
      </div>
    </div>
  </div>
  <!-- …more .accordion children, each .accordion-panel with [hidden] when closed -->
</div>
.accordion-group { display: flex; flex-direction: column; gap: var(--s-3); }
import { Accordion, AccordionGroup, Badge } from "@magicblocksai/ui";

<AccordionGroup defaultValue={["redaction"]}>
  <Accordion
    itemId="redaction"
    title="Redaction"
    caption="Strip names, emails, and phone numbers from logs."
    meta={<Badge tone="neutral">On</Badge>}
  >
    <p>PII tokens matched against your active redaction rules are replaced
       with &lt;redacted&gt; before any message is stored or shown.</p>
  </Accordion>
  <Accordion itemId="monitor"   title="Rules monitor"          caption="Watch for blocked terms in real time."   meta={<Badge tone="neutral">Off</Badge>} />
  <Accordion itemId="jailbreak" title="Jailbreak prevention"   caption="Detect prompt-injection attempts."        meta={<Badge tone="neutral">On</Badge>} />
  <Accordion itemId="moderation" title="Moderation"            caption="Flag unsafe outputs before they reach the user." meta={<Badge tone="neutral">On</Badge>} />
</AccordionGroup>

// Standalone single-panel use (no group):
<Accordion title="Persona override" caption="Use a different persona for this block." defaultOpen={false}>
  …form…
</Accordion>

9.17 FilterPopover

A trigger button with an active-count pill, opening a card-shaped popover with title / body / footer slots. The body is consumer-owned — drop in <QueryBuilder>, a stack of selects, a search box, anything. The popover handles trigger / outside-click / Esc / focus return; nothing else.

FilterPopover

.filter-popover

An open FilterPopover anchored beneath its trigger, with two active filters surfaced as a count pill on the button. The popover head holds title + caption; the footer holds Clear + Apply.

<div class="filter-popover is-open is-align-bottom-end">
  <button class="filter-popover-trigger" aria-haspopup="dialog" aria-expanded="true">
    <svg>…filter glyph…</svg>
    <span>Filter</span>
    <span class="filter-popover-count">2</span>
  </button>
  <div class="filter-popover-panel" role="dialog">
    <div class="filter-popover-head">
      <div class="filter-popover-title">Filter contacts</div>
      <div class="filter-popover-caption">Add one or more conditions.</div>
    </div>
    <div class="filter-popover-body">
      <!-- body content (consumer-owned) -->
    </div>
    <div class="filter-popover-footer">
      <button class="btn btn-ghost">Clear</button>
      <button class="btn btn-primary">Apply</button>
    </div>
  </div>
</div>
.filter-popover { display: inline-block; position: relative; }

.filter-popover-trigger {
  appearance: none;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 10px;
  border: 1px solid var(--hair);
  border-radius: var(--r-sm);
  background: var(--bg-paper);
  color: var(--fg);
  font: 500 13px/1 var(--f-body);
  cursor: pointer;
  transition: background var(--dur-2) var(--ease);
}

.filter-popover-trigger:hover:not(:disabled) { background: var(--bg-warm); }

.filter-popover-trigger:disabled { opacity: 0.55; cursor: not-allowed; }

.filter-popover.is-open .filter-popover-trigger {
  background: var(--bg-warm);
  border-color: var(--accent);
}

.filter-popover-count {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 18px;
  height: 18px;
  padding: 0 6px;
  border-radius: 999px;
  background: var(--accent);
  color: var(--paper);
  font: 600 11px/1 var(--f-mono);
}

.filter-popover-panel {
  position: absolute;
  top: calc(100% + 6px);
  z-index: 50;
  width: min(420px, calc(100vw - 32px));
  background: var(--bg-paper);
  border: 1px solid var(--hair);
  border-radius: var(--r-md);
  box-shadow: 0 12px 32px color-mix(in oklab, var(--ink) 14%, transparent);
  display: flex;
  flex-direction: column;
}

.filter-popover.is-align-bottom-end .filter-popover-panel { right: 0; }

.filter-popover.is-align-bottom-start .filter-popover-panel { left: 0; }

.filter-popover-head {
  padding: var(--s-3) var(--s-4);
  border-bottom: 1px solid var(--hair-soft);
}

.filter-popover-title { font: 500 14px/1.3 var(--f-body); color: var(--fg); }

.filter-popover-caption { font: 400 12px/1.4 var(--f-body); color: var(--fg-soft); margin-top: 2px; }

.filter-popover-body { padding: var(--s-3) var(--s-4); }

.filter-popover-footer {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--s-2);
  padding: var(--s-3) var(--s-4);
  border-top: 1px solid var(--hair-soft);
}
import { useState } from "react";
import { Button, FilterPopover, QueryBuilder } from "@magicblocksai/ui";
import type { QueryCondition, QueryField } from "@magicblocksai/ui";

const fields: QueryField[] = [
  { id: "stage", label: "Stage", type: "select",
    options: [{ value: "new", label: "New" }, { value: "open", label: "Open" }] },
  { id: "owner", label: "Owner", type: "text" },
];

function Example() {
  const [conditions, setConditions] = useState<QueryCondition[]>([]);
  return (
    <FilterPopover
      triggerLabel="Filter"
      activeCount={conditions.length}
      title="Filter contacts"
      caption="Add one or more conditions."
      footer={
        <>
          <Button variant="ghost" onClick={() => setConditions([])}>Clear</Button>
          <Button>Apply</Button>
        </>
      }
    >
      <QueryBuilder
        fields={fields}
        value={conditions}
        onValueChange={setConditions}
      />
    </FilterPopover>
  );
}

9.18 TwoPathChooserDialog

The platform’s recurring “pick one of two creation paths” shape — Key Fact: Template vs Custom, Tools & MCP: HTTP Function vs MCP Server, Forms: Custom vs Snippet, future block-template chooser. Two equal-weight cards on a centre-aligned heading. This is the body, not the dialog itself; compose with <Modal> so the consumer keeps full control of open state, focus return, and dismiss.

TwoPathChooserDialog

.two-path-chooser

A two-card chooser rendered inside a modal frame. Each card is a single button; onSelect fires the consumer-supplied action. Use the footer slot for a small badge (“Recommended”) on the preferred path.

<!-- Drop inside a <div class="modal"> — the chooser is the body, not the dialog -->
<div class="two-path-chooser">
  <div class="two-path-chooser-head">
    <h2 class="two-path-chooser-title">Create a new tool</h2>
    <p class="two-path-chooser-caption">Pick what kind of connector you want to wire up.</p>
  </div>
  <div class="two-path-chooser-grid">
    <button class="two-path-chooser-card">
      <span class="two-path-chooser-icon"><svg>…</svg></span>
      <span class="two-path-chooser-label">HTTP Function</span>
      <span class="two-path-chooser-description">Call any HTTPS endpoint with a JSON body.</span>
    </button>
    <button class="two-path-chooser-card">
      <span class="two-path-chooser-icon"><svg>…</svg></span>
      <span class="two-path-chooser-label">MCP Server</span>
      <span class="two-path-chooser-description">Register an MCP server and discover its tools.</span>
      <span class="two-path-chooser-footer">Recommended</span>
    </button>
  </div>
</div>
.two-path-chooser { display: flex; flex-direction: column; gap: var(--s-4); }

.two-path-chooser-head { display: flex; flex-direction: column; gap: 6px; text-align: center; }

.two-path-chooser-title { font: 600 18px/1.3 var(--f-display); color: var(--fg); margin: 0; }

.two-path-chooser-caption { font: 400 14px/1.5 var(--f-body); color: var(--fg-soft); margin: 0; }

.two-path-chooser-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--s-4);
}

.two-path-chooser-card {
  appearance: none;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--s-2);
  padding: var(--s-5);
  border: 1px solid var(--hair);
  border-radius: var(--r-lg);
  background: var(--bg-paper);
  color: var(--fg);
  cursor: pointer;
  text-align: center;
  transition: border-color var(--dur-2) var(--ease),
              transform var(--dur-2) var(--ease),
              box-shadow var(--dur-2) var(--ease);
}

.two-path-chooser-card:hover:not(:disabled) {
  border-color: var(--accent);
  transform: translateY(-2px);
  box-shadow: 0 6px 16px color-mix(in oklab, var(--ink) 10%, transparent);
}

.two-path-chooser-card:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.two-path-chooser-card.is-disabled,
.two-path-chooser-card:disabled { opacity: 0.5; cursor: not-allowed; }

.two-path-chooser-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 48px; height: 48px;
  border-radius: var(--r-md);
  background: var(--bg-warm);
  color: var(--fg);
  margin-bottom: var(--s-2);
}

.two-path-chooser-label { font: 600 15px/1.3 var(--f-display); }

.two-path-chooser-description { font: 400 13px/1.5 var(--f-body); color: var(--fg-soft); }

.two-path-chooser-footer {
  font: 600 10.5px/1 var(--f-mono);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--accent);
  margin-top: var(--s-1);
}

@media (max-width: 540px) {
  .two-path-chooser-grid { grid-template-columns: 1fr; }
}

@media (prefers-reduced-motion: reduce) {
  .two-path-chooser-card { transition: none; }
  .two-path-chooser-card:hover:not(:disabled) { transform: none; }
}
import { useState } from "react";
import { Modal, TwoPathChooserDialog } from "@magicblocksai/ui";

declare function openHttpEditor(): void;
declare function openMcpEditor(): void;

function Example() {
  const [open, setOpen] = useState(false);
  return (
    <Modal open={open} onOpenChange={setOpen}>
      <TwoPathChooserDialog
        title="Create a new tool"
        caption="Pick what kind of connector you want to wire up."
        options={[
          {
            id: "http",
            label: "HTTP Function",
            description: "Call any HTTPS endpoint with a JSON body.",
            onSelect: () => { setOpen(false); openHttpEditor(); },
          },
          {
            id: "mcp",
            label: "MCP Server",
            description: "Register an MCP server and discover its tools.",
            footer: "Recommended",
            onSelect: () => { setOpen(false); openMcpEditor(); },
          },
        ]}
      />
    </Modal>
  );
}

9.19 SourceFreshnessBanner

Status banner for a synced source — a Knowledge collection’s “Sitemap Changes Found / Setup Detector”, a Webhook endpoint’s last-ping result, a Connection’s freshness pill. Sits above the detail content of the synced entity. Four tones: fresh (up to date), stale (re-check overdue), changed (new content detected), error (source unreachable).

SourceFreshnessBanner

.source-freshness

Two tones stacked: changed (pink-tinted, accent dot, primary CTA) and stale (warning-tinted, amber dot, ghost CTA). Each row is grid-laid: status chip on the left, title + caption + last-checked timestamp in the middle, right-aligned action button.

Changes detected
Sitemap changes detected
8 new pages found at acme.com since last sync.
Last checked 2 hours ago
Stale
Webhook hasn’t fired in a while
Last successful ping was 11 days ago.
Last checked 30 minutes ago
<div class="source-freshness is-tone-changed" role="status">
  <div class="source-freshness-status">
    <span class="source-freshness-dot" aria-hidden="true"></span>
    <span class="source-freshness-tone-label">Changes detected</span>
  </div>
  <div class="source-freshness-body">
    <div class="source-freshness-title">Sitemap changes detected</div>
    <div class="source-freshness-caption">8 new pages found at acme.com since last sync.</div>
    <div class="source-freshness-meta">Last checked 2 hours ago</div>
  </div>
  <div class="source-freshness-action">
    <button class="btn btn-primary">Set up detector</button>
  </div>
</div>

<!-- Other tones: is-tone-fresh · is-tone-stale · is-tone-error -->
.source-freshness {
  display: grid;
  grid-template-columns: auto 1fr auto;
  gap: var(--s-3);
  align-items: center;
  padding: var(--s-3) var(--s-4);
  border-radius: var(--r-md);
  border: 1px solid var(--hair);
  background: var(--bg-paper);
}

.source-freshness.is-tone-fresh {
  background: color-mix(in oklab, var(--success-soft, #DBF5E6) 60%, var(--bg-paper));
}

.source-freshness.is-tone-stale {
  background: color-mix(in oklab, var(--warning-soft, #FFF6DE) 70%, var(--bg-paper));
}

.source-freshness.is-tone-changed {
  background: color-mix(in oklab, var(--accent) 8%, var(--bg-paper));
  border-color: color-mix(in oklab, var(--accent) 30%, var(--hair));
}

.source-freshness.is-tone-error {
  background: color-mix(in oklab, var(--error-soft, #FCE4E0) 70%, var(--bg-paper));
}

.source-freshness-status {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 10px;
  border-radius: 999px;
  background: var(--bg-paper);
  border: 1px solid var(--hair);
}

.source-freshness-dot {
  width: 8px;
  height: 8px;
  border-radius: 999px;
  background: var(--fg-faint);
}

.source-freshness.is-tone-fresh .source-freshness-dot { background: var(--success-text, #1A6A3F); }

.source-freshness.is-tone-stale .source-freshness-dot { background: var(--warning, #F9AD03); }

.source-freshness.is-tone-changed .source-freshness-dot { background: var(--accent); }

.source-freshness.is-tone-error .source-freshness-dot { background: var(--error-text, #8B2417); }

.source-freshness-tone-label {
  font: 600 11px/1 var(--f-mono);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--fg-soft);
}

.source-freshness-body { display: flex; flex-direction: column; gap: 2px; min-width: 0; }

.source-freshness-title { font: 500 13.5px/1.3 var(--f-body); color: var(--fg); }

.source-freshness-caption { font: 400 12.5px/1.4 var(--f-body); color: var(--fg-soft); }

.source-freshness-meta { font: 400 11.5px/1 var(--f-mono); color: var(--fg-faint); margin-top: 2px; }

.source-freshness-action { display: inline-flex; align-items: center; gap: var(--s-2); }
import { Button, SourceFreshnessBanner } from "@magicblocksai/ui";

<SourceFreshnessBanner
  tone="changed"
  title="Sitemap changes detected"
  caption="8 new pages found at acme.com since last sync."
  lastCheckedAt="2 hours ago"
  action={<Button>Set up detector</Button>}
/>

<SourceFreshnessBanner
  tone="stale"
  title="Webhook hasn't fired in a while"
  caption="Last successful ping was 11 days ago."
  lastCheckedAt="30 minutes ago"
  action={<Button variant="ghost">Re-check</Button>}
/>