@clovis500c/figma-bridge 1.13.0 → 1.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clovis500c/figma-bridge",
3
- "version": "1.13.0",
3
+ "version": "1.14.0",
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)
@@ -3077,7 +3351,7 @@
3077
3351
  }
3078
3352
 
3079
3353
  // plugin/lib/describe.ts
3080
- async function describe(p) {
3354
+ async function describe2(p) {
3081
3355
  let targets;
3082
3356
  if (p.nodeId)
3083
3357
  targets = [await getNode(p.nodeId)];
@@ -3922,7 +4196,7 @@
3922
4196
 
3923
4197
  // plugin/lib/roblox.ts
3924
4198
  var MAX_NODES5 = 2000;
3925
- var VECTORS = ["VECTOR", "BOOLEAN_OPERATION", "STAR", "POLYGON", "LINE"];
4199
+ var VECTORS2 = ["VECTOR", "BOOLEAN_OPERATION", "STAR", "POLYGON", "LINE"];
3926
4200
  async function robloxTree(p) {
3927
4201
  const node = p.nodeId ? await getNode(p.nodeId) : figma.currentPage.selection[0];
3928
4202
  if (!node)
@@ -3959,7 +4233,7 @@
3959
4233
  return out;
3960
4234
  }
3961
4235
  function vectorOnly2(n) {
3962
- if (VECTORS.indexOf(n.type) !== -1)
4236
+ if (VECTORS2.indexOf(n.type) !== -1)
3963
4237
  return true;
3964
4238
  if (n.type === "TEXT" || !("children" in n) || !n.children.length)
3965
4239
  return false;
@@ -4226,7 +4500,7 @@
4226
4500
  finish(id, codeError("The user cancelled the selection request", "CANCELLED"));
4227
4501
  }
4228
4502
  // package.json
4229
- var version = "1.13.0";
4503
+ var version = "1.14.0";
4230
4504
 
4231
4505
  // plugin/code.ts
4232
4506
  var DEFAULT_SIZE = { width: 340, height: 540 };
@@ -4340,7 +4614,7 @@
4340
4614
  var HANDLERS = {
4341
4615
  run_script: runScript,
4342
4616
  build,
4343
- describe,
4617
+ describe: describe2,
4344
4618
  find: find2,
4345
4619
  get_design_system: getDesignSystem,
4346
4620
  design_tokens: designTokens,
@@ -4591,7 +4865,7 @@
4591
4865
  },
4592
4866
  describe: async function(node, depth) {
4593
4867
  const id = node && typeof node === "object" ? node.id : node;
4594
- const out = await describe({ nodeId: id, depth });
4868
+ const out = await describe2({ nodeId: id, depth });
4595
4869
  return out.outline;
4596
4870
  }
4597
4871
  };