@dannote/figma-use 0.2.1 → 0.3.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.3.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
  }
@@ -1020,6 +1280,9 @@
1020
1280
  name: node.name,
1021
1281
  type: node.type
1022
1282
  };
1283
+ if (node.parent && node.parent.type !== "PAGE") {
1284
+ base.parentId = node.parent.id;
1285
+ }
1023
1286
  if ("x" in node) base.x = Math.round(node.x);
1024
1287
  if ("y" in node) base.y = Math.round(node.y);
1025
1288
  if ("width" in node) base.width = Math.round(node.width);
@@ -1036,6 +1299,9 @@
1036
1299
  if ("strokeWeight" in node && typeof node.strokeWeight === "number" && node.strokeWeight > 0) {
1037
1300
  base.strokeWeight = node.strokeWeight;
1038
1301
  }
1302
+ if ("cornerRadius" in node && typeof node.cornerRadius === "number" && node.cornerRadius > 0) {
1303
+ base.cornerRadius = node.cornerRadius;
1304
+ }
1039
1305
  if ("componentPropertyDefinitions" in node) {
1040
1306
  try {
1041
1307
  base.componentPropertyDefinitions = node.componentPropertyDefinitions;