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,207 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<style>
|
|
5
|
+
body {
|
|
6
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
7
|
+
margin: 0; padding: 16px; background-color: #f5f5f5; color: #333; height: 100vh; display: flex; flex-direction: column;
|
|
8
|
+
}
|
|
9
|
+
h2 { font-size: 14px; margin: 0 0 12px 0; color: #555; }
|
|
10
|
+
#output-container {
|
|
11
|
+
background-color: #ffffff; border: 1px solid #e0e0e0; border-radius: 4px; padding: 12px; overflow: auto; flex-grow: 1; margin-bottom: 8px;
|
|
12
|
+
}
|
|
13
|
+
pre { font-size: 12px; margin: 0; white-space: pre-wrap; word-wrap: break-word; }
|
|
14
|
+
.controls { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
|
|
15
|
+
.button-group { display: flex; gap: 8px; }
|
|
16
|
+
button {
|
|
17
|
+
background-color: #18a0fb; color: white; border: none; border-radius: 6px;
|
|
18
|
+
padding: 8px 12px; font-size: 12px; font-weight: 500; cursor: pointer;
|
|
19
|
+
}
|
|
20
|
+
.tabs { display: flex; margin-bottom: 12px; border-bottom: 1px solid #e0e0e0; }
|
|
21
|
+
.tab { padding: 8px 16px; cursor: pointer; font-size: 12px; font-weight: 500; color: #666; border-bottom: 2px solid transparent; }
|
|
22
|
+
.tab.active { color: #18a0fb; border-bottom-color: #18a0fb; }
|
|
23
|
+
.hidden { display: none; }
|
|
24
|
+
.status-bar {
|
|
25
|
+
display: grid; grid-template-columns: repeat(3, 1fr); gap: 4px; padding: 8px; background: #fff; border: 1px solid #eee; font-size: 10px; color: #666;
|
|
26
|
+
}
|
|
27
|
+
.status-item { display: flex; align-items: center; gap: 4px; }
|
|
28
|
+
.toast {
|
|
29
|
+
position: fixed; top: 20px; left: 50%; transform: translateX(-50%); background: #333; color: white; padding: 8px 16px; border-radius: 20px; font-size: 12px; transition: opacity 0.3s; opacity: 0; pointer-events: none;
|
|
30
|
+
}
|
|
31
|
+
.collision-banner { background: #fffbeb; border: 1px solid #fde68a; padding: 8px; font-size: 11px; margin-bottom: 8px; color: #92400e; border-radius: 4px; }
|
|
32
|
+
.guard-section { margin-bottom: 16px; }
|
|
33
|
+
.guard-label { font-size: 11px; font-weight: bold; margin-bottom: 4px; color: #666; }
|
|
34
|
+
</style>
|
|
35
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
|
|
36
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
|
|
37
|
+
<script src="https://unpkg.com/pixelmatch@5.3.0/index.js"></script>
|
|
38
|
+
</head>
|
|
39
|
+
<body>
|
|
40
|
+
<div class="controls">
|
|
41
|
+
<h2>Figmake v3.0.0</h2>
|
|
42
|
+
<div class="button-group">
|
|
43
|
+
<button id="copy-btn">Copy</button>
|
|
44
|
+
<button id="zip-btn">Export .ZIP</button>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<div id="collision-banner" class="collision-banner hidden"></div>
|
|
49
|
+
|
|
50
|
+
<div class="tabs">
|
|
51
|
+
<div id="react-tab" class="tab active">React</div>
|
|
52
|
+
<div id="css-tab" class="tab">CSS</div>
|
|
53
|
+
<div id="guard-tab" class="tab">🛡️ Guard</div>
|
|
54
|
+
<div id="config-tab" class="tab">Settings</div>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<div id="output-container">
|
|
58
|
+
<pre id="react-output"></pre>
|
|
59
|
+
<pre id="css-output" class="hidden"></pre>
|
|
60
|
+
|
|
61
|
+
<div id="guard-panel" class="hidden">
|
|
62
|
+
<div class="guard-section">
|
|
63
|
+
<div class="guard-label">AI Agent Guardrails</div>
|
|
64
|
+
<div class="button-group">
|
|
65
|
+
<button id="gen-lockfile-btn">Generate Lockfile</button>
|
|
66
|
+
<button id="copy-prompt-btn">Copy Prompt Context</button>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
<div class="guard-section">
|
|
70
|
+
<label class="guard-label">Agent Specific Rules</label>
|
|
71
|
+
<select id="agent-selector" style="width: 100%; margin-bottom: 8px;">
|
|
72
|
+
<option value="cursor">Cursor (.mdc)</option>
|
|
73
|
+
<option value="claude">Claude (.md)</option>
|
|
74
|
+
<option value="copilot">Copilot (.md)</option>
|
|
75
|
+
</select>
|
|
76
|
+
<button id="gen-rules-btn">Generate Rules</button>
|
|
77
|
+
</div>
|
|
78
|
+
<pre id="guard-preview" style="background: #f8f9fa; padding: 8px; border-radius: 4px; border: 1px solid #ddd;"></pre>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<div id="config-panel" class="hidden">
|
|
82
|
+
<div>
|
|
83
|
+
<label>Image Export</label>
|
|
84
|
+
<select id="imageMode">
|
|
85
|
+
<option value="placeholder">Placeholder (Blurred)</option>
|
|
86
|
+
<option value="base64">Base64 (Small assets)</option>
|
|
87
|
+
<option value="url">Keep URL source</option>
|
|
88
|
+
<option value="none">None (Empty div)</option>
|
|
89
|
+
</select>
|
|
90
|
+
</div>
|
|
91
|
+
<button id="save-settings" style="margin-top: 10px;">Save Settings</button>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<div class="status-bar">
|
|
96
|
+
<div class="status-item">📄 <span id="stat-files">0</span> files</div>
|
|
97
|
+
<div class="status-item">📝 <span id="stat-lines">0</span> lines</div>
|
|
98
|
+
<div class="status-item">🎬 <span id="stat-anims">0</span> anims</div>
|
|
99
|
+
<div id="guard-status" style="grid-column: span 3; border-top: 1px solid #eee; padding-top: 4px; color: #18a0fb;">🛡️ Vibecode Guard Ready</div>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<div id="toast" class="toast">✓ Copied to clipboard</div>
|
|
103
|
+
|
|
104
|
+
<script>
|
|
105
|
+
let currentData = null;
|
|
106
|
+
let currentMetrics = null;
|
|
107
|
+
let currentCollisions = [];
|
|
108
|
+
let guardData = null;
|
|
109
|
+
|
|
110
|
+
window.onmessage = (event) => {
|
|
111
|
+
const msg = event.data.pluginMessage;
|
|
112
|
+
if (msg.type === 'settings-loaded') {
|
|
113
|
+
document.getElementById('imageMode').value = msg.config.imageMode;
|
|
114
|
+
} else if (msg.type === 'update-properties') {
|
|
115
|
+
currentData = msg.data;
|
|
116
|
+
currentMetrics = msg.metrics;
|
|
117
|
+
currentCollisions = msg.collisions;
|
|
118
|
+
updateUI();
|
|
119
|
+
} else if (msg.type === 'guard-data') {
|
|
120
|
+
guardData = msg;
|
|
121
|
+
document.getElementById('guard-preview').textContent = msg.lockfile;
|
|
122
|
+
showToast("✓ Guard data generated");
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
function updateUI() {
|
|
127
|
+
if (!currentData) return;
|
|
128
|
+
document.getElementById('react-output').textContent = currentData[0].reactCode;
|
|
129
|
+
document.getElementById('css-output').textContent = currentData[0].css;
|
|
130
|
+
|
|
131
|
+
// Metrics
|
|
132
|
+
document.getElementById('stat-files').textContent = currentMetrics.fileCount;
|
|
133
|
+
document.getElementById('stat-lines').textContent = currentMetrics.totalLines;
|
|
134
|
+
document.getElementById('stat-anims').textContent = currentMetrics.animations;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
document.getElementById('gen-lockfile-btn').onclick = () => {
|
|
138
|
+
parent.postMessage({ pluginMessage: { type: 'generate-guard' } }, '*');
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
document.getElementById('copy-prompt-btn').onclick = () => {
|
|
142
|
+
if (!guardData) return showToast("⚠️ Generate Guard first");
|
|
143
|
+
const text = guardData.promptContext;
|
|
144
|
+
copyText(text);
|
|
145
|
+
showToast("✓ Prompt context copied");
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
function copyText(text) {
|
|
149
|
+
const textArea = document.createElement("textarea");
|
|
150
|
+
textArea.value = text;
|
|
151
|
+
document.body.appendChild(textArea);
|
|
152
|
+
textArea.select();
|
|
153
|
+
document.execCommand("copy");
|
|
154
|
+
document.body.removeChild(textArea);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function showToast(text) {
|
|
158
|
+
const toast = document.getElementById('toast');
|
|
159
|
+
toast.textContent = text;
|
|
160
|
+
toast.style.opacity = '1';
|
|
161
|
+
setTimeout(() => toast.style.opacity = '0', 2000);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const tabs = ['react', 'css', 'guard', 'config'];
|
|
165
|
+
tabs.forEach(tab => {
|
|
166
|
+
document.getElementById(tab + '-tab').onclick = () => {
|
|
167
|
+
tabs.forEach(t => {
|
|
168
|
+
document.getElementById(t + '-tab').classList.remove('active');
|
|
169
|
+
const panel = document.getElementById(t + '-output') || document.getElementById(t + '-panel');
|
|
170
|
+
if (panel) panel.classList.add('hidden');
|
|
171
|
+
});
|
|
172
|
+
document.getElementById(tab + '-tab').classList.add('active');
|
|
173
|
+
const activePanel = document.getElementById(tab + '-output') || document.getElementById(tab + '-panel');
|
|
174
|
+
if (activePanel) activePanel.classList.remove('hidden');
|
|
175
|
+
};
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
document.getElementById('copy-btn').onclick = () => {
|
|
179
|
+
const text = document.getElementById('react-output').textContent;
|
|
180
|
+
copyText(text);
|
|
181
|
+
showToast("✓ Copied component");
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
document.getElementById('save-settings').onclick = () => {
|
|
185
|
+
const config = { imageMode: document.getElementById('imageMode').value };
|
|
186
|
+
parent.postMessage({ pluginMessage: { type: 'save-settings', config } }, '*');
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
document.getElementById('zip-btn').onclick = async () => {
|
|
190
|
+
const zip = new JSZip();
|
|
191
|
+
currentData.forEach(node => {
|
|
192
|
+
zip.file(`${node.name}.tsx`, node.reactCode);
|
|
193
|
+
});
|
|
194
|
+
zip.file("README.md", "# Figmake Export\nGenerated components.");
|
|
195
|
+
if (guardData) {
|
|
196
|
+
zip.file(".figmake.lock", guardData.lockfile);
|
|
197
|
+
}
|
|
198
|
+
const blob = await zip.generateAsync({ type: "blob" });
|
|
199
|
+
const url = window.URL.createObjectURL(blob);
|
|
200
|
+
const a = document.createElement("a");
|
|
201
|
+
a.href = url;
|
|
202
|
+
a.download = "figmake-export.zip";
|
|
203
|
+
a.click();
|
|
204
|
+
};
|
|
205
|
+
</script>
|
|
206
|
+
</body>
|
|
207
|
+
</html>
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "figmake-pro",
|
|
3
|
+
"version": "3.0.0",
|
|
4
|
+
"description": "Design-to-code compiler with AI agent guardrails. Extract Figma designs as pixel-perfect React code with enforced design tokens.",
|
|
5
|
+
"keywords": ["figma", "react", "design-to-code", "design-tokens", "ai-coding", "vibecoding", "cursor", "claude"],
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": "github.com/QuaymePro/figmake",
|
|
8
|
+
"bin": {
|
|
9
|
+
"figmake": "./dist/cli/index.js"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "npm run build:plugin && npm run build:cli",
|
|
13
|
+
"build:plugin": "esbuild src/plugin/code.ts --bundle --outfile=dist/plugin/code.js",
|
|
14
|
+
"build:cli": "esbuild src/cli/index.ts --bundle --platform=node --outfile=dist/cli/index.js",
|
|
15
|
+
"test": "vitest",
|
|
16
|
+
"lint": "eslint src/",
|
|
17
|
+
"dev": "concurrently \"npm run dev:plugin\" \"npm run dev:cli\""
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"esbuild": "^0.20.0",
|
|
21
|
+
"typescript": "^5.0.0",
|
|
22
|
+
"vitest": "^1.0.0",
|
|
23
|
+
"eslint": "^8.0.0",
|
|
24
|
+
"concurrently": "^8.0.0"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"axios": "^1.6.0",
|
|
28
|
+
"commander": "^12.0.0",
|
|
29
|
+
"dotenv": "^16.4.0",
|
|
30
|
+
"fs-extra": "^11.2.0"
|
|
31
|
+
}
|
|
32
|
+
}
|
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import axios from 'axios';
|
|
3
|
+
import fs from 'fs-extra';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { Command } from 'commander';
|
|
6
|
+
import { generateReactComponent } from '../core/generators/reactGenerator';
|
|
7
|
+
import { extractDesignTokens } from '../design-system/extractDesignTokens';
|
|
8
|
+
import { generateLockfile } from '../vibecode-guard/generateLockfile';
|
|
9
|
+
import { generateCursorRules } from '../vibecode-guard/generateCursorRules';
|
|
10
|
+
import { generateClaudeRules } from '../vibecode-guard/generateClaudeRules';
|
|
11
|
+
import { generatePromptContext } from '../vibecode-guard/generatePromptContext';
|
|
12
|
+
import dotenv from 'dotenv';
|
|
13
|
+
|
|
14
|
+
dotenv.config();
|
|
15
|
+
|
|
16
|
+
const program = new Command();
|
|
17
|
+
|
|
18
|
+
program
|
|
19
|
+
.name('figmake')
|
|
20
|
+
.description('Design-to-code compiler with AI agent guardrails')
|
|
21
|
+
.version('3.0.0');
|
|
22
|
+
|
|
23
|
+
program
|
|
24
|
+
.command('export')
|
|
25
|
+
.description('Export Figma frames to React components')
|
|
26
|
+
.requiredOption('-u, --url <url>', 'Figma file URL')
|
|
27
|
+
.requiredOption('-t, --token <token>', 'Figma Personal Access Token')
|
|
28
|
+
.option('-o, --output <dir>', 'Output directory', './output')
|
|
29
|
+
.action(async (options) => {
|
|
30
|
+
const { url, token, output } = options;
|
|
31
|
+
const fileKey = extractFileKey(url);
|
|
32
|
+
if (!fileKey) return console.error('Invalid URL');
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const fileData = await fetchFigmaFile(fileKey, token);
|
|
36
|
+
const nodeMap = buildNodeMap(fileData.document);
|
|
37
|
+
const getNodeById = (id: string) => nodeMap.get(id);
|
|
38
|
+
|
|
39
|
+
const firstPage = fileData.document.children[0];
|
|
40
|
+
const frames = firstPage.children.filter((c: any) => c.type === 'FRAME' || c.type === 'COMPONENT');
|
|
41
|
+
|
|
42
|
+
await fs.ensureDir(output);
|
|
43
|
+
for (const frame of frames) {
|
|
44
|
+
console.log(`📦 Generating ${frame.name}...`);
|
|
45
|
+
const { files } = generateReactComponent(frame, { getNodeById });
|
|
46
|
+
for (const [filename, content] of Object.entries(files)) {
|
|
47
|
+
await fs.writeFile(path.join(output, filename), content);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
console.log('✅ Export complete!');
|
|
51
|
+
} catch (e: any) {
|
|
52
|
+
console.error('❌ Error:', e.message);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
program
|
|
57
|
+
.command('lockfile')
|
|
58
|
+
.description('Generate a design system lockfile')
|
|
59
|
+
.requiredOption('-u, --url <url>', 'Figma file URL')
|
|
60
|
+
.requiredOption('-t, --token <token>', 'Figma Personal Access Token')
|
|
61
|
+
.option('-o, --output <file>', 'Output filename', '.figmake.lock')
|
|
62
|
+
.action(async (options) => {
|
|
63
|
+
const { url, token, output } = options;
|
|
64
|
+
const fileKey = extractFileKey(url);
|
|
65
|
+
if (!fileKey) return;
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const fileData = await fetchFigmaFile(fileKey, token);
|
|
69
|
+
const tokens = extractDesignTokens([fileData.document]);
|
|
70
|
+
const lockfile = generateLockfile(fileData.name, fileKey, tokens);
|
|
71
|
+
await fs.writeFile(output, lockfile);
|
|
72
|
+
console.log(`✅ Lockfile generated: ${output}`);
|
|
73
|
+
} catch (e: any) {
|
|
74
|
+
console.error('❌ Error:', e.message);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
program
|
|
79
|
+
.command('guard')
|
|
80
|
+
.description('Generate AI agent constraints')
|
|
81
|
+
.requiredOption('-u, --url <url>', 'Figma file URL')
|
|
82
|
+
.requiredOption('-t, --token <token>', 'Figma Personal Access Token')
|
|
83
|
+
.option('--agent <type>', 'Agent type (cursor, claude, prompt)', 'cursor')
|
|
84
|
+
.action(async (options) => {
|
|
85
|
+
const { url, token, agent } = options;
|
|
86
|
+
const fileKey = extractFileKey(url);
|
|
87
|
+
if (!fileKey) return;
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const fileData = await fetchFigmaFile(fileKey, token);
|
|
91
|
+
const tokens = extractDesignTokens([fileData.document]);
|
|
92
|
+
|
|
93
|
+
if (agent === 'cursor') {
|
|
94
|
+
const rules = generateCursorRules(tokens);
|
|
95
|
+
await fs.ensureDir('.cursor/rules');
|
|
96
|
+
await fs.writeFile('.cursor/rules/design-system.mdc', rules);
|
|
97
|
+
console.log('🛡️ Cursor rules generated at .cursor/rules/design-system.mdc');
|
|
98
|
+
} else if (agent === 'prompt') {
|
|
99
|
+
console.log('\n--- PASTE THIS INTO YOUR AI CHAT ---\n');
|
|
100
|
+
console.log(generatePromptContext(tokens));
|
|
101
|
+
}
|
|
102
|
+
} catch (e: any) {
|
|
103
|
+
console.error('❌ Error:', e.message);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
function extractFileKey(url: string): string | null {
|
|
108
|
+
const match = url.match(/file\/([a-zA-Z0-9]+)/);
|
|
109
|
+
return match ? match[1] : null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function fetchFigmaFile(fileKey: string, token: string) {
|
|
113
|
+
const response = await axios.get(`https://api.figma.com/v1/files/${fileKey}`, {
|
|
114
|
+
headers: { 'X-Figma-Token': token }
|
|
115
|
+
});
|
|
116
|
+
return response.data;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function buildNodeMap(root: any) {
|
|
120
|
+
const map = new Map<string, any>();
|
|
121
|
+
const traverse = (node: any) => {
|
|
122
|
+
map.set(node.id, node);
|
|
123
|
+
if (node.children) node.children.forEach(traverse);
|
|
124
|
+
};
|
|
125
|
+
traverse(root);
|
|
126
|
+
return map;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
program.parse();
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface PluginConfig {
|
|
2
|
+
styling: 'inline' | 'modules' | 'styled';
|
|
3
|
+
routing: 'useState' | 'react-router' | 'nextjs';
|
|
4
|
+
typescript: 'interfaces' | 'types' | 'none';
|
|
5
|
+
animations: 'framer-motion' | 'gsap' | 'css';
|
|
6
|
+
pattern: 'functional' | 'arrow';
|
|
7
|
+
naming: 'PascalCase' | 'kebab-case' | 'camelCase';
|
|
8
|
+
exportFormat: 'single' | 'multiple' | 'barrel';
|
|
9
|
+
imageMode: 'placeholder' | 'base64' | 'url' | 'none';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const DEFAULT_CONFIG: PluginConfig = {
|
|
13
|
+
styling: 'inline',
|
|
14
|
+
routing: 'useState',
|
|
15
|
+
typescript: 'interfaces',
|
|
16
|
+
animations: 'framer-motion',
|
|
17
|
+
pattern: 'arrow',
|
|
18
|
+
naming: 'PascalCase',
|
|
19
|
+
exportFormat: 'multiple',
|
|
20
|
+
imageMode: 'placeholder',
|
|
21
|
+
};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts Figma Auto Layout and Grid properties to precise raw CSS.
|
|
3
|
+
* Handles Flexbox for standard Auto Layout and Grid for wrapped or grid-based layouts.
|
|
4
|
+
*/
|
|
5
|
+
export function figmaToCSS(node: any): string {
|
|
6
|
+
const styles: string[] = [];
|
|
7
|
+
|
|
8
|
+
// 1. Layout Mode & Display
|
|
9
|
+
if (node.layoutMode === "HORIZONTAL" || node.layoutMode === "VERTICAL") {
|
|
10
|
+
if (node.layoutWrap === "WRAP") {
|
|
11
|
+
// Use CSS Grid for Wrap layouts as requested (or Flex with wrap,
|
|
12
|
+
// but instructions specifically asked for Grid with auto-fill for wrapping)
|
|
13
|
+
styles.push("display: grid;");
|
|
14
|
+
const gap = node.itemSpacing || 0;
|
|
15
|
+
const counterGap = node.counterAxisSpacing ?? gap;
|
|
16
|
+
styles.push(`gap: ${gap}px ${counterGap}px;`);
|
|
17
|
+
|
|
18
|
+
// For auto-fill behavior, we need a base width. If children have fixed widths, we use that.
|
|
19
|
+
// Since we don't know child widths here, we'll assume a standard flex-wrap approach
|
|
20
|
+
// or grid-template-columns if specific grid props exist.
|
|
21
|
+
if (node.layoutMode === "HORIZONTAL") {
|
|
22
|
+
styles.push(`grid-template-columns: repeat(auto-fill, minmax(${node.itemSpacing || 0}px, 1fr));`);
|
|
23
|
+
}
|
|
24
|
+
} else {
|
|
25
|
+
styles.push("display: flex;");
|
|
26
|
+
styles.push(`flex-direction: ${node.layoutMode === "HORIZONTAL" ? "row" : "column"};`);
|
|
27
|
+
styles.push(`gap: ${node.itemSpacing || 0}px;`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// 2. Alignment (Figma to Flexbox/Grid mapping)
|
|
31
|
+
const primaryAlignMap: any = {
|
|
32
|
+
MIN: "flex-start",
|
|
33
|
+
CENTER: "center",
|
|
34
|
+
MAX: "flex-end",
|
|
35
|
+
SPACE_BETWEEN: "space-between",
|
|
36
|
+
};
|
|
37
|
+
const counterAlignMap: any = {
|
|
38
|
+
MIN: "flex-start",
|
|
39
|
+
CENTER: "center",
|
|
40
|
+
MAX: "flex-end",
|
|
41
|
+
BASELINE: "baseline",
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
styles.push(`justify-content: ${primaryAlignMap[node.primaryAxisAlignItems] || "flex-start"};`);
|
|
45
|
+
styles.push(`align-items: ${counterAlignMap[node.counterAxisAlignItems] || "flex-start"};`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 3. Explicit Layout Grids
|
|
49
|
+
if (node.layoutGrids && node.layoutGrids.length > 0) {
|
|
50
|
+
styles.push("display: grid;");
|
|
51
|
+
node.layoutGrids.forEach((grid: any) => {
|
|
52
|
+
if (grid.pattern === "COLUMNS") {
|
|
53
|
+
const columns = grid.count || "auto-fill";
|
|
54
|
+
const gutter = grid.gutterSize || 0;
|
|
55
|
+
const margin = grid.margin || 0;
|
|
56
|
+
styles.push(`grid-template-columns: repeat(${columns}, 1fr);`);
|
|
57
|
+
styles.push(`column-gap: ${gutter}px;`);
|
|
58
|
+
styles.push(`padding-left: ${margin}px;`);
|
|
59
|
+
styles.push(`padding-right: ${margin}px;`);
|
|
60
|
+
} else if (grid.pattern === "ROWS") {
|
|
61
|
+
const rows = grid.count || "auto-fill";
|
|
62
|
+
const gutter = grid.gutterSize || 0;
|
|
63
|
+
const margin = grid.margin || 0;
|
|
64
|
+
styles.push(`grid-template-rows: repeat(${rows}, 1fr);`);
|
|
65
|
+
styles.push(`row-gap: ${gutter}px;`);
|
|
66
|
+
styles.push(`padding-top: ${margin}px;`);
|
|
67
|
+
styles.push(`padding-bottom: ${margin}px;`);
|
|
68
|
+
} else if (grid.pattern === "GRID") {
|
|
69
|
+
styles.push(`background-image: radial-gradient(circle, #000 1px, transparent 1px);`);
|
|
70
|
+
styles.push(`background-size: ${grid.sectionSize}px ${grid.sectionSize}px;`);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 4. Sizing (Width/Height)
|
|
76
|
+
if (node.width) styles.push(`width: ${node.width}px;`);
|
|
77
|
+
if (node.height) styles.push(`height: ${node.height}px;`);
|
|
78
|
+
if (node.minWidth) styles.push(`min-width: ${node.minWidth}px;`);
|
|
79
|
+
if (node.maxWidth) styles.push(`max-width: ${node.maxWidth}px;`);
|
|
80
|
+
if (node.minHeight) styles.push(`min-height: ${node.minHeight}px;`);
|
|
81
|
+
if (node.maxHeight) styles.push(`max-height: ${node.maxHeight}px;`);
|
|
82
|
+
|
|
83
|
+
// 5. Padding
|
|
84
|
+
const pt = node.paddingTop || 0;
|
|
85
|
+
const pr = node.paddingRight || 0;
|
|
86
|
+
const pb = node.paddingBottom || 0;
|
|
87
|
+
const pl = node.paddingLeft || 0;
|
|
88
|
+
if (pt === pr && pr === pb && pb === pl && pt !== 0) {
|
|
89
|
+
styles.push(`padding: ${pt}px;`);
|
|
90
|
+
} else if (pt !== 0 || pr !== 0 || pb !== 0 || pl !== 0) {
|
|
91
|
+
styles.push(`padding: ${pt}px ${pr}px ${pb}px ${pl}px;`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 6. Corner Radius
|
|
95
|
+
if (node.cornerRadius && node.cornerRadius !== "mixed") {
|
|
96
|
+
styles.push(`border-radius: ${node.cornerRadius}px;`);
|
|
97
|
+
} else {
|
|
98
|
+
if (node.topLeftRadius) styles.push(`border-top-left-radius: ${node.topLeftRadius}px;`);
|
|
99
|
+
if (node.topRightRadius) styles.push(`border-top-right-radius: ${node.topRightRadius}px;`);
|
|
100
|
+
if (node.bottomLeftRadius) styles.push(`border-bottom-left-radius: ${node.bottomLeftRadius}px;`);
|
|
101
|
+
if (node.bottomRightRadius) styles.push(`border-bottom-right-radius: ${node.bottomRightRadius}px;`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return styles.join("\n");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Example Usage & Verification Logic
|
|
108
|
+
/*
|
|
109
|
+
const mockNode = {
|
|
110
|
+
layoutMode: "HORIZONTAL",
|
|
111
|
+
primaryAxisAlignItems: "CENTER",
|
|
112
|
+
counterAxisAlignItems: "MIN",
|
|
113
|
+
itemSpacing: 10,
|
|
114
|
+
paddingLeft: 20,
|
|
115
|
+
paddingRight: 20,
|
|
116
|
+
paddingTop: 10,
|
|
117
|
+
paddingBottom: 10,
|
|
118
|
+
width: 500,
|
|
119
|
+
cornerRadius: 8
|
|
120
|
+
};
|
|
121
|
+
console.log(figmaToCSS(mockNode));
|
|
122
|
+
*/
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
export interface MappedTransition {
|
|
2
|
+
duration: number;
|
|
3
|
+
ease: number[] | string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface ChildDelta {
|
|
7
|
+
name: string;
|
|
8
|
+
x?: number;
|
|
9
|
+
y?: number;
|
|
10
|
+
opacity?: number;
|
|
11
|
+
rotation?: number;
|
|
12
|
+
width?: number;
|
|
13
|
+
height?: number;
|
|
14
|
+
fill?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface MappedInteraction {
|
|
18
|
+
trigger: string;
|
|
19
|
+
actionType: string;
|
|
20
|
+
destinationId?: string;
|
|
21
|
+
destinationName?: string;
|
|
22
|
+
transition?: MappedTransition;
|
|
23
|
+
childDeltas?: Record<string, ChildDelta>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ExtractedAnimation {
|
|
27
|
+
nodeId: string;
|
|
28
|
+
nodeName: string;
|
|
29
|
+
interactions: MappedInteraction[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const EASING_MAP: Record<string, number[] | string> = {
|
|
33
|
+
EASE_IN: [0.42, 0, 1, 1],
|
|
34
|
+
EASE_OUT: [0, 0, 0.58, 1],
|
|
35
|
+
EASE_IN_AND_OUT: [0.42, 0, 0.58, 1],
|
|
36
|
+
LINEAR: "linear",
|
|
37
|
+
GENTLE: [0.25, 0.1, 0.25, 1],
|
|
38
|
+
QUICK: [0.15, 0, 0.15, 1],
|
|
39
|
+
BOUNCY: [0.68, -0.6, 0.32, 1.6],
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const TRIGGER_MAP: Record<string, string> = {
|
|
43
|
+
ON_CLICK: "click",
|
|
44
|
+
ON_HOVER: "hover",
|
|
45
|
+
ON_PRESS: "press",
|
|
46
|
+
ON_DRAG: "drag",
|
|
47
|
+
AFTER_TIMEOUT: "delay",
|
|
48
|
+
MOUSE_ENTER: "mouseEnter",
|
|
49
|
+
MOUSE_LEAVE: "mouseLeave",
|
|
50
|
+
WHILE_HOVERING: "whileHover",
|
|
51
|
+
WHILE_PRESSING: "whileTap",
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
function calculateChildDeltas(sourceNode: any, destNode: any): Record<string, ChildDelta> {
|
|
55
|
+
const deltas: Record<string, ChildDelta> = {};
|
|
56
|
+
if (!sourceNode.children || !destNode.children) return deltas;
|
|
57
|
+
const sourceChildren = new Map(sourceNode.children.map((c: any) => [c.name, c]));
|
|
58
|
+
const destChildren = new Map(destNode.children.map((c: any) => [c.name, c]));
|
|
59
|
+
for (const [name, destChild] of destChildren.entries()) {
|
|
60
|
+
const sourceChild: any = sourceChildren.get(name);
|
|
61
|
+
if (sourceChild) {
|
|
62
|
+
const delta: ChildDelta = { name };
|
|
63
|
+
if (sourceChild.x !== destChild.x) delta.x = destChild.x - sourceChild.x;
|
|
64
|
+
if (sourceChild.y !== destChild.y) delta.y = destChild.y - sourceChild.y;
|
|
65
|
+
if (sourceChild.opacity !== destChild.opacity) delta.opacity = destChild.opacity;
|
|
66
|
+
deltas[name] = delta;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return deltas;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function extractAnimations(node: any, getNodeById?: (id: string) => any): ExtractedAnimation | null {
|
|
73
|
+
if (!node.reactions || node.reactions.length === 0) return null;
|
|
74
|
+
const interactions: MappedInteraction[] = node.reactions.map((reaction: any) => {
|
|
75
|
+
const mapped: MappedInteraction = {
|
|
76
|
+
trigger: TRIGGER_MAP[reaction.trigger?.type] || reaction.trigger?.type,
|
|
77
|
+
actionType: reaction.action?.type,
|
|
78
|
+
};
|
|
79
|
+
if (reaction.action?.destinationId) {
|
|
80
|
+
mapped.destinationId = reaction.action.destinationId;
|
|
81
|
+
const lookup = getNodeById || (typeof figma !== 'undefined' ? figma.getNodeById : undefined);
|
|
82
|
+
if (lookup) {
|
|
83
|
+
try {
|
|
84
|
+
const destNode = lookup(mapped.destinationId);
|
|
85
|
+
if (destNode) {
|
|
86
|
+
mapped.destinationName = destNode.name;
|
|
87
|
+
if (reaction.action.navigation === "NAVIGATE") {
|
|
88
|
+
mapped.childDeltas = calculateChildDeltas(node, destNode);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
} catch (e) {}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (reaction.action?.transition) {
|
|
95
|
+
const t = reaction.action.transition;
|
|
96
|
+
mapped.transition = {
|
|
97
|
+
duration: t.duration || 0.3,
|
|
98
|
+
ease: t.easing ? (EASING_MAP[t.easing] || "easeInOut") : "easeInOut",
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
return mapped;
|
|
102
|
+
});
|
|
103
|
+
return { nodeId: node.id, nodeName: node.name, interactions };
|
|
104
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { handleImage, ImageExportConfig } from "../utils/imageExporter";
|
|
2
|
+
|
|
3
|
+
export function figmaColorToCSS(color: any, opacity: number = 1): string {
|
|
4
|
+
const r = Math.round(color.r * 255);
|
|
5
|
+
const g = Math.round(color.g * 255);
|
|
6
|
+
const b = Math.round(color.b * 255);
|
|
7
|
+
return `rgba(${r}, ${g}, ${b}, ${opacity})`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function extractNodeStyles(node: any, config?: any): any {
|
|
11
|
+
const styles: any = {};
|
|
12
|
+
const imageConfig: ImageExportConfig = {
|
|
13
|
+
mode: config?.imageMode || 'placeholder',
|
|
14
|
+
maxBase64Size: 51200,
|
|
15
|
+
placeholderStyle: 'blurred'
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
if (node.opacity !== undefined && node.opacity !== 1) styles.opacity = node.opacity;
|
|
19
|
+
|
|
20
|
+
if (node.blendMode && node.blendMode !== "PASS_THROUGH") {
|
|
21
|
+
const blendModeMap: Record<string, string> = { MULTIPLY: "multiply", SCREEN: "screen", OVERLAY: "overlay" };
|
|
22
|
+
styles.mixBlendMode = blendModeMap[node.blendMode] || "normal";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (node.fills && Array.isArray(node.fills) && node.fills.length > 0) {
|
|
26
|
+
const fill = node.fills.find((f: any) => f.visible !== false);
|
|
27
|
+
if (fill) {
|
|
28
|
+
if (fill.type === "SOLID") styles.backgroundColor = figmaColorToCSS(fill.color, fill.opacity);
|
|
29
|
+
else if (fill.type === "IMAGE") {
|
|
30
|
+
const { style: imageStyle, comment } = handleImage(fill, imageConfig);
|
|
31
|
+
Object.assign(styles, imageStyle);
|
|
32
|
+
if (comment) (styles as any).__comment = comment;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (node.cornerRadius !== undefined && node.cornerRadius !== "mixed") styles.borderRadius = `${node.cornerRadius}px`;
|
|
38
|
+
|
|
39
|
+
return styles;
|
|
40
|
+
}
|