@promptui-lib/figma-parser 0.1.9 → 0.1.10

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.
@@ -4,6 +4,8 @@ export { parseStyles, parseBackgroundColor, parseBorder, parseBorderRadius, pars
4
4
  export type { IStyleProperties } from './style-parser.js';
5
5
  export { parseTextStyles, extractTextContent } from './text-parser.js';
6
6
  export type { ITextProperties } from './text-parser.js';
7
+ export { parsePosition, parseContainerPosition, hasAutoLayout } from './position-parser.js';
8
+ export type { IPositionProperties } from './position-parser.js';
7
9
  export { parseNode, parseNodes, parseFrameName } from './node-parser.js';
8
10
  export type { IParseOptions } from './node-parser.js';
9
11
  export { detectSemanticTag, normalizeName, getSemanticAttributes } from './semantic-detector.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/parser/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AACpF,YAAY,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAE5D,OAAO,EAAE,WAAW,EAAE,oBAAoB,EAAE,WAAW,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACtH,YAAY,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAE1D,OAAO,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AACvE,YAAY,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAExD,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACzE,YAAY,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEtD,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/parser/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AACpF,YAAY,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAE5D,OAAO,EAAE,WAAW,EAAE,oBAAoB,EAAE,WAAW,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACtH,YAAY,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAE1D,OAAO,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AACvE,YAAY,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAExD,OAAO,EAAE,aAAa,EAAE,sBAAsB,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAC5F,YAAY,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAEhE,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACzE,YAAY,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEtD,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC"}
@@ -1,5 +1,6 @@
1
1
  export { parseLayout, parseSizing, parseLayoutAndSizing } from './layout-parser.js';
2
2
  export { parseStyles, parseBackgroundColor, parseBorder, parseBorderRadius, parseBoxShadow } from './style-parser.js';
3
3
  export { parseTextStyles, extractTextContent } from './text-parser.js';
4
+ export { parsePosition, parseContainerPosition, hasAutoLayout } from './position-parser.js';
4
5
  export { parseNode, parseNodes, parseFrameName } from './node-parser.js';
5
6
  export { detectSemanticTag, normalizeName, getSemanticAttributes } from './semantic-detector.js';
@@ -1 +1 @@
1
- {"version":3,"file":"layout-parser.d.ts","sourceRoot":"","sources":["../../src/parser/layout-parser.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAGrE,MAAM,WAAW,iBAAiB;IAChC,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB,aAAa,CAAC,EAAE,cAAc,CAAC;IAC/B,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B,GAAG,CAAC,EAAE,cAAc,CAAC;IACrB,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB,KAAK,CAAC,EAAE,cAAc,CAAC;IACvB,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB,IAAI,CAAC,EAAE,cAAc,CAAC;IACtB,QAAQ,CAAC,EAAE,cAAc,CAAC;CAC3B;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,UAAU,GAAG,iBAAiB,CA2F/D;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,UAAU,GAAG,iBAAiB,CAwD/D;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,UAAU,GAAG,cAAc,EAAE,CASvE"}
1
+ {"version":3,"file":"layout-parser.d.ts","sourceRoot":"","sources":["../../src/parser/layout-parser.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AA4BrE,MAAM,WAAW,iBAAiB;IAChC,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB,aAAa,CAAC,EAAE,cAAc,CAAC;IAC/B,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B,GAAG,CAAC,EAAE,cAAc,CAAC;IACrB,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB,KAAK,CAAC,EAAE,cAAc,CAAC;IACvB,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB,IAAI,CAAC,EAAE,cAAc,CAAC;IACtB,QAAQ,CAAC,EAAE,cAAc,CAAC;CAC3B;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,UAAU,GAAG,iBAAiB,CA2F/D;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,UAAU,GAAG,iBAAiB,CAoE/D;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,UAAU,GAAG,cAAc,EAAE,CASvE"}
@@ -3,6 +3,29 @@
3
3
  * Converte AutoLayout do Figma para propriedades CSS flexbox
4
4
  */
5
5
  import { findGapToken, generatePaddingToken } from '@promptui-lib/core';
6
+ /**
7
+ * Arredonda valores de pixel para evitar decimais desnecessários
8
+ * Valores inteiros ou próximos de .5 são preservados
9
+ */
10
+ function roundPixelValue(value) {
11
+ // Se é inteiro ou muito próximo de inteiro, retorna inteiro
12
+ if (Math.abs(value - Math.round(value)) < 0.01) {
13
+ return Math.round(value);
14
+ }
15
+ // Arredonda para 1 casa decimal no máximo
16
+ return Math.round(value * 10) / 10;
17
+ }
18
+ /**
19
+ * Formata valor de pixel arredondado
20
+ */
21
+ function formatPixels(value) {
22
+ const rounded = roundPixelValue(value);
23
+ // Se é inteiro, não mostra decimal
24
+ if (Number.isInteger(rounded)) {
25
+ return `${rounded}px`;
26
+ }
27
+ return `${rounded}px`;
28
+ }
6
29
  /**
7
30
  * Parseia AutoLayout de um node
8
31
  */
@@ -103,12 +126,19 @@ export function parseSizing(node) {
103
126
  if (node.absoluteBoundingBox?.width) {
104
127
  properties.width = {
105
128
  property: 'width',
106
- value: `${node.absoluteBoundingBox.width}px`,
129
+ value: formatPixels(node.absoluteBoundingBox.width),
107
130
  };
108
131
  }
109
132
  break;
110
133
  }
111
134
  }
135
+ else if (node.absoluteBoundingBox?.width) {
136
+ // Fallback: Se não tem layoutSizing mas tem dimensões, usa o tamanho absoluto
137
+ properties.width = {
138
+ property: 'width',
139
+ value: formatPixels(node.absoluteBoundingBox.width),
140
+ };
141
+ }
112
142
  // Height
113
143
  if (node.layoutSizingVertical) {
114
144
  switch (node.layoutSizingVertical) {
@@ -128,12 +158,19 @@ export function parseSizing(node) {
128
158
  if (node.absoluteBoundingBox?.height) {
129
159
  properties.height = {
130
160
  property: 'height',
131
- value: `${node.absoluteBoundingBox.height}px`,
161
+ value: formatPixels(node.absoluteBoundingBox.height),
132
162
  };
133
163
  }
134
164
  break;
135
165
  }
136
166
  }
167
+ else if (node.absoluteBoundingBox?.height) {
168
+ // Fallback: Se não tem layoutSizing mas tem dimensões, usa o tamanho absoluto
169
+ properties.height = {
170
+ property: 'height',
171
+ value: formatPixels(node.absoluteBoundingBox.height),
172
+ };
173
+ }
137
174
  return properties;
138
175
  }
139
176
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"node-parser.d.ts","sourceRoot":"","sources":["../../src/parser/node-parser.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EACV,UAAU,EACV,aAAa,EAKb,cAAc,EACd,gBAAgB,EACjB,MAAM,oBAAoB,CAAC;AAe5B,MAAM,WAAW,aAAa;IAC5B,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,gBAAgB,CAwB7D;AAuJD;;GAEG;AACH,wBAAgB,SAAS,CACvB,IAAI,EAAE,UAAU,EAChB,OAAO,GAAE,aAAkB,GAC1B,aAAa,CA6Cf;AAED;;GAEG;AACH,wBAAgB,UAAU,CACxB,KAAK,EAAE,UAAU,EAAE,EACnB,OAAO,GAAE,aAAkB,GAC1B,aAAa,EAAE,CAEjB"}
1
+ {"version":3,"file":"node-parser.d.ts","sourceRoot":"","sources":["../../src/parser/node-parser.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EACV,UAAU,EACV,aAAa,EAKb,cAAc,EACd,gBAAgB,EACjB,MAAM,oBAAoB,CAAC;AAgB5B,MAAM,WAAW,aAAa;IAC5B,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,gBAAgB,CAwB7D;AA6LD;;GAEG;AACH,wBAAgB,SAAS,CACvB,IAAI,EAAE,UAAU,EAChB,OAAO,GAAE,aAAkB,GAC1B,aAAa,CA6Cf;AAED;;GAEG;AACH,wBAAgB,UAAU,CACxB,KAAK,EAAE,UAAU,EAAE,EACnB,OAAO,GAAE,aAAkB,GAC1B,aAAa,EAAE,CAEjB"}
@@ -3,10 +3,11 @@
3
3
  * Parser principal que converte node do Figma em AST do componente
4
4
  */
5
5
  import { generateComponentName, generateFileName, generateBEMBlock, generateBEMElement, classifyComponent, extractBaseName, extractVariants, } from '@promptui-lib/core';
6
- import { parseLayoutAndSizing } from './layout-parser.js';
6
+ import { parseLayoutAndSizing, parseSizing } from './layout-parser.js';
7
7
  import { parseStyles } from './style-parser.js';
8
8
  import { parseTextStyles } from './text-parser.js';
9
9
  import { detectSemanticTag, normalizeName, getSemanticAttributes } from './semantic-detector.js';
10
+ import { parsePosition, parseContainerPosition } from './position-parser.js';
10
11
  /**
11
12
  * Parseia o nome do frame para extrair informações
12
13
  */
@@ -75,7 +76,7 @@ function nodeToJSX(node, blockName, depth = 0, parentNode) {
75
76
  /**
76
77
  * Coleta estilos de um node recursivamente
77
78
  */
78
- function collectStyles(node, blockName, depth = 0) {
79
+ function collectStyles(node, blockName, depth = 0, parentNode) {
79
80
  const blocks = [];
80
81
  const isRoot = depth === 0;
81
82
  // Usa nome normalizado (traduzido)
@@ -86,9 +87,41 @@ function collectStyles(node, blockName, depth = 0) {
86
87
  : `.${generateBEMElement(blockName, elementName)}`;
87
88
  // Coleta propriedades deste node
88
89
  const properties = [];
89
- // Layout
90
+ // Layout e sizing
91
+ // Para FRAME, COMPONENT, INSTANCE: extrai layout completo (flex, gap, etc)
92
+ // Para GROUP: extrai sizing e position: relative (container sem flex)
93
+ // Para outros tipos (RECTANGLE, etc): extrai apenas sizing (width, height)
90
94
  if (node.type === 'FRAME' || node.type === 'COMPONENT' || node.type === 'INSTANCE') {
91
95
  properties.push(...parseLayoutAndSizing(node));
96
+ // Se é um container sem Auto Layout, adiciona position: relative
97
+ const containerPos = parseContainerPosition(node);
98
+ if (containerPos) {
99
+ properties.push(containerPos);
100
+ }
101
+ }
102
+ else if (node.type === 'GROUP') {
103
+ // GROUPs são containers visuais - precisam de position: relative e sizing
104
+ const sizingProps = parseSizing(node);
105
+ const sizingArray = Object.values(sizingProps).filter((p) => p !== undefined);
106
+ properties.push(...sizingArray);
107
+ // GROUP sempre precisa de position: relative se tiver filhos
108
+ if (node.children && node.children.length > 0) {
109
+ properties.push({
110
+ property: 'position',
111
+ value: 'relative',
112
+ });
113
+ }
114
+ }
115
+ else if (node.type !== 'TEXT' && node.absoluteBoundingBox) {
116
+ // Para elementos não-texto com dimensões, extrai apenas sizing
117
+ const sizingProps = parseSizing(node);
118
+ const sizingArray = Object.values(sizingProps).filter((p) => p !== undefined);
119
+ properties.push(...sizingArray);
120
+ }
121
+ // Posicionamento absoluto (quando pai não tem Auto Layout)
122
+ if (parentNode && !isRoot) {
123
+ const positionProps = parsePosition(node, parentNode);
124
+ properties.push(...positionProps);
92
125
  }
93
126
  // Estilos visuais
94
127
  properties.push(...parseStyles(node));
@@ -103,13 +136,13 @@ function collectStyles(node, blockName, depth = 0) {
103
136
  properties,
104
137
  });
105
138
  }
106
- // Processa filhos
139
+ // Processa filhos (passa o node atual como pai)
107
140
  if (node.children) {
108
141
  for (const child of node.children) {
109
142
  if (child.visible === false || child.name.startsWith('_')) {
110
143
  continue;
111
144
  }
112
- blocks.push(...collectStyles(child, blockName, depth + 1));
145
+ blocks.push(...collectStyles(child, blockName, depth + 1, node));
113
146
  }
114
147
  }
115
148
  return blocks;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Position Parser
3
+ * Converte posicionamento e constraints do Figma para CSS
4
+ * Usado quando elementos não estão dentro de Auto Layout
5
+ */
6
+ import type { IFigmaNode, IStyleProperty } from '@promptui-lib/core';
7
+ export interface IPositionProperties {
8
+ position?: IStyleProperty;
9
+ top?: IStyleProperty;
10
+ right?: IStyleProperty;
11
+ bottom?: IStyleProperty;
12
+ left?: IStyleProperty;
13
+ transform?: IStyleProperty;
14
+ }
15
+ /**
16
+ * Verifica se um node pai tem Auto Layout
17
+ */
18
+ export declare function hasAutoLayout(node: IFigmaNode): boolean;
19
+ /**
20
+ * Parseia constraints e posição de um node
21
+ */
22
+ export declare function parsePosition(node: IFigmaNode, parentNode?: IFigmaNode): IStyleProperty[];
23
+ /**
24
+ * Verifica se o pai precisa de position: relative
25
+ * (necessário quando tem filhos com position: absolute)
26
+ */
27
+ export declare function needsRelativePosition(node: IFigmaNode): boolean;
28
+ /**
29
+ * Parseia position: relative para containers sem Auto Layout
30
+ */
31
+ export declare function parseContainerPosition(node: IFigmaNode): IStyleProperty | null;
32
+ //# sourceMappingURL=position-parser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"position-parser.d.ts","sourceRoot":"","sources":["../../src/parser/position-parser.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAgB,MAAM,oBAAoB,CAAC;AAEnF,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,GAAG,CAAC,EAAE,cAAc,CAAC;IACrB,KAAK,CAAC,EAAE,cAAc,CAAC;IACvB,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB,IAAI,CAAC,EAAE,cAAc,CAAC;IACtB,SAAS,CAAC,EAAE,cAAc,CAAC;CAC5B;AAoBD;;GAEG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAEvD;AAwBD;;GAEG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,UAAU,EAChB,UAAU,CAAC,EAAE,UAAU,GACtB,cAAc,EAAE,CAmIlB;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAM/D;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,UAAU,GAAG,cAAc,GAAG,IAAI,CAQ9E"}
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Position Parser
3
+ * Converte posicionamento e constraints do Figma para CSS
4
+ * Usado quando elementos não estão dentro de Auto Layout
5
+ */
6
+ /**
7
+ * Arredonda valores de pixel para evitar decimais desnecessários
8
+ */
9
+ function roundPixelValue(value) {
10
+ if (Math.abs(value - Math.round(value)) < 0.01) {
11
+ return Math.round(value);
12
+ }
13
+ return Math.round(value * 10) / 10;
14
+ }
15
+ /**
16
+ * Formata valor de pixel arredondado
17
+ */
18
+ function formatPixels(value) {
19
+ const rounded = roundPixelValue(value);
20
+ return `${rounded}px`;
21
+ }
22
+ /**
23
+ * Verifica se um node pai tem Auto Layout
24
+ */
25
+ export function hasAutoLayout(node) {
26
+ return node.layoutMode !== undefined && node.layoutMode !== 'NONE';
27
+ }
28
+ /**
29
+ * Calcula a posição relativa de um filho em relação ao pai
30
+ */
31
+ function calculateRelativePosition(child, parent) {
32
+ if (!child.absoluteBoundingBox || !parent.absoluteBoundingBox) {
33
+ return null;
34
+ }
35
+ const childBox = child.absoluteBoundingBox;
36
+ const parentBox = parent.absoluteBoundingBox;
37
+ return {
38
+ top: childBox.y - parentBox.y,
39
+ left: childBox.x - parentBox.x,
40
+ right: parentBox.x + parentBox.width - (childBox.x + childBox.width),
41
+ bottom: parentBox.y + parentBox.height - (childBox.y + childBox.height),
42
+ };
43
+ }
44
+ /**
45
+ * Parseia constraints e posição de um node
46
+ */
47
+ export function parsePosition(node, parentNode) {
48
+ const properties = [];
49
+ // Se não tem pai ou o pai tem Auto Layout, não precisa de posicionamento absoluto
50
+ if (!parentNode || hasAutoLayout(parentNode)) {
51
+ return properties;
52
+ }
53
+ // Calcula posição relativa
54
+ const relPos = calculateRelativePosition(node, parentNode);
55
+ if (!relPos) {
56
+ return properties;
57
+ }
58
+ const constraints = node.constraints;
59
+ // Se é um elemento dentro de um container sem Auto Layout, usa posição absoluta
60
+ properties.push({
61
+ property: 'position',
62
+ value: 'absolute',
63
+ });
64
+ // Aplica constraints ou usa posição padrão (top-left)
65
+ if (constraints) {
66
+ // Horizontal constraints
67
+ switch (constraints.horizontal) {
68
+ case 'LEFT':
69
+ properties.push({
70
+ property: 'left',
71
+ value: formatPixels(relPos.left),
72
+ });
73
+ break;
74
+ case 'RIGHT':
75
+ properties.push({
76
+ property: 'right',
77
+ value: formatPixels(relPos.right),
78
+ });
79
+ break;
80
+ case 'CENTER':
81
+ properties.push({
82
+ property: 'left',
83
+ value: '50%',
84
+ });
85
+ properties.push({
86
+ property: 'transform',
87
+ value: 'translateX(-50%)',
88
+ });
89
+ break;
90
+ case 'LEFT_RIGHT':
91
+ properties.push({
92
+ property: 'left',
93
+ value: formatPixels(relPos.left),
94
+ });
95
+ properties.push({
96
+ property: 'right',
97
+ value: formatPixels(relPos.right),
98
+ });
99
+ break;
100
+ case 'SCALE':
101
+ // Usa porcentagem
102
+ const parentWidth = parentNode.absoluteBoundingBox?.width ?? 1;
103
+ const leftPercent = (relPos.left / parentWidth) * 100;
104
+ properties.push({
105
+ property: 'left',
106
+ value: `${roundPixelValue(leftPercent)}%`,
107
+ });
108
+ break;
109
+ }
110
+ // Vertical constraints
111
+ switch (constraints.vertical) {
112
+ case 'TOP':
113
+ properties.push({
114
+ property: 'top',
115
+ value: formatPixels(relPos.top),
116
+ });
117
+ break;
118
+ case 'BOTTOM':
119
+ properties.push({
120
+ property: 'bottom',
121
+ value: formatPixels(relPos.bottom),
122
+ });
123
+ break;
124
+ case 'CENTER':
125
+ // Se já tem transform de X, combina
126
+ const existingTransform = properties.find(p => p.property === 'transform');
127
+ if (existingTransform) {
128
+ existingTransform.value = 'translate(-50%, -50%)';
129
+ }
130
+ else {
131
+ properties.push({
132
+ property: 'transform',
133
+ value: 'translateY(-50%)',
134
+ });
135
+ }
136
+ properties.push({
137
+ property: 'top',
138
+ value: '50%',
139
+ });
140
+ break;
141
+ case 'TOP_BOTTOM':
142
+ properties.push({
143
+ property: 'top',
144
+ value: formatPixels(relPos.top),
145
+ });
146
+ properties.push({
147
+ property: 'bottom',
148
+ value: formatPixels(relPos.bottom),
149
+ });
150
+ break;
151
+ case 'SCALE':
152
+ const parentHeight = parentNode.absoluteBoundingBox?.height ?? 1;
153
+ const topPercent = (relPos.top / parentHeight) * 100;
154
+ properties.push({
155
+ property: 'top',
156
+ value: `${roundPixelValue(topPercent)}%`,
157
+ });
158
+ break;
159
+ }
160
+ }
161
+ else {
162
+ // Sem constraints, usa posição top-left padrão
163
+ properties.push({
164
+ property: 'top',
165
+ value: formatPixels(relPos.top),
166
+ });
167
+ properties.push({
168
+ property: 'left',
169
+ value: formatPixels(relPos.left),
170
+ });
171
+ }
172
+ return properties;
173
+ }
174
+ /**
175
+ * Verifica se o pai precisa de position: relative
176
+ * (necessário quando tem filhos com position: absolute)
177
+ */
178
+ export function needsRelativePosition(node) {
179
+ // Se não tem Auto Layout e tem filhos, precisa de relative
180
+ if (!hasAutoLayout(node) && node.children && node.children.length > 0) {
181
+ return true;
182
+ }
183
+ return false;
184
+ }
185
+ /**
186
+ * Parseia position: relative para containers sem Auto Layout
187
+ */
188
+ export function parseContainerPosition(node) {
189
+ if (needsRelativePosition(node)) {
190
+ return {
191
+ property: 'position',
192
+ value: 'relative',
193
+ };
194
+ }
195
+ return null;
196
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"semantic-detector.d.ts","sourceRoot":"","sources":["../../src/parser/semantic-detector.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAmQrD;;GAEG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAalD;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,UAAU,EAChB,UAAU,CAAC,EAAE,UAAU,EACvB,KAAK,GAAE,MAAU,GAChB,MAAM,CAmKR;AA4JD;;;GAGG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,UAAU,EAChB,GAAG,EAAE,MAAM,EACX,cAAc,EAAE,MAAM,GACrB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAkKxB"}
1
+ {"version":3,"file":"semantic-detector.d.ts","sourceRoot":"","sources":["../../src/parser/semantic-detector.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAmQrD;;GAEG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAalD;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,UAAU,EAChB,UAAU,CAAC,EAAE,UAAU,EACvB,KAAK,GAAE,MAAU,GAChB,MAAM,CAmKR;AA4JD;;;GAGG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,UAAU,EAChB,GAAG,EAAE,MAAM,EACX,cAAc,EAAE,MAAM,GACrB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAwKxB"}
@@ -513,11 +513,17 @@ function hasInputChild(node) {
513
513
  */
514
514
  export function getSemanticAttributes(node, tag, normalizedName) {
515
515
  const attrs = {};
516
- // Links - não adiciona href automático para evitar warnings de a11y
516
+ // Links - adiciona href placeholder para evitar warnings de a11y
517
517
  // O desenvolvedor deve preencher o href correto
518
- // if (tag === 'a') {
519
- // attrs['href'] = '#'; // Removido - causa warning jsx-a11y/anchor-is-valid
520
- // }
518
+ if (tag === 'a') {
519
+ // Gera um href placeholder baseado no nome do link
520
+ const hrefSlug = normalizedName
521
+ .replace(/link$/i, '')
522
+ .replace(/[-\s]+/g, '-')
523
+ .replace(/^-|-$/g, '')
524
+ .toLowerCase();
525
+ attrs['href'] = hrefSlug ? `/${hrefSlug}` : '/';
526
+ }
521
527
  // Inputs
522
528
  if (tag === 'input') {
523
529
  // Detecta tipo de input
@@ -1 +1 @@
1
- {"version":3,"file":"style-parser.d.ts","sourceRoot":"","sources":["../../src/parser/style-parser.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EACV,UAAU,EAIV,cAAc,EACf,MAAM,oBAAoB,CAAC;AAS5B,MAAM,WAAW,gBAAgB;IAC/B,eAAe,CAAC,EAAE,cAAc,CAAC;IACjC,KAAK,CAAC,EAAE,cAAc,CAAC;IACvB,YAAY,CAAC,EAAE,cAAc,CAAC;IAC9B,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB,SAAS,CAAC,EAAE,cAAc,CAAC;IAC3B,OAAO,CAAC,EAAE,cAAc,CAAC;CAC1B;AAWD;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,UAAU,GAAG,cAAc,GAAG,IAAI,CA2B5E;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,UAAU,GAAG,cAAc,GAAG,IAAI,CAuBnE;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,UAAU,GAAG,cAAc,GAAG,IAAI,CAyBzE;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,UAAU,GAAG,cAAc,GAAG,IAAI,CAmCtE;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,UAAU,GAAG,cAAc,GAAG,IAAI,CASpE;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,UAAU,GAAG,cAAc,EAAE,CAmB9D"}
1
+ {"version":3,"file":"style-parser.d.ts","sourceRoot":"","sources":["../../src/parser/style-parser.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EACV,UAAU,EAIV,cAAc,EACf,MAAM,oBAAoB,CAAC;AAmB5B,MAAM,WAAW,gBAAgB;IAC/B,eAAe,CAAC,EAAE,cAAc,CAAC;IACjC,KAAK,CAAC,EAAE,cAAc,CAAC;IACvB,YAAY,CAAC,EAAE,cAAc,CAAC;IAC9B,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB,SAAS,CAAC,EAAE,cAAc,CAAC;IAC3B,OAAO,CAAC,EAAE,cAAc,CAAC;CAC1B;AAWD;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,UAAU,GAAG,cAAc,GAAG,IAAI,CA2B5E;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,UAAU,GAAG,cAAc,GAAG,IAAI,CAwBnE;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,UAAU,GAAG,cAAc,GAAG,IAAI,CAyBzE;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,UAAU,GAAG,cAAc,GAAG,IAAI,CAuCtE;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,UAAU,GAAG,cAAc,GAAG,IAAI,CAWpE;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,UAAU,GAAG,cAAc,EAAE,CAmB9D"}
@@ -3,6 +3,15 @@
3
3
  * Converte fills, strokes, effects do Figma para CSS
4
4
  */
5
5
  import { rgbaToHex, findColorToken, findRadiusToken, findShadowToken, generateBorderRadius, } from '@promptui-lib/core';
6
+ /**
7
+ * Arredonda valores de pixel para evitar decimais desnecessários
8
+ */
9
+ function roundPixelValue(value) {
10
+ if (Math.abs(value - Math.round(value)) < 0.01) {
11
+ return Math.round(value);
12
+ }
13
+ return Math.round(value * 10) / 10;
14
+ }
6
15
  /**
7
16
  * Extrai cor de um fill sólido
8
17
  */
@@ -49,9 +58,10 @@ export function parseBorder(node) {
49
58
  }
50
59
  const hex = rgbaToHex(stroke.color.r, stroke.color.g, stroke.color.b, stroke.opacity ?? 1);
51
60
  const token = findColorToken(hex);
61
+ const strokeWidth = roundPixelValue(node.strokeWeight);
52
62
  return {
53
63
  property: 'border',
54
- value: `${node.strokeWeight}px solid ${token ?? hex}`,
64
+ value: `${strokeWidth}px solid ${token ?? hex}`,
55
65
  token: token ?? undefined,
56
66
  };
57
67
  }
@@ -105,9 +115,13 @@ export function parseBoxShadow(node) {
105
115
  // Gera box-shadow customizado
106
116
  const { offset, radius, spread, color } = shadow;
107
117
  const hex = rgbaToHex(color.r, color.g, color.b, color.a);
118
+ const x = roundPixelValue(offset.x);
119
+ const y = roundPixelValue(offset.y);
120
+ const r = roundPixelValue(radius);
121
+ const s = roundPixelValue(spread ?? 0);
108
122
  return {
109
123
  property: 'box-shadow',
110
- value: `${offset.x}px ${offset.y}px ${radius}px ${spread ?? 0}px ${hex}`,
124
+ value: `${x}px ${y}px ${r}px ${s}px ${hex}`,
111
125
  };
112
126
  }
113
127
  /**
@@ -115,9 +129,11 @@ export function parseBoxShadow(node) {
115
129
  */
116
130
  export function parseOpacity(node) {
117
131
  if (node.opacity !== undefined && node.opacity < 1) {
132
+ // Arredonda para 2 casas decimais
133
+ const rounded = Math.round(node.opacity * 100) / 100;
118
134
  return {
119
135
  property: 'opacity',
120
- value: `${node.opacity}`,
136
+ value: `${rounded}`,
121
137
  };
122
138
  }
123
139
  return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@promptui-lib/figma-parser",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
4
4
  "private": false,
5
5
  "description": "Figma API client and parser for PromptUI",
6
6
  "license": "UNLICENSED",
@@ -30,7 +30,7 @@
30
30
  "dist"
31
31
  ],
32
32
  "dependencies": {
33
- "@promptui-lib/core": "0.1.9"
33
+ "@promptui-lib/core": "0.1.10"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@types/node": "^20.0.0",