| name | diagram-to-figma |
| description | Rebuild a Tidal Diagram as editable layers in a Figma design file. Use when the user runs /diagram-to-figma with a figma.com/design/... page URL and a diagram spec (pasted JSON from the app's "Export to Figma" button, or read live from the running app). |
/diagram-to-figma — Tidal diagram → editable Figma layers
Rebuild a diagram as native, editable Figma layers (frames, text, arrowed vectors) in a
target design file, using the Figma Plugin API via use_figma.
Inputs
- A Figma page URL — must be a design file:
figma.com/design/<key>/....
Reject figma.com/board/... (FigJam) and /slides/... — connectors and several node
types differ there. If given a non-design URL, stop and tell the user.
- A diagram spec (
FigmaSpec JSON) — one of:
- Pasted by the user (the in-app Export → Export to Figma button copies exactly this).
- Read live from the running app: open the app tab and read
localStorage["tidal-diagrams-doc"], then build the same spec shape from the doc
(kinds card/pill/cylinder/group, absolute coords, resolved hex). Prefer the pasted
spec when present — it already has resolved geometry and colors.
FigmaSpec shape (see src/diagram/figmaExport.ts):
{ title, direction,
nodes: [{ id, kind: "card|pill|cylinder|group", x, y, w, h, label,
header?, subtitle?, rows?, parent?, fillHex?, strokeHex?, textHex }],
edges: [{ source, target, label?, dotted, arrow, arrowStart?, strokeHex }] }
Coordinates are absolute canvas px (y-down). Colors are light-theme hex.
Preflight
- Load the API skill and tools. This skill pairs with
figma-use — follow its rules
(return IDs, colors 0–1, load fonts before text, atomic scripts, ≤10 ops/call). Batch-load
the Figma tools in one ToolSearch: select:use_figma,get_metadata,get_screenshot.
- Resolve the target page. From the URL, switch to the page node
(
await figma.setCurrentPageAsync(page)), or the page in the URL's node-id. Confirm the
file is design mode.
- Pick a clear origin. Scan
figma.currentPage.children for the rightmost edge and place
the diagram's wrapper there (e.g. originX = maxRight + 120, originY = 80) so it never
lands on existing work (figma-use Rule 13).
Style tokens (match the app — light theme)
These are the resolved Liquid values the app renders with. The spec bakes most into
fillHex/strokeHex/textHex; the radii, fonts, shadow, and edge shapes below are the rest.
- Fonts: card body labels + row values → Geist Mono (check
listAvailableFontsAsync; fall
back to Roboto Mono → JetBrains Mono → Inter). Headers, pills, cylinder labels, group headers →
Inter (Semi Bold for card headers, Medium for cylinder labels, Regular elsewhere). The spec's
mono:true flag marks text that should be Geist Mono.
- Card:
cornerRadius 10, fill #fafafa, 1px #e8e8e8 stroke, drop shadow
{type:'DROP_SHADOW', color:{r:0,g:0,b:0,a:0.05}, offset:{x:0,y:2}, radius:10}.
- Pill:
cornerRadius 8, fill #ffffff, 1px #b3b3b3 stroke, text #737373 Inter 13.
- Group:
cornerRadius 12, fill #ffffff, 1px #e8e8e8 stroke, header Inter 13 at top-left.
- Connector:
#c7c7c7, 1px, dotted → dashPattern [2.5,4]. Arrowheads via ARROW_LINES
stroke caps (open V — matches the app's marker).
- Edge label: a glass-pill chip —
cornerRadius 8, opaque #ffffff fill (masks the line),
1px #b3b3b3 stroke, text #737373 Inter 13, padding ~10×6, centered on the curve midpoint.
Build (incremental — validate with a screenshot between milestones)
Convert hex → {r,g,b} 0–1. Offset every node by the spec's min x/y so the diagram starts at
pad inside a wrapper, then place the wrapper at the clear origin.
- Wrapper. One
figma.createFrame() named after title, sized to the content bbox + pad,
clipsContent = false. Everything else is appended with relative coords (x - minX + pad).
- Groups first, sent to back (insert at index 0): a frame with the group tokens + header TEXT.
- Nodes — one
figma.createAutoLayout() per node, centered (primaryAxisAlignItems +
counterAxisAlignItems = 'CENTER'), append, then layoutSizingHorizontal/Vertical='FIXED',
resize(w,h), set position, radius/fill/stroke/effects per the tokens above.
- Edges — bezier, side-center attach (matches the app's
getBezierPath, curvature 0.35).
Pick the attachment side by dominant axis between centers (right/left when |dx|≥|dy|, else
top/bottom); the endpoint sits at that side's midpoint. Build a cubic via segment tangents:
const v = figma.createVector();
await v.setVectorNetworkAsync({
vertices: [
{ x: sx, y: sy, strokeCap: edge.arrowStart ? 'ARROW_LINES' : 'NONE' },
{ x: tx, y: ty, strokeCap: edge.arrow ? 'ARROW_LINES' : 'NONE' },
],
segments: [{ start: 0, end: 1, tangentStart: ts, tangentEnd: te }],
});
v.strokes = [{type:'SOLID', color: rgb(edge.strokeHex)}]; v.strokeWeight = 1;
if (edge.dotted) v.dashPattern = [2.5, 4];
wrapper.insertChild(0, v);
(Design mode has no Connector node — bezier vectors with ARROW_LINES caps are the editable
equivalent.) Add a glass-pill label chip (above the nodes) at the curve midpoint ((sx+tx)/2,(sy+ty)/2).
- Validate.
get_screenshot of the wrapper; check for overlaps, clipped text, missing arrows.
Fix targeted issues; don't rebuild wholesale. Return all created node IDs.
Notes
- Keep it to ≤10 logical ops per
use_figma call — wrapper+groups, nodes in batches, the cylinder,
then edges+labels, validating as you go.
- Every box, label, curve, and the cylinder silhouette is a real, editable layer — not a flat
image. Colors come baked into the spec; fonts/radii/shadow/curves come from the tokens above.
- The diagram direction is informational; geometry is already baked into coordinates.