Use when creating or modifying Excalidraw whiteboard files (.excalidraw) — drawing diagrams, or adding, moving, restyling, or removing shapes, arrows, and text by editing the scene JSON directly.
Install
npx skillscat add yishan-io/yishan-mono/editing-whiteboards Install via the SkillsCat registry.
Editing Whiteboards
Use this skill when a user asks you to draw on, add to, or change a Yishan whiteboard file (.excalidraw). The whiteboard tab renders an Excalidraw canvas from the file's JSON. You edit the JSON directly with your normal file tools; the canvas picks the change up from disk.
The File Format
A .excalidraw file is one JSON object:
{
"type": "excalidraw",
"version": 2,
"source": "yishan",
"elements": [],
"appState": {},
"files": {}
}elements— the drawing objects (shapes, text, arrows). Array order is z-order: later elements render on top of earlier ones.appState— scene settings. The app persists onlyviewBackgroundColor,gridSize,gridStep, andgridModeEnabled. Transient fields (scrollX,scrollY,zoom,selectedElementIds,selectionElement,editingGroupId,editingTextElement,editingLinearElement) are stripped on save — never set them, never rely on them.files— embedded images, keyed by the image element'sid(see Images).source— always keep"yishan".
Element Invariants (follow these, always)
- Ids are unique and stable. Never reuse an id and never change an existing element's id — other elements reference ids (
boundElements,containerId, imagefileskeys). - Edit surgically. Read the file, change only the fields you intend, and preserve everything else. Never regenerate the whole file from a guess — unknown fields you drop can break references.
- Bump
versionNonceandupdatedon every element you change:versionNonce+1,updated= current epoch milliseconds. Leaveversionandseedas they are. isDeleted: falsefor live elements. To remove an element, either delete it from the array (safe when nothing references it) or setisDeleted: true.- Keep bindings consistent.
- An arrow with a
startBinding/endBindingmust have its bound shape'sboundElementsinclude the arrow id (and vice versa). - Text inside a container needs
containerIdset to the container id, and the container'sboundElementsmust list the text id. - When a binding is not needed, use
null(arrow) /[](shape) /null(textcontainerId).
- An arrow with a
frameId: nullunless the element is inside an existing frame.
Element Templates
All coordinates are relative to the canvas: x/y = top-left corner, width/height ≥ 0, angle in radians. These templates are valid as-is — change only what you need.
Rectangle
{
"id": "rect-001",
"type": "rectangle",
"x": 100,
"y": 100,
"width": 200,
"height": 120,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"roundness": { "type": 3 },
"boundElements": [],
"updated": 1754500000000,
"link": null,
"locked": false,
"seed": 42,
"version": 1,
"versionNonce": 1,
"index": null,
"isDeleted": false
}Text
{
"id": "text-001",
"type": "text",
"x": 120,
"y": 120,
"width": 160,
"height": 25,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"roundness": null,
"boundElements": [],
"updated": 1754500000000,
"link": null,
"locked": false,
"seed": 7,
"version": 1,
"versionNonce": 1,
"index": null,
"isDeleted": false,
"fontSize": 20,
"fontFamily": 1,
"text": "Hello",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "Hello",
"autoResize": true,
"lineHeight": 1.25
}fontFamily: 1 = Virgil (default handwriting), 2 = Helvetica, 3 = Cascadia.- Keep
originalTextidentical totext(it stores the un-transformed text). width/heightshould roughly fit the rendered text; the canvas re-measures on load.
Arrow
{
"id": "arrow-001",
"type": "arrow",
"x": 300,
"y": 160,
"width": 200,
"height": 0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"roundness": { "type": 2 },
"boundElements": [],
"updated": 1754500000000,
"link": null,
"locked": false,
"seed": 99,
"version": 1,
"versionNonce": 1,
"index": null,
"isDeleted": false,
"points": [
[0, 0],
[200, 0]
],
"lastCommittedPoint": null,
"startBinding": null,
"endBinding": null
}pointsare relative to the element'sx/y: an arrow from(x, y)to(x + dx, y + dy)has points[[0, 0], [dx, dy]].- For a vertical arrow, set
pointsto[[0, 0], [0, height]]and keepx/y/width/heightconsistent with the first/last point.
Other shapes
ellipse, diamond, triangle, line, freedraw use the same base fields as the rectangle; line is a linear element like the arrow (with startBinding/endBinding/lastCommittedPoint and points). When unsure of a shape's extra fields, copy an existing element of that type from the file you are editing.
Images
An embedded image is an element with type: "image" plus an entry in files. The image element's fileId, the files key, and the file entry's id must all be equal:
{
"id": "img-001",
"type": "image",
"x": 100,
"y": 100,
"width": 320,
"height": 200,
"angle": 0,
"strokeColor": "transparent",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"roundness": null,
"boundElements": [],
"updated": 1754500000000,
"link": null,
"locked": false,
"seed": 11,
"version": 1,
"versionNonce": 1,
"index": null,
"isDeleted": false,
"fileId": "img-001",
"status": "saved",
"scale": [1, 1],
"crop": null
}{
"files": {
"img-001": {
"id": "img-001",
"mimeType": "image/png",
"dataURL": "data:image/png;base64,....",
"created": 1754500000000
}
}
}Set the element's width/height to the intended display size. Never store images outside files; never attach the same file entry to two element ids.
Workflow
- Read the file first. Understand existing elements, ids, and bindings before changing anything.
- Plan the edit: which elements to add, which to modify, which to remove.
- Edit surgically (see Invariants). Add new elements with fresh unique ids; bump
versionNonce/updatedon changed elements. - Verify:
- The file is valid JSON.
elementsis an array; all element ids are unique.- No dangling references (
boundElements,containerId, imagefileskeys). sourceis"yishan";filesis present (even if empty).- Re-read the file and confirm your changes landed.
What Not To Do
- Do not rewrite the whole file when a surgical change works — you risk dropping fields other elements depend on.
- Do not set transient
appStatefields (scrollX,zoom, selections) — they are stripped on save and can confuse later diffs. - Do not leave two elements with the same id.
- Do not add elements with negative
width/height; useangle: Math.PIinstead of negative dimensions for flipped content. - Do not assume the canvas repairs invalid elements — it tolerates some damage, but a valid file is the goal.