@moonpay/cli 1.49.1 → 1.51.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-7OJWAG57.js → chunk-AOKKDF4V.js} +1 -1
- package/dist/{chunk-D75SATYI.js → chunk-DMVZB2YH.js} +3 -3
- package/dist/{chunk-W5YCGWNV.js → chunk-GJLMK3FI.js} +2 -2
- package/dist/{chunk-BC4XTACV.js → chunk-OXRTYCBT.js} +1 -1
- package/dist/{client-3U3POBAC.js → client-EBMO22CL.js} +1 -1
- package/dist/index.js +2 -2
- package/dist/{ledger-P2SJ2YCC.js → ledger-S3A7KL23.js} +2 -2
- package/dist/{mcp-EVOIW4EJ.js → mcp-YYGFZ5UU.js} +1 -1
- package/dist/{store-QMBXLZXK.js → store-C3SIF4VO.js} +1 -1
- package/package.json +1 -1
- package/skills/moonpay-artifact-builder/SKILL.md +667 -0
- package/skills/moonpay-automation-builder/SKILL.md +148 -0
- package/skills/moonpay-card-onboarding/SKILL.md +150 -0
- package/skills/moonpay-skill-builder/SKILL.md +157 -0
|
@@ -0,0 +1,667 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: moonpay-artifact-builder
|
|
3
|
+
description: Author interactive HTML artifacts for the MoonPay Agents desktop app. Use when the user wants a custom interactive UI, a one-off dashboard, a status report, an editor, or a walkthrough that drives the MoonPay CLI under the hood. Artifacts are single HTML files that render in the app's sandboxed Artifacts view, call `mp` via window.mp.run, and inherit the host's dark design system. This skill defines the contract, the bridge API, and — primarily — the design system every artifact should use so the catalog feels like one product.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# MoonPay Artifact Builder
|
|
7
|
+
|
|
8
|
+
The MoonPay Agents desktop app ships an **Artifacts** view that renders self-contained HTML files inside a sandboxed iframe. Each artifact can call the local `mp` CLI through a postMessage bridge — same way Claude/ChatGPT artifacts work, but first-party and able to invoke real money-moving operations.
|
|
9
|
+
|
|
10
|
+
When you author an artifact, the goal isn't just "an HTML file that works" — it's an artifact that looks like it belongs next to the rest of the MoonPay Agents catalog. The design system below is how you get that for free.
|
|
11
|
+
|
|
12
|
+
## When to use this skill
|
|
13
|
+
|
|
14
|
+
The user wants a custom interactive UI for something they do recurringly: a dashboard, a status report, an editor, a walkthrough, a teaching artifact. Save to `~/.moonpay-agents/artifacts/<slug>.html` — it shows up in the app's Artifacts view on next list.
|
|
15
|
+
|
|
16
|
+
If the user wants a *procedural* workflow (a multi-step automation), that's a skill, not an artifact — invoke **moonpay-skill-builder** instead.
|
|
17
|
+
|
|
18
|
+
## On-disk format
|
|
19
|
+
|
|
20
|
+
A single self-contained HTML file. No build step, no bundler, no external assets except CDN libs.
|
|
21
|
+
|
|
22
|
+
### Frontmatter
|
|
23
|
+
|
|
24
|
+
The first thing in the file MUST be an HTML comment with key/value pairs:
|
|
25
|
+
|
|
26
|
+
```html
|
|
27
|
+
<!--
|
|
28
|
+
name: my-artifact-slug
|
|
29
|
+
title: My Artifact Title
|
|
30
|
+
description: One short sentence shown on the artifact card.
|
|
31
|
+
tags: [explorer, portfolio]
|
|
32
|
+
-->
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
| key | required | notes |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| `name` | required | URL-safe slug. Must match the filename (`<slug>.html`). |
|
|
38
|
+
| `title` | required | Displayed at the top of the artifact card and in the header bar. |
|
|
39
|
+
| `description` | required | One sentence on the card. Aim for ≤ 100 chars. |
|
|
40
|
+
| `tags` | optional | Comma-separated, e.g. `[explorer, tokens]`. Used for filtering. |
|
|
41
|
+
|
|
42
|
+
### Save location
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
~/.moonpay-agents/artifacts/<slug>.html
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Drop the file in this dir and it shows up the next time the app lists artifacts. No reload needed.
|
|
49
|
+
|
|
50
|
+
(Built-in MoonPay artifacts live inside the app bundle. Don't try to write there — user artifacts always go to the path above.)
|
|
51
|
+
|
|
52
|
+
## The bridge: `window.mp.run`
|
|
53
|
+
|
|
54
|
+
Artifacts call the MoonPay CLI through `window.mp.run(argv: string[]): Promise<string>`. The result is the raw stdout of `mp <argv...>` — usually JSON if you pass `--json`.
|
|
55
|
+
|
|
56
|
+
```js
|
|
57
|
+
const data = await window.mp.run(["token", "retrieve", "--token", addr, "--chain", "polygon", "--json"]);
|
|
58
|
+
const token = JSON.parse(data);
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Bridge contract
|
|
62
|
+
|
|
63
|
+
- **argv strings only.** The host validates every element is a string and rejects anything else. Numbers must be `String(x)`.
|
|
64
|
+
- **Sandboxed iframe** with `sandbox="allow-scripts"`. No same-origin → no access to host cookies, localStorage, or storage. `fetch` to outside origins is restricted by browser CORS.
|
|
65
|
+
- **One-way trust**: the host has full agency, the artifact has none unless the user has approved the underlying CLI command.
|
|
66
|
+
|
|
67
|
+
### Bridge polyfill (copy/paste into every artifact)
|
|
68
|
+
|
|
69
|
+
The host injects `window.mp.run` automatically. Include this polyfill anyway so the artifact works when previewed outside the app:
|
|
70
|
+
|
|
71
|
+
```js
|
|
72
|
+
const mp = window.mp || (() => {
|
|
73
|
+
let nextId = 1;
|
|
74
|
+
const pending = new Map();
|
|
75
|
+
window.addEventListener("message", (e) => {
|
|
76
|
+
const { id, type, result, error } = e.data || {};
|
|
77
|
+
if (type !== "mp.result" || !pending.has(id)) return;
|
|
78
|
+
const { resolve, reject } = pending.get(id);
|
|
79
|
+
pending.delete(id);
|
|
80
|
+
error ? reject(new Error(error)) : resolve(result);
|
|
81
|
+
});
|
|
82
|
+
return {
|
|
83
|
+
run(argv) {
|
|
84
|
+
const id = nextId++;
|
|
85
|
+
return new Promise((resolve, reject) => {
|
|
86
|
+
pending.set(id, { resolve, reject });
|
|
87
|
+
window.parent.postMessage({ id, type: "mp.run", argv }, "*");
|
|
88
|
+
});
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
})();
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
# Design system
|
|
97
|
+
|
|
98
|
+
This is the meat of this skill. Use these tokens in every artifact you author.
|
|
99
|
+
|
|
100
|
+
The host (the MoonPay Agents app) injects a set of `--mp-*` CSS variables into the iframe. Inherit them via `var(--mp-thing, fallback)` so the artifact looks correct standalone too.
|
|
101
|
+
|
|
102
|
+
## Color tokens
|
|
103
|
+
|
|
104
|
+
```css
|
|
105
|
+
:root {
|
|
106
|
+
color-scheme: dark;
|
|
107
|
+
|
|
108
|
+
/* Surfaces */
|
|
109
|
+
--bg: var(--mp-bg-1, #0a0a0b); /* surface 0 — page background */
|
|
110
|
+
--bg-2: var(--mp-bg-2, #131316); /* surface 1 — cards, panels */
|
|
111
|
+
--bg-3: var(--mp-bg-3, #1c1c20); /* surface 2 — inner cards, hovered states */
|
|
112
|
+
|
|
113
|
+
/* Text */
|
|
114
|
+
--fg: var(--mp-fg, #e7e7ea); /* primary text */
|
|
115
|
+
--fg-dim: var(--mp-fg-dim, #8a8a90); /* secondary text — labels, metadata */
|
|
116
|
+
--fg-faint: var(--mp-fg-faint, #5a5a60); /* tertiary text — placeholders, disabled */
|
|
117
|
+
|
|
118
|
+
/* Borders & dividers */
|
|
119
|
+
--border: var(--mp-border, #232328); /* default border */
|
|
120
|
+
--border-2: var(--mp-border-2, #2e2e34); /* emphasized border (active inputs, hover) */
|
|
121
|
+
|
|
122
|
+
/* Brand */
|
|
123
|
+
--accent: var(--mp-accent, #7e6cff); /* MoonPay purple — primary actions, focus */
|
|
124
|
+
--accent-2: var(--mp-accent-2, #9d8eff); /* lighter purple — hover, gradients */
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Semantic colors (use directly, not via var)
|
|
129
|
+
|
|
130
|
+
| Role | Hex | When to use |
|
|
131
|
+
|---|---|---|
|
|
132
|
+
| Success / positive | `#56d364` | Gains, completed states, "Active" pills |
|
|
133
|
+
| Negative / loss / error | `#f47067` | Losses, error messages, declined states |
|
|
134
|
+
| Warning | `#f4a261` | Caution, throttle, partial state |
|
|
135
|
+
| Info | `#5cc8e7` | Neutral chains, metadata badges |
|
|
136
|
+
|
|
137
|
+
## Typography
|
|
138
|
+
|
|
139
|
+
```css
|
|
140
|
+
:root {
|
|
141
|
+
--font-sans: -apple-system, BlinkMacSystemFont, "Inter", "Segoe UI", sans-serif;
|
|
142
|
+
--font-mono: ui-monospace, "JetBrains Mono", "Fira Code", monospace;
|
|
143
|
+
|
|
144
|
+
--text-xs: 11px; /* labels, badges, chart axes */
|
|
145
|
+
--text-sm: 12px; /* secondary text, table cells */
|
|
146
|
+
--text-base: 13px; /* body */
|
|
147
|
+
--text-md: 14px; /* primary text in lists */
|
|
148
|
+
--text-lg: 18px; /* section titles */
|
|
149
|
+
--text-xl: 22px; /* card headings */
|
|
150
|
+
--text-2xl: 32px; /* hero values (e.g. portfolio total) */
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Weights: **400** for body, **500** for emphasis, **600** for headings and stat values. Never 700+ in dark UI — too aggressive.
|
|
155
|
+
|
|
156
|
+
## Spacing scale
|
|
157
|
+
|
|
158
|
+
4px base unit. Use these as `padding` / `margin` / `gap`.
|
|
159
|
+
|
|
160
|
+
```css
|
|
161
|
+
--space-1: 4px; /* tight (icon-text gap) */
|
|
162
|
+
--space-2: 8px; /* compact (button padding-y, chip gap) */
|
|
163
|
+
--space-3: 12px; /* default (card inner gap) */
|
|
164
|
+
--space-4: 16px; /* comfortable (card padding) */
|
|
165
|
+
--space-6: 24px; /* page padding */
|
|
166
|
+
--space-8: 32px; /* section separator */
|
|
167
|
+
--space-12: 48px; /* hero spacing */
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Radii
|
|
171
|
+
|
|
172
|
+
```css
|
|
173
|
+
--radius-sm: 6px; /* badges, small pills */
|
|
174
|
+
--radius: 12px; /* cards, buttons */
|
|
175
|
+
--radius-lg: 16px; /* hero panels */
|
|
176
|
+
--radius-full: 9999px; /* status pills, avatars */
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Motion
|
|
180
|
+
|
|
181
|
+
```css
|
|
182
|
+
--ease: cubic-bezier(0.2, 0.8, 0.2, 1);
|
|
183
|
+
--duration-1: 120ms; /* hover state */
|
|
184
|
+
--duration-2: 180ms; /* card transitions */
|
|
185
|
+
--duration-3: 240ms; /* page-level transitions */
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Default to `transition: <prop> var(--duration-1) var(--ease)`. Never over 300ms.
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
# Component recipes
|
|
193
|
+
|
|
194
|
+
These are the shapes the host app uses. Copy them into your artifact — that's how the catalog stays visually consistent.
|
|
195
|
+
|
|
196
|
+
## Page wrapper
|
|
197
|
+
|
|
198
|
+
```html
|
|
199
|
+
<body>
|
|
200
|
+
<div class="page">
|
|
201
|
+
<h1>My Artifact</h1>
|
|
202
|
+
<p class="sub">One-line description of what this artifact shows.</p>
|
|
203
|
+
<!-- … content … -->
|
|
204
|
+
</div>
|
|
205
|
+
</body>
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
```css
|
|
209
|
+
* { box-sizing: border-box; }
|
|
210
|
+
html,
|
|
211
|
+
body { margin: 0; padding: 0; }
|
|
212
|
+
body { background: var(--bg); color: var(--fg); font-family: var(--font-sans); }
|
|
213
|
+
.page { max-width: 1100px; margin: 0 auto; padding: var(--space-6); min-height: 100vh; }
|
|
214
|
+
h1 { font-size: var(--text-xl); font-weight: 600; margin: 0 0 4px; }
|
|
215
|
+
.sub { color: var(--fg-dim); font-size: var(--text-sm); margin: 0 0 var(--space-6); }
|
|
216
|
+
h2 { font-size: var(--text-xs); font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; color: var(--fg-dim); margin: var(--space-6) 0 var(--space-3); }
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Card
|
|
220
|
+
|
|
221
|
+
```html
|
|
222
|
+
<div class="card">
|
|
223
|
+
<div class="k">Spent this month</div>
|
|
224
|
+
<div class="v">$847.20</div>
|
|
225
|
+
<div class="sub-v">12 transactions</div>
|
|
226
|
+
</div>
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
```css
|
|
230
|
+
.card { background: var(--bg-2); border: 1px solid var(--border); border-radius: var(--radius); padding: var(--space-4); }
|
|
231
|
+
.k { color: var(--fg-dim); font-size: var(--text-xs); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px; }
|
|
232
|
+
.v { font-size: var(--text-xl); font-weight: 600; font-variant-numeric: tabular-nums; }
|
|
233
|
+
.sub-v { color: var(--fg-dim); font-size: var(--text-xs); margin-top: 2px; }
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## Stat row (4-up grid)
|
|
237
|
+
|
|
238
|
+
```html
|
|
239
|
+
<div class="stat-row">
|
|
240
|
+
<div class="card"><div class="k">Today</div><div class="v">$120</div></div>
|
|
241
|
+
<div class="card"><div class="k">This week</div><div class="v">$640</div></div>
|
|
242
|
+
<div class="card"><div class="k">This month</div><div class="v">$2,340</div></div>
|
|
243
|
+
<div class="card"><div class="k">YTD</div><div class="v">$18,200</div></div>
|
|
244
|
+
</div>
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
```css
|
|
248
|
+
.stat-row { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--space-2); margin-bottom: var(--space-6); }
|
|
249
|
+
@media (max-width: 800px) { .stat-row { grid-template-columns: repeat(2, 1fr); } }
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
## Button
|
|
253
|
+
|
|
254
|
+
```html
|
|
255
|
+
<button class="btn">Primary action</button>
|
|
256
|
+
<button class="btn-ghost">Secondary</button>
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
```css
|
|
260
|
+
.btn { background: var(--accent); color: white; border: 0; border-radius: var(--radius); padding: var(--space-2) var(--space-4); font-size: var(--text-sm); font-weight: 500; cursor: pointer; transition: background var(--duration-1) var(--ease); }
|
|
261
|
+
.btn:hover { background: var(--accent-2); }
|
|
262
|
+
.btn-ghost { background: transparent; color: var(--fg); border: 1px solid var(--border); border-radius: var(--radius); padding: var(--space-2) var(--space-4); font-size: var(--text-sm); cursor: pointer; transition: border-color var(--duration-1) var(--ease); }
|
|
263
|
+
.btn-ghost:hover { border-color: var(--border-2); }
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
## Status pill
|
|
267
|
+
|
|
268
|
+
```html
|
|
269
|
+
<span class="pill pill-success"><span class="dot"></span>Active</span>
|
|
270
|
+
<span class="pill pill-warning"><span class="dot"></span>Throttled</span>
|
|
271
|
+
<span class="pill pill-error"><span class="dot"></span>Declined</span>
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
```css
|
|
275
|
+
.pill { display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px; border-radius: var(--radius-full); font-size: var(--text-xs); font-weight: 500; }
|
|
276
|
+
.pill .dot { width: 6px; height: 6px; border-radius: 50%; }
|
|
277
|
+
.pill-success { background: rgba(86, 211, 100, 0.12); color: #56d364; } .pill-success .dot { background: #56d364; }
|
|
278
|
+
.pill-warning { background: rgba(244, 162, 97, 0.12); color: #f4a261; } .pill-warning .dot { background: #f4a261; }
|
|
279
|
+
.pill-error { background: rgba(244, 112, 103, 0.12); color: #f47067; } .pill-error .dot { background: #f47067; }
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
## Text input
|
|
283
|
+
|
|
284
|
+
```html
|
|
285
|
+
<input class="input" type="text" placeholder="0x…" />
|
|
286
|
+
<input class="input" type="text" disabled value="locked" />
|
|
287
|
+
<input class="input input-error" type="text" value="not a valid address" />
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
```css
|
|
291
|
+
.input { width: 100%; background: var(--bg-2); color: var(--fg); border: 1px solid var(--border); border-radius: var(--radius); padding: var(--space-2) var(--space-3); font-size: var(--text-sm); font-family: var(--font-sans); transition: border-color var(--duration-1) var(--ease), box-shadow var(--duration-1) var(--ease); }
|
|
292
|
+
.input::placeholder { color: var(--fg-faint); }
|
|
293
|
+
.input:hover { border-color: var(--border-2); }
|
|
294
|
+
.input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 20%, transparent); }
|
|
295
|
+
.input:disabled { color: var(--fg-faint); cursor: not-allowed; }
|
|
296
|
+
.input-error { border-color: #f47067; }
|
|
297
|
+
.input-error:focus { box-shadow: 0 0 0 3px rgba(244, 112, 103, 0.18); }
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
## Select (native, restyled)
|
|
301
|
+
|
|
302
|
+
```html
|
|
303
|
+
<select class="select">
|
|
304
|
+
<option>USDC</option>
|
|
305
|
+
<option>USDT</option>
|
|
306
|
+
<option>SOL</option>
|
|
307
|
+
</select>
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
```css
|
|
311
|
+
.select { appearance: none; background: var(--bg-2) url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'><path fill='%238a8a90' d='M2 4l4 4 4-4z'/></svg>") right 10px center no-repeat; color: var(--fg); border: 1px solid var(--border); border-radius: var(--radius); padding: var(--space-2) 32px var(--space-2) var(--space-3); font-size: var(--text-sm); font-family: var(--font-sans); cursor: pointer; }
|
|
312
|
+
.select:hover { border-color: var(--border-2); }
|
|
313
|
+
.select:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 20%, transparent); }
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
## Switch (toggle)
|
|
317
|
+
|
|
318
|
+
```html
|
|
319
|
+
<label class="switch">
|
|
320
|
+
<input type="checkbox" />
|
|
321
|
+
<span class="track"></span>
|
|
322
|
+
<span class="label">Auto-refresh</span>
|
|
323
|
+
</label>
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
```css
|
|
327
|
+
.switch { display: inline-flex; align-items: center; gap: var(--space-2); cursor: pointer; font-size: var(--text-sm); user-select: none; }
|
|
328
|
+
.switch input { position: absolute; opacity: 0; pointer-events: none; }
|
|
329
|
+
.switch .track { position: relative; width: 32px; height: 18px; background: var(--bg-3); border: 1px solid var(--border); border-radius: var(--radius-full); transition: background var(--duration-1) var(--ease); }
|
|
330
|
+
.switch .track::after { content: ""; position: absolute; top: 2px; left: 2px; width: 12px; height: 12px; background: var(--fg-dim); border-radius: 50%; transition: transform var(--duration-1) var(--ease), background var(--duration-1) var(--ease); }
|
|
331
|
+
.switch input:checked + .track { background: color-mix(in srgb, var(--accent) 30%, var(--bg-3)); border-color: var(--accent); }
|
|
332
|
+
.switch input:checked + .track::after { transform: translateX(14px); background: var(--accent); }
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
## Checkbox
|
|
336
|
+
|
|
337
|
+
```html
|
|
338
|
+
<label class="check">
|
|
339
|
+
<input type="checkbox" />
|
|
340
|
+
<span class="box"></span>
|
|
341
|
+
<span>Include declined transactions</span>
|
|
342
|
+
</label>
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
```css
|
|
346
|
+
.check { display: inline-flex; align-items: center; gap: var(--space-2); cursor: pointer; font-size: var(--text-sm); user-select: none; }
|
|
347
|
+
.check input { position: absolute; opacity: 0; pointer-events: none; }
|
|
348
|
+
.check .box { width: 16px; height: 16px; background: var(--bg-2); border: 1px solid var(--border); border-radius: 4px; display: inline-flex; align-items: center; justify-content: center; transition: background var(--duration-1) var(--ease), border-color var(--duration-1) var(--ease); }
|
|
349
|
+
.check input:checked + .box { background: var(--accent); border-color: var(--accent); }
|
|
350
|
+
.check input:checked + .box::after { content: ""; width: 10px; height: 6px; border-left: 2px solid white; border-bottom: 2px solid white; transform: rotate(-45deg) translate(1px, -1px); }
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
## Tabs
|
|
354
|
+
|
|
355
|
+
```html
|
|
356
|
+
<div class="tabs" role="tablist">
|
|
357
|
+
<button class="tab is-active" data-tab="overview">Overview</button>
|
|
358
|
+
<button class="tab" data-tab="transactions">Transactions</button>
|
|
359
|
+
<button class="tab" data-tab="wallets">Wallets</button>
|
|
360
|
+
</div>
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
```css
|
|
364
|
+
.tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--border); margin-bottom: var(--space-4); }
|
|
365
|
+
.tab { background: transparent; color: var(--fg-dim); border: 0; border-bottom: 2px solid transparent; padding: var(--space-2) var(--space-3); font-size: var(--text-sm); font-weight: 500; cursor: pointer; transition: color var(--duration-1) var(--ease), border-color var(--duration-1) var(--ease); margin-bottom: -1px; }
|
|
366
|
+
.tab:hover { color: var(--fg); }
|
|
367
|
+
.tab.is-active { color: var(--fg); border-bottom-color: var(--accent); }
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
```js
|
|
371
|
+
// Wire-up: toggle .is-active and show the matching panel
|
|
372
|
+
document.querySelectorAll(".tab").forEach(t => t.addEventListener("click", () => {
|
|
373
|
+
document.querySelectorAll(".tab").forEach(x => x.classList.toggle("is-active", x === t));
|
|
374
|
+
document.querySelectorAll("[data-panel]").forEach(p => p.hidden = p.dataset.panel !== t.dataset.tab);
|
|
375
|
+
}));
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
## Dialog (modal)
|
|
379
|
+
|
|
380
|
+
Use the native `<dialog>` element — it gets accessibility for free.
|
|
381
|
+
|
|
382
|
+
```html
|
|
383
|
+
<dialog id="confirm-dialog" class="dialog">
|
|
384
|
+
<h3>Approve $500 delegation?</h3>
|
|
385
|
+
<p>This signs an SPL Approve transaction. The cap is the loss ceiling if anything goes wrong.</p>
|
|
386
|
+
<div class="dialog-actions">
|
|
387
|
+
<button class="btn-ghost" onclick="document.getElementById('confirm-dialog').close()">Cancel</button>
|
|
388
|
+
<button class="btn" onclick="approveDelegation()">Approve</button>
|
|
389
|
+
</div>
|
|
390
|
+
</dialog>
|
|
391
|
+
|
|
392
|
+
<button class="btn" onclick="document.getElementById('confirm-dialog').showModal()">Delegate wallet</button>
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
```css
|
|
396
|
+
.dialog { background: var(--bg-2); color: var(--fg); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: var(--space-6); max-width: 420px; box-shadow: 0 24px 60px -20px rgba(0, 0, 0, 0.6); }
|
|
397
|
+
.dialog::backdrop { background: rgba(0, 0, 0, 0.6); backdrop-filter: blur(2px); }
|
|
398
|
+
.dialog h3 { margin: 0 0 var(--space-2); font-size: var(--text-md); font-weight: 600; }
|
|
399
|
+
.dialog p { margin: 0 0 var(--space-4); color: var(--fg-dim); font-size: var(--text-sm); line-height: 1.5; }
|
|
400
|
+
.dialog-actions { display: flex; justify-content: flex-end; gap: var(--space-2); }
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
## Tooltip
|
|
404
|
+
|
|
405
|
+
CSS-only, attribute-driven. Add `data-tooltip` to anything.
|
|
406
|
+
|
|
407
|
+
```html
|
|
408
|
+
<span data-tooltip="On-chain delegation transaction">tx hash</span>
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
```css
|
|
412
|
+
[data-tooltip] { position: relative; cursor: help; }
|
|
413
|
+
[data-tooltip]::after { content: attr(data-tooltip); position: absolute; bottom: calc(100% + 6px); left: 50%; transform: translateX(-50%); background: var(--bg-3); color: var(--fg); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 6px 10px; font-size: var(--text-xs); white-space: nowrap; opacity: 0; pointer-events: none; transition: opacity var(--duration-1) var(--ease); }
|
|
414
|
+
[data-tooltip]:hover::after { opacity: 1; }
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
## Skeleton (loading placeholder)
|
|
418
|
+
|
|
419
|
+
```html
|
|
420
|
+
<div class="skel" style="width:120px;height:14px"></div>
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
```css
|
|
424
|
+
.skel { background: linear-gradient(90deg, var(--bg-2) 0%, var(--bg-3) 50%, var(--bg-2) 100%); background-size: 200% 100%; border-radius: var(--radius-sm); animation: skel-shimmer 1.4s infinite; }
|
|
425
|
+
@keyframes skel-shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
## Empty state
|
|
429
|
+
|
|
430
|
+
```html
|
|
431
|
+
<div class="empty">
|
|
432
|
+
No transactions yet.
|
|
433
|
+
<div class="hint">Click <strong>Start shopping</strong> above to make the first one.</div>
|
|
434
|
+
</div>
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
```css
|
|
438
|
+
.empty { background: var(--bg-2); border: 1px dashed var(--border); border-radius: var(--radius); padding: var(--space-6); text-align: center; color: var(--fg-dim); font-size: var(--text-sm); }
|
|
439
|
+
.empty .hint { color: var(--fg-faint); font-size: var(--text-xs); margin-top: 4px; }
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
## Loading / error
|
|
443
|
+
|
|
444
|
+
Every `mp.run` call should produce a visible state for both — never silent.
|
|
445
|
+
|
|
446
|
+
```css
|
|
447
|
+
.loading { color: var(--fg-dim); font-size: var(--text-sm); padding: var(--space-4) 0; }
|
|
448
|
+
.error { color: #f47067; font-family: var(--font-mono); font-size: var(--text-xs); padding: var(--space-3) 0; }
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
## Charts
|
|
452
|
+
|
|
453
|
+
Use [Chart.js](https://www.chartjs.org/) as the default. It loads from CDN, supports the chart types artifacts typically need (line, bar, doughnut, sparkline), is easy to theme with our tokens, and the agent already knows it well.
|
|
454
|
+
|
|
455
|
+
```html
|
|
456
|
+
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
|
457
|
+
<canvas id="chart" style="max-height: 260px"></canvas>
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
```js
|
|
461
|
+
// Read design tokens off the document so the chart inherits the host theme.
|
|
462
|
+
const css = getComputedStyle(document.documentElement);
|
|
463
|
+
const fg = css.getPropertyValue("--fg").trim() || "#e7e7ea";
|
|
464
|
+
const fgDim = css.getPropertyValue("--fg-dim").trim() || "#8a8a90";
|
|
465
|
+
const border = css.getPropertyValue("--border").trim() || "#232328";
|
|
466
|
+
const accent = css.getPropertyValue("--accent").trim() || "#7e6cff";
|
|
467
|
+
|
|
468
|
+
// Apply defaults once, before instantiating any charts.
|
|
469
|
+
Chart.defaults.color = fgDim;
|
|
470
|
+
Chart.defaults.borderColor = border;
|
|
471
|
+
Chart.defaults.font.family = "-apple-system, Inter, sans-serif";
|
|
472
|
+
Chart.defaults.font.size = 11;
|
|
473
|
+
Chart.defaults.plugins.legend.labels.color = fg;
|
|
474
|
+
Chart.defaults.plugins.tooltip.backgroundColor = "#1c1c20";
|
|
475
|
+
Chart.defaults.plugins.tooltip.titleColor = fg;
|
|
476
|
+
Chart.defaults.plugins.tooltip.bodyColor = fgDim;
|
|
477
|
+
Chart.defaults.plugins.tooltip.borderColor = border;
|
|
478
|
+
Chart.defaults.plugins.tooltip.borderWidth = 1;
|
|
479
|
+
Chart.defaults.plugins.tooltip.padding = 10;
|
|
480
|
+
Chart.defaults.plugins.tooltip.cornerRadius = 8;
|
|
481
|
+
|
|
482
|
+
new Chart(document.getElementById("chart"), {
|
|
483
|
+
type: "line",
|
|
484
|
+
data: {
|
|
485
|
+
labels: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
|
|
486
|
+
datasets: [{
|
|
487
|
+
label: "Portfolio",
|
|
488
|
+
data: [12400, 12550, 12480, 12700, 12860, 13020, 13180],
|
|
489
|
+
borderColor: accent,
|
|
490
|
+
backgroundColor: "rgba(126, 108, 255, 0.12)",
|
|
491
|
+
fill: true,
|
|
492
|
+
tension: 0.3,
|
|
493
|
+
pointRadius: 0,
|
|
494
|
+
borderWidth: 2,
|
|
495
|
+
}],
|
|
496
|
+
},
|
|
497
|
+
options: {
|
|
498
|
+
maintainAspectRatio: false,
|
|
499
|
+
plugins: { legend: { display: false } },
|
|
500
|
+
scales: {
|
|
501
|
+
x: { grid: { display: false }, ticks: { color: fgDim } },
|
|
502
|
+
y: { grid: { color: border }, ticks: { color: fgDim, callback: v => "$" + v.toLocaleString() } },
|
|
503
|
+
},
|
|
504
|
+
},
|
|
505
|
+
});
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
### Chart color palette (when you need multiple series)
|
|
509
|
+
|
|
510
|
+
```js
|
|
511
|
+
const SERIES = [
|
|
512
|
+
"#7e6cff", // accent (primary)
|
|
513
|
+
"#5cc8e7", // info / chain
|
|
514
|
+
"#56d364", // success
|
|
515
|
+
"#f4a261", // warning
|
|
516
|
+
"#f47067", // negative
|
|
517
|
+
"#fbbf24", // shopping
|
|
518
|
+
"#34d399", // hardware
|
|
519
|
+
"#9d8eff", // accent-2
|
|
520
|
+
];
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
Use them in order. Two series → first two. Six categories → first six. Don't pick at random — the order matters for consistency across artifacts.
|
|
524
|
+
|
|
525
|
+
### Sparkline shorthand
|
|
526
|
+
|
|
527
|
+
For inline sparklines in tables / stat cards, set `pointRadius: 0`, `borderWidth: 1.5`, and disable both axes:
|
|
528
|
+
|
|
529
|
+
```js
|
|
530
|
+
new Chart(canvas, {
|
|
531
|
+
type: "line",
|
|
532
|
+
data: { labels: data.map((_, i) => i), datasets: [{ data, borderColor: accent, borderWidth: 1.5, pointRadius: 0, fill: false }] },
|
|
533
|
+
options: { maintainAspectRatio: false, plugins: { legend: { display: false }, tooltip: { enabled: false } }, scales: { x: { display: false }, y: { display: false } } },
|
|
534
|
+
});
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
### Alternatives
|
|
538
|
+
|
|
539
|
+
- **uPlot** — better for high-density time-series (100K+ points). Use when Chart.js feels slow.
|
|
540
|
+
- **D3** — only when you need a chart Chart.js doesn't have (sankey, force-directed, custom geometries). Has a learning cliff.
|
|
541
|
+
- **Echarts** — also fine; treat as a Chart.js replacement, not an addition.
|
|
542
|
+
|
|
543
|
+
If you reach for Recharts, Plotly, or anything React-flavored — don't. Artifacts are vanilla; keep them that way.
|
|
544
|
+
|
|
545
|
+
## Number formatting
|
|
546
|
+
|
|
547
|
+
Two rules:
|
|
548
|
+
1. Use `font-variant-numeric: tabular-nums` for any column of numbers so digits align vertically.
|
|
549
|
+
2. Format USD via `Intl.NumberFormat`:
|
|
550
|
+
```js
|
|
551
|
+
const fmtUsd = (n) => "$" + n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
---
|
|
555
|
+
|
|
556
|
+
---
|
|
557
|
+
|
|
558
|
+
# Living references
|
|
559
|
+
|
|
560
|
+
The MoonPay Agents app ships built-in artifacts. These are the canonical
|
|
561
|
+
examples — read them when you need to see how the design system + bridge
|
|
562
|
+
look in a real artifact. They live inside the moonpay-agents app bundle at
|
|
563
|
+
`src-tauri/artifacts/<slug>.html`.
|
|
564
|
+
|
|
565
|
+
| Artifact | What it shows |
|
|
566
|
+
|---|---|
|
|
567
|
+
| `moon-agents-card` | Hero panel with embedded image, stat row, status pill, delegated-wallet cards with progress meters, on-chain explorer links, transactions list with empty state, "Start shopping" CTA that deep-links into a chat via the `mp.openChat` bridge extension. The reference for any "card with live status + history" artifact. |
|
|
568
|
+
|
|
569
|
+
When you're authoring something new, **read the closest built-in artifact first**. Copy its structure rather than inventing your own — that's the fastest way to make the catalog feel coherent.
|
|
570
|
+
|
|
571
|
+
## Authoring checklist
|
|
572
|
+
|
|
573
|
+
Before saving an artifact:
|
|
574
|
+
|
|
575
|
+
1. **Frontmatter present and matches filename.** `name:` field === filename without `.html`.
|
|
576
|
+
2. **Bridge polyfill included.**
|
|
577
|
+
3. **Design tokens used everywhere.** `var(--mp-*)` for colors / radii / spacing — no hardcoded `#hexvalues` for theme colors. Semantic colors (success/error) can be hardcoded since they don't theme.
|
|
578
|
+
4. **Loading + error states.** Every `mp.run` call wrapped in try/catch with a visible state.
|
|
579
|
+
5. **Self-contained.** No external CSS or JS files. CDN libs OK but optional.
|
|
580
|
+
6. **Tight scope.** Each artifact does one thing well. If you find yourself wanting two views, that's two artifacts.
|
|
581
|
+
|
|
582
|
+
## Common pitfalls
|
|
583
|
+
|
|
584
|
+
- **Numbers as argv.** `mp.run([..., size])` fails — the host rejects non-strings. Use `String(size)`.
|
|
585
|
+
- **Forgetting `--json`.** Most `mp` commands need `--json` to return parseable output.
|
|
586
|
+
- **CORS for outside APIs.** `fetch("https://api.example.com")` works from inside the iframe only if that origin sends CORS headers. Prefer routing through `mp` if possible.
|
|
587
|
+
- **Hardcoded colors.** `background: #1c1c20` won't theme if MoonPay re-skins. Use `var(--mp-bg-3, #1c1c20)`.
|
|
588
|
+
- **Mock data unmarked.** It's fine to ship a mocked artifact, but leave a `// MOCK` comment so the agent doesn't ship it as real.
|
|
589
|
+
|
|
590
|
+
## Minimal starter
|
|
591
|
+
|
|
592
|
+
```html
|
|
593
|
+
<!--
|
|
594
|
+
name: gas-tracker
|
|
595
|
+
title: Gas Tracker
|
|
596
|
+
description: Current gas prices across Ethereum, Polygon, and Base.
|
|
597
|
+
tags: [tracker, gas]
|
|
598
|
+
-->
|
|
599
|
+
<!doctype html>
|
|
600
|
+
<html><head><meta charset="utf-8"><title>Gas Tracker</title>
|
|
601
|
+
<style>
|
|
602
|
+
:root {
|
|
603
|
+
color-scheme: dark;
|
|
604
|
+
--bg: var(--mp-bg-1, #0a0a0b);
|
|
605
|
+
--bg-2: var(--mp-bg-2, #131316);
|
|
606
|
+
--fg: var(--mp-fg, #e7e7ea);
|
|
607
|
+
--fg-dim: var(--mp-fg-dim, #8a8a90);
|
|
608
|
+
--border: var(--mp-border, #232328);
|
|
609
|
+
--accent: var(--mp-accent, #7e6cff);
|
|
610
|
+
--radius: var(--mp-radius, 12px);
|
|
611
|
+
}
|
|
612
|
+
* { box-sizing: border-box; }
|
|
613
|
+
body { margin: 0; background: var(--bg); color: var(--fg); font-family: -apple-system, Inter, sans-serif; }
|
|
614
|
+
.page { max-width: 600px; margin: 0 auto; padding: 24px; }
|
|
615
|
+
h1 { font-size: 18px; font-weight: 600; margin: 0 0 4px; }
|
|
616
|
+
.sub { color: var(--fg-dim); font-size: 13px; margin: 0 0 24px; }
|
|
617
|
+
.row { display: flex; justify-content: space-between; padding: 12px 16px; background: var(--bg-2); border: 1px solid var(--border); border-radius: var(--radius); margin-bottom: 8px; font-variant-numeric: tabular-nums; }
|
|
618
|
+
.name { text-transform: capitalize; }
|
|
619
|
+
.gwei { color: var(--accent); font-weight: 600; }
|
|
620
|
+
.loading { color: var(--fg-dim); font-size: 13px; }
|
|
621
|
+
.error { color: #f47067; font-family: ui-monospace, monospace; font-size: 12px; }
|
|
622
|
+
</style></head>
|
|
623
|
+
<body>
|
|
624
|
+
<div class="page">
|
|
625
|
+
<h1>Gas tracker</h1>
|
|
626
|
+
<p class="sub">Current gas prices across major chains.</p>
|
|
627
|
+
<div id="rows" class="loading">Loading…</div>
|
|
628
|
+
</div>
|
|
629
|
+
<script>
|
|
630
|
+
// Bridge polyfill — copy into every artifact
|
|
631
|
+
const mp = window.mp || (() => {
|
|
632
|
+
let nextId = 1; const pending = new Map();
|
|
633
|
+
window.addEventListener("message", (e) => {
|
|
634
|
+
const { id, type, result, error } = e.data || {};
|
|
635
|
+
if (type !== "mp.result" || !pending.has(id)) return;
|
|
636
|
+
const { resolve, reject } = pending.get(id);
|
|
637
|
+
pending.delete(id);
|
|
638
|
+
error ? reject(new Error(error)) : resolve(result);
|
|
639
|
+
});
|
|
640
|
+
return { run(argv) { const id = nextId++; return new Promise((resolve, reject) => { pending.set(id, { resolve, reject }); window.parent.postMessage({ id, type: "mp.run", argv }, "*"); }); } };
|
|
641
|
+
})();
|
|
642
|
+
|
|
643
|
+
(async () => {
|
|
644
|
+
try {
|
|
645
|
+
const chains = ["ethereum", "polygon", "base"];
|
|
646
|
+
const results = await Promise.all(chains.map(c =>
|
|
647
|
+
mp.run(["chain", "gas-price", "--chain", c, "--json"]).then(r => JSON.parse(r))
|
|
648
|
+
));
|
|
649
|
+
document.getElementById("rows").classList.remove("loading");
|
|
650
|
+
document.getElementById("rows").innerHTML = chains.map((c, i) =>
|
|
651
|
+
`<div class="row"><span class="name">${c}</span><span class="gwei">${results[i].gwei} gwei</span></div>`
|
|
652
|
+
).join("");
|
|
653
|
+
} catch (e) {
|
|
654
|
+
document.getElementById("rows").outerHTML =
|
|
655
|
+
`<div class="error">Failed to load: ${e.message}</div>`;
|
|
656
|
+
}
|
|
657
|
+
})();
|
|
658
|
+
</script>
|
|
659
|
+
</body></html>
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
That's the shape. Color tokens, font sizes, spacing, radii, error state, tabular nums — all the design system rules in one short file.
|
|
663
|
+
|
|
664
|
+
## Related
|
|
665
|
+
|
|
666
|
+
- **moonpay-skill-builder** — author procedural skills (the chat-side companion to artifacts).
|
|
667
|
+
- **moonpay-automation-builder** — schedule a skill to run on a cron.
|