@promptui-lib/figma-parser 0.1.19 → 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 +3 -3
- package/dist/parser/index.d.ts.map +1 -1
- package/dist/parser/index.js +3 -3
- package/dist/parser/layout-parser.d.ts +8 -2
- package/dist/parser/layout-parser.d.ts.map +1 -1
- package/dist/parser/layout-parser.js +56 -9
- package/dist/parser/node-parser.d.ts.map +1 -1
- package/dist/parser/node-parser.js +336 -3
- package/dist/parser/position-parser.d.ts +5 -0
- package/dist/parser/position-parser.d.ts.map +1 -1
- package/dist/parser/position-parser.js +26 -6
- 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,10 +1,10 @@
|
|
|
1
|
-
export { parseLayout, parseSizing, parseLayoutAndSizing } from './layout-parser.js';
|
|
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';
|
|
7
|
-
export { parsePosition, parseContainerPosition, hasAutoLayout } from './position-parser.js';
|
|
7
|
+
export { parsePosition, parseContainerPosition, hasAutoLayout, isAbsolutelyPositioned } from './position-parser.js';
|
|
8
8
|
export type { IPositionProperties } from './position-parser.js';
|
|
9
9
|
export { parseNode, parseNodes, parseFrameName } from './node-parser.js';
|
|
10
10
|
export type { IParseOptions } from './node-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,MAAM,oBAAoB,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,6 +1,6 @@
|
|
|
1
|
-
export { parseLayout, parseSizing, parseLayoutAndSizing } from './layout-parser.js';
|
|
2
|
-
export { parseStyles, parseBackgroundColor, parseBorder, parseBorderRadius, parseBoxShadow } from './style-parser.js';
|
|
1
|
+
export { parseLayout, parseSizing, parseLayoutAndSizing, parsePadding } from './layout-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
|
-
export { parsePosition, parseContainerPosition, hasAutoLayout } from './position-parser.js';
|
|
4
|
+
export { parsePosition, parseContainerPosition, hasAutoLayout, isAbsolutelyPositioned } from './position-parser.js';
|
|
5
5
|
export { parseNode, parseNodes, parseFrameName } from './node-parser.js';
|
|
6
6
|
export { detectSemanticTag, normalizeName, getSemanticAttributes } from './semantic-detector.js';
|
|
@@ -15,16 +15,22 @@ export interface ILayoutProperties {
|
|
|
15
15
|
flex?: IStyleProperty;
|
|
16
16
|
flexWrap?: IStyleProperty;
|
|
17
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* Parseia padding de um node (funciona com ou sem Auto Layout)
|
|
20
|
+
*/
|
|
21
|
+
export declare function parsePadding(node: IFigmaNode): IStyleProperty | null;
|
|
18
22
|
/**
|
|
19
23
|
* Parseia AutoLayout de um node
|
|
20
24
|
*/
|
|
21
25
|
export declare function parseLayout(node: IFigmaNode): ILayoutProperties;
|
|
22
26
|
/**
|
|
23
27
|
* Parseia sizing de um node
|
|
28
|
+
* @param isRoot - indica se é o node raiz do componente
|
|
24
29
|
*/
|
|
25
|
-
export declare function parseSizing(node: IFigmaNode): ILayoutProperties;
|
|
30
|
+
export declare function parseSizing(node: IFigmaNode, isRoot?: boolean): ILayoutProperties;
|
|
26
31
|
/**
|
|
27
32
|
* Combina layout e sizing
|
|
33
|
+
* @param isRoot - indica se é o node raiz do componente
|
|
28
34
|
*/
|
|
29
|
-
export declare function parseLayoutAndSizing(node: IFigmaNode): IStyleProperty[];
|
|
35
|
+
export declare function parseLayoutAndSizing(node: IFigmaNode, isRoot?: boolean): IStyleProperty[];
|
|
30
36
|
//# sourceMappingURL=layout-parser.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"layout-parser.d.ts","sourceRoot":"","sources":["../../src/parser/layout-parser.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AA4BrE,MAAM,WAAW,iBAAiB;IAChC,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB,aAAa,CAAC,EAAE,cAAc,CAAC;IAC/B,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B,GAAG,CAAC,EAAE,cAAc,CAAC;IACrB,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB,KAAK,CAAC,EAAE,cAAc,CAAC;IACvB,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB,IAAI,CAAC,EAAE,cAAc,CAAC;IACtB,QAAQ,CAAC,EAAE,cAAc,CAAC;CAC3B;AAED;;GAEG;AACH,wBAAgB,
|
|
1
|
+
{"version":3,"file":"layout-parser.d.ts","sourceRoot":"","sources":["../../src/parser/layout-parser.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AA4BrE,MAAM,WAAW,iBAAiB;IAChC,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB,aAAa,CAAC,EAAE,cAAc,CAAC;IAC/B,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B,GAAG,CAAC,EAAE,cAAc,CAAC;IACrB,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB,KAAK,CAAC,EAAE,cAAc,CAAC;IACvB,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB,IAAI,CAAC,EAAE,cAAc,CAAC;IACtB,QAAQ,CAAC,EAAE,cAAc,CAAC;CAC3B;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,UAAU,GAAG,cAAc,GAAG,IAAI,CAuBpE;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,UAAU,GAAG,iBAAiB,CA2F/D;AAwBD;;;GAGG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,UAAU,EAAE,MAAM,GAAE,OAAe,GAAG,iBAAiB,CA+ExF;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,UAAU,EAAE,MAAM,GAAE,OAAe,GAAG,cAAc,EAAE,CAShG"}
|
|
@@ -26,6 +26,24 @@ function formatPixels(value) {
|
|
|
26
26
|
}
|
|
27
27
|
return `${rounded}px`;
|
|
28
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Parseia padding de um node (funciona com ou sem Auto Layout)
|
|
31
|
+
*/
|
|
32
|
+
export function parsePadding(node) {
|
|
33
|
+
const hasPadding = node.paddingTop ||
|
|
34
|
+
node.paddingRight ||
|
|
35
|
+
node.paddingBottom ||
|
|
36
|
+
node.paddingLeft;
|
|
37
|
+
if (!hasPadding) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
const paddingToken = generatePaddingToken(node.paddingTop ?? 0, node.paddingRight ?? 0, node.paddingBottom ?? 0, node.paddingLeft ?? 0);
|
|
41
|
+
return {
|
|
42
|
+
property: 'padding',
|
|
43
|
+
value: paddingToken,
|
|
44
|
+
token: paddingToken.startsWith('$') ? paddingToken : undefined,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
29
47
|
/**
|
|
30
48
|
* Parseia AutoLayout de um node
|
|
31
49
|
*/
|
|
@@ -102,11 +120,32 @@ export function parseLayout(node) {
|
|
|
102
120
|
}
|
|
103
121
|
return properties;
|
|
104
122
|
}
|
|
123
|
+
/**
|
|
124
|
+
* Detecta se o node é uma tela/página (root component que deve ocupar viewport)
|
|
125
|
+
*/
|
|
126
|
+
function isScreenNode(node) {
|
|
127
|
+
const name = node.name.toLowerCase().replace(/^#/, '');
|
|
128
|
+
const screenPatterns = /screen|page|view|layout|template|tela|pagina/i;
|
|
129
|
+
// Verifica se é uma tela pelo nome
|
|
130
|
+
if (screenPatterns.test(name)) {
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
// Verifica se tem dimensões típicas de tela (>= 320px width e >= 568px height)
|
|
134
|
+
const box = node.absoluteBoundingBox;
|
|
135
|
+
if (box && box.width >= 320 && box.height >= 568) {
|
|
136
|
+
// Se é um frame de nível superior com dimensões de tela
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
105
141
|
/**
|
|
106
142
|
* Parseia sizing de um node
|
|
143
|
+
* @param isRoot - indica se é o node raiz do componente
|
|
107
144
|
*/
|
|
108
|
-
export function parseSizing(node) {
|
|
145
|
+
export function parseSizing(node, isRoot = false) {
|
|
109
146
|
const properties = {};
|
|
147
|
+
// Se é o node raiz e é uma tela, usa viewport units
|
|
148
|
+
const isScreen = isRoot && isScreenNode(node);
|
|
110
149
|
// Width
|
|
111
150
|
if (node.layoutSizingHorizontal) {
|
|
112
151
|
switch (node.layoutSizingHorizontal) {
|
|
@@ -126,17 +165,17 @@ export function parseSizing(node) {
|
|
|
126
165
|
if (node.absoluteBoundingBox?.width) {
|
|
127
166
|
properties.width = {
|
|
128
167
|
property: 'width',
|
|
129
|
-
value: formatPixels(node.absoluteBoundingBox.width),
|
|
168
|
+
value: isScreen ? '100vw' : formatPixels(node.absoluteBoundingBox.width),
|
|
130
169
|
};
|
|
131
170
|
}
|
|
132
171
|
break;
|
|
133
172
|
}
|
|
134
173
|
}
|
|
135
174
|
else if (node.absoluteBoundingBox?.width) {
|
|
136
|
-
// Fallback: Se não tem layoutSizing mas tem dimensões
|
|
175
|
+
// Fallback: Se não tem layoutSizing mas tem dimensões
|
|
137
176
|
properties.width = {
|
|
138
177
|
property: 'width',
|
|
139
|
-
value: formatPixels(node.absoluteBoundingBox.width),
|
|
178
|
+
value: isScreen ? '100vw' : formatPixels(node.absoluteBoundingBox.width),
|
|
140
179
|
};
|
|
141
180
|
}
|
|
142
181
|
// Height
|
|
@@ -158,27 +197,35 @@ export function parseSizing(node) {
|
|
|
158
197
|
if (node.absoluteBoundingBox?.height) {
|
|
159
198
|
properties.height = {
|
|
160
199
|
property: 'height',
|
|
161
|
-
value: formatPixels(node.absoluteBoundingBox.height),
|
|
200
|
+
value: isScreen ? '100vh' : formatPixels(node.absoluteBoundingBox.height),
|
|
162
201
|
};
|
|
163
202
|
}
|
|
164
203
|
break;
|
|
165
204
|
}
|
|
166
205
|
}
|
|
167
206
|
else if (node.absoluteBoundingBox?.height) {
|
|
168
|
-
// Fallback: Se não tem layoutSizing mas tem dimensões
|
|
207
|
+
// Fallback: Se não tem layoutSizing mas tem dimensões
|
|
169
208
|
properties.height = {
|
|
170
209
|
property: 'height',
|
|
171
|
-
value: formatPixels(node.absoluteBoundingBox.height),
|
|
210
|
+
value: isScreen ? '100vh' : formatPixels(node.absoluteBoundingBox.height),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
// Para screens, adiciona overflow hidden para evitar scroll
|
|
214
|
+
if (isScreen) {
|
|
215
|
+
properties.flex = {
|
|
216
|
+
property: 'overflow',
|
|
217
|
+
value: 'hidden',
|
|
172
218
|
};
|
|
173
219
|
}
|
|
174
220
|
return properties;
|
|
175
221
|
}
|
|
176
222
|
/**
|
|
177
223
|
* Combina layout e sizing
|
|
224
|
+
* @param isRoot - indica se é o node raiz do componente
|
|
178
225
|
*/
|
|
179
|
-
export function parseLayoutAndSizing(node) {
|
|
226
|
+
export function parseLayoutAndSizing(node, isRoot = false) {
|
|
180
227
|
const layout = parseLayout(node);
|
|
181
|
-
const sizing = parseSizing(node);
|
|
228
|
+
const sizing = parseSizing(node, isRoot);
|
|
182
229
|
const combined = { ...layout, ...sizing };
|
|
183
230
|
return Object.values(combined).filter((prop) => prop !== undefined);
|
|
184
231
|
}
|
|
@@ -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,
|
|
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"}
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
* Parser principal que converte node do Figma em AST do componente
|
|
4
4
|
*/
|
|
5
5
|
import { generateComponentName, generateFileName, generateBEMBlock, generateBEMElement, classifyComponent, extractBaseName, extractVariants, isComponentNode, } from '@promptui-lib/core';
|
|
6
|
-
import { parseLayoutAndSizing, parseSizing } from './layout-parser.js';
|
|
6
|
+
import { parseLayoutAndSizing, parseSizing, parsePadding } from './layout-parser.js';
|
|
7
7
|
import { parseStyles } from './style-parser.js';
|
|
8
8
|
import { parseTextStyles } from './text-parser.js';
|
|
9
9
|
import { detectSemanticTag, normalizeName, getSemanticAttributes } from './semantic-detector.js';
|
|
10
|
-
import { parsePosition, parseContainerPosition } from './position-parser.js';
|
|
10
|
+
import { parsePosition, parseContainerPosition, hasAutoLayout } from './position-parser.js';
|
|
11
11
|
/**
|
|
12
12
|
* Parseia o nome do frame para extrair informações
|
|
13
13
|
*/
|
|
@@ -32,6 +32,204 @@ export function parseFrameName(name) {
|
|
|
32
32
|
extractedProps,
|
|
33
33
|
};
|
|
34
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Verifica se um node deve ser exportado como asset (SVG/imagem)
|
|
37
|
+
* Tipos que são gráficos vetoriais ou ícones devem ser exportados como SVG
|
|
38
|
+
*/
|
|
39
|
+
function isAssetNode(node) {
|
|
40
|
+
// Tipos de nodes que são gráficos vetoriais
|
|
41
|
+
const vectorTypes = ['VECTOR', 'BOOLEAN_OPERATION', 'STAR', 'LINE', 'ELLIPSE', 'REGULAR_POLYGON'];
|
|
42
|
+
if (vectorTypes.includes(node.type)) {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
// Verifica se o nome indica que é um ícone
|
|
46
|
+
const name = node.name.toLowerCase();
|
|
47
|
+
if (/^(icon|ico|icone|svg)[-_]?/i.test(name) || name.includes('-icon') || name.includes('_icon')) {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
// Frames pequenos (< 64x64) com fills complexos podem ser ícones
|
|
51
|
+
if (node.type === 'FRAME' && node.absoluteBoundingBox) {
|
|
52
|
+
const { width, height } = node.absoluteBoundingBox;
|
|
53
|
+
if (width <= 64 && height <= 64) {
|
|
54
|
+
// Verifica se tem apenas elementos vetoriais como filhos
|
|
55
|
+
const hasOnlyVectors = node.children?.every((child) => vectorTypes.includes(child.type) || child.type === 'GROUP');
|
|
56
|
+
if (hasOnlyVectors && node.children && node.children.length > 0) {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return false;
|
|
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
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Gera nome seguro para arquivo de asset
|
|
225
|
+
*/
|
|
226
|
+
function generateAssetFileName(nodeName, blockName) {
|
|
227
|
+
const cleanName = nodeName
|
|
228
|
+
.toLowerCase()
|
|
229
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
230
|
+
.replace(/^-+|-+$/g, '');
|
|
231
|
+
return `${blockName}-${cleanName || 'icon'}`;
|
|
232
|
+
}
|
|
35
233
|
/**
|
|
36
234
|
* Converte um node em elemento JSX
|
|
37
235
|
* Se o filho for um componente (marcado com #), retorna IJSXComponent em vez de JSX inline
|
|
@@ -47,6 +245,117 @@ function nodeToJSX(node, blockName, depth = 0, parentNode, usedTags) {
|
|
|
47
245
|
const className = isRoot
|
|
48
246
|
? blockName
|
|
49
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
|
+
}
|
|
50
359
|
// Detecta a tag HTML semântica apropriada (passa usedTags para evitar duplicação de <main>)
|
|
51
360
|
const tag = detectSemanticTag(node, parentNode, depth, tagsSet);
|
|
52
361
|
// Obtém atributos semânticos (href, type, etc.)
|
|
@@ -74,6 +383,22 @@ function nodeToJSX(node, blockName, depth = 0, parentNode, usedTags) {
|
|
|
74
383
|
fileName: childFileName,
|
|
75
384
|
});
|
|
76
385
|
}
|
|
386
|
+
else if (isAssetNode(child)) {
|
|
387
|
+
// Se é um asset (ícone/vetor), cria referência para exportação
|
|
388
|
+
const childNormalizedName = normalizeName(child.name);
|
|
389
|
+
const childElementName = childNormalizedName.replace(/\s+/g, '-');
|
|
390
|
+
const childClassName = generateBEMElement(blockName, childElementName);
|
|
391
|
+
children.push({
|
|
392
|
+
type: 'asset',
|
|
393
|
+
nodeId: child.id,
|
|
394
|
+
fileName: generateAssetFileName(child.name, blockName),
|
|
395
|
+
format: 'svg',
|
|
396
|
+
className: childClassName,
|
|
397
|
+
alt: childNormalizedName,
|
|
398
|
+
width: child.absoluteBoundingBox?.width,
|
|
399
|
+
height: child.absoluteBoundingBox?.height,
|
|
400
|
+
});
|
|
401
|
+
}
|
|
77
402
|
else {
|
|
78
403
|
children.push(nodeToJSX(child, blockName, depth + 1, node, tagsSet));
|
|
79
404
|
}
|
|
@@ -110,12 +435,20 @@ function collectStyles(node, blockName, depth = 0, parentNode) {
|
|
|
110
435
|
// Para GROUP: extrai sizing e position: relative (container sem flex)
|
|
111
436
|
// Para outros tipos (RECTANGLE, etc): extrai apenas sizing (width, height)
|
|
112
437
|
if (node.type === 'FRAME' || node.type === 'COMPONENT' || node.type === 'INSTANCE') {
|
|
113
|
-
properties.push(...parseLayoutAndSizing(node));
|
|
438
|
+
properties.push(...parseLayoutAndSizing(node, isRoot));
|
|
114
439
|
// Se é um container sem Auto Layout, adiciona position: relative
|
|
115
440
|
const containerPos = parseContainerPosition(node);
|
|
116
441
|
if (containerPos) {
|
|
117
442
|
properties.push(containerPos);
|
|
118
443
|
}
|
|
444
|
+
// Extrai padding mesmo se não tem Auto Layout (para elementos como inputs)
|
|
445
|
+
// parseLayoutAndSizing já extrai padding quando tem Auto Layout, mas não quando não tem
|
|
446
|
+
if (!hasAutoLayout(node)) {
|
|
447
|
+
const paddingProp = parsePadding(node);
|
|
448
|
+
if (paddingProp) {
|
|
449
|
+
properties.push(paddingProp);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
119
452
|
}
|
|
120
453
|
else if (node.type === 'GROUP') {
|
|
121
454
|
// GROUPs são containers visuais - precisam de position: relative e sizing
|
|
@@ -16,6 +16,11 @@ export interface IPositionProperties {
|
|
|
16
16
|
* Verifica se um node pai tem Auto Layout
|
|
17
17
|
*/
|
|
18
18
|
export declare function hasAutoLayout(node: IFigmaNode): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Verifica se um node está posicionado absolutamente
|
|
21
|
+
* (mesmo dentro de um container com Auto Layout)
|
|
22
|
+
*/
|
|
23
|
+
export declare function isAbsolutelyPositioned(node: IFigmaNode): boolean;
|
|
19
24
|
/**
|
|
20
25
|
* Parseia constraints e posição de um node
|
|
21
26
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"position-parser.d.ts","sourceRoot":"","sources":["../../src/parser/position-parser.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAgB,MAAM,oBAAoB,CAAC;AAEnF,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,GAAG,CAAC,EAAE,cAAc,CAAC;IACrB,KAAK,CAAC,EAAE,cAAc,CAAC;IACvB,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB,IAAI,CAAC,EAAE,cAAc,CAAC;IACtB,SAAS,CAAC,EAAE,cAAc,CAAC;CAC5B;AAoBD;;GAEG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAEvD;AAwBD;;GAEG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,UAAU,EAChB,UAAU,CAAC,EAAE,UAAU,GACtB,cAAc,EAAE,
|
|
1
|
+
{"version":3,"file":"position-parser.d.ts","sourceRoot":"","sources":["../../src/parser/position-parser.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAgB,MAAM,oBAAoB,CAAC;AAEnF,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,GAAG,CAAC,EAAE,cAAc,CAAC;IACrB,KAAK,CAAC,EAAE,cAAc,CAAC;IACvB,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB,IAAI,CAAC,EAAE,cAAc,CAAC;IACtB,SAAS,CAAC,EAAE,cAAc,CAAC;CAC5B;AAoBD;;GAEG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAEvD;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAEhE;AAwBD;;GAEG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,UAAU,EAChB,UAAU,CAAC,EAAE,UAAU,GACtB,cAAc,EAAE,CA6IlB;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAa/D;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,UAAU,GAAG,cAAc,GAAG,IAAI,CAQ9E"}
|
|
@@ -25,6 +25,13 @@ function formatPixels(value) {
|
|
|
25
25
|
export function hasAutoLayout(node) {
|
|
26
26
|
return node.layoutMode !== undefined && node.layoutMode !== 'NONE';
|
|
27
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Verifica se um node está posicionado absolutamente
|
|
30
|
+
* (mesmo dentro de um container com Auto Layout)
|
|
31
|
+
*/
|
|
32
|
+
export function isAbsolutelyPositioned(node) {
|
|
33
|
+
return node.layoutPositioning === 'ABSOLUTE';
|
|
34
|
+
}
|
|
28
35
|
/**
|
|
29
36
|
* Calcula a posição relativa de um filho em relação ao pai
|
|
30
37
|
*/
|
|
@@ -46,8 +53,16 @@ function calculateRelativePosition(child, parent) {
|
|
|
46
53
|
*/
|
|
47
54
|
export function parsePosition(node, parentNode) {
|
|
48
55
|
const properties = [];
|
|
49
|
-
|
|
50
|
-
|
|
56
|
+
if (!parentNode) {
|
|
57
|
+
return properties;
|
|
58
|
+
}
|
|
59
|
+
// Verifica se o node precisa de posicionamento absoluto:
|
|
60
|
+
// 1. O pai NÃO tem Auto Layout, OU
|
|
61
|
+
// 2. O node tem layoutPositioning: 'ABSOLUTE' (absoluto dentro de Auto Layout)
|
|
62
|
+
const parentHasAutoLayout = hasAutoLayout(parentNode);
|
|
63
|
+
const nodeIsAbsolute = isAbsolutelyPositioned(node);
|
|
64
|
+
if (parentHasAutoLayout && !nodeIsAbsolute) {
|
|
65
|
+
// Pai tem Auto Layout e o node não é absoluto - não precisa de posicionamento
|
|
51
66
|
return properties;
|
|
52
67
|
}
|
|
53
68
|
// Calcula posição relativa
|
|
@@ -56,7 +71,7 @@ export function parsePosition(node, parentNode) {
|
|
|
56
71
|
return properties;
|
|
57
72
|
}
|
|
58
73
|
const constraints = node.constraints;
|
|
59
|
-
//
|
|
74
|
+
// Elemento precisa de posição absoluta
|
|
60
75
|
properties.push({
|
|
61
76
|
property: 'position',
|
|
62
77
|
value: 'absolute',
|
|
@@ -176,11 +191,16 @@ export function parsePosition(node, parentNode) {
|
|
|
176
191
|
* (necessário quando tem filhos com position: absolute)
|
|
177
192
|
*/
|
|
178
193
|
export function needsRelativePosition(node) {
|
|
179
|
-
|
|
180
|
-
|
|
194
|
+
if (!node.children || node.children.length === 0) {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
// Se não tem Auto Layout, precisa de relative (todos os filhos serão absolutos)
|
|
198
|
+
if (!hasAutoLayout(node)) {
|
|
181
199
|
return true;
|
|
182
200
|
}
|
|
183
|
-
|
|
201
|
+
// Se tem Auto Layout, verifica se algum filho tem layoutPositioning: 'ABSOLUTE'
|
|
202
|
+
const hasAbsoluteChild = node.children.some((child) => isAbsolutelyPositioned(child));
|
|
203
|
+
return hasAbsoluteChild;
|
|
184
204
|
}
|
|
185
205
|
/**
|
|
186
206
|
* Parseia position: relative para containers sem Auto Layout
|
|
@@ -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",
|