@pedroab0/skills 1.1.0 → 1.2.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/bin/cli.js +23 -3
- package/figma-extractor/figma-plugin/code.js +461 -87
- package/figma-extractor/figma-plugin/ui.html +76 -23
- package/figma-extractor/scripts/fetch_figma.js +285 -35
- package/figma-to-code/scripts/fetch_figma.js +285 -35
- package/package.json +7 -2
- package/figma-extractor/demo/desktop_home.json +0 -2434
- package/figma-extractor/demo/dt_money_desktop_home.png +0 -0
- package/figma-extractor/demo/dt_money_preview.png +0 -0
- package/figma-extractor/demo/dt_money_selection.json +0 -472
- package/figma-extractor/demo/index.html +0 -188
- package/figma-extractor/demo/style.css +0 -360
- package/figma-extractor/figma-plugin/assets/cover-1920.png +0 -0
- package/figma-extractor/figma-plugin/assets/cover.jpg +0 -0
- package/figma-extractor/figma-plugin/assets/cover.png +0 -0
- package/figma-extractor/figma-plugin/assets/icon-128.png +0 -0
- package/figma-extractor/figma-plugin/assets/icon.png +0 -0
- package/figma-extractor/scripts/test_fetch_figma.js +0 -687
- package/figma-node-builder/scripts/test_inspect_spec.js +0 -163
- package/figma-to-code/scripts/test_fetch_figma.js +0 -660
|
@@ -261,14 +261,72 @@
|
|
|
261
261
|
setTimeout(() => URL.revokeObjectURL(url), 5000);
|
|
262
262
|
}
|
|
263
263
|
|
|
264
|
-
function flashButton(btn, text) {
|
|
265
|
-
|
|
264
|
+
function flashButton(btn, text, bgOverride = '') {
|
|
265
|
+
if (!btn.dataset.originalHtml) {
|
|
266
|
+
btn.dataset.originalHtml = btn.innerHTML;
|
|
267
|
+
}
|
|
268
|
+
if (btn._flashTimer) {
|
|
269
|
+
clearTimeout(btn._flashTimer);
|
|
270
|
+
}
|
|
266
271
|
btn.innerHTML = `<span>${text}</span>`;
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
272
|
+
if (bgOverride) {
|
|
273
|
+
btn.style.background = bgOverride;
|
|
274
|
+
} else {
|
|
275
|
+
btn.style.borderColor = '#10b981';
|
|
276
|
+
}
|
|
277
|
+
btn._flashTimer = setTimeout(() => {
|
|
278
|
+
btn.innerHTML = btn.dataset.originalHtml;
|
|
279
|
+
btn.style.background = '';
|
|
270
280
|
btn.style.borderColor = '';
|
|
271
|
-
|
|
281
|
+
btn._flashTimer = null;
|
|
282
|
+
}, 1800);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Figma plugin UI runs inside a null-origin sandboxed iframe where navigator.clipboard
|
|
286
|
+
// is blocked by Permissions-Policy. Synchronous execCommand('copy') is the official
|
|
287
|
+
// cross-browser / Figma Desktop clipboard mechanism, with Async Clipboard as fallback.
|
|
288
|
+
function copyUsingExecCommand(text) {
|
|
289
|
+
const textarea = document.createElement('textarea');
|
|
290
|
+
textarea.value = text;
|
|
291
|
+
textarea.setAttribute('readonly', '');
|
|
292
|
+
textarea.style.position = 'fixed';
|
|
293
|
+
textarea.style.top = '0';
|
|
294
|
+
textarea.style.left = '0';
|
|
295
|
+
textarea.style.width = '1px';
|
|
296
|
+
textarea.style.height = '1px';
|
|
297
|
+
textarea.style.padding = '0';
|
|
298
|
+
textarea.style.border = 'none';
|
|
299
|
+
textarea.style.outline = 'none';
|
|
300
|
+
textarea.style.boxShadow = 'none';
|
|
301
|
+
textarea.style.background = 'transparent';
|
|
302
|
+
textarea.style.opacity = '0';
|
|
303
|
+
textarea.style.pointerEvents = 'none';
|
|
304
|
+
document.body.appendChild(textarea);
|
|
305
|
+
textarea.focus({ preventScroll: true });
|
|
306
|
+
textarea.select();
|
|
307
|
+
textarea.setSelectionRange(0, textarea.value.length);
|
|
308
|
+
let copied = false;
|
|
309
|
+
try {
|
|
310
|
+
copied = document.execCommand('copy');
|
|
311
|
+
} catch (_) {
|
|
312
|
+
copied = false;
|
|
313
|
+
} finally {
|
|
314
|
+
document.body.removeChild(textarea);
|
|
315
|
+
}
|
|
316
|
+
return copied;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async function copyTextToClipboard(text) {
|
|
320
|
+
if (copyUsingExecCommand(text)) {
|
|
321
|
+
return true;
|
|
322
|
+
}
|
|
323
|
+
if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
|
|
324
|
+
try {
|
|
325
|
+
await navigator.clipboard.writeText(text);
|
|
326
|
+
return true;
|
|
327
|
+
} catch (_) {}
|
|
328
|
+
}
|
|
329
|
+
return false;
|
|
272
330
|
}
|
|
273
331
|
|
|
274
332
|
// Pure vanilla zero-dependency ZIP archive creator (Store format)
|
|
@@ -369,7 +427,7 @@
|
|
|
369
427
|
document.getElementById('node-bounds').textContent = `${w} × ${h} px`;
|
|
370
428
|
document.getElementById('hint-name').textContent = msg.name || 'component';
|
|
371
429
|
|
|
372
|
-
const jsonStr = JSON.stringify(msg.spec || {}
|
|
430
|
+
const jsonStr = JSON.stringify(msg.spec || {});
|
|
373
431
|
const sizeKb = (new Blob([jsonStr]).size / 1024).toFixed(1);
|
|
374
432
|
document.getElementById('node-size').textContent = `${sizeKb} KB`;
|
|
375
433
|
|
|
@@ -414,30 +472,25 @@
|
|
|
414
472
|
}
|
|
415
473
|
};
|
|
416
474
|
|
|
417
|
-
// Copy to clipboard
|
|
475
|
+
// Copy to clipboard (compact pruned JSON matching CLI default)
|
|
418
476
|
document.getElementById('btn-copy').onclick = async () => {
|
|
419
477
|
if (!currentData) return;
|
|
420
|
-
const jsonStr = JSON.stringify(currentData.spec
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
btn.innerHTML = '<span>✔ Copied Node JSON!</span>';
|
|
426
|
-
btn.style.background = '#16a34a';
|
|
478
|
+
const jsonStr = JSON.stringify(currentData.spec);
|
|
479
|
+
const btn = document.getElementById('btn-copy');
|
|
480
|
+
const ok = await copyTextToClipboard(jsonStr);
|
|
481
|
+
if (ok) {
|
|
482
|
+
flashButton(btn, '✔ Copied Node JSON!', '#16a34a');
|
|
427
483
|
parent.postMessage({ pluginMessage: { type: 'NOTIFY', message: `Copied "${currentData.name}.json" to clipboard!` } }, '*');
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
}, 2000);
|
|
432
|
-
} catch (err) {
|
|
433
|
-
console.error('Clipboard failed', err);
|
|
484
|
+
} else {
|
|
485
|
+
flashButton(btn, '✖ Copy failed — use .json', '#dc2626');
|
|
486
|
+
parent.postMessage({ pluginMessage: { type: 'NOTIFY', message: 'Failed to access clipboard. Please download .json instead.', error: true } }, '*');
|
|
434
487
|
}
|
|
435
488
|
};
|
|
436
489
|
|
|
437
490
|
// Download JSON
|
|
438
491
|
document.getElementById('btn-download-json').onclick = () => {
|
|
439
492
|
if (!currentData) return;
|
|
440
|
-
const jsonStr = JSON.stringify(currentData.spec
|
|
493
|
+
const jsonStr = JSON.stringify(currentData.spec);
|
|
441
494
|
const blob = new Blob([jsonStr], { type: 'application/json' });
|
|
442
495
|
triggerDownload(blob, `${currentData.name}.json`);
|
|
443
496
|
flashButton(document.getElementById('btn-download-json'), '✔ Saved .json');
|
|
@@ -453,7 +506,7 @@
|
|
|
453
506
|
// Download Both (.specs.zip)
|
|
454
507
|
document.getElementById('btn-download-both').onclick = () => {
|
|
455
508
|
if (!currentData) return;
|
|
456
|
-
const jsonStr = JSON.stringify(currentData.spec
|
|
509
|
+
const jsonStr = JSON.stringify(currentData.spec);
|
|
457
510
|
const enc = new TextEncoder();
|
|
458
511
|
const files = [
|
|
459
512
|
{ name: `${currentData.name}.json`, data: enc.encode(jsonStr) }
|
|
@@ -551,13 +551,234 @@ function normalizeComponentProps(componentProperties) {
|
|
|
551
551
|
// AST Pruning & Token Optimization
|
|
552
552
|
// -----------------------------------------------------------------------------
|
|
553
553
|
|
|
554
|
-
|
|
554
|
+
const VECTOR_PRIMITIVE_TYPES = new Set([
|
|
555
|
+
'VECTOR',
|
|
556
|
+
'BOOLEAN_OPERATION',
|
|
557
|
+
'STAR',
|
|
558
|
+
'LINE',
|
|
559
|
+
'REGULAR_POLYGON',
|
|
560
|
+
'POLYGON',
|
|
561
|
+
]);
|
|
562
|
+
|
|
563
|
+
function cleanLayerName(rawName, fallback = 'Node') {
|
|
564
|
+
if (!rawName || typeof rawName !== 'string') return fallback;
|
|
565
|
+
const trimmed = rawName.trim();
|
|
566
|
+
if (!trimmed) return fallback;
|
|
567
|
+
if (/^data:image\//i.test(trimmed)) return 'Image';
|
|
568
|
+
if (/^(?:<svg\b|<\?xml\b)/i.test(trimmed)) return 'SVG';
|
|
569
|
+
if (/^https?:\/\//i.test(trimmed) && trimmed.length > 60) {
|
|
570
|
+
try {
|
|
571
|
+
const lastSeg = trimmed.split('?')[0].split('/').pop();
|
|
572
|
+
return lastSeg && lastSeg.length <= 60 ? lastSeg : 'Image';
|
|
573
|
+
} catch (_) {
|
|
574
|
+
return 'Image';
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
const singleLine = trimmed.replace(/\s+/g, ' ');
|
|
578
|
+
return singleLine.length > 80 ? singleLine.slice(0, 80) + '…' : singleLine;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function isRawAssetString(str) {
|
|
582
|
+
if (!str || typeof str !== 'string') return false;
|
|
583
|
+
const t = str.trim();
|
|
584
|
+
return /^data:image\//i.test(t) || /^(?:<svg\b|<\?xml\b)/i.test(t);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function nodeHasImageFill(node) {
|
|
588
|
+
return Array.isArray(node.fills) && node.fills.some((f) => f && f.visible !== false && f.type === 'IMAGE');
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const GRAPHIC_NAME_RE =
|
|
592
|
+
/svg|icon|ícone|icone|logo|marca|brand|illustrat|ilustra|vector|vetor|symbol|simbol|símbolo|glyph|graphic|ornament|pattern|artwork/i;
|
|
593
|
+
|
|
594
|
+
function hasSvgExportSetting(node) {
|
|
595
|
+
if (!Array.isArray(node.exportSettings) || node.exportSettings.length === 0) return false;
|
|
596
|
+
return node.exportSettings.some((s) => s && String(s.format || '').toUpperCase() === 'SVG');
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function subtreeHasNoInstances(node, maxNodes = 400) {
|
|
600
|
+
let visited = 0;
|
|
601
|
+
function walk(curr, isRoot) {
|
|
602
|
+
if (!curr || typeof curr !== 'object' || ++visited > maxNodes) return false;
|
|
603
|
+
const t = curr.type || '';
|
|
604
|
+
if (!isRoot && (t === 'INSTANCE' || t === 'COMPONENT' || t === 'COMPONENT_SET')) return false;
|
|
605
|
+
const kids = Array.isArray(curr.children) ? curr.children.filter((c) => c && c.visible !== false) : [];
|
|
606
|
+
for (const k of kids) {
|
|
607
|
+
if (!walk(k, false)) return false;
|
|
608
|
+
}
|
|
609
|
+
return true;
|
|
610
|
+
}
|
|
611
|
+
return walk(node, true);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function analyzeVectorSubtree(node, varMap, maxNodes = 600) {
|
|
615
|
+
let visited = 0;
|
|
616
|
+
let vectorPrimitiveCount = 0;
|
|
617
|
+
let shapeCount = 0;
|
|
618
|
+
let plainRectCount = 0;
|
|
619
|
+
let primaryFill = null;
|
|
620
|
+
|
|
621
|
+
const rootType = node.type || '';
|
|
622
|
+
const hasSvgExport = hasSvgExportSetting(node);
|
|
623
|
+
|
|
624
|
+
// Preserve Auto-Layout wrapper frames with their own styling (e.g., Icon Badge) unless explicitly exported as SVG
|
|
625
|
+
if (rootType === 'FRAME' && !hasSvgExport && node.layoutMode && node.layoutMode !== 'NONE') {
|
|
626
|
+
const ownFills = simplifyFills(node.fills, node.boundVariables || {}, varMap);
|
|
627
|
+
const ownStrokes = simplifyStrokes(node.strokes, node.strokeWeight, node.strokeAlign, node.boundVariables || {}, varMap);
|
|
628
|
+
if (ownFills || ownStrokes || node.cornerRadius) {
|
|
629
|
+
return { isPure: false, primaryFill: null };
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function check(curr, isRoot) {
|
|
634
|
+
if (!curr || typeof curr !== 'object' || ++visited > maxNodes) return false;
|
|
635
|
+
const t = curr.type || '';
|
|
636
|
+
if (t === 'TEXT' || t === 'SLICE' || t === 'STICKY' || t === 'TABLE') return false;
|
|
637
|
+
if (!isRoot && (t === 'INSTANCE' || t === 'COMPONENT' || t === 'COMPONENT_SET')) return false;
|
|
638
|
+
if (nodeHasImageFill(curr)) return false;
|
|
639
|
+
|
|
640
|
+
if (!primaryFill) {
|
|
641
|
+
const fills = simplifyFills(curr.fills, curr.boundVariables || {}, varMap);
|
|
642
|
+
const solidFill = fills && fills.find((f) => f.type === 'SOLID' && f.color);
|
|
643
|
+
if (solidFill) {
|
|
644
|
+
primaryFill = solidFill;
|
|
645
|
+
} else {
|
|
646
|
+
const strokes = simplifyStrokes(curr.strokes, curr.strokeWeight, curr.strokeAlign, curr.boundVariables || {}, varMap);
|
|
647
|
+
if (strokes && strokes.colors && strokes.colors[0]) {
|
|
648
|
+
const c0 = strokes.colors[0];
|
|
649
|
+
primaryFill = typeof c0 === 'object' ? { type: 'SOLID', ...c0 } : { type: 'SOLID', color: c0 };
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
if (VECTOR_PRIMITIVE_TYPES.has(t)) {
|
|
655
|
+
vectorPrimitiveCount++;
|
|
656
|
+
return true;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
if (t === 'ELLIPSE') {
|
|
660
|
+
shapeCount++;
|
|
661
|
+
return true;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
if (t === 'RECTANGLE') {
|
|
665
|
+
if (curr.isMask === true || /clip|mask|vector|path|rect|bound|bg/i.test(String(curr.name || ''))) {
|
|
666
|
+
shapeCount++;
|
|
667
|
+
} else {
|
|
668
|
+
plainRectCount++;
|
|
669
|
+
}
|
|
670
|
+
return true;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
if (t === 'GROUP' || t === 'FRAME' || (isRoot && (t === 'INSTANCE' || t === 'COMPONENT'))) {
|
|
674
|
+
if (!isRoot && t === 'FRAME' && curr.layoutMode && curr.layoutMode !== 'NONE') {
|
|
675
|
+
return false;
|
|
676
|
+
}
|
|
677
|
+
const kids = Array.isArray(curr.children) ? curr.children.filter((c) => c && c.visible !== false) : [];
|
|
678
|
+
if (kids.length === 0) return !isRoot;
|
|
679
|
+
for (const k of kids) {
|
|
680
|
+
if (!check(k, false)) return false;
|
|
681
|
+
}
|
|
682
|
+
return true;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
return false;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
const treeOk = check(node, true);
|
|
689
|
+
const w = node.absoluteBoundingBox ? node.absoluteBoundingBox.width : 9999;
|
|
690
|
+
const h = node.absoluteBoundingBox ? node.absoluteBoundingBox.height : 9999;
|
|
691
|
+
|
|
692
|
+
if (!treeOk) {
|
|
693
|
+
if (hasSvgExport && w <= 800 && h <= 800 && !nodeHasImageFill(node) && subtreeHasNoInstances(node)) {
|
|
694
|
+
return { isPure: true, primaryFill };
|
|
695
|
+
}
|
|
696
|
+
return { isPure: false, primaryFill: null };
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
const isSmallGraphic = w <= 160 && h <= 160;
|
|
700
|
+
const hasGraphicName = GRAPHIC_NAME_RE.test(String(node.name || ''));
|
|
701
|
+
|
|
702
|
+
const hasEnoughPrimitives =
|
|
703
|
+
vectorPrimitiveCount > 0 ||
|
|
704
|
+
(shapeCount + plainRectCount >= 2 && (hasSvgExport || (isSmallGraphic && hasGraphicName)));
|
|
705
|
+
|
|
706
|
+
const rectGuardOk =
|
|
707
|
+
plainRectCount === 0 ||
|
|
708
|
+
vectorPrimitiveCount >= 2 ||
|
|
709
|
+
isSmallGraphic ||
|
|
710
|
+
hasGraphicName ||
|
|
711
|
+
hasSvgExport;
|
|
712
|
+
|
|
713
|
+
const isPure = hasEnoughPrimitives && rectGuardOk;
|
|
714
|
+
return { isPure, primaryFill };
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function analyzeMaskedImageSubtree(node) {
|
|
718
|
+
const t = node.type || '';
|
|
719
|
+
if (t !== 'GROUP' && t !== 'FRAME') return null;
|
|
720
|
+
if (node.layoutMode && node.layoutMode !== 'NONE') return null;
|
|
721
|
+
|
|
722
|
+
const kids = Array.isArray(node.children) ? node.children.filter((c) => c && c.visible !== false) : [];
|
|
723
|
+
if (kids.length === 0 || kids.length > 20) return null;
|
|
724
|
+
|
|
725
|
+
let foundImageFill = false;
|
|
726
|
+
let hasMaskOrClip = false;
|
|
727
|
+
|
|
728
|
+
function scan(curr) {
|
|
729
|
+
const ct = curr.type || '';
|
|
730
|
+
if (ct === 'TEXT' || ct === 'INSTANCE' || ct === 'COMPONENT') return false;
|
|
731
|
+
if (curr.isMask === true) hasMaskOrClip = true;
|
|
732
|
+
if (nodeHasImageFill(curr)) foundImageFill = true;
|
|
733
|
+
|
|
734
|
+
const sub = Array.isArray(curr.children) ? curr.children.filter((c) => c && c.visible !== false) : [];
|
|
735
|
+
for (const s of sub) {
|
|
736
|
+
if (!scan(s)) return false;
|
|
737
|
+
}
|
|
738
|
+
return true;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
if (!scan(node) || !foundImageFill) return null;
|
|
742
|
+
if (!hasMaskOrClip && !/mask|image|img|photo|avatar|banner|thumb/i.test(String(node.name || ''))) {
|
|
743
|
+
return null;
|
|
744
|
+
}
|
|
745
|
+
return true;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
function pruneFigmaNode(node, context = {}, options = {}, _depth = 0) {
|
|
555
749
|
if (!node || typeof node !== 'object') return node;
|
|
556
750
|
|
|
557
751
|
const { varMap, components, componentSets } = context;
|
|
752
|
+
const cleanName = cleanLayerName(node.name, 'Node');
|
|
753
|
+
const bound = node.boundVariables || {};
|
|
754
|
+
const bounds = node.absoluteBoundingBox
|
|
755
|
+
? {
|
|
756
|
+
width: Math.round(node.absoluteBoundingBox.width),
|
|
757
|
+
height: Math.round(node.absoluteBoundingBox.height),
|
|
758
|
+
}
|
|
759
|
+
: undefined;
|
|
760
|
+
|
|
761
|
+
// 1. Leaf Vector Primitives (VECTOR, BOOLEAN_OPERATION, STAR, LINE, POLYGON)
|
|
762
|
+
if (VECTOR_PRIMITIVE_TYPES.has(node.type)) {
|
|
763
|
+
const vecNode = { name: cleanName, type: 'VECTOR' };
|
|
764
|
+
if (options.includeIds && node.id) vecNode.id = node.id;
|
|
765
|
+
if (node.visible === false) vecNode.visible = false;
|
|
766
|
+
if (bounds) vecNode.bounds = bounds;
|
|
767
|
+
const fills = simplifyFills(node.fills, bound, varMap);
|
|
768
|
+
if (fills) {
|
|
769
|
+
vecNode.fills = fills;
|
|
770
|
+
} else {
|
|
771
|
+
const strokes = simplifyStrokes(node.strokes, node.strokeWeight, node.strokeAlign, bound, varMap);
|
|
772
|
+
if (strokes && strokes.colors && strokes.colors[0]) {
|
|
773
|
+
const c0 = strokes.colors[0];
|
|
774
|
+
vecNode.fills = [typeof c0 === 'object' ? { type: 'SOLID', ...c0 } : { type: 'SOLID', color: c0 }];
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
return vecNode;
|
|
778
|
+
}
|
|
558
779
|
|
|
559
780
|
const pruned = {
|
|
560
|
-
name:
|
|
781
|
+
name: cleanName,
|
|
561
782
|
type: node.type,
|
|
562
783
|
};
|
|
563
784
|
|
|
@@ -567,16 +788,14 @@ function pruneFigmaNode(node, context = {}, options = {}) {
|
|
|
567
788
|
|
|
568
789
|
if (node.visible === false) pruned.visible = false;
|
|
569
790
|
|
|
570
|
-
const bound = node.boundVariables || {};
|
|
571
|
-
|
|
572
791
|
// Component Instance & Master Resolution
|
|
573
792
|
if (node.type === 'INSTANCE') {
|
|
574
793
|
if (node.componentId && components && components[node.componentId]) {
|
|
575
794
|
const master = components[node.componentId];
|
|
576
795
|
const set = master.componentSetId && componentSets ? componentSets[master.componentSetId] : null;
|
|
577
796
|
pruned.component = {
|
|
578
|
-
name: set ? set.name : master.name,
|
|
579
|
-
variant: master.name,
|
|
797
|
+
name: cleanLayerName(set ? set.name : master.name, cleanName),
|
|
798
|
+
variant: cleanLayerName(master.name, cleanName),
|
|
580
799
|
};
|
|
581
800
|
if (options.includeIds) {
|
|
582
801
|
pruned.component.id = node.componentId;
|
|
@@ -588,12 +807,7 @@ function pruneFigmaNode(node, context = {}, options = {}) {
|
|
|
588
807
|
|
|
589
808
|
// Shallow instances: skip internal rendering sub-layers if requested
|
|
590
809
|
if (options.shallowInstances) {
|
|
591
|
-
if (
|
|
592
|
-
pruned.bounds = {
|
|
593
|
-
width: Math.round(node.absoluteBoundingBox.width),
|
|
594
|
-
height: Math.round(node.absoluteBoundingBox.height),
|
|
595
|
-
};
|
|
596
|
-
}
|
|
810
|
+
if (bounds) pruned.bounds = bounds;
|
|
597
811
|
return pruned;
|
|
598
812
|
}
|
|
599
813
|
} else if (node.type === 'COMPONENT' || node.type === 'COMPONENT_SET') {
|
|
@@ -601,11 +815,42 @@ function pruneFigmaNode(node, context = {}, options = {}) {
|
|
|
601
815
|
if (props) pruned.props = props;
|
|
602
816
|
}
|
|
603
817
|
|
|
818
|
+
// 2. Collapse Pure SVG / Icon / Vector Graphic Containers
|
|
819
|
+
if (_depth > 0 || node.type === 'GROUP' || node.type === 'INSTANCE') {
|
|
820
|
+
const vecCheck = analyzeVectorSubtree(node, varMap);
|
|
821
|
+
if (vecCheck.isPure) {
|
|
822
|
+
if (node.type !== 'INSTANCE' && node.type !== 'COMPONENT') {
|
|
823
|
+
pruned.type = 'VECTOR';
|
|
824
|
+
pruned.isSvg = true;
|
|
825
|
+
}
|
|
826
|
+
if (bounds) pruned.bounds = bounds;
|
|
827
|
+
const ownFills = simplifyFills(node.fills, bound, varMap);
|
|
828
|
+
if (ownFills) {
|
|
829
|
+
pruned.fills = ownFills;
|
|
830
|
+
} else if (vecCheck.primaryFill) {
|
|
831
|
+
pruned.fills = [vecCheck.primaryFill];
|
|
832
|
+
}
|
|
833
|
+
return pruned;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// 3. Collapse Masked Image Groups
|
|
838
|
+
if (_depth > 0 && analyzeMaskedImageSubtree(node)) {
|
|
839
|
+
if (bounds) pruned.bounds = bounds;
|
|
840
|
+
pruned.fills = [{ type: 'IMAGE' }];
|
|
841
|
+
if (node.cornerRadius) pruned.cornerRadius = node.cornerRadius;
|
|
842
|
+
return pruned;
|
|
843
|
+
}
|
|
844
|
+
|
|
604
845
|
// Layout & Flexbox properties
|
|
605
846
|
if (node.layoutMode && node.layoutMode !== 'NONE') {
|
|
606
847
|
pruned.layoutMode = node.layoutMode;
|
|
607
|
-
if (node.primaryAxisAlignItems
|
|
608
|
-
|
|
848
|
+
if (node.primaryAxisAlignItems && node.primaryAxisAlignItems !== 'MIN') {
|
|
849
|
+
pruned.primaryAxisAlignItems = node.primaryAxisAlignItems;
|
|
850
|
+
}
|
|
851
|
+
if (node.counterAxisAlignItems && node.counterAxisAlignItems !== 'MIN') {
|
|
852
|
+
pruned.counterAxisAlignItems = node.counterAxisAlignItems;
|
|
853
|
+
}
|
|
609
854
|
|
|
610
855
|
if (node.itemSpacing) {
|
|
611
856
|
pruned.itemSpacing = node.itemSpacing;
|
|
@@ -626,18 +871,15 @@ function pruneFigmaNode(node, context = {}, options = {}) {
|
|
|
626
871
|
if (padToken) pruned.paddingToken = padToken;
|
|
627
872
|
}
|
|
628
873
|
|
|
629
|
-
if (node.layoutWrap) pruned.layoutWrap = node.layoutWrap;
|
|
874
|
+
if (node.layoutWrap && node.layoutWrap !== 'NO_WRAP') pruned.layoutWrap = node.layoutWrap;
|
|
630
875
|
}
|
|
631
876
|
|
|
632
877
|
if (node.layoutGrow) pruned.layoutGrow = node.layoutGrow;
|
|
633
|
-
if (node.layoutAlign) pruned.layoutAlign = node.layoutAlign;
|
|
878
|
+
if (node.layoutAlign && node.layoutAlign !== 'INHERIT') pruned.layoutAlign = node.layoutAlign;
|
|
634
879
|
|
|
635
880
|
// Dimensions & Coordinates
|
|
636
|
-
if (
|
|
637
|
-
pruned.bounds =
|
|
638
|
-
width: Math.round(node.absoluteBoundingBox.width),
|
|
639
|
-
height: Math.round(node.absoluteBoundingBox.height),
|
|
640
|
-
};
|
|
881
|
+
if (bounds) {
|
|
882
|
+
pruned.bounds = bounds;
|
|
641
883
|
}
|
|
642
884
|
|
|
643
885
|
if (node.minWidth) pruned.minWidth = node.minWidth;
|
|
@@ -656,8 +898,9 @@ function pruneFigmaNode(node, context = {}, options = {}) {
|
|
|
656
898
|
pruned.cornerRadius = node.cornerRadius;
|
|
657
899
|
const radiusToken = resolveVariableToken(bound.cornerRadius, varMap);
|
|
658
900
|
if (radiusToken) pruned.cornerRadiusToken = radiusToken;
|
|
901
|
+
} else if (Array.isArray(node.rectangleCornerRadii) && node.rectangleCornerRadii.some((r) => r > 0)) {
|
|
902
|
+
pruned.cornerRadii = node.rectangleCornerRadii;
|
|
659
903
|
}
|
|
660
|
-
if (node.rectangleCornerRadii) pruned.cornerRadii = node.rectangleCornerRadii;
|
|
661
904
|
if (node.opacity !== undefined && node.opacity !== 1) pruned.opacity = node.opacity;
|
|
662
905
|
|
|
663
906
|
// Effects (Drop Shadows, Layer Blurs)
|
|
@@ -675,25 +918,32 @@ function pruneFigmaNode(node, context = {}, options = {}) {
|
|
|
675
918
|
|
|
676
919
|
// Typography (for TEXT nodes)
|
|
677
920
|
if (node.type === 'TEXT') {
|
|
678
|
-
pruned.characters = node.characters;
|
|
921
|
+
pruned.characters = isRawAssetString(node.characters) ? '[raw-asset-omitted]' : node.characters;
|
|
679
922
|
if (node.style) {
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
}
|
|
923
|
+
const typo = {};
|
|
924
|
+
if (node.style.fontFamily) typo.fontFamily = node.style.fontFamily;
|
|
925
|
+
if (node.style.fontSize) typo.fontSize = node.style.fontSize;
|
|
926
|
+
if (node.style.fontWeight) typo.fontWeight = node.style.fontWeight;
|
|
927
|
+
if (node.style.lineHeightPx) typo.lineHeightPx = Math.round(node.style.lineHeightPx);
|
|
928
|
+
if (node.style.letterSpacing) typo.letterSpacing = node.style.letterSpacing;
|
|
929
|
+
if (node.style.textAlignHorizontal && node.style.textAlignHorizontal !== 'LEFT') {
|
|
930
|
+
typo.textAlignHorizontal = node.style.textAlignHorizontal;
|
|
931
|
+
}
|
|
932
|
+
if (node.style.textAlignVertical && node.style.textAlignVertical !== 'TOP') {
|
|
933
|
+
typo.textAlignVertical = node.style.textAlignVertical;
|
|
934
|
+
}
|
|
935
|
+
if (Object.keys(typo).length > 0) {
|
|
936
|
+
pruned.typography = typo;
|
|
937
|
+
}
|
|
689
938
|
}
|
|
690
939
|
}
|
|
691
940
|
|
|
692
941
|
// Recursively process children
|
|
693
942
|
if (Array.isArray(node.children) && node.children.length > 0) {
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
.map((c) => pruneFigmaNode(c, context, options));
|
|
943
|
+
const visibleChildren = node.children.filter((c) => c && c.visible !== false);
|
|
944
|
+
if (visibleChildren.length > 0) {
|
|
945
|
+
pruned.children = visibleChildren.map((c) => pruneFigmaNode(c, context, options, _depth + 1));
|
|
946
|
+
}
|
|
697
947
|
}
|
|
698
948
|
|
|
699
949
|
return pruned;
|