@clovis500c/figma-bridge 1.5.0 → 1.12.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
@@ -255,6 +255,67 @@
255
255
  p = p.parent;
256
256
  return p;
257
257
  }
258
+ function toLab(c) {
259
+ const lin = function(v) {
260
+ return v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
261
+ };
262
+ const r = lin(c.r);
263
+ const g = lin(c.g);
264
+ const b = lin(c.b);
265
+ const x = (0.4124564 * r + 0.3575761 * g + 0.1804375 * b) / 0.95047;
266
+ const y = 0.2126729 * r + 0.7151522 * g + 0.072175 * b;
267
+ const z = (0.0193339 * r + 0.119192 * g + 0.9503041 * b) / 1.08883;
268
+ const f = function(t) {
269
+ return t > 216 / 24389 ? Math.cbrt(t) : (24389 / 27 * t + 16) / 116;
270
+ };
271
+ return [116 * f(y) - 16, 500 * (f(x) - f(y)), 200 * (f(y) - f(z))];
272
+ }
273
+ function deltaE(a, b) {
274
+ const l1 = toLab(a);
275
+ const l2 = toLab(b);
276
+ const rad = Math.PI / 180;
277
+ const c1 = Math.sqrt(l1[1] * l1[1] + l1[2] * l1[2]);
278
+ const c2 = Math.sqrt(l2[1] * l2[1] + l2[2] * l2[2]);
279
+ const cm = (c1 + c2) / 2;
280
+ const g = 0.5 * (1 - Math.sqrt(Math.pow(cm, 7) / (Math.pow(cm, 7) + Math.pow(25, 7))));
281
+ const a1 = l1[1] * (1 + g);
282
+ const a2 = l2[1] * (1 + g);
283
+ const cp1 = Math.sqrt(a1 * a1 + l1[2] * l1[2]);
284
+ const cp2 = Math.sqrt(a2 * a2 + l2[2] * l2[2]);
285
+ const hue = function(x, y) {
286
+ if (x === 0 && y === 0)
287
+ return 0;
288
+ const h = Math.atan2(y, x) / rad;
289
+ return h < 0 ? h + 360 : h;
290
+ };
291
+ const h1 = hue(a1, l1[2]);
292
+ const h2 = hue(a2, l2[2]);
293
+ const dl = l2[0] - l1[0];
294
+ const dc = cp2 - cp1;
295
+ let dh = 0;
296
+ if (cp1 * cp2 !== 0) {
297
+ dh = h2 - h1;
298
+ if (dh > 180)
299
+ dh -= 360;
300
+ else if (dh < -180)
301
+ dh += 360;
302
+ }
303
+ const dH = 2 * Math.sqrt(cp1 * cp2) * Math.sin(dh / 2 * rad);
304
+ const lm = (l1[0] + l2[0]) / 2;
305
+ const cpm = (cp1 + cp2) / 2;
306
+ let hm = h1 + h2;
307
+ if (cp1 * cp2 !== 0) {
308
+ if (Math.abs(h1 - h2) > 180)
309
+ hm += h1 + h2 < 360 ? 360 : -360;
310
+ hm /= 2;
311
+ }
312
+ const t = 1 - 0.17 * Math.cos((hm - 30) * rad) + 0.24 * Math.cos(2 * hm * rad) + 0.32 * Math.cos((3 * hm + 6) * rad) - 0.2 * Math.cos((4 * hm - 63) * rad);
313
+ const sl = 1 + 0.015 * Math.pow(lm - 50, 2) / Math.sqrt(20 + Math.pow(lm - 50, 2));
314
+ const sc = 1 + 0.045 * cpm;
315
+ const sh = 1 + 0.015 * cpm * t;
316
+ const rt = -2 * Math.sqrt(Math.pow(cpm, 7) / (Math.pow(cpm, 7) + Math.pow(25, 7))) * Math.sin(60 * Math.exp(-Math.pow((hm - 275) / 25, 2)) * rad);
317
+ return Math.sqrt(Math.pow(dl / sl, 2) + Math.pow(dc / sc, 2) + Math.pow(dH / sh, 2) + rt * (dc / sc) * (dH / sh));
318
+ }
258
319
 
259
320
  // plugin/lib/annotate.ts
260
321
  var COLORS = ["yellow", "orange", "red", "pink", "violet", "blue", "teal", "green"];
@@ -357,11 +418,596 @@
357
418
  return { count: out.length, nodes: out };
358
419
  }
359
420
 
421
+ // plugin/lib/health.ts
422
+ var MAX_NODES = 20000;
423
+ var MAX_ISSUES = 100;
424
+ var WEIGHTS2 = { tokens: 25, contrast: 20, typography: 15, components: 15, styles: 15, naming: 10 };
425
+ var NUMBER_FIELDS = ["itemSpacing", "counterAxisSpacing", "paddingTop", "paddingRight", "paddingBottom", "paddingLeft"];
426
+ var RADIUS_FIELDS = ["topLeftRadius", "topRightRadius", "bottomRightRadius", "bottomLeftRadius"];
427
+ async function scopeNodes(p, wholeFile, requestId) {
428
+ let roots;
429
+ let label;
430
+ if (p.nodeId) {
431
+ const n = await getNode(p.nodeId);
432
+ roots = [n];
433
+ label = n.name;
434
+ } else if (wholeFile) {
435
+ await figma.loadAllPagesAsync();
436
+ roots = figma.root.children.slice();
437
+ label = "the whole file";
438
+ } else if (figma.currentPage.selection.length) {
439
+ roots = figma.currentPage.selection.slice();
440
+ label = "the selection";
441
+ } else {
442
+ roots = figma.currentPage.children.slice();
443
+ label = "page " + figma.currentPage.name;
444
+ }
445
+ const nodes = [];
446
+ let truncated = false;
447
+ const walk = function(n) {
448
+ if (nodes.length >= MAX_NODES) {
449
+ truncated = true;
450
+ return;
451
+ }
452
+ if (n.type !== "PAGE" && n.type !== "DOCUMENT")
453
+ nodes.push(n);
454
+ if (n.type === "INSTANCE" || !("children" in n))
455
+ return;
456
+ if (n.type === "PAGE" && n.name === "⟲ Bridge checkpoints")
457
+ return;
458
+ for (let i = 0;i < n.children.length; i++)
459
+ walk(n.children[i]);
460
+ };
461
+ for (let i = 0;i < roots.length; i++) {
462
+ checkCancelled(requestId);
463
+ walk(roots[i]);
464
+ }
465
+ return { nodes, truncated, label };
466
+ }
467
+ function visibleSolids(paints) {
468
+ if (!paints || paints === figma.mixed)
469
+ return [];
470
+ return paints.filter(function(pt) {
471
+ return pt.visible !== false && pt.type === "SOLID";
472
+ });
473
+ }
474
+ function score(good, total) {
475
+ return total ? Math.round(good / total * 100) : 100;
476
+ }
477
+ function convention(name) {
478
+ const seg = name.split("/").pop() || name;
479
+ if (/^[a-z0-9]+(-[a-z0-9]+)+$/.test(seg))
480
+ return "kebab-case";
481
+ if (/^[a-z0-9]+(_[a-z0-9]+)+$/.test(seg))
482
+ return "snake_case";
483
+ if (/^[a-z]+[A-Z][A-Za-z0-9]*$/.test(seg))
484
+ return "camelCase";
485
+ if (/^[A-Z][a-z0-9]*( [A-Z0-9][a-z0-9]*)*$/.test(seg))
486
+ return "Title Case";
487
+ if (/^[a-z0-9]+$/.test(seg))
488
+ return "lowercase";
489
+ return "mixed";
490
+ }
491
+ function namingConsistency(names, what, issues) {
492
+ const counts = {};
493
+ const multiWord = [];
494
+ for (let i = 0;i < names.length; i++) {
495
+ const c = convention(names[i]);
496
+ if (c === "lowercase")
497
+ continue;
498
+ counts[c] = (counts[c] || 0) + 1;
499
+ multiWord.push(names[i]);
500
+ }
501
+ const keys = Object.keys(counts).sort(function(a, b) {
502
+ return counts[b] - counts[a];
503
+ });
504
+ const main = keys[0] || "lowercase";
505
+ const off = multiWord.filter(function(n) {
506
+ return convention(n) !== main;
507
+ });
508
+ for (let i = 0;i < Math.min(off.length, 5); i++)
509
+ issues.push({ category: "naming", message: what + ' "' + off[i] + '" is not ' + main + " like most others" });
510
+ return { score: score(multiWord.length - off.length, multiWord.length), convention: main };
511
+ }
512
+ function paintKey(paints) {
513
+ return JSON.stringify(paints.map(function(pt) {
514
+ if (pt.type === "SOLID")
515
+ return toHex(pt.color, pt.opacity) + (pt.boundVariables && pt.boundVariables.color ? "@" + pt.boundVariables.color.id : "");
516
+ if (pt.gradientStops)
517
+ return pt.type + pt.gradientStops.map(function(s) {
518
+ return toHex(s.color) + round(s.position);
519
+ });
520
+ return pt.type + (pt.imageHash || "");
521
+ }));
522
+ }
523
+ async function designSystemHealth(p, requestId) {
524
+ const scope = await scopeNodes(p, !p.nodeId, requestId);
525
+ const nodes = scope.nodes;
526
+ const issues = [];
527
+ const add = function(category, message, n) {
528
+ if (issues.length < MAX_ISSUES * 3)
529
+ issues.push(n ? { category, message, nodeId: n.id, name: n.name } : { category, message });
530
+ };
531
+ const styles = await localStyles();
532
+ const vars = await localVariables();
533
+ const components = await localComponents();
534
+ const componentNames = {};
535
+ for (let i = 0;i < components.length; i++)
536
+ componentNames[components[i].name] = true;
537
+ const used = {};
538
+ const use = function(id) {
539
+ if (typeof id === "string" && id)
540
+ used[id] = (used[id] || 0) + 1;
541
+ };
542
+ let colorTotal = 0;
543
+ let colorBound = 0;
544
+ let numberTotal = 0;
545
+ let numberBound = 0;
546
+ let textTotal = 0;
547
+ let textStyled = 0;
548
+ let contrastTotal = 0;
549
+ let contrastOk = 0;
550
+ let layerTotal = 0;
551
+ let layerDefault = 0;
552
+ let instanceCount = 0;
553
+ let overridden = 0;
554
+ let detached = 0;
555
+ const pageBg = pageBackground();
556
+ for (let i = 0;i < nodes.length; i++) {
557
+ if (i % 500 === 0)
558
+ checkCancelled(requestId);
559
+ const n = nodes[i];
560
+ const bound = n.boundVariables || {};
561
+ const keys = Object.keys(bound);
562
+ for (let k = 0;k < keys.length; k++) {
563
+ const b = bound[keys[k]];
564
+ if (Array.isArray(b))
565
+ for (let j = 0;j < b.length; j++)
566
+ use(b[j] && b[j].id);
567
+ else if (b && b.id)
568
+ use(b.id);
569
+ }
570
+ ["fillStyleId", "strokeStyleId", "effectStyleId", "textStyleId"].forEach(function(f) {
571
+ if (f in n)
572
+ use(n[f]);
573
+ });
574
+ if (n.type === "INSTANCE") {
575
+ instanceCount++;
576
+ const fields = (n.overrides || []).reduce(function(sum, o) {
577
+ return sum + o.overriddenFields.length;
578
+ }, 0);
579
+ if (fields > 12) {
580
+ overridden++;
581
+ add("components", "Instance with " + fields + " overridden properties: consider a variant", n);
582
+ }
583
+ continue;
584
+ }
585
+ if ((n.type === "FRAME" || n.type === "GROUP") && (componentNames[n.name] || /^[^=,]+=[^=,]+(, [^=,]+=[^=,]+)*$/.test(n.name))) {
586
+ detached++;
587
+ add("components", 'Frame named like a component ("' + n.name + '"): probably a detached instance', n);
588
+ }
589
+ const fillStyled = typeof n.fillStyleId === "string" && n.fillStyleId !== "";
590
+ const solids = fillStyled ? [] : visibleSolids(n.fills);
591
+ for (let k = 0;k < solids.length; k++) {
592
+ colorTotal++;
593
+ if (solids[k].boundVariables && solids[k].boundVariables.color)
594
+ colorBound++;
595
+ else
596
+ add("tokens", "Hard-coded color " + toHex(solids[k].color, solids[k].opacity), n);
597
+ }
598
+ const strokeStyled = typeof n.strokeStyleId === "string" && n.strokeStyleId !== "";
599
+ const strokes = strokeStyled ? [] : visibleSolids(n.strokes);
600
+ for (let k = 0;k < strokes.length; k++) {
601
+ colorTotal++;
602
+ if (strokes[k].boundVariables && strokes[k].boundVariables.color)
603
+ colorBound++;
604
+ }
605
+ if (fillStyled) {
606
+ colorTotal++;
607
+ colorBound++;
608
+ }
609
+ if (n.layoutMode && n.layoutMode !== "NONE") {
610
+ for (let k = 0;k < NUMBER_FIELDS.length; k++) {
611
+ const f = NUMBER_FIELDS[k];
612
+ if (f === "counterAxisSpacing" && n.layoutWrap !== "WRAP")
613
+ continue;
614
+ if (!n[f])
615
+ continue;
616
+ numberTotal++;
617
+ if (bound[f])
618
+ numberBound++;
619
+ }
620
+ }
621
+ if ("topLeftRadius" in n && n.type !== "TEXT") {
622
+ for (let k = 0;k < RADIUS_FIELDS.length; k++) {
623
+ if (!n[RADIUS_FIELDS[k]])
624
+ continue;
625
+ numberTotal++;
626
+ if (bound[RADIUS_FIELDS[k]])
627
+ numberBound++;
628
+ }
629
+ }
630
+ if (n.type === "TEXT") {
631
+ textTotal++;
632
+ if (typeof n.textStyleId === "string" && n.textStyleId)
633
+ textStyled++;
634
+ else if (n.textStyleId === figma.mixed)
635
+ textStyled += 0.5;
636
+ else
637
+ add("typography", "Text without a text style (" + (n.fontName !== figma.mixed ? n.fontName.family + " " + n.fontName.style : "mixed fonts") + ", " + (n.fontSize !== figma.mixed ? round(n.fontSize) : "mixed") + " px)", n);
638
+ const c = n.visible ? textContrast(n, pageBg) : null;
639
+ if (c) {
640
+ contrastTotal++;
641
+ if (c.ratio >= c.min)
642
+ contrastOk++;
643
+ else
644
+ add("contrast", "Contrast " + round(c.ratio) + ":1 is below " + c.min + ":1", n);
645
+ }
646
+ } else if (n.type !== "COMPONENT" && n.type !== "COMPONENT_SET" && n.type !== "SECTION") {
647
+ layerTotal++;
648
+ if (DEFAULT_NAME.test(n.name)) {
649
+ layerDefault++;
650
+ add("naming", "Default layer name", n);
651
+ }
652
+ }
653
+ }
654
+ const styleTotal = styles.paint.length + styles.text.length + styles.effect.length;
655
+ let styleProblems = 0;
656
+ const seen = {};
657
+ const dup = function(key, name, kind) {
658
+ if (seen[key]) {
659
+ styleProblems++;
660
+ add("styles", kind + ' style "' + name + '" duplicates "' + seen[key] + '"');
661
+ } else
662
+ seen[key] = name;
663
+ };
664
+ for (let i = 0;i < styles.paint.length; i++)
665
+ dup("p" + paintKey(styles.paint[i].paints), styles.paint[i].name, "Color");
666
+ for (let i = 0;i < styles.text.length; i++) {
667
+ const s = styles.text[i];
668
+ dup("t" + JSON.stringify([s.fontName, s.fontSize, s.lineHeight, s.letterSpacing, s.textCase, s.textDecoration]), s.name, "Text");
669
+ }
670
+ for (let i = 0;i < styles.effect.length; i++)
671
+ dup("e" + JSON.stringify(styles.effect[i].effects), styles.effect[i].name, "Effect");
672
+ const wholeFile = !p.nodeId;
673
+ if (wholeFile) {
674
+ const all = styles.paint.concat(styles.text, styles.effect);
675
+ for (let i = 0;i < all.length; i++) {
676
+ if (used[all[i].id])
677
+ continue;
678
+ styleProblems++;
679
+ add("styles", 'Style "' + all[i].name + '" is not used in this file');
680
+ }
681
+ }
682
+ for (let i = 0;i < vars.vars.length; i++) {
683
+ const v = vars.vars[i];
684
+ const modes = Object.keys(v.valuesByMode);
685
+ for (let k = 0;k < modes.length; k++) {
686
+ const val = v.valuesByMode[modes[k]];
687
+ if (val && val.type === "VARIABLE_ALIAS")
688
+ use(val.id);
689
+ }
690
+ }
691
+ for (let i = 0;i < styles.paint.length; i++) {
692
+ const ps = styles.paint[i].paints;
693
+ for (let k = 0;k < ps.length; k++)
694
+ if (ps[k].boundVariables && ps[k].boundVariables.color)
695
+ use(ps[k].boundVariables.color.id);
696
+ }
697
+ let unusedVars = 0;
698
+ if (wholeFile) {
699
+ for (let i = 0;i < vars.vars.length; i++) {
700
+ if (used[vars.vars[i].id])
701
+ continue;
702
+ unusedVars++;
703
+ if (unusedVars <= 10)
704
+ add("styles", 'Variable "' + vars.vars[i].name + '" is not used in this file');
705
+ }
706
+ }
707
+ let unusedComponents = 0;
708
+ if (wholeFile) {
709
+ for (let i = 0;i < Math.min(components.length, 300); i++) {
710
+ const c = components[i];
711
+ const parts = c.type === "COMPONENT_SET" ? c.children : [c];
712
+ let count = 0;
713
+ for (let k = 0;k < parts.length && !count; k++)
714
+ count += (await parts[k].getInstancesAsync()).length;
715
+ if (!count) {
716
+ unusedComponents++;
717
+ add("styles", 'Component "' + c.name + '" has no instances');
718
+ }
719
+ }
720
+ }
721
+ const varNames = vars.vars.map(function(v) {
722
+ return v.name;
723
+ });
724
+ const styleNames = styles.paint.concat(styles.text, styles.effect).map(function(s) {
725
+ return s.name;
726
+ });
727
+ const naming = namingConsistency(varNames.concat(styleNames), "Token", issues);
728
+ const categories = {
729
+ tokens: { score: score(colorBound + numberBound, colorTotal + numberTotal), colors: { bound: colorBound, total: colorTotal }, numbers: { bound: numberBound, total: numberTotal } },
730
+ contrast: { score: score(contrastOk, contrastTotal), passing: contrastOk, total: contrastTotal },
731
+ typography: { score: score(textStyled, textTotal), styled: Math.floor(textStyled), total: textTotal },
732
+ components: {
733
+ score: Math.max(0, 100 - Math.round((detached + overridden * 0.5) / Math.max(1, instanceCount + detached) * 100)),
734
+ instances: instanceCount,
735
+ detachedSuspects: detached,
736
+ heavilyOverridden: overridden,
737
+ unused: wholeFile ? unusedComponents : undefined
738
+ },
739
+ styles: {
740
+ score: score(styleTotal + vars.vars.length + components.length - styleProblems - unusedVars - unusedComponents, styleTotal + vars.vars.length + components.length),
741
+ styles: styleTotal,
742
+ variables: vars.vars.length,
743
+ problems: styleProblems + unusedVars + unusedComponents
744
+ },
745
+ naming: {
746
+ score: Math.round((score(layerTotal - layerDefault, layerTotal) + naming.score) / 2),
747
+ defaultLayerNames: layerDefault,
748
+ layers: layerTotal,
749
+ tokenConvention: naming.convention
750
+ }
751
+ };
752
+ let total = 0;
753
+ let weight = 0;
754
+ const names = Object.keys(WEIGHTS2);
755
+ for (let i = 0;i < names.length; i++) {
756
+ total += categories[names[i]].score * WEIGHTS2[names[i]];
757
+ weight += WEIGHTS2[names[i]];
758
+ }
759
+ const picked = [];
760
+ for (let pass = 0;picked.length < MAX_ISSUES && pass < MAX_ISSUES; pass++) {
761
+ let added = false;
762
+ for (let c = 0;c < names.length && picked.length < MAX_ISSUES; c++) {
763
+ const list = issues.filter(function(x) {
764
+ return x.category === names[c];
765
+ });
766
+ if (list[pass]) {
767
+ picked.push(list[pass]);
768
+ added = true;
769
+ }
770
+ }
771
+ if (!added)
772
+ break;
773
+ }
774
+ return {
775
+ scope: scope.label,
776
+ score: Math.round(total / weight),
777
+ categories,
778
+ issues: picked,
779
+ totalIssues: issues.length,
780
+ nodesChecked: nodes.length,
781
+ truncated: scope.truncated,
782
+ hint: "audit {fix:true} binds raw colors and numbers to matching variables, applies matching text styles and renames default layer names."
783
+ };
784
+ }
785
+ function resolved(v, byId, collections, depth) {
786
+ const col = collections[v.variableCollectionId];
787
+ if (!col)
788
+ return null;
789
+ const raw = v.valuesByMode[col.defaultModeId];
790
+ if (raw && raw.type === "VARIABLE_ALIAS") {
791
+ const t = byId[raw.id];
792
+ return t && (depth || 0) < 10 ? resolved(t, byId, collections, (depth || 0) + 1) : null;
793
+ }
794
+ return raw;
795
+ }
796
+ function scoped(v, wanted) {
797
+ const s = v.scopes || [];
798
+ if (!s.length || s.indexOf("ALL_SCOPES") !== -1)
799
+ return true;
800
+ for (let i = 0;i < wanted.length; i++)
801
+ if (s.indexOf(wanted[i]) !== -1)
802
+ return true;
803
+ return false;
804
+ }
805
+ var FILL_SCOPES = { TEXT: ["ALL_FILLS", "TEXT_FILL"], FRAME: ["ALL_FILLS", "FRAME_FILL"], COMPONENT: ["ALL_FILLS", "FRAME_FILL"], SECTION: ["ALL_FILLS", "FRAME_FILL"] };
806
+ async function autoFix(p, requestId) {
807
+ const all = ["colors", "numbers", "textStyles", "names"];
808
+ const fixes = Array.isArray(p.fixes) && p.fixes.length ? p.fixes : all;
809
+ for (let i = 0;i < fixes.length; i++)
810
+ if (all.indexOf(fixes[i]) === -1)
811
+ throw codeError('Unknown fix "' + fixes[i] + '" (use ' + all.join(", ") + ")", "BAD_ARGS");
812
+ const scope = await scopeNodes(p, false, requestId);
813
+ const vars = await localVariables();
814
+ const byId = {};
815
+ const collections = {};
816
+ for (let i = 0;i < vars.vars.length; i++)
817
+ byId[vars.vars[i].id] = vars.vars[i];
818
+ for (let i = 0;i < vars.collections.length; i++)
819
+ collections[vars.collections[i].id] = vars.collections[i];
820
+ const colorVars = [];
821
+ const numberVars = [];
822
+ for (let i = 0;i < vars.vars.length; i++) {
823
+ const v = vars.vars[i];
824
+ const value = resolved(v, byId, collections);
825
+ if (v.resolvedType === "COLOR" && value && typeof value === "object")
826
+ colorVars.push({ v, rgba: value });
827
+ if (v.resolvedType === "FLOAT" && typeof value === "number")
828
+ numberVars.push({ v, num: value });
829
+ }
830
+ const changes = [];
831
+ const counts = { colors: 0, numbers: 0, textStyles: 0, names: 0 };
832
+ const record = function(n, fix, from, to) {
833
+ counts[fix]++;
834
+ if (changes.length < 200)
835
+ changes.push({ nodeId: n.id, name: n.name, fix, from, to });
836
+ };
837
+ const matchColor = function(n, paint, scopes) {
838
+ const alpha = paint.opacity === undefined ? 1 : paint.opacity;
839
+ const ranked = colorVars.filter(function(c) {
840
+ return scoped(c.v, scopes) && Math.abs((c.rgba.a === undefined ? 1 : c.rgba.a) - alpha) < 0.02;
841
+ }).map(function(c) {
842
+ return { c, d: deltaE(c.rgba, paint.color) };
843
+ }).filter(function(x) {
844
+ return x.d < 2;
845
+ }).sort(function(a, b) {
846
+ return a.d - b.d;
847
+ });
848
+ for (let i = 0;i < ranked.length; i++) {
849
+ try {
850
+ const r = ranked[i].c.v.resolveForConsumer(n).value;
851
+ if (r && typeof r === "object" && deltaE(r, paint.color) < 2)
852
+ return ranked[i].c.v;
853
+ } catch (e) {}
854
+ }
855
+ return null;
856
+ };
857
+ const matchNumber = function(value, scopes, hint) {
858
+ const hits = numberVars.filter(function(c) {
859
+ return Math.abs(c.num - value) < 0.01 && scoped(c.v, scopes);
860
+ });
861
+ hits.sort(function(a, b) {
862
+ return (hint.test(b.v.name) ? 1 : 0) - (hint.test(a.v.name) ? 1 : 0);
863
+ });
864
+ return hits.length ? hits[0].v : null;
865
+ };
866
+ const textStyles = fixes.indexOf("textStyles") !== -1 ? (await localStyles()).text : [];
867
+ for (let i = 0;i < scope.nodes.length; i++) {
868
+ if (i % 200 === 0)
869
+ checkCancelled(requestId);
870
+ const n = scope.nodes[i];
871
+ if (n.type === "INSTANCE" || n.locked)
872
+ continue;
873
+ const bound = n.boundVariables || {};
874
+ if (fixes.indexOf("colors") !== -1) {
875
+ const fields = ["fills", "strokes"];
876
+ for (let f = 0;f < fields.length; f++) {
877
+ const field = fields[f];
878
+ if (!(field in n) || n[field] === figma.mixed)
879
+ continue;
880
+ if (field === "fills" && n.fillStyleId || field === "strokes" && n.strokeStyleId)
881
+ continue;
882
+ const paints = n[field].slice();
883
+ let changed = false;
884
+ for (let k = 0;k < paints.length; k++) {
885
+ const pt = paints[k];
886
+ if (pt.type !== "SOLID" || pt.visible === false || pt.boundVariables && pt.boundVariables.color)
887
+ continue;
888
+ const v = matchColor(n, pt, field === "strokes" ? ["STROKE_COLOR"] : FILL_SCOPES[n.type] || ["ALL_FILLS", "SHAPE_FILL"]);
889
+ if (!v)
890
+ continue;
891
+ paints[k] = figma.variables.setBoundVariableForPaint(pt, "color", v);
892
+ changed = true;
893
+ record(n, "colors", field + " " + toHex(pt.color, pt.opacity), "var:" + v.name);
894
+ }
895
+ if (changed)
896
+ n[field] = paints;
897
+ }
898
+ }
899
+ if (fixes.indexOf("numbers") !== -1) {
900
+ const fields = [];
901
+ if (n.layoutMode && n.layoutMode !== "NONE") {
902
+ for (let k = 0;k < NUMBER_FIELDS.length; k++)
903
+ if (NUMBER_FIELDS[k] !== "counterAxisSpacing" || n.layoutWrap === "WRAP")
904
+ fields.push(NUMBER_FIELDS[k]);
905
+ }
906
+ if ("topLeftRadius" in n && n.type !== "TEXT")
907
+ for (let k = 0;k < RADIUS_FIELDS.length; k++)
908
+ fields.push(RADIUS_FIELDS[k]);
909
+ for (let k = 0;k < fields.length; k++) {
910
+ const f = fields[k];
911
+ const value = n[f];
912
+ if (typeof value !== "number" || !value || bound[f])
913
+ continue;
914
+ const radius = f.indexOf("Radius") !== -1;
915
+ const v = matchNumber(value, radius ? ["CORNER_RADIUS"] : ["GAP"], radius ? /radius|corner|round/i : /spac|gap|pad/i);
916
+ if (!v)
917
+ continue;
918
+ try {
919
+ n.setBoundVariable(f, v);
920
+ record(n, "numbers", f + " " + round(value), "var:" + v.name);
921
+ } catch (e) {}
922
+ }
923
+ }
924
+ if (n.type === "TEXT" && textStyles.length && !n.textStyleId && n.fontName !== figma.mixed && n.fontSize !== figma.mixed && n.lineHeight !== figma.mixed && n.letterSpacing !== figma.mixed) {
925
+ for (let k = 0;k < textStyles.length; k++) {
926
+ const s = textStyles[k];
927
+ if (s.fontName.family === n.fontName.family && s.fontName.style === n.fontName.style && Math.abs(s.fontSize - n.fontSize) < 0.01 && JSON.stringify(s.lineHeight) === JSON.stringify(n.lineHeight) && s.letterSpacing.unit === n.letterSpacing.unit && Math.abs(s.letterSpacing.value - n.letterSpacing.value) < 0.01) {
928
+ try {
929
+ await loadFont(s.fontName);
930
+ await n.setTextStyleIdAsync(s.id);
931
+ record(n, "textStyles", n.fontName.family + " " + n.fontName.style + " " + round(n.fontSize), "style:" + s.name);
932
+ } catch (e) {}
933
+ break;
934
+ }
935
+ }
936
+ }
937
+ if (fixes.indexOf("names") !== -1 && n.type !== "TEXT" && DEFAULT_NAME.test(n.name)) {
938
+ const name = nameFromContent(n);
939
+ if (name && name !== n.name) {
940
+ const from = n.name;
941
+ n.name = name;
942
+ record(n, "names", from, name);
943
+ }
944
+ }
945
+ }
946
+ return { scope: scope.label, fixed: counts, changes, truncated: scope.truncated || changes.length >= 200 };
947
+ }
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
+
360
995
  // plugin/lib/audit.ts
361
- var MAX_NODES = 5000;
362
- var MAX_ISSUES = 120;
996
+ var MAX_NODES2 = 5000;
997
+ var MAX_ISSUES2 = 120;
363
998
  var DEFAULT_NAME = /^(Frame|Rectangle|Ellipse|Group|Vector|Text|Line|Polygon|Star|Component|Instance|Section|Image)( \d+)?$/;
364
- async function audit(p) {
999
+ async function audit(p, _timeoutMs, requestId) {
1000
+ if (p.fix === true || Array.isArray(p.fixes) && p.fixes.length) {
1001
+ const fixed = await autoFix(p, requestId);
1002
+ invalidateCaches();
1003
+ const after = p.scope === "design-system" ? await designSystemHealth(p, requestId) : await lint(p);
1004
+ return Object.assign({ fixed: fixed.fixed, changes: fixed.changes, fixScope: fixed.scope }, after);
1005
+ }
1006
+ if (p.scope === "design-system")
1007
+ return designSystemHealth(p, requestId);
1008
+ return lint(p);
1009
+ }
1010
+ async function lint(p) {
365
1011
  let roots;
366
1012
  if (p.nodeId)
367
1013
  roots = [await getNode(p.nodeId)];
@@ -380,11 +1026,11 @@
380
1026
  if (only && only.indexOf(rule) === -1)
381
1027
  return;
382
1028
  counts[rule] = (counts[rule] || 0) + 1;
383
- if (issues.length < MAX_ISSUES)
1029
+ if (issues.length < MAX_ISSUES2)
384
1030
  issues.push({ rule, severity, nodeId: n.id, name: n.name, message });
385
1031
  };
386
1032
  const visit = function(n, clipBox) {
387
- if (visited >= MAX_NODES)
1033
+ if (visited >= MAX_NODES2)
388
1034
  return;
389
1035
  visited++;
390
1036
  if (n.visible === false)
@@ -455,7 +1101,7 @@
455
1101
  fonts: familyList,
456
1102
  fontSizes: sizeList,
457
1103
  issues,
458
- truncated: visited >= MAX_NODES || issues.length >= MAX_ISSUES
1104
+ truncated: visited >= MAX_NODES2 || issues.length >= MAX_ISSUES2
459
1105
  };
460
1106
  }
461
1107
  function checkText(n, report, families, sizes, pageBg) {
@@ -480,19 +1126,21 @@
480
1126
  if (tb.x + tb.width > pb.x + pb.width + 1 || tb.x < pb.x - 1)
481
1127
  report("text-overflow", "warning", n, "Text extends outside its container");
482
1128
  }
1129
+ const c = textContrast(n, pageBg);
1130
+ if (c && c.ratio < c.min) {
1131
+ report("contrast", c.ratio < c.min - 1.5 ? "error" : "warning", n, "Contrast " + round(c.ratio) + ":1 is below " + c.min + ":1 (WCAG AA" + (c.large ? ", large text" : "") + ")");
1132
+ }
1133
+ }
1134
+ function textContrast(n, pageBg) {
483
1135
  const fg = solidOf(n.fills);
484
1136
  if (!fg || n.fontSize === figma.mixed)
485
- return;
1137
+ return null;
486
1138
  const bg = backgroundOf(n, pageBg);
487
1139
  if (!bg)
488
- return;
489
- const ratio = contrastRatio(fg, bg);
1140
+ return null;
490
1141
  const bold = n.fontName !== figma.mixed && /bold|black|heavy|semi/i.test(n.fontName.style);
491
1142
  const large = n.fontSize >= 24 || bold && n.fontSize >= 18.66;
492
- const min = large ? 3 : 4.5;
493
- if (ratio < min) {
494
- report("contrast", ratio < min - 1.5 ? "error" : "warning", n, "Contrast " + round(ratio) + ":1 is below " + min + ":1 (WCAG AA" + (large ? ", large text" : "") + ")");
495
- }
1143
+ return { ratio: contrastRatio(fg, bg), min: large ? 3 : 4.5, large };
496
1144
  }
497
1145
  function solidOf(paints) {
498
1146
  if (!paints || paints === figma.mixed)
@@ -529,6 +1177,186 @@
529
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;
530
1178
  }
531
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
+
532
1360
  // plugin/lib/prototype.ts
533
1361
  var TRIGGERS = {
534
1362
  click: "ON_CLICK",
@@ -1109,13 +1937,15 @@
1109
1937
  }
1110
1938
 
1111
1939
  // plugin/lib/build.ts
1112
- var MAX_NODES2 = 3000;
1940
+ var MAX_NODES3 = 3000;
1113
1941
  var SHADOW_DEFAULT = { x: 0, y: 4, blur: 16, spread: 0, color: "#0000001F" };
1114
1942
  async function build(p, _timeoutMs, requestId) {
1115
1943
  const spec = p.spec;
1116
1944
  if (!spec || typeof spec !== "object")
1117
1945
  throw codeError("`spec` must be a node object or an array of node objects", "BAD_ARGS");
1118
- 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];
1119
1949
  const d = p.defaults || {};
1120
1950
  const ctx = {
1121
1951
  warnings: [],
@@ -1126,6 +1956,8 @@
1126
1956
  defaults: { font: d.font || "Inter", color: d.color || "#111111", size: d.size || 14 },
1127
1957
  binds: [],
1128
1958
  reactions: [],
1959
+ connectors: [],
1960
+ keys: {},
1129
1961
  requestId,
1130
1962
  total: countNodes(roots),
1131
1963
  lastProgress: 0
@@ -1138,7 +1970,7 @@
1138
1970
  const node = await createNode(roots[i], parent, ctx, "spec" + (roots.length > 1 ? "[" + i + "]" : ""));
1139
1971
  if (!node)
1140
1972
  continue;
1141
- 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) {
1142
1974
  if (cursorX === null) {
1143
1975
  place(node, p.x, p.y);
1144
1976
  } else {
@@ -1151,6 +1983,7 @@
1151
1983
  }
1152
1984
  if (ctx.reactions.length)
1153
1985
  await applyBuildReactions(ctx.reactions, ctx.ids, ctx.warnings);
1986
+ const connectors = await createConnectors(ctx);
1154
1987
  for (let i = 0;i < ctx.binds.length; i++)
1155
1988
  ctx.warnings.push(ctx.binds[i].path + ": bind needs a component or componentSet ancestor, ignored");
1156
1989
  if (p.select !== false && made.length && parent.type === "PAGE" && parent === figma.currentPage) {
@@ -1162,7 +1995,7 @@
1162
1995
  rootIds: made.map(function(n) {
1163
1996
  return n.id;
1164
1997
  }),
1165
- created: ctx.count,
1998
+ created: ctx.count + connectors,
1166
1999
  ids: ctx.ids
1167
2000
  };
1168
2001
  if (ctx.warnings.length)
@@ -1243,7 +2076,7 @@
1243
2076
  function nodeType(s) {
1244
2077
  if (s.type) {
1245
2078
  const t = String(s.type).toLowerCase().replace(/[_\s-]/g, "");
1246
- 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;
1247
2080
  }
1248
2081
  if (Array.isArray(s.variants))
1249
2082
  return "componentset";
@@ -1264,12 +2097,34 @@
1264
2097
  ctx.warnings.push(path + ": not an object, skipped");
1265
2098
  return null;
1266
2099
  }
1267
- if (ctx.count >= MAX_NODES2)
1268
- throw codeError("Spec is too large (max " + MAX_NODES2 + " nodes). Split it into several build calls.", "TOO_LARGE");
2100
+ if (ctx.count >= MAX_NODES3)
2101
+ throw codeError("Spec is too large (max " + MAX_NODES3 + " nodes). Split it into several build calls.", "TOO_LARGE");
1269
2102
  checkCancelled(ctx.requestId);
1270
2103
  const type = nodeType(s);
2104
+ requireEditor(type, path);
1271
2105
  let node;
1272
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;
1273
2128
  case "frame":
1274
2129
  case "component":
1275
2130
  node = type === "component" ? figma.createComponent() : figma.createFrame();
@@ -1306,8 +2161,11 @@
1306
2161
  }
1307
2162
  ctx.count++;
1308
2163
  progress(ctx);
1309
- parent.appendChild(node);
2164
+ if (type !== "slide" || parent.type !== "PAGE")
2165
+ parent.appendChild(node);
1310
2166
  const parentAuto = isAutoLayout(parent);
2167
+ if (s.key !== undefined)
2168
+ ctx.keys[String(s.key)] = node.id;
1311
2169
  if (s.bind)
1312
2170
  ctx.binds.push({ node, bind: s.bind, path });
1313
2171
  const bindStart = ctx.binds.length;
@@ -1317,6 +2175,10 @@
1317
2175
  node.name = "icon/" + s.icon;
1318
2176
  if (type === "frame" || type === "component")
1319
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);
1320
2182
  else if (type === "componentset")
1321
2183
  await setupLayout(node, Object.assign({ layout: "row", wrap: true, gap: 16, padding: 16 }, s), ctx, path);
1322
2184
  else if (type === "text")
@@ -1326,7 +2188,7 @@
1326
2188
  else if (type === "line") {
1327
2189
  node.resize(num(s.w, s.width, 100), 0);
1328
2190
  if (s.stroke === undefined)
1329
- node.strokes = [solid(ctx.defaults.color)];
2191
+ node.strokes = [solid2(ctx.defaults.color)];
1330
2192
  } else if ((type === "rect" || type === "ellipse") && s.w === undefined && s.width === undefined && s.size === undefined) {
1331
2193
  node.resize(100, 100);
1332
2194
  }
@@ -1334,14 +2196,18 @@
1334
2196
  await overrideTexts(node, s.text, ctx);
1335
2197
  if (type === "componentset" && s.stroke === undefined) {
1336
2198
  const set = node;
1337
- set.strokes = [solid("#9747FF")];
2199
+ set.strokes = [solid2("#9747FF")];
1338
2200
  set.dashPattern = [10, 5];
1339
2201
  set.cornerRadius = 5;
1340
2202
  }
1341
- 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;
1342
2207
  if (type === "text" && Array.isArray(s.spans))
1343
2208
  await applySpans(node, s, ctx, path);
1344
- 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);
1345
2211
  if (type === "component" && !s.variantOf)
1346
2212
  await addProperties(node, s.properties, ctx.binds.splice(bindStart), ctx, path);
1347
2213
  if (parent.layoutMode === "GRID")
@@ -1356,7 +2222,7 @@
1356
2222
  }
1357
2223
  if (s.grow && parentAuto)
1358
2224
  node.layoutGrow = 1;
1359
- if (typeof s.rotation === "number")
2225
+ if (typeof s.rotation === "number" && "rotation" in node)
1360
2226
  node.rotation = s.rotation;
1361
2227
  if (s.visible === false)
1362
2228
  node.visible = false;
@@ -1375,20 +2241,83 @@
1375
2241
  }
1376
2242
  return node;
1377
2243
  }
1378
- async function setupFrame(f, s, ctx, path) {
1379
- 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 = [];
1380
2248
  await setupLayout(f, s, ctx, path);
1381
2249
  const children = Array.isArray(s.children) ? s.children : [];
1382
2250
  for (let i = 0;i < children.length; i++) {
1383
2251
  try {
1384
2252
  await createNode(children[i], f, ctx, path + ".children[" + i + "]");
1385
2253
  } catch (e) {
1386
- if (e.code === "TOO_LARGE" || e.code === "CANCELLED")
2254
+ if (e.code === "TOO_LARGE" || e.code === "CANCELLED" || e.code === "WRONG_EDITOR")
1387
2255
  throw e;
1388
2256
  ctx.warnings.push(path + ".children[" + i + "]: " + (e.message || e));
1389
2257
  }
1390
2258
  }
1391
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
+ }
1392
2321
  async function setupLayout(f, s, ctx, path) {
1393
2322
  const layout = String(s.layout || s.direction || "").toLowerCase();
1394
2323
  let mode = layout === "row" || layout === "horizontal" ? "HORIZONTAL" : layout === "column" || layout === "col" || layout === "vertical" ? "VERTICAL" : layout === "grid" ? "GRID" : "NONE";
@@ -1422,7 +2351,7 @@
1422
2351
  if (s.rowGap !== undefined)
1423
2352
  await setNumber(f, "counterAxisSpacing", s.rowGap);
1424
2353
  }
1425
- } 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") {
1426
2355
  f.resize(100, 100);
1427
2356
  }
1428
2357
  }
@@ -1562,7 +2491,7 @@
1562
2491
  t.maxLines = s.maxLines;
1563
2492
  }
1564
2493
  if (s.fill === undefined && s.color === undefined && !s.textStyle)
1565
- t.fills = [solid(ctx.defaults.color)];
2494
+ t.fills = [solid2(ctx.defaults.color)];
1566
2495
  if (s.textStyle) {
1567
2496
  const style = await findStyle("text", stripPrefix(s.textStyle));
1568
2497
  await loadFont(style.fontName);
@@ -1586,7 +2515,7 @@
1586
2515
  if (typeof color === "string" && color.indexOf("style:") === 0) {
1587
2516
  await t.setRangeFillStyleIdAsync(at, end, (await findStyle("paint", stripPrefix(color))).id);
1588
2517
  } else if (color !== undefined) {
1589
- t.setRangeFills(at, end, [await toPaint(color)]);
2518
+ t.setRangeFills(at, end, [await toPaint(color, ctx.images)]);
1590
2519
  }
1591
2520
  const deco = { underline: "UNDERLINE", strike: "STRIKETHROUGH", strikethrough: "STRIKETHROUGH", none: "NONE" };
1592
2521
  if (sp.link) {
@@ -1596,7 +2525,7 @@
1596
2525
  }
1597
2526
  if (sp.decoration && deco[sp.decoration])
1598
2527
  t.setRangeTextDecoration(at, end, deco[sp.decoration]);
1599
- const cases = { upper: "UPPER", lower: "LOWER", title: "TITLE" };
2528
+ const cases = { upper: "UPPER", lower: "LOWER", title: "TITLE", none: "ORIGINAL" };
1600
2529
  if (sp.case && cases[sp.case])
1601
2530
  t.setRangeTextCase(at, end, cases[sp.case]);
1602
2531
  } catch (e) {
@@ -1782,10 +2711,20 @@
1782
2711
  async function applyVisuals(node, s, ctx, type) {
1783
2712
  const fill = s.fill !== undefined ? s.fill : type === "text" ? s.color : undefined;
1784
2713
  if (fill !== undefined && "fills" in node && type !== "image")
1785
- await setPaints(node, "fills", fill);
2714
+ await setPaints(node, "fills", fill, ctx.images);
1786
2715
  if (s.stroke !== undefined && "strokes" in node) {
1787
- await setPaints(node, "strokes", s.stroke);
1788
- node.strokeWeight = typeof s.strokeWidth === "number" ? s.strokeWidth : 1;
2716
+ await setPaints(node, "strokes", s.stroke, ctx.images);
2717
+ if (Array.isArray(s.strokeWidth) && "strokeTopWeight" in node) {
2718
+ const w = s.strokeWidth.map(Number);
2719
+ node.strokeTopWeight = w[0] || 0;
2720
+ node.strokeRightWeight = w[1] === undefined ? w[0] || 0 : w[1] || 0;
2721
+ node.strokeBottomWeight = w[2] === undefined ? w[0] || 0 : w[2] || 0;
2722
+ node.strokeLeftWeight = w[3] === undefined ? w[1] === undefined ? w[0] || 0 : w[1] || 0 : w[3] || 0;
2723
+ } else {
2724
+ node.strokeWeight = typeof s.strokeWidth === "number" ? s.strokeWidth : 1;
2725
+ }
2726
+ if (Array.isArray(s.strokeDash) && "dashPattern" in node)
2727
+ node.dashPattern = s.strokeDash.map(Number);
1789
2728
  if ("strokeAlign" in node && type !== "line" && type !== "text") {
1790
2729
  node.strokeAlign = String(s.strokeAlign || "inside").toUpperCase();
1791
2730
  }
@@ -1839,11 +2778,11 @@
1839
2778
  function stripPrefix(v) {
1840
2779
  return String(v).replace(/^(style|var):/, "");
1841
2780
  }
1842
- function solid(hex) {
2781
+ function solid2(hex) {
1843
2782
  const c = parseHex(hex);
1844
2783
  return { type: "SOLID", color: { r: c.r, g: c.g, b: c.b }, opacity: c.a };
1845
2784
  }
1846
- async function setPaints(node, field, value) {
2785
+ async function setPaints(node, field, value, images) {
1847
2786
  if (typeof value === "string" && value.indexOf("style:") === 0) {
1848
2787
  const style = await findStyle("paint", stripPrefix(value));
1849
2788
  if (field === "fills")
@@ -1855,16 +2794,17 @@
1855
2794
  const list = value === null || value === "none" ? [] : Array.isArray(value) ? value : [value];
1856
2795
  const paints = [];
1857
2796
  for (let i = 0;i < list.length; i++)
1858
- paints.push(await toPaint(list[i]));
2797
+ paints.push(await toPaint(list[i], images));
1859
2798
  node[field] = paints;
1860
2799
  }
1861
- async function toPaint(v) {
2800
+ var FITS = { fill: "FILL", cover: "FILL", fit: "FIT", contain: "FIT", crop: "CROP", tile: "TILE" };
2801
+ async function toPaint(v, images) {
1862
2802
  if (typeof v === "string") {
1863
2803
  if (v.indexOf("var:") === 0) {
1864
2804
  const variable = await findVariable(stripPrefix(v));
1865
- return figma.variables.setBoundVariableForPaint(solid("#000000"), "color", variable);
2805
+ return figma.variables.setBoundVariableForPaint(solid2("#000000"), "color", variable);
1866
2806
  }
1867
- return solid(v);
2807
+ return solid2(v);
1868
2808
  }
1869
2809
  if (v && Array.isArray(v.gradient)) {
1870
2810
  const stops = v.gradient.map(function(c, i, all) {
@@ -1886,6 +2826,15 @@
1886
2826
  ];
1887
2827
  return { type: radial ? "GRADIENT_RADIAL" : "GRADIENT_LINEAR", gradientTransform: transform, gradientStops: stops };
1888
2828
  }
2829
+ if (v && v.imageKey !== undefined) {
2830
+ const bytes = images && images[v.imageKey];
2831
+ if (!bytes)
2832
+ throw codeError("Image fill data missing (use {image: src} with the build tool)", "BAD_ARGS");
2833
+ const paint = { type: "IMAGE", imageHash: figma.createImage(bytes).hash, scaleMode: FITS[String(v.fit || "fill").toLowerCase()] || "FILL" };
2834
+ if (typeof v.opacity === "number")
2835
+ return Object.assign({}, paint, { opacity: v.opacity });
2836
+ return paint;
2837
+ }
1889
2838
  if (v && typeof v.type === "string")
1890
2839
  return v;
1891
2840
  throw codeError("Invalid paint: " + JSON.stringify(v), "BAD_ARGS");
@@ -2313,7 +3262,7 @@
2313
3262
  }
2314
3263
 
2315
3264
  // plugin/lib/export.ts
2316
- var MAX_NODES3 = 1500;
3265
+ var MAX_NODES4 = 1500;
2317
3266
  var MAX_ASSET_BYTES = 40 << 20;
2318
3267
  var VECTOR_TYPES = ["VECTOR", "BOOLEAN_OPERATION", "STAR", "POLYGON", "ELLIPSE", "LINE"];
2319
3268
  var VISUAL_CSS = [
@@ -2423,6 +3372,11 @@
2423
3372
  }
2424
3373
  if (n.rotation && Math.abs(n.rotation) > 0.01 && n.type !== "TEXT")
2425
3374
  out.rotation = round(n.rotation);
3375
+ if (n.type === "INSTANCE") {
3376
+ const info = await instanceInfo(n);
3377
+ if (info)
3378
+ out.component = info;
3379
+ }
2426
3380
  let css = {};
2427
3381
  try {
2428
3382
  css = await n.getCSSAsync();
@@ -2487,7 +3441,7 @@
2487
3441
  const c = n.children[i];
2488
3442
  if (!c.visible)
2489
3443
  continue;
2490
- if (ctx.count >= MAX_NODES3) {
3444
+ if (ctx.count >= MAX_NODES4) {
2491
3445
  ctx.truncated = true;
2492
3446
  break;
2493
3447
  }
@@ -2530,6 +3484,33 @@
2530
3484
  })
2531
3485
  };
2532
3486
  }
3487
+ async function instanceInfo(n) {
3488
+ let main = null;
3489
+ try {
3490
+ main = await n.getMainComponentAsync();
3491
+ } catch (e) {}
3492
+ if (!main)
3493
+ return null;
3494
+ const set = main.parent && main.parent.type === "COMPONENT_SET" ? main.parent.name : null;
3495
+ const props = {};
3496
+ const defs = n.componentProperties;
3497
+ const keys = Object.keys(defs);
3498
+ for (let i = 0;i < keys.length; i++) {
3499
+ const d = defs[keys[i]];
3500
+ if (d.type === "INSTANCE_SWAP")
3501
+ continue;
3502
+ props[keys[i].split("#")[0]] = d.value;
3503
+ }
3504
+ const info = { name: set || main.name, props };
3505
+ if (set)
3506
+ info.variant = main.name;
3507
+ const text = n.findOne(function(t) {
3508
+ return t.type === "TEXT" && t.visible;
3509
+ });
3510
+ if (text && text.characters.trim())
3511
+ info.text = text.characters.trim().slice(0, 200);
3512
+ return info;
3513
+ }
2533
3514
 
2534
3515
  // plugin/lib/design-system.ts
2535
3516
  var ALL = ["colors", "text", "effects", "variables", "components"];
@@ -2650,6 +3631,183 @@
2650
3631
  return v;
2651
3632
  }
2652
3633
 
3634
+ // plugin/lib/tokens-export.ts
3635
+ var TYPES2 = { COLOR: "color", FLOAT: "number", STRING: "string", BOOLEAN: "boolean" };
3636
+ async function exportTokens(p) {
3637
+ const only = Array.isArray(p.collections) && p.collections.length ? p.collections : null;
3638
+ const collections = await figma.variables.getLocalVariableCollectionsAsync();
3639
+ const vars = await figma.variables.getLocalVariablesAsync();
3640
+ const byId = {};
3641
+ for (let i = 0;i < vars.length; i++)
3642
+ byId[vars[i].id] = vars[i];
3643
+ const collName = {};
3644
+ for (let i = 0;i < collections.length; i++)
3645
+ collName[collections[i].id] = collections[i].name;
3646
+ const warnings = [];
3647
+ const aliasOf = async function(id) {
3648
+ let v = byId[id] || null;
3649
+ if (!v) {
3650
+ try {
3651
+ v = await figma.variables.getVariableByIdAsync(id);
3652
+ } catch (e) {}
3653
+ if (v)
3654
+ byId[id] = v;
3655
+ }
3656
+ if (!v)
3657
+ return { alias: { collection: "", name: id, missing: true } };
3658
+ let collection = collName[v.variableCollectionId];
3659
+ if (collection === undefined) {
3660
+ try {
3661
+ const c = await figma.variables.getVariableCollectionByIdAsync(v.variableCollectionId);
3662
+ collection = c ? c.name : "";
3663
+ } catch (e) {
3664
+ collection = "";
3665
+ }
3666
+ collName[v.variableCollectionId] = collection;
3667
+ }
3668
+ const out = { alias: { collection, name: v.name } };
3669
+ if (v.remote)
3670
+ out.alias.remote = true;
3671
+ return out;
3672
+ };
3673
+ const value = async function(raw, type) {
3674
+ if (raw && typeof raw === "object" && raw.type === "VARIABLE_ALIAS")
3675
+ return aliasOf(raw.id);
3676
+ if (type === "COLOR" && raw && typeof raw === "object")
3677
+ return toHex(raw);
3678
+ if (type === "FLOAT")
3679
+ return round(Number(raw));
3680
+ return raw;
3681
+ };
3682
+ const outCollections = [];
3683
+ for (let c = 0;c < collections.length; c++) {
3684
+ const col = collections[c];
3685
+ if (only && only.indexOf(col.name) === -1)
3686
+ continue;
3687
+ const modes = col.modes.slice().sort(function(a, b) {
3688
+ return (a.modeId === col.defaultModeId ? 0 : 1) - (b.modeId === col.defaultModeId ? 0 : 1);
3689
+ });
3690
+ const list = [];
3691
+ for (let i = 0;i < vars.length; i++) {
3692
+ const v = vars[i];
3693
+ if (v.variableCollectionId !== col.id)
3694
+ continue;
3695
+ const values = {};
3696
+ for (let m = 0;m < modes.length; m++)
3697
+ values[modes[m].name] = await value(v.valuesByMode[modes[m].modeId], v.resolvedType);
3698
+ const item = { name: v.name, type: TYPES2[v.resolvedType] || "string", values };
3699
+ if (v.description)
3700
+ item.description = v.description;
3701
+ if (v.scopes && v.scopes.length && !(v.scopes.length === 1 && v.scopes[0] === "ALL_SCOPES"))
3702
+ item.scopes = v.scopes.slice();
3703
+ if (v.hiddenFromPublishing)
3704
+ item.hidden = true;
3705
+ list.push(item);
3706
+ }
3707
+ outCollections.push({
3708
+ name: col.name,
3709
+ modes: modes.map(function(m) {
3710
+ return m.name;
3711
+ }),
3712
+ variables: list
3713
+ });
3714
+ }
3715
+ const styles = await localStyles();
3716
+ const paint = [];
3717
+ for (let i = 0;i < styles.paint.length; i++) {
3718
+ const s = styles.paint[i];
3719
+ const paints = [];
3720
+ for (let k = 0;k < s.paints.length; k++) {
3721
+ const pt = s.paints[k];
3722
+ if (pt.visible === false)
3723
+ continue;
3724
+ if (pt.type === "SOLID") {
3725
+ const bound = pt.boundVariables && pt.boundVariables.color;
3726
+ paints.push(bound ? await aliasOf(bound.id) : toHex(pt.color, pt.opacity));
3727
+ } else if (pt.type.indexOf("GRADIENT") === 0) {
3728
+ const t = pt.gradientTransform;
3729
+ const g = {
3730
+ gradient: pt.gradientStops.map(function(st) {
3731
+ return { color: toHex(st.color), at: round(st.position) };
3732
+ })
3733
+ };
3734
+ if (pt.type === "GRADIENT_LINEAR")
3735
+ g.angle = round(Math.atan2(t[0][1], t[0][0]) * 180 / Math.PI);
3736
+ else
3737
+ g.type = pt.type.replace("GRADIENT_", "").toLowerCase();
3738
+ paints.push(g);
3739
+ } else {
3740
+ warnings.push('Paint style "' + s.name + '": ' + pt.type.toLowerCase() + " paints are not exported");
3741
+ }
3742
+ }
3743
+ if (!paints.length)
3744
+ continue;
3745
+ const item = { name: s.name, value: paints.length === 1 ? paints[0] : paints };
3746
+ if (s.description)
3747
+ item.description = s.description;
3748
+ paint.push(item);
3749
+ }
3750
+ const text = [];
3751
+ for (let i = 0;i < styles.text.length; i++) {
3752
+ const s = styles.text[i];
3753
+ const item = { name: s.name, font: s.fontName.family, style: s.fontName.style, size: round(s.fontSize) };
3754
+ const lh = s.lineHeight;
3755
+ if (lh.unit === "PIXELS")
3756
+ item.lineHeight = round(lh.value);
3757
+ else if (lh.unit === "PERCENT")
3758
+ item.lineHeight = round(lh.value) + "%";
3759
+ if (s.letterSpacing.value)
3760
+ item.letterSpacing = s.letterSpacing.unit === "PERCENT" ? round(s.letterSpacing.value) + "%" : round(s.letterSpacing.value);
3761
+ if (s.textCase && s.textCase !== "ORIGINAL")
3762
+ item.case = s.textCase === "UPPER" ? "upper" : s.textCase === "LOWER" ? "lower" : "title";
3763
+ if (s.textDecoration && s.textDecoration !== "NONE")
3764
+ item.decoration = s.textDecoration === "UNDERLINE" ? "underline" : "strike";
3765
+ if (s.paragraphSpacing)
3766
+ item.paragraphSpacing = round(s.paragraphSpacing);
3767
+ const bound = s.boundVariables || {};
3768
+ const keys = Object.keys(bound);
3769
+ if (keys.length) {
3770
+ item.variables = {};
3771
+ for (let k = 0;k < keys.length; k++)
3772
+ if (bound[keys[k]] && bound[keys[k]].id)
3773
+ item.variables[keys[k]] = (await aliasOf(bound[keys[k]].id)).alias;
3774
+ }
3775
+ if (s.description)
3776
+ item.description = s.description;
3777
+ text.push(item);
3778
+ }
3779
+ const effect = [];
3780
+ for (let i = 0;i < styles.effect.length; i++) {
3781
+ const s = styles.effect[i];
3782
+ const list = [];
3783
+ for (let k = 0;k < s.effects.length; k++) {
3784
+ const e = s.effects[k];
3785
+ if (e.visible === false)
3786
+ continue;
3787
+ if (e.type === "DROP_SHADOW" || e.type === "INNER_SHADOW") {
3788
+ const sh = { x: round(e.offset.x), y: round(e.offset.y), blur: round(e.radius), spread: round(e.spread || 0), color: toHex(e.color) };
3789
+ if (e.type === "INNER_SHADOW")
3790
+ sh.inner = true;
3791
+ if (e.boundVariables && e.boundVariables.color)
3792
+ sh.colorVariable = (await aliasOf(e.boundVariables.color.id)).alias;
3793
+ list.push(sh);
3794
+ } else {
3795
+ list.push({ type: e.type === "LAYER_BLUR" ? "layer" : "background", blur: round(e.radius) });
3796
+ }
3797
+ }
3798
+ if (!list.length)
3799
+ continue;
3800
+ const item = { name: s.name, value: list };
3801
+ if (s.description)
3802
+ item.description = s.description;
3803
+ effect.push(item);
3804
+ }
3805
+ const out = { fileName: figma.root.name, collections: outCollections, styles: { colors: paint, text, effects: effect } };
3806
+ if (warnings.length)
3807
+ out.warnings = warnings.slice(0, 40);
3808
+ return out;
3809
+ }
3810
+
2653
3811
  // plugin/lib/find.ts
2654
3812
  var MAX_LIMIT = 500;
2655
3813
  function matcher(q) {
@@ -2762,6 +3920,243 @@
2762
3920
  return out;
2763
3921
  }
2764
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
+
2765
4160
  // plugin/lib/selection.ts
2766
4161
  var MAX_WAIT_MS = 120000;
2767
4162
  var waiters = {};
@@ -2831,7 +4226,7 @@
2831
4226
  finish(id, codeError("The user cancelled the selection request", "CANCELLED"));
2832
4227
  }
2833
4228
  // package.json
2834
- var version = "1.5.0";
4229
+ var version = "1.12.0";
2835
4230
 
2836
4231
  // plugin/code.ts
2837
4232
  var DEFAULT_SIZE = { width: 340, height: 540 };
@@ -2853,7 +4248,11 @@
2853
4248
  figma.ui.postMessage(msg);
2854
4249
  }
2855
4250
  function sessionInfo() {
2856
- 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 };
2857
4256
  }
2858
4257
  function clampSize(w, h) {
2859
4258
  return {
@@ -2942,6 +4341,7 @@
2942
4341
  find: find2,
2943
4342
  get_design_system: getDesignSystem,
2944
4343
  design_tokens: designTokens,
4344
+ export_tokens: exportTokens,
2945
4345
  audit,
2946
4346
  get_css: getCss,
2947
4347
  checkpoint,
@@ -2954,14 +4354,25 @@
2954
4354
  prototype,
2955
4355
  annotate,
2956
4356
  export_tree: exportTree,
4357
+ node_info: nodeInfo,
4358
+ roblox_tree: robloxTree,
4359
+ roblox_images: robloxImages,
2957
4360
  ping: function() {
2958
4361
  return Promise.resolve({ pong: true, session: sessionInfo() });
2959
4362
  }
2960
4363
  };
4364
+ var EDITORS2 = {
4365
+ prototype: ["figma"],
4366
+ annotate: ["figma", "dev"],
4367
+ design_tokens: ["figma", "slides"],
4368
+ export_tokens: ["figma", "slides", "dev"]
4369
+ };
4370
+ var EDITOR_NAMES2 = { figma: "Figma Design", figjam: "FigJam", slides: "Figma Slides", dev: "Dev Mode" };
2961
4371
  var READ_ONLY = {
2962
4372
  describe: true,
2963
4373
  find: true,
2964
4374
  get_design_system: true,
4375
+ export_tokens: true,
2965
4376
  audit: true,
2966
4377
  get_css: true,
2967
4378
  screenshot: true,
@@ -2969,11 +4380,13 @@
2969
4380
  list_fonts: true,
2970
4381
  wait_for_selection: true,
2971
4382
  export_tree: true,
4383
+ node_info: true,
4384
+ roblox_tree: true,
2972
4385
  ping: true
2973
4386
  };
2974
4387
  async function handleRequest(msg) {
2975
4388
  const started = Date.now();
2976
- const mutates = !READ_ONLY[msg.method];
4389
+ const mutates = !READ_ONLY[msg.method] || !!(msg.params && (msg.params.fix === true || msg.params.fixes && msg.params.fixes.length));
2977
4390
  let reply;
2978
4391
  if (mutates)
2979
4392
  commitUndo();
@@ -2981,6 +4394,14 @@
2981
4394
  const handler = HANDLERS[msg.method];
2982
4395
  if (!handler)
2983
4396
  throw codeError("Unknown method: " + msg.method + " (reopen the plugin after updating it)", "UNKNOWN_METHOD");
4397
+ const editors = EDITORS2[msg.method];
4398
+ if (editors && editors.indexOf(figma.editorType) === -1) {
4399
+ throw codeError(msg.method + " is not available in " + (EDITOR_NAMES2[figma.editorType] || figma.editorType) + " (works in " + editors.map(function(e) {
4400
+ return EDITOR_NAMES2[e];
4401
+ }).join(", ") + ").", "WRONG_EDITOR");
4402
+ }
4403
+ if (mutates && figma.editorType === "dev")
4404
+ throw codeError("Dev Mode is read-only: switch the file to Design mode to let the agent edit it.", "WRONG_EDITOR");
2984
4405
  const result = await handler(msg.params || {}, Math.min(Number(msg.timeoutMs) || 30000, 120000), String(msg.id));
2985
4406
  reply = { t: "res", id: msg.id, ok: true, result };
2986
4407
  } catch (e) {
@@ -3282,6 +4703,18 @@
3282
4703
  }
3283
4704
  };
3284
4705
  }
4706
+ async function nodeInfo(p) {
4707
+ const ids = Array.isArray(p.ids) ? p.ids.slice(0, 500) : [];
4708
+ const out = {};
4709
+ for (let i = 0;i < ids.length; i++) {
4710
+ const n = await figma.getNodeByIdAsync(String(ids[i]));
4711
+ if (!n)
4712
+ continue;
4713
+ const pg = pageOf(n);
4714
+ out[ids[i]] = { name: n.name, type: n.type, page: pg ? pg.name : undefined };
4715
+ }
4716
+ return out;
4717
+ }
3285
4718
  var fontCache = null;
3286
4719
  async function listFonts(p) {
3287
4720
  if (!fontCache)