@clovis500c/figma-bridge 1.5.0 → 1.8.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)
@@ -1109,7 +1757,7 @@
1109
1757
  }
1110
1758
 
1111
1759
  // plugin/lib/build.ts
1112
- var MAX_NODES2 = 3000;
1760
+ var MAX_NODES3 = 3000;
1113
1761
  var SHADOW_DEFAULT = { x: 0, y: 4, blur: 16, spread: 0, color: "#0000001F" };
1114
1762
  async function build(p, _timeoutMs, requestId) {
1115
1763
  const spec = p.spec;
@@ -1264,8 +1912,8 @@
1264
1912
  ctx.warnings.push(path + ": not an object, skipped");
1265
1913
  return null;
1266
1914
  }
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");
1915
+ if (ctx.count >= MAX_NODES3)
1916
+ throw codeError("Spec is too large (max " + MAX_NODES3 + " nodes). Split it into several build calls.", "TOO_LARGE");
1269
1917
  checkCancelled(ctx.requestId);
1270
1918
  const type = nodeType(s);
1271
1919
  let node;
@@ -1586,7 +2234,7 @@
1586
2234
  if (typeof color === "string" && color.indexOf("style:") === 0) {
1587
2235
  await t.setRangeFillStyleIdAsync(at, end, (await findStyle("paint", stripPrefix(color))).id);
1588
2236
  } else if (color !== undefined) {
1589
- t.setRangeFills(at, end, [await toPaint(color)]);
2237
+ t.setRangeFills(at, end, [await toPaint(color, ctx.images)]);
1590
2238
  }
1591
2239
  const deco = { underline: "UNDERLINE", strike: "STRIKETHROUGH", strikethrough: "STRIKETHROUGH", none: "NONE" };
1592
2240
  if (sp.link) {
@@ -1596,7 +2244,7 @@
1596
2244
  }
1597
2245
  if (sp.decoration && deco[sp.decoration])
1598
2246
  t.setRangeTextDecoration(at, end, deco[sp.decoration]);
1599
- const cases = { upper: "UPPER", lower: "LOWER", title: "TITLE" };
2247
+ const cases = { upper: "UPPER", lower: "LOWER", title: "TITLE", none: "ORIGINAL" };
1600
2248
  if (sp.case && cases[sp.case])
1601
2249
  t.setRangeTextCase(at, end, cases[sp.case]);
1602
2250
  } catch (e) {
@@ -1782,10 +2430,20 @@
1782
2430
  async function applyVisuals(node, s, ctx, type) {
1783
2431
  const fill = s.fill !== undefined ? s.fill : type === "text" ? s.color : undefined;
1784
2432
  if (fill !== undefined && "fills" in node && type !== "image")
1785
- await setPaints(node, "fills", fill);
2433
+ await setPaints(node, "fills", fill, ctx.images);
1786
2434
  if (s.stroke !== undefined && "strokes" in node) {
1787
- await setPaints(node, "strokes", s.stroke);
1788
- node.strokeWeight = typeof s.strokeWidth === "number" ? s.strokeWidth : 1;
2435
+ await setPaints(node, "strokes", s.stroke, ctx.images);
2436
+ if (Array.isArray(s.strokeWidth) && "strokeTopWeight" in node) {
2437
+ const w = s.strokeWidth.map(Number);
2438
+ node.strokeTopWeight = w[0] || 0;
2439
+ node.strokeRightWeight = w[1] === undefined ? w[0] || 0 : w[1] || 0;
2440
+ node.strokeBottomWeight = w[2] === undefined ? w[0] || 0 : w[2] || 0;
2441
+ node.strokeLeftWeight = w[3] === undefined ? w[1] === undefined ? w[0] || 0 : w[1] || 0 : w[3] || 0;
2442
+ } else {
2443
+ node.strokeWeight = typeof s.strokeWidth === "number" ? s.strokeWidth : 1;
2444
+ }
2445
+ if (Array.isArray(s.strokeDash) && "dashPattern" in node)
2446
+ node.dashPattern = s.strokeDash.map(Number);
1789
2447
  if ("strokeAlign" in node && type !== "line" && type !== "text") {
1790
2448
  node.strokeAlign = String(s.strokeAlign || "inside").toUpperCase();
1791
2449
  }
@@ -1843,7 +2501,7 @@
1843
2501
  const c = parseHex(hex);
1844
2502
  return { type: "SOLID", color: { r: c.r, g: c.g, b: c.b }, opacity: c.a };
1845
2503
  }
1846
- async function setPaints(node, field, value) {
2504
+ async function setPaints(node, field, value, images) {
1847
2505
  if (typeof value === "string" && value.indexOf("style:") === 0) {
1848
2506
  const style = await findStyle("paint", stripPrefix(value));
1849
2507
  if (field === "fills")
@@ -1855,10 +2513,11 @@
1855
2513
  const list = value === null || value === "none" ? [] : Array.isArray(value) ? value : [value];
1856
2514
  const paints = [];
1857
2515
  for (let i = 0;i < list.length; i++)
1858
- paints.push(await toPaint(list[i]));
2516
+ paints.push(await toPaint(list[i], images));
1859
2517
  node[field] = paints;
1860
2518
  }
1861
- async function toPaint(v) {
2519
+ var FITS = { fill: "FILL", cover: "FILL", fit: "FIT", contain: "FIT", crop: "CROP", tile: "TILE" };
2520
+ async function toPaint(v, images) {
1862
2521
  if (typeof v === "string") {
1863
2522
  if (v.indexOf("var:") === 0) {
1864
2523
  const variable = await findVariable(stripPrefix(v));
@@ -1886,6 +2545,15 @@
1886
2545
  ];
1887
2546
  return { type: radial ? "GRADIENT_RADIAL" : "GRADIENT_LINEAR", gradientTransform: transform, gradientStops: stops };
1888
2547
  }
2548
+ if (v && v.imageKey !== undefined) {
2549
+ const bytes = images && images[v.imageKey];
2550
+ if (!bytes)
2551
+ throw codeError("Image fill data missing (use {image: src} with the build tool)", "BAD_ARGS");
2552
+ const paint = { type: "IMAGE", imageHash: figma.createImage(bytes).hash, scaleMode: FITS[String(v.fit || "fill").toLowerCase()] || "FILL" };
2553
+ if (typeof v.opacity === "number")
2554
+ return Object.assign({}, paint, { opacity: v.opacity });
2555
+ return paint;
2556
+ }
1889
2557
  if (v && typeof v.type === "string")
1890
2558
  return v;
1891
2559
  throw codeError("Invalid paint: " + JSON.stringify(v), "BAD_ARGS");
@@ -2313,7 +2981,7 @@
2313
2981
  }
2314
2982
 
2315
2983
  // plugin/lib/export.ts
2316
- var MAX_NODES3 = 1500;
2984
+ var MAX_NODES4 = 1500;
2317
2985
  var MAX_ASSET_BYTES = 40 << 20;
2318
2986
  var VECTOR_TYPES = ["VECTOR", "BOOLEAN_OPERATION", "STAR", "POLYGON", "ELLIPSE", "LINE"];
2319
2987
  var VISUAL_CSS = [
@@ -2423,6 +3091,11 @@
2423
3091
  }
2424
3092
  if (n.rotation && Math.abs(n.rotation) > 0.01 && n.type !== "TEXT")
2425
3093
  out.rotation = round(n.rotation);
3094
+ if (n.type === "INSTANCE") {
3095
+ const info = await instanceInfo(n);
3096
+ if (info)
3097
+ out.component = info;
3098
+ }
2426
3099
  let css = {};
2427
3100
  try {
2428
3101
  css = await n.getCSSAsync();
@@ -2487,7 +3160,7 @@
2487
3160
  const c = n.children[i];
2488
3161
  if (!c.visible)
2489
3162
  continue;
2490
- if (ctx.count >= MAX_NODES3) {
3163
+ if (ctx.count >= MAX_NODES4) {
2491
3164
  ctx.truncated = true;
2492
3165
  break;
2493
3166
  }
@@ -2530,6 +3203,33 @@
2530
3203
  })
2531
3204
  };
2532
3205
  }
3206
+ async function instanceInfo(n) {
3207
+ let main = null;
3208
+ try {
3209
+ main = await n.getMainComponentAsync();
3210
+ } catch (e) {}
3211
+ if (!main)
3212
+ return null;
3213
+ const set = main.parent && main.parent.type === "COMPONENT_SET" ? main.parent.name : null;
3214
+ const props = {};
3215
+ const defs = n.componentProperties;
3216
+ const keys = Object.keys(defs);
3217
+ for (let i = 0;i < keys.length; i++) {
3218
+ const d = defs[keys[i]];
3219
+ if (d.type === "INSTANCE_SWAP")
3220
+ continue;
3221
+ props[keys[i].split("#")[0]] = d.value;
3222
+ }
3223
+ const info = { name: set || main.name, props };
3224
+ if (set)
3225
+ info.variant = main.name;
3226
+ const text = n.findOne(function(t) {
3227
+ return t.type === "TEXT" && t.visible;
3228
+ });
3229
+ if (text && text.characters.trim())
3230
+ info.text = text.characters.trim().slice(0, 200);
3231
+ return info;
3232
+ }
2533
3233
 
2534
3234
  // plugin/lib/design-system.ts
2535
3235
  var ALL = ["colors", "text", "effects", "variables", "components"];
@@ -2650,6 +3350,183 @@
2650
3350
  return v;
2651
3351
  }
2652
3352
 
3353
+ // plugin/lib/tokens-export.ts
3354
+ var TYPES2 = { COLOR: "color", FLOAT: "number", STRING: "string", BOOLEAN: "boolean" };
3355
+ async function exportTokens(p) {
3356
+ const only = Array.isArray(p.collections) && p.collections.length ? p.collections : null;
3357
+ const collections = await figma.variables.getLocalVariableCollectionsAsync();
3358
+ const vars = await figma.variables.getLocalVariablesAsync();
3359
+ const byId = {};
3360
+ for (let i = 0;i < vars.length; i++)
3361
+ byId[vars[i].id] = vars[i];
3362
+ const collName = {};
3363
+ for (let i = 0;i < collections.length; i++)
3364
+ collName[collections[i].id] = collections[i].name;
3365
+ const warnings = [];
3366
+ const aliasOf = async function(id) {
3367
+ let v = byId[id] || null;
3368
+ if (!v) {
3369
+ try {
3370
+ v = await figma.variables.getVariableByIdAsync(id);
3371
+ } catch (e) {}
3372
+ if (v)
3373
+ byId[id] = v;
3374
+ }
3375
+ if (!v)
3376
+ return { alias: { collection: "", name: id, missing: true } };
3377
+ let collection = collName[v.variableCollectionId];
3378
+ if (collection === undefined) {
3379
+ try {
3380
+ const c = await figma.variables.getVariableCollectionByIdAsync(v.variableCollectionId);
3381
+ collection = c ? c.name : "";
3382
+ } catch (e) {
3383
+ collection = "";
3384
+ }
3385
+ collName[v.variableCollectionId] = collection;
3386
+ }
3387
+ const out = { alias: { collection, name: v.name } };
3388
+ if (v.remote)
3389
+ out.alias.remote = true;
3390
+ return out;
3391
+ };
3392
+ const value = async function(raw, type) {
3393
+ if (raw && typeof raw === "object" && raw.type === "VARIABLE_ALIAS")
3394
+ return aliasOf(raw.id);
3395
+ if (type === "COLOR" && raw && typeof raw === "object")
3396
+ return toHex(raw);
3397
+ if (type === "FLOAT")
3398
+ return round(Number(raw));
3399
+ return raw;
3400
+ };
3401
+ const outCollections = [];
3402
+ for (let c = 0;c < collections.length; c++) {
3403
+ const col = collections[c];
3404
+ if (only && only.indexOf(col.name) === -1)
3405
+ continue;
3406
+ const modes = col.modes.slice().sort(function(a, b) {
3407
+ return (a.modeId === col.defaultModeId ? 0 : 1) - (b.modeId === col.defaultModeId ? 0 : 1);
3408
+ });
3409
+ const list = [];
3410
+ for (let i = 0;i < vars.length; i++) {
3411
+ const v = vars[i];
3412
+ if (v.variableCollectionId !== col.id)
3413
+ continue;
3414
+ const values = {};
3415
+ for (let m = 0;m < modes.length; m++)
3416
+ values[modes[m].name] = await value(v.valuesByMode[modes[m].modeId], v.resolvedType);
3417
+ const item = { name: v.name, type: TYPES2[v.resolvedType] || "string", values };
3418
+ if (v.description)
3419
+ item.description = v.description;
3420
+ if (v.scopes && v.scopes.length && !(v.scopes.length === 1 && v.scopes[0] === "ALL_SCOPES"))
3421
+ item.scopes = v.scopes.slice();
3422
+ if (v.hiddenFromPublishing)
3423
+ item.hidden = true;
3424
+ list.push(item);
3425
+ }
3426
+ outCollections.push({
3427
+ name: col.name,
3428
+ modes: modes.map(function(m) {
3429
+ return m.name;
3430
+ }),
3431
+ variables: list
3432
+ });
3433
+ }
3434
+ const styles = await localStyles();
3435
+ const paint = [];
3436
+ for (let i = 0;i < styles.paint.length; i++) {
3437
+ const s = styles.paint[i];
3438
+ const paints = [];
3439
+ for (let k = 0;k < s.paints.length; k++) {
3440
+ const pt = s.paints[k];
3441
+ if (pt.visible === false)
3442
+ continue;
3443
+ if (pt.type === "SOLID") {
3444
+ const bound = pt.boundVariables && pt.boundVariables.color;
3445
+ paints.push(bound ? await aliasOf(bound.id) : toHex(pt.color, pt.opacity));
3446
+ } else if (pt.type.indexOf("GRADIENT") === 0) {
3447
+ const t = pt.gradientTransform;
3448
+ const g = {
3449
+ gradient: pt.gradientStops.map(function(st) {
3450
+ return { color: toHex(st.color), at: round(st.position) };
3451
+ })
3452
+ };
3453
+ if (pt.type === "GRADIENT_LINEAR")
3454
+ g.angle = round(Math.atan2(t[0][1], t[0][0]) * 180 / Math.PI);
3455
+ else
3456
+ g.type = pt.type.replace("GRADIENT_", "").toLowerCase();
3457
+ paints.push(g);
3458
+ } else {
3459
+ warnings.push('Paint style "' + s.name + '": ' + pt.type.toLowerCase() + " paints are not exported");
3460
+ }
3461
+ }
3462
+ if (!paints.length)
3463
+ continue;
3464
+ const item = { name: s.name, value: paints.length === 1 ? paints[0] : paints };
3465
+ if (s.description)
3466
+ item.description = s.description;
3467
+ paint.push(item);
3468
+ }
3469
+ const text = [];
3470
+ for (let i = 0;i < styles.text.length; i++) {
3471
+ const s = styles.text[i];
3472
+ const item = { name: s.name, font: s.fontName.family, style: s.fontName.style, size: round(s.fontSize) };
3473
+ const lh = s.lineHeight;
3474
+ if (lh.unit === "PIXELS")
3475
+ item.lineHeight = round(lh.value);
3476
+ else if (lh.unit === "PERCENT")
3477
+ item.lineHeight = round(lh.value) + "%";
3478
+ if (s.letterSpacing.value)
3479
+ item.letterSpacing = s.letterSpacing.unit === "PERCENT" ? round(s.letterSpacing.value) + "%" : round(s.letterSpacing.value);
3480
+ if (s.textCase && s.textCase !== "ORIGINAL")
3481
+ item.case = s.textCase === "UPPER" ? "upper" : s.textCase === "LOWER" ? "lower" : "title";
3482
+ if (s.textDecoration && s.textDecoration !== "NONE")
3483
+ item.decoration = s.textDecoration === "UNDERLINE" ? "underline" : "strike";
3484
+ if (s.paragraphSpacing)
3485
+ item.paragraphSpacing = round(s.paragraphSpacing);
3486
+ const bound = s.boundVariables || {};
3487
+ const keys = Object.keys(bound);
3488
+ if (keys.length) {
3489
+ item.variables = {};
3490
+ for (let k = 0;k < keys.length; k++)
3491
+ if (bound[keys[k]] && bound[keys[k]].id)
3492
+ item.variables[keys[k]] = (await aliasOf(bound[keys[k]].id)).alias;
3493
+ }
3494
+ if (s.description)
3495
+ item.description = s.description;
3496
+ text.push(item);
3497
+ }
3498
+ const effect = [];
3499
+ for (let i = 0;i < styles.effect.length; i++) {
3500
+ const s = styles.effect[i];
3501
+ const list = [];
3502
+ for (let k = 0;k < s.effects.length; k++) {
3503
+ const e = s.effects[k];
3504
+ if (e.visible === false)
3505
+ continue;
3506
+ if (e.type === "DROP_SHADOW" || e.type === "INNER_SHADOW") {
3507
+ 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) };
3508
+ if (e.type === "INNER_SHADOW")
3509
+ sh.inner = true;
3510
+ if (e.boundVariables && e.boundVariables.color)
3511
+ sh.colorVariable = (await aliasOf(e.boundVariables.color.id)).alias;
3512
+ list.push(sh);
3513
+ } else {
3514
+ list.push({ type: e.type === "LAYER_BLUR" ? "layer" : "background", blur: round(e.radius) });
3515
+ }
3516
+ }
3517
+ if (!list.length)
3518
+ continue;
3519
+ const item = { name: s.name, value: list };
3520
+ if (s.description)
3521
+ item.description = s.description;
3522
+ effect.push(item);
3523
+ }
3524
+ const out = { fileName: figma.root.name, collections: outCollections, styles: { colors: paint, text, effects: effect } };
3525
+ if (warnings.length)
3526
+ out.warnings = warnings.slice(0, 40);
3527
+ return out;
3528
+ }
3529
+
2653
3530
  // plugin/lib/find.ts
2654
3531
  var MAX_LIMIT = 500;
2655
3532
  function matcher(q) {
@@ -2831,7 +3708,7 @@
2831
3708
  finish(id, codeError("The user cancelled the selection request", "CANCELLED"));
2832
3709
  }
2833
3710
  // package.json
2834
- var version = "1.5.0";
3711
+ var version = "1.8.0";
2835
3712
 
2836
3713
  // plugin/code.ts
2837
3714
  var DEFAULT_SIZE = { width: 340, height: 540 };
@@ -2942,6 +3819,7 @@
2942
3819
  find: find2,
2943
3820
  get_design_system: getDesignSystem,
2944
3821
  design_tokens: designTokens,
3822
+ export_tokens: exportTokens,
2945
3823
  audit,
2946
3824
  get_css: getCss,
2947
3825
  checkpoint,
@@ -2962,6 +3840,7 @@
2962
3840
  describe: true,
2963
3841
  find: true,
2964
3842
  get_design_system: true,
3843
+ export_tokens: true,
2965
3844
  audit: true,
2966
3845
  get_css: true,
2967
3846
  screenshot: true,
@@ -2973,7 +3852,7 @@
2973
3852
  };
2974
3853
  async function handleRequest(msg) {
2975
3854
  const started = Date.now();
2976
- const mutates = !READ_ONLY[msg.method];
3855
+ const mutates = !READ_ONLY[msg.method] || !!(msg.params && (msg.params.fix === true || msg.params.fixes && msg.params.fixes.length));
2977
3856
  let reply;
2978
3857
  if (mutates)
2979
3858
  commitUndo();