Use when creating or refining a 2D DESIGN for an Otto Product story from an agent session — the Design tab's Figma/Canva-style side — an Excalidraw BOARD (`design.excalidraw`: frames as artboards, wireframes, user flows, component sheets, moodboards) or an HTML SCREEN (`design.html`: a self-contained, script-free, high-fidelity UI screen shown in a device frame). Covers Excalidraw JSON authoring rules (document shape, element fields, frames, bound text, arrows with bindings, 8-pt grid, palette, reusable components), HTML screen rules (no network, no JS, realistic content, light/dark), and the quality bar for each.
Instal·la
npx skillscat add itzikiusa/otto-os/crates-otto-skills-assets-skills-development-otto-design-2d Instal·la des del registre de SkillsCat.
Otto Design — 2D screens & boards — in-place agent
Otto's Product → Design tab hosts design artifacts on a story. For 2D work you
edit ONE file in place (the assist prompt names it and includes its current
contents); Otto previews every save live (mockup_updated) and commits the file
as a kind:"design" attachment when the turn ends:
| you're asked for | format | file you edit | renders as |
|---|---|---|---|
| a UI screen, dashboard, settings page, landing page, email | html |
design.html |
sandboxed iframe inside a device frame (iPhone / iPad / desktop), light or dark |
| a wireframe set, user flow, artboards, moodboard, component sheet, anything freeform | excalidraw |
design.excalidraw |
the Excalidraw board — the user keeps editing it by hand |
| a flow/sequence/ER/state diagram (strict, auto-laid-out) | mermaid |
design.mmd |
Mermaid (see otto-mockup for the rules) |
Write the WHOLE file each time. Keep it always-valid so every intermediate save
renders. Reply with ONE short sentence describing what you changed.
A. Excalidraw boards — design.excalidraw
Document shape
{
"type": "excalidraw",
"version": 2,
"source": "otto",
"elements": [ /* … */ ],
"appState": { "viewBackgroundColor": "#ffffff", "gridSize": 8 },
"files": {}
}type/version/elements are required. files stays {} — never embed
base64 images (the file has a size cap and the board would bloat); draw
placeholders instead. Do not put comments in the JSON.
Element fields (write these; Excalidraw defaults the rest on load)
Every element:
{ "id": "home-frame", "type": "rectangle", "x": 0, "y": 0, "width": 390, "height": 844,
"angle": 0, "strokeColor": "#1e1e1e", "backgroundColor": "transparent",
"fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0,
"opacity": 100, "groupIds": [], "frameId": null, "roundness": { "type": 3 },
"seed": 1, "version": 1, "versionNonce": 1, "isDeleted": false,
"boundElements": null, "updated": 1, "link": null, "locked": false }type∈rectangle|ellipse|diamond|text|arrow|line|frame|freedraw.
Userectangle/ellipse/text/arrow/frame— skipfreedraw.id: unique, readable slug (home-frame,home-cta,arrow-home-checkout). Stable across edits.roughness:0(architect) for product wireframes/UI;1only for
deliberately sketchy moodboards.roundness: {"type": 3}= rounded corners
(cards, buttons);null= sharp (frames, tables, dividers).fillStyle:"solid";hachure/cross-hatchread as sketch — avoid in UI wireframes.strokeWidth1 for UI, 2 for emphasis/arrows;strokeStyle"dashed"for optional/future states.
Text adds: "text", "originalText" (same string), "fontSize" (12/14/16/20/24/32/40),"fontFamily" (2 = Helvetica-like sans for UI; 3 = monospace for code; 1 = hand-drawn),"textAlign" (left|center|right), "verticalAlign" (top|middle),"lineHeight": 1.25, "containerId": null, and width/height roughly0.55 × fontSize × chars / 1.25 × fontSize × lines (Excalidraw re-measures on load).
Bound label (text centred inside a shape): give the shape"boundElements": [{ "id": "<textId>", "type": "text" }] and the text"containerId": "<shapeId>", textAlign: "center", verticalAlign: "middle".
Excalidraw positions it on load — this is how buttons, cards with titles, and
flow nodes get labels.
Arrow adds: "points": [[0, 0], [dx, dy]] (relative to x,y; add mid-points
for elbows: [[0,0],[120,0],[120,80]]), "startBinding"/"endBinding":{ "elementId": "<shapeId>", "focus": 0, "gap": 8 } or null,"startArrowhead": null, "endArrowhead": "arrow" ("triangle", "bar", "dot" also exist),"elbowed": false. Place x,y on the source shape's edge and end the last point on
the target's edge — bindings keep it attached when the user drags things. Also list the
arrow in both shapes' boundElements as { "id": "<arrowId>", "type": "arrow" }.
Frame (type: "frame") adds "name": "Home — iPhone 15". Children carry"frameId": "<frameId>". Frames clip and export as units — one frame per
artboard/screen/state. Nothing bound to a frame should sit outside its box.
Layout rules (this is what makes a board look designed)
- 8-pt grid — every
x,y,width,heighta multiple of 8 (4 for icon
nudges).appState.gridSize: 8. - Artboards as frames, laid out in a row per flow, column per step, with
80 px gutters between frames and atexttitle (fontSize 20) 40 px above
each row. Device sizes: iPhone 390×844, Android 412×915, iPad 820×1180,
desktop 1440×900, email 600×(n). Add a 44-pt status bar strip and a 34-pt
home-indicator area on phones. - Type scale: 32/24 titles, 20 section, 16 body, 14 secondary, 12 caption.
Left-align body text; centre only labels inside shapes. - Spacing: 16 outer padding, 12 between cards, 8 inside components,
48-pt touch targets (buttonsheight: 48), 56-pt list rows, 64-pt nav bars. - Palette (default; obey the story's brand if given) — ink
#1e1e1e,
secondary text#6b7280, hairline#e5e7eb, surface#ffffff, canvas#f8fafc, accent#4f46e5(white text on it), success#16a34a, warning#f59e0b, danger#dc2626, and flow-chart node fills#e0e7ff/#dcfce7/#fef3c7/#fee2e2. Max 1 accent + 1 neutral family per board. - Components, not doodles: build a button/input/card/nav/list-row/chip once
as agroupIds: ["cmp-button-primary"]cluster and repeat it with the same
sizes. Consistency beats variety. Icons: draw 24×24 ellipses/rectangles as
placeholders or use a single glyph character (☰ ⌕ ← → ✓ ✕ ★ ♥ ⚙ ⋯) in a
text element — never fake icons with many strokes. - Realistic content: real-looking names, prices, dates, counts, states
("3 of 12 shipped", "Visa •••• 4242"). No lorem ipsum, no "Label". - Flows: nodes 200×72 rounded rectangles with bound labels; decisions as
diamond200×120; arrows with bindings and a short bound label ("yes" /
"no" / "timeout"); swimlanes as tall dashed rectangles behind them
(strokeStyle: "dashed",opacity: 60). - Annotations: red-ish (
#dc2626) callout numbers in 24×24 ellipses with a
matching numbered list in atextelement beside the frame — reviewers read
those. - Keep the board small: ≤ 400 elements. One artboard row per flow; open a
second board (another design artifact) for another feature area.
Editing in place
Preserve the user's hand edits: keep element ids, keep elements you weren't asked
to change, only append/modify what the request covers. Deleting is isDeleted: true (or simply omit the element). If the file is already a full Excalidraw
document with fields you don't know, copy them through unchanged.
Minimal worked example (two-screen flow)
{ "type": "excalidraw", "version": 2, "source": "otto",
"appState": { "viewBackgroundColor": "#f8fafc", "gridSize": 8 }, "files": {},
"elements": [
{ "id": "row1-title", "type": "text", "x": 0, "y": -56, "width": 320, "height": 24, "text": "Checkout — happy path", "originalText": "Checkout — happy path", "fontSize": 20, "fontFamily": 2, "textAlign": "left", "verticalAlign": "top", "lineHeight": 1.25, "containerId": null, "angle": 0, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "groupIds": [], "frameId": null, "roundness": null, "seed": 1, "version": 1, "versionNonce": 1, "isDeleted": false, "boundElements": null, "updated": 1, "link": null, "locked": false },
{ "id": "f-cart", "type": "frame", "name": "1 · Cart", "x": 0, "y": 0, "width": 390, "height": 844, "angle": 0, "strokeColor": "#1e1e1e", "backgroundColor": "#ffffff", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "groupIds": [], "frameId": null, "roundness": null, "seed": 2, "version": 1, "versionNonce": 1, "isDeleted": false, "boundElements": null, "updated": 1, "link": null, "locked": false },
{ "id": "cart-cta", "type": "rectangle", "x": 16, "y": 764, "width": 358, "height": 48, "angle": 0, "strokeColor": "#4f46e5", "backgroundColor": "#4f46e5", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "groupIds": ["cmp-button-primary"], "frameId": "f-cart", "roundness": { "type": 3 }, "seed": 3, "version": 1, "versionNonce": 1, "isDeleted": false, "boundElements": [{ "id": "cart-cta-label", "type": "text" }, { "id": "a-cart-pay", "type": "arrow" }], "updated": 1, "link": null, "locked": false },
{ "id": "cart-cta-label", "type": "text", "x": 120, "y": 778, "width": 150, "height": 20, "text": "Pay $148.00", "originalText": "Pay $148.00", "fontSize": 16, "fontFamily": 2, "textAlign": "center", "verticalAlign": "middle", "lineHeight": 1.25, "containerId": "cart-cta", "angle": 0, "strokeColor": "#ffffff", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "groupIds": ["cmp-button-primary"], "frameId": "f-cart", "roundness": null, "seed": 4, "version": 1, "versionNonce": 1, "isDeleted": false, "boundElements": null, "updated": 1, "link": null, "locked": false },
{ "id": "f-pay", "type": "frame", "name": "2 · Payment", "x": 470, "y": 0, "width": 390, "height": 844, "angle": 0, "strokeColor": "#1e1e1e", "backgroundColor": "#ffffff", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "groupIds": [], "frameId": null, "roundness": null, "seed": 5, "version": 1, "versionNonce": 1, "isDeleted": false, "boundElements": [{ "id": "a-cart-pay", "type": "arrow" }], "updated": 1, "link": null, "locked": false },
{ "id": "a-cart-pay", "type": "arrow", "x": 374, "y": 788, "width": 96, "height": 0, "points": [[0, 0], [96, 0]], "startBinding": { "elementId": "cart-cta", "focus": 0, "gap": 8 }, "endBinding": { "elementId": "f-pay", "focus": 0, "gap": 8 }, "startArrowhead": null, "endArrowhead": "arrow", "elbowed": false, "angle": 0, "strokeColor": "#6b7280", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "groupIds": [], "frameId": null, "roundness": { "type": 2 }, "seed": 6, "version": 1, "versionNonce": 1, "isDeleted": false, "boundElements": null, "updated": 1, "link": null, "locked": false }
] }B. HTML screens — design.html
One complete, self-contained document rendered in a sandboxed iframe with
scripts disabled, inside the arena's device frame with a light/dark toggle.
Rules:
<!doctype html>,<meta charset="utf-8">,<meta name="viewport" content="width=device-width, initial-scale=1">, a<title>.- All CSS inline in one
<style>. Zero network: no<link>, no web fonts,
no remote images/scripts/iframes.system-uifonts, CSS gradients/shapes,
emoji, and inline<svg>for icons and illustrations. - No JavaScript runs. Convey state visually: the active tab, a filled form,
a selected row, a hover-looking primary button, an open dropdown drawn as a
static list, a toast already on screen. - Fit the frame: design mobile-first at 390 px wide and let it stretch to
desktop with flex/grid; never rely on a fixed body width.body { margin: 0 }.
Avoid horizontal scroll. - Light + dark: define tokens on
:rootand override them under@media (prefers-color-scheme: dark)(the device frame flips the scheme).color-scheme: light darkon:root. - Realistic content and a small cohesive palette (see the Excalidraw
palette above), a clear type scale (28/20/16/14/12), 8-pt spacing, rounded
cards with hairline borders, one accent. It should look like a screenshot of a
shipped product, not a wireframe. - Keep it under ~60 KB. One screen per file; another screen = another artifact
(the user can ask for "Checkout — step 2" as a new one).
Skeleton:
<!doctype html>
<html lang="en"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Orders — Acme Admin</title>
<style>
:root { color-scheme: light dark;
--bg:#f8fafc; --card:#fff; --line:#e5e7eb; --ink:#1e1e1e; --dim:#6b7280; --accent:#4f46e5; }
@media (prefers-color-scheme: dark) {
:root { --bg:#0f172a; --card:#1e293b; --line:#334155; --ink:#f1f5f9; --dim:#94a3b8; } }
body { margin:0; font:15px/1.5 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif; background:var(--bg); color:var(--ink); }
/* header, nav, cards, table, buttons … */
</style></head>
<body><!-- realistic UI here --></body></html>Related
- 3D scenes (
scene.json) → skillotto-design-3d. - The Design-tab mechanics (formats, files, commit, live preview) → skill
otto-mockup.