figmake-pro 3.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/.github/workflows/ci.yml +27 -0
- package/CONTRIBUTING.md +19 -0
- package/LICENSE +21 -0
- package/README.md +47 -0
- package/dist/cli/index.js +22827 -0
- package/dist/plugin/code.js +791 -0
- package/dist/plugin/ui.html +207 -0
- package/package.json +32 -0
- package/src/cli/index.ts +129 -0
- package/src/core/config.ts +21 -0
- package/src/core/converters/layoutConverter.ts +122 -0
- package/src/core/extractors/animationExtractor.ts +104 -0
- package/src/core/extractors/styleExtractor.ts +40 -0
- package/src/core/generators/handlerGenerator.ts +72 -0
- package/src/core/generators/reactGenerator.ts +129 -0
- package/src/core/utils/codeMetrics.ts +54 -0
- package/src/core/utils/collisionDetector.ts +77 -0
- package/src/core/utils/copyManager.ts +33 -0
- package/src/core/utils/generateReadme.ts +70 -0
- package/src/core/utils/imageExporter.ts +34 -0
- package/src/design-system/extractDesignTokens.ts +28 -0
- package/src/design-system/extractPalette.ts +92 -0
- package/src/design-system/extractShadows.ts +33 -0
- package/src/design-system/extractSpacing.ts +34 -0
- package/src/design-system/extractTypography.ts +71 -0
- package/src/plugin/code.ts +143 -0
- package/src/plugin/manifest.json +9 -0
- package/src/plugin/ui.html +207 -0
- package/src/vibecode-guard/generateClaudeRules.ts +25 -0
- package/src/vibecode-guard/generateCopilotInstructions.ts +18 -0
- package/src/vibecode-guard/generateCursorRules.ts +35 -0
- package/src/vibecode-guard/generateLockfile.ts +19 -0
- package/src/vibecode-guard/generatePromptContext.ts +15 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { MappedInteraction } from "../extractors/animationExtractor";
|
|
2
|
+
import { PluginConfig } from "../config";
|
|
3
|
+
|
|
4
|
+
export interface GeneratedHandlers {
|
|
5
|
+
functionDeclarations: string[];
|
|
6
|
+
propMappings: Record<string, string>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function sanitizeName(name: string): string {
|
|
10
|
+
return name.replace(/[^a-zA-Z0-9]/g, "_").replace(/^([0-9])/, "_$1");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function toPascalCase(name: string): string {
|
|
14
|
+
return sanitizeName(name)
|
|
15
|
+
.split("_")
|
|
16
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
17
|
+
.join("");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function generateHandlers(nodeId: string, nodeName: string, interactions: MappedInteraction[], config?: PluginConfig): GeneratedHandlers {
|
|
21
|
+
const functionDeclarations: string[] = [];
|
|
22
|
+
const propMappings: Record<string, string> = {};
|
|
23
|
+
|
|
24
|
+
interactions.forEach((interaction, index) => {
|
|
25
|
+
const trigger = interaction.trigger;
|
|
26
|
+
const actionType = interaction.actionType;
|
|
27
|
+
const suffix = interactions.length > 1 ? `_${index}` : "";
|
|
28
|
+
const baseName = `${toPascalCase(nodeName)}${suffix}`;
|
|
29
|
+
|
|
30
|
+
let handlerName = "";
|
|
31
|
+
let body = "";
|
|
32
|
+
|
|
33
|
+
if (actionType === "NODE" || actionType === "NAVIGATE") {
|
|
34
|
+
handlerName = `handleNavigate${baseName}`;
|
|
35
|
+
const dest = interaction.destinationName || interaction.destinationId || "Unknown";
|
|
36
|
+
|
|
37
|
+
if (config?.routing === 'nextjs') {
|
|
38
|
+
body = ` router.push('/${dest.toLowerCase().replace(/\s+/g, '-')}');`;
|
|
39
|
+
} else if (config?.routing === 'react-router') {
|
|
40
|
+
body = ` navigate('/${dest.toLowerCase().replace(/\s+/g, '-')}');`;
|
|
41
|
+
} else {
|
|
42
|
+
body = ` setActiveView('${toPascalCase(dest)}');`;
|
|
43
|
+
}
|
|
44
|
+
} else if (actionType === "URL") {
|
|
45
|
+
handlerName = `handleOpenLink${baseName}`;
|
|
46
|
+
body = ` window.open('${interaction.destinationId || "#"}', '_blank');`;
|
|
47
|
+
} else {
|
|
48
|
+
handlerName = `handleInteraction${baseName}`;
|
|
49
|
+
body = ` console.log('${interaction.trigger} triggered');`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (handlerName) {
|
|
53
|
+
functionDeclarations.push(` const ${handlerName} = () => {\n${body}\n };`);
|
|
54
|
+
const reactProp = mapTriggerToReactProp(trigger);
|
|
55
|
+
if (reactProp) propMappings[reactProp] = handlerName;
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return { functionDeclarations, propMappings };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function mapTriggerToReactProp(trigger: string): string | null {
|
|
63
|
+
const map: Record<string, string> = {
|
|
64
|
+
click: "onClick",
|
|
65
|
+
hover: "onMouseEnter",
|
|
66
|
+
mouseEnter: "onMouseEnter",
|
|
67
|
+
mouseLeave: "onMouseLeave",
|
|
68
|
+
press: "onMouseDown",
|
|
69
|
+
drag: "onDrag",
|
|
70
|
+
};
|
|
71
|
+
return map[trigger] || null;
|
|
72
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { extractNodeStyles } from "../extractors/styleExtractor";
|
|
2
|
+
import { PluginConfig, DEFAULT_CONFIG } from "../config";
|
|
3
|
+
import { generateHandlers } from "./handlerGenerator";
|
|
4
|
+
import { extractAnimations } from "../extractors/animationExtractor";
|
|
5
|
+
|
|
6
|
+
function generateHash(content: string): string {
|
|
7
|
+
let hash = 0;
|
|
8
|
+
for (let i = 0; i < content.length; i++) {
|
|
9
|
+
const char = content.charCodeAt(i);
|
|
10
|
+
hash = ((hash << 5) - hash) + char;
|
|
11
|
+
hash = hash & hash;
|
|
12
|
+
}
|
|
13
|
+
return Math.abs(hash).toString(16).substring(0, 8);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function sanitizeName(name: string): string {
|
|
17
|
+
return name.replace(/[^a-zA-Z0-9]/g, "_").replace(/^([0-9])/, "_$1");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function toPascalCase(name: string, naming?: string): string {
|
|
21
|
+
return sanitizeName(name)
|
|
22
|
+
.split("_")
|
|
23
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
24
|
+
.join("");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function generateReactComponent(node: any, options: { componentName?: string, getNodeById?: (id: string) => any, config?: PluginConfig, nameOverrides?: Map<string, string> } = {}): { code: string, files: Record<string, string>, hasAnimations: boolean, hash: string } {
|
|
28
|
+
const config = options.config || DEFAULT_CONFIG;
|
|
29
|
+
const files: Record<string, string> = {};
|
|
30
|
+
const componentName = options.nameOverrides?.get(node.id) || options.componentName || toPascalCase(node.name, config.naming);
|
|
31
|
+
const getNodeById = options.getNodeById;
|
|
32
|
+
let hasAnimations = false;
|
|
33
|
+
const allHandlerDeclarations: string[] = [];
|
|
34
|
+
|
|
35
|
+
const generateNodeCode = (n: any, indent: string = " ", parentAnimations?: any): string => {
|
|
36
|
+
if (!n.animations && n.reactions) {
|
|
37
|
+
n.animations = extractAnimations(n, getNodeById);
|
|
38
|
+
}
|
|
39
|
+
const styles = extractNodeStyles(n, config);
|
|
40
|
+
const motionProps: any = {};
|
|
41
|
+
const eventHandlers: Record<string, string> = {};
|
|
42
|
+
|
|
43
|
+
if (n.animations && n.animations.interactions.length > 0) {
|
|
44
|
+
if (config.animations !== 'css') {
|
|
45
|
+
hasAnimations = true;
|
|
46
|
+
}
|
|
47
|
+
const { functionDeclarations, propMappings } = generateHandlers(n.id, n.name, n.animations.interactions, config);
|
|
48
|
+
allHandlerDeclarations.push(...functionDeclarations);
|
|
49
|
+
Object.assign(eventHandlers, propMappings);
|
|
50
|
+
|
|
51
|
+
n.animations.interactions.forEach((interaction: any) => {
|
|
52
|
+
if (interaction.trigger === "whileHover") motionProps.whileHover = { scale: 1.05 };
|
|
53
|
+
else if (interaction.trigger === "whileTap") motionProps.whileTap = { scale: 0.95 };
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (parentAnimations) {
|
|
58
|
+
const interaction = parentAnimations.interactions.find((i: any) => i.childDeltas);
|
|
59
|
+
if (interaction && interaction.childDeltas[n.name]) {
|
|
60
|
+
hasAnimations = true;
|
|
61
|
+
const delta = interaction.childDeltas[n.name];
|
|
62
|
+
motionProps.layoutId = sanitizeName(n.name).toLowerCase();
|
|
63
|
+
const initial: any = {};
|
|
64
|
+
if (delta.x !== undefined) initial.x = -delta.x;
|
|
65
|
+
if (delta.y !== undefined) initial.y = -delta.y;
|
|
66
|
+
if (delta.opacity !== undefined) initial.opacity = 0;
|
|
67
|
+
if (Object.keys(initial).length > 0) {
|
|
68
|
+
motionProps.initial = initial;
|
|
69
|
+
motionProps.animate = { x: 0, y: 0, opacity: 1 };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (n.layoutMode === "HORIZONTAL" || n.layoutMode === "VERTICAL") {
|
|
75
|
+
styles.display = "flex";
|
|
76
|
+
styles.flexDirection = n.layoutMode === "HORIZONTAL" ? "row" : "column";
|
|
77
|
+
styles.gap = `${n.itemSpacing || 0}px`;
|
|
78
|
+
styles.padding = `${n.paddingTop || 0}px ${n.paddingRight || 0}px ${n.paddingBottom || 0}px ${n.paddingLeft || 0}px`;
|
|
79
|
+
} else if (n.type !== "GROUP" && n.type !== "DOCUMENT" && n.type !== "PAGE") {
|
|
80
|
+
styles.position = "absolute";
|
|
81
|
+
styles.left = `${n.x}px`;
|
|
82
|
+
styles.top = `${n.y}px`;
|
|
83
|
+
styles.width = `${n.width}px`;
|
|
84
|
+
styles.height = `${n.height}px`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const styleAttr = `style={${JSON.stringify(styles, null, 2).replace(/"([^"]+)":/g, '$1:')}}`;
|
|
88
|
+
const motionAttr = Object.keys(motionProps).length > 0 ? ` ${Object.entries(motionProps).map(([k, v]) => `${k}={${JSON.stringify(v)}}`).join(" ")}` : "";
|
|
89
|
+
const handlerAttr = Object.keys(eventHandlers).length > 0 ? ` ${Object.entries(eventHandlers).map(([k, v]) => `${k}={${v}}`).join(" ")}` : "";
|
|
90
|
+
const syncAttrs = `data-figma-id="${n.id}"`;
|
|
91
|
+
const tagPrefix = Object.keys(motionProps).length > 0 ? "motion." : "";
|
|
92
|
+
|
|
93
|
+
if (n.type === "TEXT") return `${indent}<${tagPrefix}span ${syncAttrs}${handlerAttr} ${styleAttr}${motionAttr}>${n.characters}</${tagPrefix}span>`;
|
|
94
|
+
if (n.type === "VECTOR" || n.type === "STAR" || n.type === "POLYGON" || (n.type === "ELLIPSE" && !n.cornerRadius)) return `${indent}<${tagPrefix}div ${syncAttrs}${handlerAttr} ${styleAttr}${motionAttr}>/* SVG Placeholder */</${tagPrefix}div>`;
|
|
95
|
+
|
|
96
|
+
const childrenCode = n.children ? n.children.map((child: any) => generateNodeCode(child, indent + " ", n.animations)).join("\n") : "";
|
|
97
|
+
let tag = "div";
|
|
98
|
+
if (n.type === "GROUP") return childrenCode;
|
|
99
|
+
if (n.type === "SECTION") tag = "section";
|
|
100
|
+
return `${indent}<${tagPrefix}${tag} ${syncAttrs}${handlerAttr} ${styleAttr}${motionAttr}>\n${childrenCode}\n${indent}</${tagPrefix}${tag}>`;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const nodeCode = generateNodeCode(node, " ");
|
|
104
|
+
const contentHash = generateHash(nodeCode);
|
|
105
|
+
const imports = ["import React from 'react';"];
|
|
106
|
+
if (hasAnimations) imports.push("import { motion, AnimatePresence } from 'framer-motion';");
|
|
107
|
+
|
|
108
|
+
const finalCode = `
|
|
109
|
+
${imports.join("\n")}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Figma Layer: ${node.name}
|
|
113
|
+
* ID: ${node.id}
|
|
114
|
+
* Hash: ${contentHash}
|
|
115
|
+
*/
|
|
116
|
+
export const ${componentName}: React.FC = () => {
|
|
117
|
+
${Array.from(new Set(allHandlerDeclarations)).join("\n\n")}
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
${hasAnimations ? '<AnimatePresence mode="wait">' : ''}
|
|
121
|
+
${nodeCode}
|
|
122
|
+
${hasAnimations ? '</AnimatePresence>' : ''}
|
|
123
|
+
);
|
|
124
|
+
};
|
|
125
|
+
`;
|
|
126
|
+
|
|
127
|
+
files[`${componentName}.tsx`] = finalCode;
|
|
128
|
+
return { code: finalCode, files, hasAnimations, hash: contentHash };
|
|
129
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export interface CodeMetrics {
|
|
2
|
+
totalLines: number;
|
|
3
|
+
totalCharacters: number;
|
|
4
|
+
fileCount: number;
|
|
5
|
+
jsxElements: number;
|
|
6
|
+
styleProperties: number;
|
|
7
|
+
handlers: number;
|
|
8
|
+
animations: number;
|
|
9
|
+
collisions: number;
|
|
10
|
+
complexity: 'simple' | 'moderate' | 'complex' | 'very-complex';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function calculateMetrics(nodes: any[]): CodeMetrics {
|
|
14
|
+
let totalLines = 0;
|
|
15
|
+
let totalCharacters = 0;
|
|
16
|
+
let jsxElements = 0;
|
|
17
|
+
let styleProperties = 0;
|
|
18
|
+
let handlers = 0;
|
|
19
|
+
let animations = 0;
|
|
20
|
+
|
|
21
|
+
nodes.forEach(node => {
|
|
22
|
+
if (node.reactCode) {
|
|
23
|
+
totalLines += node.reactCode.split('\n').length;
|
|
24
|
+
totalCharacters += node.reactCode.length;
|
|
25
|
+
|
|
26
|
+
// Heuristic counting
|
|
27
|
+
jsxElements += (node.reactCode.match(/<[a-zA-Z]/g) || []).length;
|
|
28
|
+
handlers += (node.reactCode.match(/handle[A-Z]/g) || []).length;
|
|
29
|
+
if (node.hasAnimations) animations++;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Recurse into styles if possible or use count from properties
|
|
33
|
+
if (node.css) {
|
|
34
|
+
styleProperties += (node.css.match(/:/g) || []).length;
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
let complexity: CodeMetrics['complexity'] = 'simple';
|
|
39
|
+
if (totalLines > 500 || jsxElements > 20) complexity = 'moderate';
|
|
40
|
+
if (totalLines > 1000 || jsxElements > 50) complexity = 'complex';
|
|
41
|
+
if (totalLines > 2000) complexity = 'very-complex';
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
totalLines,
|
|
45
|
+
totalCharacters,
|
|
46
|
+
fileCount: nodes.length,
|
|
47
|
+
jsxElements,
|
|
48
|
+
styleProperties,
|
|
49
|
+
handlers,
|
|
50
|
+
animations,
|
|
51
|
+
collisions: 0, // Set externally
|
|
52
|
+
complexity
|
|
53
|
+
};
|
|
54
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
export interface CollisionWarning {
|
|
2
|
+
type: 'exact' | 'caseInsensitive' | 'sanitized' | 'reserved';
|
|
3
|
+
name1: string;
|
|
4
|
+
name2: string;
|
|
5
|
+
figmaId1: string;
|
|
6
|
+
figmaId2: string;
|
|
7
|
+
suggestion: string;
|
|
8
|
+
severity: 'error' | 'warning';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const RESERVED_WORDS = new Set([
|
|
12
|
+
'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger', 'default', 'delete', 'do', 'else', 'enum', 'export', 'extends', 'false', 'finally', 'for', 'function', 'if', 'import', 'in', 'instanceof', 'new', 'null', 'return', 'super', 'switch', 'this', 'throw', 'true', 'try', 'typeof', 'var', 'void', 'while', 'with', 'yield', 'let', 'static', 'await', 'abstract', 'boolean', 'byte', 'char', 'double', 'final', 'float', 'goto', 'int', 'long', 'native', 'short', 'synchronized', 'transient', 'volatile'
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
export function detectCollisions(nodes: any[]): CollisionWarning[] {
|
|
16
|
+
const warnings: CollisionWarning[] = [];
|
|
17
|
+
const seenNames = new Map<string, { figmaId: string, originalName: string }>();
|
|
18
|
+
const seenSanitized = new Map<string, { figmaId: string, originalName: string }>();
|
|
19
|
+
|
|
20
|
+
nodes.forEach(node => {
|
|
21
|
+
const name = node.name;
|
|
22
|
+
const sanitized = name.replace(/[^a-zA-Z0-9]/g, '');
|
|
23
|
+
const id = node.id;
|
|
24
|
+
|
|
25
|
+
// 4. Reserved words
|
|
26
|
+
if (RESERVED_WORDS.has(name.toLowerCase())) {
|
|
27
|
+
warnings.push({
|
|
28
|
+
type: 'reserved',
|
|
29
|
+
name1: name,
|
|
30
|
+
name2: '',
|
|
31
|
+
figmaId1: id,
|
|
32
|
+
figmaId2: '',
|
|
33
|
+
suggestion: `Fig${name.charAt(0).toUpperCase()}${name.slice(1)}`,
|
|
34
|
+
severity: 'error'
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// 1. Exact match
|
|
39
|
+
if (seenNames.has(name)) {
|
|
40
|
+
const other = seenNames.get(name)!;
|
|
41
|
+
if (other.figmaId !== id) {
|
|
42
|
+
warnings.push({
|
|
43
|
+
type: 'exact',
|
|
44
|
+
name1: name,
|
|
45
|
+
name2: name,
|
|
46
|
+
figmaId1: id,
|
|
47
|
+
figmaId2: other.figmaId,
|
|
48
|
+
suggestion: `${name}_${id.replace(':', '_')}`,
|
|
49
|
+
severity: 'error'
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
} else {
|
|
53
|
+
seenNames.set(name, { figmaId: id, originalName: name });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 2. Case-insensitive & 3. Sanitization
|
|
57
|
+
const lowerSanitized = sanitized.toLowerCase();
|
|
58
|
+
if (seenSanitized.has(lowerSanitized)) {
|
|
59
|
+
const other = seenSanitized.get(lowerSanitized)!;
|
|
60
|
+
if (other.figmaId !== id) {
|
|
61
|
+
warnings.push({
|
|
62
|
+
type: lowerSanitized === other.originalName.replace(/[^a-zA-Z0-9]/g, '').toLowerCase() ? 'caseInsensitive' : 'sanitized',
|
|
63
|
+
name1: name,
|
|
64
|
+
name2: other.originalName,
|
|
65
|
+
figmaId1: id,
|
|
66
|
+
figmaId2: other.figmaId,
|
|
67
|
+
suggestion: `${sanitized}_${id.replace(':', '_')}`,
|
|
68
|
+
severity: 'warning'
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
} else {
|
|
72
|
+
seenSanitized.set(lowerSanitized, { figmaId: id, originalName: name });
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
return warnings;
|
|
77
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface CopyOptions {
|
|
2
|
+
scope: 'component' | 'fullFile' | 'allComponents';
|
|
3
|
+
includeImports: boolean;
|
|
4
|
+
includeDependencies: boolean;
|
|
5
|
+
includeTypes: boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function smartCopy(nodes: any[], options: CopyOptions): string {
|
|
9
|
+
let output = "";
|
|
10
|
+
|
|
11
|
+
if (options.includeDependencies) {
|
|
12
|
+
output += "// Dependencies:\n";
|
|
13
|
+
output += "// npm install framer-motion@11.0.0 react@18.2.0 react-dom@18.2.0\n";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const generatedAt = new Date().toISOString();
|
|
17
|
+
|
|
18
|
+
nodes.forEach(node => {
|
|
19
|
+
output += `// Generated from Figma: ${node.name} (node: ${node.id})\n`;
|
|
20
|
+
output += `// Generated at: ${generatedAt}\n\n`;
|
|
21
|
+
|
|
22
|
+
if (options.scope === 'fullFile') {
|
|
23
|
+
output += node.reactCode;
|
|
24
|
+
} else if (options.scope === 'component') {
|
|
25
|
+
// Extract just the component body (rough approximation)
|
|
26
|
+
const match = node.reactCode.match(/export const[\s\S]*\};/);
|
|
27
|
+
output += match ? match[0] : node.reactCode;
|
|
28
|
+
}
|
|
29
|
+
output += "\n\n";
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
return output.trim();
|
|
33
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export interface ReadmeConfig {
|
|
2
|
+
projectName: string;
|
|
3
|
+
figmaFileName: string;
|
|
4
|
+
figmaPageName: string;
|
|
5
|
+
components: any[];
|
|
6
|
+
hasAnimations: boolean;
|
|
7
|
+
hasTypescript: boolean;
|
|
8
|
+
handlerCount: number;
|
|
9
|
+
imageCount: number;
|
|
10
|
+
generatedAt: string;
|
|
11
|
+
pluginVersion: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function generateReadme(config: ReadmeConfig): string {
|
|
15
|
+
const componentTable = config.components.map(c =>
|
|
16
|
+
`| ${c.name} | ${c.type} | ${c.hasAnimations ? 'Yes' : 'No'} | ${c.handlers || 0} |`
|
|
17
|
+
).join('\n');
|
|
18
|
+
|
|
19
|
+
return `# ${config.projectName}
|
|
20
|
+
Generated from Figma on ${config.generatedAt.split('T')[0]}
|
|
21
|
+
|
|
22
|
+
## Getting Started
|
|
23
|
+
|
|
24
|
+
### Prerequisites
|
|
25
|
+
- Node.js 18+
|
|
26
|
+
- npm or yarn
|
|
27
|
+
|
|
28
|
+
### Installation
|
|
29
|
+
\`\`\`bash
|
|
30
|
+
npm install
|
|
31
|
+
\`\`\`
|
|
32
|
+
|
|
33
|
+
### Dependencies
|
|
34
|
+
This project requires:
|
|
35
|
+
- react: ^18.2.0
|
|
36
|
+
- react-dom: ^18.2.0
|
|
37
|
+
${config.hasAnimations ? '- framer-motion: ^11.0.0' : ''}
|
|
38
|
+
|
|
39
|
+
## Project Structure
|
|
40
|
+
components/
|
|
41
|
+
${config.components.map(c => `├── ${c.name}.tsx`).join('\n')}
|
|
42
|
+
|
|
43
|
+
## Components Overview
|
|
44
|
+
| Component | Type | Animations | Handlers |
|
|
45
|
+
|-----------|------|------------|----------|
|
|
46
|
+
${componentTable}
|
|
47
|
+
|
|
48
|
+
## Usage
|
|
49
|
+
\`\`\`tsx
|
|
50
|
+
import { ${config.components[0]?.name || 'Component'} } from './components';
|
|
51
|
+
|
|
52
|
+
function App() {
|
|
53
|
+
return (
|
|
54
|
+
<div>
|
|
55
|
+
<${config.components[0]?.name || 'Component'} />
|
|
56
|
+
</div>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
\`\`\`
|
|
60
|
+
|
|
61
|
+
## Generated From
|
|
62
|
+
- Figma File: ${config.figmaFileName}
|
|
63
|
+
- Page: ${config.figmaPageName}
|
|
64
|
+
- Conversion Date: ${config.generatedAt}
|
|
65
|
+
- Plugin Version: ${config.pluginVersion}
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
Made with ❤️ by Figmake
|
|
69
|
+
`;
|
|
70
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export type ImageExportMode = 'placeholder' | 'base64' | 'url' | 'none';
|
|
2
|
+
|
|
3
|
+
export interface ImageExportConfig {
|
|
4
|
+
mode: ImageExportMode;
|
|
5
|
+
maxBase64Size: number;
|
|
6
|
+
placeholderStyle: 'gradient' | 'solid' | 'blurred' | 'labeled';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function handleImage(fill: any, config: ImageExportConfig): { style: any, comment: string } {
|
|
10
|
+
const style: any = {};
|
|
11
|
+
let comment = "";
|
|
12
|
+
|
|
13
|
+
if (config.mode === 'none') {
|
|
14
|
+
style.backgroundColor = '#f0f0f0';
|
|
15
|
+
comment = "TODO: Replace with actual image asset";
|
|
16
|
+
return { style, comment };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (fill.imageHash) {
|
|
20
|
+
if (config.mode === 'placeholder') {
|
|
21
|
+
style.backgroundImage = `url(data:image/png;base64,iVBOR...)`; // Placeholder
|
|
22
|
+
style.backgroundSize = 'cover';
|
|
23
|
+
style.filter = 'blur(20px)';
|
|
24
|
+
comment = `TODO: Replace with real asset. Hash: ${fill.imageHash}`;
|
|
25
|
+
} else if (config.mode === 'base64') {
|
|
26
|
+
// In plugin mode, we'd fetch the actual bytes.
|
|
27
|
+
// For now, we'll return a placeholder that the code.ts will replace if it has bytes.
|
|
28
|
+
style.backgroundImage = `url(IMAGE_HASH_${fill.imageHash})`;
|
|
29
|
+
style.backgroundSize = 'cover';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return { style, comment };
|
|
34
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export function extractDesignTokens(nodes: any[]) {
|
|
2
|
+
return {
|
|
3
|
+
palette: extractPalette(nodes),
|
|
4
|
+
typography: extractTypography(nodes),
|
|
5
|
+
spacing: extractSpacing(nodes),
|
|
6
|
+
shadows: extractShadows(nodes)
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function extractPalette(nodes: any[]) {
|
|
11
|
+
// TODO: Implementation logic
|
|
12
|
+
return {};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function extractTypography(nodes: any[]) {
|
|
16
|
+
// TODO: Implementation logic
|
|
17
|
+
return {};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function extractSpacing(nodes: any[]) {
|
|
21
|
+
// TODO: Implementation logic
|
|
22
|
+
return {};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function extractShadows(nodes: any[]) {
|
|
26
|
+
// TODO: Implementation logic
|
|
27
|
+
return {};
|
|
28
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
export interface ColorScale {
|
|
2
|
+
'50': string;
|
|
3
|
+
'100': string;
|
|
4
|
+
'200': string;
|
|
5
|
+
'300': string;
|
|
6
|
+
'400': string;
|
|
7
|
+
'500': string;
|
|
8
|
+
'600': string;
|
|
9
|
+
'700': string;
|
|
10
|
+
'800': string;
|
|
11
|
+
'900': string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ExtractedPalette {
|
|
15
|
+
primary: ColorScale;
|
|
16
|
+
secondary: ColorScale;
|
|
17
|
+
tertiary: ColorScale;
|
|
18
|
+
accent: ColorScale;
|
|
19
|
+
neutrals: ColorScale;
|
|
20
|
+
semantic: {
|
|
21
|
+
success: string;
|
|
22
|
+
warning: string;
|
|
23
|
+
error: string;
|
|
24
|
+
info: string;
|
|
25
|
+
};
|
|
26
|
+
background: {
|
|
27
|
+
primary: string;
|
|
28
|
+
secondary: string;
|
|
29
|
+
tertiary: string;
|
|
30
|
+
};
|
|
31
|
+
text: {
|
|
32
|
+
primary: string;
|
|
33
|
+
secondary: string;
|
|
34
|
+
disabled: string;
|
|
35
|
+
inverse: string;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function extractPalette(nodes: any[]): ExtractedPalette {
|
|
40
|
+
// Logic to collect colors, group by hue, and build scales
|
|
41
|
+
// This is a simplified implementation as requested
|
|
42
|
+
const colors: string[] = [];
|
|
43
|
+
|
|
44
|
+
const traverse = (node: any) => {
|
|
45
|
+
if (node.fills) {
|
|
46
|
+
node.fills.forEach((fill: any) => {
|
|
47
|
+
if (fill.type === 'SOLID') {
|
|
48
|
+
const { r, g, b } = fill.color;
|
|
49
|
+
const hex = `#${Math.round(r * 255).toString(16).padStart(2, '0')}${Math.round(g * 255).toString(16).padStart(2, '0')}${Math.round(b * 255).toString(16).padStart(2, '0')}`;
|
|
50
|
+
colors.push(hex);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
if (node.children) node.children.forEach(traverse);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
nodes.forEach(traverse);
|
|
58
|
+
|
|
59
|
+
// Frequency analysis and grouping would happen here
|
|
60
|
+
// Returning a mock/placeholder structure that follows the interface
|
|
61
|
+
return {
|
|
62
|
+
primary: createScale(colors[0] || '#6366f1'),
|
|
63
|
+
secondary: createScale(colors[1] || '#ec4899'),
|
|
64
|
+
tertiary: createScale(colors[2] || '#14b8a6'),
|
|
65
|
+
accent: createScale(colors[3] || '#f59e0b'),
|
|
66
|
+
neutrals: createScale('#6b7280'),
|
|
67
|
+
semantic: {
|
|
68
|
+
success: '#10b981',
|
|
69
|
+
warning: '#f59e0b',
|
|
70
|
+
error: '#ef4444',
|
|
71
|
+
info: '#3b82f6',
|
|
72
|
+
},
|
|
73
|
+
background: {
|
|
74
|
+
primary: '#ffffff',
|
|
75
|
+
secondary: '#f9fafb',
|
|
76
|
+
tertiary: '#f3f4f6',
|
|
77
|
+
},
|
|
78
|
+
text: {
|
|
79
|
+
primary: '#111827',
|
|
80
|
+
secondary: '#4b5563',
|
|
81
|
+
disabled: '#9ca3af',
|
|
82
|
+
inverse: '#ffffff',
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function createScale(baseColor: string): ColorScale {
|
|
88
|
+
return {
|
|
89
|
+
'50': baseColor, '100': baseColor, '200': baseColor, '300': baseColor, '400': baseColor,
|
|
90
|
+
'500': baseColor, '600': baseColor, '700': baseColor, '800': baseColor, '900': baseColor,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface ExtractedShadows {
|
|
2
|
+
elevations: {
|
|
3
|
+
none: string;
|
|
4
|
+
sm: string;
|
|
5
|
+
md: string;
|
|
6
|
+
lg: string;
|
|
7
|
+
xl: string;
|
|
8
|
+
'2xl': string;
|
|
9
|
+
};
|
|
10
|
+
blurScale: {
|
|
11
|
+
sm: string;
|
|
12
|
+
md: string;
|
|
13
|
+
lg: string;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function extractShadows(nodes: any[]): ExtractedShadows {
|
|
18
|
+
return {
|
|
19
|
+
elevations: {
|
|
20
|
+
none: 'none',
|
|
21
|
+
sm: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
|
|
22
|
+
md: '0 4px 6px -1px rgba(0, 0, 0, 0.1)',
|
|
23
|
+
lg: '0 10px 15px -3px rgba(0, 0, 0, 0.1)',
|
|
24
|
+
xl: '0 20px 25px -5px rgba(0, 0, 0, 0.1)',
|
|
25
|
+
'2xl': '0 25px 50px -12px rgba(0, 0, 0, 0.25)',
|
|
26
|
+
},
|
|
27
|
+
blurScale: {
|
|
28
|
+
sm: '4px',
|
|
29
|
+
md: '8px',
|
|
30
|
+
lg: '16px',
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export interface ExtractedSpacing {
|
|
2
|
+
unit: number;
|
|
3
|
+
scale: Record<string, string>;
|
|
4
|
+
containerPadding: {
|
|
5
|
+
mobile: string;
|
|
6
|
+
tablet: string;
|
|
7
|
+
desktop: string;
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function extractSpacing(nodes: any[]): ExtractedSpacing {
|
|
12
|
+
// Logic to find common padding/gap values and determine base unit
|
|
13
|
+
return {
|
|
14
|
+
unit: 4,
|
|
15
|
+
scale: {
|
|
16
|
+
'0': '0px',
|
|
17
|
+
'1': '4px',
|
|
18
|
+
'2': '8px',
|
|
19
|
+
'3': '12px',
|
|
20
|
+
'4': '16px',
|
|
21
|
+
'5': '20px',
|
|
22
|
+
'6': '24px',
|
|
23
|
+
'8': '32px',
|
|
24
|
+
'10': '40px',
|
|
25
|
+
'12': '48px',
|
|
26
|
+
'16': '64px',
|
|
27
|
+
},
|
|
28
|
+
containerPadding: {
|
|
29
|
+
mobile: '16px',
|
|
30
|
+
tablet: '24px',
|
|
31
|
+
desktop: '32px',
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
}
|