@promptui-lib/figma-parser 0.1.20 → 0.1.23

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,6 +1,6 @@
1
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';
@@ -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,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,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,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
+ {"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,5 +1,5 @@
1
1
  export { parseLayout, parseSizing, parseLayoutAndSizing, parsePadding } from './layout-parser.js';
2
- export { parseStyles, parseBackgroundColor, parseBorder, parseBorderRadius, parseBoxShadow } from './style-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
4
  export { parsePosition, parseContainerPosition, hasAutoLayout, isAbsolutelyPositioned } from './position-parser.js';
5
5
  export { parseNode, parseNodes, parseFrameName } from './node-parser.js';
@@ -14,6 +14,7 @@ export interface ILayoutProperties {
14
14
  height?: IStyleProperty;
15
15
  flex?: IStyleProperty;
16
16
  flexWrap?: IStyleProperty;
17
+ flexShrink?: IStyleProperty;
17
18
  }
18
19
  /**
19
20
  * Parseia padding de um node (funciona com ou sem Auto Layout)
@@ -26,11 +27,13 @@ export declare function parseLayout(node: IFigmaNode): ILayoutProperties;
26
27
  /**
27
28
  * Parseia sizing de um node
28
29
  * @param isRoot - indica se é o node raiz do componente
30
+ * @param parentHasAutoLayout - indica se o pai tem Auto Layout (para aplicar flex-shrink)
29
31
  */
30
- export declare function parseSizing(node: IFigmaNode, isRoot?: boolean): ILayoutProperties;
32
+ export declare function parseSizing(node: IFigmaNode, isRoot?: boolean, parentHasAutoLayout?: boolean): ILayoutProperties;
31
33
  /**
32
34
  * Combina layout e sizing
33
35
  * @param isRoot - indica se é o node raiz do componente
36
+ * @param parentHasAutoLayout - indica se o pai tem Auto Layout
34
37
  */
35
- export declare function parseLayoutAndSizing(node: IFigmaNode, isRoot?: boolean): IStyleProperty[];
38
+ export declare function parseLayoutAndSizing(node: IFigmaNode, isRoot?: boolean, parentHasAutoLayout?: boolean): IStyleProperty[];
36
39
  //# 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,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"}
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;IAC1B,UAAU,CAAC,EAAE,cAAc,CAAC;CAC7B;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;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,UAAU,EAAE,MAAM,GAAE,OAAe,EAAE,mBAAmB,GAAE,OAAe,GAAG,iBAAiB,CA+F9H;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,UAAU,EAAE,MAAM,GAAE,OAAe,EAAE,mBAAmB,GAAE,OAAe,GAAG,cAAc,EAAE,CAStI"}
@@ -141,11 +141,17 @@ function isScreenNode(node) {
141
141
  /**
142
142
  * Parseia sizing de um node
143
143
  * @param isRoot - indica se é o node raiz do componente
144
+ * @param parentHasAutoLayout - indica se o pai tem Auto Layout (para aplicar flex-shrink)
144
145
  */
145
- export function parseSizing(node, isRoot = false) {
146
+ export function parseSizing(node, isRoot = false, parentHasAutoLayout = false) {
146
147
  const properties = {};
147
148
  // Se é o node raiz e é uma tela, usa viewport units
148
149
  const isScreen = isRoot && isScreenNode(node);
150
+ // Verifica se precisa de flex-shrink: 0 (quando tem sizing HUG ou FIXED dentro de flex)
151
+ const needsFlexShrink = parentHasAutoLayout && (node.layoutSizingHorizontal === 'HUG' ||
152
+ node.layoutSizingHorizontal === 'FIXED' ||
153
+ node.layoutSizingVertical === 'HUG' ||
154
+ node.layoutSizingVertical === 'FIXED');
149
155
  // Width
150
156
  if (node.layoutSizingHorizontal) {
151
157
  switch (node.layoutSizingHorizontal) {
@@ -217,15 +223,23 @@ export function parseSizing(node, isRoot = false) {
217
223
  value: 'hidden',
218
224
  };
219
225
  }
226
+ // Adiciona flex-shrink: 0 quando necessário
227
+ if (needsFlexShrink) {
228
+ properties.flexShrink = {
229
+ property: 'flex-shrink',
230
+ value: '0',
231
+ };
232
+ }
220
233
  return properties;
221
234
  }
222
235
  /**
223
236
  * Combina layout e sizing
224
237
  * @param isRoot - indica se é o node raiz do componente
238
+ * @param parentHasAutoLayout - indica se o pai tem Auto Layout
225
239
  */
226
- export function parseLayoutAndSizing(node, isRoot = false) {
240
+ export function parseLayoutAndSizing(node, isRoot = false, parentHasAutoLayout = false) {
227
241
  const layout = parseLayout(node);
228
- const sizing = parseSizing(node, isRoot);
242
+ const sizing = parseSizing(node, isRoot, parentHasAutoLayout);
229
243
  const combined = { ...layout, ...sizing };
230
244
  return Object.values(combined).filter((prop) => prop !== undefined);
231
245
  }
@@ -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,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;AA+RD;;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;AAulBD;;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"}
@@ -60,6 +60,145 @@ function isAssetNode(node) {
60
60
  }
61
61
  return false;
62
62
  }
63
+ /**
64
+ * Patterns para detectar elementos de formulário com prefixo explícito $tipo:nome
65
+ * O usuário deve marcar explicitamente os elementos para conversão
66
+ */
67
+ const FORM_ELEMENT_PREFIX_PATTERN = /^\$(input|textarea|select|checkbox|radio|button|label):/i;
68
+ /**
69
+ * Verifica se um node é um frame válido (FRAME, INSTANCE ou COMPONENT)
70
+ */
71
+ function isValidFrameNode(node) {
72
+ return node.type === 'FRAME' || node.type === 'INSTANCE' || node.type === 'COMPONENT';
73
+ }
74
+ /**
75
+ * Procura por texto nos filhos (pode estar em qualquer nível)
76
+ */
77
+ function hasTextChild(node) {
78
+ if (node.type === 'TEXT')
79
+ return true;
80
+ if (node.children) {
81
+ return node.children.some(child => hasTextChild(child));
82
+ }
83
+ return false;
84
+ }
85
+ /**
86
+ * Extrai o primeiro texto encontrado nos filhos
87
+ */
88
+ function extractFirstText(node) {
89
+ function findFirstText(n) {
90
+ if (n.type === 'TEXT' && n.characters) {
91
+ return n.characters;
92
+ }
93
+ if (n.children) {
94
+ for (const child of n.children) {
95
+ const text = findFirstText(child);
96
+ if (text)
97
+ return text;
98
+ }
99
+ }
100
+ return null;
101
+ }
102
+ return findFirstText(node) || '';
103
+ }
104
+ /**
105
+ * Extrai todos os textos encontrados nos filhos (para opções de select)
106
+ */
107
+ function extractAllTexts(node) {
108
+ const texts = [];
109
+ function findTexts(n) {
110
+ if (n.type === 'TEXT' && n.characters) {
111
+ texts.push(n.characters);
112
+ }
113
+ if (n.children) {
114
+ for (const child of n.children) {
115
+ findTexts(child);
116
+ }
117
+ }
118
+ }
119
+ findTexts(node);
120
+ return texts;
121
+ }
122
+ /**
123
+ * Detecta qual tipo de elemento de formulário um node representa
124
+ *
125
+ * IMPORTANTE: A detecção só acontece quando o nome tem prefixo explícito:
126
+ * $input:nome, $textarea:nome, $select:nome, etc.
127
+ *
128
+ * Isso preserva a estrutura visual do Figma por padrão.
129
+ */
130
+ function detectSimulatedFormElement(node) {
131
+ if (!isValidFrameNode(node)) {
132
+ return null;
133
+ }
134
+ const name = node.name.trim();
135
+ // Verifica se tem prefixo explícito $tipo:nome
136
+ const match = name.match(FORM_ELEMENT_PREFIX_PATTERN);
137
+ if (!match) {
138
+ // Sem prefixo explícito = não é um form element
139
+ return null;
140
+ }
141
+ const elementType = match[1].toLowerCase();
142
+ return elementType;
143
+ }
144
+ /**
145
+ * Extrai o nome limpo do node (remove prefixos especiais como $input:, #, etc.)
146
+ */
147
+ function extractCleanName(name) {
148
+ // Remove prefixo de form element $tipo:
149
+ let clean = name.replace(FORM_ELEMENT_PREFIX_PATTERN, '');
150
+ // Remove prefixo de exportação #
151
+ clean = clean.replace(/^#/, '');
152
+ // Remove prefixo interno _
153
+ clean = clean.replace(/^_/, '');
154
+ return clean.trim();
155
+ }
156
+ /**
157
+ * Detecta o tipo específico de input baseado no contexto
158
+ */
159
+ function detectInputType(node, parentNode) {
160
+ const nodeName = extractCleanName(node.name).toLowerCase();
161
+ const parentName = parentNode ? extractCleanName(parentNode.name).toLowerCase() : '';
162
+ const combinedContext = `${parentName} ${nodeName}`;
163
+ // Detecta por palavras-chave no contexto
164
+ if (combinedContext.includes('senha') || combinedContext.includes('password')) {
165
+ return 'password';
166
+ }
167
+ if (combinedContext.includes('email') || combinedContext.includes('e-mail')) {
168
+ return 'email';
169
+ }
170
+ if (combinedContext.includes('busca') || combinedContext.includes('search') || combinedContext.includes('pesquisa')) {
171
+ return 'search';
172
+ }
173
+ if (combinedContext.includes('telefone') || combinedContext.includes('phone') || combinedContext.includes('tel')) {
174
+ return 'tel';
175
+ }
176
+ if (combinedContext.includes('numero') || combinedContext.includes('number') || combinedContext.includes('quantidade')) {
177
+ return 'number';
178
+ }
179
+ if (combinedContext.includes('url') || combinedContext.includes('website') || combinedContext.includes('site')) {
180
+ return 'url';
181
+ }
182
+ if (combinedContext.includes('data') || combinedContext.includes('date')) {
183
+ return 'date';
184
+ }
185
+ return 'text';
186
+ }
187
+ /**
188
+ * Detecta o tipo de button baseado no contexto
189
+ */
190
+ function detectButtonType(node) {
191
+ const name = extractCleanName(node.name).toLowerCase();
192
+ const text = extractFirstText(node).toLowerCase();
193
+ const context = `${name} ${text}`;
194
+ if (context.includes('submit') || context.includes('enviar') || context.includes('entrar') || context.includes('login') || context.includes('cadastrar')) {
195
+ return 'submit';
196
+ }
197
+ if (context.includes('reset') || context.includes('limpar') || context.includes('cancelar')) {
198
+ return 'reset';
199
+ }
200
+ return 'button';
201
+ }
63
202
  /**
64
203
  * Gera nome seguro para arquivo de asset
65
204
  */
@@ -85,6 +224,117 @@ function nodeToJSX(node, blockName, depth = 0, parentNode, usedTags) {
85
224
  const className = isRoot
86
225
  ? blockName
87
226
  : generateBEMElement(blockName, elementName);
227
+ // ============================================================
228
+ // DETECÇÃO DE ELEMENTOS DE FORMULÁRIO SIMULADOS DO FIGMA
229
+ // Frames que representam inputs, selects, textareas, etc.
230
+ // ============================================================
231
+ const formElement = detectSimulatedFormElement(node);
232
+ if (formElement) {
233
+ switch (formElement) {
234
+ case 'input': {
235
+ const placeholder = extractFirstText(node);
236
+ const inputType = detectInputType(node, parentNode);
237
+ const inputProps = { type: inputType };
238
+ if (placeholder) {
239
+ inputProps['placeholder'] = placeholder;
240
+ }
241
+ return {
242
+ tag: 'input',
243
+ className,
244
+ props: inputProps,
245
+ children: [],
246
+ selfClosing: true,
247
+ };
248
+ }
249
+ case 'textarea': {
250
+ const placeholder = extractFirstText(node);
251
+ const textareaProps = { rows: '4' };
252
+ if (placeholder) {
253
+ textareaProps['placeholder'] = placeholder;
254
+ }
255
+ return {
256
+ tag: 'textarea',
257
+ className,
258
+ props: textareaProps,
259
+ children: [],
260
+ selfClosing: false,
261
+ };
262
+ }
263
+ case 'select': {
264
+ // Extrai opções do select (textos dentro dos filhos)
265
+ const options = extractAllTexts(node);
266
+ const selectChildren = [];
267
+ // Adiciona placeholder como primeira opção desabilitada
268
+ if (options.length > 0) {
269
+ selectChildren.push({
270
+ tag: 'option',
271
+ className: '',
272
+ props: { value: '', disabled: 'true', selected: 'true' },
273
+ children: [{ type: 'text', value: options[0] }],
274
+ selfClosing: false,
275
+ });
276
+ // Adiciona outras opções
277
+ for (let i = 1; i < options.length; i++) {
278
+ selectChildren.push({
279
+ tag: 'option',
280
+ className: '',
281
+ props: { value: options[i].toLowerCase().replace(/\s+/g, '-') },
282
+ children: [{ type: 'text', value: options[i] }],
283
+ selfClosing: false,
284
+ });
285
+ }
286
+ }
287
+ return {
288
+ tag: 'select',
289
+ className,
290
+ props: {},
291
+ children: selectChildren,
292
+ selfClosing: false,
293
+ };
294
+ }
295
+ case 'checkbox': {
296
+ const labelText = extractFirstText(node);
297
+ return {
298
+ tag: 'input',
299
+ className,
300
+ props: { type: 'checkbox' },
301
+ children: [],
302
+ selfClosing: true,
303
+ };
304
+ }
305
+ case 'radio': {
306
+ return {
307
+ tag: 'input',
308
+ className,
309
+ props: { type: 'radio' },
310
+ children: [],
311
+ selfClosing: true,
312
+ };
313
+ }
314
+ case 'button': {
315
+ const buttonText = extractFirstText(node);
316
+ const buttonType = detectButtonType(node);
317
+ return {
318
+ tag: 'button',
319
+ className,
320
+ props: { type: buttonType },
321
+ children: [{ type: 'text', value: buttonText }],
322
+ selfClosing: false,
323
+ };
324
+ }
325
+ case 'label': {
326
+ const textChild = node.children?.find(child => child.type === 'TEXT');
327
+ const labelText = textChild?.characters || '';
328
+ return {
329
+ tag: 'label',
330
+ className,
331
+ props: {},
332
+ children: [{ type: 'text', value: labelText }],
333
+ selfClosing: false,
334
+ };
335
+ }
336
+ }
337
+ }
88
338
  // Detecta a tag HTML semântica apropriada (passa usedTags para evitar duplicação de <main>)
89
339
  const tag = detectSemanticTag(node, parentNode, depth, tagsSet);
90
340
  // Obtém atributos semânticos (href, type, etc.)
@@ -159,12 +409,14 @@ function collectStyles(node, blockName, depth = 0, parentNode) {
159
409
  : `.${generateBEMElement(blockName, elementName)}`;
160
410
  // Coleta propriedades deste node
161
411
  const properties = [];
412
+ // Verifica se o pai tem Auto Layout (para aplicar flex-shrink nos filhos)
413
+ const parentHasAutoLayoutMode = parentNode ? hasAutoLayout(parentNode) : false;
162
414
  // Layout e sizing
163
415
  // Para FRAME, COMPONENT, INSTANCE: extrai layout completo (flex, gap, etc)
164
416
  // Para GROUP: extrai sizing e position: relative (container sem flex)
165
417
  // Para outros tipos (RECTANGLE, etc): extrai apenas sizing (width, height)
166
418
  if (node.type === 'FRAME' || node.type === 'COMPONENT' || node.type === 'INSTANCE') {
167
- properties.push(...parseLayoutAndSizing(node, isRoot));
419
+ properties.push(...parseLayoutAndSizing(node, isRoot, parentHasAutoLayoutMode));
168
420
  // Se é um container sem Auto Layout, adiciona position: relative
169
421
  const containerPos = parseContainerPosition(node);
170
422
  if (containerPos) {
@@ -181,7 +433,7 @@ function collectStyles(node, blockName, depth = 0, parentNode) {
181
433
  }
182
434
  else if (node.type === 'GROUP') {
183
435
  // GROUPs são containers visuais - precisam de position: relative e sizing
184
- const sizingProps = parseSizing(node);
436
+ const sizingProps = parseSizing(node, false, parentHasAutoLayoutMode);
185
437
  const sizingArray = Object.values(sizingProps).filter((p) => p !== undefined);
186
438
  properties.push(...sizingArray);
187
439
  // GROUP sempre precisa de position: relative se tiver filhos
@@ -194,7 +446,7 @@ function collectStyles(node, blockName, depth = 0, parentNode) {
194
446
  }
195
447
  else if (node.type !== 'TEXT' && node.absoluteBoundingBox) {
196
448
  // Para elementos não-texto com dimensões, extrai apenas sizing
197
- const sizingProps = parseSizing(node);
449
+ const sizingProps = parseSizing(node, false, parentHasAutoLayoutMode);
198
450
  const sizingArray = Object.values(sizingProps).filter((p) => p !== undefined);
199
451
  properties.push(...sizingArray);
200
452
  }
@@ -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;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"}
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;AA6BD;;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"}
@@ -13,12 +13,20 @@ function roundPixelValue(value) {
13
13
  return Math.round(value * 10) / 10;
14
14
  }
15
15
  /**
16
- * Formata valor de pixel arredondado
16
+ * Formata valor de pixel arredondado (suporta valores negativos)
17
17
  */
18
18
  function formatPixels(value) {
19
19
  const rounded = roundPixelValue(value);
20
20
  return `${rounded}px`;
21
21
  }
22
+ /**
23
+ * Formata valor de pixel para CSS, incluindo valores negativos
24
+ * Valores negativos são importantes para elementos que "vazam" do container
25
+ */
26
+ function formatPixelsWithNegative(value) {
27
+ const rounded = roundPixelValue(value);
28
+ return `${rounded}px`;
29
+ }
22
30
  /**
23
31
  * Verifica se um node pai tem Auto Layout
24
32
  */
@@ -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;AAuRrD;;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;AAgJD;;;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,33 +173,47 @@ 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
187
+ // NOTA: Patterns de form elements são desativados por padrão para preservar a estrutura visual.
188
+ // Use prefixo $input:, $label:, etc. no Figma para ativar a conversão.
189
+ // Os patterns abaixo são mantidos apenas para elementos claramente marcados.
186
190
  form: /^(form|formulario|login-form|signup-form|contact-form|search-form)/i,
187
- input: /^(input|field|textfield|entrada|campo|text-input)/i,
188
- label: /^(label|rotulo|field-label|input-label)/i,
189
- select: /^(select|dropdown|picker|seletor|selecao|combobox)/i,
190
- textarea: /^(textarea|text-area|area-texto|multiline)/i,
191
- checkbox: /^(checkbox|check-box|caixa-selecao|toggle)/i,
192
- radio: /^(radio|radio-button|opcao-radio)/i,
191
+ // input/label/select/etc NÃO são mais detectados automaticamente
192
+ // para evitar converter estruturas visuais em elementos HTML
193
+ input: /^(\$input:|never-match-automatically)/i, // Desativado - use $input:nome
194
+ label: /^(\$label:|never-match-automatically)/i, // Desativado - use $label:nome
195
+ select: /^(\$select:|never-match-automatically)/i, // Desativado - use $select:nome
196
+ textarea: /^(\$textarea:|never-match-automatically)/i, // Desativado - use $textarea:nome
197
+ checkbox: /^(\$checkbox:|never-match-automatically)/i, // Desativado - use $checkbox:nome
198
+ radio: /^(\$radio:|never-match-automatically)/i, // Desativado - use $radio:nome
193
199
  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,
200
+ // Navegação - expandido para mais variações
201
+ nav: /^(nav|navigation|navbar|menu|navegacao|breadcrumb|tabs|pagination|nav-bar|top-nav|main-nav|side-nav)/i,
202
+ // Seções estruturais - EXPANDIDO para detectar mais variações
203
+ // Header: topo da página, barra superior, cabeçalho
204
+ header: /^(header|cabecalho|top-?bar|topbar|app-?bar|page-?header|site-?header|main-?header)/i,
205
+ // Footer: rodapé, barra inferior
206
+ footer: /^(footer|rodape|bottom-?bar|bottom-?nav|page-?footer|site-?footer|main-?footer)/i,
207
+ // Main: conteúdo principal da página
208
+ main: /^(main|content|principal|conteudo|page-?content|main-?content|body-?content|central|centro)/i,
209
+ // Section: seções genéricas
210
+ section: /^(section|secao|block|grupo|area|zone|region|wrapper|container)$/i,
211
+ // Article: artigos, posts, cards de conteúdo
212
+ article: /^(article|artigo|post|blog-?post|news|noticia|story)/i,
213
+ // Aside: conteúdo lateral, complementar
214
+ aside: /^(aside|sidebar|lateral|barra-?lateral|complementar|side-?panel|side-?content)/i,
215
+ // Container/Wrapper - novo pattern para containers genéricos
216
+ container: /^(container|wrapper|box|caixa|holder|frame|panel|painel|card-?shadow|card-?container)/i,
203
217
  // Imagens e mídia
204
218
  image: /^(image|img|photo|picture|imagem|foto|thumbnail|banner)/i,
205
219
  avatar: /^(avatar|profile-pic|user-image|foto-perfil)/i,
@@ -249,6 +263,47 @@ export function normalizeName(name) {
249
263
  });
250
264
  return translatedWords.join('-');
251
265
  }
266
+ /**
267
+ * Detecta se um node é um elemento estrutural baseado em sua posição e características
268
+ * Isso complementa a detecção por nome quando o designer não nomeia corretamente
269
+ */
270
+ function detectStructuralByPosition(node, parentNode, depth = 0) {
271
+ // Só detecta por posição em níveis próximos do root (depth 1 ou 2)
272
+ if (depth > 2 || !parentNode) {
273
+ return null;
274
+ }
275
+ const siblings = parentNode.children;
276
+ if (!siblings || siblings.length < 2) {
277
+ return null;
278
+ }
279
+ // Encontra índice do node atual entre os irmãos visíveis
280
+ const visibleSiblings = siblings.filter(s => s.visible !== false && !s.name.startsWith('_'));
281
+ const nodeIndex = visibleSiblings.findIndex(s => s.id === node.id);
282
+ if (nodeIndex === -1) {
283
+ return null;
284
+ }
285
+ // Verifica características do node
286
+ const box = node.absoluteBoundingBox;
287
+ const parentBox = parentNode.absoluteBoundingBox;
288
+ if (!box || !parentBox) {
289
+ return null;
290
+ }
291
+ // Altura relativa ao pai
292
+ const heightRatio = box.height / parentBox.height;
293
+ // Primeiro elemento visível com altura pequena (< 15% do pai) = possível header
294
+ if (nodeIndex === 0 && heightRatio < 0.15) {
295
+ return 'header';
296
+ }
297
+ // Último elemento visível com altura pequena (< 10% do pai) = possível footer
298
+ if (nodeIndex === visibleSiblings.length - 1 && heightRatio < 0.10) {
299
+ return 'footer';
300
+ }
301
+ // Elemento do meio com maior altura = possível main
302
+ if (nodeIndex > 0 && nodeIndex < visibleSiblings.length - 1 && heightRatio > 0.5) {
303
+ return 'main';
304
+ }
305
+ return null;
306
+ }
252
307
  /**
253
308
  * Detecta a tag HTML semântica apropriada
254
309
  * @param usedTags - Set de tags já usadas na hierarquia (para evitar duplicação de <main>)
@@ -331,6 +386,12 @@ export function detectSemanticTag(node, parentNode, _depth = 0, usedTags) {
331
386
  if (SEMANTIC_PATTERNS.aside.test(normalizedName)) {
332
387
  return 'aside';
333
388
  }
389
+ // Container - mapeia para div mas com semântica de container
390
+ // Containers são divs que agrupam conteúdo, não têm tag semântica específica
391
+ // mas são importantes para layout
392
+ if (SEMANTIC_PATTERNS.container.test(normalizedName)) {
393
+ return 'div';
394
+ }
334
395
  // Mídia
335
396
  if (SEMANTIC_PATTERNS.figure.test(normalizedName)) {
336
397
  return 'figure';
@@ -393,6 +454,22 @@ export function detectSemanticTag(node, parentNode, _depth = 0, usedTags) {
393
454
  if (SEMANTIC_PATTERNS.alert.test(normalizedName)) {
394
455
  return 'div'; // com role="alert"
395
456
  }
457
+ // Tenta detectar por posição/estrutura se não encontrou por nome
458
+ const structuralTag = detectStructuralByPosition(node, parentNode, _depth);
459
+ if (structuralTag) {
460
+ // Verifica se já existe a tag (evita duplicação)
461
+ if (structuralTag === 'main' && usedTags?.has('main')) {
462
+ return 'section';
463
+ }
464
+ if (structuralTag === 'header' && usedTags?.has('header')) {
465
+ return 'div';
466
+ }
467
+ if (structuralTag === 'footer' && usedTags?.has('footer')) {
468
+ return 'div';
469
+ }
470
+ usedTags?.add(structuralTag);
471
+ return structuralTag;
472
+ }
396
473
  // Default
397
474
  return 'div';
398
475
  }
@@ -414,8 +491,9 @@ function detectTextTag(node, parentNode, normalizedName) {
414
491
  if (fontSize >= 24 && fontWeight >= 600) {
415
492
  return getHeadingLevel(fontSize);
416
493
  }
417
- // Labels (geralmente associados a inputs)
418
- if (SEMANTIC_PATTERNS.label.test(name)) {
494
+ // Labels - detecta com prefixo explícito $label:
495
+ // NÃO detecta automaticamente para preservar estrutura visual
496
+ if (name.startsWith('$label:')) {
419
497
  return 'label';
420
498
  }
421
499
  // Links - mas não se o pai já é um link (evita <a> aninhado)
@@ -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.20",
3
+ "version": "0.1.23",
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.20"
33
+ "@promptui-lib/core": "0.1.23"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@types/node": "^20.0.0",