446 lines
13 KiB
Python
Executable File
446 lines
13 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Analyze Figma design data and extract patterns, components, and tokens.
|
|
Compares against existing UI kit to identify new components and potential reuse opportunities.
|
|
"""
|
|
|
|
import json
|
|
import sys
|
|
import argparse
|
|
from typing import Dict, List, Any
|
|
from difflib import SequenceMatcher
|
|
|
|
|
|
def calculate_similarity(str1: str, str2: str) -> float:
|
|
"""
|
|
Calculate similarity ratio between two strings.
|
|
|
|
Args:
|
|
str1: First string
|
|
str2: Second string
|
|
|
|
Returns:
|
|
float: Similarity ratio (0.0 to 1.0)
|
|
"""
|
|
return SequenceMatcher(None, str1.lower(), str2.lower()).ratio()
|
|
|
|
|
|
def extract_components_from_metadata(metadata: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
"""
|
|
Extract component information from Figma metadata.
|
|
|
|
Args:
|
|
metadata: Figma MCP get_metadata response or manual structure
|
|
|
|
Returns:
|
|
List of components with their properties
|
|
"""
|
|
components = []
|
|
|
|
def traverse_nodes(node, depth=0):
|
|
"""Recursively traverse Figma node tree."""
|
|
if not isinstance(node, dict):
|
|
return
|
|
|
|
node_type = node.get('type', '')
|
|
node_name = node.get('name', 'Unnamed')
|
|
node_id = node.get('id', '')
|
|
|
|
# Identify components (COMPONENT, COMPONENT_SET, or instances)
|
|
if node_type in ['COMPONENT', 'COMPONENT_SET', 'INSTANCE']:
|
|
components.append({
|
|
'id': node_id,
|
|
'name': node_name,
|
|
'type': node_type,
|
|
'depth': depth,
|
|
'properties': extract_node_properties(node)
|
|
})
|
|
|
|
# Traverse children
|
|
children = node.get('children', [])
|
|
for child in children:
|
|
traverse_nodes(child, depth + 1)
|
|
|
|
# Handle both MCP format and manual format
|
|
if 'document' in metadata:
|
|
traverse_nodes(metadata['document'])
|
|
elif 'nodes' in metadata:
|
|
for node in metadata['nodes']:
|
|
traverse_nodes(node)
|
|
elif isinstance(metadata, dict):
|
|
traverse_nodes(metadata)
|
|
|
|
return components
|
|
|
|
|
|
def extract_node_properties(node: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""
|
|
Extract relevant properties from Figma node.
|
|
|
|
Args:
|
|
node: Figma node data
|
|
|
|
Returns:
|
|
Dictionary of extracted properties
|
|
"""
|
|
properties = {}
|
|
|
|
# Extract layout properties
|
|
if 'layoutMode' in node:
|
|
properties['layout'] = {
|
|
'mode': node.get('layoutMode'),
|
|
'direction': node.get('layoutDirection'),
|
|
'gap': node.get('itemSpacing'),
|
|
'padding': {
|
|
'top': node.get('paddingTop'),
|
|
'right': node.get('paddingRight'),
|
|
'bottom': node.get('paddingBottom'),
|
|
'left': node.get('paddingLeft')
|
|
}
|
|
}
|
|
|
|
# Extract sizing
|
|
if 'absoluteBoundingBox' in node:
|
|
bbox = node['absoluteBoundingBox']
|
|
properties['size'] = {
|
|
'width': bbox.get('width'),
|
|
'height': bbox.get('height')
|
|
}
|
|
|
|
# Extract variant properties
|
|
if 'componentProperties' in node:
|
|
properties['variants'] = node['componentProperties']
|
|
|
|
return properties
|
|
|
|
|
|
def categorize_component_by_name(component_name: str) -> str:
|
|
"""
|
|
Categorize component by atomic design level based on name patterns.
|
|
|
|
Args:
|
|
component_name: Component name from Figma
|
|
|
|
Returns:
|
|
'atom', 'molecule', 'organism', or 'template'
|
|
"""
|
|
name_lower = component_name.lower()
|
|
|
|
# Atoms: Basic elements
|
|
atoms = ['button', 'input', 'icon', 'text', 'badge', 'avatar', 'checkbox',
|
|
'radio', 'switch', 'label', 'link', 'image']
|
|
|
|
# Molecules: Simple combinations
|
|
molecules = ['field', 'card', 'list-item', 'menu-item', 'tab', 'breadcrumb',
|
|
'tooltip', 'dropdown', 'search', 'pagination']
|
|
|
|
# Organisms: Complex components
|
|
organisms = ['header', 'footer', 'sidebar', 'navigation', 'modal', 'form',
|
|
'table', 'dashboard', 'profile', 'chart', 'grid']
|
|
|
|
for atom in atoms:
|
|
if atom in name_lower:
|
|
return 'atom'
|
|
|
|
for molecule in molecules:
|
|
if molecule in name_lower:
|
|
return 'molecule'
|
|
|
|
for organism in organisms:
|
|
if organism in name_lower:
|
|
return 'organism'
|
|
|
|
# Default to molecule if unclear
|
|
return 'molecule'
|
|
|
|
|
|
def find_similar_components(new_component: Dict[str, Any],
|
|
ui_kit_inventory: List[Dict[str, Any]],
|
|
threshold: float = 0.7) -> List[Dict[str, Any]]:
|
|
"""
|
|
Find similar components in existing UI kit.
|
|
|
|
Args:
|
|
new_component: Component from Figma design
|
|
ui_kit_inventory: List of existing UI kit components
|
|
threshold: Similarity threshold (0.0 to 1.0)
|
|
|
|
Returns:
|
|
List of similar components with similarity scores
|
|
"""
|
|
similar = []
|
|
new_name = new_component.get('name', '')
|
|
|
|
for existing in ui_kit_inventory:
|
|
existing_name = existing.get('name', '')
|
|
similarity = calculate_similarity(new_name, existing_name)
|
|
|
|
if similarity >= threshold:
|
|
similar.append({
|
|
'name': existing_name,
|
|
'path': existing.get('path', ''),
|
|
'similarity': similarity,
|
|
'recommendation': generate_recommendation(similarity, new_name, existing_name)
|
|
})
|
|
|
|
# Sort by similarity descending
|
|
similar.sort(key=lambda x: x['similarity'], reverse=True)
|
|
|
|
return similar
|
|
|
|
|
|
def generate_recommendation(similarity: float, new_name: str, existing_name: str) -> str:
|
|
"""
|
|
Generate recommendation based on similarity score.
|
|
|
|
Args:
|
|
similarity: Similarity ratio
|
|
new_name: New component name
|
|
existing_name: Existing component name
|
|
|
|
Returns:
|
|
Recommendation string
|
|
"""
|
|
if similarity >= 0.9:
|
|
return f"Very similar to {existing_name}. Consider reusing existing component."
|
|
elif similarity >= 0.7:
|
|
return f"Similar to {existing_name}. Consider extending with new variant/prop."
|
|
else:
|
|
return f"Some similarity to {existing_name}. Review for potential shared patterns."
|
|
|
|
|
|
def analyze_design(figma_data: Dict[str, Any],
|
|
ui_kit_inventory: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""
|
|
Main analysis function: extract patterns from Figma and compare with UI kit.
|
|
|
|
Args:
|
|
figma_data: Combined Figma MCP data (metadata, variables, code_connect_map)
|
|
ui_kit_inventory: Current UI kit inventory
|
|
|
|
Returns:
|
|
Analysis results with new tokens, components, similarities, breaking changes
|
|
"""
|
|
results = {
|
|
'new_tokens': [],
|
|
'new_components': [],
|
|
'similar_components': [],
|
|
'breaking_changes': [],
|
|
'summary': {}
|
|
}
|
|
|
|
# Extract components from Figma metadata
|
|
metadata = figma_data.get('metadata', {})
|
|
figma_components = extract_components_from_metadata(metadata)
|
|
|
|
# Extract existing UI kit components
|
|
existing_components = ui_kit_inventory.get('components', [])
|
|
|
|
# Analyze each Figma component
|
|
for figma_comp in figma_components:
|
|
comp_name = figma_comp.get('name', '')
|
|
|
|
# Skip system components (starting with _, . or #)
|
|
if comp_name.startswith(('_', '.', '#')):
|
|
continue
|
|
|
|
# Find similar components
|
|
similar = find_similar_components(figma_comp, existing_components, threshold=0.7)
|
|
|
|
if similar:
|
|
# Component has similarities - potential reuse
|
|
results['similar_components'].append({
|
|
'figma_component': comp_name,
|
|
'figma_id': figma_comp.get('id'),
|
|
'category': categorize_component_by_name(comp_name),
|
|
'similar_to': similar,
|
|
'properties': figma_comp.get('properties', {})
|
|
})
|
|
else:
|
|
# New component - needs creation
|
|
results['new_components'].append({
|
|
'name': comp_name,
|
|
'id': figma_comp.get('id'),
|
|
'category': categorize_component_by_name(comp_name),
|
|
'properties': figma_comp.get('properties', {}),
|
|
'depth': figma_comp.get('depth', 0)
|
|
})
|
|
|
|
# Analyze design tokens from variables
|
|
variables = figma_data.get('variables', {})
|
|
if variables:
|
|
results['new_tokens'] = analyze_tokens(variables, ui_kit_inventory)
|
|
|
|
# Analyze breaking changes
|
|
code_connect_map = figma_data.get('code_connect_map', {})
|
|
if code_connect_map:
|
|
results['breaking_changes'] = detect_breaking_changes(
|
|
figma_components,
|
|
code_connect_map,
|
|
existing_components
|
|
)
|
|
|
|
# Generate summary
|
|
results['summary'] = {
|
|
'total_figma_components': len(figma_components),
|
|
'new_components_count': len(results['new_components']),
|
|
'similar_components_count': len(results['similar_components']),
|
|
'new_tokens_count': len(results['new_tokens']),
|
|
'breaking_changes_count': len(results['breaking_changes']),
|
|
'reuse_potential': f"{(len(results['similar_components']) / max(len(figma_components), 1)) * 100:.1f}%"
|
|
}
|
|
|
|
return results
|
|
|
|
|
|
def analyze_tokens(variables: Dict[str, Any],
|
|
ui_kit_inventory: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
"""
|
|
Analyze design tokens from Figma variables.
|
|
|
|
Args:
|
|
variables: Figma variables data
|
|
ui_kit_inventory: Current UI kit inventory with existing tokens
|
|
|
|
Returns:
|
|
List of new tokens not in current inventory
|
|
"""
|
|
new_tokens = []
|
|
existing_tokens = ui_kit_inventory.get('tokens', {})
|
|
|
|
# Handle different variable formats
|
|
for var_name, var_data in variables.items():
|
|
if isinstance(var_data, dict):
|
|
value = var_data.get('$value') or var_data.get('value')
|
|
var_type = var_data.get('$type') or var_data.get('type')
|
|
else:
|
|
value = var_data
|
|
var_type = infer_token_type(var_name, value)
|
|
|
|
# Check if token exists
|
|
if var_name not in existing_tokens:
|
|
new_tokens.append({
|
|
'name': var_name,
|
|
'value': value,
|
|
'type': var_type,
|
|
'status': 'new'
|
|
})
|
|
|
|
return new_tokens
|
|
|
|
|
|
def infer_token_type(name: str, value: Any) -> str:
|
|
"""
|
|
Infer token type from name and value.
|
|
|
|
Args:
|
|
name: Token name
|
|
value: Token value
|
|
|
|
Returns:
|
|
Token type string
|
|
"""
|
|
name_lower = name.lower()
|
|
|
|
if 'color' in name_lower or (isinstance(value, str) and value.startswith('#')):
|
|
return 'color'
|
|
elif 'spacing' in name_lower or 'gap' in name_lower or 'padding' in name_lower:
|
|
return 'dimension'
|
|
elif 'font' in name_lower or 'typography' in name_lower:
|
|
return 'typography'
|
|
elif 'radius' in name_lower or 'border' in name_lower:
|
|
return 'dimension'
|
|
elif 'shadow' in name_lower:
|
|
return 'shadow'
|
|
else:
|
|
return 'unknown'
|
|
|
|
|
|
def detect_breaking_changes(figma_components: List[Dict[str, Any]],
|
|
code_connect_map: Dict[str, Any],
|
|
existing_components: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
"""
|
|
Detect breaking changes in component mappings.
|
|
|
|
Args:
|
|
figma_components: Components from Figma
|
|
code_connect_map: Figma Code Connect mappings
|
|
existing_components: Existing UI kit components
|
|
|
|
Returns:
|
|
List of breaking changes detected
|
|
"""
|
|
breaking_changes = []
|
|
|
|
for figma_comp in figma_components:
|
|
comp_id = figma_comp.get('id')
|
|
comp_name = figma_comp.get('name')
|
|
|
|
# Check if component was previously mapped
|
|
if comp_id in code_connect_map:
|
|
mapping = code_connect_map[comp_id]
|
|
mapped_path = mapping.get('codeConnectSrc')
|
|
|
|
# Check if mapped component still exists
|
|
exists = any(
|
|
existing.get('path') == mapped_path
|
|
for existing in existing_components
|
|
)
|
|
|
|
if not exists:
|
|
breaking_changes.append({
|
|
'figma_component': comp_name,
|
|
'figma_id': comp_id,
|
|
'previous_mapping': mapped_path,
|
|
'issue': 'Mapped component no longer exists in codebase',
|
|
'recommendation': 'Re-map to new component or create new implementation'
|
|
})
|
|
|
|
return breaking_changes
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description='Analyze Figma design data and compare with UI kit'
|
|
)
|
|
parser.add_argument(
|
|
'--figma-data',
|
|
required=True,
|
|
help='Path to JSON file with Figma MCP data'
|
|
)
|
|
parser.add_argument(
|
|
'--ui-kit-inventory',
|
|
required=True,
|
|
help='Path to UI kit inventory JSON file'
|
|
)
|
|
parser.add_argument(
|
|
'--output',
|
|
help='Output file path (default: stdout)'
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
# Load Figma data
|
|
with open(args.figma_data, 'r') as f:
|
|
figma_data = json.load(f)
|
|
|
|
# Load UI kit inventory
|
|
with open(args.ui_kit_inventory, 'r') as f:
|
|
ui_kit_inventory = json.load(f)
|
|
|
|
# Run analysis
|
|
results = analyze_design(figma_data, ui_kit_inventory)
|
|
|
|
# Output results
|
|
output_json = json.dumps(results, indent=2)
|
|
|
|
if args.output:
|
|
with open(args.output, 'w') as f:
|
|
f.write(output_json)
|
|
else:
|
|
print(output_json)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|