Files
gh-alekspetrov-navigator/skills/product-design/functions/design_analyzer.py
2025-11-29 17:51:59 +08:00

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()