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,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
+ }