@promptui-lib/figma-parser 0.1.20 → 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,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';
@@ -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;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"}
@@ -60,6 +60,166 @@ function isAssetNode(node) {
60
60
  }
61
61
  return false;
62
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
+ }
63
223
  /**
64
224
  * Gera nome seguro para arquivo de asset
65
225
  */
@@ -85,6 +245,117 @@ function nodeToJSX(node, blockName, depth = 0, parentNode, usedTags) {
85
245
  const className = isRoot
86
246
  ? blockName
87
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
+ }
88
359
  // Detecta a tag HTML semântica apropriada (passa usedTags para evitar duplicação de <main>)
89
360
  const tag = detectSemanticTag(node, parentNode, depth, tagsSet);
90
361
  // Obtém atributos semânticos (href, type, etc.)
@@ -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.20",
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.20"
33
+ "@promptui-lib/core": "0.1.22"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@types/node": "^20.0.0",