--- name: visual-output description: Emit rich visual output in chat via tags — styled HTML cards, charts, tables, interactive tools, and drawn SVG/canvas diagrams, illustrations, and animations — theme-aware, centered, and responsive instead of clashing or cramped. triggers: widget, widget iframe, chart, table, visual, dashboard, render widget, illustration, diagram, draw, svg, animation, animate, graph, schematic, visualize, visual explanation, figure context_tier: heavy --- # Visual Output Gideon renders rich HTML inline in chat via `` tags. The HTML runs in a sandboxed iframe that inherits the dashboard's active theme through CSS variables. Get the styling contract right and a widget looks native on every theme — light, dark, or custom. Get it wrong (hardcoded colors, fixed pixel sizes) and it clashes or renders tiny in a corner. This skill covers both halves of visual output: - **The widget container** — format, theme variables, CSP, interactivity, and the card design system (layout, spacing, type, surface, motion). - **Drawn visuals** — SVG/canvas diagrams, schematics, charts, and animations, and the craft of making them centered, proportioned, labelled, and legible. ## Format ```html
…content…
``` - One widget per payload, self-contained. - Tailwind CSS is available inside the iframe. - For large HTML (long dashboards, big tables), save it to a file or an Artifact (see the `artifacts` skill) and reference it — don't inline hundreds of lines into the chat. ## Design system — make it look intentional The difference between a widget that looks native and one that looks slapped together is **consistent layout, spacing, type, and alignment**. Follow these. ### Layout & alignment - **Cap the width and center it.** Wrap content in `max-w-md`/`max-w-xl`/`max-w-2xl` (pick by density) with `mx-auto`. A widget that sprawls full-width in a wide chat reads as broken. Dashboards: `max-w-3xl`; a single stat/card: `max-w-sm`. - **One alignment spine.** Left-align text content; center only standalone figures (charts, single big numbers, illustrations). Don't mix center + left in one card. - **Use a grid for multi-item layouts** — `grid grid-cols-2 gap-3` (or `gap-4`), not floats or ad-hoc margins. Equal gaps everywhere; let the grid do alignment. - **Group related, separate unrelated** with whitespace + hairline dividers (`border-t border-[var(--border)]`), not boxes-within-boxes. ### Spacing rhythm (4px scale) Stick to a consistent scale so spacing feels deliberate: `gap-2`(8) `gap-3`(12) `gap-4`(16); padding `p-4`/`p-5` for cards, `p-3` for compact rows. Pick ONE card padding and one gap size per widget and reuse them. Avoid arbitrary `mt-[7px]`. ### Typography hierarchy - **Title** of the widget content: `text-base font-semibold` (or `text-lg` for a hero), `var(--text-strong)`. - **Body**: `text-sm`, `var(--text)`. **Captions/labels**: `text-xs`, `var(--muted)`. **Big stat numbers**: `text-2xl`/`text-3xl font-semibold tabular-nums`. - Max ~2 type sizes per card beyond the title. Use weight + color for hierarchy, not many sizes. `tabular-nums` for any aligned numbers/tables. ### Surface & depth - Cards: `rounded-lg` (or `rounded-xl` for hero), `bg-[var(--card)]`, `border border-[var(--border)]`. One elevation level — don't nest cards 3 deep. - Subtle, not heavy: prefer a hairline border + tint over big shadows. ### Motion - Use CSS `transition`/`@keyframes` (no JS needed for most). Easing `cubic-bezier(0.2,0,0,1)` (or `ease-in-out`); **150–300ms** for UI transitions, 2–6s for ambient loops. Animate `opacity`/`transform` only (cheap, smooth). - Reveal-on-load is fine once; avoid perpetual motion unless it's the point (a live pulse/orbit). Always honor reduced-motion: ```css @media (prefers-reduced-motion: reduce){*{animation:none!important;transition:none!important}} ``` ## Theme variables — use these, never hardcoded colors The iframe injects the active theme's palette as CSS custom properties. Style **everything** with them (directly via `style="…: var(--x)"` or Tailwind arbitrary values like `bg-[var(--card)]`) so the widget tracks the theme: | Variable | Use for | |---|---| | `var(--bg)` | page / outermost background | | `var(--text)` | default body text | | `var(--card)` / `var(--card-fg)` | panel surface / text on it | | `var(--border)` | hairlines, dividers, input borders | | `var(--accent)` / `var(--accent-hover)` | primary actions, links, highlights | | `var(--muted)` / `var(--muted-strong)` | secondary / tertiary text | | `var(--text-strong)` | emphasized headings | | `var(--bg-elevated)` / `var(--bg-hover)` | raised surfaces / hover states | | `var(--border-strong)` | stronger separators | | `var(--ok)` / `var(--warn)` / `var(--danger)` / `var(--info)` | status colors | | `var(--accent-subtle)` / `var(--ok-subtle)` / `var(--warn-subtle)` / `var(--danger-subtle)` | tinted fills/badges | **Never** use `bg-gray-900`, `text-white`, `#fff`, or any hardcoded hex — they break on the opposite theme. Zero hardcoded colors. ## Allowed scripts (CSP) The iframe's Content-Security-Policy restricts `script-src` to inline scripts plus these CDNs only: - **Tailwind** — `https://cdn.tailwindcss.com` (preloaded; classes work out of the box) - `https://cdn.jsdelivr.net` - `https://cdnjs.cloudflare.com` Load charting/visualization libs (e.g. Chart.js) from jsDelivr or cdnjs. Scripts from any other origin are blocked. `connect-src` is `'none'` — widgets can't make network calls; render data you already have. ## Interactive widgets Widgets send events back to the agent. Add `data-action` (and optional `data-payload`, a JSON string) to any clickable element: ```html ``` On click, the dashboard auto-submits a user message: `[UI] approve: {"id":"123"}`. You receive it as the next turn and can respond with text, a new widget, or both. **Forms:** inputs with a `name` attribute are auto-collected on click and merged into the payload as `formData`. Use this for creation forms — render pre-filled inputs, the user adjusts values, clicks submit, and you receive every field: ```html ``` **Living views (auto-refresh).** A widget can be a *living dashboard* that pulls fresh data on demand instead of freezing a snapshot at creation. Give it a refresh button: ```html ``` When the widget has been **saved as an artifact** (it has a stable slug), the dashboard appends `(refresh artifact "" in place)` to the `[UI] refresh` message it sends you. On that turn: re-fetch the current data (call the tools/searches that produced it), then `artifact_update("", ...)` with the freshly-rendered HTML — the SAME artifact updates in place (versioned), so the open view re-renders with live data rather than spawning a new artifact. This is how you build a "my open items" / "current status" dashboard that stays current. (If the widget isn't a saved artifact yet, save it first so it has a slug to refresh.) ## Tunable parameters (EDITMODE) — let the user restyle without asking you A widget can declare its own **tunables**. The dashboard turns them into typed controls beside the render, and dragging one restyles the widget instantly — no turn, no request, no cost to you. This is how a user gets "a bit more rounded, slightly warmer accent" without spending a message on it. Declare them ONCE, as a marker-fenced JSON object inside the widget's own script: ```html
…
``` - **Each key is a `:root` CSS custom property, minus the `--`.** `"radius"` drives `var(--radius)`. Reference every tunable through `var(--key)` in your CSS — a value the stylesheet doesn't read is a control that appears to do nothing. - **The markers are exact**: `/*EDITMODE-BEGIN*/` … `/*EDITMODE-END*/`, one block per widget, JSON object between them. Put it inside a `