@promptui-lib/figma-parser 0.1.8 → 0.1.10
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 +2 -0
- package/dist/parser/index.d.ts.map +1 -1
- package/dist/parser/index.js +1 -0
- package/dist/parser/layout-parser.d.ts.map +1 -1
- package/dist/parser/layout-parser.js +39 -2
- package/dist/parser/node-parser.d.ts.map +1 -1
- package/dist/parser/node-parser.js +38 -5
- package/dist/parser/position-parser.d.ts +32 -0
- package/dist/parser/position-parser.d.ts.map +1 -0
- package/dist/parser/position-parser.js +196 -0
- package/dist/parser/semantic-detector.d.ts.map +1 -1
- package/dist/parser/semantic-detector.js +17 -6
- package/dist/parser/style-parser.d.ts +1 -0
- package/dist/parser/style-parser.d.ts.map +1 -1
- package/dist/parser/style-parser.js +24 -3
- package/package.json +2 -2
package/dist/parser/index.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ export { parseStyles, parseBackgroundColor, parseBorder, parseBorderRadius, pars
|
|
|
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';
|
|
8
|
+
export type { IPositionProperties } from './position-parser.js';
|
|
7
9
|
export { parseNode, parseNodes, parseFrameName } from './node-parser.js';
|
|
8
10
|
export type { IParseOptions } from './node-parser.js';
|
|
9
11
|
export { detectSemanticTag, normalizeName, getSemanticAttributes } from './semantic-detector.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;AACpF,YAAY,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAE5D,OAAO,EAAE,WAAW,EAAE,oBAAoB,EAAE,WAAW,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACtH,YAAY,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAE1D,OAAO,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AACvE,YAAY,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAExD,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACzE,YAAY,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEtD,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/parser/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AACpF,YAAY,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAE5D,OAAO,EAAE,WAAW,EAAE,oBAAoB,EAAE,WAAW,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACtH,YAAY,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAE1D,OAAO,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AACvE,YAAY,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAExD,OAAO,EAAE,aAAa,EAAE,sBAAsB,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAC5F,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,6 @@
|
|
|
1
1
|
export { parseLayout, parseSizing, parseLayoutAndSizing } from './layout-parser.js';
|
|
2
2
|
export { parseStyles, parseBackgroundColor, parseBorder, parseBorderRadius, parseBoxShadow } from './style-parser.js';
|
|
3
3
|
export { parseTextStyles, extractTextContent } from './text-parser.js';
|
|
4
|
+
export { parsePosition, parseContainerPosition, hasAutoLayout } from './position-parser.js';
|
|
4
5
|
export { parseNode, parseNodes, parseFrameName } from './node-parser.js';
|
|
5
6
|
export { detectSemanticTag, normalizeName, getSemanticAttributes } from './semantic-detector.js';
|
|
@@ -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;
|
|
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,WAAW,CAAC,IAAI,EAAE,UAAU,GAAG,iBAAiB,CA2F/D;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,UAAU,GAAG,iBAAiB,CAoE/D;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,UAAU,GAAG,cAAc,EAAE,CASvE"}
|
|
@@ -3,6 +3,29 @@
|
|
|
3
3
|
* Converte AutoLayout do Figma para propriedades CSS flexbox
|
|
4
4
|
*/
|
|
5
5
|
import { findGapToken, generatePaddingToken } from '@promptui-lib/core';
|
|
6
|
+
/**
|
|
7
|
+
* Arredonda valores de pixel para evitar decimais desnecessários
|
|
8
|
+
* Valores inteiros ou próximos de .5 são preservados
|
|
9
|
+
*/
|
|
10
|
+
function roundPixelValue(value) {
|
|
11
|
+
// Se é inteiro ou muito próximo de inteiro, retorna inteiro
|
|
12
|
+
if (Math.abs(value - Math.round(value)) < 0.01) {
|
|
13
|
+
return Math.round(value);
|
|
14
|
+
}
|
|
15
|
+
// Arredonda para 1 casa decimal no máximo
|
|
16
|
+
return Math.round(value * 10) / 10;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Formata valor de pixel arredondado
|
|
20
|
+
*/
|
|
21
|
+
function formatPixels(value) {
|
|
22
|
+
const rounded = roundPixelValue(value);
|
|
23
|
+
// Se é inteiro, não mostra decimal
|
|
24
|
+
if (Number.isInteger(rounded)) {
|
|
25
|
+
return `${rounded}px`;
|
|
26
|
+
}
|
|
27
|
+
return `${rounded}px`;
|
|
28
|
+
}
|
|
6
29
|
/**
|
|
7
30
|
* Parseia AutoLayout de um node
|
|
8
31
|
*/
|
|
@@ -103,12 +126,19 @@ export function parseSizing(node) {
|
|
|
103
126
|
if (node.absoluteBoundingBox?.width) {
|
|
104
127
|
properties.width = {
|
|
105
128
|
property: 'width',
|
|
106
|
-
value:
|
|
129
|
+
value: formatPixels(node.absoluteBoundingBox.width),
|
|
107
130
|
};
|
|
108
131
|
}
|
|
109
132
|
break;
|
|
110
133
|
}
|
|
111
134
|
}
|
|
135
|
+
else if (node.absoluteBoundingBox?.width) {
|
|
136
|
+
// Fallback: Se não tem layoutSizing mas tem dimensões, usa o tamanho absoluto
|
|
137
|
+
properties.width = {
|
|
138
|
+
property: 'width',
|
|
139
|
+
value: formatPixels(node.absoluteBoundingBox.width),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
112
142
|
// Height
|
|
113
143
|
if (node.layoutSizingVertical) {
|
|
114
144
|
switch (node.layoutSizingVertical) {
|
|
@@ -128,12 +158,19 @@ export function parseSizing(node) {
|
|
|
128
158
|
if (node.absoluteBoundingBox?.height) {
|
|
129
159
|
properties.height = {
|
|
130
160
|
property: 'height',
|
|
131
|
-
value:
|
|
161
|
+
value: formatPixels(node.absoluteBoundingBox.height),
|
|
132
162
|
};
|
|
133
163
|
}
|
|
134
164
|
break;
|
|
135
165
|
}
|
|
136
166
|
}
|
|
167
|
+
else if (node.absoluteBoundingBox?.height) {
|
|
168
|
+
// Fallback: Se não tem layoutSizing mas tem dimensões, usa o tamanho absoluto
|
|
169
|
+
properties.height = {
|
|
170
|
+
property: 'height',
|
|
171
|
+
value: formatPixels(node.absoluteBoundingBox.height),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
137
174
|
return properties;
|
|
138
175
|
}
|
|
139
176
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"node-parser.d.ts","sourceRoot":"","sources":["../../src/parser/node-parser.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EACV,UAAU,EACV,aAAa,EAKb,cAAc,EACd,gBAAgB,EACjB,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"node-parser.d.ts","sourceRoot":"","sources":["../../src/parser/node-parser.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EACV,UAAU,EACV,aAAa,EAKb,cAAc,EACd,gBAAgB,EACjB,MAAM,oBAAoB,CAAC;AAgB5B,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;AA6LD;;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,10 +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, } from '@promptui-lib/core';
|
|
6
|
-
import { parseLayoutAndSizing } from './layout-parser.js';
|
|
6
|
+
import { parseLayoutAndSizing, parseSizing } 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
11
|
/**
|
|
11
12
|
* Parseia o nome do frame para extrair informações
|
|
12
13
|
*/
|
|
@@ -75,7 +76,7 @@ function nodeToJSX(node, blockName, depth = 0, parentNode) {
|
|
|
75
76
|
/**
|
|
76
77
|
* Coleta estilos de um node recursivamente
|
|
77
78
|
*/
|
|
78
|
-
function collectStyles(node, blockName, depth = 0) {
|
|
79
|
+
function collectStyles(node, blockName, depth = 0, parentNode) {
|
|
79
80
|
const blocks = [];
|
|
80
81
|
const isRoot = depth === 0;
|
|
81
82
|
// Usa nome normalizado (traduzido)
|
|
@@ -86,9 +87,41 @@ function collectStyles(node, blockName, depth = 0) {
|
|
|
86
87
|
: `.${generateBEMElement(blockName, elementName)}`;
|
|
87
88
|
// Coleta propriedades deste node
|
|
88
89
|
const properties = [];
|
|
89
|
-
// Layout
|
|
90
|
+
// Layout e sizing
|
|
91
|
+
// Para FRAME, COMPONENT, INSTANCE: extrai layout completo (flex, gap, etc)
|
|
92
|
+
// Para GROUP: extrai sizing e position: relative (container sem flex)
|
|
93
|
+
// Para outros tipos (RECTANGLE, etc): extrai apenas sizing (width, height)
|
|
90
94
|
if (node.type === 'FRAME' || node.type === 'COMPONENT' || node.type === 'INSTANCE') {
|
|
91
95
|
properties.push(...parseLayoutAndSizing(node));
|
|
96
|
+
// Se é um container sem Auto Layout, adiciona position: relative
|
|
97
|
+
const containerPos = parseContainerPosition(node);
|
|
98
|
+
if (containerPos) {
|
|
99
|
+
properties.push(containerPos);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
else if (node.type === 'GROUP') {
|
|
103
|
+
// GROUPs são containers visuais - precisam de position: relative e sizing
|
|
104
|
+
const sizingProps = parseSizing(node);
|
|
105
|
+
const sizingArray = Object.values(sizingProps).filter((p) => p !== undefined);
|
|
106
|
+
properties.push(...sizingArray);
|
|
107
|
+
// GROUP sempre precisa de position: relative se tiver filhos
|
|
108
|
+
if (node.children && node.children.length > 0) {
|
|
109
|
+
properties.push({
|
|
110
|
+
property: 'position',
|
|
111
|
+
value: 'relative',
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
else if (node.type !== 'TEXT' && node.absoluteBoundingBox) {
|
|
116
|
+
// Para elementos não-texto com dimensões, extrai apenas sizing
|
|
117
|
+
const sizingProps = parseSizing(node);
|
|
118
|
+
const sizingArray = Object.values(sizingProps).filter((p) => p !== undefined);
|
|
119
|
+
properties.push(...sizingArray);
|
|
120
|
+
}
|
|
121
|
+
// Posicionamento absoluto (quando pai não tem Auto Layout)
|
|
122
|
+
if (parentNode && !isRoot) {
|
|
123
|
+
const positionProps = parsePosition(node, parentNode);
|
|
124
|
+
properties.push(...positionProps);
|
|
92
125
|
}
|
|
93
126
|
// Estilos visuais
|
|
94
127
|
properties.push(...parseStyles(node));
|
|
@@ -103,13 +136,13 @@ function collectStyles(node, blockName, depth = 0) {
|
|
|
103
136
|
properties,
|
|
104
137
|
});
|
|
105
138
|
}
|
|
106
|
-
// Processa filhos
|
|
139
|
+
// Processa filhos (passa o node atual como pai)
|
|
107
140
|
if (node.children) {
|
|
108
141
|
for (const child of node.children) {
|
|
109
142
|
if (child.visible === false || child.name.startsWith('_')) {
|
|
110
143
|
continue;
|
|
111
144
|
}
|
|
112
|
-
blocks.push(...collectStyles(child, blockName, depth + 1));
|
|
145
|
+
blocks.push(...collectStyles(child, blockName, depth + 1, node));
|
|
113
146
|
}
|
|
114
147
|
}
|
|
115
148
|
return blocks;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Position Parser
|
|
3
|
+
* Converte posicionamento e constraints do Figma para CSS
|
|
4
|
+
* Usado quando elementos não estão dentro de Auto Layout
|
|
5
|
+
*/
|
|
6
|
+
import type { IFigmaNode, IStyleProperty } from '@promptui-lib/core';
|
|
7
|
+
export interface IPositionProperties {
|
|
8
|
+
position?: IStyleProperty;
|
|
9
|
+
top?: IStyleProperty;
|
|
10
|
+
right?: IStyleProperty;
|
|
11
|
+
bottom?: IStyleProperty;
|
|
12
|
+
left?: IStyleProperty;
|
|
13
|
+
transform?: IStyleProperty;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Verifica se um node pai tem Auto Layout
|
|
17
|
+
*/
|
|
18
|
+
export declare function hasAutoLayout(node: IFigmaNode): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Parseia constraints e posição de um node
|
|
21
|
+
*/
|
|
22
|
+
export declare function parsePosition(node: IFigmaNode, parentNode?: IFigmaNode): IStyleProperty[];
|
|
23
|
+
/**
|
|
24
|
+
* Verifica se o pai precisa de position: relative
|
|
25
|
+
* (necessário quando tem filhos com position: absolute)
|
|
26
|
+
*/
|
|
27
|
+
export declare function needsRelativePosition(node: IFigmaNode): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Parseia position: relative para containers sem Auto Layout
|
|
30
|
+
*/
|
|
31
|
+
export declare function parseContainerPosition(node: IFigmaNode): IStyleProperty | null;
|
|
32
|
+
//# sourceMappingURL=position-parser.d.ts.map
|
|
@@ -0,0 +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,CAmIlB;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAM/D;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,UAAU,GAAG,cAAc,GAAG,IAAI,CAQ9E"}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Position Parser
|
|
3
|
+
* Converte posicionamento e constraints do Figma para CSS
|
|
4
|
+
* Usado quando elementos não estão dentro de Auto Layout
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Arredonda valores de pixel para evitar decimais desnecessários
|
|
8
|
+
*/
|
|
9
|
+
function roundPixelValue(value) {
|
|
10
|
+
if (Math.abs(value - Math.round(value)) < 0.01) {
|
|
11
|
+
return Math.round(value);
|
|
12
|
+
}
|
|
13
|
+
return Math.round(value * 10) / 10;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Formata valor de pixel arredondado
|
|
17
|
+
*/
|
|
18
|
+
function formatPixels(value) {
|
|
19
|
+
const rounded = roundPixelValue(value);
|
|
20
|
+
return `${rounded}px`;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Verifica se um node pai tem Auto Layout
|
|
24
|
+
*/
|
|
25
|
+
export function hasAutoLayout(node) {
|
|
26
|
+
return node.layoutMode !== undefined && node.layoutMode !== 'NONE';
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Calcula a posição relativa de um filho em relação ao pai
|
|
30
|
+
*/
|
|
31
|
+
function calculateRelativePosition(child, parent) {
|
|
32
|
+
if (!child.absoluteBoundingBox || !parent.absoluteBoundingBox) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
const childBox = child.absoluteBoundingBox;
|
|
36
|
+
const parentBox = parent.absoluteBoundingBox;
|
|
37
|
+
return {
|
|
38
|
+
top: childBox.y - parentBox.y,
|
|
39
|
+
left: childBox.x - parentBox.x,
|
|
40
|
+
right: parentBox.x + parentBox.width - (childBox.x + childBox.width),
|
|
41
|
+
bottom: parentBox.y + parentBox.height - (childBox.y + childBox.height),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Parseia constraints e posição de um node
|
|
46
|
+
*/
|
|
47
|
+
export function parsePosition(node, parentNode) {
|
|
48
|
+
const properties = [];
|
|
49
|
+
// Se não tem pai ou o pai tem Auto Layout, não precisa de posicionamento absoluto
|
|
50
|
+
if (!parentNode || hasAutoLayout(parentNode)) {
|
|
51
|
+
return properties;
|
|
52
|
+
}
|
|
53
|
+
// Calcula posição relativa
|
|
54
|
+
const relPos = calculateRelativePosition(node, parentNode);
|
|
55
|
+
if (!relPos) {
|
|
56
|
+
return properties;
|
|
57
|
+
}
|
|
58
|
+
const constraints = node.constraints;
|
|
59
|
+
// Se é um elemento dentro de um container sem Auto Layout, usa posição absoluta
|
|
60
|
+
properties.push({
|
|
61
|
+
property: 'position',
|
|
62
|
+
value: 'absolute',
|
|
63
|
+
});
|
|
64
|
+
// Aplica constraints ou usa posição padrão (top-left)
|
|
65
|
+
if (constraints) {
|
|
66
|
+
// Horizontal constraints
|
|
67
|
+
switch (constraints.horizontal) {
|
|
68
|
+
case 'LEFT':
|
|
69
|
+
properties.push({
|
|
70
|
+
property: 'left',
|
|
71
|
+
value: formatPixels(relPos.left),
|
|
72
|
+
});
|
|
73
|
+
break;
|
|
74
|
+
case 'RIGHT':
|
|
75
|
+
properties.push({
|
|
76
|
+
property: 'right',
|
|
77
|
+
value: formatPixels(relPos.right),
|
|
78
|
+
});
|
|
79
|
+
break;
|
|
80
|
+
case 'CENTER':
|
|
81
|
+
properties.push({
|
|
82
|
+
property: 'left',
|
|
83
|
+
value: '50%',
|
|
84
|
+
});
|
|
85
|
+
properties.push({
|
|
86
|
+
property: 'transform',
|
|
87
|
+
value: 'translateX(-50%)',
|
|
88
|
+
});
|
|
89
|
+
break;
|
|
90
|
+
case 'LEFT_RIGHT':
|
|
91
|
+
properties.push({
|
|
92
|
+
property: 'left',
|
|
93
|
+
value: formatPixels(relPos.left),
|
|
94
|
+
});
|
|
95
|
+
properties.push({
|
|
96
|
+
property: 'right',
|
|
97
|
+
value: formatPixels(relPos.right),
|
|
98
|
+
});
|
|
99
|
+
break;
|
|
100
|
+
case 'SCALE':
|
|
101
|
+
// Usa porcentagem
|
|
102
|
+
const parentWidth = parentNode.absoluteBoundingBox?.width ?? 1;
|
|
103
|
+
const leftPercent = (relPos.left / parentWidth) * 100;
|
|
104
|
+
properties.push({
|
|
105
|
+
property: 'left',
|
|
106
|
+
value: `${roundPixelValue(leftPercent)}%`,
|
|
107
|
+
});
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
// Vertical constraints
|
|
111
|
+
switch (constraints.vertical) {
|
|
112
|
+
case 'TOP':
|
|
113
|
+
properties.push({
|
|
114
|
+
property: 'top',
|
|
115
|
+
value: formatPixels(relPos.top),
|
|
116
|
+
});
|
|
117
|
+
break;
|
|
118
|
+
case 'BOTTOM':
|
|
119
|
+
properties.push({
|
|
120
|
+
property: 'bottom',
|
|
121
|
+
value: formatPixels(relPos.bottom),
|
|
122
|
+
});
|
|
123
|
+
break;
|
|
124
|
+
case 'CENTER':
|
|
125
|
+
// Se já tem transform de X, combina
|
|
126
|
+
const existingTransform = properties.find(p => p.property === 'transform');
|
|
127
|
+
if (existingTransform) {
|
|
128
|
+
existingTransform.value = 'translate(-50%, -50%)';
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
properties.push({
|
|
132
|
+
property: 'transform',
|
|
133
|
+
value: 'translateY(-50%)',
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
properties.push({
|
|
137
|
+
property: 'top',
|
|
138
|
+
value: '50%',
|
|
139
|
+
});
|
|
140
|
+
break;
|
|
141
|
+
case 'TOP_BOTTOM':
|
|
142
|
+
properties.push({
|
|
143
|
+
property: 'top',
|
|
144
|
+
value: formatPixels(relPos.top),
|
|
145
|
+
});
|
|
146
|
+
properties.push({
|
|
147
|
+
property: 'bottom',
|
|
148
|
+
value: formatPixels(relPos.bottom),
|
|
149
|
+
});
|
|
150
|
+
break;
|
|
151
|
+
case 'SCALE':
|
|
152
|
+
const parentHeight = parentNode.absoluteBoundingBox?.height ?? 1;
|
|
153
|
+
const topPercent = (relPos.top / parentHeight) * 100;
|
|
154
|
+
properties.push({
|
|
155
|
+
property: 'top',
|
|
156
|
+
value: `${roundPixelValue(topPercent)}%`,
|
|
157
|
+
});
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
// Sem constraints, usa posição top-left padrão
|
|
163
|
+
properties.push({
|
|
164
|
+
property: 'top',
|
|
165
|
+
value: formatPixels(relPos.top),
|
|
166
|
+
});
|
|
167
|
+
properties.push({
|
|
168
|
+
property: 'left',
|
|
169
|
+
value: formatPixels(relPos.left),
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
return properties;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Verifica se o pai precisa de position: relative
|
|
176
|
+
* (necessário quando tem filhos com position: absolute)
|
|
177
|
+
*/
|
|
178
|
+
export function needsRelativePosition(node) {
|
|
179
|
+
// Se não tem Auto Layout e tem filhos, precisa de relative
|
|
180
|
+
if (!hasAutoLayout(node) && node.children && node.children.length > 0) {
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Parseia position: relative para containers sem Auto Layout
|
|
187
|
+
*/
|
|
188
|
+
export function parseContainerPosition(node) {
|
|
189
|
+
if (needsRelativePosition(node)) {
|
|
190
|
+
return {
|
|
191
|
+
property: 'position',
|
|
192
|
+
value: 'relative',
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
@@ -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;AAmQrD;;GAEG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAalD;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,UAAU,EAChB,UAAU,CAAC,EAAE,UAAU,EACvB,KAAK,GAAE,MAAU,GAChB,MAAM,CAmKR;
|
|
1
|
+
{"version":3,"file":"semantic-detector.d.ts","sourceRoot":"","sources":["../../src/parser/semantic-detector.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAmQrD;;GAEG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAalD;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,UAAU,EAChB,UAAU,CAAC,EAAE,UAAU,EACvB,KAAK,GAAE,MAAU,GAChB,MAAM,CAmKR;AA4JD;;;GAGG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,UAAU,EAChB,GAAG,EAAE,MAAM,EACX,cAAc,EAAE,MAAM,GACrB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAwKxB"}
|
|
@@ -400,8 +400,15 @@ function detectTextTag(node, parentNode, normalizedName) {
|
|
|
400
400
|
if (SEMANTIC_PATTERNS.label.test(name)) {
|
|
401
401
|
return 'label';
|
|
402
402
|
}
|
|
403
|
-
// Links
|
|
403
|
+
// Links - mas não se o pai já é um link (evita <a> aninhado)
|
|
404
404
|
if (SEMANTIC_PATTERNS.link.test(name)) {
|
|
405
|
+
// Verifica se o pai já é um link
|
|
406
|
+
if (parentNode) {
|
|
407
|
+
const parentName = normalizeName(parentNode.name);
|
|
408
|
+
if (SEMANTIC_PATTERNS.link.test(parentName)) {
|
|
409
|
+
return 'span'; // Texto dentro de link vira span
|
|
410
|
+
}
|
|
411
|
+
}
|
|
405
412
|
return 'a';
|
|
406
413
|
}
|
|
407
414
|
// Citações
|
|
@@ -506,12 +513,16 @@ function hasInputChild(node) {
|
|
|
506
513
|
*/
|
|
507
514
|
export function getSemanticAttributes(node, tag, normalizedName) {
|
|
508
515
|
const attrs = {};
|
|
509
|
-
// Links -
|
|
516
|
+
// Links - adiciona href placeholder para evitar warnings de a11y
|
|
517
|
+
// O desenvolvedor deve preencher o href correto
|
|
510
518
|
if (tag === 'a') {
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
519
|
+
// Gera um href placeholder baseado no nome do link
|
|
520
|
+
const hrefSlug = normalizedName
|
|
521
|
+
.replace(/link$/i, '')
|
|
522
|
+
.replace(/[-\s]+/g, '-')
|
|
523
|
+
.replace(/^-|-$/g, '')
|
|
524
|
+
.toLowerCase();
|
|
525
|
+
attrs['href'] = hrefSlug ? `/${hrefSlug}` : '/';
|
|
515
526
|
}
|
|
516
527
|
// Inputs
|
|
517
528
|
if (tag === 'input') {
|
|
@@ -13,6 +13,7 @@ export interface IStyleProperties {
|
|
|
13
13
|
}
|
|
14
14
|
/**
|
|
15
15
|
* Parseia background color de um node
|
|
16
|
+
* NOTA: Para nós TEXT, o fills representa a cor do texto, não background
|
|
16
17
|
*/
|
|
17
18
|
export declare function parseBackgroundColor(node: IFigmaNode): IStyleProperty | null;
|
|
18
19
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"style-parser.d.ts","sourceRoot":"","sources":["../../src/parser/style-parser.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EACV,UAAU,EAIV,cAAc,EACf,MAAM,oBAAoB,CAAC;
|
|
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,CAmB9D"}
|
|
@@ -3,6 +3,15 @@
|
|
|
3
3
|
* Converte fills, strokes, effects do Figma para CSS
|
|
4
4
|
*/
|
|
5
5
|
import { rgbaToHex, findColorToken, findRadiusToken, findShadowToken, generateBorderRadius, } from '@promptui-lib/core';
|
|
6
|
+
/**
|
|
7
|
+
* Arredonda valores de pixel para evitar decimais desnecessários
|
|
8
|
+
*/
|
|
9
|
+
function roundPixelValue(value) {
|
|
10
|
+
if (Math.abs(value - Math.round(value)) < 0.01) {
|
|
11
|
+
return Math.round(value);
|
|
12
|
+
}
|
|
13
|
+
return Math.round(value * 10) / 10;
|
|
14
|
+
}
|
|
6
15
|
/**
|
|
7
16
|
* Extrai cor de um fill sólido
|
|
8
17
|
*/
|
|
@@ -13,8 +22,13 @@ function extractSolidColor(fill) {
|
|
|
13
22
|
}
|
|
14
23
|
/**
|
|
15
24
|
* Parseia background color de um node
|
|
25
|
+
* NOTA: Para nós TEXT, o fills representa a cor do texto, não background
|
|
16
26
|
*/
|
|
17
27
|
export function parseBackgroundColor(node) {
|
|
28
|
+
// Não aplicar background-color em nós TEXT (fills é a cor do texto)
|
|
29
|
+
if (node.type === 'TEXT') {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
18
32
|
if (!node.fills || node.fills.length === 0) {
|
|
19
33
|
return null;
|
|
20
34
|
}
|
|
@@ -44,9 +58,10 @@ export function parseBorder(node) {
|
|
|
44
58
|
}
|
|
45
59
|
const hex = rgbaToHex(stroke.color.r, stroke.color.g, stroke.color.b, stroke.opacity ?? 1);
|
|
46
60
|
const token = findColorToken(hex);
|
|
61
|
+
const strokeWidth = roundPixelValue(node.strokeWeight);
|
|
47
62
|
return {
|
|
48
63
|
property: 'border',
|
|
49
|
-
value: `${
|
|
64
|
+
value: `${strokeWidth}px solid ${token ?? hex}`,
|
|
50
65
|
token: token ?? undefined,
|
|
51
66
|
};
|
|
52
67
|
}
|
|
@@ -100,9 +115,13 @@ export function parseBoxShadow(node) {
|
|
|
100
115
|
// Gera box-shadow customizado
|
|
101
116
|
const { offset, radius, spread, color } = shadow;
|
|
102
117
|
const hex = rgbaToHex(color.r, color.g, color.b, color.a);
|
|
118
|
+
const x = roundPixelValue(offset.x);
|
|
119
|
+
const y = roundPixelValue(offset.y);
|
|
120
|
+
const r = roundPixelValue(radius);
|
|
121
|
+
const s = roundPixelValue(spread ?? 0);
|
|
103
122
|
return {
|
|
104
123
|
property: 'box-shadow',
|
|
105
|
-
value: `${
|
|
124
|
+
value: `${x}px ${y}px ${r}px ${s}px ${hex}`,
|
|
106
125
|
};
|
|
107
126
|
}
|
|
108
127
|
/**
|
|
@@ -110,9 +129,11 @@ export function parseBoxShadow(node) {
|
|
|
110
129
|
*/
|
|
111
130
|
export function parseOpacity(node) {
|
|
112
131
|
if (node.opacity !== undefined && node.opacity < 1) {
|
|
132
|
+
// Arredonda para 2 casas decimais
|
|
133
|
+
const rounded = Math.round(node.opacity * 100) / 100;
|
|
113
134
|
return {
|
|
114
135
|
property: 'opacity',
|
|
115
|
-
value: `${
|
|
136
|
+
value: `${rounded}`,
|
|
116
137
|
};
|
|
117
138
|
}
|
|
118
139
|
return null;
|
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.10",
|
|
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.10"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@types/node": "^20.0.0",
|