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.
Files changed (34) hide show
  1. package/.github/workflows/ci.yml +27 -0
  2. package/CONTRIBUTING.md +19 -0
  3. package/LICENSE +21 -0
  4. package/README.md +47 -0
  5. package/dist/cli/index.js +22827 -0
  6. package/dist/plugin/code.js +791 -0
  7. package/dist/plugin/ui.html +207 -0
  8. package/package.json +32 -0
  9. package/src/cli/index.ts +129 -0
  10. package/src/core/config.ts +21 -0
  11. package/src/core/converters/layoutConverter.ts +122 -0
  12. package/src/core/extractors/animationExtractor.ts +104 -0
  13. package/src/core/extractors/styleExtractor.ts +40 -0
  14. package/src/core/generators/handlerGenerator.ts +72 -0
  15. package/src/core/generators/reactGenerator.ts +129 -0
  16. package/src/core/utils/codeMetrics.ts +54 -0
  17. package/src/core/utils/collisionDetector.ts +77 -0
  18. package/src/core/utils/copyManager.ts +33 -0
  19. package/src/core/utils/generateReadme.ts +70 -0
  20. package/src/core/utils/imageExporter.ts +34 -0
  21. package/src/design-system/extractDesignTokens.ts +28 -0
  22. package/src/design-system/extractPalette.ts +92 -0
  23. package/src/design-system/extractShadows.ts +33 -0
  24. package/src/design-system/extractSpacing.ts +34 -0
  25. package/src/design-system/extractTypography.ts +71 -0
  26. package/src/plugin/code.ts +143 -0
  27. package/src/plugin/manifest.json +9 -0
  28. package/src/plugin/ui.html +207 -0
  29. package/src/vibecode-guard/generateClaudeRules.ts +25 -0
  30. package/src/vibecode-guard/generateCopilotInstructions.ts +18 -0
  31. package/src/vibecode-guard/generateCursorRules.ts +35 -0
  32. package/src/vibecode-guard/generateLockfile.ts +19 -0
  33. package/src/vibecode-guard/generatePromptContext.ts +15 -0
  34. package/tsconfig.json +19 -0
@@ -0,0 +1,791 @@
1
+ "use strict";
2
+ (() => {
3
+ // src/core/converters/layoutConverter.ts
4
+ function figmaToCSS(node) {
5
+ const styles = [];
6
+ if (node.layoutMode === "HORIZONTAL" || node.layoutMode === "VERTICAL") {
7
+ if (node.layoutWrap === "WRAP") {
8
+ styles.push("display: grid;");
9
+ const gap = node.itemSpacing || 0;
10
+ const counterGap = node.counterAxisSpacing ?? gap;
11
+ styles.push(`gap: ${gap}px ${counterGap}px;`);
12
+ if (node.layoutMode === "HORIZONTAL") {
13
+ styles.push(`grid-template-columns: repeat(auto-fill, minmax(${node.itemSpacing || 0}px, 1fr));`);
14
+ }
15
+ } else {
16
+ styles.push("display: flex;");
17
+ styles.push(`flex-direction: ${node.layoutMode === "HORIZONTAL" ? "row" : "column"};`);
18
+ styles.push(`gap: ${node.itemSpacing || 0}px;`);
19
+ }
20
+ const primaryAlignMap = {
21
+ MIN: "flex-start",
22
+ CENTER: "center",
23
+ MAX: "flex-end",
24
+ SPACE_BETWEEN: "space-between"
25
+ };
26
+ const counterAlignMap = {
27
+ MIN: "flex-start",
28
+ CENTER: "center",
29
+ MAX: "flex-end",
30
+ BASELINE: "baseline"
31
+ };
32
+ styles.push(`justify-content: ${primaryAlignMap[node.primaryAxisAlignItems] || "flex-start"};`);
33
+ styles.push(`align-items: ${counterAlignMap[node.counterAxisAlignItems] || "flex-start"};`);
34
+ }
35
+ if (node.layoutGrids && node.layoutGrids.length > 0) {
36
+ styles.push("display: grid;");
37
+ node.layoutGrids.forEach((grid) => {
38
+ if (grid.pattern === "COLUMNS") {
39
+ const columns = grid.count || "auto-fill";
40
+ const gutter = grid.gutterSize || 0;
41
+ const margin = grid.margin || 0;
42
+ styles.push(`grid-template-columns: repeat(${columns}, 1fr);`);
43
+ styles.push(`column-gap: ${gutter}px;`);
44
+ styles.push(`padding-left: ${margin}px;`);
45
+ styles.push(`padding-right: ${margin}px;`);
46
+ } else if (grid.pattern === "ROWS") {
47
+ const rows = grid.count || "auto-fill";
48
+ const gutter = grid.gutterSize || 0;
49
+ const margin = grid.margin || 0;
50
+ styles.push(`grid-template-rows: repeat(${rows}, 1fr);`);
51
+ styles.push(`row-gap: ${gutter}px;`);
52
+ styles.push(`padding-top: ${margin}px;`);
53
+ styles.push(`padding-bottom: ${margin}px;`);
54
+ } else if (grid.pattern === "GRID") {
55
+ styles.push(`background-image: radial-gradient(circle, #000 1px, transparent 1px);`);
56
+ styles.push(`background-size: ${grid.sectionSize}px ${grid.sectionSize}px;`);
57
+ }
58
+ });
59
+ }
60
+ if (node.width)
61
+ styles.push(`width: ${node.width}px;`);
62
+ if (node.height)
63
+ styles.push(`height: ${node.height}px;`);
64
+ if (node.minWidth)
65
+ styles.push(`min-width: ${node.minWidth}px;`);
66
+ if (node.maxWidth)
67
+ styles.push(`max-width: ${node.maxWidth}px;`);
68
+ if (node.minHeight)
69
+ styles.push(`min-height: ${node.minHeight}px;`);
70
+ if (node.maxHeight)
71
+ styles.push(`max-height: ${node.maxHeight}px;`);
72
+ const pt = node.paddingTop || 0;
73
+ const pr = node.paddingRight || 0;
74
+ const pb = node.paddingBottom || 0;
75
+ const pl = node.paddingLeft || 0;
76
+ if (pt === pr && pr === pb && pb === pl && pt !== 0) {
77
+ styles.push(`padding: ${pt}px;`);
78
+ } else if (pt !== 0 || pr !== 0 || pb !== 0 || pl !== 0) {
79
+ styles.push(`padding: ${pt}px ${pr}px ${pb}px ${pl}px;`);
80
+ }
81
+ if (node.cornerRadius && node.cornerRadius !== "mixed") {
82
+ styles.push(`border-radius: ${node.cornerRadius}px;`);
83
+ } else {
84
+ if (node.topLeftRadius)
85
+ styles.push(`border-top-left-radius: ${node.topLeftRadius}px;`);
86
+ if (node.topRightRadius)
87
+ styles.push(`border-top-right-radius: ${node.topRightRadius}px;`);
88
+ if (node.bottomLeftRadius)
89
+ styles.push(`border-bottom-left-radius: ${node.bottomLeftRadius}px;`);
90
+ if (node.bottomRightRadius)
91
+ styles.push(`border-bottom-right-radius: ${node.bottomRightRadius}px;`);
92
+ }
93
+ return styles.join("\n");
94
+ }
95
+
96
+ // src/core/utils/imageExporter.ts
97
+ function handleImage(fill, config) {
98
+ const style = {};
99
+ let comment = "";
100
+ if (config.mode === "none") {
101
+ style.backgroundColor = "#f0f0f0";
102
+ comment = "TODO: Replace with actual image asset";
103
+ return { style, comment };
104
+ }
105
+ if (fill.imageHash) {
106
+ if (config.mode === "placeholder") {
107
+ style.backgroundImage = `url(data:image/png;base64,iVBOR...)`;
108
+ style.backgroundSize = "cover";
109
+ style.filter = "blur(20px)";
110
+ comment = `TODO: Replace with real asset. Hash: ${fill.imageHash}`;
111
+ } else if (config.mode === "base64") {
112
+ style.backgroundImage = `url(IMAGE_HASH_${fill.imageHash})`;
113
+ style.backgroundSize = "cover";
114
+ }
115
+ }
116
+ return { style, comment };
117
+ }
118
+
119
+ // src/core/extractors/styleExtractor.ts
120
+ function figmaColorToCSS(color, opacity = 1) {
121
+ const r = Math.round(color.r * 255);
122
+ const g = Math.round(color.g * 255);
123
+ const b = Math.round(color.b * 255);
124
+ return `rgba(${r}, ${g}, ${b}, ${opacity})`;
125
+ }
126
+ function extractNodeStyles(node, config) {
127
+ const styles = {};
128
+ const imageConfig = {
129
+ mode: config?.imageMode || "placeholder",
130
+ maxBase64Size: 51200,
131
+ placeholderStyle: "blurred"
132
+ };
133
+ if (node.opacity !== void 0 && node.opacity !== 1)
134
+ styles.opacity = node.opacity;
135
+ if (node.blendMode && node.blendMode !== "PASS_THROUGH") {
136
+ const blendModeMap = { MULTIPLY: "multiply", SCREEN: "screen", OVERLAY: "overlay" };
137
+ styles.mixBlendMode = blendModeMap[node.blendMode] || "normal";
138
+ }
139
+ if (node.fills && Array.isArray(node.fills) && node.fills.length > 0) {
140
+ const fill = node.fills.find((f) => f.visible !== false);
141
+ if (fill) {
142
+ if (fill.type === "SOLID")
143
+ styles.backgroundColor = figmaColorToCSS(fill.color, fill.opacity);
144
+ else if (fill.type === "IMAGE") {
145
+ const { style: imageStyle, comment } = handleImage(fill, imageConfig);
146
+ Object.assign(styles, imageStyle);
147
+ if (comment)
148
+ styles.__comment = comment;
149
+ }
150
+ }
151
+ }
152
+ if (node.cornerRadius !== void 0 && node.cornerRadius !== "mixed")
153
+ styles.borderRadius = `${node.cornerRadius}px`;
154
+ return styles;
155
+ }
156
+
157
+ // src/core/config.ts
158
+ var DEFAULT_CONFIG = {
159
+ styling: "inline",
160
+ routing: "useState",
161
+ typescript: "interfaces",
162
+ animations: "framer-motion",
163
+ pattern: "arrow",
164
+ naming: "PascalCase",
165
+ exportFormat: "multiple",
166
+ imageMode: "placeholder"
167
+ };
168
+
169
+ // src/core/generators/handlerGenerator.ts
170
+ function sanitizeName(name) {
171
+ return name.replace(/[^a-zA-Z0-9]/g, "_").replace(/^([0-9])/, "_$1");
172
+ }
173
+ function toPascalCase(name) {
174
+ return sanitizeName(name).split("_").map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("");
175
+ }
176
+ function generateHandlers(nodeId, nodeName, interactions, config) {
177
+ const functionDeclarations = [];
178
+ const propMappings = {};
179
+ interactions.forEach((interaction, index) => {
180
+ const trigger = interaction.trigger;
181
+ const actionType = interaction.actionType;
182
+ const suffix = interactions.length > 1 ? `_${index}` : "";
183
+ const baseName = `${toPascalCase(nodeName)}${suffix}`;
184
+ let handlerName = "";
185
+ let body = "";
186
+ if (actionType === "NODE" || actionType === "NAVIGATE") {
187
+ handlerName = `handleNavigate${baseName}`;
188
+ const dest = interaction.destinationName || interaction.destinationId || "Unknown";
189
+ if (config?.routing === "nextjs") {
190
+ body = ` router.push('/${dest.toLowerCase().replace(/\s+/g, "-")}');`;
191
+ } else if (config?.routing === "react-router") {
192
+ body = ` navigate('/${dest.toLowerCase().replace(/\s+/g, "-")}');`;
193
+ } else {
194
+ body = ` setActiveView('${toPascalCase(dest)}');`;
195
+ }
196
+ } else if (actionType === "URL") {
197
+ handlerName = `handleOpenLink${baseName}`;
198
+ body = ` window.open('${interaction.destinationId || "#"}', '_blank');`;
199
+ } else {
200
+ handlerName = `handleInteraction${baseName}`;
201
+ body = ` console.log('${interaction.trigger} triggered');`;
202
+ }
203
+ if (handlerName) {
204
+ functionDeclarations.push(` const ${handlerName} = () => {
205
+ ${body}
206
+ };`);
207
+ const reactProp = mapTriggerToReactProp(trigger);
208
+ if (reactProp)
209
+ propMappings[reactProp] = handlerName;
210
+ }
211
+ });
212
+ return { functionDeclarations, propMappings };
213
+ }
214
+ function mapTriggerToReactProp(trigger) {
215
+ const map = {
216
+ click: "onClick",
217
+ hover: "onMouseEnter",
218
+ mouseEnter: "onMouseEnter",
219
+ mouseLeave: "onMouseLeave",
220
+ press: "onMouseDown",
221
+ drag: "onDrag"
222
+ };
223
+ return map[trigger] || null;
224
+ }
225
+
226
+ // src/core/extractors/animationExtractor.ts
227
+ var EASING_MAP = {
228
+ EASE_IN: [0.42, 0, 1, 1],
229
+ EASE_OUT: [0, 0, 0.58, 1],
230
+ EASE_IN_AND_OUT: [0.42, 0, 0.58, 1],
231
+ LINEAR: "linear",
232
+ GENTLE: [0.25, 0.1, 0.25, 1],
233
+ QUICK: [0.15, 0, 0.15, 1],
234
+ BOUNCY: [0.68, -0.6, 0.32, 1.6]
235
+ };
236
+ var TRIGGER_MAP = {
237
+ ON_CLICK: "click",
238
+ ON_HOVER: "hover",
239
+ ON_PRESS: "press",
240
+ ON_DRAG: "drag",
241
+ AFTER_TIMEOUT: "delay",
242
+ MOUSE_ENTER: "mouseEnter",
243
+ MOUSE_LEAVE: "mouseLeave",
244
+ WHILE_HOVERING: "whileHover",
245
+ WHILE_PRESSING: "whileTap"
246
+ };
247
+ function calculateChildDeltas(sourceNode, destNode) {
248
+ const deltas = {};
249
+ if (!sourceNode.children || !destNode.children)
250
+ return deltas;
251
+ const sourceChildren = new Map(sourceNode.children.map((c) => [c.name, c]));
252
+ const destChildren = new Map(destNode.children.map((c) => [c.name, c]));
253
+ for (const [name, destChild] of destChildren.entries()) {
254
+ const sourceChild = sourceChildren.get(name);
255
+ if (sourceChild) {
256
+ const delta = { name };
257
+ if (sourceChild.x !== destChild.x)
258
+ delta.x = destChild.x - sourceChild.x;
259
+ if (sourceChild.y !== destChild.y)
260
+ delta.y = destChild.y - sourceChild.y;
261
+ if (sourceChild.opacity !== destChild.opacity)
262
+ delta.opacity = destChild.opacity;
263
+ deltas[name] = delta;
264
+ }
265
+ }
266
+ return deltas;
267
+ }
268
+ function extractAnimations(node, getNodeById) {
269
+ if (!node.reactions || node.reactions.length === 0)
270
+ return null;
271
+ const interactions = node.reactions.map((reaction) => {
272
+ const mapped = {
273
+ trigger: TRIGGER_MAP[reaction.trigger?.type] || reaction.trigger?.type,
274
+ actionType: reaction.action?.type
275
+ };
276
+ if (reaction.action?.destinationId) {
277
+ mapped.destinationId = reaction.action.destinationId;
278
+ const lookup = getNodeById || (typeof figma !== "undefined" ? figma.getNodeById : void 0);
279
+ if (lookup) {
280
+ try {
281
+ const destNode = lookup(mapped.destinationId);
282
+ if (destNode) {
283
+ mapped.destinationName = destNode.name;
284
+ if (reaction.action.navigation === "NAVIGATE") {
285
+ mapped.childDeltas = calculateChildDeltas(node, destNode);
286
+ }
287
+ }
288
+ } catch (e) {
289
+ }
290
+ }
291
+ }
292
+ if (reaction.action?.transition) {
293
+ const t = reaction.action.transition;
294
+ mapped.transition = {
295
+ duration: t.duration || 0.3,
296
+ ease: t.easing ? EASING_MAP[t.easing] || "easeInOut" : "easeInOut"
297
+ };
298
+ }
299
+ return mapped;
300
+ });
301
+ return { nodeId: node.id, nodeName: node.name, interactions };
302
+ }
303
+
304
+ // src/core/generators/reactGenerator.ts
305
+ function generateHash(content) {
306
+ let hash = 0;
307
+ for (let i = 0; i < content.length; i++) {
308
+ const char = content.charCodeAt(i);
309
+ hash = (hash << 5) - hash + char;
310
+ hash = hash & hash;
311
+ }
312
+ return Math.abs(hash).toString(16).substring(0, 8);
313
+ }
314
+ function sanitizeName2(name) {
315
+ return name.replace(/[^a-zA-Z0-9]/g, "_").replace(/^([0-9])/, "_$1");
316
+ }
317
+ function toPascalCase2(name, naming) {
318
+ return sanitizeName2(name).split("_").map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("");
319
+ }
320
+ function generateReactComponent(node, options = {}) {
321
+ const config = options.config || DEFAULT_CONFIG;
322
+ const files = {};
323
+ const componentName = options.nameOverrides?.get(node.id) || options.componentName || toPascalCase2(node.name, config.naming);
324
+ const getNodeById = options.getNodeById;
325
+ let hasAnimations = false;
326
+ const allHandlerDeclarations = [];
327
+ const generateNodeCode = (n, indent = " ", parentAnimations) => {
328
+ if (!n.animations && n.reactions) {
329
+ n.animations = extractAnimations(n, getNodeById);
330
+ }
331
+ const styles = extractNodeStyles(n, config);
332
+ const motionProps = {};
333
+ const eventHandlers = {};
334
+ if (n.animations && n.animations.interactions.length > 0) {
335
+ if (config.animations !== "css") {
336
+ hasAnimations = true;
337
+ }
338
+ const { functionDeclarations, propMappings } = generateHandlers(n.id, n.name, n.animations.interactions, config);
339
+ allHandlerDeclarations.push(...functionDeclarations);
340
+ Object.assign(eventHandlers, propMappings);
341
+ n.animations.interactions.forEach((interaction) => {
342
+ if (interaction.trigger === "whileHover")
343
+ motionProps.whileHover = { scale: 1.05 };
344
+ else if (interaction.trigger === "whileTap")
345
+ motionProps.whileTap = { scale: 0.95 };
346
+ });
347
+ }
348
+ if (parentAnimations) {
349
+ const interaction = parentAnimations.interactions.find((i) => i.childDeltas);
350
+ if (interaction && interaction.childDeltas[n.name]) {
351
+ hasAnimations = true;
352
+ const delta = interaction.childDeltas[n.name];
353
+ motionProps.layoutId = sanitizeName2(n.name).toLowerCase();
354
+ const initial = {};
355
+ if (delta.x !== void 0)
356
+ initial.x = -delta.x;
357
+ if (delta.y !== void 0)
358
+ initial.y = -delta.y;
359
+ if (delta.opacity !== void 0)
360
+ initial.opacity = 0;
361
+ if (Object.keys(initial).length > 0) {
362
+ motionProps.initial = initial;
363
+ motionProps.animate = { x: 0, y: 0, opacity: 1 };
364
+ }
365
+ }
366
+ }
367
+ if (n.layoutMode === "HORIZONTAL" || n.layoutMode === "VERTICAL") {
368
+ styles.display = "flex";
369
+ styles.flexDirection = n.layoutMode === "HORIZONTAL" ? "row" : "column";
370
+ styles.gap = `${n.itemSpacing || 0}px`;
371
+ styles.padding = `${n.paddingTop || 0}px ${n.paddingRight || 0}px ${n.paddingBottom || 0}px ${n.paddingLeft || 0}px`;
372
+ } else if (n.type !== "GROUP" && n.type !== "DOCUMENT" && n.type !== "PAGE") {
373
+ styles.position = "absolute";
374
+ styles.left = `${n.x}px`;
375
+ styles.top = `${n.y}px`;
376
+ styles.width = `${n.width}px`;
377
+ styles.height = `${n.height}px`;
378
+ }
379
+ const styleAttr = `style={${JSON.stringify(styles, null, 2).replace(/"([^"]+)":/g, "$1:")}}`;
380
+ const motionAttr = Object.keys(motionProps).length > 0 ? ` ${Object.entries(motionProps).map(([k, v]) => `${k}={${JSON.stringify(v)}}`).join(" ")}` : "";
381
+ const handlerAttr = Object.keys(eventHandlers).length > 0 ? ` ${Object.entries(eventHandlers).map(([k, v]) => `${k}={${v}}`).join(" ")}` : "";
382
+ const syncAttrs = `data-figma-id="${n.id}"`;
383
+ const tagPrefix = Object.keys(motionProps).length > 0 ? "motion." : "";
384
+ if (n.type === "TEXT")
385
+ return `${indent}<${tagPrefix}span ${syncAttrs}${handlerAttr} ${styleAttr}${motionAttr}>${n.characters}</${tagPrefix}span>`;
386
+ if (n.type === "VECTOR" || n.type === "STAR" || n.type === "POLYGON" || n.type === "ELLIPSE" && !n.cornerRadius)
387
+ return `${indent}<${tagPrefix}div ${syncAttrs}${handlerAttr} ${styleAttr}${motionAttr}>/* SVG Placeholder */</${tagPrefix}div>`;
388
+ const childrenCode = n.children ? n.children.map((child) => generateNodeCode(child, indent + " ", n.animations)).join("\n") : "";
389
+ let tag = "div";
390
+ if (n.type === "GROUP")
391
+ return childrenCode;
392
+ if (n.type === "SECTION")
393
+ tag = "section";
394
+ return `${indent}<${tagPrefix}${tag} ${syncAttrs}${handlerAttr} ${styleAttr}${motionAttr}>
395
+ ${childrenCode}
396
+ ${indent}</${tagPrefix}${tag}>`;
397
+ };
398
+ const nodeCode = generateNodeCode(node, " ");
399
+ const contentHash = generateHash(nodeCode);
400
+ const imports = ["import React from 'react';"];
401
+ if (hasAnimations)
402
+ imports.push("import { motion, AnimatePresence } from 'framer-motion';");
403
+ const finalCode = `
404
+ ${imports.join("\n")}
405
+
406
+ /**
407
+ * Figma Layer: ${node.name}
408
+ * ID: ${node.id}
409
+ * Hash: ${contentHash}
410
+ */
411
+ export const ${componentName}: React.FC = () => {
412
+ ${Array.from(new Set(allHandlerDeclarations)).join("\n\n")}
413
+
414
+ return (
415
+ ${hasAnimations ? '<AnimatePresence mode="wait">' : ""}
416
+ ${nodeCode}
417
+ ${hasAnimations ? "</AnimatePresence>" : ""}
418
+ );
419
+ };
420
+ `;
421
+ files[`${componentName}.tsx`] = finalCode;
422
+ return { code: finalCode, files, hasAnimations, hash: contentHash };
423
+ }
424
+
425
+ // src/core/utils/collisionDetector.ts
426
+ var RESERVED_WORDS = /* @__PURE__ */ new Set([
427
+ "break",
428
+ "case",
429
+ "catch",
430
+ "class",
431
+ "const",
432
+ "continue",
433
+ "debugger",
434
+ "default",
435
+ "delete",
436
+ "do",
437
+ "else",
438
+ "enum",
439
+ "export",
440
+ "extends",
441
+ "false",
442
+ "finally",
443
+ "for",
444
+ "function",
445
+ "if",
446
+ "import",
447
+ "in",
448
+ "instanceof",
449
+ "new",
450
+ "null",
451
+ "return",
452
+ "super",
453
+ "switch",
454
+ "this",
455
+ "throw",
456
+ "true",
457
+ "try",
458
+ "typeof",
459
+ "var",
460
+ "void",
461
+ "while",
462
+ "with",
463
+ "yield",
464
+ "let",
465
+ "static",
466
+ "await",
467
+ "abstract",
468
+ "boolean",
469
+ "byte",
470
+ "char",
471
+ "double",
472
+ "final",
473
+ "float",
474
+ "goto",
475
+ "int",
476
+ "long",
477
+ "native",
478
+ "short",
479
+ "synchronized",
480
+ "transient",
481
+ "volatile"
482
+ ]);
483
+ function detectCollisions(nodes) {
484
+ const warnings = [];
485
+ const seenNames = /* @__PURE__ */ new Map();
486
+ const seenSanitized = /* @__PURE__ */ new Map();
487
+ nodes.forEach((node) => {
488
+ const name = node.name;
489
+ const sanitized = name.replace(/[^a-zA-Z0-9]/g, "");
490
+ const id = node.id;
491
+ if (RESERVED_WORDS.has(name.toLowerCase())) {
492
+ warnings.push({
493
+ type: "reserved",
494
+ name1: name,
495
+ name2: "",
496
+ figmaId1: id,
497
+ figmaId2: "",
498
+ suggestion: `Fig${name.charAt(0).toUpperCase()}${name.slice(1)}`,
499
+ severity: "error"
500
+ });
501
+ }
502
+ if (seenNames.has(name)) {
503
+ const other = seenNames.get(name);
504
+ if (other.figmaId !== id) {
505
+ warnings.push({
506
+ type: "exact",
507
+ name1: name,
508
+ name2: name,
509
+ figmaId1: id,
510
+ figmaId2: other.figmaId,
511
+ suggestion: `${name}_${id.replace(":", "_")}`,
512
+ severity: "error"
513
+ });
514
+ }
515
+ } else {
516
+ seenNames.set(name, { figmaId: id, originalName: name });
517
+ }
518
+ const lowerSanitized = sanitized.toLowerCase();
519
+ if (seenSanitized.has(lowerSanitized)) {
520
+ const other = seenSanitized.get(lowerSanitized);
521
+ if (other.figmaId !== id) {
522
+ warnings.push({
523
+ type: lowerSanitized === other.originalName.replace(/[^a-zA-Z0-9]/g, "").toLowerCase() ? "caseInsensitive" : "sanitized",
524
+ name1: name,
525
+ name2: other.originalName,
526
+ figmaId1: id,
527
+ figmaId2: other.figmaId,
528
+ suggestion: `${sanitized}_${id.replace(":", "_")}`,
529
+ severity: "warning"
530
+ });
531
+ }
532
+ } else {
533
+ seenSanitized.set(lowerSanitized, { figmaId: id, originalName: name });
534
+ }
535
+ });
536
+ return warnings;
537
+ }
538
+
539
+ // src/core/utils/codeMetrics.ts
540
+ function calculateMetrics(nodes) {
541
+ let totalLines = 0;
542
+ let totalCharacters = 0;
543
+ let jsxElements = 0;
544
+ let styleProperties = 0;
545
+ let handlers = 0;
546
+ let animations = 0;
547
+ nodes.forEach((node) => {
548
+ if (node.reactCode) {
549
+ totalLines += node.reactCode.split("\n").length;
550
+ totalCharacters += node.reactCode.length;
551
+ jsxElements += (node.reactCode.match(/<[a-zA-Z]/g) || []).length;
552
+ handlers += (node.reactCode.match(/handle[A-Z]/g) || []).length;
553
+ if (node.hasAnimations)
554
+ animations++;
555
+ }
556
+ if (node.css) {
557
+ styleProperties += (node.css.match(/:/g) || []).length;
558
+ }
559
+ });
560
+ let complexity = "simple";
561
+ if (totalLines > 500 || jsxElements > 20)
562
+ complexity = "moderate";
563
+ if (totalLines > 1e3 || jsxElements > 50)
564
+ complexity = "complex";
565
+ if (totalLines > 2e3)
566
+ complexity = "very-complex";
567
+ return {
568
+ totalLines,
569
+ totalCharacters,
570
+ fileCount: nodes.length,
571
+ jsxElements,
572
+ styleProperties,
573
+ handlers,
574
+ animations,
575
+ collisions: 0,
576
+ // Set externally
577
+ complexity
578
+ };
579
+ }
580
+
581
+ // src/design-system/extractDesignTokens.ts
582
+ function extractDesignTokens(nodes) {
583
+ return {
584
+ palette: extractPalette(nodes),
585
+ typography: extractTypography(nodes),
586
+ spacing: extractSpacing(nodes),
587
+ shadows: extractShadows(nodes)
588
+ };
589
+ }
590
+ function extractPalette(nodes) {
591
+ return {};
592
+ }
593
+ function extractTypography(nodes) {
594
+ return {};
595
+ }
596
+ function extractSpacing(nodes) {
597
+ return {};
598
+ }
599
+ function extractShadows(nodes) {
600
+ return {};
601
+ }
602
+
603
+ // src/vibecode-guard/generateLockfile.ts
604
+ function generateLockfile(projectName, fileId, tokens) {
605
+ const lockfile = {
606
+ version: "2.0.0",
607
+ figmaFile: projectName,
608
+ figmaFileId: fileId,
609
+ extractedAt: (/* @__PURE__ */ new Date()).toISOString(),
610
+ designTokens: tokens,
611
+ componentLibrary: {},
612
+ // Would be populated by analyzing component sets
613
+ constraints: {
614
+ maxWidth: "1200px",
615
+ gridColumns: 12,
616
+ gutterWidth: "24px"
617
+ }
618
+ };
619
+ return JSON.stringify(lockfile, null, 2);
620
+ }
621
+
622
+ // src/vibecode-guard/generatePromptContext.ts
623
+ function generatePromptContext(tokens) {
624
+ return `
625
+ DESIGN SYSTEM (DO NOT DEVIATE):
626
+ - Primary: ${tokens.colors.primary[500]} (500), ${tokens.colors.primary[600]} (600)
627
+ - Secondary: ${tokens.colors.secondary[500]} (500)
628
+ - Font: ${tokens.typography.fontFamilies.primary}
629
+ - Spacing: ${tokens.spacing.unit}px base unit
630
+ - Radius: ${tokens.borderRadius.scale.join(", ")}
631
+
632
+ When building components for this project, use EXACTLY these values.
633
+ Do not approximate, round, or substitute. Reference the .figmake.lock file.
634
+ `;
635
+ }
636
+
637
+ // src/plugin/code.ts
638
+ var currentConfig = DEFAULT_CONFIG;
639
+ figma.showUI(__html__, { width: 600, height: 800 });
640
+ figma.clientStorage.getAsync("plugin_settings").then((settings) => {
641
+ if (settings) {
642
+ currentConfig = { ...DEFAULT_CONFIG, ...settings };
643
+ }
644
+ figma.ui.postMessage({ type: "settings-loaded", config: currentConfig });
645
+ });
646
+ figma.ui.onmessage = async (msg) => {
647
+ if (msg.type === "save-settings") {
648
+ currentConfig = msg.config;
649
+ figma.clientStorage.setAsync("plugin_settings", currentConfig);
650
+ updateSelection();
651
+ } else if (msg.type === "validate-node") {
652
+ const selection = figma.currentPage.selection;
653
+ if (selection.length > 0) {
654
+ const node = selection[0];
655
+ const bytes = await node.exportAsync({ format: "PNG", constraint: { type: "SCALE", value: 1 } });
656
+ figma.ui.postMessage({ type: "validation-image", bytes, width: node.width, height: node.height });
657
+ }
658
+ } else if (msg.type === "generate-guard") {
659
+ const tokens = extractDesignTokens([figma.root]);
660
+ const lockfile = generateLockfile(figma.root.name, figma.fileKey || "local", tokens);
661
+ const promptContext = generatePromptContext(tokens);
662
+ figma.ui.postMessage({ type: "guard-data", lockfile, promptContext, tokens });
663
+ }
664
+ };
665
+ function extractProperties(node) {
666
+ const props = {
667
+ id: node.id,
668
+ name: node.name,
669
+ type: node.type
670
+ };
671
+ if ("reactions" in node) {
672
+ props.reactions = clone(node.reactions);
673
+ props.animations = extractAnimations(node, figma.getNodeById);
674
+ }
675
+ if ("x" in node)
676
+ props.x = node.x;
677
+ if ("y" in node)
678
+ props.y = node.y;
679
+ if ("width" in node)
680
+ props.width = node.width;
681
+ if ("height" in node)
682
+ props.height = node.height;
683
+ if ("rotation" in node)
684
+ props.rotation = node.rotation;
685
+ if ("visible" in node)
686
+ props.visible = node.visible;
687
+ if ("opacity" in node)
688
+ props.opacity = node.opacity;
689
+ if ("blendMode" in node)
690
+ props.blendMode = node.blendMode;
691
+ if ("constraints" in node)
692
+ props.constraints = node.constraints;
693
+ if ("cornerRadius" in node)
694
+ props.cornerRadius = node.cornerRadius === figma.mixed ? "mixed" : node.cornerRadius;
695
+ if ("topLeftRadius" in node)
696
+ props.topLeftRadius = node.topLeftRadius;
697
+ if ("topRightRadius" in node)
698
+ props.topRightRadius = node.topRightRadius;
699
+ if ("bottomLeftRadius" in node)
700
+ props.bottomLeftRadius = node.bottomLeftRadius;
701
+ if ("bottomRightRadius" in node)
702
+ props.bottomRightRadius = node.bottomRightRadius;
703
+ if ("layoutMode" in node)
704
+ props.layoutMode = node.layoutMode;
705
+ if ("primaryAxisSizingMode" in node)
706
+ props.primaryAxisSizingMode = node.primaryAxisSizingMode;
707
+ if ("counterAxisSizingMode" in node)
708
+ props.counterAxisSizingMode = node.counterAxisSizingMode;
709
+ if ("primaryAxisAlignItems" in node)
710
+ props.primaryAxisAlignItems = node.primaryAxisAlignItems;
711
+ if ("counterAxisAlignItems" in node)
712
+ props.counterAxisAlignItems = node.counterAxisAlignItems;
713
+ if ("paddingLeft" in node)
714
+ props.paddingLeft = node.paddingLeft;
715
+ if ("paddingRight" in node)
716
+ props.paddingRight = node.paddingRight;
717
+ if ("paddingTop" in node)
718
+ props.paddingTop = node.paddingTop;
719
+ if ("paddingBottom" in node)
720
+ props.paddingBottom = node.paddingBottom;
721
+ if ("itemSpacing" in node)
722
+ props.itemSpacing = node.itemSpacing;
723
+ if ("fills" in node)
724
+ props.fills = clone(node.fills);
725
+ if ("strokes" in node)
726
+ props.strokes = clone(node.strokes);
727
+ if ("strokeWeight" in node)
728
+ props.strokeWeight = node.strokeWeight;
729
+ if ("strokeAlign" in node)
730
+ props.strokeAlign = node.strokeAlign;
731
+ if ("strokeCap" in node)
732
+ props.strokeCap = node.strokeCap;
733
+ if ("strokeJoin" in node)
734
+ props.strokeJoin = node.strokeJoin;
735
+ if ("dashPattern" in node)
736
+ props.dashPattern = node.dashPattern;
737
+ if ("effects" in node)
738
+ props.effects = clone(node.effects);
739
+ if (node.type === "TEXT") {
740
+ const textNode = node;
741
+ props.characters = textNode.characters;
742
+ props.fontName = clone(textNode.fontName);
743
+ props.fontSize = textNode.fontSize === figma.mixed ? "mixed" : textNode.fontSize;
744
+ props.letterSpacing = clone(textNode.letterSpacing);
745
+ props.lineHeight = clone(textNode.lineHeight);
746
+ props.textAlignHorizontal = textNode.textAlignHorizontal;
747
+ props.textAlignVertical = textNode.textAlignVertical;
748
+ props.textDecoration = textNode.textDecoration;
749
+ props.textCase = textNode.textCase;
750
+ }
751
+ if ("children" in node) {
752
+ props.children = node.children.map((child) => extractProperties(child));
753
+ }
754
+ props.css = figmaToCSS(node);
755
+ const reactResult = generateReactComponent(node, { getNodeById: figma.getNodeById, config: currentConfig });
756
+ props.reactCode = reactResult.code;
757
+ props.fileCount = Object.keys(reactResult.files).length;
758
+ props.generatedFiles = reactResult.files;
759
+ return props;
760
+ }
761
+ function clone(val) {
762
+ if (val === figma.mixed)
763
+ return "mixed";
764
+ const type = typeof val;
765
+ if (val === null)
766
+ return null;
767
+ if (type === "undefined" || type === "number" || type === "string" || type === "boolean")
768
+ return val;
769
+ if (type === "object") {
770
+ if (val instanceof Array)
771
+ return val.map((x) => clone(x));
772
+ if (val instanceof Uint8Array)
773
+ return Array.from(val);
774
+ const ret = {};
775
+ for (const key in val)
776
+ ret[key] = clone(val[key]);
777
+ return ret;
778
+ }
779
+ return val;
780
+ }
781
+ function updateSelection() {
782
+ const selection = figma.currentPage.selection;
783
+ const collisions = detectCollisions(selection);
784
+ const data = selection.map((node) => extractProperties(node));
785
+ const metrics = calculateMetrics(data);
786
+ metrics.collisions = collisions.length;
787
+ figma.ui.postMessage({ type: "update-properties", data, metrics, collisions });
788
+ }
789
+ figma.on("selectionchange", updateSelection);
790
+ updateSelection();
791
+ })();