Skip to main content

html-visuals

How to build a self-contained HTML visual for the session canvas or the portal viewer — the sandbox contract, responsive rules, and the chart/dashboard idioms that survive it.

معلومات المصدر

المستودع
microsoft/PilotSwarm
آخر نشاط في المصدر
٣٠ سبتمبر ٢٠٢٦ في ٠٢:٣٣
لغة SKILL.md المكتشفة
الإنجليزية
النجوم
٦
التفرعات
٣

خيارات التثبيت

يُحدَّد Prompt الذي يراجع المصدر أولًا بشكل افتراضي. يمكنك التبديل إلى أمر مباشر أو تنزيل نسخة محلية.

مراجعة ملفات المصدر

اقرأ SKILL.md وأي ملفات مرافقة يعرضها SkillsMP قبل أن تقرر التثبيت.

عرض SKILL.md

SKILL.md
تعليمات المصدر · معاينة للقراءة فقط
name
html-visuals
description
How to build a self-contained HTML visual for the session canvas or the portal viewer — the sandbox contract, responsive rules, and the chart/dashboard idioms that survive it.
# HTML Visuals When the user wants to *see* something — a chart, a dashboard, a graph, a comparison, a timeline, "show me", "visualize this" — build a single self-contained HTML page. This skill is about the PAGE; where it goes is decided once, per visual: - **Root session → the canvas.** `draw_canvas(html, note)` puts it on the user's screen live. No artifact, no `show_artifact`, no links — the canvas updating IS the delivery, and iterating means redrawing it. - **A keepable file, or no canvas (sub-agents)** → write it as an artifact and open the reader: write_artifact(filename="outage-graph.html", content="<!doctype html>…") show_artifact(filename="outage-graph.html") Include the returned `artifact://` link in your reply in this case only — the live push lands if someone is watching; the link works forever. Never send the same visual to both surfaces; one request, one destination. ## Forms: batch the input Use this whenever you draw a canvas whose job is to COLLECT input: a release sign-off, a settings sheet, a triage checklist, a survey. The cardinal rule: **The page is the scratchpad; you are the database. One user intent = one action.** A 23-item checklist is NOT 23 canvas actions — the user checks, unchecks, writes comments, changes their mind, and everything stays in page state. You hear about it exactly once, when they hit Submit. ## The contract shape Declare one submit action carrying a single `json` field (the whole form state), plus at most a couple of coarse verbs: draw_canvas(html=..., note="release sign-off", responseContract={"actions": { "submit": {"decision": "string", "form": "json"}, }}) `json` fields carry one structured object (bounded at 8 KB serialized — a checklist of dozens of items with comments fits comfortably; do not ship essays through it). ## The page pattern - Keep ALL form state in a plain JS object; every checkbox/input writes to it locally. Nothing posts until submit. - Submit gathers and posts once: function submit(decision) { parent.postMessage({ type: "canvas-action", action: "submit", data: { decision, form: { rev: FORM_REV, items: state.items } } }, "*"); lockForm("Submitted — waiting for confirmation…"); } - **Bake a form revision in.** Set `FORM_REV` to the canvas rev you drew (you know it from the draw result) and include it in the payload — if you have redrawn since, a submit carrying an older rev tells you the user answered a stale form; say so and redraw the current one. - **Lock on submit** (disable inputs, show a submitted banner). Your confirmation is the next redraw; the lock is what stops double-submits and makes the seconds of latency legible. - GO/NO-GO style decisions are two buttons calling the same submit with a different `decision` — not two actions. - On receipt, REDRAW showing what you recorded (received N items, decision, the follow-ups you are taking). The redraw is the acknowledgement. ## What NOT to do - No per-keystroke, per-checkbox, or per-row actions. If you feel the urge to know intermediate state, you have designed the form wrong. - No timers or auto-submit — a form posts on an explicit user gesture only. - Do not redraw while the form is live except in response to a submit; a redraw replaces the document and wipes everything typed so far. - Do not block the session on the form: keep working where possible, and note in chat that the canvas is waiting whenever the user may not be looking. Hard approval gates stay on `ask_user`. ## Sharing caveat Only a viewer with session WRITE access (the creator, and anyone the session is write-shared with) can post canvas actions — read-only viewers see the form, but their clicks are refused by the platform (their view of a mutating canvas may be stale). If the session is shared, say in the page footer who the form is addressed to, and let others respond in chat. ## Live data updates (update_canvas) When the canvas's CONTENT changes but its layout does not — dashboards, tickers, watchers — draw the shell ONCE, then tick with `update_canvas(data={...})`. The tick reaches the page as a message; the platform replays the latest tick into any freshly loaded page, so one idempotent renderer covers live updates and cold loads alike: addEventListener("message", (e) => { if (e.data?.type === "canvas-data") applyData(e.data.data); }); function applyData(d) { /* render the WHOLE state from d */ } - Declare `data: { example: {...} }` in the draw contract; after a memory gap, re-read it before ticking so your payload shape never drifts. - Ticks never flip the view and write no chat line; they light the unseen badge until the user looks. Cap: 32 KB for the MERGED state. - Layout change → `draw_canvas`. Content change → `update_canvas`. Never redraw to refresh numbers. ### Patch, don't re-send (update_canvas patch) `data` REPLACES the whole state — use it for the first tick after a draw and for wholesale refreshes. For every incremental change, send a PATCH: update_canvas(patch={ gauges: { cpu: 91 } }) // a few tokens update_canvas(data={ ...entire dashboard... }) // thousands — avoid Patch rules (RFC 7386 merge patch, applied server-side): - Send ONLY the subtree you are changing. Objects deep-merge into the current state; arrays and scalars replace wholesale. - `null` DELETES a key. Omit keys you are not touching — null is deletion, not "no change". - Your page keeps working unchanged: `canvas-data` messages always carry the complete merged state. A page that wants the delta too (targeted DOM updates, transitions) may ALSO listen for `canvas-patch` messages (`{type:'canvas-patch', patch, dataRev}`) — the whole-state message still arrives alongside, so the fancy path can never strand the page. - Lost track of the current state (restart, another writer)? Resync with `read_canvas(include_data=true)` and go back to patching. ### Shared dashboards (sub-agents writing the parent's canvas) Every canvas tool takes `session_id` targeting an ANCESTOR session — parent, grandparent, root; never siblings or strangers. The pattern for a parent with N children keeping one dashboard live: 1. Parent draws the shell once with a `tiles` region per child. 2. Each child patches ITS OWN subtree of the parent's canvas: `update_canvas(session_id=<parent>, patch={ tiles: { worker3: {...} } })`. Disjoint subtrees compose automatically — children cannot clobber each other. Same-path writes are last-writer-wins, attributed. 3. A child's write never flips the parent's view; the unseen badge and the activity feed carry who wrote what. ## Interactive canvases A canvas can accept structured responses while a user views it live. Declare the contract on the draw, then wire controls to `parent.postMessage`: draw_canvas(html=..., note="order form", responseContract={"actions": {"submit": {"qty": "number", "note": "string?"}}}) <button onclick="parent.postMessage({ type: 'canvas-action', action: 'submit', data: { qty: Number(qty.value) } }, '*')">Submit</button> Rules that make this work: - The browser enforces YOUR contract: undeclared actions, wrong types, and extra fields are dropped before they reach you. No contract on the current revision means nothing is accepted — a display-only canvas stays inert. - Responses arrive as `[canvas-action] {"action":...,"data":...}` user messages, hidden from the portal chat. Answer by REDRAWING with the result baked in — the canvas is both the form and the reply. - Echo input optimistically in the page (append the row, disable the button); your redraw arrives seconds later and replaces the document. - The sandbox has no `allow-forms`: wire click/keydown handlers, never rely on real form submission. Enter-to-submit = a keydown listener on the input. - **Text inputs: `font-size: 16px` minimum.** iOS Safari zooms the whole viewport when a smaller input is focused and never zooms back — the page comes out cropped and panned after every comment box. - A page taller than the viewport scrolls in the browser's own scroller — set nothing that traps it (`overflow: hidden` on body kills mobile scroll; `touch-action` overrides eat pans). Let the default body scroll work. - Do not redraw a canvas that carries input fields except in response to an action — a spontaneous redraw wipes drafts mid-typing. - Controls only function while someone is watching the portal. Never block on a canvas click; chat is the fallback and approval gates stay on ask_user. Prefer this over pasting a Markdown table when the answer is *shape* — proportion, trend, flow, overlap, ranking across many rows. Prefer a Markdown table when the answer is a handful of exact values. A table of 8 numbers does not need a chart; 200 rows of "who is waiting on what" does. ## The sandbox contract The page runs in an iframe with `allow-scripts` and **no** `allow-same-origin`. Your script runs; it just has no world outside the document. Violating any of these produces a blank or broken page, not an error message: 1. **Self-contained. No network of any kind.** No CDN `<script>` or `<link>`, no web fonts, no remote images, no `fetch`. Inline every byte: CSS in `<style>`, JS in `<script>`, data in a JS literal, images as `data:` URIs. If you need a charting library you cannot inline, hand-write the SVG. 2. **No storage.** `localStorage`, `sessionStorage`, cookies and `parent` property reads all throw — `parent.postMessage` is the one allowed call. Keep state in memory. Shared state, the session's files, and commands such as `git log` go through that call: load `canvas-apps`. 2b. **Sound: unlock on `touchend`/`click` — never only `touchstart`.** Three silent failures, none of which raise an error: - `touchstart` does NOT grant user activation (spec: only `touchend`, `click`, `keydown`, `mousedown` do), so an AudioContext created or resumed there stays suspended on iOS forever — and calling `preventDefault()` in `touchstart` also suppresses the synthetic `click`, so a page that binds game controls with `touchstart`+`preventDefault` never runs a working unlock at all. This is why a game sounds fine on desktop and is mute on every phone. Bind the unlock to `touchend` (or an un-prevented `click`) as well: `el.addEventListener("touchend", unlock)` alongside the touchstart game-input handler. - Autoplay policy suspends a context created at load. In the unlock, do `ctx = ctx || new AudioContext(); ctx.resume()`, keep one context for the page's life, and synthesize with oscillators (no files). - **iOS mutes Web Audio when the ring/silent switch is on.** In the same unlock, run `if (navigator.audioSession) navigator.audioSession.type = "playback";` (Safari 16.4+; guard it — the API does not exist elsewhere). Say so in the page's own UI ("sound needs the ring switch on" beside the mute toggle); the user cannot debug what the page never mentions. 2c. **On the session canvas, resizes reflow — they never reload.** The canvas never reloads your page for a container resize, so in-memory state (game progress, form drafts) survives window drags and rotations there. The page's inner window gets real `resize` events; if you draw to a `<canvas>` at pixel sizes or measure the viewport once at boot, add a resize listener that recomputes — CSS layouts need nothing. The artifact READER pane (show_artifact) is different: it reloads when its width settles — stateful pages belong on the session canvas. 3. **Set your own colors — on `html`, not only `body`.** iOS extends the `html` background into rubber-band overscroll; a body-only background ends at the document edge and the bounce reveals whatever sits beneath. Set `background` and `color` once on `html, body`. Add `html { overscroll-behavior: none }` if the bounce itself is unwanted. Dark-on-dark is the usual failure; check both. 4. **Budget.** The canvas caps at 900 KB; text artifacts cap at 1 MiB. A few hundred KB of inlined data is fine and normal; a multi-megabyte dump is not — aggregate first. Links to the outside world are fine (`<a href="https://…" target="_blank">`) — popups are allowed. ## Responsive, and why it matters here The reader pane is resizable, and the user *will* resize it. The viewer reloads the frame once the width settles, so a document that computes its layout only at load does get corrected — but the reload costs the reader their scroll position. Layout that reflows on its own never pays that. - Lay out with CSS grid/flex and `minmax()`, not fixed pixel columns. - Give every `<svg>` a `viewBox` and `width:100%`, so it scales without script. - If you must compute geometry in JS (Sankey flows, force layouts, anything where positions depend on measured width), add a `resize` listener that re-runs the layout. Debounce it. - Never bake the viewport width into a constant at load time. ## Dashboard shape A dashboard that reads well is almost always the same three bands: 1. **Header** — what this is, what window it covers, where the data came from. Someone opening it a week later must not have to ask. 2. **KPI row** — 4–9 tiles, one number each, in `grid-template-columns: repeat(auto-fit, minmax(150px, 1fr))` so it reflows. Big value, small uppercase label under it. Color only where it *means* something (bad = red, good = green); a rainbow of tiles means nothing. 3. **Sections** — one question per card, with a one-line note under the heading saying how to read the picture. Charts do not explain themselves. ## Picking the form | The question | The form | |---|---| | How much, across categories | Horizontal bars, sorted by value | | How it changed over time | Line; area only if the total matters | | What flows to what | Sankey / chord, nodes in fixed columns | | How two measures relate | Scatter, with the outliers labelled | | Where the mass sits | Histogram or box plot, not an average | | Exact values, few rows | A table. Not a chart. | Avoid pie charts beyond three slices, dual y-axes, and 3-D anything. ## Color and legibility - Encode with position and length first; color is for *categories* and *status*, not decoration. - Keep a small named palette in `:root` and reuse it. Semantic colors (`--bad`, `--ok`, `--warn`) must mean the same thing in every section. - Body text at 13–14px minimum, axis labels no smaller than 11px. - Never rely on color alone: pair it with a label, shape, or ordering, or a red/green reader learns nothing. - Long category names: truncate with `text-overflow: ellipsis` and put the full string in a `<title>` so hover reveals it. ## Interaction Hover tooltips are worth the code — they are how a dense chart stays readable without labelling every mark. Keep them cheap: one absolutely-positioned div, moved on `mousemove`, hidden on `mouseleave`. Skip zoom, pan, and filtering unless asked; they add a lot of script for something the user did not request. ## Self-check before you show it - Opens with no console errors and no blank regions. - **If the page has a start/pause state machine, its initial state must be VISIBLE and reachable.** A Start overlay that is `display:none` with nothing adding its show-class renders as a frozen page: the board paints once, `running` stays false, and every input handler that guards on it silently ignores the user. Trace the path from first paint to running with no keyboard — touch only. - Readable at both a narrow reader pane (~500px) and full width. - Every number on screen is traceable to the data you were given. - The header says what it is and what window it covers. - Nothing loads from the network. ## Reusable canvas apps A canvas worth drawing twice is an app: save it once, redraw it from the store forever, and never courier the bytes through your own context again. 1. **Self-describe from the first draw.** Put one comment immediately after `<!doctype html>`: ```html <!-- CANVAS-APP-MANIFEST { "manifestVersion": 1, "name": "release-signoff", "version": "1.0.0", "description": "23-item release checklist; batches checks into one submit.", "responseContract": { "actions": { "submit_signoff": { "items": "json", "note": "string?" } } }, "data": "{items:[{id,state}]} — full replacement, <=32KB", "notes": "Send update_canvas ticks to pre-check items; page posts one submit." } --> ``` The contract uses the exact draw_canvas grammar; a broken embedded contract makes later fromArtifact draws FAIL CLOSED, so keep it valid. 2. **Save = server-side copy, never a re-emit:** `write_artifact({fromArtifact: {filename: "canvas.html"}, filename: "app-<name>.html"})` — the source defaults to this session. 3. **Load = draw_canvas({fromArtifact: {sessionId?, filename}}).** The bytes go store→canvas; your tool result is the interface card — manifest summary plus the EFFECTIVE responseContract — which is everything needed to interpret `[canvas-action]` messages and author update_canvas ticks. NEVER read_artifact + draw_canvas(html: <same text>): that is the token-courier anti-pattern this mechanism exists to kill. 4. **Re-learn cheaply.** Browsing candidates: `read_artifact({sessionId, filename, manifestOnly: true})`. After context regeneration or on an inherited canvas: `read_canvas({manifestOnly: true})` returns the card plus the armed contract.
عرض على GitHub