@dannote/figma-use 0.2.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dannote/figma-use",
3
- "version": "0.2.1",
3
+ "version": "0.4.0",
4
4
  "description": "Control Figma from the command line. Full read/write access for AI agents.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -45,11 +45,13 @@
45
45
  "dependencies": {
46
46
  "citty": "^0.1.6",
47
47
  "consola": "^3.4.2",
48
- "elysia": "^1.2.25"
48
+ "elysia": "^1.2.25",
49
+ "kiwi-schema": "^0.5.0"
49
50
  },
50
51
  "devDependencies": {
51
52
  "@types/bun": "^1.3.6",
52
53
  "esbuild": "^0.25.4",
54
+ "react": "19",
53
55
  "typescript": "^5.8.3"
54
56
  }
55
57
  }
@@ -43,10 +43,142 @@
43
43
  // src/main.ts
44
44
  console.log("[Figma Bridge] Plugin main loaded at", (/* @__PURE__ */ new Date()).toISOString());
45
45
  figma.showUI(__html__, { width: 300, height: 200 });
46
+ var loadedFonts = /* @__PURE__ */ new Set();
47
+ var fontLoadPromises = /* @__PURE__ */ new Map();
48
+ var interPromise = figma.loadFontAsync({ family: "Inter", style: "Regular" });
49
+ fontLoadPromises.set("Inter:Regular", interPromise);
50
+ interPromise.then(() => loadedFonts.add("Inter:Regular"));
51
+ function loadFont(family, style) {
52
+ const key = `${family}:${style}`;
53
+ if (loadedFonts.has(key)) return;
54
+ const pending = fontLoadPromises.get(key);
55
+ if (pending) return pending;
56
+ const promise = figma.loadFontAsync({ family, style });
57
+ fontLoadPromises.set(key, promise);
58
+ promise.then(() => {
59
+ loadedFonts.add(key);
60
+ fontLoadPromises.delete(key);
61
+ });
62
+ return promise;
63
+ }
64
+ function createNodeFast(command, args, nodeCache, deferredLayouts) {
65
+ return __async(this, null, function* () {
66
+ if (!args) return null;
67
+ const {
68
+ x = 0,
69
+ y = 0,
70
+ width,
71
+ height,
72
+ name,
73
+ parentId,
74
+ fill,
75
+ stroke,
76
+ strokeWeight,
77
+ radius,
78
+ opacity,
79
+ layoutMode,
80
+ itemSpacing,
81
+ padding,
82
+ text,
83
+ fontSize,
84
+ fontFamily,
85
+ fontStyle
86
+ } = args;
87
+ let node = null;
88
+ switch (command) {
89
+ case "create-frame": {
90
+ const frame = figma.createFrame();
91
+ frame.x = x;
92
+ frame.y = y;
93
+ frame.resize(width || 100, height || 100);
94
+ if (name) frame.name = name;
95
+ if (fill) frame.fills = [{ type: "SOLID", color: hexToRgb(fill) }];
96
+ if (stroke) frame.strokes = [{ type: "SOLID", color: hexToRgb(stroke) }];
97
+ if (strokeWeight) frame.strokeWeight = strokeWeight;
98
+ if (typeof radius === "number") frame.cornerRadius = radius;
99
+ if (typeof opacity === "number") frame.opacity = opacity;
100
+ if (layoutMode && layoutMode !== "NONE") {
101
+ deferredLayouts == null ? void 0 : deferredLayouts.push({
102
+ frame,
103
+ layoutMode,
104
+ itemSpacing,
105
+ padding
106
+ });
107
+ }
108
+ node = frame;
109
+ break;
110
+ }
111
+ case "create-rectangle": {
112
+ const rect = figma.createRectangle();
113
+ rect.x = x;
114
+ rect.y = y;
115
+ rect.resize(width || 100, height || 100);
116
+ if (name) rect.name = name;
117
+ if (fill) rect.fills = [{ type: "SOLID", color: hexToRgb(fill) }];
118
+ if (stroke) rect.strokes = [{ type: "SOLID", color: hexToRgb(stroke) }];
119
+ if (strokeWeight) rect.strokeWeight = strokeWeight;
120
+ if (typeof radius === "number") rect.cornerRadius = radius;
121
+ if (typeof opacity === "number") rect.opacity = opacity;
122
+ node = rect;
123
+ break;
124
+ }
125
+ case "create-ellipse": {
126
+ const ellipse = figma.createEllipse();
127
+ ellipse.x = x;
128
+ ellipse.y = y;
129
+ ellipse.resize(width || 100, height || 100);
130
+ if (name) ellipse.name = name;
131
+ if (fill) ellipse.fills = [{ type: "SOLID", color: hexToRgb(fill) }];
132
+ if (stroke) ellipse.strokes = [{ type: "SOLID", color: hexToRgb(stroke) }];
133
+ if (strokeWeight) ellipse.strokeWeight = strokeWeight;
134
+ if (typeof opacity === "number") ellipse.opacity = opacity;
135
+ node = ellipse;
136
+ break;
137
+ }
138
+ case "create-text": {
139
+ const textNode = figma.createText();
140
+ const family = fontFamily || "Inter";
141
+ const style = fontStyle || "Regular";
142
+ yield loadFont(family, style);
143
+ textNode.fontName = { family, style };
144
+ textNode.characters = text || "";
145
+ textNode.x = x;
146
+ textNode.y = y;
147
+ if (name) textNode.name = name;
148
+ if (fontSize) textNode.fontSize = fontSize;
149
+ if (fill) textNode.fills = [{ type: "SOLID", color: hexToRgb(fill) }];
150
+ if (typeof opacity === "number") textNode.opacity = opacity;
151
+ node = textNode;
152
+ break;
153
+ }
154
+ default:
155
+ return null;
156
+ }
157
+ return node;
158
+ });
159
+ }
160
+ var NEEDS_ALL_PAGES = /* @__PURE__ */ new Set([
161
+ "get-node-info",
162
+ "get-node-tree",
163
+ "get-node-children",
164
+ "set-parent",
165
+ "clone-node",
166
+ "delete-node",
167
+ "get-pages",
168
+ "set-current-page",
169
+ "get-components",
170
+ "get-styles",
171
+ "export-node",
172
+ "screenshot"
173
+ ]);
174
+ var allPagesLoaded = false;
46
175
  figma.ui.onmessage = (msg) => __async(null, null, function* () {
47
176
  if (msg.type !== "command") return;
48
177
  try {
49
- yield figma.loadAllPagesAsync();
178
+ if (!allPagesLoaded && NEEDS_ALL_PAGES.has(msg.command)) {
179
+ yield figma.loadAllPagesAsync();
180
+ allPagesLoaded = true;
181
+ }
50
182
  const result = yield handleCommand(msg.command, msg.args);
51
183
  figma.ui.postMessage({ type: "result", id: msg.id, result });
52
184
  } catch (error) {
@@ -55,7 +187,81 @@
55
187
  });
56
188
  function handleCommand(command, args) {
57
189
  return __async(this, null, function* () {
190
+ var _a, _b, _c, _d;
58
191
  switch (command) {
192
+ // ==================== BATCH ====================
193
+ case "batch": {
194
+ const { commands } = args;
195
+ const results = [];
196
+ const refMap = /* @__PURE__ */ new Map();
197
+ const nodeCache = /* @__PURE__ */ new Map();
198
+ const deferredLayouts = [];
199
+ const internalAttachments = [];
200
+ const externalAttachments = [];
201
+ const rootNodes = [];
202
+ for (const cmd of commands) {
203
+ if (((_a = cmd.args) == null ? void 0 : _a.parentRef) && refMap.has(cmd.args.parentRef)) {
204
+ cmd.args.parentId = refMap.get(cmd.args.parentRef);
205
+ delete cmd.args.parentRef;
206
+ }
207
+ const node = yield createNodeFast(cmd.command, cmd.args, nodeCache, deferredLayouts);
208
+ if (node) {
209
+ results.push({ id: node.id, name: node.name });
210
+ nodeCache.set(node.id, node);
211
+ if ((_b = cmd.args) == null ? void 0 : _b.ref) {
212
+ refMap.set(cmd.args.ref, node.id);
213
+ }
214
+ const parentId = (_c = cmd.args) == null ? void 0 : _c.parentId;
215
+ if (parentId) {
216
+ if (nodeCache.has(parentId)) {
217
+ internalAttachments.push({ node, parentId });
218
+ } else {
219
+ externalAttachments.push({ node, parentId });
220
+ }
221
+ } else {
222
+ rootNodes.push(node);
223
+ }
224
+ } else {
225
+ const result = yield handleCommand(cmd.command, cmd.args);
226
+ results.push(result);
227
+ if ((_d = cmd.args) == null ? void 0 : _d.ref) {
228
+ refMap.set(cmd.args.ref, result.id);
229
+ }
230
+ }
231
+ }
232
+ for (const attachment of internalAttachments) {
233
+ const parent = nodeCache.get(attachment.parentId);
234
+ if (parent && "appendChild" in parent) {
235
+ parent.appendChild(attachment.node);
236
+ }
237
+ }
238
+ for (const layout of deferredLayouts) {
239
+ layout.frame.layoutMode = layout.layoutMode;
240
+ layout.frame.primaryAxisSizingMode = "AUTO";
241
+ layout.frame.counterAxisSizingMode = "AUTO";
242
+ if (layout.itemSpacing) layout.frame.itemSpacing = layout.itemSpacing;
243
+ if (layout.padding) {
244
+ layout.frame.paddingTop = layout.padding.top;
245
+ layout.frame.paddingRight = layout.padding.right;
246
+ layout.frame.paddingBottom = layout.padding.bottom;
247
+ layout.frame.paddingLeft = layout.padding.left;
248
+ }
249
+ }
250
+ for (const node of rootNodes) {
251
+ figma.currentPage.appendChild(node);
252
+ }
253
+ for (const attachment of externalAttachments) {
254
+ let parent = nodeCache.get(attachment.parentId);
255
+ if (!parent) {
256
+ parent = yield figma.getNodeByIdAsync(attachment.parentId);
257
+ }
258
+ if (parent && "appendChild" in parent) {
259
+ parent.appendChild(attachment.node);
260
+ }
261
+ }
262
+ figma.commitUndo();
263
+ return results;
264
+ }
59
265
  // ==================== READ ====================
60
266
  case "get-selection":
61
267
  return figma.currentPage.selection.map(serializeNode);
@@ -70,14 +276,52 @@
70
276
  const { id } = args;
71
277
  const node = yield figma.getNodeByIdAsync(id);
72
278
  if (!node) throw new Error("Node not found");
73
- const serializeTree = (n) => {
74
- const base = serializeNode(n);
279
+ const serializeTreeNode = (n) => {
280
+ const base = {
281
+ id: n.id,
282
+ name: n.name,
283
+ type: n.type
284
+ };
285
+ if ("x" in n) base.x = Math.round(n.x);
286
+ if ("y" in n) base.y = Math.round(n.y);
287
+ if ("width" in n) base.width = Math.round(n.width);
288
+ if ("height" in n) base.height = Math.round(n.height);
289
+ if ("fills" in n && Array.isArray(n.fills)) {
290
+ const solid = n.fills.find((f) => f.type === "SOLID");
291
+ if (solid) base.fills = [{ type: "SOLID", color: rgbToHex(solid.color) }];
292
+ }
293
+ if ("strokes" in n && Array.isArray(n.strokes) && n.strokes.length > 0) {
294
+ const solid = n.strokes.find((s) => s.type === "SOLID");
295
+ if (solid) base.strokes = [{ type: "SOLID", color: rgbToHex(solid.color) }];
296
+ }
297
+ if ("strokeWeight" in n && typeof n.strokeWeight === "number" && n.strokeWeight > 0) {
298
+ base.strokeWeight = n.strokeWeight;
299
+ }
300
+ if ("cornerRadius" in n && typeof n.cornerRadius === "number" && n.cornerRadius > 0) {
301
+ base.cornerRadius = n.cornerRadius;
302
+ }
303
+ if ("opacity" in n && n.opacity !== 1) base.opacity = n.opacity;
304
+ if ("visible" in n && !n.visible) base.visible = false;
305
+ if ("locked" in n && n.locked) base.locked = true;
306
+ if ("layoutMode" in n && n.layoutMode !== "NONE") {
307
+ base.layoutMode = n.layoutMode;
308
+ if ("itemSpacing" in n) base.itemSpacing = n.itemSpacing;
309
+ }
310
+ if (n.type === "TEXT") {
311
+ const t = n;
312
+ base.characters = t.characters;
313
+ if (typeof t.fontSize === "number") base.fontSize = t.fontSize;
314
+ if (typeof t.fontName === "object") {
315
+ base.fontFamily = t.fontName.family;
316
+ base.fontStyle = t.fontName.style;
317
+ }
318
+ }
75
319
  if ("children" in n && n.children) {
76
- base.children = n.children.map(serializeTree);
320
+ base.children = n.children.map(serializeTreeNode);
77
321
  }
78
322
  return base;
79
323
  };
80
- return serializeTree(node);
324
+ return serializeTreeNode(node);
81
325
  }
82
326
  case "get-all-components": {
83
327
  const { name, limit = 50, page } = args || {};
@@ -97,7 +341,7 @@
97
341
  }
98
342
  return components.length < limit;
99
343
  };
100
- const pages = page ? figma.root.children.filter((p) => p.id === page || p.name === page) : figma.root.children;
344
+ const pages = page ? figma.root.children.filter((p) => p.id === page || p.name === page || p.name.includes(page)) : figma.root.children;
101
345
  for (const pageNode of pages) {
102
346
  if (components.length >= limit) break;
103
347
  for (const child of pageNode.children) {
@@ -115,9 +359,15 @@
115
359
  return { id: page.id, name: page.name };
116
360
  }
117
361
  case "set-current-page": {
118
- const { id } = args;
119
- const page = yield figma.getNodeByIdAsync(id);
120
- if (!page || page.type !== "PAGE") throw new Error("Page not found");
362
+ const { page: pageArg } = args;
363
+ let page = null;
364
+ const byId = yield figma.getNodeByIdAsync(pageArg);
365
+ if (byId && byId.type === "PAGE") {
366
+ page = byId;
367
+ } else {
368
+ page = figma.root.children.find((p) => p.name === pageArg || p.name.includes(pageArg)) || null;
369
+ }
370
+ if (!page) throw new Error("Page not found");
121
371
  yield figma.setCurrentPageAsync(page);
122
372
  return { id: page.id, name: page.name };
123
373
  }
@@ -297,7 +547,7 @@
297
547
  const textNode = figma.createText();
298
548
  const family = fontFamily || "Inter";
299
549
  const style = fontStyle || "Regular";
300
- yield figma.loadFontAsync({ family, style });
550
+ yield loadFont(family, style);
301
551
  textNode.x = x;
302
552
  textNode.y = y;
303
553
  textNode.fontName = { family, style };
@@ -346,7 +596,7 @@
346
596
  const { name, fontFamily, fontStyle, fontSize } = args;
347
597
  const style = figma.createTextStyle();
348
598
  style.name = name;
349
- yield figma.loadFontAsync({ family: fontFamily || "Inter", style: fontStyle || "Regular" });
599
+ yield loadFont(fontFamily || "Inter", fontStyle || "Regular");
350
600
  style.fontName = { family: fontFamily || "Inter", style: fontStyle || "Regular" };
351
601
  if (fontSize) style.fontSize = fontSize;
352
602
  return { id: style.id, name: style.name, key: style.key };
@@ -491,7 +741,7 @@
491
741
  const node = yield figma.getNodeByIdAsync(id);
492
742
  if (!node || node.type !== "TEXT") throw new Error("Text node not found");
493
743
  const fontName = node.fontName;
494
- yield figma.loadFontAsync(fontName);
744
+ yield loadFont(fontName.family, fontName.style);
495
745
  node.characters = text;
496
746
  return serializeNode(node);
497
747
  }
@@ -511,7 +761,7 @@
511
761
  const currentFont = node.fontName;
512
762
  const family = fontFamily || currentFont.family;
513
763
  const style = fontStyle || currentFont.style;
514
- yield figma.loadFontAsync({ family, style });
764
+ yield loadFont(family, style);
515
765
  node.fontName = { family, style };
516
766
  if (fontSize !== void 0) node.fontSize = fontSize;
517
767
  return serializeNode(node);
@@ -532,14 +782,24 @@
532
782
  return node.children.map((c) => serializeWithDepth(c, 1));
533
783
  }
534
784
  case "find-by-name": {
535
- const { name, type, exact } = args;
785
+ const { name, type, exact, limit = 100 } = args;
536
786
  const results = [];
537
- figma.currentPage.findAll((n) => {
538
- const nameMatch = !name || (exact ? n.name === name : n.name.toLowerCase().includes(name.toLowerCase()));
539
- const typeMatch = !type || n.type === type;
540
- if (nameMatch && typeMatch) results.push(serializeNode(n));
541
- return false;
542
- });
787
+ const nameLower = name == null ? void 0 : name.toLowerCase();
788
+ const searchNode = (node) => {
789
+ if (results.length >= limit) return false;
790
+ const nameMatch = !nameLower || (exact ? node.name === name : node.name.toLowerCase().includes(nameLower));
791
+ const typeMatch = !type || node.type === type;
792
+ if (nameMatch && typeMatch) results.push(serializeNode(node));
793
+ if ("children" in node) {
794
+ for (const child of node.children) {
795
+ if (!searchNode(child)) return false;
796
+ }
797
+ }
798
+ return results.length < limit;
799
+ };
800
+ for (const child of figma.currentPage.children) {
801
+ if (!searchNode(child)) break;
802
+ }
543
803
  return results;
544
804
  }
545
805
  case "select-nodes": {
@@ -610,7 +870,7 @@
610
870
  const node = yield figma.getNodeByIdAsync(id);
611
871
  if (!node || node.type !== "TEXT") throw new Error("Text node not found");
612
872
  const fontName = node.fontName;
613
- yield figma.loadFontAsync(fontName);
873
+ yield loadFont(fontName.family, fontName.style);
614
874
  if (lineHeight !== void 0) {
615
875
  node.lineHeight = lineHeight === "auto" ? { unit: "AUTO" } : { unit: "PIXELS", value: lineHeight };
616
876
  }
@@ -924,10 +1184,34 @@
924
1184
  const fn = new AsyncFunction("figma", wrappedCode);
925
1185
  return yield fn(figma);
926
1186
  }
1187
+ // ==================== LAYOUT ====================
1188
+ case "trigger-layout": {
1189
+ const { nodeId } = args;
1190
+ const root = yield figma.getNodeByIdAsync(nodeId);
1191
+ if (!root) return null;
1192
+ const triggerRecursive = (node) => {
1193
+ if ("layoutMode" in node && node.layoutMode !== "NONE" && "resize" in node) {
1194
+ const w = node.width;
1195
+ const h = node.height;
1196
+ node.resize(w + 0.01, h + 0.01);
1197
+ node.resize(w, h);
1198
+ }
1199
+ if ("children" in node) {
1200
+ for (const child of node.children) {
1201
+ triggerRecursive(child);
1202
+ }
1203
+ }
1204
+ };
1205
+ triggerRecursive(root);
1206
+ return { triggered: true };
1207
+ }
927
1208
  // ==================== VARIABLES ====================
928
1209
  case "get-variables": {
929
- const { type } = args;
1210
+ const { type, simple } = args;
930
1211
  const variables = yield figma.variables.getLocalVariablesAsync(type);
1212
+ if (simple) {
1213
+ return variables.map((v) => ({ id: v.id, name: v.name }));
1214
+ }
931
1215
  return variables.map((v) => serializeVariable(v));
932
1216
  }
933
1217
  case "get-variable": {
@@ -974,6 +1258,32 @@
974
1258
  }
975
1259
  return serializeNode(node);
976
1260
  }
1261
+ case "bind-fill-variable": {
1262
+ const { nodeId, variableId, paintIndex = 0 } = args;
1263
+ const node = yield figma.getNodeByIdAsync(nodeId);
1264
+ if (!node) throw new Error("Node not found");
1265
+ if (!("fills" in node)) throw new Error("Node does not have fills");
1266
+ const variable = yield figma.variables.getVariableByIdAsync(variableId);
1267
+ if (!variable) throw new Error("Variable not found");
1268
+ const fills = node.fills;
1269
+ if (!fills[paintIndex]) throw new Error("Paint not found at index " + paintIndex);
1270
+ const newFill = figma.variables.setBoundVariableForPaint(fills[paintIndex], "color", variable);
1271
+ node.fills = [...fills.slice(0, paintIndex), newFill, ...fills.slice(paintIndex + 1)];
1272
+ return serializeNode(node);
1273
+ }
1274
+ case "bind-stroke-variable": {
1275
+ const { nodeId, variableId, paintIndex = 0 } = args;
1276
+ const node = yield figma.getNodeByIdAsync(nodeId);
1277
+ if (!node) throw new Error("Node not found");
1278
+ if (!("strokes" in node)) throw new Error("Node does not have strokes");
1279
+ const variable = yield figma.variables.getVariableByIdAsync(variableId);
1280
+ if (!variable) throw new Error("Variable not found");
1281
+ const strokes = node.strokes;
1282
+ if (!strokes[paintIndex]) throw new Error("Paint not found at index " + paintIndex);
1283
+ const newStroke = figma.variables.setBoundVariableForPaint(strokes[paintIndex], "color", variable);
1284
+ node.strokes = [...strokes.slice(0, paintIndex), newStroke, ...strokes.slice(paintIndex + 1)];
1285
+ return serializeNode(node);
1286
+ }
977
1287
  // ==================== VARIABLE COLLECTIONS ====================
978
1288
  case "get-variable-collections": {
979
1289
  const collections = yield figma.variables.getLocalVariableCollectionsAsync();
@@ -1020,6 +1330,9 @@
1020
1330
  name: node.name,
1021
1331
  type: node.type
1022
1332
  };
1333
+ if (node.parent && node.parent.type !== "PAGE") {
1334
+ base.parentId = node.parent.id;
1335
+ }
1023
1336
  if ("x" in node) base.x = Math.round(node.x);
1024
1337
  if ("y" in node) base.y = Math.round(node.y);
1025
1338
  if ("width" in node) base.width = Math.round(node.width);
@@ -1036,6 +1349,9 @@
1036
1349
  if ("strokeWeight" in node && typeof node.strokeWeight === "number" && node.strokeWeight > 0) {
1037
1350
  base.strokeWeight = node.strokeWeight;
1038
1351
  }
1352
+ if ("cornerRadius" in node && typeof node.cornerRadius === "number" && node.cornerRadius > 0) {
1353
+ base.cornerRadius = node.cornerRadius;
1354
+ }
1039
1355
  if ("componentPropertyDefinitions" in node) {
1040
1356
  try {
1041
1357
  base.componentPropertyDefinitions = node.componentPropertyDefinitions;