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.
Files changed (67) hide show
  1. package/dist/assetServer.d.ts +34 -0
  2. package/dist/assetServer.js +168 -0
  3. package/dist/codeGenerator/componentDetector.d.ts +57 -0
  4. package/dist/codeGenerator/componentDetector.js +171 -0
  5. package/dist/codeGenerator/index.d.ts +77 -0
  6. package/dist/codeGenerator/index.js +184 -0
  7. package/dist/codeGenerator/jsxGenerator.d.ts +46 -0
  8. package/dist/codeGenerator/jsxGenerator.js +182 -0
  9. package/dist/codeGenerator/styleConverter.d.ts +95 -0
  10. package/dist/codeGenerator/styleConverter.js +306 -0
  11. package/dist/hints.d.ts +14 -0
  12. package/dist/hints.js +105 -0
  13. package/dist/hub.d.ts +9 -0
  14. package/dist/hub.js +252 -0
  15. package/dist/index.d.ts +2 -0
  16. package/dist/index.js +146 -0
  17. package/dist/sandbox.d.ts +18 -0
  18. package/dist/sandbox.js +154 -0
  19. package/dist/toolRegistry.d.ts +19 -0
  20. package/dist/toolRegistry.js +729 -0
  21. package/dist/tools/captureScreenshot.d.ts +28 -0
  22. package/dist/tools/captureScreenshot.js +31 -0
  23. package/dist/tools/cloneNode.d.ts +43 -0
  24. package/dist/tools/cloneNode.js +46 -0
  25. package/dist/tools/createFrame.d.ts +157 -0
  26. package/dist/tools/createFrame.js +114 -0
  27. package/dist/tools/createInstance.d.ts +38 -0
  28. package/dist/tools/createInstance.js +41 -0
  29. package/dist/tools/createRectangle.d.ts +108 -0
  30. package/dist/tools/createRectangle.js +77 -0
  31. package/dist/tools/createText.d.ts +81 -0
  32. package/dist/tools/createText.js +67 -0
  33. package/dist/tools/deleteNode.d.ts +15 -0
  34. package/dist/tools/deleteNode.js +18 -0
  35. package/dist/tools/execute.d.ts +17 -0
  36. package/dist/tools/execute.js +68 -0
  37. package/dist/tools/getCurrentPage.d.ts +16 -0
  38. package/dist/tools/getCurrentPage.js +19 -0
  39. package/dist/tools/getDesignContext.d.ts +42 -0
  40. package/dist/tools/getDesignContext.js +55 -0
  41. package/dist/tools/getLocalComponents.d.ts +20 -0
  42. package/dist/tools/getLocalComponents.js +23 -0
  43. package/dist/tools/getNode.d.ts +30 -0
  44. package/dist/tools/getNode.js +33 -0
  45. package/dist/tools/getSelection.d.ts +10 -0
  46. package/dist/tools/getSelection.js +13 -0
  47. package/dist/tools/getStyles.d.ts +17 -0
  48. package/dist/tools/getStyles.js +20 -0
  49. package/dist/tools/getVariables.d.ts +26 -0
  50. package/dist/tools/getVariables.js +29 -0
  51. package/dist/tools/moveNode.d.ts +28 -0
  52. package/dist/tools/moveNode.js +31 -0
  53. package/dist/tools/openInEditor.d.ts +21 -0
  54. package/dist/tools/openInEditor.js +98 -0
  55. package/dist/tools/searchNodes.d.ts +30 -0
  56. package/dist/tools/searchNodes.js +46 -0
  57. package/dist/tools/searchTools.d.ts +28 -0
  58. package/dist/tools/searchTools.js +28 -0
  59. package/dist/tools/swapComponent.d.ts +23 -0
  60. package/dist/tools/swapComponent.js +26 -0
  61. package/dist/tools/updateNode.d.ts +194 -0
  62. package/dist/tools/updateNode.js +163 -0
  63. package/dist/types.d.ts +101 -0
  64. package/dist/types.js +1 -0
  65. package/dist/websocket.d.ts +30 -0
  66. package/dist/websocket.js +282 -0
  67. 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, '&amp;')
29
+ .replace(/</g, '&lt;')
30
+ .replace(/>/g, '&gt;')
31
+ .replace(/"/g, '&quot;')
32
+ .replace(/'/g, '&#39;')
33
+ .replace(/\{/g, '&#123;')
34
+ .replace(/\}/g, '&#125;');
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
+ }
@@ -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;