Glance MCP
Public MCP lets ChatGPT, Claude, or Cursor create and update Glance widgets. The host is the intelligence layer. Glance is the Home Screen.
Install Glance from the ChatGPT plugin directory (recommended), or add the connector on glance.cool/mcp. This page is how it works after you connect.
What each side does
- Your host — Jira, mail, the other apps already connected in ChatGPT or Claude. Glance does not need a Jira connector.
- Glance — the widget slot, the layout language, ingest, and pinning on iPhone.
That is different from Glance AI in the iOS app. For Jira, Gmail, and the rest of the host's apps, the host fetches then calls ingest. Public MCP can also attach on-device Apple sources with create_automation (Weather, Calendar, Reminders, Motion). Cloud Glance-pull — GitHub, Google Calendar on a cron, OAuth connect — stays in the iOS app. Glance is free to connect. The ChatGPT plugin is free; a Glance Pro plan only unlocks extra widget capabilities. For a manual connector instead of the directory plugin, use Developer Mode on glance.cool/mcp.
Auth
- MCP — sign in on glance.cool and Allow. The host then calls tools with your OAuth token. Live updates use the
ingesttool. Do notcurlthe ingest URL from the host. - Write key HTTP — for Shortcuts, Zapier, Make, or your backend. Same XOR payload. The key is shown once when the widget is created. Dynamic Ingest.
Typical flow
Agent-push — the host already has the data (Jira, mail, a headline).
get_plan— do not assume ChatGPT Plus or Claude Pro is Glance Pro. Checkui_on_demandand the fourfamily_*flags before promising views, in-place reveal, charts, grid, or table.list_widgets— if any exist, ask whether to update one or create another.- Design a tree from Layout Language and
list_golden_examples. - Save a layout with
create_template(treeplus optionalviews), or skip that on Pro+ and sendinline_uion ingest. - If new:
create_dynamic_widget, theningest. - Pin on iPhone. Pin to Home Screen.
On-device sources
create_automation on this server attaches an Apple source so the iPhone widget can read it locally. It is not Glance-pull: no Google, GitHub, Jira, or cron. Omit connection_id and schedule_cron. On-device widgets always use a saved layout (template_id), never inline_ui.
get_plan+list_widgets— ask create vs update.- Permission: user opens Glance → Settings → Integrations and taps Connect on that source only. The host cannot grant iOS permissions or check from chat. Wait until they confirm.
- Design a list (
event_1_*/reminder_1_*) or acalendarnode with{{events}}for Apple Calendar. create_template→create_dynamic_widget→create_automation(the four ids below only).ingestplaceholders for every binding so the layout attaches, then open Glance once and pin.
| integration_id | Permission | Bind these names |
|---|---|---|
| weatherkit | Weather (Location) | current_temp, feels_like, high_temp, low_temp, condition, humidity, uv_index, wind_speed, city_name, country_name |
| eventkit_calendar | Apple Calendar | List: today_count, event_1_title, event_1_time, … event_N_*. Calendar node: {{events}} (event[]). |
| eventkit_reminders | Apple Reminders | due_today_count, overdue_count, reminder_1_title, reminder_1_due, … — list only, never the calendar node. |
| coremotion | Motion & Fitness | steps_today, steps_today_fmt, distance_today, floors_today, activity_type |
Apple Calendar may be a text list (event_1_title / event_1_time, set maxEvents) or a type: "calendar" node bound to {{events}} with field_mapping: {}. calendar, shape, and mapping are Free. Reminders stay a list — do not bind reminder_* to the calendar node. You can combine sources (weather + calendar) in one call.
{
"feed_id": "<uuid>",
"template_id": "<uuid>",
"sources": [
{
"integration_id": "eventkit_calendar",
"field_mapping": {},
"config": { "maxEvents": "3", "daysAhead": "7" }
}
]
}eventkit_calendarconfig:maxEvents1–20 (default 5) for list slots;daysAhead1–90 (default 14; use 31–45 for month or columns). Always emitsevents(event[], cap 100).eventkit_remindersconfig:maxReminders1–10 (default 5). Weather and Motion:{}.- After success,
ingestplaceholders for every binding so the layout attaches. Device automations do not writefeed_state— skip this and the Home Screen has no layout. Placeholders only; live Apple data overwrites on the phone. Then open Glance once, then pin.
Ingest from the host
content keys are binding names inside {{ }}, not Jira or GitHub field IDs. Exactly one layout arm. After create_automation, this is still required — placeholders, not a live Calendar/Weather feed.
// Saved layout
{ "feed_id": "<feed uuid>", "template_id": "<template uuid>", "content": { "headline": "Standup in 10 min" } }
// UI on Demand (Pro+)
{ "feed_id": "<feed uuid>", "inline_ui": { "tree": { … }, "views": { "detail": { … } }, "widget_size": "medium" }, "content": { "headline": "Standup in 10 min" } }Never send template_id with inline_ui or a top-level tree. If get_plan.capabilities.ui_on_demand is false, only template_id. inline_ui.views is optional, same as on create_template. Details and the HTTP shape are on Dynamic Ingest · UI on Demand.
Tools
Public MCP is a subset. Cloud automations, integration OAuth, and the iOS permission prompt stay in the iOS app / Glance AI. create_automation here is the on-device attach only.
| Tool | Role |
|---|---|
| get_plan | Live quotas and capabilities. Call before promising UI on Demand, views, in-place reveal, charts, grid, or table. |
| list_widgets | Existing widgets. Ask create vs update. Do not silently duplicate. |
| create_dynamic_widget | New widget + feed. Returns write_key once, for Shortcuts / HTTP only. |
| get_widget_status | Last ingest, live layout, uiMode. Never returns a write key. |
| list_templates / get_template | Saved layouts. get_template returns tree + optional views. |
| validate_template | Optional. Structural errors block save. Design warnings do not. |
| create_template / update_template | Save or revise a layout. tree is home; views is an optional sibling. |
| list_golden_examples | S1–L2 composition and v2-* language trees. Call before the first new layout. |
| ingest | Push live content. OAuth only. XOR template_id or inline_ui. |
| create_automation | Attach Weather, Apple Calendar, Reminders, or Motion to a feed. On-device only — no cron, no Google/GitHub/Jira. |
| delete_widget | Permanent. Confirm first. |
Plans
agent_messages_per_day counts in-app Glance AI only, not ChatGPT or Claude tokens. UI on Demand, views, in-place reveal (toggle_flag + visibleFlag), charts, grids, and tables are Pro and Power. Shape, mapping, and calendar are Free. Call get_plan rather than guessing. The live numbers sit on Features.
Design skill
Same file the host can read at glance://skills/design. MCP is the tools. This skill is how the host should compose the widget. Install from glance.cool/mcp: SKILL.md, zip, or npx skills add https://glance.cool/skills/glance-design/SKILL.md.
Glance Widget Design Skill
You design iOS home screen widgets that feel native, calm, and glanceable — like Apple built them. This skill covers visual design and composition only. Workflow, tools, integrations, and sizing math live in the main agent skill.
Diversity over templates: Every widget should fit its data and the user's intent. These are principles and patterns — not a single layout to copy. Vary structure, density, and emphasis. Never force one recipe when another fits better.
Design north star
Widgets should feel like Apple system UI: clear hierarchy, generous whitespace, restrained color, SF typography. A user glancing at their home screen should read the widget in under two seconds.
| Trait | What it means in practice |
|---|---|
| Glanceable | One clear takeaway — a number, status, or headline |
| Native | System fonts, semantic colors, no "web app" density |
| Calm | Mostly neutral surfaces; color carries meaning, not decoration |
| Intentional | Every element earns its space — if it doesn't help, remove it |
Color — Apple-native default, user-guided override
Default (no user color preference)
Think Settings / Stocks / Weather — not marketing landing pages.
| Role | Guidance |
|---|---|
| Background | Omit background on root for system adaptive fill, or #FFFFFF / #F2F2F7 (light) — never loud gradients unless the user asks |
| Primary text | #000000 or omit color (system primary) |
| Secondary / labels | #8E8E93 (iOS secondary label) — captions, timestamps, units |
| Tertiary / hints | #AEAEB2 — de-emphasized metadata only |
| Accent | One semantic accent per widget — e.g. #007AFF (system blue) for links/active, #34C759 success, #FF9500 warning, #FF3B30 alert |
| Progress / fills | Muted track #E5E5EA, fill = single accent |
Rules: - Never use more than one accent hue unless the user requests a colorful or branded look. - Never fill large areas with saturated color — accents belong on small elements (badge, progress fill, status dot, one headline word). - Prefer opacity and weight over more colors to create hierarchy.
iOS color format (critical on device)
The iOS renderer only parses 6-digit hex (#RRGGBB). Values like #FFFFFFCC or #FFFFFF40 are ignored and fall back to system .primary (often black on light widgets).
- Use solid 6-digit hex for every
color,background,backgroundColor,trackColor,fillColor. - For muted/secondary text on dark or gradient backgrounds, pick a lighter solid tint (e.g.
#FFE8F5on purple gradients) — not alpha suffixes. - On colorful widgets, desaturated label colors still beat 8-char hex.
When the user gives color direction
Follow the user. If they say "dark purple theme", "brand pink #E91E8C", or "match my logo colors" — apply their palette to backgrounds, accents, and text while keeping hierarchy and readability. Still use muted variants for labels (#8E8E93 or a desaturated version of their accent).
If they share no preference, use the Apple-native defaults above.
Typography
Use SF Pro (sf-pro or omit fontFamily). Reserve sf-rounded for playful/countdown widgets; sf-mono for code/GitHub hashes only.
| Role | fontSize | fontWeight | color |
|---|---|---|---|
| Hero value | 24–32 | bold or heavy | primary |
| Section title | 15–17 | semibold | primary |
| Body / detail | 13–15 | regular or medium | primary or secondary |
| Caption / label | 10–12 | regular or medium | #8E8E93 |
| Unit / suffix | 11–13 | medium | secondary — on stat via suffix |
Limits: At most 3 distinct font sizes per widget. At most 2 weights in active use (e.g. semibold + regular).
Always set maxLines: 1 on titles and hero lines in small and medium widgets. Use maxLines: 2 sparingly on large only when content demands it.
Spacing rhythm
Pick one rhythm and stick to it for the whole tree:
| Token | Use |
|---|---|
| 8 pt | Tight gaps inside a row (icon + label) |
| 12 pt | Root padding, default spacing within sections |
| 16 pt | Between major sections on large widgets |
Root container: `padding: 12` minimum (16 for airy large layouts). Never mix 6 / 10 / 14 randomly.
Use spacer with flex: 1 to push footer elements (badge, timestamp) to the bottom on large — don't stack everything at the top.
Visual hierarchy — the one-hero rule
Every widget needs one focal point:
| Size | Typical hero |
|---|---|
small | One stat value, one large number text, or one avatar + short label |
medium | Hero left or top + supporting detail opposite/below |
large | Hero row + 2–4 supporting metrics or a content block |
Everything else is supporting — smaller, lighter color, fewer lines.
Do not give equal visual weight to five text lines. If everything is bold and 16 pt, nothing is important.
Static vs dynamic bindings
Only {{variable_name}} becomes a content field (upload form, automation ingest, bindings_required).
| Use case | Template syntax | In derived schema? |
|---|---|---|
| Live data (username, KPI value, event title) | "binding": "{{username}}" | Yes |
| Fixed section label ("Profile views today") | "binding": "Profile views today" | No |
| Fixed metric name under a ring ("Revenue") | "binding": "Revenue" | No |
| Decorative brand icon (design constant) | "binding": "https://…/icon.png" on image | No |
| SF Symbol | "binding": "bag.fill" on icon | No |
| Progress-dependent caption | "binding": "{{overall_goal_caption}}" | Yes |
Rule: If the copy or asset never changes per refresh, use a plain literal — not {{}}. This keeps the upload flow clean and matches how the goldens are authored.
Static images on device: use HTTPS PNG/JPEG URLs. Local paths like /brand-icons/… only work in React preview.
Golden reference library
Six device-approved examples live in packages/sdk/goldens/. They are the quality bar — adapt their structure to the user's data; never paste one verbatim with wrong bindings.
Before designing: call list_golden_examples (filter by widget_size or fetch one with example_id).
| ID | Size | Pattern | When to adapt |
|---|---|---|---|
| s1-glance-kpi | small | Gradient + username + 2 stacked KPI stats + static brand icon | Social/account KPIs, branded small metrics |
| s2-glance-media | small | White card: static section label → title → centered thumbnail → caption | Latest post/video/article thumbnails |
| m1-split-strip | medium | Left: static title stack · Right: hero stat | Single headline metric strips (monitoring, uptime) |
| m2-multi-source | medium | Left motion panel + right spend card + calendar event rows | Multi-integration dashboards (device + cloud) |
| l1-app-performance | large | Dark 2×2 KPI rings (z-stack + circular_progress) + quarterly progress_bar | Custom business metrics with goal progress |
| l2-outfit-of-the-day | large | Gradient + hero image + shoppable item rows (href + icon) | Creator commerce, product lists, playful layouts |
Language v2 references (same folder, v2- prefix). Load with example_id. Free users may use agenda. Pro+ goldens need get_plan family flags first.
| ID | Size | Plan | When to adapt | |||
|---|---|---|---|---|---|---|
| v2-vocab-flip | small | Pro+ views | In-place reveal — whole-card toggle_flag + visibleFlag, no views | |||
| v2-today-agenda | small | Free | Next few events as a list — calendar agenda + event[] | |||
| v2-revenue-line | medium | Pro+ charts | Today's total + cumulative hours — chart line, showAxes: true | |||
| v2-weekday-bar | medium | Pro+ charts | 7-day counts — chart bar, showAxes, showAverage, colorScale | |||
| v2-share-pie | small | Pro+ charts | Parts of a whole (≤ 8) — chart pie + innerRadius | |||
| v2-activity-grid | medium | Pro+ grid | Heat over days/weeks — grid activity + colorScale | |||
| v2-standings-table | medium | Pro+ table | Compact labeled numbers — table 3×3-ish, never 5 columns | |||
| v2-mission-control | large | Pro+ views + charts | Four-page hub — home summary + bottom nav `Home \ | Dashboard \ | Mail \ | Calendar` |
Golden techniques worth copying
- S1:
gradientroot; static caption literals above dynamicstatvalues; trailing brandimagewith static URL - S2: explicit
spacergaps between title/image/caption;maxLineson all text; white#FFFFFFroot background - M1: horizontal split with
flex: 1text column + trailing herostat(valueFontSize: 40) - M2: nested gray cards (
#F2F2F7); color accent bars beside calendar rows; formatted strings intextnotstat - L1: ring container 82×82 with
circular_progresssize 70, lineWidth 5 (prevents stroke clip); value text centered inz-stack;overall_goal_captionstays dynamic - L2:
gradientwithspacingas gap;fillContainerhero image;hrefon shop tap targets;sf-roundedfor playful headers - v2-vocab-flip: whole-card
toggle_flag+visibleFlag; prompt stays; answer appears on the same screen; noviews; reload clears the flag - v2-revenue-line: horizontal split, no divider; copy + insight +
relative_timeon the left; full-heightchartlineshowAxes: trueon the right - v2-weekday-bar: same split on light
#F2F2F7; copy + 7-day average on the left; full-heightchartbarwith axes,showAverage, andcolorScalebands (red < 10, yellow < 20, green 20+) - v2-mission-control: Notion monochrome (
#FFFFFF/#37352F/#9B9A97). Home is greeting + KPIs + unread mail + agenda. Duplicate a 4-tab bottom nav ontreeand every named view; selected tab is ink, the rest muted. Named views:dashboard(today's revenue — full-width cumulative line, hourly bars beside App / Webapp / B2B mix),mail(3 summaries),calendar(columnsdays: 3). Discover stays on home — home must read as a full widget.
Size patterns (adapt — do not copy blindly)
Prefer picking a golden reference above, then adapt. These ASCII sketches are fallbacks when no golden fits:
Small (170×170) — "one glance"
Best for: single KPI, countdown, status, one image thumbnail.
vertical container, padding 12, spacing 8 → caption text (12pt, secondary, maxLines 1) // label → stat OR large text (hero, 26–30pt) // THE number/word → optional: badge OR relative_time (11pt) // context
Avoid: horizontal rows of 3+ items, multiple stats, long paragraphs.
Medium (364×170) — "headline strip"
Best for: integration dashboard, social snapshot, calendar next event.
Pattern A — split row:
vertical, padding 12
→ horizontal row, spacing 12, alignment center
→ left column (vertical): title + subtitle
→ right: stat OR circular_progress OR avatar/imagePattern B — title + detail row:
vertical, padding 12, spacing 8 → title text (15pt semibold, maxLines 1) → horizontal row: 2–3 compact stats OR image + text pair
Avoid: more than one horizontal band; cramming 4 columns.
Large (364×382) — "dashboard card"
Best for: multi-metric boards, feed digests, rich integration views.
vertical, padding 12–16, spacing 12 → header row (horizontal): title + optional badge → main content: 2–3 stats in a row OR 2×2 grid of images/metrics → spacer (flex 1) // optional → footer: progress_bar OR relative_time OR caption // optional
Use divider between header and body only when it clarifies — not by default.
Component combinations that look polished
| Combo | When to use |
|---|---|
stat + caption text above | KPI widgets (followers, revenue, streak) |
avatar + vertical text stack in horizontal row | Social / account identity |
image (cornerRadius 8) + text beside | Latest post, thumbnail + caption |
badge (subtle bg #E5E5EA, text primary) | Status chips — "Live", "3 new" |
progress_bar (height 6–8, muted track) | Goal completion — one per widget |
z-stack + full-bleed image + text with light color | Event spotlight — use sparingly |
status_dot + text | Health / server / pipeline status |
relative_time or date in caption style | "Updated 2m ago" footer |
shape circle + mapped fillColor | One number that should read as a state (score, health) |
calendar agenda + caption | Today's events — do not rebuild event rows from text |
chart line, showAxes: false, height 64–80 | Sparkline under a title on medium |
chart pie + innerRadius 0.55 + caption | Donut KPI on small |
chart bar, showAxes: true, showAverage: true | Category counts vs a typical day — dashed mean of y |
grid 1×7 week strip | Habit / steps this week |
table 3 cols × ≤ 4 rows | Standings, ranks, tiny scoreboard |
Bottom nav text + set_view on every view | Mission Control — selected tab ink, others muted. Duplicate the bar; do not hide it. |
Whole-card toggle_flag + visibleFlag on small | Vocab card — prompt stays, answer appears on the same screen |
`stat` tips: Use valueFontSize 26–32 for hero stats; labelFontSize 11; suffix for units (K, %, °). Prefer ig_followers_fmt style formatted strings in text when the integration provides them.
Layout polish
- Alignment:
leadingfor LTR text blocks;centerfor single hero in small;trailingfor meta (time, badge) in header rows — but see reload button rule below. - Reload button: Glance overlays a reload control in the top-right (~40×40 pt). Never put hero values or must-read text there; prefer leading clusters for top-band KPIs (label + value side-by-side).
- Images: Always set explicit
width/height;cornerRadius: 8–12for photos;contentMode: "fill"for thumbnails. - Nested cards: Inner
containerwithbackground: "#F2F2F7",cornerRadius: 10,padding: 10— sparingly, one level deep max. - Full bleed: Only
gradientroot for dramatic widgets — and only when user wants flair or data suits it (event poster, media highlight).
Anti-patterns — never ship these
| Anti-pattern | Why it fails |
|---|---|
5+ text nodes all 14 pt regular | No hierarchy — wallpaper of words |
| Rainbow hex on every element | Feels non-native, noisy |
Horizontal row of 4 images on small | Guaranteed clip / crush |
stat for currency ($1,234) or % strings | Type errors + ugly fallback |
Root padding: 0 or missing | Corner clip on home screen |
| Hero stat / spend in top-trailing corner | Hidden under reload button overlay |
gap, borderRadius, textAlign style keys | Silently ignored — use correct keys from main skill |
{{}} on fixed labels or decorative icons | Forces upload fields; use static literals instead |
8-char hex colors (#RRGGBBAA) | iOS ignores them — text falls back to black primary |
bar_chart / pie_chart / webview / JS ternaries | Invalid. Use type: "chart" + variant, or a stops / when map |
Chart or views on Free without checking get_plan | 403 at save. Offer agenda / v1 stand-in, or upgrade |
| Five equal tap targets on a small hub | No hero. One primary view, others supporting |
Calendar rebuilt as event_1_title rows when they asked for a calendar | Use calendar + event[]. Indexed fields stay valid for v1 lists |
Grid rows/columns as bindings | Those are ints on the node. binding is the number[] |
Table columns: 3 (a number) | Table columns must be "{{headers}}" (string[]) |
| Month calendar with event titles | Month is dots only. Use agenda or columns for titles |
Discover / marketing tile that relies on set_view | Tiles stay on home. Views work on the subscriber Home Screen |
Layout language v2 — exact authoring
v1 trees stay valid. Anything below sets required_version: 2. Do not invent types, CSS keys, or JavaScript in bindings.
Call `get_plan` first.
| Capability | Free | Pro+ |
|---|---|---|
19 v1 types, shape, mapping, calendar | Yes | Yes |
views / action / visibleFlag (family_views) | No | Yes |
type: "chart" (family_charts) | No | Yes |
type: "grid" (family_grid) | No | Yes |
type: "table" (family_table) | No | Yes |
validate_template never 403s — it returns families_used. create_template / update_template / inline_ui 403 PREMIUM_FAMILY_REQUIRED if a flag is false. Offer a Free stand-in (agenda, stat, or both sides of a card as text) or say they need Pro.
Power is quotas, not a third language. Do not variant-gate calendar.
Mapping — closed functions of a number
Any node's color / *Color / background / fillColor / opacity / visible may be a map object instead of a hex or {{binding}}. No {{n > 80 ? green : red}}.
from must be the entire value "{{number_field}}". Choose exactly one of stops or when.
`stops` — interpolate between points. Colors blend; numbers/opacity lerp. Never use stops on visible.
"fillColor": {
"from": "{{score}}",
"stops": [
{ "at": 0, "value": "#FF3B30" },
{ "at": 20, "value": "#FFCC00" },
{ "at": 80, "value": "#34C759" }
]
}`when` — first matching clause wins. One operator per clause: lt | lte | gt | gte | eq. Optional else.
"visible": {
"from": "{{count}}",
"when": [{ "lt": 1, "value": false }],
"else": true
}colorScale on chart / grid / table is a map evaluated per mark / cell against that cell's number. Write "from": "{{activity}}" even though activity is a number[] — do not also bind activity as a scalar. Zero cells on a grid use emptyColor, not the scale.
Use mapping when the look should change with a number (tinted button, fading card, score ring). Prefer when for on/off and discrete bands; stops for a continuous wash. from must be a number field ({{steps_today}}, {{score}}) — never a formatted string ({{steps_today_fmt}}). Hex is 6-digit.
shape — Free
variant: circle | rectangle | capsule | line | triangle. Optional href (or action on Pro+). No children.
| Style | Type | Control |
|---|---|---|
size | number | Diameter / edge. Wins over width / height when set |
width / height | number | Use when the shape is not square (line, wide capsule) |
fillColor | hex, {{color}}, or map | Interior. Ignored on `line` |
strokeColor | hex / map | Outline |
strokeWidth | number | Outline thickness |
cornerRadius | number | rectangle only |
opacity | 0–1 or map | Whole shape |
Hero circle: size 64–80, centered, one caption. Status pip: size 10–14. Do not stack three shapes on small.
calendar — Free
Consumes event[]. The node does not call EventKit itself. Bind {{events}} from eventkit_calendar (device, identity field_mapping: {}) or ingest. Google Calendar is still indexed strings — do not bind it to this node yet. start is ISO 8601; all-day may be yyyy-MM-dd. Omit day / month to use today / this month on device.
variant: agenda | columns | month.
| Variant | What it shows | When to pick |
|---|---|---|
agenda | Start time (or "All day") + title. end is occupancy, not painted. | Default. Small/medium next-up |
columns | Titles only in day columns. Node days: 1, 3, or 7 | Medium/large week strip |
month | Month grid with dots only — no titles | Large "busy this month" |
| Style | Default intent |
|---|---|
width / height | Fill remaining space; set height on medium (≈ 110) |
headerFontSize / headerColor | Day / month chrome — caption scale, often #8E8E93 or white on dark |
eventFontSize / eventColor | Agenda / column titles |
dotColor / todayColor / gridColor | Month marks and today |
maxEvents | Agenda clip (e.g. 3 on small, 5 on medium) |
opacity | Whole node |
Indexed event_1_title rows remain legal. Prefer calendar when they asked for a calendar.
chart — Pro+ family_charts
type is always "chart". `bar_chart` and `pie_chart` are invalid.
variant | Required bindings | Optional | Cap |
|---|---|---|---|
bar | y number[] | x string[] categories | 24 |
line | x number[], y number[] | — | 24 |
scatter | x number[], y number[] | sizes number[] | 24 |
candle | x, open, high, low, close all number[] | — | 24 |
pie | y number[], labels string[] | — | 8 |
x on line / scatter / candle is numeric (unix, hour-of-day). ISO date strings in that array are accepted at ingest. Parallel arrays must be the same length (SERIES_LENGTH_MISMATCH).
| Style | Variants | Control | |
|---|---|---|---|
width / height | all | Medium sparkline height 64–80. Set size on pie instead | |
size | pie | Diameter. Small donut ≈ 75 | |
strokeColor / strokeWidth | line, scatter | Accent stroke. Width 2 | |
fillColor | bar, area-ish line | Single series fill. Or colorScale per bar | |
showAxes | bar, line, scatter, candle | false = sparkline. true = plot plane (grid + Y ticks + X labels) so scale is readable | |
showLabels / labelFontSize / labelColor / axisColor | cartesian | X labels. Follows showAxes unless you set it. Keep both off on small | |
orientation | bar | vertical (default) or horizontal | |
barGap / cornerRadius | bar | Gap 3, radius 3 | |
showValues | bar | Rarely — clips on small | |
colorScale | bar | Map per bar value | |
showAverage / averageColor | bar | Dashed mean of the current y series. Default color #636366. Pair with showAxes | |
showDots / dotSize / curve | line | curve: linear \ | smooth |
innerRadius | pie | 0 = pie, 0.55 = donut | |
colors | pie | string[] of 6-digit hex. Default #7B61FF #34C759 #FF9500 #FF3B30 #007AFF #AF52DE | |
upColor / downColor | candle | Default #34C759 / #FF3B30 | |
opacity | all | Whole mark |
`showAverage` (bar only). Set "showAverage": true when each bar should be judged against a typical day — weekday counts, new users, tickets. The renderer computes the arithmetic mean of the current y series and draws a dashed rule at that Y (or X if orientation is "horizontal"). It is not a second binding and not a second chart. Optional "averageColor" (default #636366). Pair with showAxes: true so the line sits on a readable scale. Line, pie, scatter, and candle ignore it. Do not fake this by stacking a chart line of constants on the bars — Y domains will not match. Web preview and iOS WidgetKit both paint it (Swift Charts RuleMark on device).
One chart is the hero. Do not put a chart and a large stat of equal weight on small. Candle / scatter belong on medium/large.
grid — Pro+ family_grid
variant is activity (omit = activity). `rows` and `columns` are positive ints on the node — not bindings. binding is number[] of length rows × columns. Max 400 cells (7×52 fits; 7×53 does not). Row-major. No per-cell tap.
| Style | Default | Control | ||
|---|---|---|---|---|
width / height | fill | Set height 72–96 on medium | ||
cellShape | square | square \ | circle \ | triangle |
cellGap | 2 | Keep 2. Larger gaps eat the canvas | ||
cellCornerRadius | 2 | Squares only | ||
emptyColor | #EBEDF0 | Color for 0. Dark themes: #161B22 | ||
colorScale | — | Map from the same series binding; each cell uses its own number | ||
opacity | 1 | Whole grid |
Week strip: "rows": 1, "columns": 7. Year: 7 × 52. GitHub-green stops are fine when the user wants that look; otherwise one accent hue.
table — Pro+ family_table
| Field | Type | Required |
|---|---|---|
columns | {{binding}} → string[] | Yes — headers |
cells | {{binding}} → string[] | Yes — row-major |
rows | {{binding}} → string[] | No — row headers |
columns: 3 (a number) is a grid size, not a table. Hard cap 4 columns × 6 rows. Numbers in cells coerce to string. Clip, no scroll.
Row count = rows.length if row headers exist, else cells.length / columns.length (must divide evenly).
| Style | Default | Control |
|---|---|---|
showColumnHeaders | true | |
showRowHeaders | true if rows bound | |
headerBackground / headerColor | #F3F4F6 / #111827 | Chrome. Dark: #1F2937 / #F9FAFB |
cellColor / cellFontSize / cellPadding | #111827 / 11 / 4 | Body type — caption scale |
gridColor / gridWidth | #E5E7EB / 1 | Hairline only |
colorScale | — | Heats a cell if the string parses as a number |
width / height / opacity | fill | Clip inside the box |
3 columns × 3–4 rows on medium. Never five headers.
Views and actions — Pro+ family_views
family_views is two patterns. Pick one per tap — do not mix them for the same interaction.
| Intent | Mechanism | Golden |
|---|---|---|
| Tap shows/hides content on the same screen | toggle_flag + visibleFlag. No views map. | v2-vocab-flip |
| Tap goes to another screen | set_view + views | v2-mission-control |
tree is always home. Optional sibling views is a map of slugs → trees. Max 4 named views. Slug: ^[a-z][a-z0-9_]{0,31}$. Do not name a view home. Depth 8 / 80 nodes per view. 64 KiB for { tree, views }.
Screen swap
{
"action": { "type": "set_view", "view": "dashboard" }
}In-place reveal — flags default false (hidden). visibleFlag shows the node when that flag is true.
{
"action": { "type": "toggle_flag", "flag": "show_answer" }
}{
"id": "answer",
"type": "text",
"binding": "{{answer}}",
"visibleFlag": "show_answer"
}XOR: a node may have action or href, never both (ACTION_HREF_XOR).
Reload (RefreshWidgetIntent) returns home and clears flags, with latest data. Ingest keeps the current view. Discover / marketing tiles stay on home — do not design a tile that only makes sense after a tap. Reveal goldens keep the prompt visible so Discover still reads.
In-place reveal is the small pattern: v2-vocab-flip. Prompt stays; answer appears on the same card. Named views on small feel cramped — prefer toggle_flag, or warn.
Hub pattern (large): v2-mission-control. Home (tree) is a summary — greeting, KPIs, unread mail, today's agenda — not an empty launchpad. Duplicate a 4-tab bottom nav (Home / Dashboard / Mail / Calendar) on home and every named view. Selected tab: ink #37352F semibold. Others: muted #9B9A97. Named views: dashboard (today's revenue: full-width cumulative line, then hourly bars + App / Webapp / B2B pie on one row), mail (fixed email rows — no for_each), calendar (columns, days: 3). set_view "home" returns home. Reload also returns home. Discover tiles stay on home.
Series bindings in content
POST the arrays the tree asked for. Extra keys 400.
{
"prompt": "ventana",
"answer": "window",
"events": [{ "start": "2026-08-30T09:00:00", "title": "Standup" }],
"hours": [9, 10, 11],
"revenue": [12, 18, 15],
"activity": [0, 1, 3, 0, 2, 4, 1],
"headers": ["W", "L", "Pct"],
"stats": ["12", "4", ".750"]
}Design process (before you write JSON)
- Clarify intent — glance KPI? digest? status? media? trend? heat? reveal on the same screen? flip between screens?
- `get_plan` — if they asked for charts / views / grid / table and the family is false, say so and offer a Free stand-in or upgrade. Do not emit the family.
- Pick `widget_size` — smallest size that fits without clutter.
- Choose one hero — what is the single takeaway? (Per view, if using views.)
- `list_golden_examples` — S1–L2 for composition;
v2-*for language nodes. Load withexample_id. - Adapt the pattern — swap bindings for real fields; static labels stay literal.
- Assign typography — hero / body / caption sizes from table.
- Apply color — native defaults unless user specified palette; 6-digit hex only. Maps and
colorScaleuse the same hex rule. - Run sizing math — main skill algorithm; shrink before dropping the hero. Chart/grid/table need an explicit
heightorsize. - (Optional) `validate_template` — max 2 calls per design; then
create_template. Structure must pass; design warnings do not block save.families_usedis informational. - Preview pause — describe what the user will see, including which view is home; invite tweaks.
Pre-submit checklist
Before create_template, confirm mentally (server structure is the save gate; padding/overflow are warnings):
- [ ] One clear hero element (per view)
- [ ] ≤ 3 font sizes, ≤ 2 active weights
- [ ] Root
padding≥ 12 (guideline —ROOT_PADDING_LOWwill not block save) - [ ] Labels use secondary color (
#8E8E93or user-muted variant) - [ ] At most one accent color (unless user asked for more)
- [ ]
maxLinesset on text that must not wrap - [ ] Top-trailing corner clear of hero values (reload button overlay)
- [ ] Static labels/icons use plain literals (no
{{}}unless value changes per refresh) - [ ] All colors are 6-digit
#RRGGBB(no alpha suffix) - [ ] Layout suits the data — not a generic placeholder grid
- [ ]
get_planchecked for anychart/grid/table/views/action - [ ] No
bar_chart,pie_chart, JS ternaries, oraction+hrefon one node - [ ] Chart series lengths match; pie ≤ 8; grid
rows*columns= array length ≤ 400; table ≤ 4×6
Talking to the user about design
- Describe the visual hierarchy in plain language: *"Large follower count up top, latest post caption below in gray."*
- Offer one alternative when useful: *"I can also do a side-by-side layout with your profile photo — prefer that?"*
- If the user says it looks plain, add structure (nested card, badge, progress) or typography contrast — not more random colors.
- For v2: name the control, not the type. *"The word is on the face; tap flips to the translation; reload shows the word again."* *"Home is three tiles; revenue is a sparkline; reload brings you back."*