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,71 @@
|
|
|
1
|
+
export interface TextStyle {
|
|
2
|
+
fontFamily: string;
|
|
3
|
+
fontSize: string;
|
|
4
|
+
fontWeight: number;
|
|
5
|
+
lineHeight: number;
|
|
6
|
+
letterSpacing: string;
|
|
7
|
+
textTransform?: 'none' | 'uppercase' | 'lowercase' | 'capitalize';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ExtractedTypography {
|
|
11
|
+
fontFamilies: {
|
|
12
|
+
primary: string;
|
|
13
|
+
secondary: string;
|
|
14
|
+
mono: string;
|
|
15
|
+
};
|
|
16
|
+
scale: {
|
|
17
|
+
xs: TextStyle;
|
|
18
|
+
sm: TextStyle;
|
|
19
|
+
base: TextStyle;
|
|
20
|
+
lg: TextStyle;
|
|
21
|
+
xl: TextStyle;
|
|
22
|
+
'2xl': TextStyle;
|
|
23
|
+
'3xl': TextStyle;
|
|
24
|
+
'4xl': TextStyle;
|
|
25
|
+
};
|
|
26
|
+
weights: {
|
|
27
|
+
light: number;
|
|
28
|
+
regular: number;
|
|
29
|
+
medium: number;
|
|
30
|
+
semibold: number;
|
|
31
|
+
bold: number;
|
|
32
|
+
};
|
|
33
|
+
lineHeights: {
|
|
34
|
+
tight: number;
|
|
35
|
+
normal: number;
|
|
36
|
+
relaxed: number;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function extractTypography(nodes: any[]): ExtractedTypography {
|
|
41
|
+
// Logic to analyze text nodes and build typography scale
|
|
42
|
+
return {
|
|
43
|
+
fontFamilies: {
|
|
44
|
+
primary: 'Inter',
|
|
45
|
+
secondary: 'system-ui',
|
|
46
|
+
mono: 'JetBrains Mono',
|
|
47
|
+
},
|
|
48
|
+
scale: {
|
|
49
|
+
xs: { fontFamily: 'Inter', fontSize: '12px', fontWeight: 400, lineHeight: 1.5, letterSpacing: '0px' },
|
|
50
|
+
sm: { fontFamily: 'Inter', fontSize: '14px', fontWeight: 400, lineHeight: 1.5, letterSpacing: '0px' },
|
|
51
|
+
base: { fontFamily: 'Inter', fontSize: '16px', fontWeight: 400, lineHeight: 1.5, letterSpacing: '0px' },
|
|
52
|
+
lg: { fontFamily: 'Inter', fontSize: '18px', fontWeight: 600, lineHeight: 1.4, letterSpacing: '0px' },
|
|
53
|
+
xl: { fontFamily: 'Inter', fontSize: '20px', fontWeight: 600, lineHeight: 1.4, letterSpacing: '0px' },
|
|
54
|
+
'2xl': { fontFamily: 'Inter', fontSize: '24px', fontWeight: 700, lineHeight: 1.2, letterSpacing: '0px' },
|
|
55
|
+
'3xl': { fontFamily: 'Inter', fontSize: '30px', fontWeight: 700, lineHeight: 1.2, letterSpacing: '0px' },
|
|
56
|
+
'4xl': { fontFamily: 'Inter', fontSize: '36px', fontWeight: 800, lineHeight: 1.1, letterSpacing: '0px' },
|
|
57
|
+
},
|
|
58
|
+
weights: {
|
|
59
|
+
light: 300,
|
|
60
|
+
regular: 400,
|
|
61
|
+
medium: 500,
|
|
62
|
+
semibold: 600,
|
|
63
|
+
bold: 700,
|
|
64
|
+
},
|
|
65
|
+
lineHeights: {
|
|
66
|
+
tight: 1.25,
|
|
67
|
+
normal: 1.5,
|
|
68
|
+
relaxed: 1.75,
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { figmaToCSS } from "../core/converters/layoutConverter";
|
|
2
|
+
import { generateReactComponent } from "../core/generators/reactGenerator";
|
|
3
|
+
import { extractAnimations } from "../core/extractors/animationExtractor";
|
|
4
|
+
import { DEFAULT_CONFIG, PluginConfig } from "../core/config";
|
|
5
|
+
import { detectCollisions } from "../core/utils/collisionDetector";
|
|
6
|
+
import { calculateMetrics } from "../core/utils/codeMetrics";
|
|
7
|
+
import { extractDesignTokens } from "../design-system/extractDesignTokens";
|
|
8
|
+
import { generateLockfile } from "../vibecode-guard/generateLockfile";
|
|
9
|
+
import { generatePromptContext } from "../vibecode-guard/generatePromptContext";
|
|
10
|
+
|
|
11
|
+
let currentConfig: PluginConfig = DEFAULT_CONFIG;
|
|
12
|
+
|
|
13
|
+
figma.showUI(__html__, { width: 600, height: 800 });
|
|
14
|
+
|
|
15
|
+
figma.clientStorage.getAsync("plugin_settings").then((settings) => {
|
|
16
|
+
if (settings) {
|
|
17
|
+
currentConfig = { ...DEFAULT_CONFIG, ...settings };
|
|
18
|
+
}
|
|
19
|
+
figma.ui.postMessage({ type: "settings-loaded", config: currentConfig });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
figma.ui.onmessage = async (msg) => {
|
|
23
|
+
if (msg.type === "save-settings") {
|
|
24
|
+
currentConfig = msg.config;
|
|
25
|
+
figma.clientStorage.setAsync("plugin_settings", currentConfig);
|
|
26
|
+
updateSelection();
|
|
27
|
+
} else if (msg.type === "validate-node") {
|
|
28
|
+
const selection = figma.currentPage.selection;
|
|
29
|
+
if (selection.length > 0) {
|
|
30
|
+
const node = selection[0];
|
|
31
|
+
const bytes = await node.exportAsync({ format: "PNG", constraint: { type: "SCALE", value: 1 } });
|
|
32
|
+
figma.ui.postMessage({ type: "validation-image", bytes, width: node.width, height: node.height });
|
|
33
|
+
}
|
|
34
|
+
} else if (msg.type === "generate-guard") {
|
|
35
|
+
const tokens = extractDesignTokens([figma.root]);
|
|
36
|
+
const lockfile = generateLockfile(figma.root.name, figma.fileKey || 'local', tokens);
|
|
37
|
+
const promptContext = generatePromptContext(tokens);
|
|
38
|
+
figma.ui.postMessage({ type: "guard-data", lockfile, promptContext, tokens });
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
function extractProperties(node: BaseNode): any {
|
|
43
|
+
const props: any = {
|
|
44
|
+
id: node.id,
|
|
45
|
+
name: node.name,
|
|
46
|
+
type: node.type,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
if ("reactions" in node) {
|
|
50
|
+
props.reactions = clone((node as any).reactions);
|
|
51
|
+
props.animations = extractAnimations(node, figma.getNodeById);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if ("x" in node) props.x = node.x;
|
|
55
|
+
if ("y" in node) props.y = node.y;
|
|
56
|
+
if ("width" in node) props.width = node.width;
|
|
57
|
+
if ("height" in node) props.height = node.height;
|
|
58
|
+
if ("rotation" in node) props.rotation = node.rotation;
|
|
59
|
+
|
|
60
|
+
if ("visible" in node) props.visible = node.visible;
|
|
61
|
+
if ("opacity" in node) props.opacity = node.opacity;
|
|
62
|
+
if ("blendMode" in node) props.blendMode = node.blendMode;
|
|
63
|
+
|
|
64
|
+
if ("constraints" in node) props.constraints = node.constraints;
|
|
65
|
+
|
|
66
|
+
if ("cornerRadius" in node) props.cornerRadius = node.cornerRadius === figma.mixed ? "mixed" : node.cornerRadius;
|
|
67
|
+
if ("topLeftRadius" in node) props.topLeftRadius = node.topLeftRadius;
|
|
68
|
+
if ("topRightRadius" in node) props.topRightRadius = node.topRightRadius;
|
|
69
|
+
if ("bottomLeftRadius" in node) props.bottomLeftRadius = node.bottomLeftRadius;
|
|
70
|
+
if ("bottomRightRadius" in node) props.bottomRightRadius = node.bottomRightRadius;
|
|
71
|
+
|
|
72
|
+
if ("layoutMode" in node) props.layoutMode = node.layoutMode;
|
|
73
|
+
if ("primaryAxisSizingMode" in node) props.primaryAxisSizingMode = node.primaryAxisSizingMode;
|
|
74
|
+
if ("counterAxisSizingMode" in node) props.counterAxisSizingMode = node.counterAxisSizingMode;
|
|
75
|
+
if ("primaryAxisAlignItems" in node) props.primaryAxisAlignItems = node.primaryAxisAlignItems;
|
|
76
|
+
if ("counterAxisAlignItems" in node) props.counterAxisAlignItems = node.counterAxisAlignItems;
|
|
77
|
+
if ("paddingLeft" in node) props.paddingLeft = node.paddingLeft;
|
|
78
|
+
if ("paddingRight" in node) props.paddingRight = node.paddingRight;
|
|
79
|
+
if ("paddingTop" in node) props.paddingTop = node.paddingTop;
|
|
80
|
+
if ("paddingBottom" in node) props.paddingBottom = node.paddingBottom;
|
|
81
|
+
if ("itemSpacing" in node) props.itemSpacing = node.itemSpacing;
|
|
82
|
+
|
|
83
|
+
if ("fills" in node) props.fills = clone(node.fills);
|
|
84
|
+
if ("strokes" in node) props.strokes = clone(node.strokes);
|
|
85
|
+
if ("strokeWeight" in node) props.strokeWeight = node.strokeWeight;
|
|
86
|
+
if ("strokeAlign" in node) props.strokeAlign = node.strokeAlign;
|
|
87
|
+
if ("strokeCap" in node) props.strokeCap = node.strokeCap;
|
|
88
|
+
if ("strokeJoin" in node) props.strokeJoin = node.strokeJoin;
|
|
89
|
+
if ("dashPattern" in node) props.dashPattern = node.dashPattern;
|
|
90
|
+
if ("effects" in node) props.effects = clone(node.effects);
|
|
91
|
+
|
|
92
|
+
if (node.type === "TEXT") {
|
|
93
|
+
const textNode = node as TextNode;
|
|
94
|
+
props.characters = textNode.characters;
|
|
95
|
+
props.fontName = clone(textNode.fontName);
|
|
96
|
+
props.fontSize = textNode.fontSize === figma.mixed ? "mixed" : textNode.fontSize;
|
|
97
|
+
props.letterSpacing = clone(textNode.letterSpacing);
|
|
98
|
+
props.lineHeight = clone(textNode.lineHeight);
|
|
99
|
+
props.textAlignHorizontal = textNode.textAlignHorizontal;
|
|
100
|
+
props.textAlignVertical = textNode.textAlignVertical;
|
|
101
|
+
props.textDecoration = textNode.textDecoration;
|
|
102
|
+
props.textCase = textNode.textCase;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if ("children" in node) {
|
|
106
|
+
props.children = node.children.map(child => extractProperties(child));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
props.css = figmaToCSS(node);
|
|
110
|
+
const reactResult = generateReactComponent(node, { getNodeById: figma.getNodeById, config: currentConfig });
|
|
111
|
+
props.reactCode = reactResult.code;
|
|
112
|
+
props.fileCount = Object.keys(reactResult.files).length;
|
|
113
|
+
props.generatedFiles = reactResult.files;
|
|
114
|
+
|
|
115
|
+
return props;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function clone(val: any): any {
|
|
119
|
+
if (val === figma.mixed) return "mixed";
|
|
120
|
+
const type = typeof val;
|
|
121
|
+
if (val === null) return null;
|
|
122
|
+
if (type === "undefined" || type === "number" || type === "string" || type === "boolean") return val;
|
|
123
|
+
if (type === "object") {
|
|
124
|
+
if (val instanceof Array) return val.map(x => clone(x));
|
|
125
|
+
if (val instanceof Uint8Array) return Array.from(val);
|
|
126
|
+
const ret: any = {};
|
|
127
|
+
for (const key in val) ret[key] = clone(val[key]);
|
|
128
|
+
return ret;
|
|
129
|
+
}
|
|
130
|
+
return val;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function updateSelection() {
|
|
134
|
+
const selection = figma.currentPage.selection;
|
|
135
|
+
const collisions = detectCollisions(selection);
|
|
136
|
+
const data = selection.map(node => extractProperties(node));
|
|
137
|
+
const metrics = calculateMetrics(data);
|
|
138
|
+
metrics.collisions = collisions.length;
|
|
139
|
+
figma.ui.postMessage({ type: "update-properties", data, metrics, collisions });
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
figma.on("selectionchange", updateSelection);
|
|
143
|
+
updateSelection();
|
|
@@ -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>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { ExtractedDesignTokens } from '../design-system/extractDesignTokens';
|
|
2
|
+
|
|
3
|
+
export function generateClaudeRules(tokens: ExtractedDesignTokens): string {
|
|
4
|
+
return `DESIGN SYSTEM CONSTRAINTS
|
|
5
|
+
|
|
6
|
+
COLORS:
|
|
7
|
+
primary-500: ${tokens.colors.primary[500]}
|
|
8
|
+
secondary-500: ${tokens.colors.secondary[500]}
|
|
9
|
+
bg-primary: ${tokens.colors.background.primary}
|
|
10
|
+
text-primary: ${tokens.colors.text.primary}
|
|
11
|
+
|
|
12
|
+
TYPOGRAPHY:
|
|
13
|
+
font: ${tokens.typography.fontFamilies.primary}
|
|
14
|
+
body: ${tokens.typography.scale.base.fontSize}/${tokens.typography.scale.base.lineHeight}
|
|
15
|
+
|
|
16
|
+
SPACING:
|
|
17
|
+
unit: ${tokens.spacing.unit}px
|
|
18
|
+
scale: [${Object.values(tokens.spacing.scale).join(', ')}]
|
|
19
|
+
|
|
20
|
+
VIOLATIONS TO AVOID:
|
|
21
|
+
- Do not invent new colors
|
|
22
|
+
- Do not use system fonts instead of ${tokens.typography.fontFamilies.primary}
|
|
23
|
+
- Do not use spacing values outside of the scale
|
|
24
|
+
`;
|
|
25
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ExtractedDesignTokens } from '../design-system/extractDesignTokens';
|
|
2
|
+
|
|
3
|
+
export function generateCopilotInstructions(tokens: ExtractedDesignTokens): string {
|
|
4
|
+
return `# Copilot Instructions
|
|
5
|
+
|
|
6
|
+
This project's design system is enforced. Suggestions MUST respect these constraints.
|
|
7
|
+
|
|
8
|
+
## Style Rules
|
|
9
|
+
- Primary Color: ${tokens.colors.primary[500]}
|
|
10
|
+
- Font: ${tokens.typography.fontFamilies.primary}
|
|
11
|
+
- Spacing Unit: ${tokens.spacing.unit}px
|
|
12
|
+
|
|
13
|
+
## Implementation
|
|
14
|
+
- Import styles from design token files
|
|
15
|
+
- Use the spacing scale: ${Object.values(tokens.spacing.scale).join(', ')}
|
|
16
|
+
- Match component patterns exactly
|
|
17
|
+
`;
|
|
18
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { ExtractedDesignTokens } from '../design-system/extractDesignTokens';
|
|
2
|
+
|
|
3
|
+
export function generateCursorRules(tokens: ExtractedDesignTokens): string {
|
|
4
|
+
return `---
|
|
5
|
+
description: Design system constraints — DO NOT HALLUCINATE STYLES
|
|
6
|
+
globs: **/*.tsx,**/*.jsx,**/*.css
|
|
7
|
+
alwaysApply: true
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Design System Rules
|
|
11
|
+
|
|
12
|
+
## Colors — USE EXACTLY THESE, DO NOT INVENT
|
|
13
|
+
- Primary: ${tokens.colors.primary[500]} (500), ${tokens.colors.primary[600]} (600)
|
|
14
|
+
- Secondary: ${tokens.colors.secondary[500]} (500)
|
|
15
|
+
- Background: ${tokens.colors.background.primary} (primary), ${tokens.colors.background.secondary} (secondary)
|
|
16
|
+
- Text: ${tokens.colors.text.primary} (primary), ${tokens.colors.text.secondary} (secondary)
|
|
17
|
+
|
|
18
|
+
## Typography — EXACT VALUES
|
|
19
|
+
- Font: ${tokens.typography.fontFamilies.primary}
|
|
20
|
+
- Headings: ${tokens.typography.scale['2xl'].fontSize}/${tokens.typography.scale['2xl'].lineHeight} (h1), ${tokens.typography.scale.xl.fontSize} (h2)
|
|
21
|
+
- Body: ${tokens.typography.scale.base.fontSize}/${tokens.typography.scale.base.lineHeight}
|
|
22
|
+
|
|
23
|
+
## Spacing Scale — USE THESE, NOT RANDOM VALUES
|
|
24
|
+
- Base unit: ${tokens.spacing.unit}px
|
|
25
|
+
- Scale: ${Object.values(tokens.spacing.scale).join(', ')}
|
|
26
|
+
|
|
27
|
+
## When Building Components:
|
|
28
|
+
1. ALWAYS import colors from the design token file
|
|
29
|
+
2. NEVER use arbitrary color values
|
|
30
|
+
3. NEVER change font families
|
|
31
|
+
4. USE the spacing scale for all padding/margin/gap
|
|
32
|
+
5. MATCH border-radius to the design system (${tokens.borderRadius.scale.join(', ')})
|
|
33
|
+
6. WHEN IN DOUBT, reference the .figmake.lock file
|
|
34
|
+
`;
|
|
35
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ExtractedDesignTokens } from '../design-system/extractDesignTokens';
|
|
2
|
+
|
|
3
|
+
export function generateLockfile(projectName: string, fileId: string, tokens: ExtractedDesignTokens): string {
|
|
4
|
+
const lockfile = {
|
|
5
|
+
version: "2.0.0",
|
|
6
|
+
figmaFile: projectName,
|
|
7
|
+
figmaFileId: fileId,
|
|
8
|
+
extractedAt: new Date().toISOString(),
|
|
9
|
+
designTokens: tokens,
|
|
10
|
+
componentLibrary: {}, // Would be populated by analyzing component sets
|
|
11
|
+
constraints: {
|
|
12
|
+
maxWidth: "1200px",
|
|
13
|
+
gridColumns: 12,
|
|
14
|
+
gutterWidth: "24px"
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
return JSON.stringify(lockfile, null, 2);
|
|
19
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { ExtractedDesignTokens } from '../design-system/extractDesignTokens';
|
|
2
|
+
|
|
3
|
+
export function generatePromptContext(tokens: ExtractedDesignTokens): string {
|
|
4
|
+
return `
|
|
5
|
+
DESIGN SYSTEM (DO NOT DEVIATE):
|
|
6
|
+
- Primary: ${tokens.colors.primary[500]} (500), ${tokens.colors.primary[600]} (600)
|
|
7
|
+
- Secondary: ${tokens.colors.secondary[500]} (500)
|
|
8
|
+
- Font: ${tokens.typography.fontFamilies.primary}
|
|
9
|
+
- Spacing: ${tokens.spacing.unit}px base unit
|
|
10
|
+
- Radius: ${tokens.borderRadius.scale.join(', ')}
|
|
11
|
+
|
|
12
|
+
When building components for this project, use EXACTLY these values.
|
|
13
|
+
Do not approximate, round, or substitute. Reference the .figmake.lock file.
|
|
14
|
+
`;
|
|
15
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"lib": ["ESNext", "DOM"],
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"moduleResolution": "node",
|
|
7
|
+
"resolveJsonModule": true,
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"outDir": "dist",
|
|
13
|
+
"baseUrl": ".",
|
|
14
|
+
"paths": {
|
|
15
|
+
"@/*": ["src/*"]
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"include": ["src/**/*"]
|
|
19
|
+
}
|