Everything you need to evaluate MagicBlocks on a small team.
- Up to 3 seats
- 1,000 records
- Community support
- Standard integrations
Chapter 22 · Operator · Plans & billing
The operator-side surface for plans, usage, invoices, and payment methods. Six components compose the kit’s “everything billing” workflow: plan tier cards, usage meters, invoice rows, payment method displays, the billing history table, and a drop-in page wrapper.
A plan-tier card composed of name + price/period + description + feature list + CTA. Three intensities cover the common 3-tier pricing layout: intent-muted for the low-end tier, intent-accent for the highlighted recommended tier (accent border + tinted background), and intent-ink for the high-end enterprise tier (ink background + paper text). An optional pill in the top-right corner marks the recommended or current plan. Below 480px the grid stacks; the card itself just flexes to container width.
Three side-by-side cards demonstrating the canonical pricing layout. The Pro card carries intent-accent and a “Recommended” pill; the Enterprise card uses intent-ink for the high-end inversion. Feature bullets render with a small var(--success)-tinted check glyph; the ink variant flips the check to var(--paper) for contrast. CTA pins to the bottom of each card so unequal feature counts don’t misalign the action row.
Everything you need to evaluate MagicBlocks on a small team.
Everything in Free, plus the power-user surface area most teams need.
For organisations that need procurement, compliance, and dedicated support.
<div class="plan-card-grid">
<article class="plan-card intent-muted">
<div class="plan-card-head">
<div class="plan-card-name">Free</div>
<div class="plan-card-price">$0</div>
<div class="plan-card-period">billed monthly</div>
</div>
<p class="plan-card-desc">Everything you need…</p>
<ul class="plan-card-features">
<li><svg class="plan-card-feature-check">…</svg><span>Up to 3 seats</span></li>
…
</ul>
<div class="plan-card-cta">
<button type="button" class="btn btn-ghost">Get started</button>
</div>
</article>
<article class="plan-card intent-accent">
<span class="plan-card-pill">Recommended</span>
…
</article>
<article class="plan-card intent-ink">…</article>
</div>
.plan-card-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: var(--s-4);
align-items: stretch;
}
.plan-card {
position: relative;
display: flex;
flex-direction: column;
gap: var(--s-4);
padding: var(--s-5);
border-radius: var(--r-md);
border: 1px solid var(--hair);
background: var(--bg-paper);
min-height: 360px;
}
.plan-card.intent-muted {
background: var(--bg-paper);
border-color: var(--hair);
}
.plan-card.intent-accent {
background: color-mix(in oklab, var(--accent) 6%, var(--bg-paper));
border-color: var(--accent);
border-width: 1.5px;
}
.plan-card.intent-ink {
background: var(--ink);
border-color: var(--ink);
color: var(--paper);
}
.plan-card-pill {
position: absolute;
top: var(--s-3);
right: var(--s-3);
display: inline-flex;
align-items: center;
padding: 4px 10px;
border-radius: 999px;
background: var(--accent);
color: var(--paper);
font: 600 10.5px/1 var(--f-mono);
letter-spacing: 0.06em;
text-transform: uppercase;
}
.plan-card-head {
display: flex;
flex-direction: column;
gap: var(--s-1);
}
.plan-card-name {
font: 600 16px/1.25 var(--f-display);
color: var(--fg);
}
.plan-card-price {
font: 700 32px/1.05 var(--f-display);
color: var(--fg);
font-variant-numeric: tabular-nums;
}
.plan-card-period {
font: 500 12px/1.3 var(--f-mono);
color: var(--fg-soft);
}
.plan-card-desc {
margin: 0;
font: 400 14px/1.5 var(--f-body);
color: var(--fg-soft);
}
.plan-card-features {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: var(--s-2);
}
.plan-card-features li {
display: flex;
align-items: flex-start;
gap: var(--s-2);
font: 400 14px/1.45 var(--f-body);
color: var(--fg);
}
.plan-card-feature-check {
flex-shrink: 0;
width: 16px;
height: 16px;
margin-top: 2px;
color: var(--success);
}
.plan-card-cta {
margin-top: auto;
display: flex;
}
.plan-card-cta > * {
flex: 1;
}
.plan-card.intent-ink .plan-card-name,
.plan-card.intent-ink .plan-card-price {
color: var(--paper);
}
.plan-card.intent-ink .plan-card-period,
.plan-card.intent-ink .plan-card-desc {
color: color-mix(in oklab, var(--paper) 70%, transparent);
}
.plan-card.intent-ink .plan-card-features li {
color: color-mix(in oklab, var(--paper) 90%, transparent);
}
/* …additional rules trimmed for brevity — see _shared.css */
import { PlanCard } from "@magicblocksai/ui";
<div className="plan-card-grid">
<PlanCard
name="Free"
price="$0"
period="billed monthly"
description="Everything you need to evaluate MagicBlocks on a small team."
features={[
"Up to 3 seats",
"1,000 records",
"Community support",
"Standard integrations",
]}
cta={<button type="button" className="btn btn-ghost">Get started</button>}
/>
<PlanCard
name="Pro"
price="$29"
period="per seat / month"
description="Everything in Free, plus the power-user surface area most teams need."
features={[
"Unlimited seats",
"Unlimited records",
"Priority support",
"Custom domains",
"SSO & SCIM",
"Audit log access",
]}
cta={<button type="button" className="btn btn-primary">Start free trial</button>}
intent="accent"
pill="Recommended"
/>
<PlanCard
name="Enterprise"
price="Custom"
period="billed annually"
description="For organisations that need procurement, compliance, and dedicated support."
features={[
"Everything in Pro",
"Dedicated success manager",
"SOC 2 + HIPAA",
"99.99% SLA",
"Custom contracts",
]}
cta={<button type="button" className="btn btn-ink">Contact sales</button>}
intent="ink"
/>
</div>
A labeled progress bar with current/limit numbers, an overage indicator, and an optional projection line. Pure presentational — the visual state derives from current / limit: at or above 100% renders .is-over with an var(--error) tint, at or above 80% renders .is-approaching with a var(--warning) tint, and below 80% uses the default var(--accent) fill. The bar fill animates from 0 to its target width on mount via transition: width var(--dur-3); prefers-reduced-motion: reduce suppresses the transition.
Three meters stacked in a .usage-meter-stack wrapper. API calls sits at ~31% of the limit (healthy, default accent fill). Storage sits at 88% (approaching, warning tint). Seats sits at 130% (over, error tint with an .usage-meter-overage label reporting the delta).
<div class="usage-meter-stack">
<div class="usage-meter">
<div class="usage-meter-head">
<span class="usage-meter-label">API calls this month</span>
<span class="usage-meter-numbers">31,247 / 100,000 calls</span>
</div>
<div class="usage-meter-bar">
<span class="usage-meter-fill" style="width:31.247%"></span>
</div>
</div>
<div class="usage-meter is-approaching">
<div class="usage-meter-head">
<span class="usage-meter-label">Storage</span>
<span class="usage-meter-numbers">8.8 / 10.0 GB</span>
</div>
<div class="usage-meter-bar">
<span class="usage-meter-fill" style="width:88%"></span>
</div>
</div>
<div class="usage-meter is-over">
<div class="usage-meter-head">
<span class="usage-meter-label">Seats</span>
<span class="usage-meter-numbers">26 / 20 seats</span>
</div>
<div class="usage-meter-bar">
<span class="usage-meter-fill" style="width:100%"></span>
</div>
<span class="usage-meter-overage">
<svg class="usage-meter-overage-icon">…</svg>Over by 6 seats
</span>
</div>
</div>
.usage-meter-stack {
display: flex;
flex-direction: column;
gap: var(--s-4);
max-width: 480px;
}
.usage-meter {
display: flex;
flex-direction: column;
gap: var(--s-2);
}
.usage-meter-head {
display: flex;
flex-direction: row;
align-items: baseline;
justify-content: space-between;
gap: var(--s-2);
}
.usage-meter-label {
font: 500 14px/1.3 var(--f-body);
color: var(--fg);
}
.usage-meter-numbers {
font: 500 13px/1.3 var(--f-mono);
color: var(--fg-soft);
font-variant-numeric: tabular-nums;
}
.usage-meter-bar {
position: relative;
height: 8px;
border-radius: 999px;
background: var(--bg-sunk);
overflow: hidden;
}
.usage-meter-fill {
position: absolute;
top: 0;
left: 0;
bottom: 0;
width: 0;
background: var(--accent);
border-radius: 999px;
transition: width var(--dur-3) var(--ease);
}
.usage-meter.is-approaching .usage-meter-fill {
background: var(--warning);
}
.usage-meter.is-over .usage-meter-fill {
background: var(--error);
}
.usage-meter-projection {
position: absolute;
top: 0;
bottom: 0;
width: 1px;
border-left: 1px dashed var(--fg-dim);
pointer-events: none;
}
.usage-meter-overage {
display: inline-flex;
align-items: center;
gap: 6px;
font: 500 12px/1.3 var(--f-body);
color: var(--error-text);
}
.usage-meter-overage-icon {
flex-shrink: 0;
width: 14px;
height: 14px;
color: var(--error-text);
}
@media (max-width: 480px) {
.usage-meter-head {
flex-direction: column;
align-items: flex-start;
gap: var(--s-1);
}
}
@media (prefers-reduced-motion: reduce) {
.usage-meter-fill {
transition: none;
}
}
import { UsageMeter } from "@magicblocksai/ui";
<div className="usage-meter-stack">
<UsageMeter
label="API calls this month"
current={31247}
limit={100000}
unit="calls"
/>
<UsageMeter
label="Storage"
current={8.8}
limit={10}
unit="GB"
format={(n) => n.toFixed(1)}
/>
<UsageMeter
label="Seats"
current={26}
limit={20}
unit="seats"
/>
</div>
A single invoice row primitive — date + number + amount + status pill + optional download icon. Used inside <BillingHistoryTable> (section 24.5) or composed standalone inside a .invoice-row-list wrapper for custom table-shaped layouts. The status pill colour-codes four states: paid (success tint), open (accent tint), void (sunken neutral), uncollectible (error tint). Below 480px the grid reflows to two rows; the download icon, when present, spans both rows on the right edge.
Four invoice rows stacked inside a .invoice-row-list wrapper. The first two are paid (success pill + download icon), the third is still open (accent pill, no download — pending), and the fourth is void (sunken pill, no actions). The wrapper provides the hairline border + rounded corners; the per-row bottom hairlines align to the radius via overflow: hidden on the wrapper.
<div class="invoice-row-list">
<div class="invoice-row">
<span class="invoice-row-date">14 May 2026</span>
<span class="invoice-row-number">INV-2026-0142</span>
<span class="invoice-row-amount">$292.00</span>
<span class="invoice-row-status is-paid">Paid</span>
<a class="invoice-row-download" href="/invoices/INV-2026-0142.pdf"
aria-label="Download invoice INV-2026-0142 as PDF">
<svg>…</svg>
</a>
</div>
<div class="invoice-row">
<span class="invoice-row-date">14 Mar 2026</span>
<span class="invoice-row-number">INV-2026-0094</span>
<span class="invoice-row-amount">$32.00</span>
<span class="invoice-row-status is-open">Open</span>
</div>
<div class="invoice-row">
<span class="invoice-row-date">14 Feb 2026</span>
<span class="invoice-row-number">INV-2026-0070</span>
<span class="invoice-row-amount">$292.00</span>
<span class="invoice-row-status is-void">Void</span>
</div>
</div>
.invoice-row-list {
border: 1px solid var(--hair);
border-radius: var(--r-md);
overflow: hidden;
background: var(--bg-paper);
}
.invoice-row {
display: grid;
grid-template-columns: auto 1fr auto auto auto;
align-items: center;
gap: var(--s-3);
padding: var(--s-3) var(--s-4);
border-bottom: 1px solid var(--hair);
transition: background var(--dur-2) var(--ease);
}
.invoice-row:last-child {
border-bottom: 0;
}
.invoice-row.is-interactive {
cursor: pointer;
}
.invoice-row.is-interactive:hover {
background: var(--bg-paper);
}
.invoice-row-date {
font: 500 13px/1.3 var(--f-mono);
color: var(--fg-soft);
white-space: nowrap;
}
.invoice-row-number {
font: 400 14px/1.3 var(--f-body);
color: var(--fg);
}
.invoice-row-amount {
font: 600 14px/1.3 var(--f-body);
color: var(--fg);
font-variant-numeric: tabular-nums;
text-align: right;
min-width: 80px;
}
.invoice-row-status {
display: inline-flex;
align-items: center;
padding: 2px 8px;
border-radius: var(--r-pill);
font: 600 11px/1.4 var(--f-mono);
letter-spacing: 0.04em;
text-transform: uppercase;
white-space: nowrap;
}
.invoice-row-status.is-paid {
background: var(--success-soft);
color: var(--success-text);
}
.invoice-row-status.is-open {
background: var(--accent-soft);
color: var(--accent-text);
}
.invoice-row-status.is-void {
background: var(--bg-sunk);
color: var(--fg-dim);
}
.invoice-row-status.is-uncollectible {
background: var(--error-soft);
color: var(--error-text);
}
.invoice-row-download {
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border-radius: var(--r-sm);
color: var(--fg-soft);
background: transparent;
text-decoration: none;
transition: background var(--dur-2) var(--ease), color var(--dur-2) var(--ease);
}
.invoice-row-download:hover {
background: var(--bg-sunk);
color: var(--fg);
}
.invoice-row-download svg {
width: 16px;
height: 16px;
}
/* …additional rules trimmed for brevity — see _shared.css */
import { InvoiceRow } from "@magicblocksai/ui";
<div className="invoice-row-list">
<InvoiceRow
date="14 May 2026"
number="INV-2026-0142"
amount="$292.00"
status="paid"
pdfHref="/invoices/INV-2026-0142.pdf"
/>
<InvoiceRow
date="14 Apr 2026"
number="INV-2026-0118"
amount="$292.00"
status="paid"
pdfHref="/invoices/INV-2026-0118.pdf"
/>
<InvoiceRow
date="14 Mar 2026"
number="INV-2026-0094"
amount="$32.00"
status="open"
/>
<InvoiceRow
date="14 Feb 2026"
number="INV-2026-0070"
amount="$292.00"
status="void"
/>
</div>
A card-on-file display — brand mark + last 4 digits + expiry + cardholder name + Edit and Remove action buttons. An optional .payment-method-card-pill in the top-right of the head marks the default card. The brand glyph is an inline SVG inside a fixed-size .payment-method-card-brand wrapper (48×32); its currentColor strokes pick up the wrapper’s text colour so dark-mode flips automatically. Below 480px the grid stacks (single column) and the action buttons stretch full-width via flex: 1.
Two cards side-by-side inside a .payment-method-grid wrapper. The Visa card carries the “Primary” pill and both action buttons (Edit + Remove); the Mastercard has Edit only (no destructive action on the fallback card). Edit uses .btn-ghost for a quiet hover; Remove uses .btn-danger-outline to signal the destructive intent without screaming.
<div class="payment-method-grid">
<div class="payment-method-card">
<div class="payment-method-card-head">
<span class="payment-method-card-brand"><svg>…</svg></span>
<span class="payment-method-card-pill">Primary</span>
</div>
<div class="payment-method-card-meta">
<span class="payment-method-card-last4">•••• 4242</span>
<span class="payment-method-card-expiry">Expires 12/27</span>
<span class="payment-method-card-name">Jay Stockwell</span>
</div>
<div class="payment-method-card-actions">
<button type="button" class="btn btn-ghost btn-sm">Edit</button>
<button type="button" class="btn btn-danger-outline btn-sm">Remove</button>
</div>
</div>
<div class="payment-method-card">
<div class="payment-method-card-head">
<span class="payment-method-card-brand"><svg>…</svg></span>
</div>
<div class="payment-method-card-meta">
<span class="payment-method-card-last4">•••• 8888</span>
<span class="payment-method-card-expiry">Expires 09/26</span>
<span class="payment-method-card-name">Jay Stockwell</span>
</div>
<div class="payment-method-card-actions">
<button type="button" class="btn btn-ghost btn-sm">Edit</button>
</div>
</div>
</div>
.payment-method-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: var(--s-4);
}
.payment-method-card {
position: relative;
display: flex;
flex-direction: column;
gap: var(--s-3);
padding: var(--s-4) var(--s-5);
border: 1px solid var(--hair);
border-radius: var(--r-md);
background: var(--bg-paper);
}
.payment-method-card-head {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
gap: var(--s-3);
}
.payment-method-card-brand {
display: inline-flex;
align-items: center;
justify-content: center;
width: 48px;
height: 32px;
color: var(--fg);
flex: 0 0 auto;
}
.payment-method-card-brand svg {
width: 100%;
height: 100%;
}
.payment-method-card-pill {
display: inline-flex;
align-items: center;
padding: 2px 8px;
border-radius: var(--r-pill);
background: var(--accent-soft);
color: var(--accent-text);
font: 600 11px/1.4 var(--f-mono);
letter-spacing: 0.04em;
text-transform: uppercase;
white-space: nowrap;
}
.payment-method-card-meta {
display: flex;
flex-direction: column;
gap: 2px;
}
.payment-method-card-last4 {
font: 600 18px/1.3 var(--f-mono);
color: var(--fg);
font-variant-numeric: tabular-nums;
letter-spacing: 0.05em;
}
.payment-method-card-expiry {
font: 400 13px/1.3 var(--f-mono);
color: var(--fg-dim);
}
.payment-method-card-name {
font: 400 13px/1.3 var(--f-body);
color: var(--fg-soft);
}
.payment-method-card-actions {
display: flex;
flex-direction: row;
gap: var(--s-2);
justify-content: flex-end;
margin-top: auto;
}
@media (max-width: 480px) {
.payment-method-grid {
grid-template-columns: 1fr;
}
.payment-method-card-actions {
flex-direction: row;
width: 100%;
}
.payment-method-card-actions .btn {
flex: 1;
}
}
import { PaymentMethodCard } from "@magicblocksai/ui";
<div className="payment-method-grid">
<PaymentMethodCard
brand="visa"
last4="4242"
expiry="12/27"
name="Jay Stockwell"
isPrimary
onEdit={() => openCardEditor("pm_visa_4242")}
onRemove={() => removeCard("pm_visa_4242")}
/>
<PaymentMethodCard
brand="mastercard"
last4="8888"
expiry="09/26"
name="Jay Stockwell"
onEdit={() => openCardEditor("pm_mc_8888")}
/>
</div>
A composed table wrapping .invoice-row-list (chapter 24.3) with a top control row — filter chips on the left + a sort toggle on the right — and a non-semantic header row aligned to the per-row grid. The chip group reuses the kit’s existing .chip / .chip-button / .chip-active family from chapter 07; each chip carries a .chip-count showing the number of matching invoices. The list chrome (border + radius + overflow hidden) stays as the single source of truth from chapter 24.3 — this section only layers controls + header + empty-state on top. Below 480px the control row stacks and the chip group becomes horizontally scrollable.
Eight invoices covering all four status states: 4 paid (success pill + download icon), 2 open (accent pill — no download yet), 1 void (sunken pill, no actions), and 1 uncollectible (error pill, no actions). The filter chip group shows the per-status counts inline (All 8 / Paid 4 / Open 2 / Void 1); the active chip carries aria-pressed="true" + the accent fill. The Date column header doubles as the sort toggle — clicking it flips the order between newest-first and oldest-first.
<div class="billing-history-table">
<div class="billing-history-table-controls">
<div class="billing-history-table-filters chip-row" role="group" aria-label="Filter invoices by status">
<button type="button" class="chip chip-button chip-active" aria-pressed="true">
<span>All</span><span class="chip-count">8</span>
</button>
<button type="button" class="chip chip-button" aria-pressed="false">
<span>Paid</span><span class="chip-count">4</span>
</button>
<button type="button" class="chip chip-button" aria-pressed="false">
<span>Open</span><span class="chip-count">2</span>
</button>
<button type="button" class="chip chip-button" aria-pressed="false">
<span>Void</span><span class="chip-count">1</span>
</button>
</div>
<button type="button" class="billing-history-table-sort">
<span>Date</span>
<span class="billing-history-table-sort-dir" aria-hidden="true">↓</span>
<span class="billing-history-table-sort-label">Newest first</span>
</button>
</div>
<div class="invoice-row-list billing-history-table-list">
<div class="billing-history-table-header" role="row">
<span role="columnheader">Date</span>
<span role="columnheader">Invoice</span>
<span role="columnheader">Amount</span>
<span role="columnheader">Status</span>
<span role="columnheader" aria-label="Actions"></span>
</div>
<div class="invoice-row">…</div>
…
</div>
</div>
.billing-history-table {
display: flex;
flex-direction: column;
gap: var(--s-4);
}
.billing-history-table-controls {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
gap: var(--s-3);
}
.billing-history-table-filters {
/* Inherits `.chip-row` for inline-flex + wrap + gap. The class is here so
this control gets its own selector hook for the mobile reflow. */
flex-wrap: wrap;
}
.billing-history-table-sort {
display: inline-flex;
align-items: center;
gap: var(--s-2);
appearance: none;
background: var(--bg-paper);
color: var(--fg);
border: 1px solid var(--hair);
border-radius: var(--r-pill);
padding: 4px 12px;
font: 500 12.5px/1.3 var(--f-body);
cursor: pointer;
white-space: nowrap;
transition: background var(--dur-1) var(--ease), color var(--dur-1) var(--ease), border-color var(--dur-1) var(--ease);
}
.billing-history-table-sort:hover {
background: var(--bg-sunk);
border-color: color-mix(in oklab, var(--fg) 25%, transparent);
}
.billing-history-table-sort:focus-visible {
outline: 0;
box-shadow: var(--sh-focus);
}
.billing-history-table-sort-dir {
font: 600 14px/1 var(--f-mono);
color: var(--accent);
}
.billing-history-table-sort-label {
color: var(--fg-soft);
font: 400 12px/1.3 var(--f-body);
}
.billing-history-table-list {
/* `.invoice-row-list` provides the border + radius + overflow + paper
background; we just need a hook for the header row below. */
}
.billing-history-table-header {
display: grid;
grid-template-columns: auto 1fr auto auto auto;
align-items: center;
gap: var(--s-3);
padding: var(--s-3) var(--s-4);
background: var(--bg-sunk);
border-bottom: 1px solid var(--hair);
font: 600 11px/1.4 var(--f-mono);
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--fg-soft);
}
.billing-history-table-empty {
padding: var(--s-6) var(--s-4);
text-align: center;
color: var(--fg-soft);
font: 400 14px/1.5 var(--f-body);
background: var(--bg-paper);
}
/* …additional rules trimmed for brevity — see _shared.css */
import { BillingHistoryTable } from "@magicblocksai/ui";
<BillingHistoryTable
invoices={[
{ id: "i1", date: "14 May 2026", number: "INV-2026-0142", amount: "$292.00", status: "paid", pdfHref: "/invoices/INV-2026-0142.pdf", sortKey: 1747180800 },
{ id: "i2", date: "14 Apr 2026", number: "INV-2026-0118", amount: "$292.00", status: "paid", pdfHref: "/invoices/INV-2026-0118.pdf", sortKey: 1744588800 },
{ id: "i3", date: "14 Mar 2026", number: "INV-2026-0094", amount: "$32.00", status: "open", sortKey: 1742083200 },
{ id: "i4", date: "14 Feb 2026", number: "INV-2026-0070", amount: "$292.00", status: "paid", pdfHref: "/invoices/INV-2026-0070.pdf", sortKey: 1739491200 },
{ id: "i5", date: "14 Jan 2026", number: "INV-2026-0046", amount: "$292.00", status: "paid", pdfHref: "/invoices/INV-2026-0046.pdf", sortKey: 1736812800 },
{ id: "i6", date: "14 Dec 2025", number: "INV-2025-0922", amount: "$292.00", status: "uncollectible", sortKey: 1734134400 },
{ id: "i7", date: "14 Nov 2025", number: "INV-2025-0898", amount: "$48.00", status: "open", sortKey: 1731542400 },
{ id: "i8", date: "14 Oct 2025", number: "INV-2025-0874", amount: "$292.00", status: "void", sortKey: 1728864000 },
]}
onRowClick={(id) => openInvoice(id)}
/>
A page-shaped wrapper composing the chapter’s five primitives into the canonical billing-page layout: Plans → Usage → Payment methods → Billing history. Each section gets a small uppercase label above its content; the wrapper itself is a flex column with var(--s-8) between sections. Stateless glue — the composed children (<BillingHistoryTable> in particular) own any interactive state they need. The composed primitives carry their own mobile reflow, so the wrapper has nothing extra to add for narrow viewports. The consumer’s page-shell wrapper handles max-width; here the demo is shown unconstrained inside the .demo-stage.
A realistic mid-tier customer view: the Pro plan is recommended (accent intent + pill), API calls are healthy (~25%), storage is approaching the limit (88%, warning tint), one Visa-on-file is marked primary, and four recent invoices mix paid + open. The page wrapper just stacks the four sections; everything inside each section delegates to the primitive from 24.1–24.5.
Everything you need to evaluate MagicBlocks on a small team.
Everything in Free, plus the power-user surface area most teams need.
For organisations that need procurement, compliance, and dedicated support.
<div class="billing-page">
<section class="billing-page-section">
<h2 class="billing-page-section-label">Plan</h2>
<div class="plan-card-grid">
<article class="plan-card intent-muted">…</article>
<article class="plan-card intent-accent">…</article>
<article class="plan-card intent-ink">…</article>
</div>
</section>
<section class="billing-page-section">
<h2 class="billing-page-section-label">Usage</h2>
<div class="usage-meter-stack">…</div>
</section>
<section class="billing-page-section">
<h2 class="billing-page-section-label">Payment methods</h2>
<div class="payment-method-grid">…</div>
</section>
<section class="billing-page-section">
<h2 class="billing-page-section-label">Billing history</h2>
<div class="billing-history-table">…</div>
</section>
</div>
.billing-page {
display: flex;
flex-direction: column;
gap: var(--s-8);
}
.billing-page-section {
display: flex;
flex-direction: column;
gap: var(--s-4);
}
.billing-page-section-label {
margin: 0;
font: 600 16px/1.3 var(--f-display);
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--fg-soft);
}
import { BillingPage } from "@magicblocksai/ui";
<BillingPage
plans={[
{
name: "Free",
price: "$0",
period: "billed monthly",
description: "Everything you need to evaluate MagicBlocks on a small team.",
features: ["Up to 3 seats", "1,000 records", "Community support"],
cta: <button type="button" className="btn btn-ghost">Get started</button>,
},
{
name: "Pro",
price: "$29",
period: "per seat / month",
description: "Everything in Free, plus the power-user surface area most teams need.",
features: ["Unlimited seats", "Unlimited records", "Priority support", "SSO & SCIM"],
cta: <button type="button" className="btn btn-primary">Start free trial</button>,
intent: "accent",
pill: "Recommended",
},
{
name: "Enterprise",
price: "Custom",
period: "billed annually",
description: "For organisations that need procurement, compliance, and dedicated support.",
features: ["Everything in Pro", "SOC 2 + HIPAA", "99.99% SLA"],
cta: <button type="button" className="btn btn-ink">Contact sales</button>,
intent: "ink",
},
]}
usage={[
{ label: "API calls this month", current: 24500, limit: 100000, unit: "calls" },
{ label: "Storage", current: 8.8, limit: 10, unit: "GB", format: (n) => n.toFixed(1) },
]}
paymentMethods={[
{
brand: "visa",
last4: "4242",
expiry: "12/27",
name: "Jay Stockwell",
isPrimary: true,
onEdit: () => openCardEditor("pm_visa_4242"),
onRemove: () => removeCard("pm_visa_4242"),
},
]}
invoices={[
{ id: "i1", date: "14 May 2026", number: "INV-2026-0142", amount: "$292.00", status: "paid", pdfHref: "/invoices/INV-2026-0142.pdf", sortKey: 1747180800 },
{ id: "i2", date: "14 Apr 2026", number: "INV-2026-0118", amount: "$292.00", status: "paid", pdfHref: "/invoices/INV-2026-0118.pdf", sortKey: 1744588800 },
{ id: "i3", date: "14 Mar 2026", number: "INV-2026-0094", amount: "$32.00", status: "open", sortKey: 1742083200 },
{ id: "i4", date: "14 Feb 2026", number: "INV-2026-0070", amount: "$292.00", status: "paid", pdfHref: "/invoices/INV-2026-0070.pdf", sortKey: 1739491200 },
]}
/>