import { FigmaNode, FigmaFill, figmaColorToHex } from './api.js';
export interface DesignSpecs {
name: string;
type: string;
dimensions: {
width: number;
height: number;
};
spacing: {
paddingTop?: number;
paddingRight?: number;
paddingBottom?: number;
paddingLeft?: number;
gap?: number;
};
colors: {
background?: string;
text?: string;
border?: string;
};
typography?: {
fontFamily?: string;
fontSize?: number;
fontWeight?: number;
lineHeight?: number;
letterSpacing?: number;
textAlign?: string;
};
border?: {
width?: number;
radius?: number | number[];
color?: string;
};
layout?: {
direction?: 'row' | 'column';
alignItems?: string;
justifyContent?: string;
};
children: DesignSpecs[];
text?: string;
}
/**
* Extract design specifications from a Figma node
*/
export function extractDesignSpecs(node: FigmaNode): DesignSpecs {
const specs: DesignSpecs = {
name: node.name,
type: node.type,
dimensions: {
width: node.absoluteBoundingBox?.width || 0,
height: node.absoluteBoundingBox?.height || 0
},
spacing: {},
colors: {},
children: []
};
// Extract spacing/padding
if (node.paddingTop !== undefined) specs.spacing.paddingTop = node.paddingTop;
if (node.paddingRight !== undefined) specs.spacing.paddingRight = node.paddingRight;
if (node.paddingBottom !== undefined) specs.spacing.paddingBottom = node.paddingBottom;
if (node.paddingLeft !== undefined) specs.spacing.paddingLeft = node.paddingLeft;
if (node.itemSpacing !== undefined) specs.spacing.gap = node.itemSpacing;
// Extract colors from fills
if (node.fills && node.fills.length > 0) {
const visibleFill = node.fills.find(f => f.visible !== false && f.type === 'SOLID');
if (visibleFill?.color) {
if (node.type === 'TEXT') {
specs.colors.text = figmaColorToHex(visibleFill.color);
} else {
specs.colors.background = figmaColorToHex(visibleFill.color);
}
}
}
// Extract border/stroke
if (node.strokes && node.strokes.length > 0) {
const stroke = node.strokes[0];
if (stroke.color) {
specs.colors.border = figmaColorToHex(stroke.color);
}
}
if (node.strokeWeight || node.cornerRadius || node.rectangleCornerRadii) {
specs.border = {};
if (node.strokeWeight) specs.border.width = node.strokeWeight;
if (node.cornerRadius) specs.border.radius = node.cornerRadius;
if (node.rectangleCornerRadii) specs.border.radius = node.rectangleCornerRadii;
if (specs.colors.border) specs.border.color = specs.colors.border;
}
// Extract typography for text nodes
if (node.type === 'TEXT' && node.style) {
specs.typography = {
fontFamily: node.style.fontFamily,
fontSize: node.style.fontSize,
fontWeight: node.style.fontWeight,
lineHeight: node.style.lineHeightPx,
letterSpacing: node.style.letterSpacing,
textAlign: node.style.textAlignHorizontal?.toLowerCase()
};
specs.text = node.characters;
}
// Extract layout info for auto-layout frames
if (node.layoutMode && node.layoutMode !== 'NONE') {
specs.layout = {
direction: node.layoutMode === 'HORIZONTAL' ? 'row' : 'column',
alignItems: mapFigmaAlign(node.counterAxisAlignItems),
justifyContent: mapFigmaAlign(node.primaryAxisAlignItems)
};
}
// Recursively extract children
if (node.children) {
specs.children = node.children.map(child => extractDesignSpecs(child));
}
return specs;
}
function mapFigmaAlign(align?: string): string | undefined {
if (!align) return undefined;
const mapping: Record<string, string> = {
'MIN': 'flex-start',
'CENTER': 'center',
'MAX': 'flex-end',
'SPACE_BETWEEN': 'space-between'
};
return mapping[align] || align.toLowerCase();
}
/**
* Flatten all design specs into a list for easier comparison
*/
export function flattenSpecs(specs: DesignSpecs, path: string = ''): FlattenedSpec[] {
const currentPath = path ? `${path} > ${specs.name}` : specs.name;
const result: FlattenedSpec[] = [];
// Add current node's specs
result.push({
path: currentPath,
type: specs.type,
specs: specs
});
// Recursively flatten children
for (const child of specs.children) {
result.push(...flattenSpecs(child, currentPath));
}
return result;
}
export interface FlattenedSpec {
path: string;
type: string;
specs: DesignSpecs;
}
/**
* Format specs as a readable summary
*/
export function formatSpecsSummary(specs: DesignSpecs, indent: number = 0): string {
const pad = ' '.repeat(indent);
const lines: string[] = [];
lines.push(`${pad}**${specs.name}** (${specs.type})`);
lines.push(`${pad} Size: ${specs.dimensions.width}x${specs.dimensions.height}px`);
if (Object.keys(specs.spacing).length > 0) {
const spacing = [];
if (specs.spacing.paddingTop !== undefined || specs.spacing.paddingBottom !== undefined ||
specs.spacing.paddingLeft !== undefined || specs.spacing.paddingRight !== undefined) {
spacing.push(`padding: ${specs.spacing.paddingTop || 0}/${specs.spacing.paddingRight || 0}/${specs.spacing.paddingBottom || 0}/${specs.spacing.paddingLeft || 0}px`);
}
if (specs.spacing.gap !== undefined) {
spacing.push(`gap: ${specs.spacing.gap}px`);
}
if (spacing.length > 0) {
lines.push(`${pad} Spacing: ${spacing.join(', ')}`);
}
}
if (specs.colors.background) {
lines.push(`${pad} Background: ${specs.colors.background}`);
}
if (specs.colors.text) {
lines.push(`${pad} Text Color: ${specs.colors.text}`);
}
if (specs.typography) {
const typo = [];
if (specs.typography.fontFamily) typo.push(specs.typography.fontFamily);
if (specs.typography.fontSize) typo.push(`${specs.typography.fontSize}px`);
if (specs.typography.fontWeight) typo.push(`weight: ${specs.typography.fontWeight}`);
if (specs.typography.lineHeight) typo.push(`line-height: ${specs.typography.lineHeight}px`);
lines.push(`${pad} Typography: ${typo.join(', ')}`);
}
if (specs.border) {
const border = [];
if (specs.border.width) border.push(`${specs.border.width}px`);
if (specs.border.radius) {
const radius = Array.isArray(specs.border.radius)
? specs.border.radius.join('/')
: specs.border.radius;
border.push(`radius: ${radius}px`);
}
if (specs.border.color) border.push(specs.border.color);
lines.push(`${pad} Border: ${border.join(', ')}`);
}
if (specs.layout) {
lines.push(`${pad} Layout: ${specs.layout.direction}, align: ${specs.layout.alignItems}, justify: ${specs.layout.justifyContent}`);
}
if (specs.text) {
lines.push(`${pad} Text: "${specs.text}"`);
}
// Format children
for (const child of specs.children) {
lines.push(formatSpecsSummary(child, indent + 1));
}
return lines.join('\n');
}