@promptui-lib/figma-parser 0.1.19 → 0.1.22

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.
@@ -1,10 +1,10 @@
1
- export { parseLayout, parseSizing, parseLayoutAndSizing } from './layout-parser.js';
1
+ export { parseLayout, parseSizing, parseLayoutAndSizing, parsePadding } from './layout-parser.js';
2
2
  export type { ILayoutProperties } from './layout-parser.js';
3
- export { parseStyles, parseBackgroundColor, parseBorder, parseBorderRadius, parseBoxShadow } from './style-parser.js';
3
+ export { parseStyles, parseBackgroundColor, parseBorder, parseBorderRadius, parseBoxShadow, parseOverflow, parseTransform } from './style-parser.js';
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';
7
+ export { parsePosition, parseContainerPosition, hasAutoLayout, isAbsolutelyPositioned } from './position-parser.js';
8
8
  export type { IPositionProperties } from './position-parser.js';
9
9
  export { parseNode, parseNodes, parseFrameName } from './node-parser.js';
10
10
  export type { IParseOptions } from './node-parser.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,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
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/parser/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,oBAAoB,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClG,YAAY,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAE5D,OAAO,EAAE,WAAW,EAAE,oBAAoB,EAAE,WAAW,EAAE,iBAAiB,EAAE,cAAc,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACrJ,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,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AACpH,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,6 +1,6 @@
1
- export { parseLayout, parseSizing, parseLayoutAndSizing } from './layout-parser.js';
2
- export { parseStyles, parseBackgroundColor, parseBorder, parseBorderRadius, parseBoxShadow } from './style-parser.js';
1
+ export { parseLayout, parseSizing, parseLayoutAndSizing, parsePadding } from './layout-parser.js';
2
+ export { parseStyles, parseBackgroundColor, parseBorder, parseBorderRadius, parseBoxShadow, parseOverflow, parseTransform } from './style-parser.js';
3
3
  export { parseTextStyles, extractTextContent } from './text-parser.js';
4
- export { parsePosition, parseContainerPosition, hasAutoLayout } from './position-parser.js';
4
+ export { parsePosition, parseContainerPosition, hasAutoLayout, isAbsolutelyPositioned } from './position-parser.js';
5
5
  export { parseNode, parseNodes, parseFrameName } from './node-parser.js';
6
6
  export { detectSemanticTag, normalizeName, getSemanticAttributes } from './semantic-detector.js';
@@ -15,16 +15,22 @@ export interface ILayoutProperties {
15
15
  flex?: IStyleProperty;
16
16
  flexWrap?: IStyleProperty;
17
17
  }
18
+ /**
19
+ * Parseia padding de um node (funciona com ou sem Auto Layout)
20
+ */
21
+ export declare function parsePadding(node: IFigmaNode): IStyleProperty | null;
18
22
  /**
19
23
  * Parseia AutoLayout de um node
20
24
  */
21
25
  export declare function parseLayout(node: IFigmaNode): ILayoutProperties;
22
26
  /**
23
27
  * Parseia sizing de um node
28
+ * @param isRoot - indica se é o node raiz do componente
24
29
  */
25
- export declare function parseSizing(node: IFigmaNode): ILayoutProperties;
30
+ export declare function parseSizing(node: IFigmaNode, isRoot?: boolean): ILayoutProperties;
26
31
  /**
27
32
  * Combina layout e sizing
33
+ * @param isRoot - indica se é o node raiz do componente
28
34
  */
29
- export declare function parseLayoutAndSizing(node: IFigmaNode): IStyleProperty[];
35
+ export declare function parseLayoutAndSizing(node: IFigmaNode, isRoot?: boolean): IStyleProperty[];
30
36
  //# sourceMappingURL=layout-parser.d.ts.map
@@ -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;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"}
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,YAAY,CAAC,IAAI,EAAE,UAAU,GAAG,cAAc,GAAG,IAAI,CAuBpE;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,UAAU,GAAG,iBAAiB,CA2F/D;AAwBD;;;GAGG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,UAAU,EAAE,MAAM,GAAE,OAAe,GAAG,iBAAiB,CA+ExF;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,UAAU,EAAE,MAAM,GAAE,OAAe,GAAG,cAAc,EAAE,CAShG"}
@@ -26,6 +26,24 @@ function formatPixels(value) {
26
26
  }
27
27
  return `${rounded}px`;
28
28
  }
29
+ /**
30
+ * Parseia padding de um node (funciona com ou sem Auto Layout)
31
+ */
32
+ export function parsePadding(node) {
33
+ const hasPadding = node.paddingTop ||
34
+ node.paddingRight ||
35
+ node.paddingBottom ||
36
+ node.paddingLeft;
37
+ if (!hasPadding) {
38
+ return null;
39
+ }
40
+ const paddingToken = generatePaddingToken(node.paddingTop ?? 0, node.paddingRight ?? 0, node.paddingBottom ?? 0, node.paddingLeft ?? 0);
41
+ return {
42
+ property: 'padding',
43
+ value: paddingToken,
44
+ token: paddingToken.startsWith('$') ? paddingToken : undefined,
45
+ };
46
+ }
29
47
  /**
30
48
  * Parseia AutoLayout de um node
31
49
  */
@@ -102,11 +120,32 @@ export function parseLayout(node) {
102
120
  }
103
121
  return properties;
104
122
  }
123
+ /**
124
+ * Detecta se o node é uma tela/página (root component que deve ocupar viewport)
125
+ */
126
+ function isScreenNode(node) {
127
+ const name = node.name.toLowerCase().replace(/^#/, '');
128
+ const screenPatterns = /screen|page|view|layout|template|tela|pagina/i;
129
+ // Verifica se é uma tela pelo nome
130
+ if (screenPatterns.test(name)) {
131
+ return true;
132
+ }
133
+ // Verifica se tem dimensões típicas de tela (>= 320px width e >= 568px height)
134
+ const box = node.absoluteBoundingBox;
135
+ if (box && box.width >= 320 && box.height >= 568) {
136
+ // Se é um frame de nível superior com dimensões de tela
137
+ return true;
138
+ }
139
+ return false;
140
+ }
105
141
  /**
106
142
  * Parseia sizing de um node
143
+ * @param isRoot - indica se é o node raiz do componente
107
144
  */
108
- export function parseSizing(node) {
145
+ export function parseSizing(node, isRoot = false) {
109
146
  const properties = {};
147
+ // Se é o node raiz e é uma tela, usa viewport units
148
+ const isScreen = isRoot && isScreenNode(node);
110
149
  // Width
111
150
  if (node.layoutSizingHorizontal) {
112
151
  switch (node.layoutSizingHorizontal) {
@@ -126,17 +165,17 @@ export function parseSizing(node) {
126
165
  if (node.absoluteBoundingBox?.width) {
127
166
  properties.width = {
128
167
  property: 'width',
129
- value: formatPixels(node.absoluteBoundingBox.width),
168
+ value: isScreen ? '100vw' : formatPixels(node.absoluteBoundingBox.width),
130
169
  };
131
170
  }
132
171
  break;
133
172
  }
134
173
  }
135
174
  else if (node.absoluteBoundingBox?.width) {
136
- // Fallback: Se não tem layoutSizing mas tem dimensões, usa o tamanho absoluto
175
+ // Fallback: Se não tem layoutSizing mas tem dimensões
137
176
  properties.width = {
138
177
  property: 'width',
139
- value: formatPixels(node.absoluteBoundingBox.width),
178
+ value: isScreen ? '100vw' : formatPixels(node.absoluteBoundingBox.width),
140
179
  };
141
180
  }
142
181
  // Height
@@ -158,27 +197,35 @@ export function parseSizing(node) {
158
197
  if (node.absoluteBoundingBox?.height) {
159
198
  properties.height = {
160
199
  property: 'height',
161
- value: formatPixels(node.absoluteBoundingBox.height),
200
+ value: isScreen ? '100vh' : formatPixels(node.absoluteBoundingBox.height),
162
201
  };
163
202
  }
164
203
  break;
165
204
  }
166
205
  }
167
206
  else if (node.absoluteBoundingBox?.height) {
168
- // Fallback: Se não tem layoutSizing mas tem dimensões, usa o tamanho absoluto
207
+ // Fallback: Se não tem layoutSizing mas tem dimensões
169
208
  properties.height = {
170
209
  property: 'height',
171
- value: formatPixels(node.absoluteBoundingBox.height),
210
+ value: isScreen ? '100vh' : formatPixels(node.absoluteBoundingBox.height),
211
+ };
212
+ }
213
+ // Para screens, adiciona overflow hidden para evitar scroll
214
+ if (isScreen) {
215
+ properties.flex = {
216
+ property: 'overflow',
217
+ value: 'hidden',
172
218
  };
173
219
  }
174
220
  return properties;
175
221
  }
176
222
  /**
177
223
  * Combina layout e sizing
224
+ * @param isRoot - indica se é o node raiz do componente
178
225
  */
179
- export function parseLayoutAndSizing(node) {
226
+ export function parseLayoutAndSizing(node, isRoot = false) {
180
227
  const layout = parseLayout(node);
181
- const sizing = parseSizing(node);
228
+ const sizing = parseSizing(node, isRoot);
182
229
  const combined = { ...layout, ...sizing };
183
230
  return Object.values(combined).filter((prop) => prop !== undefined);
184
231
  }
@@ -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,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;AAuND;;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,EAOb,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;AAqmBD;;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,11 +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, isComponentNode, } from '@promptui-lib/core';
6
- import { parseLayoutAndSizing, parseSizing } from './layout-parser.js';
6
+ import { parseLayoutAndSizing, parseSizing, parsePadding } 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
+ import { parsePosition, parseContainerPosition, hasAutoLayout } from './position-parser.js';
11
11
  /**
12
12
  * Parseia o nome do frame para extrair informações
13
13
  */
@@ -32,6 +32,204 @@ export function parseFrameName(name) {
32
32
  extractedProps,
33
33
  };
34
34
  }
35
+ /**
36
+ * Verifica se um node deve ser exportado como asset (SVG/imagem)
37
+ * Tipos que são gráficos vetoriais ou ícones devem ser exportados como SVG
38
+ */
39
+ function isAssetNode(node) {
40
+ // Tipos de nodes que são gráficos vetoriais
41
+ const vectorTypes = ['VECTOR', 'BOOLEAN_OPERATION', 'STAR', 'LINE', 'ELLIPSE', 'REGULAR_POLYGON'];
42
+ if (vectorTypes.includes(node.type)) {
43
+ return true;
44
+ }
45
+ // Verifica se o nome indica que é um ícone
46
+ const name = node.name.toLowerCase();
47
+ if (/^(icon|ico|icone|svg)[-_]?/i.test(name) || name.includes('-icon') || name.includes('_icon')) {
48
+ return true;
49
+ }
50
+ // Frames pequenos (< 64x64) com fills complexos podem ser ícones
51
+ if (node.type === 'FRAME' && node.absoluteBoundingBox) {
52
+ const { width, height } = node.absoluteBoundingBox;
53
+ if (width <= 64 && height <= 64) {
54
+ // Verifica se tem apenas elementos vetoriais como filhos
55
+ const hasOnlyVectors = node.children?.every((child) => vectorTypes.includes(child.type) || child.type === 'GROUP');
56
+ if (hasOnlyVectors && node.children && node.children.length > 0) {
57
+ return true;
58
+ }
59
+ }
60
+ }
61
+ return false;
62
+ }
63
+ /**
64
+ * Patterns para detectar elementos de formulário simulados
65
+ */
66
+ const FORM_ELEMENT_PATTERNS = {
67
+ // Input de texto (uma linha)
68
+ input: /^(input|text-?input|entrada|campo-texto|field-input|input-field)$/i,
69
+ // Textarea (múltiplas linhas)
70
+ textarea: /^(textarea|text-?area|area-?texto|multiline|multi-line|mensagem-input|message-input)$/i,
71
+ // Select/Dropdown
72
+ select: /^(select|dropdown|combo-?box|picker|seletor|selecao|lista-selecao|select-input)$/i,
73
+ // Checkbox
74
+ checkbox: /^(checkbox|check-?box|caixa-?selecao|toggle|switch)$/i,
75
+ // Radio button
76
+ radio: /^(radio|radio-?button|opcao-?radio|radio-?input)$/i,
77
+ // Button
78
+ button: /^(button|btn|botao|submit|cancel|action|cta)$/i,
79
+ // Label
80
+ label: /^(label|rotulo|field-?label|input-?label)$/i,
81
+ };
82
+ /**
83
+ * Verifica se um node é um frame válido (FRAME, INSTANCE ou COMPONENT)
84
+ */
85
+ function isValidFrameNode(node) {
86
+ return node.type === 'FRAME' || node.type === 'INSTANCE' || node.type === 'COMPONENT';
87
+ }
88
+ /**
89
+ * Procura por texto nos filhos (pode estar em qualquer nível)
90
+ */
91
+ function hasTextChild(node) {
92
+ if (node.type === 'TEXT')
93
+ return true;
94
+ if (node.children) {
95
+ return node.children.some(child => hasTextChild(child));
96
+ }
97
+ return false;
98
+ }
99
+ /**
100
+ * Extrai o primeiro texto encontrado nos filhos
101
+ */
102
+ function extractFirstText(node) {
103
+ function findFirstText(n) {
104
+ if (n.type === 'TEXT' && n.characters) {
105
+ return n.characters;
106
+ }
107
+ if (n.children) {
108
+ for (const child of n.children) {
109
+ const text = findFirstText(child);
110
+ if (text)
111
+ return text;
112
+ }
113
+ }
114
+ return null;
115
+ }
116
+ return findFirstText(node) || '';
117
+ }
118
+ /**
119
+ * Extrai todos os textos encontrados nos filhos (para opções de select)
120
+ */
121
+ function extractAllTexts(node) {
122
+ const texts = [];
123
+ function findTexts(n) {
124
+ if (n.type === 'TEXT' && n.characters) {
125
+ texts.push(n.characters);
126
+ }
127
+ if (n.children) {
128
+ for (const child of n.children) {
129
+ findTexts(child);
130
+ }
131
+ }
132
+ }
133
+ findTexts(node);
134
+ return texts;
135
+ }
136
+ /**
137
+ * Detecta qual tipo de elemento de formulário um node representa
138
+ */
139
+ function detectSimulatedFormElement(node) {
140
+ if (!isValidFrameNode(node)) {
141
+ return null;
142
+ }
143
+ const name = node.name.toLowerCase().trim();
144
+ // Verifica cada pattern
145
+ if (FORM_ELEMENT_PATTERNS.textarea.test(name)) {
146
+ return 'textarea';
147
+ }
148
+ if (FORM_ELEMENT_PATTERNS.select.test(name)) {
149
+ return 'select';
150
+ }
151
+ if (FORM_ELEMENT_PATTERNS.checkbox.test(name)) {
152
+ return 'checkbox';
153
+ }
154
+ if (FORM_ELEMENT_PATTERNS.radio.test(name)) {
155
+ return 'radio';
156
+ }
157
+ if (FORM_ELEMENT_PATTERNS.button.test(name)) {
158
+ // Só é button se tiver texto dentro
159
+ if (hasTextChild(node)) {
160
+ return 'button';
161
+ }
162
+ }
163
+ if (FORM_ELEMENT_PATTERNS.label.test(name)) {
164
+ // Só é label se tiver texto direto como filho
165
+ if (node.children?.some(child => child.type === 'TEXT')) {
166
+ return 'label';
167
+ }
168
+ }
169
+ if (FORM_ELEMENT_PATTERNS.input.test(name)) {
170
+ // Só é input se tiver texto dentro (placeholder)
171
+ if (hasTextChild(node)) {
172
+ return 'input';
173
+ }
174
+ }
175
+ return null;
176
+ }
177
+ /**
178
+ * Detecta o tipo específico de input baseado no contexto
179
+ */
180
+ function detectInputType(node, parentNode) {
181
+ const nodeName = node.name.toLowerCase();
182
+ const parentName = parentNode?.name.toLowerCase() || '';
183
+ const combinedContext = `${parentName} ${nodeName}`;
184
+ // Detecta por palavras-chave no contexto
185
+ if (combinedContext.includes('senha') || combinedContext.includes('password')) {
186
+ return 'password';
187
+ }
188
+ if (combinedContext.includes('email') || combinedContext.includes('e-mail')) {
189
+ return 'email';
190
+ }
191
+ if (combinedContext.includes('busca') || combinedContext.includes('search') || combinedContext.includes('pesquisa')) {
192
+ return 'search';
193
+ }
194
+ if (combinedContext.includes('telefone') || combinedContext.includes('phone') || combinedContext.includes('tel')) {
195
+ return 'tel';
196
+ }
197
+ if (combinedContext.includes('numero') || combinedContext.includes('number') || combinedContext.includes('quantidade')) {
198
+ return 'number';
199
+ }
200
+ if (combinedContext.includes('url') || combinedContext.includes('website') || combinedContext.includes('site')) {
201
+ return 'url';
202
+ }
203
+ if (combinedContext.includes('data') || combinedContext.includes('date')) {
204
+ return 'date';
205
+ }
206
+ return 'text';
207
+ }
208
+ /**
209
+ * Detecta o tipo de button baseado no contexto
210
+ */
211
+ function detectButtonType(node) {
212
+ const name = node.name.toLowerCase();
213
+ const text = extractFirstText(node).toLowerCase();
214
+ const context = `${name} ${text}`;
215
+ if (context.includes('submit') || context.includes('enviar') || context.includes('entrar') || context.includes('login') || context.includes('cadastrar')) {
216
+ return 'submit';
217
+ }
218
+ if (context.includes('reset') || context.includes('limpar') || context.includes('cancelar')) {
219
+ return 'reset';
220
+ }
221
+ return 'button';
222
+ }
223
+ /**
224
+ * Gera nome seguro para arquivo de asset
225
+ */
226
+ function generateAssetFileName(nodeName, blockName) {
227
+ const cleanName = nodeName
228
+ .toLowerCase()
229
+ .replace(/[^a-z0-9]+/g, '-')
230
+ .replace(/^-+|-+$/g, '');
231
+ return `${blockName}-${cleanName || 'icon'}`;
232
+ }
35
233
  /**
36
234
  * Converte um node em elemento JSX
37
235
  * Se o filho for um componente (marcado com #), retorna IJSXComponent em vez de JSX inline
@@ -47,6 +245,117 @@ function nodeToJSX(node, blockName, depth = 0, parentNode, usedTags) {
47
245
  const className = isRoot
48
246
  ? blockName
49
247
  : generateBEMElement(blockName, elementName);
248
+ // ============================================================
249
+ // DETECÇÃO DE ELEMENTOS DE FORMULÁRIO SIMULADOS DO FIGMA
250
+ // Frames que representam inputs, selects, textareas, etc.
251
+ // ============================================================
252
+ const formElement = detectSimulatedFormElement(node);
253
+ if (formElement) {
254
+ switch (formElement) {
255
+ case 'input': {
256
+ const placeholder = extractFirstText(node);
257
+ const inputType = detectInputType(node, parentNode);
258
+ const inputProps = { type: inputType };
259
+ if (placeholder) {
260
+ inputProps['placeholder'] = placeholder;
261
+ }
262
+ return {
263
+ tag: 'input',
264
+ className,
265
+ props: inputProps,
266
+ children: [],
267
+ selfClosing: true,
268
+ };
269
+ }
270
+ case 'textarea': {
271
+ const placeholder = extractFirstText(node);
272
+ const textareaProps = { rows: '4' };
273
+ if (placeholder) {
274
+ textareaProps['placeholder'] = placeholder;
275
+ }
276
+ return {
277
+ tag: 'textarea',
278
+ className,
279
+ props: textareaProps,
280
+ children: [],
281
+ selfClosing: false,
282
+ };
283
+ }
284
+ case 'select': {
285
+ // Extrai opções do select (textos dentro dos filhos)
286
+ const options = extractAllTexts(node);
287
+ const selectChildren = [];
288
+ // Adiciona placeholder como primeira opção desabilitada
289
+ if (options.length > 0) {
290
+ selectChildren.push({
291
+ tag: 'option',
292
+ className: '',
293
+ props: { value: '', disabled: 'true', selected: 'true' },
294
+ children: [{ type: 'text', value: options[0] }],
295
+ selfClosing: false,
296
+ });
297
+ // Adiciona outras opções
298
+ for (let i = 1; i < options.length; i++) {
299
+ selectChildren.push({
300
+ tag: 'option',
301
+ className: '',
302
+ props: { value: options[i].toLowerCase().replace(/\s+/g, '-') },
303
+ children: [{ type: 'text', value: options[i] }],
304
+ selfClosing: false,
305
+ });
306
+ }
307
+ }
308
+ return {
309
+ tag: 'select',
310
+ className,
311
+ props: {},
312
+ children: selectChildren,
313
+ selfClosing: false,
314
+ };
315
+ }
316
+ case 'checkbox': {
317
+ const labelText = extractFirstText(node);
318
+ return {
319
+ tag: 'input',
320
+ className,
321
+ props: { type: 'checkbox' },
322
+ children: [],
323
+ selfClosing: true,
324
+ };
325
+ }
326
+ case 'radio': {
327
+ return {
328
+ tag: 'input',
329
+ className,
330
+ props: { type: 'radio' },
331
+ children: [],
332
+ selfClosing: true,
333
+ };
334
+ }
335
+ case 'button': {
336
+ const buttonText = extractFirstText(node);
337
+ const buttonType = detectButtonType(node);
338
+ return {
339
+ tag: 'button',
340
+ className,
341
+ props: { type: buttonType },
342
+ children: [{ type: 'text', value: buttonText }],
343
+ selfClosing: false,
344
+ };
345
+ }
346
+ case 'label': {
347
+ const textChild = node.children?.find(child => child.type === 'TEXT');
348
+ const labelText = textChild?.characters || '';
349
+ return {
350
+ tag: 'label',
351
+ className,
352
+ props: {},
353
+ children: [{ type: 'text', value: labelText }],
354
+ selfClosing: false,
355
+ };
356
+ }
357
+ }
358
+ }
50
359
  // Detecta a tag HTML semântica apropriada (passa usedTags para evitar duplicação de <main>)
51
360
  const tag = detectSemanticTag(node, parentNode, depth, tagsSet);
52
361
  // Obtém atributos semânticos (href, type, etc.)
@@ -74,6 +383,22 @@ function nodeToJSX(node, blockName, depth = 0, parentNode, usedTags) {
74
383
  fileName: childFileName,
75
384
  });
76
385
  }
386
+ else if (isAssetNode(child)) {
387
+ // Se é um asset (ícone/vetor), cria referência para exportação
388
+ const childNormalizedName = normalizeName(child.name);
389
+ const childElementName = childNormalizedName.replace(/\s+/g, '-');
390
+ const childClassName = generateBEMElement(blockName, childElementName);
391
+ children.push({
392
+ type: 'asset',
393
+ nodeId: child.id,
394
+ fileName: generateAssetFileName(child.name, blockName),
395
+ format: 'svg',
396
+ className: childClassName,
397
+ alt: childNormalizedName,
398
+ width: child.absoluteBoundingBox?.width,
399
+ height: child.absoluteBoundingBox?.height,
400
+ });
401
+ }
77
402
  else {
78
403
  children.push(nodeToJSX(child, blockName, depth + 1, node, tagsSet));
79
404
  }
@@ -110,12 +435,20 @@ function collectStyles(node, blockName, depth = 0, parentNode) {
110
435
  // Para GROUP: extrai sizing e position: relative (container sem flex)
111
436
  // Para outros tipos (RECTANGLE, etc): extrai apenas sizing (width, height)
112
437
  if (node.type === 'FRAME' || node.type === 'COMPONENT' || node.type === 'INSTANCE') {
113
- properties.push(...parseLayoutAndSizing(node));
438
+ properties.push(...parseLayoutAndSizing(node, isRoot));
114
439
  // Se é um container sem Auto Layout, adiciona position: relative
115
440
  const containerPos = parseContainerPosition(node);
116
441
  if (containerPos) {
117
442
  properties.push(containerPos);
118
443
  }
444
+ // Extrai padding mesmo se não tem Auto Layout (para elementos como inputs)
445
+ // parseLayoutAndSizing já extrai padding quando tem Auto Layout, mas não quando não tem
446
+ if (!hasAutoLayout(node)) {
447
+ const paddingProp = parsePadding(node);
448
+ if (paddingProp) {
449
+ properties.push(paddingProp);
450
+ }
451
+ }
119
452
  }
120
453
  else if (node.type === 'GROUP') {
121
454
  // GROUPs são containers visuais - precisam de position: relative e sizing
@@ -16,6 +16,11 @@ export interface IPositionProperties {
16
16
  * Verifica se um node pai tem Auto Layout
17
17
  */
18
18
  export declare function hasAutoLayout(node: IFigmaNode): boolean;
19
+ /**
20
+ * Verifica se um node está posicionado absolutamente
21
+ * (mesmo dentro de um container com Auto Layout)
22
+ */
23
+ export declare function isAbsolutelyPositioned(node: IFigmaNode): boolean;
19
24
  /**
20
25
  * Parseia constraints e posição de um node
21
26
  */
@@ -1 +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"}
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;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAEhE;AAwBD;;GAEG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,UAAU,EAChB,UAAU,CAAC,EAAE,UAAU,GACtB,cAAc,EAAE,CA6IlB;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAa/D;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,UAAU,GAAG,cAAc,GAAG,IAAI,CAQ9E"}
@@ -25,6 +25,13 @@ function formatPixels(value) {
25
25
  export function hasAutoLayout(node) {
26
26
  return node.layoutMode !== undefined && node.layoutMode !== 'NONE';
27
27
  }
28
+ /**
29
+ * Verifica se um node está posicionado absolutamente
30
+ * (mesmo dentro de um container com Auto Layout)
31
+ */
32
+ export function isAbsolutelyPositioned(node) {
33
+ return node.layoutPositioning === 'ABSOLUTE';
34
+ }
28
35
  /**
29
36
  * Calcula a posição relativa de um filho em relação ao pai
30
37
  */
@@ -46,8 +53,16 @@ function calculateRelativePosition(child, parent) {
46
53
  */
47
54
  export function parsePosition(node, parentNode) {
48
55
  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)) {
56
+ if (!parentNode) {
57
+ return properties;
58
+ }
59
+ // Verifica se o node precisa de posicionamento absoluto:
60
+ // 1. O pai NÃO tem Auto Layout, OU
61
+ // 2. O node tem layoutPositioning: 'ABSOLUTE' (absoluto dentro de Auto Layout)
62
+ const parentHasAutoLayout = hasAutoLayout(parentNode);
63
+ const nodeIsAbsolute = isAbsolutelyPositioned(node);
64
+ if (parentHasAutoLayout && !nodeIsAbsolute) {
65
+ // Pai tem Auto Layout e o node não é absoluto - não precisa de posicionamento
51
66
  return properties;
52
67
  }
53
68
  // Calcula posição relativa
@@ -56,7 +71,7 @@ export function parsePosition(node, parentNode) {
56
71
  return properties;
57
72
  }
58
73
  const constraints = node.constraints;
59
- // Se é um elemento dentro de um container sem Auto Layout, usa posição absoluta
74
+ // Elemento precisa de posição absoluta
60
75
  properties.push({
61
76
  property: 'position',
62
77
  value: 'absolute',
@@ -176,11 +191,16 @@ export function parsePosition(node, parentNode) {
176
191
  * (necessário quando tem filhos com position: absolute)
177
192
  */
178
193
  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) {
194
+ if (!node.children || node.children.length === 0) {
195
+ return false;
196
+ }
197
+ // Se não tem Auto Layout, precisa de relative (todos os filhos serão absolutos)
198
+ if (!hasAutoLayout(node)) {
181
199
  return true;
182
200
  }
183
- return false;
201
+ // Se tem Auto Layout, verifica se algum filho tem layoutPositioning: 'ABSOLUTE'
202
+ const hasAbsoluteChild = node.children.some((child) => isAbsolutelyPositioned(child));
203
+ return hasAbsoluteChild;
184
204
  }
185
205
  /**
186
206
  * Parseia position: relative para containers sem Auto Layout
@@ -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;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,UAAU,EAChB,UAAU,CAAC,EAAE,UAAU,EACvB,MAAM,GAAE,MAAU,EAClB,QAAQ,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,GACrB,MAAM,CAoLR;AA+ID;;;GAGG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,UAAU,EAChB,GAAG,EAAE,MAAM,EACX,cAAc,EAAE,MAAM,GACrB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAgLxB"}
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;AAkRrD;;GAEG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAalD;AA0DD;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,UAAU,EAChB,UAAU,CAAC,EAAE,UAAU,EACvB,MAAM,GAAE,MAAU,EAClB,QAAQ,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,GACrB,MAAM,CA4MR;AA+ID;;;GAGG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,UAAU,EAChB,GAAG,EAAE,MAAM,EACX,cAAc,EAAE,MAAM,GACrB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAgLxB"}
@@ -173,13 +173,14 @@ const NAME_TRANSLATIONS = {
173
173
  };
174
174
  /**
175
175
  * Patterns para detectar tipos de elementos
176
+ * NOTA: Usamos patterns mais flexíveis para capturar variações comuns no Figma
176
177
  */
177
178
  const SEMANTIC_PATTERNS = {
178
179
  // Títulos
179
180
  heading: /^(title|heading|h[1-6]|titulo|cabecalho|header-text|headline)/i,
180
181
  subheading: /^(subtitle|subheading|subtitulo|sub-title)/i,
181
- // Botões
182
- button: /^(btn|button|cta|submit|cancel|action|botao|icon-button)/i,
182
+ // Botões - detecta em qualquer posição do nome
183
+ button: /(^btn$|^button$|^cta$|^submit$|^cancel$|^action$|^botao$|icon-button|-btn$|-button$)/i,
183
184
  // Links
184
185
  link: /(link|anchor|href|ancora|forgot|esqueci|saiba-mais|ver-mais|leia-mais)/i,
185
186
  // Formulários
@@ -191,15 +192,23 @@ const SEMANTIC_PATTERNS = {
191
192
  checkbox: /^(checkbox|check-box|caixa-selecao|toggle)/i,
192
193
  radio: /^(radio|radio-button|opcao-radio)/i,
193
194
  fieldset: /^(fieldset|field-group|grupo-campos)/i,
194
- // Navegação
195
- nav: /^(nav|navigation|navbar|menu|navegacao|breadcrumb|tabs|pagination)/i,
196
- // Seções estruturais
197
- header: /^(header|cabecalho|top-bar|topbar|app-bar)/i,
198
- footer: /^(footer|rodape|bottom-bar|bottom-nav)/i,
199
- main: /^(main|content|principal|conteudo|page-content)/i,
200
- section: /^(section|secao|block|grupo)/i,
201
- article: /^(article|artigo|post|card|blog-post|news)/i,
202
- aside: /^(aside|sidebar|lateral|barra-lateral|complementar)/i,
195
+ // Navegação - expandido para mais variações
196
+ nav: /^(nav|navigation|navbar|menu|navegacao|breadcrumb|tabs|pagination|nav-bar|top-nav|main-nav|side-nav)/i,
197
+ // Seções estruturais - EXPANDIDO para detectar mais variações
198
+ // Header: topo da página, barra superior, cabeçalho
199
+ header: /^(header|cabecalho|top-?bar|topbar|app-?bar|page-?header|site-?header|main-?header)/i,
200
+ // Footer: rodapé, barra inferior
201
+ footer: /^(footer|rodape|bottom-?bar|bottom-?nav|page-?footer|site-?footer|main-?footer)/i,
202
+ // Main: conteúdo principal da página
203
+ main: /^(main|content|principal|conteudo|page-?content|main-?content|body-?content|central|centro)/i,
204
+ // Section: seções genéricas
205
+ section: /^(section|secao|block|grupo|area|zone|region|wrapper|container)$/i,
206
+ // Article: artigos, posts, cards de conteúdo
207
+ article: /^(article|artigo|post|blog-?post|news|noticia|story)/i,
208
+ // Aside: conteúdo lateral, complementar
209
+ aside: /^(aside|sidebar|lateral|barra-?lateral|complementar|side-?panel|side-?content)/i,
210
+ // Container/Wrapper - novo pattern para containers genéricos
211
+ container: /^(container|wrapper|box|caixa|holder|frame|panel|painel|card-?shadow|card-?container)/i,
203
212
  // Imagens e mídia
204
213
  image: /^(image|img|photo|picture|imagem|foto|thumbnail|banner)/i,
205
214
  avatar: /^(avatar|profile-pic|user-image|foto-perfil)/i,
@@ -249,6 +258,47 @@ export function normalizeName(name) {
249
258
  });
250
259
  return translatedWords.join('-');
251
260
  }
261
+ /**
262
+ * Detecta se um node é um elemento estrutural baseado em sua posição e características
263
+ * Isso complementa a detecção por nome quando o designer não nomeia corretamente
264
+ */
265
+ function detectStructuralByPosition(node, parentNode, depth = 0) {
266
+ // Só detecta por posição em níveis próximos do root (depth 1 ou 2)
267
+ if (depth > 2 || !parentNode) {
268
+ return null;
269
+ }
270
+ const siblings = parentNode.children;
271
+ if (!siblings || siblings.length < 2) {
272
+ return null;
273
+ }
274
+ // Encontra índice do node atual entre os irmãos visíveis
275
+ const visibleSiblings = siblings.filter(s => s.visible !== false && !s.name.startsWith('_'));
276
+ const nodeIndex = visibleSiblings.findIndex(s => s.id === node.id);
277
+ if (nodeIndex === -1) {
278
+ return null;
279
+ }
280
+ // Verifica características do node
281
+ const box = node.absoluteBoundingBox;
282
+ const parentBox = parentNode.absoluteBoundingBox;
283
+ if (!box || !parentBox) {
284
+ return null;
285
+ }
286
+ // Altura relativa ao pai
287
+ const heightRatio = box.height / parentBox.height;
288
+ // Primeiro elemento visível com altura pequena (< 15% do pai) = possível header
289
+ if (nodeIndex === 0 && heightRatio < 0.15) {
290
+ return 'header';
291
+ }
292
+ // Último elemento visível com altura pequena (< 10% do pai) = possível footer
293
+ if (nodeIndex === visibleSiblings.length - 1 && heightRatio < 0.10) {
294
+ return 'footer';
295
+ }
296
+ // Elemento do meio com maior altura = possível main
297
+ if (nodeIndex > 0 && nodeIndex < visibleSiblings.length - 1 && heightRatio > 0.5) {
298
+ return 'main';
299
+ }
300
+ return null;
301
+ }
252
302
  /**
253
303
  * Detecta a tag HTML semântica apropriada
254
304
  * @param usedTags - Set de tags já usadas na hierarquia (para evitar duplicação de <main>)
@@ -331,6 +381,12 @@ export function detectSemanticTag(node, parentNode, _depth = 0, usedTags) {
331
381
  if (SEMANTIC_PATTERNS.aside.test(normalizedName)) {
332
382
  return 'aside';
333
383
  }
384
+ // Container - mapeia para div mas com semântica de container
385
+ // Containers são divs que agrupam conteúdo, não têm tag semântica específica
386
+ // mas são importantes para layout
387
+ if (SEMANTIC_PATTERNS.container.test(normalizedName)) {
388
+ return 'div';
389
+ }
334
390
  // Mídia
335
391
  if (SEMANTIC_PATTERNS.figure.test(normalizedName)) {
336
392
  return 'figure';
@@ -393,6 +449,22 @@ export function detectSemanticTag(node, parentNode, _depth = 0, usedTags) {
393
449
  if (SEMANTIC_PATTERNS.alert.test(normalizedName)) {
394
450
  return 'div'; // com role="alert"
395
451
  }
452
+ // Tenta detectar por posição/estrutura se não encontrou por nome
453
+ const structuralTag = detectStructuralByPosition(node, parentNode, _depth);
454
+ if (structuralTag) {
455
+ // Verifica se já existe a tag (evita duplicação)
456
+ if (structuralTag === 'main' && usedTags?.has('main')) {
457
+ return 'section';
458
+ }
459
+ if (structuralTag === 'header' && usedTags?.has('header')) {
460
+ return 'div';
461
+ }
462
+ if (structuralTag === 'footer' && usedTags?.has('footer')) {
463
+ return 'div';
464
+ }
465
+ usedTags?.add(structuralTag);
466
+ return structuralTag;
467
+ }
396
468
  // Default
397
469
  return 'div';
398
470
  }
@@ -32,6 +32,15 @@ export declare function parseBoxShadow(node: IFigmaNode): IStyleProperty | null;
32
32
  * Parseia opacity de um node
33
33
  */
34
34
  export declare function parseOpacity(node: IFigmaNode): IStyleProperty | null;
35
+ /**
36
+ * Parseia overflow de um node (clipsContent)
37
+ */
38
+ export declare function parseOverflow(node: IFigmaNode): IStyleProperty | null;
39
+ /**
40
+ * Parseia rotation/transform de um node
41
+ * A rotação vem em graus e precisa ser convertida para CSS
42
+ */
43
+ export declare function parseTransform(node: IFigmaNode): IStyleProperty | null;
35
44
  /**
36
45
  * Parseia todos os estilos visuais de um node
37
46
  */
@@ -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;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"}
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,aAAa,CAAC,IAAI,EAAE,UAAU,GAAG,cAAc,GAAG,IAAI,CASrE;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,UAAU,GAAG,cAAc,GAAG,IAAI,CA+BtE;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,UAAU,GAAG,cAAc,EAAE,CAyB9D"}
@@ -138,6 +138,50 @@ export function parseOpacity(node) {
138
138
  }
139
139
  return null;
140
140
  }
141
+ /**
142
+ * Parseia overflow de um node (clipsContent)
143
+ */
144
+ export function parseOverflow(node) {
145
+ if (node.clipsContent === true) {
146
+ return {
147
+ property: 'overflow',
148
+ value: 'hidden',
149
+ };
150
+ }
151
+ return null;
152
+ }
153
+ /**
154
+ * Parseia rotation/transform de um node
155
+ * A rotação vem em graus e precisa ser convertida para CSS
156
+ */
157
+ export function parseTransform(node) {
158
+ // Verifica se tem rotação direta
159
+ if (node.rotation !== undefined && node.rotation !== 0) {
160
+ // Figma usa graus, CSS também
161
+ const degrees = roundPixelValue(node.rotation);
162
+ return {
163
+ property: 'transform',
164
+ value: `rotate(${degrees}deg)`,
165
+ };
166
+ }
167
+ // Verifica se tem relativeTransform com rotação
168
+ // A matriz 2D [[a, b, tx], [c, d, ty]] onde a rotação pode ser extraída de a,b,c,d
169
+ if (node.relativeTransform) {
170
+ const [[a, b], [c, d]] = node.relativeTransform;
171
+ // Calcula ângulo de rotação a partir da matriz
172
+ // rotation = atan2(b, a) em radianos
173
+ const radians = Math.atan2(b, a);
174
+ const degrees = radians * (180 / Math.PI);
175
+ // Só aplica se houver rotação significativa (> 0.1 grau)
176
+ if (Math.abs(degrees) > 0.1) {
177
+ return {
178
+ property: 'transform',
179
+ value: `rotate(${roundPixelValue(degrees)}deg)`,
180
+ };
181
+ }
182
+ }
183
+ return null;
184
+ }
141
185
  /**
142
186
  * Parseia todos os estilos visuais de um node
143
187
  */
@@ -158,5 +202,11 @@ export function parseStyles(node) {
158
202
  const opacity = parseOpacity(node);
159
203
  if (opacity)
160
204
  properties.push(opacity);
205
+ const overflow = parseOverflow(node);
206
+ if (overflow)
207
+ properties.push(overflow);
208
+ const transform = parseTransform(node);
209
+ if (transform)
210
+ properties.push(transform);
161
211
  return properties;
162
212
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@promptui-lib/figma-parser",
3
- "version": "0.1.19",
3
+ "version": "0.1.22",
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.19"
33
+ "@promptui-lib/core": "0.1.22"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@types/node": "^20.0.0",