figma-coder-mcp 0.2.1 → 0.3.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/LICENSE +21 -21
- package/README.md +124 -72
- package/dist/bin.js +920 -142
- package/package.json +5 -5
package/dist/bin.js
CHANGED
|
@@ -295,7 +295,8 @@ var require_client = __commonJS({
|
|
|
295
295
|
async renderImages(fileKey, nodeIds, format = "svg", requestAuth, scale = 1) {
|
|
296
296
|
const auth = this.resolveAuth(requestAuth);
|
|
297
297
|
const ids = nodeIds.map(url_1.normalizeNodeId).join(",");
|
|
298
|
-
const
|
|
298
|
+
const svgOpts = format === "svg" ? "&svg_outline_text=true&svg_simplify_stroke=true" : "";
|
|
299
|
+
const data = await this.get(`/images/${fileKey}?ids=${encodeURIComponent(ids)}&format=${format}&scale=${scale}${svgOpts}`, auth);
|
|
299
300
|
if (data.err) {
|
|
300
301
|
throw new errors_1.FigmaApiError(`Figma image render error: ${data.err}`, 502);
|
|
301
302
|
}
|
|
@@ -361,8 +362,9 @@ var require_style_ir = __commonJS({
|
|
|
361
362
|
"../packages/figma-core/dist/converter/style-ir.js"(exports) {
|
|
362
363
|
"use strict";
|
|
363
364
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
364
|
-
exports.StyleIr = void 0;
|
|
365
|
+
exports.StyleIr = exports.IMAGE_FILL_TOKEN = void 0;
|
|
365
366
|
var color_1 = require_color();
|
|
367
|
+
exports.IMAGE_FILL_TOKEN = "__FIGMA_IMAGE__";
|
|
366
368
|
var ALIGN_MAIN = {
|
|
367
369
|
MIN: "flex-start",
|
|
368
370
|
CENTER: "center",
|
|
@@ -375,7 +377,7 @@ var require_style_ir = __commonJS({
|
|
|
375
377
|
MAX: "flex-end",
|
|
376
378
|
BASELINE: "baseline"
|
|
377
379
|
};
|
|
378
|
-
var StyleIr = class {
|
|
380
|
+
var StyleIr = class _StyleIr {
|
|
379
381
|
/** Convert a Figma node subtree into the Style IR tree. */
|
|
380
382
|
convert(root) {
|
|
381
383
|
return this.walk(root, void 0);
|
|
@@ -392,10 +394,22 @@ var require_style_ir = __commonJS({
|
|
|
392
394
|
this.applyEffects(node, css);
|
|
393
395
|
}
|
|
394
396
|
this.applyOpacity(node, css);
|
|
397
|
+
this.applyBlend(node, css);
|
|
395
398
|
const isText = node.type === "TEXT";
|
|
396
399
|
if (isText)
|
|
397
400
|
this.applyText(node, css);
|
|
398
401
|
const tag = isText ? "p" : "div";
|
|
402
|
+
if (asset?.kind === "vector") {
|
|
403
|
+
return {
|
|
404
|
+
id: node.id,
|
|
405
|
+
name: node.name,
|
|
406
|
+
figmaType: node.type,
|
|
407
|
+
tag,
|
|
408
|
+
css,
|
|
409
|
+
asset,
|
|
410
|
+
children: []
|
|
411
|
+
};
|
|
412
|
+
}
|
|
399
413
|
const visibleChildren = (node.children ?? []).filter((c) => c.visible !== false);
|
|
400
414
|
const maskChild = visibleChildren.find((c) => c.isMask);
|
|
401
415
|
if (maskChild) {
|
|
@@ -405,17 +419,79 @@ var require_style_ir = __commonJS({
|
|
|
405
419
|
this.applyRadius(maskChild, css);
|
|
406
420
|
}
|
|
407
421
|
const children = visibleChildren.map((c) => this.walk(c, node));
|
|
422
|
+
const runs = isText ? this.textRuns(node) : void 0;
|
|
408
423
|
return {
|
|
409
424
|
id: node.id,
|
|
410
425
|
name: node.name,
|
|
411
426
|
figmaType: node.type,
|
|
412
427
|
tag,
|
|
413
428
|
...isText ? { text: node.characters ?? "" } : {},
|
|
429
|
+
...runs ? { runs } : {},
|
|
414
430
|
css,
|
|
415
431
|
...asset ? { asset } : {},
|
|
416
432
|
children
|
|
417
433
|
};
|
|
418
434
|
}
|
|
435
|
+
/**
|
|
436
|
+
* Split a text node into styled runs when it has per-character overrides
|
|
437
|
+
* (a bold word, a colored span). Returns undefined for plain single-style
|
|
438
|
+
* text (the common case) so the renderer emits one string. `css` per run holds
|
|
439
|
+
* ONLY the overridden props; the <p> base style supplies the rest.
|
|
440
|
+
*/
|
|
441
|
+
textRuns(node) {
|
|
442
|
+
const chars = node.characters ?? "";
|
|
443
|
+
const overrides = node.characterStyleOverrides;
|
|
444
|
+
const table = node.styleOverrideTable;
|
|
445
|
+
if (!chars || !overrides?.length || !table)
|
|
446
|
+
return void 0;
|
|
447
|
+
if (!overrides.some((id) => id && table[String(id)]))
|
|
448
|
+
return void 0;
|
|
449
|
+
const raw = [];
|
|
450
|
+
let curId = -1;
|
|
451
|
+
for (let i = 0; i < chars.length; i++) {
|
|
452
|
+
const id = overrides[i] ?? 0;
|
|
453
|
+
if (id !== curId || raw.length === 0) {
|
|
454
|
+
raw.push({ id, text: "" });
|
|
455
|
+
curId = id;
|
|
456
|
+
}
|
|
457
|
+
raw[raw.length - 1].text += chars[i];
|
|
458
|
+
}
|
|
459
|
+
if (raw.length <= 1)
|
|
460
|
+
return void 0;
|
|
461
|
+
return raw.map((r) => ({
|
|
462
|
+
text: r.text,
|
|
463
|
+
css: r.id && table[String(r.id)] ? this.overrideCss(table[String(r.id)]) : {}
|
|
464
|
+
}));
|
|
465
|
+
}
|
|
466
|
+
/** Map an override style entry (partial TypeStyle + optional fills) to CSS. */
|
|
467
|
+
overrideCss(o) {
|
|
468
|
+
const css = {};
|
|
469
|
+
if (o.fontWeight)
|
|
470
|
+
css["font-weight"] = String(o.fontWeight);
|
|
471
|
+
if (o.fontSize)
|
|
472
|
+
css["font-size"] = (0, color_1.px)(o.fontSize);
|
|
473
|
+
if (o.fontFamily)
|
|
474
|
+
css["font-family"] = `'${o.fontFamily}', sans-serif`;
|
|
475
|
+
if (o.italic)
|
|
476
|
+
css["font-style"] = "italic";
|
|
477
|
+
if (o.letterSpacing)
|
|
478
|
+
css["letter-spacing"] = (0, color_1.px)(o.letterSpacing);
|
|
479
|
+
if (o.lineHeightPx)
|
|
480
|
+
css["line-height"] = (0, color_1.px)(o.lineHeightPx);
|
|
481
|
+
if (o.textDecoration === "UNDERLINE")
|
|
482
|
+
css["text-decoration"] = "underline";
|
|
483
|
+
if (o.textDecoration === "STRIKETHROUGH")
|
|
484
|
+
css["text-decoration"] = "line-through";
|
|
485
|
+
if (o.textCase === "UPPER")
|
|
486
|
+
css["text-transform"] = "uppercase";
|
|
487
|
+
if (o.textCase === "LOWER")
|
|
488
|
+
css["text-transform"] = "lowercase";
|
|
489
|
+
const fills = this.visiblePaints(o.fills);
|
|
490
|
+
const p = fills[fills.length - 1];
|
|
491
|
+
if (p?.type === "SOLID" && p.color)
|
|
492
|
+
css.color = (0, color_1.figmaColorToCss)(p.color, p.opacity ?? 1);
|
|
493
|
+
return css;
|
|
494
|
+
}
|
|
419
495
|
// ---- layout -------------------------------------------------------------
|
|
420
496
|
applyLayout(node, parent, css) {
|
|
421
497
|
const auto = node.layoutMode === "HORIZONTAL" || node.layoutMode === "VERTICAL";
|
|
@@ -457,8 +533,60 @@ var require_style_ir = __commonJS({
|
|
|
457
533
|
const box = node.absoluteBoundingBox;
|
|
458
534
|
if (!box)
|
|
459
535
|
return;
|
|
460
|
-
|
|
461
|
-
|
|
536
|
+
this.applyAxisSize(node, parent, css, "H", box.width);
|
|
537
|
+
this.applyAxisSize(node, parent, css, "V", box.height);
|
|
538
|
+
if (node.minWidth != null)
|
|
539
|
+
css["min-width"] = (0, color_1.px)(node.minWidth);
|
|
540
|
+
if (node.maxWidth != null)
|
|
541
|
+
css["max-width"] = (0, color_1.px)(node.maxWidth);
|
|
542
|
+
if (node.minHeight != null)
|
|
543
|
+
css["min-height"] = (0, color_1.px)(node.minHeight);
|
|
544
|
+
if (node.maxHeight != null)
|
|
545
|
+
css["max-height"] = (0, color_1.px)(node.maxHeight);
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* Size one axis. Inside an auto-layout parent we honor Figma's sizing intent —
|
|
549
|
+
* FILL → grow (flex-1) or stretch the cross axis, HUG → let content size it,
|
|
550
|
+
* FIXED → px — instead of freezing every box to its pixel dimensions. Outside
|
|
551
|
+
* auto-layout (absolute children, root) we keep the exact px.
|
|
552
|
+
*/
|
|
553
|
+
applyAxisSize(node, parent, css, axis, boxDim) {
|
|
554
|
+
const dim = axis === "H" ? "width" : "height";
|
|
555
|
+
const parentMainHorizontal = parent?.layoutMode === "HORIZONTAL";
|
|
556
|
+
const parentAuto = parentMainHorizontal || parent?.layoutMode === "VERTICAL";
|
|
557
|
+
const sizing = this.axisSizing(node, axis, parentMainHorizontal);
|
|
558
|
+
if (!parentAuto || !sizing) {
|
|
559
|
+
css[dim] = (0, color_1.px)(boxDim);
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
const isMainAxis = axis === "H" === parentMainHorizontal;
|
|
563
|
+
if (sizing === "FILL") {
|
|
564
|
+
if (isMainAxis)
|
|
565
|
+
css.flex = "1 1 0%";
|
|
566
|
+
else
|
|
567
|
+
css["align-self"] = "stretch";
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
if (sizing === "HUG") {
|
|
571
|
+
const hasContent = (node.children?.length ?? 0) > 0 || node.type === "TEXT";
|
|
572
|
+
if (!hasContent)
|
|
573
|
+
css[dim] = (0, color_1.px)(boxDim);
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
css[dim] = (0, color_1.px)(boxDim);
|
|
577
|
+
}
|
|
578
|
+
/** Resolve a node's sizing mode on an axis, falling back from the modern
|
|
579
|
+
* layoutSizing* fields to the legacy layoutGrow/layoutAlign. */
|
|
580
|
+
axisSizing(node, axis, parentMainHorizontal) {
|
|
581
|
+
const explicit = axis === "H" ? node.layoutSizingHorizontal : node.layoutSizingVertical;
|
|
582
|
+
if (explicit)
|
|
583
|
+
return explicit;
|
|
584
|
+
const isMainAxis = axis === "H" === parentMainHorizontal;
|
|
585
|
+
if (isMainAxis && node.layoutGrow === 1)
|
|
586
|
+
return "FILL";
|
|
587
|
+
if (!isMainAxis && node.layoutAlign === "STRETCH")
|
|
588
|
+
return "FILL";
|
|
589
|
+
return void 0;
|
|
462
590
|
}
|
|
463
591
|
// ---- paints -------------------------------------------------------------
|
|
464
592
|
visiblePaints(paints) {
|
|
@@ -472,22 +600,109 @@ var require_style_ir = __commonJS({
|
|
|
472
600
|
const fills = this.visiblePaints(node.fills);
|
|
473
601
|
if (!fills.length)
|
|
474
602
|
return;
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
603
|
+
if (fills.length === 1) {
|
|
604
|
+
const only = fills[0];
|
|
605
|
+
if (only.type === "SOLID" && only.color) {
|
|
606
|
+
css["background-color"] = (0, color_1.figmaColorToCss)(only.color, only.opacity ?? 1);
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
if (only.type === "IMAGE" && asset?.kind === "image") {
|
|
610
|
+
css["background-image"] = `url(${exports.IMAGE_FILL_TOKEN})`;
|
|
611
|
+
this.applyImageSizing(only, css);
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
const layers = [];
|
|
616
|
+
let hasImage = false;
|
|
617
|
+
for (let i = fills.length - 1; i >= 0; i--) {
|
|
618
|
+
const paint = fills[i];
|
|
619
|
+
if (paint.type === "IMAGE") {
|
|
620
|
+
if (asset?.kind !== "image")
|
|
621
|
+
continue;
|
|
622
|
+
layers.push(`url(${exports.IMAGE_FILL_TOKEN})`);
|
|
623
|
+
hasImage = true;
|
|
624
|
+
continue;
|
|
625
|
+
}
|
|
626
|
+
const layer = this.paintToLayer(paint);
|
|
627
|
+
if (layer)
|
|
628
|
+
layers.push(layer);
|
|
629
|
+
}
|
|
630
|
+
if (!layers.length)
|
|
631
|
+
return;
|
|
632
|
+
css["background-image"] = layers.join(", ");
|
|
633
|
+
if (hasImage) {
|
|
483
634
|
css["background-size"] = "cover";
|
|
484
635
|
css["background-position"] = "center";
|
|
485
636
|
}
|
|
486
637
|
}
|
|
487
|
-
|
|
638
|
+
/**
|
|
639
|
+
* Translate a Figma IMAGE paint's fit (scaleMode + crop transform) into CSS
|
|
640
|
+
* background sizing. Previously we always emitted cover/center, which only
|
|
641
|
+
* matches FILL-without-crop; FIT/TILE/STRETCH and cropped fills rendered the
|
|
642
|
+
* wrong region/scale.
|
|
643
|
+
*/
|
|
644
|
+
applyImageSizing(paint, css) {
|
|
645
|
+
const pct = (n) => `${Number(n.toFixed(3))}%`;
|
|
646
|
+
const m = paint.imageTransform;
|
|
647
|
+
if (m && m.length >= 2 && m[0].length >= 3) {
|
|
648
|
+
const sx = m[0][0];
|
|
649
|
+
const sy = m[1][1];
|
|
650
|
+
const tx = m[0][2];
|
|
651
|
+
const ty = m[1][2];
|
|
652
|
+
if (sx > 0 && sy > 0 && sx < 1 && sy < 1) {
|
|
653
|
+
css["background-size"] = `${pct(100 / sx)} ${pct(100 / sy)}`;
|
|
654
|
+
css["background-position"] = `${pct(tx / (1 - sx) * 100)} ${pct(ty / (1 - sy) * 100)}`;
|
|
655
|
+
css["background-repeat"] = "no-repeat";
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
switch (paint.scaleMode) {
|
|
660
|
+
case "FIT":
|
|
661
|
+
css["background-size"] = "contain";
|
|
662
|
+
css["background-position"] = "center";
|
|
663
|
+
css["background-repeat"] = "no-repeat";
|
|
664
|
+
break;
|
|
665
|
+
case "STRETCH":
|
|
666
|
+
css["background-size"] = "100% 100%";
|
|
667
|
+
break;
|
|
668
|
+
case "TILE":
|
|
669
|
+
css["background-repeat"] = "repeat";
|
|
670
|
+
break;
|
|
671
|
+
case "FILL":
|
|
672
|
+
default:
|
|
673
|
+
css["background-size"] = "cover";
|
|
674
|
+
css["background-position"] = "center";
|
|
675
|
+
break;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
/** A single Figma paint as one CSS `background-image` layer (or null if it
|
|
679
|
+
* carries no renderable color, e.g. an image handled separately). */
|
|
680
|
+
paintToLayer(paint) {
|
|
681
|
+
if (paint.type === "SOLID" && paint.color) {
|
|
682
|
+
const c = (0, color_1.figmaColorToCss)(paint.color, paint.opacity ?? 1);
|
|
683
|
+
return `linear-gradient(${c}, ${c})`;
|
|
684
|
+
}
|
|
685
|
+
if (paint.type === "GRADIENT_LINEAR")
|
|
686
|
+
return this.linearGradient(paint);
|
|
687
|
+
if (paint.type === "GRADIENT_RADIAL")
|
|
688
|
+
return this.radialGradient(paint);
|
|
689
|
+
if (paint.type === "GRADIENT_ANGULAR")
|
|
690
|
+
return this.angularGradient(paint);
|
|
691
|
+
if (paint.type === "GRADIENT_DIAMOND")
|
|
692
|
+
return this.radialGradient(paint);
|
|
693
|
+
return null;
|
|
694
|
+
}
|
|
695
|
+
/** Comma-joined `color pos%` stops shared by every gradient kind. */
|
|
696
|
+
gradientStops(paint) {
|
|
488
697
|
const stops = paint.gradientStops ?? [];
|
|
489
698
|
if (stops.length < 2)
|
|
490
699
|
return null;
|
|
700
|
+
return stops.map((s) => `${(0, color_1.figmaColorToCss)(s.color, paint.opacity ?? 1)} ${Math.round(s.position * 100)}%`).join(", ");
|
|
701
|
+
}
|
|
702
|
+
linearGradient(paint) {
|
|
703
|
+
const stopStr = this.gradientStops(paint);
|
|
704
|
+
if (!stopStr)
|
|
705
|
+
return null;
|
|
491
706
|
let angle = 180;
|
|
492
707
|
const handles = paint.gradientHandlePositions;
|
|
493
708
|
if (handles && handles.length >= 2) {
|
|
@@ -495,19 +710,101 @@ var require_style_ir = __commonJS({
|
|
|
495
710
|
const dy = handles[1].y - handles[0].y;
|
|
496
711
|
angle = Math.round(Math.atan2(dy, dx) * 180 / Math.PI + 90);
|
|
497
712
|
}
|
|
498
|
-
const stopStr = stops.map((s) => `${(0, color_1.figmaColorToCss)(s.color, paint.opacity ?? 1)} ${Math.round(s.position * 100)}%`).join(", ");
|
|
499
713
|
return `linear-gradient(${angle}deg, ${stopStr})`;
|
|
500
714
|
}
|
|
715
|
+
/**
|
|
716
|
+
* Radial gradient. Figma's handles are normalized box coordinates: [0] is the
|
|
717
|
+
* center, [1] the end of one radius, [2] the end of the perpendicular radius.
|
|
718
|
+
* We map the center to a percentage position and each radius to a percentage
|
|
719
|
+
* of the box (an axis-aligned ellipse — CSS can't rotate a radial gradient, so
|
|
720
|
+
* a rotated Figma radial is approximated). Falls back to a centered circle.
|
|
721
|
+
*/
|
|
722
|
+
radialGradient(paint) {
|
|
723
|
+
const stopStr = this.gradientStops(paint);
|
|
724
|
+
if (!stopStr)
|
|
725
|
+
return null;
|
|
726
|
+
const h = paint.gradientHandlePositions;
|
|
727
|
+
if (!h || h.length < 3) {
|
|
728
|
+
return `radial-gradient(circle at center, ${stopStr})`;
|
|
729
|
+
}
|
|
730
|
+
const pct = (n) => `${Number((n * 100).toFixed(2))}%`;
|
|
731
|
+
const cx = h[0].x;
|
|
732
|
+
const cy = h[0].y;
|
|
733
|
+
const rx = Math.hypot(h[1].x - cx, h[1].y - cy);
|
|
734
|
+
const ry = Math.hypot(h[2].x - cx, h[2].y - cy);
|
|
735
|
+
return `radial-gradient(ellipse ${pct(rx)} ${pct(ry)} at ${pct(cx)} ${pct(cy)}, ${stopStr})`;
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* Angular (conic) gradient. Handle [0] is the center and [0]->[1] sets the
|
|
739
|
+
* start angle. CSS conic accepts percentage stops, matching Figma's 0..1.
|
|
740
|
+
*/
|
|
741
|
+
angularGradient(paint) {
|
|
742
|
+
const stopStr = this.gradientStops(paint);
|
|
743
|
+
if (!stopStr)
|
|
744
|
+
return null;
|
|
745
|
+
let angle = 0;
|
|
746
|
+
let cx = 0.5;
|
|
747
|
+
let cy = 0.5;
|
|
748
|
+
const h = paint.gradientHandlePositions;
|
|
749
|
+
if (h && h.length >= 2) {
|
|
750
|
+
cx = h[0].x;
|
|
751
|
+
cy = h[0].y;
|
|
752
|
+
angle = Math.round(Math.atan2(h[1].y - cy, h[1].x - cx) * 180 / Math.PI + 90);
|
|
753
|
+
}
|
|
754
|
+
const pct = (n) => `${Number((n * 100).toFixed(2))}%`;
|
|
755
|
+
return `conic-gradient(from ${angle}deg at ${pct(cx)} ${pct(cy)}, ${stopStr})`;
|
|
756
|
+
}
|
|
501
757
|
applyStroke(node, css) {
|
|
502
758
|
const strokes = this.visiblePaints(node.strokes);
|
|
503
|
-
if (!strokes.length
|
|
759
|
+
if (!strokes.length)
|
|
504
760
|
return;
|
|
505
761
|
const paint = strokes[strokes.length - 1];
|
|
506
|
-
if (paint.type
|
|
507
|
-
|
|
762
|
+
if (paint.type !== "SOLID" || !paint.color)
|
|
763
|
+
return;
|
|
764
|
+
const color = (0, color_1.figmaColorToCss)(paint.color, paint.opacity ?? 1);
|
|
765
|
+
if (node.type === "LINE") {
|
|
766
|
+
if (!node.strokeWeight)
|
|
767
|
+
return;
|
|
768
|
+
const box = node.absoluteBoundingBox;
|
|
769
|
+
const vertical = !!box && box.height > box.width;
|
|
770
|
+
css[vertical ? "border-left" : "border-top"] = `${(0, color_1.px)(node.strokeWeight)} solid ${color}`;
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
const ind = node.individualStrokeWeights;
|
|
774
|
+
if (ind && (ind.top !== ind.right || ind.right !== ind.bottom || ind.bottom !== ind.left)) {
|
|
775
|
+
const side = (w2, name) => {
|
|
776
|
+
if (w2 > 0)
|
|
777
|
+
css[`border-${name}`] = `${(0, color_1.px)(w2)} solid ${color}`;
|
|
778
|
+
};
|
|
779
|
+
side(ind.top, "top");
|
|
780
|
+
side(ind.right, "right");
|
|
781
|
+
side(ind.bottom, "bottom");
|
|
782
|
+
side(ind.left, "left");
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
if (!node.strokeWeight)
|
|
786
|
+
return;
|
|
787
|
+
const w = (0, color_1.px)(node.strokeWeight);
|
|
788
|
+
switch (node.strokeAlign) {
|
|
789
|
+
case "OUTSIDE":
|
|
790
|
+
css.outline = `${w} solid ${color}`;
|
|
791
|
+
css["outline-offset"] = "0px";
|
|
792
|
+
break;
|
|
793
|
+
case "CENTER":
|
|
794
|
+
css.outline = `${w} solid ${color}`;
|
|
795
|
+
css["outline-offset"] = `${(0, color_1.px)(-(node.strokeWeight / 2))}`;
|
|
796
|
+
break;
|
|
797
|
+
case "INSIDE":
|
|
798
|
+
default:
|
|
799
|
+
css.border = `${w} solid ${color}`;
|
|
800
|
+
break;
|
|
508
801
|
}
|
|
509
802
|
}
|
|
510
803
|
applyRadius(node, css) {
|
|
804
|
+
if (node.type === "ELLIPSE") {
|
|
805
|
+
css["border-radius"] = "50%";
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
511
808
|
if (node.rectangleCornerRadii) {
|
|
512
809
|
const [tl, tr, br, bl] = node.rectangleCornerRadii;
|
|
513
810
|
if (tl || tr || br || bl) {
|
|
@@ -581,25 +878,163 @@ var require_style_ir = __commonJS({
|
|
|
581
878
|
css["white-space"] = "pre";
|
|
582
879
|
else if (s.textAutoResize === "HEIGHT")
|
|
583
880
|
css["white-space"] = "pre-wrap";
|
|
881
|
+
if (s.textAlignVertical === "CENTER" || s.textAlignVertical === "BOTTOM") {
|
|
882
|
+
css.display = "flex";
|
|
883
|
+
css["flex-direction"] = "column";
|
|
884
|
+
css["justify-content"] = s.textAlignVertical === "CENTER" ? "center" : "flex-end";
|
|
885
|
+
}
|
|
584
886
|
}
|
|
585
887
|
const fills = this.visiblePaints(node.fills);
|
|
586
888
|
const paint = fills[fills.length - 1];
|
|
587
889
|
if (paint?.type === "SOLID" && paint.color) {
|
|
588
890
|
css.color = (0, color_1.figmaColorToCss)(paint.color, paint.opacity ?? 1);
|
|
891
|
+
} else if (paint) {
|
|
892
|
+
const layer = this.paintToLayer(paint);
|
|
893
|
+
if (layer) {
|
|
894
|
+
css["background-image"] = layer;
|
|
895
|
+
css["-webkit-background-clip"] = "text";
|
|
896
|
+
css["background-clip"] = "text";
|
|
897
|
+
css.color = "transparent";
|
|
898
|
+
}
|
|
589
899
|
}
|
|
590
900
|
}
|
|
901
|
+
/** Map a Figma layer blend mode to CSS `mix-blend-mode` (no-op for NORMAL). */
|
|
902
|
+
applyBlend(node, css) {
|
|
903
|
+
const bm = node.blendMode;
|
|
904
|
+
if (!bm || bm === "NORMAL" || bm === "PASS_THROUGH")
|
|
905
|
+
return;
|
|
906
|
+
const css_ = bm.toLowerCase().replace(/_/g, "-");
|
|
907
|
+
const allowed = /* @__PURE__ */ new Set([
|
|
908
|
+
"multiply",
|
|
909
|
+
"screen",
|
|
910
|
+
"overlay",
|
|
911
|
+
"darken",
|
|
912
|
+
"lighten",
|
|
913
|
+
"color-dodge",
|
|
914
|
+
"color-burn",
|
|
915
|
+
"hard-light",
|
|
916
|
+
"soft-light",
|
|
917
|
+
"difference",
|
|
918
|
+
"exclusion",
|
|
919
|
+
"hue",
|
|
920
|
+
"saturation",
|
|
921
|
+
"color",
|
|
922
|
+
"luminosity"
|
|
923
|
+
]);
|
|
924
|
+
if (allowed.has(css_))
|
|
925
|
+
css["mix-blend-mode"] = css_;
|
|
926
|
+
}
|
|
591
927
|
// ---- assets -------------------------------------------------------------
|
|
592
928
|
detectAsset(node) {
|
|
929
|
+
const imagePaint = [...this.visiblePaints(node.fills)].reverse().find((p) => p.type === "IMAGE" && p.imageRef);
|
|
593
930
|
const vectorTypes = ["VECTOR", "STAR", "LINE", "ELLIPSE", "REGULAR_POLYGON", "BOOLEAN_OPERATION"];
|
|
594
|
-
if (vectorTypes.includes(node.type))
|
|
931
|
+
if (vectorTypes.includes(node.type)) {
|
|
932
|
+
if (node.type === "ELLIPSE" && imagePaint) {
|
|
933
|
+
return { kind: "image", imageRef: imagePaint.imageRef };
|
|
934
|
+
}
|
|
935
|
+
if (this.isCssRenderableVector(node))
|
|
936
|
+
return void 0;
|
|
595
937
|
return { kind: "vector" };
|
|
596
|
-
|
|
938
|
+
}
|
|
597
939
|
if (imagePaint)
|
|
598
940
|
return { kind: "image", imageRef: imagePaint.imageRef };
|
|
941
|
+
if (this.isVectorOnlyContainer(node))
|
|
942
|
+
return { kind: "vector" };
|
|
599
943
|
return void 0;
|
|
600
944
|
}
|
|
945
|
+
/**
|
|
946
|
+
* Whether a vector-typed node is faithfully renderable with CSS, so it can
|
|
947
|
+
* skip server-side rasterization.
|
|
948
|
+
*
|
|
949
|
+
* - ELLIPSE / LINE encode their geometry in the node type, so CSS always
|
|
950
|
+
* reproduces them (circle/oval via border-radius:50%, line via a border).
|
|
951
|
+
* - VECTOR / BOOLEAN_OPERATION have arbitrary geometry the IR can't see, so
|
|
952
|
+
* they only qualify with a positive "rounded rectangle" signal (corner
|
|
953
|
+
* radius or a background blur — i.e. a panel) plus at least one paint we can
|
|
954
|
+
* render (a solid/gradient fill or a stroke-only outline).
|
|
955
|
+
*
|
|
956
|
+
* A rotated node never qualifies: the IR emits no transform, so the baked
|
|
957
|
+
* raster (which already carries the rotation) is the faithful fallback.
|
|
958
|
+
*/
|
|
959
|
+
isCssRenderableVector(node) {
|
|
960
|
+
if (this.hasRotation(node))
|
|
961
|
+
return false;
|
|
962
|
+
const fills = this.visiblePaints(node.fills);
|
|
963
|
+
if (fills.some((p) => p.type === "IMAGE"))
|
|
964
|
+
return false;
|
|
965
|
+
if (node.type === "ELLIPSE" || node.type === "LINE")
|
|
966
|
+
return true;
|
|
967
|
+
if (node.type === "VECTOR" || node.type === "BOOLEAN_OPERATION") {
|
|
968
|
+
const hasPaintableFill = fills.some((p) => p.type === "SOLID" || p.type === "GRADIENT_LINEAR");
|
|
969
|
+
if (!hasPaintableFill)
|
|
970
|
+
return false;
|
|
971
|
+
const hasRadius = (node.cornerRadius ?? 0) > 0 || (node.rectangleCornerRadii?.some((r) => r > 0) ?? false);
|
|
972
|
+
const hasBackgroundBlur = (node.effects ?? []).some((e) => e.type === "BACKGROUND_BLUR" && e.visible !== false);
|
|
973
|
+
return hasRadius || hasBackgroundBlur;
|
|
974
|
+
}
|
|
975
|
+
return false;
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* Whether a container should collapse into a single rendered SVG: its entire
|
|
979
|
+
* visible subtree is vector paths (no text, no image fills, no plain boxes)
|
|
980
|
+
* AND at least one of those paths actually needs rasterizing (a subtree of
|
|
981
|
+
* only CSS-renderable shapes is cheaper kept as elements).
|
|
982
|
+
*/
|
|
983
|
+
isVectorOnlyContainer(node) {
|
|
984
|
+
if (!_StyleIr.CONTAINER_TYPES.includes(node.type))
|
|
985
|
+
return false;
|
|
986
|
+
if (!node.absoluteBoundingBox)
|
|
987
|
+
return false;
|
|
988
|
+
const isAutoLayout = node.layoutMode === "HORIZONTAL" || node.layoutMode === "VERTICAL";
|
|
989
|
+
const childCount = (node.children ?? []).filter((c) => c.visible !== false).length;
|
|
990
|
+
if (isAutoLayout && childCount < _StyleIr.SVG_COLLAPSE_AUTOLAYOUT_THRESHOLD)
|
|
991
|
+
return false;
|
|
992
|
+
const scan = this.collapseScan(node);
|
|
993
|
+
return scan.pure && scan.hasRaster;
|
|
994
|
+
}
|
|
995
|
+
/**
|
|
996
|
+
* Classify a subtree for the collapse decision. `pure` is true only when every
|
|
997
|
+
* visible node is a vector shape; `hasRaster` is true when at least one of
|
|
998
|
+
* them would hit /v1/images (i.e. isn't reproducible as a CSS box).
|
|
999
|
+
*/
|
|
1000
|
+
collapseScan(node) {
|
|
1001
|
+
const NOT_PURE = { pure: false, hasRaster: false };
|
|
1002
|
+
if (node.visible === false)
|
|
1003
|
+
return { pure: true, hasRaster: false };
|
|
1004
|
+
if (node.type === "TEXT")
|
|
1005
|
+
return NOT_PURE;
|
|
1006
|
+
const hasImageFill = this.visiblePaints(node.fills).some((p) => p.type === "IMAGE" && p.imageRef);
|
|
1007
|
+
if (hasImageFill)
|
|
1008
|
+
return NOT_PURE;
|
|
1009
|
+
const vectorTypes = ["VECTOR", "STAR", "LINE", "ELLIPSE", "REGULAR_POLYGON", "BOOLEAN_OPERATION"];
|
|
1010
|
+
if (vectorTypes.includes(node.type)) {
|
|
1011
|
+
return { pure: true, hasRaster: !this.isCssRenderableVector(node) };
|
|
1012
|
+
}
|
|
1013
|
+
if (_StyleIr.CONTAINER_TYPES.includes(node.type)) {
|
|
1014
|
+
const kids = (node.children ?? []).filter((c) => c.visible !== false);
|
|
1015
|
+
if (!kids.length)
|
|
1016
|
+
return NOT_PURE;
|
|
1017
|
+
let hasRaster = false;
|
|
1018
|
+
for (const kid of kids) {
|
|
1019
|
+
const r = this.collapseScan(kid);
|
|
1020
|
+
if (!r.pure)
|
|
1021
|
+
return NOT_PURE;
|
|
1022
|
+
hasRaster = hasRaster || r.hasRaster;
|
|
1023
|
+
}
|
|
1024
|
+
return { pure: true, hasRaster };
|
|
1025
|
+
}
|
|
1026
|
+
return NOT_PURE;
|
|
1027
|
+
}
|
|
1028
|
+
/** True when the node is rotated beyond a tiny floating-point tolerance
|
|
1029
|
+
* (~0.5°). Figma reports rotation in radians; axis-aligned nodes carry
|
|
1030
|
+
* values like 8.7e-8 that must read as "not rotated". */
|
|
1031
|
+
hasRotation(node) {
|
|
1032
|
+
return typeof node.rotation === "number" && Math.abs(node.rotation) > 0.01;
|
|
1033
|
+
}
|
|
601
1034
|
};
|
|
602
1035
|
exports.StyleIr = StyleIr;
|
|
1036
|
+
StyleIr.CONTAINER_TYPES = ["GROUP", "FRAME", "INSTANCE", "COMPONENT"];
|
|
1037
|
+
StyleIr.SVG_COLLAPSE_AUTOLAYOUT_THRESHOLD = 10;
|
|
603
1038
|
}
|
|
604
1039
|
});
|
|
605
1040
|
|
|
@@ -612,6 +1047,111 @@ var require_tailwind_mapper = __commonJS({
|
|
|
612
1047
|
function arb(value) {
|
|
613
1048
|
return value.replace(/\s+/g, "_");
|
|
614
1049
|
}
|
|
1050
|
+
var SPACING = [
|
|
1051
|
+
["0", 0],
|
|
1052
|
+
["px", 1],
|
|
1053
|
+
["0.5", 2],
|
|
1054
|
+
["1", 4],
|
|
1055
|
+
["1.5", 6],
|
|
1056
|
+
["2", 8],
|
|
1057
|
+
["2.5", 10],
|
|
1058
|
+
["3", 12],
|
|
1059
|
+
["3.5", 14],
|
|
1060
|
+
["4", 16],
|
|
1061
|
+
["5", 20],
|
|
1062
|
+
["6", 24],
|
|
1063
|
+
["7", 28],
|
|
1064
|
+
["8", 32],
|
|
1065
|
+
["9", 36],
|
|
1066
|
+
["10", 40],
|
|
1067
|
+
["11", 44],
|
|
1068
|
+
["12", 48],
|
|
1069
|
+
["14", 56],
|
|
1070
|
+
["16", 64],
|
|
1071
|
+
["20", 80],
|
|
1072
|
+
["24", 96],
|
|
1073
|
+
["28", 112],
|
|
1074
|
+
["32", 128],
|
|
1075
|
+
["36", 144],
|
|
1076
|
+
["40", 160],
|
|
1077
|
+
["44", 176],
|
|
1078
|
+
["48", 192],
|
|
1079
|
+
["52", 208],
|
|
1080
|
+
["56", 224],
|
|
1081
|
+
["60", 240],
|
|
1082
|
+
["64", 256],
|
|
1083
|
+
["72", 288],
|
|
1084
|
+
["80", 320],
|
|
1085
|
+
["96", 384]
|
|
1086
|
+
];
|
|
1087
|
+
var RADIUS = [
|
|
1088
|
+
["-none", 0],
|
|
1089
|
+
["-sm", 2],
|
|
1090
|
+
["", 4],
|
|
1091
|
+
["-md", 6],
|
|
1092
|
+
["-lg", 8],
|
|
1093
|
+
["-xl", 12],
|
|
1094
|
+
["-2xl", 16],
|
|
1095
|
+
["-3xl", 24],
|
|
1096
|
+
["-full", 9999]
|
|
1097
|
+
];
|
|
1098
|
+
var FONT_SIZE = [
|
|
1099
|
+
["xs", 12],
|
|
1100
|
+
["sm", 14],
|
|
1101
|
+
["base", 16],
|
|
1102
|
+
["lg", 18],
|
|
1103
|
+
["xl", 20],
|
|
1104
|
+
["2xl", 24],
|
|
1105
|
+
["3xl", 30],
|
|
1106
|
+
["4xl", 36],
|
|
1107
|
+
["5xl", 48],
|
|
1108
|
+
["6xl", 60],
|
|
1109
|
+
["7xl", 72],
|
|
1110
|
+
["8xl", 96],
|
|
1111
|
+
["9xl", 128]
|
|
1112
|
+
];
|
|
1113
|
+
var FONT_WEIGHT = {
|
|
1114
|
+
"100": "thin",
|
|
1115
|
+
"200": "extralight",
|
|
1116
|
+
"300": "light",
|
|
1117
|
+
"400": "normal",
|
|
1118
|
+
"500": "medium",
|
|
1119
|
+
"600": "semibold",
|
|
1120
|
+
"700": "bold",
|
|
1121
|
+
"800": "extrabold",
|
|
1122
|
+
"900": "black"
|
|
1123
|
+
};
|
|
1124
|
+
function pxNum(value) {
|
|
1125
|
+
const m = /^(-?\d*\.?\d+)px$/.exec(value.trim());
|
|
1126
|
+
return m ? parseFloat(m[1]) : null;
|
|
1127
|
+
}
|
|
1128
|
+
function nearest(n, scale) {
|
|
1129
|
+
let best = null;
|
|
1130
|
+
let bestPx = 0;
|
|
1131
|
+
let bestD = Infinity;
|
|
1132
|
+
for (const [tok, px] of scale) {
|
|
1133
|
+
const d = Math.abs(px - n);
|
|
1134
|
+
if (d < bestD) {
|
|
1135
|
+
bestD = d;
|
|
1136
|
+
best = tok;
|
|
1137
|
+
bestPx = px;
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
if (best === null)
|
|
1141
|
+
return null;
|
|
1142
|
+
return bestD <= 0.5 ? best : null;
|
|
1143
|
+
}
|
|
1144
|
+
function spacingClass(prefix, value, round) {
|
|
1145
|
+
if (round) {
|
|
1146
|
+
const n = pxNum(value);
|
|
1147
|
+
if (n !== null && n >= 0) {
|
|
1148
|
+
const tok = nearest(n, SPACING);
|
|
1149
|
+
if (tok !== null)
|
|
1150
|
+
return `${prefix}-${tok}`;
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
return `${prefix}-[${arb(value)}]`;
|
|
1154
|
+
}
|
|
615
1155
|
var JUSTIFY = {
|
|
616
1156
|
"flex-start": "justify-start",
|
|
617
1157
|
center: "justify-center",
|
|
@@ -630,7 +1170,8 @@ var require_tailwind_mapper = __commonJS({
|
|
|
630
1170
|
right: "text-right",
|
|
631
1171
|
justify: "text-justify"
|
|
632
1172
|
};
|
|
633
|
-
function cssToTailwind(css) {
|
|
1173
|
+
function cssToTailwind(css, opts = {}) {
|
|
1174
|
+
const round = opts.round ?? false;
|
|
634
1175
|
const classes = [];
|
|
635
1176
|
const leftover = {};
|
|
636
1177
|
const push = (c) => {
|
|
@@ -663,16 +1204,16 @@ var require_tailwind_mapper = __commonJS({
|
|
|
663
1204
|
ITEMS[value] ? push(ITEMS[value]) : leftover[prop] = value;
|
|
664
1205
|
break;
|
|
665
1206
|
case "gap":
|
|
666
|
-
push(
|
|
1207
|
+
push(spacingClass("gap", value, round));
|
|
667
1208
|
break;
|
|
668
1209
|
case "padding":
|
|
669
|
-
applyPadding(value, push, leftover);
|
|
1210
|
+
applyPadding(value, push, leftover, round);
|
|
670
1211
|
break;
|
|
671
1212
|
case "width":
|
|
672
|
-
push(
|
|
1213
|
+
push(spacingClass("w", value, round));
|
|
673
1214
|
break;
|
|
674
1215
|
case "height":
|
|
675
|
-
push(
|
|
1216
|
+
push(spacingClass("h", value, round));
|
|
676
1217
|
break;
|
|
677
1218
|
case "position":
|
|
678
1219
|
if (value === "absolute")
|
|
@@ -683,10 +1224,10 @@ var require_tailwind_mapper = __commonJS({
|
|
|
683
1224
|
leftover[prop] = value;
|
|
684
1225
|
break;
|
|
685
1226
|
case "left":
|
|
686
|
-
push(
|
|
1227
|
+
push(spacingClass("left", value, round));
|
|
687
1228
|
break;
|
|
688
1229
|
case "top":
|
|
689
|
-
push(
|
|
1230
|
+
push(spacingClass("top", value, round));
|
|
690
1231
|
break;
|
|
691
1232
|
case "overflow":
|
|
692
1233
|
if (value === "hidden")
|
|
@@ -713,7 +1254,14 @@ var require_tailwind_mapper = __commonJS({
|
|
|
713
1254
|
applyBorder(value, push, leftover);
|
|
714
1255
|
break;
|
|
715
1256
|
case "border-radius":
|
|
716
|
-
value.includes(" ")
|
|
1257
|
+
if (value.includes(" ")) {
|
|
1258
|
+
leftover[prop] = value;
|
|
1259
|
+
} else if (round && value === "50%") {
|
|
1260
|
+
push("rounded-full");
|
|
1261
|
+
} else {
|
|
1262
|
+
const tok = round ? snapRadius(value) : null;
|
|
1263
|
+
push(tok !== null ? `rounded${tok}` : `rounded-[${arb(value)}]`);
|
|
1264
|
+
}
|
|
717
1265
|
break;
|
|
718
1266
|
case "box-shadow":
|
|
719
1267
|
push(`shadow-[${arb(value)}]`);
|
|
@@ -731,11 +1279,13 @@ var require_tailwind_mapper = __commonJS({
|
|
|
731
1279
|
case "color":
|
|
732
1280
|
push(`text-[${arb(value)}]`);
|
|
733
1281
|
break;
|
|
734
|
-
case "font-size":
|
|
735
|
-
|
|
1282
|
+
case "font-size": {
|
|
1283
|
+
const tok = round ? nearest(pxNum(value) ?? -1, FONT_SIZE) : null;
|
|
1284
|
+
push(tok ? `text-${tok}` : `text-[${arb(value)}]`);
|
|
736
1285
|
break;
|
|
1286
|
+
}
|
|
737
1287
|
case "font-weight":
|
|
738
|
-
push(`font-[${arb(value)}]`);
|
|
1288
|
+
push(round && FONT_WEIGHT[value] ? `font-${FONT_WEIGHT[value]}` : `font-[${arb(value)}]`);
|
|
739
1289
|
break;
|
|
740
1290
|
case "line-height":
|
|
741
1291
|
push(`leading-[${arb(value)}]`);
|
|
@@ -768,16 +1318,29 @@ var require_tailwind_mapper = __commonJS({
|
|
|
768
1318
|
}
|
|
769
1319
|
return { classes, leftover };
|
|
770
1320
|
}
|
|
771
|
-
function
|
|
1321
|
+
function snapRadius(value) {
|
|
1322
|
+
const n = pxNum(value);
|
|
1323
|
+
if (n === null || n < 0)
|
|
1324
|
+
return null;
|
|
1325
|
+
return nearest(n, RADIUS);
|
|
1326
|
+
}
|
|
1327
|
+
function applyPadding(value, push, leftover, round) {
|
|
772
1328
|
const parts = value.split(/\s+/);
|
|
773
1329
|
if (parts.length === 4) {
|
|
774
1330
|
const [t, r, b, l] = parts;
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
1331
|
+
if (round && t === r && r === b && b === l) {
|
|
1332
|
+
push(spacingClass("p", t, round));
|
|
1333
|
+
} else if (round && t === b && l === r) {
|
|
1334
|
+
push(spacingClass("py", t, round));
|
|
1335
|
+
push(spacingClass("px", r, round));
|
|
1336
|
+
} else {
|
|
1337
|
+
push(spacingClass("pt", t, round));
|
|
1338
|
+
push(spacingClass("pr", r, round));
|
|
1339
|
+
push(spacingClass("pb", b, round));
|
|
1340
|
+
push(spacingClass("pl", l, round));
|
|
1341
|
+
}
|
|
779
1342
|
} else if (parts.length === 1) {
|
|
780
|
-
push(
|
|
1343
|
+
push(spacingClass("p", parts[0], round));
|
|
781
1344
|
} else {
|
|
782
1345
|
leftover.padding = value;
|
|
783
1346
|
}
|
|
@@ -808,6 +1371,7 @@ var require_html_renderer = __commonJS({
|
|
|
808
1371
|
"use strict";
|
|
809
1372
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
810
1373
|
exports.HtmlRenderer = void 0;
|
|
1374
|
+
var style_ir_1 = require_style_ir();
|
|
811
1375
|
var tailwind_mapper_1 = require_tailwind_mapper();
|
|
812
1376
|
function escapeHtml(s) {
|
|
813
1377
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
@@ -815,14 +1379,19 @@ var require_html_renderer = __commonJS({
|
|
|
815
1379
|
function styleString(css) {
|
|
816
1380
|
return Object.entries(css).map(([k, v]) => `${k}: ${v}`).join("; ");
|
|
817
1381
|
}
|
|
1382
|
+
function svgPlaceholder(markup) {
|
|
1383
|
+
return `data:image/svg+xml,${encodeURIComponent(markup)}`;
|
|
1384
|
+
}
|
|
1385
|
+
var IMAGE_PLACEHOLDER = svgPlaceholder("<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100' preserveAspectRatio='xMidYMid slice'><rect width='100' height='100' fill='#e5e7eb'/><circle cx='35' cy='34' r='9' fill='#c4cad3'/><path d='M16 78l22-26 15 15 16-16 17 27z' fill='#c4cad3'/></svg>");
|
|
1386
|
+
var VECTOR_PLACEHOLDER = svgPlaceholder("<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' preserveAspectRatio='xMidYMid meet'><rect width='24' height='24' rx='4' fill='#eef0f3'/><path d='M7 14l3.5-4 2.5 3 2-2.5L18 14z' fill='#b9c0cb'/><circle cx='9' cy='8.5' r='1.6' fill='#b9c0cb'/></svg>");
|
|
818
1387
|
var HtmlRenderer = class _HtmlRenderer {
|
|
819
1388
|
/** Render the Style IR tree into an HTML fragment string. */
|
|
820
1389
|
renderFragment(node, opts = {}) {
|
|
821
|
-
return this.render(node, opts.mode ?? "tailwind", 0);
|
|
1390
|
+
return this.render(node, opts.mode ?? "tailwind", 0, opts.round ?? true);
|
|
822
1391
|
}
|
|
823
1392
|
/** Render a standalone, previewable HTML document (Tailwind Play CDN). */
|
|
824
1393
|
renderDocument(node, opts = {}) {
|
|
825
|
-
const body = this.render(node, opts.mode ?? "tailwind", 2);
|
|
1394
|
+
const body = this.render(node, opts.mode ?? "tailwind", 2, opts.round ?? true);
|
|
826
1395
|
return this.wrapDocument(body, node.name || "Figma Export", this.fontLinks(node));
|
|
827
1396
|
}
|
|
828
1397
|
/** Wrap an arbitrary HTML fragment (e.g. LLM output) into a previewable document. */
|
|
@@ -867,21 +1436,30 @@ ${bodyHtml}
|
|
|
867
1436
|
}
|
|
868
1437
|
return links.join("\n");
|
|
869
1438
|
}
|
|
870
|
-
render(node, mode, depth) {
|
|
1439
|
+
render(node, mode, depth, round) {
|
|
871
1440
|
const indent = " ".repeat(depth);
|
|
872
|
-
const isVectorAsset = node.asset?.kind === "vector"
|
|
873
|
-
const isImageAsset = node.asset?.kind === "image"
|
|
1441
|
+
const isVectorAsset = node.asset?.kind === "vector";
|
|
1442
|
+
const isImageAsset = node.asset?.kind === "image";
|
|
874
1443
|
const extraStyle = {};
|
|
1444
|
+
let baseCss = node.css;
|
|
875
1445
|
if (isImageAsset) {
|
|
876
|
-
|
|
1446
|
+
const imageUrl = node.assetSrc ? `url(${node.assetSrc})` : `url(${IMAGE_PLACEHOLDER})`;
|
|
1447
|
+
const stack = node.css["background-image"];
|
|
1448
|
+
extraStyle["background-image"] = stack ? stack.split(`url(${style_ir_1.IMAGE_FILL_TOKEN})`).join(imageUrl) : imageUrl;
|
|
1449
|
+
if (!node.assetSrc) {
|
|
1450
|
+
extraStyle["background-size"] = "cover";
|
|
1451
|
+
extraStyle["background-position"] = "center";
|
|
1452
|
+
}
|
|
1453
|
+
const { ["background-image"]: _omit, ...rest } = node.css;
|
|
1454
|
+
baseCss = rest;
|
|
877
1455
|
}
|
|
878
1456
|
const attrs = [];
|
|
879
1457
|
if (mode === "inline") {
|
|
880
|
-
const css = { ...
|
|
1458
|
+
const css = { ...baseCss, ...extraStyle };
|
|
881
1459
|
if (Object.keys(css).length)
|
|
882
1460
|
attrs.push(`style="${escapeHtml(styleString(css))}"`);
|
|
883
1461
|
} else {
|
|
884
|
-
const { classes, leftover } = (0, tailwind_mapper_1.cssToTailwind)(
|
|
1462
|
+
const { classes, leftover } = (0, tailwind_mapper_1.cssToTailwind)(baseCss, { round });
|
|
885
1463
|
const style = { ...leftover, ...extraStyle };
|
|
886
1464
|
if (classes.length)
|
|
887
1465
|
attrs.push(`class="${classes.join(" ")}"`);
|
|
@@ -891,7 +1469,15 @@ ${bodyHtml}
|
|
|
891
1469
|
attrs.push(`data-figma-name="${escapeHtml(node.name)}"`);
|
|
892
1470
|
const attrStr = attrs.length ? " " + attrs.join(" ") : "";
|
|
893
1471
|
if (isVectorAsset) {
|
|
894
|
-
|
|
1472
|
+
const src = node.assetSrc ?? VECTOR_PLACEHOLDER;
|
|
1473
|
+
return `${indent}<img${attrStr} src="${src}" alt="${escapeHtml(node.name)}" />`;
|
|
1474
|
+
}
|
|
1475
|
+
if (node.runs && node.runs.length) {
|
|
1476
|
+
const spans = node.runs.map((r) => {
|
|
1477
|
+
const s = Object.keys(r.css).length ? ` style="${escapeHtml(styleString(r.css))}"` : "";
|
|
1478
|
+
return `<span${s}>${escapeHtml(r.text)}</span>`;
|
|
1479
|
+
}).join("");
|
|
1480
|
+
return `${indent}<${node.tag}${attrStr}>${spans}</${node.tag}>`;
|
|
895
1481
|
}
|
|
896
1482
|
if (node.text !== void 0) {
|
|
897
1483
|
return `${indent}<${node.tag}${attrStr}>${escapeHtml(node.text)}</${node.tag}>`;
|
|
@@ -899,7 +1485,7 @@ ${bodyHtml}
|
|
|
899
1485
|
if (!node.children.length) {
|
|
900
1486
|
return `${indent}<${node.tag}${attrStr}></${node.tag}>`;
|
|
901
1487
|
}
|
|
902
|
-
const childHtml = node.children.map((c) => this.render(c, mode, depth + 1)).join("\n");
|
|
1488
|
+
const childHtml = node.children.map((c) => this.render(c, mode, depth + 1, round)).join("\n");
|
|
903
1489
|
return `${indent}<${node.tag}${attrStr}>
|
|
904
1490
|
${childHtml}
|
|
905
1491
|
${indent}</${node.tag}>`;
|
|
@@ -931,45 +1517,213 @@ ${indent}</${node.tag}>`;
|
|
|
931
1517
|
}
|
|
932
1518
|
});
|
|
933
1519
|
|
|
1520
|
+
// ../packages/figma-core/dist/converter/compact-design.js
|
|
1521
|
+
var require_compact_design = __commonJS({
|
|
1522
|
+
"../packages/figma-core/dist/converter/compact-design.js"(exports) {
|
|
1523
|
+
"use strict";
|
|
1524
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
1525
|
+
exports.toCompactDesign = toCompactDesign2;
|
|
1526
|
+
exports.stableStringify = stableStringify;
|
|
1527
|
+
var MIN_TEMPLATE_NODES = 3;
|
|
1528
|
+
var POSITIONAL_KEYS = /* @__PURE__ */ new Set(["left", "top", "width", "height"]);
|
|
1529
|
+
function toCompactDesign2(root) {
|
|
1530
|
+
const styles = {};
|
|
1531
|
+
const styleIds = /* @__PURE__ */ new Map();
|
|
1532
|
+
let styleSeq = 0;
|
|
1533
|
+
const styleRef = (css) => {
|
|
1534
|
+
if (!css || Object.keys(css).length === 0)
|
|
1535
|
+
return void 0;
|
|
1536
|
+
const key = stableStringify(css);
|
|
1537
|
+
let id = styleIds.get(key);
|
|
1538
|
+
if (!id) {
|
|
1539
|
+
id = `s${++styleSeq}`;
|
|
1540
|
+
styleIds.set(key, id);
|
|
1541
|
+
styles[id] = css;
|
|
1542
|
+
}
|
|
1543
|
+
return id;
|
|
1544
|
+
};
|
|
1545
|
+
const hashCount = /* @__PURE__ */ new Map();
|
|
1546
|
+
const build = (node) => {
|
|
1547
|
+
const children = node.children.map(build);
|
|
1548
|
+
const compact = { tag: node.tag };
|
|
1549
|
+
if (node.name)
|
|
1550
|
+
compact.name = node.name;
|
|
1551
|
+
if (node.figmaType && node.figmaType !== "FRAME")
|
|
1552
|
+
compact.type = node.figmaType;
|
|
1553
|
+
const box = {};
|
|
1554
|
+
const rest = {};
|
|
1555
|
+
for (const [k, v] of Object.entries(node.css)) {
|
|
1556
|
+
if (POSITIONAL_KEYS.has(k))
|
|
1557
|
+
box[k] = v;
|
|
1558
|
+
else
|
|
1559
|
+
rest[k] = v;
|
|
1560
|
+
}
|
|
1561
|
+
if (Object.keys(box).length)
|
|
1562
|
+
compact.box = box;
|
|
1563
|
+
const sref = styleRef(rest);
|
|
1564
|
+
if (sref)
|
|
1565
|
+
compact.style = sref;
|
|
1566
|
+
if (node.text !== void 0)
|
|
1567
|
+
compact.text = node.text;
|
|
1568
|
+
if (node.asset)
|
|
1569
|
+
compact.asset = { kind: node.asset.kind };
|
|
1570
|
+
if (children.length)
|
|
1571
|
+
compact.children = children.map((c) => c.compact);
|
|
1572
|
+
const hash = stableStringify({
|
|
1573
|
+
t: compact.tag,
|
|
1574
|
+
ty: compact.type,
|
|
1575
|
+
b: compact.box,
|
|
1576
|
+
s: compact.style,
|
|
1577
|
+
x: compact.text,
|
|
1578
|
+
a: compact.asset?.kind,
|
|
1579
|
+
c: children.map((c) => c.hash)
|
|
1580
|
+
});
|
|
1581
|
+
hashCount.set(hash, (hashCount.get(hash) ?? 0) + 1);
|
|
1582
|
+
const size = 1 + children.reduce((n, c) => n + c.size, 0);
|
|
1583
|
+
return { compact, children, hash, size };
|
|
1584
|
+
};
|
|
1585
|
+
const built = build(root);
|
|
1586
|
+
const elements = {};
|
|
1587
|
+
const elementIds = /* @__PURE__ */ new Map();
|
|
1588
|
+
let elemSeq = 0;
|
|
1589
|
+
const templatize = (b) => {
|
|
1590
|
+
if (b.compact.children) {
|
|
1591
|
+
b.compact.children = b.children.map(templatize);
|
|
1592
|
+
}
|
|
1593
|
+
if (b.size >= MIN_TEMPLATE_NODES && (hashCount.get(b.hash) ?? 0) >= 2) {
|
|
1594
|
+
let id = elementIds.get(b.hash);
|
|
1595
|
+
if (!id) {
|
|
1596
|
+
id = `e${++elemSeq}`;
|
|
1597
|
+
elementIds.set(b.hash, id);
|
|
1598
|
+
elements[id] = b.compact;
|
|
1599
|
+
}
|
|
1600
|
+
return { ref: id };
|
|
1601
|
+
}
|
|
1602
|
+
return b.compact;
|
|
1603
|
+
};
|
|
1604
|
+
const rootCompact = templatize(built);
|
|
1605
|
+
return { name: root.name, globalVars: { styles }, elements, root: rootCompact };
|
|
1606
|
+
}
|
|
1607
|
+
function stableStringify(value) {
|
|
1608
|
+
return JSON.stringify(value, (_k, v) => v && typeof v === "object" && !Array.isArray(v) ? Object.keys(v).sort().reduce((acc, k) => {
|
|
1609
|
+
acc[k] = v[k];
|
|
1610
|
+
return acc;
|
|
1611
|
+
}, {}) : v);
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
});
|
|
1615
|
+
|
|
934
1616
|
// ../packages/figma-core/dist/assets/asset-resolver.js
|
|
935
1617
|
var require_asset_resolver = __commonJS({
|
|
936
1618
|
"../packages/figma-core/dist/assets/asset-resolver.js"(exports) {
|
|
937
1619
|
"use strict";
|
|
938
1620
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
939
1621
|
exports.AssetResolver = void 0;
|
|
1622
|
+
var errors_1 = require_errors();
|
|
940
1623
|
var AssetResolver = class {
|
|
941
1624
|
constructor(figma, cache, logger) {
|
|
942
1625
|
this.figma = figma;
|
|
943
1626
|
this.cache = cache;
|
|
944
1627
|
this.logger = logger;
|
|
945
1628
|
this.fetchConcurrency = 8;
|
|
1629
|
+
this.renderBatchSize = 20;
|
|
1630
|
+
this.renderMaxAttempts = 4;
|
|
1631
|
+
this.renderMaxBackoffMs = 4e3;
|
|
946
1632
|
}
|
|
947
1633
|
async resolve(root, fileKey, auth, scale = 2) {
|
|
948
1634
|
const vectors = [];
|
|
949
1635
|
const images = [];
|
|
950
1636
|
this.collect(root, vectors, images);
|
|
951
1637
|
if (!vectors.length && !images.length) {
|
|
952
|
-
return {
|
|
1638
|
+
return {
|
|
1639
|
+
vectors: 0,
|
|
1640
|
+
images: 0,
|
|
1641
|
+
embedded: 0,
|
|
1642
|
+
fromCache: 0,
|
|
1643
|
+
unresolvedVectors: 0,
|
|
1644
|
+
unresolvedImages: 0,
|
|
1645
|
+
rateLimited: false
|
|
1646
|
+
};
|
|
953
1647
|
}
|
|
954
1648
|
let fromCache = 0;
|
|
955
1649
|
fromCache += await this.fillFromCache(vectors, fileKey, "svg", 1);
|
|
956
1650
|
fromCache += await this.fillImageFillsFromCache(images, fileKey);
|
|
957
1651
|
const vMissing = vectors.filter((n) => !n.assetSrc);
|
|
958
|
-
|
|
959
|
-
if (vMissing.length) {
|
|
960
|
-
try {
|
|
961
|
-
svgUrls = await this.figma.renderImages(fileKey, vMissing.map((n) => n.id), "svg", auth);
|
|
962
|
-
await this.retryMissing(vMissing, svgUrls, fileKey, "svg", auth, 1);
|
|
963
|
-
} catch (err) {
|
|
964
|
-
this.logger.warn(`Vector render failed (keeping cached vectors + image fills): ${err.message}`);
|
|
965
|
-
}
|
|
966
|
-
}
|
|
1652
|
+
const { urls: svgUrls, rateLimited } = vMissing.length ? await this.renderVectors(vMissing.map((n) => n.id), fileKey, auth) : { urls: {}, rateLimited: false };
|
|
967
1653
|
const iMissing = images.filter((n) => !n.assetSrc && n.asset?.imageRef);
|
|
968
1654
|
let embedded = 0;
|
|
969
1655
|
embedded += await this.embed(vMissing, svgUrls, "image/svg+xml", fileKey, "svg", 1);
|
|
970
1656
|
embedded += await this.embedImageFills(iMissing, fileKey, auth);
|
|
1657
|
+
const unresolvedVectors = vectors.filter((n) => !n.assetSrc).length;
|
|
1658
|
+
const unresolvedImages = images.filter((n) => !n.assetSrc).length;
|
|
971
1659
|
this.logger.log(`Assets: ${vectors.length} vectors, ${images.length} image fills, ${fromCache} from cache, ${embedded} newly embedded`);
|
|
972
|
-
|
|
1660
|
+
if (unresolvedVectors || unresolvedImages) {
|
|
1661
|
+
this.logger.warn(`Assets unresolved: ${unresolvedVectors}/${vectors.length} vectors, ${unresolvedImages}/${images.length} image fills` + (rateLimited ? " (Figma render endpoint rate-limited; they fall back to placeholders)" : ""));
|
|
1662
|
+
}
|
|
1663
|
+
return {
|
|
1664
|
+
vectors: vectors.length,
|
|
1665
|
+
images: images.length,
|
|
1666
|
+
embedded,
|
|
1667
|
+
fromCache,
|
|
1668
|
+
unresolvedVectors,
|
|
1669
|
+
unresolvedImages,
|
|
1670
|
+
rateLimited
|
|
1671
|
+
};
|
|
1672
|
+
}
|
|
1673
|
+
/**
|
|
1674
|
+
* Render vector node ids via /v1/images in small batches. Each batch is
|
|
1675
|
+
* retried with capped exponential backoff on a 429 and isolated so an
|
|
1676
|
+
* exhausted batch leaves only its own ids unresolved. Returns the merged
|
|
1677
|
+
* id -> url map and whether any batch was rate-limited.
|
|
1678
|
+
*/
|
|
1679
|
+
async renderVectors(ids, fileKey, auth) {
|
|
1680
|
+
const urls = {};
|
|
1681
|
+
let rateLimited = false;
|
|
1682
|
+
for (let i = 0; i < ids.length; i += this.renderBatchSize) {
|
|
1683
|
+
const batch = ids.slice(i, i + this.renderBatchSize);
|
|
1684
|
+
try {
|
|
1685
|
+
const rendered = await this.withRenderRetry(() => this.figma.renderImages(fileKey, batch, "svg", auth), (was429) => {
|
|
1686
|
+
if (was429)
|
|
1687
|
+
rateLimited = true;
|
|
1688
|
+
});
|
|
1689
|
+
for (const [id, url] of Object.entries(rendered)) {
|
|
1690
|
+
if (url)
|
|
1691
|
+
urls[id] = url;
|
|
1692
|
+
}
|
|
1693
|
+
} catch (err) {
|
|
1694
|
+
if (err instanceof errors_1.FigmaApiError && err.status === 429)
|
|
1695
|
+
rateLimited = true;
|
|
1696
|
+
this.logger.warn(`Vector render batch [${i}, ${i + batch.length}) failed after retries: ${err.message}`);
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
return { urls, rateLimited };
|
|
1700
|
+
}
|
|
1701
|
+
/**
|
|
1702
|
+
* Run a render call, retrying on a Figma 429 with capped exponential backoff.
|
|
1703
|
+
* The backoff is deliberately capped (`renderMaxBackoffMs`) so a multi-hour
|
|
1704
|
+
* `Retry-After` can never block the request — we exhaust a few quick attempts
|
|
1705
|
+
* and then surface the failure instead of waiting.
|
|
1706
|
+
*/
|
|
1707
|
+
async withRenderRetry(fn, onRateLimit) {
|
|
1708
|
+
let lastErr;
|
|
1709
|
+
for (let attempt = 0; attempt < this.renderMaxAttempts; attempt++) {
|
|
1710
|
+
try {
|
|
1711
|
+
return await fn();
|
|
1712
|
+
} catch (err) {
|
|
1713
|
+
lastErr = err;
|
|
1714
|
+
const is429 = err instanceof errors_1.FigmaApiError && err.status === 429;
|
|
1715
|
+
if (!is429 || attempt === this.renderMaxAttempts - 1)
|
|
1716
|
+
throw err;
|
|
1717
|
+
onRateLimit(true);
|
|
1718
|
+
const backoff = Math.min(this.renderMaxBackoffMs, 500 * 2 ** attempt);
|
|
1719
|
+
this.logger.warn(`Figma render rate-limited; retrying in ${backoff}ms (attempt ${attempt + 1}/${this.renderMaxAttempts})`);
|
|
1720
|
+
await this.sleep(backoff);
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
throw lastErr;
|
|
1724
|
+
}
|
|
1725
|
+
sleep(ms) {
|
|
1726
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
973
1727
|
}
|
|
974
1728
|
assetKey(fileKey, id, format, scale) {
|
|
975
1729
|
return `asset:${fileKey}:${id}:${format}:${scale}`;
|
|
@@ -1057,22 +1811,6 @@ var require_asset_resolver = __commonJS({
|
|
|
1057
1811
|
for (const child of node.children)
|
|
1058
1812
|
this.collect(child, vectors, images);
|
|
1059
1813
|
}
|
|
1060
|
-
async retryMissing(nodes, urlMap, fileKey, format, auth, scale) {
|
|
1061
|
-
const missing = nodes.filter((n) => !urlMap[n.id]).map((n) => n.id);
|
|
1062
|
-
if (!missing.length)
|
|
1063
|
-
return;
|
|
1064
|
-
try {
|
|
1065
|
-
const retry = await this.figma.renderImages(fileKey, missing, format, auth, scale);
|
|
1066
|
-
for (const [id, url] of Object.entries(retry)) {
|
|
1067
|
-
if (url)
|
|
1068
|
-
urlMap[id] = url;
|
|
1069
|
-
}
|
|
1070
|
-
const stillMissing = missing.filter((id) => !urlMap[id]).length;
|
|
1071
|
-
this.logger.log(`Retried ${missing.length} missing ${format} assets; ${stillMissing} still unrendered`);
|
|
1072
|
-
} catch (err) {
|
|
1073
|
-
this.logger.warn(`Asset retry (${format}) failed: ${err.message}`);
|
|
1074
|
-
}
|
|
1075
|
-
}
|
|
1076
1814
|
async embed(nodes, urlMap, mime, fileKey, format, scale) {
|
|
1077
1815
|
let count = 0;
|
|
1078
1816
|
for (let i = 0; i < nodes.length; i += this.fetchConcurrency) {
|
|
@@ -1580,16 +2318,22 @@ var require_convert = __commonJS({
|
|
|
1580
2318
|
const node = await this.client.fetchNode(target2.fileKey, target2.nodeId, auth);
|
|
1581
2319
|
const ir = this.styleIr.convert(node);
|
|
1582
2320
|
let assetWarning;
|
|
1583
|
-
if (options.assets
|
|
2321
|
+
if (options.assets === true) {
|
|
1584
2322
|
try {
|
|
1585
|
-
await this.assets.resolve(ir, target2.fileKey, auth, options.assetScale ?? 2);
|
|
2323
|
+
const r = await this.assets.resolve(ir, target2.fileKey, auth, options.assetScale ?? 2);
|
|
2324
|
+
const unresolved = r.unresolvedVectors + r.unresolvedImages;
|
|
2325
|
+
if (unresolved > 0) {
|
|
2326
|
+
assetWarning = `${unresolved} asset(s) could not be rendered (${r.unresolvedVectors} vectors, ${r.unresolvedImages} image fills) and fall back to placeholders` + (r.rateLimited ? ". Figma\u2019s render endpoint is rate-limited \u2014 retry later for full fidelity." : ".");
|
|
2327
|
+
this.logger.warn(assetWarning);
|
|
2328
|
+
}
|
|
1586
2329
|
} catch (err) {
|
|
1587
2330
|
assetWarning = `Assets not fully resolved: ${err.message}`;
|
|
1588
2331
|
this.logger.warn(assetWarning);
|
|
1589
2332
|
}
|
|
1590
2333
|
}
|
|
1591
2334
|
const mode = options.mode ?? "tailwind";
|
|
1592
|
-
|
|
2335
|
+
const round = options.round ?? true;
|
|
2336
|
+
let fragment = this.renderer.renderFragment(ir, { mode, round });
|
|
1593
2337
|
let usedLlm = false;
|
|
1594
2338
|
if (options.llm && this.restructure?.enabled) {
|
|
1595
2339
|
fragment = await this.restructure.restructure(fragment);
|
|
@@ -1695,7 +2439,7 @@ var require_dist = __commonJS({
|
|
|
1695
2439
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports2, p)) __createBinding(exports2, m, p);
|
|
1696
2440
|
};
|
|
1697
2441
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
1698
|
-
exports.createFigmaCore = exports.countNodes = exports.FigmaConverter = exports.stripHeavyAssets = exports.stripCodeFence = exports.runAgent = exports.RefineAgent = exports.LlmRestructure = exports.ollamaConfigFromEnv = exports.OllamaClient = exports.AssetResolver = exports.px = exports.figmaColorToCss = exports.cssToTailwind = exports.HtmlRenderer = exports.StyleIr = exports.resolveFigmaTarget = exports.normalizeNodeId = exports.parseFigmaUrl = exports.FigmaClient = exports.NoopLogger = exports.ConsoleLogger = exports.DiskCache = exports.MemoryCache = exports.FigmaInputError = exports.FigmaApiError = void 0;
|
|
2442
|
+
exports.createFigmaCore = exports.countNodes = exports.FigmaConverter = exports.stripHeavyAssets = exports.stripCodeFence = exports.runAgent = exports.RefineAgent = exports.LlmRestructure = exports.ollamaConfigFromEnv = exports.OllamaClient = exports.AssetResolver = exports.stableStringify = exports.toCompactDesign = exports.px = exports.figmaColorToCss = exports.cssToTailwind = exports.HtmlRenderer = exports.IMAGE_FILL_TOKEN = exports.StyleIr = exports.resolveFigmaTarget = exports.normalizeNodeId = exports.parseFigmaUrl = exports.FigmaClient = exports.NoopLogger = exports.ConsoleLogger = exports.DiskCache = exports.MemoryCache = exports.FigmaInputError = exports.FigmaApiError = void 0;
|
|
1699
2443
|
var errors_1 = require_errors();
|
|
1700
2444
|
Object.defineProperty(exports, "FigmaApiError", { enumerable: true, get: function() {
|
|
1701
2445
|
return errors_1.FigmaApiError;
|
|
@@ -1735,6 +2479,9 @@ var require_dist = __commonJS({
|
|
|
1735
2479
|
Object.defineProperty(exports, "StyleIr", { enumerable: true, get: function() {
|
|
1736
2480
|
return style_ir_1.StyleIr;
|
|
1737
2481
|
} });
|
|
2482
|
+
Object.defineProperty(exports, "IMAGE_FILL_TOKEN", { enumerable: true, get: function() {
|
|
2483
|
+
return style_ir_1.IMAGE_FILL_TOKEN;
|
|
2484
|
+
} });
|
|
1738
2485
|
var html_renderer_1 = require_html_renderer();
|
|
1739
2486
|
Object.defineProperty(exports, "HtmlRenderer", { enumerable: true, get: function() {
|
|
1740
2487
|
return html_renderer_1.HtmlRenderer;
|
|
@@ -1750,6 +2497,13 @@ var require_dist = __commonJS({
|
|
|
1750
2497
|
Object.defineProperty(exports, "px", { enumerable: true, get: function() {
|
|
1751
2498
|
return color_1.px;
|
|
1752
2499
|
} });
|
|
2500
|
+
var compact_design_1 = require_compact_design();
|
|
2501
|
+
Object.defineProperty(exports, "toCompactDesign", { enumerable: true, get: function() {
|
|
2502
|
+
return compact_design_1.toCompactDesign;
|
|
2503
|
+
} });
|
|
2504
|
+
Object.defineProperty(exports, "stableStringify", { enumerable: true, get: function() {
|
|
2505
|
+
return compact_design_1.stableStringify;
|
|
2506
|
+
} });
|
|
1753
2507
|
var asset_resolver_1 = require_asset_resolver();
|
|
1754
2508
|
Object.defineProperty(exports, "AssetResolver", { enumerable: true, get: function() {
|
|
1755
2509
|
return asset_resolver_1.AssetResolver;
|
|
@@ -1805,31 +2559,37 @@ var import_core = __toESM(require_dist(), 1);
|
|
|
1805
2559
|
import { promises as fs2 } from "fs";
|
|
1806
2560
|
import * as path2 from "path";
|
|
1807
2561
|
|
|
1808
|
-
// src/
|
|
2562
|
+
// src/settings.ts
|
|
1809
2563
|
import { promises as fs } from "fs";
|
|
1810
2564
|
import * as os from "os";
|
|
1811
2565
|
import * as path from "path";
|
|
1812
2566
|
var DIR = path.join(os.homedir(), ".figma-mcp");
|
|
1813
|
-
var FILE = path.join(DIR, "
|
|
1814
|
-
function
|
|
2567
|
+
var FILE = path.join(DIR, "settings.json");
|
|
2568
|
+
function settingsPath() {
|
|
1815
2569
|
return FILE;
|
|
1816
2570
|
}
|
|
1817
|
-
async function
|
|
2571
|
+
async function loadSettings() {
|
|
1818
2572
|
try {
|
|
1819
2573
|
const raw = await fs.readFile(FILE, "utf8");
|
|
1820
|
-
|
|
2574
|
+
const parsed = JSON.parse(raw);
|
|
2575
|
+
if (!parsed.env || typeof parsed.env !== "object") parsed.env = {};
|
|
2576
|
+
return parsed;
|
|
1821
2577
|
} catch {
|
|
1822
|
-
return {};
|
|
2578
|
+
return { env: {} };
|
|
1823
2579
|
}
|
|
1824
2580
|
}
|
|
1825
|
-
async function
|
|
1826
|
-
const current = await
|
|
1827
|
-
const next = {
|
|
2581
|
+
async function saveSettings(patch) {
|
|
2582
|
+
const current = await loadSettings();
|
|
2583
|
+
const next = {
|
|
2584
|
+
...current,
|
|
2585
|
+
...patch,
|
|
2586
|
+
env: { ...current.env ?? {}, ...patch.env ?? {} }
|
|
2587
|
+
};
|
|
1828
2588
|
await fs.mkdir(DIR, { recursive: true });
|
|
1829
2589
|
await fs.writeFile(FILE, JSON.stringify(next, null, 2), { mode: 384 });
|
|
1830
2590
|
return next;
|
|
1831
2591
|
}
|
|
1832
|
-
async function
|
|
2592
|
+
async function clearSettings() {
|
|
1833
2593
|
try {
|
|
1834
2594
|
await fs.unlink(FILE);
|
|
1835
2595
|
} catch {
|
|
@@ -1839,24 +2599,25 @@ async function clearCredentials() {
|
|
|
1839
2599
|
// src/refresh.ts
|
|
1840
2600
|
var SKEW_MS = 6e4;
|
|
1841
2601
|
async function ensureFreshApiToken(apiUrl, force = false) {
|
|
1842
|
-
const
|
|
1843
|
-
|
|
2602
|
+
const settings = await loadSettings();
|
|
2603
|
+
const apiToken = settings.env?.FIGMA_MCP_TOKEN;
|
|
2604
|
+
if (!apiToken || !settings.refreshToken) return;
|
|
1844
2605
|
if (!force) {
|
|
1845
|
-
if (!
|
|
1846
|
-
if (Date.now() <
|
|
2606
|
+
if (!settings.apiTokenExpiresAt) return;
|
|
2607
|
+
if (Date.now() < settings.apiTokenExpiresAt - SKEW_MS) return;
|
|
1847
2608
|
}
|
|
1848
2609
|
const base = apiUrl.replace(/\/+$/, "");
|
|
1849
2610
|
try {
|
|
1850
2611
|
const res = await fetch(`${base}/auth/figma/cli-refresh`, {
|
|
1851
2612
|
method: "POST",
|
|
1852
2613
|
headers: { "Content-Type": "application/json" },
|
|
1853
|
-
body: JSON.stringify({ refresh_token:
|
|
2614
|
+
body: JSON.stringify({ refresh_token: settings.refreshToken })
|
|
1854
2615
|
});
|
|
1855
2616
|
if (!res.ok) return;
|
|
1856
2617
|
const t = await res.json();
|
|
1857
|
-
await
|
|
1858
|
-
|
|
1859
|
-
refreshToken: t.refresh_token ??
|
|
2618
|
+
await saveSettings({
|
|
2619
|
+
env: { FIGMA_MCP_TOKEN: t.access_token },
|
|
2620
|
+
refreshToken: t.refresh_token ?? settings.refreshToken,
|
|
1860
2621
|
apiTokenExpiresAt: t.expires_in ? Date.now() + t.expires_in * 1e3 : void 0
|
|
1861
2622
|
});
|
|
1862
2623
|
} catch {
|
|
@@ -1864,13 +2625,15 @@ async function ensureFreshApiToken(apiUrl, force = false) {
|
|
|
1864
2625
|
}
|
|
1865
2626
|
|
|
1866
2627
|
// src/config.ts
|
|
2628
|
+
var DEFAULT_API_URL = "https://figmacoder-api.sitenow.cloud";
|
|
1867
2629
|
async function resolveConfig(overrides = {}) {
|
|
1868
|
-
let
|
|
1869
|
-
|
|
1870
|
-
const
|
|
1871
|
-
const
|
|
2630
|
+
let settings = await loadSettings();
|
|
2631
|
+
let env = settings.env ?? {};
|
|
2632
|
+
const mode = overrides.mode ?? process.env.FIGMA_MCP_MODE ?? env.FIGMA_MCP_MODE ?? "auto";
|
|
2633
|
+
const pat = overrides.pat ?? process.env.FIGMA_PAT ?? process.env.FIGMA_TOKEN ?? env.FIGMA_PAT ?? env.FIGMA_TOKEN;
|
|
2634
|
+
const apiUrl = overrides.apiUrl ?? process.env.FIGMA_MCP_API ?? env.FIGMA_MCP_API ?? DEFAULT_API_URL;
|
|
1872
2635
|
const apiTokenOverride = overrides.apiToken ?? process.env.FIGMA_MCP_TOKEN;
|
|
1873
|
-
const outDir = overrides.outDir ?? process.env.FIGMA_MCP_OUT_DIR ?? `${process.cwd()}/figma-output`;
|
|
2636
|
+
const outDir = overrides.outDir ?? process.env.FIGMA_MCP_OUT_DIR ?? env.FIGMA_MCP_OUT_DIR ?? `${process.cwd()}/figma-output`;
|
|
1874
2637
|
let effectiveMode;
|
|
1875
2638
|
if (mode === "local") {
|
|
1876
2639
|
effectiveMode = "local";
|
|
@@ -1881,9 +2644,10 @@ async function resolveConfig(overrides = {}) {
|
|
|
1881
2644
|
}
|
|
1882
2645
|
if (effectiveMode === "remote" && apiUrl && !apiTokenOverride) {
|
|
1883
2646
|
await ensureFreshApiToken(apiUrl);
|
|
1884
|
-
|
|
2647
|
+
settings = await loadSettings();
|
|
2648
|
+
env = settings.env ?? {};
|
|
1885
2649
|
}
|
|
1886
|
-
const apiToken = apiTokenOverride ??
|
|
2650
|
+
const apiToken = apiTokenOverride ?? env.FIGMA_MCP_TOKEN;
|
|
1887
2651
|
return { mode, effectiveMode, pat, apiUrl, apiToken, outDir };
|
|
1888
2652
|
}
|
|
1889
2653
|
function describeConfig(cfg) {
|
|
@@ -1907,7 +2671,7 @@ async function post(cfg, route, body) {
|
|
|
1907
2671
|
let res = await fetch(url, { method: "POST", headers: authHeaders(cfg), body: payload });
|
|
1908
2672
|
if (res.status === 401 && cfg.apiToken) {
|
|
1909
2673
|
await ensureFreshApiToken(cfg.apiUrl, true);
|
|
1910
|
-
const refreshed = (await
|
|
2674
|
+
const refreshed = (await loadSettings()).env?.FIGMA_MCP_TOKEN;
|
|
1911
2675
|
if (refreshed && refreshed !== cfg.apiToken) {
|
|
1912
2676
|
res = await fetch(url, { method: "POST", headers: authHeaders(cfg, refreshed), body: payload });
|
|
1913
2677
|
}
|
|
@@ -1936,7 +2700,9 @@ function remoteExtractStyles(cfg, body) {
|
|
|
1936
2700
|
var INLINE_LIMIT = 1e5;
|
|
1937
2701
|
var core;
|
|
1938
2702
|
var corePat;
|
|
2703
|
+
var coreOverride;
|
|
1939
2704
|
function getCore(pat) {
|
|
2705
|
+
if (coreOverride) return coreOverride;
|
|
1940
2706
|
if (!core || corePat !== pat) {
|
|
1941
2707
|
core = (0, import_core.createFigmaCore)({ figmaToken: pat });
|
|
1942
2708
|
corePat = pat;
|
|
@@ -1966,6 +2732,7 @@ async function convertFigmaToHtml(args, overrides = {}) {
|
|
|
1966
2732
|
document: args.document ?? true,
|
|
1967
2733
|
assets: args.assets,
|
|
1968
2734
|
assetScale: args.assetScale,
|
|
2735
|
+
round: args.round,
|
|
1969
2736
|
llm: args.llm
|
|
1970
2737
|
});
|
|
1971
2738
|
result = { name: r.name, nodeCount: r.nodeCount, mode: r.mode, llm: r.llm, html: r.html };
|
|
@@ -1976,6 +2743,7 @@ async function convertFigmaToHtml(args, overrides = {}) {
|
|
|
1976
2743
|
document: args.document ?? true,
|
|
1977
2744
|
assets: args.assets,
|
|
1978
2745
|
assetScale: args.assetScale,
|
|
2746
|
+
round: args.round,
|
|
1979
2747
|
llm: args.llm
|
|
1980
2748
|
});
|
|
1981
2749
|
result = { name: r.name, nodeCount: r.nodeCount, mode: r.mode, llm: r.llm, html: r.html, previewUrl: r.previewUrl };
|
|
@@ -2003,23 +2771,33 @@ async function getFigmaData(args, overrides = {}) {
|
|
|
2003
2771
|
} else {
|
|
2004
2772
|
ir = await remoteExtractStyles(cfg, target(args));
|
|
2005
2773
|
}
|
|
2006
|
-
const json = JSON.stringify(ir, null, 2);
|
|
2007
2774
|
const nodeCount = countIr(ir);
|
|
2775
|
+
const compact = (0, import_core.toCompactDesign)(ir);
|
|
2776
|
+
const json = JSON.stringify(compact);
|
|
2777
|
+
const legend = [
|
|
2778
|
+
"Compact design format (token-optimized):",
|
|
2779
|
+
"- `globalVars.styles`: id -> CSS declaration map. A node's full CSS = { ...globalVars.styles[node.style], ...node.box }.",
|
|
2780
|
+
"- `node.box`: per-node left/top/width/height (kept inline because unique).",
|
|
2781
|
+
'- `elements`: id -> a subtree that repeats; a node `{ref:"e1"}` is an instance of elements.e1.',
|
|
2782
|
+
"- `root`: the node tree (each node: tag, optional type/name/text/asset/style/box/children)."
|
|
2783
|
+
].join("\n");
|
|
2008
2784
|
if (json.length <= INLINE_LIMIT) {
|
|
2009
|
-
return [
|
|
2785
|
+
return [
|
|
2786
|
+
`Compact design for "${compact.name}" (${nodeCount} nodes, ${describeConfig(cfg)}):`,
|
|
2787
|
+
legend,
|
|
2788
|
+
"",
|
|
2789
|
+
json
|
|
2790
|
+
].join("\n");
|
|
2010
2791
|
}
|
|
2011
|
-
const filePath = await writeOut(cfg.outDir, `${sanitize(
|
|
2792
|
+
const filePath = await writeOut(cfg.outDir, `${sanitize(compact.name)}.design.json`, json);
|
|
2012
2793
|
return [
|
|
2013
|
-
`
|
|
2794
|
+
`Compact design for "${compact.name}" is large (${nodeCount} nodes, ${(json.length / 1024).toFixed(1)} KB) \u2014 written to a file.`,
|
|
2014
2795
|
`- file: ${filePath}`,
|
|
2015
|
-
`-
|
|
2796
|
+
`- ${Object.keys(compact.globalVars.styles).length} shared styles, ${Object.keys(compact.elements).length} repeated-subtree templates`,
|
|
2016
2797
|
"",
|
|
2017
|
-
|
|
2018
|
-
...(ir.children || []).map(
|
|
2019
|
-
(c, i) => ` ${i}. "${c.name}" <${c.tag}> ${c.figmaType} (${(c.children || []).length} children)`
|
|
2020
|
-
),
|
|
2798
|
+
legend,
|
|
2021
2799
|
"",
|
|
2022
|
-
"Read the file for the full tree
|
|
2800
|
+
"Read the file for the full tree."
|
|
2023
2801
|
].join("\n");
|
|
2024
2802
|
}
|
|
2025
2803
|
function countIr(node) {
|
|
@@ -2049,7 +2827,7 @@ function buildServer() {
|
|
|
2049
2827
|
"get_figma_data",
|
|
2050
2828
|
{
|
|
2051
2829
|
title: "Get Figma design data",
|
|
2052
|
-
description: "Fetch
|
|
2830
|
+
description: "Fetch a Figma file/node as a compact, token-efficient design tree for an agent to reason about and generate code from. Styles are deduped into globalVars.styles (nodes carry short `style` refs), per-node position/size is inline as `box`, and repeated subtrees are hoisted into `elements` ({ref}). Small results are returned inline; large ones are written to a JSON file and summarised.",
|
|
2053
2831
|
inputSchema: targetShape
|
|
2054
2832
|
},
|
|
2055
2833
|
safe((a) => getFigmaData(a))
|
|
@@ -2058,13 +2836,16 @@ function buildServer() {
|
|
|
2058
2836
|
"convert_figma_to_html",
|
|
2059
2837
|
{
|
|
2060
2838
|
title: "Convert Figma to HTML + Tailwind",
|
|
2061
|
-
description: "Convert a Figma file/node into finished, self-contained HTML + Tailwind (deterministic Style IR,
|
|
2839
|
+
description: "Convert a Figma file/node into finished, self-contained HTML + Tailwind (deterministic Style IR, optional LLM restructure). Asset inlining is off by default (placeholders shown for images/vectors); pass assets:true to render & inline them. The HTML is written to a file; a compact summary + path is returned to keep context small.",
|
|
2062
2840
|
inputSchema: {
|
|
2063
2841
|
...targetShape,
|
|
2064
2842
|
mode: z.enum(["tailwind", "inline"]).optional().describe("Output mode. Default 'tailwind'."),
|
|
2065
2843
|
document: z.boolean().optional().describe("Emit a full HTML document (default true) vs a fragment."),
|
|
2066
|
-
assets: z.boolean().optional().describe("Export & inline vectors/images as data URIs. Default
|
|
2844
|
+
assets: z.boolean().optional().describe("Export & inline vectors/images as data URIs. Default false (placeholders shown instead)."),
|
|
2067
2845
|
assetScale: z.number().min(1).max(4).optional().describe("Raster export scale (1-4). Default 2."),
|
|
2846
|
+
round: z.boolean().optional().describe(
|
|
2847
|
+
"Snap on-grid values to idiomatic Tailwind scale tokens (p-4, gap-6, rounded-lg, text-2xl); off-grid values stay exact. Default true. Set false for exact arbitrary values everywhere."
|
|
2848
|
+
),
|
|
2068
2849
|
llm: z.boolean().optional().describe("Run the LLM restructure pass (needs OLLAMA_* config). Default false."),
|
|
2069
2850
|
outFile: z.string().optional().describe("Override the output file name (within the output dir).")
|
|
2070
2851
|
}
|
|
@@ -2100,9 +2881,7 @@ var page = (title, body) => `<!doctype html><meta charset="utf-8"><title>figma-c
|
|
|
2100
2881
|
async function login(opts = {}) {
|
|
2101
2882
|
const cfg = await resolveConfig({ apiUrl: opts.apiUrl });
|
|
2102
2883
|
if (!cfg.apiUrl) {
|
|
2103
|
-
throw new Error(
|
|
2104
|
-
"OAuth login needs the backend URL. Set it with `figma-coder-mcp set-api <URL>` or FIGMA_MCP_API."
|
|
2105
|
-
);
|
|
2884
|
+
throw new Error("OAuth login needs the backend URL. Set FIGMA_MCP_API or pass --api <URL>.");
|
|
2106
2885
|
}
|
|
2107
2886
|
const base = cfg.apiUrl.replace(/\/+$/, "");
|
|
2108
2887
|
const tokens = await new Promise((resolve2, reject) => {
|
|
@@ -2155,13 +2934,15 @@ If it doesn't open automatically, visit:
|
|
|
2155
2934
|
openBrowser(authorizeUrl);
|
|
2156
2935
|
});
|
|
2157
2936
|
});
|
|
2158
|
-
await
|
|
2159
|
-
|
|
2160
|
-
|
|
2937
|
+
await saveSettings({
|
|
2938
|
+
env: {
|
|
2939
|
+
FIGMA_MCP_API: base,
|
|
2940
|
+
FIGMA_MCP_TOKEN: tokens.access_token
|
|
2941
|
+
},
|
|
2161
2942
|
refreshToken: tokens.refresh_token,
|
|
2162
2943
|
apiTokenExpiresAt: tokens.expires_in ? Date.now() + tokens.expires_in * 1e3 : void 0
|
|
2163
2944
|
});
|
|
2164
|
-
console.error(`\u2713 Saved Figma OAuth session to ${
|
|
2945
|
+
console.error(`\u2713 Saved Figma OAuth session to ${settingsPath()}`);
|
|
2165
2946
|
console.error("Run the MCP in remote mode (set FIGMA_MCP_MODE=remote) to use it.");
|
|
2166
2947
|
}
|
|
2167
2948
|
|
|
@@ -2192,29 +2973,31 @@ function targetFrom(value, flags) {
|
|
|
2192
2973
|
if (/figma\.com\//.test(value)) return { figmaUrl: value, nodeId };
|
|
2193
2974
|
return { fileKey: value, nodeId };
|
|
2194
2975
|
}
|
|
2195
|
-
var HELP = `figma-coder-mcp
|
|
2976
|
+
var HELP = `figma-coder-mcp: Figma -> HTML/Tailwind for AI agents (MCP server + CLI)
|
|
2196
2977
|
|
|
2197
2978
|
Usage:
|
|
2198
2979
|
figma-coder-mcp [serve] Start the MCP server over stdio (default)
|
|
2199
2980
|
figma-coder-mcp convert <url|key> [..] One-off convert to HTML (writes a file)
|
|
2200
2981
|
figma-coder-mcp data <url|key> [..] One-off: print/save the Style IR
|
|
2201
2982
|
figma-coder-mcp set-token <PAT> Store a Figma personal access token (local mode)
|
|
2202
|
-
figma-coder-mcp set-api <URL> Store the backend converter URL (remote mode)
|
|
2203
2983
|
figma-coder-mcp login [--api <URL>] Log in with Figma via OAuth (remote mode)
|
|
2204
2984
|
figma-coder-mcp status Show how requests will be served (no secrets)
|
|
2205
|
-
figma-coder-mcp logout Remove stored
|
|
2985
|
+
figma-coder-mcp logout Remove stored settings
|
|
2206
2986
|
figma-coder-mcp help Show this help
|
|
2207
2987
|
|
|
2208
2988
|
Common flags (convert/data):
|
|
2209
2989
|
--node <id> Node id ("1-23" or "1:23")
|
|
2210
2990
|
--mode <m> tailwind | inline (convert)
|
|
2211
|
-
--
|
|
2991
|
+
--assets Render & inline assets (convert; off by default, placeholders shown)
|
|
2992
|
+
--no-round Keep exact arbitrary values (default snaps on-grid values to Tailwind tokens)
|
|
2212
2993
|
--llm Run LLM restructure (convert; needs OLLAMA_* env)
|
|
2213
2994
|
--out <file> Output file name (convert)
|
|
2214
2995
|
|
|
2215
2996
|
Config (env or stored): FIGMA_PAT, FIGMA_MCP_API, FIGMA_MCP_TOKEN, FIGMA_MCP_MODE,
|
|
2216
2997
|
FIGMA_MCP_OUT_DIR, FIGMA_MCP_NO_BROWSER (skip auto-opening the browser on login).
|
|
2217
|
-
Auto mode prefers local (PAT) so it works even if the backend is down
|
|
2998
|
+
Auto mode prefers local (PAT) so it works even if the backend is down; with no PAT
|
|
2999
|
+
it uses the hosted backend by default (override the URL with FIGMA_MCP_API).
|
|
3000
|
+
Stored settings live in ~/.figma-mcp/settings.json under "env" (edit it directly if you like).`;
|
|
2218
3001
|
async function main() {
|
|
2219
3002
|
const [positionals, flags] = parseArgs(process.argv.slice(2));
|
|
2220
3003
|
const cmd = positionals[0] ?? "serve";
|
|
@@ -2227,7 +3010,9 @@ async function main() {
|
|
|
2227
3010
|
const text = await convertFigmaToHtml({
|
|
2228
3011
|
...targetFrom(positionals[1], flags),
|
|
2229
3012
|
mode: flags.mode === "inline" ? "inline" : flags.mode === "tailwind" ? "tailwind" : void 0,
|
|
2230
|
-
assets: flags["no-assets"] ? false : void 0,
|
|
3013
|
+
assets: flags.assets ? true : flags["no-assets"] ? false : void 0,
|
|
3014
|
+
round: flags["no-round"] ? false : void 0,
|
|
3015
|
+
// default true (in core)
|
|
2231
3016
|
llm: flags.llm === true,
|
|
2232
3017
|
outFile: typeof flags.out === "string" ? flags.out : void 0
|
|
2233
3018
|
});
|
|
@@ -2242,15 +3027,8 @@ async function main() {
|
|
|
2242
3027
|
case "set-token": {
|
|
2243
3028
|
const pat = positionals[1];
|
|
2244
3029
|
if (!pat) throw new Error("Usage: figma-coder-mcp set-token <PAT>");
|
|
2245
|
-
await
|
|
2246
|
-
console.log(`Stored
|
|
2247
|
-
return;
|
|
2248
|
-
}
|
|
2249
|
-
case "set-api": {
|
|
2250
|
-
const url = positionals[1];
|
|
2251
|
-
if (!url) throw new Error("Usage: figma-coder-mcp set-api <URL>");
|
|
2252
|
-
await saveCredentials({ apiUrl: url });
|
|
2253
|
-
console.log(`Stored backend URL (${url}) in ${credentialsPath()}`);
|
|
3030
|
+
await saveSettings({ env: { FIGMA_PAT: pat } });
|
|
3031
|
+
console.log(`Stored FIGMA_PAT in ${settingsPath()}`);
|
|
2254
3032
|
return;
|
|
2255
3033
|
}
|
|
2256
3034
|
case "login": {
|
|
@@ -2260,12 +3038,12 @@ async function main() {
|
|
|
2260
3038
|
case "status": {
|
|
2261
3039
|
const cfg = await resolveConfig();
|
|
2262
3040
|
console.log(describeConfig(cfg));
|
|
2263
|
-
console.log(`
|
|
3041
|
+
console.log(`settings: ${settingsPath()}`);
|
|
2264
3042
|
return;
|
|
2265
3043
|
}
|
|
2266
3044
|
case "logout":
|
|
2267
|
-
await
|
|
2268
|
-
console.log("Cleared stored
|
|
3045
|
+
await clearSettings();
|
|
3046
|
+
console.log("Cleared stored settings.");
|
|
2269
3047
|
return;
|
|
2270
3048
|
case "help":
|
|
2271
3049
|
case "--help":
|