@promptui-lib/figma-parser 0.1.6 → 0.1.8

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;AAqJD;;;GAGG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,UAAU,EAChB,GAAG,EAAE,MAAM,EACX,cAAc,EAAE,MAAM,GACrB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAoKxB"}
@@ -0,0 +1,664 @@
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
404
+ if (SEMANTIC_PATTERNS.link.test(name)) {
405
+ return 'a';
406
+ }
407
+ // Citações
408
+ if (SEMANTIC_PATTERNS.blockquote.test(name)) {
409
+ return 'blockquote';
410
+ }
411
+ // Código
412
+ if (SEMANTIC_PATTERNS.code.test(name)) {
413
+ return 'code';
414
+ }
415
+ // Data/hora
416
+ if (SEMANTIC_PATTERNS.time.test(name)) {
417
+ return 'time';
418
+ }
419
+ // Endereço
420
+ if (SEMANTIC_PATTERNS.address.test(name)) {
421
+ return 'address';
422
+ }
423
+ // Verifica contexto do pai
424
+ if (parentNode) {
425
+ const parentName = normalizeName(parentNode.name);
426
+ // Se está dentro de um botão
427
+ if (SEMANTIC_PATTERNS.button.test(parentName)) {
428
+ return 'span';
429
+ }
430
+ // Se está dentro de um link
431
+ if (SEMANTIC_PATTERNS.link.test(parentName)) {
432
+ return 'span';
433
+ }
434
+ // Se está dentro de um label
435
+ if (SEMANTIC_PATTERNS.label.test(parentName)) {
436
+ return 'span';
437
+ }
438
+ // Se está dentro de um heading
439
+ if (SEMANTIC_PATTERNS.heading.test(parentName)) {
440
+ return 'span';
441
+ }
442
+ // Se pai é um item de lista
443
+ if (SEMANTIC_PATTERNS.listItem.test(parentName)) {
444
+ return 'span';
445
+ }
446
+ // Se pai é uma célula de tabela
447
+ if (SEMANTIC_PATTERNS.tableCell.test(parentName) || SEMANTIC_PATTERNS.tableHeader.test(parentName)) {
448
+ return 'span';
449
+ }
450
+ // Se pai é um fieldset, pode ser a legend
451
+ if (SEMANTIC_PATTERNS.fieldset.test(parentName)) {
452
+ return 'legend';
453
+ }
454
+ // Se pai é figure, pode ser figcaption
455
+ if (SEMANTIC_PATTERNS.figure.test(parentName)) {
456
+ return 'figcaption';
457
+ }
458
+ }
459
+ // Parágrafos e descrições
460
+ if (SEMANTIC_PATTERNS.paragraph.test(name)) {
461
+ return 'p';
462
+ }
463
+ // Texto genérico - decide baseado no tamanho
464
+ const textLength = node.characters?.length ?? 0;
465
+ // Texto longo (>100 chars) -> parágrafo
466
+ if (textLength > 100) {
467
+ return 'p';
468
+ }
469
+ // Texto médio com quebras de linha -> parágrafo
470
+ if (node.characters?.includes('\n')) {
471
+ return 'p';
472
+ }
473
+ return 'span';
474
+ }
475
+ /**
476
+ * Determina o nível do heading baseado no tamanho da fonte
477
+ */
478
+ function getHeadingLevel(fontSize) {
479
+ if (fontSize >= 36)
480
+ return 'h1';
481
+ if (fontSize >= 30)
482
+ return 'h2';
483
+ if (fontSize >= 24)
484
+ return 'h3';
485
+ if (fontSize >= 20)
486
+ return 'h4';
487
+ if (fontSize >= 18)
488
+ return 'h5';
489
+ return 'h6';
490
+ }
491
+ /**
492
+ * Verifica se um node contém um input como filho
493
+ */
494
+ function hasInputChild(node) {
495
+ if (!node.children)
496
+ return false;
497
+ return node.children.some(child => {
498
+ const childName = normalizeName(child.name);
499
+ return SEMANTIC_PATTERNS.input.test(childName) ||
500
+ child.name.toLowerCase().includes('input');
501
+ });
502
+ }
503
+ /**
504
+ * Gera atributos adicionais baseado no contexto
505
+ * Segue boas práticas de SEO e acessibilidade
506
+ */
507
+ export function getSemanticAttributes(node, tag, normalizedName) {
508
+ const attrs = {};
509
+ // Links - SEO: links externos devem ter rel="noopener noreferrer"
510
+ if (tag === 'a') {
511
+ attrs['href'] = '#';
512
+ // Se for link externo, adicionar segurança
513
+ // attrs['rel'] = 'noopener noreferrer';
514
+ // attrs['target'] = '_blank';
515
+ }
516
+ // Inputs
517
+ if (tag === 'input') {
518
+ // Detecta tipo de input
519
+ if (normalizedName.includes('email')) {
520
+ attrs['type'] = 'email';
521
+ attrs['placeholder'] = 'email@example.com';
522
+ }
523
+ else if (normalizedName.includes('password') || normalizedName.includes('senha')) {
524
+ attrs['type'] = 'password';
525
+ attrs['placeholder'] = '********';
526
+ }
527
+ else if (normalizedName.includes('search') || normalizedName.includes('busca')) {
528
+ attrs['type'] = 'search';
529
+ attrs['placeholder'] = 'Search...';
530
+ }
531
+ else if (normalizedName.includes('phone') || normalizedName.includes('telefone') || normalizedName.includes('tel')) {
532
+ attrs['type'] = 'tel';
533
+ attrs['placeholder'] = '+1 (555) 000-0000';
534
+ }
535
+ else if (normalizedName.includes('number') || normalizedName.includes('numero') || normalizedName.includes('quantidade')) {
536
+ attrs['type'] = 'number';
537
+ }
538
+ else if (normalizedName.includes('date') || normalizedName.includes('data')) {
539
+ attrs['type'] = 'date';
540
+ }
541
+ else if (normalizedName.includes('time') || normalizedName.includes('hora')) {
542
+ attrs['type'] = 'time';
543
+ }
544
+ else if (normalizedName.includes('checkbox')) {
545
+ attrs['type'] = 'checkbox';
546
+ }
547
+ else if (normalizedName.includes('radio')) {
548
+ attrs['type'] = 'radio';
549
+ }
550
+ else if (normalizedName.includes('file') || normalizedName.includes('arquivo') || normalizedName.includes('upload')) {
551
+ attrs['type'] = 'file';
552
+ }
553
+ else if (normalizedName.includes('url') || normalizedName.includes('website') || normalizedName.includes('site')) {
554
+ attrs['type'] = 'url';
555
+ attrs['placeholder'] = 'https://';
556
+ }
557
+ else {
558
+ attrs['type'] = 'text';
559
+ }
560
+ }
561
+ // Textarea
562
+ if (tag === 'textarea') {
563
+ attrs['rows'] = '4';
564
+ }
565
+ // Select
566
+ if (tag === 'select') {
567
+ // Será preenchido com options
568
+ }
569
+ // Botões
570
+ if (tag === 'button') {
571
+ if (normalizedName.includes('submit') || normalizedName.includes('enviar') || normalizedName.includes('login')) {
572
+ attrs['type'] = 'submit';
573
+ }
574
+ else if (normalizedName.includes('reset') || normalizedName.includes('limpar')) {
575
+ attrs['type'] = 'reset';
576
+ }
577
+ else {
578
+ attrs['type'] = 'button';
579
+ }
580
+ }
581
+ // Imagens
582
+ if (tag === 'img') {
583
+ attrs['src'] = '';
584
+ attrs['alt'] = normalizedName.replace(/-/g, ' ');
585
+ attrs['loading'] = 'lazy';
586
+ }
587
+ // Vídeo
588
+ if (tag === 'video') {
589
+ attrs['controls'] = '';
590
+ }
591
+ // Áudio
592
+ if (tag === 'audio') {
593
+ attrs['controls'] = '';
594
+ }
595
+ // Dialog
596
+ if (tag === 'dialog') {
597
+ // dialog não precisa de atributos especiais por padrão
598
+ }
599
+ // Time
600
+ if (tag === 'time') {
601
+ attrs['datetime'] = '';
602
+ }
603
+ // Progress
604
+ if (tag === 'progress') {
605
+ attrs['value'] = '0';
606
+ attrs['max'] = '100';
607
+ }
608
+ // Meter
609
+ if (tag === 'meter') {
610
+ attrs['value'] = '0';
611
+ attrs['min'] = '0';
612
+ attrs['max'] = '100';
613
+ }
614
+ // Divider/hr não precisa de atributos
615
+ // =====================================================
616
+ // ARIA e Acessibilidade (a11y)
617
+ // =====================================================
618
+ // Alert role para notificações
619
+ if (SEMANTIC_PATTERNS.alert.test(normalizedName)) {
620
+ attrs['role'] = 'alert';
621
+ attrs['aria-live'] = 'polite';
622
+ }
623
+ // Navegação
624
+ if (tag === 'nav') {
625
+ // Pode adicionar aria-label se necessário
626
+ if (normalizedName.includes('main') || normalizedName.includes('principal')) {
627
+ attrs['aria-label'] = 'Main navigation';
628
+ }
629
+ else if (normalizedName.includes('breadcrumb')) {
630
+ attrs['aria-label'] = 'Breadcrumb';
631
+ }
632
+ else if (normalizedName.includes('footer')) {
633
+ attrs['aria-label'] = 'Footer navigation';
634
+ }
635
+ }
636
+ // Dialog/Modal
637
+ if (tag === 'dialog') {
638
+ attrs['aria-modal'] = 'true';
639
+ attrs['aria-labelledby'] = ''; // Deve ser preenchido com ID do título
640
+ }
641
+ // Accordion/Details
642
+ if (tag === 'details') {
643
+ // details/summary já tem semântica built-in
644
+ }
645
+ // Progress/Loading
646
+ if (SEMANTIC_PATTERNS.progress.test(normalizedName)) {
647
+ attrs['role'] = 'status';
648
+ attrs['aria-busy'] = 'true';
649
+ attrs['aria-label'] = 'Loading';
650
+ }
651
+ // Botões de ícone (sem texto visível)
652
+ if (tag === 'button' && SEMANTIC_PATTERNS.icon.test(normalizedName)) {
653
+ attrs['aria-label'] = normalizedName.replace(/-/g, ' ');
654
+ }
655
+ // Imagens decorativas vs informativas
656
+ if (tag === 'img') {
657
+ // Se for ícone ou decorativo, alt pode ser vazio
658
+ if (SEMANTIC_PATTERNS.icon.test(normalizedName) || normalizedName.includes('decorative')) {
659
+ attrs['alt'] = '';
660
+ attrs['aria-hidden'] = 'true';
661
+ }
662
+ }
663
+ return attrs;
664
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@promptui-lib/figma-parser",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
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.6"
33
+ "@promptui-lib/core": "0.1.8"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@types/node": "^20.0.0",