@promptui-lib/figma-parser 0.1.0
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/client/figma-client.d.ts +66 -0
- package/dist/client/figma-client.d.ts.map +1 -0
- package/dist/client/figma-client.js +117 -0
- package/dist/client/index.d.ts +3 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/parser/index.d.ts +9 -0
- package/dist/parser/index.d.ts.map +1 -0
- package/dist/parser/index.js +4 -0
- package/dist/parser/layout-parser.d.ts +30 -0
- package/dist/parser/layout-parser.d.ts.map +1 -0
- package/dist/parser/layout-parser.js +147 -0
- package/dist/parser/node-parser.d.ts +22 -0
- package/dist/parser/node-parser.d.ts.map +1 -0
- package/dist/parser/node-parser.js +186 -0
- package/dist/parser/style-parser.d.ts +38 -0
- package/dist/parser/style-parser.d.ts.map +1 -0
- package/dist/parser/style-parser.js +141 -0
- package/dist/parser/text-parser.d.ts +61 -0
- package/dist/parser/text-parser.d.ts.map +1 -0
- package/dist/parser/text-parser.js +196 -0
- package/package.json +45 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Figma REST API Client
|
|
3
|
+
* Cliente para comunicação com a API do Figma
|
|
4
|
+
*/
|
|
5
|
+
import type { IFigmaNode, IFigmaFile } from '@promptui-lib/core';
|
|
6
|
+
export interface IFigmaClientConfig {
|
|
7
|
+
token: string;
|
|
8
|
+
timeout?: number;
|
|
9
|
+
}
|
|
10
|
+
export interface IGetFileNodesResponse {
|
|
11
|
+
name: string;
|
|
12
|
+
lastModified: string;
|
|
13
|
+
nodes: Record<string, {
|
|
14
|
+
document: IFigmaNode;
|
|
15
|
+
}>;
|
|
16
|
+
}
|
|
17
|
+
export declare class FigmaClient {
|
|
18
|
+
private baseUrl;
|
|
19
|
+
private token;
|
|
20
|
+
private timeout;
|
|
21
|
+
constructor(config: IFigmaClientConfig);
|
|
22
|
+
/**
|
|
23
|
+
* Faz requisição autenticada para a API do Figma
|
|
24
|
+
*/
|
|
25
|
+
private request;
|
|
26
|
+
/**
|
|
27
|
+
* Busca arquivo completo do Figma
|
|
28
|
+
*/
|
|
29
|
+
getFile(fileId: string): Promise<IFigmaFile>;
|
|
30
|
+
/**
|
|
31
|
+
* Busca nodes específicos de um arquivo
|
|
32
|
+
*/
|
|
33
|
+
getFileNodes(fileId: string, nodeIds: string[]): Promise<IGetFileNodesResponse>;
|
|
34
|
+
/**
|
|
35
|
+
* Busca um único node
|
|
36
|
+
*/
|
|
37
|
+
getNode(fileId: string, nodeId: string): Promise<IFigmaNode | null>;
|
|
38
|
+
/**
|
|
39
|
+
* Busca imagens de nodes
|
|
40
|
+
*/
|
|
41
|
+
getImages(fileId: string, nodeIds: string[], format?: 'jpg' | 'png' | 'svg' | 'pdf', scale?: number): Promise<Record<string, string>>;
|
|
42
|
+
/**
|
|
43
|
+
* Busca componentes de um arquivo
|
|
44
|
+
*/
|
|
45
|
+
getComponents(fileId: string): Promise<IFigmaFile['components']>;
|
|
46
|
+
/**
|
|
47
|
+
* Busca estilos de um arquivo
|
|
48
|
+
*/
|
|
49
|
+
getStyles(fileId: string): Promise<IFigmaFile['styles']>;
|
|
50
|
+
/**
|
|
51
|
+
* Lista todos os frames/componentes de nível superior
|
|
52
|
+
*/
|
|
53
|
+
listTopLevelFrames(fileId: string): Promise<IFigmaNode[]>;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Erro específico da API do Figma
|
|
57
|
+
*/
|
|
58
|
+
export declare class FigmaApiError extends Error {
|
|
59
|
+
statusCode: number;
|
|
60
|
+
constructor(message: string, statusCode: number);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Cria instância do cliente Figma
|
|
64
|
+
*/
|
|
65
|
+
export declare function createFigmaClient(config: IFigmaClientConfig): FigmaClient;
|
|
66
|
+
//# sourceMappingURL=figma-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"figma-client.d.ts","sourceRoot":"","sources":["../../src/client/figma-client.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAEjE,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,QAAQ,EAAE,UAAU,CAAA;KAAE,CAAC,CAAC;CACjD;AAED,qBAAa,WAAW;IACtB,OAAO,CAAC,OAAO,CAA8B;IAC7C,OAAO,CAAC,KAAK,CAAS;IACtB,OAAO,CAAC,OAAO,CAAS;gBAEZ,MAAM,EAAE,kBAAkB;IAKtC;;OAEG;YACW,OAAO;IA0BrB;;OAEG;IACG,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAIlD;;OAEG;IACG,YAAY,CAChB,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EAAE,GAChB,OAAO,CAAC,qBAAqB,CAAC;IAOjC;;OAEG;IACG,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IAMzE;;OAEG;IACG,SAAS,CACb,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EAAE,EACjB,MAAM,GAAE,KAAK,GAAG,KAAK,GAAG,KAAK,GAAG,KAAa,EAC7C,KAAK,GAAE,MAAU,GAChB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAQlC;;OAEG;IACG,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,YAAY,CAAC,CAAC;IAKtE;;OAEG;IACG,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;IAK9D;;OAEG;IACG,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC;CAsBhE;AAED;;GAEG;AACH,qBAAa,aAAc,SAAQ,KAAK;IAG7B,UAAU,EAAE,MAAM;gBADzB,OAAO,EAAE,MAAM,EACR,UAAU,EAAE,MAAM;CAK5B;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,kBAAkB,GAAG,WAAW,CAEzE"}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Figma REST API Client
|
|
3
|
+
* Cliente para comunicação com a API do Figma
|
|
4
|
+
*/
|
|
5
|
+
export class FigmaClient {
|
|
6
|
+
baseUrl = 'https://api.figma.com/v1';
|
|
7
|
+
token;
|
|
8
|
+
timeout;
|
|
9
|
+
constructor(config) {
|
|
10
|
+
this.token = config.token;
|
|
11
|
+
this.timeout = config.timeout ?? 30000;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Faz requisição autenticada para a API do Figma
|
|
15
|
+
*/
|
|
16
|
+
async request(endpoint) {
|
|
17
|
+
const controller = new AbortController();
|
|
18
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
19
|
+
try {
|
|
20
|
+
const response = await fetch(`${this.baseUrl}${endpoint}`, {
|
|
21
|
+
headers: {
|
|
22
|
+
'X-Figma-Token': this.token,
|
|
23
|
+
},
|
|
24
|
+
signal: controller.signal,
|
|
25
|
+
});
|
|
26
|
+
if (!response.ok) {
|
|
27
|
+
const error = await response.text();
|
|
28
|
+
throw new FigmaApiError(`Figma API error: ${response.status} - ${error}`, response.status);
|
|
29
|
+
}
|
|
30
|
+
return response.json();
|
|
31
|
+
}
|
|
32
|
+
finally {
|
|
33
|
+
clearTimeout(timeoutId);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Busca arquivo completo do Figma
|
|
38
|
+
*/
|
|
39
|
+
async getFile(fileId) {
|
|
40
|
+
return this.request(`/files/${fileId}`);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Busca nodes específicos de um arquivo
|
|
44
|
+
*/
|
|
45
|
+
async getFileNodes(fileId, nodeIds) {
|
|
46
|
+
const ids = nodeIds.join(',');
|
|
47
|
+
return this.request(`/files/${fileId}/nodes?ids=${encodeURIComponent(ids)}`);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Busca um único node
|
|
51
|
+
*/
|
|
52
|
+
async getNode(fileId, nodeId) {
|
|
53
|
+
const response = await this.getFileNodes(fileId, [nodeId]);
|
|
54
|
+
const nodeData = response.nodes[nodeId];
|
|
55
|
+
return nodeData?.document ?? null;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Busca imagens de nodes
|
|
59
|
+
*/
|
|
60
|
+
async getImages(fileId, nodeIds, format = 'png', scale = 2) {
|
|
61
|
+
const ids = nodeIds.join(',');
|
|
62
|
+
const response = await this.request(`/images/${fileId}?ids=${encodeURIComponent(ids)}&format=${format}&scale=${scale}`);
|
|
63
|
+
return response.images;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Busca componentes de um arquivo
|
|
67
|
+
*/
|
|
68
|
+
async getComponents(fileId) {
|
|
69
|
+
const file = await this.getFile(fileId);
|
|
70
|
+
return file.components;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Busca estilos de um arquivo
|
|
74
|
+
*/
|
|
75
|
+
async getStyles(fileId) {
|
|
76
|
+
const file = await this.getFile(fileId);
|
|
77
|
+
return file.styles;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Lista todos os frames/componentes de nível superior
|
|
81
|
+
*/
|
|
82
|
+
async listTopLevelFrames(fileId) {
|
|
83
|
+
const file = await this.getFile(fileId);
|
|
84
|
+
const frames = [];
|
|
85
|
+
// Percorre as páginas (CANVAS) do documento
|
|
86
|
+
for (const page of file.document.children ?? []) {
|
|
87
|
+
if (page.type === 'CANVAS') {
|
|
88
|
+
// Adiciona frames de nível superior da página
|
|
89
|
+
for (const child of page.children ?? []) {
|
|
90
|
+
if (child.type === 'FRAME' ||
|
|
91
|
+
child.type === 'COMPONENT' ||
|
|
92
|
+
child.type === 'COMPONENT_SET') {
|
|
93
|
+
frames.push(child);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return frames;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Erro específico da API do Figma
|
|
103
|
+
*/
|
|
104
|
+
export class FigmaApiError extends Error {
|
|
105
|
+
statusCode;
|
|
106
|
+
constructor(message, statusCode) {
|
|
107
|
+
super(message);
|
|
108
|
+
this.statusCode = statusCode;
|
|
109
|
+
this.name = 'FigmaApiError';
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Cria instância do cliente Figma
|
|
114
|
+
*/
|
|
115
|
+
export function createFigmaClient(config) {
|
|
116
|
+
return new FigmaClient(config);
|
|
117
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAClF,YAAY,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { FigmaClient, FigmaApiError, createFigmaClient } from './figma-client.js';
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { parseLayout, parseSizing, parseLayoutAndSizing } from './layout-parser.js';
|
|
2
|
+
export type { ILayoutProperties } from './layout-parser.js';
|
|
3
|
+
export { parseStyles, parseBackgroundColor, parseBorder, parseBorderRadius, parseBoxShadow } from './style-parser.js';
|
|
4
|
+
export type { IStyleProperties } from './style-parser.js';
|
|
5
|
+
export { parseTextStyles, extractTextContent } from './text-parser.js';
|
|
6
|
+
export type { ITextProperties } from './text-parser.js';
|
|
7
|
+
export { parseNode, parseNodes, parseFrameName } from './node-parser.js';
|
|
8
|
+
export type { IParseOptions } from './node-parser.js';
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { parseLayout, parseSizing, parseLayoutAndSizing } from './layout-parser.js';
|
|
2
|
+
export { parseStyles, parseBackgroundColor, parseBorder, parseBorderRadius, parseBoxShadow } from './style-parser.js';
|
|
3
|
+
export { parseTextStyles, extractTextContent } from './text-parser.js';
|
|
4
|
+
export { parseNode, parseNodes, parseFrameName } from './node-parser.js';
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layout Parser
|
|
3
|
+
* Converte AutoLayout do Figma para propriedades CSS flexbox
|
|
4
|
+
*/
|
|
5
|
+
import type { IFigmaNode, IStyleProperty } from '@promptui-lib/core';
|
|
6
|
+
export interface ILayoutProperties {
|
|
7
|
+
display?: IStyleProperty;
|
|
8
|
+
flexDirection?: IStyleProperty;
|
|
9
|
+
justifyContent?: IStyleProperty;
|
|
10
|
+
alignItems?: IStyleProperty;
|
|
11
|
+
gap?: IStyleProperty;
|
|
12
|
+
padding?: IStyleProperty;
|
|
13
|
+
width?: IStyleProperty;
|
|
14
|
+
height?: IStyleProperty;
|
|
15
|
+
flex?: IStyleProperty;
|
|
16
|
+
flexWrap?: IStyleProperty;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Parseia AutoLayout de um node
|
|
20
|
+
*/
|
|
21
|
+
export declare function parseLayout(node: IFigmaNode): ILayoutProperties;
|
|
22
|
+
/**
|
|
23
|
+
* Parseia sizing de um node
|
|
24
|
+
*/
|
|
25
|
+
export declare function parseSizing(node: IFigmaNode): ILayoutProperties;
|
|
26
|
+
/**
|
|
27
|
+
* Combina layout e sizing
|
|
28
|
+
*/
|
|
29
|
+
export declare function parseLayoutAndSizing(node: IFigmaNode): IStyleProperty[];
|
|
30
|
+
//# sourceMappingURL=layout-parser.d.ts.map
|
|
@@ -0,0 +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;AAGrE,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,CAwD/D;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,UAAU,GAAG,cAAc,EAAE,CASvE"}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layout Parser
|
|
3
|
+
* Converte AutoLayout do Figma para propriedades CSS flexbox
|
|
4
|
+
*/
|
|
5
|
+
import { findGapToken, generatePaddingToken } from '@promptui-lib/core';
|
|
6
|
+
/**
|
|
7
|
+
* Parseia AutoLayout de um node
|
|
8
|
+
*/
|
|
9
|
+
export function parseLayout(node) {
|
|
10
|
+
const properties = {};
|
|
11
|
+
// Se não tem AutoLayout, retorna vazio
|
|
12
|
+
if (!node.layoutMode || node.layoutMode === 'NONE') {
|
|
13
|
+
return properties;
|
|
14
|
+
}
|
|
15
|
+
// Display flex
|
|
16
|
+
properties.display = {
|
|
17
|
+
property: 'display',
|
|
18
|
+
value: 'flex',
|
|
19
|
+
};
|
|
20
|
+
// Flex direction
|
|
21
|
+
properties.flexDirection = {
|
|
22
|
+
property: 'flex-direction',
|
|
23
|
+
value: node.layoutMode === 'HORIZONTAL' ? 'row' : 'column',
|
|
24
|
+
};
|
|
25
|
+
// Justify content (primary axis)
|
|
26
|
+
if (node.primaryAxisAlignItems) {
|
|
27
|
+
const justifyMap = {
|
|
28
|
+
MIN: 'flex-start',
|
|
29
|
+
CENTER: 'center',
|
|
30
|
+
MAX: 'flex-end',
|
|
31
|
+
SPACE_BETWEEN: 'space-between',
|
|
32
|
+
};
|
|
33
|
+
properties.justifyContent = {
|
|
34
|
+
property: 'justify-content',
|
|
35
|
+
value: justifyMap[node.primaryAxisAlignItems] ?? 'flex-start',
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
// Align items (counter axis)
|
|
39
|
+
if (node.counterAxisAlignItems) {
|
|
40
|
+
const alignMap = {
|
|
41
|
+
MIN: 'flex-start',
|
|
42
|
+
CENTER: 'center',
|
|
43
|
+
MAX: 'flex-end',
|
|
44
|
+
BASELINE: 'baseline',
|
|
45
|
+
};
|
|
46
|
+
properties.alignItems = {
|
|
47
|
+
property: 'align-items',
|
|
48
|
+
value: alignMap[node.counterAxisAlignItems] ?? 'flex-start',
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
// Gap
|
|
52
|
+
if (node.itemSpacing !== undefined && node.itemSpacing > 0) {
|
|
53
|
+
const gapToken = findGapToken(node.itemSpacing);
|
|
54
|
+
properties.gap = {
|
|
55
|
+
property: 'gap',
|
|
56
|
+
value: gapToken,
|
|
57
|
+
token: gapToken.startsWith('$') ? gapToken : undefined,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
// Padding
|
|
61
|
+
const haspadding = node.paddingTop ||
|
|
62
|
+
node.paddingRight ||
|
|
63
|
+
node.paddingBottom ||
|
|
64
|
+
node.paddingLeft;
|
|
65
|
+
if (haspadding) {
|
|
66
|
+
const paddingToken = generatePaddingToken(node.paddingTop ?? 0, node.paddingRight ?? 0, node.paddingBottom ?? 0, node.paddingLeft ?? 0);
|
|
67
|
+
properties.padding = {
|
|
68
|
+
property: 'padding',
|
|
69
|
+
value: paddingToken,
|
|
70
|
+
token: paddingToken.startsWith('$') ? paddingToken : undefined,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
// Flex wrap
|
|
74
|
+
if (node.layoutWrap === 'WRAP') {
|
|
75
|
+
properties.flexWrap = {
|
|
76
|
+
property: 'flex-wrap',
|
|
77
|
+
value: 'wrap',
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return properties;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Parseia sizing de um node
|
|
84
|
+
*/
|
|
85
|
+
export function parseSizing(node) {
|
|
86
|
+
const properties = {};
|
|
87
|
+
// Width
|
|
88
|
+
if (node.layoutSizingHorizontal) {
|
|
89
|
+
switch (node.layoutSizingHorizontal) {
|
|
90
|
+
case 'FILL':
|
|
91
|
+
properties.width = {
|
|
92
|
+
property: 'width',
|
|
93
|
+
value: '100%',
|
|
94
|
+
};
|
|
95
|
+
break;
|
|
96
|
+
case 'HUG':
|
|
97
|
+
properties.width = {
|
|
98
|
+
property: 'width',
|
|
99
|
+
value: 'fit-content',
|
|
100
|
+
};
|
|
101
|
+
break;
|
|
102
|
+
case 'FIXED':
|
|
103
|
+
if (node.absoluteBoundingBox?.width) {
|
|
104
|
+
properties.width = {
|
|
105
|
+
property: 'width',
|
|
106
|
+
value: `${node.absoluteBoundingBox.width}px`,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// Height
|
|
113
|
+
if (node.layoutSizingVertical) {
|
|
114
|
+
switch (node.layoutSizingVertical) {
|
|
115
|
+
case 'FILL':
|
|
116
|
+
properties.height = {
|
|
117
|
+
property: 'height',
|
|
118
|
+
value: '100%',
|
|
119
|
+
};
|
|
120
|
+
break;
|
|
121
|
+
case 'HUG':
|
|
122
|
+
properties.height = {
|
|
123
|
+
property: 'height',
|
|
124
|
+
value: 'fit-content',
|
|
125
|
+
};
|
|
126
|
+
break;
|
|
127
|
+
case 'FIXED':
|
|
128
|
+
if (node.absoluteBoundingBox?.height) {
|
|
129
|
+
properties.height = {
|
|
130
|
+
property: 'height',
|
|
131
|
+
value: `${node.absoluteBoundingBox.height}px`,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return properties;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Combina layout e sizing
|
|
141
|
+
*/
|
|
142
|
+
export function parseLayoutAndSizing(node) {
|
|
143
|
+
const layout = parseLayout(node);
|
|
144
|
+
const sizing = parseSizing(node);
|
|
145
|
+
const combined = { ...layout, ...sizing };
|
|
146
|
+
return Object.values(combined).filter((prop) => prop !== undefined);
|
|
147
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node Parser
|
|
3
|
+
* Parser principal que converte node do Figma em AST do componente
|
|
4
|
+
*/
|
|
5
|
+
import type { IFigmaNode, IComponentAST, ComponentLayer, IParsedFrameName } from '@promptui-lib/core';
|
|
6
|
+
export interface IParseOptions {
|
|
7
|
+
forceLayer?: ComponentLayer;
|
|
8
|
+
componentName?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Parseia o nome do frame para extrair informações
|
|
12
|
+
*/
|
|
13
|
+
export declare function parseFrameName(name: string): IParsedFrameName;
|
|
14
|
+
/**
|
|
15
|
+
* Parser principal - converte node do Figma em AST do componente
|
|
16
|
+
*/
|
|
17
|
+
export declare function parseNode(node: IFigmaNode, options?: IParseOptions): IComponentAST;
|
|
18
|
+
/**
|
|
19
|
+
* Parseia múltiplos nodes
|
|
20
|
+
*/
|
|
21
|
+
export declare function parseNodes(nodes: IFigmaNode[], options?: IParseOptions): IComponentAST[];
|
|
22
|
+
//# sourceMappingURL=node-parser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"node-parser.d.ts","sourceRoot":"","sources":["../../src/parser/node-parser.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EACV,UAAU,EACV,aAAa,EAKb,cAAc,EACd,gBAAgB,EACjB,MAAM,oBAAoB,CAAC;AAc5B,MAAM,WAAW,aAAa;IAC5B,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,gBAAgB,CAwB7D;AA4ID;;GAEG;AACH,wBAAgB,SAAS,CACvB,IAAI,EAAE,UAAU,EAChB,OAAO,GAAE,aAAkB,GAC1B,aAAa,CA6Cf;AAED;;GAEG;AACH,wBAAgB,UAAU,CACxB,KAAK,EAAE,UAAU,EAAE,EACnB,OAAO,GAAE,aAAkB,GAC1B,aAAa,EAAE,CAEjB"}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node Parser
|
|
3
|
+
* Parser principal que converte node do Figma em AST do componente
|
|
4
|
+
*/
|
|
5
|
+
import { generateComponentName, generateFileName, generateBEMBlock, generateBEMElement, classifyComponent, extractBaseName, extractVariants, } from '@promptui-lib/core';
|
|
6
|
+
import { parseLayoutAndSizing } from './layout-parser.js';
|
|
7
|
+
import { parseStyles } from './style-parser.js';
|
|
8
|
+
import { parseTextStyles } from './text-parser.js';
|
|
9
|
+
/**
|
|
10
|
+
* Parseia o nome do frame para extrair informações
|
|
11
|
+
*/
|
|
12
|
+
export function parseFrameName(name) {
|
|
13
|
+
const isExportable = name.startsWith('#');
|
|
14
|
+
const isInternal = name.startsWith('_');
|
|
15
|
+
// Remove prefixos especiais
|
|
16
|
+
let cleanName = name.replace(/^[#_]/, '');
|
|
17
|
+
// Extrai props do formato $prop:name
|
|
18
|
+
const propMatches = cleanName.match(/\$prop:(\w+)/g) ?? [];
|
|
19
|
+
const extractedProps = propMatches.map((match) => {
|
|
20
|
+
const propName = match.replace('$prop:', '');
|
|
21
|
+
return { name: propName, type: 'string' };
|
|
22
|
+
});
|
|
23
|
+
// Remove props do nome
|
|
24
|
+
cleanName = cleanName.replace(/\$prop:\w+/g, '').trim();
|
|
25
|
+
return {
|
|
26
|
+
baseName: extractBaseName(cleanName),
|
|
27
|
+
variants: extractVariants(cleanName),
|
|
28
|
+
isExportable,
|
|
29
|
+
isInternal,
|
|
30
|
+
extractedProps,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Converte um node em elemento JSX
|
|
35
|
+
*/
|
|
36
|
+
function nodeToJSX(node, blockName, depth = 0) {
|
|
37
|
+
const isRoot = depth === 0;
|
|
38
|
+
const elementName = isRoot ? '' : node.name.toLowerCase().replace(/\s+/g, '-');
|
|
39
|
+
const className = isRoot
|
|
40
|
+
? blockName
|
|
41
|
+
: generateBEMElement(blockName, elementName);
|
|
42
|
+
// Determina a tag HTML
|
|
43
|
+
let tag = 'div';
|
|
44
|
+
if (node.type === 'TEXT') {
|
|
45
|
+
tag = 'span';
|
|
46
|
+
}
|
|
47
|
+
// Processa filhos
|
|
48
|
+
const children = [];
|
|
49
|
+
if (node.type === 'TEXT' && node.characters) {
|
|
50
|
+
children.push({ type: 'text', value: node.characters });
|
|
51
|
+
}
|
|
52
|
+
else if (node.children) {
|
|
53
|
+
for (const child of node.children) {
|
|
54
|
+
// Ignora nodes invisíveis ou internos
|
|
55
|
+
if (child.visible === false || child.name.startsWith('_')) {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
children.push(nodeToJSX(child, blockName, depth + 1));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
tag,
|
|
63
|
+
className,
|
|
64
|
+
props: {},
|
|
65
|
+
children,
|
|
66
|
+
selfClosing: children.length === 0 && node.type !== 'TEXT',
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Coleta estilos de um node recursivamente
|
|
71
|
+
*/
|
|
72
|
+
function collectStyles(node, blockName, depth = 0) {
|
|
73
|
+
const blocks = [];
|
|
74
|
+
const isRoot = depth === 0;
|
|
75
|
+
const elementName = isRoot ? '' : node.name.toLowerCase().replace(/\s+/g, '-');
|
|
76
|
+
const selector = isRoot
|
|
77
|
+
? `.${blockName}`
|
|
78
|
+
: `.${generateBEMElement(blockName, elementName)}`;
|
|
79
|
+
// Coleta propriedades deste node
|
|
80
|
+
const properties = [];
|
|
81
|
+
// Layout
|
|
82
|
+
if (node.type === 'FRAME' || node.type === 'COMPONENT' || node.type === 'INSTANCE') {
|
|
83
|
+
properties.push(...parseLayoutAndSizing(node));
|
|
84
|
+
}
|
|
85
|
+
// Estilos visuais
|
|
86
|
+
properties.push(...parseStyles(node));
|
|
87
|
+
// Estilos de texto
|
|
88
|
+
if (node.type === 'TEXT') {
|
|
89
|
+
properties.push(...parseTextStyles(node));
|
|
90
|
+
}
|
|
91
|
+
// Só adiciona bloco se tiver propriedades
|
|
92
|
+
if (properties.length > 0) {
|
|
93
|
+
blocks.push({
|
|
94
|
+
selector,
|
|
95
|
+
properties,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
// Processa filhos
|
|
99
|
+
if (node.children) {
|
|
100
|
+
for (const child of node.children) {
|
|
101
|
+
if (child.visible === false || child.name.startsWith('_')) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
blocks.push(...collectStyles(child, blockName, depth + 1));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return blocks;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Extrai props do componente baseado na estrutura
|
|
111
|
+
*/
|
|
112
|
+
function extractProps(node, parsedName) {
|
|
113
|
+
const props = [];
|
|
114
|
+
// Props extraídas do nome
|
|
115
|
+
for (const prop of parsedName.extractedProps) {
|
|
116
|
+
props.push({
|
|
117
|
+
name: prop.name,
|
|
118
|
+
type: prop.type,
|
|
119
|
+
required: false,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
// Prop children se tiver filhos de texto
|
|
123
|
+
const hasTextChildren = node.children?.some((child) => child.type === 'TEXT');
|
|
124
|
+
if (hasTextChildren) {
|
|
125
|
+
props.push({
|
|
126
|
+
name: 'children',
|
|
127
|
+
type: 'ReactNode',
|
|
128
|
+
required: false,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
// Prop className padrão
|
|
132
|
+
props.push({
|
|
133
|
+
name: 'className',
|
|
134
|
+
type: 'string',
|
|
135
|
+
required: false,
|
|
136
|
+
defaultValue: "''",
|
|
137
|
+
});
|
|
138
|
+
return props;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Parser principal - converte node do Figma em AST do componente
|
|
142
|
+
*/
|
|
143
|
+
export function parseNode(node, options = {}) {
|
|
144
|
+
const parsedName = parseFrameName(node.name);
|
|
145
|
+
// Nome do componente
|
|
146
|
+
const name = options.componentName ?? generateComponentName(node.name);
|
|
147
|
+
const fileName = generateFileName(name);
|
|
148
|
+
// Classificação
|
|
149
|
+
const layer = classifyComponent(node, options.forceLayer);
|
|
150
|
+
// Classe BEM base
|
|
151
|
+
const blockName = generateBEMBlock(name);
|
|
152
|
+
// Props
|
|
153
|
+
const props = extractProps(node, parsedName);
|
|
154
|
+
// JSX
|
|
155
|
+
const jsx = nodeToJSX(node, blockName);
|
|
156
|
+
// Styles
|
|
157
|
+
const styles = collectStyles(node, blockName);
|
|
158
|
+
// Imports
|
|
159
|
+
const imports = [
|
|
160
|
+
{
|
|
161
|
+
from: 'react',
|
|
162
|
+
named: props.some((p) => p.type === 'ReactNode') ? ['ReactNode'] : undefined,
|
|
163
|
+
typeOnly: true,
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
from: `./${fileName}.scss`,
|
|
167
|
+
default: undefined,
|
|
168
|
+
named: undefined,
|
|
169
|
+
},
|
|
170
|
+
].filter((imp) => imp.named || imp.default || imp.from.endsWith('.scss'));
|
|
171
|
+
return {
|
|
172
|
+
name,
|
|
173
|
+
fileName,
|
|
174
|
+
layer,
|
|
175
|
+
props,
|
|
176
|
+
jsx,
|
|
177
|
+
styles,
|
|
178
|
+
imports,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Parseia múltiplos nodes
|
|
183
|
+
*/
|
|
184
|
+
export function parseNodes(nodes, options = {}) {
|
|
185
|
+
return nodes.map((node) => parseNode(node, options));
|
|
186
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Style Parser
|
|
3
|
+
* Converte fills, strokes, effects do Figma para CSS
|
|
4
|
+
*/
|
|
5
|
+
import type { IFigmaNode, IStyleProperty } from '@promptui-lib/core';
|
|
6
|
+
export interface IStyleProperties {
|
|
7
|
+
backgroundColor?: IStyleProperty;
|
|
8
|
+
color?: IStyleProperty;
|
|
9
|
+
borderRadius?: IStyleProperty;
|
|
10
|
+
border?: IStyleProperty;
|
|
11
|
+
boxShadow?: IStyleProperty;
|
|
12
|
+
opacity?: IStyleProperty;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Parseia background color de um node
|
|
16
|
+
*/
|
|
17
|
+
export declare function parseBackgroundColor(node: IFigmaNode): IStyleProperty | null;
|
|
18
|
+
/**
|
|
19
|
+
* Parseia border de um node
|
|
20
|
+
*/
|
|
21
|
+
export declare function parseBorder(node: IFigmaNode): IStyleProperty | null;
|
|
22
|
+
/**
|
|
23
|
+
* Parseia border-radius de um node
|
|
24
|
+
*/
|
|
25
|
+
export declare function parseBorderRadius(node: IFigmaNode): IStyleProperty | null;
|
|
26
|
+
/**
|
|
27
|
+
* Parseia box-shadow de um node
|
|
28
|
+
*/
|
|
29
|
+
export declare function parseBoxShadow(node: IFigmaNode): IStyleProperty | null;
|
|
30
|
+
/**
|
|
31
|
+
* Parseia opacity de um node
|
|
32
|
+
*/
|
|
33
|
+
export declare function parseOpacity(node: IFigmaNode): IStyleProperty | null;
|
|
34
|
+
/**
|
|
35
|
+
* Parseia todos os estilos visuais de um node
|
|
36
|
+
*/
|
|
37
|
+
export declare function parseStyles(node: IFigmaNode): IStyleProperty[];
|
|
38
|
+
//# sourceMappingURL=style-parser.d.ts.map
|
|
@@ -0,0 +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;AAS5B,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;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,UAAU,GAAG,cAAc,GAAG,IAAI,CAsB5E;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,UAAU,GAAG,cAAc,GAAG,IAAI,CAuBnE;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,CAmCtE;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,UAAU,GAAG,cAAc,GAAG,IAAI,CASpE;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,UAAU,GAAG,cAAc,EAAE,CAmB9D"}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Style Parser
|
|
3
|
+
* Converte fills, strokes, effects do Figma para CSS
|
|
4
|
+
*/
|
|
5
|
+
import { rgbaToHex, findColorToken, findRadiusToken, findShadowToken, generateBorderRadius, } from '@promptui-lib/core';
|
|
6
|
+
/**
|
|
7
|
+
* Extrai cor de um fill sólido
|
|
8
|
+
*/
|
|
9
|
+
function extractSolidColor(fill) {
|
|
10
|
+
const { r, g, b, a } = fill.color;
|
|
11
|
+
const opacity = fill.opacity ?? 1;
|
|
12
|
+
return rgbaToHex(r, g, b, a * opacity);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Parseia background color de um node
|
|
16
|
+
*/
|
|
17
|
+
export function parseBackgroundColor(node) {
|
|
18
|
+
if (!node.fills || node.fills.length === 0) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
// Pega o primeiro fill visível sólido
|
|
22
|
+
const solidFill = node.fills.find((fill) => fill.type === 'SOLID');
|
|
23
|
+
if (!solidFill) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
const hex = extractSolidColor(solidFill);
|
|
27
|
+
const token = findColorToken(hex);
|
|
28
|
+
return {
|
|
29
|
+
property: 'background-color',
|
|
30
|
+
value: token ?? hex,
|
|
31
|
+
token: token ?? undefined,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Parseia border de um node
|
|
36
|
+
*/
|
|
37
|
+
export function parseBorder(node) {
|
|
38
|
+
if (!node.strokes || node.strokes.length === 0 || !node.strokeWeight) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
const stroke = node.strokes[0];
|
|
42
|
+
if (stroke.type !== 'SOLID') {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
const hex = rgbaToHex(stroke.color.r, stroke.color.g, stroke.color.b, stroke.opacity ?? 1);
|
|
46
|
+
const token = findColorToken(hex);
|
|
47
|
+
return {
|
|
48
|
+
property: 'border',
|
|
49
|
+
value: `${node.strokeWeight}px solid ${token ?? hex}`,
|
|
50
|
+
token: token ?? undefined,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Parseia border-radius de um node
|
|
55
|
+
*/
|
|
56
|
+
export function parseBorderRadius(node) {
|
|
57
|
+
// Verifica se tem corner radius diferentes
|
|
58
|
+
if (node.rectangleCornerRadii) {
|
|
59
|
+
const [tl, tr, br, bl] = node.rectangleCornerRadii;
|
|
60
|
+
const radius = generateBorderRadius(tl, tr, br, bl);
|
|
61
|
+
return {
|
|
62
|
+
property: 'border-radius',
|
|
63
|
+
value: radius,
|
|
64
|
+
token: radius.startsWith('$') ? radius : undefined,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
// Corner radius uniforme
|
|
68
|
+
if (node.cornerRadius !== undefined && node.cornerRadius > 0) {
|
|
69
|
+
const token = findRadiusToken(node.cornerRadius);
|
|
70
|
+
return {
|
|
71
|
+
property: 'border-radius',
|
|
72
|
+
value: token,
|
|
73
|
+
token: token.startsWith('$') ? token : undefined,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Parseia box-shadow de um node
|
|
80
|
+
*/
|
|
81
|
+
export function parseBoxShadow(node) {
|
|
82
|
+
if (!node.effects || node.effects.length === 0) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
// Filtra apenas drop shadows visíveis
|
|
86
|
+
const shadows = node.effects.filter((effect) => effect.type === 'DROP_SHADOW' && effect.visible);
|
|
87
|
+
if (shadows.length === 0) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
// Usa o primeiro shadow
|
|
91
|
+
const shadow = shadows[0];
|
|
92
|
+
const token = findShadowToken(shadow.radius);
|
|
93
|
+
if (token) {
|
|
94
|
+
return {
|
|
95
|
+
property: 'box-shadow',
|
|
96
|
+
value: token,
|
|
97
|
+
token,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
// Gera box-shadow customizado
|
|
101
|
+
const { offset, radius, spread, color } = shadow;
|
|
102
|
+
const hex = rgbaToHex(color.r, color.g, color.b, color.a);
|
|
103
|
+
return {
|
|
104
|
+
property: 'box-shadow',
|
|
105
|
+
value: `${offset.x}px ${offset.y}px ${radius}px ${spread ?? 0}px ${hex}`,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Parseia opacity de um node
|
|
110
|
+
*/
|
|
111
|
+
export function parseOpacity(node) {
|
|
112
|
+
if (node.opacity !== undefined && node.opacity < 1) {
|
|
113
|
+
return {
|
|
114
|
+
property: 'opacity',
|
|
115
|
+
value: `${node.opacity}`,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Parseia todos os estilos visuais de um node
|
|
122
|
+
*/
|
|
123
|
+
export function parseStyles(node) {
|
|
124
|
+
const properties = [];
|
|
125
|
+
const background = parseBackgroundColor(node);
|
|
126
|
+
if (background)
|
|
127
|
+
properties.push(background);
|
|
128
|
+
const border = parseBorder(node);
|
|
129
|
+
if (border)
|
|
130
|
+
properties.push(border);
|
|
131
|
+
const borderRadius = parseBorderRadius(node);
|
|
132
|
+
if (borderRadius)
|
|
133
|
+
properties.push(borderRadius);
|
|
134
|
+
const boxShadow = parseBoxShadow(node);
|
|
135
|
+
if (boxShadow)
|
|
136
|
+
properties.push(boxShadow);
|
|
137
|
+
const opacity = parseOpacity(node);
|
|
138
|
+
if (opacity)
|
|
139
|
+
properties.push(opacity);
|
|
140
|
+
return properties;
|
|
141
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text Parser
|
|
3
|
+
* Converte estilos de texto do Figma para CSS
|
|
4
|
+
*/
|
|
5
|
+
import type { IFigmaNode, IStyleProperty, ITextStyle } from '@promptui-lib/core';
|
|
6
|
+
export interface ITextProperties {
|
|
7
|
+
fontFamily?: IStyleProperty;
|
|
8
|
+
fontSize?: IStyleProperty;
|
|
9
|
+
fontWeight?: IStyleProperty;
|
|
10
|
+
lineHeight?: IStyleProperty;
|
|
11
|
+
letterSpacing?: IStyleProperty;
|
|
12
|
+
textAlign?: IStyleProperty;
|
|
13
|
+
textDecoration?: IStyleProperty;
|
|
14
|
+
textTransform?: IStyleProperty;
|
|
15
|
+
color?: IStyleProperty;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Parseia font-family
|
|
19
|
+
*/
|
|
20
|
+
export declare function parseFontFamily(style: ITextStyle): IStyleProperty;
|
|
21
|
+
/**
|
|
22
|
+
* Parseia font-size
|
|
23
|
+
*/
|
|
24
|
+
export declare function parseFontSize(style: ITextStyle): IStyleProperty;
|
|
25
|
+
/**
|
|
26
|
+
* Parseia font-weight
|
|
27
|
+
*/
|
|
28
|
+
export declare function parseFontWeight(style: ITextStyle): IStyleProperty;
|
|
29
|
+
/**
|
|
30
|
+
* Parseia line-height
|
|
31
|
+
*/
|
|
32
|
+
export declare function parseLineHeight(style: ITextStyle): IStyleProperty | null;
|
|
33
|
+
/**
|
|
34
|
+
* Parseia letter-spacing
|
|
35
|
+
*/
|
|
36
|
+
export declare function parseLetterSpacing(style: ITextStyle): IStyleProperty | null;
|
|
37
|
+
/**
|
|
38
|
+
* Parseia text-align
|
|
39
|
+
*/
|
|
40
|
+
export declare function parseTextAlign(style: ITextStyle): IStyleProperty | null;
|
|
41
|
+
/**
|
|
42
|
+
* Parseia text-decoration
|
|
43
|
+
*/
|
|
44
|
+
export declare function parseTextDecoration(style: ITextStyle): IStyleProperty | null;
|
|
45
|
+
/**
|
|
46
|
+
* Parseia text-transform
|
|
47
|
+
*/
|
|
48
|
+
export declare function parseTextTransform(style: ITextStyle): IStyleProperty | null;
|
|
49
|
+
/**
|
|
50
|
+
* Parseia cor do texto
|
|
51
|
+
*/
|
|
52
|
+
export declare function parseTextColor(node: IFigmaNode): IStyleProperty | null;
|
|
53
|
+
/**
|
|
54
|
+
* Parseia todos os estilos de texto de um node
|
|
55
|
+
*/
|
|
56
|
+
export declare function parseTextStyles(node: IFigmaNode): IStyleProperty[];
|
|
57
|
+
/**
|
|
58
|
+
* Extrai conteúdo de texto de um node
|
|
59
|
+
*/
|
|
60
|
+
export declare function extractTextContent(node: IFigmaNode): string | null;
|
|
61
|
+
//# sourceMappingURL=text-parser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"text-parser.d.ts","sourceRoot":"","sources":["../../src/parser/text-parser.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAUjF,MAAM,WAAW,eAAe;IAC9B,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B,aAAa,CAAC,EAAE,cAAc,CAAC;IAC/B,SAAS,CAAC,EAAE,cAAc,CAAC;IAC3B,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,aAAa,CAAC,EAAE,cAAc,CAAC;IAC/B,KAAK,CAAC,EAAE,cAAc,CAAC;CACxB;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,UAAU,GAAG,cAAc,CAQjE;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,UAAU,GAAG,cAAc,CAQ/D;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,UAAU,GAAG,cAAc,CAQjE;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,UAAU,GAAG,cAAc,GAAG,IAAI,CAyBxE;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,UAAU,GAAG,cAAc,GAAG,IAAI,CAS3E;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,UAAU,GAAG,cAAc,GAAG,IAAI,CAevE;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,UAAU,GAAG,cAAc,GAAG,IAAI,CAc5E;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,UAAU,GAAG,cAAc,GAAG,IAAI,CAe3E;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,UAAU,GAAG,cAAc,GAAG,IAAI,CAoBtE;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,UAAU,GAAG,cAAc,EAAE,CA0ClE;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,GAAG,IAAI,CAMlE"}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text Parser
|
|
3
|
+
* Converte estilos de texto do Figma para CSS
|
|
4
|
+
*/
|
|
5
|
+
import { rgbaToHex, findColorToken, findFontSizeToken, findFontWeightToken, findLineHeightToken, findFontFamilyToken, } from '@promptui-lib/core';
|
|
6
|
+
/**
|
|
7
|
+
* Parseia font-family
|
|
8
|
+
*/
|
|
9
|
+
export function parseFontFamily(style) {
|
|
10
|
+
const token = findFontFamilyToken(style.fontFamily);
|
|
11
|
+
return {
|
|
12
|
+
property: 'font-family',
|
|
13
|
+
value: token,
|
|
14
|
+
token: token.startsWith('$') ? token : undefined,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Parseia font-size
|
|
19
|
+
*/
|
|
20
|
+
export function parseFontSize(style) {
|
|
21
|
+
const token = findFontSizeToken(style.fontSize);
|
|
22
|
+
return {
|
|
23
|
+
property: 'font-size',
|
|
24
|
+
value: token,
|
|
25
|
+
token: token.startsWith('$') ? token : undefined,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Parseia font-weight
|
|
30
|
+
*/
|
|
31
|
+
export function parseFontWeight(style) {
|
|
32
|
+
const token = findFontWeightToken(style.fontWeight);
|
|
33
|
+
return {
|
|
34
|
+
property: 'font-weight',
|
|
35
|
+
value: token,
|
|
36
|
+
token: token.startsWith('$') ? token : undefined,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Parseia line-height
|
|
41
|
+
*/
|
|
42
|
+
export function parseLineHeight(style) {
|
|
43
|
+
if (style.lineHeightPx) {
|
|
44
|
+
// Calcula ratio baseado no fontSize
|
|
45
|
+
const ratio = style.lineHeightPx / style.fontSize;
|
|
46
|
+
const token = findLineHeightToken(ratio);
|
|
47
|
+
return {
|
|
48
|
+
property: 'line-height',
|
|
49
|
+
value: token,
|
|
50
|
+
token: token.startsWith('$') ? token : undefined,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
if (style.lineHeightPercent) {
|
|
54
|
+
const ratio = style.lineHeightPercent / 100;
|
|
55
|
+
const token = findLineHeightToken(ratio);
|
|
56
|
+
return {
|
|
57
|
+
property: 'line-height',
|
|
58
|
+
value: token,
|
|
59
|
+
token: token.startsWith('$') ? token : undefined,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Parseia letter-spacing
|
|
66
|
+
*/
|
|
67
|
+
export function parseLetterSpacing(style) {
|
|
68
|
+
if (style.letterSpacing && style.letterSpacing !== 0) {
|
|
69
|
+
return {
|
|
70
|
+
property: 'letter-spacing',
|
|
71
|
+
value: `${style.letterSpacing}px`,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Parseia text-align
|
|
78
|
+
*/
|
|
79
|
+
export function parseTextAlign(style) {
|
|
80
|
+
if (!style.textAlignHorizontal || style.textAlignHorizontal === 'LEFT') {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
const alignMap = {
|
|
84
|
+
CENTER: 'center',
|
|
85
|
+
RIGHT: 'right',
|
|
86
|
+
JUSTIFIED: 'justify',
|
|
87
|
+
};
|
|
88
|
+
return {
|
|
89
|
+
property: 'text-align',
|
|
90
|
+
value: alignMap[style.textAlignHorizontal] ?? 'left',
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Parseia text-decoration
|
|
95
|
+
*/
|
|
96
|
+
export function parseTextDecoration(style) {
|
|
97
|
+
if (!style.textDecoration || style.textDecoration === 'NONE') {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
const decorationMap = {
|
|
101
|
+
UNDERLINE: 'underline',
|
|
102
|
+
STRIKETHROUGH: 'line-through',
|
|
103
|
+
};
|
|
104
|
+
return {
|
|
105
|
+
property: 'text-decoration',
|
|
106
|
+
value: decorationMap[style.textDecoration] ?? 'none',
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Parseia text-transform
|
|
111
|
+
*/
|
|
112
|
+
export function parseTextTransform(style) {
|
|
113
|
+
if (!style.textCase || style.textCase === 'ORIGINAL') {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
const transformMap = {
|
|
117
|
+
UPPER: 'uppercase',
|
|
118
|
+
LOWER: 'lowercase',
|
|
119
|
+
TITLE: 'capitalize',
|
|
120
|
+
};
|
|
121
|
+
return {
|
|
122
|
+
property: 'text-transform',
|
|
123
|
+
value: transformMap[style.textCase] ?? 'none',
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Parseia cor do texto
|
|
128
|
+
*/
|
|
129
|
+
export function parseTextColor(node) {
|
|
130
|
+
if (!node.fills || node.fills.length === 0) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
const solidFill = node.fills.find((fill) => fill.type === 'SOLID');
|
|
134
|
+
if (!solidFill || solidFill.type !== 'SOLID') {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
const { r, g, b, a } = solidFill.color;
|
|
138
|
+
const opacity = solidFill.opacity ?? 1;
|
|
139
|
+
const hex = rgbaToHex(r, g, b, a * opacity);
|
|
140
|
+
const token = findColorToken(hex);
|
|
141
|
+
return {
|
|
142
|
+
property: 'color',
|
|
143
|
+
value: token ?? hex,
|
|
144
|
+
token: token ?? undefined,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Parseia todos os estilos de texto de um node
|
|
149
|
+
*/
|
|
150
|
+
export function parseTextStyles(node) {
|
|
151
|
+
if (node.type !== 'TEXT' || !node.style) {
|
|
152
|
+
return [];
|
|
153
|
+
}
|
|
154
|
+
const style = node.style;
|
|
155
|
+
const properties = [];
|
|
156
|
+
// Font family
|
|
157
|
+
properties.push(parseFontFamily(style));
|
|
158
|
+
// Font size
|
|
159
|
+
properties.push(parseFontSize(style));
|
|
160
|
+
// Font weight
|
|
161
|
+
properties.push(parseFontWeight(style));
|
|
162
|
+
// Line height
|
|
163
|
+
const lineHeight = parseLineHeight(style);
|
|
164
|
+
if (lineHeight)
|
|
165
|
+
properties.push(lineHeight);
|
|
166
|
+
// Letter spacing
|
|
167
|
+
const letterSpacing = parseLetterSpacing(style);
|
|
168
|
+
if (letterSpacing)
|
|
169
|
+
properties.push(letterSpacing);
|
|
170
|
+
// Text align
|
|
171
|
+
const textAlign = parseTextAlign(style);
|
|
172
|
+
if (textAlign)
|
|
173
|
+
properties.push(textAlign);
|
|
174
|
+
// Text decoration
|
|
175
|
+
const textDecoration = parseTextDecoration(style);
|
|
176
|
+
if (textDecoration)
|
|
177
|
+
properties.push(textDecoration);
|
|
178
|
+
// Text transform
|
|
179
|
+
const textTransform = parseTextTransform(style);
|
|
180
|
+
if (textTransform)
|
|
181
|
+
properties.push(textTransform);
|
|
182
|
+
// Color
|
|
183
|
+
const color = parseTextColor(node);
|
|
184
|
+
if (color)
|
|
185
|
+
properties.push(color);
|
|
186
|
+
return properties;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Extrai conteúdo de texto de um node
|
|
190
|
+
*/
|
|
191
|
+
export function extractTextContent(node) {
|
|
192
|
+
if (node.type === 'TEXT' && node.characters) {
|
|
193
|
+
return node.characters;
|
|
194
|
+
}
|
|
195
|
+
return null;
|
|
196
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@promptui-lib/figma-parser",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Figma API client and parser for PromptUI",
|
|
6
|
+
"license": "UNLICENSED",
|
|
7
|
+
"author": {
|
|
8
|
+
"name": "Desiree Menezes",
|
|
9
|
+
"url": "https://github.com/desireemenezes"
|
|
10
|
+
},
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "https://github.com/desireemenezes/promptUI.git",
|
|
14
|
+
"directory": "packages/figma-parser"
|
|
15
|
+
},
|
|
16
|
+
"homepage": "https://github.com/desireemenezes/promptUI#readme",
|
|
17
|
+
"type": "module",
|
|
18
|
+
"main": "./dist/index.js",
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"import": "./dist/index.js"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"dist"
|
|
31
|
+
],
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@promptui-lib/core": "0.1.0"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/node": "^20.0.0",
|
|
37
|
+
"typescript": "^5.3.0",
|
|
38
|
+
"rimraf": "^5.0.0"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsc",
|
|
42
|
+
"dev": "tsc --watch",
|
|
43
|
+
"clean": "rimraf dist"
|
|
44
|
+
}
|
|
45
|
+
}
|