@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.
@@ -551,13 +551,234 @@ function normalizeComponentProps(componentProperties) {
551
551
  // AST Pruning & Token Optimization
552
552
  // -----------------------------------------------------------------------------
553
553
 
554
- function pruneFigmaNode(node, context = {}, options = {}) {
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: node.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 (node.absoluteBoundingBox) {
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) pruned.primaryAxisAlignItems = node.primaryAxisAlignItems;
608
- if (node.counterAxisAlignItems) pruned.counterAxisAlignItems = node.counterAxisAlignItems;
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 (node.absoluteBoundingBox) {
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
- pruned.typography = {
681
- fontFamily: node.style.fontFamily,
682
- fontSize: node.style.fontSize,
683
- fontWeight: node.style.fontWeight,
684
- lineHeightPx: node.style.lineHeightPx ? Math.round(node.style.lineHeightPx) : undefined,
685
- letterSpacing: node.style.letterSpacing,
686
- textAlignHorizontal: node.style.textAlignHorizontal,
687
- textAlignVertical: node.style.textAlignVertical,
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
- pruned.children = node.children
695
- .filter((c) => c.visible !== false)
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pedroab0/skills",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Curated collection of modular, production-ready skills for AI coding agents (Claude Code, Cursor, Kiro, OpenCode, Antigravity)",
5
5
  "bin": {
6
6
  "skills": "bin/cli.js",
@@ -41,6 +41,11 @@
41
41
  "figma-extractor",
42
42
  "figma-node-builder",
43
43
  "README.md",
44
- "LICENSE"
44
+ "LICENSE",
45
+ "!**/demo/**",
46
+ "!**/demos/**",
47
+ "!**/.specs/**",
48
+ "!**/test_*.js",
49
+ "!**/figma-plugin/assets/**"
45
50
  ]
46
51
  }