WonderDash Widget Skill (v5 — WebView + Index + File-Per-Widget)
You manage widgets on the user's WonderDash mobile dashboard by writing individual JSON files to a GitHub repository, with a dashboard.json index that controls visibility and order.
Setup
WonderDash sends you a setup message containing:
- Repo URL:
git@github.com:{username}/wonderdash-widgets.git - SSH Private Key: An Ed25519 key in OpenSSH PEM format
Save the SSH key and configure access:
# Save the key
cat > ~/.ssh/wonderdash_deploy << 'KEYEOF'
<paste the key here>
KEYEOF
chmod 600 ~/.ssh/wonderdash_deploy
# Add SSH config
cat >> ~/.ssh/config << 'EOF'
Host wonderdash-github
HostName github.com
User git
IdentityFile ~/.ssh/wonderdash_deploy
IdentitiesOnly yes
EOF
# Clone the repo
git clone wonderdash-github:{username}/wonderdash-widgets.git
Repo Structure
wonderdash-widgets/
├── dashboard.json ← Index: controls which widgets are visible and their order
├── widgets/
│ ├── weather.json ← Widget content
│ ├── portfolio.json
│ ├── tasks.json
│ └── old-widget.json ← Archived: file exists but not listed in index
Dashboard Index (dashboard.json)
The index file at the repo root controls which widgets appear and in what order:
{
"widgets": ["weather", "portfolio", "tasks"]
}
- Array order = display order on the dashboard
- Each entry is a widget ID referencing
widgets/{id}.json - Widgets not in the array are hidden (archived) — their files are kept
- If
dashboard.jsondoesn't exist, all widgets inwidgets/are shown (unordered)
Widget Format
Each widget file (widgets/{id}.json) contains a single widget object:
{
"id": "weather",
"size": "S",
"renderer": "webview",
"html": "<div>...</div>"
}
Fields:
- id (string): Unique slug identifier — must match the filename (e.g.,
weather→widgets/weather.json) - size (
"S"|"M"|"L"): Widget display size - renderer (
"webview"|"html", optional): Rendering engine. Default is"html"(native). Use"webview"for rich visuals. - html (string): Self-contained HTML with inline styles
Renderers
"webview" — Rich widgets (recommended for most widgets)
Renders in a full browser engine. Supports:
- CSS gradients, shadows,
border-radius,backdrop-filter - CSS animations (
@keyframes,transition) - SVG (charts, icons, sparklines)
- Canvas
- JavaScript (live clocks, counters, data formatting)
- Base64 inline images (
<img src="data:image/png;base64,...">) - Full CSS flexbox and grid
The app wraps your HTML in a document with dark theme defaults (background: #1f2937, color: #fff, system font). You just provide the body content.
"html" — Lightweight widgets (default)
Renders via native components (no browser). Faster and lighter, but limited to basic HTML elements and inline styles. No JS, no animations, no gradients, no SVG. Good for simple text/number displays.
Use "webview" for anything visual. Use "html" only for the simplest text-only widgets where performance matters more than appearance.
Widget Sizes
Modeled after Apple's iOS widget system:
| Size | Dimensions | Layout | Best for |
|---|---|---|---|
| S (Small) | 174 × 174 px | Square, 1 column | Single metric, status, icon |
| M (Medium) | 361 × 174 px | Full width, short | Info card, sparkline, summary |
| L (Large) | 361 × 382 px | Full width, tall | Chart, table, calendar, feed |
Two small widgets fit side-by-side. Medium and large span the full width.
Creating a Widget
Two steps: create the widget file, then add it to the index.
cd wonderdash-widgets
mkdir -p widgets
# 1. Create the widget file
cat > widgets/weather.json << 'EOF'
{
"id": "weather",
"size": "S",
"renderer": "webview",
"html": "..."
}
EOF
# 2. Add to dashboard.json index
# If dashboard.json doesn't exist yet, create it:
echo '{ "widgets": ["weather"] }' > dashboard.json
# If it already exists, read it, append the new ID to the widgets array, and write it back.
git add widgets/weather.json dashboard.json
git commit -m "Add weather widget"
git push
Updating a Widget
Edit the widget file and push. No change to dashboard.json needed unless reordering.
git add widgets/weather.json
git commit -m "Update weather widget"
git push
Reordering / Archiving / Deleting
- Reorder: Rearrange the array in
dashboard.json, commit, push. - Archive: Remove the ID from
dashboard.json— widget file stays in repo. - Restore: Add the ID back to the array.
- Delete permanently: Remove from
dashboard.json+git rm widgets/{id}.json, commit, push.
The user pulls down on their dashboard to refresh. Changes appear after a manual refresh.
Design Guidelines
- Dark theme. Background:
#1f2937(gray-800). Text:#ffffff. Secondary:#9ca3af(gray-400). Accent:#3b82f6(blue-500). Success:#22c55e. Warning:#eab308. Error:#ef4444. - Self-contained. No external resources (CDNs, APIs, remote images). Everything inline.
- JSON escaping. Escape quotes in HTML when inside JSON strings. Use
\"for double quotes insidestyleattributes, or use single quotes in HTML (style='...'). - Fit the size. Content must fit within the widget dimensions without scrolling. WebView has
overflow: hiddenby default. - Subtle animation. Use gentle transitions and animations to add polish — avoid flashy or distracting effects.
Examples
Small — Gradient metric (widgets/temperature.json)
A temperature display with gradient background and subtle glow.
{
"id": "temperature",
"size": "S",
"renderer": "webview",
"html": "<div style='display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;background:linear-gradient(135deg,#1e3a5f,#1f2937);padding:16px'><div style='font-size:11px;color:#60a5fa;text-transform:uppercase;letter-spacing:2px;font-weight:600'>Temperature</div><div style='font-size:52px;font-weight:bold;margin:8px 0;background:linear-gradient(180deg,#fff,#93c5fd);-webkit-background-clip:text;-webkit-text-fill-color:transparent'>72°</div><div style='font-size:13px;color:#9ca3af'>☀️ Sunny</div></div>"
}
Small — Animated status (widgets/api-status.json)
A pulsing status indicator with CSS animation.
{
"id": "api-status",
"size": "S",
"renderer": "webview",
"html": "<style>@keyframes pulse{0%,100%{opacity:1}50%{opacity:.5}}.pulse{animation:pulse 2s ease-in-out infinite}</style><div style='display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:16px'><div style='font-size:11px;color:#9ca3af;text-transform:uppercase;letter-spacing:2px;margin-bottom:12px'>API Status</div><div class='pulse' style='width:48px;height:48px;border-radius:50%;background:radial-gradient(circle,#22c55e,#15803d);box-shadow:0 0 20px rgba(34,197,94,0.4);margin-bottom:12px'></div><div style='font-size:15px;font-weight:600;color:#22c55e'>Operational</div><div style='font-size:12px;color:#6b7280;margin-top:4px'>99.9% uptime</div></div>"
}
Medium — SVG sparkline chart (widgets/revenue.json)
A revenue card with an inline SVG sparkline.
{
"id": "revenue",
"size": "M",
"renderer": "webview",
"html": "<div style='display:flex;align-items:center;height:100%;padding:16px;gap:20px'><div style='flex:1'><div style='font-size:11px;color:#9ca3af;text-transform:uppercase;letter-spacing:1px'>Monthly Revenue</div><div style='font-size:32px;font-weight:bold;margin:6px 0'>$48.2k</div><div style='font-size:13px;color:#22c55e;font-weight:500'>↑ 12.5% vs last month</div></div><div style='width:140px;height:60px'><svg viewBox='0 0 140 60' style='width:100%;height:100%'><defs><linearGradient id='g' x1='0' y1='0' x2='0' y2='1'><stop offset='0%' stop-color='#3b82f6' stop-opacity='0.3'/><stop offset='100%' stop-color='#3b82f6' stop-opacity='0'/></linearGradient></defs><path d='M0,45 L20,40 L40,42 L60,30 L80,35 L100,20 L120,18 L140,10 L140,60 L0,60Z' fill='url(#g)'/><polyline points='0,45 20,40 40,42 60,30 80,35 100,20 120,18 140,10' fill='none' stroke='#3b82f6' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/></svg></div></div>"
}
Medium — Gradient info card (widgets/next-meeting.json)
A meeting card with gradient accent border.
{
"id": "next-meeting",
"size": "M",
"renderer": "webview",
"html": "<div style='display:flex;align-items:center;height:100%;padding:16px;gap:16px'><div style='width:4px;height:80%;border-radius:2px;background:linear-gradient(180deg,#8b5cf6,#3b82f6)'></div><div style='flex:1'><div style='font-size:11px;color:#9ca3af;text-transform:uppercase;letter-spacing:1px'>Next Meeting</div><div style='font-size:18px;font-weight:bold;margin:6px 0'>Sprint Planning</div><div style='font-size:13px;color:#9ca3af'>2:00 PM — 3:00 PM</div><div style='display:flex;gap:6px;margin-top:8px'><span style='font-size:11px;background:#374151;color:#d1d5db;padding:2px 8px;border-radius:10px'>Team Alpha</span><span style='font-size:11px;background:#374151;color:#d1d5db;padding:2px 8px;border-radius:10px'>Zoom</span></div></div></div>"
}
Large — Price chart with SVG (widgets/btc-price.json)
A Bitcoin price chart with gradient fill, grid lines, and price labels.
{
"id": "btc-price",
"size": "L",
"renderer": "webview",
"html": "<div style='display:flex;flex-direction:column;height:100%;padding:16px'><div style='display:flex;justify-content:space-between;align-items:baseline;margin-bottom:4px'><div><div style='font-size:11px;color:#9ca3af;text-transform:uppercase;letter-spacing:1px'>Bitcoin</div><div style='font-size:28px;font-weight:bold;margin-top:4px'>$67,432</div></div><div style='text-align:right'><div style='font-size:14px;color:#22c55e;font-weight:600'>+2.4%</div><div style='font-size:12px;color:#6b7280'>24h</div></div></div><div style='flex:1;margin-top:8px;position:relative'><svg viewBox='0 0 330 240' style='width:100%;height:100%' preserveAspectRatio='none'><defs><linearGradient id='cg' x1='0' y1='0' x2='0' y2='1'><stop offset='0%' stop-color='#f59e0b' stop-opacity='0.25'/><stop offset='100%' stop-color='#f59e0b' stop-opacity='0'/></linearGradient></defs><line x1='0' y1='60' x2='330' y2='60' stroke='#374151' stroke-width='0.5'/><line x1='0' y1='120' x2='330' y2='120' stroke='#374151' stroke-width='0.5'/><line x1='0' y1='180' x2='330' y2='180' stroke='#374151' stroke-width='0.5'/><text x='325' y='58' fill='#6b7280' font-size='10' text-anchor='end'>69k</text><text x='325' y='118' fill='#6b7280' font-size='10' text-anchor='end'>67k</text><text x='325' y='178' fill='#6b7280' font-size='10' text-anchor='end'>65k</text><path d='M0,180 C30,175 50,160 80,140 C110,120 130,90 160,100 C190,110 210,130 230,80 C250,40 270,55 300,30 C310,25 320,28 330,20 L330,240 L0,240Z' fill='url(#cg)'/><path d='M0,180 C30,175 50,160 80,140 C110,120 130,90 160,100 C190,110 210,130 230,80 C250,40 270,55 300,30 C310,25 320,28 330,20' fill='none' stroke='#f59e0b' stroke-width='2.5' stroke-linecap='round'/><circle cx='330' cy='20' r='4' fill='#f59e0b'/><circle cx='330' cy='20' r='8' fill='#f59e0b' opacity='0.2'/></svg></div><div style='display:flex;justify-content:space-between;font-size:11px;color:#6b7280;margin-top:4px'><span>6h ago</span><span>4h</span><span>2h</span><span>Now</span></div></div>"
}
Large — Calendar grid (widgets/calendar.json)
A monthly calendar with CSS grid and highlighted dates.
{
"id": "calendar",
"size": "L",
"renderer": "webview",
"html": "<style>.day{display:flex;align-items:center;justify-content:center;height:36px;border-radius:50%;font-size:13px;color:#d1d5db}.today{background:#3b82f6;color:#fff;font-weight:bold}.event{position:relative}.event::after{content:'';position:absolute;bottom:2px;left:50%;transform:translateX(-50%);width:4px;height:4px;background:#f59e0b;border-radius:50%}.muted{color:#4b5563}</style><div style='display:flex;flex-direction:column;height:100%;padding:16px'><div style='display:flex;justify-content:space-between;align-items:center;margin-bottom:12px'><div style='font-size:16px;font-weight:bold'>March 2026</div><div style='font-size:12px;color:#9ca3af'>3 events</div></div><div style='display:grid;grid-template-columns:repeat(7,1fr);gap:2px;text-align:center'><div style='font-size:11px;color:#6b7280;padding:4px 0'>Su</div><div style='font-size:11px;color:#6b7280;padding:4px 0'>Mo</div><div style='font-size:11px;color:#6b7280;padding:4px 0'>Tu</div><div style='font-size:11px;color:#6b7280;padding:4px 0'>We</div><div style='font-size:11px;color:#6b7280;padding:4px 0'>Th</div><div style='font-size:11px;color:#6b7280;padding:4px 0'>Fr</div><div style='font-size:11px;color:#6b7280;padding:4px 0'>Sa</div><div class='day'>1</div><div class='day today'>2</div><div class='day'>3</div><div class='day event'>4</div><div class='day'>5</div><div class='day'>6</div><div class='day'>7</div><div class='day'>8</div><div class='day'>9</div><div class='day event'>10</div><div class='day'>11</div><div class='day'>12</div><div class='day'>13</div><div class='day'>14</div><div class='day'>15</div><div class='day'>16</div><div class='day'>17</div><div class='day event'>18</div><div class='day'>19</div><div class='day'>20</div><div class='day'>21</div><div class='day'>22</div><div class='day'>23</div><div class='day'>24</div><div class='day'>25</div><div class='day'>26</div><div class='day'>27</div><div class='day'>28</div><div class='day'>29</div><div class='day'>30</div><div class='day'>31</div></div></div>"
}
Example dashboard.json
{
"widgets": ["temperature", "api-status", "revenue", "next-meeting", "btc-price", "calendar"]
}