@promptui-lib/figma-parser 0.1.20 → 0.1.23
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/layout-parser.d.ts +5 -2
- package/dist/parser/layout-parser.d.ts.map +1 -1
- package/dist/parser/layout-parser.js +17 -3
- package/dist/parser/node-parser.d.ts.map +1 -1
- package/dist/parser/node-parser.js +255 -3
- package/dist/parser/position-parser.d.ts.map +1 -1
- package/dist/parser/position-parser.js +9 -1
- package/dist/parser/semantic-detector.d.ts.map +1 -1
- package/dist/parser/semantic-detector.js +97 -19
- 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';
|
|
@@ -14,6 +14,7 @@ export interface ILayoutProperties {
|
|
|
14
14
|
height?: IStyleProperty;
|
|
15
15
|
flex?: IStyleProperty;
|
|
16
16
|
flexWrap?: IStyleProperty;
|
|
17
|
+
flexShrink?: IStyleProperty;
|
|
17
18
|
}
|
|
18
19
|
/**
|
|
19
20
|
* Parseia padding de um node (funciona com ou sem Auto Layout)
|
|
@@ -26,11 +27,13 @@ export declare function parseLayout(node: IFigmaNode): ILayoutProperties;
|
|
|
26
27
|
/**
|
|
27
28
|
* Parseia sizing de um node
|
|
28
29
|
* @param isRoot - indica se é o node raiz do componente
|
|
30
|
+
* @param parentHasAutoLayout - indica se o pai tem Auto Layout (para aplicar flex-shrink)
|
|
29
31
|
*/
|
|
30
|
-
export declare function parseSizing(node: IFigmaNode, isRoot?: boolean): ILayoutProperties;
|
|
32
|
+
export declare function parseSizing(node: IFigmaNode, isRoot?: boolean, parentHasAutoLayout?: boolean): ILayoutProperties;
|
|
31
33
|
/**
|
|
32
34
|
* Combina layout e sizing
|
|
33
35
|
* @param isRoot - indica se é o node raiz do componente
|
|
36
|
+
* @param parentHasAutoLayout - indica se o pai tem Auto Layout
|
|
34
37
|
*/
|
|
35
|
-
export declare function parseLayoutAndSizing(node: IFigmaNode, isRoot?: boolean): IStyleProperty[];
|
|
38
|
+
export declare function parseLayoutAndSizing(node: IFigmaNode, isRoot?: boolean, parentHasAutoLayout?: boolean): IStyleProperty[];
|
|
36
39
|
//# 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;
|
|
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;IAC1B,UAAU,CAAC,EAAE,cAAc,CAAC;CAC7B;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;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,UAAU,EAAE,MAAM,GAAE,OAAe,EAAE,mBAAmB,GAAE,OAAe,GAAG,iBAAiB,CA+F9H;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,UAAU,EAAE,MAAM,GAAE,OAAe,EAAE,mBAAmB,GAAE,OAAe,GAAG,cAAc,EAAE,CAStI"}
|
|
@@ -141,11 +141,17 @@ function isScreenNode(node) {
|
|
|
141
141
|
/**
|
|
142
142
|
* Parseia sizing de um node
|
|
143
143
|
* @param isRoot - indica se é o node raiz do componente
|
|
144
|
+
* @param parentHasAutoLayout - indica se o pai tem Auto Layout (para aplicar flex-shrink)
|
|
144
145
|
*/
|
|
145
|
-
export function parseSizing(node, isRoot = false) {
|
|
146
|
+
export function parseSizing(node, isRoot = false, parentHasAutoLayout = false) {
|
|
146
147
|
const properties = {};
|
|
147
148
|
// Se é o node raiz e é uma tela, usa viewport units
|
|
148
149
|
const isScreen = isRoot && isScreenNode(node);
|
|
150
|
+
// Verifica se precisa de flex-shrink: 0 (quando tem sizing HUG ou FIXED dentro de flex)
|
|
151
|
+
const needsFlexShrink = parentHasAutoLayout && (node.layoutSizingHorizontal === 'HUG' ||
|
|
152
|
+
node.layoutSizingHorizontal === 'FIXED' ||
|
|
153
|
+
node.layoutSizingVertical === 'HUG' ||
|
|
154
|
+
node.layoutSizingVertical === 'FIXED');
|
|
149
155
|
// Width
|
|
150
156
|
if (node.layoutSizingHorizontal) {
|
|
151
157
|
switch (node.layoutSizingHorizontal) {
|
|
@@ -217,15 +223,23 @@ export function parseSizing(node, isRoot = false) {
|
|
|
217
223
|
value: 'hidden',
|
|
218
224
|
};
|
|
219
225
|
}
|
|
226
|
+
// Adiciona flex-shrink: 0 quando necessário
|
|
227
|
+
if (needsFlexShrink) {
|
|
228
|
+
properties.flexShrink = {
|
|
229
|
+
property: 'flex-shrink',
|
|
230
|
+
value: '0',
|
|
231
|
+
};
|
|
232
|
+
}
|
|
220
233
|
return properties;
|
|
221
234
|
}
|
|
222
235
|
/**
|
|
223
236
|
* Combina layout e sizing
|
|
224
237
|
* @param isRoot - indica se é o node raiz do componente
|
|
238
|
+
* @param parentHasAutoLayout - indica se o pai tem Auto Layout
|
|
225
239
|
*/
|
|
226
|
-
export function parseLayoutAndSizing(node, isRoot = false) {
|
|
240
|
+
export function parseLayoutAndSizing(node, isRoot = false, parentHasAutoLayout = false) {
|
|
227
241
|
const layout = parseLayout(node);
|
|
228
|
-
const sizing = parseSizing(node, isRoot);
|
|
242
|
+
const sizing = parseSizing(node, isRoot, parentHasAutoLayout);
|
|
229
243
|
const combined = { ...layout, ...sizing };
|
|
230
244
|
return Object.values(combined).filter((prop) => prop !== undefined);
|
|
231
245
|
}
|
|
@@ -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;AAulBD;;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,145 @@ function isAssetNode(node) {
|
|
|
60
60
|
}
|
|
61
61
|
return false;
|
|
62
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* Patterns para detectar elementos de formulário com prefixo explícito $tipo:nome
|
|
65
|
+
* O usuário deve marcar explicitamente os elementos para conversão
|
|
66
|
+
*/
|
|
67
|
+
const FORM_ELEMENT_PREFIX_PATTERN = /^\$(input|textarea|select|checkbox|radio|button|label):/i;
|
|
68
|
+
/**
|
|
69
|
+
* Verifica se um node é um frame válido (FRAME, INSTANCE ou COMPONENT)
|
|
70
|
+
*/
|
|
71
|
+
function isValidFrameNode(node) {
|
|
72
|
+
return node.type === 'FRAME' || node.type === 'INSTANCE' || node.type === 'COMPONENT';
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Procura por texto nos filhos (pode estar em qualquer nível)
|
|
76
|
+
*/
|
|
77
|
+
function hasTextChild(node) {
|
|
78
|
+
if (node.type === 'TEXT')
|
|
79
|
+
return true;
|
|
80
|
+
if (node.children) {
|
|
81
|
+
return node.children.some(child => hasTextChild(child));
|
|
82
|
+
}
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Extrai o primeiro texto encontrado nos filhos
|
|
87
|
+
*/
|
|
88
|
+
function extractFirstText(node) {
|
|
89
|
+
function findFirstText(n) {
|
|
90
|
+
if (n.type === 'TEXT' && n.characters) {
|
|
91
|
+
return n.characters;
|
|
92
|
+
}
|
|
93
|
+
if (n.children) {
|
|
94
|
+
for (const child of n.children) {
|
|
95
|
+
const text = findFirstText(child);
|
|
96
|
+
if (text)
|
|
97
|
+
return text;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
return findFirstText(node) || '';
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Extrai todos os textos encontrados nos filhos (para opções de select)
|
|
106
|
+
*/
|
|
107
|
+
function extractAllTexts(node) {
|
|
108
|
+
const texts = [];
|
|
109
|
+
function findTexts(n) {
|
|
110
|
+
if (n.type === 'TEXT' && n.characters) {
|
|
111
|
+
texts.push(n.characters);
|
|
112
|
+
}
|
|
113
|
+
if (n.children) {
|
|
114
|
+
for (const child of n.children) {
|
|
115
|
+
findTexts(child);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
findTexts(node);
|
|
120
|
+
return texts;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Detecta qual tipo de elemento de formulário um node representa
|
|
124
|
+
*
|
|
125
|
+
* IMPORTANTE: A detecção só acontece quando o nome tem prefixo explícito:
|
|
126
|
+
* $input:nome, $textarea:nome, $select:nome, etc.
|
|
127
|
+
*
|
|
128
|
+
* Isso preserva a estrutura visual do Figma por padrão.
|
|
129
|
+
*/
|
|
130
|
+
function detectSimulatedFormElement(node) {
|
|
131
|
+
if (!isValidFrameNode(node)) {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
const name = node.name.trim();
|
|
135
|
+
// Verifica se tem prefixo explícito $tipo:nome
|
|
136
|
+
const match = name.match(FORM_ELEMENT_PREFIX_PATTERN);
|
|
137
|
+
if (!match) {
|
|
138
|
+
// Sem prefixo explícito = não é um form element
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
const elementType = match[1].toLowerCase();
|
|
142
|
+
return elementType;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Extrai o nome limpo do node (remove prefixos especiais como $input:, #, etc.)
|
|
146
|
+
*/
|
|
147
|
+
function extractCleanName(name) {
|
|
148
|
+
// Remove prefixo de form element $tipo:
|
|
149
|
+
let clean = name.replace(FORM_ELEMENT_PREFIX_PATTERN, '');
|
|
150
|
+
// Remove prefixo de exportação #
|
|
151
|
+
clean = clean.replace(/^#/, '');
|
|
152
|
+
// Remove prefixo interno _
|
|
153
|
+
clean = clean.replace(/^_/, '');
|
|
154
|
+
return clean.trim();
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Detecta o tipo específico de input baseado no contexto
|
|
158
|
+
*/
|
|
159
|
+
function detectInputType(node, parentNode) {
|
|
160
|
+
const nodeName = extractCleanName(node.name).toLowerCase();
|
|
161
|
+
const parentName = parentNode ? extractCleanName(parentNode.name).toLowerCase() : '';
|
|
162
|
+
const combinedContext = `${parentName} ${nodeName}`;
|
|
163
|
+
// Detecta por palavras-chave no contexto
|
|
164
|
+
if (combinedContext.includes('senha') || combinedContext.includes('password')) {
|
|
165
|
+
return 'password';
|
|
166
|
+
}
|
|
167
|
+
if (combinedContext.includes('email') || combinedContext.includes('e-mail')) {
|
|
168
|
+
return 'email';
|
|
169
|
+
}
|
|
170
|
+
if (combinedContext.includes('busca') || combinedContext.includes('search') || combinedContext.includes('pesquisa')) {
|
|
171
|
+
return 'search';
|
|
172
|
+
}
|
|
173
|
+
if (combinedContext.includes('telefone') || combinedContext.includes('phone') || combinedContext.includes('tel')) {
|
|
174
|
+
return 'tel';
|
|
175
|
+
}
|
|
176
|
+
if (combinedContext.includes('numero') || combinedContext.includes('number') || combinedContext.includes('quantidade')) {
|
|
177
|
+
return 'number';
|
|
178
|
+
}
|
|
179
|
+
if (combinedContext.includes('url') || combinedContext.includes('website') || combinedContext.includes('site')) {
|
|
180
|
+
return 'url';
|
|
181
|
+
}
|
|
182
|
+
if (combinedContext.includes('data') || combinedContext.includes('date')) {
|
|
183
|
+
return 'date';
|
|
184
|
+
}
|
|
185
|
+
return 'text';
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Detecta o tipo de button baseado no contexto
|
|
189
|
+
*/
|
|
190
|
+
function detectButtonType(node) {
|
|
191
|
+
const name = extractCleanName(node.name).toLowerCase();
|
|
192
|
+
const text = extractFirstText(node).toLowerCase();
|
|
193
|
+
const context = `${name} ${text}`;
|
|
194
|
+
if (context.includes('submit') || context.includes('enviar') || context.includes('entrar') || context.includes('login') || context.includes('cadastrar')) {
|
|
195
|
+
return 'submit';
|
|
196
|
+
}
|
|
197
|
+
if (context.includes('reset') || context.includes('limpar') || context.includes('cancelar')) {
|
|
198
|
+
return 'reset';
|
|
199
|
+
}
|
|
200
|
+
return 'button';
|
|
201
|
+
}
|
|
63
202
|
/**
|
|
64
203
|
* Gera nome seguro para arquivo de asset
|
|
65
204
|
*/
|
|
@@ -85,6 +224,117 @@ function nodeToJSX(node, blockName, depth = 0, parentNode, usedTags) {
|
|
|
85
224
|
const className = isRoot
|
|
86
225
|
? blockName
|
|
87
226
|
: generateBEMElement(blockName, elementName);
|
|
227
|
+
// ============================================================
|
|
228
|
+
// DETECÇÃO DE ELEMENTOS DE FORMULÁRIO SIMULADOS DO FIGMA
|
|
229
|
+
// Frames que representam inputs, selects, textareas, etc.
|
|
230
|
+
// ============================================================
|
|
231
|
+
const formElement = detectSimulatedFormElement(node);
|
|
232
|
+
if (formElement) {
|
|
233
|
+
switch (formElement) {
|
|
234
|
+
case 'input': {
|
|
235
|
+
const placeholder = extractFirstText(node);
|
|
236
|
+
const inputType = detectInputType(node, parentNode);
|
|
237
|
+
const inputProps = { type: inputType };
|
|
238
|
+
if (placeholder) {
|
|
239
|
+
inputProps['placeholder'] = placeholder;
|
|
240
|
+
}
|
|
241
|
+
return {
|
|
242
|
+
tag: 'input',
|
|
243
|
+
className,
|
|
244
|
+
props: inputProps,
|
|
245
|
+
children: [],
|
|
246
|
+
selfClosing: true,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
case 'textarea': {
|
|
250
|
+
const placeholder = extractFirstText(node);
|
|
251
|
+
const textareaProps = { rows: '4' };
|
|
252
|
+
if (placeholder) {
|
|
253
|
+
textareaProps['placeholder'] = placeholder;
|
|
254
|
+
}
|
|
255
|
+
return {
|
|
256
|
+
tag: 'textarea',
|
|
257
|
+
className,
|
|
258
|
+
props: textareaProps,
|
|
259
|
+
children: [],
|
|
260
|
+
selfClosing: false,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
case 'select': {
|
|
264
|
+
// Extrai opções do select (textos dentro dos filhos)
|
|
265
|
+
const options = extractAllTexts(node);
|
|
266
|
+
const selectChildren = [];
|
|
267
|
+
// Adiciona placeholder como primeira opção desabilitada
|
|
268
|
+
if (options.length > 0) {
|
|
269
|
+
selectChildren.push({
|
|
270
|
+
tag: 'option',
|
|
271
|
+
className: '',
|
|
272
|
+
props: { value: '', disabled: 'true', selected: 'true' },
|
|
273
|
+
children: [{ type: 'text', value: options[0] }],
|
|
274
|
+
selfClosing: false,
|
|
275
|
+
});
|
|
276
|
+
// Adiciona outras opções
|
|
277
|
+
for (let i = 1; i < options.length; i++) {
|
|
278
|
+
selectChildren.push({
|
|
279
|
+
tag: 'option',
|
|
280
|
+
className: '',
|
|
281
|
+
props: { value: options[i].toLowerCase().replace(/\s+/g, '-') },
|
|
282
|
+
children: [{ type: 'text', value: options[i] }],
|
|
283
|
+
selfClosing: false,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return {
|
|
288
|
+
tag: 'select',
|
|
289
|
+
className,
|
|
290
|
+
props: {},
|
|
291
|
+
children: selectChildren,
|
|
292
|
+
selfClosing: false,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
case 'checkbox': {
|
|
296
|
+
const labelText = extractFirstText(node);
|
|
297
|
+
return {
|
|
298
|
+
tag: 'input',
|
|
299
|
+
className,
|
|
300
|
+
props: { type: 'checkbox' },
|
|
301
|
+
children: [],
|
|
302
|
+
selfClosing: true,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
case 'radio': {
|
|
306
|
+
return {
|
|
307
|
+
tag: 'input',
|
|
308
|
+
className,
|
|
309
|
+
props: { type: 'radio' },
|
|
310
|
+
children: [],
|
|
311
|
+
selfClosing: true,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
case 'button': {
|
|
315
|
+
const buttonText = extractFirstText(node);
|
|
316
|
+
const buttonType = detectButtonType(node);
|
|
317
|
+
return {
|
|
318
|
+
tag: 'button',
|
|
319
|
+
className,
|
|
320
|
+
props: { type: buttonType },
|
|
321
|
+
children: [{ type: 'text', value: buttonText }],
|
|
322
|
+
selfClosing: false,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
case 'label': {
|
|
326
|
+
const textChild = node.children?.find(child => child.type === 'TEXT');
|
|
327
|
+
const labelText = textChild?.characters || '';
|
|
328
|
+
return {
|
|
329
|
+
tag: 'label',
|
|
330
|
+
className,
|
|
331
|
+
props: {},
|
|
332
|
+
children: [{ type: 'text', value: labelText }],
|
|
333
|
+
selfClosing: false,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
88
338
|
// Detecta a tag HTML semântica apropriada (passa usedTags para evitar duplicação de <main>)
|
|
89
339
|
const tag = detectSemanticTag(node, parentNode, depth, tagsSet);
|
|
90
340
|
// Obtém atributos semânticos (href, type, etc.)
|
|
@@ -159,12 +409,14 @@ function collectStyles(node, blockName, depth = 0, parentNode) {
|
|
|
159
409
|
: `.${generateBEMElement(blockName, elementName)}`;
|
|
160
410
|
// Coleta propriedades deste node
|
|
161
411
|
const properties = [];
|
|
412
|
+
// Verifica se o pai tem Auto Layout (para aplicar flex-shrink nos filhos)
|
|
413
|
+
const parentHasAutoLayoutMode = parentNode ? hasAutoLayout(parentNode) : false;
|
|
162
414
|
// Layout e sizing
|
|
163
415
|
// Para FRAME, COMPONENT, INSTANCE: extrai layout completo (flex, gap, etc)
|
|
164
416
|
// Para GROUP: extrai sizing e position: relative (container sem flex)
|
|
165
417
|
// Para outros tipos (RECTANGLE, etc): extrai apenas sizing (width, height)
|
|
166
418
|
if (node.type === 'FRAME' || node.type === 'COMPONENT' || node.type === 'INSTANCE') {
|
|
167
|
-
properties.push(...parseLayoutAndSizing(node, isRoot));
|
|
419
|
+
properties.push(...parseLayoutAndSizing(node, isRoot, parentHasAutoLayoutMode));
|
|
168
420
|
// Se é um container sem Auto Layout, adiciona position: relative
|
|
169
421
|
const containerPos = parseContainerPosition(node);
|
|
170
422
|
if (containerPos) {
|
|
@@ -181,7 +433,7 @@ function collectStyles(node, blockName, depth = 0, parentNode) {
|
|
|
181
433
|
}
|
|
182
434
|
else if (node.type === 'GROUP') {
|
|
183
435
|
// GROUPs são containers visuais - precisam de position: relative e sizing
|
|
184
|
-
const sizingProps = parseSizing(node);
|
|
436
|
+
const sizingProps = parseSizing(node, false, parentHasAutoLayoutMode);
|
|
185
437
|
const sizingArray = Object.values(sizingProps).filter((p) => p !== undefined);
|
|
186
438
|
properties.push(...sizingArray);
|
|
187
439
|
// GROUP sempre precisa de position: relative se tiver filhos
|
|
@@ -194,7 +446,7 @@ function collectStyles(node, blockName, depth = 0, parentNode) {
|
|
|
194
446
|
}
|
|
195
447
|
else if (node.type !== 'TEXT' && node.absoluteBoundingBox) {
|
|
196
448
|
// Para elementos não-texto com dimensões, extrai apenas sizing
|
|
197
|
-
const sizingProps = parseSizing(node);
|
|
449
|
+
const sizingProps = parseSizing(node, false, parentHasAutoLayoutMode);
|
|
198
450
|
const sizingArray = Object.values(sizingProps).filter((p) => p !== undefined);
|
|
199
451
|
properties.push(...sizingArray);
|
|
200
452
|
}
|
|
@@ -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;
|
|
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;AA6BD;;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"}
|
|
@@ -13,12 +13,20 @@ function roundPixelValue(value) {
|
|
|
13
13
|
return Math.round(value * 10) / 10;
|
|
14
14
|
}
|
|
15
15
|
/**
|
|
16
|
-
* Formata valor de pixel arredondado
|
|
16
|
+
* Formata valor de pixel arredondado (suporta valores negativos)
|
|
17
17
|
*/
|
|
18
18
|
function formatPixels(value) {
|
|
19
19
|
const rounded = roundPixelValue(value);
|
|
20
20
|
return `${rounded}px`;
|
|
21
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Formata valor de pixel para CSS, incluindo valores negativos
|
|
24
|
+
* Valores negativos são importantes para elementos que "vazam" do container
|
|
25
|
+
*/
|
|
26
|
+
function formatPixelsWithNegative(value) {
|
|
27
|
+
const rounded = roundPixelValue(value);
|
|
28
|
+
return `${rounded}px`;
|
|
29
|
+
}
|
|
22
30
|
/**
|
|
23
31
|
* Verifica se um node pai tem Auto Layout
|
|
24
32
|
*/
|
|
@@ -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;AAuRrD;;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;AAgJD;;;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,33 +173,47 @@ 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
|
|
187
|
+
// NOTA: Patterns de form elements são desativados por padrão para preservar a estrutura visual.
|
|
188
|
+
// Use prefixo $input:, $label:, etc. no Figma para ativar a conversão.
|
|
189
|
+
// Os patterns abaixo são mantidos apenas para elementos claramente marcados.
|
|
186
190
|
form: /^(form|formulario|login-form|signup-form|contact-form|search-form)/i,
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
191
|
+
// input/label/select/etc NÃO são mais detectados automaticamente
|
|
192
|
+
// para evitar converter estruturas visuais em elementos HTML
|
|
193
|
+
input: /^(\$input:|never-match-automatically)/i, // Desativado - use $input:nome
|
|
194
|
+
label: /^(\$label:|never-match-automatically)/i, // Desativado - use $label:nome
|
|
195
|
+
select: /^(\$select:|never-match-automatically)/i, // Desativado - use $select:nome
|
|
196
|
+
textarea: /^(\$textarea:|never-match-automatically)/i, // Desativado - use $textarea:nome
|
|
197
|
+
checkbox: /^(\$checkbox:|never-match-automatically)/i, // Desativado - use $checkbox:nome
|
|
198
|
+
radio: /^(\$radio:|never-match-automatically)/i, // Desativado - use $radio:nome
|
|
193
199
|
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
|
-
|
|
200
|
+
// Navegação - expandido para mais variações
|
|
201
|
+
nav: /^(nav|navigation|navbar|menu|navegacao|breadcrumb|tabs|pagination|nav-bar|top-nav|main-nav|side-nav)/i,
|
|
202
|
+
// Seções estruturais - EXPANDIDO para detectar mais variações
|
|
203
|
+
// Header: topo da página, barra superior, cabeçalho
|
|
204
|
+
header: /^(header|cabecalho|top-?bar|topbar|app-?bar|page-?header|site-?header|main-?header)/i,
|
|
205
|
+
// Footer: rodapé, barra inferior
|
|
206
|
+
footer: /^(footer|rodape|bottom-?bar|bottom-?nav|page-?footer|site-?footer|main-?footer)/i,
|
|
207
|
+
// Main: conteúdo principal da página
|
|
208
|
+
main: /^(main|content|principal|conteudo|page-?content|main-?content|body-?content|central|centro)/i,
|
|
209
|
+
// Section: seções genéricas
|
|
210
|
+
section: /^(section|secao|block|grupo|area|zone|region|wrapper|container)$/i,
|
|
211
|
+
// Article: artigos, posts, cards de conteúdo
|
|
212
|
+
article: /^(article|artigo|post|blog-?post|news|noticia|story)/i,
|
|
213
|
+
// Aside: conteúdo lateral, complementar
|
|
214
|
+
aside: /^(aside|sidebar|lateral|barra-?lateral|complementar|side-?panel|side-?content)/i,
|
|
215
|
+
// Container/Wrapper - novo pattern para containers genéricos
|
|
216
|
+
container: /^(container|wrapper|box|caixa|holder|frame|panel|painel|card-?shadow|card-?container)/i,
|
|
203
217
|
// Imagens e mídia
|
|
204
218
|
image: /^(image|img|photo|picture|imagem|foto|thumbnail|banner)/i,
|
|
205
219
|
avatar: /^(avatar|profile-pic|user-image|foto-perfil)/i,
|
|
@@ -249,6 +263,47 @@ export function normalizeName(name) {
|
|
|
249
263
|
});
|
|
250
264
|
return translatedWords.join('-');
|
|
251
265
|
}
|
|
266
|
+
/**
|
|
267
|
+
* Detecta se um node é um elemento estrutural baseado em sua posição e características
|
|
268
|
+
* Isso complementa a detecção por nome quando o designer não nomeia corretamente
|
|
269
|
+
*/
|
|
270
|
+
function detectStructuralByPosition(node, parentNode, depth = 0) {
|
|
271
|
+
// Só detecta por posição em níveis próximos do root (depth 1 ou 2)
|
|
272
|
+
if (depth > 2 || !parentNode) {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
const siblings = parentNode.children;
|
|
276
|
+
if (!siblings || siblings.length < 2) {
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
// Encontra índice do node atual entre os irmãos visíveis
|
|
280
|
+
const visibleSiblings = siblings.filter(s => s.visible !== false && !s.name.startsWith('_'));
|
|
281
|
+
const nodeIndex = visibleSiblings.findIndex(s => s.id === node.id);
|
|
282
|
+
if (nodeIndex === -1) {
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
// Verifica características do node
|
|
286
|
+
const box = node.absoluteBoundingBox;
|
|
287
|
+
const parentBox = parentNode.absoluteBoundingBox;
|
|
288
|
+
if (!box || !parentBox) {
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
// Altura relativa ao pai
|
|
292
|
+
const heightRatio = box.height / parentBox.height;
|
|
293
|
+
// Primeiro elemento visível com altura pequena (< 15% do pai) = possível header
|
|
294
|
+
if (nodeIndex === 0 && heightRatio < 0.15) {
|
|
295
|
+
return 'header';
|
|
296
|
+
}
|
|
297
|
+
// Último elemento visível com altura pequena (< 10% do pai) = possível footer
|
|
298
|
+
if (nodeIndex === visibleSiblings.length - 1 && heightRatio < 0.10) {
|
|
299
|
+
return 'footer';
|
|
300
|
+
}
|
|
301
|
+
// Elemento do meio com maior altura = possível main
|
|
302
|
+
if (nodeIndex > 0 && nodeIndex < visibleSiblings.length - 1 && heightRatio > 0.5) {
|
|
303
|
+
return 'main';
|
|
304
|
+
}
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
252
307
|
/**
|
|
253
308
|
* Detecta a tag HTML semântica apropriada
|
|
254
309
|
* @param usedTags - Set de tags já usadas na hierarquia (para evitar duplicação de <main>)
|
|
@@ -331,6 +386,12 @@ export function detectSemanticTag(node, parentNode, _depth = 0, usedTags) {
|
|
|
331
386
|
if (SEMANTIC_PATTERNS.aside.test(normalizedName)) {
|
|
332
387
|
return 'aside';
|
|
333
388
|
}
|
|
389
|
+
// Container - mapeia para div mas com semântica de container
|
|
390
|
+
// Containers são divs que agrupam conteúdo, não têm tag semântica específica
|
|
391
|
+
// mas são importantes para layout
|
|
392
|
+
if (SEMANTIC_PATTERNS.container.test(normalizedName)) {
|
|
393
|
+
return 'div';
|
|
394
|
+
}
|
|
334
395
|
// Mídia
|
|
335
396
|
if (SEMANTIC_PATTERNS.figure.test(normalizedName)) {
|
|
336
397
|
return 'figure';
|
|
@@ -393,6 +454,22 @@ export function detectSemanticTag(node, parentNode, _depth = 0, usedTags) {
|
|
|
393
454
|
if (SEMANTIC_PATTERNS.alert.test(normalizedName)) {
|
|
394
455
|
return 'div'; // com role="alert"
|
|
395
456
|
}
|
|
457
|
+
// Tenta detectar por posição/estrutura se não encontrou por nome
|
|
458
|
+
const structuralTag = detectStructuralByPosition(node, parentNode, _depth);
|
|
459
|
+
if (structuralTag) {
|
|
460
|
+
// Verifica se já existe a tag (evita duplicação)
|
|
461
|
+
if (structuralTag === 'main' && usedTags?.has('main')) {
|
|
462
|
+
return 'section';
|
|
463
|
+
}
|
|
464
|
+
if (structuralTag === 'header' && usedTags?.has('header')) {
|
|
465
|
+
return 'div';
|
|
466
|
+
}
|
|
467
|
+
if (structuralTag === 'footer' && usedTags?.has('footer')) {
|
|
468
|
+
return 'div';
|
|
469
|
+
}
|
|
470
|
+
usedTags?.add(structuralTag);
|
|
471
|
+
return structuralTag;
|
|
472
|
+
}
|
|
396
473
|
// Default
|
|
397
474
|
return 'div';
|
|
398
475
|
}
|
|
@@ -414,8 +491,9 @@ function detectTextTag(node, parentNode, normalizedName) {
|
|
|
414
491
|
if (fontSize >= 24 && fontWeight >= 600) {
|
|
415
492
|
return getHeadingLevel(fontSize);
|
|
416
493
|
}
|
|
417
|
-
// Labels
|
|
418
|
-
|
|
494
|
+
// Labels - só detecta com prefixo explícito $label:
|
|
495
|
+
// NÃO detecta automaticamente para preservar estrutura visual
|
|
496
|
+
if (name.startsWith('$label:')) {
|
|
419
497
|
return 'label';
|
|
420
498
|
}
|
|
421
499
|
// Links - mas não se o pai já é um link (evita <a> aninhado)
|
|
@@ -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.23",
|
|
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.23"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@types/node": "^20.0.0",
|