figma-code-agent-mcp 1.0.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/assetServer.d.ts +34 -0
- package/dist/assetServer.js +168 -0
- package/dist/codeGenerator/componentDetector.d.ts +57 -0
- package/dist/codeGenerator/componentDetector.js +171 -0
- package/dist/codeGenerator/index.d.ts +77 -0
- package/dist/codeGenerator/index.js +184 -0
- package/dist/codeGenerator/jsxGenerator.d.ts +46 -0
- package/dist/codeGenerator/jsxGenerator.js +182 -0
- package/dist/codeGenerator/styleConverter.d.ts +95 -0
- package/dist/codeGenerator/styleConverter.js +306 -0
- package/dist/hints.d.ts +14 -0
- package/dist/hints.js +105 -0
- package/dist/hub.d.ts +9 -0
- package/dist/hub.js +252 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +146 -0
- package/dist/sandbox.d.ts +18 -0
- package/dist/sandbox.js +154 -0
- package/dist/toolRegistry.d.ts +19 -0
- package/dist/toolRegistry.js +729 -0
- package/dist/tools/captureScreenshot.d.ts +28 -0
- package/dist/tools/captureScreenshot.js +31 -0
- package/dist/tools/cloneNode.d.ts +43 -0
- package/dist/tools/cloneNode.js +46 -0
- package/dist/tools/createFrame.d.ts +157 -0
- package/dist/tools/createFrame.js +114 -0
- package/dist/tools/createInstance.d.ts +38 -0
- package/dist/tools/createInstance.js +41 -0
- package/dist/tools/createRectangle.d.ts +108 -0
- package/dist/tools/createRectangle.js +77 -0
- package/dist/tools/createText.d.ts +81 -0
- package/dist/tools/createText.js +67 -0
- package/dist/tools/deleteNode.d.ts +15 -0
- package/dist/tools/deleteNode.js +18 -0
- package/dist/tools/execute.d.ts +17 -0
- package/dist/tools/execute.js +68 -0
- package/dist/tools/getCurrentPage.d.ts +16 -0
- package/dist/tools/getCurrentPage.js +19 -0
- package/dist/tools/getDesignContext.d.ts +42 -0
- package/dist/tools/getDesignContext.js +55 -0
- package/dist/tools/getLocalComponents.d.ts +20 -0
- package/dist/tools/getLocalComponents.js +23 -0
- package/dist/tools/getNode.d.ts +30 -0
- package/dist/tools/getNode.js +33 -0
- package/dist/tools/getSelection.d.ts +10 -0
- package/dist/tools/getSelection.js +13 -0
- package/dist/tools/getStyles.d.ts +17 -0
- package/dist/tools/getStyles.js +20 -0
- package/dist/tools/getVariables.d.ts +26 -0
- package/dist/tools/getVariables.js +29 -0
- package/dist/tools/moveNode.d.ts +28 -0
- package/dist/tools/moveNode.js +31 -0
- package/dist/tools/openInEditor.d.ts +21 -0
- package/dist/tools/openInEditor.js +98 -0
- package/dist/tools/searchNodes.d.ts +30 -0
- package/dist/tools/searchNodes.js +46 -0
- package/dist/tools/searchTools.d.ts +28 -0
- package/dist/tools/searchTools.js +28 -0
- package/dist/tools/swapComponent.d.ts +23 -0
- package/dist/tools/swapComponent.js +26 -0
- package/dist/tools/updateNode.d.ts +194 -0
- package/dist/tools/updateNode.js +163 -0
- package/dist/types.d.ts +101 -0
- package/dist/types.js +1 -0
- package/dist/websocket.d.ts +30 -0
- package/dist/websocket.js +282 -0
- package/package.json +29 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts Figma node tree to JSX/React code
|
|
3
|
+
*/
|
|
4
|
+
import { FigmaNodeStyle } from './styleConverter.js';
|
|
5
|
+
export interface FigmaNode extends FigmaNodeStyle {
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
type: string;
|
|
9
|
+
children?: FigmaNode[];
|
|
10
|
+
characters?: string;
|
|
11
|
+
componentName?: string;
|
|
12
|
+
componentSetName?: string;
|
|
13
|
+
variantProperties?: Record<string, string>;
|
|
14
|
+
visible?: boolean;
|
|
15
|
+
componentDescription?: string;
|
|
16
|
+
componentDocumentationLinks?: Array<{
|
|
17
|
+
uri: string;
|
|
18
|
+
}>;
|
|
19
|
+
variantGroupProperties?: Record<string, string[]>;
|
|
20
|
+
}
|
|
21
|
+
export interface ImageAsset {
|
|
22
|
+
nodeId: string;
|
|
23
|
+
url: string;
|
|
24
|
+
variableName: string;
|
|
25
|
+
}
|
|
26
|
+
export interface GeneratedComponent {
|
|
27
|
+
name: string;
|
|
28
|
+
code: string;
|
|
29
|
+
isMainComponent: boolean;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Convert a name to a valid React component name (PascalCase)
|
|
33
|
+
*/
|
|
34
|
+
export declare function toComponentName(name: string): string;
|
|
35
|
+
/**
|
|
36
|
+
* Convert a name to a valid variable name (camelCase)
|
|
37
|
+
*/
|
|
38
|
+
export declare function toVariableName(name: string): string;
|
|
39
|
+
/**
|
|
40
|
+
* Generate a complete React component from a Figma node tree
|
|
41
|
+
*/
|
|
42
|
+
export declare function generateComponent(node: FigmaNode, imageUrls: Map<string, string>, componentName?: string): GeneratedComponent;
|
|
43
|
+
/**
|
|
44
|
+
* Generate multiple components if the node tree contains component instances
|
|
45
|
+
*/
|
|
46
|
+
export declare function generateAllComponents(rootNode: FigmaNode, imageUrls: Map<string, string>): GeneratedComponent[];
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts Figma node tree to JSX/React code
|
|
3
|
+
*/
|
|
4
|
+
import { getClassName } from './styleConverter.js';
|
|
5
|
+
/**
|
|
6
|
+
* Convert a name to a valid React component name (PascalCase)
|
|
7
|
+
*/
|
|
8
|
+
export function toComponentName(name) {
|
|
9
|
+
return name
|
|
10
|
+
.replace(/[^a-zA-Z0-9\s]/g, ' ')
|
|
11
|
+
.split(/\s+/)
|
|
12
|
+
.filter(Boolean)
|
|
13
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
14
|
+
.join('');
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Convert a name to a valid variable name (camelCase)
|
|
18
|
+
*/
|
|
19
|
+
export function toVariableName(name) {
|
|
20
|
+
const pascal = toComponentName(name);
|
|
21
|
+
return pascal.charAt(0).toLowerCase() + pascal.slice(1);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Escape text content for JSX
|
|
25
|
+
*/
|
|
26
|
+
function escapeJsxText(text) {
|
|
27
|
+
return text
|
|
28
|
+
.replace(/&/g, '&')
|
|
29
|
+
.replace(/</g, '<')
|
|
30
|
+
.replace(/>/g, '>')
|
|
31
|
+
.replace(/"/g, '"')
|
|
32
|
+
.replace(/'/g, ''')
|
|
33
|
+
.replace(/\{/g, '{')
|
|
34
|
+
.replace(/\}/g, '}');
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Generate indentation string
|
|
38
|
+
*/
|
|
39
|
+
function indent(level) {
|
|
40
|
+
return ' '.repeat(level);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Check if a node should be rendered as an image
|
|
44
|
+
*/
|
|
45
|
+
function isImageNode(node, imageUrls) {
|
|
46
|
+
return imageUrls.has(node.id);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Get the appropriate HTML element for a Figma node type
|
|
50
|
+
*/
|
|
51
|
+
function getElementTag(node) {
|
|
52
|
+
switch (node.type) {
|
|
53
|
+
case 'TEXT':
|
|
54
|
+
return 'p';
|
|
55
|
+
case 'RECTANGLE':
|
|
56
|
+
case 'ELLIPSE':
|
|
57
|
+
case 'POLYGON':
|
|
58
|
+
case 'STAR':
|
|
59
|
+
case 'VECTOR':
|
|
60
|
+
case 'BOOLEAN_OPERATION':
|
|
61
|
+
return 'div';
|
|
62
|
+
case 'FRAME':
|
|
63
|
+
case 'GROUP':
|
|
64
|
+
case 'COMPONENT':
|
|
65
|
+
case 'COMPONENT_SET':
|
|
66
|
+
return 'div';
|
|
67
|
+
case 'INSTANCE':
|
|
68
|
+
return 'div'; // Will be handled specially for component instances
|
|
69
|
+
default:
|
|
70
|
+
return 'div';
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Generate JSX for a single node
|
|
75
|
+
*/
|
|
76
|
+
function generateNodeJsx(node, imageUrls, ctx) {
|
|
77
|
+
// Skip invisible nodes
|
|
78
|
+
if (node.visible === false) {
|
|
79
|
+
return '';
|
|
80
|
+
}
|
|
81
|
+
const indentStr = indent(ctx.indent);
|
|
82
|
+
// Handle image nodes
|
|
83
|
+
if (isImageNode(node, imageUrls)) {
|
|
84
|
+
const imageUrl = imageUrls.get(node.id);
|
|
85
|
+
const varName = `img${toComponentName(node.name)}`;
|
|
86
|
+
// Track this image asset
|
|
87
|
+
ctx.imageAssets.push({
|
|
88
|
+
nodeId: node.id,
|
|
89
|
+
url: imageUrl,
|
|
90
|
+
variableName: varName,
|
|
91
|
+
});
|
|
92
|
+
const className = getClassName(node);
|
|
93
|
+
const classAttr = className ? ` className="${className}"` : '';
|
|
94
|
+
return `${indentStr}<img src={${varName}}${classAttr} alt="${escapeJsxText(node.name)}" data-node-id="${node.id}" />`;
|
|
95
|
+
}
|
|
96
|
+
// Handle text nodes
|
|
97
|
+
if (node.type === 'TEXT' && node.characters) {
|
|
98
|
+
const className = getClassName(node, true);
|
|
99
|
+
const classAttr = className ? ` className="${className}"` : '';
|
|
100
|
+
const text = escapeJsxText(node.characters);
|
|
101
|
+
// Use span for inline text, p for block text
|
|
102
|
+
const tag = node.characters.includes('\n') ? 'p' : 'span';
|
|
103
|
+
return `${indentStr}<${tag}${classAttr} data-node-id="${node.id}">${text}</${tag}>`;
|
|
104
|
+
}
|
|
105
|
+
// Handle component instances
|
|
106
|
+
if (node.type === 'INSTANCE' && node.componentName) {
|
|
107
|
+
const componentName = toComponentName(node.componentName);
|
|
108
|
+
ctx.componentInstances.add(componentName);
|
|
109
|
+
// Build props from variant properties
|
|
110
|
+
const props = [];
|
|
111
|
+
if (node.variantProperties) {
|
|
112
|
+
for (const [key, value] of Object.entries(node.variantProperties)) {
|
|
113
|
+
const propName = toVariableName(key);
|
|
114
|
+
props.push(`${propName}="${value}"`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const propsStr = props.length > 0 ? ' ' + props.join(' ') : '';
|
|
118
|
+
return `${indentStr}<${componentName}${propsStr} data-node-id="${node.id}" />`;
|
|
119
|
+
}
|
|
120
|
+
// Handle container nodes
|
|
121
|
+
const tag = getElementTag(node);
|
|
122
|
+
const className = getClassName(node);
|
|
123
|
+
const classAttr = className ? ` className="${className}"` : '';
|
|
124
|
+
// If no children, self-close
|
|
125
|
+
if (!node.children || node.children.length === 0) {
|
|
126
|
+
return `${indentStr}<${tag}${classAttr} data-node-id="${node.id}" />`;
|
|
127
|
+
}
|
|
128
|
+
// Generate children
|
|
129
|
+
const childCtx = { ...ctx, indent: ctx.indent + 1 };
|
|
130
|
+
const childrenJsx = node.children
|
|
131
|
+
.map((child) => generateNodeJsx(child, imageUrls, childCtx))
|
|
132
|
+
.filter(Boolean)
|
|
133
|
+
.join('\n');
|
|
134
|
+
if (!childrenJsx) {
|
|
135
|
+
return `${indentStr}<${tag}${classAttr} data-node-id="${node.id}" />`;
|
|
136
|
+
}
|
|
137
|
+
return `${indentStr}<${tag}${classAttr} data-node-id="${node.id}">\n${childrenJsx}\n${indentStr}</${tag}>`;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Generate image variable declarations
|
|
141
|
+
*/
|
|
142
|
+
function generateImageDeclarations(assets) {
|
|
143
|
+
if (assets.length === 0)
|
|
144
|
+
return '';
|
|
145
|
+
const declarations = assets.map((asset) => `const ${asset.variableName} = "${asset.url}";`);
|
|
146
|
+
return declarations.join('\n') + '\n\n';
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Generate a complete React component from a Figma node tree
|
|
150
|
+
*/
|
|
151
|
+
export function generateComponent(node, imageUrls, componentName) {
|
|
152
|
+
const name = componentName || toComponentName(node.name) || 'Component';
|
|
153
|
+
const ctx = {
|
|
154
|
+
imageAssets: [],
|
|
155
|
+
componentInstances: new Set(),
|
|
156
|
+
indent: 2,
|
|
157
|
+
};
|
|
158
|
+
const jsx = generateNodeJsx(node, imageUrls, ctx);
|
|
159
|
+
// Generate image declarations
|
|
160
|
+
const imageDeclarations = generateImageDeclarations(ctx.imageAssets);
|
|
161
|
+
// Build the component code
|
|
162
|
+
const code = `${imageDeclarations}export default function ${name}() {
|
|
163
|
+
return (
|
|
164
|
+
${jsx}
|
|
165
|
+
);
|
|
166
|
+
}`;
|
|
167
|
+
return {
|
|
168
|
+
name,
|
|
169
|
+
code,
|
|
170
|
+
isMainComponent: true,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Generate multiple components if the node tree contains component instances
|
|
175
|
+
*/
|
|
176
|
+
export function generateAllComponents(rootNode, imageUrls) {
|
|
177
|
+
const components = [];
|
|
178
|
+
// Generate the main component
|
|
179
|
+
const mainComponent = generateComponent(rootNode, imageUrls);
|
|
180
|
+
components.push(mainComponent);
|
|
181
|
+
return components;
|
|
182
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts Figma node styles to Tailwind CSS classes
|
|
3
|
+
*/
|
|
4
|
+
interface FigmaColor {
|
|
5
|
+
r: number;
|
|
6
|
+
g: number;
|
|
7
|
+
b: number;
|
|
8
|
+
}
|
|
9
|
+
interface FigmaFill {
|
|
10
|
+
type: string;
|
|
11
|
+
color?: FigmaColor;
|
|
12
|
+
opacity?: number;
|
|
13
|
+
}
|
|
14
|
+
interface FigmaStroke {
|
|
15
|
+
type: string;
|
|
16
|
+
color?: FigmaColor;
|
|
17
|
+
opacity?: number;
|
|
18
|
+
}
|
|
19
|
+
interface FigmaEffect {
|
|
20
|
+
type: string;
|
|
21
|
+
color?: {
|
|
22
|
+
r: number;
|
|
23
|
+
g: number;
|
|
24
|
+
b: number;
|
|
25
|
+
a: number;
|
|
26
|
+
};
|
|
27
|
+
offset?: {
|
|
28
|
+
x: number;
|
|
29
|
+
y: number;
|
|
30
|
+
};
|
|
31
|
+
radius?: number;
|
|
32
|
+
spread?: number;
|
|
33
|
+
visible?: boolean;
|
|
34
|
+
}
|
|
35
|
+
export interface FigmaNodeStyle {
|
|
36
|
+
width?: number;
|
|
37
|
+
height?: number;
|
|
38
|
+
fills?: FigmaFill[];
|
|
39
|
+
strokes?: FigmaStroke[];
|
|
40
|
+
strokeWeight?: number;
|
|
41
|
+
cornerRadius?: number;
|
|
42
|
+
topLeftRadius?: number;
|
|
43
|
+
topRightRadius?: number;
|
|
44
|
+
bottomLeftRadius?: number;
|
|
45
|
+
bottomRightRadius?: number;
|
|
46
|
+
effects?: FigmaEffect[];
|
|
47
|
+
layoutMode?: 'NONE' | 'HORIZONTAL' | 'VERTICAL';
|
|
48
|
+
primaryAxisAlignItems?: 'MIN' | 'CENTER' | 'MAX' | 'SPACE_BETWEEN';
|
|
49
|
+
counterAxisAlignItems?: 'MIN' | 'CENTER' | 'MAX' | 'BASELINE';
|
|
50
|
+
itemSpacing?: number;
|
|
51
|
+
paddingTop?: number;
|
|
52
|
+
paddingBottom?: number;
|
|
53
|
+
paddingLeft?: number;
|
|
54
|
+
paddingRight?: number;
|
|
55
|
+
layoutWrap?: 'NO_WRAP' | 'WRAP';
|
|
56
|
+
layoutGrow?: number;
|
|
57
|
+
layoutSizingHorizontal?: 'FIXED' | 'HUG' | 'FILL';
|
|
58
|
+
layoutSizingVertical?: 'FIXED' | 'HUG' | 'FILL';
|
|
59
|
+
fontSize?: number;
|
|
60
|
+
fontFamily?: string;
|
|
61
|
+
fontStyle?: string;
|
|
62
|
+
fontWeight?: number | symbol;
|
|
63
|
+
textAlignHorizontal?: 'LEFT' | 'CENTER' | 'RIGHT' | 'JUSTIFIED';
|
|
64
|
+
textAlignVertical?: 'TOP' | 'CENTER' | 'BOTTOM';
|
|
65
|
+
lineHeight?: {
|
|
66
|
+
value: number;
|
|
67
|
+
unit: string;
|
|
68
|
+
} | number | symbol;
|
|
69
|
+
letterSpacing?: {
|
|
70
|
+
value: number;
|
|
71
|
+
unit: string;
|
|
72
|
+
} | number | symbol;
|
|
73
|
+
textDecoration?: 'NONE' | 'UNDERLINE' | 'STRIKETHROUGH';
|
|
74
|
+
textCase?: 'ORIGINAL' | 'UPPER' | 'LOWER' | 'TITLE';
|
|
75
|
+
opacity?: number;
|
|
76
|
+
blendMode?: string;
|
|
77
|
+
clipsContent?: boolean;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Convert RGB values (0-1 range) to hex color
|
|
81
|
+
*/
|
|
82
|
+
export declare function rgbToHex(r: number, g: number, b: number): string;
|
|
83
|
+
/**
|
|
84
|
+
* Convert Figma node styles to Tailwind classes
|
|
85
|
+
*/
|
|
86
|
+
export declare function convertToTailwind(node: FigmaNodeStyle): string[];
|
|
87
|
+
/**
|
|
88
|
+
* Convert text-specific styles to Tailwind classes
|
|
89
|
+
*/
|
|
90
|
+
export declare function convertTextToTailwind(node: FigmaNodeStyle): string[];
|
|
91
|
+
/**
|
|
92
|
+
* Combine all styles into a className string
|
|
93
|
+
*/
|
|
94
|
+
export declare function getClassName(node: FigmaNodeStyle, isText?: boolean): string;
|
|
95
|
+
export {};
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts Figma node styles to Tailwind CSS classes
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Convert RGB values (0-1 range) to hex color
|
|
6
|
+
*/
|
|
7
|
+
export function rgbToHex(r, g, b) {
|
|
8
|
+
const toHex = (n) => {
|
|
9
|
+
const hex = Math.round(n * 255)
|
|
10
|
+
.toString(16)
|
|
11
|
+
.padStart(2, '0');
|
|
12
|
+
return hex;
|
|
13
|
+
};
|
|
14
|
+
return `#${toHex(r)}${toHex(g)}${toHex(b)}`.toUpperCase();
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Convert Figma node styles to Tailwind classes
|
|
18
|
+
*/
|
|
19
|
+
export function convertToTailwind(node) {
|
|
20
|
+
const classes = [];
|
|
21
|
+
// Dimensions
|
|
22
|
+
if (node.layoutSizingHorizontal === 'FILL') {
|
|
23
|
+
classes.push('w-full');
|
|
24
|
+
}
|
|
25
|
+
else if (node.width !== undefined && node.layoutSizingHorizontal !== 'HUG') {
|
|
26
|
+
classes.push(`w-[${Math.round(node.width)}px]`);
|
|
27
|
+
}
|
|
28
|
+
if (node.layoutSizingVertical === 'FILL') {
|
|
29
|
+
classes.push('h-full');
|
|
30
|
+
}
|
|
31
|
+
else if (node.height !== undefined && node.layoutSizingVertical !== 'HUG') {
|
|
32
|
+
classes.push(`h-[${Math.round(node.height)}px]`);
|
|
33
|
+
}
|
|
34
|
+
// Auto-layout (Flexbox)
|
|
35
|
+
if (node.layoutMode && node.layoutMode !== 'NONE') {
|
|
36
|
+
classes.push('flex');
|
|
37
|
+
classes.push(node.layoutMode === 'VERTICAL' ? 'flex-col' : 'flex-row');
|
|
38
|
+
// Wrap
|
|
39
|
+
if (node.layoutWrap === 'WRAP') {
|
|
40
|
+
classes.push('flex-wrap');
|
|
41
|
+
}
|
|
42
|
+
// Gap/spacing
|
|
43
|
+
if (node.itemSpacing !== undefined && node.itemSpacing > 0) {
|
|
44
|
+
classes.push(`gap-[${Math.round(node.itemSpacing)}px]`);
|
|
45
|
+
}
|
|
46
|
+
// Primary axis alignment (justify-content)
|
|
47
|
+
if (node.primaryAxisAlignItems) {
|
|
48
|
+
const justifyMap = {
|
|
49
|
+
MIN: 'justify-start',
|
|
50
|
+
CENTER: 'justify-center',
|
|
51
|
+
MAX: 'justify-end',
|
|
52
|
+
SPACE_BETWEEN: 'justify-between',
|
|
53
|
+
};
|
|
54
|
+
if (justifyMap[node.primaryAxisAlignItems] && node.primaryAxisAlignItems !== 'MIN') {
|
|
55
|
+
classes.push(justifyMap[node.primaryAxisAlignItems]);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// Counter axis alignment (align-items)
|
|
59
|
+
if (node.counterAxisAlignItems) {
|
|
60
|
+
const alignMap = {
|
|
61
|
+
MIN: 'items-start',
|
|
62
|
+
CENTER: 'items-center',
|
|
63
|
+
MAX: 'items-end',
|
|
64
|
+
BASELINE: 'items-baseline',
|
|
65
|
+
};
|
|
66
|
+
if (alignMap[node.counterAxisAlignItems] && node.counterAxisAlignItems !== 'MIN') {
|
|
67
|
+
classes.push(alignMap[node.counterAxisAlignItems]);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// Padding
|
|
72
|
+
const pt = node.paddingTop ?? 0;
|
|
73
|
+
const pb = node.paddingBottom ?? 0;
|
|
74
|
+
const pl = node.paddingLeft ?? 0;
|
|
75
|
+
const pr = node.paddingRight ?? 0;
|
|
76
|
+
if (pt === pb && pl === pr && pt === pl && pt > 0) {
|
|
77
|
+
// All padding equal
|
|
78
|
+
classes.push(`p-[${Math.round(pt)}px]`);
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
if (pt === pb && pt > 0) {
|
|
82
|
+
classes.push(`py-[${Math.round(pt)}px]`);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
if (pt > 0)
|
|
86
|
+
classes.push(`pt-[${Math.round(pt)}px]`);
|
|
87
|
+
if (pb > 0)
|
|
88
|
+
classes.push(`pb-[${Math.round(pb)}px]`);
|
|
89
|
+
}
|
|
90
|
+
if (pl === pr && pl > 0) {
|
|
91
|
+
classes.push(`px-[${Math.round(pl)}px]`);
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
if (pl > 0)
|
|
95
|
+
classes.push(`pl-[${Math.round(pl)}px]`);
|
|
96
|
+
if (pr > 0)
|
|
97
|
+
classes.push(`pr-[${Math.round(pr)}px]`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// Background color
|
|
101
|
+
if (node.fills && node.fills.length > 0) {
|
|
102
|
+
const fill = node.fills[0];
|
|
103
|
+
if (fill.type === 'SOLID' && fill.color) {
|
|
104
|
+
const hex = rgbToHex(fill.color.r, fill.color.g, fill.color.b);
|
|
105
|
+
classes.push(`bg-[${hex}]`);
|
|
106
|
+
if (fill.opacity !== undefined && fill.opacity < 1) {
|
|
107
|
+
classes.push(`bg-opacity-[${Math.round(fill.opacity * 100)}]`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// Border/stroke
|
|
112
|
+
if (node.strokes && node.strokes.length > 0) {
|
|
113
|
+
const stroke = node.strokes[0];
|
|
114
|
+
if (stroke.type === 'SOLID' && stroke.color) {
|
|
115
|
+
const hex = rgbToHex(stroke.color.r, stroke.color.g, stroke.color.b);
|
|
116
|
+
const weight = node.strokeWeight ?? 1;
|
|
117
|
+
if (weight === 1) {
|
|
118
|
+
classes.push('border');
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
classes.push(`border-[${Math.round(weight)}px]`);
|
|
122
|
+
}
|
|
123
|
+
classes.push(`border-[${hex}]`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// Corner radius
|
|
127
|
+
if (node.cornerRadius !== undefined && node.cornerRadius > 0) {
|
|
128
|
+
if (typeof node.cornerRadius === 'number') {
|
|
129
|
+
classes.push(`rounded-[${Math.round(node.cornerRadius)}px]`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
else if (node.topLeftRadius !== undefined ||
|
|
133
|
+
node.topRightRadius !== undefined ||
|
|
134
|
+
node.bottomLeftRadius !== undefined ||
|
|
135
|
+
node.bottomRightRadius !== undefined) {
|
|
136
|
+
// Individual corner radii
|
|
137
|
+
const tl = node.topLeftRadius ?? 0;
|
|
138
|
+
const tr = node.topRightRadius ?? 0;
|
|
139
|
+
const bl = node.bottomLeftRadius ?? 0;
|
|
140
|
+
const br = node.bottomRightRadius ?? 0;
|
|
141
|
+
if (tl === tr && bl === br && tl === bl && tl > 0) {
|
|
142
|
+
classes.push(`rounded-[${Math.round(tl)}px]`);
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
if (tl > 0)
|
|
146
|
+
classes.push(`rounded-tl-[${Math.round(tl)}px]`);
|
|
147
|
+
if (tr > 0)
|
|
148
|
+
classes.push(`rounded-tr-[${Math.round(tr)}px]`);
|
|
149
|
+
if (bl > 0)
|
|
150
|
+
classes.push(`rounded-bl-[${Math.round(bl)}px]`);
|
|
151
|
+
if (br > 0)
|
|
152
|
+
classes.push(`rounded-br-[${Math.round(br)}px]`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// Effects (shadows)
|
|
156
|
+
if (node.effects && node.effects.length > 0) {
|
|
157
|
+
for (const effect of node.effects) {
|
|
158
|
+
if ((effect.type === 'DROP_SHADOW' || effect.type === 'INNER_SHADOW') &&
|
|
159
|
+
effect.visible !== false) {
|
|
160
|
+
if (effect.color && effect.offset && effect.radius !== undefined) {
|
|
161
|
+
const { r, g, b, a } = effect.color;
|
|
162
|
+
const rgba = `rgba(${Math.round(r * 255)},${Math.round(g * 255)},${Math.round(b * 255)},${a.toFixed(2)})`;
|
|
163
|
+
const shadow = `${effect.offset.x}px ${effect.offset.y}px ${effect.radius}px ${effect.spread ?? 0}px ${rgba}`;
|
|
164
|
+
if (effect.type === 'INNER_SHADOW') {
|
|
165
|
+
classes.push(`shadow-[inset_${shadow.replace(/\s+/g, '_')}]`);
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
classes.push(`shadow-[${shadow.replace(/\s+/g, '_')}]`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// Opacity
|
|
175
|
+
if (node.opacity !== undefined && node.opacity < 1) {
|
|
176
|
+
classes.push(`opacity-[${Math.round(node.opacity * 100)}]`);
|
|
177
|
+
}
|
|
178
|
+
// Overflow (clipping)
|
|
179
|
+
if (node.clipsContent === true) {
|
|
180
|
+
classes.push('overflow-hidden');
|
|
181
|
+
}
|
|
182
|
+
// Flex grow
|
|
183
|
+
if (node.layoutGrow !== undefined && node.layoutGrow > 0) {
|
|
184
|
+
classes.push('flex-1');
|
|
185
|
+
}
|
|
186
|
+
return classes;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Convert text-specific styles to Tailwind classes
|
|
190
|
+
*/
|
|
191
|
+
export function convertTextToTailwind(node) {
|
|
192
|
+
const classes = [];
|
|
193
|
+
// Font size
|
|
194
|
+
if (node.fontSize !== undefined) {
|
|
195
|
+
classes.push(`text-[${Math.round(node.fontSize)}px]`);
|
|
196
|
+
}
|
|
197
|
+
// Font weight
|
|
198
|
+
if (node.fontWeight !== undefined && typeof node.fontWeight === 'number') {
|
|
199
|
+
const weightMap = {
|
|
200
|
+
100: 'font-thin',
|
|
201
|
+
200: 'font-extralight',
|
|
202
|
+
300: 'font-light',
|
|
203
|
+
400: 'font-normal',
|
|
204
|
+
500: 'font-medium',
|
|
205
|
+
600: 'font-semibold',
|
|
206
|
+
700: 'font-bold',
|
|
207
|
+
800: 'font-extrabold',
|
|
208
|
+
900: 'font-black',
|
|
209
|
+
};
|
|
210
|
+
if (weightMap[node.fontWeight]) {
|
|
211
|
+
classes.push(weightMap[node.fontWeight]);
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
classes.push(`font-[${node.fontWeight}]`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
else if (node.fontStyle) {
|
|
218
|
+
// Map font style names to weights
|
|
219
|
+
const styleWeightMap = {
|
|
220
|
+
Thin: 'font-thin',
|
|
221
|
+
ExtraLight: 'font-extralight',
|
|
222
|
+
Light: 'font-light',
|
|
223
|
+
Regular: 'font-normal',
|
|
224
|
+
Medium: 'font-medium',
|
|
225
|
+
SemiBold: 'font-semibold',
|
|
226
|
+
Semibold: 'font-semibold',
|
|
227
|
+
Bold: 'font-bold',
|
|
228
|
+
ExtraBold: 'font-extrabold',
|
|
229
|
+
Black: 'font-black',
|
|
230
|
+
};
|
|
231
|
+
if (styleWeightMap[node.fontStyle]) {
|
|
232
|
+
classes.push(styleWeightMap[node.fontStyle]);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
// Text alignment
|
|
236
|
+
if (node.textAlignHorizontal) {
|
|
237
|
+
const alignMap = {
|
|
238
|
+
LEFT: 'text-left',
|
|
239
|
+
CENTER: 'text-center',
|
|
240
|
+
RIGHT: 'text-right',
|
|
241
|
+
JUSTIFIED: 'text-justify',
|
|
242
|
+
};
|
|
243
|
+
if (alignMap[node.textAlignHorizontal] && node.textAlignHorizontal !== 'LEFT') {
|
|
244
|
+
classes.push(alignMap[node.textAlignHorizontal]);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
// Line height
|
|
248
|
+
if (node.lineHeight && typeof node.lineHeight === 'object' && 'value' in node.lineHeight) {
|
|
249
|
+
const lh = node.lineHeight;
|
|
250
|
+
if (lh.unit === 'PIXELS') {
|
|
251
|
+
classes.push(`leading-[${Math.round(lh.value)}px]`);
|
|
252
|
+
}
|
|
253
|
+
else if (lh.unit === 'PERCENT') {
|
|
254
|
+
classes.push(`leading-[${(lh.value / 100).toFixed(2)}]`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
// Letter spacing
|
|
258
|
+
if (node.letterSpacing &&
|
|
259
|
+
typeof node.letterSpacing === 'object' &&
|
|
260
|
+
'value' in node.letterSpacing) {
|
|
261
|
+
const ls = node.letterSpacing;
|
|
262
|
+
if (ls.unit === 'PIXELS') {
|
|
263
|
+
classes.push(`tracking-[${ls.value.toFixed(1)}px]`);
|
|
264
|
+
}
|
|
265
|
+
else if (ls.unit === 'PERCENT') {
|
|
266
|
+
classes.push(`tracking-[${(ls.value / 100).toFixed(3)}em]`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
// Text decoration
|
|
270
|
+
if (node.textDecoration === 'UNDERLINE') {
|
|
271
|
+
classes.push('underline');
|
|
272
|
+
}
|
|
273
|
+
else if (node.textDecoration === 'STRIKETHROUGH') {
|
|
274
|
+
classes.push('line-through');
|
|
275
|
+
}
|
|
276
|
+
// Text case
|
|
277
|
+
if (node.textCase === 'UPPER') {
|
|
278
|
+
classes.push('uppercase');
|
|
279
|
+
}
|
|
280
|
+
else if (node.textCase === 'LOWER') {
|
|
281
|
+
classes.push('lowercase');
|
|
282
|
+
}
|
|
283
|
+
else if (node.textCase === 'TITLE') {
|
|
284
|
+
classes.push('capitalize');
|
|
285
|
+
}
|
|
286
|
+
// Text color (from fills)
|
|
287
|
+
if (node.fills && node.fills.length > 0) {
|
|
288
|
+
const fill = node.fills[0];
|
|
289
|
+
if (fill.type === 'SOLID' && fill.color) {
|
|
290
|
+
const hex = rgbToHex(fill.color.r, fill.color.g, fill.color.b);
|
|
291
|
+
classes.push(`text-[${hex}]`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return classes;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Combine all styles into a className string
|
|
298
|
+
*/
|
|
299
|
+
export function getClassName(node, isText = false) {
|
|
300
|
+
const baseClasses = convertToTailwind(node);
|
|
301
|
+
const textClasses = isText ? convertTextToTailwind(node) : [];
|
|
302
|
+
const allClasses = [...baseClasses, ...textClasses];
|
|
303
|
+
// Remove duplicates and empty strings
|
|
304
|
+
const uniqueClasses = [...new Set(allClasses)].filter(Boolean);
|
|
305
|
+
return uniqueClasses.join(' ');
|
|
306
|
+
}
|
package/dist/hints.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contextual hints to help AI agents improve their Figma workflow.
|
|
3
|
+
* These are appended to tool responses to encourage best practices.
|
|
4
|
+
*/
|
|
5
|
+
export type HintContext = {
|
|
6
|
+
toolName: string;
|
|
7
|
+
result: unknown;
|
|
8
|
+
args: Record<string, unknown>;
|
|
9
|
+
};
|
|
10
|
+
export declare const IMPROVEMENT_AGENT_PROMPT: string;
|
|
11
|
+
export declare function getImprovementHint(): string | null;
|
|
12
|
+
export declare function resetImprovementHint(): void;
|
|
13
|
+
export declare function getHintsForTool(context: HintContext): string[];
|
|
14
|
+
export declare function formatResponseWithHints(result: unknown, toolName: string, args: Record<string, unknown>): string;
|