figma-prototype-mcp 0.30.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 +25 -0
- package/README.md +169 -0
- package/dist/figma-plugin/code.js +1203 -0
- package/dist/figma-plugin/manifest.json +13 -0
- package/dist/figma-plugin/ui.html +123 -0
- package/dist/server/index.js +1155 -0
- package/package.json +61 -0
|
@@ -0,0 +1,1203 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
(() => {
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __defProps = Object.defineProperties;
|
|
5
|
+
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
|
|
6
|
+
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
9
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
10
|
+
var __spreadValues = (a, b) => {
|
|
11
|
+
for (var prop in b || (b = {}))
|
|
12
|
+
if (__hasOwnProp.call(b, prop))
|
|
13
|
+
__defNormalProp(a, prop, b[prop]);
|
|
14
|
+
if (__getOwnPropSymbols)
|
|
15
|
+
for (var prop of __getOwnPropSymbols(b)) {
|
|
16
|
+
if (__propIsEnum.call(b, prop))
|
|
17
|
+
__defNormalProp(a, prop, b[prop]);
|
|
18
|
+
}
|
|
19
|
+
return a;
|
|
20
|
+
};
|
|
21
|
+
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
|
|
22
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
23
|
+
|
|
24
|
+
// src/figma-plugin/reaction-builder.ts
|
|
25
|
+
function resolveEasing(input) {
|
|
26
|
+
if (input === void 0) return { type: "EASE_OUT" };
|
|
27
|
+
if (typeof input === "string") return { type: input };
|
|
28
|
+
if (input.type === "CUSTOM_CUBIC_BEZIER") {
|
|
29
|
+
return {
|
|
30
|
+
type: "CUSTOM_CUBIC_BEZIER",
|
|
31
|
+
easingFunctionCubicBezier: { x1: input.x1, y1: input.y1, x2: input.x2, y2: input.y2 }
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
type: "CUSTOM_SPRING",
|
|
36
|
+
easingFunctionSpring: {
|
|
37
|
+
mass: input.mass,
|
|
38
|
+
stiffness: input.stiffness,
|
|
39
|
+
damping: input.damping
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function buildTransition(input) {
|
|
44
|
+
var _a, _b, _c;
|
|
45
|
+
if (input === "INSTANT") return null;
|
|
46
|
+
if (input === "DISSOLVE" || input === "SMART_ANIMATE") {
|
|
47
|
+
return {
|
|
48
|
+
type: input,
|
|
49
|
+
duration: 0.3,
|
|
50
|
+
easing: { type: "EASE_OUT" }
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
if (input.type === "MOVE_IN" || input.type === "MOVE_OUT" || input.type === "PUSH" || input.type === "SLIDE_IN" || input.type === "SLIDE_OUT") {
|
|
54
|
+
return {
|
|
55
|
+
type: input.type,
|
|
56
|
+
direction: input.direction,
|
|
57
|
+
matchLayers: (_a = input.matchLayers) != null ? _a : false,
|
|
58
|
+
duration: (_b = input.duration) != null ? _b : 0.3,
|
|
59
|
+
easing: resolveEasing(input.easing)
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
type: input.type,
|
|
64
|
+
duration: (_c = input.duration) != null ? _c : 0.3,
|
|
65
|
+
easing: resolveEasing(input.easing)
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function isSmartAnimate(input) {
|
|
69
|
+
if (input === "SMART_ANIMATE") return true;
|
|
70
|
+
return typeof input !== "string" && input.type === "SMART_ANIMATE";
|
|
71
|
+
}
|
|
72
|
+
function degradeTransition(input, degradeTo) {
|
|
73
|
+
if (!isSmartAnimate(input)) return input;
|
|
74
|
+
if (degradeTo === "INSTANT") return "INSTANT";
|
|
75
|
+
if (typeof input === "string") return "DISSOLVE";
|
|
76
|
+
const obj = input;
|
|
77
|
+
return { type: "DISSOLVE", duration: obj.duration, easing: obj.easing };
|
|
78
|
+
}
|
|
79
|
+
function buildTrigger(input, legacyAfterTimeoutSeconds) {
|
|
80
|
+
var _a, _b;
|
|
81
|
+
if (typeof input === "string") {
|
|
82
|
+
if (input === "AFTER_TIMEOUT") {
|
|
83
|
+
if (legacyAfterTimeoutSeconds === void 0) {
|
|
84
|
+
throw new Error("buildTrigger: afterTimeoutSeconds is required when name is AFTER_TIMEOUT");
|
|
85
|
+
}
|
|
86
|
+
return { type: "AFTER_TIMEOUT", timeout: legacyAfterTimeoutSeconds };
|
|
87
|
+
}
|
|
88
|
+
return { type: input };
|
|
89
|
+
}
|
|
90
|
+
switch (input.type) {
|
|
91
|
+
case "ON_CLICK":
|
|
92
|
+
case "ON_HOVER":
|
|
93
|
+
case "ON_PRESS":
|
|
94
|
+
case "ON_DRAG":
|
|
95
|
+
case "ON_MEDIA_END":
|
|
96
|
+
return { type: input.type };
|
|
97
|
+
case "AFTER_TIMEOUT":
|
|
98
|
+
return { type: "AFTER_TIMEOUT", timeout: input.timeout };
|
|
99
|
+
case "MOUSE_UP":
|
|
100
|
+
case "MOUSE_DOWN":
|
|
101
|
+
return { type: input.type, delay: (_a = input.delay) != null ? _a : 0 };
|
|
102
|
+
case "MOUSE_ENTER":
|
|
103
|
+
case "MOUSE_LEAVE":
|
|
104
|
+
return {
|
|
105
|
+
type: input.type,
|
|
106
|
+
delay: (_b = input.delay) != null ? _b : 0
|
|
107
|
+
};
|
|
108
|
+
case "ON_KEY_DOWN":
|
|
109
|
+
return { type: "ON_KEY_DOWN", device: input.device, keyCodes: input.keyCodes };
|
|
110
|
+
case "ON_MEDIA_HIT":
|
|
111
|
+
return { type: "ON_MEDIA_HIT", mediaHitTime: input.mediaHitTime };
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function buildNavigateReaction(input) {
|
|
115
|
+
const action = __spreadValues({
|
|
116
|
+
type: "NODE",
|
|
117
|
+
destinationId: input.targetFrameId,
|
|
118
|
+
navigation: "NAVIGATE",
|
|
119
|
+
transition: buildTransition(input.transition)
|
|
120
|
+
}, input.resetScrollPosition !== void 0 && { resetScrollPosition: input.resetScrollPosition });
|
|
121
|
+
return {
|
|
122
|
+
trigger: buildTrigger(input.trigger, input.afterTimeoutSeconds),
|
|
123
|
+
actions: [action]
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function buildChangeToReaction(input) {
|
|
127
|
+
const action = {
|
|
128
|
+
type: "NODE",
|
|
129
|
+
destinationId: input.targetVariantId,
|
|
130
|
+
navigation: "CHANGE_TO",
|
|
131
|
+
transition: buildTransition(input.transition)
|
|
132
|
+
};
|
|
133
|
+
return {
|
|
134
|
+
trigger: buildTrigger(input.trigger, input.afterTimeoutSeconds),
|
|
135
|
+
actions: [action]
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function changeToCurrentVariantError(currentVariantId, targetVariantId) {
|
|
139
|
+
if (currentVariantId !== null && currentVariantId === targetVariantId) {
|
|
140
|
+
return `Change-to target ${targetVariantId} is already the instance's current variant; nothing would change`;
|
|
141
|
+
}
|
|
142
|
+
return void 0;
|
|
143
|
+
}
|
|
144
|
+
function buildScrollReaction(input) {
|
|
145
|
+
const action = __spreadValues({
|
|
146
|
+
type: "NODE",
|
|
147
|
+
destinationId: input.targetNodeId,
|
|
148
|
+
navigation: "SCROLL_TO",
|
|
149
|
+
transition: buildTransition(input.transition)
|
|
150
|
+
}, input.resetScrollPosition !== void 0 && { resetScrollPosition: input.resetScrollPosition });
|
|
151
|
+
return {
|
|
152
|
+
trigger: buildTrigger(input.trigger, input.afterTimeoutSeconds),
|
|
153
|
+
actions: [action]
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
function buildOverlayReaction(input) {
|
|
157
|
+
const action = __spreadValues({
|
|
158
|
+
type: "NODE",
|
|
159
|
+
destinationId: input.targetFrameId,
|
|
160
|
+
navigation: "OVERLAY",
|
|
161
|
+
transition: buildTransition(input.transition)
|
|
162
|
+
}, input.resetScrollPosition !== void 0 && { resetScrollPosition: input.resetScrollPosition });
|
|
163
|
+
return {
|
|
164
|
+
trigger: buildTrigger(input.trigger, input.afterTimeoutSeconds),
|
|
165
|
+
actions: [action]
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
function buildCloseReaction(input) {
|
|
169
|
+
return {
|
|
170
|
+
trigger: buildTrigger(input.trigger, input.afterTimeoutSeconds),
|
|
171
|
+
actions: [{ type: "CLOSE" }]
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
function buildBackReaction(input) {
|
|
175
|
+
return {
|
|
176
|
+
trigger: buildTrigger(input.trigger, input.afterTimeoutSeconds),
|
|
177
|
+
actions: [{ type: "BACK" }]
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
function buildUrlReaction(input) {
|
|
181
|
+
var _a;
|
|
182
|
+
return {
|
|
183
|
+
trigger: buildTrigger(input.trigger, input.afterTimeoutSeconds),
|
|
184
|
+
actions: [{ type: "URL", url: input.url, openInNewTab: (_a = input.openInNewTab) != null ? _a : false }]
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
function buildSwapOverlayReaction(input) {
|
|
188
|
+
const action = __spreadValues({
|
|
189
|
+
type: "NODE",
|
|
190
|
+
destinationId: input.targetFrameId,
|
|
191
|
+
navigation: "SWAP",
|
|
192
|
+
transition: buildTransition(input.transition)
|
|
193
|
+
}, input.resetScrollPosition !== void 0 && { resetScrollPosition: input.resetScrollPosition });
|
|
194
|
+
return {
|
|
195
|
+
trigger: buildTrigger(input.trigger, input.afterTimeoutSeconds),
|
|
196
|
+
actions: [action]
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
function buildConditionalReaction(input) {
|
|
200
|
+
const blocks = [
|
|
201
|
+
{ condition: input.condition, actions: input.thenActions }
|
|
202
|
+
];
|
|
203
|
+
if (input.elseActions && input.elseActions.length > 0) {
|
|
204
|
+
blocks.push({ actions: input.elseActions });
|
|
205
|
+
}
|
|
206
|
+
return {
|
|
207
|
+
trigger: buildTrigger(input.trigger, input.afterTimeoutSeconds),
|
|
208
|
+
actions: [{ type: "CONDITIONAL", conditionalBlocks: blocks }]
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// src/figma-plugin/command-queue.ts
|
|
213
|
+
var CommandQueue = class {
|
|
214
|
+
constructor() {
|
|
215
|
+
__publicField(this, "tail", Promise.resolve());
|
|
216
|
+
}
|
|
217
|
+
enqueue(fn) {
|
|
218
|
+
const result = this.tail.then(() => fn(), () => fn());
|
|
219
|
+
this.tail = result.then(
|
|
220
|
+
() => void 0,
|
|
221
|
+
() => void 0
|
|
222
|
+
);
|
|
223
|
+
return result;
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
// src/figma-plugin/variable-literal.ts
|
|
228
|
+
var HEX_REGEX = /^#[0-9a-fA-F]{6}([0-9a-fA-F]{2})?$/;
|
|
229
|
+
function hexToRgb(hex) {
|
|
230
|
+
const clean = hex.slice(1);
|
|
231
|
+
const r = parseInt(clean.slice(0, 2), 16) / 255;
|
|
232
|
+
const g = parseInt(clean.slice(2, 4), 16) / 255;
|
|
233
|
+
const b = parseInt(clean.slice(4, 6), 16) / 255;
|
|
234
|
+
const a = clean.length === 8 ? parseInt(clean.slice(6, 8), 16) / 255 : 1;
|
|
235
|
+
return { r, g, b, a };
|
|
236
|
+
}
|
|
237
|
+
function rgbToHex(rgb) {
|
|
238
|
+
const toHex = (n) => Math.max(0, Math.min(255, Math.round(n * 255))).toString(16).padStart(2, "0").toUpperCase();
|
|
239
|
+
const base = `#${toHex(rgb.r)}${toHex(rgb.g)}${toHex(rgb.b)}`;
|
|
240
|
+
if (rgb.a === void 0 || Math.round(rgb.a * 255) === 255) return base;
|
|
241
|
+
return base + toHex(rgb.a);
|
|
242
|
+
}
|
|
243
|
+
function validateVariableLiteralCompat(variable, value, context) {
|
|
244
|
+
const valueType = typeof value;
|
|
245
|
+
if (variable.resolvedType === "COLOR") {
|
|
246
|
+
if (context === "comparison") {
|
|
247
|
+
throw new Error(`Variable "${variable.name}" is COLOR; conditional comparison against COLOR variables is not supported in v1.18 (use BOOLEAN/FLOAT/STRING for condition.value)`);
|
|
248
|
+
}
|
|
249
|
+
if (valueType !== "string") {
|
|
250
|
+
throw new Error(`Variable "${variable.name}" is COLOR; cannot assign ${valueType} literal (expected hex string)`);
|
|
251
|
+
}
|
|
252
|
+
if (!HEX_REGEX.test(value)) {
|
|
253
|
+
throw new Error(`Variable "${variable.name}" is COLOR; value must be a hex string like #RRGGBB or #RRGGBBAA (got "${value}")`);
|
|
254
|
+
}
|
|
255
|
+
return { type: "COLOR", resolvedType: "COLOR", value: hexToRgb(value) };
|
|
256
|
+
}
|
|
257
|
+
const expected = variable.resolvedType === "BOOLEAN" ? "boolean" : variable.resolvedType === "FLOAT" ? "number" : variable.resolvedType === "STRING" ? "string" : "unknown";
|
|
258
|
+
if (expected === "unknown") {
|
|
259
|
+
throw new Error(`Variable "${variable.name}" has unsupported type ${variable.resolvedType} for ${context} (supported: BOOLEAN, FLOAT, STRING, COLOR)`);
|
|
260
|
+
}
|
|
261
|
+
if (valueType !== expected) {
|
|
262
|
+
const action = context === "comparison" ? "compare against" : "assign";
|
|
263
|
+
throw new Error(`Variable "${variable.name}" is ${variable.resolvedType}; cannot ${action} ${valueType} literal (expected ${expected})`);
|
|
264
|
+
}
|
|
265
|
+
if (expected === "boolean") {
|
|
266
|
+
return { type: "BOOLEAN", resolvedType: "BOOLEAN", value };
|
|
267
|
+
}
|
|
268
|
+
if (expected === "number") {
|
|
269
|
+
return { type: "FLOAT", resolvedType: "FLOAT", value };
|
|
270
|
+
}
|
|
271
|
+
return { type: "STRING", resolvedType: "STRING", value };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// src/figma-plugin/variable-catalog.ts
|
|
275
|
+
function filterVariables(items, filters) {
|
|
276
|
+
var _a;
|
|
277
|
+
const q = (_a = filters.nameQuery) == null ? void 0 : _a.toLowerCase();
|
|
278
|
+
return items.filter((v) => {
|
|
279
|
+
if (filters.resolvedType && v.resolvedType !== filters.resolvedType) return false;
|
|
280
|
+
if (q && !v.name.toLowerCase().includes(q)) return false;
|
|
281
|
+
return true;
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
function formatVariableNotFoundError(name, localNames, libraryNames) {
|
|
285
|
+
const render = (names) => names.length ? `[${names.join(", ")}]` : "(none)";
|
|
286
|
+
return `Variable "${name}" not found. Available \u2014 local: ${render(localNames)}; library: ${render(libraryNames)}. Use list_variables to inspect.`;
|
|
287
|
+
}
|
|
288
|
+
function selectVariableMatch(name, collection, candidates) {
|
|
289
|
+
const byName = candidates.filter((c) => c.name === name);
|
|
290
|
+
if (byName.length === 0) return { kind: "none" };
|
|
291
|
+
const pool = collection === void 0 ? byName : byName.filter((c) => c.collection === collection);
|
|
292
|
+
if (pool.length === 0) return { kind: "none" };
|
|
293
|
+
if (pool.length === 1) return { kind: "match", item: pool[0] };
|
|
294
|
+
return { kind: "ambiguous", collections: pool.map((c) => c.collection) };
|
|
295
|
+
}
|
|
296
|
+
function formatAmbiguousVariableError(name, collections, scope) {
|
|
297
|
+
return `Variable "${name}" is ambiguous \u2014 it exists in multiple ${scope} collections: [${collections.join(", ")}]. Specify the \`collection\` field to disambiguate (use list_variables to see collection names).`;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// src/figma-plugin/node-tree.ts
|
|
301
|
+
function findEnclosingFrameId(node) {
|
|
302
|
+
let cur = node.parent;
|
|
303
|
+
while (cur) {
|
|
304
|
+
if (cur.type === "FRAME") return cur.id;
|
|
305
|
+
cur = cur.parent;
|
|
306
|
+
}
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
function hasReactions(node) {
|
|
310
|
+
return Array.isArray(node.reactions) && node.reactions.length > 0;
|
|
311
|
+
}
|
|
312
|
+
function findScrollableAncestor(node) {
|
|
313
|
+
let cur = node.parent;
|
|
314
|
+
while (cur) {
|
|
315
|
+
if (cur.overflowDirection !== void 0 && cur.overflowDirection !== "NONE") {
|
|
316
|
+
return cur;
|
|
317
|
+
}
|
|
318
|
+
cur = cur.parent;
|
|
319
|
+
}
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
function pathOf(node) {
|
|
323
|
+
const parts = [];
|
|
324
|
+
let cur = node;
|
|
325
|
+
while (cur && cur.type !== "DOCUMENT") {
|
|
326
|
+
parts.unshift(cur.name);
|
|
327
|
+
cur = cur.parent;
|
|
328
|
+
}
|
|
329
|
+
return parts.join(" > ");
|
|
330
|
+
}
|
|
331
|
+
function findTopLevelFrameNode(node) {
|
|
332
|
+
let cur = node;
|
|
333
|
+
let top = null;
|
|
334
|
+
while (cur) {
|
|
335
|
+
if (cur.type === "FRAME") {
|
|
336
|
+
const p = cur.parent;
|
|
337
|
+
if (!p || p.type === "PAGE" || p.type === "SECTION") top = cur;
|
|
338
|
+
}
|
|
339
|
+
cur = cur.parent;
|
|
340
|
+
}
|
|
341
|
+
return top;
|
|
342
|
+
}
|
|
343
|
+
function collectDescendantLayerPaths(node) {
|
|
344
|
+
const paths = /* @__PURE__ */ new Set();
|
|
345
|
+
const visit = (n, prefix) => {
|
|
346
|
+
var _a;
|
|
347
|
+
for (const child of (_a = n.children) != null ? _a : []) {
|
|
348
|
+
const path = prefix ? `${prefix}/${child.name}` : child.name;
|
|
349
|
+
paths.add(path);
|
|
350
|
+
visit(child, path);
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
visit(node, "");
|
|
354
|
+
return paths;
|
|
355
|
+
}
|
|
356
|
+
function framesShareLayer(a, b) {
|
|
357
|
+
const pathsA = collectDescendantLayerPaths(a);
|
|
358
|
+
for (const path of collectDescendantLayerPaths(b)) {
|
|
359
|
+
if (pathsA.has(path)) return true;
|
|
360
|
+
}
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// src/figma-plugin/condition-codec.ts
|
|
365
|
+
var COMPARISON_OPERATOR_MAP = {
|
|
366
|
+
"==": "EQUALS",
|
|
367
|
+
"!=": "NOT_EQUAL",
|
|
368
|
+
"<": "LESS_THAN",
|
|
369
|
+
"<=": "LESS_THAN_OR_EQUAL",
|
|
370
|
+
">": "GREATER_THAN",
|
|
371
|
+
">=": "GREATER_THAN_OR_EQUAL"
|
|
372
|
+
};
|
|
373
|
+
var OPERATOR_INVERSE = Object.fromEntries(
|
|
374
|
+
Object.entries(COMPARISON_OPERATOR_MAP).map(([k, v]) => [v, k])
|
|
375
|
+
);
|
|
376
|
+
function buildConditionExpression(input) {
|
|
377
|
+
return {
|
|
378
|
+
type: "EXPRESSION",
|
|
379
|
+
resolvedType: "BOOLEAN",
|
|
380
|
+
value: {
|
|
381
|
+
expressionFunction: COMPARISON_OPERATOR_MAP[input.operator],
|
|
382
|
+
expressionArguments: [
|
|
383
|
+
{
|
|
384
|
+
type: "VARIABLE_ALIAS",
|
|
385
|
+
resolvedType: input.resolvedType,
|
|
386
|
+
value: { type: "VARIABLE_ALIAS", id: input.variableId }
|
|
387
|
+
},
|
|
388
|
+
input.literal
|
|
389
|
+
]
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
function buildCompoundConditionExpression(input) {
|
|
394
|
+
return {
|
|
395
|
+
type: "EXPRESSION",
|
|
396
|
+
resolvedType: "BOOLEAN",
|
|
397
|
+
value: {
|
|
398
|
+
expressionFunction: input.join,
|
|
399
|
+
expressionArguments: input.operands
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
function decodeComparison(condition) {
|
|
404
|
+
var _a, _b;
|
|
405
|
+
if (!condition || condition.type !== "EXPRESSION" || !condition.value) return null;
|
|
406
|
+
const expr = condition.value;
|
|
407
|
+
const operator = OPERATOR_INVERSE[expr.expressionFunction];
|
|
408
|
+
if (!operator) return null;
|
|
409
|
+
const args = (_a = expr.expressionArguments) != null ? _a : [];
|
|
410
|
+
if (args.length !== 2) return null;
|
|
411
|
+
const aliasArg = args[0];
|
|
412
|
+
const literalArg = args[1];
|
|
413
|
+
if ((aliasArg == null ? void 0 : aliasArg.type) !== "VARIABLE_ALIAS") return null;
|
|
414
|
+
const variableId = (_b = aliasArg.value) == null ? void 0 : _b.id;
|
|
415
|
+
let value;
|
|
416
|
+
if ((literalArg == null ? void 0 : literalArg.type) === "BOOLEAN" || (literalArg == null ? void 0 : literalArg.type) === "FLOAT" || (literalArg == null ? void 0 : literalArg.type) === "STRING") {
|
|
417
|
+
value = literalArg.value;
|
|
418
|
+
}
|
|
419
|
+
return { variableId, operator, value };
|
|
420
|
+
}
|
|
421
|
+
var JOIN_INVERSE = { AND: "all", OR: "any" };
|
|
422
|
+
function decodeConditionExpression(condition) {
|
|
423
|
+
var _a, _b;
|
|
424
|
+
const join = (condition == null ? void 0 : condition.type) === "EXPRESSION" ? JOIN_INVERSE[(_a = condition.value) == null ? void 0 : _a.expressionFunction] : void 0;
|
|
425
|
+
if (join) {
|
|
426
|
+
const operands = (_b = condition.value.expressionArguments) != null ? _b : [];
|
|
427
|
+
const conditions = [];
|
|
428
|
+
for (const op of operands) {
|
|
429
|
+
const c = decodeComparison(op);
|
|
430
|
+
if (!c) return { raw: condition };
|
|
431
|
+
conditions.push(c);
|
|
432
|
+
}
|
|
433
|
+
return { join, conditions };
|
|
434
|
+
}
|
|
435
|
+
const single = decodeComparison(condition);
|
|
436
|
+
return single != null ? single : { raw: condition };
|
|
437
|
+
}
|
|
438
|
+
function detectTogglePattern(blocks) {
|
|
439
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m;
|
|
440
|
+
if (!Array.isArray(blocks) || blocks.length !== 2) return null;
|
|
441
|
+
const [b0, b1] = blocks;
|
|
442
|
+
if ((b1 == null ? void 0 : b1.condition) !== void 0) return null;
|
|
443
|
+
const cond = b0 == null ? void 0 : b0.condition;
|
|
444
|
+
if (!cond || cond.type !== "EXPRESSION" || !cond.value) return null;
|
|
445
|
+
if (cond.value.expressionFunction !== "EQUALS") return null;
|
|
446
|
+
const args = cond.value.expressionArguments;
|
|
447
|
+
if (!Array.isArray(args) || args.length !== 2) return null;
|
|
448
|
+
const aliasArg = args[0], boolArg = args[1];
|
|
449
|
+
if ((aliasArg == null ? void 0 : aliasArg.type) !== "VARIABLE_ALIAS") return null;
|
|
450
|
+
if ((boolArg == null ? void 0 : boolArg.type) !== "BOOLEAN" || boolArg.value !== true) return null;
|
|
451
|
+
const varId = (_a = aliasArg == null ? void 0 : aliasArg.value) == null ? void 0 : _a.id;
|
|
452
|
+
if (!varId) return null;
|
|
453
|
+
const a0 = b0 == null ? void 0 : b0.actions;
|
|
454
|
+
if (!Array.isArray(a0) || a0.length !== 1) return null;
|
|
455
|
+
if (((_b = a0[0]) == null ? void 0 : _b.type) !== "SET_VARIABLE") return null;
|
|
456
|
+
if (((_c = a0[0]) == null ? void 0 : _c.variableId) !== varId) return null;
|
|
457
|
+
if (((_e = (_d = a0[0]) == null ? void 0 : _d.variableValue) == null ? void 0 : _e.type) !== "BOOLEAN" || ((_g = (_f = a0[0]) == null ? void 0 : _f.variableValue) == null ? void 0 : _g.value) !== false) return null;
|
|
458
|
+
const a1 = b1 == null ? void 0 : b1.actions;
|
|
459
|
+
if (!Array.isArray(a1) || a1.length !== 1) return null;
|
|
460
|
+
if (((_h = a1[0]) == null ? void 0 : _h.type) !== "SET_VARIABLE") return null;
|
|
461
|
+
if (((_i = a1[0]) == null ? void 0 : _i.variableId) !== varId) return null;
|
|
462
|
+
if (((_k = (_j = a1[0]) == null ? void 0 : _j.variableValue) == null ? void 0 : _k.type) !== "BOOLEAN" || ((_m = (_l = a1[0]) == null ? void 0 : _l.variableValue) == null ? void 0 : _m.value) !== true) return null;
|
|
463
|
+
return varId;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// src/figma-plugin/action-echo.ts
|
|
467
|
+
async function encodeActionForListEcho(action, resolvers) {
|
|
468
|
+
var _a, _b, _c;
|
|
469
|
+
if (!action || typeof action !== "object") return { type: "UNKNOWN" };
|
|
470
|
+
if (action.type === "CONDITIONAL") {
|
|
471
|
+
const blocks = Array.isArray(action.conditionalBlocks) ? action.conditionalBlocks : [];
|
|
472
|
+
const toggleVarId = detectTogglePattern(blocks);
|
|
473
|
+
if (toggleVarId) {
|
|
474
|
+
const varName = await resolvers.variableName(toggleVarId);
|
|
475
|
+
return { type: "toggle_variable", variable: varName != null ? varName : `<id:${toggleVarId}>` };
|
|
476
|
+
}
|
|
477
|
+
const standardPattern = blocks.length >= 1 && blocks.length <= 2 && blocks[0].condition !== void 0 && (blocks.length === 1 || blocks[1].condition === void 0);
|
|
478
|
+
if (!standardPattern) {
|
|
479
|
+
return { type: "CONDITIONAL", raw: blocks };
|
|
480
|
+
}
|
|
481
|
+
const decodedCondition = await decodeConditionForEcho(blocks[0].condition, resolvers);
|
|
482
|
+
const thenActions = await Promise.all(
|
|
483
|
+
((_a = blocks[0].actions) != null ? _a : []).map((a) => encodeActionForListEcho(a, resolvers))
|
|
484
|
+
);
|
|
485
|
+
const elseActions = blocks.length === 2 ? await Promise.all(((_b = blocks[1].actions) != null ? _b : []).map((a) => encodeActionForListEcho(a, resolvers))) : void 0;
|
|
486
|
+
return { type: "CONDITIONAL", condition: decodedCondition, then: thenActions, else: elseActions };
|
|
487
|
+
}
|
|
488
|
+
if (action.type === "SET_VARIABLE") {
|
|
489
|
+
let varName;
|
|
490
|
+
if (action.variableId) varName = await resolvers.variableName(action.variableId);
|
|
491
|
+
const vd = action.variableValue;
|
|
492
|
+
let value;
|
|
493
|
+
if ((vd == null ? void 0 : vd.type) === "COLOR" && (vd == null ? void 0 : vd.value) && typeof vd.value === "object" && "r" in vd.value && "g" in vd.value && "b" in vd.value) {
|
|
494
|
+
value = rgbToHex(vd.value);
|
|
495
|
+
} else {
|
|
496
|
+
value = vd == null ? void 0 : vd.value;
|
|
497
|
+
}
|
|
498
|
+
return { type: "set_variable", variable: varName != null ? varName : `<id:${action.variableId}>`, value };
|
|
499
|
+
}
|
|
500
|
+
const destId = action.destinationId;
|
|
501
|
+
const destName = destId ? resolvers.nodeName(destId) : void 0;
|
|
502
|
+
return {
|
|
503
|
+
type: (_c = action.type) != null ? _c : "UNKNOWN",
|
|
504
|
+
navigation: action.navigation,
|
|
505
|
+
url: action.url,
|
|
506
|
+
openInNewTab: action.openInNewTab,
|
|
507
|
+
destinationId: destId,
|
|
508
|
+
destinationName: destName,
|
|
509
|
+
transition: action.transition,
|
|
510
|
+
resetScrollPosition: action.resetScrollPosition
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
async function decodeConditionForEcho(condition, resolvers) {
|
|
514
|
+
const decoded = decodeConditionExpression(condition);
|
|
515
|
+
if ("raw" in decoded) return { raw: decoded.raw };
|
|
516
|
+
const echoLeaf = async (c) => {
|
|
517
|
+
const name = c.variableId ? await resolvers.variableName(c.variableId) : void 0;
|
|
518
|
+
return { variable: name != null ? name : `<id:${c.variableId}>`, operator: c.operator, value: c.value };
|
|
519
|
+
};
|
|
520
|
+
if ("join" in decoded) {
|
|
521
|
+
const conditions = [];
|
|
522
|
+
for (const c of decoded.conditions) conditions.push(await echoLeaf(c));
|
|
523
|
+
return decoded.join === "all" ? { all: conditions } : { any: conditions };
|
|
524
|
+
}
|
|
525
|
+
const leaf = await echoLeaf(decoded);
|
|
526
|
+
return __spreadProps(__spreadValues({}, leaf), {
|
|
527
|
+
raw: leaf.variable.startsWith("<id:") ? condition : void 0
|
|
528
|
+
// keep raw if we lost the name
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// src/figma-plugin/motion-degrade.ts
|
|
533
|
+
function resolveNavigateTransition(args) {
|
|
534
|
+
const { source, destFrame, transition, degradeTo } = args;
|
|
535
|
+
if (!isSmartAnimate(transition)) return { transition };
|
|
536
|
+
const srcTop = findTopLevelFrameNode(source);
|
|
537
|
+
if (!srcTop) return { transition };
|
|
538
|
+
if (framesShareLayer(srcTop, destFrame)) return { transition };
|
|
539
|
+
const to = degradeTo != null ? degradeTo : "DISSOLVE";
|
|
540
|
+
return {
|
|
541
|
+
transition: degradeTransition(transition, to),
|
|
542
|
+
warning: `SMART_ANIMATE has no matching layers between "${srcTop.name}" and "${destFrame.name}"; degraded to ${to}`
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// src/figma-plugin/flow-graph.ts
|
|
547
|
+
function assembleFlowGraph(input) {
|
|
548
|
+
const nameById = new Map(input.frames.map((f) => [f.id, f.name]));
|
|
549
|
+
const limited = input.interactions.slice(0, input.limit);
|
|
550
|
+
const interactions = limited.map((i) => {
|
|
551
|
+
var _a;
|
|
552
|
+
return __spreadProps(__spreadValues({}, i), {
|
|
553
|
+
frameName: i.frameId !== null ? (_a = nameById.get(i.frameId)) != null ? _a : null : null
|
|
554
|
+
});
|
|
555
|
+
});
|
|
556
|
+
return {
|
|
557
|
+
page: input.page,
|
|
558
|
+
frames: input.frames,
|
|
559
|
+
interactions,
|
|
560
|
+
truncated: input.interactions.length > input.limit
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// src/figma-plugin/code.ts
|
|
565
|
+
figma.showUI(__html__, { width: 320, height: 220 });
|
|
566
|
+
var commandQueue = new CommandQueue();
|
|
567
|
+
figma.ui.onmessage = (msg) => {
|
|
568
|
+
if ((msg == null ? void 0 : msg.type) === "load-channel") {
|
|
569
|
+
figma.clientStorage.getAsync("channel").then((value) => {
|
|
570
|
+
figma.ui.postMessage({
|
|
571
|
+
type: "channel-loaded",
|
|
572
|
+
channel: typeof value === "string" && value.length > 0 ? value : null
|
|
573
|
+
});
|
|
574
|
+
});
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
if ((msg == null ? void 0 : msg.type) === "save-channel" && typeof msg.channel === "string" && msg.channel.length > 0) {
|
|
578
|
+
figma.clientStorage.setAsync("channel", msg.channel).catch(() => {
|
|
579
|
+
});
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
if ((msg == null ? void 0 : msg.type) === "command" && msg.envelope) {
|
|
583
|
+
const envelope = msg.envelope;
|
|
584
|
+
void commandQueue.enqueue(async () => {
|
|
585
|
+
const response = await dispatch(envelope.command, envelope.params);
|
|
586
|
+
figma.ui.postMessage({
|
|
587
|
+
type: "response",
|
|
588
|
+
envelope: __spreadValues({ id: envelope.id, type: "response" }, response)
|
|
589
|
+
});
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
};
|
|
593
|
+
async function dispatch(command, params) {
|
|
594
|
+
var _a;
|
|
595
|
+
try {
|
|
596
|
+
switch (command) {
|
|
597
|
+
case "GET_CANVAS_OVERVIEW":
|
|
598
|
+
return { status: "ok", result: await handleGetCanvasOverview(params) };
|
|
599
|
+
case "GET_PROTOTYPE_FLOW":
|
|
600
|
+
return { status: "ok", result: await handleGetPrototypeFlow(params) };
|
|
601
|
+
case "FIND_NODES":
|
|
602
|
+
return { status: "ok", result: await handleFindNodes(params) };
|
|
603
|
+
case "LIST_VARIABLES":
|
|
604
|
+
return { status: "ok", result: await handleListVariables(params) };
|
|
605
|
+
case "CREATE_REACTIONS":
|
|
606
|
+
return { status: "ok", result: await handleCreateReactions(params) };
|
|
607
|
+
case "LIST_REACTIONS":
|
|
608
|
+
return { status: "ok", result: await handleListReactions(params) };
|
|
609
|
+
case "CLEAR_REACTIONS":
|
|
610
|
+
return { status: "ok", result: await handleClearReactions(params) };
|
|
611
|
+
case "SET_FRAME_SCROLL":
|
|
612
|
+
return { status: "ok", result: await handleSetFrameScroll(params) };
|
|
613
|
+
default:
|
|
614
|
+
return { status: "error", error: { code: "UNKNOWN_COMMAND", message: `Unknown command: ${command}` } };
|
|
615
|
+
}
|
|
616
|
+
} catch (err) {
|
|
617
|
+
return { status: "error", error: { code: "PLUGIN_EXCEPTION", message: (_a = err == null ? void 0 : err.message) != null ? _a : String(err) } };
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
async function loadPage(pageId) {
|
|
621
|
+
if (!pageId) {
|
|
622
|
+
await figma.loadAllPagesAsync();
|
|
623
|
+
return figma.currentPage;
|
|
624
|
+
}
|
|
625
|
+
await figma.loadAllPagesAsync();
|
|
626
|
+
const page = figma.getNodeById(pageId);
|
|
627
|
+
if (!page || page.type !== "PAGE") throw new Error(`Page not found: ${pageId}`);
|
|
628
|
+
return page;
|
|
629
|
+
}
|
|
630
|
+
async function buildNonConditionalAction(action, trigger, afterTimeoutSeconds, transition, sourceNode, degradeTo) {
|
|
631
|
+
var _a;
|
|
632
|
+
if (action.type === "navigate") {
|
|
633
|
+
const target = figma.getNodeById(action.targetFrameId);
|
|
634
|
+
if (!target) throw new Error(`Target frame not found: ${action.targetFrameId}`);
|
|
635
|
+
if (target.type !== "FRAME") {
|
|
636
|
+
throw new Error(`Target must be a frame: ${action.targetFrameId} (got ${target.type})`);
|
|
637
|
+
}
|
|
638
|
+
const { transition: effectiveTransition, warning } = resolveNavigateTransition({
|
|
639
|
+
source: sourceNode,
|
|
640
|
+
destFrame: target,
|
|
641
|
+
transition,
|
|
642
|
+
degradeTo
|
|
643
|
+
});
|
|
644
|
+
const reaction = buildNavigateReaction({
|
|
645
|
+
targetFrameId: action.targetFrameId,
|
|
646
|
+
trigger,
|
|
647
|
+
afterTimeoutSeconds,
|
|
648
|
+
transition: effectiveTransition,
|
|
649
|
+
resetScrollPosition: action.resetScrollPosition
|
|
650
|
+
});
|
|
651
|
+
return { built: reaction.actions[0], warning };
|
|
652
|
+
}
|
|
653
|
+
if (action.type === "scroll") {
|
|
654
|
+
const target = figma.getNodeById(action.targetNodeId);
|
|
655
|
+
if (!target) throw new Error(`Scroll target node not found: ${action.targetNodeId}`);
|
|
656
|
+
const scrollable = findScrollableAncestor(target);
|
|
657
|
+
let warning;
|
|
658
|
+
if (!scrollable) {
|
|
659
|
+
warning = `Scroll target ${action.targetNodeId} (${target.name}) has no scrollable ancestor frame; the prototype scroll will not animate at runtime`;
|
|
660
|
+
}
|
|
661
|
+
const reaction = buildScrollReaction({
|
|
662
|
+
targetNodeId: action.targetNodeId,
|
|
663
|
+
trigger,
|
|
664
|
+
afterTimeoutSeconds,
|
|
665
|
+
transition,
|
|
666
|
+
resetScrollPosition: action.resetScrollPosition
|
|
667
|
+
});
|
|
668
|
+
return { built: reaction.actions[0], warning };
|
|
669
|
+
}
|
|
670
|
+
if (action.type === "overlay") {
|
|
671
|
+
const target = figma.getNodeById(action.targetFrameId);
|
|
672
|
+
if (!target) throw new Error(`Overlay target frame not found: ${action.targetFrameId}`);
|
|
673
|
+
if (target.type !== "FRAME") {
|
|
674
|
+
throw new Error(`Overlay target must be a frame: ${action.targetFrameId} (got ${target.type})`);
|
|
675
|
+
}
|
|
676
|
+
const reaction = buildOverlayReaction({
|
|
677
|
+
targetFrameId: action.targetFrameId,
|
|
678
|
+
trigger,
|
|
679
|
+
afterTimeoutSeconds,
|
|
680
|
+
transition,
|
|
681
|
+
resetScrollPosition: action.resetScrollPosition
|
|
682
|
+
});
|
|
683
|
+
return { built: reaction.actions[0] };
|
|
684
|
+
}
|
|
685
|
+
if (action.type === "close") {
|
|
686
|
+
const reaction = buildCloseReaction({ trigger, afterTimeoutSeconds });
|
|
687
|
+
return { built: reaction.actions[0] };
|
|
688
|
+
}
|
|
689
|
+
if (action.type === "back") {
|
|
690
|
+
const reaction = buildBackReaction({ trigger, afterTimeoutSeconds });
|
|
691
|
+
return { built: reaction.actions[0] };
|
|
692
|
+
}
|
|
693
|
+
if (action.type === "url") {
|
|
694
|
+
const reaction = buildUrlReaction({
|
|
695
|
+
trigger,
|
|
696
|
+
afterTimeoutSeconds,
|
|
697
|
+
url: action.url,
|
|
698
|
+
openInNewTab: action.openInNewTab
|
|
699
|
+
});
|
|
700
|
+
return { built: reaction.actions[0] };
|
|
701
|
+
}
|
|
702
|
+
if (action.type === "set_variable") {
|
|
703
|
+
const { variable, warning } = await resolveVariableByName(action.variable, action.collection);
|
|
704
|
+
const variableValue = buildSetVariableData(variable, action.value);
|
|
705
|
+
const built = {
|
|
706
|
+
type: "SET_VARIABLE",
|
|
707
|
+
variableId: variable.id,
|
|
708
|
+
variableValue
|
|
709
|
+
};
|
|
710
|
+
return { built, warning };
|
|
711
|
+
}
|
|
712
|
+
if (action.type === "swap_overlay") {
|
|
713
|
+
const target = figma.getNodeById(action.targetFrameId);
|
|
714
|
+
if (!target) throw new Error(`Swap overlay target frame not found: ${action.targetFrameId}`);
|
|
715
|
+
if (target.type !== "FRAME") {
|
|
716
|
+
throw new Error(`Swap overlay target must be a frame: ${action.targetFrameId} (got ${target.type})`);
|
|
717
|
+
}
|
|
718
|
+
const reaction = buildSwapOverlayReaction({
|
|
719
|
+
trigger,
|
|
720
|
+
afterTimeoutSeconds,
|
|
721
|
+
transition,
|
|
722
|
+
targetFrameId: action.targetFrameId,
|
|
723
|
+
resetScrollPosition: action.resetScrollPosition
|
|
724
|
+
});
|
|
725
|
+
return { built: reaction.actions[0] };
|
|
726
|
+
}
|
|
727
|
+
if (action.type === "change_to") {
|
|
728
|
+
const target = figma.getNodeById(action.targetVariantId);
|
|
729
|
+
if (!target) throw new Error(`Change-to target not found: ${action.targetVariantId}`);
|
|
730
|
+
if (target.type !== "COMPONENT") {
|
|
731
|
+
throw new Error(`Change-to target must be a component variant: ${action.targetVariantId} (got ${target.type})`);
|
|
732
|
+
}
|
|
733
|
+
let cur = sourceNode;
|
|
734
|
+
let instance = null;
|
|
735
|
+
while (cur) {
|
|
736
|
+
if (cur.type === "INSTANCE") {
|
|
737
|
+
instance = cur;
|
|
738
|
+
break;
|
|
739
|
+
}
|
|
740
|
+
cur = cur.parent;
|
|
741
|
+
}
|
|
742
|
+
const hasInstance = instance !== null;
|
|
743
|
+
if (instance) {
|
|
744
|
+
const mainComponent = await instance.getMainComponentAsync();
|
|
745
|
+
const sameVariantError = changeToCurrentVariantError(
|
|
746
|
+
(_a = mainComponent == null ? void 0 : mainComponent.id) != null ? _a : null,
|
|
747
|
+
action.targetVariantId
|
|
748
|
+
);
|
|
749
|
+
if (sameVariantError) throw new Error(sameVariantError);
|
|
750
|
+
}
|
|
751
|
+
const reaction = buildChangeToReaction({
|
|
752
|
+
targetVariantId: action.targetVariantId,
|
|
753
|
+
trigger,
|
|
754
|
+
afterTimeoutSeconds,
|
|
755
|
+
transition
|
|
756
|
+
});
|
|
757
|
+
const warning = hasInstance ? void 0 : `Change-to source ${sourceNode.id} is not (and is not inside) a component instance; the reaction will not animate at runtime`;
|
|
758
|
+
return { built: reaction.actions[0], warning };
|
|
759
|
+
}
|
|
760
|
+
throw new Error(`Unhandled action type: ${action.type}`);
|
|
761
|
+
}
|
|
762
|
+
async function resolveVariableByName(name, collection) {
|
|
763
|
+
var _a;
|
|
764
|
+
const all = await figma.variables.getLocalVariablesAsync();
|
|
765
|
+
const localDescriptors = await Promise.all(
|
|
766
|
+
all.map(async (v) => {
|
|
767
|
+
var _a2;
|
|
768
|
+
const col = await figma.variables.getVariableCollectionByIdAsync(v.variableCollectionId);
|
|
769
|
+
return {
|
|
770
|
+
name: v.name,
|
|
771
|
+
id: v.id,
|
|
772
|
+
resolvedType: v.resolvedType,
|
|
773
|
+
collection: (_a2 = col == null ? void 0 : col.name) != null ? _a2 : "",
|
|
774
|
+
ref: v
|
|
775
|
+
};
|
|
776
|
+
})
|
|
777
|
+
);
|
|
778
|
+
const localPick = selectVariableMatch(name, collection, localDescriptors);
|
|
779
|
+
if (localPick.kind === "match") {
|
|
780
|
+
return { variable: localPick.item.ref };
|
|
781
|
+
}
|
|
782
|
+
if (localPick.kind === "ambiguous") {
|
|
783
|
+
throw new Error(formatAmbiguousVariableError(name, localPick.collections, "local"));
|
|
784
|
+
}
|
|
785
|
+
const libraryDescriptors = [];
|
|
786
|
+
try {
|
|
787
|
+
const collections = await figma.teamLibrary.getAvailableLibraryVariableCollectionsAsync();
|
|
788
|
+
for (const col of collections) {
|
|
789
|
+
const vars = await figma.teamLibrary.getVariablesInLibraryCollectionAsync(col.key);
|
|
790
|
+
for (const v of vars) {
|
|
791
|
+
libraryDescriptors.push({
|
|
792
|
+
name: v.name,
|
|
793
|
+
key: v.key,
|
|
794
|
+
resolvedType: v.resolvedType,
|
|
795
|
+
collection: col.name,
|
|
796
|
+
libraryName: col.libraryName
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
} catch (e) {
|
|
801
|
+
}
|
|
802
|
+
const libPick = selectVariableMatch(name, collection, libraryDescriptors);
|
|
803
|
+
if (libPick.kind === "ambiguous") {
|
|
804
|
+
throw new Error(formatAmbiguousVariableError(name, libPick.collections, "library"));
|
|
805
|
+
}
|
|
806
|
+
if (libPick.kind === "match") {
|
|
807
|
+
try {
|
|
808
|
+
const imported = await figma.variables.importVariableByKeyAsync(libPick.item.key);
|
|
809
|
+
return {
|
|
810
|
+
variable: imported,
|
|
811
|
+
warning: `Imported library variable "${name}" from "${libPick.item.libraryName}".`
|
|
812
|
+
};
|
|
813
|
+
} catch (err) {
|
|
814
|
+
throw new Error(
|
|
815
|
+
`Found library variable "${name}" in "${libPick.item.libraryName}" but failed to import it: ${(_a = err == null ? void 0 : err.message) != null ? _a : String(err)}`
|
|
816
|
+
);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
throw new Error(
|
|
820
|
+
formatVariableNotFoundError(name, all.map((v) => v.name), libraryDescriptors.map((v) => v.name))
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
async function buildCondition(input) {
|
|
824
|
+
let firstWarning;
|
|
825
|
+
const buildLeaf = async (leaf) => {
|
|
826
|
+
const { variable, warning } = await resolveVariableByName(leaf.variable, leaf.collection);
|
|
827
|
+
if (warning && !firstWarning) firstWarning = warning;
|
|
828
|
+
const literalVD = validateVariableLiteralCompat(
|
|
829
|
+
{ name: variable.name, resolvedType: variable.resolvedType },
|
|
830
|
+
leaf.value,
|
|
831
|
+
"comparison"
|
|
832
|
+
);
|
|
833
|
+
return buildConditionExpression({
|
|
834
|
+
variableId: variable.id,
|
|
835
|
+
resolvedType: variable.resolvedType,
|
|
836
|
+
operator: leaf.operator,
|
|
837
|
+
literal: literalVD
|
|
838
|
+
});
|
|
839
|
+
};
|
|
840
|
+
if ("all" in input || "any" in input) {
|
|
841
|
+
const join = "all" in input ? "AND" : "OR";
|
|
842
|
+
const leaves = "all" in input ? input.all : input.any;
|
|
843
|
+
const operands = await Promise.all(leaves.map(buildLeaf));
|
|
844
|
+
return { condition: buildCompoundConditionExpression({ join, operands }), warning: firstWarning };
|
|
845
|
+
}
|
|
846
|
+
const condition = await buildLeaf(input);
|
|
847
|
+
return { condition, warning: firstWarning };
|
|
848
|
+
}
|
|
849
|
+
function buildSetVariableData(variable, value) {
|
|
850
|
+
return validateVariableLiteralCompat(
|
|
851
|
+
{ name: variable.name, resolvedType: variable.resolvedType },
|
|
852
|
+
value,
|
|
853
|
+
"assignment"
|
|
854
|
+
);
|
|
855
|
+
}
|
|
856
|
+
async function handleGetCanvasOverview(params) {
|
|
857
|
+
const page = await loadPage(params.pageId);
|
|
858
|
+
const frames = page.children.filter((n) => n.type === "FRAME").map((f) => {
|
|
859
|
+
var _a, _b;
|
|
860
|
+
return {
|
|
861
|
+
id: f.id,
|
|
862
|
+
name: f.name,
|
|
863
|
+
width: f.width,
|
|
864
|
+
height: f.height,
|
|
865
|
+
isStartFrame: (_b = (_a = page.flowStartingPoints) == null ? void 0 : _a.some((p) => p.nodeId === f.id)) != null ? _b : false
|
|
866
|
+
};
|
|
867
|
+
});
|
|
868
|
+
const selection = figma.currentPage.selection.map((n) => ({
|
|
869
|
+
id: n.id,
|
|
870
|
+
name: n.name,
|
|
871
|
+
type: n.type,
|
|
872
|
+
parentFrameId: findEnclosingFrameId(n),
|
|
873
|
+
hasExistingReactions: hasReactions(n)
|
|
874
|
+
}));
|
|
875
|
+
return { page: { id: page.id, name: page.name }, frames, selection };
|
|
876
|
+
}
|
|
877
|
+
async function handleGetPrototypeFlow(params) {
|
|
878
|
+
var _a, _b, _c, _d, _e, _f;
|
|
879
|
+
const page = await loadPage(params.pageId);
|
|
880
|
+
const limit = (_a = params.limit) != null ? _a : 500;
|
|
881
|
+
const frames = page.findAll((n) => n.type === "FRAME").map((f) => {
|
|
882
|
+
var _a2, _b2;
|
|
883
|
+
return {
|
|
884
|
+
id: f.id,
|
|
885
|
+
name: f.name,
|
|
886
|
+
isStartFrame: (_b2 = (_a2 = page.flowStartingPoints) == null ? void 0 : _a2.some((p) => p.nodeId === f.id)) != null ? _b2 : false
|
|
887
|
+
};
|
|
888
|
+
});
|
|
889
|
+
const reactiveNodes = page.findAll(
|
|
890
|
+
(n) => {
|
|
891
|
+
var _a2, _b2;
|
|
892
|
+
return "reactions" in n && ((_b2 = (_a2 = n.reactions) == null ? void 0 : _a2.length) != null ? _b2 : 0) > 0;
|
|
893
|
+
}
|
|
894
|
+
);
|
|
895
|
+
const interactions = [];
|
|
896
|
+
for (const node of reactiveNodes) {
|
|
897
|
+
const frameId = node.type === "FRAME" ? node.id : findEnclosingFrameId(node);
|
|
898
|
+
const reactions = (_b = node.reactions) != null ? _b : [];
|
|
899
|
+
for (const r of reactions) {
|
|
900
|
+
const firstAction = (_e = (_d = (_c = r.actions) == null ? void 0 : _c[0]) != null ? _d : r.action) != null ? _e : {};
|
|
901
|
+
interactions.push({
|
|
902
|
+
frameId,
|
|
903
|
+
sourceNodeId: node.id,
|
|
904
|
+
sourceNodeName: node.name,
|
|
905
|
+
trigger: (_f = r.trigger) != null ? _f : { type: "UNKNOWN" },
|
|
906
|
+
action: await encodeActionForListEcho(firstAction, echoResolvers)
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
return assembleFlowGraph({ page: { id: page.id, name: page.name }, frames, interactions, limit });
|
|
911
|
+
}
|
|
912
|
+
async function handleFindNodes(params) {
|
|
913
|
+
var _a, _b;
|
|
914
|
+
const scope = (_a = params.scope) != null ? _a : "page";
|
|
915
|
+
const limit = (_b = params.limit) != null ? _b : 50;
|
|
916
|
+
const q = params.query.toLowerCase();
|
|
917
|
+
let root;
|
|
918
|
+
if (scope === "document") {
|
|
919
|
+
await figma.loadAllPagesAsync();
|
|
920
|
+
root = figma.root;
|
|
921
|
+
} else {
|
|
922
|
+
root = figma.currentPage;
|
|
923
|
+
}
|
|
924
|
+
const matches = root.findAll((n) => {
|
|
925
|
+
if (!n.name.toLowerCase().includes(q)) return false;
|
|
926
|
+
if (params.nodeTypes && params.nodeTypes.length && !params.nodeTypes.includes(n.type)) return false;
|
|
927
|
+
return true;
|
|
928
|
+
});
|
|
929
|
+
const truncated = matches.length > limit;
|
|
930
|
+
const sliced = matches.slice(0, limit);
|
|
931
|
+
return {
|
|
932
|
+
nodes: sliced.map((n) => ({
|
|
933
|
+
id: n.id,
|
|
934
|
+
name: n.name,
|
|
935
|
+
type: n.type,
|
|
936
|
+
parentFrameId: findEnclosingFrameId(n),
|
|
937
|
+
path: pathOf(n)
|
|
938
|
+
})),
|
|
939
|
+
truncated
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
async function handleListVariables(params) {
|
|
943
|
+
var _a;
|
|
944
|
+
const includeRemote = (_a = params.includeRemote) != null ? _a : true;
|
|
945
|
+
const filters = { resolvedType: params.resolvedType, nameQuery: params.nameQuery };
|
|
946
|
+
const localVars = await figma.variables.getLocalVariablesAsync();
|
|
947
|
+
const localDescriptors = await Promise.all(
|
|
948
|
+
localVars.map(async (v) => {
|
|
949
|
+
var _a2;
|
|
950
|
+
const col = await figma.variables.getVariableCollectionByIdAsync(v.variableCollectionId);
|
|
951
|
+
return {
|
|
952
|
+
name: v.name,
|
|
953
|
+
id: v.id,
|
|
954
|
+
resolvedType: v.resolvedType,
|
|
955
|
+
collection: (_a2 = col == null ? void 0 : col.name) != null ? _a2 : ""
|
|
956
|
+
};
|
|
957
|
+
})
|
|
958
|
+
);
|
|
959
|
+
const local = filterVariables(localDescriptors, filters);
|
|
960
|
+
let library = [];
|
|
961
|
+
let remoteEnumerated = false;
|
|
962
|
+
if (includeRemote) {
|
|
963
|
+
try {
|
|
964
|
+
const collections = await figma.teamLibrary.getAvailableLibraryVariableCollectionsAsync();
|
|
965
|
+
const all = [];
|
|
966
|
+
for (const col of collections) {
|
|
967
|
+
const vars = await figma.teamLibrary.getVariablesInLibraryCollectionAsync(col.key);
|
|
968
|
+
for (const v of vars) {
|
|
969
|
+
all.push({
|
|
970
|
+
name: v.name,
|
|
971
|
+
key: v.key,
|
|
972
|
+
resolvedType: v.resolvedType,
|
|
973
|
+
collection: col.name,
|
|
974
|
+
libraryName: col.libraryName
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
library = filterVariables(all, filters);
|
|
979
|
+
remoteEnumerated = true;
|
|
980
|
+
} catch (e) {
|
|
981
|
+
library = [];
|
|
982
|
+
remoteEnumerated = false;
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
return { local, library, remoteEnumerated };
|
|
986
|
+
}
|
|
987
|
+
async function handleCreateReactions(params) {
|
|
988
|
+
var _a;
|
|
989
|
+
await figma.loadAllPagesAsync();
|
|
990
|
+
const results = [];
|
|
991
|
+
let successCount = 0;
|
|
992
|
+
let errorCount = 0;
|
|
993
|
+
let warningCount = 0;
|
|
994
|
+
for (const conn of params.connections) {
|
|
995
|
+
try {
|
|
996
|
+
const source = figma.getNodeById(conn.sourceNodeId);
|
|
997
|
+
if (!source) throw new Error(`Source node not found: ${conn.sourceNodeId}`);
|
|
998
|
+
if (!("setReactionsAsync" in source) || typeof source.setReactionsAsync !== "function") {
|
|
999
|
+
throw new Error(`Node cannot have reactions: ${source.name} (type: ${source.type})`);
|
|
1000
|
+
}
|
|
1001
|
+
let newReaction;
|
|
1002
|
+
let warning;
|
|
1003
|
+
if (conn.action.type === "conditional") {
|
|
1004
|
+
const { condition, warning: condWarning } = await buildCondition(conn.action.condition);
|
|
1005
|
+
if (condWarning) warning = condWarning;
|
|
1006
|
+
const thenBuilt = [];
|
|
1007
|
+
for (const a of conn.action.then) {
|
|
1008
|
+
const r = await buildNonConditionalAction(a, conn.trigger, conn.afterTimeoutSeconds, conn.transition, source, conn.degradeTo);
|
|
1009
|
+
thenBuilt.push(r.built);
|
|
1010
|
+
if (r.warning && !warning) warning = r.warning;
|
|
1011
|
+
}
|
|
1012
|
+
let elseBuilt;
|
|
1013
|
+
if (conn.action.else) {
|
|
1014
|
+
elseBuilt = [];
|
|
1015
|
+
for (const a of conn.action.else) {
|
|
1016
|
+
const r = await buildNonConditionalAction(a, conn.trigger, conn.afterTimeoutSeconds, conn.transition, source, conn.degradeTo);
|
|
1017
|
+
elseBuilt.push(r.built);
|
|
1018
|
+
if (r.warning && !warning) warning = r.warning;
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
newReaction = buildConditionalReaction({
|
|
1022
|
+
trigger: conn.trigger,
|
|
1023
|
+
afterTimeoutSeconds: conn.afterTimeoutSeconds,
|
|
1024
|
+
condition,
|
|
1025
|
+
thenActions: thenBuilt,
|
|
1026
|
+
elseActions: elseBuilt
|
|
1027
|
+
});
|
|
1028
|
+
} else if (conn.action.type === "toggle_variable") {
|
|
1029
|
+
const { variable, warning: resolveWarning } = await resolveVariableByName(
|
|
1030
|
+
conn.action.variable,
|
|
1031
|
+
conn.action.collection
|
|
1032
|
+
);
|
|
1033
|
+
if (variable.resolvedType !== "BOOLEAN") {
|
|
1034
|
+
throw new Error(`Cannot toggle non-BOOLEAN variable "${conn.action.variable}" (type: ${variable.resolvedType}); toggle_variable requires BOOLEAN`);
|
|
1035
|
+
}
|
|
1036
|
+
if (resolveWarning) warning = resolveWarning;
|
|
1037
|
+
const condition = {
|
|
1038
|
+
type: "EXPRESSION",
|
|
1039
|
+
resolvedType: "BOOLEAN",
|
|
1040
|
+
value: {
|
|
1041
|
+
expressionFunction: "EQUALS",
|
|
1042
|
+
expressionArguments: [
|
|
1043
|
+
{
|
|
1044
|
+
type: "VARIABLE_ALIAS",
|
|
1045
|
+
resolvedType: "BOOLEAN",
|
|
1046
|
+
value: { type: "VARIABLE_ALIAS", id: variable.id }
|
|
1047
|
+
},
|
|
1048
|
+
{ type: "BOOLEAN", resolvedType: "BOOLEAN", value: true }
|
|
1049
|
+
]
|
|
1050
|
+
}
|
|
1051
|
+
};
|
|
1052
|
+
const setFalseAction = {
|
|
1053
|
+
type: "SET_VARIABLE",
|
|
1054
|
+
variableId: variable.id,
|
|
1055
|
+
variableValue: { type: "BOOLEAN", resolvedType: "BOOLEAN", value: false }
|
|
1056
|
+
};
|
|
1057
|
+
const setTrueAction = {
|
|
1058
|
+
type: "SET_VARIABLE",
|
|
1059
|
+
variableId: variable.id,
|
|
1060
|
+
variableValue: { type: "BOOLEAN", resolvedType: "BOOLEAN", value: true }
|
|
1061
|
+
};
|
|
1062
|
+
newReaction = buildConditionalReaction({
|
|
1063
|
+
trigger: conn.trigger,
|
|
1064
|
+
afterTimeoutSeconds: conn.afterTimeoutSeconds,
|
|
1065
|
+
condition,
|
|
1066
|
+
thenActions: [setFalseAction],
|
|
1067
|
+
elseActions: [setTrueAction]
|
|
1068
|
+
});
|
|
1069
|
+
} else {
|
|
1070
|
+
const { built, warning: branchWarning } = await buildNonConditionalAction(
|
|
1071
|
+
conn.action,
|
|
1072
|
+
conn.trigger,
|
|
1073
|
+
conn.afterTimeoutSeconds,
|
|
1074
|
+
conn.transition,
|
|
1075
|
+
source,
|
|
1076
|
+
conn.degradeTo
|
|
1077
|
+
);
|
|
1078
|
+
if (branchWarning) warning = branchWarning;
|
|
1079
|
+
newReaction = {
|
|
1080
|
+
trigger: buildTrigger(conn.trigger, conn.afterTimeoutSeconds),
|
|
1081
|
+
actions: [built]
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
const existing = "reactions" in source ? source.reactions : [];
|
|
1085
|
+
const next = params.replaceExisting ? [newReaction] : [...existing, newReaction];
|
|
1086
|
+
await source.setReactionsAsync(next);
|
|
1087
|
+
const result = {
|
|
1088
|
+
sourceNodeId: conn.sourceNodeId,
|
|
1089
|
+
status: "success",
|
|
1090
|
+
reactionIndex: next.length - 1
|
|
1091
|
+
};
|
|
1092
|
+
if (warning) {
|
|
1093
|
+
result.warning = warning;
|
|
1094
|
+
warningCount++;
|
|
1095
|
+
}
|
|
1096
|
+
results.push(result);
|
|
1097
|
+
successCount++;
|
|
1098
|
+
} catch (err) {
|
|
1099
|
+
results.push({
|
|
1100
|
+
sourceNodeId: conn.sourceNodeId,
|
|
1101
|
+
status: "error",
|
|
1102
|
+
error: (_a = err == null ? void 0 : err.message) != null ? _a : String(err)
|
|
1103
|
+
});
|
|
1104
|
+
errorCount++;
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
return { results, successCount, errorCount, warningCount };
|
|
1108
|
+
}
|
|
1109
|
+
var echoResolvers = {
|
|
1110
|
+
variableName: async (id) => {
|
|
1111
|
+
var _a;
|
|
1112
|
+
try {
|
|
1113
|
+
return (_a = await figma.variables.getVariableByIdAsync(id)) == null ? void 0 : _a.name;
|
|
1114
|
+
} catch (e) {
|
|
1115
|
+
return void 0;
|
|
1116
|
+
}
|
|
1117
|
+
},
|
|
1118
|
+
nodeName: (id) => {
|
|
1119
|
+
var _a, _b;
|
|
1120
|
+
return (_b = (_a = figma.getNodeById(id)) == null ? void 0 : _a.name) != null ? _b : void 0;
|
|
1121
|
+
}
|
|
1122
|
+
};
|
|
1123
|
+
async function handleListReactions(params) {
|
|
1124
|
+
var _a;
|
|
1125
|
+
await figma.loadAllPagesAsync();
|
|
1126
|
+
const node = figma.getNodeById(params.nodeId);
|
|
1127
|
+
if (!node) throw new Error(`Node not found: ${params.nodeId}`);
|
|
1128
|
+
if (!("reactions" in node)) throw new Error(`Node has no reactions field: ${node.name}`);
|
|
1129
|
+
const reactions = (_a = node.reactions) != null ? _a : [];
|
|
1130
|
+
return {
|
|
1131
|
+
nodeId: node.id,
|
|
1132
|
+
nodeName: node.name,
|
|
1133
|
+
reactions: await Promise.all(reactions.map(async (r, i) => {
|
|
1134
|
+
var _a2, _b, _c, _d;
|
|
1135
|
+
const firstAction = (_c = (_b = (_a2 = r.actions) == null ? void 0 : _a2[0]) != null ? _b : r.action) != null ? _c : {};
|
|
1136
|
+
return {
|
|
1137
|
+
index: i,
|
|
1138
|
+
trigger: (_d = r.trigger) != null ? _d : { type: "UNKNOWN" },
|
|
1139
|
+
action: await encodeActionForListEcho(firstAction, echoResolvers)
|
|
1140
|
+
};
|
|
1141
|
+
}))
|
|
1142
|
+
};
|
|
1143
|
+
}
|
|
1144
|
+
async function handleClearReactions(params) {
|
|
1145
|
+
var _a, _b;
|
|
1146
|
+
await figma.loadAllPagesAsync();
|
|
1147
|
+
const results = [];
|
|
1148
|
+
for (const nodeId of params.nodeIds) {
|
|
1149
|
+
try {
|
|
1150
|
+
const node = figma.getNodeById(nodeId);
|
|
1151
|
+
if (!node) throw new Error(`Node not found: ${nodeId}`);
|
|
1152
|
+
if (!("setReactionsAsync" in node)) throw new Error(`Node cannot have reactions: ${node.name}`);
|
|
1153
|
+
const existing = (_a = node.reactions) != null ? _a : [];
|
|
1154
|
+
let next;
|
|
1155
|
+
let removedCount;
|
|
1156
|
+
if (params.indices && params.indices.length > 0) {
|
|
1157
|
+
const toRemove = new Set(params.indices);
|
|
1158
|
+
next = existing.filter((_, i) => !toRemove.has(i));
|
|
1159
|
+
removedCount = existing.length - next.length;
|
|
1160
|
+
} else {
|
|
1161
|
+
next = [];
|
|
1162
|
+
removedCount = existing.length;
|
|
1163
|
+
}
|
|
1164
|
+
await node.setReactionsAsync(next);
|
|
1165
|
+
results.push({ nodeId, removedCount, status: "success" });
|
|
1166
|
+
} catch (err) {
|
|
1167
|
+
results.push({ nodeId, removedCount: 0, status: "error", error: (_b = err == null ? void 0 : err.message) != null ? _b : String(err) });
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
return { results };
|
|
1171
|
+
}
|
|
1172
|
+
async function handleSetFrameScroll(params) {
|
|
1173
|
+
var _a;
|
|
1174
|
+
await figma.loadAllPagesAsync();
|
|
1175
|
+
const results = [];
|
|
1176
|
+
let successCount = 0;
|
|
1177
|
+
let errorCount = 0;
|
|
1178
|
+
for (const { frameId, direction, fixedChildren } of params.frames) {
|
|
1179
|
+
const applied = [];
|
|
1180
|
+
try {
|
|
1181
|
+
const node = figma.getNodeById(frameId);
|
|
1182
|
+
if (!node) throw new Error(`Frame not found: ${frameId}`);
|
|
1183
|
+
if (node.type !== "FRAME") {
|
|
1184
|
+
throw new Error(`Node is not a FRAME: ${node.name} (type: ${node.type})`);
|
|
1185
|
+
}
|
|
1186
|
+
if (direction !== void 0) {
|
|
1187
|
+
node.overflowDirection = direction;
|
|
1188
|
+
applied.push("direction");
|
|
1189
|
+
}
|
|
1190
|
+
if (fixedChildren !== void 0) {
|
|
1191
|
+
node.numberOfFixedChildren = fixedChildren;
|
|
1192
|
+
applied.push("fixedChildren");
|
|
1193
|
+
}
|
|
1194
|
+
results.push({ frameId, status: "success", applied });
|
|
1195
|
+
successCount++;
|
|
1196
|
+
} catch (e) {
|
|
1197
|
+
results.push({ frameId, status: "error", applied, error: (_a = e == null ? void 0 : e.message) != null ? _a : String(e) });
|
|
1198
|
+
errorCount++;
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
return { results, successCount, errorCount };
|
|
1202
|
+
}
|
|
1203
|
+
})();
|