@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.
- package/dist/parser/index.d.ts +1 -1
- package/dist/parser/index.d.ts.map +1 -1
- package/dist/parser/index.js +1 -1
- package/dist/parser/node-parser.d.ts.map +1 -1
- package/dist/parser/node-parser.js +271 -0
- package/dist/parser/semantic-detector.d.ts.map +1 -1
- package/dist/parser/semantic-detector.js +83 -11
- package/dist/parser/style-parser.d.ts +9 -0
- package/dist/parser/style-parser.d.ts.map +1 -1
- package/dist/parser/style-parser.js +50 -0
- package/package.json +2 -2
package/dist/parser/index.d.ts
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/parser/index.js
CHANGED
|
@@ -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;
|
|
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;
|
|
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:
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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,
|
|
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.
|
|
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.
|
|
33
|
+
"@promptui-lib/core": "0.1.22"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@types/node": "^20.0.0",
|