import axios from "axios";
import dotenv from "dotenv";
import { writeFile } from "fs/promises";
import * as sass from "sass";
dotenv.config();
const FIGMA_TOKEN = process.env.FIGMA_TOKEN;
const figmaApi = axios.create({
baseURL: "https://api.figma.com/v1/",
headers: { "X-Figma-Token": FIGMA_TOKEN },
});
// Helper function to normalize names for CSS classes
function normalizeClassName(name: string): string {
let normalized = name
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
// CSS classes can't start with a number, prefix with 'c-'
if (/^\d/.test(normalized)) {
normalized = 'c-' + normalized;
}
return normalized;
}
// Helper to convert Figma color to CSS
function colorToCSS(color: any, opacity = 1): string {
const r = Math.round(color.r * 255);
const g = Math.round(color.g * 255);
const b = Math.round(color.b * 255);
const a = (color.a ?? 1) * opacity;
return a === 1 ? `rgb(${r}, ${g}, ${b})` : `rgba(${r}, ${g}, ${b}, ${a})`;
}
// Helper to get text styles
function getTextStyles(node: any): any {
const styles: any = {};
if (node.style) {
if (node.style.fontSize) {
styles.fontSize = `${node.style.fontSize}px`;
}
if (node.style.fontWeight) {
styles.fontWeight = node.style.fontWeight;
}
if (node.style.fontFamily) {
styles.fontFamily = `"${node.style.fontFamily}", sans-serif`;
}
if (node.style.letterSpacing) {
styles.letterSpacing = `${node.style.letterSpacing}px`;
}
if (node.style.lineHeightPx) {
styles.lineHeight = `${node.style.lineHeightPx}px`;
}
if (node.style.textAlignHorizontal) {
const align = node.style.textAlignHorizontal.toLowerCase();
if (align === 'left' || align === 'right' || align === 'center') {
styles.textAlign = align;
}
}
}
if (node.fills && node.fills.length > 0) {
const fill = node.fills[0];
if (fill.type === "SOLID" && fill.color && fill.visible !== false) {
styles.color = colorToCSS(fill.color, fill.opacity);
}
}
return styles;
}
// Generate SCSS from Figma node structure
function generateSCSS(node: any, componentClass: string, indent = 0): string {
const spaces = ' '.repeat(indent);
let scss = '';
const nodeClass = normalizeClassName(node.id);
const isRoot = indent === 0;
if (isRoot) {
scss += `.${componentClass} {\n`;
scss += ` position: relative;\n`;
if (node.absoluteBoundingBox) {
scss += ` width: 100%;\n`;
scss += ` max-width: ${Math.round(node.absoluteBoundingBox.width)}px;\n`;
scss += ` min-height: ${Math.round(node.absoluteBoundingBox.height)}px;\n`;
}
// Background
if (node.fills && node.fills.length > 0) {
const fill = node.fills[0];
if (fill.type === "SOLID" && fill.color && fill.visible !== false) {
scss += ` background-color: ${colorToCSS(fill.color, fill.opacity)};\n`;
}
}
// Padding for container
if (node.type === "FRAME" || node.type === "COMPONENT") {
scss += ` padding: 20px;\n`;
}
// Border radius
if (node.cornerRadius && node.cornerRadius > 0) {
scss += ` border-radius: ${node.cornerRadius}px;\n`;
}
// Shadows
if (node.effects && node.effects.length > 0) {
const shadows = node.effects
.filter((e: any) => e.type === "DROP_SHADOW" && e.visible !== false)
.map((e: any) => {
const color = colorToCSS(e.color, e.color.a);
return `${e.offset.x}px ${e.offset.y}px ${e.radius}px ${color}`;
});
if (shadows.length > 0) {
scss += ` box-shadow: ${shadows.join(', ')};\n`;
}
}
scss += `\n`;
} else {
// Skip generating CSS for icons and images - they have their own styling
if (isLikelyIcon(node) || isLikelyImage(node)) {
return '';
}
scss += `${spaces}.${nodeClass} {\n`;
// Layout properties
if (node.absoluteBoundingBox) {
// Use Bootstrap-friendly sizing
if (node.type === "FRAME" || node.type === "GROUP" || node.type === "COMPONENT") {
scss += `${spaces} margin-bottom: 1rem;\n`;
}
// Width constraints
const width = Math.round(node.absoluteBoundingBox.width);
if (width < 50) {
// Small elements - use auto width
} else if (width < 200) {
scss += `${spaces} width: auto;\n`;
} else {
scss += `${spaces} width: 100%;\n`;
}
}
// Background
if (node.fills && node.fills.length > 0) {
const fill = node.fills[0];
if (fill.type === "SOLID" && fill.color && fill.visible !== false) {
scss += `${spaces} background-color: ${colorToCSS(fill.color, fill.opacity)};\n`;
}
}
// Text styles for TEXT nodes
if (node.type === "TEXT") {
const textStyles = getTextStyles(node);
for (const [key, value] of Object.entries(textStyles)) {
const cssKey = key.replace(/([A-Z])/g, '-$1').toLowerCase();
scss += `${spaces} ${cssKey}: ${value};\n`;
}
}
// Padding for containers
if (node.type === "FRAME" || node.type === "COMPONENT") {
scss += `${spaces} padding: 1rem;\n`;
}
// Border radius
if (node.cornerRadius && node.cornerRadius > 0) {
scss += `${spaces} border-radius: ${node.cornerRadius}px;\n`;
}
// Borders
if (node.strokes && node.strokes.length > 0 && node.strokeWeight) {
const stroke = node.strokes[0];
if (stroke.type === "SOLID" && stroke.color) {
scss += `${spaces} border: ${node.strokeWeight}px solid ${colorToCSS(stroke.color)};\n`;
}
}
// Shadows
if (node.effects && node.effects.length > 0) {
const shadows = node.effects
.filter((e: any) => e.type === "DROP_SHADOW" && e.visible !== false)
.map((e: any) => {
const color = colorToCSS(e.color, e.color.a);
return `${e.offset.x}px ${e.offset.y}px ${e.radius}px ${color}`;
});
if (shadows.length > 0) {
scss += `${spaces} box-shadow: ${shadows.join(', ')};\n`;
}
}
scss += `\n`;
}
// Process children recursively
if (node.children && node.children.length > 0) {
// Check if children should use flexbox/grid
const hasMultipleChildren = node.children.length > 1;
if (hasMultipleChildren && (node.type === "FRAME" || node.type === "COMPONENT")) {
// Add flex layout to parent
scss += `${spaces} display: flex;\n`;
scss += `${spaces} flex-direction: column;\n`;
scss += `${spaces} gap: 1rem;\n`;
scss += `\n`;
}
for (const child of node.children) {
if (child.visible !== false) {
scss += generateSCSS(child, componentClass, indent + 1);
}
}
}
scss += `${spaces}}\n\n`;
return scss;
}
// Helper to detect if node might be an icon
function isLikelyIcon(node: any): boolean {
const name = node.name?.toLowerCase() || '';
const width = node.absoluteBoundingBox?.width || 0;
const height = node.absoluteBoundingBox?.height || 0;
const isSmall = width <= 64 && height <= 64;
const isSquarish = Math.abs(width - height) < 10;
return (
isSmall &&
isSquarish &&
(name.includes('icon') ||
name.includes('logo') ||
name.includes('symbol') ||
node.type === 'VECTOR' ||
node.type === 'BOOLEAN_OPERATION')
);
}
// Helper to detect if node might be an image
function isLikelyImage(node: any): boolean {
const name = node.name?.toLowerCase() || '';
const width = node.absoluteBoundingBox?.width || 0;
const height = node.absoluteBoundingBox?.height || 0;
const isLargeEnough = width > 100 || height > 100;
// Check if it has image fills
if (node.fills && node.fills.length > 0) {
const hasImageFill = node.fills.some((fill: any) => fill.type === 'IMAGE');
if (hasImageFill) return true;
}
return (
isLargeEnough &&
(name.includes('image') ||
name.includes('photo') ||
name.includes('picture') ||
name.includes('img') ||
name.includes('thumbnail'))
);
}
// Helper to get appropriate Font Awesome icon based on context
function getIconClass(node: any): string {
const name = node.name?.toLowerCase() || '';
// Common icon mappings
if (name.includes('user') || name.includes('profile') || name.includes('avatar')) return 'fa-user';
if (name.includes('search') || name.includes('magnify')) return 'fa-search';
if (name.includes('menu') || name.includes('hamburger') || name.includes('bars')) return 'fa-bars';
if (name.includes('close') || name.includes('x') || name.includes('times')) return 'fa-times';
if (name.includes('home')) return 'fa-home';
if (name.includes('mail') || name.includes('envelope') || name.includes('email')) return 'fa-envelope';
if (name.includes('phone') || name.includes('call')) return 'fa-phone';
if (name.includes('star')) return 'fa-star';
if (name.includes('heart')) return 'fa-heart';
if (name.includes('cart') || name.includes('shopping')) return 'fa-shopping-cart';
if (name.includes('settings') || name.includes('gear') || name.includes('cog')) return 'fa-cog';
if (name.includes('arrow-right')) return 'fa-arrow-right';
if (name.includes('arrow-left')) return 'fa-arrow-left';
if (name.includes('check')) return 'fa-check';
if (name.includes('bell') || name.includes('notification')) return 'fa-bell';
if (name.includes('calendar') || name.includes('date')) return 'fa-calendar';
if (name.includes('clock') || name.includes('time')) return 'fa-clock';
if (name.includes('location') || name.includes('map') || name.includes('pin')) return 'fa-map-marker-alt';
if (name.includes('download')) return 'fa-download';
if (name.includes('upload')) return 'fa-upload';
if (name.includes('share')) return 'fa-share-alt';
if (name.includes('edit') || name.includes('pencil')) return 'fa-edit';
if (name.includes('trash') || name.includes('delete')) return 'fa-trash';
if (name.includes('play')) return 'fa-play';
if (name.includes('pause')) return 'fa-pause';
// Default icon
return 'fa-circle';
}
// Generate semantic HTML from Figma node
function generateHTML(node: any, indent = 2): string {
const spaces = ' '.repeat(indent);
const nodeClass = normalizeClassName(node.id);
// Handle icons with Font Awesome
if (isLikelyIcon(node)) {
const iconClass = getIconClass(node);
return `${spaces}<i class="fas ${iconClass} ${nodeClass}" aria-hidden="true"></i>\n`;
}
// Handle images with placeholders
if (isLikelyImage(node)) {
const width = Math.round(node.absoluteBoundingBox?.width || 300);
const height = Math.round(node.absoluteBoundingBox?.height || 200);
const alt = node.name || 'Image';
return `${spaces}<img src="https://via.placeholder.com/${width}x${height}" alt="${alt}" class="${nodeClass} img-fluid" />\n`;
}
if (node.type === "TEXT") {
const text = node.characters || '';
// Detect heading vs paragraph
const fontSize = node.style?.fontSize || 16;
const fontWeight = node.style?.fontWeight || 400;
if (fontSize > 24 || fontWeight >= 700) {
return `${spaces}<h2 class="${nodeClass}">${text}</h2>\n`;
} else if (fontSize > 18) {
return `${spaces}<h3 class="${nodeClass}">${text}</h3>\n`;
} else {
return `${spaces}<p class="${nodeClass}">${text}</p>\n`;
}
}
if (!node.children || node.children.length === 0) {
return `${spaces}<div class="${nodeClass}"></div>\n`;
}
// Use semantic HTML5 elements
let tag = 'div';
if (node.name?.toLowerCase().includes('header')) tag = 'header';
if (node.name?.toLowerCase().includes('footer')) tag = 'footer';
if (node.name?.toLowerCase().includes('nav')) tag = 'nav';
if (node.name?.toLowerCase().includes('article')) tag = 'article';
if (node.name?.toLowerCase().includes('section')) tag = 'section';
let html = `${spaces}<${tag} class="${nodeClass}">\n`;
for (const child of node.children) {
if (child.visible !== false) {
html += generateHTML(child, indent + 2);
}
}
html += `${spaces}</${tag}>\n`;
return html;
}
// Main function to generate design with Bootstrap
async function generateDesignWithBootstrap(fileKey: string, nodeId: string) {
try {
console.log("\nšØ Generating Bootstrap-based design with screenshot reference...\n");
console.log(`File: ${fileKey}`);
console.log(`Node: ${nodeId}\n`);
// Step 1: Fetch design data
console.log("š Step 1: Fetching Figma design data...");
const response = await figmaApi.get(`files/${fileKey}/nodes`, {
params: { ids: nodeId }
});
const nodeData = response.data.nodes[nodeId];
if (!nodeData) {
throw new Error(`Node ${nodeId} not found`);
}
const node = nodeData.document;
const componentName = normalizeClassName(node.name || 'component');
console.log(`ā
Found node: ${node.name} (${node.type})`);
console.log(` Component class: .${componentName}`);
console.log(` Size: ${Math.round(node.absoluteBoundingBox?.width)}x${Math.round(node.absoluteBoundingBox?.height)}px\n`);
// Step 2: Fetch screenshot
console.log("šø Step 2: Fetching screenshot...");
let screenshotUrl = "";
let screenshotFile = "";
try {
const imageResponse = await figmaApi.get(`images/${fileKey}`, {
params: { ids: nodeId, format: "png", scale: 2 }
});
if (imageResponse.data.images && imageResponse.data.images[nodeId]) {
screenshotUrl = imageResponse.data.images[nodeId];
console.log(`ā
Screenshot URL obtained`);
// Download the screenshot
console.log("ā¬ļø Downloading screenshot...");
const imgData = await axios.get(screenshotUrl, { responseType: "arraybuffer" });
screenshotFile = `output/${componentName}-screenshot.png`;
await writeFile(screenshotFile, Buffer.from(imgData.data));
console.log(`ā
Screenshot saved: ${screenshotFile}`);
console.log(` Size: ${(imgData.data.length / 1024).toFixed(2)} KB\n`);
}
} catch (error: any) {
console.log(`ā ļø Screenshot not available: ${error.message}\n`);
}
// Step 3: Generate SCSS
console.log("šØ Step 3: Generating SCSS...");
const scss = generateSCSS(node, componentName);
// Save SCSS file
const scssFile = `output/${componentName}.scss`;
await writeFile(scssFile, scss);
console.log(`ā
SCSS saved: ${scssFile}\n`);
// Step 4: Compile SCSS to CSS
console.log("āļø Step 4: Compiling SCSS to CSS...");
const result = sass.compileString(scss);
const compiledCSS = result.css;
console.log(`ā
SCSS compiled successfully\n`);
// Step 5: Generate HTML
console.log("š Step 5: Generating HTML...");
const htmlContent = generateHTML(node);
// Step 6: Create final HTML with Bootstrap
const finalHTML = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${node.name || 'Figma Design'}</title>
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Font Awesome Icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" integrity="sha512-iecdLmaskl7CVkqkXNQ/ZH/XLlvWZOJyj7Yy7tcenmpD1ypASozpmT/E0iPtmFIB46ZmdtAc9eNBvH0H/ZpiBw==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<style>
/*
COMPONENT: ${node.name}
CLASS: .${componentName}
šø SCREENSHOT REFERENCE: ${screenshotFile || 'Not available'}
${screenshotUrl ? `Original URL: ${screenshotUrl}` : ''}
š” This design uses Bootstrap 5 + Font Awesome + custom SCSS
- Bootstrap 5: Responsive grid, utilities, and components
- Font Awesome 6: Icons detected and replaced with <i> tags
- Placeholder Images: via.placeholder.com for image elements
- Custom SCSS: Compiled from Figma design properties
- Component class: .${componentName} (from Figma layer name)
šØ Icons & Images:
- Small vector elements ā Font Awesome icons
- Image fills/large elements ā Placeholder images
- Replace placeholder URLs with your actual images
- Change icon classes to match your needs
šÆ Compare with screenshot to refine:
1. Check spacing and alignment
2. Verify colors and typography
3. Replace placeholder images with real assets
4. Adjust icon classes if needed
5. Use Bootstrap utility classes for quick tweaks
*/
/* Compiled CSS from SCSS */
${compiledCSS}
/* Icon styling */
.fas, .far, .fab {
transition: transform 0.2s;
}
.fas:hover, .far:hover, .fab:hover {
transform: scale(1.1);
}
</style>
</head>
<body class="bg-light">
<div class="container my-5">
${screenshotFile ? `<!--
šø SCREENSHOT SAVED: ${screenshotFile}
Open side-by-side to compare and refine
šØ Icons: Using Font Awesome 6 (https://fontawesome.com/icons)
š¼ļø Images: Using placeholder.com (replace with your images)
-->\n ` : ''}
<div class="${componentName}">
${htmlContent} </div>
</div>
<!-- Bootstrap JS Bundle -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>`;
// Save final HTML
const htmlFile = `output/${componentName}.html`;
await writeFile(htmlFile, finalHTML);
console.log(`ā
HTML saved: ${htmlFile}\n`);
// Summary
console.log("š GENERATION SUMMARY:");
console.log(` ā
Component name: ${node.name}`);
console.log(` ā
Component class: .${componentName}`);
console.log(` ā
SCSS file: ${scssFile}`);
console.log(` ā
HTML file: ${htmlFile}`);
console.log(` ā
Screenshot: ${screenshotFile || 'Not available'}`);
console.log(` ā
Bootstrap 5: Included via CDN\n`);
console.log("šÆ NEXT STEPS:");
console.log(` 1. Open ${htmlFile} in browser`);
if (screenshotFile) {
console.log(` 2. Open ${screenshotFile} for comparison`);
}
console.log(` 3. Edit ${scssFile} to refine styles`);
console.log(` 4. Use Bootstrap utility classes for quick adjustments`);
console.log(` 5. Compare with screenshot and iterate\n`);
} catch (error: any) {
console.error("ā Error:", error.message);
if (error.response) {
console.error("Response:", error.response.data);
}
}
}
// Test with Threat Intel Hub design
const fileKey = "INORb289gzR56ny4JuCPLo";
const nodeId = "2380:4009";
generateDesignWithBootstrap(fileKey, nodeId);