mcp-figma-toolkit 1.0.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/LICENSE +21 -0
- package/README.md +444 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +530 -0
- package/dist/server.js.map +1 -0
- package/package.json +57 -0
- package/plugin/manifest.json +12 -0
- package/plugin/plugin.js +1311 -0
- package/plugin/ui.html +30 -0
package/plugin/plugin.js
ADDED
|
@@ -0,0 +1,1311 @@
|
|
|
1
|
+
// Show UI (hidden) so we can use Web APIs (WebSocket in ui.html)
|
|
2
|
+
figma.showUI(__html__, { visible: false });
|
|
3
|
+
|
|
4
|
+
// ---------- Bridge ----------
|
|
5
|
+
figma.ui.onmessage = async (msg) => {
|
|
6
|
+
const { id, action, args } = msg || {};
|
|
7
|
+
try {
|
|
8
|
+
const result = await handleAction(action, args || {});
|
|
9
|
+
reply(id, Object.assign({ ok: true }, result || {}));
|
|
10
|
+
} catch (e) {
|
|
11
|
+
reply(id, { ok: false }, e instanceof Error ? e.message : String(e));
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
function reply(replyTo, result, error) {
|
|
15
|
+
figma.ui.postMessage({ replyTo, result, error });
|
|
16
|
+
}
|
|
17
|
+
const page = () => figma.currentPage;
|
|
18
|
+
|
|
19
|
+
// ---------- Utilities ----------
|
|
20
|
+
function hexToRGB(hex) {
|
|
21
|
+
const v = String(hex || "").replace("#", "").trim();
|
|
22
|
+
if (!/^[0-9a-fA-F]{6}$/.test(v)) throw new Error("Invalid hex color");
|
|
23
|
+
return { r: parseInt(v.slice(0,2),16)/255, g: parseInt(v.slice(2,4),16)/255, b: parseInt(v.slice(4,6),16)/255 };
|
|
24
|
+
}
|
|
25
|
+
function getNode(id) {
|
|
26
|
+
const n = figma.getNodeById(id);
|
|
27
|
+
if (!n) throw new Error("Node not found: " + id);
|
|
28
|
+
return n;
|
|
29
|
+
}
|
|
30
|
+
function assertFills(n) {
|
|
31
|
+
if (!("fills" in n)) throw new Error("Node does not support fills");
|
|
32
|
+
}
|
|
33
|
+
function base64ToUint8Array(b64) {
|
|
34
|
+
// Pure JS base64 decoder (atob not available in Figma plugin main context)
|
|
35
|
+
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
36
|
+
const lookup = new Uint8Array(256);
|
|
37
|
+
for (let i = 0; i < chars.length; i++) lookup[chars.charCodeAt(i)] = i;
|
|
38
|
+
|
|
39
|
+
// Remove padding and calculate output length
|
|
40
|
+
let padding = 0;
|
|
41
|
+
if (b64.endsWith("==")) padding = 2;
|
|
42
|
+
else if (b64.endsWith("=")) padding = 1;
|
|
43
|
+
|
|
44
|
+
const len = b64.length;
|
|
45
|
+
const bufferLength = Math.floor(len * 3 / 4) - padding;
|
|
46
|
+
const bytes = new Uint8Array(bufferLength);
|
|
47
|
+
|
|
48
|
+
let p = 0;
|
|
49
|
+
for (let i = 0; i < len; i += 4) {
|
|
50
|
+
const e1 = lookup[b64.charCodeAt(i)];
|
|
51
|
+
const e2 = lookup[b64.charCodeAt(i + 1)];
|
|
52
|
+
const e3 = lookup[b64.charCodeAt(i + 2)];
|
|
53
|
+
const e4 = lookup[b64.charCodeAt(i + 3)];
|
|
54
|
+
|
|
55
|
+
if (p < bufferLength) bytes[p++] = (e1 << 2) | (e2 >> 4);
|
|
56
|
+
if (p < bufferLength) bytes[p++] = ((e2 & 15) << 4) | (e3 >> 2);
|
|
57
|
+
if (p < bufferLength) bytes[p++] = ((e3 & 3) << 6) | e4;
|
|
58
|
+
}
|
|
59
|
+
return bytes;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function uint8ArrayToBase64(bytes) {
|
|
63
|
+
// Pure JS base64 encoder (btoa not available in Figma plugin main context)
|
|
64
|
+
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
65
|
+
let result = "";
|
|
66
|
+
const len = bytes.length;
|
|
67
|
+
for (let i = 0; i < len; i += 3) {
|
|
68
|
+
const b1 = bytes[i];
|
|
69
|
+
const b2 = i + 1 < len ? bytes[i + 1] : 0;
|
|
70
|
+
const b3 = i + 2 < len ? bytes[i + 2] : 0;
|
|
71
|
+
result += chars[b1 >> 2];
|
|
72
|
+
result += chars[((b1 & 3) << 4) | (b2 >> 4)];
|
|
73
|
+
result += i + 1 < len ? chars[((b2 & 15) << 2) | (b3 >> 6)] : "=";
|
|
74
|
+
result += i + 2 < len ? chars[b3 & 63] : "=";
|
|
75
|
+
}
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ---------- Actions dispatcher ----------
|
|
80
|
+
async function handleAction(action, input) {
|
|
81
|
+
switch (action) {
|
|
82
|
+
// Create
|
|
83
|
+
case "create_frame": return createFrame(input);
|
|
84
|
+
case "create_rectangle": return createRectangle(input);
|
|
85
|
+
case "create_ellipse": return createEllipse(input);
|
|
86
|
+
case "create_line": return createLine(input);
|
|
87
|
+
case "create_polygon": return createPolygon(input);
|
|
88
|
+
case "create_star": return createStar(input);
|
|
89
|
+
case "add_text": return addText(input);
|
|
90
|
+
case "place_image_base64": return placeImageBase64(input);
|
|
91
|
+
case "create_vector": return createVector(input);
|
|
92
|
+
case "place_image_url": return placeImageUrl(input);
|
|
93
|
+
|
|
94
|
+
// Selection / find / pages
|
|
95
|
+
case "find_nodes": return findNodes(input);
|
|
96
|
+
case "select_nodes": return selectNodes(input);
|
|
97
|
+
case "get_selection": return getSelection();
|
|
98
|
+
case "create_page": return createPage(input);
|
|
99
|
+
case "set_current_page": return setCurrentPage(input);
|
|
100
|
+
|
|
101
|
+
// Node management
|
|
102
|
+
case "rename_node": return renameNode(input);
|
|
103
|
+
case "delete_node": return deleteNode(input);
|
|
104
|
+
case "duplicate_node": return duplicateNode(input);
|
|
105
|
+
case "resize_node": return resizeNode(input);
|
|
106
|
+
case "rotate_node": return rotateNode(input);
|
|
107
|
+
case "set_position": return setPosition(input);
|
|
108
|
+
case "group_nodes": return groupNodes(input);
|
|
109
|
+
case "ungroup": return ungroup(input);
|
|
110
|
+
|
|
111
|
+
// Styling
|
|
112
|
+
case "set_fill": return setFill(input);
|
|
113
|
+
case "set_stroke": return setStroke(input);
|
|
114
|
+
case "set_corner_radius": return setCornerRadius(input);
|
|
115
|
+
case "set_opacity": return setOpacity(input);
|
|
116
|
+
case "set_blend_mode": return setBlendMode(input);
|
|
117
|
+
case "add_effect": return addEffect(input);
|
|
118
|
+
case "clear_effects": return clearEffects(input);
|
|
119
|
+
case "set_gradient_fill": return setGradientFill(input);
|
|
120
|
+
case "set_gradient_stroke": return setGradientStroke(input);
|
|
121
|
+
case "set_text_gradient": return setTextGradient(input);
|
|
122
|
+
case "layout_grid_add": return layoutGridAdd(input);
|
|
123
|
+
case "layout_grid_clear": return layoutGridClear(input);
|
|
124
|
+
|
|
125
|
+
// Auto Layout & Constraints
|
|
126
|
+
case "set_auto_layout": return setAutoLayout(input);
|
|
127
|
+
case "set_constraints": return setConstraints(input);
|
|
128
|
+
|
|
129
|
+
// Text
|
|
130
|
+
case "set_text_content": return setTextContent(input);
|
|
131
|
+
case "set_text_style": return setTextStyle(input);
|
|
132
|
+
case "set_text_color": return setTextColor(input);
|
|
133
|
+
|
|
134
|
+
// Components / booleans
|
|
135
|
+
case "create_component": return createComponent(input);
|
|
136
|
+
case "create_instance": return createInstance(input);
|
|
137
|
+
case "detach_instance": return detachInstance(input);
|
|
138
|
+
case "boolean_op": return booleanOp(input);
|
|
139
|
+
|
|
140
|
+
// Export / data / generic
|
|
141
|
+
case "export_node": return exportNode(input);
|
|
142
|
+
case "set_plugin_data": return setPluginData(input);
|
|
143
|
+
case "get_plugin_data": return getPluginData(input);
|
|
144
|
+
case "set_properties": return setProperties(input);
|
|
145
|
+
|
|
146
|
+
// Variables
|
|
147
|
+
case "create_variable_collection": return createVariableCollection(input);
|
|
148
|
+
case "create_variable": return createVariable(input);
|
|
149
|
+
case "get_local_variable_collections": return getLocalVariableCollections(input);
|
|
150
|
+
case "get_local_variables": return getLocalVariables(input);
|
|
151
|
+
case "set_variable_value": return setVariableValue(input);
|
|
152
|
+
case "bind_variable": return bindVariable(input);
|
|
153
|
+
case "unbind_variable": return unbindVariable(input);
|
|
154
|
+
case "delete_variable": return deleteVariable(input);
|
|
155
|
+
case "delete_variable_collection": return deleteVariableCollection(input);
|
|
156
|
+
|
|
157
|
+
// Styles
|
|
158
|
+
case "create_text_style": return createTextStyle(input);
|
|
159
|
+
case "create_effect_style": return createEffectStyle(input);
|
|
160
|
+
case "get_local_text_styles": return getLocalTextStyles(input);
|
|
161
|
+
case "get_local_effect_styles": return getLocalEffectStyles(input);
|
|
162
|
+
case "apply_text_style": return applyTextStyle(input);
|
|
163
|
+
case "apply_effect_style": return applyEffectStyle(input);
|
|
164
|
+
case "update_text_style": return updateTextStyle(input);
|
|
165
|
+
case "update_effect_style": return updateEffectStyle(input);
|
|
166
|
+
case "delete_style": return deleteStyle(input);
|
|
167
|
+
|
|
168
|
+
// Enhanced Components
|
|
169
|
+
case "create_component_from_node": return createComponentFromNode(input);
|
|
170
|
+
case "create_component_set": return createComponentSet(input);
|
|
171
|
+
case "add_component_property": return addComponentProperty(input);
|
|
172
|
+
case "set_instance_property": return setInstanceProperty(input);
|
|
173
|
+
case "get_component_properties": return getComponentProperties(input);
|
|
174
|
+
|
|
175
|
+
default:
|
|
176
|
+
throw new Error("Unknown action: " + action);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ---------- Create ----------
|
|
181
|
+
function getParent(parentId) {
|
|
182
|
+
if (!parentId) return page();
|
|
183
|
+
const parent = getNode(parentId);
|
|
184
|
+
if (!("appendChild" in parent)) throw new Error("Parent cannot contain children");
|
|
185
|
+
return parent;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function createFrame({ name = "Frame", width = 800, height = 600, x = 0, y = 0, parentId }) {
|
|
189
|
+
const f = figma.createFrame();
|
|
190
|
+
f.name = name; f.resize(width, height); f.x = x; f.y = y;
|
|
191
|
+
getParent(parentId).appendChild(f);
|
|
192
|
+
return { nodeId: f.id, type: f.type, name: f.name, width, height };
|
|
193
|
+
}
|
|
194
|
+
function createRectangle({ width, height, x = 0, y = 0, cornerRadius, hex, parentId }) {
|
|
195
|
+
const r = figma.createRectangle(); r.resize(width, height);
|
|
196
|
+
if (typeof cornerRadius === "number") r.cornerRadius = cornerRadius;
|
|
197
|
+
if (hex) r.fills = [{ type: "SOLID", color: hexToRGB(hex) }];
|
|
198
|
+
r.x = x; r.y = y; getParent(parentId).appendChild(r);
|
|
199
|
+
return { nodeId: r.id, type: r.type };
|
|
200
|
+
}
|
|
201
|
+
function createEllipse({ width, height, x = 0, y = 0, hex, parentId }) {
|
|
202
|
+
const e = figma.createEllipse(); e.resize(width, height);
|
|
203
|
+
if (hex) e.fills = [{ type: "SOLID", color: hexToRGB(hex) }];
|
|
204
|
+
e.x = x; e.y = y; getParent(parentId).appendChild(e);
|
|
205
|
+
return { nodeId: e.id, type: e.type };
|
|
206
|
+
}
|
|
207
|
+
function createLine({ x = 0, y = 0, length, rotation = 0, strokeHex = "#111827", strokeWeight = 1, parentId }) {
|
|
208
|
+
const l = figma.createLine();
|
|
209
|
+
l.x = x; l.y = y; l.rotation = rotation;
|
|
210
|
+
l.strokes = [{ type: "SOLID", color: hexToRGB(strokeHex) }];
|
|
211
|
+
l.strokeWeight = strokeWeight;
|
|
212
|
+
// Figma line length controlled via vector network — easiest: resize in x.
|
|
213
|
+
l.resize(length, 0);
|
|
214
|
+
getParent(parentId).appendChild(l);
|
|
215
|
+
return { nodeId: l.id, type: l.type };
|
|
216
|
+
}
|
|
217
|
+
function createPolygon({ sides, width, height, x = 0, y = 0, hex, parentId }) {
|
|
218
|
+
const p = figma.createPolygon(); p.pointCount = sides; p.resize(width, height);
|
|
219
|
+
if (hex) p.fills = [{ type: "SOLID", color: hexToRGB(hex) }];
|
|
220
|
+
p.x = x; p.y = y; getParent(parentId).appendChild(p);
|
|
221
|
+
return { nodeId: p.id, type: p.type };
|
|
222
|
+
}
|
|
223
|
+
function createStar({ points, width, height, x = 0, y = 0, hex, parentId }) {
|
|
224
|
+
const s = figma.createStar(); s.pointCount = points; s.resize(width, height);
|
|
225
|
+
if (hex) s.fills = [{ type: "SOLID", color: hexToRGB(hex) }];
|
|
226
|
+
s.x = x; s.y = y; getParent(parentId).appendChild(s);
|
|
227
|
+
return { nodeId: s.id, type: s.type };
|
|
228
|
+
}
|
|
229
|
+
async function addText({ text, x = 0, y = 0, fontFamily = "Inter", fontStyle = "Regular", fontSize = 32, parentId }) {
|
|
230
|
+
await figma.loadFontAsync({ family: fontFamily, style: fontStyle });
|
|
231
|
+
const t = figma.createText();
|
|
232
|
+
t.characters = text; t.fontName = { family: fontFamily, style: fontStyle };
|
|
233
|
+
if (fontSize) t.fontSize = fontSize;
|
|
234
|
+
t.x = x; t.y = y; getParent(parentId).appendChild(t);
|
|
235
|
+
return { nodeId: t.id, type: t.type, text: t.characters };
|
|
236
|
+
}
|
|
237
|
+
function placeImageBase64({ width, height, x = 0, y = 0, base64, parentId }) {
|
|
238
|
+
const bytes = base64ToUint8Array(base64);
|
|
239
|
+
const image = figma.createImage(bytes);
|
|
240
|
+
const r = figma.createRectangle(); r.resize(width, height); r.x = x; r.y = y;
|
|
241
|
+
r.fills = [{ type: "IMAGE", imageHash: image.hash, scaleMode: "FILL" }];
|
|
242
|
+
getParent(parentId).appendChild(r);
|
|
243
|
+
return { nodeId: r.id, type: r.type };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Convert SVG path to Figma's vectorPaths API format
|
|
247
|
+
// vectorPaths API supports: M, L, C, Q, Z (and H, V converted to L)
|
|
248
|
+
// vectorPaths API does NOT support: A (arc), S (smooth curve), T (smooth quadratic)
|
|
249
|
+
// This function converts relative to absolute AND converts S→C, T→Q
|
|
250
|
+
// Arc (A) commands throw an error - use svgString parameter instead for full SVG support
|
|
251
|
+
function normalizePathToAbsolute(pathData) {
|
|
252
|
+
const cmdRegex = /([MmLlHhVvCcSsQqTtAaZz])([^MmLlHhVvCcSsQqTtAaZz]*)/g;
|
|
253
|
+
const numRegex = /-?[\d.]+(?:e[+-]?\d+)?/gi;
|
|
254
|
+
|
|
255
|
+
let result = "";
|
|
256
|
+
let curX = 0, curY = 0;
|
|
257
|
+
let startX = 0, startY = 0;
|
|
258
|
+
// Track last control point for S and T commands
|
|
259
|
+
let lastCubicX2 = 0, lastCubicY2 = 0;
|
|
260
|
+
let lastQuadX1 = 0, lastQuadY1 = 0;
|
|
261
|
+
let lastCmd = '';
|
|
262
|
+
|
|
263
|
+
let match;
|
|
264
|
+
while ((match = cmdRegex.exec(pathData)) !== null) {
|
|
265
|
+
const cmd = match[1];
|
|
266
|
+
const argsStr = match[2];
|
|
267
|
+
const nums = argsStr.match(numRegex) || [];
|
|
268
|
+
const args = nums.map(Number);
|
|
269
|
+
|
|
270
|
+
const isRelative = cmd === cmd.toLowerCase();
|
|
271
|
+
const absCmd = cmd.toUpperCase();
|
|
272
|
+
|
|
273
|
+
switch (absCmd) {
|
|
274
|
+
case 'M':
|
|
275
|
+
for (let i = 0; i < args.length; i += 2) {
|
|
276
|
+
const x = isRelative ? curX + args[i] : args[i];
|
|
277
|
+
const y = isRelative ? curY + args[i + 1] : args[i + 1];
|
|
278
|
+
result += (i === 0 ? 'M ' : 'L ') + x + ' ' + y + ' ';
|
|
279
|
+
curX = x; curY = y;
|
|
280
|
+
if (i === 0) { startX = x; startY = y; }
|
|
281
|
+
}
|
|
282
|
+
lastCmd = 'M';
|
|
283
|
+
break;
|
|
284
|
+
|
|
285
|
+
case 'L':
|
|
286
|
+
for (let i = 0; i < args.length; i += 2) {
|
|
287
|
+
const x = isRelative ? curX + args[i] : args[i];
|
|
288
|
+
const y = isRelative ? curY + args[i + 1] : args[i + 1];
|
|
289
|
+
result += 'L ' + x + ' ' + y + ' ';
|
|
290
|
+
curX = x; curY = y;
|
|
291
|
+
}
|
|
292
|
+
lastCmd = 'L';
|
|
293
|
+
break;
|
|
294
|
+
|
|
295
|
+
case 'H':
|
|
296
|
+
for (let i = 0; i < args.length; i++) {
|
|
297
|
+
const x = isRelative ? curX + args[i] : args[i];
|
|
298
|
+
result += 'L ' + x + ' ' + curY + ' ';
|
|
299
|
+
curX = x;
|
|
300
|
+
}
|
|
301
|
+
lastCmd = 'H';
|
|
302
|
+
break;
|
|
303
|
+
|
|
304
|
+
case 'V':
|
|
305
|
+
for (let i = 0; i < args.length; i++) {
|
|
306
|
+
const y = isRelative ? curY + args[i] : args[i];
|
|
307
|
+
result += 'L ' + curX + ' ' + y + ' ';
|
|
308
|
+
curY = y;
|
|
309
|
+
}
|
|
310
|
+
lastCmd = 'V';
|
|
311
|
+
break;
|
|
312
|
+
|
|
313
|
+
case 'C':
|
|
314
|
+
for (let i = 0; i < args.length; i += 6) {
|
|
315
|
+
const x1 = isRelative ? curX + args[i] : args[i];
|
|
316
|
+
const y1 = isRelative ? curY + args[i + 1] : args[i + 1];
|
|
317
|
+
const x2 = isRelative ? curX + args[i + 2] : args[i + 2];
|
|
318
|
+
const y2 = isRelative ? curY + args[i + 3] : args[i + 3];
|
|
319
|
+
const x = isRelative ? curX + args[i + 4] : args[i + 4];
|
|
320
|
+
const y = isRelative ? curY + args[i + 5] : args[i + 5];
|
|
321
|
+
result += 'C ' + x1 + ' ' + y1 + ' ' + x2 + ' ' + y2 + ' ' + x + ' ' + y + ' ';
|
|
322
|
+
lastCubicX2 = x2; lastCubicY2 = y2;
|
|
323
|
+
curX = x; curY = y;
|
|
324
|
+
}
|
|
325
|
+
lastCmd = 'C';
|
|
326
|
+
break;
|
|
327
|
+
|
|
328
|
+
case 'S': // Smooth curve → Convert to C
|
|
329
|
+
for (let i = 0; i < args.length; i += 4) {
|
|
330
|
+
// First control point is reflection of last cubic's second control point
|
|
331
|
+
let x1, y1;
|
|
332
|
+
if (lastCmd === 'C' || lastCmd === 'S') {
|
|
333
|
+
x1 = 2 * curX - lastCubicX2;
|
|
334
|
+
y1 = 2 * curY - lastCubicY2;
|
|
335
|
+
} else {
|
|
336
|
+
x1 = curX;
|
|
337
|
+
y1 = curY;
|
|
338
|
+
}
|
|
339
|
+
const x2 = isRelative ? curX + args[i] : args[i];
|
|
340
|
+
const y2 = isRelative ? curY + args[i + 1] : args[i + 1];
|
|
341
|
+
const x = isRelative ? curX + args[i + 2] : args[i + 2];
|
|
342
|
+
const y = isRelative ? curY + args[i + 3] : args[i + 3];
|
|
343
|
+
result += 'C ' + x1 + ' ' + y1 + ' ' + x2 + ' ' + y2 + ' ' + x + ' ' + y + ' ';
|
|
344
|
+
lastCubicX2 = x2; lastCubicY2 = y2;
|
|
345
|
+
curX = x; curY = y;
|
|
346
|
+
}
|
|
347
|
+
lastCmd = 'S';
|
|
348
|
+
break;
|
|
349
|
+
|
|
350
|
+
case 'Q':
|
|
351
|
+
for (let i = 0; i < args.length; i += 4) {
|
|
352
|
+
const x1 = isRelative ? curX + args[i] : args[i];
|
|
353
|
+
const y1 = isRelative ? curY + args[i + 1] : args[i + 1];
|
|
354
|
+
const x = isRelative ? curX + args[i + 2] : args[i + 2];
|
|
355
|
+
const y = isRelative ? curY + args[i + 3] : args[i + 3];
|
|
356
|
+
result += 'Q ' + x1 + ' ' + y1 + ' ' + x + ' ' + y + ' ';
|
|
357
|
+
lastQuadX1 = x1; lastQuadY1 = y1;
|
|
358
|
+
curX = x; curY = y;
|
|
359
|
+
}
|
|
360
|
+
lastCmd = 'Q';
|
|
361
|
+
break;
|
|
362
|
+
|
|
363
|
+
case 'T': // Smooth quadratic → Convert to Q
|
|
364
|
+
for (let i = 0; i < args.length; i += 2) {
|
|
365
|
+
let x1, y1;
|
|
366
|
+
if (lastCmd === 'Q' || lastCmd === 'T') {
|
|
367
|
+
x1 = 2 * curX - lastQuadX1;
|
|
368
|
+
y1 = 2 * curY - lastQuadY1;
|
|
369
|
+
} else {
|
|
370
|
+
x1 = curX;
|
|
371
|
+
y1 = curY;
|
|
372
|
+
}
|
|
373
|
+
const x = isRelative ? curX + args[i] : args[i];
|
|
374
|
+
const y = isRelative ? curY + args[i + 1] : args[i + 1];
|
|
375
|
+
result += 'Q ' + x1 + ' ' + y1 + ' ' + x + ' ' + y + ' ';
|
|
376
|
+
lastQuadX1 = x1; lastQuadY1 = y1;
|
|
377
|
+
curX = x; curY = y;
|
|
378
|
+
}
|
|
379
|
+
lastCmd = 'T';
|
|
380
|
+
break;
|
|
381
|
+
|
|
382
|
+
case 'A': // Arc - not supported by Figma's vectorPaths API
|
|
383
|
+
throw new Error("SVG Arc commands (A/a) are not supported in pathData mode. Use 'svgString' parameter instead for full SVG support.");
|
|
384
|
+
|
|
385
|
+
case 'Z':
|
|
386
|
+
result += 'Z ';
|
|
387
|
+
curX = startX; curY = startY;
|
|
388
|
+
lastCmd = 'Z';
|
|
389
|
+
break;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return result.trim();
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function createVector({ pathData, width, height, x = 0, y = 0, fillHex, strokeHex, strokeWeight = 1, name = "Vector", parentId, svgString }) {
|
|
397
|
+
// If full SVG string provided, use createNodeFromSvg (handles arcs, circles, etc.)
|
|
398
|
+
if (svgString) {
|
|
399
|
+
const frame = figma.createNodeFromSvg(svgString);
|
|
400
|
+
frame.name = name;
|
|
401
|
+
frame.x = x;
|
|
402
|
+
frame.y = y;
|
|
403
|
+
if (width && height) {
|
|
404
|
+
frame.resize(width, height);
|
|
405
|
+
}
|
|
406
|
+
getParent(parentId).appendChild(frame);
|
|
407
|
+
return { nodeId: frame.id, type: frame.type, name: frame.name };
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Validate pathData is provided
|
|
411
|
+
if (!pathData) {
|
|
412
|
+
throw new Error("Either 'svgString' or 'pathData' must be provided");
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Otherwise use pathData with vectorPaths API
|
|
416
|
+
const vector = figma.createVector();
|
|
417
|
+
vector.name = name;
|
|
418
|
+
|
|
419
|
+
// Convert relative commands to absolute (Figma vectorPaths only supports absolute)
|
|
420
|
+
const normalizedPath = normalizePathToAbsolute(pathData);
|
|
421
|
+
|
|
422
|
+
vector.vectorPaths = [{
|
|
423
|
+
windingRule: "NONZERO",
|
|
424
|
+
data: normalizedPath
|
|
425
|
+
}];
|
|
426
|
+
|
|
427
|
+
// Resize to desired dimensions
|
|
428
|
+
vector.resize(width, height);
|
|
429
|
+
vector.x = x;
|
|
430
|
+
vector.y = y;
|
|
431
|
+
|
|
432
|
+
// Apply fill if specified
|
|
433
|
+
if (fillHex) {
|
|
434
|
+
vector.fills = [{ type: "SOLID", color: hexToRGB(fillHex) }];
|
|
435
|
+
} else {
|
|
436
|
+
vector.fills = [];
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Apply stroke if specified
|
|
440
|
+
if (strokeHex) {
|
|
441
|
+
vector.strokes = [{ type: "SOLID", color: hexToRGB(strokeHex) }];
|
|
442
|
+
vector.strokeWeight = strokeWeight;
|
|
443
|
+
vector.strokeCap = "ROUND";
|
|
444
|
+
vector.strokeJoin = "ROUND";
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
getParent(parentId).appendChild(vector);
|
|
448
|
+
return { nodeId: vector.id, type: vector.type, name: vector.name };
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function placeImageUrl({ width, height, x = 0, y = 0, base64, cornerRadius = 0, name = "Image", parentId }) {
|
|
452
|
+
// base64 is passed from server after fetching the URL
|
|
453
|
+
const bytes = base64ToUint8Array(base64);
|
|
454
|
+
const image = figma.createImage(bytes);
|
|
455
|
+
const r = figma.createRectangle();
|
|
456
|
+
r.name = name;
|
|
457
|
+
r.resize(width, height);
|
|
458
|
+
r.x = x;
|
|
459
|
+
r.y = y;
|
|
460
|
+
if (cornerRadius > 0) r.cornerRadius = cornerRadius;
|
|
461
|
+
r.fills = [{ type: "IMAGE", imageHash: image.hash, scaleMode: "FILL" }];
|
|
462
|
+
getParent(parentId).appendChild(r);
|
|
463
|
+
return { nodeId: r.id, type: r.type, name: r.name };
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// ---------- Selection / find / pages ----------
|
|
467
|
+
function findNodes({ type, nameContains, within }) {
|
|
468
|
+
let scope = within ? getNode(within) : page();
|
|
469
|
+
if (!("findAll" in scope)) throw new Error("Invalid 'within' scope");
|
|
470
|
+
const nodes = scope.findAll(n => {
|
|
471
|
+
const typeOk = type ? n.type === type : true;
|
|
472
|
+
const nameOk = nameContains ? (("name" in n) && String(n.name).toLowerCase().includes(nameContains.toLowerCase())) : true;
|
|
473
|
+
return typeOk && nameOk;
|
|
474
|
+
});
|
|
475
|
+
return nodes.map(n => ({ id: n.id, type: n.type, name: "name" in n ? n.name : undefined }));
|
|
476
|
+
}
|
|
477
|
+
function selectNodes({ nodeIds }) {
|
|
478
|
+
const nodes = nodeIds.map(getNode).filter(n => !!n);
|
|
479
|
+
figma.currentPage.selection = nodes;
|
|
480
|
+
return { selected: nodes.map(n => n.id) };
|
|
481
|
+
}
|
|
482
|
+
function getSelection() {
|
|
483
|
+
return figma.currentPage.selection.map(n => ({ id: n.id, type: n.type, name: "name" in n ? n.name : undefined }));
|
|
484
|
+
}
|
|
485
|
+
function createPage({ name = "Page", makeCurrent = true }) {
|
|
486
|
+
const p = figma.createPage(); p.name = name;
|
|
487
|
+
if (makeCurrent) figma.currentPage = p;
|
|
488
|
+
return { pageId: p.id, name: p.name };
|
|
489
|
+
}
|
|
490
|
+
function setCurrentPage({ pageId }) {
|
|
491
|
+
const p = getNode(pageId);
|
|
492
|
+
if (p.type !== "PAGE") throw new Error("Not a page");
|
|
493
|
+
figma.currentPage = p;
|
|
494
|
+
return { pageId: p.id };
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// ---------- Node management ----------
|
|
498
|
+
function renameNode({ nodeId, name }) { const n = getNode(nodeId); if ("name" in n) n.name = name; return { nodeId }; }
|
|
499
|
+
function deleteNode({ nodeId }) { const n = getNode(nodeId); n.remove(); return { removed: nodeId }; }
|
|
500
|
+
function duplicateNode({ nodeId, x, y }) {
|
|
501
|
+
const n = getNode(nodeId); const copy = n.clone();
|
|
502
|
+
if (typeof x === "number") copy.x = x;
|
|
503
|
+
if (typeof y === "number") copy.y = y;
|
|
504
|
+
n.parent && n.parent.appendChild(copy);
|
|
505
|
+
return { nodeId: copy.id };
|
|
506
|
+
}
|
|
507
|
+
function resizeNode({ nodeId, width, height }) { const n = getNode(nodeId); if (!("resize" in n)) throw new Error("Node cannot be resized"); n.resize(width, height); return { nodeId }; }
|
|
508
|
+
function rotateNode({ nodeId, rotation }) { const n = getNode(nodeId); if (!("rotation" in n)) throw new Error("No rotation on node"); n.rotation = rotation; return { nodeId }; }
|
|
509
|
+
function setPosition({ nodeId, x, y }) { const n = getNode(nodeId); if (!("x" in n && "y" in n)) throw new Error("Node not positionable"); n.x = x; n.y = y; return { nodeId }; }
|
|
510
|
+
function groupNodes({ nodeIds, name = "Group" }) {
|
|
511
|
+
const nodes = nodeIds.map(getNode).filter(n => !!n && "visible" in n);
|
|
512
|
+
if (nodes.length < 2) throw new Error("Need 2+ nodes");
|
|
513
|
+
const parent = nodes[0].parent || page();
|
|
514
|
+
const g = figma.group(nodes, parent); g.name = name; return { nodeId: g.id, type: g.type };
|
|
515
|
+
}
|
|
516
|
+
function ungroup({ groupId }) {
|
|
517
|
+
const g = getNode(groupId);
|
|
518
|
+
if (g.type !== "GROUP") throw new Error("Not a group");
|
|
519
|
+
const parent = g.parent || page();
|
|
520
|
+
const children = [];
|
|
521
|
+
for (let i = 0; i < g.children.length; i++) children.push(g.children[i]);
|
|
522
|
+
for (const c of children) parent.appendChild(c);
|
|
523
|
+
g.remove();
|
|
524
|
+
return { released: children.map(c => c.id) };
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// ---------- Styling ----------
|
|
528
|
+
function setFill({ nodeId, hex, opacity }) {
|
|
529
|
+
const n = getNode(nodeId); assertFills(n);
|
|
530
|
+
const fill = { type: "SOLID", color: hexToRGB(hex) };
|
|
531
|
+
if (typeof opacity === "number") fill.opacity = Math.max(0, Math.min(1, opacity));
|
|
532
|
+
n.fills = [fill];
|
|
533
|
+
return { nodeId };
|
|
534
|
+
}
|
|
535
|
+
function setStroke({ nodeId, hex, opacity, strokeWeight, strokeAlign, dashPattern, cap, join }) {
|
|
536
|
+
const n = getNode(nodeId);
|
|
537
|
+
if (!("strokes" in n)) throw new Error("Node does not support strokes");
|
|
538
|
+
const s = { type: "SOLID", color: hexToRGB(hex) };
|
|
539
|
+
if (typeof opacity === "number") s.opacity = Math.max(0, Math.min(1, opacity));
|
|
540
|
+
n.strokes = [s];
|
|
541
|
+
if (strokeWeight != null) n.strokeWeight = strokeWeight;
|
|
542
|
+
if (strokeAlign) n.strokeAlign = strokeAlign;
|
|
543
|
+
if (dashPattern) n.dashPattern = dashPattern;
|
|
544
|
+
if (cap) n.strokeCap = cap;
|
|
545
|
+
if (join) n.strokeJoin = join;
|
|
546
|
+
return { nodeId };
|
|
547
|
+
}
|
|
548
|
+
function setCornerRadius({ nodeId, radius, topLeft, topRight, bottomRight, bottomLeft }) {
|
|
549
|
+
const n = getNode(nodeId);
|
|
550
|
+
if ("cornerRadius" in n && typeof radius === "number") n.cornerRadius = radius;
|
|
551
|
+
if ("topLeftRadius" in n) {
|
|
552
|
+
if (typeof topLeft === "number") n.topLeftRadius = topLeft;
|
|
553
|
+
if (typeof topRight === "number") n.topRightRadius = topRight;
|
|
554
|
+
if (typeof bottomRight === "number") n.bottomRightRadius = bottomRight;
|
|
555
|
+
if (typeof bottomLeft === "number") n.bottomLeftRadius = bottomLeft;
|
|
556
|
+
}
|
|
557
|
+
return { nodeId };
|
|
558
|
+
}
|
|
559
|
+
function setOpacity({ nodeId, opacity }) { const n = getNode(nodeId); if (!("opacity" in n)) throw new Error("No opacity on node"); n.opacity = Math.max(0, Math.min(1, opacity)); return { nodeId }; }
|
|
560
|
+
function setBlendMode({ nodeId, mode }) { const n = getNode(nodeId); if (!("blendMode" in n)) throw new Error("No blend mode"); n.blendMode = mode; return { nodeId }; }
|
|
561
|
+
function addEffect({ nodeId, type, radius = 8, spread = 0, hex = "#000000", opacity = 0.25, offsetX = 0, offsetY = 2 }) {
|
|
562
|
+
const n = getNode(nodeId);
|
|
563
|
+
if (!("effects" in n)) throw new Error("Node does not support effects");
|
|
564
|
+
const newEff = (() => {
|
|
565
|
+
if (type === "LAYER_BLUR" || type === "BACKGROUND_BLUR") {
|
|
566
|
+
return { type, radius, visible: true };
|
|
567
|
+
}
|
|
568
|
+
const rgb = hexToRGB(hex);
|
|
569
|
+
const color = { r: rgb.r, g: rgb.g, b: rgb.b, a: opacity };
|
|
570
|
+
return {
|
|
571
|
+
type,
|
|
572
|
+
radius,
|
|
573
|
+
spread,
|
|
574
|
+
color,
|
|
575
|
+
offset: { x: offsetX, y: offsetY },
|
|
576
|
+
visible: true,
|
|
577
|
+
blendMode: "NORMAL"
|
|
578
|
+
};
|
|
579
|
+
})();
|
|
580
|
+
const currentEffects = [];
|
|
581
|
+
for (let i = 0; i < n.effects.length; i++) currentEffects.push(n.effects[i]);
|
|
582
|
+
currentEffects.push(newEff);
|
|
583
|
+
n.effects = currentEffects;
|
|
584
|
+
return { nodeId, effects: n.effects.length };
|
|
585
|
+
}
|
|
586
|
+
function clearEffects({ nodeId }) { const n = getNode(nodeId); if (!("effects" in n)) throw new Error("Node does not support effects"); n.effects = []; return { nodeId }; }
|
|
587
|
+
|
|
588
|
+
// ---------- Gradient tools ----------
|
|
589
|
+
function angleToTransform(angle = 135) {
|
|
590
|
+
// Convert angle to Figma's gradient transform matrix
|
|
591
|
+
// Figma uses a 2x3 matrix: [[a, c, tx], [b, d, ty]]
|
|
592
|
+
const radians = (angle * Math.PI) / 180;
|
|
593
|
+
const cos = Math.cos(radians);
|
|
594
|
+
const sin = Math.sin(radians);
|
|
595
|
+
// Adjust for Figma's coordinate system (0,0 is top-left, gradient goes from 0 to 1)
|
|
596
|
+
return [
|
|
597
|
+
[cos, sin, 0.5 - cos * 0.5 - sin * 0.5],
|
|
598
|
+
[-sin, cos, 0.5 + sin * 0.5 - cos * 0.5]
|
|
599
|
+
];
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function hexToRGBA(hex, opacity = 1) {
|
|
603
|
+
const rgb = hexToRGB(hex);
|
|
604
|
+
return { r: rgb.r, g: rgb.g, b: rgb.b, a: opacity };
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function setGradientFill({ nodeId, startHex, endHex, angle = 135, startOpacity = 1, endOpacity = 1 }) {
|
|
608
|
+
const n = getNode(nodeId);
|
|
609
|
+
assertFills(n);
|
|
610
|
+
const gradientFill = {
|
|
611
|
+
type: "GRADIENT_LINEAR",
|
|
612
|
+
gradientTransform: angleToTransform(angle),
|
|
613
|
+
gradientStops: [
|
|
614
|
+
{ position: 0, color: hexToRGBA(startHex, startOpacity) },
|
|
615
|
+
{ position: 1, color: hexToRGBA(endHex, endOpacity) }
|
|
616
|
+
]
|
|
617
|
+
};
|
|
618
|
+
n.fills = [gradientFill];
|
|
619
|
+
return { nodeId };
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function setGradientStroke({ nodeId, startHex, endHex, strokeWeight = 1, angle = 135, strokeAlign = "CENTER" }) {
|
|
623
|
+
const n = getNode(nodeId);
|
|
624
|
+
if (!("strokes" in n)) throw new Error("Node does not support strokes");
|
|
625
|
+
const gradientStroke = {
|
|
626
|
+
type: "GRADIENT_LINEAR",
|
|
627
|
+
gradientTransform: angleToTransform(angle),
|
|
628
|
+
gradientStops: [
|
|
629
|
+
{ position: 0, color: hexToRGBA(startHex, 1) },
|
|
630
|
+
{ position: 1, color: hexToRGBA(endHex, 1) }
|
|
631
|
+
]
|
|
632
|
+
};
|
|
633
|
+
n.strokes = [gradientStroke];
|
|
634
|
+
n.strokeWeight = strokeWeight;
|
|
635
|
+
n.strokeAlign = strokeAlign;
|
|
636
|
+
return { nodeId };
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function setTextGradient({ nodeId, startHex, endHex, angle = 135 }) {
|
|
640
|
+
const t = getNode(nodeId);
|
|
641
|
+
if (t.type !== "TEXT") throw new Error("Not a text node");
|
|
642
|
+
const gradientFill = {
|
|
643
|
+
type: "GRADIENT_LINEAR",
|
|
644
|
+
gradientTransform: angleToTransform(angle),
|
|
645
|
+
gradientStops: [
|
|
646
|
+
{ position: 0, color: hexToRGBA(startHex, 1) },
|
|
647
|
+
{ position: 1, color: hexToRGBA(endHex, 1) }
|
|
648
|
+
]
|
|
649
|
+
};
|
|
650
|
+
t.fills = [gradientFill];
|
|
651
|
+
return { nodeId };
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function layoutGridAdd({ nodeId, pattern = "COLUMNS", count = 12, gutterSize = 20, sectionSize = 80, hex = "#E5E7EB", opacity = 0.5 }) {
|
|
655
|
+
const n = getNode(nodeId);
|
|
656
|
+
if (!("layoutGrids" in n)) throw new Error("Node does not support layoutGrids");
|
|
657
|
+
const rgb = hexToRGB(hex);
|
|
658
|
+
const g = { pattern, count, gutterSize, sectionSize, color: { r: rgb.r, g: rgb.g, b: rgb.b, a: opacity } };
|
|
659
|
+
const currentGrids = [];
|
|
660
|
+
for (let i = 0; i < n.layoutGrids.length; i++) currentGrids.push(n.layoutGrids[i]);
|
|
661
|
+
currentGrids.push(g);
|
|
662
|
+
n.layoutGrids = currentGrids;
|
|
663
|
+
return { nodeId, grids: n.layoutGrids.length };
|
|
664
|
+
}
|
|
665
|
+
function layoutGridClear({ nodeId }) { const n = getNode(nodeId); if (!("layoutGrids" in n)) throw new Error("Node does not support layoutGrids"); n.layoutGrids = []; return { nodeId }; }
|
|
666
|
+
|
|
667
|
+
// ---------- Auto Layout & Constraints ----------
|
|
668
|
+
function setAutoLayout(input) {
|
|
669
|
+
const nodeId = input.nodeId;
|
|
670
|
+
const props = Object.assign({}, input);
|
|
671
|
+
delete props.nodeId;
|
|
672
|
+
const f = getNode(nodeId);
|
|
673
|
+
if (f.type !== "FRAME") throw new Error("Auto Layout only on frames");
|
|
674
|
+
const map = {
|
|
675
|
+
layoutMode: "layoutMode",
|
|
676
|
+
primaryAxisSizingMode: "primaryAxisSizingMode",
|
|
677
|
+
counterAxisSizingMode: "counterAxisSizingMode",
|
|
678
|
+
itemSpacing: "itemSpacing",
|
|
679
|
+
paddingTop: "paddingTop",
|
|
680
|
+
paddingRight: "paddingRight",
|
|
681
|
+
paddingBottom: "paddingBottom",
|
|
682
|
+
paddingLeft: "paddingLeft",
|
|
683
|
+
primaryAxisAlignItems: "primaryAxisAlignItems",
|
|
684
|
+
counterAxisAlignItems: "counterAxisAlignItems",
|
|
685
|
+
layoutWrap: "layoutWrap",
|
|
686
|
+
counterAxisSpacing: "counterAxisSpacing",
|
|
687
|
+
layoutPositioning: "layoutPositioning"
|
|
688
|
+
};
|
|
689
|
+
for (const k in map) if (k in props) f[map[k]] = props[k];
|
|
690
|
+
return { nodeId: f.id };
|
|
691
|
+
}
|
|
692
|
+
function setConstraints({ nodeId, horizontal, vertical }) {
|
|
693
|
+
const n = getNode(nodeId);
|
|
694
|
+
if (!("constraints" in n)) throw new Error("No constraints on node");
|
|
695
|
+
n.constraints = {
|
|
696
|
+
horizontal: horizontal || n.constraints.horizontal,
|
|
697
|
+
vertical: vertical || n.constraints.vertical
|
|
698
|
+
};
|
|
699
|
+
return { nodeId };
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// ---------- Text ----------
|
|
703
|
+
async function setTextContent({ nodeId, text }) {
|
|
704
|
+
const t = getNode(nodeId);
|
|
705
|
+
if (t.type !== "TEXT") throw new Error("Not a text node");
|
|
706
|
+
const font = t.fontName;
|
|
707
|
+
if (font && typeof font !== "symbol") await figma.loadFontAsync(font);
|
|
708
|
+
t.characters = text;
|
|
709
|
+
return { nodeId };
|
|
710
|
+
}
|
|
711
|
+
async function setTextStyle({ nodeId, fontFamily, fontStyle, fontSize, lineHeight, letterSpacing, textAlignHorizontal, textAutoResize }) {
|
|
712
|
+
const t = getNode(nodeId);
|
|
713
|
+
if (t.type !== "TEXT") throw new Error("Not a text node");
|
|
714
|
+
const fam = fontFamily || (typeof t.fontName !== "symbol" ? t.fontName.family : "Inter");
|
|
715
|
+
const sty = fontStyle || (typeof t.fontName !== "symbol" ? t.fontName.style : "Regular");
|
|
716
|
+
await figma.loadFontAsync({ family: fam, style: sty });
|
|
717
|
+
t.fontName = { family: fam, style: sty };
|
|
718
|
+
if (fontSize != null) t.fontSize = fontSize;
|
|
719
|
+
if (lineHeight != null) t.lineHeight = { unit: "PIXELS", value: lineHeight };
|
|
720
|
+
if (letterSpacing != null) t.letterSpacing = { unit: "PIXELS", value: letterSpacing };
|
|
721
|
+
if (textAlignHorizontal) t.textAlignHorizontal = textAlignHorizontal;
|
|
722
|
+
if (textAutoResize) t.textAutoResize = textAutoResize;
|
|
723
|
+
return { nodeId };
|
|
724
|
+
}
|
|
725
|
+
function setTextColor({ nodeId, hex, opacity }) {
|
|
726
|
+
const t = getNode(nodeId);
|
|
727
|
+
if (t.type !== "TEXT") throw new Error("Not a text node");
|
|
728
|
+
const fill = { type: "SOLID", color: hexToRGB(hex) };
|
|
729
|
+
if (typeof opacity === "number") fill.opacity = Math.max(0, Math.min(1, opacity));
|
|
730
|
+
t.fills = [fill];
|
|
731
|
+
return { nodeId };
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// ---------- Components & Boolean ----------
|
|
735
|
+
function createComponent({ name = "Component", fromNodeIds }) {
|
|
736
|
+
const c = figma.createComponent(); c.name = name;
|
|
737
|
+
page().appendChild(c);
|
|
738
|
+
if (Array.isArray(fromNodeIds) && fromNodeIds.length) {
|
|
739
|
+
const nodes = fromNodeIds.map(getNode);
|
|
740
|
+
for (const n of nodes) c.appendChild(n);
|
|
741
|
+
}
|
|
742
|
+
return { nodeId: c.id, type: c.type };
|
|
743
|
+
}
|
|
744
|
+
function createInstance({ componentId, x = 0, y = 0 }) {
|
|
745
|
+
const c = getNode(componentId);
|
|
746
|
+
if (c.type !== "COMPONENT") throw new Error("Not a component");
|
|
747
|
+
const inst = c.createInstance(); inst.x = x; inst.y = y; page().appendChild(inst);
|
|
748
|
+
return { nodeId: inst.id, type: inst.type };
|
|
749
|
+
}
|
|
750
|
+
function detachInstance({ nodeId }) {
|
|
751
|
+
const n = getNode(nodeId);
|
|
752
|
+
if ("detachInstance" in n) {
|
|
753
|
+
const d = n.detachInstance();
|
|
754
|
+
return { nodeId: d.id, type: d.type };
|
|
755
|
+
}
|
|
756
|
+
throw new Error("Node is not an instance");
|
|
757
|
+
}
|
|
758
|
+
function booleanOp({ op, nodeIds, name = "Boolean" }) {
|
|
759
|
+
const nodes = nodeIds.map(getNode);
|
|
760
|
+
const parent = nodes[0].parent || page();
|
|
761
|
+
let res;
|
|
762
|
+
switch (op) {
|
|
763
|
+
case "UNION": res = figma.union(nodes, parent); break;
|
|
764
|
+
case "SUBTRACT": res = figma.subtract(nodes, parent); break;
|
|
765
|
+
case "INTERSECT": res = figma.intersect(nodes, parent); break;
|
|
766
|
+
case "EXCLUDE": res = figma.exclude(nodes, parent); break;
|
|
767
|
+
}
|
|
768
|
+
res.name = name;
|
|
769
|
+
return { nodeId: res.id, type: res.type };
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// ---------- Export / plugin data / generic ----------
|
|
773
|
+
async function exportNode({ nodeId, format = "PNG", scale = 1 }) {
|
|
774
|
+
const n = getNode(nodeId);
|
|
775
|
+
const bytes = await n.exportAsync({ format, constraint: { type: "SCALE", value: scale } });
|
|
776
|
+
const base64 = uint8ArrayToBase64(bytes);
|
|
777
|
+
return { format, base64 };
|
|
778
|
+
}
|
|
779
|
+
function setPluginData({ nodeId, key, value }) {
|
|
780
|
+
const n = getNode(nodeId);
|
|
781
|
+
n.setPluginData(key, JSON.stringify(value));
|
|
782
|
+
return { nodeId };
|
|
783
|
+
}
|
|
784
|
+
function getPluginData({ nodeId, key }) {
|
|
785
|
+
const n = getNode(nodeId);
|
|
786
|
+
const raw = n.getPluginData(key);
|
|
787
|
+
try {
|
|
788
|
+
return { value: JSON.parse(raw) };
|
|
789
|
+
} catch (e) {
|
|
790
|
+
return { value: raw };
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
function setProperties({ nodeId, props }) {
|
|
794
|
+
const n = getNode(nodeId);
|
|
795
|
+
// Whitelisted scalar props (expand as needed)
|
|
796
|
+
const allowed = [
|
|
797
|
+
"x","y","rotation","opacity","visible","locked",
|
|
798
|
+
"layoutAlign","layoutGrow",
|
|
799
|
+
"fills","strokes","strokeWeight","strokeAlign","dashPattern","blendMode",
|
|
800
|
+
"itemSpacing","paddingTop","paddingRight","paddingBottom","paddingLeft",
|
|
801
|
+
"primaryAxisAlignItems","counterAxisAlignItems","layoutMode",
|
|
802
|
+
"primaryAxisSizingMode","counterAxisSizingMode","layoutWrap","counterAxisSpacing",
|
|
803
|
+
"textAlignHorizontal","textAlignVertical"
|
|
804
|
+
];
|
|
805
|
+
for (const k of Object.keys(props || {})) {
|
|
806
|
+
if (allowed.includes(k)) {
|
|
807
|
+
try { n[k] = props[k]; } catch (_) {}
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
return { nodeId };
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// ========== VARIABLES ==========
|
|
814
|
+
|
|
815
|
+
function createVariableCollection({ name, modes = ["Default"] }) {
|
|
816
|
+
const collection = figma.variables.createVariableCollection(name);
|
|
817
|
+
|
|
818
|
+
// Ensure collection is visible (not hidden from publishing)
|
|
819
|
+
collection.hiddenFromPublishing = false;
|
|
820
|
+
|
|
821
|
+
// Rename the default mode and add additional modes
|
|
822
|
+
const modeIds = [];
|
|
823
|
+
for (let i = 0; i < modes.length; i++) {
|
|
824
|
+
if (i === 0) {
|
|
825
|
+
// Rename the default mode that's automatically created
|
|
826
|
+
collection.renameMode(collection.modes[0].modeId, modes[i]);
|
|
827
|
+
modeIds.push(collection.modes[0].modeId);
|
|
828
|
+
} else {
|
|
829
|
+
// Add new modes
|
|
830
|
+
const modeId = collection.addMode(modes[i]);
|
|
831
|
+
modeIds.push(modeId);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
return {
|
|
835
|
+
collectionId: collection.id,
|
|
836
|
+
name: collection.name,
|
|
837
|
+
modes: collection.modes.map(m => ({ modeId: m.modeId, name: m.name }))
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
function createVariable({ collectionId, name, resolvedType, values, scopes }) {
|
|
842
|
+
// resolvedType: "COLOR" | "FLOAT" | "STRING" | "BOOLEAN"
|
|
843
|
+
const collection = figma.variables.getVariableCollectionById(collectionId);
|
|
844
|
+
if (!collection) throw new Error("Variable collection not found: " + collectionId);
|
|
845
|
+
|
|
846
|
+
const variable = figma.variables.createVariable(name, collection, resolvedType);
|
|
847
|
+
|
|
848
|
+
// Ensure variable is not hidden from publishing (affects visibility)
|
|
849
|
+
variable.hiddenFromPublishing = false;
|
|
850
|
+
|
|
851
|
+
// Set scopes to make variable visible in UI picker
|
|
852
|
+
// If not provided, default to ALL_SCOPES for visibility
|
|
853
|
+
if (scopes && scopes.length > 0) {
|
|
854
|
+
variable.scopes = scopes;
|
|
855
|
+
} else {
|
|
856
|
+
// Default scopes based on type
|
|
857
|
+
if (resolvedType === "COLOR") {
|
|
858
|
+
variable.scopes = ["ALL_FILLS", "STROKE_COLOR", "EFFECT_COLOR"];
|
|
859
|
+
} else if (resolvedType === "FLOAT") {
|
|
860
|
+
variable.scopes = ["ALL_SCOPES"];
|
|
861
|
+
} else if (resolvedType === "STRING") {
|
|
862
|
+
variable.scopes = ["ALL_SCOPES"];
|
|
863
|
+
}
|
|
864
|
+
// BOOLEAN variables don't support scopes
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// Set values for each mode if provided
|
|
868
|
+
if (values) {
|
|
869
|
+
for (const modeId in values) {
|
|
870
|
+
let value = values[modeId];
|
|
871
|
+
// Convert hex to RGB for COLOR type
|
|
872
|
+
if (resolvedType === "COLOR" && typeof value === "string") {
|
|
873
|
+
value = hexToRGB(value);
|
|
874
|
+
}
|
|
875
|
+
variable.setValueForMode(modeId, value);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
return {
|
|
880
|
+
variableId: variable.id,
|
|
881
|
+
name: variable.name,
|
|
882
|
+
resolvedType: variable.resolvedType,
|
|
883
|
+
collectionId: collection.id,
|
|
884
|
+
scopes: variable.scopes
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
function getLocalVariableCollections() {
|
|
889
|
+
const collections = figma.variables.getLocalVariableCollections();
|
|
890
|
+
return collections.map(c => ({
|
|
891
|
+
collectionId: c.id,
|
|
892
|
+
name: c.name,
|
|
893
|
+
modes: c.modes.map(m => ({ modeId: m.modeId, name: m.name })),
|
|
894
|
+
variableIds: c.variableIds
|
|
895
|
+
}));
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
function getLocalVariables({ collectionId } = {}) {
|
|
899
|
+
const variables = figma.variables.getLocalVariables();
|
|
900
|
+
const filtered = collectionId
|
|
901
|
+
? variables.filter(v => v.variableCollectionId === collectionId)
|
|
902
|
+
: variables;
|
|
903
|
+
|
|
904
|
+
return filtered.map(v => {
|
|
905
|
+
const valuesByMode = {};
|
|
906
|
+
const collection = figma.variables.getVariableCollectionById(v.variableCollectionId);
|
|
907
|
+
if (collection) {
|
|
908
|
+
for (const mode of collection.modes) {
|
|
909
|
+
let val = v.valuesByMode[mode.modeId];
|
|
910
|
+
// Convert RGB back to hex for COLOR type
|
|
911
|
+
if (v.resolvedType === "COLOR" && val && typeof val === "object" && "r" in val) {
|
|
912
|
+
val = rgbToHex(val);
|
|
913
|
+
}
|
|
914
|
+
valuesByMode[mode.modeId] = val;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
return {
|
|
918
|
+
variableId: v.id,
|
|
919
|
+
name: v.name,
|
|
920
|
+
resolvedType: v.resolvedType,
|
|
921
|
+
collectionId: v.variableCollectionId,
|
|
922
|
+
valuesByMode
|
|
923
|
+
};
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
function setVariableValue({ variableId, modeId, value }) {
|
|
928
|
+
const variable = figma.variables.getVariableById(variableId);
|
|
929
|
+
if (!variable) throw new Error("Variable not found: " + variableId);
|
|
930
|
+
|
|
931
|
+
let processedValue = value;
|
|
932
|
+
if (variable.resolvedType === "COLOR" && typeof value === "string") {
|
|
933
|
+
processedValue = hexToRGB(value);
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
variable.setValueForMode(modeId, processedValue);
|
|
937
|
+
return { variableId, modeId, value: processedValue };
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
function bindVariable({ nodeId, field, variableId }) {
|
|
941
|
+
// field examples: "fill", "stroke", "width", "height", "itemSpacing", etc.
|
|
942
|
+
const node = getNode(nodeId);
|
|
943
|
+
const variable = figma.variables.getVariableById(variableId);
|
|
944
|
+
if (!variable) throw new Error("Variable not found: " + variableId);
|
|
945
|
+
|
|
946
|
+
// Handle different field types
|
|
947
|
+
if (field === "fill" || field === "fills") {
|
|
948
|
+
// For fill, bind variable to the first solid paint
|
|
949
|
+
if (!("fills" in node)) throw new Error("Node does not support fills");
|
|
950
|
+
const solidPaint = figma.variables.setBoundVariableForPaint(
|
|
951
|
+
{ type: "SOLID", color: { r: 0, g: 0, b: 0 } },
|
|
952
|
+
"color",
|
|
953
|
+
variable
|
|
954
|
+
);
|
|
955
|
+
node.fills = [solidPaint];
|
|
956
|
+
} else if (field === "stroke" || field === "strokes") {
|
|
957
|
+
if (!("strokes" in node)) throw new Error("Node does not support strokes");
|
|
958
|
+
const solidPaint = figma.variables.setBoundVariableForPaint(
|
|
959
|
+
{ type: "SOLID", color: { r: 0, g: 0, b: 0 } },
|
|
960
|
+
"color",
|
|
961
|
+
variable
|
|
962
|
+
);
|
|
963
|
+
node.strokes = [solidPaint];
|
|
964
|
+
} else {
|
|
965
|
+
// For other scalar properties like width, height, itemSpacing, padding, etc.
|
|
966
|
+
node.setBoundVariable(field, variable);
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
return { nodeId, field, variableId };
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
function unbindVariable({ nodeId, field }) {
|
|
973
|
+
const node = getNode(nodeId);
|
|
974
|
+
|
|
975
|
+
if (field === "fill" || field === "fills") {
|
|
976
|
+
// For fills, we need to replace with an unbound solid paint
|
|
977
|
+
if (!("fills" in node)) throw new Error("Node does not support fills");
|
|
978
|
+
const currentFills = node.fills;
|
|
979
|
+
if (currentFills.length > 0 && currentFills[0].type === "SOLID") {
|
|
980
|
+
// Keep the current color but remove the variable binding
|
|
981
|
+
node.fills = [{ type: "SOLID", color: currentFills[0].color }];
|
|
982
|
+
}
|
|
983
|
+
} else if (field === "stroke" || field === "strokes") {
|
|
984
|
+
// For strokes, same approach
|
|
985
|
+
if (!("strokes" in node)) throw new Error("Node does not support strokes");
|
|
986
|
+
const currentStrokes = node.strokes;
|
|
987
|
+
if (currentStrokes.length > 0 && currentStrokes[0].type === "SOLID") {
|
|
988
|
+
node.strokes = [{ type: "SOLID", color: currentStrokes[0].color }];
|
|
989
|
+
}
|
|
990
|
+
} else {
|
|
991
|
+
// For scalar properties, use setBoundVariable with null
|
|
992
|
+
node.setBoundVariable(field, null);
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
return { nodeId, field };
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
function deleteVariable({ variableId }) {
|
|
999
|
+
const variable = figma.variables.getVariableById(variableId);
|
|
1000
|
+
if (!variable) throw new Error("Variable not found: " + variableId);
|
|
1001
|
+
variable.remove();
|
|
1002
|
+
return { deleted: variableId };
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
function deleteVariableCollection({ collectionId }) {
|
|
1006
|
+
const collection = figma.variables.getVariableCollectionById(collectionId);
|
|
1007
|
+
if (!collection) throw new Error("Variable collection not found: " + collectionId);
|
|
1008
|
+
collection.remove();
|
|
1009
|
+
return { deleted: collectionId };
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
// Helper to convert RGB to hex
|
|
1013
|
+
function rgbToHex(rgb) {
|
|
1014
|
+
const toHex = (n) => {
|
|
1015
|
+
const hex = Math.round(n * 255).toString(16);
|
|
1016
|
+
return hex.length === 1 ? "0" + hex : hex;
|
|
1017
|
+
};
|
|
1018
|
+
return "#" + toHex(rgb.r) + toHex(rgb.g) + toHex(rgb.b);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
// ========== STYLES ==========
|
|
1022
|
+
|
|
1023
|
+
async function createTextStyle({ name, fontFamily = "Inter", fontStyle = "Regular", fontSize = 16, lineHeight, letterSpacing, textCase, textDecoration }) {
|
|
1024
|
+
await figma.loadFontAsync({ family: fontFamily, style: fontStyle });
|
|
1025
|
+
|
|
1026
|
+
const style = figma.createTextStyle();
|
|
1027
|
+
style.name = name;
|
|
1028
|
+
style.fontName = { family: fontFamily, style: fontStyle };
|
|
1029
|
+
style.fontSize = fontSize;
|
|
1030
|
+
|
|
1031
|
+
if (lineHeight != null) {
|
|
1032
|
+
style.lineHeight = typeof lineHeight === "number"
|
|
1033
|
+
? { unit: "PIXELS", value: lineHeight }
|
|
1034
|
+
: lineHeight;
|
|
1035
|
+
}
|
|
1036
|
+
if (letterSpacing != null) {
|
|
1037
|
+
style.letterSpacing = typeof letterSpacing === "number"
|
|
1038
|
+
? { unit: "PIXELS", value: letterSpacing }
|
|
1039
|
+
: letterSpacing;
|
|
1040
|
+
}
|
|
1041
|
+
if (textCase) style.textCase = textCase;
|
|
1042
|
+
if (textDecoration) style.textDecoration = textDecoration;
|
|
1043
|
+
|
|
1044
|
+
return {
|
|
1045
|
+
styleId: style.id,
|
|
1046
|
+
name: style.name,
|
|
1047
|
+
fontFamily: style.fontName.family,
|
|
1048
|
+
fontStyle: style.fontName.style,
|
|
1049
|
+
fontSize: style.fontSize
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
function createEffectStyle({ name, effects }) {
|
|
1054
|
+
const style = figma.createEffectStyle();
|
|
1055
|
+
style.name = name;
|
|
1056
|
+
|
|
1057
|
+
// Convert effects array to Figma format
|
|
1058
|
+
if (effects && Array.isArray(effects)) {
|
|
1059
|
+
style.effects = effects.map(eff => {
|
|
1060
|
+
if (eff.type === "DROP_SHADOW" || eff.type === "INNER_SHADOW") {
|
|
1061
|
+
const rgb = eff.hex ? hexToRGB(eff.hex) : { r: 0, g: 0, b: 0 };
|
|
1062
|
+
return {
|
|
1063
|
+
type: eff.type,
|
|
1064
|
+
radius: eff.radius || 8,
|
|
1065
|
+
spread: eff.spread || 0,
|
|
1066
|
+
color: { r: rgb.r, g: rgb.g, b: rgb.b, a: eff.opacity || 0.25 },
|
|
1067
|
+
offset: { x: eff.offsetX || 0, y: eff.offsetY || 2 },
|
|
1068
|
+
visible: true,
|
|
1069
|
+
blendMode: "NORMAL"
|
|
1070
|
+
};
|
|
1071
|
+
} else if (eff.type === "LAYER_BLUR" || eff.type === "BACKGROUND_BLUR") {
|
|
1072
|
+
return {
|
|
1073
|
+
type: eff.type,
|
|
1074
|
+
radius: eff.radius || 8,
|
|
1075
|
+
visible: true
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
return eff;
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
return {
|
|
1083
|
+
styleId: style.id,
|
|
1084
|
+
name: style.name,
|
|
1085
|
+
effectCount: style.effects.length
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
function getLocalTextStyles() {
|
|
1090
|
+
const styles = figma.getLocalTextStyles();
|
|
1091
|
+
return styles.map(s => ({
|
|
1092
|
+
styleId: s.id,
|
|
1093
|
+
name: s.name,
|
|
1094
|
+
fontFamily: s.fontName.family,
|
|
1095
|
+
fontStyle: s.fontName.style,
|
|
1096
|
+
fontSize: s.fontSize,
|
|
1097
|
+
lineHeight: s.lineHeight,
|
|
1098
|
+
letterSpacing: s.letterSpacing
|
|
1099
|
+
}));
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
function getLocalEffectStyles() {
|
|
1103
|
+
const styles = figma.getLocalEffectStyles();
|
|
1104
|
+
return styles.map(s => ({
|
|
1105
|
+
styleId: s.id,
|
|
1106
|
+
name: s.name,
|
|
1107
|
+
effects: s.effects.map(e => ({
|
|
1108
|
+
type: e.type,
|
|
1109
|
+
radius: e.radius,
|
|
1110
|
+
spread: "spread" in e ? e.spread : undefined,
|
|
1111
|
+
offsetX: "offset" in e ? e.offset.x : undefined,
|
|
1112
|
+
offsetY: "offset" in e ? e.offset.y : undefined,
|
|
1113
|
+
opacity: "color" in e ? e.color.a : undefined,
|
|
1114
|
+
hex: "color" in e ? rgbToHex(e.color) : undefined
|
|
1115
|
+
}))
|
|
1116
|
+
}));
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
async function applyTextStyle({ nodeId, styleId }) {
|
|
1120
|
+
const node = getNode(nodeId);
|
|
1121
|
+
if (node.type !== "TEXT") throw new Error("Not a text node");
|
|
1122
|
+
|
|
1123
|
+
const style = figma.getStyleById(styleId);
|
|
1124
|
+
if (!style || style.type !== "TEXT") throw new Error("Text style not found: " + styleId);
|
|
1125
|
+
|
|
1126
|
+
// Load font before applying
|
|
1127
|
+
await figma.loadFontAsync(style.fontName);
|
|
1128
|
+
node.textStyleId = styleId;
|
|
1129
|
+
|
|
1130
|
+
return { nodeId, styleId };
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
function applyEffectStyle({ nodeId, styleId }) {
|
|
1134
|
+
const node = getNode(nodeId);
|
|
1135
|
+
if (!("effectStyleId" in node)) throw new Error("Node does not support effect styles");
|
|
1136
|
+
|
|
1137
|
+
const style = figma.getStyleById(styleId);
|
|
1138
|
+
if (!style || style.type !== "EFFECT") throw new Error("Effect style not found: " + styleId);
|
|
1139
|
+
|
|
1140
|
+
node.effectStyleId = styleId;
|
|
1141
|
+
return { nodeId, styleId };
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
async function updateTextStyle({ styleId, name, fontFamily, fontStyle, fontSize, lineHeight, letterSpacing }) {
|
|
1145
|
+
const style = figma.getStyleById(styleId);
|
|
1146
|
+
if (!style || style.type !== "TEXT") throw new Error("Text style not found: " + styleId);
|
|
1147
|
+
|
|
1148
|
+
if (name) style.name = name;
|
|
1149
|
+
if (fontFamily || fontStyle) {
|
|
1150
|
+
const fam = fontFamily || style.fontName.family;
|
|
1151
|
+
const sty = fontStyle || style.fontName.style;
|
|
1152
|
+
await figma.loadFontAsync({ family: fam, style: sty });
|
|
1153
|
+
style.fontName = { family: fam, style: sty };
|
|
1154
|
+
}
|
|
1155
|
+
if (fontSize != null) style.fontSize = fontSize;
|
|
1156
|
+
if (lineHeight != null) {
|
|
1157
|
+
style.lineHeight = typeof lineHeight === "number"
|
|
1158
|
+
? { unit: "PIXELS", value: lineHeight }
|
|
1159
|
+
: lineHeight;
|
|
1160
|
+
}
|
|
1161
|
+
if (letterSpacing != null) {
|
|
1162
|
+
style.letterSpacing = typeof letterSpacing === "number"
|
|
1163
|
+
? { unit: "PIXELS", value: letterSpacing }
|
|
1164
|
+
: letterSpacing;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
return { styleId, name: style.name };
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
function updateEffectStyle({ styleId, name, effects }) {
|
|
1171
|
+
const style = figma.getStyleById(styleId);
|
|
1172
|
+
if (!style || style.type !== "EFFECT") throw new Error("Effect style not found: " + styleId);
|
|
1173
|
+
|
|
1174
|
+
if (name) style.name = name;
|
|
1175
|
+
if (effects && Array.isArray(effects)) {
|
|
1176
|
+
style.effects = effects.map(eff => {
|
|
1177
|
+
if (eff.type === "DROP_SHADOW" || eff.type === "INNER_SHADOW") {
|
|
1178
|
+
const rgb = eff.hex ? hexToRGB(eff.hex) : { r: 0, g: 0, b: 0 };
|
|
1179
|
+
return {
|
|
1180
|
+
type: eff.type,
|
|
1181
|
+
radius: eff.radius || 8,
|
|
1182
|
+
spread: eff.spread || 0,
|
|
1183
|
+
color: { r: rgb.r, g: rgb.g, b: rgb.b, a: eff.opacity || 0.25 },
|
|
1184
|
+
offset: { x: eff.offsetX || 0, y: eff.offsetY || 2 },
|
|
1185
|
+
visible: true,
|
|
1186
|
+
blendMode: "NORMAL"
|
|
1187
|
+
};
|
|
1188
|
+
} else if (eff.type === "LAYER_BLUR" || eff.type === "BACKGROUND_BLUR") {
|
|
1189
|
+
return {
|
|
1190
|
+
type: eff.type,
|
|
1191
|
+
radius: eff.radius || 8,
|
|
1192
|
+
visible: true
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1195
|
+
return eff;
|
|
1196
|
+
});
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
return { styleId, name: style.name, effectCount: style.effects.length };
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
function deleteStyle({ styleId }) {
|
|
1203
|
+
const style = figma.getStyleById(styleId);
|
|
1204
|
+
if (!style) throw new Error("Style not found: " + styleId);
|
|
1205
|
+
style.remove();
|
|
1206
|
+
return { deleted: styleId };
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
// ========== ENHANCED COMPONENTS ==========
|
|
1210
|
+
|
|
1211
|
+
function createComponentFromNode({ nodeId, name }) {
|
|
1212
|
+
const node = getNode(nodeId);
|
|
1213
|
+
if (!("type" in node)) throw new Error("Invalid node");
|
|
1214
|
+
|
|
1215
|
+
// Create component and move node's children/properties into it
|
|
1216
|
+
const component = figma.createComponentFromNode(node);
|
|
1217
|
+
if (name) component.name = name;
|
|
1218
|
+
|
|
1219
|
+
return {
|
|
1220
|
+
componentId: component.id,
|
|
1221
|
+
name: component.name,
|
|
1222
|
+
type: component.type
|
|
1223
|
+
};
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
function createComponentSet({ componentIds, name = "Component Set" }) {
|
|
1227
|
+
// Get all components
|
|
1228
|
+
const components = componentIds.map(id => {
|
|
1229
|
+
const node = getNode(id);
|
|
1230
|
+
if (node.type !== "COMPONENT") throw new Error("Node is not a component: " + id);
|
|
1231
|
+
return node;
|
|
1232
|
+
});
|
|
1233
|
+
|
|
1234
|
+
if (components.length < 1) throw new Error("Need at least 1 component");
|
|
1235
|
+
|
|
1236
|
+
// Combine into component set
|
|
1237
|
+
const componentSet = figma.combineAsVariants(components, page());
|
|
1238
|
+
componentSet.name = name;
|
|
1239
|
+
|
|
1240
|
+
return {
|
|
1241
|
+
componentSetId: componentSet.id,
|
|
1242
|
+
name: componentSet.name,
|
|
1243
|
+
type: componentSet.type,
|
|
1244
|
+
variantCount: componentSet.children.length
|
|
1245
|
+
};
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
function addComponentProperty({ componentId, propertyName, propertyType, defaultValue, preferredValues }) {
|
|
1249
|
+
const component = getNode(componentId);
|
|
1250
|
+
if (component.type !== "COMPONENT" && component.type !== "COMPONENT_SET") {
|
|
1251
|
+
throw new Error("Node is not a component or component set");
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
// propertyType: "BOOLEAN" | "TEXT" | "INSTANCE_SWAP" | "VARIANT"
|
|
1255
|
+
const propDef = {
|
|
1256
|
+
type: propertyType,
|
|
1257
|
+
defaultValue: defaultValue
|
|
1258
|
+
};
|
|
1259
|
+
|
|
1260
|
+
if (preferredValues) {
|
|
1261
|
+
propDef.preferredValues = preferredValues;
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
component.addComponentProperty(propertyName, propDef.type, propDef.defaultValue);
|
|
1265
|
+
|
|
1266
|
+
return {
|
|
1267
|
+
componentId,
|
|
1268
|
+
propertyName,
|
|
1269
|
+
propertyType
|
|
1270
|
+
};
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
function setInstanceProperty({ instanceId, propertyName, value }) {
|
|
1274
|
+
const instance = getNode(instanceId);
|
|
1275
|
+
if (instance.type !== "INSTANCE") throw new Error("Node is not an instance");
|
|
1276
|
+
|
|
1277
|
+
// Get component properties
|
|
1278
|
+
const props = instance.componentProperties;
|
|
1279
|
+
if (!props || !(propertyName in props)) {
|
|
1280
|
+
throw new Error("Property not found: " + propertyName);
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
instance.setProperties({ [propertyName]: value });
|
|
1284
|
+
|
|
1285
|
+
return { instanceId, propertyName, value };
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
function getComponentProperties({ componentId }) {
|
|
1289
|
+
const component = getNode(componentId);
|
|
1290
|
+
if (component.type !== "COMPONENT" && component.type !== "COMPONENT_SET" && component.type !== "INSTANCE") {
|
|
1291
|
+
throw new Error("Node is not a component, component set, or instance");
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
const definitions = component.componentPropertyDefinitions || {};
|
|
1295
|
+
const values = component.componentProperties || {};
|
|
1296
|
+
|
|
1297
|
+
return {
|
|
1298
|
+
componentId,
|
|
1299
|
+
definitions: Object.entries(definitions).map(([name, def]) => ({
|
|
1300
|
+
name,
|
|
1301
|
+
type: def.type,
|
|
1302
|
+
defaultValue: def.defaultValue,
|
|
1303
|
+
variantOptions: def.variantOptions
|
|
1304
|
+
})),
|
|
1305
|
+
values: Object.entries(values).map(([name, val]) => ({
|
|
1306
|
+
name,
|
|
1307
|
+
value: val.value,
|
|
1308
|
+
type: val.type
|
|
1309
|
+
}))
|
|
1310
|
+
};
|
|
1311
|
+
}
|