diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json
index 242f863..ccfcec2 100644
--- a/.claude-plugin/marketplace.json
+++ b/.claude-plugin/marketplace.json
@@ -16,6 +16,12 @@
"source": "./plugins/dead-code",
"description": "Find and remove unused code added in the current branch",
"version": "1.0.0"
+ },
+ {
+ "name": "figma-icon-sync",
+ "source": "./plugins/figma-icon-sync",
+ "description": "Sync icons from Figma design system to your codebase using Figma MCP",
+ "version": "1.0.0"
}
]
}
diff --git a/README.md b/README.md
index 5b9b146..4dba5b8 100644
--- a/README.md
+++ b/README.md
@@ -13,6 +13,9 @@ Claude Code plugins for code quality and development workflows.
# Install the dead-code plugin
/plugin install dead-code@fsai
+
+# Install the figma-icon-sync plugin
+/plugin install figma-icon-sync@fsai
```
## Plugins
@@ -37,3 +40,21 @@ Remove dead code from a branch.
Claude will diff against master, identify dead code, and remove it.
+### figma-icon-sync
+
+Sync icons from Figma design system to your codebase using Figma MCP.
+
+**Prerequisites:** Requires Figma MCP to be connected. Add it with:
+```bash
+claude mcp add --transport http figma https://mcp.figma.com/mcp
+```
+Then authenticate via `/mcp` > figma > Authenticate.
+
+**Usage:** Just ask Claude Code:
+- "Sync icons from Figma"
+- "Download home and settings icons from the design system"
+- "Update icons from Figma"
+- "Check available icons in Figma"
+
+Claude will browse your Figma file, extract SVG icons with proper naming conventions (PascalCase, Outlined suffix), and add them to your icons package.
+
diff --git a/plugins/figma-icon-sync/.claude-plugin/plugin.json b/plugins/figma-icon-sync/.claude-plugin/plugin.json
new file mode 100644
index 0000000..bcff55d
--- /dev/null
+++ b/plugins/figma-icon-sync/.claude-plugin/plugin.json
@@ -0,0 +1,10 @@
+{
+ "name": "figma-icon-sync",
+ "version": "1.0.0",
+ "description": "Sync icons from Figma design system to your codebase using Figma MCP",
+ "author": {
+ "name": "bill0x2a",
+ "email": "bill@franchisesystems.ai"
+ },
+ "license": "MIT"
+}
diff --git a/plugins/figma-icon-sync/references/figma-mcp-icons.md b/plugins/figma-icon-sync/references/figma-mcp-icons.md
new file mode 100644
index 0000000..c3b04bc
--- /dev/null
+++ b/plugins/figma-icon-sync/references/figma-mcp-icons.md
@@ -0,0 +1,156 @@
+# Figma MCP Icon Extraction Reference
+
+## Available MCP Tools for Icon Work
+
+### get_metadata
+Returns sparse XML representation of layers. Best for navigating large files.
+
+**Use for:**
+- Initial file structure exploration
+- Finding icon sections and categories
+- Getting node IDs for specific icons
+
+**Example prompt:**
+```
+Use get_metadata to explore the structure of this Figma file:
+https://www.figma.com/design/xe1fVdPmJnvHPMKoE4Jasm/central-icon-system
+```
+
+### get_design_context
+Returns full design context including code generation. Use for actual icon extraction.
+
+**Use for:**
+- Extracting SVG code for specific icons
+- Getting exact styling and paths
+
+**Example prompt:**
+```
+Use get_design_context to extract the icon at node-id 123:456 as SVG code.
+Output clean SVG with viewBox="0 0 24 24", no transforms or metadata.
+```
+
+### get_screenshot
+Takes a screenshot of selection. Useful for visual verification.
+
+**Use for:**
+- Verifying correct icon selection
+- Visual comparison during sync
+
+## File Key Extraction
+
+The file key is the alphanumeric string in the Figma URL between `/design/` and the next `/`:
+
+```
+https://www.figma.com/design/xe1fVdPmJnvHPMKoE4Jasm/central-icon-system
+ ^^^^^^^^^^^^^^^^^^^^^^^^
+ This is the file key
+```
+
+## Node ID Format
+
+Figma node IDs follow the pattern: `123:456` or `123-456`
+
+When referencing in URLs, they appear as query parameters:
+```
+https://www.figma.com/design/FILE_KEY/name?node-id=123-456
+```
+
+## Icon Extraction Strategy
+
+### For Small Icon Libraries (< 50 icons)
+
+1. Use `get_metadata` once to get full structure
+2. Identify all icon nodes
+3. Extract each with `get_design_context`
+
+### For Large Icon Libraries (> 50 icons)
+
+1. Use `get_metadata` to get page/section structure
+2. Identify relevant sections based on user needs
+3. Drill down into specific sections
+4. Extract only needed icons to avoid rate limits
+
+### For Icon Updates
+
+1. Identify icons currently used in codebase
+2. Map to Figma node IDs
+3. Extract only changed or missing icons
+4. Compare SVG content for actual changes
+
+## SVG Cleanup Requirements
+
+Icons from Figma typically need cleanup:
+
+| Issue | Solution |
+|-------|----------|
+| Non-24x24 dimensions | Normalize viewBox and size |
+| Figma metadata | Remove data-* attributes |
+| XML declarations | Remove |
+| Comments | Remove all HTML comments |
+| Empty groups | Remove elements |
+| IDs | Remove id attributes |
+
+## Common Icon Naming Patterns in Design Systems
+
+| Pattern | Example | Normalized |
+|---------|---------|------------|
+| kebab-case | `arrow-left` | `ArrowLeft.svg` |
+| With size | `24/arrow-left` | `ArrowLeft.svg` |
+| With variant | `arrow-left-outlined` | `ArrowLeftOutlined.svg` |
+| With prefix | `icon-arrow-left` | `ArrowLeft.svg` |
+| Numbered | `01-arrow-left` | `ArrowLeft.svg` |
+
+## Rate Limits
+
+Figma API rate limits apply to MCP tool calls:
+
+- **Free accounts:** 6 tool calls per month
+- **Paid accounts:** Per-minute limits (Tier 1 REST API limits)
+
+### Strategies to Minimize API Calls
+
+1. Use `get_metadata` once, save locally
+2. Batch icon extractions when possible
+3. Cache node IDs for repeat syncs
+4. Only sync changed icons
+
+## Error Handling
+
+### "Too many requests"
+Wait and retry, or reduce batch size
+
+### "Node not found"
+Verify node ID format, check if icon was moved/deleted
+
+### "File access denied"
+Ensure Figma MCP is authenticated with proper permissions
+
+### Large response truncation
+Use `get_metadata` first to get structure, then extract individual nodes
+
+## Integration with Project Build
+
+After syncing icons, typical build steps:
+
+1. **Verify icons added:**
+ ```bash
+ git diff --name-only packages/icons/
+ ```
+
+2. **Build icons package:**
+ ```bash
+ # Check package.json for exact command
+ pnpm build:icons
+ # or
+ pnpm --filter @company/icons build
+ ```
+
+3. **Verify build output:**
+ ```bash
+ ls packages/icons/dist/
+ ```
+
+4. **Test imports:**
+ ```typescript
+ import { ArrowLeft, ArrowLeftOutlined } from '@company/icons';
+ ```
diff --git a/plugins/figma-icon-sync/scripts/browse_icons.py b/plugins/figma-icon-sync/scripts/browse_icons.py
new file mode 100644
index 0000000..1f71b39
--- /dev/null
+++ b/plugins/figma-icon-sync/scripts/browse_icons.py
@@ -0,0 +1,348 @@
+#!/usr/bin/env python3
+"""
+Figma Icon Browser
+
+Parses Figma metadata output to help navigate and search for icons.
+Useful for processing large icon design systems.
+
+Usage:
+ python browse_icons.py --list
+ python browse_icons.py --search "home"
+ python browse_icons.py --section "Navigation"
+
+Requires Python 3.7+
+"""
+
+import re
+import sys
+import json
+import argparse
+from pathlib import Path
+from typing import List, Optional, Dict, Any
+from dataclasses import dataclass
+
+
+@dataclass
+class IconInfo:
+ """Information about a single icon."""
+ name: str
+ node_id: str
+ section: str
+ is_outlined: bool
+ path: str # Full path in the Figma structure
+
+
+def parse_figma_metadata(content: str) -> List[IconInfo]:
+ """
+ Parse Figma metadata to extract icon information.
+
+ Supports both XML and JSON formats from get_metadata tool.
+
+ Args:
+ content: Raw metadata string from Figma MCP
+
+ Returns:
+ List of IconInfo objects
+ """
+ icons = [] # type: List[IconInfo]
+
+ # Try parsing as JSON first
+ try:
+ data = json.loads(content)
+ icons = parse_json_metadata(data)
+ except json.JSONDecodeError:
+ # Fall back to XML parsing
+ icons = parse_xml_metadata(content)
+
+ return icons
+
+
+def parse_json_metadata(
+ data: Any,
+ path: str = "",
+ section: str = ""
+) -> List[IconInfo]:
+ """Parse JSON format metadata."""
+ icons = [] # type: List[IconInfo]
+
+ if isinstance(data, dict):
+ name = data.get('name', '')
+ node_type = data.get('type', '')
+ node_id = data.get('id', '')
+
+ # Update section based on frame/page names
+ current_section = section
+ if node_type in ['PAGE', 'FRAME', 'SECTION']:
+ current_section = name
+
+ current_path = f"{path}/{name}" if path else name
+
+ # Check if this looks like an icon
+ if is_icon_node(name, node_type):
+ is_outlined = check_outlined_variant(name)
+ icons.append(IconInfo(
+ name=name,
+ node_id=node_id,
+ section=current_section,
+ is_outlined=is_outlined,
+ path=current_path
+ ))
+
+ # Recurse into children
+ children = data.get('children', [])
+ for child in children:
+ icons.extend(parse_json_metadata(child, current_path, current_section))
+
+ return icons
+
+
+def parse_xml_metadata(content: str) -> List[IconInfo]:
+ """Parse XML format metadata."""
+ icons = [] # type: List[IconInfo]
+
+ # Simple regex-based parsing for XML
+ # Pattern to match icon-like elements
+ node_pattern = re.compile(
+ r'<(\w+)[^>]*\s+name="([^"]+)"[^>]*\s+id="([^"]+)"',
+ re.IGNORECASE
+ )
+
+ current_section = ""
+
+ for match in node_pattern.finditer(content):
+ node_type = match.group(1)
+ name = match.group(2)
+ node_id = match.group(3)
+
+ # Track sections
+ if node_type.upper() in ['FRAME', 'PAGE', 'SECTION']:
+ current_section = name
+ continue
+
+ # Check if this looks like an icon
+ if is_icon_node(name, node_type):
+ is_outlined = check_outlined_variant(name)
+ icons.append(IconInfo(
+ name=name,
+ node_id=node_id,
+ section=current_section,
+ is_outlined=is_outlined,
+ path=f"{current_section}/{name}"
+ ))
+
+ return icons
+
+
+def is_icon_node(name: str, node_type: str) -> bool:
+ """Determine if a node is likely an icon."""
+ # Skip container types
+ if node_type.upper() in ['PAGE', 'DOCUMENT', 'SECTION']:
+ return False
+
+ # Common icon patterns
+ icon_patterns = [
+ r'^icon[-_]', # Prefix: icon-name
+ r'[-_]icon$', # Suffix: name-icon
+ r'^ic[-_]', # Prefix: ic-name
+ r'[-_]ic$', # Suffix: name-ic
+ r'^\d+[-_]', # Numbered: 24-home
+ ]
+
+ # Check against patterns
+ lower_name = name.lower()
+ for pattern in icon_patterns:
+ if re.search(pattern, lower_name):
+ return True
+
+ # If the node is a COMPONENT or COMPONENT_SET, it's likely an icon
+ if node_type.upper() in ['COMPONENT', 'COMPONENT_SET', 'INSTANCE']:
+ # Additional check: exclude obviously non-icon components
+ exclude_patterns = ['button', 'input', 'card', 'modal', 'dialog', 'form']
+ if not any(p in lower_name for p in exclude_patterns):
+ return True
+
+ return False
+
+
+def check_outlined_variant(name: str) -> bool:
+ """Check if an icon name indicates an outlined variant."""
+ outlined_patterns = [
+ r'[-_\s]outlined?$',
+ r'[-_\s]outline$',
+ r'[-_\s]line$',
+ r'[-_\s]stroke$',
+ r'^outline[-_\s]',
+ ]
+
+ lower_name = name.lower()
+ return any(re.search(p, lower_name) for p in outlined_patterns)
+
+
+def search_icons(icons: List[IconInfo], query: str) -> List[IconInfo]:
+ """Search icons by name."""
+ query_lower = query.lower()
+ return [
+ icon for icon in icons
+ if query_lower in icon.name.lower()
+ ]
+
+
+def filter_by_section(icons: List[IconInfo], section: str) -> List[IconInfo]:
+ """Filter icons by section name."""
+ section_lower = section.lower()
+ return [
+ icon for icon in icons
+ if section_lower in icon.section.lower()
+ ]
+
+
+def list_sections(icons: List[IconInfo]) -> List[str]:
+ """Get unique sections from icons."""
+ return sorted(set(icon.section for icon in icons if icon.section))
+
+
+def format_icon_list(icons: List[IconInfo], verbose: bool = False) -> str:
+ """Format icon list for display."""
+ lines = [] # type: List[str]
+
+ if verbose:
+ for icon in icons:
+ variant = "outlined" if icon.is_outlined else "filled"
+ lines.append(f" {icon.name}")
+ lines.append(f" Node ID: {icon.node_id}")
+ lines.append(f" Section: {icon.section}")
+ lines.append(f" Variant: {variant}")
+ lines.append("")
+ else:
+ # Group by section
+ by_section = {} # type: Dict[str, List[IconInfo]]
+ for icon in icons:
+ section = icon.section or "Uncategorized"
+ if section not in by_section:
+ by_section[section] = []
+ by_section[section].append(icon)
+
+ for section, section_icons in sorted(by_section.items()):
+ lines.append(f"\n## {section}")
+ for icon in sorted(section_icons, key=lambda x: x.name):
+ variant = " (outlined)" if icon.is_outlined else ""
+ lines.append(f" - {icon.name}{variant}")
+
+ return "\n".join(lines)
+
+
+def main() -> int:
+ """Main entry point."""
+ parser = argparse.ArgumentParser(
+ description='Browse and search Figma icon metadata'
+ )
+
+ parser.add_argument(
+ 'metadata_file',
+ help='File containing Figma metadata output'
+ )
+ parser.add_argument(
+ '--list', '-l',
+ action='store_true',
+ help='List all icons'
+ )
+ parser.add_argument(
+ '--search', '-s',
+ type=str,
+ help='Search for icons by name'
+ )
+ parser.add_argument(
+ '--section',
+ type=str,
+ help='Filter by section name'
+ )
+ parser.add_argument(
+ '--sections',
+ action='store_true',
+ help='List all sections'
+ )
+ parser.add_argument(
+ '--verbose', '-v',
+ action='store_true',
+ help='Show detailed information'
+ )
+ parser.add_argument(
+ '--json',
+ action='store_true',
+ help='Output as JSON'
+ )
+
+ args = parser.parse_args()
+
+ # Validate metadata file exists
+ metadata_path = Path(args.metadata_file)
+ if not metadata_path.exists():
+ print(f"Error: Metadata file not found: {args.metadata_file}")
+ return 1
+
+ if not metadata_path.is_file():
+ print(f"Error: Path is not a file: {args.metadata_file}")
+ return 1
+
+ # Read metadata file with error handling
+ try:
+ content = metadata_path.read_text(encoding='utf-8')
+ except UnicodeDecodeError:
+ print(f"Error: Failed to read file (encoding error): {args.metadata_file}")
+ return 1
+ except OSError as e:
+ print(f"Error: Failed to read file: {e}")
+ return 1
+
+ if not content.strip():
+ print("Error: Metadata file is empty")
+ return 1
+
+ icons = parse_figma_metadata(content)
+
+ print(f"Found {len(icons)} icons in metadata\n")
+
+ # Apply filters
+ if args.search:
+ icons = search_icons(icons, args.search)
+ print(f"Search results for '{args.search}': {len(icons)} icons\n")
+
+ if args.section:
+ icons = filter_by_section(icons, args.section)
+ print(f"Icons in section '{args.section}': {len(icons)}\n")
+
+ # Output
+ if args.sections:
+ sections = list_sections(icons)
+ print("Sections:")
+ for section in sections:
+ print(f" - {section}")
+ elif args.json:
+ output = [
+ {
+ 'name': icon.name,
+ 'node_id': icon.node_id,
+ 'section': icon.section,
+ 'is_outlined': icon.is_outlined,
+ 'path': icon.path
+ }
+ for icon in icons
+ ]
+ print(json.dumps(output, indent=2))
+ elif args.list or args.search or args.section:
+ print(format_icon_list(icons, args.verbose))
+ else:
+ # Default: show summary
+ sections = list_sections(icons)
+ print("Summary:")
+ print(f" Total icons: {len(icons)}")
+ print(f" Filled: {len([i for i in icons if not i.is_outlined])}")
+ print(f" Outlined: {len([i for i in icons if i.is_outlined])}")
+ print(f" Sections: {len(sections)}")
+ print("\nUse --list to see all icons, --search to find specific icons")
+
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/plugins/figma-icon-sync/scripts/optimize_svg.py b/plugins/figma-icon-sync/scripts/optimize_svg.py
new file mode 100644
index 0000000..9e49a3c
--- /dev/null
+++ b/plugins/figma-icon-sync/scripts/optimize_svg.py
@@ -0,0 +1,313 @@
+#!/usr/bin/env python3
+"""
+SVG Icon Optimizer
+
+Optimizes SVG icons downloaded from Figma to ensure:
+- Consistent 24x24 dimensions
+- Clean, minimal SVG code
+- Proper naming conventions
+
+Usage:
+ python optimize_svg.py [--outlined]
+ python optimize_svg.py --batch
+
+Requires Python 3.7+
+"""
+
+import re
+import sys
+import argparse
+from pathlib import Path
+from typing import List, Optional, Tuple
+
+
+def optimize_svg(svg_content: str) -> str:
+ """
+ Clean and optimize SVG content for icon usage.
+
+ Args:
+ svg_content: Raw SVG string from Figma
+
+ Returns:
+ Optimized SVG string with 24x24 dimensions
+ """
+ # Ensure viewBox is 0 0 24 24
+ if 'viewBox' in svg_content:
+ svg_content = re.sub(
+ r'viewBox="[^"]*"',
+ 'viewBox="0 0 24 24"',
+ svg_content
+ )
+ else:
+ svg_content = svg_content.replace('