This is page 3 of 3. Use http://codebase.md/mhmzdev/figma-flutter-mcp?lines=false&page={x} to view the full context.
# Directory Structure
```
├── .changeset
│ └── config.json
├── .github
│ └── workflows
│ └── release.yml
├── .gitignore
├── CHANGELOG.md
├── CONTRIBUTING.md
├── docs
│ ├── cursor_rules_example.md
│ ├── figma-flutter-mcp.md
│ ├── figma-framework-mcp.md
│ ├── getting-started.md
│ └── images
│ ├── button.png
│ ├── figma-flutter-mcp.png
│ ├── screen.png
│ ├── svg.gif
│ ├── svgs_clean.gif
│ ├── text-style-frame.png
│ └── theme-frame.png
├── LICENSE.md
├── package.json
├── README.ja.md
├── README.ko.md
├── README.md
├── README.zh-cn.md
├── README.zh-tw.md
├── src
│ ├── cli.ts
│ ├── config.ts
│ ├── extractors
│ │ ├── colors
│ │ │ ├── core.ts
│ │ │ ├── extractor.ts
│ │ │ ├── index.ts
│ │ │ └── types.ts
│ │ ├── components
│ │ │ ├── core.ts
│ │ │ ├── deduplicated-extractor.ts
│ │ │ ├── extractor.ts
│ │ │ ├── index.ts
│ │ │ ├── types.ts
│ │ │ └── variant-analyzer.ts
│ │ ├── flutter
│ │ │ ├── global-vars.ts
│ │ │ ├── index.ts
│ │ │ ├── style-library.ts
│ │ │ └── style-merger.ts
│ │ ├── screens
│ │ │ ├── core.ts
│ │ │ ├── extractor.ts
│ │ │ ├── index.ts
│ │ │ └── types.ts
│ │ └── typography
│ │ ├── core.ts
│ │ ├── extractor.ts
│ │ ├── index.ts
│ │ └── types.ts
│ ├── figma-config.ts
│ ├── server.ts
│ ├── services
│ │ └── figma.ts
│ ├── tools
│ │ ├── flutter
│ │ │ ├── assets
│ │ │ │ ├── asset-manager.ts
│ │ │ │ ├── assets.ts
│ │ │ │ └── svg-assets.ts
│ │ │ ├── components
│ │ │ │ ├── component-tool.ts
│ │ │ │ ├── deduplicated-helpers.ts
│ │ │ │ └── helpers.ts
│ │ │ ├── index.ts
│ │ │ ├── screens
│ │ │ │ ├── helpers.ts
│ │ │ │ └── screen-tool.ts
│ │ │ ├── semantic-detection.ts
│ │ │ ├── theme
│ │ │ │ ├── colors
│ │ │ │ │ ├── theme-generator.ts
│ │ │ │ │ └── theme-tool.ts
│ │ │ │ └── typography
│ │ │ │ ├── typography-generator.ts
│ │ │ │ └── typography-tool.ts
│ │ │ └── visual-context.ts
│ │ └── index.ts
│ ├── types
│ │ ├── errors.ts
│ │ ├── figma.ts
│ │ └── flutter.ts
│ └── utils
│ ├── figma-url-parser.ts
│ ├── helpers.ts
│ ├── logger.ts
│ ├── retry.ts
│ └── validation.ts
└── tsconfig.json
```
# Files
--------------------------------------------------------------------------------
/src/extractors/screens/extractor.ts:
--------------------------------------------------------------------------------
```typescript
// src/extractors/screens/extractor.mts
import type {FigmaNode} from '../../types/figma.js';
import type {
ScreenAnalysis,
ScreenMetadata,
ScreenLayoutInfo,
ScreenSection,
NavigationInfo,
NavigationElement,
ScreenAssetInfo,
SkippedNodeInfo,
ScreenExtractionOptions
} from './types.js';
import type {ComponentChild, NestedComponentInfo} from '../components/types.js';
import {
extractLayoutInfo,
extractStylingInfo,
createComponentChild,
createNestedComponentInfo,
calculateVisualImportance,
isComponentNode
} from '../components/extractor.js';
import { detectSectionTypeAdvanced } from '../../tools/flutter/semantic-detection.js';
/**
* Extract screen metadata
*/
export function extractScreenMetadata(node: FigmaNode): ScreenMetadata {
const dimensions = {
width: node.absoluteBoundingBox?.width || 0,
height: node.absoluteBoundingBox?.height || 0
};
const deviceType = detectDeviceType(dimensions);
const orientation = detectOrientation(dimensions);
return {
name: node.name,
type: node.type as 'FRAME' | 'PAGE' | 'COMPONENT',
nodeId: node.id,
deviceType,
orientation,
dimensions
};
}
/**
* Extract screen layout information
*/
export function extractScreenLayoutInfo(node: FigmaNode): ScreenLayoutInfo {
const baseLayout = extractLayoutInfo(node);
return {
...baseLayout,
scrollable: detectScrollable(node),
hasHeader: detectHeader(node),
hasFooter: detectFooter(node),
hasNavigation: detectNavigation(node),
contentArea: calculateContentArea(node)
};
}
/**
* Analyze screen sections (header, content, footer, etc.)
*/
export function analyzeScreenSections(
node: FigmaNode,
options: Required<ScreenExtractionOptions>
): {
sections: ScreenSection[];
components: NestedComponentInfo[];
skippedNodes: SkippedNodeInfo[];
} {
const sections: ScreenSection[] = [];
const components: NestedComponentInfo[] = [];
const skippedNodes: SkippedNodeInfo[] = [];
if (!node.children || node.children.length === 0) {
return {sections, components, skippedNodes};
}
// Filter visible nodes unless includeHiddenNodes is true
let visibleChildren = node.children;
if (!options.includeHiddenNodes) {
visibleChildren = node.children.filter(child => child.visible !== false);
}
// Filter out device UI elements (status bars, notches, home indicators, etc.)
const filteredDeviceUI = visibleChildren.filter(child => isDeviceUIElement(child, node));
visibleChildren = visibleChildren.filter(child => !isDeviceUIElement(child, node));
// Add filtered device UI elements to skipped nodes for reporting
filteredDeviceUI.forEach(deviceUINode => {
skippedNodes.push({
nodeId: deviceUINode.id,
name: deviceUINode.name,
type: deviceUINode.type,
reason: 'device_ui_element'
});
});
// Analyze each child as potential section
const sectionsWithImportance = visibleChildren.map(child => {
const siblings = visibleChildren.filter(sibling => sibling.id !== child.id);
return {
node: child,
importance: calculateSectionImportance(child),
sectionType: detectSectionType(child, node, siblings)
};
});
// Sort by importance
sectionsWithImportance.sort((a, b) => b.importance - a.importance);
// Process up to maxSections
const processedCount = Math.min(sectionsWithImportance.length, options.maxSections);
for (let i = 0; i < sectionsWithImportance.length; i++) {
const {node: child, importance, sectionType} = sectionsWithImportance[i];
if (i < processedCount) {
const section = createScreenSection(child, sectionType, importance, options);
sections.push(section);
// Collect nested components from this section
section.components.forEach(comp => {
if (!components.find(c => c.nodeId === comp.nodeId)) {
components.push(comp);
}
});
} else {
skippedNodes.push({
nodeId: child.id,
name: child.name,
type: child.type,
reason: 'max_sections'
});
}
}
return {sections, components, skippedNodes};
}
/**
* Extract navigation information
*/
export function extractNavigationInfo(node: FigmaNode): NavigationInfo {
const navigationElements: NavigationElement[] = [];
// Traverse to find navigation elements
traverseForNavigation(node, navigationElements);
return {
hasTabBar: detectTabBar(node),
hasAppBar: detectAppBar(node),
hasDrawer: detectDrawer(node),
hasBottomSheet: detectBottomSheet(node),
navigationElements
};
}
/**
* Extract screen assets information
*/
export function extractScreenAssets(node: FigmaNode): ScreenAssetInfo[] {
const assets: ScreenAssetInfo[] = [];
traverseForAssets(node, assets);
return assets;
}
/**
* Create screen section
*/
function createScreenSection(
node: FigmaNode,
sectionType: ScreenSection['type'],
importance: number,
options: Required<ScreenExtractionOptions>
): ScreenSection {
const children: ComponentChild[] = [];
const components: NestedComponentInfo[] = [];
// Analyze section children
if (node.children) {
const visibleChildren = options.includeHiddenNodes
? node.children
: node.children.filter(child => child.visible !== false);
visibleChildren.forEach(child => {
const childImportance = calculateVisualImportance(child);
const isComponent = isComponentNode(child);
if (isComponent) {
components.push(createNestedComponentInfo(child));
}
// Pass parent and siblings for better semantic detection
const siblings = visibleChildren.filter(sibling => sibling.id !== child.id);
children.push(createComponentChild(child, childImportance, isComponent, {
maxChildNodes: 20, // Higher limit for screens
maxDepth: options.maxDepth,
includeHiddenNodes: options.includeHiddenNodes,
prioritizeComponents: true,
extractTextContent: true
}, node, siblings));
});
}
return {
id: `section_${node.id}`,
name: node.name,
type: sectionType,
nodeId: node.id,
layout: extractLayoutInfo(node),
styling: extractStylingInfo(node),
children,
components,
importance
};
}
/**
* Calculate section importance for prioritization
*/
function calculateSectionImportance(node: FigmaNode): number {
let score = 0;
// Size importance (0-3 points)
const area = (node.absoluteBoundingBox?.width || 0) * (node.absoluteBoundingBox?.height || 0);
if (area > 50000) score += 3;
else if (area > 20000) score += 2;
else if (area > 5000) score += 1;
// Position importance (0-2 points) - top elements are more important
const y = node.absoluteBoundingBox?.y || 0;
if (y < 100) score += 2; // Header area
else if (y < 200) score += 1;
// Type importance (0-3 points)
if (node.type === 'COMPONENT' || node.type === 'INSTANCE') score += 3;
else if (node.type === 'FRAME') score += 2;
// Name-based importance (0-2 points)
const name = node.name.toLowerCase();
if (name.includes('header') || name.includes('nav') || name.includes('footer')) score += 2;
else if (name.includes('content') || name.includes('main') || name.includes('body')) score += 1;
return Math.min(score, 10);
}
/**
* Detect section type based on node properties
* Enhanced with multi-factor analysis and confidence scoring
*/
function detectSectionType(node: FigmaNode, parent?: FigmaNode, siblings?: FigmaNode[]): ScreenSection['type'] {
// Try advanced detection first
try {
const classification = detectSectionTypeAdvanced(node, parent, siblings);
// Use advanced classification if confidence is high enough
if (classification.confidence >= 0.6) {
return classification.type as ScreenSection['type'];
}
// Log reasoning for debugging (in development)
if (process.env.NODE_ENV === 'development') {
console.debug(`Low confidence (${classification.confidence}) for section "${node.name}": ${classification.reasoning.join(', ')}`);
}
} catch (error) {
// Fall back to legacy detection if advanced detection fails
console.warn('Advanced section detection failed, using legacy method:', error);
}
// Legacy detection as fallback
return detectSectionTypeLegacy(node);
}
/**
* Legacy section type detection (fallback)
*/
function detectSectionTypeLegacy(node: FigmaNode): ScreenSection['type'] {
const name = node.name.toLowerCase();
const bounds = node.absoluteBoundingBox;
// Name-based detection
if (name.includes('header') || name.includes('app bar') || name.includes('top bar')) return 'header';
if (name.includes('footer') || name.includes('bottom') || name.includes('tab bar')) return 'footer';
if (name.includes('nav') || name.includes('menu') || name.includes('sidebar')) return 'navigation';
if (name.includes('modal') || name.includes('dialog') || name.includes('popup')) return 'modal';
if (name.includes('content') || name.includes('main') || name.includes('body')) return 'content';
// Position-based detection
if (bounds) {
const screenHeight = bounds.y + bounds.height;
// Top 15% of screen likely header
if (bounds.y < screenHeight * 0.15) return 'header';
// Bottom 15% of screen likely footer/navigation
if (bounds.y > screenHeight * 0.85) return 'footer';
// Side areas might be navigation
if (bounds.width < 100 && bounds.height > 200) return 'sidebar';
}
return 'content';
}
/**
* Detect device type based on dimensions
*/
function detectDeviceType(dimensions: {width: number; height: number}): ScreenMetadata['deviceType'] {
const {width, height} = dimensions;
const maxDimension = Math.max(width, height);
const minDimension = Math.min(width, height);
// Mobile devices (typical ranges)
if (maxDimension <= 900 && minDimension <= 500) return 'mobile';
// Tablet devices
if (maxDimension <= 1400 && minDimension <= 1000) return 'tablet';
// Desktop
if (maxDimension > 1400) return 'desktop';
return 'unknown';
}
/**
* Detect orientation
*/
function detectOrientation(dimensions: {width: number; height: number}): ScreenMetadata['orientation'] {
return dimensions.width > dimensions.height ? 'landscape' : 'portrait';
}
/**
* Detect if screen is scrollable
*/
function detectScrollable(node: FigmaNode): boolean {
// Check for scroll properties or overflow
const nodeAny = node as any;
return !!(nodeAny.overflowDirection || nodeAny.scrollBehavior);
}
/**
* Detect header presence
*/
function detectHeader(node: FigmaNode): boolean {
if (!node.children) return false;
return node.children.some(child => {
const name = child.name.toLowerCase();
const bounds = child.absoluteBoundingBox;
return (name.includes('header') || name.includes('app bar') || name.includes('top bar')) ||
(bounds && bounds.y < 150); // Top area
});
}
/**
* Detect footer presence
*/
function detectFooter(node: FigmaNode): boolean {
if (!node.children) return false;
const screenHeight = node.absoluteBoundingBox?.height || 0;
return node.children.some(child => {
const name = child.name.toLowerCase();
const bounds = child.absoluteBoundingBox;
return (name.includes('footer') || name.includes('bottom') || name.includes('tab bar')) ||
(bounds && bounds.y > screenHeight * 0.8); // Bottom area
});
}
/**
* Detect navigation presence
*/
function detectNavigation(node: FigmaNode): boolean {
if (!node.children) return false;
return node.children.some(child => {
const name = child.name.toLowerCase();
return name.includes('nav') || name.includes('menu') || name.includes('tab') || name.includes('drawer');
});
}
/**
* Calculate content area
*/
function calculateContentArea(node: FigmaNode): ScreenLayoutInfo['contentArea'] {
const bounds = node.absoluteBoundingBox;
if (!bounds) return undefined;
// Simple heuristic: assume content is the main area minus header/footer
return {
x: bounds.x,
y: bounds.y + 100, // Assume 100px header
width: bounds.width,
height: bounds.height - 200 // Assume 100px header + 100px footer
};
}
/**
* Detect tab bar
*/
function detectTabBar(node: FigmaNode): boolean {
return traverseAndCheck(node, child => {
const name = child.name.toLowerCase();
return !!(name.includes('tab bar') || name.includes('bottom nav') ||
(name.includes('tab') && child.children && child.children.length > 1));
});
}
/**
* Detect app bar
*/
function detectAppBar(node: FigmaNode): boolean {
return traverseAndCheck(node, child => {
const name = child.name.toLowerCase();
return !!(name.includes('app bar') || name.includes('header') || name.includes('top bar'));
});
}
/**
* Detect drawer
*/
function detectDrawer(node: FigmaNode): boolean {
return traverseAndCheck(node, child => {
const name = child.name.toLowerCase();
return !!(name.includes('drawer') || name.includes('sidebar') || name.includes('menu'));
});
}
/**
* Detect bottom sheet
*/
function detectBottomSheet(node: FigmaNode): boolean {
return traverseAndCheck(node, child => {
const name = child.name.toLowerCase();
return !!(name.includes('bottom sheet') || name.includes('modal') || name.includes('popup'));
});
}
/**
* Traverse and check condition
*/
function traverseAndCheck(node: FigmaNode, condition: (node: FigmaNode) => boolean): boolean {
if (condition(node)) return true;
if (node.children) {
return node.children.some(child => traverseAndCheck(child, condition));
}
return false;
}
/**
* Traverse for navigation elements
*/
function traverseForNavigation(node: FigmaNode, results: NavigationElement[], depth: number = 0): void {
if (depth > 3) return;
const name = node.name.toLowerCase();
// Check if this node is a navigation element
if (isNavigationElement(node)) {
results.push({
nodeId: node.id,
name: node.name,
type: detectNavigationElementType(node),
text: extractNavigationText(node),
icon: hasIcon(node),
isActive: detectActiveState(node)
});
}
// Traverse children
if (node.children) {
node.children.forEach(child => {
traverseForNavigation(child, results, depth + 1);
});
}
}
/**
* Check if node is navigation element
*/
function isNavigationElement(node: FigmaNode): boolean {
const name = node.name.toLowerCase();
return name.includes('tab') || name.includes('button') || name.includes('link') ||
name.includes('menu') || name.includes('nav') ||
(node.type === 'INSTANCE' && name.includes('item'));
}
/**
* Detect navigation element type
*/
function detectNavigationElementType(node: FigmaNode): NavigationElement['type'] {
const name = node.name.toLowerCase();
if (name.includes('tab')) return 'tab';
if (name.includes('button')) return 'button';
if (name.includes('link')) return 'link';
if (name.includes('icon')) return 'icon';
if (name.includes('menu')) return 'menu';
return 'other';
}
/**
* Extract navigation text
*/
function extractNavigationText(node: FigmaNode): string | undefined {
// Look for text children
if (node.children) {
for (const child of node.children) {
if (child.type === 'TEXT' && child.name) {
return child.name;
}
}
}
// Fallback to node name if it looks like text
const name = node.name;
if (name && !name.toLowerCase().includes('component') && !name.toLowerCase().includes('instance')) {
return name;
}
return undefined;
}
/**
* Check if node has icon
*/
function hasIcon(node: FigmaNode): boolean {
if (!node.children) return false;
return node.children.some(child => {
const name = child.name.toLowerCase();
return name.includes('icon') || child.type === 'VECTOR';
});
}
/**
* Detect active state
*/
function detectActiveState(node: FigmaNode): boolean {
const name = node.name.toLowerCase();
return name.includes('active') || name.includes('selected') || name.includes('current');
}
/**
* Check if a node represents device UI elements that should be ignored
*/
function isDeviceUIElement(child: FigmaNode, parent: FigmaNode): boolean {
const name = child.name.toLowerCase();
const bounds = child.absoluteBoundingBox;
const parentBounds = parent.absoluteBoundingBox;
if (!bounds || !parentBounds) return false;
// Name-based detection for common device UI elements
const deviceUIKeywords = [
'status bar', 'statusbar', 'status_bar',
'battery', 'signal', 'wifi', 'cellular', 'carrier',
'notch', 'dynamic island', 'safe area',
'home indicator', 'home_indicator', 'home bar',
'navigation bar', 'system bar', 'system_bar',
'chin', 'bezel', 'nav bar',
'clock', 'time indicator',
'signal strength', 'battery indicator',
'screen recording', 'screen_recording'
];
// Check if name contains device UI keywords
if (deviceUIKeywords.some(keyword => name.includes(keyword))) {
return true;
}
// Position and size-based detection
const screenWidth = parentBounds.width;
const screenHeight = parentBounds.height;
const elementWidth = bounds.width;
const elementHeight = bounds.height;
const elementY = bounds.y - parentBounds.y; // Relative position
const elementX = bounds.x - parentBounds.x;
// Status bar detection (top of screen)
if (elementY <= 50 && // Very close to top
elementWidth >= screenWidth * 0.8 && // Nearly full width
elementHeight <= 50) { // Thin height
// Additional checks for status bar content
if (hasStatusBarContent(child)) {
return true;
}
}
// Home indicator detection (bottom of screen)
if (elementY >= screenHeight - 50 && // Very close to bottom
elementWidth <= screenWidth * 0.4 && // Narrow width (typical home indicator)
elementHeight <= 20 && // Very thin
elementX >= screenWidth * 0.3 && // Centered horizontally
elementX <= screenWidth * 0.7) {
return true;
}
// Notch/Dynamic Island detection (top center, small area)
if (elementY <= 30 && // Very top
elementWidth <= screenWidth * 0.3 && // Small width
elementHeight <= 30 && // Small height
elementX >= screenWidth * 0.35 && // Centered area
elementX <= screenWidth * 0.65) {
return true;
}
// Side bezels or navigation areas
if ((elementX <= 20 || elementX >= screenWidth - 20) && // At edges
elementWidth <= 40 && // Thin
elementHeight >= screenHeight * 0.3) { // Tall
return true;
}
// Small elements in corners (likely UI indicators)
const isInCorner = (elementX <= 50 || elementX >= screenWidth - 50) &&
(elementY <= 50 || elementY >= screenHeight - 50);
if (isInCorner && elementWidth <= 80 && elementHeight <= 80) {
return true;
}
return false;
}
/**
* Check if a node contains typical status bar content
*/
function hasStatusBarContent(node: FigmaNode): boolean {
if (!node.children) return false;
const statusBarElements = node.children.some(child => {
const name = child.name.toLowerCase();
return name.includes('battery') ||
name.includes('signal') ||
name.includes('wifi') ||
name.includes('time') ||
name.includes('clock') ||
name.includes('carrier') ||
name.includes('cellular') ||
(child.type === 'TEXT' && /^\d{1,2}:\d{2}/.test(child.name)); // Time format
});
return statusBarElements;
}
/**
* Traverse for assets
*/
function traverseForAssets(node: FigmaNode, results: ScreenAssetInfo[], depth: number = 0): void {
if (depth > 4) return;
// Check if this node is an asset
if (isAssetNode(node)) {
results.push({
nodeId: node.id,
name: node.name,
type: detectAssetType(node),
size: detectAssetSize(node),
usage: detectAssetUsage(node)
});
}
// Traverse children
if (node.children) {
node.children.forEach(child => {
traverseForAssets(child, results, depth + 1);
});
}
}
/**
* Check if node is an asset
*/
function isAssetNode(node: FigmaNode): boolean {
// Check for image fills
if (node.fills && node.fills.some((fill: any) => fill.type === 'IMAGE')) return true;
// Check for vectors that are likely assets
if (node.type === 'VECTOR') {
const name = node.name.toLowerCase();
return name.includes('image') || name.includes('illustration') ||
name.includes('icon') || name.includes('logo');
}
return false;
}
/**
* Detect asset type
*/
function detectAssetType(node: FigmaNode): ScreenAssetInfo['type'] {
const name = node.name.toLowerCase();
if (name.includes('icon')) return 'icon';
if (name.includes('illustration') || name.includes('graphic')) return 'illustration';
if (name.includes('background') || name.includes('bg')) return 'background';
return 'image';
}
/**
* Detect asset size
*/
function detectAssetSize(node: FigmaNode): ScreenAssetInfo['size'] {
const bounds = node.absoluteBoundingBox;
if (!bounds) return 'medium';
const area = bounds.width * bounds.height;
if (area < 2500) return 'small'; // < 50x50
if (area > 40000) return 'large'; // > 200x200
return 'medium';
}
/**
* Detect asset usage
*/
function detectAssetUsage(node: FigmaNode): ScreenAssetInfo['usage'] {
const name = node.name.toLowerCase();
if (name.includes('logo') || name.includes('brand')) return 'branding';
if (name.includes('nav') || name.includes('menu') || name.includes('tab')) return 'navigation';
if (name.includes('background') || name.includes('decoration')) return 'decorative';
return 'content';
}
```
--------------------------------------------------------------------------------
/src/extractors/components/extractor.ts:
--------------------------------------------------------------------------------
```typescript
// src/extractors/components/extractor.mts
import type {FigmaNode, FigmaColor, FigmaEffect} from '../../types/figma.js';
import type {
ComponentMetadata,
LayoutInfo,
StylingInfo,
ComponentChild,
NestedComponentInfo,
SkippedNodeInfo,
CategorizedEffects,
ColorInfo,
StrokeInfo,
CornerRadii,
PaddingInfo,
TextInfo,
ComponentExtractionOptions
} from './types.js';
import { detectSemanticTypeAdvanced, generateSemanticContext } from '../../tools/flutter/semantic-detection.js';
/**
* Extract component metadata
*/
export function extractMetadata(node: FigmaNode, userDefinedAsComponent: boolean): ComponentMetadata {
const metadata: ComponentMetadata = {
name: node.name,
type: node.type as 'COMPONENT' | 'COMPONENT_SET' | 'FRAME',
nodeId: node.id,
isUserDefinedComponent: userDefinedAsComponent
};
// Add component-specific metadata
if (node.type === 'COMPONENT' || node.type === 'COMPONENT_SET') {
metadata.componentKey = (node as any).componentKey;
}
if (node.type === 'COMPONENT_SET') {
metadata.variantCount = node.children?.length || 0;
}
return metadata;
}
/**
* Extract layout information
*/
export function extractLayoutInfo(node: FigmaNode): LayoutInfo {
const layout: LayoutInfo = {
type: determineLayoutType(node),
dimensions: {
width: node.absoluteBoundingBox?.width || 0,
height: node.absoluteBoundingBox?.height || 0
}
};
// Auto-layout specific properties
if (node.layoutMode) {
layout.direction = node.layoutMode === 'HORIZONTAL' ? 'horizontal' : 'vertical';
layout.spacing = node.itemSpacing || 0;
// Extract padding
if (hasPadding(node)) {
layout.padding = extractPadding(node);
}
// Alignment properties
layout.alignItems = (node as any).primaryAxisAlignItems;
layout.justifyContent = (node as any).counterAxisAlignItems;
}
// Constraints
if (node.constraints) {
layout.constraints = node.constraints;
}
return layout;
}
/**
* Extract styling information
*/
export function extractStylingInfo(node: FigmaNode): StylingInfo {
const styling: StylingInfo = {};
// Fills (background colors/gradients)
if (node.fills && node.fills.length > 0) {
styling.fills = node.fills.map(convertFillToColorInfo);
}
// Strokes (borders)
if (node.strokes && node.strokes.length > 0) {
styling.strokes = node.strokes.map(convertStrokeInfo);
}
// Effects (shadows, blurs)
if (node.effects && node.effects.length > 0) {
styling.effects = categorizeEffects(node.effects);
}
// Corner radius
const cornerRadius = extractCornerRadius(node);
if (cornerRadius) {
styling.cornerRadius = cornerRadius;
}
// Opacity
if ((node as any).opacity !== undefined && (node as any).opacity !== 1) {
styling.opacity = (node as any).opacity;
}
return styling;
}
/**
* Analyze child nodes with prioritization and limits
*/
export function analyzeChildren(
node: FigmaNode,
options: Required<ComponentExtractionOptions>
): {
children: ComponentChild[];
nestedComponents: NestedComponentInfo[];
skippedNodes: SkippedNodeInfo[];
} {
const children: ComponentChild[] = [];
const nestedComponents: NestedComponentInfo[] = [];
const skippedNodes: SkippedNodeInfo[] = [];
if (!node.children || node.children.length === 0) {
return {children, nestedComponents, skippedNodes};
}
// Filter visible nodes unless includeHiddenNodes is true
let visibleChildren = node.children;
if (!options.includeHiddenNodes) {
visibleChildren = node.children.filter(child => child.visible !== false);
}
// Calculate visual importance for all children
const childrenWithImportance = visibleChildren.map(child => ({
node: child,
importance: calculateVisualImportance(child),
isComponent: isComponentNode(child)
}));
// Sort by importance (components first if prioritized, then by visual importance)
childrenWithImportance.sort((a, b) => {
if (options.prioritizeComponents) {
if (a.isComponent && !b.isComponent) return -1;
if (!a.isComponent && b.isComponent) return 1;
}
return b.importance - a.importance;
});
// Process up to maxChildNodes
const processedCount = Math.min(childrenWithImportance.length, options.maxChildNodes);
for (let i = 0; i < childrenWithImportance.length; i++) {
const {node: child, importance, isComponent} = childrenWithImportance[i];
if (i < processedCount) {
// Check if this is a nested component
if (isComponent) {
nestedComponents.push(createNestedComponentInfo(child));
}
// Pass parent and siblings for better semantic detection
const siblings = visibleChildren.filter(sibling => sibling.id !== child.id);
children.push(createComponentChild(child, importance, isComponent, options, node, siblings));
} else {
// Track skipped nodes
skippedNodes.push({
nodeId: child.id,
name: child.name,
type: child.type,
reason: 'max_nodes'
});
}
}
return {children, nestedComponents, skippedNodes};
}
/**
* Create nested component information
*/
export function createNestedComponentInfo(node: FigmaNode): NestedComponentInfo {
return {
nodeId: node.id,
name: node.name,
componentKey: (node as any).componentKey,
masterComponent: (node as any).masterComponent?.key,
isComponentInstance: node.type === 'INSTANCE',
needsSeparateAnalysis: true,
instanceType: node.type === 'INSTANCE' ? 'COMPONENT' : node.type as 'COMPONENT' | 'COMPONENT_SET'
};
}
/**
* Create component child information
*/
export function createComponentChild(
node: FigmaNode,
importance: number,
isNestedComponent: boolean,
options: Required<ComponentExtractionOptions>,
parent?: FigmaNode,
siblings?: FigmaNode[]
): ComponentChild {
const child: ComponentChild = {
nodeId: node.id,
name: node.name,
type: node.type,
isNestedComponent,
visualImportance: importance
};
// Extract basic info for non-component children
if (!isNestedComponent) {
child.basicInfo = {
layout: extractBasicLayout(node),
styling: extractBasicStyling(node)
};
// Extract text info for text nodes
if (node.type === 'TEXT' && options.extractTextContent) {
child.basicInfo.text = extractTextInfo(node, parent, siblings);
}
}
return child;
}
/**
* Calculate visual importance score (1-10)
*/
export function calculateVisualImportance(node: FigmaNode): number {
let score = 0;
// Size importance (0-4 points)
const area = (node.absoluteBoundingBox?.width || 0) * (node.absoluteBoundingBox?.height || 0);
if (area > 10000) score += 4;
else if (area > 5000) score += 3;
else if (area > 1000) score += 2;
else if (area > 100) score += 1;
// Type importance (0-3 points)
if (node.type === 'COMPONENT' || node.type === 'INSTANCE') score += 3;
else if (node.type === 'FRAME') score += 2;
else if (node.type === 'TEXT') score += 2;
else if (node.type === 'VECTOR') score += 1;
// Styling importance (0-2 points)
if (node.fills && node.fills.length > 0) score += 1;
if (node.effects && node.effects.length > 0) score += 1;
// Has children importance (0-1 point)
if (node.children && node.children.length > 0) score += 1;
return Math.min(score, 10);
}
/**
* Check if node is a component
*/
export function isComponentNode(node: FigmaNode): boolean {
return node.type === 'COMPONENT' || node.type === 'INSTANCE' || node.type === 'COMPONENT_SET';
}
/**
* Determine layout type from node properties
*/
export function determineLayoutType(node: FigmaNode): 'auto-layout' | 'absolute' | 'frame' {
if (node.layoutMode) {
return 'auto-layout';
}
if (node.type === 'FRAME' || node.type === 'COMPONENT') {
return 'frame';
}
return 'absolute';
}
/**
* Check if node has padding
*/
export function hasPadding(node: FigmaNode): boolean {
return !!(node.paddingTop || node.paddingRight || node.paddingBottom || node.paddingLeft);
}
/**
* Extract padding information
*/
export function extractPadding(node: FigmaNode): PaddingInfo {
const top = node.paddingTop || 0;
const right = node.paddingRight || 0;
const bottom = node.paddingBottom || 0;
const left = node.paddingLeft || 0;
return {
top,
right,
bottom,
left,
isUniform: top === right && right === bottom && bottom === left
};
}
/**
* Convert fill to color info
*/
export function convertFillToColorInfo(fill: any): ColorInfo {
const colorInfo: ColorInfo = {
type: fill.type,
opacity: fill.opacity
};
if (fill.color) {
colorInfo.color = fill.color;
colorInfo.hex = rgbaToHex(fill.color);
}
if (fill.gradientStops) {
colorInfo.gradientStops = fill.gradientStops;
}
return colorInfo;
}
/**
* Convert stroke to stroke info
*/
export function convertStrokeInfo(stroke: any): StrokeInfo {
return {
type: stroke.type,
color: stroke.color,
hex: rgbaToHex(stroke.color),
weight: (stroke as any).strokeWeight || 1,
align: (stroke as any).strokeAlign
};
}
/**
* Categorize effects for Flutter mapping
*/
export function categorizeEffects(effects: FigmaEffect[]): CategorizedEffects {
const categorized: CategorizedEffects = {
dropShadows: [],
innerShadows: [],
blurs: []
};
effects.forEach(effect => {
if (effect.type === 'DROP_SHADOW' && effect.visible !== false) {
categorized.dropShadows.push({
color: effect.color!,
hex: rgbaToHex(effect.color!),
offset: effect.offset || {x: 0, y: 0},
radius: effect.radius,
spread: effect.spread,
opacity: effect.color?.a || 1
});
} else if (effect.type === 'INNER_SHADOW' && effect.visible !== false) {
categorized.innerShadows.push({
color: effect.color!,
hex: rgbaToHex(effect.color!),
offset: effect.offset || {x: 0, y: 0},
radius: effect.radius,
spread: effect.spread,
opacity: effect.color?.a || 1
});
} else if ((effect.type === 'LAYER_BLUR' || effect.type === 'BACKGROUND_BLUR') && effect.visible !== false) {
categorized.blurs.push({
type: effect.type,
radius: effect.radius
});
}
});
return categorized;
}
/**
* Extract corner radius
*/
export function extractCornerRadius(node: FigmaNode): number | CornerRadii | undefined {
const nodeAny = node as any;
if (nodeAny.cornerRadius !== undefined) {
return nodeAny.cornerRadius;
}
// Check for individual corner radii
if (nodeAny.rectangleCornerRadii && Array.isArray(nodeAny.rectangleCornerRadii)) {
const [topLeft, topRight, bottomRight, bottomLeft] = nodeAny.rectangleCornerRadii;
const isUniform = topLeft === topRight && topRight === bottomRight && bottomRight === bottomLeft;
if (isUniform) {
return topLeft;
}
return {
topLeft,
topRight,
bottomLeft,
bottomRight,
isUniform: false
};
}
return undefined;
}
/**
* Extract basic layout info for non-component children
*/
export function extractBasicLayout(node: FigmaNode): Partial<LayoutInfo> {
return {
type: determineLayoutType(node),
dimensions: {
width: node.absoluteBoundingBox?.width || 0,
height: node.absoluteBoundingBox?.height || 0
}
};
}
/**
* Extract basic styling info for non-component children
*/
export function extractBasicStyling(node: FigmaNode): Partial<StylingInfo> {
const styling: Partial<StylingInfo> = {};
if (node.fills && node.fills.length > 0) {
styling.fills = node.fills.slice(0, 1).map(convertFillToColorInfo); // Limit to primary fill
}
const cornerRadius = extractCornerRadius(node);
if (cornerRadius) {
styling.cornerRadius = cornerRadius;
}
return styling;
}
/**
* Extract enhanced text information
*/
export function extractTextInfo(node: FigmaNode, parent?: FigmaNode, siblings?: FigmaNode[]): TextInfo | undefined {
if (node.type !== 'TEXT') return undefined;
const textContent = getActualTextContent(node);
const isPlaceholder = isPlaceholderText(textContent);
return {
content: textContent,
isPlaceholder,
fontFamily: node.style?.fontFamily,
fontSize: node.style?.fontSize,
fontWeight: node.style?.fontWeight,
textAlign: node.style?.textAlignHorizontal,
textCase: detectTextCase(textContent),
semanticType: detectSemanticType(textContent, node.name, node, parent, siblings),
placeholder: isPlaceholder
};
}
/**
* Get actual text content from various sources
*/
function getActualTextContent(node: FigmaNode): string {
// 1. Primary source: characters property (official Figma API text content)
if (node.characters && node.characters.trim().length > 0) {
return node.characters.trim();
}
// 2. Check fills for text content (sometimes stored in fill metadata)
if (node.fills) {
for (const fill of node.fills) {
if ((fill as any).textData || (fill as any).content) {
const textContent = (fill as any).textData || (fill as any).content;
if (textContent && textContent.trim().length > 0) {
return textContent.trim();
}
}
}
}
// 3. Check for text in component properties (for component instances)
if (node.type === 'INSTANCE' && (node as any).componentProperties) {
const textProps = extractTextFromComponentProperties((node as any).componentProperties);
if (textProps && textProps.trim().length > 0) {
return textProps.trim();
}
}
// 4. Analyze node name for meaningful content
const nodeName = node.name;
// If node name looks like actual content (not generic), use it
if (isLikelyActualContent(nodeName)) {
return nodeName;
}
// 5. Fallback to node name with placeholder flag
return nodeName;
}
/**
* Check if node name looks like actual content vs generic label
*/
function isLikelyActualContent(name: string): boolean {
const genericPatterns = [
/^text$/i,
/^label$/i,
/^heading$/i,
/^title$/i,
/^body\s*\d*$/i,
/^text\s*\d+$/i,
/^heading\s*\d+$/i,
/^h\d+$/i,
/^lorem\s+ipsum/i,
/^sample\s+text/i,
/^placeholder/i,
/^example\s+text/i,
/^demo\s+text/i,
/^text\s*layer/i,
/^component\s*\d+/i
];
// If it matches generic patterns, it's probably not actual content
if (genericPatterns.some(pattern => pattern.test(name))) {
return false;
}
// If it's very short and common UI text, it might be actual content
const shortUIText = ['ok', 'yes', 'no', 'save', 'cancel', 'close', 'menu', 'home', 'back', 'next', 'login', 'signup'];
if (name.length <= 8 && shortUIText.includes(name.toLowerCase())) {
return true;
}
// If it contains real words and is reasonably long, likely actual content
if (name.length > 3 && name.length < 100) {
// Check if it has word-like structure
const hasWords = /\b[a-zA-Z]{2,}\b/.test(name);
const hasSpaces = name.includes(' ');
if (hasWords && (hasSpaces || name.length > 8)) {
return true;
}
}
return false;
}
/**
* Check if text content is placeholder/dummy text
*/
function isPlaceholderText(content: string): boolean {
if (!content || content.trim().length === 0) {
return true;
}
const trimmedContent = content.trim().toLowerCase();
// Common placeholder patterns
const placeholderPatterns = [
/lorem\s+ipsum/i,
/dolor\s+sit\s+amet/i,
/consectetur\s+adipiscing/i,
/the\s+quick\s+brown\s+fox/i,
/sample\s+text/i,
/placeholder/i,
/example\s+text/i,
/demo\s+text/i,
/test\s+content/i,
/dummy\s+text/i,
/text\s+goes\s+here/i,
/your\s+text\s+here/i,
/add\s+text\s+here/i,
/enter\s+text/i,
/\[.*\]/, // Text in brackets like [Your text here]
/^heading\s*\d*$/i,
/^title\s*\d*$/i,
/^body\s*\d*$/i,
/^text\s*\d*$/i,
/^label\s*\d*$/i,
/^h[1-6]$/i,
/^paragraph$/i,
/^caption$/i,
/^subtitle$/i,
/^overline$/i
];
// Check against placeholder patterns
if (placeholderPatterns.some(pattern => pattern.test(content))) {
return true;
}
// Check for generic single words that are likely placeholders
const genericWords = [
'text', 'label', 'title', 'heading', 'body', 'content',
'description', 'subtitle', 'caption', 'paragraph', 'copy'
];
if (genericWords.includes(trimmedContent)) {
return true;
}
// Check for repeated characters (like "AAAA" or "xxxx")
if (content.length > 2 && /^(.)\1+$/.test(content.trim())) {
return true;
}
// Check for Lorem Ipsum variations
if (/lorem|ipsum|dolor|sit|amet|consectetur|adipiscing|elit/i.test(content)) {
return true;
}
return false;
}
/**
* Extract text from component properties
*/
function extractTextFromComponentProperties(properties: any): string | null {
if (!properties || typeof properties !== 'object') {
return null;
}
// Look for common text property names
const textPropertyNames = ['text', 'label', 'title', 'content', 'value', 'caption'];
for (const propName of textPropertyNames) {
if (properties[propName] && typeof properties[propName] === 'string') {
return properties[propName];
}
}
// Look for any string property that might contain text
for (const [key, value] of Object.entries(properties)) {
if (typeof value === 'string' && value.length > 0 && !isPlaceholderText(value)) {
return value;
}
}
return null;
}
/**
* Detect text case pattern
*/
function detectTextCase(content: string): 'uppercase' | 'lowercase' | 'capitalize' | 'sentence' | 'mixed' {
if (content.length === 0) return 'mixed';
const isAllUpper = content === content.toUpperCase() && content !== content.toLowerCase();
const isAllLower = content === content.toLowerCase() && content !== content.toUpperCase();
if (isAllUpper) return 'uppercase';
if (isAllLower) return 'lowercase';
// Check if it's title case (first letter of each word capitalized)
const words = content.split(/\s+/);
const isTitleCase = words.every(word => {
return word.length === 0 || word[0] === word[0].toUpperCase();
});
if (isTitleCase) return 'capitalize';
// Check if it's sentence case (first letter capitalized, rest normal)
if (content[0] === content[0].toUpperCase()) {
return 'sentence';
}
return 'mixed';
}
/**
* Detect semantic type of text based on content and context
* Enhanced with multi-factor analysis and confidence scoring
*/
function detectSemanticType(
content: string,
nodeName: string,
node?: any,
parent?: any,
siblings?: any[]
): 'heading' | 'body' | 'label' | 'button' | 'link' | 'caption' | 'error' | 'success' | 'warning' | 'other' {
// Skip detection for placeholder text
if (isPlaceholderText(content)) {
return 'other';
}
// Use advanced semantic detection if node properties are available
if (node) {
try {
const context = generateSemanticContext(node, parent, siblings);
const classification = detectSemanticTypeAdvanced(content, nodeName, context, node);
// Only use advanced classification if confidence is high enough
if (classification.confidence >= 0.6) {
return classification.type;
}
// Log reasoning for debugging (in development)
if (process.env.NODE_ENV === 'development') {
console.debug(`Low confidence (${classification.confidence}) for "${content}": ${classification.reasoning.join(', ')}`);
}
} catch (error) {
// Fall back to legacy detection if advanced detection fails
console.warn('Advanced semantic detection failed, using legacy method:', error);
}
}
// Legacy detection as fallback
return detectSemanticTypeLegacy(content, nodeName);
}
/**
* Legacy semantic type detection (fallback)
*/
function detectSemanticTypeLegacy(content: string, nodeName: string): 'heading' | 'body' | 'label' | 'button' | 'link' | 'caption' | 'error' | 'success' | 'warning' | 'other' {
const lowerContent = content.toLowerCase().trim();
const lowerNodeName = nodeName.toLowerCase();
// Button text patterns - exact matches for common button labels
const buttonPatterns = [
/^(click|tap|press|submit|send|save|cancel|ok|yes|no|continue|next|back|close|done|finish|start|begin)$/i,
/^(login|log in|sign in|signup|sign up|register|logout|log out|sign out)$/i,
/^(buy|purchase|add|remove|delete|edit|update|create|new|get started|learn more|try now)$/i,
/^(download|upload|share|copy|paste|cut|undo|redo|refresh|reload|search|filter|sort|apply|reset|clear)$/i,
/^(accept|decline|agree|disagree|confirm|verify|validate|approve|reject)$/i
];
if (buttonPatterns.some(pattern => pattern.test(content)) || lowerNodeName.includes('button')) {
return 'button';
}
// Error/status text patterns - more comprehensive detection
const errorPatterns = /error|invalid|required|missing|failed|wrong|incorrect|forbidden|unauthorized|not found|unavailable|expired|timeout/i;
const successPatterns = /success|completed|done|saved|updated|created|uploaded|downloaded|sent|delivered|confirmed|verified|approved/i;
const warningPatterns = /warning|caution|note|important|attention|notice|alert|reminder|tip|info|information/i;
if (errorPatterns.test(content)) {
return 'error';
}
if (successPatterns.test(content)) {
return 'success';
}
if (warningPatterns.test(content)) {
return 'warning';
}
// Link patterns - navigation and informational links
const linkPatterns = [
/^(learn more|read more|see more|view all|show all|details|click here|view details)$/i,
/^(about|contact|help|support|faq|terms|privacy|policy|documentation|docs|guide|tutorial)$/i,
/^(home|dashboard|profile|settings|preferences|account|billing|notifications|security)$/i
];
if (linkPatterns.some(pattern => pattern.test(content)) || lowerNodeName.includes('link')) {
return 'link';
}
// Heading patterns - based on structure and context
if (content.length < 80 && !content.endsWith('.') && !content.includes('\n')) {
if (lowerNodeName.includes('heading') || lowerNodeName.includes('title') || /h[1-6]/.test(lowerNodeName)) {
return 'heading';
}
// Check if it looks like a title (short, starts with capital, no sentence punctuation)
if (content.length < 50 && /^[A-Z]/.test(content) && !/[.!?]$/.test(content)) {
return 'heading';
}
}
// Label patterns - form labels and descriptive text
if (content.length < 40 && (content.endsWith(':') || lowerNodeName.includes('label') || lowerNodeName.includes('field'))) {
return 'label';
}
// Caption patterns - short descriptive text
if (content.length < 120 && (
lowerNodeName.includes('caption') ||
lowerNodeName.includes('subtitle') ||
lowerNodeName.includes('description') ||
lowerNodeName.includes('meta')
)) {
return 'caption';
}
// Body text - longer content, paragraphs
if (content.length > 80 || content.includes('\n') || content.includes('. ')) {
return 'body';
}
return 'other';
}
/**
* Generate Flutter widget suggestion based on semantic type and text info
*/
export function generateFlutterTextWidget(textInfo: TextInfo): string {
// Escape single quotes in content for Dart strings
const escapedContent = textInfo.content.replace(/'/g, "\\'");
if (textInfo.isPlaceholder) {
return `Text('${escapedContent}') // TODO: Replace with actual content`;
}
// Generate style properties based on text info
const styleProps: string[] = [];
if (textInfo.fontFamily) {
styleProps.push(`fontFamily: '${textInfo.fontFamily}'`);
}
if (textInfo.fontSize) {
styleProps.push(`fontSize: ${textInfo.fontSize}`);
}
if (textInfo.fontWeight && textInfo.fontWeight !== 400) {
const fontWeight = textInfo.fontWeight >= 700 ? 'FontWeight.bold' :
textInfo.fontWeight >= 600 ? 'FontWeight.w600' :
textInfo.fontWeight >= 500 ? 'FontWeight.w500' :
textInfo.fontWeight <= 300 ? 'FontWeight.w300' : 'FontWeight.normal';
styleProps.push(`fontWeight: ${fontWeight}`);
}
const customStyle = styleProps.length > 0 ? `TextStyle(${styleProps.join(', ')})` : null;
switch (textInfo.semanticType) {
case 'button':
return `ElevatedButton(\n onPressed: () {\n // TODO: Implement button action\n },\n child: Text('${escapedContent}'),\n)`;
case 'link':
return `TextButton(\n onPressed: () {\n // TODO: Implement navigation\n },\n child: Text('${escapedContent}'),\n)`;
case 'heading':
const headingStyle = customStyle || 'Theme.of(context).textTheme.headlineMedium';
return `Text(\n '${escapedContent}',\n style: ${headingStyle},\n)`;
case 'body':
const bodyStyle = customStyle || 'Theme.of(context).textTheme.bodyMedium';
return `Text(\n '${escapedContent}',\n style: ${bodyStyle},\n)`;
case 'caption':
const captionStyle = customStyle || 'Theme.of(context).textTheme.bodySmall';
return `Text(\n '${escapedContent}',\n style: ${captionStyle},\n)`;
case 'label':
const labelStyle = customStyle || 'Theme.of(context).textTheme.labelMedium';
return `Text(\n '${escapedContent}',\n style: ${labelStyle},\n)`;
case 'error':
const errorStyle = customStyle ?
`${customStyle.slice(0, -1)}, color: Theme.of(context).colorScheme.error)` :
'TextStyle(color: Theme.of(context).colorScheme.error)';
return `Text(\n '${escapedContent}',\n style: ${errorStyle},\n)`;
case 'success':
const successStyle = customStyle ?
`${customStyle.slice(0, -1)}, color: Colors.green)` :
'TextStyle(color: Colors.green)';
return `Text(\n '${escapedContent}',\n style: ${successStyle},\n)`;
case 'warning':
const warningStyle = customStyle ?
`${customStyle.slice(0, -1)}, color: Colors.orange)` :
'TextStyle(color: Colors.orange)';
return `Text(\n '${escapedContent}',\n style: ${warningStyle},\n)`;
default:
return customStyle ?
`Text(\n '${escapedContent}',\n style: ${customStyle},\n)` :
`Text('${escapedContent}')`;
}
}
/**
* Get all text content from a component tree
*/
export function extractAllTextContent(node: FigmaNode): Array<{nodeId: string, textInfo: TextInfo, widgetSuggestion: string}> {
const textNodes: Array<{nodeId: string, textInfo: TextInfo, widgetSuggestion: string}> = [];
traverseForText(node, textNodes);
return textNodes;
}
/**
* Recursively traverse node tree to find all text nodes
*/
function traverseForText(
node: FigmaNode,
results: Array<{nodeId: string, textInfo: TextInfo, widgetSuggestion: string}>,
depth: number = 0
): void {
if (depth > 5) return; // Prevent infinite recursion
if (node.type === 'TEXT') {
const textInfo = extractTextInfo(node);
if (textInfo) {
results.push({
nodeId: node.id,
textInfo,
widgetSuggestion: generateFlutterTextWidget(textInfo)
});
}
}
if (node.children) {
node.children.forEach(child => {
traverseForText(child, results, depth + 1);
});
}
}
/**
* Convert RGBA color to hex string
*/
export function rgbaToHex(color: FigmaColor): string {
const r = Math.round(color.r * 255);
const g = Math.round(color.g * 255);
const b = Math.round(color.b * 255);
return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`.toUpperCase();
}
```
--------------------------------------------------------------------------------
/src/tools/flutter/components/component-tool.ts:
--------------------------------------------------------------------------------
```typescript
// src/tools/flutter/component/component-tool.mts
import {z} from "zod";
import type {McpServer} from "@modelcontextprotocol/sdk/server/mcp.js";
import {FigmaService} from "../../../services/figma.js";
import {
ComponentExtractor,
VariantAnalyzer,
parseComponentInput,
type ComponentAnalysis,
type ComponentVariant,
DeduplicatedComponentExtractor,
type DeduplicatedComponentAnalysis
} from "../../../extractors/components/index.js";
import {FlutterStyleLibrary, OptimizationReport} from "../../../extractors/flutter/style-library.js";
import {Logger} from "../../../utils/logger.js";
import {
generateVariantSelectionPrompt,
generateComponentAnalysisReport,
generateStructureInspectionReport
} from "./helpers.js";
import {
generateFlutterImplementation,
generateComprehensiveDeduplicatedReport,
generateStyleLibraryReport,
addVisualContextToDeduplicatedReport
} from "./deduplicated-helpers.js";
import {
createAssetsDirectory,
generateAssetFilename,
downloadImage,
getFileStats,
updatePubspecAssets,
generateAssetConstants,
groupAssetsByBaseName,
type AssetInfo
} from "../assets/asset-manager.js";
import {join} from 'path';
export function registerComponentTools(server: McpServer, figmaApiKey: string) {
// Main component analysis tool
server.registerTool(
"analyze_figma_component",
{
title: "Analyze Figma Component",
description: "Analyze a Figma component or component set to extract layout, styling, and structure information for Flutter widget creation. Use analyze_full_screen for complete screen layouts.",
inputSchema: {
input: z.string().describe("Figma component URL or file ID"),
nodeId: z.string().optional().describe("Node ID (if providing file ID separately)"),
userDefinedComponent: z.boolean().optional().describe("Treat a FRAME as a component (when designer hasn't converted to actual component yet) (default: false)"),
maxChildNodes: z.number().optional().describe("Maximum child nodes to analyze (default: 10)"),
includeVariants: z.boolean().optional().describe("Include variant analysis for component sets (default: true)"),
variantSelection: z.array(z.string()).optional().describe("Specific variant names to analyze (if >3 variants)"),
projectPath: z.string().optional().describe("Path to Flutter project for asset export (defaults to current directory)"),
exportAssets: z.boolean().optional().describe("Automatically export image assets found in component (default: true)"),
useDeduplication: z.boolean().optional().describe("Use style deduplication for token efficiency (default: true)"),
generateFlutterCode: z.boolean().optional().describe("Generate full Flutter implementation code (default: false)"),
resetStyleLibrary: z.boolean().optional().describe("Reset style library before analysis (default: false)"),
autoOptimize: z.boolean().optional().describe("Auto-optimize style library during analysis (default: true)")
}
},
async ({input, nodeId, userDefinedComponent = false, maxChildNodes = 10, includeVariants = true, variantSelection, projectPath = process.cwd(), exportAssets = true, useDeduplication = true, generateFlutterCode = false, resetStyleLibrary = false, autoOptimize = true}) => {
const token = figmaApiKey;
if (!token) {
return {
content: [{
type: "text",
text: "Error: Figma access token not configured. Please set FIGMA_API_KEY environment variable."
}]
};
}
try {
// Reset style library if requested
const styleLibrary = FlutterStyleLibrary.getInstance();
if (resetStyleLibrary) {
styleLibrary.reset();
}
// Configure auto-optimization
styleLibrary.setAutoOptimization(autoOptimize);
Logger.info(`🎯 Component Analysis Started:`, {
input: input.substring(0, 50) + '...',
nodeId,
useDeduplication,
autoOptimize,
resetStyleLibrary
});
// Parse input to get file ID and node ID
const parsedInput = parseComponentInput(input, nodeId);
if (!parsedInput.isValid) {
return {
content: [{
type: "text",
text: `Error parsing input: ${parsedInput.error || 'Invalid input format'}`
}]
};
}
const figmaService = new FigmaService(token);
// Get the component node
const componentNode = await figmaService.getNode(parsedInput.fileId, parsedInput.nodeId);
if (!componentNode) {
return {
content: [{
type: "text",
text: `Component with node ID "${parsedInput.nodeId}" not found in file.`
}]
};
}
// Validate that this is a component or user-defined component
const isActualComponent = componentNode.type === 'COMPONENT' || componentNode.type === 'COMPONENT_SET' || componentNode.type === 'INSTANCE';
const isUserDefinedFrame = componentNode.type === 'FRAME' && userDefinedComponent;
if (!isActualComponent && !isUserDefinedFrame) {
if (componentNode.type === 'FRAME') {
return {
content: [{
type: "text",
text: `Node "${componentNode.name}" is a FRAME. If this should be treated as a component, set userDefinedComponent: true. For analyzing complete screens, use the analyze_full_screen tool instead.`
}]
};
} else {
return {
content: [{
type: "text",
text: `Node "${componentNode.name}" is not a component (type: ${componentNode.type}). For analyzing full screens, use the analyze_full_screen tool instead.`
}]
};
}
}
// Check if this is a component set and handle variants
let variantAnalysis: ComponentVariant[] | undefined;
let selectedVariants: ComponentVariant[] = [];
if (componentNode.type === 'COMPONENT_SET' && includeVariants) {
const variantAnalyzer = new VariantAnalyzer();
variantAnalysis = await variantAnalyzer.analyzeComponentSet(componentNode);
if (variantAnalyzer.shouldPromptForVariantSelection(variantAnalysis)) {
// More than 3 variants - check if user provided selection
if (!variantSelection || variantSelection.length === 0) {
const selectionInfo = variantAnalyzer.getVariantSelectionInfo(variantAnalysis);
return {
content: [{
type: "text",
text: generateVariantSelectionPrompt(componentNode.name, selectionInfo, variantAnalysis)
}]
};
} else {
// Filter variants based on user selection
selectedVariants = variantAnalyzer.filterVariantsBySelection(variantAnalysis, {
variantNames: variantSelection,
includeDefault: true
});
if (selectedVariants.length === 0) {
return {
content: [{
type: "text",
text: `No variants found matching selection: ${variantSelection.join(', ')}`
}]
};
}
}
} else {
// 3 or fewer variants - analyze all
selectedVariants = variantAnalysis;
}
}
// Analyze the main component
let analysisReport: string;
if (useDeduplication) {
Logger.info(`🔧 Using enhanced deduplication for component analysis`);
// Use deduplicated extractor
const deduplicatedExtractor = new DeduplicatedComponentExtractor();
let deduplicatedAnalysis: DeduplicatedComponentAnalysis;
if (componentNode.type === 'COMPONENT_SET') {
// For component sets, analyze the default variant or first selected variant
const targetVariant = selectedVariants.find(v => v.isDefault) || selectedVariants[0];
if (targetVariant) {
const variantNode = await figmaService.getNode(parsedInput.fileId, targetVariant.nodeId);
deduplicatedAnalysis = await deduplicatedExtractor.analyzeComponent(variantNode, true);
} else {
// Fallback to analyzing the component set itself
deduplicatedAnalysis = await deduplicatedExtractor.analyzeComponent(componentNode, true);
}
} else {
// Regular component, instance, or user-defined frame
deduplicatedAnalysis = await deduplicatedExtractor.analyzeComponent(componentNode, true);
}
Logger.info(`📊 Deduplication analysis complete:`, {
styleRefs: Object.keys(deduplicatedAnalysis.styleRefs).length,
children: deduplicatedAnalysis.children.length,
nestedComponents: deduplicatedAnalysis.nestedComponents.length,
newStyleDefinitions: deduplicatedAnalysis.newStyleDefinitions ? Object.keys(deduplicatedAnalysis.newStyleDefinitions).length : 0
});
analysisReport = generateComprehensiveDeduplicatedReport(deduplicatedAnalysis, true);
// Add visual context for deduplicated analysis
if (parsedInput.source === 'url') {
// Reconstruct the Figma URL from the parsed input
const figmaUrl = `https://www.figma.com/design/${parsedInput.fileId}/?node-id=${parsedInput.nodeId}`;
analysisReport += "\n\n" + addVisualContextToDeduplicatedReport(
deduplicatedAnalysis,
figmaUrl,
parsedInput.nodeId
);
}
if (generateFlutterCode) {
analysisReport += "\n\n" + generateFlutterImplementation(deduplicatedAnalysis);
}
} else {
// Use original extractor
const componentExtractor = new ComponentExtractor({
maxChildNodes,
extractTextContent: true,
prioritizeComponents: true
});
let componentAnalysis: ComponentAnalysis;
if (componentNode.type === 'COMPONENT_SET') {
// For component sets, analyze the default variant or first selected variant
const targetVariant = selectedVariants.find(v => v.isDefault) || selectedVariants[0];
if (targetVariant) {
const variantNode = await figmaService.getNode(parsedInput.fileId, targetVariant.nodeId);
componentAnalysis = await componentExtractor.analyzeComponent(variantNode, userDefinedComponent);
} else {
// Fallback to analyzing the component set itself
componentAnalysis = await componentExtractor.analyzeComponent(componentNode, userDefinedComponent);
}
} else {
// Regular component, instance, or user-defined frame
componentAnalysis = await componentExtractor.analyzeComponent(componentNode, userDefinedComponent);
}
analysisReport = generateComponentAnalysisReport(
componentAnalysis,
variantAnalysis,
selectedVariants,
parsedInput
);
}
// Detect and export image assets if enabled
let assetExportInfo = '';
if (exportAssets) {
try {
// Use the existing filterImageNodes logic from assets.mts
const imageNodes = await filterImageNodesInComponent(parsedInput.fileId, [parsedInput.nodeId], figmaService);
if (imageNodes.length > 0) {
const exportedAssets = await exportComponentAssets(
imageNodes,
parsedInput.fileId,
figmaService,
projectPath
);
assetExportInfo = generateAssetExportReport(exportedAssets);
}
} catch (assetError) {
assetExportInfo = `\nAsset Export Warning: ${assetError instanceof Error ? assetError.message : String(assetError)}\n`;
}
}
return {
content: [{
type: "text",
text: analysisReport + assetExportInfo
}]
};
} catch (error) {
return {
content: [{
type: "text",
text: `Error analyzing component: ${error instanceof Error ? error.message : String(error)}`
}]
};
}
}
);
// Helper tool to list component variants
server.registerTool(
"list_component_variants",
{
title: "List Component Variants",
description: "List all variants in a Figma component set to help with variant selection",
inputSchema: {
input: z.string().describe("Figma component set URL or file ID"),
nodeId: z.string().optional().describe("Node ID (if providing file ID separately)")
}
},
async ({input, nodeId}) => {
const token = figmaApiKey;
if (!token) {
return {
content: [{
type: "text",
text: "Error: Figma access token not configured."
}]
};
}
try {
const parsedInput = parseComponentInput(input, nodeId);
if (!parsedInput.isValid) {
return {
content: [{
type: "text",
text: `Error parsing input: ${parsedInput.error}`
}]
};
}
const figmaService = new FigmaService(token);
const componentNode = await figmaService.getNode(parsedInput.fileId, parsedInput.nodeId);
if (!componentNode) {
return {
content: [{
type: "text",
text: `Component set with node ID "${parsedInput.nodeId}" not found.`
}]
};
}
if (componentNode.type !== 'COMPONENT_SET') {
return {
content: [{
type: "text",
text: `Node "${componentNode.name}" is not a component set (type: ${componentNode.type}). This tool is only for component sets with variants.`
}]
};
}
const variantAnalyzer = new VariantAnalyzer();
const variants = await variantAnalyzer.analyzeComponentSet(componentNode);
const summary = variantAnalyzer.generateVariantSummary(variants);
const selectionInfo = variantAnalyzer.getVariantSelectionInfo(variants);
let output = `Component Set: ${componentNode.name}\n\n${summary}\n`;
if (variants.length > 3) {
output += `\nTo analyze specific variants, use the analyze_figma_component tool with variantSelection parameter.\n`;
output += `Example variant names you can select:\n`;
selectionInfo.variantNames.slice(0, 5).forEach(name => {
output += `- "${name}"\n`;
});
}
return {
content: [{type: "text", text: output}]
};
} catch (error) {
return {
content: [{
type: "text",
text: `Error listing variants: ${error instanceof Error ? error.message : String(error)}`
}]
};
}
}
);
// Helper tool to inspect component structure
server.registerTool(
"inspect_component_structure",
{
title: "Inspect Component Structure",
description: "Get a quick overview of component structure, children, and nested components. Use inspect_screen_structure for full screens.",
inputSchema: {
input: z.string().describe("Figma component URL or file ID"),
nodeId: z.string().optional().describe("Node ID (if providing file ID separately)"),
userDefinedComponent: z.boolean().optional().describe("Treat a FRAME as a component (when designer hasn't converted to actual component yet) (default: false)"),
showAllChildren: z.boolean().optional().describe("Show all children regardless of limits (default: false)")
}
},
async ({input, nodeId, userDefinedComponent = false, showAllChildren = false}) => {
const token = figmaApiKey;
if (!token) {
return {
content: [{
type: "text",
text: "Error: Figma access token not configured."
}]
};
}
try {
const parsedInput = parseComponentInput(input, nodeId);
if (!parsedInput.isValid) {
return {
content: [{
type: "text",
text: `Error parsing input: ${parsedInput.error}`
}]
};
}
const figmaService = new FigmaService(token);
const componentNode = await figmaService.getNode(parsedInput.fileId, parsedInput.nodeId);
if (!componentNode) {
return {
content: [{
type: "text",
text: `Component with node ID "${parsedInput.nodeId}" not found.`
}]
};
}
// Validate that this is a component or user-defined component
const isActualComponent = componentNode.type === 'COMPONENT' || componentNode.type === 'COMPONENT_SET' || componentNode.type === 'INSTANCE';
const isUserDefinedFrame = componentNode.type === 'FRAME' && userDefinedComponent;
if (!isActualComponent && !isUserDefinedFrame) {
if (componentNode.type === 'FRAME') {
return {
content: [{
type: "text",
text: `Node "${componentNode.name}" is a FRAME. If this should be treated as a component, set userDefinedComponent: true. For inspecting complete screens, use the inspect_screen_structure tool instead.`
}]
};
} else {
return {
content: [{
type: "text",
text: `Node "${componentNode.name}" is not a component (type: ${componentNode.type}). For inspecting full screens, use the inspect_screen_structure tool instead.`
}]
};
}
}
const output = generateStructureInspectionReport(componentNode, showAllChildren);
return {
content: [{type: "text", text: output}]
};
} catch (error) {
return {
content: [{
type: "text",
text: `Error inspecting structure: ${error instanceof Error ? error.message : String(error)}`
}]
};
}
}
);
// Dedicated Flutter code generation tool
server.registerTool(
"generate_flutter_implementation",
{
title: "Generate Flutter Implementation",
description: "Generate complete Flutter widget code using cached style definitions",
inputSchema: {
componentNodeId: z.string().describe("Node ID of the analyzed component"),
includeStyleDefinitions: z.boolean().optional().describe("Include style definitions in output (default: true)"),
widgetName: z.string().optional().describe("Custom widget class name")
}
},
async ({ componentNodeId, includeStyleDefinitions = true, widgetName }) => {
try {
const styleLibrary = FlutterStyleLibrary.getInstance();
const styles = styleLibrary.getAllStyles();
let output = "🏗️ Flutter Implementation\n";
output += `${'='.repeat(50)}\n\n`;
if (includeStyleDefinitions && styles.length > 0) {
output += "📋 Style Definitions:\n";
output += `${'─'.repeat(30)}\n`;
styles.forEach(style => {
output += `// ${style.id} (${style.category}, used ${style.usageCount} times)\n`;
output += `final ${style.id} = ${style.flutterCode};\n\n`;
});
output += "\n";
} else if (styles.length === 0) {
output += "⚠️ No cached styles found. Please analyze a component first.\n\n";
}
output += generateWidgetClass(componentNodeId, widgetName || 'CustomWidget', styles);
// Add usage summary
if (styles.length > 0) {
output += "\n\n📊 Style Library Summary:\n";
output += `${'─'.repeat(30)}\n`;
output += `• Total unique styles: ${styles.length}\n`;
const categoryStats = styles.reduce((acc, style) => {
acc[style.category] = (acc[style.category] || 0) + 1;
return acc;
}, {} as Record<string, number>);
Object.entries(categoryStats).forEach(([category, count]) => {
output += `• ${category}: ${count} style(s)\n`;
});
const totalUsage = styles.reduce((sum, style) => sum + style.usageCount, 0);
output += `• Total style usage: ${totalUsage}\n`;
const efficiency = styles.length > 0 ? ((totalUsage - styles.length) / totalUsage * 100).toFixed(1) : '0.0';
output += `• Deduplication efficiency: ${efficiency}% reduction\n`;
}
return {
content: [{ type: "text", text: output }]
};
} catch (error) {
return {
content: [{
type: "text",
text: `Error generating Flutter implementation: ${error instanceof Error ? error.message : String(error)}`
}]
};
}
}
);
// Style library status tool
server.registerTool(
"style_library_status",
{
title: "Style Library Status",
description: "Get comprehensive status report of the cached style library",
inputSchema: {}
},
async () => {
try {
const report = generateStyleLibraryReport();
return {
content: [{ type: "text", text: report }]
};
} catch (error) {
return {
content: [{
type: "text",
text: `Error generating style library report: ${error instanceof Error ? error.message : String(error)}`
}]
};
}
}
);
}
/**
* OPTIMIZED: Filter image nodes within a component - only searches within target nodes
*/
async function filterImageNodesInComponent(fileId: string, targetNodeIds: string[], figmaService: FigmaService): Promise<Array<{id: string, name: string, node: any}>> {
// OPTIMIZED: Only get the target nodes instead of the entire file (massive performance improvement)
const targetNodes = await figmaService.getNodes(fileId, targetNodeIds);
const allNodesWithImages: Array<{id: string, name: string, node: any}> = [];
function extractImageNodes(node: any, nodeId: string = node.id): void {
// Check if this node has image fills
if (node.fills && node.fills.some((fill: any) => fill.type === 'IMAGE' && fill.visible !== false)) {
allNodesWithImages.push({
id: nodeId,
name: node.name,
node: node
});
}
// Check if this is a vector/illustration that should be exported
if (node.type === 'VECTOR' && node.name) {
const name = node.name.toLowerCase();
if ((name.includes('image') || name.includes('illustration') || name.includes('graphic')) &&
!name.includes('icon') && !name.includes('button')) {
allNodesWithImages.push({
id: nodeId,
name: node.name,
node: node
});
}
}
// Recursively check children
if (node.children) {
node.children.forEach((child: any) => {
extractImageNodes(child, child.id);
});
}
}
// OPTIMIZED: Extract only from target nodes instead of entire file
// This eliminates the need for expensive boundary checking since we only search within target nodes
Object.values(targetNodes).forEach((node: any) => {
extractImageNodes(node);
});
// OPTIMIZED: No filtering needed since we only searched within target nodes
return allNodesWithImages;
}
// REMOVED: isNodeWithinTarget function no longer needed since we only search within target nodes
/**
* Export component assets to Flutter project
*/
async function exportComponentAssets(
imageNodes: Array<{id: string, name: string, node: any}>,
fileId: string,
figmaService: FigmaService,
projectPath: string
): Promise<AssetInfo[]> {
if (imageNodes.length === 0) {
return [];
}
// Create assets directory structure
const assetsDir = await createAssetsDirectory(projectPath);
const downloadedAssets: AssetInfo[] = [];
// Export images at 2x scale (standard for Flutter)
const imageUrls = await figmaService.getImageExportUrls(fileId, imageNodes.map(n => n.id), {
format: 'png',
scale: 2
});
for (const imageNode of imageNodes) {
const imageUrl = imageUrls[imageNode.id];
if (!imageUrl) continue;
const filename = generateAssetFilename(imageNode.name, 'png', 2, false);
const filepath = join(assetsDir, filename);
try {
// Download the image
await downloadImage(imageUrl, filepath);
// Get file size for reporting
const stats = await getFileStats(filepath);
downloadedAssets.push({
nodeId: imageNode.id,
nodeName: imageNode.name,
filename,
path: `assets/images/${filename}`,
size: stats.size
});
} catch (downloadError) {
console.warn(`Failed to download image ${imageNode.name}:`, downloadError);
}
}
if (downloadedAssets.length > 0) {
// Update pubspec.yaml
const pubspecPath = join(projectPath, 'pubspec.yaml');
await updatePubspecAssets(pubspecPath, downloadedAssets);
// Generate asset constants file
await generateAssetConstants(downloadedAssets, projectPath);
}
return downloadedAssets;
}
/**
* Generate asset export report
*/
function generateAssetExportReport(exportedAssets: AssetInfo[]): string {
if (exportedAssets.length === 0) {
return '';
}
let report = `\n${'='.repeat(50)}\n`;
report += `🖼️ AUTOMATIC ASSET EXPORT\n`;
report += `${'='.repeat(50)}\n\n`;
report += `Found and exported ${exportedAssets.length} image asset(s) from the component:\n\n`;
// Group by base name for cleaner output
const groupedAssets = groupAssetsByBaseName(exportedAssets);
Object.entries(groupedAssets).forEach(([baseName, assets]) => {
report += `📁 ${baseName}:\n`;
assets.forEach(asset => {
report += ` • ${asset.filename} (${asset.size})\n`;
});
});
report += `\n✅ Assets Configuration:\n`;
report += ` • Images saved to: assets/images/\n`;
report += ` • pubspec.yaml updated with asset declarations\n`;
report += ` • Asset constants generated in: lib/constants/assets.dart\n\n`;
report += `🚀 Usage in Flutter:\n`;
report += ` import 'package:your_app/constants/assets.dart';\n\n`;
exportedAssets.forEach(asset => {
const constantName = asset.filename
.replace(/\.[^/.]+$/, '') // Remove extension
.replace(/[^a-zA-Z0-9]/g, ' ') // Replace special chars with space
.replace(/\s+/g, ' ') // Replace multiple spaces with single
.trim()
.split(' ')
.map((word, index) => index === 0 ? word.toLowerCase() : word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join('');
report += ` Image.asset(Assets.${constantName}) // ${asset.nodeName}\n`;
});
report += `\n${'='.repeat(50)}\n`;
return report;
}
/**
* Generate widget class implementation
*/
function generateWidgetClass(componentNodeId: string, widgetName: string, styles: Array<any>): string {
let output = `🎯 Widget Implementation:\n`;
output += `${'─'.repeat(30)}\n`;
// Widget composition best practices
output += `🏗️ Widget Composition Guidelines:\n`;
output += `- Start with inline widget tree composition in build() method\n`;
output += `- Continue composing inline until you reach ~200 lines of code\n`;
output += `- Only then extract parts into private StatelessWidget classes\n`;
output += `- Use private widgets (prefix with _) for internal breakdown\n`;
output += `- Avoid functional widgets - always use StatelessWidget classes\n\n`;
output += `class ${widgetName} extends StatelessWidget {\n`;
output += ` const ${widgetName}({Key? key}) : super(key: key);\n\n`;
output += ` @override\n`;
output += ` Widget build(BuildContext context) {\n`;
// Find relevant styles for this component
const decorationStyles = styles.filter(s => s.category === 'decoration');
const paddingStyles = styles.filter(s => s.category === 'padding');
const textStyles = styles.filter(s => s.category === 'text');
if (decorationStyles.length > 0 || paddingStyles.length > 0) {
output += ` return Container(\n`;
// Add decoration if available
if (decorationStyles.length > 0) {
const decorationStyle = decorationStyles[0]; // Use first decoration style
output += ` decoration: ${decorationStyle.id},\n`;
}
// Add padding if available
if (paddingStyles.length > 0) {
const paddingStyle = paddingStyles[0]; // Use first padding style
output += ` padding: ${paddingStyle.id},\n`;
}
// Add child content
if (textStyles.length > 0) {
const textStyle = textStyles[0]; // Use first text style
output += ` child: Text(\n`;
output += ` 'Sample Text', // TODO: Replace with actual content\n`;
output += ` style: ${textStyle.id},\n`;
output += ` ),\n`;
} else {
output += ` child: Column(\n`;
output += ` children: [\n`;
output += ` // TODO: Add your widget content here\n`;
output += ` Text('Component Content'),\n`;
output += ` ],\n`;
output += ` ),\n`;
}
output += ` );\n`;
} else if (textStyles.length > 0) {
// Just a text widget if only text styles are available
const textStyle = textStyles[0];
output += ` return Text(\n`;
output += ` 'Sample Text', // TODO: Replace with actual content\n`;
output += ` style: ${textStyle.id},\n`;
output += ` );\n`;
} else {
// Fallback for when no cached styles are available
output += ` return Container(\n`;
output += ` // TODO: Implement widget using component node ID: ${componentNodeId}\n`;
output += ` // No cached styles found - please analyze a component first\n`;
output += ` child: Text('Widget Placeholder'),\n`;
output += ` );\n`;
}
output += ` }\n`;
output += `}\n`;
// Add usage instructions
output += `\n💡 Usage Instructions:\n`;
output += `${'─'.repeat(30)}\n`;
output += `1. Start by building the complete widget tree inline in build() method\n`;
output += `2. Keep composing widgets inline until you reach ~200 lines\n`;
output += `3. Only then extract reusable parts into private StatelessWidget classes\n`;
output += `4. Replace 'Sample Text' with actual content from Figma\n`;
output += `5. Customize the widget structure and add any missing properties\n\n`;
if (styles.length > 0) {
output += `📦 Available Style References:\n`;
output += `${'─'.repeat(30)}\n`;
styles.forEach(style => {
output += `• ${style.id} (${style.category})\n`;
});
}
return output;
}
```