@promptui-lib/figma-parser 0.1.9 → 0.1.11

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,EAMb,cAAc,EACd,gBAAgB,EACjB,MAAM,oBAAoB,CAAC;AAiB5B,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;AAkND;;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"}
@@ -2,11 +2,12 @@
2
2
  * Node Parser
3
3
  * Parser principal que converte node do Figma em AST do componente
4
4
  */
5
- import { generateComponentName, generateFileName, generateBEMBlock, generateBEMElement, classifyComponent, extractBaseName, extractVariants, } from '@promptui-lib/core';
6
- import { parseLayoutAndSizing } from './layout-parser.js';
5
+ import { generateComponentName, generateFileName, generateBEMBlock, generateBEMElement, classifyComponent, extractBaseName, extractVariants, isComponentNode, } from '@promptui-lib/core';
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
  */
@@ -33,6 +34,7 @@ export function parseFrameName(name) {
33
34
  }
34
35
  /**
35
36
  * Converte um node em elemento JSX
37
+ * Se o filho for um componente (marcado com #), retorna IJSXComponent em vez de JSX inline
36
38
  */
37
39
  function nodeToJSX(node, blockName, depth = 0, parentNode) {
38
40
  const isRoot = depth === 0;
@@ -57,7 +59,21 @@ function nodeToJSX(node, blockName, depth = 0, parentNode) {
57
59
  if (child.visible === false || child.name.startsWith('_')) {
58
60
  continue;
59
61
  }
60
- children.push(nodeToJSX(child, blockName, depth + 1, node));
62
+ // Se o filho é um componente (marcado com #), cria referência ao componente
63
+ if (isComponentNode(child.name)) {
64
+ const childComponentName = generateComponentName(child.name);
65
+ const childFileName = generateFileName(childComponentName);
66
+ const childLayer = classifyComponent(child);
67
+ children.push({
68
+ type: 'component',
69
+ componentName: childComponentName,
70
+ layer: childLayer,
71
+ fileName: childFileName,
72
+ });
73
+ }
74
+ else {
75
+ children.push(nodeToJSX(child, blockName, depth + 1, node));
76
+ }
61
77
  }
62
78
  }
63
79
  // Tags self-closing
@@ -75,7 +91,7 @@ function nodeToJSX(node, blockName, depth = 0, parentNode) {
75
91
  /**
76
92
  * Coleta estilos de um node recursivamente
77
93
  */
78
- function collectStyles(node, blockName, depth = 0) {
94
+ function collectStyles(node, blockName, depth = 0, parentNode) {
79
95
  const blocks = [];
80
96
  const isRoot = depth === 0;
81
97
  // Usa nome normalizado (traduzido)
@@ -86,9 +102,41 @@ function collectStyles(node, blockName, depth = 0) {
86
102
  : `.${generateBEMElement(blockName, elementName)}`;
87
103
  // Coleta propriedades deste node
88
104
  const properties = [];
89
- // Layout
105
+ // Layout e sizing
106
+ // Para FRAME, COMPONENT, INSTANCE: extrai layout completo (flex, gap, etc)
107
+ // Para GROUP: extrai sizing e position: relative (container sem flex)
108
+ // Para outros tipos (RECTANGLE, etc): extrai apenas sizing (width, height)
90
109
  if (node.type === 'FRAME' || node.type === 'COMPONENT' || node.type === 'INSTANCE') {
91
110
  properties.push(...parseLayoutAndSizing(node));
111
+ // Se é um container sem Auto Layout, adiciona position: relative
112
+ const containerPos = parseContainerPosition(node);
113
+ if (containerPos) {
114
+ properties.push(containerPos);
115
+ }
116
+ }
117
+ else if (node.type === 'GROUP') {
118
+ // GROUPs são containers visuais - precisam de position: relative e sizing
119
+ const sizingProps = parseSizing(node);
120
+ const sizingArray = Object.values(sizingProps).filter((p) => p !== undefined);
121
+ properties.push(...sizingArray);
122
+ // GROUP sempre precisa de position: relative se tiver filhos
123
+ if (node.children && node.children.length > 0) {
124
+ properties.push({
125
+ property: 'position',
126
+ value: 'relative',
127
+ });
128
+ }
129
+ }
130
+ else if (node.type !== 'TEXT' && node.absoluteBoundingBox) {
131
+ // Para elementos não-texto com dimensões, extrai apenas sizing
132
+ const sizingProps = parseSizing(node);
133
+ const sizingArray = Object.values(sizingProps).filter((p) => p !== undefined);
134
+ properties.push(...sizingArray);
135
+ }
136
+ // Posicionamento absoluto (quando pai não tem Auto Layout)
137
+ if (parentNode && !isRoot) {
138
+ const positionProps = parsePosition(node, parentNode);
139
+ properties.push(...positionProps);
92
140
  }
93
141
  // Estilos visuais
94
142
  properties.push(...parseStyles(node));
@@ -103,13 +151,18 @@ function collectStyles(node, blockName, depth = 0) {
103
151
  properties,
104
152
  });
105
153
  }
106
- // Processa filhos
154
+ // Processa filhos (passa o node atual como pai)
155
+ // Ignora filhos marcados com # pois são componentes com seus próprios estilos
107
156
  if (node.children) {
108
157
  for (const child of node.children) {
109
158
  if (child.visible === false || child.name.startsWith('_')) {
110
159
  continue;
111
160
  }
112
- blocks.push(...collectStyles(child, blockName, depth + 1));
161
+ // Ignora componentes filhos - eles têm seus próprios estilos
162
+ if (isComponentNode(child.name)) {
163
+ continue;
164
+ }
165
+ blocks.push(...collectStyles(child, blockName, depth + 1, node));
113
166
  }
114
167
  }
115
168
  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.11",
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.11"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@types/node": "^20.0.0",