@clovis500c/figma-bridge 1.13.0 → 1.14.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clovis500c/figma-bridge",
3
- "version": "1.13.0",
3
+ "version": "1.14.2",
4
4
  "type": "module",
5
5
  "description": "Local MCP bridge that lets any AI agent design in the Figma desktop app, with no rate limits.",
6
6
  "keywords": [
package/plugin/code.js CHANGED
@@ -418,6 +418,304 @@
418
418
  return { count: out.length, nodes: out };
419
419
  }
420
420
 
421
+ // plugin/lib/naming.ts
422
+ var GENERIC_NAME = /^(frame|rectangle|ellipse|group|vector|text|line|polygon|star|component|instance|section|image|auto ?layout|union|subtract|intersect|exclude|boolean|layer|shape|slice|mask|container|wrapper|div|span|box)(\s*\d+)?(\s+copy(\s*\d+)?)?$/i;
423
+ function isGenericName(n) {
424
+ const name = n.name.trim();
425
+ if (!name || GENERIC_NAME.test(name))
426
+ return true;
427
+ if (n.kind === "text" && n.text !== undefined) {
428
+ const t = n.text.trim();
429
+ const a = name.toLowerCase();
430
+ const b = t.toLowerCase();
431
+ return a === b || a.length >= 12 && b.indexOf(a) === 0;
432
+ }
433
+ return false;
434
+ }
435
+ var NUMERIC = /^[\s$€£¥+\-−]*[\d][\d\s.,:%/×x+\-−]*[\s$€£¥%kKmMbB]*$/;
436
+ var SCREEN_W = [360, 375, 390, 393, 402, 412, 414, 428, 430, 440];
437
+ function visibleKids(n) {
438
+ return n.children.filter(function(c) {
439
+ return !c.hidden;
440
+ });
441
+ }
442
+ function maxFont(n) {
443
+ let m = n.kind === "text" && n.fontSize ? n.fontSize : 0;
444
+ for (let i = 0;i < n.children.length; i++) {
445
+ const c = n.children[i];
446
+ if (c && !c.hidden)
447
+ m = Math.max(m, maxFont(c));
448
+ }
449
+ return m;
450
+ }
451
+ function onlyKinds(list, kinds) {
452
+ for (let i = 0;i < list.length; i++) {
453
+ const c = list[i];
454
+ if (c && kinds.indexOf(c.kind) === -1)
455
+ return false;
456
+ }
457
+ return list.length > 0;
458
+ }
459
+ function buttonLike(n) {
460
+ const kids = visibleKids(n);
461
+ if (!n.background || n.h > 72 || kids.length === 0 || kids.length > 3)
462
+ return false;
463
+ let texts = 0;
464
+ for (let i = 0;i < kids.length; i++) {
465
+ const c = kids[i];
466
+ if (!c)
467
+ continue;
468
+ if (c.kind === "text") {
469
+ texts++;
470
+ if ((c.text || "").length > 32)
471
+ return false;
472
+ } else if (c.kind !== "vector" && c.kind !== "image")
473
+ return false;
474
+ }
475
+ return texts === 1;
476
+ }
477
+ function isScreen(n) {
478
+ return SCREEN_W.indexOf(Math.round(n.w)) !== -1 && n.h >= 640 || n.w >= 1024 && n.h >= 600;
479
+ }
480
+ function largestText(n, parent) {
481
+ const others = parent ? visibleKids(parent).filter(function(c) {
482
+ return c !== n && c.kind === "text";
483
+ }) : [];
484
+ const size = n.fontSize || 14;
485
+ return others.length > 0 && others.every(function(c) {
486
+ return (c.fontSize || 14) < size;
487
+ });
488
+ }
489
+ function covers(n, p) {
490
+ return Math.abs(n.x) <= 1 && Math.abs(n.y) <= 1 && n.w >= p.w - 1 && n.h >= p.h - 1;
491
+ }
492
+ function roleOf(n, parent, root, top, inButton) {
493
+ const kids = visibleKids(n);
494
+ if (n.kind === "text") {
495
+ const t = (n.text || "").trim();
496
+ const size = n.fontSize || 14;
497
+ if (inButton)
498
+ return "Label";
499
+ if (NUMERIC.test(t))
500
+ return "Value";
501
+ if (size >= top - 0.5 && size >= 18)
502
+ return "Title";
503
+ if (size >= 16 && largestText(n, parent))
504
+ return "Title";
505
+ if (size >= 20)
506
+ return "Heading";
507
+ if (t.length > 60)
508
+ return "Description";
509
+ if (size <= 12)
510
+ return "Caption";
511
+ if (t.length > 30)
512
+ return "Description";
513
+ return "Label";
514
+ }
515
+ if (n.kind === "vector")
516
+ return "Icon";
517
+ if (n.kind === "image" && kids.length === 0) {
518
+ const round = n.ellipse || n.radius !== undefined && Math.abs(n.w - n.h) <= 1 && n.radius >= n.w / 2 - 1;
519
+ return round ? "Avatar" : "Image";
520
+ }
521
+ if (kids.length === 0) {
522
+ if (Math.min(n.w, n.h) <= 2 && Math.max(n.w, n.h) > 8)
523
+ return "Divider";
524
+ if (parent && covers(n, parent))
525
+ return "Background";
526
+ if (n.clickable)
527
+ return "Button";
528
+ if (n.ellipse)
529
+ return n.w <= 16 ? "Dot" : "Circle";
530
+ return "Shape";
531
+ }
532
+ if (n.clickable)
533
+ return "Button";
534
+ if (buttonLike(n)) {
535
+ const label = kids.filter(function(c) {
536
+ return c.kind === "text";
537
+ })[0];
538
+ return n.h <= 24 || label && (label.fontSize || 14) <= 12 ? "Badge" : "Button";
539
+ }
540
+ if (!parent) {
541
+ if (isScreen(n))
542
+ return "Screen";
543
+ } else if (parent === root && isScreen(root)) {
544
+ if (n.w >= root.w * 0.9 && n.y <= root.h * 0.1 && n.h <= root.h * 0.25)
545
+ return "Header";
546
+ if (n.w >= root.w * 0.9 && n.y + n.h >= root.h * 0.9 && n.h <= root.h * 0.25)
547
+ return "Footer";
548
+ if (n.h >= root.h * 0.9 && n.w <= root.w * 0.35 && n.x <= 1)
549
+ return "Sidebar";
550
+ }
551
+ const flow = kids.filter(function(c) {
552
+ return !c.absolute;
553
+ });
554
+ if (flow.length >= 2) {
555
+ const r = baseName(flow[0]);
556
+ const same = flow.every(function(c) {
557
+ return baseName(c) === r;
558
+ });
559
+ if (same && r && r !== "Label" && r !== "Value" && r !== "Divider") {
560
+ if (r === "Button")
561
+ return "Actions";
562
+ return r + (n.layout === "GRID" || n.wrap ? "Grid" : "List");
563
+ }
564
+ }
565
+ if (n.background)
566
+ return n.radius ? "Card" : "Panel";
567
+ if (onlyKinds(kids, ["text"]))
568
+ return "TextGroup";
569
+ if (n.layout === "HORIZONTAL")
570
+ return "Row";
571
+ if (n.layout === "GRID")
572
+ return "Grid";
573
+ return "Container";
574
+ }
575
+ function baseName(n) {
576
+ return pascalName(n.role || n.name).replace(/\d+$/, "");
577
+ }
578
+ function inferRoles(root) {
579
+ const top = maxFont(root);
580
+ const walk = function(n, parent, inButton) {
581
+ const button = n.kind !== "text" && (n.clickable || buttonLike(n) || /button|btn/i.test(n.name));
582
+ for (let i = 0;i < n.children.length; i++) {
583
+ const c = n.children[i];
584
+ if (c)
585
+ walk(c, n, inButton || button);
586
+ }
587
+ if (isGenericName(n))
588
+ n.role = roleOf(n, parent, root, top, inButton);
589
+ };
590
+ walk(root, null, false);
591
+ }
592
+ function pascalName(name) {
593
+ let segs = name.split("/").map(function(s) {
594
+ return s.replace(/^[\w-]+:/, "").trim();
595
+ }).filter(function(s) {
596
+ return s && !/^default$/i.test(s);
597
+ });
598
+ if (segs.length > 1)
599
+ segs = segs.slice(1).concat([segs[0]]);
600
+ const words = segs.join(" ").normalize("NFKD").replace(/[̀-ͯ]/g, "").replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(/[^A-Za-z0-9]+/).filter(Boolean);
601
+ const out = words.map(function(w) {
602
+ return w === w.toUpperCase() && w.length <= 4 ? w : w.charAt(0).toUpperCase() + w.slice(1).toLowerCase();
603
+ }).join("");
604
+ return out.slice(0, 48);
605
+ }
606
+ var ROLE_WORDS = ("Actions Avatar Background Badge Banner Bar Body Button Card Column Container Content Controls Details Dialog Divider Footer Frame Grid Group Gui " + "Header Hero Hud HUD Info Item List Menu Modal Nav Navbar Navigation Overlay Panel Popup Row Screen Section Sidebar Slot Stack Stats Tab Tabs Tile " + "Toolbar Tooltip Window Wrapper").split(" ");
607
+
608
+ // plugin/lib/roles.ts
609
+ var VECTORS = ["VECTOR", "BOOLEAN_OPERATION", "STAR", "POLYGON", "LINE"];
610
+ function visiblePaints(list) {
611
+ if (!list || list === figma.mixed)
612
+ return [];
613
+ return list.filter(function(p) {
614
+ return p.visible !== false && (p.opacity === undefined || p.opacity > 0);
615
+ });
616
+ }
617
+ function vectorOnly(n) {
618
+ if (VECTORS.indexOf(n.type) !== -1)
619
+ return true;
620
+ if (n.type === "TEXT" || !("children" in n) || !n.children.length)
621
+ return false;
622
+ for (let i = 0;i < n.children.length; i++)
623
+ if (n.children[i].visible !== false && !vectorOnly(n.children[i]))
624
+ return false;
625
+ return true;
626
+ }
627
+ function fontSizeOf(t) {
628
+ if (typeof t.fontSize === "number")
629
+ return t.fontSize;
630
+ let max = 0;
631
+ try {
632
+ const segs = t.getStyledTextSegments(["fontSize"]);
633
+ for (let i = 0;i < segs.length; i++)
634
+ max = Math.max(max, segs[i].fontSize);
635
+ } catch (e) {}
636
+ return max || 14;
637
+ }
638
+ function describe(n, depth) {
639
+ const fills = "fills" in n ? visiblePaints(n.fills) : [];
640
+ const strokes = "strokes" in n ? visiblePaints(n.strokes) : [];
641
+ const kids = "children" in n && n.type !== "TEXT" && depth < 12 ? n.children : [];
642
+ const image = fills.length === 1 && fills[0].type === "IMAGE";
643
+ let kind = "shape";
644
+ if (n.type === "TEXT")
645
+ kind = "text";
646
+ else if (vectorOnly(n))
647
+ kind = "vector";
648
+ else if (image && !kids.length)
649
+ kind = "image";
650
+ else if (kids.length || n.layoutMode && n.layoutMode !== "NONE")
651
+ kind = "frame";
652
+ let radius = 0;
653
+ if (typeof n.cornerRadius === "number")
654
+ radius = n.cornerRadius;
655
+ else if ("topLeftRadius" in n)
656
+ radius = Math.max(n.topLeftRadius, n.topRightRadius, n.bottomRightRadius, n.bottomLeftRadius);
657
+ const r = {
658
+ node: n,
659
+ kind,
660
+ name: n.name,
661
+ x: n.x || 0,
662
+ y: n.y || 0,
663
+ w: n.width || 0,
664
+ h: n.height || 0,
665
+ hidden: n.visible === false,
666
+ background: fills.length > 0 || strokes.length > 0,
667
+ radius,
668
+ ellipse: n.type === "ELLIPSE",
669
+ clickable: !!(n.reactions && n.reactions.some(function(x) {
670
+ return x.trigger && (x.trigger.type === "ON_CLICK" || x.trigger.type === "ON_PRESS");
671
+ })),
672
+ absolute: n.layoutPositioning === "ABSOLUTE",
673
+ children: []
674
+ };
675
+ if (n.type === "TEXT") {
676
+ r.text = n.characters;
677
+ r.fontSize = fontSizeOf(n);
678
+ }
679
+ if (n.layoutMode && n.layoutMode !== "NONE")
680
+ r.layout = n.layoutMode;
681
+ if (n.layoutWrap === "WRAP")
682
+ r.wrap = true;
683
+ if (kind !== "vector") {
684
+ for (let i = 0;i < kids.length; i++)
685
+ r.children.push(describe(kids[i], depth + 1));
686
+ }
687
+ return r;
688
+ }
689
+ function roleNames(root) {
690
+ const tree = describe(root, 0);
691
+ inferRoles(tree);
692
+ const out = {};
693
+ const walk = function(r) {
694
+ if (r.role)
695
+ out[r.node.id] = r.role;
696
+ for (let i = 0;i < r.children.length; i++)
697
+ walk(r.children[i]);
698
+ };
699
+ walk(tree);
700
+ return out;
701
+ }
702
+ function applyRoleNames(root, rename) {
703
+ const names = roleNames(root);
704
+ let count = 0;
705
+ const walk = function(n) {
706
+ const role = names[n.id];
707
+ if (role && rename(n) && n.name !== role) {
708
+ n.name = role;
709
+ count++;
710
+ }
711
+ if ("children" in n && n.type !== "INSTANCE")
712
+ for (let i = 0;i < n.children.length; i++)
713
+ walk(n.children[i]);
714
+ };
715
+ walk(root);
716
+ return count;
717
+ }
718
+
421
719
  // plugin/lib/health.ts
422
720
  var MAX_NODES = 20000;
423
721
  var MAX_ISSUES = 100;
@@ -829,6 +1127,15 @@
829
1127
  }
830
1128
  const changes = [];
831
1129
  const counts = { colors: 0, numbers: 0, textStyles: 0, names: 0 };
1130
+ const roleCache = {};
1131
+ const roleFor = function(n) {
1132
+ let top = n;
1133
+ while (top.parent && top.parent.type !== "PAGE" && top.parent.type !== "DOCUMENT" && top.parent.type !== "SECTION")
1134
+ top = top.parent;
1135
+ if (!roleCache[top.id])
1136
+ roleCache[top.id] = roleNames(top);
1137
+ return roleCache[top.id][n.id] || null;
1138
+ };
832
1139
  const record = function(n, fix, from, to) {
833
1140
  counts[fix]++;
834
1141
  if (changes.length < 200)
@@ -935,7 +1242,7 @@
935
1242
  }
936
1243
  }
937
1244
  if (fixes.indexOf("names") !== -1 && n.type !== "TEXT" && DEFAULT_NAME.test(n.name)) {
938
- const name = nameFromContent(n);
1245
+ const name = roleFor(n);
939
1246
  if (name && name !== n.name) {
940
1247
  const from = n.name;
941
1248
  n.name = name;
@@ -945,52 +1252,6 @@
945
1252
  }
946
1253
  return { scope: scope.label, fixed: counts, changes, truncated: scope.truncated || changes.length >= 200 };
947
1254
  }
948
- function firstText(n, depth) {
949
- if (n.type === "TEXT")
950
- return n.characters;
951
- if (depth > 3 || !("children" in n))
952
- return "";
953
- for (let i = 0;i < n.children.length; i++) {
954
- if (n.children[i].visible === false)
955
- continue;
956
- const t = firstText(n.children[i], depth + 1);
957
- if (t.trim())
958
- return t;
959
- }
960
- return "";
961
- }
962
- function hasImage(n) {
963
- return "fills" in n && n.fills !== figma.mixed && n.fills.some(function(f) {
964
- return f.type === "IMAGE" && f.visible !== false;
965
- });
966
- }
967
- function vectorOnly(n) {
968
- if (n.type === "VECTOR" || n.type === "BOOLEAN_OPERATION" || n.type === "STAR" || n.type === "POLYGON")
969
- return true;
970
- if (!("children" in n) || !n.children.length || n.type === "TEXT")
971
- return false;
972
- for (let i = 0;i < n.children.length; i++)
973
- if (!vectorOnly(n.children[i]))
974
- return false;
975
- return true;
976
- }
977
- function nameFromContent(n) {
978
- if (hasImage(n))
979
- return "Image";
980
- if (n.type === "VECTOR" || (n.type === "GROUP" || n.type === "FRAME") && vectorOnly(n))
981
- return "Icon";
982
- const text = firstText(n, 0).split(`
983
- `)[0].trim();
984
- if (text)
985
- return text.length > 32 ? text.slice(0, 31) + "…" : text;
986
- if (n.layoutMode === "HORIZONTAL")
987
- return "Row";
988
- if (n.layoutMode === "VERTICAL")
989
- return "Column";
990
- if (n.layoutMode === "GRID")
991
- return "Grid";
992
- return null;
993
- }
994
1255
 
995
1256
  // plugin/lib/audit.ts
996
1257
  var MAX_NODES2 = 5000;
@@ -1958,6 +2219,7 @@
1958
2219
  reactions: [],
1959
2220
  connectors: [],
1960
2221
  keys: {},
2222
+ unnamed: {},
1961
2223
  requestId,
1962
2224
  total: countNodes(roots),
1963
2225
  lastProgress: 0
@@ -1981,6 +2243,15 @@
1981
2243
  }
1982
2244
  made.push(node);
1983
2245
  }
2246
+ for (let i = 0;i < made.length; i++) {
2247
+ try {
2248
+ applyRoleNames(made[i], function(n) {
2249
+ return !!ctx.unnamed[n.id];
2250
+ });
2251
+ } catch (e) {
2252
+ ctx.warnings.push("Layer names: " + (e.message || e));
2253
+ }
2254
+ }
1984
2255
  if (ctx.reactions.length)
1985
2256
  await applyBuildReactions(ctx.reactions, ctx.ids, ctx.warnings);
1986
2257
  const connectors = await createConnectors(ctx);
@@ -2173,6 +2444,8 @@
2173
2444
  node.name = String(s.name);
2174
2445
  else if (type === "icon")
2175
2446
  node.name = "icon/" + s.icon;
2447
+ else if (NAMED_BY_ROLE.indexOf(type) !== -1)
2448
+ ctx.unnamed[node.id] = true;
2176
2449
  if (type === "frame" || type === "component")
2177
2450
  await setupFrame(node, s, ctx, path);
2178
2451
  else if (type === "slide")
@@ -2241,6 +2514,7 @@
2241
2514
  }
2242
2515
  return node;
2243
2516
  }
2517
+ var NAMED_BY_ROLE = ["frame", "component", "text", "rect", "ellipse", "line", "image", "svg"];
2244
2518
  var FIXED_LOOK = ["sticky", "shape", "table", "codeblock"];
2245
2519
  async function setupFrame(f, s, ctx, path, keepFills) {
2246
2520
  if (!keepFills)
@@ -2325,6 +2599,8 @@
2325
2599
  if (mode === "GRID") {
2326
2600
  if (await setupGrid(f, s, ctx, path))
2327
2601
  return;
2602
+ if (Array.isArray(s.gridSize))
2603
+ return placeGridItems(f, s);
2328
2604
  mode = "HORIZONTAL";
2329
2605
  s = Object.assign({}, s, { wrap: true, rowGap: s.rowGap !== undefined ? s.rowGap : s.gap, gap: s.columnGap !== undefined ? s.columnGap : s.gap });
2330
2606
  }
@@ -2369,7 +2645,28 @@
2369
2645
  return { type: "FIXED", value: parseFloat(px[1]) };
2370
2646
  return fallback === "FLEX" ? { type: "FLEX", value: 1 } : { type: "HUG" };
2371
2647
  }
2648
+ var GRID_FALLBACK = `Figma's grid track API failed (a Figma bug: "invalid id"): grids were built as wrapping rows, or at their measured positions for imported pages.`;
2649
+ var gridTracksBroken = false;
2650
+ function tracksReadable(f) {
2651
+ try {
2652
+ const t = f.gridColumnSizes[0];
2653
+ return !!t && typeof t.type === "string";
2654
+ } catch (e) {
2655
+ return false;
2656
+ }
2657
+ }
2372
2658
  async function setupGrid(f, s, ctx, path) {
2659
+ const fallback = function() {
2660
+ gridTracksBroken = true;
2661
+ try {
2662
+ f.layoutMode = "NONE";
2663
+ } catch (e) {}
2664
+ if (ctx.warnings.indexOf(GRID_FALLBACK) === -1)
2665
+ ctx.warnings.push(GRID_FALLBACK);
2666
+ return false;
2667
+ };
2668
+ if (gridTracksBroken)
2669
+ return fallback();
2373
2670
  try {
2374
2671
  f.layoutMode = "GRID";
2375
2672
  } catch (e) {
@@ -2399,14 +2696,16 @@
2399
2696
  set("rows", function() {
2400
2697
  f.gridRowCount = rowCount;
2401
2698
  });
2402
- set("column sizes", function() {
2699
+ if (!tracksReadable(f))
2700
+ return fallback();
2701
+ try {
2403
2702
  for (let i = 0;i < colCount; i++)
2404
- applyTrack(f.gridColumnSizes[i], track(cols ? cols[i] : undefined, fixedW ? "FLEX" : "HUG"));
2405
- });
2406
- set("row sizes", function() {
2703
+ applyTrack(f, "gridColumnSizes", i, track(cols ? cols[i] : undefined, fixedW ? "FLEX" : "HUG"));
2407
2704
  for (let i = 0;i < rowCount; i++)
2408
- applyTrack(f.gridRowSizes[i], track(rows ? rows[i] : undefined, fixedH ? "FLEX" : "HUG"));
2409
- });
2705
+ applyTrack(f, "gridRowSizes", i, track(rows ? rows[i] : undefined, fixedH ? "FLEX" : "HUG"));
2706
+ } catch (e) {
2707
+ return fallback();
2708
+ }
2410
2709
  if (!fixedW)
2411
2710
  set("sizing", function() {
2412
2711
  f.layoutSizingHorizontal = "HUG";
@@ -2429,12 +2728,30 @@
2429
2728
  await setPadding(f, s.padding);
2430
2729
  return true;
2431
2730
  }
2432
- function applyTrack(t, spec) {
2433
- if (!t)
2731
+ function placeGridItems(f, s) {
2732
+ if (s.w === "hug" || s.w === undefined)
2733
+ s.w = s.gridSize[0];
2734
+ if (s.h === "hug" || s.h === undefined)
2735
+ s.h = s.gridSize[1];
2736
+ const kids = Array.isArray(s.children) ? s.children : [];
2737
+ for (let i = 0;i < kids.length; i++) {
2738
+ const p = kids[i] && kids[i].place;
2739
+ if (!Array.isArray(p))
2740
+ continue;
2741
+ kids[i].x = p[0];
2742
+ kids[i].y = p[1];
2743
+ kids[i].w = p[2];
2744
+ if (kids[i].type !== "text" || kids[i].h !== undefined)
2745
+ kids[i].h = p[3];
2746
+ }
2747
+ }
2748
+ function applyTrack(f, field, i, spec) {
2749
+ if (!f[field][i])
2434
2750
  return;
2435
- t.type = spec.type;
2436
- if (spec.value !== undefined && spec.type !== "HUG")
2437
- t.value = spec.value;
2751
+ if (f[field][i].type !== spec.type)
2752
+ f[field][i].type = spec.type;
2753
+ if (spec.value !== undefined && spec.type !== "HUG" && f[field][i].value !== spec.value)
2754
+ f[field][i].value = spec.value;
2438
2755
  }
2439
2756
  function spanOf(s, axis) {
2440
2757
  if (!s || typeof s !== "object")
@@ -2853,10 +3170,13 @@
2853
3170
  const right = v[1] === undefined ? v[0] : v[1];
2854
3171
  const bottom = v[2] === undefined ? v[0] : v[2];
2855
3172
  const left = v[3] === undefined ? right : v[3];
2856
- await setNumber(f, "paddingTop", top);
2857
- await setNumber(f, "paddingRight", right);
2858
- await setNumber(f, "paddingBottom", bottom);
2859
- await setNumber(f, "paddingLeft", left);
3173
+ const clamp = function(x) {
3174
+ return typeof x === "number" ? Math.max(0, x) : x;
3175
+ };
3176
+ await setNumber(f, "paddingTop", clamp(top));
3177
+ await setNumber(f, "paddingRight", clamp(right));
3178
+ await setNumber(f, "paddingBottom", clamp(bottom));
3179
+ await setNumber(f, "paddingLeft", clamp(left));
2860
3180
  }
2861
3181
  function lineHeight(v) {
2862
3182
  if (v === "auto")
@@ -3077,7 +3397,7 @@
3077
3397
  }
3078
3398
 
3079
3399
  // plugin/lib/describe.ts
3080
- async function describe(p) {
3400
+ async function describe2(p) {
3081
3401
  let targets;
3082
3402
  if (p.nodeId)
3083
3403
  targets = [await getNode(p.nodeId)];
@@ -3922,7 +4242,7 @@
3922
4242
 
3923
4243
  // plugin/lib/roblox.ts
3924
4244
  var MAX_NODES5 = 2000;
3925
- var VECTORS = ["VECTOR", "BOOLEAN_OPERATION", "STAR", "POLYGON", "LINE"];
4245
+ var VECTORS2 = ["VECTOR", "BOOLEAN_OPERATION", "STAR", "POLYGON", "LINE"];
3926
4246
  async function robloxTree(p) {
3927
4247
  const node = p.nodeId ? await getNode(p.nodeId) : figma.currentPage.selection[0];
3928
4248
  if (!node)
@@ -3959,7 +4279,7 @@
3959
4279
  return out;
3960
4280
  }
3961
4281
  function vectorOnly2(n) {
3962
- if (VECTORS.indexOf(n.type) !== -1)
4282
+ if (VECTORS2.indexOf(n.type) !== -1)
3963
4283
  return true;
3964
4284
  if (n.type === "TEXT" || !("children" in n) || !n.children.length)
3965
4285
  return false;
@@ -4040,7 +4360,13 @@
4040
4360
  const image = figma.getImageByHash(h);
4041
4361
  if (!image)
4042
4362
  continue;
4043
- const bytes = await image.getBytesAsync();
4363
+ let bytes;
4364
+ try {
4365
+ bytes = await image.getBytesAsync();
4366
+ } catch (e) {
4367
+ out.rasterize = true;
4368
+ continue;
4369
+ }
4044
4370
  if (ctx.imageBytes + bytes.length > 40 << 20)
4045
4371
  continue;
4046
4372
  ctx.imageBytes += bytes.length;
@@ -4112,22 +4438,39 @@
4112
4438
  })
4113
4439
  };
4114
4440
  }
4441
+ function withTimeout(p, ms, message) {
4442
+ return new Promise(function(resolve, reject) {
4443
+ const timer = setTimeout(function() {
4444
+ reject(new Error(message));
4445
+ }, ms);
4446
+ p.then(function(v) {
4447
+ clearTimeout(timer);
4448
+ resolve(v);
4449
+ }, function(e) {
4450
+ clearTimeout(timer);
4451
+ reject(e);
4452
+ });
4453
+ });
4454
+ }
4115
4455
  async function robloxImages(p) {
4116
4456
  const items = Array.isArray(p.items) ? p.items : [];
4117
4457
  const out = [];
4118
4458
  for (let i = 0;i < items.length; i++) {
4119
4459
  const it = items[i];
4120
- const scale = Math.max(1, Math.min(4, Number(it.scale) || 2));
4460
+ let scale = Math.max(1, Math.min(4, Number(it.scale) || 2));
4121
4461
  let temp = null;
4122
4462
  try {
4123
4463
  let node = await getNode(it.id);
4124
4464
  if (it.mode === "panel" || it.mode === "shadow") {
4125
4465
  temp = node.clone();
4466
+ figma.currentPage.appendChild(temp);
4126
4467
  if ("children" in temp)
4127
4468
  for (let k = temp.children.length - 1;k >= 0; k--)
4128
4469
  temp.children[k].remove();
4129
4470
  if (it.mode === "panel")
4130
- temp.effects = [];
4471
+ temp.effects = node.effects.filter(function(e) {
4472
+ return e.type !== "DROP_SHADOW";
4473
+ });
4131
4474
  else {
4132
4475
  temp.effects = node.effects.filter(function(e) {
4133
4476
  return e.type === "DROP_SHADOW" && e.visible !== false;
@@ -4136,7 +4479,10 @@
4136
4479
  }
4137
4480
  node = temp;
4138
4481
  }
4139
- const bytes = await node.exportAsync({ format: "PNG", constraint: { type: "SCALE", value: scale } });
4482
+ const box = node.absoluteRenderBounds || node.absoluteBoundingBox;
4483
+ if (box)
4484
+ scale = Math.min(scale, Math.floor(1024 / Math.max(1, box.width, box.height) * 1000) / 1000);
4485
+ const bytes = await withTimeout(node.exportAsync({ format: "PNG", constraint: { type: "SCALE", value: scale } }), 20000, "export timed out");
4140
4486
  const rb = node.absoluteRenderBounds || node.absoluteBoundingBox;
4141
4487
  const bb = node.absoluteBoundingBox;
4142
4488
  out.push({
@@ -4226,7 +4572,7 @@
4226
4572
  finish(id, codeError("The user cancelled the selection request", "CANCELLED"));
4227
4573
  }
4228
4574
  // package.json
4229
- var version = "1.13.0";
4575
+ var version = "1.14.2";
4230
4576
 
4231
4577
  // plugin/code.ts
4232
4578
  var DEFAULT_SIZE = { width: 340, height: 540 };
@@ -4340,7 +4686,7 @@
4340
4686
  var HANDLERS = {
4341
4687
  run_script: runScript,
4342
4688
  build,
4343
- describe,
4689
+ describe: describe2,
4344
4690
  find: find2,
4345
4691
  get_design_system: getDesignSystem,
4346
4692
  design_tokens: designTokens,
@@ -4591,7 +4937,7 @@
4591
4937
  },
4592
4938
  describe: async function(node, depth) {
4593
4939
  const id = node && typeof node === "object" ? node.id : node;
4594
- const out = await describe({ nodeId: id, depth });
4940
+ const out = await describe2({ nodeId: id, depth });
4595
4941
  return out.outline;
4596
4942
  }
4597
4943
  };
package/plugin/ui.html CHANGED
@@ -523,7 +523,8 @@ const partials = new Map();
523
523
  function encode(msg) {
524
524
  const text = JSON.stringify(msg);
525
525
  if (text.length <= CHUNK) return [text];
526
- const cid = crypto.randomUUID();
526
+ // Not crypto.randomUUID: the plugin iframe is not a secure context, where it is undefined.
527
+ const cid = Date.now().toString(36) + Math.random().toString(36).slice(2);
527
528
  const n = Math.ceil(text.length / CHUNK);
528
529
  const frames = [];
529
530
  for (let i = 0; i < n; i++) frames.push(JSON.stringify({ t: "chunk", cid, i, n, d: text.slice(i * CHUNK, (i + 1) * CHUNK) }));
@@ -541,7 +542,15 @@ function decode(raw) {
541
542
  }
542
543
  function send(msg) {
543
544
  if (!ws || ws.readyState !== WebSocket.OPEN) return false;
544
- for (const f of encode(msg)) ws.send(f);
545
+ let frames;
546
+ try {
547
+ frames = encode(msg);
548
+ } catch (e) {
549
+ // A reply that can't be encoded still answers the request, so the agent doesn't wait for a timeout.
550
+ if (msg.t !== "res") return false;
551
+ frames = encode({ t: "res", id: msg.id, ok: false, error: "Could not send the result: " + ((e && e.message) || e), code: "PLUGIN_ERROR" });
552
+ }
553
+ for (const f of frames) ws.send(f);
545
554
  return true;
546
555
  }
547
556