@promptui-lib/figma-parser 0.1.7 → 0.1.9

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.
@@ -6,4 +6,5 @@ export { parseTextStyles, extractTextContent } from './text-parser.js';
6
6
  export type { ITextProperties } from './text-parser.js';
7
7
  export { parseNode, parseNodes, parseFrameName } from './node-parser.js';
8
8
  export type { IParseOptions } from './node-parser.js';
9
+ export { detectSemanticTag, normalizeName, getSemanticAttributes } from './semantic-detector.js';
9
10
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/parser/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AACpF,YAAY,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAE5D,OAAO,EAAE,WAAW,EAAE,oBAAoB,EAAE,WAAW,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACtH,YAAY,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAE1D,OAAO,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AACvE,YAAY,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAExD,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACzE,YAAY,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/parser/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AACpF,YAAY,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAE5D,OAAO,EAAE,WAAW,EAAE,oBAAoB,EAAE,WAAW,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACtH,YAAY,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAE1D,OAAO,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AACvE,YAAY,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAExD,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACzE,YAAY,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEtD,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC"}
@@ -2,3 +2,4 @@ export { parseLayout, parseSizing, parseLayoutAndSizing } from './layout-parser.
2
2
  export { parseStyles, parseBackgroundColor, parseBorder, parseBorderRadius, parseBoxShadow } from './style-parser.js';
3
3
  export { parseTextStyles, extractTextContent } from './text-parser.js';
4
4
  export { parseNode, parseNodes, parseFrameName } from './node-parser.js';
5
+ export { detectSemanticTag, normalizeName, getSemanticAttributes } from './semantic-detector.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,EAKb,cAAc,EACd,gBAAgB,EACjB,MAAM,oBAAoB,CAAC;AAc5B,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;AA4ID;;GAEG;AACH,wBAAgB,SAAS,CACvB,IAAI,EAAE,UAAU,EAChB,OAAO,GAAE,aAAkB,GAC1B,aAAa,CA6Cf;AAED;;GAEG;AACH,wBAAgB,UAAU,CACxB,KAAK,EAAE,UAAU,EAAE,EACnB,OAAO,GAAE,aAAkB,GAC1B,aAAa,EAAE,CAEjB"}
1
+ {"version":3,"file":"node-parser.d.ts","sourceRoot":"","sources":["../../src/parser/node-parser.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EACV,UAAU,EACV,aAAa,EAKb,cAAc,EACd,gBAAgB,EACjB,MAAM,oBAAoB,CAAC;AAe5B,MAAM,WAAW,aAAa;IAC5B,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,gBAAgB,CAwB7D;AAuJD;;GAEG;AACH,wBAAgB,SAAS,CACvB,IAAI,EAAE,UAAU,EAChB,OAAO,GAAE,aAAkB,GAC1B,aAAa,CA6Cf;AAED;;GAEG;AACH,wBAAgB,UAAU,CACxB,KAAK,EAAE,UAAU,EAAE,EACnB,OAAO,GAAE,aAAkB,GAC1B,aAAa,EAAE,CAEjB"}
@@ -6,6 +6,7 @@ import { generateComponentName, generateFileName, generateBEMBlock, generateBEME
6
6
  import { parseLayoutAndSizing } from './layout-parser.js';
7
7
  import { parseStyles } from './style-parser.js';
8
8
  import { parseTextStyles } from './text-parser.js';
9
+ import { detectSemanticTag, normalizeName, getSemanticAttributes } from './semantic-detector.js';
9
10
  /**
10
11
  * Parseia o nome do frame para extrair informações
11
12
  */
@@ -33,17 +34,18 @@ export function parseFrameName(name) {
33
34
  /**
34
35
  * Converte um node em elemento JSX
35
36
  */
36
- function nodeToJSX(node, blockName, depth = 0) {
37
+ function nodeToJSX(node, blockName, depth = 0, parentNode) {
37
38
  const isRoot = depth === 0;
38
- const elementName = isRoot ? '' : node.name.toLowerCase().replace(/\s+/g, '-');
39
+ // Normaliza o nome (traduz PT->EN)
40
+ const normalizedName = normalizeName(node.name);
41
+ const elementName = isRoot ? '' : normalizedName.replace(/\s+/g, '-');
39
42
  const className = isRoot
40
43
  ? blockName
41
44
  : generateBEMElement(blockName, elementName);
42
- // Determina a tag HTML
43
- let tag = 'div';
44
- if (node.type === 'TEXT') {
45
- tag = 'span';
46
- }
45
+ // Detecta a tag HTML semântica apropriada
46
+ const tag = detectSemanticTag(node, parentNode, depth);
47
+ // Obtém atributos semânticos (href, type, etc.)
48
+ const semanticAttrs = getSemanticAttributes(node, tag, normalizedName);
47
49
  // Processa filhos
48
50
  const children = [];
49
51
  if (node.type === 'TEXT' && node.characters) {
@@ -55,15 +57,19 @@ function nodeToJSX(node, blockName, depth = 0) {
55
57
  if (child.visible === false || child.name.startsWith('_')) {
56
58
  continue;
57
59
  }
58
- children.push(nodeToJSX(child, blockName, depth + 1));
60
+ children.push(nodeToJSX(child, blockName, depth + 1, node));
59
61
  }
60
62
  }
63
+ // Tags self-closing
64
+ const selfClosingTags = ['img', 'input', 'br', 'hr'];
65
+ const isSelfClosing = selfClosingTags.includes(tag) ||
66
+ (children.length === 0 && node.type !== 'TEXT');
61
67
  return {
62
68
  tag,
63
69
  className,
64
- props: {},
70
+ props: semanticAttrs,
65
71
  children,
66
- selfClosing: children.length === 0 && node.type !== 'TEXT',
72
+ selfClosing: isSelfClosing,
67
73
  };
68
74
  }
69
75
  /**
@@ -72,7 +78,9 @@ function nodeToJSX(node, blockName, depth = 0) {
72
78
  function collectStyles(node, blockName, depth = 0) {
73
79
  const blocks = [];
74
80
  const isRoot = depth === 0;
75
- const elementName = isRoot ? '' : node.name.toLowerCase().replace(/\s+/g, '-');
81
+ // Usa nome normalizado (traduzido)
82
+ const normalizedName = normalizeName(node.name);
83
+ const elementName = isRoot ? '' : normalizedName.replace(/\s+/g, '-');
76
84
  const selector = isRoot
77
85
  ? `.${blockName}`
78
86
  : `.${generateBEMElement(blockName, elementName)}`;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Semantic Detector
3
+ * Detecta a tag HTML5 semântica apropriada baseado no contexto do Figma
4
+ * Referência: https://developer.mozilla.org/pt-BR/docs/Web/HTML/Element
5
+ */
6
+ import type { IFigmaNode } from '@promptui-lib/core';
7
+ /**
8
+ * Normaliza um nome traduzindo termos comuns
9
+ */
10
+ export declare function normalizeName(name: string): string;
11
+ /**
12
+ * Detecta a tag HTML semântica apropriada
13
+ */
14
+ export declare function detectSemanticTag(node: IFigmaNode, parentNode?: IFigmaNode, depth?: number): string;
15
+ /**
16
+ * Gera atributos adicionais baseado no contexto
17
+ * Segue boas práticas de SEO e acessibilidade
18
+ */
19
+ export declare function getSemanticAttributes(node: IFigmaNode, tag: string, normalizedName: string): Record<string, string>;
20
+ //# sourceMappingURL=semantic-detector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"semantic-detector.d.ts","sourceRoot":"","sources":["../../src/parser/semantic-detector.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAmQrD;;GAEG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAalD;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,UAAU,EAChB,UAAU,CAAC,EAAE,UAAU,EACvB,KAAK,GAAE,MAAU,GAChB,MAAM,CAmKR;AA4JD;;;GAGG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,UAAU,EAChB,GAAG,EAAE,MAAM,EACX,cAAc,EAAE,MAAM,GACrB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAkKxB"}
@@ -0,0 +1,669 @@
1
+ /**
2
+ * Semantic Detector
3
+ * Detecta a tag HTML5 semântica apropriada baseado no contexto do Figma
4
+ * Referência: https://developer.mozilla.org/pt-BR/docs/Web/HTML/Element
5
+ */
6
+ /**
7
+ * Mapeamento de nomes comuns PT -> EN
8
+ */
9
+ const NAME_TRANSLATIONS = {
10
+ // Navegação
11
+ 'navegacao': 'navigation',
12
+ 'menu': 'menu',
13
+ 'nav': 'nav',
14
+ 'breadcrumb': 'breadcrumb',
15
+ 'migalhas': 'breadcrumb',
16
+ 'abas': 'tabs',
17
+ 'guias': 'tabs',
18
+ // Botões
19
+ 'botao': 'button',
20
+ 'btn': 'button',
21
+ 'enviar': 'submit',
22
+ 'cancelar': 'cancel',
23
+ 'entrar': 'login',
24
+ 'sair': 'logout',
25
+ 'cadastrar': 'register',
26
+ 'salvar': 'save',
27
+ 'excluir': 'delete',
28
+ 'editar': 'edit',
29
+ 'fechar': 'close',
30
+ 'voltar': 'back',
31
+ 'proximo': 'next',
32
+ 'anterior': 'previous',
33
+ 'adicionar': 'add',
34
+ 'remover': 'remove',
35
+ 'copiar': 'copy',
36
+ 'colar': 'paste',
37
+ 'baixar': 'download',
38
+ 'enviar-arquivo': 'upload',
39
+ // Formulários
40
+ 'formulario': 'form',
41
+ 'campo': 'field',
42
+ 'entrada': 'input',
43
+ 'senha': 'password',
44
+ 'email': 'email',
45
+ 'nome': 'name',
46
+ 'telefone': 'phone',
47
+ 'endereco': 'address',
48
+ 'busca': 'search',
49
+ 'pesquisa': 'search',
50
+ 'filtro': 'filter',
51
+ 'selecao': 'select',
52
+ 'seletor': 'select',
53
+ 'dropdown': 'select',
54
+ 'checkbox': 'checkbox',
55
+ 'caixa-selecao': 'checkbox',
56
+ 'radio': 'radio',
57
+ 'opcao': 'option',
58
+ 'textarea': 'textarea',
59
+ 'area-texto': 'textarea',
60
+ // Labels e textos
61
+ 'rotulo': 'label',
62
+ 'legenda': 'caption',
63
+ 'descricao': 'description',
64
+ 'dica': 'hint',
65
+ 'erro': 'error',
66
+ 'sucesso': 'success',
67
+ 'aviso': 'warning',
68
+ 'ajuda': 'help',
69
+ 'info': 'info',
70
+ 'obrigatorio': 'required',
71
+ // Títulos
72
+ 'titulo': 'title',
73
+ 'subtitulo': 'subtitle',
74
+ 'cabecalho': 'header',
75
+ 'rodape': 'footer',
76
+ 'manchete': 'headline',
77
+ // Links
78
+ 'link': 'link',
79
+ 'ancora': 'anchor',
80
+ 'esqueci': 'forgot',
81
+ 'esqueci-minha-senha': 'forgot-password',
82
+ 'esqueceu-senha': 'forgot-password',
83
+ 'saiba-mais': 'learn-more',
84
+ 'ver-mais': 'see-more',
85
+ 'leia-mais': 'read-more',
86
+ // Seções
87
+ 'secao': 'section',
88
+ 'artigo': 'article',
89
+ 'conteudo': 'content',
90
+ 'principal': 'main',
91
+ 'lateral': 'sidebar',
92
+ 'barra-lateral': 'sidebar',
93
+ 'complementar': 'aside',
94
+ // Cards e containers
95
+ 'cartao': 'card',
96
+ 'caixa': 'box',
97
+ 'container': 'container',
98
+ 'painel': 'panel',
99
+ 'modal': 'dialog',
100
+ 'dialogo': 'dialog',
101
+ 'popup': 'dialog',
102
+ 'gaveta': 'drawer',
103
+ 'acordeao': 'accordion',
104
+ 'expansivel': 'expandable',
105
+ 'colapsavel': 'collapsible',
106
+ // Imagens e mídia
107
+ 'imagem': 'image',
108
+ 'foto': 'photo',
109
+ 'icone': 'icon',
110
+ 'logo': 'logo',
111
+ 'avatar': 'avatar',
112
+ 'miniatura': 'thumbnail',
113
+ 'banner': 'banner',
114
+ 'video': 'video',
115
+ 'audio': 'audio',
116
+ 'player': 'player',
117
+ 'galeria': 'gallery',
118
+ 'carrossel': 'carousel',
119
+ 'slider': 'slider',
120
+ // Listas
121
+ 'lista': 'list',
122
+ 'item': 'item',
123
+ 'grade': 'grid',
124
+ 'tabela': 'table',
125
+ 'linha': 'row',
126
+ 'coluna': 'column',
127
+ 'celula': 'cell',
128
+ // Feedback
129
+ 'texto': 'text',
130
+ 'paragrafo': 'paragraph',
131
+ 'mensagem': 'message',
132
+ 'notificacao': 'notification',
133
+ 'alerta': 'alert',
134
+ 'toast': 'toast',
135
+ 'snackbar': 'snackbar',
136
+ 'carregando': 'loading',
137
+ 'progresso': 'progress',
138
+ 'spinner': 'spinner',
139
+ 'esqueleto': 'skeleton',
140
+ // Estados
141
+ 'placeholder': 'placeholder',
142
+ 'vazio': 'empty',
143
+ 'erro-estado': 'error-state',
144
+ 'desabilitado': 'disabled',
145
+ 'ativo': 'active',
146
+ 'selecionado': 'selected',
147
+ 'focado': 'focused',
148
+ // Outros
149
+ 'bem-vindo': 'welcome',
150
+ 'bemvindo': 'welcome',
151
+ 'acesse': 'access',
152
+ 'sua-conta': 'your-account',
153
+ 'minha-conta': 'my-account',
154
+ 'perfil': 'profile',
155
+ 'configuracoes': 'settings',
156
+ 'preferencias': 'preferences',
157
+ 'separador': 'divider',
158
+ 'divisor': 'divider',
159
+ 'espacador': 'spacer',
160
+ 'badge': 'badge',
161
+ 'etiqueta': 'tag',
162
+ 'chip': 'chip',
163
+ 'tooltip': 'tooltip',
164
+ 'dica-ferramenta': 'tooltip',
165
+ 'data': 'date',
166
+ 'hora': 'time',
167
+ 'preco': 'price',
168
+ 'valor': 'value',
169
+ 'quantidade': 'quantity',
170
+ 'total': 'total',
171
+ 'resumo': 'summary',
172
+ 'detalhes': 'details',
173
+ };
174
+ /**
175
+ * Patterns para detectar tipos de elementos
176
+ */
177
+ const SEMANTIC_PATTERNS = {
178
+ // Títulos
179
+ heading: /^(title|heading|h[1-6]|titulo|cabecalho|header-text|headline)/i,
180
+ subheading: /^(subtitle|subheading|subtitulo|sub-title)/i,
181
+ // Botões
182
+ button: /^(btn|button|cta|submit|cancel|action|botao|icon-button)/i,
183
+ // Links
184
+ link: /(link|anchor|href|ancora|forgot|esqueci|saiba-mais|ver-mais|leia-mais)/i,
185
+ // Formulários
186
+ 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,
193
+ 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,
203
+ // Imagens e mídia
204
+ image: /^(image|img|photo|picture|imagem|foto|thumbnail|banner)/i,
205
+ avatar: /^(avatar|profile-pic|user-image|foto-perfil)/i,
206
+ icon: /^(icon|icone|ico)/i,
207
+ figure: /^(figure|figura|media-container)/i,
208
+ video: /^(video|player|media)/i,
209
+ audio: /^(audio|sound|som)/i,
210
+ // Listas
211
+ list: /^(list|ul|ol|menu-items|lista|items)/i,
212
+ listItem: /^(item|li|list-item|menu-item|option)/i,
213
+ // Tabelas
214
+ table: /^(table|tabela|data-table|grid-table)/i,
215
+ tableHead: /^(thead|table-head|cabecalho-tabela)/i,
216
+ tableBody: /^(tbody|table-body|corpo-tabela)/i,
217
+ tableRow: /^(tr|row|linha|table-row)/i,
218
+ tableCell: /^(td|cell|celula|table-cell)/i,
219
+ tableHeader: /^(th|header-cell|celula-cabecalho)/i,
220
+ // Texto
221
+ paragraph: /^(paragraph|text|body|description|paragrafo|descricao|copy)/i,
222
+ blockquote: /^(quote|blockquote|citacao|testimonial)/i,
223
+ code: /^(code|codigo|snippet|pre)/i,
224
+ time: /^(time|date|data|hora|datetime|timestamp)/i,
225
+ address: /^(address|endereco|contact-info)/i,
226
+ // Interativos
227
+ dialog: /^(dialog|modal|popup|overlay|dialogo)/i,
228
+ details: /^(details|accordion|expandable|collapsible|detalhes)/i,
229
+ summary: /^(summary|accordion-header|resumo)/i,
230
+ progress: /^(progress|loading|progresso|carregando|loader)/i,
231
+ meter: /^(meter|gauge|medidor)/i,
232
+ // Outros
233
+ divider: /^(divider|separator|hr|divisor|separador)/i,
234
+ badge: /^(badge|tag|chip|etiqueta|label-badge)/i,
235
+ tooltip: /^(tooltip|hint|dica-ferramenta)/i,
236
+ alert: /^(alert|notification|toast|snackbar|alerta|notificacao)/i,
237
+ };
238
+ /**
239
+ * Normaliza um nome traduzindo termos comuns
240
+ */
241
+ export function normalizeName(name) {
242
+ // Remove prefixos especiais
243
+ let normalized = name.replace(/^[#_]/, '').toLowerCase();
244
+ // Divide em palavras
245
+ const words = normalized.split(/[-_\s]+/);
246
+ // Traduz cada palavra
247
+ const translatedWords = words.map(word => {
248
+ return NAME_TRANSLATIONS[word] || word;
249
+ });
250
+ return translatedWords.join('-');
251
+ }
252
+ /**
253
+ * Detecta a tag HTML semântica apropriada
254
+ */
255
+ export function detectSemanticTag(node, parentNode, depth = 0) {
256
+ const name = node.name.toLowerCase();
257
+ const normalizedName = normalizeName(name);
258
+ // Texto sempre usa tag apropriada baseada no contexto
259
+ if (node.type === 'TEXT') {
260
+ return detectTextTag(node, parentNode, normalizedName);
261
+ }
262
+ // Verifica patterns para outros tipos (ordem importa!)
263
+ // Botões
264
+ if (SEMANTIC_PATTERNS.button.test(normalizedName)) {
265
+ return 'button';
266
+ }
267
+ // Links
268
+ if (SEMANTIC_PATTERNS.link.test(normalizedName)) {
269
+ return 'a';
270
+ }
271
+ // Formulários
272
+ if (SEMANTIC_PATTERNS.form.test(normalizedName)) {
273
+ return 'form';
274
+ }
275
+ if (SEMANTIC_PATTERNS.fieldset.test(normalizedName)) {
276
+ return 'fieldset';
277
+ }
278
+ if (SEMANTIC_PATTERNS.select.test(normalizedName)) {
279
+ return 'select';
280
+ }
281
+ if (SEMANTIC_PATTERNS.textarea.test(normalizedName)) {
282
+ return 'textarea';
283
+ }
284
+ if (SEMANTIC_PATTERNS.checkbox.test(normalizedName)) {
285
+ return 'input'; // type será checkbox
286
+ }
287
+ if (SEMANTIC_PATTERNS.radio.test(normalizedName)) {
288
+ return 'input'; // type será radio
289
+ }
290
+ if (SEMANTIC_PATTERNS.input.test(normalizedName)) {
291
+ return hasInputChild(node) ? 'div' : 'input';
292
+ }
293
+ // Navegação
294
+ if (SEMANTIC_PATTERNS.nav.test(normalizedName)) {
295
+ return 'nav';
296
+ }
297
+ // Seções estruturais
298
+ if (SEMANTIC_PATTERNS.header.test(normalizedName)) {
299
+ return depth === 0 ? 'header' : 'header';
300
+ }
301
+ if (SEMANTIC_PATTERNS.footer.test(normalizedName)) {
302
+ return 'footer';
303
+ }
304
+ if (SEMANTIC_PATTERNS.main.test(normalizedName)) {
305
+ return 'main';
306
+ }
307
+ if (SEMANTIC_PATTERNS.section.test(normalizedName)) {
308
+ return 'section';
309
+ }
310
+ if (SEMANTIC_PATTERNS.article.test(normalizedName)) {
311
+ return 'article';
312
+ }
313
+ if (SEMANTIC_PATTERNS.aside.test(normalizedName)) {
314
+ return 'aside';
315
+ }
316
+ // Mídia
317
+ if (SEMANTIC_PATTERNS.figure.test(normalizedName)) {
318
+ return 'figure';
319
+ }
320
+ if (SEMANTIC_PATTERNS.video.test(normalizedName)) {
321
+ return 'video';
322
+ }
323
+ if (SEMANTIC_PATTERNS.audio.test(normalizedName)) {
324
+ return 'audio';
325
+ }
326
+ if (SEMANTIC_PATTERNS.avatar.test(normalizedName) || SEMANTIC_PATTERNS.image.test(normalizedName)) {
327
+ return 'img';
328
+ }
329
+ if (SEMANTIC_PATTERNS.icon.test(normalizedName)) {
330
+ return 'span'; // Icons geralmente são spans com classes ou SVG inline
331
+ }
332
+ // Tabelas
333
+ if (SEMANTIC_PATTERNS.table.test(normalizedName)) {
334
+ return 'table';
335
+ }
336
+ if (SEMANTIC_PATTERNS.tableHead.test(normalizedName)) {
337
+ return 'thead';
338
+ }
339
+ if (SEMANTIC_PATTERNS.tableBody.test(normalizedName)) {
340
+ return 'tbody';
341
+ }
342
+ if (SEMANTIC_PATTERNS.tableRow.test(normalizedName)) {
343
+ return 'tr';
344
+ }
345
+ if (SEMANTIC_PATTERNS.tableHeader.test(normalizedName)) {
346
+ return 'th';
347
+ }
348
+ if (SEMANTIC_PATTERNS.tableCell.test(normalizedName)) {
349
+ return 'td';
350
+ }
351
+ // Listas
352
+ if (SEMANTIC_PATTERNS.list.test(normalizedName)) {
353
+ return 'ul';
354
+ }
355
+ if (SEMANTIC_PATTERNS.listItem.test(normalizedName)) {
356
+ return 'li';
357
+ }
358
+ // Interativos
359
+ if (SEMANTIC_PATTERNS.dialog.test(normalizedName)) {
360
+ return 'dialog';
361
+ }
362
+ if (SEMANTIC_PATTERNS.details.test(normalizedName)) {
363
+ return 'details';
364
+ }
365
+ if (SEMANTIC_PATTERNS.summary.test(normalizedName)) {
366
+ return 'summary';
367
+ }
368
+ if (SEMANTIC_PATTERNS.progress.test(normalizedName)) {
369
+ return 'div'; // progress element é muito específico
370
+ }
371
+ // Outros
372
+ if (SEMANTIC_PATTERNS.divider.test(normalizedName)) {
373
+ return 'hr';
374
+ }
375
+ if (SEMANTIC_PATTERNS.alert.test(normalizedName)) {
376
+ return 'div'; // com role="alert"
377
+ }
378
+ // Default
379
+ return 'div';
380
+ }
381
+ /**
382
+ * Detecta a tag apropriada para elementos de texto
383
+ */
384
+ function detectTextTag(node, parentNode, normalizedName) {
385
+ const name = normalizedName || normalizeName(node.name);
386
+ const fontSize = node.style?.fontSize ?? 16;
387
+ const fontWeight = node.style?.fontWeight ?? 400;
388
+ // Títulos - detecta por nome ou por tamanho de fonte
389
+ if (SEMANTIC_PATTERNS.heading.test(name)) {
390
+ return getHeadingLevel(fontSize);
391
+ }
392
+ if (SEMANTIC_PATTERNS.subheading.test(name)) {
393
+ return 'h2';
394
+ }
395
+ // Se fonte é grande e bold, provavelmente é heading
396
+ if (fontSize >= 24 && fontWeight >= 600) {
397
+ return getHeadingLevel(fontSize);
398
+ }
399
+ // Labels (geralmente associados a inputs)
400
+ if (SEMANTIC_PATTERNS.label.test(name)) {
401
+ return 'label';
402
+ }
403
+ // Links - mas não se o pai já é um link (evita <a> aninhado)
404
+ if (SEMANTIC_PATTERNS.link.test(name)) {
405
+ // Verifica se o pai já é um link
406
+ if (parentNode) {
407
+ const parentName = normalizeName(parentNode.name);
408
+ if (SEMANTIC_PATTERNS.link.test(parentName)) {
409
+ return 'span'; // Texto dentro de link vira span
410
+ }
411
+ }
412
+ return 'a';
413
+ }
414
+ // Citações
415
+ if (SEMANTIC_PATTERNS.blockquote.test(name)) {
416
+ return 'blockquote';
417
+ }
418
+ // Código
419
+ if (SEMANTIC_PATTERNS.code.test(name)) {
420
+ return 'code';
421
+ }
422
+ // Data/hora
423
+ if (SEMANTIC_PATTERNS.time.test(name)) {
424
+ return 'time';
425
+ }
426
+ // Endereço
427
+ if (SEMANTIC_PATTERNS.address.test(name)) {
428
+ return 'address';
429
+ }
430
+ // Verifica contexto do pai
431
+ if (parentNode) {
432
+ const parentName = normalizeName(parentNode.name);
433
+ // Se está dentro de um botão
434
+ if (SEMANTIC_PATTERNS.button.test(parentName)) {
435
+ return 'span';
436
+ }
437
+ // Se está dentro de um link
438
+ if (SEMANTIC_PATTERNS.link.test(parentName)) {
439
+ return 'span';
440
+ }
441
+ // Se está dentro de um label
442
+ if (SEMANTIC_PATTERNS.label.test(parentName)) {
443
+ return 'span';
444
+ }
445
+ // Se está dentro de um heading
446
+ if (SEMANTIC_PATTERNS.heading.test(parentName)) {
447
+ return 'span';
448
+ }
449
+ // Se pai é um item de lista
450
+ if (SEMANTIC_PATTERNS.listItem.test(parentName)) {
451
+ return 'span';
452
+ }
453
+ // Se pai é uma célula de tabela
454
+ if (SEMANTIC_PATTERNS.tableCell.test(parentName) || SEMANTIC_PATTERNS.tableHeader.test(parentName)) {
455
+ return 'span';
456
+ }
457
+ // Se pai é um fieldset, pode ser a legend
458
+ if (SEMANTIC_PATTERNS.fieldset.test(parentName)) {
459
+ return 'legend';
460
+ }
461
+ // Se pai é figure, pode ser figcaption
462
+ if (SEMANTIC_PATTERNS.figure.test(parentName)) {
463
+ return 'figcaption';
464
+ }
465
+ }
466
+ // Parágrafos e descrições
467
+ if (SEMANTIC_PATTERNS.paragraph.test(name)) {
468
+ return 'p';
469
+ }
470
+ // Texto genérico - decide baseado no tamanho
471
+ const textLength = node.characters?.length ?? 0;
472
+ // Texto longo (>100 chars) -> parágrafo
473
+ if (textLength > 100) {
474
+ return 'p';
475
+ }
476
+ // Texto médio com quebras de linha -> parágrafo
477
+ if (node.characters?.includes('\n')) {
478
+ return 'p';
479
+ }
480
+ return 'span';
481
+ }
482
+ /**
483
+ * Determina o nível do heading baseado no tamanho da fonte
484
+ */
485
+ function getHeadingLevel(fontSize) {
486
+ if (fontSize >= 36)
487
+ return 'h1';
488
+ if (fontSize >= 30)
489
+ return 'h2';
490
+ if (fontSize >= 24)
491
+ return 'h3';
492
+ if (fontSize >= 20)
493
+ return 'h4';
494
+ if (fontSize >= 18)
495
+ return 'h5';
496
+ return 'h6';
497
+ }
498
+ /**
499
+ * Verifica se um node contém um input como filho
500
+ */
501
+ function hasInputChild(node) {
502
+ if (!node.children)
503
+ return false;
504
+ return node.children.some(child => {
505
+ const childName = normalizeName(child.name);
506
+ return SEMANTIC_PATTERNS.input.test(childName) ||
507
+ child.name.toLowerCase().includes('input');
508
+ });
509
+ }
510
+ /**
511
+ * Gera atributos adicionais baseado no contexto
512
+ * Segue boas práticas de SEO e acessibilidade
513
+ */
514
+ export function getSemanticAttributes(node, tag, normalizedName) {
515
+ const attrs = {};
516
+ // Links - não adiciona href automático para evitar warnings de a11y
517
+ // O desenvolvedor deve preencher o href correto
518
+ // if (tag === 'a') {
519
+ // attrs['href'] = '#'; // Removido - causa warning jsx-a11y/anchor-is-valid
520
+ // }
521
+ // Inputs
522
+ if (tag === 'input') {
523
+ // Detecta tipo de input
524
+ if (normalizedName.includes('email')) {
525
+ attrs['type'] = 'email';
526
+ attrs['placeholder'] = 'email@example.com';
527
+ }
528
+ else if (normalizedName.includes('password') || normalizedName.includes('senha')) {
529
+ attrs['type'] = 'password';
530
+ attrs['placeholder'] = '********';
531
+ }
532
+ else if (normalizedName.includes('search') || normalizedName.includes('busca')) {
533
+ attrs['type'] = 'search';
534
+ attrs['placeholder'] = 'Search...';
535
+ }
536
+ else if (normalizedName.includes('phone') || normalizedName.includes('telefone') || normalizedName.includes('tel')) {
537
+ attrs['type'] = 'tel';
538
+ attrs['placeholder'] = '+1 (555) 000-0000';
539
+ }
540
+ else if (normalizedName.includes('number') || normalizedName.includes('numero') || normalizedName.includes('quantidade')) {
541
+ attrs['type'] = 'number';
542
+ }
543
+ else if (normalizedName.includes('date') || normalizedName.includes('data')) {
544
+ attrs['type'] = 'date';
545
+ }
546
+ else if (normalizedName.includes('time') || normalizedName.includes('hora')) {
547
+ attrs['type'] = 'time';
548
+ }
549
+ else if (normalizedName.includes('checkbox')) {
550
+ attrs['type'] = 'checkbox';
551
+ }
552
+ else if (normalizedName.includes('radio')) {
553
+ attrs['type'] = 'radio';
554
+ }
555
+ else if (normalizedName.includes('file') || normalizedName.includes('arquivo') || normalizedName.includes('upload')) {
556
+ attrs['type'] = 'file';
557
+ }
558
+ else if (normalizedName.includes('url') || normalizedName.includes('website') || normalizedName.includes('site')) {
559
+ attrs['type'] = 'url';
560
+ attrs['placeholder'] = 'https://';
561
+ }
562
+ else {
563
+ attrs['type'] = 'text';
564
+ }
565
+ }
566
+ // Textarea
567
+ if (tag === 'textarea') {
568
+ attrs['rows'] = '4';
569
+ }
570
+ // Select
571
+ if (tag === 'select') {
572
+ // Será preenchido com options
573
+ }
574
+ // Botões
575
+ if (tag === 'button') {
576
+ if (normalizedName.includes('submit') || normalizedName.includes('enviar') || normalizedName.includes('login')) {
577
+ attrs['type'] = 'submit';
578
+ }
579
+ else if (normalizedName.includes('reset') || normalizedName.includes('limpar')) {
580
+ attrs['type'] = 'reset';
581
+ }
582
+ else {
583
+ attrs['type'] = 'button';
584
+ }
585
+ }
586
+ // Imagens
587
+ if (tag === 'img') {
588
+ attrs['src'] = '';
589
+ attrs['alt'] = normalizedName.replace(/-/g, ' ');
590
+ attrs['loading'] = 'lazy';
591
+ }
592
+ // Vídeo
593
+ if (tag === 'video') {
594
+ attrs['controls'] = '';
595
+ }
596
+ // Áudio
597
+ if (tag === 'audio') {
598
+ attrs['controls'] = '';
599
+ }
600
+ // Dialog
601
+ if (tag === 'dialog') {
602
+ // dialog não precisa de atributos especiais por padrão
603
+ }
604
+ // Time
605
+ if (tag === 'time') {
606
+ attrs['datetime'] = '';
607
+ }
608
+ // Progress
609
+ if (tag === 'progress') {
610
+ attrs['value'] = '0';
611
+ attrs['max'] = '100';
612
+ }
613
+ // Meter
614
+ if (tag === 'meter') {
615
+ attrs['value'] = '0';
616
+ attrs['min'] = '0';
617
+ attrs['max'] = '100';
618
+ }
619
+ // Divider/hr não precisa de atributos
620
+ // =====================================================
621
+ // ARIA e Acessibilidade (a11y)
622
+ // =====================================================
623
+ // Alert role para notificações
624
+ if (SEMANTIC_PATTERNS.alert.test(normalizedName)) {
625
+ attrs['role'] = 'alert';
626
+ attrs['aria-live'] = 'polite';
627
+ }
628
+ // Navegação
629
+ if (tag === 'nav') {
630
+ // Pode adicionar aria-label se necessário
631
+ if (normalizedName.includes('main') || normalizedName.includes('principal')) {
632
+ attrs['aria-label'] = 'Main navigation';
633
+ }
634
+ else if (normalizedName.includes('breadcrumb')) {
635
+ attrs['aria-label'] = 'Breadcrumb';
636
+ }
637
+ else if (normalizedName.includes('footer')) {
638
+ attrs['aria-label'] = 'Footer navigation';
639
+ }
640
+ }
641
+ // Dialog/Modal
642
+ if (tag === 'dialog') {
643
+ attrs['aria-modal'] = 'true';
644
+ attrs['aria-labelledby'] = ''; // Deve ser preenchido com ID do título
645
+ }
646
+ // Accordion/Details
647
+ if (tag === 'details') {
648
+ // details/summary já tem semântica built-in
649
+ }
650
+ // Progress/Loading
651
+ if (SEMANTIC_PATTERNS.progress.test(normalizedName)) {
652
+ attrs['role'] = 'status';
653
+ attrs['aria-busy'] = 'true';
654
+ attrs['aria-label'] = 'Loading';
655
+ }
656
+ // Botões de ícone (sem texto visível)
657
+ if (tag === 'button' && SEMANTIC_PATTERNS.icon.test(normalizedName)) {
658
+ attrs['aria-label'] = normalizedName.replace(/-/g, ' ');
659
+ }
660
+ // Imagens decorativas vs informativas
661
+ if (tag === 'img') {
662
+ // Se for ícone ou decorativo, alt pode ser vazio
663
+ if (SEMANTIC_PATTERNS.icon.test(normalizedName) || normalizedName.includes('decorative')) {
664
+ attrs['alt'] = '';
665
+ attrs['aria-hidden'] = 'true';
666
+ }
667
+ }
668
+ return attrs;
669
+ }
@@ -13,6 +13,7 @@ export interface IStyleProperties {
13
13
  }
14
14
  /**
15
15
  * Parseia background color de um node
16
+ * NOTA: Para nós TEXT, o fills representa a cor do texto, não background
16
17
  */
17
18
  export declare function parseBackgroundColor(node: IFigmaNode): IStyleProperty | null;
18
19
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"style-parser.d.ts","sourceRoot":"","sources":["../../src/parser/style-parser.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EACV,UAAU,EAIV,cAAc,EACf,MAAM,oBAAoB,CAAC;AAS5B,MAAM,WAAW,gBAAgB;IAC/B,eAAe,CAAC,EAAE,cAAc,CAAC;IACjC,KAAK,CAAC,EAAE,cAAc,CAAC;IACvB,YAAY,CAAC,EAAE,cAAc,CAAC;IAC9B,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB,SAAS,CAAC,EAAE,cAAc,CAAC;IAC3B,OAAO,CAAC,EAAE,cAAc,CAAC;CAC1B;AAWD;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,UAAU,GAAG,cAAc,GAAG,IAAI,CAsB5E;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,UAAU,GAAG,cAAc,GAAG,IAAI,CAuBnE;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,UAAU,GAAG,cAAc,GAAG,IAAI,CAyBzE;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,UAAU,GAAG,cAAc,GAAG,IAAI,CAmCtE;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,UAAU,GAAG,cAAc,GAAG,IAAI,CASpE;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,UAAU,GAAG,cAAc,EAAE,CAmB9D"}
1
+ {"version":3,"file":"style-parser.d.ts","sourceRoot":"","sources":["../../src/parser/style-parser.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EACV,UAAU,EAIV,cAAc,EACf,MAAM,oBAAoB,CAAC;AAS5B,MAAM,WAAW,gBAAgB;IAC/B,eAAe,CAAC,EAAE,cAAc,CAAC;IACjC,KAAK,CAAC,EAAE,cAAc,CAAC;IACvB,YAAY,CAAC,EAAE,cAAc,CAAC;IAC9B,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB,SAAS,CAAC,EAAE,cAAc,CAAC;IAC3B,OAAO,CAAC,EAAE,cAAc,CAAC;CAC1B;AAWD;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,UAAU,GAAG,cAAc,GAAG,IAAI,CA2B5E;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,UAAU,GAAG,cAAc,GAAG,IAAI,CAuBnE;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,UAAU,GAAG,cAAc,GAAG,IAAI,CAyBzE;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,UAAU,GAAG,cAAc,GAAG,IAAI,CAmCtE;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,UAAU,GAAG,cAAc,GAAG,IAAI,CASpE;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,UAAU,GAAG,cAAc,EAAE,CAmB9D"}
@@ -13,8 +13,13 @@ function extractSolidColor(fill) {
13
13
  }
14
14
  /**
15
15
  * Parseia background color de um node
16
+ * NOTA: Para nós TEXT, o fills representa a cor do texto, não background
16
17
  */
17
18
  export function parseBackgroundColor(node) {
19
+ // Não aplicar background-color em nós TEXT (fills é a cor do texto)
20
+ if (node.type === 'TEXT') {
21
+ return null;
22
+ }
18
23
  if (!node.fills || node.fills.length === 0) {
19
24
  return null;
20
25
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@promptui-lib/figma-parser",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
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.7"
33
+ "@promptui-lib/core": "0.1.9"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@types/node": "^20.0.0",