@clovis500c/figma-bridge 1.8.0 → 1.13.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/plugin/code.js CHANGED
@@ -1177,6 +1177,186 @@
1177
1177
  return a.x >= b.x - 0.5 && a.y >= b.y - 0.5 && a.x + a.width <= b.x + b.width + 0.5 && a.y + a.height <= b.y + b.height + 0.5;
1178
1178
  }
1179
1179
 
1180
+ // plugin/lib/figjam.ts
1181
+ var EDITORS = { sticky: "figjam", shape: "figjam", connector: "figjam", table: "figjam", codeblock: "figjam", slide: "slides" };
1182
+ var EDITOR_NAMES = { figma: "Figma Design", figjam: "FigJam", slides: "Figma Slides", dev: "Dev Mode" };
1183
+ function requireEditor(type, path) {
1184
+ const wanted = EDITORS[type];
1185
+ if (wanted && figma.editorType !== wanted) {
1186
+ throw codeError(path + ': "' + type + '" needs a ' + EDITOR_NAMES[wanted] + " file; this file is open in " + (EDITOR_NAMES[figma.editorType] || figma.editorType) + ".", "WRONG_EDITOR");
1187
+ }
1188
+ }
1189
+ var STICKY_COLORS = {
1190
+ white: "#FFFFFF",
1191
+ gray: "#E6E6E6",
1192
+ grey: "#E6E6E6",
1193
+ green: "#B3EFBD",
1194
+ teal: "#B3F4EF",
1195
+ blue: "#A8DAFF",
1196
+ violet: "#D3BDFF",
1197
+ purple: "#D3BDFF",
1198
+ pink: "#FFA8DB",
1199
+ red: "#FFB8A8",
1200
+ orange: "#FFD3A8",
1201
+ yellow: "#FFE299"
1202
+ };
1203
+ function solid(color) {
1204
+ const hex = STICKY_COLORS[String(color).toLowerCase()] || color;
1205
+ const c = parseHex(hex);
1206
+ return { type: "SOLID", color: { r: c.r, g: c.g, b: c.b }, opacity: c.a };
1207
+ }
1208
+ async function setSublayerText(sub, text, opts) {
1209
+ const font = sub.fontName;
1210
+ if (font !== figma.mixed)
1211
+ await loadFont(font);
1212
+ else if (sub.characters.length) {
1213
+ const fonts = sub.getRangeAllFontNames(0, sub.characters.length);
1214
+ for (let i = 0;i < fonts.length; i++)
1215
+ await loadFont(fonts[i]);
1216
+ }
1217
+ if (opts && opts.font) {
1218
+ await loadFont(opts.font);
1219
+ sub.fontName = opts.font;
1220
+ }
1221
+ sub.characters = String(text === undefined || text === null ? "" : text);
1222
+ if (opts && typeof opts.size === "number")
1223
+ sub.fontSize = opts.size;
1224
+ if (opts && opts.color)
1225
+ sub.fills = [solid(opts.color)];
1226
+ }
1227
+ async function createSticky(s) {
1228
+ const n = figma.createSticky();
1229
+ if (s.wide)
1230
+ n.isWideWidth = true;
1231
+ if (s.author === false)
1232
+ n.authorVisible = false;
1233
+ if (s.color || s.fill)
1234
+ n.fills = [solid(s.color || s.fill)];
1235
+ await setSublayerText(n.text, s.text);
1236
+ return n;
1237
+ }
1238
+ var SHAPES = {
1239
+ square: "SQUARE",
1240
+ rect: "SQUARE",
1241
+ rectangle: "SQUARE",
1242
+ rounded: "ROUNDED_RECTANGLE",
1243
+ roundedrectangle: "ROUNDED_RECTANGLE",
1244
+ ellipse: "ELLIPSE",
1245
+ circle: "ELLIPSE",
1246
+ diamond: "DIAMOND",
1247
+ decision: "DIAMOND",
1248
+ triangle: "TRIANGLE_UP",
1249
+ triangleup: "TRIANGLE_UP",
1250
+ triangledown: "TRIANGLE_DOWN",
1251
+ parallelogram: "PARALLELOGRAM_RIGHT",
1252
+ parallelogramleft: "PARALLELOGRAM_LEFT",
1253
+ database: "ENG_DATABASE",
1254
+ cylinder: "ENG_DATABASE",
1255
+ queue: "ENG_QUEUE",
1256
+ file: "ENG_FILE",
1257
+ folder: "ENG_FOLDER",
1258
+ trapezoid: "TRAPEZOID",
1259
+ process: "PREDEFINED_PROCESS",
1260
+ subroutine: "PREDEFINED_PROCESS",
1261
+ shield: "SHIELD",
1262
+ document: "DOCUMENT_SINGLE",
1263
+ documents: "DOCUMENT_MULTIPLE",
1264
+ input: "MANUAL_INPUT",
1265
+ hexagon: "HEXAGON",
1266
+ chevron: "CHEVRON",
1267
+ pentagon: "PENTAGON",
1268
+ octagon: "OCTAGON",
1269
+ star: "STAR",
1270
+ plus: "PLUS",
1271
+ arrowleft: "ARROW_LEFT",
1272
+ arrowright: "ARROW_RIGHT",
1273
+ junction: "SUMMING_JUNCTION",
1274
+ or: "OR",
1275
+ speech: "SPEECH_BUBBLE",
1276
+ speechbubble: "SPEECH_BUBBLE",
1277
+ storage: "INTERNAL_STORAGE"
1278
+ };
1279
+ async function createShape(s, path, warnings) {
1280
+ const n = figma.createShapeWithText();
1281
+ const key = String(s.shape || "rounded").toLowerCase().replace(/[^a-z]/g, "");
1282
+ const shape = SHAPES[key] || String(s.shape || "").toUpperCase();
1283
+ try {
1284
+ n.shapeType = shape;
1285
+ } catch (e) {
1286
+ warnings.push(path + ': unknown shape "' + s.shape + '", used a rounded rectangle');
1287
+ n.shapeType = "ROUNDED_RECTANGLE";
1288
+ }
1289
+ const w = typeof s.w === "number" ? s.w : typeof s.width === "number" ? s.width : 0;
1290
+ const h = typeof s.h === "number" ? s.h : typeof s.height === "number" ? s.height : 0;
1291
+ if (w || h)
1292
+ n.resize(w || n.width, h || n.height);
1293
+ if (s.fill !== undefined)
1294
+ n.fills = s.fill === null ? [] : [solid(s.fill)];
1295
+ if (s.stroke !== undefined)
1296
+ n.strokes = s.stroke === null ? [] : [solid(s.stroke)];
1297
+ if (typeof s.strokeWidth === "number")
1298
+ n.strokeWeight = s.strokeWidth;
1299
+ if (s.text !== undefined)
1300
+ await setSublayerText(n.text, s.text, { size: s.size, color: s.color });
1301
+ return n;
1302
+ }
1303
+ async function createTable(s, path) {
1304
+ const rows = Array.isArray(s.rows) ? s.rows : [];
1305
+ if (!rows.length)
1306
+ throw codeError(path + ': a table needs rows:[["Header 1","Header 2"],["a","b"]]', "BAD_ARGS");
1307
+ let cols = 0;
1308
+ for (let i = 0;i < rows.length; i++)
1309
+ cols = Math.max(cols, Array.isArray(rows[i]) ? rows[i].length : 1);
1310
+ const t = figma.createTable(rows.length, cols);
1311
+ for (let r = 0;r < rows.length; r++) {
1312
+ for (let c = 0;c < cols; c++) {
1313
+ const value = Array.isArray(rows[r]) ? rows[r][c] : c === 0 ? rows[r] : "";
1314
+ const cell = t.cellAt(r, c);
1315
+ await setSublayerText(cell.text, value === undefined ? "" : value);
1316
+ if (r === 0 && s.header !== false)
1317
+ cell.fills = [solid(s.headerFill || "#F2F2F2")];
1318
+ }
1319
+ }
1320
+ return t;
1321
+ }
1322
+ var LANGUAGES = ["TYPESCRIPT", "CPP", "RUBY", "CSS", "JAVASCRIPT", "HTML", "JSON", "GRAPHQL", "PYTHON", "GO", "SQL", "SWIFT", "KOTLIN", "RUST", "BASH", "PLAINTEXT", "DART"];
1323
+ var LANGUAGE_ALIASES = { ts: "TYPESCRIPT", tsx: "TYPESCRIPT", js: "JAVASCRIPT", jsx: "JAVASCRIPT", "c++": "CPP", py: "PYTHON", sh: "BASH", shell: "BASH", text: "PLAINTEXT", txt: "PLAINTEXT", golang: "GO", rb: "RUBY" };
1324
+ function createCodeBlock(s) {
1325
+ const n = figma.createCodeBlock();
1326
+ const lang = String(s.language || "plaintext").toLowerCase();
1327
+ const code = LANGUAGE_ALIASES[lang] || lang.toUpperCase();
1328
+ n.codeLanguage = LANGUAGES.indexOf(code) !== -1 ? code : "PLAINTEXT";
1329
+ n.code = String(s.code === undefined ? "" : s.code);
1330
+ return n;
1331
+ }
1332
+ function createSlide() {
1333
+ return figma.createSlide();
1334
+ }
1335
+ var CAPS = { arrow: "ARROW_LINES", triangle: "ARROW_EQUILATERAL", filled: "TRIANGLE_FILLED", diamond: "DIAMOND_FILLED", circle: "CIRCLE_FILLED", none: "NONE" };
1336
+ var LINES = { elbowed: "ELBOWED", elbow: "ELBOWED", straight: "STRAIGHT", curved: "CURVED", curve: "CURVED" };
1337
+ var MAGNETS = ["AUTO", "TOP", "LEFT", "BOTTOM", "RIGHT", "CENTER", "NONE"];
1338
+ async function createConnector(s, fromId, toId) {
1339
+ const n = figma.createConnector();
1340
+ const magnet = function(v) {
1341
+ const m = String(v || "AUTO").toUpperCase();
1342
+ return MAGNETS.indexOf(m) !== -1 ? m : "AUTO";
1343
+ };
1344
+ n.connectorStart = { endpointNodeId: fromId, magnet: magnet(s.fromMagnet) };
1345
+ n.connectorEnd = { endpointNodeId: toId, magnet: magnet(s.toMagnet) };
1346
+ n.connectorLineType = LINES[String(s.line || "elbowed").toLowerCase()] || "ELBOWED";
1347
+ n.connectorEndStrokeCap = CAPS[String(s.endArrow === undefined ? "arrow" : s.endArrow).toLowerCase()] || "ARROW_LINES";
1348
+ n.connectorStartStrokeCap = CAPS[String(s.startArrow === undefined ? "none" : s.startArrow).toLowerCase()] || "NONE";
1349
+ if (s.color || s.stroke)
1350
+ n.strokes = [solid(s.color || s.stroke)];
1351
+ if (typeof s.strokeWidth === "number")
1352
+ n.strokeWeight = s.strokeWidth;
1353
+ if (s.dashed)
1354
+ n.dashPattern = [8, 6];
1355
+ if (s.label !== undefined && s.label !== "")
1356
+ await setSublayerText(n.text, s.label);
1357
+ return n;
1358
+ }
1359
+
1180
1360
  // plugin/lib/prototype.ts
1181
1361
  var TRIGGERS = {
1182
1362
  click: "ON_CLICK",
@@ -1763,7 +1943,9 @@
1763
1943
  const spec = p.spec;
1764
1944
  if (!spec || typeof spec !== "object")
1765
1945
  throw codeError("`spec` must be a node object or an array of node objects", "BAD_ARGS");
1766
- const roots = Array.isArray(spec) ? spec : [spec];
1946
+ const roots = Array.isArray(spec) ? spec : Array.isArray(spec.slides) ? spec.slides.map(function(sl) {
1947
+ return Object.assign({ type: "slide" }, sl);
1948
+ }) : [spec];
1767
1949
  const d = p.defaults || {};
1768
1950
  const ctx = {
1769
1951
  warnings: [],
@@ -1774,6 +1956,8 @@
1774
1956
  defaults: { font: d.font || "Inter", color: d.color || "#111111", size: d.size || 14 },
1775
1957
  binds: [],
1776
1958
  reactions: [],
1959
+ connectors: [],
1960
+ keys: {},
1777
1961
  requestId,
1778
1962
  total: countNodes(roots),
1779
1963
  lastProgress: 0
@@ -1786,7 +1970,7 @@
1786
1970
  const node = await createNode(roots[i], parent, ctx, "spec" + (roots.length > 1 ? "[" + i + "]" : ""));
1787
1971
  if (!node)
1788
1972
  continue;
1789
- if (!isAutoLayout(parent) && roots[i].x === undefined && roots[i].y === undefined) {
1973
+ if (node.type !== "SLIDE" && !isAutoLayout(parent) && roots[i].x === undefined && roots[i].y === undefined) {
1790
1974
  if (cursorX === null) {
1791
1975
  place(node, p.x, p.y);
1792
1976
  } else {
@@ -1799,6 +1983,7 @@
1799
1983
  }
1800
1984
  if (ctx.reactions.length)
1801
1985
  await applyBuildReactions(ctx.reactions, ctx.ids, ctx.warnings);
1986
+ const connectors = await createConnectors(ctx);
1802
1987
  for (let i = 0;i < ctx.binds.length; i++)
1803
1988
  ctx.warnings.push(ctx.binds[i].path + ": bind needs a component or componentSet ancestor, ignored");
1804
1989
  if (p.select !== false && made.length && parent.type === "PAGE" && parent === figma.currentPage) {
@@ -1810,7 +1995,7 @@
1810
1995
  rootIds: made.map(function(n) {
1811
1996
  return n.id;
1812
1997
  }),
1813
- created: ctx.count,
1998
+ created: ctx.count + connectors,
1814
1999
  ids: ctx.ids
1815
2000
  };
1816
2001
  if (ctx.warnings.length)
@@ -1891,7 +2076,7 @@
1891
2076
  function nodeType(s) {
1892
2077
  if (s.type) {
1893
2078
  const t = String(s.type).toLowerCase().replace(/[_\s-]/g, "");
1894
- return t === "rectangle" ? "rect" : t === "circle" ? "ellipse" : t === "variants" ? "componentset" : t;
2079
+ return t === "rectangle" ? "rect" : t === "circle" ? "ellipse" : t === "variants" ? "componentset" : t === "shapewithtext" ? "shape" : t === "code" ? "codeblock" : t;
1895
2080
  }
1896
2081
  if (Array.isArray(s.variants))
1897
2082
  return "componentset";
@@ -1916,8 +2101,30 @@
1916
2101
  throw codeError("Spec is too large (max " + MAX_NODES3 + " nodes). Split it into several build calls.", "TOO_LARGE");
1917
2102
  checkCancelled(ctx.requestId);
1918
2103
  const type = nodeType(s);
2104
+ requireEditor(type, path);
1919
2105
  let node;
1920
2106
  switch (type) {
2107
+ case "connector":
2108
+ ctx.connectors.push({ spec: s, parent, path });
2109
+ return null;
2110
+ case "sticky":
2111
+ node = await createSticky(s);
2112
+ break;
2113
+ case "shape":
2114
+ node = await createShape(s, path, ctx.warnings);
2115
+ break;
2116
+ case "table":
2117
+ node = await createTable(s, path);
2118
+ break;
2119
+ case "codeblock":
2120
+ node = createCodeBlock(s);
2121
+ break;
2122
+ case "section":
2123
+ node = figma.createSection();
2124
+ break;
2125
+ case "slide":
2126
+ node = createSlide();
2127
+ break;
1921
2128
  case "frame":
1922
2129
  case "component":
1923
2130
  node = type === "component" ? figma.createComponent() : figma.createFrame();
@@ -1954,8 +2161,11 @@
1954
2161
  }
1955
2162
  ctx.count++;
1956
2163
  progress(ctx);
1957
- parent.appendChild(node);
2164
+ if (type !== "slide" || parent.type !== "PAGE")
2165
+ parent.appendChild(node);
1958
2166
  const parentAuto = isAutoLayout(parent);
2167
+ if (s.key !== undefined)
2168
+ ctx.keys[String(s.key)] = node.id;
1959
2169
  if (s.bind)
1960
2170
  ctx.binds.push({ node, bind: s.bind, path });
1961
2171
  const bindStart = ctx.binds.length;
@@ -1965,6 +2175,10 @@
1965
2175
  node.name = "icon/" + s.icon;
1966
2176
  if (type === "frame" || type === "component")
1967
2177
  await setupFrame(node, s, ctx, path);
2178
+ else if (type === "slide")
2179
+ await setupFrame(node, s, ctx, path, true);
2180
+ else if (type === "section")
2181
+ await setupSection(node, s, ctx, path);
1968
2182
  else if (type === "componentset")
1969
2183
  await setupLayout(node, Object.assign({ layout: "row", wrap: true, gap: 16, padding: 16 }, s), ctx, path);
1970
2184
  else if (type === "text")
@@ -1974,7 +2188,7 @@
1974
2188
  else if (type === "line") {
1975
2189
  node.resize(num(s.w, s.width, 100), 0);
1976
2190
  if (s.stroke === undefined)
1977
- node.strokes = [solid(ctx.defaults.color)];
2191
+ node.strokes = [solid2(ctx.defaults.color)];
1978
2192
  } else if ((type === "rect" || type === "ellipse") && s.w === undefined && s.width === undefined && s.size === undefined) {
1979
2193
  node.resize(100, 100);
1980
2194
  }
@@ -1982,14 +2196,18 @@
1982
2196
  await overrideTexts(node, s.text, ctx);
1983
2197
  if (type === "componentset" && s.stroke === undefined) {
1984
2198
  const set = node;
1985
- set.strokes = [solid("#9747FF")];
2199
+ set.strokes = [solid2("#9747FF")];
1986
2200
  set.dashPattern = [10, 5];
1987
2201
  set.cornerRadius = 5;
1988
2202
  }
1989
- await applyVisuals(node, s, ctx, type);
2203
+ if (FIXED_LOOK.indexOf(type) === -1)
2204
+ await applyVisuals(node, s, ctx, type);
2205
+ else if (typeof s.opacity === "number")
2206
+ node.opacity = s.opacity;
1990
2207
  if (type === "text" && Array.isArray(s.spans))
1991
2208
  await applySpans(node, s, ctx, path);
1992
- applySize(node, s, parentAuto, type, ctx, path);
2209
+ if (FIXED_LOOK.indexOf(type) === -1 && type !== "section" && type !== "slide")
2210
+ applySize(node, s, parentAuto, type, ctx, path);
1993
2211
  if (type === "component" && !s.variantOf)
1994
2212
  await addProperties(node, s.properties, ctx.binds.splice(bindStart), ctx, path);
1995
2213
  if (parent.layoutMode === "GRID")
@@ -2004,7 +2222,7 @@
2004
2222
  }
2005
2223
  if (s.grow && parentAuto)
2006
2224
  node.layoutGrow = 1;
2007
- if (typeof s.rotation === "number")
2225
+ if (typeof s.rotation === "number" && "rotation" in node)
2008
2226
  node.rotation = s.rotation;
2009
2227
  if (s.visible === false)
2010
2228
  node.visible = false;
@@ -2023,20 +2241,83 @@
2023
2241
  }
2024
2242
  return node;
2025
2243
  }
2026
- async function setupFrame(f, s, ctx, path) {
2027
- f.fills = [];
2244
+ var FIXED_LOOK = ["sticky", "shape", "table", "codeblock"];
2245
+ async function setupFrame(f, s, ctx, path, keepFills) {
2246
+ if (!keepFills)
2247
+ f.fills = [];
2028
2248
  await setupLayout(f, s, ctx, path);
2029
2249
  const children = Array.isArray(s.children) ? s.children : [];
2030
2250
  for (let i = 0;i < children.length; i++) {
2031
2251
  try {
2032
2252
  await createNode(children[i], f, ctx, path + ".children[" + i + "]");
2033
2253
  } catch (e) {
2034
- if (e.code === "TOO_LARGE" || e.code === "CANCELLED")
2254
+ if (e.code === "TOO_LARGE" || e.code === "CANCELLED" || e.code === "WRONG_EDITOR")
2035
2255
  throw e;
2036
2256
  ctx.warnings.push(path + ".children[" + i + "]: " + (e.message || e));
2037
2257
  }
2038
2258
  }
2039
2259
  }
2260
+ async function setupSection(sec, s, ctx, path) {
2261
+ const children = Array.isArray(s.children) ? s.children : [];
2262
+ const made = [];
2263
+ for (let i = 0;i < children.length; i++) {
2264
+ try {
2265
+ const n = await createNode(children[i], sec, ctx, path + ".children[" + i + "]");
2266
+ if (n)
2267
+ made.push(n);
2268
+ } catch (e) {
2269
+ if (e.code === "TOO_LARGE" || e.code === "CANCELLED" || e.code === "WRONG_EDITOR")
2270
+ throw e;
2271
+ ctx.warnings.push(path + ".children[" + i + "]: " + (e.message || e));
2272
+ }
2273
+ }
2274
+ const w = num(s.w, s.width, 0);
2275
+ const h = num(s.h, s.height, 0);
2276
+ if (w && h) {
2277
+ sec.resizeWithoutConstraints(w, h);
2278
+ return;
2279
+ }
2280
+ const pad = typeof s.padding === "number" ? s.padding : 40;
2281
+ let right = 0;
2282
+ let bottom = 0;
2283
+ for (let i = 0;i < made.length; i++) {
2284
+ right = Math.max(right, made[i].x + made[i].width);
2285
+ bottom = Math.max(bottom, made[i].y + made[i].height);
2286
+ }
2287
+ sec.resizeWithoutConstraints(Math.max(w || right + pad, 100), Math.max(h || bottom + pad, 100));
2288
+ }
2289
+ async function createConnectors(ctx) {
2290
+ let made = 0;
2291
+ for (let i = 0;i < ctx.connectors.length; i++) {
2292
+ const c = ctx.connectors[i];
2293
+ const ends = [];
2294
+ const sides = ["from", "to"];
2295
+ for (let k = 0;k < 2; k++) {
2296
+ const ref = c.spec[sides[k]];
2297
+ let id = ref === undefined ? "" : ctx.keys[String(ref)] || ctx.ids[String(ref)] || "";
2298
+ if (!id && ref !== undefined && /^[\dI;:]+$/.test(String(ref))) {
2299
+ const n = await figma.getNodeByIdAsync(String(ref));
2300
+ if (n)
2301
+ id = n.id;
2302
+ }
2303
+ ends.push(id);
2304
+ }
2305
+ if (!ends[0] || !ends[1]) {
2306
+ ctx.warnings.push(c.path + ': connector ends not found (from "' + c.spec.from + '", to "' + c.spec.to + '")');
2307
+ continue;
2308
+ }
2309
+ try {
2310
+ const n = await createConnector(c.spec, ends[0], ends[1]);
2311
+ c.parent.appendChild(n);
2312
+ if (c.spec.name)
2313
+ n.name = String(c.spec.name);
2314
+ made++;
2315
+ } catch (e) {
2316
+ ctx.warnings.push(c.path + ": " + (e.message || e));
2317
+ }
2318
+ }
2319
+ return made;
2320
+ }
2040
2321
  async function setupLayout(f, s, ctx, path) {
2041
2322
  const layout = String(s.layout || s.direction || "").toLowerCase();
2042
2323
  let mode = layout === "row" || layout === "horizontal" ? "HORIZONTAL" : layout === "column" || layout === "col" || layout === "vertical" ? "VERTICAL" : layout === "grid" ? "GRID" : "NONE";
@@ -2070,7 +2351,7 @@
2070
2351
  if (s.rowGap !== undefined)
2071
2352
  await setNumber(f, "counterAxisSpacing", s.rowGap);
2072
2353
  }
2073
- } else if (s.w === undefined && s.width === undefined && s.size === undefined) {
2354
+ } else if (s.w === undefined && s.width === undefined && s.size === undefined && f.type !== "SLIDE") {
2074
2355
  f.resize(100, 100);
2075
2356
  }
2076
2357
  }
@@ -2210,7 +2491,7 @@
2210
2491
  t.maxLines = s.maxLines;
2211
2492
  }
2212
2493
  if (s.fill === undefined && s.color === undefined && !s.textStyle)
2213
- t.fills = [solid(ctx.defaults.color)];
2494
+ t.fills = [solid2(ctx.defaults.color)];
2214
2495
  if (s.textStyle) {
2215
2496
  const style = await findStyle("text", stripPrefix(s.textStyle));
2216
2497
  await loadFont(style.fontName);
@@ -2497,7 +2778,7 @@
2497
2778
  function stripPrefix(v) {
2498
2779
  return String(v).replace(/^(style|var):/, "");
2499
2780
  }
2500
- function solid(hex) {
2781
+ function solid2(hex) {
2501
2782
  const c = parseHex(hex);
2502
2783
  return { type: "SOLID", color: { r: c.r, g: c.g, b: c.b }, opacity: c.a };
2503
2784
  }
@@ -2521,9 +2802,9 @@
2521
2802
  if (typeof v === "string") {
2522
2803
  if (v.indexOf("var:") === 0) {
2523
2804
  const variable = await findVariable(stripPrefix(v));
2524
- return figma.variables.setBoundVariableForPaint(solid("#000000"), "color", variable);
2805
+ return figma.variables.setBoundVariableForPaint(solid2("#000000"), "color", variable);
2525
2806
  }
2526
- return solid(v);
2807
+ return solid2(v);
2527
2808
  }
2528
2809
  if (v && Array.isArray(v.gradient)) {
2529
2810
  const stops = v.gradient.map(function(c, i, all) {
@@ -3639,6 +3920,243 @@
3639
3920
  return out;
3640
3921
  }
3641
3922
 
3923
+ // plugin/lib/roblox.ts
3924
+ var MAX_NODES5 = 2000;
3925
+ var VECTORS = ["VECTOR", "BOOLEAN_OPERATION", "STAR", "POLYGON", "LINE"];
3926
+ async function robloxTree(p) {
3927
+ const node = p.nodeId ? await getNode(p.nodeId) : figma.currentPage.selection[0];
3928
+ if (!node)
3929
+ throw codeError("No nodeId given and nothing is selected", "BAD_ARGS");
3930
+ if (node.type === "PAGE" || node.type === "DOCUMENT")
3931
+ throw codeError("Export a frame, not a page", "BAD_ARGS");
3932
+ const ctx = { count: 0, truncated: false, images: {}, imageBytes: 0 };
3933
+ const tree = await walk2(node, null, ctx);
3934
+ return { tree, images: ctx.images, nodes: ctx.count, truncated: ctx.truncated, fileName: figma.root.name };
3935
+ }
3936
+ function paints(list) {
3937
+ if (!list || list === figma.mixed)
3938
+ return [];
3939
+ const out = [];
3940
+ for (let i = 0;i < list.length; i++) {
3941
+ const pt = list[i];
3942
+ if (pt.visible === false)
3943
+ continue;
3944
+ const o = { type: pt.type, opacity: pt.opacity === undefined ? 1 : round(pt.opacity) };
3945
+ if (pt.type === "SOLID")
3946
+ o.color = toHex(pt.color);
3947
+ else if (pt.type.indexOf("GRADIENT") === 0) {
3948
+ const t = pt.gradientTransform;
3949
+ o.angle = round(Math.atan2(t[0][1], t[0][0]) * 180 / Math.PI);
3950
+ o.stops = pt.gradientStops.map(function(s) {
3951
+ return { color: toHex({ r: s.color.r, g: s.color.g, b: s.color.b }), alpha: round(s.color.a), at: round(s.position) };
3952
+ });
3953
+ } else if (pt.type === "IMAGE") {
3954
+ o.imageHash = pt.imageHash;
3955
+ o.scaleMode = pt.scaleMode;
3956
+ }
3957
+ out.push(o);
3958
+ }
3959
+ return out;
3960
+ }
3961
+ function vectorOnly2(n) {
3962
+ if (VECTORS.indexOf(n.type) !== -1)
3963
+ return true;
3964
+ if (n.type === "TEXT" || !("children" in n) || !n.children.length)
3965
+ return false;
3966
+ for (let i = 0;i < n.children.length; i++)
3967
+ if (n.children[i].visible && !vectorOnly2(n.children[i]))
3968
+ return false;
3969
+ return true;
3970
+ }
3971
+ async function walk2(n, parent, ctx) {
3972
+ ctx.count++;
3973
+ const box = n.absoluteBoundingBox || { x: 0, y: 0, width: n.width || 0, height: n.height || 0 };
3974
+ const pbox = parent && parent.absoluteBoundingBox;
3975
+ const out = {
3976
+ id: n.id,
3977
+ name: n.name,
3978
+ type: n.type,
3979
+ x: pbox ? round(box.x - pbox.x) : 0,
3980
+ y: pbox ? round(box.y - pbox.y) : 0,
3981
+ w: round(box.width),
3982
+ h: round(box.height)
3983
+ };
3984
+ if (n.visible === false)
3985
+ out.hidden = true;
3986
+ if (typeof n.opacity === "number" && n.opacity < 1)
3987
+ out.opacity = round(n.opacity);
3988
+ if (n.rotation && Math.abs(n.rotation) > 0.01)
3989
+ out.rotation = round(-n.rotation);
3990
+ if (n.clipsContent)
3991
+ out.clip = true;
3992
+ if (n.constraints)
3993
+ out.constraints = { h: n.constraints.horizontal, v: n.constraints.vertical };
3994
+ if (n.layoutPositioning === "ABSOLUTE")
3995
+ out.absolute = true;
3996
+ if (n.layoutGrow === 1)
3997
+ out.grow = true;
3998
+ if (n.layoutAlign === "STRETCH")
3999
+ out.stretch = true;
4000
+ if ("layoutSizingHorizontal" in n)
4001
+ out.sizing = { h: n.layoutSizingHorizontal, v: n.layoutSizingVertical };
4002
+ const rb = n.absoluteRenderBounds;
4003
+ if (rb && (Math.abs(rb.x - box.x) > 0.5 || Math.abs(rb.y - box.y) > 0.5 || Math.abs(rb.width - box.width) > 0.5 || Math.abs(rb.height - box.height) > 0.5)) {
4004
+ out.render = { x: round(rb.x - box.x), y: round(rb.y - box.y), w: round(rb.width), h: round(rb.height) };
4005
+ }
4006
+ if ("fills" in n)
4007
+ out.fills = paints(n.fills);
4008
+ if ("strokes" in n && n.strokes.length) {
4009
+ out.strokes = paints(n.strokes);
4010
+ out.strokeWeight = n.strokeWeight === figma.mixed ? round(n.strokeTopWeight || 1) : round(n.strokeWeight);
4011
+ out.strokeAlign = n.strokeAlign;
4012
+ if (n.dashPattern && n.dashPattern.length)
4013
+ out.dashed = true;
4014
+ }
4015
+ if ("topLeftRadius" in n) {
4016
+ const r = [n.topLeftRadius, n.topRightRadius, n.bottomRightRadius, n.bottomLeftRadius].map(round);
4017
+ if (r[0] || r[1] || r[2] || r[3])
4018
+ out.radius = r;
4019
+ } else if (typeof n.cornerRadius === "number" && n.cornerRadius)
4020
+ out.radius = [round(n.cornerRadius), round(n.cornerRadius), round(n.cornerRadius), round(n.cornerRadius)];
4021
+ if ("effects" in n && n.effects.length) {
4022
+ out.effects = n.effects.filter(function(e) {
4023
+ return e.visible !== false;
4024
+ }).map(function(e) {
4025
+ return e.type === "DROP_SHADOW" || e.type === "INNER_SHADOW" ? { type: e.type, x: round(e.offset.x), y: round(e.offset.y), blur: round(e.radius), spread: round(e.spread || 0), color: toHex({ r: e.color.r, g: e.color.g, b: e.color.b }), alpha: round(e.color.a) } : { type: e.type, blur: round(e.radius) };
4026
+ });
4027
+ }
4028
+ if (n.reactions && n.reactions.length) {
4029
+ out.clickable = n.reactions.some(function(r) {
4030
+ return r.trigger && (r.trigger.type === "ON_CLICK" || r.trigger.type === "ON_PRESS");
4031
+ });
4032
+ }
4033
+ if (n.type !== "TEXT" && vectorOnly2(n))
4034
+ out.vector = true;
4035
+ const fills = out.fills || [];
4036
+ for (let i = 0;i < fills.length; i++) {
4037
+ const h = fills[i].imageHash;
4038
+ if (!h || ctx.images[h] !== undefined)
4039
+ continue;
4040
+ const image = figma.getImageByHash(h);
4041
+ if (!image)
4042
+ continue;
4043
+ const bytes = await image.getBytesAsync();
4044
+ if (ctx.imageBytes + bytes.length > 40 << 20)
4045
+ continue;
4046
+ ctx.imageBytes += bytes.length;
4047
+ ctx.images[h] = figma.base64Encode(bytes);
4048
+ }
4049
+ if (n.type === "TEXT") {
4050
+ out.text = textOf2(n);
4051
+ return out;
4052
+ }
4053
+ if (n.layoutMode && n.layoutMode !== "NONE") {
4054
+ const l = {
4055
+ mode: n.layoutMode,
4056
+ padding: [n.paddingTop, n.paddingRight, n.paddingBottom, n.paddingLeft].map(round),
4057
+ gap: round(n.itemSpacing || 0),
4058
+ primary: n.primaryAxisAlignItems,
4059
+ counter: n.counterAxisAlignItems
4060
+ };
4061
+ if (n.layoutWrap === "WRAP") {
4062
+ l.wrap = true;
4063
+ l.counterGap = round(n.counterAxisSpacing || 0);
4064
+ }
4065
+ if (n.layoutMode === "GRID") {
4066
+ l.columns = n.gridColumnCount;
4067
+ l.rows = n.gridRowCount;
4068
+ l.columnGap = round(n.gridColumnGap || 0);
4069
+ l.rowGap = round(n.gridRowGap || 0);
4070
+ }
4071
+ out.layout = l;
4072
+ }
4073
+ if ("children" in n && n.children.length && !out.vector) {
4074
+ out.children = [];
4075
+ for (let i = 0;i < n.children.length; i++) {
4076
+ if (ctx.count >= MAX_NODES5) {
4077
+ ctx.truncated = true;
4078
+ break;
4079
+ }
4080
+ out.children.push(await walk2(n.children[i], n, ctx));
4081
+ }
4082
+ }
4083
+ return out;
4084
+ }
4085
+ function textOf2(t) {
4086
+ const segs = t.getStyledTextSegments(["fontName", "fontSize", "fontWeight", "fills", "textDecoration", "textCase", "letterSpacing", "lineHeight"]);
4087
+ return {
4088
+ characters: t.characters,
4089
+ align: t.textAlignHorizontal,
4090
+ valign: t.textAlignVertical,
4091
+ autoResize: t.textAutoResize,
4092
+ maxLines: t.textTruncation === "ENDING" ? t.maxLines || 1 : 0,
4093
+ segments: segs.map(function(s) {
4094
+ const fill = s.fills.filter(function(f) {
4095
+ return f.visible !== false;
4096
+ })[0];
4097
+ const seg = { text: s.characters, family: s.fontName.family, style: s.fontName.style, weight: s.fontWeight, size: round(s.fontSize) };
4098
+ if (fill && fill.type === "SOLID") {
4099
+ seg.color = toHex(fill.color);
4100
+ if (fill.opacity !== undefined && fill.opacity < 1)
4101
+ seg.alpha = round(fill.opacity);
4102
+ }
4103
+ if (s.textDecoration !== "NONE")
4104
+ seg.decoration = s.textDecoration;
4105
+ if (s.textCase && s.textCase !== "ORIGINAL")
4106
+ seg.textCase = s.textCase;
4107
+ if (s.letterSpacing && s.letterSpacing.value)
4108
+ seg.letterSpacing = round(s.letterSpacing.unit === "PERCENT" ? s.letterSpacing.value / 100 * s.fontSize : s.letterSpacing.value);
4109
+ if (s.lineHeight && s.lineHeight.unit !== "AUTO")
4110
+ seg.lineHeight = round(s.lineHeight.unit === "PERCENT" ? s.lineHeight.value / 100 * s.fontSize : s.lineHeight.value);
4111
+ return seg;
4112
+ })
4113
+ };
4114
+ }
4115
+ async function robloxImages(p) {
4116
+ const items = Array.isArray(p.items) ? p.items : [];
4117
+ const out = [];
4118
+ for (let i = 0;i < items.length; i++) {
4119
+ const it = items[i];
4120
+ const scale = Math.max(1, Math.min(4, Number(it.scale) || 2));
4121
+ let temp = null;
4122
+ try {
4123
+ let node = await getNode(it.id);
4124
+ if (it.mode === "panel" || it.mode === "shadow") {
4125
+ temp = node.clone();
4126
+ if ("children" in temp)
4127
+ for (let k = temp.children.length - 1;k >= 0; k--)
4128
+ temp.children[k].remove();
4129
+ if (it.mode === "panel")
4130
+ temp.effects = [];
4131
+ else {
4132
+ temp.effects = node.effects.filter(function(e) {
4133
+ return e.type === "DROP_SHADOW" && e.visible !== false;
4134
+ });
4135
+ temp.strokes = [];
4136
+ }
4137
+ node = temp;
4138
+ }
4139
+ const bytes = await node.exportAsync({ format: "PNG", constraint: { type: "SCALE", value: scale } });
4140
+ const rb = node.absoluteRenderBounds || node.absoluteBoundingBox;
4141
+ const bb = node.absoluteBoundingBox;
4142
+ out.push({
4143
+ id: it.id,
4144
+ mode: it.mode || "full",
4145
+ scale,
4146
+ b64: figma.base64Encode(bytes),
4147
+ offset: rb && bb ? { x: round(rb.x - bb.x), y: round(rb.y - bb.y) } : { x: 0, y: 0 },
4148
+ size: rb ? { w: round(rb.width), h: round(rb.height) } : { w: round(node.width), h: round(node.height) }
4149
+ });
4150
+ } catch (e) {
4151
+ out.push({ id: it.id, mode: it.mode || "full", error: String(e.message || e) });
4152
+ } finally {
4153
+ if (temp)
4154
+ temp.remove();
4155
+ }
4156
+ }
4157
+ return { images: out };
4158
+ }
4159
+
3642
4160
  // plugin/lib/selection.ts
3643
4161
  var MAX_WAIT_MS = 120000;
3644
4162
  var waiters = {};
@@ -3708,7 +4226,7 @@
3708
4226
  finish(id, codeError("The user cancelled the selection request", "CANCELLED"));
3709
4227
  }
3710
4228
  // package.json
3711
- var version = "1.8.0";
4229
+ var version = "1.13.0";
3712
4230
 
3713
4231
  // plugin/code.ts
3714
4232
  var DEFAULT_SIZE = { width: 340, height: 540 };
@@ -3730,7 +4248,11 @@
3730
4248
  figma.ui.postMessage(msg);
3731
4249
  }
3732
4250
  function sessionInfo() {
3733
- return { id: sessionId, fileName: figma.root.name, page: figma.currentPage.name };
4251
+ let fileKey;
4252
+ try {
4253
+ fileKey = figma.fileKey;
4254
+ } catch (e) {}
4255
+ return { id: sessionId, fileName: figma.root.name, page: figma.currentPage.name, pageId: figma.currentPage.id, editorType: figma.editorType, fileKey };
3734
4256
  }
3735
4257
  function clampSize(w, h) {
3736
4258
  return {
@@ -3787,6 +4309,9 @@
3787
4309
  size = { width: DEFAULT_SIZE.width, height: DEFAULT_SIZE.height };
3788
4310
  applySize2();
3789
4311
  figma.clientStorage.setAsync("size", size);
4312
+ } else if (msg.t === "reopen") {
4313
+ figma.notify("Figma Bridge updated: run it again (Ctrl+Alt+P)", { timeout: 6000 });
4314
+ figma.closePlugin();
3790
4315
  } else if (msg.t === "notify")
3791
4316
  figma.notify(String(msg.text), { timeout: 2500 });
3792
4317
  else if (msg.t === "focus")
@@ -3832,10 +4357,20 @@
3832
4357
  prototype,
3833
4358
  annotate,
3834
4359
  export_tree: exportTree,
4360
+ node_info: nodeInfo,
4361
+ roblox_tree: robloxTree,
4362
+ roblox_images: robloxImages,
3835
4363
  ping: function() {
3836
4364
  return Promise.resolve({ pong: true, session: sessionInfo() });
3837
4365
  }
3838
4366
  };
4367
+ var EDITORS2 = {
4368
+ prototype: ["figma"],
4369
+ annotate: ["figma", "dev"],
4370
+ design_tokens: ["figma", "slides"],
4371
+ export_tokens: ["figma", "slides", "dev"]
4372
+ };
4373
+ var EDITOR_NAMES2 = { figma: "Figma Design", figjam: "FigJam", slides: "Figma Slides", dev: "Dev Mode" };
3839
4374
  var READ_ONLY = {
3840
4375
  describe: true,
3841
4376
  find: true,
@@ -3848,6 +4383,8 @@
3848
4383
  list_fonts: true,
3849
4384
  wait_for_selection: true,
3850
4385
  export_tree: true,
4386
+ node_info: true,
4387
+ roblox_tree: true,
3851
4388
  ping: true
3852
4389
  };
3853
4390
  async function handleRequest(msg) {
@@ -3860,6 +4397,14 @@
3860
4397
  const handler = HANDLERS[msg.method];
3861
4398
  if (!handler)
3862
4399
  throw codeError("Unknown method: " + msg.method + " (reopen the plugin after updating it)", "UNKNOWN_METHOD");
4400
+ const editors = EDITORS2[msg.method];
4401
+ if (editors && editors.indexOf(figma.editorType) === -1) {
4402
+ throw codeError(msg.method + " is not available in " + (EDITOR_NAMES2[figma.editorType] || figma.editorType) + " (works in " + editors.map(function(e) {
4403
+ return EDITOR_NAMES2[e];
4404
+ }).join(", ") + ").", "WRONG_EDITOR");
4405
+ }
4406
+ if (mutates && figma.editorType === "dev")
4407
+ throw codeError("Dev Mode is read-only: switch the file to Design mode to let the agent edit it.", "WRONG_EDITOR");
3863
4408
  const result = await handler(msg.params || {}, Math.min(Number(msg.timeoutMs) || 30000, 120000), String(msg.id));
3864
4409
  reply = { t: "res", id: msg.id, ok: true, result };
3865
4410
  } catch (e) {
@@ -4161,6 +4706,18 @@
4161
4706
  }
4162
4707
  };
4163
4708
  }
4709
+ async function nodeInfo(p) {
4710
+ const ids = Array.isArray(p.ids) ? p.ids.slice(0, 500) : [];
4711
+ const out = {};
4712
+ for (let i = 0;i < ids.length; i++) {
4713
+ const n = await figma.getNodeByIdAsync(String(ids[i]));
4714
+ if (!n)
4715
+ continue;
4716
+ const pg = pageOf(n);
4717
+ out[ids[i]] = { name: n.name, type: n.type, page: pg ? pg.name : undefined };
4718
+ }
4719
+ return out;
4720
+ }
4164
4721
  var fontCache = null;
4165
4722
  async function listFonts(p) {
4166
4723
  if (!fontCache)